Merge pull request #311 from rsnodgrass/feat/ux-system-import
feat(ui): import comprehensive UX system from beads
This commit is contained in:
@@ -6,7 +6,7 @@ require (
|
|||||||
github.com/BurntSushi/toml v1.6.0
|
github.com/BurntSushi/toml v1.6.0
|
||||||
github.com/charmbracelet/bubbles v0.21.0
|
github.com/charmbracelet/bubbles v0.21.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
|
||||||
github.com/go-rod/rod v0.116.2
|
github.com/go-rod/rod v0.116.2
|
||||||
github.com/gofrs/flock v0.13.0
|
github.com/gofrs/flock v0.13.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
@@ -16,22 +16,30 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.3.3 // indirect
|
github.com/charmbracelet/colorprofile v0.3.3 // indirect
|
||||||
|
github.com/charmbracelet/glamour v0.10.0 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.11.3 // indirect
|
github.com/charmbracelet/x/ansi v0.11.3 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
|
||||||
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.6.1 // indirect
|
github.com/clipperhouse/displaywidth v0.6.1 // indirect
|
||||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/reflow v0.3.0 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/spf13/pflag v1.0.9 // indirect
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
@@ -41,5 +49,8 @@ require (
|
|||||||
github.com/ysmood/got v0.40.0 // indirect
|
github.com/ysmood/got v0.40.0 // indirect
|
||||||
github.com/ysmood/gson v0.7.3 // indirect
|
github.com/ysmood/gson v0.7.3 // indirect
|
||||||
github.com/ysmood/leakless v0.9.0 // indirect
|
github.com/ysmood/leakless v0.9.0 // indirect
|
||||||
|
github.com/yuin/goldmark v1.7.8 // indirect
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||||
|
golang.org/x/net v0.33.0 // indirect
|
||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,23 +1,33 @@
|
|||||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||||
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
|
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
|
||||||
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
|
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
|
||||||
|
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
|
||||||
|
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
|
||||||
github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
|
github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
|
||||||
github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
|
github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||||
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
|
||||||
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
|
||||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
github.com/clipperhouse/displaywidth v0.6.1 h1:/zMlAezfDzT2xy6acHBzwIfyu2ic0hgkT83UX5EY2gY=
|
github.com/clipperhouse/displaywidth v0.6.1 h1:/zMlAezfDzT2xy6acHBzwIfyu2ic0hgkT83UX5EY2gY=
|
||||||
@@ -29,6 +39,8 @@ github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsV
|
|||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||||
|
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
|
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
|
||||||
@@ -37,6 +49,8 @@ github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
|||||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
@@ -45,16 +59,23 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||||
|
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
@@ -80,9 +101,16 @@ github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE=
|
|||||||
github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
|
github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
|
||||||
github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=
|
github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=
|
||||||
github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
|
github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
|
||||||
|
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
|
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||||
|
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/steveyegge/gastown/internal/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// colorizedHelpFunc wraps Cobra's default help with semantic coloring.
|
||||||
|
// Applies subtle accent color to group headers for visual hierarchy.
|
||||||
|
func colorizedHelpFunc(cmd *cobra.Command, args []string) {
|
||||||
|
// build full help output: Long description + Usage
|
||||||
|
var output strings.Builder
|
||||||
|
|
||||||
|
// include Long description first (like Cobra's default help)
|
||||||
|
if cmd.Long != "" {
|
||||||
|
output.WriteString(cmd.Long)
|
||||||
|
output.WriteString("\n\n")
|
||||||
|
} else if cmd.Short != "" {
|
||||||
|
output.WriteString(cmd.Short)
|
||||||
|
output.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the usage string which contains commands, flags, etc.
|
||||||
|
output.WriteString(cmd.UsageString())
|
||||||
|
|
||||||
|
// apply semantic coloring
|
||||||
|
result := colorizeHelpOutput(output.String())
|
||||||
|
fmt.Print(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// colorizeHelpOutput applies semantic colors to help text
|
||||||
|
// - Group headers get accent color for visual hierarchy
|
||||||
|
// - Section headers (Examples:, Flags:) get accent color
|
||||||
|
// - Command names get subtle styling for scanability
|
||||||
|
// - Flag names get bold styling, types get muted
|
||||||
|
// - Default values get muted styling
|
||||||
|
func colorizeHelpOutput(help string) string {
|
||||||
|
// match group header lines (e.g., "Working With Issues:")
|
||||||
|
// these are standalone lines ending with ":" and followed by commands
|
||||||
|
groupHeaderRE := regexp.MustCompile(`(?m)^([A-Z][A-Za-z &]+:)\s*$`)
|
||||||
|
|
||||||
|
result := groupHeaderRE.ReplaceAllStringFunc(help, func(match string) string {
|
||||||
|
// trim whitespace, colorize, then restore
|
||||||
|
trimmed := strings.TrimSpace(match)
|
||||||
|
return ui.RenderAccent(trimmed)
|
||||||
|
})
|
||||||
|
|
||||||
|
// match section headers in subcommand help (Examples:, Flags:, etc.)
|
||||||
|
sectionHeaderRE := regexp.MustCompile(`(?m)^(Examples|Flags|Usage|Global Flags|Aliases|Available Commands):`)
|
||||||
|
result = sectionHeaderRE.ReplaceAllStringFunc(result, func(match string) string {
|
||||||
|
return ui.RenderAccent(match)
|
||||||
|
})
|
||||||
|
|
||||||
|
// match command lines: " command Description text"
|
||||||
|
// commands are indented with 2 spaces, followed by spaces, then description
|
||||||
|
// pattern matches: indent + command-name (with hyphens) + spacing + description
|
||||||
|
cmdLineRE := regexp.MustCompile(`(?m)^( )([a-z][a-z0-9]*(?:-[a-z0-9]+)*)(\s{2,})(.*)$`)
|
||||||
|
|
||||||
|
result = cmdLineRE.ReplaceAllStringFunc(result, func(match string) string {
|
||||||
|
parts := cmdLineRE.FindStringSubmatch(match)
|
||||||
|
if len(parts) != 5 {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
indent := parts[1]
|
||||||
|
cmdName := parts[2]
|
||||||
|
spacing := parts[3]
|
||||||
|
description := parts[4]
|
||||||
|
|
||||||
|
// colorize command references in description (e.g., 'comments add')
|
||||||
|
description = colorizeCommandRefs(description)
|
||||||
|
|
||||||
|
// highlight entry point hints (e.g., "(start here)")
|
||||||
|
description = highlightEntryPoints(description)
|
||||||
|
|
||||||
|
// subtle styling on command name for scanability
|
||||||
|
return indent + ui.RenderCommand(cmdName) + spacing + description
|
||||||
|
})
|
||||||
|
|
||||||
|
// match flag lines: " -f, --file string Description"
|
||||||
|
// pattern: indent + flags + spacing + optional type + description
|
||||||
|
flagLineRE := regexp.MustCompile(`(?m)^(\s+)(-\w,\s+--[\w-]+|--[\w-]+)(\s+)(string|int|duration|bool)?(\s*.*)$`)
|
||||||
|
result = flagLineRE.ReplaceAllStringFunc(result, func(match string) string {
|
||||||
|
parts := flagLineRE.FindStringSubmatch(match)
|
||||||
|
if len(parts) < 6 {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
indent := parts[1]
|
||||||
|
flags := parts[2]
|
||||||
|
spacing := parts[3]
|
||||||
|
typeStr := parts[4]
|
||||||
|
desc := parts[5]
|
||||||
|
|
||||||
|
// mute default values in description
|
||||||
|
desc = muteDefaults(desc)
|
||||||
|
|
||||||
|
if typeStr != "" {
|
||||||
|
return indent + ui.RenderCommand(flags) + spacing + ui.RenderMuted(typeStr) + desc
|
||||||
|
}
|
||||||
|
return indent + ui.RenderCommand(flags) + spacing + desc
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// muteDefaults applies muted styling to default value annotations
|
||||||
|
func muteDefaults(text string) string {
|
||||||
|
defaultRE := regexp.MustCompile(`(\(default[^)]*\))`)
|
||||||
|
return defaultRE.ReplaceAllStringFunc(text, func(match string) string {
|
||||||
|
return ui.RenderMuted(match)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// highlightEntryPoints applies accent styling to entry point hints like "(start here)"
|
||||||
|
func highlightEntryPoints(text string) string {
|
||||||
|
entryRE := regexp.MustCompile(`(\(start here\))`)
|
||||||
|
return entryRE.ReplaceAllStringFunc(text, func(match string) string {
|
||||||
|
return ui.RenderAccent(match)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// colorizeCommandRefs applies command styling to references in text
|
||||||
|
// Matches patterns like 'command name' or 'bd command'
|
||||||
|
func colorizeCommandRefs(text string) string {
|
||||||
|
// match 'command words' in single quotes (e.g., 'comments add')
|
||||||
|
cmdRefRE := regexp.MustCompile(`'([a-z][a-z0-9 -]+)'`)
|
||||||
|
|
||||||
|
return cmdRefRE.ReplaceAllStringFunc(text, func(match string) string {
|
||||||
|
// extract the command name without quotes
|
||||||
|
inner := match[1 : len(match)-1]
|
||||||
|
return "'" + ui.RenderCommand(inner) + "'"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Set custom help function for colorized output
|
||||||
|
rootCmd.SetHelpFunc(colorizedHelpFunc)
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/steveyegge/gastown/internal/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Style definitions for thanks output using ui package colors
|
||||||
|
var (
|
||||||
|
thanksTitleStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(ui.ColorWarn)
|
||||||
|
|
||||||
|
thanksSubtitleStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(ui.ColorMuted)
|
||||||
|
|
||||||
|
thanksSectionStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(ui.ColorAccent).
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
|
thanksNameStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(ui.ColorPass)
|
||||||
|
|
||||||
|
thanksDimStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(ui.ColorMuted)
|
||||||
|
)
|
||||||
|
|
||||||
|
// thanksBoxStyle returns a bordered box style for the thanks header
|
||||||
|
func thanksBoxStyle(width int) lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.DoubleBorder()).
|
||||||
|
BorderForeground(ui.ColorMuted).
|
||||||
|
Padding(1, 4).
|
||||||
|
Width(width - 4).
|
||||||
|
Align(lipgloss.Center)
|
||||||
|
}
|
||||||
|
|
||||||
|
// gastownContributors maps HUMAN contributor names to their commit counts.
|
||||||
|
// Agent names (gastown/*, beads/*, lowercase single-word names) are excluded.
|
||||||
|
// Generated from: git shortlog -sn --all (then filtered for humans only)
|
||||||
|
var gastownContributors = map[string]int{
|
||||||
|
"Steve Yegge": 2056,
|
||||||
|
"Mike Lady": 19,
|
||||||
|
"Olivier Debeuf De Rijcker": 13,
|
||||||
|
"Danno Mayer": 11,
|
||||||
|
"Dan Shapiro": 7,
|
||||||
|
"Subhrajit Makur": 7,
|
||||||
|
"Julian Knutsen": 5,
|
||||||
|
"Darko Luketic": 4,
|
||||||
|
"Martin Emde": 4,
|
||||||
|
"Greg Hughes": 3,
|
||||||
|
"Avyukth": 2,
|
||||||
|
"Ben Kraus": 2,
|
||||||
|
"Joshua Vial": 2,
|
||||||
|
"Austin Wallace": 1,
|
||||||
|
"Cameron Palmer": 1,
|
||||||
|
"Chris Sloane": 1,
|
||||||
|
"Cong": 1,
|
||||||
|
"Dave Laird": 1,
|
||||||
|
"Dave Williams": 1,
|
||||||
|
"Jacob": 1,
|
||||||
|
"Johann Taberlet": 1,
|
||||||
|
"Joshua Samuel": 1,
|
||||||
|
"Madison Bullard": 1,
|
||||||
|
"PepijnSenders": 1,
|
||||||
|
"Raymond Weitekamp": 1,
|
||||||
|
"Sohail Mohammad": 1,
|
||||||
|
"Zachary Rosen": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
var thanksCmd = &cobra.Command{
|
||||||
|
Use: "thanks",
|
||||||
|
Short: "Thank the human contributors to Gas Town",
|
||||||
|
GroupID: GroupDiag,
|
||||||
|
Long: `Display acknowledgments to all the humans who have contributed
|
||||||
|
to the Gas Town project. This command celebrates the collaborative
|
||||||
|
effort behind the multi-agent workspace manager.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
printThanksPage()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// getContributorsSorted returns contributor names sorted by commit count descending
|
||||||
|
func getContributorsSorted() []string {
|
||||||
|
names := make([]string, 0, len(gastownContributors))
|
||||||
|
for name := range gastownContributors {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.SortFunc(names, func(a, b string) int {
|
||||||
|
// sort by commit count descending, then by name ascending for ties
|
||||||
|
countCmp := cmp.Compare(gastownContributors[b], gastownContributors[a])
|
||||||
|
if countCmp != 0 {
|
||||||
|
return countCmp
|
||||||
|
}
|
||||||
|
return cmp.Compare(a, b)
|
||||||
|
})
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// printThanksPage renders the complete thanks page
|
||||||
|
func printThanksPage() {
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// get sorted contributors, split into featured (top 20) and rest
|
||||||
|
sorted := getContributorsSorted()
|
||||||
|
featuredCount := 20
|
||||||
|
if len(sorted) < featuredCount {
|
||||||
|
featuredCount = len(sorted)
|
||||||
|
}
|
||||||
|
featured := sorted[:featuredCount]
|
||||||
|
additional := sorted[featuredCount:]
|
||||||
|
|
||||||
|
// calculate content width based on 4 columns
|
||||||
|
cols := 4
|
||||||
|
contentWidth := calculateColumnsWidth(featured, cols)
|
||||||
|
if contentWidth < 60 {
|
||||||
|
contentWidth = 60
|
||||||
|
}
|
||||||
|
|
||||||
|
// build header content
|
||||||
|
title := thanksTitleStyle.Render("THANK YOU!")
|
||||||
|
subtitle := thanksSubtitleStyle.Render("To all the humans who contributed to Gas Town")
|
||||||
|
headerContent := title + "\n\n" + subtitle
|
||||||
|
|
||||||
|
// render header in bordered box
|
||||||
|
header := thanksBoxStyle(contentWidth).Render(headerContent)
|
||||||
|
fmt.Println(header)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// print featured contributors section
|
||||||
|
fmt.Println(thanksSectionStyle.Render(" Featured Contributors"))
|
||||||
|
fmt.Println()
|
||||||
|
printThanksColumns(featured, cols)
|
||||||
|
|
||||||
|
// print additional contributors if any
|
||||||
|
if len(additional) > 0 {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(thanksSectionStyle.Render(" Additional Contributors"))
|
||||||
|
fmt.Println()
|
||||||
|
printThanksWrappedList("", additional, contentWidth)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateColumnsWidth determines the width needed for n columns of names
|
||||||
|
func calculateColumnsWidth(names []string, cols int) int {
|
||||||
|
maxWidth := 0
|
||||||
|
for _, name := range names {
|
||||||
|
if len(name) > maxWidth {
|
||||||
|
maxWidth = len(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cap at 20 characters per column
|
||||||
|
if maxWidth > 20 {
|
||||||
|
maxWidth = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
// add padding between columns
|
||||||
|
colWidth := maxWidth + 2
|
||||||
|
|
||||||
|
return colWidth * cols
|
||||||
|
}
|
||||||
|
|
||||||
|
// printThanksColumns prints names in n columns, reading left-to-right
|
||||||
|
func printThanksColumns(names []string, cols int) {
|
||||||
|
if len(names) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// find max width for alignment
|
||||||
|
maxWidth := 0
|
||||||
|
for _, name := range names {
|
||||||
|
if len(name) > maxWidth {
|
||||||
|
maxWidth = len(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if maxWidth > 20 {
|
||||||
|
maxWidth = 20
|
||||||
|
}
|
||||||
|
colWidth := maxWidth + 2
|
||||||
|
|
||||||
|
// print in rows, reading left to right (matches bd thanks)
|
||||||
|
for i := 0; i < len(names); i += cols {
|
||||||
|
fmt.Print(" ")
|
||||||
|
for j := 0; j < cols && i+j < len(names); j++ {
|
||||||
|
name := names[i+j]
|
||||||
|
if len(name) > 20 {
|
||||||
|
name = name[:17] + "..."
|
||||||
|
}
|
||||||
|
// pad BEFORE styling to avoid ANSI code width issues
|
||||||
|
padded := fmt.Sprintf("%-*s", colWidth, name)
|
||||||
|
fmt.Print(thanksNameStyle.Render(padded))
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// printThanksWrappedList prints a comma-separated list with word wrapping
|
||||||
|
func printThanksWrappedList(label string, names []string, maxWidth int) {
|
||||||
|
indent := " "
|
||||||
|
|
||||||
|
fmt.Print(indent)
|
||||||
|
lineLen := len(indent)
|
||||||
|
|
||||||
|
if label != "" {
|
||||||
|
fmt.Print(thanksSectionStyle.Render(label) + " ")
|
||||||
|
lineLen += len(label) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, name := range names {
|
||||||
|
suffix := ", "
|
||||||
|
if i == len(names)-1 {
|
||||||
|
suffix = ""
|
||||||
|
}
|
||||||
|
entry := name + suffix
|
||||||
|
|
||||||
|
if lineLen+len(entry) > maxWidth && lineLen > len(indent) {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Print(indent)
|
||||||
|
lineLen = len(indent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print(thanksDimStyle.Render(entry))
|
||||||
|
lineLen += len(entry)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(thanksCmd)
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ func NewAgentBeadsCheck() *AgentBeadsCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "agent-beads-exist",
|
CheckName: "agent-beads-exist",
|
||||||
CheckDescription: "Verify agent beads exist for all agents",
|
CheckDescription: "Verify agent beads exist for all agents",
|
||||||
|
CheckCategory: CategoryRig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func NewBdDaemonCheck() *BdDaemonCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "bd-daemon",
|
CheckName: "bd-daemon",
|
||||||
CheckDescription: "Check if bd (beads) daemon is running",
|
CheckDescription: "Check if bd (beads) daemon is running",
|
||||||
|
CheckCategory: CategoryInfrastructure,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ func NewBeadsDatabaseCheck() *BeadsDatabaseCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "beads-database",
|
CheckName: "beads-database",
|
||||||
CheckDescription: "Verify beads database is properly initialized",
|
CheckDescription: "Verify beads database is properly initialized",
|
||||||
|
CheckCategory: CategoryConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -176,6 +177,7 @@ func NewPrefixConflictCheck() *PrefixConflictCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "prefix-conflict",
|
CheckName: "prefix-conflict",
|
||||||
CheckDescription: "Check for duplicate beads prefixes across rigs",
|
CheckDescription: "Check for duplicate beads prefixes across rigs",
|
||||||
|
CheckCategory: CategoryConfig,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,6 +245,7 @@ func NewPrefixMismatchCheck() *PrefixMismatchCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "prefix-mismatch",
|
CheckName: "prefix-mismatch",
|
||||||
CheckDescription: "Check for prefix mismatches between rigs.json and routes.jsonl",
|
CheckDescription: "Check for prefix mismatches between rigs.json and routes.jsonl",
|
||||||
|
CheckCategory: CategoryConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func NewBootHealthCheck() *BootHealthCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "boot-health",
|
CheckName: "boot-health",
|
||||||
CheckDescription: "Check Boot watchdog health (the vet checks on the dog)",
|
CheckDescription: "Check Boot watchdog health (the vet checks on the dog)",
|
||||||
|
CheckCategory: CategoryInfrastructure,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ func NewBranchCheck() *BranchCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "persistent-role-branches",
|
CheckName: "persistent-role-branches",
|
||||||
CheckDescription: "Detect persistent roles not on main branch",
|
CheckDescription: "Detect persistent roles not on main branch",
|
||||||
|
CheckCategory: CategoryCleanup,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -213,6 +214,7 @@ func NewBeadsSyncOrphanCheck() *BeadsSyncOrphanCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "beads-sync-orphans",
|
CheckName: "beads-sync-orphans",
|
||||||
CheckDescription: "Detect orphaned code on beads-sync branch",
|
CheckDescription: "Detect orphaned code on beads-sync branch",
|
||||||
|
CheckCategory: CategoryCleanup,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -338,6 +340,7 @@ func NewCloneDivergenceCheck() *CloneDivergenceCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "clone-divergence",
|
CheckName: "clone-divergence",
|
||||||
CheckDescription: "Detect emergency divergence between git clones",
|
CheckDescription: "Detect emergency divergence between git clones",
|
||||||
|
CheckCategory: CategoryCleanup,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ func NewClaudeSettingsCheck() *ClaudeSettingsCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "claude-settings",
|
CheckName: "claude-settings",
|
||||||
CheckDescription: "Verify Claude settings.json files match expected templates",
|
CheckDescription: "Verify Claude settings.json files match expected templates",
|
||||||
|
CheckCategory: CategoryConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ func NewCommandsCheck() *CommandsCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "commands-provisioned",
|
CheckName: "commands-provisioned",
|
||||||
CheckDescription: "Check .claude/commands/ is provisioned at town level",
|
CheckDescription: "Check .claude/commands/ is provisioned at town level",
|
||||||
|
CheckCategory: CategoryConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ func NewSettingsCheck() *SettingsCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "rig-settings",
|
CheckName: "rig-settings",
|
||||||
CheckDescription: "Check that rigs have settings/ directory",
|
CheckDescription: "Check that rigs have settings/ directory",
|
||||||
|
CheckCategory: CategoryConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -105,6 +106,7 @@ func NewRuntimeGitignoreCheck() *RuntimeGitignoreCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "runtime-gitignore",
|
CheckName: "runtime-gitignore",
|
||||||
CheckDescription: "Check that .runtime/ directories are gitignored",
|
CheckDescription: "Check that .runtime/ directories are gitignored",
|
||||||
|
CheckCategory: CategoryConfig,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,6 +196,7 @@ func NewLegacyGastownCheck() *LegacyGastownCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "legacy-gastown",
|
CheckName: "legacy-gastown",
|
||||||
CheckDescription: "Check for old .gastown/ directories that should be migrated",
|
CheckDescription: "Check for old .gastown/ directories that should be migrated",
|
||||||
|
CheckCategory: CategoryConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -281,6 +284,7 @@ func NewSessionHookCheck() *SessionHookCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "session-hooks",
|
CheckName: "session-hooks",
|
||||||
CheckDescription: "Check that settings.json hooks use session-start.sh or --hook flag",
|
CheckDescription: "Check that settings.json hooks use session-start.sh or --hook flag",
|
||||||
|
CheckCategory: CategoryConfig,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -549,6 +553,7 @@ func NewCustomTypesCheck() *CustomTypesCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "beads-custom-types",
|
CheckName: "beads-custom-types",
|
||||||
CheckDescription: "Check that Gas Town custom types are registered with beads",
|
CheckDescription: "Check that Gas Town custom types are registered with beads",
|
||||||
|
CheckCategory: CategoryConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ func NewCrashReportCheck() *CrashReportCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "crash-reports",
|
CheckName: "crash-reports",
|
||||||
CheckDescription: "Check for recent macOS crash reports (tmux, Claude)",
|
CheckDescription: "Check for recent macOS crash reports (tmux, Claude)",
|
||||||
|
CheckCategory: CategoryCleanup,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ func NewCrewStateCheck() *CrewStateCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "crew-state",
|
CheckName: "crew-state",
|
||||||
CheckDescription: "Validate crew worker state.json files",
|
CheckDescription: "Validate crew worker state.json files",
|
||||||
|
CheckCategory: CategoryCleanup,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -239,6 +240,7 @@ func NewCrewWorktreeCheck() *CrewWorktreeCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "crew-worktrees",
|
CheckName: "crew-worktrees",
|
||||||
CheckDescription: "Detect stale cross-rig worktrees in crew directories",
|
CheckDescription: "Detect stale cross-rig worktrees in crew directories",
|
||||||
|
CheckCategory: CategoryCleanup,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func NewDaemonCheck() *DaemonCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "daemon",
|
CheckName: "daemon",
|
||||||
CheckDescription: "Check if Gas Town daemon is running",
|
CheckDescription: "Check if Gas Town daemon is running",
|
||||||
|
CheckCategory: CategoryInfrastructure,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ func (d *Doctor) Checks() []Check {
|
|||||||
return d.checks
|
return d.checks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// categoryGetter interface for checks that provide a category
|
||||||
|
type categoryGetter interface {
|
||||||
|
Category() string
|
||||||
|
}
|
||||||
|
|
||||||
// Run executes all registered checks and returns a report.
|
// Run executes all registered checks and returns a report.
|
||||||
func (d *Doctor) Run(ctx *CheckContext) *Report {
|
func (d *Doctor) Run(ctx *CheckContext) *Report {
|
||||||
report := NewReport()
|
report := NewReport()
|
||||||
@@ -37,6 +42,10 @@ func (d *Doctor) Run(ctx *CheckContext) *Report {
|
|||||||
if result.Name == "" {
|
if result.Name == "" {
|
||||||
result.Name = check.Name()
|
result.Name = check.Name()
|
||||||
}
|
}
|
||||||
|
// Set category from check if available
|
||||||
|
if cg, ok := check.(categoryGetter); ok && result.Category == "" {
|
||||||
|
result.Category = cg.Category()
|
||||||
|
}
|
||||||
report.Add(result)
|
report.Add(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +62,10 @@ func (d *Doctor) Fix(ctx *CheckContext) *Report {
|
|||||||
if result.Name == "" {
|
if result.Name == "" {
|
||||||
result.Name = check.Name()
|
result.Name = check.Name()
|
||||||
}
|
}
|
||||||
|
// Set category from check if available
|
||||||
|
if cg, ok := check.(categoryGetter); ok && result.Category == "" {
|
||||||
|
result.Category = cg.Category()
|
||||||
|
}
|
||||||
|
|
||||||
// Attempt fix if check failed and is fixable
|
// Attempt fix if check failed and is fixable
|
||||||
if result.Status != StatusOK && check.CanFix() {
|
if result.Status != StatusOK && check.CanFix() {
|
||||||
@@ -63,6 +76,10 @@ func (d *Doctor) Fix(ctx *CheckContext) *Report {
|
|||||||
if result.Name == "" {
|
if result.Name == "" {
|
||||||
result.Name = check.Name()
|
result.Name = check.Name()
|
||||||
}
|
}
|
||||||
|
// Set category again after re-run
|
||||||
|
if cg, ok := check.(categoryGetter); ok && result.Category == "" {
|
||||||
|
result.Category = cg.Category()
|
||||||
|
}
|
||||||
// Update message to indicate fix was applied
|
// Update message to indicate fix was applied
|
||||||
if result.Status == StatusOK {
|
if result.Status == StatusOK {
|
||||||
result.Message = result.Message + " (fixed)"
|
result.Message = result.Message + " (fixed)"
|
||||||
@@ -84,6 +101,12 @@ func (d *Doctor) Fix(ctx *CheckContext) *Report {
|
|||||||
type BaseCheck struct {
|
type BaseCheck struct {
|
||||||
CheckName string
|
CheckName string
|
||||||
CheckDescription string
|
CheckDescription string
|
||||||
|
CheckCategory string // Category for grouping (e.g., CategoryCore)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category returns the check's category for grouping in output.
|
||||||
|
func (b *BaseCheck) Category() string {
|
||||||
|
return b.CheckCategory
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name returns the check name.
|
// Name returns the check name.
|
||||||
|
|||||||
@@ -219,8 +219,12 @@ func TestReport_Print(t *testing.T) {
|
|||||||
if !bytes.Contains(buf.Bytes(), []byte("TestCheck")) {
|
if !bytes.Contains(buf.Bytes(), []byte("TestCheck")) {
|
||||||
t.Error("Output should contain check name")
|
t.Error("Output should contain check name")
|
||||||
}
|
}
|
||||||
if !bytes.Contains(buf.Bytes(), []byte("2 checks")) {
|
// New summary format: "✓ N passed ⚠ N warnings ✖ N failed"
|
||||||
t.Error("Output should contain summary")
|
if !bytes.Contains(buf.Bytes(), []byte("1 passed")) {
|
||||||
|
t.Error("Output should contain summary with passed count")
|
||||||
|
}
|
||||||
|
if !bytes.Contains(buf.Bytes(), []byte("1 warnings")) {
|
||||||
|
t.Error("Output should contain summary with warnings count")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ func NewEnvVarsCheck() *EnvVarsCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "env-vars",
|
CheckName: "env-vars",
|
||||||
CheckDescription: "Verify tmux session environment variables match expected values",
|
CheckDescription: "Verify tmux session environment variables match expected values",
|
||||||
|
CheckCategory: CategoryConfig,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ func NewFormulaCheck() *FormulaCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "formulas",
|
CheckName: "formulas",
|
||||||
CheckDescription: "Check embedded formulas are up-to-date",
|
CheckDescription: "Check embedded formulas are up-to-date",
|
||||||
|
CheckCategory: CategoryConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ func NewGlobalStateCheck() *GlobalStateCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "global-state",
|
CheckName: "global-state",
|
||||||
CheckDescription: "Validates Gas Town global state and shell integration",
|
CheckDescription: "Validates Gas Town global state and shell integration",
|
||||||
|
CheckCategory: CategoryCore,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ func NewHookAttachmentValidCheck() *HookAttachmentValidCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "hook-attachment-valid",
|
CheckName: "hook-attachment-valid",
|
||||||
CheckDescription: "Verify attached molecules exist and are not closed",
|
CheckDescription: "Verify attached molecules exist and are not closed",
|
||||||
|
CheckCategory: CategoryHooks,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -207,6 +208,7 @@ func NewHookSingletonCheck() *HookSingletonCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "hook-singleton",
|
CheckName: "hook-singleton",
|
||||||
CheckDescription: "Ensure each agent has at most one handoff bead",
|
CheckDescription: "Ensure each agent has at most one handoff bead",
|
||||||
|
CheckCategory: CategoryHooks,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -346,6 +348,7 @@ func NewOrphanedAttachmentsCheck() *OrphanedAttachmentsCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "orphaned-attachments",
|
CheckName: "orphaned-attachments",
|
||||||
CheckDescription: "Detect handoff beads for non-existent agents",
|
CheckDescription: "Detect handoff beads for non-existent agents",
|
||||||
|
CheckCategory: CategoryHooks,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// IdentityCollisionCheck checks for agent identity collisions and stale locks.
|
// IdentityCollisionCheck checks for agent identity collisions and stale locks.
|
||||||
type IdentityCollisionCheck struct{}
|
type IdentityCollisionCheck struct {
|
||||||
|
BaseCheck
|
||||||
|
}
|
||||||
|
|
||||||
// NewIdentityCollisionCheck creates a new identity collision check.
|
// NewIdentityCollisionCheck creates a new identity collision check.
|
||||||
func NewIdentityCollisionCheck() *IdentityCollisionCheck {
|
func NewIdentityCollisionCheck() *IdentityCollisionCheck {
|
||||||
return &IdentityCollisionCheck{}
|
return &IdentityCollisionCheck{
|
||||||
}
|
BaseCheck: BaseCheck{
|
||||||
|
CheckName: "identity-collision",
|
||||||
func (c *IdentityCollisionCheck) Name() string {
|
CheckDescription: "Check for agent identity collisions and stale locks",
|
||||||
return "identity-collision"
|
CheckCategory: CategoryInfrastructure,
|
||||||
}
|
},
|
||||||
|
}
|
||||||
func (c *IdentityCollisionCheck) Description() string {
|
|
||||||
return "Check for agent identity collisions and stale locks"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *IdentityCollisionCheck) CanFix() bool {
|
func (c *IdentityCollisionCheck) CanFix() bool {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func NewLifecycleHygieneCheck() *LifecycleHygieneCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "lifecycle-hygiene",
|
CheckName: "lifecycle-hygiene",
|
||||||
CheckDescription: "Check for stale lifecycle messages",
|
CheckDescription: "Check for stale lifecycle messages",
|
||||||
|
CheckCategory: CategoryConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func NewOrphanSessionCheck() *OrphanSessionCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "orphan-sessions",
|
CheckName: "orphan-sessions",
|
||||||
CheckDescription: "Detect orphaned tmux sessions",
|
CheckDescription: "Detect orphaned tmux sessions",
|
||||||
|
CheckCategory: CategoryCleanup,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -244,6 +245,7 @@ func NewOrphanProcessCheck() *OrphanProcessCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "orphan-processes",
|
CheckName: "orphan-processes",
|
||||||
CheckDescription: "Detect runtime processes outside tmux",
|
CheckDescription: "Detect runtime processes outside tmux",
|
||||||
|
CheckCategory: CategoryCleanup,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func NewPatrolMoleculesExistCheck() *PatrolMoleculesExistCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "patrol-molecules-exist",
|
CheckName: "patrol-molecules-exist",
|
||||||
CheckDescription: "Check if patrol molecules exist for each rig",
|
CheckDescription: "Check if patrol molecules exist for each rig",
|
||||||
|
CheckCategory: CategoryPatrol,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -155,6 +156,7 @@ func NewPatrolHooksWiredCheck() *PatrolHooksWiredCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "patrol-hooks-wired",
|
CheckName: "patrol-hooks-wired",
|
||||||
CheckDescription: "Check if hooks trigger patrol execution",
|
CheckDescription: "Check if hooks trigger patrol execution",
|
||||||
|
CheckCategory: CategoryPatrol,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -229,6 +231,7 @@ func NewPatrolNotStuckCheck() *PatrolNotStuckCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "patrol-not-stuck",
|
CheckName: "patrol-not-stuck",
|
||||||
CheckDescription: "Check for stuck patrol wisps (>1h in_progress)",
|
CheckDescription: "Check for stuck patrol wisps (>1h in_progress)",
|
||||||
|
CheckCategory: CategoryPatrol,
|
||||||
},
|
},
|
||||||
stuckThreshold: DefaultStuckThreshold,
|
stuckThreshold: DefaultStuckThreshold,
|
||||||
}
|
}
|
||||||
@@ -351,6 +354,7 @@ func NewPatrolPluginsAccessibleCheck() *PatrolPluginsAccessibleCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "patrol-plugins-accessible",
|
CheckName: "patrol-plugins-accessible",
|
||||||
CheckDescription: "Check if plugin directories exist and are readable",
|
CheckDescription: "Check if plugin directories exist and are readable",
|
||||||
|
CheckCategory: CategoryPatrol,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -420,6 +424,7 @@ func NewPatrolRolesHavePromptsCheck() *PatrolRolesHavePromptsCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "patrol-roles-have-prompts",
|
CheckName: "patrol-roles-have-prompts",
|
||||||
CheckDescription: "Check if internal/templates/roles/*.md.tmpl exist for each patrol role",
|
CheckDescription: "Check if internal/templates/roles/*.md.tmpl exist for each patrol role",
|
||||||
|
CheckCategory: CategoryPatrol,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ func NewPreCheckoutHookCheck() *PreCheckoutHookCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "pre-checkout-hook",
|
CheckName: "pre-checkout-hook",
|
||||||
CheckDescription: "Verify pre-checkout hook prevents branch switches",
|
CheckDescription: "Verify pre-checkout hook prevents branch switches",
|
||||||
|
CheckCategory: CategoryHooks,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ func NewRepoFingerprintCheck() *RepoFingerprintCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "repo-fingerprint",
|
CheckName: "repo-fingerprint",
|
||||||
CheckDescription: "Verify beads database has valid repository fingerprint",
|
CheckDescription: "Verify beads database has valid repository fingerprint",
|
||||||
|
CheckCategory: CategoryInfrastructure,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ func NewRigBeadsCheck() *RigBeadsCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "rig-beads-exist",
|
CheckName: "rig-beads-exist",
|
||||||
CheckDescription: "Verify rig identity beads exist for all rigs",
|
CheckDescription: "Verify rig identity beads exist for all rigs",
|
||||||
|
CheckCategory: CategoryRig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ func NewRigIsGitRepoCheck() *RigIsGitRepoCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "rig-is-git-repo",
|
CheckName: "rig-is-git-repo",
|
||||||
CheckDescription: "Verify rig has a valid mayor/rig git clone",
|
CheckDescription: "Verify rig has a valid mayor/rig git clone",
|
||||||
|
CheckCategory: CategoryRig,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,6 +100,7 @@ func NewGitExcludeConfiguredCheck() *GitExcludeConfiguredCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "git-exclude-configured",
|
CheckName: "git-exclude-configured",
|
||||||
CheckDescription: "Check .git/info/exclude has Gas Town directories",
|
CheckDescription: "Check .git/info/exclude has Gas Town directories",
|
||||||
|
CheckCategory: CategoryRig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -249,6 +251,7 @@ func NewHooksPathConfiguredCheck() *HooksPathConfiguredCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "hooks-path-configured",
|
CheckName: "hooks-path-configured",
|
||||||
CheckDescription: "Check core.hooksPath is set for all clones",
|
CheckDescription: "Check core.hooksPath is set for all clones",
|
||||||
|
CheckCategory: CategoryRig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -371,6 +374,7 @@ func NewWitnessExistsCheck() *WitnessExistsCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "witness-exists",
|
CheckName: "witness-exists",
|
||||||
CheckDescription: "Verify witness/ directory structure exists",
|
CheckDescription: "Verify witness/ directory structure exists",
|
||||||
|
CheckCategory: CategoryRig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -477,6 +481,7 @@ func NewRefineryExistsCheck() *RefineryExistsCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "refinery-exists",
|
CheckName: "refinery-exists",
|
||||||
CheckDescription: "Verify refinery/ directory structure exists",
|
CheckDescription: "Verify refinery/ directory structure exists",
|
||||||
|
CheckCategory: CategoryRig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -582,6 +587,7 @@ func NewMayorCloneExistsCheck() *MayorCloneExistsCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "mayor-clone-exists",
|
CheckName: "mayor-clone-exists",
|
||||||
CheckDescription: "Verify mayor/rig/ git clone exists",
|
CheckDescription: "Verify mayor/rig/ git clone exists",
|
||||||
|
CheckCategory: CategoryRig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -664,6 +670,7 @@ func NewPolecatClonesValidCheck() *PolecatClonesValidCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "polecat-clones-valid",
|
CheckName: "polecat-clones-valid",
|
||||||
CheckDescription: "Verify polecat directories are valid git clones",
|
CheckDescription: "Verify polecat directories are valid git clones",
|
||||||
|
CheckCategory: CategoryRig,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -798,6 +805,7 @@ func NewBeadsConfigValidCheck() *BeadsConfigValidCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "beads-config-valid",
|
CheckName: "beads-config-valid",
|
||||||
CheckDescription: "Verify beads configuration if .beads/ exists",
|
CheckDescription: "Verify beads configuration if .beads/ exists",
|
||||||
|
CheckCategory: CategoryRig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -893,6 +901,7 @@ func NewBeadsRedirectCheck() *BeadsRedirectCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "beads-redirect",
|
CheckName: "beads-redirect",
|
||||||
CheckDescription: "Verify rig-level beads redirect for tracked beads",
|
CheckDescription: "Verify rig-level beads redirect for tracked beads",
|
||||||
|
CheckCategory: CategoryRig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1105,6 +1114,7 @@ func NewBareRepoRefspecCheck() *BareRepoRefspecCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "bare-repo-refspec",
|
CheckName: "bare-repo-refspec",
|
||||||
CheckDescription: "Verify bare repo has correct refspec for worktrees",
|
CheckDescription: "Verify bare repo has correct refspec for worktrees",
|
||||||
|
CheckCategory: CategoryRig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ func NewRoutesCheck() *RoutesCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "routes-config",
|
CheckName: "routes-config",
|
||||||
CheckDescription: "Check beads routing configuration",
|
CheckDescription: "Check beads routing configuration",
|
||||||
|
CheckCategory: CategoryConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ func NewSparseCheckoutCheck() *SparseCheckoutCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "sparse-checkout",
|
CheckName: "sparse-checkout",
|
||||||
CheckDescription: "Verify sparse checkout excludes Claude context files (.claude/, CLAUDE.md, etc.)",
|
CheckDescription: "Verify sparse checkout excludes Claude context files (.claude/, CLAUDE.md, etc.)",
|
||||||
|
CheckCategory: CategoryRig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ func NewStaleBinaryCheck() *StaleBinaryCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "stale-binary",
|
CheckName: "stale-binary",
|
||||||
CheckDescription: "Check if gt binary is up to date with repo",
|
CheckDescription: "Check if gt binary is up to date with repo",
|
||||||
|
CheckCategory: CategoryInfrastructure,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func NewThemeCheck() *ThemeCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "themes",
|
CheckName: "themes",
|
||||||
CheckDescription: "Check tmux session theme configuration",
|
CheckDescription: "Check tmux session theme configuration",
|
||||||
|
CheckCategory: CategoryConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ func NewLinkedPaneCheck() *LinkedPaneCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "linked-panes",
|
CheckName: "linked-panes",
|
||||||
CheckDescription: "Detect tmux sessions sharing panes (causes crosstalk)",
|
CheckDescription: "Detect tmux sessions sharing panes (causes crosstalk)",
|
||||||
|
CheckCategory: CategoryInfrastructure,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func NewTownGitCheck() *TownGitCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "town-git",
|
CheckName: "town-git",
|
||||||
CheckDescription: "Verify town root is under version control",
|
CheckDescription: "Verify town root is under version control",
|
||||||
|
CheckCategory: CategoryCore,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ func NewTownRootBranchCheck() *TownRootBranchCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "town-root-branch",
|
CheckName: "town-root-branch",
|
||||||
CheckDescription: "Verify town root is on main branch",
|
CheckDescription: "Verify town root is on main branch",
|
||||||
|
CheckCategory: CategoryCore,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+136
-40
@@ -4,12 +4,34 @@ package doctor
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveyegge/gastown/internal/style"
|
"github.com/steveyegge/gastown/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Category constants for grouping checks
|
||||||
|
const (
|
||||||
|
CategoryCore = "Core"
|
||||||
|
CategoryInfrastructure = "Infrastructure"
|
||||||
|
CategoryRig = "Rig"
|
||||||
|
CategoryPatrol = "Patrol"
|
||||||
|
CategoryConfig = "Configuration"
|
||||||
|
CategoryCleanup = "Cleanup"
|
||||||
|
CategoryHooks = "Hooks"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CategoryOrder defines the display order for categories
|
||||||
|
var CategoryOrder = []string{
|
||||||
|
CategoryCore,
|
||||||
|
CategoryInfrastructure,
|
||||||
|
CategoryRig,
|
||||||
|
CategoryPatrol,
|
||||||
|
CategoryConfig,
|
||||||
|
CategoryCleanup,
|
||||||
|
CategoryHooks,
|
||||||
|
}
|
||||||
|
|
||||||
// CheckStatus represents the result status of a health check.
|
// CheckStatus represents the result status of a health check.
|
||||||
type CheckStatus int
|
type CheckStatus int
|
||||||
|
|
||||||
@@ -55,11 +77,12 @@ func (ctx *CheckContext) RigPath() string {
|
|||||||
|
|
||||||
// CheckResult represents the outcome of a health check.
|
// CheckResult represents the outcome of a health check.
|
||||||
type CheckResult struct {
|
type CheckResult struct {
|
||||||
Name string // Check name
|
Name string // Check name
|
||||||
Status CheckStatus // Result status
|
Status CheckStatus // Result status
|
||||||
Message string // Primary result message
|
Message string // Primary result message
|
||||||
Details []string // Additional information
|
Details []string // Additional information
|
||||||
FixHint string // Suggestion if not auto-fixable
|
FixHint string // Suggestion if not auto-fixable
|
||||||
|
Category string // Category for grouping (e.g., CategoryCore)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check defines the interface for a health check.
|
// Check defines the interface for a health check.
|
||||||
@@ -135,59 +158,132 @@ func (r *Report) IsHealthy() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Print outputs the report to the given writer.
|
// Print outputs the report to the given writer.
|
||||||
|
// Matches bd doctor UX: grouped by category, semantic icons, warnings section.
|
||||||
func (r *Report) Print(w io.Writer, verbose bool) {
|
func (r *Report) Print(w io.Writer, verbose bool) {
|
||||||
// Print individual check results
|
// Print header with version placeholder (caller should set via PrintWithVersion)
|
||||||
|
_, _ = fmt.Fprintln(w)
|
||||||
|
|
||||||
|
// Group checks by category
|
||||||
|
checksByCategory := make(map[string][]*CheckResult)
|
||||||
for _, check := range r.Checks {
|
for _, check := range r.Checks {
|
||||||
r.printCheck(w, check, verbose)
|
cat := check.Category
|
||||||
|
if cat == "" {
|
||||||
|
cat = "Other"
|
||||||
|
}
|
||||||
|
checksByCategory[cat] = append(checksByCategory[cat], check)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print summary (output errors non-actionable)
|
// Track warnings/errors for summary section
|
||||||
_, _ = fmt.Fprintln(w)
|
var warnings []*CheckResult
|
||||||
|
|
||||||
|
// Print checks by category in defined order
|
||||||
|
for _, category := range CategoryOrder {
|
||||||
|
checks, exists := checksByCategory[category]
|
||||||
|
if !exists || len(checks) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print category header
|
||||||
|
_, _ = fmt.Fprintln(w, ui.RenderCategory(category))
|
||||||
|
|
||||||
|
// Print each check in this category
|
||||||
|
for _, check := range checks {
|
||||||
|
r.printCheck(w, check, verbose)
|
||||||
|
if check.Status != StatusOK {
|
||||||
|
warnings = append(warnings, check)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintln(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print any checks without a category
|
||||||
|
if otherChecks, exists := checksByCategory["Other"]; exists && len(otherChecks) > 0 {
|
||||||
|
_, _ = fmt.Fprintln(w, ui.RenderCategory("Other"))
|
||||||
|
for _, check := range otherChecks {
|
||||||
|
r.printCheck(w, check, verbose)
|
||||||
|
if check.Status != StatusOK {
|
||||||
|
warnings = append(warnings, check)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintln(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print separator and summary
|
||||||
|
_, _ = fmt.Fprintln(w, ui.RenderSeparator())
|
||||||
r.printSummary(w)
|
r.printSummary(w)
|
||||||
|
|
||||||
|
// Print warnings/errors section with fixes
|
||||||
|
r.printWarningsSection(w, warnings)
|
||||||
}
|
}
|
||||||
|
|
||||||
// printCheck outputs a single check result (output errors non-actionable).
|
// printCheck outputs a single check result with semantic styling.
|
||||||
func (r *Report) printCheck(w io.Writer, check *CheckResult, verbose bool) {
|
func (r *Report) printCheck(w io.Writer, check *CheckResult, verbose bool) {
|
||||||
var prefix string
|
var statusIcon string
|
||||||
switch check.Status {
|
switch check.Status {
|
||||||
case StatusOK:
|
case StatusOK:
|
||||||
prefix = style.SuccessPrefix
|
statusIcon = ui.RenderPassIcon()
|
||||||
case StatusWarning:
|
case StatusWarning:
|
||||||
prefix = style.WarningPrefix
|
statusIcon = ui.RenderWarnIcon()
|
||||||
case StatusError:
|
case StatusError:
|
||||||
prefix = style.ErrorPrefix
|
statusIcon = ui.RenderFailIcon()
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _ = fmt.Fprintf(w, "%s %s: %s\n", prefix, check.Name, check.Message)
|
// Print check line: icon + name + muted message
|
||||||
|
_, _ = fmt.Fprintf(w, " %s %s", statusIcon, check.Name)
|
||||||
|
if check.Message != "" {
|
||||||
|
_, _ = fmt.Fprintf(w, "%s", ui.RenderMuted(" "+check.Message))
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintln(w)
|
||||||
|
|
||||||
// Print details in verbose mode or for non-OK results
|
// Print details in verbose mode or for non-OK results (with tree connector)
|
||||||
if len(check.Details) > 0 && (verbose || check.Status != StatusOK) {
|
if len(check.Details) > 0 && (verbose || check.Status != StatusOK) {
|
||||||
for _, detail := range check.Details {
|
for _, detail := range check.Details {
|
||||||
_, _ = fmt.Fprintf(w, " %s\n", detail)
|
_, _ = fmt.Fprintf(w, " %s%s\n", ui.MutedStyle.Render(ui.TreeLast), ui.RenderMuted(detail))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print fix hint for errors/warnings
|
|
||||||
if check.FixHint != "" && check.Status != StatusOK {
|
|
||||||
_, _ = fmt.Fprintf(w, " %s %s\n", style.ArrowPrefix, check.FixHint)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// printSummary outputs the summary line (output errors non-actionable).
|
// printSummary outputs the summary line with semantic icons.
|
||||||
func (r *Report) printSummary(w io.Writer) {
|
func (r *Report) printSummary(w io.Writer) {
|
||||||
parts := []string{
|
summary := fmt.Sprintf("%s %d passed %s %d warnings %s %d failed",
|
||||||
fmt.Sprintf("%d checks", r.Summary.Total),
|
ui.RenderPassIcon(), r.Summary.OK,
|
||||||
}
|
ui.RenderWarnIcon(), r.Summary.Warnings,
|
||||||
|
ui.RenderFailIcon(), r.Summary.Errors,
|
||||||
if r.Summary.OK > 0 {
|
)
|
||||||
parts = append(parts, style.Success.Render(fmt.Sprintf("%d passed", r.Summary.OK)))
|
_, _ = fmt.Fprintln(w, summary)
|
||||||
}
|
}
|
||||||
if r.Summary.Warnings > 0 {
|
|
||||||
parts = append(parts, style.Warning.Render(fmt.Sprintf("%d warnings", r.Summary.Warnings)))
|
// printWarningsSection outputs numbered warnings/errors sorted by severity.
|
||||||
}
|
func (r *Report) printWarningsSection(w io.Writer, warnings []*CheckResult) {
|
||||||
if r.Summary.Errors > 0 {
|
if len(warnings) == 0 {
|
||||||
parts = append(parts, style.Error.Render(fmt.Sprintf("%d errors", r.Summary.Errors)))
|
_, _ = fmt.Fprintln(w)
|
||||||
}
|
_, _ = fmt.Fprintln(w, ui.RenderPass(ui.IconPass+" All checks passed"))
|
||||||
|
return
|
||||||
_, _ = fmt.Fprintln(w, strings.Join(parts, ", "))
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintln(w)
|
||||||
|
_, _ = fmt.Fprintln(w, ui.RenderWarn(ui.IconWarn+" WARNINGS"))
|
||||||
|
|
||||||
|
// Sort by severity: errors first, then warnings
|
||||||
|
slices.SortStableFunc(warnings, func(a, b *CheckResult) int {
|
||||||
|
if a.Status == StatusError && b.Status != StatusError {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if a.Status != StatusError && b.Status == StatusError {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
|
for i, check := range warnings {
|
||||||
|
line := fmt.Sprintf("%s: %s", check.Name, check.Message)
|
||||||
|
if check.Status == StatusError {
|
||||||
|
_, _ = fmt.Fprintf(w, " %s %s %s\n", ui.RenderFailIcon(), ui.RenderFail(fmt.Sprintf("%d.", i+1)), ui.RenderFail(line))
|
||||||
|
} else {
|
||||||
|
_, _ = fmt.Fprintf(w, " %s %s %s\n", ui.RenderWarnIcon(), ui.RenderWarn(fmt.Sprintf("%d.", i+1)), line)
|
||||||
|
}
|
||||||
|
if check.FixHint != "" {
|
||||||
|
_, _ = fmt.Fprintf(w, " %s%s\n", ui.MutedStyle.Render(ui.TreeLast), check.FixHint)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func NewWispGCCheck() *WispGCCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "wisp-gc",
|
CheckName: "wisp-gc",
|
||||||
CheckDescription: "Detect and clean orphaned wisps (>1h old)",
|
CheckDescription: "Detect and clean orphaned wisps (>1h old)",
|
||||||
|
CheckCategory: CategoryCleanup,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
threshold: 1 * time.Hour,
|
threshold: 1 * time.Hour,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ func NewTownConfigExistsCheck() *TownConfigExistsCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "town-config-exists",
|
CheckName: "town-config-exists",
|
||||||
CheckDescription: "Check that mayor/town.json exists",
|
CheckDescription: "Check that mayor/town.json exists",
|
||||||
|
CheckCategory: CategoryCore,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,6 +54,7 @@ func NewTownConfigValidCheck() *TownConfigValidCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "town-config-valid",
|
CheckName: "town-config-valid",
|
||||||
CheckDescription: "Check that mayor/town.json is valid with required fields",
|
CheckDescription: "Check that mayor/town.json is valid with required fields",
|
||||||
|
CheckCategory: CategoryCore,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,6 +132,7 @@ func NewRigsRegistryExistsCheck() *RigsRegistryExistsCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "rigs-registry-exists",
|
CheckName: "rigs-registry-exists",
|
||||||
CheckDescription: "Check that mayor/rigs.json exists",
|
CheckDescription: "Check that mayor/rigs.json exists",
|
||||||
|
CheckCategory: CategoryCore,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -188,6 +191,7 @@ func NewRigsRegistryValidCheck() *RigsRegistryValidCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "rigs-registry-valid",
|
CheckName: "rigs-registry-valid",
|
||||||
CheckDescription: "Check that registered rigs exist on disk",
|
CheckDescription: "Check that registered rigs exist on disk",
|
||||||
|
CheckCategory: CategoryCore,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -320,6 +324,7 @@ func NewMayorExistsCheck() *MayorExistsCheck {
|
|||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "mayor-exists",
|
CheckName: "mayor-exists",
|
||||||
CheckDescription: "Check that mayor/ directory exists with required files",
|
CheckDescription: "Check that mayor/ directory exists with required files",
|
||||||
|
CheckCategory: CategoryCore,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-14
@@ -1,48 +1,50 @@
|
|||||||
// Package style provides consistent terminal styling using Lipgloss.
|
// Package style provides consistent terminal styling using Lipgloss.
|
||||||
|
// Uses the Ayu theme colors from internal/ui for semantic consistency.
|
||||||
package style
|
package style
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/steveyegge/gastown/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Success style for positive outcomes
|
// Success style for positive outcomes (green)
|
||||||
Success = lipgloss.NewStyle().
|
Success = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("10")). // Green
|
Foreground(ui.ColorPass).
|
||||||
Bold(true)
|
Bold(true)
|
||||||
|
|
||||||
// Warning style for cautionary messages
|
// Warning style for cautionary messages (yellow)
|
||||||
Warning = lipgloss.NewStyle().
|
Warning = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("11")). // Yellow
|
Foreground(ui.ColorWarn).
|
||||||
Bold(true)
|
Bold(true)
|
||||||
|
|
||||||
// Error style for failures
|
// Error style for failures (red)
|
||||||
Error = lipgloss.NewStyle().
|
Error = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("9")). // Red
|
Foreground(ui.ColorFail).
|
||||||
Bold(true)
|
Bold(true)
|
||||||
|
|
||||||
// Info style for informational messages
|
// Info style for informational messages (blue)
|
||||||
Info = lipgloss.NewStyle().
|
Info = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("12")) // Blue
|
Foreground(ui.ColorAccent)
|
||||||
|
|
||||||
// Dim style for secondary information
|
// Dim style for secondary information (gray)
|
||||||
Dim = lipgloss.NewStyle().
|
Dim = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("8")) // Gray
|
Foreground(ui.ColorMuted)
|
||||||
|
|
||||||
// Bold style for emphasis
|
// Bold style for emphasis
|
||||||
Bold = lipgloss.NewStyle().
|
Bold = lipgloss.NewStyle().
|
||||||
Bold(true)
|
Bold(true)
|
||||||
|
|
||||||
// SuccessPrefix is the checkmark prefix for success messages
|
// SuccessPrefix is the checkmark prefix for success messages
|
||||||
SuccessPrefix = Success.Render("✓")
|
SuccessPrefix = Success.Render(ui.IconPass)
|
||||||
|
|
||||||
// WarningPrefix is the warning prefix
|
// WarningPrefix is the warning prefix
|
||||||
WarningPrefix = Warning.Render("⚠")
|
WarningPrefix = Warning.Render(ui.IconWarn)
|
||||||
|
|
||||||
// ErrorPrefix is the error prefix
|
// ErrorPrefix is the error prefix
|
||||||
ErrorPrefix = Error.Render("✗")
|
ErrorPrefix = Error.Render(ui.IconFail)
|
||||||
|
|
||||||
// ArrowPrefix for action indicators
|
// ArrowPrefix for action indicators
|
||||||
ArrowPrefix = Info.Render("→")
|
ArrowPrefix = Info.Render("→")
|
||||||
@@ -52,5 +54,5 @@ var (
|
|||||||
// The format and args work like fmt.Printf.
|
// The format and args work like fmt.Printf.
|
||||||
func PrintWarning(format string, args ...interface{}) {
|
func PrintWarning(format string, args ...interface{}) {
|
||||||
msg := fmt.Sprintf(format, args...)
|
msg := fmt.Sprintf(format, args...)
|
||||||
fmt.Printf("%s %s\n", Warning.Render("⚠ Warning:"), msg)
|
fmt.Printf("%s %s\n", Warning.Render(ui.IconWarn+" Warning:"), msg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,18 @@ package feed
|
|||||||
import (
|
import (
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/steveyegge/gastown/internal/constants"
|
"github.com/steveyegge/gastown/internal/constants"
|
||||||
|
"github.com/steveyegge/gastown/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Color palette
|
// Color palette using Ayu theme colors from ui package
|
||||||
var (
|
var (
|
||||||
colorPrimary = lipgloss.Color("12") // Blue
|
colorPrimary = ui.ColorAccent // Blue
|
||||||
colorSuccess = lipgloss.Color("10") // Green
|
colorSuccess = ui.ColorPass // Green
|
||||||
colorWarning = lipgloss.Color("11") // Yellow
|
colorWarning = ui.ColorWarn // Yellow
|
||||||
colorError = lipgloss.Color("9") // Red
|
colorError = ui.ColorFail // Red
|
||||||
colorDim = lipgloss.Color("8") // Gray
|
colorDim = ui.ColorMuted // Gray
|
||||||
colorHighlight = lipgloss.Color("14") // Cyan
|
colorHighlight = lipgloss.AdaptiveColor{Light: "#59c2ff", Dark: "#59c2ff"} // Cyan (Ayu)
|
||||||
colorAccent = lipgloss.Color("13") // Magenta
|
colorAccent = lipgloss.AdaptiveColor{Light: "#d2a6ff", Dark: "#d2a6ff"} // Purple (Ayu)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Styles for the feed TUI
|
// Styles for the feed TUI
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/glamour"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RenderMarkdown renders markdown text with glamour styling.
|
||||||
|
// Returns raw markdown on failure for graceful degradation.
|
||||||
|
func RenderMarkdown(markdown string) string {
|
||||||
|
// agent mode outputs plain text for machine parsing
|
||||||
|
if IsAgentMode() {
|
||||||
|
return markdown
|
||||||
|
}
|
||||||
|
|
||||||
|
// no styling when colors are disabled
|
||||||
|
if !ShouldUseColor() {
|
||||||
|
return markdown
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapWidth := getTerminalWidth()
|
||||||
|
|
||||||
|
renderer, err := glamour.NewTermRenderer(
|
||||||
|
glamour.WithAutoStyle(),
|
||||||
|
glamour.WithWordWrap(wrapWidth),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return markdown
|
||||||
|
}
|
||||||
|
|
||||||
|
rendered, err := renderer.Render(markdown)
|
||||||
|
if err != nil {
|
||||||
|
return markdown
|
||||||
|
}
|
||||||
|
|
||||||
|
return rendered
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTerminalWidth returns the terminal width for word wrapping.
|
||||||
|
// Caps at 100 chars for readability (research suggests 50-75 optimal, 80-100 comfortable).
|
||||||
|
// Falls back to 80 if detection fails.
|
||||||
|
func getTerminalWidth() int {
|
||||||
|
const (
|
||||||
|
defaultWidth = 80
|
||||||
|
maxWidth = 100
|
||||||
|
)
|
||||||
|
|
||||||
|
fd := int(os.Stdout.Fd())
|
||||||
|
if !term.IsTerminal(fd) {
|
||||||
|
return defaultWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
width, _, err := term.GetSize(fd)
|
||||||
|
if err != nil || width <= 0 {
|
||||||
|
return defaultWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
if width > maxWidth {
|
||||||
|
return maxWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
return width
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PagerOptions configures pager behavior for command output.
|
||||||
|
type PagerOptions struct {
|
||||||
|
// NoPager disables pager for this command (--no-pager flag)
|
||||||
|
NoPager bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldUsePager determines if output should be piped to a pager.
|
||||||
|
// Returns false if explicitly disabled, env var set, or stdout is not a TTY.
|
||||||
|
func shouldUsePager(opts PagerOptions) bool {
|
||||||
|
if opts.NoPager {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if os.Getenv("GT_NO_PAGER") != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !term.IsTerminal(int(os.Stdout.Fd())) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPagerCommand returns the pager command to use.
|
||||||
|
// Checks GT_PAGER, then PAGER, defaults to "less".
|
||||||
|
func getPagerCommand() string {
|
||||||
|
if pager := os.Getenv("GT_PAGER"); pager != "" {
|
||||||
|
return pager
|
||||||
|
}
|
||||||
|
if pager := os.Getenv("PAGER"); pager != "" {
|
||||||
|
return pager
|
||||||
|
}
|
||||||
|
return "less"
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTerminalHeight returns the terminal height in lines.
|
||||||
|
// Returns 0 if unable to determine (not a TTY).
|
||||||
|
func getTerminalHeight() int {
|
||||||
|
fd := int(os.Stdout.Fd())
|
||||||
|
if !term.IsTerminal(fd) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
_, height, err := term.GetSize(fd)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return height
|
||||||
|
}
|
||||||
|
|
||||||
|
// contentHeight counts the number of lines in content.
|
||||||
|
// Returns 0 if content is empty.
|
||||||
|
func contentHeight(content string) int {
|
||||||
|
if content == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return strings.Count(content, "\n") + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToPager pipes content to a pager if appropriate.
|
||||||
|
// Prints directly if pager is disabled, stdout is not a TTY, or content fits in terminal.
|
||||||
|
func ToPager(content string, opts PagerOptions) error {
|
||||||
|
if !shouldUsePager(opts) {
|
||||||
|
fmt.Print(content)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
termHeight := getTerminalHeight()
|
||||||
|
lines := contentHeight(content)
|
||||||
|
|
||||||
|
// print directly if content fits in terminal (leave room for prompt)
|
||||||
|
if termHeight > 0 && lines <= termHeight-1 {
|
||||||
|
fmt.Print(content)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pagerCmd := getPagerCommand()
|
||||||
|
parts := strings.Fields(pagerCmd)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
fmt.Print(content)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(parts[0], parts[1:]...)
|
||||||
|
cmd.Stdin = strings.NewReader(content)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
// set LESS options if not already configured
|
||||||
|
// -R: allow ANSI color codes
|
||||||
|
// -F: quit if content fits on one screen
|
||||||
|
// -X: don't clear screen on exit
|
||||||
|
if os.Getenv("LESS") == "" {
|
||||||
|
cmd.Env = append(os.Environ(), "LESS=-RFX")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
// Package ui provides terminal styling for gastown CLI output.
|
||||||
|
// Uses the Ayu color theme with adaptive light/dark mode support.
|
||||||
|
// Design philosophy: semantic colors that communicate meaning at a glance,
|
||||||
|
// minimal visual noise, and consistent rendering across all commands.
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/muesli/termenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if !ShouldUseColor() {
|
||||||
|
// disable colors when not appropriate (non-TTY, NO_COLOR, etc.)
|
||||||
|
lipgloss.SetColorProfile(termenv.Ascii)
|
||||||
|
} else {
|
||||||
|
// use TrueColor for distinct priority/status colors in modern terminals
|
||||||
|
lipgloss.SetColorProfile(termenv.TrueColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ayu theme color palette
|
||||||
|
// Dark: https://terminalcolors.com/themes/ayu/dark/
|
||||||
|
// Light: https://terminalcolors.com/themes/ayu/light/
|
||||||
|
// Source: https://github.com/ayu-theme/ayu-colors
|
||||||
|
var (
|
||||||
|
// Core semantic colors (Ayu theme - adaptive light/dark)
|
||||||
|
ColorPass = lipgloss.AdaptiveColor{
|
||||||
|
Light: "#86b300", // ayu light bright green
|
||||||
|
Dark: "#c2d94c", // ayu dark bright green
|
||||||
|
}
|
||||||
|
ColorWarn = lipgloss.AdaptiveColor{
|
||||||
|
Light: "#f2ae49", // ayu light bright yellow
|
||||||
|
Dark: "#ffb454", // ayu dark bright yellow
|
||||||
|
}
|
||||||
|
ColorFail = lipgloss.AdaptiveColor{
|
||||||
|
Light: "#f07171", // ayu light bright red
|
||||||
|
Dark: "#f07178", // ayu dark bright red
|
||||||
|
}
|
||||||
|
ColorMuted = lipgloss.AdaptiveColor{
|
||||||
|
Light: "#828c99", // ayu light muted
|
||||||
|
Dark: "#6c7680", // ayu dark muted
|
||||||
|
}
|
||||||
|
ColorAccent = lipgloss.AdaptiveColor{
|
||||||
|
Light: "#399ee6", // ayu light bright blue
|
||||||
|
Dark: "#59c2ff", // ayu dark bright blue
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Workflow Status Colors ===
|
||||||
|
// Only actionable states get color - open/closed match standard text
|
||||||
|
ColorStatusOpen = lipgloss.AdaptiveColor{
|
||||||
|
Light: "", // standard text color
|
||||||
|
Dark: "",
|
||||||
|
}
|
||||||
|
ColorStatusInProgress = lipgloss.AdaptiveColor{
|
||||||
|
Light: "#f2ae49", // yellow - active work, very visible
|
||||||
|
Dark: "#ffb454",
|
||||||
|
}
|
||||||
|
ColorStatusClosed = lipgloss.AdaptiveColor{
|
||||||
|
Light: "#9099a1", // slightly dimmed - visually shows "done"
|
||||||
|
Dark: "#8090a0",
|
||||||
|
}
|
||||||
|
ColorStatusBlocked = lipgloss.AdaptiveColor{
|
||||||
|
Light: "#f07171", // red - needs attention
|
||||||
|
Dark: "#f26d78",
|
||||||
|
}
|
||||||
|
ColorStatusPinned = lipgloss.AdaptiveColor{
|
||||||
|
Light: "#d2a6ff", // purple - special/elevated
|
||||||
|
Dark: "#d2a6ff",
|
||||||
|
}
|
||||||
|
ColorStatusHooked = lipgloss.AdaptiveColor{
|
||||||
|
Light: "#59c2ff", // cyan - actively worked by agent
|
||||||
|
Dark: "#59c2ff",
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Priority Colors ===
|
||||||
|
// P0/P1/P2 get color - they need attention
|
||||||
|
// P3/P4 are neutral (low/backlog don't need visual urgency)
|
||||||
|
ColorPriorityP0 = lipgloss.AdaptiveColor{
|
||||||
|
Light: "#f07171", // bright red - critical, demands attention
|
||||||
|
Dark: "#f07178",
|
||||||
|
}
|
||||||
|
ColorPriorityP1 = lipgloss.AdaptiveColor{
|
||||||
|
Light: "#ff8f40", // orange - high priority, needs attention soon
|
||||||
|
Dark: "#ff8f40",
|
||||||
|
}
|
||||||
|
ColorPriorityP2 = lipgloss.AdaptiveColor{
|
||||||
|
Light: "#e6b450", // muted gold - medium priority, visible but calm
|
||||||
|
Dark: "#e6b450",
|
||||||
|
}
|
||||||
|
ColorPriorityP3 = lipgloss.AdaptiveColor{
|
||||||
|
Light: "", // neutral - low priority
|
||||||
|
Dark: "",
|
||||||
|
}
|
||||||
|
ColorPriorityP4 = lipgloss.AdaptiveColor{
|
||||||
|
Light: "", // neutral - backlog
|
||||||
|
Dark: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Issue Type Colors ===
|
||||||
|
// Bugs and epics get color - they need attention
|
||||||
|
// All other types use standard text
|
||||||
|
ColorTypeBug = lipgloss.AdaptiveColor{
|
||||||
|
Light: "#f07171", // bright red - bugs are problems
|
||||||
|
Dark: "#f26d78",
|
||||||
|
}
|
||||||
|
ColorTypeFeature = lipgloss.AdaptiveColor{
|
||||||
|
Light: "", // standard text color
|
||||||
|
Dark: "",
|
||||||
|
}
|
||||||
|
ColorTypeTask = lipgloss.AdaptiveColor{
|
||||||
|
Light: "", // standard text color
|
||||||
|
Dark: "",
|
||||||
|
}
|
||||||
|
ColorTypeEpic = lipgloss.AdaptiveColor{
|
||||||
|
Light: "#d2a6ff", // purple - larger scope work
|
||||||
|
Dark: "#d2a6ff",
|
||||||
|
}
|
||||||
|
ColorTypeChore = lipgloss.AdaptiveColor{
|
||||||
|
Light: "", // standard text color
|
||||||
|
Dark: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Issue ID Color ===
|
||||||
|
// IDs use standard text color - subtle, not attention-grabbing
|
||||||
|
ColorID = lipgloss.AdaptiveColor{
|
||||||
|
Light: "", // standard text color
|
||||||
|
Dark: "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Core styles - consistent across all commands
|
||||||
|
var (
|
||||||
|
PassStyle = lipgloss.NewStyle().Foreground(ColorPass)
|
||||||
|
WarnStyle = lipgloss.NewStyle().Foreground(ColorWarn)
|
||||||
|
FailStyle = lipgloss.NewStyle().Foreground(ColorFail)
|
||||||
|
MutedStyle = lipgloss.NewStyle().Foreground(ColorMuted)
|
||||||
|
AccentStyle = lipgloss.NewStyle().Foreground(ColorAccent)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Issue ID style
|
||||||
|
var IDStyle = lipgloss.NewStyle().Foreground(ColorID)
|
||||||
|
|
||||||
|
// Status styles for workflow states
|
||||||
|
var (
|
||||||
|
StatusOpenStyle = lipgloss.NewStyle().Foreground(ColorStatusOpen)
|
||||||
|
StatusInProgressStyle = lipgloss.NewStyle().Foreground(ColorStatusInProgress)
|
||||||
|
StatusClosedStyle = lipgloss.NewStyle().Foreground(ColorStatusClosed)
|
||||||
|
StatusBlockedStyle = lipgloss.NewStyle().Foreground(ColorStatusBlocked)
|
||||||
|
StatusPinnedStyle = lipgloss.NewStyle().Foreground(ColorStatusPinned)
|
||||||
|
StatusHookedStyle = lipgloss.NewStyle().Foreground(ColorStatusHooked)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Priority styles - P0 is bold for extra emphasis
|
||||||
|
var (
|
||||||
|
PriorityP0Style = lipgloss.NewStyle().Foreground(ColorPriorityP0).Bold(true)
|
||||||
|
PriorityP1Style = lipgloss.NewStyle().Foreground(ColorPriorityP1)
|
||||||
|
PriorityP2Style = lipgloss.NewStyle().Foreground(ColorPriorityP2)
|
||||||
|
PriorityP3Style = lipgloss.NewStyle().Foreground(ColorPriorityP3)
|
||||||
|
PriorityP4Style = lipgloss.NewStyle().Foreground(ColorPriorityP4)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Type styles for issue categories
|
||||||
|
var (
|
||||||
|
TypeBugStyle = lipgloss.NewStyle().Foreground(ColorTypeBug)
|
||||||
|
TypeFeatureStyle = lipgloss.NewStyle().Foreground(ColorTypeFeature)
|
||||||
|
TypeTaskStyle = lipgloss.NewStyle().Foreground(ColorTypeTask)
|
||||||
|
TypeEpicStyle = lipgloss.NewStyle().Foreground(ColorTypeEpic)
|
||||||
|
TypeChoreStyle = lipgloss.NewStyle().Foreground(ColorTypeChore)
|
||||||
|
)
|
||||||
|
|
||||||
|
// CategoryStyle for section headers - bold with accent color
|
||||||
|
var CategoryStyle = lipgloss.NewStyle().Bold(true).Foreground(ColorAccent)
|
||||||
|
|
||||||
|
// BoldStyle for emphasis
|
||||||
|
var BoldStyle = lipgloss.NewStyle().Bold(true)
|
||||||
|
|
||||||
|
// CommandStyle for command names - subtle contrast, not attention-grabbing
|
||||||
|
var CommandStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{
|
||||||
|
Light: "#5c6166", // slightly darker than standard
|
||||||
|
Dark: "#bfbdb6", // slightly brighter than standard
|
||||||
|
})
|
||||||
|
|
||||||
|
// Status icons - consistent semantic indicators
|
||||||
|
// Design: small Unicode symbols, NOT emoji-style icons for visual consistency
|
||||||
|
const (
|
||||||
|
IconPass = "✓"
|
||||||
|
IconWarn = "⚠"
|
||||||
|
IconFail = "✖"
|
||||||
|
IconSkip = "-"
|
||||||
|
IconInfo = "ℹ"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Issue status icons - used consistently across all commands
|
||||||
|
// Design principle: icons > text labels for scannability
|
||||||
|
const (
|
||||||
|
StatusIconOpen = "○" // available to work (hollow circle)
|
||||||
|
StatusIconInProgress = "◐" // active work (half-filled)
|
||||||
|
StatusIconBlocked = "●" // needs attention (filled circle)
|
||||||
|
StatusIconClosed = "✓" // completed (checkmark)
|
||||||
|
StatusIconDeferred = "❄" // scheduled for later (snowflake)
|
||||||
|
StatusIconPinned = "📌" // elevated priority
|
||||||
|
)
|
||||||
|
|
||||||
|
// Priority icon - small filled circle, colored by priority level
|
||||||
|
const PriorityIcon = "●"
|
||||||
|
|
||||||
|
// Tree characters for hierarchical display
|
||||||
|
const (
|
||||||
|
TreeChild = "⎿ " // child indicator
|
||||||
|
TreeLast = "└─ " // last child / detail line
|
||||||
|
TreeIndent = " " // 2-space indent per level
|
||||||
|
)
|
||||||
|
|
||||||
|
// Separators - 42 characters wide
|
||||||
|
const (
|
||||||
|
SeparatorLight = "──────────────────────────────────────────"
|
||||||
|
SeparatorHeavy = "══════════════════════════════════════════"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === Core Render Functions ===
|
||||||
|
|
||||||
|
// RenderPass renders text with pass (green) styling
|
||||||
|
func RenderPass(s string) string {
|
||||||
|
return PassStyle.Render(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderWarn renders text with warning (yellow) styling
|
||||||
|
func RenderWarn(s string) string {
|
||||||
|
return WarnStyle.Render(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderFail renders text with fail (red) styling
|
||||||
|
func RenderFail(s string) string {
|
||||||
|
return FailStyle.Render(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderMuted renders text with muted (gray) styling
|
||||||
|
func RenderMuted(s string) string {
|
||||||
|
return MutedStyle.Render(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderAccent renders text with accent (blue) styling
|
||||||
|
func RenderAccent(s string) string {
|
||||||
|
return AccentStyle.Render(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderCategory renders a category header in uppercase with accent color
|
||||||
|
func RenderCategory(s string) string {
|
||||||
|
return CategoryStyle.Render(strings.ToUpper(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderSeparator renders the light separator line in muted color
|
||||||
|
func RenderSeparator() string {
|
||||||
|
return MutedStyle.Render(SeparatorLight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderBold renders text in bold
|
||||||
|
func RenderBold(s string) string {
|
||||||
|
return BoldStyle.Render(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderCommand renders a command name with subtle styling
|
||||||
|
func RenderCommand(s string) string {
|
||||||
|
return CommandStyle.Render(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Icon Render Functions ===
|
||||||
|
|
||||||
|
// RenderPassIcon renders the pass icon with styling
|
||||||
|
func RenderPassIcon() string {
|
||||||
|
return PassStyle.Render(IconPass)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderWarnIcon renders the warning icon with styling
|
||||||
|
func RenderWarnIcon() string {
|
||||||
|
return WarnStyle.Render(IconWarn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderFailIcon renders the fail icon with styling
|
||||||
|
func RenderFailIcon() string {
|
||||||
|
return FailStyle.Render(IconFail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderSkipIcon renders the skip icon with styling
|
||||||
|
func RenderSkipIcon() string {
|
||||||
|
return MutedStyle.Render(IconSkip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderInfoIcon renders the info icon with styling
|
||||||
|
func RenderInfoIcon() string {
|
||||||
|
return AccentStyle.Render(IconInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Issue Component Renderers ===
|
||||||
|
|
||||||
|
// RenderID renders an issue ID with semantic styling
|
||||||
|
func RenderID(id string) string {
|
||||||
|
return IDStyle.Render(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderStatus renders a status with semantic styling
|
||||||
|
// in_progress/blocked/pinned get color; open/closed use standard text
|
||||||
|
func RenderStatus(status string) string {
|
||||||
|
switch status {
|
||||||
|
case "in_progress":
|
||||||
|
return StatusInProgressStyle.Render(status)
|
||||||
|
case "blocked":
|
||||||
|
return StatusBlockedStyle.Render(status)
|
||||||
|
case "pinned":
|
||||||
|
return StatusPinnedStyle.Render(status)
|
||||||
|
case "hooked":
|
||||||
|
return StatusHookedStyle.Render(status)
|
||||||
|
case "closed":
|
||||||
|
return StatusClosedStyle.Render(status)
|
||||||
|
default: // open and others
|
||||||
|
return StatusOpenStyle.Render(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderStatusIcon returns the appropriate icon for a status with semantic coloring
|
||||||
|
// This is the canonical source for status icon rendering - use this everywhere
|
||||||
|
func RenderStatusIcon(status string) string {
|
||||||
|
switch status {
|
||||||
|
case "open":
|
||||||
|
return StatusIconOpen // no color - available but not urgent
|
||||||
|
case "in_progress":
|
||||||
|
return StatusInProgressStyle.Render(StatusIconInProgress)
|
||||||
|
case "blocked":
|
||||||
|
return StatusBlockedStyle.Render(StatusIconBlocked)
|
||||||
|
case "closed":
|
||||||
|
return StatusClosedStyle.Render(StatusIconClosed)
|
||||||
|
case "deferred":
|
||||||
|
return MutedStyle.Render(StatusIconDeferred)
|
||||||
|
case "pinned":
|
||||||
|
return StatusPinnedStyle.Render(StatusIconPinned)
|
||||||
|
default:
|
||||||
|
return "?" // unknown status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatusIcon returns just the icon character without styling
|
||||||
|
// Useful when you need to apply custom styling or for non-TTY output
|
||||||
|
func GetStatusIcon(status string) string {
|
||||||
|
switch status {
|
||||||
|
case "open":
|
||||||
|
return StatusIconOpen
|
||||||
|
case "in_progress":
|
||||||
|
return StatusIconInProgress
|
||||||
|
case "blocked":
|
||||||
|
return StatusIconBlocked
|
||||||
|
case "closed":
|
||||||
|
return StatusIconClosed
|
||||||
|
case "deferred":
|
||||||
|
return StatusIconDeferred
|
||||||
|
case "pinned":
|
||||||
|
return StatusIconPinned
|
||||||
|
default:
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatusStyle returns the lipgloss style for a given status
|
||||||
|
// Use this when you need to apply the semantic color to custom text
|
||||||
|
func GetStatusStyle(status string) lipgloss.Style {
|
||||||
|
switch status {
|
||||||
|
case "in_progress":
|
||||||
|
return StatusInProgressStyle
|
||||||
|
case "blocked":
|
||||||
|
return StatusBlockedStyle
|
||||||
|
case "closed":
|
||||||
|
return StatusClosedStyle
|
||||||
|
case "deferred":
|
||||||
|
return MutedStyle
|
||||||
|
case "pinned":
|
||||||
|
return StatusPinnedStyle
|
||||||
|
case "hooked":
|
||||||
|
return StatusHookedStyle
|
||||||
|
default: // open and others - no special styling
|
||||||
|
return lipgloss.NewStyle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderPriority renders a priority level with semantic styling
|
||||||
|
// Format: "● P0" (icon + label)
|
||||||
|
// P0/P1/P2 get color; P3/P4 use standard text
|
||||||
|
func RenderPriority(priority int) string {
|
||||||
|
label := fmt.Sprintf("%s P%d", PriorityIcon, priority)
|
||||||
|
switch priority {
|
||||||
|
case 0:
|
||||||
|
return PriorityP0Style.Render(label)
|
||||||
|
case 1:
|
||||||
|
return PriorityP1Style.Render(label)
|
||||||
|
case 2:
|
||||||
|
return PriorityP2Style.Render(label)
|
||||||
|
case 3:
|
||||||
|
return PriorityP3Style.Render(label)
|
||||||
|
case 4:
|
||||||
|
return PriorityP4Style.Render(label)
|
||||||
|
default:
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderPriorityCompact renders just the priority label without icon
|
||||||
|
// Format: "P0"
|
||||||
|
// Use when space is constrained or icon would be redundant
|
||||||
|
func RenderPriorityCompact(priority int) string {
|
||||||
|
label := fmt.Sprintf("P%d", priority)
|
||||||
|
switch priority {
|
||||||
|
case 0:
|
||||||
|
return PriorityP0Style.Render(label)
|
||||||
|
case 1:
|
||||||
|
return PriorityP1Style.Render(label)
|
||||||
|
case 2:
|
||||||
|
return PriorityP2Style.Render(label)
|
||||||
|
case 3:
|
||||||
|
return PriorityP3Style.Render(label)
|
||||||
|
case 4:
|
||||||
|
return PriorityP4Style.Render(label)
|
||||||
|
default:
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderType renders an issue type with semantic styling
|
||||||
|
// bugs and epics get color; all other types use standard text
|
||||||
|
func RenderType(issueType string) string {
|
||||||
|
switch issueType {
|
||||||
|
case "bug":
|
||||||
|
return TypeBugStyle.Render(issueType)
|
||||||
|
case "feature":
|
||||||
|
return TypeFeatureStyle.Render(issueType)
|
||||||
|
case "task":
|
||||||
|
return TypeTaskStyle.Render(issueType)
|
||||||
|
case "epic":
|
||||||
|
return TypeEpicStyle.Render(issueType)
|
||||||
|
case "chore":
|
||||||
|
return TypeChoreStyle.Render(issueType)
|
||||||
|
default:
|
||||||
|
return issueType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderIssueCompact renders a compact one-line issue summary
|
||||||
|
// Format: ID [Priority] [Type] Status - Title
|
||||||
|
// When status is "closed", the entire line is dimmed to show it's done
|
||||||
|
func RenderIssueCompact(id string, priority int, issueType, status, title string) string {
|
||||||
|
line := fmt.Sprintf("%s [P%d] [%s] %s - %s",
|
||||||
|
id, priority, issueType, status, title)
|
||||||
|
if status == "closed" {
|
||||||
|
// entire line is dimmed - visually shows "done"
|
||||||
|
return StatusClosedStyle.Render(line)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s [%s] [%s] %s - %s",
|
||||||
|
RenderID(id),
|
||||||
|
RenderPriority(priority),
|
||||||
|
RenderType(issueType),
|
||||||
|
RenderStatus(status),
|
||||||
|
title,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderPriorityForStatus renders priority with color only if not closed
|
||||||
|
func RenderPriorityForStatus(priority int, status string) string {
|
||||||
|
if status == "closed" {
|
||||||
|
return fmt.Sprintf("P%d", priority)
|
||||||
|
}
|
||||||
|
return RenderPriority(priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderTypeForStatus renders type with color only if not closed
|
||||||
|
func RenderTypeForStatus(issueType, status string) string {
|
||||||
|
if status == "closed" {
|
||||||
|
return issueType
|
||||||
|
}
|
||||||
|
return RenderType(issueType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderClosedLine renders an entire line in the closed/dimmed style
|
||||||
|
func RenderClosedLine(line string) string {
|
||||||
|
return StatusClosedStyle.Render(line)
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsTerminal returns true if stdout is connected to a terminal (TTY).
|
||||||
|
func IsTerminal() bool {
|
||||||
|
return term.IsTerminal(int(os.Stdout.Fd()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldUseColor determines if ANSI color codes should be used.
|
||||||
|
// Respects NO_COLOR (https://no-color.org/), CLICOLOR, and CLICOLOR_FORCE conventions.
|
||||||
|
func ShouldUseColor() bool {
|
||||||
|
// NO_COLOR takes precedence - any value disables color
|
||||||
|
if _, exists := os.LookupEnv("NO_COLOR"); exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLICOLOR=0 disables color
|
||||||
|
if os.Getenv("CLICOLOR") == "0" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLICOLOR_FORCE enables color even in non-TTY
|
||||||
|
if _, exists := os.LookupEnv("CLICOLOR_FORCE"); exists {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// default: use color only if stdout is a TTY
|
||||||
|
return IsTerminal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldUseEmoji determines if emoji decorations should be used.
|
||||||
|
// Disabled in non-TTY mode to keep output machine-readable.
|
||||||
|
func ShouldUseEmoji() bool {
|
||||||
|
// GT_NO_EMOJI disables emoji output
|
||||||
|
if _, exists := os.LookupEnv("GT_NO_EMOJI"); exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// default: use emoji only if stdout is a TTY
|
||||||
|
return IsTerminal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAgentMode returns true if the CLI is running in agent-optimized mode.
|
||||||
|
// This is triggered by:
|
||||||
|
// - GT_AGENT_MODE=1 environment variable (explicit)
|
||||||
|
// - CLAUDE_CODE environment variable (auto-detect Claude Code)
|
||||||
|
//
|
||||||
|
// Agent mode provides ultra-compact output optimized for LLM context windows.
|
||||||
|
func IsAgentMode() bool {
|
||||||
|
if os.Getenv("GT_AGENT_MODE") == "1" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// auto-detect Claude Code environment
|
||||||
|
if os.Getenv("CLAUDE_CODE") != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user