0xray 4.0.1 → 4.0.2

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.
Files changed (55) hide show
  1. package/.xray/features.json +1 -1
  2. package/AGENTS-consumer.md +4 -4
  3. package/AGENTS.md +3 -3
  4. package/README.md +1 -1
  5. package/SKILLS.md +1 -1
  6. package/dist/AGENTS.md +3 -3
  7. package/dist/CHANGELOG.md +36 -0
  8. package/dist/README.md +1 -1
  9. package/dist/SKILLS.md +1 -1
  10. package/dist/cli/commands/foundry-mint-wear.d.ts +2 -0
  11. package/dist/cli/commands/foundry-mint-wear.js +14 -0
  12. package/dist/cli/commands/hermes-install.js +2 -0
  13. package/dist/cli/commands/openclaw-install.js +2 -0
  14. package/dist/cli/commands/opencode-install.js +2 -0
  15. package/dist/enforcement/enforcer-tools.d.ts +7 -0
  16. package/dist/enforcement/enforcer-tools.js +18 -109
  17. package/dist/integrations/grok/grok-cli.js +2 -0
  18. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  19. package/dist/processors/implementations/version-compliance-processor.d.ts +3 -34
  20. package/dist/processors/implementations/version-compliance-processor.js +8 -135
  21. package/package.json +15 -14
  22. package/scripts/foundry/CHANGELOG.md +34 -0
  23. package/scripts/foundry/LICENSE +21 -0
  24. package/scripts/foundry/README.md +67 -0
  25. package/scripts/foundry/ci-monitor.mjs +115 -0
  26. package/scripts/foundry/cli.js +2 -0
  27. package/scripts/foundry/cli.mjs +55 -0
  28. package/scripts/foundry/docs-build.mjs +38 -0
  29. package/scripts/foundry/hooks.mjs +36 -0
  30. package/scripts/foundry/mill-root.mjs +62 -0
  31. package/scripts/foundry/mint-suit.cjs +313 -0
  32. package/scripts/foundry/mint.mjs +33 -0
  33. package/scripts/foundry/package.json +53 -0
  34. package/scripts/foundry/reconcile-version.mjs +150 -0
  35. package/scripts/foundry/release-gate.mjs +100 -0
  36. package/scripts/foundry/release.mjs +266 -0
  37. package/scripts/foundry/validate-release-docs.mjs +316 -0
  38. package/scripts/foundry/version-manager.mjs +444 -0
  39. package/scripts/node/ci-cd-auto-fix.cjs +12 -257
  40. package/scripts/node/consumer-install-smoke.mjs +2 -0
  41. package/scripts/node/enforce-version-compliance.sh +5 -20
  42. package/scripts/node/enforce-version-compliance.ts +13 -113
  43. package/scripts/node/foundry-shim.mjs +19 -0
  44. package/scripts/node/github-actions-monitor.cjs +13 -18
  45. package/scripts/node/postinstall.cjs +36 -1
  46. package/scripts/node/reconcile-version.mjs +3 -142
  47. package/scripts/node/release-gate.mjs +3 -74
  48. package/scripts/node/release.mjs +3 -171
  49. package/scripts/node/validate-release-docs.mjs +3 -285
  50. package/scripts/node/version-manager.mjs +3 -738
  51. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  52. package/xray/features.json +1 -1
  53. package/scripts/node/release.js +0 -17
  54. package/scripts/node/sync-versions.mjs +0 -18
  55. package/scripts/node/universal-version-manager.js +0 -1043
