@promptctl/cc-candybar 1.42.0 → 1.42.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptctl/cc-candybar",
3
- "version": "1.42.0",
3
+ "version": "1.42.1",
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.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"
94
+ "@promptctl/cc-candybar-darwin-arm64": "1.42.1",
95
+ "@promptctl/cc-candybar-darwin-x64": "1.42.1",
96
+ "@promptctl/cc-candybar-linux-x64": "1.42.1",
97
+ "@promptctl/cc-candybar-linux-arm64": "1.42.1"
98
98
  }
99
99
  }
package/src/index.ts CHANGED
@@ -92,7 +92,7 @@ async function main(): Promise<void> {
92
92
  // Default fallthrough = the existing stdin-driven render flow.
93
93
  const subcommand = process.argv[2];
94
94
  if (subcommand === "install") {
95
- runInstall(process.argv.slice(3));
95
+ await runInstall(process.argv.slice(3));
96
96
  process.exit(0);
97
97
  }
98
98
  if (subcommand === "install-url-handler") {
@@ -0,0 +1,197 @@
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
+ }
@@ -9,6 +9,7 @@ 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
11
  import { PACKAGE_VERSION } from "../version";
12
+ import { assessCurrency, currencyReport, fetchLatestVersion } from "./currency";
12
13
 
13
14
  const PACKAGE_NAME = "@promptctl/cc-candybar";
14
15
  const BUNDLE_ID = "com.cccandybar.url-handler";
@@ -451,7 +452,7 @@ function installSuccessMessage(): string {
451
452
  );
452
453
  }
453
454
 
454
- export function runInstall(rendererArgs: string[]): void {
455
+ export async function runInstall(rendererArgs: string[]): Promise<void> {
455
456
  const force = rendererArgs.includes("--force");
456
457
  const filteredArgs = rendererArgs.filter((a) => a !== "--force");
457
458
 
@@ -471,6 +472,21 @@ export function runInstall(rendererArgs: string[]): void {
471
472
  updateClaudeSettings(staged.binPath, argsToInstall, force);
472
473
 
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);
474
490
  }
475
491
 
476
492
  function updateClaudeSettings(