projectstore-claude 0.29.3 → 0.30.0
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 +13 -12
- package/bin/projectstore-claude.mjs +2 -2
- 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 +153 -77
- package/node_modules/projectstore/commands/bind.md +6 -17
- package/node_modules/projectstore/commands/doctor.md +12 -7
- package/node_modules/projectstore/commands/statusline.md +1 -1
- package/node_modules/projectstore/commands/story.md +15 -2
- package/node_modules/projectstore/docs/extending.md +11 -6
- package/node_modules/projectstore/docs/getting-started.md +21 -1
- package/node_modules/projectstore/docs/harnesses.md +13 -10
- package/node_modules/projectstore/docs/how-it-works.md +6 -4
- package/node_modules/projectstore/harnesses/claude-code.json +16 -15
- package/node_modules/projectstore/harnesses/codex.json +12 -13
- package/node_modules/projectstore/hooks/band.tsx +48 -20
- 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 +50 -18
- package/node_modules/projectstore/hooks/session-stop.mjs +8 -6
- package/node_modules/projectstore/package.json +11 -2
- package/node_modules/projectstore/scripts/band-shared.mjs +141 -24
- package/node_modules/projectstore/scripts/band.mjs +72 -21
- package/node_modules/projectstore/scripts/binding.mjs +616 -85
- package/node_modules/projectstore/scripts/build-adapters.mjs +73 -29
- package/node_modules/projectstore/scripts/cli.mjs +1015 -185
- package/node_modules/projectstore/scripts/codemap.mjs +22 -7
- package/node_modules/projectstore/scripts/convergence.mjs +253 -83
- package/node_modules/projectstore/scripts/diff-refs.mjs +14 -8
- package/node_modules/projectstore/scripts/doctor-report.mjs +91 -23
- package/node_modules/projectstore/scripts/doctor.mjs +1317 -371
- package/node_modules/projectstore/scripts/draft.mjs +5 -2
- package/node_modules/projectstore/scripts/fetch-shell.mjs +188 -39
- package/node_modules/projectstore/scripts/graph.mjs +19 -13
- package/node_modules/projectstore/scripts/harness.mjs +123 -48
- package/node_modules/projectstore/scripts/install-harness.mjs +3532 -618
- package/node_modules/projectstore/scripts/kanban.mjs +2 -1
- package/node_modules/projectstore/scripts/lib.mjs +694 -159
- package/node_modules/projectstore/scripts/mcp.mjs +386 -105
- package/node_modules/projectstore/scripts/portable-registration.mjs +224 -51
- package/node_modules/projectstore/scripts/provenance.mjs +26 -10
- package/node_modules/projectstore/scripts/query.mjs +378 -80
- package/node_modules/projectstore/scripts/reconcile.mjs +55 -18
- package/node_modules/projectstore/scripts/run-host.mjs +62 -17
- package/node_modules/projectstore/scripts/scaffold.mjs +89 -28
- package/node_modules/projectstore/scripts/setup.mjs +987 -0
- package/node_modules/projectstore/scripts/stages.mjs +5 -1
- package/node_modules/projectstore/scripts/statusline-launcher.mjs +36 -11
- package/node_modules/projectstore/scripts/statusline.mjs +32 -14
- package/node_modules/projectstore/scripts/story-section.mjs +39 -17
- package/node_modules/projectstore/scripts/surfaces.mjs +416 -95
- package/node_modules/projectstore/scripts/term.mjs +382 -97
- 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 +74 -27
- package/node_modules/projectstore/scripts/worktree.mjs +16 -5
- package/package.json +3 -3
|
@@ -79,19 +79,34 @@ function main() {
|
|
|
79
79
|
lines.push(`| [[${folder.path}/${e.id}/epic\\|${e.id}]] | ${e.title} | ${e.status} | ${refs} |`);
|
|
80
80
|
}
|
|
81
81
|
if (storyRows.length) {
|
|
82
|
-
lines.push(
|
|
83
|
-
"
|
|
82
|
+
lines.push(
|
|
83
|
+
"",
|
|
84
|
+
"## Story-level refs (files each story touched)",
|
|
85
|
+
"",
|
|
86
|
+
"| Epic | Story | code_refs |",
|
|
87
|
+
"|------|-------|-----------|",
|
|
88
|
+
);
|
|
84
89
|
for (const s of storyRows) {
|
|
85
90
|
lines.push(`| ${s.epic} | ${s.title} | ${s.refs.map((r) => `\`${r}\``).join(", ")} |`);
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
93
|
lines.push("");
|
|
89
94
|
|
|
90
|
-
process.stdout.write(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
process.stdout.write(
|
|
96
|
+
JSON.stringify(
|
|
97
|
+
{
|
|
98
|
+
path: join(cfg.vault_path, "code-map.md"),
|
|
99
|
+
content: lines.join("\n"),
|
|
100
|
+
stats: {
|
|
101
|
+
epics: epics.length,
|
|
102
|
+
epics_with_refs: epics.filter((e) => e.refs.length).length,
|
|
103
|
+
story_rows: storyRows.length,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
null,
|
|
107
|
+
2,
|
|
108
|
+
) + "\n",
|
|
109
|
+
);
|
|
95
110
|
}
|
|
96
111
|
|
|
97
112
|
if (isMain(import.meta.url)) {
|
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
// decide({ copies, version, read, skipped, verb, args })
|
|
13
13
|
// → stop or proceed, with the stop's texts
|
|
14
14
|
// proceedCase(decision, v) → the words a newer row adds when not stopped
|
|
15
|
-
// channelChoices({ harness,
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
15
|
+
// channelChoices({ harness, copy, running, gate, pin }) → the ways to settle a
|
|
16
|
+
// project's channel (rule 2's helper: the
|
|
17
|
+
// gate's text, and a newer copy's): setup's
|
|
18
|
+
// pinFor(root, version) → the version a command names: the running
|
|
19
|
+
// one, on a package root
|
|
20
|
+
// newerCopyDeferral({ copy, running, decision, harness, pin })
|
|
19
21
|
// → a newer copy not switched off: { reason, advice }
|
|
20
|
-
// switchConsequence(
|
|
22
|
+
// switchConsequence() → a switch row's consequence line (rule 2)
|
|
21
23
|
// switchReason({ copy, running, runs, decision, unbound })
|
|
22
24
|
// → what a switch row adds under the channel "npm"
|
|
23
25
|
// noCopyToKeep({ harness, surface, off }) → the channel "git" with nothing to keep
|
|
@@ -39,15 +41,23 @@
|
|
|
39
41
|
//
|
|
40
42
|
// Imports lib, harness, portable-registration and surfaces, never the fetch
|
|
41
43
|
// or the host runner: every spawn and resolver is the caller's. Only the
|
|
42
|
-
// installer
|
|
43
|
-
//
|
|
44
|
-
//
|
|
44
|
+
// installer and setup's front-end (setup.mjs, which reads the running copies
|
|
45
|
+
// before its questions) import this file; doctor, lib and the hooks never do,
|
|
46
|
+
// so the SessionStart graph never loads it (tests/convergence.test.mjs
|
|
47
|
+
// asserts both). Pure node, no external deps — the read is Node's own fetch.
|
|
45
48
|
|
|
46
|
-
import { readFileSync, mkdirSync,
|
|
49
|
+
import { readFileSync, mkdirSync, realpathSync } from "node:fs";
|
|
47
50
|
import { dirname, join, resolve, sep } from "node:path";
|
|
48
51
|
import { homedir } from "node:os";
|
|
49
|
-
import {
|
|
50
|
-
|
|
52
|
+
import {
|
|
53
|
+
cmpPrecedence,
|
|
54
|
+
installChannel,
|
|
55
|
+
projectCopiesByKey,
|
|
56
|
+
writeFileAtomic,
|
|
57
|
+
envFlag,
|
|
58
|
+
isEphemeralRoot,
|
|
59
|
+
} from "./lib.mjs";
|
|
60
|
+
import { loadHarness, loadHarnesses } from "./harness.mjs";
|
|
51
61
|
import { analysePortableRegistration, portableRegistrationPaths } from "./portable-registration.mjs";
|
|
52
62
|
import { registrationPaths } from "./surfaces.mjs";
|
|
53
63
|
|
|
@@ -65,6 +75,11 @@ export const ACCEPT = "application/vnd.npm.install-v1+json";
|
|
|
65
75
|
// resolver's own 10 s (REGISTRY_BUDGET_MS, fetch-shell.mjs) before it.
|
|
66
76
|
export const READ_TIMEOUT_MS = 2000;
|
|
67
77
|
export const NOTICE_BUDGET_MS = 2000;
|
|
78
|
+
// What the resolver leaves of a shared bound is no time for a request below
|
|
79
|
+
// this: no registry answers in less, and the resolver's timer and Date.now()
|
|
80
|
+
// round differently, so a resolver that took the whole bound can read as one
|
|
81
|
+
// that left 1 ms of it.
|
|
82
|
+
export const MIN_READ_MS = 50;
|
|
68
83
|
export const FRESH_MS = 24 * 60 * 60 * 1000;
|
|
69
84
|
export const NO_CHECK_ENV = "PROJECTSTORE_NO_UPDATE_CHECK";
|
|
70
85
|
|
|
@@ -72,13 +87,25 @@ export const NO_CHECK_ENV = "PROJECTSTORE_NO_UPDATE_CHECK";
|
|
|
72
87
|
// anything else counts as missing — never printed, never compared.
|
|
73
88
|
const NUM = "(?:0|[1-9]\\d*)";
|
|
74
89
|
const PRE = `(?:${NUM}|\\d*[A-Za-z-][0-9A-Za-z-]*)`;
|
|
75
|
-
const SEMVER = new RegExp(
|
|
90
|
+
const SEMVER = new RegExp(
|
|
91
|
+
`^${NUM}\\.${NUM}\\.${NUM}(?:-${PRE}(?:\\.${PRE})*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$`,
|
|
92
|
+
);
|
|
76
93
|
export const isSemver = (v) => typeof v === "string" && SEMVER.test(v);
|
|
77
94
|
|
|
78
95
|
// ─── Which roots check (rule 6) and show the notice (rule 8) ────────────
|
|
79
96
|
|
|
80
|
-
const real = (p) => {
|
|
81
|
-
|
|
97
|
+
const real = (p) => {
|
|
98
|
+
try {
|
|
99
|
+
return realpathSync(p);
|
|
100
|
+
} catch {
|
|
101
|
+
return resolve(p);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const within = (root, dir) => {
|
|
105
|
+
const r = real(root),
|
|
106
|
+
d = real(dir);
|
|
107
|
+
return r === d || r.startsWith(d + sep);
|
|
108
|
+
};
|
|
82
109
|
|
|
83
110
|
// A copy a host runs, updated through its own channel (layout spec contract
|
|
84
111
|
// 12): never stopped, never told to update. installChannel names the host's
|
|
@@ -130,7 +157,10 @@ export const updateCheckOff = (env) => envFlag(env, NO_CHECK_ENV);
|
|
|
130
157
|
// - a portable registration: the global registration's recorded version,
|
|
131
158
|
// when it is effectively enabled for this project.
|
|
132
159
|
// `analyse(harness, s)` lets the caller share one analysis with the fetch.
|
|
133
|
-
export function runningCopies(
|
|
160
|
+
export function runningCopies(
|
|
161
|
+
projectDir,
|
|
162
|
+
{ harnesses = [], root, home = homedir(), env = process.env, channel = null, version = null, analyse = null } = {},
|
|
163
|
+
) {
|
|
134
164
|
const out = [];
|
|
135
165
|
for (const id of harnesses) {
|
|
136
166
|
const harness = loadHarness(id);
|
|
@@ -140,10 +170,13 @@ export function runningCopies(projectDir, { harnesses = [], root, home = homedir
|
|
|
140
170
|
if (s.format === "host-plugin-registration") {
|
|
141
171
|
if (channel === "git") continue;
|
|
142
172
|
for (const e of projectCopiesByKey(home, projectDir)) {
|
|
143
|
-
if (typeof e.version === "string" && e.version)
|
|
173
|
+
if (typeof e.version === "string" && e.version)
|
|
174
|
+
out.push({ harness: id, key: e.key, version: e.version, path: e.path, scope: e.scope, own: e.own });
|
|
144
175
|
}
|
|
145
176
|
} else if (s.format === "portable-plugin-registration") {
|
|
146
|
-
const a = analyse
|
|
177
|
+
const a = analyse
|
|
178
|
+
? analyse(harness, s)
|
|
179
|
+
: analysePortableRegistration(projectDir, s, { root, home, harness, env, version });
|
|
147
180
|
const v = a?.ownership?.version;
|
|
148
181
|
if (typeof v === "string" && v && a.enabled) out.push({ harness: id, key: a.id, version: v });
|
|
149
182
|
}
|
|
@@ -152,7 +185,8 @@ export function runningCopies(projectDir, { harnesses = [], root, home = homedir
|
|
|
152
185
|
return out;
|
|
153
186
|
}
|
|
154
187
|
|
|
155
|
-
export const newerCopies = (copies, version) =>
|
|
188
|
+
export const newerCopies = (copies, version) =>
|
|
189
|
+
version ? copies.filter((c) => cmpPrecedence(c.version, version) > 0) : [];
|
|
156
190
|
// The lowest of them: a fresh cache entry below it is read again (rule 8).
|
|
157
191
|
export const lowestVersion = (copies) => copies.map((c) => c.version).sort(cmpPrecedence)[0] || null;
|
|
158
192
|
|
|
@@ -163,7 +197,9 @@ function readCache(cachePath) {
|
|
|
163
197
|
try {
|
|
164
198
|
const c = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
165
199
|
return c && typeof c === "object" && !Array.isArray(c) ? c : {};
|
|
166
|
-
} catch {
|
|
200
|
+
} catch {
|
|
201
|
+
return {};
|
|
202
|
+
}
|
|
167
203
|
}
|
|
168
204
|
|
|
169
205
|
// Fresh for a day; an `at` in the future is stale (a clock that moved back).
|
|
@@ -178,29 +214,44 @@ function fresh(entry, t) {
|
|
|
178
214
|
// its connection is released.
|
|
179
215
|
function request(url, { fetch, timers, timeoutMs }) {
|
|
180
216
|
return new Promise((settle) => {
|
|
181
|
-
let done = false,
|
|
217
|
+
let done = false,
|
|
218
|
+
timer = null;
|
|
182
219
|
const ac = typeof AbortController === "function" ? new AbortController() : null;
|
|
183
220
|
const finish = (r) => {
|
|
184
221
|
if (done) return;
|
|
185
222
|
done = true;
|
|
186
|
-
if (timer !== null) {
|
|
223
|
+
if (timer !== null) {
|
|
224
|
+
try {
|
|
225
|
+
timers.clearTimeout(timer);
|
|
226
|
+
} catch {}
|
|
227
|
+
}
|
|
187
228
|
settle(r);
|
|
188
229
|
};
|
|
189
230
|
timer = timers.setTimeout(() => {
|
|
190
231
|
timer = null;
|
|
191
|
-
try {
|
|
232
|
+
try {
|
|
233
|
+
ac?.abort();
|
|
234
|
+
} catch {}
|
|
192
235
|
finish({ error: `no answer within ${timeoutMs / 1000} s` });
|
|
193
236
|
}, timeoutMs);
|
|
194
|
-
try {
|
|
237
|
+
try {
|
|
238
|
+
timer?.unref?.();
|
|
239
|
+
} catch {}
|
|
195
240
|
Promise.resolve()
|
|
196
241
|
.then(() => fetch(url, { headers: { accept: ACCEPT }, ...(ac ? { signal: ac.signal } : {}) }))
|
|
197
242
|
.then(async (res) => {
|
|
198
243
|
if (!res || !res.ok) {
|
|
199
|
-
try {
|
|
244
|
+
try {
|
|
245
|
+
Promise.resolve(res?.body?.cancel?.()).catch(() => {});
|
|
246
|
+
} catch {}
|
|
200
247
|
return finish({ error: `HTTP ${res?.status ?? "error"}` });
|
|
201
248
|
}
|
|
202
249
|
let body;
|
|
203
|
-
try {
|
|
250
|
+
try {
|
|
251
|
+
body = await res.json();
|
|
252
|
+
} catch {
|
|
253
|
+
return finish({ error: "the answer is not JSON" });
|
|
254
|
+
}
|
|
204
255
|
const latest = body && typeof body === "object" ? body["dist-tags"]?.latest : undefined;
|
|
205
256
|
if (!isSemver(latest)) return finish({ error: "the answer names no dist-tags.latest" });
|
|
206
257
|
finish({ latest });
|
|
@@ -217,7 +268,15 @@ function request(url, { fetch, timers, timeoutMs }) {
|
|
|
217
268
|
function writeCache(cachePath, cache, registry, latest, at) {
|
|
218
269
|
if (!cachePath) return;
|
|
219
270
|
const prev = cache.user_latest && typeof cache.user_latest === "object" ? cache.user_latest : null;
|
|
220
|
-
const next = {
|
|
271
|
+
const next = {
|
|
272
|
+
...cache,
|
|
273
|
+
[registry]: { latest, at },
|
|
274
|
+
user_latest: {
|
|
275
|
+
latest,
|
|
276
|
+
at,
|
|
277
|
+
published_at: prev && prev.latest === latest && typeof prev.published_at === "string" ? prev.published_at : null,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
221
280
|
try {
|
|
222
281
|
mkdirSync(dirname(cachePath), { recursive: true });
|
|
223
282
|
writeFileAtomic(cachePath, JSON.stringify(next, null, 2) + "\n", { sweep: false });
|
|
@@ -230,7 +289,17 @@ function writeCache(cachePath, cache, registry, latest, at) {
|
|
|
230
289
|
// `atLeast`, the lowest newer copy rule 6 asks about: a release may have come
|
|
231
290
|
// out since it was written. The cache never supplies a URL: an entry is only
|
|
232
291
|
// looked up with the registry this run resolved. Never throws.
|
|
233
|
-
|
|
292
|
+
/* eslint-disable n/no-unsupported-features/node-builtins -- the global fetch is experimental but present at Node 20 and stable from 21; without it readLatest answers "no fetch in this Node", and the suite runs it on Node 20 in CI */
|
|
293
|
+
export async function readLatest({
|
|
294
|
+
registry,
|
|
295
|
+
cachePath = null,
|
|
296
|
+
fetch = globalThis.fetch,
|
|
297
|
+
now = Date.now,
|
|
298
|
+
timers = globalThis,
|
|
299
|
+
timeoutMs = READ_TIMEOUT_MS,
|
|
300
|
+
atLeast = null,
|
|
301
|
+
} = {}) {
|
|
302
|
+
/* eslint-enable n/no-unsupported-features/node-builtins */
|
|
234
303
|
try {
|
|
235
304
|
const cache = readCache(cachePath);
|
|
236
305
|
const entry = cache[registry];
|
|
@@ -261,13 +330,22 @@ export function coreCommand(verb, { version = null, args = "" } = {}) {
|
|
|
261
330
|
// part on its own line — the envelope's form, which an agent relays as it
|
|
262
331
|
// is; `advice` is what PLAN draws, the commands in their own blocks.
|
|
263
332
|
export function advised(text, blocks = []) {
|
|
264
|
-
return {
|
|
333
|
+
return {
|
|
334
|
+
reason: [text, ...blocks.flatMap((b) => [b.lead, b.command])]
|
|
335
|
+
.filter((x) => x !== null && x !== undefined)
|
|
336
|
+
.join("\n"),
|
|
337
|
+
advice: { text, blocks },
|
|
338
|
+
};
|
|
265
339
|
}
|
|
266
340
|
|
|
267
341
|
// The run's own selection, repeated: --harness and --surface as given, and
|
|
268
342
|
// --no-register; never --project (the ADR's decision 5).
|
|
269
343
|
function runArgs({ harnesses = [], surfaces = null, register = true } = {}) {
|
|
270
|
-
return [
|
|
344
|
+
return [
|
|
345
|
+
...harnesses.map((h) => `--harness ${h}`),
|
|
346
|
+
...(surfaces || []).map((s) => `--surface ${s}`),
|
|
347
|
+
...(register === false ? ["--no-register"] : []),
|
|
348
|
+
].join(" ");
|
|
271
349
|
}
|
|
272
350
|
|
|
273
351
|
// Stop when the registry's latest is at least a newer copy this project runs
|
|
@@ -276,12 +354,21 @@ function runArgs({ harnesses = [], surfaces = null, register = true } = {}) {
|
|
|
276
354
|
// readLatest's answer, null when nothing was read; `skipped` says the read
|
|
277
355
|
// was switched off by PROJECTSTORE_NO_UPDATE_CHECK. The refusal's text puts
|
|
278
356
|
// each command on a line of its own, never followed by punctuation.
|
|
279
|
-
export function decide({
|
|
357
|
+
export function decide({
|
|
358
|
+
copies = [],
|
|
359
|
+
version = null,
|
|
360
|
+
read = null,
|
|
361
|
+
skipped = false,
|
|
362
|
+
verb = "upgrade",
|
|
363
|
+
args = {},
|
|
364
|
+
} = {}) {
|
|
280
365
|
const newer = newerCopies(copies, version);
|
|
281
366
|
const decision = { checks: true, version, newer, read: skipped ? { skipped: true } : read, stop: null };
|
|
282
367
|
const latest = read && isSemver(read.latest) ? read.latest : null;
|
|
283
368
|
if (skipped || !latest) return decision;
|
|
284
|
-
const top = newer
|
|
369
|
+
const top = newer
|
|
370
|
+
.filter((c) => cmpPrecedence(latest, c.version) >= 0)
|
|
371
|
+
.sort((a, b) => cmpPrecedence(b.version, a.version))[0];
|
|
285
372
|
if (!top) return decision;
|
|
286
373
|
const lead = `this project runs ${PACKAGE} ${top.version} (${top.key}), newer than this package (${version}), and ${top.version} is published. Nothing is written. Run the newer one from a terminal in this project:`;
|
|
287
374
|
const commands = [coreCommand(verb, { version: "latest", args: runArgs(args) }), `npm install -g ${PACKAGE}@latest`];
|
|
@@ -294,7 +381,8 @@ export function decide({ copies = [], version = null, read = null, skipped = fal
|
|
|
294
381
|
// `v`: the dry run's note when no decision was made (plan reads nothing), none
|
|
295
382
|
// for a root that never checks, else what the read found.
|
|
296
383
|
export function proceedCase(decision, v) {
|
|
297
|
-
if (decision === null || decision === undefined)
|
|
384
|
+
if (decision === null || decision === undefined)
|
|
385
|
+
return `install and upgrade read the registry first, and stop when ${v} is published`;
|
|
298
386
|
if (!decision.checks) return null;
|
|
299
387
|
const r = decision.read;
|
|
300
388
|
if (!r) return null;
|
|
@@ -304,55 +392,79 @@ export function proceedCase(decision, v) {
|
|
|
304
392
|
return `${v} is published (the registry's latest is ${r.latest})`;
|
|
305
393
|
}
|
|
306
394
|
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
|
|
395
|
+
// The version a command a person runs next names (the setup story, rule 17;
|
|
396
|
+
// the converge story's review follow-up): the running version on a package
|
|
397
|
+
// root — npx's cache, a node_modules — so a run of `npx projectstore@0.30.0`
|
|
398
|
+
// is answered by commands that run 0.30.0 again, never whatever is latest. A
|
|
399
|
+
// checkout or a host's own copy names none.
|
|
400
|
+
export function pinFor(root, version) {
|
|
401
|
+
return version && root && isEphemeralRoot(root) ? version : null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Rule 2's helper: the ways to settle which copy a project runs, when a copy
|
|
405
|
+
// from another marketplace is enabled for it — setup's own commands (the
|
|
406
|
+
// setup story, rule 17): the terminal run, `npx projectstore setup`, which
|
|
407
|
+
// asks, then the two runs that name the answer without a terminal, `--channel
|
|
408
|
+
// npm` and `--channel git`, each alone on its line (a `|` copied into a shell
|
|
409
|
+
// is a pipe). Each names `@<version>` when the pin says so (pinFor), and
|
|
410
|
+
// never --project (the ADR's decision 5).
|
|
411
|
+
// - `gate` ({ args: { harnesses }, keys, version }): the channel gate's text
|
|
412
|
+
// (rule 2), for a switch planned under no channel answer without a
|
|
413
|
+
// terminal, the run's own --harness repeated in each command. setup takes
|
|
414
|
+
// no --surface, so a narrowed run's --surface is not repeated. A block with
|
|
415
|
+
// no lead follows the one before it.
|
|
416
|
+
// - otherwise, a newer copy that is not switched off (rule 6), for `harness`:
|
|
417
|
+
// keeping it lets that copy write this project's surfaces (the keep), and
|
|
418
|
+
// switching runs this older package — both named, after the terminal run.
|
|
419
|
+
export function channelChoices({ harness = null, copy = null, running = null, gate = null, pin = null } = {}) {
|
|
324
420
|
if (gate) {
|
|
325
421
|
const from = gate.keys?.length ? ` (${gate.keys.join(", ")})` : "";
|
|
326
|
-
|
|
422
|
+
const run = (extra = "") =>
|
|
423
|
+
coreCommand("setup", {
|
|
424
|
+
version: gate.version || null,
|
|
425
|
+
args: [runArgs({ harnesses: gate.args?.harnesses || [] }), extra].filter(Boolean).join(" "),
|
|
426
|
+
});
|
|
427
|
+
return [
|
|
428
|
+
{
|
|
429
|
+
lead: `Nothing written: this project loads ${PACKAGE} from the git marketplace${from}, and moving it to the npm install is a choice made at a terminal in this project:`,
|
|
430
|
+
command: run(),
|
|
431
|
+
},
|
|
432
|
+
{ lead: "Without a terminal, name the channel:", command: run("--channel npm") },
|
|
433
|
+
{ lead: null, command: run("--channel git") },
|
|
434
|
+
];
|
|
327
435
|
}
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
436
|
+
const run = (extra = "") =>
|
|
437
|
+
coreCommand("setup", { version: pin, args: [`--harness ${harness.id}`, extra].filter(Boolean).join(" ") });
|
|
438
|
+
return [
|
|
439
|
+
{ lead: "Choose at a terminal in this project; setup asks whether to keep that copy or switch:", command: run() },
|
|
440
|
+
{
|
|
441
|
+
lead: `Without a terminal, keep ${copy?.key || "that copy"}, which then writes this project's surfaces itself:`,
|
|
442
|
+
command: run("--channel git"),
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
lead: `Or move this project to this package's npm install, the older ${running}:`,
|
|
446
|
+
command: run("--channel npm"),
|
|
447
|
+
},
|
|
448
|
+
];
|
|
337
449
|
}
|
|
338
450
|
|
|
339
451
|
// A newer copy from another marketplace under channel null: not switched
|
|
340
452
|
// off, because switching it off would run the project on an older package.
|
|
341
453
|
// The rows name both versions and the case, then the ways out.
|
|
342
|
-
export function newerCopyDeferral({ copy, running, decision, harness,
|
|
454
|
+
export function newerCopyDeferral({ copy, running, decision, harness, pin = null }) {
|
|
343
455
|
const c = proceedCase(decision, copy.version);
|
|
344
456
|
const text = `this project runs a newer copy (${copy.key} ${copy.version}) than this package (${running}), so it is not switched off${c ? ` — ${c}` : ""}.`;
|
|
345
|
-
return advised(text, channelChoices({ harness,
|
|
457
|
+
return advised(text, channelChoices({ harness, copy, running, pin }));
|
|
346
458
|
}
|
|
347
459
|
|
|
348
460
|
// The consequence line of a switch (rule 2; presentation spec contract 7):
|
|
349
461
|
// the ADR's three facts — the git-marketplace copy is off for this project,
|
|
350
462
|
// the host's update no longer reaches it, and what updates it from now on —
|
|
351
463
|
// with no backticks, and no command followed by punctuation. The command is
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
export function switchConsequence(
|
|
355
|
-
return `this project stops loading the git-marketplace copy, and /plugin update no longer reaches it — from now on it is updated by ${
|
|
464
|
+
// setup's, in the core form and unpinned (the setup story, rule 17): what
|
|
465
|
+
// updates the project from now on is whichever version runs it next.
|
|
466
|
+
export function switchConsequence() {
|
|
467
|
+
return `this project stops loading the git-marketplace copy, and /plugin update no longer reaches it — from now on it is updated by ${coreCommand("setup")}`;
|
|
356
468
|
}
|
|
357
469
|
|
|
358
470
|
// The words a switch row adds under the channel "npm" (rules 2 and 6): over a
|
|
@@ -367,7 +479,9 @@ export function switchReason({ copy, running, runs = running, decision, unbound
|
|
|
367
479
|
// would be false.
|
|
368
480
|
if (copy && typeof copy.version === "string" && runs && cmpPrecedence(copy.version, runs) > 0) {
|
|
369
481
|
const c = proceedCase(decision, copy.version);
|
|
370
|
-
parts.push(
|
|
482
|
+
parts.push(
|
|
483
|
+
`${copy.key} ${copy.version} is newer than this package (${running})${c ? ` — ${c}` : ""}; turning it off runs this project on the older ${runs}`,
|
|
484
|
+
);
|
|
371
485
|
}
|
|
372
486
|
if (unbound) parts.push("the project is not bound yet: the switch is recorded in its binding once it is");
|
|
373
487
|
return parts.length ? parts.join("; ") : null;
|
|
@@ -380,9 +494,18 @@ export function switchReason({ copy, running, runs = running, decision, unbound
|
|
|
380
494
|
// npm row leaves — is turned back on in the host. Each command alone on its
|
|
381
495
|
// line (presentation spec contract 6).
|
|
382
496
|
export function noCopyToKeep({ harness, surface, off = [] }) {
|
|
383
|
-
const blocks = off.map((key) => ({
|
|
384
|
-
|
|
385
|
-
|
|
497
|
+
const blocks = off.map((key) => ({
|
|
498
|
+
lead: `This checkout's local settings hold ${key} off. To load it again, run this in ${harness.display_name}:`,
|
|
499
|
+
command: `/plugin enable ${key}`,
|
|
500
|
+
}));
|
|
501
|
+
blocks.push({
|
|
502
|
+
lead: "The way back from an earlier switch to the npm install is its uninstall, from a terminal in this project:",
|
|
503
|
+
command: coreCommand("uninstall", { args: runArgs({ harnesses: [harness.id], surfaces: [surface] }) }),
|
|
504
|
+
});
|
|
505
|
+
return advised(
|
|
506
|
+
`no git-marketplace copy of ${PACKAGE} is enabled for this project, so there is none to keep. Nothing is written.`,
|
|
507
|
+
blocks,
|
|
508
|
+
);
|
|
386
509
|
}
|
|
387
510
|
|
|
388
511
|
// ─── The keep (rule 5) ──────────────────────────────────────────────────
|
|
@@ -394,15 +517,29 @@ export function noCopyToKeep({ harness, surface, off = [] }) {
|
|
|
394
517
|
// the status line would opt it in (rule 5). `process.execPath`, not `node`:
|
|
395
518
|
// the Node that runs us is sure to exist, and the PLAN shows the argv verbatim.
|
|
396
519
|
export function keepArgv(copy, { verb, harness, projectDir }) {
|
|
397
|
-
return [
|
|
520
|
+
return [
|
|
521
|
+
process.execPath,
|
|
522
|
+
join(copy.path, "bin", "projectstore.mjs"),
|
|
523
|
+
verb,
|
|
524
|
+
"--harness",
|
|
525
|
+
harness.id,
|
|
526
|
+
"--no-register",
|
|
527
|
+
"--json",
|
|
528
|
+
"--project",
|
|
529
|
+
projectDir,
|
|
530
|
+
];
|
|
398
531
|
}
|
|
399
532
|
|
|
400
533
|
// The bin's `--json` envelope, `{ schema_version, verb, project, ok, result }`:
|
|
401
534
|
// its `result`, or null when stdout holds none. The top-level `ok` is never
|
|
402
535
|
// read — for plan it is `ok && !incomplete`, for a write verb the gate too.
|
|
403
536
|
export function keepResult(stdout) {
|
|
404
|
-
let env
|
|
405
|
-
try {
|
|
537
|
+
let env;
|
|
538
|
+
try {
|
|
539
|
+
env = JSON.parse(String(stdout || "").trim());
|
|
540
|
+
} catch {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
406
543
|
const r = env && typeof env === "object" && !Array.isArray(env) ? env.result : null;
|
|
407
544
|
return r && typeof r === "object" && !Array.isArray(r) ? r : null;
|
|
408
545
|
}
|
|
@@ -422,17 +559,37 @@ export function keepResult(stdout) {
|
|
|
422
559
|
export async function readKeepPlan(copy, { harness, projectDir, env, run, budget = 120000 } = {}) {
|
|
423
560
|
const argv = keepArgv(copy, { verb: "plan", harness, projectDir });
|
|
424
561
|
let r;
|
|
425
|
-
try {
|
|
426
|
-
|
|
562
|
+
try {
|
|
563
|
+
r = await run(argv[0], argv.slice(1), { env, cwd: projectDir, budget });
|
|
564
|
+
} catch (e) {
|
|
565
|
+
r = { status: null, stdout: "", stderr: "", error: e };
|
|
566
|
+
}
|
|
427
567
|
const status = r?.status ?? null;
|
|
428
568
|
const stderr = String(r?.stderr || "").trim();
|
|
429
569
|
const result = keepResult(r?.stdout);
|
|
430
|
-
if (!result)
|
|
570
|
+
if (!result)
|
|
571
|
+
return { argv, envelope: false, status, stderr, error: r?.error ? String(r.error.message || r.error) : null };
|
|
431
572
|
const items = Array.isArray(result.items) ? result.items.filter((i) => i && typeof i === "object") : [];
|
|
432
|
-
const writes = items
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
573
|
+
const writes = items
|
|
574
|
+
.filter((i) => !["skip", "refuse"].includes(i.action))
|
|
575
|
+
.map((i) => ({ surface: String(i.surface ?? ""), action: String(i.action ?? ""), path: i.path ?? null }));
|
|
576
|
+
const refusals = [
|
|
577
|
+
...(Array.isArray(result.refusals) ? result.refusals : []),
|
|
578
|
+
...items.filter((i) => i.action === "refuse").map((i) => i.reason),
|
|
579
|
+
].filter((x) => typeof x === "string" && x);
|
|
580
|
+
const deferred = items
|
|
581
|
+
.filter((i) => i.deferred === true)
|
|
582
|
+
.map((i) => ({ surface: String(i.surface ?? ""), reason: typeof i.reason === "string" ? i.reason : null }));
|
|
583
|
+
return {
|
|
584
|
+
argv,
|
|
585
|
+
envelope: true,
|
|
586
|
+
ok: result.ok !== false,
|
|
587
|
+
incomplete: result.incomplete === true,
|
|
588
|
+
refusals,
|
|
589
|
+
writes,
|
|
590
|
+
deferred,
|
|
591
|
+
status,
|
|
592
|
+
};
|
|
436
593
|
}
|
|
437
594
|
|
|
438
595
|
// The kept copy's own upgrade, in the form channelChoices names a copy's bin:
|
|
@@ -440,19 +597,28 @@ export async function readKeepPlan(copy, { harness, projectDir, env, run, budget
|
|
|
440
597
|
// preview and its own question (we never run what we could not preview,
|
|
441
598
|
// install spec contract 9).
|
|
442
599
|
export function ownUpgrade(copy, harness) {
|
|
443
|
-
return {
|
|
600
|
+
return {
|
|
601
|
+
lead: "Run that copy's own upgrade from a terminal in this project; it shows its own plan and asks:",
|
|
602
|
+
command: `node "${join(copy.path, "bin", "projectstore.mjs")}" upgrade --harness ${harness.id} --no-register`,
|
|
603
|
+
};
|
|
444
604
|
}
|
|
445
605
|
|
|
446
606
|
// This checkout's npm registration enabled beside the kept copy: the keep
|
|
447
607
|
// changes no registration, so it stays; its uninstall (rule 4) drops it.
|
|
448
608
|
export function npmRowBeside({ harness, id, surface }) {
|
|
449
|
-
return {
|
|
609
|
+
return {
|
|
610
|
+
lead: `This checkout's npm registration (${id}) stays enabled beside the kept copy. To drop it, from a terminal in this project:`,
|
|
611
|
+
command: coreCommand("uninstall", { args: runArgs({ harnesses: [harness.id], surfaces: [surface] }) }),
|
|
612
|
+
};
|
|
450
613
|
}
|
|
451
614
|
|
|
452
615
|
// A newer registration that is damaged: never rewritten or staged over from
|
|
453
616
|
// the older package; repaired from its own version.
|
|
454
617
|
export function repairAdvice(harness, v) {
|
|
455
|
-
return {
|
|
618
|
+
return {
|
|
619
|
+
lead: "not downgraded — repair it from its own version:",
|
|
620
|
+
command: coreCommand("upgrade", { version: v, args: `--harness ${harness.id}` }),
|
|
621
|
+
};
|
|
456
622
|
}
|
|
457
623
|
|
|
458
624
|
// ─── The update notice (rule 8) ─────────────────────────────────────────
|
|
@@ -464,5 +630,9 @@ export function repairAdvice(harness, v) {
|
|
|
464
630
|
export function updateNotice({ latest, version, root, verb = "upgrade" } = {}) {
|
|
465
631
|
if (!isSemver(latest) || !version || cmpPrecedence(latest, version) <= 0) return null;
|
|
466
632
|
const npx = /[\\/]_npx[\\/]/.test(String(root || ""));
|
|
467
|
-
return {
|
|
633
|
+
return {
|
|
634
|
+
latest,
|
|
635
|
+
text: `newer published: ${latest} (this is ${version}). To update:`,
|
|
636
|
+
command: npx ? coreCommand(verb, { version: "latest" }) : `npm install -g ${PACKAGE}@latest`,
|
|
637
|
+
};
|
|
468
638
|
}
|
|
@@ -112,14 +112,20 @@ function main() {
|
|
|
112
112
|
]);
|
|
113
113
|
const files = filterFiles(committed);
|
|
114
114
|
|
|
115
|
-
const fallback = !range && !since || (files.length === 0 && uncommitted.length === 0);
|
|
116
|
-
process.stdout.write(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
115
|
+
const fallback = (!range && !since) || (files.length === 0 && uncommitted.length === 0);
|
|
116
|
+
process.stdout.write(
|
|
117
|
+
JSON.stringify(
|
|
118
|
+
{
|
|
119
|
+
since: since || null,
|
|
120
|
+
range: usedRange,
|
|
121
|
+
files,
|
|
122
|
+
uncommitted,
|
|
123
|
+
fallback,
|
|
124
|
+
},
|
|
125
|
+
null,
|
|
126
|
+
2,
|
|
127
|
+
) + "\n",
|
|
128
|
+
);
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|