@davidorex/pi-context-cli 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +127 -7
- package/dist/bin.js +0 -0
- package/dist/cli.d.ts +118 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +584 -30
- package/dist/cli.js.map +1 -1
- package/dist/pi-bound.d.ts +119 -0
- package/dist/pi-bound.d.ts.map +1 -0
- package/dist/pi-bound.js +257 -0
- package/dist/pi-bound.js.map +1 -0
- package/dist/render.d.ts +57 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +95 -0
- package/dist/render.js.map +1 -0
- package/package.json +4 -3
package/dist/cli.js
CHANGED
|
@@ -27,13 +27,32 @@ import { execSync } from "node:child_process";
|
|
|
27
27
|
import { readFileSync } from "node:fs";
|
|
28
28
|
import path from "node:path";
|
|
29
29
|
import { createInterface } from "node:readline";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
import { renderBlocked, renderConflicts } from "@davidorex/pi-context";
|
|
32
|
+
import { nextId, readBlock, resolveBlockItemSchema } from "@davidorex/pi-context/block-api";
|
|
33
|
+
import { loadConfig } from "@davidorex/pi-context/context";
|
|
34
|
+
import { schemaPath } from "@davidorex/pi-context/context-dir";
|
|
30
35
|
import { boundedJsonOutput, ops, renderOpResultText, } from "@davidorex/pi-context/ops";
|
|
36
|
+
import { validateFromFile } from "@davidorex/pi-context/schema-validator";
|
|
37
|
+
import { readSchema } from "@davidorex/pi-context/schema-write";
|
|
38
|
+
import { runPiBound } from "./pi-bound.js";
|
|
39
|
+
import { formatAjvError, isValidationError, renderTable } from "./render.js";
|
|
31
40
|
/**
|
|
32
41
|
* The surfaced command set: every op the CLI exposes. Derived by reflection —
|
|
33
42
|
* NOT a hardcoded list. A `surface: "process"` op (currently only list-tools)
|
|
34
43
|
* is excluded here by the partition, never by name.
|
|
35
44
|
*/
|
|
36
45
|
export const useOps = ops.filter((o) => o.surface === "use");
|
|
46
|
+
/**
|
|
47
|
+
* The pi-context-cli package version, read ONCE at module load from the shipped
|
|
48
|
+
* package.json. Resolved RELATIVE to this module's URL (mirrors pi-bound.ts's
|
|
49
|
+
* createRequire(import.meta.url) pattern) so it works from the BUILT bin: from
|
|
50
|
+
* `dist/cli.js`, `../package.json` is the package root (npm always ships
|
|
51
|
+
* package.json in a published tarball). A plain `import pkg from "../package.json"`
|
|
52
|
+
* is deliberately avoided — tsconfig.build's rootDir:"./src" / include:["src/**"]
|
|
53
|
+
* would make that compile path a hazard. `--version`/`-v` prints this value.
|
|
54
|
+
*/
|
|
55
|
+
export const PKG_VERSION = JSON.parse(readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8")).version;
|
|
37
56
|
/**
|
|
38
57
|
* Extract the literal string values of a string-enum field, or null for any
|
|
39
58
|
* non-enum shape. A typebox `Type.Union([Type.Literal("eq"), …])` serializes at
|
|
@@ -157,6 +176,29 @@ export function parseOpArgs(op, argv, cwdBase = process.cwd()) {
|
|
|
157
176
|
out.help = true;
|
|
158
177
|
continue;
|
|
159
178
|
}
|
|
179
|
+
if (tok === "--show-schema") {
|
|
180
|
+
// FGAP-022 — global flag (not an op param): print the block contract and exit.
|
|
181
|
+
out.showSchema = true;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (op.name === "append-block-item" && (tok === "--dryRun" || tok === "--dry-run") && props.dryRun === undefined) {
|
|
185
|
+
// FGAP-024 — `--dry-run` is a GLOBAL flag scoped to append-block-item, the
|
|
186
|
+
// sole op the main() honor branch handles: it declares no `dryRun` param yet
|
|
187
|
+
// supports a client-side prospective-whole-file dry run. The token is captured
|
|
188
|
+
// here and NEVER routed into params, because the frozen op would reject
|
|
189
|
+
// `dryRun` as an unknown flag. Both the camel and kebab tokens are matched
|
|
190
|
+
// explicitly since, not being a schema key on this op, neither would resolve
|
|
191
|
+
// through the kebab→camel normalization below.
|
|
192
|
+
//
|
|
193
|
+
// For ops that DO declare a `dryRun` param (relation ops, upsert, update, …)
|
|
194
|
+
// `props.dryRun === undefined` is false, so this branch was already skipped and
|
|
195
|
+
// the token flows to the boolean-param handling, preserving the op's own dryRun
|
|
196
|
+
// semantics. For every OTHER no-`dryRun` op (the non-append block-mutation ops)
|
|
197
|
+
// the token is NOT swallowed here; it falls through to the unknown-flag throw
|
|
198
|
+
// below (UsageError → exit 2), a clean rejection rather than a silent write.
|
|
199
|
+
out.dryRun = true;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
160
202
|
if (tok === "--json") {
|
|
161
203
|
out.json = true;
|
|
162
204
|
continue;
|
|
@@ -172,10 +214,36 @@ export function parseOpArgs(op, argv, cwdBase = process.cwd()) {
|
|
|
172
214
|
out.cwd = path.isAbsolute(v) ? v : path.resolve(cwdBase, v);
|
|
173
215
|
continue;
|
|
174
216
|
}
|
|
217
|
+
if (tok === "--format") {
|
|
218
|
+
// FGAP-021 — explicit render selector. `text` reproduces each op's prior
|
|
219
|
+
// run() text; `json` is the `--json` envelope; `table` projects a renderable
|
|
220
|
+
// array (read-page / data array) as markdown. An unknown value is an operator
|
|
221
|
+
// error, not a silent fallback.
|
|
222
|
+
const v = argv[++i];
|
|
223
|
+
if (v === undefined)
|
|
224
|
+
throw new UsageError("--format requires one of: text, json, table");
|
|
225
|
+
if (v !== "text" && v !== "json" && v !== "table") {
|
|
226
|
+
throw new UsageError(`--format expects one of: text, json, table; got '${v}'`);
|
|
227
|
+
}
|
|
228
|
+
out.format = v;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
175
231
|
if (tok === "--writer") {
|
|
176
232
|
const v = argv[++i];
|
|
177
233
|
if (v === undefined)
|
|
178
234
|
throw new UsageError("--writer requires a JSON argument");
|
|
235
|
+
// Shorthand `kind:id` (FGAP-025): a value that is neither `{`-prefixed JSON
|
|
236
|
+
// nor an `@file` reference and matches `<kind>:<identifier>` expands to the
|
|
237
|
+
// canonical {kind, <id-field>:<rest>} WriterIdentity (the id-field per
|
|
238
|
+
// WRITER_KIND_IDENTIFIER_FIELD). The rest may itself contain colons (an
|
|
239
|
+
// email or step-id) — only the FIRST colon delimits kind from identifier.
|
|
240
|
+
// A JSON / @file value (the canonical form) is parsed unchanged.
|
|
241
|
+
const shorthand = /^(human|agent|monitor|workflow):(.+)$/.exec(v);
|
|
242
|
+
if (shorthand && !v.startsWith("@")) {
|
|
243
|
+
const kind = shorthand[1];
|
|
244
|
+
out.explicitWriter = { kind, [WRITER_KIND_IDENTIFIER_FIELD[kind]]: shorthand[2] };
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
179
247
|
try {
|
|
180
248
|
out.explicitWriter = v.startsWith("@") ? JSON.parse(readFileSync(v.slice(1), "utf8")) : JSON.parse(v);
|
|
181
249
|
}
|
|
@@ -184,10 +252,57 @@ export function parseOpArgs(op, argv, cwdBase = process.cwd()) {
|
|
|
184
252
|
}
|
|
185
253
|
continue;
|
|
186
254
|
}
|
|
255
|
+
// `--where field:op:value` shorthand (FGAP-025): a single token expanding to
|
|
256
|
+
// the op's declared field/op/value params. Split on the FIRST TWO colons only —
|
|
257
|
+
// the value segment may itself contain colons. The canonical explicit
|
|
258
|
+
// `--field/--op/--value` flags remain available and pass through unchanged.
|
|
259
|
+
if (tok === "--where") {
|
|
260
|
+
const v = argv[++i];
|
|
261
|
+
if (v === undefined)
|
|
262
|
+
throw new UsageError("--where requires a field:op:value argument");
|
|
263
|
+
const c1 = v.indexOf(":");
|
|
264
|
+
const c2 = c1 >= 0 ? v.indexOf(":", c1 + 1) : -1;
|
|
265
|
+
if (c1 < 0 || c2 < 0) {
|
|
266
|
+
throw new UsageError(`--where expects field:op:value; got '${v}'`);
|
|
267
|
+
}
|
|
268
|
+
out.params.field = v.slice(0, c1);
|
|
269
|
+
out.params.op = v.slice(c1 + 1, c2);
|
|
270
|
+
out.params.value = v.slice(c2 + 1);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
187
273
|
if (!tok.startsWith("--")) {
|
|
188
274
|
throw new UsageError(`unexpected argument: ${tok}`);
|
|
189
275
|
}
|
|
190
|
-
|
|
276
|
+
// Raw token kept verbatim for error messages; `field` resolves to the op's
|
|
277
|
+
// actual property key via the normalization layer below.
|
|
278
|
+
let field = tok.slice(2);
|
|
279
|
+
// FGAP-032 — `--id` aliases the op's single declared id-param. An op may key
|
|
280
|
+
// its id param `id`, or `<x>Id` (itemId/parentId/taskId/unitId/…). When the op
|
|
281
|
+
// has no literal `id` property and exactly one such param, `--id` resolves to
|
|
282
|
+
// it; zero leaves `field` as-is (the unknown-flag error fires); two or more is
|
|
283
|
+
// ambiguous (the caller must name the explicit flag). An id-param is always
|
|
284
|
+
// string-typed (every identity selector is Type.String / Type.Optional(String));
|
|
285
|
+
// the string-type guard excludes boolean flags whose name happens to end in `Id`
|
|
286
|
+
// — e.g. append-block-item's `autoId` (a Type.Boolean allocation flag, not an
|
|
287
|
+
// identity selector) — so `--id` never silently binds to them.
|
|
288
|
+
if (field === "id" && props.id === undefined) {
|
|
289
|
+
const idParams = Object.keys(props).filter((k) => (k === "id" || /Id$/.test(k)) && fieldType(props[k]) === "string");
|
|
290
|
+
if (idParams.length === 1) {
|
|
291
|
+
field = idParams[0];
|
|
292
|
+
}
|
|
293
|
+
else if (idParams.length >= 2) {
|
|
294
|
+
throw new UsageError(`ambiguous --id (op declares ${idParams.length} id-params: ${idParams.join(", ")}); use the explicit flag`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else if (props[field] === undefined && field.includes("-")) {
|
|
298
|
+
// FGAP-064 — kebab→camel normalization: a conventional `--dry-run` / `--id`
|
|
299
|
+
// kebab form resolves to the camelCase op-schema key (`dryRun`) when that
|
|
300
|
+
// camel key exists. An unresolved kebab token stays raw → unknown-flag error.
|
|
301
|
+
const camel = field.replace(/-([a-z0-9])/g, (_m, c) => c.toUpperCase());
|
|
302
|
+
if (props[camel] !== undefined) {
|
|
303
|
+
field = camel;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
191
306
|
const fschema = props[field];
|
|
192
307
|
if (fschema === undefined) {
|
|
193
308
|
throw new UsageError(`unknown flag: ${tok}`);
|
|
@@ -229,17 +344,50 @@ export function parseOpArgs(op, argv, cwdBase = process.cwd()) {
|
|
|
229
344
|
: JSON.parse(value);
|
|
230
345
|
}
|
|
231
346
|
catch (err) {
|
|
232
|
-
|
|
347
|
+
// FGAP-025: the `value` field (the comparison operand of
|
|
348
|
+
// filter-block-items, Type.Unknown) is the sole CSV-shorthand target. A
|
|
349
|
+
// bare unquoted operand that is not valid JSON is retained as the raw
|
|
350
|
+
// string so the post-loop `--op in` CSV pass can split it (and an
|
|
351
|
+
// unquoted scalar operand still reaches the op as a string). Every other
|
|
352
|
+
// json field keeps the strict parse-or-error contract (e.g. a malformed
|
|
353
|
+
// `--item` is an operator error, never a silent string).
|
|
354
|
+
if (field === "value" && !value.startsWith("@")) {
|
|
355
|
+
out.params[field] = value;
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
throw new UsageError(`--${field}: ${err instanceof Error ? err.message : String(err)}`);
|
|
359
|
+
}
|
|
233
360
|
}
|
|
234
361
|
}
|
|
235
362
|
}
|
|
236
363
|
if (out.explicitWriter !== undefined) {
|
|
237
364
|
out.params.writer = out.explicitWriter;
|
|
238
365
|
}
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
366
|
+
// CSV `--op in` normalization (FGAP-025): the `in` membership operator takes a
|
|
367
|
+
// list value. When `op === "in"` and the value arrived as a single string,
|
|
368
|
+
// split it on commas into the string array the op's declared shape expects.
|
|
369
|
+
// Argv-order-independent — runs after the whole loop, so `--value a,b,c --op in`
|
|
370
|
+
// and `--op in --value a,b,c` both normalize. A value already an array passes
|
|
371
|
+
// through unchanged (the typeof guard).
|
|
372
|
+
if (out.params.op === "in" && typeof out.params.value === "string") {
|
|
373
|
+
out.params.value = out.params.value.split(",");
|
|
374
|
+
}
|
|
375
|
+
// Required-field check — the exemption set derives from AUTO_SUPPLIED, the single
|
|
376
|
+
// source for the CLI auto-supplied param contract: a key there is auto-supplied, so
|
|
377
|
+
// it is exempt from this missing-required check, rendered bracketed-optional in the
|
|
378
|
+
// per-op synopsis (isSynopsisRequired), and `autoSupplied`-annotated in the Flags
|
|
379
|
+
// block. Concretely today: `writer` (injectWriter fills it from the resolved operator
|
|
380
|
+
// identity after parse) and `arrayKey` (injectArrayKey derives it from
|
|
381
|
+
// config.block_kinds[].array_key for the block-mutation ops after parse, FGAP-019, so
|
|
382
|
+
// a `--block` without an explicit `--arrayKey` must not be flagged missing here).
|
|
383
|
+
// Adding a new AUTO_SUPPLIED key also requires adding its injector (injectWriter /
|
|
384
|
+
// injectArrayKey-style): the map single-sources the exemption/help CONTRACT, not the
|
|
385
|
+
// value-supply wiring.
|
|
386
|
+
// `--help` and `--show-schema` exit before any op invocation and need no item, so
|
|
387
|
+
// the required-field check is skipped for them (FGAP-022). `--dryRun` still requires
|
|
388
|
+
// the op's declared inputs (it validates a prospective item) and so is NOT exempt.
|
|
389
|
+
if (!out.help && !out.showSchema) {
|
|
390
|
+
const required = (schema.required ?? []).filter((r) => !(r in AUTO_SUPPLIED));
|
|
243
391
|
const missing = required.filter((r) => !(r in out.params));
|
|
244
392
|
if (missing.length > 0) {
|
|
245
393
|
throw new UsageError(`missing required: ${missing.map((m) => `--${m}`).join(", ")}`);
|
|
@@ -259,6 +407,33 @@ export function injectWriter(op, params, identity) {
|
|
|
259
407
|
}
|
|
260
408
|
return params;
|
|
261
409
|
}
|
|
410
|
+
/**
|
|
411
|
+
* Schema- + config-driven arrayKey injection (FGAP-019), mirroring injectWriter.
|
|
412
|
+
* The 7 block-mutation ops still DECLARE `arrayKey` required (their in-pi schema +
|
|
413
|
+
* handler are byte-unchanged and still receive + require it) — the CLI supplies it
|
|
414
|
+
* pre-call so a caller passes only `--block`. When the op declares `arrayKey`, none
|
|
415
|
+
* was passed, and a string `block` is present, derive the array_key from the
|
|
416
|
+
* config block_kinds entry whose canonical_id matches `block` (canonical_id ≠
|
|
417
|
+
* array_key in real data, e.g. framework-gaps → gaps, so the derivation is genuinely
|
|
418
|
+
* needed). Best-effort: no substrate, no config, or an unknown block leaves arrayKey
|
|
419
|
+
* unset — the op then throws its own missing-param error. Mutates and returns params.
|
|
420
|
+
*/
|
|
421
|
+
export function injectArrayKey(op, params, cwd) {
|
|
422
|
+
const props = objectSchema(op).properties ?? {};
|
|
423
|
+
if (props.arrayKey !== undefined && params.arrayKey === undefined && typeof params.block === "string") {
|
|
424
|
+
let cfg = null;
|
|
425
|
+
try {
|
|
426
|
+
cfg = loadConfig(cwd);
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
cfg = null;
|
|
430
|
+
}
|
|
431
|
+
const bk = cfg?.block_kinds.find((b) => b.canonical_id === params.block);
|
|
432
|
+
if (bk)
|
|
433
|
+
params.arrayKey = bk.array_key;
|
|
434
|
+
}
|
|
435
|
+
return params;
|
|
436
|
+
}
|
|
262
437
|
/**
|
|
263
438
|
* Build the DispatchContext threaded into `op.run` as its 3rd arg so every CLI
|
|
264
439
|
* write op stamps attestation (created_by / created_at) on schemas that declare
|
|
@@ -330,37 +505,200 @@ export function authDecision(op, opts) {
|
|
|
330
505
|
reason: `${op.name} requires authorization; re-run with --yes in a non-interactive context`,
|
|
331
506
|
};
|
|
332
507
|
}
|
|
333
|
-
/**
|
|
334
|
-
|
|
508
|
+
/**
|
|
509
|
+
* Single source for the CLI's auto-supplied params: a field declared `required`
|
|
510
|
+
* by the op schema that the CLI fills after parse, so the caller never passes it.
|
|
511
|
+
* Maps the param name to its provenance phrase. This map is the ONE source for
|
|
512
|
+
* both the synopsis exemption (bracketed-optional, `isSynopsisRequired`) and the
|
|
513
|
+
* per-flag `autoSupplied` annotation surfaced in the Flags block + json help —
|
|
514
|
+
* reconciling the Flags `(required)` schema-truth with the optional synopsis so
|
|
515
|
+
* neither surface contradicts the other (TASK-042 iterate-to-zero finding):
|
|
516
|
+
* - writer: injectWriter fills it from the resolved operator identity
|
|
517
|
+
* - arrayKey: injectArrayKey derives it from config.block_kinds[].array_key
|
|
518
|
+
*/
|
|
519
|
+
export const AUTO_SUPPLIED = {
|
|
520
|
+
writer: "auto-injected",
|
|
521
|
+
arrayKey: "auto-derived from --block",
|
|
522
|
+
};
|
|
523
|
+
/**
|
|
524
|
+
* A field is synopsis-required iff the schema lists it required AND the CLI does
|
|
525
|
+
* NOT auto-supply it. Auto-supplied params (AUTO_SUPPLIED) are post-parse fills,
|
|
526
|
+
* so they render bracketed-optional in the synopsis even when schema-required —
|
|
527
|
+
* derived from AUTO_SUPPLIED so the exemption and the `autoSupplied` annotation
|
|
528
|
+
* share one source (no hardcoded writer/arrayKey literal here).
|
|
529
|
+
*/
|
|
530
|
+
function isSynopsisRequired(field, schemaRequired) {
|
|
531
|
+
return schemaRequired.has(field) && !(field in AUTO_SUPPLIED);
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Build the structured per-op help model from the op's typebox parameters +
|
|
535
|
+
* the registry `examples` + the help-group siblings. Pure — no I/O.
|
|
536
|
+
*/
|
|
537
|
+
export function buildHelpModel(op) {
|
|
335
538
|
const schema = objectSchema(op);
|
|
336
539
|
const props = schema.properties ?? {};
|
|
337
540
|
const required = new Set(schema.required ?? []);
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
541
|
+
const flags = Object.entries(props).map(([name, fschema]) => {
|
|
542
|
+
const vals = stringEnumValues(fschema);
|
|
543
|
+
return {
|
|
544
|
+
name,
|
|
545
|
+
type: vals ? vals.join("|") : fieldType(fschema),
|
|
546
|
+
required: required.has(name),
|
|
547
|
+
...(fschema.description ? { description: fschema.description } : {}),
|
|
548
|
+
...(name in AUTO_SUPPLIED ? { autoSupplied: AUTO_SUPPLIED[name] } : {}),
|
|
549
|
+
};
|
|
550
|
+
});
|
|
551
|
+
// Synopsis: required (sans writer/arrayKey) then optional, each `--<f> <type>`,
|
|
552
|
+
// optionals bracketed. writer/arrayKey fall into the optional/bracketed set.
|
|
553
|
+
const synReq = [];
|
|
554
|
+
const synOpt = [];
|
|
555
|
+
for (const f of flags) {
|
|
556
|
+
const token = `--${f.name} <${f.type}>`;
|
|
557
|
+
if (isSynopsisRequired(f.name, required))
|
|
558
|
+
synReq.push(token);
|
|
559
|
+
else
|
|
560
|
+
synOpt.push(`[${token}]`);
|
|
561
|
+
}
|
|
562
|
+
const synopsis = [`pi-context ${op.name}`, ...synReq, ...synOpt].join(" ");
|
|
563
|
+
const group = groupForOp(op.name);
|
|
564
|
+
const related = useOps
|
|
565
|
+
.filter((o) => o.name !== op.name && groupForOp(o.name) === group)
|
|
566
|
+
.map((o) => o.name)
|
|
567
|
+
.sort();
|
|
568
|
+
return {
|
|
569
|
+
name: op.name,
|
|
570
|
+
synopsis,
|
|
571
|
+
summary: op.promptSnippet ?? op.description,
|
|
572
|
+
flags,
|
|
573
|
+
examples: op.examples ?? [],
|
|
574
|
+
related,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Best-of-breed per-op help text (TASK-042): `<name> — <description>` → SYNOPSIS →
|
|
579
|
+
* Flags (per-field, enum joins + required/optional + desc; json fields show
|
|
580
|
+
* `<json | @file>`) → EXAMPLES → RELATED (omitted when empty) → footer →
|
|
581
|
+
* the Global flags trailer. Plain text.
|
|
582
|
+
*/
|
|
583
|
+
export function deriveHelp(op) {
|
|
584
|
+
const model = buildHelpModel(op);
|
|
585
|
+
const lines = [`${op.name} — ${op.description}`, "", "SYNOPSIS", ` ${model.synopsis}`, ""];
|
|
586
|
+
if (model.flags.length === 0) {
|
|
341
587
|
lines.push(" (no parameters)");
|
|
342
588
|
}
|
|
343
589
|
else {
|
|
344
590
|
lines.push("Flags:");
|
|
345
|
-
for (const
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
591
|
+
for (const f of model.flags) {
|
|
592
|
+
const typeShown = f.type === "json" ? "json | @file" : f.type;
|
|
593
|
+
// required/optional is schema-truth; an auto-supplied param appends its
|
|
594
|
+
// provenance (`required; auto-derived from --block`) so the (required) tag
|
|
595
|
+
// here does not contradict the bracketed-optional synopsis.
|
|
596
|
+
const baseTag = f.required ? "required" : "optional";
|
|
597
|
+
const tag = f.autoSupplied ? `${baseTag}; ${f.autoSupplied}` : baseTag;
|
|
598
|
+
const desc = f.description ? ` — ${f.description}` : "";
|
|
599
|
+
lines.push(` --${f.name} <${typeShown}> (${tag})${desc}`);
|
|
351
600
|
}
|
|
352
601
|
}
|
|
353
|
-
|
|
602
|
+
if (model.examples.length > 0) {
|
|
603
|
+
lines.push("", "EXAMPLES");
|
|
604
|
+
for (const ex of model.examples)
|
|
605
|
+
lines.push(` ${ex}`);
|
|
606
|
+
}
|
|
607
|
+
if (model.related.length > 0) {
|
|
608
|
+
lines.push("", "RELATED", ` ${model.related.join(" ")}`);
|
|
609
|
+
}
|
|
610
|
+
lines.push("", `Run 'pi-context --help' for all commands; '${op.name} --help --format json' for machine-readable help.`, "Global flags: --cwd <dir> --json --format text|json|table --yes --writer <json> --show-schema --dry-run --help");
|
|
354
611
|
return lines.join("\n");
|
|
355
612
|
}
|
|
356
|
-
/**
|
|
613
|
+
/** Membership test against an explicit name-set. */
|
|
614
|
+
const oneOf = (...names) => (name) => names.includes(name);
|
|
615
|
+
const HELP_GROUPS = [
|
|
616
|
+
{
|
|
617
|
+
label: "Read & query",
|
|
618
|
+
match: (n) => n.startsWith("read-") ||
|
|
619
|
+
oneOf("filter-block-items", "join-blocks", "resolve-item-by-id", "resolve-items-by-id", "find-references", "walk-ancestors", "context-walk-descendants", "context-edges-for-lens", "context-lens-view", "context-status", "context-check-status", "context-current-state", "context-bootstrap-state", "context-validate", "context-validate-relations", "gather-execution-context", "validate-block-items")(n),
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
// Relations BEFORE Block writes: `*-relation` / `*-relations` must win over
|
|
623
|
+
// the `append-`/`remove-` block-write prefixes below.
|
|
624
|
+
label: "Relations",
|
|
625
|
+
match: (n) => n.endsWith("-relation") || n.endsWith("-relations"),
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
label: "Block writes",
|
|
629
|
+
match: (n) => oneOf("append-block-item", "update-block-item", "upsert-block-item", "remove-block-item", "append-block-nested-item", "update-block-nested-item", "remove-block-nested-item", "write-block")(n),
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
label: "Schema & config",
|
|
633
|
+
match: oneOf("write-schema", "write-schema-migration", "amend-config", "update", "resolve-conflict", "resolve-blocked", "rename-canonical-id"),
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
label: "Substrate lifecycle",
|
|
637
|
+
match: oneOf("context-init", "context-accept-all", "context-install", "context-switch", "context-list", "context-archive"),
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
label: "Workflow",
|
|
641
|
+
match: (n) => n.startsWith("context-roadmap-") || oneOf("complete-task", "promote-item")(n),
|
|
642
|
+
},
|
|
643
|
+
];
|
|
644
|
+
/**
|
|
645
|
+
* Classify a use-op into its top-level help group LABEL (first-match-wins over the
|
|
646
|
+
* ordered HELP_GROUPS). Throws when a use-op matches no group — a new op that slips
|
|
647
|
+
* past every rule fails loudly here rather than silently vanishing from the help.
|
|
648
|
+
* The drift-guard test asserts every `useOps` name maps to exactly one group.
|
|
649
|
+
*/
|
|
650
|
+
export function groupForOp(name) {
|
|
651
|
+
const group = HELP_GROUPS.find((g) => g.match(name));
|
|
652
|
+
if (group === undefined) {
|
|
653
|
+
throw new Error(`groupForOp: op '${name}' matches no help group — add it to HELP_GROUPS`);
|
|
654
|
+
}
|
|
655
|
+
return group.label;
|
|
656
|
+
}
|
|
657
|
+
/** Max one-liner width before truncation in the grouped help. */
|
|
658
|
+
const HELP_ONELINER_WIDTH = 72;
|
|
659
|
+
/**
|
|
660
|
+
* One-liner for an op row: prefer `promptSnippet` (the terse reflection summary),
|
|
661
|
+
* fall back to `description`. Collapse to a single line, then truncate uniformly to
|
|
662
|
+
* ~HELP_ONELINER_WIDTH at the last word boundary, appending an ellipsis when cut.
|
|
663
|
+
* Applied to EVERY row identically — no per-op special-casing, and the ops-registry
|
|
664
|
+
* text itself is never edited.
|
|
665
|
+
*/
|
|
666
|
+
export function helpOneLiner(op) {
|
|
667
|
+
const raw = (op.promptSnippet ?? op.description).split("\n")[0].trim();
|
|
668
|
+
if (raw.length <= HELP_ONELINER_WIDTH)
|
|
669
|
+
return raw;
|
|
670
|
+
const head = raw.slice(0, HELP_ONELINER_WIDTH);
|
|
671
|
+
const lastSpace = head.lastIndexOf(" ");
|
|
672
|
+
const cut = lastSpace > 0 ? head.slice(0, lastSpace) : head;
|
|
673
|
+
return `${cut.trimEnd()}…`;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Grouped, scannable top-level help. Sections appear in HELP_GROUPS order (only
|
|
677
|
+
* groups with ≥1 op), ops sorted alphabetically within a group, each row a name
|
|
678
|
+
* padded to the group's own max-name width followed by the truncated one-liner.
|
|
679
|
+
* A static Process modes section surfaces `pi-bound` (a process mode, not a
|
|
680
|
+
* substrate op). The Global flags block is retained verbatim plus `--version`.
|
|
681
|
+
* Plain text only.
|
|
682
|
+
*/
|
|
357
683
|
export function deriveTopHelp() {
|
|
358
|
-
const lines = ["pi-context <op> [flags]", ""
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
684
|
+
const lines = ["pi-context <op> [flags]", ""];
|
|
685
|
+
for (const group of HELP_GROUPS) {
|
|
686
|
+
const groupOps = useOps
|
|
687
|
+
.filter((o) => groupForOp(o.name) === group.label)
|
|
688
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
689
|
+
if (groupOps.length === 0)
|
|
690
|
+
continue;
|
|
691
|
+
const width = Math.max(...groupOps.map((o) => o.name.length));
|
|
692
|
+
lines.push(group.label);
|
|
693
|
+
for (const op of groupOps) {
|
|
694
|
+
lines.push(` ${op.name.padEnd(width)} — ${helpOneLiner(op)}`);
|
|
695
|
+
}
|
|
696
|
+
lines.push("");
|
|
362
697
|
}
|
|
363
|
-
lines.push("
|
|
698
|
+
lines.push("Process modes");
|
|
699
|
+
lines.push(" pi-bound — Launch an embedded pi agent in-process on a bounded tool surface");
|
|
700
|
+
lines.push("");
|
|
701
|
+
lines.push("Global flags:", " --cwd <dir> substrate root (default: cwd)", " --json emit { ok, op, output } envelope (≡ --format json)", " --format <fmt> render as text | json | table (default: text, or json with --json)", " --yes, --force pre-authorize gated ops in non-interactive contexts", " --writer <json> override the auto-resolved writer identity", " --show-schema preview a block op's contract (array_key/required/types/id) and exit", " --dry-run append-block-item: validate the prospective file, write nothing", " --version, -v print the pi-context version and exit", " --help, -h this help, or per-op help after an op name");
|
|
364
702
|
return lines.join("\n");
|
|
365
703
|
}
|
|
366
704
|
/** Prompt the operator on an interactive TTY. Resolves true on y/yes. */
|
|
@@ -374,12 +712,53 @@ function promptConfirm(opName) {
|
|
|
374
712
|
});
|
|
375
713
|
});
|
|
376
714
|
}
|
|
715
|
+
/**
|
|
716
|
+
* FGAP-021 — extract the renderable row array from an OpResult for `--format table`,
|
|
717
|
+
* or null when the result is not a complete tabular collection. Precedence:
|
|
718
|
+
* - `{read}` whose `data` is an array AND `complete !== false` (an over-cap read,
|
|
719
|
+
* complete:false with data:null, is NOT tabular) → that array;
|
|
720
|
+
* - `{read}` whose `data` is an object exposing an `items` array → `.items`
|
|
721
|
+
* (the paged-collection shape: {items,total,hasMore});
|
|
722
|
+
* - `{json}` whose value is an array → that array;
|
|
723
|
+
* - anything else (prose, scalar/object data, over-cap, non-array) → null,
|
|
724
|
+
* so the dispatch falls back to text rather than emit a degenerate table.
|
|
725
|
+
*/
|
|
726
|
+
function tabularRows(r) {
|
|
727
|
+
if (typeof r === "string")
|
|
728
|
+
return null;
|
|
729
|
+
if ("read" in r) {
|
|
730
|
+
const read = r.read;
|
|
731
|
+
if (read.complete === false)
|
|
732
|
+
return null;
|
|
733
|
+
if (Array.isArray(read.data))
|
|
734
|
+
return read.data;
|
|
735
|
+
if (read.data !== null && typeof read.data === "object") {
|
|
736
|
+
const items = read.data.items;
|
|
737
|
+
if (Array.isArray(items))
|
|
738
|
+
return items;
|
|
739
|
+
}
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
if ("json" in r && Array.isArray(r.json))
|
|
743
|
+
return r.json;
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
377
746
|
export async function main(argv) {
|
|
378
747
|
const first = argv[0];
|
|
379
748
|
if (first === undefined || first === "--help" || first === "-h") {
|
|
380
749
|
process.stdout.write(`${deriveTopHelp()}\n`);
|
|
381
750
|
return 0;
|
|
382
751
|
}
|
|
752
|
+
if (first === "--version" || first === "-v") {
|
|
753
|
+
// CHANGE B — print the package version (read once at module load, build-safe
|
|
754
|
+
// relative to dist/cli.js) and exit. STDOUT, exit 0. Placed before the
|
|
755
|
+
// pi-bound branch and resolveOp so `--version`/`-v` are never mistaken for ops.
|
|
756
|
+
process.stdout.write(`pi-context ${PKG_VERSION}\n`);
|
|
757
|
+
return 0;
|
|
758
|
+
}
|
|
759
|
+
if (first === "pi-bound") {
|
|
760
|
+
return runPiBound(argv.slice(1));
|
|
761
|
+
}
|
|
383
762
|
const op = resolveOp(first);
|
|
384
763
|
if (op === undefined) {
|
|
385
764
|
if (isProcessOnlyOp(first)) {
|
|
@@ -402,11 +781,114 @@ export async function main(argv) {
|
|
|
402
781
|
throw err;
|
|
403
782
|
}
|
|
404
783
|
if (parsed.help) {
|
|
405
|
-
|
|
784
|
+
// `--help --format json` (or `--help --json`) emits the machine-readable
|
|
785
|
+
// HelpModel; otherwise the text template. table → text fallback (only json
|
|
786
|
+
// diverges). parsed.format/parsed.json are already populated by parseOpArgs.
|
|
787
|
+
const helpFormat = parsed.format ?? (parsed.json ? "json" : "text");
|
|
788
|
+
if (helpFormat === "json") {
|
|
789
|
+
process.stdout.write(`${JSON.stringify(buildHelpModel(op))}\n`);
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
process.stdout.write(`${deriveHelp(op)}\n`);
|
|
793
|
+
}
|
|
794
|
+
return 0;
|
|
795
|
+
}
|
|
796
|
+
// FGAP-022 — `--show-schema`: preview the block contract (array_key / required /
|
|
797
|
+
// field types / id pattern) and exit 0 BEFORE any write. Only meaningful for the
|
|
798
|
+
// block-mutation ops (the ops that declare `arrayKey` and take a `--block`); on any
|
|
799
|
+
// other op it is a misuse (exit 2). Reads the installed schema through the lifted
|
|
800
|
+
// readSchema → resolveBlockItemSchema path (no op change).
|
|
801
|
+
if (parsed.showSchema) {
|
|
802
|
+
const opProps = objectSchema(op).properties ?? {};
|
|
803
|
+
if (opProps.arrayKey === undefined || typeof parsed.params.block !== "string") {
|
|
804
|
+
process.stderr.write(`error: --show-schema applies only to a block op with --block <name>\n`);
|
|
805
|
+
return 2;
|
|
806
|
+
}
|
|
807
|
+
const block = parsed.params.block;
|
|
808
|
+
const schema = readSchema(parsed.cwd, block);
|
|
809
|
+
if (schema === null) {
|
|
810
|
+
process.stderr.write(`error: schema not found for block ${block}\n`);
|
|
811
|
+
return 3;
|
|
812
|
+
}
|
|
813
|
+
const { arrayKey, itemSchema } = resolveBlockItemSchema(schema);
|
|
814
|
+
const props = (itemSchema.properties ?? {});
|
|
815
|
+
const required = (itemSchema.required ?? []);
|
|
816
|
+
const lines = [
|
|
817
|
+
`Block: ${block}`,
|
|
818
|
+
`Array key: ${arrayKey}`,
|
|
819
|
+
`Required fields: ${required.join(", ")}`,
|
|
820
|
+
"All fields:",
|
|
821
|
+
];
|
|
822
|
+
for (const [name, fschema] of Object.entries(props)) {
|
|
823
|
+
const enumVals = Array.isArray(fschema.enum) ? fschema.enum : null;
|
|
824
|
+
const type = typeof fschema.type === "string"
|
|
825
|
+
? fschema.type
|
|
826
|
+
: typeof fschema.$ref === "string"
|
|
827
|
+
? fschema.$ref
|
|
828
|
+
: enumVals
|
|
829
|
+
? "enum"
|
|
830
|
+
: "object";
|
|
831
|
+
const enumSuffix = enumVals ? ` [enum: ${enumVals.join(", ")}]` : "";
|
|
832
|
+
lines.push(` - ${name}: ${type}${enumSuffix}`);
|
|
833
|
+
}
|
|
834
|
+
const idPattern = props.id?.pattern ?? "(none)";
|
|
835
|
+
lines.push(`ID pattern: ${idPattern}`);
|
|
836
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
406
837
|
return 0;
|
|
407
838
|
}
|
|
408
839
|
const identity = resolveIdentity();
|
|
409
840
|
injectWriter(op, parsed.params, identity);
|
|
841
|
+
injectArrayKey(op, parsed.params, parsed.cwd);
|
|
842
|
+
// FGAP-024 — append-block-item `--dry-run`: client-side prospective-whole-file
|
|
843
|
+
// validation. Replicates the op's autoId allocation, builds the prospective file
|
|
844
|
+
// {...existing, [arrayKey]: [...items, item]}, and validates it against the WHOLE
|
|
845
|
+
// block schema (matching exactly what appendToBlock validates on write) — then
|
|
846
|
+
// RETURNS before the auth/op-run block, so the frozen op is never invoked and
|
|
847
|
+
// nothing is written. The `--dryRun` flag itself never enters parsed.params, so
|
|
848
|
+
// the op would never see it even if reached.
|
|
849
|
+
if (op.name === "append-block-item" && parsed.dryRun) {
|
|
850
|
+
const block = parsed.params.block;
|
|
851
|
+
const arrayKey = parsed.params.arrayKey;
|
|
852
|
+
if (typeof arrayKey !== "string") {
|
|
853
|
+
process.stderr.write(`error: cannot resolve array key for block ${block}\n`);
|
|
854
|
+
return 2;
|
|
855
|
+
}
|
|
856
|
+
let item = (parsed.params.item ?? {});
|
|
857
|
+
if (parsed.params.autoId && (item == null || item.id === undefined)) {
|
|
858
|
+
try {
|
|
859
|
+
item = { ...item, id: nextId(parsed.cwd, block) };
|
|
860
|
+
}
|
|
861
|
+
catch (e) {
|
|
862
|
+
process.stderr.write(`error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
863
|
+
return 4;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
let existing = {};
|
|
867
|
+
try {
|
|
868
|
+
existing = readBlock(parsed.cwd, block);
|
|
869
|
+
}
|
|
870
|
+
catch {
|
|
871
|
+
existing = {};
|
|
872
|
+
}
|
|
873
|
+
const items = Array.isArray(existing[arrayKey]) ? existing[arrayKey] : [];
|
|
874
|
+
const prospective = { ...existing, [arrayKey]: [...items, item] };
|
|
875
|
+
try {
|
|
876
|
+
validateFromFile(schemaPath(parsed.cwd, block), prospective, `${block}.${arrayKey}[item]`);
|
|
877
|
+
}
|
|
878
|
+
catch (e) {
|
|
879
|
+
if (isValidationError(e)) {
|
|
880
|
+
process.stderr.write(`error: ${formatAjvError(e)}\n`);
|
|
881
|
+
return 5;
|
|
882
|
+
}
|
|
883
|
+
if (/schema (file )?not found/i.test(String(e.message))) {
|
|
884
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
885
|
+
return 3;
|
|
886
|
+
}
|
|
887
|
+
throw e;
|
|
888
|
+
}
|
|
889
|
+
process.stdout.write(`[dry-run] PASS${item.id ? ` — would append ${item.id}` : ""}\n`);
|
|
890
|
+
return 0;
|
|
891
|
+
}
|
|
410
892
|
const dctx = buildCliDispatchContext(parsed.explicitWriter, identity);
|
|
411
893
|
const decision = authDecision(op, {
|
|
412
894
|
yes: parsed.yes,
|
|
@@ -425,9 +907,12 @@ export async function main(argv) {
|
|
|
425
907
|
return 1;
|
|
426
908
|
}
|
|
427
909
|
}
|
|
910
|
+
// Resolve the effective render. `--format` wins; absent it, `--json` → json,
|
|
911
|
+
// else text — so `--json` and `--format json` are exact aliases.
|
|
912
|
+
const format = parsed.format ?? (parsed.json ? "json" : "text");
|
|
428
913
|
try {
|
|
429
914
|
const r = await op.run(parsed.cwd, parsed.params, dctx);
|
|
430
|
-
if (
|
|
915
|
+
if (format === "json") {
|
|
431
916
|
// FGAP-013: emit `output` as a JSON VALUE, not a stringified JSON string.
|
|
432
917
|
// Prose → the string itself; a read op → its structured ReadStructured
|
|
433
918
|
// (data + paging/cap metadata); a data op → its raw JSON value. No
|
|
@@ -441,22 +926,91 @@ export async function main(argv) {
|
|
|
441
926
|
const output = boundedJsonOutput(r);
|
|
442
927
|
process.stdout.write(`${JSON.stringify({ ok: true, op: op.name, output })}\n`);
|
|
443
928
|
}
|
|
929
|
+
else if (format === "table") {
|
|
930
|
+
// FGAP-021 — extract the renderable array, falling back to text whenever the
|
|
931
|
+
// result is not a complete tabular collection (over-cap, prose, or a non-array
|
|
932
|
+
// data op) so a degenerate one-row table never substitutes for the real output.
|
|
933
|
+
const arr = tabularRows(r);
|
|
934
|
+
if (arr !== null) {
|
|
935
|
+
process.stdout.write(`${renderTable(arr)}\n`);
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
process.stdout.write(`${renderOpResultText(r)}\n`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
444
941
|
else {
|
|
445
942
|
// Default text surface stays byte-identical to before: the shared renderer
|
|
446
943
|
// reproduces each op's prior `run()` text (prose / JSON.stringify / read footer).
|
|
447
|
-
|
|
944
|
+
// Ops declaring `verbatimText` (e.g. read-catalog-schema) emit their string
|
|
945
|
+
// OpResult byte-exact — reproducing the file's own bytes (its single trailing
|
|
946
|
+
// `\n` included) WITHOUT appending a second one — so the output round-trips to
|
|
947
|
+
// a file / diffs cleanly against an on-disk source whose bytes it reproduces
|
|
948
|
+
// verbatim (the catalog schema file carries its own trailing `\n`; appending
|
|
949
|
+
// another would double it to `}\n\n` and show a phantom trailing-empty-line).
|
|
950
|
+
if (op.verbatimText) {
|
|
951
|
+
process.stdout.write(renderOpResultText(r));
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
process.stdout.write(`${renderOpResultText(r)}\n`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
// TASK-037 — FEAT-006 T4: the `update` op returns the whole UpdateResult under
|
|
958
|
+
// `{ json }`; if it recorded any irreconcilable 3-way-merge conflicts, the CLI
|
|
959
|
+
// SURFACES them — it does NOT spawn a subordinate resolver. The CALLING agent
|
|
960
|
+
// reconciles via the existing `read-schema` / `write-schema` ops. On a NON-json
|
|
961
|
+
// surface (text or table), render the conflict report (renderConflicts carries the
|
|
962
|
+
// reconcile-via-write-schema guidance line) below the op's own output. Under
|
|
963
|
+
// `--format json` the structured `conflicts` array already prints in the op-result
|
|
964
|
+
// envelope above — do NOT double-emit. A non-`update` op, or an `update` with no
|
|
965
|
+
// conflicts, is a no-op here.
|
|
966
|
+
if (format !== "json" && op.name === "update" && r && typeof r === "object" && "json" in r) {
|
|
967
|
+
const update = r.json;
|
|
968
|
+
const conflicts = update?.conflicts;
|
|
969
|
+
if (Array.isArray(conflicts) && conflicts.length > 0) {
|
|
970
|
+
process.stdout.write(`${renderConflicts(conflicts)}\n`);
|
|
971
|
+
}
|
|
972
|
+
// TASK-048 — FGAP-077: surface the per-schema blocked-resync diagnostic
|
|
973
|
+
// (reason, version pair, per-item failures) below the op's own output on the
|
|
974
|
+
// non-json surface. Under --format json the structured blockedDetail array
|
|
975
|
+
// already prints in the op-result envelope above — do NOT double-emit.
|
|
976
|
+
const blockedDetail = update?.blockedDetail;
|
|
977
|
+
if (Array.isArray(blockedDetail) && blockedDetail.length > 0) {
|
|
978
|
+
process.stdout.write(`${renderBlocked(blockedDetail)}\n`);
|
|
979
|
+
}
|
|
448
980
|
}
|
|
449
981
|
return 0;
|
|
450
982
|
}
|
|
451
983
|
catch (err) {
|
|
452
|
-
|
|
453
|
-
|
|
984
|
+
// FGAP-023 — an AJV ValidationError surfaces field-named guidance (which field,
|
|
985
|
+
// what constraint) rather than the raw concatenated `.message`. The shaped message
|
|
986
|
+
// flows through both the `--json` envelope and the stderr line below.
|
|
987
|
+
const message = isValidationError(err) ? formatAjvError(err) : err instanceof Error ? err.message : String(err);
|
|
988
|
+
if (format === "json") {
|
|
454
989
|
process.stdout.write(`${JSON.stringify({ ok: false, op: op.name, error: message })}\n`);
|
|
455
990
|
}
|
|
456
991
|
else {
|
|
457
992
|
process.stderr.write(`error: ${message}\n`);
|
|
458
993
|
}
|
|
459
|
-
|
|
994
|
+
// FGAP-026 — granular exit codes distinguishing error classes. Name/message-based
|
|
995
|
+
// (instanceof is unreliable across the package boundary): validation → 5;
|
|
996
|
+
// not-initialized / BootstrapNotFoundError → 1 (generic runtime); schema-absent →
|
|
997
|
+
// 3; id-allocation failure → 4; everything else → 1. Usage/arg errors are 2,
|
|
998
|
+
// classified earlier at the UsageError catch. The message emit above is unchanged.
|
|
999
|
+
// Ordering matters: schema-absent is tested BEFORE id-allocation because the
|
|
1000
|
+
// schema-missing throw from nextId ("nextId: schema not found for block …") also
|
|
1001
|
+
// matches the id-allocation pattern — its true cause is the absent schema (→ 3),
|
|
1002
|
+
// not an allocation failure (→ 4). The genuine allocation throws (no id.pattern;
|
|
1003
|
+
// not prefix+width parseable) do not contain "schema not found" and stay 4.
|
|
1004
|
+
let code = 1;
|
|
1005
|
+
if (isValidationError(err))
|
|
1006
|
+
code = 5;
|
|
1007
|
+
else if (err instanceof Error && err.name === "BootstrapNotFoundError")
|
|
1008
|
+
code = 1;
|
|
1009
|
+
else if (err instanceof Error && /schema (file )?not found/i.test(err.message))
|
|
1010
|
+
code = 3;
|
|
1011
|
+
else if (err instanceof Error && /nextId|id pattern|allocate/i.test(err.message))
|
|
1012
|
+
code = 4;
|
|
1013
|
+
return code;
|
|
460
1014
|
}
|
|
461
1015
|
}
|
|
462
1016
|
//# sourceMappingURL=cli.js.map
|