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/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,13 +1,34 @@
|
|
|
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",
|
|
@@ -20,25 +41,10 @@
|
|
|
20
41
|
"publish:npm": "bun run scripts/publish.ts -- --npm-only",
|
|
21
42
|
"publish:jsr": "bun run scripts/publish.ts -- --jsr-only"
|
|
22
43
|
},
|
|
23
|
-
"
|
|
24
|
-
"src",
|
|
25
|
-
"README.md",
|
|
26
|
-
"LICENSE"
|
|
27
|
-
],
|
|
28
|
-
"engines": {
|
|
29
|
-
"bun": ">=1.0.0"
|
|
30
|
-
},
|
|
31
|
-
"author": "Ali AlNaghmoush (https://github.com/alialnaghmoush)",
|
|
32
|
-
"repository": {
|
|
33
|
-
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/alialnaghmoush/gflows.git"
|
|
35
|
-
},
|
|
44
|
+
"type": "module",
|
|
36
45
|
"dependencies": {
|
|
37
|
-
"@
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"@biomejs/biome": "^2.4.4",
|
|
41
|
-
"@types/bun": "latest",
|
|
42
|
-
"typescript": "^5"
|
|
46
|
+
"@clack/prompts": "^1.7.0",
|
|
47
|
+
"ink": "^7.1.1",
|
|
48
|
+
"react": "^19.2.8"
|
|
43
49
|
}
|
|
44
50
|
}
|
package/src/cli.ts
CHANGED
|
@@ -6,405 +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, --destroy)
|
|
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
|
-
destroy: { type: "boolean" as const },
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Resolve -C/--path to absolute directory; validate it exists and is a directory. */
|
|
109
|
-
function resolveCwd(pathFlag: string | undefined): string {
|
|
110
|
-
if (!pathFlag || pathFlag.trim() === "") {
|
|
111
|
-
return process.cwd();
|
|
112
|
-
}
|
|
113
|
-
const absolute = resolve(process.cwd(), pathFlag.trim());
|
|
114
|
-
if (!existsSync(absolute)) {
|
|
115
|
-
console.error(`gflows: path does not exist: ${absolute}`);
|
|
116
|
-
process.exit(EXIT_USER);
|
|
117
|
-
}
|
|
118
|
-
const stat = statSync(absolute, { throwIfNoEntry: false });
|
|
119
|
-
if (!stat || !stat.isDirectory()) {
|
|
120
|
-
console.error(`gflows: path is not a directory: ${absolute}`);
|
|
121
|
-
process.exit(EXIT_USER);
|
|
122
|
-
}
|
|
123
|
-
return absolute;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Returns the command name closest to `input` by edit distance, or undefined if no close match.
|
|
128
|
-
* Used for "did you mean?" when the user mistypes a command.
|
|
129
|
-
*/
|
|
130
|
-
function closestCommand(input: string): Command | undefined {
|
|
131
|
-
if (!input || input.length < 2) return undefined;
|
|
132
|
-
const target = input.toLowerCase();
|
|
133
|
-
let best: Command | undefined;
|
|
134
|
-
let bestDistance = 3; // only suggest if within 2 edits
|
|
135
|
-
for (const cmd of COMMANDS) {
|
|
136
|
-
const d = editDistance(target, cmd);
|
|
137
|
-
if (d < bestDistance) {
|
|
138
|
-
bestDistance = d;
|
|
139
|
-
best = cmd as Command;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return best;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Levenshtein edit distance between two strings. */
|
|
146
|
-
function editDistance(a: string, b: string): number {
|
|
147
|
-
const m = a.length;
|
|
148
|
-
const n = b.length;
|
|
149
|
-
const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
150
|
-
for (let i = 0; i <= m; i++) {
|
|
151
|
-
const row = dp[i];
|
|
152
|
-
if (row) row[0] = i;
|
|
153
|
-
}
|
|
154
|
-
for (let j = 0; j <= n; j++) {
|
|
155
|
-
const row = dp[0];
|
|
156
|
-
if (row) row[j] = j;
|
|
157
|
-
}
|
|
158
|
-
for (let i = 1; i <= m; i++) {
|
|
159
|
-
for (let j = 1; j <= n; j++) {
|
|
160
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
161
|
-
const v1 = dp[i - 1]?.[j] ?? 0;
|
|
162
|
-
const v2 = dp[i]?.[j - 1] ?? 0;
|
|
163
|
-
const v3 = dp[i - 1]?.[j - 1] ?? 0;
|
|
164
|
-
const rowI = dp[i];
|
|
165
|
-
if (rowI) rowI[j] = Math.min(v1 + 1, v2 + 1, v3 + cost);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return dp[m]?.[n] ?? 0;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Resolve command from positionals and short flags. Short wins if both present. */
|
|
172
|
-
function resolveCommand(
|
|
173
|
-
positionals: string[],
|
|
174
|
-
values: Record<string, string | boolean | undefined>,
|
|
175
|
-
): Command | undefined {
|
|
176
|
-
for (const [_short, cmd] of Object.entries(SHORT_TO_COMMAND)) {
|
|
177
|
-
const key = cmd === "delete" ? "delete" : cmd;
|
|
178
|
-
if (values[key] === true) return cmd as Command;
|
|
179
|
-
}
|
|
180
|
-
const first = positionals[0];
|
|
181
|
-
if (first && COMMANDS.includes(first as Command)) {
|
|
182
|
-
return first as Command;
|
|
183
|
-
}
|
|
184
|
-
return undefined;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** Resolve branch type for start/finish/list. -r means release for start/finish; for list, -r means include-remote (handled in flags). */
|
|
188
|
-
function resolveType(
|
|
189
|
-
command: Command,
|
|
190
|
-
positionals: string[],
|
|
191
|
-
values: Record<string, string | boolean | undefined>,
|
|
192
|
-
): BranchType | undefined {
|
|
193
|
-
if (command !== "start" && command !== "finish" && command !== "list") {
|
|
194
|
-
return undefined;
|
|
195
|
-
}
|
|
196
|
-
// Type from short flags (for start/finish, -r => release)
|
|
197
|
-
if (command === "list") {
|
|
198
|
-
// For list, -r is include-remote only (see includeRemote below); type from other shorts or positional (not -r)
|
|
199
|
-
if (values.feature === true) return "feature";
|
|
200
|
-
if (values.bugfix === true) return "bugfix";
|
|
201
|
-
if (values.chore === true) return "chore";
|
|
202
|
-
if (values.hotfix === true) return "hotfix";
|
|
203
|
-
if (values.spike === true) return "spike";
|
|
204
|
-
// release type for list only via positional, e.g. "gflows list release"
|
|
205
|
-
} else {
|
|
206
|
-
// start / finish: -r => release
|
|
207
|
-
if (values.release === true) return "release";
|
|
208
|
-
if (values.feature === true) return "feature";
|
|
209
|
-
if (values.bugfix === true) return "bugfix";
|
|
210
|
-
if (values.chore === true) return "chore";
|
|
211
|
-
if (values.hotfix === true) return "hotfix";
|
|
212
|
-
if (values.spike === true) return "spike";
|
|
213
|
-
}
|
|
214
|
-
// Type from second positional
|
|
215
|
-
const idx = positionals[0] && COMMANDS.includes(positionals[0] as Command) ? 1 : 0;
|
|
216
|
-
const pos = positionals[idx];
|
|
217
|
-
if (pos && BRANCH_TYPES.includes(pos as BranchType)) {
|
|
218
|
-
return pos as BranchType;
|
|
219
|
-
}
|
|
220
|
-
return undefined;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** Resolve name (third positional for start; -B for finish; first positional for completion). */
|
|
224
|
-
function resolveName(
|
|
225
|
-
command: Command,
|
|
226
|
-
positionals: string[],
|
|
227
|
-
values: Record<string, string | boolean | undefined>,
|
|
228
|
-
): string | undefined {
|
|
229
|
-
const branch = values.branch;
|
|
230
|
-
if (typeof branch === "string" && branch.trim() !== "") {
|
|
231
|
-
return branch.trim();
|
|
232
|
-
}
|
|
233
|
-
const skip = positionals[0] && COMMANDS.includes(positionals[0] as Command) ? 1 : 0;
|
|
234
|
-
if (command === "start") {
|
|
235
|
-
const typeFromPos = positionals[skip] && BRANCH_TYPES.includes(positionals[skip] as BranchType);
|
|
236
|
-
if (typeFromPos) {
|
|
237
|
-
return positionals[skip + 1];
|
|
238
|
-
}
|
|
239
|
-
return positionals[skip];
|
|
240
|
-
}
|
|
241
|
-
if (command === "completion") {
|
|
242
|
-
const shell = positionals[skip];
|
|
243
|
-
if (shell === "bash" || shell === "zsh" || shell === "fish") return shell;
|
|
244
|
-
return undefined;
|
|
245
|
-
}
|
|
246
|
-
if (command === "bump") {
|
|
247
|
-
const dir = positionals[skip];
|
|
248
|
-
const _typ = positionals[skip + 1];
|
|
249
|
-
if (dir === "up" || dir === "down") {
|
|
250
|
-
return dir;
|
|
251
|
-
}
|
|
252
|
-
return undefined;
|
|
253
|
-
}
|
|
254
|
-
if (command === "switch") {
|
|
255
|
-
return positionals[skip];
|
|
256
|
-
}
|
|
257
|
-
return undefined;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/** Resolve bump direction and type from positionals (bump [up|down] [patch|minor|major]). */
|
|
261
|
-
function resolveBump(
|
|
262
|
-
positionals: string[],
|
|
263
|
-
_values: Record<string, string | boolean | undefined>,
|
|
264
|
-
): { direction?: "up" | "down"; type?: "patch" | "minor" | "major" } {
|
|
265
|
-
const skip = positionals[0] === "bump" ? 1 : 0;
|
|
266
|
-
const a = positionals[skip];
|
|
267
|
-
const b = positionals[skip + 1];
|
|
268
|
-
const direction = a === "up" || a === "down" ? a : undefined;
|
|
269
|
-
const type = b === "patch" || b === "minor" || b === "major" ? b : undefined;
|
|
270
|
-
return { direction, type };
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/** Parse raw argv into ParsedArgs. Resolves -C first, then command/type/name and flags. */
|
|
274
|
-
export function parse(argv: string[] = Bun.argv.slice(2)): ParsedArgs {
|
|
275
|
-
const config = {
|
|
276
|
-
...buildParseArgsOptions(),
|
|
277
|
-
args: argv,
|
|
278
|
-
};
|
|
279
|
-
const { values, positionals } = parseArgs(config);
|
|
280
|
-
const v = values as Record<string, string | boolean | undefined>;
|
|
281
|
-
|
|
282
|
-
const pathRaw = v.path;
|
|
283
|
-
const pathStr = typeof pathRaw === "string" ? pathRaw : undefined;
|
|
284
|
-
const cwd = resolveCwd(pathStr);
|
|
285
|
-
|
|
286
|
-
const command = resolveCommand(positionals, v);
|
|
287
|
-
if (!command) {
|
|
288
|
-
const first = positionals[0];
|
|
289
|
-
const suggestion = typeof first === "string" ? closestCommand(first) : undefined;
|
|
290
|
-
if (suggestion) {
|
|
291
|
-
console.error(`gflows: unknown command '${first}'. Did you mean '${suggestion}'?`);
|
|
292
|
-
} else {
|
|
293
|
-
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;
|
|
294
29
|
}
|
|
30
|
+
console.error("gflows: missing command. Use 'gflows help' for usage.");
|
|
295
31
|
process.exit(EXIT_USER);
|
|
296
32
|
}
|
|
297
33
|
|
|
298
|
-
const type = resolveType(command, positionals, v);
|
|
299
|
-
const name = resolveName(command, positionals, v);
|
|
300
|
-
const { direction: bumpDirection, type: bumpType } =
|
|
301
|
-
command === "bump" ? resolveBump(positionals, v) : { direction: undefined, type: undefined };
|
|
302
|
-
|
|
303
|
-
const branchNames =
|
|
304
|
-
command === "delete"
|
|
305
|
-
? positionals[0] && COMMANDS.includes(positionals[0] as Command)
|
|
306
|
-
? positionals.slice(1)
|
|
307
|
-
: positionals
|
|
308
|
-
: undefined;
|
|
309
|
-
|
|
310
|
-
// -r context: for list → includeRemote; for start/finish → already used as type release
|
|
311
|
-
const includeRemote =
|
|
312
|
-
command === "list"
|
|
313
|
-
? v.includeRemote === true || v["include-remote"] === true || v.release === true
|
|
314
|
-
: false;
|
|
315
|
-
|
|
316
|
-
let completionShell: "bash" | "zsh" | "fish" | undefined;
|
|
317
|
-
if (command === "completion" && name === "bash") completionShell = "bash";
|
|
318
|
-
else if (command === "completion" && name === "zsh") completionShell = "zsh";
|
|
319
|
-
else if (command === "completion" && name === "fish") completionShell = "fish";
|
|
320
|
-
|
|
321
|
-
let switchMode: "restore" | "clean" | "cancel" | "move" | "destroy" | undefined;
|
|
322
|
-
if (command === "switch") {
|
|
323
|
-
const restore = v.restore === true;
|
|
324
|
-
const clean = v.clean === true;
|
|
325
|
-
const cancel = v.cancel === true;
|
|
326
|
-
const move = v.move === true;
|
|
327
|
-
const destroy = v.destroy === true;
|
|
328
|
-
const count = [restore, clean, cancel, move, destroy].filter(Boolean).length;
|
|
329
|
-
if (count > 1) {
|
|
330
|
-
console.error(
|
|
331
|
-
"gflows switch: only one of --restore, --clean, --cancel, --move, or --destroy may be used at a time.",
|
|
332
|
-
);
|
|
333
|
-
process.exit(EXIT_USER);
|
|
334
|
-
}
|
|
335
|
-
if (restore) switchMode = "restore";
|
|
336
|
-
else if (clean) switchMode = "clean";
|
|
337
|
-
else if (cancel) switchMode = "cancel";
|
|
338
|
-
else if (move) switchMode = "move";
|
|
339
|
-
else if (destroy) switchMode = "destroy";
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return {
|
|
343
|
-
command,
|
|
344
|
-
cwd,
|
|
345
|
-
type,
|
|
346
|
-
name,
|
|
347
|
-
completionShell,
|
|
348
|
-
branchNames,
|
|
349
|
-
bumpDirection,
|
|
350
|
-
bumpType,
|
|
351
|
-
push: v.push === true,
|
|
352
|
-
noPush: v.noPush === true || v["no-push"] === true,
|
|
353
|
-
main: typeof v.main === "string" && v.main.trim() !== "" ? v.main.trim() : undefined,
|
|
354
|
-
dev: typeof v.dev === "string" && v.dev.trim() !== "" ? v.dev.trim() : undefined,
|
|
355
|
-
remote: typeof v.remote === "string" ? v.remote : undefined,
|
|
356
|
-
branch: typeof v.branch === "string" ? v.branch : undefined,
|
|
357
|
-
yes: v.yes === true,
|
|
358
|
-
dryRun: v.dryRun === true,
|
|
359
|
-
verbose: v.verbose === true,
|
|
360
|
-
quiet: v.quiet === true,
|
|
361
|
-
force: v.force === true,
|
|
362
|
-
path: pathStr,
|
|
363
|
-
fromMain: v.from === "main",
|
|
364
|
-
noFf: v.noFf === true,
|
|
365
|
-
deleteAfterFinish: v.deleteBranch === true,
|
|
366
|
-
noDeleteAfterFinish: v.noDelete === true,
|
|
367
|
-
signTag: v.sign === true,
|
|
368
|
-
noTag: v.noTag === true,
|
|
369
|
-
tagMessage: typeof v.tagMessage === "string" ? v.tagMessage : undefined,
|
|
370
|
-
message: typeof v.message === "string" ? v.message : undefined,
|
|
371
|
-
includeRemote,
|
|
372
|
-
switchMode,
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/** Run the CLI: parse, dispatch, set exit code. */
|
|
377
|
-
async function run(): Promise<void> {
|
|
378
34
|
const args = parse();
|
|
379
35
|
lastParsedArgs = args;
|
|
380
|
-
const { command } = args;
|
|
381
36
|
|
|
382
|
-
if (command === "help") {
|
|
37
|
+
if (args.command === "help") {
|
|
383
38
|
const { run: runHelp } = await import("./commands/help.js");
|
|
384
39
|
await runHelp(args);
|
|
385
40
|
return;
|
|
386
41
|
}
|
|
387
|
-
if (command === "version") {
|
|
42
|
+
if (args.command === "version") {
|
|
388
43
|
const { run: runVersion } = await import("./commands/version.js");
|
|
389
44
|
await runVersion(args);
|
|
390
45
|
return;
|
|
391
46
|
}
|
|
392
47
|
|
|
393
|
-
const mod = await import(`./commands/${command}.js`).catch(() => null);
|
|
48
|
+
const mod = await import(`./commands/${args.command}.js`).catch(() => null);
|
|
394
49
|
if (!mod || typeof mod.run !== "function") {
|
|
395
|
-
console.error(`gflows: command '${command}' is not implemented.`);
|
|
50
|
+
console.error(`gflows: command '${args.command}' is not implemented.`);
|
|
396
51
|
process.exit(EXIT_GIT);
|
|
397
52
|
}
|
|
398
53
|
await mod.run(args);
|
|
399
54
|
}
|
|
400
55
|
|
|
401
|
-
// Top-level try/catch and unhandledRejection so exit code is always set
|
|
402
56
|
function main(): void {
|
|
403
57
|
let exitCode: number | null = null;
|
|
404
58
|
|
|
405
59
|
const handleRejection = (reason: unknown): void => {
|
|
406
60
|
if (exitCode !== null) return;
|
|
407
|
-
|
|
61
|
+
printError(reason);
|
|
408
62
|
const verbose = lastParsedArgs?.verbose ?? !!process.env.GFLOWS_VERBOSE;
|
|
409
63
|
if (verbose && reason instanceof Error && reason.stack) {
|
|
410
64
|
console.error(reason.stack);
|
|
@@ -422,7 +76,7 @@ function main(): void {
|
|
|
422
76
|
})
|
|
423
77
|
.catch((err: unknown) => {
|
|
424
78
|
if (exitCode !== null) return;
|
|
425
|
-
|
|
79
|
+
printError(err);
|
|
426
80
|
const verbose = lastParsedArgs?.verbose ?? !!process.env.GFLOWS_VERBOSE;
|
|
427
81
|
if (verbose && err instanceof Error && err.stack) {
|
|
428
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
|
|