@rune-kit/rune 2.1.1

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 (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +357 -0
  3. package/agents/.gitkeep +0 -0
  4. package/agents/architect.md +29 -0
  5. package/agents/asset-creator.md +11 -0
  6. package/agents/audit.md +11 -0
  7. package/agents/autopsy.md +11 -0
  8. package/agents/brainstorm.md +11 -0
  9. package/agents/browser-pilot.md +11 -0
  10. package/agents/coder.md +29 -0
  11. package/agents/completion-gate.md +11 -0
  12. package/agents/constraint-check.md +11 -0
  13. package/agents/context-engine.md +11 -0
  14. package/agents/cook.md +11 -0
  15. package/agents/db.md +11 -0
  16. package/agents/debug.md +11 -0
  17. package/agents/dependency-doctor.md +11 -0
  18. package/agents/deploy.md +11 -0
  19. package/agents/design.md +11 -0
  20. package/agents/docs-seeker.md +11 -0
  21. package/agents/fix.md +11 -0
  22. package/agents/hallucination-guard.md +11 -0
  23. package/agents/incident.md +11 -0
  24. package/agents/integrity-check.md +11 -0
  25. package/agents/journal.md +11 -0
  26. package/agents/launch.md +11 -0
  27. package/agents/logic-guardian.md +11 -0
  28. package/agents/marketing.md +11 -0
  29. package/agents/onboard.md +11 -0
  30. package/agents/perf.md +11 -0
  31. package/agents/plan.md +11 -0
  32. package/agents/preflight.md +11 -0
  33. package/agents/problem-solver.md +11 -0
  34. package/agents/rescue.md +11 -0
  35. package/agents/research.md +11 -0
  36. package/agents/researcher.md +29 -0
  37. package/agents/review-intake.md +11 -0
  38. package/agents/review.md +11 -0
  39. package/agents/reviewer.md +28 -0
  40. package/agents/safeguard.md +11 -0
  41. package/agents/sast.md +11 -0
  42. package/agents/scanner.md +28 -0
  43. package/agents/scope-guard.md +11 -0
  44. package/agents/scout.md +11 -0
  45. package/agents/sentinel.md +11 -0
  46. package/agents/sequential-thinking.md +11 -0
  47. package/agents/session-bridge.md +11 -0
  48. package/agents/skill-forge.md +11 -0
  49. package/agents/skill-router.md +11 -0
  50. package/agents/surgeon.md +11 -0
  51. package/agents/team.md +11 -0
  52. package/agents/test.md +11 -0
  53. package/agents/trend-scout.md +11 -0
  54. package/agents/verification.md +11 -0
  55. package/agents/video-creator.md +11 -0
  56. package/agents/watchdog.md +11 -0
  57. package/agents/worktree.md +11 -0
  58. package/commands/.gitkeep +0 -0
  59. package/commands/rune.md +168 -0
  60. package/compiler/__tests__/openclaw-adapter.test.js +140 -0
  61. package/compiler/__tests__/parser.test.js +55 -0
  62. package/compiler/adapters/antigravity.js +59 -0
  63. package/compiler/adapters/claude.js +37 -0
  64. package/compiler/adapters/cursor.js +67 -0
  65. package/compiler/adapters/generic.js +60 -0
  66. package/compiler/adapters/index.js +45 -0
  67. package/compiler/adapters/openclaw.js +150 -0
  68. package/compiler/adapters/windsurf.js +60 -0
  69. package/compiler/bin/rune.js +288 -0
  70. package/compiler/doctor.js +153 -0
  71. package/compiler/emitter.js +240 -0
  72. package/compiler/parser.js +208 -0
  73. package/compiler/transformer.js +69 -0
  74. package/compiler/transforms/branding.js +27 -0
  75. package/compiler/transforms/cross-references.js +29 -0
  76. package/compiler/transforms/frontmatter.js +38 -0
  77. package/compiler/transforms/hooks.js +68 -0
  78. package/compiler/transforms/subagents.js +36 -0
  79. package/compiler/transforms/tool-names.js +60 -0
  80. package/contexts/dev.md +34 -0
  81. package/contexts/research.md +43 -0
  82. package/contexts/review.md +55 -0
  83. package/extensions/ai-ml/PACK.md +517 -0
  84. package/extensions/analytics/PACK.md +557 -0
  85. package/extensions/backend/PACK.md +678 -0
  86. package/extensions/chrome-ext/PACK.md +995 -0
  87. package/extensions/content/PACK.md +381 -0
  88. package/extensions/devops/PACK.md +520 -0
  89. package/extensions/ecommerce/PACK.md +280 -0
  90. package/extensions/gamedev/PACK.md +393 -0
  91. package/extensions/mobile/PACK.md +273 -0
  92. package/extensions/saas/PACK.md +805 -0
  93. package/extensions/security/PACK.md +536 -0
  94. package/extensions/trading/PACK.md +597 -0
  95. package/extensions/ui/PACK.md +947 -0
  96. package/package.json +47 -0
  97. package/skills/.gitkeep +0 -0
  98. package/skills/adversary/SKILL.md +271 -0
  99. package/skills/asset-creator/SKILL.md +157 -0
  100. package/skills/audit/SKILL.md +466 -0
  101. package/skills/autopsy/SKILL.md +200 -0
  102. package/skills/ba/SKILL.md +279 -0
  103. package/skills/brainstorm/SKILL.md +266 -0
  104. package/skills/browser-pilot/SKILL.md +168 -0
  105. package/skills/completion-gate/SKILL.md +151 -0
  106. package/skills/constraint-check/SKILL.md +165 -0
  107. package/skills/context-engine/SKILL.md +176 -0
  108. package/skills/cook/SKILL.md +636 -0
  109. package/skills/db/SKILL.md +256 -0
  110. package/skills/debug/SKILL.md +240 -0
  111. package/skills/dependency-doctor/SKILL.md +235 -0
  112. package/skills/deploy/SKILL.md +174 -0
  113. package/skills/design/DESIGN-REFERENCE.md +365 -0
  114. package/skills/design/SKILL.md +462 -0
  115. package/skills/doc-processor/SKILL.md +254 -0
  116. package/skills/docs/SKILL.md +336 -0
  117. package/skills/docs-seeker/SKILL.md +166 -0
  118. package/skills/fix/SKILL.md +192 -0
  119. package/skills/git/SKILL.md +285 -0
  120. package/skills/hallucination-guard/SKILL.md +204 -0
  121. package/skills/incident/SKILL.md +241 -0
  122. package/skills/integrity-check/SKILL.md +169 -0
  123. package/skills/journal/SKILL.md +190 -0
  124. package/skills/launch/SKILL.md +330 -0
  125. package/skills/logic-guardian/SKILL.md +240 -0
  126. package/skills/marketing/SKILL.md +229 -0
  127. package/skills/mcp-builder/SKILL.md +311 -0
  128. package/skills/onboard/SKILL.md +298 -0
  129. package/skills/perf/SKILL.md +297 -0
  130. package/skills/plan/SKILL.md +520 -0
  131. package/skills/preflight/SKILL.md +231 -0
  132. package/skills/problem-solver/SKILL.md +284 -0
  133. package/skills/rescue/SKILL.md +434 -0
  134. package/skills/research/SKILL.md +122 -0
  135. package/skills/review/SKILL.md +354 -0
  136. package/skills/review-intake/SKILL.md +222 -0
  137. package/skills/safeguard/SKILL.md +188 -0
  138. package/skills/sast/SKILL.md +190 -0
  139. package/skills/scaffold/SKILL.md +276 -0
  140. package/skills/scope-guard/SKILL.md +150 -0
  141. package/skills/scout/SKILL.md +232 -0
  142. package/skills/sentinel/SKILL.md +320 -0
  143. package/skills/sentinel-env/SKILL.md +226 -0
  144. package/skills/sequential-thinking/SKILL.md +234 -0
  145. package/skills/session-bridge/SKILL.md +287 -0
  146. package/skills/skill-forge/SKILL.md +317 -0
  147. package/skills/skill-router/SKILL.md +267 -0
  148. package/skills/surgeon/SKILL.md +203 -0
  149. package/skills/team/SKILL.md +397 -0
  150. package/skills/test/SKILL.md +271 -0
  151. package/skills/trend-scout/SKILL.md +145 -0
  152. package/skills/verification/SKILL.md +201 -0
  153. package/skills/video-creator/SKILL.md +201 -0
  154. package/skills/watchdog/SKILL.md +166 -0
  155. package/skills/worktree/SKILL.md +140 -0
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Rune CLI
5
+ *
6
+ * Commands:
7
+ * rune init — Interactive setup for a new project
8
+ * rune build — Compile skills for the configured platform
9
+ * rune doctor — Validate compiled output
10
+ */
11
+
12
+ import { readFile, writeFile } from 'node:fs/promises';
13
+ import { existsSync } from 'node:fs';
14
+ import path from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { createInterface } from 'node:readline';
17
+ import { getAdapter, listPlatforms } from '../adapters/index.js';
18
+ import { buildAll } from '../emitter.js';
19
+ import { runDoctor, formatDoctorResults } from '../doctor.js';
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = path.dirname(__filename);
23
+ const RUNE_ROOT = path.resolve(__dirname, '../..');
24
+
25
+ const CONFIG_FILE = 'rune.config.json';
26
+
27
+ // ─── Helpers ───
28
+
29
+ function log(msg) { console.log(msg); }
30
+ function logStep(icon, msg) { console.log(` ${icon} ${msg}`); }
31
+
32
+ async function readConfig(projectRoot) {
33
+ const configPath = path.join(projectRoot, CONFIG_FILE);
34
+ if (!existsSync(configPath)) return null;
35
+ return JSON.parse(await readFile(configPath, 'utf-8'));
36
+ }
37
+
38
+ async function writeConfig(projectRoot, config) {
39
+ const configPath = path.join(projectRoot, CONFIG_FILE);
40
+ await writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
41
+ }
42
+
43
+ function detectPlatform(projectRoot) {
44
+ if (existsSync(path.join(projectRoot, '.claude-plugin'))) return 'claude';
45
+ if (existsSync(path.join(projectRoot, '.cursor'))) return 'cursor';
46
+ if (existsSync(path.join(projectRoot, '.windsurf'))) return 'windsurf';
47
+ if (existsSync(path.join(projectRoot, '.agent'))) return 'antigravity';
48
+ if (existsSync(path.join(projectRoot, '.openclaw'))) return 'openclaw';
49
+ return null;
50
+ }
51
+
52
+ function discoverExtensions() {
53
+ const extDir = path.join(RUNE_ROOT, 'extensions');
54
+ if (!existsSync(extDir)) return [];
55
+ return [];
56
+ }
57
+
58
+ async function prompt(question) {
59
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
60
+ return new Promise(resolve => {
61
+ rl.question(question, answer => {
62
+ rl.close();
63
+ resolve(answer.trim());
64
+ });
65
+ });
66
+ }
67
+
68
+ // ─── Commands ───
69
+
70
+ async function cmdInit(projectRoot, args) {
71
+ log('');
72
+ log(' ╭──────────────────────────────────────────╮');
73
+ log(' │ Rune — Less skills. Deeper connections. │');
74
+ log(' ╰──────────────────────────────────────────╯');
75
+ log('');
76
+
77
+ // Platform detection / selection
78
+ let platform = args.platform || detectPlatform(projectRoot);
79
+
80
+ if (platform) {
81
+ logStep('→', `Detected: ${platform}`);
82
+ } else {
83
+ log(' Available platforms: ' + listPlatforms().join(', '));
84
+ const answer = await prompt(' ? Select platform: ');
85
+ platform = answer.toLowerCase();
86
+ if (!listPlatforms().includes(platform)) {
87
+ platform = 'generic';
88
+ logStep('→', `Unknown platform, using generic adapter`);
89
+ }
90
+ }
91
+
92
+ if (platform === 'claude') {
93
+ logStep('✓', 'Claude Code detected — Rune works as a native plugin. No compilation needed.');
94
+ log('');
95
+ return;
96
+ }
97
+
98
+ // Extension pack selection
99
+ const extensions = args.extensions
100
+ ? args.extensions.split(',')
101
+ : null; // null = all
102
+
103
+ // Build config
104
+ const config = {
105
+ $schema: 'https://rune-kit.github.io/rune/config-schema.json',
106
+ version: 1,
107
+ platform,
108
+ source: RUNE_ROOT,
109
+ skills: {
110
+ disabled: args.disable ? args.disable.split(',') : [],
111
+ },
112
+ extensions: {
113
+ enabled: extensions,
114
+ },
115
+ output: {
116
+ index: true,
117
+ },
118
+ };
119
+
120
+ await writeConfig(projectRoot, config);
121
+ logStep('✓', 'Created rune.config.json');
122
+
123
+ // Auto-build
124
+ const adapter = getAdapter(platform);
125
+ const stats = await buildAll({
126
+ runeRoot: RUNE_ROOT,
127
+ outputRoot: projectRoot,
128
+ adapter,
129
+ disabledSkills: config.skills.disabled,
130
+ enabledPacks: config.extensions.enabled,
131
+ });
132
+
133
+ logStep('✓', `Built ${stats.skillCount} skills + ${stats.packCount} extensions to ${adapter.outputDir}/`);
134
+
135
+ if (stats.errors.length > 0) {
136
+ for (const err of stats.errors) {
137
+ logStep('✗', `Error: ${err.file} — ${err.error}`);
138
+ }
139
+ }
140
+
141
+ log('');
142
+ log(' Next: Start coding. Rune skills are active in your AI assistant.');
143
+ log('');
144
+ }
145
+
146
+ async function cmdBuild(projectRoot, args) {
147
+ const config = await readConfig(projectRoot);
148
+
149
+ const platform = args.platform || config?.platform;
150
+ if (!platform) {
151
+ log(' ✗ No platform configured. Run `rune init` first.');
152
+ process.exit(1);
153
+ }
154
+
155
+ if (platform === 'claude') {
156
+ log(' Claude Code uses source SKILL.md files directly. No compilation needed.');
157
+ return;
158
+ }
159
+
160
+ const adapter = getAdapter(platform);
161
+ const runeRoot = config?.source || RUNE_ROOT;
162
+ const outputRoot = args.output || projectRoot;
163
+ const disabledSkills = config?.skills?.disabled || [];
164
+ const enabledPacks = config?.extensions?.enabled || null;
165
+
166
+ log('');
167
+ log(` [parse] Discovering skills...`);
168
+
169
+ const stats = await buildAll({
170
+ runeRoot,
171
+ outputRoot,
172
+ adapter,
173
+ disabledSkills,
174
+ enabledPacks,
175
+ });
176
+
177
+ log(` [transform] Platform: ${stats.platform}`);
178
+ log(` [transform] Resolved ${stats.crossRefsResolved} cross-references`);
179
+ log(` [transform] Resolved ${stats.toolRefsResolved} tool-name references`);
180
+ log(` [emit] ${stats.skillCount} skills + ${stats.packCount} extensions`);
181
+
182
+ if (stats.skipped.length > 0) {
183
+ log(` [skip] ${stats.skipped.length} disabled: ${stats.skipped.join(', ')}`);
184
+ }
185
+
186
+ if (stats.errors.length > 0) {
187
+ for (const err of stats.errors) {
188
+ log(` [error] ${err.file}: ${err.error}`);
189
+ }
190
+ }
191
+
192
+ log('');
193
+ log(` ✓ Built ${stats.files.length} files to ${adapter.outputDir}/`);
194
+ log('');
195
+ }
196
+
197
+ async function cmdDoctor(projectRoot, args) {
198
+ const config = await readConfig(projectRoot);
199
+
200
+ if (!config) {
201
+ log(' ✗ No rune.config.json found. Run `rune init` first.');
202
+ process.exit(1);
203
+ }
204
+
205
+ const platform = args.platform || config.platform;
206
+ const adapter = getAdapter(platform);
207
+ const runeRoot = config.source || RUNE_ROOT;
208
+
209
+ const results = await runDoctor({
210
+ outputRoot: projectRoot,
211
+ adapter,
212
+ config,
213
+ runeRoot,
214
+ });
215
+
216
+ log(formatDoctorResults(results));
217
+
218
+ if (!results.healthy) process.exit(1);
219
+ }
220
+
221
+ // ─── Arg Parsing ───
222
+
223
+ function parseArgs(argv) {
224
+ const args = {};
225
+ const positional = [];
226
+
227
+ for (let i = 0; i < argv.length; i++) {
228
+ const arg = argv[i];
229
+ if (arg.startsWith('--')) {
230
+ const key = arg.slice(2);
231
+ const next = argv[i + 1];
232
+ if (next && !next.startsWith('--')) {
233
+ args[key] = next;
234
+ i++;
235
+ } else {
236
+ args[key] = true;
237
+ }
238
+ } else {
239
+ positional.push(arg);
240
+ }
241
+ }
242
+
243
+ return { command: positional[0], args };
244
+ }
245
+
246
+ // ─── Main ───
247
+
248
+ async function main() {
249
+ const { command, args } = parseArgs(process.argv.slice(2));
250
+ const projectRoot = process.cwd();
251
+
252
+ switch (command) {
253
+ case 'init':
254
+ await cmdInit(projectRoot, args);
255
+ break;
256
+ case 'build':
257
+ await cmdBuild(projectRoot, args);
258
+ break;
259
+ case 'doctor':
260
+ await cmdDoctor(projectRoot, args);
261
+ break;
262
+ case 'help':
263
+ case '--help':
264
+ case undefined:
265
+ log('');
266
+ log(' Rune CLI — Skill mesh for AI coding assistants');
267
+ log('');
268
+ log(' Commands:');
269
+ log(' init Interactive setup (auto-detects platform)');
270
+ log(' build Compile skills for configured platform');
271
+ log(' doctor Validate compiled output');
272
+ log('');
273
+ log(' Options:');
274
+ log(' --platform <name> Override platform (cursor, windsurf, antigravity, openclaw, generic)');
275
+ log(' --output <dir> Override output directory');
276
+ log(' --disable <skills> Comma-separated skills to disable');
277
+ log('');
278
+ break;
279
+ default:
280
+ log(` ✗ Unknown command: ${command}. Run \`rune help\` for usage.`);
281
+ process.exit(1);
282
+ }
283
+ }
284
+
285
+ main().catch(err => {
286
+ console.error(' ✗ Fatal:', err.message);
287
+ process.exit(1);
288
+ });
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Doctor — Validates compiled output
3
+ *
4
+ * Checks: files exist, cross-references resolve, layer discipline, source freshness.
5
+ */
6
+
7
+ import { readdir, readFile, stat } from 'node:fs/promises';
8
+ import { existsSync } from 'node:fs';
9
+ import path from 'node:path';
10
+
11
+ /**
12
+ * Run doctor checks on compiled output
13
+ *
14
+ * @param {object} options
15
+ * @param {string} options.outputRoot - project root
16
+ * @param {object} options.adapter - platform adapter
17
+ * @param {object} options.config - rune.config.json contents
18
+ * @param {string} options.runeRoot - rune source root
19
+ * @returns {Promise<object>} doctor results
20
+ */
21
+ export async function runDoctor({ outputRoot, adapter, config, runeRoot }) {
22
+ const results = {
23
+ platform: adapter.name,
24
+ checks: [],
25
+ warnings: [],
26
+ errors: [],
27
+ healthy: true,
28
+ };
29
+
30
+ // Check 1: Config exists
31
+ const configPath = path.join(outputRoot, 'rune.config.json');
32
+ if (existsSync(configPath)) {
33
+ results.checks.push({ name: 'Config file', status: 'pass' });
34
+ } else {
35
+ results.checks.push({ name: 'Config file', status: 'fail', detail: 'rune.config.json not found' });
36
+ results.errors.push('rune.config.json not found. Run `rune init` first.');
37
+ results.healthy = false;
38
+ }
39
+
40
+ // Check 2: Output directory exists
41
+ if (adapter.name === 'claude') {
42
+ results.checks.push({ name: 'Output directory', status: 'skip', detail: 'Claude Code uses source directly' });
43
+ return results;
44
+ }
45
+
46
+ const outputDir = path.join(outputRoot, adapter.outputDir);
47
+ if (existsSync(outputDir)) {
48
+ results.checks.push({ name: 'Output directory', status: 'pass', detail: outputDir });
49
+ } else {
50
+ results.checks.push({ name: 'Output directory', status: 'fail', detail: `${outputDir} not found` });
51
+ results.errors.push(`Output directory ${outputDir} not found. Run \`rune build\` first.`);
52
+ results.healthy = false;
53
+ return results;
54
+ }
55
+
56
+ // Check 3: Count skill files
57
+ const files = await readdir(outputDir);
58
+ const skillFiles = files.filter(f => f.startsWith('rune-') && f !== `rune-index${adapter.fileExtension}`);
59
+ const expectedSkillCount = 55 - (config.skills?.disabled?.length || 0);
60
+
61
+ if (skillFiles.length >= expectedSkillCount) {
62
+ results.checks.push({ name: 'Skill files', status: 'pass', detail: `${skillFiles.length}/${expectedSkillCount}` });
63
+ } else {
64
+ results.checks.push({ name: 'Skill files', status: 'warn', detail: `${skillFiles.length}/${expectedSkillCount} present` });
65
+ results.warnings.push(`Expected ${expectedSkillCount} skill files, found ${skillFiles.length}`);
66
+ }
67
+
68
+ // Check 4: Cross-reference integrity
69
+ const crossRefErrors = await checkCrossRefs(outputDir, skillFiles, adapter);
70
+ if (crossRefErrors.length === 0) {
71
+ results.checks.push({ name: 'Cross-references', status: 'pass' });
72
+ } else {
73
+ results.checks.push({ name: 'Cross-references', status: 'warn', detail: `${crossRefErrors.length} dangling` });
74
+ results.warnings.push(...crossRefErrors);
75
+ }
76
+
77
+ // Check 5: Index file exists
78
+ const indexFile = `rune-index${adapter.fileExtension}`;
79
+ if (files.includes(indexFile)) {
80
+ results.checks.push({ name: 'Index file', status: 'pass' });
81
+ } else {
82
+ results.checks.push({ name: 'Index file', status: 'warn', detail: 'Missing index file' });
83
+ results.warnings.push('Index file not found. Rebuild with `rune build`.');
84
+ }
85
+
86
+ // Check 6: Disabled skills warning
87
+ const disabled = config.skills?.disabled || [];
88
+ if (disabled.length > 0) {
89
+ results.warnings.push(`${disabled.length} skills disabled: ${disabled.join(', ')}`);
90
+ }
91
+
92
+ if (results.errors.length > 0) results.healthy = false;
93
+
94
+ return results;
95
+ }
96
+
97
+ /**
98
+ * Check that all cross-references in compiled files point to existing files
99
+ */
100
+ async function checkCrossRefs(outputDir, files, adapter) {
101
+ const errors = [];
102
+ const fileSet = new Set(files);
103
+
104
+ for (const file of files) {
105
+ const content = await readFile(path.join(outputDir, file), 'utf-8');
106
+
107
+ // Look for references to other rune skills
108
+ const refPattern = /rune-([a-z][\w-]*)/g;
109
+ let match;
110
+ while ((match = refPattern.exec(content)) !== null) {
111
+ const refName = match[1];
112
+ const expectedFile = `rune-${refName}${adapter.fileExtension}`;
113
+ if (!fileSet.has(expectedFile) && refName !== 'index' && refName !== 'kit') {
114
+ errors.push(`${file}: references rune-${refName} but ${expectedFile} not found`);
115
+ }
116
+ }
117
+ }
118
+
119
+ return [...new Set(errors)]; // deduplicate
120
+ }
121
+
122
+ /**
123
+ * Format doctor results for console output
124
+ */
125
+ export function formatDoctorResults(results) {
126
+ const lines = [];
127
+ lines.push(`\n Platform: ${results.platform}`);
128
+
129
+ for (const check of results.checks) {
130
+ const icon = check.status === 'pass' ? '✓' : check.status === 'warn' ? '!' : '✗';
131
+ const detail = check.detail ? ` (${check.detail})` : '';
132
+ lines.push(` [${icon}] ${check.name}${detail}`);
133
+ }
134
+
135
+ if (results.warnings.length > 0) {
136
+ lines.push('');
137
+ for (const w of results.warnings) {
138
+ lines.push(` ⚠ ${w}`);
139
+ }
140
+ }
141
+
142
+ if (results.errors.length > 0) {
143
+ lines.push('');
144
+ for (const e of results.errors) {
145
+ lines.push(` ✗ ${e}`);
146
+ }
147
+ }
148
+
149
+ lines.push('');
150
+ lines.push(results.healthy ? ' ✓ Rune installation healthy' : ' ✗ Rune installation has issues');
151
+
152
+ return lines.join('\n');
153
+ }
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Emitter
3
+ *
4
+ * Writes transformed skill files to the platform's output directory.
5
+ * Handles file naming, directory creation, and index generation.
6
+ */
7
+
8
+ import { readdir, readFile, mkdir, writeFile } from 'node:fs/promises';
9
+ import { existsSync } from 'node:fs';
10
+ import path from 'node:path';
11
+ import { parseSkill, parsePack } from './parser.js';
12
+ import { transformSkill } from './transformer.js';
13
+
14
+ /**
15
+ * Discover all SKILL.md files in the skills directory
16
+ *
17
+ * @param {string} skillsDir - path to skills/ directory
18
+ * @returns {Promise<string[]>} array of SKILL.md file paths
19
+ */
20
+ async function discoverSkills(skillsDir) {
21
+ const entries = await readdir(skillsDir, { withFileTypes: true });
22
+ const paths = [];
23
+
24
+ for (const entry of entries) {
25
+ if (!entry.isDirectory()) continue;
26
+ const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
27
+ if (existsSync(skillFile)) {
28
+ paths.push(skillFile);
29
+ }
30
+ }
31
+
32
+ return paths.sort();
33
+ }
34
+
35
+ /**
36
+ * Discover all PACK.md files in the extensions directory
37
+ *
38
+ * @param {string} extensionsDir - path to extensions/ directory
39
+ * @param {string[]} [enabledPacks] - list of enabled pack names (null = all)
40
+ * @returns {Promise<string[]>} array of PACK.md file paths
41
+ */
42
+ async function discoverPacks(extensionsDir, enabledPacks = null) {
43
+ if (!existsSync(extensionsDir)) return [];
44
+
45
+ const entries = await readdir(extensionsDir, { withFileTypes: true });
46
+ const paths = [];
47
+
48
+ for (const entry of entries) {
49
+ if (!entry.isDirectory()) continue;
50
+ if (enabledPacks && !enabledPacks.includes(entry.name) && !enabledPacks.includes(`@rune/${entry.name}`)) {
51
+ continue;
52
+ }
53
+ const packFile = path.join(extensionsDir, entry.name, 'PACK.md');
54
+ if (existsSync(packFile)) {
55
+ paths.push(packFile);
56
+ }
57
+ }
58
+
59
+ return paths.sort();
60
+ }
61
+
62
+ /**
63
+ * Generate output filename for a skill
64
+ */
65
+ function outputFileName(skillName, adapter) {
66
+ return `${adapter.skillPrefix}${skillName}${adapter.skillSuffix}${adapter.fileExtension}`;
67
+ }
68
+
69
+ /**
70
+ * Build all skills for a target platform
71
+ *
72
+ * @param {object} options
73
+ * @param {string} options.runeRoot - root of the Rune repo
74
+ * @param {string} options.outputRoot - where to write output (project root or dist/)
75
+ * @param {object} options.adapter - platform adapter
76
+ * @param {string[]} [options.disabledSkills] - skills to skip
77
+ * @param {string[]} [options.enabledPacks] - extension packs to include (null = all)
78
+ * @returns {Promise<object>} build result stats
79
+ */
80
+ export async function buildAll({ runeRoot, outputRoot, adapter, disabledSkills = [], enabledPacks = null }) {
81
+ // Claude Code = passthrough, no build needed
82
+ if (adapter.name === 'claude') {
83
+ return {
84
+ platform: 'claude',
85
+ message: 'Claude Code uses source SKILL.md files directly. No compilation needed.',
86
+ skillCount: 0,
87
+ packCount: 0,
88
+ files: [],
89
+ };
90
+ }
91
+
92
+ const skillsDir = path.join(runeRoot, 'skills');
93
+ const extensionsDir = path.join(runeRoot, 'extensions');
94
+ const outputDir = path.join(outputRoot, adapter.outputDir);
95
+
96
+ // Ensure output directory exists
97
+ await mkdir(outputDir, { recursive: true });
98
+
99
+ const skillPaths = await discoverSkills(skillsDir);
100
+ const packPaths = await discoverPacks(extensionsDir, enabledPacks);
101
+
102
+ const stats = {
103
+ platform: adapter.name,
104
+ skillCount: 0,
105
+ packCount: 0,
106
+ crossRefsResolved: 0,
107
+ toolRefsResolved: 0,
108
+ files: [],
109
+ skipped: [],
110
+ errors: [],
111
+ };
112
+
113
+ // Build skills
114
+ for (const skillPath of skillPaths) {
115
+ try {
116
+ const content = await readFile(skillPath, 'utf-8');
117
+ const parsed = parseSkill(content, skillPath);
118
+
119
+ // Check disabled
120
+ if (disabledSkills.includes(parsed.name)) {
121
+ stats.skipped.push(parsed.name);
122
+ continue;
123
+ }
124
+
125
+ const { header, body, footer } = transformSkill(parsed, adapter);
126
+ const output = [header, body, footer].filter(Boolean).join('\n');
127
+ const fileName = outputFileName(parsed.name, adapter);
128
+ const outputPath = path.join(outputDir, fileName);
129
+
130
+ await writeFile(outputPath, output, 'utf-8');
131
+
132
+ stats.skillCount++;
133
+ stats.crossRefsResolved += parsed.crossRefs.length;
134
+ stats.toolRefsResolved += parsed.toolRefs.length;
135
+ stats.files.push(fileName);
136
+ } catch (err) {
137
+ stats.errors.push({ file: skillPath, error: err.message });
138
+ }
139
+ }
140
+
141
+ // Build extension packs
142
+ for (const packPath of packPaths) {
143
+ try {
144
+ const content = await readFile(packPath, 'utf-8');
145
+ const parsed = parsePack(content, packPath);
146
+ const { header, body, footer } = transformSkill(parsed, adapter);
147
+ const output = [header, body, footer].filter(Boolean).join('\n');
148
+ const packName = path.basename(path.dirname(packPath));
149
+ const fileName = outputFileName(`ext-${packName}`, adapter);
150
+ const outputPath = path.join(outputDir, fileName);
151
+
152
+ await writeFile(outputPath, output, 'utf-8');
153
+
154
+ stats.packCount++;
155
+ stats.files.push(fileName);
156
+ } catch (err) {
157
+ stats.errors.push({ file: packPath, error: err.message });
158
+ }
159
+ }
160
+
161
+ // Generate index file
162
+ const indexContent = generateIndex(stats, adapter);
163
+ const indexFileName = outputFileName('index', adapter);
164
+ await writeFile(path.join(outputDir, indexFileName), indexContent, 'utf-8');
165
+ stats.files.push(indexFileName);
166
+
167
+ // OpenClaw adapter: generate manifest + TypeScript entry point
168
+ if (adapter.name === 'openclaw' && adapter.generateManifest && adapter.generateEntryPoint) {
169
+ const pluginJsonPath = path.join(runeRoot, '.claude-plugin', 'plugin.json');
170
+ let pluginJson = { version: '0.0.0' };
171
+ if (existsSync(pluginJsonPath)) {
172
+ pluginJson = JSON.parse(await readFile(pluginJsonPath, 'utf-8'));
173
+ }
174
+
175
+ // Collect parsed skills for manifest/entry generation
176
+ const parsedSkills = [];
177
+ for (const sp of skillPaths) {
178
+ try {
179
+ const c = await readFile(sp, 'utf-8');
180
+ parsedSkills.push(parseSkill(c, sp));
181
+ } catch { /* skip on error */ }
182
+ }
183
+
184
+ // Read skill-router content for system prompt injection
185
+ const routerPath = path.join(runeRoot, 'skills', 'skill-router', 'SKILL.md');
186
+ let routerContent = '';
187
+ if (existsSync(routerPath)) {
188
+ routerContent = await readFile(routerPath, 'utf-8');
189
+ }
190
+
191
+ // Write openclaw.plugin.json to parent of skills dir (.openclaw/rune/)
192
+ const openclawRoot = path.resolve(outputDir, '..');
193
+ const manifest = adapter.generateManifest(parsedSkills, pluginJson);
194
+ await writeFile(
195
+ path.join(openclawRoot, 'openclaw.plugin.json'),
196
+ JSON.stringify(manifest, null, 2) + '\n',
197
+ 'utf-8',
198
+ );
199
+ stats.files.push('openclaw.plugin.json');
200
+
201
+ // Write src/index.ts entry point
202
+ const srcDir = path.join(openclawRoot, 'src');
203
+ await mkdir(srcDir, { recursive: true });
204
+ const entryPoint = adapter.generateEntryPoint(parsedSkills, routerContent);
205
+ await writeFile(path.join(srcDir, 'index.ts'), entryPoint, 'utf-8');
206
+ stats.files.push('src/index.ts');
207
+ }
208
+
209
+ return stats;
210
+ }
211
+
212
+ /**
213
+ * Generate an index file listing all compiled skills
214
+ */
215
+ function generateIndex(stats, adapter) {
216
+ const lines = [
217
+ '# Rune Skill Index',
218
+ '',
219
+ `> Platform: ${adapter.name} | Skills: ${stats.skillCount} | Extensions: ${stats.packCount}`,
220
+ '',
221
+ '## Core Skills',
222
+ '',
223
+ ...stats.files
224
+ .filter(f => !f.includes('ext-') && !f.includes('index'))
225
+ .map(f => `- ${f}`),
226
+ '',
227
+ ];
228
+
229
+ const extFiles = stats.files.filter(f => f.includes('ext-'));
230
+ if (extFiles.length > 0) {
231
+ lines.push('## Extension Packs', '', ...extFiles.map(f => `- ${f}`), '');
232
+ }
233
+
234
+ lines.push(
235
+ '---',
236
+ '> Rune Skill Mesh — https://github.com/rune-kit/rune',
237
+ );
238
+
239
+ return lines.join('\n');
240
+ }