0xray 4.0.0 → 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.
- package/.mcp.json +15 -55
- package/.xray/features.json +2 -2
- package/AGENTS-consumer.md +6 -6
- package/AGENTS.md +9 -9
- package/README.md +19 -19
- package/SKILLS.md +2 -2
- package/dist/AGENTS.md +9 -9
- package/dist/CHANGELOG.md +54 -2
- package/dist/README.md +19 -19
- package/dist/SKILLS.md +2 -2
- package/dist/cli/commands/foundry-mint-wear.d.ts +2 -0
- package/dist/cli/commands/foundry-mint-wear.js +14 -0
- package/dist/cli/commands/hermes-install.js +2 -0
- package/dist/cli/commands/openclaw-install.js +2 -0
- package/dist/cli/commands/opencode-install.js +2 -0
- package/dist/cli/index.js +10 -1
- package/dist/enforcement/enforcer-tools.d.ts +7 -0
- package/dist/enforcement/enforcer-tools.js +18 -109
- package/dist/integrations/grok/grok-cli.js +2 -0
- package/dist/integrations/hermes-agent/bridge.mjs +7 -0
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +4 -2
- package/dist/processors/implementations/version-compliance-processor.d.ts +3 -34
- package/dist/processors/implementations/version-compliance-processor.js +8 -135
- package/opencode.json +0 -8
- package/package.json +15 -14
- package/scripts/foundry/CHANGELOG.md +34 -0
- package/scripts/foundry/LICENSE +21 -0
- package/scripts/foundry/README.md +67 -0
- package/scripts/foundry/ci-monitor.mjs +115 -0
- package/scripts/foundry/cli.js +2 -0
- package/scripts/foundry/cli.mjs +55 -0
- package/scripts/foundry/docs-build.mjs +38 -0
- package/scripts/foundry/hooks.mjs +36 -0
- package/scripts/foundry/mill-root.mjs +62 -0
- package/scripts/foundry/mint-suit.cjs +313 -0
- package/scripts/foundry/mint.mjs +33 -0
- package/scripts/foundry/package.json +53 -0
- package/scripts/foundry/reconcile-version.mjs +150 -0
- package/scripts/foundry/release-gate.mjs +100 -0
- package/scripts/foundry/release.mjs +266 -0
- package/scripts/foundry/validate-release-docs.mjs +316 -0
- package/scripts/foundry/version-manager.mjs +444 -0
- package/scripts/node/ci-cd-auto-fix.cjs +12 -257
- package/scripts/node/consumer-install-smoke.mjs +57 -2
- package/scripts/node/enforce-version-compliance.sh +5 -20
- package/scripts/node/enforce-version-compliance.ts +13 -113
- package/scripts/node/foundry-shim.mjs +19 -0
- package/scripts/node/github-actions-monitor.cjs +13 -18
- package/scripts/node/postinstall.cjs +36 -1
- package/scripts/node/reconcile-version.mjs +3 -146
- package/scripts/node/release-gate.mjs +3 -74
- package/scripts/node/release.mjs +3 -171
- package/scripts/node/validate-release-docs.mjs +3 -285
- package/scripts/node/version-manager.mjs +3 -708
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +4 -2
- package/xray/features.json +2 -2
- package/scripts/node/release.js +0 -17
- package/scripts/node/sync-versions.mjs +0 -18
- package/scripts/node/universal-version-manager.js +0 -1043
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Mill CI monitor. Reports GitHub Actions for the milled cwd.
|
|
4
|
+
* Not PPE. Not an auto-push bot.
|
|
5
|
+
*
|
|
6
|
+
* npx @0xray/foundry ci [--commit SHA] [--report]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { resolveMillRoot } from "./mill-root.mjs";
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const commitIdx = args.indexOf("--commit");
|
|
16
|
+
const shaArg = commitIdx >= 0 ? args[commitIdx + 1] : "";
|
|
17
|
+
const sha = (shaArg && !shaArg.startsWith("-") ? shaArg : process.env.GITHUB_SHA || "").trim();
|
|
18
|
+
const root = resolveMillRoot();
|
|
19
|
+
const reportPath = path.join(root, ".opencode", "logs", "ci-cd-monitor-report.json");
|
|
20
|
+
|
|
21
|
+
function writeReport(report) {
|
|
22
|
+
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
23
|
+
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function emptyReport(status, reason) {
|
|
27
|
+
return {
|
|
28
|
+
ci_status: status,
|
|
29
|
+
health_score: status === "healthy" || status === "unknown" ? 100 : 0,
|
|
30
|
+
issues: [],
|
|
31
|
+
reason,
|
|
32
|
+
sha: sha || null,
|
|
33
|
+
generatedAt: new Date().toISOString(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || "";
|
|
39
|
+
const repo = process.env.GITHUB_REPOSITORY || "";
|
|
40
|
+
if (!token || !repo.includes("/")) {
|
|
41
|
+
const report = emptyReport(
|
|
42
|
+
"unknown",
|
|
43
|
+
"no GITHUB_TOKEN or GITHUB_REPOSITORY — mill CI monitor skipped",
|
|
44
|
+
);
|
|
45
|
+
writeReport(report);
|
|
46
|
+
process.stdout.write("SUCCESS: mill CI monitor skipped (no GitHub token)\n");
|
|
47
|
+
process.stdout.write("No failed jobs\n");
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const url = new URL(`https://api.github.com/repos/${repo}/actions/runs`);
|
|
52
|
+
url.searchParams.set("per_page", "50");
|
|
53
|
+
if (sha) url.searchParams.set("head_sha", sha);
|
|
54
|
+
|
|
55
|
+
const res = await fetch(url, {
|
|
56
|
+
headers: {
|
|
57
|
+
Authorization: `Bearer ${token}`,
|
|
58
|
+
Accept: "application/vnd.github+json",
|
|
59
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
60
|
+
"User-Agent": "0xray-foundry-ci-monitor",
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
const report = emptyReport("warning", `GitHub API ${res.status}`);
|
|
65
|
+
writeReport(report);
|
|
66
|
+
process.stderr.write(`mill ci: GitHub API ${res.status}\n`);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
const body = await res.json();
|
|
70
|
+
const runs = Array.isArray(body.workflow_runs) ? body.workflow_runs : [];
|
|
71
|
+
const failed = runs.filter((run) => {
|
|
72
|
+
const conclusion = String(run.conclusion || "");
|
|
73
|
+
return conclusion === "failure" || conclusion === "timed_out" || conclusion === "startup_failure";
|
|
74
|
+
});
|
|
75
|
+
const issues = failed.map((run) => ({
|
|
76
|
+
name: run.name || run.display_title || "workflow",
|
|
77
|
+
conclusion: run.conclusion,
|
|
78
|
+
html_url: run.html_url,
|
|
79
|
+
head_sha: run.head_sha,
|
|
80
|
+
}));
|
|
81
|
+
const uniqueFails = new Set(issues.map((i) => i.name)).size;
|
|
82
|
+
const health = Math.max(0, 100 - uniqueFails * 20);
|
|
83
|
+
const ci_status = uniqueFails === 0 ? "healthy" : uniqueFails >= 3 ? "critical" : "warning";
|
|
84
|
+
const report = {
|
|
85
|
+
ci_status,
|
|
86
|
+
health_score: health,
|
|
87
|
+
issues,
|
|
88
|
+
sha: sha || null,
|
|
89
|
+
run_count: runs.length,
|
|
90
|
+
generatedAt: new Date().toISOString(),
|
|
91
|
+
};
|
|
92
|
+
writeReport(report);
|
|
93
|
+
|
|
94
|
+
if (uniqueFails === 0) {
|
|
95
|
+
process.stdout.write("SUCCESS: All workflows passed\n");
|
|
96
|
+
process.stdout.write("No failed jobs\n");
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
process.stdout.write(`FAILURE: ${issues.map((i) => i.name).join(", ")}\n`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const isMain =
|
|
104
|
+
process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
105
|
+
|
|
106
|
+
if (isMain) {
|
|
107
|
+
main()
|
|
108
|
+
.then((code) => process.exit(code))
|
|
109
|
+
.catch((err) => {
|
|
110
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { main };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @0xray/foundry CLI — mill, not exo. No fifth MCP.
|
|
4
|
+
*
|
|
5
|
+
* npx @0xray/foundry <reconcile|stamp|gate|release|docs-check|docs-build|mint|ci|hooks> [...args]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { millScript, resolveMillRoot } from "./mill-root.mjs";
|
|
10
|
+
|
|
11
|
+
const COMMANDS = {
|
|
12
|
+
reconcile: { script: "reconcile-version.mjs", preset: [] },
|
|
13
|
+
stamp: { script: "version-manager.mjs", preset: ["--artifacts-only"] },
|
|
14
|
+
gate: { script: "release-gate.mjs", preset: [] },
|
|
15
|
+
release: { script: "release.mjs", preset: [] },
|
|
16
|
+
"docs-check": { script: "validate-release-docs.mjs", preset: [] },
|
|
17
|
+
"docs-build": { script: "docs-build.mjs", preset: [] },
|
|
18
|
+
mint: { script: "mint.mjs", preset: [] },
|
|
19
|
+
ci: { script: "ci-monitor.mjs", preset: [] },
|
|
20
|
+
hooks: { script: "hooks.mjs", preset: [] },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const HELP =
|
|
24
|
+
"Usage: npx @0xray/foundry <reconcile|stamp|gate|release|docs-check|docs-build|mint|ci|hooks> [...args]\n" +
|
|
25
|
+
"\n" +
|
|
26
|
+
"Mill, not exo. FOUNDRY_ROOT overrides cwd (the repo being milled).\n" +
|
|
27
|
+
"release bumps/commits/pushes/publishes the milled cwd — pass --dry-run or --i-mean-it (or FOUNDRY_RELEASE=1).\n" +
|
|
28
|
+
"Mint overlays their plant onto project hangers (mill-fill). PPE stays worn.\n" +
|
|
29
|
+
"gate is build+test. docs-build is Docusaurus on the 0xray exo. ci reports GitHub Actions (no auto-push). hooks installs git pre/post hooks.\n";
|
|
30
|
+
|
|
31
|
+
const cmd = process.argv[2];
|
|
32
|
+
const rest = process.argv.slice(3);
|
|
33
|
+
|
|
34
|
+
if (cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
35
|
+
process.stdout.write(HELP);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!cmd || !COMMANDS[cmd]) {
|
|
40
|
+
process.stderr.write(HELP);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const spec = COMMANDS[cmd];
|
|
45
|
+
const root = resolveMillRoot();
|
|
46
|
+
const result = spawnSync(
|
|
47
|
+
process.execPath,
|
|
48
|
+
[millScript(spec.script), ...spec.preset, ...rest],
|
|
49
|
+
{
|
|
50
|
+
stdio: "inherit",
|
|
51
|
+
cwd: root,
|
|
52
|
+
env: { ...process.env, FOUNDRY_ROOT: root },
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Mill Docusaurus build. Full corpus only on the 0xray exo (docs-site/).
|
|
4
|
+
* Stranger mills skip. Not PPE. Not an 8th MCP.
|
|
5
|
+
*
|
|
6
|
+
* npx @0xray/foundry docs-build
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { isXrayExoRepo, resolveMillRoot } from "./mill-root.mjs";
|
|
14
|
+
|
|
15
|
+
function run(cmd, args, cwd) {
|
|
16
|
+
const result = spawnSync(cmd, args, { cwd, stdio: "inherit", env: process.env });
|
|
17
|
+
return result.status === null ? 1 : result.status;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function main() {
|
|
21
|
+
const root = resolveMillRoot();
|
|
22
|
+
if (!isXrayExoRepo(root)) {
|
|
23
|
+
process.stdout.write("docs-build: skipped (not 0xray exo with docs-site)\n");
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
const docs = path.join(root, "docs-site");
|
|
27
|
+
if (!fs.existsSync(path.join(docs, "node_modules"))) {
|
|
28
|
+
const install = run("npm", ["ci"], docs);
|
|
29
|
+
if (install !== 0) process.exit(install);
|
|
30
|
+
}
|
|
31
|
+
process.exit(run("npm", ["run", "build"], docs));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const isMain =
|
|
35
|
+
process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
36
|
+
if (isMain) {
|
|
37
|
+
main();
|
|
38
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Mill git hooks: install pre-commit / post-commit / pre-push / post-push.
|
|
4
|
+
* Wear-time installer lives at scripts/hooks/install-hooks.cjs (dogfood)
|
|
5
|
+
* or node_modules/0xray/scripts/hooks/install-hooks.cjs (published mill).
|
|
6
|
+
*
|
|
7
|
+
* npx @0xray/foundry hooks
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { resolveHookInstaller, resolveMillRoot } from "./mill-root.mjs";
|
|
14
|
+
|
|
15
|
+
function main() {
|
|
16
|
+
const root = resolveMillRoot();
|
|
17
|
+
const installer = resolveHookInstaller(root);
|
|
18
|
+
if (!installer) {
|
|
19
|
+
process.stderr.write(
|
|
20
|
+
"mill hooks: no install-hooks.cjs (dogfood scripts/hooks or node_modules/0xray/scripts/hooks)\n",
|
|
21
|
+
);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const result = spawnSync(process.execPath, [installer, ...process.argv.slice(2)], {
|
|
25
|
+
stdio: "inherit",
|
|
26
|
+
cwd: root,
|
|
27
|
+
env: { ...process.env, FOUNDRY_ROOT: root },
|
|
28
|
+
});
|
|
29
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const isMain =
|
|
33
|
+
process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
34
|
+
if (isMain) {
|
|
35
|
+
main();
|
|
36
|
+
}
|
|
@@ -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
|
+
}
|