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.
Files changed (69) hide show
  1. package/dist/src/cli/commands/hook-handle.d.ts +2 -2
  2. package/dist/src/cli/commands/hook-handle.js +5 -10
  3. package/dist/src/cli/commands/hooks-commands.js +44 -29
  4. package/dist/src/cli/commands/project-commands.js +7 -1
  5. package/dist/src/cli/commands/workspace-commands.js +1 -2
  6. package/dist/src/cli/program.js +3 -4
  7. package/dist/src/services/dashboard/project-dashboard-service.d.ts +0 -7
  8. package/dist/src/services/dashboard/project-dashboard-service.js +1 -8
  9. package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +45 -40
  10. package/dist/src/services/dispatch/sub-agent-dispatcher.js +25 -20
  11. package/dist/src/services/ide/adapters/claude-code-adapter.js +0 -3
  12. package/dist/src/services/ide/adapters/trae-adapter.js +2 -17
  13. package/dist/src/services/ide/ide-types.d.ts +1 -18
  14. package/dist/src/services/progress/progress-service.d.ts +23 -103
  15. package/dist/src/services/progress/progress-service.js +24 -137
  16. package/dist/src/services/scan/file-size-scan.d.ts +4 -0
  17. package/dist/src/services/scan/file-size-scan.js +32 -3
  18. package/dist/src/services/skills/hooks-settings-service.d.ts +57 -5
  19. package/dist/src/services/skills/hooks-settings-service.js +153 -28
  20. package/dist/src/shared/incrementing-number.d.ts +0 -8
  21. package/dist/src/shared/incrementing-number.js +11 -1
  22. package/dist/src/shared/version.d.ts +1 -1
  23. package/dist/src/shared/version.js +1 -1
  24. package/package.json +1 -1
  25. package/skills/peaks-prd/SKILL.md +16 -16
  26. package/skills/peaks-prd/references/workflow.md +4 -4
  27. package/skills/peaks-qa/SKILL.md +25 -32
  28. package/skills/peaks-qa/references/qa-fanout-contract.md +6 -6
  29. package/skills/peaks-qa/references/regression-gates.md +1 -1
  30. package/skills/peaks-rd/SKILL.md +8 -21
  31. package/skills/peaks-rd/references/{openspec-mcp-cli.md → openspec-cli.md} +11 -14
  32. package/skills/peaks-solo/SKILL.md +1 -1
  33. package/skills/peaks-solo/references/a2a-artifact-mapping.md +1 -1
  34. package/skills/peaks-solo/references/browser-workflow.md +49 -38
  35. package/skills/peaks-solo/references/external-skill-invocation.md +9 -7
  36. package/skills/peaks-solo/references/{openspec-mcp-workflow.md → openspec-workflow.md} +5 -20
  37. package/skills/peaks-solo/references/runbook.md +21 -21
  38. package/skills/peaks-solo/references/sub-agent-dispatch.md +16 -35
  39. package/skills/peaks-solo/references/swarm-dispatch-contract.md +9 -9
  40. package/skills/peaks-ui/SKILL.md +22 -24
  41. package/skills/peaks-ui/references/workflow.md +2 -2
  42. package/dist/src/cli/commands/mcp-commands.d.ts +0 -3
  43. package/dist/src/cli/commands/mcp-commands.js +0 -144
  44. package/dist/src/cli/commands/progress-close-kill.d.ts +0 -51
  45. package/dist/src/cli/commands/progress-close-kill.js +0 -152
  46. package/dist/src/cli/commands/progress-commands.d.ts +0 -3
  47. package/dist/src/cli/commands/progress-commands.js +0 -379
  48. package/dist/src/cli/commands/progress-start-spawn.d.ts +0 -59
  49. package/dist/src/cli/commands/progress-start-spawn.js +0 -140
  50. package/dist/src/cli/commands/progress-watch-render.d.ts +0 -80
  51. package/dist/src/cli/commands/progress-watch-render.js +0 -308
  52. package/dist/src/services/mcp/mcp-apply-service.d.ts +0 -31
  53. package/dist/src/services/mcp/mcp-apply-service.js +0 -112
  54. package/dist/src/services/mcp/mcp-call-service.d.ts +0 -17
  55. package/dist/src/services/mcp/mcp-call-service.js +0 -34
  56. package/dist/src/services/mcp/mcp-client-service.d.ts +0 -14
  57. package/dist/src/services/mcp/mcp-client-service.js +0 -49
  58. package/dist/src/services/mcp/mcp-install-registry.d.ts +0 -11
  59. package/dist/src/services/mcp/mcp-install-registry.js +0 -38
  60. package/dist/src/services/mcp/mcp-plan-service.d.ts +0 -29
  61. package/dist/src/services/mcp/mcp-plan-service.js +0 -109
  62. package/dist/src/services/mcp/mcp-protocol.d.ts +0 -24
  63. package/dist/src/services/mcp/mcp-protocol.js +0 -41
  64. package/dist/src/services/mcp/mcp-scan-service.d.ts +0 -8
  65. package/dist/src/services/mcp/mcp-scan-service.js +0 -214
  66. package/dist/src/services/mcp/mcp-stdio-transport.d.ts +0 -10
  67. package/dist/src/services/mcp/mcp-stdio-transport.js +0 -50
  68. package/dist/src/services/mcp/mcp-types.d.ts +0 -31
  69. 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;