projectstore-claude 0.29.2 → 0.29.4
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 +1 -1
- package/bin/projectstore-claude.mjs +1 -1
- package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
- package/node_modules/projectstore/.claude-plugin/plugin.json +2 -1
- package/node_modules/projectstore/README.md +44 -7
- package/node_modules/projectstore/commands/bar.md +30 -0
- package/node_modules/projectstore/commands/bind.md +16 -20
- package/node_modules/projectstore/commands/doctor.md +12 -7
- package/node_modules/projectstore/commands/scaffold.md +24 -16
- package/node_modules/projectstore/commands/status.md +1 -1
- package/node_modules/projectstore/commands/statusline.md +2 -2
- package/node_modules/projectstore/commands/story.md +15 -2
- package/node_modules/projectstore/docs/alongside.md +167 -0
- package/node_modules/projectstore/docs/extending.md +17 -3
- package/node_modules/projectstore/docs/getting-started.md +18 -9
- package/node_modules/projectstore/docs/harnesses.md +1 -1
- package/node_modules/projectstore/docs/how-it-works.md +12 -3
- package/node_modules/projectstore/harnesses/claude-code.json +23 -8
- package/node_modules/projectstore/harnesses/codex.json +16 -6
- package/node_modules/projectstore/hooks/band-state.d.ts +44 -0
- package/node_modules/projectstore/hooks/band.tsx +622 -0
- package/node_modules/projectstore/hooks/hooks.json +1 -0
- package/node_modules/projectstore/hooks/pre-compact.mjs +7 -10
- package/node_modules/projectstore/hooks/session-rules.mjs +14 -8
- package/node_modules/projectstore/hooks/session-start.mjs +56 -20
- package/node_modules/projectstore/hooks/session-stop.mjs +8 -6
- package/node_modules/projectstore/package.json +12 -2
- package/node_modules/projectstore/scaffold/headings.json +8 -0
- package/node_modules/projectstore/scripts/band-shared.mjs +446 -0
- package/node_modules/projectstore/scripts/band.mjs +207 -0
- package/node_modules/projectstore/scripts/binding.mjs +733 -62
- package/node_modules/projectstore/scripts/build-adapters.mjs +98 -35
- package/node_modules/projectstore/scripts/cli.mjs +1463 -267
- package/node_modules/projectstore/scripts/codemap.mjs +22 -7
- package/node_modules/projectstore/scripts/convergence.mjs +638 -0
- package/node_modules/projectstore/scripts/diff-refs.mjs +14 -8
- package/node_modules/projectstore/scripts/doctor-report.mjs +253 -0
- package/node_modules/projectstore/scripts/doctor.mjs +1384 -420
- package/node_modules/projectstore/scripts/draft.mjs +5 -2
- package/node_modules/projectstore/scripts/fetch-shell.mjs +369 -0
- package/node_modules/projectstore/scripts/graph.mjs +19 -13
- package/node_modules/projectstore/scripts/harness.mjs +283 -37
- package/node_modules/projectstore/scripts/install-harness.mjs +4701 -575
- package/node_modules/projectstore/scripts/kanban.mjs +6 -21
- package/node_modules/projectstore/scripts/lib.mjs +933 -199
- package/node_modules/projectstore/scripts/mcp.mjs +386 -105
- package/node_modules/projectstore/scripts/portable-registration.mjs +276 -56
- package/node_modules/projectstore/scripts/provenance.mjs +26 -10
- package/node_modules/projectstore/scripts/query.mjs +441 -72
- package/node_modules/projectstore/scripts/reconcile.mjs +55 -18
- package/node_modules/projectstore/scripts/run-host.mjs +139 -0
- package/node_modules/projectstore/scripts/scaffold.mjs +244 -0
- package/node_modules/projectstore/scripts/setup.mjs +987 -0
- package/node_modules/projectstore/scripts/stages.mjs +84 -0
- package/node_modules/projectstore/scripts/statusline-launcher.mjs +36 -11
- package/node_modules/projectstore/scripts/statusline.mjs +55 -14
- package/node_modules/projectstore/scripts/story-section.mjs +39 -17
- package/node_modules/projectstore/scripts/surfaces.mjs +485 -99
- package/node_modules/projectstore/scripts/term.mjs +1027 -52
- package/node_modules/projectstore/scripts/tokens.mjs +106 -28
- package/node_modules/projectstore/scripts/touch-session.mjs +109 -70
- package/node_modules/projectstore/scripts/version-guard.mjs +79 -27
- package/node_modules/projectstore/scripts/worktree.mjs +16 -5
- package/node_modules/projectstore/templates/de/strings.json +63 -1
- package/node_modules/projectstore/templates/de/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/en/strings.json +63 -1
- package/node_modules/projectstore/templates/en/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/es/strings.json +63 -1
- package/node_modules/projectstore/templates/es/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/fr/strings.json +63 -1
- package/node_modules/projectstore/templates/fr/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/ru/strings.json +63 -1
- package/node_modules/projectstore/templates/ru/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/zh/strings.json +63 -1
- package/node_modules/projectstore/templates/zh/vault-readme.md.tmpl +13 -0
- package/package.json +2 -2
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
//
|
|
25
25
|
// Pure node, no external deps — same constraint as lib.mjs. Read-only.
|
|
26
26
|
|
|
27
|
-
import { readFileSync, existsSync, readdirSync, realpathSync } from "node:fs";
|
|
28
|
-
import { join, dirname, resolve, sep } from "node:path";
|
|
27
|
+
import { readFileSync, existsSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
28
|
+
import { join, dirname, resolve, sep, relative, isAbsolute } from "node:path";
|
|
29
29
|
import { homedir } from "node:os";
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
31
|
|
|
@@ -51,9 +51,11 @@ let _cache = null;
|
|
|
51
51
|
export function loadHarnesses(dir = MANIFEST_DIR) {
|
|
52
52
|
if (_cache && _cache.dir === dir) return _cache.map;
|
|
53
53
|
const map = new Map();
|
|
54
|
-
let names
|
|
54
|
+
let names;
|
|
55
55
|
try {
|
|
56
|
-
names = readdirSync(dir)
|
|
56
|
+
names = readdirSync(dir)
|
|
57
|
+
.filter((n) => n.endsWith(".json"))
|
|
58
|
+
.sort();
|
|
57
59
|
} catch {
|
|
58
60
|
names = [];
|
|
59
61
|
}
|
|
@@ -153,7 +155,8 @@ export function identifiedHarnessId(env = process.env, dir = MANIFEST_DIR) {
|
|
|
153
155
|
if (forced && loadHarnesses(dir).has(forced)) return forced;
|
|
154
156
|
// A parent of ours decided before it named the project in the harness's own
|
|
155
157
|
// vocabulary (childEnv), so its answer stands — an empty one included.
|
|
156
|
-
if (env[IDENTIFIED_ENV] !== undefined)
|
|
158
|
+
if (env[IDENTIFIED_ENV] !== undefined)
|
|
159
|
+
return loadHarnesses(dir).has(env[IDENTIFIED_ENV]) ? env[IDENTIFIED_ENV] : null;
|
|
157
160
|
const best = ranked(env, dir);
|
|
158
161
|
if (best && best.strong) return best.id;
|
|
159
162
|
for (const m of loadHarnesses(dir).values()) {
|
|
@@ -226,23 +229,175 @@ export function resetDetection() {
|
|
|
226
229
|
// process" from branded environment variables, and using it here would
|
|
227
230
|
// conjure .claude/ for a Codex user. Not memoised: it is per directory, and a
|
|
228
231
|
// test builds a project per case.
|
|
229
|
-
|
|
232
|
+
//
|
|
233
|
+
// `state` adds the second signal `upgrade` reads: a harness whose state
|
|
234
|
+
// directory of ours exists (layoutPaths(p).harnessState(id), layout spec
|
|
235
|
+
// contract 5). A Codex session wrote its welcome marker there, so Codex is
|
|
236
|
+
// used here even with no .codex/ (measured in this repository, 2026-10-08).
|
|
237
|
+
// Never a machine-global registration: one exists for every project on the
|
|
238
|
+
// machine and would select its harness everywhere. One entry per manifest,
|
|
239
|
+
// the directory first, in manifest order as before.
|
|
240
|
+
export function detectHarnesses(projectDir, { dir = MANIFEST_DIR, state = false } = {}) {
|
|
230
241
|
const out = [];
|
|
242
|
+
const p = state ? layoutPaths(projectDir, { dir }) : null;
|
|
231
243
|
for (const m of loadHarnesses(dir).values()) {
|
|
232
244
|
const d = m.runtime?.harness_dir;
|
|
233
|
-
if (d && existsSync(join(projectDir, d)))
|
|
245
|
+
if (d && existsSync(join(projectDir, d))) {
|
|
246
|
+
out.push({ id: m.id, why: "directory", evidence: d });
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (!p) continue;
|
|
250
|
+
const at = p.harnessState(m.id);
|
|
251
|
+
let isDir = false;
|
|
252
|
+
try {
|
|
253
|
+
isDir = statSync(at).isDirectory();
|
|
254
|
+
} catch {}
|
|
255
|
+
if (isDir) out.push({ id: m.id, why: "state", evidence: relative(projectDir, at) });
|
|
234
256
|
}
|
|
235
257
|
return out;
|
|
236
258
|
}
|
|
237
259
|
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
260
|
+
// A pure PATH walk for a host binary — a read, not a subprocess: the analysers
|
|
261
|
+
// run on every doctor call. Returns the absolute path or null. It lives here,
|
|
262
|
+
// beside hostPresence, which needs it; lib.mjs re-exports it under the name
|
|
263
|
+
// it already published.
|
|
264
|
+
export function whichOnPath(name, env = process.env) {
|
|
265
|
+
const sep = process.platform === "win32" ? ";" : ":";
|
|
266
|
+
const exts = process.platform === "win32" ? ["", ".cmd", ".exe", ".bat"] : [""];
|
|
267
|
+
for (const dir of String(env.PATH || "")
|
|
268
|
+
.split(sep)
|
|
269
|
+
.filter(Boolean)) {
|
|
270
|
+
for (const ext of exts) {
|
|
271
|
+
const p = join(dir, name + ext);
|
|
272
|
+
try {
|
|
273
|
+
const st = statSync(p);
|
|
274
|
+
if (st.isFile() && (process.platform === "win32" || st.mode & 0o111)) return p;
|
|
275
|
+
} catch {}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Whether a harness's host is on this MACHINE (the story "projectstore setup
|
|
282
|
+
// asks, shows one plan and installs every harness it finds, with the vault",
|
|
283
|
+
// rule 5): its registration surface's CLI found on PATH, and its home
|
|
284
|
+
// directory present — the manifest's home variable when set, else
|
|
285
|
+
// <home>/<home_default>. Not detectHarnesses, which answers for a PROJECT, and
|
|
286
|
+
// not detectHarnessId, which names the harness running this process. Setup's
|
|
287
|
+
// pre-tick is the one place machine presence counts; install keeps contract 8.
|
|
288
|
+
// Every name comes from the manifest. Read-only: a stat per PATH entry and one
|
|
289
|
+
// for the home.
|
|
290
|
+
// → { cli, home, bin, homeDir }: `cli` is the absolute path found, or null;
|
|
291
|
+
// `home` the directory when it exists, or null. `bin` and `homeDir` are
|
|
292
|
+
// what was looked for (null when the manifest names none), so a reason can
|
|
293
|
+
// name them when either is missing.
|
|
294
|
+
export function hostPresence(manifest, { env = process.env, home = homedir() } = {}) {
|
|
295
|
+
const s =
|
|
296
|
+
Object.entries(manifest?.surfaces || {}).find(
|
|
297
|
+
([k, x]) =>
|
|
298
|
+
!k.startsWith("_") &&
|
|
299
|
+
x?.kind === "registration" &&
|
|
300
|
+
x.supported !== false &&
|
|
301
|
+
typeof x.cli?.bin === "string" &&
|
|
302
|
+
x.cli.bin,
|
|
303
|
+
)?.[1] || null;
|
|
304
|
+
const bin = s ? s.cli.bin : null;
|
|
305
|
+
const r = manifest?.runtime || {};
|
|
306
|
+
const fromEnv = r.home_env && env ? env[r.home_env] : null;
|
|
307
|
+
const homeDir = fromEnv || (home && r.home_default ? join(home, r.home_default) : null);
|
|
308
|
+
let present = false;
|
|
309
|
+
if (homeDir) {
|
|
310
|
+
try {
|
|
311
|
+
present = statSync(homeDir).isDirectory();
|
|
312
|
+
} catch {}
|
|
244
313
|
}
|
|
245
|
-
return
|
|
314
|
+
return { cli: bin ? whichOnPath(bin, env || {}) : null, home: present ? homeDir : null, bin, homeDir };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// The agents block of a RUN (the story "One run plans the agents block once,
|
|
318
|
+
// and a bare uninstall selects every harness the project uses", rules 1–2):
|
|
319
|
+
// the files every manifest of the run names for it, in the FIRST manifest's
|
|
320
|
+
// order. The block is written to the first of them, so it lands where every
|
|
321
|
+
// harness of the run can read it, natively or through the import the run
|
|
322
|
+
// creates. One manifest gives its own list unchanged, which is what keeps a
|
|
323
|
+
// single-harness plan as it was.
|
|
324
|
+
export function commonBlockFiles(manifests) {
|
|
325
|
+
const lists = manifests.map((m) => agentsBlockRow(m)?.[1].files || []);
|
|
326
|
+
if (!lists.length) return [];
|
|
327
|
+
return lists[0].filter((f) => lists.every((l) => l.includes(f)));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// A manifest's agents-block row as [key, surface], or null: found by its
|
|
331
|
+
// format, as the installer's handlers are keyed, never by the key's name.
|
|
332
|
+
export function agentsBlockRow(manifest) {
|
|
333
|
+
return (
|
|
334
|
+
Object.entries(manifest?.surfaces || {}).find(([k, s]) => !k.startsWith("_") && s?.format === "markdown-block") ||
|
|
335
|
+
null
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Rule 2, for the schema test: the manifests' lists share a file, or a run of
|
|
340
|
+
// all of them has nowhere to write the block. Null when they do; otherwise
|
|
341
|
+
// the problem, naming each manifest's list.
|
|
342
|
+
export function blockTargetProblem(manifests) {
|
|
343
|
+
const withBlock = manifests.filter((m) => agentsBlockRow(m));
|
|
344
|
+
if (commonBlockFiles(withBlock).length) return null;
|
|
345
|
+
return `the agents-block files lists share no file, so a run of every harness has no file to write the block to: ${withBlock.map((m) => `${m.id} ${JSON.stringify(agentsBlockRow(m)[1].files || [])}`).join(", ")}`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// The refusal when nothing is detected and nothing is named — built from the
|
|
349
|
+
// manifests, so a new harness appears in it without an edit here. With
|
|
350
|
+
// `state` (upgrade's set) it names both signals the selection read.
|
|
351
|
+
export function harnessRefusal(projectDir, { dir = MANIFEST_DIR, state = false } = {}) {
|
|
352
|
+
const p = state ? layoutPaths(projectDir, { dir }) : null;
|
|
353
|
+
const rows = [...loadHarnesses(dir).values()].map((m) => {
|
|
354
|
+
const by = `detected by its project directory: ${m.runtime?.harness_dir || "?"}`;
|
|
355
|
+
return {
|
|
356
|
+
id: m.id,
|
|
357
|
+
display: m.display_name,
|
|
358
|
+
why: p ? `${by}, or by its state directory: ${relative(projectDir, p.harnessState(m.id))}` : by,
|
|
359
|
+
};
|
|
360
|
+
});
|
|
361
|
+
return [`No harness detected in ${projectDir}, and none named. Name one:`, ...harnessNameLines(rows)].join("\n");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// That refusal's rows, one per harness — `--harness <id>`, then its display
|
|
365
|
+
// name and why — shared with setup's (its rule 12), so the two cannot drift.
|
|
366
|
+
// The ids are padded to one width, so every reason starts in one column.
|
|
367
|
+
export function harnessNameLines(rows) {
|
|
368
|
+
const width = Math.max(0, ...rows.map((r) => r.id.length));
|
|
369
|
+
return rows.map((r) => ` --harness ${r.id.padEnd(width)} (${r.display}; ${r.why})`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Inside a live session of the host, its CLI and the session both rewrite the
|
|
373
|
+
// same settings files on their own schedules; the registration is planned
|
|
374
|
+
// only from a terminal outside one. The host marks its sessions in the
|
|
375
|
+
// environment (manifest runtime.session_env). It lives here, beside
|
|
376
|
+
// detection, so the installer and the shell fetch read one predicate without
|
|
377
|
+
// an import cycle.
|
|
378
|
+
export function insideHostSession(env, harness) {
|
|
379
|
+
// runtime.session_env, not detect_env: a Bash tool inside a session carries
|
|
380
|
+
// the session marker, not the plugin-root variables a hook receives
|
|
381
|
+
// (measured 2026-09-05; the critic's third pass caught the first draft
|
|
382
|
+
// keying on detect_env, which never fired in a session).
|
|
383
|
+
return (harness?.runtime?.session_env || []).some((k) => env && env[k]);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Whether a harness's registration — and the shell fetch that would feed it —
|
|
387
|
+
// defers in this environment (the setup story, rule 15: the front-door ADR's
|
|
388
|
+
// first open question, answered by the maintainer on 2026-10-08). It defers
|
|
389
|
+
// inside the harness's own session (insideHostSession, runtime.session_env),
|
|
390
|
+
// or while one of its runtime.registration_defer_env variables is set. That
|
|
391
|
+
// second list defers the registration and the fetch and nothing else:
|
|
392
|
+
// isInteractive, the layout move and session identity read session_env alone.
|
|
393
|
+
// Codex's exec tool sets CODEX_THREAD_ID in every command (measured
|
|
394
|
+
// 2026-10-05), while its session_env stays empty so the doctor skill's
|
|
395
|
+
// `upgrade --json` still moves the layout there. Keyed on the PLANNED
|
|
396
|
+
// harness's manifest: inside one host's session another host's registration
|
|
397
|
+
// does not defer.
|
|
398
|
+
export function registrationDeferred(env, harness) {
|
|
399
|
+
if (insideHostSession(env, harness)) return true;
|
|
400
|
+
return (harness?.runtime?.registration_defer_env || []).some((k) => env && env[k]);
|
|
246
401
|
}
|
|
247
402
|
|
|
248
403
|
export function activeHarness(env = process.env, dir = MANIFEST_DIR) {
|
|
@@ -280,7 +435,10 @@ export function invocation(manifest, name, { kind = "commands", args = "" } = {}
|
|
|
280
435
|
// read these same patterns.
|
|
281
436
|
export function uiWordPatterns(manifest) {
|
|
282
437
|
const esc = (t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
283
|
-
return (manifest?.ui_vocabulary || []).map((word) => ({
|
|
438
|
+
return (manifest?.ui_vocabulary || []).map((word) => ({
|
|
439
|
+
word,
|
|
440
|
+
re: new RegExp(`(?<![\\w.-])${esc(word)}(?![\\w-]|\\.\\w)`, "g"),
|
|
441
|
+
}));
|
|
284
442
|
}
|
|
285
443
|
|
|
286
444
|
// The harness whose distribution shell bundles this core, by the shell's
|
|
@@ -301,7 +459,9 @@ export function bundlingShellHarnessId({ core = REPO_ROOT, dir = MANIFEST_DIR }
|
|
|
301
459
|
const name = JSON.parse(readFileSync(join(dirname(parent), "package.json"), "utf8")).name;
|
|
302
460
|
id = [...loadHarnesses(dir).values()].find((m) => m.install?.shell && m.install.shell === name)?.id ?? null;
|
|
303
461
|
}
|
|
304
|
-
} catch {
|
|
462
|
+
} catch {
|
|
463
|
+
id = null;
|
|
464
|
+
}
|
|
305
465
|
if (memo) _bundling = id;
|
|
306
466
|
return id;
|
|
307
467
|
}
|
|
@@ -325,10 +485,8 @@ function speaker(env, dir) {
|
|
|
325
485
|
if (identified) return identified;
|
|
326
486
|
const bundled = bundlingShellHarnessId({ dir });
|
|
327
487
|
if (bundled) return bundled;
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if (named) return named.id;
|
|
331
|
-
}
|
|
488
|
+
const named = shellHarnessId(env, dir);
|
|
489
|
+
if (named) return named;
|
|
332
490
|
return sourceHarness(dir)?.id ?? null;
|
|
333
491
|
}
|
|
334
492
|
|
|
@@ -434,7 +592,13 @@ function hostPluginRoot(env) {
|
|
|
434
592
|
// bin's child), is honoured as named. Real paths on both sides: the module's
|
|
435
593
|
// own is already real, so a root under a symlink (macOS's /var and /tmp)
|
|
436
594
|
// would never compare.
|
|
437
|
-
const realOr = (p) => {
|
|
595
|
+
const realOr = (p) => {
|
|
596
|
+
try {
|
|
597
|
+
return realpathSync(p);
|
|
598
|
+
} catch {
|
|
599
|
+
return resolve(p);
|
|
600
|
+
}
|
|
601
|
+
};
|
|
438
602
|
let _realCore = null;
|
|
439
603
|
function containsThisCore(root) {
|
|
440
604
|
const core = (_realCore ??= realOr(REPO_ROOT));
|
|
@@ -461,7 +625,7 @@ export function agentHome(env = process.env, home = homedir()) {
|
|
|
461
625
|
// path is spelled — every reader and writer goes through layoutPaths(), and a
|
|
462
626
|
// test greps the rest of scripts/, hooks/ and bin/ for the literals. The
|
|
463
627
|
// legacy shape (.claude/projectstore.json, .claude/.projectstore/…) is named
|
|
464
|
-
// here too, for the readers' fallback through 0.
|
|
628
|
+
// here too, for the readers' fallback through 0.30 (layout spec, contracts
|
|
465
629
|
// 0, 1 and 7); its harness directory is the source harness's own.
|
|
466
630
|
export const LAYOUT = Object.freeze({
|
|
467
631
|
root: ".projectstore",
|
|
@@ -516,19 +680,75 @@ export function layoutPaths(projectDir, { harnessDir = null, dir = MANIFEST_DIR
|
|
|
516
680
|
};
|
|
517
681
|
}
|
|
518
682
|
|
|
519
|
-
// The
|
|
520
|
-
//
|
|
521
|
-
//
|
|
522
|
-
// the
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
683
|
+
// ─── The user-level cache (layout spec contract 0, the user-level class) ─
|
|
684
|
+
//
|
|
685
|
+
// What is ours on the machine but belongs to no project and no harness home:
|
|
686
|
+
// the shell fetch's scratch (<root>/fetch/<pid>-<ms>/, removed when its run
|
|
687
|
+
// ends) and the registry answer the update notice caches for a day
|
|
688
|
+
// (<root>/registry.json, named now and read by no one yet). The XDG Base
|
|
689
|
+
// Directory spec puts a cache at $XDG_CACHE_HOME when that is an absolute
|
|
690
|
+
// path and ignores a relative one; an empty one is ignored too. Otherwise it
|
|
691
|
+
// is <home>/.cache. With neither, `root` is null and the fetch refuses. This
|
|
692
|
+
// is the one place either name is spelled (contract 0's grep): `from` and
|
|
693
|
+
// `variable` carry them into a refusal's text. env and home are always
|
|
694
|
+
// parameters.
|
|
695
|
+
export function cachePaths({ env = process.env, home = homedir() } = {}) {
|
|
696
|
+
const variable = "XDG_CACHE_HOME";
|
|
697
|
+
const xdg = env ? env[variable] : null;
|
|
698
|
+
const fromEnv = typeof xdg === "string" && xdg !== "" && isAbsolute(xdg);
|
|
699
|
+
const base = fromEnv ? xdg : home ? join(home, ".cache") : null;
|
|
700
|
+
const root = base ? join(base, "projectstore") : null;
|
|
701
|
+
return {
|
|
702
|
+
root,
|
|
703
|
+
fetch: root ? join(root, "fetch") : null,
|
|
704
|
+
fetchRun: (id) => (root ? join(root, "fetch", id) : null),
|
|
705
|
+
registry: root ? join(root, "registry.json") : null,
|
|
706
|
+
from: fromEnv ? variable : "the home directory",
|
|
707
|
+
variable,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// The core through npx: how every command a person is told to type next
|
|
712
|
+
// begins (the story "User-facing text says where to run and names no project
|
|
713
|
+
// path and no shell", rules 2 and 6). `npx` runs it with no bin on PATH.
|
|
714
|
+
export const CORE_NPX = "npx projectstore";
|
|
715
|
+
|
|
716
|
+
// The command a person types from a terminal (the front-door ADR, decision 5;
|
|
717
|
+
// the story above, rule 2): always the core, with one --harness per manifest
|
|
718
|
+
// in the order given — `npx projectstore[@<v>] <verb> --harness a [--harness
|
|
719
|
+
// b] [args]`. It names no shell, whatever the manifest's install.shell says,
|
|
720
|
+
// and its callers pass no --project: where to run it is their prose, `from a
|
|
721
|
+
// terminal in "<dir>"` (lib.mjs fromTerminalIn). The id stays because a bare
|
|
722
|
+
// upgrade or uninstall plans every harness the project uses. An empty array
|
|
723
|
+
// names none, for a verb that takes no --harness (bind, status, scaffold,
|
|
724
|
+
// reconcile). Finding messages, deferred reasons and DONE's tip are built
|
|
725
|
+
// here, so the installer spells no command of its own. The string is built
|
|
726
|
+
// here, not by the converge story's coreCommand, whose module imports this one.
|
|
727
|
+
export function packageCommand(harnesses, verb, { version = null, args = "" } = {}) {
|
|
728
|
+
const list = Array.isArray(harnesses) ? harnesses : [harnesses];
|
|
729
|
+
const ids = list.map((h) => ` --harness ${h?.id || "<id>"}`).join("");
|
|
730
|
+
return `${CORE_NPX}${version ? `@${version}` : ""} ${verb}${ids}${args ? ` ${args}` : ""}`;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// The package this run was invoked as: the shell it came through (the shell's
|
|
734
|
+
// bin sets PROJECTSTORE_SHELL, and fixes the harness), else the core. Only
|
|
735
|
+
// what describes the binary that ran uses it — the help's first line, its
|
|
736
|
+
// Usage lines, the fixed-harness line and the usage errors (the story above,
|
|
737
|
+
// rule 6). A command printed for a person to type next is the core's
|
|
738
|
+
// (packageCommand, CORE_NPX), through a shell too.
|
|
739
|
+
export function invokedAs(env = process.env) {
|
|
740
|
+
const shell = env.PROJECTSTORE_SHELL || null;
|
|
741
|
+
return { shell, cmd: shell ? `npx ${shell}` : CORE_NPX };
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// The harness a shell fixes: the manifest whose install.shell is the name the
|
|
745
|
+
// shell's bin exports (PROJECTSTORE_SHELL), or null. Never speakingHarnessId,
|
|
746
|
+
// which a session's identity overrides: the Codex shell run inside a Claude
|
|
747
|
+
// Code session still installs Codex, so its help's examples name codex.
|
|
748
|
+
export function shellHarnessId(env = process.env, dir = MANIFEST_DIR) {
|
|
749
|
+
const shell = env.PROJECTSTORE_SHELL;
|
|
750
|
+
if (!shell) return null;
|
|
751
|
+
return [...loadHarnesses(dir).values()].find((m) => m.install?.shell === shell)?.id ?? null;
|
|
532
752
|
}
|
|
533
753
|
|
|
534
754
|
// The overlay a harness reads: <project>/.projectstore/harness/<overlay>.json —
|
|
@@ -552,6 +772,22 @@ export function harnessForOverlay(key, dir = MANIFEST_DIR) {
|
|
|
552
772
|
return all.find((m) => m.runtime?.overlay === key) || all.find((m) => m.id === key) || null;
|
|
553
773
|
}
|
|
554
774
|
|
|
775
|
+
// Whether a harness draws the band above the prompt (the band spec, contract
|
|
776
|
+
// 1): the one predicate for "a harness without a band", read by the generator
|
|
777
|
+
// (which keeps the module out of such a harness's tree) and by `band status`.
|
|
778
|
+
export function hasBand(manifest) {
|
|
779
|
+
return manifest?.surfaces?.band?.supported === true;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// The session id the host hands a tool process, by the manifest's name for
|
|
783
|
+
// it (runtime.session_id_env), or null where the harness names none or the
|
|
784
|
+
// process got none.
|
|
785
|
+
export function hostSessionId(env = process.env, harness = activeHarness(env)) {
|
|
786
|
+
const k = harness?.runtime?.session_id_env;
|
|
787
|
+
const v = k && env ? env[k] : null;
|
|
788
|
+
return typeof v === "string" && v ? v : null;
|
|
789
|
+
}
|
|
790
|
+
|
|
555
791
|
// A reader's fallback: the new path when it exists, else the legacy one when
|
|
556
792
|
// THAT exists, else the new path (a writer's target) — at most two existsSync
|
|
557
793
|
// calls, never a directory scan (contract 1; the SessionStart budget).
|
|
@@ -681,7 +917,11 @@ export function lintPatterns(harness, dir = MANIFEST_DIR) {
|
|
|
681
917
|
// shared_env to the derivation below would forbid Claude Code's own variable
|
|
682
918
|
// inside Claude Code's own tree.
|
|
683
919
|
const o = harness.runtime || {};
|
|
684
|
-
const own = new Set(
|
|
920
|
+
const own = new Set(
|
|
921
|
+
[o.project_dir_env, o.plugin_root_env, o.home_env, ...(o.detect_env || []), ...(o.shared_env || [])].filter(
|
|
922
|
+
Boolean,
|
|
923
|
+
),
|
|
924
|
+
);
|
|
685
925
|
for (const m of loadHarnesses(dir).values()) {
|
|
686
926
|
if (m.id === harness.id) continue;
|
|
687
927
|
for (const t of m.tools?.write_tools || []) out.push({ pattern: `\\b${t}\\b`, class: "token", derived: true });
|
|
@@ -689,7 +929,13 @@ export function lintPatterns(harness, dir = MANIFEST_DIR) {
|
|
|
689
929
|
// shared_env included: a name is no less branded for being set by two
|
|
690
930
|
// harnesses, and it exists in no other field of the manifest that declares
|
|
691
931
|
// it — CLAUDE_PLUGIN_DATA appears nowhere but codex.json's shared_env.
|
|
692
|
-
for (const k of [
|
|
932
|
+
for (const k of [
|
|
933
|
+
r.project_dir_env,
|
|
934
|
+
r.plugin_root_env,
|
|
935
|
+
r.home_env,
|
|
936
|
+
...(r.detect_env || []),
|
|
937
|
+
...(r.shared_env || []),
|
|
938
|
+
]) {
|
|
693
939
|
if (k && !own.has(k)) out.push({ pattern: `\\b${k}\\b`, class: "token", derived: true });
|
|
694
940
|
}
|
|
695
941
|
}
|