projectstore-claude 0.29.2 → 0.29.3

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.
Files changed (57) hide show
  1. package/README.md +1 -1
  2. package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
  3. package/node_modules/projectstore/.claude-plugin/plugin.json +2 -1
  4. package/node_modules/projectstore/README.md +27 -7
  5. package/node_modules/projectstore/commands/bar.md +30 -0
  6. package/node_modules/projectstore/commands/bind.md +14 -7
  7. package/node_modules/projectstore/commands/scaffold.md +24 -16
  8. package/node_modules/projectstore/commands/status.md +1 -1
  9. package/node_modules/projectstore/commands/statusline.md +1 -1
  10. package/node_modules/projectstore/docs/alongside.md +167 -0
  11. package/node_modules/projectstore/docs/extending.md +17 -3
  12. package/node_modules/projectstore/docs/getting-started.md +18 -9
  13. package/node_modules/projectstore/docs/harnesses.md +1 -1
  14. package/node_modules/projectstore/docs/how-it-works.md +7 -0
  15. package/node_modules/projectstore/harnesses/claude-code.json +18 -3
  16. package/node_modules/projectstore/harnesses/codex.json +12 -1
  17. package/node_modules/projectstore/hooks/band-state.d.ts +44 -0
  18. package/node_modules/projectstore/hooks/band.tsx +594 -0
  19. package/node_modules/projectstore/hooks/hooks.json +1 -0
  20. package/node_modules/projectstore/hooks/session-start.mjs +6 -2
  21. package/node_modules/projectstore/package.json +2 -1
  22. package/node_modules/projectstore/scaffold/headings.json +8 -0
  23. package/node_modules/projectstore/scripts/band-shared.mjs +329 -0
  24. package/node_modules/projectstore/scripts/band.mjs +156 -0
  25. package/node_modules/projectstore/scripts/binding.mjs +157 -17
  26. package/node_modules/projectstore/scripts/build-adapters.mjs +26 -7
  27. package/node_modules/projectstore/scripts/cli.mjs +545 -179
  28. package/node_modules/projectstore/scripts/convergence.mjs +468 -0
  29. package/node_modules/projectstore/scripts/doctor-report.mjs +185 -0
  30. package/node_modules/projectstore/scripts/doctor.mjs +80 -62
  31. package/node_modules/projectstore/scripts/fetch-shell.mjs +283 -0
  32. package/node_modules/projectstore/scripts/harness.mjs +178 -7
  33. package/node_modules/projectstore/scripts/install-harness.mjs +1488 -257
  34. package/node_modules/projectstore/scripts/kanban.mjs +4 -20
  35. package/node_modules/projectstore/scripts/lib.mjs +257 -58
  36. package/node_modules/projectstore/scripts/portable-registration.mjs +62 -15
  37. package/node_modules/projectstore/scripts/query.mjs +83 -12
  38. package/node_modules/projectstore/scripts/run-host.mjs +94 -0
  39. package/node_modules/projectstore/scripts/scaffold.mjs +183 -0
  40. package/node_modules/projectstore/scripts/stages.mjs +80 -0
  41. package/node_modules/projectstore/scripts/statusline.mjs +25 -2
  42. package/node_modules/projectstore/scripts/surfaces.mjs +78 -13
  43. package/node_modules/projectstore/scripts/term.mjs +729 -39
  44. package/node_modules/projectstore/scripts/version-guard.mjs +5 -0
  45. package/node_modules/projectstore/templates/de/strings.json +63 -1
  46. package/node_modules/projectstore/templates/de/vault-readme.md.tmpl +13 -0
  47. package/node_modules/projectstore/templates/en/strings.json +63 -1
  48. package/node_modules/projectstore/templates/en/vault-readme.md.tmpl +13 -0
  49. package/node_modules/projectstore/templates/es/strings.json +63 -1
  50. package/node_modules/projectstore/templates/es/vault-readme.md.tmpl +13 -0
  51. package/node_modules/projectstore/templates/fr/strings.json +63 -1
  52. package/node_modules/projectstore/templates/fr/vault-readme.md.tmpl +13 -0
  53. package/node_modules/projectstore/templates/ru/strings.json +63 -1
  54. package/node_modules/projectstore/templates/ru/vault-readme.md.tmpl +13 -0
  55. package/node_modules/projectstore/templates/zh/strings.json +63 -1
  56. package/node_modules/projectstore/templates/zh/vault-readme.md.tmpl +13 -0
  57. package/package.json +2 -2
