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,444 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Foundry stamper — JSON version fields + CHANGELOG only.
5
+ *
6
+ * Does NOT bump package.json (reconcile-version.mjs is the one bumper).
7
+ * Does NOT rewrite markdown prose (validate-release-docs.mjs verifies).
8
+ *
9
+ * Usage:
10
+ * npx @0xray/foundry stamp
11
+ */
12
+
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import { fileURLToPath } from 'url';
16
+ import { execSync } from 'child_process';
17
+ import { isXrayExoRepo, millScript, resolveMillRoot } from './mill-root.mjs';
18
+
19
+ const rootDir = resolveMillRoot();
20
+
21
+ // Files to update with version
22
+ const VERSION_FILES = [
23
+ { file: 'package.json', field: 'version', pattern: /"version":\s*"[^"]+"/ }
24
+ ];
25
+
26
+ // Commit types for changelog grouping
27
+ const COMMIT_TYPES = {
28
+ feat: { emoji: '✨', title: 'Features', prefix: 'feat:' },
29
+ fix: { emoji: '🐛', title: 'Bug Fixes', prefix: 'fix:' },
30
+ docs: { emoji: '📚', title: 'Documentation', prefix: 'docs:' },
31
+ chore: { emoji: '🔧', title: 'Maintenance', prefix: 'chore:' },
32
+ refactor: { emoji: '♻️', title: 'Refactoring', prefix: 'refactor:' },
33
+ perf: { emoji: '⚡', title: 'Performance', prefix: 'perf:' },
34
+ test: { emoji: '🧪', title: 'Tests', prefix: 'test:' },
35
+ style: { emoji: '💎', title: 'Styles', prefix: 'style:' },
36
+ ci: { emoji: '👷', title: 'CI/CD', prefix: 'ci:' },
37
+ build: { emoji: '📦', title: 'Builds', prefix: 'build:' },
38
+ revert: { emoji: '⏪', title: 'Reverts', prefix: 'revert:' }
39
+ };
40
+
41
+ /**
42
+ * Get the last git tag (most recent version)
43
+ */
44
+ function getLastGitTag() {
45
+ try {
46
+ const tag = execSync('git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0"', {
47
+ cwd: rootDir,
48
+ encoding: 'utf-8'
49
+ }).trim();
50
+ return tag || 'v0.0.0';
51
+ } catch {
52
+ return 'v0.0.0';
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Extract commits since the last tag
58
+ */
59
+ function getCommitsSinceLastTag() {
60
+ const lastTag = getLastGitTag();
61
+ console.log(`📊 Found last tag: ${lastTag}`);
62
+
63
+ try {
64
+ // Get commits since last tag with conventional format
65
+ const commits = execSync(
66
+ `git log ${lastTag}..HEAD --oneline --format="%s||%h"`,
67
+ { cwd: rootDir, encoding: 'utf-8' }
68
+ ).trim().split('\n').filter(Boolean);
69
+
70
+ return commits.map(commit => {
71
+ const [message, hash] = commit.split('||');
72
+ return { message: message.trim(), hash: hash.trim() };
73
+ });
74
+ } catch (error) {
75
+ // If no tags or error, get all commits from initial commit
76
+ console.log('⚠️ Could not get commits from tag, using all commits');
77
+ try {
78
+ const commits = execSync(
79
+ `git log --oneline --format="%s||%h" -n 50`,
80
+ { cwd: rootDir, encoding: 'utf-8' }
81
+ ).trim().split('\n').filter(Boolean);
82
+ return commits.map(commit => {
83
+ const [message, hash] = commit.split('||');
84
+ return { message: message.trim(), hash: hash.trim() };
85
+ });
86
+ } catch {
87
+ return [];
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Parse commits and group by type
94
+ */
95
+ function parseCommitsByType(commits) {
96
+ const grouped = {
97
+ feat: [],
98
+ fix: [],
99
+ docs: [],
100
+ chore: [],
101
+ refactor: [],
102
+ perf: [],
103
+ test: [],
104
+ style: [],
105
+ ci: [],
106
+ build: [],
107
+ revert: [],
108
+ other: []
109
+ };
110
+
111
+ for (const commit of commits) {
112
+ const message = commit.message.toLowerCase();
113
+ let matched = false;
114
+
115
+ for (const [type, config] of Object.entries(COMMIT_TYPES)) {
116
+ if (message.startsWith(config.prefix)) {
117
+ grouped[type].push(commit);
118
+ matched = true;
119
+ break;
120
+ }
121
+ }
122
+
123
+ if (!matched) {
124
+ grouped.other.push(commit);
125
+ }
126
+ }
127
+
128
+ return grouped;
129
+ }
130
+
131
+ /**
132
+ * Generate changelog content from commits
133
+ */
134
+ function generateChangelogFromCommits(commits) {
135
+ const grouped = parseCommitsByType(commits);
136
+ const sections = [];
137
+
138
+ // Build sections in preferred order
139
+ const typeOrder = ['feat', 'fix', 'perf', 'refactor', 'docs', 'test', 'style', 'ci', 'build', 'chore', 'revert'];
140
+
141
+ for (const type of typeOrder) {
142
+ if (grouped[type].length > 0) {
143
+ const config = COMMIT_TYPES[type];
144
+ const items = grouped[type].map(c => `- ${c.message} (${c.hash})`).join('\n');
145
+ sections.push(`### ${config.emoji} ${config.title}\n${items}`);
146
+ }
147
+ }
148
+
149
+ // Add other/unclassified if any
150
+ if (grouped.other.length > 0 && grouped.other.length <= 5) {
151
+ const items = grouped.other.map(c => `- ${c.message} (${c.hash})`).join('\n');
152
+ sections.push(`### 🔎 Other Changes\n${items}`);
153
+ }
154
+
155
+ return sections.join('\n\n') || '- Version bump';
156
+ }
157
+
158
+ /**
159
+ * Count actual framework components
160
+ * @param {string} [baseDir]
161
+ */
162
+ export function getFrameworkCounts(baseDir = resolveMillRoot()) {
163
+ const counts = {
164
+ agents: 0,
165
+ mcps: 0,
166
+ skills: 0,
167
+ codexTerms: 0,
168
+ };
169
+
170
+ // Count agents (.yml files in src/opencode/agents/ — source of truth)
171
+ const agentsDir = path.join(baseDir, 'src/opencode/agents');
172
+ if (fs.existsSync(agentsDir)) {
173
+ counts.agents = fs.readdirSync(agentsDir)
174
+ .filter(f => f.endsWith('.yml'))
175
+ .length;
176
+ }
177
+
178
+ // Consumer MCP count from .mcp.json SSOT (7 servers), not internal dist/mcps inventory
179
+ const mcpJsonPath = path.join(baseDir, '.mcp.json');
180
+ if (fs.existsSync(mcpJsonPath)) {
181
+ try {
182
+ const mcpJson = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8'));
183
+ const servers = mcpJson.mcpServers || mcpJson.servers || {};
184
+ const xrayServers = Object.keys(servers).filter((name) => name.startsWith('xray-'));
185
+ counts.mcps = xrayServers.length;
186
+ } catch {
187
+ counts.mcps = isXrayExoRepo(baseDir) ? 7 : 0;
188
+ }
189
+ } else {
190
+ counts.mcps = isXrayExoRepo(baseDir) ? 7 : 0;
191
+ }
192
+
193
+ // Count skills (directories in src/skills/ with SKILL.md)
194
+ const skillsDir = path.join(baseDir, 'src/skills');
195
+ if (fs.existsSync(skillsDir)) {
196
+ counts.skills = fs.readdirSync(skillsDir)
197
+ .filter(f => fs.statSync(path.join(skillsDir, f)).isDirectory())
198
+ .filter(f => fs.existsSync(path.join(skillsDir, f, 'SKILL.md')))
199
+ .length;
200
+ }
201
+
202
+ const codexPath = path.join(baseDir, 'xray/codex.json');
203
+ if (fs.existsSync(codexPath)) {
204
+ try {
205
+ const codex = JSON.parse(fs.readFileSync(codexPath, 'utf-8'));
206
+ const terms = codex.terms && typeof codex.terms === 'object' ? Object.keys(codex.terms) : [];
207
+ if (terms.length > 0) counts.codexTerms = terms.length;
208
+ } catch {
209
+ /* keep default */
210
+ }
211
+ }
212
+
213
+ return counts;
214
+ }
215
+
216
+ function getChangelogEntry(newVersion, changeDescription) {
217
+ const date = new Date().toISOString().split('T')[0];
218
+
219
+ // If manual description provided, use it; otherwise auto-generate from commits
220
+ let content;
221
+ if (changeDescription) {
222
+ content = changeDescription;
223
+ } else {
224
+ // Auto-generate from git commits
225
+ console.log('📝 No description provided, auto-generating from git commits...');
226
+ const commits = getCommitsSinceLastTag();
227
+ console.log(`📊 Found ${commits.length} commits since last release`);
228
+ content = generateChangelogFromCommits(commits);
229
+ }
230
+
231
+ return `## [${newVersion}] - ${date}
232
+
233
+ ### 🔄 Changes
234
+
235
+ ${content}
236
+
237
+ ---
238
+
239
+ `;
240
+ }
241
+
242
+ function changelogHasVersion(newVersion) {
243
+ const changelogPath = path.join(rootDir, 'CHANGELOG.md');
244
+ if (!fs.existsSync(changelogPath)) return false;
245
+ const changelog = fs.readFileSync(changelogPath, 'utf-8');
246
+ return new RegExp(`^## \\[${newVersion.replace(/\./g, '\\.')}\\]`, 'm').test(changelog);
247
+ }
248
+
249
+ function updateChangelog(newVersion, changeDescription) {
250
+ const changelogPath = path.join(rootDir, 'CHANGELOG.md');
251
+ const newEntry = getChangelogEntry(newVersion, changeDescription);
252
+
253
+ if (!fs.existsSync(changelogPath)) {
254
+ fs.writeFileSync(changelogPath, `# Changelog\n\n${newEntry}`);
255
+ process.stdout.write('Created CHANGELOG.md\n');
256
+ return;
257
+ }
258
+
259
+ let changelog = fs.readFileSync(changelogPath, 'utf-8');
260
+
261
+ if (changelogHasVersion(newVersion)) {
262
+ console.log(`ℹ️ CHANGELOG.md already has [${newVersion}] — skipping duplicate entry`);
263
+ return;
264
+ }
265
+
266
+ const unreleasedHead = changelog.match(/^## \[Unreleased\][^\n]*/m);
267
+ let headerEnd = changelog.indexOf('## [');
268
+ if (unreleasedHead && typeof unreleasedHead.index === 'number') {
269
+ const rest = changelog.slice(unreleasedHead.index + unreleasedHead[0].length);
270
+ const next = rest.search(/\n## \[/);
271
+ headerEnd =
272
+ next === -1
273
+ ? changelog.length
274
+ : unreleasedHead.index + unreleasedHead[0].length + next + 1;
275
+ }
276
+ if (headerEnd === -1) {
277
+ fs.writeFileSync(changelogPath, `${changelog.trimEnd()}\n\n${newEntry}`);
278
+ process.stdout.write('Updated CHANGELOG.md\n');
279
+ return;
280
+ }
281
+
282
+ const newChangelog = changelog.slice(0, headerEnd) + newEntry + changelog.slice(headerEnd);
283
+
284
+ fs.writeFileSync(changelogPath, newChangelog);
285
+ console.log(`✅ Updated CHANGELOG.md`);
286
+ }
287
+
288
+ /** Markdown prose is verified, not rewritten. */
289
+
290
+ export const DOCS_SITE_HEADER_FILES = [
291
+ 'docs-site/docs/index.md',
292
+ 'docs-site/docs/introduction.md',
293
+ 'docs-site/docs/guides/getting-started.md',
294
+ 'docs-site/docs/full-reference.md',
295
+ 'docs-site/docs/architecture/GROK_GUIDE.md',
296
+ 'docs-site/docs/mcp/README.md',
297
+ 'docs-site/docs/agents/README.md',
298
+ ];
299
+
300
+ /** Line identity (major.minor). Patch belongs in CHANGELOG + package.json only. */
301
+ export function eraFromVersion(version) {
302
+ const parts = String(version).split('.');
303
+ return `${parts[0] || '0'}.${parts[1] || '0'}`;
304
+ }
305
+
306
+ export function buildDocsHeader(_counts, version) {
307
+ return `**${eraFromVersion(version)}** — a suit that survives the context window`;
308
+ }
309
+
310
+
311
+
312
+ function updatePluginJsonVersion(newVersion) {
313
+ const pluginPath = path.join(rootDir, '.grok-plugin/plugin.json');
314
+ if (!fs.existsSync(pluginPath)) return;
315
+ const plugin = JSON.parse(fs.readFileSync(pluginPath, 'utf-8'));
316
+ plugin.version = newVersion;
317
+ fs.writeFileSync(pluginPath, JSON.stringify(plugin, null, 2) + '\n');
318
+ console.log(`✅ Updated .grok-plugin/plugin.json (version: ${newVersion})`);
319
+ }
320
+
321
+ function updateJsonVersionField(relPath, newVersion) {
322
+ const full = path.join(rootDir, relPath);
323
+ if (!fs.existsSync(full)) return;
324
+ const data = JSON.parse(fs.readFileSync(full, 'utf-8'));
325
+ data.version = newVersion;
326
+ fs.writeFileSync(full, JSON.stringify(data, null, 2) + '\n');
327
+ }
328
+
329
+ function updateFeaturesJsonVersion(newVersion) {
330
+ updateJsonVersionField('xray/features.json', newVersion);
331
+ }
332
+
333
+ function updateOpenclawPluginVersion(newVersion) {
334
+ updateJsonVersionField('src/integrations/openclaw/plugin/xray-pre-tool/package.json', newVersion);
335
+ }
336
+
337
+ function getCurrentVersion() {
338
+ const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf-8'));
339
+ return pkg.version;
340
+ }
341
+
342
+ /** Paths written by release artifact updates (existing files only). */
343
+ export function getReleaseArtifactPaths(baseDir = resolveMillRoot()) {
344
+ const candidates = [
345
+ 'package.json',
346
+ 'CHANGELOG.md',
347
+ 'README.md',
348
+ 'AGENTS.md',
349
+ 'AGENTS-consumer.md',
350
+ 'SKILLS.md',
351
+ '.grok-plugin/plugin.json',
352
+ 'docs/README.md',
353
+ 'docs-site/docs/index.md',
354
+ 'docs-site/docs/introduction.md',
355
+ 'docs-site/docs/guides/getting-started.md',
356
+ 'docs-site/docs/full-reference.md',
357
+ 'docs-site/docs/guides/integrations.md',
358
+ 'docs-site/docs/guides/features-since-3.1.md',
359
+ 'docs-site/docs/guides/features-json.md',
360
+ 'docs-site/docs/guides/memory-routing.md',
361
+ 'docs-site/docs/guides/aside-context.md',
362
+ 'docs-site/docs/guides/repertoire.md',
363
+ 'docs-site/docs/guides/consumer-migration.md',
364
+ 'docs-site/docs/mcp/README.md',
365
+ 'docs-site/docs/agents/README.md',
366
+ 'docs-site/docs/architecture/GROK_GUIDE.md',
367
+ 'docs-site/sidebars.ts',
368
+ 'xray/features.json',
369
+ ];
370
+ return candidates.filter((rel) => fs.existsSync(path.join(baseDir, rel)));
371
+ }
372
+
373
+ /** Update CHANGELOG + README + AGENTS (+ consumer/docs) for current package.json version (no bump). */
374
+ function updateReleaseArtifactsOnly(changeDescription = '') {
375
+ const current = getCurrentVersion();
376
+ const counts = getFrameworkCounts();
377
+ process.stdout.write(`Release artifacts for v${current} (JSON + CHANGELOG only)\n`);
378
+ process.stdout.write(`counts: ${counts.agents} agents, ${counts.mcps} MCPs, ${counts.skills} skills\n`);
379
+ updateChangelog(current, changeDescription);
380
+ updatePluginJsonVersion(current);
381
+ updateFeaturesJsonVersion(current);
382
+ updateOpenclawPluginVersion(current);
383
+ runReleaseDocsValidation();
384
+ process.stdout.write(`Release artifacts updated for v${current}\n`);
385
+ }
386
+
387
+ function runReleaseDocsValidation() {
388
+ try {
389
+ execSync(`${JSON.stringify(process.execPath)} ${JSON.stringify(millScript('validate-release-docs.mjs'))}`, {
390
+ cwd: rootDir,
391
+ stdio: 'inherit',
392
+ env: { ...process.env, FOUNDRY_ROOT: rootDir },
393
+ });
394
+ } catch {
395
+ console.error('\n❌ Release artifact docs failed validation — fix before tagging\n');
396
+ process.exit(1);
397
+ }
398
+ }
399
+
400
+ function refuseBump(reason) {
401
+ process.stderr.write(`${reason}\n`);
402
+ process.stderr.write('One bumper: npx @0xray/foundry reconcile [patch|minor|major] --apply\n');
403
+ process.stderr.write('Then stamp: npx @0xray/foundry stamp\n');
404
+ process.stderr.write('Ship: npx @0xray/foundry release [patch|minor|major]\n');
405
+ process.exit(1);
406
+ }
407
+
408
+ function main() {
409
+ const args = process.argv.slice(2);
410
+
411
+ if (args.includes('--help') || args.includes('-h')) {
412
+ process.stdout.write(`Current version: ${getCurrentVersion()}\n`);
413
+ process.stdout.write('Usage: npx @0xray/foundry stamp\n');
414
+ process.stdout.write('Bump is refused. Use reconcile-version.mjs --apply, then this stamper.\n');
415
+ process.exit(0);
416
+ }
417
+
418
+ if (args.includes('--artifacts-only')) {
419
+ updateReleaseArtifactsOnly('');
420
+ return;
421
+ }
422
+
423
+ const bumpish = args.find(
424
+ (a) =>
425
+ a === '--tag' ||
426
+ a === '-t' ||
427
+ a === 'major' ||
428
+ a === 'minor' ||
429
+ a === 'patch' ||
430
+ /^\d+\.\d+\.\d+/.test(a),
431
+ );
432
+ if (bumpish) {
433
+ refuseBump(`version-manager does not bump or tag (got ${bumpish}).`);
434
+ }
435
+
436
+ refuseBump('version-manager is artifacts-only. Pass --artifacts-only.');
437
+ }
438
+
439
+ const isMainModule =
440
+ process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
441
+
442
+ if (isMainModule) {
443
+ main();
444
+ }