@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,249 +0,0 @@
1
- import fs from "node:fs";
2
- import { debug } from "../../utils/logger";
3
-
4
- // Logger callable used for watcher lifecycle / failure events. Daemon
5
- // injects `dlog` (writes to daemon.log at the requested level); other
6
- // consumers (var-system tests, demo) take the default which routes
7
- // through `debug` — present-but-quiet unless CC_CANDYBAR_DEBUG is set.
8
- // [LAW:no-defensive-null-guards] Default is always non-null; injection
9
- // replaces the implementation, never adds a "no logger" mode.
10
- export type WatcherLogger = (
11
- level: "info" | "warn" | "error",
12
- message: string,
13
- ) => void;
14
-
15
- const defaultLogger: WatcherLogger = (_level, message) => debug(message);
16
-
17
- // [LAW:single-enforcer] One registry owns *all* fs watchers for any consumer
18
- // (git cache, config cache, ...). Scattered watchers across modules would leak
19
- // FDs and miss cleanup at shutdown.
20
-
21
- // [LAW:one-source-of-truth] Exported: the invalidation-debounce floor is the
22
- // single source consumers derive from (e.g. the load harness's git-churn
23
- // interval must sit above it). Restating "50" anywhere else would let the two
24
- // drift silently.
25
- export const DEBOUNCE_MS = 50;
26
- const DEFAULT_MAX_WATCHERS = 128;
27
-
28
- // Absolute filesystem paths the consumer wants invalidation for.
29
- // `files`: regular files (existence required to watch).
30
- // `dirs`: directories. Each may optionally restrict which child filenames
31
- // should fire — without a filter every change in the dir fires (legacy
32
- // git-refs behavior); with a filter only matching basenames fire (config
33
- // case: avoids noise from sibling files in ~/.claude/).
34
- export interface DirTarget {
35
- path: string;
36
- // If set, only fire when fs.watch reports a filename in this list.
37
- // Filenames are basenames (not paths) — that's what fs.watch supplies.
38
- filenames?: readonly string[];
39
- }
40
-
41
- export interface WatchTargets {
42
- files: readonly string[];
43
- dirs: readonly DirTarget[];
44
- }
45
-
46
- interface WatcherSlot {
47
- key: string;
48
- watchers: fs.FSWatcher[];
49
- refcount: number;
50
- debounceTimer: NodeJS.Timeout | null;
51
- onInvalidate: () => void;
52
- targets: WatchTargets;
53
- // Last-seen accessed-at, so LRU eviction picks the staler one.
54
- lastTouched: number;
55
- }
56
-
57
- export interface WatcherHandle {
58
- release(): void;
59
- }
60
-
61
- export interface WatcherCounters {
62
- watchersOpened: number;
63
- watchersClosed: number;
64
- watchersEvicted: number;
65
- }
66
-
67
- export class WatcherRegistry {
68
- private readonly slots = new Map<string, WatcherSlot>();
69
- private readonly maxWatchers: number;
70
- private readonly counters?: WatcherCounters;
71
- private readonly logger: WatcherLogger;
72
- private closed = false;
73
-
74
- constructor(
75
- opts: {
76
- maxWatchers?: number;
77
- counters?: WatcherCounters;
78
- // [LAW:single-enforcer] One injection point per consumer. The daemon
79
- // passes `dlog` so watcher-failure events land in daemon.log; non-
80
- // daemon consumers keep the default debug-routed logger and never
81
- // touch daemon log files.
82
- logger?: WatcherLogger;
83
- } = {},
84
- ) {
85
- this.maxWatchers = opts.maxWatchers ?? DEFAULT_MAX_WATCHERS;
86
- this.counters = opts.counters;
87
- this.logger = opts.logger ?? defaultLogger;
88
- }
89
-
90
- // Acquire (or share) a watcher set keyed by `key`. Multiple acquires on the
91
- // same key share a single underlying FSWatcher set; refcount tracks active
92
- // consumers. Subsequent acquires *replace* onInvalidate so the latest
93
- // consumer's callback is the one that fires — by design, callers funnel into
94
- // a single cache module whose callback is a stable closure over the cache map.
95
- // `targets` from the first acquire wins; subsequent acquires keep the
96
- // original target set (consumers must use a fresh key if they need different
97
- // targets).
98
- acquire(
99
- key: string,
100
- targets: WatchTargets,
101
- onInvalidate: () => void,
102
- ): WatcherHandle {
103
- if (this.closed) {
104
- // Registry already shut down; return a no-op handle so callers don't
105
- // crash mid-shutdown.
106
- return { release: () => {} };
107
- }
108
-
109
- const existing = this.slots.get(key);
110
- if (existing) {
111
- existing.refcount++;
112
- existing.onInvalidate = onInvalidate;
113
- existing.lastTouched = Date.now();
114
- // LRU bump.
115
- this.slots.delete(key);
116
- this.slots.set(key, existing);
117
- return this.makeHandle(key);
118
- }
119
-
120
- const slot: WatcherSlot = {
121
- key,
122
- watchers: [],
123
- refcount: 1,
124
- debounceTimer: null,
125
- onInvalidate,
126
- targets,
127
- lastTouched: Date.now(),
128
- };
129
- this.openWatchers(slot);
130
- this.slots.set(key, slot);
131
- if (this.counters) this.counters.watchersOpened++;
132
- this.evictIfNeeded();
133
- return this.makeHandle(key);
134
- }
135
-
136
- private makeHandle(key: string): WatcherHandle {
137
- let released = false;
138
- return {
139
- release: () => {
140
- if (released) return;
141
- released = true;
142
- const slot = this.slots.get(key);
143
- if (!slot) return;
144
- slot.refcount = Math.max(0, slot.refcount - 1);
145
- if (slot.refcount === 0) {
146
- this.closeSlot(slot);
147
- this.slots.delete(key);
148
- }
149
- },
150
- };
151
- }
152
-
153
- private openWatchers(slot: WatcherSlot): void {
154
- const fire = () => {
155
- if (slot.debounceTimer) return; // already pending
156
- slot.debounceTimer = setTimeout(() => {
157
- slot.debounceTimer = null;
158
- try {
159
- slot.onInvalidate();
160
- } catch (e) {
161
- this.logger(
162
- "warn",
163
- `watcher invalidate threw: ${(e as Error).message}`,
164
- );
165
- }
166
- }, DEBOUNCE_MS);
167
- slot.debounceTimer.unref();
168
- };
169
-
170
- for (const target of slot.targets.files) {
171
- try {
172
- const w = fs.watch(target, { persistent: false }, fire);
173
- w.on("error", (e) => {
174
- this.logger("warn", `watcher error ${target}: ${e.message}`);
175
- });
176
- slot.watchers.push(w);
177
- } catch (e) {
178
- this.logger("warn", `watch failed ${target}: ${(e as Error).message}`);
179
- }
180
- }
181
-
182
- for (const target of slot.targets.dirs) {
183
- try {
184
- const filterSet = target.filenames ? new Set(target.filenames) : null;
185
- const onDirEvent = (_evt: string, filename: string | null) => {
186
- // [LAW:dataflow-not-control-flow] Filter is a value (Set) — same
187
- // code path every event; the Set's .has() decides whether to fire.
188
- if (filterSet && (!filename || !filterSet.has(filename))) return;
189
- fire();
190
- };
191
- const w = fs.watch(target.path, { persistent: false }, onDirEvent);
192
- w.on("error", (e) => {
193
- this.logger("warn", `watcher error ${target.path}: ${e.message}`);
194
- });
195
- slot.watchers.push(w);
196
- } catch (e) {
197
- this.logger(
198
- "warn",
199
- `watch failed ${target.path}: ${(e as Error).message}`,
200
- );
201
- }
202
- }
203
- }
204
-
205
- private closeSlot(slot: WatcherSlot): void {
206
- if (slot.debounceTimer) {
207
- clearTimeout(slot.debounceTimer);
208
- slot.debounceTimer = null;
209
- }
210
- for (const w of slot.watchers) {
211
- try {
212
- w.close();
213
- } catch {}
214
- }
215
- slot.watchers = [];
216
- if (this.counters) this.counters.watchersClosed++;
217
- }
218
-
219
- private evictIfNeeded(): void {
220
- while (this.slots.size > this.maxWatchers) {
221
- // Map iteration order = insertion order = LRU order (we re-insert on
222
- // access).
223
- const oldest = this.slots.keys().next().value;
224
- if (oldest === undefined) break;
225
- const slot = this.slots.get(oldest)!;
226
- this.closeSlot(slot);
227
- this.slots.delete(oldest);
228
- if (this.counters) this.counters.watchersEvicted++;
229
- // Force the consumer to drop their entry too — without this the cache
230
- // would keep stale data with no watcher behind it.
231
- try {
232
- slot.onInvalidate();
233
- } catch {}
234
- this.logger("info", `watcher LRU evict ${oldest}`);
235
- }
236
- }
237
-
238
- size(): number {
239
- return this.slots.size;
240
- }
241
-
242
- closeAll(): void {
243
- this.closed = true;
244
- for (const slot of this.slots.values()) {
245
- this.closeSlot(slot);
246
- }
247
- this.slots.clear();
248
- }
249
- }
@@ -1,120 +0,0 @@
1
- // [LAW:single-enforcer] The `vars` / `segments` / `config` CLIs are ONE behavior
2
- // — fetch a debug snapshot from the running daemon and render it — parameterized
3
- // by `what`, not three commands. The daemon's `debug` protocol message is the
4
- // single introspection authority (src/daemon/debug.ts produces DebugSnapshot);
5
- // this is its client binding, the mirror of client-stats.ts. Like daemon-stats,
6
- // it does NOT spawn a daemon: introspecting a dead daemon is meaningless.
7
-
8
- import process from "node:process";
9
- import { describeFailure, requestOutcome } from "./client-transport";
10
- import type { RoundTripBudgets, RoundTripOutcome } from "./client-transport";
11
- import type {
12
- DebugSnapshot,
13
- DebugWhat,
14
- SegmentSnapshot,
15
- VarSnapshot,
16
- } from "./debug-types";
17
- import type { DslConfig } from "../config/dsl-types";
18
-
19
- // Operator-driven introspection path: legitimately slower budgets than the
20
- // render hot path, carried as this caller's values through the shared
21
- // round-trip in ./client-transport. [LAW:dataflow-not-control-flow]
22
- const BUDGETS: RoundTripBudgets = { connectMs: 200, budgetMs: 500 };
23
-
24
- // `cc-candybar <vars|segments|config> [--json]` — `what` selects the projection.
25
- export async function runDebug(
26
- what: DebugWhat,
27
- args: readonly string[],
28
- ): Promise<void> {
29
- const wantJson = args.includes("--json");
30
-
31
- const outcome = await fetchDebug(what);
32
- if (outcome.kind !== "ok") {
33
- process.stderr.write(`${what}: ${describeFailure(outcome)}\n`);
34
- process.exit(1);
35
- }
36
-
37
- if (wantJson) {
38
- process.stdout.write(JSON.stringify(outcome.value, null, 2) + "\n");
39
- return;
40
- }
41
-
42
- process.stdout.write(formatDebug(outcome.value));
43
- }
44
-
45
- function fetchDebug(what: DebugWhat): Promise<RoundTripOutcome<DebugSnapshot>> {
46
- // [LAW:no-defensive-null-guards] exception: trust boundary. The response is
47
- // an unchecked cast from socket JSON; the presence check is the explicit
48
- // narrowing at the wire edge (an ok response without `debug` classifies as
49
- // permanent/malformed_response in the transport).
50
- return requestOutcome({ kind: "debug", what }, BUDGETS, (resp) =>
51
- "debug" in resp ? resp.debug : undefined,
52
- );
53
- }
54
-
55
- // [LAW:types-are-the-program] One total fold over the DebugSnapshot union; each
56
- // arm renders its own shape. The switch is exhaustive (the `never` default makes
57
- // a new `what` a compile error here), so the renderer can never fall out of
58
- // lockstep with the protocol's `what` set — the projection is residue of the
59
- // union, not a hand-maintained dispatch.
60
- export function formatDebug(s: DebugSnapshot): string {
61
- switch (s.what) {
62
- case "vars":
63
- return formatVars(s.vars);
64
- case "segments":
65
- return formatSegments(s.segments);
66
- case "config":
67
- return formatConfig(s.config);
68
- default: {
69
- const _exhaustive: never = s;
70
- return _exhaustive;
71
- }
72
- }
73
- }
74
-
75
- function formatVars(vars: readonly VarSnapshot[]): string {
76
- if (vars.length === 0) return "no variables (DSL not active)\n";
77
- const lines: string[] = [`variables (${vars.length})`, ``];
78
- const nameW = vars.reduce((w, v) => Math.max(w, v.name.length), 4);
79
- const srcW = vars.reduce((w, v) => Math.max(w, (v.source ?? "—").length), 6);
80
- for (const v of vars) {
81
- const age = v.ageMs === null ? "" : ` ${fmtAge(v.ageMs)}`;
82
- const err = v.lastError ? ` ✗ ${v.lastError.message}` : "";
83
- lines.push(
84
- ` ${v.name.padEnd(nameW)} ${(v.source ?? "—").padEnd(srcW)} ${v.type.padEnd(7)} ${fmtValue(v.value)}${age}${err}`,
85
- );
86
- }
87
- return lines.join("\n") + "\n";
88
- }
89
-
90
- function formatSegments(segments: readonly SegmentSnapshot[]): string {
91
- if (segments.length === 0) return "no segments (DSL not active)\n";
92
- const lines: string[] = [`segments (${segments.length})`, ``];
93
- for (const seg of segments) {
94
- lines.push(` ${seg.name}`);
95
- lines.push(` template ${seg.template}`);
96
- if (seg.referencedVars.length > 0) {
97
- lines.push(` vars ${seg.referencedVars.join(", ")}`);
98
- }
99
- if (seg.lastRender !== null) {
100
- lines.push(` last ${seg.lastRender}`);
101
- }
102
- }
103
- return lines.join("\n") + "\n";
104
- }
105
-
106
- function formatConfig(config: DslConfig | null): string {
107
- if (config === null) return "config: DSL not active\n";
108
- return JSON.stringify(config, null, 2) + "\n";
109
- }
110
-
111
- function fmtValue(v: unknown): string {
112
- const s = typeof v === "string" ? v : JSON.stringify(v);
113
- return s.length > 60 ? s.slice(0, 57) + "…" : s;
114
- }
115
-
116
- function fmtAge(ms: number): string {
117
- if (ms < 1000) return `${ms}ms`;
118
- if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
119
- return `${Math.floor(ms / 60_000)}m`;
120
- }
@@ -1,130 +0,0 @@
1
- import process from "node:process";
2
- import { describeFailure, requestOutcome } from "./client-transport";
3
- import type { RoundTripBudgets, RoundTripOutcome } from "./client-transport";
4
- import type { StatsSnapshot } from "./stats";
5
-
6
- // Operator-driven introspection path: legitimately slower budgets than the
7
- // render hot path, carried as this caller's values through the shared
8
- // round-trip in ./client-transport. [LAW:dataflow-not-control-flow]
9
- const BUDGETS: RoundTripBudgets = { connectMs: 200, budgetMs: 500 };
10
-
11
- // Query the running daemon for stats. Does NOT spawn a daemon — stats on a
12
- // dead daemon is meaningless. Exits non-zero on failure with a clear message.
13
- export async function runDaemonStats(args: readonly string[]): Promise<void> {
14
- const wantJson = args.includes("--json");
15
-
16
- const outcome = await fetchStats();
17
- if (outcome.kind !== "ok") {
18
- process.stderr.write(`daemon-stats: ${describeFailure(outcome)}\n`);
19
- process.exit(1);
20
- }
21
-
22
- if (wantJson) {
23
- process.stdout.write(JSON.stringify(outcome.value, null, 2) + "\n");
24
- return;
25
- }
26
-
27
- process.stdout.write(formatStats(outcome.value));
28
- }
29
-
30
- function fetchStats(): Promise<RoundTripOutcome<StatsSnapshot>> {
31
- // [LAW:no-defensive-null-guards] exception: trust boundary. The response is
32
- // an unchecked cast from socket JSON; the presence check is the explicit
33
- // narrowing at the wire edge (an ok response without `stats` classifies as
34
- // permanent/malformed_response in the transport).
35
- return requestOutcome({ kind: "stats" }, BUDGETS, (resp) =>
36
- "stats" in resp ? resp.stats : undefined,
37
- );
38
- }
39
-
40
- function fmtBytes(n: number): string {
41
- if (n < 1024) return `${n}B`;
42
- if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
43
- return `${(n / 1024 / 1024).toFixed(1)}MB`;
44
- }
45
-
46
- function fmtRate(hits: number, misses: number): string {
47
- const total = hits + misses;
48
- if (total === 0) return "n/a";
49
- return `${((hits / total) * 100).toFixed(1)}%`;
50
- }
51
-
52
- function fmtUptime(sec: number): string {
53
- if (sec < 60) return `${sec}s`;
54
- if (sec < 3600) return `${Math.floor(sec / 60)}m${sec % 60}s`;
55
- const h = Math.floor(sec / 3600);
56
- const m = Math.floor((sec % 3600) / 60);
57
- return `${h}h${m}m`;
58
- }
59
-
60
- export function formatStats(s: StatsSnapshot): string {
61
- const lines: string[] = [];
62
- lines.push(`cc-candybar daemon stats`);
63
- lines.push(``);
64
- lines.push(`process`);
65
- lines.push(` pid ${s.pid}`);
66
- lines.push(` version ${s.version}`);
67
- lines.push(` protocol ${s.protocolVersion}`);
68
- lines.push(` startedAt ${s.startedAt}`);
69
- lines.push(` uptime ${fmtUptime(s.uptimeSec)}`);
70
- lines.push(` rss ${fmtBytes(s.rssBytes)}`);
71
- lines.push(` heapUsed ${fmtBytes(s.heapUsedBytes)}`);
72
- lines.push(` heapTotal ${fmtBytes(s.heapTotalBytes)}`);
73
- lines.push(` external ${fmtBytes(s.externalBytes)}`);
74
- lines.push(` arrayBuffers ${fmtBytes(s.arrayBuffersBytes)}`);
75
- lines.push(``);
76
- lines.push(`requests`);
77
- lines.push(` total ${s.requests.total}`);
78
- lines.push(` errored ${s.requests.errored}`);
79
- lines.push(` timedOut ${s.requests.timedOut}`);
80
- lines.push(` inFlight ${s.requests.inFlight}`);
81
- lines.push(``);
82
- lines.push(`gitCache`);
83
- lines.push(` size ${s.gitCache.size}`);
84
- lines.push(
85
- ` hit rate ${fmtRate(s.gitCache.hits, s.gitCache.misses)} (${s.gitCache.hits} / ${s.gitCache.hits + s.gitCache.misses})`,
86
- );
87
- lines.push(` invalidations ${s.gitCache.invalidations}`);
88
- lines.push(``);
89
- lines.push(`usageCache`);
90
- lines.push(` size ${s.usageCache.size}`);
91
- lines.push(
92
- ` hit rate ${fmtRate(s.usageCache.hits, s.usageCache.misses)} (${s.usageCache.hits} / ${s.usageCache.hits + s.usageCache.misses})`,
93
- );
94
- lines.push(` sweeps ${s.usageCache.sweeps}`);
95
- lines.push(``);
96
- lines.push(`renderCache`);
97
- lines.push(` size ${s.renderCache.size}`);
98
- lines.push(`watchers`);
99
- lines.push(` active ${s.watchers.active}`);
100
- lines.push(` opened ${s.watchers.opened}`);
101
- lines.push(` closed ${s.watchers.closed}`);
102
- lines.push(` evicted ${s.watchers.evicted}`);
103
- lines.push(``);
104
- lines.push(`subprocesses`);
105
- lines.push(` total ${s.subprocesses.total}`);
106
- lines.push(` inFlight ${s.subprocesses.inFlight}`);
107
- lines.push(` lastMinute ${s.subprocesses.lastMinute}`);
108
- // Snapshot already includes only executed categories (stats.ts:
109
- // snapshotSubprocesses keeps byCategory and the histograms symmetric).
110
- const activeCats = Object.entries(s.subprocesses.byCategory);
111
- // [LAW:dataflow-not-control-flow] Column width is a function of the data,
112
- // not a hardcoded constant that drifts when new categories are added. The
113
- // padEnd(13) baseline matches the " pid " etc. columns above so
114
- // short categories still line up; longer ones expand the column.
115
- const colWidth = activeCats.reduce((w, [cat]) => Math.max(w, cat.length), 13);
116
- for (const [cat, n] of activeCats) {
117
- const p50 = s.subprocesses.p50DurationMs[cat];
118
- const p99 = s.subprocesses.p99DurationMs[cat];
119
- const timing =
120
- p50 !== undefined && p99 !== undefined
121
- ? ` (p50 ${p50}ms · p99 ${p99}ms)`
122
- : "";
123
- lines.push(` ${cat.padEnd(colWidth)} ${n}${timing}`);
124
- }
125
- if (s.nextRestartReason) {
126
- lines.push(``);
127
- lines.push(`nextRestart ${s.nextRestartReason}`);
128
- }
129
- return lines.join("\n") + "\n";
130
- }