@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/help-text.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { DISCLOSURE_GLYPH_CLOSED } from "./config/disclosure";
|
|
2
|
-
import { HELP_GLYPH_CLOSED } from "./config/help";
|
|
3
|
-
import { NODE_FLAGS } from "./cli-flags";
|
|
4
|
-
|
|
5
|
-
// [LAW:effects-at-boundaries] Pure data, no I/O — index.ts owns the console.log
|
|
6
|
-
// effect. Kept as its own module so the text is importable (and testable) without
|
|
7
|
-
// pulling in index.ts's top-level `main()` call.
|
|
8
|
-
// [LAW:one-source-of-truth] The disclosure glyph comes from config/disclosure.ts
|
|
9
|
-
// (the same constant the theme/look picker itself renders with), so this text
|
|
10
|
-
// can't drift from what a user actually sees on the bar.
|
|
11
|
-
// [LAW:one-source-of-truth] THE help corpus, as data. `--help` and the bar's
|
|
12
|
-
// own `(?)` disclosures are two RENDERINGS of these arrays, never two copies of
|
|
13
|
-
// the sentences: a `(?)` segment's template IS one of these strings, and the
|
|
14
|
-
// paragraphs below interpolate the same values. A help sentence typed into a
|
|
15
|
-
// segment template — where nothing would ever notice it drifting from the CLI's
|
|
16
|
-
// wording — is the defect this shape exists to make unrepresentable.
|
|
17
|
-
//
|
|
18
|
-
// [LAW:representation] One line is one CELL on the bar, so each is a complete
|
|
19
|
-
// thought that stands alone and each stays short: `(?)` bodies drop below their
|
|
20
|
-
// row, and a body that overflows `term.cols` wraps into more rows than the fact
|
|
21
|
-
// it explains is worth. Every line leads with the glyph it explains, so the
|
|
22
|
-
// reader matches text to affordance by shape rather than by reading order.
|
|
23
|
-
export const EDIT_MODE_HELP = [
|
|
24
|
-
"+ inserts here",
|
|
25
|
-
"- removes the one left of it",
|
|
26
|
-
"↺ undoes edits",
|
|
27
|
-
] as const;
|
|
28
|
-
|
|
29
|
-
export const PERSIST_HELP = [
|
|
30
|
-
"☐ this session only",
|
|
31
|
-
"☑ default for every session",
|
|
32
|
-
] as const;
|
|
33
|
-
|
|
34
|
-
export const HELP_TEXT = `
|
|
35
|
-
cc-candybar - Beautiful powerline statusline for Claude Code
|
|
36
|
-
|
|
37
|
-
Usage: cc-candybar [options]
|
|
38
|
-
|
|
39
|
-
Standalone Commands:
|
|
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>)
|
|
42
|
-
|
|
43
|
-
Debugging:
|
|
44
|
-
CC_CANDYBAR_DEBUG=1 Enable debug logging for troubleshooting
|
|
45
|
-
|
|
46
|
-
Configuration:
|
|
47
|
-
Layout and segment options are defined in .cc-candybar.json5 (place in your
|
|
48
|
-
project dir, cwd, or ~/.config/cc-candybar/config.json5). Use CC_CANDYBAR_CONFIG
|
|
49
|
-
to point at a specific file. See the default config for all available options:
|
|
50
|
-
node dist/index.mjs debug --project-dir . --cwd .
|
|
51
|
-
|
|
52
|
-
Every bar carries a settings menu, whatever your config says — no config
|
|
53
|
-
needed, and writing your own \`root\` cannot delete it. Click
|
|
54
|
-
☰ ${DISCLOSURE_GLYPH_CLOSED} on the bar for preset switching, edit mode, and a config menu
|
|
55
|
-
of clickable theme/look/style/wrap/padding controls. The \`persist?\`
|
|
56
|
-
checkbox there chooses where a change lands: ${PERSIST_HELP.join(", ")}.
|
|
57
|
-
|
|
58
|
-
Anywhere the bar shows ${HELP_GLYPH_CLOSED}, clicking it reveals these same instructions
|
|
59
|
-
in place. In edit mode: ${EDIT_MODE_HELP.join(", ")}.
|
|
60
|
-
|
|
61
|
-
Subcommands:
|
|
62
|
-
install One-shot setup: stages the runtime (native render
|
|
63
|
-
binary + dist bundle) at a stable path, creates the
|
|
64
|
-
URL handler app + cc-candybar:// scheme (macOS), and
|
|
65
|
-
writes the staged entry as the statusLine command in
|
|
66
|
-
~/.claude/settings.json. Re-run to update.
|
|
67
|
-
install-url-handler Just stage the runtime and create + register the URL
|
|
68
|
-
handler app (macOS only).
|
|
69
|
-
url-handle URL Internal — invoked by the URL handler app on
|
|
70
|
-
cmd-click. Parses cc-candybar://<verb>/<value> and
|
|
71
|
-
dispatches (currently: copy to clipboard).
|
|
72
|
-
daemon-stats [--json] Query the running daemon for runtime stats:
|
|
73
|
-
uptime, RSS, cache hit rates, watcher count,
|
|
74
|
-
request totals. Does not spawn a daemon.
|
|
75
|
-
|
|
76
|
-
Config tooling:
|
|
77
|
-
check [config-file] Validate a config on the full render pipeline (parse
|
|
78
|
-
→ merge → validate → register → render) with no
|
|
79
|
-
daemon. With no path, checks the same file the daemon
|
|
80
|
-
would load from here. Exit 0 clean (warnings on
|
|
81
|
-
stderr), 1 invalid, 2 unreadable. "lint" is an alias.
|
|
82
|
-
schema Print the JSON Schema for the config file shape
|
|
83
|
-
(.cc-candybar.json5). Point an editor's $schema at it
|
|
84
|
-
for autocomplete + structural validation.
|
|
85
|
-
vars [--json] Declared variables: source kind, value, last error.
|
|
86
|
-
segments [--json] Segment templates and their last rendered output.
|
|
87
|
-
config [--json] The effective merged config. (All three query the
|
|
88
|
-
running daemon; none spawn one.)
|
|
89
|
-
|
|
90
|
-
`;
|
package/src/index.ts
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import type { ClaudeHookData } from "./utils/claude";
|
|
4
|
-
|
|
5
|
-
import process from "node:process";
|
|
6
|
-
import { json } from "node:stream/consumers";
|
|
7
|
-
import { debug } from "./utils/logger";
|
|
8
|
-
import { runInstall, runInstallUrlHandler, runUrlHandle } from "./install";
|
|
9
|
-
import { runDaemon } from "./daemon/server";
|
|
10
|
-
import { tryRenderViaDaemon } from "./daemon/client";
|
|
11
|
-
import { runDaemonStats } from "./daemon/client-stats";
|
|
12
|
-
import { runDebug } from "./daemon/client-debug";
|
|
13
|
-
import { isDebugWhat } from "./daemon/debug-types";
|
|
14
|
-
import { runSchema } from "./config/cli";
|
|
15
|
-
import { runCheck } from "./check";
|
|
16
|
-
import { obtainDaemonKick } from "./daemon/acquire";
|
|
17
|
-
import { planOutcome } from "./render/outcome-plan";
|
|
18
|
-
import { HELP_TEXT } from "./help-text";
|
|
19
|
-
import { NODE_FLAGS } from "./cli-flags";
|
|
20
|
-
import { PACKAGE_VERSION } from "./version";
|
|
21
|
-
|
|
22
|
-
// Read terminal width from the live shell context (no subprocess). Returns
|
|
23
|
-
// undefined when nothing reliable is available; the daemon falls back to its
|
|
24
|
-
// own pure lookup chain in that case. Always-COLUMNS-first because Bash
|
|
25
|
-
// exports it on resize and Claude Code propagates it to hook commands.
|
|
26
|
-
// stderr (not stdout) is the TTY-side fallback: when invoked as a Claude
|
|
27
|
-
// statusline hook, stdin is the hook JSON pipe and stdout is the captured
|
|
28
|
-
// statusline pipe, leaving stderr as the only stream still attached to the
|
|
29
|
-
// parent terminal. Mirrors the Rust client's TIOCGWINSZ-on-STDERR_FILENO.
|
|
30
|
-
function detectTermCols(): number | undefined {
|
|
31
|
-
const env = process.env.COLUMNS;
|
|
32
|
-
if (env) {
|
|
33
|
-
const n = parseInt(env, 10);
|
|
34
|
-
if (!isNaN(n) && n > 0) return n;
|
|
35
|
-
}
|
|
36
|
-
const cols = process.stderr.columns;
|
|
37
|
-
if (cols && cols > 0) return cols;
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// The env vars an SSH login shell inherits from sshd. Any one of them present
|
|
42
|
-
// and non-empty means this session arrived over the network.
|
|
43
|
-
//
|
|
44
|
-
// [LAW:one-source-of-truth] This vocabulary is mirrored by the Rust client
|
|
45
|
-
// (rust-client/src/main.rs) and diffed by scripts/check-protocol.mjs, which
|
|
46
|
-
// anchors on the declaration below — keep it a named const holding string
|
|
47
|
-
// literals, or repoint the CHECKS row in the same commit. Both runtimes must
|
|
48
|
-
// agree on what "SSH" means or the fast path and the fallback path would
|
|
49
|
-
// disagree about the same session.
|
|
50
|
-
//
|
|
51
|
-
// All three are checked, not just SSH_CONNECTION: SSH_CLIENT is what older
|
|
52
|
-
// sshd builds (and the user's git-taculous zsh theme) key on, and SSH_TTY is
|
|
53
|
-
// the one that survives some `sudo` env_keep policies. Extra names can only
|
|
54
|
-
// widen recall of a fact that is otherwise reported as a plain `false`.
|
|
55
|
-
const SSH_ENV_VARS = ["SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY"] as const;
|
|
56
|
-
|
|
57
|
-
const hasFlag = (flags: readonly string[]): boolean =>
|
|
58
|
-
flags.some((f) => process.argv.includes(f));
|
|
59
|
-
|
|
60
|
-
// [LAW:dataflow-not-control-flow] A fold over the vocabulary, not a chain of
|
|
61
|
-
// ifs — adding a name is a data edit.
|
|
62
|
-
//
|
|
63
|
-
// Unlike detectTermCols this is TOTAL: the client reads its own environment, so
|
|
64
|
-
// "no SSH var set" is the affirmative answer "local", never a failure to
|
|
65
|
-
// determine. It therefore always reports, and the daemon reads an ABSENT `ssh`
|
|
66
|
-
// hint as "this client is too old to answer" rather than as "local".
|
|
67
|
-
function detectSsh(): boolean {
|
|
68
|
-
return SSH_ENV_VARS.some((name) => (process.env[name] ?? "") !== "");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function showHelpText(): void {
|
|
72
|
-
console.log(HELP_TEXT);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function main(): Promise<void> {
|
|
76
|
-
try {
|
|
77
|
-
if (hasFlag(NODE_FLAGS.help)) {
|
|
78
|
-
showHelpText();
|
|
79
|
-
process.exit(0);
|
|
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
|
-
}
|
|
89
|
-
|
|
90
|
-
// [LAW:dataflow-not-control-flow] Subcommand dispatch is data: argv[2]
|
|
91
|
-
// selects the handler. Each handler short-circuits via process.exit().
|
|
92
|
-
// Default fallthrough = the existing stdin-driven render flow.
|
|
93
|
-
const subcommand = process.argv[2];
|
|
94
|
-
if (subcommand === "install") {
|
|
95
|
-
await runInstall(process.argv.slice(3));
|
|
96
|
-
process.exit(0);
|
|
97
|
-
}
|
|
98
|
-
if (subcommand === "install-url-handler") {
|
|
99
|
-
runInstallUrlHandler();
|
|
100
|
-
process.exit(0);
|
|
101
|
-
}
|
|
102
|
-
if (subcommand === "url-handle") {
|
|
103
|
-
await runUrlHandle(process.argv[3]);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (subcommand === "daemon") {
|
|
107
|
-
runDaemon();
|
|
108
|
-
return; // daemon owns its own lifecycle
|
|
109
|
-
}
|
|
110
|
-
if (subcommand === "daemon-stats") {
|
|
111
|
-
await runDaemonStats(process.argv.slice(3));
|
|
112
|
-
process.exit(0);
|
|
113
|
-
}
|
|
114
|
-
// [LAW:one-type-per-behavior] `lint` is an alias of `check` — one config
|
|
115
|
-
// verdict, one pipeline, one exit-code contract (0/1/2). check subsumes the
|
|
116
|
-
// old lint (same loader, plus register + render coverage).
|
|
117
|
-
if (subcommand === "check" || subcommand === "lint") {
|
|
118
|
-
runCheck(process.argv.slice(3)); // owns its own exit code (0/1/2)
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
if (subcommand === "schema") {
|
|
122
|
-
runSchema(); // owns its own exit code
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
// [LAW:dataflow-not-control-flow] vars/segments/config are ONE handler
|
|
126
|
-
// parameterized by `what` — the subcommand name IS the DebugWhat. The guard
|
|
127
|
-
// is the canonical list (debug-types), so a new debug projection is reachable
|
|
128
|
-
// here with no second-site edit.
|
|
129
|
-
if (isDebugWhat(subcommand)) {
|
|
130
|
-
await runDebug(subcommand, process.argv.slice(3));
|
|
131
|
-
process.exit(0);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (process.stdin.isTTY === true) {
|
|
135
|
-
console.error(`Error: This tool requires input from Claude Code
|
|
136
|
-
|
|
137
|
-
cc-candybar is designed to be used as a Claude Code statusLine command.
|
|
138
|
-
It reads hook data from stdin and outputs formatted statusline.
|
|
139
|
-
|
|
140
|
-
Add to ~/.claude/settings.json:
|
|
141
|
-
{
|
|
142
|
-
"statusLine": {
|
|
143
|
-
"type": "command",
|
|
144
|
-
"command": "cc-candybar --style=powerline"
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
Run with --help for more options.
|
|
149
|
-
|
|
150
|
-
To test output manually:
|
|
151
|
-
echo '{"session_id":"test-session","workspace":{"project_dir":"/path/to/project"},"model":{"id":"claude-sonnet-4-5","display_name":"Claude"}}' | cc-candybar --style=powerline`);
|
|
152
|
-
process.exit(1);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
debug(`Working directory: ${process.cwd()}`);
|
|
156
|
-
debug(`Process args:`, process.argv);
|
|
157
|
-
|
|
158
|
-
const hookData = (await json(process.stdin)) as ClaudeHookData;
|
|
159
|
-
debug(`Received hook data:`, JSON.stringify(hookData, null, 2));
|
|
160
|
-
|
|
161
|
-
if (!hookData) {
|
|
162
|
-
console.error("Error: No input data received from stdin");
|
|
163
|
-
showHelpText();
|
|
164
|
-
process.exit(1);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// [LAW:one-source-of-truth] The daemon is the *only* renderer. The CLI is
|
|
168
|
-
// a dumb relay: forward stdin to the daemon, print whatever comes back.
|
|
169
|
-
// There is no inline render path — two renderers would drift (the CLI has
|
|
170
|
-
// no shared gitService/usageProvider, no per-session state, no warm
|
|
171
|
-
// caches). On daemon miss we spawn detached and emit empty output; the
|
|
172
|
-
// next status-line refresh hits the warm daemon and renders for real.
|
|
173
|
-
//
|
|
174
|
-
// [LAW:single-enforcer] Client hints are captured here, in the user's
|
|
175
|
-
// shell environment, then trusted by the daemon. The daemon's own env
|
|
176
|
-
// reflects whichever shell launched it minutes/hours ago, so it can
|
|
177
|
-
// measure neither the active terminal nor whether THIS session came in
|
|
178
|
-
// over SSH — only the live client can. One daemon serves a local session
|
|
179
|
-
// and an SSH session at the same time, so the answer genuinely differs per
|
|
180
|
-
// request.
|
|
181
|
-
const outcome = await tryRenderViaDaemon(
|
|
182
|
-
hookData,
|
|
183
|
-
process.argv,
|
|
184
|
-
process.cwd(),
|
|
185
|
-
{ termCols: detectTermCols(), ssh: detectSsh() },
|
|
186
|
-
);
|
|
187
|
-
// [LAW:types-are-the-program] Three variants, one per outcome kind. The
|
|
188
|
-
// "kick on every failure" pattern was the load-bearing half of the
|
|
189
|
-
// 452-corpse spiral (kz8.5) — kicking on `permanent` failures keeps
|
|
190
|
-
// respawning a daemon that will refuse the next request identically.
|
|
191
|
-
// [LAW:dataflow-not-control-flow] planOutcome maps each variant to a
|
|
192
|
-
// plan value (output, kick, debug); the side effects below run against
|
|
193
|
-
// the plan in fixed order. Variability lives in the data.
|
|
194
|
-
const plan = planOutcome(outcome);
|
|
195
|
-
if (plan.debug !== null) {
|
|
196
|
-
debug(plan.debug);
|
|
197
|
-
}
|
|
198
|
-
if (plan.kick) {
|
|
199
|
-
obtainDaemonKick();
|
|
200
|
-
}
|
|
201
|
-
process.stdout.write(plan.output);
|
|
202
|
-
process.exit(0);
|
|
203
|
-
} catch (error) {
|
|
204
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
205
|
-
console.error("Error generating statusline:", errorMessage);
|
|
206
|
-
process.exit(1);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
main();
|
package/src/install/currency.ts
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
// Is the runtime `install` just staged the newest published release? pnpm
|
|
2
|
-
// resolves `@latest` through its dlx cache and its release-age gate
|
|
3
|
-
// (`minimumReleaseAge`), either of which can hand back an older release with
|
|
4
|
-
// no error — so `pnpm dlx @promptctl/cc-candybar@latest install` can stage a
|
|
5
|
-
// version several releases behind and report success. This module answers the
|
|
6
|
-
// currency question as data: a strict version parser at the border, the
|
|
7
|
-
// registry lookup as an `Outcome`, one total fold into a `Currency`, and one
|
|
8
|
-
// formatter that describes the report for `runInstall` to perform.
|
|
9
|
-
|
|
10
|
-
import { ABSENT, failed, type Outcome } from "../utils/outcome";
|
|
11
|
-
|
|
12
|
-
// [LAW:types-are-the-program] A release version is exactly MAJOR.MINOR.PATCH.
|
|
13
|
-
// semantic-release on `main` mints nothing else, and the ordering the currency
|
|
14
|
-
// verdict needs is total only over that shape — so the parser refuses anything
|
|
15
|
-
// else rather than admitting a prerelease it could not order.
|
|
16
|
-
export type Version = readonly [major: number, minor: number, patch: number];
|
|
17
|
-
|
|
18
|
-
const RELEASE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
19
|
-
|
|
20
|
-
// A parse either produces a value or fails with a reason; "absent" is not a
|
|
21
|
-
// shape text can have.
|
|
22
|
-
type Parsed<T> = Exclude<Outcome<T>, { kind: "absent" }>;
|
|
23
|
-
|
|
24
|
-
// [LAW:parse-dont-validate] The one crossing from text to `Version`. Both the
|
|
25
|
-
// baked stamp and the registry's dist-tag pass through here; downstream code
|
|
26
|
-
// takes `Version` and never re-checks the shape. The failure is typed, not
|
|
27
|
-
// thrown: on either side it flows into the `unchecked` verdict, so the
|
|
28
|
-
// advisory check can never take the install down with it.
|
|
29
|
-
export function parseReleaseVersion(text: string): Parsed<Version> {
|
|
30
|
-
const m = RELEASE_VERSION.exec(text);
|
|
31
|
-
return m
|
|
32
|
-
? { kind: "ok", value: [Number(m[1]), Number(m[2]), Number(m[3])] }
|
|
33
|
-
: {
|
|
34
|
-
kind: "failed",
|
|
35
|
-
reason: `"${text}" is not a release version (expected MAJOR.MINOR.PATCH)`,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function formatVersion(v: Version): string {
|
|
40
|
-
return v.join(".");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Lexicographic over the triple: the first differing component decides.
|
|
44
|
-
function compareVersions(
|
|
45
|
-
[aMajor, aMinor, aPatch]: Version,
|
|
46
|
-
[bMajor, bMinor, bPatch]: Version,
|
|
47
|
-
): number {
|
|
48
|
-
return aMajor - bMajor || aMinor - bMinor || aPatch - bPatch;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export const REGISTRY_URL = "https://registry.npmjs.org";
|
|
52
|
-
|
|
53
|
-
// The check rides at the end of an install; it may not hang one. A registry
|
|
54
|
-
// that answers slower than this is reported as unreachable, not waited on.
|
|
55
|
-
export const REGISTRY_TIMEOUT_MS = 5_000;
|
|
56
|
-
|
|
57
|
-
// [LAW:effects-at-boundaries] The one network effect in the install path. It
|
|
58
|
-
// takes `fetch` as a parameter so the pure core above and the tests never
|
|
59
|
-
// touch the wire; the caller hands in the global. Every way the lookup can
|
|
60
|
-
// fail to answer — refused, timed out, non-2xx, unparseable body, dist-tag not
|
|
61
|
-
// a release — collapses to `failed` with its reason preserved, and a registry
|
|
62
|
-
// that lists no `latest` tag at all is `absent`. [LAW:no-silent-failure] None
|
|
63
|
-
// of those is ever reported as "current".
|
|
64
|
-
export async function fetchLatestVersion(
|
|
65
|
-
packageName: string,
|
|
66
|
-
fetchImpl: typeof fetch,
|
|
67
|
-
): Promise<Outcome<Version>> {
|
|
68
|
-
const url = `${REGISTRY_URL}/-/package/${encodeURIComponent(packageName)}/dist-tags`;
|
|
69
|
-
try {
|
|
70
|
-
const res = await fetchImpl(url, {
|
|
71
|
-
signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS),
|
|
72
|
-
});
|
|
73
|
-
if (!res.ok) {
|
|
74
|
-
return failed(`registry responded ${res.status} for ${url}`);
|
|
75
|
-
}
|
|
76
|
-
const tags = (await res.json()) as { latest?: unknown };
|
|
77
|
-
if (typeof tags.latest !== "string") {
|
|
78
|
-
return ABSENT;
|
|
79
|
-
}
|
|
80
|
-
return parseReleaseVersion(tags.latest);
|
|
81
|
-
} catch (err) {
|
|
82
|
-
return failed(err instanceof Error ? err.message : String(err));
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// [LAW:types-are-the-program] The verdict. `unchecked` is its own arm rather
|
|
87
|
-
// than a `current` with a flag: an install that could not compare has no
|
|
88
|
-
// currency, and no consumer may read it as having one. It carries the stamp
|
|
89
|
-
// as text because the stamp itself may be what failed to parse.
|
|
90
|
-
export type Currency =
|
|
91
|
-
| { readonly kind: "current"; readonly installed: Version }
|
|
92
|
-
| {
|
|
93
|
-
readonly kind: "stale";
|
|
94
|
-
readonly installed: Version;
|
|
95
|
-
readonly latest: Version;
|
|
96
|
-
}
|
|
97
|
-
| {
|
|
98
|
-
readonly kind: "ahead";
|
|
99
|
-
readonly installed: Version;
|
|
100
|
-
readonly latest: Version;
|
|
101
|
-
}
|
|
102
|
-
| {
|
|
103
|
-
readonly kind: "unchecked";
|
|
104
|
-
readonly installed: string;
|
|
105
|
-
readonly reason: string;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
// [LAW:dataflow-not-control-flow] One total fold: the stamp, the lookup
|
|
109
|
-
// outcome, and the version ordering all flow in as values, and every
|
|
110
|
-
// combination lands in exactly one arm.
|
|
111
|
-
export function assessCurrency(
|
|
112
|
-
stamp: string,
|
|
113
|
-
latest: Outcome<Version>,
|
|
114
|
-
): Currency {
|
|
115
|
-
const installed = parseReleaseVersion(stamp);
|
|
116
|
-
if (installed.kind === "failed") {
|
|
117
|
-
return { kind: "unchecked", installed: stamp, reason: installed.reason };
|
|
118
|
-
}
|
|
119
|
-
switch (latest.kind) {
|
|
120
|
-
case "failed":
|
|
121
|
-
return { kind: "unchecked", installed: stamp, reason: latest.reason };
|
|
122
|
-
case "absent":
|
|
123
|
-
return {
|
|
124
|
-
kind: "unchecked",
|
|
125
|
-
installed: stamp,
|
|
126
|
-
reason: "the registry lists no `latest` dist-tag",
|
|
127
|
-
};
|
|
128
|
-
case "ok": {
|
|
129
|
-
const order = compareVersions(installed.value, latest.value);
|
|
130
|
-
if (order < 0) {
|
|
131
|
-
return {
|
|
132
|
-
kind: "stale",
|
|
133
|
-
installed: installed.value,
|
|
134
|
-
latest: latest.value,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
if (order > 0) {
|
|
138
|
-
return {
|
|
139
|
-
kind: "ahead",
|
|
140
|
-
installed: installed.value,
|
|
141
|
-
latest: latest.value,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
return { kind: "current", installed: installed.value };
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// A description of the report, not the report itself: `runInstall` owns the
|
|
150
|
-
// write. Warnings go to stderr, confirmations to stdout — the CLI's stream
|
|
151
|
-
// contract, so a scripted install can separate the two.
|
|
152
|
-
export interface CurrencyReport {
|
|
153
|
-
readonly stream: "stdout" | "stderr";
|
|
154
|
-
readonly text: string;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// [LAW:no-silent-failure] The stale arm names the cause and the exact command
|
|
158
|
-
// that gets the current release now; the unchecked arm says the check was
|
|
159
|
-
// skipped and why, and never implies the install is current.
|
|
160
|
-
export function currencyReport(
|
|
161
|
-
packageName: string,
|
|
162
|
-
currency: Currency,
|
|
163
|
-
): CurrencyReport {
|
|
164
|
-
switch (currency.kind) {
|
|
165
|
-
case "current":
|
|
166
|
-
return {
|
|
167
|
-
stream: "stdout",
|
|
168
|
-
text: `✓ cc-candybar ${formatVersion(currency.installed)} is the latest release.\n`,
|
|
169
|
-
};
|
|
170
|
-
case "ahead":
|
|
171
|
-
return {
|
|
172
|
-
stream: "stdout",
|
|
173
|
-
text:
|
|
174
|
-
`cc-candybar ${formatVersion(currency.installed)} is newer than the registry's latest release ` +
|
|
175
|
-
`(${formatVersion(currency.latest)}): an unpublished build.\n`,
|
|
176
|
-
};
|
|
177
|
-
case "stale": {
|
|
178
|
-
const latest = formatVersion(currency.latest);
|
|
179
|
-
return {
|
|
180
|
-
stream: "stderr",
|
|
181
|
-
text:
|
|
182
|
-
`⚠ cc-candybar ${formatVersion(currency.installed)} was staged, but the latest release is ${latest}.\n` +
|
|
183
|
-
` pnpm's release-age gate (minimumReleaseAge) and its dlx cache can both\n` +
|
|
184
|
-
` resolve \`@latest\` to an older release without saying so.\n` +
|
|
185
|
-
` To install ${latest} now, name it explicitly:\n` +
|
|
186
|
-
` pnpm dlx ${packageName}@${latest} install\n`,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
case "unchecked":
|
|
190
|
-
return {
|
|
191
|
-
stream: "stderr",
|
|
192
|
-
text:
|
|
193
|
-
`⚠ Could not check for a cc-candybar release newer than ${currency.installed} ` +
|
|
194
|
-
`(${currency.reason}); the registry check was skipped.\n`,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
}
|