peaks-cli 1.3.3 → 1.3.5
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/dist/src/cli/commands/core-artifact-commands.js +6 -3
- package/dist/src/cli/commands/hook-handle.d.ts +2 -2
- package/dist/src/cli/commands/hook-handle.js +5 -10
- package/dist/src/cli/commands/hooks-commands.js +44 -29
- package/dist/src/cli/commands/project-commands.js +15 -5
- package/dist/src/cli/commands/workflow-commands.js +2 -1
- package/dist/src/cli/commands/workspace-commands.js +1 -2
- package/dist/src/cli/program.js +3 -2
- package/dist/src/services/dashboard/project-dashboard-service.d.ts +23 -0
- package/dist/src/services/dashboard/project-dashboard-service.js +21 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +45 -40
- package/dist/src/services/dispatch/sub-agent-dispatcher.js +25 -20
- package/dist/src/services/ide/adapters/claude-code-adapter.js +27 -2
- package/dist/src/services/ide/adapters/trae-adapter.d.ts +19 -11
- package/dist/src/services/ide/adapters/trae-adapter.js +45 -19
- package/dist/src/services/ide/hook-protocol.d.ts +7 -4
- package/dist/src/services/ide/hook-protocol.js +7 -4
- package/dist/src/services/ide/ide-types.d.ts +61 -16
- package/dist/src/services/ide/resource-profile.d.ts +52 -0
- package/dist/src/services/ide/resource-profile.js +33 -0
- package/dist/src/services/memory/project-context-service.js +2 -1
- package/dist/src/services/memory/project-memory-service.js +4 -3
- package/dist/src/services/perf/perf-baseline-service.js +2 -1
- package/dist/src/services/progress/progress-service.d.ts +23 -103
- package/dist/src/services/progress/progress-service.js +24 -137
- package/dist/src/services/scan/file-size-scan.d.ts +4 -0
- package/dist/src/services/scan/file-size-scan.js +32 -3
- package/dist/src/services/session/getSessionDir.d.ts +1 -0
- package/dist/src/services/session/getSessionDir.js +27 -0
- package/dist/src/services/session/index.d.ts +1 -0
- package/dist/src/services/session/index.js +1 -0
- package/dist/src/services/skills/hooks-settings-service.d.ts +57 -5
- package/dist/src/services/skills/hooks-settings-service.js +153 -28
- package/dist/src/services/standards/ide-aware-standards-service.d.ts +94 -0
- package/dist/src/services/standards/ide-aware-standards-service.js +89 -0
- package/dist/src/services/standards/project-standards-service.d.ts +1 -2
- package/dist/src/shared/incrementing-number.d.ts +0 -8
- package/dist/src/shared/incrementing-number.js +11 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/scripts/install-skills.mjs +112 -2
- package/skills/peaks-ide/SKILL.md +1 -1
- package/skills/peaks-ide/references/audit-log-helper.md +52 -0
- package/skills/peaks-qa/SKILL.md +104 -62
- package/skills/peaks-qa/references/qa-fanout-contract.md +6 -6
- package/skills/peaks-rd/SKILL.md +88 -73
- package/skills/peaks-solo/SKILL.md +52 -22
- package/skills/peaks-solo/references/browser-workflow.md +22 -20
- package/skills/peaks-solo/references/runbook.md +21 -21
- package/skills/peaks-solo/references/sub-agent-dispatch.md +44 -1
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +9 -9
- package/skills/peaks-ui/SKILL.md +18 -9
- package/dist/src/cli/commands/progress-close-kill.d.ts +0 -51
- package/dist/src/cli/commands/progress-close-kill.js +0 -152
- package/dist/src/cli/commands/progress-commands.d.ts +0 -3
- package/dist/src/cli/commands/progress-commands.js +0 -379
- package/dist/src/cli/commands/progress-start-spawn.d.ts +0 -59
- package/dist/src/cli/commands/progress-start-spawn.js +0 -140
- package/dist/src/cli/commands/progress-watch-render.d.ts +0 -80
- package/dist/src/cli/commands/progress-watch-render.js +0 -308
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure platform-specific spawn-args construction for
|
|
3
|
-
* `peaks progress start`. Extracted out of the commander
|
|
4
|
-
* action handler so the three platform branches (darwin /
|
|
5
|
-
* linux / win32) can be unit-tested without spawning real
|
|
6
|
-
* terminals.
|
|
7
|
-
*
|
|
8
|
-
* The CLI action calls `buildStartSpawn` once per start
|
|
9
|
-
* attempt and feeds the returned `{ command, args }` pair to
|
|
10
|
-
* `child_process.spawn(..., { detached: true, stdio: 'ignore' })`.
|
|
11
|
-
*
|
|
12
|
-
* Cross-platform strategy (see progress-commands.ts for the
|
|
13
|
-
* full design notes):
|
|
14
|
-
*
|
|
15
|
-
* - macOS: `osascript -e 'tell application "Terminal" to
|
|
16
|
-
* do script "<shell>"'`. The shell command sets
|
|
17
|
-
* the title via the OSC 0 escape (printf), runs
|
|
18
|
-
* the brand banner, then exec's the watch.
|
|
19
|
-
* - Linux: First existing terminal emulator in
|
|
20
|
-
* { gnome-terminal, konsole, xfce4-terminal,
|
|
21
|
-
* tilix, alacritty, kitty }, with per-emulator
|
|
22
|
-
* flag translation. The shell inside also sets
|
|
23
|
-
* the title via OSC 0 (emulator --title is
|
|
24
|
-
* best-effort only — the shell overrides it).
|
|
25
|
-
* - Win32: `cmd /c start "<title>" cmd /k <shell>`. The
|
|
26
|
-
* shell uses the `title` builtin (cmd.exe
|
|
27
|
-
* builtin) to re-anchor the title before the
|
|
28
|
-
* watch runs, because cmd.exe overrides the
|
|
29
|
-
* start-title with the running command name.
|
|
30
|
-
*
|
|
31
|
-
* The function never throws on unsupported platforms; it
|
|
32
|
-
* returns a discriminated `unsupported` result so the caller
|
|
33
|
-
* can surface a clean error envelope.
|
|
34
|
-
*/
|
|
35
|
-
import { existsSync } from 'node:fs';
|
|
36
|
-
/** Brand banner the user sees in the spawned shell. */
|
|
37
|
-
const BANNER = 'echo "peaks-cli — sub-agent progress"';
|
|
38
|
-
/**
|
|
39
|
-
* Build the POSIX OSC 0 title escape. The single-quote
|
|
40
|
-
* escape is bash's `'\''` for embedding a single quote in a
|
|
41
|
-
* single-quoted string; we need it because windowTitle may
|
|
42
|
-
* contain user-provided text (the `--reason` argument).
|
|
43
|
-
*/
|
|
44
|
-
function buildPosixTitleCmd(windowTitle) {
|
|
45
|
-
const escaped = windowTitle.replaceAll("'", "'\\''");
|
|
46
|
-
return `printf '\\033]0;${escaped}\\007'`;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* cmd.exe `title` builtin call. Quoting is REQUIRED: cmd /k parses the
|
|
50
|
-
* subsequent tokens as a command line, and a `:` in the unquoted title (the
|
|
51
|
-
* canonical peaks windowTitle starts with `peaks-cli:`) gets mis-read as a
|
|
52
|
-
* drive-letter prefix, triggering Windows' "找不到文件 'sub-agent'" dialog.
|
|
53
|
-
*
|
|
54
|
-
* The double-quote wrap makes the whole title one parameter to `title`.
|
|
55
|
-
* Defensive guard: reject embedded `"`, `\n`, or `\r` (the `cmd /k` parser
|
|
56
|
-
* will not survive a literal newline or an un-escaped quote in the title).
|
|
57
|
-
* Callers (progress-commands.ts:267) compose the title from CLI args; if
|
|
58
|
-
* the user passed a `--reason` containing one of these characters, the
|
|
59
|
-
* spawn attempt is abandoned with a tagged result so the CLI can surface
|
|
60
|
-
* a clear error envelope.
|
|
61
|
-
*/
|
|
62
|
-
function buildWinTitleCmd(windowTitle) {
|
|
63
|
-
if (windowTitle.includes('"') || windowTitle.includes('\n') || windowTitle.includes('\r')) {
|
|
64
|
-
return { ok: false, unsupported: true };
|
|
65
|
-
}
|
|
66
|
-
return { ok: true, cmd: `title "${windowTitle}"` };
|
|
67
|
-
}
|
|
68
|
-
/** Shared helper: the shell command that runs the watch. */
|
|
69
|
-
function buildWatchCommand(peaksBin, projectRoot) {
|
|
70
|
-
return `${peaksBin} progress watch --project "${projectRoot}"`;
|
|
71
|
-
}
|
|
72
|
-
export function buildStartSpawn(options) {
|
|
73
|
-
const { peaksBin, projectRoot, windowTitle, platform: currentPlatform } = options;
|
|
74
|
-
const watchCommand = buildWatchCommand(peaksBin, projectRoot);
|
|
75
|
-
const posixTitleCmd = buildPosixTitleCmd(windowTitle);
|
|
76
|
-
const winTitleCmd = buildWinTitleCmd(windowTitle);
|
|
77
|
-
if (currentPlatform === 'darwin') {
|
|
78
|
-
const innerShell = `${posixTitleCmd}; ${BANNER}; ${watchCommand}`;
|
|
79
|
-
const escapedInner = innerShell.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
|
|
80
|
-
return {
|
|
81
|
-
ok: true,
|
|
82
|
-
command: 'osascript',
|
|
83
|
-
args: [
|
|
84
|
-
'-e',
|
|
85
|
-
`tell application "Terminal" to do script "${escapedInner}"`,
|
|
86
|
-
'-e',
|
|
87
|
-
'tell application "Terminal" to activate'
|
|
88
|
-
]
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
if (currentPlatform === 'linux') {
|
|
92
|
-
const exists = options.linuxTerminalExists ?? ((name) => existsSync(`/usr/bin/${name}`));
|
|
93
|
-
const candidates = ['gnome-terminal', 'konsole', 'xfce4-terminal', 'tilix', 'alacritty', 'kitty'];
|
|
94
|
-
const terminal = candidates.find((c) => exists(c)) ?? candidates[0];
|
|
95
|
-
const titleArg = ['--title', windowTitle];
|
|
96
|
-
const bannerShell = `bash -c '${posixTitleCmd}; ${BANNER}; exec ${watchCommand}'`;
|
|
97
|
-
if (terminal === 'alacritty' || terminal === 'kitty') {
|
|
98
|
-
return {
|
|
99
|
-
ok: true,
|
|
100
|
-
command: terminal,
|
|
101
|
-
args: ['--class', 'peaks-cli-progress', ...titleArg, '-e', bannerShell]
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
if (terminal === 'gnome-terminal' || terminal === 'tilix' || terminal === 'xfce4-terminal') {
|
|
105
|
-
return {
|
|
106
|
-
ok: true,
|
|
107
|
-
command: terminal,
|
|
108
|
-
args: [...titleArg, '--', '/bin/bash', '-lc', bannerShell]
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
if (terminal === 'konsole') {
|
|
112
|
-
return {
|
|
113
|
-
ok: true,
|
|
114
|
-
command: terminal,
|
|
115
|
-
args: ['--title', windowTitle, '--p', 'tabtitle', windowTitle, '-e', bannerShell]
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
// xterm / fallback: no --title support; bannerShell only.
|
|
119
|
-
return { ok: true, command: terminal, args: ['-e', bannerShell] };
|
|
120
|
-
}
|
|
121
|
-
if (currentPlatform === 'win32') {
|
|
122
|
-
if (!winTitleCmd.ok) {
|
|
123
|
-
return { ok: false, unsupported: true };
|
|
124
|
-
}
|
|
125
|
-
const bannerCmd = `${winTitleCmd.cmd} && echo peaks-cli --- sub-agent progress && ${watchCommand}`;
|
|
126
|
-
return {
|
|
127
|
-
ok: true,
|
|
128
|
-
command: 'cmd',
|
|
129
|
-
// Wrap bannerCmd in an EXTRA pair of outer quotes so the OUTER
|
|
130
|
-
// `cmd /c`'s script parser sees the banner as a single arg
|
|
131
|
-
// (not a `&&`-chained script). The INNER `cmd /k` in the new
|
|
132
|
-
// window strips the extra outer quotes and parses the banner
|
|
133
|
-
// as a normal script. windowTitle is left unquoted: Node's
|
|
134
|
-
// spawn applies the correct Windows escaping naturally,
|
|
135
|
-
// which is more robust than pre-quoting.
|
|
136
|
-
args: ['/c', 'start', windowTitle, 'cmd', '/k', `"${bannerCmd}"`]
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
return { ok: false, unsupported: true };
|
|
140
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* In-place progress renderer for `peaks progress watch`.
|
|
3
|
-
*
|
|
4
|
-
* Goals (in order of importance):
|
|
5
|
-
* 1. **In-place overwrite.** Every tick the dynamic rows
|
|
6
|
-
* (status line + progress bar) get rewritten, NOT
|
|
7
|
-
* appended. We bypass `io.stdout` (which is
|
|
8
|
-
* `console.log` and adds a trailing `\n` per call) and
|
|
9
|
-
* write directly to `process.stdout`. The cursor-up
|
|
10
|
-
* and erase-line escapes are the same ones terminal-kit
|
|
11
|
-
* emits; we do not need a terminal-kit dependency for
|
|
12
|
-
* them.
|
|
13
|
-
* 2. **Clear PEAKS-CLI branding.** A static 3-line header
|
|
14
|
-
* with the brand bar, the project root, and the
|
|
15
|
-
* progress-file path is painted once. The user always
|
|
16
|
-
* sees what they are looking at, even after the
|
|
17
|
-
* watch loop has overwritten the dynamic rows a
|
|
18
|
-
* thousand times.
|
|
19
|
-
* 3. **Graceful degrade.** When stdout is not a TTY
|
|
20
|
-
* (CI / pipe / `--json`), we fall back to a single
|
|
21
|
-
* static snapshot per tick (no cursor moves, no
|
|
22
|
-
* SGR colour) and a single newline. This keeps the
|
|
23
|
-
* tool scriptable without dropping into a wall of
|
|
24
|
-
* escape codes.
|
|
25
|
-
*
|
|
26
|
-
* Token-cost note: this module is rendered to the user's
|
|
27
|
-
* terminal, never into LLM context. The watch side has
|
|
28
|
-
* zero token cost.
|
|
29
|
-
*/
|
|
30
|
-
import type { SubAgentProgress } from '../../services/progress/progress-service.js';
|
|
31
|
-
export type WatchRendererOptions = {
|
|
32
|
-
projectRoot: string;
|
|
33
|
-
progressFilePath: string;
|
|
34
|
-
};
|
|
35
|
-
export declare class WatchRenderer {
|
|
36
|
-
private readonly projectRoot;
|
|
37
|
-
private readonly progressFilePath;
|
|
38
|
-
private readonly width;
|
|
39
|
-
private readonly isTty;
|
|
40
|
-
private hasRenderedDynamic;
|
|
41
|
-
constructor(options: WatchRendererOptions);
|
|
42
|
-
/**
|
|
43
|
-
* Paint the static header once at the top of the watch
|
|
44
|
-
* (the PEAKS-CLI wordmark, separator, project, path). Then
|
|
45
|
-
* paint the dynamic rows for the first tick. From here on
|
|
46
|
-
* the dynamic rows are the only thing we touch.
|
|
47
|
-
*/
|
|
48
|
-
start(): void;
|
|
49
|
-
/**
|
|
50
|
-
* Repaint the 2 dynamic rows in place. First call moves
|
|
51
|
-
* the cursor up N rows from the bottom of the previously
|
|
52
|
-
* painted block; subsequent calls do the same.
|
|
53
|
-
*/
|
|
54
|
-
tick(data: SubAgentProgress | null, tickCount: number): void;
|
|
55
|
-
private paintDynamicOnce;
|
|
56
|
-
/**
|
|
57
|
-
* Paint a final 2-line verdict + a farewell line below the
|
|
58
|
-
* dashboard, then return. The cursor stays at the bottom
|
|
59
|
-
* of the farewell so the user's shell prompt lands on the
|
|
60
|
-
* next row.
|
|
61
|
-
*/
|
|
62
|
-
finalize(data: SubAgentProgress): void;
|
|
63
|
-
/**
|
|
64
|
-
* Force a non-ANSI snapshot of the current state, used by
|
|
65
|
-
* the `--once` mode and for fallback when stdout is not a
|
|
66
|
-
* TTY. Does NOT touch the cursor state — safe to call from
|
|
67
|
-
* any context.
|
|
68
|
-
*/
|
|
69
|
-
static snapshot(data: SubAgentProgress | null): {
|
|
70
|
-
status: string;
|
|
71
|
-
bar: string;
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Strip ANSI escapes from a string. Used for visible-length
|
|
76
|
-
* accounting; not for re-painting.
|
|
77
|
-
*/
|
|
78
|
-
export declare function stripAnsi(input: string): string;
|
|
79
|
-
/** Reset terminal SGR — used on early-return error paths. */
|
|
80
|
-
export declare function resetTerminal(): void;
|
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* In-place progress renderer for `peaks progress watch`.
|
|
3
|
-
*
|
|
4
|
-
* Goals (in order of importance):
|
|
5
|
-
* 1. **In-place overwrite.** Every tick the dynamic rows
|
|
6
|
-
* (status line + progress bar) get rewritten, NOT
|
|
7
|
-
* appended. We bypass `io.stdout` (which is
|
|
8
|
-
* `console.log` and adds a trailing `\n` per call) and
|
|
9
|
-
* write directly to `process.stdout`. The cursor-up
|
|
10
|
-
* and erase-line escapes are the same ones terminal-kit
|
|
11
|
-
* emits; we do not need a terminal-kit dependency for
|
|
12
|
-
* them.
|
|
13
|
-
* 2. **Clear PEAKS-CLI branding.** A static 3-line header
|
|
14
|
-
* with the brand bar, the project root, and the
|
|
15
|
-
* progress-file path is painted once. The user always
|
|
16
|
-
* sees what they are looking at, even after the
|
|
17
|
-
* watch loop has overwritten the dynamic rows a
|
|
18
|
-
* thousand times.
|
|
19
|
-
* 3. **Graceful degrade.** When stdout is not a TTY
|
|
20
|
-
* (CI / pipe / `--json`), we fall back to a single
|
|
21
|
-
* static snapshot per tick (no cursor moves, no
|
|
22
|
-
* SGR colour) and a single newline. This keeps the
|
|
23
|
-
* tool scriptable without dropping into a wall of
|
|
24
|
-
* escape codes.
|
|
25
|
-
*
|
|
26
|
-
* Token-cost note: this module is rendered to the user's
|
|
27
|
-
* terminal, never into LLM context. The watch side has
|
|
28
|
-
* zero token cost.
|
|
29
|
-
*/
|
|
30
|
-
import chalk from 'chalk';
|
|
31
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
32
|
-
// Raw byte writes. We do NOT go through `io.stdout` because the
|
|
33
|
-
// default `io.stdout` is `console.log` and appends a trailing
|
|
34
|
-
// newline, which would defeat in-place overwrite.
|
|
35
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
36
|
-
function rawWrite(text) {
|
|
37
|
-
process.stdout.write(text);
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Number of lines that the dynamic dashboard occupies. We
|
|
41
|
-
* rewrite exactly this many rows per tick via cursor-up +
|
|
42
|
-
* erase-line, so the static header above and the
|
|
43
|
-
* `press Ctrl-C` footer below stay put.
|
|
44
|
-
*/
|
|
45
|
-
const DYNAMIC_LINES = 2;
|
|
46
|
-
/** ANSI: cursor up N rows. */
|
|
47
|
-
const CURSOR_UP_N = (n) => `\x1b[${n}A`;
|
|
48
|
-
/** ANSI: erase the current row from cursor to end-of-line. */
|
|
49
|
-
const ERASE_LINE = '\x1b[2K';
|
|
50
|
-
/** ANSI: reset all SGR attributes. */
|
|
51
|
-
const RESET = '\x1b[0m';
|
|
52
|
-
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
53
|
-
const PHASE_LABEL = {
|
|
54
|
-
starting: 'starting',
|
|
55
|
-
running: 'running',
|
|
56
|
-
verifying: 'verifying',
|
|
57
|
-
completing: 'completing',
|
|
58
|
-
finished: 'finished',
|
|
59
|
-
failed: 'failed',
|
|
60
|
-
idle: 'idle'
|
|
61
|
-
};
|
|
62
|
-
const PHASE_COLOR = {
|
|
63
|
-
starting: chalk.cyan,
|
|
64
|
-
running: chalk.cyan,
|
|
65
|
-
verifying: chalk.cyan,
|
|
66
|
-
completing: chalk.cyan,
|
|
67
|
-
finished: chalk.green,
|
|
68
|
-
failed: chalk.red,
|
|
69
|
-
idle: chalk.gray
|
|
70
|
-
};
|
|
71
|
-
function pickSpinnerFrame(tick) {
|
|
72
|
-
return SPINNER_FRAMES[Math.abs(tick) % SPINNER_FRAMES.length];
|
|
73
|
-
}
|
|
74
|
-
function formatElapsed(ms) {
|
|
75
|
-
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
76
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
77
|
-
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
78
|
-
const seconds = totalSeconds % 60;
|
|
79
|
-
if (hours > 0) {
|
|
80
|
-
return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
81
|
-
}
|
|
82
|
-
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* `elapsedMs` based fake progress, 0..1, capped at 1 after
|
|
86
|
-
* 10 minutes. Visual cue that the watch is alive; NOT a real
|
|
87
|
-
* percent-complete.
|
|
88
|
-
*/
|
|
89
|
-
function computeFakeProgress(data) {
|
|
90
|
-
if (data === null)
|
|
91
|
-
return 0;
|
|
92
|
-
const startedAtMs = new Date(data.current.startedAt).getTime();
|
|
93
|
-
if (Number.isNaN(startedAtMs))
|
|
94
|
-
return 0;
|
|
95
|
-
const elapsedMs = Math.max(0, Date.now() - startedAtMs);
|
|
96
|
-
const upperBoundMs = 10 * 60 * 1000;
|
|
97
|
-
return Math.min(1, elapsedMs / upperBoundMs);
|
|
98
|
-
}
|
|
99
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
100
|
-
// ASCII art for the PEAKS-CLI brand bar. Kept in code (not a
|
|
101
|
-
// dependency) so the user sees the brand even when terminal-kit
|
|
102
|
-
// is unavailable.
|
|
103
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
104
|
-
/**
|
|
105
|
-
* Three-row ASCII wordmark for PEAKS-CLI. Each letter is 5
|
|
106
|
-
* columns wide; P-E-A-K-S take 5 letters (25 cols), the
|
|
107
|
-
* dash takes 9 cols (with surrounding space), C-L-I take 3
|
|
108
|
-
* letters (15 cols). Total ≈ 49 cols, fits in 80-col
|
|
109
|
-
* terminals without wrapping.
|
|
110
|
-
*
|
|
111
|
-
* The block-character density (full blocks on the
|
|
112
|
-
* prominent rows) makes the brand visually dominant
|
|
113
|
-
* against the gray "sub-agent progress watch" tagline
|
|
114
|
-
* that sits below — so the user always knows whose
|
|
115
|
-
* dashboard they are looking at, even when the title
|
|
116
|
-
* bar is hidden.
|
|
117
|
-
*/
|
|
118
|
-
const PEAKS_CLI_ASCII = [
|
|
119
|
-
'█████ █████ █████ ██ ██ ██████ █████ ██ ███',
|
|
120
|
-
'█ █ █ █ █ █ ██ ██ ██ ██ ██ ██ ',
|
|
121
|
-
'█████ █████ █████ ██████ ██ ████ █████ ███'
|
|
122
|
-
];
|
|
123
|
-
/**
|
|
124
|
-
* Render the static header: the three-line big PEAKS-CLI
|
|
125
|
-
* wordmark, a separator, a small "sub-agent progress watch"
|
|
126
|
-
* tagline, and the project / file path. Painted ONCE at the
|
|
127
|
-
* top of the watch, then never touched again.
|
|
128
|
-
*
|
|
129
|
-
* Visual hierarchy (per user feedback):
|
|
130
|
-
* 1. PEAKS-CLI — 3-line big block art, bold cyan
|
|
131
|
-
* 2. separator
|
|
132
|
-
* 3. sub-agent progress watch — 1 line, smaller, gray
|
|
133
|
-
* 4. project / path — 1 line each, gray
|
|
134
|
-
*/
|
|
135
|
-
function renderHeader(projectRoot, progressFilePath, isTty) {
|
|
136
|
-
if (!isTty) {
|
|
137
|
-
// Non-TTY: emit a single header line, no colour. The
|
|
138
|
-
// dashboard still emits 2 dynamic lines per tick, so
|
|
139
|
-
// consumers that need to parse the output can rely on
|
|
140
|
-
// a known line count.
|
|
141
|
-
return [
|
|
142
|
-
`PEAKS-CLI · sub-agent progress watch · project=${projectRoot}`,
|
|
143
|
-
`path: ${progressFilePath}`
|
|
144
|
-
].join('\n') + '\n';
|
|
145
|
-
}
|
|
146
|
-
// PEAKS-CLI block-art rows in bold cyan (the "big" brand).
|
|
147
|
-
const brandLines = PEAKS_CLI_ASCII.map((row) => chalk.bold.cyan(` ${row}`));
|
|
148
|
-
// The tagline below the brand — small, gray, no big
|
|
149
|
-
// chrome. The user sees the brand first, the
|
|
150
|
-
// descriptor second.
|
|
151
|
-
const tagline = chalk.gray(' sub-agent progress watch');
|
|
152
|
-
const projLine = chalk.gray(` project: ${projectRoot}`);
|
|
153
|
-
const pathLine = chalk.gray(` path: ${progressFilePath}`);
|
|
154
|
-
const separator = chalk.gray(' ' + '─'.repeat(60));
|
|
155
|
-
return [
|
|
156
|
-
brandLines[0] ?? '',
|
|
157
|
-
brandLines[1] ?? '',
|
|
158
|
-
brandLines[2] ?? '',
|
|
159
|
-
separator,
|
|
160
|
-
tagline,
|
|
161
|
-
projLine,
|
|
162
|
-
pathLine,
|
|
163
|
-
''
|
|
164
|
-
].join('\n');
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Render the 2-line dynamic dashboard. Status row on top
|
|
168
|
-
* (spinner + phase + elapsed + step), progress bar on the
|
|
169
|
-
* bottom. When `data` is null (no progress file yet), we
|
|
170
|
-
* still show a live spinner + a "waiting…" message so the
|
|
171
|
-
* user knows the watch is alive.
|
|
172
|
-
*/
|
|
173
|
-
function renderDynamicRows(data, tick, width, isTty) {
|
|
174
|
-
const progressFraction = computeFakeProgress(data);
|
|
175
|
-
if (data === null) {
|
|
176
|
-
return {
|
|
177
|
-
status: isTty
|
|
178
|
-
? ` ${chalk.gray(pickSpinnerFrame(tick))} ${chalk.gray('idle')} ${chalk.gray('(no progress file yet — sub-agent has not started)')}`
|
|
179
|
-
: `idle (no progress file yet)`,
|
|
180
|
-
bar: isTty ? renderBar(0, width, isTty) : ''
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
const startedAtMs = new Date(data.current.startedAt).getTime();
|
|
184
|
-
const elapsedMs = Number.isNaN(startedAtMs) ? 0 : Math.max(0, Date.now() - startedAtMs);
|
|
185
|
-
const phase = PHASE_LABEL[data.current.phase];
|
|
186
|
-
const step = data.current.step.length > 60 ? data.current.step.slice(0, 57) + '...' : data.current.step;
|
|
187
|
-
const verdict = data.current.verdict ? ` verdict=${data.current.verdict}` : '';
|
|
188
|
-
const role = data.role ? ` role=${data.role}` : '';
|
|
189
|
-
const spinner = pickSpinnerFrame(tick);
|
|
190
|
-
if (!isTty) {
|
|
191
|
-
return {
|
|
192
|
-
status: `${spinner} ${phase} ${formatElapsed(elapsedMs)} ${step}${role}${verdict}`,
|
|
193
|
-
bar: ''
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
const colorize = PHASE_COLOR[data.current.phase] ?? chalk.cyan;
|
|
197
|
-
const spinnerColor = phase === 'failed' ? chalk.red
|
|
198
|
-
: phase === 'finished' ? chalk.green
|
|
199
|
-
: chalk.cyan;
|
|
200
|
-
const statusLine = ` ${spinnerColor(spinner)} ${colorize(phase.padEnd(11))} ${chalk.yellow(formatElapsed(elapsedMs))} ${step}${chalk.gray(role)}${verdict ? chalk.gray(verdict) : ''}`;
|
|
201
|
-
return {
|
|
202
|
-
status: statusLine,
|
|
203
|
-
bar: renderBar(progressFraction, width, isTty)
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
function renderBar(fraction, width, isTty) {
|
|
207
|
-
if (!isTty)
|
|
208
|
-
return '';
|
|
209
|
-
// 8ths-of-cell block characters: ░ (empty) U+2591, full blocks
|
|
210
|
-
// U+2588..U+258F for the partial last cell.
|
|
211
|
-
const barCells = Math.max(10, Math.min(50, Math.floor(width * 0.4)));
|
|
212
|
-
const filled = Math.round(barCells * Math.max(0, Math.min(1, fraction)) * 8);
|
|
213
|
-
const fullBlocks = Math.floor(filled / 8);
|
|
214
|
-
const fracBlock = filled % 8;
|
|
215
|
-
const emptyCells = barCells * 8 - filled;
|
|
216
|
-
const fullStr = '█'.repeat(fullBlocks);
|
|
217
|
-
const fracStr = fracBlock > 0 ? String.fromCharCode(0x2588 + (8 - fracBlock)) : '';
|
|
218
|
-
const emptyStr = '░'.repeat(Math.floor(emptyCells / 8));
|
|
219
|
-
const percent = String(Math.round(fraction * 100)).padStart(3, ' ');
|
|
220
|
-
return ` ${chalk.green(fullStr + fracStr + emptyStr)} ${chalk.gray(`${percent}%`)}`;
|
|
221
|
-
}
|
|
222
|
-
export class WatchRenderer {
|
|
223
|
-
projectRoot;
|
|
224
|
-
progressFilePath;
|
|
225
|
-
width;
|
|
226
|
-
isTty;
|
|
227
|
-
hasRenderedDynamic = false;
|
|
228
|
-
constructor(options) {
|
|
229
|
-
this.projectRoot = options.projectRoot;
|
|
230
|
-
this.progressFilePath = options.progressFilePath;
|
|
231
|
-
this.width = (process.stdout.columns ?? 120) - 1;
|
|
232
|
-
this.isTty = process.stdout.isTTY === true;
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Paint the static header once at the top of the watch
|
|
236
|
-
* (the PEAKS-CLI wordmark, separator, project, path). Then
|
|
237
|
-
* paint the dynamic rows for the first tick. From here on
|
|
238
|
-
* the dynamic rows are the only thing we touch.
|
|
239
|
-
*/
|
|
240
|
-
start() {
|
|
241
|
-
rawWrite(renderHeader(this.projectRoot, this.progressFilePath, this.isTty));
|
|
242
|
-
this.paintDynamicOnce(null, 0);
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Repaint the 2 dynamic rows in place. First call moves
|
|
246
|
-
* the cursor up N rows from the bottom of the previously
|
|
247
|
-
* painted block; subsequent calls do the same.
|
|
248
|
-
*/
|
|
249
|
-
tick(data, tickCount) {
|
|
250
|
-
if (this.hasRenderedDynamic) {
|
|
251
|
-
// Move cursor up to the top of the previously-painted
|
|
252
|
-
// dynamic block.
|
|
253
|
-
rawWrite(CURSOR_UP_N(DYNAMIC_LINES));
|
|
254
|
-
}
|
|
255
|
-
this.paintDynamicOnce(data, tickCount);
|
|
256
|
-
}
|
|
257
|
-
paintDynamicOnce(data, tick) {
|
|
258
|
-
const { status, bar } = renderDynamicRows(data, tick, this.width, this.isTty);
|
|
259
|
-
// Erase-then-rewrite the status row.
|
|
260
|
-
rawWrite(ERASE_LINE + status + '\n');
|
|
261
|
-
// Erase-then-rewrite the bar row. (In non-TTY mode the
|
|
262
|
-
// bar is empty; we still emit a newline so the line
|
|
263
|
-
// count stays consistent.)
|
|
264
|
-
rawWrite(ERASE_LINE + bar + '\n');
|
|
265
|
-
this.hasRenderedDynamic = true;
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Paint a final 2-line verdict + a farewell line below the
|
|
269
|
-
* dashboard, then return. The cursor stays at the bottom
|
|
270
|
-
* of the farewell so the user's shell prompt lands on the
|
|
271
|
-
* next row.
|
|
272
|
-
*/
|
|
273
|
-
finalize(data) {
|
|
274
|
-
// Rewrite the dynamic block one last time so the user can
|
|
275
|
-
// read the verdict.
|
|
276
|
-
if (this.hasRenderedDynamic) {
|
|
277
|
-
rawWrite(CURSOR_UP_N(DYNAMIC_LINES));
|
|
278
|
-
}
|
|
279
|
-
this.paintDynamicOnce(data, Number.MAX_SAFE_INTEGER);
|
|
280
|
-
// Then emit the farewell BELOW the dashboard, in green.
|
|
281
|
-
const verdictSuffix = data.current.verdict !== undefined ? ` (verdict=${data.current.verdict})` : '';
|
|
282
|
-
const farewell = this.isTty
|
|
283
|
-
? chalk.green(`✔ peaks progress watch: sub-agent reached phase=${data.current.phase}${verdictSuffix} at ${new Date().toISOString()}. Auto-closing watch window.`)
|
|
284
|
-
: `peaks progress watch: sub-agent reached phase=${data.current.phase}${verdictSuffix} at ${new Date().toISOString()}. Auto-closing watch window.`;
|
|
285
|
-
rawWrite(ERASE_LINE + farewell + '\n');
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Force a non-ANSI snapshot of the current state, used by
|
|
289
|
-
* the `--once` mode and for fallback when stdout is not a
|
|
290
|
-
* TTY. Does NOT touch the cursor state — safe to call from
|
|
291
|
-
* any context.
|
|
292
|
-
*/
|
|
293
|
-
static snapshot(data) {
|
|
294
|
-
return renderDynamicRows(data, 0, 80, false);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Strip ANSI escapes from a string. Used for visible-length
|
|
299
|
-
* accounting; not for re-painting.
|
|
300
|
-
*/
|
|
301
|
-
export function stripAnsi(input) {
|
|
302
|
-
// eslint-disable-next-line no-control-regex
|
|
303
|
-
return input.replace(/\x1b\[[0-9;]*m/g, '');
|
|
304
|
-
}
|
|
305
|
-
/** Reset terminal SGR — used on early-return error paths. */
|
|
306
|
-
export function resetTerminal() {
|
|
307
|
-
rawWrite(RESET);
|
|
308
|
-
}
|