@@ -0,0 +1,62 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const millDir = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ export function millPackageDir() {
8
+ return millDir;
9
+ }
10
+
11
+ export function millScript(basename) {
12
+ return path.join(millDir, basename);
13
+ }
14
+
15
+ /** Repo being milled: FOUNDRY_ROOT, else cwd. Not this package's __dirname. */
16
+ export function resolveMillRoot() {
17
+ const env = process.env.FOUNDRY_ROOT;
18
+ if (typeof env === "string" && env.trim()) {
19
+ return path.resolve(env.trim());
20
+ }
21
+ return process.cwd();
22
+ }
23
+
24
+ /**
25
+ * Git hook installer. Dogfood is `scripts/hooks` next to `scripts/foundry`.
26
+ * Published mill lives in `node_modules/@0xray/foundry` — sibling `@0xray/hooks`
27
+ * does not exist. Use the worn 0xray garment installer instead.
28
+ */
29
+ export function resolveHookInstaller(rootDir = resolveMillRoot(), millPkg = millPackageDir()) {
30
+ const candidates = [
31
+ path.join(millPkg, "..", "hooks", "install-hooks.cjs"),
32
+ path.join(rootDir, "node_modules", "0xray", "scripts", "hooks", "install-hooks.cjs"),
33
+ path.join(rootDir, "scripts", "hooks", "install-hooks.cjs"),
34
+ ];
35
+ return candidates.find((p) => fs.existsSync(p)) ?? null;
36
+ }
37
+
38
+ export function readRootPackage(rootDir = resolveMillRoot()) {
39
+ const pkgPath = path.join(rootDir, "package.json");
40
+ if (!fs.existsSync(pkgPath)) {
41
+ return { name: null, version: null, scripts: {} };
42
+ }
43
+ try {
44
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
45
+ return {
46
+ name: typeof pkg.name === "string" ? pkg.name : null,
47
+ version: typeof pkg.version === "string" ? pkg.version : null,
48
+ scripts: pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {},
49
+ };
50
+ } catch {
51
+ return { name: null, version: null, scripts: {} };
52
+ }
53
+ }
54
+
55
+ export function hasDocsSite(rootDir = resolveMillRoot()) {
56
+ return fs.existsSync(path.join(rootDir, "docs-site"));
57
+ }
58
+
59
+ export function isXrayExoRepo(rootDir = resolveMillRoot()) {
60
+ const pkg = readRootPackage(rootDir);
61
+ return pkg.name === "0xray" && hasDocsSite(rootDir);
62
+ }
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Mill overlay: their plant onto the hanger. Not PPE (evaluatePreToolGate).
3
+ *
4
+ * Default SSOT (override with foundry.json or .xray/foundry.json):
5
+ * xray/codex.json, xray/features.json, xray/config.json,
6
+ * src/skills/<name>/SKILL.md, src/opencode/agents/*.yml
7
+ */
8
+
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+
12
+ const DEFAULT_PARAMS = {
13
+ codex: "xray/codex.json",
14
+ features: "xray/features.json",
15
+ config: "xray/config.json",
16
+ skills: "src/skills",
17
+ agents: "src/opencode/agents",
18
+ agentsCard: "xray/AGENTS.md",
19
+ };
20
+
21
+ function deepMerge(src, dest) {
22
+ if (typeof src !== "object" || src === null) return dest !== undefined ? dest : src;
23
+ if (Array.isArray(src)) return Array.isArray(dest) ? dest : src;
24
+ const result = {};
25
+ for (const key of Object.keys(src)) {
26
+ result[key] =
27
+ dest && typeof dest[key] !== "undefined" ? deepMerge(src[key], dest[key]) : src[key];
28
+ }
29
+ if (dest && typeof dest === "object") {
30
+ for (const key of Object.keys(dest)) {
31
+ if (!(key in src)) result[key] = dest[key];
32
+ }
33
+ }
34
+ return result;
35
+ }
36
+
37
+ function readPackageIdentity(pkgPath) {
38
+ if (!fs.existsSync(pkgPath)) return { name: null, version: null };
39
+ try {
40
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
41
+ return {
42
+ name: typeof pkg.name === "string" ? pkg.name : null,
43
+ version: typeof pkg.version === "string" ? pkg.version : null,
44
+ };
45
+ } catch {
46
+ return { name: null, version: null };
47
+ }
48
+ }
49
+
50
+ function isExoRepo(dir) {
51
+ try {
52
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
53
+ return pkg.name === "0xray" && fs.existsSync(path.join(dir, "docs-site"));
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ function isDogfood(millPackageRoot, targetDir) {
60
+ if (path.resolve(millPackageRoot) === path.resolve(targetDir)) return true;
61
+ return isExoRepo(targetDir);
62
+ }
63
+
64
+ /** Rel must stay inside targetDir. Absolute and `..` escapes return null. */
65
+ function resolveInside(targetDir, rel) {
66
+ if (typeof rel !== "string" || !rel.trim()) return null;
67
+ if (path.isAbsolute(rel)) return null;
68
+ const rootAbs = path.resolve(targetDir);
69
+ const resolved = path.resolve(rootAbs, rel);
70
+ const prefix = rootAbs.endsWith(path.sep) ? rootAbs : `${rootAbs}${path.sep}`;
71
+ if (resolved !== rootAbs && !resolved.startsWith(prefix)) return null;
72
+ return resolved;
73
+ }
74
+
75
+ function loadFoundryParams(targetDir) {
76
+ const candidates = [
77
+ path.join(targetDir, "foundry.json"),
78
+ path.join(targetDir, ".xray", "foundry.json"),
79
+ ];
80
+ let extra = {};
81
+ for (const candidate of candidates) {
82
+ if (!fs.existsSync(candidate)) continue;
83
+ try {
84
+ const parsed = JSON.parse(fs.readFileSync(candidate, "utf8"));
85
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) extra = parsed;
86
+ } catch {
87
+ extra = {};
88
+ }
89
+ break;
90
+ }
91
+ const params = { ...DEFAULT_PARAMS };
92
+ for (const key of ["codex", "features", "config", "skills", "agents", "agentsCard"]) {
93
+ if (typeof extra[key] === "string" && extra[key].trim()) params[key] = extra[key].trim();
94
+ }
95
+ return params;
96
+ }
97
+
98
+ function isDirectory(p) {
99
+ try {
100
+ return Boolean(p) && fs.existsSync(p) && fs.statSync(p).isDirectory();
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ /** Project skill hangers only. */
107
+ function listSkillHangers(targetDir) {
108
+ const dests = [path.join(targetDir, ".opencode", "skills")];
109
+ const projectGrok = path.join(targetDir, ".grok", "plugins", "0xray", "skills");
110
+ if (isDirectory(projectGrok)) dests.push(projectGrok);
111
+ return dests;
112
+ }
113
+
114
+ function overlayJsonFacet(src, dest) {
115
+ if (!src || !fs.existsSync(src) || !fs.statSync(src).isFile()) return false;
116
+ if (path.resolve(src) === path.resolve(dest)) return false;
117
+ let theirs;
118
+ try {
119
+ theirs = JSON.parse(fs.readFileSync(src, "utf8"));
120
+ } catch {
121
+ return false;
122
+ }
123
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
124
+ if (!fs.existsSync(dest)) {
125
+ fs.writeFileSync(dest, `${JSON.stringify(theirs, null, 2)}\n`);
126
+ return true;
127
+ }
128
+ try {
129
+ const mill = JSON.parse(fs.readFileSync(dest, "utf8"));
130
+ const merged = deepMerge(mill, theirs);
131
+ fs.writeFileSync(dest, `${JSON.stringify(merged, null, 2)}\n`);
132
+ return true;
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
137
+
138
+ function listConsumerSkillNames(targetDir, skillsRel) {
139
+ const rel = skillsRel || DEFAULT_PARAMS.skills;
140
+ const skillsSrc = resolveInside(targetDir, rel);
141
+ if (!isDirectory(skillsSrc)) return [];
142
+ const names = [];
143
+ for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
144
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
145
+ const skillMd = path.join(skillsSrc, entry.name, "SKILL.md");
146
+ if (!fs.existsSync(skillMd)) continue;
147
+ names.push(entry.name);
148
+ }
149
+ return names;
150
+ }
151
+
152
+ function listConsumerAgentFiles(targetDir, agentsRel) {
153
+ const rel = agentsRel || DEFAULT_PARAMS.agents;
154
+ const agentsSrc = resolveInside(targetDir, rel);
155
+ if (!isDirectory(agentsSrc)) return [];
156
+ return fs
157
+ .readdirSync(agentsSrc, { withFileTypes: true })
158
+ .filter(
159
+ (entry) =>
160
+ entry.isFile() && (entry.name.endsWith(".yml") || entry.name.endsWith(".yaml")),
161
+ )
162
+ .map((entry) => entry.name);
163
+ }
164
+
165
+ const MANAGED_AGENTS_MARKER = "<!-- 0xray-managed -->";
166
+
167
+ function overlayAgentsCard(targetDir, params) {
168
+ const src = resolveInside(targetDir, params.agentsCard || DEFAULT_PARAMS.agentsCard);
169
+ if (!src || !fs.existsSync(src) || !fs.statSync(src).isFile()) return false;
170
+ const dest = path.join(targetDir, "AGENTS.md");
171
+ if (path.resolve(src) === path.resolve(dest)) return false;
172
+ if (fs.existsSync(dest) && !fs.readFileSync(dest, "utf8").includes(MANAGED_AGENTS_MARKER)) {
173
+ return false;
174
+ }
175
+ fs.copyFileSync(src, dest);
176
+ return true;
177
+ }
178
+
179
+ function overlayConsumerTree(targetDir, log, params) {
180
+ const resolved = params || loadFoundryParams(targetDir);
181
+ const skillsRel = resolved.skills;
182
+ const agentsRel = resolved.agents;
183
+ const skills = listConsumerSkillNames(targetDir, skillsRel);
184
+ const agents = listConsumerAgentFiles(targetDir, agentsRel);
185
+ const skillsSrc = resolveInside(targetDir, skillsRel);
186
+ const agentsSrc = resolveInside(targetDir, agentsRel);
187
+ const skillHangers = listSkillHangers(targetDir);
188
+ const agentsDest = path.join(targetDir, ".opencode", "agents");
189
+
190
+ for (const hanger of skillHangers) {
191
+ for (const name of skills) {
192
+ const src = path.join(skillsSrc, name, "SKILL.md");
193
+ const destMd = path.join(hanger, name, "SKILL.md");
194
+ if (path.resolve(src) === path.resolve(destMd)) continue;
195
+ fs.mkdirSync(path.dirname(destMd), { recursive: true });
196
+ fs.copyFileSync(src, destMd);
197
+ }
198
+ }
199
+ for (const file of agents) {
200
+ const src = path.join(agentsSrc, file);
201
+ const dest = path.join(agentsDest, file);
202
+ if (path.resolve(src) === path.resolve(dest)) continue;
203
+ fs.mkdirSync(agentsDest, { recursive: true });
204
+ fs.copyFileSync(src, dest);
205
+ }
206
+
207
+ if (log && (skills.length > 0 || agents.length > 0)) {
208
+ log("foundry-mint", "Overlaid consumer skills/agents onto hangers", "info", {
209
+ skills: skills.length,
210
+ agents: agents.length,
211
+ skillHangers: skillHangers.length,
212
+ });
213
+ }
214
+ return { skills, agents, params: resolved };
215
+ }
216
+
217
+ function mintConsumerFromSsot(packageRoot, targetDir, log, tree) {
218
+ if (isDogfood(packageRoot, targetDir)) {
219
+ return { garment: "dogfood", skipped: true, consumer: readPackageIdentity(path.join(targetDir, "package.json")) };
220
+ }
221
+ const mill = readPackageIdentity(path.join(packageRoot, "package.json"));
222
+ const consumer = readPackageIdentity(path.join(targetDir, "package.json"));
223
+ const skills = Array.isArray(tree?.skills) ? tree.skills : [];
224
+ const agents = Array.isArray(tree?.agents) ? tree.agents : [];
225
+ const constitution = Boolean(tree?.codex);
226
+ const features = Boolean(tree?.features);
227
+ const config = Boolean(tree?.config);
228
+ const agentsCard = Boolean(tree?.agentsCard);
229
+ const overlayed =
230
+ skills.length > 0 ||
231
+ agents.length > 0 ||
232
+ constitution ||
233
+ features ||
234
+ config ||
235
+ agentsCard;
236
+ const garment = overlayed ? "overlay" : "copied-onto-hanger";
237
+ const inventory = {
238
+ mill: { name: mill.name || "@0xray/foundry", version: mill.version },
239
+ consumer: { name: consumer.name, version: consumer.version },
240
+ garment,
241
+ params: tree?.params || loadFoundryParams(targetDir),
242
+ tree: { skills, agents },
243
+ facets: {
244
+ constitution,
245
+ features,
246
+ config,
247
+ agentsCard,
248
+ skills,
249
+ agents,
250
+ },
251
+ mintedAt: new Date().toISOString(),
252
+ };
253
+ const xrayDir = path.join(targetDir, ".xray");
254
+ if (!fs.existsSync(xrayDir)) fs.mkdirSync(xrayDir, { recursive: true });
255
+ fs.writeFileSync(
256
+ path.join(xrayDir, "foundry-inventory.json"),
257
+ `${JSON.stringify(inventory, null, 2)}\n`,
258
+ );
259
+ if (log) {
260
+ log("foundry-mint", "Minted foundry-inventory from consumer mill SSOT", "info", {
261
+ consumer: consumer.name,
262
+ version: consumer.version,
263
+ garment,
264
+ });
265
+ }
266
+ return inventory;
267
+ }
268
+
269
+ function mintConsumerSuit(millPackageRoot, targetDir, log) {
270
+ if (isDogfood(millPackageRoot, targetDir)) {
271
+ return { garment: "dogfood", skipped: true };
272
+ }
273
+ const params = loadFoundryParams(targetDir);
274
+ const tree = overlayConsumerTree(targetDir, log, params);
275
+ tree.codex = overlayJsonFacet(
276
+ resolveInside(targetDir, params.codex),
277
+ path.join(targetDir, ".xray", "codex.json"),
278
+ );
279
+ tree.features = overlayJsonFacet(
280
+ resolveInside(targetDir, params.features),
281
+ path.join(targetDir, ".xray", "features.json"),
282
+ );
283
+ tree.config = overlayJsonFacet(
284
+ resolveInside(targetDir, params.config),
285
+ path.join(targetDir, ".xray", "config.json"),
286
+ );
287
+ tree.agentsCard = overlayAgentsCard(targetDir, params);
288
+ if (log && (tree.codex || tree.features || tree.config)) {
289
+ log("foundry-mint", "Overlaid consumer suit facets onto .xray", "info", {
290
+ constitution: tree.codex,
291
+ features: tree.features,
292
+ config: tree.config,
293
+ });
294
+ }
295
+ return mintConsumerFromSsot(millPackageRoot, targetDir, log, tree);
296
+ }
297
+
298
+ module.exports = {
299
+ DEFAULT_PARAMS,
300
+ deepMerge,
301
+ loadFoundryParams,
302
+ resolveInside,
303
+ overlayJsonFacet,
304
+ overlayAgentsCard,
305
+ overlayConsumerTree,
306
+ listConsumerSkillNames,
307
+ listConsumerAgentFiles,
308
+ listSkillHangers,
309
+ mintConsumerFromSsot,
310
+ mintConsumerSuit,
311
+ isDogfood,
312
+ readPackageIdentity,
313
+ };
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Overlay their mill SSOT onto the hanger (cwd / FOUNDRY_ROOT).
4
+ * Not a fifth 0xray CLI. Not PPE.
5
+ */
6
+
7
+ import { createRequire } from "node:module";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { millPackageDir, resolveMillRoot } from "./mill-root.mjs";
11
+
12
+ const require = createRequire(import.meta.url);
13
+ const { mintConsumerSuit } = require("./mint-suit.cjs");
14
+
15
+ function main() {
16
+ const target = resolveMillRoot();
17
+ const millRoot = millPackageDir();
18
+ const inventory = mintConsumerSuit(millRoot, target, (component, action, status) => {
19
+ process.stdout.write(`${component} ${action} ${status}\n`);
20
+ });
21
+ if (inventory.skipped) {
22
+ process.stderr.write("mint: skipped dogfood / 0xray exo plant (not a consumer hanger)\n");
23
+ process.exit(0);
24
+ }
25
+ process.stdout.write(`${JSON.stringify(inventory, null, 2)}\n`);
26
+ }
27
+
28
+ const isMain =
29
+ process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
30
+
31
+ if (isMain) {
32
+ main();
33
+ }
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@0xray/foundry",
3
+ "version": "0.1.4",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "0xRay mill: mint their suit params, stamp, gate, publish, CI report, git hooks. Not the worn exo.",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/0xRayAI/xray.git",
11
+ "directory": "scripts/foundry"
12
+ },
13
+ "homepage": "https://github.com/0xRayAI/xray/tree/main/scripts/foundry",
14
+ "bugs": {
15
+ "url": "https://github.com/0xRayAI/xray/issues"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "bin": {
21
+ "0xray-foundry": "cli.js"
22
+ },
23
+ "files": [
24
+ "*.mjs",
25
+ "*.cjs",
26
+ "cli.js",
27
+ "README.md",
28
+ "LICENSE",
29
+ "CHANGELOG.md"
30
+ ],
31
+ "exports": {
32
+ "./version-manager": "./version-manager.mjs",
33
+ "./validate-release-docs": "./validate-release-docs.mjs",
34
+ "./reconcile-version": "./reconcile-version.mjs",
35
+ "./release": "./release.mjs",
36
+ "./release-gate": "./release-gate.mjs",
37
+ "./mill-root": "./mill-root.mjs",
38
+ "./mint-suit": "./mint-suit.cjs",
39
+ "./ci-monitor": "./ci-monitor.mjs",
40
+ "./hooks": "./hooks.mjs",
41
+ "./docs-build": "./docs-build.mjs"
42
+ },
43
+ "engines": {
44
+ "node": ">=18"
45
+ },
46
+ "keywords": [
47
+ "0xray",
48
+ "foundry",
49
+ "mill",
50
+ "release",
51
+ "changelog"
52
+ ]
53
+ }
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * reconcile-version.mjs — Single version truth before bump/tag/publish.
4
+ *
5
+ * Sources: npm registry, package.json, latest git tag.
6
+ *
7
+ * Usage:
8
+ * npx @0xray/foundry reconcile
9
+ * npx @0xray/foundry reconcile --check
10
+ * npx @0xray/foundry reconcile patch
11
+ * npx @0xray/foundry reconcile patch --apply
12
+ */
13
+
14
+ import { execSync } from "child_process";
15
+ import fs from "fs";
16
+ import path from "path";
17
+ import { fileURLToPath } from "url";
18
+ import { readRootPackage, resolveMillRoot } from "./mill-root.mjs";
19
+
20
+ const rootDir = resolveMillRoot();
21
+ const pkgPath = path.join(rootDir, "package.json");
22
+
23
+ const args = process.argv.slice(2);
24
+ const checkMode = args.includes("--check");
25
+ const applyMode = args.includes("--apply");
26
+ const printTarget = args.includes("--print-target");
27
+ const bumpType = args.find((a) => ["major", "minor", "patch"].includes(a)) || null;
28
+
29
+ function run(cmd) {
30
+ try {
31
+ return execSync(cmd, { cwd: rootDir, encoding: "utf-8" }).trim();
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ function readLocalVersion() {
38
+ return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
39
+ }
40
+
41
+ function readPublishedVersion() {
42
+ const name = readRootPackage(rootDir).name;
43
+ if (!name) return "0.0.0";
44
+ const v = run(`npm view ${name} version`);
45
+ return v || "0.0.0";
46
+ }
47
+
48
+ function readLatestTag() {
49
+ const tag = run('git describe --tags --abbrev=0 2>/dev/null');
50
+ if (!tag) return null;
51
+ return tag.replace(/^v/, "");
52
+ }
53
+
54
+ function parseVersion(version) {
55
+ const [major, minor, patch] = version.split(".").map((n) => parseInt(n, 10));
56
+ return { major, minor, patch };
57
+ }
58
+
59
+ function bumpVersion(version, type) {
60
+ const v = parseVersion(version);
61
+ switch (type) {
62
+ case "major":
63
+ return `${v.major + 1}.0.0`;
64
+ case "minor":
65
+ return `${v.major}.${v.minor + 1}.0`;
66
+ case "patch":
67
+ return `${v.major}.${v.minor}.${v.patch + 1}`;
68
+ default:
69
+ return version;
70
+ }
71
+ }
72
+
73
+ function compare(a, b) {
74
+ const va = parseVersion(a);
75
+ const vb = parseVersion(b);
76
+ if (va.major !== vb.major) return va.major - vb.major;
77
+ if (va.minor !== vb.minor) return va.minor - vb.minor;
78
+ return va.patch - vb.patch;
79
+ }
80
+
81
+ function maxVersion(...versions) {
82
+ return versions.reduce((best, cur) => (compare(cur, best) > 0 ? cur : best));
83
+ }
84
+
85
+ function fail(msg) {
86
+ console.error(`❌ ${msg}`);
87
+ process.exit(1);
88
+ }
89
+
90
+ function main() {
91
+ const local = readLocalVersion();
92
+ const npm = readPublishedVersion();
93
+ const tag = readLatestTag();
94
+
95
+ const baseline = maxVersion(npm, tag || "0.0.0");
96
+ const target = bumpType ? bumpVersion(baseline, bumpType) : local;
97
+
98
+ if (printTarget && bumpType) {
99
+ console.log(target);
100
+ return;
101
+ }
102
+
103
+ console.log("\n=== Version Reconcile ===\n");
104
+ console.log(` npm registry : ${npm}`);
105
+ console.log(` package.json : ${local}`);
106
+ console.log(` latest tag : ${tag ? `v${tag}` : "(none)"}`);
107
+ if (bumpType) {
108
+ console.log(` bump ${bumpType} : ${target} (from baseline ${baseline})`);
109
+ }
110
+
111
+ if (applyMode && bumpType) {
112
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
113
+ pkg.version = target;
114
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
115
+ console.log(`\n✅ Updated package.json → ${target}`);
116
+ }
117
+
118
+ if (!checkMode) {
119
+ if (compare(local, npm) <= 0 && !bumpType) {
120
+ console.log("\n⚠️ Local version is not ahead of npm — bump before publishing.");
121
+ } else {
122
+ console.log("\n✅ Reconcile report complete.");
123
+ }
124
+ return;
125
+ }
126
+
127
+ // Strict checks for release gate / pre-tag
128
+ if (compare(local, npm) <= 0) {
129
+ fail(`package.json (${local}) must be > npm (${npm}). Run: npx @0xray/foundry reconcile patch --apply`);
130
+ }
131
+
132
+ if (tag && compare(local, tag) < 0) {
133
+ fail(`package.json (${local}) is behind latest tag v${tag}`);
134
+ }
135
+
136
+ if (tag && compare(local, tag) > 0 && compare(npm, tag) === 0) {
137
+ console.log(`✅ Ready to tag v${local} (npm at ${npm})`);
138
+ } else if (compare(local, npm) > 0) {
139
+ console.log(`✅ Ready to publish ${local} (npm at ${npm})`);
140
+ } else {
141
+ console.log("✅ Version reconcile OK");
142
+ }
143
+ }
144
+
145
+ const isMain =
146
+ process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
147
+
148
+ if (isMain) {
149
+ main();
150
+ }