projectstore-claude 0.28.2 → 0.29.1
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 +3 -1
- package/bin/projectstore-claude.mjs +14 -9
- 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 +2 -2
- package/node_modules/projectstore/commands/review.md +1 -1
- package/node_modules/projectstore/harnesses/claude-code.json +20 -2
- package/node_modules/projectstore/harnesses/codex.json +24 -4
- package/node_modules/projectstore/hooks/pre-compact.mjs +2 -1
- package/node_modules/projectstore/hooks/session-rules.mjs +19 -7
- package/node_modules/projectstore/hooks/session-start.mjs +20 -18
- package/node_modules/projectstore/package.json +2 -2
- package/node_modules/projectstore/scripts/binding.mjs +3 -3
- package/node_modules/projectstore/scripts/build-adapters.mjs +51 -18
- package/node_modules/projectstore/scripts/cli.mjs +176 -29
- package/node_modules/projectstore/scripts/codemap.mjs +2 -2
- package/node_modules/projectstore/scripts/doctor.mjs +110 -58
- package/node_modules/projectstore/scripts/draft.mjs +4 -3
- package/node_modules/projectstore/scripts/graph.mjs +3 -2
- package/node_modules/projectstore/scripts/harness.mjs +89 -0
- package/node_modules/projectstore/scripts/install-harness.mjs +336 -93
- package/node_modules/projectstore/scripts/kanban.mjs +3 -2
- package/node_modules/projectstore/scripts/lib.mjs +69 -8
- package/node_modules/projectstore/scripts/mcp.mjs +2 -2
- package/node_modules/projectstore/scripts/query.mjs +2 -1
- package/node_modules/projectstore/scripts/reconcile.mjs +4 -3
- package/node_modules/projectstore/scripts/story-section.mjs +2 -1
- package/node_modules/projectstore/scripts/surfaces.mjs +2 -1
- package/node_modules/projectstore/scripts/term.mjs +149 -0
- package/node_modules/projectstore/scripts/touch-session.mjs +6 -1
- package/node_modules/projectstore/scripts/worktree.mjs +12 -7
- package/package.json +2 -2
|
@@ -41,9 +41,9 @@ import { readFileSync, existsSync } from "node:fs";
|
|
|
41
41
|
import { resolve, dirname } from "node:path";
|
|
42
42
|
import { fileURLToPath } from "node:url";
|
|
43
43
|
import { parseArgs } from "node:util";
|
|
44
|
-
import
|
|
44
|
+
import * as term from "./term.mjs";
|
|
45
45
|
import { projectRootDeclared, childEnv, harnessIds, harnessForOverlay, pinPluginRoot } from "./harness.mjs";
|
|
46
|
-
import { readConfigAt, readOverlayAt, resolveAgentModel, writeOverlayAt, overlayId, layoutRoster } from "./lib.mjs";
|
|
46
|
+
import { readConfigAt, readOverlayAt, resolveAgentModel, writeOverlayAt, overlayId, layoutRoster, commandForm } from "./lib.mjs";
|
|
47
47
|
import { READ_OPERATIONS, LINEAGE_KINDS, LINEAGE_DEFAULT_DEPTH, SEARCH_DEFAULT_LIMIT, GRAPH_EDGE_CAP, DIRECTIONS } from "./query.mjs";
|
|
48
48
|
// binding.mjs is a write module imported statically where the install family
|
|
49
49
|
// is lazy: it is a dependency-free leaf with no side effects, so the MCP
|
|
@@ -83,14 +83,15 @@ export function resolveProject({ project = null, env = process.env, cwd = proces
|
|
|
83
83
|
const opt = (name, arg, summary, multiple = false) => Object.freeze({ name, arg, summary, multiple });
|
|
84
84
|
const JSON_OPT = opt("json", false, "the envelope");
|
|
85
85
|
const READ_JSON = [JSON_OPT];
|
|
86
|
-
const HARNESS_OPT = opt("harness", "<id>", "the harness — and,
|
|
86
|
+
const HARNESS_OPT = opt("harness", "<id>", "the harness — and, without a terminal, the confirmation (a terminal is asked); there is no --yes", true);
|
|
87
87
|
const SURFACE_OPT = opt("surface", "<key>", "one surface and those beneath it", true);
|
|
88
88
|
// The layout move's remedy names this for any copy but the package's own
|
|
89
89
|
// registration (the layout spec, contract 12 as amended 2026-10-03): it makes
|
|
90
90
|
// "no host command" true by construction instead of by recognising the root.
|
|
91
91
|
const NO_REGISTER_OPT = opt("no-register", false, "leave the plugin registration alone: change only this project's files");
|
|
92
|
-
const
|
|
93
|
-
const
|
|
92
|
+
const VERBOSE_OPT = opt("verbose", false, "every row's reasoning, each step's why and the host's own notes");
|
|
93
|
+
const INSTALL_OPTS = [HARNESS_OPT, SURFACE_OPT, NO_REGISTER_OPT, VERBOSE_OPT, JSON_OPT];
|
|
94
|
+
const UNINSTALL_OPTS = [HARNESS_OPT, SURFACE_OPT, opt("global", false, "also remove the harness-global plugin registration"), VERBOSE_OPT, JSON_OPT];
|
|
94
95
|
|
|
95
96
|
export const VERBS = Object.freeze([
|
|
96
97
|
Object.freeze({
|
|
@@ -172,7 +173,7 @@ export const VERBS = Object.freeze([
|
|
|
172
173
|
run: runBind(false),
|
|
173
174
|
}),
|
|
174
175
|
Object.freeze({
|
|
175
|
-
verb: "init", summary:
|
|
176
|
+
verb: "init", summary: `init <vault> — create the vault directory and bind to it; the layout's folders come from ${commandForm("scaffold")}.`,
|
|
176
177
|
module: "./binding.mjs", wraps: "new", how: "import", output: "envelope", writes: true, requiresBinding: false, mcp: Object.freeze([]),
|
|
177
178
|
options: [opt("layout", "<name>", `the layout (default ${DEFAULT_LAYOUT})`), opt("language", "<code>", `the template language (default ${DEFAULT_LANGUAGE})`), opt("rebind", false, "an already bound project: create the new vault and point the project at it; every other setting is kept"), JSON_OPT],
|
|
178
179
|
run: runBind(true),
|
|
@@ -195,17 +196,141 @@ export const VERBS = Object.freeze([
|
|
|
195
196
|
// story that adds a verb in slices.
|
|
196
197
|
export const PLANNED_VERBS = Object.freeze([]);
|
|
197
198
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
// The verbs as a person meets them: setting a project up, reading the vault,
|
|
200
|
+
// keeping it consistent, serving it. A verb not listed lands in "Other", so a
|
|
201
|
+
// new row is never hidden by this table.
|
|
202
|
+
const HELP_GROUPS = [
|
|
203
|
+
["Set up", ["install", "upgrade", "uninstall", "plan", "bind", "init", "agents"]],
|
|
204
|
+
["Read", ["status", "search", "show", "graph", "codemap", "orientation"]],
|
|
205
|
+
["Check and repair", ["doctor", "reconcile"]],
|
|
206
|
+
["Serve", ["mcp", "version"]],
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
// Examples per verb. `{cmd}` is how this run was invoked (a shell's own name
|
|
210
|
+
// when PROJECTSTORE_SHELL says so, else the core — a shell passes the read
|
|
211
|
+
// verbs through, so its name serves them too), `{h}` the harness argument the
|
|
212
|
+
// core needs and a shell fixes.
|
|
213
|
+
const EXAMPLES = {
|
|
214
|
+
install: ["{cmd} install{h}", "{cmd} plan{h} # the same plan; nothing is written"],
|
|
215
|
+
upgrade: ["{cmd}@<version> upgrade{h} # the version you name is the version that runs", "{cmd} upgrade{h} --verbose"],
|
|
216
|
+
uninstall: ["{cmd} uninstall{h}", "{cmd} uninstall{h} --surface statusline"],
|
|
217
|
+
plan: ["{cmd} plan{h}", "{cmd} plan{h} --json # one envelope, for scripts and agents"],
|
|
218
|
+
status: ["{cmd} status", "{cmd} status --json"],
|
|
219
|
+
orientation: ["{cmd} orientation --json"],
|
|
220
|
+
search: ['{cmd} search "entry rule" --kind spec', "{cmd} search queue --status accepted --limit 5"],
|
|
221
|
+
show: ["{cmd} show adr/README.md", "{cmd} show epics/PS-CORE/epic.md --section acceptance"],
|
|
222
|
+
graph: ["{cmd} graph neighbors adr/README.md --direction out", "{cmd} graph lineage epics/PS-CORE/epic.md --depth 2"],
|
|
223
|
+
codemap: ["{cmd} codemap --for PS-CORE", "{cmd} codemap --for scripts/lib.mjs --reverse"],
|
|
224
|
+
doctor: ["{cmd} doctor", "{cmd} doctor --install --json"],
|
|
225
|
+
reconcile: ["{cmd} reconcile # what would change; nothing is written", "{cmd} reconcile --write"],
|
|
226
|
+
bind: ["{cmd} bind ~/vaults/my-project", "{cmd} bind ~/vaults/other --rebind"],
|
|
227
|
+
init: ["{cmd} init ~/vaults/new-project --language ru"],
|
|
228
|
+
agents: ["{cmd} agents show", "{cmd} agents configure{h} --default opus --agent clerk=sonnet"],
|
|
229
|
+
mcp: ['{cmd} mcp --project "$PWD"'],
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Positional arguments a summary does not already name.
|
|
233
|
+
const ARGS = { search: "<phrase>", show: "<path>" };
|
|
234
|
+
|
|
235
|
+
// "bind <vault> — bind this project…" names its own form; "graph neighbors
|
|
236
|
+
// <path> | graph lineage <path> — …" names two.
|
|
237
|
+
function forms(row) {
|
|
238
|
+
const m = /^(.+?) — (.+)$/.exec(row.summary);
|
|
239
|
+
if (m && m[1].startsWith(row.verb + " ")) {
|
|
240
|
+
const list = m[1].split(" | ").map((f) => f.replace(new RegExp(`^${row.verb} `), ""));
|
|
241
|
+
return { forms: list, about: m[2][0].toUpperCase() + m[2].slice(1) };
|
|
242
|
+
}
|
|
243
|
+
return { forms: [ARGS[row.verb] || ""], about: row.summary };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function invocation(env = process.env) {
|
|
247
|
+
const shell = env.PROJECTSTORE_SHELL || null;
|
|
248
|
+
return { shell, cmd: shell ? `npx ${shell}` : "npx projectstore" };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function optionLines(options) {
|
|
252
|
+
const rows = options.map((o) => [`--${o.name}${o.arg ? " " + o.arg : ""}`, `${o.summary}${o.multiple ? " (repeatable)" : ""}`]);
|
|
253
|
+
const w = Math.min(28, Math.max(0, ...rows.map(([l]) => l.length)));
|
|
254
|
+
return rows.map(([l, r]) => (l.length > w ? ` ${l}\n ${" ".repeat(w)} ${r}` : ` ${l.padEnd(w)} ${r}`));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const EXIT_CODES = "Exit codes 0 ok · 1 findings or a refusal · 2 usage · 3 not bound";
|
|
258
|
+
|
|
259
|
+
export function usage(env = process.env) {
|
|
260
|
+
const { cmd } = invocation(env);
|
|
261
|
+
const lines = [
|
|
262
|
+
"projectstore — project memory for coding agents: decisions, specs, epics and stories as plain markdown.",
|
|
263
|
+
"",
|
|
264
|
+
"Usage",
|
|
265
|
+
` ${cmd} <verb> [options]`,
|
|
266
|
+
` ${cmd} <verb> --help one verb's options and examples`,
|
|
267
|
+
];
|
|
268
|
+
const seen = new Set();
|
|
269
|
+
const groups = HELP_GROUPS.map(([title, names]) => [title, names.map((n) => VERBS.find((v) => v.verb === n)).filter(Boolean)]);
|
|
270
|
+
for (const [, rows] of groups) for (const r of rows) seen.add(r.verb);
|
|
271
|
+
const other = VERBS.filter((v) => !seen.has(v.verb));
|
|
272
|
+
if (other.length) groups.push(["Other", other]);
|
|
273
|
+
for (const [title, rows] of groups) {
|
|
274
|
+
if (!rows.length) continue;
|
|
275
|
+
lines.push("", title);
|
|
276
|
+
for (const v of rows) lines.push(` ${v.verb.padEnd(12)} ${v.summary}`);
|
|
203
277
|
}
|
|
204
|
-
if (PLANNED_VERBS.length) lines.push("", `
|
|
205
|
-
lines.push(
|
|
278
|
+
if (PLANNED_VERBS.length) lines.push("", `Planned: ${PLANNED_VERBS.map((v) => `${v.verb} (${v.lands})`).join(", ")}`);
|
|
279
|
+
lines.push(
|
|
280
|
+
"",
|
|
281
|
+
"Every verb",
|
|
282
|
+
" --project <dir> the project (default: the host session's project, else the current directory)",
|
|
283
|
+
" --json one envelope { schema_version, verb, project, ok, result } — for scripts and agents",
|
|
284
|
+
" --version the package version",
|
|
285
|
+
"",
|
|
286
|
+
"Tips",
|
|
287
|
+
" install, upgrade and uninstall show their plan first; `plan` prints the same plan and writes nothing.",
|
|
288
|
+
" At a terminal they ask before writing. Without one, --harness is the confirmation — there is no --yes.",
|
|
289
|
+
" Unattended in a terminal (a script, a Makefile): pass --json, set CI=1, or give stdin no terminal (`</dev/null`).",
|
|
290
|
+
" --verbose on those verbs adds every row's reasoning and the host's own notes.",
|
|
291
|
+
"",
|
|
292
|
+
`Harnesses ${harnessIds().join(", ")}`,
|
|
293
|
+
EXIT_CODES,
|
|
294
|
+
);
|
|
206
295
|
return lines.join("\n");
|
|
207
296
|
}
|
|
208
297
|
|
|
298
|
+
// One verb: what it does, how to call it, every option, examples.
|
|
299
|
+
export function verbHelp(row, env = process.env) {
|
|
300
|
+
const { shell, cmd } = invocation(env);
|
|
301
|
+
const fixed = Boolean(shell) && row.options.some((o) => o.name === "harness");
|
|
302
|
+
const opts = row.options.filter((o) => !(fixed && o.name === "harness"));
|
|
303
|
+
const { forms: list, about } = forms(row);
|
|
304
|
+
const lines = [`${cmd} ${row.verb} — ${about}`, "", "Usage"];
|
|
305
|
+
for (const f of list) lines.push(` ${cmd} ${row.verb}${f ? " " + f : ""}${opts.length ? " [options]" : ""}`);
|
|
306
|
+
if (opts.length) lines.push("", "Options", ...optionLines(opts));
|
|
307
|
+
if (fixed) lines.push("", `${shell} names the harness itself: without a terminal, the verb is its own confirmation.`);
|
|
308
|
+
else if (row.options.some((o) => o.name === "harness")) lines.push("", `Harnesses ${harnessIds().join(", ")}`);
|
|
309
|
+
const ex = EXAMPLES[row.verb] || [];
|
|
310
|
+
const h = fixed ? "" : " --harness <id>";
|
|
311
|
+
const rendered = ex.map((e) => e.split("{cmd}").join(cmd).split("{h}").join(h).split(" # "));
|
|
312
|
+
const col = Math.max(0, ...rendered.filter((r) => r.length > 1).map(([c]) => c.length));
|
|
313
|
+
if (ex.length) lines.push("", "Examples", ...rendered.map(([c, note]) => ` ${note ? `${c.padEnd(col)} # ${note}` : c}`));
|
|
314
|
+
lines.push("", EXIT_CODES);
|
|
315
|
+
return lines.join("\n");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// The nearest name within two edits — for a mistyped verb or option.
|
|
319
|
+
export function nearest(word, names) {
|
|
320
|
+
const d = (a, b) => {
|
|
321
|
+
const m = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
322
|
+
for (let j = 1; j <= b.length; j++) m[0][j] = j;
|
|
323
|
+
for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) m[i][j] = Math.min(m[i - 1][j] + 1, m[i][j - 1] + 1, m[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
324
|
+
return m[a.length][b.length];
|
|
325
|
+
};
|
|
326
|
+
let best = null, score = 3;
|
|
327
|
+
for (const n of names) {
|
|
328
|
+
const k = n.startsWith(word) && word.length >= 3 ? 0 : d(word, n);
|
|
329
|
+
if (k < score) { best = n; score = k; }
|
|
330
|
+
}
|
|
331
|
+
return best;
|
|
332
|
+
}
|
|
333
|
+
|
|
209
334
|
// ─── run ───────────────────────────────────────────────────────────────
|
|
210
335
|
|
|
211
336
|
export async function run(argv, { env = process.env, cwd = process.cwd(), stdin = process.stdin, stdout = process.stdout, stderr = process.stderr, ask = null } = {}) {
|
|
@@ -217,7 +342,7 @@ export async function run(argv, { env = process.env, cwd = process.cwd(), stdin
|
|
|
217
342
|
strict: true,
|
|
218
343
|
options: {
|
|
219
344
|
project: { type: "string" }, json: { type: "boolean" }, help: { type: "boolean", short: "h" }, version: { type: "boolean", short: "v" },
|
|
220
|
-
harness: { type: "string", multiple: true }, surface: { type: "string", multiple: true }, global: { type: "boolean" }, "no-register": { type: "boolean" },
|
|
345
|
+
harness: { type: "string", multiple: true }, surface: { type: "string", multiple: true }, global: { type: "boolean" }, "no-register": { type: "boolean" }, verbose: { type: "boolean" },
|
|
221
346
|
write: { type: "boolean" }, only: { type: "string" }, install: { type: "boolean" }, vault: { type: "boolean" },
|
|
222
347
|
kind: { type: "string", multiple: true }, status: { type: "string" }, limit: { type: "string" }, "include-derived": { type: "boolean" }, "case-sensitive": { type: "boolean" },
|
|
223
348
|
body: { type: "boolean" }, section: { type: "string" }, direction: { type: "string" }, depth: { type: "string" }, for: { type: "string" }, reverse: { type: "boolean" },
|
|
@@ -228,7 +353,14 @@ export async function run(argv, { env = process.env, cwd = process.cwd(), stdin
|
|
|
228
353
|
} catch (e) {
|
|
229
354
|
// --json cannot be known before parsing; a raw scan is enough here.
|
|
230
355
|
if (argv.includes("--json")) stdout.write(JSON.stringify(envelope(argv.find((a) => !a.startsWith("-")) || null, null, false, { error: e.message }), null, 2) + "\n");
|
|
231
|
-
|
|
356
|
+
// Node's own wording for an unknown option explains "--" positionals; a
|
|
357
|
+
// person who typed --verbos wants the option they meant.
|
|
358
|
+
const unknown = e.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION" ? /'(-{1,2}[^']+)'/.exec(e.message)?.[1] : null;
|
|
359
|
+
const row = VERBS.find((v) => v.verb === argv.find((a) => !a.startsWith("-")));
|
|
360
|
+
const names = [...(row ? row.options.map((o) => o.name) : VERBS.flatMap((v) => v.options.map((o) => o.name))), "project", "json", "help", "version"];
|
|
361
|
+
const hint = unknown ? nearest(unknown.replace(/^-+/, ""), [...new Set(names)]) : null;
|
|
362
|
+
const message = unknown ? `${row ? row.verb + " does not take" : "unknown option"} ${unknown}${hint ? ` — did you mean --${hint}?` : ""}` : e.message;
|
|
363
|
+
stderr.write(`${message}\nRun ${row ? `\`${row.verb} --help\`` : "--help"} for the options.\n`);
|
|
232
364
|
return 2;
|
|
233
365
|
}
|
|
234
366
|
const { values, positionals } = parsed;
|
|
@@ -236,7 +368,7 @@ export async function run(argv, { env = process.env, cwd = process.cwd(), stdin
|
|
|
236
368
|
// a consumer (the MCP server, a script) always has something to parse.
|
|
237
369
|
const fail = (verb, project, message, code, { help = false } = {}) => {
|
|
238
370
|
if (values.json) stdout.write(JSON.stringify(envelope(verb, project, false, { error: message, exit: code }), null, 2) + "\n");
|
|
239
|
-
stderr.write(message + "\n" + (help ? usage() + "\n" : ""));
|
|
371
|
+
stderr.write(message + "\n" + (help ? usage(env) + "\n" : ""));
|
|
240
372
|
return code;
|
|
241
373
|
};
|
|
242
374
|
// In-process reads resolve layouts and registries from THIS package, as the
|
|
@@ -244,11 +376,17 @@ export async function run(argv, { env = process.env, cwd = process.cwd(), stdin
|
|
|
244
376
|
// session's variable points at.
|
|
245
377
|
pinPluginRoot(PACKAGE_ROOT);
|
|
246
378
|
if (values.version) return runVersion({ values, stdout });
|
|
247
|
-
if (values.help
|
|
379
|
+
if (values.help && positionals.length) {
|
|
380
|
+
const row = VERBS.find((v) => v.verb === positionals[0]);
|
|
381
|
+
if (row) { stdout.write(verbHelp(row, env) + "\n"); return 0; }
|
|
382
|
+
}
|
|
383
|
+
if (values.help || !positionals.length) { (values.help ? stdout : stderr).write(usage(env) + "\n"); return values.help ? 0 : 2; }
|
|
248
384
|
const verb = positionals[0];
|
|
249
385
|
const row = VERBS.find((v) => v.verb === verb);
|
|
250
386
|
if (!row) {
|
|
251
387
|
const planned = PLANNED_VERBS.find((v) => v.verb === verb);
|
|
388
|
+
const guess = planned ? null : nearest(verb, VERBS.map((v) => v.verb));
|
|
389
|
+
if (guess) return fail(verb, null, `unknown verb: ${verb} — did you mean ${guess}?\nRun --help for every verb.`, 2);
|
|
252
390
|
return fail(verb, null, (planned ? `${verb} lands with roadmap ${planned.lands}; not in this release.` : `unknown verb: ${verb}`), 2, { help: true });
|
|
253
391
|
}
|
|
254
392
|
// The options map is global (parseArgs), the rows are not: an option a row
|
|
@@ -257,10 +395,13 @@ export async function run(argv, { env = process.env, cwd = process.cwd(), stdin
|
|
|
257
395
|
const GLOBAL = new Set(["project", "json", "help", "version"]);
|
|
258
396
|
const declared = new Set(row.options.map((o) => o.name));
|
|
259
397
|
const stray = Object.keys(values).filter((k) => !GLOBAL.has(k) && !declared.has(k));
|
|
260
|
-
if (stray.length)
|
|
398
|
+
if (stray.length) {
|
|
399
|
+
const guess = nearest(stray[0], row.options.map((o) => o.name));
|
|
400
|
+
return fail(verb, null, `${verb} does not take --${stray[0]}${guess ? ` — did you mean --${guess}?` : ""}\nRun \`${verb} --help\` for the options.`, 2);
|
|
401
|
+
}
|
|
261
402
|
const project = resolveProject({ project: values.project, env, cwd });
|
|
262
403
|
const cfg = readConfigAt(project);
|
|
263
|
-
if (row.requiresBinding && !cfg) return fail(verb, project, `${project} is not bound to a vault — run
|
|
404
|
+
if (row.requiresBinding && !cfg) return fail(verb, project, `${project} is not bound to a vault — run ${commandForm("bind", { args: "<vault>", env })} in a session, or \`projectstore bind <vault>\` (\`projectstore init <vault>\` also creates the vault).`, 3);
|
|
264
405
|
try {
|
|
265
406
|
return await row.run({ row, values, positionals: positionals.slice(1), cfg, project, env, cwd, stdin, stdout, stderr, ask });
|
|
266
407
|
} catch (e) {
|
|
@@ -285,8 +426,9 @@ function ownEnv(env, project) {
|
|
|
285
426
|
async function confirmWrite(question, { stdin, stdout, ask }) {
|
|
286
427
|
if (ask) return /^y(es)?$/i.test(String(await ask(question)).trim());
|
|
287
428
|
if (!(stdin && stdin.isTTY && stdout && stdout.isTTY)) return null; // no terminal: refuse
|
|
288
|
-
|
|
289
|
-
|
|
429
|
+
// End of input is a no (term.mjs askLine), never a question left pending.
|
|
430
|
+
const answer = await term.askLine(question, stdin, stdout);
|
|
431
|
+
return answer !== null && /^y(es)?$/i.test(answer.trim());
|
|
290
432
|
}
|
|
291
433
|
|
|
292
434
|
// ─── verbs ─────────────────────────────────────────────────────────────
|
|
@@ -573,23 +715,28 @@ async function runReconcile({ values, project, env, stdin, stdout, stderr, ask }
|
|
|
573
715
|
|
|
574
716
|
async function runInstallVerb({ row, values, project, env, stdin, stdout, ask }) {
|
|
575
717
|
const ih = await import("./install-harness.mjs");
|
|
576
|
-
const opts = { harnesses: values.harness || [], surfaces: values.surface && values.surface.length ? values.surface : null, globalRemoval: Boolean(values.global), register: !values["no-register"], root: PACKAGE_ROOT, env: ownEnv(env, project), stdin, stdout, ask };
|
|
718
|
+
const opts = { harnesses: values.harness || [], surfaces: values.surface && values.surface.length ? values.surface : null, globalRemoval: Boolean(values.global), register: !values["no-register"], root: PACKAGE_ROOT, env: ownEnv(env, project), stdin, stdout, ask, json: Boolean(values.json), verbose: Boolean(values.verbose) };
|
|
577
719
|
if (row.verb === "plan") {
|
|
578
720
|
const p = ih.plan(project, opts);
|
|
579
|
-
|
|
721
|
+
if (values.json) stdout.write(JSON.stringify(envelope("plan", project, p.ok && !p.incomplete, { ...p, items: p.items.map(ih.publicItem) }), null, 2) + "\n");
|
|
722
|
+
else {
|
|
723
|
+
const c = term.caps(stdout, env);
|
|
724
|
+
stdout.write(ih.renderPreview(p, { verbose: opts.verbose, verb: "plan", paint: term.painter(c), icon: (n) => term.icon(c, n), width: c.live ? c.width : 0 }));
|
|
725
|
+
}
|
|
580
726
|
return p.ok && !p.incomplete ? 0 : 1;
|
|
581
727
|
}
|
|
582
|
-
|
|
728
|
+
// Text mode hands runVerb the stream: the plan is printed before any
|
|
729
|
+
// question, then each step, then DONE (install spec contracts 9 and 18).
|
|
730
|
+
const r = await ih.runVerb(row.verb, project, values.json ? opts : { ...opts, out: stdout });
|
|
583
731
|
// A registration the plan could not make (no host CLI) or a host command
|
|
584
732
|
// that failed is exit 1 with the rest applied (install spec contract 4′).
|
|
585
733
|
const ok = r.plan.ok && !r.plan.incomplete && !r.failed && (r.gate.confirmed || r.gate.why === "nothing-to-do");
|
|
586
734
|
if (values.json) {
|
|
587
735
|
stdout.write(JSON.stringify(envelope(row.verb, project, ok, { gate: r.gate, applied: r.applied, failed: r.failed, incomplete: r.plan.incomplete, plannedAgainst: r.plan.plannedAgainst, items: r.plan.items.map(ih.publicItem), refusals: r.plan.refusals, reports: r.plan.reports }), null, 2) + "\n");
|
|
588
|
-
} else {
|
|
589
|
-
stdout.write(r.
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
else if (r.gate.why === "declined") stdout.write("nothing written.\n");
|
|
736
|
+
} else if (r.gate.why === "non-tty") {
|
|
737
|
+
stdout.write(`Nothing written: without a terminal, a bare ${row.verb} refuses. Name the harness to confirm: --harness ${r.plan.detected.map((d) => d.id).join(" | ") || harnessIds().join(" | ")}\n`);
|
|
738
|
+
} else if (r.gate.why === "declined") {
|
|
739
|
+
stdout.write("Nothing written.\n");
|
|
593
740
|
}
|
|
594
741
|
return ok ? 0 : 1;
|
|
595
742
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
import { readConfig, loadLayout, folderByKind, parseFrontmatter, nowIso, isMain } from "./lib.mjs";
|
|
13
|
+
import { readConfig, loadLayout, folderByKind, parseFrontmatter, nowIso, isMain, commandForm } from "./lib.mjs";
|
|
14
14
|
|
|
15
15
|
function die(msg) {
|
|
16
16
|
process.stderr.write(`projectstore/codemap: ${msg}\n`);
|
|
@@ -29,7 +29,7 @@ function parseRefs(raw) {
|
|
|
29
29
|
|
|
30
30
|
function main() {
|
|
31
31
|
const cfg = readConfig();
|
|
32
|
-
if (!cfg) die(
|
|
32
|
+
if (!cfg) die(`No projectstore config. Run ${commandForm("bind")} first.`);
|
|
33
33
|
const layout = loadLayout(cfg.layout);
|
|
34
34
|
const folder = folderByKind(layout, "epic");
|
|
35
35
|
if (!folder) die("Layout has no epic folder — nothing to map.");
|