projectstore-claude 0.30.0 → 0.30.2
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/bin/projectstore-claude.mjs +1 -1
- package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
- package/node_modules/projectstore/.claude-plugin/plugin.json +1 -1
- package/node_modules/projectstore/README.md +92 -0
- package/node_modules/projectstore/commands/bind.md +5 -5
- package/node_modules/projectstore/commands/statusline.md +8 -8
- package/node_modules/projectstore/docs/getting-started.md +33 -11
- package/node_modules/projectstore/docs/harnesses.md +6 -0
- package/node_modules/projectstore/harnesses/claude-code.json +76 -4
- package/node_modules/projectstore/harnesses/codex.json +32 -1
- package/node_modules/projectstore/package.json +1 -1
- package/node_modules/projectstore/scripts/agents-change.mjs +84 -0
- package/node_modules/projectstore/scripts/binding.mjs +40 -0
- package/node_modules/projectstore/scripts/cli.mjs +86 -85
- package/node_modules/projectstore/scripts/configure-menu.mjs +589 -0
- package/node_modules/projectstore/scripts/configure.mjs +919 -0
- package/node_modules/projectstore/scripts/harness.mjs +21 -3
- package/node_modules/projectstore/scripts/install-harness.mjs +29 -11
- package/node_modules/projectstore/scripts/lib.mjs +41 -5
- package/node_modules/projectstore/scripts/menu.mjs +742 -0
- package/node_modules/projectstore/scripts/models.mjs +505 -0
- package/node_modules/projectstore/scripts/term.mjs +167 -0
- package/package.json +2 -2
|
@@ -302,9 +302,7 @@ export function hostPresence(manifest, { env = process.env, home = homedir() } =
|
|
|
302
302
|
x.cli.bin,
|
|
303
303
|
)?.[1] || null;
|
|
304
304
|
const bin = s ? s.cli.bin : null;
|
|
305
|
-
const
|
|
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);
|
|
305
|
+
const homeDir = harnessHome(manifest, { env, home });
|
|
308
306
|
let present = false;
|
|
309
307
|
if (homeDir) {
|
|
310
308
|
try {
|
|
@@ -314,6 +312,15 @@ export function hostPresence(manifest, { env = process.env, home = homedir() } =
|
|
|
314
312
|
return { cli: bin ? whichOnPath(bin, env || {}) : null, home: present ? homeDir : null, bin, homeDir };
|
|
315
313
|
}
|
|
316
314
|
|
|
315
|
+
// A harness's own home directory, as the harness resolves it: its home
|
|
316
|
+
// variable (runtime.home_env) when set, else home_default under the home
|
|
317
|
+
// directory, else null. Whether it exists is the caller's question.
|
|
318
|
+
export function harnessHome(manifest, { env = process.env, home = homedir() } = {}) {
|
|
319
|
+
const r = manifest?.runtime || {};
|
|
320
|
+
const fromEnv = r.home_env && env ? env[r.home_env] : null;
|
|
321
|
+
return fromEnv || (home && r.home_default ? join(home, r.home_default) : null);
|
|
322
|
+
}
|
|
323
|
+
|
|
317
324
|
// The agents block of a RUN (the story "One run plans the agents block once,
|
|
318
325
|
// and a bare uninstall selects every harness the project uses", rules 1–2):
|
|
319
326
|
// the files every manifest of the run names for it, in the FIRST manifest's
|
|
@@ -703,6 +710,9 @@ export function cachePaths({ env = process.env, home = homedir() } = {}) {
|
|
|
703
710
|
fetch: root ? join(root, "fetch") : null,
|
|
704
711
|
fetchRun: (id) => (root ? join(root, "fetch", id) : null),
|
|
705
712
|
registry: root ? join(root, "registry.json") : null,
|
|
713
|
+
// A harness's model list, as its last live read answered (the ADR "Agent
|
|
714
|
+
// model lists are read from each harness's own catalog…", decision 4).
|
|
715
|
+
models: (id) => (root && id ? join(root, "models", `${id}.json`) : null),
|
|
706
716
|
from: fromEnv ? variable : "the home directory",
|
|
707
717
|
variable,
|
|
708
718
|
};
|
|
@@ -779,6 +789,14 @@ export function hasBand(manifest) {
|
|
|
779
789
|
return manifest?.surfaces?.band?.supported === true;
|
|
780
790
|
}
|
|
781
791
|
|
|
792
|
+
// Whether a harness has the status-line surface: the one predicate for "a
|
|
793
|
+
// harness without a status line", read by configure (its spec, contract 2).
|
|
794
|
+
// Install's own test is the same: a surface declared and not unsupported.
|
|
795
|
+
export function hasStatusLine(manifest) {
|
|
796
|
+
const s = manifest?.surfaces?.statusline;
|
|
797
|
+
return Boolean(s) && s.supported !== false;
|
|
798
|
+
}
|
|
799
|
+
|
|
782
800
|
// The session id the host hands a tool process, by the manifest's name for
|
|
783
801
|
// it (runtime.session_id_env), or null where the harness names none or the
|
|
784
802
|
// process got none.
|
|
@@ -1678,15 +1678,21 @@ function planKeepRecord(ctx) {
|
|
|
1678
1678
|
});
|
|
1679
1679
|
}
|
|
1680
1680
|
|
|
1681
|
-
// The status line's flag (the setup story, rule 9
|
|
1682
|
-
// `statusline.enabled`,
|
|
1683
|
-
//
|
|
1684
|
-
//
|
|
1685
|
-
//
|
|
1681
|
+
// The status line's flag (the setup story, rule 9; the configure spec,
|
|
1682
|
+
// contract 6): `statusline.enabled`, from setup's answer or from a run that
|
|
1683
|
+
// names the surface — install records on, uninstall off. This item is the
|
|
1684
|
+
// flag's one writer; every other key of a `statusline` object is kept.
|
|
1685
|
+
// Planned for the run's harness that has the status-line surface, when the
|
|
1686
|
+
// binding the run leaves does not already say the same; only when there is a
|
|
1687
|
+
// binding to record it in.
|
|
1686
1688
|
const STATUSLINE_FLAG = "statusline.enabled";
|
|
1687
|
-
function planStatuslineFlag(ctx, value) {
|
|
1689
|
+
function planStatuslineFlag(ctx, value, mode = "install") {
|
|
1688
1690
|
const b = ctx.binding;
|
|
1689
1691
|
if (!b || !b.bound || typeof value !== "boolean" || b.statusline === value) return null;
|
|
1692
|
+
const off =
|
|
1693
|
+
mode === "uninstall"
|
|
1694
|
+
? "the status line is removed in this run, and the SessionStart hook keeps it off"
|
|
1695
|
+
: "the status line stays off, and setup does not ask again";
|
|
1690
1696
|
return bindingItem({
|
|
1691
1697
|
harness: ctx.harness.id,
|
|
1692
1698
|
surface: "statusline_flag",
|
|
@@ -1697,7 +1703,7 @@ function planStatuslineFlag(ctx, value) {
|
|
|
1697
1703
|
why: {
|
|
1698
1704
|
[STATUSLINE_FLAG]: value
|
|
1699
1705
|
? "the status line is wired in this run, and the SessionStart hook keeps it current"
|
|
1700
|
-
:
|
|
1706
|
+
: off,
|
|
1701
1707
|
},
|
|
1702
1708
|
said: {
|
|
1703
1709
|
unbound: "the project is not bound; the status line's flag is recorded once it is",
|
|
@@ -2342,12 +2348,24 @@ export function plan(
|
|
|
2342
2348
|
const s = loadHarness(id).surfaces?.statusline;
|
|
2343
2349
|
return Boolean(s) && s.supported !== false;
|
|
2344
2350
|
}) || null;
|
|
2345
|
-
|
|
2346
|
-
|
|
2351
|
+
// The flag's value: setup's answer when it gave one; else, when the run
|
|
2352
|
+
// names the status-line surface, what the run does to it — on for install,
|
|
2353
|
+
// off for uninstall. Never on for a line this run neither wires nor finds
|
|
2354
|
+
// ours: a foreign slot, the keep's branch, a root that is never wired.
|
|
2355
|
+
const flagValue = (ctx, keep) => {
|
|
2356
|
+
if (typeof statusline === "boolean") return statusline;
|
|
2357
|
+
if (!(surfaces || []).includes("statusline")) return null;
|
|
2358
|
+
if (mode === "uninstall") return false;
|
|
2359
|
+
if (keep || ctx.slotForeign.has("statusline")) return null;
|
|
2360
|
+
const row = out.items.find((i) => i.harness === ctx.harness.id && i.surface === "statusline");
|
|
2361
|
+
return row && (row.action !== "skip" || row.state === "ours-current") ? true : null;
|
|
2362
|
+
};
|
|
2363
|
+
const recordFor = (ctx, { keep = false } = {}) => {
|
|
2347
2364
|
if (ctx.harness.id === flagHarness) {
|
|
2348
|
-
const flag = planStatuslineFlag(ctx,
|
|
2365
|
+
const flag = planStatuslineFlag(ctx, flagValue(ctx, keep), mode);
|
|
2349
2366
|
if (flag) marks.push(flag);
|
|
2350
2367
|
}
|
|
2368
|
+
if (mode === "uninstall") return;
|
|
2351
2369
|
if (recordKeep && keeping && ctx.harness.id === keeping.harness.id) {
|
|
2352
2370
|
const kept = planKeepRecord(ctx);
|
|
2353
2371
|
if (kept) marks.push(kept);
|
|
@@ -2464,7 +2482,7 @@ export function plan(
|
|
|
2464
2482
|
out.refusals.push(...ctx.refusals);
|
|
2465
2483
|
if (ctx.incomplete) out.incomplete = true;
|
|
2466
2484
|
stateFor(id, harness);
|
|
2467
|
-
recordFor(ctx);
|
|
2485
|
+
recordFor(ctx, { keep: true });
|
|
2468
2486
|
continue;
|
|
2469
2487
|
}
|
|
2470
2488
|
const hostRows = [];
|
|
@@ -90,10 +90,15 @@ export function sharedRoleForm(name) {
|
|
|
90
90
|
return invocation(sourceHarness(), name, { kind: "agents" });
|
|
91
91
|
}
|
|
92
92
|
// What a welcome says about updates, and the product name a message uses.
|
|
93
|
-
export function updateHint({ env = process.env } = {}) {
|
|
93
|
+
export function updateHint({ env = process.env, root = pluginRoot(), home = homedir() } = {}) {
|
|
94
94
|
const m = speakingHarness(env) || {};
|
|
95
95
|
// A harness that declares no hint gets none — never another harness's advice.
|
|
96
|
-
|
|
96
|
+
// A copy loaded from another channel than the hint's own says that
|
|
97
|
+
// channel's way to update (installChannel: string tests, no file read, so
|
|
98
|
+
// the SessionStart budget holds): a git-marketplace copy keeps the host's
|
|
99
|
+
// auto-update toggle, the npm registration and a checkout get the npm form.
|
|
100
|
+
const channel = m.update_hint?.by_channel ? installChannel(root, { home, harness: m }) : null;
|
|
101
|
+
const hint = (channel && m.update_hint.by_channel[channel]) || m.update_hint || { line: "", welcome: [] };
|
|
97
102
|
// `{upgrade}` is the core's upgrade for this harness, pinned to latest so it
|
|
98
103
|
// runs as printed, with no --project: the hint says where to run it (the
|
|
99
104
|
// story "User-facing text says where to run…", rule 2). Any hint that holds
|
|
@@ -1723,8 +1728,9 @@ export function listOf(fm, key) {
|
|
|
1723
1728
|
// the policy survives clones and second machines), Obsidian hides it, and it
|
|
1724
1729
|
// is intentionally NOT inside <vault>/.projectstore/, whose .gitignore ("*")
|
|
1725
1730
|
// would defeat the whole point. Keys: spec_policy ("required"|"optional"),
|
|
1726
|
-
// lifecycle_gates ("on"|"off"), spec_policy_since (ISO-8601, stamped
|
|
1727
|
-
// spec_policy
|
|
1731
|
+
// lifecycle_gates ("on"|"off"), spec_policy_since (ISO-8601, stamped whenever
|
|
1732
|
+
// spec_policy becomes "required" from anything else; kept otherwise — the
|
|
1733
|
+
// configure spec, contract 7).
|
|
1728
1734
|
export function vaultConfigPath(vault) {
|
|
1729
1735
|
return join(vault, LAYOUT.vaultConfig);
|
|
1730
1736
|
}
|
|
@@ -1740,8 +1746,38 @@ export function readVaultConfig(vault) {
|
|
|
1740
1746
|
}
|
|
1741
1747
|
}
|
|
1742
1748
|
|
|
1749
|
+
// The strict read a writer needs: a file that exists and is not a JSON
|
|
1750
|
+
// object is `corrupt`, never an empty config a write would then replace.
|
|
1751
|
+
export function readVaultConfigStrict(vault) {
|
|
1752
|
+
const path = vaultConfigPath(vault);
|
|
1753
|
+
if (!existsSync(path)) return { path, present: false, corrupt: false, cfg: {} };
|
|
1754
|
+
try {
|
|
1755
|
+
const v = JSON.parse(readFileSync(path, "utf8"));
|
|
1756
|
+
if (v && typeof v === "object" && !Array.isArray(v)) return { path, present: true, corrupt: false, cfg: v };
|
|
1757
|
+
} catch {}
|
|
1758
|
+
return { path, present: true, corrupt: true, cfg: null };
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// The config a change leaves, pure. A return to "required" re-stamps
|
|
1762
|
+
// spec_policy_since: a story done before the policy (re)took effect is exempt
|
|
1763
|
+
// (ADR-007 decision 6), and since every story the old stamp exempted closed
|
|
1764
|
+
// before the new one, no exemption is lost.
|
|
1765
|
+
export function nextVaultConfig(before, patch, now = new Date().toISOString()) {
|
|
1766
|
+
const next = { ...before, ...patch };
|
|
1767
|
+
if (patch.spec_policy === "required" && before.spec_policy !== "required") {
|
|
1768
|
+
// Never earlier than the stamp it replaces: one written ahead of this
|
|
1769
|
+
// clock (a hand edit, a machine whose clock ran fast) would otherwise
|
|
1770
|
+
// shrink the exemption the old stamp gave.
|
|
1771
|
+
const prev = before.spec_policy_since;
|
|
1772
|
+
next.spec_policy_since = prev && String(prev) > now ? prev : now;
|
|
1773
|
+
}
|
|
1774
|
+
return next;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// The one writer of the vault config: atomic, and every key it was not asked
|
|
1778
|
+
// to change is kept as it was.
|
|
1743
1779
|
export function writeVaultConfig(vault, cfg) {
|
|
1744
|
-
|
|
1780
|
+
writeFileAtomic(vaultConfigPath(vault), JSON.stringify(cfg, null, 2) + "\n");
|
|
1745
1781
|
}
|
|
1746
1782
|
|
|
1747
1783
|
// Legacy exemption (ADR-007 Decision 6): a story is exempt from spec-first
|