gflows 0.1.18 → 1.0.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 +78 -0
- package/CHANGELOG.md +48 -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 +239 -0
- package/src/commands/pr.ts +120 -0
- package/src/commands/schema.ts +99 -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 +391 -0
- package/src/tui/HubShell.tsx +193 -0
- package/src/tui/flows.tsx +229 -0
- package/src/tui/hub.ts +114 -0
- package/src/tui/prompts.tsx +207 -0
- package/src/tui/slash.ts +63 -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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config get/set for .gflows.json.
|
|
3
|
+
* @module commands/config
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readConfigFile, resolveConfig, writeConfigFile } from "../config.js";
|
|
7
|
+
import { EXIT_USER } from "../constants.js";
|
|
8
|
+
import { resolveRepoRoot } from "../git.js";
|
|
9
|
+
import { hint, success } from "../out.js";
|
|
10
|
+
import type { BranchType, GflowsConfigFile, ParsedArgs } from "../types.js";
|
|
11
|
+
|
|
12
|
+
const PREFIX_TYPES: BranchType[] = ["feature", "bugfix", "chore", "release", "hotfix", "spike"];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* get/set gflows configuration.
|
|
16
|
+
*/
|
|
17
|
+
export async function run(args: ParsedArgs): Promise<void> {
|
|
18
|
+
const repoRoot = await resolveRepoRoot(args.cwd);
|
|
19
|
+
const isTTY = Boolean(process.stdin.isTTY);
|
|
20
|
+
|
|
21
|
+
let action = args.configAction;
|
|
22
|
+
let key = args.configKey;
|
|
23
|
+
let value = args.configValue;
|
|
24
|
+
|
|
25
|
+
if (!action && isTTY) {
|
|
26
|
+
const { inputPrompt, selectPrompt } = await import("../prompts.js");
|
|
27
|
+
action = await selectPrompt<"get" | "set">({
|
|
28
|
+
message: "Config action",
|
|
29
|
+
options: [
|
|
30
|
+
{ label: "get", value: "get" },
|
|
31
|
+
{ label: "set", value: "set" },
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
key = await inputPrompt({ message: "Key (main|dev|remote|prefixes.<type>)" });
|
|
35
|
+
if (action === "set") {
|
|
36
|
+
value = await inputPrompt({ message: "Value" });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!action || (action !== "get" && action !== "set")) {
|
|
41
|
+
console.error(
|
|
42
|
+
"gflows config: use `gflows config get <key>` or `gflows config set <key> <value>`.",
|
|
43
|
+
);
|
|
44
|
+
process.exit(EXIT_USER);
|
|
45
|
+
}
|
|
46
|
+
if (!key) {
|
|
47
|
+
console.error("gflows config: missing key.");
|
|
48
|
+
process.exit(EXIT_USER);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const resolved = resolveConfig(
|
|
52
|
+
repoRoot,
|
|
53
|
+
{ main: args.main, dev: args.dev, remote: args.remote },
|
|
54
|
+
{ verbose: args.verbose },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (action === "get") {
|
|
58
|
+
const out = getValue(resolved, key);
|
|
59
|
+
if (out === undefined) {
|
|
60
|
+
console.error(`gflows config: unknown key '${key}'.`);
|
|
61
|
+
process.exit(EXIT_USER);
|
|
62
|
+
}
|
|
63
|
+
if (args.json) {
|
|
64
|
+
console.log(JSON.stringify({ [key]: out }, null, 2));
|
|
65
|
+
} else {
|
|
66
|
+
console.log(String(out));
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (value === undefined || value === "") {
|
|
72
|
+
console.error("gflows config set: missing value.");
|
|
73
|
+
process.exit(EXIT_USER);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const current = readConfigFile(repoRoot).config ?? {};
|
|
77
|
+
const next = setValue(current, key, value);
|
|
78
|
+
if (!args.dryRun) {
|
|
79
|
+
writeConfigFile(repoRoot, next);
|
|
80
|
+
}
|
|
81
|
+
if (!args.quiet) {
|
|
82
|
+
success(`gflows: set ${key} = ${value}`);
|
|
83
|
+
}
|
|
84
|
+
hint("Stored in .gflows.json");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getValue(resolved: ReturnType<typeof resolveConfig>, key: string): string | undefined {
|
|
88
|
+
if (key === "main") return resolved.main;
|
|
89
|
+
if (key === "dev") return resolved.dev;
|
|
90
|
+
if (key === "remote") return resolved.remote;
|
|
91
|
+
if (key.startsWith("prefixes.")) {
|
|
92
|
+
const t = key.slice("prefixes.".length) as BranchType;
|
|
93
|
+
if (PREFIX_TYPES.includes(t)) return resolved.prefixes[t];
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function setValue(current: GflowsConfigFile, key: string, value: string): GflowsConfigFile {
|
|
99
|
+
const next: GflowsConfigFile = { ...current, prefixes: { ...current.prefixes } };
|
|
100
|
+
if (key === "main") {
|
|
101
|
+
next.main = value;
|
|
102
|
+
return next;
|
|
103
|
+
}
|
|
104
|
+
if (key === "dev") {
|
|
105
|
+
next.dev = value;
|
|
106
|
+
return next;
|
|
107
|
+
}
|
|
108
|
+
if (key === "remote") {
|
|
109
|
+
next.remote = value;
|
|
110
|
+
return next;
|
|
111
|
+
}
|
|
112
|
+
if (key.startsWith("prefixes.")) {
|
|
113
|
+
const t = key.slice("prefixes.".length) as BranchType;
|
|
114
|
+
if (!PREFIX_TYPES.includes(t)) {
|
|
115
|
+
console.error(`gflows config: unknown prefix type '${t}'.`);
|
|
116
|
+
process.exit(EXIT_USER);
|
|
117
|
+
}
|
|
118
|
+
next.prefixes = { ...next.prefixes, [t]: value };
|
|
119
|
+
return next;
|
|
120
|
+
}
|
|
121
|
+
console.error(`gflows config: unknown key '${key}'.`);
|
|
122
|
+
process.exit(EXIT_USER);
|
|
123
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Continue a suspended gflows multi-step operation after resolving conflicts.
|
|
3
|
+
* @module commands/continue
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EXIT_USER } from "../constants.js";
|
|
7
|
+
import { resolveRepoRoot } from "../git.js";
|
|
8
|
+
import { hint } from "../out.js";
|
|
9
|
+
import { readActiveRun } from "../run-state.js";
|
|
10
|
+
import type { ParsedArgs } from "../types.js";
|
|
11
|
+
import { executeFinishSteps } from "./finish.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resumes the active suspended run (finish, sync, …).
|
|
15
|
+
*/
|
|
16
|
+
export async function run(args: ParsedArgs): Promise<void> {
|
|
17
|
+
const repoRoot = await resolveRepoRoot(args.cwd);
|
|
18
|
+
const state = readActiveRun(repoRoot);
|
|
19
|
+
if (!state || (state.status !== "suspended" && state.status !== "running")) {
|
|
20
|
+
console.error("gflows continue: no suspended operation to continue.");
|
|
21
|
+
hint("If you hit a merge conflict during finish/sync, resolve it, then retry continue.");
|
|
22
|
+
process.exit(EXIT_USER);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const opts = { dryRun: args.dryRun, verbose: args.verbose, quiet: args.quiet };
|
|
26
|
+
|
|
27
|
+
if (state.command === "finish") {
|
|
28
|
+
await executeFinishSteps(repoRoot, state, opts);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (state.command === "sync") {
|
|
33
|
+
const { executeSyncSteps } = await import("./sync.js");
|
|
34
|
+
await executeSyncSteps(repoRoot, state, opts);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.error(`gflows continue: unknown suspended command '${state.command}'.`);
|
|
39
|
+
process.exit(EXIT_USER);
|
|
40
|
+
}
|
package/src/commands/delete.ts
CHANGED
|
@@ -91,13 +91,13 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
91
91
|
process.exit(EXIT_USER);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
const {
|
|
95
|
-
const chosen = await
|
|
94
|
+
const { multiSelectPrompt } = await import("../prompts.js");
|
|
95
|
+
const chosen = await multiSelectPrompt({
|
|
96
96
|
message: "Delete branch(es)",
|
|
97
|
-
|
|
97
|
+
options: workflowBranches.map((b) => ({ label: b, value: b })),
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
if (
|
|
100
|
+
if (chosen.length === 0) {
|
|
101
101
|
if (!quiet) {
|
|
102
102
|
console.error("No branches selected.");
|
|
103
103
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor: check whether the repo is ready for gflows workflows.
|
|
3
|
+
* @module commands/doctor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readConfigFile, resolveConfig } from "../config.js";
|
|
7
|
+
import {
|
|
8
|
+
getCurrentBranch,
|
|
9
|
+
isClean,
|
|
10
|
+
isDetachedHead,
|
|
11
|
+
isRebaseOrMergeInProgress,
|
|
12
|
+
resolveRepoRoot,
|
|
13
|
+
revParse,
|
|
14
|
+
runGit,
|
|
15
|
+
} from "../git.js";
|
|
16
|
+
import { hint, success } from "../out.js";
|
|
17
|
+
import { readActiveRun } from "../run-state.js";
|
|
18
|
+
import type { ParsedArgs } from "../types.js";
|
|
19
|
+
|
|
20
|
+
interface Check {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
name: string;
|
|
23
|
+
detail: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Runs health checks and prints a report (or JSON).
|
|
28
|
+
*/
|
|
29
|
+
export async function run(args: ParsedArgs): Promise<void> {
|
|
30
|
+
let repoRoot: string;
|
|
31
|
+
try {
|
|
32
|
+
repoRoot = await resolveRepoRoot(args.cwd);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (args.json) {
|
|
35
|
+
console.log(JSON.stringify({ ok: false, error: String(err) }, null, 2));
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const config = resolveConfig(
|
|
42
|
+
repoRoot,
|
|
43
|
+
{ main: args.main, dev: args.dev, remote: args.remote },
|
|
44
|
+
{ verbose: args.verbose },
|
|
45
|
+
);
|
|
46
|
+
const cfgRead = readConfigFile(repoRoot);
|
|
47
|
+
const checks: Check[] = [];
|
|
48
|
+
|
|
49
|
+
checks.push({
|
|
50
|
+
ok: true,
|
|
51
|
+
name: "git-repo",
|
|
52
|
+
detail: repoRoot,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const mainOk = await revParse(repoRoot, config.main, [], { verbose: false }).then(
|
|
56
|
+
() => true,
|
|
57
|
+
() => false,
|
|
58
|
+
);
|
|
59
|
+
checks.push({
|
|
60
|
+
ok: mainOk,
|
|
61
|
+
name: "main-branch",
|
|
62
|
+
detail: mainOk ? `found '${config.main}'` : `missing '${config.main}' — create it first`,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const devOk = await revParse(repoRoot, config.dev, [], { verbose: false }).then(
|
|
66
|
+
() => true,
|
|
67
|
+
() => false,
|
|
68
|
+
);
|
|
69
|
+
checks.push({
|
|
70
|
+
ok: devOk,
|
|
71
|
+
name: "dev-branch",
|
|
72
|
+
detail: devOk ? `found '${config.dev}'` : `missing '${config.dev}' — run gflows init`,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const remoteList = await runGit(["remote"], { cwd: repoRoot });
|
|
76
|
+
const hasRemote = remoteList.stdout
|
|
77
|
+
.split("\n")
|
|
78
|
+
.map((s) => s.trim())
|
|
79
|
+
.includes(config.remote);
|
|
80
|
+
checks.push({
|
|
81
|
+
ok: hasRemote,
|
|
82
|
+
name: "remote",
|
|
83
|
+
detail: hasRemote
|
|
84
|
+
? `remote '${config.remote}' configured`
|
|
85
|
+
: `remote '${config.remote}' missing (push will fail)`,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
checks.push({
|
|
89
|
+
ok: !cfgRead.invalid,
|
|
90
|
+
name: "config",
|
|
91
|
+
detail: cfgRead.invalid
|
|
92
|
+
? "config file invalid — using defaults"
|
|
93
|
+
: cfgRead.config
|
|
94
|
+
? "config loaded"
|
|
95
|
+
: "using defaults (no .gflows.json)",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const detached = await isDetachedHead(repoRoot);
|
|
99
|
+
checks.push({
|
|
100
|
+
ok: !detached,
|
|
101
|
+
name: "head",
|
|
102
|
+
detail: detached ? "detached HEAD" : `on ${(await getCurrentBranch(repoRoot)) ?? "?"}`,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const inProgress = isRebaseOrMergeInProgress(repoRoot);
|
|
106
|
+
const active = readActiveRun(repoRoot);
|
|
107
|
+
checks.push({
|
|
108
|
+
ok: !inProgress && !active,
|
|
109
|
+
name: "run-state",
|
|
110
|
+
detail: active
|
|
111
|
+
? `suspended '${active.command}' — gflows continue | abort | undo`
|
|
112
|
+
: inProgress
|
|
113
|
+
? "git merge/rebase in progress — gflows continue or abort"
|
|
114
|
+
: "clean",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const clean = await isClean(repoRoot);
|
|
118
|
+
checks.push({
|
|
119
|
+
ok: true,
|
|
120
|
+
name: "working-tree",
|
|
121
|
+
detail: clean ? "clean" : "dirty (uncommitted changes)",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const ok = checks.every((c) => c.ok);
|
|
125
|
+
|
|
126
|
+
if (args.json) {
|
|
127
|
+
console.log(JSON.stringify({ ok, config, checks }, null, 2));
|
|
128
|
+
if (!ok) process.exit(2);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const c of checks) {
|
|
133
|
+
const mark = c.ok ? "✓" : "✗";
|
|
134
|
+
console.log(`${mark} ${c.name}: ${c.detail}`);
|
|
135
|
+
}
|
|
136
|
+
if (ok) {
|
|
137
|
+
if (!args.quiet) success("gflows doctor: all critical checks passed.");
|
|
138
|
+
} else {
|
|
139
|
+
console.error("gflows doctor: some checks failed.");
|
|
140
|
+
hint("Fix the ✗ items, then retry. Run gflows init if dev is missing.");
|
|
141
|
+
process.exit(2);
|
|
142
|
+
}
|
|
143
|
+
}
|