@tertium/gith 0.4.0 → 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 +33 -16
  2. package/bin/gith.js +170 -79
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -1,22 +1,31 @@
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
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).
17
24
 
18
25
  See [Command design rules](docs/RULES.md) before changing or adding commands and flags.
19
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.
28
+
20
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.
21
30
 
22
31
  ## Usage
@@ -28,6 +37,8 @@ gith create -r 1.2.0 # release/1.2.0 from develop
28
37
  gith create -h 1.2.1 # hotfix/1.2.1 from main
29
38
  gith create --release # next release version from the latest tag
30
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
31
42
  gith switch # list local and origin branches, and switch to the one you pick
32
43
  gith switch -b login-redirect # switch to develop--login-redirect, creating it from develop if it is missing
33
44
  gith switch -commit 3f2a1b9 # detach HEAD at that commit, to look at old code
@@ -44,9 +55,9 @@ gith merge --release # this branch into the only release, or pick
44
55
  gith merge -h 1.2.1 # this branch into hotfix/1.2.1
45
56
  gith merge --current-release # this branch into the newest release/* (local or origin)
46
57
  gith merge --current-hotfix # this branch into the newest hotfix/* (local or origin)
47
- gith release --patch # npm version patch, published through main
48
- gith release --minor # npm version minor, from develop into main
49
- 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
50
61
 
51
62
  # Fix history
52
63
  gith undo # undo the last commit, keep its changes staged (not if pushed)
@@ -75,14 +86,14 @@ gith remove --detached # pick local branches that have no origin co
75
86
  gith remove --detached --all # remove all of them: lists them, then asks Y/n
76
87
 
77
88
  # Look at the repo
78
- gith status # branch vs origin, and staged, unstaged and untracked counts
89
+ gith status # working-tree summary, all local and remote-tracking branches
79
90
  gith list --commits # the last 5 commits on this branch
80
91
  gith list --commits --graph # the same, as a branch graph
81
92
  gith list --commits -l 10 # the last 10 (-limit 10 works too)
82
93
  gith list --tags # the newest 5 tags with a LOCAL and ORIGIN column
83
94
  gith list --tags -l 20 # the newest 20
84
95
  gith list --stashes # the newest 5 stashes (-l for more)
85
- gith show --pockets # the same, with Gorlum: my precious pockets!
96
+ gith show --pockets # the same, with the gith mascot: my precious pockets!
86
97
  gith find -commit "login" -limit 10 # commits whose message mentions login (search is the same; default 5)
87
98
  gith search -file "*api.ts" -limit 3 # commits that changed a matching file, with the files listed
88
99
  gith find -change "TODO" # commits that added or removed the text in their diff
@@ -138,6 +149,7 @@ One rule applies to every command: **a single dash always takes a value, and a d
138
149
  | `-file <pattern>`, `-change <text>` | value | `find` |
139
150
  | `-l <n>`, `-limit <n>` | value, positive whole number | every listing: `list`, `tree`, the picker |
140
151
  | `--patch`, `--minor`, `--major` | switch | `release` |
152
+ | `--update-version`, `--npm` | switches, required together for npm version updates | `release`, `create` with `-r`/`-h`/`--release`/`--hotfix` |
141
153
  | `--overall`, `--current-year`, `--current-month`, `--current-week`, `--last-year`, `--last-month`, `--last-week` | switch, one at a time | `stat` |
142
154
  | `--local`, `--remote` | switch | `remove` (both sides when neither is given) |
143
155
  | `--obsolete`, `--detached` | switch | `remove` |
@@ -160,7 +172,7 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
160
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.
161
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.
162
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.
163
- - **`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.
164
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.
165
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.
166
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.
@@ -171,7 +183,7 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
171
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.
172
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`).
173
185
  - `-limit <n>` sets how many to show (default 5). The filters combine with AND.
