@inixiative/config 0.5.200 → 0.5.201

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 CHANGED
@@ -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`), all committed, and re-locked onto the new config release so the PR's CI judges it against the candidates. A lane checkout that adopts publishes nothing this run. A consumer that passes against candidates gets the same branch name: its default branch is still on the previous versions until the branch merges. 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, 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 PR closed without merging holds its candidates until its branch is deleted.
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`), all committed, and 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, 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 with its PR when `gh` can find one.
116
+ The summary lists what was blessed, every candidate with what holds it, 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 their changes go through review: the train commits them on a `train/ecosystem-sync-<date>` branch, in a worktree, and puts the checkout back on its default branch, clean.
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, consumer and 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 open a PR for each consumer and adoption branch; otherwise it prints the commands. `--dry-run` walks the same plan and prints each step it would take (range rewrites, re-locks and checks, commits, review branches, publishes, BOM blessings, pushes) without running any of them; it only fetches and reads git state and `npm view`.
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 PRs included. Downstream repos pick up toolchain changes via `sync`.
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
@@ -1547,7 +1547,7 @@ if (command === "train") {
1547
1547
  const failed = [];
1548
1548
  const failedLanes = /* @__PURE__ */ new Set();
1549
1549
  const committed = /* @__PURE__ */ new Set();
1550
- const reviewBranches = [];
1550
+ const adoptionBranches = [];
1551
1551
  const waiting = [];
1552
1552
  const walked = /* @__PURE__ */ new Set();
1553
1553
  const worktrees = [];
@@ -1557,7 +1557,10 @@ if (command === "train") {
1557
1557
  cwd: repo,
1558
1558
  stdio: "inherit"
1559
1559
  });
1560
- } else git(repo, "worktree", "remove", "--force", path);
1560
+ } else {
1561
+ git(repo, "worktree", "remove", "--force", path);
1562
+ rmSync3(dirname3(path), { recursive: true, force: true });
1563
+ }
1561
1564
  };
