@inixiative/config 0.5.79 → 0.5.81
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 +17 -4
- package/dist/cli.js +363 -102
- package/package.json +1 -1
- package/versions.json +5 -2
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ One version of `@inixiative/config` names one coherent ecosystem state:
|
|
|
43
43
|
- `ecosystem` — the primitives lane's blessed, mutually-verified `@inixiative/*` set, plus this package
|
|
44
44
|
- `agentic` — the agentic lane's blessed set
|
|
45
45
|
- `consumers` — private repos that follow the blessed sets but never publish, keyed by GitHub `owner/repo`: `lanes` names the lanes each consumes, `upstream` the repo it syncs from
|
|
46
|
+
- `candidates` — published lane versions newer than their blessed one, waiting for every dependent to adopt them (see `train`); absent when there are none
|
|
46
47
|
|
|
47
48
|
```jsonc
|
|
48
49
|
"consumers": {
|
|
@@ -71,7 +72,7 @@ bunx @inixiative/config ports [project]
|
|
|
71
72
|
|
|
72
73
|
`ports` prints each project's local ports from `ports.json` (also exported as `@inixiative/config/ports.json`). A project's port for a service is the service's usual default plus 100 times the project's block, so no two projects collide and none sits on a default: Archive (block 7) serves HTTP on 4700 with Postgres on 6132; Kingdom (block 2) has web 3200, API 8200, Postgres 5632, Redis 6579. Reserved ports (macOS AirPlay on 5000 and 7000) are never assigned. Give a new project the next free block, and a new kind of service its usual default; the tests fail on any collision.
|
|
73
74
|
|
|
74
|
-
`check` is read-only and exits non-zero on drift — run it in CI after a frozen-lockfile install. It verifies toolchain pins — declared once, at the root, pinned exactly: a workspace member declaring one is an error, and `bun.lock` must resolve each only to its pin (no second copy) — toolchain peer ranges (they must admit the pin and no older major; `sync` writes `>=<pin major>.<pin minor>.0`), `"latest"` ranges, `packageManager`/`.bun-version`, legacy `bun.lockb`, a committed (tracked, un-ignored) `bun.lock`, required scripts (`check`/`typecheck`/`lint`/`test`), lefthook (dep + `lefthook.yml` extending the shared hooks + `prepare` script; git repos only), stub `extends`, presence of this package, and for every ecosystem dependency — in the root or any workspace member, of either lane — that the declared range admits the blessed version and the lockfile actually resolves to it (the stale-lockfile class). A range that does not admit the blessed version is rewritten to `^<blessed>`, except an exact pin, which stays exact and moves to the blessed version. An npm alias (`"@inixiative/session-archive": "npm:@inixiative/archive@^0.2.1"`) is held to its target's blessed version and rewritten as an alias; `workspace:`, `file:` and `link:` ranges are a repo linking itself and are left alone.
|
|
75
|
+
`check` is read-only and exits non-zero on drift — run it in CI after a frozen-lockfile install. It verifies toolchain pins — declared once, at the root, pinned exactly: a workspace member declaring one is an error, and `bun.lock` must resolve each only to its pin (no second copy) — toolchain peer ranges (they must admit the pin and no older major; `sync` writes `>=<pin major>.<pin minor>.0`), `"latest"` ranges, `packageManager`/`.bun-version`, legacy `bun.lockb`, a committed (tracked, un-ignored) `bun.lock`, required scripts (`check`/`typecheck`/`lint`/`test`), lefthook (dep + `lefthook.yml` extending the shared hooks + `prepare` script; git repos only), stub `extends`, presence of this package, and for every ecosystem dependency — in the root or any workspace member, of either lane — that the declared range admits the blessed version and the lockfile actually resolves to it (the stale-lockfile class). A package with a candidate also accepts the candidate and anything between: a range admitting either passes, and so does a lock from the blessed version up to the candidate. A range that does not admit the blessed version is rewritten to `^<blessed>`, except an exact pin, which stays exact and moves to the blessed version. An npm alias (`"@inixiative/session-archive": "npm:@inixiative/archive@^0.2.1"`) is held to its target's blessed version and rewritten as an alias; `workspace:`, `file:` and `link:` ranges are a repo linking itself and are left alone.
|
|
75
76
|
|
|
76
77
|
`sync` applies every fix (deleting member toolchain entries and their stale member links), re-locks via `bun install` (`--force` when a second toolchain copy survives), and runs `bun update` on stale ecosystem entries in the directory that declares them. It refuses to run on a dirty working tree without `--force`. The react preset is inferred from a `react` dependency; override with `--preset`. Missing scripts are filled with defaults; existing script bodies are never touched.
|
|
77
78
|
|
|
@@ -79,11 +80,23 @@ bunx @inixiative/config ports [project]
|
|
|
79
80
|
|
|
80
81
|
`train` is the release cascade, run from a git checkout of this repo; it refuses to train from a copy that isn't one (a bunx or npm cache). One train at a time per machine: it takes one machine-wide lock, `~/.inixiative/train.lock`, shared by every clone, worktree and lane, and refuses to start while another train holds it, naming the holder and the lock path. The lock is created atomically and records the holder's pid, hostname and process start time. A lock is stale, and taken over, when its process is gone, its pid now belongs to a different process, or the file can't be parsed. A lock from another host is never judged stale; if its train is truly gone, delete the file. The lock is released on exit and on SIGINT, SIGTERM or SIGHUP. `--dry-run` takes no lock. It walks every lane in order (primitives, then agentic), or only the lane `--lane` names. Origin is the truth: it refuses to run from a config checkout behind origin, and it fast-forwards clean checkouts to origin before working (dirty or diverged repos still skip). It only ships the default branch: a checkout on a feature branch is skipped whole — never re-locked, committed to, or published from.
|
|
81
82
|
|
|
82
|
-
Lanes stand alone. A lane with a package missing a checkout, or claimed by ambiguous checkouts, does not run; a lane whose check, commit or publish fails stops there and
|
|
83
|
+
Lanes stand alone. A lane with a package missing a checkout, or claimed by ambiguous checkouts, does not run; a lane whose own check (not one against candidates, below), commit or publish fails stops there: what it already published stays a candidate, and none of its candidates is blessed that run. Neither holds back the other lane. Within a lane it walks checkouts in dependency order; per checkout it: bumps ecosystem ranges to the blessed sets with their candidates in place → re-locks → runs the repo's `check` → commits only its own changes (`package.json` files + `bun.lock`) → publishes each lane package whose local version is ahead of npm, in dependency order, and waits until the registry serves it (up to 15 minutes, since npm holds new versions for processing) → records it as a candidate in the BOM.
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
Blessing is two-phase, so a dependent that needs work never deadlocks a release:
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
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.
|
|
88
|
+
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 (with the repo's root `.env*` files copied in): 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.
|
|
89
|
+
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.
|
|
90
|
+
|
|
91
|
+
The summary lists what was blessed, every candidate with what holds it, and each pending adoption branch with its PR when `gh` can find one.
|
|
92
|
+
|
|
93
|
+
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. 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.
|
|
94
|
+
|
|
95
|
+
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.
|
|
96
|
+
|
|
97
|
+
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.
|
|
98
|
+
|
|
99
|
+
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`.
|
|
87
100
|
|
|
88
101
|
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.
|
|
89
102
|
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5
5
|
import { existsSync as existsSync2, mkdtempSync, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
6
6
|
import { hostname as hostname2, tmpdir } from "os";
|
|
7
|
-
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
7
|
+
import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/args.ts
|
|
10
10
|
import { parseArgs } from "util";
|
|
@@ -44,6 +44,33 @@ var blessedVersions = (manifest2) => ({
|
|
|
44
44
|
...manifest2.ecosystem,
|
|
45
45
|
...manifest2.agentic
|
|
46
46
|
});
|
|
47
|
+
var laneOf = (manifest2, name) => LANES.find((lane2) => name in lanePackages(manifest2, lane2)) ?? null;
|
|
48
|
+
var adoptionTarget = (manifest2) => {
|
|
49
|
+
const candidates = manifest2.candidates ?? {};
|
|
50
|
+
const overlay = (section) => Object.fromEntries(
|
|
51
|
+
Object.entries(section).map(([name, version]) => [name, candidates[name] ?? version])
|
|
52
|
+
);
|
|
53
|
+
return {
|
|
54
|
+
...manifest2,
|
|
55
|
+
ecosystem: overlay(manifest2.ecosystem),
|
|
56
|
+
agentic: overlay(manifest2.agentic ?? {})
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
var promoteCandidates = (manifest2, names) => {
|
|
60
|
+
const promoted = [];
|
|
61
|
+
for (const name of names) {
|
|
62
|
+
const version = manifest2.candidates?.[name];
|
|
63
|
+
const lane2 = laneOf(manifest2, name);
|
|
64
|
+
if (!version || !lane2 || !manifest2.candidates) continue;
|
|
65
|
+
laneSection(manifest2, lane2)[name] = version;
|
|
66
|
+
delete manifest2.candidates[name];
|
|
67
|
+
promoted.push(`${name}@${version}`);
|
|
68
|
+
}
|
|
69
|
+
if (manifest2.candidates && Object.keys(manifest2.candidates).length === 0) {
|
|
70
|
+
delete manifest2.candidates;
|
|
71
|
+
}
|
|
72
|
+
return promoted;
|
|
73
|
+
};
|
|
47
74
|
var consumersFor = (manifest2, lanes2) => {
|
|
48
75
|
const consumers = manifest2.consumers ?? {};
|
|
49
76
|
const selected = Object.keys(consumers).filter((repo) => consumers[repo].lanes.some((lane2) => lanes2.includes(lane2))).sort();
|
|
@@ -296,6 +323,10 @@ var orderBy = (items, key, deps) => {
|
|
|
296
323
|
}
|
|
297
324
|
return order;
|
|
298
325
|
};
|
|
326
|
+
var declaredDependencies = (dir2) => {
|
|
327
|
+
const pkg = readJson(join(dir2, "package.json"));
|
|
328
|
+
return pkg ? dependencyNames(dir2, ["package.json", ...workspacePackagePaths(dir2, pkg)]) : /* @__PURE__ */ new Set();
|
|
329
|
+
};
|
|
299
330
|
var publishOrder = (checkout) => {
|
|
300
331
|
const names = new Set(checkout.packages.map((entry) => entry.name));
|
|
301
332
|
const deps = new Map(
|
|
@@ -681,6 +712,7 @@ function inspect(dir2, manifest2, presetOverride) {
|
|
|
681
712
|
}
|
|
682
713
|
}
|
|
683
714
|
const blessedSet = blessedVersions(manifest2);
|
|
715
|
+
const candidateSet = manifest2.candidates ?? {};
|
|
684
716
|
const staleKeys = /* @__PURE__ */ new Set();
|
|
685
717
|
for (const target of rangeTargets) {
|
|
686
718
|
for (const field of DEP_FIELDS) {
|
|
@@ -693,14 +725,19 @@ function inspect(dir2, manifest2, presetOverride) {
|
|
|
693
725
|
const blessed = blessedSet[name];
|
|
694
726
|
if (!blessed || name === target.pkg.name || !deps) continue;
|
|
695
727
|
const rewrite = (next) => name === key ? next : `npm:${name}@${next}`;
|
|
696
|
-
const
|
|
728
|
+
const candidate = candidateSet[name];
|
|
729
|
+
const okBlessed = admits(range, blessed);
|
|
730
|
+
const okCandidate = candidate ? admits(range, candidate) : false;
|
|
731
|
+
const ok = okBlessed === true || okCandidate === true ? true : okBlessed;
|
|
732
|
+
const accepted = candidate === blessed ? `candidate ${candidate}` : `blessed ${blessed}${candidate ? ` or candidate ${candidate}` : ""}`;
|
|
697
733
|
if (ok === false) {
|
|
698
734
|
const next = /^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(range) ? blessed : `^${blessed}`;
|
|
699
735
|
findings.push({
|
|
700
736
|
level: "error",
|
|
701
737
|
kind: "ecosystem-range",
|
|
702
738
|
name,
|
|
703
|
-
|
|
739
|
+
target: name,
|
|
740
|
+
message: `${target.label}${key} ${field} range ${spec} does not admit ${accepted} \u2192 ${rewrite(next)}`,
|
|
704
741
|
fix: () => {
|
|
705
742
|
deps[key] = rewrite(next);
|
|
706
743
|
target.mark();
|
|
@@ -710,28 +747,31 @@ function inspect(dir2, manifest2, presetOverride) {
|
|
|
710
747
|
if (ok === null) {
|
|
711
748
|
findings.push({
|
|
712
749
|
level: "warn",
|
|
713
|
-
message: `${target.label}${key} range ${spec} not understood; verify manually against ${
|
|
750
|
+
message: `${target.label}${key} range ${spec} not understood; verify manually against ${accepted}`
|
|
714
751
|
});
|
|
715
752
|
}
|
|
716
753
|
if (!lockText || field === "peerDependencies" || staleKeys.has(key)) continue;
|
|
717
754
|
const locked = lockedVersion(lockText, key, name);
|
|
755
|
+
const lockAccepted = locked === blessed || !!locked && locked === candidate || !!locked && !!candidate && compare(locked, blessed) >= 0 && compare(locked, candidate) <= 0;
|
|
718
756
|
if (locked && admits(range, locked) === false) {
|
|
719
757
|
staleKeys.add(key);
|
|
720
758
|
findings.push({
|
|
721
759
|
level: "error",
|
|
722
760
|
kind: "stale-lock",
|
|
723
761
|
name: key,
|
|
762
|
+
target: name,
|
|
724
763
|
path: target.path,
|
|
725
764
|
message: `${target.label}${key} locked at ${locked}, violating declared ${spec} (stale lockfile) \u2192 re-lock`
|
|
726
765
|
});
|
|
727
|
-
} else if (locked &&
|
|
766
|
+
} else if (locked && !lockAccepted) {
|
|
728
767
|
staleKeys.add(key);
|
|
729
768
|
findings.push({
|
|
730
769
|
level: "error",
|
|
731
770
|
kind: "stale-lock",
|
|
732
771
|
name: key,
|
|
772
|
+
target: name,
|
|
733
773
|
path: target.path,
|
|
734
|
-
message: `${target.label}${key} locked at ${locked}, blessed is
|
|
774
|
+
message: `${target.label}${key} locked at ${locked}, ${accepted.replace(/^(blessed|candidate) /, "$1 is ")} \u2192 re-lock`
|
|
735
775
|
});
|
|
736
776
|
}
|
|
737
777
|
}
|
|
@@ -1119,6 +1159,9 @@ if (command === "scan") {
|
|
|
1119
1159
|
const missing = [...plan.lanes.flatMap((lane2) => lane2.missing), ...plan.missingConsumers];
|
|
1120
1160
|
const ambiguous = [...plan.lanes.flatMap((lane2) => lane2.ambiguous), ...plan.ambiguousConsumers];
|
|
1121
1161
|
const drifted = [];
|
|
1162
|
+
for (const [name, version] of Object.entries(manifest.candidates ?? {})) {
|
|
1163
|
+
console.log(`candidate: ${name}@${version} (blessed ${blessedVersions(manifest)[name]})`);
|
|
1164
|
+
}
|
|
1122
1165
|
for (const checkout of checkouts) {
|
|
1123
1166
|
console.log(`
|
|
1124
1167
|
${checkout.name} \u2014 ${checkout.dir} (${laneLabel(checkout)})`);
|
|
@@ -1152,6 +1195,8 @@ scanned ${checkouts.length} repos (${lanes.join(" + ")}): ${drifted.length === 0
|
|
|
1152
1195
|
}
|
|
1153
1196
|
var Abort = class extends Error {
|
|
1154
1197
|
};
|
|
1198
|
+
var CheckFailed = class extends Abort {
|
|
1199
|
+
};
|
|
1155
1200
|
if (command === "train") {
|
|
1156
1201
|
const effect = (description, planned, run) => {
|
|
1157
1202
|
if (!dryRun) return run();
|
|
@@ -1179,9 +1224,9 @@ if (command === "train") {
|
|
|
1179
1224
|
checkout: packageRoot
|
|
1180
1225
|
});
|
|
1181
1226
|
if (!lock.ok) {
|
|
1182
|
-
const { pid, hostname: host, startedAt, lanes:
|
|
1227
|
+
const { pid, hostname: host, startedAt, lanes: held2, root, checkout } = lock.heldBy;
|
|
1183
1228
|
console.error(
|
|
1184
|
-
`\u2717 another train is running (pid ${pid} on ${host}, lanes ${
|
|
1229
|
+
`\u2717 another train is running (pid ${pid} on ${host}, lanes ${held2.join(" + ")}, root ${root}, from ${checkout}, since ${startedAt}) \u2014 wait for it to finish; one train at a time. If that train is truly gone, delete ${lock.path}`
|
|
1185
1230
|
);
|
|
1186
1231
|
process.exit(1);
|
|
1187
1232
|
}
|
|
@@ -1212,12 +1257,21 @@ if (command === "train") {
|
|
|
1212
1257
|
console.error(`\u2717 no ecosystem repos found under ${dir}`);
|
|
1213
1258
|
process.exit(1);
|
|
1214
1259
|
}
|
|
1215
|
-
const
|
|
1260
|
+
const recorded = [];
|
|
1216
1261
|
const skipped = [];
|
|
1217
1262
|
const failed = [];
|
|
1263
|
+
const failedLanes = /* @__PURE__ */ new Set();
|
|
1218
1264
|
const committed = /* @__PURE__ */ new Set();
|
|
1219
1265
|
const reviewBranches = [];
|
|
1220
|
-
|
|
1266
|
+
const waiting = [];
|
|
1267
|
+
const walked = /* @__PURE__ */ new Set();
|
|
1268
|
+
const worktrees = [];
|
|
1269
|
+
process.on("exit", () => {
|
|
1270
|
+
for (const { repo, path } of worktrees.splice(0)) {
|
|
1271
|
+
git(repo, "worktree", "remove", "--force", path);
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
const target = () => adoptionTarget(manifest);
|
|
1221
1275
|
const ready = (checkout) => {
|
|
1222
1276
|
const porcelain = git(checkout.dir, "status", "--porcelain");
|
|
1223
1277
|
if (porcelain.status !== 0) {
|
|
@@ -1263,14 +1317,18 @@ if (command === "train") {
|
|
|
1263
1317
|
return true;
|
|
1264
1318
|
};
|
|
1265
1319
|
const bumpRanges = (checkout) => {
|
|
1266
|
-
const inspection = inspect(checkout.dir,
|
|
1320
|
+
const inspection = inspect(checkout.dir, target());
|
|
1267
1321
|
const bumps = inspection.findings.filter((finding) => finding.kind === "ecosystem-range");
|
|
1268
1322
|
for (const bump of bumps) console.log(` ${bump.message}`);
|
|
1269
1323
|
if (bumps.length > 0) {
|
|
1270
|
-
effect(
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1324
|
+
effect(
|
|
1325
|
+
`rewrite ${bumps.length} ecosystem range(s) to the blessed set and its candidates`,
|
|
1326
|
+
void 0,
|
|
1327
|
+
() => {
|
|
1328
|
+
for (const bump of bumps) bump.fix?.();
|
|
1329
|
+
inspection.flush();
|
|
1330
|
+
}
|
|
1331
|
+
);
|
|
1274
1332
|
}
|
|
1275
1333
|
return {
|
|
1276
1334
|
inspection,
|
|
@@ -1284,32 +1342,170 @@ if (command === "train") {
|
|
|
1284
1342
|
}
|
|
1285
1343
|
for (const { cwd, names } of staleUpdates(
|
|
1286
1344
|
checkout.dir,
|
|
1287
|
-
inspect(checkout.dir,
|
|
1345
|
+
inspect(checkout.dir, target()).findings
|
|
1288
1346
|
)) {
|
|
1289
1347
|
spawnSync2("bun", ["update", ...names], { cwd, stdio: "inherit" });
|
|
1290
1348
|
}
|
|
1291
1349
|
if (check && spawnSync2("bun", ["run", "check"], { cwd: checkout.dir, stdio: "inherit" }).status !== 0) {
|
|
1292
|
-
throw new
|
|
1350
|
+
throw new CheckFailed(`check failed in ${checkout.name}`);
|
|
1293
1351
|
}
|
|
1294
1352
|
});
|
|
1295
1353
|
const followers = [];
|
|
1296
|
-
const
|
|
1354
|
+
const restore = (checkout) => effect(`restore ${checkout.name} to HEAD`, void 0, () => {
|
|
1355
|
+
if (git(checkout.dir, "status", "--porcelain").stdout.trim() === "") return;
|
|
1356
|
+
git(checkout.dir, "reset", "--quiet", "--hard", "HEAD");
|
|
1357
|
+
git(checkout.dir, "clean", "--quiet", "-fd");
|
|
1358
|
+
spawnSync2("bun", ["install", "--frozen-lockfile"], { cwd: checkout.dir, stdio: "inherit" });
|
|
1359
|
+
});
|
|
1360
|
+
const movesOf = (checkout) => {
|
|
1361
|
+
const candidates2 = manifest.candidates ?? {};
|
|
1362
|
+
const names = inspect(checkout.dir, target()).findings.flatMap(
|
|
1363
|
+
(finding) => (finding.kind === "ecosystem-range" || finding.kind === "stale-lock") && finding.target && finding.target in candidates2 ? [finding.target] : []
|
|
1364
|
+
);
|
|
1365
|
+
return [...new Set(names)].sort();
|
|
1366
|
+
};
|
|
1367
|
+
const adoptionBranch = (moving) => `train/adopt-${moving.map((name) => `${name.replace(/^@inixiative\//, "")}-${manifest.candidates?.[name]}`).join("-")}`;
|
|
1368
|
+
const ahead = (cwd, ref) => {
|
|
1369
|
+
if (git(cwd, "rev-parse", "--verify", "--quiet", ref).status !== 0) return null;
|
|
1370
|
+
return Number(git(cwd, "rev-list", "--count", `HEAD..${ref}`).stdout.trim());
|
|
1371
|
+
};
|
|
1372
|
+
const pendingAdoption = (checkout, moving) => {
|
|
1373
|
+
if (moving.length === 0) return false;
|
|
1374
|
+
const branch = adoptionBranch(moving);
|
|
1375
|
+
const remote = ahead(checkout.dir, `refs/remotes/origin/${branch}`);
|
|
1376
|
+
const local = ahead(checkout.dir, `refs/heads/${branch}`);
|
|
1377
|
+
if (!remote && !local) return false;
|
|
1378
|
+
const state = remote ? "unmerged" : "unpushed";
|
|
1379
|
+
console.log(
|
|
1380
|
+
`\u23F3 adopting ${moving.join(", ")} on ${branch} \u2014 ${state === "unmerged" ? "waiting for it to merge" : "never pushed: push it and open its PR"}`
|
|
1381
|
+
);
|
|
1382
|
+
waiting.push({ name: checkout.name, dir: checkout.dir, moving, state, branch });
|
|
1383
|
+
walked.add(checkout.dir);
|
|
1384
|
+
return true;
|
|
1385
|
+
};
|
|
1386
|
+
const worktree = (checkout, branch) => {
|
|
1387
|
+
const path = join2(mkdtempSync(join2(tmpdir(), "inixiative-train-")), basename(checkout.dir));
|
|
1388
|
+
if (git(checkout.dir, "worktree", "add", "--quiet", "-b", branch, path, "HEAD").status !== 0) {
|
|
1389
|
+
throw new Abort(`could not create ${branch} in ${checkout.name}`);
|
|
1390
|
+
}
|
|
1391
|
+
worktrees.push({ repo: checkout.dir, path });
|
|
1392
|
+
for (const name of readdirSync2(checkout.dir)) {
|
|
1393
|
+
if (/^\.env(\..+)?$/.test(name) && !existsSync2(join2(path, name))) {
|
|
1394
|
+
writeFileSync2(join2(path, name), readFileSync2(join2(checkout.dir, name)));
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return { ...checkout, dir: path };
|
|
1398
|
+
};
|
|
1399
|
+
const dropBranch = (checkout, branch) => {
|
|
1400
|
+
const entry = worktrees.find(
|
|
1401
|
+
(tree) => tree.repo === checkout.dir && currentBranch(tree.path) === branch
|
|
1402
|
+
);
|
|
1403
|
+
if (entry) {
|
|
1404
|
+
git(checkout.dir, "worktree", "remove", "--force", entry.path);
|
|
1405
|
+
worktrees.splice(worktrees.indexOf(entry), 1);
|
|
1406
|
+
}
|
|
1407
|
+
git(checkout.dir, "branch", "-D", branch);
|
|
1408
|
+
};
|
|
1409
|
+
const defaultBranch = (cwd) => offDefaultBranch(cwd)?.main ?? currentBranch(cwd);
|
|
1410
|
+
const reviewBranch = (checkout, packagePaths, moving) => {
|
|
1411
|
+
const branch = moving.length > 0 ? adoptionBranch(moving) : freeBranch(checkout.dir, `train/ecosystem-sync-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
|
|
1412
|
+
const base = defaultBranch(checkout.dir);
|
|
1413
|
+
const opened = effect(
|
|
1414
|
+
`create review branch ${branch} in ${checkout.name} (in a worktree; the checkout stays on ${base})`,
|
|
1415
|
+
true,
|
|
1416
|
+
() => {
|
|
1417
|
+
const branchCheckout = worktree(checkout, branch);
|
|
1418
|
+
for (const path of [...packagePaths, "bun.lock"]) {
|
|
1419
|
+
if (existsSync2(join2(checkout.dir, path))) {
|
|
1420
|
+
writeFileSync2(join2(branchCheckout.dir, path), readFileSync2(join2(checkout.dir, path)));
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
restore(checkout);
|
|
1424
|
+
try {
|
|
1425
|
+
relockAndCheck(branchCheckout, false);
|
|
1426
|
+
const message = moving.length > 0 ? `chore: adopt ${moving.map((name) => `${name}@${manifest.candidates?.[name]}`).join(", ")}` : void 0;
|
|
1427
|
+
if (!commitOwned(branchCheckout, packagePaths, true, message)) {
|
|
1428
|
+
dropBranch(checkout, branch);
|
|
1429
|
+
return false;
|
|
1430
|
+
}
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
dropBranch(checkout, branch);
|
|
1433
|
+
throw error;
|
|
1434
|
+
}
|
|
1435
|
+
followers.push({ checkout: branchCheckout, onBranch: true });
|
|
1436
|
+
console.log(`committed on ${branch} for review; ${checkout.name} stays on ${base}`);
|
|
1437
|
+
return true;
|
|
1438
|
+
}
|
|
1439
|
+
);
|
|
1440
|
+
if (!opened) return;
|
|
1441
|
+
reviewBranches.push({ dir: checkout.dir, branch, base });
|
|
1442
|
+
if (moving.length > 0) {
|
|
1443
|
+
waiting.push({ name: checkout.name, dir: checkout.dir, moving, state: "opened", branch });
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
const adopt = (checkout, moving) => {
|
|
1447
|
+
const branch = adoptionBranch(moving);
|
|
1448
|
+
const base = defaultBranch(checkout.dir);
|
|
1449
|
+
const label = moving.map((name) => `${name}@${manifest.candidates?.[name]}`).join(", ");
|
|
1450
|
+
const script = checkout.pkg.scripts?.["train:adopt"];
|
|
1451
|
+
try {
|
|
1452
|
+
effect(
|
|
1453
|
+
`open ${branch} in ${checkout.name}: re-lock onto ${label}${script ? ", run train:adopt" : ""}, commit`,
|
|
1454
|
+
void 0,
|
|
1455
|
+
() => {
|
|
1456
|
+
const branchCheckout = worktree(checkout, branch);
|
|
1457
|
+
try {
|
|
1458
|
+
bumpRanges(branchCheckout);
|
|
1459
|
+
relockAndCheck(branchCheckout, false);
|
|
1460
|
+
if (script && spawnSync2("bun", ["run", "train:adopt"], {
|
|
1461
|
+
cwd: branchCheckout.dir,
|
|
1462
|
+
stdio: "inherit"
|
|
1463
|
+
}).status !== 0) {
|
|
1464
|
+
console.log(
|
|
1465
|
+
`\u26A0 train:adopt failed in ${checkout.name} \u2014 the PR needs work before it merges`
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
git(branchCheckout.dir, "add", "-A");
|
|
1469
|
+
const commit = spawnSync2("git", ["commit", "-m", `chore: adopt ${label}`], {
|
|
1470
|
+
cwd: branchCheckout.dir,
|
|
1471
|
+
stdio: "inherit"
|
|
1472
|
+
});
|
|
1473
|
+
if (commit.status !== 0) {
|
|
1474
|
+
throw new Abort(
|
|
1475
|
+
`commit failed on ${branch} in ${checkout.name} (its hooks reject the adoption)`
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
} catch (error) {
|
|
1479
|
+
dropBranch(checkout, branch);
|
|
1480
|
+
throw error;
|
|
1481
|
+
}
|
|
1482
|
+
followers.push({ checkout: branchCheckout, onBranch: true });
|
|
1483
|
+
}
|
|
1484
|
+
);
|
|
1485
|
+
reviewBranches.push({ dir: checkout.dir, branch, base });
|
|
1486
|
+
waiting.push({ name: checkout.name, dir: checkout.dir, moving, state: "opened", branch });
|
|
1487
|
+
} catch (error) {
|
|
1488
|
+
if (!(error instanceof Abort)) throw error;
|
|
1489
|
+
failed.push(checkout.name);
|
|
1490
|
+
waiting.push({ name: checkout.name, dir: checkout.dir, moving, state: "failed" });
|
|
1491
|
+
console.error(`\u2717 ${error.message} \u2014 adopt ${label} in ${checkout.name} by hand`);
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
const commitOwned = (checkout, packagePaths, changes, message = "chore: sync ecosystem deps to blessed set") => {
|
|
1297
1495
|
if (dryRun && !changes) return false;
|
|
1298
1496
|
return effect(`commit its package.json files + bun.lock in ${checkout.name}`, true, () => {
|
|
1299
|
-
const tracked = [...packagePaths, "bun.lock"]
|
|
1497
|
+
const tracked = [...packagePaths, "bun.lock"].filter(
|
|
1498
|
+
(path) => existsSync2(join2(checkout.dir, path))
|
|
1499
|
+
);
|
|
1300
1500
|
const mutated = git(checkout.dir, "status", "--porcelain", "--", ...tracked);
|
|
1301
1501
|
if (mutated.status !== 0 || mutated.stdout.trim().length === 0) return false;
|
|
1302
1502
|
if (git(checkout.dir, "add", "--", ...tracked).status !== 0) {
|
|
1303
1503
|
throw new Abort(`git add failed in ${checkout.name} \u2014 is bun.lock ignored?`);
|
|
1304
1504
|
}
|
|
1305
|
-
const commit = spawnSync2(
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
cwd: checkout.dir,
|
|
1310
|
-
stdio: "inherit"
|
|
1311
|
-
}
|
|
1312
|
-
);
|
|
1505
|
+
const commit = spawnSync2("git", ["commit", "-m", message], {
|
|
1506
|
+
cwd: checkout.dir,
|
|
1507
|
+
stdio: "inherit"
|
|
1508
|
+
});
|
|
1313
1509
|
if (commit.status !== 0) throw new Abort(`commit failed in ${checkout.name}`);
|
|
1314
1510
|
return true;
|
|
1315
1511
|
});
|
|
@@ -1350,8 +1546,7 @@ if (command === "train") {
|
|
|
1350
1546
|
\u2550 ${lane2} lane: ${checkouts.map((checkout) => checkout.name).join(" \u2192 ") || "(none)"}`
|
|
1351
1547
|
);
|
|
1352
1548
|
const section = laneSection(manifest, lane2);
|
|
1353
|
-
const
|
|
1354
|
-
const laneBlessed = [];
|
|
1549
|
+
const laneRecorded = [];
|
|
1355
1550
|
try {
|
|
1356
1551
|
if (missing.length > 0) {
|
|
1357
1552
|
throw new Abort(
|
|
@@ -1370,7 +1565,6 @@ if (command === "train") {
|
|
|
1370
1565
|
skipped.push(checkout.name);
|
|
1371
1566
|
continue;
|
|
1372
1567
|
}
|
|
1373
|
-
const { inspection, bumped, staleLock } = bumpRanges(checkout);
|
|
1374
1568
|
const releases = checkout.packages.map((entry) => {
|
|
1375
1569
|
const local = JSON.parse(readFileSync2(join2(checkout.dir, entry.path), "utf8")).version ?? "0.0.0";
|
|
1376
1570
|
const remote = npmLatest(entry.name);
|
|
@@ -1386,47 +1580,50 @@ if (command === "train") {
|
|
|
1386
1580
|
skipped.push(checkout.name);
|
|
1387
1581
|
continue;
|
|
1388
1582
|
}
|
|
1583
|
+
const moving = movesOf(checkout);
|
|
1584
|
+
if (pendingAdoption(checkout, moving)) continue;
|
|
1585
|
+
const { inspection, bumped, staleLock } = bumpRanges(checkout);
|
|
1389
1586
|
const relocked = bumped || staleLock || releases.some((release) => release.ahead);
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
if (
|
|
1587
|
+
try {
|
|
1588
|
+
if (relocked) relockAndCheck(checkout);
|
|
1589
|
+
if (commitOwned(checkout, inspection.packagePaths, relocked)) committed.add(checkout.dir);
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
if (!(error instanceof Abort)) throw error;
|
|
1592
|
+
restore(checkout);
|
|
1593
|
+
if (!(error instanceof CheckFailed) || moving.length === 0) throw error;
|
|
1594
|
+
console.log(`\u2717 ${error.message} against the candidates \u2014 it adopts them on a branch`);
|
|
1595
|
+
walked.add(checkout.dir);
|
|
1596
|
+
adopt(checkout, moving);
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
walked.add(checkout.dir);
|
|
1600
|
+
followers.push({ checkout, onBranch: false });
|
|
1601
|
+
for (const { entry, local, remote, ahead: ahead2 } of releases) {
|
|
1602
|
+
if (ahead2) {
|
|
1395
1603
|
publish(checkout, entry, local, remote);
|
|
1396
1604
|
} else if (section[entry.name] === local) {
|
|
1397
1605
|
console.log(`\u2713 ${entry.name}@${local} already on npm and blessed`);
|
|
1398
1606
|
continue;
|
|
1607
|
+
} else if (manifest.candidates?.[entry.name] === local) {
|
|
1608
|
+
console.log(`\u2713 ${entry.name}@${local} already on npm and a candidate`);
|
|
1609
|
+
continue;
|
|
1399
1610
|
} else {
|
|
1400
|
-
console.log(`\u2713 ${entry.name}@${local} already on npm \u2014
|
|
1611
|
+
console.log(`\u2713 ${entry.name}@${local} already on npm \u2014 recording it as a candidate`);
|
|
1401
1612
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1613
|
+
manifest.candidates = { ...manifest.candidates, [entry.name]: local };
|
|
1614
|
+
laneRecorded.push(`${entry.name}@${local}`);
|
|
1404
1615
|
}
|
|
1405
1616
|
}
|
|
1406
|
-
if (laneBlessed.length > 0) {
|
|
1407
|
-
effect(
|
|
1408
|
-
`bless ${laneBlessed.join(", ")} in versions.json`,
|
|
1409
|
-
void 0,
|
|
1410
|
-
() => writeManifest(manifest)
|
|
1411
|
-
);
|
|
1412
|
-
bomDirty = true;
|
|
1413
|
-
blessed.push(...laneBlessed);
|
|
1414
|
-
}
|
|
1415
1617
|
console.log(
|
|
1416
|
-
`${lane2} lane
|
|
1618
|
+
`${lane2} lane candidates: ${laneRecorded.length === 0 ? "nothing new" : laneRecorded.join(", ")}`
|
|
1417
1619
|
);
|
|
1418
1620
|
} catch (error) {
|
|
1419
1621
|
if (!(error instanceof Abort)) throw error;
|
|
1420
|
-
|
|
1421
|
-
Object.assign(section, before);
|
|
1622
|
+
failedLanes.add(lane2);
|
|
1422
1623
|
failed.push(`${lane2} lane`);
|
|
1423
|
-
console.error(`\u2717 ${error.message} \u2014 ${lane2} lane stopped; its
|
|
1424
|
-
if (laneBlessed.length > 0) {
|
|
1425
|
-
console.error(
|
|
1426
|
-
` published but not blessed: ${laneBlessed.join(", ")} \u2014 the next train blesses them`
|
|
1427
|
-
);
|
|
1428
|
-
}
|
|
1624
|
+
console.error(`\u2717 ${error.message} \u2014 ${lane2} lane stopped; none of its candidates is blessed`);
|
|
1429
1625
|
}
|
|
1626
|
+
recorded.push(...laneRecorded);
|
|
1430
1627
|
}
|
|
1431
1628
|
if (plan.consumers.length > 0 || plan.missingConsumers.length > 0) {
|
|
1432
1629
|
console.log(
|
|
@@ -1449,39 +1646,97 @@ if (command === "train") {
|
|
|
1449
1646
|
skipped.push(checkout.name);
|
|
1450
1647
|
continue;
|
|
1451
1648
|
}
|
|
1649
|
+
const moving = movesOf(checkout);
|
|
1650
|
+
if (pendingAdoption(checkout, moving)) continue;
|
|
1651
|
+
const { inspection, bumped, staleLock } = bumpRanges(checkout);
|
|
1652
|
+
if (!bumped && !staleLock) {
|
|
1653
|
+
walked.add(checkout.dir);
|
|
1654
|
+
console.log("\u2713 already on the blessed set");
|
|
1655
|
+
continue;
|
|
1656
|
+
}
|
|
1452
1657
|
try {
|
|
1453
|
-
const { inspection, bumped, staleLock } = bumpRanges(checkout);
|
|
1454
|
-
if (!bumped && !staleLock) {
|
|
1455
|
-
console.log("\u2713 already on the blessed set");
|
|
1456
|
-
continue;
|
|
1457
|
-
}
|
|
1458
1658
|
relockAndCheck(checkout);
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
checkout.dir,
|
|
1462
|
-
`train/ecosystem-sync-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`
|
|
1463
|
-
);
|
|
1464
|
-
const switched = effect(
|
|
1465
|
-
`create review branch ${branch} in ${checkout.name}`,
|
|
1466
|
-
true,
|
|
1467
|
-
() => git(checkout.dir, "switch", "-c", branch).status === 0
|
|
1468
|
-
);
|
|
1469
|
-
if (!switched) throw new Abort(`could not create ${branch} in ${checkout.name}`);
|
|
1470
|
-
followers.push(checkout);
|
|
1471
|
-
if (commitOwned(checkout, inspection.packagePaths, true)) {
|
|
1472
|
-
reviewBranches.push({ dir: checkout.dir, branch, base });
|
|
1473
|
-
if (!dryRun) {
|
|
1474
|
-
console.log(`committed on ${branch} for review; the checkout stays on it until merged`);
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1659
|
+
reviewBranch(checkout, inspection.packagePaths, moving);
|
|
1660
|
+
walked.add(checkout.dir);
|
|
1477
1661
|
} catch (error) {
|
|
1478
1662
|
if (!(error instanceof Abort)) throw error;
|
|
1663
|
+
restore(checkout);
|
|
1664
|
+
if (error instanceof CheckFailed && moving.length > 0) {
|
|
1665
|
+
console.log(`\u2717 ${error.message} against the candidates \u2014 it adopts them on a branch`);
|
|
1666
|
+
walked.add(checkout.dir);
|
|
1667
|
+
adopt(checkout, moving);
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1479
1670
|
failed.push(checkout.name);
|
|
1480
|
-
console.error(`\u2717 ${error.message} \u2014 its
|
|
1671
|
+
console.error(`\u2717 ${error.message} \u2014 its checkout is back on its default branch`);
|
|
1481
1672
|
}
|
|
1482
1673
|
}
|
|
1674
|
+
const candidateNames = Object.keys(manifest.candidates ?? {});
|
|
1675
|
+
if (candidateNames.length > 0) {
|
|
1676
|
+
const everything = trainPlan(dir, manifest, LANES);
|
|
1677
|
+
for (const checkout of [
|
|
1678
|
+
...everything.lanes.flatMap((entry) => entry.checkouts),
|
|
1679
|
+
...everything.consumers
|
|
1680
|
+
]) {
|
|
1681
|
+
if (walked.has(checkout.dir)) continue;
|
|
1682
|
+
const declared = declaredDependencies(checkout.dir);
|
|
1683
|
+
const moving = candidateNames.filter((name) => declared.has(name));
|
|
1684
|
+
if (moving.length > 0) {
|
|
1685
|
+
waiting.push({ name: checkout.name, dir: checkout.dir, moving, state: "unverified" });
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
for (const repo of [
|
|
1689
|
+
...everything.missingConsumers,
|
|
1690
|
+
...everything.ambiguousConsumers.map((entry) => entry.key)
|
|
1691
|
+
]) {
|
|
1692
|
+
const consumed = manifest.consumers?.[repo]?.lanes ?? [];
|
|
1693
|
+
const moving = candidateNames.filter((name) => {
|
|
1694
|
+
const lane2 = laneOf(manifest, name);
|
|
1695
|
+
return !!lane2 && consumed.includes(lane2);
|
|
1696
|
+
});
|
|
1697
|
+
if (moving.length > 0) waiting.push({ name: repo, dir, moving, state: "unverified" });
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
const held = new Set(waiting.flatMap((entry) => entry.moving));
|
|
1701
|
+
const promotable = candidateNames.filter((name) => {
|
|
1702
|
+
const lane2 = laneOf(manifest, name);
|
|
1703
|
+
return !!lane2 && !failedLanes.has(lane2) && !held.has(name);
|
|
1704
|
+
});
|
|
1705
|
+
const promoted = promoteCandidates(manifest, promotable);
|
|
1706
|
+
const bomDirty = recorded.length > 0 || promoted.length > 0;
|
|
1707
|
+
if (bomDirty) {
|
|
1708
|
+
effect(
|
|
1709
|
+
"write the candidates and blessings to versions.json",
|
|
1710
|
+
void 0,
|
|
1711
|
+
() => writeManifest(manifest)
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
const why = {
|
|
1715
|
+
opened: "opened this run",
|
|
1716
|
+
unmerged: "waiting for its PR to merge",
|
|
1717
|
+
unpushed: "committed but never pushed",
|
|
1718
|
+
failed: "the train could not commit it \u2014 adopt by hand",
|
|
1719
|
+
unverified: "not verified this run"
|
|
1720
|
+
};
|
|
1483
1721
|
console.log(`
|
|
1484
|
-
blessed: ${
|
|
1722
|
+
blessed: ${promoted.length === 0 ? "nothing" : promoted.join(", ")}`);
|
|
1723
|
+
const candidates = Object.entries(manifest.candidates ?? {});
|
|
1724
|
+
if (candidates.length > 0) {
|
|
1725
|
+
console.log("candidates (check accepts them; not yet blessed):");
|
|
1726
|
+
for (const [name, version] of candidates) {
|
|
1727
|
+
const on = waiting.filter((entry) => entry.moving.includes(name)).map((entry) => `${entry.name} (${entry.branch ?? why[entry.state]})`);
|
|
1728
|
+
console.log(` ${name}@${version}${on.length === 0 ? "" : ` \u2014 waiting on ${on.join(", ")}`}`);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
if (waiting.length > 0) {
|
|
1732
|
+
console.log("pending (merge each adoption PR, then run the train again to bless):");
|
|
1733
|
+
for (const entry of waiting) {
|
|
1734
|
+
const url = entry.state === "unmerged" && !dryRun && entry.branch ? prUrl(entry.dir, entry.branch) : null;
|
|
1735
|
+
console.log(
|
|
1736
|
+
` ${entry.name}: ${entry.branch ? `${entry.branch} \u2014 ` : ""}${why[entry.state]}${url ? ` ${url}` : ""}`
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1485
1740
|
if (skipped.length > 0) console.log(`skipped: ${skipped.join(", ")}`);
|
|
1486
1741
|
if (failed.length > 0) console.log(`failed: ${failed.join(", ")}`);
|
|
1487
1742
|
const remoteVersion = npmLatest("@inixiative/config");
|
|
@@ -1520,26 +1775,25 @@ blessed: ${blessed.length === 0 ? "nothing" : blessed.join(", ")}`);
|
|
|
1520
1775
|
process.exit(1);
|
|
1521
1776
|
}
|
|
1522
1777
|
});
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
}
|
|
1778
|
+
const message = `chore: ${[
|
|
1779
|
+
promoted.length > 0 ? `bless ${promoted.join(", ")}` : "",
|
|
1780
|
+
recorded.length > 0 ? `candidate ${recorded.join(", ")}` : ""
|
|
1781
|
+
].filter(Boolean).join("; ") || "release"} \u2014 ${version}`;
|
|
1782
|
+
effect(`commit "${message}" in @inixiative/config`, void 0, () => {
|
|
1783
|
+
spawnSync2("git", ["add", "--", "package.json", "versions.json", "test/fixtures"], {
|
|
1784
|
+
cwd: packageRoot
|
|
1785
|
+
});
|
|
1786
|
+
if (spawnSync2("git", ["diff", "--cached", "--quiet"], { cwd: packageRoot }).status === 0)
|
|
1787
|
+
return;
|
|
1788
|
+
const commit = spawnSync2("git", ["commit", "-m", message], {
|
|
1789
|
+
cwd: packageRoot,
|
|
1790
|
+
stdio: "inherit"
|
|
1791
|
+
});
|
|
1792
|
+
if (commit.status !== 0) {
|
|
1793
|
+
console.error("\u2717 commit failed in @inixiative/config \u2014 aborting before publish");
|
|
1794
|
+
process.exit(1);
|
|
1541
1795
|
}
|
|
1542
|
-
);
|
|
1796
|
+
});
|
|
1543
1797
|
effect(`publish @inixiative/config@${version} to npm`, void 0, () => {
|
|
1544
1798
|
const publish2 = spawnSync2("npm", ["publish"], { cwd: packageRoot, stdio: "inherit" });
|
|
1545
1799
|
if (publish2.status !== 0 || !servedByRegistry("@inixiative/config", version)) {
|
|
@@ -1551,12 +1805,12 @@ blessed: ${blessed.length === 0 ? "nothing" : blessed.join(", ")}`);
|
|
|
1551
1805
|
committed.add(packageRoot);
|
|
1552
1806
|
if (followers.length > 0) console.log(`
|
|
1553
1807
|
\u2550 re-lock onto @inixiative/config@${version}`);
|
|
1554
|
-
for (const checkout of followers) {
|
|
1808
|
+
for (const { checkout, onBranch } of followers) {
|
|
1555
1809
|
try {
|
|
1556
1810
|
const { inspection, bumped, staleLock } = bumpRanges(checkout);
|
|
1557
1811
|
if (!bumped && !staleLock) continue;
|
|
1558
1812
|
relockAndCheck(checkout, false);
|
|
1559
|
-
if (commitOwned(checkout, inspection.packagePaths, true) && !
|
|
1813
|
+
if (commitOwned(checkout, inspection.packagePaths, true) && !onBranch)
|
|
1560
1814
|
committed.add(checkout.dir);
|
|
1561
1815
|
if (!dryRun) console.log(`\u2713 ${checkout.name} locked to @inixiative/config@${version}`);
|
|
1562
1816
|
} catch (error) {
|
|
@@ -1603,6 +1857,13 @@ dry run complete \u2014 a real train would commit in ${committed.size} repo(s) a
|
|
|
1603
1857
|
}
|
|
1604
1858
|
process.exit(failed.length > 0 ? 1 : 0);
|
|
1605
1859
|
}
|
|
1860
|
+
function prUrl(cwd, branch) {
|
|
1861
|
+
const view = spawnSync2("gh", ["pr", "view", branch, "--json", "url", "--jq", ".url"], {
|
|
1862
|
+
cwd,
|
|
1863
|
+
encoding: "utf8"
|
|
1864
|
+
});
|
|
1865
|
+
return view.status === 0 ? view.stdout.trim() || null : null;
|
|
1866
|
+
}
|
|
1606
1867
|
function currentBranch(cwd) {
|
|
1607
1868
|
return git(cwd, "rev-parse", "--abbrev-ref", "HEAD").stdout.trim();
|
|
1608
1869
|
}
|
package/package.json
CHANGED
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.81",
|
|
17
17
|
"@inixiative/json-rules": "3.5.4",
|
|
18
18
|
"@inixiative/permissions": "0.4.0",
|
|
19
19
|
"@inixiative/transitions": "0.5.0",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"agentic": {
|
|
26
26
|
"@inixiative/signet": "0.4.0",
|
|
27
|
-
"@inixiative/archive": "0.
|
|
27
|
+
"@inixiative/archive": "0.18.0",
|
|
28
28
|
"@inixiative/agent-session": "0.6.3",
|
|
29
29
|
"@inixiative/foundry-core": "0.5.1",
|
|
30
30
|
"@inixiative/foundry": "0.5.3",
|
|
@@ -48,5 +48,8 @@
|
|
|
48
48
|
"agentic"
|
|
49
49
|
]
|
|
50
50
|
}
|
|
51
|
+
},
|
|
52
|
+
"candidates": {
|
|
53
|
+
"@inixiative/archive": "0.19.0"
|
|
51
54
|
}
|
|
52
55
|
}
|