@tertium/gith 0.3.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +67 -34
  2. package/bin/gith.js +486 -149
  3. package/package.json +5 -5
package/README.md CHANGED
@@ -1,19 +1,30 @@
1
1
  # @tertium/gith
2
2
 
3
+ <img src="assets/gith-icon.svg" alt="gith mascot with three commit sprouts" width="128" />
4
+
3
5
  gith: git-only branch flow CLI (goal branches, release/hotfix containers, synced merges).
4
6
 
5
7
  ```
6
- o
7
- /
8
- o----o----o
8
+ o
9
+ o | o
9
10
  \ | /
10
11
  \ | /
11
- (o_o) gith v0.1.0
12
- /|_|\ git happens.
13
- d b
12
+ \ | /
13
+ \|/
14
+ |
15
+ .---.
16
+ ( o o ) gith
17
+ '---' git happens.
18
+ |
19
+ /|\
20
+ / \
14
21
  ```
15
22
 
16
- Git helper: the branch flow from [git-convention](https://github.com/Tertiumnon/git-convention) (work branches named develop--<goal>, `release/*` and `hotfix/*` containers, merges that sync both sides with origin), plus everyday git commands: status, switch, update, undo, save, squash, stash, remove, list, tree, sync, clean, reset, open, doctor and stat. Any command takes `--dry-run` to show the git commands it would run. No dependencies beyond git and [Bun](https://bun.com).
23
+ Git helper: the branch flow from [git-convention](https://github.com/Tertiumnon/git-convention) (work branches named develop--<goal>, `release/*` and `hotfix/*` containers, merges that sync both sides with origin), plus everyday git commands: status, switch, update, undo, save, squash, stash, remove, list, tree, push, pull, sync, clean, reset, open, doctor and stat. Any command takes `--dry-run` to show the git commands it would run. No dependencies beyond git and [Bun](https://bun.com).
24
+
25
+ See [Command design rules](docs/RULES.md) before changing or adding commands and flags.
26
+
27
+ See [CHANGELOG.md](CHANGELOG.md) for user-facing release notes and the changes included in each version. GitHub Releases are also published for tagged versions.
17
28
 
18
29
  > **Very early state (0.1.0).** Commands and flags may change without notice, and the tool has only been run against throwaway test repositories. Check the result with `git log` and `git status` before you push.
19
30
 
@@ -24,30 +35,33 @@ Git helper: the branch flow from [git-convention](https://github.com/Tertiumnon/
24
35
  gith create -b login-redirect # work branch develop--login-redirect from develop; stays on the current branch
25
36
  gith create -r 1.2.0 # release/1.2.0 from develop
26
37
  gith create -h 1.2.1 # hotfix/1.2.1 from main
27
- gith create --release # release, version = minor after the latest tag
28
- gith create --hotfix # hotfix, version = patch after the latest tag
38
+ gith create --release # next release version from the latest tag
39
+ gith create --hotfix # next hotfix version from the latest tag
40
+ gith create -r 1.2.0 --update-version --npm # also set npm version to 1.2.0 on the new release branch
41
+ gith create --hotfix --update-version --npm # same for an automatically selected hotfix version
29
42
  gith switch # list local and origin branches, and switch to the one you pick
30
43
  gith switch -b login-redirect # switch to develop--login-redirect, creating it from develop if it is missing
31
44
  gith switch -commit 3f2a1b9 # detach HEAD at that commit, to look at old code
32
45
  gith switch --release # switch to the newest release/* (local or origin); -r 1.9.0 for one version
33
46
  gith switch --hotfix # switch to the newest hotfix/* (local or origin); -h 1.2.1 for one version
34
47
  gith update # merge origin/develop into this work branch
35
- gith merge # this branch into develop
48
+ gith merge --develop # this branch into develop
36
49
  gith finish --current-release # release the newest release/*: version, changelog, merge to main, tag, merge main to develop, then ask before deleting the branch
37
50
  gith finish --current-hotfix # the same for the newest hotfix/*
38
51
  gith merge # pick the branch to merge, then the target from the list
39
52
  gith merge --main # this branch into main
40
- gith merge -r 1.2.0 # this branch into release/1.2.0 (-r alone: the only release, or pick from the list)
53
+ gith merge -r 1.2.0 # this branch into release/1.2.0
54
+ gith merge --release # this branch into the only release, or pick one
41
55
  gith merge -h 1.2.1 # this branch into hotfix/1.2.1
42
56
  gith merge --current-release # this branch into the newest release/* (local or origin)
43
57
  gith merge --current-hotfix # this branch into the newest hotfix/* (local or origin)
44
- gith release --patch # npm version patch, published through main
45
- gith release --minor # npm version minor, from develop into main
46
- gith release --major # npm version major, from develop into main
58
+ gith release --patch # sync main into develop without changing package versions
59
+ gith release --minor # merge develop into main and sync back
60
+ gith release --major --update-version --npm # bump npm major version and release through main
47
61
 
48
62
  # Fix history
49
63
  gith undo # undo the last commit, keep its changes staged (not if pushed)
50
- gith save [-m "msg"] [--all] # stages unstaged changes first (--all without asking), commits locally, asks to amend the previous commit or to make a new one, then asks to push (force-push after an amend)
64
+ gith save -m "msg" [--all] ["--&push"] # new commit without prompts; push only with "--&push"
51
65
  gith squash -limit 3 [-m "msg"] # combine the last 3 commits into one (keeps the oldest message)
52
66
 
53
67
  # Save and reset work
@@ -72,14 +86,14 @@ gith remove --detached # pick local branches that have no origin co
72
86
  gith remove --detached --all # remove all of them: lists them, then asks Y/n
73
87
 
74
88
  # Look at the repo
75
- gith status # branch vs origin, and staged, unstaged and untracked counts
89
+ gith status # working-tree summary, all local and remote-tracking branches
76
90
  gith list --commits # the last 5 commits on this branch
77
91
  gith list --commits --graph # the same, as a branch graph
78
92
  gith list --commits -l 10 # the last 10 (-limit 10 works too)
79
93
  gith list --tags # the newest 5 tags with a LOCAL and ORIGIN column
80
94
  gith list --tags -l 20 # the newest 20
81
95
  gith list --stashes # the newest 5 stashes (-l for more)
82
- gith show --pockets # the same, with Gorlum: my precious pockets!
96
+ gith show --pockets # the same, with the gith mascot: my precious pockets!
83
97
  gith find -commit "login" -limit 10 # commits whose message mentions login (search is the same; default 5)
84
98
  gith search -file "*api.ts" -limit 3 # commits that changed a matching file, with the files listed
85
99
  gith find -change "TODO" # commits that added or removed the text in their diff
@@ -89,6 +103,8 @@ gith stat # fun facts about this branch: biggest commi
89
103
  gith stat --last-week -limit 50 # the same for last week, looking at no more than 50 commits
90
104
 
91
105
  # Remote and health
106
+ gith push [--all-branches] # push to origin with tracking and ahead/behind counts; never force
107
+ gith pull [--all-branches] # fast-forward from origin; stop on divergence
92
108
  gith sync # fetch all remotes and tags, prune stale refs, then sync this branch with origin both ways
93
109
  gith sync --develop | --main # the same for develop or main
94
110
  gith sync develop:develop # two-way sync of one branch with origin
@@ -97,6 +113,10 @@ gith doctor # check the repo is ready for the branch flo
97
113
  gith changelog --dry-run # preview the CHANGELOG.md entry for package.json's version
98
114
  gith changelog # write it as its own commit (docs(changelog): <version>); --no-commit only writes
99
115
 
116
+ # Set up
117
+ gith setup -n npm # write .githooks/pre-commit (lint, typecheck, test) and set core.hooksPath
118
+ gith setup -n npm --force # overwrite an existing hook
119
+
100
120
  # Help
101
121
  gith help # full usage
102
122
  gith --version # the version, with the mascot
@@ -107,34 +127,40 @@ gith list --tags --verbose
107
127
 
108
128
  ## Flags
109
129
 
110
- One rule applies to every command: **a single dash always takes a value, and a double dash never does.** So `-limit 10` is right, and `--limit 10` and `-remote` are errors that name the right spelling.
130
+ One rule applies to every command: **a single dash always takes a value, and a double dash never does.** So `-limit 10` is right, while `--limit 10`, `-remote`, `-r` and `-h` without values are errors. Use `--release` or `--hotfix` for automatic selection.
111
131
 
112
132
  | Flag | Kind | Used by |
113
133
  | --- | --- | --- |
114
134
  | `-b <name>`, `-branch <name>` | value | `create`, `switch`, `remove` |
115
135
  | `--branch` | alone: open the picker | `remove` |
116
- | `-r <version>`, `-release <version>` | value | `create`, `merge` |
117
- | `-r` alone | the only release, or a pick from the list | `merge` |
118
- | `--release` | alone: version from the latest tag | `create` |
136
+ | `-r <version>`, `-release <version>` | value | `create`, `switch`, `merge` |
137
+ | `--release` | automatic version in `create`; newest container in `switch`; only container or picker in `merge` | `create`, `switch`, `merge` |
119
138
  | `--main` | alone: merge into main | `merge` |
120
139
  | `--current-release`, `--current-hotfix` | switch: merge into the newest release or hotfix | `merge` |
121
- | `-h <version>`, `-hotfix <version>` | value | `create`, `merge` |
122
- | `-h` alone | the only hotfix, or a pick from the list | `merge` |
123
- | `--hotfix` | alone: version from the latest tag | `create` |
124
- | `-limit <n>` | value | `squash`: how many commits to combine |
125
- | `-m <msg>`, `-message <msg>` | value | `squash` |
140
+ | `-h <version>`, `-hotfix <version>` | value | `create`, `switch`, `merge` |
141
+ | `--hotfix` | automatic version in `create`; newest container in `switch`; only container or picker in `merge` | `create`, `switch`, `merge` |
142
+ | `-limit <n>` | value | `squash`: commits to combine; listings and `stat`: rows or commits to show |
143
+ | `-m <msg>`, `-message <msg>` | value | `save` (required), `squash` |
144
+ | `--all` | switch | `save`: stage all changes |
145
+ | `"--&push"` | switch | `save`: push after committing |
146
+ | `--all-branches` | switch | `push`, `pull`: all local branches |
126
147
  | `-n <name>`, `-name <name>` | value | `stash` |
148
+ | `-commit <sha>` | value | `switch`: detach at a commit |
149
+ | `-file <pattern>`, `-change <text>` | value | `find` |
127
150
  | `-l <n>`, `-limit <n>` | value, positive whole number | every listing: `list`, `tree`, the picker |
128
151
  | `--patch`, `--minor`, `--major` | switch | `release` |
152
+ | `--update-version`, `--npm` | switches, required together for npm version updates | `release`, `create` with `-r`/`-h`/`--release`/`--hotfix` |
129
153
  | `--overall`, `--current-year`, `--current-month`, `--current-week`, `--last-year`, `--last-month`, `--last-week` | switch, one at a time | `stat` |
130
154
  | `--local`, `--remote` | switch | `remove` (both sides when neither is given) |
131
155
  | `--obsolete`, `--detached` | switch | `remove` |
156
+ | `--force` | switch | `setup`: replace an existing hook |
157
+ | `--no-commit` | switch | `changelog`: update the file without committing |
132
158
  | `-days <n>` | value | `remove --obsolete` |
133
159
  | `--tags`, `--commits`, `--graph`, `--stashes`, `--pockets` | switch | `list` (also `show`) |
134
160
  | `--verbose` | switch | every command |
135
161
  | `--dry-run` | switch | every command |
136
162
 
137
- Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `list --tags` and `--commits`) accept only one of them. `-limit` caps listings and `stat`, and on `squash` it sets how many commits to combine. `-message` only applies to `squash`.
163
+ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `list --tags` and `--commits`) accept only one of them. `-limit` caps listings and `stat`, and on `squash` it sets how many commits to combine. `-message` applies to `save` and `squash`.
138
164
 
139
165
  `--verbose` and `--dry-run` can go anywhere on the line:
140
166
  - `--dry-run` prints each git command that would change something, and runs none of them. Read-only checks still run, so a refusal (for example, "HEAD is already on origin") appears in a dry run too. `clean` and `reset` also list the files they would remove or discard.
@@ -146,17 +172,18 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
146
172
  - **`switch -commit <sha>`** detaches HEAD at that commit so you can look at old code. It refuses a commit that does not exist, and it refuses while tracked files have changes (untracked files do not block it). It lists the files that block it, and points to `gith stash`. On a detached HEAD, `gith switch -b <name>` creates the branch at the current commit, so commits made there stay on it. It does not create or switch branches for other commits, so commits made on a detached HEAD are easy to lose: name a branch before you leave.
147
173
  - **`remove -b <branch>`** removes the branch on both sides: the local branch first (with `git branch -d`, so git refuses to delete an unmerged branch), then the origin branch. A side that does not exist is skipped, so a branch that lives only here is still removed. A name that exists in neither place is an error. `--local` touches only this clone; `--remote` touches only origin. Containers (`develop`, `main`, `release/*`, `hotfix/*`) are refused.
148
174
  - **`remove`** with no name opens the list of branches and asks which to remove, the same as `remove --branch`. By default it lists branch pairs, meaning branches that exist both here and on origin. `--local` lists local branches and `--remote` lists origin branches. Each choice is removed on the same sides as above. `-limit <n>` shows only the first `n` (all by default), and the picker numbers only those. Pick with the arrow keys: up and down move, space checks an item, enter confirms the checked items, and esc cancels. `switch` takes one item with enter. The list needs a terminal, and that is checked before anything is listed. The checked-out branch and containers are never listed. Choosing items is the confirmation, so nothing else is asked. If a delete fails (for example, an unmerged branch), the removal stops there and earlier picks stay deleted.
149
- - **`status`** prints the branch and how far it is ahead of or behind its origin branch, then counts staged, unstaged, untracked and conflicting files. It changes nothing.
175
+ - **`status`** prints the current branch and how far it is ahead of or behind its upstream, then counts staged, unstaged, untracked and conflicting files. It also lists all local branches (`*` marks the current branch; `+` marks branches in other worktrees) and remote-tracking branches from every configured remote. Remote branches reflect the last fetch; run `git fetch --all --prune` to refresh them. Empty lists are shown explicitly. Status changes nothing and does not contact remotes.
150
176
  - **`update`** fetches `origin/develop` and merges it into the current work branch. It needs a clean working tree and refuses containers. If the merge conflicts, it aborts the merge and says so. It does not push.
151
177
  - **`undo`** runs `git reset --soft HEAD~1`: the last commit is removed and its changes stay staged. It refuses when HEAD is already on origin, because undoing a pushed commit needs a force-push.
152
- - **`save`** commits the staged changes locally and never pushes without asking. When changes are not staged yet, it stages them first: a single file is staged silently, and with several it asks `Stage all N changes first? [Y/n]` (a no keeps only what is staged; `--all` stages without asking). If the branch has commits of its own (commits origin's `develop`, or `main`, lacks), it asks `Amend the previous commit? [Y/n]`. Yes folds the changes into the last commit with `git commit --amend --no-edit`, keeping its message; `-m` replaces it. No, or no own commits, creates a new commit, asking for its message (an empty answer is refused, and `-m` skips the question). It refuses with nothing staged. Then it asks `Push <branch> to origin? [Y/n]`, a plain `git push -u origin <branch>`. After an amend of a commit origin already has, it asks `Force-push <branch> to origin? [Y/n]` instead, which runs `git push --force-with-lease origin <branch>` and refuses if origin moved since your last fetch. A no leaves the commit local. The questions need a terminal, so a script must pass `--yes`, and a commit made before the push question stays local if that question is not answered.
178
+ - **`push` / `pull`** work with the current branch and its same-named branch on origin. They report fresh ahead/behind counts and stop with instructions when histories diverge. Push sets upstream tracking and publishes missing branches; it never force-pushes. Pull requires clean tracked files, fast-forwards only, and points to `gith push` when local commits remain. `--all-branches` applies to every local branch, preflighting all before updating any. Push sends them atomically (origin must support atomic pushes); pull requires each to exist on origin and updates branches without switching. Branches checked out in other worktrees cannot be updated by pull. Remote-only branches are not created. Pull may leave earlier branches updated if a later Git operation fails; it never rolls back successful fast-forwards. Dry runs print actions without fetching or changing refs.
179
+ - **`save`** requires a nonempty `-m` and always creates a new commit, without prompts or amending. It stages a single unstaged file automatically; use `--all` to stage several, or stage your selection with `git add`. With nothing staged it fails. It saves locally unless `"--&push"` is supplied, which runs `gith push` after committing. Quote the flag because shells interpret `&`. A failed push leaves the new commit saved locally; retry with `gith push`.
153
180
  - **`stash`** saves changes with `git stash push --include-untracked`. `-n` names the stash. It refuses when there is nothing to save.
154
181
  - **`find`** (also `search`) lists the newest commits on this branch that match every filter given:
155
182
  - `-commit <text>` matches the commit message, ignoring case, and the text is literal.
156
183
  - `-file <pattern>` matches the paths changed by the commit, using git's pattern rules (`*api.ts` matches `src/api.ts`). Each commit lists the matching files it changed.
157
184
  - `-change <text>` matches commits whose diff adds or removes the text (`git log -S`). With `--regex`, it matches a regular expression against the diff (`git log -G`).
158
185
  - `-limit <n>` sets how many to show (default 5). The filters combine with AND.
159
- - **`show`** is a synonym for `list`: `gith show --tags` and `gith list --tags` do the same. `--pockets` is a fun name for `--stashes`, and it prints a Gorlum figure above the list.
186
+ - **`show`** is a synonym for `list`: `gith show --tags` and `gith list --tags` do the same. `--pockets` is a fun name for `--stashes`, and it prints the gith mascot above the list, matching `gith --version` and the SVG/PNG logo in `assets/`.
160
187
  - **`list --stashes`** shows the stashes, newest first, with their date and description. Like the other listings, it shows 5 by default, and `-limit` changes that.
161
188
  - **`squash -limit <n>`** combines the last `n` commits on the current work branch into one, using `reset --soft` and a single commit. Without `-m` the result keeps the oldest of the combined commits, its message and author. It refuses when `n` is below 2, when `n` would reach the first commit, when a merge is among them, when any of them is already on origin, on a container, or with a dirty working tree.
162
189
  - **`stat`** reads the history of the current branch and prints a table of facts: commit count, top author, the biggest commit (lines added and removed), average size, the most changed file, the busiest day, weekday and hour, the longest streak of days with a commit, night owl and weekend commits, and the conventional commit types. The period is one switch: `--overall` (the default), `--current-year`, `--current-month`, `--current-week` (weeks start on Monday), or the matching `--last-` switch for the previous year, month or week. `-limit <n>` looks only at the last `n` commits. Stat changes nothing.
@@ -166,12 +193,13 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
166
193
  - **`remove --detached`** fetches and prunes origin, then lists local branches that have no origin copy and lets you pick from them, the same as `remove --branch --local` but for branches with no origin copy only.
167
194
  - **Every removal shows what it will remove** before deleting anything, including the direct `remove -b` form. The picker and the prompt both show it first.
168
195
  - **`open`** opens origin in the browser, turning an `ssh` or `https` remote into a web address. GitHub and GitLab style addresses work.
196
+ - **`setup -n npm`** writes `.githooks/pre-commit` for an npm project and sets `core.hooksPath` to `.githooks`, so every clone runs the same checks. The hook runs `lint`, `typecheck` and `test` (the scripts are picked up from package.json; TypeScript projects always run `lint` and `typecheck`, and `test` only when it exists). It uses the package manager from the lockfile (`bun`, `pnpm`, `yarn`, or `npm`). It adds a `prepare` script so installing the project turns the hooks on again. It refuses to overwrite an existing hook unless `--force` is given, and it must run in the folder that holds `.git`. Only `npm` is supported for now.
169
197
  - **`doctor`** checks git, a repository, origin, its reachability, `main` and `develop` on origin, and a clean working tree. It exits 1 if a required check fails, and only warns about the tree.
170
198
  - **`sync`** runs `git fetch --all --tags --prune`, then syncs the current branch with origin in both directions. `sync --develop` and `sync --main` do the same for that branch, even when another branch is checked out.
171
199
  - **`sync <branch>:<branch>`** (for example `sync develop:develop`) is the same as `sync --develop` for any branch name. If origin is ahead, it fetches the branch like `git fetch origin develop:develop`; if local is ahead, it pushes it; if it is the same, it says so. A checked-out branch is fast-forwarded with `merge --ff-only`. Diverged branches stop with an error, and nothing is forced. A branch missing on one side is created there. `sync develop` alone works too.
172
200
  - **`tree`** lists the working trees of the repository (`git worktree list`): each path, its branch or `(detached)`, and its short HEAD. The current one is marked `*`, and bare, locked or prunable trees are noted. `-limit <n>` caps the rows; all are shown by default.
173
201
  - **`list --tags`** shows every tag once, with yes/no columns for local and origin, so you can see which tags are unpushed or missing locally. Version tags are listed newest first, and only the first 5 are shown by default. `-limit <n>` (or `-l <n>`) changes that and adds a note when rows are left out.
174
- - **`list --commits`** shows the newest commits on the current branch, one per line: short hash, date, author and subject. With `--graph` it draws the branch graph instead. It shows 5 by default; `-l <n>` (or `-limit <n>`) changes that and must be a positive whole number. `-limit` only applies to listings. `gith list` with no flag prints usage.
202
+ - **`list --commits`** shows the newest commits on the current branch, one per line: short hash, date, author and subject. With `--graph` it draws a framed branch graph with colored topology lanes, highlighted hashes and branch/tag labels, and author/date rows. Colors are enabled in terminals and disabled for pipes, `NO_COLOR`, or `TERM=dumb`. Git preserves merge topology; history remains scoped to the current branch. It shows 5 by default; `-l <n>` (or `-limit <n>`) changes that and must be a positive whole number. `-limit` only applies to listings. `gith list` with no flag prints usage.
175
203
  - **`clean`** runs `git clean -fd`. It permanently removes untracked files and directories, but keeps ignored files.
176
204
  - **`reset`** runs `git reset --hard` (discards staged and unstaged edits on this branch), then `git clean -fd`. It cannot be undone, so commit or stash first. It does not move the branch to origin.
177
205
 
@@ -179,6 +207,7 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
179
207
 
180
208
  - **`create -b`** creates `develop--<name>` from `origin/develop`, pushes it with upstream tracking, and leaves the current branch checked out. The prefix shows the source branch; `create -b develop` and `create -b main` are refused. It does not touch the working tree, so it works with uncommitted changes.
181
209
  - **`create -r` / `create -h`** creates a container from its base (`develop` for release, `main` for hotfix), refusing names that already exist on origin. Like `create -b`, it does not switch.
210
+ - **`create -r/-h/--release/--hotfix --update-version --npm`** also sets npm's version to the branch version and commits `package.json` and any npm lockfiles on that new branch before pushing. Both flags are required together and are rejected for work branches. This uses a temporary worktree, leaves your checked-out branch and local changes intact, and creates no version tag. npm lifecycle scripts are skipped; changelog preparation remains part of finishing the release. An already matching package version needs no commit. Without both flags, creation does not run npm or change package versions. The selected base must contain a `package.json` with a version. If npm or the commit fails, the new branch and temporary worktree remain available for inspection and are not pushed.
182
211
  - **With `--release` or `--hotfix` (no value)**, the version is worked out from the highest `vX.Y.Z` tag across local tags and origin: `-r` takes the next minor (`1.2.1` → `1.3.0`), `-h` takes the next patch (`1.2.1` → `1.2.2`). With no tags yet, the first release is `0.1.0` and the first hotfix `0.0.1`. The version is printed before the branch is created.
183
212
  - **`merge --current-release`** and **`--current-hotfix`** pick the newest `release/*` or `hotfix/*` branch by version (numbers compare as numbers, so `1.10.0` beats `1.9.0`), checking both local branches and origin, and merge into it. The target must exist on origin, because the merge pushes it; a newest branch that only exists here stops the command with a message.
184
213
  - **`finish --current-release`** (and `--current-hotfix`) releases the newest `release/*` (or `hotfix/*`), named by its version X.Y.Z. It makes `package.json`'s version match the name (committed as `X.Y.Z`, only when they differ), adds the changelog entry as its own commit (when it is missing), merges the branch into `main` with `--no-ff`, tags `vX.Y.Z` and pushes, merges `main` into `develop` when `develop` exists on origin, and then asks before deleting the branch. A conflict aborts the merge and stops. Projects without a `package.json` keep their version by hand.
@@ -191,10 +220,14 @@ Containers (`develop`, `main`, `release/*`, `hotfix/*`) can't be merged from, an
191
220
 
192
221
  Run as `gith release --patch`, `--minor` or `--major`.
193
222
 
194
- - **`patch`** runs from `main`: bumps the version, pushes with tags, then rebases `develop` onto `main`. The push back to `develop` uses `--force-with-lease`, so it refuses to overwrite a `develop` that moved on origin.
195
- - **`minor` and `major`** run from `develop`: bump the version, push with tags, merge `develop` into `main`, then bring `main` back to `develop`.
223
+ - **`patch`** runs from `main`: pushes with tags, then rebases `develop` onto `main`. The push back to `develop` uses `--force-with-lease`, so it refuses to overwrite a `develop` that moved on origin.
224
+ - **`minor` and `major`** run from `develop`: push with tags, merge `develop` into `main`, then bring `main` back to `develop`.
225
+
226
+ Both need a clean working tree. By default, they perform only the Git flow: no package manager runs, no package files change, and no new version tag is created. Existing annotated tags reachable from pushed commits can still be pushed.
227
+
228
+ Add **both `--update-version --npm`** to run `npm version` with the selected patch, minor, or major bump before pushing. This requires npm and a `package.json` with a version; npm lifecycle scripts run as usual. Either flag alone is an error. npm is currently the only supported versioning tool. This repository's `release:patch`, `release:minor`, and `release:major` scripts pass both flags explicitly.
196
229
 
197
- Both need a clean working tree and a `package.json` with a version, because they call `npm version`.
230
+ Pushing a `v*` tag publishes the package and creates a GitHub Release with generated notes.
198
231
 
199
232
  ## Errors
200
233
 
package/bin/gith.js CHANGED
@@ -6,8 +6,8 @@ import { exit } from "node:process";
6
6
  // src/components/args/args.constants.ts
7
7
  var SPECS = new Map([
8
8
  ["branch", "auto"],
9
- ["release", "optional"],
10
- ["hotfix", "optional"],
9
+ ["release", "auto"],
10
+ ["hotfix", "auto"],
11
11
  ["limit", "value"],
12
12
  ["message", "value"],
13
13
  ["name", "value"],
@@ -19,6 +19,8 @@ var SPECS = new Map([
19
19
  ["patch", "switch"],
20
20
  ["minor", "switch"],
21
21
  ["major", "switch"],
22
+ ["update-version", "switch"],
23
+ ["npm", "switch"],
22
24
  ["remote", "switch"],
23
25
  ["local", "switch"],
24
26
  ["tags", "switch"],
@@ -33,7 +35,10 @@ var SPECS = new Map([
33
35
  ["pockets", "switch"],
34
36
  ["detached", "switch"],
35
37
  ["all", "switch"],
38
+ ["&push", "switch"],
39
+ ["all-branches", "switch"],
36
40
  ["no-commit", "switch"],
41
+ ["force", "switch"],
37
42
  ["overall", "switch"],
38
43
  ["current-year", "switch"],
39
44
  ["current-month", "switch"],
@@ -57,8 +62,13 @@ var SHORT = {
57
62
  };
58
63
 
59
64
  // src/components/args/args.utils.ts
65
+ var assertAllowedFlags = (flags, allowed, command) => {
66
+ const extra = [...flags.keys()].find((key) => !allowed.includes(key));
67
+ if (extra !== undefined)
68
+ throw new Error(`${spelling(extra)} is not supported by gith ${command}.`);
69
+ };
60
70
  function resolveFlag(token) {
61
- const match = /^(--?)([a-z][a-z-]*)$/.exec(token);
71
+ const match = /^(--?)([a-z][a-z-]*|&push)$/.exec(token);
62
72
  if (!match)
63
73
  return;
64
74
  const word = match[2] ?? "";
@@ -93,8 +103,6 @@ function parseFlags(argv) {
93
103
  if (hasNext) {
94
104
  flags.set(name, next);
95
105
  i++;
96
- } else if (kind === "optional") {
97
- flags.set(name, "");
98
106
  } else {
99
107
  throw new Error(`${token} needs a value.`);
100
108
  }
@@ -166,7 +174,7 @@ function capture(args) {
166
174
  return run(args.split(" "));
167
175
  }
168
176
  function hasOnOrigin(branch) {
169
- return capture(`ls-remote --heads origin ${branch}`) !== "";
177
+ return captureArgs(["ls-remote", "--heads", "origin", `refs/heads/${branch}`]) !== "";
170
178
  }
171
179
  function isAncestor(ancestor, descendant) {
172
180
  try {
@@ -443,7 +451,7 @@ function find(flags) {
443
451
 
444
452
  // src/components/flow/flow.ts
445
453
  import { execSync } from "node:child_process";
446
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
454
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
447
455
 
448
456
  // src/components/prompt/prompt.ts
449
457
  import { emitKeypressEvents } from "node:readline";
@@ -488,20 +496,6 @@ async function confirm(question) {
488
496
  rl.close();
489
497
  }
490
498
  }
491
- async function ask(question) {
492
- if (isDryRun()) {
493
- console.log(`dry run: would ask: ${question}`);
494
- return "<message>";
495
- }
496
- if (!process.stdin.isTTY)
497
- throw new Error("needs a terminal to ask; pass -m <message>.");
498
- const rl = createInterface({ input: process.stdin, output: process.stdout });
499
- try {
500
- return (await rl.question(question)).trim();
501
- } finally {
502
- rl.close();
503
- }
504
- }
505
499
  async function select(question, options, multi) {
506
500
  if (!process.stdin.isTTY)
507
501
  throw new Error("this needs a terminal to pick.");
@@ -612,6 +606,16 @@ function nextVersion(tags, bump) {
612
606
  }
613
607
 
614
608
  // src/components/flow/flow.utils.ts
609
+ import { spawnSync } from "node:child_process";
610
+ import {
611
+ existsSync as existsSync2,
612
+ mkdtempSync,
613
+ realpathSync,
614
+ symlinkSync,
615
+ unlinkSync
616
+ } from "node:fs";
617
+ import { tmpdir } from "node:os";
618
+ import path2 from "node:path";
615
619
  function containers(kind) {
616
620
  const prefix = `${kind}/`;
617
621
  const local = captureArgs([
@@ -648,31 +652,86 @@ function nextContainerVersion(kind) {
648
652
  console.log(`Next ${kind} version from existing tags: ${version}`);
649
653
  return version;
650
654
  }
651
- function open(kind, version) {
655
+ var open = (kind, version, shouldUpdateVersion) => {
656
+ if (shouldUpdateVersion && !/^\d+\.\d+\.\d+$/.test(version))
657
+ throw new Error("npm version updates require a version in X.Y.Z format; use -r or -h with that version.");
652
658
  const name = containerName(kind, version);
659
+ assertBranchName(name);
660
+ if (hasLocal(name))
661
+ throw new Error(`'${name}' already exists locally.`);
653
662
  if (hasOnOrigin(name))
654
663
  throw new Error(`'${name}' already exists on origin.`);
655
664
  const base = containerBase(kind);
656
665
  gitArgs(["fetch", "origin", base]);
666
+ const needsVersionUpdate = shouldUpdateVersion && parsePackageVersion(captureArgs(["show", `origin/${base}:package.json`])) !== version;
657
667
  gitArgs(["branch", name, `origin/${base}`]);
668
+ if (needsVersionUpdate)
669
+ updateContainerVersion(name, version);
658
670
  gitArgs(["push", "-u", "origin", name]);
659
- }
671
+ };
672
+ var updateContainerVersion = (name, version) => {
673
+ if (isDryRun()) {
674
+ process.stdout.write(`> npm version ${version} --no-git-tag-version --ignore-scripts (temporary worktree for ${name})
675
+ `);
676
+ return;
677
+ }
678
+ const cwd = mkdtempSync(path2.join(tmpdir(), "gith-version-"));
679
+ gitArgs(["worktree", "add", cwd, name]);
680
+ const hasDependencies = existsSync2("node_modules");
681
+ if (hasDependencies)
682
+ symlinkSync(realpathSync("node_modules"), path2.join(cwd, "node_modules"), "junction");
683
+ try {
684
+ commitContainerVersion(cwd, version);
685
+ } finally {
686
+ if (hasDependencies)
687
+ unlinkSync(path2.join(cwd, "node_modules"));
688
+ }
689
+ gitArgs(["worktree", "remove", cwd]);
690
+ };
691
+ var commitContainerVersion = (cwd, version) => {
692
+ const result = spawnSync("npm", ["version", version, "--no-git-tag-version", "--ignore-scripts"], {
693
+ cwd,
694
+ stdio: "inherit",
695
+ shell: process.platform === "win32"
696
+ });
697
+ if (result.error || result.status !== 0)
698
+ throw new Error(`npm version failed; inspect the preserved worktree at ${cwd} and retry.`, { cause: result.error });
699
+ gitArgs([
700
+ "add",
701
+ "--",
702
+ ...["package.json", "package-lock.json", "npm-shrinkwrap.json"].filter((file) => existsSync2(path2.join(cwd, file)))
703
+ ], { cwd });
704
+ gitArgs(["commit", "-m", version], { cwd });
705
+ };
660
706
 
661
707
  // src/components/flow/flow.ts
662
- function create(flags) {
708
+ var create = (flags) => {
709
+ assertAllowedFlags(flags, ["branch", "release", "hotfix", "update-version", "npm"], "create");
710
+ if (flags.has("update-version") !== flags.has("npm"))
711
+ throw new Error("Version updates require both --update-version and --npm; pass both flags or omit both.");
663
712
  const kind = onlyOne(flags, ["branch", "release", "hotfix"]);
664
713
  if (!kind)
665
- throw new Error("usage: gith create -b <branch> | -r [version] | -h [version]");
714
+ throw new Error("usage: gith create -b <branch> | (-r <version> | -h <version> | --release | --hotfix) [--update-version --npm]");
666
715
  const value = flags.get(kind) ?? "";
667
716
  if (kind === "branch") {
717
+ if (flags.has("update-version"))
718
+ throw new Error("--update-version --npm only applies to release or hotfix branch creation.");
668
719
  if (!value)
669
720
  throw new Error("usage: gith create -b <branch>");
670
721
  createBranch(value);
671
722
  return;
672
723
  }
673
- open(kind, value || nextContainerVersion(kind));
674
- }
724
+ open(kind, value || nextContainerVersion(kind), flags.has("update-version"));
725
+ };
675
726
  async function merge(flags) {
727
+ assertAllowedFlags(flags, [
728
+ "develop",
729
+ "main",
730
+ "release",
731
+ "hotfix",
732
+ "current-release",
733
+ "current-hotfix"
734
+ ], "merge");
676
735
  if (flags.size === 0)
677
736
  return mergePick();
678
737
  const kind = onlyOne(flags, [
@@ -818,7 +877,7 @@ async function mergePick() {
818
877
  await mergeInto(target, source);
819
878
  }
820
879
  function alignVersion(version) {
821
- if (!existsSync2("package.json"))
880
+ if (!existsSync3("package.json"))
822
881
  return;
823
882
  const current = parsePackageVersion(readFileSync2("package.json", "utf8"));
824
883
  if (current === version)
@@ -834,10 +893,10 @@ function alignVersion(version) {
834
893
  gitArgs(["commit", "-m", version, "--", "package.json"]);
835
894
  }
836
895
  function addChangelogEntry(version) {
837
- if (!existsSync2("package.json"))
896
+ if (!existsSync3("package.json"))
838
897
  return;
839
898
  const file = "CHANGELOG.md";
840
- if (existsSync2(file) && readFileSync2(file, "utf8").includes(`## ${version} - `))
899
+ if (existsSync3(file) && readFileSync2(file, "utf8").includes(`## ${version} - `))
841
900
  return;
842
901
  changelog(new Map);
843
902
  }
@@ -892,15 +951,109 @@ function assertNotPushed() {
892
951
  throw new Error("HEAD is already on origin; changing it would need a force-push.");
893
952
  }
894
953
  }
895
- function remoteSha(branch) {
896
- return captureArgs(["ls-remote", "--heads", "origin", branch]).split("\t")[0] ?? "";
897
- }
898
- async function askMessage() {
899
- const text = await ask("Commit message: ");
900
- if (text === "")
901
- throw new Error("a commit needs a message; nothing was committed.");
902
- return text;
903
- }
954
+
955
+ // src/components/remote/remote.utils.ts
956
+ var getBranches = (flags, command) => {
957
+ if ([...flags.keys()].some((key) => key !== "all-branches"))
958
+ throw new Error(`usage: gith ${command} [--all-branches]`);
959
+ if (!flags.has("all-branches"))
960
+ return [currentBranch()];
961
+ const branches = captureArgs([
962
+ "for-each-ref",
963
+ "--format=%(refname:strip=2)",
964
+ "refs/heads/"
965
+ ]);
966
+ if (branches === "")
967
+ throw new Error("No local branches; create a branch and commit before syncing.");
968
+ return branches.split(`
969
+ `);
970
+ };
971
+ var compareOrigin = (branch) => {
972
+ gitArgs(["fetch", "--no-tags", "origin", `refs/heads/${branch}`]);
973
+ const counts = captureArgs([
974
+ "rev-list",
975
+ "--left-right",
976
+ "--count",
977
+ `refs/heads/${branch}...FETCH_HEAD`
978
+ ]);
979
+ if (!/^\d+\s+\d+$/.test(counts))
980
+ throw new Error("Cannot compare commits with origin; check the repository with git fsck.");
981
+ const parts = counts.split(/\s+/);
982
+ const ahead = Number(parts[0]);
983
+ const behind = Number(parts[1]);
984
+ process.stdout.write(`${branch} -> origin/${branch}: ahead ${ahead}, behind ${behind}.
985
+ `);
986
+ return { ahead, behind };
987
+ };
988
+ var updateBranch = (branch, current) => {
989
+ if (branch === current) {
990
+ gitArgs(["fetch", "--no-tags", "origin", `refs/heads/${branch}`]);
991
+ gitArgs(["merge", "--ff-only", "FETCH_HEAD"]);
992
+ } else {
993
+ gitArgs([
994
+ "fetch",
995
+ "--no-tags",
996
+ "origin",
997
+ `refs/heads/${branch}:refs/heads/${branch}`
998
+ ]);
999
+ }
1000
+ };
1001
+
1002
+ // src/components/remote/remote.ts
1003
+ var push = (flags) => {
1004
+ const branches = getBranches(flags, "push");
1005
+ for (const branch of branches) {
1006
+ if (isDryRun())
1007
+ continue;
1008
+ if (!hasOnOrigin(branch)) {
1009
+ process.stdout.write(`Publishing ${branch} on origin and setting upstream tracking.
1010
+ `);
1011
+ continue;
1012
+ }
1013
+ const { ahead, behind } = compareOrigin(branch);
1014
+ if (behind > 0)
1015
+ throw new Error(ahead > 0 ? `${branch} has diverged; merge or rebase origin's changes, then run gith push. Nothing was pushed.` : `${branch} is behind origin; run gith pull before gith push. Nothing was pushed.`);
1016
+ process.stdout.write(`Pushing ${ahead} commit(s) from ${branch}; ensuring upstream tracking.
1017
+ `);
1018
+ }
1019
+ gitArgs([
1020
+ "push",
1021
+ ...flags.has("all-branches") ? ["--atomic"] : [],
1022
+ "-u",
1023
+ "origin",
1024
+ ...branches.map((branch) => `refs/heads/${branch}:refs/heads/${branch}`)
1025
+ ]);
1026
+ if (!isDryRun())
1027
+ process.stdout.write(`Push complete: ${branches.length} branch(es) synced with origin.
1028
+ `);
1029
+ };
1030
+ var pull = (flags) => {
1031
+ const branches = getBranches(flags, "pull");
1032
+ const current = currentBranch();
1033
+ assertCleanTree();
1034
+ const updates = [];
1035
+ for (const branch of branches) {
1036
+ if (isDryRun())
1037
+ continue;
1038
+ if (!hasOnOrigin(branch))
1039
+ throw new Error(`origin/${branch} does not exist; run gith push to publish this branch first.`);
1040
+ const { ahead, behind } = compareOrigin(branch);
1041
+ if (ahead > 0 && behind > 0)
1042
+ throw new Error(`${branch} has diverged; merge or rebase origin's changes explicitly. No commits were changed.`);
1043
+ if (behind > 0)
1044
+ updates.push({ branch, behind });
1045
+ else
1046
+ process.stdout.write(ahead > 0 ? `${branch}: no incoming commits; run gith push to publish ${ahead} local commit(s).
1047
+ ` : `${branch}: already up to date.
1048
+ `);
1049
+ }
1050
+ for (const { branch, behind } of isDryRun() ? branches.map((branch) => ({ branch, behind: 0 })) : updates) {
1051
+ updateBranch(branch, current);
1052
+ if (!isDryRun())
1053
+ process.stdout.write(`Pulled ${behind} commit(s) into ${branch}.
1054
+ `);
1055
+ }
1056
+ };
904
1057
 
905
1058
  // src/components/history/history.ts
906
1059
  function update(flags) {
@@ -926,43 +1079,33 @@ function undo(flags) {
926
1079
  assertNotPushed();
927
1080
  gitArgs(["reset", "--soft", "HEAD~1"]);
928
1081
  }
929
- async function save(flags) {
1082
+ var save = (flags) => {
930
1083
  const message = flags.get("message");
931
- const extra = [...flags.keys()].filter((key) => key !== "message" && key !== "all");
932
- if (extra.length > 0)
933
- throw new Error("usage: gith save [-m <message>] [--all]");
934
- const branch = currentBranch();
1084
+ const extra = [...flags.keys()].filter((key) => key !== "message" && key !== "all" && key !== "&push");
1085
+ if (extra.length > 0 || message === undefined || message.trim() === "")
1086
+ throw new Error('usage: gith save -m <message> [--all] ["--&push"]');
1087
+ currentBranch();
935
1088
  const unstaged = captureArgs(["add", "-A", "--dry-run"]).split(`
936
1089
  `).filter(Boolean);
937
- const stageAll = flags.has("all") || unstaged.length === 1 || unstaged.length > 1 && await confirm(`Stage all ${unstaged.length} changes first? [Y/n] `);
1090
+ const stageAll = flags.has("all") || unstaged.length === 1;
938
1091
  if (stageAll && unstaged.length > 0) {
939
- console.log(`Staging ${unstaged.length} unstaged change(s):`);
940
- for (const line of unstaged)
941
- console.log(` ${line}`);
1092
+ process.stdout.write(`Staging ${unstaged.length} unstaged change(s):
1093
+ ${unstaged.join(`
1094
+ `)}
1095
+ `);
942
1096
  }
943
1097
  if (stageAll)
944
1098
  gitArgs(["add", "-A"]);
945
- if (captureArgs(["diff", "--cached", "--name-only"]) === "") {
946
- throw new Error("nothing staged; stage the changes to save.");
1099
+ if (captureArgs(["diff", "--cached", "--name-only"]) === "" && !(isDryRun() && stageAll && unstaged.length > 0)) {
1100
+ throw new Error("nothing staged; stage the changes with git add, or use gith save -m <message> --all.");
947
1101
  }
948
- const previous = captureArgs(["rev-parse", "HEAD"]);
949
- const amend = hasOwnCommit() && await confirm("Amend the previous commit? [Y/n] ");
950
- if (amend) {
951
- gitArgs(message ? ["commit", "--amend", "-m", message] : ["commit", "--amend", "--no-edit"]);
952
- } else {
953
- gitArgs(["commit", "-m", message ?? await askMessage()]);
954
- }
955
- const rewrites = amend && remoteSha(branch) === previous;
956
- const question = rewrites ? `Force-push ${branch} to origin? [Y/n] ` : `Push ${branch} to origin? [Y/n] `;
957
- if (!await confirm(question)) {
958
- console.log("Saved locally; not pushed.");
959
- return;
960
- }
961
- if (rewrites)
962
- gitArgs(["push", "--force-with-lease", "origin", branch]);
963
- else
964
- gitArgs(["push", "-u", "origin", branch]);
965
- }
1102
+ gitArgs(["commit", "-m", message]);
1103
+ process.stdout.write(isDryRun() ? `Would save locally.
1104
+ ` : `Saved locally.
1105
+ `);
1106
+ if (flags.has("&push"))
1107
+ push(new Map);
1108
+ };
966
1109
  async function squash(flags) {
967
1110
  const count = parseLimit(flags);
968
1111
  const message = flags.get("message");
@@ -1004,37 +1147,70 @@ async function squash(flags) {
1004
1147
  else
1005
1148
  gitArgs(["commit", "-C", oldest]);
1006
1149
  }
1007
- function hasOwnCommit() {
1008
- for (const base of ["origin/develop", "origin/main"]) {
1009
- try {
1010
- return captureArgs(["rev-list", "--count", `${base}..HEAD`]) !== "0";
1011
- } catch {}
1012
- }
1013
- return true;
1014
- }
1150
+
1151
+ // src/components/version/version.constants.ts
1152
+ var MASCOT = [
1153
+ " o",
1154
+ " o | o",
1155
+ " \\ | /",
1156
+ " \\ | /",
1157
+ " \\ | /",
1158
+ " \\|/",
1159
+ " |",
1160
+ " .---.",
1161
+ " ( o o )",
1162
+ " '---'",
1163
+ " |",
1164
+ " /|\\",
1165
+ " / \\"
1166
+ ];
1015
1167
 
1016
1168
  // src/components/list/list.constants.ts
1017
1169
  var DEFAULT_LIMIT2 = 5;
1018
- var GORLUM = [
1019
- ' .-"""-.',
1020
- " / o o \\",
1021
- " | ___ | my precious pockets!",
1022
- " \\ (___) /",
1023
- " '-._.-'"
1170
+ var GRAPH_ARGS = [
1171
+ "-c",
1172
+ "log.graphColors=cyan,magenta,blue,yellow,green,red",
1173
+ "log",
1174
+ "--graph",
1175
+ "--topo-order",
1176
+ "--decorate=short",
1177
+ "--date=format:%Y-%m-%d %H:%M",
1178
+ "--format=%C(bold yellow)%h%C(reset) %C(bold)%s%C(reset) %C(auto)%d%C(reset)%n%C(dim) └─ %an · %ad%C(reset)%n"
1024
1179
  ];
1025
1180
 
1026
1181
  // src/components/list/list.utils.ts
1027
1182
  function yesNo(value) {
1028
1183
  return value ? "yes" : "no";
1029
1184
  }
1030
- function commits(limit, graph) {
1185
+ var commits = (limit, graph) => {
1031
1186
  if (!hasHead()) {
1032
- console.log("No commits.");
1187
+ process.stdout.write(`No commits.
1188
+ `);
1033
1189
  return;
1034
1190
  }
1035
- const shape = graph ? ["--graph", "--oneline", "--decorate"] : ["--date=short", "--format=%h %ad %an %s"];
1036
- console.log(captureArgs(["log", `-n${limit}`, ...shape]));
1037
- }
1191
+ if (!graph) {
1192
+ process.stdout.write(`${captureArgs(["log", `-n${limit}`, "--date=short", "--format=%h %ad %an %s"])}
1193
+ `);
1194
+ return;
1195
+ }
1196
+ const { NO_COLOR: noColor, TERM: terminal } = process.env;
1197
+ const color = process.stdout.isTTY && noColor === undefined && terminal !== "dumb";
1198
+ const branch = captureArgs(["rev-parse", "--abbrev-ref", "HEAD"]);
1199
+ process.stdout.write(`
1200
+ ╭─ COMMIT GRAPH ─────────────────────────────────────────
1201
+ │ ${branch} · branch history · limit ${limit}
1202
+ │
1203
+ `);
1204
+ process.stdout.write(`${captureArgs([
1205
+ ...GRAPH_ARGS,
1206
+ `-n${limit}`,
1207
+ color ? "--color=always" : "--color=never"
1208
+ ])}
1209
+ `);
1210
+ process.stdout.write(`╰─ * commit · branches & tags in parentheses · -l <n> shows more
1211
+
1212
+ `);
1213
+ };
1038
1214
  function hasHead() {
1039
1215
  try {
1040
1216
  captureArgs(["rev-parse", "--verify", "--quiet", "HEAD"]);
@@ -1064,6 +1240,7 @@ function tagPairs(local, origin) {
1064
1240
  });
1065
1241
  }
1066
1242
  function list(flags) {
1243
+ assertAllowedFlags(flags, ["tags", "commits", "stashes", "pockets", "graph", "limit"], "list");
1067
1244
  const kind = onlyOne(flags, ["tags", "commits", "stashes", "pockets"]);
1068
1245
  if (!kind)
1069
1246
  throw new Error("usage: gith list --tags | --commits [--graph] | --stashes | --pockets [-limit <n>]");
@@ -1083,10 +1260,12 @@ function localTags() {
1083
1260
  return captureArgs(["tag", "--list"]).split(`
1084
1261
  `).filter(Boolean);
1085
1262
  }
1086
- function stashes(limit, withGorlum) {
1087
- if (withGorlum)
1088
- console.log(GORLUM.join(`
1089
- `));
1263
+ var stashes = (limit, withMascot) => {
1264
+ if (withMascot)
1265
+ process.stdout.write(`${MASCOT.join(`
1266
+ `)}
1267
+ my precious pockets!
1268
+ `);
1090
1269
  const rows = captureArgs([
1091
1270
  "stash",
1092
1271
  "list",
@@ -1095,7 +1274,9 @@ function stashes(limit, withGorlum) {
1095
1274
  ]).split(`
1096
1275
  `).filter(Boolean);
1097
1276
  if (rows.length === 0) {
1098
- console.log(withGorlum ? "There is nothing in my pockets!" : "No stashes.");
1277
+ process.stdout.write(withMascot ? `There is nothing in my pockets!
1278
+ ` : `No stashes.
1279
+ `);
1099
1280
  return;
1100
1281
  }
1101
1282
  const shown = rows.slice(0, limit);
@@ -1104,7 +1285,7 @@ function stashes(limit, withGorlum) {
1104
1285
  if (shown.length < rows.length) {
1105
1286
  console.log(`showing ${shown.length} of ${rows.length}; -limit <n> shows more`);
1106
1287
  }
1107
- }
1288
+ };
1108
1289
  function tags(limit) {
1109
1290
  const pairs = tagPairs(localTags(), originTags());
1110
1291
  if (pairs.length === 0) {
@@ -1196,6 +1377,7 @@ async function switchBranch(flags) {
1196
1377
  switchToCommit(commit);
1197
1378
  return;
1198
1379
  }
1380
+ assertAllowedFlags(flags, ["branch"], "switch");
1199
1381
  const branch = requireBranch(flags, "switch");
1200
1382
  const from = isDetached() ? "HEAD" : undefined;
1201
1383
  const exists = (name) => hasLocal(name) || hasOnOrigin(name);
@@ -1262,6 +1444,16 @@ function detachedOptions(times, current) {
1262
1444
  return options.sort((x, y) => x.name.localeCompare(y.name));
1263
1445
  }
1264
1446
  async function remove(flags) {
1447
+ assertAllowedFlags(flags, [
1448
+ "branch",
1449
+ "local",
1450
+ "remote",
1451
+ "obsolete",
1452
+ "detached",
1453
+ "days",
1454
+ "all",
1455
+ "limit"
1456
+ ], "remove");
1265
1457
  const name = flags.get("branch") ?? "";
1266
1458
  const side = onlyOne(flags, ["local", "remote"]);
1267
1459
  const scope = side ?? "pair";
@@ -1571,23 +1763,26 @@ function npmVersion(kind) {
1571
1763
  }
1572
1764
 
1573
1765
  // src/components/release/release.ts
1574
- function release(kind) {
1766
+ var release = (kind, flags) => {
1575
1767
  if (!kind || !KINDS.includes(kind))
1576
- throw new Error("usage: gith release <patch|minor|major>");
1768
+ throw new Error("usage: gith release --patch | --minor | --major [--update-version --npm]");
1769
+ if (flags.has("update-version") !== flags.has("npm"))
1770
+ throw new Error("Version updates require both --update-version and --npm; pass both flags or omit both.");
1577
1771
  assertCleanTree();
1578
1772
  if (kind === "patch") {
1579
- releasePatch();
1773
+ releasePatch(flags.has("update-version"));
1580
1774
  } else {
1581
- releaseFromDevelop(kind);
1775
+ releaseFromDevelop(kind, flags.has("update-version"));
1582
1776
  }
1583
- }
1584
- function releasePatch() {
1777
+ };
1778
+ var releasePatch = (shouldUpdateVersion) => {
1585
1779
  console.log(`
1586
1780
  \uD83D\uDCCB Starting patch release from main...
1587
1781
  `);
1588
1782
  git("checkout main");
1589
1783
  git("pull");
1590
- npmVersion("patch");
1784
+ if (shouldUpdateVersion)
1785
+ npmVersion("patch");
1591
1786
  git("push --follow-tags");
1592
1787
  git("checkout develop");
1593
1788
  git("pull");
@@ -1598,8 +1793,8 @@ function releasePatch() {
1598
1793
  console.log(`
1599
1794
  ✅ Patch release complete!
1600
1795
  `);
1601
- }
1602
- function releaseFromDevelop(kind) {
1796
+ };
1797
+ var releaseFromDevelop = (kind, shouldUpdateVersion) => {
1603
1798
  console.log(`
1604
1799
  \uD83D\uDCCB Starting ${kind} release from develop...
1605
1800
  `);
@@ -1607,7 +1802,8 @@ function releaseFromDevelop(kind) {
1607
1802
  if (current !== "develop")
1608
1803
  throw new Error(`must be on 'develop', currently on '${current}'.`);
1609
1804
  git("pull");
1610
- npmVersion(kind);
1805
+ if (shouldUpdateVersion)
1806
+ npmVersion(kind);
1611
1807
  git("push --follow-tags");
1612
1808
  git("checkout main");
1613
1809
  git("pull");
@@ -1615,10 +1811,120 @@ function releaseFromDevelop(kind) {
1615
1811
  git("push");
1616
1812
  git("checkout develop");
1617
1813
  git("pull origin main");
1814
+ git("push");
1618
1815
  assertSynced(["main", "develop"]);
1619
1816
  console.log(`
1620
1817
  ✅ ${kind} release complete!
1621
1818
  `);
1819
+ };
1820
+
1821
+ // src/components/setup/setup.ts
1822
+ import {
1823
+ chmodSync,
1824
+ existsSync as existsSync5,
1825
+ mkdirSync,
1826
+ readFileSync as readFileSync3,
1827
+ writeFileSync as writeFileSync2
1828
+ } from "node:fs";
1829
+ import path4 from "node:path";
1830
+
1831
+ // src/components/setup/setup.constants.ts
1832
+ var HOOKS_DIR = ".githooks";
1833
+ var HOOK_FILE = `${HOOKS_DIR}/pre-commit`;
1834
+ var HOOKS_PATH_COMMAND = `git config core.hooksPath ${HOOKS_DIR}`;
1835
+ var LOCKFILES = [
1836
+ ["bun.lock", "bun"],
1837
+ ["bun.lockb", "bun"],
1838
+ ["pnpm-lock.yaml", "pnpm"],
1839
+ ["yarn.lock", "yarn"],
1840
+ ["package-lock.json", "npm"]
1841
+ ];
1842
+ var RUN_PREFIX = {
1843
+ bun: "bun run",
1844
+ pnpm: "pnpm run",
1845
+ yarn: "yarn",
1846
+ npm: "npm run"
1847
+ };
1848
+
1849
+ // src/components/setup/setup.utils.ts
1850
+ import { existsSync as existsSync4 } from "node:fs";
1851
+ import path3 from "node:path";
1852
+ function checkBlock(manager, script, label) {
1853
+ return `echo "▶ ${label}"
1854
+ ${RUN_PREFIX[manager]} ${script}
1855
+
1856
+ `;
1857
+ }
1858
+ function buildPreCommitHook(pkg, manager, isTypeScript) {
1859
+ const checks = [
1860
+ isTypeScript || pkg.scripts?.lint ? checkBlock(manager, "lint", "Lint") : "",
1861
+ isTypeScript || pkg.scripts?.typecheck ? checkBlock(manager, "typecheck", "Typecheck") : "",
1862
+ pkg.scripts?.test ? checkBlock(manager, "test", "Test") : ""
1863
+ ].join("");
1864
+ return `#!/usr/bin/env sh
1865
+ # Generated by gith setup -n npm - runs on every commit via core.hooksPath
1866
+ set -e
1867
+
1868
+ ${checks}`;
1869
+ }
1870
+ function detectPackageManager(projectPath) {
1871
+ const lockfile = LOCKFILES.find(([file]) => existsSync4(path3.join(projectPath, file)));
1872
+ return lockfile?.[1] ?? "npm";
1873
+ }
1874
+ function isTypeScriptProject(projectPath, pkg) {
1875
+ if (existsSync4(path3.join(projectPath, "tsconfig.json")))
1876
+ return true;
1877
+ return Boolean(pkg.dependencies?.typescript || pkg.devDependencies?.typescript);
1878
+ }
1879
+ function withPrepareScript(pkg) {
1880
+ return { ...pkg, scripts: { ...pkg.scripts, prepare: HOOKS_PATH_COMMAND } };
1881
+ }
1882
+
1883
+ // src/components/setup/setup.ts
1884
+ var SETUP_USAGE = "usage: gith setup -n npm [--force]";
1885
+ function setup(flags) {
1886
+ const unknown = [...flags.keys()].filter((key) => key !== "name" && key !== "force");
1887
+ if (unknown.length > 0 || flags.get("name") === undefined) {
1888
+ throw new Error(SETUP_USAGE);
1889
+ }
1890
+ const target = flags.get("name");
1891
+ if (target !== "npm") {
1892
+ throw new Error(`unknown setup '${target}': only npm is supported. ${SETUP_USAGE}`);
1893
+ }
1894
+ setupNpmHook({ force: flags.has("force") });
1895
+ }
1896
+ function setupNpmHook({ force }) {
1897
+ const root = process.cwd();
1898
+ if (!existsSync5(path4.join(root, ".git"))) {
1899
+ throw new Error("not a git repository root: run it in the folder that holds .git");
1900
+ }
1901
+ const packagePath = path4.join(root, "package.json");
1902
+ if (!existsSync5(packagePath))
1903
+ throw new Error("no package.json in this folder");
1904
+ const hookPath = path4.join(root, HOOK_FILE);
1905
+ if (existsSync5(hookPath) && !force) {
1906
+ throw new Error(`${HOOK_FILE} already exists. Pass --force to overwrite it.`);
1907
+ }
1908
+ const pkg = JSON.parse(readFileSync3(packagePath, "utf8"));
1909
+ const manager = detectPackageManager(root);
1910
+ const typescript = isTypeScriptProject(root, pkg);
1911
+ const hook = buildPreCommitHook(pkg, manager, typescript);
1912
+ if (isDryRun()) {
1913
+ console.log(`dry run: would write ${HOOK_FILE} (package manager: ${manager})`);
1914
+ return;
1915
+ }
1916
+ mkdirSync(path4.join(root, HOOKS_DIR), { recursive: true });
1917
+ writeFileSync2(hookPath, hook);
1918
+ try {
1919
+ chmodSync(hookPath, 493);
1920
+ } catch {}
1921
+ gitArgs(["config", "core.hooksPath", HOOKS_DIR]);
1922
+ if (pkg.scripts?.prepare !== HOOKS_PATH_COMMAND) {
1923
+ writeFileSync2(packagePath, `${JSON.stringify(withPrepareScript(pkg), null, 2)}
1924
+ `);
1925
+ console.log(`✏️ Added "prepare" → "${HOOKS_PATH_COMMAND}" to package.json`);
1926
+ }
1927
+ console.log(`✅ Wrote ${HOOK_FILE} and set core.hooksPath (package manager: ${manager}${typescript ? ", TypeScript" : ""})`);
1622
1928
  }
1623
1929
 
1624
1930
  // src/components/stash/stash.ts
@@ -1819,6 +2125,7 @@ function table(rows) {
1819
2125
  `);
1820
2126
  }
1821
2127
  function stat(flags) {
2128
+ assertAllowedFlags(flags, [...PERIODS, "limit"], "stat");
1822
2129
  const period = onlyOne(flags, PERIODS) ?? "overall";
1823
2130
  const limit = parseLimit(flags);
1824
2131
  const range = periodRange(period, new Date);
@@ -1878,19 +2185,32 @@ function parseStatus(output) {
1878
2185
  }
1879
2186
  return status;
1880
2187
  }
1881
- function status(flags) {
2188
+ var status = (flags) => {
1882
2189
  if (flags.size > 0)
1883
2190
  throw new Error("usage: gith status");
1884
2191
  const info = parseStatus(captureArgs(["status", "--porcelain=v2", "--branch"]));
1885
2192
  const against = info.upstream ? `${info.branch} -> ${info.upstream} ahead ${info.ahead}, behind ${info.behind}` : `${info.branch} (no upstream)`;
1886
- console.log(against);
2193
+ process.stdout.write(`${against}
2194
+ `);
1887
2195
  const changed = info.staged + info.unstaged + info.untracked + info.conflicts;
1888
- if (changed === 0) {
1889
- console.log("working tree clean");
1890
- return;
2196
+ process.stdout.write(changed === 0 ? `working tree clean
2197
+ ` : `staged ${info.staged}, unstaged ${info.unstaged}, untracked ${info.untracked}, conflicts ${info.conflicts}
2198
+ `);
2199
+ for (const [heading, scope] of [
2200
+ ["Local branches (* current, + other worktree)", "--list"],
2201
+ ["Remote branches (last fetched)", "--remotes"]
2202
+ ]) {
2203
+ const branches = captureArgs(["branch", "--no-color", scope]);
2204
+ process.stdout.write(`
2205
+ ${heading}:
2206
+ `);
2207
+ process.stdout.write(branches.length === 0 ? ` (none)
2208
+ ` : `${branches.split(`
2209
+ `).map((branch) => ` ${branch.trim()}`).join(`
2210
+ `)}
2211
+ `);
1891
2212
  }
1892
- console.log(`staged ${info.staged}, unstaged ${info.unstaged}, untracked ${info.untracked}, conflicts ${info.conflicts}`);
1893
- }
2213
+ };
1894
2214
 
1895
2215
  // src/components/tree/tree.utils.ts
1896
2216
  function flagsOf(worktree) {
@@ -1958,34 +2278,22 @@ function tree(flags) {
1958
2278
  }
1959
2279
 
1960
2280
  // src/components/version/version.ts
1961
- import { readFileSync as readFileSync4 } from "node:fs";
1962
-
1963
- // src/components/version/version.constants.ts
1964
- var MASCOT = [
1965
- " o",
1966
- " /",
1967
- " o----o----o",
1968
- " \\ | /",
1969
- " \\ | /",
1970
- " (o_o)",
1971
- " /|_|\\",
1972
- " d b"
1973
- ];
2281
+ import { readFileSync as readFileSync5 } from "node:fs";
1974
2282
 
1975
2283
  // src/components/version/version.utils.ts
1976
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
1977
- import path2 from "node:path";
2284
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
2285
+ import path5 from "node:path";
1978
2286
  import { fileURLToPath } from "node:url";
1979
2287
  function packageJsonPath() {
1980
- let dir = path2.dirname(fileURLToPath(import.meta.url));
2288
+ let dir = path5.dirname(fileURLToPath(import.meta.url));
1981
2289
  for (;; ) {
1982
- const candidate = path2.join(dir, "package.json");
1983
- if (existsSync3(candidate)) {
1984
- const pkg = JSON.parse(readFileSync3(candidate, "utf8"));
2290
+ const candidate = path5.join(dir, "package.json");
2291
+ if (existsSync6(candidate)) {
2292
+ const pkg = JSON.parse(readFileSync4(candidate, "utf8"));
1985
2293
  if (pkg.name === "@tertium/gith")
1986
2294
  return candidate;
1987
2295
  }
1988
- const parent = path2.dirname(dir);
2296
+ const parent = path5.dirname(dir);
1989
2297
  if (parent === dir)
1990
2298
  throw new Error("gith package.json not found");
1991
2299
  dir = parent;
@@ -1994,12 +2302,23 @@ function packageJsonPath() {
1994
2302
 
1995
2303
  // src/components/version/version.ts
1996
2304
  function readVersion() {
1997
- const pkg = JSON.parse(readFileSync4(packageJsonPath(), "utf8"));
2305
+ const pkg = JSON.parse(readFileSync5(packageJsonPath(), "utf8"));
1998
2306
  return pkg.version ?? "unknown";
1999
2307
  }
2000
2308
  function versionText() {
2001
2309
  const version = readVersion();
2002
- const side = ["", "", "", "", "", `gith v${version}`, "git happens.", ""];
2310
+ const side = [
2311
+ "",
2312
+ "",
2313
+ "",
2314
+ "",
2315
+ "",
2316
+ "",
2317
+ "",
2318
+ "",
2319
+ `gith v${version}`,
2320
+ "git happens."
2321
+ ];
2003
2322
  return MASCOT.map((line, i) => `${line.padEnd(16)}${side[i] ?? ""}`.trimEnd()).join(`
2004
2323
  `);
2005
2324
  }
@@ -2011,24 +2330,26 @@ Start and finish work
2011
2330
  gith create -b <branch> work branch develop--<branch> from develop (no switch)
2012
2331
  gith create -r <version> release/<version> from develop
2013
2332
  gith create -h <version> hotfix/<version> from main
2014
- gith create --release | --hotfix version from the latest tag: minor for release, patch for hotfix
2333
+ gith create --release | --hotfix next version from the latest tag
2334
+ gith create -r <version> --update-version --npm also commit npm version on the new branch; works with -h, --release, --hotfix
2015
2335
  gith switch list local and origin branches, and switch to the one picked
2016
2336
  gith switch -b <branch> switch to a branch, creating develop--<branch> from develop if missing (on a detached HEAD: <branch> from that commit)
2017
2337
  gith switch -commit <sha> detach HEAD at a commit, to look at it (tracked files must be clean)
2018
- gith switch --release | --hotfix switch to the newest release/* or hotfix/* (local or origin); -r / -h do the same; -r <v> / -h <v> for one version
2338
+ gith switch --release | --hotfix switch to the newest release/* or hotfix/* (local or origin); -r <v> | -h <v> for one version
2019
2339
  gith update merge origin/develop into this work branch
2020
2340
  gith finish --current-release | --current-hotfix release the newest release/* or hotfix/*: version, changelog, merge to main, tag, back to develop
2021
2341
  gith merge --develop merge this branch into develop
2022
2342
  gith merge --main merge this branch into main
2023
2343
  gith merge pick the branch to merge, then the target (main, develop, release/*, hotfix/*)
2024
- gith merge [-r <v> | -h <v>] merge this branch into release/<v> or hotfix/<v>; without a version, the only one or a pick from the list
2344
+ gith merge --release | --hotfix merge into the only matching container, or pick one
2345
+ gith merge -r <v> | -h <v> merge into release/<v> or hotfix/<v>
2025
2346
  gith merge --current-release merge this branch into the newest release (local or origin)
2026
2347
  gith merge --current-hotfix merge this branch into the newest hotfix (local or origin)
2027
- gith release --patch | --minor | --major bump the version and publish
2348
+ gith release --patch | --minor | --major sync release branches; add --update-version --npm to bump npm version
2028
2349
 
2029
2350
  Fix history
2030
2351
  gith undo undo the last commit, keep its changes staged (not if pushed)
2031
- gith save [-m <msg>] [--all] commit staged changes locally; stages unstaged changes first (one silently, several after asking; --all without asking), then asks to amend the previous commit (else asks for a message), then asks to push, or to force-push after an amend
2352
+ gith save -m <msg> [--all] ["--&push"] new commit without prompts; push only with "--&push"
2032
2353
  gith squash -limit <n> [-m <msg>] combine the last n commits into one (keeps the oldest message)
2033
2354
 
2034
2355
  Save and reset work
@@ -2051,24 +2372,27 @@ Clean up branches (every removal shows what it will remove first)
2051
2372
  gith rm alias of remove
2052
2373
 
2053
2374
  Look at the repo
2054
- gith status branch vs origin, and staged, unstaged and untracked counts
2055
- gith list --commits [--graph] [-l <n>] newest commits on this branch (default 5)
2375
+ gith status working-tree summary, all local and remote-tracking branches
2376
+ gith list --commits [--graph] [-l <n>] newest commits on this branch; --graph adds colored lanes, refs, author/date (default 5)
2056
2377
  gith list --tags [-l <n>] tags with a local and origin column (newest 5 by default)
2057
- gith list --stashes [-l <n>] stashes, newest first (default 5); --pockets shows them with Gorlum
2378
+ gith list --stashes [-l <n>] stashes, newest first (default 5); --pockets shows them with the gith mascot
2058
2379
  gith show ... same as gith list
2059
2380
  gith tree [-l <n>] working trees: path, branch, HEAD (current marked *)
2060
2381
  gith find|search [-commit <text>] [-file <pattern>] [-change <text>] [--regex] [-limit <n>]
2061
2382
  newest commits on this branch matching every filter (default 5)
2062
- gith stat [period] [-limit <n>] fun facts about this branch: biggest commit, busiest day, streaks
2063
- period: --overall (default), --current-year|-month|-week, --last-year|-month|-week
2383
+ gith stat [--overall | --current-year | --current-month | --current-week | --last-year | --last-month | --last-week] [-limit <n>]
2384
+ fun facts about this branch: biggest commit, busiest day, streaks
2064
2385
 
2065
2386
  Remote and health
2387
+ gith push [--all-branches] push to origin, set tracking, report ahead/behind; never force
2388
+ gith pull [--all-branches] fast-forward from origin; refuse diverged histories
2066
2389
  gith sync fetch all remotes and tags, prune stale refs, then sync this branch with origin both ways
2067
2390
  gith sync --develop | --main the same for develop or main
2068
2391
  gith sync develop:develop two-way sync of one branch with origin: fetch it when behind, push it when ahead, stop when diverged
2069
2392
  gith open open origin in the browser
2070
2393
  gith doctor check the repo is ready for the branch flow (exit 1 on failures)
2071
2394
  gith changelog [--dry-run] add an entry for package.json's version to CHANGELOG.md (commits since the last tag)
2395
+ gith setup -n npm [--force] write a shared pre-commit hook (lint, typecheck, test) for an npm project; --force overwrites it
2072
2396
 
2073
2397
  Help
2074
2398
  gith help show this help
@@ -2077,10 +2401,13 @@ Help
2077
2401
  --dry-run (any command) print the git commands that would run, without running them
2078
2402
  --yes (any command) answer yes to every question (for scripts)
2079
2403
 
2080
- Flags: a single dash always takes a value, a double dash never does.
2081
- -b -r -h -l -m -n -limit -message -name -release -hotfix -branch take a value, e.g. -limit 10
2082
- --patch --minor --major --local --remote --obsolete --detached --tags --commits --graph --stashes --pockets switches
2083
- --branch --release --hotfix alone pick automatically (picker, latest tag)
2404
+ Flags: every single-dash flag requires a value; double-dash flags never take one.
2405
+ -b -r -h -l -m -n -limit -message -name -branch -release -hotfix -commit -file -change -days take values
2406
+ --patch --minor --major --local --remote --obsolete --detached --tags --commits --graph --stashes --pockets --branch --regex
2407
+ --develop --main --current-release --current-hotfix --all --&push --all-branches --no-commit --force
2408
+ --update-version --npm release or create release/hotfix: opt in to npm version updates (both required)
2409
+ --overall --current-year --current-month --current-week --last-year --last-month --last-week switches
2410
+ --branch lists removable branches; --release / --hotfix choose a version or branch by command context
2084
2411
  -limit caps listings and stat (or -l); -days sets the age for remove --obsolete`;
2085
2412
  var args = process.argv.slice(2).filter((arg) => {
2086
2413
  if (arg === "--verbose")
@@ -2137,7 +2464,13 @@ try {
2137
2464
  undo(parseFlags(rest));
2138
2465
  break;
2139
2466
  case "save":
2140
- await save(parseFlags(rest));
2467
+ save(parseFlags(rest));
2468
+ break;
2469
+ case "push":
2470
+ push(parseFlags(rest));
2471
+ break;
2472
+ case "pull":
2473
+ pull(parseFlags(rest));
2141
2474
  break;
2142
2475
  case "squash":
2143
2476
  await squash(parseFlags(rest));
@@ -2161,6 +2494,9 @@ try {
2161
2494
  case "changelog":
2162
2495
  changelog(parseFlags(rest));
2163
2496
  break;
2497
+ case "setup":
2498
+ setup(parseFlags(rest));
2499
+ break;
2164
2500
  case "doctor":
2165
2501
  if (!doctor(parseFlags(rest)))
2166
2502
  exit(1);
@@ -2173,10 +2509,11 @@ try {
2173
2509
  break;
2174
2510
  case "release": {
2175
2511
  const flags = parseFlags(rest);
2512
+ assertAllowedFlags(flags, ["patch", "minor", "major", "update-version", "npm"], "release");
2176
2513
  const kind = onlyOne(flags, ["patch", "minor", "major"]);
2177
2514
  if (!kind)
2178
- throw new Error("usage: gith release --patch | --minor | --major");
2179
- release(kind);
2515
+ throw new Error("usage: gith release --patch | --minor | --major [--update-version --npm]");
2516
+ release(kind, flags);
2180
2517
  break;
2181
2518
  }
2182
2519
  case "--version":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tertium/gith",
3
- "version": "0.3.3",
3
+ "version": "0.5.0",
4
4
  "description": "Git helper: branch flow with goal branches, release and hotfix containers, and everyday git commands.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,10 +19,10 @@
19
19
  "format:check": "biome format src",
20
20
  "typecheck": "tsc --noEmit",
21
21
  "prepare": "git config core.hooksPath .githooks || true",
22
- "release:patch": "node bin/gith.js release --patch",
23
- "release:minor": "node bin/gith.js release --minor",
24
- "release:major": "node bin/gith.js release --major",
25
- "version": "bun run build && node bin/gith.js changelog"
22
+ "release:patch": "node bin/gith.js release --patch --update-version --npm",
23
+ "release:minor": "node bin/gith.js release --minor --update-version --npm",
24
+ "release:major": "node bin/gith.js release --major --update-version --npm",
25
+ "version": "bun run build && node bin/gith.js changelog --no-commit && git add -- CHANGELOG.md"
26
26
  },
27
27
  "license": "MIT",
28
28
  "author": "Vitalii Balabanov",