@promptctl/cc-candybar 1.42.1 → 1.43.0

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 (126) hide show
  1. package/dist/index.mjs +72 -71
  2. package/package.json +5 -6
  3. package/src/check.ts +0 -478
  4. package/src/cli-flags.ts +0 -8
  5. package/src/click/wire.ts +0 -158
  6. package/src/config/action.ts +0 -329
  7. package/src/config/cli.ts +0 -71
  8. package/src/config/default-dsl-config.ts +0 -1645
  9. package/src/config/disclosure.ts +0 -170
  10. package/src/config/dsl-loader.ts +0 -339
  11. package/src/config/dsl-types.ts +0 -581
  12. package/src/config/edit-chrome.ts +0 -559
  13. package/src/config/help.ts +0 -151
  14. package/src/config/ident.ts +0 -22
  15. package/src/config/layout-ops.ts +0 -177
  16. package/src/config/loader/actions.ts +0 -972
  17. package/src/config/loader/cache.ts +0 -206
  18. package/src/config/loader/cross-ref.ts +0 -714
  19. package/src/config/loader/cycles.ts +0 -148
  20. package/src/config/loader/diagnostics.ts +0 -99
  21. package/src/config/loader/discovery.ts +0 -182
  22. package/src/config/loader/edit-mode.ts +0 -137
  23. package/src/config/loader/emit-schema.ts +0 -68
  24. package/src/config/loader/globals.ts +0 -269
  25. package/src/config/loader/helpers.ts +0 -48
  26. package/src/config/loader/layout.ts +0 -693
  27. package/src/config/loader/looks.ts +0 -96
  28. package/src/config/loader/menu-synth.ts +0 -435
  29. package/src/config/loader/merge.ts +0 -115
  30. package/src/config/loader/persist-target.ts +0 -67
  31. package/src/config/loader/presets.ts +0 -119
  32. package/src/config/loader/refs.ts +0 -100
  33. package/src/config/loader/reserved-namespace.ts +0 -38
  34. package/src/config/loader/segments.ts +0 -120
  35. package/src/config/loader/validate-core.ts +0 -737
  36. package/src/config/loader/variables.ts +0 -260
  37. package/src/config/menu-keys.ts +0 -139
  38. package/src/config/option-domain.ts +0 -164
  39. package/src/config/presets.ts +0 -326
  40. package/src/config/settings-menu.ts +0 -775
  41. package/src/daemon/acquire.ts +0 -684
  42. package/src/daemon/cache/git.ts +0 -649
  43. package/src/daemon/cache/render.ts +0 -623
  44. package/src/daemon/cache/session-usage-store.ts +0 -720
  45. package/src/daemon/cache/watchers.ts +0 -249
  46. package/src/daemon/client-debug.ts +0 -120
  47. package/src/daemon/client-stats.ts +0 -130
  48. package/src/daemon/client-transport.ts +0 -273
  49. package/src/daemon/client.ts +0 -78
  50. package/src/daemon/config-overrides-store.ts +0 -663
  51. package/src/daemon/debug-types.ts +0 -91
  52. package/src/daemon/debug.ts +0 -264
  53. package/src/daemon/fork-bomb-breaker.ts +0 -351
  54. package/src/daemon/limits.ts +0 -211
  55. package/src/daemon/log.ts +0 -81
  56. package/src/daemon/parent-watchdog.ts +0 -87
  57. package/src/daemon/paths.ts +0 -211
  58. package/src/daemon/process-fingerprint.ts +0 -146
  59. package/src/daemon/protocol.ts +0 -292
  60. package/src/daemon/render-payload.ts +0 -1256
  61. package/src/daemon/server.ts +0 -1330
  62. package/src/daemon/session-state-file.ts +0 -108
  63. package/src/daemon/session-state.ts +0 -237
  64. package/src/daemon/socket-lease.ts +0 -209
  65. package/src/daemon/socket-ownership.ts +0 -209
  66. package/src/daemon/stats.ts +0 -235
  67. package/src/daemon/verbs/config-validators.ts +0 -250
  68. package/src/daemon/verbs/index.ts +0 -706
  69. package/src/daemon/verbs/state-validators.ts +0 -249
  70. package/src/daemon/verbs/validator-registry.ts +0 -457
  71. package/src/demo/dsl.ts +0 -143
  72. package/src/demo/mock-data.ts +0 -67
  73. package/src/demo/statusline.json5 +0 -94
  74. package/src/dsl/node-registry.ts +0 -374
  75. package/src/dsl/render.ts +0 -803
  76. package/src/help-text.ts +0 -90
  77. package/src/index.ts +0 -210
  78. package/src/install/currency.ts +0 -197
  79. package/src/install/index.ts +0 -557
  80. package/src/proc/launch.ts +0 -459
  81. package/src/proc/stats-handle.ts +0 -13
  82. package/src/render/action.ts +0 -883
  83. package/src/render/active-segment.ts +0 -78
  84. package/src/render/diagnostic-style.ts +0 -23
  85. package/src/render/diagnostic-text.ts +0 -77
  86. package/src/render/error-glyph.ts +0 -53
  87. package/src/render/menu.ts +0 -257
  88. package/src/render/outcome-plan.ts +0 -45
  89. package/src/render/picker.ts +0 -372
  90. package/src/render/segment-color.ts +0 -74
  91. package/src/render/split-lines.ts +0 -51
  92. package/src/render/strip.ts +0 -228
  93. package/src/segments/cache.ts +0 -131
  94. package/src/segments/context.ts +0 -190
  95. package/src/segments/git.ts +0 -1084
  96. package/src/segments/metrics.ts +0 -187
  97. package/src/segments/pricing.ts +0 -452
  98. package/src/segments/session.ts +0 -23
  99. package/src/segments/tmux.ts +0 -74
  100. package/src/template-engine/cells.ts +0 -90
  101. package/src/template-engine/colors.ts +0 -124
  102. package/src/template-engine/engine.ts +0 -108
  103. package/src/template-engine/funcs.ts +0 -232
  104. package/src/template-engine/index.ts +0 -11
  105. package/src/template-engine/layout.ts +0 -133
  106. package/src/template-engine/scope.ts +0 -62
  107. package/src/template-engine/sparkline.ts +0 -79
  108. package/src/themes/index.ts +0 -20
  109. package/src/themes/palette-resolvers.ts +0 -84
  110. package/src/themes/policy.ts +0 -393
  111. package/src/utils/cache.ts +0 -206
  112. package/src/utils/claude.ts +0 -683
  113. package/src/utils/color-support.ts +0 -118
  114. package/src/utils/formatters.ts +0 -99
  115. package/src/utils/logger.ts +0 -5
  116. package/src/utils/outcome.ts +0 -33
  117. package/src/utils/schema-validator.ts +0 -126
  118. package/src/utils/single-flight.ts +0 -57
  119. package/src/utils/terminal-width.ts +0 -51
  120. package/src/utils/terminal.ts +0 -11
  121. package/src/utils/transcript-fs.ts +0 -279
  122. package/src/var-system/index.ts +0 -24
  123. package/src/var-system/sources.ts +0 -1047
  124. package/src/var-system/store.ts +0 -223
  125. package/src/var-system/types.ts +0 -57
  126. package/src/version.ts +0 -17
