@inixiative/config 0.5.200 → 0.5.202
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 +7 -7
- package/dist/cli.js +215 -93
- package/package.json +1 -1
- package/versions.json +3 -2
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ TypeScript 6.0 notes, discovered by this repo's fixture suite:
|
|
|
71
71
|
bunx @inixiative/config check [dir] [--preset=base|node|react]
|
|
72
72
|
bunx @inixiative/config sync [dir] [--preset=...] [--force] [--no-install]
|
|
73
73
|
bunx @inixiative/config scan [root] [--lane=primitives|agentic]
|
|
74
|
-
bunx @inixiative/config train [root] [--lane=primitives|agentic] [--push] [--dry-run]
|
|
74
|
+
bunx @inixiative/config train [root] [--lane=primitives|agentic] [--push] [--dry-run] [--adopt-check-timeout=<seconds>]
|
|
75
75
|
bunx @inixiative/config ports [project]
|
|
76
76
|
bunx @inixiative/config terms [dir] [--write-baseline]
|
|
77
77
|
```
|
|
@@ -110,10 +110,10 @@ Lanes stand alone. A lane with a package missing a checkout, or claimed by ambig
|
|
|
110
110
|
Blessing is two-phase, so a dependent that needs work never deadlocks a release:
|
|
111
111
|
|
|
112
112
|
1. **Candidate.** A published version is recorded under `candidates`, and the config release that ends the train carries it, so `check` in any repo's CI accepts it from then on.
|
|
113
|
-
2. **Adopt.** A repo (lane checkout or consumer) whose check fails against the candidates it was moved onto doesn't stop the train. Its checkout is put back as it was, and the train opens `train/adopt-<pkg>-<version>` in a worktree (the repo's own `worktree:create <base> <branch>` when it declares one, so a template-based repo gets its slot's databases, released with its `worktree:destroy`; otherwise a temporary linked worktree; either way with the repo's ignored `.env*` files copied in where missing): the same bump and re-lock, then the repo's `train:adopt` script if its package.json declares one (Foundry's re-records cassettes with `bun run test:live`),
|
|
114
|
-
3. **Promote.** At the end of each run a candidate is blessed (moved into its lane's section) unless something holds it: its lane stopped; a repo has an adoption branch for it that is not yet on its default branch (opened this run, unmerged, or committed but never pushed); or a repo that declares it was not walked to an outcome this run (another lane's checkout, a skipped or failed one, a consumer without a checkout). A later train finds a waiting adoption branch and opens nothing new; once it merges, the repo is on the candidate and the train promotes it. A candidate every dependent already accepts is blessed in the run that published it. An adoption
|
|
113
|
+
2. **Adopt.** A repo (lane checkout or consumer) whose check fails against the candidates it was moved onto doesn't stop the train. Its checkout is put back as it was, and the train opens `train/adopt-<pkg>-<version>` in a worktree (the repo's own `worktree:create <base> <branch>` when it declares one, so a template-based repo gets its slot's databases, released with its `worktree:destroy`; otherwise a temporary linked worktree; either way with the repo's ignored `.env*` files copied in where missing): the same bump and re-lock, then the repo's `train:adopt` script if its package.json declares one (Foundry's re-records cassettes with `bun run test:live`), and a commit. The train stages its own writes (the package.json files and `bun.lock`) before `train:adopt` runs and captures what the script wrote as it exits. That output joins the commit only if the script passed and the repo's `check` then passes with it in place, within `--adopt-check-timeout` seconds (default 1800; a timeout is a failure). That check runs against the config release the train started from, before the branch is re-locked onto the new one. Anything the check itself writes is never committed. Otherwise the adoption is **incomplete**: the commit is the re-lock alone (`chore: adopt <label> (incomplete)`, with a `Train-Adopt: incomplete (<reason>)` trailer), and the script's output, edits to owned files included, is saved on `<branch>-output`, one commit on top of it, which `--push` pushes beside the branch. Later trains read the trailer, keep reporting the branch as incomplete, and print what to run to finish it: `bun run train:adopt && bun run check` on the branch, then a commit with the trailer `Train-Adopt: complete`. A repo that declares no `train:adopt` gets the re-lock alone. `--dry-run` never fails a check, so for each repo moving onto candidates it prints how an adoption would be decided. The branch is then re-locked onto the new config release so its CI judges it against the candidates. `--push` pushes the branch; the train opens no PR. A lane checkout that adopts publishes nothing this run. A consumer that passes against candidates commits on its default branch, like any other passing repo. If the adoption's commit is refused (its hooks typecheck), the branch is deleted and the summary says to adopt by hand.
|
|
114
|
+
3. **Promote.** At the end of each run a candidate is blessed (moved into its lane's section) unless something holds it: its lane stopped; a repo has an adoption branch for it that is not yet on its default branch (opened this run, incomplete, unmerged, or committed but never pushed); or a repo that declares it was not walked to an outcome this run (another lane's checkout, a skipped or failed one, a consumer without a checkout). A later train finds a waiting adoption branch and opens nothing new; once it merges, the repo is on the candidate and the train promotes it. A candidate every dependent already accepts is blessed in the run that published it. An adoption branch abandoned without merging holds its candidates until it is deleted; the next train that moves that repo deletes it.
|
|
115
115
|
|
|
116
|
-
The summary lists what was blessed, every candidate with what holds it, every hold with its reason, and each pending adoption branch
|
|
116
|
+
The summary lists what was blessed, every candidate with what holds it (an incomplete adoption is marked so), every hold with its reason, and each pending adoption branch.
|
|
117
117
|
|
|
118
118
|
**Holds.** A hold keeps a package's breaking line out of the blessed set while its dependents can't take it yet (signet 0.12 waiting on Kingdom's grants cutover), without stopping the package's own releases or anyone else's:
|
|
119
119
|
|
|
@@ -139,11 +139,11 @@ Every version at or above `below` is held; versions below it ride the train as u
|
|
|
139
139
|
|
|
140
140
|
A root package publishes with `npm publish`; a workspace member is packed with `bun pm pack`, which rewrites `workspace:` ranges to the versions being released, and the tarball is published with `npm publish`, so OTP prompts pass through either way. A package already on npm at its local version but neither blessed nor a candidate (published by hand, or by a train whose lane later failed) becomes a candidate on the next run, unless a hold covers that version. Likewise, an `@inixiative/config` version committed ahead of npm (a release whose publish failed) is published by the next run, even when it blesses nothing new.
|
|
141
141
|
|
|
142
|
-
Consumers run once, after every selected lane, upstreams first, against the full blessed set: a consumer of both lanes ends coherent with both whichever lane ran. They get the same bump → re-lock → `check`, never a publish, and
|
|
142
|
+
Consumers run once, after every selected lane, upstreams first, against the full blessed set: a consumer of both lanes ends coherent with both whichever lane ran. They get the same bump → re-lock → `check`, never a publish, and a passing re-lock is committed on their default branch, as a lane repo's is. Only a consumer that fails against candidates gets a branch (an adoption, above).
|
|
143
143
|
|
|
144
144
|
A train never leaves a checkout dirty or off its default branch. It only works in a checkout it found clean, so a checkout that then fails is reset to HEAD (tracked and untracked files; ignored ones such as node_modules and `.env` stay) and re-installed from its lockfile. Every branch it makes is committed in a temporary worktree, removed when the train exits.
|
|
145
145
|
|
|
146
|
-
When anything was recorded or blessed it then ships this package last, so the BOM names the new state: bumps its own patch version if npm already has the current one, blesses that version in the BOM, brings the `test/fixtures` consumers onto the blessed set, runs `check`, commits, publishes. Then every checkout the train re-locked (lane packages on their default branch,
|
|
146
|
+
When anything was recorded or blessed it then ships this package last, so the BOM names the new state: bumps its own patch version if npm already has the current one, blesses that version in the BOM, brings the `test/fixtures` consumers onto the blessed set, runs `check`, commits, publishes. Then every checkout the train re-locked (lane packages and consumers on their default branch, adoption branches in their worktrees) is re-locked onto that new `@inixiative/config` and committed, so none is left pinning the previous release. It never pushes unless asked: pass `--push` to push every repo it committed to and each adoption branch, then delete every other `train/*` branch, local and on origin, in each repo it moved (that repo is now on the whole target, so they are superseded). It opens no PRs. Without `--push` it prints the commands. Its worktrees, and the temporary dirs that hold them, are removed however the run ends. `--dry-run` walks the same plan and prints each step it would take (range rewrites, re-locks and checks, commits, adoption branches, publishes, BOM blessings, pushes) without running any of them; it only fetches and reads git state and `npm view`.
|
|
147
147
|
|
|
148
148
|
Each command accepts only its own flags. An unknown or misplaced flag (`--dryrun`, `--push` on `sync`) is a usage error and exits 2 before anything runs.
|
|
149
149
|
|
|
@@ -173,4 +173,4 @@ Two lanes, each publishing in dependency order, with config last:
|
|
|
173
173
|
- **primitives:** for example json-rules → permissions → transitions → rules-builder, plus prisma-map, atlas and gloss. Consumers: template, kingdom.
|
|
174
174
|
- **agentic:** signet, kcp (the Kingdom CLI proxy, on signet), archive and agent-session, then foundry-core → foundry → foundry-acp (foundry depends on archive), built on the primitives' blessed set. foundry-core, foundry and foundry-acp publish from the foundry monorepo's `packages/core`, `packages/foundry` and `packages/acp`. Consumers: oracle (a private leaf), kingdom.
|
|
175
175
|
|
|
176
|
-
A release is `train` (or `train --lane=agentic` to leave the primitives alone): bump the package's version on its default branch, then run the train. `train` automates the whole walk, this package and the consumer
|
|
176
|
+
A release is `train` (or `train --lane=agentic` to leave the primitives alone): bump the package's version on its default branch, then run the train. `train` automates the whole walk, this package and the consumer bumps included. Downstream repos pick up toolchain changes via `sync`.
|
package/dist/cli.js
CHANGED
|
@@ -1008,12 +1008,13 @@ var OPTIONS = {
|
|
|
1008
1008
|
check: { preset },
|
|
1009
1009
|
sync: { preset, force: flag, "no-install": flag },
|
|
1010
1010
|
scan: { lane },
|
|
1011
|
-
train: { lane, push: flag, "dry-run": flag },
|
|
1011
|
+
train: { lane, push: flag, "dry-run": flag, "adopt-check-timeout": { type: "string" } },
|
|
1012
1012
|
ports: {},
|
|
1013
1013
|
terms: { "write-baseline": flag }
|
|
1014
1014
|
};
|
|
1015
1015
|
var UsageError = class extends Error {
|
|
1016
1016
|
};
|
|
1017
|
+
var DEFAULT_ADOPT_CHECK_TIMEOUT = 30 * 60;
|
|
1017
1018
|
var isCommand = (value) => COMMANDS.includes(value);
|
|
1018
1019
|
var parseCommandLine = (argv) => {
|
|
1019
1020
|
const command2 = argv[0];
|
|
@@ -1050,6 +1051,11 @@ var parseCommandLine = (argv) => {
|
|
|
1050
1051
|
if (typeof laneValue === "string" && !isLane(laneValue)) {
|
|
1051
1052
|
throw new UsageError(`unknown lane: ${laneValue}`);
|
|
1052
1053
|
}
|
|
1054
|
+
const timeoutValue = values["adopt-check-timeout"];
|
|
1055
|
+
const adoptCheckTimeout2 = typeof timeoutValue === "string" ? Number(timeoutValue) : DEFAULT_ADOPT_CHECK_TIMEOUT;
|
|
1056
|
+
if (!Number.isInteger(adoptCheckTimeout2) || adoptCheckTimeout2 <= 0) {
|
|
1057
|
+
throw new UsageError(`--adopt-check-timeout takes whole seconds, got ${timeoutValue}`);
|
|
1058
|
+
}
|
|
1053
1059
|
return {
|
|
1054
1060
|
command: command2,
|
|
1055
1061
|
positionals: positionals2,
|
|
@@ -1059,6 +1065,7 @@ var parseCommandLine = (argv) => {
|
|
|
1059
1065
|
noInstall: values["no-install"] === true,
|
|
1060
1066
|
push: values.push === true,
|
|
1061
1067
|
dryRun: values["dry-run"] === true,
|
|
1068
|
+
adoptCheckTimeout: adoptCheckTimeout2,
|
|
1062
1069
|
writeBaseline: values["write-baseline"] === true
|
|
1063
1070
|
};
|
|
1064
1071
|
};
|
|
@@ -1349,7 +1356,7 @@ try {
|
|
|
1349
1356
|
);
|
|
1350
1357
|
process.exit(2);
|
|
1351
1358
|
}
|
|
1352
|
-
var { command, positionals, preset: presetFlag, dryRun } = commandLine;
|
|
1359
|
+
var { command, positionals, preset: presetFlag, dryRun, adoptCheckTimeout } = commandLine;
|
|
1353
1360
|
var dir = resolve2(positionals[0] ?? ".");
|
|
1354
1361
|
if (command === "ports") {
|
|
1355
1362
|
const project = positionals[0];
|
|
@@ -1547,7 +1554,7 @@ if (command === "train") {
|
|
|
1547
1554
|
const failed = [];
|
|
1548
1555
|
const failedLanes = /* @__PURE__ */ new Set();
|
|
1549
1556
|
const committed = /* @__PURE__ */ new Set();
|
|
1550
|
-
const
|
|
1557
|
+
const adoptionBranches = [];
|
|
1551
1558
|
const waiting = [];
|
|
1552
1559
|
const walked = /* @__PURE__ */ new Set();
|
|
1553
1560
|
const worktrees = [];
|
|
@@ -1557,7 +1564,10 @@ if (command === "train") {
|
|
|
1557
1564
|
cwd: repo,
|
|
1558
1565
|
stdio: "inherit"
|
|
1559
1566
|
});
|
|
1560
|
-
} else
|
|
1567
|
+
} else {
|
|
1568
|
+
git(repo, "worktree", "remove", "--force", path);
|
|
1569
|
+
rmSync3(dirname3(path), { recursive: true, force: true });
|
|
1570
|
+
}
|
|
1561
1571
|
};
|
|
1562
1572
|
process.on("exit", () => {
|
|
1563
1573
|
for (const entry of worktrees.splice(0)) removeWorktree(entry);
|
|
@@ -1661,18 +1671,52 @@ if (command === "train") {
|
|
|
1661
1671
|
if (git(cwd, "rev-parse", "--verify", "--quiet", ref).status !== 0) return null;
|
|
1662
1672
|
return Number(git(cwd, "rev-list", "--count", `HEAD..${ref}`).stdout.trim());
|
|
1663
1673
|
};
|
|
1674
|
+
const outputBranch = (branch) => `${branch}-output`;
|
|
1675
|
+
const incompleteReason = (cwd, ref) => {
|
|
1676
|
+
const newest = git(
|
|
1677
|
+
cwd,
|
|
1678
|
+
"log",
|
|
1679
|
+
"--format=%(trailers:key=Train-Adopt,valueonly,separator=%x2C)",
|
|
1680
|
+
`HEAD..${ref}`
|
|
1681
|
+
).stdout.split("\n").map((value) => value.trim()).find(Boolean);
|
|
1682
|
+
if (!newest?.startsWith("incomplete")) return null;
|
|
1683
|
+
return /\((.*)\)/.exec(newest)?.[1] ?? "no reason recorded";
|
|
1684
|
+
};
|
|
1664
1685
|
const pendingAdoption = (checkout, moving) => {
|
|
1665
1686
|
if (moving.length === 0) return false;
|
|
1666
1687
|
const branch = adoptionBranch(moving);
|
|
1667
1688
|
const remote = ahead(checkout.dir, `refs/remotes/origin/${branch}`);
|
|
1668
1689
|
const local = ahead(checkout.dir, `refs/heads/${branch}`);
|
|
1669
1690
|
if (!remote && !local) return false;
|
|
1691
|
+
walked.add(checkout.dir);
|
|
1692
|
+
const reason = incompleteReason(
|
|
1693
|
+
checkout.dir,
|
|
1694
|
+
remote ? `refs/remotes/origin/${branch}` : `refs/heads/${branch}`
|
|
1695
|
+
);
|
|
1696
|
+
if (reason) {
|
|
1697
|
+
const output = outputBranch(branch);
|
|
1698
|
+
const saved = [`refs/heads/${output}`, `refs/remotes/origin/${output}`].some(
|
|
1699
|
+
(ref) => git(checkout.dir, "rev-parse", "--verify", "--quiet", ref).status === 0
|
|
1700
|
+
);
|
|
1701
|
+
console.log(`\u23F3 adopting ${moving.join(", ")} on ${branch} \u2014 incomplete (${reason})`);
|
|
1702
|
+
console.log(
|
|
1703
|
+
` finish it: on ${branch}, run \`bun run train:adopt && bun run check\`, commit what it wrote with \`git commit --trailer 'Train-Adopt: complete'\`, and push${saved ? `; the last train's output is on ${output}` : ""}`
|
|
1704
|
+
);
|
|
1705
|
+
waiting.push({
|
|
1706
|
+
name: checkout.name,
|
|
1707
|
+
dir: checkout.dir,
|
|
1708
|
+
moving,
|
|
1709
|
+
state: "incomplete",
|
|
1710
|
+
branch,
|
|
1711
|
+
reason
|
|
1712
|
+
});
|
|
1713
|
+
return true;
|
|
1714
|
+
}
|
|
1670
1715
|
const state = remote ? "unmerged" : "unpushed";
|
|
1671
1716
|
console.log(
|
|
1672
|
-
`\u23F3 adopting ${moving.join(", ")} on ${branch} \u2014 ${state === "unmerged" ? "waiting for it to merge" : "never pushed: push it
|
|
1717
|
+
`\u23F3 adopting ${moving.join(", ")} on ${branch} \u2014 ${state === "unmerged" ? "waiting for it to merge" : "never pushed: push it"}`
|
|
1673
1718
|
);
|
|
1674
1719
|
waiting.push({ name: checkout.name, dir: checkout.dir, moving, state, branch });
|
|
1675
|
-
walked.add(checkout.dir);
|
|
1676
1720
|
return true;
|
|
1677
1721
|
};
|
|
1678
1722
|
const worktree = (checkout, branch) => {
|
|
@@ -1713,67 +1757,93 @@ if (command === "train") {
|
|
|
1713
1757
|
git(checkout.dir, "branch", "-D", branch);
|
|
1714
1758
|
};
|
|
1715
1759
|
const defaultBranch = (cwd) => offDefaultBranch(cwd)?.main ?? currentBranch(cwd);
|
|
1716
|
-
const
|
|
1717
|
-
const
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
restore(checkout);
|
|
1730
|
-
try {
|
|
1731
|
-
relockAndCheck(branchCheckout, false);
|
|
1732
|
-
const message = moving.length > 0 ? `chore: adopt ${moving.map((name) => `${name}@${manifest.candidates?.[name]}`).join(", ")}` : void 0;
|
|
1733
|
-
if (!commitOwned(branchCheckout, packagePaths, true, message)) {
|
|
1734
|
-
console.log(`\u2713 nothing to commit for ${checkout.name} after its re-lock`);
|
|
1735
|
-
dropBranch(checkout, branch);
|
|
1736
|
-
return false;
|
|
1737
|
-
}
|
|
1738
|
-
} catch (error) {
|
|
1739
|
-
dropBranch(checkout, branch);
|
|
1740
|
-
throw error;
|
|
1760
|
+
const captureOutput = (cwd) => {
|
|
1761
|
+
const changed = [
|
|
1762
|
+
...git(cwd, "diff", "--name-only", "-z").stdout.split("\0"),
|
|
1763
|
+
...git(cwd, "ls-files", "--others", "--exclude-standard", "-z").stdout.split("\0")
|
|
1764
|
+
].filter(Boolean);
|
|
1765
|
+
if (changed.length === 0) return null;
|
|
1766
|
+
const scratch = mkdtempSync(join3(tmpdir(), "inixiative-train-"));
|
|
1767
|
+
try {
|
|
1768
|
+
const env = { ...process.env, GIT_INDEX_FILE: join3(scratch, "index") };
|
|
1769
|
+
const step = (...args) => {
|
|
1770
|
+
const result = spawnSync3("git", args, { cwd, env, encoding: "utf8" });
|
|
1771
|
+
if (result.status !== 0) {
|
|
1772
|
+
throw new Abort(`git ${args[0]} failed capturing train:adopt's output`);
|
|
1741
1773
|
}
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
if (moving.length > 0) {
|
|
1750
|
-
waiting.push({ name: checkout.name, dir: checkout.dir, moving, state: "opened", branch });
|
|
1774
|
+
return result.stdout.trim();
|
|
1775
|
+
};
|
|
1776
|
+
step("read-tree", git(cwd, "write-tree").stdout.trim());
|
|
1777
|
+
step("add", "-A", "--", ...changed);
|
|
1778
|
+
return step("write-tree");
|
|
1779
|
+
} finally {
|
|
1780
|
+
rmSync3(scratch, { recursive: true, force: true });
|
|
1751
1781
|
}
|
|
1752
1782
|
};
|
|
1783
|
+
const adoptionPlan = (script, output) => script ? `re-lock, run train:adopt, then the check \u2014 complete if both pass, else incomplete with the re-lock only and train:adopt's output saved on ${output}` : "re-lock and commit";
|
|
1784
|
+
const previewAdoption = (checkout, moving) => {
|
|
1785
|
+
if (!dryRun || moving.length === 0) return;
|
|
1786
|
+
const branch = adoptionBranch(moving);
|
|
1787
|
+
console.log(
|
|
1788
|
+
` [dry-run] would, if its check fails against the candidates, open ${branch} in ${checkout.name}: ${adoptionPlan(checkout.pkg.scripts?.["train:adopt"], outputBranch(branch))}`
|
|
1789
|
+
);
|
|
1790
|
+
};
|
|
1753
1791
|
const adopt = (checkout, moving) => {
|
|
1754
1792
|
const branch = adoptionBranch(moving);
|
|
1755
|
-
const
|
|
1793
|
+
const output = outputBranch(branch);
|
|
1756
1794
|
const label = moving.map((name) => `${name}@${manifest.candidates?.[name]}`).join(", ");
|
|
1757
1795
|
const script = checkout.pkg.scripts?.["train:adopt"];
|
|
1758
1796
|
try {
|
|
1759
|
-
effect(
|
|
1760
|
-
`open ${branch} in ${checkout.name}:
|
|
1761
|
-
|
|
1797
|
+
const outcome = effect(
|
|
1798
|
+
`open ${branch} in ${checkout.name}: ${adoptionPlan(script, output)}`,
|
|
1799
|
+
{ reason: null, saved: false },
|
|
1762
1800
|
() => {
|
|
1763
1801
|
const branchCheckout = worktree(checkout, branch);
|
|
1802
|
+
let reason = null;
|
|
1803
|
+
let saved = false;
|
|
1764
1804
|
try {
|
|
1765
|
-
bumpRanges(branchCheckout);
|
|
1805
|
+
const { inspection } = bumpRanges(branchCheckout);
|
|
1766
1806
|
relockAndCheck(branchCheckout, false);
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
`\u26A0 train:adopt failed in ${checkout.name} \u2014 the PR needs work before it merges`
|
|
1773
|
-
);
|
|
1807
|
+
const owned = [...inspection.packagePaths, "bun.lock"].filter(
|
|
1808
|
+
(path) => existsSync3(join3(branchCheckout.dir, path))
|
|
1809
|
+
);
|
|
1810
|
+
if (git(branchCheckout.dir, "add", "--", ...owned).status !== 0) {
|
|
1811
|
+
throw new Abort(`git add failed on ${branch} in ${checkout.name}`);
|
|
1774
1812
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1813
|
+
let produced = null;
|
|
1814
|
+
if (script) {
|
|
1815
|
+
const adopted = spawnSync3("bun", ["run", "train:adopt"], {
|
|
1816
|
+
cwd: branchCheckout.dir,
|
|
1817
|
+
stdio: "inherit"
|
|
1818
|
+
}).status === 0;
|
|
1819
|
+
produced = captureOutput(branchCheckout.dir);
|
|
1820
|
+
const leftOut = `its output is left out; ${branch} carries the re-lock only and needs work before it merges`;
|
|
1821
|
+
if (!adopted) {
|
|
1822
|
+
reason = "train:adopt failed";
|
|
1823
|
+
console.log(`\u26A0 train:adopt failed in ${checkout.name} \u2014 ${leftOut}`);
|
|
1824
|
+
} else {
|
|
1825
|
+
const check = spawnSync3("bun", ["run", "check"], {
|
|
1826
|
+
cwd: branchCheckout.dir,
|
|
1827
|
+
stdio: "inherit",
|
|
1828
|
+
timeout: adoptCheckTimeout * 1e3,
|
|
1829
|
+
killSignal: "SIGKILL"
|
|
1830
|
+
});
|
|
1831
|
+
if (check.status !== 0) {
|
|
1832
|
+
reason = "check failed after train:adopt";
|
|
1833
|
+
console.log(
|
|
1834
|
+
check.error || check.signal ? `\u26A0 check timed out after ${adoptCheckTimeout}s in ${checkout.name} after train:adopt \u2014 ${leftOut}` : `\u26A0 check still fails in ${checkout.name} after train:adopt \u2014 ${leftOut}`
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
if (!reason && produced) git(branchCheckout.dir, "read-tree", produced);
|
|
1839
|
+
}
|
|
1840
|
+
const message = reason ? [
|
|
1841
|
+
"-m",
|
|
1842
|
+
`chore: adopt ${label} (incomplete)`,
|
|
1843
|
+
"-m",
|
|
1844
|
+
`Train-Adopt: incomplete (${reason})`
|
|
1845
|
+
] : ["-m", `chore: adopt ${label}`];
|
|
1846
|
+
const commit = spawnSync3("git", ["commit", ...message], {
|
|
1777
1847
|
cwd: branchCheckout.dir,
|
|
1778
1848
|
stdio: "inherit"
|
|
1779
1849
|
});
|
|
@@ -1782,15 +1852,44 @@ if (command === "train") {
|
|
|
1782
1852
|
`commit failed on ${branch} in ${checkout.name} (its hooks reject the adoption)`
|
|
1783
1853
|
);
|
|
1784
1854
|
}
|
|
1855
|
+
if (reason && produced) {
|
|
1856
|
+
const saving = spawnSync3(
|
|
1857
|
+
"git",
|
|
1858
|
+
[
|
|
1859
|
+
"commit-tree",
|
|
1860
|
+
produced,
|
|
1861
|
+
"-p",
|
|
1862
|
+
"HEAD",
|
|
1863
|
+
"-m",
|
|
1864
|
+
`train:adopt output for ${branch} (${reason}), not adopted`
|
|
1865
|
+
],
|
|
1866
|
+
{ cwd: branchCheckout.dir, encoding: "utf8" }
|
|
1867
|
+
);
|
|
1868
|
+
saved = saving.status === 0 && git(branchCheckout.dir, "branch", "-f", output, saving.stdout.trim()).status === 0;
|
|
1869
|
+
console.log(
|
|
1870
|
+
saved ? ` its output is saved on ${output} (git -C ${checkout.dir} diff ${branch} ${output})` : `\u2717 could not save train:adopt's output on ${output} in ${checkout.name}`
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
git(branchCheckout.dir, "reset", "--quiet", "--hard", "HEAD");
|
|
1874
|
+
git(branchCheckout.dir, "clean", "--quiet", "-fd");
|
|
1785
1875
|
} catch (error) {
|
|
1786
1876
|
dropBranch(checkout, branch);
|
|
1877
|
+
git(checkout.dir, "branch", "-D", output);
|
|
1787
1878
|
throw error;
|
|
1788
1879
|
}
|
|
1789
1880
|
followers.push({ checkout: branchCheckout, onBranch: true });
|
|
1881
|
+
return { reason, saved };
|
|
1790
1882
|
}
|
|
1791
1883
|
);
|
|
1792
|
-
|
|
1793
|
-
waiting.push({
|
|
1884
|
+
adoptionBranches.push({ dir: checkout.dir, branch, ...outcome.saved ? { output } : {} });
|
|
1885
|
+
waiting.push({
|
|
1886
|
+
name: checkout.name,
|
|
1887
|
+
dir: checkout.dir,
|
|
1888
|
+
moving,
|
|
1889
|
+
state: outcome.reason ? "incomplete" : "opened",
|
|
1890
|
+
branch,
|
|
1891
|
+
...outcome.reason ? { reason: outcome.reason } : {}
|
|
1892
|
+
});
|
|
1794
1893
|
} catch (error) {
|
|
1795
1894
|
if (!(error instanceof Abort)) throw error;
|
|
1796
1895
|
failed.push(checkout.name);
|
|
@@ -1891,6 +1990,7 @@ if (command === "train") {
|
|
|
1891
1990
|
if (pendingAdoption(checkout, moving)) continue;
|
|
1892
1991
|
const { inspection, bumped, staleLock } = bumpRanges(checkout);
|
|
1893
1992
|
const relocked = bumped || staleLock || releases.some((release) => release.ahead);
|
|
1993
|
+
if (relocked) previewAdoption(checkout, moving);
|
|
1894
1994
|
try {
|
|
1895
1995
|
if (relocked) relockAndCheck(checkout);
|
|
1896
1996
|
if (commitOwned(checkout, inspection.packagePaths, relocked)) committed.add(checkout.dir);
|
|
@@ -1969,9 +2069,13 @@ if (command === "train") {
|
|
|
1969
2069
|
console.log("\u2713 already on the blessed set");
|
|
1970
2070
|
continue;
|
|
1971
2071
|
}
|
|
2072
|
+
previewAdoption(checkout, moving);
|
|
1972
2073
|
try {
|
|
1973
2074
|
relockAndCheck(checkout);
|
|
1974
|
-
|
|
2075
|
+
const message = moving.length > 0 ? `chore: adopt ${moving.map((name) => `${name}@${manifest.candidates?.[name]}`).join(", ")}` : void 0;
|
|
2076
|
+
if (commitOwned(checkout, inspection.packagePaths, true, message))
|
|
2077
|
+
committed.add(checkout.dir);
|
|
2078
|
+
followers.push({ checkout, onBranch: false });
|
|
1975
2079
|
walked.add(checkout.dir);
|
|
1976
2080
|
} catch (error) {
|
|
1977
2081
|
if (!(error instanceof Abort)) throw error;
|
|
@@ -2028,7 +2132,8 @@ if (command === "train") {
|
|
|
2028
2132
|
}
|
|
2029
2133
|
const why = {
|
|
2030
2134
|
opened: "opened this run",
|
|
2031
|
-
|
|
2135
|
+
incomplete: "incomplete",
|
|
2136
|
+
unmerged: "waiting for its branch to merge",
|
|
2032
2137
|
unpushed: "committed but never pushed",
|
|
2033
2138
|
failed: "the train could not commit it \u2014 adopt by hand",
|
|
2034
2139
|
unverified: "not verified this run"
|
|
@@ -2039,17 +2144,18 @@ blessed: ${promoted.length === 0 ? "nothing" : promoted.join(", ")}`);
|
|
|
2039
2144
|
if (candidates.length > 0) {
|
|
2040
2145
|
console.log("candidates (check accepts them; not yet blessed):");
|
|
2041
2146
|
for (const [name, version] of candidates) {
|
|
2042
|
-
const on = waiting.filter((entry) => entry.moving.includes(name)).map(
|
|
2147
|
+
const on = waiting.filter((entry) => entry.moving.includes(name)).map(
|
|
2148
|
+
(entry) => `${entry.name} (${entry.branch ?? why[entry.state]}${entry.state === "incomplete" ? ", incomplete" : ""})`
|
|
2149
|
+
);
|
|
2043
2150
|
console.log(` ${name}@${version}${on.length === 0 ? "" : ` \u2014 waiting on ${on.join(", ")}`}`);
|
|
2044
2151
|
}
|
|
2045
2152
|
}
|
|
2046
2153
|
for (const line of holdLines(manifest)) console.log(line);
|
|
2047
2154
|
if (waiting.length > 0) {
|
|
2048
|
-
console.log("pending (merge each adoption
|
|
2155
|
+
console.log("pending (merge each adoption branch, then run the train again to bless):");
|
|
2049
2156
|
for (const entry of waiting) {
|
|
2050
|
-
const url = entry.state === "unmerged" && !dryRun && entry.branch ? prUrl(entry.dir, entry.branch) : null;
|
|
2051
2157
|
console.log(
|
|
2052
|
-
` ${entry.name}: ${entry.branch ? `${entry.branch} \u2014 ` : ""}${
|
|
2158
|
+
` ${entry.name}: ${entry.branch ? `${entry.branch} \u2014 ` : ""}${entry.state === "incomplete" ? `incomplete (${entry.reason}) \u2014 finish it on the branch` : why[entry.state]}`
|
|
2053
2159
|
);
|
|
2054
2160
|
}
|
|
2055
2161
|
}
|
|
@@ -2136,13 +2242,24 @@ blessed: ${promoted.length === 0 ? "nothing" : promoted.join(", ")}`);
|
|
|
2136
2242
|
}
|
|
2137
2243
|
}
|
|
2138
2244
|
}
|
|
2245
|
+
const moved = [.../* @__PURE__ */ new Set([...committed, ...adoptionBranches.map((entry) => entry.dir)])];
|
|
2246
|
+
const keep = new Set(
|
|
2247
|
+
adoptionBranches.flatMap(
|
|
2248
|
+
(entry) => [entry.branch, entry.output].flatMap((branch) => branch ? [`${entry.dir}\0${branch}`] : [])
|
|
2249
|
+
)
|
|
2250
|
+
);
|
|
2251
|
+
const superseded = (repoDir) => {
|
|
2252
|
+
const refs = (prefix) => git(repoDir, "for-each-ref", "--format=%(refname:short)", prefix).stdout.split("\n").map((ref) => ref.trim().replace(/^origin\//, "")).filter((branch) => branch.startsWith("train/") && !keep.has(`${repoDir}\0${branch}`));
|
|
2253
|
+
return { local: refs("refs/heads/train/"), remote: refs("refs/remotes/origin/train/") };
|
|
2254
|
+
};
|
|
2139
2255
|
if (dryRun) {
|
|
2140
2256
|
console.log(
|
|
2141
2257
|
`
|
|
2142
|
-
dry run complete \u2014 a real train would commit in ${committed.size} repo(s) and open ${
|
|
2258
|
+
dry run complete \u2014 a real train would commit in ${committed.size} repo(s) and open ${adoptionBranches.length} adoption branch(es)${commandLine.push ? ", then push them and delete the train branches they supersede" : ""}:`
|
|
2143
2259
|
);
|
|
2144
2260
|
for (const repoDir of committed) console.log(` ${repoDir}`);
|
|
2145
|
-
for (const { dir: repoDir, branch } of
|
|
2261
|
+
for (const { dir: repoDir, branch } of adoptionBranches)
|
|
2262
|
+
console.log(` ${repoDir} (${branch})`);
|
|
2146
2263
|
} else if (commandLine.push) {
|
|
2147
2264
|
for (const repoDir of committed) {
|
|
2148
2265
|
if (spawnSync3("git", ["push"], { cwd: repoDir, stdio: "inherit" }).status !== 0) {
|
|
@@ -2150,45 +2267,50 @@ dry run complete \u2014 a real train would commit in ${committed.size} repo(s) a
|
|
|
2150
2267
|
process.exit(1);
|
|
2151
2268
|
}
|
|
2152
2269
|
}
|
|
2153
|
-
for (const { dir: repoDir, branch,
|
|
2154
|
-
const
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2270
|
+
for (const { dir: repoDir, branch, output } of adoptionBranches) {
|
|
2271
|
+
for (const ref of output ? [branch, output] : [branch]) {
|
|
2272
|
+
const push = spawnSync3("git", ["push", "-u", "origin", ref], {
|
|
2273
|
+
cwd: repoDir,
|
|
2274
|
+
stdio: "inherit"
|
|
2275
|
+
});
|
|
2276
|
+
if (push.status !== 0) console.error(`\u2717 could not push ${ref} in ${repoDir}`);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
for (const repoDir of moved) {
|
|
2280
|
+
git(repoDir, "fetch", "--quiet", "--prune", "origin");
|
|
2281
|
+
const { local, remote } = superseded(repoDir);
|
|
2282
|
+
const gone = /* @__PURE__ */ new Set();
|
|
2283
|
+
for (const branch of remote) {
|
|
2284
|
+
if (git(repoDir, "push", "--quiet", "origin", "--delete", branch).status === 0) {
|
|
2285
|
+
gone.add(branch);
|
|
2286
|
+
} else console.error(`\u2717 could not delete origin/${branch} in ${repoDir}`);
|
|
2287
|
+
}
|
|
2288
|
+
for (const branch of local) {
|
|
2289
|
+
if (git(repoDir, "branch", "-D", branch).status === 0) gone.add(branch);
|
|
2290
|
+
else console.error(`\u2717 could not delete ${branch} in ${repoDir}`);
|
|
2291
|
+
}
|
|
2292
|
+
if (gone.size > 0) console.log(`\u{1F9F9} ${repoDir}: deleted ${[...gone].join(", ")}`);
|
|
2163
2293
|
}
|
|
2164
|
-
console.log(`pushed: ${committed.size +
|
|
2165
|
-
} else if (committed.size > 0 ||
|
|
2294
|
+
console.log(`pushed: ${committed.size + adoptionBranches.length} repos`);
|
|
2295
|
+
} else if (committed.size > 0 || adoptionBranches.length > 0) {
|
|
2166
2296
|
console.log("train does not push without --push \u2014 review its commits, then push:");
|
|
2167
2297
|
for (const repoDir of committed) console.log(` git -C ${repoDir} push`);
|
|
2168
|
-
for (const { dir: repoDir, branch,
|
|
2169
|
-
console.log(
|
|
2170
|
-
|
|
2171
|
-
|
|
2298
|
+
for (const { dir: repoDir, branch, output } of adoptionBranches) {
|
|
2299
|
+
console.log(` git -C ${repoDir} push -u origin ${branch}`);
|
|
2300
|
+
if (output) console.log(` git -C ${repoDir} push -u origin ${output}`);
|
|
2301
|
+
}
|
|
2302
|
+
for (const repoDir of moved) {
|
|
2303
|
+
const { local, remote } = superseded(repoDir);
|
|
2304
|
+
for (const branch of remote)
|
|
2305
|
+
console.log(` git -C ${repoDir} push origin --delete ${branch}`);
|
|
2306
|
+
if (local.length > 0) console.log(` git -C ${repoDir} branch -D ${local.join(" ")}`);
|
|
2172
2307
|
}
|
|
2173
2308
|
}
|
|
2174
2309
|
process.exit(failed.length > 0 ? 1 : 0);
|
|
2175
2310
|
}
|
|
2176
|
-
function prUrl(cwd, branch) {
|
|
2177
|
-
const view = spawnSync3("gh", ["pr", "view", branch, "--json", "url", "--jq", ".url"], {
|
|
2178
|
-
cwd,
|
|
2179
|
-
encoding: "utf8"
|
|
2180
|
-
});
|
|
2181
|
-
return view.status === 0 ? view.stdout.trim() || null : null;
|
|
2182
|
-
}
|
|
2183
2311
|
function currentBranch(cwd) {
|
|
2184
2312
|
return git(cwd, "rev-parse", "--abbrev-ref", "HEAD").stdout.trim();
|
|
2185
2313
|
}
|
|
2186
|
-
function freeBranch(cwd, name) {
|
|
2187
|
-
const taken = (candidate2) => git(cwd, "rev-parse", "--verify", "--quiet", `refs/heads/${candidate2}`).status === 0;
|
|
2188
|
-
let candidate = name;
|
|
2189
|
-
for (let n = 2; taken(candidate); n++) candidate = `${name}-${n}`;
|
|
2190
|
-
return candidate;
|
|
2191
|
-
}
|
|
2192
2314
|
function servedByRegistry(name, version) {
|
|
2193
2315
|
const deadline = Date.now() + 15 * 6e4;
|
|
2194
2316
|
while (true) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inixiative/config",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.202",
|
|
4
4
|
"description": "Shared toolchain and theme for the inixiative packages: tsconfig/biome/tsup presets, theme CSS, a version manifest (BOM), and a sync/check/train CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/versions.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"@types/bun"
|
|
14
14
|
],
|
|
15
15
|
"ecosystem": {
|
|
16
|
-
"@inixiative/config": "0.5.
|
|
16
|
+
"@inixiative/config": "0.5.202",
|
|
17
17
|
"@inixiative/json-rules": "3.16.0",
|
|
18
18
|
"@inixiative/permissions": "0.21.1",
|
|
19
19
|
"@inixiative/transitions": "0.7.2",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"candidates": {
|
|
54
54
|
"@inixiative/atlas": "0.5.0",
|
|
55
|
-
"@inixiative/json-rules": "3.
|
|
55
|
+
"@inixiative/json-rules": "3.21.0",
|
|
56
|
+
"@inixiative/signet": "0.13.0"
|
|
56
57
|
}
|
|
57
58
|
}
|