@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/install/index.ts
DELETED
|
@@ -1,557 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
|
-
import { launchSync } from "../proc/launch";
|
|
6
|
-
import { tryClickViaDaemon } from "../daemon/client";
|
|
7
|
-
import type { PermanentOutcome } from "../daemon/client-transport";
|
|
8
|
-
import { obtainDaemonKick } from "../daemon/acquire";
|
|
9
|
-
import { URL_SCHEME, VERB_COPY } from "../click/wire";
|
|
10
|
-
import { DISCLOSURE_GLYPH_CLOSED } from "../config/disclosure";
|
|
11
|
-
import { PACKAGE_VERSION } from "../version";
|
|
12
|
-
import { assessCurrency, currencyReport, fetchLatestVersion } from "./currency";
|
|
13
|
-
|
|
14
|
-
const PACKAGE_NAME = "@promptctl/cc-candybar";
|
|
15
|
-
const BUNDLE_ID = "com.cccandybar.url-handler";
|
|
16
|
-
const APP_NAME = "CCCandybarURLHandler";
|
|
17
|
-
|
|
18
|
-
// [LAW:one-source-of-truth] `install` writes no renderer flags into
|
|
19
|
-
// ~/.claude/settings.json. All authoring lives in `.cc-candybar.json5` or
|
|
20
|
-
// `.cc-candybar.json` (see resolveDslConfigPath — both extensions accepted,
|
|
21
|
-
// .json5 preferred); the install command's job is staging the runtime,
|
|
22
|
-
// wiring the URL handler, and pointing settings at the staged entry.
|
|
23
|
-
const DEFAULT_INSTALL_ARGS: readonly string[] = [];
|
|
24
|
-
|
|
25
|
-
// [LAW:one-type-per-behavior] One platform → one package name; the render
|
|
26
|
-
// entry is the same contract everywhere, only the artifact differs.
|
|
27
|
-
const PLATFORM_PACKAGES: Record<string, string> = {
|
|
28
|
-
"darwin-arm64": "@promptctl/cc-candybar-darwin-arm64",
|
|
29
|
-
"darwin-x64": "@promptctl/cc-candybar-darwin-x64",
|
|
30
|
-
"linux-x64": "@promptctl/cc-candybar-linux-x64",
|
|
31
|
-
"linux-arm64": "@promptctl/cc-candybar-linux-arm64",
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
function shellEscape(arg: string): string {
|
|
35
|
-
// Safe characters that don't need quoting in any reasonable shell.
|
|
36
|
-
if (/^[A-Za-z0-9_./=,:-]+$/.test(arg)) return arg;
|
|
37
|
-
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function buildStatusLineCommand(
|
|
41
|
-
binPath: string,
|
|
42
|
-
rendererArgs: readonly string[],
|
|
43
|
-
): string {
|
|
44
|
-
return [binPath, ...rendererArgs].map(shellEscape).join(" ");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function appBundlePath(): string {
|
|
48
|
-
return path.join(os.homedir(), "Applications", `${APP_NAME}.app`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function settingsJsonPath(): string {
|
|
52
|
-
return path.join(os.homedir(), ".claude", "settings.json");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function ensureMacOS(): void {
|
|
56
|
-
if (process.platform !== "darwin") {
|
|
57
|
-
throw new Error(
|
|
58
|
-
`URL handler installation requires macOS (found platform: ${process.platform}).`,
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// [LAW:one-source-of-truth] The staged runtime lives at ONE stable path per
|
|
64
|
-
// platform, outside any package manager's store — pnpm cache pruning or a
|
|
65
|
-
// version bump can never yank the files the statusline and daemon run from.
|
|
66
|
-
function supportDir(): string {
|
|
67
|
-
if (process.platform === "darwin") {
|
|
68
|
-
return path.join(
|
|
69
|
-
os.homedir(),
|
|
70
|
-
"Library",
|
|
71
|
-
"Application Support",
|
|
72
|
-
"CCCandybar",
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
const xdgData =
|
|
76
|
-
process.env.XDG_DATA_HOME ?? path.join(os.homedir(), ".local", "share");
|
|
77
|
-
return path.join(xdgData, "cc-candybar");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function stagedBinPath(): string {
|
|
81
|
-
return path.join(supportDir(), "bin", "cc-candybar");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function stagedDistPath(): string {
|
|
85
|
-
return path.join(supportDir(), "dist", "index.mjs");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function appleScriptSource(nodePath: string, scriptPath: string): string {
|
|
89
|
-
// Bake absolute paths into the AppleScript so click-time invocation doesn't
|
|
90
|
-
// depend on PATH or install-time cache state. The dist bundle is fully
|
|
91
|
-
// self-contained (tsdown noExternal), so no NODE_PATH is needed.
|
|
92
|
-
const escNode = nodePath.replace(/"/g, '\\"');
|
|
93
|
-
const escScript = scriptPath.replace(/"/g, '\\"');
|
|
94
|
-
return [
|
|
95
|
-
"on open location L",
|
|
96
|
-
`\tdo shell script "'${escNode}' '${escScript}' url-handle " & quoted form of L`,
|
|
97
|
-
"end open location",
|
|
98
|
-
].join("\n");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// [LAW:one-source-of-truth] The bundle that contains *this* function is
|
|
102
|
-
// the thing we need to stage. Two invocation paths reach us:
|
|
103
|
-
// - via the bin shim: process.argv[1] = ".../bin/cc-candybar" which
|
|
104
|
-
// dynamically imports ../dist/index.mjs — resolve the sibling dist.
|
|
105
|
-
// - direct node: process.argv[1] = ".../dist/index.mjs". Use as-is.
|
|
106
|
-
export function locateBundledDist(argv1: string | undefined): string {
|
|
107
|
-
if (!argv1) {
|
|
108
|
-
throw new Error("install: process.argv[1] not set");
|
|
109
|
-
}
|
|
110
|
-
if (argv1.endsWith(".mjs") || argv1.endsWith(".js")) {
|
|
111
|
-
return argv1;
|
|
112
|
-
}
|
|
113
|
-
// Treat argv[1] as a bin shim and assume sibling dist/index.mjs.
|
|
114
|
-
return path.resolve(path.dirname(argv1), "..", "dist", "index.mjs");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// The staged render entry: the prebuilt native binary when this platform has
|
|
118
|
-
// one, else the node bin shim. Same contract either way — read hookData on
|
|
119
|
-
// stdin, resolve ../dist/index.mjs by adjacency — so downstream code never
|
|
120
|
-
// cares which flavor was staged. [LAW:one-type-per-behavior]
|
|
121
|
-
type RenderEntry =
|
|
122
|
-
| { kind: "native"; sourcePath: string }
|
|
123
|
-
| { kind: "node-shim"; sourcePath: string };
|
|
124
|
-
|
|
125
|
-
function resolveRenderEntry(sourceDist: string): RenderEntry {
|
|
126
|
-
const key = `${process.platform}-${process.arch}`;
|
|
127
|
-
const pkgName = PLATFORM_PACKAGES[key];
|
|
128
|
-
if (pkgName) {
|
|
129
|
-
// Anchored to the bundle's real location, not this module's compiled
|
|
130
|
-
// form (which ts-jest loads as CJS where import.meta is illegal) — the
|
|
131
|
-
// platform package lives in node_modules beside the installed dist.
|
|
132
|
-
const require = createRequire(sourceDist);
|
|
133
|
-
try {
|
|
134
|
-
return {
|
|
135
|
-
kind: "native",
|
|
136
|
-
sourcePath: require.resolve(`${pkgName}/bin/cc-candybar`),
|
|
137
|
-
};
|
|
138
|
-
} catch {
|
|
139
|
-
// Optional dependency absent (unsupported install, pruned optionals).
|
|
140
|
-
// Falls through to the node shim — announced by the caller, never
|
|
141
|
-
// silent. [LAW:no-silent-failure]
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return {
|
|
145
|
-
kind: "node-shim",
|
|
146
|
-
sourcePath: path.resolve(
|
|
147
|
-
path.dirname(sourceDist),
|
|
148
|
-
"..",
|
|
149
|
-
"bin",
|
|
150
|
-
"cc-candybar",
|
|
151
|
-
),
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function stageFile(source: string, dest: string): void {
|
|
156
|
-
// Re-running install FROM the staged runtime makes source === dest;
|
|
157
|
-
// copyFileSync would truncate the file onto itself. Identity is "already
|
|
158
|
-
// staged", not an error.
|
|
159
|
-
if (path.resolve(source) === path.resolve(dest)) return;
|
|
160
|
-
fs.copyFileSync(source, dest);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// [LAW:one-source-of-truth] The staged file is the authority on what got
|
|
164
|
-
// staged. resolveRenderEntry's kind describes the *source lookup*, and on a
|
|
165
|
-
// re-run from the staged runtime that lookup resolves the identity path
|
|
166
|
-
// (source === dest), preserving whatever is on disk — possibly a native
|
|
167
|
-
// binary from a prior install that the lookup couldn't see. So the announced
|
|
168
|
-
// kind derives from the artifact itself: both flavors are ours, and the node
|
|
169
|
-
// shim is a "#!" script while the native binary is Mach-O/ELF.
|
|
170
|
-
function stagedEntryKind(binPath: string): RenderEntry["kind"] {
|
|
171
|
-
const fd = fs.openSync(binPath, "r");
|
|
172
|
-
try {
|
|
173
|
-
const magic = Buffer.alloc(2);
|
|
174
|
-
const bytesRead = fs.readSync(fd, magic, 0, 2, 0);
|
|
175
|
-
// [LAW:no-silent-failure] Under 2 bytes neither flavor exists — the file
|
|
176
|
-
// is a corrupt artifact (a crashed prior copy preserved by the identity
|
|
177
|
-
// path), not a native binary.
|
|
178
|
-
if (bytesRead < 2) {
|
|
179
|
-
throw new Error(
|
|
180
|
-
`install: staged render entry at ${binPath} is truncated ` +
|
|
181
|
-
`(${bytesRead} byte(s)). Re-run: pnpm dlx ${PACKAGE_NAME}@latest install`,
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
return magic.toString("latin1") === "#!" ? "node-shim" : "native";
|
|
185
|
-
} finally {
|
|
186
|
-
fs.closeSync(fd);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export interface StagedRuntime {
|
|
191
|
-
binPath: string;
|
|
192
|
-
distPath: string;
|
|
193
|
-
entryKind: RenderEntry["kind"];
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Stage the full runtime at the stable path: dist/index.mjs (daemon + CLI
|
|
197
|
-
// bundle) and bin/cc-candybar (render entry) as adjacent files. Adjacency IS
|
|
198
|
-
// the contract — every entry flavor locates the bundle via ../dist/index.mjs.
|
|
199
|
-
export function runStageRuntime(): StagedRuntime {
|
|
200
|
-
const sourceDist = locateBundledDist(process.argv[1]);
|
|
201
|
-
if (!fs.existsSync(sourceDist)) {
|
|
202
|
-
throw new Error(
|
|
203
|
-
`install: bundled dist not found at ${sourceDist}. Reinstall the package.`,
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const entry = resolveRenderEntry(sourceDist);
|
|
208
|
-
if (!fs.existsSync(entry.sourcePath)) {
|
|
209
|
-
throw new Error(
|
|
210
|
-
`install: render entry not found at ${entry.sourcePath}. Reinstall the package.`,
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
fs.mkdirSync(path.dirname(stagedDistPath()), { recursive: true });
|
|
215
|
-
fs.mkdirSync(path.dirname(stagedBinPath()), { recursive: true });
|
|
216
|
-
stageFile(sourceDist, stagedDistPath());
|
|
217
|
-
stageFile(entry.sourcePath, stagedBinPath());
|
|
218
|
-
fs.chmodSync(stagedBinPath(), 0o755);
|
|
219
|
-
|
|
220
|
-
// The pre-1.21 layout kept a second copy of the bundle as url-handler.mjs;
|
|
221
|
-
// dist/index.mjs is the one staged bundle now. Remove the stale copy so it
|
|
222
|
-
// can't drift. [LAW:one-source-of-truth]
|
|
223
|
-
fs.rmSync(path.join(supportDir(), "url-handler.mjs"), { force: true });
|
|
224
|
-
|
|
225
|
-
const stagedKind = stagedEntryKind(stagedBinPath());
|
|
226
|
-
process.stdout.write(
|
|
227
|
-
`Staged cc-candybar v${PACKAGE_VERSION} runtime at ${supportDir()}\n` +
|
|
228
|
-
(stagedKind === "native"
|
|
229
|
-
? ` render entry: native binary (${process.platform}-${process.arch})\n`
|
|
230
|
-
: ` render entry: node shim (no native binary for ${process.platform}-${process.arch}; renders are correct but pay node startup)\n`),
|
|
231
|
-
);
|
|
232
|
-
return {
|
|
233
|
-
binPath: stagedBinPath(),
|
|
234
|
-
distPath: stagedDistPath(),
|
|
235
|
-
entryKind: stagedKind,
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function infoPlistPatch(): Array<{ key: string; xml: string }> {
|
|
240
|
-
return [
|
|
241
|
-
{
|
|
242
|
-
key: "CFBundleIdentifier",
|
|
243
|
-
xml: `<string>${BUNDLE_ID}</string>`,
|
|
244
|
-
},
|
|
245
|
-
{
|
|
246
|
-
key: "CFBundleURLTypes",
|
|
247
|
-
xml: [
|
|
248
|
-
"<array>",
|
|
249
|
-
" <dict>",
|
|
250
|
-
" <key>CFBundleURLName</key>",
|
|
251
|
-
` <string>Claude Powerline Click Action</string>`,
|
|
252
|
-
" <key>CFBundleURLSchemes</key>",
|
|
253
|
-
" <array>",
|
|
254
|
-
` <string>${URL_SCHEME}</string>`,
|
|
255
|
-
" </array>",
|
|
256
|
-
" </dict>",
|
|
257
|
-
"</array>",
|
|
258
|
-
].join("\n"),
|
|
259
|
-
},
|
|
260
|
-
];
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Callers establish the macOS precondition ([LAW:single-enforcer] — the
|
|
264
|
-
// subcommand entry checks before any side effect; runInstall reaches here
|
|
265
|
-
// only through its darwin dispatch).
|
|
266
|
-
function installUrlHandlerFrom(stagedDist: string): void {
|
|
267
|
-
const bundle = appBundlePath();
|
|
268
|
-
fs.mkdirSync(path.dirname(bundle), { recursive: true });
|
|
269
|
-
|
|
270
|
-
// If a previous handler exists, remove it so osacompile can write fresh.
|
|
271
|
-
if (fs.existsSync(bundle)) {
|
|
272
|
-
fs.rmSync(bundle, { recursive: true, force: true });
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
process.stdout.write(`Building ${bundle}\n`);
|
|
276
|
-
const osa = launchSync({
|
|
277
|
-
bin: "/usr/bin/osacompile",
|
|
278
|
-
args: ["-o", bundle, "-e", appleScriptSource(process.execPath, stagedDist)],
|
|
279
|
-
category: "install.osacompile",
|
|
280
|
-
});
|
|
281
|
-
if (!osa.ok) {
|
|
282
|
-
process.stderr.write(osa.stderr);
|
|
283
|
-
throw new Error(`osacompile failed (${osa.reason})`);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const plistPath = path.join(bundle, "Contents", "Info.plist");
|
|
287
|
-
|
|
288
|
-
for (const { key } of infoPlistPatch()) {
|
|
289
|
-
// plutil errors if the key already exists; pre-delete so the operation is
|
|
290
|
-
// idempotent. Ignore failures (key may not exist on a fresh build).
|
|
291
|
-
launchSync({
|
|
292
|
-
bin: "/usr/bin/plutil",
|
|
293
|
-
args: ["-remove", key, plistPath],
|
|
294
|
-
category: "install.plutil",
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
for (const { key, xml } of infoPlistPatch()) {
|
|
299
|
-
const r = launchSync({
|
|
300
|
-
bin: "/usr/bin/plutil",
|
|
301
|
-
args: ["-insert", key, "-xml", xml, plistPath],
|
|
302
|
-
category: "install.plutil",
|
|
303
|
-
});
|
|
304
|
-
if (!r.ok) {
|
|
305
|
-
process.stderr.write(r.stderr);
|
|
306
|
-
throw new Error(`plutil -insert ${key} failed (${r.reason})`);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
process.stdout.write(`Registering ${URL_SCHEME}:// with Launch Services\n`);
|
|
311
|
-
const lsr = launchSync({
|
|
312
|
-
bin: "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister",
|
|
313
|
-
args: ["-f", bundle],
|
|
314
|
-
category: "install.lsregister",
|
|
315
|
-
});
|
|
316
|
-
if (!lsr.ok) {
|
|
317
|
-
process.stderr.write(lsr.stderr);
|
|
318
|
-
throw new Error(`lsregister failed (${lsr.reason})`);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
process.stdout.write(`✓ ${APP_NAME}.app installed and registered.\n`);
|
|
322
|
-
process.stdout.write(
|
|
323
|
-
` Test: open '${URL_SCHEME}://hello-world' && pbpaste\n`,
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
export function runInstallUrlHandler(): void {
|
|
328
|
-
// Precondition before any side effect: on a non-mac this fails with zero
|
|
329
|
-
// files written, not after staging the runtime.
|
|
330
|
-
ensureMacOS();
|
|
331
|
-
const staged = runStageRuntime();
|
|
332
|
-
installUrlHandlerFrom(staged.distPath);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
interface ParsedUrl {
|
|
336
|
-
verb: string;
|
|
337
|
-
value: string;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// [LAW:dataflow-not-control-flow] Parse the URL into a {verb, value} pair
|
|
341
|
-
// without using `new URL`, which lowercases hosts (would mangle case-sensitive
|
|
342
|
-
// session ids). Format: cc-candybar://<verb>/<tail> | cc-candybar://<value>
|
|
343
|
-
// (bare → copy). The verb ends at the FIRST `/`; everything after is the raw
|
|
344
|
-
// value. The dispatch effect list rides as `dispatch/e=…&e=…`, so its query-
|
|
345
|
-
// style payload is just the tail — `?` is NOT a delimiter, it is ordinary data
|
|
346
|
-
// in a bare-copy value (`cc-candybar://hello?world` copies "hello?world").
|
|
347
|
-
//
|
|
348
|
-
// [LAW:single-enforcer] Only the VERB is decoded here. The value is passed RAW
|
|
349
|
-
// to the daemon; each verb's handler decodes its own value at its boundary (the
|
|
350
|
-
// verb that owns the structure owns its decode). A whole-value decode here would
|
|
351
|
-
// un-escape structural separators inside a nested value — the exact hazard that
|
|
352
|
-
// made compound clicks unrepresentable — so it is deliberately absent.
|
|
353
|
-
export function parseHandlerUrl(
|
|
354
|
-
rawUrl: string,
|
|
355
|
-
scheme: string = URL_SCHEME,
|
|
356
|
-
): ParsedUrl {
|
|
357
|
-
const prefix = `${scheme}://`;
|
|
358
|
-
if (!rawUrl.startsWith(prefix)) {
|
|
359
|
-
throw new Error(`expected ${prefix} scheme, got: ${rawUrl}`);
|
|
360
|
-
}
|
|
361
|
-
const rest = rawUrl.slice(prefix.length);
|
|
362
|
-
const slash = rest.indexOf("/");
|
|
363
|
-
if (slash === -1) {
|
|
364
|
-
return { verb: VERB_COPY, value: rest };
|
|
365
|
-
}
|
|
366
|
-
return {
|
|
367
|
-
verb: decodeURIComponent(rest.slice(0, slash)),
|
|
368
|
-
value: rest.slice(slash + 1),
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// [LAW:single-enforcer] url-handle is a thin IPC shim: parse the URL, send
|
|
373
|
-
// the click request to the daemon, exit. There is NO in-process verb
|
|
374
|
-
// dispatch and NO direct disk mutation. The daemon is the only writer of
|
|
375
|
-
// click-side state ([LAW:one-source-of-truth] for SessionState); kicking a
|
|
376
|
-
// daemon on a transient failure is the only recovery, and it's
|
|
377
|
-
// fire-and-forget so the next click hits a warm daemon.
|
|
378
|
-
//
|
|
379
|
-
// A `permanent` daemon outcome (BAD_REQUEST for an unknown verb,
|
|
380
|
-
// VERSION_MISMATCH against a future daemon) exits non-zero with the daemon's
|
|
381
|
-
// error message so the failure is visible — never silently swallowed by a
|
|
382
|
-
// local fallback that would diverge from the daemon's truth.
|
|
383
|
-
export async function runUrlHandle(rawUrl: string | undefined): Promise<void> {
|
|
384
|
-
if (!rawUrl) {
|
|
385
|
-
process.stderr.write("url-handle: missing URL argument.\n");
|
|
386
|
-
process.exit(1);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
let parsed: ParsedUrl;
|
|
390
|
-
try {
|
|
391
|
-
parsed = parseHandlerUrl(rawUrl);
|
|
392
|
-
} catch (err) {
|
|
393
|
-
process.stderr.write(
|
|
394
|
-
`url-handle: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
395
|
-
);
|
|
396
|
-
process.exit(1);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const outcome = await tryClickViaDaemon(parsed.verb, parsed.value);
|
|
400
|
-
if (outcome.kind === "ok") {
|
|
401
|
-
process.exit(0);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (outcome.kind === "transient") {
|
|
405
|
-
// [LAW:dataflow-not-control-flow] Fire-and-forget kick; the user's click
|
|
406
|
-
// is lost (the daemon couldn't service it), but the next click hits a
|
|
407
|
-
// warm daemon. Mirrors the render-path's transient recovery.
|
|
408
|
-
obtainDaemonKick();
|
|
409
|
-
process.stderr.write(
|
|
410
|
-
`url-handle: daemon unavailable (${outcome.cause}: ${outcome.message})\n`,
|
|
411
|
-
);
|
|
412
|
-
process.exit(1);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// [LAW:dataflow-not-control-flow] Format each permanent cause from its
|
|
416
|
-
// own typed payload, not by probing for "message" on a generic outcome.
|
|
417
|
-
// The PermanentOutcome union already discriminates by `cause`; the switch
|
|
418
|
-
// mirrors that discriminator one-to-one and pulls the right fields.
|
|
419
|
-
process.stderr.write(formatPermanent(outcome) + "\n");
|
|
420
|
-
process.exit(1);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// [LAW:single-enforcer] One place that turns a PermanentOutcome into a
|
|
424
|
-
// human-readable diagnostic. Each cause carries its own payload (version
|
|
425
|
-
// mismatch carries the protocol numbers; everything else carries a
|
|
426
|
-
// message); the formatter consumes exactly the fields the cause defines.
|
|
427
|
-
function formatPermanent(outcome: PermanentOutcome): string {
|
|
428
|
-
switch (outcome.cause) {
|
|
429
|
-
case "version_mismatch":
|
|
430
|
-
return `url-handle: daemon rejected click (version mismatch: client v${outcome.clientV} ≠ daemon v${outcome.daemonV})`;
|
|
431
|
-
case "bad_request":
|
|
432
|
-
return `url-handle: daemon rejected click (bad request: ${outcome.message})`;
|
|
433
|
-
case "render_failed":
|
|
434
|
-
return `url-handle: daemon rejected click (handler failed: ${outcome.message})`;
|
|
435
|
-
case "malformed_response":
|
|
436
|
-
return `url-handle: daemon rejected click (malformed response: ${outcome.message})`;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// [LAW:effects-at-boundaries] Pure string builder — runInstall performs the
|
|
441
|
-
// actual write. Kept separate so the message content is testable without
|
|
442
|
-
// driving the full (fs + Launch Services) install side effects.
|
|
443
|
-
// [LAW:one-source-of-truth] The disclosure glyph comes from config/disclosure.ts,
|
|
444
|
-
// the same constant the theme/look picker itself renders with, so this tip
|
|
445
|
-
// can't drift from what the bundled default bar actually shows.
|
|
446
|
-
function installSuccessMessage(): string {
|
|
447
|
-
return (
|
|
448
|
-
`✓ install complete.\n` +
|
|
449
|
-
` Restart Claude Code to pick up the new statusline.\n` +
|
|
450
|
-
` Tip: every bar carries a settings menu — click ☰ ${DISCLOSURE_GLYPH_CLOSED} for preset\n` +
|
|
451
|
-
` switching, edit mode, and clickable theme/look/style/wrap/padding controls.\n`
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
export async function runInstall(rendererArgs: string[]): Promise<void> {
|
|
456
|
-
const force = rendererArgs.includes("--force");
|
|
457
|
-
const filteredArgs = rendererArgs.filter((a) => a !== "--force");
|
|
458
|
-
|
|
459
|
-
const argsToInstall =
|
|
460
|
-
filteredArgs.length > 0 ? filteredArgs : [...DEFAULT_INSTALL_ARGS];
|
|
461
|
-
|
|
462
|
-
const staged = runStageRuntime();
|
|
463
|
-
|
|
464
|
-
if (process.platform === "darwin") {
|
|
465
|
-
installUrlHandlerFrom(staged.distPath);
|
|
466
|
-
} else {
|
|
467
|
-
process.stdout.write(
|
|
468
|
-
"Skipping URL handler (cmd-click verbs are macOS-only).\n",
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
updateClaudeSettings(staged.binPath, argsToInstall, force);
|
|
473
|
-
|
|
474
|
-
process.stdout.write(installSuccessMessage());
|
|
475
|
-
|
|
476
|
-
// Last: a stale-version warning is the final thing on screen, and the
|
|
477
|
-
// lookup starts only after the synchronous work above — spawnSync blocks
|
|
478
|
-
// the event loop, so a fetch started earlier could not progress and would
|
|
479
|
-
// burn its timeout budget idle. [LAW:no-ambient-temporal-coupling] The
|
|
480
|
-
// staged runtime works either way; the verdict informs, it never fails the
|
|
481
|
-
// install. [LAW:no-silent-failure]
|
|
482
|
-
const report = currencyReport(
|
|
483
|
-
PACKAGE_NAME,
|
|
484
|
-
assessCurrency(
|
|
485
|
-
PACKAGE_VERSION,
|
|
486
|
-
await fetchLatestVersion(PACKAGE_NAME, fetch),
|
|
487
|
-
),
|
|
488
|
-
);
|
|
489
|
-
process[report.stream].write(report.text);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function updateClaudeSettings(
|
|
493
|
-
binPath: string,
|
|
494
|
-
rendererArgs: readonly string[],
|
|
495
|
-
force: boolean,
|
|
496
|
-
overridePath?: string,
|
|
497
|
-
): void {
|
|
498
|
-
const target = overridePath ?? settingsJsonPath();
|
|
499
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
500
|
-
|
|
501
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
502
|
-
let settings: Record<string, any> = {};
|
|
503
|
-
if (fs.existsSync(target)) {
|
|
504
|
-
try {
|
|
505
|
-
settings = JSON.parse(fs.readFileSync(target, "utf-8"));
|
|
506
|
-
} catch (err) {
|
|
507
|
-
throw new Error(
|
|
508
|
-
`Could not parse ${target}: ${err instanceof Error ? err.message : String(err)}`,
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
const existing = settings.statusLine?.command as string | undefined;
|
|
514
|
-
// [LAW:one-source-of-truth] Detection: a command we (or a prior version of
|
|
515
|
-
// us) wrote either starts with the legacy `pnpm dlx` form (an open prefix —
|
|
516
|
-
// a version suffix follows) or has the staged bin path — quoted or bare —
|
|
517
|
-
// as its entire first token. Any other value is a user customization we
|
|
518
|
-
// must not silently destroy.
|
|
519
|
-
// [LAW:types-are-the-program] Token, not prefix: a bare startsWith(binPath)
|
|
520
|
-
// would also claim `<binPath>-backup …` as ours and overwrite it.
|
|
521
|
-
const managedTokens = [binPath, shellEscape(binPath)];
|
|
522
|
-
const isOurs =
|
|
523
|
-
typeof existing === "string" &&
|
|
524
|
-
(existing.startsWith(`pnpm dlx ${PACKAGE_NAME}@`) ||
|
|
525
|
-
managedTokens.some(
|
|
526
|
-
(token) => existing === token || existing.startsWith(`${token} `),
|
|
527
|
-
));
|
|
528
|
-
|
|
529
|
-
if (existing && !isOurs && !force) {
|
|
530
|
-
process.stderr.write(
|
|
531
|
-
`Skipping settings.json update: existing statusLine.command appears customized.\n` +
|
|
532
|
-
` Current: ${existing}\n` +
|
|
533
|
-
` To overwrite, re-run with --force.\n`,
|
|
534
|
-
);
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
settings.statusLine = {
|
|
539
|
-
type: "command",
|
|
540
|
-
command: buildStatusLineCommand(binPath, rendererArgs),
|
|
541
|
-
};
|
|
542
|
-
|
|
543
|
-
fs.writeFileSync(target, JSON.stringify(settings, null, 2));
|
|
544
|
-
process.stdout.write(`Updated ${target}\n`);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// Exports for testing
|
|
548
|
-
export const __test__ = {
|
|
549
|
-
shellEscape,
|
|
550
|
-
buildStatusLineCommand,
|
|
551
|
-
DEFAULT_INSTALL_ARGS,
|
|
552
|
-
updateClaudeSettings,
|
|
553
|
-
resolveRenderEntry,
|
|
554
|
-
stageFile,
|
|
555
|
-
stagedEntryKind,
|
|
556
|
-
installSuccessMessage,
|
|
557
|
-
};
|