@descent-vtt/spec-brief 0.8.0 → 0.10.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +56 -6
  3. package/dist/archive.js.map +1 -1
  4. package/dist/brief.js.map +1 -1
  5. package/dist/cli.d.ts +18 -1
  6. package/dist/cli.js +273 -55
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config.js +3 -2
  9. package/dist/config.js.map +1 -1
  10. package/dist/corpus.js +1 -0
  11. package/dist/corpus.js.map +1 -1
  12. package/dist/engine.d.ts +43 -0
  13. package/dist/engine.js +74 -12
  14. package/dist/engine.js.map +1 -1
  15. package/dist/fs.d.ts +15 -1
  16. package/dist/fs.js +21 -8
  17. package/dist/fs.js.map +1 -1
  18. package/dist/git.d.ts +11 -0
  19. package/dist/git.js +38 -4
  20. package/dist/git.js.map +1 -1
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js.map +1 -1
  23. package/dist/lint.js.map +1 -1
  24. package/dist/literal.d.ts +11 -0
  25. package/dist/literal.js +14 -0
  26. package/dist/literal.js.map +1 -0
  27. package/dist/markdown.js.map +1 -1
  28. package/dist/plugins.js +2 -0
  29. package/dist/plugins.js.map +1 -1
  30. package/dist/report.js +3 -2
  31. package/dist/report.js.map +1 -1
  32. package/dist/rules.js +2 -1
  33. package/dist/rules.js.map +1 -1
  34. package/dist/schedule.js.map +1 -1
  35. package/dist/schema.js.map +1 -1
  36. package/dist/scope.js.map +1 -1
  37. package/dist/text.js.map +1 -1
  38. package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -1
  39. package/dist/vendor/spec-core/markdown/headings.js.map +1 -1
  40. package/dist/vendor/spec-core/markdown/links.js.map +1 -1
  41. package/dist/vendor/spec-core/markdown/scan.js.map +1 -1
  42. package/dist/vendor/spec-core/pattern/automaton.js.map +1 -1
  43. package/dist/vendor/spec-core/pattern/charset.js.map +1 -1
  44. package/dist/vendor/spec-core/pattern/regex.js.map +1 -1
  45. package/dist/vendor/spec-core/text/lines.js.map +1 -1
  46. package/dist/vendor/spec-core/text/width.js.map +1 -1
  47. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -7,11 +7,13 @@
7
7
  * must never exit as though it found nothing.
8
8
  */
9
9
  import { readFile, stat } from 'node:fs/promises';
10
+ import { resolve } from 'node:path';
10
11
  import { parseArgs } from 'node:util';
11
12
  import { TransactionError, ConflictError } from './apply.js';
12
13
  import { reported } from './collisions.js';
13
14
  import { CONFIG_FILES, ConfigError, initialConfig } from './config.js';
14
- import { BriefEngine, EngineError, today } from './engine.js';
15
+ import { idKey } from './corpus.js';
16
+ import { BriefEngine, EngineError, isDate, today } from './engine.js';
15
17
  import { NodeFileSystem } from './fs.js';
16
18
  import { GitError } from './git.js';
17
19
  import { failing, sortFindings, summarise } from './lint.js';
@@ -93,11 +95,11 @@ function untrue(command, option) {
93
95
  return command === 'init' || command === 'new' || command === 'list';
94
96
  case 'color':
95
97
  return command === 'init' || command === 'new';
96
- // Each caller asks only whether the answer is true, so the mutant that
97
- // leaves this case out, and answers nothing, is equivalent.
98
- default:
99
- return false;
100
98
  }
99
+ // After the switch and not as its `default`: a `default` with its return
100
+ // taken out answers nothing, which each caller reads as false, and no test
101
+ // can tell that mutant from the code (ADR-0009).
102
+ return false;
101
103
  }
