@rungs/cli 0.1.2 → 0.2.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/src/cli.ts CHANGED
@@ -3,11 +3,14 @@ import { fileURLToPath } from 'node:url';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  import { auditModules, loadAllModules } from './manifest.ts';
5
5
  import { detect, scanRepo } from './detect.ts';
6
- import { addModule, adoptableGates, registerGates, resolveInstallOrder, writeInstallRecord } from './add.ts';
6
+ import { addModule, adoptableGates, blockedByParadigm, registerGates, resolveInstallOrder, writeInstallRecord } from './add.ts';
7
7
  import { render, writeReport, type Harness } from './render.ts';
8
8
  import { resolveParams } from './substitute.ts';
9
- import { appendLedger, ledgerQuestions, loadRegistry, runGates } from './check.ts';
9
+ import { appendLedger, type GateRun, ledgerQuestions, loadRegistry, runGates, UnknownTierError } from './check.ts';
10
10
  import { applyUpgrade, eject, planUpgrade, PROFILES, readRecord, setupGit } from './lifecycle.ts';
11
+ import { explain, IN_SCOPE as EXPLAINABLE } from './explain.ts';
12
+ import { applyArchive, planArchive } from './backlog.ts';
13
+ import { existsSync } from 'node:fs';
11
14
  import type { DetectResult, Manifest } from './types.ts';
12
15
 
