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/src/commands/mcp.ts
CHANGED
|
@@ -93,6 +93,27 @@ const TOOLS = [
|
|
|
93
93
|
},
|
|
94
94
|
},
|
|
95
95
|
},
|
|
96
|
+
{
|
|
97
|
+
name: "gflows_release",
|
|
98
|
+
description:
|
|
99
|
+
"Quick release from dev: optional bump (or keep current version), merge into main, tag, sync main→dev. Pass bumpType or keepCurrent.",
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
path: { type: "string" },
|
|
104
|
+
bumpType: {
|
|
105
|
+
type: "string",
|
|
106
|
+
description: "Semver segment to bump: patch, minor, or major (omit when keepCurrent)",
|
|
107
|
+
},
|
|
108
|
+
keepCurrent: {
|
|
109
|
+
type: "boolean",
|
|
110
|
+
description: "Tag the package version already on disk (no bump commit)",
|
|
111
|
+
},
|
|
112
|
+
push: { type: "boolean" },
|
|
113
|
+
preview: { type: "boolean" },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
96
117
|
{
|
|
97
118
|
name: "gflows_schema",
|
|
98
119
|
description: "Return the gflows command schema JSON.",
|
|
@@ -159,6 +180,19 @@ async function runTool(
|
|
|
159
180
|
if (args.push) argv.push("-p");
|
|
160
181
|
else argv.push("-P");
|
|
161
182
|
break;
|
|
183
|
+
case "gflows_release": {
|
|
184
|
+
if (args.keepCurrent === true) {
|
|
185
|
+
argv.push("release", "current");
|
|
186
|
+
} else if (typeof args.bumpType === "string" && args.bumpType.trim() !== "") {
|
|
187
|
+
argv.push("release", "up", args.bumpType.trim());
|
|
188
|
+
} else {
|
|
189
|
+
throw new Error("gflows_release: pass bumpType (patch|minor|major) or keepCurrent: true");
|
|
190
|
+
}
|
|
191
|
+
if (args.preview) argv.push("--preview");
|
|
192
|
+
if (args.push) argv.push("-p");
|
|
193
|
+
else argv.push("-P");
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
162
196
|
case "gflows_schema":
|
|
163
197
|
argv.push("schema");
|
|
164
198
|
break;
|
|
@@ -185,7 +219,7 @@ async function runTool(
|
|
|
185
219
|
*/
|
|
186
220
|
export async function run(_args: ParsedArgs): Promise<void> {
|
|
187
221
|
console.error(
|
|
188
|
-
"gflows mcp: listening on stdio (JSON-RPC). Tools: status, doctor, info, list, start, sync, finish, schema.",
|
|
222
|
+
"gflows mcp: listening on stdio (JSON-RPC). Tools: status, doctor, info, list, start, sync, finish, release, schema.",
|
|
189
223
|
);
|
|
190
224
|
|
|
191
225
|
const decoder = new TextDecoder();
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick release from the long-lived `dev` branch: bump (or keep), merge into main, tag, sync main→dev.
|
|
3
|
+
* @module commands/release
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { resolveConfig } from "../config.js";
|
|
8
|
+
import { EXIT_USER } from "../constants.js";
|
|
9
|
+
import { DirtyWorkingTreeError, NothingToFinishError } from "../errors.js";
|
|
10
|
+
import { normalizeTagVersion } from "../flow.js";
|
|
11
|
+
import {
|
|
12
|
+
assertNoRebaseOrMerge,
|
|
13
|
+
assertNotDetached,
|
|
14
|
+
getAheadBehind,
|
|
15
|
+
getCurrentBranch,
|
|
16
|
+
isClean,
|
|
17
|
+
resolveRepoRoot,
|
|
18
|
+
runGit,
|
|
19
|
+
tagExists,
|
|
20
|
+
} from "../git.js";
|
|
21
|
+
import { hint, success } from "../out.js";
|
|
22
|
+
import { PACKAGE_JSON } from "../packages.js";
|
|
23
|
+
import type { BumpType, ParsedArgs } from "../types.js";
|
|
24
|
+
import {
|
|
25
|
+
applyVersionToPackages,
|
|
26
|
+
computeBump,
|
|
27
|
+
JSR_JSON,
|
|
28
|
+
readPackageVersion,
|
|
29
|
+
sortedPackageRoots,
|
|
30
|
+
} from "../version-bump.js";
|
|
31
|
+
import { type FinishContext, runFinishPlan } from "./finish.js";
|
|
32
|
+
|
|
33
|
+
/** Interactive choice: keep package version as-is, or bump a semver segment. */
|
|
34
|
+
type ReleaseVersionChoice = "current" | BumpType;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Runs quick release: only valid when HEAD is the configured `dev` branch.
|
|
38
|
+
*/
|
|
39
|
+
export async function run(args: ParsedArgs): Promise<void> {
|
|
40
|
+
const repoRoot = await resolveRepoRoot(args.cwd);
|
|
41
|
+
const config = resolveConfig(
|
|
42
|
+
repoRoot,
|
|
43
|
+
{ main: args.main, dev: args.dev, remote: args.remote },
|
|
44
|
+
{ verbose: args.verbose },
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const opts = { dryRun: args.dryRun, verbose: args.verbose, quiet: args.quiet };
|
|
48
|
+
const isTTY = Boolean(process.stdin.isTTY);
|
|
49
|
+
|
|
50
|
+
await assertNotDetached(repoRoot);
|
|
51
|
+
assertNoRebaseOrMerge(repoRoot);
|
|
52
|
+
|
|
53
|
+
const current = await getCurrentBranch(repoRoot, opts);
|
|
54
|
+
if (!current || current !== config.dev) {
|
|
55
|
+
console.error(
|
|
56
|
+
`gflows release: must be on '${config.dev}' (currently ${current ? `'${current}'` : "detached"}).`,
|
|
57
|
+
);
|
|
58
|
+
hint("Checkout dev, or use: gflows start release vX.Y.Z && gflows finish release -y -P");
|
|
59
|
+
process.exit(2);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!args.force) {
|
|
63
|
+
const clean = await isClean(repoRoot, opts);
|
|
64
|
+
if (!clean) {
|
|
65
|
+
throw new DirtyWorkingTreeError(
|
|
66
|
+
"Working tree has uncommitted changes. Commit or stash them before release, or use --force.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { ahead } = await getAheadBehind(repoRoot, config.main, config.dev, opts);
|
|
72
|
+
if (ahead === 0) {
|
|
73
|
+
throw new NothingToFinishError(
|
|
74
|
+
`Nothing to release: '${config.dev}' has no commits beyond '${config.main}'.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let keepCurrent = args.keepCurrent === true;
|
|
79
|
+
let bumpType: BumpType | undefined = args.bumpType;
|
|
80
|
+
|
|
81
|
+
if (args.bumpDirection === "down") {
|
|
82
|
+
console.error(
|
|
83
|
+
"gflows release: only 'up' or 'current' is supported (not 'down'). Example: gflows release up patch",
|
|
84
|
+
);
|
|
85
|
+
process.exit(EXIT_USER);
|
|
86
|
+
}
|
|
87
|
+
if (args.bumpDirection && args.bumpDirection !== "up") {
|
|
88
|
+
console.error(
|
|
89
|
+
"gflows release: expected 'up <patch|minor|major>' or 'current'. Example: gflows release up patch",
|
|
90
|
+
);
|
|
91
|
+
process.exit(EXIT_USER);
|
|
92
|
+
}
|
|
93
|
+
if (keepCurrent && (args.bumpDirection || bumpType)) {
|
|
94
|
+
console.error("gflows release: use either 'current' or 'up <patch|minor|major>', not both.");
|
|
95
|
+
process.exit(EXIT_USER);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!keepCurrent && !bumpType) {
|
|
99
|
+
if (!isTTY) {
|
|
100
|
+
console.error(
|
|
101
|
+
"gflows release: when not in a TTY, specify version mode. Examples: gflows release current -y -P or gflows release up patch -y -P",
|
|
102
|
+
);
|
|
103
|
+
process.exit(EXIT_USER);
|
|
104
|
+
}
|
|
105
|
+
const { selectPrompt } = await import("../prompts.js");
|
|
106
|
+
const computedPreview = (() => {
|
|
107
|
+
try {
|
|
108
|
+
return computeBump(repoRoot, "up", "patch");
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
113
|
+
const currentVer = computedPreview?.oldVersion ?? "?";
|
|
114
|
+
// `release up` already chose bumping — only ask which segment.
|
|
115
|
+
if (args.bumpDirection === "up") {
|
|
116
|
+
bumpType = await selectPrompt<BumpType>({
|
|
117
|
+
message: `Bump version (current ${currentVer})`,
|
|
118
|
+
options: [
|
|
119
|
+
{ label: "patch (x.y.Z)", value: "patch" },
|
|
120
|
+
{ label: "minor (x.Y.0)", value: "minor" },
|
|
121
|
+
{ label: "major (X.0.0)", value: "major" },
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
const choice = await selectPrompt<ReleaseVersionChoice>({
|
|
126
|
+
message: `Version for release (current ${currentVer})`,
|
|
127
|
+
options: [
|
|
128
|
+
{ label: `keep current (${currentVer})`, value: "current" },
|
|
129
|
+
{ label: "bump patch (x.y.Z)", value: "patch" },
|
|
130
|
+
{ label: "bump minor (x.Y.0)", value: "minor" },
|
|
131
|
+
{ label: "bump major (X.0.0)", value: "major" },
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
if (choice === "current") {
|
|
135
|
+
keepCurrent = true;
|
|
136
|
+
} else {
|
|
137
|
+
bumpType = choice;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let doPush = false;
|
|
143
|
+
if (args.push && !args.noPush) doPush = true;
|
|
144
|
+
else if (args.noPush) doPush = false;
|
|
145
|
+
else if (!isTTY) {
|
|
146
|
+
console.error("gflows release: specify --push (-p) or --no-push (-P) when not interactive.");
|
|
147
|
+
process.exit(EXIT_USER);
|
|
148
|
+
} else if (!args.yes) {
|
|
149
|
+
const { confirmPrompt } = await import("../prompts.js");
|
|
150
|
+
doPush = await confirmPrompt({
|
|
151
|
+
message: "Push after release?",
|
|
152
|
+
initialValue: false,
|
|
153
|
+
});
|
|
154
|
+
} else {
|
|
155
|
+
// -y without push polarity in TTY: default no-push (same as finish when -y alone)
|
|
156
|
+
doPush = false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let oldVersion: string;
|
|
160
|
+
let newVersion: string;
|
|
161
|
+
let roots: string[];
|
|
162
|
+
|
|
163
|
+
if (keepCurrent) {
|
|
164
|
+
roots = sortedPackageRoots(repoRoot);
|
|
165
|
+
if (roots.length === 0) {
|
|
166
|
+
console.error(
|
|
167
|
+
`gflows release: no package.json found under ${repoRoot}. Run from project root or use -C <dir>.`,
|
|
168
|
+
);
|
|
169
|
+
process.exit(EXIT_USER);
|
|
170
|
+
}
|
|
171
|
+
const primaryRoot = roots[0];
|
|
172
|
+
if (primaryRoot === undefined) {
|
|
173
|
+
console.error(
|
|
174
|
+
`gflows release: no package.json found under ${repoRoot}. Run from project root or use -C <dir>.`,
|
|
175
|
+
);
|
|
176
|
+
process.exit(EXIT_USER);
|
|
177
|
+
}
|
|
178
|
+
oldVersion = readPackageVersion(primaryRoot).raw;
|
|
179
|
+
newVersion = oldVersion;
|
|
180
|
+
} else {
|
|
181
|
+
if (!bumpType) {
|
|
182
|
+
console.error(
|
|
183
|
+
"gflows release: expected 'up <patch|minor|major>' or 'current'. Example: gflows release up patch",
|
|
184
|
+
);
|
|
185
|
+
process.exit(EXIT_USER);
|
|
186
|
+
}
|
|
187
|
+
({ oldVersion, newVersion, roots } = computeBump(repoRoot, "up", bumpType));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const tagName = normalizeTagVersion(newVersion);
|
|
191
|
+
|
|
192
|
+
if (await tagExists(repoRoot, tagName, opts)) {
|
|
193
|
+
console.error(`gflows release: tag '${tagName}' already exists.`);
|
|
194
|
+
hint(
|
|
195
|
+
keepCurrent
|
|
196
|
+
? "Bump the version first, or delete the tag if you intend to recreate it."
|
|
197
|
+
: "Use a different bump (minor/major), or delete the tag if you intend to recreate it.",
|
|
198
|
+
);
|
|
199
|
+
process.exit(2);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
console.error("gflows release plan:");
|
|
203
|
+
console.error(` from: ${config.dev}`);
|
|
204
|
+
if (keepCurrent) {
|
|
205
|
+
console.error(` version: ${newVersion} (keep current, no bump)`);
|
|
206
|
+
} else {
|
|
207
|
+
console.error(` bump: ${oldVersion} → ${newVersion}`);
|
|
208
|
+
}
|
|
209
|
+
console.error(` merge: → ${config.main}, then ${config.main} → ${config.dev}`);
|
|
210
|
+
console.error(` tag: ${tagName}`);
|
|
211
|
+
console.error(` push: ${doPush ? "yes" : "no"}`);
|
|
212
|
+
|
|
213
|
+
if (args.preview) {
|
|
214
|
+
success("gflows: preview only (no changes).");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!args.yes && isTTY) {
|
|
219
|
+
const { confirmPrompt } = await import("../prompts.js");
|
|
220
|
+
const ok = await confirmPrompt({
|
|
221
|
+
message: "Proceed with quick release?",
|
|
222
|
+
initialValue: true,
|
|
223
|
+
});
|
|
224
|
+
if (!ok) {
|
|
225
|
+
success("gflows: release cancelled.");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
} else if (!args.yes && !isTTY) {
|
|
229
|
+
console.error("gflows release: pass -y to accept the plan when not interactive.");
|
|
230
|
+
process.exit(EXIT_USER);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!keepCurrent && !opts.dryRun) {
|
|
234
|
+
applyVersionToPackages(repoRoot, roots, newVersion);
|
|
235
|
+
for (const dir of roots) {
|
|
236
|
+
await runGit(["add", "--", join(dir, PACKAGE_JSON)], {
|
|
237
|
+
cwd: repoRoot,
|
|
238
|
+
dryRun: false,
|
|
239
|
+
verbose: args.verbose,
|
|
240
|
+
}).catch(() => undefined);
|
|
241
|
+
await runGit(["add", "--", join(dir, JSR_JSON)], {
|
|
242
|
+
cwd: repoRoot,
|
|
243
|
+
dryRun: false,
|
|
244
|
+
verbose: args.verbose,
|
|
245
|
+
}).catch(() => undefined);
|
|
246
|
+
}
|
|
247
|
+
await runGit(["commit", "-m", `chore: bump to ${newVersion}`], {
|
|
248
|
+
cwd: repoRoot,
|
|
249
|
+
dryRun: false,
|
|
250
|
+
verbose: args.verbose,
|
|
251
|
+
});
|
|
252
|
+
if (!args.quiet) {
|
|
253
|
+
success(`gflows: bumped version ${oldVersion} → ${newVersion} and committed.`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const ctx: FinishContext = {
|
|
258
|
+
branchToFinish: config.dev,
|
|
259
|
+
type: "release",
|
|
260
|
+
version: newVersion,
|
|
261
|
+
mergeTarget: "main-then-dev",
|
|
262
|
+
shouldDelete: false,
|
|
263
|
+
doPush,
|
|
264
|
+
noFf: args.noFf,
|
|
265
|
+
squash: false,
|
|
266
|
+
message: args.message,
|
|
267
|
+
signTag: args.signTag,
|
|
268
|
+
noTag: false,
|
|
269
|
+
tagMessage: args.tagMessage,
|
|
270
|
+
remote: args.remote ?? config.remote,
|
|
271
|
+
main: config.main,
|
|
272
|
+
dev: config.dev,
|
|
273
|
+
bumpOnFinish: false,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
await runFinishPlan(repoRoot, ctx, {
|
|
277
|
+
...opts,
|
|
278
|
+
printPlan: false,
|
|
279
|
+
});
|
|
280
|
+
}
|
package/src/commands/schema.ts
CHANGED
|
@@ -56,6 +56,14 @@ export async function run(_args: ParsedArgs): Promise<void> {
|
|
|
56
56
|
"-M/--tag-message",
|
|
57
57
|
"-m/--message",
|
|
58
58
|
],
|
|
59
|
+
release: [
|
|
60
|
+
"up patch|minor|major | current",
|
|
61
|
+
"-y",
|
|
62
|
+
"-p/--push",
|
|
63
|
+
"-P/--no-push",
|
|
64
|
+
"--preview",
|
|
65
|
+
"-s/--sign",
|
|
66
|
+
],
|
|
59
67
|
start: ["-o/--from", "--force", "-p/--push"],
|
|
60
68
|
sync: ["--rebase", "--force"],
|
|
61
69
|
list: ["-r/--include-remote", "--json"],
|
|
@@ -66,6 +74,7 @@ export async function run(_args: ParsedArgs): Promise<void> {
|
|
|
66
74
|
agentNotes: [
|
|
67
75
|
"Always pass explicit flags in non-TTY (never assume interactive hub).",
|
|
68
76
|
"Prefer -y to accept finish plan (delete defaults on); pass -P or -p for push.",
|
|
77
|
+
"Quick release from dev: gflows release up patch -y -p (or -P); or gflows release current -y -p after a manual bump.",
|
|
69
78
|
"On conflict: gflows continue | gflows abort | gflows undo.",
|
|
70
79
|
"Use gflows schema and AGENTS.md as source of truth.",
|
|
71
80
|
],
|
|
@@ -78,6 +87,10 @@ function commandMeta(command: string): { summary: string; interactiveOk: boolean
|
|
|
78
87
|
init: { summary: "Ensure main; create dev", interactiveOk: true },
|
|
79
88
|
start: { summary: "Create workflow branch", interactiveOk: true },
|
|
80
89
|
finish: { summary: "Merge and close workflow branch", interactiveOk: true },
|
|
90
|
+
release: {
|
|
91
|
+
summary: "Quick release from dev (bump or keep version, merge main, tag)",
|
|
92
|
+
interactiveOk: true,
|
|
93
|
+
},
|
|
81
94
|
switch: { summary: "Switch workflow branch", interactiveOk: true },
|
|
82
95
|
delete: { summary: "Delete local workflow branches", interactiveOk: true },
|
|
83
96
|
list: { summary: "List workflow branches", interactiveOk: false },
|
package/src/interactive.ts
CHANGED
|
@@ -82,6 +82,11 @@ function buildLegacyOptions(
|
|
|
82
82
|
{ value: "sync", label: "Sync this branch with base" },
|
|
83
83
|
{ value: "pr", label: "Open a pull request" },
|
|
84
84
|
{ value: "finish", label: "Finish / merge this branch" },
|
|
85
|
+
);
|
|
86
|
+
if (snap?.current === snap?.dev) {
|
|
87
|
+
items.push({ value: "release", label: "Quick release to main" });
|
|
88
|
+
}
|
|
89
|
+
items.push(
|
|
85
90
|
{ value: "switch", label: "Switch branch" },
|
|
86
91
|
{ value: "list", label: "List branches" },
|
|
87
92
|
{ value: "viz", label: "Refresh map" },
|
|
@@ -121,6 +126,24 @@ async function runHubAction(cwd: string, action: string): Promise<void> {
|
|
|
121
126
|
await dispatch(cwd, ["finish", "-y", push ? "-p" : "-P"]);
|
|
122
127
|
return;
|
|
123
128
|
}
|
|
129
|
+
if (action === "release") {
|
|
130
|
+
const choice = await selectPrompt<"current" | "patch" | "minor" | "major">({
|
|
131
|
+
message: "Version for release",
|
|
132
|
+
options: [
|
|
133
|
+
{ label: "keep current", value: "current" },
|
|
134
|
+
{ label: "bump patch (x.y.Z)", value: "patch" },
|
|
135
|
+
{ label: "bump minor (x.Y.0)", value: "minor" },
|
|
136
|
+
{ label: "bump major (X.0.0)", value: "major" },
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
const push = await confirmPrompt({ message: "Push after release?", initialValue: false });
|
|
140
|
+
const releaseArgv =
|
|
141
|
+
choice === "current"
|
|
142
|
+
? (["release", "current", "-y", push ? "-p" : "-P"] as string[])
|
|
143
|
+
: (["release", "up", choice, "-y", push ? "-p" : "-P"] as string[]);
|
|
144
|
+
await dispatch(cwd, releaseArgv);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
124
147
|
if (action === "sync") {
|
|
125
148
|
const rebase = await confirmPrompt({
|
|
126
149
|
message: "Rebase onto base (instead of merge)?",
|
package/src/parse.ts
CHANGED
|
@@ -234,9 +234,11 @@ function resolveName(
|
|
|
234
234
|
if (shell === "bash" || shell === "zsh" || shell === "fish") return shell;
|
|
235
235
|
return undefined;
|
|
236
236
|
}
|
|
237
|
-
if (command === "bump") {
|
|
237
|
+
if (command === "bump" || command === "release") {
|
|
238
238
|
const dir = positionals[skip];
|
|
239
|
-
if (dir === "up" || dir === "down"
|
|
239
|
+
if (dir === "up" || dir === "down" || (command === "release" && dir === "current")) {
|
|
240
|
+
return dir;
|
|
241
|
+
}
|
|
240
242
|
return undefined;
|
|
241
243
|
}
|
|
242
244
|
if (command === "switch" || command === "pr" || command === "sync") {
|
|
@@ -248,11 +250,15 @@ function resolveName(
|
|
|
248
250
|
function resolveBump(positionals: string[]): {
|
|
249
251
|
direction?: "up" | "down";
|
|
250
252
|
type?: "patch" | "minor" | "major";
|
|
253
|
+
keepCurrent?: boolean;
|
|
251
254
|
} {
|
|
252
|
-
// Skip leading
|
|
253
|
-
const skip = positionals[0] === "bump" ? 1 : 0;
|
|
255
|
+
// Skip leading command when present; with -U, positionals start at up/down
|
|
256
|
+
const skip = positionals[0] === "bump" || positionals[0] === "release" ? 1 : 0;
|
|
254
257
|
const a = positionals[skip];
|
|
255
258
|
const b = positionals[skip + 1];
|
|
259
|
+
if (positionals[0] === "release" && a === "current") {
|
|
260
|
+
return { keepCurrent: true };
|
|
261
|
+
}
|
|
256
262
|
const direction = a === "up" || a === "down" ? a : undefined;
|
|
257
263
|
const type = b === "patch" || b === "minor" || b === "major" ? b : undefined;
|
|
258
264
|
return { direction, type };
|
|
@@ -297,6 +303,7 @@ function emptyArgs(cwd: string, pathStr: string | undefined): ParsedArgs {
|
|
|
297
303
|
squash: false,
|
|
298
304
|
preview: false,
|
|
299
305
|
bumpOnFinish: false,
|
|
306
|
+
keepCurrent: false,
|
|
300
307
|
includeRemote: false,
|
|
301
308
|
json: false,
|
|
302
309
|
rebase: false,
|
|
@@ -336,8 +343,13 @@ export function parse(
|
|
|
336
343
|
|
|
337
344
|
const type = resolveType(command, positionals, v);
|
|
338
345
|
const name = resolveName(command, positionals, v);
|
|
339
|
-
const
|
|
340
|
-
command === "bump"
|
|
346
|
+
const bumpResolved =
|
|
347
|
+
command === "bump" || command === "release"
|
|
348
|
+
? resolveBump(positionals)
|
|
349
|
+
: { direction: undefined, type: undefined, keepCurrent: undefined };
|
|
350
|
+
const bumpDirection = bumpResolved.direction;
|
|
351
|
+
const bumpType = bumpResolved.type;
|
|
352
|
+
const keepCurrent = bumpResolved.keepCurrent === true;
|
|
341
353
|
const configArgs = command === "config" ? resolveConfigArgs(positionals) : {};
|
|
342
354
|
|
|
343
355
|
const branchNames =
|
|
@@ -393,6 +405,7 @@ export function parse(
|
|
|
393
405
|
branchNames,
|
|
394
406
|
bumpDirection,
|
|
395
407
|
bumpType,
|
|
408
|
+
keepCurrent,
|
|
396
409
|
configAction: configArgs.action,
|
|
397
410
|
configKey: configArgs.key,
|
|
398
411
|
configValue: configArgs.value,
|
package/src/recommend.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type RecommendAction =
|
|
|
11
11
|
| "continue"
|
|
12
12
|
| "sync"
|
|
13
13
|
| "finish"
|
|
14
|
+
| "release"
|
|
14
15
|
| "pr"
|
|
15
16
|
| "start"
|
|
16
17
|
| "commit"
|
|
@@ -79,7 +80,7 @@ export function recommend(snap: VizSnapshot): Recommendation {
|
|
|
79
80
|
return {
|
|
80
81
|
action: "start",
|
|
81
82
|
label: "Start a feature (or chore / bugfix)",
|
|
82
|
-
detail: "Integration line — short-lived branches start here",
|
|
83
|
+
detail: "Integration line — short-lived branches start here; /release for quick ship to main",
|
|
83
84
|
menuLabel: "Start new work",
|
|
84
85
|
};
|
|
85
86
|
}
|
package/src/tui/HubHome.tsx
CHANGED
|
@@ -289,7 +289,7 @@ export function HubHome({
|
|
|
289
289
|
) : (
|
|
290
290
|
<Text color={MUTED}>
|
|
291
291
|
{showHelp
|
|
292
|
-
? "/init /start /sync /pr /finish /doctor /info /help · esc clear · ctrl+c quit"
|
|
292
|
+
? "/init /start /sync /pr /finish /release /doctor /info /help · esc clear · ctrl+c quit"
|
|
293
293
|
: "? for shortcuts · / for command menu"}
|
|
294
294
|
</Text>
|
|
295
295
|
)}
|
|
@@ -372,6 +372,7 @@ function buildActions(snap: VizSnapshot | null, recommended: RecommendAction | n
|
|
|
372
372
|
push("sync", "Sync with base", "/sync");
|
|
373
373
|
push("pr", "Open pull request", "/pr");
|
|
374
374
|
push("finish", "Finish / merge branch", "/finish");
|
|
375
|
+
if (snap?.current === snap?.dev) push("release", "Quick release to main", "/release");
|
|
375
376
|
if (snap?.suspended) push("continue", "Continue suspended", "/continue");
|
|
376
377
|
push("switch", "Switch branch", "/switch");
|
|
377
378
|
push("list", "List branches", "/list");
|
package/src/tui/HubShell.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
FinishFlow,
|
|
12
12
|
InitFlow,
|
|
13
13
|
ListFlow,
|
|
14
|
+
ReleaseFlow,
|
|
14
15
|
StartFlow,
|
|
15
16
|
SwitchFlow,
|
|
16
17
|
SyncFlow,
|
|
@@ -31,6 +32,7 @@ type Screen =
|
|
|
31
32
|
| { id: "home"; flash?: string }
|
|
32
33
|
| { id: "start" }
|
|
33
34
|
| { id: "finish" }
|
|
35
|
+
| { id: "release" }
|
|
34
36
|
| { id: "sync" }
|
|
35
37
|
| { id: "list" }
|
|
36
38
|
| { id: "switch" }
|
|
@@ -87,6 +89,9 @@ export function HubShell({ cwd, onDone }: HubShellProps): React.ReactElement {
|
|
|
87
89
|
case "finish":
|
|
88
90
|
setScreen({ id: "finish" });
|
|
89
91
|
return true;
|
|
92
|
+
case "release":
|
|
93
|
+
setScreen({ id: "release" });
|
|
94
|
+
return true;
|
|
90
95
|
case "sync":
|
|
91
96
|
setScreen({ id: "sync" });
|
|
92
97
|
return true;
|
|
@@ -156,6 +161,14 @@ export function HubShell({ cwd, onDone }: HubShellProps): React.ReactElement {
|
|
|
156
161
|
setScreen({ id: "finish" });
|
|
157
162
|
return;
|
|
158
163
|
}
|
|
164
|
+
if (cmd === "release") {
|
|
165
|
+
if (rest.length > 0) {
|
|
166
|
+
runArgv(["release", "-y", "-P", ...rest]);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
setScreen({ id: "release" });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
159
172
|
if (cmd === "sync") {
|
|
160
173
|
if (rest.length > 0) {
|
|
161
174
|
runArgv(["sync", "--force", ...rest]);
|
|
@@ -217,6 +230,9 @@ export function HubShell({ cwd, onDone }: HubShellProps): React.ReactElement {
|
|
|
217
230
|
if (screen.id === "finish") {
|
|
218
231
|
return <FinishFlow onCancel={cancelWizard} onDone={runArgv} />;
|
|
219
232
|
}
|
|
233
|
+
if (screen.id === "release") {
|
|
234
|
+
return <ReleaseFlow onCancel={cancelWizard} onDone={runArgv} />;
|
|
235
|
+
}
|
|
220
236
|
if (screen.id === "sync") {
|
|
221
237
|
return <SyncFlow onCancel={cancelWizard} onDone={runArgv} />;
|
|
222
238
|
}
|
package/src/tui/flows.tsx
CHANGED
|
@@ -124,6 +124,60 @@ export function FinishFlow({
|
|
|
124
124
|
);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Collects argv for `gflows release …` (quick release from dev) inside Ink.
|
|
129
|
+
*/
|
|
130
|
+
export function ReleaseFlow({
|
|
131
|
+
onDone,
|
|
132
|
+
onCancel,
|
|
133
|
+
}: {
|
|
134
|
+
onDone: (argv: string[]) => void;
|
|
135
|
+
onCancel: () => void;
|
|
136
|
+
}): React.ReactElement {
|
|
137
|
+
const [step, setStep] = useState<"type" | "push">("type");
|
|
138
|
+
const [choice, setChoice] = useState<"current" | "patch" | "minor" | "major">("current");
|
|
139
|
+
|
|
140
|
+
if (step === "type") {
|
|
141
|
+
return (
|
|
142
|
+
<WizardFrame title="Quick release from dev">
|
|
143
|
+
<InkSelect<"current" | "patch" | "minor" | "major">
|
|
144
|
+
message="Version for release"
|
|
145
|
+
options={[
|
|
146
|
+
{ value: "current", label: "keep current" },
|
|
147
|
+
{ value: "patch", label: "bump patch (x.y.Z)" },
|
|
148
|
+
{ value: "minor", label: "bump minor (x.Y.0)" },
|
|
149
|
+
{ value: "major", label: "bump major (X.0.0)" },
|
|
150
|
+
]}
|
|
151
|
+
onCancel={onCancel}
|
|
152
|
+
onSubmit={(t) => {
|
|
153
|
+
setChoice(t);
|
|
154
|
+
setStep("push");
|
|
155
|
+
}}
|
|
156
|
+
/>
|
|
157
|
+
</WizardFrame>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const title = choice === "current" ? "Release · keep current" : `Release · up ${choice}`;
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<WizardFrame title={title}>
|
|
165
|
+
<InkConfirm
|
|
166
|
+
message="Push after release?"
|
|
167
|
+
initialValue={false}
|
|
168
|
+
onCancel={onCancel}
|
|
169
|
+
onSubmit={(push) => {
|
|
170
|
+
onDone(
|
|
171
|
+
choice === "current"
|
|
172
|
+
? ["release", "current", "-y", push ? "-p" : "-P"]
|
|
173
|
+
: ["release", "up", choice, "-y", push ? "-p" : "-P"],
|
|
174
|
+
);
|
|
175
|
+
}}
|
|
176
|
+
/>
|
|
177
|
+
</WizardFrame>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
127
181
|
/**
|
|
128
182
|
* Collects argv for `gflows sync …` entirely inside Ink.
|
|
129
183
|
*/
|
package/src/tui/slash.ts
CHANGED
|
@@ -20,6 +20,7 @@ export const SLASH_COMMANDS: readonly SlashCommand[] = [
|
|
|
20
20
|
{ name: "sync", hint: "Sync with base (wizard)" },
|
|
21
21
|
{ name: "pr", hint: "Open pull request" },
|
|
22
22
|
{ name: "finish", hint: "Finish / merge (wizard)" },
|
|
23
|
+
{ name: "release", hint: "Quick release from dev" },
|
|
23
24
|
{ name: "continue", hint: "Continue suspended run" },
|
|
24
25
|
{ name: "switch", hint: "Switch branch" },
|
|
25
26
|
{ name: "list", hint: "List branches" },
|
package/src/types.ts
CHANGED
|
@@ -27,6 +27,7 @@ export type Command =
|
|
|
27
27
|
| "init"
|
|
28
28
|
| "start"
|
|
29
29
|
| "finish"
|
|
30
|
+
| "release"
|
|
30
31
|
| "switch"
|
|
31
32
|
| "delete"
|
|
32
33
|
| "list"
|
|
@@ -52,6 +53,7 @@ export const ALL_COMMANDS: Command[] = [
|
|
|
52
53
|
"init",
|
|
53
54
|
"start",
|
|
54
55
|
"finish",
|
|
56
|
+
"release",
|
|
55
57
|
"switch",
|
|
56
58
|
"delete",
|
|
57
59
|
"list",
|
|
@@ -132,6 +134,11 @@ export interface ParsedArgs {
|
|
|
132
134
|
bumpDirection?: BumpDirection;
|
|
133
135
|
/** Bump type (patch | minor | major). */
|
|
134
136
|
bumpType?: BumpType;
|
|
137
|
+
/**
|
|
138
|
+
* Quick release: tag the package version already on disk (no bump commit).
|
|
139
|
+
* From `gflows release current`.
|
|
140
|
+
*/
|
|
141
|
+
keepCurrent?: boolean;
|
|
135
142
|
/** Config get/set action. */
|
|
136
143
|
configAction?: ConfigAction;
|
|
137
144
|
/** Config key (main, dev, remote, or prefixes.<type>). */
|