@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,457 +0,0 @@
1
- // [LAW:one-type-per-behavior] The keyed-validator-registry ALGEBRA, extracted
2
- // from state-validators.ts (candybar-config-engine-71o.2) so it has exactly
3
- // ONE implementation shared by two independent keyspaces: SessionState writes
4
- // (`set` actions, state-validators.ts) and persistent config writes (`persist`
5
- // actions, config-validators.ts). What differs between the two is only DATA —
6
- // which keys are baseline/permanent and what namespace the keys live in — so
7
- // this module is the "one cutter" and each keyspace is an instance of it, not
8
- // a hand-rolled copy of the merge/dispose/rebuild logic.
9
- //
10
- // [LAW:one-source-of-truth] THE spec algebra: a key's live registrations
11
- // (DerivedValidatorSpec[]) collapse to ONE spec via mergeKeySpecs, and a spec
12
- // is residue-projected to a KeyValidator via validatorForSpec. Both keyspaces
13
- // read this from the SAME functions, so "what does a range/allow-list/int
14
- // spec mean" cannot drift between session and config gates.
15
-
16
- // [LAW:types-are-the-program] Discriminated union — every legal return is
17
- // either an accepted-and-canonicalized string or a structured rejection
18
- // reason. There is no third state (no `null`, no thrown exception path
19
- // inside a validator). The verb body matches exhaustively on `ok`.
20
- export type ValidateResult =
21
- | { ok: true; value: string }
22
- | { ok: false; reason: string };
23
-
24
- // [LAW:one-type-per-behavior] All key validators have the same shape — they
25
- // don't carry the key name, the registry does. The validator's only concern
26
- // is: does this raw string belong in this key's value-set?
27
- export type KeyValidator = (rawValue: string) => ValidateResult;
28
-
29
- // [LAW:types-are-the-program] A derived key's SEMANTIC identity — the data an
30
- // action's `set`/`persist` declares about a key, from which the validator is
31
- // residue. A key is one of three key shapes: an integer (a menu's page
32
- // index), an allow-list (the union of values some button can write), or a
33
- // bounded integer range (a stepper's value). The registry compares specs to
34
- // decide whether two registrations can share a key (same `kind`) and merges
35
- // them by unioning content (allow-list members; range bounds); the opaque
36
- // `KeyValidator` it builds from the spec cannot be compared or merged, which
37
- // is why registration takes the spec and owns validator construction.
38
- //
39
- // [LAW:one-source-of-truth] The spec carries only content (kind + allow-list
40
- // members + range bounds), never the human label — the label is a pure
41
- // function of the key, computed where the validator is built, so two
42
- // registrations of one key yield byte-identical validators regardless of
43
- // which config registered first.
44
- export type DerivedValidatorSpec =
45
- | { readonly kind: "int" }
46
- | { readonly kind: "allow-list"; readonly allowed: readonly string[] }
47
- | {
48
- // [LAW:one-source-of-truth] A bounded-integer state key (a stepper's
49
- // value). `min`/`max` gate the value; `seed` is the value an UNSET key
50
- // reads as — sourced from the backing default so the first relative
51
- // click steps from the same number the bar displays (not silently from
52
- // `min`). The validator ignores `seed` (it only clamps); the caller
53
- // reads it via rangeParamsFor when the key is unset.
54
- readonly kind: "range";
55
- readonly min: number;
56
- readonly max: number;
57
- readonly seed: number;
58
- };
59
-
60
- // [LAW:one-source-of-truth] One contribution shape — a (key, spec) pair —
61
- // every action's write declaration projects to. mergeContributions folds a
62
- // list of these into the final per-key validator specs, so multiple actions
63
- // writing one key feed ONE coherence merge regardless of which action
64
- // authored the write.
65
- export interface KeySpecContribution {
66
- readonly key: string;
67
- readonly spec: DerivedValidatorSpec;
68
- }
69
-
70
- const INT_RE = /^-?\d+$/;
71
-
72
- // [LAW:one-source-of-truth] The unset seed for a stepped key is the backing
73
- // default — the SAME number the bar displays before the first click — so the
74
- // first relative step doesn't silently start from `min`. Absent or
75
- // non-integer default falls back to `min` (the historical render-side
76
- // behavior).
77
- export function clampSeed(
78
- seed: number | undefined,
79
- min: number,
80
- max: number,
81
- ): number {
82
- if (seed === undefined) return min;
83
- return Math.max(min, Math.min(max, seed));
84
- }
85
-
86
- // [LAW:one-type-per-behavior] The "values come from list Y" pattern IS the
87
- // canonical widget-config use case (theme picker draws from themes(), style
88
- // picker draws from styles(), a custom enum picker draws from a
89
- // user-declared list). One factory builds the validator from the list —
90
- // every callsite that registers an allow-list key passes through the same
91
- // shape, so error messages, empty-input rejection, and lookup semantics are
92
- // identical by construction.
93
- //
94
- // [LAW:no-silent-fallbacks] Empty input is rejected with a label-referencing
95
- // reason rather than silently mapped to a default.
96
- //
97
- // [LAW:one-source-of-truth] `wire` names the ACTUAL wire this allow-list's
98
- // values travel over — "set-state" for SessionState keys, "set-config" for
99
- // config-overrides keys — so the slash-rejection message points at the wire
100
- // the operator is actually debugging. Defaults to "set-state" (this
101
- // factory's original, sole caller) so existing direct callers (tests) don't
102
- // need to pass it; validatorForSpec passes the correct wire for its noun.
103
- export function makeAllowListValidator(
104
- allowed: readonly string[],
105
- label: string,
106
- wire: string = "set-state",
107
- ): KeyValidator {
108
- // [LAW:types-are-the-program] The factory's contract is "options = allow
109
- // list" — every value the picker can RENDER must also be a value the wire
110
- // can DELIVER. Two structural reasons a declared option can't reach the
111
- // validator as itself: (1) the wire splits the tail on "/"; (2) the
112
- // validator's empty-input rejection fires before the allow-list check, so
113
- // an "" in the allow list would be listed-but-undeliverable. Catching at
114
- // factory-build time (config-load) surfaces a misconfigured option list
115
- // immediately, not on the operator's first click.
116
- const slashOffenders = allowed.filter((v) => v.includes("/"));
117
- if (slashOffenders.length > 0) {
118
- throw new Error(
119
- `makeAllowListValidator(${label}): values contain "/" — the ${wire} ` +
120
- `wire shape splits values on "/" so slash-bearing options cannot ` +
121
- `be addressed. Offending values: ${slashOffenders.join(", ")}`,
122
- );
123
- }
124
- if (allowed.includes("")) {
125
- throw new Error(
126
- `makeAllowListValidator(${label}): empty string is not a writable ` +
127
- `option — the validator rejects empty input before the allow-list ` +
128
- `check, so an "" in the allowed list could be rendered but never ` +
129
- `delivered. Remove "" from the allowed list.`,
130
- );
131
- }
132
- const allowedSet: ReadonlySet<string> = new Set(allowed);
133
- const allowedList = [...allowed];
134
- return (raw) => {
135
- if (!raw) return { ok: false, reason: `${label} value is required` };
136
- if (!allowedSet.has(raw)) {
137
- return {
138
- ok: false,
139
- reason: `unknown ${label} "${raw}" (have: ${allowedList.join(", ")})`,
140
- };
141
- }
142
- return { ok: true, value: raw };
143
- };
144
- }
145
-
146
- // [LAW:types-are-the-program] An integer-valued state key (a menu's page
147
- // index). The wire delivers a string; the validator IS the parse boundary —
148
- // it accepts only `^-?\d+$` and canonicalizes to the minimal decimal form.
149
- // Negative is legal: -1 is the menu's CLOSED sentinel.
150
- export function makeIntValidator(label: string): KeyValidator {
151
- return (raw) => {
152
- if (!raw) return { ok: false, reason: `${label} value is required` };
153
- if (!INT_RE.test(raw)) {
154
- return { ok: false, reason: `${label} must be an integer, got "${raw}"` };
155
- }
156
- const neg = raw[0] === "-";
157
- const digits = (neg ? raw.slice(1) : raw).replace(/^0+/, "");
158
- if (digits === "") return { ok: true, value: "0" };
159
- return { ok: true, value: neg ? `-${digits}` : digits };
160
- };
161
- }
162
-
163
- // [LAW:types-are-the-program] A bounded-integer state key (a stepper's
164
- // value). The validator is the parse-AND-clamp boundary: it accepts only
165
- // `^-?\d+$` then clamps into [min,max]. [LAW:single-enforcer] This is the ONE
166
- // place bounds are enforced.
167
- export function makeRangeValidator(
168
- min: number,
169
- max: number,
170
- label: string,
171
- ): KeyValidator {
172
- return (raw) => {
173
- if (!raw) return { ok: false, reason: `${label} value is required` };
174
- if (!INT_RE.test(raw)) {
175
- return { ok: false, reason: `${label} must be an integer, got "${raw}"` };
176
- }
177
- const clamped = Math.max(min, Math.min(max, parseInt(raw, 10)));
178
- return { ok: true, value: String(clamped) };
179
- };
180
- }
181
-
182
- // [LAW:types-are-the-program] Collapse one key's spec contributions into the
183
- // single spec that gates it. A key is an INTEGER spec (a paged cursor `int`
184
- // or a bounded `range`) or an allow-list — never both. An integer spec
185
- // ABSORBS integer allow-list members (a trigger writing "0" to a page cursor
186
- // is a legal int write), and a NON-integer member aimed at it is the genuine
187
- // contradiction that throws. Two ranges widen-union; two allow-lists union;
188
- // an int and a range on one key conflict.
189
- // [LAW:one-source-of-truth] `noun` ("state"/"config") names the keyspace in
190
- // every thrown message — this function is the SAME merge both
191
- // deriveActionValidators (state-validators.ts) and deriveConfigActionValidators
192
- // (config-validators.ts) call, so a conflict thrown while merging a `persist`
193
- // action's contributions must say "config", never the SessionState-era
194
- // "state" wording (or the operator debugging a persist action gets pointed at
195
- // the wrong keyspace's mental model).
196
- export function mergeKeySpecs(
197
- key: string,
198
- specs: readonly DerivedValidatorSpec[],
199
- noun: string = "state",
200
- ): DerivedValidatorSpec {
201
- type Range = Extract<DerivedValidatorSpec, { kind: "range" }>;
202
- const ranges = specs.filter((s): s is Range => s.kind === "range");
203
- const hasInt = specs.some((s) => s.kind === "int");
204
- const allowed = specs.flatMap((s) =>
205
- s.kind === "allow-list" ? s.allowed : [],
206
- );
207
- if (ranges.length === 0 && !hasInt) {
208
- return { kind: "allow-list", allowed: [...new Set(allowed)] };
209
- }
210
- const nonInt = allowed.filter((v) => !INT_RE.test(v));
211
- if (nonInt.length > 0) {
212
- throw new Error(
213
- `${noun} action table: key "${key}" is an integer spec (a paged ` +
214
- `cursor or a bounded value) but a click writes non-integer ` +
215
- `value(s) to it (${nonInt.join(", ")}). A ${noun} key has one key ` +
216
- `shape — point that click at a distinct key, or write an integer.`,
217
- );
218
- }
219
- if (hasInt && ranges.length > 0) {
220
- throw new Error(
221
- `${noun} action table: key "${key}" is declared as both a paged ` +
222
- `cursor (int) and a bounded value (range) — a ${noun} key has one key ` +
223
- `shape. Use distinct keys.`,
224
- );
225
- }
226
- if (ranges.length > 0) {
227
- const min = Math.min(...ranges.map((r) => r.min));
228
- const max = Math.max(...ranges.map((r) => r.max));
229
- const outOfRange = allowed.filter((v) => {
230
- const n = parseInt(v, 10);
231
- return n < min || n > max;
232
- });
233
- if (outOfRange.length > 0) {
234
- throw new Error(
235
- `${noun} action table: key "${key}" is a bounded range [${min},${max}] ` +
236
- `but a click writes out-of-range value(s) to it ` +
237
- `(${outOfRange.join(", ")}). The range gate would clamp them, storing a ` +
238
- `different value than the click renders — write an in-range integer, ` +
239
- `or point that click at a distinct key.`,
240
- );
241
- }
242
- const seed = clampSeed(ranges[0]!.seed, min, max);
243
- return { kind: "range", min, max, seed };
244
- }
245
- return { kind: "int" };
246
- }
247
-
248
- // [LAW:one-source-of-truth] The click-wire verb name a keyspace's writes
249
- // travel over — "set-state" for the SessionState keyspace, "set-config" for
250
- // config-overrides. Mirrors loader/actions.ts's wireName (same concept, the
251
- // loader's discriminator vocabulary is "set"/"persist" instead of
252
- // "state"/"config").
253
- function wireForNoun(noun: string): string {
254
- return noun === "config" ? "set-config" : "set-state";
255
- }
256
-
257
- // [LAW:types-are-the-program] The validator is RESIDUE of a SETTLED spec:
258
- // given one merged spec, its validator is forced. Pure projection — kind ⇒
259
- // constructor — with NO union or widen of its own. `noun` ("state"/"config")
260
- // is threaded through so the SAME shared projection labels a rejection
261
- // message with the keyspace it actually belongs to — this is the one place
262
- // that builds every validator, so it is the one place that can misname the
263
- // keyspace if the noun doesn't ride along.
264
- function validatorForSpec(
265
- key: string,
266
- spec: DerivedValidatorSpec,
267
- noun: string,
268
- ): KeyValidator {
269
- if (spec.kind === "int") return makeIntValidator(`menu page "${key}"`);
270
- if (spec.kind === "range")
271
- return makeRangeValidator(spec.min, spec.max, `${noun} stepper "${key}"`);
272
- return makeAllowListValidator(
273
- spec.allowed,
274
- `${noun} "${key}"`,
275
- wireForNoun(noun),
276
- );
277
- }
278
-
279
- function buildValidatorFromSpecs(
280
- key: string,
281
- specs: readonly DerivedValidatorSpec[],
282
- noun: string,
283
- ): KeyValidator {
284
- return validatorForSpec(key, mergeKeySpecs(key, specs, noun), noun);
285
- }
286
-
287
- // [LAW:single-enforcer] THE coherence merge: group every contribution by key
288
- // and collapse each key's specs into the one spec that gates it. `noun`
289
- // names the keyspace (default "state" — the original, sole caller before
290
- // config-validators.ts's twin) so a conflict thrown mid-merge for a
291
- // `persist` action's contributions names the config keyspace, not state.
292
- export function mergeContributions(
293
- contributions: readonly KeySpecContribution[],
294
- noun: string = "state",
295
- ): KeySpecContribution[] {
296
- const byKey = new Map<string, DerivedValidatorSpec[]>();
297
- for (const { key, spec } of contributions) {
298
- const specs = byKey.get(key);
299
- if (specs) specs.push(spec);
300
- else byKey.set(key, [spec]);
301
- }
302
- return [...byKey].map(([key, specs]) => ({
303
- key,
304
- spec: mergeKeySpecs(key, specs, noun),
305
- }));
306
- }
307
-
308
- export interface RangeParams {
309
- readonly min: number;
310
- readonly max: number;
311
- readonly seed: number;
312
- }
313
-
314
- interface BaselineEntry {
315
- readonly permanent: true;
316
- readonly validator: KeyValidator;
317
- }
318
- interface DerivedEntry {
319
- readonly permanent: false;
320
- readonly kind: DerivedValidatorSpec["kind"];
321
- validator: KeyValidator;
322
- readonly specs: DerivedValidatorSpec[];
323
- }
324
- type ValidatorEntry = BaselineEntry | DerivedEntry;
325
-
326
- export interface ValidatorRegistry {
327
- register(key: string, spec: DerivedValidatorSpec): () => void;
328
- validate(key: string, rawValue: string): ValidateResult;
329
- listKeys(): readonly string[];
330
- // [LAW:one-source-of-truth] The permanent/baseline subset of listKeys() —
331
- // exposed so a consumer that needs to distinguish "derived from an action
332
- // table" from "always writable" (e.g. dropping an allow-list contribution
333
- // aimed at a baseline key) reads it from the registry that owns the
334
- // distinction, rather than re-declaring the baseline set as a second list.
335
- listBaselineKeys(): readonly string[];
336
- rangeParamsFor(key: string): RangeParams | null;
337
- }
338
-
339
- // [LAW:one-type-per-behavior] ONE registry implementation, instantiated once
340
- // per keyspace. `baseline` seeds PERMANENT entries (raw KeyValidator
341
- // functions, never re-claimable — SessionState's legacy theme/style/
342
- // toolbar-expanded); an empty baseline (config-overrides' keyspace) means
343
- // every key is fully derived from the action table, exactly the epic's
344
- // "zero engine edits to add a menu-able field" goal. `noun` names the
345
- // keyspace in every message ("state"/"config") so the two instances stay
346
- // operator-distinguishable — a "state key" and "config key" error can never
347
- // be confused for the other keyspace's gate.
348
- //
349
- // [LAW:no-silent-fallbacks] An unknown key is a caller-visible rejection
350
- // (validate) or a loud throw (a baseline re-claim, a kind clash) — never a
351
- // silent accept-and-store.
352
- export function createValidatorRegistry(
353
- baseline: Readonly<Record<string, KeyValidator>>,
354
- noun: string = "state",
355
- ): ValidatorRegistry {
356
- const entries = new Map<string, ValidatorEntry>(
357
- Object.entries(baseline).map(([key, validator]) => [
358
- key,
359
- { validator, permanent: true } as const,
360
- ]),
361
- );
362
-
363
- function baselineKeys(): readonly string[] {
364
- const out: string[] = [];
365
- for (const [key, entry] of entries) if (entry.permanent) out.push(key);
366
- return out;
367
- }
368
-
369
- return {
370
- register(key, spec) {
371
- if (!key) throw new Error("register: key is required");
372
- // [LAW:types-are-the-program] The set-state wire splits its tail on
373
- // `/`, so a slash-bearing key can never be addressed — listing it
374
- // would be registry-vs-wire drift. Reject at registration so the
375
- // unreachable-but-listed state is unrepresentable.
376
- if (key.includes("/")) {
377
- throw new Error(
378
- `register: key "${key}" contains "/" — the wire shape splits on ` +
379
- `"/" so a slash-bearing key cannot be addressed. Use a slash-free key.`,
380
- );
381
- }
382
- const existing = entries.get(key);
383
- if (existing) {
384
- if (existing.permanent) {
385
- throw new Error(
386
- `register: key "${key}" is a built-in ${noun} key and cannot be ` +
387
- `re-claimed (built-in keys: ${[...baselineKeys()].join(", ")})`,
388
- );
389
- }
390
- if (existing.kind !== spec.kind) {
391
- throw new Error(
392
- `register: key "${key}" is already a ${existing.kind} ${noun} key; ` +
393
- `cannot also register it as ${spec.kind}. A ${noun} key has one ` +
394
- `key shape — a menu page index (int) and a button allow-list ` +
395
- `cannot share a key.`,
396
- );
397
- }
398
- existing.specs.push(spec);
399
- existing.validator = buildValidatorFromSpecs(key, existing.specs, noun);
400
- } else {
401
- const specs = [spec];
402
- entries.set(key, {
403
- permanent: false,
404
- kind: spec.kind,
405
- validator: buildValidatorFromSpecs(key, specs, noun),
406
- specs,
407
- });
408
- }
409
- let active = true;
410
- return () => {
411
- if (!active) return;
412
- active = false;
413
- const entry = entries.get(key);
414
- if (!entry || entry.permanent) return;
415
- const i = entry.specs.indexOf(spec);
416
- if (i >= 0) entry.specs.splice(i, 1);
417
- if (entry.specs.length === 0) {
418
- entries.delete(key);
419
- } else {
420
- entry.validator = buildValidatorFromSpecs(key, entry.specs, noun);
421
- }
422
- };
423
- },
424
-
425
- validate(key, rawValue) {
426
- const entry = entries.get(key);
427
- if (!entry) {
428
- return {
429
- ok: false,
430
- reason: `unknown ${noun} key "${key}" (have: ${[...entries.keys()].join(", ")})`,
431
- };
432
- }
433
- return entry.validator(rawValue);
434
- },
435
-
436
- listKeys() {
437
- return [...entries.keys()];
438
- },
439
-
440
- listBaselineKeys() {
441
- return baselineKeys();
442
- },
443
-
444
- rangeParamsFor(key) {
445
- const entry = entries.get(key);
446
- if (!entry || entry.permanent || entry.kind !== "range") return null;
447
- const spec = mergeKeySpecs(key, entry.specs, noun);
448
- if (spec.kind !== "range") {
449
- throw new Error(
450
- `rangeParamsFor: key "${key}" holds range specs but the merge ` +
451
- `produced a ${spec.kind} spec — the entry-kind invariant is broken.`,
452
- );
453
- }
454
- return { min: spec.min, max: spec.max, seed: spec.seed };
455
- },
456
- };
457
- }
package/src/demo/dsl.ts DELETED
@@ -1,143 +0,0 @@
1
- // Minimal end-to-end demo of the segment DSL render spine.
2
- //
3
- // pnpm demo:dsl # renders src/demo/statusline.json5
4
- // pnpm demo:dsl path/to/other.json5 # renders any DSL config
5
- //
6
- // [LAW:single-enforcer] This renders through registerDslConfig + renderDsl
7
- // — the exact spine the daemon calls. There is no demo-only render path; what
8
- // prints here is what production produces.
9
- //
10
- // [LAW:dataflow-not-control-flow] The body is straight-line: read config →
11
- // register → render frames → dispose. The config file and payload are data;
12
- // swapping either changes the output without changing this code. Rendering N
13
- // frames over time is not branching — it lets the asynchronous sources (shell,
14
- // time) populate the store and shows the line come alive, exactly as the daemon
15
- // re-renders on each status-line tick.
16
-
17
- import { readFileSync } from "node:fs";
18
- import { fileURLToPath } from "node:url";
19
- import { dirname, join } from "node:path";
20
- import process from "node:process";
21
- import { setTimeout as sleep } from "node:timers/promises";
22
-
23
- import {
24
- parseDslConfig,
25
- mergeWithDefault,
26
- validateConfig,
27
- } from "../config/dsl-loader.js";
28
- import { DEFAULT_DSL_CONFIG } from "../config/default-dsl-config.js";
29
- import { VariableStore } from "../var-system/store.js";
30
- import { SourceRegistry } from "../var-system/sources.js";
31
- import { SessionState } from "../daemon/session-state.js";
32
- import { listResolvablePaletteNames } from "../themes/policy.js";
33
- import { lookKeyByName, paletteForThemeName } from "../themes/index.js";
34
- import { resolveEffectiveGlobals } from "../daemon/render-payload.js";
35
- import { registerDslConfig, renderDsl } from "../dsl/render.js";
36
- import { DEFAULT_TERMINAL_WIDTH } from "../render/strip.js";
37
- import { applyClaudeCodeReserve } from "../utils/terminal-width.js";
38
-
39
- const FRAMES = 4;
40
- const FRAME_INTERVAL_MS = 450;
41
-
42
- const here = dirname(fileURLToPath(import.meta.url));
43
- const configPath = process.argv[2] ?? join(here, "statusline.json5");
44
- const source = readFileSync(configPath, "utf-8");
45
-
46
- // [LAW:one-source-of-truth] The palette names the loader accepts are exactly
47
- // the names the renderer can resolve — both derive from the same registry, so
48
- // we hand the loader the live set rather than a hand-maintained copy.
49
- //
50
- // Full three-stage pipeline: parse → merge → validate. The renderer accepts
51
- // only `ValidatedConfig`, so the chain is type-enforced.
52
- const ALLOWED = new Set(listResolvablePaletteNames());
53
- const raw = parseDslConfig(configPath, source, ALLOWED);
54
- const merged = mergeWithDefault(raw, DEFAULT_DSL_CONFIG);
55
- const config = validateConfig(merged, configPath, source, ALLOWED);
56
-
57
- // One Claude Code status-line hook event, faked. The `input` vars in the
58
- // config (cwd, model, session) read their values out of this object.
59
- const payload = {
60
- hook_event_name: "Status",
61
- session_id: "demo0a1b-2c3d-4e5f-6a7b-8c9d0e1f2a3b",
62
- cwd: process.cwd(),
63
- model: { id: "claude-opus-4-7", display_name: "Opus 4.7" },
64
- workspace: {
65
- current_dir: process.cwd(),
66
- project_dir: process.cwd(),
67
- },
68
- };
69
-
70
- // The demo has no SessionState, so every resolution below is the config default
71
- // over its floor. The PRESET resolves first — its fragment supplies the display
72
- // globals every other option reads — the same preset-first order server.ts and
73
- // check.ts resolve in, so the demo prints the arrangement a fresh session opens
74
- // in.
75
- // [LAW:one-source-of-truth] THE daemon's resolver, not a mirror of it — a
76
- // fresh-session pick reader (null for every key) and no overrides log to be
77
- // customized by. The demo previously restated this chain field by field and had
78
- // already drifted: it hardcoded `style: "powerline"` below and so ignored a
79
- // config's own `globals.style`.
80
- const effective = resolveEffectiveGlobals(
81
- config,
82
- () => null,
83
- () => false,
84
- );
85
- const basePalette = paletteForThemeName(effective.theme);
86
- const lookKey = lookKeyByName(config.looks, effective.look);
87
-
88
- // A fresh store + registry for this run. (A hot-reloading daemon would
89
- // dispose() the old pair and build new ones — see registerDslConfig's docs.)
90
- // registerDslConfig wires the time/shell sources' timers and watchers onto the
91
- // registry, so dispose() must run even if registration or rendering throws —
92
- // otherwise those handles keep the process alive. try/finally guarantees it.
93
- const store = new VariableStore();
94
- // [LAW:no-silent-failure] An EMPTY SessionState — `kind: "state"` variables
95
- // (the default config's style picker, any interactive config) require one at
96
- // registration; without it declareState fails and the segment renders an error
97
- // cell. The demo never clicks, so an empty store is correct: every state var
98
- // resolves to its declared default (closed pickers, "(default)" labels).
99
- const registry = new SourceRegistry(store, "", undefined, new SessionState());
100
- try {
101
- const compiled = registerDslConfig(config, registry, {
102
- cwd: process.cwd(),
103
- });
104
-
105
- process.stdout.write(
106
- `\n DSL demo — ${configPath}\n` +
107
- ` rendered through registerDslConfig + renderDsl (the daemon's spine)\n` +
108
- ` watch the git branch segment appear and the clock tick:\n\n`,
109
- );
110
-
111
- for (let frame = 0; frame < FRAMES; frame++) {
112
- const line = renderDsl(
113
- config,
114
- compiled,
115
- store,
116
- registry,
117
- payload,
118
- basePalette,
119
- {
120
- style: effective.style,
121
- separator: effective.separator,
122
- colorCompatibility: effective.colorCompatibility,
123
- // [LAW:one-source-of-truth] Demo applies the same Claude-Code-UI
124
- // reserve the daemon does so demo output matches the bytes a real
125
- // statusline would emit at the same terminal width.
126
- width: applyClaudeCodeReserve(
127
- process.stdout.columns ?? DEFAULT_TERMINAL_WIDTH,
128
- ),
129
- wrap: effective.autoWrap,
130
- padding: effective.padding,
131
- charset: effective.charset,
132
- },
133
- undefined,
134
- { look: lookKey, preset: effective.preset },
135
- );
136
- process.stdout.write(` ${line}\n`);
137
- if (frame < FRAMES - 1) await sleep(FRAME_INTERVAL_MS);
138
- }
139
-
140
- process.stdout.write("\n");
141
- } finally {
142
- registry.dispose();
143
- }
@@ -1,67 +0,0 @@
1
- export interface MockSegment {
2
- type: string;
3
- text: string;
4
- }
5
-
6
- export interface MockSample {
7
- name: string;
8
- segments: MockSegment[];
9
- }
10
-
11
- export const MOCK_SAMPLES: MockSample[] = [
12
- {
13
- name: "Default Session",
14
- segments: [
15
- { type: "directory", text: "~/projects/my-app" },
16
- { type: "git", text: "main ✗" },
17
- { type: "model", text: "Claude Sonnet 4" },
18
- { type: "session", text: "$2.34 · 45k tok" },
19
- { type: "context", text: "38%" },
20
- ],
21
- },
22
- {
23
- name: "Heavy Usage",
24
- segments: [
25
- { type: "directory", text: "~/work/api-server" },
26
- { type: "git", text: "feat/auth ✗ +3" },
27
- { type: "model", text: "Claude Opus 4" },
28
- { type: "session", text: "$12.87 · 156k tok" },
29
- { type: "context", text: "72%" },
30
- { type: "block", text: "blk #3 $4.20" },
31
- { type: "today", text: "today $18.50" },
32
- ],
33
- },
34
- {
35
- name: "Critical Context",
36
- segments: [
37
- { type: "directory", text: "~/big-project" },
38
- { type: "git", text: "main ✓" },
39
- { type: "model", text: "Claude Opus 4" },
40
- { type: "session", text: "$45.20 · 198k tok" },
41
- { type: "contextCritical", text: "95% ⚠" },
42
- { type: "metrics", text: "1.2s 42msg" },
43
- ],
44
- },
45
- {
46
- name: "Full Segments",
47
- segments: [
48
- { type: "directory", text: "~/code/toolkit" },
49
- { type: "git", text: "fix/bug-123 ↑2 ↓1" },
50
- { type: "model", text: "Claude Sonnet 4" },
51
- { type: "session", text: "$5.67 · 78k tok" },
52
- { type: "block", text: "blk #7 $1.23" },
53
- { type: "today", text: "today $22.10" },
54
- { type: "context", text: "45%" },
55
- { type: "metrics", text: "0.8s 89msg +420 -180" },
56
- { type: "version", text: "v1.2.3" },
57
- { type: "weekly", text: "wk $87.30" },
58
- ],
59
- },
60
- {
61
- name: "Minimal",
62
- segments: [
63
- { type: "directory", text: "~" },
64
- { type: "model", text: "Claude" },
65
- ],
66
- },
67
- ];