projectstore-claude 0.30.4 → 0.30.6
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/node_modules/projectstore/.claude-plugin/marketplace.json +2 -2
- package/node_modules/projectstore/.claude-plugin/plugin.json +6 -2
- package/node_modules/projectstore/.mcp.json +1 -2
- package/node_modules/projectstore/README.md +65 -16
- package/node_modules/projectstore/agents/archaeologist.md +1 -1
- package/node_modules/projectstore/agents/librarian.md +1 -1
- package/node_modules/projectstore/agents/planner.md +1 -1
- package/node_modules/projectstore/commands/doctor.md +14 -11
- package/node_modules/projectstore/commands/reconcile.md +1 -1
- package/node_modules/projectstore/commands/review.md +2 -2
- package/node_modules/projectstore/commands/story.md +1 -1
- package/node_modules/projectstore/docs/getting-started.md +3 -4
- package/node_modules/projectstore/docs/harnesses.md +1 -1
- package/node_modules/projectstore/docs/how-it-works.md +7 -0
- package/node_modules/projectstore/docs/images/icon.png +0 -0
- package/node_modules/projectstore/harnesses/claude-code.json +61 -11
- package/node_modules/projectstore/harnesses/codex.json +45 -15
- package/node_modules/projectstore/hooks/band.tsx +19 -8
- package/node_modules/projectstore/hooks/hooks.json +6 -6
- package/node_modules/projectstore/hooks/session-start.mjs +55 -24
- package/node_modules/projectstore/hooks/session-stop.mjs +43 -20
- package/node_modules/projectstore/package.json +11 -12
- package/node_modules/projectstore/scripts/band-shared.mjs +17 -2
- package/node_modules/projectstore/scripts/band.mjs +20 -2
- package/node_modules/projectstore/scripts/binding.mjs +1 -1
- package/node_modules/projectstore/scripts/build-adapters.mjs +45 -4
- package/node_modules/projectstore/scripts/cli.mjs +74 -1373
- package/node_modules/projectstore/scripts/convergence.mjs +59 -16
- package/node_modules/projectstore/scripts/doctor.mjs +199 -36
- package/node_modules/projectstore/scripts/harness.mjs +88 -6
- package/node_modules/projectstore/scripts/install-handlers.mjs +925 -0
- package/node_modules/projectstore/scripts/install-harness.mjs +129 -2081
- package/node_modules/projectstore/scripts/install-layout.mjs +453 -0
- package/node_modules/projectstore/scripts/install-preview.mjs +747 -0
- package/node_modules/projectstore/scripts/lib.mjs +387 -107
- package/node_modules/projectstore/scripts/mcp-server.mjs +33 -0
- package/node_modules/projectstore/scripts/mcp.mjs +60 -60
- package/node_modules/projectstore/scripts/model-spec.mjs +24 -0
- package/node_modules/projectstore/scripts/models.mjs +6 -10
- package/node_modules/projectstore/scripts/query.mjs +7 -6
- package/node_modules/projectstore/scripts/read-verbs.mjs +277 -0
- package/node_modules/projectstore/scripts/setup.mjs +62 -12
- package/node_modules/projectstore/scripts/statusline-launcher.mjs +36 -3
- package/node_modules/projectstore/scripts/surfaces.mjs +18 -5
- package/node_modules/projectstore/scripts/touch-session.mjs +30 -15
- package/node_modules/projectstore/scripts/verbs.mjs +1202 -0
- package/node_modules/projectstore/scripts/version-guard.mjs +57 -7
- package/package.json +3 -3
- package/node_modules/projectstore/docs/images/loop-light.svg +0 -94
- package/node_modules/projectstore/docs/images/team-light.svg +0 -79
|
@@ -2,27 +2,16 @@
|
|
|
2
2
|
//
|
|
3
3
|
// The token-free front-end: `projectstore <verb>` as a thin argv/output
|
|
4
4
|
// shell over the same core operations the hooks and the command files call
|
|
5
|
-
// (distribution ADR decisions 3 and 5). No logic lives here
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// a refusal, 2 usage or an internal failure, 3 not bound.
|
|
16
|
-
//
|
|
17
|
-
// The parse is per verb. A scan finds the verb first, knowing only the
|
|
18
|
-
// globals (--project <dir>, --json, --help/-h, --version/-v — the only options
|
|
19
|
-
// that may come before it); then one strict parseArgs runs on that verb's row
|
|
20
|
-
// plus the globals, so one name can be a flag on one verb and take a value on
|
|
21
|
-
// another (`doctor --vault` beside the front-door ADR's `setup --vault
|
|
22
|
-
// <path>`). A row's `usage` forms are both its help's Usage lines and the
|
|
23
|
-
// bound on its arguments: an argument past the form is a usage error that
|
|
24
|
-
// names it, never dropped. Every error raised before a verb runs names the
|
|
25
|
-
// verb the scan found, in the envelope and on stderr.
|
|
5
|
+
// (distribution ADR decisions 3 and 5). No logic lives here: the verb table,
|
|
6
|
+
// the envelope and the per-verb parse are verbs.mjs's, the read verbs'
|
|
7
|
+
// handlers are read-verbs.mjs's, and this module holds the handlers of the
|
|
8
|
+
// verbs that write (and of mcp), and run(), which dispatches every verb of
|
|
9
|
+
// the one table through both halves. It re-exports what it exported before
|
|
10
|
+
// the split — the table is a CONTRACT that packaging/shells.mjs and the
|
|
11
|
+
// tests read from here — and the MCP server imports none of it: it runs the
|
|
12
|
+
// read runner (the story "The MCP server's entry reaches no install verb
|
|
13
|
+
// through a read-verb runner that shares the CLI's verb table…",
|
|
14
|
+
// PS-DIRECTORY).
|
|
26
15
|
//
|
|
27
16
|
// The gate (distribution ADR decision 6) binds every write verb this bin
|
|
28
17
|
// exposes: the install family through install-harness.mjs's own preview and
|
|
@@ -33,40 +22,15 @@
|
|
|
33
22
|
// declares, in the vault the binding names — so it asks at a terminal and,
|
|
34
23
|
// without one, `--write` is the confirmation. There is no --yes.
|
|
35
24
|
//
|
|
36
|
-
// The
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// branded name is written. bin/ reads no branded variable; the portability
|
|
40
|
-
// suite greps it too.
|
|
41
|
-
//
|
|
42
|
-
// The bin runs ITS OWN copy of the core: sibling scripts resolve from this
|
|
43
|
-
// file's URL, and the plugin root handed to children and to the installer
|
|
44
|
-
// is this package's root — so `npx projectstore doctor` inside a Claude Code
|
|
45
|
-
// session runs the npm copy's doctor over the npm copy's templates, and
|
|
46
|
-
// reports the npm copy's version, rather than the marketplace copy's.
|
|
47
|
-
//
|
|
48
|
-
// The install verbs are imported lazily: hooks never import this module
|
|
49
|
-
// (the suite asserts it), but the MCP server will, and it needs no installer.
|
|
25
|
+
// The install verbs, setup, configure, reconcile and the MCP server are
|
|
26
|
+
// imported lazily, on the run that needs them: hooks never import this module
|
|
27
|
+
// (the suite asserts it), and a verb that runs none of them loads none.
|
|
50
28
|
// Pure node, no external deps.
|
|
51
29
|
|
|
52
30
|
import { spawnSync } from "node:child_process";
|
|
53
|
-
import {
|
|
54
|
-
import { resolve, dirname } from "node:path";
|
|
55
|
-
import { fileURLToPath } from "node:url";
|
|
56
|
-
import { parseArgs } from "node:util";
|
|
31
|
+
import { existsSync } from "node:fs";
|
|
57
32
|
import * as term from "./term.mjs";
|
|
58
|
-
import {
|
|
59
|
-
projectRootDeclared,
|
|
60
|
-
childEnv,
|
|
61
|
-
harnessIds,
|
|
62
|
-
pinPluginRoot,
|
|
63
|
-
invokedAs,
|
|
64
|
-
shellHarnessId,
|
|
65
|
-
CORE_NPX,
|
|
66
|
-
packageCommand,
|
|
67
|
-
loadHarnesses,
|
|
68
|
-
invocation,
|
|
69
|
-
} from "./harness.mjs";
|
|
33
|
+
import { harnessIds, packageCommand } from "./harness.mjs";
|
|
70
34
|
import {
|
|
71
35
|
readConfigAt,
|
|
72
36
|
readOverlayAt,
|
|
@@ -77,21 +41,11 @@ import {
|
|
|
77
41
|
commandForm,
|
|
78
42
|
projectDirRefusal,
|
|
79
43
|
fromTerminalIn,
|
|
80
|
-
doctorFallbackText,
|
|
81
44
|
refreshSessionPointer,
|
|
82
45
|
} from "./lib.mjs";
|
|
83
|
-
import {
|
|
84
|
-
READ_OPERATIONS,
|
|
85
|
-
LINEAGE_KINDS,
|
|
86
|
-
LINEAGE_DEFAULT_DEPTH,
|
|
87
|
-
SEARCH_DEFAULT_LIMIT,
|
|
88
|
-
GRAPH_EDGE_CAP,
|
|
89
|
-
DIRECTIONS,
|
|
90
|
-
} from "./query.mjs";
|
|
91
46
|
// binding.mjs and scaffold.mjs beneath it are write modules imported
|
|
92
47
|
// statically where the install family is lazy: dependency-free, with no side
|
|
93
|
-
// effects at import
|
|
94
|
-
// trip on.
|
|
48
|
+
// effects at import.
|
|
95
49
|
import {
|
|
96
50
|
planBind,
|
|
97
51
|
applyBind,
|
|
@@ -113,1119 +67,26 @@ import {
|
|
|
113
67
|
renderBandPlan,
|
|
114
68
|
SUBCOMMANDS as BAND_SUBCOMMANDS,
|
|
115
69
|
} from "./band.mjs";
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// cwd-by-inference from harness.mjs, which is what an in-process caller with
|
|
137
|
-
// its own cwd would otherwise inherit), then the caller's cwd.
|
|
138
|
-
export function resolveProject({ project = null, env = process.env, cwd = process.cwd() } = {}) {
|
|
139
|
-
if (project) return resolve(cwd, project);
|
|
140
|
-
if (env.PROJECTSTORE_PROJECT_DIR) return resolve(cwd, env.PROJECTSTORE_PROJECT_DIR);
|
|
141
|
-
const declared = projectRootDeclared(env);
|
|
142
|
-
return declared ? resolve(cwd, declared) : resolve(cwd);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// ─── The verb table ────────────────────────────────────────────────────
|
|
146
|
-
//
|
|
147
|
-
// wraps: "script" (spawned), "module" (imported), "new" (code that exists
|
|
148
|
-
// only for the CLI). output: "envelope" (--json wraps), "text". mcp: the MCP
|
|
149
|
-
// tools that mirror the verb (MCP ADR decision 2), empty when none.
|
|
150
|
-
// options: the row's parser — opt()'s arg is the type (false a flag, a
|
|
151
|
-
// placeholder a value), multiple the repetition. usage: the forms the help
|
|
152
|
-
// prints after the verb, verbatim, which also bound its arguments: a literal
|
|
153
|
-
// sub-word (`neighbors`), a <placeholder> (`<phrase…>` takes the rest), or a
|
|
154
|
-
// --name the form requires (its placeholder is the option's value, not an
|
|
155
|
-
// argument). NO_FORMS: the verb takes no argument.
|
|
156
|
-
|
|
157
|
-
export const opt = (name, arg, summary, multiple = false) => Object.freeze({ name, arg, summary, multiple });
|
|
158
|
-
const JSON_OPT = opt("json", false, "the envelope");
|
|
159
|
-
// The options every verb takes, and the only ones that may come before the
|
|
160
|
-
// verb: the scan knows their arity before it knows the verb (--project takes
|
|
161
|
-
// a value, the rest do not). Last in every parser map, so a row cannot
|
|
162
|
-
// retype one. SHORT is their one-letter forms (`-hv`: version wins).
|
|
163
|
-
const GLOBAL_OPTS = Object.freeze([
|
|
164
|
-
opt("project", "<dir>", "the project"),
|
|
165
|
-
JSON_OPT,
|
|
166
|
-
opt("help", false, "the help"),
|
|
167
|
-
opt("version", false, "the package version"),
|
|
168
|
-
]);
|
|
169
|
-
const SHORT = Object.freeze({ help: "h", version: "v" });
|
|
170
|
-
const NO_FORMS = Object.freeze([]);
|
|
171
|
-
const READ_JSON = [JSON_OPT];
|
|
172
|
-
const HARNESS_OPT = opt(
|
|
173
|
-
"harness",
|
|
174
|
-
"<id>",
|
|
175
|
-
"the harness — and, without a terminal, the confirmation (a terminal is asked); there is no --yes",
|
|
176
|
-
true,
|
|
177
|
-
);
|
|
178
|
-
const SURFACE_OPT = opt("surface", "<key>", "one surface and those beneath it", true);
|
|
179
|
-
// The layout move's remedy names this for any copy but the package's own
|
|
180
|
-
// registration (the layout spec, contract 12 as amended 2026-10-03): it makes
|
|
181
|
-
// "no host command" true by construction instead of by recognising the root.
|
|
182
|
-
const NO_REGISTER_OPT = opt(
|
|
183
|
-
"no-register",
|
|
184
|
-
false,
|
|
185
|
-
"leave the plugin registration alone: change only this project's files",
|
|
186
|
-
);
|
|
187
|
-
const VERBOSE_OPT = opt("verbose", false, "every row's reasoning, each step's why and the host's own notes");
|
|
188
|
-
// doctor's --verbose is the same flag in its own words: on a terminal, the
|
|
189
|
-
// report the pipe gets — every note, every instance, each message whole.
|
|
190
|
-
const DOCTOR_VERBOSE_OPT = opt(
|
|
191
|
-
VERBOSE_OPT.name,
|
|
192
|
-
VERBOSE_OPT.arg,
|
|
193
|
-
"on a terminal too: every note, every instance, whole messages",
|
|
194
|
-
);
|
|
195
|
-
const INSTALL_OPTS = [HARNESS_OPT, SURFACE_OPT, NO_REGISTER_OPT, VERBOSE_OPT, JSON_OPT];
|
|
196
|
-
const UNINSTALL_OPTS = [
|
|
197
|
-
HARNESS_OPT,
|
|
198
|
-
SURFACE_OPT,
|
|
199
|
-
opt("global", false, "also remove the harness-global plugin registration"),
|
|
200
|
-
VERBOSE_OPT,
|
|
201
|
-
JSON_OPT,
|
|
202
|
-
];
|
|
203
|
-
|
|
204
|
-
// configure's own --harness: it names the overlay agents.* keys write, and
|
|
205
|
-
// is no confirmation — naming a change is (its spec, contracts 1 and 8).
|
|
206
|
-
const CONFIGURE_OPTS = [
|
|
207
|
-
opt("set", "<key>=<value>", "change one setting; without a terminal, naming it is the confirmation", true),
|
|
208
|
-
opt("harness", "<id>", "the overlay agents.* keys write; other keys ignore it", true),
|
|
209
|
-
opt("dry-run", false, "the plan; nothing is written"),
|
|
210
|
-
];
|
|
211
|
-
|
|
212
|
-
// setup's own options (the setup story, rule 1): seven, beside the globals.
|
|
213
|
-
// `--vault <path>` takes a value here where doctor's `--vault` is a flag —
|
|
214
|
-
// the per-verb parse's own case.
|
|
215
|
-
const SETUP_OPTS = [
|
|
216
|
-
HARNESS_OPT,
|
|
217
|
-
opt(
|
|
218
|
-
"vault",
|
|
219
|
-
"<path>",
|
|
220
|
-
"the vault: an existing folder is bound, a missing or empty one is created (naming it confirms)",
|
|
221
|
-
),
|
|
222
|
-
opt("layout", "<name>", `a new vault's layout (default ${DEFAULT_LAYOUT})`),
|
|
223
|
-
opt("language", "<code>", `a new vault's template language (default ${DEFAULT_LANGUAGE})`),
|
|
224
|
-
opt(
|
|
225
|
-
"channel",
|
|
226
|
-
"<npm|git>",
|
|
227
|
-
"with a git-marketplace copy enabled, without a terminal: npm switches to the npm install, git keeps the git marketplace",
|
|
228
|
-
),
|
|
229
|
-
opt("dry-run", false, "the questions and the plan; nothing is written or fetched"),
|
|
230
|
-
VERBOSE_OPT,
|
|
231
|
-
];
|
|
232
|
-
|
|
233
|
-
export const VERBS = Object.freeze([
|
|
234
|
-
Object.freeze({
|
|
235
|
-
verb: "setup",
|
|
236
|
-
summary: "Set up this project: every harness it uses, and a vault.",
|
|
237
|
-
usage: NO_FORMS,
|
|
238
|
-
module: "./setup.mjs",
|
|
239
|
-
wraps: "new",
|
|
240
|
-
how: "import",
|
|
241
|
-
output: "envelope",
|
|
242
|
-
writes: true,
|
|
243
|
-
requiresBinding: false,
|
|
244
|
-
mcp: Object.freeze([]),
|
|
245
|
-
options: [...SETUP_OPTS, JSON_OPT],
|
|
246
|
-
run: runSetupVerb,
|
|
247
|
-
}),
|
|
248
|
-
Object.freeze({
|
|
249
|
-
verb: "configure",
|
|
250
|
-
summary: "Change this project's settings after setup, behind one plan.",
|
|
251
|
-
usage: NO_FORMS,
|
|
252
|
-
module: "./configure.mjs",
|
|
253
|
-
wraps: "new",
|
|
254
|
-
how: "import",
|
|
255
|
-
output: "envelope",
|
|
256
|
-
writes: true,
|
|
257
|
-
requiresBinding: false,
|
|
258
|
-
mcp: Object.freeze([]),
|
|
259
|
-
options: [...CONFIGURE_OPTS, JSON_OPT],
|
|
260
|
-
run: runConfigureVerb,
|
|
261
|
-
}),
|
|
262
|
-
Object.freeze({
|
|
263
|
-
verb: "doctor",
|
|
264
|
-
summary: "Check the install wiring and the vault's consistency.",
|
|
265
|
-
usage: NO_FORMS,
|
|
266
|
-
module: "./doctor.mjs",
|
|
267
|
-
wraps: "script",
|
|
268
|
-
how: "spawn",
|
|
269
|
-
output: "envelope",
|
|
270
|
-
writes: false,
|
|
271
|
-
requiresBinding: false,
|
|
272
|
-
mcp: Object.freeze(["doctor"]),
|
|
273
|
-
options: [
|
|
274
|
-
opt("install", false, "only the install section"),
|
|
275
|
-
opt("vault", false, "only the vault section"),
|
|
276
|
-
DOCTOR_VERBOSE_OPT,
|
|
277
|
-
JSON_OPT,
|
|
278
|
-
],
|
|
279
|
-
run: runDoctor,
|
|
280
|
-
}),
|
|
281
|
-
Object.freeze({
|
|
282
|
-
verb: "reconcile",
|
|
283
|
-
summary: "Regenerate the derived views (kanban, code map, graph, indexes).",
|
|
284
|
-
usage: NO_FORMS,
|
|
285
|
-
module: "./reconcile.mjs",
|
|
286
|
-
wraps: "module",
|
|
287
|
-
how: "import",
|
|
288
|
-
output: "envelope",
|
|
289
|
-
writes: true,
|
|
290
|
-
requiresBinding: true,
|
|
291
|
-
mcp: Object.freeze([]),
|
|
292
|
-
options: [
|
|
293
|
-
opt(
|
|
294
|
-
"write",
|
|
295
|
-
false,
|
|
296
|
-
"apply the regeneration (asks on a terminal; --only names what is written and confirms headless)",
|
|
297
|
-
),
|
|
298
|
-
opt("only", "<target>", "one derived view"),
|
|
299
|
-
JSON_OPT,
|
|
300
|
-
],
|
|
301
|
-
run: runReconcile,
|
|
302
|
-
}),
|
|
303
|
-
Object.freeze({
|
|
304
|
-
verb: "plan",
|
|
305
|
-
summary: "Show what install would write for a harness, without writing.",
|
|
306
|
-
usage: NO_FORMS,
|
|
307
|
-
module: "./install-harness.mjs",
|
|
308
|
-
wraps: "module",
|
|
309
|
-
how: "import",
|
|
310
|
-
output: "envelope",
|
|
311
|
-
writes: false,
|
|
312
|
-
requiresBinding: false,
|
|
313
|
-
mcp: Object.freeze([]),
|
|
314
|
-
options: INSTALL_OPTS,
|
|
315
|
-
run: runInstallVerb,
|
|
316
|
-
}),
|
|
317
|
-
Object.freeze({
|
|
318
|
-
verb: "install",
|
|
319
|
-
summary: "Install projectstore's surfaces for a harness, behind a preview.",
|
|
320
|
-
usage: NO_FORMS,
|
|
321
|
-
module: "./install-harness.mjs",
|
|
322
|
-
wraps: "module",
|
|
323
|
-
how: "import",
|
|
324
|
-
output: "envelope",
|
|
325
|
-
writes: true,
|
|
326
|
-
requiresBinding: false,
|
|
327
|
-
mcp: Object.freeze([]),
|
|
328
|
-
options: INSTALL_OPTS,
|
|
329
|
-
run: runInstallVerb,
|
|
330
|
-
}),
|
|
331
|
-
Object.freeze({
|
|
332
|
-
verb: "uninstall",
|
|
333
|
-
summary: "Remove what install wrote, and only that.",
|
|
334
|
-
usage: NO_FORMS,
|
|
335
|
-
module: "./install-harness.mjs",
|
|
336
|
-
wraps: "module",
|
|
337
|
-
how: "import",
|
|
338
|
-
output: "envelope",
|
|
339
|
-
writes: true,
|
|
340
|
-
requiresBinding: false,
|
|
341
|
-
mcp: Object.freeze([]),
|
|
342
|
-
options: UNINSTALL_OPTS,
|
|
343
|
-
run: runInstallVerb,
|
|
344
|
-
}),
|
|
345
|
-
Object.freeze({
|
|
346
|
-
verb: "upgrade",
|
|
347
|
-
summary: "After an update, refresh what install wrote and leave the rest.",
|
|
348
|
-
usage: NO_FORMS,
|
|
349
|
-
module: "./install-harness.mjs",
|
|
350
|
-
wraps: "module",
|
|
351
|
-
how: "import",
|
|
352
|
-
output: "envelope",
|
|
353
|
-
writes: true,
|
|
354
|
-
requiresBinding: false,
|
|
355
|
-
mcp: Object.freeze([]),
|
|
356
|
-
options: INSTALL_OPTS,
|
|
357
|
-
run: runInstallVerb,
|
|
358
|
-
}),
|
|
359
|
-
Object.freeze({
|
|
360
|
-
verb: "status",
|
|
361
|
-
summary: "What is bound, what is in progress, and whether views are fresh.",
|
|
362
|
-
usage: NO_FORMS,
|
|
363
|
-
module: "./query.mjs",
|
|
364
|
-
wraps: "new",
|
|
365
|
-
how: "import",
|
|
366
|
-
output: "envelope",
|
|
367
|
-
writes: false,
|
|
368
|
-
requiresBinding: false,
|
|
369
|
-
mcp: Object.freeze(["status"]),
|
|
370
|
-
options: READ_JSON,
|
|
371
|
-
run: runRead("status"),
|
|
372
|
-
}),
|
|
373
|
-
Object.freeze({
|
|
374
|
-
verb: "orientation",
|
|
375
|
-
summary: "The SessionStart skeleton and the facts behind it.",
|
|
376
|
-
usage: NO_FORMS,
|
|
377
|
-
module: "./query.mjs",
|
|
378
|
-
wraps: "module",
|
|
379
|
-
how: "import",
|
|
380
|
-
output: "envelope",
|
|
381
|
-
writes: false,
|
|
382
|
-
requiresBinding: true,
|
|
383
|
-
mcp: Object.freeze(["orientation"]),
|
|
384
|
-
options: READ_JSON,
|
|
385
|
-
run: runRead("orientation"),
|
|
386
|
-
}),
|
|
387
|
-
Object.freeze({
|
|
388
|
-
verb: "search",
|
|
389
|
-
summary: "Find a phrase in the vault's artifacts, literal and bounded.",
|
|
390
|
-
usage: Object.freeze(["<phrase…>"]),
|
|
391
|
-
module: "./query.mjs",
|
|
392
|
-
wraps: "new",
|
|
393
|
-
how: "import",
|
|
394
|
-
output: "envelope",
|
|
395
|
-
writes: false,
|
|
396
|
-
requiresBinding: true,
|
|
397
|
-
mcp: Object.freeze(["search"]),
|
|
398
|
-
options: [
|
|
399
|
-
opt("kind", "<type>", "only artifacts of this kind", true),
|
|
400
|
-
opt("status", "<status>", "only artifacts in this status"),
|
|
401
|
-
opt("limit", "<n>", `at most n matches (default ${SEARCH_DEFAULT_LIMIT}, hard cap 100)`),
|
|
402
|
-
opt("include-derived", false, "search the derived views too"),
|
|
403
|
-
opt("case-sensitive", false, "match case"),
|
|
404
|
-
JSON_OPT,
|
|
405
|
-
],
|
|
406
|
-
run: runRead("search"),
|
|
407
|
-
}),
|
|
408
|
-
Object.freeze({
|
|
409
|
-
verb: "show",
|
|
410
|
-
summary: "One artifact's frontmatter; its body or a section on request.",
|
|
411
|
-
usage: Object.freeze(["<path>"]),
|
|
412
|
-
module: "./query.mjs",
|
|
413
|
-
wraps: "module",
|
|
414
|
-
how: "import",
|
|
415
|
-
output: "envelope",
|
|
416
|
-
writes: false,
|
|
417
|
-
requiresBinding: true,
|
|
418
|
-
mcp: Object.freeze(["get_artifact"]),
|
|
419
|
-
options: [
|
|
420
|
-
opt("body", false, "include the body"),
|
|
421
|
-
opt("section", "<id>", "one section by its registry id (description, acceptance, …)"),
|
|
422
|
-
JSON_OPT,
|
|
423
|
-
],
|
|
424
|
-
run: runRead("show"),
|
|
425
|
-
}),
|
|
426
|
-
Object.freeze({
|
|
427
|
-
verb: "graph",
|
|
428
|
-
summary: "The live link graph by vault path.",
|
|
429
|
-
usage: Object.freeze(["neighbors <path>", "lineage <path>"]),
|
|
430
|
-
module: "./query.mjs",
|
|
431
|
-
wraps: "new",
|
|
432
|
-
how: "import",
|
|
433
|
-
output: "envelope",
|
|
434
|
-
writes: false,
|
|
435
|
-
requiresBinding: true,
|
|
436
|
-
mcp: Object.freeze(["neighbors", "lineage"]),
|
|
437
|
-
options: [
|
|
438
|
-
opt("kind", "<edge-kind>", "only edges of this kind (lineage: one of its four)", true),
|
|
439
|
-
opt("direction", DIRECTIONS.join("|"), "neighbors: which edges"),
|
|
440
|
-
opt("depth", "<n>", `lineage: how far (default ${LINEAGE_DEFAULT_DEPTH})`),
|
|
441
|
-
opt("limit", "<n>", `neighbors: cap per direction (at most ${GRAPH_EDGE_CAP})`),
|
|
442
|
-
JSON_OPT,
|
|
443
|
-
],
|
|
444
|
-
run: runGraph,
|
|
445
|
-
}),
|
|
446
|
-
Object.freeze({
|
|
447
|
-
verb: "codemap",
|
|
448
|
-
summary: "The code an epic or artifact maps to, or a path's artifacts.",
|
|
449
|
-
usage: Object.freeze(["--for <selector>"]),
|
|
450
|
-
module: "./query.mjs",
|
|
451
|
-
wraps: "new",
|
|
452
|
-
how: "import",
|
|
453
|
-
output: "envelope",
|
|
454
|
-
writes: false,
|
|
455
|
-
requiresBinding: true,
|
|
456
|
-
mcp: Object.freeze(["code_refs"]),
|
|
457
|
-
options: [
|
|
458
|
-
opt("for", "<selector>", "an epic id, an artifact, or a repo path"),
|
|
459
|
-
opt("reverse", false, "read the selector as a path even if it names an artifact"),
|
|
460
|
-
JSON_OPT,
|
|
461
|
-
],
|
|
462
|
-
run: runCodemap,
|
|
463
|
-
}),
|
|
464
|
-
Object.freeze({
|
|
465
|
-
verb: "agents",
|
|
466
|
-
summary: "The harness overlay's agents block: the model each agent runs.",
|
|
467
|
-
usage: Object.freeze(["model <name>", "show", "configure"]),
|
|
468
|
-
module: "./lib.mjs",
|
|
469
|
-
wraps: "new",
|
|
470
|
-
how: "import",
|
|
471
|
-
output: "envelope",
|
|
472
|
-
writes: true,
|
|
473
|
-
requiresBinding: false,
|
|
474
|
-
mcp: Object.freeze([]),
|
|
475
|
-
options: [
|
|
476
|
-
opt(
|
|
477
|
-
"harness",
|
|
478
|
-
"<id>",
|
|
479
|
-
"configure: the overlay to write — and, non-interactively, the confirmation; there is no --yes",
|
|
480
|
-
true,
|
|
481
|
-
),
|
|
482
|
-
opt(
|
|
483
|
-
"default",
|
|
484
|
-
"<model>",
|
|
485
|
-
"configure: agents.default.model (pins the clerk to sonnet unless --agent clerk=… says otherwise; an empty model clears it)",
|
|
486
|
-
),
|
|
487
|
-
opt("agent", "<name>=<model>", "configure: agents.per_agent.<name>.model (an empty model removes the key)", true),
|
|
488
|
-
opt("reset", false, "configure: empty the agents block first; --default and --agent given with it apply on top"),
|
|
489
|
-
JSON_OPT,
|
|
490
|
-
],
|
|
491
|
-
run: runAgents,
|
|
492
|
-
}),
|
|
493
|
-
Object.freeze({
|
|
494
|
-
verb: "band",
|
|
495
|
-
summary: "The band above the prompt: switch it, pin an epic, ask why.",
|
|
496
|
-
usage: Object.freeze(["on", "off", "status", "epic <ID>"]),
|
|
497
|
-
module: "./band.mjs",
|
|
498
|
-
wraps: "new",
|
|
499
|
-
how: "import",
|
|
500
|
-
output: "envelope",
|
|
501
|
-
writes: true,
|
|
502
|
-
requiresBinding: true,
|
|
503
|
-
mcp: Object.freeze([]),
|
|
504
|
-
options: [JSON_OPT],
|
|
505
|
-
run: runBand,
|
|
506
|
-
}),
|
|
507
|
-
Object.freeze({
|
|
508
|
-
verb: "session",
|
|
509
|
-
summary: "Refresh this session's current story from its file.",
|
|
510
|
-
usage: Object.freeze(["refresh <path>"]),
|
|
511
|
-
module: "./lib.mjs",
|
|
512
|
-
wraps: "new",
|
|
513
|
-
how: "import",
|
|
514
|
-
output: "envelope",
|
|
515
|
-
writes: true,
|
|
516
|
-
requiresBinding: true,
|
|
517
|
-
mcp: Object.freeze([]),
|
|
518
|
-
options: [JSON_OPT],
|
|
519
|
-
run: runSession,
|
|
520
|
-
}),
|
|
521
|
-
Object.freeze({
|
|
522
|
-
verb: "bind",
|
|
523
|
-
summary: "Bind this project to an existing vault; naming it confirms.",
|
|
524
|
-
usage: Object.freeze(["<vault>"]),
|
|
525
|
-
module: "./binding.mjs",
|
|
526
|
-
wraps: "new",
|
|
527
|
-
how: "import",
|
|
528
|
-
output: "envelope",
|
|
529
|
-
writes: true,
|
|
530
|
-
requiresBinding: false,
|
|
531
|
-
mcp: Object.freeze([]),
|
|
532
|
-
options: [
|
|
533
|
-
opt("layout", "<name>", `the layout (default ${DEFAULT_LAYOUT})`),
|
|
534
|
-
opt("language", "<code>", `the template language (default ${DEFAULT_LANGUAGE})`),
|
|
535
|
-
opt("rebind", false, "point an already bound project at another vault; every other setting is kept"),
|
|
536
|
-
JSON_OPT,
|
|
537
|
-
],
|
|
538
|
-
run: runBind(false),
|
|
539
|
-
}),
|
|
540
|
-
Object.freeze({
|
|
541
|
-
verb: "init",
|
|
542
|
-
summary: "Create a vault (folder, git repository, READMEs) and bind to it.",
|
|
543
|
-
usage: Object.freeze(["<vault>"]),
|
|
544
|
-
module: "./binding.mjs",
|
|
545
|
-
wraps: "new",
|
|
546
|
-
how: "import",
|
|
547
|
-
output: "envelope",
|
|
548
|
-
writes: true,
|
|
549
|
-
requiresBinding: false,
|
|
550
|
-
mcp: Object.freeze([]),
|
|
551
|
-
options: [
|
|
552
|
-
opt("layout", "<name>", `the layout (default ${DEFAULT_LAYOUT})`),
|
|
553
|
-
opt("language", "<code>", `the template language (default ${DEFAULT_LANGUAGE})`),
|
|
554
|
-
opt(
|
|
555
|
-
"rebind",
|
|
556
|
-
false,
|
|
557
|
-
"an already bound project: create the new vault and point the project at it; every other setting is kept",
|
|
558
|
-
),
|
|
559
|
-
JSON_OPT,
|
|
560
|
-
],
|
|
561
|
-
run: runBind(true),
|
|
562
|
-
}),
|
|
563
|
-
Object.freeze({
|
|
564
|
-
verb: "scaffold",
|
|
565
|
-
summary: "Create the vault's missing folders and READMEs; rewrite nothing.",
|
|
566
|
-
usage: NO_FORMS,
|
|
567
|
-
module: "./scaffold.mjs",
|
|
568
|
-
wraps: "new",
|
|
569
|
-
how: "import",
|
|
570
|
-
output: "envelope",
|
|
571
|
-
writes: true,
|
|
572
|
-
requiresBinding: true,
|
|
573
|
-
mcp: Object.freeze([]),
|
|
574
|
-
options: [
|
|
575
|
-
opt("write", false, "create them (asks at a terminal; without one, --write is the confirmation)"),
|
|
576
|
-
JSON_OPT,
|
|
577
|
-
],
|
|
578
|
-
run: runScaffold,
|
|
579
|
-
}),
|
|
580
|
-
Object.freeze({
|
|
581
|
-
verb: "mcp",
|
|
582
|
-
summary: "Serve the read tools over MCP (stdio) for the project you name.",
|
|
583
|
-
usage: NO_FORMS,
|
|
584
|
-
module: "./mcp.mjs",
|
|
585
|
-
wraps: "new",
|
|
586
|
-
how: "import",
|
|
587
|
-
output: "text",
|
|
588
|
-
writes: false,
|
|
589
|
-
requiresBinding: false,
|
|
590
|
-
mcp: Object.freeze([]),
|
|
591
|
-
options: [],
|
|
592
|
-
run: runMcp,
|
|
593
|
-
}),
|
|
594
|
-
Object.freeze({
|
|
595
|
-
verb: "version",
|
|
596
|
-
summary: "Print the package version (also --version).",
|
|
597
|
-
usage: NO_FORMS,
|
|
598
|
-
module: null,
|
|
599
|
-
wraps: "new",
|
|
600
|
-
how: "import",
|
|
601
|
-
output: "envelope",
|
|
602
|
-
writes: false,
|
|
603
|
-
requiresBinding: false,
|
|
604
|
-
mcp: Object.freeze([]),
|
|
605
|
-
options: [JSON_OPT],
|
|
606
|
-
run: runVersion,
|
|
607
|
-
}),
|
|
608
|
-
]);
|
|
609
|
-
|
|
610
|
-
// Verbs the story names that land with a later slice — listed so
|
|
611
|
-
// `projectstore <verb>` says where instead of "unknown verb". Empty since
|
|
612
|
-
// A7: every verb the CLI story names has landed. The seam stays for the next
|
|
613
|
-
// story that adds a verb in slices.
|
|
614
|
-
export const PLANNED_VERBS = Object.freeze([]);
|
|
615
|
-
|
|
616
|
-
// The verbs as a person meets them (presentation spec contract 10): starting,
|
|
617
|
-
// reading the vault, keeping it consistent. Every verb not listed here is
|
|
618
|
-
// ADVANCED, one line of names in table order — so a new row is never hidden
|
|
619
|
-
// by this table, and init and scaffold, which the contract names in no group,
|
|
620
|
-
// are there too. A group lists only the verbs this run's table has. With
|
|
621
|
-
// setup, START is setup and uninstall, and install and bind are ADVANCED's
|
|
622
|
-
// (contract 10; the setup story, rule 1).
|
|
623
|
-
const HELP_GROUPS = [
|
|
624
|
-
["START", ["setup", "configure", "uninstall"]],
|
|
625
|
-
["VAULT", ["status", "search", "show", "graph", "codemap"]],
|
|
626
|
-
["CHECK AND REPAIR", ["doctor", "reconcile", "upgrade"]],
|
|
627
|
-
];
|
|
628
|
-
// The column a group's summaries start at: two spaces, the verb in twelve,
|
|
629
|
-
// one space. A summary fits 80 columns from there, so each is at most 65
|
|
630
|
-
// characters, in ASCII (tests/presentation.test.mjs, criterion 5, holds every
|
|
631
|
-
// row to it).
|
|
632
|
-
const VERB_COLUMN = 12;
|
|
633
|
-
export const SUMMARY_MAX = 80 - (2 + VERB_COLUMN + 1);
|
|
634
|
-
|
|
635
|
-
// Examples per verb. They are what to run next, so they are the core's form
|
|
636
|
-
// through a shell too (the story "User-facing text says where to run and
|
|
637
|
-
// names no project path and no shell", rule 6): `{cmd}` is the core through
|
|
638
|
-
// npx, and `{h}` the harness argument — ` --harness <id>` from the core, and
|
|
639
|
-
// through a shell ` --harness <the shell's own harness>` (shellHarnessId),
|
|
640
|
-
// never a session's identity. `{fixed}` is that argument through a shell
|
|
641
|
-
// only: the shell inserts --harness for every verb that declares it
|
|
642
|
-
// (packaging/shells.mjs, HARNESS_VERBS), so an example the core runs bare —
|
|
643
|
-
// setup, which asks; agents show, which reads the session's overlay — names
|
|
644
|
-
// the shell's harness through it and stays bare from the core. A `{core}`
|
|
645
|
-
// example is the core's alone: a shell's help leaves it out, since it says
|
|
646
|
-
// what the shell's own harness does not (every harness, the way back from a
|
|
647
|
-
// switch). `{switch.surface}`,
|
|
648
|
-
// `{switch.host}` and `{switch.doctor}` name the harness whose project can
|
|
649
|
-
// switch channels — the one with a host plugin registration — from its
|
|
650
|
-
// manifest: its surface key, its display name and its doctor form
|
|
651
|
-
// (switchHarness, below); an example naming them is left out when no
|
|
652
|
-
// manifest has one.
|
|
653
|
-
const EXAMPLES = {
|
|
654
|
-
setup: [
|
|
655
|
-
"{cmd} setup{fixed}",
|
|
656
|
-
"{cmd} setup{fixed} --dry-run # the questions and the plan; nothing is written or fetched",
|
|
657
|
-
"{cmd} setup{h} --vault ~/vaults/my-project # without a terminal: --harness confirms, --vault names the vault",
|
|
658
|
-
],
|
|
659
|
-
configure: [
|
|
660
|
-
"{cmd} configure # at a terminal: the settings, then what to change",
|
|
661
|
-
"{cmd} configure --set statusline=on",
|
|
662
|
-
"{cmd} configure --set agents.critic=fable{h} # an agent's model, in that harness's overlay",
|
|
663
|
-
"{cmd} configure --json # the settings, for scripts and agents",
|
|
664
|
-
],
|
|
665
|
-
install: ["{cmd} install{h}", "{cmd} plan{h} # the same plan; nothing is written"],
|
|
666
|
-
upgrade: [
|
|
667
|
-
"{cmd}@<version> upgrade{h} # the version you name is the version that runs",
|
|
668
|
-
"{cmd} upgrade{h} --verbose",
|
|
669
|
-
],
|
|
670
|
-
uninstall: [
|
|
671
|
-
"{core}{cmd} uninstall # every harness this project uses",
|
|
672
|
-
"{cmd} uninstall{h}",
|
|
673
|
-
"{cmd} uninstall{h} --surface statusline",
|
|
674
|
-
"{core}{cmd} uninstall{h} --surface {switch.surface} # back to the git marketplace after a switch, then restart {switch.host} and run {switch.doctor} in it",
|
|
675
|
-
],
|
|
676
|
-
plan: ["{cmd} plan{h}", "{cmd} plan{h} --json # one envelope, for scripts and agents"],
|
|
677
|
-
status: ["{cmd} status", "{cmd} status --json"],
|
|
678
|
-
orientation: ["{cmd} orientation --json"],
|
|
679
|
-
search: ['{cmd} search "entry rule" --kind spec', "{cmd} search queue --status accepted --limit 5"],
|
|
680
|
-
show: ["{cmd} show adr/README.md", "{cmd} show epics/PS-CORE/epic.md --section acceptance"],
|
|
681
|
-
graph: ["{cmd} graph neighbors adr/README.md --direction out", "{cmd} graph lineage epics/PS-CORE/epic.md --depth 2"],
|
|
682
|
-
codemap: ["{cmd} codemap --for PS-CORE", "{cmd} codemap --for scripts/lib.mjs --reverse"],
|
|
683
|
-
doctor: ["{cmd} doctor", "{cmd} doctor --install --json"],
|
|
684
|
-
reconcile: ["{cmd} reconcile # what would change; nothing is written", "{cmd} reconcile --write"],
|
|
685
|
-
bind: ["{cmd} bind ~/vaults/my-project", "{cmd} bind ~/vaults/other --rebind"],
|
|
686
|
-
init: ["{cmd} init ~/vaults/new-project --language ru"],
|
|
687
|
-
scaffold: ["{cmd} scaffold # what would be created; nothing is written", "{cmd} scaffold --write"],
|
|
688
|
-
agents: ["{cmd} agents show{fixed}", "{cmd} agents configure{h} --default opus --agent clerk=sonnet"],
|
|
689
|
-
band: [
|
|
690
|
-
"{cmd} band status",
|
|
691
|
-
"{cmd} band off",
|
|
692
|
-
"{cmd} band epic PS-CORE # a session with no epic of its own shows this one",
|
|
693
|
-
],
|
|
694
|
-
session: ["{cmd} session refresh epics/PS-CORE/stories/story-x.md --json # after a write no hook saw"],
|
|
695
|
-
mcp: ['{cmd} mcp --project "$PWD"'],
|
|
696
|
-
};
|
|
697
|
-
|
|
698
|
-
// How this run was invoked is invokedAs (harness.mjs), the one spelling of
|
|
699
|
-
// the binary that ran: the help's first line, its Usage lines, the
|
|
700
|
-
// fixed-harness line and the usage errors. What a person runs next is the
|
|
701
|
-
// core's (CORE_NPX, packageCommand), as status's and bind's Next lines are.
|
|
702
|
-
|
|
703
|
-
// The harness whose project can switch channels (the converge story's rule
|
|
704
|
-
// 4): the one whose manifest declares a host plugin registration. Its surface
|
|
705
|
-
// key, display name and doctor form, from the manifest — `invocation()` in
|
|
706
|
-
// harness.mjs, never a literal; null when no manifest has one.
|
|
707
|
-
function switchHarness() {
|
|
708
|
-
for (const m of loadHarnesses().values()) {
|
|
709
|
-
const row = Object.entries(m.surfaces || {}).find(
|
|
710
|
-
([k, s]) => !k.startsWith("_") && s?.format === "host-plugin-registration",
|
|
711
|
-
);
|
|
712
|
-
if (row)
|
|
713
|
-
return {
|
|
714
|
-
"switch.surface": row[0],
|
|
715
|
-
"switch.host": m.display_name,
|
|
716
|
-
"switch.doctor": invocation(m, "doctor", { args: "--fix" }),
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
return null;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// An aligned options block: each option's form in a column at most 28 wide,
|
|
723
|
-
// its description beside it — or, past that column, on the next line under
|
|
724
|
-
// it. On a live terminal a description wraps with its column as the hanging
|
|
725
|
-
// indent; anywhere else it stays one line, whole, for a reader that greps.
|
|
726
|
-
function optionLines(options, c = term.PLAIN) {
|
|
727
|
-
const rows = options.map((o) => [
|
|
728
|
-
term.glyphText(c, `--${o.name}${o.arg ? " " + o.arg : ""}`),
|
|
729
|
-
term.glyphText(c, `${o.summary}${o.multiple ? " (repeatable)" : ""}`),
|
|
730
|
-
]);
|
|
731
|
-
const w = Math.min(28, Math.max(0, ...rows.map(([l]) => l.length)));
|
|
732
|
-
const pad = " ".repeat(2 + w + 2);
|
|
733
|
-
const text = (r) => term.wrap(r, c.live ? c.width : 0, pad);
|
|
734
|
-
return rows.map(([l, r]) => (l.length > w ? ` ${l}\n${pad}${text(r)}` : ` ${l.padEnd(w)} ${text(r)}`));
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
const EXIT_CODES = "0 ok, 1 findings or a refusal, 2 usage, 3 not bound, 130 cancelled";
|
|
738
|
-
// The facts that close a help screen (`Harnesses`, `Exit codes`): the key in
|
|
739
|
-
// the explanation role, its value at one column for both.
|
|
740
|
-
const FACT_KEY = "Exit codes".length;
|
|
741
|
-
const fact = (c, key, value) =>
|
|
742
|
-
`${term.painter(c)("explanation", key)}${" ".repeat(FACT_KEY - key.length + 2)}${value}`;
|
|
743
|
-
|
|
744
|
-
// What the bin is, in the help's badge: one line with the name and version,
|
|
745
|
-
// short enough that a prerelease version (0.30.0-rc.12) still fits 80 columns.
|
|
746
|
-
const PURPOSE = "project memory for coding agents, in markdown";
|
|
747
|
-
|
|
748
|
-
// What a verb's help says beneath its Usage when the summary has no room for
|
|
749
|
-
// it: here, how mcp is told its project.
|
|
750
|
-
const NOTES = {
|
|
751
|
-
mcp: "It serves the project --project names, else PROJECTSTORE_PROJECT_DIR or the host session's project; never the directory it was started in.",
|
|
752
|
-
};
|
|
753
|
-
|
|
754
|
-
// The global options, as every verb takes them.
|
|
755
|
-
const EVERY_VERB = Object.freeze([
|
|
756
|
-
opt("project", "<dir>", "the project (default: the session's, else this directory)"),
|
|
757
|
-
opt("json", false, "the result as one JSON envelope, for scripts and agents"),
|
|
758
|
-
opt("version", false, "the package version"),
|
|
759
|
-
]);
|
|
760
|
-
|
|
761
|
-
// Top-level help (presentation spec contract 10): the badge — name, version,
|
|
762
|
-
// purpose; Usage; the named groups, each verb in the action role and its
|
|
763
|
-
// summary on one line; ADVANCED, one line of the remaining verbs; the options
|
|
764
|
-
// every verb takes; examples, each command alone on its line (contract 6);
|
|
765
|
-
// the tips, whose plan command stands alone too; the harnesses and the exit
|
|
766
|
-
// codes. `verbs` is the table this run answers for — VERBS, or a test's own.
|
|
767
|
-
// `caps` is the writing stream's (term.mjs): painted at a terminal, plain on a
|
|
768
|
-
// pipe, and every line of it fits 80 columns either way.
|
|
769
|
-
export function usage(env = process.env, verbs = VERBS, { caps: c = term.PLAIN, version = packageVersion() } = {}) {
|
|
770
|
-
const { shell, cmd } = invokedAs(env);
|
|
771
|
-
const paint = term.painter(c);
|
|
772
|
-
const width = c.live ? c.width : 0;
|
|
773
|
-
const prose = (text) => " " + term.wrap(term.glyphText(c, text), width, " ");
|
|
774
|
-
const lines = [
|
|
775
|
-
term.badge(c, "projectstore", [version, PURPOSE]),
|
|
776
|
-
"",
|
|
777
|
-
term.heading(c, "Usage"),
|
|
778
|
-
...term.rows(c, [
|
|
779
|
-
{ target: `${cmd} <verb> [options]`, note: "run it inside your code project" },
|
|
780
|
-
{ target: `${cmd} <verb> --help`, note: "one verb's options and examples" },
|
|
781
|
-
]),
|
|
782
|
-
];
|
|
783
|
-
const listed = new Set(HELP_GROUPS.flatMap(([, names]) => names));
|
|
784
|
-
for (const [title, names] of HELP_GROUPS) {
|
|
785
|
-
const rows = names.map((n) => verbs.find((v) => v.verb === n)).filter(Boolean);
|
|
786
|
-
if (!rows.length) continue;
|
|
787
|
-
lines.push("", term.heading(c, title));
|
|
788
|
-
for (const v of rows) {
|
|
789
|
-
const lead = ` ${paint("action", v.verb)}${" ".repeat(Math.max(0, VERB_COLUMN - v.verb.length))} `;
|
|
790
|
-
lines.push(lead + term.wrap(term.glyphText(c, v.summary), width, " ".repeat(term.plain(lead).length)));
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
// ADVANCED is one line of verbs, broken at 80 columns in every mode: with
|
|
794
|
-
// install and bind among them (contract 10, once setup exists) it no longer
|
|
795
|
-
// fits one row, and a pipe gets the same lines a terminal does.
|
|
796
|
-
const advanced = verbs.filter((v) => !listed.has(v.verb));
|
|
797
|
-
if (advanced.length)
|
|
798
|
-
lines.push(
|
|
799
|
-
"",
|
|
800
|
-
term.heading(c, "ADVANCED"),
|
|
801
|
-
" " + term.wrap(advanced.map((v) => paint("action", v.verb)).join(` ${term.icon(c, "dot")} `), 80, " "),
|
|
802
|
-
);
|
|
803
|
-
if (PLANNED_VERBS.length) lines.push("", `Planned: ${PLANNED_VERBS.map((v) => `${v.verb} (${v.lands})`).join(", ")}`);
|
|
804
|
-
// A command is offered only for a verb this table has. The examples and the
|
|
805
|
-
// tips' plan are what to run next: the core's form, through a shell with
|
|
806
|
-
// the shell's own harness (rule 6 of the story "User-facing text says
|
|
807
|
-
// where to run…"); setup names it only through a shell, as EXAMPLES'
|
|
808
|
-
// `{fixed}` does.
|
|
809
|
-
const h = ` --harness ${(shell && shellHarnessId(env)) || "<id>"}`;
|
|
810
|
-
const has = (verb) => verbs.some((v) => v.verb === verb);
|
|
811
|
-
const examples = [
|
|
812
|
-
["setup", `${CORE_NPX} setup${shell ? h : ""}`],
|
|
813
|
-
["status", `${CORE_NPX} status`],
|
|
814
|
-
["doctor", `${CORE_NPX} doctor`],
|
|
815
|
-
]
|
|
816
|
-
.filter(([v]) => has(v))
|
|
817
|
-
.map(([, x]) => x);
|
|
818
|
-
lines.push("", term.heading(c, "Options"), ...optionLines(EVERY_VERB, c));
|
|
819
|
-
if (examples.length) lines.push("", term.heading(c, "Examples"), ...term.commandBlock(c, examples));
|
|
820
|
-
lines.push(
|
|
821
|
-
"",
|
|
822
|
-
term.heading(c, "Tips"),
|
|
823
|
-
prose("setup, configure, install, upgrade and uninstall show a plan, then ask."),
|
|
824
|
-
prose(
|
|
825
|
-
`Without a terminal, ${shell ? "the verb is its own" : "--harness is the"} confirmation ${term.icon(c, "dash")} there is no --yes.`,
|
|
826
|
-
),
|
|
827
|
-
prose("Unattended in a terminal: pass --json, set CI=1, or give stdin </dev/null."),
|
|
828
|
-
prose("--verbose on those verbs adds every row's reasoning and the host's notes."),
|
|
829
|
-
);
|
|
830
|
-
if (has("plan"))
|
|
831
|
-
lines.push(
|
|
832
|
-
prose("plan prints the same plan and writes nothing:"),
|
|
833
|
-
...term.commandBlock(c, [`${CORE_NPX} plan${h}`]),
|
|
834
|
-
);
|
|
835
|
-
lines.push("", fact(c, "Harnesses", harnessIds().join(", ")), fact(c, "Exit codes", EXIT_CODES));
|
|
836
|
-
return lines.join("\n");
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// One verb: what it does, how to call it, every option, examples. The Usage
|
|
840
|
-
// lines are the row's own forms, the same declaration that bounds its
|
|
841
|
-
// arguments — never parsed out of the summary, so help and the parser cannot
|
|
842
|
-
// disagree about what a verb takes. Its first line is the summary the
|
|
843
|
-
// top-level help lists, after the command; `caps` as usage()'s.
|
|
844
|
-
export function verbHelp(row, env = process.env, { caps: c = term.PLAIN } = {}) {
|
|
845
|
-
const { shell, cmd } = invokedAs(env);
|
|
846
|
-
const paint = term.painter(c);
|
|
847
|
-
const fixed = Boolean(shell) && row.options.some((o) => o.name === "harness");
|
|
848
|
-
const opts = row.options.filter((o) => !(fixed && o.name === "harness"));
|
|
849
|
-
const prose = (text) => " " + term.wrap(term.glyphText(c, text), c.live ? c.width : 0, " ");
|
|
850
|
-
const head = `${paint("emphasis", `${cmd} ${row.verb}`)} ${term.icon(c, "dash")} ${term.glyphText(c, row.summary)}`;
|
|
851
|
-
const lines = [term.wrap(head, c.live ? c.width : 0, " "), "", term.heading(c, "Usage")];
|
|
852
|
-
for (const f of row.usage.length ? row.usage : [""])
|
|
853
|
-
lines.push(` ${cmd} ${row.verb}${f ? " " + term.glyphText(c, f) : ""}${opts.length ? " [options]" : ""}`);
|
|
854
|
-
if (NOTES[row.verb]) lines.push("", prose(NOTES[row.verb]));
|
|
855
|
-
if (opts.length) lines.push("", term.heading(c, "Options"), ...optionLines(opts, c));
|
|
856
|
-
if (fixed)
|
|
857
|
-
lines.push(
|
|
858
|
-
"",
|
|
859
|
-
term.wrap(
|
|
860
|
-
row.verb === "configure"
|
|
861
|
-
? `${shell} names the harness itself: its overlay is the one agents.* keys write.`
|
|
862
|
-
: `${shell} names the harness itself: without a terminal, the verb is its own confirmation.`,
|
|
863
|
-
c.live ? c.width : 0,
|
|
864
|
-
" ",
|
|
865
|
-
),
|
|
866
|
-
);
|
|
867
|
-
else if (row.options.some((o) => o.name === "harness")) lines.push("", fact(c, "Harnesses", harnessIds().join(", ")));
|
|
868
|
-
const sw = switchHarness();
|
|
869
|
-
const ex = (EXAMPLES[row.verb] || [])
|
|
870
|
-
.filter((e) => !(fixed && e.startsWith("{core}")) && (sw || !e.includes("{switch.")))
|
|
871
|
-
.map((e) => e.replace(/^\{core\}/, "").replace(/\{(switch\.\w+)\}/g, (_, k) => sw[k]));
|
|
872
|
-
// What to run next is the core's form: through a shell, its own harness.
|
|
873
|
-
const h = ` --harness ${(fixed && shellHarnessId(env)) || "<id>"}`;
|
|
874
|
-
// Each example command stands alone on its line, after a blank one
|
|
875
|
-
// (contract 6): nothing after it, so a paste takes exactly the command — a
|
|
876
|
-
// trailing `# note` broke a paste into a shell that reads no comments. A
|
|
877
|
-
// note leads its command, a blank line between them, as every other
|
|
878
|
-
// introduced command does (DONE's tip, doctor's fix, bind's Next);
|
|
879
|
-
// commands with no note share one block.
|
|
880
|
-
const blocks = [];
|
|
881
|
-
const render = (e) =>
|
|
882
|
-
e
|
|
883
|
-
.split("{cmd}")
|
|
884
|
-
.join(CORE_NPX)
|
|
885
|
-
.split("{h}")
|
|
886
|
-
.join(h)
|
|
887
|
-
.split("{fixed}")
|
|
888
|
-
.join(fixed ? h : "");
|
|
889
|
-
for (const [x, note] of ex.map((e) => render(e).split(" # "))) {
|
|
890
|
-
const last = blocks.at(-1);
|
|
891
|
-
if (!note && last && !last.note) last.cmds.push(x);
|
|
892
|
-
else blocks.push({ note: note ? `${note[0].toUpperCase()}${note.slice(1)}:` : null, cmds: [x] });
|
|
893
|
-
}
|
|
894
|
-
if (blocks.length) lines.push("", term.heading(c, "Examples"));
|
|
895
|
-
for (const b of blocks)
|
|
896
|
-
lines.push(
|
|
897
|
-
"",
|
|
898
|
-
...(b.note ? [prose(paint("explanation", b.note)), ""] : []),
|
|
899
|
-
...b.cmds.map((x) => ` ${paint("action", x)}`),
|
|
900
|
-
);
|
|
901
|
-
lines.push("", fact(c, "Exit codes", EXIT_CODES));
|
|
902
|
-
return lines.join("\n");
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
// The nearest name within two edits — for a mistyped verb or option.
|
|
906
|
-
export function nearest(word, names) {
|
|
907
|
-
const d = (a, b) => {
|
|
908
|
-
const m = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
909
|
-
for (let j = 1; j <= b.length; j++) m[0][j] = j;
|
|
910
|
-
for (let i = 1; i <= a.length; i++)
|
|
911
|
-
for (let j = 1; j <= b.length; j++)
|
|
912
|
-
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));
|
|
913
|
-
return m[a.length][b.length];
|
|
914
|
-
};
|
|
915
|
-
let best = null,
|
|
916
|
-
score = 3;
|
|
917
|
-
for (const n of names) {
|
|
918
|
-
const k = n.startsWith(word) && word.length >= 3 ? 0 : d(word, n);
|
|
919
|
-
if (k < score) {
|
|
920
|
-
best = n;
|
|
921
|
-
score = k;
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
return best;
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
// ─── run ───────────────────────────────────────────────────────────────
|
|
928
|
-
|
|
929
|
-
// Rule 1 of the per-verb parse: the verb is found before anything is parsed.
|
|
930
|
-
// Before it only the globals' arity is known; after it, its row's too. Left
|
|
931
|
-
// to right, to the first "--": a "--" before the verb makes the next token
|
|
932
|
-
// the verb (`projectstore -- doctor`), and one after it leaves the rest to
|
|
933
|
-
// the verb, so the MCP server's `-- <query>` is never read as a global. Only
|
|
934
|
-
// the exact tokens set a flag: `--json=v` is a global with a bad value, which
|
|
935
|
-
// the parse words (rule 6), not a verb's option. A bare --project takes the
|
|
936
|
-
// next token whatever it is, as parseArgs does, and so does a bare option of
|
|
937
|
-
// the verb's that takes a value: `reconcile --only --help` and the MCP
|
|
938
|
-
// server's `--kind -h` hand the parse a value it refuses, never the help.
|
|
939
|
-
// Any other option before the verb is misplaced (rule 3). A bare word right
|
|
940
|
-
// after one is presumed to be its value when it is not a verb and some verb
|
|
941
|
-
// gives that option a value — for the message only (`--harness codex plan`
|
|
942
|
-
// reads back as `plan --harness codex`); nothing is parsed on that guess.
|
|
943
|
-
function scanArgv(argv, verbs) {
|
|
944
|
-
const scan = { verb: null, at: -1, json: false, help: false, version: false, misplaced: [] };
|
|
945
|
-
const takesValue = (row, name) => row.options.some((o) => o.name === name && o.arg);
|
|
946
|
-
let row = null;
|
|
947
|
-
for (let i = 0; i < argv.length; i++) {
|
|
948
|
-
const a = argv[i];
|
|
949
|
-
if (a === "--") {
|
|
950
|
-
if (scan.at === -1 && i + 1 < argv.length) {
|
|
951
|
-
scan.verb = argv[i + 1];
|
|
952
|
-
scan.at = i + 1;
|
|
953
|
-
}
|
|
954
|
-
break;
|
|
955
|
-
}
|
|
956
|
-
if (a.length > 1 && a.startsWith("-")) {
|
|
957
|
-
if (/^-[hv]+$/.test(a)) {
|
|
958
|
-
if (a.includes("h")) scan.help = true;
|
|
959
|
-
if (a.includes("v")) scan.version = true;
|
|
960
|
-
continue;
|
|
961
|
-
}
|
|
962
|
-
if (a === "--json") scan.json = true;
|
|
963
|
-
if (a === "--help") scan.help = true;
|
|
964
|
-
if (a === "--version") scan.version = true;
|
|
965
|
-
const name = a.startsWith("--") ? a.slice(2).split("=")[0] : null;
|
|
966
|
-
const inline = a.includes("=");
|
|
967
|
-
if (!inline && (name === "project" || (row && takesValue(row, name)))) i++;
|
|
968
|
-
else if (scan.at === -1 && !GLOBAL_OPTS.some((o) => o.name === name))
|
|
969
|
-
scan.misplaced.push({ name, raw: a, at: i, inline, value: undefined });
|
|
970
|
-
continue;
|
|
971
|
-
}
|
|
972
|
-
if (scan.at !== -1) continue;
|
|
973
|
-
const prev = scan.misplaced[scan.misplaced.length - 1];
|
|
974
|
-
if (
|
|
975
|
-
!verbs.some((v) => v.verb === a) &&
|
|
976
|
-
prev &&
|
|
977
|
-
prev.at === i - 1 &&
|
|
978
|
-
!prev.inline &&
|
|
979
|
-
verbs.some((v) => takesValue(v, prev.name))
|
|
980
|
-
) {
|
|
981
|
-
prev.value = a;
|
|
982
|
-
continue;
|
|
983
|
-
}
|
|
984
|
-
scan.verb = a;
|
|
985
|
-
scan.at = i;
|
|
986
|
-
row = verbs.find((v) => v.verb === a) || null;
|
|
987
|
-
}
|
|
988
|
-
return scan;
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
// A row's options and the globals, in parser order: the later declaration
|
|
992
|
-
// wins, so a row cannot retype a global. No row (no verb) is the globals alone.
|
|
993
|
-
const optionsOf = (row) => [...(row ? row.options : []), ...GLOBAL_OPTS];
|
|
994
|
-
|
|
995
|
-
// Rule 2: the row is the parser — arg is the type, multiple the repetition.
|
|
996
|
-
function parserOptions(row) {
|
|
997
|
-
const options = {};
|
|
998
|
-
for (const o of optionsOf(row))
|
|
999
|
-
options[o.name] = {
|
|
1000
|
-
type: o.arg ? "string" : "boolean",
|
|
1001
|
-
...(o.multiple ? { multiple: true } : {}),
|
|
1002
|
-
...(SHORT[o.name] ? { short: SHORT[o.name] } : {}),
|
|
1003
|
-
};
|
|
1004
|
-
return options;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
// "install does not take --verbos — did you mean --verbose?" With a row the
|
|
1008
|
-
// candidates are its options and the globals; without one (no verb, or a
|
|
1009
|
-
// word that is none), every option the table declares, as 0.29.2 offered.
|
|
1010
|
-
function unknownOption(row, raw, verbs) {
|
|
1011
|
-
const names = (row ? optionsOf(row) : [...verbs.flatMap((v) => v.options), ...GLOBAL_OPTS]).map((o) => o.name);
|
|
1012
|
-
const hint = nearest(raw.replace(/^-+/, ""), [...new Set(names)]);
|
|
1013
|
-
return `${row ? `${row.verb} does not take` : "unknown option"} ${raw}${hint ? ` — did you mean --${hint}?` : ""}`;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
// Rule 6: Node's own wording explains "--" positionals; a person who typed
|
|
1017
|
-
// --verbos wants the option they meant, and one who left --only bare wants
|
|
1018
|
-
// its form. A bad value comes in three shapes — an inline value on a flag
|
|
1019
|
-
// (`--json=v`), none at all (`--only` last), one that looks like an option
|
|
1020
|
-
// (`--only --json`) — and each quotes the long name, with ` <value>` or a
|
|
1021
|
-
// `-h, ` short form around it: the declaration words the answer. Node's text
|
|
1022
|
-
// is the fallback when no name can be read.
|
|
1023
|
-
function parseFailure(e, row, verbs) {
|
|
1024
|
-
const raw = e.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION" ? /'(-{1,2}[^']+)'/.exec(e.message)?.[1] : null;
|
|
1025
|
-
if (raw) return unknownOption(row, raw, verbs);
|
|
1026
|
-
const name =
|
|
1027
|
-
e.code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE"
|
|
1028
|
-
? /'(?:-[^-], )?(--[^' ]+)(?: <value>)?'/.exec(e.message)?.[1]
|
|
1029
|
-
: null;
|
|
1030
|
-
const o = name ? optionsOf(row).findLast((x) => `--${x.name}` === name) : null;
|
|
1031
|
-
if (!o) return e.message;
|
|
1032
|
-
return o.arg ? `\`--${o.name}\` needs a value: \`--${o.name} ${o.arg}\`` : `\`--${o.name}\` takes no value`;
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
// Rule 4: a usage form, word by word — a --name the form requires (the word
|
|
1036
|
-
// after it is the option's value when it takes one, not an argument), a
|
|
1037
|
-
// <placeholder> (`<phrase…>` takes the rest), or a literal sub-word. The
|
|
1038
|
-
// bound is how many arguments the form holds.
|
|
1039
|
-
function formOf(row, form) {
|
|
1040
|
-
const f = { form, literals: [], required: [], placeholders: 0, variadic: false };
|
|
1041
|
-
const words = form.split(" ");
|
|
1042
|
-
for (let i = 0; i < words.length; i++) {
|
|
1043
|
-
const w = words[i];
|
|
1044
|
-
if (w.startsWith("--")) {
|
|
1045
|
-
f.required.push(w.slice(2));
|
|
1046
|
-
if (row.options.find((o) => o.name === w.slice(2))?.arg) i++;
|
|
1047
|
-
} else if (w.startsWith("<")) {
|
|
1048
|
-
f.placeholders++;
|
|
1049
|
-
if (w.endsWith("…>")) f.variadic = true;
|
|
1050
|
-
} else f.literals.push(w);
|
|
1051
|
-
}
|
|
1052
|
-
f.bound = f.variadic ? Infinity : f.literals.length + f.placeholders;
|
|
1053
|
-
return f;
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
// The form the arguments take: the first whose literal words lead them and
|
|
1057
|
-
// whose required options were given. null when none does, and the verb's own
|
|
1058
|
-
// message then says what it takes ("agents takes model <name>, show, or
|
|
1059
|
-
// configure") — which is how `codemap PS-CORE` keeps its --for hint while
|
|
1060
|
-
// `codemap --for X extra` names `extra`. A row with no forms binds nothing.
|
|
1061
|
-
function matchForm(row, args, values) {
|
|
1062
|
-
if (!row.usage.length) return { form: "", bound: 0 };
|
|
1063
|
-
for (const form of row.usage) {
|
|
1064
|
-
const f = formOf(row, form);
|
|
1065
|
-
if (f.literals.every((w, i) => args[i] === w) && f.required.every((n) => values[n] !== undefined)) return f;
|
|
1066
|
-
}
|
|
1067
|
-
return null;
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
// `verbs` is the table this run answers for: VERBS, or a test's own — the
|
|
1071
|
-
// scan, the help, the verb lookup and every hint read it, never VERBS behind it.
|
|
1072
|
-
export async function run(
|
|
1073
|
-
argv,
|
|
1074
|
-
{
|
|
1075
|
-
verbs = VERBS,
|
|
1076
|
-
env = process.env,
|
|
1077
|
-
cwd = process.cwd(),
|
|
1078
|
-
stdin = process.stdin,
|
|
1079
|
-
stdout = process.stdout,
|
|
1080
|
-
stderr = process.stderr,
|
|
1081
|
-
ask = null,
|
|
1082
|
-
} = {},
|
|
1083
|
-
) {
|
|
1084
|
-
const scan = scanArgv(argv, verbs);
|
|
1085
|
-
const row = verbs.find((v) => v.verb === scan.verb) || null;
|
|
1086
|
-
// A failure before a verb runs still answers in the envelope under --json:
|
|
1087
|
-
// a consumer (the MCP server, a script) always has something to parse. The
|
|
1088
|
-
// verb is the scan's, whichever step failed (rule 7) — never the first
|
|
1089
|
-
// bare word, which may be --project's value.
|
|
1090
|
-
const fail = (message, code, { project = null, help = false } = {}) => {
|
|
1091
|
-
if (scan.json)
|
|
1092
|
-
stdout.write(JSON.stringify(envelope(scan.verb, project, false, { error: message, exit: code }), null, 2) + "\n");
|
|
1093
|
-
stderr.write(message + "\n" + (help ? usage(env, verbs, { caps: term.caps(stderr, env) }) + "\n" : ""));
|
|
1094
|
-
return code;
|
|
1095
|
-
};
|
|
1096
|
-
// A usage error says where the options are: the verb's help, else the top
|
|
1097
|
-
// (a word that is not a verb has no help to point at).
|
|
1098
|
-
const refuse = (message) => fail(`${message}\nRun ${row ? `\`${row.verb} --help\`` : "--help"} for the options.`, 2);
|
|
1099
|
-
// In-process reads resolve layouts and registries from THIS package, as the
|
|
1100
|
-
// children already do through ownEnv — not from whichever copy the host
|
|
1101
|
-
// session's variable points at.
|
|
1102
|
-
pinPluginRoot(PACKAGE_ROOT);
|
|
1103
|
-
// Rule 8: help and version answer first, whatever else the line holds, so
|
|
1104
|
-
// `doctor extra --help` and `--harness codex plan --help` print the help.
|
|
1105
|
-
if (scan.version) return runVersion({ values: { json: scan.json }, stdout });
|
|
1106
|
-
// Help is painted at a terminal and plain on a pipe: the caps are stdout's.
|
|
1107
|
-
if (scan.help) {
|
|
1108
|
-
const caps = term.caps(stdout, env);
|
|
1109
|
-
stdout.write((row ? verbHelp(row, env, { caps }) : usage(env, verbs, { caps })) + "\n");
|
|
1110
|
-
return 0;
|
|
1111
|
-
}
|
|
1112
|
-
// With no verb to scope it, an option no verb declares is unknown before
|
|
1113
|
-
// anything else is said, as in 0.29.2: `--bogus frobnicate` names --bogus,
|
|
1114
|
-
// and `--verbos instal` offers --verbose.
|
|
1115
|
-
const stray = row ? null : scan.misplaced.find((m) => !verbs.some((v) => v.options.some((o) => o.name === m.name)));
|
|
1116
|
-
if (stray) return refuse(unknownOption(null, stray.raw.split("=")[0], verbs));
|
|
1117
|
-
if (scan.verb === null) {
|
|
1118
|
-
// No verb. A verb's option (`--harness codex`, `--verbose`) is the usage,
|
|
1119
|
-
// as before — not "unknown verb: codex"; otherwise the globals alone are
|
|
1120
|
-
// parsed, so a bad value of one (`--project` last) is named.
|
|
1121
|
-
if (!scan.misplaced.length) {
|
|
1122
|
-
try {
|
|
1123
|
-
parseArgs({ args: argv, options: parserOptions(null), strict: true, allowPositionals: true });
|
|
1124
|
-
} catch (e) {
|
|
1125
|
-
return refuse(parseFailure(e, null, verbs));
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
stderr.write(usage(env, verbs, { caps: term.caps(stderr, env) }) + "\n");
|
|
1129
|
-
return 2;
|
|
1130
|
-
}
|
|
1131
|
-
const verb = scan.verb;
|
|
1132
|
-
if (!row) {
|
|
1133
|
-
const planned = PLANNED_VERBS.find((v) => v.verb === verb);
|
|
1134
|
-
const guess = planned
|
|
1135
|
-
? null
|
|
1136
|
-
: nearest(
|
|
1137
|
-
verb,
|
|
1138
|
-
verbs.map((v) => v.verb),
|
|
1139
|
-
);
|
|
1140
|
-
if (guess) return fail(`unknown verb: ${verb} — did you mean ${guess}?\nRun --help for every verb.`, 2);
|
|
1141
|
-
return fail(
|
|
1142
|
-
planned ? `${verb} lands with roadmap ${planned.lands}; not in this release.` : `unknown verb: ${verb}`,
|
|
1143
|
-
2,
|
|
1144
|
-
{ help: true },
|
|
1145
|
-
);
|
|
1146
|
-
}
|
|
1147
|
-
// Rule 3: only the globals come before the verb (git's model). A verb's
|
|
1148
|
-
// own option there is shown in its place; one the verb does not declare is
|
|
1149
|
-
// unknown, scoped to the verb. Accepting it would need its arity before the
|
|
1150
|
-
// verb is known, which is what per-verb types make ambiguous.
|
|
1151
|
-
if (scan.misplaced.length) {
|
|
1152
|
-
const m = scan.misplaced[0];
|
|
1153
|
-
const o = m.name === null ? null : row.options.find((x) => x.name === m.name);
|
|
1154
|
-
if (!o) return refuse(unknownOption(row, m.raw.split("=")[0], verbs));
|
|
1155
|
-
// Through a shell the harness is fixed (verbHelp's test: the row declares
|
|
1156
|
-
// it): the order shown drops it, since the shell inserts it after the verb.
|
|
1157
|
-
// The rest of the line follows, so the command shown is the one to run.
|
|
1158
|
-
const { shell, cmd } = invokedAs(env);
|
|
1159
|
-
const moved = shell && o.name === "harness" ? [] : [m.raw, ...(o.arg && m.value !== undefined ? [m.value] : [])];
|
|
1160
|
-
return refuse(
|
|
1161
|
-
`\`--${o.name}\` belongs after the verb: \`${[cmd, verb, ...moved, ...argv.slice(scan.at + 1)].join(" ")}\``,
|
|
1162
|
-
);
|
|
1163
|
-
}
|
|
1164
|
-
// Rule 2: one strict parse with this row's options plus the globals, on the
|
|
1165
|
-
// full argv so a token's index is the user's (rule 4's hints read them).
|
|
1166
|
-
let parsed;
|
|
1167
|
-
try {
|
|
1168
|
-
parsed = parseArgs({ args: argv, options: parserOptions(row), strict: true, allowPositionals: true, tokens: true });
|
|
1169
|
-
} catch (e) {
|
|
1170
|
-
return refuse(parseFailure(e, row, verbs));
|
|
1171
|
-
}
|
|
1172
|
-
const { values, positionals, tokens } = parsed;
|
|
1173
|
-
// Rule 4: an argument past the matched form is named, never dropped. The
|
|
1174
|
-
// hint says how it became one: the flag before it takes no value, or a
|
|
1175
|
-
// "--" made everything after it an argument.
|
|
1176
|
-
const args = tokens.filter((t) => t.kind === "positional" && t.index !== scan.at);
|
|
1177
|
-
const form = matchForm(
|
|
1178
|
-
row,
|
|
1179
|
-
args.map((t) => t.value),
|
|
1180
|
-
values,
|
|
1181
|
-
);
|
|
1182
|
-
if (form && args.length > form.bound) {
|
|
1183
|
-
const extra = args[form.bound];
|
|
1184
|
-
const prev = tokens.find((t) => t.index === extra.index - 1);
|
|
1185
|
-
const end = tokens.find((t) => t.kind === "option-terminator");
|
|
1186
|
-
const why =
|
|
1187
|
-
prev && prev.kind === "option" && prev.value === undefined
|
|
1188
|
-
? ` — \`--${prev.name}\` takes no value`
|
|
1189
|
-
: end && extra.index > end.index
|
|
1190
|
-
? " — everything after `--` is an argument"
|
|
1191
|
-
: "";
|
|
1192
|
-
return refuse(
|
|
1193
|
-
`${form.bound ? `\`${verb} ${form.form}\` takes no further argument` : `${verb} takes no argument`} \`${extra.value}\`${why}`,
|
|
1194
|
-
);
|
|
1195
|
-
}
|
|
1196
|
-
const project = resolveProject({ project: values.project, env, cwd });
|
|
1197
|
-
const cfg = readConfigAt(project);
|
|
1198
|
-
if (row.requiresBinding && !cfg) return fail(notBoundText(project, env), 3, { project });
|
|
1199
|
-
try {
|
|
1200
|
-
return await row.run({
|
|
1201
|
-
row,
|
|
1202
|
-
values,
|
|
1203
|
-
positionals: positionals.slice(1),
|
|
1204
|
-
cfg,
|
|
1205
|
-
project,
|
|
1206
|
-
env,
|
|
1207
|
-
cwd,
|
|
1208
|
-
stdin,
|
|
1209
|
-
stdout,
|
|
1210
|
-
stderr,
|
|
1211
|
-
ask,
|
|
1212
|
-
});
|
|
1213
|
-
} catch (e) {
|
|
1214
|
-
// An internal failure is not "findings" (1) — it is 2, with an envelope
|
|
1215
|
-
// when one was asked for.
|
|
1216
|
-
const msg = e && e.message ? e.message : String(e);
|
|
1217
|
-
if (values.json) stdout.write(JSON.stringify(envelope(verb, project, false, { error: msg }), null, 2) + "\n");
|
|
1218
|
-
stderr.write(`${verb} failed: ${msg}\n`);
|
|
1219
|
-
return 2;
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
// The environment a child or the installer runs in: the resolved project and
|
|
1224
|
-
// THIS package as the plugin root, so the bin never answers for a sibling
|
|
1225
|
-
// copy of the core.
|
|
1226
|
-
function ownEnv(env, project) {
|
|
1227
|
-
return childEnv(env, { projectRoot: project, pluginRoot: PACKAGE_ROOT });
|
|
1228
|
-
}
|
|
70
|
+
import { dispatch, envelope, usageFail, ownEnv, nearest, notBoundText, PACKAGE_ROOT } from "./verbs.mjs";
|
|
71
|
+
import { READ_HANDLERS } from "./read-verbs.mjs";
|
|
72
|
+
|
|
73
|
+
// What this module exported before the table moved, from where it lives now.
|
|
74
|
+
export {
|
|
75
|
+
SCHEMA_VERSION,
|
|
76
|
+
PACKAGE_ROOT,
|
|
77
|
+
packageVersion,
|
|
78
|
+
envelope,
|
|
79
|
+
resolveProject,
|
|
80
|
+
opt,
|
|
81
|
+
VERBS,
|
|
82
|
+
PLANNED_VERBS,
|
|
83
|
+
SUMMARY_MAX,
|
|
84
|
+
usage,
|
|
85
|
+
verbHelp,
|
|
86
|
+
nearest,
|
|
87
|
+
notBoundText,
|
|
88
|
+
} from "./verbs.mjs";
|
|
89
|
+
export { doctorText } from "./read-verbs.mjs";
|
|
1229
90
|
|
|
1230
91
|
// The one interactive question every write verb asks when nothing named the
|
|
1231
92
|
// write. Streams and `ask` are parameters so it is testable without a tty.
|
|
@@ -1237,59 +98,16 @@ async function confirmWrite(question, { stdin, stdout, ask }) {
|
|
|
1237
98
|
return answer !== null && /^y(es)?$/i.test(answer.trim());
|
|
1238
99
|
}
|
|
1239
100
|
|
|
1240
|
-
// ─── verbs
|
|
1241
|
-
|
|
1242
|
-
// The read verbs: one call into query.mjs, the result in the envelope or
|
|
1243
|
-
// rendered. A usage error from the operation (a bad path, a missing query)
|
|
1244
|
-
// is exit 2 with the message, never a stack trace. A renderer gets the
|
|
1245
|
-
// stream's caps — painted at a terminal, plain on a pipe — and the env its
|
|
1246
|
-
// command forms are named for; one that takes neither ignores them.
|
|
1247
|
-
function emitRead({ verb, values, project, stdout, env }, op, result, ok = true) {
|
|
1248
|
-
if (values.json) stdout.write(JSON.stringify(envelope(verb, project, ok, result), null, 2) + "\n");
|
|
1249
|
-
else stdout.write(op.render(result, { caps: term.caps(stdout, env), env }));
|
|
1250
|
-
return ok ? 0 : 1;
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
function usageFail(e, { verb, values, project, stdout, stderr }) {
|
|
1254
|
-
if (values.json) stdout.write(JSON.stringify(envelope(verb, project, false, { error: e.message }), null, 2) + "\n");
|
|
1255
|
-
stderr.write(`${verb}: ${e.message}\n`);
|
|
1256
|
-
return 2;
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
function runRead(name) {
|
|
1260
|
-
return async (ctx) => {
|
|
1261
|
-
const { row, values, positionals, cfg, project } = ctx;
|
|
1262
|
-
const op = READ_OPERATIONS[name];
|
|
1263
|
-
try {
|
|
1264
|
-
let result;
|
|
1265
|
-
if (name === "status") result = op.fn(cfg, { project });
|
|
1266
|
-
else if (name === "orientation") result = await op.fn(cfg);
|
|
1267
|
-
else if (name === "search")
|
|
1268
|
-
result = op.fn(cfg, positionals.join(" "), {
|
|
1269
|
-
kinds: values.kind || null,
|
|
1270
|
-
status: values.status ?? null,
|
|
1271
|
-
limit: values.limit,
|
|
1272
|
-
includeDerived: Boolean(values["include-derived"]),
|
|
1273
|
-
caseSensitive: Boolean(values["case-sensitive"]),
|
|
1274
|
-
});
|
|
1275
|
-
else if (name === "show")
|
|
1276
|
-
result = op.fn(cfg, positionals[0], { body: Boolean(values.body), section: values.section ?? null });
|
|
1277
|
-
return emitRead({ verb: row.verb, ...ctx }, op, result);
|
|
1278
|
-
} catch (e) {
|
|
1279
|
-
if (e && e.code === "USAGE") return usageFail(e, { verb: row.verb, ...ctx });
|
|
1280
|
-
throw e;
|
|
1281
|
-
}
|
|
1282
|
-
};
|
|
1283
|
-
}
|
|
101
|
+
// ─── The write verbs' handlers ─────────────────────────────────────────
|
|
1284
102
|
|
|
1285
103
|
// The MCP server, imported lazily like the install family: no other verb
|
|
1286
|
-
// carries the protocol code.
|
|
1287
|
-
// the
|
|
1288
|
-
//
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
const {
|
|
1292
|
-
return
|
|
104
|
+
// carries the protocol code. It serves the project the flag names, else the
|
|
105
|
+
// one the environment supplies, never this process's cwd — mcp.mjs's
|
|
106
|
+
// serveSupplied, the one path scripts/mcp-server.mjs (what .mcp.json runs)
|
|
107
|
+
// takes too.
|
|
108
|
+
async function runMcp({ values, env, cwd, stdin, stdout, stderr }) {
|
|
109
|
+
const { serveSupplied } = await import("./mcp.mjs");
|
|
110
|
+
return serveSupplied({ project: values.project ?? null, env, cwd, stdin, stdout, stderr });
|
|
1293
111
|
}
|
|
1294
112
|
|
|
1295
113
|
// git's user.name for a fresh config's default_author, from the project's
|
|
@@ -1664,140 +482,6 @@ async function runSession(ctx) {
|
|
|
1664
482
|
return ok ? 0 : 1;
|
|
1665
483
|
}
|
|
1666
484
|
|
|
1667
|
-
async function runGraph(ctx) {
|
|
1668
|
-
const { row, values, positionals, cfg } = ctx;
|
|
1669
|
-
const sub = positionals[0];
|
|
1670
|
-
const path = positionals[1];
|
|
1671
|
-
if (!["neighbors", "lineage"].includes(sub))
|
|
1672
|
-
return usageFail(Object.assign(new Error("graph takes neighbors <path> or lineage <path>"), { code: "USAGE" }), {
|
|
1673
|
-
verb: row.verb,
|
|
1674
|
-
...ctx,
|
|
1675
|
-
});
|
|
1676
|
-
const op = READ_OPERATIONS[sub];
|
|
1677
|
-
try {
|
|
1678
|
-
// The row declares the union; each mode takes its own — an option the
|
|
1679
|
-
// mode would ignore is a usage error, not a silent no-op.
|
|
1680
|
-
const notFor = sub === "neighbors" ? ["depth"] : ["direction", "limit"];
|
|
1681
|
-
for (const o of notFor)
|
|
1682
|
-
if (values[o] !== undefined)
|
|
1683
|
-
throw Object.assign(new Error(`--${o} is not an option of graph ${sub}`), { code: "USAGE" });
|
|
1684
|
-
const result =
|
|
1685
|
-
sub === "neighbors"
|
|
1686
|
-
? op.fn(cfg, path, { kinds: values.kind || null, direction: values.direction, limit: values.limit })
|
|
1687
|
-
: op.fn(cfg, path, {
|
|
1688
|
-
depth: values.depth,
|
|
1689
|
-
kinds: values.kind && values.kind.length ? values.kind : LINEAGE_KINDS,
|
|
1690
|
-
});
|
|
1691
|
-
return emitRead({ verb: `graph ${sub}`, ...ctx }, op, result);
|
|
1692
|
-
} catch (e) {
|
|
1693
|
-
if (e && e.code === "USAGE") return usageFail(e, { verb: row.verb, ...ctx });
|
|
1694
|
-
throw e;
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
async function runCodemap(ctx) {
|
|
1699
|
-
const { row, values, cfg } = ctx;
|
|
1700
|
-
const op = READ_OPERATIONS.codeRefs;
|
|
1701
|
-
try {
|
|
1702
|
-
if (!values.for)
|
|
1703
|
-
throw Object.assign(new Error("codemap takes --for <selector>; regeneration is reconcile --only codemap"), {
|
|
1704
|
-
code: "USAGE",
|
|
1705
|
-
});
|
|
1706
|
-
return emitRead({ verb: row.verb, ...ctx }, op, op.fn(cfg, values.for, { reverse: Boolean(values.reverse) }));
|
|
1707
|
-
} catch (e) {
|
|
1708
|
-
if (e && e.code === "USAGE") return usageFail(e, { verb: row.verb, ...ctx });
|
|
1709
|
-
throw e;
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
function runVersion({ values, stdout }) {
|
|
1714
|
-
const version = packageVersion();
|
|
1715
|
-
stdout.write(
|
|
1716
|
-
values.json ? JSON.stringify(envelope("version", null, true, { version }), null, 2) + "\n" : version + "\n",
|
|
1717
|
-
);
|
|
1718
|
-
return 0;
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
function spawnDoctor(project, env, flags) {
|
|
1722
|
-
// A project that does not exist is still a project doctor can report on
|
|
1723
|
-
// ("not bound") — so the cwd is only set when it can be.
|
|
1724
|
-
return spawnSync(process.execPath, [script("doctor.mjs"), ...flags], {
|
|
1725
|
-
encoding: "utf8",
|
|
1726
|
-
cwd: existsSync(project) ? project : undefined,
|
|
1727
|
-
env: ownEnv(env, project),
|
|
1728
|
-
timeout: 60000,
|
|
1729
|
-
maxBuffer: 1 << 24,
|
|
1730
|
-
});
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
// Spawned, never imported (MCP ADR decision 2): doctor's main writes to
|
|
1734
|
-
// stdout and is not exported, and importing its checks would be a second
|
|
1735
|
-
// doctor. One spawn, always --json: its findings decide the exit code
|
|
1736
|
-
// (doctor.mjs sets none of its own), and the text a person reads is drawn
|
|
1737
|
-
// from those same findings here, in this process, by doctor-report.mjs — a
|
|
1738
|
-
// renderer that imports no check (the presentation spec, contract 11). So a
|
|
1739
|
-
// terminal gets colour, which a piped child could not tell it had, and the
|
|
1740
|
-
// report and the exit code can never come from two different runs.
|
|
1741
|
-
async function runDoctor({ values, project, env, stdout, stderr }) {
|
|
1742
|
-
const sections = [values.install && "--install", values.vault && "--vault"].filter(Boolean);
|
|
1743
|
-
const r = spawnDoctor(project, env, ["--json", ...sections]);
|
|
1744
|
-
const fail = (why) => {
|
|
1745
|
-
if (values.json) stdout.write(JSON.stringify(envelope("doctor", project, false, { error: why }), null, 2) + "\n");
|
|
1746
|
-
stderr.write(`doctor failed: ${why}\n`);
|
|
1747
|
-
return 2;
|
|
1748
|
-
};
|
|
1749
|
-
if (r.error) return fail(r.error.message);
|
|
1750
|
-
if (r.status !== 0 && !r.stdout) return fail((r.stderr || "").trim() || `exit ${r.status}`);
|
|
1751
|
-
let findings;
|
|
1752
|
-
try {
|
|
1753
|
-
findings = JSON.parse(r.stdout);
|
|
1754
|
-
} catch {
|
|
1755
|
-
return fail("unparseable output");
|
|
1756
|
-
}
|
|
1757
|
-
const ok = !findings.some((f) => f.level === "issue");
|
|
1758
|
-
if (values.json) stdout.write(JSON.stringify(envelope("doctor", project, ok, findings), null, 2) + "\n");
|
|
1759
|
-
else {
|
|
1760
|
-
// The sections as doctor.mjs derives them: no flag is both.
|
|
1761
|
-
const groups = sections.length
|
|
1762
|
-
? [values.install && "install", values.vault && "vault"].filter(Boolean)
|
|
1763
|
-
: ["install", "vault"];
|
|
1764
|
-
stdout.write(
|
|
1765
|
-
await doctorText(findings, groups, {
|
|
1766
|
-
caps: term.caps(stdout, env),
|
|
1767
|
-
verbose: Boolean(values.verbose),
|
|
1768
|
-
project,
|
|
1769
|
-
env: ownEnv(env, project),
|
|
1770
|
-
stderr,
|
|
1771
|
-
}),
|
|
1772
|
-
);
|
|
1773
|
-
}
|
|
1774
|
-
return ok ? 0 : 1;
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
// doctor's text from its findings. `env` is the child's environment
|
|
1778
|
-
// (ownEnv), so every command form the report names is the one doctor itself
|
|
1779
|
-
// would name. A renderer that cannot load, or throws, must not cost the
|
|
1780
|
-
// reader the findings: they are printed one per line, `level check message
|
|
1781
|
-
// — file`, and the Summary line still ends them (lib.mjs). `load` stands in
|
|
1782
|
-
// for the import in a test.
|
|
1783
|
-
export async function doctorText(
|
|
1784
|
-
findings,
|
|
1785
|
-
groups,
|
|
1786
|
-
{ caps, verbose = false, project = null, env = process.env, stderr = null } = {},
|
|
1787
|
-
{ load = () => import("./doctor-report.mjs") } = {},
|
|
1788
|
-
) {
|
|
1789
|
-
try {
|
|
1790
|
-
const { report } = await load();
|
|
1791
|
-
return report(findings, groups, { caps, verbose, version: packageVersion(), project, env });
|
|
1792
|
-
} catch (e) {
|
|
1793
|
-
if (stderr)
|
|
1794
|
-
stderr.write(
|
|
1795
|
-
`doctor: the report could not be drawn (${e && e.message ? e.message : e}); its findings follow, one per line.\n`,
|
|
1796
|
-
);
|
|
1797
|
-
return doctorFallbackText(findings, { env });
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
485
|
async function runReconcile({ values, project, env, stdin, stdout, stderr, ask }) {
|
|
1802
486
|
const write = Boolean(values.write);
|
|
1803
487
|
const only = values.only ?? null;
|
|
@@ -1836,19 +520,6 @@ async function runReconcile({ values, project, env, stdin, stdout, stderr, ask }
|
|
|
1836
520
|
return failed ? 1 : 0;
|
|
1837
521
|
}
|
|
1838
522
|
|
|
1839
|
-
// The not-bound refusal, for every verb that needs a binding and for configure.
|
|
1840
|
-
// Each form alone on its line after the lead, the terminal's in the core's
|
|
1841
|
-
// form, the directory in the lead (the story "User-facing text says where to
|
|
1842
|
-
// run and names no project path and no shell", rules 1, 4 and 6).
|
|
1843
|
-
export function notBoundText(project, env = process.env) {
|
|
1844
|
-
return [
|
|
1845
|
-
`${project} is not bound to a vault. Bind it in a session, or ${fromTerminalIn(project)}, where init creates the vault too:`,
|
|
1846
|
-
commandForm("bind", { args: "<vault>", env }),
|
|
1847
|
-
packageCommand([], "bind", { args: "<vault>" }),
|
|
1848
|
-
packageCommand([], "init", { args: "<vault>" }),
|
|
1849
|
-
].join("\n");
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
523
|
// configure (its spec): loaded on first use, like setup. The module returns a
|
|
1853
524
|
// usage error, an unbound project, a refusal's message, or a result; the bin
|
|
1854
525
|
// writes the line, the envelope and the exit code.
|
|
@@ -1963,3 +634,33 @@ async function runInstallVerb({ row, values, project, env, cwd, stdin, stdout, a
|
|
|
1963
634
|
}
|
|
1964
635
|
return ok ? 0 : 1;
|
|
1965
636
|
}
|
|
637
|
+
|
|
638
|
+
// ─── run ───────────────────────────────────────────────────────────────
|
|
639
|
+
|
|
640
|
+
// Every verb's handler, keyed by verb: the read half from read-verbs.mjs,
|
|
641
|
+
// the rest here. Exported so a test can wrap one (setup's sandboxed home) in
|
|
642
|
+
// a table of its own.
|
|
643
|
+
export const HANDLERS = Object.freeze({
|
|
644
|
+
...READ_HANDLERS,
|
|
645
|
+
setup: runSetupVerb,
|
|
646
|
+
configure: runConfigureVerb,
|
|
647
|
+
reconcile: runReconcile,
|
|
648
|
+
plan: runInstallVerb,
|
|
649
|
+
install: runInstallVerb,
|
|
650
|
+
uninstall: runInstallVerb,
|
|
651
|
+
upgrade: runInstallVerb,
|
|
652
|
+
agents: runAgents,
|
|
653
|
+
band: runBand,
|
|
654
|
+
session: runSession,
|
|
655
|
+
bind: runBind(false),
|
|
656
|
+
init: runBind(true),
|
|
657
|
+
scaffold: runScaffold,
|
|
658
|
+
mcp: runMcp,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// run(argv, opts) — the bin's one call: the parse over the one table, every
|
|
662
|
+
// verb served. `opts` as dispatch()'s (verbs.mjs): `verbs` is the table this
|
|
663
|
+
// run answers for, VERBS or a test's own, whose rows may carry their own run.
|
|
664
|
+
export function run(argv, opts = {}) {
|
|
665
|
+
return dispatch(argv, HANDLERS, opts);
|
|
666
|
+
}
|