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/viz.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal visualization for gflows: status panel, lifecycle, branch map.
|
|
3
|
+
* Modern CLI chrome (Claude Code / Codex–style), no TUI framework.
|
|
4
|
+
* @module viz
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { resolveConfig } from "./config.js";
|
|
8
|
+
import {
|
|
9
|
+
classifyBranch,
|
|
10
|
+
filterWorkflowBranches,
|
|
11
|
+
formatMergeTarget,
|
|
12
|
+
getBaseBranchName,
|
|
13
|
+
resolveMergeTarget,
|
|
14
|
+
} from "./flow.js";
|
|
15
|
+
import { branchList, getAheadBehind, getCurrentBranch, resolveRepoRoot, revParse } from "./git.js";
|
|
16
|
+
import { isLifecycleStep, LIFECYCLE_ORDER, recommend } from "./recommend.js";
|
|
17
|
+
import { readActiveRun } from "./run-state.js";
|
|
18
|
+
import type { BranchType, ResolvedConfig } from "./types.js";
|
|
19
|
+
import {
|
|
20
|
+
ansi,
|
|
21
|
+
chip,
|
|
22
|
+
colorEnabled,
|
|
23
|
+
keyHints,
|
|
24
|
+
paint,
|
|
25
|
+
recommendLine,
|
|
26
|
+
renderPanel,
|
|
27
|
+
rule,
|
|
28
|
+
section,
|
|
29
|
+
} from "./ui.js";
|
|
30
|
+
|
|
31
|
+
/** One workflow branch row for the map. */
|
|
32
|
+
export interface VizBranchRow {
|
|
33
|
+
name: string;
|
|
34
|
+
type: BranchType | "main" | "dev" | "unknown";
|
|
35
|
+
current: boolean;
|
|
36
|
+
ahead: number;
|
|
37
|
+
behind: number;
|
|
38
|
+
base: string;
|
|
39
|
+
mergeTargetDisplay: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Snapshot used to render the visual panel. */
|
|
43
|
+
export interface VizSnapshot {
|
|
44
|
+
main: string;
|
|
45
|
+
dev: string;
|
|
46
|
+
current: string | null;
|
|
47
|
+
suspended: string | null;
|
|
48
|
+
/** True when main or dev is missing — need `gflows init`. */
|
|
49
|
+
needsInit: boolean;
|
|
50
|
+
rows: VizBranchRow[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Collects repo state for visualization.
|
|
55
|
+
*/
|
|
56
|
+
export async function collectVizSnapshot(
|
|
57
|
+
cwd: string,
|
|
58
|
+
overrides?: { main?: string; dev?: string; remote?: string },
|
|
59
|
+
): Promise<VizSnapshot> {
|
|
60
|
+
const root = await resolveRepoRoot(cwd);
|
|
61
|
+
const config = resolveConfig(root, overrides ?? {}, {});
|
|
62
|
+
const current = await getCurrentBranch(root, {});
|
|
63
|
+
const active = readActiveRun(root);
|
|
64
|
+
const all = await branchList(root, { dryRun: false, verbose: false });
|
|
65
|
+
const workflow = filterWorkflowBranches(all, config.prefixes);
|
|
66
|
+
|
|
67
|
+
const mainOk = await revParse(root, config.main, [], { verbose: false }).then(
|
|
68
|
+
() => true,
|
|
69
|
+
() => false,
|
|
70
|
+
);
|
|
71
|
+
const devOk = await revParse(root, config.dev, [], { verbose: false }).then(
|
|
72
|
+
() => true,
|
|
73
|
+
() => false,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const rows: VizBranchRow[] = [];
|
|
77
|
+
|
|
78
|
+
for (const name of [config.main, config.dev]) {
|
|
79
|
+
if (!all.includes(name)) continue;
|
|
80
|
+
rows.push({
|
|
81
|
+
name,
|
|
82
|
+
type: name === config.main ? "main" : "dev",
|
|
83
|
+
current: current === name,
|
|
84
|
+
ahead: 0,
|
|
85
|
+
behind: 0,
|
|
86
|
+
base: "—",
|
|
87
|
+
mergeTargetDisplay: name === config.main ? "production" : "integration",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const name of workflow) {
|
|
92
|
+
const kind = classifyBranch(name, config);
|
|
93
|
+
if (!kind || kind === "main" || kind === "dev") {
|
|
94
|
+
rows.push({
|
|
95
|
+
name,
|
|
96
|
+
type: "unknown",
|
|
97
|
+
current: current === name,
|
|
98
|
+
ahead: 0,
|
|
99
|
+
behind: 0,
|
|
100
|
+
base: "—",
|
|
101
|
+
mergeTargetDisplay: "—",
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const base = getBaseBranchName(kind, false, config);
|
|
106
|
+
const mergeTarget = await resolveMergeTarget(root, name, kind, config, {});
|
|
107
|
+
const ab = await getAheadBehind(root, base, name, {});
|
|
108
|
+
rows.push({
|
|
109
|
+
name,
|
|
110
|
+
type: kind,
|
|
111
|
+
current: current === name,
|
|
112
|
+
ahead: ab.ahead,
|
|
113
|
+
behind: ab.behind,
|
|
114
|
+
base,
|
|
115
|
+
mergeTargetDisplay: formatMergeTarget(mergeTarget, config),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
main: config.main,
|
|
121
|
+
dev: config.dev,
|
|
122
|
+
current,
|
|
123
|
+
suspended: active ? `${active.command} (${active.status})` : null,
|
|
124
|
+
needsInit: !mainOk || !devOk,
|
|
125
|
+
rows,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Renders a flow legend for the main/dev model.
|
|
131
|
+
*/
|
|
132
|
+
export function renderFlowLegend(config: Pick<ResolvedConfig, "main" | "dev">): string[] {
|
|
133
|
+
const m = config.main;
|
|
134
|
+
const d = config.dev;
|
|
135
|
+
return [
|
|
136
|
+
section("Flow"),
|
|
137
|
+
` ${paint(ansi.green + ansi.bold, m)} ${paint(ansi.dim, "← release / hotfix")}`,
|
|
138
|
+
` ${paint(ansi.dim, "│")}`,
|
|
139
|
+
` ${paint(ansi.dim, "└─→")} ${paint(ansi.cyan + ansi.bold, d)} ${paint(ansi.dim, "← feature / bugfix / chore / spike")}`,
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Renders an ASCII branch map highlighting the current branch.
|
|
145
|
+
*/
|
|
146
|
+
export function renderBranchMap(snap: VizSnapshot): string[] {
|
|
147
|
+
const lines: string[] = [section("Branches")];
|
|
148
|
+
if (snap.suspended) {
|
|
149
|
+
lines.push(` ${chip("suspended", "warn")} ${snap.suspended}`);
|
|
150
|
+
}
|
|
151
|
+
if (snap.needsInit) {
|
|
152
|
+
lines.push(` ${chip("setup", "warn")} missing ${snap.main} and/or ${snap.dev}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const longLived = snap.rows.filter((r) => r.type === "main" || r.type === "dev");
|
|
156
|
+
const workflow = snap.rows.filter((r) => r.type !== "main" && r.type !== "dev");
|
|
157
|
+
|
|
158
|
+
for (const row of longLived) {
|
|
159
|
+
lines.push(formatRow(row));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (workflow.length === 0) {
|
|
163
|
+
lines.push(paint(ansi.dim, " (no workflow branches yet)"));
|
|
164
|
+
} else {
|
|
165
|
+
lines.push(paint(ansi.dim, " │"));
|
|
166
|
+
for (let i = 0; i < workflow.length; i++) {
|
|
167
|
+
const row = workflow[i];
|
|
168
|
+
if (!row) continue;
|
|
169
|
+
const last = i === workflow.length - 1;
|
|
170
|
+
const elbow = last ? "└─" : "├─";
|
|
171
|
+
lines.push(formatWorkflowRow(row, elbow));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return lines;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function formatRow(row: VizBranchRow): string {
|
|
179
|
+
const mark = row.current ? paint(ansi.green, "●") : paint(ansi.dim, "○");
|
|
180
|
+
const name = row.current
|
|
181
|
+
? colorEnabled()
|
|
182
|
+
? `${ansi.bold}${ansi.green}${row.name}${ansi.reset}`
|
|
183
|
+
: row.name
|
|
184
|
+
: row.name;
|
|
185
|
+
const tag =
|
|
186
|
+
row.type === "main"
|
|
187
|
+
? chip("main", "warn")
|
|
188
|
+
: row.type === "dev"
|
|
189
|
+
? chip("dev", "info")
|
|
190
|
+
: chip(String(row.type), "muted");
|
|
191
|
+
return ` ${mark} ${name} ${tag}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function formatWorkflowRow(row: VizBranchRow, elbow: string): string {
|
|
195
|
+
const mark = row.current ? paint(ansi.green, "●") : paint(ansi.dim, "○");
|
|
196
|
+
const name = row.current
|
|
197
|
+
? colorEnabled()
|
|
198
|
+
? `${ansi.bold}${ansi.green}${row.name}${ansi.reset}`
|
|
199
|
+
: row.name
|
|
200
|
+
: row.name;
|
|
201
|
+
const ab =
|
|
202
|
+
row.ahead === 0 && row.behind === 0
|
|
203
|
+
? paint(ansi.dim, "synced")
|
|
204
|
+
: paint(ansi.dim, `+${row.ahead}/-${row.behind} vs ${row.base}`);
|
|
205
|
+
return ` ${elbow} ${mark} ${name} ${paint(ansi.dim, `(${row.type})`)} ${ab}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Renders “you are here” next-step panel for the current branch.
|
|
210
|
+
*/
|
|
211
|
+
export function renderYouAreHere(snap: VizSnapshot): string[] {
|
|
212
|
+
const rec = recommend(snap);
|
|
213
|
+
const lines: string[] = [section("Status")];
|
|
214
|
+
if (!snap.current) {
|
|
215
|
+
lines.push(paint(ansi.yellow, " HEAD detached"));
|
|
216
|
+
} else {
|
|
217
|
+
const row = snap.rows.find((r) => r.name === snap.current);
|
|
218
|
+
lines.push(` ${paint(ansi.green, "●")} ${paint(ansi.bold, snap.current)}`);
|
|
219
|
+
if (row) {
|
|
220
|
+
lines.push(
|
|
221
|
+
paint(
|
|
222
|
+
ansi.dim,
|
|
223
|
+
` type ${row.type} · base ${row.base} · merge ${row.mergeTargetDisplay}`,
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
if (row.type !== "main" && row.type !== "dev" && row.type !== "unknown") {
|
|
227
|
+
lines.push(paint(ansi.dim, ` ahead/behind +${row.ahead} / -${row.behind}`));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
lines.push(` ${recommendLine(rec.label)}`);
|
|
232
|
+
lines.push(paint(ansi.dim, ` ${rec.detail}`));
|
|
233
|
+
return lines;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Lifecycle guide strip with current step emphasized.
|
|
238
|
+
*/
|
|
239
|
+
export function renderLifecycle(snap: VizSnapshot): string[] {
|
|
240
|
+
const rec = recommend(snap);
|
|
241
|
+
const highlight = isLifecycleStep(rec.action) ? rec.action : null;
|
|
242
|
+
const path = LIFECYCLE_ORDER.map((step) =>
|
|
243
|
+
highlight === step ? paint(ansi.green + ansi.bold, step) : paint(ansi.dim, step),
|
|
244
|
+
).join(paint(ansi.dim, " → "));
|
|
245
|
+
return [section("Lifecycle"), ` ${path}`];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Full visual panel as printable lines (for hub or `gflows viz`).
|
|
250
|
+
*/
|
|
251
|
+
export function renderVizPanel(snap: VizSnapshot): string[] {
|
|
252
|
+
const rec = recommend(snap);
|
|
253
|
+
const where = snap.current ?? "detached";
|
|
254
|
+
const headerBody = [
|
|
255
|
+
`${paint(ansi.green, "●")} ${paint(ansi.bold, where)} ${chip(snap.needsInit ? "needs init" : "ready", snap.needsInit ? "warn" : "ok")}`,
|
|
256
|
+
recommendLine(rec.label),
|
|
257
|
+
paint(ansi.dim, rec.detail),
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
return [
|
|
261
|
+
"",
|
|
262
|
+
...renderPanel("gflows", headerBody),
|
|
263
|
+
"",
|
|
264
|
+
...renderLifecycle(snap),
|
|
265
|
+
"",
|
|
266
|
+
...renderFlowLegend({ main: snap.main, dev: snap.dev }),
|
|
267
|
+
"",
|
|
268
|
+
...renderBranchMap(snap),
|
|
269
|
+
"",
|
|
270
|
+
...renderYouAreHere(snap),
|
|
271
|
+
"",
|
|
272
|
+
rule(56),
|
|
273
|
+
` ${keyHints([
|
|
274
|
+
["↑↓", "move"],
|
|
275
|
+
["enter", "select"],
|
|
276
|
+
["ctrl+c", "exit"],
|
|
277
|
+
])}`,
|
|
278
|
+
"",
|
|
279
|
+
];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Prints the visual panel to stdout.
|
|
284
|
+
*/
|
|
285
|
+
export async function printViz(
|
|
286
|
+
cwd: string,
|
|
287
|
+
overrides?: { main?: string; dev?: string; remote?: string },
|
|
288
|
+
): Promise<VizSnapshot> {
|
|
289
|
+
const snap = await collectVizSnapshot(cwd, overrides);
|
|
290
|
+
for (const line of renderVizPanel(snap)) {
|
|
291
|
+
console.log(line);
|
|
292
|
+
}
|
|
293
|
+
return snap;
|
|
294
|
+
}
|