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.
- package/package.json +39 -0
- package/src/cli.js +1068 -0
- 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();
|