13
16
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -65,7 +68,7 @@ function cmdModules(showParams = false) {
65
68
  const issues = auditModules(mods);
66
69
  console.log();
67
70
  if (issues.length === 0) {
68
- console.log(c.green(' audit clean') + c.dim(' — every parameter accounted for, every gate has a table and a why'));
71
+ console.log(c.green(' audit clean') + c.dim(' — every parameter accounted for; every gate has a table, a why, and a declared applicability'));
69
72
  } else {
70
73
  console.log(c.red(` ${issues.length} issue(s):`));
71
74
  for (const i of issues) console.log(` ${c.yellow(i.module)} ${c.dim(i.kind)} — ${i.detail}`);
@@ -74,7 +77,7 @@ function cmdModules(showParams = false) {
74
77
  return issues.length === 0 ? 0 : 1;
75
78
  }
76
79
 
77
- function cmdDoctor(target: string) {
80
+ function cmdDoctor(target: string, doExplain = false) {
78
81
  const root = resolve(target);
79
82
  const mods = loadAllModules(MODULES);
80
83
  console.log(c.bold(`\nrungs doctor — ${root}\n`));
@@ -153,6 +156,11 @@ function cmdDoctor(target: string) {
153
156
  console.log(c.dim(' system is good, complete, or working — only that files are where a'));
154
157
  console.log(c.dim(" module's files would be. Signatures under-detect on purpose.\n"));
155
158
 
159
+ reportLedger(root);
160
+
161
+ if (doExplain) reportExplain(mods, results, root, files);
162
+ else advertiseAnalysis(results);
163
+
156
164
  // `doctor` is the command the README makes the entry point, and it used to stop on the sentence
157
165
  // above — fifteen `absent` lines and nothing to do next. The recommendation is deliberately a
158
166
  // **single** command, and never the maximal one: the brief names selling rung 5 to a rung-1 repo
@@ -186,6 +194,101 @@ function firstSentence(s: string): string {
186
194
  return s.trim().replace(/\s+/g, ' ').split(/(?<=\.)\s/)[0];
187
195
  }
188
196
 
197
+ /**
198
+ * Say that the analysis exists, and how much of it there is. Never what it
199
+ * found (WI-049).
200
+ *
201
+ * `--explain` is the capability both external reviews called the strongest
202
+ * thing here, and plain `doctor` printed no occurrence of the string `explain`
203
+ * — it was reachable only from `--help`. WI-038 put the *findings* behind a flag
204
+ * for a measured reason: 114 on `hexguard` would bury the `Next` line that
205
+ * WI-005 exists to protect. The flag was never the problem; the silence was.
206
+ *
207
+ * **It reports scope, not findings, and it runs no engine.** The first version
208
+ * printed a finding count, which meant running the detectors on the plain path.
209
+ * Measured on `rift-forge` 2026-08-16: plain `doctor` went from **1.6s to
210
+ * 16.8s** warm — a 10× tax on the entry point to advertise a flag. WI-049's
211
+ * plan named this outcome in advance and named this fallback.
212
+ *
213
+ * So the number is the one detection already computed. It claims what it can
214
+ * prove: these are things the repo has, and our checks can read them. It does
215
+ * not claim anything was found, because finding out costs the 15 seconds.
216
+ */
217
+ function advertiseAnalysis(results: DetectResult[]) {
218
+ const inScope = results.filter((r) => EXPLAINABLE.has(r.state)).length;
219
+ if (!inScope) return;
220
+
221
+ console.log(c.bold(' Analysis\n'));
222
+ console.log(` ${inScope} of these are things this repo already has, and can be checked against it.`);
223
+ console.log(` ${c.cyan('rungs doctor --explain')} ${c.dim('— evidenced findings, and the incident behind each check')}\n`);
224
+ }
225
+
226
+ /**
227
+ * The defect half of `doctor` (WI-038). Every line carries a path and a count
228
+ * or a quote; there is no score, grade, bar, or maturity label anywhere, and
229
+ * there is not going to be — ADR-0005 tier C refuses composites permanently,
230
+ * and a single word over incommensurable signals is the purest form of the
231
+ * probe-encoding-a-guess the corpus warns about.
232
+ *
233
+ * The incident is attached to each detector rather than to each finding: it is
234
+ * why the check exists, not what was found, and repeating it per row would bury
235
+ * the evidence under the provenance.
236
+ */
237
+ function reportExplain(mods: Manifest[], results: DetectResult[], root: string, files: string[]) {
238
+ const { reported, skipped, scope } = explain(mods, results, root, files);
239
+
240
+ console.log(c.bold(' What it also checked\n'));
241
+
242
+ if (!scope.length) {
243
+ console.log(c.dim(' Nothing — detectors run only over what this repo already has, and'));
244
+ console.log(c.dim(' detection found no equivalent of any module. There is nothing here to'));
245
+ console.log(c.dim(' check that would not be checking our conventions against your repo.\n'));
246
+ return;
247
+ }
248
+
249
+ const total = reported.reduce((n, r) => n + r.findings.length, 0);
250
+ console.log(
251
+ c.dim(` ran the detectors for ${scope.length} module(s) this repo already has: `) + c.dim(scope.join(' ')) + '\n',
252
+ );
253
+
254
+ for (const r of reported) {
255
+ const n = r.findings.length;
256
+ console.log(` ${c.yellow(r.gate.padEnd(34))} ${c.bold(String(n))} ${n === 1 ? 'finding' : 'findings'}`);
257
+ for (const f of r.findings.slice(0, 4)) {
258
+ console.log(c.dim(` ${f.file ? `${f.file}: ` : ''}${f.message}`));
259
+ }
260
+ if (n > 4) console.log(c.dim(` …and ${n - 4} more`));
261
+ if (r.why) console.log(c.dim(` why: ${firstSentence(r.why)}`));
262
+ console.log();
263
+ }
264
+
265
+ if (!total) {
266
+ console.log(c.dim(' No detector fired. That is not a clean bill of health — see below.\n'));
267
+ }
268
+
269
+ // Pins. ADR-0005's rule that green must never read as verified applies with
270
+ // more force here than in the ledger: this pass runs our checks over content
271
+ // written to somebody else's conventions, and the honest failure mode is a
272
+ // sound finding in a frame the repo never adopted.
273
+ console.log(c.dim(' This is not an audit, and it is deliberately incomplete:'));
274
+ console.log(c.dim(' · Detectors ran only for modules this repo already has an equivalent of.'));
275
+ console.log(c.dim(" · They read rungs-shaped inputs. A finding may be true and framed against"));
276
+ console.log(c.dim(' a convention you never adopted — that is our defect, not yours.'));
277
+ if (skipped.command) {
278
+ console.log(c.dim(` · ${skipped.command} command gate(s) not run. rungs does not execute commands in a repo it is only reading.`));
279
+ }
280
+ if (skipped.undeclared.length) {
281
+ console.log(c.dim(` · ${skipped.undeclared.length} gate(s) never said whether they can read a repo like yours, so they did not: ${skipped.undeclared.join(' ')}`));
282
+ }
283
+ if (skipped.unimplemented.length) {
284
+ console.log(c.dim(` · ${skipped.unimplemented.length} declared gate(s) have no engine and were skipped, never passed: ${skipped.unimplemented.join(' ')}`));
285
+ }
286
+ for (const e of skipped.errored) {
287
+ console.log(c.dim(` · ${e.gate} could not run here (${e.message}) — a fact about this pass, not about your repo.`));
288
+ }
289
+ console.log();
290
+ }
291
+
189
292
  function cmdAdd(names: string[], root: string, dryRun: boolean, harnesses: Harness[], stamp: string) {
190
293
  const mods = loadAllModules(MODULES);
191
294
  const { order, missing } = resolveInstallOrder(names, mods);
@@ -222,9 +325,74 @@ function cmdAdd(names: string[], root: string, dryRun: boolean, harnesses: Harne
222
325
  console.log(c.bold(`\nrungs add ${names.join(' ')} → ${root}${dryRun ? c.yellow(' (dry run)') : ''}\n`));
223
326
  if (pulled.length) console.log(c.dim(` pulled in by dependency: ${pulled.map((m) => m.name).join(', ')}\n`));
224
327
 
328
+ // ADR-0004 state 5: a repo that solves this module's problem a different way
329
+ // gets the comparison and a stop, not an install beside what it already runs.
330
+ //
331
+ // The state existed in the ADR and in `doctor` and nowhere else, so `add`
332
+ // wrote straight over it — for every paradigm, since the CLI shipped
333
+ // (WI-043, from F-014). Measured 2026-08-16: a repo with `.github/ISSUE_TEMPLATE/`
334
+ // reported `backlog paradigm · external-tracker`, and `add backlog` then wrote
335
+ // `docs/`, `AGENTS.md`, `.ai/` and 12 gates without mentioning it once.
336
+ //
337
+ // Unlike `--confirm-threshold` above, this refusal **also applies under
338
+ // `--dry-run`**. A preview that installs what the real run refuses is a
339
+ // preview of a different command.
340
+ const scanned = scanRepo(root);
341
+ const paradigms = new Set(
342
+ order.map((m) => detect(m, root, scanned)).filter((r) => r.state === 'paradigm').map((r) => r.module),
343
+ );
344
+ const overridden = flags.has('--confirm-paradigm');
345
+ const blocked = overridden ? new Map<string, string>() : blockedByParadigm(order, paradigms);
346
+
347
+ // An override that prints nothing is indistinguishable from a detection that
348
+ // found nothing, and the two want opposite follow-ups.
349
+ if (overridden && paradigms.size) {
350
+ for (const name of paradigms) {
351
+ const p = detect(order.find((m) => m.name === name)!, root, scanned).paradigm!;
352
+ console.log(
353
+ c.yellow(` ${name}: installing over an existing ${p.id}`) +
354
+ c.dim(` (${p.matched[0]}) — --confirm-paradigm`),
355
+ );
356
+ }
357
+ console.log(c.dim(' You will have two systems for one job. That is a choice, not a merge.\n'));
358
+ }
359
+
360
+ // Re-resolve from what survives rather than filtering `order` in place. A
361
+ // dependency is only ever pulled in *for* something; `add backlog` on an
362
+ // issue-tracker repo was still writing `instructions` and `gates`, which
363
+ // nobody asked for and which were pulled in solely for the module being
364
+ // refused. Recomputing the closure drops them, and keeps anything a *surviving*
365
+ // request still needs.
366
+ let toInstall = order;
367
+ if (blocked.size) {
368
+ for (const mod of order) {
369
+ const cause = blocked.get(mod.name);
370
+ if (!cause) continue;
371
+ if (cause === mod.name) {
372
+ const p = detect(mod, root, scanned).paradigm!;
373
+ console.log(c.yellow(` ${mod.name}: this repo already does this another way — ${p.id}`));
374
+ console.log(c.dim(` matched ${p.matched[0]}`));
375
+ for (const line of (p.note ?? '').trim().split('\n')) console.log(c.dim(` ${line || ''}`));
376
+ if (p.compare) console.log(c.dim(` compare: ${p.compare}`));
377
+ } else {
378
+ console.log(c.yellow(` ${mod.name}: not installed — it requires ${cause}.`));
379
+ }
380
+ }
381
+ toInstall = resolveInstallOrder(names.filter((n) => !blocked.has(n)), mods).order;
382
+ const dropped = order.filter((m) => !toInstall.includes(m) && !blocked.has(m.name));
383
+ if (dropped.length) {
384
+ console.log(c.dim(` ${dropped.map((m) => m.name).join(', ')} not written — pulled in only for the above`));
385
+ }
386
+ console.log(
387
+ c.dim(`\n Pass --confirm-paradigm to install anyway.`) +
388
+ (toInstall.length ? c.dim(' Continuing with the rest.\n') : c.dim(' Nothing was written.\n')),
389
+ );
390
+ if (!toInstall.length) return 1;
391
+ }
392
+
225
393
  const installed: Manifest[] = [];
226
394
  const wrote = new Map<string, Set<string>>();
227
- for (const mod of order) {
395
+ for (const mod of toInstall) {
228
396
  if (mod.threshold?.confirm && !dryRun && !flags.has('--confirm-threshold')) {
229
397
  console.log(
230
398
  c.yellow(` ${mod.name}: requires ${mod.threshold.minimum}+ ${mod.threshold.metric}.`) +
@@ -300,9 +468,35 @@ function cmdRender(root: string, harnesses: Harness[], stamp: string) {
300
468
  }
301
469
 
302
470
  function cmdCheck(root: string, tier: string | undefined, stamp: string) {
303
- const runs = runGates(root, tier);
471
+ let runs: GateRun[];
472
+ try {
473
+ runs = runGates(root, tier);
474
+ } catch (e) {
475
+ // ADR-0008. A tier nobody declared used to select nothing and exit as though
476
+ // the gates had passed — the one failure mode a release step cannot have.
477
+ if (!(e instanceof UnknownTierError)) throw e;
478
+ console.log(c.yellow(`\n unknown tier "${e.requested}"`) + c.dim(` — this repo declares ${e.declared.join(', ')}.`));
479
+ console.log(c.dim(' Nothing ran. Use `rungs check` to run every registered gate.\n'));
480
+ return 1;
481
+ }
304
482
  if (!runs.length) {
305
- console.log(c.yellow('\n no gates registered is this a rungs repo?\n'));
483
+ // Two situations printed the same sentence, and it was the wrong one for the case that
484
+ // actually happens: a registry full of `fast` gates filtered by `--full` asked "is this a
485
+ // rungs repo?" about a repo holding 25 of them, and `cut-release` told every consumer to
486
+ // gate a release on exactly that command (F-020). Blame the filter when there is one.
487
+ //
488
+ // Hooks are excluded because a hook fires on a tool call rather than in the runner: it is
489
+ // registered, and no tier value could ever have selected it. Counting it here would offer
490
+ // the reader a gate that changing the tier cannot reach.
491
+ const runnable = loadRegistry(root).gates.filter((g) => !g.trigger);
492
+ if (runnable.length && tier) {
493
+ const tiers = [...new Set(runnable.map((g) => g.tier).filter(Boolean))];
494
+ console.log(c.yellow(`\n no gates in the ${tier} tier — ${runnable.length} are registered`) +
495
+ c.dim(` (${tiers.length ? tiers.join(', ') : 'none tiered'}).`));
496
+ console.log(c.dim(' Nothing ran. Use `rungs check` to run every registered gate.\n'));
497
+ } else {
498
+ console.log(c.yellow('\n no gates registered — is this a rungs repo?\n'));
499
+ }
306
500
  return 1;
307
501
  }
308
502
  appendLedger(root, runs, stamp);
@@ -334,25 +528,82 @@ function cmdCheck(root: string, tier: string | undefined, stamp: string) {
334
528
  );
335
529
  }
336
530
 
531
+ console.log();
532
+ return n('fail') + n('unimplemented') + n('error') > 0 ? 1 : 0;
533
+ }
534
+
535
+ /**
536
+ * ADR-0005 tier B: the two questions the ledger can ask without judgement.
537
+ *
538
+ * This printed from `check` and belonged in `doctor`, which is what both the
539
+ * ADR and the README say (F-012). The ADR does not merely name the command, it
540
+ * gives the reason: *"They must be pull (`doctor`), never push; no output
541
+ * during normal runs."* `check` is the normal run — it is what CI and every
542
+ * pre-merge habit invoke — so printing there was the push the tier was written
543
+ * to forbid, arriving inside the feature that forbade it.
544
+ */
545
+ function reportLedger(root: string) {
337
546
  const { gates } = loadRegistry(root);
338
547
  const q = ledgerQuestions(root, gates);
339
- if (q.neverFired.length || q.alwaysFires.length) {
340
- console.log(c.bold(`\n Ledger questions ${c.dim(`(${q.runs} recorded runs)`)}`));
341
- for (const g of q.neverFired.slice(0, 3)) {
342
- console.log(` ${c.cyan(g.id)} has never fired. ${c.dim(firstSentence(g.why ?? ''))}`);
343
- console.log(c.dim(' Is that still a risk here, or is the gate scoped too narrowly?'));
344
- }
345
- for (const g of q.alwaysFires.slice(0, 3)) {
346
- console.log(` ${c.cyan(g.id)} fails ${g.rate}. ${c.dim('Red by default is a gate people learn to bypass.')}`);
347
- }
348
- console.log(
349
- c.dim('\n These are questions, not verdicts. The ledger records whether a gate ran'),
350
- );
351
- console.log(c.dim(' and whether it fired — never whether it is valuable. Gates invoked'));
352
- console.log(c.dim(' directly, and CI runs, are not counted.'));
548
+ if (!q.neverFired.length && !q.alwaysFires.length) return;
549
+
550
+ console.log(c.bold(` Ledger questions ${c.dim(`(${q.runs} recorded runs)`)}`));
551
+ for (const g of q.neverFired.slice(0, 3)) {
552
+ console.log(` ${c.cyan(g.id)} has never fired. ${c.dim(firstSentence(g.why ?? ''))}`);
553
+ console.log(c.dim(' Is that still a risk here, or is the gate scoped too narrowly?'));
353
554
  }
354
- console.log();
355
- return n('fail') + n('unimplemented') + n('error') > 0 ? 1 : 0;
555
+ for (const g of q.alwaysFires.slice(0, 3)) {
556
+ console.log(` ${c.cyan(g.id)} fails ${g.rate}. ${c.dim('Red by default is a gate people learn to bypass.')}`);
557
+ }
558
+ console.log(c.dim('\n These are questions, not verdicts. The ledger records whether a gate ran'));
559
+ console.log(c.dim(' and whether it fired — never whether it is valuable. Gates invoked'));
560
+ console.log(c.dim(' directly, and CI runs, are not counted.\n'));
561
+ }
562
+
563
+ function cmdBacklogArchive(root: string, dryRun: boolean) {
564
+ const record = readRecord(root);
565
+ const configured = record?.modules['backlog']?.params?.root;
566
+ const backlogRoot = `docs/${configured ?? 'backlog'}`;
567
+
568
+ if (!existsSync(join(root, ...backlogRoot.split('/'), 'items'))) {
569
+ console.log(c.red(`\n no backlog at ${backlogRoot}/items\n`));
570
+ return 1;
571
+ }
572
+
573
+ const plan = planArchive(root, backlogRoot);
574
+ console.log(c.bold(`\nrungs backlog archive → ${root}${dryRun ? c.yellow(' (dry run)') : ''}\n`));
575
+
576
+ for (const h of plan.held) console.log(c.yellow(` held ${h.file}`) + c.dim(` — ${h.reason}`));
577
+ if (plan.held.length) console.log();
578
+
579
+ if (!plan.moves.length) {
580
+ console.log(c.dim(' nothing to archive — no item is done or rejected.\n'));
581
+ return 0;
582
+ }
583
+
584
+ const byStatus = new Map<string, number>();
585
+ for (const m of plan.moves) byStatus.set(m.status, (byStatus.get(m.status) ?? 0) + 1);
586
+ console.log(
587
+ ` ${c.bold(String(plan.moves.length))} item(s) — ${[...byStatus].map(([s, n]) => `${n} ${s}`).join(' · ')}`,
588
+ );
589
+ for (const m of plan.moves.slice(0, 5)) console.log(c.dim(` ${m.from} → ${m.to}`));
590
+ if (plan.moves.length > 5) console.log(c.dim(` …and ${plan.moves.length - 5} more`));
591
+
592
+ const touched = plan.rewrites.filter((r) => r.links);
593
+ const links = touched.reduce((n, r) => n + r.links, 0);
594
+ console.log(`\n ${c.bold(String(links))} link(s) repointed across ${touched.length} file(s)`);
595
+ for (const r of touched.slice(0, 5)) console.log(c.dim(` ${r.file} (${r.links})`));
596
+ if (touched.length > 5) console.log(c.dim(` …and ${touched.length - 5} more`));
597
+
598
+ if (dryRun) {
599
+ console.log(c.dim('\n Nothing written. Drop --dry-run to apply.\n'));
600
+ return 0;
601
+ }
602
+
603
+ applyArchive(root, plan);
604
+ console.log(c.green(`\n archived ${plan.moves.length} item(s)`) + c.dim(' — ids stay spent and every citation still resolves.'));
605
+ console.log(c.dim(' Run `rungs check` to confirm.\n'));
606
+ return 0;
356
607
  }
357
608
 
358
609
  function cmdInit(root: string, profile: string, dryRun: boolean, harnesses: Harness[], stamp: string) {
@@ -395,9 +646,18 @@ function cmdUpgrade(root: string, apply: boolean) {
395
646
  }
396
647
  }
397
648
 
398
- if (apply && stale) {
399
- const written = applyUpgrade(root, mods, record, plan);
400
- console.log(c.green(`\n updated ${written} file(s)`));
649
+ // Not `apply && stale`. A module version that only adds a gate has no stale
650
+ // file, so the whole apply step was skipped and the registry silently kept the
651
+ // old block — F-016, measured on a scratch consumer where `session` 1.1.0 →
652
+ // 1.2.0 added a gate and `rungs check` went on running the previous twenty.
653
+ if (apply) {
654
+ const { written, gates, recorded } = applyUpgrade(root, mods, record, plan);
655
+ const parts = [
656
+ written ? `${written} file(s)` : '',
657
+ gates ? `${gates} gate registration(s)` : '',
658
+ recorded ? `${recorded} record line(s)` : '',
659
+ ].filter(Boolean);
660
+ console.log(c.green(`\n updated ${parts.length ? parts.join(' · ') : 'nothing'}`));
401
661
  }
402
662
  console.log(
403
663
  `\n ${stale} to update · ${diverged} diverged\n` +
@@ -457,11 +717,14 @@ const COMMANDS: [usage: string, blurb: string][] = [
457
717
  ['eject [path]', 'materialise the engines; stop depending on rungs'],
458
718
  ['setup git [path]', 'install the merge drivers .gitattributes names'],
459
719
  ['modules', 'list the module set and audit the manifests'],
720
+ ['backlog archive [path]', 'move finished items to archive/, repointing every link'],
460
721
  ];
461
722
 
462
723
  /** Every flag the parser honours. A flag absent here is a flag nobody can find. */
463
724
  const FLAGS: [flag: string, blurb: string][] = [
464
725
  ['--dry-run', 'report what would happen, write nothing'],
726
+ ['--explain', "doctor: also run the detectors over what this repo already has"],
727
+ ['--confirm-paradigm', 'add: install a module this repo already solves another way'],
465
728
  ['--into <path>', 'add: install into this repo instead of the working directory'],
466
729
  ['--set m.param=value', 'add/init: override a module parameter. Repeatable'],
467
730
  ['--confirm-threshold', 'add: install a module whose rung is above this repo'],
@@ -547,7 +810,14 @@ switch (cmd) {
547
810
  case 'modules':
548
811
  process.exit(cmdModules(flags.has('--params')));
549
812
  case 'doctor':
550
- process.exit(cmdDoctor(args[0] ?? process.cwd()));
813
+ process.exit(cmdDoctor(args[0] ?? process.cwd(), flags.has('--explain')));
814
+ case 'backlog': {
815
+ if (args[0] !== 'archive') {
816
+ console.log(c.red(`\n unknown: rungs backlog ${args[0] ?? ''}`) + c.dim('\n The only subcommand is `archive`.\n'));
817
+ process.exit(1);
818
+ }
819
+ process.exit(cmdBacklogArchive(resolve(args[1] ?? process.cwd()), flags.has('--dry-run')));
820
+ }
551
821
  case 'check': {
552
822
  const tier = args[1] ?? (flags.has('--full') ? 'full' : flags.has('--fast') ? 'fast' : undefined);
553
823
  process.exit(cmdCheck(resolve(args[0] ?? process.cwd()), tier, STAMP));