174
- - **`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/`.
175
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.
176
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.
177
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.
@@ -195,6 +207,7 @@ Commands that choose between flags (for example `create -b`, `-r` and `-h`, or `
195
207
 
196
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.
197
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.
198
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.
199
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.
200
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.
@@ -207,10 +220,14 @@ Containers (`develop`, `main`, `release/*`, `hotfix/*`) can't be merged from, an
207
220
 
208
221
  Run as `gith release --patch`, `--minor` or `--major`.
209
222
 
210
- - **`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.
211
- - **`minor` and `major`** run from `develop`: bump the version with its changelog entry, 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.
212
229
 
213
- 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.
214
231
 
215
232
  ## Errors
216
233
 
package/bin/gith.js CHANGED
@@ -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"],
@@ -449,7 +451,7 @@ function find(flags) {
449
451
 
450
452
  // src/components/flow/flow.ts
451
453
  import { execSync } from "node:child_process";
452
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
454
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
453
455
 
454
456
  // src/components/prompt/prompt.ts
455
457
  import { emitKeypressEvents } from "node:readline";
@@ -604,6 +606,16 @@ function nextVersion(tags, bump) {
604
606
  }
605
607
 
606
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";
607
619
  function containers(kind) {
608
620
  const prefix = `${kind}/`;
609
621
  const local = captureArgs([
@@ -640,31 +652,77 @@ function nextContainerVersion(kind) {
640
652
  console.log(`Next ${kind} version from existing tags: ${version}`);
641
653
  return version;
642
654
  }
643
- 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.");
644
658
  const name = containerName(kind, version);
659
+ assertBranchName(name);
660
+ if (hasLocal(name))
661
+ throw new Error(`'${name}' already exists locally.`);
645
662
  if (hasOnOrigin(name))
646
663
  throw new Error(`'${name}' already exists on origin.`);
647
664
  const base = containerBase(kind);
648
665
  gitArgs(["fetch", "origin", base]);
666
+ const needsVersionUpdate = shouldUpdateVersion && parsePackageVersion(captureArgs(["show", `origin/${base}:package.json`])) !== version;
649
667
  gitArgs(["branch", name, `origin/${base}`]);
668
+ if (needsVersionUpdate)
669
+ updateContainerVersion(name, version);
650
670
  gitArgs(["push", "-u", "origin", name]);
651
- }
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
+ };
652
706
 
653
707
  // src/components/flow/flow.ts
654
- function create(flags) {
655
- assertAllowedFlags(flags, ["branch", "release", "hotfix"], "create");
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.");
656
712
  const kind = onlyOne(flags, ["branch", "release", "hotfix"]);
657
713
  if (!kind)
658
- throw new Error("usage: gith create -b <branch> | -r <version> | -h <version> | --release | --hotfix");
714
+ throw new Error("usage: gith create -b <branch> | (-r <version> | -h <version> | --release | --hotfix) [--update-version --npm]");
659
715
  const value = flags.get(kind) ?? "";
660
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.");
661
719
  if (!value)
662
720
  throw new Error("usage: gith create -b <branch>");
663
721
  createBranch(value);
664
722
  return;
665
723
  }
666
- open(kind, value || nextContainerVersion(kind));
667
- }
724
+ open(kind, value || nextContainerVersion(kind), flags.has("update-version"));
725
+ };
668
726
  async function merge(flags) {
669
727
  assertAllowedFlags(flags, [
670
728
  "develop",
@@ -819,7 +877,7 @@ async function mergePick() {
819
877
  await mergeInto(target, source);
820
878
  }
821
879
  function alignVersion(version) {
822
- if (!existsSync2("package.json"))
880
+ if (!existsSync3("package.json"))
823
881
  return;
824
882
  const current = parsePackageVersion(readFileSync2("package.json", "utf8"));
825
883
  if (current === version)
@@ -835,10 +893,10 @@ function alignVersion(version) {
835
893
  gitArgs(["commit", "-m", version, "--", "package.json"]);
836
894
  }
837
895
  function addChangelogEntry(version) {
838
- if (!existsSync2("package.json"))
896
+ if (!existsSync3("package.json"))
839
897
  return;
840
898
  const file = "CHANGELOG.md";
841
- if (existsSync2(file) && readFileSync2(file, "utf8").includes(`## ${version} - `))
899
+ if (existsSync3(file) && readFileSync2(file, "utf8").includes(`## ${version} - `))
842
900
  return;
843
901
  changelog(new Map);
844
902
  }
@@ -1090,6 +1148,23 @@ async function squash(flags) {
1090
1148
  gitArgs(["commit", "-C", oldest]);
1091
1149
  }
1092
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
+ ];
1167
+
1093
1168
  // src/components/list/list.constants.ts
1094
1169
  var DEFAULT_LIMIT2 = 5;
1095
1170
  var GRAPH_ARGS = [
@@ -1102,13 +1177,6 @@ var GRAPH_ARGS = [
1102
1177
  "--date=format:%Y-%m-%d %H:%M",
1103
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"
1104
1179
  ];
1105
- var GORLUM = [
1106
- ' .-"""-.',
1107
- " / o o \\",
1108
- " | ___ | my precious pockets!",
1109
- " \\ (___) /",
1110
- " '-._.-'"
1111
- ];
1112
1180
 
1113
1181
  // src/components/list/list.utils.ts
1114
1182
  function yesNo(value) {
@@ -1192,10 +1260,12 @@ function localTags() {
1192
1260
  return captureArgs(["tag", "--list"]).split(`
1193
1261
  `).filter(Boolean);
1194
1262
  }
1195
- function stashes(limit, withGorlum) {
1196
- if (withGorlum)
1197
- console.log(GORLUM.join(`
1198
- `));
1263
+ var stashes = (limit, withMascot) => {
1264
+ if (withMascot)
1265
+ process.stdout.write(`${MASCOT.join(`
1266
+ `)}
1267
+ my precious pockets!
1268
+ `);
1199
1269
  const rows = captureArgs([
1200
1270
  "stash",
1201
1271
  "list",
@@ -1204,7 +1274,9 @@ function stashes(limit, withGorlum) {
1204
1274
  ]).split(`
1205
1275
  `).filter(Boolean);
1206
1276
  if (rows.length === 0) {
1207
- 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
+ `);
1208
1280
  return;
1209
1281
  }
1210
1282
  const shown = rows.slice(0, limit);
@@ -1213,7 +1285,7 @@ function stashes(limit, withGorlum) {
1213
1285
  if (shown.length < rows.length) {
1214
1286
  console.log(`showing ${shown.length} of ${rows.length}; -limit <n> shows more`);
1215
1287
  }
1216
- }
1288
+ };
1217
1289
  function tags(limit) {
1218
1290
  const pairs = tagPairs(localTags(), originTags());
1219
1291
  if (pairs.length === 0) {
@@ -1691,23 +1763,26 @@ function npmVersion(kind) {
1691
1763
  }
1692
1764
 
1693
1765
  // src/components/release/release.ts
1694
- function release(kind) {
1766
+ var release = (kind, flags) => {
1695
1767
  if (!kind || !KINDS.includes(kind))
1696
- 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.");
1697
1771
  assertCleanTree();
1698
1772
  if (kind === "patch") {
1699
- releasePatch();
1773
+ releasePatch(flags.has("update-version"));
1700
1774
  } else {
1701
- releaseFromDevelop(kind);
1775
+ releaseFromDevelop(kind, flags.has("update-version"));
1702
1776
  }
1703
- }
1704
- function releasePatch() {
1777
+ };
1778
+ var releasePatch = (shouldUpdateVersion) => {
1705
1779
  console.log(`
1706
1780
  \uD83D\uDCCB Starting patch release from main...
1707
1781
  `);
1708
1782
  git("checkout main");
1709
1783
  git("pull");
1710
- npmVersion("patch");
1784
+ if (shouldUpdateVersion)
1785
+ npmVersion("patch");
1711
1786
  git("push --follow-tags");
1712
1787
  git("checkout develop");
1713
1788
  git("pull");
@@ -1718,8 +1793,8 @@ function releasePatch() {
1718
1793
  console.log(`
1719
1794
  ✅ Patch release complete!
1720
1795
  `);
1721
- }
1722
- function releaseFromDevelop(kind) {
1796
+ };
1797
+ var releaseFromDevelop = (kind, shouldUpdateVersion) => {
1723
1798
  console.log(`
1724
1799
  \uD83D\uDCCB Starting ${kind} release from develop...
1725
1800
  `);
@@ -1727,7 +1802,8 @@ function releaseFromDevelop(kind) {
1727
1802
  if (current !== "develop")
1728
1803
  throw new Error(`must be on 'develop', currently on '${current}'.`);
1729
1804
  git("pull");
1730
- npmVersion(kind);
1805
+ if (shouldUpdateVersion)
1806
+ npmVersion(kind);
1731
1807
  git("push --follow-tags");
1732
1808
  git("checkout main");
1733
1809
  git("pull");
@@ -1735,21 +1811,22 @@ function releaseFromDevelop(kind) {
1735
1811
  git("push");
1736
1812
  git("checkout develop");
1737
1813
  git("pull origin main");
1814
+ git("push");
1738
1815
  assertSynced(["main", "develop"]);
1739
1816
  console.log(`
1740
1817
  ✅ ${kind} release complete!
1741
1818
  `);
1742
- }
1819
+ };
1743
1820
 
1744
1821
  // src/components/setup/setup.ts
1745
1822
  import {
1746
1823
  chmodSync,
1747
- existsSync as existsSync4,
1824
+ existsSync as existsSync5,
1748
1825
  mkdirSync,
1749
1826
  readFileSync as readFileSync3,
1750
1827
  writeFileSync as writeFileSync2
1751
1828
  } from "node:fs";
1752
- import path3 from "node:path";
1829
+ import path4 from "node:path";
1753
1830
 
1754
1831
  // src/components/setup/setup.constants.ts
1755
1832
  var HOOKS_DIR = ".githooks";
@@ -1770,8 +1847,8 @@ var RUN_PREFIX = {
1770
1847
  };
1771
1848
 
1772
1849
  // src/components/setup/setup.utils.ts
1773
- import { existsSync as existsSync3 } from "node:fs";
1774
- import path2 from "node:path";
1850
+ import { existsSync as existsSync4 } from "node:fs";
1851
+ import path3 from "node:path";
1775
1852
  function checkBlock(manager, script, label) {
1776
1853
  return `echo "▶ ${label}"
1777
1854
  ${RUN_PREFIX[manager]} ${script}
@@ -1791,11 +1868,11 @@ set -e
1791
1868
  ${checks}`;
1792
1869
  }
1793
1870
  function detectPackageManager(projectPath) {
1794
- const lockfile = LOCKFILES.find(([file]) => existsSync3(path2.join(projectPath, file)));
1871
+ const lockfile = LOCKFILES.find(([file]) => existsSync4(path3.join(projectPath, file)));
1795
1872
  return lockfile?.[1] ?? "npm";
1796
1873
  }
1797
1874
  function isTypeScriptProject(projectPath, pkg) {
1798
- if (existsSync3(path2.join(projectPath, "tsconfig.json")))
1875
+ if (existsSync4(path3.join(projectPath, "tsconfig.json")))
1799
1876
  return true;
1800
1877
  return Boolean(pkg.dependencies?.typescript || pkg.devDependencies?.typescript);
1801
1878
  }
@@ -1818,14 +1895,14 @@ function setup(flags) {
1818
1895
  }
1819
1896
  function setupNpmHook({ force }) {
1820
1897
  const root = process.cwd();
1821
- if (!existsSync4(path3.join(root, ".git"))) {
1898
+ if (!existsSync5(path4.join(root, ".git"))) {
1822
1899
  throw new Error("not a git repository root: run it in the folder that holds .git");
1823
1900
  }
1824
- const packagePath = path3.join(root, "package.json");
1825
- if (!existsSync4(packagePath))
1901
+ const packagePath = path4.join(root, "package.json");
1902
+ if (!existsSync5(packagePath))
1826
1903
  throw new Error("no package.json in this folder");
1827
- const hookPath = path3.join(root, HOOK_FILE);
1828
- if (existsSync4(hookPath) && !force) {
1904
+ const hookPath = path4.join(root, HOOK_FILE);
1905
+ if (existsSync5(hookPath) && !force) {
1829
1906
  throw new Error(`${HOOK_FILE} already exists. Pass --force to overwrite it.`);
1830
1907
  }
1831
1908
  const pkg = JSON.parse(readFileSync3(packagePath, "utf8"));
@@ -1836,7 +1913,7 @@ function setupNpmHook({ force }) {
1836
1913
  console.log(`dry run: would write ${HOOK_FILE} (package manager: ${manager})`);
1837
1914
  return;
1838
1915
  }
1839
- mkdirSync(path3.join(root, HOOKS_DIR), { recursive: true });
1916
+ mkdirSync(path4.join(root, HOOKS_DIR), { recursive: true });
1840
1917
  writeFileSync2(hookPath, hook);
1841
1918
  try {
1842
1919
  chmodSync(hookPath, 493);
@@ -2108,19 +2185,32 @@ function parseStatus(output) {
2108
2185
  }
2109
2186
  return status;
2110
2187
  }
2111
- function status(flags) {
2188
+ var status = (flags) => {
2112
2189
  if (flags.size > 0)
2113
2190
  throw new Error("usage: gith status");
2114
2191
  const info = parseStatus(captureArgs(["status", "--porcelain=v2", "--branch"]));
2115
2192
  const against = info.upstream ? `${info.branch} -> ${info.upstream} ahead ${info.ahead}, behind ${info.behind}` : `${info.branch} (no upstream)`;
2116
- console.log(against);
2193
+ process.stdout.write(`${against}
2194
+ `);
2117
2195
  const changed = info.staged + info.unstaged + info.untracked + info.conflicts;
2118
- if (changed === 0) {
2119
- console.log("working tree clean");
2120
- 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
+ `);
2121
2212
  }
2122
- console.log(`staged ${info.staged}, unstaged ${info.unstaged}, untracked ${info.untracked}, conflicts ${info.conflicts}`);
2123
- }
2213
+ };
2124
2214
 
2125
2215
  // src/components/tree/tree.utils.ts
2126
2216
  function flagsOf(worktree) {
@@ -2190,32 +2280,20 @@ function tree(flags) {
2190
2280
  // src/components/version/version.ts
2191
2281
  import { readFileSync as readFileSync5 } from "node:fs";
2192
2282
 
2193
- // src/components/version/version.constants.ts
2194
- var MASCOT = [
2195
- " o",
2196
- " /",
2197
- " o----o----o",
2198
- " \\ | /",
2199
- " \\ | /",
2200
- " (o_o)",
2201
- " /|_|\\",
2202
- " d b"
2203
- ];
2204
-
2205
2283
  // src/components/version/version.utils.ts
2206
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
2207
- import path4 from "node:path";
2284
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
2285
+ import path5 from "node:path";
2208
2286
  import { fileURLToPath } from "node:url";
2209
2287
  function packageJsonPath() {
2210
- let dir = path4.dirname(fileURLToPath(import.meta.url));
2288
+ let dir = path5.dirname(fileURLToPath(import.meta.url));
2211
2289
  for (;; ) {
2212
- const candidate = path4.join(dir, "package.json");
2213
- if (existsSync5(candidate)) {
2290
+ const candidate = path5.join(dir, "package.json");
2291
+ if (existsSync6(candidate)) {
2214
2292
  const pkg = JSON.parse(readFileSync4(candidate, "utf8"));
2215
2293
  if (pkg.name === "@tertium/gith")
2216
2294
  return candidate;
2217
2295
  }
2218
- const parent = path4.dirname(dir);
2296
+ const parent = path5.dirname(dir);
2219
2297
  if (parent === dir)
2220
2298
  throw new Error("gith package.json not found");
2221
2299
  dir = parent;
@@ -2229,7 +2307,18 @@ function readVersion() {
2229
2307
  }
2230
2308
  function versionText() {
2231
2309
  const version = readVersion();
2232
- 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
+ ];
2233
2322
  return MASCOT.map((line, i) => `${line.padEnd(16)}${side[i] ?? ""}`.trimEnd()).join(`
2234
2323
  `);
2235
2324
  }
@@ -2242,6 +2331,7 @@ Start and finish work
2242
2331
  gith create -r <version> release/<version> from develop
2243
2332
  gith create -h <version> hotfix/<version> from main
2244
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
2245
2335
  gith switch list local and origin branches, and switch to the one picked
2246
2336
  gith switch -b <branch> switch to a branch, creating develop--<branch> from develop if missing (on a detached HEAD: <branch> from that commit)
2247
2337
  gith switch -commit <sha> detach HEAD at a commit, to look at it (tracked files must be clean)
@@ -2255,7 +2345,7 @@ Start and finish work
2255
2345
  gith merge -r <v> | -h <v> merge into release/<v> or hotfix/<v>
2256
2346
  gith merge --current-release merge this branch into the newest release (local or origin)
2257
2347
  gith merge --current-hotfix merge this branch into the newest hotfix (local or origin)
2258
- 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
2259
2349
 
2260
2350
  Fix history
2261
2351
  gith undo undo the last commit, keep its changes staged (not if pushed)
@@ -2282,10 +2372,10 @@ Clean up branches (every removal shows what it will remove first)
2282
2372
  gith rm alias of remove
2283
2373
 
2284
2374
  Look at the repo
2285
- gith status branch vs origin, and staged, unstaged and untracked counts
2375
+ gith status working-tree summary, all local and remote-tracking branches
2286
2376
  gith list --commits [--graph] [-l <n>] newest commits on this branch; --graph adds colored lanes, refs, author/date (default 5)
2287
2377
  gith list --tags [-l <n>] tags with a local and origin column (newest 5 by default)
2288
- 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
2289
2379
  gith show ... same as gith list
2290
2380
  gith tree [-l <n>] working trees: path, branch, HEAD (current marked *)
2291
2381
  gith find|search [-commit <text>] [-file <pattern>] [-change <text>] [--regex] [-limit <n>]
@@ -2315,6 +2405,7 @@ Flags: every single-dash flag requires a value; double-dash flags never take one
2315
2405
  -b -r -h -l -m -n -limit -message -name -branch -release -hotfix -commit -file -change -days take values
2316
2406
  --patch --minor --major --local --remote --obsolete --detached --tags --commits --graph --stashes --pockets --branch --regex
2317
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)
2318
2409
  --overall --current-year --current-month --current-week --last-year --last-month --last-week switches
2319
2410
  --branch lists removable branches; --release / --hotfix choose a version or branch by command context
2320
2411
  -limit caps listings and stat (or -l); -days sets the age for remove --obsolete`;
@@ -2418,11 +2509,11 @@ try {
2418
2509
  break;
2419
2510
  case "release": {
2420
2511
  const flags = parseFlags(rest);
2421
- assertAllowedFlags(flags, ["patch", "minor", "major"], "release");
2512
+ assertAllowedFlags(flags, ["patch", "minor", "major", "update-version", "npm"], "release");
2422
2513
  const kind = onlyOne(flags, ["patch", "minor", "major"]);
2423
2514
  if (!kind)
2424
- throw new Error("usage: gith release --patch | --minor | --major");
2425
- release(kind);
2515
+ throw new Error("usage: gith release --patch | --minor | --major [--update-version --npm]");
2516
+ release(kind, flags);
2426
2517
  break;
2427
2518
  }
2428
2519
  case "--version":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tertium/gith",
3
- "version": "0.4.0",
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,9 +19,9 @@
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",
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
25
  "version": "bun run build && node bin/gith.js changelog --no-commit && git add -- CHANGELOG.md"
26
26
  },
27
27
  "license": "MIT",