@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.
- package/dist/index.mjs +72 -71
- package/package.json +5 -6
- package/src/check.ts +0 -478
- package/src/cli-flags.ts +0 -8
- package/src/click/wire.ts +0 -158
- package/src/config/action.ts +0 -329
- package/src/config/cli.ts +0 -71
- package/src/config/default-dsl-config.ts +0 -1645
- package/src/config/disclosure.ts +0 -170
- package/src/config/dsl-loader.ts +0 -339
- package/src/config/dsl-types.ts +0 -581
- package/src/config/edit-chrome.ts +0 -559
- package/src/config/help.ts +0 -151
- package/src/config/ident.ts +0 -22
- package/src/config/layout-ops.ts +0 -177
- package/src/config/loader/actions.ts +0 -972
- package/src/config/loader/cache.ts +0 -206
- package/src/config/loader/cross-ref.ts +0 -714
- package/src/config/loader/cycles.ts +0 -148
- package/src/config/loader/diagnostics.ts +0 -99
- package/src/config/loader/discovery.ts +0 -182
- package/src/config/loader/edit-mode.ts +0 -137
- package/src/config/loader/emit-schema.ts +0 -68
- package/src/config/loader/globals.ts +0 -269
- package/src/config/loader/helpers.ts +0 -48
- package/src/config/loader/layout.ts +0 -693
- package/src/config/loader/looks.ts +0 -96
- package/src/config/loader/menu-synth.ts +0 -435
- package/src/config/loader/merge.ts +0 -115
- package/src/config/loader/persist-target.ts +0 -67
- package/src/config/loader/presets.ts +0 -119
- package/src/config/loader/refs.ts +0 -100
- package/src/config/loader/reserved-namespace.ts +0 -38
- package/src/config/loader/segments.ts +0 -120
- package/src/config/loader/validate-core.ts +0 -737
- package/src/config/loader/variables.ts +0 -260
- package/src/config/menu-keys.ts +0 -139
- package/src/config/option-domain.ts +0 -164
- package/src/config/presets.ts +0 -326
- package/src/config/settings-menu.ts +0 -775
- package/src/daemon/acquire.ts +0 -684
- package/src/daemon/cache/git.ts +0 -649
- package/src/daemon/cache/render.ts +0 -623
- package/src/daemon/cache/session-usage-store.ts +0 -720
- package/src/daemon/cache/watchers.ts +0 -249
- package/src/daemon/client-debug.ts +0 -120
- package/src/daemon/client-stats.ts +0 -130
- package/src/daemon/client-transport.ts +0 -273
- package/src/daemon/client.ts +0 -78
- package/src/daemon/config-overrides-store.ts +0 -663
- package/src/daemon/debug-types.ts +0 -91
- package/src/daemon/debug.ts +0 -264
- package/src/daemon/fork-bomb-breaker.ts +0 -351
- package/src/daemon/limits.ts +0 -211
- package/src/daemon/log.ts +0 -81
- package/src/daemon/parent-watchdog.ts +0 -87
- package/src/daemon/paths.ts +0 -211
- package/src/daemon/process-fingerprint.ts +0 -146
- package/src/daemon/protocol.ts +0 -292
- package/src/daemon/render-payload.ts +0 -1256
- package/src/daemon/server.ts +0 -1330
- package/src/daemon/session-state-file.ts +0 -108
- package/src/daemon/session-state.ts +0 -237
- package/src/daemon/socket-lease.ts +0 -209
- package/src/daemon/socket-ownership.ts +0 -209
- package/src/daemon/stats.ts +0 -235
- package/src/daemon/verbs/config-validators.ts +0 -250
- package/src/daemon/verbs/index.ts +0 -706
- package/src/daemon/verbs/state-validators.ts +0 -249
- package/src/daemon/verbs/validator-registry.ts +0 -457
- package/src/demo/dsl.ts +0 -143
- package/src/demo/mock-data.ts +0 -67
- package/src/demo/statusline.json5 +0 -94
- package/src/dsl/node-registry.ts +0 -374
- package/src/dsl/render.ts +0 -803
- package/src/help-text.ts +0 -90
- package/src/index.ts +0 -210
- package/src/install/currency.ts +0 -197
- package/src/install/index.ts +0 -557
- package/src/proc/launch.ts +0 -459
- package/src/proc/stats-handle.ts +0 -13
- package/src/render/action.ts +0 -883
- package/src/render/active-segment.ts +0 -78
- package/src/render/diagnostic-style.ts +0 -23
- package/src/render/diagnostic-text.ts +0 -77
- package/src/render/error-glyph.ts +0 -53
- package/src/render/menu.ts +0 -257
- package/src/render/outcome-plan.ts +0 -45
- package/src/render/picker.ts +0 -372
- package/src/render/segment-color.ts +0 -74
- package/src/render/split-lines.ts +0 -51
- package/src/render/strip.ts +0 -228
- package/src/segments/cache.ts +0 -131
- package/src/segments/context.ts +0 -190
- package/src/segments/git.ts +0 -1084
- package/src/segments/metrics.ts +0 -187
- package/src/segments/pricing.ts +0 -452
- package/src/segments/session.ts +0 -23
- package/src/segments/tmux.ts +0 -74
- package/src/template-engine/cells.ts +0 -90
- package/src/template-engine/colors.ts +0 -124
- package/src/template-engine/engine.ts +0 -108
- package/src/template-engine/funcs.ts +0 -232
- package/src/template-engine/index.ts +0 -11
- package/src/template-engine/layout.ts +0 -133
- package/src/template-engine/scope.ts +0 -62
- package/src/template-engine/sparkline.ts +0 -79
- package/src/themes/index.ts +0 -20
- package/src/themes/palette-resolvers.ts +0 -84
- package/src/themes/policy.ts +0 -393
- package/src/utils/cache.ts +0 -206
- package/src/utils/claude.ts +0 -683
- package/src/utils/color-support.ts +0 -118
- package/src/utils/formatters.ts +0 -99
- package/src/utils/logger.ts +0 -5
- package/src/utils/outcome.ts +0 -33
- package/src/utils/schema-validator.ts +0 -126
- package/src/utils/single-flight.ts +0 -57
- package/src/utils/terminal-width.ts +0 -51
- package/src/utils/terminal.ts +0 -11
- package/src/utils/transcript-fs.ts +0 -279
- package/src/var-system/index.ts +0 -24
- package/src/var-system/sources.ts +0 -1047
- package/src/var-system/store.ts +0 -223
- package/src/var-system/types.ts +0 -57
- 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
|
-
}
|