projectstore-codex 0.28.2 → 0.29.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/.codex-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/bin/projectstore-codex.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/harnesses/claude-code.json +3 -0
- package/node_modules/projectstore/harnesses/codex.json +4 -0
- package/node_modules/projectstore/package.json +1 -1
- package/node_modules/projectstore/scripts/build-adapters.mjs +8 -5
- package/node_modules/projectstore/scripts/cli.mjs +173 -26
- package/node_modules/projectstore/scripts/install-harness.mjs +336 -93
- package/node_modules/projectstore/scripts/term.mjs +147 -0
- package/package.json +2 -2
- package/skills/projectstore-agents/SKILL.md +5 -4
- package/skills/projectstore-doctor/SKILL.md +3 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectstore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Your agent runs the project through a verified loop: task → artifact (ADR / spec / epic / story) → adversarial critic → backlog → planner → reviewer → done. Plain markdown in git — any model can pick the project up tomorrow.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Evgenii Konev",
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ From a terminal in your project:
|
|
|
8
8
|
npx projectstore-codex install --project "$PWD"
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
The installer
|
|
11
|
+
The installer prints its plan first — every mutation and every Codex command, verbatim — and at a terminal asks `Apply N changes? [Y/n]` (without one, naming the shell is the confirmation; `--json` never asks). It then stages a stable local marketplace under `CODEX_HOME`, asks Codex's own CLI to install the plugin, verifies the materialised cache by version and payload digest, and says what to do next. `npx projectstore-codex plan --project "$PWD"` prints the same plan and writes nothing; `--verbose` adds every row's reasoning; `<verb> --help` lists a verb's options with examples. Restart Codex, approve the ProjectStore hooks when prompted, then run `$projectstore-bind <vault-path>`. Codex picks up hook changes late: a release that changes hooks may take effect only in the session after next.
|
|
12
12
|
|
|
13
13
|
Upgrade with `npx projectstore-codex@<version> upgrade --project "$PWD"`. Project uninstall leaves the user-global Codex plugin in place; `uninstall --global` is the explicit machine-wide removal.
|
|
14
14
|
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
// `--harness codex` inserted after a verb that takes it. Every other
|
|
9
9
|
// argument passes through, so `projectstore-codex <verb> …` is exactly
|
|
10
10
|
// `projectstore <verb> --harness codex …` — the same preview, the
|
|
11
|
-
// same files, the same exit code. Naming the shell
|
|
12
|
-
//
|
|
11
|
+
// same files, the same exit code. Naming the shell names the harness, exactly
|
|
12
|
+
// as --harness does: without a terminal that is the confirmation the core's
|
|
13
|
+
// install gate asks for; at a terminal the core shows the plan and asks (the
|
|
14
|
+
// install spec, contract 9 as amended 2026-10-04).
|
|
13
15
|
import { existsSync } from "node:fs";
|
|
14
16
|
import { spawnSync } from "node:child_process";
|
|
15
17
|
import { constants as osConstants } from "node:os";
|
|
@@ -72,17 +74,20 @@ if (!core) {
|
|
|
72
74
|
process.stderr.write(`${SHELL}: ${fixed.error}\n`);
|
|
73
75
|
process.exitCode = 2;
|
|
74
76
|
} else {
|
|
75
|
-
// stdio inherited: the core's install gate asks on a
|
|
76
|
-
// without one, so the child must see the
|
|
77
|
-
// timeout — the child waits on a human at the
|
|
78
|
-
// exit(): the core's own bin says why (a pending
|
|
77
|
+
// stdio inherited: the core's install gate shows the plan and asks on a
|
|
78
|
+
// terminal, and refuses a bare run without one, so the child must see the
|
|
79
|
+
// real stdin and stdout. No timeout — the child waits on a human at the
|
|
80
|
+
// question. exitCode, not exit(): the core's own bin says why (a pending
|
|
81
|
+
// write on a pipe). PROJECTSTORE_SHELL lets the core's --help speak this
|
|
82
|
+
// shell's name; nothing the core plans or writes reads it.
|
|
79
83
|
const r = spawnSync(process.execPath, [core, ...fixed.argv], {
|
|
80
84
|
stdio: "inherit",
|
|
81
|
-
env: { ...process.env, PROJECTSTORE_DISTRIBUTION_ROOT: root },
|
|
85
|
+
env: { ...process.env, PROJECTSTORE_DISTRIBUTION_ROOT: root, PROJECTSTORE_SHELL: SHELL },
|
|
82
86
|
});
|
|
83
87
|
if (r.error) process.stderr.write(`${SHELL}: ${r.error.message}\n`);
|
|
84
|
-
// A signal is relayed the shell way (128 + its number):
|
|
85
|
-
//
|
|
88
|
+
// A signal is relayed the shell way (128 + its number): the core reads
|
|
89
|
+
// its answer in the terminal's line mode, so Ctrl-C at the question is a
|
|
90
|
+
// real SIGINT — 130 here as it would be on the core itself.
|
|
86
91
|
process.exitCode = r.status ?? (r.signal ? 128 + (osConstants.signals[r.signal] || 0) : 2);
|
|
87
92
|
}
|
|
88
93
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"name": "projectstore",
|
|
13
13
|
"displayName": "projectstore",
|
|
14
14
|
"description": "📚 Your agent runs the project through a verified loop: task → artifact (ADR · spec · epic · story) → adversarial critic → backlog → planner → reviewer → done. Plain markdown in an Obsidian-friendly vault, every write approved by you — and any model can pick the project up tomorrow.",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.29.0",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "Evgenii Konev",
|
|
18
18
|
"email": "ekonev@smartandpoint.com",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectstore",
|
|
3
3
|
"displayName": "projectstore",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.29.0",
|
|
5
5
|
"description": "Your agent runs the project through a verified loop: task → artifact (ADR / spec / epic / story) → adversarial critic → backlog → planner → reviewer → done. Plain markdown in git — any model can pick the project up tomorrow.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Evgenii Konev @ SmartAndPoint",
|
|
@@ -94,7 +94,7 @@ Contributors: `git clone` this repo, then `claude --plugin-dir ./ProjectStore`.
|
|
|
94
94
|
npx projectstore-claude install --project "$PWD"
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
The same tree is published to npm as [`projectstore`](https://www.npmjs.com/package/projectstore) — one source package carrying every harness's manifest — and `projectstore-claude` is its Claude Code shell: the core pinned at the same version and bundled inside, the harness fixed, so the one command has the same shape on every harness. It registers the plugin with Claude Code: it writes a small local marketplace of its own under your Claude home, then drives `claude plugin marketplace add` / `plugin install` **at local scope**, so the registration lands in this checkout's `.claude/settings.local.json` and nowhere else.
|
|
97
|
+
The same tree is published to npm as [`projectstore`](https://www.npmjs.com/package/projectstore) — one source package carrying every harness's manifest — and `projectstore-claude` is its Claude Code shell: the core pinned at the same version and bundled inside, the harness fixed, so the one command has the same shape on every harness. It registers the plugin with Claude Code: it writes a small local marketplace of its own under your Claude home, then drives `claude plugin marketplace add` / `plugin install` **at local scope**, so the registration lands in this checkout's `.claude/settings.local.json` and nowhere else. It prints its plan first — every file it writes and every host command, verbatim — and at a terminal asks `Apply N changes? [Y/n]`; then it shows each step as it runs and what to do next. Without a terminal (a script, CI, an agent's tool) naming the harness is the confirmation. `plan` prints the same plan and writes nothing; `--verbose` adds every row's reasoning; `npx projectstore-claude <verb> --help` lists a verb's options with examples. Restart Claude Code afterwards. A git-marketplace copy already enabled for the checkout is silenced there (not globally) so the plugin does not load twice; `uninstall` turns it back on. Pin or upgrade with `npx projectstore-claude@<version> upgrade --project "$PWD"` — the version you name is the version you run. The core's low-level form, `npx projectstore <verb> --harness claude-code …`, is exactly what the shell runs. bun works the same on the packed bin.
|
|
98
98
|
|
|
99
99
|
**Codex has its own shell with the same one-command shape:**
|
|
100
100
|
`npx projectstore-codex install --project "$PWD"`. It carries a Codex
|
|
@@ -120,7 +120,7 @@ The package also carries a `bin`. Without a session — in CI, or in a shell —
|
|
|
120
120
|
|
|
121
121
|
```
|
|
122
122
|
npx projectstore doctor --json
|
|
123
|
-
npx projectstore install --harness claude-code # the low-level form the shell runs:
|
|
123
|
+
npx projectstore install --harness claude-code # the low-level form the shell runs: shows the plan, asks at a terminal, then writes; without a terminal naming the harness is the confirmation — there is no --yes
|
|
124
124
|
npx projectstore reconcile --write --only kanban
|
|
125
125
|
```
|
|
126
126
|
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
"/plugin marketplace add SmartAndPoint/ProjectStore && /plugin install projectstore@SmartAndPoint (git marketplace, user-wide)",
|
|
21
21
|
"restart Claude Code"
|
|
22
22
|
],
|
|
23
|
+
"next": [
|
|
24
|
+
"restart Claude Code in this project"
|
|
25
|
+
],
|
|
23
26
|
"notes": [
|
|
24
27
|
"Git marketplace only: auto-update is OFF by default for third-party plugins: /plugin -> Marketplaces -> SmartAndPoint -> toggle auto-update. The npm registration updates when you run `npx projectstore-claude upgrade --project <dir>`.",
|
|
25
28
|
"Pin a git-marketplace release with: /plugin marketplace add SmartAndPoint/ProjectStore#<tag>; pin the npm registration with npx projectstore-claude@<version>.",
|
|
@@ -298,6 +298,10 @@
|
|
|
298
298
|
"restart Codex and approve the ProjectStore hooks when prompted",
|
|
299
299
|
"run `$projectstore-bind <vault-path>` in Codex"
|
|
300
300
|
],
|
|
301
|
+
"next": [
|
|
302
|
+
"restart Codex from a terminal and approve the ProjectStore hooks when it asks",
|
|
303
|
+
"in an unbound project, run `$projectstore-bind <vault-path>` in Codex"
|
|
304
|
+
],
|
|
301
305
|
"notes": [
|
|
302
306
|
"The installation is user-global because Codex's marketplace, plugin row and cache live in CODEX_HOME; the AGENTS.md block is project-local.",
|
|
303
307
|
"Upgrade with `npx projectstore-codex@<version> upgrade --project \"$PWD\"`; the named package version is also the plugin version.",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectstore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Your agent runs the project through a verified loop: task → artifact (ADR / spec / epic / story) → adversarial critic → backlog → planner → reviewer → done. Plain markdown in git — any model can pick the project up tomorrow.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"project-management",
|
|
@@ -80,10 +80,11 @@ const COMMAND_OVERRIDES = {
|
|
|
80
80
|
|
|
81
81
|
## register / unregister
|
|
82
82
|
|
|
83
|
-
Preview the requested change
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
Preview the requested change with \`plan --harness codex --surface agents_block
|
|
84
|
+
--project "$PWD"\` and ask for explicit approval. On approval, run the core's
|
|
85
|
+
\`install\` or \`uninstall\` verb with \`--harness codex --surface agents_block
|
|
86
|
+
--project "$PWD" --json\` — \`--json\` never waits on a terminal's question — and
|
|
87
|
+
report the envelope's result. Never edit the managed block by hand.
|
|
87
88
|
|
|
88
89
|
## status
|
|
89
90
|
|
|
@@ -134,7 +135,9 @@ arguments and \`--json\`. Summarize every finding without re-deriving it.
|
|
|
134
135
|
When \`--fix\` is absent, remain read-only. When it is present, separate fixes
|
|
135
136
|
by owner: derived vault views use \`$projectstore-reconcile\`; Codex plugin or
|
|
136
137
|
agents-block drift uses the core's \`upgrade --harness codex\` path. Preview
|
|
137
|
-
each mutation and ask for explicit approval before
|
|
138
|
+
each mutation with \`plan --harness codex\` and ask for explicit approval before
|
|
139
|
+
running it; then run \`upgrade --harness codex --json\`, whose envelope is the
|
|
140
|
+
result — \`--json\` never waits on a terminal's question. Unsupported
|
|
138
141
|
surfaces remain unsupported; do not create host configuration by hand. Never
|
|
139
142
|
claim a fix after a non-zero exit.`,
|
|
140
143
|
},
|
|
@@ -41,7 +41,7 @@ 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
46
|
import { readConfigAt, readOverlayAt, resolveAgentModel, writeOverlayAt, overlayId, layoutRoster } from "./lib.mjs";
|
|
47
47
|
import { READ_OPERATIONS, LINEAGE_KINDS, LINEAGE_DEFAULT_DEPTH, SEARCH_DEFAULT_LIMIT, GRAPH_EDGE_CAP, DIRECTIONS } from "./query.mjs";
|
|
@@ -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({
|
|
@@ -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,7 +395,10 @@ 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
404
|
if (row.requiresBinding && !cfg) return fail(verb, project, `${project} is not bound to a vault — run /projectstore:bind <vault> in a session, or \`projectstore bind <vault>\` (\`projectstore init <vault>\` also creates the vault).`, 3);
|
|
@@ -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
|
}
|