@inixiative/config 0.5.35 → 0.5.36
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 +30 -7
- package/dist/cli.js +498 -201
- package/package.json +1 -1
- package/versions.json +25 -1
package/README.md
CHANGED
|
@@ -40,7 +40,19 @@ One version of `@inixiative/config` names one coherent ecosystem state:
|
|
|
40
40
|
- `bun` — blessed runtime, written to `.bun-version` and `packageManager`
|
|
41
41
|
- `toolchain` — exact pins for `@biomejs/biome`, `typescript`, `tsup`, `@types/bun`
|
|
42
42
|
- `required` — toolchain packages every repo must carry
|
|
43
|
-
- `ecosystem` — the blessed, mutually-verified `@inixiative/*` set
|
|
43
|
+
- `ecosystem` — the primitives lane's blessed, mutually-verified `@inixiative/*` set, plus this package
|
|
44
|
+
- `agentic` — the agentic lane's blessed set
|
|
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
|
+
|
|
47
|
+
```jsonc
|
|
48
|
+
"consumers": {
|
|
49
|
+
"inixiative/template": { "lanes": ["primitives"] },
|
|
50
|
+
"inixiative/kingdom": { "lanes": ["primitives", "agentic"], "upstream": "inixiative/template" },
|
|
51
|
+
"inixiative/oracle": { "lanes": ["agentic"] }
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A lane is a release cascade with its own blessed set. `primitives` (json-rules, permissions, transitions, rules-builder, prisma-map, atlas, gloss, archive) ships first; `agentic` (agent-session → foundry-core → foundry) builds on the primitives' blessed set and never the reverse. `check` holds every repo to the union of all lanes, so a repo that mixes both answers to both.
|
|
44
56
|
|
|
45
57
|
TypeScript 6.0 notes, discovered by this repo's fixture suite:
|
|
46
58
|
|
|
@@ -52,17 +64,23 @@ TypeScript 6.0 notes, discovered by this repo's fixture suite:
|
|
|
52
64
|
```
|
|
53
65
|
bunx @inixiative/config check [dir] [--preset=base|node|react]
|
|
54
66
|
bunx @inixiative/config sync [dir] [--preset=...] [--force] [--no-install]
|
|
55
|
-
bunx @inixiative/config scan [root]
|
|
56
|
-
bunx @inixiative/config train [root]
|
|
67
|
+
bunx @inixiative/config scan [root] [--lane=primitives|agentic]
|
|
68
|
+
bunx @inixiative/config train [root] [--lane=primitives|agentic] [--push]
|
|
57
69
|
```
|
|
58
70
|
|
|
59
|
-
`check` is read-only and exits non-zero on drift — run it in CI after a frozen-lockfile install. It verifies toolchain pins, `"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 that the declared range admits the blessed version and the lockfile actually resolves to it (the stale-lockfile class).
|
|
71
|
+
`check` is read-only and exits non-zero on drift — run it in CI after a frozen-lockfile install. It verifies toolchain pins, `"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). 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.
|
|
60
72
|
|
|
61
73
|
`sync` applies every fix, re-locks via `bun install`, and runs `bun update` on stale ecosystem entries. 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.
|
|
62
74
|
|
|
63
|
-
`scan` runs `check` across every ecosystem checkout under a root directory. Targets are derived from the BOM
|
|
75
|
+
`scan` runs `check` across every ecosystem checkout under a root directory, for every lane or just the one `--lane` names. Targets are derived from the BOM: lane packages match by package name — the root package or any workspace member, so a monorepo such as foundry is found by the packages it publishes — and consumers match by their `origin` GitHub repo. Directory names and local layout never need declaring: scan looks at each child of the root and one level into a child without a `package.json` (a workspace folder such as `kingdom-workspace/`). Linked git worktrees are other sessions' branches and never targets; when several clones claim one package or repo, only those with an `origin` remote count, and if that still leaves more than one the target is reported ambiguous rather than guessed. Origin is the truth, not the checkout: scan fetches per repo and fails on checkouts behind origin (their findings describe a stale tree), fails when a BOM entry has no checkout at all, warns when a checkout sits on a non-default branch (its findings describe the branch, not what ships), and surfaces unmerged `claude/*` session branches.
|
|
76
|
+
|
|
77
|
+
`train` is the release cascade, run from this repo's checkout. 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.
|
|
64
78
|
|
|
65
|
-
|
|
79
|
+
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 leaves its BOM entries as they were. 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 → 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 → records it in its lane's section of the BOM. 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 not yet blessed — published by hand, or by a train whose lane later failed — is blessed on the next run.
|
|
80
|
+
|
|
81
|
+
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 and leaves the checkout there until it is merged.
|
|
82
|
+
|
|
83
|
+
When anything was 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. It never pushes unless asked: pass `--push` to push every repo it committed to and open a PR for each consumer branch; otherwise it prints the commands.
|
|
66
84
|
|
|
67
85
|
Division of labor: this CLI owns the toolchain set, ecosystem coherence, stubs, and lockfile format. Renovate owns everything else plus bumping `@inixiative/config` itself, and must be fenced off the toolchain packages.
|
|
68
86
|
|
|
@@ -85,4 +103,9 @@ jobs:
|
|
|
85
103
|
|
|
86
104
|
## Release flow
|
|
87
105
|
|
|
88
|
-
|
|
106
|
+
Two lanes, each publishing in dependency order, with config last:
|
|
107
|
+
|
|
108
|
+
- **primitives** — for example json-rules → permissions → transitions → rules-builder, plus prisma-map, atlas, gloss and archive (a standalone MIT package). Consumers: template, kingdom.
|
|
109
|
+
- **agentic** — agent-session → foundry-core → foundry, built on the primitives' blessed set. foundry-core and foundry publish from the foundry monorepo's `packages/core` and `packages/foundry`. Consumers: oracle (a private leaf), kingdom.
|
|
110
|
+
|
|
111
|
+
A release is `train` (or `train --lane=agentic` to leave the primitives alone): bump the package's version on its default branch, then run the train. `train` automates the whole walk, this package and the consumer PRs included. Downstream repos pick up toolchain changes via `sync`.
|
package/dist/cli.js
CHANGED
|
@@ -2,15 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5
|
-
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
6
|
-
import {
|
|
5
|
+
import { existsSync as existsSync2, mkdtempSync, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
6
|
+
import { tmpdir } from "os";
|
|
7
|
+
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
7
8
|
|
|
8
9
|
// src/lib.ts
|
|
9
10
|
import { spawnSync } from "child_process";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
existsSync,
|
|
13
|
+
globSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
statSync,
|
|
18
|
+
writeFileSync
|
|
19
|
+
} from "fs";
|
|
11
20
|
import { dirname, join, sep } from "path";
|
|
12
21
|
import { fileURLToPath } from "url";
|
|
13
22
|
var packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
23
|
+
var LANES = ["primitives", "agentic"];
|
|
24
|
+
var isLane = (value) => LANES.includes(value);
|
|
25
|
+
var lanePackages = (manifest2, lane) => {
|
|
26
|
+
const section = lane === "primitives" ? manifest2.ecosystem : manifest2.agentic ?? {};
|
|
27
|
+
const { "@inixiative/config": _config, ...packages } = section;
|
|
28
|
+
return packages;
|
|
29
|
+
};
|
|
30
|
+
var laneSection = (manifest2, lane) => {
|
|
31
|
+
if (lane === "primitives") return manifest2.ecosystem;
|
|
32
|
+
manifest2.agentic ??= {};
|
|
33
|
+
return manifest2.agentic;
|
|
34
|
+
};
|
|
35
|
+
var blessedVersions = (manifest2) => ({
|
|
36
|
+
...manifest2.ecosystem,
|
|
37
|
+
...manifest2.agentic
|
|
38
|
+
});
|
|
39
|
+
var consumersFor = (manifest2, lanes2) => {
|
|
40
|
+
const consumers = manifest2.consumers ?? {};
|
|
41
|
+
const selected = Object.keys(consumers).filter((repo) => consumers[repo].lanes.some((lane) => lanes2.includes(lane))).sort();
|
|
42
|
+
const order = [];
|
|
43
|
+
const visit = (repo, trail) => {
|
|
44
|
+
if (order.includes(repo)) return;
|
|
45
|
+
if (trail.includes(repo))
|
|
46
|
+
throw new Error(`consumer upstream cycle: ${[...trail, repo].join(" \u2192 ")}`);
|
|
47
|
+
const upstream = consumers[repo].upstream;
|
|
48
|
+
if (upstream && selected.includes(upstream)) visit(upstream, [...trail, repo]);
|
|
49
|
+
order.push(repo);
|
|
50
|
+
};
|
|
51
|
+
for (const repo of selected) visit(repo, []);
|
|
52
|
+
return order;
|
|
53
|
+
};
|
|
14
54
|
var REQUIRED_SCRIPTS = {
|
|
15
55
|
typecheck: "tsc --noEmit",
|
|
16
56
|
lint: "biome check .",
|
|
@@ -107,65 +147,184 @@ var admits = (range, version) => {
|
|
|
107
147
|
}
|
|
108
148
|
return vMajor === major && vMinor === minor;
|
|
109
149
|
};
|
|
110
|
-
var lockedVersion = (lockText,
|
|
150
|
+
var lockedVersion = (lockText, key, target = key) => {
|
|
111
151
|
const lock = tryParseJsonc(lockText);
|
|
112
152
|
const packages = lock?.packages;
|
|
113
153
|
if (!packages || typeof packages !== "object") return null;
|
|
114
|
-
const entry = packages[
|
|
154
|
+
const entry = packages[key];
|
|
115
155
|
const spec = Array.isArray(entry) ? entry[0] : null;
|
|
116
|
-
if (typeof spec !== "string" || !spec.startsWith(`${
|
|
117
|
-
const version = spec.slice(
|
|
156
|
+
if (typeof spec !== "string" || !spec.startsWith(`${target}@`)) return null;
|
|
157
|
+
const version = spec.slice(target.length + 1);
|
|
118
158
|
return /^\d/.test(version) ? version : null;
|
|
119
159
|
};
|
|
120
|
-
var
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
160
|
+
var npmAlias = (spec) => {
|
|
161
|
+
const match = spec.match(/^npm:((?:@[^/@]+\/)?[^@]+)@(.+)$/);
|
|
162
|
+
return match ? { name: match[1], range: match[2] } : null;
|
|
163
|
+
};
|
|
164
|
+
var readJson = (path) => {
|
|
165
|
+
try {
|
|
166
|
+
return tryParseJsonc(readFileSync(path, "utf8"));
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
var originRepo = (dir2) => {
|
|
172
|
+
const url = spawnSync("git", ["remote", "get-url", "origin"], { cwd: dir2, encoding: "utf8" });
|
|
173
|
+
if (url.status !== 0) return null;
|
|
174
|
+
const match = url.stdout.trim().match(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?\/?$/);
|
|
175
|
+
return match ? match[1] : null;
|
|
176
|
+
};
|
|
177
|
+
var isLinkedWorktree = (dir2) => {
|
|
178
|
+
const dotGit = join(dir2, ".git");
|
|
179
|
+
return existsSync(dotGit) && !statSync(dotGit).isDirectory();
|
|
180
|
+
};
|
|
181
|
+
var childDirs = (dir2) => readdirSync(dir2, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).filter((entry) => entry.name !== "node_modules").map((entry) => join(dir2, entry.name));
|
|
182
|
+
var candidateDirs = (root) => {
|
|
183
|
+
const dirs = [];
|
|
184
|
+
for (const child of childDirs(root)) {
|
|
185
|
+
const nested = existsSync(join(child, "package.json")) ? [child] : childDirs(child).filter((dir2) => existsSync(join(dir2, "package.json")));
|
|
186
|
+
dirs.push(...nested.filter((dir2) => !isLinkedWorktree(dir2)));
|
|
187
|
+
}
|
|
188
|
+
return dirs;
|
|
189
|
+
};
|
|
190
|
+
var discover = (root, manifest2, lanes2 = LANES) => {
|
|
191
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
192
|
+
for (const lane of lanes2) {
|
|
193
|
+
for (const name of Object.keys(lanePackages(manifest2, lane))) wanted.set(name, lane);
|
|
194
|
+
}
|
|
195
|
+
const consumerRepos = new Set(consumersFor(manifest2, lanes2));
|
|
196
|
+
const claims = /* @__PURE__ */ new Map();
|
|
197
|
+
const claim = (key, checkout) => claims.set(key, [...claims.get(key) ?? [], checkout]);
|
|
198
|
+
for (const dir2 of candidateDirs(root)) {
|
|
199
|
+
const pkg = readJson(join(dir2, "package.json"));
|
|
200
|
+
if (!pkg) continue;
|
|
201
|
+
const name = typeof pkg.name === "string" ? pkg.name : dir2;
|
|
202
|
+
const provided = [
|
|
203
|
+
{ name: pkg.name, path: "package.json" },
|
|
204
|
+
...workspacePackagePaths(dir2, pkg).map((path) => ({
|
|
205
|
+
name: readJson(join(dir2, path))?.name,
|
|
206
|
+
path
|
|
207
|
+
}))
|
|
208
|
+
].filter(
|
|
209
|
+
(entry) => typeof entry.name === "string" && wanted.has(entry.name)
|
|
210
|
+
);
|
|
211
|
+
if (provided.length > 0) {
|
|
212
|
+
const checkout = {
|
|
213
|
+
dir: dir2,
|
|
214
|
+
name,
|
|
215
|
+
pkg,
|
|
216
|
+
lane: wanted.get(provided[0].name),
|
|
217
|
+
packages: provided
|
|
218
|
+
};
|
|
219
|
+
for (const entry of provided) claim(entry.name, checkout);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (consumerRepos.size === 0 || !existsSync(join(dir2, ".git"))) continue;
|
|
223
|
+
const repo = originRepo(dir2);
|
|
224
|
+
if (repo && consumerRepos.has(repo)) {
|
|
225
|
+
const consumer = manifest2.consumers[repo];
|
|
226
|
+
claim(repo, { dir: dir2, name, pkg, lane: null, packages: [], consumer: { repo, ...consumer } });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const chosen = /* @__PURE__ */ new Map();
|
|
230
|
+
const ambiguous = [];
|
|
231
|
+
for (const [key, candidates] of claims) {
|
|
232
|
+
const pool = candidates.length > 1 ? candidates.filter((candidate) => originRepo(candidate.dir)) : candidates;
|
|
233
|
+
if (pool.length === 1) chosen.set(pool[0].dir, pool[0]);
|
|
234
|
+
else ambiguous.push({ key, dirs: candidates.map((candidate) => candidate.dir).sort() });
|
|
235
|
+
}
|
|
236
|
+
const ambiguousDirs = new Set(ambiguous.flatMap((entry) => entry.dirs));
|
|
237
|
+
const checkouts = [...chosen.values()].filter((checkout) => !ambiguousDirs.has(checkout.dir)).map((checkout) => ({ ...checkout, packages: publishOrder(checkout) })).sort((a, b) => a.name.localeCompare(b.name));
|
|
238
|
+
return { checkouts, ambiguous: ambiguous.sort((a, b) => a.key.localeCompare(b.key)) };
|
|
239
|
+
};
|
|
240
|
+
var dependencyNames = (dir2, paths) => {
|
|
241
|
+
const names = /* @__PURE__ */ new Set();
|
|
242
|
+
for (const path of paths) {
|
|
243
|
+
const pkg = readJson(join(dir2, path));
|
|
129
244
|
for (const field of DEP_FIELDS) {
|
|
130
|
-
for (const
|
|
131
|
-
|
|
245
|
+
for (const [key, spec] of Object.entries(pkg?.[field] ?? {})) {
|
|
246
|
+
names.add(key);
|
|
247
|
+
const alias = npmAlias(spec);
|
|
248
|
+
if (alias) names.add(alias.name);
|
|
132
249
|
}
|
|
133
250
|
}
|
|
134
|
-
dependsOn.set(repo.name, edges);
|
|
135
251
|
}
|
|
252
|
+
return names;
|
|
253
|
+
};
|
|
254
|
+
var orderBy = (items, key, deps) => {
|
|
136
255
|
const order = [];
|
|
137
256
|
const done = /* @__PURE__ */ new Set();
|
|
138
|
-
while (done.size <
|
|
139
|
-
const ready =
|
|
140
|
-
(repo) => !done.has(repo.name) && [...dependsOn.get(repo.name)].every((dep) => done.has(dep))
|
|
141
|
-
).sort((a, b) => a.name.localeCompare(b.name));
|
|
257
|
+
while (done.size < items.length) {
|
|
258
|
+
const ready = items.filter((item) => !done.has(key(item)) && deps(item).every((dep) => done.has(dep))).sort((a, b) => key(a).localeCompare(key(b)));
|
|
142
259
|
if (ready.length === 0) throw new Error("dependency cycle among ecosystem repos");
|
|
143
|
-
for (const
|
|
144
|
-
order.push(
|
|
145
|
-
done.add(
|
|
260
|
+
for (const item of ready) {
|
|
261
|
+
order.push(item);
|
|
262
|
+
done.add(key(item));
|
|
146
263
|
}
|
|
147
264
|
}
|
|
148
265
|
return order;
|
|
149
266
|
};
|
|
150
|
-
var
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
267
|
+
var publishOrder = (checkout) => {
|
|
268
|
+
const names = new Set(checkout.packages.map((entry) => entry.name));
|
|
269
|
+
const deps = new Map(
|
|
270
|
+
checkout.packages.map((entry) => [
|
|
271
|
+
entry.name,
|
|
272
|
+
[...dependencyNames(checkout.dir, [entry.path])].filter(
|
|
273
|
+
(dep) => names.has(dep) && dep !== entry.name
|
|
274
|
+
)
|
|
275
|
+
])
|
|
276
|
+
);
|
|
277
|
+
return orderBy(
|
|
278
|
+
checkout.packages,
|
|
279
|
+
(entry) => entry.name,
|
|
280
|
+
(entry) => deps.get(entry.name) ?? []
|
|
281
|
+
);
|
|
282
|
+
};
|
|
283
|
+
var topoOrder = (checkouts) => {
|
|
284
|
+
const publishing = checkouts.filter((checkout) => !checkout.consumer);
|
|
285
|
+
const owner = /* @__PURE__ */ new Map();
|
|
286
|
+
for (const checkout of publishing) {
|
|
287
|
+
for (const entry of checkout.packages) owner.set(entry.name, checkout.dir);
|
|
288
|
+
}
|
|
289
|
+
return orderBy(
|
|
290
|
+
publishing,
|
|
291
|
+
(checkout) => checkout.dir,
|
|
292
|
+
(checkout) => {
|
|
293
|
+
const paths = ["package.json", ...workspacePackagePaths(checkout.dir, checkout.pkg)];
|
|
294
|
+
return [...dependencyNames(checkout.dir, paths)].map((dep) => owner.get(dep)).filter((dir2) => !!dir2 && dir2 !== checkout.dir);
|
|
295
|
+
}
|
|
296
|
+
);
|
|
162
297
|
};
|
|
163
|
-
var
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
298
|
+
var trainPlan = (root, manifest2, lanes2 = LANES) => {
|
|
299
|
+
const { checkouts, ambiguous } = discover(root, manifest2, lanes2);
|
|
300
|
+
const consumerOrder = consumersFor(manifest2, lanes2);
|
|
301
|
+
const byRepo = new Map(
|
|
302
|
+
checkouts.flatMap(
|
|
303
|
+
(checkout) => checkout.consumer ? [[checkout.consumer.repo, checkout]] : []
|
|
304
|
+
)
|
|
167
305
|
);
|
|
306
|
+
return {
|
|
307
|
+
lanes: LANES.filter((lane) => lanes2.includes(lane)).map((lane) => {
|
|
308
|
+
const names = new Set(Object.keys(lanePackages(manifest2, lane)));
|
|
309
|
+
const own = checkouts.filter((checkout) => checkout.lane === lane);
|
|
310
|
+
return {
|
|
311
|
+
lane,
|
|
312
|
+
checkouts: topoOrder(own),
|
|
313
|
+
missing: [...names].filter(
|
|
314
|
+
(name) => !own.some((checkout) => checkout.packages.some((entry) => entry.name === name)) && !ambiguous.some((entry) => entry.key === name)
|
|
315
|
+
),
|
|
316
|
+
ambiguous: ambiguous.filter((entry) => names.has(entry.key))
|
|
317
|
+
};
|
|
318
|
+
}),
|
|
319
|
+
consumers: consumerOrder.flatMap((repo) => byRepo.get(repo) ?? []),
|
|
320
|
+
missingConsumers: consumerOrder.filter(
|
|
321
|
+
(repo) => !byRepo.has(repo) && !ambiguous.some((entry) => entry.key === repo)
|
|
322
|
+
),
|
|
323
|
+
ambiguousConsumers: ambiguous.filter((entry) => consumerOrder.includes(entry.key))
|
|
324
|
+
};
|
|
168
325
|
};
|
|
326
|
+
var writeManifest = (manifest2) => writeFileSync(join(packageRoot, "versions.json"), `${JSON.stringify(manifest2, null, 2)}
|
|
327
|
+
`);
|
|
169
328
|
var workspacePackagePaths = (dir2, pkg) => {
|
|
170
329
|
const raw = pkg.workspaces;
|
|
171
330
|
const globs = Array.isArray(raw) ? raw : Array.isArray(raw?.packages) ? raw.packages : [];
|
|
@@ -407,22 +566,28 @@ function inspect(dir2, manifest2, presetOverride) {
|
|
|
407
566
|
}
|
|
408
567
|
}))
|
|
409
568
|
];
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (
|
|
569
|
+
const blessedSet = blessedVersions(manifest2);
|
|
570
|
+
const staleKeys = /* @__PURE__ */ new Set();
|
|
571
|
+
for (const target of rangeTargets) {
|
|
572
|
+
for (const field of DEP_FIELDS) {
|
|
573
|
+
const deps = target.pkg[field];
|
|
574
|
+
for (const [key, spec] of Object.entries(deps ?? {})) {
|
|
575
|
+
if (/^(file|link|workspace):/.test(spec)) continue;
|
|
576
|
+
const alias = npmAlias(spec);
|
|
577
|
+
const name = alias && !(key in blessedSet) ? alias.name : key;
|
|
578
|
+
const range = alias && name === alias.name ? alias.range : spec;
|
|
579
|
+
const blessed = blessedSet[name];
|
|
580
|
+
if (!blessed || name === target.pkg.name || !deps) continue;
|
|
581
|
+
const rewrite = (next) => name === key ? next : `npm:${name}@${next}`;
|
|
417
582
|
const ok = admits(range, blessed);
|
|
418
583
|
if (ok === false) {
|
|
419
584
|
findings.push({
|
|
420
585
|
level: "error",
|
|
421
586
|
kind: "ecosystem-range",
|
|
422
587
|
name,
|
|
423
|
-
message: `${target.label}${
|
|
588
|
+
message: `${target.label}${key} ${field} range ${spec} does not admit blessed ${blessed} \u2192 ${rewrite(`^${blessed}`)}`,
|
|
424
589
|
fix: () => {
|
|
425
|
-
deps[
|
|
590
|
+
deps[key] = rewrite(`^${blessed}`);
|
|
426
591
|
target.mark();
|
|
427
592
|
}
|
|
428
593
|
});
|
|
@@ -430,28 +595,28 @@ function inspect(dir2, manifest2, presetOverride) {
|
|
|
430
595
|
if (ok === null) {
|
|
431
596
|
findings.push({
|
|
432
597
|
level: "warn",
|
|
433
|
-
message: `${target.label}${
|
|
598
|
+
message: `${target.label}${key} range ${spec} not understood; verify manually against ${blessed}`
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
if (!lockText || field === "peerDependencies" || staleKeys.has(key)) continue;
|
|
602
|
+
const locked = lockedVersion(lockText, key, name);
|
|
603
|
+
if (locked && admits(range, locked) === false) {
|
|
604
|
+
staleKeys.add(key);
|
|
605
|
+
findings.push({
|
|
606
|
+
level: "error",
|
|
607
|
+
kind: "stale-lock",
|
|
608
|
+
name: key,
|
|
609
|
+
message: `${target.label}${key} locked at ${locked}, violating declared ${spec} (stale lockfile) \u2192 re-lock`
|
|
610
|
+
});
|
|
611
|
+
} else if (locked && locked !== blessed) {
|
|
612
|
+
staleKeys.add(key);
|
|
613
|
+
findings.push({
|
|
614
|
+
level: "error",
|
|
615
|
+
kind: "stale-lock",
|
|
616
|
+
name: key,
|
|
617
|
+
message: `${target.label}${key} locked at ${locked}, blessed is ${blessed} \u2192 re-lock`
|
|
434
618
|
});
|
|
435
619
|
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
const installed = pkg.dependencies?.[name] ?? pkg.devDependencies?.[name];
|
|
439
|
-
if (lockText && installed) {
|
|
440
|
-
const locked = lockedVersion(lockText, name);
|
|
441
|
-
if (locked && admits(installed, locked) === false) {
|
|
442
|
-
findings.push({
|
|
443
|
-
level: "error",
|
|
444
|
-
kind: "stale-lock",
|
|
445
|
-
name,
|
|
446
|
-
message: `${name} locked at ${locked}, violating declared ${installed} (stale lockfile) \u2192 re-lock`
|
|
447
|
-
});
|
|
448
|
-
} else if (locked && locked !== blessed) {
|
|
449
|
-
findings.push({
|
|
450
|
-
level: "error",
|
|
451
|
-
kind: "stale-lock",
|
|
452
|
-
name,
|
|
453
|
-
message: `${name} locked at ${locked}, blessed is ${blessed} \u2192 re-lock`
|
|
454
|
-
});
|
|
455
620
|
}
|
|
456
621
|
}
|
|
457
622
|
}
|
|
@@ -535,16 +700,19 @@ var args = process.argv.slice(2);
|
|
|
535
700
|
var command = args[0];
|
|
536
701
|
var flags = new Set(args.filter((arg) => arg.startsWith("--")).map((arg) => arg.split("=")[0]));
|
|
537
702
|
var presetFlag = args.find((arg) => arg.startsWith("--preset="))?.split("=")[1];
|
|
703
|
+
var laneFlag = args.find((arg) => arg.startsWith("--lane="))?.split("=")[1];
|
|
538
704
|
var positional = args.slice(1).filter((arg) => !arg.startsWith("--"));
|
|
539
705
|
var dir = resolve(positional[0] ?? ".");
|
|
540
706
|
var usage = () => {
|
|
541
707
|
console.log(
|
|
542
|
-
"Usage: inixiative-config <check|sync|scan|train> [dir] [--preset=base|node|react] [--force] [--no-install]"
|
|
708
|
+
"Usage: inixiative-config <check|sync|scan|train> [dir] [--preset=base|node|react] [--lane=primitives|agentic] [--force] [--no-install] [--push]"
|
|
543
709
|
);
|
|
544
710
|
process.exit(2);
|
|
545
711
|
};
|
|
546
712
|
if (!["check", "sync", "scan", "train"].includes(command)) usage();
|
|
547
713
|
if (presetFlag && !["base", "node", "react"].includes(presetFlag)) usage();
|
|
714
|
+
if (laneFlag !== void 0 && !isLane(laneFlag)) usage();
|
|
715
|
+
var lanes = laneFlag && isLane(laneFlag) ? [laneFlag] : [...LANES];
|
|
548
716
|
var manifest = loadManifest();
|
|
549
717
|
var report = (findings) => {
|
|
550
718
|
for (const finding of findings) {
|
|
@@ -600,18 +768,21 @@ if (command === "check") {
|
|
|
600
768
|
const { findings } = inspect(dir, manifest, presetFlag);
|
|
601
769
|
process.exit(report(findings) > 0 ? 1 : 0);
|
|
602
770
|
}
|
|
771
|
+
var laneLabel = (checkout) => checkout.consumer ? `consumer of ${checkout.consumer.lanes.join(" + ")}` : `${checkout.lane} lane`;
|
|
603
772
|
if (command === "scan") {
|
|
604
|
-
const
|
|
605
|
-
|
|
773
|
+
const plan = trainPlan(dir, manifest, lanes);
|
|
774
|
+
const checkouts = [...plan.lanes.flatMap((lane) => lane.checkouts), ...plan.consumers];
|
|
775
|
+
if (checkouts.length === 0) {
|
|
606
776
|
console.error(`\u2717 no ecosystem repos found under ${dir}`);
|
|
607
777
|
process.exit(1);
|
|
608
778
|
}
|
|
609
|
-
const missing =
|
|
779
|
+
const missing = [...plan.lanes.flatMap((lane) => lane.missing), ...plan.missingConsumers];
|
|
780
|
+
const ambiguous = [...plan.lanes.flatMap((lane) => lane.ambiguous), ...plan.ambiguousConsumers];
|
|
610
781
|
const drifted = [];
|
|
611
|
-
for (const
|
|
782
|
+
for (const checkout of checkouts) {
|
|
612
783
|
console.log(`
|
|
613
|
-
${
|
|
614
|
-
const off = offDefaultBranch(
|
|
784
|
+
${checkout.name} \u2014 ${checkout.dir} (${laneLabel(checkout)})`);
|
|
785
|
+
const off = offDefaultBranch(checkout.dir);
|
|
615
786
|
const branchFindings = off ? [
|
|
616
787
|
{
|
|
617
788
|
level: "warn",
|
|
@@ -620,175 +791,280 @@ ${repo.name} \u2014 ${repo.dir}`);
|
|
|
620
791
|
] : [];
|
|
621
792
|
const findings = [
|
|
622
793
|
...branchFindings,
|
|
623
|
-
...staleCheckoutFindings(
|
|
624
|
-
...inspect(
|
|
794
|
+
...staleCheckoutFindings(checkout.dir),
|
|
795
|
+
...inspect(checkout.dir, manifest).findings
|
|
625
796
|
];
|
|
626
|
-
if (report(findings) > 0) drifted.push(
|
|
627
|
-
for (const branch of unmergedSessionBranches(
|
|
797
|
+
if (report(findings) > 0) drifted.push(checkout.name);
|
|
798
|
+
for (const branch of unmergedSessionBranches(checkout.dir)) {
|
|
628
799
|
console.log(`\u26A0 unmerged session branch: ${branch}`);
|
|
629
800
|
}
|
|
630
801
|
}
|
|
631
802
|
if (missing.length > 0) console.error(`
|
|
632
803
|
\u2717 no checkout found for: ${missing.join(", ")}`);
|
|
804
|
+
for (const entry of ambiguous) {
|
|
805
|
+
console.error(`\u2717 ambiguous checkouts for ${entry.key}: ${entry.dirs.join(", ")}`);
|
|
806
|
+
}
|
|
633
807
|
console.log(
|
|
634
808
|
`
|
|
635
|
-
scanned ${
|
|
809
|
+
scanned ${checkouts.length} repos (${lanes.join(" + ")}): ${drifted.length === 0 ? "all in sync" : `${drifted.length} drifted (${drifted.join(", ")})`}${missing.length > 0 ? `, ${missing.length} missing` : ""}${ambiguous.length > 0 ? `, ${ambiguous.length} ambiguous` : ""}`
|
|
636
810
|
);
|
|
637
|
-
process.exit(drifted.length > 0 || missing.length > 0 ? 1 : 0);
|
|
811
|
+
process.exit(drifted.length > 0 || missing.length > 0 || ambiguous.length > 0 ? 1 : 0);
|
|
638
812
|
}
|
|
813
|
+
var Abort = class extends Error {
|
|
814
|
+
};
|
|
639
815
|
if (command === "train") {
|
|
640
|
-
const order = topoOrder(dir, manifest);
|
|
641
|
-
if (order.length === 0) {
|
|
642
|
-
console.error(`\u2717 no ecosystem repos found under ${dir}`);
|
|
643
|
-
process.exit(1);
|
|
644
|
-
}
|
|
645
|
-
const missing = missingRepos(order, manifest);
|
|
646
|
-
if (missing.length > 0) {
|
|
647
|
-
console.error(
|
|
648
|
-
`\u2717 no checkout under ${dir} for: ${missing.join(", ")} \u2014 a partial train ships an incoherent set`
|
|
649
|
-
);
|
|
650
|
-
process.exit(1);
|
|
651
|
-
}
|
|
652
816
|
git(packageRoot, "fetch", "--quiet");
|
|
653
817
|
if (behindOrigin(packageRoot) > 0) {
|
|
654
818
|
console.error("\u2717 this config checkout is behind origin \u2014 its blessed set is stale; pull first");
|
|
655
819
|
process.exit(1);
|
|
656
820
|
}
|
|
657
|
-
|
|
658
|
-
|
|
821
|
+
const plan = trainPlan(dir, manifest, lanes);
|
|
822
|
+
if (plan.lanes.every((lane) => lane.checkouts.length === 0) && plan.consumers.length === 0) {
|
|
823
|
+
console.error(`\u2717 no ecosystem repos found under ${dir}`);
|
|
824
|
+
process.exit(1);
|
|
825
|
+
}
|
|
826
|
+
const blessed = [];
|
|
659
827
|
const skipped = [];
|
|
660
|
-
const
|
|
828
|
+
const failed = [];
|
|
829
|
+
const committed = /* @__PURE__ */ new Set();
|
|
830
|
+
const reviewBranches = [];
|
|
661
831
|
let bomDirty = false;
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
\u25B8 ${repo.name} \u2014 ${repo.dir}`);
|
|
665
|
-
const porcelain = spawnSync2("git", ["status", "--porcelain"], {
|
|
666
|
-
cwd: repo.dir,
|
|
667
|
-
encoding: "utf8"
|
|
668
|
-
});
|
|
832
|
+
const ready = (checkout) => {
|
|
833
|
+
const porcelain = git(checkout.dir, "status", "--porcelain");
|
|
669
834
|
if (porcelain.status !== 0) {
|
|
670
835
|
console.log("\u26A0 not a git checkout \u2014 skipping");
|
|
671
|
-
|
|
672
|
-
continue;
|
|
836
|
+
return false;
|
|
673
837
|
}
|
|
674
|
-
const off = offDefaultBranch(
|
|
838
|
+
const off = offDefaultBranch(checkout.dir);
|
|
675
839
|
if (off) {
|
|
676
840
|
console.log(
|
|
677
841
|
`\u26A0 on ${off.branch}, not ${off.main} \u2014 the train only ships the default branch, skipping`
|
|
678
842
|
);
|
|
679
|
-
|
|
680
|
-
continue;
|
|
843
|
+
return false;
|
|
681
844
|
}
|
|
682
845
|
if (porcelain.stdout.trim().length > 0) {
|
|
683
|
-
console.log("\u26A0 dirty working tree \u2014 commit
|
|
684
|
-
|
|
685
|
-
continue;
|
|
846
|
+
console.log("\u26A0 dirty working tree \u2014 commit your work first, skipping");
|
|
847
|
+
return false;
|
|
686
848
|
}
|
|
687
|
-
git(
|
|
688
|
-
for (const branch of unmergedSessionBranches(
|
|
849
|
+
git(checkout.dir, "fetch", "--quiet");
|
|
850
|
+
for (const branch of unmergedSessionBranches(checkout.dir)) {
|
|
689
851
|
console.log(`\u26A0 unmerged session branch: ${branch} \u2014 a bump may be hiding there`);
|
|
690
852
|
}
|
|
691
|
-
const behind = behindOrigin(
|
|
853
|
+
const behind = behindOrigin(checkout.dir);
|
|
692
854
|
if (behind > 0) {
|
|
693
|
-
const localOnly = git(
|
|
855
|
+
const localOnly = git(checkout.dir, "rev-list", "--count", "@{upstream}..HEAD");
|
|
694
856
|
if (localOnly.status === 0 && Number(localOnly.stdout.trim()) > 0) {
|
|
695
857
|
console.log("\u26A0 diverged from origin \u2014 reconcile first, skipping");
|
|
696
|
-
|
|
697
|
-
continue;
|
|
858
|
+
return false;
|
|
698
859
|
}
|
|
699
|
-
|
|
700
|
-
if (ff.status !== 0) {
|
|
860
|
+
if (git(checkout.dir, "merge", "--ff-only", "@{upstream}").status !== 0) {
|
|
701
861
|
console.log("\u26A0 fast-forward to origin failed \u2014 reconcile first, skipping");
|
|
702
|
-
|
|
703
|
-
continue;
|
|
862
|
+
return false;
|
|
704
863
|
}
|
|
705
864
|
console.log(`\u2193 fast-forwarded to origin (+${behind})`);
|
|
706
|
-
|
|
865
|
+
checkout.pkg = JSON.parse(readFileSync2(join2(checkout.dir, "package.json"), "utf8"));
|
|
707
866
|
}
|
|
708
|
-
|
|
867
|
+
return true;
|
|
868
|
+
};
|
|
869
|
+
const bumpRanges = (checkout) => {
|
|
870
|
+
const inspection = inspect(checkout.dir, manifest);
|
|
709
871
|
const bumps = inspection.findings.filter((finding) => finding.kind === "ecosystem-range");
|
|
710
872
|
for (const bump of bumps) {
|
|
711
873
|
console.log(` ${bump.message}`);
|
|
712
874
|
bump.fix?.();
|
|
713
875
|
}
|
|
714
876
|
inspection.flush();
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
877
|
+
return {
|
|
878
|
+
inspection,
|
|
879
|
+
bumped: bumps.length > 0,
|
|
880
|
+
staleLock: inspection.findings.some((finding) => finding.kind === "stale-lock")
|
|
881
|
+
};
|
|
882
|
+
};
|
|
883
|
+
const relockAndCheck = (checkout) => {
|
|
884
|
+
if (spawnSync2("bun", ["install"], { cwd: checkout.dir, stdio: "inherit" }).status !== 0) {
|
|
885
|
+
throw new Abort(`bun install failed in ${checkout.name}`);
|
|
886
|
+
}
|
|
887
|
+
const stale = inspect(checkout.dir, manifest).findings.filter((finding) => finding.kind === "stale-lock" && finding.name).map((finding) => finding.name);
|
|
888
|
+
if (stale.length > 0) {
|
|
889
|
+
spawnSync2("bun", ["update", ...new Set(stale)], { cwd: checkout.dir, stdio: "inherit" });
|
|
890
|
+
}
|
|
891
|
+
if (spawnSync2("bun", ["run", "check"], { cwd: checkout.dir, stdio: "inherit" }).status !== 0) {
|
|
892
|
+
throw new Abort(`check failed in ${checkout.name}`);
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
const commitOwned = (checkout, packagePaths) => {
|
|
896
|
+
const tracked = [...packagePaths, "bun.lock"];
|
|
897
|
+
const mutated = git(checkout.dir, "status", "--porcelain", "--", ...tracked);
|
|
898
|
+
if (mutated.status !== 0 || mutated.stdout.trim().length === 0) return false;
|
|
899
|
+
if (git(checkout.dir, "add", "--", ...tracked).status !== 0) {
|
|
900
|
+
throw new Abort(`git add failed in ${checkout.name} \u2014 is bun.lock ignored?`);
|
|
901
|
+
}
|
|
902
|
+
const commit = spawnSync2("git", ["commit", "-m", "chore: sync ecosystem deps to blessed set"], {
|
|
903
|
+
cwd: checkout.dir,
|
|
904
|
+
stdio: "inherit"
|
|
719
905
|
});
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
continue;
|
|
906
|
+
if (commit.status !== 0) throw new Abort(`commit failed in ${checkout.name}`);
|
|
907
|
+
return true;
|
|
908
|
+
};
|
|
909
|
+
const publish = (checkout, entry) => {
|
|
910
|
+
if (entry.path === "package.json") {
|
|
911
|
+
return spawnSync2("npm", ["publish"], { cwd: checkout.dir, stdio: "inherit" }).status === 0;
|
|
727
912
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
913
|
+
const memberDir = join2(checkout.dir, dirname2(entry.path));
|
|
914
|
+
const out = mkdtempSync(join2(tmpdir(), "inixiative-train-"));
|
|
915
|
+
try {
|
|
916
|
+
const pack = spawnSync2("bun", ["pm", "pack", "--destination", out], {
|
|
917
|
+
cwd: memberDir,
|
|
918
|
+
stdio: "inherit"
|
|
919
|
+
});
|
|
920
|
+
const tarball = readdirSync2(out).find((name) => name.endsWith(".tgz"));
|
|
921
|
+
if (pack.status !== 0 || !tarball) return false;
|
|
922
|
+
return spawnSync2("npm", ["publish", join2(out, tarball)], { cwd: memberDir, stdio: "inherit" }).status === 0;
|
|
923
|
+
} finally {
|
|
924
|
+
rmSync2(out, { recursive: true, force: true });
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
const npmLatest = (name) => {
|
|
928
|
+
const remote = spawnSync2("npm", ["view", `${name}@latest`, "version"], { encoding: "utf8" });
|
|
929
|
+
return remote.status === 0 ? remote.stdout.trim() : null;
|
|
930
|
+
};
|
|
931
|
+
for (const { lane, checkouts, missing, ambiguous } of plan.lanes) {
|
|
932
|
+
console.log(
|
|
933
|
+
`
|
|
934
|
+
\u2550 ${lane} lane: ${checkouts.map((checkout) => checkout.name).join(" \u2192 ") || "(none)"}`
|
|
935
|
+
);
|
|
936
|
+
const section = laneSection(manifest, lane);
|
|
937
|
+
const before = { ...section };
|
|
938
|
+
const laneBlessed = [];
|
|
939
|
+
try {
|
|
940
|
+
if (missing.length > 0) {
|
|
941
|
+
throw new Abort(
|
|
942
|
+
`no checkout under ${dir} for: ${missing.join(", ")} \u2014 a partial lane ships an incoherent set`
|
|
943
|
+
);
|
|
733
944
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
945
|
+
if (ambiguous.length > 0) {
|
|
946
|
+
throw new Abort(
|
|
947
|
+
ambiguous.map((entry) => `ambiguous checkouts for ${entry.key}: ${entry.dirs.join(", ")}`).join("; ")
|
|
948
|
+
);
|
|
737
949
|
}
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
950
|
+
for (const checkout of checkouts) {
|
|
951
|
+
console.log(`
|
|
952
|
+
\u25B8 ${checkout.name} \u2014 ${checkout.dir}`);
|
|
953
|
+
if (!ready(checkout)) {
|
|
954
|
+
skipped.push(checkout.name);
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
const { inspection, bumped, staleLock } = bumpRanges(checkout);
|
|
958
|
+
const releases = checkout.packages.map((entry) => {
|
|
959
|
+
const local = JSON.parse(readFileSync2(join2(checkout.dir, entry.path), "utf8")).version ?? "0.0.0";
|
|
960
|
+
const remote = npmLatest(entry.name);
|
|
961
|
+
return { entry, local, remote, ahead: remote === null || compare(local, remote) > 0 };
|
|
962
|
+
});
|
|
963
|
+
const behind = releases.find(
|
|
964
|
+
(release) => release.remote && compare(release.local, release.remote) < 0
|
|
965
|
+
);
|
|
966
|
+
if (behind) {
|
|
967
|
+
console.log(
|
|
968
|
+
`\u26A0 ${behind.entry.name} local ${behind.local} is behind npm ${behind.remote} \u2014 pull first, skipping`
|
|
969
|
+
);
|
|
970
|
+
skipped.push(checkout.name);
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
if (bumped || staleLock || releases.some((release) => release.ahead))
|
|
974
|
+
relockAndCheck(checkout);
|
|
975
|
+
if (commitOwned(checkout, inspection.packagePaths)) committed.add(checkout.dir);
|
|
976
|
+
for (const { entry, local, remote, ahead } of releases) {
|
|
977
|
+
if (ahead) {
|
|
978
|
+
console.log(`publishing ${entry.name}@${local} (npm has ${remote ?? "none"})`);
|
|
979
|
+
if (!publish(checkout, entry)) throw new Abort(`publish failed for ${entry.name}`);
|
|
980
|
+
if (!servedByRegistry(entry.name, local)) {
|
|
981
|
+
throw new Abort(
|
|
982
|
+
`npm accepted ${entry.name}@${local} but the registry does not serve it`
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
} else if (section[entry.name] === local) {
|
|
986
|
+
console.log(`\u2713 ${entry.name}@${local} already on npm and blessed`);
|
|
987
|
+
continue;
|
|
988
|
+
} else {
|
|
989
|
+
console.log(`\u2713 ${entry.name}@${local} already on npm \u2014 blessing it`);
|
|
990
|
+
}
|
|
991
|
+
section[entry.name] = local;
|
|
992
|
+
laneBlessed.push(`${entry.name}@${local}`);
|
|
993
|
+
}
|
|
742
994
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
spawnSync2("git", ["add", "--", ...tracked], { cwd: repo.dir });
|
|
751
|
-
const commit = spawnSync2(
|
|
752
|
-
"git",
|
|
753
|
-
["commit", "-m", "chore: sync ecosystem deps to blessed set"],
|
|
754
|
-
{ cwd: repo.dir, stdio: "inherit" }
|
|
995
|
+
if (laneBlessed.length > 0) {
|
|
996
|
+
writeManifest(manifest);
|
|
997
|
+
bomDirty = true;
|
|
998
|
+
blessed.push(...laneBlessed);
|
|
999
|
+
}
|
|
1000
|
+
console.log(
|
|
1001
|
+
`${lane} lane blessed: ${laneBlessed.length === 0 ? "nothing new" : laneBlessed.join(", ")}`
|
|
755
1002
|
);
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
if (!(error instanceof Abort)) throw error;
|
|
1005
|
+
for (const name of Object.keys(section)) delete section[name];
|
|
1006
|
+
Object.assign(section, before);
|
|
1007
|
+
failed.push(`${lane} lane`);
|
|
1008
|
+
console.error(`\u2717 ${error.message} \u2014 ${lane} lane stopped; its BOM entries are unchanged`);
|
|
1009
|
+
if (laneBlessed.length > 0) {
|
|
1010
|
+
console.error(
|
|
1011
|
+
` published but not blessed: ${laneBlessed.join(", ")} \u2014 the next train blesses them`
|
|
1012
|
+
);
|
|
759
1013
|
}
|
|
760
|
-
committed.push(repo.dir);
|
|
761
1014
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1015
|
+
}
|
|
1016
|
+
if (plan.consumers.length > 0 || plan.missingConsumers.length > 0) {
|
|
1017
|
+
console.log(
|
|
1018
|
+
`
|
|
1019
|
+
\u2550 consumers: ${plan.consumers.map((checkout) => checkout.name).join(" \u2192 ") || "(none)"}`
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
for (const repo of plan.missingConsumers) {
|
|
1023
|
+
console.error(`\u2717 no checkout under ${dir} for consumer ${repo}`);
|
|
1024
|
+
failed.push(repo);
|
|
1025
|
+
}
|
|
1026
|
+
for (const entry of plan.ambiguousConsumers) {
|
|
1027
|
+
console.error(`\u2717 ambiguous checkouts for consumer ${entry.key}: ${entry.dirs.join(", ")}`);
|
|
1028
|
+
failed.push(entry.key);
|
|
1029
|
+
}
|
|
1030
|
+
for (const checkout of plan.consumers) {
|
|
1031
|
+
console.log(`
|
|
1032
|
+
\u25B8 ${checkout.name} \u2014 ${checkout.dir} (${laneLabel(checkout)})`);
|
|
1033
|
+
if (!ready(checkout)) {
|
|
1034
|
+
skipped.push(checkout.name);
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
try {
|
|
1038
|
+
const { inspection, bumped, staleLock } = bumpRanges(checkout);
|
|
1039
|
+
if (!bumped && !staleLock) {
|
|
1040
|
+
console.log("\u2713 already on the blessed set");
|
|
1041
|
+
continue;
|
|
768
1042
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1043
|
+
relockAndCheck(checkout);
|
|
1044
|
+
const base = offDefaultBranch(checkout.dir)?.main ?? currentBranch(checkout.dir);
|
|
1045
|
+
const branch = freeBranch(
|
|
1046
|
+
checkout.dir,
|
|
1047
|
+
`train/ecosystem-sync-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`
|
|
1048
|
+
);
|
|
1049
|
+
if (git(checkout.dir, "switch", "-c", branch).status !== 0) {
|
|
1050
|
+
throw new Abort(`could not create ${branch} in ${checkout.name}`);
|
|
774
1051
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1052
|
+
if (commitOwned(checkout, inspection.packagePaths)) {
|
|
1053
|
+
reviewBranches.push({ dir: checkout.dir, branch, base });
|
|
1054
|
+
console.log(`committed on ${branch} for review; the checkout stays on it until merged`);
|
|
1055
|
+
}
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
if (!(error instanceof Abort)) throw error;
|
|
1058
|
+
failed.push(checkout.name);
|
|
1059
|
+
console.error(`\u2717 ${error.message} \u2014 its working tree is left as is for inspection`);
|
|
782
1060
|
}
|
|
783
1061
|
}
|
|
784
1062
|
console.log(`
|
|
785
|
-
|
|
1063
|
+
blessed: ${blessed.length === 0 ? "nothing" : blessed.join(", ")}`);
|
|
786
1064
|
if (skipped.length > 0) console.log(`skipped: ${skipped.join(", ")}`);
|
|
1065
|
+
if (failed.length > 0) console.log(`failed: ${failed.join(", ")}`);
|
|
787
1066
|
if (bomDirty) {
|
|
788
|
-
const
|
|
789
|
-
encoding: "utf8"
|
|
790
|
-
});
|
|
791
|
-
const remoteVersion = remote.status === 0 ? remote.stdout.trim() : null;
|
|
1067
|
+
const remoteVersion = npmLatest("@inixiative/config");
|
|
792
1068
|
let version = ownVersion();
|
|
793
1069
|
if (remoteVersion !== null && compare(version, remoteVersion) <= 0) {
|
|
794
1070
|
version = bumpPatch(remoteVersion);
|
|
@@ -816,38 +1092,59 @@ published: ${published.length === 0 ? "nothing" : published.join(", ")}`);
|
|
|
816
1092
|
});
|
|
817
1093
|
const commit = spawnSync2(
|
|
818
1094
|
"git",
|
|
819
|
-
["commit", "-m", `chore: bless ${
|
|
820
|
-
{
|
|
821
|
-
cwd: packageRoot,
|
|
822
|
-
stdio: "inherit"
|
|
823
|
-
}
|
|
1095
|
+
["commit", "-m", `chore: bless ${blessed.join(", ")} \u2014 ${version}`],
|
|
1096
|
+
{ cwd: packageRoot, stdio: "inherit" }
|
|
824
1097
|
);
|
|
825
1098
|
if (commit.status !== 0) {
|
|
826
1099
|
console.error("\u2717 commit failed in @inixiative/config \u2014 aborting before publish");
|
|
827
1100
|
process.exit(1);
|
|
828
1101
|
}
|
|
829
|
-
const
|
|
830
|
-
if (
|
|
1102
|
+
const publish2 = spawnSync2("npm", ["publish"], { cwd: packageRoot, stdio: "inherit" });
|
|
1103
|
+
if (publish2.status !== 0 || !servedByRegistry("@inixiative/config", version)) {
|
|
831
1104
|
console.error("\u2717 publish failed for @inixiative/config");
|
|
832
1105
|
process.exit(1);
|
|
833
1106
|
}
|
|
834
|
-
committed.
|
|
1107
|
+
committed.add(packageRoot);
|
|
835
1108
|
console.log(`published: @inixiative/config@${version} \u2014 the BOM names the new state`);
|
|
836
1109
|
}
|
|
837
1110
|
if (flags.has("--push")) {
|
|
838
1111
|
for (const repoDir of committed) {
|
|
839
|
-
|
|
840
|
-
if (push.status !== 0) {
|
|
1112
|
+
if (spawnSync2("git", ["push"], { cwd: repoDir, stdio: "inherit" }).status !== 0) {
|
|
841
1113
|
console.error(`\u2717 push failed in ${repoDir}`);
|
|
842
1114
|
process.exit(1);
|
|
843
1115
|
}
|
|
844
1116
|
}
|
|
845
|
-
|
|
846
|
-
|
|
1117
|
+
for (const { dir: repoDir, branch, base } of reviewBranches) {
|
|
1118
|
+
const push = spawnSync2("git", ["push", "-u", "origin", branch], {
|
|
1119
|
+
cwd: repoDir,
|
|
1120
|
+
stdio: "inherit"
|
|
1121
|
+
});
|
|
1122
|
+
const pr = push.status === 0 && spawnSync2("gh", ["pr", "create", "--fill", "--base", base, "--head", branch], {
|
|
1123
|
+
cwd: repoDir,
|
|
1124
|
+
stdio: "inherit"
|
|
1125
|
+
}).status === 0;
|
|
1126
|
+
if (!pr) console.error(`\u2717 could not push ${branch} and open its PR in ${repoDir}`);
|
|
1127
|
+
}
|
|
1128
|
+
console.log(`pushed: ${committed.size + reviewBranches.length} repos`);
|
|
1129
|
+
} else if (committed.size > 0 || reviewBranches.length > 0) {
|
|
847
1130
|
console.log("train does not push without --push \u2014 review its commits, then push:");
|
|
848
1131
|
for (const repoDir of committed) console.log(` git -C ${repoDir} push`);
|
|
1132
|
+
for (const { dir: repoDir, branch, base } of reviewBranches) {
|
|
1133
|
+
console.log(
|
|
1134
|
+
` git -C ${repoDir} push -u origin ${branch} && (cd ${repoDir} && gh pr create --fill --base ${base})`
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
849
1137
|
}
|
|
850
|
-
process.exit(0);
|
|
1138
|
+
process.exit(failed.length > 0 ? 1 : 0);
|
|
1139
|
+
}
|
|
1140
|
+
function currentBranch(cwd) {
|
|
1141
|
+
return git(cwd, "rev-parse", "--abbrev-ref", "HEAD").stdout.trim();
|
|
1142
|
+
}
|
|
1143
|
+
function freeBranch(cwd, name) {
|
|
1144
|
+
const taken = (candidate2) => git(cwd, "rev-parse", "--verify", "--quiet", `refs/heads/${candidate2}`).status === 0;
|
|
1145
|
+
let candidate = name;
|
|
1146
|
+
for (let n = 2; taken(candidate); n++) candidate = `${name}-${n}`;
|
|
1147
|
+
return candidate;
|
|
851
1148
|
}
|
|
852
1149
|
function servedByRegistry(name, version) {
|
|
853
1150
|
for (let attempt = 0; attempt < 10; attempt++) {
|
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.36",
|
|
17
17
|
"@inixiative/json-rules": "2.25.0",
|
|
18
18
|
"@inixiative/permissions": "0.3.2",
|
|
19
19
|
"@inixiative/transitions": "0.1.0",
|
|
@@ -22,5 +22,29 @@
|
|
|
22
22
|
"@inixiative/atlas": "0.1.1",
|
|
23
23
|
"@inixiative/gloss": "0.0.4",
|
|
24
24
|
"@inixiative/archive": "0.2.1"
|
|
25
|
+
},
|
|
26
|
+
"agentic": {
|
|
27
|
+
"@inixiative/agent-session": "0.2.0",
|
|
28
|
+
"@inixiative/foundry-core": "0.1.0",
|
|
29
|
+
"@inixiative/foundry": "0.1.0"
|
|
30
|
+
},
|
|
31
|
+
"consumers": {
|
|
32
|
+
"inixiative/template": {
|
|
33
|
+
"lanes": [
|
|
34
|
+
"primitives"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"inixiative/kingdom": {
|
|
38
|
+
"lanes": [
|
|
39
|
+
"primitives",
|
|
40
|
+
"agentic"
|
|
41
|
+
],
|
|
42
|
+
"upstream": "inixiative/template"
|
|
43
|
+
},
|
|
44
|
+
"inixiative/oracle": {
|
|
45
|
+
"lanes": [
|
|
46
|
+
"agentic"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
25
49
|
}
|
|
26
50
|
}
|