@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,393 +0,0 @@
1
- // [LAW:single-enforcer] The vocabulary and the resolution for every `globals`
2
- // field a config can set or a click can pick: what values each field admits,
3
- // what its floor is, and the one function that turns a session pick, a config
4
- // default and that floor into the value a render actually uses.
5
- //
6
- // It sits in the themes module because theme/look/style were the first three
7
- // fields to need it, and it is a LEAF: the config loader (validation, JSON
8
- // schema) and the render layer both import it, which is what keeps a
9
- // config↔render cycle from forming [LAW:one-way-deps].
10
- //
11
- // No color arithmetic lives here. cc-candybar selects theme NAMES and style
12
- // IDENTIFIERS; every color *value* operation (hydrate hex, resolve specs,
13
- // darken/contrast, hue/transpose) lives in rich-js, as does the semantic/anchor
14
- // knowledge of which tokens keep their hue (ANCHORED_ROOTS).
15
-
16
- import {
17
- listThemePalettes,
18
- type ColorSystemSpec,
19
- type ThemeKey,
20
- } from "@promptctl/rich-js";
21
-
22
- // --- Theme name aliasing ---
23
-
24
- const THEME_ALIASES: Record<string, string> = {
25
- dark: "textual-dark",
26
- light: "textual-light",
27
- };
28
-
29
- export function resolvePaletteName(name: string): string {
30
- return THEME_ALIASES[name] ?? name;
31
- }
32
-
33
- // --- The one globals resolution ---
34
-
35
- // [LAW:one-type-per-behavior] THE resolution every globals field a click can
36
- // pick shares: the session's own value, over the config default, over a floor.
37
- // Written once because the fields differ only in DATA — which floor, and how a
38
- // raw SessionState string becomes a value of that field's type. Theme, look,
39
- // preset, style, autoWrap and padding are all this function with different
40
- // arguments, so the precedence order cannot land on one field and miss another.
41
- //
42
- // [LAW:dataflow-not-control-flow] The `??` chain IS the precedence; there is no
43
- // "does this session have one" branch. A session that has never clicked passes
44
- // null and lands on the config default by the same code path a session that
45
- // clicked lands on its pick.
46
- //
47
- // [LAW:parse-dont-validate] `parseSession` is the boundary between an untyped
48
- // SessionState string and this field's domain: it returns the typed value or
49
- // null, and null means "no session pick" — indistinguishable, on purpose, from
50
- // never having clicked. That is what makes a stale entry from a prior config's
51
- // vocabulary (or a value a since-narrowed gate would now refuse) resolve to the
52
- // default rather than throw or render something the label disagrees with
53
- // [LAW:no-silent-failure] — the caller publishes what this returns as
54
- // `<field>.effective`, so bar and label always trace to one value.
55
- //
56
- // [LAW:one-source-of-truth] `staged` is the RIGHTMOST rung of the precedence
57
- // chain documented in src/config/presets.ts — a fragment some transient MODE of
58
- // the bar puts on top while it is on (today: edit mode's `editGlobals`). It
59
- // outranks even the session pick because it is decided LATER: a user picks a
60
- // style, then afterwards enters edit mode. That is the same lifetime rule that
61
- // forced the preset's own position, applied one rung further along, which is
62
- // why it is a parameter of THIS function rather than a check at any call site —
63
- // a chain with a rung missing from one field is exactly the drift a single
64
- // resolver exists to prevent.
65
- //
66
- // [LAW:dataflow-not-control-flow] Absent ⇒ `undefined`, which the `??` chain
67
- // skips by the same code path a session that never clicked skips its own rung.
68
- // There is no "is edit mode on" branch anywhere below this line; the mode is
69
- // carried entirely by whether this argument has a value.
70
- export function effectiveGlobal<T>(
71
- staged: T | null | undefined,
72
- sessionPick: string | null,
73
- configDefault: T | null | undefined,
74
- floor: T,
75
- parseSession: (raw: string) => T | null,
76
- ): T {
77
- const picked = sessionPick === null ? null : parseSession(sessionPick);
78
- return staged ?? picked ?? configDefault ?? floor;
79
- }
80
-
81
- // The theme name a render should use, as data.
82
- // [LAW:one-source-of-truth] The single definition of "which theme is effective";
83
- // every render derives basePalette through this, so the rendered palette can
84
- // never disagree with the chosen theme. The theme domain is OPEN — registry
85
- // names, aliases, and per-session sentinels all resolve downstream — so its
86
- // parse is identity: there is no membership to check here, and pretending
87
- // otherwise would collapse names `paletteForThemeName` handles fine.
88
- export function effectiveThemeName(
89
- stagedPalette: string | undefined,
90
- sessionTheme: string | null,
91
- globalsPalette: string | undefined,
92
- ): string {
93
- return effectiveGlobal(
94
- stagedPalette,
95
- sessionTheme,
96
- globalsPalette,
97
- "textual-dark",
98
- (raw) => raw,
99
- );
100
- }
101
-
102
- function listThemeAliases(): readonly string[] {
103
- return Object.keys(THEME_ALIASES);
104
- }
105
-
106
- // [LAW:one-source-of-truth] The set of names that resolve to a concrete Palette
107
- // is exactly registry names ∪ aliases — the same inputs resolvePaletteName +
108
- // getThemePalette accept. "custom" and "random" are deliberately absent: neither
109
- // names a concrete palette (custom needs inline colors; random is a per-session
110
- // sentinel). Config validators that gate a palette PULL (DSL `palette:` field)
111
- // must reuse this, not re-derive it.
112
- export function listResolvablePaletteNames(): readonly string[] {
113
- return [...listThemePalettes(), ...listThemeAliases()];
114
- }
115
-
116
- // --- Per-config member selection ---
117
-
118
- // The `effectiveGlobal` instance for every selection whose domain is PER-CONFIG
119
- // (declared in the config, not a registry-static list). `looks` and `presets`
120
- // are both exactly this — what differs between them is only the floor name and
121
- // which map holds the members, i.e. DATA.
122
- //
123
- // The config default runs through the SAME membership parse the session pick
124
- // does, one layer down: a `look:`/`preset:` naming a member the config no longer
125
- // declares is no default at all, and collapses to the floor exactly as a stale
126
- // session pick does. The loader cannot catch that for a per-config domain, so
127
- // this resolution is where it is caught.
128
- //
129
- // The floor's membership is a load-time guarantee, not a runtime hope: the
130
- // bundled stdlib ships it and merge-by-name cannot remove it.
131
- //
132
- // The staged rung runs through the SAME membership parse the other two do: a
133
- // fragment naming a member the config does not declare is no more a pick than a
134
- // stale session entry is, and collapses one rung onward rather than throwing.
135
- export function effectiveMemberName(
136
- stagedName: string | undefined,
137
- sessionPick: string | null,
138
- configDefault: string | undefined,
139
- floor: string,
140
- declared: Readonly<Record<string, unknown>>,
141
- ): string {
142
- const member = (raw: string): string | null =>
143
- Object.prototype.hasOwnProperty.call(declared, raw) ? raw : null;
144
- return effectiveGlobal(
145
- stagedName === undefined ? null : member(stagedName),
146
- sessionPick,
147
- member(configDefault ?? floor),
148
- floor,
149
- member,
150
- );
151
- }
152
-
153
- // --- Look (theme-adaptation) identifiers ---
154
-
155
- // [LAW:one-source-of-truth] The look domain's instance of the shared resolver
156
- // above — the floor is `"none"`, the identity adaptation every merged config
157
- // carries. A named wrapper (not a bare call at each site) so the floor is
158
- // spelled once and the three call sites cannot disagree about it.
159
- export function effectiveLookName(
160
- stagedLook: string | undefined,
161
- sessionLook: string | null,
162
- globalsLook: string | undefined,
163
- declaredLooks: Readonly<Record<string, ThemeKey>>,
164
- ): string {
165
- return effectiveMemberName(
166
- stagedLook,
167
- sessionLook,
168
- globalsLook,
169
- "none",
170
- declaredLooks,
171
- );
172
- }
173
-
174
- // [LAW:single-enforcer] The one place an effective look NAME becomes the
175
- // ThemeKey a render transposes with. By the time a name reaches here it must be
176
- // a member: effectiveLookName collapses unknown names to the "none" floor, and
177
- // mergeWithDefault guarantees the bundled "none" exists in every DslConfig.
178
- // [LAW:no-defensive-null-guards] the throw is the loud failure for that broken
179
- // invariant (a hand-built config missing the stdlib), never a silent identity
180
- // fallback that would hide the drift.
181
- export function lookKeyByName(
182
- looks: Readonly<Record<string, ThemeKey>>,
183
- name: string,
184
- ): ThemeKey {
185
- const key = looks[name];
186
- if (key === undefined) {
187
- throw new Error(
188
- `Look "${name}" is not declared in this config — effectiveLookName ` +
189
- `collapses unknown names to "none", and every merged config carries ` +
190
- `"none"; a miss here is merge/policy drift`,
191
- );
192
- }
193
- return key;
194
- }
195
-
196
- // --- Powerline strip-style identifiers ---
197
-
198
- // [LAW:one-source-of-truth][LAW:types-are-the-program] The single canonical set
199
- // of powerline cap/separator shapes a render can take. The `StripStyle` type is
200
- // DERIVED from this const, so the picker's option domain, the SessionState
201
- // validator, the `styles()` template binding, and `pickJoiner`'s dispatch all
202
- // trace to one literal — adding a shape here forces a new `pickJoiner` arm at
203
- // compile time (the joiner switch is total over `StripStyle`). This is where the
204
- // drift between "what you can pick" and "what actually renders" is closed.
205
- export const STRIP_STYLES = ["powerline", "capsule", "plain"] as const;
206
- export type StripStyle = (typeof STRIP_STYLES)[number];
207
-
208
- // [LAW:types-are-the-program] The trust-boundary narrowing from a raw
209
- // SessionState string (or a config default) to the closed `StripStyle` union.
210
- export function isStripStyle(value: string): value is StripStyle {
211
- return (STRIP_STYLES as readonly string[]).includes(value);
212
- }
213
-
214
- // The strip style a render should use, as data.
215
- // [LAW:one-type-per-behavior] `effectiveGlobal` over a closed registry-static
216
- // vocabulary: the narrowing guard IS the parse. `pickJoiner` would render an
217
- // unknown style as powerline anyway; parsing here keeps the returned TYPE
218
- // honest rather than silently widening it.
219
- //
220
- // A stale SessionState entry (a member of a prior option vocabulary) is an
221
- // ABSENT session pick, not a pick of the floor: it falls through to the config
222
- // default, and only reaches "powerline" when the config declares no style
223
- // either. That is a deliberate change from the pre-`effectiveGlobal` spelling,
224
- // which collapsed straight to the floor and skipped the user's own declared
225
- // default — a config saying `style: "capsule"` deserves capsule when a session
226
- // entry goes stale, not powerline. Every field here now shares that one rule
227
- // [LAW:one-source-of-truth]; test/session-globals.test.ts pins it with a stale
228
- // pick over a valid non-floor default, the case the old tests never exercised.
229
- export function effectiveStripStyle(
230
- stagedStyle: StripStyle | undefined,
231
- sessionStyle: string | null,
232
- globalsStyle: StripStyle | undefined,
233
- ): StripStyle {
234
- return effectiveGlobal(
235
- stagedStyle,
236
- sessionStyle,
237
- globalsStyle,
238
- "powerline",
239
- (raw) => (isStripStyle(raw) ? raw : null),
240
- );
241
- }
242
-
243
- // --- Joiner charset identifiers ---
244
-
245
- // [LAW:one-source-of-truth][LAW:types-are-the-program] The single canonical set
246
- // of glyph vocabularies the strip joiners can render with (the legacy
247
- // display.charset). Same species as STRIP_STYLES — a closed render-vocabulary
248
- // enum hosted in this leaf policy module so the config loader (validation +
249
- // JSON-schema emit) and the render layer (glyph dispatch) both derive from one
250
- // literal without a config↔render cycle [LAW:one-way-deps]. "ascii" swaps the
251
- // powerline-private-use cap glyphs (U+E0Bx — tofu without a Nerd Font) for
252
- // plain-ASCII equivalents; it is orthogonal to StripStyle: style picks the
253
- // joiner SHAPE, charset picks the glyph VALUES fed to it.
254
- // [config-only] Unlike STRIP_STYLES there is no SessionState/click half, so no
255
- // narrowing guard or effective* resolver — the config global over the default
256
- // is the whole resolution. That is a decision, not a gap: charset describes the
257
- // TERMINAL (does its font carry the powerline private-use glyphs), not a taste.
258
- // It does not vary session-to-session on one machine, so a per-session override
259
- // would be a knob whose only honest setting is the one already in the config.
260
- // Same for COLOR_COMPATIBILITIES below.
261
- export const CHARSETS = ["unicode", "ascii"] as const;
262
- export type Charset = (typeof CHARSETS)[number];
263
-
264
- // --- Color-depth identifiers ---
265
-
266
- // [LAW:one-source-of-truth][LAW:types-are-the-program] The single canonical set
267
- // of color depths a config can pin (the legacy display.colorCompatibility).
268
- // Same species as CHARSETS: a closed render-vocabulary enum hosted in this leaf
269
- // policy module so the config loader (validation + JSON-schema emit) and the
270
- // render layer both derive from one literal without a config↔render cycle
271
- // [LAW:one-way-deps]. `satisfies` ties every member to rich-js's
272
- // ColorSystemSpec at compile time WITHOUT widening the derived union — if
273
- // rich-js renames a depth, this literal fails to compile rather than drifting.
274
- //
275
- // Deliberately NARROWER than ColorSystemSpec: "auto" (and null) are excluded.
276
- // The daemon is long-lived and detached, so its process env is NOT the client
277
- // terminal's — rich-js env detection would silently downsample against the
278
- // wrong terminal [LAW:no-silent-failure]. Honoring "auto" needs a client
279
- // capability hint over the wire (the termCols pattern); until that lands, the
280
- // loader rejects "auto" with a pointer instead of shipping a lie.
281
- export const COLOR_COMPATIBILITIES = [
282
- "truecolor",
283
- "256",
284
- "ansi",
285
- "none",
286
- ] as const satisfies readonly ColorSystemSpec[];
287
- export type ColorCompatibility = (typeof COLOR_COMPATIBILITIES)[number];
288
-
289
- // --- Layout globals (autoWrap, padding) ---
290
- //
291
- // These two DO have a session half, unlike charset/colorCompatibility above:
292
- // wrapping and cell padding are how much bar you want on your screen right now
293
- // — a taste that legitimately differs between one session in a wide terminal
294
- // and another in a split pane. Their floors and domains live here, beside the
295
- // other globals vocabularies, because both the config loader (range validation,
296
- // JSON-schema emit) and the render layer need them and config must not import
297
- // render [LAW:one-way-deps]. src/render/strip.ts re-exports them so render-layer
298
- // callers keep their existing import site.
299
-
300
- // [LAW:one-source-of-truth] The one statement of the globals.autoWrap default
301
- // (on — current behavior).
302
- export const DEFAULT_WRAP = true;
303
-
304
- // [LAW:one-source-of-truth] The spelling of a boolean as a SessionState string.
305
- // SessionState holds strings, so "true"/"false" is the wire vocabulary for every
306
- // boolean globals field — the same two members the bundled default's
307
- // `cycle: [...]` toggle writes and the parse below reads. Spelled once so a
308
- // toggle cannot write a member the resolver refuses to parse.
309
- // [LAW:one-source-of-truth] The two members, named, because their ORDER is
310
- // meaningful and differs per control: a cycle's members are ordered
311
- // default-state-first (an unwritten key counts as the first member and clicks
312
- // to the second), so `autoWrap` — on by default — cycles ["true","false"]
313
- // while `persist?` — off by default — cycles [BOOLEAN_FALSE, BOOLEAN_TRUE].
314
- // Spelling the members rather than reversing the pair keeps each declaration's
315
- // default state readable at its own site.
316
- export const BOOLEAN_TRUE = "true";
317
- export const BOOLEAN_FALSE = "false";
318
- export const BOOLEAN_MEMBERS = [BOOLEAN_TRUE, BOOLEAN_FALSE] as const;
319
-
320
- // [LAW:one-source-of-truth] The one statement of the globals.padding default
321
- // (one space per side inside each segment cell — current behavior, matching the
322
- // legacy display.padding).
323
- export const DEFAULT_PADDING = 1;
324
-
325
- // [LAW:one-source-of-truth] THE padding domain: an integer, inclusive both ends.
326
- // Read by the loader's `padding` int spec (config-file values), by the bundled
327
- // default's stepper actions (`min`/`max`, which bound what a click may persist),
328
- // and by the session parse below. When those were three copies of `0`/`16`, a
329
- // widened range could land on the file and miss the clicks.
330
- export const PADDING_RANGE = { min: 0, max: 16 } as const;
331
-
332
- // [LAW:parse-dont-validate] A SessionState string to a boolean, or null for
333
- // anything else. `??` in effectiveGlobal (never `||`) is what keeps a parsed
334
- // `false` a real answer rather than falling through to the default.
335
- //
336
- // [LAW:one-source-of-truth] Exported because SessionState holds strings and
337
- // BOOLEAN_MEMBERS above is the one spelling of a boolean in that store — so
338
- // every reader of a boolean session key parses it HERE, not with its own
339
- // truthiness rule. The second reader is the dual-destination action's
340
- // `persistWhen` selector (src/render/action.ts): "is persist? checked" is the
341
- // same question `autoWrap`'s toggle asks of its own key, and a bespoke
342
- // `raw !== ""` there would accept values this parse rejects.
343
- export function parseSessionBoolean(raw: string): boolean | null {
344
- return raw === BOOLEAN_TRUE ? true : raw === BOOLEAN_FALSE ? false : null;
345
- }
346
-
347
- // [LAW:parse-dont-validate] A SessionState string to a padding value inside the
348
- // one declared range. The digits test comes first because `Number("")` is 0 and
349
- // `Number(" 3 ")` is 3 — an empty or padded entry would otherwise parse to a
350
- // value nobody wrote.
351
- function parsePadding(raw: string): number | null {
352
- if (!/^\d+$/.test(raw)) return null;
353
- const value = Number(raw);
354
- return value >= PADDING_RANGE.min && value <= PADDING_RANGE.max
355
- ? value
356
- : null;
357
- }
358
-
359
- // Whether a render wraps over-wide rows, as data. The session's pick over the
360
- // config default over the on floor — `effectiveGlobal` with a boolean domain.
361
- export function effectiveAutoWrap(
362
- stagedAutoWrap: boolean | undefined,
363
- sessionAutoWrap: string | null,
364
- globalsAutoWrap: boolean | undefined,
365
- ): boolean {
366
- return effectiveGlobal(
367
- stagedAutoWrap,
368
- sessionAutoWrap,
369
- globalsAutoWrap,
370
- DEFAULT_WRAP,
371
- parseSessionBoolean,
372
- );
373
- }
374
-
375
- // The intra-cell padding a render uses, as data. A session value outside
376
- // PADDING_RANGE falls through to the config default, the same rule every other
377
- // field here follows: the gate already refuses out-of-range clicks, so a value
378
- // that gets here is a stale entry from a narrower-since range, and the user's
379
- // own default is the honest answer rather than a render at a width nobody
380
- // chose.
381
- export function effectivePadding(
382
- stagedPadding: number | undefined,
383
- sessionPadding: string | null,
384
- globalsPadding: number | undefined,
385
- ): number {
386
- return effectiveGlobal(
387
- stagedPadding,
388
- sessionPadding,
389
- globalsPadding,
390
- DEFAULT_PADDING,
391
- parsePadding,
392
- );
393
- }
@@ -1,206 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { createHash } from "node:crypto";
4
- import { setTimeout } from "node:timers/promises";
5
- import { debug } from "./logger";
6
- import { cacheDir } from "../daemon/paths";
7
-
8
- interface ErrnoError extends Error {
9
- code?: string;
10
- }
11
-
12
- export interface CacheEntry<T> {
13
- data: T;
14
- timestamp: number;
15
- }
16
-
17
- export class CacheManager {
18
- private static readonly CACHE_DIR = cacheDir();
19
- private static readonly USAGE_CACHE_DIR = path.join(this.CACHE_DIR, "usage");
20
- private static readonly LOCKS_DIR = path.join(this.CACHE_DIR, "locks");
21
-
22
- private static isLocked(name: string): boolean {
23
- const lockFile = path.join(this.LOCKS_DIR, name);
24
- if (!fs.existsSync(lockFile)) {
25
- return false;
26
- }
27
-
28
- try {
29
- const lockContent = fs.readFileSync(lockFile, "utf-8");
30
- const pid = parseInt(lockContent.trim(), 10);
31
-
32
- if (isNaN(pid)) {
33
- debug(`Invalid PID in lock file ${name}, removing stale lock`);
34
- fs.unlinkSync(lockFile);
35
- return false;
36
- }
37
-
38
- try {
39
- process.kill(pid, 0);
40
- return true;
41
- } catch (error) {
42
- if ((error as ErrnoError).code === "ESRCH") {
43
- debug(`Removing stale lock file ${name} for dead process ${pid}`);
44
- fs.unlinkSync(lockFile);
45
- return false;
46
- }
47
- debug(`Error checking process ${pid} for lock ${name}:`, error);
48
- return true;
49
- }
50
- } catch (error) {
51
- debug(`Error reading lock file ${name}:`, error);
52
- return true;
53
- }
54
- }
55
-
56
- private static async acquireLock(
57
- name: string,
58
- timeout = 5000,
59
- ): Promise<boolean> {
60
- const RETRY_DELAY_MS = 50;
61
- const FILE_CREATE_FLAG = "wx";
62
-
63
- await this.ensureCacheDirectories();
64
- const lockFile = path.join(this.LOCKS_DIR, name);
65
- const startTime = Date.now();
66
- const lockContent = String(process.pid);
67
-
68
- while (Date.now() - startTime < timeout) {
69
- try {
70
- await fs.promises.writeFile(lockFile, lockContent, {
71
- flag: FILE_CREATE_FLAG,
72
- });
73
- debug(`Lock acquired for ${name}`);
74
- return true;
75
- } catch (error) {
76
- if ((error as ErrnoError).code === "EEXIST") {
77
- await setTimeout(RETRY_DELAY_MS);
78
- } else {
79
- throw error;
80
- }
81
- }
82
- }
83
- debug(`Failed to acquire lock for ${name} within ${timeout}ms`);
84
- return false;
85
- }
86
-
87
- private static async releaseLock(name: string): Promise<void> {
88
- const lockFile = path.join(this.LOCKS_DIR, name);
89
- try {
90
- await fs.promises.unlink(lockFile);
91
- debug(`Lock released for ${name}`);
92
- } catch (error) {
93
- if ((error as ErrnoError).code !== "ENOENT") {
94
- debug(`Error releasing lock for ${name}:`, error);
95
- }
96
- }
97
- }
98
-
99
- static async ensureCacheDirectories(): Promise<void> {
100
- try {
101
- await Promise.all([
102
- fs.promises.mkdir(this.CACHE_DIR, { recursive: true }),
103
- fs.promises.mkdir(this.USAGE_CACHE_DIR, { recursive: true }),
104
- fs.promises.mkdir(this.LOCKS_DIR, { recursive: true }),
105
- ]);
106
- } catch (error) {
107
- debug("Failed to create cache directories:", error);
108
- }
109
- }
110
-
111
- static createProjectHash(projectPath: string): string {
112
- return createHash("md5").update(projectPath).digest("hex").substring(0, 8);
113
- }
114
-
115
- static async getUsageCache(
116
- cacheType: "pricing",
117
- latestMtime?: number,
118
- ): Promise<unknown> {
119
- const MAX_RETRIES = 3;
120
- const RETRY_DELAY_MS = 75;
121
- const FILE_ENCODING = "utf-8";
122
-
123
- await this.ensureCacheDirectories();
124
- const cachePath = path.join(this.USAGE_CACHE_DIR, `${cacheType}.json`);
125
- const lockName = `${cacheType}.usage.lock`;
126
-
127
- for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
128
- const isCurrentlyLocked = this.isLocked(lockName);
129
- if (isCurrentlyLocked) {
130
- debug(`Cache for ${cacheType} is locked, waiting...`);
131
- await setTimeout(RETRY_DELAY_MS);
132
- continue;
133
- }
134
-
135
- try {
136
- const content = await fs.promises.readFile(cachePath, FILE_ENCODING);
137
- const cached: CacheEntry<unknown> = JSON.parse(content);
138
- const cacheIsValid = !latestMtime || cached.timestamp >= latestMtime;
139
-
140
- if (cacheIsValid) {
141
- debug(`[CACHE-HIT] ${cacheType} disk cache: found`);
142
- return this.deserializeDates(cached.data);
143
- } else {
144
- debug(
145
- `${cacheType} cache outdated: cache=${cached.timestamp}, latest=${latestMtime}`,
146
- );
147
- return null;
148
- }
149
- } catch (error) {
150
- if ((error as ErrnoError).code === "ENOENT") {
151
- debug(`No shared ${cacheType} usage cache found`);
152
- return null;
153
- }
154
- const attemptNumber = attempt + 1;
155
- debug(
156
- `Attempt ${attemptNumber} failed to read ${cacheType} cache: ${(error as Error).message}. Retrying...`,
157
- );
158
- await setTimeout(RETRY_DELAY_MS);
159
- }
160
- }
161
-
162
- debug(`Failed to read ${cacheType} cache after ${MAX_RETRIES} attempts.`);
163
- return null;
164
- }
165
-
166
- private static deserializeDates(data: unknown): unknown {
167
- if (Array.isArray(data)) {
168
- return data.map((entry) => ({
169
- ...entry,
170
- timestamp: new Date(entry.timestamp),
171
- }));
172
- }
173
- return data;
174
- }
175
-
176
- static async setUsageCache(
177
- cacheType: "pricing",
178
- data: unknown,
179
- latestMtime?: number,
180
- ): Promise<void> {
181
- const lockName = `${cacheType}.usage.lock`;
182
- const lockAcquired = await this.acquireLock(lockName);
183
- if (!lockAcquired) {
184
- debug(`Could not acquire lock to set usage cache for ${cacheType}`);
185
- return;
186
- }
187
-
188
- try {
189
- await this.ensureCacheDirectories();
190
- const cachePath = path.join(this.USAGE_CACHE_DIR, `${cacheType}.json`);
191
- const cacheTimestamp = latestMtime || Date.now();
192
- const cacheEntry: CacheEntry<unknown> = {
193
- data,
194
- timestamp: cacheTimestamp,
195
- };
196
- const cacheContent = JSON.stringify(cacheEntry);
197
-
198
- await fs.promises.writeFile(cachePath, cacheContent, "utf-8");
199
- debug(`[CACHE-SET] ${cacheType} disk cache stored`);
200
- } catch (error) {
201
- debug(`Failed to save ${cacheType} usage cache:`, error);
202
- } finally {
203
- await this.releaseLock(lockName);
204
- }
205
- }
206
- }