@@ -1,459 +0,0 @@
1
- // [LAW:single-enforcer] Every subprocess in the Node runtime goes through one
2
- // boundary. The primitive owns the import of `node:child_process`; every other
3
- // module imports `launch`/`launchSync` from here. The ESLint config (and the
4
- // kz8.2 DoD grep) enforces this.
5
- //
6
- // [LAW:one-type-per-behavior] `exec`/`execFile`/`spawn` and their sync twins
7
- // are seven names for one act. `LaunchOpts` is the single shape; sync vs async
8
- // is a separate function pair, not a config flag.
9
- //
10
- // [LAW:dataflow-not-control-flow] Categories flow through one boundary as
11
- // data; the body is the same code path for every category. The metering layer
12
- // reads the category off the request, not off the call site.
13
- //
14
- // [LAW:types-are-the-program] (kz8.6) Process lifetime is encoded in the
15
- // operation, not in a flag. `launch`/`launchSync` are *waited*: the child is
16
- // reaped before the caller resumes, so it cannot outlive its frame.
17
- // `launchDetachedSync` is the *orphan*: it detaches and unrefs, deliberately
18
- // outliving its caller — the daemon-handoff escape hatch, used only by the
19
- // daemon-acquisition path. There is no `detached: boolean` flag on `LaunchOpts`
20
- // ([LAW:no-mode-explosion]); the two lifetimes are two functions with two
21
- // return contracts, so an unwaited helper that survives a render frame is
22
- // unrepresentable here rather than forbidden by convention.
23
-
24
- import { spawn, spawnSync } from "node:child_process";
25
- import type { ChildProcess, StdioOptions } from "node:child_process";
26
-
27
- import type { LaunchStatsHandle } from "./stats-handle";
28
-
29
- // Closed list of subprocess categories. Adding a new spawn site requires
30
- // adding its category here, which forces a code review of the new launch
31
- // pattern. [LAW:no-mode-explosion]: no per-site escape hatch.
32
- export const LAUNCH_CATEGORIES = [
33
- "git",
34
- // Forge CLIs (gh / glab) for the PR/MR lookup. A network-bound spawn,
35
- // separate from "git" so daemon-stats attributes it independently and a
36
- // future rate limit can target it without throttling local git.
37
- "forge",
38
- "user-shell",
39
- "tmux",
40
- "click.pbcopy",
41
- "click.open",
42
- "install.plutil",
43
- "install.osacompile",
44
- "install.lsregister",
45
- "install.pbcopy",
46
- "install.open",
47
- "daemon-spawn",
48
- // Process start-time fingerprint (`ps -o lstart=`) for socket-lease liveness
49
- // (process-fingerprint.ts). Spawned only at daemon start + EADDRINUSE
50
- // arbitration — never per render — so it needs no rate limit.
51
- "process-fingerprint",
52
- ] as const;
53
-
54
- export type LaunchCategory = (typeof LAUNCH_CATEGORIES)[number];
55
-
56
- // [LAW:single-enforcer] Per-category minimum interval between spawn attempts
57
- // (start timestamps). The limiter records on attempt, not on success — a
58
- // failed spawn still arms the timer so a broken binary can't be retried in a
59
- // tight loop. Sparse map: categories without entries have no rate limit.
60
- // [LAW:no-mode-explosion] Bounds are constants here, not config knobs — the
61
- // caps protect the host from misbehaving renderers/templates and don't need
62
- // user tuning. Bump these if a legitimate workload starts hitting them.
63
- const RATE_LIMITS: Partial<Record<LaunchCategory, number>> = {
64
- // Click verbs: a misbehaving template emitting many clickable links + a
65
- // user rapid-clicking = unbounded helpers. One spawn per second is enough
66
- // for any human click cadence.
67
- "click.pbcopy": 1000,
68
- "click.open": 1000,
69
- };
70
-
71
- // [LAW:one-source-of-truth] Last-attempt timestamp per category — the data
72
- // the rate-limit decision reads. Recorded for every attempted spawn (success
73
- // or spawn-error); rate-limit rejections do NOT update this, because no
74
- // spawn was attempted. Module-scope state is acceptable here because
75
- // `launch.ts` is itself the single enforcer; nothing else mutates this.
76
- const lastStartAt = new Map<LaunchCategory, number>();
77
-
78
- // [LAW:dataflow-not-control-flow] The rate-limit decision is a pure function
79
- // of (category, now, last-start, policy). Same code path every call; the
80
- // result type carries which branch fired.
81
- function checkRateLimit(
82
- category: LaunchCategory,
83
- ):
84
- | { allowed: true }
85
- | { allowed: false; minIntervalMs: number; sinceLastMs: number } {
86
- const minIntervalMs = RATE_LIMITS[category];
87
- if (minIntervalMs === undefined) return { allowed: true };
88
- const last = lastStartAt.get(category);
89
- if (last === undefined) return { allowed: true };
90
- const sinceLastMs = Date.now() - last;
91
- if (sinceLastMs >= minIntervalMs) return { allowed: true };
92
- return { allowed: false, minIntervalMs, sinceLastMs };
93
- }
94
-
95
- function recordStart(category: LaunchCategory): void {
96
- lastStartAt.set(category, Date.now());
97
- }
98
-
99
- // Exposed for tests only — resets the rate-limit tracker so each test starts
100
- // from a clean state.
101
- export function __resetRateLimitsForTest(): void {
102
- lastStartAt.clear();
103
- }
104
-
105
- export interface LaunchOpts {
106
- bin: string;
107
- args?: string[];
108
- cwd?: string;
109
- env?: NodeJS.ProcessEnv;
110
- timeoutMs?: number;
111
- stdinInput?: string | Buffer;
112
- category: LaunchCategory;
113
- }
114
-
115
- export type LaunchResult =
116
- | { ok: true; stdout: string; stderr: string; exitCode: number | null }
117
- | {
118
- ok: false;
119
- // [LAW:one-type-per-behavior] Distinct termination causes get distinct
120
- // tags so callers + stats can attribute correctly. "timeout" means the
121
- // local timer fired; "signal" means the OS or external killer ended the
122
- // child for some other reason (SIGKILL/SIGINT/SIGPIPE/SIGHUP/...);
123
- // "non-zero" is a clean exit with a non-zero code; "spawn-error" is a
124
- // failure before the child started; "rate-limited" means the primitive
125
- // refused to spawn because the per-category minimum interval was not
126
- // yet elapsed — no child process was launched.
127
- reason:
128
- | "timeout"
129
- | "signal"
130
- | "spawn-error"
131
- | "non-zero"
132
- | "rate-limited";
133
- stdout: string;
134
- stderr: string;
135
- exitCode: number | null;
136
- signal: NodeJS.Signals | null;
137
- error?: string;
138
- };
139
-
140
- function rateLimitedResult(
141
- category: LaunchCategory,
142
- minIntervalMs: number,
143
- sinceLastMs: number,
144
- ): LaunchResult {
145
- return {
146
- ok: false,
147
- reason: "rate-limited",
148
- stdout: "",
149
- stderr: "",
150
- exitCode: null,
151
- signal: null,
152
- error: `rate-limited: ${category} min interval ${minIntervalMs}ms, last start ${sinceLastMs}ms ago`,
153
- };
154
- }
155
-
156
- let statsHandle: LaunchStatsHandle | null = null;
157
-
158
- // Install the stats handle once, at daemon startup. Other runtimes (Node
159
- // fallback, install path) leave it null and pay no metering cost.
160
- export function setLaunchStats(handle: LaunchStatsHandle | null): void {
161
- statsHandle = handle;
162
- }
163
-
164
- // [LAW:types-are-the-program] Grace between SIGTERM and SIGKILL on the timeout
165
- // path. The lifetime invariant ("waited — child reaped before the caller
166
- // resumes") requires that a child which ignores SIGTERM is still gone before we
167
- // resolve. SIGTERM lets well-behaved children flush/clean up; SIGKILL is the
168
- // backstop so the promise cannot resolve while the child is still alive.
169
- const TIMEOUT_KILL_GRACE_MS = 250;
170
-
171
- export async function launch(opts: LaunchOpts): Promise<LaunchResult> {
172
- const gate = checkRateLimit(opts.category);
173
- if (!gate.allowed) {
174
- return rateLimitedResult(
175
- opts.category,
176
- gate.minIntervalMs,
177
- gate.sinceLastMs,
178
- );
179
- }
180
- recordStart(opts.category);
181
- const t0 = Date.now();
182
- statsHandle?.onStart(opts.category);
183
-
184
- return new Promise<LaunchResult>((resolve) => {
185
- let child: ChildProcess;
186
- try {
187
- child = spawn(opts.bin, opts.args ?? [], {
188
- cwd: opts.cwd,
189
- env: opts.env,
190
- stdio: ["pipe", "pipe", "pipe"],
191
- });
192
- } catch (err) {
193
- statsHandle?.onEnd(opts.category, Date.now() - t0);
194
- resolve({
195
- ok: false,
196
- reason: "spawn-error",
197
- stdout: "",
198
- stderr: "",
199
- exitCode: null,
200
- signal: null,
201
- error: err instanceof Error ? err.message : String(err),
202
- });
203
- return;
204
- }
205
-
206
- let stdout = "";
207
- let stderr = "";
208
- let settled = false;
209
- let timer: NodeJS.Timeout | null = null;
210
- let killTimer: NodeJS.Timeout | null = null;
211
- // [LAW:dataflow-not-control-flow] Whether the close was caused by *our*
212
- // timer is data we have to carry. The OS doesn't tell us why a child was
213
- // signalled — without this flag, SIGKILL from the OOM killer, SIGINT
214
- // propagated through the tty, SIGPIPE on a closed pipe, etc. all get
215
- // misreported as "timeout".
216
- let timedOut = false;
217
-
218
- const settle = (r: LaunchResult) => {
219
- if (settled) return;
220
- settled = true;
221
- if (timer) clearTimeout(timer);
222
- if (killTimer) clearTimeout(killTimer);
223
- statsHandle?.onEnd(opts.category, Date.now() - t0);
224
- resolve(r);
225
- };
226
-
227
- child.stdout?.on("data", (chunk: Buffer) => {
228
- stdout += chunk.toString("utf8");
229
- });
230
- child.stderr?.on("data", (chunk: Buffer) => {
231
- stderr += chunk.toString("utf8");
232
- });
233
-
234
- child.on("error", (err) => {
235
- settle({
236
- ok: false,
237
- reason: "spawn-error",
238
- stdout,
239
- stderr,
240
- exitCode: null,
241
- signal: null,
242
- error: err.message,
243
- });
244
- });
245
-
246
- child.on("close", (code, signal) => {
247
- // [LAW:types-are-the-program] We resolve here, on the *actual* exit —
248
- // including the timeout path. Once `timedOut` is set the deadline has
249
- // elapsed, so the outcome is "timeout" regardless of which signal
250
- // (SIGTERM or the escalated SIGKILL) finally ended the child.
251
- if (timedOut) {
252
- settle({
253
- ok: false,
254
- reason: "timeout",
255
- stdout,
256
- stderr,
257
- exitCode: code,
258
- signal,
259
- });
260
- return;
261
- }
262
- if (code === 0) {
263
- settle({ ok: true, stdout, stderr, exitCode: code });
264
- return;
265
- }
266
- const reason: "signal" | "non-zero" = signal ? "signal" : "non-zero";
267
- settle({
268
- ok: false,
269
- reason,
270
- stdout,
271
- stderr,
272
- exitCode: code,
273
- signal,
274
- });
275
- });
276
-
277
- if (opts.timeoutMs && opts.timeoutMs > 0) {
278
- timer = setTimeout(() => {
279
- timedOut = true;
280
- // [LAW:types-are-the-program] Do NOT settle here. We signal and let
281
- // the `close` handler resolve once the child is actually gone, so the
282
- // promise never resolves while the child is still alive. SIGTERM
283
- // first; SIGKILL after a grace period if the child ignores it.
284
- //
285
- // [LAW:dataflow-not-control-flow] `child.pid` is undefined when the
286
- // spawn failed asynchronously (ENOENT) — there is no process to signal
287
- // and the `error` event settles that case. kill() on a pid-less child
288
- // signals the wrong target (verified: it can terminate the caller), so
289
- // escalation is gated on the pid actually existing.
290
- if (child.pid !== undefined) {
291
- child.kill("SIGTERM");
292
- killTimer = setTimeout(() => {
293
- child.kill("SIGKILL");
294
- }, TIMEOUT_KILL_GRACE_MS);
295
- }
296
- }, opts.timeoutMs);
297
- }
298
-
299
- if (opts.stdinInput !== undefined && child.stdin) {
300
- child.stdin.end(opts.stdinInput);
301
- } else if (child.stdin) {
302
- child.stdin.end();
303
- }
304
- });
305
- }
306
-
307
- // Sync variant. For callers that genuinely cannot be async — the spawn
308
- // outcome must be settled before the function returns.
309
- export function launchSync(opts: LaunchOpts): LaunchResult {
310
- const gate = checkRateLimit(opts.category);
311
- if (!gate.allowed) {
312
- return rateLimitedResult(
313
- opts.category,
314
- gate.minIntervalMs,
315
- gate.sinceLastMs,
316
- );
317
- }
318
- recordStart(opts.category);
319
- const t0 = Date.now();
320
- statsHandle?.onStart(opts.category);
321
-
322
- const stdio: StdioOptions = ["pipe", "pipe", "pipe"];
323
- try {
324
- const result = spawnSync(opts.bin, opts.args ?? [], {
325
- cwd: opts.cwd,
326
- env: opts.env,
327
- input: opts.stdinInput,
328
- timeout:
329
- opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : undefined,
330
- stdio,
331
- encoding: "utf8",
332
- });
333
-
334
- statsHandle?.onEnd(opts.category, Date.now() - t0);
335
-
336
- const stdout = typeof result.stdout === "string" ? result.stdout : "";
337
- const stderr = typeof result.stderr === "string" ? result.stderr : "";
338
-
339
- if (result.error) {
340
- // Node sets `error` for ETIMEDOUT and ENOENT; distinguish by code.
341
- const code = (result.error as NodeJS.ErrnoException).code;
342
- const reason: "timeout" | "spawn-error" =
343
- code === "ETIMEDOUT" ? "timeout" : "spawn-error";
344
- return {
345
- ok: false,
346
- reason,
347
- stdout,
348
- stderr,
349
- exitCode: null,
350
- signal: result.signal ?? null,
351
- error: result.error.message,
352
- };
353
- }
354
-
355
- if (result.status === 0) {
356
- return { ok: true, stdout, stderr, exitCode: result.status };
357
- }
358
-
359
- // [LAW:dataflow-not-control-flow] The reason data lives in the
360
- // spawnSync result, not in the surrounding control flow. Node sets
361
- // `result.signal` whenever the child died from a signal — including but
362
- // not limited to the timeout's SIGTERM. We can only attribute "timeout"
363
- // when a timeout was actually requested; otherwise the signal came from
364
- // somewhere else (OOM killer, ctrl-C through the tty group, etc.).
365
- const hasTimeout = opts.timeoutMs !== undefined && opts.timeoutMs > 0;
366
- const reason: "timeout" | "signal" | "non-zero" = result.signal
367
- ? hasTimeout && result.signal === "SIGTERM"
368
- ? "timeout"
369
- : "signal"
370
- : "non-zero";
371
- return {
372
- ok: false,
373
- reason,
374
- stdout,
375
- stderr,
376
- exitCode: result.status,
377
- signal: result.signal ?? null,
378
- };
379
- } catch (err) {
380
- statsHandle?.onEnd(opts.category, Date.now() - t0);
381
- return {
382
- ok: false,
383
- reason: "spawn-error",
384
- stdout: "",
385
- stderr: "",
386
- exitCode: null,
387
- signal: null,
388
- error: err instanceof Error ? err.message : String(err),
389
- };
390
- }
391
- }
392
-
393
- // [LAW:single-enforcer] The one orphan operation: a detached, unref'd,
394
- // fire-and-forget launch that deliberately outlives its caller. This is the
395
- // only Node-side launch with that lifetime; everything else waits. It returns
396
- // the typed spawn outcome synchronously (so a failed spawn surfaces as
397
- // `ok: false` rather than a discarded Promise reporting success), and meters
398
- // through the stats handle so orphan spawns still show up in daemon-stats.
399
- export function launchDetachedSync(opts: LaunchOpts): LaunchResult {
400
- const gate = checkRateLimit(opts.category);
401
- if (!gate.allowed) {
402
- return rateLimitedResult(
403
- opts.category,
404
- gate.minIntervalMs,
405
- gate.sinceLastMs,
406
- );
407
- }
408
- recordStart(opts.category);
409
- const t0 = Date.now();
410
- statsHandle?.onStart(opts.category);
411
- const result = launchDetachedSyncInner(opts);
412
- statsHandle?.onEnd(opts.category, Date.now() - t0);
413
- return result;
414
- }
415
-
416
- function launchDetachedSyncInner(opts: LaunchOpts): LaunchResult {
417
- let child;
418
- try {
419
- child = spawn(opts.bin, opts.args ?? [], {
420
- cwd: opts.cwd,
421
- env: opts.env,
422
- detached: true,
423
- stdio: "ignore",
424
- });
425
- } catch (err) {
426
- // spawn throws synchronously on some failure modes (invalid options,
427
- // EACCES on some platforms).
428
- return {
429
- ok: false,
430
- reason: "spawn-error",
431
- stdout: "",
432
- stderr: "",
433
- exitCode: null,
434
- signal: null,
435
- error: err instanceof Error ? err.message : String(err),
436
- };
437
- }
438
- // [LAW:no-silent-fallbacks] spawn() with ENOENT (e.g. missing binary) does
439
- // *not* throw — it returns a ChildProcess with pid=undefined that emits
440
- // 'error' asynchronously. Two things matter here:
441
- // 1. The 'error' must have a listener or Node crashes the process.
442
- // 2. The synchronous return must reflect that the spawn failed.
443
- // We attach a no-op listener and use the synchronously-observable absence
444
- // of a pid as the spawn-failure signal.
445
- child.once("error", () => {});
446
- if (child.pid === undefined) {
447
- return {
448
- ok: false,
449
- reason: "spawn-error",
450
- stdout: "",
451
- stderr: "",
452
- exitCode: null,
453
- signal: null,
454
- error: `spawn(${opts.bin}): no pid (binary not found or unexecutable)`,
455
- };
456
- }
457
- child.unref();
458
- return { ok: true, stdout: "", stderr: "", exitCode: null };
459
- }
@@ -1,13 +0,0 @@
1
- // [LAW:locality-or-seam] The seam between `launch.ts` and `RuntimeStats`.
2
- // `launch.ts` cannot import `RuntimeStats` directly — that would couple every
3
- // caller's runtime (Node fallback, install path) to the daemon's stats object.
4
- // Instead, the daemon constructs a stats object that implements this
5
- // interface and hands it to `setLaunchStats()` at startup. Other runtimes
6
- // pass null; the launcher no-ops.
7
-
8
- import type { LaunchCategory } from "./launch";
9
-
10
- export interface LaunchStatsHandle {
11
- onStart(category: LaunchCategory): void;
12
- onEnd(category: LaunchCategory, durationMs: number): void;
13
- }