peaks-cli 1.3.4 → 1.3.6
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/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 +7 -1
- package/dist/src/cli/commands/workspace-commands.js +1 -2
- package/dist/src/cli/program.js +3 -4
- package/dist/src/services/dashboard/project-dashboard-service.d.ts +0 -7
- package/dist/src/services/dashboard/project-dashboard-service.js +1 -8
- 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 +0 -3
- package/dist/src/services/ide/adapters/trae-adapter.js +2 -17
- package/dist/src/services/ide/ide-types.d.ts +1 -18
- 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/skills/hooks-settings-service.d.ts +57 -5
- package/dist/src/services/skills/hooks-settings-service.js +153 -28
- 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/skills/peaks-prd/SKILL.md +16 -16
- package/skills/peaks-prd/references/workflow.md +4 -4
- package/skills/peaks-qa/SKILL.md +25 -32
- package/skills/peaks-qa/references/qa-fanout-contract.md +6 -6
- package/skills/peaks-qa/references/regression-gates.md +1 -1
- package/skills/peaks-rd/SKILL.md +8 -21
- package/skills/peaks-rd/references/{openspec-mcp-cli.md → openspec-cli.md} +11 -14
- package/skills/peaks-solo/SKILL.md +1 -1
- package/skills/peaks-solo/references/a2a-artifact-mapping.md +1 -1
- package/skills/peaks-solo/references/browser-workflow.md +49 -38
- package/skills/peaks-solo/references/external-skill-invocation.md +9 -7
- package/skills/peaks-solo/references/{openspec-mcp-workflow.md → openspec-workflow.md} +5 -20
- package/skills/peaks-solo/references/runbook.md +21 -21
- package/skills/peaks-solo/references/sub-agent-dispatch.md +16 -35
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +9 -9
- package/skills/peaks-ui/SKILL.md +22 -24
- package/skills/peaks-ui/references/workflow.md +2 -2
- package/dist/src/cli/commands/mcp-commands.d.ts +0 -3
- package/dist/src/cli/commands/mcp-commands.js +0 -144
- 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
- package/dist/src/services/mcp/mcp-apply-service.d.ts +0 -31
- package/dist/src/services/mcp/mcp-apply-service.js +0 -112
- package/dist/src/services/mcp/mcp-call-service.d.ts +0 -17
- package/dist/src/services/mcp/mcp-call-service.js +0 -34
- package/dist/src/services/mcp/mcp-client-service.d.ts +0 -14
- package/dist/src/services/mcp/mcp-client-service.js +0 -49
- package/dist/src/services/mcp/mcp-install-registry.d.ts +0 -11
- package/dist/src/services/mcp/mcp-install-registry.js +0 -38
- package/dist/src/services/mcp/mcp-plan-service.d.ts +0 -29
- package/dist/src/services/mcp/mcp-plan-service.js +0 -109
- package/dist/src/services/mcp/mcp-protocol.d.ts +0 -24
- package/dist/src/services/mcp/mcp-protocol.js +0 -41
- package/dist/src/services/mcp/mcp-scan-service.d.ts +0 -8
- package/dist/src/services/mcp/mcp-scan-service.js +0 -214
- package/dist/src/services/mcp/mcp-stdio-transport.d.ts +0 -10
- package/dist/src/services/mcp/mcp-stdio-transport.js +0 -50
- package/dist/src/services/mcp/mcp-types.d.ts +0 -31
- package/dist/src/services/mcp/mcp-types.js +0 -1
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { platform } from 'node:os';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import ora from 'ora';
|
|
5
|
-
import { clearSpawnRecord, isRecentSpawn, phaseAutoClosesSpawn, readSpawnRecord, readSubAgentProgress, resolveProgressProjectRoot, subAgentProgressPath, subAgentSpawnPath, writeSpawnRecord, writeSubAgentProgress } from '../../services/progress/progress-service.js';
|
|
6
|
-
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
7
|
-
import { fail, ok } from '../../shared/result.js';
|
|
8
|
-
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
9
|
-
import { killSpawnedTerminal } from './progress-close-kill.js';
|
|
10
|
-
import { buildStartSpawn } from './progress-start-spawn.js';
|
|
11
|
-
import { WatchRenderer } from './progress-watch-render.js';
|
|
12
|
-
export function registerProgressCommands(program, io) {
|
|
13
|
-
const progress = program.command('progress').description('Sub-agent progress surfacing (LLM-side step writes, user-side watch, auto-spawn new terminal)');
|
|
14
|
-
// ─────────────────────────────────────────────────────────────────
|
|
15
|
-
// peaks progress step
|
|
16
|
-
// LLM-side: called by the LLM on phase transitions. Near-zero
|
|
17
|
-
// token cost — one Bash call per phase change. Writes
|
|
18
|
-
// `.peaks/_sub_agents/<sid>/subagent-progress.json`. No auto-spawn
|
|
19
|
-
// here; the LLM invokes `peaks progress start` separately when
|
|
20
|
-
// the user-visible window needs to open.
|
|
21
|
-
// ─────────────────────────────────────────────────────────────────
|
|
22
|
-
addJsonOption(progress
|
|
23
|
-
.command('step')
|
|
24
|
-
.description('Record a sub-agent step / phase transition. Idempotent on (step, phase); transitions append to history.')
|
|
25
|
-
.requiredOption('--request-id <rid>', 'the same <rid> used by peaks request init')
|
|
26
|
-
.requiredOption('--role <role>', 'rd | qa | ui | sc | prd (the role of the sub-agent calling this)')
|
|
27
|
-
.requiredOption('--step <text>', 'free-form human-readable step label, e.g. "running test/ut"')
|
|
28
|
-
.requiredOption('--phase <phase>', 'starting | running | verifying | completing | finished | failed | idle')
|
|
29
|
-
.option('--verdict <verdict>', 'pass | return-to-rd | blocked (only when phase is finished or failed)')
|
|
30
|
-
.option('--project <path>', 'target project root (defaults to git root or cwd)')
|
|
31
|
-
.option('--reason <text>', 'human-readable reason for the step write, recorded in the response data')).action((options) => {
|
|
32
|
-
try {
|
|
33
|
-
const projectRoot = options.project !== undefined
|
|
34
|
-
? options.project
|
|
35
|
-
: resolveProgressProjectRoot(undefined, process.cwd());
|
|
36
|
-
const canonical = resolveCanonicalProjectRoot(projectRoot);
|
|
37
|
-
const data = writeSubAgentProgress({
|
|
38
|
-
projectRoot: canonical,
|
|
39
|
-
requestId: options.requestId,
|
|
40
|
-
role: options.role,
|
|
41
|
-
step: options.step,
|
|
42
|
-
phase: options.phase,
|
|
43
|
-
...(options.verdict !== undefined ? { verdict: options.verdict } : {}),
|
|
44
|
-
...(options.reason !== undefined ? { outerSessionId: options.reason } : {})
|
|
45
|
-
});
|
|
46
|
-
printResult(io, ok('progress.step', {
|
|
47
|
-
projectRoot: canonical,
|
|
48
|
-
path: subAgentProgressPath(canonical),
|
|
49
|
-
sessionId: data.sessionId,
|
|
50
|
-
requestId: data.requestId,
|
|
51
|
-
role: data.role,
|
|
52
|
-
phase: data.current.phase,
|
|
53
|
-
step: data.current.step,
|
|
54
|
-
startedAt: data.current.startedAt,
|
|
55
|
-
updatedAt: data.current.updatedAt
|
|
56
|
-
}), options.json);
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
printResult(io, fail('progress.step', 'PROGRESS_STEP_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Verify the project path exists and that peaks workspace init has been run for it']), options.json);
|
|
60
|
-
process.exitCode = 1;
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
// ─────────────────────────────────────────────────────────────────
|
|
64
|
-
// peaks progress watch
|
|
65
|
-
// User-side: run in a separate terminal. 1s poll + ASCII
|
|
66
|
-
// spinner + elapsed. --once for a single snapshot.
|
|
67
|
-
// ─────────────────────────────────────────────────────────────────
|
|
68
|
-
addJsonOption(progress
|
|
69
|
-
.command('watch')
|
|
70
|
-
.description('Watch the sub-agent progress file in a loop (1s poll + ASCII spinner). --once for a single snapshot.')
|
|
71
|
-
.option('--project <path>', 'target project root (defaults to git root or cwd)')
|
|
72
|
-
.option('--once', 'print a single snapshot and exit (for use in scripts or statusline hooks)', false)
|
|
73
|
-
.option('--interval-ms <ms>', 'poll interval in milliseconds (default 1000)', (value) => Number.parseInt(value, 10))).action(async (options) => {
|
|
74
|
-
try {
|
|
75
|
-
const projectRoot = options.project !== undefined
|
|
76
|
-
? options.project
|
|
77
|
-
: resolveProgressProjectRoot(undefined, process.cwd());
|
|
78
|
-
const canonical = resolveCanonicalProjectRoot(projectRoot);
|
|
79
|
-
const intervalMs = options.intervalMs !== undefined && Number.isFinite(options.intervalMs) && options.intervalMs > 0
|
|
80
|
-
? options.intervalMs
|
|
81
|
-
: 1000;
|
|
82
|
-
if (options.once === true) {
|
|
83
|
-
const result = readSubAgentProgress({ projectRoot: canonical });
|
|
84
|
-
if (!result.ok) {
|
|
85
|
-
printResult(io, fail('progress.watch', 'NO_PROGRESS_DATA', `No progress file present yet (${result.reason})`, { projectRoot: canonical, path: subAgentProgressPath(canonical) }, ['Run peaks progress step once on the LLM side to bootstrap the file']), options.json);
|
|
86
|
-
process.exitCode = 1;
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
printResult(io, ok('progress.watch.snapshot', {
|
|
90
|
-
projectRoot: canonical,
|
|
91
|
-
path: result.path,
|
|
92
|
-
data: result.data
|
|
93
|
-
}), options.json);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
// Long-running watch loop. The render layer lives in
|
|
97
|
-
// ./progress-watch-render.ts; the watch loop just polls
|
|
98
|
-
// the file and calls renderer.tick(data, n). The renderer
|
|
99
|
-
// owns cursor positioning and in-place overwrite so the
|
|
100
|
-
// output does not grow line by line as it did in the
|
|
101
|
-
// previous console.log-based implementation.
|
|
102
|
-
const renderer = new WatchRenderer({
|
|
103
|
-
projectRoot: canonical,
|
|
104
|
-
progressFilePath: subAgentProgressPath(canonical)
|
|
105
|
-
});
|
|
106
|
-
renderer.start();
|
|
107
|
-
// Hint line is painted once BELOW the dynamic block, so
|
|
108
|
-
// it is not erased on each tick. The user sees the
|
|
109
|
-
// spinner + bar above, the static hint at the bottom.
|
|
110
|
-
io.stdout(chalk.gray(' press Ctrl-C to stop watching\n'));
|
|
111
|
-
let tick = 0;
|
|
112
|
-
// eslint-disable-next-line no-constant-condition
|
|
113
|
-
while (true) {
|
|
114
|
-
const result = readSubAgentProgress({ projectRoot: canonical });
|
|
115
|
-
const data = result.ok ? result.data : null;
|
|
116
|
-
renderer.tick(data, tick);
|
|
117
|
-
tick += 1;
|
|
118
|
-
// Auto-close: when the sub-agent reaches a terminal
|
|
119
|
-
// phase (finished or failed per phaseAutoClosesSpawn),
|
|
120
|
-
// paint the final frame so the user can read the
|
|
121
|
-
// verdict, then exit. Exiting the watch process makes
|
|
122
|
-
// most terminal emulators close the window
|
|
123
|
-
// (Terminal.app / gnome-terminal / konsole all do;
|
|
124
|
-
// alacritty / kitty keep it). We also clear the
|
|
125
|
-
// spawn record so a subsequent `peaks progress close`
|
|
126
|
-
// reports "nothing to close" instead of "closed a
|
|
127
|
-
// ghost record".
|
|
128
|
-
//
|
|
129
|
-
// We deliberately do NOT auto-close on `blocked` —
|
|
130
|
-
// `blocked` means the user needs to read the watch
|
|
131
|
-
// output and decide what to do.
|
|
132
|
-
if (data !== null && phaseAutoClosesSpawn(data.current.phase)) {
|
|
133
|
-
renderer.finalize(data);
|
|
134
|
-
clearSpawnRecord(canonical);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
await new Promise((resolveWait) => setTimeout(resolveWait, intervalMs));
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
printResult(io, fail('progress.watch', 'PROGRESS_WATCH_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Verify the project path exists and that the progress file is readable']), options.json);
|
|
142
|
-
process.exitCode = 1;
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
// ─────────────────────────────────────────────────────────────────
|
|
146
|
-
// peaks progress start
|
|
147
|
-
// The "auto-spawn a new terminal running watch" entry point.
|
|
148
|
-
// Called by the LLM at the first phase transition of a slice,
|
|
149
|
-
// once per session. Cross-platform: macOS uses osascript with
|
|
150
|
-
// Terminal.app; Linux tries gnome-terminal / konsole /
|
|
151
|
-
// xterm in order; Windows uses `start cmd`. The user can
|
|
152
|
-
// close the new terminal at any time; re-invoking is a no-op
|
|
153
|
-
// if a watch is already running in another terminal.
|
|
154
|
-
// ─────────────────────────────────────────────────────────────────
|
|
155
|
-
addJsonOption(progress
|
|
156
|
-
.command('start')
|
|
157
|
-
.description('Auto-spawn a new terminal running `peaks progress watch` for this project. Called by the LLM at the first phase transition; the user can close the new terminal at any time.')
|
|
158
|
-
.option('--project <path>', 'target project root (defaults to git root or cwd)')
|
|
159
|
-
.option('--reason <text>', 'human-readable reason for the auto-spawn, recorded in the response data')
|
|
160
|
-
.option('--quiet', 'suppress human-readable output (the Task-tool PreToolUse hook uses this to keep the LLM context clean)')).action(async (options) => {
|
|
161
|
-
try {
|
|
162
|
-
const projectRoot = options.project !== undefined
|
|
163
|
-
? options.project
|
|
164
|
-
: resolveProgressProjectRoot(undefined, process.cwd());
|
|
165
|
-
const canonical = resolveCanonicalProjectRoot(projectRoot);
|
|
166
|
-
// Idempotency check: when the Task-tool PreToolUse hook fires
|
|
167
|
-
// `peaks progress start` on every Task call, a fresh terminal
|
|
168
|
-
// should NOT be spawned if a watch window was already opened for
|
|
169
|
-
// this session within the last 5 minutes. The user closes the
|
|
170
|
-
// window deliberately; we honor that until the record ages out.
|
|
171
|
-
const recent = isRecentSpawn(canonical);
|
|
172
|
-
if (recent.recent) {
|
|
173
|
-
if (options.quiet !== true) {
|
|
174
|
-
// Non-hook path: keep the human feedback (so an LLM running
|
|
175
|
-
// peaks progress start manually understands the no-op).
|
|
176
|
-
}
|
|
177
|
-
printResult(io, ok('progress.start', {
|
|
178
|
-
projectRoot: canonical,
|
|
179
|
-
spawned: false,
|
|
180
|
-
idempotent: true,
|
|
181
|
-
reason: recent.reason,
|
|
182
|
-
ageMs: recent.ageMs,
|
|
183
|
-
note: 'a recent spawn record exists; the watch window is presumed open. Re-run after 5 min (TTL) or `peaks progress close` to force a fresh spawn.'
|
|
184
|
-
}, [], recent.reason === 'recent-spawn'
|
|
185
|
-
? []
|
|
186
|
-
: ['run `peaks progress close` to clear the stale record and force a fresh spawn']), options.json);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
const currentPlatform = platform();
|
|
190
|
-
const peaksBin = process.argv[1] ?? 'peaks';
|
|
191
|
-
const reasonSuffix = options.reason !== undefined ? ` — ${options.reason}` : '';
|
|
192
|
-
// Window / tab title shared across platforms. The user
|
|
193
|
-
// asked for visible "this is peaks-cli" branding so the
|
|
194
|
-
// spawned terminal is identifiable at a glance; the title
|
|
195
|
-
// also makes `peaks progress close` self-documenting.
|
|
196
|
-
//
|
|
197
|
-
// Em-dash (U+2014) instead of a colon. On Windows, cmd /c's
|
|
198
|
-
// script parser interprets a `:` even inside quotes as a
|
|
199
|
-
// drive-letter prefix, so `peaks-cli: sub-agent progress`
|
|
200
|
-
// triggers the "Windows 找不到文件 'sub-agent'" dialog. The
|
|
201
|
-
// em-dash is a no-op for cmd / c, bash, and AppleScript
|
|
202
|
-
// string parsing — the visible branding is preserved.
|
|
203
|
-
const windowTitle = `peaks-cli — sub-agent progress${reasonSuffix}`;
|
|
204
|
-
const watchCommand = `${peaksBin} progress watch --project "${canonical}"`;
|
|
205
|
-
// Build the platform-specific spawn command + args. This
|
|
206
|
-
// is extracted to ./progress-start-spawn.ts so the three
|
|
207
|
-
// platform branches can be unit-tested without spawning
|
|
208
|
-
// a real terminal.
|
|
209
|
-
const spawnSpec = buildStartSpawn({
|
|
210
|
-
peaksBin,
|
|
211
|
-
projectRoot: canonical,
|
|
212
|
-
windowTitle,
|
|
213
|
-
platform: currentPlatform
|
|
214
|
-
});
|
|
215
|
-
if (!spawnSpec.ok) {
|
|
216
|
-
printResult(io, fail('progress.start', 'UNSUPPORTED_PLATFORM', `Cannot auto-spawn a terminal on platform "${currentPlatform}". Run \`peaks progress watch --project "${canonical}"\` in a new terminal yourself.`, { projectRoot: canonical }, ['macOS / Linux / Windows are supported; other platforms need a manual terminal']), options.json);
|
|
217
|
-
process.exitCode = 1;
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
const spawnCommand = spawnSpec.command;
|
|
221
|
-
const spawnArgs = spawnSpec.args;
|
|
222
|
-
// Brief ora feedback while the terminal is launching.
|
|
223
|
-
// Skipped entirely in non-TTY mode (the LLM calls this
|
|
224
|
-
// from a Bash tool, where ora would just hang on
|
|
225
|
-
// animation) and in --json mode (where the structured
|
|
226
|
-
// response is the only signal the caller consumes).
|
|
227
|
-
const showSpinner = process.stdout.isTTY === true && options.json !== true;
|
|
228
|
-
const spinner = showSpinner
|
|
229
|
-
? ora(`auto-spawning ${spawnCommand}…`).start()
|
|
230
|
-
: null;
|
|
231
|
-
try {
|
|
232
|
-
// spawn() with detached:true + unref() is the documented
|
|
233
|
-
// Node.js way to start a long-lived child from a CLI
|
|
234
|
-
// without blocking. We ignore stdio because the spawned
|
|
235
|
-
// terminal owns the child process group from now on; the
|
|
236
|
-
// peaks CLI exits and the terminal keeps running.
|
|
237
|
-
const child = spawn(spawnCommand, spawnArgs, { detached: true, stdio: 'ignore' });
|
|
238
|
-
child.unref();
|
|
239
|
-
// Give the spawn a beat to surface EACCES/ENOENT. We do
|
|
240
|
-
// not await the child (it is intentionally long-lived).
|
|
241
|
-
await new Promise((resolveSpawn, rejectSpawn) => {
|
|
242
|
-
const timer = setTimeout(() => resolveSpawn(), 200);
|
|
243
|
-
child.once('error', (spawnError) => {
|
|
244
|
-
clearTimeout(timer);
|
|
245
|
-
rejectSpawn(spawnError);
|
|
246
|
-
});
|
|
247
|
-
child.once('spawn', () => {
|
|
248
|
-
clearTimeout(timer);
|
|
249
|
-
resolveSpawn();
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
if (spinner !== null) {
|
|
253
|
-
spinner.succeed(`spawned ${spawnCommand} (new window is opening)`);
|
|
254
|
-
}
|
|
255
|
-
// Persist the spawn record so `peaks progress close` (and
|
|
256
|
-
// the watch-side auto-exit) can find and kill the window
|
|
257
|
-
// later. We write the record AFTER the spawn fires so a
|
|
258
|
-
// failed spawn never leaves a stale record behind. The
|
|
259
|
-
// record is per-session: a session rotation invalidates it
|
|
260
|
-
// because the new session gets a fresh record path.
|
|
261
|
-
const spawnRecord = writeSpawnRecord({
|
|
262
|
-
projectRoot: canonical,
|
|
263
|
-
pid: child.pid ?? 0,
|
|
264
|
-
platform: currentPlatform,
|
|
265
|
-
command: spawnCommand,
|
|
266
|
-
args: spawnArgs,
|
|
267
|
-
...(options.reason !== undefined ? { reason: options.reason } : {}),
|
|
268
|
-
windowTitle
|
|
269
|
-
});
|
|
270
|
-
printResult(io, ok('progress.start', {
|
|
271
|
-
projectRoot: canonical,
|
|
272
|
-
platform: currentPlatform,
|
|
273
|
-
spawned: `${spawnCommand} ${spawnArgs.join(' ')}`,
|
|
274
|
-
watchCommand,
|
|
275
|
-
...(options.reason !== undefined ? { reason: options.reason } : {}),
|
|
276
|
-
...(spawnRecord === null
|
|
277
|
-
? {
|
|
278
|
-
spawnRecord: null,
|
|
279
|
-
warning: 'no peaks session binding — `peaks progress close` will not be able to find this window. Close it manually.'
|
|
280
|
-
}
|
|
281
|
-
: {
|
|
282
|
-
spawnRecord: {
|
|
283
|
-
path: subAgentSpawnPath(canonical),
|
|
284
|
-
windowTitle: spawnRecord.windowTitle,
|
|
285
|
-
spawnedAt: spawnRecord.spawnedAt
|
|
286
|
-
}
|
|
287
|
-
}),
|
|
288
|
-
autoClose: 'the watch window will close itself when the sub-agent hits `finished` or `failed`',
|
|
289
|
-
note: 'A new terminal window is opening in the background. It will run `peaks progress watch` and refresh every second. Close the new terminal at any time, or run `peaks progress close` to programmatically close it.'
|
|
290
|
-
}), options.json);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
catch (spawnError) {
|
|
294
|
-
if (spinner !== null) {
|
|
295
|
-
spinner.fail(`auto-spawn failed: ${getErrorMessage(spawnError)}`);
|
|
296
|
-
}
|
|
297
|
-
printResult(io, fail('progress.start', 'TERMINAL_SPAWN_FAILED', `Auto-spawn failed: ${getErrorMessage(spawnError)}. Run \`peaks progress watch --project "${canonical}"\` in a new terminal yourself.`, { projectRoot: canonical, platform: currentPlatform, attempted: `${spawnCommand} ${spawnArgs.join(' ')}` }, ['Verify a terminal emulator is installed (e.g. gnome-terminal / Terminal.app)']), options.json);
|
|
298
|
-
process.exitCode = 1;
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
catch (error) {
|
|
303
|
-
printResult(io, fail('progress.start', 'PROGRESS_START_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Verify the project path exists and a terminal emulator is available']), options.json);
|
|
304
|
-
process.exitCode = 1;
|
|
305
|
-
}
|
|
306
|
-
});
|
|
307
|
-
// ─────────────────────────────────────────────────────────────────
|
|
308
|
-
// peaks progress close
|
|
309
|
-
// Manual escape hatch: kill the spawned watch window and
|
|
310
|
-
// clear the spawn record. Idempotent — re-running is a no-op
|
|
311
|
-
// once the record is gone, and the response distinguishes
|
|
312
|
-
// "nothing to close" from "closed it" so callers / hooks can
|
|
313
|
-
// tell the difference. The close is best-effort: if the
|
|
314
|
-
// watch process has already exited but the record is stale,
|
|
315
|
-
// we still clear the record.
|
|
316
|
-
// ─────────────────────────────────────────────────────────────────
|
|
317
|
-
addJsonOption(progress
|
|
318
|
-
.command('close')
|
|
319
|
-
.description('Close the spawned `peaks progress watch` window for this session. Idempotent: re-running when no window is open is a no-op.')
|
|
320
|
-
.option('--project <path>', 'target project root (defaults to git root or cwd)')).action(async (options) => {
|
|
321
|
-
try {
|
|
322
|
-
const projectRoot = options.project !== undefined
|
|
323
|
-
? options.project
|
|
324
|
-
: resolveProgressProjectRoot(undefined, process.cwd());
|
|
325
|
-
const canonical = resolveCanonicalProjectRoot(projectRoot);
|
|
326
|
-
const result = readSpawnRecord(canonical);
|
|
327
|
-
if (!result.ok) {
|
|
328
|
-
// Differentiate the failure modes so callers can decide
|
|
329
|
-
// whether to surface a warning. no-binding means peaks
|
|
330
|
-
// workspace init has not been run; no-spawn-record /
|
|
331
|
-
// invalid-json means there is nothing to close (start
|
|
332
|
-
// has not been called this session, or the window
|
|
333
|
-
// already auto-closed and the record was cleared).
|
|
334
|
-
if (result.reason === 'no-binding') {
|
|
335
|
-
printResult(io, fail('progress.close', 'NO_BINDING', 'no peaks session binding — nothing to close', { projectRoot: canonical, path: subAgentSpawnPath(canonical) }, ['Run peaks workspace init for this project first']), options.json);
|
|
336
|
-
process.exitCode = 1;
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
printResult(io, ok('progress.close', {
|
|
340
|
-
projectRoot: canonical,
|
|
341
|
-
closed: false,
|
|
342
|
-
reason: result.reason,
|
|
343
|
-
note: 'no spawn record found — nothing to close (start has not been called this session, or the window has already auto-closed)'
|
|
344
|
-
}), options.json);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
const record = result.data;
|
|
348
|
-
// Best-effort close. We try three signals in order:
|
|
349
|
-
// (1) `pkill -f <watch command>` — the long-lived watch
|
|
350
|
-
// process. Killing it makes the terminal emulator
|
|
351
|
-
// close on most platforms (Terminal.app, gnome-
|
|
352
|
-
// terminal, konsole) but not all (alacritty, kitty
|
|
353
|
-
// keep the window).
|
|
354
|
-
// (2) macOS: AppleScript to close the Terminal.app
|
|
355
|
-
// window with the matching custom title.
|
|
356
|
-
// (3) Linux: wmctrl/xdotool by WM class as a fallback.
|
|
357
|
-
// Windows: taskkill /F /FI on the window title.
|
|
358
|
-
// We never throw from the close path — a failed close is
|
|
359
|
-
// a UX paper cut, not a correctness bug. The record is
|
|
360
|
-
// still cleared so the next `progress start` does not
|
|
361
|
-
// see a stale record.
|
|
362
|
-
const closeResult = await killSpawnedTerminal(record, canonical, platform());
|
|
363
|
-
clearSpawnRecord(canonical);
|
|
364
|
-
printResult(io, ok('progress.close', {
|
|
365
|
-
projectRoot: canonical,
|
|
366
|
-
closed: closeResult.signals.length > 0,
|
|
367
|
-
signals: closeResult.signals,
|
|
368
|
-
warnings: closeResult.warnings,
|
|
369
|
-
windowTitle: record.windowTitle,
|
|
370
|
-
spawnedAt: record.spawnedAt,
|
|
371
|
-
note: 'spawn record cleared. The next `peaks progress start` will spawn a fresh window.'
|
|
372
|
-
}), options.json);
|
|
373
|
-
}
|
|
374
|
-
catch (error) {
|
|
375
|
-
printResult(io, fail('progress.close', 'PROGRESS_CLOSE_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Verify the project path exists and that peaks workspace init has been run for it']), options.json);
|
|
376
|
-
process.exitCode = 1;
|
|
377
|
-
}
|
|
378
|
-
});
|
|
379
|
-
}
|
|
@@ -1,59 +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
|
-
export type StartSpawnSpec = {
|
|
36
|
-
ok: true;
|
|
37
|
-
command: string;
|
|
38
|
-
args: string[];
|
|
39
|
-
} | {
|
|
40
|
-
ok: false;
|
|
41
|
-
unsupported: true;
|
|
42
|
-
};
|
|
43
|
-
export type BuildStartSpawnOptions = {
|
|
44
|
-
/** The peaks binary path the spawned shell will invoke. */
|
|
45
|
-
peaksBin: string;
|
|
46
|
-
/** Canonical project root (used to build the watch command). */
|
|
47
|
-
projectRoot: string;
|
|
48
|
-
/** Window/tab title shared across platforms. */
|
|
49
|
-
windowTitle: string;
|
|
50
|
-
/** Current platform (from `os.platform()`). */
|
|
51
|
-
platform: NodeJS.Platform;
|
|
52
|
-
/**
|
|
53
|
-
* Override for terminal detection on Linux. Defaults to
|
|
54
|
-
* `existsSync('/usr/bin/<name>')` for each candidate. Tests
|
|
55
|
-
* pass a stub to make the linux branch deterministic.
|
|
56
|
-
*/
|
|
57
|
-
linuxTerminalExists?: (name: string) => boolean;
|
|
58
|
-
};
|
|
59
|
-
export declare function buildStartSpawn(options: BuildStartSpawnOptions): StartSpawnSpec;
|
|
@@ -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;
|