102
104
  /**
103
105
  * Whether the option asks the command to leave out what it never does, and
@@ -115,11 +117,9 @@ function alreadySo(command, option) {
115
117
  return command === 'init';
116
118
  case 'no-color':
117
119
  return command === 'init' || command === 'new';
118
- // Each caller asks only whether the answer is true, so the mutant that
119
- // leaves this case out, and answers nothing, is equivalent.
120
- default:
121
- return false;
122
120
  }
121
+ // After the switch, for the reason `untrue` has.
122
+ return false;
123
123
  }
124
124
  /** Options by their names on the command line, as a sentence lists them. */
125
125
  function listed(names) {
@@ -151,8 +151,137 @@ function unread(command, spec, values) {
151
151
  }
152
152
  if (flag(values, 'color') && flag(values, 'no-color'))
153
153
  return '--color is given beside --no-color, which turns colour off; give one of the two';
154
+ // The engine refuses the pair as well, for a caller of the library; here it
155
+ // is refused with the line, where --help and --version are no way past it.
156
+ if (flag(values, 'no-git') && (text(values, 'commit') !== undefined || text(values, 'base') !== undefined)) {
157
+ return 'a commit or a base needs git; drop --no-git, or drop --commit and --base';
158
+ }
159
+ return null;
160
+ }
161
+ /**
162
+ * A resolved path as two values of one option compare. On Windows a drive
163
+ * letter names its volume in either case, and a path resolved there starts
164
+ * with its drive letter or a backslash, so its first character is folded;
165
+ * the rest is compared as resolved (spec-core ADR-0005, amended 2026-10-19).
166
+ * A path written `\\?\c:\x` keeps its letter as written.
167
+ */
168
+ export function onVolume(path, platform = process.platform) {
169
+ // Folded to either case, two letters compare the same, and the folded path is never shown.
170
+ return platform === 'win32' ? `${path.charAt(0).toUpperCase()}${path.slice(1)}` : path;
171
+ }
172
+ /**
173
+ * An option's value as the command reads it, so that one thing written two
174
+ * ways is asked for once: a directory as the run resolves it, its drive
175
+ * letter in either case, a number by what its digits count, a list by the
176
+ * set of ids it names, each as a brief's id is matched, a summary as it is
177
+ * trimmed. Any other value is read as it is written, in its own case, which
178
+ * is how each of those options reads it. A revision is read by git, which
179
+ * the run asks once there are two (`sameCommits`).
180
+ */
181
+ function asRead(option, value, root) {
182
+ switch (option) {
183
+ case 'root':
184
+ return onVolume(resolve(value));
185
+ case 'config':
186
+ return onVolume(resolve(root, value));
187
+ case 'wave':
188
+ case 'pr':
189
+ return String(digits(value) ?? value);
190
+ case 'depends-on':
191
+ // `7` and `007` name one brief, and `8,7` the briefs `7,8` does.
192
+ return JSON.stringify([...new Set(idsOf(value).map(idKey))].sort());
193
+ case 'summary':
194
+ return value.trim();
195
+ case 'briefs':
196
+ case 'archive':
197
+ // Last: one outside the repository leaves the switch and is read as it
198
+ // was written, for `directories` to refuse by what it was given. A catch
199
+ // that returned it would have a mutant, its return taken out, that fell
200
+ // into the next case and read it the same, which no test can tell apart.
201
+ try {
202
+ return normalisePath(value);
203
+ }
204
+ catch { }
205
+ }
206
+ // After the switch, for the reason `untrue` has.
207
+ return value;
208
+ }
209
+ /** The refusal of an option given two values, each shown as JSON, as an empty value is, where what cannot be seen can be. */
210
+ function twoValues(option, first, second) {
211
+ return `--${option} is given ${JSON.stringify(first)} and ${JSON.stringify(second)}, and takes one value; give one of the two`;
212
+ }
213
+ /**
214
+ * Why a line that gives one option two values is not one to run: the later
215
+ * was read, and the earlier went unread with no word of it, so `--root
216
+ * nowhere --root .` ran in `.` (spec-core ADR-0005, amended 2026-10-18).
217
+ * Two revisions written two ways are kept in `revisions`, where it is
218
+ * given, for git to say whether they name one commit.
219
+ */
220
+ function twice(options, values, cwd, revisions) {
221
+ // A configuration is found from the root, as the engine finds it.
222
+ const root = resolve(text(values, 'root') ?? cwd);
223
+ const first = new Map();
224
+ for (const token of options) {
225
+ // A flag has no value to compare, and one given twice is that flag once.
226
+ // Taken in here it would be read as nothing each time, which decides
227
+ // nothing either: the mutant that takes it in is equivalent.
228
+ if (token.value === undefined)
229
+ continue;
230
+ const read = asRead(token.name, token.value, root);
231
+ const earlier = first.get(token.name);
232
+ if (earlier === undefined)
233
+ first.set(token.name, { written: token.value, read });
234
+ else if (earlier.read !== read) {
235
+ if (revisions === null || (token.name !== 'commit' && token.name !== 'base'))
236
+ return twoValues(token.name, earlier.written, token.value);
237
+ revisions.set(token.name, [...(revisions.get(token.name) ?? [earlier.written]), token.value]);
238
+ }
239
+ }
240
+ return null;
241
+ }
242
+ /**
243
+ * `--help` beside `--version` asks for two answers, and printed one of
244
+ * them; the other went unread. Named as each was written, `-h` or `--help`.
245
+ */
246
+ function answers(options) {
247
+ let asked;
248
+ for (const token of options) {
249
+ if (token.name !== 'help' && token.name !== 'version')
250
+ continue;
251
+ if (asked === undefined)
252
+ asked = token;
253
+ else if (asked.name !== token.name)
254
+ return `${asked.rawName} is given beside ${token.rawName}, and one of the two answers would go unread; give one of the two`;
255
+ }
154
256
  return null;
155
257
  }
