@rune-kit/rune 2.4.0 → 2.6.0

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.
@@ -1,355 +1,355 @@
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 { existsSync } from 'node:fs';
13
- import { readFile, writeFile } from 'node:fs/promises';
14
- import path from 'node:path';
15
- import { createInterface } from 'node:readline';
16
- import { fileURLToPath } from 'node:url';
17
- import { getAdapter, listPlatforms } from '../adapters/index.js';
18
- import { formatDoctorResults, runDoctor } from '../doctor.js';
19
- import { buildAll } from '../emitter.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) {
30
- console.log(msg);
31
- }
32
- function logStep(icon, msg) {
33
- console.log(` ${icon} ${msg}`);
34
- }
35
-
36
- async function readConfig(projectRoot) {
37
- const configPath = path.join(projectRoot, CONFIG_FILE);
38
- if (!existsSync(configPath)) return null;
39
- return JSON.parse(await readFile(configPath, 'utf-8'));
40
- }
41
-
42
- async function writeConfig(projectRoot, config) {
43
- const configPath = path.join(projectRoot, CONFIG_FILE);
44
- await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
45
- }
46
-
47
- function detectPlatform(projectRoot) {
48
- if (existsSync(path.join(projectRoot, '.claude-plugin'))) return 'claude';
49
- if (existsSync(path.join(projectRoot, '.cursor'))) return 'cursor';
50
- if (existsSync(path.join(projectRoot, '.windsurf'))) return 'windsurf';
51
- if (existsSync(path.join(projectRoot, '.agent'))) return 'antigravity';
52
- if (existsSync(path.join(projectRoot, '.openclaw'))) return 'openclaw';
53
- if (existsSync(path.join(projectRoot, '.codex'))) return 'codex';
54
- if (existsSync(path.join(projectRoot, '.opencode'))) return 'opencode';
55
- return null;
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
- /**
69
- * Resolve tier source paths from config.
70
- * Paths can be absolute or relative to projectRoot.
71
- *
72
- * Config format:
73
- * "tiers": { "pro": "../Pro/extensions", "business": "../Business/extensions" }
74
- *
75
- * @param {Object} tiers - tier config object
76
- * @param {string} projectRoot - base for relative paths
77
- * @returns {Object} resolved { pro?: string, business?: string }
78
- */
79
- function resolveTierSources(tiers, projectRoot) {
80
- if (!tiers) return {};
81
- const resolved = {};
82
- for (const tier of ['pro', 'business']) {
83
- if (tiers[tier]) {
84
- resolved[tier] = path.resolve(projectRoot, tiers[tier]);
85
- }
86
- }
87
- return resolved;
88
- }
89
-
90
- // ─── Commands ───
91
-
92
- async function cmdInit(projectRoot, args) {
93
- log('');
94
- log(' ╭──────────────────────────────────────────╮');
95
- log(' │ Rune — Less skills. Deeper connections. │');
96
- log(' ╰──────────────────────────────────────────╯');
97
- log('');
98
-
99
- // Platform detection / selection
100
- let platform = args.platform || detectPlatform(projectRoot);
101
-
102
- if (platform) {
103
- logStep('→', `Detected: ${platform}`);
104
- } else {
105
- log(` Available platforms: ${listPlatforms().join(', ')}`);
106
- const answer = await prompt(' ? Select platform: ');
107
- platform = answer.toLowerCase();
108
- if (!listPlatforms().includes(platform)) {
109
- platform = 'generic';
110
- logStep('→', `Unknown platform, using generic adapter`);
111
- }
112
- }
113
-
114
- if (platform === 'claude') {
115
- logStep('✓', 'Claude Code detected — Rune works as a native plugin. No compilation needed.');
116
- log('');
117
- return;
118
- }
119
-
120
- // Extension pack selection
121
- const extensions = args.extensions ? args.extensions.split(',') : null; // null = all
122
-
123
- // Build config
124
- const config = {
125
- $schema: 'https://rune-kit.github.io/rune/config-schema.json',
126
- version: 1,
127
- platform,
128
- source: '@rune-kit/rune',
129
- skills: {
130
- disabled: args.disable ? args.disable.split(',') : [],
131
- },
132
- extensions: {
133
- enabled: extensions,
134
- },
135
- output: {
136
- index: true,
137
- },
138
- };
139
-
140
- await writeConfig(projectRoot, config);
141
- logStep('✓', 'Created rune.config.json');
142
-
143
- // Auto-build
144
- const adapter = getAdapter(platform);
145
- const tierSources = resolveTierSources(config.tiers, projectRoot);
146
- const stats = await buildAll({
147
- runeRoot: RUNE_ROOT,
148
- outputRoot: projectRoot,
149
- adapter,
150
- disabledSkills: config.skills.disabled,
151
- enabledPacks: config.extensions.enabled,
152
- tierSources,
153
- });
154
-
155
- logStep('✓', `Built ${stats.skillCount} skills + ${stats.packCount} extensions to ${adapter.outputDir}/`);
156
-
157
- if (stats.errors.length > 0) {
158
- for (const err of stats.errors) {
159
- logStep('✗', `Error: ${err.file} — ${err.error}`);
160
- }
161
- }
162
-
163
- log('');
164
- log(' Next steps:');
165
- log(' 1. /rune onboard Generate project context (CLAUDE.md + .rune/)');
166
- log(' 2. /rune cook "..." Build a feature (full TDD cycle)');
167
- log(' 3. /rune help See all 59 skills');
168
- log('');
169
- }
170
-
171
- async function cmdBuild(projectRoot, args) {
172
- const config = await readConfig(projectRoot);
173
-
174
- const platform = args.platform || config?.platform;
175
- if (!platform) {
176
- log(' ✗ No platform configured. Run `rune init` first.');
177
- process.exit(1);
178
- }
179
-
180
- if (platform === 'claude') {
181
- log(' Claude Code uses source SKILL.md files directly. No compilation needed.');
182
- return;
183
- }
184
-
185
- const adapter = getAdapter(platform);
186
- const runeRoot = config?.source === '@rune-kit/rune' ? RUNE_ROOT : config?.source || RUNE_ROOT;
187
- const outputRoot = typeof args.output === 'string' ? args.output : projectRoot;
188
- const disabledSkills = config?.skills?.disabled || [];
189
- const enabledPacks = config?.extensions?.enabled || null;
190
- const tierSources = resolveTierSources(config?.tiers, projectRoot);
191
-
192
- log('');
193
- log(` [parse] Discovering skills...`);
194
-
195
- const stats = await buildAll({
196
- runeRoot,
197
- outputRoot,
198
- adapter,
199
- disabledSkills,
200
- enabledPacks,
201
- tierSources,
202
- });
203
-
204
- log(` [transform] Platform: ${stats.platform}`);
205
- log(` [transform] Resolved ${stats.crossRefsResolved} cross-references`);
206
- log(` [transform] Resolved ${stats.toolRefsResolved} tool-name references`);
207
- log(` [emit] ${stats.skillCount} skills + ${stats.packCount} extensions`);
208
-
209
- if (stats.tierOverrides?.length > 0) {
210
- log(` [tier] ${stats.tierOverrides.length} pack(s) resolved from higher tiers:`);
211
- for (const override of stats.tierOverrides) {
212
- log(` → ${override.pack} (${override.tier})`);
213
- }
214
- }
215
-
216
- if (stats.skipped.length > 0) {
217
- log(` [skip] ${stats.skipped.length} disabled: ${stats.skipped.join(', ')}`);
218
- }
219
-
220
- if (stats.errors.length > 0) {
221
- for (const err of stats.errors) {
222
- log(` [error] ${err.file}: ${err.error}`);
223
- }
224
- }
225
-
226
- log('');
227
- log(` ✓ Built ${stats.files.length} files to ${adapter.outputDir}/`);
228
- log('');
229
- }
230
-
231
- async function cmdDoctor(projectRoot, args) {
232
- const config = await readConfig(projectRoot);
233
-
234
- if (!config) {
235
- // No config = CI or fresh clone. Run source-only checks (split packs).
236
- log('');
237
- log(' ℹ No rune.config.json found — running source-only checks.');
238
- const results = await runDoctor({
239
- outputRoot: projectRoot,
240
- adapter: getAdapter('claude'),
241
- config: {},
242
- runeRoot: RUNE_ROOT,
243
- });
244
- log(formatDoctorResults(results));
245
- if (!results.healthy) process.exit(1);
246
- return;
247
- }
248
-
249
- const platform = args.platform || config.platform;
250
- const adapter = getAdapter(platform);
251
- const runeRoot = config.source === '@rune-kit/rune' ? RUNE_ROOT : config.source || RUNE_ROOT;
252
-
253
- const results = await runDoctor({
254
- outputRoot: projectRoot,
255
- adapter,
256
- config,
257
- runeRoot,
258
- });
259
-
260
- log(formatDoctorResults(results));
261
-
262
- if (!results.healthy) process.exit(1);
263
- }
264
-
265
- // ─── Arg Parsing ───
266
-
267
- // Flags that require a string value (not boolean)
268
- const VALUE_REQUIRED_FLAGS = new Set(['platform', 'output', 'disable', 'extensions']);
269
-
270
- function parseArgs(argv) {
271
- const args = {};
272
- const positional = [];
273
-
274
- for (let i = 0; i < argv.length; i++) {
275
- const arg = argv[i];
276
- if (arg.startsWith('--')) {
277
- const key = arg.slice(2);
278
- const next = argv[i + 1];
279
- if (next && !next.startsWith('--')) {
280
- args[key] = next;
281
- i++;
282
- } else if (VALUE_REQUIRED_FLAGS.has(key)) {
283
- log(` ✗ Flag --${key} requires a value. Example: --${key} <value>`);
284
- process.exit(1);
285
- } else {
286
- args[key] = true;
287
- }
288
- } else {
289
- positional.push(arg);
290
- }
291
- }
292
-
293
- return { command: positional[0], args };
294
- }
295
-
296
- // ─── Main ───
297
-
298
- async function main() {
299
- const { command, args } = parseArgs(process.argv.slice(2));
300
- const projectRoot = process.cwd();
301
-
302
- // Handle --version / -v as flag (not positional command)
303
- if (args.version || args.v) {
304
- const pkg = JSON.parse(await readFile(path.join(RUNE_ROOT, 'package.json'), 'utf-8'));
305
- log(` rune v${pkg.version}`);
306
- return;
307
- }
308
-
309
- switch (command) {
310
- case 'init':
311
- await cmdInit(projectRoot, args);
312
- break;
313
- case 'build':
314
- await cmdBuild(projectRoot, args);
315
- break;
316
- case 'doctor':
317
- await cmdDoctor(projectRoot, args);
318
- break;
319
- case 'version':
320
- case '--version':
321
- case '-v': {
322
- const pkg = JSON.parse(await readFile(path.join(RUNE_ROOT, 'package.json'), 'utf-8'));
323
- log(` rune v${pkg.version}`);
324
- break;
325
- }
326
- case 'help':
327
- case '--help':
328
- case undefined:
329
- log('');
330
- log(' Rune CLI — Skill mesh for AI coding assistants');
331
- log('');
332
- log(' Commands:');
333
- log(' init Interactive setup (auto-detects platform)');
334
- log(' build Compile skills for configured platform');
335
- log(' doctor Validate compiled output');
336
- log('');
337
- log(' Options:');
338
- log(
339
- ' --platform <name> Override platform (cursor, windsurf, antigravity, codex, openclaw, opencode, generic)',
340
- );
341
- log(' --output <dir> Override output directory');
342
- log(' --disable <skills> Comma-separated skills to disable');
343
- log(' --version, -v Show version');
344
- log('');
345
- break;
346
- default:
347
- log(` ✗ Unknown command: ${command}. Run \`rune help\` for usage.`);
348
- process.exit(1);
349
- }
350
- }
351
-
352
- main().catch((err) => {
353
- console.error(' ✗ Fatal:', err.message);
354
- process.exit(1);
355
- });
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 { existsSync } from 'node:fs';
13
+ import { readFile, writeFile } from 'node:fs/promises';
14
+ import path from 'node:path';
15
+ import { createInterface } from 'node:readline';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { getAdapter, listPlatforms } from '../adapters/index.js';
18
+ import { formatDoctorResults, runDoctor } from '../doctor.js';
19
+ import { buildAll } from '../emitter.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) {
30
+ console.log(msg);
31
+ }
32
+ function logStep(icon, msg) {
33
+ console.log(` ${icon} ${msg}`);
34
+ }
35
+
36
+ async function readConfig(projectRoot) {
37
+ const configPath = path.join(projectRoot, CONFIG_FILE);
38
+ if (!existsSync(configPath)) return null;
39
+ return JSON.parse(await readFile(configPath, 'utf-8'));
40
+ }
41
+
42
+ async function writeConfig(projectRoot, config) {
43
+ const configPath = path.join(projectRoot, CONFIG_FILE);
44
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
45
+ }
46
+
47
+ function detectPlatform(projectRoot) {
48
+ if (existsSync(path.join(projectRoot, '.claude-plugin'))) return 'claude';
49
+ if (existsSync(path.join(projectRoot, '.cursor'))) return 'cursor';
50
+ if (existsSync(path.join(projectRoot, '.windsurf'))) return 'windsurf';
51
+ if (existsSync(path.join(projectRoot, '.agents'))) return 'antigravity';
52
+ if (existsSync(path.join(projectRoot, '.openclaw'))) return 'openclaw';
53
+ if (existsSync(path.join(projectRoot, '.codex'))) return 'codex';
54
+ if (existsSync(path.join(projectRoot, '.opencode'))) return 'opencode';
55
+ return null;
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
+ /**
69
+ * Resolve tier source paths from config.
70
+ * Paths can be absolute or relative to projectRoot.
71
+ *
72
+ * Config format:
73
+ * "tiers": { "pro": "../Pro/extensions", "business": "../Business/extensions" }
74
+ *
75
+ * @param {Object} tiers - tier config object
76
+ * @param {string} projectRoot - base for relative paths
77
+ * @returns {Object} resolved { pro?: string, business?: string }
78
+ */
79
+ function resolveTierSources(tiers, projectRoot) {
80
+ if (!tiers) return {};
81
+ const resolved = {};
82
+ for (const tier of ['pro', 'business']) {
83
+ if (tiers[tier]) {
84
+ resolved[tier] = path.resolve(projectRoot, tiers[tier]);
85
+ }
86
+ }
87
+ return resolved;
88
+ }
89
+
90
+ // ─── Commands ───
91
+
92
+ async function cmdInit(projectRoot, args) {
93
+ log('');
94
+ log(' ╭──────────────────────────────────────────╮');
95
+ log(' │ Rune — Less skills. Deeper connections. │');
96
+ log(' ╰──────────────────────────────────────────╯');
97
+ log('');
98
+
99
+ // Platform detection / selection
100
+ let platform = args.platform || detectPlatform(projectRoot);
101
+
102
+ if (platform) {
103
+ logStep('→', `Detected: ${platform}`);
104
+ } else {
105
+ log(` Available platforms: ${listPlatforms().join(', ')}`);
106
+ const answer = await prompt(' ? Select platform: ');
107
+ platform = answer.toLowerCase();
108
+ if (!listPlatforms().includes(platform)) {
109
+ platform = 'generic';
110
+ logStep('→', `Unknown platform, using generic adapter`);
111
+ }
112
+ }
113
+
114
+ if (platform === 'claude') {
115
+ logStep('✓', 'Claude Code detected — Rune works as a native plugin. No compilation needed.');
116
+ log('');
117
+ return;
118
+ }
119
+
120
+ // Extension pack selection
121
+ const extensions = args.extensions ? args.extensions.split(',') : null; // null = all
122
+
123
+ // Build config
124
+ const config = {
125
+ $schema: 'https://rune-kit.github.io/rune/config-schema.json',
126
+ version: 1,
127
+ platform,
128
+ source: '@rune-kit/rune',
129
+ skills: {
130
+ disabled: args.disable ? args.disable.split(',') : [],
131
+ },
132
+ extensions: {
133
+ enabled: extensions,
134
+ },
135
+ output: {
136
+ index: true,
137
+ },
138
+ };
139
+
140
+ await writeConfig(projectRoot, config);
141
+ logStep('✓', 'Created rune.config.json');
142
+
143
+ // Auto-build
144
+ const adapter = getAdapter(platform);
145
+ const tierSources = resolveTierSources(config.tiers, projectRoot);
146
+ const stats = await buildAll({
147
+ runeRoot: RUNE_ROOT,
148
+ outputRoot: projectRoot,
149
+ adapter,
150
+ disabledSkills: config.skills.disabled,
151
+ enabledPacks: config.extensions.enabled,
152
+ tierSources,
153
+ });
154
+
155
+ logStep('✓', `Built ${stats.skillCount} skills + ${stats.packCount} extensions to ${adapter.outputDir}/`);
156
+
157
+ if (stats.errors.length > 0) {
158
+ for (const err of stats.errors) {
159
+ logStep('✗', `Error: ${err.file} — ${err.error}`);
160
+ }
161
+ }
162
+
163
+ log('');
164
+ log(' Next steps:');
165
+ log(' 1. /rune onboard Generate project context (CLAUDE.md + .rune/)');
166
+ log(' 2. /rune cook "..." Build a feature (full TDD cycle)');
167
+ log(' 3. /rune help See all 59 skills');
168
+ log('');
169
+ }
170
+
171
+ async function cmdBuild(projectRoot, args) {
172
+ const config = await readConfig(projectRoot);
173
+
174
+ const platform = args.platform || config?.platform;
175
+ if (!platform) {
176
+ log(' ✗ No platform configured. Run `rune init` first.');
177
+ process.exit(1);
178
+ }
179
+
180
+ if (platform === 'claude') {
181
+ log(' Claude Code uses source SKILL.md files directly. No compilation needed.');
182
+ return;
183
+ }
184
+
185
+ const adapter = getAdapter(platform);
186
+ const runeRoot = config?.source === '@rune-kit/rune' ? RUNE_ROOT : config?.source || RUNE_ROOT;
187
+ const outputRoot = typeof args.output === 'string' ? args.output : projectRoot;
188
+ const disabledSkills = config?.skills?.disabled || [];
189
+ const enabledPacks = config?.extensions?.enabled || null;
190
+ const tierSources = resolveTierSources(config?.tiers, projectRoot);
191
+
192
+ log('');
193
+ log(` [parse] Discovering skills...`);
194
+
195
+ const stats = await buildAll({
196
+ runeRoot,
197
+ outputRoot,
198
+ adapter,
199
+ disabledSkills,
200
+ enabledPacks,
201
+ tierSources,
202
+ });
203
+
204
+ log(` [transform] Platform: ${stats.platform}`);
205
+ log(` [transform] Resolved ${stats.crossRefsResolved} cross-references`);
206
+ log(` [transform] Resolved ${stats.toolRefsResolved} tool-name references`);
207
+ log(` [emit] ${stats.skillCount} skills + ${stats.packCount} extensions`);
208
+
209
+ if (stats.tierOverrides?.length > 0) {
210
+ log(` [tier] ${stats.tierOverrides.length} pack(s) resolved from higher tiers:`);
211
+ for (const override of stats.tierOverrides) {
212
+ log(` → ${override.pack} (${override.tier})`);
213
+ }
214
+ }
215
+
216
+ if (stats.skipped.length > 0) {
217
+ log(` [skip] ${stats.skipped.length} disabled: ${stats.skipped.join(', ')}`);
218
+ }
219
+
220
+ if (stats.errors.length > 0) {
221
+ for (const err of stats.errors) {
222
+ log(` [error] ${err.file}: ${err.error}`);
223
+ }
224
+ }
225
+
226
+ log('');
227
+ log(` ✓ Built ${stats.files.length} files to ${adapter.outputDir}/`);
228
+ log('');
229
+ }
230
+
231
+ async function cmdDoctor(projectRoot, args) {
232
+ const config = await readConfig(projectRoot);
233
+
234
+ if (!config) {
235
+ // No config = CI or fresh clone. Run source-only checks (split packs).
236
+ log('');
237
+ log(' ℹ No rune.config.json found — running source-only checks.');
238
+ const results = await runDoctor({
239
+ outputRoot: projectRoot,
240
+ adapter: getAdapter('claude'),
241
+ config: {},
242
+ runeRoot: RUNE_ROOT,
243
+ });
244
+ log(formatDoctorResults(results));
245
+ if (!results.healthy) process.exit(1);
246
+ return;
247
+ }
248
+
249
+ const platform = args.platform || config.platform;
250
+ const adapter = getAdapter(platform);
251
+ const runeRoot = config.source === '@rune-kit/rune' ? RUNE_ROOT : config.source || RUNE_ROOT;
252
+
253
+ const results = await runDoctor({
254
+ outputRoot: projectRoot,
255
+ adapter,
256
+ config,
257
+ runeRoot,
258
+ });
259
+
260
+ log(formatDoctorResults(results));
261
+
262
+ if (!results.healthy) process.exit(1);
263
+ }
264
+
265
+ // ─── Arg Parsing ───
266
+
267
+ // Flags that require a string value (not boolean)
268
+ const VALUE_REQUIRED_FLAGS = new Set(['platform', 'output', 'disable', 'extensions']);
269
+
270
+ function parseArgs(argv) {
271
+ const args = {};
272
+ const positional = [];
273
+
274
+ for (let i = 0; i < argv.length; i++) {
275
+ const arg = argv[i];
276
+ if (arg.startsWith('--')) {
277
+ const key = arg.slice(2);
278
+ const next = argv[i + 1];
279
+ if (next && !next.startsWith('--')) {
280
+ args[key] = next;
281
+ i++;
282
+ } else if (VALUE_REQUIRED_FLAGS.has(key)) {
283
+ log(` ✗ Flag --${key} requires a value. Example: --${key} <value>`);
284
+ process.exit(1);
285
+ } else {
286
+ args[key] = true;
287
+ }
288
+ } else {
289
+ positional.push(arg);
290
+ }
291
+ }
292
+
293
+ return { command: positional[0], args };
294
+ }
295
+
296
+ // ─── Main ───
297
+
298
+ async function main() {
299
+ const { command, args } = parseArgs(process.argv.slice(2));
300
+ const projectRoot = process.cwd();
301
+
302
+ // Handle --version / -v as flag (not positional command)
303
+ if (args.version || args.v) {
304
+ const pkg = JSON.parse(await readFile(path.join(RUNE_ROOT, 'package.json'), 'utf-8'));
305
+ log(` rune v${pkg.version}`);
306
+ return;
307
+ }
308
+
309
+ switch (command) {
310
+ case 'init':
311
+ await cmdInit(projectRoot, args);
312
+ break;
313
+ case 'build':
314
+ await cmdBuild(projectRoot, args);
315
+ break;
316
+ case 'doctor':
317
+ await cmdDoctor(projectRoot, args);
318
+ break;
319
+ case 'version':
320
+ case '--version':
321
+ case '-v': {
322
+ const pkg = JSON.parse(await readFile(path.join(RUNE_ROOT, 'package.json'), 'utf-8'));
323
+ log(` rune v${pkg.version}`);
324
+ break;
325
+ }
326
+ case 'help':
327
+ case '--help':
328
+ case undefined:
329
+ log('');
330
+ log(' Rune CLI — Skill mesh for AI coding assistants');
331
+ log('');
332
+ log(' Commands:');
333
+ log(' init Interactive setup (auto-detects platform)');
334
+ log(' build Compile skills for configured platform');
335
+ log(' doctor Validate compiled output');
336
+ log('');
337
+ log(' Options:');
338
+ log(
339
+ ' --platform <name> Override platform (cursor, windsurf, antigravity, codex, openclaw, opencode, generic)',
340
+ );
341
+ log(' --output <dir> Override output directory');
342
+ log(' --disable <skills> Comma-separated skills to disable');
343
+ log(' --version, -v Show version');
344
+ log('');
345
+ break;
346
+ default:
347
+ log(` ✗ Unknown command: ${command}. Run \`rune help\` for usage.`);
348
+ process.exit(1);
349
+ }
350
+ }
351
+
352
+ main().catch((err) => {
353
+ console.error(' ✗ Fatal:', err.message);
354
+ process.exit(1);
355
+ });