slashdojo 0.1.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.
Files changed (3) hide show
  1. package/package.json +39 -0
  2. package/src/cli.js +1068 -0
  3. package/src/index.js +355 -0
package/src/cli.js ADDED
@@ -0,0 +1,1068 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dojo — CLI for the Dojo skill registry.
4
+ *
5
+ * Implements SPEC.md §20 CLI Reference: discovery, knowledge, installation,
6
+ * execution, authoring, publishing, configuration, registry, and utilities.
7
+ */
8
+
9
+ import { Dojo } from './index.js';
10
+ import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync, readdirSync, statSync } from 'fs';
11
+ import { join, resolve, basename, dirname, relative } from 'path';
12
+ import { homedir } from 'os';
13
+ import { execSync, spawn } from 'child_process';
14
+ import { createHash } from 'crypto';
15
+
16
+ // ─── ANSI helpers ────────────────────────────────────────
17
+ const C = process.env.NO_COLOR
18
+ ? { r: '', g: '', y: '', b: '', m: '', c: '', dim: '', bold: '', reset: '' }
19
+ : {
20
+ r: '\x1b[31m', g: '\x1b[32m', y: '\x1b[33m', b: '\x1b[34m',
21
+ m: '\x1b[35m', c: '\x1b[36m', dim: '\x1b[2m', bold: '\x1b[1m',
22
+ reset: '\x1b[0m'
23
+ };
24
+
25
+ // ─── Paths ──────────────────────────────────────────────
26
+ const DOJO_HOME = process.env.DOJO_HOME || join(homedir(), '.dojo');
27
+ const SKILLS_DIR = join(DOJO_HOME, 'skills');
28
+ const LOCK_FILE = join(DOJO_HOME, 'skill-lock.json');
29
+ const CONFIG_FILE = join(DOJO_HOME, 'config.json');
30
+ const SECRETS_FILE = join(DOJO_HOME, 'secrets.json');
31
+
32
+ function ensureHome() {
33
+ mkdirSync(SKILLS_DIR, { recursive: true });
34
+ }
35
+
36
+ // ─── Config helpers ─────────────────────────────────────
37
+ function loadConfig() {
38
+ if (!existsSync(CONFIG_FILE)) return { registries: [] };
39
+ return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
40
+ }
41
+
42
+ function saveConfig(cfg) {
43
+ ensureHome();
44
+ writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + '\n');
45
+ }
46
+
47
+ function loadLock() {
48
+ if (!existsSync(LOCK_FILE)) return { resolved: {}, resolved_at: null, registry: null };
49
+ return JSON.parse(readFileSync(LOCK_FILE, 'utf-8'));
50
+ }
51
+
52
+ function saveLock(lock) {
53
+ ensureHome();
54
+ writeFileSync(LOCK_FILE, JSON.stringify(lock, null, 2) + '\n');
55
+ }
56
+
57
+ function loadSecrets() {
58
+ if (!existsSync(SECRETS_FILE)) return {};
59
+ return JSON.parse(readFileSync(SECRETS_FILE, 'utf-8'));
60
+ }
61
+
62
+ function saveSecrets(s) {
63
+ ensureHome();
64
+ writeFileSync(SECRETS_FILE, JSON.stringify(s, null, 2) + '\n');
65
+ }
66
+
67
+ // ─── Client factory ─────────────────────────────────────
68
+ function makeClient() {
69
+ const cfg = loadConfig();
70
+ const registries = cfg.registries?.map(r => r.url) || [];
71
+ return new Dojo({
72
+ registries,
73
+ registry: cfg.default_registry || registries[0],
74
+ token: cfg.token || process.env.DOJO_TOKEN
75
+ });
76
+ }
77
+
78
+ // ─── Arg parsing ────────────────────────────────────────
79
+ function parseArgs(argv) {
80
+ const args = [];
81
+ const flags = {};
82
+ let i = 0;
83
+ while (i < argv.length) {
84
+ const a = argv[i];
85
+ if (a.startsWith('--')) {
86
+ const key = a.slice(2);
87
+ const eq = key.indexOf('=');
88
+ if (eq >= 0) {
89
+ flags[key.slice(0, eq)] = key.slice(eq + 1);
90
+ } else if (i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
91
+ flags[key] = argv[++i];
92
+ } else {
93
+ flags[key] = true;
94
+ }
95
+ } else if (a.startsWith('-') && a.length === 2) {
96
+ const key = a.slice(1);
97
+ if (i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
98
+ flags[key] = argv[++i];
99
+ } else {
100
+ flags[key] = true;
101
+ }
102
+ } else {
103
+ args.push(a);
104
+ }
105
+ i++;
106
+ }
107
+ return { args, flags };
108
+ }
109
+
110
+ // ─── Output helpers ─────────────────────────────────────
111
+ function json(data) {
112
+ console.log(JSON.stringify(data, null, 2));
113
+ }
114
+
115
+ function die(msg) {
116
+ console.error(`${C.r}error${C.reset}: ${msg}`);
117
+ process.exit(1);
118
+ }
119
+
120
+ function heading(text) {
121
+ console.log(`\n${C.bold}${text}${C.reset}`);
122
+ }
123
+
124
+ function row(label, value) {
125
+ console.log(` ${C.dim}${label}${C.reset} ${value}`);
126
+ }
127
+
128
+ function skillLine(s, score) {
129
+ const tag = s.type ? `${C.dim}[${s.type}]${C.reset} ` : '';
130
+ const sc = score != null ? ` ${C.dim}(${(score * 100).toFixed(0)}%)${C.reset}` : '';
131
+ console.log(` ${C.c}${s.uri}${C.reset} ${tag}${s.context || ''}${sc}`);
132
+ }
133
+
134
+ // ─── Commands ───────────────────────────────────────────
135
+
136
+ // ── Discovery ───────────────────────────────────────────
137
+
138
+ async function cmdSearch(args, flags) {
139
+ if (!args.length) die('usage: dojo search <query>');
140
+ const client = makeClient();
141
+ const results = await client.search(args.join(' '), {
142
+ eco: flags.eco,
143
+ type: flags.type,
144
+ tags: flags.tags,
145
+ limit: flags.limit,
146
+ offset: flags.offset,
147
+ mode: flags.mode
148
+ });
149
+ if (flags.json) return json(results);
150
+ if (!results.results?.length) return console.log('No results.');
151
+ heading(`${results.total} result${results.total === 1 ? '' : 's'}`);
152
+ for (const r of results.results) {
153
+ skillLine(r, r.score);
154
+ }
155
+ }
156
+
157
+ async function cmdResolve(args, flags) {
158
+ if (!args.length) die('usage: dojo resolve <need>');
159
+ const client = makeClient();
160
+ const data = await client.need(args.join(' '), {
161
+ tags: flags.tags, type: flags.type, limit: flags.limit, mode: flags.mode
162
+ });
163
+ if (flags.json) return json(data);
164
+ if (!data) return console.log('No matching skill found.');
165
+ heading('Best match');
166
+ skillLine(data);
167
+ if (data.scripts?.length) {
168
+ row('scripts', data.scripts.map(s => s.id).join(', '));
169
+ }
170
+ }
171
+
172
+ async function cmdTree(args, flags) {
173
+ if (!args.length) die('usage: dojo tree <ecosystem>');
174
+ const client = makeClient();
175
+ const tree = await client.tree(args[0], Number(flags.depth) || 4);
176
+ if (flags.json) return json(tree);
177
+ printTree(tree, 0, Number(flags.depth) || 4);
178
+ }
179
+
180
+ function printTree(node, depth, maxDepth) {
181
+ if (depth > maxDepth) return;
182
+ const indent = ' '.repeat(depth);
183
+ const tag = node.type ? `${C.dim}[${node.type}]${C.reset} ` : '';
184
+ console.log(`${indent}${C.c}${node.uri}${C.reset} ${tag}${node.context || ''}`);
185
+ for (const child of node.skills || node.children || []) {
186
+ printTree(child, depth + 1, maxDepth);
187
+ }
188
+ }
189
+
190
+ async function cmdInfo(args, flags) {
191
+ if (!args.length) die('usage: dojo info <uri>');
192
+ const client = makeClient();
193
+ const data = await client.get(args[0]);
194
+ if (flags.json) return json(data);
195
+ const s = data.skill;
196
+ heading(s.uri);
197
+ row('type', s.type || 'skill');
198
+ row('version', s.version || 'n/a');
199
+ if (s.context) row('context', s.context);
200
+ if (s.tags?.length) row('tags', s.tags.join(', '));
201
+ if (s.scripts?.length) {
202
+ row('scripts', s.scripts.map(sc => `${sc.id} (${sc.lang})`).join(', '));
203
+ }
204
+ if (s.depends?.length) {
205
+ row('depends', s.depends.map(d => `${d.uri}${d.optional ? ' (opt)' : ''}`).join(', '));
206
+ }
207
+ if (data.ancestors?.length) {
208
+ row('ancestors', data.ancestors.map(a => a.uri).join(' → '));
209
+ }
210
+ if (data.children?.length) {
211
+ row('children', data.children.map(c => c.uri).join(', '));
212
+ }
213
+ }
214
+
215
+ // ── Knowledge ───────────────────────────────────────────
216
+
217
+ async function cmdLearn(args, flags) {
218
+ if (!args.length) die('usage: dojo learn <uri> [--section <id>] [--question <q>]');
219
+ let uri = args[0];
220
+ let section = flags.section;
221
+ // Support uri#section shorthand
222
+ const hash = uri.indexOf('#');
223
+ if (hash >= 0) {
224
+ section = section || uri.slice(hash + 1);
225
+ uri = uri.slice(0, hash);
226
+ }
227
+ const client = makeClient();
228
+ const params = new URLSearchParams();
229
+ if (section) params.set('section', section);
230
+ if (flags.question) params.set('question', flags.question);
231
+ const qs = params.toString();
232
+ const data = await client._fetch(`/v1/learn/${uri}${qs ? '?' + qs : ''}`);
233
+ if (flags.json) return json(data);
234
+
235
+ const node = data.node;
236
+ heading(node.uri);
237
+ if (node.context) console.log(`${C.dim}${node.context}${C.reset}\n`);
238
+ if (data.focused_section) {
239
+ console.log(`${C.bold}§ ${data.focused_section.title}${C.reset}`);
240
+ console.log(data.focused_section.body || '');
241
+ } else if (node.body) {
242
+ console.log(node.body);
243
+ }
244
+ if (node.sections?.length && !data.focused_section) {
245
+ heading('Sections');
246
+ for (const sec of node.sections) {
247
+ console.log(` ${C.y}#${sec.id}${C.reset} ${sec.title}`);
248
+ }
249
+ }
250
+ if (data.reading_path?.length) {
251
+ heading('Reading path');
252
+ for (const step of data.reading_path) {
253
+ console.log(` → ${C.c}${step.uri || step.route}${C.reset} ${step.why || step.title || ''}`);
254
+ }
255
+ }
256
+ }
257
+
258
+ async function cmdBacklinks(args, flags) {
259
+ if (!args.length) die('usage: dojo backlinks <uri>');
260
+ const client = makeClient();
261
+ const data = await client._fetch(`/v1/backlinks/${args[0]}`);
262
+ if (flags.json) return json(data);
263
+ if (!data.backlinks?.length) return console.log('No backlinks.');
264
+ heading(`Backlinks for ${args[0]}`);
265
+ for (const bl of data.backlinks) {
266
+ console.log(` ${C.c}${bl.from}${C.reset} ${C.dim}(${bl.type})${C.reset} ${bl.context || ''}`);
267
+ }
268
+ }
269
+
270
+ async function cmdGraph(args, flags) {
271
+ if (!args.length) die('usage: dojo graph <uri>');
272
+ const client = makeClient();
273
+ const depth = Number(flags.depth) || 2;
274
+ const data = await client._fetch(`/v1/graph/${args[0]}?depth=${depth}`);
275
+ if (flags.json) return json(data);
276
+ heading(`Graph around ${data.center}`);
277
+ if (data.nodes?.length) {
278
+ console.log(` ${data.nodes.length} node${data.nodes.length === 1 ? '' : 's'}`);
279
+ for (const n of data.nodes) {
280
+ console.log(` ${C.c}${n.uri}${C.reset} ${C.dim}[${n.type}]${C.reset}`);
281
+ }
282
+ }
283
+ if (data.edges?.length) {
284
+ heading('Edges');
285
+ for (const e of data.edges) {
286
+ console.log(` ${e.from} ${C.dim}─${e.type}→${C.reset} ${e.to}`);
287
+ }
288
+ }
289
+ }
290
+
291
+ async function cmdAlias(args, flags) {
292
+ if (!args.length) die('usage: dojo alias <name>');
293
+ const client = makeClient();
294
+ const data = await client._fetch(`/v1/alias/${encodeURIComponent(args.join(' '))}`);
295
+ if (flags.json) return json(data);
296
+ console.log(`${C.c}${data.uri}${C.reset} ${data.context || ''}`);
297
+ }
298
+
299
+ // ── Installation ────────────────────────────────────────
300
+
301
+ async function cmdInstall(args, flags) {
302
+ if (!args.length) die('usage: dojo install <uri>[@version] [--chain <variant>] [--dry-run]');
303
+ ensureHome();
304
+ const client = makeClient();
305
+
306
+ let uri = args[0];
307
+ let version;
308
+ const atIdx = uri.lastIndexOf('@');
309
+ if (atIdx > 0) {
310
+ version = uri.slice(atIdx + 1);
311
+ uri = uri.slice(0, atIdx);
312
+ }
313
+
314
+ console.log(`${C.dim}Fetching ${uri}${version ? '@' + version : ''}...${C.reset}`);
315
+
316
+ // Fetch bundle
317
+ const params = new URLSearchParams();
318
+ if (version) params.set('version', version);
319
+ if (flags.chain) params.set('chain', flags.chain);
320
+ const qs = params.toString();
321
+ const bundle = await client._fetch(`/v1/bundle/${uri}${qs ? '?' + qs : ''}`);
322
+
323
+ if (flags['dry-run']) {
324
+ heading('Dry run — would install:');
325
+ row('uri', bundle.uri);
326
+ row('files', `${bundle.files?.length || 0} file(s)`);
327
+ if (bundle.files) {
328
+ for (const f of bundle.files) {
329
+ console.log(` ${C.dim}${f.kind}${C.reset} ${f.path} (${f.size} bytes)`);
330
+ }
331
+ }
332
+ return;
333
+ }
334
+
335
+ // Write files to ~/.dojo/skills/<uri>/
336
+ const skillDir = join(SKILLS_DIR, ...uri.split('/'));
337
+ mkdirSync(skillDir, { recursive: true });
338
+
339
+ let fileCount = 0;
340
+ if (bundle.files) {
341
+ for (const f of bundle.files) {
342
+ if (f.content != null) {
343
+ const target = join(skillDir, f.path);
344
+ mkdirSync(dirname(target), { recursive: true });
345
+ writeFileSync(target, f.content);
346
+ fileCount++;
347
+ }
348
+ }
349
+ }
350
+
351
+ // Also write manifest
352
+ if (bundle.manifest) {
353
+ writeFileSync(join(skillDir, 'skill.json'), JSON.stringify(bundle.manifest, null, 2) + '\n');
354
+ }
355
+
356
+ // Update lockfile
357
+ const lock = loadLock();
358
+ lock.resolved[uri] = version || bundle.manifest?.version || 'latest';
359
+ lock.resolved_at = new Date().toISOString();
360
+ lock.registry = client.registry;
361
+ saveLock(lock);
362
+
363
+ // Install npm packages if any scripts need them
364
+ const scripts = bundle.manifest?.scripts || [];
365
+ const allPkgs = scripts.flatMap(s => s.packages || []);
366
+ if (allPkgs.length) {
367
+ console.log(`${C.dim}Installing packages: ${allPkgs.join(', ')}...${C.reset}`);
368
+ try {
369
+ execSync(`npm install --prefix "${skillDir}" ${allPkgs.join(' ')}`, { stdio: 'inherit' });
370
+ } catch {
371
+ console.warn(`${C.y}warn${C.reset}: package install failed — scripts may not run`);
372
+ }
373
+ }
374
+
375
+ console.log(`${C.g}✓${C.reset} Installed ${C.c}${uri}${C.reset} (${fileCount} file${fileCount === 1 ? '' : 's'})`);
376
+ }
377
+
378
+ async function cmdUninstall(args, _flags) {
379
+ if (!args.length) die('usage: dojo uninstall <uri>');
380
+ const uri = args[0];
381
+ const skillDir = join(SKILLS_DIR, ...uri.split('/'));
382
+ if (!existsSync(skillDir)) die(`${uri} is not installed`);
383
+
384
+ rmSync(skillDir, { recursive: true, force: true });
385
+
386
+ // Clean empty parent dirs
387
+ let parent = dirname(skillDir);
388
+ while (parent !== SKILLS_DIR) {
389
+ try {
390
+ const entries = readdirSync(parent);
391
+ if (entries.length === 0) rmSync(parent, { recursive: true });
392
+ else break;
393
+ } catch { break; }
394
+ parent = dirname(parent);
395
+ }
396
+
397
+ // Update lockfile
398
+ const lock = loadLock();
399
+ delete lock.resolved[uri];
400
+ lock.resolved_at = new Date().toISOString();
401
+ saveLock(lock);
402
+
403
+ console.log(`${C.g}✓${C.reset} Uninstalled ${C.c}${uri}${C.reset}`);
404
+ }
405
+
406
+ async function cmdUpdate(args, flags) {
407
+ const lock = loadLock();
408
+ const uris = flags.all ? Object.keys(lock.resolved) : args;
409
+ if (!uris.length) die('usage: dojo update <uri> or dojo update --all');
410
+ for (const uri of uris) {
411
+ console.log(`${C.dim}Updating ${uri}...${C.reset}`);
412
+ await cmdInstall([uri], {});
413
+ }
414
+ }
415
+
416
+ function cmdList(_args, flags) {
417
+ const lock = loadLock();
418
+ const entries = Object.entries(lock.resolved);
419
+ if (flags.json) return json(lock);
420
+ if (!entries.length) return console.log('No skills installed.');
421
+ heading(`${entries.length} installed skill${entries.length === 1 ? '' : 's'}`);
422
+ for (const [uri, ver] of entries) {
423
+ console.log(` ${C.c}${uri}${C.reset} ${C.dim}${ver}${C.reset}`);
424
+ }
425
+ }
426
+
427
+ async function cmdOutdated(_args, flags) {
428
+ const lock = loadLock();
429
+ const entries = Object.entries(lock.resolved);
430
+ if (!entries.length) return console.log('No skills installed.');
431
+ const client = makeClient();
432
+ const outdated = [];
433
+ for (const [uri, ver] of entries) {
434
+ try {
435
+ const data = await client.get(uri);
436
+ const latest = data.skill?.version;
437
+ if (latest && latest !== ver) {
438
+ outdated.push({ uri, current: ver, latest });
439
+ }
440
+ } catch { /* skip */ }
441
+ }
442
+ if (flags.json) return json(outdated);
443
+ if (!outdated.length) return console.log('All skills up to date.');
444
+ heading('Outdated skills');
445
+ for (const o of outdated) {
446
+ console.log(` ${C.c}${o.uri}${C.reset} ${C.r}${o.current}${C.reset} → ${C.g}${o.latest}${C.reset}`);
447
+ }
448
+ }
449
+
450
+ // ── Execution ───────────────────────────────────────────
451
+
452
+ async function cmdRun(args, flags) {
453
+ if (!args.length) die('usage: dojo run <uri> [script-id] [--input JSON] [--env K=V]');
454
+ const uri = args[0];
455
+ const scriptId = args[1];
456
+ const client = makeClient();
457
+
458
+ let input = {};
459
+ if (flags.input) {
460
+ try { input = JSON.parse(flags.input); }
461
+ catch { die('--input must be valid JSON'); }
462
+ }
463
+
464
+ // Merge --env flags
465
+ if (flags.env) {
466
+ const pairs = Array.isArray(flags.env) ? flags.env : [flags.env];
467
+ for (const pair of pairs) {
468
+ const eq = pair.indexOf('=');
469
+ if (eq > 0) process.env[pair.slice(0, eq)] = pair.slice(eq + 1);
470
+ }
471
+ }
472
+
473
+ if (flags['dry-run']) {
474
+ const data = await client.get(uri);
475
+ const s = data.skill;
476
+ heading('Dry run');
477
+ row('uri', s.uri);
478
+ if (s.scripts?.length) {
479
+ for (const sc of s.scripts) {
480
+ row(`script:${sc.id}`, `${sc.lang} — ${sc.entry || 'inline'}`);
481
+ }
482
+ }
483
+ return;
484
+ }
485
+
486
+ const result = await client.run(uri, input, scriptId);
487
+ if (typeof result === 'string') {
488
+ process.stdout.write(result);
489
+ } else {
490
+ json(result);
491
+ }
492
+ }
493
+
494
+ // ── Authoring ───────────────────────────────────────────
495
+
496
+ function cmdInit(args, flags) {
497
+ const name = args[0] || 'my-skill';
498
+ const type = flags.type || 'skill';
499
+ const parent = flags.parent || '';
500
+ const dir = resolve(name);
501
+
502
+ if (existsSync(dir) && readdirSync(dir).length) {
503
+ die(`directory ${name} already exists and is not empty`);
504
+ }
505
+
506
+ mkdirSync(dir, { recursive: true });
507
+ mkdirSync(join(dir, 'scripts'), { recursive: true });
508
+
509
+ const manifest = {
510
+ name,
511
+ version: '0.1.0',
512
+ uri: parent ? `${parent}/${name}` : name,
513
+ type,
514
+ context: `TODO: describe what ${name} does`,
515
+ ...(parent ? { parent } : {}),
516
+ tags: [],
517
+ scripts: [],
518
+ schema: {
519
+ input: { type: 'object', properties: {} },
520
+ output: { type: 'object', properties: {} }
521
+ },
522
+ author: '',
523
+ license: 'MIT',
524
+ created: new Date().toISOString(),
525
+ updated: new Date().toISOString(),
526
+ status: 'draft'
527
+ };
528
+
529
+ writeFileSync(join(dir, 'node.json'), JSON.stringify(manifest, null, 2) + '\n');
530
+ writeFileSync(join(dir, 'SKILL.md'), `---
531
+ name: ${name}
532
+ description: TODO
533
+ license: MIT
534
+ ---
535
+
536
+ # ${name}
537
+
538
+ TODO: describe this skill.
539
+
540
+ ## Fast path
541
+
542
+ - TODO
543
+
544
+ ## Workflow
545
+
546
+ 1. TODO
547
+ `);
548
+
549
+ console.log(`${C.g}✓${C.reset} Scaffolded ${C.c}${name}${C.reset} (${type})`);
550
+ console.log(` ${C.dim}${dir}${C.reset}`);
551
+ }
552
+
553
+ function cmdValidate(args, _flags) {
554
+ const dir = resolve(args[0] || '.');
555
+ const errors = [];
556
+ const warnings = [];
557
+
558
+ // Check manifest
559
+ const manifestPath = existsSync(join(dir, 'node.json'))
560
+ ? join(dir, 'node.json')
561
+ : existsSync(join(dir, 'skill.json'))
562
+ ? join(dir, 'skill.json')
563
+ : null;
564
+
565
+ if (!manifestPath) {
566
+ die('No node.json or skill.json found');
567
+ }
568
+
569
+ let manifest;
570
+ try {
571
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
572
+ } catch (e) {
573
+ die(`Invalid JSON in ${basename(manifestPath)}: ${e.message}`);
574
+ }
575
+
576
+ if (!manifest.name) errors.push('missing "name"');
577
+ if (!manifest.uri) errors.push('missing "uri"');
578
+ if (!manifest.type) warnings.push('missing "type" — defaults to "skill"');
579
+ if (!manifest.context) warnings.push('missing "context" — no description');
580
+ if (!manifest.version) warnings.push('missing "version"');
581
+
582
+ // Validate scripts reference existing files
583
+ for (const s of manifest.scripts || []) {
584
+ if (s.entry && !existsSync(join(dir, s.entry))) {
585
+ errors.push(`script "${s.id}" entry not found: ${s.entry}`);
586
+ }
587
+ }
588
+
589
+ // Check SKILL.md
590
+ if (!existsSync(join(dir, 'SKILL.md'))) {
591
+ warnings.push('no SKILL.md found');
592
+ }
593
+
594
+ if (errors.length) {
595
+ heading('Errors');
596
+ for (const e of errors) console.log(` ${C.r}✗${C.reset} ${e}`);
597
+ }
598
+ if (warnings.length) {
599
+ heading('Warnings');
600
+ for (const w of warnings) console.log(` ${C.y}!${C.reset} ${w}`);
601
+ }
602
+ if (!errors.length && !warnings.length) {
603
+ console.log(`${C.g}✓${C.reset} ${manifestPath} is valid`);
604
+ }
605
+ if (errors.length) process.exit(1);
606
+ }
607
+
608
+ function cmdTest(args, flags) {
609
+ const dir = resolve(args[0] || '.');
610
+ const testDir = join(dir, 'tests');
611
+ const testFiles = [];
612
+
613
+ if (existsSync(testDir)) {
614
+ for (const f of readdirSync(testDir)) {
615
+ if (f.endsWith('.test.js') || f.endsWith('.test.mjs')) testFiles.push(join(testDir, f));
616
+ }
617
+ }
618
+ // Also check root-level test files
619
+ if (existsSync(dir)) {
620
+ for (const f of readdirSync(dir)) {
621
+ if ((f.endsWith('.test.js') || f.endsWith('.test.mjs')) && !f.startsWith('.')) {
622
+ testFiles.push(join(dir, f));
623
+ }
624
+ }
625
+ }
626
+
627
+ if (!testFiles.length) die('No test files found');
628
+
629
+ const caseFilter = flags.case;
630
+ const nodeArgs = ['--test'];
631
+ if (caseFilter) nodeArgs.push('--test-name-pattern', caseFilter);
632
+ nodeArgs.push(...testFiles);
633
+
634
+ console.log(`${C.dim}Running ${testFiles.length} test file(s)...${C.reset}`);
635
+ const child = spawn('node', nodeArgs, { stdio: 'inherit', cwd: dir });
636
+ child.on('close', code => process.exit(code || 0));
637
+ }
638
+
639
+ function cmdPack(args, _flags) {
640
+ const dir = resolve(args[0] || '.');
641
+ const manifestPath = existsSync(join(dir, 'node.json'))
642
+ ? join(dir, 'node.json')
643
+ : join(dir, 'skill.json');
644
+
645
+ if (!existsSync(manifestPath)) die('No manifest found');
646
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
647
+ const name = (manifest.uri || manifest.name || 'skill').replace(/\//g, '-');
648
+ const ver = manifest.version || '0.0.0';
649
+ const tarball = `${name}-${ver}.tar.gz`;
650
+
651
+ try {
652
+ execSync(`tar czf "${tarball}" --exclude=node_modules --exclude=.git -C "${dirname(dir)}" "${basename(dir)}"`, {
653
+ stdio: 'inherit'
654
+ });
655
+ const stats = statSync(tarball);
656
+ const hash = createHash('sha256').update(readFileSync(tarball)).digest('hex');
657
+ console.log(`${C.g}✓${C.reset} Packed ${C.c}${tarball}${C.reset} (${stats.size} bytes, sha256:${hash.slice(0, 12)}...)`);
658
+ } catch (e) {
659
+ die(`pack failed: ${e.message}`);
660
+ }
661
+ }
662
+
663
+ // ── Publishing ──────────────────────────────────────────
664
+
665
+ async function cmdPublish(args, flags) {
666
+ const dir = resolve(args[0] || '.');
667
+ const manifestPath = existsSync(join(dir, 'node.json'))
668
+ ? join(dir, 'node.json')
669
+ : join(dir, 'skill.json');
670
+
671
+ if (!existsSync(manifestPath)) die('No manifest found');
672
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
673
+
674
+ const client = makeClient();
675
+ if (flags.registry) {
676
+ client.registry = flags.registry;
677
+ client.registryCandidates = [flags.registry];
678
+ }
679
+
680
+ console.log(`${C.dim}Publishing ${manifest.uri}@${manifest.version || '?'}...${C.reset}`);
681
+ const result = await client.publish(manifest);
682
+ console.log(`${C.g}✓${C.reset} Published ${C.c}${result.uri}${C.reset}@${result.version}`);
683
+ }
684
+
685
+ async function cmdYank(args, _flags) {
686
+ if (!args.length) die('usage: dojo yank <uri>@<version>');
687
+ const input = args[0];
688
+ const atIdx = input.lastIndexOf('@');
689
+ if (atIdx <= 0) die('version required: dojo yank <uri>@<version>');
690
+ const uri = input.slice(0, atIdx);
691
+ const version = input.slice(atIdx + 1);
692
+ const client = makeClient();
693
+ await client._fetch(`/v1/skills/${uri}`, {
694
+ method: 'DELETE',
695
+ body: { version },
696
+ auth: true
697
+ });
698
+ console.log(`${C.g}✓${C.reset} Yanked ${C.c}${uri}${C.reset}@${version}`);
699
+ }
700
+
701
+ async function cmdDeprecate(args, flags) {
702
+ if (!args.length) die('usage: dojo deprecate <uri> --message "..."');
703
+ const client = makeClient();
704
+ await client._fetch(`/v1/skills/${args[0]}`, {
705
+ method: 'PATCH',
706
+ body: { status: 'deprecated', deprecation_message: flags.message || 'Deprecated' },
707
+ auth: true
708
+ });
709
+ console.log(`${C.g}✓${C.reset} Deprecated ${C.c}${args[0]}${C.reset}`);
710
+ }
711
+
712
+ // ── Configuration ───────────────────────────────────────
713
+
714
+ function cmdConfig(args, flags) {
715
+ const sub = args[0];
716
+ if (sub === 'list') {
717
+ const cfg = loadConfig();
718
+ if (flags.json) return json(cfg);
719
+ heading('Configuration');
720
+ for (const [k, v] of Object.entries(cfg)) {
721
+ if (k === 'registries') {
722
+ row('registries', '');
723
+ for (const r of v) console.log(` ${r.url} ${C.dim}(priority: ${r.priority || '-'})${C.reset}`);
724
+ } else if (k === 'token') {
725
+ row(k, '***');
726
+ } else {
727
+ row(k, typeof v === 'object' ? JSON.stringify(v) : v);
728
+ }
729
+ }
730
+ return;
731
+ }
732
+ if (sub === 'set') {
733
+ const key = args[1];
734
+ const value = args[2];
735
+ if (!key || !value) die('usage: dojo config set <key> <value>');
736
+ const cfg = loadConfig();
737
+ if (key === 'registry') {
738
+ cfg.default_registry = value;
739
+ } else if (key === 'token') {
740
+ cfg.token = value;
741
+ } else {
742
+ cfg[key] = value;
743
+ }
744
+ saveConfig(cfg);
745
+ console.log(`${C.g}✓${C.reset} Set ${key}`);
746
+ return;
747
+ }
748
+ die('usage: dojo config <set|list>');
749
+ }
750
+
751
+ function cmdSecrets(args, _flags) {
752
+ const sub = args[0];
753
+ if (sub === 'list') {
754
+ const secrets = loadSecrets();
755
+ const keys = Object.keys(secrets);
756
+ if (!keys.length) return console.log('No secrets stored.');
757
+ heading(`${keys.length} secret${keys.length === 1 ? '' : 's'}`);
758
+ for (const k of keys) {
759
+ console.log(` ${C.c}${k}${C.reset}`);
760
+ }
761
+ return;
762
+ }
763
+ if (sub === 'set') {
764
+ const key = args[1];
765
+ if (!key) die('usage: dojo secrets set <key>');
766
+ // Read value from stdin or prompt
767
+ const value = args[2] || process.env[key];
768
+ if (!value) die('Provide value as third argument or set it in the environment');
769
+ const secrets = loadSecrets();
770
+ secrets[key] = value;
771
+ saveSecrets(secrets);
772
+ console.log(`${C.g}✓${C.reset} Stored secret ${C.c}${key}${C.reset}`);
773
+ return;
774
+ }
775
+ die('usage: dojo secrets <set|list>');
776
+ }
777
+
778
+ // ── Registry management ─────────────────────────────────
779
+
780
+ function cmdRegistry(args, _flags) {
781
+ const sub = args[0];
782
+ const cfg = loadConfig();
783
+ if (!cfg.registries) cfg.registries = [];
784
+
785
+ if (sub === 'list') {
786
+ if (!cfg.registries.length) return console.log('No registries configured (using defaults).');
787
+ heading('Registries');
788
+ for (const r of cfg.registries) {
789
+ console.log(` ${C.c}${r.url}${C.reset} priority=${r.priority || '-'}`);
790
+ }
791
+ return;
792
+ }
793
+ if (sub === 'add') {
794
+ const url = args[1];
795
+ if (!url) die('usage: dojo registry add <url>');
796
+ if (cfg.registries.some(r => r.url === url)) die('Registry already configured');
797
+ cfg.registries.push({ url, priority: cfg.registries.length + 1 });
798
+ saveConfig(cfg);
799
+ console.log(`${C.g}✓${C.reset} Added registry ${C.c}${url}${C.reset}`);
800
+ return;
801
+ }
802
+ if (sub === 'remove') {
803
+ const url = args[1];
804
+ if (!url) die('usage: dojo registry remove <url>');
805
+ cfg.registries = cfg.registries.filter(r => r.url !== url);
806
+ saveConfig(cfg);
807
+ console.log(`${C.g}✓${C.reset} Removed registry ${C.c}${url}${C.reset}`);
808
+ return;
809
+ }
810
+ die('usage: dojo registry <list|add|remove>');
811
+ }
812
+
813
+ async function cmdMirror(args, flags) {
814
+ if (args[0] !== 'sync' || !flags.from) die('usage: dojo mirror sync --from <url>');
815
+ console.log(`${C.dim}Syncing from ${flags.from}...${C.reset}`);
816
+ const remote = new Dojo({ registry: flags.from });
817
+ const local = makeClient();
818
+ const root = await remote._fetch('/v1');
819
+ for (const eco of root.ecosystems || []) {
820
+ try {
821
+ const tree = await remote.tree(eco.uri || eco, 10);
822
+ console.log(` ${C.dim}${eco.uri || eco}${C.reset}`);
823
+ // Could publish each node, but for now just report
824
+ } catch { /* skip */ }
825
+ }
826
+ console.log(`${C.g}✓${C.reset} Mirror sync complete (listing only — publish not yet automated)`);
827
+ }
828
+
829
+ // ── Utilities ───────────────────────────────────────────
830
+
831
+ async function cmdLink(args, _flags) {
832
+ if (args.length < 2) die('usage: dojo link <from-uri> <to-uri>');
833
+ console.log(`${C.y}!${C.reset} Link creation requires editing the source manifest.`);
834
+ console.log(` Add to ${C.c}${args[0]}${C.reset} node.json links array:`);
835
+ console.log(` { "uri": "${args[1]}", "context": "..." }`);
836
+ }
837
+
838
+ async function cmdDiff(args, flags) {
839
+ if (args.length < 2) die('usage: dojo diff <uri>@v1 <uri>@v2');
840
+ const client = makeClient();
841
+ const parse = (s) => {
842
+ const at = s.lastIndexOf('@');
843
+ return at > 0 ? { uri: s.slice(0, at), version: s.slice(at + 1) } : { uri: s, version: null };
844
+ };
845
+ const a = parse(args[0]);
846
+ const b = parse(args[1]);
847
+ const [da, db] = await Promise.all([
848
+ client.get(a.uri, a.version),
849
+ client.get(b.uri, b.version)
850
+ ]);
851
+ if (flags.json) return json({ a: da.skill, b: db.skill });
852
+ heading(`Diff: ${args[0]} vs ${args[1]}`);
853
+ const keys = new Set([...Object.keys(da.skill || {}), ...Object.keys(db.skill || {})]);
854
+ for (const k of [...keys].sort()) {
855
+ const va = JSON.stringify(da.skill?.[k]);
856
+ const vb = JSON.stringify(db.skill?.[k]);
857
+ if (va !== vb) {
858
+ console.log(` ${C.y}${k}${C.reset}`);
859
+ if (va) console.log(` ${C.r}- ${va.slice(0, 120)}${C.reset}`);
860
+ if (vb) console.log(` ${C.g}+ ${vb.slice(0, 120)}${C.reset}`);
861
+ }
862
+ }
863
+ }
864
+
865
+ function cmdAudit(_args, flags) {
866
+ const lock = loadLock();
867
+ const entries = Object.entries(lock.resolved);
868
+ if (!entries.length) return console.log('No skills installed.');
869
+ heading('Security audit');
870
+ let issues = 0;
871
+ for (const [uri, ver] of entries) {
872
+ const skillDir = join(SKILLS_DIR, ...uri.split('/'));
873
+ const manifestPath = join(skillDir, 'skill.json');
874
+ if (!existsSync(manifestPath)) {
875
+ console.log(` ${C.y}!${C.reset} ${uri} — no local manifest (orphaned lock entry)`);
876
+ issues++;
877
+ continue;
878
+ }
879
+ try {
880
+ const m = JSON.parse(readFileSync(manifestPath, 'utf-8'));
881
+ const scripts = m.scripts || [];
882
+ for (const s of scripts) {
883
+ if (s.inline) {
884
+ console.log(` ${C.y}!${C.reset} ${uri}:${s.id} — contains inline code`);
885
+ issues++;
886
+ }
887
+ const envKeys = Object.entries(s.env || {}).filter(([, v]) => v.secret);
888
+ if (envKeys.length) {
889
+ console.log(` ${C.dim}i${C.reset} ${uri}:${s.id} — requires secrets: ${envKeys.map(([k]) => k).join(', ')}`);
890
+ }
891
+ }
892
+ } catch {
893
+ console.log(` ${C.r}✗${C.reset} ${uri} — corrupt manifest`);
894
+ issues++;
895
+ }
896
+ }
897
+ if (!issues) console.log(` ${C.g}✓${C.reset} No issues found`);
898
+ }
899
+
900
+ function cmdCompletions(args, _flags) {
901
+ const shell = args[0] || 'bash';
902
+ const commands = [
903
+ 'search', 'resolve', 'tree', 'info',
904
+ 'learn', 'backlinks', 'graph', 'alias',
905
+ 'install', 'uninstall', 'update', 'list', 'outdated',
906
+ 'run',
907
+ 'init', 'validate', 'test', 'pack',
908
+ 'publish', 'yank', 'deprecate',
909
+ 'config', 'secrets',
910
+ 'registry', 'mirror',
911
+ 'link', 'diff', 'audit', 'completions',
912
+ 'help', 'version'
913
+ ];
914
+
915
+ if (shell === 'bash') {
916
+ console.log(`# dojo bash completions — add to ~/.bashrc:
917
+ _dojo() {
918
+ local cur=\${COMP_WORDS[COMP_CWORD]}
919
+ COMPREPLY=( $(compgen -W "${commands.join(' ')}" -- "$cur") )
920
+ }
921
+ complete -F _dojo dojo`);
922
+ } else if (shell === 'zsh') {
923
+ console.log(`# dojo zsh completions — add to ~/.zshrc:
924
+ _dojo() {
925
+ _arguments '1:command:(${commands.join(' ')})'
926
+ }
927
+ compdef _dojo dojo`);
928
+ } else if (shell === 'fish') {
929
+ for (const cmd of commands) {
930
+ console.log(`complete -c dojo -n '__fish_use_subcommand' -a '${cmd}'`);
931
+ }
932
+ } else {
933
+ die(`Unknown shell: ${shell}. Supported: bash, zsh, fish`);
934
+ }
935
+ }
936
+
937
+ // ── Help / Version ──────────────────────────────────────
938
+
939
+ function cmdHelp() {
940
+ console.log(`
941
+ ${C.bold}dojo${C.reset} — CLI for the Dojo skill registry
942
+
943
+ ${C.bold}Discovery${C.reset}
944
+ search <query> Full-text search
945
+ resolve <need> Natural language resolution
946
+ tree <ecosystem> View ecosystem tree
947
+ info <uri> Detailed skill info
948
+
949
+ ${C.bold}Knowledge${C.reset}
950
+ learn <uri> Read a node's knowledge
951
+ backlinks <uri> Incoming references
952
+ graph <uri> Local knowledge graph
953
+ alias <name> Resolve an alias
954
+
955
+ ${C.bold}Installation${C.reset}
956
+ install <uri>[@version] Install a skill
957
+ uninstall <uri> Remove an installed skill
958
+ update <uri>|--all Update skills
959
+ list List installed skills
960
+ outdated Show available updates
961
+
962
+ ${C.bold}Execution${C.reset}
963
+ run <uri> [script-id] Run a skill's script
964
+
965
+ ${C.bold}Authoring${C.reset}
966
+ init [name] Scaffold a new skill
967
+ validate [path] Validate a manifest
968
+ test [path] Run skill tests
969
+ pack [path] Create distributable tarball
970
+
971
+ ${C.bold}Publishing${C.reset}
972
+ publish [path] Publish to registry
973
+ yank <uri>@<version> Soft-delete a version
974
+ deprecate <uri> Mark deprecated
975
+
976
+ ${C.bold}Configuration${C.reset}
977
+ config set <key> <value> Set config value
978
+ config list Show all config
979
+ secrets set <key> Store a secret
980
+ secrets list List secret keys
981
+
982
+ ${C.bold}Registry${C.reset}
983
+ registry list|add|remove Manage registries
984
+ mirror sync --from <url> Sync a mirror
985
+
986
+ ${C.bold}Utilities${C.reset}
987
+ link <from> <to> Create a cross-skill reference
988
+ diff <uri>@v1 <uri>@v2 Compare versions
989
+ audit Security audit installed skills
990
+ completions <shell> Generate shell completions
991
+
992
+ ${C.bold}Flags${C.reset}
993
+ --json Machine-readable JSON output
994
+ --dry-run Preview without side effects
995
+ --help, -h Show this help
996
+ --version, -v Show version
997
+ `);
998
+ }
999
+
1000
+ // ─── Router ─────────────────────────────────────────────
1001
+
1002
+ const COMMANDS = {
1003
+ // Discovery
1004
+ search: cmdSearch,
1005
+ resolve: cmdResolve,
1006
+ tree: cmdTree,
1007
+ info: cmdInfo,
1008
+ // Knowledge
1009
+ learn: cmdLearn,
1010
+ backlinks: cmdBacklinks,
1011
+ graph: cmdGraph,
1012
+ alias: cmdAlias,
1013
+ // Installation
1014
+ install: cmdInstall,
1015
+ uninstall: cmdUninstall,
1016
+ update: cmdUpdate,
1017
+ list: cmdList,
1018
+ outdated: cmdOutdated,
1019
+ // Execution
1020
+ run: cmdRun,
1021
+ // Authoring
1022
+ init: cmdInit,
1023
+ validate: cmdValidate,
1024
+ test: cmdTest,
1025
+ pack: cmdPack,
1026
+ // Publishing
1027
+ publish: cmdPublish,
1028
+ yank: cmdYank,
1029
+ deprecate: cmdDeprecate,
1030
+ // Configuration
1031
+ config: cmdConfig,
1032
+ secrets: cmdSecrets,
1033
+ // Registry
1034
+ registry: cmdRegistry,
1035
+ mirror: cmdMirror,
1036
+ // Utilities
1037
+ link: cmdLink,
1038
+ diff: cmdDiff,
1039
+ audit: cmdAudit,
1040
+ completions: cmdCompletions,
1041
+ // Meta
1042
+ help: cmdHelp,
1043
+ version: () => {
1044
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
1045
+ console.log(`dojo ${pkg.version}`);
1046
+ }
1047
+ };
1048
+
1049
+ async function main() {
1050
+ const { args, flags } = parseArgs(process.argv.slice(2));
1051
+
1052
+ if (flags.version || flags.v) return COMMANDS.version();
1053
+ if (flags.help || flags.h || !args.length) return cmdHelp();
1054
+
1055
+ const command = args[0];
1056
+ const handler = COMMANDS[command];
1057
+ if (!handler) {
1058
+ die(`Unknown command: ${command}\nRun "dojo help" for usage.`);
1059
+ }
1060
+
1061
+ try {
1062
+ await handler(args.slice(1), flags);
1063
+ } catch (e) {
1064
+ die(e.message);
1065
+ }
1066
+ }
1067
+
1068
+ main();