gflows 0.1.18 → 1.0.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.
- package/AGENTS.md +78 -0
- package/CHANGELOG.md +40 -0
- package/README.md +279 -505
- package/llms.txt +22 -0
- package/package.json +29 -23
- package/src/cli.ts +21 -367
- package/src/commands/abort.ts +39 -0
- package/src/commands/bump.ts +10 -10
- package/src/commands/completion.ts +14 -4
- package/src/commands/config.ts +123 -0
- package/src/commands/continue.ts +40 -0
- package/src/commands/delete.ts +4 -4
- package/src/commands/doctor.ts +143 -0
- package/src/commands/finish.ts +363 -137
- package/src/commands/help.ts +29 -21
- package/src/commands/init.ts +99 -18
- package/src/commands/list.ts +6 -1
- package/src/commands/mcp.ts +238 -0
- package/src/commands/pr.ts +120 -0
- package/src/commands/schema.ts +98 -0
- package/src/commands/start.ts +33 -31
- package/src/commands/status.ts +70 -51
- package/src/commands/switch.ts +14 -19
- package/src/commands/sync.ts +160 -0
- package/src/commands/undo.ts +78 -0
- package/src/commands/version.ts +3 -15
- package/src/commands/viz.ts +18 -0
- package/src/dispatch.ts +21 -0
- package/src/errors.ts +55 -12
- package/src/flow.ts +157 -0
- package/src/git.ts +135 -8
- package/src/index.ts +24 -1
- package/src/interactive.ts +209 -0
- package/src/out.ts +11 -4
- package/src/package-scripts.ts +73 -0
- package/src/parse.ts +430 -0
- package/src/prompts.ts +89 -0
- package/src/recommend.ts +132 -0
- package/src/run-state.ts +165 -0
- package/src/tui/HubHome.tsx +343 -0
- package/src/tui/HubShell.tsx +186 -0
- package/src/tui/flows.tsx +140 -0
- package/src/tui/hub.ts +89 -0
- package/src/tui/prompts.tsx +207 -0
- package/src/types.ts +62 -3
- package/src/ui.ts +132 -0
- package/src/version.ts +24 -0
- package/src/viz.ts +294 -0
package/src/commands/finish.ts
CHANGED
|
@@ -1,81 +1,234 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Finish command: merge workflow branch into target(s), optional tag, delete branch, and push.
|
|
3
|
-
*
|
|
4
|
-
* Uses normal merge with optional --no-ff; on conflict exits with clear message. Release/hotfix: merge to main then dev, create tag.
|
|
3
|
+
* Guards empty/dirty finishes, delete-by-default, bugfix-from-main, plan preview, run-state.
|
|
5
4
|
* @module commands/finish
|
|
6
5
|
*/
|
|
7
6
|
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
8
9
|
import { getBranchTypeMeta, resolveConfig } from "../config.js";
|
|
9
|
-
import { EXIT_USER
|
|
10
|
-
import { BranchNotFoundError } from "../errors.js";
|
|
10
|
+
import { EXIT_USER } from "../constants.js";
|
|
11
|
+
import { BranchNotFoundError, DirtyWorkingTreeError, NothingToFinishError } from "../errors.js";
|
|
12
|
+
import {
|
|
13
|
+
filterWorkflowBranches,
|
|
14
|
+
formatMergeTarget,
|
|
15
|
+
normalizeTagVersion,
|
|
16
|
+
parseBranchTypeAndVersion,
|
|
17
|
+
primaryTargetBranch,
|
|
18
|
+
resolveMergeTarget,
|
|
19
|
+
} from "../flow.js";
|
|
11
20
|
import {
|
|
12
21
|
assertNoRebaseOrMerge,
|
|
13
22
|
assertNotDetached,
|
|
14
23
|
branchList,
|
|
15
24
|
checkout,
|
|
16
25
|
deleteBranch,
|
|
26
|
+
deleteRemoteBranch,
|
|
27
|
+
getAheadBehind,
|
|
17
28
|
getCurrentBranch,
|
|
29
|
+
getUpstream,
|
|
30
|
+
isClean,
|
|
18
31
|
merge,
|
|
19
32
|
push,
|
|
20
33
|
resolveRepoRoot,
|
|
34
|
+
resolveSha,
|
|
35
|
+
runGit,
|
|
21
36
|
tag,
|
|
22
37
|
tagExists,
|
|
23
38
|
} from "../git.js";
|
|
24
39
|
import { hint, success } from "../out.js";
|
|
25
|
-
import
|
|
40
|
+
import {
|
|
41
|
+
completeRun,
|
|
42
|
+
type GflowsRunState,
|
|
43
|
+
startRun,
|
|
44
|
+
suspendRun,
|
|
45
|
+
writeActiveRun,
|
|
46
|
+
} from "../run-state.js";
|
|
47
|
+
import type { BranchType, MergeTarget, ParsedArgs, ResolvedConfig } from "../types.js";
|
|
26
48
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
49
|
+
interface FinishContext {
|
|
50
|
+
branchToFinish: string;
|
|
51
|
+
type: BranchType;
|
|
52
|
+
version?: string;
|
|
53
|
+
mergeTarget: MergeTarget;
|
|
54
|
+
shouldDelete: boolean;
|
|
55
|
+
doPush: boolean;
|
|
56
|
+
noFf: boolean;
|
|
57
|
+
squash: boolean;
|
|
58
|
+
message?: string;
|
|
59
|
+
signTag: boolean;
|
|
60
|
+
noTag: boolean;
|
|
61
|
+
tagMessage?: string;
|
|
62
|
+
remote: string;
|
|
63
|
+
main: string;
|
|
64
|
+
dev: string;
|
|
65
|
+
bumpOnFinish: boolean;
|
|
31
66
|
}
|
|
32
67
|
|
|
33
68
|
/**
|
|
34
|
-
*
|
|
69
|
+
* Runs remaining finish steps from run state (also used by continue).
|
|
35
70
|
*/
|
|
36
|
-
async function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
71
|
+
export async function executeFinishSteps(
|
|
72
|
+
repoRoot: string,
|
|
73
|
+
state: GflowsRunState,
|
|
74
|
+
opts: { dryRun: boolean; verbose: boolean; quiet: boolean },
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
const ctx = state.context as unknown as FinishContext;
|
|
77
|
+
let i = state.nextStep;
|
|
78
|
+
while (i < state.steps.length) {
|
|
79
|
+
const step = state.steps[i];
|
|
80
|
+
if (!step) break;
|
|
81
|
+
try {
|
|
82
|
+
await runFinishStep(repoRoot, step.id, ctx, opts);
|
|
83
|
+
i += 1;
|
|
84
|
+
state.nextStep = i;
|
|
85
|
+
writeActiveRun(repoRoot, { ...state, status: "running" });
|
|
86
|
+
} catch (err) {
|
|
87
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
88
|
+
suspendRun(repoRoot, { ...state, nextStep: i }, msg);
|
|
89
|
+
throw err;
|
|
48
90
|
}
|
|
49
91
|
}
|
|
50
|
-
|
|
92
|
+
completeRun(repoRoot, state);
|
|
93
|
+
if (!opts.quiet && !opts.dryRun) {
|
|
94
|
+
const tagName = typeof state.undo.createdTag === "string" ? state.undo.createdTag : undefined;
|
|
95
|
+
const tagSuffix = tagName ? ` (tag ${tagName})` : "";
|
|
96
|
+
const targets = formatMergeTarget(ctx.mergeTarget, {
|
|
97
|
+
main: ctx.main,
|
|
98
|
+
dev: ctx.dev,
|
|
99
|
+
remote: ctx.remote,
|
|
100
|
+
prefixes: {
|
|
101
|
+
feature: "feature/",
|
|
102
|
+
bugfix: "bugfix/",
|
|
103
|
+
chore: "chore/",
|
|
104
|
+
release: "release/",
|
|
105
|
+
hotfix: "hotfix/",
|
|
106
|
+
spike: "spike/",
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
success(`gflows: finished '${ctx.branchToFinish}' into ${targets}${tagSuffix}.`);
|
|
110
|
+
hint("Run gflows start <type> <name> to create a new workflow branch.");
|
|
111
|
+
}
|
|
51
112
|
}
|
|
52
113
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
114
|
+
async function runFinishStep(
|
|
115
|
+
repoRoot: string,
|
|
116
|
+
stepId: string,
|
|
117
|
+
ctx: FinishContext,
|
|
118
|
+
opts: { dryRun: boolean; verbose: boolean; quiet: boolean },
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
const mergeOpts = {
|
|
121
|
+
...opts,
|
|
122
|
+
noFf: ctx.noFf,
|
|
123
|
+
squash: ctx.squash,
|
|
124
|
+
message: ctx.message,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
switch (stepId) {
|
|
128
|
+
case "bump": {
|
|
129
|
+
if (!ctx.bumpOnFinish) return;
|
|
130
|
+
// Bump is handled before run starts when requested; step reserved for resume
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
case "merge-dev": {
|
|
134
|
+
await checkout(repoRoot, ctx.dev, opts);
|
|
135
|
+
await merge(repoRoot, ctx.branchToFinish, mergeOpts);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
case "merge-main": {
|
|
139
|
+
await checkout(repoRoot, ctx.main, opts);
|
|
140
|
+
await merge(repoRoot, ctx.branchToFinish, mergeOpts);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
case "tag": {
|
|
144
|
+
if (ctx.noTag || !ctx.version) return;
|
|
145
|
+
const tagName = normalizeTagVersion(ctx.version);
|
|
146
|
+
await tag(repoRoot, tagName, {
|
|
147
|
+
...opts,
|
|
148
|
+
sign: ctx.signTag,
|
|
149
|
+
tagMessage: ctx.tagMessage,
|
|
150
|
+
});
|
|
151
|
+
if (!opts.quiet && !opts.dryRun) {
|
|
152
|
+
success(`gflows: created tag '${tagName}'.`);
|
|
67
153
|
}
|
|
68
|
-
return
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
case "merge-main-into-dev": {
|
|
157
|
+
await checkout(repoRoot, ctx.dev, opts);
|
|
158
|
+
await merge(repoRoot, ctx.main, { ...opts, noFf: ctx.noFf, message: ctx.message });
|
|
159
|
+
return;
|
|
69
160
|
}
|
|
161
|
+
case "delete": {
|
|
162
|
+
if (!ctx.shouldDelete) return;
|
|
163
|
+
const upstream = await getUpstream(repoRoot, ctx.branchToFinish, opts).catch(() => null);
|
|
164
|
+
await deleteBranch(repoRoot, ctx.branchToFinish, { ...opts, force: true });
|
|
165
|
+
if (!opts.quiet && !opts.dryRun) {
|
|
166
|
+
success(`gflows: deleted branch '${ctx.branchToFinish}'.`);
|
|
167
|
+
}
|
|
168
|
+
if (!opts.dryRun && upstream) {
|
|
169
|
+
const code = await deleteRemoteBranch(repoRoot, ctx.remote, ctx.branchToFinish, opts);
|
|
170
|
+
if (code === 0 && !opts.quiet) {
|
|
171
|
+
success(`gflows: deleted remote branch '${ctx.remote}/${ctx.branchToFinish}'.`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
case "push": {
|
|
177
|
+
if (!ctx.doPush) return;
|
|
178
|
+
const refsToPush: string[] = [ctx.dev];
|
|
179
|
+
if (ctx.mergeTarget === "main-then-dev") {
|
|
180
|
+
refsToPush.push(ctx.main);
|
|
181
|
+
}
|
|
182
|
+
const didCreateTag = Boolean(ctx.version && !ctx.noTag);
|
|
183
|
+
const pushCode = await push(repoRoot, ctx.remote, refsToPush, didCreateTag, opts);
|
|
184
|
+
if (pushCode !== 0) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
"Merge and tag succeeded locally, but push failed. Retry with `git push` or `gflows continue` / `gflows finish ... --push`.",
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (!opts.quiet && !opts.dryRun) {
|
|
190
|
+
success(`gflows: pushed to ${ctx.remote}.`);
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
case "changelog": {
|
|
195
|
+
maybeTouchChangelog(repoRoot, ctx.version, opts);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
default:
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function maybeTouchChangelog(
|
|
204
|
+
repoRoot: string,
|
|
205
|
+
version: string | undefined,
|
|
206
|
+
opts: { dryRun: boolean; quiet: boolean },
|
|
207
|
+
): void {
|
|
208
|
+
if (!version) return;
|
|
209
|
+
const path = join(repoRoot, "CHANGELOG.md");
|
|
210
|
+
if (!existsSync(path)) return;
|
|
211
|
+
if (opts.dryRun) return;
|
|
212
|
+
try {
|
|
213
|
+
const raw = readFileSync(path, "utf-8");
|
|
214
|
+
if (!raw.includes("## [Unreleased]")) return;
|
|
215
|
+
const tag = normalizeTagVersion(version);
|
|
216
|
+
if (raw.includes(`## [${tag.replace(/^v/, "")}]`) || raw.includes(`## [${tag}]`)) return;
|
|
217
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
218
|
+
const ver = tag.replace(/^v/, "");
|
|
219
|
+
const stub = `## [Unreleased]\n\n## [${ver}] - ${date}\n\n`;
|
|
220
|
+
const updated = raw.replace("## [Unreleased]\n", stub);
|
|
221
|
+
writeFileSync(path, updated, "utf-8");
|
|
222
|
+
if (!opts.quiet) {
|
|
223
|
+
success(`gflows: updated CHANGELOG.md for ${ver}.`);
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
// ignore changelog failures
|
|
70
227
|
}
|
|
71
|
-
return null;
|
|
72
228
|
}
|
|
73
229
|
|
|
74
230
|
/**
|
|
75
|
-
* Runs the finish command
|
|
76
|
-
* Pre-checks: repo, not detached HEAD, no rebase/merge, branch is not main/dev; for release/hotfix tag must not exist.
|
|
77
|
-
*
|
|
78
|
-
* @param args - Parsed CLI args (cwd, type, branch, push, noPush, remote, dryRun, verbose, quiet, yes, noFf, deleteAfterFinish, noDeleteAfterFinish, signTag, noTag, tagMessage).
|
|
231
|
+
* Runs the finish command.
|
|
79
232
|
*/
|
|
80
233
|
export async function run(args: ParsedArgs): Promise<void> {
|
|
81
234
|
const repoRoot = await resolveRepoRoot(args.cwd);
|
|
@@ -85,14 +238,9 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
85
238
|
{ verbose: args.verbose },
|
|
86
239
|
);
|
|
87
240
|
|
|
88
|
-
const opts
|
|
89
|
-
dryRun: args.dryRun,
|
|
90
|
-
verbose: args.verbose,
|
|
91
|
-
};
|
|
92
|
-
|
|
241
|
+
const opts = { dryRun: args.dryRun, verbose: args.verbose, quiet: args.quiet };
|
|
93
242
|
const isTTY = Boolean(process.stdin.isTTY);
|
|
94
243
|
|
|
95
|
-
// Resolve branch to finish (and guard main/dev early so we can error before picker)
|
|
96
244
|
let branchToFinish: string;
|
|
97
245
|
const explicitBranch =
|
|
98
246
|
typeof args.branch === "string" && args.branch.trim() !== "" ? args.branch.trim() : undefined;
|
|
@@ -100,15 +248,24 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
100
248
|
if (explicitBranch) {
|
|
101
249
|
branchToFinish = explicitBranch;
|
|
102
250
|
} else if (isTTY) {
|
|
103
|
-
const workflow =
|
|
251
|
+
const workflow = filterWorkflowBranches(
|
|
252
|
+
await branchList(repoRoot, { dryRun: false, verbose: false }),
|
|
253
|
+
config.prefixes,
|
|
254
|
+
);
|
|
104
255
|
if (workflow.length === 0) {
|
|
105
256
|
console.error("gflows finish: no workflow branches found.");
|
|
257
|
+
hint("Create one with: gflows start feature <name>");
|
|
106
258
|
process.exit(EXIT_USER);
|
|
107
259
|
}
|
|
108
|
-
const
|
|
109
|
-
|
|
260
|
+
const current = await getCurrentBranch(repoRoot, opts);
|
|
261
|
+
const { selectPrompt } = await import("../prompts.js");
|
|
262
|
+
branchToFinish = await selectPrompt({
|
|
110
263
|
message: "Branch to finish",
|
|
111
|
-
|
|
264
|
+
options: workflow.map((b) => ({
|
|
265
|
+
label: b === current ? `${b} (current)` : b,
|
|
266
|
+
value: b,
|
|
267
|
+
})),
|
|
268
|
+
initialValue: current && workflow.includes(current) ? current : workflow[0],
|
|
112
269
|
});
|
|
113
270
|
} else {
|
|
114
271
|
const current = await getCurrentBranch(repoRoot, opts);
|
|
@@ -121,7 +278,6 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
121
278
|
branchToFinish = current;
|
|
122
279
|
}
|
|
123
280
|
|
|
124
|
-
// Refuse finish on main or dev
|
|
125
281
|
if (branchToFinish === config.main || branchToFinish === config.dev) {
|
|
126
282
|
console.error(
|
|
127
283
|
`gflows finish: cannot finish the long-lived branch '${branchToFinish}'. Finish a workflow branch (feature, bugfix, etc.) instead.`,
|
|
@@ -129,12 +285,11 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
129
285
|
process.exit(2);
|
|
130
286
|
}
|
|
131
287
|
|
|
132
|
-
// Infer type from branch name (or use args.type if provided and consistent)
|
|
133
288
|
const parsed = parseBranchTypeAndVersion(branchToFinish, config.prefixes);
|
|
134
289
|
const type: BranchType | undefined = args.type ?? parsed?.type ?? undefined;
|
|
135
290
|
if (!type) {
|
|
136
291
|
console.error(
|
|
137
|
-
`gflows finish: cannot determine branch type for '${branchToFinish}'. Specify type (e.g. gflows finish feature) or use a
|
|
292
|
+
`gflows finish: cannot determine branch type for '${branchToFinish}'. Specify type (e.g. gflows finish feature) or use a known prefix (${Object.values(config.prefixes).join(", ")}).`,
|
|
138
293
|
);
|
|
139
294
|
process.exit(EXIT_USER);
|
|
140
295
|
}
|
|
@@ -145,19 +300,38 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
145
300
|
process.exit(EXIT_USER);
|
|
146
301
|
}
|
|
147
302
|
|
|
148
|
-
const meta = getBranchTypeMeta(type);
|
|
149
303
|
const version = parsed?.version;
|
|
304
|
+
const meta = getBranchTypeMeta(type);
|
|
305
|
+
const mergeTarget = await resolveMergeTarget(repoRoot, branchToFinish, type, config, opts);
|
|
150
306
|
|
|
151
|
-
// Pre-checks: not detached, no rebase/merge
|
|
152
307
|
await assertNotDetached(repoRoot);
|
|
153
308
|
assertNoRebaseOrMerge(repoRoot);
|
|
154
309
|
|
|
155
|
-
|
|
310
|
+
if (!args.force) {
|
|
311
|
+
const current = await getCurrentBranch(repoRoot, opts);
|
|
312
|
+
if (current === branchToFinish) {
|
|
313
|
+
const clean = await isClean(repoRoot, opts);
|
|
314
|
+
if (!clean) {
|
|
315
|
+
throw new DirtyWorkingTreeError(
|
|
316
|
+
"Working tree has uncommitted changes. Commit or stash them before finish, or use --force.",
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const primary = primaryTargetBranch(mergeTarget, config);
|
|
323
|
+
const { ahead } = await getAheadBehind(repoRoot, primary, branchToFinish, opts);
|
|
324
|
+
if (ahead === 0) {
|
|
325
|
+
throw new NothingToFinishError(
|
|
326
|
+
`Nothing to finish: '${branchToFinish}' has no commits beyond '${primary}'.`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
156
330
|
if (meta.tagOnFinish && version) {
|
|
157
331
|
const tagName = normalizeTagVersion(version);
|
|
158
|
-
|
|
159
|
-
if (exists) {
|
|
332
|
+
if (await tagExists(repoRoot, tagName, opts)) {
|
|
160
333
|
console.error(`gflows finish: tag '${tagName}' already exists.`);
|
|
334
|
+
hint("Use a new version, or delete the tag if you intend to recreate it.");
|
|
161
335
|
process.exit(2);
|
|
162
336
|
}
|
|
163
337
|
} else if (meta.tagOnFinish && !version) {
|
|
@@ -167,7 +341,6 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
167
341
|
process.exit(EXIT_USER);
|
|
168
342
|
}
|
|
169
343
|
|
|
170
|
-
// Ensure the branch we're finishing exists (e.g. if -B was used)
|
|
171
344
|
const branches = await branchList(repoRoot, { ...opts, dryRun: false });
|
|
172
345
|
if (!branches.includes(branchToFinish)) {
|
|
173
346
|
throw new BranchNotFoundError(
|
|
@@ -175,92 +348,145 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
175
348
|
);
|
|
176
349
|
}
|
|
177
350
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
await
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const tagName = normalizeTagVersion(version);
|
|
189
|
-
await tag(repoRoot, tagName, {
|
|
190
|
-
...opts,
|
|
191
|
-
sign: args.signTag,
|
|
192
|
-
tagMessage: args.tagMessage,
|
|
193
|
-
});
|
|
194
|
-
if (!args.quiet && !args.dryRun) {
|
|
195
|
-
success(`gflows: created tag '${tagName}'.`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
await checkout(repoRoot, config.dev, opts);
|
|
200
|
-
await merge(repoRoot, config.main, { ...opts, noFf });
|
|
351
|
+
// Delete default ON
|
|
352
|
+
let shouldDelete = true;
|
|
353
|
+
if (args.noDeleteAfterFinish) shouldDelete = false;
|
|
354
|
+
else if (args.deleteAfterFinish) shouldDelete = true;
|
|
355
|
+
else if (!args.yes && isTTY) {
|
|
356
|
+
const { confirmPrompt } = await import("../prompts.js");
|
|
357
|
+
shouldDelete = await confirmPrompt({
|
|
358
|
+
message: "Delete branch after finish?",
|
|
359
|
+
initialValue: true,
|
|
360
|
+
});
|
|
201
361
|
}
|
|
362
|
+
// -y accepts plan including default delete
|
|
202
363
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
364
|
+
let doPush = false;
|
|
365
|
+
if (args.push && !args.noPush) doPush = true;
|
|
366
|
+
else if (args.noPush) doPush = false;
|
|
367
|
+
else if (!isTTY) {
|
|
368
|
+
console.error("gflows finish: specify --push (-p) or --no-push (-P) when not interactive.");
|
|
369
|
+
process.exit(EXIT_USER);
|
|
370
|
+
} else if (!args.yes) {
|
|
371
|
+
const { confirmPrompt } = await import("../prompts.js");
|
|
372
|
+
doPush = await confirmPrompt({
|
|
373
|
+
message: "Push after finish?",
|
|
374
|
+
initialValue: false,
|
|
375
|
+
});
|
|
215
376
|
}
|
|
216
|
-
|
|
217
|
-
|
|
377
|
+
|
|
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;
|
|
218
393
|
}
|
|
219
394
|
|
|
220
|
-
if (
|
|
221
|
-
await
|
|
222
|
-
if (!args.quiet) {
|
|
223
|
-
success(`gflows: deleted branch '${branchToFinish}'.`);
|
|
224
|
-
}
|
|
395
|
+
if (args.bumpOnFinish && (type === "release" || type === "hotfix") && version) {
|
|
396
|
+
await bumpAndCommit(repoRoot, version, args, config);
|
|
225
397
|
}
|
|
226
398
|
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
?
|
|
230
|
-
|
|
231
|
-
|
|
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
|
+
];
|
|
232
414
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
const ctx: FinishContext = {
|
|
421
|
+
branchToFinish,
|
|
422
|
+
type,
|
|
423
|
+
version,
|
|
424
|
+
mergeTarget,
|
|
425
|
+
shouldDelete,
|
|
426
|
+
doPush,
|
|
427
|
+
noFf: args.noFf,
|
|
428
|
+
squash: args.squash,
|
|
429
|
+
message: args.message,
|
|
430
|
+
signTag: args.signTag,
|
|
431
|
+
noTag: args.noTag,
|
|
432
|
+
tagMessage: args.tagMessage,
|
|
433
|
+
remote: args.remote ?? config.remote,
|
|
434
|
+
main: config.main,
|
|
435
|
+
dev: config.dev,
|
|
436
|
+
bumpOnFinish: args.bumpOnFinish,
|
|
437
|
+
};
|
|
241
438
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
439
|
+
const state = startRun(repoRoot, "finish", steps, ctx as unknown as Record<string, unknown>, {
|
|
440
|
+
branchSha,
|
|
441
|
+
mainSha,
|
|
442
|
+
devSha,
|
|
443
|
+
prevBranch,
|
|
444
|
+
createdTag: tagName ?? null,
|
|
445
|
+
branchName: branchToFinish,
|
|
446
|
+
shouldDelete,
|
|
447
|
+
main: config.main,
|
|
448
|
+
dev: config.dev,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
await executeFinishSteps(repoRoot, state, opts);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function bumpAndCommit(
|
|
455
|
+
repoRoot: string,
|
|
456
|
+
version: string,
|
|
457
|
+
args: ParsedArgs,
|
|
458
|
+
_config: ResolvedConfig,
|
|
459
|
+
): Promise<void> {
|
|
460
|
+
const ver = version.replace(/^v/, "");
|
|
461
|
+
// Use bump command logic via spawning package files — light inline for finish --bump
|
|
462
|
+
const pkgPath = join(repoRoot, "package.json");
|
|
463
|
+
if (!existsSync(pkgPath)) return;
|
|
464
|
+
try {
|
|
465
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { version?: string };
|
|
466
|
+
pkg.version = ver;
|
|
467
|
+
if (!args.dryRun) {
|
|
468
|
+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
|
|
247
469
|
}
|
|
248
|
-
const
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
);
|
|
253
|
-
process.exit(2);
|
|
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");
|
|
254
475
|
}
|
|
255
|
-
if (!args.
|
|
256
|
-
|
|
476
|
+
if (!args.dryRun) {
|
|
477
|
+
await runGit(["add", "package.json", "jsr.json"], {
|
|
478
|
+
cwd: repoRoot,
|
|
479
|
+
dryRun: args.dryRun,
|
|
480
|
+
verbose: args.verbose,
|
|
481
|
+
});
|
|
482
|
+
await runGit(["commit", "-m", `chore: bump to ${ver}`], {
|
|
483
|
+
cwd: repoRoot,
|
|
484
|
+
dryRun: args.dryRun,
|
|
485
|
+
verbose: args.verbose,
|
|
486
|
+
});
|
|
487
|
+
if (!args.quiet) success(`gflows: bumped version to ${ver} and committed.`);
|
|
257
488
|
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (!args.quiet && !args.dryRun) {
|
|
261
|
-
const tagSuffix = createdTagName ? ` (tag ${createdTagName})` : "";
|
|
262
|
-
success(`gflows: finished '${branchToFinish}' into ${meta.mergeTarget}${tagSuffix}.`);
|
|
263
|
-
// Hint: suggest next step — create a new workflow branch
|
|
264
|
-
hint("Run gflows start <type> <name> to create a new workflow branch.");
|
|
489
|
+
} catch {
|
|
490
|
+
hint("finish --bump: could not bump/commit version files; continuing finish.");
|
|
265
491
|
}
|
|
266
492
|
}
|