@@ -44,6 +44,7 @@ import {
44
44
  claudeHome,
45
45
  whichOnPath,
46
46
  installedPluginEntries,
47
+ projectCopiesByKey,
47
48
  pluginEnabled,
48
49
  treeDigest,
49
50
  packageDigest,
@@ -52,6 +53,7 @@ import {
52
53
  RUNTIME_GITIGNORE_HEADER,
53
54
  LAUNCHER_HEADER,
54
55
  commandForm,
56
+ registrationProvenance,
55
57
  } from "./lib.mjs";
56
58
  import { analysePortableRegistration, portableRegistrationPaths, portablePayloadRoot } from "./portable-registration.mjs";
57
59
 
@@ -89,7 +91,12 @@ export function isOurFile(text) {
89
91
 
90
92
  // ─── markdown-block: the ADR-002 block ─────────────────────────────────
91
93
 
92
- export function analyseBlock(projectDir, s, { root = pluginRoot(), manifestDir = MANIFEST_DIR } = {}) {
94
+ // `layout` names the roster the run leaves (a layout name; the binding's when
95
+ // null). `readers` is a run's harnesses as [{ display, files }], passed when a
96
+ // run names more than one: the migration reason then names the one whose
97
+ // files lack the block's file, rather than the harness whose surface `s` is
98
+ // (the story "One run plans the agents block once…", rules 1 and 3).
99
+ export function analyseBlock(projectDir, s, { root = pluginRoot(), manifestDir = MANIFEST_DIR, layout = null, readers = null } = {}) {
93
100
  const files = s.files || ["AGENTS.md", "CLAUDE.md"];
94
101
  const PREFERRED = files[0], FALLBACK = files[files.length - 1];
95
102
  // LOOK across every file any manifest names; WRITE to one this harness can
@@ -120,8 +127,9 @@ export function analyseBlock(projectDir, s, { root = pluginRoot(), manifestDir =
120
127
  a.version = agentsBlockVersion(tmpl.text);
121
128
  const cfg = readConfigAt(projectDir);
122
129
  let roster = null;
123
- if (cfg && typeof cfg.layout === "string") {
124
- try { roster = loadLayout(cfg.layout, root).agents || null; } catch { roster = null; }
130
+ const name = layout ?? cfg?.layout;
131
+ if (typeof name === "string") {
132
+ try { roster = loadLayout(name, root).agents || null; } catch { roster = null; }
125
133
  }
126
134
  a.desired = renderAgentsBlock(tmpl.text, roster);
127
135
  a.current = withBlock.find((e) => e.file === preferred.file) || withBlock[0] || null;
@@ -134,7 +142,10 @@ export function analyseBlock(projectDir, s, { root = pluginRoot(), manifestDir =
134
142
  // never acquires an AGENTS.md it did not ask for, while installing Codex into
135
143
  // that same project moves the substance to the file Codex can read and leaves
136
144
  // CLAUDE.md importing it.
137
- if (!a.current.own) return { ...a, state: "ours-stale", reason: `in ${a.current.file}, which ${s.harness_display || "this harness"} does not read; install moves it to ${preferred.file}` };
145
+ if (!a.current.own) {
146
+ const reader = readers?.find((r) => !(r.files || []).includes(a.current.file))?.display;
147
+ return { ...a, state: "ours-stale", reason: `in ${a.current.file}, which ${reader || s.harness_display || "this harness"} does not read; install moves it to ${preferred.file}` };
148
+ }
138
149
  if (a.current.file !== preferred.file && preferred.present) return { ...a, state: "ours-stale", reason: `in ${a.current.file}; install migrates it to ${preferred.file}` };
139
150
  if (a.duplicates.length) return { ...a, state: "ours-stale", reason: `also in ${a.duplicates.map((e) => e.file).join(", ")}; install keeps the one in ${a.current.file}` };
140
151
  if (a.current.block.v === a.version && a.current.block.block === a.desired) return { ...a, state: "ours-current" };
@@ -250,8 +261,8 @@ export function analyseRegistration(projectDir, s, { root = pluginRoot(), home =
250
261
  // Machine-global facts: our directory, the host's marketplace registry, every row of ours.
251
262
  if (a.dir.present) {
252
263
  a.dir.manifest = readJson(paths.manifest);
253
- const prov = a.dir.manifest && a.dir.manifest[s.provenance_key];
254
- if (prov && typeof prov === "object" && typeof prov.pkg === "string") {
264
+ const prov = registrationProvenance(a.dir.manifest, s);
265
+ if (prov) {
255
266
  a.dir.prov = prov; a.dir.pkg = prov.pkg;
256
267
  a.dir.disabled = Array.isArray(prov.disabled) ? prov.disabled.filter((x) => typeof x === "string") : [];
257
268
  if (typeof prov.project === "string" && !samePath(prov.project, projectDir)) a.writtenBy = prov.project;
@@ -278,12 +289,13 @@ export function analyseRegistration(projectDir, s, { root = pluginRoot(), home =
278
289
  a.installPath = row ? row.path : null;
279
290
  a.installedVersion = row ? row.version : null;
280
291
  a.enabled = pluginEnabled(id, home, projectDir);
281
- // Competing registrations of the same plugin, enabled for this project.
282
- const seen = new Set();
283
- for (const e of installedPluginEntries(home, projectDir)) {
284
- if (e.key === id || seen.has(e.key) || !e.present || e.enabled === false) continue;
285
- if (e.projectPath && !samePath(e.projectPath, projectDir)) continue;
286
- seen.add(e.key); a.others.push({ key: e.key, path: e.path, version: e.version });
292
+ // Competing registrations of the same plugin, enabled for this project:
293
+ // contract 12's selection, one row per key — the row the host loads here,
294
+ // this checkout's own before a user-scope one. The convergence story's rule
295
+ // 6 reads the same rows (lib's projectCopiesByKey), so the deferral of a
296
+ // newer copy and the stop never disagree about its version.
297
+ for (const e of projectCopiesByKey(home, projectDir)) {
298
+ if (e.key !== id) a.others.push({ key: e.key, path: e.path, version: e.version });
287
299
  }
288
300
  a.newer = Boolean(a.dir.pkg) && cmpPrecedence(a.dir.pkg, pkg) > 0;
289
301
  // Same version, different payload: the directory's recorded digest against
@@ -318,6 +330,15 @@ export function analyseRegistration(projectDir, s, { root = pluginRoot(), home =
318
330
  return { ...a, state: "current", reason: a.newer ? `the directory is at ${a.dir.pkg}, newer than this package (${pkg})${a.writtenBy ? `, written from ${a.writtenBy}` : ""} — not downgraded` : null };
319
331
  }
320
332
 
333
+ // The root a registration renders the project's dependent surfaces against,
334
+ // read-only (contract 4′, two phases): the host's install path of this
335
+ // checkout's row, when that copy is on disk and enabled; else null. One rule
336
+ // for plan() (a run that leaves the registration out) and surfaceStates
337
+ // (doctor), so the two cannot disagree about which root a launcher is for.
338
+ export function registrationRenderRoot(a) {
339
+ return a && a.installed && (a.installed.present ?? true) && a.enabled ? a.installPath : null;
340
+ }
341
+
321
342
  // ─── the project-level layout: legacy / new / both (the layout ADR) ─────
322
343
  //
323
344
  // Which of the two layouts a project holds, file by file — what the `layout`
@@ -357,6 +378,35 @@ export function analyseLayout(projectDir, { harness = null } = {}) {
357
378
  };
358
379
  }
359
380
 
381
+ // ─── a harness's state directory: the welcome marker (contract 13) ──────
382
+ //
383
+ // What a project uninstall may take from .projectstore/state/<id>/: the
384
+ // welcome marker, and the directory once it is empty. Without that the
385
+ // directory outlives the uninstall and the next bare upgrade selects the
386
+ // removed harness again (layout spec contract 5). The marker carries no
387
+ // provenance line, so it is recognised three ways at once — the third
388
+ // recogniser contract 13 accepts beside the provenance line and rung 1″: its
389
+ // path, layoutPaths(p).welcomed(id); the state directory's .gitignore carrying
390
+ // RUNTIME_GITIGNORE_HEADER; and its content, one ISO timestamp line, exactly
391
+ // as the SessionStart hook writes it. Anything else in the directory — a
392
+ // launcher, a file of the user's — is named in `others` and never ours to
393
+ // take here: a launcher is its surface item's. Read-only, like everything in
394
+ // this module.
395
+ const WELCOME_STAMP = /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z\n?$/;
396
+ export function analyseHarnessState(projectDir, id) {
397
+ const p = layoutPaths(projectDir);
398
+ const dir = p.harnessState(id);
399
+ let names;
400
+ try { names = readdirSync(dir).sort(); } catch { return { dir, present: false, names: [], headed: false, marker: null, others: [] }; }
401
+ let headed = false;
402
+ try { headed = readFileSync(p.stateGitignore, "utf8").includes(RUNTIME_GITIGNORE_HEADER); } catch {}
403
+ const markerPath = p.welcomed(id);
404
+ const markerName = markerPath.slice(dir.length + 1);
405
+ const t = names.includes(markerName) ? readText(markerPath) : { present: false, text: null };
406
+ const marker = headed && t.present && typeof t.text === "string" && WELCOME_STAMP.test(t.text) ? markerPath : null;
407
+ return { dir, present: true, names, headed, marker, others: names.filter((n) => !(marker && n === markerName)) };
408
+ }
409
+
360
410
  // ─── every surface, for doctor ─────────────────────────────────────────
361
411
 
362
412
  export { analysePortableRegistration, portableRegistrationPaths };
@@ -378,9 +428,21 @@ export function surfaceStates(projectDir, { home = homedir(), root = pluginRoot(
378
428
  // that merely contained the harness's directory.
379
429
  const rows = Object.entries(m.surfaces || {}).filter(([k, s]) => !k.startsWith("_") && s.kind !== "host" && s.supported !== false && ANALYSERS[s.format]
380
430
  && !(s.condition === "distribution_root" && !portablePayloadRoot(s, { root, env })));
431
+ // The render root, as plan() takes it (contract 0 as amended 2026-10-10):
432
+ // an enabled, produced registration's install path. The launcher and the
433
+ // status-line entry are analysed against it — from the marketplace
434
+ // directory the host loads an npm registration from, or from a package
435
+ // root, the running root alone read the launcher as "not produced here".
436
+ // Read once, before the rows (the registration sorts last in the
437
+ // manifest); its own row reuses the result, since the analysis digests
438
+ // the payload.
439
+ const reg = rows.find(([, s]) => s.kind === "registration");
440
+ const regA = reg ? ANALYSERS[reg[1].format](projectDir, reg[1], { root, home, harness: m, env }) : null;
441
+ const renderRoot = (regA && regA.produced !== false && registrationRenderRoot(regA)) || root;
381
442
  const states = [];
382
443
  for (const [key, s] of rows) {
383
- const a = ANALYSERS[s.format](projectDir, s, { root, home, harness: m, env });
444
+ const dependent = renderRoot !== root && ["json-entry", "mjs"].includes(s.format);
445
+ const a = reg && s === reg[1] ? regA : ANALYSERS[s.format](projectDir, s, { root, home, harness: m, env, ...(dependent ? { renderRoot } : {}) });
384
446
  const entry = s.kind === "shared" ? (a.entryKey || s.marker?.pointer || null) : null;
385
447
  const path = a.legacyPath || a.path || (a.current ? a.current.path : (a.preferred ? a.preferred.path : join(projectDir, s.file || "")));
386
448
  const row = { harness: m.id, surface: key, kind: s.kind, path, entry, state: a.state, reason: a.reason || a.refusal || null, writtenBy: a.writtenBy || null, sameProject: Boolean(a.sameProject), produced: a.produced !== false, legacy: Boolean(a.legacy), installedPkg: a.installedPkg || null, present: a.file ? a.file.present : (a.current ? true : (a.curEntry ? true : false)) };
@@ -392,6 +454,9 @@ export function surfaceStates(projectDir, { home = homedir(), root = pluginRoot(
392
454
  dirPkg: a.dir?.pkg || a.ownership?.version || null,
393
455
  installedVersion: a.installedVersion,
394
456
  installPath: a.installPath,
457
+ // The plugin's root inside our directory (registrationPaths' payload):
458
+ // what a host that loads the registration in place runs from.
459
+ payload: a.paths?.payload || null,
395
460
  enabled: a.enabled,
396
461
  others: a.others || [],
397
462
  otherProjects: a.otherProjects || 0,