@promptctl/cc-candybar 1.41.2 → 1.42.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 +76 -75
- package/package.json +5 -5
- package/src/cli-flags.ts +8 -0
- package/src/daemon/cache/render.ts +66 -5
- package/src/daemon/client-stats.ts +1 -0
- package/src/daemon/server.ts +21 -5
- package/src/daemon/stats.ts +8 -2
- package/src/help-text.ts +3 -1
- package/src/index.ts +14 -4
- package/src/install/index.ts +1 -6
- package/src/version.ts +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promptctl/cc-candybar",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.42.0",
|
|
4
4
|
"description": "Statusline renderer for Claude Code — a JSON5-configurable DSL with daemon-cached data sources, byte-clean palette-aware composition, and OSC8 click verbs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.mjs",
|
|
@@ -91,9 +91,9 @@
|
|
|
91
91
|
"mobx": "^6.15.0"
|
|
92
92
|
},
|
|
93
93
|
"optionalDependencies": {
|
|
94
|
-
"@promptctl/cc-candybar-darwin-arm64": "1.
|
|
95
|
-
"@promptctl/cc-candybar-darwin-x64": "1.
|
|
96
|
-
"@promptctl/cc-candybar-linux-x64": "1.
|
|
97
|
-
"@promptctl/cc-candybar-linux-arm64": "1.
|
|
94
|
+
"@promptctl/cc-candybar-darwin-arm64": "1.42.0",
|
|
95
|
+
"@promptctl/cc-candybar-darwin-x64": "1.42.0",
|
|
96
|
+
"@promptctl/cc-candybar-linux-x64": "1.42.0",
|
|
97
|
+
"@promptctl/cc-candybar-linux-arm64": "1.42.0"
|
|
98
98
|
}
|
|
99
99
|
}
|
package/src/cli-flags.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// [LAW:one-source-of-truth] The bare flags Node answers itself. The dispatch in
|
|
2
|
+
// index.ts reads it, `--help` renders its own lines from it, and the Rust client
|
|
3
|
+
// routes exactly these to Node (its NODE_FLAGS; check-protocol diffs the two),
|
|
4
|
+
// so a spelling added here without its mirror fails the build, not the user.
|
|
5
|
+
export const NODE_FLAGS = {
|
|
6
|
+
help: ["-h", "--help"],
|
|
7
|
+
version: ["-V", "--version"],
|
|
8
|
+
} as const;
|
|
@@ -61,6 +61,45 @@ export interface RenderDeps {
|
|
|
61
61
|
watchers: WatcherRegistry;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// [LAW:no-ambient-temporal-coupling] The cache's outward lifecycle signal. A
|
|
65
|
+
// reload is the one event the cache alone knows the completion of — it runs
|
|
66
|
+
// from a debounced fs watcher, on the cache's own schedule — so anything that
|
|
67
|
+
// must run AFTER a reload (an operator log of its outcome, a test asserting
|
|
68
|
+
// on the state it wrote) needs the cache to say so, or it is left betting on
|
|
69
|
+
// a clock. The same named-bag idiom as renderDsl's RenderObservers
|
|
70
|
+
// [LAW:locality-or-seam]: a caller states what it passes by name, and a new
|
|
71
|
+
// observer is one field here, not a constructor signature every caller
|
|
72
|
+
// re-counts.
|
|
73
|
+
export interface RenderCacheObservers {
|
|
74
|
+
// Fires once per completed reload of an entry — the initial population in
|
|
75
|
+
// getOrCreate and every watcher-driven reload alike, success (a fresh
|
|
76
|
+
// `state`) and failure (`lastError` set, prior state preserved) alike —
|
|
77
|
+
// after the entry's fields and watcher are settled. The entry is handed
|
|
78
|
+
// over as ReloadedEntry so the observer reads the outcome from the one
|
|
79
|
+
// place it lives and the type, not this comment, keeps it from writing
|
|
80
|
+
// there. Trusted non-throwing (the same contract as onSegmentError): an
|
|
81
|
+
// observer that throws is a caller bug surfaced loudly, never absorbed here.
|
|
82
|
+
readonly onReload?: (entry: ReloadedEntry) => void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// [LAW:types-are-the-program] The observer's view of an entry: an ALLOW-LIST
|
|
86
|
+
// of the load's outcome. Nothing that owns a resource — the watcher handle,
|
|
87
|
+
// the SourceRegistry, the validator disposers, the render-cell sink — crosses
|
|
88
|
+
// this seam, so a handle added to either type later is closed by
|
|
89
|
+
// construction, not by remembering to omit it (`Readonly` alone would not:
|
|
90
|
+
// it cannot stop a method call such as `dispose()`).
|
|
91
|
+
export type ReloadedEntry = Readonly<
|
|
92
|
+
Pick<
|
|
93
|
+
CacheEntry,
|
|
94
|
+
| "projectDir"
|
|
95
|
+
| "cwd"
|
|
96
|
+
| "configFile"
|
|
97
|
+
| "configFilePath"
|
|
98
|
+
| "lastError"
|
|
99
|
+
| "lastWarning"
|
|
100
|
+
>
|
|
101
|
+
> & { readonly state: Readonly<Pick<DslRenderState, "config">> | null };
|
|
102
|
+
|
|
64
103
|
// [LAW:types-are-the-program] The DSL render state for an entry is one
|
|
65
104
|
// optionally-null bundle, not five independently-optional fields. Either
|
|
66
105
|
// every field is populated (a render is possible) or all are null (parse
|
|
@@ -154,10 +193,15 @@ export class RenderCache {
|
|
|
154
193
|
private readonly entries = new Map<string, CacheEntry>();
|
|
155
194
|
private readonly deps: RenderDeps;
|
|
156
195
|
private readonly maxEntries: number;
|
|
196
|
+
private readonly observers: RenderCacheObservers;
|
|
157
197
|
|
|
158
|
-
constructor(
|
|
198
|
+
constructor(
|
|
199
|
+
deps: RenderDeps,
|
|
200
|
+
opts: { maxEntries?: number; observers?: RenderCacheObservers } = {},
|
|
201
|
+
) {
|
|
159
202
|
this.deps = deps;
|
|
160
203
|
this.maxEntries = opts.maxEntries ?? MAX_ENTRIES;
|
|
204
|
+
this.observers = opts.observers ?? {};
|
|
161
205
|
}
|
|
162
206
|
|
|
163
207
|
// [LAW:dataflow-not-control-flow] One uniform shape: every entry has the
|
|
@@ -188,9 +232,15 @@ export class RenderCache {
|
|
|
188
232
|
state: null,
|
|
189
233
|
watcher: null,
|
|
190
234
|
};
|
|
191
|
-
|
|
235
|
+
// [LAW:single-enforcer] Insert, bound, then load — in that order. From the
|
|
236
|
+
// moment the entry owns live handles it is reachable for eviction and
|
|
237
|
+
// dispose, so a throwing observer cannot strand a registry outside the
|
|
238
|
+
// map, and a reentrant getOrCreate for this key finds the entry under
|
|
239
|
+
// construction rather than building a duplicate. The bound runs before
|
|
240
|
+
// the load because it is a fact about the MAP, complete at insertion:
|
|
241
|
+
// nothing the load does (including an observer throwing) can skip it.
|
|
242
|
+
// [LAW:dataflow-not-control-flow]
|
|
192
243
|
this.entries.set(key, entry);
|
|
193
|
-
|
|
194
244
|
if (this.entries.size > this.maxEntries) {
|
|
195
245
|
const oldest = this.entries.keys().next().value;
|
|
196
246
|
if (oldest !== undefined) {
|
|
@@ -205,6 +255,7 @@ export class RenderCache {
|
|
|
205
255
|
this.entries.delete(oldest);
|
|
206
256
|
}
|
|
207
257
|
}
|
|
258
|
+
this.reloadInto(entry);
|
|
208
259
|
|
|
209
260
|
return entry;
|
|
210
261
|
}
|
|
@@ -220,7 +271,17 @@ export class RenderCache {
|
|
|
220
271
|
// new DslConfig disposes the old SourceRegistry before constructing a new
|
|
221
272
|
// one. The registry owns timers, watchers, MobX reactions, and git
|
|
222
273
|
// subscriptions — dropping it without dispose leaks every handle.
|
|
274
|
+
//
|
|
275
|
+
// [LAW:single-enforcer] One publish point for "this reload completed":
|
|
276
|
+
// loadFromDisk owns the outcome (it returns through more than one arm),
|
|
277
|
+
// and this wrapper is the only caller, so the signal structurally cannot
|
|
278
|
+
// be skipped by whichever arm a reload takes — or by an arm added later.
|
|
223
279
|
private reloadInto(entry: CacheEntry): void {
|
|
280
|
+
this.loadFromDisk(entry);
|
|
281
|
+
this.observers.onReload?.(entry);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private loadFromDisk(entry: CacheEntry): void {
|
|
224
285
|
const resolvedPath = resolveDslConfigPath(
|
|
225
286
|
entry.projectDir,
|
|
226
287
|
entry.cwd,
|
|
@@ -286,7 +347,7 @@ export class RenderCache {
|
|
|
286
347
|
// store, registry, compiled segments, palette — as one transaction. Any
|
|
287
348
|
// failure inside disposes the partially-built registry so we don't leak
|
|
288
349
|
// timers/watchers from a half-constructed reload, then rethrows so the
|
|
289
|
-
// caller (
|
|
350
|
+
// caller (loadFromDisk) preserves the prior `entry.state` unchanged.
|
|
290
351
|
private buildState(
|
|
291
352
|
entry: CacheEntry,
|
|
292
353
|
resolvedPath: string | null,
|
|
@@ -396,7 +457,7 @@ export class RenderCache {
|
|
|
396
457
|
// key) are part of the same construction transaction as the registry: any
|
|
397
458
|
// failure (registration, a duplicate-key throw) disposes every handle built
|
|
398
459
|
// so far — registry AND already-installed validators — before rethrowing, so
|
|
399
|
-
//
|
|
460
|
+
// loadFromDisk preserves the prior last-known-good with nothing half-installed.
|
|
400
461
|
const validatorDisposers: Array<() => void> = [];
|
|
401
462
|
try {
|
|
402
463
|
// [LAW:one-source-of-truth] The action runtime reads session.id + current
|
|
@@ -64,6 +64,7 @@ export function formatStats(s: StatsSnapshot): string {
|
|
|
64
64
|
lines.push(`process`);
|
|
65
65
|
lines.push(` pid ${s.pid}`);
|
|
66
66
|
lines.push(` version ${s.version}`);
|
|
67
|
+
lines.push(` protocol ${s.protocolVersion}`);
|
|
67
68
|
lines.push(` startedAt ${s.startedAt}`);
|
|
68
69
|
lines.push(` uptime ${fmtUptime(s.uptimeSec)}`);
|
|
69
70
|
lines.push(` rss ${fmtBytes(s.rssBytes)}`);
|
package/src/daemon/server.ts
CHANGED
|
@@ -131,11 +131,27 @@ const sessionState = new SessionState();
|
|
|
131
131
|
const contextProvider = new ContextProvider();
|
|
132
132
|
const metricsProvider = new MetricsProvider();
|
|
133
133
|
const tmuxService = new TmuxService();
|
|
134
|
-
const renderCache = new RenderCache(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
const renderCache = new RenderCache(
|
|
135
|
+
{
|
|
136
|
+
gitService,
|
|
137
|
+
sessionState,
|
|
138
|
+
watchers: watcherRegistry,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
observers: {
|
|
142
|
+
// [LAW:no-silent-failure] Every config (re)load's outcome lands in
|
|
143
|
+
// daemon.log beside the "config change detected" line that preceded it
|
|
144
|
+
// — the operator's only record of whether a save was picked up cleanly,
|
|
145
|
+
// kept rendering last-known-good behind an error, or resolved to a
|
|
146
|
+
// different file. Same info level as the detection line.
|
|
147
|
+
onReload: (entry) =>
|
|
148
|
+
dlog(
|
|
149
|
+
"info",
|
|
150
|
+
`config loaded projectDir=${entry.projectDir} cwd=${entry.cwd} file=${entry.configFilePath ?? "<bundled default>"} error=${entry.lastError === null ? "none" : JSON.stringify(entry.lastError)} warning=${entry.lastWarning === null ? "none" : JSON.stringify(entry.lastWarning)}`,
|
|
151
|
+
),
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
);
|
|
139
155
|
|
|
140
156
|
const REQUEST_TIMEOUT_MS = 200;
|
|
141
157
|
const BIN_CHECK_INTERVAL_MS = 60 * 1000;
|
package/src/daemon/stats.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { LaunchCategory } from "../proc/launch";
|
|
6
6
|
import type { LaunchStatsHandle } from "../proc/stats-handle";
|
|
7
7
|
import { PROTOCOL_VERSION } from "./protocol";
|
|
8
|
+
import { PACKAGE_VERSION } from "../version";
|
|
8
9
|
|
|
9
10
|
// Rolling window for "last minute" counts. Keep timestamps for each launch in
|
|
10
11
|
// a ring buffer; eviction happens lazily on read.
|
|
@@ -28,7 +29,11 @@ const HISTOGRAM_CAP = 16;
|
|
|
28
29
|
|
|
29
30
|
export interface StatsSnapshot {
|
|
30
31
|
pid: number;
|
|
31
|
-
|
|
32
|
+
// [LAW:one-source-of-truth] The daemon's own baked package stamp — set
|
|
33
|
+
// against `cc-candybar --version` to diagnose client-vs-daemon skew in one
|
|
34
|
+
// step. The wire contract number is a different fact under its own name.
|
|
35
|
+
version: string;
|
|
36
|
+
protocolVersion: number;
|
|
32
37
|
startedAt: string;
|
|
33
38
|
uptimeSec: number;
|
|
34
39
|
rssBytes: number;
|
|
@@ -199,7 +204,8 @@ export class RuntimeStats {
|
|
|
199
204
|
const mem = process.memoryUsage();
|
|
200
205
|
return {
|
|
201
206
|
pid: process.pid,
|
|
202
|
-
version:
|
|
207
|
+
version: PACKAGE_VERSION,
|
|
208
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
203
209
|
startedAt: this.startedAt.toISOString(),
|
|
204
210
|
uptimeSec: Math.floor((Date.now() - this.startedAt.getTime()) / 1000),
|
|
205
211
|
rssBytes: mem.rss,
|
package/src/help-text.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DISCLOSURE_GLYPH_CLOSED } from "./config/disclosure";
|
|
2
2
|
import { HELP_GLYPH_CLOSED } from "./config/help";
|
|
3
|
+
import { NODE_FLAGS } from "./cli-flags";
|
|
3
4
|
|
|
4
5
|
// [LAW:effects-at-boundaries] Pure data, no I/O — index.ts owns the console.log
|
|
5
6
|
// effect. Kept as its own module so the text is importable (and testable) without
|
|
@@ -36,7 +37,8 @@ cc-candybar - Beautiful powerline statusline for Claude Code
|
|
|
36
37
|
Usage: cc-candybar [options]
|
|
37
38
|
|
|
38
39
|
Standalone Commands:
|
|
39
|
-
|
|
40
|
+
${NODE_FLAGS.help.join(", ").padEnd(25)}Show this help
|
|
41
|
+
${NODE_FLAGS.version.join(", ").padEnd(25)}Print the version of this runtime (cc-candybar <version>)
|
|
40
42
|
|
|
41
43
|
Debugging:
|
|
42
44
|
CC_CANDYBAR_DEBUG=1 Enable debug logging for troubleshooting
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,8 @@ import { runCheck } from "./check";
|
|
|
16
16
|
import { obtainDaemonKick } from "./daemon/acquire";
|
|
17
17
|
import { planOutcome } from "./render/outcome-plan";
|
|
18
18
|
import { HELP_TEXT } from "./help-text";
|
|
19
|
+
import { NODE_FLAGS } from "./cli-flags";
|
|
20
|
+
import { PACKAGE_VERSION } from "./version";
|
|
19
21
|
|
|
20
22
|
// Read terminal width from the live shell context (no subprocess). Returns
|
|
21
23
|
// undefined when nothing reliable is available; the daemon falls back to its
|
|
@@ -52,6 +54,9 @@ function detectTermCols(): number | undefined {
|
|
|
52
54
|
// widen recall of a fact that is otherwise reported as a plain `false`.
|
|
53
55
|
const SSH_ENV_VARS = ["SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY"] as const;
|
|
54
56
|
|
|
57
|
+
const hasFlag = (flags: readonly string[]): boolean =>
|
|
58
|
+
flags.some((f) => process.argv.includes(f));
|
|
59
|
+
|
|
55
60
|
// [LAW:dataflow-not-control-flow] A fold over the vocabulary, not a chain of
|
|
56
61
|
// ifs — adding a name is a data edit.
|
|
57
62
|
//
|
|
@@ -69,13 +74,18 @@ function showHelpText(): void {
|
|
|
69
74
|
|
|
70
75
|
async function main(): Promise<void> {
|
|
71
76
|
try {
|
|
72
|
-
|
|
73
|
-
process.argv.includes("--help") || process.argv.includes("-h");
|
|
74
|
-
|
|
75
|
-
if (showHelp) {
|
|
77
|
+
if (hasFlag(NODE_FLAGS.help)) {
|
|
76
78
|
showHelpText();
|
|
77
79
|
process.exit(0);
|
|
78
80
|
}
|
|
81
|
+
// [LAW:one-type-per-behavior] Answers "what is THIS binary" from the baked
|
|
82
|
+
// stamp alone — never a daemon probe, which would fail exactly when the
|
|
83
|
+
// flag is most needed (no working daemon). Daemon skew is the stats
|
|
84
|
+
// snapshot's `version` field.
|
|
85
|
+
if (hasFlag(NODE_FLAGS.version)) {
|
|
86
|
+
console.log(`cc-candybar ${PACKAGE_VERSION}`);
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
79
89
|
|
|
80
90
|
// [LAW:dataflow-not-control-flow] Subcommand dispatch is data: argv[2]
|
|
81
91
|
// selects the handler. Each handler short-circuits via process.exit().
|
package/src/install/index.ts
CHANGED
|
@@ -8,12 +8,7 @@ import type { PermanentOutcome } from "../daemon/client-transport";
|
|
|
8
8
|
import { obtainDaemonKick } from "../daemon/acquire";
|
|
9
9
|
import { URL_SCHEME, VERB_COPY } from "../click/wire";
|
|
10
10
|
import { DISCLOSURE_GLYPH_CLOSED } from "../config/disclosure";
|
|
11
|
-
|
|
12
|
-
// [LAW:one-source-of-truth] Replaced at build time by tsdown's `define` option
|
|
13
|
-
// from package.json — the single version stamp install output reports.
|
|
14
|
-
declare const __PACKAGE_VERSION__: string;
|
|
15
|
-
const PACKAGE_VERSION =
|
|
16
|
-
typeof __PACKAGE_VERSION__ !== "undefined" ? __PACKAGE_VERSION__ : "dev";
|
|
11
|
+
import { PACKAGE_VERSION } from "../version";
|
|
17
12
|
|
|
18
13
|
const PACKAGE_NAME = "@promptctl/cc-candybar";
|
|
19
14
|
const BUNDLE_ID = "com.cccandybar.url-handler";
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// [LAW:one-source-of-truth] THE version stamp of this runtime. package.json is
|
|
2
|
+
// the sole authority: tsdown's `define` bakes it into the bundle, and
|
|
3
|
+
// scripts/version-stamp.cjs preloads it for untranspiled source. Every consumer
|
|
4
|
+
// — `--version`, the install banner, the daemon's stats snapshot — reads this
|
|
5
|
+
// one export.
|
|
6
|
+
declare const __PACKAGE_VERSION__: string;
|
|
7
|
+
|
|
8
|
+
// [LAW:no-silent-failure] A runtime that cannot say what it is must say THAT.
|
|
9
|
+
// The old `"dev"` fallback was an answer-shaped void in a flag whose whole job
|
|
10
|
+
// is answering this question; an unsubstituted build now fails at module load.
|
|
11
|
+
if (typeof __PACKAGE_VERSION__ === "undefined") {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"__PACKAGE_VERSION__ was not substituted: build via tsdown (define), or preload scripts/version-stamp.cjs when running source",
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const PACKAGE_VERSION: string = __PACKAGE_VERSION__;
|