@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,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
|
-
}
|
package/src/demo/mock-data.ts
DELETED
|
@@ -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
|
-
];
|