@inixiative/config 0.5.64 → 0.5.65
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 +1 -1
- package/dist/cli.js +166 -3
- package/package.json +1 -1
- package/versions.json +5 -5
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ bunx @inixiative/config ports [project]
|
|
|
77
77
|
|
|
78
78
|
`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, a clone directly under the root wins over one nested in a workspace folder, 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.
|
|
79
79
|
|
|
80
|
-
`train` is the release cascade, run from this repo's
|
|
80
|
+
`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
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 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 (up to 15 minutes, since npm holds new versions for processing) → 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.
|
|
83
83
|
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5
5
|
import { existsSync as existsSync2, mkdtempSync, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
6
|
-
import { tmpdir } from "os";
|
|
6
|
+
import { hostname as hostname2, tmpdir } from "os";
|
|
7
7
|
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/args.ts
|
|
@@ -11,15 +11,20 @@ import { parseArgs } from "util";
|
|
|
11
11
|
|
|
12
12
|
// src/lib.ts
|
|
13
13
|
import { spawnSync } from "child_process";
|
|
14
|
+
import { randomUUID } from "crypto";
|
|
14
15
|
import {
|
|
15
16
|
existsSync,
|
|
16
17
|
globSync,
|
|
18
|
+
linkSync,
|
|
19
|
+
mkdirSync,
|
|
17
20
|
readdirSync,
|
|
18
21
|
readFileSync,
|
|
22
|
+
realpathSync,
|
|
19
23
|
rmSync,
|
|
20
24
|
statSync,
|
|
21
25
|
writeFileSync
|
|
22
26
|
} from "fs";
|
|
27
|
+
import { homedir, hostname } from "os";
|
|
23
28
|
import { dirname, join, resolve, sep } from "path";
|
|
24
29
|
import { fileURLToPath } from "url";
|
|
25
30
|
var packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -806,6 +811,115 @@ function inspect(dir2, manifest2, presetOverride) {
|
|
|
806
811
|
}
|
|
807
812
|
};
|
|
808
813
|
}
|
|
814
|
+
var trainLockPath = () => join(homedir(), ".inixiative", "train.lock");
|
|
815
|
+
var isAlive = (pid) => {
|
|
816
|
+
try {
|
|
817
|
+
process.kill(pid, 0);
|
|
818
|
+
return true;
|
|
819
|
+
} catch (error) {
|
|
820
|
+
return error.code === "EPERM";
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
var processStart = (pid) => {
|
|
824
|
+
const ps = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf8" });
|
|
825
|
+
const start = ps.status === 0 ? ps.stdout.trim() : "";
|
|
826
|
+
return start === "" ? null : start;
|
|
827
|
+
};
|
|
828
|
+
var parseLock = (contents) => {
|
|
829
|
+
try {
|
|
830
|
+
const lock = JSON.parse(contents);
|
|
831
|
+
return lock && typeof lock === "object" && !Array.isArray(lock) && typeof lock.pid === "number" ? lock : null;
|
|
832
|
+
} catch {
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
var staleness = (contents, lock) => {
|
|
837
|
+
if (!lock) return { reason: "corrupt", contents };
|
|
838
|
+
if (lock.hostname !== hostname()) return null;
|
|
839
|
+
if (!isAlive(lock.pid)) return { reason: "gone", holder: lock };
|
|
840
|
+
const start = processStart(lock.pid);
|
|
841
|
+
if (lock.processStart && start && start !== lock.processStart) {
|
|
842
|
+
return { reason: "pid reused", holder: lock };
|
|
843
|
+
}
|
|
844
|
+
return null;
|
|
845
|
+
};
|
|
846
|
+
var readOrNull = (path) => {
|
|
847
|
+
try {
|
|
848
|
+
return readFileSync(path, "utf8");
|
|
849
|
+
} catch (error) {
|
|
850
|
+
if (error.code === "ENOENT") return null;
|
|
851
|
+
throw error;
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
var createExclusive = (path, contents) => {
|
|
855
|
+
const draft = `${path}.${process.pid}.${randomUUID()}`;
|
|
856
|
+
writeFileSync(draft, contents, { flag: "wx" });
|
|
857
|
+
try {
|
|
858
|
+
linkSync(draft, path);
|
|
859
|
+
return true;
|
|
860
|
+
} catch (error) {
|
|
861
|
+
if (error.code === "EEXIST") return false;
|
|
862
|
+
throw error;
|
|
863
|
+
} finally {
|
|
864
|
+
rmSync(draft, { force: true });
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
var TAKEOVER_TIMEOUT_MS = 1e4;
|
|
868
|
+
var removeStale = (path, stale) => {
|
|
869
|
+
const mutex = `${path}.takeover`;
|
|
870
|
+
if (!createExclusive(mutex, `${process.pid}
|
|
871
|
+
`)) {
|
|
872
|
+
try {
|
|
873
|
+
if (Date.now() - statSync(mutex).mtimeMs > TAKEOVER_TIMEOUT_MS)
|
|
874
|
+
rmSync(mutex, { force: true });
|
|
875
|
+
} catch {
|
|
876
|
+
}
|
|
877
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
try {
|
|
881
|
+
if (readOrNull(path) === stale) rmSync(path, { force: true });
|
|
882
|
+
} finally {
|
|
883
|
+
rmSync(mutex, { force: true });
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
var acquireTrainLock = (path, holder) => {
|
|
887
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
888
|
+
const mine = `${JSON.stringify(holder, null, 2)}
|
|
889
|
+
`;
|
|
890
|
+
let reclaimed;
|
|
891
|
+
for (; ; ) {
|
|
892
|
+
if (createExclusive(path, mine)) {
|
|
893
|
+
const release = () => {
|
|
894
|
+
if (readOrNull(path) === mine) rmSync(path, { force: true });
|
|
895
|
+
};
|
|
896
|
+
return reclaimed ? { ok: true, release, reclaimed } : { ok: true, release };
|
|
897
|
+
}
|
|
898
|
+
const current = readOrNull(path);
|
|
899
|
+
if (current === null) continue;
|
|
900
|
+
const lock = parseLock(current);
|
|
901
|
+
const stale = staleness(current, lock);
|
|
902
|
+
if (!stale && lock) return { ok: false, heldBy: lock, path };
|
|
903
|
+
if (!stale) continue;
|
|
904
|
+
reclaimed = stale;
|
|
905
|
+
removeStale(path, current);
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
var holdTrainLock = (release) => {
|
|
909
|
+
process.on("exit", release);
|
|
910
|
+
for (const [signal, code] of [
|
|
911
|
+
["SIGINT", 130],
|
|
912
|
+
["SIGTERM", 143],
|
|
913
|
+
["SIGHUP", 129]
|
|
914
|
+
]) {
|
|
915
|
+
process.on(signal, () => process.exit(code));
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
var isConfigCheckout = (dir2) => {
|
|
919
|
+
const top = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: dir2, encoding: "utf8" });
|
|
920
|
+
if (top.status !== 0) return false;
|
|
921
|
+
return realpathSync(top.stdout.trim()) === realpathSync(dir2) && originRepo(dir2) === "inixiative/config";
|
|
922
|
+
};
|
|
809
923
|
|
|
810
924
|
// src/args.ts
|
|
811
925
|
var COMMANDS = ["check", "sync", "scan", "train", "ports"];
|
|
@@ -828,17 +942,28 @@ var parseCommandLine = (argv) => {
|
|
|
828
942
|
if (!isCommand(command2)) throw new UsageError(`unknown command: ${command2 ?? "(none)"}`);
|
|
829
943
|
let values;
|
|
830
944
|
let positionals2;
|
|
945
|
+
let tokens;
|
|
831
946
|
try {
|
|
832
947
|
const options = OPTIONS[command2];
|
|
833
|
-
({ values, positionals: positionals2 } = parseArgs({
|
|
948
|
+
({ values, positionals: positionals2, tokens } = parseArgs({
|
|
834
949
|
args: argv.slice(1),
|
|
835
950
|
options,
|
|
836
951
|
allowPositionals: true,
|
|
837
|
-
strict: true
|
|
952
|
+
strict: true,
|
|
953
|
+
tokens: true
|
|
838
954
|
}));
|
|
839
955
|
} catch (error) {
|
|
840
956
|
throw new UsageError(`${command2}: ${error instanceof Error ? error.message : String(error)}`);
|
|
841
957
|
}
|
|
958
|
+
if (positionals2.length > 1)
|
|
959
|
+
throw new UsageError(
|
|
960
|
+
`${command2}: unexpected argument ${positionals2[1]} \u2014 did you mean --${positionals2[1]}?`
|
|
961
|
+
);
|
|
962
|
+
const names = tokens.flatMap(
|
|
963
|
+
(token) => token.kind === "option" && token.name ? [token.name] : []
|
|
964
|
+
);
|
|
965
|
+
const repeated = names.find((name, index) => names.indexOf(name) !== index);
|
|
966
|
+
if (repeated) throw new UsageError(`${command2}: --${repeated} given more than once`);
|
|
842
967
|
const presetValue = values.preset;
|
|
843
968
|
if (typeof presetValue === "string" && !PRESETS.includes(presetValue)) {
|
|
844
969
|
throw new UsageError(`unknown preset: ${presetValue}`);
|
|
@@ -1034,6 +1159,44 @@ if (command === "train") {
|
|
|
1034
1159
|
return planned;
|
|
1035
1160
|
};
|
|
1036
1161
|
if (dryRun) console.log("dry run: nothing is installed, written, committed, published or pushed");
|
|
1162
|
+
if (!isConfigCheckout(packageRoot)) {
|
|
1163
|
+
const message = `this config (${packageRoot}) is not a git checkout of inixiative/config \u2014 run the train from a clone, never a bunx or npm copy`;
|
|
1164
|
+
if (!dryRun) {
|
|
1165
|
+
console.error(`\u2717 ${message}`);
|
|
1166
|
+
process.exit(1);
|
|
1167
|
+
}
|
|
1168
|
+
console.log(`\u26A0 ${message}`);
|
|
1169
|
+
}
|
|
1170
|
+
if (!dryRun) {
|
|
1171
|
+
const path = trainLockPath();
|
|
1172
|
+
const lock = acquireTrainLock(path, {
|
|
1173
|
+
pid: process.pid,
|
|
1174
|
+
hostname: hostname2(),
|
|
1175
|
+
processStart: processStart(process.pid),
|
|
1176
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1177
|
+
lanes,
|
|
1178
|
+
root: dir,
|
|
1179
|
+
checkout: packageRoot
|
|
1180
|
+
});
|
|
1181
|
+
if (!lock.ok) {
|
|
1182
|
+
const { pid, hostname: host, startedAt, lanes: held, root, checkout } = lock.heldBy;
|
|
1183
|
+
console.error(
|
|
1184
|
+
`\u2717 another train is running (pid ${pid} on ${host}, lanes ${held.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
|
+
);
|
|
1186
|
+
process.exit(1);
|
|
1187
|
+
}
|
|
1188
|
+
const stale = lock.reclaimed;
|
|
1189
|
+
if (stale?.reason === "corrupt") {
|
|
1190
|
+
console.log(
|
|
1191
|
+
`\u26A0 took over a corrupt train lock at ${path} (unparsable: ${JSON.stringify(stale.contents)})`
|
|
1192
|
+
);
|
|
1193
|
+
} else if (stale) {
|
|
1194
|
+
console.log(
|
|
1195
|
+
`\u26A0 took over a stale train lock (pid ${stale.holder.pid} ${stale.reason === "gone" ? "is gone" : "now belongs to another process"})`
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
holdTrainLock(lock.release);
|
|
1199
|
+
}
|
|
1037
1200
|
git(packageRoot, "fetch", "--quiet");
|
|
1038
1201
|
if (behindOrigin(packageRoot) > 0) {
|
|
1039
1202
|
if (!dryRun) {
|
package/package.json
CHANGED
package/versions.json
CHANGED
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
"@types/bun"
|
|
14
14
|
],
|
|
15
15
|
"ecosystem": {
|
|
16
|
-
"@inixiative/config": "0.5.
|
|
17
|
-
"@inixiative/json-rules": "3.5.
|
|
16
|
+
"@inixiative/config": "0.5.65",
|
|
17
|
+
"@inixiative/json-rules": "3.5.1",
|
|
18
18
|
"@inixiative/permissions": "0.4.0",
|
|
19
|
-
"@inixiative/transitions": "0.
|
|
20
|
-
"@inixiative/rules-builder": "0.
|
|
21
|
-
"@inixiative/prisma-map": "0.3.
|
|
19
|
+
"@inixiative/transitions": "0.5.0",
|
|
20
|
+
"@inixiative/rules-builder": "0.31.0",
|
|
21
|
+
"@inixiative/prisma-map": "0.3.1",
|
|
22
22
|
"@inixiative/atlas": "0.2.0",
|
|
23
23
|
"@inixiative/gloss": "0.0.5"
|
|
24
24
|
},
|