projectstore-claude 0.29.3 → 0.29.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/projectstore-claude.mjs +1 -1
- package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
- package/node_modules/projectstore/.claude-plugin/plugin.json +1 -1
- package/node_modules/projectstore/README.md +17 -0
- package/node_modules/projectstore/commands/bind.md +6 -17
- package/node_modules/projectstore/commands/doctor.md +12 -7
- package/node_modules/projectstore/commands/statusline.md +1 -1
- package/node_modules/projectstore/commands/story.md +15 -2
- package/node_modules/projectstore/docs/how-it-works.md +5 -3
- package/node_modules/projectstore/harnesses/claude-code.json +5 -5
- package/node_modules/projectstore/harnesses/codex.json +4 -5
- package/node_modules/projectstore/hooks/band.tsx +48 -20
- package/node_modules/projectstore/hooks/pre-compact.mjs +7 -10
- package/node_modules/projectstore/hooks/session-rules.mjs +14 -8
- package/node_modules/projectstore/hooks/session-start.mjs +50 -18
- package/node_modules/projectstore/hooks/session-stop.mjs +8 -6
- package/node_modules/projectstore/package.json +11 -2
- package/node_modules/projectstore/scripts/band-shared.mjs +141 -24
- package/node_modules/projectstore/scripts/band.mjs +72 -21
- package/node_modules/projectstore/scripts/binding.mjs +616 -85
- package/node_modules/projectstore/scripts/build-adapters.mjs +73 -29
- package/node_modules/projectstore/scripts/cli.mjs +1015 -185
- package/node_modules/projectstore/scripts/codemap.mjs +22 -7
- package/node_modules/projectstore/scripts/convergence.mjs +253 -83
- package/node_modules/projectstore/scripts/diff-refs.mjs +14 -8
- package/node_modules/projectstore/scripts/doctor-report.mjs +91 -23
- package/node_modules/projectstore/scripts/doctor.mjs +1317 -371
- package/node_modules/projectstore/scripts/draft.mjs +5 -2
- package/node_modules/projectstore/scripts/fetch-shell.mjs +106 -20
- package/node_modules/projectstore/scripts/graph.mjs +19 -13
- package/node_modules/projectstore/scripts/harness.mjs +123 -48
- package/node_modules/projectstore/scripts/install-harness.mjs +3511 -616
- package/node_modules/projectstore/scripts/kanban.mjs +2 -1
- package/node_modules/projectstore/scripts/lib.mjs +694 -159
- package/node_modules/projectstore/scripts/mcp.mjs +386 -105
- package/node_modules/projectstore/scripts/portable-registration.mjs +224 -51
- package/node_modules/projectstore/scripts/provenance.mjs +26 -10
- package/node_modules/projectstore/scripts/query.mjs +378 -80
- package/node_modules/projectstore/scripts/reconcile.mjs +55 -18
- package/node_modules/projectstore/scripts/run-host.mjs +62 -17
- package/node_modules/projectstore/scripts/scaffold.mjs +89 -28
- package/node_modules/projectstore/scripts/setup.mjs +987 -0
- package/node_modules/projectstore/scripts/stages.mjs +5 -1
- package/node_modules/projectstore/scripts/statusline-launcher.mjs +36 -11
- package/node_modules/projectstore/scripts/statusline.mjs +32 -14
- package/node_modules/projectstore/scripts/story-section.mjs +39 -17
- package/node_modules/projectstore/scripts/surfaces.mjs +416 -95
- package/node_modules/projectstore/scripts/term.mjs +382 -97
- package/node_modules/projectstore/scripts/tokens.mjs +106 -28
- package/node_modules/projectstore/scripts/touch-session.mjs +109 -70
- package/node_modules/projectstore/scripts/version-guard.mjs +74 -27
- package/node_modules/projectstore/scripts/worktree.mjs +16 -5
- package/package.json +2 -2
|
@@ -55,16 +55,64 @@ import { resolve, dirname } from "node:path";
|
|
|
55
55
|
import { fileURLToPath } from "node:url";
|
|
56
56
|
import { parseArgs } from "node:util";
|
|
57
57
|
import * as term from "./term.mjs";
|
|
58
|
-
import {
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
import {
|
|
59
|
+
projectRootDeclared,
|
|
60
|
+
childEnv,
|
|
61
|
+
harnessIds,
|
|
62
|
+
harnessForOverlay,
|
|
63
|
+
pinPluginRoot,
|
|
64
|
+
invokedAs,
|
|
65
|
+
shellHarnessId,
|
|
66
|
+
CORE_NPX,
|
|
67
|
+
packageCommand,
|
|
68
|
+
loadHarnesses,
|
|
69
|
+
invocation,
|
|
70
|
+
} from "./harness.mjs";
|
|
71
|
+
import {
|
|
72
|
+
readConfigAt,
|
|
73
|
+
readOverlayAt,
|
|
74
|
+
resolveAgentModel,
|
|
75
|
+
writeOverlayAt,
|
|
76
|
+
overlayId,
|
|
77
|
+
layoutRoster,
|
|
78
|
+
commandForm,
|
|
79
|
+
projectDirRefusal,
|
|
80
|
+
fromTerminalIn,
|
|
81
|
+
doctorFallbackText,
|
|
82
|
+
refreshSessionPointer,
|
|
83
|
+
} from "./lib.mjs";
|
|
84
|
+
import {
|
|
85
|
+
READ_OPERATIONS,
|
|
86
|
+
LINEAGE_KINDS,
|
|
87
|
+
LINEAGE_DEFAULT_DEPTH,
|
|
88
|
+
SEARCH_DEFAULT_LIMIT,
|
|
89
|
+
GRAPH_EDGE_CAP,
|
|
90
|
+
DIRECTIONS,
|
|
91
|
+
} from "./query.mjs";
|
|
61
92
|
// binding.mjs and scaffold.mjs beneath it are write modules imported
|
|
62
93
|
// statically where the install family is lazy: dependency-free, with no side
|
|
63
94
|
// effects at import, so the MCP server's module graph gains nothing it could
|
|
64
95
|
// trip on.
|
|
65
|
-
import {
|
|
96
|
+
import {
|
|
97
|
+
planBind,
|
|
98
|
+
applyBind,
|
|
99
|
+
renderBindPlan,
|
|
100
|
+
bindResult,
|
|
101
|
+
bindFailed,
|
|
102
|
+
normaliseVaultPath,
|
|
103
|
+
shellQuote,
|
|
104
|
+
DEFAULT_LAYOUT,
|
|
105
|
+
DEFAULT_LANGUAGE,
|
|
106
|
+
} from "./binding.mjs";
|
|
66
107
|
import { planScaffold, applyScaffold, scaffoldResult, renderScaffoldPlan } from "./scaffold.mjs";
|
|
67
|
-
import {
|
|
108
|
+
import {
|
|
109
|
+
planBand,
|
|
110
|
+
applyBand,
|
|
111
|
+
bandStatus,
|
|
112
|
+
renderBandStatus,
|
|
113
|
+
renderBandPlan,
|
|
114
|
+
SUBCOMMANDS as BAND_SUBCOMMANDS,
|
|
115
|
+
} from "./band.mjs";
|
|
68
116
|
|
|
69
117
|
export const SCHEMA_VERSION = 1;
|
|
70
118
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -72,7 +120,11 @@ export const PACKAGE_ROOT = dirname(HERE);
|
|
|
72
120
|
const script = (name) => resolve(HERE, name);
|
|
73
121
|
|
|
74
122
|
export function packageVersion() {
|
|
75
|
-
try {
|
|
123
|
+
try {
|
|
124
|
+
return String(JSON.parse(readFileSync(resolve(PACKAGE_ROOT, "package.json"), "utf8")).version || "");
|
|
125
|
+
} catch {
|
|
126
|
+
return "";
|
|
127
|
+
}
|
|
76
128
|
}
|
|
77
129
|
|
|
78
130
|
export function envelope(verb, project, ok, result) {
|
|
@@ -108,129 +160,428 @@ const JSON_OPT = opt("json", false, "the envelope");
|
|
|
108
160
|
// verb: the scan knows their arity before it knows the verb (--project takes
|
|
109
161
|
// a value, the rest do not). Last in every parser map, so a row cannot
|
|
110
162
|
// retype one. SHORT is their one-letter forms (`-hv`: version wins).
|
|
111
|
-
const GLOBAL_OPTS = Object.freeze([
|
|
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
|
+
]);
|
|
112
169
|
const SHORT = Object.freeze({ help: "h", version: "v" });
|
|
113
170
|
const NO_FORMS = Object.freeze([]);
|
|
114
171
|
const READ_JSON = [JSON_OPT];
|
|
115
|
-
const HARNESS_OPT = 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
|
+
);
|
|
116
178
|
const SURFACE_OPT = opt("surface", "<key>", "one surface and those beneath it", true);
|
|
117
179
|
// The layout move's remedy names this for any copy but the package's own
|
|
118
180
|
// registration (the layout spec, contract 12 as amended 2026-10-03): it makes
|
|
119
181
|
// "no host command" true by construction instead of by recognising the root.
|
|
120
|
-
const NO_REGISTER_OPT = opt(
|
|
182
|
+
const NO_REGISTER_OPT = opt(
|
|
183
|
+
"no-register",
|
|
184
|
+
false,
|
|
185
|
+
"leave the plugin registration alone: change only this project's files",
|
|
186
|
+
);
|
|
121
187
|
const VERBOSE_OPT = opt("verbose", false, "every row's reasoning, each step's why and the host's own notes");
|
|
122
188
|
// doctor's --verbose is the same flag in its own words: on a terminal, the
|
|
123
189
|
// report the pipe gets — every note, every instance, each message whole.
|
|
124
|
-
const DOCTOR_VERBOSE_OPT = opt(
|
|
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
|
+
);
|
|
125
195
|
const INSTALL_OPTS = [HARNESS_OPT, SURFACE_OPT, NO_REGISTER_OPT, VERBOSE_OPT, JSON_OPT];
|
|
126
|
-
const UNINSTALL_OPTS = [
|
|
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
|
+
// setup's own options (the setup story, rule 1): seven, beside the globals.
|
|
205
|
+
// `--vault <path>` takes a value here where doctor's `--vault` is a flag —
|
|
206
|
+
// the per-verb parse's own case.
|
|
207
|
+
const SETUP_OPTS = [
|
|
208
|
+
HARNESS_OPT,
|
|
209
|
+
opt(
|
|
210
|
+
"vault",
|
|
211
|
+
"<path>",
|
|
212
|
+
"the vault: an existing folder is bound, a missing or empty one is created (naming it confirms)",
|
|
213
|
+
),
|
|
214
|
+
opt("layout", "<name>", `a new vault's layout (default ${DEFAULT_LAYOUT})`),
|
|
215
|
+
opt("language", "<code>", `a new vault's template language (default ${DEFAULT_LANGUAGE})`),
|
|
216
|
+
opt(
|
|
217
|
+
"channel",
|
|
218
|
+
"<npm|git>",
|
|
219
|
+
"with a git-marketplace copy enabled, without a terminal: npm switches to the npm install, git keeps the git marketplace",
|
|
220
|
+
),
|
|
221
|
+
opt("dry-run", false, "the questions and the plan; nothing is written or fetched"),
|
|
222
|
+
VERBOSE_OPT,
|
|
223
|
+
];
|
|
127
224
|
|
|
128
225
|
export const VERBS = Object.freeze([
|
|
129
226
|
Object.freeze({
|
|
130
|
-
verb: "
|
|
131
|
-
|
|
132
|
-
|
|
227
|
+
verb: "setup",
|
|
228
|
+
summary: "Set up this project: every harness it uses, and a vault.",
|
|
229
|
+
usage: NO_FORMS,
|
|
230
|
+
module: "./setup.mjs",
|
|
231
|
+
wraps: "new",
|
|
232
|
+
how: "import",
|
|
233
|
+
output: "envelope",
|
|
234
|
+
writes: true,
|
|
235
|
+
requiresBinding: false,
|
|
236
|
+
mcp: Object.freeze([]),
|
|
237
|
+
options: [...SETUP_OPTS, JSON_OPT],
|
|
238
|
+
run: runSetupVerb,
|
|
239
|
+
}),
|
|
240
|
+
Object.freeze({
|
|
241
|
+
verb: "doctor",
|
|
242
|
+
summary: "Check the install wiring and the vault's consistency.",
|
|
243
|
+
usage: NO_FORMS,
|
|
244
|
+
module: "./doctor.mjs",
|
|
245
|
+
wraps: "script",
|
|
246
|
+
how: "spawn",
|
|
247
|
+
output: "envelope",
|
|
248
|
+
writes: false,
|
|
249
|
+
requiresBinding: false,
|
|
250
|
+
mcp: Object.freeze(["doctor"]),
|
|
251
|
+
options: [
|
|
252
|
+
opt("install", false, "only the install section"),
|
|
253
|
+
opt("vault", false, "only the vault section"),
|
|
254
|
+
DOCTOR_VERBOSE_OPT,
|
|
255
|
+
JSON_OPT,
|
|
256
|
+
],
|
|
133
257
|
run: runDoctor,
|
|
134
258
|
}),
|
|
135
259
|
Object.freeze({
|
|
136
|
-
verb: "reconcile",
|
|
137
|
-
|
|
138
|
-
|
|
260
|
+
verb: "reconcile",
|
|
261
|
+
summary: "Regenerate the derived views (kanban, code map, graph, indexes).",
|
|
262
|
+
usage: NO_FORMS,
|
|
263
|
+
module: "./reconcile.mjs",
|
|
264
|
+
wraps: "module",
|
|
265
|
+
how: "import",
|
|
266
|
+
output: "envelope",
|
|
267
|
+
writes: true,
|
|
268
|
+
requiresBinding: true,
|
|
269
|
+
mcp: Object.freeze([]),
|
|
270
|
+
options: [
|
|
271
|
+
opt(
|
|
272
|
+
"write",
|
|
273
|
+
false,
|
|
274
|
+
"apply the regeneration (asks on a terminal; --only names what is written and confirms headless)",
|
|
275
|
+
),
|
|
276
|
+
opt("only", "<target>", "one derived view"),
|
|
277
|
+
JSON_OPT,
|
|
278
|
+
],
|
|
139
279
|
run: runReconcile,
|
|
140
280
|
}),
|
|
141
281
|
Object.freeze({
|
|
142
|
-
verb: "plan",
|
|
143
|
-
|
|
144
|
-
|
|
282
|
+
verb: "plan",
|
|
283
|
+
summary: "Show what install would write for a harness, without writing.",
|
|
284
|
+
usage: NO_FORMS,
|
|
285
|
+
module: "./install-harness.mjs",
|
|
286
|
+
wraps: "module",
|
|
287
|
+
how: "import",
|
|
288
|
+
output: "envelope",
|
|
289
|
+
writes: false,
|
|
290
|
+
requiresBinding: false,
|
|
291
|
+
mcp: Object.freeze([]),
|
|
292
|
+
options: INSTALL_OPTS,
|
|
293
|
+
run: runInstallVerb,
|
|
145
294
|
}),
|
|
146
295
|
Object.freeze({
|
|
147
|
-
verb: "install",
|
|
148
|
-
|
|
149
|
-
|
|
296
|
+
verb: "install",
|
|
297
|
+
summary: "Install projectstore's surfaces for a harness, behind a preview.",
|
|
298
|
+
usage: NO_FORMS,
|
|
299
|
+
module: "./install-harness.mjs",
|
|
300
|
+
wraps: "module",
|
|
301
|
+
how: "import",
|
|
302
|
+
output: "envelope",
|
|
303
|
+
writes: true,
|
|
304
|
+
requiresBinding: false,
|
|
305
|
+
mcp: Object.freeze([]),
|
|
306
|
+
options: INSTALL_OPTS,
|
|
307
|
+
run: runInstallVerb,
|
|
150
308
|
}),
|
|
151
309
|
Object.freeze({
|
|
152
|
-
verb: "uninstall",
|
|
153
|
-
|
|
154
|
-
|
|
310
|
+
verb: "uninstall",
|
|
311
|
+
summary: "Remove what install wrote, and only that.",
|
|
312
|
+
usage: NO_FORMS,
|
|
313
|
+
module: "./install-harness.mjs",
|
|
314
|
+
wraps: "module",
|
|
315
|
+
how: "import",
|
|
316
|
+
output: "envelope",
|
|
317
|
+
writes: true,
|
|
318
|
+
requiresBinding: false,
|
|
319
|
+
mcp: Object.freeze([]),
|
|
320
|
+
options: UNINSTALL_OPTS,
|
|
321
|
+
run: runInstallVerb,
|
|
155
322
|
}),
|
|
156
323
|
Object.freeze({
|
|
157
|
-
verb: "upgrade",
|
|
158
|
-
|
|
159
|
-
|
|
324
|
+
verb: "upgrade",
|
|
325
|
+
summary: "After an update, refresh what install wrote and leave the rest.",
|
|
326
|
+
usage: NO_FORMS,
|
|
327
|
+
module: "./install-harness.mjs",
|
|
328
|
+
wraps: "module",
|
|
329
|
+
how: "import",
|
|
330
|
+
output: "envelope",
|
|
331
|
+
writes: true,
|
|
332
|
+
requiresBinding: false,
|
|
333
|
+
mcp: Object.freeze([]),
|
|
334
|
+
options: INSTALL_OPTS,
|
|
335
|
+
run: runInstallVerb,
|
|
160
336
|
}),
|
|
161
337
|
Object.freeze({
|
|
162
|
-
verb: "status",
|
|
163
|
-
|
|
164
|
-
|
|
338
|
+
verb: "status",
|
|
339
|
+
summary: "What is bound, what is in progress, and whether views are fresh.",
|
|
340
|
+
usage: NO_FORMS,
|
|
341
|
+
module: "./query.mjs",
|
|
342
|
+
wraps: "new",
|
|
343
|
+
how: "import",
|
|
344
|
+
output: "envelope",
|
|
345
|
+
writes: false,
|
|
346
|
+
requiresBinding: false,
|
|
347
|
+
mcp: Object.freeze(["status"]),
|
|
348
|
+
options: READ_JSON,
|
|
349
|
+
run: runRead("status"),
|
|
165
350
|
}),
|
|
166
351
|
Object.freeze({
|
|
167
|
-
verb: "orientation",
|
|
168
|
-
|
|
169
|
-
|
|
352
|
+
verb: "orientation",
|
|
353
|
+
summary: "The SessionStart skeleton and the facts behind it.",
|
|
354
|
+
usage: NO_FORMS,
|
|
355
|
+
module: "./query.mjs",
|
|
356
|
+
wraps: "module",
|
|
357
|
+
how: "import",
|
|
358
|
+
output: "envelope",
|
|
359
|
+
writes: false,
|
|
360
|
+
requiresBinding: true,
|
|
361
|
+
mcp: Object.freeze(["orientation"]),
|
|
362
|
+
options: READ_JSON,
|
|
363
|
+
run: runRead("orientation"),
|
|
170
364
|
}),
|
|
171
365
|
Object.freeze({
|
|
172
|
-
verb: "search",
|
|
173
|
-
|
|
174
|
-
|
|
366
|
+
verb: "search",
|
|
367
|
+
summary: "Find a phrase in the vault's artifacts, literal and bounded.",
|
|
368
|
+
usage: Object.freeze(["<phrase…>"]),
|
|
369
|
+
module: "./query.mjs",
|
|
370
|
+
wraps: "new",
|
|
371
|
+
how: "import",
|
|
372
|
+
output: "envelope",
|
|
373
|
+
writes: false,
|
|
374
|
+
requiresBinding: true,
|
|
375
|
+
mcp: Object.freeze(["search"]),
|
|
376
|
+
options: [
|
|
377
|
+
opt("kind", "<type>", "only artifacts of this kind", true),
|
|
378
|
+
opt("status", "<status>", "only artifacts in this status"),
|
|
379
|
+
opt("limit", "<n>", `at most n matches (default ${SEARCH_DEFAULT_LIMIT}, hard cap 100)`),
|
|
380
|
+
opt("include-derived", false, "search the derived views too"),
|
|
381
|
+
opt("case-sensitive", false, "match case"),
|
|
382
|
+
JSON_OPT,
|
|
383
|
+
],
|
|
175
384
|
run: runRead("search"),
|
|
176
385
|
}),
|
|
177
386
|
Object.freeze({
|
|
178
|
-
verb: "show",
|
|
179
|
-
|
|
180
|
-
|
|
387
|
+
verb: "show",
|
|
388
|
+
summary: "One artifact's frontmatter; its body or a section on request.",
|
|
389
|
+
usage: Object.freeze(["<path>"]),
|
|
390
|
+
module: "./query.mjs",
|
|
391
|
+
wraps: "module",
|
|
392
|
+
how: "import",
|
|
393
|
+
output: "envelope",
|
|
394
|
+
writes: false,
|
|
395
|
+
requiresBinding: true,
|
|
396
|
+
mcp: Object.freeze(["get_artifact"]),
|
|
397
|
+
options: [
|
|
398
|
+
opt("body", false, "include the body"),
|
|
399
|
+
opt("section", "<id>", "one section by its registry id (description, acceptance, …)"),
|
|
400
|
+
JSON_OPT,
|
|
401
|
+
],
|
|
181
402
|
run: runRead("show"),
|
|
182
403
|
}),
|
|
183
404
|
Object.freeze({
|
|
184
|
-
verb: "graph",
|
|
185
|
-
|
|
186
|
-
|
|
405
|
+
verb: "graph",
|
|
406
|
+
summary: "The live link graph by vault path.",
|
|
407
|
+
usage: Object.freeze(["neighbors <path>", "lineage <path>"]),
|
|
408
|
+
module: "./query.mjs",
|
|
409
|
+
wraps: "new",
|
|
410
|
+
how: "import",
|
|
411
|
+
output: "envelope",
|
|
412
|
+
writes: false,
|
|
413
|
+
requiresBinding: true,
|
|
414
|
+
mcp: Object.freeze(["neighbors", "lineage"]),
|
|
415
|
+
options: [
|
|
416
|
+
opt("kind", "<edge-kind>", "only edges of this kind (lineage: one of its four)", true),
|
|
417
|
+
opt("direction", DIRECTIONS.join("|"), "neighbors: which edges"),
|
|
418
|
+
opt("depth", "<n>", `lineage: how far (default ${LINEAGE_DEFAULT_DEPTH})`),
|
|
419
|
+
opt("limit", "<n>", `neighbors: cap per direction (at most ${GRAPH_EDGE_CAP})`),
|
|
420
|
+
JSON_OPT,
|
|
421
|
+
],
|
|
187
422
|
run: runGraph,
|
|
188
423
|
}),
|
|
189
424
|
Object.freeze({
|
|
190
|
-
verb: "codemap",
|
|
191
|
-
|
|
192
|
-
|
|
425
|
+
verb: "codemap",
|
|
426
|
+
summary: "The code an epic or artifact maps to, or a path's artifacts.",
|
|
427
|
+
usage: Object.freeze(["--for <selector>"]),
|
|
428
|
+
module: "./query.mjs",
|
|
429
|
+
wraps: "new",
|
|
430
|
+
how: "import",
|
|
431
|
+
output: "envelope",
|
|
432
|
+
writes: false,
|
|
433
|
+
requiresBinding: true,
|
|
434
|
+
mcp: Object.freeze(["code_refs"]),
|
|
435
|
+
options: [
|
|
436
|
+
opt("for", "<selector>", "an epic id, an artifact, or a repo path"),
|
|
437
|
+
opt("reverse", false, "read the selector as a path even if it names an artifact"),
|
|
438
|
+
JSON_OPT,
|
|
439
|
+
],
|
|
193
440
|
run: runCodemap,
|
|
194
441
|
}),
|
|
195
442
|
Object.freeze({
|
|
196
|
-
verb: "agents",
|
|
197
|
-
|
|
198
|
-
|
|
443
|
+
verb: "agents",
|
|
444
|
+
summary: "The harness overlay's agents block: the model each agent runs.",
|
|
445
|
+
usage: Object.freeze(["model <name>", "show", "configure"]),
|
|
446
|
+
module: "./lib.mjs",
|
|
447
|
+
wraps: "new",
|
|
448
|
+
how: "import",
|
|
449
|
+
output: "envelope",
|
|
450
|
+
writes: true,
|
|
451
|
+
requiresBinding: false,
|
|
452
|
+
mcp: Object.freeze([]),
|
|
453
|
+
options: [
|
|
454
|
+
opt(
|
|
455
|
+
"harness",
|
|
456
|
+
"<id>",
|
|
457
|
+
"configure: the overlay to write — and, non-interactively, the confirmation; there is no --yes",
|
|
458
|
+
true,
|
|
459
|
+
),
|
|
460
|
+
opt(
|
|
461
|
+
"default",
|
|
462
|
+
"<model>",
|
|
463
|
+
"configure: agents.default.model (pins the clerk to sonnet unless --agent clerk=… says otherwise; an empty model clears it)",
|
|
464
|
+
),
|
|
465
|
+
opt("agent", "<name>=<model>", "configure: agents.per_agent.<name>.model (an empty model removes the key)", true),
|
|
466
|
+
opt("reset", false, "configure: empty the agents block first; --default and --agent given with it apply on top"),
|
|
467
|
+
JSON_OPT,
|
|
468
|
+
],
|
|
199
469
|
run: runAgents,
|
|
200
470
|
}),
|
|
201
471
|
Object.freeze({
|
|
202
|
-
verb: "band",
|
|
203
|
-
|
|
472
|
+
verb: "band",
|
|
473
|
+
summary: "The band above the prompt: switch it, pin an epic, ask why.",
|
|
474
|
+
usage: Object.freeze(["on", "off", "status", "epic <ID>"]),
|
|
475
|
+
module: "./band.mjs",
|
|
476
|
+
wraps: "new",
|
|
477
|
+
how: "import",
|
|
478
|
+
output: "envelope",
|
|
479
|
+
writes: true,
|
|
480
|
+
requiresBinding: true,
|
|
481
|
+
mcp: Object.freeze([]),
|
|
204
482
|
options: [JSON_OPT],
|
|
205
483
|
run: runBand,
|
|
206
484
|
}),
|
|
207
485
|
Object.freeze({
|
|
208
|
-
verb: "
|
|
209
|
-
|
|
210
|
-
|
|
486
|
+
verb: "session",
|
|
487
|
+
summary: "Refresh this session's current story from its file.",
|
|
488
|
+
usage: Object.freeze(["refresh <path>"]),
|
|
489
|
+
module: "./lib.mjs",
|
|
490
|
+
wraps: "new",
|
|
491
|
+
how: "import",
|
|
492
|
+
output: "envelope",
|
|
493
|
+
writes: true,
|
|
494
|
+
requiresBinding: true,
|
|
495
|
+
mcp: Object.freeze([]),
|
|
496
|
+
options: [JSON_OPT],
|
|
497
|
+
run: runSession,
|
|
498
|
+
}),
|
|
499
|
+
Object.freeze({
|
|
500
|
+
verb: "bind",
|
|
501
|
+
summary: "Bind this project to an existing vault; naming it confirms.",
|
|
502
|
+
usage: Object.freeze(["<vault>"]),
|
|
503
|
+
module: "./binding.mjs",
|
|
504
|
+
wraps: "new",
|
|
505
|
+
how: "import",
|
|
506
|
+
output: "envelope",
|
|
507
|
+
writes: true,
|
|
508
|
+
requiresBinding: false,
|
|
509
|
+
mcp: Object.freeze([]),
|
|
510
|
+
options: [
|
|
511
|
+
opt("layout", "<name>", `the layout (default ${DEFAULT_LAYOUT})`),
|
|
512
|
+
opt("language", "<code>", `the template language (default ${DEFAULT_LANGUAGE})`),
|
|
513
|
+
opt("rebind", false, "point an already bound project at another vault; every other setting is kept"),
|
|
514
|
+
JSON_OPT,
|
|
515
|
+
],
|
|
211
516
|
run: runBind(false),
|
|
212
517
|
}),
|
|
213
518
|
Object.freeze({
|
|
214
|
-
verb: "init",
|
|
215
|
-
|
|
216
|
-
|
|
519
|
+
verb: "init",
|
|
520
|
+
summary: "Create a vault (folder, git repository, READMEs) and bind to it.",
|
|
521
|
+
usage: Object.freeze(["<vault>"]),
|
|
522
|
+
module: "./binding.mjs",
|
|
523
|
+
wraps: "new",
|
|
524
|
+
how: "import",
|
|
525
|
+
output: "envelope",
|
|
526
|
+
writes: true,
|
|
527
|
+
requiresBinding: false,
|
|
528
|
+
mcp: Object.freeze([]),
|
|
529
|
+
options: [
|
|
530
|
+
opt("layout", "<name>", `the layout (default ${DEFAULT_LAYOUT})`),
|
|
531
|
+
opt("language", "<code>", `the template language (default ${DEFAULT_LANGUAGE})`),
|
|
532
|
+
opt(
|
|
533
|
+
"rebind",
|
|
534
|
+
false,
|
|
535
|
+
"an already bound project: create the new vault and point the project at it; every other setting is kept",
|
|
536
|
+
),
|
|
537
|
+
JSON_OPT,
|
|
538
|
+
],
|
|
217
539
|
run: runBind(true),
|
|
218
540
|
}),
|
|
219
541
|
Object.freeze({
|
|
220
|
-
verb: "scaffold",
|
|
221
|
-
|
|
222
|
-
|
|
542
|
+
verb: "scaffold",
|
|
543
|
+
summary: "Create the vault's missing folders and READMEs; rewrite nothing.",
|
|
544
|
+
usage: NO_FORMS,
|
|
545
|
+
module: "./scaffold.mjs",
|
|
546
|
+
wraps: "new",
|
|
547
|
+
how: "import",
|
|
548
|
+
output: "envelope",
|
|
549
|
+
writes: true,
|
|
550
|
+
requiresBinding: true,
|
|
551
|
+
mcp: Object.freeze([]),
|
|
552
|
+
options: [
|
|
553
|
+
opt("write", false, "create them (asks at a terminal; without one, --write is the confirmation)"),
|
|
554
|
+
JSON_OPT,
|
|
555
|
+
],
|
|
223
556
|
run: runScaffold,
|
|
224
557
|
}),
|
|
225
558
|
Object.freeze({
|
|
226
|
-
verb: "mcp",
|
|
227
|
-
|
|
228
|
-
|
|
559
|
+
verb: "mcp",
|
|
560
|
+
summary: "Serve the read tools over MCP (stdio) for the project you name.",
|
|
561
|
+
usage: NO_FORMS,
|
|
562
|
+
module: "./mcp.mjs",
|
|
563
|
+
wraps: "new",
|
|
564
|
+
how: "import",
|
|
565
|
+
output: "text",
|
|
566
|
+
writes: false,
|
|
567
|
+
requiresBinding: false,
|
|
568
|
+
mcp: Object.freeze([]),
|
|
569
|
+
options: [],
|
|
570
|
+
run: runMcp,
|
|
229
571
|
}),
|
|
230
572
|
Object.freeze({
|
|
231
|
-
verb: "version",
|
|
232
|
-
|
|
233
|
-
|
|
573
|
+
verb: "version",
|
|
574
|
+
summary: "Print the package version (also --version).",
|
|
575
|
+
usage: NO_FORMS,
|
|
576
|
+
module: null,
|
|
577
|
+
wraps: "new",
|
|
578
|
+
how: "import",
|
|
579
|
+
output: "envelope",
|
|
580
|
+
writes: false,
|
|
581
|
+
requiresBinding: false,
|
|
582
|
+
mcp: Object.freeze([]),
|
|
583
|
+
options: [JSON_OPT],
|
|
584
|
+
run: runVersion,
|
|
234
585
|
}),
|
|
235
586
|
]);
|
|
236
587
|
|
|
@@ -244,11 +595,11 @@ export const PLANNED_VERBS = Object.freeze([]);
|
|
|
244
595
|
// reading the vault, keeping it consistent. Every verb not listed here is
|
|
245
596
|
// ADVANCED, one line of names in table order — so a new row is never hidden
|
|
246
597
|
// by this table, and init and scaffold, which the contract names in no group,
|
|
247
|
-
// are there too. A group lists only the verbs this run's table has.
|
|
248
|
-
// setup
|
|
249
|
-
//
|
|
598
|
+
// are there too. A group lists only the verbs this run's table has. With
|
|
599
|
+
// setup, START is setup and uninstall, and install and bind are ADVANCED's
|
|
600
|
+
// (contract 10; the setup story, rule 1).
|
|
250
601
|
const HELP_GROUPS = [
|
|
251
|
-
["START", ["
|
|
602
|
+
["START", ["setup", "uninstall"]],
|
|
252
603
|
["VAULT", ["status", "search", "show", "graph", "codemap"]],
|
|
253
604
|
["CHECK AND REPAIR", ["doctor", "reconcile", "upgrade"]],
|
|
254
605
|
];
|
|
@@ -259,20 +610,41 @@ const HELP_GROUPS = [
|
|
|
259
610
|
const VERB_COLUMN = 12;
|
|
260
611
|
export const SUMMARY_MAX = 80 - (2 + VERB_COLUMN + 1);
|
|
261
612
|
|
|
262
|
-
// Examples per verb.
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
// a shell `
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
613
|
+
// Examples per verb. They are what to run next, so they are the core's form
|
|
614
|
+
// through a shell too (the story "User-facing text says where to run and
|
|
615
|
+
// names no project path and no shell", rule 6): `{cmd}` is the core through
|
|
616
|
+
// npx, and `{h}` the harness argument — ` --harness <id>` from the core, and
|
|
617
|
+
// through a shell ` --harness <the shell's own harness>` (shellHarnessId),
|
|
618
|
+
// never a session's identity. `{fixed}` is that argument through a shell
|
|
619
|
+
// only: the shell inserts --harness for every verb that declares it
|
|
620
|
+
// (packaging/shells.mjs, HARNESS_VERBS), so an example the core runs bare —
|
|
621
|
+
// setup, which asks; agents show, which reads the session's overlay — names
|
|
622
|
+
// the shell's harness through it and stays bare from the core. A `{core}`
|
|
623
|
+
// example is the core's alone: a shell's help leaves it out, since it says
|
|
624
|
+
// what the shell's own harness does not (every harness, the way back from a
|
|
625
|
+
// switch). `{switch.surface}`,
|
|
626
|
+
// `{switch.host}` and `{switch.doctor}` name the harness whose project can
|
|
627
|
+
// switch channels — the one with a host plugin registration — from its
|
|
628
|
+
// manifest: its surface key, its display name and its doctor form
|
|
629
|
+
// (switchHarness, below); an example naming them is left out when no
|
|
630
|
+
// manifest has one.
|
|
272
631
|
const EXAMPLES = {
|
|
632
|
+
setup: [
|
|
633
|
+
"{cmd} setup{fixed}",
|
|
634
|
+
"{cmd} setup{fixed} --dry-run # the questions and the plan; nothing is written or fetched",
|
|
635
|
+
"{cmd} setup{h} --vault ~/vaults/my-project # without a terminal: --harness confirms, --vault names the vault",
|
|
636
|
+
],
|
|
273
637
|
install: ["{cmd} install{h}", "{cmd} plan{h} # the same plan; nothing is written"],
|
|
274
|
-
upgrade: [
|
|
275
|
-
|
|
638
|
+
upgrade: [
|
|
639
|
+
"{cmd}@<version> upgrade{h} # the version you name is the version that runs",
|
|
640
|
+
"{cmd} upgrade{h} --verbose",
|
|
641
|
+
],
|
|
642
|
+
uninstall: [
|
|
643
|
+
"{core}{cmd} uninstall # every harness this project uses",
|
|
644
|
+
"{cmd} uninstall{h}",
|
|
645
|
+
"{cmd} uninstall{h} --surface statusline",
|
|
646
|
+
"{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",
|
|
647
|
+
],
|
|
276
648
|
plan: ["{cmd} plan{h}", "{cmd} plan{h} --json # one envelope, for scripts and agents"],
|
|
277
649
|
status: ["{cmd} status", "{cmd} status --json"],
|
|
278
650
|
orientation: ["{cmd} orientation --json"],
|
|
@@ -285,13 +657,20 @@ const EXAMPLES = {
|
|
|
285
657
|
bind: ["{cmd} bind ~/vaults/my-project", "{cmd} bind ~/vaults/other --rebind"],
|
|
286
658
|
init: ["{cmd} init ~/vaults/new-project --language ru"],
|
|
287
659
|
scaffold: ["{cmd} scaffold # what would be created; nothing is written", "{cmd} scaffold --write"],
|
|
288
|
-
agents: ["{cmd} agents show", "{cmd} agents configure{h} --default opus --agent clerk=sonnet"],
|
|
289
|
-
band: [
|
|
660
|
+
agents: ["{cmd} agents show{fixed}", "{cmd} agents configure{h} --default opus --agent clerk=sonnet"],
|
|
661
|
+
band: [
|
|
662
|
+
"{cmd} band status",
|
|
663
|
+
"{cmd} band off",
|
|
664
|
+
"{cmd} band epic PS-CORE # a session with no epic of its own shows this one",
|
|
665
|
+
],
|
|
666
|
+
session: ["{cmd} session refresh epics/PS-CORE/stories/story-x.md --json # after a write no hook saw"],
|
|
290
667
|
mcp: ['{cmd} mcp --project "$PWD"'],
|
|
291
668
|
};
|
|
292
669
|
|
|
293
|
-
// How this run was invoked is invokedAs (harness.mjs), the one spelling
|
|
294
|
-
//
|
|
670
|
+
// How this run was invoked is invokedAs (harness.mjs), the one spelling of
|
|
671
|
+
// the binary that ran: the help's first line, its Usage lines, the
|
|
672
|
+
// fixed-harness line and the usage errors. What a person runs next is the
|
|
673
|
+
// core's (CORE_NPX, packageCommand), as status's and bind's Next lines are.
|
|
295
674
|
|
|
296
675
|
// The harness whose project can switch channels (the converge story's rule
|
|
297
676
|
// 4): the one whose manifest declares a host plugin registration. Its surface
|
|
@@ -299,8 +678,15 @@ const EXAMPLES = {
|
|
|
299
678
|
// harness.mjs, never a literal; null when no manifest has one.
|
|
300
679
|
function switchHarness() {
|
|
301
680
|
for (const m of loadHarnesses().values()) {
|
|
302
|
-
const row = Object.entries(m.surfaces || {}).find(
|
|
303
|
-
|
|
681
|
+
const row = Object.entries(m.surfaces || {}).find(
|
|
682
|
+
([k, s]) => !k.startsWith("_") && s?.format === "host-plugin-registration",
|
|
683
|
+
);
|
|
684
|
+
if (row)
|
|
685
|
+
return {
|
|
686
|
+
"switch.surface": row[0],
|
|
687
|
+
"switch.host": m.display_name,
|
|
688
|
+
"switch.doctor": invocation(m, "doctor", { args: "--fix" }),
|
|
689
|
+
};
|
|
304
690
|
}
|
|
305
691
|
return null;
|
|
306
692
|
}
|
|
@@ -310,7 +696,10 @@ function switchHarness() {
|
|
|
310
696
|
// it. On a live terminal a description wraps with its column as the hanging
|
|
311
697
|
// indent; anywhere else it stays one line, whole, for a reader that greps.
|
|
312
698
|
function optionLines(options, c = term.PLAIN) {
|
|
313
|
-
const rows = options.map((o) => [
|
|
699
|
+
const rows = options.map((o) => [
|
|
700
|
+
term.glyphText(c, `--${o.name}${o.arg ? " " + o.arg : ""}`),
|
|
701
|
+
term.glyphText(c, `${o.summary}${o.multiple ? " (repeatable)" : ""}`),
|
|
702
|
+
]);
|
|
314
703
|
const w = Math.min(28, Math.max(0, ...rows.map(([l]) => l.length)));
|
|
315
704
|
const pad = " ".repeat(2 + w + 2);
|
|
316
705
|
const text = (r) => term.wrap(r, c.live ? c.width : 0, pad);
|
|
@@ -321,7 +710,8 @@ const EXIT_CODES = "0 ok, 1 findings or a refusal, 2 usage, 3 not bound, 130 can
|
|
|
321
710
|
// The facts that close a help screen (`Harnesses`, `Exit codes`): the key in
|
|
322
711
|
// the explanation role, its value at one column for both.
|
|
323
712
|
const FACT_KEY = "Exit codes".length;
|
|
324
|
-
const fact = (c, key, value) =>
|
|
713
|
+
const fact = (c, key, value) =>
|
|
714
|
+
`${term.painter(c)("explanation", key)}${" ".repeat(FACT_KEY - key.length + 2)}${value}`;
|
|
325
715
|
|
|
326
716
|
// What the bin is, in the help's badge: one line with the name and version,
|
|
327
717
|
// short enough that a prerelease version (0.30.0-rc.12) still fits 80 columns.
|
|
@@ -357,7 +747,10 @@ export function usage(env = process.env, verbs = VERBS, { caps: c = term.PLAIN,
|
|
|
357
747
|
term.badge(c, "projectstore", [version, PURPOSE]),
|
|
358
748
|
"",
|
|
359
749
|
term.heading(c, "Usage"),
|
|
360
|
-
...term.rows(c, [
|
|
750
|
+
...term.rows(c, [
|
|
751
|
+
{ target: `${cmd} <verb> [options]`, note: "run it inside your code project" },
|
|
752
|
+
{ target: `${cmd} <verb> --help`, note: "one verb's options and examples" },
|
|
753
|
+
]),
|
|
361
754
|
];
|
|
362
755
|
const listed = new Set(HELP_GROUPS.flatMap(([, names]) => names));
|
|
363
756
|
for (const [title, names] of HELP_GROUPS) {
|
|
@@ -369,24 +762,48 @@ export function usage(env = process.env, verbs = VERBS, { caps: c = term.PLAIN,
|
|
|
369
762
|
lines.push(lead + term.wrap(term.glyphText(c, v.summary), width, " ".repeat(term.plain(lead).length)));
|
|
370
763
|
}
|
|
371
764
|
}
|
|
765
|
+
// ADVANCED is one line of verbs, broken at 80 columns in every mode: with
|
|
766
|
+
// install and bind among them (contract 10, once setup exists) it no longer
|
|
767
|
+
// fits one row, and a pipe gets the same lines a terminal does.
|
|
372
768
|
const advanced = verbs.filter((v) => !listed.has(v.verb));
|
|
373
|
-
if (advanced.length)
|
|
769
|
+
if (advanced.length)
|
|
770
|
+
lines.push(
|
|
771
|
+
"",
|
|
772
|
+
term.heading(c, "ADVANCED"),
|
|
773
|
+
" " + term.wrap(advanced.map((v) => paint("action", v.verb)).join(` ${term.icon(c, "dot")} `), 80, " "),
|
|
774
|
+
);
|
|
374
775
|
if (PLANNED_VERBS.length) lines.push("", `Planned: ${PLANNED_VERBS.map((v) => `${v.verb} (${v.lands})`).join(", ")}`);
|
|
375
|
-
// A command is offered only for a verb this table has.
|
|
376
|
-
|
|
776
|
+
// A command is offered only for a verb this table has. The examples and the
|
|
777
|
+
// tips' plan are what to run next: the core's form, through a shell with
|
|
778
|
+
// the shell's own harness (rule 6 of the story "User-facing text says
|
|
779
|
+
// where to run…"); setup names it only through a shell, as EXAMPLES'
|
|
780
|
+
// `{fixed}` does.
|
|
781
|
+
const h = ` --harness ${(shell && shellHarnessId(env)) || "<id>"}`;
|
|
377
782
|
const has = (verb) => verbs.some((v) => v.verb === verb);
|
|
378
|
-
const examples = [
|
|
783
|
+
const examples = [
|
|
784
|
+
["setup", `${CORE_NPX} setup${shell ? h : ""}`],
|
|
785
|
+
["status", `${CORE_NPX} status`],
|
|
786
|
+
["doctor", `${CORE_NPX} doctor`],
|
|
787
|
+
]
|
|
788
|
+
.filter(([v]) => has(v))
|
|
789
|
+
.map(([, x]) => x);
|
|
379
790
|
lines.push("", term.heading(c, "Options"), ...optionLines(EVERY_VERB, c));
|
|
380
791
|
if (examples.length) lines.push("", term.heading(c, "Examples"), ...term.commandBlock(c, examples));
|
|
381
792
|
lines.push(
|
|
382
793
|
"",
|
|
383
794
|
term.heading(c, "Tips"),
|
|
384
|
-
prose("install, upgrade and uninstall show
|
|
385
|
-
prose(
|
|
795
|
+
prose("setup, install, upgrade and uninstall show a plan, then ask at a terminal."),
|
|
796
|
+
prose(
|
|
797
|
+
`Without a terminal, ${shell ? "the verb is its own" : "--harness is the"} confirmation ${term.icon(c, "dash")} there is no --yes.`,
|
|
798
|
+
),
|
|
386
799
|
prose("Unattended in a terminal: pass --json, set CI=1, or give stdin </dev/null."),
|
|
387
800
|
prose("--verbose on those verbs adds every row's reasoning and the host's notes."),
|
|
388
801
|
);
|
|
389
|
-
if (has("plan"))
|
|
802
|
+
if (has("plan"))
|
|
803
|
+
lines.push(
|
|
804
|
+
prose("plan prints the same plan and writes nothing:"),
|
|
805
|
+
...term.commandBlock(c, [`${CORE_NPX} plan${h}`]),
|
|
806
|
+
);
|
|
390
807
|
lines.push("", fact(c, "Harnesses", harnessIds().join(", ")), fact(c, "Exit codes", EXIT_CODES));
|
|
391
808
|
return lines.join("\n");
|
|
392
809
|
}
|
|
@@ -404,14 +821,26 @@ export function verbHelp(row, env = process.env, { caps: c = term.PLAIN } = {})
|
|
|
404
821
|
const prose = (text) => " " + term.wrap(term.glyphText(c, text), c.live ? c.width : 0, " ");
|
|
405
822
|
const head = `${paint("emphasis", `${cmd} ${row.verb}`)} ${term.icon(c, "dash")} ${term.glyphText(c, row.summary)}`;
|
|
406
823
|
const lines = [term.wrap(head, c.live ? c.width : 0, " "), "", term.heading(c, "Usage")];
|
|
407
|
-
for (const f of row.usage.length ? row.usage : [""])
|
|
824
|
+
for (const f of row.usage.length ? row.usage : [""])
|
|
825
|
+
lines.push(` ${cmd} ${row.verb}${f ? " " + term.glyphText(c, f) : ""}${opts.length ? " [options]" : ""}`);
|
|
408
826
|
if (NOTES[row.verb]) lines.push("", prose(NOTES[row.verb]));
|
|
409
827
|
if (opts.length) lines.push("", term.heading(c, "Options"), ...optionLines(opts, c));
|
|
410
|
-
if (fixed)
|
|
828
|
+
if (fixed)
|
|
829
|
+
lines.push(
|
|
830
|
+
"",
|
|
831
|
+
term.wrap(
|
|
832
|
+
`${shell} names the harness itself: without a terminal, the verb is its own confirmation.`,
|
|
833
|
+
c.live ? c.width : 0,
|
|
834
|
+
" ",
|
|
835
|
+
),
|
|
836
|
+
);
|
|
411
837
|
else if (row.options.some((o) => o.name === "harness")) lines.push("", fact(c, "Harnesses", harnessIds().join(", ")));
|
|
412
838
|
const sw = switchHarness();
|
|
413
|
-
const ex = (EXAMPLES[row.verb] || [])
|
|
414
|
-
|
|
839
|
+
const ex = (EXAMPLES[row.verb] || [])
|
|
840
|
+
.filter((e) => !(fixed && e.startsWith("{core}")) && (sw || !e.includes("{switch.")))
|
|
841
|
+
.map((e) => e.replace(/^\{core\}/, "").replace(/\{(switch\.\w+)\}/g, (_, k) => sw[k]));
|
|
842
|
+
// What to run next is the core's form: through a shell, its own harness.
|
|
843
|
+
const h = ` --harness ${(fixed && shellHarnessId(env)) || "<id>"}`;
|
|
415
844
|
// Each example command stands alone on its line, after a blank one
|
|
416
845
|
// (contract 6): nothing after it, so a paste takes exactly the command — a
|
|
417
846
|
// trailing `# note` broke a paste into a shell that reads no comments. A
|
|
@@ -419,13 +848,26 @@ export function verbHelp(row, env = process.env, { caps: c = term.PLAIN } = {})
|
|
|
419
848
|
// introduced command does (DONE's tip, doctor's fix, bind's Next);
|
|
420
849
|
// commands with no note share one block.
|
|
421
850
|
const blocks = [];
|
|
422
|
-
|
|
851
|
+
const render = (e) =>
|
|
852
|
+
e
|
|
853
|
+
.split("{cmd}")
|
|
854
|
+
.join(CORE_NPX)
|
|
855
|
+
.split("{h}")
|
|
856
|
+
.join(h)
|
|
857
|
+
.split("{fixed}")
|
|
858
|
+
.join(fixed ? h : "");
|
|
859
|
+
for (const [x, note] of ex.map((e) => render(e).split(" # "))) {
|
|
423
860
|
const last = blocks.at(-1);
|
|
424
861
|
if (!note && last && !last.note) last.cmds.push(x);
|
|
425
862
|
else blocks.push({ note: note ? `${note[0].toUpperCase()}${note.slice(1)}:` : null, cmds: [x] });
|
|
426
863
|
}
|
|
427
864
|
if (blocks.length) lines.push("", term.heading(c, "Examples"));
|
|
428
|
-
for (const b of blocks)
|
|
865
|
+
for (const b of blocks)
|
|
866
|
+
lines.push(
|
|
867
|
+
"",
|
|
868
|
+
...(b.note ? [prose(paint("explanation", b.note)), ""] : []),
|
|
869
|
+
...b.cmds.map((x) => ` ${paint("action", x)}`),
|
|
870
|
+
);
|
|
429
871
|
lines.push("", fact(c, "Exit codes", EXIT_CODES));
|
|
430
872
|
return lines.join("\n");
|
|
431
873
|
}
|
|
@@ -435,13 +877,19 @@ export function nearest(word, names) {
|
|
|
435
877
|
const d = (a, b) => {
|
|
436
878
|
const m = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
437
879
|
for (let j = 1; j <= b.length; j++) m[0][j] = j;
|
|
438
|
-
for (let i = 1; i <= a.length; i++)
|
|
880
|
+
for (let i = 1; i <= a.length; i++)
|
|
881
|
+
for (let j = 1; j <= b.length; j++)
|
|
882
|
+
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));
|
|
439
883
|
return m[a.length][b.length];
|
|
440
884
|
};
|
|
441
|
-
let best = null,
|
|
885
|
+
let best = null,
|
|
886
|
+
score = 3;
|
|
442
887
|
for (const n of names) {
|
|
443
888
|
const k = n.startsWith(word) && word.length >= 3 ? 0 : d(word, n);
|
|
444
|
-
if (k < score) {
|
|
889
|
+
if (k < score) {
|
|
890
|
+
best = n;
|
|
891
|
+
score = k;
|
|
892
|
+
}
|
|
445
893
|
}
|
|
446
894
|
return best;
|
|
447
895
|
}
|
|
@@ -469,7 +917,10 @@ function scanArgv(argv, verbs) {
|
|
|
469
917
|
for (let i = 0; i < argv.length; i++) {
|
|
470
918
|
const a = argv[i];
|
|
471
919
|
if (a === "--") {
|
|
472
|
-
if (scan.at === -1 && i + 1 < argv.length) {
|
|
920
|
+
if (scan.at === -1 && i + 1 < argv.length) {
|
|
921
|
+
scan.verb = argv[i + 1];
|
|
922
|
+
scan.at = i + 1;
|
|
923
|
+
}
|
|
473
924
|
break;
|
|
474
925
|
}
|
|
475
926
|
if (a.length > 1 && a.startsWith("-")) {
|
|
@@ -484,12 +935,22 @@ function scanArgv(argv, verbs) {
|
|
|
484
935
|
const name = a.startsWith("--") ? a.slice(2).split("=")[0] : null;
|
|
485
936
|
const inline = a.includes("=");
|
|
486
937
|
if (!inline && (name === "project" || (row && takesValue(row, name)))) i++;
|
|
487
|
-
else if (scan.at === -1 && !GLOBAL_OPTS.some((o) => o.name === name))
|
|
938
|
+
else if (scan.at === -1 && !GLOBAL_OPTS.some((o) => o.name === name))
|
|
939
|
+
scan.misplaced.push({ name, raw: a, at: i, inline, value: undefined });
|
|
488
940
|
continue;
|
|
489
941
|
}
|
|
490
942
|
if (scan.at !== -1) continue;
|
|
491
943
|
const prev = scan.misplaced[scan.misplaced.length - 1];
|
|
492
|
-
if (
|
|
944
|
+
if (
|
|
945
|
+
!verbs.some((v) => v.verb === a) &&
|
|
946
|
+
prev &&
|
|
947
|
+
prev.at === i - 1 &&
|
|
948
|
+
!prev.inline &&
|
|
949
|
+
verbs.some((v) => takesValue(v, prev.name))
|
|
950
|
+
) {
|
|
951
|
+
prev.value = a;
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
493
954
|
scan.verb = a;
|
|
494
955
|
scan.at = i;
|
|
495
956
|
row = verbs.find((v) => v.verb === a) || null;
|
|
@@ -504,7 +965,12 @@ const optionsOf = (row) => [...(row ? row.options : []), ...GLOBAL_OPTS];
|
|
|
504
965
|
// Rule 2: the row is the parser — arg is the type, multiple the repetition.
|
|
505
966
|
function parserOptions(row) {
|
|
506
967
|
const options = {};
|
|
507
|
-
for (const o of optionsOf(row))
|
|
968
|
+
for (const o of optionsOf(row))
|
|
969
|
+
options[o.name] = {
|
|
970
|
+
type: o.arg ? "string" : "boolean",
|
|
971
|
+
...(o.multiple ? { multiple: true } : {}),
|
|
972
|
+
...(SHORT[o.name] ? { short: SHORT[o.name] } : {}),
|
|
973
|
+
};
|
|
508
974
|
return options;
|
|
509
975
|
}
|
|
510
976
|
|
|
@@ -527,7 +993,10 @@ function unknownOption(row, raw, verbs) {
|
|
|
527
993
|
function parseFailure(e, row, verbs) {
|
|
528
994
|
const raw = e.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION" ? /'(-{1,2}[^']+)'/.exec(e.message)?.[1] : null;
|
|
529
995
|
if (raw) return unknownOption(row, raw, verbs);
|
|
530
|
-
const name =
|
|
996
|
+
const name =
|
|
997
|
+
e.code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE"
|
|
998
|
+
? /'(?:-[^-], )?(--[^' ]+)(?: <value>)?'/.exec(e.message)?.[1]
|
|
999
|
+
: null;
|
|
531
1000
|
const o = name ? optionsOf(row).findLast((x) => `--${x.name}` === name) : null;
|
|
532
1001
|
if (!o) return e.message;
|
|
533
1002
|
return o.arg ? `\`--${o.name}\` needs a value: \`--${o.name} ${o.arg}\`` : `\`--${o.name}\` takes no value`;
|
|
@@ -570,7 +1039,18 @@ function matchForm(row, args, values) {
|
|
|
570
1039
|
|
|
571
1040
|
// `verbs` is the table this run answers for: VERBS, or a test's own — the
|
|
572
1041
|
// scan, the help, the verb lookup and every hint read it, never VERBS behind it.
|
|
573
|
-
export async function run(
|
|
1042
|
+
export async function run(
|
|
1043
|
+
argv,
|
|
1044
|
+
{
|
|
1045
|
+
verbs = VERBS,
|
|
1046
|
+
env = process.env,
|
|
1047
|
+
cwd = process.cwd(),
|
|
1048
|
+
stdin = process.stdin,
|
|
1049
|
+
stdout = process.stdout,
|
|
1050
|
+
stderr = process.stderr,
|
|
1051
|
+
ask = null,
|
|
1052
|
+
} = {},
|
|
1053
|
+
) {
|
|
574
1054
|
const scan = scanArgv(argv, verbs);
|
|
575
1055
|
const row = verbs.find((v) => v.verb === scan.verb) || null;
|
|
576
1056
|
// A failure before a verb runs still answers in the envelope under --json:
|
|
@@ -578,7 +1058,8 @@ export async function run(argv, { verbs = VERBS, env = process.env, cwd = proces
|
|
|
578
1058
|
// verb is the scan's, whichever step failed (rule 7) — never the first
|
|
579
1059
|
// bare word, which may be --project's value.
|
|
580
1060
|
const fail = (message, code, { project = null, help = false } = {}) => {
|
|
581
|
-
if (scan.json)
|
|
1061
|
+
if (scan.json)
|
|
1062
|
+
stdout.write(JSON.stringify(envelope(scan.verb, project, false, { error: message, exit: code }), null, 2) + "\n");
|
|
582
1063
|
stderr.write(message + "\n" + (help ? usage(env, verbs, { caps: term.caps(stderr, env) }) + "\n" : ""));
|
|
583
1064
|
return code;
|
|
584
1065
|
};
|
|
@@ -608,7 +1089,11 @@ export async function run(argv, { verbs = VERBS, env = process.env, cwd = proces
|
|
|
608
1089
|
// as before — not "unknown verb: codex"; otherwise the globals alone are
|
|
609
1090
|
// parsed, so a bad value of one (`--project` last) is named.
|
|
610
1091
|
if (!scan.misplaced.length) {
|
|
611
|
-
try {
|
|
1092
|
+
try {
|
|
1093
|
+
parseArgs({ args: argv, options: parserOptions(null), strict: true, allowPositionals: true });
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
return refuse(parseFailure(e, null, verbs));
|
|
1096
|
+
}
|
|
612
1097
|
}
|
|
613
1098
|
stderr.write(usage(env, verbs, { caps: term.caps(stderr, env) }) + "\n");
|
|
614
1099
|
return 2;
|
|
@@ -616,9 +1101,18 @@ export async function run(argv, { verbs = VERBS, env = process.env, cwd = proces
|
|
|
616
1101
|
const verb = scan.verb;
|
|
617
1102
|
if (!row) {
|
|
618
1103
|
const planned = PLANNED_VERBS.find((v) => v.verb === verb);
|
|
619
|
-
const guess = planned
|
|
1104
|
+
const guess = planned
|
|
1105
|
+
? null
|
|
1106
|
+
: nearest(
|
|
1107
|
+
verb,
|
|
1108
|
+
verbs.map((v) => v.verb),
|
|
1109
|
+
);
|
|
620
1110
|
if (guess) return fail(`unknown verb: ${verb} — did you mean ${guess}?\nRun --help for every verb.`, 2);
|
|
621
|
-
return fail(
|
|
1111
|
+
return fail(
|
|
1112
|
+
planned ? `${verb} lands with roadmap ${planned.lands}; not in this release.` : `unknown verb: ${verb}`,
|
|
1113
|
+
2,
|
|
1114
|
+
{ help: true },
|
|
1115
|
+
);
|
|
622
1116
|
}
|
|
623
1117
|
// Rule 3: only the globals come before the verb (git's model). A verb's
|
|
624
1118
|
// own option there is shown in its place; one the verb does not declare is
|
|
@@ -633,7 +1127,9 @@ export async function run(argv, { verbs = VERBS, env = process.env, cwd = proces
|
|
|
633
1127
|
// The rest of the line follows, so the command shown is the one to run.
|
|
634
1128
|
const { shell, cmd } = invokedAs(env);
|
|
635
1129
|
const moved = shell && o.name === "harness" ? [] : [m.raw, ...(o.arg && m.value !== undefined ? [m.value] : [])];
|
|
636
|
-
return refuse(
|
|
1130
|
+
return refuse(
|
|
1131
|
+
`\`--${o.name}\` belongs after the verb: \`${[cmd, verb, ...moved, ...argv.slice(scan.at + 1)].join(" ")}\``,
|
|
1132
|
+
);
|
|
637
1133
|
}
|
|
638
1134
|
// Rule 2: one strict parse with this row's options plus the globals, on the
|
|
639
1135
|
// full argv so a token's index is the user's (rule 4's hints read them).
|
|
@@ -648,20 +1144,55 @@ export async function run(argv, { verbs = VERBS, env = process.env, cwd = proces
|
|
|
648
1144
|
// hint says how it became one: the flag before it takes no value, or a
|
|
649
1145
|
// "--" made everything after it an argument.
|
|
650
1146
|
const args = tokens.filter((t) => t.kind === "positional" && t.index !== scan.at);
|
|
651
|
-
const form = matchForm(
|
|
1147
|
+
const form = matchForm(
|
|
1148
|
+
row,
|
|
1149
|
+
args.map((t) => t.value),
|
|
1150
|
+
values,
|
|
1151
|
+
);
|
|
652
1152
|
if (form && args.length > form.bound) {
|
|
653
1153
|
const extra = args[form.bound];
|
|
654
1154
|
const prev = tokens.find((t) => t.index === extra.index - 1);
|
|
655
1155
|
const end = tokens.find((t) => t.kind === "option-terminator");
|
|
656
|
-
const why =
|
|
657
|
-
|
|
658
|
-
|
|
1156
|
+
const why =
|
|
1157
|
+
prev && prev.kind === "option" && prev.value === undefined
|
|
1158
|
+
? ` — \`--${prev.name}\` takes no value`
|
|
1159
|
+
: end && extra.index > end.index
|
|
1160
|
+
? " — everything after `--` is an argument"
|
|
1161
|
+
: "";
|
|
1162
|
+
return refuse(
|
|
1163
|
+
`${form.bound ? `\`${verb} ${form.form}\` takes no further argument` : `${verb} takes no argument`} \`${extra.value}\`${why}`,
|
|
1164
|
+
);
|
|
659
1165
|
}
|
|
660
1166
|
const project = resolveProject({ project: values.project, env, cwd });
|
|
661
1167
|
const cfg = readConfigAt(project);
|
|
662
|
-
|
|
1168
|
+
// Each form alone on its line after the lead, the terminal's in the core's
|
|
1169
|
+
// form, the directory in the lead (the story "User-facing text says where to
|
|
1170
|
+
// run and names no project path and no shell", rules 1, 4 and 6).
|
|
1171
|
+
if (row.requiresBinding && !cfg)
|
|
1172
|
+
return fail(
|
|
1173
|
+
[
|
|
1174
|
+
`${project} is not bound to a vault. Bind it in a session, or ${fromTerminalIn(project)}, where init creates the vault too:`,
|
|
1175
|
+
commandForm("bind", { args: "<vault>", env }),
|
|
1176
|
+
packageCommand([], "bind", { args: "<vault>" }),
|
|
1177
|
+
packageCommand([], "init", { args: "<vault>" }),
|
|
1178
|
+
].join("\n"),
|
|
1179
|
+
3,
|
|
1180
|
+
{ project },
|
|
1181
|
+
);
|
|
663
1182
|
try {
|
|
664
|
-
return await row.run({
|
|
1183
|
+
return await row.run({
|
|
1184
|
+
row,
|
|
1185
|
+
values,
|
|
1186
|
+
positionals: positionals.slice(1),
|
|
1187
|
+
cfg,
|
|
1188
|
+
project,
|
|
1189
|
+
env,
|
|
1190
|
+
cwd,
|
|
1191
|
+
stdin,
|
|
1192
|
+
stdout,
|
|
1193
|
+
stderr,
|
|
1194
|
+
ask,
|
|
1195
|
+
});
|
|
665
1196
|
} catch (e) {
|
|
666
1197
|
// An internal failure is not "findings" (1) — it is 2, with an envelope
|
|
667
1198
|
// when one was asked for.
|
|
@@ -716,8 +1247,16 @@ function runRead(name) {
|
|
|
716
1247
|
let result;
|
|
717
1248
|
if (name === "status") result = op.fn(cfg, { project });
|
|
718
1249
|
else if (name === "orientation") result = await op.fn(cfg);
|
|
719
|
-
else if (name === "search")
|
|
720
|
-
|
|
1250
|
+
else if (name === "search")
|
|
1251
|
+
result = op.fn(cfg, positionals.join(" "), {
|
|
1252
|
+
kinds: values.kind || null,
|
|
1253
|
+
status: values.status ?? null,
|
|
1254
|
+
limit: values.limit,
|
|
1255
|
+
includeDerived: Boolean(values["include-derived"]),
|
|
1256
|
+
caseSensitive: Boolean(values["case-sensitive"]),
|
|
1257
|
+
});
|
|
1258
|
+
else if (name === "show")
|
|
1259
|
+
result = op.fn(cfg, positionals[0], { body: Boolean(values.body), section: values.section ?? null });
|
|
721
1260
|
return emitRead({ verb: row.verb, ...ctx }, op, result);
|
|
722
1261
|
} catch (e) {
|
|
723
1262
|
if (e && e.code === "USAGE") return usageFail(e, { verb: row.verb, ...ctx });
|
|
@@ -758,9 +1297,22 @@ function runBind(init) {
|
|
|
758
1297
|
return async (ctx) => {
|
|
759
1298
|
const { row, values, positionals, project, env, stdout, stderr } = ctx;
|
|
760
1299
|
const vault = positionals[0];
|
|
761
|
-
if (!vault)
|
|
1300
|
+
if (!vault)
|
|
1301
|
+
return usageFail(Object.assign(new Error(`${row.verb} takes the vault path`), { code: "USAGE" }), {
|
|
1302
|
+
verb: row.verb,
|
|
1303
|
+
...ctx,
|
|
1304
|
+
});
|
|
762
1305
|
// The author is the caller's to find: the plan reads nothing ambient.
|
|
763
|
-
const plan = planBind(project, {
|
|
1306
|
+
const plan = planBind(project, {
|
|
1307
|
+
vault,
|
|
1308
|
+
layout: values.layout ?? null,
|
|
1309
|
+
language: values.language ?? null,
|
|
1310
|
+
rebind: Boolean(values.rebind),
|
|
1311
|
+
init,
|
|
1312
|
+
author: gitAuthor(project, env),
|
|
1313
|
+
env,
|
|
1314
|
+
root: PACKAGE_ROOT,
|
|
1315
|
+
});
|
|
764
1316
|
const usage = plan.refusals.find((r) => r.code === "USAGE");
|
|
765
1317
|
if (usage) return usageFail(Object.assign(new Error(usage.message), { code: "USAGE" }), { verb: row.verb, ...ctx });
|
|
766
1318
|
let done = null;
|
|
@@ -796,21 +1348,38 @@ async function runScaffold(ctx) {
|
|
|
796
1348
|
// readConfigAt hands back the stored path raw: `~`, relative, a trailing slash.
|
|
797
1349
|
const norm = normaliseVaultPath(cfg.vault_path, project);
|
|
798
1350
|
const vault = typeof norm === "string" ? norm : null;
|
|
1351
|
+
// Each refusal ends with its command alone on its line, in the core's form,
|
|
1352
|
+
// the directory in the lead (the story "User-facing text says where to
|
|
1353
|
+
// run…", rules 1, 4 and 6). In a session, bind asks before a rebind.
|
|
799
1354
|
if (!vault) {
|
|
800
|
-
const why =
|
|
1355
|
+
const why = [
|
|
1356
|
+
`the binding names no usable vault path${norm && norm.error ? ` (${norm.error})` : ""}. Rebind it in a session, or ${fromTerminalIn(project)}:`,
|
|
1357
|
+
commandForm("bind", { args: "<vault>", env }),
|
|
1358
|
+
packageCommand([], "bind", { args: "<vault> --rebind" }),
|
|
1359
|
+
].join("\n");
|
|
801
1360
|
return emit(false, { error: why }, why + "\n", 1);
|
|
802
1361
|
}
|
|
803
1362
|
if (!existsSync(vault)) {
|
|
804
|
-
const why =
|
|
1363
|
+
const why = [
|
|
1364
|
+
`${vault} does not exist: the binding names a vault directory that is not there. init creates it under this binding — the directory, its git repository and the layout's folders — ${fromTerminalIn(project)}:`,
|
|
1365
|
+
packageCommand([], "init", { args: shellQuote(vault) }),
|
|
1366
|
+
].join("\n");
|
|
805
1367
|
return emit(false, { error: why, vault_path: vault }, why + "\n", 1);
|
|
806
1368
|
}
|
|
807
|
-
const plan = planScaffold(vault, {
|
|
1369
|
+
const plan = planScaffold(vault, {
|
|
1370
|
+
layout: cfg.layout || DEFAULT_LAYOUT,
|
|
1371
|
+
language: cfg.language || DEFAULT_LANGUAGE,
|
|
1372
|
+
root: PACKAGE_ROOT,
|
|
1373
|
+
});
|
|
808
1374
|
if (!plan.ok) return emit(false, scaffoldResult(plan), renderScaffoldPlan(plan, null, { env }), 1);
|
|
809
|
-
if (!values.write || plan.creates === 0)
|
|
1375
|
+
if (!values.write || plan.creates === 0)
|
|
1376
|
+
return emit(true, scaffoldResult(plan), renderScaffoldPlan(plan, null, { env, project }), 0);
|
|
810
1377
|
// An injected `ask` is a terminal, as for install. Otherwise the installer's
|
|
811
1378
|
// definition of one (TTYs, CI, a host session's markers) is the one
|
|
812
1379
|
// contract 9 names — imported lazily, as runInstallVerb imports it.
|
|
813
|
-
const interactive =
|
|
1380
|
+
const interactive =
|
|
1381
|
+
!values.json &&
|
|
1382
|
+
(ask ? true : (await import("./install-harness.mjs")).isInteractive({ stdin, stdout, env, json: false }));
|
|
814
1383
|
if (interactive) {
|
|
815
1384
|
stdout.write(renderScaffoldPlan(plan, null, { env, asking: true }));
|
|
816
1385
|
if (!(await term.askApply(plan.creates, { stdin, stdout, ask, caps: term.caps(stdout, env) }))) {
|
|
@@ -834,21 +1403,32 @@ async function runAgents(ctx) {
|
|
|
834
1403
|
if (!["model", "show", "configure"].includes(sub)) return usage("agents takes model <name>, show, or configure");
|
|
835
1404
|
// The flag IS the confirmation, so it names exactly one overlay: two are a
|
|
836
1405
|
// question, not an answer.
|
|
837
|
-
if (values.harness && values.harness.length > 1)
|
|
1406
|
+
if (values.harness && values.harness.length > 1)
|
|
1407
|
+
return usage("--harness names exactly one overlay (it is the confirmation); given twice");
|
|
838
1408
|
const named = values.harness && values.harness[0];
|
|
839
|
-
if (named && !harnessIds().includes(named))
|
|
1409
|
+
if (named && !harnessIds().includes(named))
|
|
1410
|
+
return usage(`unknown harness: ${named} — known: ${harnessIds().join(", ")}`);
|
|
840
1411
|
const harness = named || overlayId(env);
|
|
841
1412
|
const forConfigure = ["default", "agent", "reset"].filter((o) => values[o] !== undefined);
|
|
842
1413
|
if (sub !== "configure" && forConfigure.length) return usage(`--${forConfigure[0]} is an option of agents configure`);
|
|
843
1414
|
// A refusal goes to stderr, as bind's does; --json keeps its envelope on stdout.
|
|
844
|
-
const emit = (verb, ok, result, text) => {
|
|
1415
|
+
const emit = (verb, ok, result, text) => {
|
|
1416
|
+
if (values.json) stdout.write(JSON.stringify(envelope(verb, project, ok, result), null, 2) + "\n");
|
|
1417
|
+
else (ok ? stdout : stderr).write(text);
|
|
1418
|
+
return ok ? 0 : 1;
|
|
1419
|
+
};
|
|
845
1420
|
const cfg = readConfigAt(project);
|
|
846
1421
|
const roster = layoutRoster(cfg);
|
|
847
1422
|
if (sub === "model") {
|
|
848
1423
|
const name = positionals[1];
|
|
849
1424
|
if (!name) return usage("agents model takes the agent's bare name (critic, planner, …)");
|
|
850
1425
|
const r = resolveAgentModel(project, name, { harness });
|
|
851
|
-
return emit(
|
|
1426
|
+
return emit(
|
|
1427
|
+
"agents model",
|
|
1428
|
+
true,
|
|
1429
|
+
r,
|
|
1430
|
+
`${name}: ${r.model ? `${r.model} (${r.source}, ${r.overlay})` : "no model configured — the agent's frontmatter decides"}\n`,
|
|
1431
|
+
);
|
|
852
1432
|
}
|
|
853
1433
|
const before = readOverlayAt(project, harness);
|
|
854
1434
|
const inBinding = Boolean(cfg && cfg.agents && typeof cfg.agents === "object");
|
|
@@ -856,34 +1436,72 @@ async function runAgents(ctx) {
|
|
|
856
1436
|
// Resolved per roster agent (per configured name when no layout loads), so
|
|
857
1437
|
// a reader reports what would run without restating the two-term rule.
|
|
858
1438
|
const names = [...new Set([...(roster || []), ...Object.keys(before.agents.per_agent)])].sort();
|
|
859
|
-
const resolved = Object.fromEntries(
|
|
1439
|
+
const resolved = Object.fromEntries(
|
|
1440
|
+
names.map((n) => {
|
|
1441
|
+
const r = resolveAgentModel(project, n, { harness });
|
|
1442
|
+
return [n, { model: r.model, source: r.source }];
|
|
1443
|
+
}),
|
|
1444
|
+
);
|
|
860
1445
|
const unknown = roster ? Object.keys(before.agents.per_agent).filter((n) => !roster.includes(n)) : [];
|
|
861
|
-
const lines = [
|
|
1446
|
+
const lines = [
|
|
1447
|
+
`overlay: ${before.path || "(none: no harness detected and none named)"}${before.path && !before.present ? " (absent)" : ""}`,
|
|
1448
|
+
];
|
|
862
1449
|
if (before.unparseable) lines.push(" not valid JSON");
|
|
863
1450
|
if (before.agents.default) lines.push(` default: ${before.agents.default}`);
|
|
864
|
-
for (const n of names)
|
|
1451
|
+
for (const n of names)
|
|
1452
|
+
lines.push(
|
|
1453
|
+
` ${n}: ${resolved[n].model ? `${resolved[n].model} (${resolved[n].source})` : "— (the agent's frontmatter)"}${unknown.includes(n) ? " — not in the roster: nothing runs under this name" : ""}`,
|
|
1454
|
+
);
|
|
865
1455
|
if (before.rejected.length) lines.push(` ignored (not an overlay key): ${before.rejected.join(", ")}`);
|
|
866
1456
|
if (inBinding) lines.push(" the binding still carries an agents block — a pre-0.28 leftover; run upgrade");
|
|
867
|
-
return emit(
|
|
1457
|
+
return emit(
|
|
1458
|
+
"agents show",
|
|
1459
|
+
true,
|
|
1460
|
+
{
|
|
1461
|
+
harness,
|
|
1462
|
+
path: before.path,
|
|
1463
|
+
present: before.present,
|
|
1464
|
+
unparseable: before.unparseable,
|
|
1465
|
+
agents: before.agents,
|
|
1466
|
+
resolved,
|
|
1467
|
+
roster,
|
|
1468
|
+
unknown,
|
|
1469
|
+
rejected: before.rejected,
|
|
1470
|
+
agents_in_binding: inBinding,
|
|
1471
|
+
},
|
|
1472
|
+
lines.join("\n") + "\n",
|
|
1473
|
+
);
|
|
868
1474
|
}
|
|
869
1475
|
// configure — in a project that exists (the install spec, contract 19;
|
|
870
1476
|
// the layout spec, contract 4): refused before the preview and the gate.
|
|
871
1477
|
const missingProject = projectDirRefusal(project);
|
|
872
1478
|
if (missingProject) return emit("agents configure", false, { error: missingProject }, `${missingProject}\n`);
|
|
873
1479
|
if (!harness) return usage("no harness detected and none named: agents configure names --harness <id>");
|
|
874
|
-
if (before.unparseable)
|
|
875
|
-
|
|
1480
|
+
if (before.unparseable)
|
|
1481
|
+
return emit(
|
|
1482
|
+
"agents configure",
|
|
1483
|
+
false,
|
|
1484
|
+
{ error: `${before.path} is not valid JSON; fix or remove it first`, harness, path: before.path },
|
|
1485
|
+
`${before.path} is not valid JSON; fix or remove it first\n`,
|
|
1486
|
+
);
|
|
1487
|
+
if (!values.reset && values.default === undefined && !(values.agent || []).length)
|
|
1488
|
+
return usage("agents configure takes --default <model>, --agent <name>=<model> (repeatable) or --reset");
|
|
876
1489
|
// --reset empties the block first; --default and --agent then apply on top of
|
|
877
1490
|
// the emptied block — "reset, then set" is one call, and nothing given is
|
|
878
1491
|
// silently dropped.
|
|
879
1492
|
const base = values.reset ? { default: null, per_agent: {} } : before.agents;
|
|
880
|
-
const next = {
|
|
1493
|
+
const next = {
|
|
1494
|
+
default: values.default !== undefined ? values.default || null : base.default,
|
|
1495
|
+
per_agent: { ...base.per_agent },
|
|
1496
|
+
};
|
|
881
1497
|
for (const spec of values.agent || []) {
|
|
882
1498
|
const m = /^([a-z][a-z0-9-]*)=(.*)$/.exec(spec);
|
|
883
1499
|
if (!m) return usage(`--agent takes <name>=<model>, got ${spec}`);
|
|
884
1500
|
// A name outside the roster would be written, listed, and never run.
|
|
885
|
-
if (roster && !roster.includes(m[1]))
|
|
886
|
-
|
|
1501
|
+
if (roster && !roster.includes(m[1]))
|
|
1502
|
+
return usage(`no agent named ${m[1]} in the ${cfg.layout} roster — known: ${roster.join(", ")}`);
|
|
1503
|
+
if (m[2]) next.per_agent[m[1]] = m[2];
|
|
1504
|
+
else delete next.per_agent[m[1]];
|
|
887
1505
|
}
|
|
888
1506
|
// The clerk transcribes; a strong default must not lift it (commands/agents.md,
|
|
889
1507
|
// ADR-008). WHICH model it is pinned to is the harness's vocabulary, so the
|
|
@@ -902,29 +1520,77 @@ async function runAgents(ctx) {
|
|
|
902
1520
|
const clerkAsked = (values.agent || []).some((s) => s.startsWith("clerk="));
|
|
903
1521
|
if (next.default && !next.per_agent.clerk && !clerkAsked) {
|
|
904
1522
|
const cheap = harnessForOverlay(harness)?.agent_translation?.cheap_model || null;
|
|
905
|
-
if (cheap) {
|
|
906
|
-
|
|
1523
|
+
if (cheap) {
|
|
1524
|
+
next.per_agent.clerk = cheap;
|
|
1525
|
+
pinnedClerk = true;
|
|
1526
|
+
} else clerkPinUnavailable = true;
|
|
907
1527
|
}
|
|
908
1528
|
// Keys inside the agents block that the allowlist rejected (an `effort`, a
|
|
909
1529
|
// stray shape) are rewritten out by the write and announced here; keys
|
|
910
1530
|
// outside the block are kept, and stay doctor's to name.
|
|
911
1531
|
const dropped = before.rejected.filter((k) => k.startsWith("agents."));
|
|
912
|
-
const same =
|
|
913
|
-
|
|
1532
|
+
const same =
|
|
1533
|
+
!dropped.length &&
|
|
1534
|
+
JSON.stringify({ d: before.agents.default, p: before.agents.per_agent }) ===
|
|
1535
|
+
JSON.stringify({ d: next.default, p: next.per_agent });
|
|
1536
|
+
const preview = [
|
|
1537
|
+
`agents configure — ${harness} — ${before.path}`,
|
|
1538
|
+
` default: ${before.agents.default || "—"} → ${next.default || "—"}`,
|
|
1539
|
+
];
|
|
914
1540
|
const names = new Set([...Object.keys(before.agents.per_agent), ...Object.keys(next.per_agent)]);
|
|
915
|
-
for (const n of [...names].sort())
|
|
1541
|
+
for (const n of [...names].sort())
|
|
1542
|
+
preview.push(
|
|
1543
|
+
` ${n}: ${before.agents.per_agent[n] || "—"} → ${next.per_agent[n] || "—"}${pinnedClerk && n === "clerk" ? " (pinned: the clerk stays cheap under a strong default)" : ""}`,
|
|
1544
|
+
);
|
|
916
1545
|
// Silence here would read as "the clerk is fine", and it is not: it takes the
|
|
917
1546
|
// strong default, which is the one thing ADR-008 says must not happen to it.
|
|
918
|
-
if (clerkPinUnavailable)
|
|
919
|
-
|
|
920
|
-
|
|
1547
|
+
if (clerkPinUnavailable)
|
|
1548
|
+
preview.push(
|
|
1549
|
+
` clerk: takes the default — ${harness} declares no cheap model to pin it to, so name one with --agent clerk=<model>`,
|
|
1550
|
+
);
|
|
1551
|
+
if (dropped.length)
|
|
1552
|
+
preview.push(` drops from the agents block (an overlay carries only models): ${dropped.join(", ")}`);
|
|
1553
|
+
if (same)
|
|
1554
|
+
return emit(
|
|
1555
|
+
"agents configure",
|
|
1556
|
+
true,
|
|
1557
|
+
{ harness, path: before.path, wrote: false, agents: next, pinnedClerk, clerkPinUnavailable, dropped },
|
|
1558
|
+
preview.join("\n") + "\n Nothing to change.\n",
|
|
1559
|
+
);
|
|
921
1560
|
// The gate: a named harness confirms; otherwise ask, and refuse without a terminal.
|
|
922
1561
|
let confirmed = Boolean(named);
|
|
923
|
-
if (!confirmed) {
|
|
924
|
-
|
|
1562
|
+
if (!confirmed) {
|
|
1563
|
+
const a = await confirmWrite(preview.join("\n") + `\nWrite ${before.path}? [y/N] `, { stdin, stdout, ask });
|
|
1564
|
+
if (a === null)
|
|
1565
|
+
return usage("a non-interactive agents configure names --harness <id> to confirm; there is no --yes");
|
|
1566
|
+
confirmed = a;
|
|
1567
|
+
}
|
|
1568
|
+
if (!confirmed)
|
|
1569
|
+
return emit(
|
|
1570
|
+
"agents configure",
|
|
1571
|
+
false,
|
|
1572
|
+
{ harness, path: before.path, wrote: false, declined: true, agents: next, dropped },
|
|
1573
|
+
preview.join("\n") + "\n nothing written.\n",
|
|
1574
|
+
);
|
|
925
1575
|
const path = writeOverlayAt(project, harness, next);
|
|
926
1576
|
const after = readOverlayAt(project, harness);
|
|
927
|
-
return emit(
|
|
1577
|
+
return emit(
|
|
1578
|
+
"agents configure",
|
|
1579
|
+
true,
|
|
1580
|
+
{
|
|
1581
|
+
harness,
|
|
1582
|
+
path,
|
|
1583
|
+
wrote: true,
|
|
1584
|
+
agents: after.agents,
|
|
1585
|
+
pinnedClerk,
|
|
1586
|
+
clerkPinUnavailable,
|
|
1587
|
+
dropped,
|
|
1588
|
+
rejected: after.rejected,
|
|
1589
|
+
agents_in_binding: inBinding,
|
|
1590
|
+
},
|
|
1591
|
+
preview.join("\n") +
|
|
1592
|
+
`\n wrote ${path}.${inBinding ? " The binding still carries an agents block (pre-0.28); run upgrade to move it." : ""}\n`,
|
|
1593
|
+
);
|
|
928
1594
|
}
|
|
929
1595
|
|
|
930
1596
|
// The band's switch (the band spec, contract 13): one atomic binding write per
|
|
@@ -936,41 +1602,107 @@ async function runBand(ctx) {
|
|
|
936
1602
|
const sub = positionals[0];
|
|
937
1603
|
const arg = positionals[1];
|
|
938
1604
|
const usage = (m) => usageFail(Object.assign(new Error(m), { code: "USAGE" }), { verb: row.verb, ...ctx });
|
|
939
|
-
if (!BAND_SUBCOMMANDS.includes(sub))
|
|
940
|
-
|
|
1605
|
+
if (!BAND_SUBCOMMANDS.includes(sub))
|
|
1606
|
+
return usage(`band takes ${BAND_SUBCOMMANDS.join(", ")} (epic takes an epic id, or none to release the pin)`);
|
|
1607
|
+
const emit = (verb, ok, result, text) => {
|
|
1608
|
+
if (values.json) stdout.write(JSON.stringify(envelope(verb, project, ok, result), null, 2) + "\n");
|
|
1609
|
+
else (ok ? stdout : stderr).write(text);
|
|
1610
|
+
return ok ? 0 : 1;
|
|
1611
|
+
};
|
|
941
1612
|
if (sub === "status") {
|
|
942
1613
|
const s = bandStatus(project, cfg, { env, root: PACKAGE_ROOT });
|
|
943
1614
|
return emit("band status", true, s, renderBandStatus(s));
|
|
944
1615
|
}
|
|
945
1616
|
const plan = planBand(project, sub, arg, { env, root: PACKAGE_ROOT });
|
|
946
|
-
if (plan.refusal)
|
|
1617
|
+
if (plan.refusal)
|
|
1618
|
+
return emit(
|
|
1619
|
+
`band ${sub}`,
|
|
1620
|
+
false,
|
|
1621
|
+
{ error: plan.refusal.message, code: plan.refusal.code, path: plan.path },
|
|
1622
|
+
`${plan.refusal.message}\n`,
|
|
1623
|
+
);
|
|
947
1624
|
const preview = renderBandPlan(plan, sub, arg);
|
|
948
|
-
if (!plan.changed)
|
|
1625
|
+
if (!plan.changed)
|
|
1626
|
+
return emit(
|
|
1627
|
+
`band ${sub}`,
|
|
1628
|
+
true,
|
|
1629
|
+
{ path: plan.path, wrote: false, band: plan.after },
|
|
1630
|
+
preview + " Nothing to change.\n",
|
|
1631
|
+
);
|
|
949
1632
|
let confirmed = Boolean(values.json);
|
|
950
1633
|
if (!confirmed) {
|
|
951
1634
|
const a = await confirmWrite(preview + `Write ${plan.path}? [y/N] `, { stdin, stdout, ask });
|
|
952
1635
|
if (a === null) return usage("a non-interactive band write passes --json; there is no --yes");
|
|
953
1636
|
confirmed = a;
|
|
954
1637
|
}
|
|
955
|
-
if (!confirmed)
|
|
1638
|
+
if (!confirmed)
|
|
1639
|
+
return emit(
|
|
1640
|
+
`band ${sub}`,
|
|
1641
|
+
false,
|
|
1642
|
+
{ path: plan.path, wrote: false, declined: true, band: plan.after },
|
|
1643
|
+
preview + " nothing written.\n",
|
|
1644
|
+
);
|
|
956
1645
|
const done = applyBand(plan);
|
|
957
|
-
return emit(
|
|
1646
|
+
return emit(
|
|
1647
|
+
`band ${sub}`,
|
|
1648
|
+
true,
|
|
1649
|
+
{ path: plan.path, wrote: done.wrote, band: plan.after },
|
|
1650
|
+
preview + ` wrote ${plan.path}. The band follows within a few seconds; no restart.\n`,
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// `session refresh <path>`: this session's pointer from a story file, after a
|
|
1655
|
+
// write no hook saw (the close ceremony's copy). Machine-local state, like the
|
|
1656
|
+
// hooks' own writes — no confirmation. No session: nothing written, exit 0.
|
|
1657
|
+
async function runSession(ctx) {
|
|
1658
|
+
const { row, values, positionals, cfg, project, env, stdout, stderr } = ctx;
|
|
1659
|
+
if (positionals[0] !== "refresh" || !positionals[1] || positionals.length > 2)
|
|
1660
|
+
return usageFail(
|
|
1661
|
+
Object.assign(new Error("session takes refresh <path> (a story file in the vault)"), { code: "USAGE" }),
|
|
1662
|
+
{
|
|
1663
|
+
verb: row.verb,
|
|
1664
|
+
...ctx,
|
|
1665
|
+
},
|
|
1666
|
+
);
|
|
1667
|
+
const r = refreshSessionPointer(project, cfg, positionals[1], { env });
|
|
1668
|
+
const ok = !r.refusal;
|
|
1669
|
+
const result = ok ? r : { error: r.refusal.message, code: r.refusal.code };
|
|
1670
|
+
if (values.json) stdout.write(JSON.stringify(envelope("session refresh", project, ok, result), null, 2) + "\n");
|
|
1671
|
+
else if (!ok) stderr.write(`${r.refusal.message}\n`);
|
|
1672
|
+
else if (!r.refreshed) stdout.write("No session id from the host: nothing written.\n");
|
|
1673
|
+
else {
|
|
1674
|
+
const p = r.pointer;
|
|
1675
|
+
stdout.write(
|
|
1676
|
+
`This session's story: ${p.story_title || p.active_story}${p.story_status ? ` (${p.story_status})` : ""}.\n`,
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
return ok ? 0 : 1;
|
|
958
1680
|
}
|
|
959
1681
|
|
|
960
1682
|
async function runGraph(ctx) {
|
|
961
1683
|
const { row, values, positionals, cfg } = ctx;
|
|
962
1684
|
const sub = positionals[0];
|
|
963
1685
|
const path = positionals[1];
|
|
964
|
-
if (!["neighbors", "lineage"].includes(sub))
|
|
1686
|
+
if (!["neighbors", "lineage"].includes(sub))
|
|
1687
|
+
return usageFail(Object.assign(new Error("graph takes neighbors <path> or lineage <path>"), { code: "USAGE" }), {
|
|
1688
|
+
verb: row.verb,
|
|
1689
|
+
...ctx,
|
|
1690
|
+
});
|
|
965
1691
|
const op = READ_OPERATIONS[sub];
|
|
966
1692
|
try {
|
|
967
1693
|
// The row declares the union; each mode takes its own — an option the
|
|
968
1694
|
// mode would ignore is a usage error, not a silent no-op.
|
|
969
1695
|
const notFor = sub === "neighbors" ? ["depth"] : ["direction", "limit"];
|
|
970
|
-
for (const o of notFor)
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1696
|
+
for (const o of notFor)
|
|
1697
|
+
if (values[o] !== undefined)
|
|
1698
|
+
throw Object.assign(new Error(`--${o} is not an option of graph ${sub}`), { code: "USAGE" });
|
|
1699
|
+
const result =
|
|
1700
|
+
sub === "neighbors"
|
|
1701
|
+
? op.fn(cfg, path, { kinds: values.kind || null, direction: values.direction, limit: values.limit })
|
|
1702
|
+
: op.fn(cfg, path, {
|
|
1703
|
+
depth: values.depth,
|
|
1704
|
+
kinds: values.kind && values.kind.length ? values.kind : LINEAGE_KINDS,
|
|
1705
|
+
});
|
|
974
1706
|
return emitRead({ verb: `graph ${sub}`, ...ctx }, op, result);
|
|
975
1707
|
} catch (e) {
|
|
976
1708
|
if (e && e.code === "USAGE") return usageFail(e, { verb: row.verb, ...ctx });
|
|
@@ -982,7 +1714,10 @@ async function runCodemap(ctx) {
|
|
|
982
1714
|
const { row, values, cfg } = ctx;
|
|
983
1715
|
const op = READ_OPERATIONS.codeRefs;
|
|
984
1716
|
try {
|
|
985
|
-
if (!values.for)
|
|
1717
|
+
if (!values.for)
|
|
1718
|
+
throw Object.assign(new Error("codemap takes --for <selector>; regeneration is reconcile --only codemap"), {
|
|
1719
|
+
code: "USAGE",
|
|
1720
|
+
});
|
|
986
1721
|
return emitRead({ verb: row.verb, ...ctx }, op, op.fn(cfg, values.for, { reverse: Boolean(values.reverse) }));
|
|
987
1722
|
} catch (e) {
|
|
988
1723
|
if (e && e.code === "USAGE") return usageFail(e, { verb: row.verb, ...ctx });
|
|
@@ -992,14 +1727,22 @@ async function runCodemap(ctx) {
|
|
|
992
1727
|
|
|
993
1728
|
function runVersion({ values, stdout }) {
|
|
994
1729
|
const version = packageVersion();
|
|
995
|
-
stdout.write(
|
|
1730
|
+
stdout.write(
|
|
1731
|
+
values.json ? JSON.stringify(envelope("version", null, true, { version }), null, 2) + "\n" : version + "\n",
|
|
1732
|
+
);
|
|
996
1733
|
return 0;
|
|
997
1734
|
}
|
|
998
1735
|
|
|
999
1736
|
function spawnDoctor(project, env, flags) {
|
|
1000
1737
|
// A project that does not exist is still a project doctor can report on
|
|
1001
1738
|
// ("not bound") — so the cwd is only set when it can be.
|
|
1002
|
-
return spawnSync(process.execPath, [script("doctor.mjs"), ...flags], {
|
|
1739
|
+
return spawnSync(process.execPath, [script("doctor.mjs"), ...flags], {
|
|
1740
|
+
encoding: "utf8",
|
|
1741
|
+
cwd: existsSync(project) ? project : undefined,
|
|
1742
|
+
env: ownEnv(env, project),
|
|
1743
|
+
timeout: 60000,
|
|
1744
|
+
maxBuffer: 1 << 24,
|
|
1745
|
+
});
|
|
1003
1746
|
}
|
|
1004
1747
|
|
|
1005
1748
|
// Spawned, never imported (MCP ADR decision 2): doctor's main writes to
|
|
@@ -1021,13 +1764,27 @@ async function runDoctor({ values, project, env, stdout, stderr }) {
|
|
|
1021
1764
|
if (r.error) return fail(r.error.message);
|
|
1022
1765
|
if (r.status !== 0 && !r.stdout) return fail((r.stderr || "").trim() || `exit ${r.status}`);
|
|
1023
1766
|
let findings;
|
|
1024
|
-
try {
|
|
1767
|
+
try {
|
|
1768
|
+
findings = JSON.parse(r.stdout);
|
|
1769
|
+
} catch {
|
|
1770
|
+
return fail("unparseable output");
|
|
1771
|
+
}
|
|
1025
1772
|
const ok = !findings.some((f) => f.level === "issue");
|
|
1026
1773
|
if (values.json) stdout.write(JSON.stringify(envelope("doctor", project, ok, findings), null, 2) + "\n");
|
|
1027
1774
|
else {
|
|
1028
1775
|
// The sections as doctor.mjs derives them: no flag is both.
|
|
1029
|
-
const groups = sections.length
|
|
1030
|
-
|
|
1776
|
+
const groups = sections.length
|
|
1777
|
+
? [values.install && "install", values.vault && "vault"].filter(Boolean)
|
|
1778
|
+
: ["install", "vault"];
|
|
1779
|
+
stdout.write(
|
|
1780
|
+
await doctorText(findings, groups, {
|
|
1781
|
+
caps: term.caps(stdout, env),
|
|
1782
|
+
verbose: Boolean(values.verbose),
|
|
1783
|
+
project,
|
|
1784
|
+
env: ownEnv(env, project),
|
|
1785
|
+
stderr,
|
|
1786
|
+
}),
|
|
1787
|
+
);
|
|
1031
1788
|
}
|
|
1032
1789
|
return ok ? 0 : 1;
|
|
1033
1790
|
}
|
|
@@ -1038,12 +1795,20 @@ async function runDoctor({ values, project, env, stdout, stderr }) {
|
|
|
1038
1795
|
// reader the findings: they are printed one per line, `level check message
|
|
1039
1796
|
// — file`, and the Summary line still ends them (lib.mjs). `load` stands in
|
|
1040
1797
|
// for the import in a test.
|
|
1041
|
-
export async function doctorText(
|
|
1798
|
+
export async function doctorText(
|
|
1799
|
+
findings,
|
|
1800
|
+
groups,
|
|
1801
|
+
{ caps, verbose = false, project = null, env = process.env, stderr = null } = {},
|
|
1802
|
+
{ load = () => import("./doctor-report.mjs") } = {},
|
|
1803
|
+
) {
|
|
1042
1804
|
try {
|
|
1043
1805
|
const { report } = await load();
|
|
1044
1806
|
return report(findings, groups, { caps, verbose, version: packageVersion(), project, env });
|
|
1045
1807
|
} catch (e) {
|
|
1046
|
-
if (stderr)
|
|
1808
|
+
if (stderr)
|
|
1809
|
+
stderr.write(
|
|
1810
|
+
`doctor: the report could not be drawn (${e && e.message ? e.message : e}); its findings follow, one per line.\n`,
|
|
1811
|
+
);
|
|
1047
1812
|
return doctorFallbackText(findings, { env });
|
|
1048
1813
|
}
|
|
1049
1814
|
}
|
|
@@ -1054,13 +1819,21 @@ async function runReconcile({ values, project, env, stdin, stdout, stderr, ask }
|
|
|
1054
1819
|
// The gate: --only names what is written and confirms headless; a bare
|
|
1055
1820
|
// --write asks on a terminal and refuses without one.
|
|
1056
1821
|
if (write && !only) {
|
|
1057
|
-
const yes = await confirmWrite(`Regenerate every derived view of ${project}'s vault? [y/N] `, {
|
|
1822
|
+
const yes = await confirmWrite(`Regenerate every derived view of ${project}'s vault? [y/N] `, {
|
|
1823
|
+
stdin,
|
|
1824
|
+
stdout,
|
|
1825
|
+
ask,
|
|
1826
|
+
});
|
|
1058
1827
|
if (yes === null) {
|
|
1059
1828
|
stderr.write("a bare reconcile --write in a non-TTY refuses; name what is written to confirm: --only <target>\n");
|
|
1060
|
-
if (values.json)
|
|
1829
|
+
if (values.json)
|
|
1830
|
+
stdout.write(JSON.stringify(envelope("reconcile", project, false, { refused: "non-tty" }), null, 2) + "\n");
|
|
1061
1831
|
return 1;
|
|
1062
1832
|
}
|
|
1063
|
-
if (!yes) {
|
|
1833
|
+
if (!yes) {
|
|
1834
|
+
stdout.write("nothing written.\n");
|
|
1835
|
+
return 0;
|
|
1836
|
+
}
|
|
1064
1837
|
}
|
|
1065
1838
|
const { runReconcile: core } = await import("./reconcile.mjs");
|
|
1066
1839
|
let out;
|
|
@@ -1069,7 +1842,8 @@ async function runReconcile({ values, project, env, stdin, stdout, stderr, ask }
|
|
|
1069
1842
|
} catch (e) {
|
|
1070
1843
|
const msg = e && e.message ? e.message : String(e);
|
|
1071
1844
|
stderr.write(msg + "\n");
|
|
1072
|
-
if (values.json)
|
|
1845
|
+
if (values.json)
|
|
1846
|
+
stdout.write(JSON.stringify(envelope("reconcile", project, false, { error: msg }), null, 2) + "\n");
|
|
1073
1847
|
return e && e.code === "UNBOUND" ? 3 : 2;
|
|
1074
1848
|
}
|
|
1075
1849
|
const failed = write && out.summary && out.summary.failed > 0;
|
|
@@ -1077,14 +1851,54 @@ async function runReconcile({ values, project, env, stdin, stdout, stderr, ask }
|
|
|
1077
1851
|
return failed ? 1 : 0;
|
|
1078
1852
|
}
|
|
1079
1853
|
|
|
1854
|
+
// setup (the setup story): its own module, imported lazily as the install
|
|
1855
|
+
// family is, run with this package as its root and in the environment every
|
|
1856
|
+
// installer run gets (ownEnv, for the project setup settles on). The envelope
|
|
1857
|
+
// is written here, as every verb's is: its result is the write verbs' keys
|
|
1858
|
+
// plus `setup`. A usage error is exit 2 with its envelope.
|
|
1859
|
+
async function runSetupVerb(ctx) {
|
|
1860
|
+
const { runSetup } = await import("./setup.mjs");
|
|
1861
|
+
const { values, env, stdout } = ctx;
|
|
1862
|
+
const r = await runSetup({
|
|
1863
|
+
...ctx,
|
|
1864
|
+
root: PACKAGE_ROOT,
|
|
1865
|
+
ownEnv: (p) => ownEnv(env, p),
|
|
1866
|
+
author: (p) => gitAuthor(p, env),
|
|
1867
|
+
});
|
|
1868
|
+
if (r.error) return usageFail(Object.assign(new Error(r.error), { code: "USAGE" }), { verb: "setup", ...ctx });
|
|
1869
|
+
if (values.json && r.result)
|
|
1870
|
+
stdout.write(JSON.stringify(envelope("setup", r.project, r.ok, r.result), null, 2) + "\n");
|
|
1871
|
+
return r.code;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1080
1874
|
async function runInstallVerb({ row, values, project, env, cwd, stdin, stdout, ask }) {
|
|
1081
1875
|
const ih = await import("./install-harness.mjs");
|
|
1082
|
-
const opts = {
|
|
1876
|
+
const opts = {
|
|
1877
|
+
harnesses: values.harness || [],
|
|
1878
|
+
surfaces: values.surface && values.surface.length ? values.surface : null,
|
|
1879
|
+
globalRemoval: Boolean(values.global),
|
|
1880
|
+
register: !values["no-register"],
|
|
1881
|
+
root: PACKAGE_ROOT,
|
|
1882
|
+
env: ownEnv(env, project),
|
|
1883
|
+
cwd,
|
|
1884
|
+
stdin,
|
|
1885
|
+
stdout,
|
|
1886
|
+
ask,
|
|
1887
|
+
json: Boolean(values.json),
|
|
1888
|
+
verbose: Boolean(values.verbose),
|
|
1889
|
+
};
|
|
1083
1890
|
// plan goes through the installer's own planOptions, as its main() does, so
|
|
1084
1891
|
// the two entry points read the same root version for a dry-run row.
|
|
1085
1892
|
if (row.verb === "plan") {
|
|
1086
1893
|
const p = ih.plan(project, ih.planOptions(opts));
|
|
1087
|
-
if (values.json)
|
|
1894
|
+
if (values.json)
|
|
1895
|
+
stdout.write(
|
|
1896
|
+
JSON.stringify(
|
|
1897
|
+
envelope("plan", project, p.ok && !p.incomplete, { ...p, items: p.items.map(ih.publicItem) }),
|
|
1898
|
+
null,
|
|
1899
|
+
2,
|
|
1900
|
+
) + "\n",
|
|
1901
|
+
);
|
|
1088
1902
|
else {
|
|
1089
1903
|
const c = term.caps(stdout, env);
|
|
1090
1904
|
stdout.write(ih.renderPreview(p, { verbose: opts.verbose, verb: "plan", caps: c }));
|
|
@@ -1100,12 +1914,28 @@ async function runInstallVerb({ row, values, project, env, cwd, stdin, stdout, a
|
|
|
1100
1914
|
if (values.json) {
|
|
1101
1915
|
// `fetched` is additive: the shells a run fetched before its plan, [] for
|
|
1102
1916
|
// a run that fetched none (uninstall always).
|
|
1103
|
-
stdout.write(
|
|
1917
|
+
stdout.write(
|
|
1918
|
+
JSON.stringify(
|
|
1919
|
+
envelope(row.verb, project, ok, {
|
|
1920
|
+
gate: r.gate,
|
|
1921
|
+
applied: r.applied,
|
|
1922
|
+
failed: r.failed,
|
|
1923
|
+
incomplete: r.plan.incomplete,
|
|
1924
|
+
plannedAgainst: r.plan.plannedAgainst,
|
|
1925
|
+
items: r.plan.items.map(ih.publicItem),
|
|
1926
|
+
refusals: r.plan.refusals,
|
|
1927
|
+
reports: r.plan.reports,
|
|
1928
|
+
fetched: r.fetched || [],
|
|
1929
|
+
}),
|
|
1930
|
+
null,
|
|
1931
|
+
2,
|
|
1932
|
+
) + "\n",
|
|
1933
|
+
);
|
|
1104
1934
|
} else {
|
|
1105
1935
|
// The installer's own words for a gate that wrote nothing — a bare run
|
|
1106
1936
|
// without a terminal, a declined question, a channel switch without a
|
|
1107
1937
|
// terminal — shared with its main(), so the two entries say the same.
|
|
1108
|
-
stdout.write(ih.gateText(r, { verb: row.verb, harnesses: opts.harnesses,
|
|
1938
|
+
stdout.write(ih.gateText(r, { verb: row.verb, harnesses: opts.harnesses, caps: term.caps(stdout, env) }));
|
|
1109
1939
|
}
|
|
1110
1940
|
return ok ? 0 : 1;
|
|
1111
1941
|
}
|