1562
1565
  process.on("exit", () => {
1563
1566
  for (const entry of worktrees.splice(0)) removeWorktree(entry);
@@ -1669,7 +1672,7 @@ if (command === "train") {
1669
1672
  if (!remote && !local) return false;
1670
1673
  const state = remote ? "unmerged" : "unpushed";
1671
1674
  console.log(
1672
- `\u23F3 adopting ${moving.join(", ")} on ${branch} \u2014 ${state === "unmerged" ? "waiting for it to merge" : "never pushed: push it and open its PR"}`
1675
+ `\u23F3 adopting ${moving.join(", ")} on ${branch} \u2014 ${state === "unmerged" ? "waiting for it to merge" : "never pushed: push it"}`
1673
1676
  );
1674
1677
  waiting.push({ name: checkout.name, dir: checkout.dir, moving, state, branch });
1675
1678
  walked.add(checkout.dir);
@@ -1713,46 +1716,8 @@ if (command === "train") {
1713
1716
  git(checkout.dir, "branch", "-D", branch);
1714
1717
  };
1715
1718
  const defaultBranch = (cwd) => offDefaultBranch(cwd)?.main ?? currentBranch(cwd);
1716
- const reviewBranch = (checkout, packagePaths, moving) => {
1717
- const branch = moving.length > 0 ? adoptionBranch(moving) : freeBranch(checkout.dir, `train/ecosystem-sync-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
1718
- const base = defaultBranch(checkout.dir);
1719
- const opened = effect(
1720
- `create review branch ${branch} in ${checkout.name} (in a worktree; the checkout stays on ${base})`,
1721
- true,
1722
- () => {
1723
- const branchCheckout = worktree(checkout, branch);
1724
- for (const path of [...packagePaths, "bun.lock"]) {
1725
- if (existsSync3(join3(checkout.dir, path))) {
1726
- writeFileSync3(join3(branchCheckout.dir, path), readFileSync3(join3(checkout.dir, path)));
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;
1741
- }
1742
- followers.push({ checkout: branchCheckout, onBranch: true });
1743
- console.log(`committed on ${branch} for review; ${checkout.name} stays on ${base}`);
1744
- return true;
1745
- }
1746
- );
1747
- if (!opened) return;
1748
- reviewBranches.push({ dir: checkout.dir, branch, base });
1749
- if (moving.length > 0) {
1750
- waiting.push({ name: checkout.name, dir: checkout.dir, moving, state: "opened", branch });
1751
- }
1752
- };
1753
1719
  const adopt = (checkout, moving) => {
1754
1720
  const branch = adoptionBranch(moving);
1755
- const base = defaultBranch(checkout.dir);
1756
1721
  const label = moving.map((name) => `${name}@${manifest.candidates?.[name]}`).join(", ");
1757
1722
  const script = checkout.pkg.scripts?.["train:adopt"];
1758
1723
  try {
@@ -1769,7 +1734,7 @@ if (command === "train") {
1769
1734
  stdio: "inherit"
1770
1735
  }).status !== 0) {
1771
1736
  console.log(
1772
- `\u26A0 train:adopt failed in ${checkout.name} \u2014 the PR needs work before it merges`
1737
+ `\u26A0 train:adopt failed in ${checkout.name} \u2014 the branch needs work before it merges`
1773
1738
  );
1774
1739
  }
1775
1740
  git(branchCheckout.dir, "add", "-A");
@@ -1789,7 +1754,7 @@ if (command === "train") {
1789
1754
  followers.push({ checkout: branchCheckout, onBranch: true });
1790
1755
  }
1791
1756
  );
1792
- reviewBranches.push({ dir: checkout.dir, branch, base });
1757
+ adoptionBranches.push({ dir: checkout.dir, branch });
1793
1758
  waiting.push({ name: checkout.name, dir: checkout.dir, moving, state: "opened", branch });
1794
1759
  } catch (error) {
1795
1760
  if (!(error instanceof Abort)) throw error;
@@ -1971,7 +1936,10 @@ if (command === "train") {
1971
1936
  }
1972
1937
  try {
1973
1938
  relockAndCheck(checkout);
1974
- reviewBranch(checkout, inspection.packagePaths, moving);
1939
+ const message = moving.length > 0 ? `chore: adopt ${moving.map((name) => `${name}@${manifest.candidates?.[name]}`).join(", ")}` : void 0;
1940
+ if (commitOwned(checkout, inspection.packagePaths, true, message))
1941
+ committed.add(checkout.dir);
1942
+ followers.push({ checkout, onBranch: false });
1975
1943
  walked.add(checkout.dir);
1976
1944
  } catch (error) {
1977
1945
  if (!(error instanceof Abort)) throw error;
@@ -2028,7 +1996,7 @@ if (command === "train") {
2028
1996
  }
2029
1997
  const why = {
2030
1998
  opened: "opened this run",
2031
- unmerged: "waiting for its PR to merge",
1999
+ unmerged: "waiting for its branch to merge",
2032
2000
  unpushed: "committed but never pushed",
2033
2001
  failed: "the train could not commit it \u2014 adopt by hand",
2034
2002
  unverified: "not verified this run"
@@ -2045,11 +2013,10 @@ blessed: ${promoted.length === 0 ? "nothing" : promoted.join(", ")}`);
2045
2013
  }
2046
2014
  for (const line of holdLines(manifest)) console.log(line);
2047
2015
  if (waiting.length > 0) {
2048
- console.log("pending (merge each adoption PR, then run the train again to bless):");
2016
+ console.log("pending (merge each adoption branch, then run the train again to bless):");
2049
2017
  for (const entry of waiting) {
2050
- const url = entry.state === "unmerged" && !dryRun && entry.branch ? prUrl(entry.dir, entry.branch) : null;
2051
2018
  console.log(
2052
- ` ${entry.name}: ${entry.branch ? `${entry.branch} \u2014 ` : ""}${why[entry.state]}${url ? ` ${url}` : ""}`
2019
+ ` ${entry.name}: ${entry.branch ? `${entry.branch} \u2014 ` : ""}${why[entry.state]}`
2053
2020
  );
2054
2021
  }
2055
2022
  }
@@ -2136,13 +2103,20 @@ blessed: ${promoted.length === 0 ? "nothing" : promoted.join(", ")}`);
2136
2103
  }
2137
2104
  }
2138
2105
  }
2106
+ const moved = [.../* @__PURE__ */ new Set([...committed, ...adoptionBranches.map((entry) => entry.dir)])];
2107
+ const keep = new Set(adoptionBranches.map((entry) => `${entry.dir}\0${entry.branch}`));
2108
+ const superseded = (repoDir) => {
2109
+ 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}`));
2110
+ return { local: refs("refs/heads/train/"), remote: refs("refs/remotes/origin/train/") };
2111
+ };
2139
2112
  if (dryRun) {
2140
2113
  console.log(
2141
2114
  `
2142
- dry run complete \u2014 a real train would commit in ${committed.size} repo(s) and open ${reviewBranches.length} review branch(es)${commandLine.push ? ", then push them" : ""}:`
2115
+ 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
2116
  );
2144
2117
  for (const repoDir of committed) console.log(` ${repoDir}`);
2145
- for (const { dir: repoDir, branch } of reviewBranches) console.log(` ${repoDir} (${branch})`);
2118
+ for (const { dir: repoDir, branch } of adoptionBranches)
2119
+ console.log(` ${repoDir} (${branch})`);
2146
2120
  } else if (commandLine.push) {
2147
2121
  for (const repoDir of committed) {
2148
2122
  if (spawnSync3("git", ["push"], { cwd: repoDir, stdio: "inherit" }).status !== 0) {
@@ -2150,45 +2124,47 @@ dry run complete \u2014 a real train would commit in ${committed.size} repo(s) a
2150
2124
  process.exit(1);
2151
2125
  }
2152
2126
  }
2153
- for (const { dir: repoDir, branch, base } of reviewBranches) {
2127
+ for (const { dir: repoDir, branch } of adoptionBranches) {
2154
2128
  const push = spawnSync3("git", ["push", "-u", "origin", branch], {
2155
2129
  cwd: repoDir,
2156
2130
  stdio: "inherit"
2157
2131
  });
2158
- const pr = push.status === 0 && spawnSync3("gh", ["pr", "create", "--fill", "--base", base, "--head", branch], {
2159
- cwd: repoDir,
2160
- stdio: "inherit"
2161
- }).status === 0;
2162
- if (!pr) console.error(`\u2717 could not push ${branch} and open its PR in ${repoDir}`);
2132
+ if (push.status !== 0) console.error(`\u2717 could not push ${branch} in ${repoDir}`);
2133
+ }
2134
+ for (const repoDir of moved) {
2135
+ git(repoDir, "fetch", "--quiet", "--prune", "origin");
2136
+ const { local, remote } = superseded(repoDir);
2137
+ const gone = /* @__PURE__ */ new Set();
2138
+ for (const branch of remote) {
2139
+ if (git(repoDir, "push", "--quiet", "origin", "--delete", branch).status === 0) {
2140
+ gone.add(branch);
2141
+ } else console.error(`\u2717 could not delete origin/${branch} in ${repoDir}`);
2142
+ }
2143
+ for (const branch of local) {
2144
+ if (git(repoDir, "branch", "-D", branch).status === 0) gone.add(branch);
2145
+ else console.error(`\u2717 could not delete ${branch} in ${repoDir}`);
2146
+ }
2147
+ if (gone.size > 0) console.log(`\u{1F9F9} ${repoDir}: deleted ${[...gone].join(", ")}`);
2163
2148
  }
2164
- console.log(`pushed: ${committed.size + reviewBranches.length} repos`);
2165
- } else if (committed.size > 0 || reviewBranches.length > 0) {
2149
+ console.log(`pushed: ${committed.size + adoptionBranches.length} repos`);
2150
+ } else if (committed.size > 0 || adoptionBranches.length > 0) {
2166
2151
  console.log("train does not push without --push \u2014 review its commits, then push:");
2167
2152
  for (const repoDir of committed) console.log(` git -C ${repoDir} push`);
2168
- for (const { dir: repoDir, branch, base } of reviewBranches) {
2169
- console.log(
2170
- ` git -C ${repoDir} push -u origin ${branch} && (cd ${repoDir} && gh pr create --fill --base ${base})`
2171
- );
2153
+ for (const { dir: repoDir, branch } of adoptionBranches) {
2154
+ console.log(` git -C ${repoDir} push -u origin ${branch}`);
2155
+ }
2156
+ for (const repoDir of moved) {
2157
+ const { local, remote } = superseded(repoDir);
2158
+ for (const branch of remote)
2159
+ console.log(` git -C ${repoDir} push origin --delete ${branch}`);
2160
+ if (local.length > 0) console.log(` git -C ${repoDir} branch -D ${local.join(" ")}`);
2172
2161
  }
2173
2162
  }
2174
2163
  process.exit(failed.length > 0 ? 1 : 0);
2175
2164
  }
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
2165
  function currentBranch(cwd) {
2184
2166
  return git(cwd, "rev-parse", "--abbrev-ref", "HEAD").stdout.trim();
2185
2167
  }
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
2168
  function servedByRegistry(name, version) {
2193
2169
  const deadline = Date.now() + 15 * 6e4;
2194
2170
  while (true) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inixiative/config",
3
- "version": "0.5.200",
3
+ "version": "0.5.201",
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.200",
16
+ "@inixiative/config": "0.5.201",
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,6 @@
52
52
  },
53
53
  "candidates": {
54
54
  "@inixiative/atlas": "0.5.0",
55
- "@inixiative/json-rules": "3.20.0"
55
+ "@inixiative/json-rules": "3.21.0"
56
56
  }
57
57
  }