@rungs/cli 0.2.0 → 0.3.1
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 +28 -15
- package/dist/cli.js +411 -46
- package/dist/cli.js.map +4 -4
- package/modules/ci/files/{{workflow_path}} +9 -1
- package/modules/ci/module.toml +1 -1
- package/modules/concurrency/files/docs/concurrent-sessions.md +11 -5
- package/modules/concurrency/module.toml +1 -1
- package/modules/release/gates/release.toml +29 -4
- package/modules/release/module.toml +1 -1
- package/modules/release/skills/cut-release/SKILL.md +4 -4
- package/package.json +1 -1
- package/src/backlog.ts +17 -2
- package/src/check.ts +9 -2
- package/src/cli.ts +109 -0
- package/src/concurrency.ts +412 -0
- package/src/engines.ts +22 -3
- package/src/engines2.ts +42 -11
- package/src/engines3.ts +4 -3
- package/src/lifecycle.ts +9 -4
- package/src/selftest.ts +16 -0
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { fileURLToPath as
|
|
5
|
-
import { dirname as
|
|
4
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
5
|
+
import { dirname as dirname9, join as join14, resolve as resolve5 } from "node:path";
|
|
6
6
|
|
|
7
7
|
// src/manifest.ts
|
|
8
8
|
import { readdirSync as readdirSync2, readFileSync, statSync as statSync2 } from "node:fs";
|
|
@@ -850,14 +850,15 @@ function writeReport(repoRoot, entries, harnesses, stamp) {
|
|
|
850
850
|
|
|
851
851
|
// src/check.ts
|
|
852
852
|
import { appendFileSync, existsSync as existsSync7, readFileSync as readFileSync8 } from "node:fs";
|
|
853
|
-
import { execSync
|
|
853
|
+
import { execSync } from "node:child_process";
|
|
854
854
|
import { dirname as dirname5, join as join10 } from "node:path";
|
|
855
|
-
import { fileURLToPath } from "node:url";
|
|
855
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
856
856
|
import { parse as parse2 } from "smol-toml";
|
|
857
857
|
|
|
858
858
|
// src/engines.ts
|
|
859
859
|
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
|
|
860
860
|
import { join as join9, dirname as dirname4, resolve as resolve2 } from "node:path";
|
|
861
|
+
import { fileURLToPath } from "node:url";
|
|
861
862
|
import { parse as parseToml } from "smol-toml";
|
|
862
863
|
|
|
863
864
|
// src/selftest.ts
|
|
@@ -880,6 +881,11 @@ function build(root, table, fx, input) {
|
|
|
880
881
|
if (typeof input === "string") return [write(targetPath(table), `${input}
|
|
881
882
|
`)];
|
|
882
883
|
if (!fx || typeof fx !== "object") return null;
|
|
884
|
+
if (fx.packages && typeof fx.packages === "object") {
|
|
885
|
+
return Object.entries(fx.packages).map(
|
|
886
|
+
([rel, version]) => write(rel, JSON.stringify({ name: rel.replace(/\W/g, "-"), version }))
|
|
887
|
+
);
|
|
888
|
+
}
|
|
883
889
|
if (Array.isArray(fx.fragments) && typeof fx.version === "string") {
|
|
884
890
|
const dir = fx.dir ?? "changelog.d";
|
|
885
891
|
const written = fx.fragments.map((n) => write(`${dir}/${n}`, `# ${n}
|
|
@@ -929,7 +935,8 @@ var CONTEXT_FREE = /* @__PURE__ */ new Set([
|
|
|
929
935
|
"file-budget",
|
|
930
936
|
"register-schema",
|
|
931
937
|
"file-population",
|
|
932
|
-
"changelog-freshness"
|
|
938
|
+
"changelog-freshness",
|
|
939
|
+
"computed-claim"
|
|
933
940
|
]);
|
|
934
941
|
function deparam(spec, dir) {
|
|
935
942
|
const walk3 = (v) => typeof v === "string" ? v.replace(/\{\{[^}]+\}\}/g, dir) : Array.isArray(v) ? v.map(walk3) : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk3(x)])) : v;
|
|
@@ -952,6 +959,10 @@ function runSelfTests(gateId, engine, table, blocks) {
|
|
|
952
959
|
const files = build(root, table, b.fixture, b.input);
|
|
953
960
|
let spec = b.fixture?.opted_in ? Array.isArray(table) ? table.map((s) => ({ ...s, extensions_opted_in: b.fixture.opted_in })) : { ...table, extensions_opted_in: b.fixture.opted_in } : table;
|
|
954
961
|
if (Array.isArray(b.fixture?.fragments)) spec = deparam(spec, b.fixture.dir ?? "changelog.d");
|
|
962
|
+
if (Array.isArray(b.fixture?.exclude)) {
|
|
963
|
+
const ex = b.fixture.exclude;
|
|
964
|
+
spec = Array.isArray(spec) ? spec.map((s) => ({ ...s, exclude: ex })) : { ...spec, exclude: ex };
|
|
965
|
+
}
|
|
955
966
|
if (!files) {
|
|
956
967
|
out.push({ gate: gateId, expect, outcome: "unrun", detail: `no builder for fixture ${JSON.stringify(b.fixture).slice(0, 60)}` });
|
|
957
968
|
continue;
|
|
@@ -976,7 +987,7 @@ function runSelfTests(gateId, engine, table, blocks) {
|
|
|
976
987
|
|
|
977
988
|
// src/engines2.ts
|
|
978
989
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
979
|
-
import {
|
|
990
|
+
import { execFileSync } from "node:child_process";
|
|
980
991
|
import { join as join7 } from "node:path";
|
|
981
992
|
var read = (root, rel) => {
|
|
982
993
|
try {
|
|
@@ -1227,12 +1238,13 @@ var crossReference = (t, root, files) => {
|
|
|
1227
1238
|
}
|
|
1228
1239
|
return { findings, examined: skills.length };
|
|
1229
1240
|
};
|
|
1241
|
+
var gitArgs = (root, args2) => execFileSync("git", args2, { cwd: root, stdio: "pipe" }).toString().trim();
|
|
1230
1242
|
function landedWork(root, branch, base) {
|
|
1231
|
-
const
|
|
1243
|
+
const git2 = (...args2) => gitArgs(root, args2);
|
|
1232
1244
|
try {
|
|
1233
|
-
const tip =
|
|
1234
|
-
if (tip ===
|
|
1235
|
-
return
|
|
1245
|
+
const tip = git2("rev-parse", branch);
|
|
1246
|
+
if (tip === git2("rev-parse", base)) return false;
|
|
1247
|
+
return git2("log", base, "--merges", "--format=%P").split("\n").some((line) => line.trim().split(/\s+/).slice(1).includes(tip));
|
|
1236
1248
|
} catch {
|
|
1237
1249
|
return true;
|
|
1238
1250
|
}
|
|
@@ -1242,10 +1254,7 @@ var gitStatusReconcile = (t, root, files) => {
|
|
|
1242
1254
|
let merged;
|
|
1243
1255
|
try {
|
|
1244
1256
|
merged = new Set(
|
|
1245
|
-
|
|
1246
|
-
cwd: root,
|
|
1247
|
-
stdio: "pipe"
|
|
1248
|
-
}).toString().split("\n").map((s) => s.trim()).filter(Boolean)
|
|
1257
|
+
gitArgs(root, ["branch", "--merged", t.integration_branch ?? "main", "--format=%(refname:short)"]).split("\n").map((s) => s.trim()).filter(Boolean)
|
|
1249
1258
|
);
|
|
1250
1259
|
} catch {
|
|
1251
1260
|
return { findings: [{ message: "cannot read git branches; status not reconciled" }], examined: 0 };
|
|
@@ -1270,8 +1279,10 @@ var computedClaim = (t, root, files) => {
|
|
|
1270
1279
|
let examined = 0;
|
|
1271
1280
|
for (const spec of specs) {
|
|
1272
1281
|
const values = /* @__PURE__ */ new Map();
|
|
1282
|
+
const excluded = (rel) => (spec.exclude ?? []).some((p) => matchAny([rel], p).length > 0);
|
|
1273
1283
|
for (const src of spec.sources ?? []) {
|
|
1274
1284
|
for (const rel of matchAny(files, src.file)) {
|
|
1285
|
+
if (excluded(rel)) continue;
|
|
1275
1286
|
const text = read(root, rel);
|
|
1276
1287
|
let v;
|
|
1277
1288
|
if (src.path && rel.endsWith(".json")) {
|
|
@@ -1290,8 +1301,9 @@ var computedClaim = (t, root, files) => {
|
|
|
1290
1301
|
}
|
|
1291
1302
|
const distinct = new Set(values.values());
|
|
1292
1303
|
if (spec.rule === "all-agree" && distinct.size > 1) {
|
|
1304
|
+
const where = [...values.entries()].map(([rel, v]) => `${rel}=${v}`).join(", ");
|
|
1293
1305
|
findings.push({
|
|
1294
|
-
message: `${spec.id} disagrees across ${values.size} locations: ${
|
|
1306
|
+
message: `${spec.id} disagrees across ${values.size} locations: ${where}` + (spec.autofix ? ` \u2014 run \`${spec.autofix}\`` : "") + (spec.exclude?.length ? "" : ". If one of these is versioned independently, list it in `exclude`.")
|
|
1295
1307
|
});
|
|
1296
1308
|
}
|
|
1297
1309
|
}
|
|
@@ -1300,7 +1312,7 @@ var computedClaim = (t, root, files) => {
|
|
|
1300
1312
|
|
|
1301
1313
|
// src/engines3.ts
|
|
1302
1314
|
import { readFileSync as readFileSync6 } from "node:fs";
|
|
1303
|
-
import {
|
|
1315
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1304
1316
|
import { join as join8 } from "node:path";
|
|
1305
1317
|
var read2 = (root, rel) => {
|
|
1306
1318
|
try {
|
|
@@ -1434,7 +1446,7 @@ var rulePropagation = (t, root, files) => {
|
|
|
1434
1446
|
var gitState = (t, root) => {
|
|
1435
1447
|
let out;
|
|
1436
1448
|
try {
|
|
1437
|
-
out =
|
|
1449
|
+
out = execFileSync2("git", ["worktree", "list", "--porcelain"], { cwd: root, stdio: "pipe" }).toString();
|
|
1438
1450
|
} catch {
|
|
1439
1451
|
return { findings: [{ message: "cannot read git worktrees; checkout state unknown" }], examined: 0 };
|
|
1440
1452
|
}
|
|
@@ -1459,7 +1471,7 @@ var mergeDriverCheck = (t, root) => {
|
|
|
1459
1471
|
for (const driver of required) {
|
|
1460
1472
|
let configured = "";
|
|
1461
1473
|
try {
|
|
1462
|
-
configured =
|
|
1474
|
+
configured = execFileSync2("git", ["config", "--get", `merge.${driver}.driver`], { cwd: root, stdio: "pipe" }).toString().trim();
|
|
1463
1475
|
} catch {
|
|
1464
1476
|
}
|
|
1465
1477
|
if (!configured) {
|
|
@@ -1511,6 +1523,7 @@ var boardReconcile = (t, root, _files) => {
|
|
|
1511
1523
|
};
|
|
1512
1524
|
|
|
1513
1525
|
// src/engines.ts
|
|
1526
|
+
var CLI_MODULES = join9(dirname4(fileURLToPath(import.meta.url)), "..", "modules");
|
|
1514
1527
|
var read3 = (root, rel) => {
|
|
1515
1528
|
try {
|
|
1516
1529
|
return readFileSync7(join9(root, rel), "utf8");
|
|
@@ -1712,7 +1725,7 @@ var gateMeta = (_t, root) => {
|
|
|
1712
1725
|
const table = entry.match(/^table\s*=\s*"(.+)"/m)?.[1];
|
|
1713
1726
|
if (!id || kind !== "declared" || !table) continue;
|
|
1714
1727
|
examined++;
|
|
1715
|
-
const tablePath = join9(
|
|
1728
|
+
const tablePath = join9(CLI_MODULES, dirname4(table), "gates", table.split("/").pop());
|
|
1716
1729
|
const src = existsSync6(tablePath) ? readFileSync7(tablePath, "utf8") : "";
|
|
1717
1730
|
const forGate = [...src.matchAll(/\[\[self_test\]\][\s\S]*?(?=\n\[\[|\n\[|$)/g)].map((m) => m[0]).filter((b) => b.includes(`gate = "${id}"`) || b.includes(`gate = "${id}"`) || b.includes(`gate = "${id}"`));
|
|
1718
1731
|
for (const direction of ["pass", "fail"]) {
|
|
@@ -1738,7 +1751,7 @@ function optedInExtensions(rel, spec) {
|
|
|
1738
1751
|
const name = rel.split("/").slice(-2)[0];
|
|
1739
1752
|
if (!name) return /* @__PURE__ */ new Set();
|
|
1740
1753
|
try {
|
|
1741
|
-
const mods = loadAllModules(
|
|
1754
|
+
const mods = loadAllModules(CLI_MODULES);
|
|
1742
1755
|
const owner = mods.find((m) => m.skills?.[name]?.extensions);
|
|
1743
1756
|
return new Set(Object.keys(owner?.skills?.[name]?.extensions ?? {}));
|
|
1744
1757
|
} catch {
|
|
@@ -1749,7 +1762,7 @@ var escapeRe3 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
|
1749
1762
|
function parseTable(path, module) {
|
|
1750
1763
|
if (!existsSync6(path)) return null;
|
|
1751
1764
|
try {
|
|
1752
|
-
const mods = loadAllModules(
|
|
1765
|
+
const mods = loadAllModules(CLI_MODULES);
|
|
1753
1766
|
const params = resolveParams(mods, {}, ".");
|
|
1754
1767
|
return parseToml(substitute(readFileSync7(path, "utf8"), module, params));
|
|
1755
1768
|
} catch {
|
|
@@ -1802,7 +1815,7 @@ function isImplemented(engine) {
|
|
|
1802
1815
|
}
|
|
1803
1816
|
|
|
1804
1817
|
// src/check.ts
|
|
1805
|
-
var MODULES = join10(dirname5(
|
|
1818
|
+
var MODULES = join10(dirname5(fileURLToPath2(import.meta.url)), "..", "modules");
|
|
1806
1819
|
function loadRegistry(repoRoot) {
|
|
1807
1820
|
const path = join10(repoRoot, ".ai", "gates.toml");
|
|
1808
1821
|
if (!existsSync7(path)) return { runner: {}, gates: [] };
|
|
@@ -1825,7 +1838,7 @@ var UnknownTierError = class extends Error {
|
|
|
1825
1838
|
this.declared = declared;
|
|
1826
1839
|
}
|
|
1827
1840
|
};
|
|
1828
|
-
function runGates(repoRoot, tier, now = () => Date.now()) {
|
|
1841
|
+
function runGates(repoRoot, tier, now = () => Date.now(), only) {
|
|
1829
1842
|
const { runner, gates } = loadRegistry(repoRoot);
|
|
1830
1843
|
const runnerTiers = Array.isArray(runner?.tiers) ? runner.tiers : [];
|
|
1831
1844
|
if (tier && runnerTiers.length && !runnerTiers.includes(tier)) {
|
|
@@ -1835,6 +1848,7 @@ function runGates(repoRoot, tier, now = () => Date.now()) {
|
|
|
1835
1848
|
const runs = [];
|
|
1836
1849
|
for (const g of gates) {
|
|
1837
1850
|
if (g.trigger) continue;
|
|
1851
|
+
if (only && !only.has(g.id)) continue;
|
|
1838
1852
|
if (tier && !tierSelects(runnerTiers, tier, g.tier)) continue;
|
|
1839
1853
|
const started = now();
|
|
1840
1854
|
let status = "pass";
|
|
@@ -1842,7 +1856,7 @@ function runGates(repoRoot, tier, now = () => Date.now()) {
|
|
|
1842
1856
|
let examined = 0;
|
|
1843
1857
|
if (g.kind === "command" && g.command) {
|
|
1844
1858
|
try {
|
|
1845
|
-
|
|
1859
|
+
execSync(g.command, { cwd: repoRoot, stdio: "pipe" });
|
|
1846
1860
|
} catch (e) {
|
|
1847
1861
|
status = "fail";
|
|
1848
1862
|
findings = [{ message: String(e.stderr ?? e.stdout ?? e.message).trim().split("\n").slice(-3).join(" ") }];
|
|
@@ -1964,10 +1978,10 @@ function ledgerQuestions(repoRoot, gates) {
|
|
|
1964
1978
|
// src/lifecycle.ts
|
|
1965
1979
|
import { copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1966
1980
|
import { dirname as dirname6, join as join11 } from "node:path";
|
|
1967
|
-
import { fileURLToPath as
|
|
1968
|
-
import {
|
|
1981
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1982
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1969
1983
|
import { parse as parse3 } from "smol-toml";
|
|
1970
|
-
var SRC = dirname6(
|
|
1984
|
+
var SRC = dirname6(fileURLToPath3(import.meta.url));
|
|
1971
1985
|
var PROFILES = {
|
|
1972
1986
|
minimal: ["instructions"],
|
|
1973
1987
|
tracked: ["instructions", "gates", "backlog", "findings", "adr", "session"],
|
|
@@ -2180,8 +2194,8 @@ function setupGit(repoRoot, dryRun = false) {
|
|
|
2180
2194
|
');process.exit(1)"` : "git merge-file -L ours -L base -L theirs %A %O %B";
|
|
2181
2195
|
if (!dryRun) {
|
|
2182
2196
|
try {
|
|
2183
|
-
|
|
2184
|
-
|
|
2197
|
+
execFileSync3("git", ["config", `merge.${d}.name`, `rungs ${d.replace("rungs-", "")} driver`], { cwd: repoRoot, stdio: "pipe" });
|
|
2198
|
+
execFileSync3("git", ["config", `merge.${d}.driver`, cmd2], { cwd: repoRoot, stdio: "pipe" });
|
|
2185
2199
|
} catch {
|
|
2186
2200
|
continue;
|
|
2187
2201
|
}
|
|
@@ -2191,7 +2205,7 @@ function setupGit(repoRoot, dryRun = false) {
|
|
|
2191
2205
|
let rerere = false;
|
|
2192
2206
|
if (!dryRun) {
|
|
2193
2207
|
try {
|
|
2194
|
-
|
|
2208
|
+
execFileSync3("git", ["config", "rerere.enabled", "true"], { cwd: repoRoot, stdio: "pipe" });
|
|
2195
2209
|
rerere = true;
|
|
2196
2210
|
} catch {
|
|
2197
2211
|
}
|
|
@@ -2286,15 +2300,17 @@ function planArchive(repoRoot, backlogRoot = "docs/backlog") {
|
|
|
2286
2300
|
const files = walk(repoRoot);
|
|
2287
2301
|
const items = files.filter((f) => posix(f).startsWith(posix(relative2(repoRoot, itemsDir)) + "/") && f.endsWith(".md"));
|
|
2288
2302
|
for (const rel of items) {
|
|
2289
|
-
|
|
2303
|
+
const base = posix(rel).split("/").pop();
|
|
2304
|
+
if (/^(README|TEMPLATE)\.md$/i.test(base)) continue;
|
|
2290
2305
|
const text = readFileSync10(join12(repoRoot, rel), "utf8");
|
|
2291
2306
|
const status = field(text, "status");
|
|
2292
2307
|
const id = field(text, "id");
|
|
2293
2308
|
if (!FINISHED.has(status)) continue;
|
|
2294
2309
|
if (field(text, "type") === "epic") {
|
|
2295
2310
|
const children = (text.match(/^children:\s*\[(.*)\]/m)?.[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
2311
|
+
const archived = files.filter((f) => posix(f).startsWith(posix(relative2(repoRoot, archiveDir)) + "/") && f.endsWith(".md"));
|
|
2296
2312
|
const unfinished = children.filter((c2) => {
|
|
2297
|
-
const f = items.find((i) => i.includes(`${c2}-`));
|
|
2313
|
+
const f = items.find((i) => i.includes(`${c2}-`)) ?? archived.find((i) => i.includes(`${c2}-`));
|
|
2298
2314
|
return !f || !FINISHED.has(field(readFileSync10(join12(repoRoot, f), "utf8"), "status"));
|
|
2299
2315
|
});
|
|
2300
2316
|
if (unfinished.length) {
|
|
@@ -2362,10 +2378,268 @@ function applyArchive(repoRoot, plan) {
|
|
|
2362
2378
|
}
|
|
2363
2379
|
}
|
|
2364
2380
|
|
|
2381
|
+
// src/concurrency.ts
|
|
2382
|
+
import { existsSync as existsSync10, mkdtempSync as mkdtempSync2, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync6, unlinkSync } from "node:fs";
|
|
2383
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
2384
|
+
import { hostname } from "node:os";
|
|
2385
|
+
import { join as join13, resolve as resolve4, dirname as dirname8, basename as basename2 } from "node:path";
|
|
2386
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
2387
|
+
function loopParams(root) {
|
|
2388
|
+
const p = installedParams(root).concurrency ?? {};
|
|
2389
|
+
const integration = String(p.integration_branch ?? "main");
|
|
2390
|
+
const greenPrefix = String(p.green_prefix ?? "green/");
|
|
2391
|
+
return {
|
|
2392
|
+
integration,
|
|
2393
|
+
// The green ref marks the last *verified* merge of the integration branch,
|
|
2394
|
+
// so it is prefix + that branch — not prefix + whatever you are cutting.
|
|
2395
|
+
greenRef: `${greenPrefix}${integration}`,
|
|
2396
|
+
integPrefix: String(p.integ_prefix ?? "integ/")
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
function git(root, args2) {
|
|
2400
|
+
return execFileSync4("git", args2, { cwd: root, stdio: "pipe", encoding: "utf8" }).trim();
|
|
2401
|
+
}
|
|
2402
|
+
function gitOk(root, args2) {
|
|
2403
|
+
try {
|
|
2404
|
+
git(root, args2);
|
|
2405
|
+
return true;
|
|
2406
|
+
} catch {
|
|
2407
|
+
return false;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
function revParse(root, ref) {
|
|
2411
|
+
try {
|
|
2412
|
+
return git(root, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
|
|
2413
|
+
} catch {
|
|
2414
|
+
return null;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
function sessionStart(root, branch, at, dryRun = false) {
|
|
2418
|
+
const { integration, greenRef } = loopParams(root);
|
|
2419
|
+
const lines = [];
|
|
2420
|
+
if (!branch) return { ok: false, lines: ["a branch name is required: `rungs session start <branch> [path]`"] };
|
|
2421
|
+
if (revParse(root, `refs/heads/${branch}`)) {
|
|
2422
|
+
return { ok: false, lines: [`branch '${branch}' already exists \u2014 pick another name, or check out the worktree that holds it`] };
|
|
2423
|
+
}
|
|
2424
|
+
const green = revParse(root, `refs/heads/${greenRef}`);
|
|
2425
|
+
const base = green ? greenRef : integration;
|
|
2426
|
+
const baseSha = green ?? revParse(root, integration);
|
|
2427
|
+
if (!baseSha) return { ok: false, lines: [`neither '${greenRef}' nor '${integration}' resolves \u2014 is this the right repo?`] };
|
|
2428
|
+
if (green) {
|
|
2429
|
+
lines.push(`base ${greenRef} (${baseSha.slice(0, 8)}) \u2014 the last verified merge`);
|
|
2430
|
+
} else {
|
|
2431
|
+
lines.push(`no ${greenRef} ref yet \u2014 cutting from the tip of ${integration} (${baseSha.slice(0, 8)}) instead.`);
|
|
2432
|
+
lines.push(`That tip has not been verified by a land. The first successful \`rungs land\` creates ${greenRef}.`);
|
|
2433
|
+
}
|
|
2434
|
+
const path = resolve4(at ?? join13(dirname8(root), `${basename2(root)}-${branch.replace(/[^\w.-]+/g, "-")}`));
|
|
2435
|
+
if (existsSync10(path)) return { ok: false, lines: [`${path} already exists \u2014 rungs never writes over a directory it did not create`] };
|
|
2436
|
+
lines.push(`worktree ${path}`);
|
|
2437
|
+
lines.push(`branch ${branch}`);
|
|
2438
|
+
if (dryRun) return { ok: true, lines };
|
|
2439
|
+
try {
|
|
2440
|
+
git(root, ["worktree", "add", "-b", branch, path, baseSha]);
|
|
2441
|
+
} catch (e) {
|
|
2442
|
+
return { ok: false, lines: [...lines, `git refused: ${String(e.stderr ?? e.message).trim().split("\n").slice(-2).join(" ")}`] };
|
|
2443
|
+
}
|
|
2444
|
+
return { ok: true, lines };
|
|
2445
|
+
}
|
|
2446
|
+
function preflight(root) {
|
|
2447
|
+
const { integration } = loopParams(root);
|
|
2448
|
+
if (!revParse(root, integration)) return { ok: false, lines: [`'${integration}' does not resolve \u2014 is this the right repo?`] };
|
|
2449
|
+
let base;
|
|
2450
|
+
try {
|
|
2451
|
+
base = git(root, ["merge-base", "HEAD", integration]);
|
|
2452
|
+
} catch {
|
|
2453
|
+
return { ok: false, lines: [`no merge base between HEAD and ${integration}; nothing to compare`] };
|
|
2454
|
+
}
|
|
2455
|
+
const names = (args2) => new Set(git(root, args2).split("\n").map((s) => s.trim()).filter(Boolean));
|
|
2456
|
+
const theirs = names(["diff", "--name-only", base, integration]);
|
|
2457
|
+
const mine = /* @__PURE__ */ new Set([
|
|
2458
|
+
...names(["diff", "--name-only", base, "HEAD"]),
|
|
2459
|
+
...names(["diff", "--name-only", "HEAD"]),
|
|
2460
|
+
...names(["diff", "--name-only", "--cached"])
|
|
2461
|
+
]);
|
|
2462
|
+
const ahead = Number(git(root, ["rev-list", "--count", `${base}..${integration}`]));
|
|
2463
|
+
const overlap = [...mine].filter((f) => theirs.has(f)).sort();
|
|
2464
|
+
const lines = [
|
|
2465
|
+
`${integration} is ${ahead} commit(s) ahead of your base, touching ${theirs.size} file(s).`,
|
|
2466
|
+
`You have touched ${mine.size} file(s).`
|
|
2467
|
+
];
|
|
2468
|
+
if (!overlap.length) {
|
|
2469
|
+
lines.push("No overlap. The commit count is not the signal \u2014 these two sets not intersecting is.");
|
|
2470
|
+
return { ok: true, lines };
|
|
2471
|
+
}
|
|
2472
|
+
lines.push(`${overlap.length} file(s) changed on both sides:`);
|
|
2473
|
+
for (const f of overlap.slice(0, 20)) lines.push(` ${f}`);
|
|
2474
|
+
if (overlap.length > 20) lines.push(` \u2026and ${overlap.length - 20} more`);
|
|
2475
|
+
lines.push("Merge sooner rather than later. Shared code is a scheduling problem, not a tooling one.");
|
|
2476
|
+
return { ok: true, lines };
|
|
2477
|
+
}
|
|
2478
|
+
function lockPath(root) {
|
|
2479
|
+
return join13(git(root, ["rev-parse", "--git-common-dir"]).replace(/^\.git$/, join13(root, ".git")), "rungs-land.lock");
|
|
2480
|
+
}
|
|
2481
|
+
function alive(pid) {
|
|
2482
|
+
try {
|
|
2483
|
+
process.kill(pid, 0);
|
|
2484
|
+
return true;
|
|
2485
|
+
} catch (e) {
|
|
2486
|
+
return e?.code === "EPERM";
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
function land(root, branch, runner, dryRun = false) {
|
|
2490
|
+
const { integration, greenRef, integPrefix } = loopParams(root);
|
|
2491
|
+
const lines = [];
|
|
2492
|
+
if (!branch) return { ok: false, lines: ["a branch name is required: `rungs land <branch>`"] };
|
|
2493
|
+
const head = revParse(root, `refs/heads/${branch}`);
|
|
2494
|
+
if (!head) return { ok: false, lines: [`branch '${branch}' does not exist`] };
|
|
2495
|
+
const before = revParse(root, `refs/heads/${integration}`);
|
|
2496
|
+
if (!before) return { ok: false, lines: [`'${integration}' does not resolve`] };
|
|
2497
|
+
const lp = lockPath(root);
|
|
2498
|
+
if (existsSync10(lp)) {
|
|
2499
|
+
try {
|
|
2500
|
+
const held = JSON.parse(readFileSync11(lp, "utf8"));
|
|
2501
|
+
if (held.host === hostname() && alive(held.pid)) {
|
|
2502
|
+
return {
|
|
2503
|
+
ok: false,
|
|
2504
|
+
lines: [
|
|
2505
|
+
`another land is in progress: pid ${held.pid} on ${held.host}, landing '${held.branch}' since ${held.started}.`,
|
|
2506
|
+
"Concurrent landing is refused, not silently merged."
|
|
2507
|
+
]
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
lines.push(`taking over a stale lock from pid ${held.pid} (${held.started}) \u2014 that process is gone.`);
|
|
2511
|
+
} catch {
|
|
2512
|
+
lines.push("an unreadable lock file was replaced.");
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
if (dryRun) {
|
|
2516
|
+
lines.push(`would merge ${branch} (${head.slice(0, 8)}) onto ${integration} (${before.slice(0, 8)}) via ${integPrefix}${branch}, verify, then advance.`);
|
|
2517
|
+
return { ok: true, lines };
|
|
2518
|
+
}
|
|
2519
|
+
const lock = { pid: process.pid, host: hostname(), started: (/* @__PURE__ */ new Date()).toISOString(), branch };
|
|
2520
|
+
writeFileSync6(lp, JSON.stringify(lock));
|
|
2521
|
+
const scratch = mkdtempSync2(join13(tmpdir2(), "rungs-land-"));
|
|
2522
|
+
const parked = `${integPrefix}${branch}`;
|
|
2523
|
+
try {
|
|
2524
|
+
git(root, ["worktree", "add", "--detach", scratch, before]);
|
|
2525
|
+
try {
|
|
2526
|
+
git(scratch, ["-c", "user.email=rungs@localhost", "-c", "user.name=rungs", "merge", "--no-ff", "-m", `land ${branch}`, head]);
|
|
2527
|
+
} catch (e) {
|
|
2528
|
+
const conflicts = (() => {
|
|
2529
|
+
try {
|
|
2530
|
+
return git(scratch, ["diff", "--name-only", "--diff-filter=U"]).split("\n").filter(Boolean);
|
|
2531
|
+
} catch {
|
|
2532
|
+
return [];
|
|
2533
|
+
}
|
|
2534
|
+
})();
|
|
2535
|
+
lines.push(`merge conflict \u2014 ${integration} is unchanged.`);
|
|
2536
|
+
for (const f of conflicts.slice(0, 15)) lines.push(` ${f}`);
|
|
2537
|
+
lines.push("Reconcile generated artifacts by regenerating, never by merging text.");
|
|
2538
|
+
return { ok: false, lines };
|
|
2539
|
+
}
|
|
2540
|
+
const merged = git(scratch, ["rev-parse", "HEAD"]);
|
|
2541
|
+
const res = runner(scratch);
|
|
2542
|
+
lines.push(`merged tree ${merged.slice(0, 8)} \u2014 ${res.pass} pass \xB7 ${res.failing.length} fail`);
|
|
2543
|
+
if (res.failing.length) {
|
|
2544
|
+
const ids = new Set(res.failing.map((f) => f.id));
|
|
2545
|
+
let base = null;
|
|
2546
|
+
try {
|
|
2547
|
+
git(scratch, ["reset", "--hard", before]);
|
|
2548
|
+
base = runner(scratch, ids);
|
|
2549
|
+
} catch {
|
|
2550
|
+
base = null;
|
|
2551
|
+
}
|
|
2552
|
+
const attributable = base !== null && base.failing.length + base.pass >= ids.size;
|
|
2553
|
+
const baseFindings = new Map((base?.failing ?? []).map((f) => [f.id, new Set(f.findings)]));
|
|
2554
|
+
const introduced = [];
|
|
2555
|
+
const inherited = [];
|
|
2556
|
+
for (const f of res.failing) {
|
|
2557
|
+
const seen = attributable ? baseFindings.get(f.id) ?? /* @__PURE__ */ new Set() : null;
|
|
2558
|
+
const fresh = seen ? f.findings.filter((x) => !seen.has(x)) : f.findings;
|
|
2559
|
+
if (fresh.length) introduced.push({ id: f.id, findings: fresh });
|
|
2560
|
+
else inherited.push(f);
|
|
2561
|
+
}
|
|
2562
|
+
for (const f of inherited) {
|
|
2563
|
+
lines.push(` inherited ${f.id}${f.findings[0] ? ` \u2014 ${f.findings[0]}` : ""}`);
|
|
2564
|
+
}
|
|
2565
|
+
for (const f of introduced) {
|
|
2566
|
+
lines.push(` INTRODUCED ${f.id}${f.findings[0] ? ` \u2014 ${f.findings[0]}` : ""}`);
|
|
2567
|
+
for (const extra of f.findings.slice(1, 4)) lines.push(` ${extra}`);
|
|
2568
|
+
}
|
|
2569
|
+
if (base === null) {
|
|
2570
|
+
lines.push(" The merge base could not be gated, so nothing here is attributable and all of it blocks.");
|
|
2571
|
+
} else if (!attributable) {
|
|
2572
|
+
lines.push(" Some gates could not be attributed against the merge base, so they block. We do not land on an unknown.");
|
|
2573
|
+
}
|
|
2574
|
+
if (introduced.length) {
|
|
2575
|
+
git(root, ["update-ref", `refs/heads/${parked}`, merged]);
|
|
2576
|
+
lines.push(
|
|
2577
|
+
`${introduced.length} introduced by this branch. ${integration} is unchanged, and the merged tree is parked on '${parked}' \u2014 fix it there and land again.`
|
|
2578
|
+
);
|
|
2579
|
+
return { ok: false, lines };
|
|
2580
|
+
}
|
|
2581
|
+
lines.push(
|
|
2582
|
+
`${inherited.length} failure(s), all already red on ${integration} before this branch. Landing anyway \u2014 they are not this branch's to fix, and blocking on them is how a gate gets bypassed.`
|
|
2583
|
+
);
|
|
2584
|
+
git(scratch, ["reset", "--hard", merged]);
|
|
2585
|
+
}
|
|
2586
|
+
try {
|
|
2587
|
+
git(root, ["update-ref", `refs/heads/${integration}`, merged, before]);
|
|
2588
|
+
} catch {
|
|
2589
|
+
git(root, ["update-ref", `refs/heads/${parked}`, merged]);
|
|
2590
|
+
return {
|
|
2591
|
+
ok: false,
|
|
2592
|
+
lines: [
|
|
2593
|
+
...lines,
|
|
2594
|
+
`${integration} moved while this land was verifying, so the advance was refused rather than overwriting it.`,
|
|
2595
|
+
`Your verified merge is parked on '${parked}'. Re-run \`rungs land ${branch}\` to rebuild it on the new tip.`
|
|
2596
|
+
]
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
git(root, ["update-ref", `refs/heads/${greenRef}`, merged]);
|
|
2600
|
+
lines.push(`${integration} \u2192 ${merged.slice(0, 8)}, and ${greenRef} now marks it verified.`);
|
|
2601
|
+
if (revParse(root, `refs/heads/${parked}`)) git(root, ["update-ref", "-d", `refs/heads/${parked}`]);
|
|
2602
|
+
return { ok: true, lines };
|
|
2603
|
+
} finally {
|
|
2604
|
+
try {
|
|
2605
|
+
git(root, ["worktree", "remove", "--force", scratch]);
|
|
2606
|
+
} catch {
|
|
2607
|
+
rmSync2(scratch, { recursive: true, force: true });
|
|
2608
|
+
try {
|
|
2609
|
+
git(root, ["worktree", "prune"]);
|
|
2610
|
+
} catch {
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
try {
|
|
2614
|
+
unlinkSync(lp);
|
|
2615
|
+
} catch {
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
function worktrees(root) {
|
|
2620
|
+
const { integration } = loopParams(root);
|
|
2621
|
+
const out = git(root, ["worktree", "list", "--porcelain"]);
|
|
2622
|
+
const rows = [];
|
|
2623
|
+
for (const block of out.split("\n\n").filter((b) => b.trim())) {
|
|
2624
|
+
const path = block.match(/^worktree (.+)$/m)?.[1];
|
|
2625
|
+
const branch = block.match(/^branch refs\/heads\/(.+)$/m)?.[1];
|
|
2626
|
+
if (!path || !branch || branch === integration) continue;
|
|
2627
|
+
const merged = gitOk(root, ["merge-base", "--is-ancestor", branch, integration]);
|
|
2628
|
+
let dirty = false;
|
|
2629
|
+
try {
|
|
2630
|
+
dirty = git(path, ["status", "--porcelain"]).length > 0;
|
|
2631
|
+
} catch {
|
|
2632
|
+
dirty = false;
|
|
2633
|
+
}
|
|
2634
|
+
rows.push({ path, branch, merged, dirty });
|
|
2635
|
+
}
|
|
2636
|
+
return { rows, integration };
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2365
2639
|
// src/cli.ts
|
|
2366
|
-
import { existsSync as
|
|
2367
|
-
var HERE =
|
|
2368
|
-
var MODULES2 =
|
|
2640
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
2641
|
+
var HERE = dirname9(fileURLToPath4(import.meta.url));
|
|
2642
|
+
var MODULES2 = join14(HERE, "..", "modules");
|
|
2369
2643
|
var c = {
|
|
2370
2644
|
dim: (s) => `\x1B[2m${s}\x1B[0m`,
|
|
2371
2645
|
bold: (s) => `\x1B[1m${s}\x1B[0m`,
|
|
@@ -2423,7 +2697,7 @@ ${mods.length} modules
|
|
|
2423
2697
|
return issues.length === 0 ? 0 : 1;
|
|
2424
2698
|
}
|
|
2425
2699
|
function cmdDoctor(target, doExplain = false) {
|
|
2426
|
-
const root =
|
|
2700
|
+
const root = resolve5(target);
|
|
2427
2701
|
const mods = loadAllModules(MODULES2);
|
|
2428
2702
|
console.log(c.bold(`
|
|
2429
2703
|
rungs doctor \u2014 ${root}
|
|
@@ -2594,6 +2868,29 @@ function cmdAdd(names, root, dryRun, harnesses, stamp) {
|
|
|
2594
2868
|
}
|
|
2595
2869
|
(overrides[modName] ??= {})[param] = rhs.join("=");
|
|
2596
2870
|
}
|
|
2871
|
+
for (const [modName, vals] of Object.entries(overrides)) {
|
|
2872
|
+
const mod = mods.find((m) => m.name === modName);
|
|
2873
|
+
if (!mod) {
|
|
2874
|
+
console.log(
|
|
2875
|
+
c.red(`
|
|
2876
|
+
--set names a module that does not exist: ${modName}`) + c.dim(`
|
|
2877
|
+
Known: ${mods.map((m) => m.name).join(", ")}
|
|
2878
|
+
`)
|
|
2879
|
+
);
|
|
2880
|
+
return 1;
|
|
2881
|
+
}
|
|
2882
|
+
for (const k of Object.keys(vals)) {
|
|
2883
|
+
if (!(k in mod.params)) {
|
|
2884
|
+
const known = Object.keys(mod.params);
|
|
2885
|
+
console.log(
|
|
2886
|
+
c.red(`
|
|
2887
|
+
--set names a parameter ${modName} does not have: ${k}`) + c.dim(`
|
|
2888
|
+
${known.length ? `${modName} takes: ${known.join(", ")}` : `${modName} takes no parameters`}`) + c.dim("\n `rungs modules --params` lists every parameter and its default.\n")
|
|
2889
|
+
);
|
|
2890
|
+
return 1;
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2597
2894
|
const params = resolveParams(mods, overrides, root);
|
|
2598
2895
|
for (const [m, vals] of Object.entries(overrides)) {
|
|
2599
2896
|
for (const [k, v] of Object.entries(vals)) console.log(c.dim(` set ${m}.${k} = ${v}`));
|
|
@@ -2712,6 +3009,49 @@ rungs render \u2014 ${root}
|
|
|
2712
3009
|
}
|
|
2713
3010
|
return 0;
|
|
2714
3011
|
}
|
|
3012
|
+
function report(r) {
|
|
3013
|
+
console.log();
|
|
3014
|
+
for (const l of r.lines) console.log(` ${r.ok ? l : c.yellow(l)}`);
|
|
3015
|
+
console.log();
|
|
3016
|
+
return r.ok ? 0 : 1;
|
|
3017
|
+
}
|
|
3018
|
+
function landRunner(dir, only) {
|
|
3019
|
+
const runs = runGates(dir, void 0, void 0, only);
|
|
3020
|
+
const failing = runs.filter((r) => r.status === "fail" || r.status === "error");
|
|
3021
|
+
return {
|
|
3022
|
+
pass: runs.filter((r) => r.status === "pass").length,
|
|
3023
|
+
// `file: message`, so the same broken link in the same file is the same
|
|
3024
|
+
// finding across two runs, and a *new* one is visibly not.
|
|
3025
|
+
failing: failing.map((r) => ({
|
|
3026
|
+
id: r.id,
|
|
3027
|
+
findings: r.findings.map((f) => `${f.file ? `${f.file}: ` : ""}${f.message}`)
|
|
3028
|
+
}))
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
function cmdWorktrees(root) {
|
|
3032
|
+
const { rows, integration } = worktrees(root);
|
|
3033
|
+
console.log(c.bold(`
|
|
3034
|
+
rungs worktrees \u2014 merged into ${integration}?
|
|
3035
|
+
`));
|
|
3036
|
+
if (!rows.length) {
|
|
3037
|
+
console.log(c.dim(" no linked worktrees. `rungs session start <branch>` creates one.\n"));
|
|
3038
|
+
return 0;
|
|
3039
|
+
}
|
|
3040
|
+
for (const w of rows) {
|
|
3041
|
+
const state = w.merged && w.dirty ? c.red("merged \xB7 DIRTY") : w.merged ? c.green("merged \xB7 prunable") : c.dim("in flight");
|
|
3042
|
+
console.log(` ${state.padEnd(28)} ${w.branch.padEnd(30)} ${c.dim(w.path)}`);
|
|
3043
|
+
}
|
|
3044
|
+
const risky = rows.filter((w) => w.merged && w.dirty);
|
|
3045
|
+
const prunable = rows.filter((w) => w.merged && !w.dirty);
|
|
3046
|
+
console.log();
|
|
3047
|
+
if (risky.length) {
|
|
3048
|
+
console.log(c.red(` ${risky.length} worktree(s) hold uncommitted work on a branch that already landed.`));
|
|
3049
|
+
console.log(c.dim(" That is where work actually gets lost. Commit it somewhere or decide to drop it."));
|
|
3050
|
+
}
|
|
3051
|
+
if (prunable.length) console.log(c.dim(` ${prunable.length} prunable. Removing a worktree is your call, not this command's.`));
|
|
3052
|
+
console.log();
|
|
3053
|
+
return 0;
|
|
3054
|
+
}
|
|
2715
3055
|
function cmdCheck(root, tier, stamp) {
|
|
2716
3056
|
let runs;
|
|
2717
3057
|
try {
|
|
@@ -2782,7 +3122,7 @@ function cmdBacklogArchive(root, dryRun) {
|
|
|
2782
3122
|
const record = readRecord(root);
|
|
2783
3123
|
const configured = record?.modules["backlog"]?.params?.root;
|
|
2784
3124
|
const backlogRoot = `docs/${configured ?? "backlog"}`;
|
|
2785
|
-
if (!
|
|
3125
|
+
if (!existsSync11(join14(root, ...backlogRoot.split("/"), "items"))) {
|
|
2786
3126
|
console.log(c.red(`
|
|
2787
3127
|
no backlog at ${backlogRoot}/items
|
|
2788
3128
|
`));
|
|
@@ -2907,7 +3247,11 @@ var COMMANDS = [
|
|
|
2907
3247
|
["eject [path]", "materialise the engines; stop depending on rungs"],
|
|
2908
3248
|
["setup git [path]", "install the merge drivers .gitattributes names"],
|
|
2909
3249
|
["modules", "list the module set and audit the manifests"],
|
|
2910
|
-
["backlog archive [path]", "move finished items to archive/, repointing every link"]
|
|
3250
|
+
["backlog archive [path]", "move finished items to archive/, repointing every link"],
|
|
3251
|
+
["session start <branch>", "cut a branch and worktree from the last verified merge"],
|
|
3252
|
+
["preflight [path]", "did the integration branch change files you changed?"],
|
|
3253
|
+
["land <branch>", "merge \u2192 verify the merged tree \u2192 advance, or refuse and park it"],
|
|
3254
|
+
["worktrees [path]", "which worktrees are merged, prunable, or merged and still dirty"]
|
|
2911
3255
|
];
|
|
2912
3256
|
var FLAGS = [
|
|
2913
3257
|
["--dry-run", "report what would happen, write nothing"],
|
|
@@ -2986,22 +3330,29 @@ switch (cmd) {
|
|
|
2986
3330
|
unknown: rungs backlog ${args[0] ?? ""}`) + c.dim("\n The only subcommand is `archive`.\n"));
|
|
2987
3331
|
process.exit(1);
|
|
2988
3332
|
}
|
|
2989
|
-
process.exit(cmdBacklogArchive(
|
|
3333
|
+
process.exit(cmdBacklogArchive(resolve5(args[1] ?? process.cwd()), flags.has("--dry-run")));
|
|
2990
3334
|
}
|
|
2991
3335
|
case "check": {
|
|
2992
3336
|
const tier = args[1] ?? (flags.has("--full") ? "full" : flags.has("--fast") ? "fast" : void 0);
|
|
2993
|
-
process.exit(cmdCheck(
|
|
3337
|
+
process.exit(cmdCheck(resolve5(args[0] ?? process.cwd()), tier, STAMP));
|
|
2994
3338
|
}
|
|
2995
3339
|
case "init": {
|
|
2996
3340
|
const profile = args[1] ?? "tracked";
|
|
2997
|
-
process.exit(cmdInit(
|
|
3341
|
+
process.exit(cmdInit(resolve5(args[0] ?? process.cwd()), profile, flags.has("--dry-run"), HARNESSES, STAMP));
|
|
2998
3342
|
}
|
|
2999
3343
|
case "upgrade":
|
|
3000
|
-
process.exit(cmdUpgrade(
|
|
3344
|
+
process.exit(cmdUpgrade(resolve5(args[0] ?? process.cwd()), flags.has("--apply")));
|
|
3001
3345
|
case "eject":
|
|
3002
|
-
process.exit(cmdEject(
|
|
3346
|
+
process.exit(cmdEject(resolve5(args[0] ?? process.cwd()), flags.has("--dry-run")));
|
|
3003
3347
|
case "setup": {
|
|
3004
|
-
|
|
3348
|
+
if (args[0] !== "git") {
|
|
3349
|
+
console.log(
|
|
3350
|
+
c.red(`
|
|
3351
|
+
unknown: rungs setup ${args[0] ?? ""}`.trimEnd()) + c.dim("\n The only subcommand is `git`, and the path comes after it: `rungs setup git [path]`.\n")
|
|
3352
|
+
);
|
|
3353
|
+
process.exit(1);
|
|
3354
|
+
}
|
|
3355
|
+
const r = setupGit(resolve5(args[1] ?? process.cwd()), flags.has("--dry-run"));
|
|
3005
3356
|
console.log(
|
|
3006
3357
|
r.drivers.length ? `
|
|
3007
3358
|
installed ${r.drivers.length} merge driver(s): ${r.drivers.join(", ")}` + (r.rerere ? c.dim(" \xB7 rerere on") : "") + c.dim("\n Declared drivers were inert until now \u2014 a fresh clone needs this once.\n") : c.dim("\n no rungs merge drivers declared in .gitattributes\n")
|
|
@@ -3009,11 +3360,25 @@ switch (cmd) {
|
|
|
3009
3360
|
process.exit(0);
|
|
3010
3361
|
}
|
|
3011
3362
|
case "render":
|
|
3012
|
-
process.exit(cmdRender(
|
|
3363
|
+
process.exit(cmdRender(resolve5(args[0] ?? process.cwd()), HARNESSES, STAMP));
|
|
3364
|
+
case "session": {
|
|
3365
|
+
if (args[0] !== "start") {
|
|
3366
|
+
console.log(c.red(`
|
|
3367
|
+
unknown: rungs session ${args[0] ?? ""}`.trimEnd()) + c.dim("\n The only subcommand is `start`: `rungs session start <branch> [path]`.\n"));
|
|
3368
|
+
process.exit(1);
|
|
3369
|
+
}
|
|
3370
|
+
process.exit(report(sessionStart(process.cwd(), args[1], args[2], flags.has("--dry-run"))));
|
|
3371
|
+
}
|
|
3372
|
+
case "preflight":
|
|
3373
|
+
process.exit(report(preflight(resolve5(args[0] ?? process.cwd()))));
|
|
3374
|
+
case "land":
|
|
3375
|
+
process.exit(report(land(process.cwd(), args[0], landRunner, flags.has("--dry-run"))));
|
|
3376
|
+
case "worktrees":
|
|
3377
|
+
process.exit(cmdWorktrees(resolve5(args[0] ?? process.cwd())));
|
|
3013
3378
|
case "add": {
|
|
3014
3379
|
const target = flags.has("--into") ? args[args.length - 1] : process.cwd();
|
|
3015
3380
|
const names = flags.has("--into") ? args.slice(0, -1) : args;
|
|
3016
|
-
process.exit(cmdAdd(names,
|
|
3381
|
+
process.exit(cmdAdd(names, resolve5(target), flags.has("--dry-run"), HARNESSES, STAMP));
|
|
3017
3382
|
}
|
|
3018
3383
|
default: {
|
|
3019
3384
|
const wantedHelp = cmd === void 0 || cmd === "help" || cmd === "--help" || cmd === "-h";
|