@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
package/src/themes/policy.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/cache.ts
DELETED
|
@@ -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
|
-
}
|