gflows 0.1.17 → 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 -502
- package/llms.txt +22 -0
- package/package.json +30 -23
- package/src/cli.ts +21 -364
- 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 -20
- 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 +50 -20
- 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 +63 -4
- package/src/ui.ts +132 -0
- package/src/version.ts +24 -0
- package/src/viz.ts +294 -0
package/llms.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# gflows
|
|
2
|
+
|
|
3
|
+
> Lightweight Bun/TypeScript CLI for main+dev Git branching workflows (feature, bugfix, chore, release, hotfix, spike).
|
|
4
|
+
|
|
5
|
+
## Docs
|
|
6
|
+
|
|
7
|
+
- [AGENTS.md](AGENTS.md): instructions for coding agents (recipes, exit codes, MCP)
|
|
8
|
+
- [README.md](README.md): human quick start, command reference, flowcharts
|
|
9
|
+
- Machine catalog: run `gflows schema` (JSON)
|
|
10
|
+
|
|
11
|
+
## Key commands
|
|
12
|
+
|
|
13
|
+
- `gflows` (TTY): Ink fullscreen hub (frame, tips, actions, `/` slash commands, status bar); Clack prompts
|
|
14
|
+
- `gflows viz`: scrollback status panel — lifecycle, branches, recommended next step
|
|
15
|
+
- `gflows init` / `start` / `finish` / `sync` / `pr`
|
|
16
|
+
- `gflows continue` / `undo` / `abort` — recovery after conflicts
|
|
17
|
+
- `gflows doctor` / `config` / `status --json` / `mcp`
|
|
18
|
+
|
|
19
|
+
## Package
|
|
20
|
+
|
|
21
|
+
- npm: `gflows`
|
|
22
|
+
- JSR: `@alialnaghmoush/gflows`
|
package/package.json
CHANGED
|
@@ -1,43 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gflows",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"author": "Ali AlNaghmoush (https://github.com/alialnaghmoush)",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/alialnaghmoush/gflows.git"
|
|
8
|
+
},
|
|
7
9
|
"module": "src/index.ts",
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@biomejs/biome": "^2.4.4",
|
|
12
|
+
"@types/bun": "latest",
|
|
13
|
+
"@types/react": "^19.2.17",
|
|
14
|
+
"typescript": "^5"
|
|
15
|
+
},
|
|
8
16
|
"bin": {
|
|
9
17
|
"gflows": "src/cli.ts"
|
|
10
18
|
},
|
|
19
|
+
"description": "A lightweight CLI for consistent Git branching workflows (main + dev, feature/bugfix/chore/release/hotfix).",
|
|
20
|
+
"engines": {
|
|
21
|
+
"bun": ">=1.0.0"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"AGENTS.md",
|
|
28
|
+
"llms.txt",
|
|
29
|
+
"CHANGELOG.md"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
11
32
|
"scripts": {
|
|
12
33
|
"test": "bun test",
|
|
13
34
|
"typecheck": "tsc --noEmit",
|
|
14
35
|
"lint": "biome check .",
|
|
15
36
|
"lint:fix": "biome check . --write",
|
|
16
37
|
"format": "biome format . --write",
|
|
38
|
+
"check": "bun run test && bun run typecheck && bun run lint",
|
|
17
39
|
"gflows": "bun run src/cli.ts",
|
|
18
40
|
"publish:all": "bun run scripts/publish.ts",
|
|
19
41
|
"publish:npm": "bun run scripts/publish.ts -- --npm-only",
|
|
20
42
|
"publish:jsr": "bun run scripts/publish.ts -- --jsr-only"
|
|
21
43
|
},
|
|
22
|
-
"
|
|
23
|
-
"src",
|
|
24
|
-
"README.md",
|
|
25
|
-
"LICENSE"
|
|
26
|
-
],
|
|
27
|
-
"engines": {
|
|
28
|
-
"bun": ">=1.0.0"
|
|
29
|
-
},
|
|
30
|
-
"author": "Ali AlNaghmoush (https://github.com/alialnaghmoush)",
|
|
31
|
-
"repository": {
|
|
32
|
-
"type": "git",
|
|
33
|
-
"url": "git+https://github.com/alialnaghmoush/gflows.git"
|
|
34
|
-
},
|
|
44
|
+
"type": "module",
|
|
35
45
|
"dependencies": {
|
|
36
|
-
"@
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"@biomejs/biome": "^2.4.4",
|
|
40
|
-
"@types/bun": "latest",
|
|
41
|
-
"typescript": "^5"
|
|
46
|
+
"@clack/prompts": "^1.7.0",
|
|
47
|
+
"ink": "^7.1.1",
|
|
48
|
+
"react": "^19.2.8"
|
|
42
49
|
}
|
|
43
50
|
}
|
package/src/cli.ts
CHANGED
|
@@ -6,402 +6,59 @@
|
|
|
6
6
|
* @module cli
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { existsSync, statSync } from "node:fs";
|
|
10
|
-
import { resolve } from "node:path";
|
|
11
|
-
import { parseArgs } from "node:util";
|
|
12
9
|
import { EXIT_GIT, EXIT_OK, EXIT_USER } from "./constants.js";
|
|
13
|
-
import { exitCodeForError } from "./errors.js";
|
|
14
|
-
import
|
|
10
|
+
import { exitCodeForError, printError } from "./errors.js";
|
|
11
|
+
import { parse } from "./parse.js";
|
|
12
|
+
import type { ParsedArgs } from "./types.js";
|
|
15
13
|
|
|
16
14
|
/** Last parsed args, set at start of run(); used by catch/rejection to respect -v for stack trace. */
|
|
17
15
|
let lastParsedArgs: ParsedArgs | null = null;
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
"init",
|
|
21
|
-
"start",
|
|
22
|
-
"finish",
|
|
23
|
-
"switch",
|
|
24
|
-
"delete",
|
|
25
|
-
"list",
|
|
26
|
-
"bump",
|
|
27
|
-
"completion",
|
|
28
|
-
"status",
|
|
29
|
-
"help",
|
|
30
|
-
"version",
|
|
31
|
-
];
|
|
17
|
+
export { parse } from "./parse.js";
|
|
32
18
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
t: "status",
|
|
44
|
-
h: "help",
|
|
45
|
-
V: "version",
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
function buildParseArgsOptions() {
|
|
49
|
-
return {
|
|
50
|
-
args: Bun.argv.slice(2),
|
|
51
|
-
strict: false,
|
|
52
|
-
allowPositionals: true,
|
|
53
|
-
options: {
|
|
54
|
-
// -C / --path (must be first conceptually for cwd resolution)
|
|
55
|
-
path: { type: "string" as const, short: "C" },
|
|
56
|
-
// Command shorts
|
|
57
|
-
init: { type: "boolean" as const, short: "I" },
|
|
58
|
-
start: { type: "boolean" as const, short: "S" },
|
|
59
|
-
finish: { type: "boolean" as const, short: "F" },
|
|
60
|
-
switch: { type: "boolean" as const, short: "W" },
|
|
61
|
-
delete: { type: "boolean" as const, short: "L" },
|
|
62
|
-
list: { type: "boolean" as const, short: "l" },
|
|
63
|
-
status: { type: "boolean" as const, short: "t" },
|
|
64
|
-
help: { type: "boolean" as const, short: "h" },
|
|
65
|
-
version: { type: "boolean" as const, short: "V" },
|
|
66
|
-
// Type shorts (-r is context-dependent: list → include-remote, start/finish → release)
|
|
67
|
-
feature: { type: "boolean" as const, short: "f" },
|
|
68
|
-
bugfix: { type: "boolean" as const, short: "b" },
|
|
69
|
-
chore: { type: "boolean" as const, short: "c" },
|
|
70
|
-
release: { type: "boolean" as const, short: "r" },
|
|
71
|
-
hotfix: { type: "boolean" as const, short: "x" },
|
|
72
|
-
spike: { type: "boolean" as const, short: "e" },
|
|
73
|
-
// Common
|
|
74
|
-
push: { type: "boolean" as const, short: "p" },
|
|
75
|
-
noPush: { type: "boolean" as const, short: "P" },
|
|
76
|
-
"no-push": { type: "boolean" as const },
|
|
77
|
-
main: { type: "string" as const },
|
|
78
|
-
dev: { type: "string" as const },
|
|
79
|
-
remote: { type: "string" as const, short: "R" },
|
|
80
|
-
from: { type: "string" as const, short: "o" },
|
|
81
|
-
branch: { type: "string" as const, short: "B" },
|
|
82
|
-
yes: { type: "boolean" as const, short: "y" },
|
|
83
|
-
dryRun: { type: "boolean" as const, short: "d" },
|
|
84
|
-
verbose: { type: "boolean" as const, short: "v" },
|
|
85
|
-
quiet: { type: "boolean" as const, short: "q" },
|
|
86
|
-
force: { type: "boolean" as const },
|
|
87
|
-
// finish (-D/--delete-branch, -N/--no-delete)
|
|
88
|
-
noFf: { type: "boolean" as const },
|
|
89
|
-
deleteBranch: { type: "boolean" as const, short: "D" },
|
|
90
|
-
noDelete: { type: "boolean" as const, short: "N" },
|
|
91
|
-
sign: { type: "boolean" as const, short: "s" },
|
|
92
|
-
noTag: { type: "boolean" as const, short: "T" },
|
|
93
|
-
tagMessage: { type: "string" as const, short: "M" },
|
|
94
|
-
message: { type: "string" as const, short: "m" },
|
|
95
|
-
// list (-r is context-dependent: list → include-remote; start/finish → release)
|
|
96
|
-
includeRemote: { type: "boolean" as const },
|
|
97
|
-
"include-remote": { type: "boolean" as const },
|
|
98
|
-
// switch: explicit mode (--restore, --clean, --cancel, --move)
|
|
99
|
-
restore: { type: "boolean" as const },
|
|
100
|
-
clean: { type: "boolean" as const },
|
|
101
|
-
cancel: { type: "boolean" as const },
|
|
102
|
-
move: { type: "boolean" as const },
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Resolve -C/--path to absolute directory; validate it exists and is a directory. */
|
|
108
|
-
function resolveCwd(pathFlag: string | undefined): string {
|
|
109
|
-
if (!pathFlag || pathFlag.trim() === "") {
|
|
110
|
-
return process.cwd();
|
|
111
|
-
}
|
|
112
|
-
const absolute = resolve(process.cwd(), pathFlag.trim());
|
|
113
|
-
if (!existsSync(absolute)) {
|
|
114
|
-
console.error(`gflows: path does not exist: ${absolute}`);
|
|
115
|
-
process.exit(EXIT_USER);
|
|
116
|
-
}
|
|
117
|
-
const stat = statSync(absolute, { throwIfNoEntry: false });
|
|
118
|
-
if (!stat || !stat.isDirectory()) {
|
|
119
|
-
console.error(`gflows: path is not a directory: ${absolute}`);
|
|
120
|
-
process.exit(EXIT_USER);
|
|
121
|
-
}
|
|
122
|
-
return absolute;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Returns the command name closest to `input` by edit distance, or undefined if no close match.
|
|
127
|
-
* Used for "did you mean?" when the user mistypes a command.
|
|
128
|
-
*/
|
|
129
|
-
function closestCommand(input: string): Command | undefined {
|
|
130
|
-
if (!input || input.length < 2) return undefined;
|
|
131
|
-
const target = input.toLowerCase();
|
|
132
|
-
let best: Command | undefined;
|
|
133
|
-
let bestDistance = 3; // only suggest if within 2 edits
|
|
134
|
-
for (const cmd of COMMANDS) {
|
|
135
|
-
const d = editDistance(target, cmd);
|
|
136
|
-
if (d < bestDistance) {
|
|
137
|
-
bestDistance = d;
|
|
138
|
-
best = cmd as Command;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return best;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Levenshtein edit distance between two strings. */
|
|
145
|
-
function editDistance(a: string, b: string): number {
|
|
146
|
-
const m = a.length;
|
|
147
|
-
const n = b.length;
|
|
148
|
-
const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
149
|
-
for (let i = 0; i <= m; i++) {
|
|
150
|
-
const row = dp[i];
|
|
151
|
-
if (row) row[0] = i;
|
|
152
|
-
}
|
|
153
|
-
for (let j = 0; j <= n; j++) {
|
|
154
|
-
const row = dp[0];
|
|
155
|
-
if (row) row[j] = j;
|
|
156
|
-
}
|
|
157
|
-
for (let i = 1; i <= m; i++) {
|
|
158
|
-
for (let j = 1; j <= n; j++) {
|
|
159
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
160
|
-
const v1 = dp[i - 1]?.[j] ?? 0;
|
|
161
|
-
const v2 = dp[i]?.[j - 1] ?? 0;
|
|
162
|
-
const v3 = dp[i - 1]?.[j - 1] ?? 0;
|
|
163
|
-
const rowI = dp[i];
|
|
164
|
-
if (rowI) rowI[j] = Math.min(v1 + 1, v2 + 1, v3 + cost);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return dp[m]?.[n] ?? 0;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Resolve command from positionals and short flags. Short wins if both present. */
|
|
171
|
-
function resolveCommand(
|
|
172
|
-
positionals: string[],
|
|
173
|
-
values: Record<string, string | boolean | undefined>,
|
|
174
|
-
): Command | undefined {
|
|
175
|
-
for (const [_short, cmd] of Object.entries(SHORT_TO_COMMAND)) {
|
|
176
|
-
const key = cmd === "delete" ? "delete" : cmd;
|
|
177
|
-
if (values[key] === true) return cmd as Command;
|
|
178
|
-
}
|
|
179
|
-
const first = positionals[0];
|
|
180
|
-
if (first && COMMANDS.includes(first as Command)) {
|
|
181
|
-
return first as Command;
|
|
182
|
-
}
|
|
183
|
-
return undefined;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** Resolve branch type for start/finish/list. -r means release for start/finish; for list, -r means include-remote (handled in flags). */
|
|
187
|
-
function resolveType(
|
|
188
|
-
command: Command,
|
|
189
|
-
positionals: string[],
|
|
190
|
-
values: Record<string, string | boolean | undefined>,
|
|
191
|
-
): BranchType | undefined {
|
|
192
|
-
if (command !== "start" && command !== "finish" && command !== "list") {
|
|
193
|
-
return undefined;
|
|
194
|
-
}
|
|
195
|
-
// Type from short flags (for start/finish, -r => release)
|
|
196
|
-
if (command === "list") {
|
|
197
|
-
// For list, -r is include-remote only (see includeRemote below); type from other shorts or positional (not -r)
|
|
198
|
-
if (values.feature === true) return "feature";
|
|
199
|
-
if (values.bugfix === true) return "bugfix";
|
|
200
|
-
if (values.chore === true) return "chore";
|
|
201
|
-
if (values.hotfix === true) return "hotfix";
|
|
202
|
-
if (values.spike === true) return "spike";
|
|
203
|
-
// release type for list only via positional, e.g. "gflows list release"
|
|
204
|
-
} else {
|
|
205
|
-
// start / finish: -r => release
|
|
206
|
-
if (values.release === true) return "release";
|
|
207
|
-
if (values.feature === true) return "feature";
|
|
208
|
-
if (values.bugfix === true) return "bugfix";
|
|
209
|
-
if (values.chore === true) return "chore";
|
|
210
|
-
if (values.hotfix === true) return "hotfix";
|
|
211
|
-
if (values.spike === true) return "spike";
|
|
212
|
-
}
|
|
213
|
-
// Type from second positional
|
|
214
|
-
const idx = positionals[0] && COMMANDS.includes(positionals[0] as Command) ? 1 : 0;
|
|
215
|
-
const pos = positionals[idx];
|
|
216
|
-
if (pos && BRANCH_TYPES.includes(pos as BranchType)) {
|
|
217
|
-
return pos as BranchType;
|
|
218
|
-
}
|
|
219
|
-
return undefined;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/** Resolve name (third positional for start; -B for finish; first positional for completion). */
|
|
223
|
-
function resolveName(
|
|
224
|
-
command: Command,
|
|
225
|
-
positionals: string[],
|
|
226
|
-
values: Record<string, string | boolean | undefined>,
|
|
227
|
-
): string | undefined {
|
|
228
|
-
const branch = values.branch;
|
|
229
|
-
if (typeof branch === "string" && branch.trim() !== "") {
|
|
230
|
-
return branch.trim();
|
|
231
|
-
}
|
|
232
|
-
const skip = positionals[0] && COMMANDS.includes(positionals[0] as Command) ? 1 : 0;
|
|
233
|
-
if (command === "start") {
|
|
234
|
-
const typeFromPos = positionals[skip] && BRANCH_TYPES.includes(positionals[skip] as BranchType);
|
|
235
|
-
if (typeFromPos) {
|
|
236
|
-
return positionals[skip + 1];
|
|
237
|
-
}
|
|
238
|
-
return positionals[skip];
|
|
239
|
-
}
|
|
240
|
-
if (command === "completion") {
|
|
241
|
-
const shell = positionals[skip];
|
|
242
|
-
if (shell === "bash" || shell === "zsh" || shell === "fish") return shell;
|
|
243
|
-
return undefined;
|
|
244
|
-
}
|
|
245
|
-
if (command === "bump") {
|
|
246
|
-
const dir = positionals[skip];
|
|
247
|
-
const _typ = positionals[skip + 1];
|
|
248
|
-
if (dir === "up" || dir === "down") {
|
|
249
|
-
return dir;
|
|
250
|
-
}
|
|
251
|
-
return undefined;
|
|
252
|
-
}
|
|
253
|
-
if (command === "switch") {
|
|
254
|
-
return positionals[skip];
|
|
255
|
-
}
|
|
256
|
-
return undefined;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/** Resolve bump direction and type from positionals (bump [up|down] [patch|minor|major]). */
|
|
260
|
-
function resolveBump(
|
|
261
|
-
positionals: string[],
|
|
262
|
-
_values: Record<string, string | boolean | undefined>,
|
|
263
|
-
): { direction?: "up" | "down"; type?: "patch" | "minor" | "major" } {
|
|
264
|
-
const skip = positionals[0] === "bump" ? 1 : 0;
|
|
265
|
-
const a = positionals[skip];
|
|
266
|
-
const b = positionals[skip + 1];
|
|
267
|
-
const direction = a === "up" || a === "down" ? a : undefined;
|
|
268
|
-
const type = b === "patch" || b === "minor" || b === "major" ? b : undefined;
|
|
269
|
-
return { direction, type };
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** Parse raw argv into ParsedArgs. Resolves -C first, then command/type/name and flags. */
|
|
273
|
-
export function parse(argv: string[] = Bun.argv.slice(2)): ParsedArgs {
|
|
274
|
-
const config = {
|
|
275
|
-
...buildParseArgsOptions(),
|
|
276
|
-
args: argv,
|
|
277
|
-
};
|
|
278
|
-
const { values, positionals } = parseArgs(config);
|
|
279
|
-
const v = values as Record<string, string | boolean | undefined>;
|
|
280
|
-
|
|
281
|
-
const pathRaw = v.path;
|
|
282
|
-
const pathStr = typeof pathRaw === "string" ? pathRaw : undefined;
|
|
283
|
-
const cwd = resolveCwd(pathStr);
|
|
284
|
-
|
|
285
|
-
const command = resolveCommand(positionals, v);
|
|
286
|
-
if (!command) {
|
|
287
|
-
const first = positionals[0];
|
|
288
|
-
const suggestion = typeof first === "string" ? closestCommand(first) : undefined;
|
|
289
|
-
if (suggestion) {
|
|
290
|
-
console.error(`gflows: unknown command '${first}'. Did you mean '${suggestion}'?`);
|
|
291
|
-
} else {
|
|
292
|
-
console.error("gflows: missing command. Use 'gflows help' for usage.");
|
|
19
|
+
/** Run the CLI: parse, dispatch, set exit code. */
|
|
20
|
+
async function run(): Promise<void> {
|
|
21
|
+
const rawArgv = Bun.argv.slice(2);
|
|
22
|
+
const isTTY = Boolean(process.stdin.isTTY);
|
|
23
|
+
|
|
24
|
+
if (rawArgv.length === 0) {
|
|
25
|
+
if (isTTY) {
|
|
26
|
+
const { runHub } = await import("./interactive.js");
|
|
27
|
+
await runHub(process.cwd());
|
|
28
|
+
return;
|
|
293
29
|
}
|
|
30
|
+
console.error("gflows: missing command. Use 'gflows help' for usage.");
|
|
294
31
|
process.exit(EXIT_USER);
|
|
295
32
|
}
|
|
296
33
|
|
|
297
|
-
const type = resolveType(command, positionals, v);
|
|
298
|
-
const name = resolveName(command, positionals, v);
|
|
299
|
-
const { direction: bumpDirection, type: bumpType } =
|
|
300
|
-
command === "bump" ? resolveBump(positionals, v) : { direction: undefined, type: undefined };
|
|
301
|
-
|
|
302
|
-
const branchNames =
|
|
303
|
-
command === "delete"
|
|
304
|
-
? positionals[0] && COMMANDS.includes(positionals[0] as Command)
|
|
305
|
-
? positionals.slice(1)
|
|
306
|
-
: positionals
|
|
307
|
-
: undefined;
|
|
308
|
-
|
|
309
|
-
// -r context: for list → includeRemote; for start/finish → already used as type release
|
|
310
|
-
const includeRemote =
|
|
311
|
-
command === "list"
|
|
312
|
-
? v.includeRemote === true || v["include-remote"] === true || v.release === true
|
|
313
|
-
: false;
|
|
314
|
-
|
|
315
|
-
let completionShell: "bash" | "zsh" | "fish" | undefined;
|
|
316
|
-
if (command === "completion" && name === "bash") completionShell = "bash";
|
|
317
|
-
else if (command === "completion" && name === "zsh") completionShell = "zsh";
|
|
318
|
-
else if (command === "completion" && name === "fish") completionShell = "fish";
|
|
319
|
-
|
|
320
|
-
let switchMode: "restore" | "clean" | "cancel" | "move" | undefined;
|
|
321
|
-
if (command === "switch") {
|
|
322
|
-
const restore = v.restore === true;
|
|
323
|
-
const clean = v.clean === true;
|
|
324
|
-
const cancel = v.cancel === true;
|
|
325
|
-
const move = v.move === true;
|
|
326
|
-
const count = [restore, clean, cancel, move].filter(Boolean).length;
|
|
327
|
-
if (count > 1) {
|
|
328
|
-
console.error(
|
|
329
|
-
"gflows switch: only one of --restore, --clean, --cancel, or --move may be used at a time.",
|
|
330
|
-
);
|
|
331
|
-
process.exit(EXIT_USER);
|
|
332
|
-
}
|
|
333
|
-
if (restore) switchMode = "restore";
|
|
334
|
-
else if (clean) switchMode = "clean";
|
|
335
|
-
else if (cancel) switchMode = "cancel";
|
|
336
|
-
else if (move) switchMode = "move";
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return {
|
|
340
|
-
command,
|
|
341
|
-
cwd,
|
|
342
|
-
type,
|
|
343
|
-
name,
|
|
344
|
-
completionShell,
|
|
345
|
-
branchNames,
|
|
346
|
-
bumpDirection,
|
|
347
|
-
bumpType,
|
|
348
|
-
push: v.push === true,
|
|
349
|
-
noPush: v.noPush === true || v["no-push"] === true,
|
|
350
|
-
main: typeof v.main === "string" && v.main.trim() !== "" ? v.main.trim() : undefined,
|
|
351
|
-
dev: typeof v.dev === "string" && v.dev.trim() !== "" ? v.dev.trim() : undefined,
|
|
352
|
-
remote: typeof v.remote === "string" ? v.remote : undefined,
|
|
353
|
-
branch: typeof v.branch === "string" ? v.branch : undefined,
|
|
354
|
-
yes: v.yes === true,
|
|
355
|
-
dryRun: v.dryRun === true,
|
|
356
|
-
verbose: v.verbose === true,
|
|
357
|
-
quiet: v.quiet === true,
|
|
358
|
-
force: v.force === true,
|
|
359
|
-
path: pathStr,
|
|
360
|
-
fromMain: v.from === "main",
|
|
361
|
-
noFf: v.noFf === true,
|
|
362
|
-
deleteAfterFinish: v.deleteBranch === true,
|
|
363
|
-
noDeleteAfterFinish: v.noDelete === true,
|
|
364
|
-
signTag: v.sign === true,
|
|
365
|
-
noTag: v.noTag === true,
|
|
366
|
-
tagMessage: typeof v.tagMessage === "string" ? v.tagMessage : undefined,
|
|
367
|
-
message: typeof v.message === "string" ? v.message : undefined,
|
|
368
|
-
includeRemote,
|
|
369
|
-
switchMode,
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/** Run the CLI: parse, dispatch, set exit code. */
|
|
374
|
-
async function run(): Promise<void> {
|
|
375
34
|
const args = parse();
|
|
376
35
|
lastParsedArgs = args;
|
|
377
|
-
const { command } = args;
|
|
378
36
|
|
|
379
|
-
if (command === "help") {
|
|
37
|
+
if (args.command === "help") {
|
|
380
38
|
const { run: runHelp } = await import("./commands/help.js");
|
|
381
39
|
await runHelp(args);
|
|
382
40
|
return;
|
|
383
41
|
}
|
|
384
|
-
if (command === "version") {
|
|
42
|
+
if (args.command === "version") {
|
|
385
43
|
const { run: runVersion } = await import("./commands/version.js");
|
|
386
44
|
await runVersion(args);
|
|
387
45
|
return;
|
|
388
46
|
}
|
|
389
47
|
|
|
390
|
-
const mod = await import(`./commands/${command}.js`).catch(() => null);
|
|
48
|
+
const mod = await import(`./commands/${args.command}.js`).catch(() => null);
|
|
391
49
|
if (!mod || typeof mod.run !== "function") {
|
|
392
|
-
console.error(`gflows: command '${command}' is not implemented.`);
|
|
50
|
+
console.error(`gflows: command '${args.command}' is not implemented.`);
|
|
393
51
|
process.exit(EXIT_GIT);
|
|
394
52
|
}
|
|
395
53
|
await mod.run(args);
|
|
396
54
|
}
|
|
397
55
|
|
|
398
|
-
// Top-level try/catch and unhandledRejection so exit code is always set
|
|
399
56
|
function main(): void {
|
|
400
57
|
let exitCode: number | null = null;
|
|
401
58
|
|
|
402
59
|
const handleRejection = (reason: unknown): void => {
|
|
403
60
|
if (exitCode !== null) return;
|
|
404
|
-
|
|
61
|
+
printError(reason);
|
|
405
62
|
const verbose = lastParsedArgs?.verbose ?? !!process.env.GFLOWS_VERBOSE;
|
|
406
63
|
if (verbose && reason instanceof Error && reason.stack) {
|
|
407
64
|
console.error(reason.stack);
|
|
@@ -419,7 +76,7 @@ function main(): void {
|
|
|
419
76
|
})
|
|
420
77
|
.catch((err: unknown) => {
|
|
421
78
|
if (exitCode !== null) return;
|
|
422
|
-
|
|
79
|
+
printError(err);
|
|
423
80
|
const verbose = lastParsedArgs?.verbose ?? !!process.env.GFLOWS_VERBOSE;
|
|
424
81
|
if (verbose && err instanceof Error && err.stack) {
|
|
425
82
|
console.error(err.stack);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abort a suspended gflows operation and clear merge/rebase state when present.
|
|
3
|
+
* @module commands/abort
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { isRebaseOrMergeInProgress, mergeAbort, rebaseAbort, resolveRepoRoot } from "../git.js";
|
|
9
|
+
import { hint, success } from "../out.js";
|
|
10
|
+
import { clearActiveRun, readActiveRun } from "../run-state.js";
|
|
11
|
+
import type { ParsedArgs } from "../types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Aborts the suspended run and any in-progress git merge/rebase.
|
|
15
|
+
*/
|
|
16
|
+
export async function run(args: ParsedArgs): Promise<void> {
|
|
17
|
+
const repoRoot = await resolveRepoRoot(args.cwd);
|
|
18
|
+
const state = readActiveRun(repoRoot);
|
|
19
|
+
const opts = { dryRun: args.dryRun, verbose: args.verbose };
|
|
20
|
+
|
|
21
|
+
if (isRebaseOrMergeInProgress(repoRoot)) {
|
|
22
|
+
const root = join(repoRoot, ".git");
|
|
23
|
+
if (existsSync(join(root, "MERGE_HEAD"))) {
|
|
24
|
+
await mergeAbort(repoRoot, opts);
|
|
25
|
+
} else if (existsSync(join(root, "rebase-merge")) || existsSync(join(root, "rebase-apply"))) {
|
|
26
|
+
await rebaseAbort(repoRoot, opts);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (state) {
|
|
31
|
+
clearActiveRun(repoRoot);
|
|
32
|
+
if (!args.quiet) {
|
|
33
|
+
success(`gflows: aborted suspended '${state.command}' operation.`);
|
|
34
|
+
}
|
|
35
|
+
} else if (!args.quiet) {
|
|
36
|
+
success("gflows: no suspended operation; cleared any merge/rebase in progress.");
|
|
37
|
+
}
|
|
38
|
+
hint("Repository should be ready for a new gflows command.");
|
|
39
|
+
}
|
package/src/commands/bump.ts
CHANGED
|
@@ -195,20 +195,20 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
195
195
|
);
|
|
196
196
|
process.exit(EXIT_USER);
|
|
197
197
|
} else {
|
|
198
|
-
const {
|
|
199
|
-
direction = await
|
|
198
|
+
const { selectPrompt } = await import("../prompts.js");
|
|
199
|
+
direction = await selectPrompt<"up" | "down">({
|
|
200
200
|
message: "Direction",
|
|
201
|
-
|
|
202
|
-
{
|
|
203
|
-
{
|
|
201
|
+
options: [
|
|
202
|
+
{ label: "Up (bump)", value: "up" },
|
|
203
|
+
{ label: "Down (rollback)", value: "down" },
|
|
204
204
|
],
|
|
205
205
|
});
|
|
206
|
-
type = await
|
|
206
|
+
type = await selectPrompt<"patch" | "minor" | "major">({
|
|
207
207
|
message: "Type",
|
|
208
|
-
|
|
209
|
-
{
|
|
210
|
-
{
|
|
211
|
-
{
|
|
208
|
+
options: [
|
|
209
|
+
{ label: "patch (x.y.Z)", value: "patch" },
|
|
210
|
+
{ label: "minor (x.Y.0)", value: "minor" },
|
|
211
|
+
{ label: "major (X.0.0)", value: "major" },
|
|
212
212
|
],
|
|
213
213
|
});
|
|
214
214
|
}
|
|
@@ -12,10 +12,20 @@ const COMMANDS = [
|
|
|
12
12
|
"init",
|
|
13
13
|
"start",
|
|
14
14
|
"finish",
|
|
15
|
+
"sync",
|
|
16
|
+
"pr",
|
|
17
|
+
"viz",
|
|
15
18
|
"switch",
|
|
16
19
|
"delete",
|
|
17
20
|
"list",
|
|
18
21
|
"bump",
|
|
22
|
+
"doctor",
|
|
23
|
+
"config",
|
|
24
|
+
"schema",
|
|
25
|
+
"continue",
|
|
26
|
+
"undo",
|
|
27
|
+
"abort",
|
|
28
|
+
"mcp",
|
|
19
29
|
"completion",
|
|
20
30
|
"status",
|
|
21
31
|
"help",
|
|
@@ -66,9 +76,9 @@ _gflows() {
|
|
|
66
76
|
fi
|
|
67
77
|
done
|
|
68
78
|
if [[ -n "$path" ]]; then
|
|
69
|
-
gflows -C "$path" list 2>/dev/null
|
|
79
|
+
gflows -C "$path" list -q 2>/dev/null
|
|
70
80
|
else
|
|
71
|
-
gflows list 2>/dev/null
|
|
81
|
+
gflows list -q 2>/dev/null
|
|
72
82
|
fi
|
|
73
83
|
}
|
|
74
84
|
|
|
@@ -145,7 +155,7 @@ _gflows_list_branches() {
|
|
|
145
155
|
fi
|
|
146
156
|
(( i++ ))
|
|
147
157
|
done
|
|
148
|
-
(( ${D}#path[@]} > 0 )) && gflows "${D}path[@]}" list 2>/dev/null || gflows list 2>/dev/null
|
|
158
|
+
(( ${D}#path[@]} > 0 )) && gflows "${D}path[@]}" list -q 2>/dev/null || gflows list -q 2>/dev/null
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
_gflows() {
|
|
@@ -231,7 +241,7 @@ function __gflows_list_branches
|
|
|
231
241
|
if test -n "$path"
|
|
232
242
|
gflows -C "$path" list 2>/dev/null
|
|
233
243
|
else
|
|
234
|
-
gflows list 2>/dev/null
|
|
244
|
+
gflows list -q 2>/dev/null
|
|
235
245
|
end
|
|
236
246
|
end
|
|
237
247
|
|