@inixiative/config 0.5.80 → 0.5.82
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 +386 -104
- 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 ignored `.env*` files copied in, at any depth): 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
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
existsSync as existsSync2,
|
|
7
|
+
mkdirSync as mkdirSync2,
|
|
8
|
+
mkdtempSync,
|
|
9
|
+
readdirSync as readdirSync2,
|
|
10
|
+
readFileSync as readFileSync2,
|
|
11
|
+
rmSync as rmSync2,
|
|
12
|
+
writeFileSync as writeFileSync2
|
|
13
|
+
} from "fs";
|
|
6
14
|
import { hostname as hostname2, tmpdir } from "os";
|
|
7
|
-
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
15
|
+
import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
8
16
|
|
|
9
17
|
// src/args.ts
|
|
10
18
|
import { parseArgs } from "util";
|
|
@@ -44,6 +52,33 @@ var blessedVersions = (manifest2) => ({
|
|
|
44
52
|
...manifest2.ecosystem,
|
|
45
53
|
...manifest2.agentic
|
|
46
54
|
});
|
|
55
|
+
var laneOf = (manifest2, name) => LANES.find((lane2) => name in lanePackages(manifest2, lane2)) ?? null;
|
|
56
|
+
var adoptionTarget = (manifest2) => {
|
|
57
|
+
const candidates = manifest2.candidates ?? {};
|
|
58
|
+
const overlay = (section) => Object.fromEntries(
|
|
59
|
+
Object.entries(section).map(([name, version]) => [name, candidates[name] ?? version])
|
|
60
|
+
);
|
|
61
|
+
return {
|
|
62
|
+
...manifest2,
|
|
63
|
+
ecosystem: overlay(manifest2.ecosystem),
|
|
64
|
+
agentic: overlay(manifest2.agentic ?? {})
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
var promoteCandidates = (manifest2, names) => {
|
|
68
|
+
const promoted = [];
|
|
69
|
+
for (const name of names) {
|
|
70
|
+
const version = manifest2.candidates?.[name];
|
|
71
|
+
const lane2 = laneOf(manifest2, name);
|
|
72
|
+
if (!version || !lane2 || !manifest2.candidates) continue;
|
|
73
|
+
laneSection(manifest2, lane2)[name] = version;
|
|
74
|
+
delete manifest2.candidates[name];
|
|
75
|
+
promoted.push(`${name}@${version}`);
|
|
76
|
+
}
|
|
77
|
+
if (manifest2.candidates && Object.keys(manifest2.candidates).length === 0) {
|
|
78
|
+
delete manifest2.candidates;
|
|
79
|
+
}
|
|
80
|
+
return promoted;
|
|
81
|
+
};
|
|
47
82
|
var consumersFor = (manifest2, lanes2) => {
|
|
48
83
|
const consumers = manifest2.consumers ?? {};
|
|
49
84
|
const selected = Object.keys(consumers).filter((repo) => consumers[repo].lanes.some((lane2) => lanes2.includes(lane2))).sort();
|
|
@@ -296,6 +331,10 @@ var orderBy = (items, key, deps) => {
|
|
|
296
331
|
}
|
|
297
332
|
return order;
|
|
298
333
|
};
|
|
334
|
+
var declaredDependencies = (dir2) => {
|
|
335
|
+
const pkg = readJson(join(dir2, "package.json"));
|
|
336
|
+
return pkg ? dependencyNames(dir2, ["package.json", ...workspacePackagePaths(dir2, pkg)]) : /* @__PURE__ */ new Set();
|
|
337
|
+
};
|
|
299
338
|
var publishOrder = (checkout) => {
|
|
300
339
|
const names = new Set(checkout.packages.map((entry) => entry.name));
|
|
301
340
|
const deps = new Map(
|
|
@@ -681,6 +720,7 @@ function inspect(dir2, manifest2, presetOverride) {
|
|
|
681
720
|
}
|
|
682
721
|
}
|
|
683
722
|
const blessedSet = blessedVersions(manifest2);
|
|
723
|
+
const candidateSet = manifest2.candidates ?? {};
|
|
684
724
|
const staleKeys = /* @__PURE__ */ new Set();
|
|
685
725
|
for (const target of rangeTargets) {
|
|
686
726
|
for (const field of DEP_FIELDS) {
|
|
@@ -693,14 +733,19 @@ function inspect(dir2, manifest2, presetOverride) {
|
|
|
693
733
|
const blessed = blessedSet[name];
|
|
694
734
|
if (!blessed || name === target.pkg.name || !deps) continue;
|
|
695
735
|
const rewrite = (next) => name === key ? next : `npm:${name}@${next}`;
|
|
696
|
-
const
|
|
736
|
+
const candidate = candidateSet[name];
|
|
737
|
+
const okBlessed = admits(range, blessed);
|
|
738
|
+
const okCandidate = candidate ? admits(range, candidate) : false;
|
|
739
|
+
const ok = okBlessed === true || okCandidate === true ? true : okBlessed;
|
|
740
|
+
const accepted = candidate === blessed ? `candidate ${candidate}` : `blessed ${blessed}${candidate ? ` or candidate ${candidate}` : ""}`;
|
|
697
741
|
if (ok === false) {
|
|
698
742
|
const next = /^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(range) ? blessed : `^${blessed}`;
|
|
699
743
|
findings.push({
|
|
700
744
|
level: "error",
|
|
701
745
|
kind: "ecosystem-range",
|
|
702
746
|
name,
|
|
703
|
-
|
|
747
|
+
target: name,
|
|
748
|
+
message: `${target.label}${key} ${field} range ${spec} does not admit ${accepted} \u2192 ${rewrite(next)}`,
|
|
704
749
|
fix: () => {
|
|
705
750
|
deps[key] = rewrite(next);
|
|
706
751
|
target.mark();
|
|
@@ -710,28 +755,31 @@ function inspect(dir2, manifest2, presetOverride) {
|
|
|
710
755
|
if (ok === null) {
|
|
711
756
|
findings.push({
|
|
712
757
|
level: "warn",
|
|
713
|
-
message: `${target.label}${key} range ${spec} not understood; verify manually against ${
|
|
758
|
+
message: `${target.label}${key} range ${spec} not understood; verify manually against ${accepted}`
|
|
714
759
|
});
|
|
715
760
|
}
|
|
716
761
|
if (!lockText || field === "peerDependencies" || staleKeys.has(key)) continue;
|
|
717
762
|
const locked = lockedVersion(lockText, key, name);
|
|
763
|
+
const lockAccepted = locked === blessed || !!locked && locked === candidate || !!locked && !!candidate && compare(locked, blessed) >= 0 && compare(locked, candidate) <= 0;
|
|
718
764
|
if (locked && admits(range, locked) === false) {
|
|
719
765
|
staleKeys.add(key);
|
|
720
766
|
findings.push({
|
|
721
767
|
level: "error",
|
|
722
768
|
kind: "stale-lock",
|
|
723
769
|
name: key,
|
|
770
|
+
target: name,
|
|
724
771
|
path: target.path,
|
|
725
772
|
message: `${target.label}${key} locked at ${locked}, violating declared ${spec} (stale lockfile) \u2192 re-lock`
|
|
726
773
|
});
|
|
727
|
-
} else if (locked &&
|
|
774
|
+
} else if (locked && !lockAccepted) {
|
|
728
775
|
staleKeys.add(key);
|
|
729
776
|
findings.push({
|
|
730
777
|
level: "error",
|
|
731
778
|
kind: "stale-lock",
|
|
732
779
|
name: key,
|
|
780
|
+
target: name,
|
|
733
781
|
path: target.path,
|
|
734
|
-
message: `${target.label}${key} locked at ${locked}, blessed is
|
|
782
|
+
message: `${target.label}${key} locked at ${locked}, ${accepted.replace(/^(blessed|candidate) /, "$1 is ")} \u2192 re-lock`
|
|
735
783
|
});
|
|
736
784
|
}
|
|
737
785
|
}
|
|
@@ -1119,6 +1167,9 @@ if (command === "scan") {
|
|
|
1119
1167
|
const missing = [...plan.lanes.flatMap((lane2) => lane2.missing), ...plan.missingConsumers];
|
|
1120
1168
|
const ambiguous = [...plan.lanes.flatMap((lane2) => lane2.ambiguous), ...plan.ambiguousConsumers];
|
|
1121
1169
|
const drifted = [];
|
|
1170
|
+
for (const [name, version] of Object.entries(manifest.candidates ?? {})) {
|
|
1171
|
+
console.log(`candidate: ${name}@${version} (blessed ${blessedVersions(manifest)[name]})`);
|
|
1172
|
+
}
|
|
1122
1173
|
for (const checkout of checkouts) {
|
|
1123
1174
|
console.log(`
|
|
1124
1175
|
${checkout.name} \u2014 ${checkout.dir} (${laneLabel(checkout)})`);
|
|
@@ -1152,6 +1203,8 @@ scanned ${checkouts.length} repos (${lanes.join(" + ")}): ${drifted.length === 0
|
|
|
1152
1203
|
}
|
|
1153
1204
|
var Abort = class extends Error {
|
|
1154
1205
|
};
|
|
1206
|
+
var CheckFailed = class extends Abort {
|
|
1207
|
+
};
|
|
1155
1208
|
if (command === "train") {
|
|
1156
1209
|
const effect = (description, planned, run) => {
|
|
1157
1210
|
if (!dryRun) return run();
|
|
@@ -1179,9 +1232,9 @@ if (command === "train") {
|
|
|
1179
1232
|
checkout: packageRoot
|
|
1180
1233
|
});
|
|
1181
1234
|
if (!lock.ok) {
|
|
1182
|
-
const { pid, hostname: host, startedAt, lanes:
|
|
1235
|
+
const { pid, hostname: host, startedAt, lanes: held2, root, checkout } = lock.heldBy;
|
|
1183
1236
|
console.error(
|
|
1184
|
-
`\u2717 another train is running (pid ${pid} on ${host}, lanes ${
|
|
1237
|
+
`\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
1238
|
);
|
|
1186
1239
|
process.exit(1);
|
|
1187
1240
|
}
|
|
@@ -1212,12 +1265,21 @@ if (command === "train") {
|
|
|
1212
1265
|
console.error(`\u2717 no ecosystem repos found under ${dir}`);
|
|
1213
1266
|
process.exit(1);
|
|
1214
1267
|
}
|
|
1215
|
-
const
|
|
1268
|
+
const recorded = [];
|
|
1216
1269
|
const skipped = [];
|
|
1217
1270
|
const failed = [];
|
|
1271
|
+
const failedLanes = /* @__PURE__ */ new Set();
|
|
1218
1272
|
const committed = /* @__PURE__ */ new Set();
|
|
1219
1273
|
const reviewBranches = [];
|
|
1220
|
-
|
|
1274
|
+
const waiting = [];
|
|
1275
|
+
const walked = /* @__PURE__ */ new Set();
|
|
1276
|
+
const worktrees = [];
|
|
1277
|
+
process.on("exit", () => {
|
|
1278
|
+
for (const { repo, path } of worktrees.splice(0)) {
|
|
1279
|
+
git(repo, "worktree", "remove", "--force", path);
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
const target = () => adoptionTarget(manifest);
|
|
1221
1283
|
const ready = (checkout) => {
|
|
1222
1284
|
const porcelain = git(checkout.dir, "status", "--porcelain");
|
|
1223
1285
|
if (porcelain.status !== 0) {
|
|
@@ -1263,14 +1325,18 @@ if (command === "train") {
|
|
|
1263
1325
|
return true;
|
|
1264
1326
|
};
|
|
1265
1327
|
const bumpRanges = (checkout) => {
|
|
1266
|
-
const inspection = inspect(checkout.dir,
|
|
1328
|
+
const inspection = inspect(checkout.dir, target());
|
|
1267
1329
|
const bumps = inspection.findings.filter((finding) => finding.kind === "ecosystem-range");
|
|
1268
1330
|
for (const bump of bumps) console.log(` ${bump.message}`);
|
|
1269
1331
|
if (bumps.length > 0) {
|
|
1270
|
-
effect(
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1332
|
+
effect(
|
|
1333
|
+
`rewrite ${bumps.length} ecosystem range(s) to the blessed set and its candidates`,
|
|
1334
|
+
void 0,
|
|
1335
|
+
() => {
|
|
1336
|
+
for (const bump of bumps) bump.fix?.();
|
|
1337
|
+
inspection.flush();
|
|
1338
|
+
}
|
|
1339
|
+
);
|
|
1274
1340
|
}
|
|
1275
1341
|
return {
|
|
1276
1342
|
inspection,
|
|
@@ -1279,37 +1345,188 @@ if (command === "train") {
|
|
|
1279
1345
|
};
|
|
1280
1346
|
};
|
|
1281
1347
|
const relockAndCheck = (checkout, check = true) => effect(`re-lock ${checkout.name}${check ? " and run its check" : ""}`, void 0, () => {
|
|
1282
|
-
|
|
1348
|
+
const install = (...flags) => spawnSync2("bun", ["install", ...flags], { cwd: checkout.dir, stdio: "inherit" }).status === 0;
|
|
1349
|
+
if (!install() && !install("--no-cache")) {
|
|
1283
1350
|
throw new Abort(`bun install failed in ${checkout.name}`);
|
|
1284
1351
|
}
|
|
1285
1352
|
for (const { cwd, names } of staleUpdates(
|
|
1286
1353
|
checkout.dir,
|
|
1287
|
-
inspect(checkout.dir,
|
|
1354
|
+
inspect(checkout.dir, target()).findings
|
|
1288
1355
|
)) {
|
|
1289
1356
|
spawnSync2("bun", ["update", ...names], { cwd, stdio: "inherit" });
|
|
1290
1357
|
}
|
|
1291
1358
|
if (check && spawnSync2("bun", ["run", "check"], { cwd: checkout.dir, stdio: "inherit" }).status !== 0) {
|
|
1292
|
-
throw new
|
|
1359
|
+
throw new CheckFailed(`check failed in ${checkout.name}`);
|
|
1293
1360
|
}
|
|
1294
1361
|
});
|
|
1295
1362
|
const followers = [];
|
|
1296
|
-
const
|
|
1363
|
+
const restore = (checkout) => effect(`restore ${checkout.name} to HEAD`, void 0, () => {
|
|
1364
|
+
if (git(checkout.dir, "status", "--porcelain").stdout.trim() === "") return;
|
|
1365
|
+
git(checkout.dir, "reset", "--quiet", "--hard", "HEAD");
|
|
1366
|
+
git(checkout.dir, "clean", "--quiet", "-fd");
|
|
1367
|
+
spawnSync2("bun", ["install", "--frozen-lockfile"], { cwd: checkout.dir, stdio: "inherit" });
|
|
1368
|
+
});
|
|
1369
|
+
const movesOf = (checkout) => {
|
|
1370
|
+
const candidates2 = manifest.candidates ?? {};
|
|
1371
|
+
const names = inspect(checkout.dir, target()).findings.flatMap(
|
|
1372
|
+
(finding) => (finding.kind === "ecosystem-range" || finding.kind === "stale-lock") && finding.target && finding.target in candidates2 ? [finding.target] : []
|
|
1373
|
+
);
|
|
1374
|
+
return [...new Set(names)].sort();
|
|
1375
|
+
};
|
|
1376
|
+
const adoptionBranch = (moving) => `train/adopt-${moving.map((name) => `${name.replace(/^@inixiative\//, "")}-${manifest.candidates?.[name]}`).join("-")}`;
|
|
1377
|
+
const ahead = (cwd, ref) => {
|
|
1378
|
+
if (git(cwd, "rev-parse", "--verify", "--quiet", ref).status !== 0) return null;
|
|
1379
|
+
return Number(git(cwd, "rev-list", "--count", `HEAD..${ref}`).stdout.trim());
|
|
1380
|
+
};
|
|
1381
|
+
const pendingAdoption = (checkout, moving) => {
|
|
1382
|
+
if (moving.length === 0) return false;
|
|
1383
|
+
const branch = adoptionBranch(moving);
|
|
1384
|
+
const remote = ahead(checkout.dir, `refs/remotes/origin/${branch}`);
|
|
1385
|
+
const local = ahead(checkout.dir, `refs/heads/${branch}`);
|
|
1386
|
+
if (!remote && !local) return false;
|
|
1387
|
+
const state = remote ? "unmerged" : "unpushed";
|
|
1388
|
+
console.log(
|
|
1389
|
+
`\u23F3 adopting ${moving.join(", ")} on ${branch} \u2014 ${state === "unmerged" ? "waiting for it to merge" : "never pushed: push it and open its PR"}`
|
|
1390
|
+
);
|
|
1391
|
+
waiting.push({ name: checkout.name, dir: checkout.dir, moving, state, branch });
|
|
1392
|
+
walked.add(checkout.dir);
|
|
1393
|
+
return true;
|
|
1394
|
+
};
|
|
1395
|
+
const worktree = (checkout, branch) => {
|
|
1396
|
+
const path = join2(mkdtempSync(join2(tmpdir(), "inixiative-train-")), basename(checkout.dir));
|
|
1397
|
+
if (git(checkout.dir, "worktree", "add", "--quiet", "-b", branch, path, "HEAD").status !== 0) {
|
|
1398
|
+
throw new Abort(`could not create ${branch} in ${checkout.name}`);
|
|
1399
|
+
}
|
|
1400
|
+
worktrees.push({ repo: checkout.dir, path });
|
|
1401
|
+
const envFiles = git(
|
|
1402
|
+
checkout.dir,
|
|
1403
|
+
"ls-files",
|
|
1404
|
+
"--others",
|
|
1405
|
+
"--ignored",
|
|
1406
|
+
"--exclude-standard",
|
|
1407
|
+
"--",
|
|
1408
|
+
":(glob)**/.env*"
|
|
1409
|
+
).stdout.split("\n").filter(
|
|
1410
|
+
(file) => /(^|\/)\.env[^/]*$/.test(file) && !/(^|\/)(node_modules|\.worktrees)\//.test(file)
|
|
1411
|
+
);
|
|
1412
|
+
for (const file of envFiles) {
|
|
1413
|
+
if (existsSync2(join2(path, file))) continue;
|
|
1414
|
+
mkdirSync2(dirname2(join2(path, file)), { recursive: true });
|
|
1415
|
+
writeFileSync2(join2(path, file), readFileSync2(join2(checkout.dir, file)));
|
|
1416
|
+
}
|
|
1417
|
+
return { ...checkout, dir: path };
|
|
1418
|
+
};
|
|
1419
|
+
const dropBranch = (checkout, branch) => {
|
|
1420
|
+
const entry = worktrees.find(
|
|
1421
|
+
(tree) => tree.repo === checkout.dir && currentBranch(tree.path) === branch
|
|
1422
|
+
);
|
|
1423
|
+
if (entry) {
|
|
1424
|
+
git(checkout.dir, "worktree", "remove", "--force", entry.path);
|
|
1425
|
+
worktrees.splice(worktrees.indexOf(entry), 1);
|
|
1426
|
+
}
|
|
1427
|
+
git(checkout.dir, "branch", "-D", branch);
|
|
1428
|
+
};
|
|
1429
|
+
const defaultBranch = (cwd) => offDefaultBranch(cwd)?.main ?? currentBranch(cwd);
|
|
1430
|
+
const reviewBranch = (checkout, packagePaths, moving) => {
|
|
1431
|
+
const branch = moving.length > 0 ? adoptionBranch(moving) : freeBranch(checkout.dir, `train/ecosystem-sync-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
|
|
1432
|
+
const base = defaultBranch(checkout.dir);
|
|
1433
|
+
const opened = effect(
|
|
1434
|
+
`create review branch ${branch} in ${checkout.name} (in a worktree; the checkout stays on ${base})`,
|
|
1435
|
+
true,
|
|
1436
|
+
() => {
|
|
1437
|
+
const branchCheckout = worktree(checkout, branch);
|
|
1438
|
+
for (const path of [...packagePaths, "bun.lock"]) {
|
|
1439
|
+
if (existsSync2(join2(checkout.dir, path))) {
|
|
1440
|
+
writeFileSync2(join2(branchCheckout.dir, path), readFileSync2(join2(checkout.dir, path)));
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
restore(checkout);
|
|
1444
|
+
try {
|
|
1445
|
+
relockAndCheck(branchCheckout, false);
|
|
1446
|
+
const message = moving.length > 0 ? `chore: adopt ${moving.map((name) => `${name}@${manifest.candidates?.[name]}`).join(", ")}` : void 0;
|
|
1447
|
+
if (!commitOwned(branchCheckout, packagePaths, true, message)) {
|
|
1448
|
+
console.log(`\u2713 nothing to commit for ${checkout.name} after its re-lock`);
|
|
1449
|
+
dropBranch(checkout, branch);
|
|
1450
|
+
return false;
|
|
1451
|
+
}
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
dropBranch(checkout, branch);
|
|
1454
|
+
throw error;
|
|
1455
|
+
}
|
|
1456
|
+
followers.push({ checkout: branchCheckout, onBranch: true });
|
|
1457
|
+
console.log(`committed on ${branch} for review; ${checkout.name} stays on ${base}`);
|
|
1458
|
+
return true;
|
|
1459
|
+
}
|
|
1460
|
+
);
|
|
1461
|
+
if (!opened) return;
|
|
1462
|
+
reviewBranches.push({ dir: checkout.dir, branch, base });
|
|
1463
|
+
if (moving.length > 0) {
|
|
1464
|
+
waiting.push({ name: checkout.name, dir: checkout.dir, moving, state: "opened", branch });
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
const adopt = (checkout, moving) => {
|
|
1468
|
+
const branch = adoptionBranch(moving);
|
|
1469
|
+
const base = defaultBranch(checkout.dir);
|
|
1470
|
+
const label = moving.map((name) => `${name}@${manifest.candidates?.[name]}`).join(", ");
|
|
1471
|
+
const script = checkout.pkg.scripts?.["train:adopt"];
|
|
1472
|
+
try {
|
|
1473
|
+
effect(
|
|
1474
|
+
`open ${branch} in ${checkout.name}: re-lock onto ${label}${script ? ", run train:adopt" : ""}, commit`,
|
|
1475
|
+
void 0,
|
|
1476
|
+
() => {
|
|
1477
|
+
const branchCheckout = worktree(checkout, branch);
|
|
1478
|
+
try {
|
|
1479
|
+
bumpRanges(branchCheckout);
|
|
1480
|
+
relockAndCheck(branchCheckout, false);
|
|
1481
|
+
if (script && spawnSync2("bun", ["run", "train:adopt"], {
|
|
1482
|
+
cwd: branchCheckout.dir,
|
|
1483
|
+
stdio: "inherit"
|
|
1484
|
+
}).status !== 0) {
|
|
1485
|
+
console.log(
|
|
1486
|
+
`\u26A0 train:adopt failed in ${checkout.name} \u2014 the PR needs work before it merges`
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
git(branchCheckout.dir, "add", "-A");
|
|
1490
|
+
const commit = spawnSync2("git", ["commit", "-m", `chore: adopt ${label}`], {
|
|
1491
|
+
cwd: branchCheckout.dir,
|
|
1492
|
+
stdio: "inherit"
|
|
1493
|
+
});
|
|
1494
|
+
if (commit.status !== 0) {
|
|
1495
|
+
throw new Abort(
|
|
1496
|
+
`commit failed on ${branch} in ${checkout.name} (its hooks reject the adoption)`
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
} catch (error) {
|
|
1500
|
+
dropBranch(checkout, branch);
|
|
1501
|
+
throw error;
|
|
1502
|
+
}
|
|
1503
|
+
followers.push({ checkout: branchCheckout, onBranch: true });
|
|
1504
|
+
}
|
|
1505
|
+
);
|
|
1506
|
+
reviewBranches.push({ dir: checkout.dir, branch, base });
|
|
1507
|
+
waiting.push({ name: checkout.name, dir: checkout.dir, moving, state: "opened", branch });
|
|
1508
|
+
} catch (error) {
|
|
1509
|
+
if (!(error instanceof Abort)) throw error;
|
|
1510
|
+
failed.push(checkout.name);
|
|
1511
|
+
waiting.push({ name: checkout.name, dir: checkout.dir, moving, state: "failed" });
|
|
1512
|
+
console.error(`\u2717 ${error.message} \u2014 adopt ${label} in ${checkout.name} by hand`);
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
const commitOwned = (checkout, packagePaths, changes, message = "chore: sync ecosystem deps to blessed set") => {
|
|
1297
1516
|
if (dryRun && !changes) return false;
|
|
1298
1517
|
return effect(`commit its package.json files + bun.lock in ${checkout.name}`, true, () => {
|
|
1299
|
-
const tracked = [...packagePaths, "bun.lock"]
|
|
1518
|
+
const tracked = [...packagePaths, "bun.lock"].filter(
|
|
1519
|
+
(path) => existsSync2(join2(checkout.dir, path))
|
|
1520
|
+
);
|
|
1300
1521
|
const mutated = git(checkout.dir, "status", "--porcelain", "--", ...tracked);
|
|
1301
1522
|
if (mutated.status !== 0 || mutated.stdout.trim().length === 0) return false;
|
|
1302
1523
|
if (git(checkout.dir, "add", "--", ...tracked).status !== 0) {
|
|
1303
1524
|
throw new Abort(`git add failed in ${checkout.name} \u2014 is bun.lock ignored?`);
|
|
1304
1525
|
}
|
|
1305
|
-
const commit = spawnSync2(
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
cwd: checkout.dir,
|
|
1310
|
-
stdio: "inherit"
|
|
1311
|
-
}
|
|
1312
|
-
);
|
|
1526
|
+
const commit = spawnSync2("git", ["commit", "-m", message], {
|
|
1527
|
+
cwd: checkout.dir,
|
|
1528
|
+
stdio: "inherit"
|
|
1529
|
+
});
|
|
1313
1530
|
if (commit.status !== 0) throw new Abort(`commit failed in ${checkout.name}`);
|
|
1314
1531
|
return true;
|
|
1315
1532
|
});
|
|
@@ -1350,8 +1567,7 @@ if (command === "train") {
|
|
|
1350
1567
|
\u2550 ${lane2} lane: ${checkouts.map((checkout) => checkout.name).join(" \u2192 ") || "(none)"}`
|
|
1351
1568
|
);
|
|
1352
1569
|
const section = laneSection(manifest, lane2);
|
|
1353
|
-
const
|
|
1354
|
-
const laneBlessed = [];
|
|
1570
|
+
const laneRecorded = [];
|
|
1355
1571
|
try {
|
|
1356
1572
|
if (missing.length > 0) {
|
|
1357
1573
|
throw new Abort(
|
|
@@ -1370,7 +1586,6 @@ if (command === "train") {
|
|
|
1370
1586
|
skipped.push(checkout.name);
|
|
1371
1587
|
continue;
|
|
1372
1588
|
}
|
|
1373
|
-
const { inspection, bumped, staleLock } = bumpRanges(checkout);
|
|
1374
1589
|
const releases = checkout.packages.map((entry) => {
|
|
1375
1590
|
const local = JSON.parse(readFileSync2(join2(checkout.dir, entry.path), "utf8")).version ?? "0.0.0";
|
|
1376
1591
|
const remote = npmLatest(entry.name);
|
|
@@ -1386,47 +1601,50 @@ if (command === "train") {
|
|
|
1386
1601
|
skipped.push(checkout.name);
|
|
1387
1602
|
continue;
|
|
1388
1603
|
}
|
|
1604
|
+
const moving = movesOf(checkout);
|
|
1605
|
+
if (pendingAdoption(checkout, moving)) continue;
|
|
1606
|
+
const { inspection, bumped, staleLock } = bumpRanges(checkout);
|
|
1389
1607
|
const relocked = bumped || staleLock || releases.some((release) => release.ahead);
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
if (
|
|
1608
|
+
try {
|
|
1609
|
+
if (relocked) relockAndCheck(checkout);
|
|
1610
|
+
if (commitOwned(checkout, inspection.packagePaths, relocked)) committed.add(checkout.dir);
|
|
1611
|
+
} catch (error) {
|
|
1612
|
+
if (!(error instanceof Abort)) throw error;
|
|
1613
|
+
restore(checkout);
|
|
1614
|
+
if (!(error instanceof CheckFailed) || moving.length === 0) throw error;
|
|
1615
|
+
console.log(`\u2717 ${error.message} against the candidates \u2014 it adopts them on a branch`);
|
|
1616
|
+
walked.add(checkout.dir);
|
|
1617
|
+
adopt(checkout, moving);
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
walked.add(checkout.dir);
|
|
1621
|
+
followers.push({ checkout, onBranch: false });
|
|
1622
|
+
for (const { entry, local, remote, ahead: ahead2 } of releases) {
|
|
1623
|
+
if (ahead2) {
|
|
1395
1624
|
publish(checkout, entry, local, remote);
|
|
1396
1625
|
} else if (section[entry.name] === local) {
|
|
1397
1626
|
console.log(`\u2713 ${entry.name}@${local} already on npm and blessed`);
|
|
1398
1627
|
continue;
|
|
1628
|
+
} else if (manifest.candidates?.[entry.name] === local) {
|
|
1629
|
+
console.log(`\u2713 ${entry.name}@${local} already on npm and a candidate`);
|
|
1630
|
+
continue;
|
|
1399
1631
|
} else {
|
|
1400
|
-
console.log(`\u2713 ${entry.name}@${local} already on npm \u2014
|
|
1632
|
+
console.log(`\u2713 ${entry.name}@${local} already on npm \u2014 recording it as a candidate`);
|
|
1401
1633
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1634
|
+
manifest.candidates = { ...manifest.candidates, [entry.name]: local };
|
|
1635
|
+
laneRecorded.push(`${entry.name}@${local}`);
|
|
1404
1636
|
}
|
|
1405
1637
|
}
|
|
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
1638
|
console.log(
|
|
1416
|
-
`${lane2} lane
|
|
1639
|
+
`${lane2} lane candidates: ${laneRecorded.length === 0 ? "nothing new" : laneRecorded.join(", ")}`
|
|
1417
1640
|
);
|
|
1418
1641
|
} catch (error) {
|
|
1419
1642
|
if (!(error instanceof Abort)) throw error;
|
|
1420
|
-
|
|
1421
|
-
Object.assign(section, before);
|
|
1643
|
+
failedLanes.add(lane2);
|
|
1422
1644
|
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
|
-
}
|
|
1645
|
+
console.error(`\u2717 ${error.message} \u2014 ${lane2} lane stopped; none of its candidates is blessed`);
|
|
1429
1646
|
}
|
|
1647
|
+
recorded.push(...laneRecorded);
|
|
1430
1648
|
}
|
|
1431
1649
|
if (plan.consumers.length > 0 || plan.missingConsumers.length > 0) {
|
|
1432
1650
|
console.log(
|
|
@@ -1449,39 +1667,97 @@ if (command === "train") {
|
|
|
1449
1667
|
skipped.push(checkout.name);
|
|
1450
1668
|
continue;
|
|
1451
1669
|
}
|
|
1670
|
+
const moving = movesOf(checkout);
|
|
1671
|
+
if (pendingAdoption(checkout, moving)) continue;
|
|
1672
|
+
const { inspection, bumped, staleLock } = bumpRanges(checkout);
|
|
1673
|
+
if (!bumped && !staleLock) {
|
|
1674
|
+
walked.add(checkout.dir);
|
|
1675
|
+
console.log("\u2713 already on the blessed set");
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1452
1678
|
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
1679
|
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
|
-
}
|
|
1680
|
+
reviewBranch(checkout, inspection.packagePaths, moving);
|
|
1681
|
+
walked.add(checkout.dir);
|
|
1477
1682
|
} catch (error) {
|
|
1478
1683
|
if (!(error instanceof Abort)) throw error;
|
|
1684
|
+
restore(checkout);
|
|
1685
|
+
if (error instanceof CheckFailed && moving.length > 0) {
|
|
1686
|
+
console.log(`\u2717 ${error.message} against the candidates \u2014 it adopts them on a branch`);
|
|
1687
|
+
walked.add(checkout.dir);
|
|
1688
|
+
adopt(checkout, moving);
|
|
1689
|
+
continue;
|
|
1690
|
+
}
|
|
1479
1691
|
failed.push(checkout.name);
|
|
1480
|
-
console.error(`\u2717 ${error.message} \u2014 its
|
|
1692
|
+
console.error(`\u2717 ${error.message} \u2014 its checkout is back on its default branch`);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
const candidateNames = Object.keys(manifest.candidates ?? {});
|
|
1696
|
+
if (candidateNames.length > 0) {
|
|
1697
|
+
const everything = trainPlan(dir, manifest, LANES);
|
|
1698
|
+
for (const checkout of [
|
|
1699
|
+
...everything.lanes.flatMap((entry) => entry.checkouts),
|
|
1700
|
+
...everything.consumers
|
|
1701
|
+
]) {
|
|
1702
|
+
if (walked.has(checkout.dir)) continue;
|
|
1703
|
+
const declared = declaredDependencies(checkout.dir);
|
|
1704
|
+
const moving = candidateNames.filter((name) => declared.has(name));
|
|
1705
|
+
if (moving.length > 0) {
|
|
1706
|
+
waiting.push({ name: checkout.name, dir: checkout.dir, moving, state: "unverified" });
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
for (const repo of [
|
|
1710
|
+
...everything.missingConsumers,
|
|
1711
|
+
...everything.ambiguousConsumers.map((entry) => entry.key)
|
|
1712
|
+
]) {
|
|
1713
|
+
const consumed = manifest.consumers?.[repo]?.lanes ?? [];
|
|
1714
|
+
const moving = candidateNames.filter((name) => {
|
|
1715
|
+
const lane2 = laneOf(manifest, name);
|
|
1716
|
+
return !!lane2 && consumed.includes(lane2);
|
|
1717
|
+
});
|
|
1718
|
+
if (moving.length > 0) waiting.push({ name: repo, dir, moving, state: "unverified" });
|
|
1481
1719
|
}
|
|
1482
1720
|
}
|
|
1721
|
+
const held = new Set(waiting.flatMap((entry) => entry.moving));
|
|
1722
|
+
const promotable = candidateNames.filter((name) => {
|
|
1723
|
+
const lane2 = laneOf(manifest, name);
|
|
1724
|
+
return !!lane2 && !failedLanes.has(lane2) && !held.has(name);
|
|
1725
|
+
});
|
|
1726
|
+
const promoted = promoteCandidates(manifest, promotable);
|
|
1727
|
+
const bomDirty = recorded.length > 0 || promoted.length > 0;
|
|
1728
|
+
if (bomDirty) {
|
|
1729
|
+
effect(
|
|
1730
|
+
"write the candidates and blessings to versions.json",
|
|
1731
|
+
void 0,
|
|
1732
|
+
() => writeManifest(manifest)
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
const why = {
|
|
1736
|
+
opened: "opened this run",
|
|
1737
|
+
unmerged: "waiting for its PR to merge",
|
|
1738
|
+
unpushed: "committed but never pushed",
|
|
1739
|
+
failed: "the train could not commit it \u2014 adopt by hand",
|
|
1740
|
+
unverified: "not verified this run"
|
|
1741
|
+
};
|
|
1483
1742
|
console.log(`
|
|
1484
|
-
blessed: ${
|
|
1743
|
+
blessed: ${promoted.length === 0 ? "nothing" : promoted.join(", ")}`);
|
|
1744
|
+
const candidates = Object.entries(manifest.candidates ?? {});
|
|
1745
|
+
if (candidates.length > 0) {
|
|
1746
|
+
console.log("candidates (check accepts them; not yet blessed):");
|
|
1747
|
+
for (const [name, version] of candidates) {
|
|
1748
|
+
const on = waiting.filter((entry) => entry.moving.includes(name)).map((entry) => `${entry.name} (${entry.branch ?? why[entry.state]})`);
|
|
1749
|
+
console.log(` ${name}@${version}${on.length === 0 ? "" : ` \u2014 waiting on ${on.join(", ")}`}`);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
if (waiting.length > 0) {
|
|
1753
|
+
console.log("pending (merge each adoption PR, then run the train again to bless):");
|
|
1754
|
+
for (const entry of waiting) {
|
|
1755
|
+
const url = entry.state === "unmerged" && !dryRun && entry.branch ? prUrl(entry.dir, entry.branch) : null;
|
|
1756
|
+
console.log(
|
|
1757
|
+
` ${entry.name}: ${entry.branch ? `${entry.branch} \u2014 ` : ""}${why[entry.state]}${url ? ` ${url}` : ""}`
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1485
1761
|
if (skipped.length > 0) console.log(`skipped: ${skipped.join(", ")}`);
|
|
1486
1762
|
if (failed.length > 0) console.log(`failed: ${failed.join(", ")}`);
|
|
1487
1763
|
const remoteVersion = npmLatest("@inixiative/config");
|
|
@@ -1520,26 +1796,25 @@ blessed: ${blessed.length === 0 ? "nothing" : blessed.join(", ")}`);
|
|
|
1520
1796
|
process.exit(1);
|
|
1521
1797
|
}
|
|
1522
1798
|
});
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
}
|
|
1799
|
+
const message = `chore: ${[
|
|
1800
|
+
promoted.length > 0 ? `bless ${promoted.join(", ")}` : "",
|
|
1801
|
+
recorded.length > 0 ? `candidate ${recorded.join(", ")}` : ""
|
|
1802
|
+
].filter(Boolean).join("; ") || "release"} \u2014 ${version}`;
|
|
1803
|
+
effect(`commit "${message}" in @inixiative/config`, void 0, () => {
|
|
1804
|
+
spawnSync2("git", ["add", "--", "package.json", "versions.json", "test/fixtures"], {
|
|
1805
|
+
cwd: packageRoot
|
|
1806
|
+
});
|
|
1807
|
+
if (spawnSync2("git", ["diff", "--cached", "--quiet"], { cwd: packageRoot }).status === 0)
|
|
1808
|
+
return;
|
|
1809
|
+
const commit = spawnSync2("git", ["commit", "-m", message], {
|
|
1810
|
+
cwd: packageRoot,
|
|
1811
|
+
stdio: "inherit"
|
|
1812
|
+
});
|
|
1813
|
+
if (commit.status !== 0) {
|
|
1814
|
+
console.error("\u2717 commit failed in @inixiative/config \u2014 aborting before publish");
|
|
1815
|
+
process.exit(1);
|
|
1541
1816
|
}
|
|
1542
|
-
);
|
|
1817
|
+
});
|
|
1543
1818
|
effect(`publish @inixiative/config@${version} to npm`, void 0, () => {
|
|
1544
1819
|
const publish2 = spawnSync2("npm", ["publish"], { cwd: packageRoot, stdio: "inherit" });
|
|
1545
1820
|
if (publish2.status !== 0 || !servedByRegistry("@inixiative/config", version)) {
|
|
@@ -1551,12 +1826,12 @@ blessed: ${blessed.length === 0 ? "nothing" : blessed.join(", ")}`);
|
|
|
1551
1826
|
committed.add(packageRoot);
|
|
1552
1827
|
if (followers.length > 0) console.log(`
|
|
1553
1828
|
\u2550 re-lock onto @inixiative/config@${version}`);
|
|
1554
|
-
for (const checkout of followers) {
|
|
1829
|
+
for (const { checkout, onBranch } of followers) {
|
|
1555
1830
|
try {
|
|
1556
1831
|
const { inspection, bumped, staleLock } = bumpRanges(checkout);
|
|
1557
1832
|
if (!bumped && !staleLock) continue;
|
|
1558
1833
|
relockAndCheck(checkout, false);
|
|
1559
|
-
if (commitOwned(checkout, inspection.packagePaths, true) && !
|
|
1834
|
+
if (commitOwned(checkout, inspection.packagePaths, true) && !onBranch)
|
|
1560
1835
|
committed.add(checkout.dir);
|
|
1561
1836
|
if (!dryRun) console.log(`\u2713 ${checkout.name} locked to @inixiative/config@${version}`);
|
|
1562
1837
|
} catch (error) {
|
|
@@ -1603,6 +1878,13 @@ dry run complete \u2014 a real train would commit in ${committed.size} repo(s) a
|
|
|
1603
1878
|
}
|
|
1604
1879
|
process.exit(failed.length > 0 ? 1 : 0);
|
|
1605
1880
|
}
|
|
1881
|
+
function prUrl(cwd, branch) {
|
|
1882
|
+
const view = spawnSync2("gh", ["pr", "view", branch, "--json", "url", "--jq", ".url"], {
|
|
1883
|
+
cwd,
|
|
1884
|
+
encoding: "utf8"
|
|
1885
|
+
});
|
|
1886
|
+
return view.status === 0 ? view.stdout.trim() || null : null;
|
|
1887
|
+
}
|
|
1606
1888
|
function currentBranch(cwd) {
|
|
1607
1889
|
return git(cwd, "rev-parse", "--abbrev-ref", "HEAD").stdout.trim();
|
|
1608
1890
|
}
|
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.82",
|
|
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.19.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/agent-session": "0.6.4"
|
|
51
54
|
}
|
|
52
55
|
}
|