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
package/src/commands/help.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Help command: print quick reference
|
|
2
|
+
* Help command: print quick reference and list commands/flags.
|
|
3
3
|
* @module commands/help
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -7,59 +7,67 @@ import type { ParsedArgs } from "../types.js";
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Runs the help command: prints usage, commands, types, flags, and exit codes to stdout.
|
|
10
|
-
* @param _args - Parsed CLI args (unused; kept for command signature consistency).
|
|
11
10
|
*/
|
|
12
11
|
export async function run(_args: ParsedArgs): Promise<void> {
|
|
13
12
|
const out = `
|
|
14
13
|
gflows — Modern Git branching workflow CLI
|
|
15
14
|
|
|
16
15
|
Usage: gflows <command> [type] [name] [flags]
|
|
16
|
+
gflows (TTY: fullscreen hub)
|
|
17
17
|
|
|
18
18
|
Commands:
|
|
19
|
-
init, -I Ensure main, create dev
|
|
20
|
-
start, -S Create workflow branch
|
|
21
|
-
finish, -F Merge and close branch
|
|
19
|
+
init, -I Ensure main, create dev (TTY: wizard; optional package.json script alias)
|
|
20
|
+
start, -S Create workflow branch (TTY: prompts if args missing)
|
|
21
|
+
finish, -F Merge and close branch (plan + delete by default)
|
|
22
|
+
sync Update current branch from its base
|
|
23
|
+
pr Open PR/MR via gh or glab (correct base)
|
|
22
24
|
switch, -W Switch branch (picker or name)
|
|
23
25
|
delete, -L Delete local branch(es)
|
|
24
26
|
list, -l List branches by type
|
|
25
|
-
bump Bump or rollback package version
|
|
26
|
-
completion Print shell completion script
|
|
27
27
|
status, -t Show current branch flow info
|
|
28
|
+
viz Visual branch map + flow (also shown in interactive hub)
|
|
29
|
+
doctor Repo health checks
|
|
30
|
+
config get/set .gflows.json
|
|
31
|
+
bump, -U Bump or rollback package version
|
|
32
|
+
continue Resume after merge conflict
|
|
33
|
+
undo Undo last completed gflows operation
|
|
34
|
+
abort Abort suspended operation
|
|
35
|
+
schema Machine-readable command catalog (JSON)
|
|
36
|
+
mcp Start MCP server for AI agents
|
|
37
|
+
completion Print shell completion script
|
|
28
38
|
help, -h Show this usage
|
|
29
39
|
version, -V Show version
|
|
30
40
|
|
|
31
41
|
Types: feature (-f), bugfix (-b), chore (-c), release (-r), hotfix (-x), spike (-e)
|
|
32
42
|
|
|
33
43
|
Common flags:
|
|
34
|
-
-p, --push Push after init/start/finish
|
|
35
|
-
-P, --no-push Do not push (init
|
|
44
|
+
-p, --push Push after init/start/finish/pr
|
|
45
|
+
-P, --no-push Do not push (init defaults to push; start does not)
|
|
36
46
|
--main <name> Main branch (init: persist to .gflows.json)
|
|
37
47
|
--dev <name> Dev branch (init: persist to .gflows.json)
|
|
38
48
|
-R, --remote <name> Remote for push (init: persist to .gflows.json)
|
|
39
49
|
-o, --from <branch> Base branch override (e.g. -o main for bugfix)
|
|
40
|
-
-B, --branch <name> Branch name (finish
|
|
41
|
-
-y, --yes
|
|
50
|
+
-B, --branch <name> Branch name (finish/pr)
|
|
51
|
+
-y, --yes Accept plan / skip confirmations (finish: delete default on)
|
|
42
52
|
-d, --dry-run Log actions only, no writes
|
|
43
53
|
-v, --verbose Verbose output
|
|
44
54
|
-q, --quiet Minimal output
|
|
45
55
|
-C, --path <dir> Run as if in <dir>
|
|
56
|
+
--json Machine-readable output (status/list/doctor/config)
|
|
46
57
|
|
|
58
|
+
Init: --script-alias <name> Add package.json script (e.g. g → "gflows")
|
|
59
|
+
--no-script-alias Never add a script alias
|
|
47
60
|
Start: --force Allow dirty working tree
|
|
48
61
|
Finish: --no-ff Always create merge commit; -D/--delete, -N/--no-delete;
|
|
49
|
-
-s/--sign, -T/--no-tag, -M/--tag-message, -m/--message
|
|
62
|
+
--squash, --preview, --bump; -s/--sign, -T/--no-tag, -M/--tag-message, -m/--message
|
|
63
|
+
Sync: --rebase, --force
|
|
50
64
|
List: -r, --include-remote Include remote-tracking branches
|
|
51
|
-
Switch: --move
|
|
52
|
-
--restore Save for this branch; restore target's saved state (if any)
|
|
53
|
-
--clean Discard changes and switch clean at HEAD
|
|
54
|
-
--destroy Delete current branch and switch to target (not main/dev)
|
|
55
|
-
--cancel Abort switching
|
|
65
|
+
Switch: --move | --restore | --clean | --destroy | --cancel
|
|
56
66
|
|
|
57
67
|
Exit codes: 0 success, 1 usage/validation, 2 Git or system error.
|
|
58
68
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
• gflows finish <type> — merge current workflow branch (use -B <name> to specify branch)
|
|
62
|
-
• gflows list -r — include remote branches; gflows status — show current branch flow
|
|
69
|
+
Stuck? gflows continue | gflows abort | gflows undo | gflows doctor
|
|
70
|
+
Agents: see AGENTS.md, gflows schema, gflows mcp
|
|
63
71
|
`;
|
|
64
72
|
console.log(out.trim());
|
|
65
73
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { resolveConfig, writeConfigFile } from "../config.js";
|
|
|
8
8
|
import { BranchNotFoundError, NotRepoError } from "../errors.js";
|
|
9
9
|
import { branchList, push, resolveRepoRoot, revParse, runGit } from "../git.js";
|
|
10
10
|
import { banner, hint, success } from "../out.js";
|
|
11
|
+
import { ensureGflowsScriptAlias } from "../package-scripts.js";
|
|
11
12
|
import type { ParsedArgs } from "../types.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -20,12 +21,56 @@ import type { ParsedArgs } from "../types.js";
|
|
|
20
21
|
*/
|
|
21
22
|
export async function run(args: ParsedArgs): Promise<void> {
|
|
22
23
|
const repoRoot = await resolveRepoRoot(args.cwd);
|
|
24
|
+
|
|
25
|
+
// TTY setup wizard when no explicit config flags
|
|
26
|
+
let main = args.main;
|
|
27
|
+
let dev = args.dev;
|
|
28
|
+
let remote = args.remote;
|
|
29
|
+
let scriptAlias = args.noScriptAlias ? undefined : args.scriptAlias;
|
|
30
|
+
if (
|
|
31
|
+
process.stdin.isTTY &&
|
|
32
|
+
main === undefined &&
|
|
33
|
+
dev === undefined &&
|
|
34
|
+
remote === undefined &&
|
|
35
|
+
!args.yes
|
|
36
|
+
) {
|
|
37
|
+
const { confirmPrompt, inputPrompt, selectPrompt } = await import("../prompts.js");
|
|
38
|
+
const defaults = resolveConfig(repoRoot, {}, { verbose: args.verbose });
|
|
39
|
+
main = await inputPrompt({ message: "Main branch name", defaultValue: defaults.main });
|
|
40
|
+
dev = await inputPrompt({ message: "Dev branch name", defaultValue: defaults.dev });
|
|
41
|
+
remote = await inputPrompt({ message: "Remote name", defaultValue: defaults.remote });
|
|
42
|
+
const persist = await confirmPrompt({ message: "Write .gflows.json?", initialValue: true });
|
|
43
|
+
if (persist && !args.dryRun) {
|
|
44
|
+
writeConfigFile(repoRoot, { main, dev, remote });
|
|
45
|
+
}
|
|
46
|
+
if (!args.noScriptAlias && scriptAlias === undefined) {
|
|
47
|
+
const aliasChoice = await selectPrompt({
|
|
48
|
+
message: "Add a short script in package.json? (shell alias is still nicer — see docs)",
|
|
49
|
+
options: [
|
|
50
|
+
{
|
|
51
|
+
value: "g",
|
|
52
|
+
label: '"g": "gflows"',
|
|
53
|
+
hint: "bun run g -- start feature x",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
value: "gflows",
|
|
57
|
+
label: '"gflows": "gflows"',
|
|
58
|
+
hint: "bun run gflows -- …",
|
|
59
|
+
},
|
|
60
|
+
{ value: "skip", label: "Skip", hint: "use a shell alias instead" },
|
|
61
|
+
],
|
|
62
|
+
initialValue: "g",
|
|
63
|
+
});
|
|
64
|
+
scriptAlias = aliasChoice === "skip" ? undefined : aliasChoice;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
23
68
|
const config = resolveConfig(
|
|
24
69
|
repoRoot,
|
|
25
70
|
{
|
|
26
|
-
main
|
|
27
|
-
dev
|
|
28
|
-
remote
|
|
71
|
+
main,
|
|
72
|
+
dev,
|
|
73
|
+
remote,
|
|
29
74
|
},
|
|
30
75
|
{ verbose: args.verbose },
|
|
31
76
|
);
|
|
@@ -72,32 +117,68 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
72
117
|
|
|
73
118
|
const doPush = !args.noPush;
|
|
74
119
|
if (doPush) {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
120
|
+
const remotes = await runGit(["remote"], { cwd: repoRoot, ...opts });
|
|
121
|
+
const hasRemote = remotes.stdout
|
|
122
|
+
.split("\n")
|
|
123
|
+
.map((s) => s.trim())
|
|
124
|
+
.includes(config.remote);
|
|
125
|
+
if (!hasRemote) {
|
|
126
|
+
if (!args.quiet) {
|
|
127
|
+
hint(
|
|
128
|
+
`Remote '${config.remote}' not found — skipped push. Add a remote or re-run with --no-push.`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
const pushCode = await push(repoRoot, config.remote, [config.dev], false, opts);
|
|
133
|
+
if (pushCode !== 0) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Push failed. Local branch '${config.dev}' was created. Retry with \`git push ${config.remote} ${config.dev}\` or \`gflows init --no-push\`.`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (!args.quiet && !args.dryRun) {
|
|
139
|
+
success(`gflows: pushed '${config.dev}' to '${config.remote}'.`);
|
|
140
|
+
}
|
|
83
141
|
}
|
|
84
142
|
}
|
|
85
143
|
|
|
86
|
-
const hasConfigFlags =
|
|
87
|
-
args.main !== undefined || args.dev !== undefined || args.remote !== undefined;
|
|
144
|
+
const hasConfigFlags = main !== undefined || dev !== undefined || remote !== undefined;
|
|
88
145
|
if (!args.dryRun && hasConfigFlags) {
|
|
89
146
|
writeConfigFile(repoRoot, {
|
|
90
|
-
...(
|
|
91
|
-
...(
|
|
92
|
-
...(
|
|
147
|
+
...(main !== undefined && { main }),
|
|
148
|
+
...(dev !== undefined && { dev }),
|
|
149
|
+
...(remote !== undefined && { remote }),
|
|
93
150
|
});
|
|
94
151
|
if (!args.quiet) {
|
|
95
152
|
success("gflows: updated .gflows.json with provided options.");
|
|
96
153
|
}
|
|
97
154
|
}
|
|
98
155
|
|
|
156
|
+
if (scriptAlias && !args.noScriptAlias) {
|
|
157
|
+
const result = ensureGflowsScriptAlias(repoRoot, scriptAlias, { dryRun: args.dryRun });
|
|
158
|
+
if (!args.quiet) {
|
|
159
|
+
if (result.status === "added") {
|
|
160
|
+
success(
|
|
161
|
+
args.dryRun
|
|
162
|
+
? `gflows: would add scripts["${result.name}"] = "gflows" to package.json.`
|
|
163
|
+
: `gflows: added scripts["${result.name}"] = "gflows" to package.json.`,
|
|
164
|
+
);
|
|
165
|
+
hint(`Run with: bun run ${result.name} -- start feature <name>`);
|
|
166
|
+
hint("Even shorter daily use: alias g=gflows (or alias g='bunx gflows')");
|
|
167
|
+
} else if (result.status === "unchanged") {
|
|
168
|
+
hint(`package.json already has scripts["${result.name}"] → gflows.`);
|
|
169
|
+
} else if (result.status === "conflict") {
|
|
170
|
+
hint(`Skipped script alias: scripts["${result.name}"] is already "${result.existing}".`);
|
|
171
|
+
} else if (result.status === "no-package") {
|
|
172
|
+
hint("No package.json here — skipped script alias. Use: alias g=gflows");
|
|
173
|
+
} else if (result.status === "invalid") {
|
|
174
|
+
hint(`Invalid script alias name '${result.name}'. Use letters/numbers/_-: (e.g. g).`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} else if (!args.quiet) {
|
|
178
|
+
hint("Tip: alias g=gflows in your shell, or re-run init with --script-alias g");
|
|
179
|
+
}
|
|
180
|
+
|
|
99
181
|
if (!args.quiet) {
|
|
100
|
-
|
|
101
|
-
hint("Run gflows start feature <name> to create a workflow branch.");
|
|
182
|
+
hint("Next: gflows start feature <name>");
|
|
102
183
|
}
|
|
103
184
|
}
|
package/src/commands/list.ts
CHANGED
|
@@ -71,12 +71,17 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
71
71
|
const mainAndDev = [config.main, config.dev].filter((b) => allBranches.includes(b));
|
|
72
72
|
const sorted = [...mainAndDev, ...[...workflowBranches].sort()];
|
|
73
73
|
|
|
74
|
+
if (args.json) {
|
|
75
|
+
console.log(JSON.stringify({ branches: sorted }, null, 2));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
for (const b of sorted) {
|
|
75
80
|
console.log(b);
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
if (!quiet && sorted.length > 0) {
|
|
79
|
-
// Hint
|
|
84
|
+
// Hint on stderr — stdout stays script-friendly
|
|
80
85
|
hint("Use gflows switch <branch> to switch to a branch.");
|
|
81
86
|
}
|
|
82
87
|
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin stdio MCP server exposing gflows tools for coding agents.
|
|
3
|
+
* @module commands/mcp
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EXIT_OK } from "../constants.js";
|
|
7
|
+
import type { ParsedArgs } from "../types.js";
|
|
8
|
+
import { getVersion } from "../version.js";
|
|
9
|
+
|
|
10
|
+
interface JsonRpcRequest {
|
|
11
|
+
jsonrpc?: string;
|
|
12
|
+
id?: string | number | null;
|
|
13
|
+
method?: string;
|
|
14
|
+
params?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const TOOLS = [
|
|
18
|
+
{
|
|
19
|
+
name: "gflows_status",
|
|
20
|
+
description: "Show current branch flow info (non-interactive).",
|
|
21
|
+
inputSchema: {
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: { path: { type: "string", description: "Repo path" }, json: { type: "boolean" } },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "gflows_doctor",
|
|
28
|
+
description: "Run repo health checks.",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: { path: { type: "string" } },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "gflows_list",
|
|
36
|
+
description: "List workflow branches.",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
path: { type: "string" },
|
|
41
|
+
type: { type: "string" },
|
|
42
|
+
includeRemote: { type: "boolean" },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "gflows_start",
|
|
48
|
+
description: "Create a workflow branch. Requires type and name.",
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: {
|
|
52
|
+
path: { type: "string" },
|
|
53
|
+
type: { type: "string" },
|
|
54
|
+
name: { type: "string" },
|
|
55
|
+
from: { type: "string" },
|
|
56
|
+
push: { type: "boolean" },
|
|
57
|
+
},
|
|
58
|
+
required: ["type", "name"],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "gflows_sync",
|
|
63
|
+
description: "Sync current workflow branch from its base.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
path: { type: "string" },
|
|
68
|
+
rebase: { type: "boolean" },
|
|
69
|
+
force: { type: "boolean" },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "gflows_finish",
|
|
75
|
+
description: "Finish a workflow branch (non-interactive; pass -y and push flag).",
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: "object",
|
|
78
|
+
properties: {
|
|
79
|
+
path: { type: "string" },
|
|
80
|
+
branch: { type: "string" },
|
|
81
|
+
type: { type: "string" },
|
|
82
|
+
push: { type: "boolean" },
|
|
83
|
+
noDelete: { type: "boolean" },
|
|
84
|
+
preview: { type: "boolean" },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "gflows_schema",
|
|
90
|
+
description: "Return the gflows command schema JSON.",
|
|
91
|
+
inputSchema: { type: "object", properties: {} },
|
|
92
|
+
},
|
|
93
|
+
] as const;
|
|
94
|
+
|
|
95
|
+
function respond(id: string | number | null | undefined, result: unknown): void {
|
|
96
|
+
const msg = { jsonrpc: "2.0", id: id ?? null, result };
|
|
97
|
+
console.log(JSON.stringify(msg));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function respondError(id: string | number | null | undefined, message: string): void {
|
|
101
|
+
console.log(
|
|
102
|
+
JSON.stringify({
|
|
103
|
+
jsonrpc: "2.0",
|
|
104
|
+
id: id ?? null,
|
|
105
|
+
error: { code: -32000, message },
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function runTool(
|
|
111
|
+
name: string,
|
|
112
|
+
args: Record<string, unknown>,
|
|
113
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
114
|
+
const cliPath = new URL("../cli.ts", import.meta.url).pathname;
|
|
115
|
+
const path = typeof args.path === "string" ? args.path : process.cwd();
|
|
116
|
+
const argv: string[] = ["-C", path, "-y"];
|
|
117
|
+
|
|
118
|
+
switch (name) {
|
|
119
|
+
case "gflows_status":
|
|
120
|
+
argv.push("status");
|
|
121
|
+
if (args.json !== false) argv.push("--json");
|
|
122
|
+
break;
|
|
123
|
+
case "gflows_doctor":
|
|
124
|
+
argv.push("doctor", "--json");
|
|
125
|
+
break;
|
|
126
|
+
case "gflows_list":
|
|
127
|
+
argv.push("list", "-q");
|
|
128
|
+
if (typeof args.type === "string") argv.push(args.type);
|
|
129
|
+
if (args.includeRemote) argv.push("-r");
|
|
130
|
+
break;
|
|
131
|
+
case "gflows_start":
|
|
132
|
+
argv.push("start", String(args.type), String(args.name));
|
|
133
|
+
if (typeof args.from === "string") argv.push("-o", args.from);
|
|
134
|
+
if (args.push) argv.push("-p");
|
|
135
|
+
else argv.push("-P");
|
|
136
|
+
break;
|
|
137
|
+
case "gflows_sync":
|
|
138
|
+
argv.push("sync");
|
|
139
|
+
if (args.rebase) argv.push("--rebase");
|
|
140
|
+
if (args.force) argv.push("--force");
|
|
141
|
+
break;
|
|
142
|
+
case "gflows_finish":
|
|
143
|
+
argv.push("finish");
|
|
144
|
+
if (typeof args.type === "string") argv.push(args.type);
|
|
145
|
+
if (typeof args.branch === "string") argv.push("-B", args.branch);
|
|
146
|
+
if (args.preview) argv.push("--preview");
|
|
147
|
+
if (args.noDelete) argv.push("-N");
|
|
148
|
+
if (args.push) argv.push("-p");
|
|
149
|
+
else argv.push("-P");
|
|
150
|
+
break;
|
|
151
|
+
case "gflows_schema":
|
|
152
|
+
argv.push("schema");
|
|
153
|
+
break;
|
|
154
|
+
default:
|
|
155
|
+
return { stdout: "", stderr: `Unknown tool ${name}`, exitCode: 1 };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const proc = Bun.spawn(["bun", cliPath, ...argv], {
|
|
159
|
+
cwd: path,
|
|
160
|
+
stdout: "pipe",
|
|
161
|
+
stderr: "pipe",
|
|
162
|
+
env: { ...process.env },
|
|
163
|
+
});
|
|
164
|
+
const [stdout, stderr] = await Promise.all([
|
|
165
|
+
new Response(proc.stdout).text(),
|
|
166
|
+
new Response(proc.stderr).text(),
|
|
167
|
+
]);
|
|
168
|
+
const exitCode = await proc.exited;
|
|
169
|
+
return { stdout, stderr, exitCode };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Starts a minimal JSON-RPC MCP-like stdio loop.
|
|
174
|
+
*/
|
|
175
|
+
export async function run(_args: ParsedArgs): Promise<void> {
|
|
176
|
+
console.error(
|
|
177
|
+
"gflows mcp: listening on stdio (JSON-RPC). Tools: status, doctor, list, start, sync, finish, schema.",
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const decoder = new TextDecoder();
|
|
181
|
+
let buffer = "";
|
|
182
|
+
|
|
183
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
184
|
+
buffer += decoder.decode(chunk);
|
|
185
|
+
const lines = buffer.split("\n");
|
|
186
|
+
buffer = lines.pop() ?? "";
|
|
187
|
+
for (const line of lines) {
|
|
188
|
+
const trimmed = line.trim();
|
|
189
|
+
if (!trimmed) continue;
|
|
190
|
+
let req: JsonRpcRequest;
|
|
191
|
+
try {
|
|
192
|
+
req = JSON.parse(trimmed) as JsonRpcRequest;
|
|
193
|
+
} catch {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (req.method === "initialize") {
|
|
198
|
+
respond(req.id, {
|
|
199
|
+
protocolVersion: "2024-11-05",
|
|
200
|
+
capabilities: { tools: {} },
|
|
201
|
+
serverInfo: { name: "gflows", version: getVersion() },
|
|
202
|
+
});
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (req.method === "notifications/initialized") {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (req.method === "tools/list") {
|
|
209
|
+
respond(req.id, { tools: TOOLS });
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (req.method === "tools/call") {
|
|
213
|
+
const params = req.params ?? {};
|
|
214
|
+
const name = String(params.name ?? "");
|
|
215
|
+
const toolArgs = (params.arguments ?? {}) as Record<string, unknown>;
|
|
216
|
+
try {
|
|
217
|
+
const result = await runTool(name, toolArgs);
|
|
218
|
+
respond(req.id, {
|
|
219
|
+
content: [
|
|
220
|
+
{
|
|
221
|
+
type: "text",
|
|
222
|
+
text: JSON.stringify(result),
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
isError: result.exitCode !== 0,
|
|
226
|
+
});
|
|
227
|
+
} catch (err) {
|
|
228
|
+
respondError(req.id, err instanceof Error ? err.message : String(err));
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (req.method === "ping") {
|
|
233
|
+
respond(req.id, {});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
process.exit(EXIT_OK);
|
|
239
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open a pull/merge request against the correct merge target via gh or glab.
|
|
3
|
+
* @module commands/pr
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { resolveConfig } from "../config.js";
|
|
7
|
+
import { EXIT_GIT, EXIT_USER } from "../constants.js";
|
|
8
|
+
import {
|
|
9
|
+
classifyBranch,
|
|
10
|
+
formatMergeTarget,
|
|
11
|
+
parseBranchTypeAndVersion,
|
|
12
|
+
primaryTargetBranch,
|
|
13
|
+
resolveMergeTarget,
|
|
14
|
+
} from "../flow.js";
|
|
15
|
+
import { assertNotDetached, getCurrentBranch, push, resolveRepoRoot, runGit } from "../git.js";
|
|
16
|
+
import { hint, success } from "../out.js";
|
|
17
|
+
import type { ParsedArgs } from "../types.js";
|
|
18
|
+
|
|
19
|
+
async function which(cmd: string): Promise<boolean> {
|
|
20
|
+
const r = Bun.spawn(["which", cmd], { stdout: "pipe", stderr: "pipe" });
|
|
21
|
+
await r.exited;
|
|
22
|
+
return r.exitCode === 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates a PR/MR for the current (or named) workflow branch.
|
|
27
|
+
*/
|
|
28
|
+
export async function run(args: ParsedArgs): Promise<void> {
|
|
29
|
+
const repoRoot = await resolveRepoRoot(args.cwd);
|
|
30
|
+
const config = resolveConfig(
|
|
31
|
+
repoRoot,
|
|
32
|
+
{ main: args.main, dev: args.dev, remote: args.remote },
|
|
33
|
+
{ verbose: args.verbose },
|
|
34
|
+
);
|
|
35
|
+
const opts = { dryRun: args.dryRun, verbose: args.verbose };
|
|
36
|
+
|
|
37
|
+
await assertNotDetached(repoRoot);
|
|
38
|
+
const branch =
|
|
39
|
+
(typeof args.branch === "string" && args.branch.trim()) ||
|
|
40
|
+
args.name ||
|
|
41
|
+
(await getCurrentBranch(repoRoot, opts));
|
|
42
|
+
if (!branch) {
|
|
43
|
+
console.error("gflows pr: no branch. Checkout a workflow branch or pass -B <name>.");
|
|
44
|
+
process.exit(EXIT_USER);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const classification = classifyBranch(branch, config);
|
|
48
|
+
if (classification === "main" || classification === "dev" || classification === null) {
|
|
49
|
+
console.error(`gflows pr: '${branch}' is not a workflow branch.`);
|
|
50
|
+
process.exit(EXIT_USER);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const parsed = parseBranchTypeAndVersion(branch, config.prefixes);
|
|
54
|
+
const type = parsed?.type ?? classification;
|
|
55
|
+
const mergeTarget = await resolveMergeTarget(repoRoot, branch, type, config, opts);
|
|
56
|
+
const base = primaryTargetBranch(mergeTarget, config);
|
|
57
|
+
|
|
58
|
+
const hasGh = await which("gh");
|
|
59
|
+
const hasGlab = await which("glab");
|
|
60
|
+
if (!hasGh && !hasGlab) {
|
|
61
|
+
console.error("gflows pr: neither `gh` nor `glab` found on PATH.");
|
|
62
|
+
hint("Install GitHub CLI (gh) or GitLab CLI (glab), then retry.");
|
|
63
|
+
process.exit(EXIT_GIT);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.error("gflows pr plan:");
|
|
67
|
+
console.error(` head: ${branch}`);
|
|
68
|
+
console.error(` base: ${base}`);
|
|
69
|
+
console.error(` via: ${hasGh ? "gh" : "glab"}`);
|
|
70
|
+
|
|
71
|
+
if (args.dryRun || args.preview) {
|
|
72
|
+
success("gflows: preview only (no PR created).");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Ensure upstream
|
|
77
|
+
const remote = args.remote ?? config.remote;
|
|
78
|
+
const pushCode = await push(repoRoot, remote, [branch], false, opts);
|
|
79
|
+
if (pushCode !== 0) {
|
|
80
|
+
// try set upstream
|
|
81
|
+
const r = await runGit(["push", "-u", remote, branch], { cwd: repoRoot, ...opts });
|
|
82
|
+
if (r.exitCode !== 0) {
|
|
83
|
+
console.error("gflows pr: failed to push branch to remote.");
|
|
84
|
+
process.exit(EXIT_GIT);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const title = branch;
|
|
89
|
+
if (hasGh) {
|
|
90
|
+
const proc = Bun.spawn(
|
|
91
|
+
["gh", "pr", "create", "--base", base, "--head", branch, "--title", title, "--body", ""],
|
|
92
|
+
{ cwd: repoRoot, stdout: "inherit", stderr: "inherit" },
|
|
93
|
+
);
|
|
94
|
+
const code = await proc.exited;
|
|
95
|
+
if (code !== 0) process.exit(EXIT_GIT);
|
|
96
|
+
} else {
|
|
97
|
+
const proc = Bun.spawn(
|
|
98
|
+
[
|
|
99
|
+
"glab",
|
|
100
|
+
"mr",
|
|
101
|
+
"create",
|
|
102
|
+
"--target-branch",
|
|
103
|
+
base,
|
|
104
|
+
"--source-branch",
|
|
105
|
+
branch,
|
|
106
|
+
"--title",
|
|
107
|
+
title,
|
|
108
|
+
"--yes",
|
|
109
|
+
],
|
|
110
|
+
{ cwd: repoRoot, stdout: "inherit", stderr: "inherit" },
|
|
111
|
+
);
|
|
112
|
+
const code = await proc.exited;
|
|
113
|
+
if (code !== 0) process.exit(EXIT_GIT);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!args.quiet) {
|
|
117
|
+
success(`gflows: opened PR/MR for '${branch}' → ${base}.`);
|
|
118
|
+
hint(`Merge target(s) for this type: ${formatMergeTarget(mergeTarget, config)}.`);
|
|
119
|
+
}
|
|
120
|
+
}
|