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,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ink-native prompts used inside the hub (no Clack drop-out).
|
|
3
|
+
* @module tui/prompts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Box, Text, useInput } from "ink";
|
|
7
|
+
import type React from "react";
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
|
|
10
|
+
const ACCENT = "#E88C4A";
|
|
11
|
+
const MUTED = "#8A8A8A";
|
|
12
|
+
const FG = "#E6E6E6";
|
|
13
|
+
|
|
14
|
+
/** Option for {@link InkSelect}. */
|
|
15
|
+
export interface InkSelectOption<T extends string> {
|
|
16
|
+
value: T;
|
|
17
|
+
label: string;
|
|
18
|
+
hint?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Arrow-key select list.
|
|
23
|
+
*/
|
|
24
|
+
export function InkSelect<T extends string>({
|
|
25
|
+
message,
|
|
26
|
+
options,
|
|
27
|
+
initialIndex = 0,
|
|
28
|
+
onSubmit,
|
|
29
|
+
onCancel,
|
|
30
|
+
}: {
|
|
31
|
+
message: string;
|
|
32
|
+
options: InkSelectOption<T>[];
|
|
33
|
+
initialIndex?: number;
|
|
34
|
+
onSubmit: (value: T) => void;
|
|
35
|
+
onCancel?: () => void;
|
|
36
|
+
}): React.ReactElement {
|
|
37
|
+
const [selected, setSelected] = useState(
|
|
38
|
+
Math.min(Math.max(0, initialIndex), Math.max(0, options.length - 1)),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
useInput((ch, key) => {
|
|
42
|
+
if (key.escape || (key.ctrl && ch === "c")) {
|
|
43
|
+
onCancel?.();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (key.upArrow) {
|
|
47
|
+
setSelected((i) => Math.max(0, i - 1));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (key.downArrow) {
|
|
51
|
+
setSelected((i) => Math.min(options.length - 1, i + 1));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (key.return) {
|
|
55
|
+
const opt = options[selected];
|
|
56
|
+
if (opt) onSubmit(opt.value);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Box flexDirection="column">
|
|
62
|
+
<Text bold color={FG}>
|
|
63
|
+
{message}
|
|
64
|
+
</Text>
|
|
65
|
+
{options.map((opt, i) => {
|
|
66
|
+
const active = i === selected;
|
|
67
|
+
return (
|
|
68
|
+
<Text key={opt.value} color={active ? FG : MUTED} bold={active}>
|
|
69
|
+
{active ? <Text color={ACCENT}>❯ </Text> : " "}
|
|
70
|
+
{opt.label}
|
|
71
|
+
{opt.hint ? <Text color={MUTED}>{` ${opt.hint}`}</Text> : null}
|
|
72
|
+
</Text>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
<Text color={MUTED}>↑↓ · enter · esc cancel</Text>
|
|
76
|
+
</Box>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Yes/no confirm (←/→ or y/n).
|
|
82
|
+
*/
|
|
83
|
+
export function InkConfirm({
|
|
84
|
+
message,
|
|
85
|
+
initialValue = true,
|
|
86
|
+
onSubmit,
|
|
87
|
+
onCancel,
|
|
88
|
+
}: {
|
|
89
|
+
message: string;
|
|
90
|
+
initialValue?: boolean;
|
|
91
|
+
onSubmit: (value: boolean) => void;
|
|
92
|
+
onCancel?: () => void;
|
|
93
|
+
}): React.ReactElement {
|
|
94
|
+
const [value, setValue] = useState(initialValue);
|
|
95
|
+
|
|
96
|
+
useInput((ch, key) => {
|
|
97
|
+
if (key.escape || (key.ctrl && ch === "c")) {
|
|
98
|
+
onCancel?.();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (key.leftArrow || key.rightArrow || ch === " ") {
|
|
102
|
+
setValue((v) => !v);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (ch === "y" || ch === "Y") {
|
|
106
|
+
onSubmit(true);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (ch === "n" || ch === "N") {
|
|
110
|
+
onSubmit(false);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (key.return) onSubmit(value);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<Box flexDirection="column">
|
|
118
|
+
<Text bold color={FG}>
|
|
119
|
+
{message}
|
|
120
|
+
</Text>
|
|
121
|
+
<Text>
|
|
122
|
+
<Text color={value ? ACCENT : MUTED} bold={value}>
|
|
123
|
+
{value ? "●" : "○"} Yes
|
|
124
|
+
</Text>
|
|
125
|
+
{" "}
|
|
126
|
+
<Text color={!value ? ACCENT : MUTED} bold={!value}>
|
|
127
|
+
{!value ? "●" : "○"} No
|
|
128
|
+
</Text>
|
|
129
|
+
</Text>
|
|
130
|
+
<Text color={MUTED}>←→ / y n · enter · esc cancel</Text>
|
|
131
|
+
</Box>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Single-line text input.
|
|
137
|
+
*/
|
|
138
|
+
export function InkText({
|
|
139
|
+
message,
|
|
140
|
+
placeholder,
|
|
141
|
+
initialValue = "",
|
|
142
|
+
onSubmit,
|
|
143
|
+
onCancel,
|
|
144
|
+
}: {
|
|
145
|
+
message: string;
|
|
146
|
+
placeholder?: string;
|
|
147
|
+
initialValue?: string;
|
|
148
|
+
onSubmit: (value: string) => void;
|
|
149
|
+
onCancel?: () => void;
|
|
150
|
+
}): React.ReactElement {
|
|
151
|
+
const [value, setValue] = useState(initialValue);
|
|
152
|
+
|
|
153
|
+
useInput((ch, key) => {
|
|
154
|
+
if (key.escape || (key.ctrl && ch === "c")) {
|
|
155
|
+
onCancel?.();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (key.return) {
|
|
159
|
+
const out = value.trim() || (placeholder ?? "");
|
|
160
|
+
if (out) onSubmit(out);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (key.backspace || key.delete) {
|
|
164
|
+
setValue((s) => [...s].slice(0, -1).join(""));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (ch && !key.ctrl && !key.meta && ch >= " ") {
|
|
168
|
+
setValue((s) => s + ch);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<Box flexDirection="column">
|
|
174
|
+
<Text bold color={FG}>
|
|
175
|
+
{message}
|
|
176
|
+
</Text>
|
|
177
|
+
<Text>
|
|
178
|
+
<Text color={ACCENT}>❯ </Text>
|
|
179
|
+
{value || <Text color={MUTED}>{placeholder ?? ""}</Text>}
|
|
180
|
+
<Text color={ACCENT}>█</Text>
|
|
181
|
+
</Text>
|
|
182
|
+
<Text color={MUTED}>type · enter · esc cancel</Text>
|
|
183
|
+
</Box>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Framed wizard chrome around a prompt step.
|
|
189
|
+
*/
|
|
190
|
+
export function WizardFrame({
|
|
191
|
+
title,
|
|
192
|
+
children,
|
|
193
|
+
}: {
|
|
194
|
+
title: string;
|
|
195
|
+
children: React.ReactNode;
|
|
196
|
+
}): React.ReactElement {
|
|
197
|
+
return (
|
|
198
|
+
<Box flexDirection="column" borderStyle="round" borderColor={ACCENT} paddingX={1} paddingY={0}>
|
|
199
|
+
<Text bold color={ACCENT}>
|
|
200
|
+
{title}
|
|
201
|
+
</Text>
|
|
202
|
+
<Box marginTop={1} flexDirection="column">
|
|
203
|
+
{children}
|
|
204
|
+
</Box>
|
|
205
|
+
</Box>
|
|
206
|
+
);
|
|
207
|
+
}
|
package/src/tui/slash.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash-command catalog for the Ink hub prompt.
|
|
3
|
+
* @module tui/slash
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** One slash command shown in hub autocomplete. */
|
|
7
|
+
export interface SlashCommand {
|
|
8
|
+
/** Command name without leading `/`. */
|
|
9
|
+
name: string;
|
|
10
|
+
/** Short description for the suggestion list. */
|
|
11
|
+
hint: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Hub slash commands (order = suggestion priority).
|
|
16
|
+
*/
|
|
17
|
+
export const SLASH_COMMANDS: readonly SlashCommand[] = [
|
|
18
|
+
{ name: "init", hint: "Initialize repo" },
|
|
19
|
+
{ name: "start", hint: "Start new work (wizard)" },
|
|
20
|
+
{ name: "sync", hint: "Sync with base (wizard)" },
|
|
21
|
+
{ name: "pr", hint: "Open pull request" },
|
|
22
|
+
{ name: "finish", hint: "Finish / merge (wizard)" },
|
|
23
|
+
{ name: "continue", hint: "Continue suspended run" },
|
|
24
|
+
{ name: "switch", hint: "Switch branch" },
|
|
25
|
+
{ name: "list", hint: "List branches" },
|
|
26
|
+
{ name: "doctor", hint: "Doctor checks" },
|
|
27
|
+
{ name: "help", hint: "Show help" },
|
|
28
|
+
{ name: "status", hint: "Repo status" },
|
|
29
|
+
{ name: "config", hint: "Show config" },
|
|
30
|
+
{ name: "bump", hint: "Bump version" },
|
|
31
|
+
{ name: "viz", hint: "Branch map (on home)" },
|
|
32
|
+
{ name: "quit", hint: "Leave hub" },
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Filters slash commands by the typed buffer (e.g. `/st` → start, status, switch).
|
|
37
|
+
*/
|
|
38
|
+
export function filterSlashCommands(input: string): SlashCommand[] {
|
|
39
|
+
if (!input.startsWith("/")) return [];
|
|
40
|
+
const body = input.slice(1);
|
|
41
|
+
const token = (body.split(/\s+/)[0] ?? "").toLowerCase();
|
|
42
|
+
// After a completed command + space, stop suggesting names
|
|
43
|
+
if (body.includes(" ") && token.length > 0) {
|
|
44
|
+
const exact = SLASH_COMMANDS.some((c) => c.name === token);
|
|
45
|
+
if (exact) return [];
|
|
46
|
+
}
|
|
47
|
+
if (token.length === 0) return [...SLASH_COMMANDS];
|
|
48
|
+
return SLASH_COMMANDS.filter((c) => c.name.startsWith(token));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Completes the command portion of a slash buffer (Tab).
|
|
53
|
+
* @returns updated input, or null if nothing to complete.
|
|
54
|
+
*/
|
|
55
|
+
export function completeSlashInput(input: string, selectedIndex = 0): string | null {
|
|
56
|
+
const matches = filterSlashCommands(input);
|
|
57
|
+
if (matches.length === 0) return null;
|
|
58
|
+
const pick = matches[Math.min(selectedIndex, matches.length - 1)];
|
|
59
|
+
if (!pick) return null;
|
|
60
|
+
const rest = input.includes(" ") ? input.slice(input.indexOf(" ")) : "";
|
|
61
|
+
if (rest.trim().length > 0) return null;
|
|
62
|
+
return `/${pick.name}${matches.length === 1 ? " " : ""}`;
|
|
63
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -34,7 +34,42 @@ export type Command =
|
|
|
34
34
|
| "completion"
|
|
35
35
|
| "status"
|
|
36
36
|
| "help"
|
|
37
|
-
| "version"
|
|
37
|
+
| "version"
|
|
38
|
+
| "sync"
|
|
39
|
+
| "pr"
|
|
40
|
+
| "doctor"
|
|
41
|
+
| "config"
|
|
42
|
+
| "schema"
|
|
43
|
+
| "continue"
|
|
44
|
+
| "undo"
|
|
45
|
+
| "abort"
|
|
46
|
+
| "mcp"
|
|
47
|
+
| "viz";
|
|
48
|
+
|
|
49
|
+
/** All CLI commands in display order. */
|
|
50
|
+
export const ALL_COMMANDS: Command[] = [
|
|
51
|
+
"init",
|
|
52
|
+
"start",
|
|
53
|
+
"finish",
|
|
54
|
+
"switch",
|
|
55
|
+
"delete",
|
|
56
|
+
"list",
|
|
57
|
+
"bump",
|
|
58
|
+
"sync",
|
|
59
|
+
"pr",
|
|
60
|
+
"viz",
|
|
61
|
+
"doctor",
|
|
62
|
+
"config",
|
|
63
|
+
"schema",
|
|
64
|
+
"continue",
|
|
65
|
+
"undo",
|
|
66
|
+
"abort",
|
|
67
|
+
"completion",
|
|
68
|
+
"status",
|
|
69
|
+
"help",
|
|
70
|
+
"version",
|
|
71
|
+
"mcp",
|
|
72
|
+
];
|
|
38
73
|
|
|
39
74
|
/** Branch prefix overrides per type (e.g. "feature" -> "feature/"). */
|
|
40
75
|
export interface BranchPrefixes {
|
|
@@ -75,6 +110,9 @@ export type BumpDirection = "up" | "down";
|
|
|
75
110
|
/** Bump type (semver segment). */
|
|
76
111
|
export type BumpType = "patch" | "minor" | "major";
|
|
77
112
|
|
|
113
|
+
/** Config subcommand for `gflows config`. */
|
|
114
|
+
export type ConfigAction = "get" | "set";
|
|
115
|
+
|
|
78
116
|
/** Parsed CLI arguments after resolving command, type, name, and flags. */
|
|
79
117
|
export interface ParsedArgs {
|
|
80
118
|
command: Command;
|
|
@@ -92,6 +130,12 @@ export interface ParsedArgs {
|
|
|
92
130
|
bumpDirection?: BumpDirection;
|
|
93
131
|
/** Bump type (patch | minor | major). */
|
|
94
132
|
bumpType?: BumpType;
|
|
133
|
+
/** Config get/set action. */
|
|
134
|
+
configAction?: ConfigAction;
|
|
135
|
+
/** Config key (main, dev, remote, or prefixes.<type>). */
|
|
136
|
+
configKey?: string;
|
|
137
|
+
/** Config value for set. */
|
|
138
|
+
configValue?: string;
|
|
95
139
|
// Common flags
|
|
96
140
|
push: boolean;
|
|
97
141
|
noPush: boolean;
|
|
@@ -107,7 +151,9 @@ export interface ParsedArgs {
|
|
|
107
151
|
quiet: boolean;
|
|
108
152
|
force: boolean;
|
|
109
153
|
path: string | undefined;
|
|
110
|
-
|
|
154
|
+
/** Raw --from / -o value (base branch override). */
|
|
155
|
+
from: string | undefined;
|
|
156
|
+
/** True when --from points at main (configured or literal). */
|
|
111
157
|
fromMain: boolean;
|
|
112
158
|
// finish
|
|
113
159
|
noFf: boolean;
|
|
@@ -117,8 +163,21 @@ export interface ParsedArgs {
|
|
|
117
163
|
noTag: boolean;
|
|
118
164
|
tagMessage: string | undefined;
|
|
119
165
|
message: string | undefined;
|
|
120
|
-
|
|
166
|
+
squash: boolean;
|
|
167
|
+
preview: boolean;
|
|
168
|
+
bumpOnFinish: boolean;
|
|
169
|
+
// list / status
|
|
121
170
|
includeRemote: boolean;
|
|
171
|
+
json: boolean;
|
|
172
|
+
// sync
|
|
173
|
+
rebase: boolean;
|
|
122
174
|
// switch: explicit mode when uncommitted (overrides prompt)
|
|
123
175
|
switchMode?: "restore" | "clean" | "cancel" | "move" | "destroy";
|
|
176
|
+
/**
|
|
177
|
+
* Optional package.json script name to wire to `gflows` during init
|
|
178
|
+
* (e.g. `"g"` → `bun run g -- start feature x`). Undefined = ask (TTY) or skip.
|
|
179
|
+
*/
|
|
180
|
+
scriptAlias: string | undefined;
|
|
181
|
+
/** When true, never add a package.json script alias during init. */
|
|
182
|
+
noScriptAlias: boolean;
|
|
124
183
|
}
|
package/src/ui.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modern terminal chrome shared by viz and the interactive hub.
|
|
3
|
+
* Lightweight ANSI panels (Claude Code / Codex–style), no TUI framework.
|
|
4
|
+
* @module ui
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const RESET = "\x1b[0m";
|
|
8
|
+
const BOLD = "\x1b[1m";
|
|
9
|
+
const DIM = "\x1b[2m";
|
|
10
|
+
const CYAN = "\x1b[36m";
|
|
11
|
+
const GREEN = "\x1b[32m";
|
|
12
|
+
const YELLOW = "\x1b[33m";
|
|
13
|
+
const MAGENTA = "\x1b[35m";
|
|
14
|
+
const BLUE = "\x1b[34m";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Whether stdout can use ANSI color.
|
|
18
|
+
*/
|
|
19
|
+
export function colorEnabled(): boolean {
|
|
20
|
+
if (process.env.NO_COLOR) return false;
|
|
21
|
+
return typeof process.stdout.isTTY === "boolean" && process.stdout.isTTY;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Wraps text in an ANSI code when color is enabled.
|
|
26
|
+
*/
|
|
27
|
+
export function paint(code: string, text: string): string {
|
|
28
|
+
return colorEnabled() ? `${code}${text}${RESET}` : text;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Shared ANSI codes for callers that compose styles. */
|
|
32
|
+
export const ansi = {
|
|
33
|
+
reset: RESET,
|
|
34
|
+
bold: BOLD,
|
|
35
|
+
dim: DIM,
|
|
36
|
+
cyan: CYAN,
|
|
37
|
+
green: GREEN,
|
|
38
|
+
yellow: YELLOW,
|
|
39
|
+
magenta: MAGENTA,
|
|
40
|
+
blue: BLUE,
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
const ESC = "\u001b";
|
|
44
|
+
const ANSI_RE = new RegExp(`${ESC}\\[[0-9;]*m`, "g");
|
|
45
|
+
const ANSI_PREFIX_RE = new RegExp(`^${ESC}\\[[0-9;]*m`);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Visible length of a string ignoring ANSI escape sequences.
|
|
49
|
+
*/
|
|
50
|
+
export function visibleLength(text: string): number {
|
|
51
|
+
return text.replace(ANSI_RE, "").length;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Pads or soft-truncates a line to a target visible width.
|
|
56
|
+
*/
|
|
57
|
+
export function padVisible(text: string, width: number): string {
|
|
58
|
+
const len = visibleLength(text);
|
|
59
|
+
if (len === width) return text;
|
|
60
|
+
if (len < width) return text + " ".repeat(width - len);
|
|
61
|
+
let out = "";
|
|
62
|
+
let vis = 0;
|
|
63
|
+
let i = 0;
|
|
64
|
+
while (i < text.length && vis < width - 1) {
|
|
65
|
+
if (text[i] === ESC) {
|
|
66
|
+
const m = text.slice(i).match(ANSI_PREFIX_RE);
|
|
67
|
+
if (m) {
|
|
68
|
+
out += m[0];
|
|
69
|
+
i += m[0].length;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
out += text[i];
|
|
74
|
+
vis++;
|
|
75
|
+
i++;
|
|
76
|
+
}
|
|
77
|
+
return `${out}…`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Renders a rounded status panel (title bar + body rows).
|
|
82
|
+
*/
|
|
83
|
+
export function renderPanel(title: string, body: string[], width = 56): string[] {
|
|
84
|
+
const inner = Math.max(24, width - 2);
|
|
85
|
+
const labeled = `─ ${title} `;
|
|
86
|
+
const fill = Math.max(0, inner - visibleLength(labeled));
|
|
87
|
+
const top = paint(CYAN + BOLD, `╭${labeled}${"─".repeat(fill)}╮`);
|
|
88
|
+
const bottom = paint(DIM, `╰${"─".repeat(inner)}╯`);
|
|
89
|
+
const rows = body.map((line) => {
|
|
90
|
+
const content = padVisible(` ${line}`, inner);
|
|
91
|
+
return `${paint(DIM, "│")}${content}${paint(DIM, "│")}`;
|
|
92
|
+
});
|
|
93
|
+
return [top, ...rows, bottom];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Section header.
|
|
98
|
+
*/
|
|
99
|
+
export function section(title: string): string {
|
|
100
|
+
return paint(BOLD, title);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Dim horizontal rule.
|
|
105
|
+
*/
|
|
106
|
+
export function rule(width = 56): string {
|
|
107
|
+
return paint(DIM, "─".repeat(width));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Accent arrow for recommended next step.
|
|
112
|
+
*/
|
|
113
|
+
export function recommendLine(label: string): string {
|
|
114
|
+
return `${paint(GREEN + BOLD, "→")} ${paint(BOLD, label)}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Chip / badge text.
|
|
119
|
+
*/
|
|
120
|
+
export function chip(text: string, tone: "ok" | "warn" | "info" | "muted" = "muted"): string {
|
|
121
|
+
const code = tone === "ok" ? GREEN : tone === "warn" ? YELLOW : tone === "info" ? CYAN : DIM;
|
|
122
|
+
return paint(code, `[${text}]`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Keyboard hint footer.
|
|
127
|
+
*/
|
|
128
|
+
export function keyHints(pairs: Array<[string, string]>): string {
|
|
129
|
+
return pairs
|
|
130
|
+
.map(([key, label]) => `${paint(BOLD, key)} ${paint(DIM, label)}`)
|
|
131
|
+
.join(paint(DIM, " · "));
|
|
132
|
+
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package version helper for CLI chrome and schema.
|
|
3
|
+
* @module version
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Reads the installed/package version from package.json.
|
|
14
|
+
*/
|
|
15
|
+
export function getVersion(): string {
|
|
16
|
+
try {
|
|
17
|
+
const pkgPath = join(__dirname, "..", "package.json");
|
|
18
|
+
const raw = readFileSync(pkgPath, "utf-8");
|
|
19
|
+
const pkg = JSON.parse(raw) as { version?: string };
|
|
20
|
+
return pkg.version ?? "0.0.0";
|
|
21
|
+
} catch {
|
|
22
|
+
return "0.0.0";
|
|
23
|
+
}
|
|
24
|
+
}
|