gflows 1.1.1 → 1.2.1
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.
- package/AGENTS.md +6 -2
- package/CHANGELOG.md +18 -0
- package/README.md +12 -2
- package/llms.txt +3 -1
- package/package.json +1 -1
- package/src/commands/bump.ts +6 -170
- package/src/commands/completion.ts +27 -0
- package/src/commands/finish.ts +113 -74
- package/src/commands/help.ts +3 -1
- package/src/commands/mcp.ts +35 -1
- package/src/commands/release.ts +280 -0
- package/src/commands/schema.ts +13 -0
- package/src/interactive.ts +23 -0
- package/src/parse.ts +19 -6
- package/src/recommend.ts +2 -1
- package/src/tui/HubHome.tsx +2 -1
- package/src/tui/HubShell.tsx +16 -0
- package/src/tui/flows.tsx +54 -0
- package/src/tui/slash.ts +1 -0
- package/src/types.ts +7 -0
- package/src/version-bump.ts +236 -0
package/AGENTS.md
CHANGED
|
@@ -9,7 +9,7 @@ gflows is a **local Git branching workflow CLI** (main + dev + typed short-lived
|
|
|
9
9
|
3. **Do not finish empty branches** — if there are no commits beyond the merge target, finish exits `2`. Commit first.
|
|
10
10
|
4. **Conflicts** — resolve files, then `gflows continue`. Or `gflows abort` / `gflows undo`.
|
|
11
11
|
5. **Discover the API** — run `gflows schema` (JSON) or read this file + README.
|
|
12
|
-
6. **MCP** — `gflows mcp` (stdio JSON-RPC) exposes status/doctor/info/list/start/sync/finish/schema tools.
|
|
12
|
+
6. **MCP** — `gflows mcp` (stdio JSON-RPC) exposes status/doctor/info/list/start/sync/finish/release/schema tools.
|
|
13
13
|
7. **Hub / viz** — bare `gflows` (TTY) opens an Ink fullscreen hub (`/` commands). Prompts use Clack. `gflows viz` prints the scrollback status panel.
|
|
14
14
|
|
|
15
15
|
## Exit codes
|
|
@@ -39,13 +39,17 @@ gflows start bugfix hotfix-login -o main
|
|
|
39
39
|
# … commit …
|
|
40
40
|
gflows finish bugfix -y -p # merges main, then main→dev
|
|
41
41
|
|
|
42
|
-
# Release
|
|
42
|
+
# Release (branch workflow)
|
|
43
43
|
gflows bump up minor
|
|
44
44
|
# commit version files, or: gflows finish release --bump …
|
|
45
45
|
gflows start release v1.2.0
|
|
46
46
|
# … changelog / commits …
|
|
47
47
|
gflows finish release -y -p
|
|
48
48
|
|
|
49
|
+
# Quick release from dev (bump or keep version + merge main + tag, no release/* branch)
|
|
50
|
+
gflows release up patch -y -p # or -P; must be on dev
|
|
51
|
+
gflows release current -y -p # after a manual bump; tags package version as-is
|
|
52
|
+
|
|
49
53
|
# Stuck
|
|
50
54
|
gflows doctor --json
|
|
51
55
|
gflows continue
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.1] - 2026-07-25
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Quick release can keep the package version already on disk: `gflows release current -y -p` (or choose **keep current** in the hub / interactive prompt)
|
|
15
|
+
- MCP `gflows_release` accepts `keepCurrent: true` as an alternative to `bumpType`
|
|
16
|
+
|
|
17
|
+
## [1.2.0] - 2026-07-25
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `gflows release` — quick release from `dev`: bump package version, merge into `main`, tag, sync `main` → `dev` (`up patch|minor|major`, `-y`, `-p`/`-P`)
|
|
22
|
+
- Hub `/release` wizard and MCP tool `gflows_release`
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- `gflows bump` (and finish `--bump`) update `package.json` version in place so tab/space formatting is preserved (Biome-friendly)
|
|
27
|
+
|
|
10
28
|
## [1.1.1] - 2026-07-25
|
|
11
29
|
|
|
12
30
|
### Fixed
|
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ Then:
|
|
|
49
49
|
g # or: gflows
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
You get a fullscreen menu: what’s next, branch map, actions. **Select** an item (or type `/start`, `/finish`, `/sync`, …). Follow-up questions (type, name, push, rebase) stay **inside the Ink UI** — no drop-out to a separate prompt tool. Only the git command itself runs on the main screen, then you press enter to return.
|
|
52
|
+
You get a fullscreen menu: what’s next, branch map, actions. **Select** an item (or type `/start`, `/finish`, `/release`, `/sync`, …). Follow-up questions (type, name, push, rebase) stay **inside the Ink UI** — no drop-out to a separate prompt tool. Only the git command itself runs on the main screen, then you press enter to return.
|
|
53
53
|
|
|
54
54
|
```
|
|
55
55
|
❯ ★ Start new work
|
|
@@ -130,6 +130,7 @@ main ← release / hotfix only (production)
|
|
|
130
130
|
| **dev** | Integration (created by `init` from main) |
|
|
131
131
|
| **feature / bugfix / chore / spike** | Day-to-day work → merge to **dev** |
|
|
132
132
|
| **release / hotfix** | Versioned (`vX.Y.Z`) → **main**, then **main → dev**, + tag |
|
|
133
|
+
| **quick release** (`gflows release`) | From **dev** only: bump or keep → **main** → tag → sync **dev** |
|
|
133
134
|
|
|
134
135
|
|
|
135
136
|
**Lifecycle:** `init` → `start` → commit → (`sync`) → (`pr`) → `finish`
|
|
@@ -216,6 +217,14 @@ Hub: `g` → select the action. Typed short form assumes `alias g=gflows`.
|
|
|
216
217
|
| `gflows start release v1.4.0` | `g -S -r v1.4.0` |
|
|
217
218
|
| `gflows finish release -y -p` | `g -F -r -y -p` |
|
|
218
219
|
|
|
220
|
+
**Quick release from `dev`** — merge **main**, tag, sync **main → dev** (no `release/*` branch). Choose a bump, or keep a version you already set manually.
|
|
221
|
+
|
|
222
|
+
| Long | Short |
|
|
223
|
+
|------|-------|
|
|
224
|
+
| `gflows release up patch -y -p` | — (bump, then ship) |
|
|
225
|
+
| `gflows release up minor -y -p` | — |
|
|
226
|
+
| `gflows release current -y -p` | — (keep `package.json` version; no bump commit) |
|
|
227
|
+
|
|
219
228
|
**Hotfix**
|
|
220
229
|
|
|
221
230
|
| Long | Short |
|
|
@@ -247,7 +256,7 @@ Bare `gflows` / `g` in a **TTY** opens the Ink hub (see [Way 1](#way-1--hub-reco
|
|
|
247
256
|
| Input | Action |
|
|
248
257
|
| ------------------------------------------ | -------------------------------------- |
|
|
249
258
|
| ↑↓ Enter | Select an action — Ink wizard if needed |
|
|
250
|
-
| `/start` `/finish` `/sync` …
|
|
259
|
+
| `/start` `/finish` `/release` `/sync` … | Slash command; wizards stay in the hub |
|
|
251
260
|
| `?` | Shortcut help |
|
|
252
261
|
| `q` / Esc / Ctrl+C | Quit |
|
|
253
262
|
|
|
@@ -267,6 +276,7 @@ Bare `gflows` / `g` in a **TTY** opens the Ink hub (see [Way 1](#way-1--hub-reco
|
|
|
267
276
|
| `init` `-I` | Ensure main; create dev |
|
|
268
277
|
| `start` `-S` | Create typed branch |
|
|
269
278
|
| `finish` `-F` | Merge + close (plan; delete default **on**) |
|
|
279
|
+
| `release` | Quick release from `dev` (bump/keep + main + tag)|
|
|
270
280
|
| `sync` | Update branch from its base |
|
|
271
281
|
| `pr` | Open PR/MR (`gh` / `glab`) |
|
|
272
282
|
| `switch` `-W` | Switch branch |
|
package/llms.txt
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
- `gflows` (TTY): Ink fullscreen hub (frame, tips, actions, `/` slash commands, status bar); Clack prompts
|
|
14
14
|
- `gflows viz`: scrollback status panel — lifecycle, branches, recommended next step
|
|
15
|
-
- `gflows init` / `start` / `finish` / `sync` / `pr`
|
|
15
|
+
- `gflows init` / `start` / `finish` / `release` / `sync` / `pr`
|
|
16
|
+
- `gflows release up patch -y -p` — quick ship from `dev` (bump + merge main + tag)
|
|
17
|
+
- `gflows release current -y -p` — same, but keep the package version already on disk (after a manual bump)
|
|
16
18
|
- `gflows continue` / `undo` / `abort` — recovery after conflicts
|
|
17
19
|
- `gflows doctor` / `info` / `config` / `status --json` / `mcp`
|
|
18
20
|
|
package/package.json
CHANGED
package/src/commands/bump.ts
CHANGED
|
@@ -5,142 +5,10 @@
|
|
|
5
5
|
* @module commands/bump
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { join, relative } from "node:path";
|
|
10
8
|
import { EXIT_OK, EXIT_USER } from "../constants.js";
|
|
11
|
-
import { InvalidVersionError } from "../errors.js";
|
|
12
9
|
import { hint, success } from "../out.js";
|
|
13
|
-
import { findPackageRoots, PACKAGE_JSON } from "../packages.js";
|
|
14
10
|
import type { BumpDirection, BumpType, ParsedArgs } from "../types.js";
|
|
15
|
-
|
|
16
|
-
const JSR_JSON = "jsr.json";
|
|
17
|
-
|
|
18
|
-
/** Semver triplet. */
|
|
19
|
-
interface Semver {
|
|
20
|
-
major: number;
|
|
21
|
-
minor: number;
|
|
22
|
-
patch: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Parses a version string (vX.Y.Z or X.Y.Z) into components.
|
|
27
|
-
* @throws InvalidVersionError if format is invalid
|
|
28
|
-
*/
|
|
29
|
-
function parseVersion(version: string): Semver {
|
|
30
|
-
const trimmed = version.trim();
|
|
31
|
-
const normalized = trimmed.startsWith("v") ? trimmed.slice(1) : trimmed;
|
|
32
|
-
const parts = normalized.split(".");
|
|
33
|
-
if (parts.length !== 3 || parts.some((p) => !/^\d+$/.test(p))) {
|
|
34
|
-
throw new InvalidVersionError(
|
|
35
|
-
`Invalid version '${version}'. Expected format: X.Y.Z or vX.Y.Z (e.g. 1.2.3 or v1.2.3).`,
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
const [p0, p1, p2] = parts;
|
|
39
|
-
return {
|
|
40
|
-
major: parseInt(p0 ?? "0", 10),
|
|
41
|
-
minor: parseInt(p1 ?? "0", 10),
|
|
42
|
-
patch: parseInt(p2 ?? "0", 10),
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Formats semver as string (no leading v, for package.json).
|
|
48
|
-
*/
|
|
49
|
-
function formatVersion(semver: Semver): string {
|
|
50
|
-
return `${semver.major}.${semver.minor}.${semver.patch}`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Computes new version for bump up (patch/minor/major).
|
|
55
|
-
*/
|
|
56
|
-
function bumpUp(semver: Semver, type: BumpType): Semver {
|
|
57
|
-
switch (type) {
|
|
58
|
-
case "patch":
|
|
59
|
-
return { ...semver, patch: semver.patch + 1 };
|
|
60
|
-
case "minor":
|
|
61
|
-
return { major: semver.major, minor: semver.minor + 1, patch: 0 };
|
|
62
|
-
case "major":
|
|
63
|
-
return { major: semver.major + 1, minor: 0, patch: 0 };
|
|
64
|
-
default:
|
|
65
|
-
return semver;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Computes new version for rollback (down), with floor at 0.
|
|
71
|
-
*/
|
|
72
|
-
function bumpDown(semver: Semver, type: BumpType): Semver {
|
|
73
|
-
switch (type) {
|
|
74
|
-
case "patch":
|
|
75
|
-
return {
|
|
76
|
-
...semver,
|
|
77
|
-
patch: Math.max(0, semver.patch - 1),
|
|
78
|
-
};
|
|
79
|
-
case "minor":
|
|
80
|
-
return {
|
|
81
|
-
major: semver.major,
|
|
82
|
-
minor: Math.max(0, semver.minor - 1),
|
|
83
|
-
patch: 0,
|
|
84
|
-
};
|
|
85
|
-
case "major":
|
|
86
|
-
return {
|
|
87
|
-
major: Math.max(0, semver.major - 1),
|
|
88
|
-
minor: semver.minor,
|
|
89
|
-
patch: semver.patch,
|
|
90
|
-
};
|
|
91
|
-
default:
|
|
92
|
-
return semver;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Reads version from package.json in dir.
|
|
98
|
-
* @throws InvalidVersionError if version is missing or invalid
|
|
99
|
-
*/
|
|
100
|
-
function readPackageVersion(dir: string): { raw: string; semver: Semver } {
|
|
101
|
-
const path = join(dir, PACKAGE_JSON);
|
|
102
|
-
if (!existsSync(path)) {
|
|
103
|
-
throw new InvalidVersionError(
|
|
104
|
-
`No package.json found at ${path}. Run from project root or use -C <dir>.`,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
const raw = readFileSync(path, "utf-8");
|
|
108
|
-
const data = JSON.parse(raw) as Record<string, unknown>;
|
|
109
|
-
const version = data.version;
|
|
110
|
-
if (typeof version !== "string" || version.trim() === "") {
|
|
111
|
-
throw new InvalidVersionError(
|
|
112
|
-
'package.json has no valid \'version\' field. Add a "version" field (e.g. "0.0.0") to package.json.',
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
const semver = parseVersion(version);
|
|
116
|
-
return { raw: version.trim(), semver };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Writes version to package.json, preserving other keys and formatting as much as possible.
|
|
121
|
-
* Uses JSON.stringify with 2 spaces for consistency.
|
|
122
|
-
*/
|
|
123
|
-
function writePackageVersion(dir: string, newVersion: string): void {
|
|
124
|
-
const path = join(dir, PACKAGE_JSON);
|
|
125
|
-
const raw = readFileSync(path, "utf-8");
|
|
126
|
-
const data = JSON.parse(raw) as Record<string, unknown>;
|
|
127
|
-
data.version = newVersion;
|
|
128
|
-
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Updates version in jsr.json if the file exists. Only the version value is changed;
|
|
133
|
-
* the rest of the file (format, commas, spacing) is left unchanged.
|
|
134
|
-
*/
|
|
135
|
-
function syncJsrVersion(dir: string, newVersion: string): boolean {
|
|
136
|
-
const path = join(dir, JSR_JSON);
|
|
137
|
-
if (!existsSync(path)) return false;
|
|
138
|
-
const raw = readFileSync(path, "utf-8");
|
|
139
|
-
const updated = raw.replace(/"version":\s*"[^"]*"/, `"version": "${newVersion}"`);
|
|
140
|
-
if (updated === raw) return false;
|
|
141
|
-
writeFileSync(path, updated, "utf-8");
|
|
142
|
-
return true;
|
|
143
|
-
}
|
|
11
|
+
import { applyVersionToPackages, computeBump } from "../version-bump.js";
|
|
144
12
|
|
|
145
13
|
/**
|
|
146
14
|
* Run the bump command.
|
|
@@ -182,34 +50,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
182
50
|
});
|
|
183
51
|
}
|
|
184
52
|
|
|
185
|
-
|
|
186
|
-
roots = [...roots].sort((a, b) => {
|
|
187
|
-
if (a === cwd && b !== cwd) return -1;
|
|
188
|
-
if (a !== cwd && b === cwd) return 1;
|
|
189
|
-
return a.localeCompare(b);
|
|
190
|
-
});
|
|
191
|
-
if (roots.length === 0) {
|
|
192
|
-
throw new InvalidVersionError(
|
|
193
|
-
`No package.json found under ${cwd}. Run from project root or use -C <dir>.`,
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
const primaryRoot = roots[0];
|
|
197
|
-
if (primaryRoot === undefined) {
|
|
198
|
-
throw new InvalidVersionError(
|
|
199
|
-
`No package.json found under ${cwd}. Run from project root or use -C <dir>.`,
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
const { raw: oldVersion, semver } = readPackageVersion(primaryRoot);
|
|
203
|
-
const newSemver = direction === "up" ? bumpUp(semver, type) : bumpDown(semver, type);
|
|
204
|
-
const newVersion = formatVersion(newSemver);
|
|
205
|
-
|
|
206
|
-
const filesToUpdate: string[] = [];
|
|
207
|
-
for (const dir of roots) {
|
|
208
|
-
filesToUpdate.push(relative(cwd, join(dir, PACKAGE_JSON)) || PACKAGE_JSON);
|
|
209
|
-
if (existsSync(join(dir, JSR_JSON))) {
|
|
210
|
-
filesToUpdate.push(relative(cwd, join(dir, JSR_JSON)) || JSR_JSON);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
53
|
+
const { oldVersion, newVersion, roots, filesToUpdate } = computeBump(cwd, direction, type);
|
|
213
54
|
|
|
214
55
|
if (dryRun) {
|
|
215
56
|
if (!quiet) {
|
|
@@ -219,18 +60,13 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
219
60
|
process.exit(EXIT_OK);
|
|
220
61
|
}
|
|
221
62
|
|
|
222
|
-
const updated
|
|
223
|
-
for (const dir of roots) {
|
|
224
|
-
writePackageVersion(dir, newVersion);
|
|
225
|
-
updated.push(relative(cwd, join(dir, PACKAGE_JSON)) || PACKAGE_JSON);
|
|
226
|
-
if (syncJsrVersion(dir, newVersion)) {
|
|
227
|
-
updated.push(relative(cwd, join(dir, JSR_JSON)) || JSR_JSON);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
63
|
+
const updated = applyVersionToPackages(cwd, roots, newVersion);
|
|
230
64
|
|
|
231
65
|
if (!quiet) {
|
|
232
66
|
success(`Bumped version: ${oldVersion} → ${newVersion}`);
|
|
233
67
|
success(`Updated: ${updated.join(", ")}`);
|
|
234
|
-
hint(
|
|
68
|
+
hint(
|
|
69
|
+
"Commit the change, then run gflows start release vX.Y.Z — or gflows release up <type> from dev.",
|
|
70
|
+
);
|
|
235
71
|
}
|
|
236
72
|
}
|
|
@@ -12,6 +12,7 @@ const COMMANDS = [
|
|
|
12
12
|
"init",
|
|
13
13
|
"start",
|
|
14
14
|
"finish",
|
|
15
|
+
"release",
|
|
15
16
|
"sync",
|
|
16
17
|
"pr",
|
|
17
18
|
"viz",
|
|
@@ -132,6 +133,15 @@ _gflows() {
|
|
|
132
133
|
COMPREPLY=()
|
|
133
134
|
fi
|
|
134
135
|
;;
|
|
136
|
+
release)
|
|
137
|
+
if (( cword == cmd_idx + 1 )); then
|
|
138
|
+
COMPREPLY=($(compgen -W "up current" -- "$cur"))
|
|
139
|
+
elif (( cword == cmd_idx + 2 )) && [[ "${D}words[cmd_idx + 1]}" == "up" ]]; then
|
|
140
|
+
COMPREPLY=($(compgen -W "${BUMP_TYPES.join(" ")}" -- "$cur"))
|
|
141
|
+
else
|
|
142
|
+
COMPREPLY=()
|
|
143
|
+
fi
|
|
144
|
+
;;
|
|
135
145
|
*)
|
|
136
146
|
COMPREPLY=()
|
|
137
147
|
;;
|
|
@@ -212,6 +222,13 @@ _gflows() {
|
|
|
212
222
|
_values "direction" ${BUMP_DIRECTIONS.map((d) => `"${d}"`).join(" ")}
|
|
213
223
|
fi
|
|
214
224
|
;;
|
|
225
|
+
release)
|
|
226
|
+
if [[ "${D}words[CURRENT-1]}" == "up" ]]; then
|
|
227
|
+
_values "bump-type" ${BUMP_TYPES.map((t) => `"${t}"`).join(" ")}
|
|
228
|
+
else
|
|
229
|
+
_values "mode" "up" "current"
|
|
230
|
+
fi
|
|
231
|
+
;;
|
|
215
232
|
esac
|
|
216
233
|
;;
|
|
217
234
|
esac
|
|
@@ -253,6 +270,8 @@ complete -c gflows -f -n "not __fish_seen_subcommand_from ${COMMANDS.join(" ")}"
|
|
|
253
270
|
-a "start" -d "Create workflow branch"
|
|
254
271
|
complete -c gflows -f -n "not __fish_seen_subcommand_from ${COMMANDS.join(" ")}" \\
|
|
255
272
|
-a "finish" -d "Merge and close branch"
|
|
273
|
+
complete -c gflows -f -n "not __fish_seen_subcommand_from ${COMMANDS.join(" ")}" \\
|
|
274
|
+
-a "release" -d "Quick release from dev"
|
|
256
275
|
complete -c gflows -f -n "not __fish_seen_subcommand_from ${COMMANDS.join(" ")}" \\
|
|
257
276
|
-a "switch" -d "Switch branch"
|
|
258
277
|
complete -c gflows -f -n "not __fish_seen_subcommand_from ${COMMANDS.join(" ")}" \\
|
|
@@ -315,6 +334,14 @@ complete -c gflows -f -n "__fish_seen_subcommand_from bump; and not __fish_seen_
|
|
|
315
334
|
complete -c gflows -f -n "__fish_seen_subcommand_from bump; and __fish_seen_subcommand_from up down" \\
|
|
316
335
|
-a "patch minor major"
|
|
317
336
|
|
|
337
|
+
# release up [patch|minor|major] | current
|
|
338
|
+
complete -c gflows -f -n "__fish_seen_subcommand_from release; and not __fish_seen_subcommand_from up current" \\
|
|
339
|
+
-a "up" -d "Bump then release"
|
|
340
|
+
complete -c gflows -f -n "__fish_seen_subcommand_from release; and not __fish_seen_subcommand_from up current" \\
|
|
341
|
+
-a "current" -d "Keep current version (no bump)"
|
|
342
|
+
complete -c gflows -f -n "__fish_seen_subcommand_from release; and __fish_seen_subcommand_from up" \\
|
|
343
|
+
-a "patch minor major"
|
|
344
|
+
|
|
318
345
|
# Common flags
|
|
319
346
|
complete -c gflows -x -n "__fish_seen_subcommand_from ${COMMANDS.join(" ")}" -l path -s C -d "Run as if in dir" -a "(__fish_complete_directories)"
|
|
320
347
|
complete -c gflows -f -n "__fish_seen_subcommand_from ${COMMANDS.join(" ")}" -l help -s h -d "Show help"
|
package/src/commands/finish.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
tagExists,
|
|
38
38
|
} from "../git.js";
|
|
39
39
|
import { hint, success } from "../out.js";
|
|
40
|
+
import { PACKAGE_JSON } from "../packages.js";
|
|
40
41
|
import {
|
|
41
42
|
completeRun,
|
|
42
43
|
type GflowsRunState,
|
|
@@ -44,9 +45,11 @@ import {
|
|
|
44
45
|
suspendRun,
|
|
45
46
|
writeActiveRun,
|
|
46
47
|
} from "../run-state.js";
|
|
47
|
-
import type { BranchType, MergeTarget, ParsedArgs
|
|
48
|
+
import type { BranchType, MergeTarget, ParsedArgs } from "../types.js";
|
|
49
|
+
import { applyVersionToPackages, JSR_JSON, sortedPackageRoots } from "../version-bump.js";
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
/** Context persisted for finish / quick-release multi-step runs. */
|
|
52
|
+
export interface FinishContext {
|
|
50
53
|
branchToFinish: string;
|
|
51
54
|
type: BranchType;
|
|
52
55
|
version?: string;
|
|
@@ -65,6 +68,19 @@ interface FinishContext {
|
|
|
65
68
|
bumpOnFinish: boolean;
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
/** Options for {@link runFinishPlan}. */
|
|
72
|
+
export interface FinishPlanOptions {
|
|
73
|
+
dryRun: boolean;
|
|
74
|
+
verbose: boolean;
|
|
75
|
+
quiet: boolean;
|
|
76
|
+
/** Plan header label (default: "gflows finish plan"). */
|
|
77
|
+
planLabel?: string;
|
|
78
|
+
/** When true, print plan and return without executing. */
|
|
79
|
+
preview?: boolean;
|
|
80
|
+
/** When false, skip printing the plan (caller already did). Default true. */
|
|
81
|
+
printPlan?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
68
84
|
/**
|
|
69
85
|
* Runs remaining finish steps from run state (also used by continue).
|
|
70
86
|
*/
|
|
@@ -375,48 +391,10 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
375
391
|
});
|
|
376
392
|
}
|
|
377
393
|
|
|
378
|
-
const targetsDisplay = formatMergeTarget(mergeTarget, config);
|
|
379
|
-
const tagName =
|
|
380
|
-
meta.tagOnFinish && version && !args.noTag ? normalizeTagVersion(version) : undefined;
|
|
381
|
-
|
|
382
|
-
console.error("gflows finish plan:");
|
|
383
|
-
console.error(` branch: ${branchToFinish}`);
|
|
384
|
-
console.error(` merge: → ${targetsDisplay}`);
|
|
385
|
-
if (tagName) console.error(` tag: ${tagName}`);
|
|
386
|
-
console.error(` delete: ${shouldDelete ? "yes" : "no"}`);
|
|
387
|
-
console.error(` push: ${doPush ? "yes" : "no"}`);
|
|
388
|
-
if (args.squash) console.error(" squash: yes");
|
|
389
|
-
|
|
390
|
-
if (args.preview) {
|
|
391
|
-
success("gflows: preview only (no changes).");
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
394
|
if (args.bumpOnFinish && (type === "release" || type === "hotfix") && version) {
|
|
396
|
-
await bumpAndCommit(repoRoot, version, args
|
|
395
|
+
await bumpAndCommit(repoRoot, version, args);
|
|
397
396
|
}
|
|
398
397
|
|
|
399
|
-
const steps =
|
|
400
|
-
mergeTarget === "dev"
|
|
401
|
-
? [
|
|
402
|
-
{ id: "merge-dev", label: `Merge into ${config.dev}` },
|
|
403
|
-
...(shouldDelete ? [{ id: "delete", label: "Delete branch" }] : []),
|
|
404
|
-
...(doPush ? [{ id: "push", label: "Push" }] : []),
|
|
405
|
-
]
|
|
406
|
-
: [
|
|
407
|
-
{ id: "merge-main", label: `Merge into ${config.main}` },
|
|
408
|
-
...(tagName ? [{ id: "tag", label: `Tag ${tagName}` }] : []),
|
|
409
|
-
{ id: "merge-main-into-dev", label: `Merge ${config.main} into ${config.dev}` },
|
|
410
|
-
...(tagName ? [{ id: "changelog", label: "Update CHANGELOG" }] : []),
|
|
411
|
-
...(shouldDelete ? [{ id: "delete", label: "Delete branch" }] : []),
|
|
412
|
-
...(doPush ? [{ id: "push", label: "Push" }] : []),
|
|
413
|
-
];
|
|
414
|
-
|
|
415
|
-
const branchSha = await resolveSha(repoRoot, branchToFinish, opts);
|
|
416
|
-
const mainSha = await resolveSha(repoRoot, config.main, opts);
|
|
417
|
-
const devSha = await resolveSha(repoRoot, config.dev, opts);
|
|
418
|
-
const prevBranch = await getCurrentBranch(repoRoot, opts);
|
|
419
|
-
|
|
420
398
|
const ctx: FinishContext = {
|
|
421
399
|
branchToFinish,
|
|
422
400
|
type,
|
|
@@ -436,56 +414,117 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
436
414
|
bumpOnFinish: args.bumpOnFinish,
|
|
437
415
|
};
|
|
438
416
|
|
|
417
|
+
await runFinishPlan(repoRoot, ctx, {
|
|
418
|
+
...opts,
|
|
419
|
+
preview: args.preview,
|
|
420
|
+
planLabel: "gflows finish plan",
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Prints a finish-style plan and executes it via the shared step runner.
|
|
426
|
+
* Used by `finish` and quick `release` from dev.
|
|
427
|
+
*/
|
|
428
|
+
export async function runFinishPlan(
|
|
429
|
+
repoRoot: string,
|
|
430
|
+
ctx: FinishContext,
|
|
431
|
+
opts: FinishPlanOptions,
|
|
432
|
+
): Promise<void> {
|
|
433
|
+
const tagName = ctx.version && !ctx.noTag ? normalizeTagVersion(ctx.version) : undefined;
|
|
434
|
+
const targetsDisplay = formatMergeTarget(ctx.mergeTarget, {
|
|
435
|
+
main: ctx.main,
|
|
436
|
+
dev: ctx.dev,
|
|
437
|
+
remote: ctx.remote,
|
|
438
|
+
prefixes: {
|
|
439
|
+
feature: "feature/",
|
|
440
|
+
bugfix: "bugfix/",
|
|
441
|
+
chore: "chore/",
|
|
442
|
+
release: "release/",
|
|
443
|
+
hotfix: "hotfix/",
|
|
444
|
+
spike: "spike/",
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
if (opts.printPlan !== false) {
|
|
449
|
+
console.error(`${opts.planLabel ?? "gflows finish plan"}:`);
|
|
450
|
+
console.error(` branch: ${ctx.branchToFinish}`);
|
|
451
|
+
console.error(` merge: → ${targetsDisplay}`);
|
|
452
|
+
if (tagName) console.error(` tag: ${tagName}`);
|
|
453
|
+
console.error(` delete: ${ctx.shouldDelete ? "yes" : "no"}`);
|
|
454
|
+
console.error(` push: ${ctx.doPush ? "yes" : "no"}`);
|
|
455
|
+
if (ctx.squash) console.error(" squash: yes");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (opts.preview) {
|
|
459
|
+
success("gflows: preview only (no changes).");
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const steps =
|
|
464
|
+
ctx.mergeTarget === "dev"
|
|
465
|
+
? [
|
|
466
|
+
{ id: "merge-dev", label: `Merge into ${ctx.dev}` },
|
|
467
|
+
...(ctx.shouldDelete ? [{ id: "delete", label: "Delete branch" }] : []),
|
|
468
|
+
...(ctx.doPush ? [{ id: "push", label: "Push" }] : []),
|
|
469
|
+
]
|
|
470
|
+
: [
|
|
471
|
+
{ id: "merge-main", label: `Merge into ${ctx.main}` },
|
|
472
|
+
...(tagName ? [{ id: "tag", label: `Tag ${tagName}` }] : []),
|
|
473
|
+
{ id: "merge-main-into-dev", label: `Merge ${ctx.main} into ${ctx.dev}` },
|
|
474
|
+
...(tagName ? [{ id: "changelog", label: "Update CHANGELOG" }] : []),
|
|
475
|
+
...(ctx.shouldDelete ? [{ id: "delete", label: "Delete branch" }] : []),
|
|
476
|
+
...(ctx.doPush ? [{ id: "push", label: "Push" }] : []),
|
|
477
|
+
];
|
|
478
|
+
|
|
479
|
+
const gitOpts = { dryRun: opts.dryRun, verbose: opts.verbose, quiet: opts.quiet };
|
|
480
|
+
const branchSha = await resolveSha(repoRoot, ctx.branchToFinish, gitOpts);
|
|
481
|
+
const mainSha = await resolveSha(repoRoot, ctx.main, gitOpts);
|
|
482
|
+
const devSha = await resolveSha(repoRoot, ctx.dev, gitOpts);
|
|
483
|
+
const prevBranch = await getCurrentBranch(repoRoot, gitOpts);
|
|
484
|
+
|
|
439
485
|
const state = startRun(repoRoot, "finish", steps, ctx as unknown as Record<string, unknown>, {
|
|
440
486
|
branchSha,
|
|
441
487
|
mainSha,
|
|
442
488
|
devSha,
|
|
443
489
|
prevBranch,
|
|
444
490
|
createdTag: tagName ?? null,
|
|
445
|
-
branchName: branchToFinish,
|
|
446
|
-
shouldDelete,
|
|
447
|
-
main:
|
|
448
|
-
dev:
|
|
491
|
+
branchName: ctx.branchToFinish,
|
|
492
|
+
shouldDelete: ctx.shouldDelete,
|
|
493
|
+
main: ctx.main,
|
|
494
|
+
dev: ctx.dev,
|
|
449
495
|
});
|
|
450
496
|
|
|
451
|
-
await executeFinishSteps(repoRoot, state,
|
|
497
|
+
await executeFinishSteps(repoRoot, state, gitOpts);
|
|
452
498
|
}
|
|
453
499
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
): Promise<void> {
|
|
500
|
+
/**
|
|
501
|
+
* Bumps package versions (format-preserving) and commits on the current branch.
|
|
502
|
+
*/
|
|
503
|
+
async function bumpAndCommit(repoRoot: string, version: string, args: ParsedArgs): Promise<void> {
|
|
504
|
+
if (args.dryRun) return;
|
|
460
505
|
const ver = version.replace(/^v/, "");
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (!existsSync(pkgPath)) return;
|
|
506
|
+
const roots = sortedPackageRoots(repoRoot);
|
|
507
|
+
if (roots.length === 0) return;
|
|
464
508
|
try {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
|
|
469
|
-
}
|
|
470
|
-
const jsrPath = join(repoRoot, "jsr.json");
|
|
471
|
-
if (existsSync(jsrPath)) {
|
|
472
|
-
let jsrRaw = readFileSync(jsrPath, "utf-8");
|
|
473
|
-
jsrRaw = jsrRaw.replace(/"version"\s*:\s*"[^"]*"/, `"version": "${ver}"`);
|
|
474
|
-
if (!args.dryRun) writeFileSync(jsrPath, jsrRaw, "utf-8");
|
|
475
|
-
}
|
|
476
|
-
if (!args.dryRun) {
|
|
477
|
-
await runGit(["add", "package.json", "jsr.json"], {
|
|
509
|
+
applyVersionToPackages(repoRoot, roots, ver);
|
|
510
|
+
for (const dir of roots) {
|
|
511
|
+
await runGit(["add", "--", join(dir, PACKAGE_JSON)], {
|
|
478
512
|
cwd: repoRoot,
|
|
479
|
-
dryRun:
|
|
513
|
+
dryRun: false,
|
|
480
514
|
verbose: args.verbose,
|
|
481
|
-
});
|
|
482
|
-
await runGit(["
|
|
515
|
+
}).catch(() => undefined);
|
|
516
|
+
await runGit(["add", "--", join(dir, JSR_JSON)], {
|
|
483
517
|
cwd: repoRoot,
|
|
484
|
-
dryRun:
|
|
518
|
+
dryRun: false,
|
|
485
519
|
verbose: args.verbose,
|
|
486
|
-
});
|
|
487
|
-
if (!args.quiet) success(`gflows: bumped version to ${ver} and committed.`);
|
|
520
|
+
}).catch(() => undefined);
|
|
488
521
|
}
|
|
522
|
+
await runGit(["commit", "-m", `chore: bump to ${ver}`], {
|
|
523
|
+
cwd: repoRoot,
|
|
524
|
+
dryRun: false,
|
|
525
|
+
verbose: args.verbose,
|
|
526
|
+
});
|
|
527
|
+
if (!args.quiet) success(`gflows: bumped version to ${ver} and committed.`);
|
|
489
528
|
} catch {
|
|
490
529
|
hint("finish --bump: could not bump/commit version files; continuing finish.");
|
|
491
530
|
}
|
package/src/commands/help.ts
CHANGED
|
@@ -19,6 +19,7 @@ Commands:
|
|
|
19
19
|
init, -I Ensure main, create dev (TTY: wizard; optional package.json script alias)
|
|
20
20
|
start, -S Create workflow branch (TTY: prompts if args missing)
|
|
21
21
|
finish, -F Merge and close branch (plan + delete by default)
|
|
22
|
+
release Quick release from dev (bump or keep version + merge main + tag)
|
|
22
23
|
sync Update current branch from its base
|
|
23
24
|
pr Open PR/MR via gh or glab (correct base)
|
|
24
25
|
switch, -W Switch branch (picker or name)
|
|
@@ -42,7 +43,7 @@ Commands:
|
|
|
42
43
|
Types: feature (-f), bugfix (-b), chore (-c), release (-r), hotfix (-x), spike (-e)
|
|
43
44
|
|
|
44
45
|
Common flags:
|
|
45
|
-
-p, --push Push after init/start/finish/pr
|
|
46
|
+
-p, --push Push after init/start/finish/release/pr
|
|
46
47
|
-P, --no-push Do not push (init defaults to push; start does not)
|
|
47
48
|
--main <name> Main branch (init: persist to .gflows.json)
|
|
48
49
|
--dev <name> Dev branch (init: persist to .gflows.json)
|
|
@@ -61,6 +62,7 @@ Init: --script-alias <name> Add package.json script (e.g. g → "gflows")
|
|
|
61
62
|
Start: --force Allow dirty working tree
|
|
62
63
|
Finish: --no-ff Always create merge commit; -D/--delete, -N/--no-delete;
|
|
63
64
|
--squash, --preview, --bump; -s/--sign, -T/--no-tag, -M/--tag-message, -m/--message
|
|
65
|
+
Release: up <patch|minor|major> | current (from dev only); -y -p|-P; --preview; -s/--sign
|
|
64
66
|
Sync: --rebase, --force
|
|
65
67
|
List: -r, --include-remote Include remote-tracking branches
|
|
66
68
|
Switch: --move | --restore | --clean | --destroy | --cancel
|