258
+ /**
259
+ * What a command's own options hold, refused where the line is read, by what
260
+ * each holds: a number, a list of ids, a date, a revision, a directory, and a
261
+ * second brief where a command takes one. What only the tree can answer - a
262
+ * root that is not there, an id that is taken, a type the configuration does
263
+ * not have - is asked by the run, which `--help` and `--version` do not start.
264
+ */
265
+ function checkValues(command, values, positionals) {
266
+ integer(values, 'wave', 0);
267
+ integer(values, 'pr', 1);
268
+ dependencies(values);
269
+ const date = text(values, 'date');
270
+ if (date !== undefined && !isDate(date))
271
+ throw new UsageError(`"${date}" is not a date written YYYY-MM-DD`);
272
+ for (const option of ['commit', 'base']) {
273
+ // Handed to git, it would be read as one of git's options.
274
+ const revision = text(values, option);
275
+ if (revision?.startsWith('-') === true)
276
+ throw new UsageError(`"${revision}" is not a revision`);
277
+ }
278
+ // Only init takes --briefs and --archive; for any other command they are
279
+ // the defaults, which are two directories inside the root.
280
+ directories(values);
281
+ if ((command === 'archive' || command === 'unarchive') && positionals.length > 1) {
282
+ throw new UsageError(`${command} takes one brief: spec-brief ${command} <brief>`);
283
+ }
284
+ }
156
285
  /**
157
286
  * The commands by name, and nothing for a command line that names none. A map,
158
287
  * because an object answers to more names than it was given: `constructor` and
@@ -243,6 +372,12 @@ are --config beside --no-config and --color beside --no-color, of which one
243
372
  would go unread. An option that asks a command to leave out what it never
244
373
  does is taken, as init --no-git: it asks for what is already so.
245
374
 
375
+ An option that takes one value and is given two is refused, as --root a
376
+ --root b, since the earlier would go unread; one value given twice, however
377
+ it is written, is taken once, as --root . --root ./. A line refused for any
378
+ of these is refused with --help or --version on it too, and so is --help
379
+ beside --version.
380
+
246
381
  Exit codes: 0 clean, 1 findings or refused, 2 the run could not be trusted.
247
382
  `;
248
383
  /** Global options that take a value, so the value is not mistaken for the command. */
