@promptctl/cc-candybar 1.0.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/LICENSE +21 -0
- package/README.md +145 -0
- package/bin/cc-candybar +6 -0
- package/dist/index.mjs +185 -0
- package/package.json +99 -0
- package/plugin/.claude-plugin/plugin.json +11 -0
- package/plugin/bin/preview.sh +305 -0
- package/plugin/commands/candybar.md +403 -0
- package/plugin/templates/config-essential.json +36 -0
- package/plugin/templates/config-full.json +55 -0
- package/plugin/templates/config-standard.json +39 -0
- package/plugin/templates/config-tui-compact.json +48 -0
- package/plugin/templates/config-tui-full.json +89 -0
- package/plugin/templates/config-tui-standard.json +56 -0
- package/plugin/templates/config-tui.json +18 -0
- package/plugin/templates/nerd-fonts-sample.txt +5 -0
- package/schema/cc-candybar.schema.json +1379 -0
- package/src/click/wire.ts +113 -0
- package/src/config/action.ts +91 -0
- package/src/config/cli.ts +170 -0
- package/src/config/default-dsl-config.ts +661 -0
- package/src/config/dsl-loader.ts +265 -0
- package/src/config/dsl-types.ts +425 -0
- package/src/config/loader/actions.ts +530 -0
- package/src/config/loader/cache.ts +206 -0
- package/src/config/loader/cross-ref.ts +326 -0
- package/src/config/loader/cycles.ts +148 -0
- package/src/config/loader/diagnostics.ts +99 -0
- package/src/config/loader/discovery.ts +182 -0
- package/src/config/loader/emit-schema.ts +63 -0
- package/src/config/loader/globals.ts +42 -0
- package/src/config/loader/helpers.ts +48 -0
- package/src/config/loader/layout.ts +688 -0
- package/src/config/loader/merge.ts +40 -0
- package/src/config/loader/refs.ts +96 -0
- package/src/config/loader/segments.ts +120 -0
- package/src/config/loader/validate-core.ts +674 -0
- package/src/config/loader/variables.ts +260 -0
- package/src/daemon/acquire.ts +411 -0
- package/src/daemon/cache/git.ts +553 -0
- package/src/daemon/cache/render.ts +449 -0
- package/src/daemon/cache/session-usage-store.ts +446 -0
- package/src/daemon/cache/watchers.ts +245 -0
- package/src/daemon/client-debug.ts +120 -0
- package/src/daemon/client-stats.ts +129 -0
- package/src/daemon/client-transport.ts +273 -0
- package/src/daemon/client.ts +75 -0
- package/src/daemon/debug-types.ts +91 -0
- package/src/daemon/debug.ts +264 -0
- package/src/daemon/limits.ts +154 -0
- package/src/daemon/log.ts +69 -0
- package/src/daemon/parent-watchdog.ts +80 -0
- package/src/daemon/paths.ts +127 -0
- package/src/daemon/protocol.ts +235 -0
- package/src/daemon/render-payload.ts +611 -0
- package/src/daemon/server.ts +1103 -0
- package/src/daemon/session-state-file.ts +108 -0
- package/src/daemon/session-state.ts +237 -0
- package/src/daemon/stats.ts +229 -0
- package/src/daemon/verbs/index.ts +458 -0
- package/src/daemon/verbs/state-validators.ts +708 -0
- package/src/demo/dsl.ts +117 -0
- package/src/demo/mock-data.ts +67 -0
- package/src/demo/statusline.json5 +92 -0
- package/src/dsl/node-registry.ts +281 -0
- package/src/dsl/render.ts +558 -0
- package/src/index.ts +206 -0
- package/src/install/index.ts +410 -0
- package/src/proc/launch.ts +451 -0
- package/src/proc/stats-handle.ts +13 -0
- package/src/render/action.ts +458 -0
- package/src/render/diagnostic-style.ts +23 -0
- package/src/render/diagnostic-text.ts +77 -0
- package/src/render/error-glyph.ts +53 -0
- package/src/render/outcome-plan.ts +45 -0
- package/src/render/picker.ts +231 -0
- package/src/render/split-lines.ts +51 -0
- package/src/render/strip.ts +103 -0
- package/src/segments/cache.ts +131 -0
- package/src/segments/context.ts +190 -0
- package/src/segments/git.ts +561 -0
- package/src/segments/metrics.ts +101 -0
- package/src/segments/pricing.ts +452 -0
- package/src/segments/session.ts +188 -0
- package/src/segments/tmux.ts +74 -0
- package/src/template-engine/cells.ts +90 -0
- package/src/template-engine/colors.ts +102 -0
- package/src/template-engine/engine.ts +108 -0
- package/src/template-engine/funcs.ts +216 -0
- package/src/template-engine/index.ts +11 -0
- package/src/template-engine/layout.ts +112 -0
- package/src/template-engine/scope.ts +62 -0
- package/src/themes/index.ts +19 -0
- package/src/themes/palette-resolvers.ts +86 -0
- package/src/themes/policy.ts +79 -0
- package/src/themes/session-random.ts +88 -0
- package/src/utils/cache.ts +206 -0
- package/src/utils/claude.ts +616 -0
- package/src/utils/color-support.ts +118 -0
- package/src/utils/formatters.ts +77 -0
- package/src/utils/logger.ts +5 -0
- package/src/utils/outcome.ts +33 -0
- package/src/utils/schema-validator.ts +126 -0
- package/src/utils/single-flight.ts +57 -0
- package/src/utils/terminal-width.ts +43 -0
- package/src/utils/terminal.ts +11 -0
- package/src/utils/transcript-fs.ts +162 -0
- package/src/var-system/index.ts +24 -0
- package/src/var-system/sources.ts +1038 -0
- package/src/var-system/store.ts +223 -0
- package/src/var-system/types.ts +57 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// [LAW:one-source-of-truth] The daemon's MobX store is THE place where
|
|
2
|
+
// every variable's value lives. Templates, segments, and click handlers
|
|
3
|
+
// all read through this store; there is no parallel cache, no shadow
|
|
4
|
+
// state, no per-renderer copy.
|
|
5
|
+
//
|
|
6
|
+
// Two node kinds — `box` for externally-driven values (input JSON,
|
|
7
|
+
// shell output, fs watchers, TTL timers) and `computed` for derived
|
|
8
|
+
// values (templates, git fields wrapping shell). MobX auto-tracks
|
|
9
|
+
// dependencies when a computed's deriver reads other variables; the
|
|
10
|
+
// invalidation graph is built without us declaring it.
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
observable,
|
|
14
|
+
computed,
|
|
15
|
+
runInAction as mobxRunInAction,
|
|
16
|
+
type IObservableValue,
|
|
17
|
+
type IComputedValue,
|
|
18
|
+
} from "mobx";
|
|
19
|
+
import { typeOf, type VarType, type VarValue } from "./types";
|
|
20
|
+
|
|
21
|
+
export interface VarNode {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly type: VarType;
|
|
24
|
+
readonly kind: "box" | "computed";
|
|
25
|
+
read(): VarValue;
|
|
26
|
+
// [LAW:types-are-the-program] Age is a property of the node, not of an
|
|
27
|
+
// external bookkeeping layer — duplicating it in a side map would let the
|
|
28
|
+
// two diverge. `number` for box nodes (epoch ms of last set, including the
|
|
29
|
+
// initial-value set at construction); `null` for computed nodes, whose
|
|
30
|
+
// freshness is governed by MobX invalidation, not a single timestamp.
|
|
31
|
+
lastUpdatedMs(): number | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class BoxNode implements VarNode {
|
|
35
|
+
readonly kind = "box" as const;
|
|
36
|
+
private readonly cell: IObservableValue<VarValue>;
|
|
37
|
+
// [LAW:single-enforcer] One write path (`set`) updates both the value and
|
|
38
|
+
// the timestamp; introspection reads from the same place renderers do.
|
|
39
|
+
private lastSetAt: number;
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
readonly name: string,
|
|
43
|
+
readonly type: VarType,
|
|
44
|
+
initial: VarValue,
|
|
45
|
+
) {
|
|
46
|
+
assertType(name, type, initial, "initial value");
|
|
47
|
+
this.cell = observable.box(initial, { deep: false });
|
|
48
|
+
this.lastSetAt = Date.now();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
read(): VarValue {
|
|
52
|
+
return this.cell.get();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
set(value: VarValue): void {
|
|
56
|
+
assertType(this.name, this.type, value, "set value");
|
|
57
|
+
this.cell.set(value);
|
|
58
|
+
this.lastSetAt = Date.now();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
lastUpdatedMs(): number {
|
|
62
|
+
return this.lastSetAt;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class ComputedNode implements VarNode {
|
|
67
|
+
readonly kind = "computed" as const;
|
|
68
|
+
private readonly cell: IComputedValue<VarValue>;
|
|
69
|
+
|
|
70
|
+
constructor(
|
|
71
|
+
readonly name: string,
|
|
72
|
+
readonly type: VarType,
|
|
73
|
+
deriver: () => VarValue,
|
|
74
|
+
) {
|
|
75
|
+
// [LAW:one-source-of-truth] keepAlive caches the value across reads
|
|
76
|
+
// so `.get()` re-runs the deriver only when a tracked dep
|
|
77
|
+
// invalidates — without it, MobX treats an unobserved computed as
|
|
78
|
+
// "not cached" and re-runs on every read. The render path is pull-
|
|
79
|
+
// only (no autorun), so keepAlive is the only mode that gives the
|
|
80
|
+
// reactive-cache contract the proposal promises.
|
|
81
|
+
this.cell = computed(
|
|
82
|
+
() => {
|
|
83
|
+
const v = deriver();
|
|
84
|
+
assertType(this.name, this.type, v, "computed result");
|
|
85
|
+
return v;
|
|
86
|
+
},
|
|
87
|
+
{ keepAlive: true },
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
read(): VarValue {
|
|
92
|
+
return this.cell.get();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
lastUpdatedMs(): null {
|
|
96
|
+
// [LAW:no-defensive-null-guards] Computed nodes have no single
|
|
97
|
+
// "updated" moment — the cache is valid until a tracked dep changes.
|
|
98
|
+
// Returning null is structurally distinct from "updated at 0," so a
|
|
99
|
+
// consumer can render "—" for computed and a real age for boxes.
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assertType(
|
|
105
|
+
name: string,
|
|
106
|
+
declared: VarType,
|
|
107
|
+
value: VarValue,
|
|
108
|
+
context: string,
|
|
109
|
+
): void {
|
|
110
|
+
const actual = typeOf(value);
|
|
111
|
+
if (actual !== declared) {
|
|
112
|
+
throw new TypeError(
|
|
113
|
+
`Variable "${name}": ${context} type ${actual} does not match declared type ${declared}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// [LAW:single-enforcer] All declarations and reads go through one
|
|
119
|
+
// VariableStore instance per daemon. Two stores cannot coexist for the
|
|
120
|
+
// same daemon — the dep graph would split, click handlers would mutate
|
|
121
|
+
// one while renders read the other.
|
|
122
|
+
|
|
123
|
+
export class VariableStore {
|
|
124
|
+
private readonly nodes = new Map<string, VarNode>();
|
|
125
|
+
|
|
126
|
+
defineBox(name: string, type: VarType, initial: VarValue): void {
|
|
127
|
+
this.assertNotDefined(name);
|
|
128
|
+
this.nodes.set(name, new BoxNode(name, type, initial));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Computed deriver receives a `read` function that returns the value
|
|
132
|
+
// of any variable in the store. Calling `read(other)` from inside the
|
|
133
|
+
// deriver is what registers the dependency with MobX — the deriver's
|
|
134
|
+
// body is the dep graph.
|
|
135
|
+
defineComputed(
|
|
136
|
+
name: string,
|
|
137
|
+
type: VarType,
|
|
138
|
+
deriver: (read: (other: string) => VarValue) => VarValue,
|
|
139
|
+
): void {
|
|
140
|
+
this.assertNotDefined(name);
|
|
141
|
+
const reader = (other: string): VarValue => this.read(other);
|
|
142
|
+
this.nodes.set(name, new ComputedNode(name, type, () => deriver(reader)));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
read(name: string): VarValue {
|
|
146
|
+
return this.requireNode(name).read();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
setBox(name: string, value: VarValue): void {
|
|
150
|
+
const node = this.requireNode(name);
|
|
151
|
+
if (node.kind !== "box") {
|
|
152
|
+
throw new TypeError(
|
|
153
|
+
`Variable "${name}" is a ${node.kind}, not a box (use defineBox to create a settable variable)`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
// [LAW:single-enforcer] All mutations go through one action. MobX
|
|
157
|
+
// strict-mode (the default in v6) rejects modifications outside an
|
|
158
|
+
// action once the observable has observers — and our keepAlive
|
|
159
|
+
// computeds always do. Wrapping setBox here means callers do not
|
|
160
|
+
// need to remember; runInAction stays useful for batching multiple
|
|
161
|
+
// sets so dependents invalidate once.
|
|
162
|
+
mobxRunInAction(() => (node as BoxNode).set(value));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Wrap multi-variable updates so dependents only invalidate once per
|
|
166
|
+
// batch. Used by the render path to push a whole input payload before
|
|
167
|
+
// any computed re-evaluates.
|
|
168
|
+
runInAction(fn: () => void): void {
|
|
169
|
+
mobxRunInAction(fn);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
has(name: string): boolean {
|
|
173
|
+
return this.nodes.has(name);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getType(name: string): VarType {
|
|
177
|
+
return this.requireNode(name).type;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getKind(name: string): "box" | "computed" {
|
|
181
|
+
return this.requireNode(name).kind;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// [LAW:types-are-the-program] Introspection (src/daemon/debug.ts) needs
|
|
185
|
+
// the whole node — type, kind, lastUpdatedMs — in one lookup, but
|
|
186
|
+
// returning the BoxNode directly would leak `.set` structurally even
|
|
187
|
+
// though VarNode does not advertise it. The returned wrapper is a fresh
|
|
188
|
+
// object exposing only the VarNode surface — `.set` is unreachable at
|
|
189
|
+
// any level (no structural escape, no plain-JS reach-through). The
|
|
190
|
+
// mutation path remains gated behind `setBox`, which wraps in
|
|
191
|
+
// runInAction to satisfy MobX strict-mode.
|
|
192
|
+
//
|
|
193
|
+
// [LAW:single-enforcer] One requireNode call per consumer-row — the
|
|
194
|
+
// round-1 dedup fix in introspectVars relies on the caller getting both
|
|
195
|
+
// type/kind and a read() in one go without paying for a second
|
|
196
|
+
// requireNode. The wrapper preserves that.
|
|
197
|
+
getNode(name: string): VarNode {
|
|
198
|
+
const node = this.requireNode(name);
|
|
199
|
+
return {
|
|
200
|
+
name: node.name,
|
|
201
|
+
type: node.type,
|
|
202
|
+
kind: node.kind,
|
|
203
|
+
read: () => node.read(),
|
|
204
|
+
lastUpdatedMs: () => node.lastUpdatedMs(),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
names(): string[] {
|
|
209
|
+
return [...this.nodes.keys()];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private requireNode(name: string): VarNode {
|
|
213
|
+
const node = this.nodes.get(name);
|
|
214
|
+
if (!node) throw new ReferenceError(`Unknown variable "${name}"`);
|
|
215
|
+
return node;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private assertNotDefined(name: string): void {
|
|
219
|
+
if (this.nodes.has(name)) {
|
|
220
|
+
throw new Error(`Variable "${name}" is already declared`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// [LAW:one-type-per-behavior] Every variable, every source kind, every
|
|
2
|
+
// template result lands in this same union. Adding a new value type would
|
|
3
|
+
// be a code change that ripples through the cast helpers and the filter
|
|
4
|
+
// pipeline — there is no "extra" variant available at the type layer.
|
|
5
|
+
|
|
6
|
+
export type VarType = "string" | "number" | "boolean";
|
|
7
|
+
export type VarValue = string | number | boolean;
|
|
8
|
+
|
|
9
|
+
export function typeOf(v: VarValue): VarType {
|
|
10
|
+
const t = typeof v;
|
|
11
|
+
if (t === "string" || t === "number" || t === "boolean") return t;
|
|
12
|
+
// [LAW:no-defensive-null-guards] This is a trust-boundary check —
|
|
13
|
+
// values arrive from user-authored templates and external sources, so
|
|
14
|
+
// an unsupported runtime value must fail loudly here, not silently
|
|
15
|
+
// downstream.
|
|
16
|
+
throw new TypeError(
|
|
17
|
+
`Variable values must be string|number|boolean (got ${t})`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Cast helpers used by the filter pipeline (chunk 2 will consume these).
|
|
22
|
+
// Each cast either returns a typed value or throws with a useful message.
|
|
23
|
+
// Total casts (toString) never throw; partial casts (toNumber, toBool)
|
|
24
|
+
// throw on ambiguous input rather than silently coercing.
|
|
25
|
+
|
|
26
|
+
export function toString(v: VarValue): string {
|
|
27
|
+
return typeof v === "string" ? v : String(v);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function toNumber(v: VarValue): number {
|
|
31
|
+
if (typeof v === "number") return v;
|
|
32
|
+
if (typeof v === "boolean") return v ? 1 : 0;
|
|
33
|
+
const trimmed = v.trim();
|
|
34
|
+
if (trimmed === "") {
|
|
35
|
+
throw new TypeError(`Cannot cast empty string to number`);
|
|
36
|
+
}
|
|
37
|
+
const n = Number(trimmed);
|
|
38
|
+
if (!Number.isFinite(n)) {
|
|
39
|
+
throw new TypeError(`Cannot cast ${JSON.stringify(v)} to number`);
|
|
40
|
+
}
|
|
41
|
+
return n;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function toBool(v: VarValue): boolean {
|
|
45
|
+
if (typeof v === "boolean") return v;
|
|
46
|
+
if (typeof v === "number") {
|
|
47
|
+
if (v === 0 || v === 1) return v === 1;
|
|
48
|
+
throw new TypeError(
|
|
49
|
+
`Cannot cast number ${v} to bool (only 0 and 1 are accepted)`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (v === "true") return true;
|
|
53
|
+
if (v === "false") return false;
|
|
54
|
+
throw new TypeError(
|
|
55
|
+
`Cannot cast ${JSON.stringify(v)} to bool (expected "true" or "false")`,
|
|
56
|
+
);
|
|
57
|
+
}
|