@promptctl/cc-candybar 1.42.0 → 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/README.md +4 -0
  2. package/dist/index.mjs +81 -80
  3. package/package.json +5 -6
  4. package/src/check.ts +0 -478
  5. package/src/cli-flags.ts +0 -8
  6. package/src/click/wire.ts +0 -158
  7. package/src/config/action.ts +0 -329
  8. package/src/config/cli.ts +0 -71
  9. package/src/config/default-dsl-config.ts +0 -1645
  10. package/src/config/disclosure.ts +0 -170
  11. package/src/config/dsl-loader.ts +0 -339
  12. package/src/config/dsl-types.ts +0 -581
  13. package/src/config/edit-chrome.ts +0 -559
  14. package/src/config/help.ts +0 -151
  15. package/src/config/ident.ts +0 -22
  16. package/src/config/layout-ops.ts +0 -177
  17. package/src/config/loader/actions.ts +0 -972
  18. package/src/config/loader/cache.ts +0 -206
  19. package/src/config/loader/cross-ref.ts +0 -714
  20. package/src/config/loader/cycles.ts +0 -148
  21. package/src/config/loader/diagnostics.ts +0 -99
  22. package/src/config/loader/discovery.ts +0 -182
  23. package/src/config/loader/edit-mode.ts +0 -137
  24. package/src/config/loader/emit-schema.ts +0 -68
  25. package/src/config/loader/globals.ts +0 -269
  26. package/src/config/loader/helpers.ts +0 -48
  27. package/src/config/loader/layout.ts +0 -693
  28. package/src/config/loader/looks.ts +0 -96
  29. package/src/config/loader/menu-synth.ts +0 -435
  30. package/src/config/loader/merge.ts +0 -115
  31. package/src/config/loader/persist-target.ts +0 -67
  32. package/src/config/loader/presets.ts +0 -119
  33. package/src/config/loader/refs.ts +0 -100
  34. package/src/config/loader/reserved-namespace.ts +0 -38
  35. package/src/config/loader/segments.ts +0 -120
  36. package/src/config/loader/validate-core.ts +0 -737
  37. package/src/config/loader/variables.ts +0 -260
  38. package/src/config/menu-keys.ts +0 -139
  39. package/src/config/option-domain.ts +0 -164
  40. package/src/config/presets.ts +0 -326
  41. package/src/config/settings-menu.ts +0 -775
  42. package/src/daemon/acquire.ts +0 -684
  43. package/src/daemon/cache/git.ts +0 -649
  44. package/src/daemon/cache/render.ts +0 -623
  45. package/src/daemon/cache/session-usage-store.ts +0 -720
  46. package/src/daemon/cache/watchers.ts +0 -249
  47. package/src/daemon/client-debug.ts +0 -120
  48. package/src/daemon/client-stats.ts +0 -130
  49. package/src/daemon/client-transport.ts +0 -273
  50. package/src/daemon/client.ts +0 -78
  51. package/src/daemon/config-overrides-store.ts +0 -663
  52. package/src/daemon/debug-types.ts +0 -91
  53. package/src/daemon/debug.ts +0 -264
  54. package/src/daemon/fork-bomb-breaker.ts +0 -351
  55. package/src/daemon/limits.ts +0 -211
  56. package/src/daemon/log.ts +0 -81
  57. package/src/daemon/parent-watchdog.ts +0 -87
  58. package/src/daemon/paths.ts +0 -211
  59. package/src/daemon/process-fingerprint.ts +0 -146
  60. package/src/daemon/protocol.ts +0 -292
  61. package/src/daemon/render-payload.ts +0 -1256
  62. package/src/daemon/server.ts +0 -1330
  63. package/src/daemon/session-state-file.ts +0 -108
  64. package/src/daemon/session-state.ts +0 -237
  65. package/src/daemon/socket-lease.ts +0 -209
  66. package/src/daemon/socket-ownership.ts +0 -209
  67. package/src/daemon/stats.ts +0 -235
  68. package/src/daemon/verbs/config-validators.ts +0 -250
  69. package/src/daemon/verbs/index.ts +0 -706
  70. package/src/daemon/verbs/state-validators.ts +0 -249
  71. package/src/daemon/verbs/validator-registry.ts +0 -457
  72. package/src/demo/dsl.ts +0 -143
  73. package/src/demo/mock-data.ts +0 -67
  74. package/src/demo/statusline.json5 +0 -94
  75. package/src/dsl/node-registry.ts +0 -374
  76. package/src/dsl/render.ts +0 -803
  77. package/src/help-text.ts +0 -90
  78. package/src/index.ts +0 -210
  79. package/src/install/index.ts +0 -541
  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
- }