@@ -261,7 +396,14 @@ function commandOf(argv) {
261
396
  }
262
397
  return undefined;
263
398
  }
264
- export function parse(argv) {
399
+ /**
400
+ * The line, read and checked. Everything the line alone decides is refused
401
+ * here, before `--help` and `--version` are answered, as an option the
402
+ * parser does not know always was: a wrong line is wrong with either of them
403
+ * on it (spec-core ADR-0005, amended 2026-10-18). `cwd` is the directory a
404
+ * run without `--root` starts in, from which a `--config` is found.
405
+ */
406
+ export function parse(argv, cwd = process.cwd()) {
265
407
  const command = commandOf(argv);
266
408
  const spec = COMMANDS.get(command);
267
409
  if (command !== undefined && spec === undefined) {
@@ -269,12 +411,17 @@ export function parse(argv) {
269
411
  }
270
412
  let parsed;
271
413
  try {
272
- parsed = parseArgs({
414
+ const read = parseArgs({
273
415
  args: [...argv],
274
416
  options: { ...GLOBAL, ...(spec?.options ?? {}) },
275
417
  allowPositionals: true,
276
418
  strict: true,
419
+ tokens: true,
277
420
  });
421
+ // The cast stands on the name, with nothing under it: Stryker makes no
422
+ // mutant of what an `as` holds, and the call above is what the parser
423
+ // is asked (ADR-0009).
424
+ parsed = read;
278
425
  }
279
426
  catch (error) {
280
427
  throw new UsageError(error.message);
@@ -294,7 +441,48 @@ export function parse(argv) {
294
441
  const extra = positionals.map((argument) => `"${argument}"`).join(', ');
295
442
  throw new UsageError(`${command} takes no argument, not ${extra}: spec-brief ${command}${spec.noArgument}`);
296
443
  }
297
- return { command, values: parsed.values, positionals };
444
+ const options = parsed.tokens.filter((token) => token.kind === 'option');
445
+ // Two revisions written two ways are git's to read, by the run: `--help`
446
+ // and `--version` start none, and `--no-git` asks git nothing, so with
447
+ // any of the three they are two values, as the line reads them.
448
+ const revisions = flag(parsed.values, 'help') || flag(parsed.values, 'version') || flag(parsed.values, 'no-git') ? null : new Map();
449
+ const refusal = twice(options, parsed.values, cwd, revisions) ??
450
+ answers(options) ??
451
+ (spec === undefined ? null : (unread(command, spec, parsed.values) ?? unwritten(command, spec, parsed.values)));
452
+ if (refusal !== null)
453
+ throw new UsageError(refusal);
454
+ checkValues(command, parsed.values, positionals);
455
+ return { command, values: parsed.values, positionals, revisions: revisions ?? new Map() };
456
+ }
457
+ /**
458
+ * Two revisions of one option are one value where the run's git says both
459
+ * name one commit, and two where it names two, or cannot say of one: with
460
+ * no repository, outside one, or for a revision that names none. It is asked
461
+ * once, for every revision of both options.
462
+ */
463
+ async function sameCommits(engine, revisions) {
464
+ // Asked nothing, a git process answers nothing: the mutant that starts one is equivalent.
465
+ if (revisions.size === 0)
466
+ return;
467
+ const asked = [...revisions.values()].flat();
468
+ // A git a caller of the library hands the engine may have no `commits`;
469
+ // the command line's is always NodeGit's, which has, so the mutant that
470
+ // calls it without asking is equivalent here.
471
+ const named = await engine.git?.commits?.(asked);
472
+ const commitOf = new Map(asked.map((revision, index) => [revision, named?.[index] ?? null]));
473
+ for (const [option, [first, ...later]] of revisions) {
474
+ const commit = commitOf.get(first) ?? null;
475
+ const other = later.find((revision) => commit === null || commitOf.get(revision) !== commit);
476
+ if (other !== undefined)
477
+ throw new UsageError(twoValues(option, first, other));
478
+ }
479
+ }
480
+ /** A `--format` the command does not write, which the help lists by command. */
481
+ function unwritten(command, spec, values) {
482
+ const asked = text(values, 'format') ?? 'pretty';
483
+ // Cast on the name, as above. The check below is what makes it true.
484
+ const format = asked;
485
+ return spec.formats.includes(format) ? null : `--format for ${command} is one of ${spec.formats.join(', ')}, not "${format}"`;
298
486
  }
299
487
  /** The value of an option declared a string, which is all `parseArgs` gives one. */
300
488
  function text(values, key) {
@@ -303,15 +491,63 @@ function text(values, key) {
303
491
  function flag(values, key) {
304
492
  return values[key] === true;
305
493
  }
494
+ /** The number a value's digits count, or nothing for a value that is not digits alone. */
495
+ function digits(raw) {
496
+ return /^\d+$/.test(raw) ? Number(raw) : undefined;
497
+ }
306
498
  function integer(values, key, minimum) {
307
499
  const raw = text(values, key);
308
500
  if (raw === undefined)
309
501
  return undefined;
310
- if (!/^\d+$/.test(raw) || Number(raw) < minimum)
502
+ const number = digits(raw);
503
+ if (number === undefined || number < minimum)
311
504
  throw new UsageError(`--${key} must be a whole number of at least ${minimum}, not "${raw}"`);
312
- return Number(raw);
505
+ return number;
506
+ }
507
+ /** The ids of a list written with commas between them, spaces beside each taken off. */
508
+ function idsOf(listed) {
509
+ return listed.split(',').map((id) => id.trim());
510
+ }
511
+ /** `--depends-on` as the ids it names, or nothing where it is not given. */
512
+ function dependencies(values) {
513
+ const listed = text(values, 'depends-on');
514
+ const dependsOn = listed === undefined ? undefined : idsOf(listed);
515
+ // A list that names no brief is not a brief that depends on none: written
516
+ // as given, "," scaffolded a brief with no dependency and said nothing.
517
+ if (dependsOn?.every((d) => d === '')) {
518
+ throw new UsageError(`--depends-on is ${JSON.stringify(listed)}, which names no brief; give it ids with commas between them, as in 7,8, or leave the option out`);
519
+ }
520
+ // And a place in it that names none is a brief left out: "$A,$B" with one
521
+ // of the two unset scaffolded a brief that depends on the other alone.
522
+ if (dependsOn?.includes('')) {
523
+ throw new UsageError(`--depends-on is ${JSON.stringify(listed)}, which names no brief in one of its places; give it ids with one comma between each two, as in 7,8, and none before or after`);
524
+ }
525
+ return dependsOn;
526
+ }
527
+ /**
528
+ * The directories `init` sets up. One above the root is refused as the
529
+ * configuration refuses one (config.ts), in the same words, before anything
530
+ * is written.
531
+ */
532
+ function directories(values) {
533
+ const inside = (option, given) => {
534
+ try {
535
+ return normalisePath(given);
536
+ }
537
+ catch {
538
+ throw new UsageError(`--${option} must be a directory inside the repository, not "${given}"`);
539
+ }
540
+ };
541
+ const briefs = inside('briefs', text(values, 'briefs') ?? 'briefs');
542
+ const archive = inside('archive', text(values, 'archive') ?? `${briefs}/archive`);
543
+ if (briefs === '' || archive === '' || briefs === archive)
544
+ throw new UsageError('--briefs and --archive must be two directories');
545
+ return { briefs, archive };
313
546
  }
314
547
  async function version() {
548
+ // `JSON.parse` reads a file's bytes as it reads their text in UTF-8, so the
549
+ // encoding says what the manifest is and decides nothing: with none named,
550
+ // the version read is the same, and no test can tell the two apart.
315
551
  const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
316
552
  return pkg.version;
317
553
  }
@@ -336,12 +572,14 @@ function useColor(values, io) {
336
572
  return false;
337
573
  return (io.stdout ?? process.stdout).isTTY === true;
338
574
  }
339
- async function openEngine(run) {
575
+ /** The engine for a run, which asks git nothing where `needsGit` is false and a configuration file names the root. */
576
+ async function openEngine(run, needsGit = true) {
340
577
  return BriefEngine.open({
341
578
  cwd: text(run.values, 'root') ?? run.cwd,
342
579
  config: text(run.values, 'config'),
343
580
  ...(flag(run.values, 'no-git') ? { git: null } : {}),
344
581
  noConfig: flag(run.values, 'no-config'),
582
+ needsGit,
345
583
  });
346
584
  }
347
585
  function emitFindings(run, command, findings, checked, extra = {}) {
@@ -374,20 +612,7 @@ async function runInit(run) {
374
612
  if (await fs.exists(name))
375
613
  throw new UsageError(`${name} already exists here; edit it instead`);
376
614
  }
377
- // A directory above the root is refused as the configuration refuses one
378
- // (config.ts), in the same words, before anything is written.
379
- const inside = (option, given) => {
380
- try {
381
- return normalisePath(given);
382
- }
383
- catch {
384
- throw new UsageError(`--${option} must be a directory inside the repository, not "${given}"`);
385
- }
386
- };
387
- const briefs = inside('briefs', text(run.values, 'briefs') ?? 'briefs');
388
- const archive = inside('archive', text(run.values, 'archive') ?? `${briefs}/archive`);
389
- if (briefs === '' || archive === '' || briefs === archive)
390
- throw new UsageError('--briefs and --archive must be two directories');
615
+ const { briefs, archive } = directories(run.values);
391
616
  const config = `${JSON.stringify(initialConfig(briefs, archive), null, 2)}\n`;
392
617
  await fs.write('.spec-brief.json', config);
393
618
  const written = ['.spec-brief.json'];
@@ -408,18 +633,7 @@ async function runNew(run) {
408
633
  const title = run.positionals.join(' ').trim();
409
634
  if (title === '')
410
635
  throw new UsageError('new needs a title: spec-brief new "<title>"');
411
- const listed = text(run.values, 'depends-on');
412
- const dependsOn = listed?.split(',').map((d) => d.trim());
413
- // A list that names no brief is not a brief that depends on none: written
414
- // as given, "," scaffolded a brief with no dependency and said nothing.
415
- if (dependsOn?.every((d) => d === '')) {
416
- throw new UsageError(`--depends-on is ${JSON.stringify(listed)}, which names no brief; give it ids with commas between them, as in 7,8, or leave the option out`);
417
- }
418
- // And a place in it that names none is a brief left out: "$A,$B" with one
419
- // of the two unset scaffolded a brief that depends on the other alone.
420
- if (dependsOn?.includes('')) {
421
- throw new UsageError(`--depends-on is ${JSON.stringify(listed)}, which names no brief in one of its places; give it ids with one comma between each two, as in 7,8, and none before or after`);
422
- }
636
+ const dependsOn = dependencies(run.values);
423
637
  const engine = await openEngine(run);
424
638
  const created = await engine.create({
425
639
  title,
@@ -443,7 +657,9 @@ async function runLint(run) {
443
657
  return emitFindings(run, 'lint', findings, checked, { sections: sectionsJson(engine.config) });
444
658
  }
445
659
  async function runList(run) {
446
- const engine = await openEngine(run);
660
+ // Nothing `list` answers is read from git, so git is asked nothing where
661
+ // the root is a configuration file's, and the answer can say so.
662
+ const engine = await openEngine(run, false);
447
663
  engine.requireBriefs();
448
664
  const briefs = flag(run.values, 'archived') ? engine.corpus.briefs : engine.corpus.live;
449
665
  const ready = new Set(engine.ready());
@@ -451,10 +667,14 @@ async function runList(run) {
451
667
  .map((brief) => ({ brief, ready: ready.has(brief), waitingOn: engine.waitingOn(brief) }))
452
668
  .filter((row) => !flag(run.values, 'ready') || row.ready);
453
669
  if (run.format === 'json') {
670
+ const { keep, ...read } = engine.inputs();
454
671
  run.out(jsonDocument('list', run.version, {
455
672
  ok: true,
456
673
  briefs: rows.map((r) => briefJson(r.brief, { ready: r.ready, waitingOn: r.waitingOn })),
457
674
  sections: sectionsJson(engine.config),
675
+ // What the answer was read from, for a tool that would give it again
676
+ // without starting this one, and the version that read it.
677
+ inputs: { keep, tool: run.version, ...read },
458
678
  }));
459
679
  }
460
680
  else {
@@ -503,9 +723,11 @@ async function runSchedule(run) {
503
723
  }
504
724
  async function runTransition(run, action) {
505
725
  const reference = run.positionals[0];
506
- if (reference === undefined || run.positionals.length > 1)
726
+ // A second brief is refused with the line; none at all is what --help is asked for.
727
+ if (reference === undefined)
507
728
  throw new UsageError(`${action} takes one brief: spec-brief ${action} <brief>`);
508
729
  const engine = await openEngine(run);
730
+ await sameCommits(engine, run.revisions);
509
731
  const dryRun = flag(run.values, 'dry-run');
510
732
  const plan = action === 'archive'
511
733
  ? await engine.planArchive(reference, {
@@ -516,7 +738,8 @@ async function runTransition(run, action) {
516
738
  date: text(run.values, 'date') ?? run.today(),
517
739
  allowDirty: flag(run.values, 'allow-dirty'),
518
740
  strict: run.strict,
519
- noGit: flag(run.values, 'no-git'),
741
+ // No noGit: under --no-git the engine is opened without git, and a
742
+ // commit or a base beside it is refused with the line.
520
743
  })
521
744
  : engine.planUnarchive(reference, { strict: run.strict });
522
745
  const refused = plan.blocking.length > 0;
@@ -536,13 +759,13 @@ export async function main(argv = process.argv.slice(2), io = {}) {
536
759
  };
537
760
  let parsed;
538
761
  try {
539
- parsed = parse(argv);
762
+ parsed = parse(argv, io.cwd ?? process.cwd());
540
763
  }
541
764
  catch (error) {
542
765
  err(error.message);
543
766
  return EXIT_ERROR;
544
767
  }
545
- const { command, values, positionals } = parsed;
768
+ const { command, values, positionals, revisions } = parsed;
546
769
  // From here on, so that what comes before a command is covered as the
547
770
  // command is: --version and --help, the package's own manifest, the colour.
548
771
  // An error there left main as a rejection, which the launcher ended as
@@ -557,15 +780,9 @@ export async function main(argv = process.argv.slice(2), io = {}) {
557
780
  // Asked for, the help is the answer; printed because no command was named, it is the error.
558
781
  return flag(values, 'help') ? EXIT_OK : EXIT_ERROR;
559
782
  }
560
- const spec = COMMANDS.get(command);
561
- const refusal = unread(command, spec, values);
562
- if (refusal !== null)
563
- throw new UsageError(refusal);
564
- const format = (text(values, 'format') ?? 'pretty');
565
- if (!spec.formats.includes(format)) {
566
- err(`--format for ${command} is one of ${spec.formats.join(', ')}, not "${format}"`);
567
- return EXIT_ERROR;
568
- }
783
+ const asked = text(values, 'format') ?? 'pretty';
784
+ // Cast on the name, as in `parse`, which refused a format the command does not write.
785
+ const format = asked;
569
786
  // A file, or a name with nothing behind it, had the search for a
570
787
  // configuration go on from its parent: the run reported on a tree nobody
571
788
  // had named, and `new` wrote into it. A path that cannot be reached is
@@ -591,6 +808,7 @@ export async function main(argv = process.argv.slice(2), io = {}) {
591
808
  version: await version(),
592
809
  values,
593
810
  positionals,
811
+ revisions,
594
812
  cwd: io.cwd ?? process.cwd(),
595
813
  };
596
814
  switch (command) {