@rungs/cli 0.3.0 → 0.4.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/README.md +6 -6
- package/dist/cli.js +2194 -488
- package/dist/cli.js.map +4 -4
- package/modules/README.md +25 -3
- package/modules/adr/files/{{path}}/README.md +1 -1
- package/modules/adr/gates/adr.toml +1 -1
- package/modules/adr/module.toml +1 -1
- package/modules/audit/fragments/AGENTS.md +2 -2
- package/modules/audit/module.toml +1 -1
- package/modules/audit/skills/assess/SKILL.md +1 -1
- package/modules/backlog/files/docs/{{root}}/BACKLOG.md +1 -1
- package/modules/backlog/files/docs/{{root}}/README.md +2 -2
- package/modules/backlog/files/docs/{{root}}/archive/README.md +1 -1
- package/modules/backlog/files/docs/{{root}}/items/README.md +1 -1
- package/modules/backlog/fragments/AGENTS.md +2 -2
- package/modules/backlog/module.toml +1 -1
- package/modules/backlog/skills/work-item/SKILL.md +1 -1
- package/modules/ci/files/{{workflow_path}} +3 -3
- package/modules/ci/module.toml +1 -1
- package/modules/concurrency/files/docs/concurrent-sessions.md +66 -18
- package/modules/concurrency/fragments/AGENTS.md +5 -4
- package/modules/concurrency/fragments/gitattributes +2 -2
- package/modules/concurrency/gates/concurrency.toml +3 -3
- package/modules/concurrency/module.toml +1 -1
- package/modules/doc-authority/files/{{registry_path}} +1 -1
- package/modules/doc-authority/module.toml +1 -1
- package/modules/findings/files/docs/{{backlog.root}}/FINDINGS.md +1 -1
- package/modules/findings/gates/findings.toml +5 -0
- package/modules/findings/module.toml +1 -1
- package/modules/findings/skills/record-finding/SKILL.md +1 -1
- package/modules/gates/files/.ai/gates.toml +1 -1
- package/modules/gates/fragments/AGENTS.md +6 -5
- package/modules/gates/module.toml +1 -1
- package/modules/instructions/files/.ai/rules/README.md +2 -2
- package/modules/instructions/files/.ai/rungs.mjs +52 -0
- package/modules/instructions/files/AGENTS.md +4 -2
- package/modules/instructions/files/CLAUDE.md +1 -1
- package/modules/instructions/fragments/AGENTS.md +2 -2
- package/modules/instructions/gates/core.toml +2 -2
- package/modules/instructions/module.toml +1 -1
- package/modules/release/files/{{changelog_dir}}/CONSUMED_THROUGH +1 -0
- package/modules/release/gates/release.toml +169 -17
- package/modules/release/module.toml +9 -5
- package/modules/release/skills/cut-release/SKILL.md +43 -15
- package/modules/session/files/{{archive}}/README.md +1 -1
- package/modules/session/files/{{path}} +2 -2
- package/modules/session/module.toml +1 -1
- package/modules/specs/files/{{path}}/README.md +2 -2
- package/modules/specs/module.toml +1 -1
- package/modules/workflows/module.toml +1 -1
- package/modules/workflows/rules/planning-tiers.md +1 -1
- package/package.json +3 -2
- package/src/add.ts +204 -48
- package/src/backlog.ts +354 -48
- package/src/check.ts +54 -33
- package/src/cli.ts +196 -69
- package/src/concurrency.ts +628 -42
- package/src/detect.ts +11 -3
- package/src/emitted-path.ts +274 -0
- package/src/engine-table.ts +66 -0
- package/src/engines.ts +40 -32
- package/src/engines2.ts +424 -29
- package/src/engines3.ts +115 -23
- package/src/explain.ts +3 -7
- package/src/help.ts +43 -0
- package/src/lifecycle.ts +95 -31
- package/src/manifest.ts +41 -5
- package/src/render.ts +106 -21
- package/src/selftest.ts +87 -10
- package/src/storage-key.ts +20 -0
- package/src/substitute.ts +47 -5
- package/src/text.ts +11 -0
- package/src/types.ts +16 -3
- package/src/version-source.ts +144 -0
package/src/cli.ts
CHANGED
|
@@ -3,16 +3,18 @@ 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, blockedByParadigm, registerGates, resolveInstallOrder, writeInstallRecord } from './add.ts';
|
|
7
|
-
import { render, writeReport, type Harness } from './render.ts';
|
|
6
|
+
import { addModule, adoptableGates, blockedByConflict, blockedByParadigm, moduleEmissionCandidates, preflightModuleEmissions, prospectiveRuleEmissions, type ConflictBlock, registerGates, resolveInstallOrder, writeInstallRecord } from './add.ts';
|
|
7
|
+
import { preflightRender, render, writeReport, type Harness } from './render.ts';
|
|
8
8
|
import { resolveParams } from './substitute.ts';
|
|
9
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
11
|
import { explain, IN_SCOPE as EXPLAINABLE } from './explain.ts';
|
|
12
|
-
import { applyArchive, planArchive } from './backlog.ts';
|
|
12
|
+
import { applyArchive, planArchive, resolveArchiveTree } from './backlog.ts';
|
|
13
13
|
import { land, preflight, sessionStart, worktrees } from './concurrency.ts';
|
|
14
14
|
import { existsSync } from 'node:fs';
|
|
15
15
|
import type { DetectResult, Manifest } from './types.ts';
|
|
16
|
+
import { UnsafeEmittedPathError } from './emitted-path.ts';
|
|
17
|
+
import { COMMANDS, FLAGS } from './help.ts';
|
|
16
18
|
|
|
17
19
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
18
20
|
const MODULES = join(HERE, '..', 'modules');
|
|
@@ -26,6 +28,12 @@ const c = {
|
|
|
26
28
|
cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
|
|
27
29
|
};
|
|
28
30
|
|
|
31
|
+
function pathRefusal(error: unknown): number {
|
|
32
|
+
if (!(error instanceof UnsafeEmittedPathError)) throw error;
|
|
33
|
+
console.log(c.red(`\n refused: ${error.message}\n`) + c.dim(' Nothing was written. Fix the named path or repository alias and retry.\n'));
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
const STATE_LABEL: Record<DetectResult['state'], string> = {
|
|
30
38
|
absent: c.dim('absent'),
|
|
31
39
|
'ours-current': c.green('ours'),
|
|
@@ -40,8 +48,17 @@ function cmdModules(showParams = false) {
|
|
|
40
48
|
console.log(c.bold(`\n${mods.length} modules\n`));
|
|
41
49
|
for (const m of mods) {
|
|
42
50
|
const deps = m.requires.length ? c.dim(` ← ${m.requires.join(', ')}`) : '';
|
|
43
|
-
|
|
51
|
+
// A `designed` module is marked wherever a module is named, because a
|
|
52
|
+
// distinction the manifest declares and no surface prints is the field
|
|
53
|
+
// being unread all over again (F-038, and F-037 nearly repeated it). The
|
|
54
|
+
// extracted case is unmarked: it is what every bundled module is, and a
|
|
55
|
+
// badge on all fifteen would carry no information.
|
|
56
|
+
const designed = m.provenance.kind === 'designed' ? c.yellow(' designed') : '';
|
|
57
|
+
console.log(` ${c.bold(m.name.padEnd(14))} rung ${m.rung}${deps}${designed}`);
|
|
44
58
|
console.log(` ${' '.repeat(14)} ${c.dim(m.summary)}`);
|
|
59
|
+
if (designed) {
|
|
60
|
+
console.log(` ${' '.repeat(14)} ${c.dim(`not extracted — ${firstSentence(m.provenance.rationale ?? '')}`)}`);
|
|
61
|
+
}
|
|
45
62
|
// Rendered from the manifest at the moment it is asked for, never written down. A committed
|
|
46
63
|
// parameter table would be correct the day it was generated and silently wrong the day a
|
|
47
64
|
// default moved — which is the failure this flag exists to answer (WI-006).
|
|
@@ -84,7 +101,12 @@ function cmdDoctor(target: string, doExplain = false) {
|
|
|
84
101
|
console.log(c.bold(`\nrungs doctor — ${root}\n`));
|
|
85
102
|
|
|
86
103
|
const files = scanRepo(root);
|
|
87
|
-
|
|
104
|
+
let record;
|
|
105
|
+
try {
|
|
106
|
+
record = readRecord(root);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return pathRefusal(error);
|
|
109
|
+
}
|
|
88
110
|
console.log(
|
|
89
111
|
c.dim(` scanned ${files.length} files`) +
|
|
90
112
|
(record ? c.dim(` · installed ${Object.keys(record.modules).length} module(s)`) : c.dim(' · not a rungs repo')) +
|
|
@@ -95,10 +117,15 @@ function cmdDoctor(target: string, doExplain = false) {
|
|
|
95
117
|
Object.entries(record?.modules ?? {}).flatMap(([n, e]) => (e.params ? [[n, e.params]] : [])),
|
|
96
118
|
), root);
|
|
97
119
|
const skillsDir = record?.harnesses.includes('claude') === false ? '.agents/skills' : '.claude/skills';
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
120
|
+
let results;
|
|
121
|
+
try {
|
|
122
|
+
results = mods.map((m) => {
|
|
123
|
+
const installed = record?.modules[m.name];
|
|
124
|
+
return detect(m, root, files, installed ? { ...installed, skillsDir, params_all: params } : undefined);
|
|
125
|
+
});
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return pathRefusal(error);
|
|
128
|
+
}
|
|
102
129
|
const byState = (s: DetectResult['state']) => results.filter((r) => r.state === s);
|
|
103
130
|
|
|
104
131
|
for (const r of results) {
|
|
@@ -386,6 +413,41 @@ function cmdAdd(names: string[], root: string, dryRun: boolean, harnesses: Harne
|
|
|
386
413
|
console.log(c.dim(' You will have two systems for one job. That is a choice, not a merge.\n'));
|
|
387
414
|
}
|
|
388
415
|
|
|
416
|
+
// F-038: a module's `[conflicts]` was parsed and read by nothing, so a
|
|
417
|
+
// declared incompatibility installed in silence. It refuses the same way a
|
|
418
|
+
// paradigm does — state it and stop — but it is computed separately, because
|
|
419
|
+
// a paradigm is *inferred from files* and a conflict is *declared by an
|
|
420
|
+
// author*, and a refusal should say which of the two it is.
|
|
421
|
+
//
|
|
422
|
+
// What counts as present: what the repo already has, plus everything else in
|
|
423
|
+
// this install set. Two modules that cannot coexist cannot arrive together
|
|
424
|
+
// either.
|
|
425
|
+
let record;
|
|
426
|
+
try {
|
|
427
|
+
record = readRecord(root);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
return pathRefusal(error);
|
|
430
|
+
}
|
|
431
|
+
const present = new Set([...Object.keys(record?.modules ?? {}), ...order.map((m) => m.name)]);
|
|
432
|
+
const declared = blockedByConflict(order, present, mods);
|
|
433
|
+
const conflictOverride = flags.has('--confirm-conflict');
|
|
434
|
+
|
|
435
|
+
// Computed even when overridden, and printed — for the reason the paradigm
|
|
436
|
+
// override just above says: an override that prints nothing is
|
|
437
|
+
// indistinguishable from finding nothing, and the two want opposite
|
|
438
|
+
// follow-ups.
|
|
439
|
+
if (conflictOverride && declared.size) {
|
|
440
|
+
for (const [name, clash] of declared) {
|
|
441
|
+
if (clash.cause !== name) continue;
|
|
442
|
+
console.log(
|
|
443
|
+
c.yellow(` ${name}: installing alongside ${clash.with}, which it declares a conflict with`) +
|
|
444
|
+
c.dim(' — --confirm-conflict'),
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
console.log(c.dim(' The author said these two do not coexist. You are overruling them, not merging them.\n'));
|
|
448
|
+
}
|
|
449
|
+
const conflicts = conflictOverride ? new Map<string, ConflictBlock>() : declared;
|
|
450
|
+
|
|
389
451
|
// Re-resolve from what survives rather than filtering `order` in place. A
|
|
390
452
|
// dependency is only ever pulled in *for* something; `add backlog` on an
|
|
391
453
|
// issue-tracker repo was still writing `instructions` and `gates`, which
|
|
@@ -393,7 +455,7 @@ function cmdAdd(names: string[], root: string, dryRun: boolean, harnesses: Harne
|
|
|
393
455
|
// refused. Recomputing the closure drops them, and keeps anything a *surviving*
|
|
394
456
|
// request still needs.
|
|
395
457
|
let toInstall = order;
|
|
396
|
-
if (blocked.size) {
|
|
458
|
+
if (blocked.size || conflicts.size) {
|
|
397
459
|
for (const mod of order) {
|
|
398
460
|
const cause = blocked.get(mod.name);
|
|
399
461
|
if (!cause) continue;
|
|
@@ -407,18 +469,68 @@ function cmdAdd(names: string[], root: string, dryRun: boolean, harnesses: Harne
|
|
|
407
469
|
console.log(c.yellow(` ${mod.name}: not installed — it requires ${cause}.`));
|
|
408
470
|
}
|
|
409
471
|
}
|
|
410
|
-
|
|
411
|
-
|
|
472
|
+
for (const mod of order) {
|
|
473
|
+
const clash = conflicts.get(mod.name);
|
|
474
|
+
// A module refused by both is reported once, under the paradigm, which
|
|
475
|
+
// carries the more specific evidence — a matched path.
|
|
476
|
+
if (!clash || blocked.has(mod.name)) continue;
|
|
477
|
+
if (clash.cause === mod.name) {
|
|
478
|
+
const declarer = mod.conflicts.includes(clash.with) ? mod.name : clash.with;
|
|
479
|
+
console.log(c.yellow(` ${mod.name}: conflicts with ${clash.with}`));
|
|
480
|
+
console.log(
|
|
481
|
+
c.dim(` declared by ${declarer}`) +
|
|
482
|
+
c.dim(present.has(clash.with) && !order.some((m) => m.name === clash.with) ? ', which this repo already has' : ', and both were requested'),
|
|
483
|
+
);
|
|
484
|
+
} else {
|
|
485
|
+
console.log(c.yellow(` ${mod.name}: not installed — it requires ${clash.cause}.`));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const refused = new Set([...blocked.keys(), ...conflicts.keys()]);
|
|
489
|
+
toInstall = resolveInstallOrder(names.filter((n) => !refused.has(n)), mods).order;
|
|
490
|
+
const dropped = order.filter((m) => !toInstall.includes(m) && !refused.has(m.name));
|
|
412
491
|
if (dropped.length) {
|
|
413
492
|
console.log(c.dim(` ${dropped.map((m) => m.name).join(', ')} not written — pulled in only for the above`));
|
|
414
493
|
}
|
|
494
|
+
const escapes = [blocked.size ? '--confirm-paradigm' : '', conflicts.size ? '--confirm-conflict' : ''].filter(Boolean);
|
|
415
495
|
console.log(
|
|
416
|
-
c.dim(`\n Pass
|
|
496
|
+
c.dim(`\n Pass ${escapes.join(' / ')} to install anyway.`) +
|
|
417
497
|
(toInstall.length ? c.dim(' Continuing with the rest.\n') : c.dim(' Nothing was written.\n')),
|
|
418
498
|
);
|
|
419
499
|
if (!toInstall.length) return 1;
|
|
420
500
|
}
|
|
421
501
|
|
|
502
|
+
// A module nobody paid for is a different thing to install than one extracted
|
|
503
|
+
// from a repo that did, and the person typing `add` is who should be told
|
|
504
|
+
// (F-037). One line, at the only moment it changes a decision.
|
|
505
|
+
for (const mod of toInstall.filter((m) => m.provenance.kind === 'designed')) {
|
|
506
|
+
console.log(
|
|
507
|
+
c.yellow(` ${mod.name}: designed, not extracted`) +
|
|
508
|
+
c.dim(` — ${firstSentence(mod.provenance.rationale ?? '')}`),
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// This is one operation, even though files, gates, the install record and
|
|
513
|
+
// harness renderings are applied in phases. Validate every selected module
|
|
514
|
+
// and every post-phase output before the first module can write.
|
|
515
|
+
const actualInstall = toInstall.filter(
|
|
516
|
+
(mod) => !(mod.threshold?.confirm && !dryRun && !flags.has('--confirm-threshold')),
|
|
517
|
+
);
|
|
518
|
+
try {
|
|
519
|
+
preflightModuleEmissions(actualInstall, root, params, skillsDir);
|
|
520
|
+
preflightRender(
|
|
521
|
+
root,
|
|
522
|
+
harnesses,
|
|
523
|
+
prospectiveRuleEmissions(actualInstall, params, skillsDir),
|
|
524
|
+
[
|
|
525
|
+
...moduleEmissionCandidates(actualInstall, params, skillsDir),
|
|
526
|
+
{ moduleName: 'rungs', target: '.ai/gates.toml', shared: true, writeExisting: true },
|
|
527
|
+
{ moduleName: 'rungs', target: '.ai/rungs.toml', writeExisting: true },
|
|
528
|
+
],
|
|
529
|
+
);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
return pathRefusal(error);
|
|
532
|
+
}
|
|
533
|
+
|
|
422
534
|
const installed: Manifest[] = [];
|
|
423
535
|
const wrote = new Map<string, Set<string>>();
|
|
424
536
|
for (const mod of toInstall) {
|
|
@@ -463,7 +575,7 @@ function cmdAdd(names: string[], root: string, dryRun: boolean, harnesses: Harne
|
|
|
463
575
|
}
|
|
464
576
|
|
|
465
577
|
if (!dryRun) {
|
|
466
|
-
writeInstallRecord(root,
|
|
578
|
+
writeInstallRecord(root, installed, params, harnesses, stamp, skillsDir, wrote);
|
|
467
579
|
const entries = render(root, harnesses);
|
|
468
580
|
writeReport(root, entries, harnesses, stamp);
|
|
469
581
|
console.log(
|
|
@@ -477,6 +589,11 @@ function cmdAdd(names: string[], root: string, dryRun: boolean, harnesses: Harne
|
|
|
477
589
|
}
|
|
478
590
|
|
|
479
591
|
function cmdRender(root: string, harnesses: Harness[], stamp: string) {
|
|
592
|
+
try {
|
|
593
|
+
preflightRender(root, harnesses);
|
|
594
|
+
} catch (error) {
|
|
595
|
+
return pathRefusal(error);
|
|
596
|
+
}
|
|
480
597
|
const entries = render(root, harnesses);
|
|
481
598
|
writeReport(root, entries, harnesses, stamp);
|
|
482
599
|
console.log(c.bold(`\nrungs render — ${root}\n`));
|
|
@@ -514,11 +631,18 @@ function landRunner(dir: string, only?: ReadonlySet<string>) {
|
|
|
514
631
|
const failing = runs.filter((r) => r.status === 'fail' || r.status === 'error');
|
|
515
632
|
return {
|
|
516
633
|
pass: runs.filter((r) => r.status === 'pass').length,
|
|
517
|
-
//
|
|
518
|
-
//
|
|
634
|
+
// Keep a stable comparison identity separate from the operator-facing
|
|
635
|
+
// diagnostic. `file: identity` makes the same broken link in the same file
|
|
636
|
+
// the same finding across runs, while command diagnostics can stay complete.
|
|
519
637
|
failing: failing.map((r) => ({
|
|
520
638
|
id: r.id,
|
|
521
|
-
findings: r.findings.map((f) =>
|
|
639
|
+
findings: r.findings.map((f) => {
|
|
640
|
+
const prefix = f.file ? `${f.file}: ` : '';
|
|
641
|
+
return {
|
|
642
|
+
identity: `${prefix}${f.identity ?? f.message}`,
|
|
643
|
+
diagnostic: `${prefix}${f.message}`,
|
|
644
|
+
};
|
|
645
|
+
}),
|
|
522
646
|
})),
|
|
523
647
|
};
|
|
524
648
|
}
|
|
@@ -588,7 +712,7 @@ function cmdCheck(root: string, tier: string | undefined, stamp: string) {
|
|
|
588
712
|
(r.examined ? c.dim(` ${r.examined} examined`) : ''),
|
|
589
713
|
);
|
|
590
714
|
for (const f of r.findings.slice(0, 4)) {
|
|
591
|
-
console.log(` ${c.dim(f.file ? `${f.file}: ` : '')}${f.message}`);
|
|
715
|
+
console.log(` ${c.dim(f.file ? `${f.file}: ` : '')}${f.message.replace(/\n/g, '\n ')}`);
|
|
592
716
|
}
|
|
593
717
|
if (r.findings.length > 4) console.log(c.dim(` …and ${r.findings.length - 4} more`));
|
|
594
718
|
}
|
|
@@ -640,16 +764,29 @@ function reportLedger(root: string) {
|
|
|
640
764
|
}
|
|
641
765
|
|
|
642
766
|
function cmdBacklogArchive(root: string, dryRun: boolean) {
|
|
643
|
-
|
|
767
|
+
let record;
|
|
768
|
+
try {
|
|
769
|
+
record = readRecord(root);
|
|
770
|
+
} catch (error) {
|
|
771
|
+
return pathRefusal(error);
|
|
772
|
+
}
|
|
644
773
|
const configured = record?.modules['backlog']?.params?.root;
|
|
645
774
|
const backlogRoot = `docs/${configured ?? 'backlog'}`;
|
|
646
775
|
|
|
647
|
-
|
|
776
|
+
let tree;
|
|
777
|
+
let plan;
|
|
778
|
+
try {
|
|
779
|
+
tree = resolveArchiveTree(root, backlogRoot);
|
|
780
|
+
plan = planArchive(root, backlogRoot);
|
|
781
|
+
} catch (error) {
|
|
782
|
+
return pathRefusal(error);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (!tree.itemsExists) {
|
|
648
786
|
console.log(c.red(`\n no backlog at ${backlogRoot}/items\n`));
|
|
649
787
|
return 1;
|
|
650
788
|
}
|
|
651
789
|
|
|
652
|
-
const plan = planArchive(root, backlogRoot);
|
|
653
790
|
console.log(c.bold(`\nrungs backlog archive → ${root}${dryRun ? c.yellow(' (dry run)') : ''}\n`));
|
|
654
791
|
|
|
655
792
|
for (const h of plan.held) console.log(c.yellow(` held ${h.file}`) + c.dim(` — ${h.reason}`));
|
|
@@ -679,14 +816,24 @@ function cmdBacklogArchive(root: string, dryRun: boolean) {
|
|
|
679
816
|
return 0;
|
|
680
817
|
}
|
|
681
818
|
|
|
682
|
-
|
|
819
|
+
try {
|
|
820
|
+
applyArchive(root, plan);
|
|
821
|
+
} catch (error) {
|
|
822
|
+
return pathRefusal(error);
|
|
823
|
+
}
|
|
683
824
|
console.log(c.green(`\n archived ${plan.moves.length} item(s)`) + c.dim(' — ids stay spent and every citation still resolves.'));
|
|
684
825
|
console.log(c.dim(' Run `rungs check` to confirm.\n'));
|
|
685
826
|
return 0;
|
|
686
827
|
}
|
|
687
828
|
|
|
688
829
|
function cmdInit(root: string, profile: string, dryRun: boolean, harnesses: Harness[], stamp: string) {
|
|
689
|
-
|
|
830
|
+
let existing;
|
|
831
|
+
try {
|
|
832
|
+
existing = readRecord(root);
|
|
833
|
+
} catch (error) {
|
|
834
|
+
return pathRefusal(error);
|
|
835
|
+
}
|
|
836
|
+
if (existing) {
|
|
690
837
|
console.log(
|
|
691
838
|
c.yellow('\n this repo is already initialised.') +
|
|
692
839
|
c.dim(' Use `rungs add <module>` to install more, or `rungs upgrade`.\n'),
|
|
@@ -703,13 +850,23 @@ function cmdInit(root: string, profile: string, dryRun: boolean, harnesses: Harn
|
|
|
703
850
|
}
|
|
704
851
|
|
|
705
852
|
function cmdUpgrade(root: string, apply: boolean) {
|
|
706
|
-
|
|
853
|
+
let record;
|
|
854
|
+
try {
|
|
855
|
+
record = readRecord(root);
|
|
856
|
+
} catch (error) {
|
|
857
|
+
return pathRefusal(error);
|
|
858
|
+
}
|
|
707
859
|
if (!record) {
|
|
708
860
|
console.log(c.yellow('\n not a rungs repo — nothing to upgrade.\n'));
|
|
709
861
|
return 1;
|
|
710
862
|
}
|
|
711
863
|
const mods = loadAllModules(MODULES);
|
|
712
|
-
|
|
864
|
+
let plan;
|
|
865
|
+
try {
|
|
866
|
+
plan = planUpgrade(root, mods, record);
|
|
867
|
+
} catch (error) {
|
|
868
|
+
return pathRefusal(error);
|
|
869
|
+
}
|
|
713
870
|
console.log(c.bold(`\nrungs upgrade — ${root}${apply ? '' : c.yellow(' (preview)')}\n`));
|
|
714
871
|
|
|
715
872
|
let stale = 0;
|
|
@@ -730,7 +887,13 @@ function cmdUpgrade(root: string, apply: boolean) {
|
|
|
730
887
|
// old block — F-016, measured on a scratch consumer where `session` 1.1.0 →
|
|
731
888
|
// 1.2.0 added a gate and `rungs check` went on running the previous twenty.
|
|
732
889
|
if (apply) {
|
|
733
|
-
|
|
890
|
+
let result;
|
|
891
|
+
try {
|
|
892
|
+
result = applyUpgrade(root, mods, record, plan);
|
|
893
|
+
} catch (error) {
|
|
894
|
+
return pathRefusal(error);
|
|
895
|
+
}
|
|
896
|
+
const { written, gates, recorded } = result;
|
|
734
897
|
const parts = [
|
|
735
898
|
written ? `${written} file(s)` : '',
|
|
736
899
|
gates ? `${gates} gate registration(s)` : '',
|
|
@@ -747,7 +910,13 @@ function cmdUpgrade(root: string, apply: boolean) {
|
|
|
747
910
|
}
|
|
748
911
|
|
|
749
912
|
function cmdEject(root: string, dryRun: boolean) {
|
|
750
|
-
|
|
913
|
+
let record;
|
|
914
|
+
try {
|
|
915
|
+
record = readRecord(root);
|
|
916
|
+
} catch (error) {
|
|
917
|
+
return pathRefusal(error);
|
|
918
|
+
}
|
|
919
|
+
if (!record) {
|
|
751
920
|
console.log(c.yellow('\n not a rungs repo — nothing to eject.\n'));
|
|
752
921
|
return 1;
|
|
753
922
|
}
|
|
@@ -775,48 +944,6 @@ function cmdEject(root: string, dryRun: boolean) {
|
|
|
775
944
|
*/
|
|
776
945
|
const VALUE_FLAGS = new Set(['--set']);
|
|
777
946
|
|
|
778
|
-
/**
|
|
779
|
-
* The command surface, defined once and rendered into `--help`.
|
|
780
|
-
*
|
|
781
|
-
* It was a template literal listing eight of the nine commands — `setup git` was missing entirely —
|
|
782
|
-
* beside a README table listing all nine, which is two hand-kept inventories of one fact. They had
|
|
783
|
-
* already drifted, in both directions: help omitted a real command, and three real flags appeared
|
|
784
|
-
* in neither. Keep this table beside the switch it describes, and add a row when you add a `case`.
|
|
785
|
-
*
|
|
786
|
-
* The README's table is still hand-kept and still a second inventory. That is a known cost, not an
|
|
787
|
-
* oversight — see WI-004.
|
|
788
|
-
*/
|
|
789
|
-
const COMMANDS: [usage: string, blurb: string][] = [
|
|
790
|
-
['init [path] [profile]', 'scaffold a repo — minimal · tracked · disciplined · hardened · fleet'],
|
|
791
|
-
['doctor [path]', 'detect what a repo already has, installed or not'],
|
|
792
|
-
['add <module…> [--into p]', 'install modules, resolving dependencies and adopting what exists'],
|
|
793
|
-
['check [path] [tier]', 'run the registered gates and record the ledger'],
|
|
794
|
-
['render [path]', 're-emit path-scoped rules per harness'],
|
|
795
|
-
['upgrade [path]', 'move to newer module versions, never touching what you edited'],
|
|
796
|
-
['eject [path]', 'materialise the engines; stop depending on rungs'],
|
|
797
|
-
['setup git [path]', 'install the merge drivers .gitattributes names'],
|
|
798
|
-
['modules', 'list the module set and audit the manifests'],
|
|
799
|
-
['backlog archive [path]', 'move finished items to archive/, repointing every link'],
|
|
800
|
-
['session start <branch>', 'cut a branch and worktree from the last verified merge'],
|
|
801
|
-
['preflight [path]', 'did the integration branch change files you changed?'],
|
|
802
|
-
['land <branch>', 'merge → verify the merged tree → advance, or refuse and park it'],
|
|
803
|
-
['worktrees [path]', 'which worktrees are merged, prunable, or merged and still dirty'],
|
|
804
|
-
];
|
|
805
|
-
|
|
806
|
-
/** Every flag the parser honours. A flag absent here is a flag nobody can find. */
|
|
807
|
-
const FLAGS: [flag: string, blurb: string][] = [
|
|
808
|
-
['--dry-run', 'report what would happen, write nothing'],
|
|
809
|
-
['--explain', "doctor: also run the detectors over what this repo already has"],
|
|
810
|
-
['--confirm-paradigm', 'add: install a module this repo already solves another way'],
|
|
811
|
-
['--into <path>', 'add: install into this repo instead of the working directory'],
|
|
812
|
-
['--set m.param=value', 'add/init: override a module parameter. Repeatable'],
|
|
813
|
-
['--confirm-threshold', 'add: install a module whose rung is above this repo'],
|
|
814
|
-
['--apply', 'upgrade: write the changes, rather than preview them'],
|
|
815
|
-
['--fast, --full', 'check: pick the gate tier, as the positional also does'],
|
|
816
|
-
['--params', 'modules: show every module parameter, its default and its allowed values'],
|
|
817
|
-
['--copilot', 'also emit Copilot instruction files'],
|
|
818
|
-
];
|
|
819
|
-
|
|
820
947
|
function renderHelp(): string {
|
|
821
948
|
const pad = Math.max(...COMMANDS.map(([u]) => u.length)) + 2;
|
|
822
949
|
const fpad = Math.max(...FLAGS.map(([f]) => f.length)) + 2;
|