@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.
- package/LICENSE +21 -0
- package/README.md +357 -0
- package/agents/.gitkeep +0 -0
- package/agents/architect.md +29 -0
- package/agents/asset-creator.md +11 -0
- package/agents/audit.md +11 -0
- package/agents/autopsy.md +11 -0
- package/agents/brainstorm.md +11 -0
- package/agents/browser-pilot.md +11 -0
- package/agents/coder.md +29 -0
- package/agents/completion-gate.md +11 -0
- package/agents/constraint-check.md +11 -0
- package/agents/context-engine.md +11 -0
- package/agents/cook.md +11 -0
- package/agents/db.md +11 -0
- package/agents/debug.md +11 -0
- package/agents/dependency-doctor.md +11 -0
- package/agents/deploy.md +11 -0
- package/agents/design.md +11 -0
- package/agents/docs-seeker.md +11 -0
- package/agents/fix.md +11 -0
- package/agents/hallucination-guard.md +11 -0
- package/agents/incident.md +11 -0
- package/agents/integrity-check.md +11 -0
- package/agents/journal.md +11 -0
- package/agents/launch.md +11 -0
- package/agents/logic-guardian.md +11 -0
- package/agents/marketing.md +11 -0
- package/agents/onboard.md +11 -0
- package/agents/perf.md +11 -0
- package/agents/plan.md +11 -0
- package/agents/preflight.md +11 -0
- package/agents/problem-solver.md +11 -0
- package/agents/rescue.md +11 -0
- package/agents/research.md +11 -0
- package/agents/researcher.md +29 -0
- package/agents/review-intake.md +11 -0
- package/agents/review.md +11 -0
- package/agents/reviewer.md +28 -0
- package/agents/safeguard.md +11 -0
- package/agents/sast.md +11 -0
- package/agents/scanner.md +28 -0
- package/agents/scope-guard.md +11 -0
- package/agents/scout.md +11 -0
- package/agents/sentinel.md +11 -0
- package/agents/sequential-thinking.md +11 -0
- package/agents/session-bridge.md +11 -0
- package/agents/skill-forge.md +11 -0
- package/agents/skill-router.md +11 -0
- package/agents/surgeon.md +11 -0
- package/agents/team.md +11 -0
- package/agents/test.md +11 -0
- package/agents/trend-scout.md +11 -0
- package/agents/verification.md +11 -0
- package/agents/video-creator.md +11 -0
- package/agents/watchdog.md +11 -0
- package/agents/worktree.md +11 -0
- package/commands/.gitkeep +0 -0
- package/commands/rune.md +168 -0
- package/compiler/__tests__/openclaw-adapter.test.js +140 -0
- package/compiler/__tests__/parser.test.js +55 -0
- package/compiler/adapters/antigravity.js +59 -0
- package/compiler/adapters/claude.js +37 -0
- package/compiler/adapters/cursor.js +67 -0
- package/compiler/adapters/generic.js +60 -0
- package/compiler/adapters/index.js +45 -0
- package/compiler/adapters/openclaw.js +150 -0
- package/compiler/adapters/windsurf.js +60 -0
- package/compiler/bin/rune.js +288 -0
- package/compiler/doctor.js +153 -0
- package/compiler/emitter.js +240 -0
- package/compiler/parser.js +208 -0
- package/compiler/transformer.js +69 -0
- package/compiler/transforms/branding.js +27 -0
- package/compiler/transforms/cross-references.js +29 -0
- package/compiler/transforms/frontmatter.js +38 -0
- package/compiler/transforms/hooks.js +68 -0
- package/compiler/transforms/subagents.js +36 -0
- package/compiler/transforms/tool-names.js +60 -0
- package/contexts/dev.md +34 -0
- package/contexts/research.md +43 -0
- package/contexts/review.md +55 -0
- package/extensions/ai-ml/PACK.md +517 -0
- package/extensions/analytics/PACK.md +557 -0
- package/extensions/backend/PACK.md +678 -0
- package/extensions/chrome-ext/PACK.md +995 -0
- package/extensions/content/PACK.md +381 -0
- package/extensions/devops/PACK.md +520 -0
- package/extensions/ecommerce/PACK.md +280 -0
- package/extensions/gamedev/PACK.md +393 -0
- package/extensions/mobile/PACK.md +273 -0
- package/extensions/saas/PACK.md +805 -0
- package/extensions/security/PACK.md +536 -0
- package/extensions/trading/PACK.md +597 -0
- package/extensions/ui/PACK.md +947 -0
- package/package.json +47 -0
- package/skills/.gitkeep +0 -0
- package/skills/adversary/SKILL.md +271 -0
- package/skills/asset-creator/SKILL.md +157 -0
- package/skills/audit/SKILL.md +466 -0
- package/skills/autopsy/SKILL.md +200 -0
- package/skills/ba/SKILL.md +279 -0
- package/skills/brainstorm/SKILL.md +266 -0
- package/skills/browser-pilot/SKILL.md +168 -0
- package/skills/completion-gate/SKILL.md +151 -0
- package/skills/constraint-check/SKILL.md +165 -0
- package/skills/context-engine/SKILL.md +176 -0
- package/skills/cook/SKILL.md +636 -0
- package/skills/db/SKILL.md +256 -0
- package/skills/debug/SKILL.md +240 -0
- package/skills/dependency-doctor/SKILL.md +235 -0
- package/skills/deploy/SKILL.md +174 -0
- package/skills/design/DESIGN-REFERENCE.md +365 -0
- package/skills/design/SKILL.md +462 -0
- package/skills/doc-processor/SKILL.md +254 -0
- package/skills/docs/SKILL.md +336 -0
- package/skills/docs-seeker/SKILL.md +166 -0
- package/skills/fix/SKILL.md +192 -0
- package/skills/git/SKILL.md +285 -0
- package/skills/hallucination-guard/SKILL.md +204 -0
- package/skills/incident/SKILL.md +241 -0
- package/skills/integrity-check/SKILL.md +169 -0
- package/skills/journal/SKILL.md +190 -0
- package/skills/launch/SKILL.md +330 -0
- package/skills/logic-guardian/SKILL.md +240 -0
- package/skills/marketing/SKILL.md +229 -0
- package/skills/mcp-builder/SKILL.md +311 -0
- package/skills/onboard/SKILL.md +298 -0
- package/skills/perf/SKILL.md +297 -0
- package/skills/plan/SKILL.md +520 -0
- package/skills/preflight/SKILL.md +231 -0
- package/skills/problem-solver/SKILL.md +284 -0
- package/skills/rescue/SKILL.md +434 -0
- package/skills/research/SKILL.md +122 -0
- package/skills/review/SKILL.md +354 -0
- package/skills/review-intake/SKILL.md +222 -0
- package/skills/safeguard/SKILL.md +188 -0
- package/skills/sast/SKILL.md +190 -0
- package/skills/scaffold/SKILL.md +276 -0
- package/skills/scope-guard/SKILL.md +150 -0
- package/skills/scout/SKILL.md +232 -0
- package/skills/sentinel/SKILL.md +320 -0
- package/skills/sentinel-env/SKILL.md +226 -0
- package/skills/sequential-thinking/SKILL.md +234 -0
- package/skills/session-bridge/SKILL.md +287 -0
- package/skills/skill-forge/SKILL.md +317 -0
- package/skills/skill-router/SKILL.md +267 -0
- package/skills/surgeon/SKILL.md +203 -0
- package/skills/team/SKILL.md +397 -0
- package/skills/test/SKILL.md +271 -0
- package/skills/trend-scout/SKILL.md +145 -0
- package/skills/verification/SKILL.md +201 -0
- package/skills/video-creator/SKILL.md +201 -0
- package/skills/watchdog/SKILL.md +166 -0
- 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
|
+
}
|