0xray 4.0.2 → 4.0.3
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/.xray/features.json +1 -1
- package/AGENTS-consumer.md +1 -1
- package/dist/CHANGELOG.md +7 -0
- package/dist/cli/commands/foundry-mint-wear.d.ts +1 -0
- package/dist/cli/commands/foundry-mint-wear.js +5 -2
- package/dist/cli/commands/opencode-install.js +9 -3
- package/dist/cli/commands/skill-install.js +4 -0
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/package.json +1 -1
- package/scripts/foundry/CHANGELOG.md +4 -0
- package/scripts/foundry/README.md +2 -0
- package/scripts/foundry/mint-suit.cjs +107 -23
- package/scripts/foundry/package.json +3 -2
- package/scripts/foundry/plant/agents/mill.yml +5 -0
- package/scripts/foundry/plant/skills/mill/SKILL.md +22 -0
- package/scripts/node/consumer-install-smoke.mjs +15 -0
- package/scripts/node/install-bridges.cjs +43 -29
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/xray/features.json +1 -1
package/.xray/features.json
CHANGED
package/AGENTS-consumer.md
CHANGED
|
@@ -19,7 +19,7 @@ Before other work, Read `.xray/state/STATION.md`. Compaction and host change are
|
|
|
19
19
|
`npm install 0xray` runs `install-bridges.cjs` and:
|
|
20
20
|
|
|
21
21
|
- Copies this file → **`AGENTS.md`** in your project root
|
|
22
|
-
-
|
|
22
|
+
- Mill plant (`mill` skill) onto hangers, then overlays **your** `src/skills` (same name wins). Does **not** dump 45/42 costume unless `foundry.json` `"costume": true`. Does **not** write root **`SKILLS.md`**
|
|
23
23
|
- Seeds **`.gitignore`** (from template, if absent)
|
|
24
24
|
- Deploys **`.xray/`** (`codex.json`, `features.json`, `config.json`) then overlays **your** mill SSOT (`xray/codex.json`, `xray/features.json`, `src/skills`, `src/opencode/agents`; remap with `foundry.json`)
|
|
25
25
|
- Writes **`.mcp.json`** with 7 MCPs servers (`npx -y 0xray mcp …`)
|
package/dist/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,13 @@ The format is based on [Conventional Commits](https://www.conventionalcommits.or
|
|
|
26
26
|
- Mill CI report: `mill-monitor.yml` (fresh workflow ID) listens for mill CI completion. Report only.
|
|
27
27
|
- Published mill `hooks` uses the 0xray garment installer (`node_modules/0xray/scripts/hooks`), not `node_modules/@0xray/hooks`. Mill tree 0.1.4 (not a 0xray bump).
|
|
28
28
|
- Nested mill in the 0xray tarball (`scripts/foundry/`). Wear no longer needs a second `npm i @0xray/foundry` for mint-on-install. 4.0.2.
|
|
29
|
+
- Empty garment: mill plant (`mill` skill) is the hanger. Not 45/42 costume unless `foundry.json` `"costume": true`. Mill 0.1.5.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## [4.0.3] - 2026-09-04
|
|
34
|
+
|
|
35
|
+
Empty garment. Their plant is the suit. Mill plant (`mill` skill) is the factory discipline on the hanger. Not 45/42 costume unless `foundry.json` `"costume": true`. Factory Repertoire still enable-when-resolves. PPE stays worn. Mill **0.1.5**. Not 5.0.0.
|
|
29
36
|
|
|
30
37
|
---
|
|
31
38
|
|
|
@@ -4,10 +4,13 @@ import { fileURLToPath } from "url";
|
|
|
4
4
|
import { frameworkLogger } from "../../core/framework-logger.js";
|
|
5
5
|
const require = createRequire(import.meta.url);
|
|
6
6
|
const packageRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
7
|
+
const millSuit = require(path.join(packageRoot, "scripts/foundry/mint-suit.cjs"));
|
|
8
|
+
export function wantsCostume(targetDir) {
|
|
9
|
+
return millSuit.wantsCostume(targetDir);
|
|
10
|
+
}
|
|
7
11
|
/** Re-apply mill overlay after CLI re-wear copies mill skills. */
|
|
8
12
|
export function mintAfterWear(targetDir) {
|
|
9
|
-
|
|
10
|
-
mintConsumerSuit(packageRoot, targetDir, (component, action, status, details) => {
|
|
13
|
+
millSuit.mintConsumerSuit(packageRoot, targetDir, (component, action, status, details) => {
|
|
11
14
|
const level = status === "error" ? "error" : "info";
|
|
12
15
|
frameworkLogger.log(component, action, level, details);
|
|
13
16
|
});
|
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import { frameworkLogger } from '../../core/framework-logger.js';
|
|
5
5
|
import { syncBuiltinSkills } from './skill-install.js';
|
|
6
|
-
import { mintAfterWear } from './foundry-mint-wear.js';
|
|
6
|
+
import { mintAfterWear, wantsCostume } from './foundry-mint-wear.js';
|
|
7
7
|
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
9
9
|
const wiring = require(path.join(__dirname, '..', '..', '..', 'scripts', 'node', 'bridge-mcp-wiring.cjs'));
|
|
@@ -105,8 +105,14 @@ async function installForOpencode(options = {}) {
|
|
|
105
105
|
console.log('Use --force to reinstall.');
|
|
106
106
|
}
|
|
107
107
|
else {
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
if (wantsCostume(process.cwd())) {
|
|
109
|
+
copyDir(opencodeSource, opencodeDest);
|
|
110
|
+
frameworkLogger.log('opencode-integration', 'opencode-copied', 'info', { destination: opencodeDest });
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
fs.mkdirSync(opencodeDest, { recursive: true });
|
|
114
|
+
frameworkLogger.log('opencode-integration', 'empty-garment-plugin', 'info', { destination: opencodeDest });
|
|
115
|
+
}
|
|
110
116
|
const pluginSource = path.join(__dirname, '..', '..', '..', 'dist', 'plugin', 'xray-codex-injection.js');
|
|
111
117
|
const pluginDest = path.join(opencodeDest, 'plugin', 'xray-codex-injection.js');
|
|
112
118
|
if (fs.existsSync(pluginSource)) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, cpSync, rmSync, statSync, unlinkSync, copyFileSync } from "fs";
|
|
2
2
|
import { join, basename, dirname } from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
|
+
import { createRequire } from "module";
|
|
4
5
|
import { execSync } from "child_process";
|
|
5
6
|
function getLocalRegistryPath() {
|
|
6
7
|
return join(process.cwd(), ".xray", "skill-registry.json");
|
|
@@ -484,6 +485,9 @@ export async function skillRegistryCommand(action, args) {
|
|
|
484
485
|
// Each platform's install command calls this to copy framework skills.
|
|
485
486
|
export function syncBuiltinSkills(targetSkillsDir, packageRoot) {
|
|
486
487
|
const root = packageRoot ?? join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
488
|
+
const millSuit = createRequire(import.meta.url)(join(root, "scripts/foundry/mint-suit.cjs"));
|
|
489
|
+
if (!millSuit.wantsCostume(process.cwd()))
|
|
490
|
+
return 0;
|
|
487
491
|
const candidateDirs = [
|
|
488
492
|
join(root, "dist", "skills"),
|
|
489
493
|
join(root, "src", "skills"),
|
package/package.json
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.5] - 2026-09-04
|
|
4
|
+
|
|
5
|
+
Empty garment: consumer hangers get mill plant (`plant/skills/mill`), not 45/42 costume. `foundry.json` `"costume": true` restores the dump. Plant overlay still mill-fill.
|
|
6
|
+
|
|
3
7
|
## [0.1.4] - 2026-09-04
|
|
4
8
|
|
|
5
9
|
`npx @0xray/foundry hooks` on a published mill uses the worn garment
|
|
@@ -52,6 +52,8 @@ After the garment is on the hanger, overlay their plant (defaults; remap in `fou
|
|
|
52
52
|
|
|
53
53
|
**Mill-fill is law:** their keys/files win; mill names they did not plant stay unless they overlay the same name. JSON facets always merge. Skill overlay is project hangers only. CLI re-wear calls mint **once** after mill copies.
|
|
54
54
|
|
|
55
|
+
**Empty garment (default):** mill plant (`plant/skills/mill`) is the hanger. Not 45 skills / 42 agents. `foundry.json` `"costume": true` copies that mill garment. Factory Repertoire still enable-when-resolves. PPE stays worn.
|
|
56
|
+
|
|
55
57
|
## Publish this mill
|
|
56
58
|
|
|
57
59
|
From **this directory**, not the 0xray repo root:
|
|
@@ -72,22 +72,25 @@ function resolveInside(targetDir, rel) {
|
|
|
72
72
|
return resolved;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
function
|
|
75
|
+
function loadFoundryExtra(targetDir) {
|
|
76
76
|
const candidates = [
|
|
77
77
|
path.join(targetDir, "foundry.json"),
|
|
78
78
|
path.join(targetDir, ".xray", "foundry.json"),
|
|
79
79
|
];
|
|
80
|
-
let extra = {};
|
|
81
80
|
for (const candidate of candidates) {
|
|
82
81
|
if (!fs.existsSync(candidate)) continue;
|
|
83
82
|
try {
|
|
84
83
|
const parsed = JSON.parse(fs.readFileSync(candidate, "utf8"));
|
|
85
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
84
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
86
85
|
} catch {
|
|
87
|
-
|
|
86
|
+
return {};
|
|
88
87
|
}
|
|
89
|
-
break;
|
|
90
88
|
}
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function loadFoundryParams(targetDir) {
|
|
93
|
+
const extra = loadFoundryExtra(targetDir);
|
|
91
94
|
const params = { ...DEFAULT_PARAMS };
|
|
92
95
|
for (const key of ["codex", "features", "config", "skills", "agents", "agentsCard"]) {
|
|
93
96
|
if (typeof extra[key] === "string" && extra[key].trim()) params[key] = extra[key].trim();
|
|
@@ -95,6 +98,80 @@ function loadFoundryParams(targetDir) {
|
|
|
95
98
|
return params;
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
/** 45/42 costume dump. Default false on consumers. Exo mill keeps the costume. */
|
|
102
|
+
function wantsCostume(targetDir) {
|
|
103
|
+
if (isExoRepo(targetDir)) return true;
|
|
104
|
+
return loadFoundryExtra(targetDir).costume === true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function millPlantDir(millPackageRoot) {
|
|
108
|
+
const nested = path.join(millPackageRoot, "scripts", "foundry", "plant");
|
|
109
|
+
if (isDirectory(nested)) return nested;
|
|
110
|
+
const packed = path.join(millPackageRoot, "plant");
|
|
111
|
+
if (isDirectory(packed)) return packed;
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function listSkillNamesAt(skillsSrc) {
|
|
116
|
+
if (!isDirectory(skillsSrc)) return [];
|
|
117
|
+
const names = [];
|
|
118
|
+
for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
|
|
119
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
120
|
+
const skillMd = path.join(skillsSrc, entry.name, "SKILL.md");
|
|
121
|
+
if (!fs.existsSync(skillMd)) continue;
|
|
122
|
+
names.push(entry.name);
|
|
123
|
+
}
|
|
124
|
+
return names;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function listAgentFilesAt(agentsSrc) {
|
|
128
|
+
if (!isDirectory(agentsSrc)) return [];
|
|
129
|
+
return fs
|
|
130
|
+
.readdirSync(agentsSrc, { withFileTypes: true })
|
|
131
|
+
.filter(
|
|
132
|
+
(entry) =>
|
|
133
|
+
entry.isFile() && (entry.name.endsWith(".yml") || entry.name.endsWith(".yaml")),
|
|
134
|
+
)
|
|
135
|
+
.map((entry) => entry.name);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Mill mill-fill mill plant: mill mill mill mill-fill mill names they did not plant stay. */
|
|
139
|
+
function millFillMillPlant(millPackageRoot, targetDir, log) {
|
|
140
|
+
const plant = millPlantDir(millPackageRoot);
|
|
141
|
+
if (!plant) return { skills: [], agents: [] };
|
|
142
|
+
const skillsSrc = path.join(plant, "skills");
|
|
143
|
+
const agentsSrc = path.join(plant, "agents");
|
|
144
|
+
const skills = listSkillNamesAt(skillsSrc);
|
|
145
|
+
const agents = listAgentFilesAt(agentsSrc);
|
|
146
|
+
const skillHangers = listSkillHangers(targetDir);
|
|
147
|
+
const agentsDest = path.join(targetDir, ".opencode", "agents");
|
|
148
|
+
for (const hanger of skillHangers) {
|
|
149
|
+
for (const name of skills) {
|
|
150
|
+
const src = path.join(skillsSrc, name, "SKILL.md");
|
|
151
|
+
const destMd = path.join(hanger, name, "SKILL.md");
|
|
152
|
+
if (path.resolve(src) === path.resolve(destMd)) continue;
|
|
153
|
+
if (fs.existsSync(destMd)) continue;
|
|
154
|
+
fs.mkdirSync(path.dirname(destMd), { recursive: true });
|
|
155
|
+
fs.copyFileSync(src, destMd);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const file of agents) {
|
|
159
|
+
const src = path.join(agentsSrc, file);
|
|
160
|
+
const dest = path.join(agentsDest, file);
|
|
161
|
+
if (path.resolve(src) === path.resolve(dest)) continue;
|
|
162
|
+
if (fs.existsSync(dest)) continue;
|
|
163
|
+
fs.mkdirSync(agentsDest, { recursive: true });
|
|
164
|
+
fs.copyFileSync(src, dest);
|
|
165
|
+
}
|
|
166
|
+
if (log && (skills.length > 0 || agents.length > 0)) {
|
|
167
|
+
log("foundry-mint", "Mill plant onto hangers", "info", {
|
|
168
|
+
skills: skills.length,
|
|
169
|
+
agents: agents.length,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return { skills, agents };
|
|
173
|
+
}
|
|
174
|
+
|
|
98
175
|
function isDirectory(p) {
|
|
99
176
|
try {
|
|
100
177
|
return Boolean(p) && fs.existsSync(p) && fs.statSync(p).isDirectory();
|
|
@@ -138,28 +215,13 @@ function overlayJsonFacet(src, dest) {
|
|
|
138
215
|
function listConsumerSkillNames(targetDir, skillsRel) {
|
|
139
216
|
const rel = skillsRel || DEFAULT_PARAMS.skills;
|
|
140
217
|
const skillsSrc = resolveInside(targetDir, rel);
|
|
141
|
-
|
|
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;
|
|
218
|
+
return listSkillNamesAt(skillsSrc);
|
|
150
219
|
}
|
|
151
220
|
|
|
152
221
|
function listConsumerAgentFiles(targetDir, agentsRel) {
|
|
153
222
|
const rel = agentsRel || DEFAULT_PARAMS.agents;
|
|
154
223
|
const agentsSrc = resolveInside(targetDir, rel);
|
|
155
|
-
|
|
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);
|
|
224
|
+
return listAgentFilesAt(agentsSrc);
|
|
163
225
|
}
|
|
164
226
|
|
|
165
227
|
const MANAGED_AGENTS_MARKER = "<!-- 0xray-managed -->";
|
|
@@ -233,13 +295,28 @@ function mintConsumerFromSsot(packageRoot, targetDir, log, tree) {
|
|
|
233
295
|
features ||
|
|
234
296
|
config ||
|
|
235
297
|
agentsCard;
|
|
236
|
-
const
|
|
298
|
+
const millPlanted =
|
|
299
|
+
(Array.isArray(tree?.millPlantSkills) && tree.millPlantSkills.length > 0) ||
|
|
300
|
+
(Array.isArray(tree?.millPlantAgents) && tree.millPlantAgents.length > 0);
|
|
301
|
+
const costume = wantsCostume(targetDir);
|
|
302
|
+
const garment = overlayed
|
|
303
|
+
? "overlay"
|
|
304
|
+
: millPlanted
|
|
305
|
+
? "mill-fill"
|
|
306
|
+
: costume
|
|
307
|
+
? "copied-onto-hanger"
|
|
308
|
+
: "mill-fill";
|
|
237
309
|
const inventory = {
|
|
238
310
|
mill: { name: mill.name || "@0xray/foundry", version: mill.version },
|
|
239
311
|
consumer: { name: consumer.name, version: consumer.version },
|
|
240
312
|
garment,
|
|
241
313
|
params: tree?.params || loadFoundryParams(targetDir),
|
|
242
314
|
tree: { skills, agents },
|
|
315
|
+
millPlant: {
|
|
316
|
+
skills: Array.isArray(tree?.millPlantSkills) ? tree.millPlantSkills : [],
|
|
317
|
+
agents: Array.isArray(tree?.millPlantAgents) ? tree.millPlantAgents : [],
|
|
318
|
+
},
|
|
319
|
+
costume,
|
|
243
320
|
facets: {
|
|
244
321
|
constitution,
|
|
245
322
|
features,
|
|
@@ -271,7 +348,10 @@ function mintConsumerSuit(millPackageRoot, targetDir, log) {
|
|
|
271
348
|
return { garment: "dogfood", skipped: true };
|
|
272
349
|
}
|
|
273
350
|
const params = loadFoundryParams(targetDir);
|
|
351
|
+
const millPlant = millFillMillPlant(millPackageRoot, targetDir, log);
|
|
274
352
|
const tree = overlayConsumerTree(targetDir, log, params);
|
|
353
|
+
tree.millPlantSkills = millPlant.skills;
|
|
354
|
+
tree.millPlantAgents = millPlant.agents;
|
|
275
355
|
tree.codex = overlayJsonFacet(
|
|
276
356
|
resolveInside(targetDir, params.codex),
|
|
277
357
|
path.join(targetDir, ".xray", "codex.json"),
|
|
@@ -299,6 +379,10 @@ module.exports = {
|
|
|
299
379
|
DEFAULT_PARAMS,
|
|
300
380
|
deepMerge,
|
|
301
381
|
loadFoundryParams,
|
|
382
|
+
loadFoundryExtra,
|
|
383
|
+
wantsCostume,
|
|
384
|
+
millPlantDir,
|
|
385
|
+
millFillMillPlant,
|
|
302
386
|
resolveInside,
|
|
303
387
|
overlayJsonFacet,
|
|
304
388
|
overlayAgentsCard,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0xray/foundry",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "0xRay mill: mint their suit params, stamp, gate, publish, CI report, git hooks. Not the worn exo.",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"cli.js",
|
|
27
27
|
"README.md",
|
|
28
28
|
"LICENSE",
|
|
29
|
-
"CHANGELOG.md"
|
|
29
|
+
"CHANGELOG.md",
|
|
30
|
+
"plant"
|
|
30
31
|
],
|
|
31
32
|
"exports": {
|
|
32
33
|
"./version-manager": "./version-manager.mjs",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mill
|
|
3
|
+
description: Mill mill-fill mill plant. Their plant is the suit. Factory Repertoire. Not 45/42 costume.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Mill
|
|
7
|
+
|
|
8
|
+
The mill **fastens** their plant onto the hanger. It does not copy the 0xRay 45-skill / 42-agent costume unless they opt in.
|
|
9
|
+
|
|
10
|
+
**Plant (their SSOT):** `xray/codex.json`, `xray/features.json`, `xray/config.json`, `src/skills/<name>/SKILL.md`, `src/opencode/agents/*.yml`. Remap with `foundry.json`.
|
|
11
|
+
|
|
12
|
+
**Mill-fill:** their keys/files win. Mill mill mill-fill mill names they did not plant stay. JSON always merges.
|
|
13
|
+
|
|
14
|
+
**Factory:** Repertoire organ enable-when-resolves. Seven generated MCP. Constitution (PPE) stays worn. Not a fifth MCP. Not 5.0.
|
|
15
|
+
|
|
16
|
+
**Costume:** `foundry.json` `"costume": true` copies the 45/42 mill garment. Default is empty garment + this mill mill-fill mill plant.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx @0xray/foundry mint
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Nested mill: `node node_modules/0xray/scripts/foundry/cli.js mint`.
|
|
@@ -210,6 +210,7 @@ function main() {
|
|
|
210
210
|
"scripts/node/postinstall.cjs",
|
|
211
211
|
"scripts/foundry/mint-suit.cjs",
|
|
212
212
|
"scripts/foundry/hooks.mjs",
|
|
213
|
+
"scripts/foundry/plant/skills/mill/SKILL.md",
|
|
213
214
|
"package.json",
|
|
214
215
|
"xray/features.json",
|
|
215
216
|
"xray/features.schema.json",
|
|
@@ -223,6 +224,20 @@ function main() {
|
|
|
223
224
|
console.log(` ✅ ${rel}`);
|
|
224
225
|
}
|
|
225
226
|
|
|
227
|
+
const millSkillHanger = path.join(tmpRoot, ".opencode", "skills", "mill", "SKILL.md");
|
|
228
|
+
if (!fs.existsSync(millSkillHanger)) {
|
|
229
|
+
throw new Error("empty garment missing mill plant skill on hanger");
|
|
230
|
+
}
|
|
231
|
+
const hangerSkills = path.join(tmpRoot, ".opencode", "skills");
|
|
232
|
+
const hangerNames = fs.existsSync(hangerSkills)
|
|
233
|
+
? fs.readdirSync(hangerSkills).filter((n) =>
|
|
234
|
+
fs.existsSync(path.join(hangerSkills, n, "SKILL.md")),
|
|
235
|
+
)
|
|
236
|
+
: [];
|
|
237
|
+
if (hangerNames.length > 3) {
|
|
238
|
+
throw new Error(`costume dump on empty garment: ${hangerNames.join(",")}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
226
241
|
const installedPkg = JSON.parse(fs.readFileSync(path.join(nmRoot, "package.json"), "utf-8"));
|
|
227
242
|
if (installedPkg.version !== version) {
|
|
228
243
|
throw new Error(`Installed version ${installedPkg.version} != packed ${version}`);
|
|
@@ -8,6 +8,7 @@ const fs = require("fs");
|
|
|
8
8
|
const path = require("path");
|
|
9
9
|
const os = require("os");
|
|
10
10
|
const { execSync } = require("child_process");
|
|
11
|
+
const { wantsCostume } = require("../foundry/mint-suit.cjs");
|
|
11
12
|
const {
|
|
12
13
|
wireHermesBridge,
|
|
13
14
|
wireOpencodeBridge,
|
|
@@ -92,6 +93,11 @@ function syncBuiltinSkills(targetSkillsDir, packageRoot) {
|
|
|
92
93
|
return copied;
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
function syncCostumeSkills(targetSkillsDir, packageRoot, targetDir) {
|
|
97
|
+
if (targetDir && !wantsCostume(targetDir)) return 0;
|
|
98
|
+
return syncBuiltinSkills(targetSkillsDir, packageRoot);
|
|
99
|
+
}
|
|
100
|
+
|
|
95
101
|
function copyTree(src, dest, relPath = "") {
|
|
96
102
|
if (!fs.existsSync(src)) return;
|
|
97
103
|
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
@@ -267,38 +273,46 @@ function writeGrokDiscoveredHooks(targetDir, hooksPath, log, label) {
|
|
|
267
273
|
}
|
|
268
274
|
}
|
|
269
275
|
|
|
276
|
+
function copyOpencodePlugin(packageRoot, opencodeDest, log) {
|
|
277
|
+
const pluginSource = path.join(packageRoot, "dist", "plugin", "xray-codex-injection.js");
|
|
278
|
+
const pluginDest = path.join(opencodeDest, "plugin", "xray-codex-injection.js");
|
|
279
|
+
if (!fs.existsSync(pluginSource)) return;
|
|
280
|
+
const pluginDestDir = path.dirname(pluginDest);
|
|
281
|
+
if (!fs.existsSync(pluginDestDir)) fs.mkdirSync(pluginDestDir, { recursive: true });
|
|
282
|
+
const shouldCopy =
|
|
283
|
+
!fs.existsSync(pluginDest) ||
|
|
284
|
+
fs.statSync(pluginSource).mtime > fs.statSync(pluginDest).mtime;
|
|
285
|
+
if (shouldCopy) {
|
|
286
|
+
fs.copyFileSync(pluginSource, pluginDest);
|
|
287
|
+
log("opencode-bridge", "plugin updated", "info");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
270
291
|
function installOpencodeBridge(targetDir, packageRoot, log) {
|
|
271
292
|
const opencodeSource = path.join(packageRoot, ".opencode");
|
|
272
293
|
const opencodeDest = path.join(targetDir, ".opencode");
|
|
273
|
-
|
|
274
|
-
log("opencode-bridge", "skipped", "warn", { reason: ".opencode source missing" });
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
294
|
+
const costume = wantsCostume(targetDir);
|
|
277
295
|
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
log("opencode-bridge", ".opencode exists — merging skills/plugin only", "info");
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const pluginSource = path.join(packageRoot, "dist", "plugin", "xray-codex-injection.js");
|
|
286
|
-
const pluginDest = path.join(opencodeDest, "plugin", "xray-codex-injection.js");
|
|
287
|
-
if (fs.existsSync(pluginSource)) {
|
|
288
|
-
const pluginDestDir = path.dirname(pluginDest);
|
|
289
|
-
if (!fs.existsSync(pluginDestDir)) fs.mkdirSync(pluginDestDir, { recursive: true });
|
|
290
|
-
const shouldCopy =
|
|
291
|
-
!fs.existsSync(pluginDest) ||
|
|
292
|
-
fs.statSync(pluginSource).mtime > fs.statSync(pluginDest).mtime;
|
|
293
|
-
if (shouldCopy) {
|
|
294
|
-
fs.copyFileSync(pluginSource, pluginDest);
|
|
295
|
-
log("opencode-bridge", "plugin updated", "info");
|
|
296
|
+
if (costume) {
|
|
297
|
+
if (!fs.existsSync(opencodeSource)) {
|
|
298
|
+
log("opencode-bridge", "skipped", "warn", { reason: ".opencode source missing" });
|
|
299
|
+
return;
|
|
296
300
|
}
|
|
301
|
+
if (!fs.existsSync(opencodeDest)) {
|
|
302
|
+
copyTree(opencodeSource, opencodeDest);
|
|
303
|
+
log("opencode-bridge", "copied .opencode tree", "info");
|
|
304
|
+
} else {
|
|
305
|
+
log("opencode-bridge", ".opencode exists — merging skills/plugin only", "info");
|
|
306
|
+
}
|
|
307
|
+
copyOpencodePlugin(packageRoot, opencodeDest, log);
|
|
308
|
+
const copied = syncCostumeSkills(path.join(opencodeDest, "skills"), packageRoot, targetDir);
|
|
309
|
+
if (copied > 0) log("opencode-bridge", `skills synced (${copied})`, "info", { path: ".opencode/skills/" });
|
|
310
|
+
} else {
|
|
311
|
+
log("opencode-bridge", "empty garment — plugin only, mill mill-fill mill plant", "info");
|
|
312
|
+
fs.mkdirSync(opencodeDest, { recursive: true });
|
|
313
|
+
copyOpencodePlugin(packageRoot, opencodeDest, log);
|
|
297
314
|
}
|
|
298
315
|
|
|
299
|
-
const copied = syncBuiltinSkills(path.join(opencodeDest, "skills"), packageRoot);
|
|
300
|
-
if (copied > 0) log("opencode-bridge", `skills synced (${copied})`, "info", { path: ".opencode/skills/" });
|
|
301
|
-
|
|
302
316
|
try {
|
|
303
317
|
const count = wireOpencodeBridge(targetDir);
|
|
304
318
|
log("opencode-bridge", `opencode.json mcp wired (${count} servers)`, "info");
|
|
@@ -376,14 +390,14 @@ function installGrokBridge(targetDir, packageRoot, log) {
|
|
|
376
390
|
log("grok-bridge", "plugin copied", "info", { path: rel || dest });
|
|
377
391
|
writePluginMcpJson(dest, targetDir, log, "grok-bridge");
|
|
378
392
|
patchGrokHooks(dest, packageRoot, targetDir, log, "grok-bridge");
|
|
379
|
-
const copied =
|
|
393
|
+
const copied = syncCostumeSkills(path.join(dest, "skills"), packageRoot, targetDir);
|
|
380
394
|
if (copied > 0) log("grok-bridge", `plugin skills synced (${copied})`, "info", { path: rel });
|
|
381
395
|
}
|
|
382
396
|
}
|
|
383
397
|
|
|
384
398
|
if (!ephemeral) {
|
|
385
399
|
const grokGlobalSkills = path.join(home, ".grok", "skills");
|
|
386
|
-
const globalCopied =
|
|
400
|
+
const globalCopied = syncCostumeSkills(grokGlobalSkills, packageRoot, targetDir);
|
|
387
401
|
if (globalCopied > 0) {
|
|
388
402
|
log("grok-bridge", `global skills synced (${globalCopied})`, "info", { path: "~/.grok/skills/" });
|
|
389
403
|
}
|
|
@@ -421,7 +435,7 @@ function installHermesBridge(targetDir, packageRoot, log) {
|
|
|
421
435
|
|
|
422
436
|
writePluginMcpJson(targetPluginDir, targetDir, log, "hermes-bridge");
|
|
423
437
|
|
|
424
|
-
const copied =
|
|
438
|
+
const copied = syncCostumeSkills(path.join(targetPluginDir, "skills"), packageRoot, targetDir);
|
|
425
439
|
if (copied > 0) log("hermes-bridge", `skills synced (${copied})`, "info");
|
|
426
440
|
|
|
427
441
|
const rootMarker = path.join(targetPluginDir, "xray-consumer-root.txt");
|
|
@@ -473,7 +487,7 @@ function installOpenclawBridge(targetDir, packageRoot, log) {
|
|
|
473
487
|
log("openclaw-bridge", "config created", "info", { path: ".xray/config/openclaw.json" });
|
|
474
488
|
}
|
|
475
489
|
|
|
476
|
-
const copied =
|
|
490
|
+
const copied = syncCostumeSkills(path.join(os.homedir(), ".openclaw", "skills"), packageRoot, targetDir);
|
|
477
491
|
if (copied > 0) log("openclaw-bridge", `skills synced (${copied})`, "info", { path: "~/.openclaw/skills/" });
|
|
478
492
|
|
|
479
493
|
try {
|