@v1nvn/statusline 0.30.8 → 0.30.9

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/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # @v1nvn/statusline
2
2
 
3
3
  The statusline package: the CLI that writes the two settings keys painting
4
- Claude Code's status line and agent panel, and the renderer those keys spawn —
5
- `dist/render.mjs`, synced into the plugin data dir and run with `node`. Pure
4
+ Claude Code's status line and agent panel, plus the resolver those keys spawn
5
+ and the renderer it imports — `dist/resolver.mjs` deployed into the plugin
6
+ data dir, `dist/render.mjs` resolved from the installed plugin. Pure
6
7
  TypeScript, zero bash.
7
8
 
8
9
  User-facing docs: [root README § statusline](../../README.md#statusline).
@@ -22,7 +23,7 @@ offers the picker in chat, and writes the pick.
22
23
  In a terminal outside Claude Code — the wizard, the terminal guide:
23
24
 
24
25
  ```sh
25
- npx -y @v1nvn/statusline@0.30.8 configure
26
+ npx -y @v1nvn/statusline@0.30.9 configure
26
27
  ```
27
28
 
28
29
  Pass one stacks the five theme bars: `j/k` focus · `w` width · enter picks.
@@ -35,31 +36,35 @@ One CLI, both ways: `/lab` inside a session runs these same commands; `npx`
35
36
  runs them in a terminal. Every subcommand takes `--home <dir>` to operate on
36
37
  another home instead of `$HOME`.
37
38
 
38
- | Command | Does |
39
- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
40
- | `configure` | the sole writer — writes both settings keys and syncs the bundled renderer into the data dir. Bare on a TTY: the wizard. With flags: `--theme <name>` (quiet, lean, classic, rich, custom) is the base design, item flags override it, `--layout` overrides its layout; a layout item nothing picks is an error naming it; a foreign key needs `--force` |
41
- | `preview` | render the candidate bar and panel row through the same resolver the keys paint with, writing nothing; `--plain` strips the color escapes so glyphs survive chat |
42
- | `catalog` | the themes block (`*` marks the theme the key names) then one line per item, `*` marking the resolved variant; `--themes` cuts to the block |
43
- | `status` | one row per fact — node, the data-dir renderer, both keys, the theme the key names, backup, captures — plus a verdict; exit 0 healthy, 1 needs action, every action row names its fix |
44
- | `restore` | both keys back to their pre-lab values from `backup.json`, then deletes the lab data — `--dry-run` prints the plan; `--force` splices over a key changed after the takeover |
39
+ | Command | Does |
40
+ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
41
+ | `configure` | the sole writer — writes both settings keys and deploys the bundled resolver into the data dir. Bare on a TTY: the wizard. With flags: `--theme <name>` (quiet, lean, classic, rich, custom) is the base design, item flags override it, `--layout` overrides its layout; a layout item nothing picks is an error naming it; a foreign key needs `--force` |
42
+ | `preview` | render the candidate bar and panel row through the same theme resolution the keys paint with, writing nothing; `--plain` strips the color escapes so glyphs survive chat |
43
+ | `catalog` | the themes block (`*` marks the theme the key names) then one line per item, `*` marking the resolved variant; `--themes` cuts to the block |
44
+ | `status` | one row per fact — node, the renderer the install record resolves, both keys, the theme the key names, backup, captures — plus a verdict; exit 0 healthy, 1 needs action, every action row names its fix |
45
+ | `restore` | both keys back to their pre-lab values from `backup.json`, then deletes the lab data — `--dry-run` prints the plan; `--force` splices over a key changed after the takeover |
45
46
 
46
47
  ```sh
47
- npx -y @v1nvn/statusline@0.30.8 preview --theme rich --plain # chat-safe sketch, nothing written
48
- npx -y @v1nvn/statusline@0.30.8 configure --theme rich # the write — live on the next paint
49
- npx -y @v1nvn/statusline@0.30.8 configure --theme rich --bar percent # one swap on top of the theme
50
- npx -y @v1nvn/statusline@0.30.8 catalog --themes
51
- npx -y @v1nvn/statusline@0.30.8 status
52
- npx -y @v1nvn/statusline@0.30.8 restore
48
+ npx -y @v1nvn/statusline@0.30.9 preview --theme rich --plain # chat-safe sketch, nothing written
49
+ npx -y @v1nvn/statusline@0.30.9 configure --theme rich # the write — live on the next paint
50
+ npx -y @v1nvn/statusline@0.30.9 configure --theme rich --bar percent # one swap on top of the theme
51
+ npx -y @v1nvn/statusline@0.30.9 catalog --themes
52
+ npx -y @v1nvn/statusline@0.30.9 status
53
+ npx -y @v1nvn/statusline@0.30.9 restore
53
54
  ```
54
55
 
55
56
  ## Develop
56
57
 
57
58
  ```sh
58
- yarn workspace @v1nvn/statusline build # vite → dist/ (index.js + render.mjs)
59
+ yarn workspace @v1nvn/statusline build # vite → dist/ (index.js + render.mjs + resolver.mjs)
59
60
  yarn workspace @v1nvn/statusline test # vitest
60
- yarn lint && yarn typecheck # from the repo root
61
+ yarn lint && yarn typecheck # from the repo root
62
+ yarn build # root — every package, then syncs the plugin artifact
61
63
  ```
62
64
 
65
+ `plugins/statusline/render.mjs` is a committed build artifact: the root
66
+ `yarn build` copies `dist/render.mjs` there, and CI drift-gates the copy.
67
+
63
68
  Node ≥ 22. One workspace dep: `@v1nvn/agentic-core` (usage/exit helpers).
64
69
 
65
70
  ## Modules
@@ -78,8 +83,10 @@ Node ≥ 22. One workspace dep: `@v1nvn/agentic-core` (usage/exit helpers).
78
83
  | `src/wizard.ts` · `src/wizard-tui.ts` | the terminal wizard — theme pass, then refinement of the pick's overrides |
79
84
  | `src/payloads.ts` · `src/demo-repo.ts` | preview plumbing — fixtures anchored to the moment, in-process renders; demo git repo |
80
85
  | `src/render/entry.ts` | the `render.mjs` entry the keys spawn — argv to line or panel, both capture tees |
86
+ | `src/render/resolver.ts` | the deployed entry the keys spawn — resolves the install record, imports its render.mjs |
87
+ | `src/render/install-record.ts` | `resolveInstall` — the install record, newest usable entry wins |
81
88
  | `src/render/argv.ts` | the renderer's argv grammar — `node:util` parseArgs, never commander |
82
- | `src/render/theme.ts` | `resolvePaint` — the one theme resolver, shared by paint and display |
89
+ | `src/render/theme.ts` | `resolvePaint` — the one theme resolution, shared by paint and display |
83
90
  | `src/render/engine.ts` | the line engine — compose, `vlen`, the fit ladder |
84
91
  | `src/render/items.ts` | the item registry — 16 items, defaults, rungs, `DEFAULT_LAYOUT` |
85
92
  | `src/render/segments.ts` | the per-item segment renderers |
@@ -93,27 +100,29 @@ Node ≥ 22. One workspace dep: `@v1nvn/agentic-core` (usage/exit helpers).
93
100
 
94
101
  ## Contracts
95
102
 
96
- - `configure` is the sole writer: one run writes both keys and syncs the
97
- bundled `render.mjs` into the data dir — its only other write is
103
+ - `configure` is the sole writer: one run writes both keys and deploys the
104
+ bundled resolver as the data-dir `render.mjs` — its only other write is
98
105
  `backup.json`. `preview`, `catalog`, and `status` write nothing;
99
106
  `captures/` is the renderer's paint-time tee, not a CLI write.
100
107
  - The key value is one direct data-dir command:
101
108
  `node "$HOME/.claude/plugins/data/statusline-agentic/render.mjs" --theme=lean --bar=gauge || true`
102
- — no glob resolver, no plugin-cache coupling; the panel key adds the `panel`
109
+ — the data-dir file is a resolver: it reads Claude Code's install record
110
+ (`installed_plugins.json`) and imports the newest installed plugin's
111
+ `render.mjs` with argv and stdin intact; the panel key adds the `panel`
103
112
  positional. Key spellings are pinned byte-exact by tests.
104
113
  - Decisions ride argv: `--theme` first, then one `--<item>=<alt>` per pick
105
114
  that differs from the theme's own, `--layout` only when passed. Ambient
106
115
  state stays env — `NO_COLOR`, `COLUMNS`, `HOME`.
107
116
  - The renderer resolves the theme at paint: item flags beat it, `--layout`
108
- beats its layout — one resolver, no second path. `catalog` stars through
109
- it, `preview` and the wizard render through it in-process, and `status`
110
- reads the theme name straight from the key.
117
+ beats its layout — one resolution path, no second one. `catalog` stars
118
+ through it, `preview` and the wizard render through it in-process, and
119
+ `status` reads the theme name straight from the key.
111
120
  - The theme name rides the key: `status` and `catalog` read it straight from
112
121
  the key, swaps appended — `theme: lean +bar=gauge` — so a swap keeps the
113
122
  name.
114
- - The renderer bundle is dependency-free (node built-ins only) and parses its
115
- own argv — never commander, never `@v1nvn/agentic-core` — so any node ≥ 18
116
- the host has paints.
123
+ - The render bundles are dependency-free (node built-ins only) and parse
124
+ their own argv — never commander, never `@v1nvn/agentic-core` — so any
125
+ node ≥ 18 the host has paints.
117
126
  - One splice home: the settings.json span primitives live in `configure.ts`;
118
127
  `restore` imports them.
119
128
  - `restore` never runs the renderer — it works uninstalled, deleting
package/dist/index.js CHANGED
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
- import { delimiter, dirname, join, sep } from "node:path";
4
+ import { basename, delimiter, dirname, join, sep } from "node:path";
5
5
  import { execFileSync, spawnSync } from "node:child_process";
6
6
  import { Command } from "commander";
7
7
  import { fileURLToPath } from "node:url";
8
- import { createHash } from "node:crypto";
9
8
  //#region ../core/dist/index.js
10
9
  function parseQuietly(program, args, recover) {
11
10
  try {
@@ -1332,7 +1331,7 @@ function catalog(options) {
1332
1331
  //#region src/cli.ts
1333
1332
  var VERSION = {
1334
1333
  name: "@v1nvn/statusline",
1335
- version: "0.30.8",
1334
+ version: "0.30.9",
1336
1335
  description: "Browse statusline variants and configure the live status line and agent panel — the statusline CLI behind the statusline plugin.",
1337
1336
  type: "module",
1338
1337
  bin: "dist/index.js",
@@ -1467,11 +1466,10 @@ function parseArgs(args) {
1467
1466
  ...typeof options.plain === "boolean" ? { plain: options.plain } : {}
1468
1467
  };
1469
1468
  }
1470
- //#endregion
1471
- //#region src/configure.ts
1472
- var BUNDLED_RENDERER = fileURLToPath(new URL("../dist/render.mjs", import.meta.url));
1473
- function syncRenderer(home) {
1474
- const source = readFileSync(BUNDLED_RENDERER);
1469
+ fileURLToPath(new URL("../dist/render.mjs", import.meta.url));
1470
+ var BUNDLED_RESOLVER = fileURLToPath(new URL("../dist/resolver.mjs", import.meta.url));
1471
+ function syncResolver(home) {
1472
+ const source = readFileSync(BUNDLED_RESOLVER);
1475
1473
  const dest = renderMjsPath(home);
1476
1474
  if (existsSync(dest) && readFileSync(dest).equals(source)) return;
1477
1475
  mkdirSync(dirname(dest), { recursive: true });
@@ -1480,7 +1478,7 @@ function syncRenderer(home) {
1480
1478
  renameSync(tmp, dest);
1481
1479
  }
1482
1480
  var SETTINGS_KEYS = ["statusLine", "subagentStatusLine"];
1483
- function isObject(value) {
1481
+ function isObject$1(value) {
1484
1482
  return typeof value === "object" && value !== null && !Array.isArray(value);
1485
1483
  }
1486
1484
  function readOrNull(file) {
@@ -1498,11 +1496,11 @@ function parseSettings(file, raw) {
1498
1496
  } catch (e) {
1499
1497
  throw new Error(`${file} is not valid JSON`, { cause: e });
1500
1498
  }
1501
- if (!isObject(parsed)) throw new Error(`${file} does not hold a settings object`);
1499
+ if (!isObject$1(parsed)) throw new Error(`${file} does not hold a settings object`);
1502
1500
  return parsed;
1503
1501
  }
1504
1502
  function memberCommand(value) {
1505
- if (!isObject(value) || value.type !== "command") return null;
1503
+ if (!isObject$1(value) || value.type !== "command") return null;
1506
1504
  return typeof value.command === "string" ? value.command : null;
1507
1505
  }
1508
1506
  function isOurMember(key, value) {
@@ -1722,7 +1720,7 @@ function configure(options) {
1722
1720
  }, options.force ?? false);
1723
1721
  if (plan.adds.length > 0 || plan.repoints.length > 0) writeBackupIfAbsent(options.home, plan);
1724
1722
  commitSettings(plan);
1725
- syncRenderer(options.home);
1723
+ syncResolver(options.home);
1726
1724
  }
1727
1725
  //#endregion
1728
1726
  //#region src/demo-repo.ts
@@ -2031,7 +2029,7 @@ var PANEL_STEP_S = 300;
2031
2029
  function anchoredP1(repoDir, now) {
2032
2030
  const payload = JSON.parse(readFileSync(join(PAYLOADS_DIR, "p1.json"), "utf8"));
2033
2031
  const cache = payload.prompt_cache;
2034
- if (isObject(cache)) {
2032
+ if (isObject$1(cache)) {
2035
2033
  if (cache.warm === true) {
2036
2034
  cache.expires_at = now + P1_WARM_IN;
2037
2035
  cache.last_miss_at = now + P1_WARM_IN - 3900;
@@ -2041,9 +2039,9 @@ function anchoredP1(repoDir, now) {
2041
2039
  }
2042
2040
  }
2043
2041
  const limits = payload.rate_limits;
2044
- if (isObject(limits)) for (const [limitKey, offset] of Object.entries(P1_RESETS_IN)) {
2042
+ if (isObject$1(limits)) for (const [limitKey, offset] of Object.entries(P1_RESETS_IN)) {
2045
2043
  const limit = limits[limitKey];
2046
- if (isObject(limit)) limit.resets_at = now + offset;
2044
+ if (isObject$1(limit)) limit.resets_at = now + offset;
2047
2045
  }
2048
2046
  payload.workspace.current_dir = repoDir;
2049
2047
  return `${JSON.stringify(payload, null, 2)}\n`;
@@ -2154,7 +2152,7 @@ function readBackup(home) {
2154
2152
  } catch (e) {
2155
2153
  throw new Error(`${file} is not valid JSON`, { cause: e });
2156
2154
  }
2157
- if (!isObject(parsed) || typeof parsed.createdFile !== "boolean" || !isObject(parsed.keys)) throw new Error(`${file} is not a lab backup`);
2155
+ if (!isObject$1(parsed) || typeof parsed.createdFile !== "boolean" || !isObject$1(parsed.keys)) throw new Error(`${file} is not a lab backup`);
2158
2156
  const keys = {};
2159
2157
  for (const key of SETTINGS_KEYS) {
2160
2158
  const saved = parsed.keys[key];
@@ -2263,6 +2261,60 @@ function restore(options) {
2263
2261
  };
2264
2262
  }
2265
2263
  //#endregion
2264
+ //#region src/render/install-record.ts
2265
+ var RECORD_FILE = join(".claude", "plugins", "installed_plugins.json");
2266
+ var UNREADABLE = "installed_plugins.json unreadable";
2267
+ var NONE_USABLE = "no statusline@agentic install with a render.mjs";
2268
+ function isObject(value) {
2269
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2270
+ }
2271
+ function ourEntries(home) {
2272
+ let raw;
2273
+ try {
2274
+ raw = readFileSync(join(home, RECORD_FILE), "utf8");
2275
+ } catch {
2276
+ return null;
2277
+ }
2278
+ let parsed;
2279
+ try {
2280
+ parsed = JSON.parse(raw);
2281
+ } catch {
2282
+ return null;
2283
+ }
2284
+ const plugins = isObject(parsed) ? parsed.plugins : void 0;
2285
+ const ours = isObject(plugins) ? plugins["statusline@agentic"] : null;
2286
+ return Array.isArray(ours) ? ours : ours === void 0 ? [] : null;
2287
+ }
2288
+ function stamp(value) {
2289
+ const ms = typeof value === "string" ? Date.parse(value) : NaN;
2290
+ return Number.isNaN(ms) ? 0 : ms;
2291
+ }
2292
+ function isNewer(candidate, incumbent) {
2293
+ const updated = stamp(candidate.lastUpdated) - stamp(incumbent.lastUpdated);
2294
+ return updated > 0 || updated === 0 && stamp(candidate.installedAt) > stamp(incumbent.installedAt);
2295
+ }
2296
+ function usable(value) {
2297
+ return isObject(value) && typeof value.installPath === "string" && existsSync(join(value.installPath, "render.mjs"));
2298
+ }
2299
+ function resolveInstall(home) {
2300
+ const entries = ourEntries(home);
2301
+ if (entries === null) return {
2302
+ kind: "unresolved",
2303
+ reason: UNREADABLE
2304
+ };
2305
+ const installed = entries.filter(usable);
2306
+ if (installed.length === 0) return {
2307
+ kind: "unresolved",
2308
+ reason: NONE_USABLE
2309
+ };
2310
+ const best = installed.reduce((incumbent, candidate) => isNewer(candidate, incumbent) ? candidate : incumbent);
2311
+ return {
2312
+ installPath: best.installPath,
2313
+ kind: "resolved",
2314
+ version: typeof best.version === "string" ? best.version : basename(best.installPath)
2315
+ };
2316
+ }
2317
+ //#endregion
2266
2318
  //#region src/status.ts
2267
2319
  function keyState(key, value) {
2268
2320
  if (value === void 0) return { kind: "absent" };
@@ -2319,33 +2371,25 @@ function driftFindings(config) {
2319
2371
  function nodeOnPath(pathVar) {
2320
2372
  return pathVar.split(delimiter).filter((dir) => dir !== "").map((dir) => join(dir, "node")).find(existsSync) ?? null;
2321
2373
  }
2322
- function rendererHash(bytes) {
2323
- return createHash("sha256").update(bytes).digest("hex").slice(0, 12);
2324
- }
2325
2374
  function rendererState(home) {
2326
- if (!existsSync(BUNDLED_RENDERER)) return { kind: "no-bundle" };
2327
- const cli = readFileSync(BUNDLED_RENDERER);
2328
- const dataFile = renderMjsPath(home);
2329
- if (!existsSync(dataFile)) return { kind: "missing" };
2330
- const hash = rendererHash(readFileSync(dataFile));
2331
- return hash === rendererHash(cli) ? {
2332
- hash,
2333
- kind: "current"
2375
+ if (!existsSync(renderMjsPath(home))) return { kind: "missing" };
2376
+ const record = resolveInstall(home);
2377
+ return record.kind === "resolved" ? {
2378
+ kind: "resolves",
2379
+ version: record.version
2334
2380
  } : {
2335
- cli: rendererHash(cli),
2336
- data: hash,
2337
- kind: "stale"
2381
+ kind: "unresolved",
2382
+ reason: record.reason
2338
2383
  };
2339
2384
  }
2340
- function rendererRow(state) {
2341
- if (state.kind === "current") return `renderer: current — ${state.hash}`;
2342
- if (state.kind === "stale") return `renderer: stale — data ${state.data}, this CLI ${state.cli} — fix: rerun configure --force --theme classic`;
2343
- if (state.kind === "no-bundle") return "renderer: this CLI ships no render.mjs — fix: rerun /lab to refresh the CLI";
2344
- return "renderer: missing — fix: rerun configure --force --theme classic";
2385
+ function rendererRow(state, theme) {
2386
+ if (state.kind === "resolves") return `renderer: resolves → ${state.version}`;
2387
+ if (state.kind === "missing") return `renderer: missing — fix: rerun configure --force --theme ${theme}`;
2388
+ return `renderer: unresolved — ${state.reason} — fix: claude plugin install statusline@agentic`;
2345
2389
  }
2346
- function configRow(findings) {
2390
+ function configRow(findings, theme) {
2347
2391
  if (findings.length === 0) return "config: no drift";
2348
- return `config: drift — ${findings.map(findingText).join(", ")} — fix: rerun configure --theme classic`;
2392
+ return `config: drift — ${findings.map(findingText).join(", ")} — fix: rerun configure --theme ${theme}`;
2349
2393
  }
2350
2394
  function backupRow(home) {
2351
2395
  let backup;
@@ -2386,20 +2430,21 @@ function status(options) {
2386
2430
  const main = keyState("statusLine", members.statusLine);
2387
2431
  const subagent = keyState("subagentStatusLine", members.subagentStatusLine);
2388
2432
  const config = readKeyConfig(options.home);
2433
+ const theme = config.theme ?? "classic";
2389
2434
  const panelConfig = parsePanelCommand(memberCommand(members.subagentStatusLine));
2390
2435
  const findings = main.kind === "ours" ? driftFindings(config) : [];
2391
2436
  const node = nodeOnPath(options.path ?? process.env.PATH ?? "");
2392
2437
  const renderer = rendererState(options.home);
2393
2438
  const rows = [
2394
2439
  node === null ? "node: missing — fix: install node ≥ 18 from nodejs.org, then restart Claude Code" : `node: on PATH (${node})`,
2395
- rendererRow(renderer),
2440
+ rendererRow(renderer, theme),
2396
2441
  keyRow("statusLine", main, configDetail(config)),
2397
2442
  keyRow("subagentStatusLine", subagent, panelDetail(panelConfig)),
2398
- ...main.kind === "ours" ? [configRow(findings), ...themeRow(config)] : [],
2443
+ ...main.kind === "ours" ? [configRow(findings, theme), ...themeRow(config)] : [],
2399
2444
  backupRow(options.home),
2400
2445
  capturesRow(options.home)
2401
2446
  ];
2402
- const healthy = node !== null && renderer.kind === "current" && main.kind === "ours" && subagent.kind === "ours" && findings.length === 0;
2447
+ const healthy = node !== null && renderer.kind === "resolves" && main.kind === "ours" && subagent.kind === "ours" && findings.length === 0;
2403
2448
  return {
2404
2449
  healthy,
2405
2450
  rows: [...rows, healthy ? "healthy" : "unhealthy"]