CLI that turns LLM input into well-formatted Conventional Commits
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(cli): add self-upgrade command

Add upgrade subcommand to check for and apply updates to the
formatted-commit binary via go install. Includes TUI confirm, spinner,
and non-TTY fallback.

Update README and CLI help with upgrade instructions. Refresh module
dependencies to support the new command.

Co-authored-by: Crush <crush@charm.land>

Amolith (Oct 23, 2025, 9:53 AM -0600) 60345ad5 ca2ee9a5

+317 -20
+20 -14
README.md
··· 48 48 go install git.secluded.site/formatted-commit@latest 49 49 ``` 50 50 51 + Check for upgrades with `formatted-commit upgrade`, then apply with 52 + `formatted-commit upgrade -a`. 53 + 51 54 Copy/paste this into wherever you tell your models how to write commits. For 52 55 Crush, that might be `~/.config/crush/CRUSH.md` or `./CRUSH.md` in a repo. For 53 56 [my Crush fork](https://git.secluded.site/crush) and Amp, that's ··· 118 121 119 122 USAGE 120 123 121 - 122 - formatted-commit [--flags] 123 - 124 + formatted-commit [command] [--flags] 124 125 125 126 EXAMPLES 126 - 127 127 128 128 # With co-author 129 129 formatted-commit -t feat -m "do a thing" -T "Crush <crush@charm.land>" ··· 142 142 formatted-commit -t refactor -s "web/git-bug" -m "fancy shmancy" \ 143 143 -b "Had to do a weird thing because..." 144 144 145 + # Check for upgrades 146 + formatted-commit upgrade 147 + 148 + # Then apply 149 + formatted-commit upgrade -a 145 150 146 151 COMMANDS 147 152 148 - help [command] Help about any command 153 + help [command] Help about any command 154 + upgrade [--flags] Check for and apply updates 149 155 150 156 FLAGS 151 157 152 - -a --amend Amend the previous commit (optional) 153 - -b --body Commit body (optional) 154 - -B --breaking Mark as breaking change (optional) 155 - -h --help Help for formatted-commit 156 - -m --message Commit message (required) 157 - -s --scope Commit scope (optional) 158 - -T --trailer Trailer in 'Sentence-case-key: value' format (optional, repeatable) 159 - -t --type Commit type (required) 160 - -v --version Version for formatted-commit 158 + -a --amend Amend the previous commit (optional) 159 + -b --body Commit body (optional) 160 + -B --breaking Mark as breaking change (optional) 161 + -h --help Help for formatted-commit 162 + -m --message Commit message (required) 163 + -s --scope Commit scope (optional) 164 + -T --trailer Trailer in 'Sentence-case-key: value' format (optional, repeatable) 165 + -t --type Commit type (required) 166 + -v --version Version for formatted-commit 161 167 ```
+17
go.mod
··· 7 7 go 1.25.3 8 8 9 9 require ( 10 + github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 11 + github.com/charmbracelet/bubbletea v1.3.6 10 12 github.com/charmbracelet/fang v0.4.3 13 + github.com/charmbracelet/huh v0.8.0 14 + github.com/charmbracelet/lipgloss v1.1.0 11 15 github.com/microcosm-cc/bluemonday v1.0.27 12 16 github.com/spf13/cobra v1.10.1 17 + golang.org/x/mod v0.17.0 18 + golang.org/x/term v0.30.0 13 19 ) 14 20 15 21 require ( 22 + github.com/atotto/clipboard v0.1.4 // indirect 23 + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 16 24 github.com/aymerick/douceur v0.2.0 // indirect 25 + github.com/catppuccin/go v0.3.0 // indirect 17 26 github.com/charmbracelet/colorprofile v0.3.2 // indirect 18 27 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea // indirect 19 28 github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef // indirect 20 29 github.com/charmbracelet/x/ansi v0.10.1 // indirect 21 30 github.com/charmbracelet/x/cellbuf v0.0.13 // indirect 22 31 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect 32 + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect 23 33 github.com/charmbracelet/x/term v0.2.1 // indirect 24 34 github.com/charmbracelet/x/termios v0.1.1 // indirect 25 35 github.com/charmbracelet/x/windows v0.2.2 // indirect 36 + github.com/dustin/go-humanize v1.0.1 // indirect 37 + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 26 38 github.com/gorilla/css v1.0.1 // indirect 27 39 github.com/inconshreveable/mousetrap v1.1.0 // indirect 28 40 github.com/lucasb-eyer/go-colorful v1.3.0 // indirect 41 + github.com/mattn/go-isatty v0.0.20 // indirect 42 + github.com/mattn/go-localereader v0.0.1 // indirect 29 43 github.com/mattn/go-runewidth v0.0.16 // indirect 44 + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect 45 + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 30 46 github.com/muesli/cancelreader v0.2.2 // indirect 31 47 github.com/muesli/mango v0.1.0 // indirect 32 48 github.com/muesli/mango-cobra v1.2.0 // indirect 33 49 github.com/muesli/mango-pflag v0.1.0 // indirect 34 50 github.com/muesli/roff v0.1.0 // indirect 51 + github.com/muesli/termenv v0.16.0 // indirect 35 52 github.com/rivo/uniseg v0.4.7 // indirect 36 53 github.com/spf13/pflag v1.0.9 // indirect 37 54 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+50 -6
go.sum
··· 1 - github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= 2 - github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 1 + github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= 2 + github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= 3 + github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 4 + github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 5 + github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 6 + github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 7 + github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= 8 + github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= 3 9 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 4 10 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 11 + github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= 12 + github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= 13 + github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= 14 + github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= 15 + github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= 16 + github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= 5 17 github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= 6 18 github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI= 7 19 github.com/charmbracelet/fang v0.4.3 h1:qXeMxnL4H6mSKBUhDefHu8NfikFbP/MBNTfqTrXvzmY= 8 20 github.com/charmbracelet/fang v0.4.3/go.mod h1:wHJKQYO5ReYsxx+yZl+skDtrlKO/4LLEQ6EXsdHhRhg= 21 + github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY= 22 + github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= 23 + github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 24 + github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 9 25 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea h1:g1HfUgSMvye8mgecMD1mPscpt+pzJoDEiSA+p2QXzdQ= 10 26 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea/go.mod h1:ngHerf1JLJXBrDXdphn5gFrBPriCL437uwukd5c93pM= 11 27 github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef h1:VrWaUi2LXYLjfjCHowdSOEc6dQ9Ro14KY7Bw4IWd19M= ··· 14 30 github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= 15 31 github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= 16 32 github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 33 + github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= 34 + github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= 35 + github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= 36 + github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= 17 37 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0= 18 38 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0= 19 - github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= 20 - github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= 39 + github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= 40 + github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= 41 + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= 42 + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= 21 43 github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 22 44 github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 23 45 github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= 24 46 github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= 25 47 github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= 26 48 github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= 49 + github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= 50 + github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= 27 51 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 52 + github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= 53 + github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 28 54 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 29 55 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 56 + github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 57 + github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 58 + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 59 + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 30 60 github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= 31 61 github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= 32 62 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 33 63 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 34 64 github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= 35 65 github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 66 + github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 67 + github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 68 + github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 69 + github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 36 70 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 37 71 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 38 72 github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 39 73 github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 74 + github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= 75 + github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= 76 + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 77 + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 40 78 github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 41 79 github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 42 80 github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= ··· 47 85 github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= 48 86 github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= 49 87 github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= 88 + github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 89 + github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 50 90 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 51 91 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 52 92 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= ··· 63 103 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 64 104 golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= 65 105 golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 66 - golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 67 - golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 106 + golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 107 + golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 68 108 golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 69 109 golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 70 110 golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= 71 111 golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 112 + golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 113 + golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 72 114 golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= 73 115 golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 116 + golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 117 + golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 74 118 golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 75 119 golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 76 120 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+6
main.go
··· 49 49 # Including scope for more precise changes 50 50 formatted-commit -t refactor -s "web/git-bug" -m "fancy shmancy" \ 51 51 -b "Had to do a weird thing because..." 52 + 53 + # Check for upgrades 54 + formatted-commit upgrade 55 + 56 + # Then apply 57 + formatted-commit upgrade -a 52 58 `, 53 59 RunE: func(cmd *cobra.Command, args []string) error { 54 60 subject, err := buildAndValidateSubject(commitType, scope, message, breakingChange)
+224
upgrade.go
··· 1 + package main 2 + 3 + import ( 4 + "bytes" 5 + "encoding/json" 6 + "fmt" 7 + "io" 8 + "os" 9 + "os/exec" 10 + "runtime/debug" 11 + "strings" 12 + 13 + "github.com/charmbracelet/bubbles/spinner" 14 + tea "github.com/charmbracelet/bubbletea" 15 + "github.com/charmbracelet/huh" 16 + "github.com/charmbracelet/lipgloss" 17 + "github.com/spf13/cobra" 18 + "golang.org/x/mod/semver" 19 + "golang.org/x/term" 20 + ) 21 + 22 + type moduleInfo struct { 23 + Version string `json:"Version"` 24 + Time string `json:"Time"` 25 + } 26 + 27 + var ( 28 + successStyle = lipgloss.NewStyle().Foreground(lipgloss.CompleteAdaptiveColor{ 29 + Light: lipgloss.CompleteColor{TrueColor: "#00AA00", ANSI256: "34", ANSI: "2"}, 30 + Dark: lipgloss.CompleteColor{TrueColor: "#00FF00", ANSI256: "46", ANSI: "10"}, 31 + }).Bold(true) 32 + warnStyle = lipgloss.NewStyle().Foreground(lipgloss.CompleteAdaptiveColor{ 33 + Light: lipgloss.CompleteColor{TrueColor: "#AA5500", ANSI256: "166", ANSI: "3"}, 34 + Dark: lipgloss.CompleteColor{TrueColor: "#FFAA00", ANSI256: "214", ANSI: "11"}, 35 + }).Bold(true) 36 + errorStyle = lipgloss.NewStyle().Foreground(lipgloss.CompleteAdaptiveColor{ 37 + Light: lipgloss.CompleteColor{TrueColor: "#AA0000", ANSI256: "124", ANSI: "1"}, 38 + Dark: lipgloss.CompleteColor{TrueColor: "#FF5555", ANSI256: "203", ANSI: "9"}, 39 + }).Bold(true) 40 + chipStyle = lipgloss.NewStyle().Padding(0, 1).Bold(true). 41 + Background(lipgloss.CompleteAdaptiveColor{ 42 + Light: lipgloss.CompleteColor{TrueColor: "#F2F2FF", ANSI256: "195", ANSI: "7"}, 43 + Dark: lipgloss.CompleteColor{TrueColor: "#5A4A8A", ANSI256: "98", ANSI: "5"}, 44 + }) 45 + check = "✓" 46 + cross = "✗" 47 + arrow = "→" 48 + ) 49 + 50 + var upgradeCmd = &cobra.Command{ 51 + Use: "upgrade", 52 + Short: "Check for and apply updates", 53 + RunE: func(cmd *cobra.Command, args []string) error { 54 + apply, _ := cmd.Flags().GetBool("apply") 55 + return runUpgrade(apply) 56 + }, 57 + } 58 + 59 + func init() { 60 + upgradeCmd.Flags().BoolP("apply", "a", false, "apply the upgrade") 61 + rootCmd.AddCommand(upgradeCmd) 62 + } 63 + 64 + func runUpgrade(apply bool) error { 65 + info, ok := debug.ReadBuildInfo() 66 + if !ok { 67 + return fmt.Errorf("unable to read build info") 68 + } 69 + 70 + if !strings.HasPrefix(info.Main.Path, "git.secluded.site/") { 71 + return fmt.Errorf("this binary was not installed from git.secluded.site, cannot upgrade") 72 + } 73 + 74 + currentVersion := info.Main.Version 75 + isDevel := currentVersion == "" || currentVersion == "(devel)" 76 + 77 + fmt.Println("Checking for updates…") 78 + 79 + latestVersion, err := getLatestVersion(info.Main.Path) 80 + if err != nil { 81 + return fmt.Errorf("failed to check for updates: %w", err) 82 + } 83 + 84 + if !isDevel { 85 + cmp := semver.Compare(currentVersion, latestVersion) 86 + if cmp == 0 { 87 + if apply { 88 + return fmt.Errorf("already up-to-date") 89 + } 90 + fmt.Printf("%s %s\n", successStyle.Render(check), "Already up to date") 91 + return nil 92 + } else if cmp > 0 { 93 + fmt.Printf("%s %s %s (remote %s)\n", 94 + warnStyle.Render("!"), 95 + "Local version is newer", 96 + chipStyle.Render(currentVersion), 97 + latestVersion) 98 + return nil 99 + } 100 + } 101 + 102 + if isDevel { 103 + fmt.Println(warnStyle.Render("Dev build detected.")) 104 + fmt.Printf("Latest: %s\n", chipStyle.Render(latestVersion)) 105 + } else { 106 + fmt.Printf("Update available: %s %s %s\n", 107 + chipStyle.Render(currentVersion), 108 + arrow, 109 + chipStyle.Render(latestVersion)) 110 + } 111 + 112 + if !apply { 113 + fmt.Println("\nRun with --apply to upgrade") 114 + return nil 115 + } 116 + 117 + var confirm bool 118 + prompt := huh.NewConfirm(). 119 + Title(fmt.Sprintf("Upgrade to %s?", latestVersion)). 120 + Value(&confirm) 121 + 122 + if err := prompt.Run(); err != nil { 123 + return err 124 + } 125 + 126 + if !confirm { 127 + fmt.Println("Upgrade cancelled") 128 + return nil 129 + } 130 + 131 + if err := performUpgrade(info.Main.Path, latestVersion); err != nil { 132 + fmt.Printf("%s %s\n", errorStyle.Render(cross), "Upgrade failed") 133 + return err 134 + } 135 + 136 + fmt.Printf("%s %s %s\n", 137 + successStyle.Render(check), 138 + "Upgraded to", 139 + chipStyle.Render(latestVersion)) 140 + return nil 141 + } 142 + 143 + func getLatestVersion(modulePath string) (string, error) { 144 + cmd := exec.Command("go", "list", "-m", "-json", modulePath+"@latest") 145 + output, err := cmd.Output() 146 + if err != nil { 147 + return "", err 148 + } 149 + 150 + var info moduleInfo 151 + if err := json.Unmarshal(output, &info); err != nil { 152 + return "", err 153 + } 154 + 155 + return info.Version, nil 156 + } 157 + 158 + func performUpgrade(modulePath, version string) error { 159 + if !term.IsTerminal(int(os.Stdout.Fd())) { 160 + fmt.Println("Installing…") 161 + return runGoInstall(modulePath) 162 + } 163 + 164 + m := spinnerModel{ 165 + spinner: spinner.New(spinner.WithSpinner(spinner.Dot)), 166 + text: fmt.Sprintf("Installing %s…", chipStyle.Render(version)), 167 + run: func() error { return runGoInstall(modulePath) }, 168 + } 169 + 170 + if _, err := tea.NewProgram(m).Run(); err != nil { 171 + return err 172 + } 173 + return m.err 174 + } 175 + 176 + func runGoInstall(modulePath string) error { 177 + cmd := exec.Command("go", "install", modulePath+"@latest") 178 + var stderr bytes.Buffer 179 + cmd.Stdout = io.Discard 180 + cmd.Stderr = &stderr 181 + 182 + if err := cmd.Run(); err != nil { 183 + return fmt.Errorf("%w\n%s", err, strings.TrimSpace(stderr.String())) 184 + } 185 + return nil 186 + } 187 + 188 + type installMsg struct{ err error } 189 + 190 + type spinnerModel struct { 191 + spinner spinner.Model 192 + text string 193 + err error 194 + run func() error 195 + done bool 196 + } 197 + 198 + func (m spinnerModel) Init() tea.Cmd { 199 + return tea.Batch(m.spinner.Tick, func() tea.Msg { 200 + return installMsg{err: m.run()} 201 + }) 202 + } 203 + 204 + func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 205 + switch msg := msg.(type) { 206 + case spinner.TickMsg: 207 + var cmd tea.Cmd 208 + m.spinner, cmd = m.spinner.Update(msg) 209 + return m, cmd 210 + case installMsg: 211 + m.err = msg.err 212 + m.done = true 213 + return m, tea.Quit 214 + default: 215 + return m, nil 216 + } 217 + } 218 + 219 + func (m spinnerModel) View() string { 220 + if m.done { 221 + return "" 222 + } 223 + return fmt.Sprintf("%s %s", m.spinner.View(), m.text) 224 + }