install-agent-skill 1.0.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/README.bigtech.md +191 -0
- package/README.md +164 -0
- package/bin/add-skill.js +5 -0
- package/bin/add-skill.modular.js +5 -0
- package/bin/add-skill.v1.backup.js +1904 -0
- package/bin/cli.js +100 -0
- package/bin/lib/commands/analyze.js +70 -0
- package/bin/lib/commands/cache.js +65 -0
- package/bin/lib/commands/doctor.js +75 -0
- package/bin/lib/commands/help.js +42 -0
- package/bin/lib/commands/info.js +38 -0
- package/bin/lib/commands/init.js +39 -0
- package/bin/lib/commands/install.js +297 -0
- package/bin/lib/commands/list.js +43 -0
- package/bin/lib/commands/lock.js +57 -0
- package/bin/lib/commands/uninstall.js +48 -0
- package/bin/lib/commands/update.js +55 -0
- package/bin/lib/commands/validate.js +69 -0
- package/bin/lib/commands/verify.js +56 -0
- package/bin/lib/config.js +80 -0
- package/bin/lib/helpers.js +155 -0
- package/bin/lib/installer.js +49 -0
- package/bin/lib/skills.js +114 -0
- package/bin/lib/types.js +82 -0
- package/bin/lib/ui.js +132 -0
- package/package.json +56 -0
- package/specs/ADD_SKILL_SPEC.md +333 -0
- package/specs/REGISTRY_V2_SPEC.md +334 -0
|
@@ -0,0 +1,1904 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @dataguruin/add-skill
|
|
4
|
+
* Enterprise-grade Agent Skill Manager
|
|
5
|
+
* VERSION 4.0.0 - Antigravity Skills Edition
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - list: Show all installed skills with metadata
|
|
9
|
+
* - uninstall: Remove a skill
|
|
10
|
+
* - update: Update a single skill
|
|
11
|
+
* - cache: Manage cache (clear, info)
|
|
12
|
+
* - init: Initialize .agent/skills directory
|
|
13
|
+
* - validate: Validate skill structure against Antigravity spec
|
|
14
|
+
* - analyze: Analyze SKILL.md frontmatter and structure
|
|
15
|
+
* - Colored output with fallback
|
|
16
|
+
* - Progress spinners
|
|
17
|
+
* - JSON output mode (--json)
|
|
18
|
+
* - Verbose mode (--verbose)
|
|
19
|
+
* - Offline mode (--offline)
|
|
20
|
+
* - Backup before upgrade
|
|
21
|
+
* - Better error handling
|
|
22
|
+
* - Enhanced SKILL.md parsing (description, tags, author)
|
|
23
|
+
* - Progressive Disclosure structure detection
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { execSync, spawn } from "child_process";
|
|
27
|
+
import fs from "fs";
|
|
28
|
+
import path from "path";
|
|
29
|
+
import os from "os";
|
|
30
|
+
import crypto from "crypto";
|
|
31
|
+
import { createRequire } from "module";
|
|
32
|
+
import { promisify } from "util";
|
|
33
|
+
import readline from "readline";
|
|
34
|
+
|
|
35
|
+
/* ===================== BOOTSTRAP ===================== */
|
|
36
|
+
|
|
37
|
+
const require = createRequire(import.meta.url);
|
|
38
|
+
const pkg = (() => {
|
|
39
|
+
try {
|
|
40
|
+
return require("../package.json");
|
|
41
|
+
} catch {
|
|
42
|
+
return { version: "4.0.0" };
|
|
43
|
+
}
|
|
44
|
+
})();
|
|
45
|
+
|
|
46
|
+
const cwd = process.cwd();
|
|
47
|
+
|
|
48
|
+
/* ===================== CONSTANTS ===================== */
|
|
49
|
+
|
|
50
|
+
const WORKSPACE = path.join(cwd, ".agent", "skills");
|
|
51
|
+
const GLOBAL_DIR = path.join(os.homedir(), ".gemini", "antigravity", "skills");
|
|
52
|
+
const CACHE_ROOT =
|
|
53
|
+
process.env.ADD_SKILL_CACHE_DIR ||
|
|
54
|
+
path.join(os.homedir(), ".cache", "add-skill");
|
|
55
|
+
const REGISTRY_CACHE = path.join(CACHE_ROOT, "registries");
|
|
56
|
+
const REGISTRIES_FILE = path.join(CACHE_ROOT, "registries.json");
|
|
57
|
+
const BACKUP_DIR = path.join(CACHE_ROOT, "backups");
|
|
58
|
+
const CONFIG_FILE = path.join(CACHE_ROOT, "config.json");
|
|
59
|
+
|
|
60
|
+
/* ===================== ARG PARSE ===================== */
|
|
61
|
+
|
|
62
|
+
const args = process.argv.slice(2);
|
|
63
|
+
const command = args[0];
|
|
64
|
+
const flags = new Set(args.filter((a) => a.startsWith("--")));
|
|
65
|
+
const params = args.filter((a) => !a.startsWith("--")).slice(1);
|
|
66
|
+
|
|
67
|
+
const DRY = flags.has("--dry-run");
|
|
68
|
+
const STRICT = flags.has("--strict");
|
|
69
|
+
const FIX = flags.has("--fix");
|
|
70
|
+
const LOCKED = flags.has("--locked");
|
|
71
|
+
const VERBOSE = flags.has("--verbose") || flags.has("-v");
|
|
72
|
+
const JSON_OUTPUT = flags.has("--json");
|
|
73
|
+
const OFFLINE = flags.has("--offline");
|
|
74
|
+
const FORCE = flags.has("--force") || flags.has("-f");
|
|
75
|
+
const GLOBAL = flags.has("--global") || flags.has("-g");
|
|
76
|
+
|
|
77
|
+
/* ===================== COLORS ===================== */
|
|
78
|
+
|
|
79
|
+
const supportsColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
80
|
+
|
|
81
|
+
const colors = {
|
|
82
|
+
reset: supportsColor ? "\x1b[0m" : "",
|
|
83
|
+
bold: supportsColor ? "\x1b[1m" : "",
|
|
84
|
+
dim: supportsColor ? "\x1b[2m" : "",
|
|
85
|
+
red: supportsColor ? "\x1b[31m" : "",
|
|
86
|
+
green: supportsColor ? "\x1b[32m" : "",
|
|
87
|
+
yellow: supportsColor ? "\x1b[33m" : "",
|
|
88
|
+
blue: supportsColor ? "\x1b[34m" : "",
|
|
89
|
+
magenta: supportsColor ? "\x1b[35m" : "",
|
|
90
|
+
cyan: supportsColor ? "\x1b[36m" : "",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const icons = {
|
|
94
|
+
success: supportsColor ? "✅" : "[OK]",
|
|
95
|
+
error: supportsColor ? "❌" : "[ERROR]",
|
|
96
|
+
warning: supportsColor ? "⚠️" : "[WARN]",
|
|
97
|
+
info: supportsColor ? "ℹ️" : "[INFO]",
|
|
98
|
+
check: supportsColor ? "✔" : "[+]",
|
|
99
|
+
cross: supportsColor ? "✖" : "[-]",
|
|
100
|
+
arrow: supportsColor ? "→" : "->",
|
|
101
|
+
spinner: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
|
102
|
+
package: supportsColor ? "📦" : "[PKG]",
|
|
103
|
+
lock: supportsColor ? "🔒" : "[LOCK]",
|
|
104
|
+
rocket: supportsColor ? "🚀" : "[GO]",
|
|
105
|
+
trash: supportsColor ? "🗑️" : "[DEL]",
|
|
106
|
+
refresh: supportsColor ? "♻️" : "[UPD]",
|
|
107
|
+
folder: supportsColor ? "📁" : "[DIR]",
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/* ===================== UI ENGINE ===================== */
|
|
111
|
+
|
|
112
|
+
const theme = {
|
|
113
|
+
border: supportsColor ? "\x1b[90m" : "", // Gray
|
|
114
|
+
accent: supportsColor ? "\x1b[36m" : "", // Cyan
|
|
115
|
+
text: supportsColor ? "\x1b[37m" : "", // White
|
|
116
|
+
dim: supportsColor ? "\x1b[2m" : "", // Dim
|
|
117
|
+
reset: supportsColor ? "\x1b[0m" : "",
|
|
118
|
+
success: supportsColor ? "\x1b[32m" : "", // Green
|
|
119
|
+
error: supportsColor ? "\x1b[31m" : "", // Red
|
|
120
|
+
warning: supportsColor ? "\x1b[33m" : "", // Yellow
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const symbols = {
|
|
124
|
+
top: "┌",
|
|
125
|
+
middle: "│",
|
|
126
|
+
bottom: "└",
|
|
127
|
+
item: "◇",
|
|
128
|
+
success: "✔",
|
|
129
|
+
error: "✖",
|
|
130
|
+
arrow: "→",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
class TreeUI {
|
|
134
|
+
constructor(title) {
|
|
135
|
+
this.title = title;
|
|
136
|
+
this.stepCount = 0;
|
|
137
|
+
this.isDone = false;
|
|
138
|
+
|
|
139
|
+
// Print Header
|
|
140
|
+
if (!JSON_OUTPUT) {
|
|
141
|
+
console.log(); // spacer
|
|
142
|
+
console.log(`${theme.border}${symbols.top} ${theme.accent}\x1b[1m${title}${theme.reset}`);
|
|
143
|
+
console.log(`${theme.border}${symbols.middle}${theme.reset}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
step(text, icon = symbols.item) {
|
|
148
|
+
if (JSON_OUTPUT) return;
|
|
149
|
+
console.log(`${theme.border}${symbols.middle} ${theme.accent}${icon} ${theme.text}${text}${theme.reset}`);
|
|
150
|
+
console.log(`${theme.border}${symbols.middle}${theme.reset}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
log(text) {
|
|
154
|
+
if (JSON_OUTPUT) return;
|
|
155
|
+
console.log(`${theme.border}${symbols.middle} ${theme.dim}${text}${theme.reset}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
success(text) {
|
|
159
|
+
if (JSON_OUTPUT) return;
|
|
160
|
+
this.close(text, theme.success, symbols.success);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
error(text) {
|
|
164
|
+
if (JSON_OUTPUT) {
|
|
165
|
+
console.log(JSON.stringify({ success: false, error: text }));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.close(text, theme.error, symbols.error);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
close(text, color, icon) {
|
|
172
|
+
if (this.isDone) return;
|
|
173
|
+
this.isDone = true;
|
|
174
|
+
console.log(`${theme.border}${symbols.bottom} ${color}${icon} ${text}${theme.reset}\n`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Global UI instance
|
|
179
|
+
let ui = new TreeUI("add-skill");
|
|
180
|
+
|
|
181
|
+
/* ===================== HELPERS ===================== */
|
|
182
|
+
|
|
183
|
+
function fatal(msg, error = null) {
|
|
184
|
+
ui.error(msg);
|
|
185
|
+
if (error && VERBOSE) {
|
|
186
|
+
console.error(theme.dim + error.stack + theme.reset);
|
|
187
|
+
}
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function success(msg) { ui.success(msg); }
|
|
192
|
+
function info(msg) { ui.step(msg); }
|
|
193
|
+
function warn(msg) { ui.step(msg, "⚠️"); }
|
|
194
|
+
function verbose(msg) { if (VERBOSE) ui.log(msg); }
|
|
195
|
+
function outputJSON(data) { if (JSON_OUTPUT) console.log(JSON.stringify(data, null, 2)); }
|
|
196
|
+
|
|
197
|
+
function spin(text) {
|
|
198
|
+
// Simplified spinner for TreeUI (renders as step)
|
|
199
|
+
ui.step(text + "...");
|
|
200
|
+
return {
|
|
201
|
+
succeed: (t) => { /* already logged start, simplistic approach */ },
|
|
202
|
+
fail: (t) => ui.log(`${theme.error}Failed: ${t}${theme.reset}`)
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* ===================== PROMPT ===================== */
|
|
207
|
+
|
|
208
|
+
async function confirm(question) {
|
|
209
|
+
if (FORCE) return true;
|
|
210
|
+
if (!process.stdin.isTTY) return false;
|
|
211
|
+
|
|
212
|
+
const rl = readline.createInterface({
|
|
213
|
+
input: process.stdin,
|
|
214
|
+
output: process.stdout,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return new Promise((resolve) => {
|
|
218
|
+
rl.question(`${question} (y/N) `, (answer) => {
|
|
219
|
+
rl.close();
|
|
220
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* ===================== CORE HELPERS ===================== */
|
|
226
|
+
|
|
227
|
+
function merkleHash(dir) {
|
|
228
|
+
const files = [];
|
|
229
|
+
function walk(p) {
|
|
230
|
+
for (const f of fs.readdirSync(p)) {
|
|
231
|
+
if (f === ".skill-source.json") continue; // exclude metadata
|
|
232
|
+
const full = path.join(p, f);
|
|
233
|
+
const stat = fs.statSync(full);
|
|
234
|
+
if (stat.isDirectory()) walk(full);
|
|
235
|
+
else {
|
|
236
|
+
const h = crypto
|
|
237
|
+
.createHash("sha256")
|
|
238
|
+
.update(fs.readFileSync(full))
|
|
239
|
+
.digest("hex");
|
|
240
|
+
files.push(`${path.relative(dir, full)}:${h}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
walk(dir);
|
|
245
|
+
files.sort();
|
|
246
|
+
return crypto.createHash("sha256").update(files.join("|")).digest("hex");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function resolveScope() {
|
|
250
|
+
if (GLOBAL) return GLOBAL_DIR;
|
|
251
|
+
if (fs.existsSync(path.join(cwd, ".agent"))) return WORKSPACE;
|
|
252
|
+
return GLOBAL_DIR;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function loadSkillLock() {
|
|
256
|
+
const f = path.join(cwd, ".agent", "skill-lock.json");
|
|
257
|
+
if (!fs.existsSync(f)) fatal("skill-lock.json not found");
|
|
258
|
+
return JSON.parse(fs.readFileSync(f, "utf-8"));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function getInstalledSkills() {
|
|
262
|
+
const scope = resolveScope();
|
|
263
|
+
const skills = [];
|
|
264
|
+
|
|
265
|
+
if (!fs.existsSync(scope)) return skills;
|
|
266
|
+
|
|
267
|
+
for (const name of fs.readdirSync(scope)) {
|
|
268
|
+
const dir = path.join(scope, name);
|
|
269
|
+
if (!fs.statSync(dir).isDirectory()) continue;
|
|
270
|
+
|
|
271
|
+
const metaFile = path.join(dir, ".skill-source.json");
|
|
272
|
+
const skillFile = path.join(dir, "SKILL.md");
|
|
273
|
+
|
|
274
|
+
if (fs.existsSync(metaFile) || fs.existsSync(skillFile)) {
|
|
275
|
+
const meta = fs.existsSync(metaFile)
|
|
276
|
+
? JSON.parse(fs.readFileSync(metaFile, "utf-8"))
|
|
277
|
+
: {};
|
|
278
|
+
const hasSkillMd = fs.existsSync(skillFile);
|
|
279
|
+
|
|
280
|
+
// Parse SKILL.md frontmatter for Antigravity Skills metadata
|
|
281
|
+
let skillMeta = {};
|
|
282
|
+
if (hasSkillMd) {
|
|
283
|
+
skillMeta = parseSkillMdFrontmatter(skillFile);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Detect Progressive Disclosure structure
|
|
287
|
+
const structure = detectSkillStructure(dir);
|
|
288
|
+
|
|
289
|
+
skills.push({
|
|
290
|
+
name,
|
|
291
|
+
path: dir,
|
|
292
|
+
...meta,
|
|
293
|
+
hasSkillMd,
|
|
294
|
+
// Antigravity Skills metadata
|
|
295
|
+
description: skillMeta.description || meta.description || "",
|
|
296
|
+
tags: skillMeta.tags || meta.tags || [],
|
|
297
|
+
author: skillMeta.author || meta.publisher || "",
|
|
298
|
+
version: skillMeta.version || meta.ref || "unknown",
|
|
299
|
+
type: skillMeta.type || "standard",
|
|
300
|
+
authority: skillMeta.authority || "normal",
|
|
301
|
+
// Progressive Disclosure structure
|
|
302
|
+
structure,
|
|
303
|
+
size: getDirSize(dir),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return skills;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Parse SKILL.md YAML frontmatter
|
|
313
|
+
* Supports multi-line description with > syntax
|
|
314
|
+
*/
|
|
315
|
+
function parseSkillMdFrontmatter(skillMdPath) {
|
|
316
|
+
try {
|
|
317
|
+
const content = fs.readFileSync(skillMdPath, "utf-8");
|
|
318
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
319
|
+
if (!match) return {};
|
|
320
|
+
|
|
321
|
+
const yaml = match[1];
|
|
322
|
+
const meta = {};
|
|
323
|
+
let currentKey = null;
|
|
324
|
+
let multiLineValue = "";
|
|
325
|
+
let isMultiLine = false;
|
|
326
|
+
|
|
327
|
+
for (const line of yaml.split(/\r?\n/)) {
|
|
328
|
+
|
|
329
|
+
// Handle multi-line values (> or |)
|
|
330
|
+
if (isMultiLine) {
|
|
331
|
+
if (line.startsWith(" ") || line.trim() === "") {
|
|
332
|
+
multiLineValue += " " + line.trim();
|
|
333
|
+
continue;
|
|
334
|
+
} else {
|
|
335
|
+
meta[currentKey] = multiLineValue.trim();
|
|
336
|
+
isMultiLine = false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const colonIndex = line.indexOf(":");
|
|
341
|
+
if (colonIndex === -1) continue;
|
|
342
|
+
|
|
343
|
+
const key = line.substring(0, colonIndex).trim();
|
|
344
|
+
let value = line.substring(colonIndex + 1).trim();
|
|
345
|
+
|
|
346
|
+
// Handle multi-line indicator
|
|
347
|
+
if (value === ">" || value === "|") {
|
|
348
|
+
isMultiLine = true;
|
|
349
|
+
currentKey = key;
|
|
350
|
+
multiLineValue = "";
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Handle inline values
|
|
355
|
+
if (key && value) {
|
|
356
|
+
// Parse tags as array
|
|
357
|
+
if (key === "tags") {
|
|
358
|
+
meta[key] = value.split(",").map((t) => t.trim()).filter(Boolean);
|
|
359
|
+
} else {
|
|
360
|
+
meta[key] = value;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Finalize any pending multi-line value
|
|
366
|
+
if (isMultiLine && currentKey) {
|
|
367
|
+
meta[currentKey] = multiLineValue.trim();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return meta;
|
|
371
|
+
} catch (err) {
|
|
372
|
+
verbose(`Failed to parse SKILL.md: ${err.message}`);
|
|
373
|
+
return {};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Detect Progressive Disclosure structure
|
|
379
|
+
* Returns information about skill's directory structure
|
|
380
|
+
*/
|
|
381
|
+
function detectSkillStructure(skillDir) {
|
|
382
|
+
const structure = {
|
|
383
|
+
hasResources: false,
|
|
384
|
+
hasExamples: false,
|
|
385
|
+
hasScripts: false,
|
|
386
|
+
hasConstitution: false,
|
|
387
|
+
hasDoctrines: false,
|
|
388
|
+
hasEnforcement: false,
|
|
389
|
+
hasAssets: false,
|
|
390
|
+
hasProposals: false,
|
|
391
|
+
directories: [],
|
|
392
|
+
files: [],
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const items = fs.readdirSync(skillDir);
|
|
397
|
+
|
|
398
|
+
for (const item of items) {
|
|
399
|
+
const fullPath = path.join(skillDir, item);
|
|
400
|
+
const stat = fs.statSync(fullPath);
|
|
401
|
+
|
|
402
|
+
if (stat.isDirectory()) {
|
|
403
|
+
structure.directories.push(item);
|
|
404
|
+
|
|
405
|
+
// Check for Antigravity Skills standard directories
|
|
406
|
+
switch (item.toLowerCase()) {
|
|
407
|
+
case "resources":
|
|
408
|
+
structure.hasResources = true;
|
|
409
|
+
break;
|
|
410
|
+
case "examples":
|
|
411
|
+
structure.hasExamples = true;
|
|
412
|
+
break;
|
|
413
|
+
case "scripts":
|
|
414
|
+
structure.hasScripts = true;
|
|
415
|
+
break;
|
|
416
|
+
case "constitution":
|
|
417
|
+
structure.hasConstitution = true;
|
|
418
|
+
break;
|
|
419
|
+
case "doctrines":
|
|
420
|
+
structure.hasDoctrines = true;
|
|
421
|
+
break;
|
|
422
|
+
case "enforcement":
|
|
423
|
+
structure.hasEnforcement = true;
|
|
424
|
+
break;
|
|
425
|
+
case "assets":
|
|
426
|
+
structure.hasAssets = true;
|
|
427
|
+
break;
|
|
428
|
+
case "proposals":
|
|
429
|
+
structure.hasProposals = true;
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
structure.files.push(item);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} catch (err) {
|
|
437
|
+
verbose(`Failed to detect structure: ${err.message}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return structure;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
function getDirSize(dir) {
|
|
446
|
+
let size = 0;
|
|
447
|
+
function walk(p) {
|
|
448
|
+
for (const f of fs.readdirSync(p)) {
|
|
449
|
+
const full = path.join(p, f);
|
|
450
|
+
const stat = fs.statSync(full);
|
|
451
|
+
if (stat.isDirectory()) walk(full);
|
|
452
|
+
else size += stat.size;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
walk(dir);
|
|
456
|
+
return size;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function formatBytes(bytes) {
|
|
460
|
+
if (bytes < 1024) return bytes + " B";
|
|
461
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
|
462
|
+
return (bytes / 1024 / 1024).toFixed(1) + " MB";
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function formatDate(isoString) {
|
|
466
|
+
if (!isoString) return "unknown";
|
|
467
|
+
const date = new Date(isoString);
|
|
468
|
+
return date.toLocaleDateString() + " " + date.toLocaleTimeString();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* ===================== REGISTRY HELPERS ===================== */
|
|
472
|
+
|
|
473
|
+
function registryCachePath(name) {
|
|
474
|
+
return path.join(REGISTRY_CACHE, `${name}.json`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function loadRegistries() {
|
|
478
|
+
if (!fs.existsSync(REGISTRIES_FILE)) return [];
|
|
479
|
+
try {
|
|
480
|
+
return JSON.parse(fs.readFileSync(REGISTRIES_FILE, "utf-8"));
|
|
481
|
+
} catch {
|
|
482
|
+
return [];
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function saveRegistries(regs) {
|
|
487
|
+
fs.mkdirSync(path.dirname(REGISTRIES_FILE), { recursive: true });
|
|
488
|
+
fs.writeFileSync(REGISTRIES_FILE, JSON.stringify(regs, null, 2));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function verifyRegistrySignature(index) {
|
|
492
|
+
if (!index.signature) return false;
|
|
493
|
+
|
|
494
|
+
const { algorithm, value, publicKey } = index.signature;
|
|
495
|
+
if (algorithm !== "ed25519") return false;
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
const clone = { ...index };
|
|
499
|
+
delete clone.signature;
|
|
500
|
+
|
|
501
|
+
const data = Buffer.from(JSON.stringify(clone));
|
|
502
|
+
const sig = Buffer.from(value, "base64");
|
|
503
|
+
const key = Buffer.from(publicKey, "base64");
|
|
504
|
+
|
|
505
|
+
return crypto.verify(null, data, key, sig);
|
|
506
|
+
} catch (err) {
|
|
507
|
+
verbose(`Signature verification failed: ${err.message}`);
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function fetchRegistry(url) {
|
|
513
|
+
const name = url.replace(/[^a-z0-9]/gi, "_").toLowerCase();
|
|
514
|
+
fs.mkdirSync(REGISTRY_CACHE, { recursive: true });
|
|
515
|
+
|
|
516
|
+
if (OFFLINE) {
|
|
517
|
+
const cached = registryCachePath(name);
|
|
518
|
+
if (fs.existsSync(cached)) {
|
|
519
|
+
verbose(`Using cached registry: ${url}`);
|
|
520
|
+
return JSON.parse(fs.readFileSync(cached, "utf-8"));
|
|
521
|
+
}
|
|
522
|
+
fatal("Offline mode: no cached registry available");
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
verbose(`Fetching registry: ${url}`);
|
|
527
|
+
const json = execSync(`curl -fsSL "${url}"`, {
|
|
528
|
+
encoding: "utf-8",
|
|
529
|
+
timeout: 30000,
|
|
530
|
+
});
|
|
531
|
+
const index = JSON.parse(json);
|
|
532
|
+
|
|
533
|
+
if (!verifyRegistrySignature(index)) {
|
|
534
|
+
warn("Registry signature verification failed");
|
|
535
|
+
if (STRICT) fatal("Invalid registry signature (strict mode)");
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
fs.writeFileSync(registryCachePath(name), JSON.stringify(index, null, 2));
|
|
539
|
+
return index;
|
|
540
|
+
} catch (err) {
|
|
541
|
+
verbose(`Failed to fetch registry: ${err.message}`);
|
|
542
|
+
const cached = registryCachePath(name);
|
|
543
|
+
if (fs.existsSync(cached)) {
|
|
544
|
+
warn("Using cached registry (fetch failed)");
|
|
545
|
+
return JSON.parse(fs.readFileSync(cached, "utf-8"));
|
|
546
|
+
}
|
|
547
|
+
fatal("Failed to fetch registry and no cache available");
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function loadAllRegistries() {
|
|
552
|
+
const regs = loadRegistries();
|
|
553
|
+
const result = [];
|
|
554
|
+
|
|
555
|
+
for (const url of regs) {
|
|
556
|
+
try {
|
|
557
|
+
const index = fetchRegistry(url);
|
|
558
|
+
result.push({ url, ...index });
|
|
559
|
+
} catch (err) {
|
|
560
|
+
warn(`Failed to load registry: ${url}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return result;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/* ===================== BACKUP HELPERS ===================== */
|
|
567
|
+
|
|
568
|
+
function createBackup(skillDir, skillName) {
|
|
569
|
+
if (DRY) return null;
|
|
570
|
+
|
|
571
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
572
|
+
const backupPath = path.join(BACKUP_DIR, `${skillName}_${timestamp}`);
|
|
573
|
+
|
|
574
|
+
fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
|
575
|
+
fs.cpSync(skillDir, backupPath, { recursive: true });
|
|
576
|
+
|
|
577
|
+
verbose(`Created backup: ${backupPath}`);
|
|
578
|
+
return backupPath;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function listBackups(skillName = null) {
|
|
582
|
+
if (!fs.existsSync(BACKUP_DIR)) return [];
|
|
583
|
+
|
|
584
|
+
const backups = [];
|
|
585
|
+
for (const name of fs.readdirSync(BACKUP_DIR)) {
|
|
586
|
+
if (skillName && !name.startsWith(skillName + "_")) continue;
|
|
587
|
+
|
|
588
|
+
const backupPath = path.join(BACKUP_DIR, name);
|
|
589
|
+
const stat = fs.statSync(backupPath);
|
|
590
|
+
|
|
591
|
+
backups.push({
|
|
592
|
+
name,
|
|
593
|
+
path: backupPath,
|
|
594
|
+
createdAt: stat.mtime,
|
|
595
|
+
size: getDirSize(backupPath),
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return backups.sort((a, b) => b.createdAt - a.createdAt);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/* ===================== PUBLISH HELPER ===================== */
|
|
603
|
+
|
|
604
|
+
function publishSkill(skillDir, privateKeyPath) {
|
|
605
|
+
if (!fs.existsSync(skillDir)) fatal("Skill directory not found");
|
|
606
|
+
|
|
607
|
+
const skillName = path.basename(skillDir);
|
|
608
|
+
const checksum = merkleHash(skillDir);
|
|
609
|
+
|
|
610
|
+
const metaFile = path.join(skillDir, ".skill-source.json");
|
|
611
|
+
const skillMd = path.join(skillDir, "SKILL.md");
|
|
612
|
+
|
|
613
|
+
if (!fs.existsSync(skillMd)) fatal("SKILL.md missing");
|
|
614
|
+
|
|
615
|
+
// Parse SKILL.md frontmatter
|
|
616
|
+
let skillMeta = {};
|
|
617
|
+
try {
|
|
618
|
+
const content = fs.readFileSync(skillMd, "utf-8");
|
|
619
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
620
|
+
if (match) {
|
|
621
|
+
const yaml = match[1];
|
|
622
|
+
for (const line of yaml.split("\n")) {
|
|
623
|
+
const [key, ...valueParts] = line.split(":");
|
|
624
|
+
if (key && valueParts.length) {
|
|
625
|
+
skillMeta[key.trim()] = valueParts.join(":").trim();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
} catch (err) {
|
|
630
|
+
verbose(`Failed to parse SKILL.md frontmatter: ${err.message}`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const meta = fs.existsSync(metaFile)
|
|
634
|
+
? JSON.parse(fs.readFileSync(metaFile, "utf-8"))
|
|
635
|
+
: {};
|
|
636
|
+
|
|
637
|
+
const manifest = {
|
|
638
|
+
skill: skillName,
|
|
639
|
+
repo: meta.repo || "local",
|
|
640
|
+
version: meta.ref || skillMeta.version || "1.0.0",
|
|
641
|
+
ref: meta.ref || null,
|
|
642
|
+
checksum: `sha256:${checksum}`,
|
|
643
|
+
publisher: meta.publisher || skillMeta.author || os.userInfo().username,
|
|
644
|
+
engines: meta.engines || [],
|
|
645
|
+
tags: meta.tags || skillMeta.tags?.split(",").map((t) => t.trim()) || [],
|
|
646
|
+
description: meta.description || skillMeta.description || "",
|
|
647
|
+
createdAt: new Date().toISOString(),
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const data = Buffer.from(JSON.stringify(manifest));
|
|
651
|
+
const privateKey = fs.readFileSync(privateKeyPath);
|
|
652
|
+
|
|
653
|
+
const signature = crypto.sign(null, data, privateKey);
|
|
654
|
+
|
|
655
|
+
const output = {
|
|
656
|
+
manifest,
|
|
657
|
+
signature: {
|
|
658
|
+
algorithm: "ed25519",
|
|
659
|
+
keyId: `${manifest.publisher}-skill-v1`,
|
|
660
|
+
value: signature.toString("base64"),
|
|
661
|
+
},
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
const outFile = `${skillName}.publish.json`;
|
|
665
|
+
|
|
666
|
+
if (DRY) {
|
|
667
|
+
info(`Would create: ${outFile}`);
|
|
668
|
+
outputJSON(output);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
fs.writeFileSync(outFile, JSON.stringify(output, null, 2));
|
|
673
|
+
success(`Skill manifest generated: ${outFile}`);
|
|
674
|
+
outputJSON(output);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/* ===================== INSTALL HELPER ===================== */
|
|
678
|
+
|
|
679
|
+
function resolveSkillSpec(spec) {
|
|
680
|
+
if (spec.includes("/")) return null;
|
|
681
|
+
|
|
682
|
+
const registries = loadAllRegistries();
|
|
683
|
+
for (const r of registries) {
|
|
684
|
+
if (r.skills?.[spec]) {
|
|
685
|
+
const s = r.skills[spec];
|
|
686
|
+
const v = s.latest;
|
|
687
|
+
return {
|
|
688
|
+
repo: s.repo,
|
|
689
|
+
skill: spec,
|
|
690
|
+
ref: s.versions[v].ref,
|
|
691
|
+
checksum: s.versions[v].checksum,
|
|
692
|
+
registry: r.url,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function parseSkillSpec(spec) {
|
|
700
|
+
const [repoPart, skillPart] = spec.split("#");
|
|
701
|
+
const [org, repo] = repoPart.split("/");
|
|
702
|
+
const [skill, ref] = (skillPart || "").split("@");
|
|
703
|
+
return { org, repo, skill, ref };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/* ===================== COMMANDS ===================== */
|
|
707
|
+
|
|
708
|
+
function runInit() {
|
|
709
|
+
const targetDir = GLOBAL ? GLOBAL_DIR : WORKSPACE;
|
|
710
|
+
|
|
711
|
+
if (fs.existsSync(targetDir)) {
|
|
712
|
+
info(`Skills directory already exists: ${targetDir}`);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (DRY) {
|
|
717
|
+
info(`Would create: ${targetDir}`);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
722
|
+
success(`Initialized skills directory: ${targetDir}`);
|
|
723
|
+
|
|
724
|
+
// Create .gitignore if in workspace
|
|
725
|
+
if (!GLOBAL) {
|
|
726
|
+
const gitignore = path.join(cwd, ".agent", ".gitignore");
|
|
727
|
+
if (!fs.existsSync(gitignore)) {
|
|
728
|
+
fs.writeFileSync(gitignore, "# Skill caches\nskills/*/.skill-source.json\n");
|
|
729
|
+
verbose("Created .gitignore");
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function runList() {
|
|
735
|
+
const skills = getInstalledSkills();
|
|
736
|
+
|
|
737
|
+
if (skills.length === 0) {
|
|
738
|
+
info("No skills installed");
|
|
739
|
+
outputJSON({ skills: [] });
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (JSON_OUTPUT) {
|
|
744
|
+
outputJSON({ skills });
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
log(`\n${icons.package} ${colors.bold}Installed Skills${colors.reset}\n`);
|
|
749
|
+
log(`${colors.dim}Location: ${resolveScope()}${colors.reset}\n`);
|
|
750
|
+
|
|
751
|
+
for (const skill of skills) {
|
|
752
|
+
const status = skill.hasSkillMd
|
|
753
|
+
? `${colors.green}${icons.check}${colors.reset}`
|
|
754
|
+
: `${colors.yellow}${icons.warning}${colors.reset}`;
|
|
755
|
+
|
|
756
|
+
// Authority badge
|
|
757
|
+
let authorityBadge = "";
|
|
758
|
+
if (skill.authority === "supreme") {
|
|
759
|
+
authorityBadge = ` ${colors.magenta}[SUPREME]${colors.reset}`;
|
|
760
|
+
} else if (skill.authority === "constitutional") {
|
|
761
|
+
authorityBadge = ` ${colors.cyan}[CONST]${colors.reset}`;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
log(
|
|
765
|
+
` ${status} ${colors.bold}${skill.name}${colors.reset}` +
|
|
766
|
+
`${authorityBadge} ` +
|
|
767
|
+
`${colors.dim}v${skill.version} (${formatBytes(skill.size)})${colors.reset}`
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
// Show description if available
|
|
771
|
+
if (skill.description && VERBOSE) {
|
|
772
|
+
const desc = skill.description.length > 80
|
|
773
|
+
? skill.description.substring(0, 77) + "..."
|
|
774
|
+
: skill.description;
|
|
775
|
+
log(` ${colors.dim}${desc}${colors.reset}`);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Show Progressive Disclosure structure
|
|
779
|
+
if (VERBOSE && skill.structure) {
|
|
780
|
+
const features = [];
|
|
781
|
+
if (skill.structure.hasResources) features.push("resources");
|
|
782
|
+
if (skill.structure.hasExamples) features.push("examples");
|
|
783
|
+
if (skill.structure.hasScripts) features.push("scripts");
|
|
784
|
+
if (skill.structure.hasConstitution) features.push("constitution");
|
|
785
|
+
if (skill.structure.hasDoctrines) features.push("doctrines");
|
|
786
|
+
if (skill.structure.hasEnforcement) features.push("enforcement");
|
|
787
|
+
|
|
788
|
+
if (features.length > 0) {
|
|
789
|
+
log(` ${colors.cyan}Structure: ${features.join(", ")}${colors.reset}`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Show tags
|
|
794
|
+
if (skill.tags && skill.tags.length > 0 && VERBOSE) {
|
|
795
|
+
log(` ${colors.yellow}Tags: ${skill.tags.join(", ")}${colors.reset}`);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (VERBOSE) {
|
|
799
|
+
log(` ${colors.dim}Repo: ${skill.repo || "local"}${colors.reset}`);
|
|
800
|
+
log(` ${colors.dim}Author: ${skill.author || "unknown"}${colors.reset}`);
|
|
801
|
+
log(` ${colors.dim}Installed: ${formatDate(skill.installedAt)}${colors.reset}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
log(`\n${colors.dim}Total: ${skills.length} skill(s)${colors.reset}\n`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
function runLock() {
|
|
811
|
+
if (!fs.existsSync(WORKSPACE)) fatal("No .agent/skills directory");
|
|
812
|
+
|
|
813
|
+
const skills = {};
|
|
814
|
+
for (const name of fs.readdirSync(WORKSPACE)) {
|
|
815
|
+
const dir = path.join(WORKSPACE, name);
|
|
816
|
+
if (!fs.statSync(dir).isDirectory()) continue;
|
|
817
|
+
|
|
818
|
+
const metaFile = path.join(dir, ".skill-source.json");
|
|
819
|
+
if (!fs.existsSync(metaFile)) continue;
|
|
820
|
+
|
|
821
|
+
const meta = JSON.parse(fs.readFileSync(metaFile, "utf-8"));
|
|
822
|
+
skills[name] = {
|
|
823
|
+
repo: meta.repo,
|
|
824
|
+
skill: meta.skill,
|
|
825
|
+
ref: meta.ref,
|
|
826
|
+
checksum: `sha256:${meta.checksum}`,
|
|
827
|
+
publisher: meta.publisher || null,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const lock = {
|
|
832
|
+
lockVersion: 1,
|
|
833
|
+
generatedAt: new Date().toISOString(),
|
|
834
|
+
generator: `@dataguruin/add-skill@${pkg.version}`,
|
|
835
|
+
skills,
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
if (DRY) {
|
|
839
|
+
info("Would generate skill-lock.json");
|
|
840
|
+
outputJSON(lock);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
fs.mkdirSync(path.join(cwd, ".agent"), { recursive: true });
|
|
845
|
+
fs.writeFileSync(
|
|
846
|
+
path.join(cwd, ".agent", "skill-lock.json"),
|
|
847
|
+
JSON.stringify(lock, null, 2)
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
success("skill-lock.json generated");
|
|
851
|
+
outputJSON(lock);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function runVerify() {
|
|
855
|
+
const scope = resolveScope();
|
|
856
|
+
let issues = 0;
|
|
857
|
+
const results = [];
|
|
858
|
+
|
|
859
|
+
if (!fs.existsSync(scope)) {
|
|
860
|
+
info("No skills directory found");
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
for (const name of fs.readdirSync(scope)) {
|
|
865
|
+
const dir = path.join(scope, name);
|
|
866
|
+
if (!fs.statSync(dir).isDirectory()) continue;
|
|
867
|
+
|
|
868
|
+
const metaFile = path.join(dir, ".skill-source.json");
|
|
869
|
+
if (!fs.existsSync(metaFile)) {
|
|
870
|
+
log(`${icons.cross} ${colors.red}${name}: missing metadata${colors.reset}`);
|
|
871
|
+
results.push({ name, status: "missing_metadata" });
|
|
872
|
+
issues++;
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const meta = JSON.parse(fs.readFileSync(metaFile, "utf-8"));
|
|
877
|
+
const actual = merkleHash(dir);
|
|
878
|
+
|
|
879
|
+
if (actual !== meta.checksum) {
|
|
880
|
+
log(`${icons.cross} ${colors.red}${name}: checksum mismatch${colors.reset}`);
|
|
881
|
+
results.push({ name, status: "checksum_mismatch", expected: meta.checksum, actual });
|
|
882
|
+
issues++;
|
|
883
|
+
} else {
|
|
884
|
+
log(`${icons.check} ${colors.green}${name}${colors.reset}`);
|
|
885
|
+
results.push({ name, status: "ok" });
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
outputJSON({ verified: results.length, issues, results });
|
|
890
|
+
|
|
891
|
+
if (issues && STRICT) process.exit(1);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function runDoctor() {
|
|
895
|
+
const scope = resolveScope();
|
|
896
|
+
let errors = 0;
|
|
897
|
+
let warnings = 0;
|
|
898
|
+
const results = [];
|
|
899
|
+
|
|
900
|
+
if (!fs.existsSync(scope)) {
|
|
901
|
+
info("No skills directory found");
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const lock = fs.existsSync(path.join(cwd, ".agent", "skill-lock.json"))
|
|
906
|
+
? loadSkillLock()
|
|
907
|
+
: null;
|
|
908
|
+
|
|
909
|
+
for (const name of fs.readdirSync(scope)) {
|
|
910
|
+
const dir = path.join(scope, name);
|
|
911
|
+
if (!fs.statSync(dir).isDirectory()) continue;
|
|
912
|
+
|
|
913
|
+
const result = { name, issues: [] };
|
|
914
|
+
|
|
915
|
+
if (!fs.existsSync(path.join(dir, "SKILL.md"))) {
|
|
916
|
+
log(`${icons.cross} ${colors.red}${name}: missing SKILL.md${colors.reset}`);
|
|
917
|
+
result.issues.push("missing_skill_md");
|
|
918
|
+
errors++;
|
|
919
|
+
results.push(result);
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const metaFile = path.join(dir, ".skill-source.json");
|
|
924
|
+
if (!fs.existsSync(metaFile)) {
|
|
925
|
+
log(`${icons.cross} ${colors.red}${name}: missing metadata${colors.reset}`);
|
|
926
|
+
result.issues.push("missing_metadata");
|
|
927
|
+
errors++;
|
|
928
|
+
results.push(result);
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const meta = JSON.parse(fs.readFileSync(metaFile, "utf-8"));
|
|
933
|
+
const actual = merkleHash(dir);
|
|
934
|
+
|
|
935
|
+
if (actual !== meta.checksum) {
|
|
936
|
+
if (FIX && !DRY) {
|
|
937
|
+
meta.checksum = actual;
|
|
938
|
+
fs.writeFileSync(metaFile, JSON.stringify(meta, null, 2));
|
|
939
|
+
warn(`${name}: checksum fixed`);
|
|
940
|
+
result.issues.push("checksum_fixed");
|
|
941
|
+
} else {
|
|
942
|
+
log(`${STRICT ? icons.cross : icons.warning} ${STRICT ? colors.red : colors.yellow}${name}: checksum drift${colors.reset}`);
|
|
943
|
+
result.issues.push("checksum_drift");
|
|
944
|
+
STRICT ? errors++ : warnings++;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (lock && !lock.skills[name]) {
|
|
949
|
+
log(`${STRICT ? icons.cross : icons.warning} ${STRICT ? colors.red : colors.yellow}${name}: not in skill-lock.json${colors.reset}`);
|
|
950
|
+
result.issues.push("not_in_lock");
|
|
951
|
+
STRICT ? errors++ : warnings++;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (result.issues.length === 0) {
|
|
955
|
+
log(`${icons.check} ${colors.green}${name}${colors.reset}`);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
results.push(result);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
outputJSON({ errors, warnings, results });
|
|
962
|
+
|
|
963
|
+
if (STRICT && errors) process.exit(1);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function runUpgradeAll() {
|
|
967
|
+
const scope = resolveScope();
|
|
968
|
+
if (!fs.existsSync(scope)) {
|
|
969
|
+
info("No skills directory found");
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const skills = getInstalledSkills().filter((s) => s.repo && s.repo !== "local");
|
|
974
|
+
|
|
975
|
+
if (skills.length === 0) {
|
|
976
|
+
info("No upgradable skills found");
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
log(`\n${icons.refresh} Upgrading ${skills.length} skill(s)...\n`);
|
|
981
|
+
|
|
982
|
+
for (const skill of skills) {
|
|
983
|
+
const spinner = spin(`Upgrading ${skill.name}`);
|
|
984
|
+
|
|
985
|
+
try {
|
|
986
|
+
if (!DRY) {
|
|
987
|
+
createBackup(skill.path, skill.name);
|
|
988
|
+
fs.rmSync(skill.path, { recursive: true, force: true });
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const spec = `${skill.repo}#${skill.skill}${skill.ref ? "@" + skill.ref : ""}`;
|
|
992
|
+
|
|
993
|
+
if (DRY) {
|
|
994
|
+
spinner.succeed(`Would upgrade: ${skill.name}`);
|
|
995
|
+
} else {
|
|
996
|
+
execSync(`node "${process.argv[1]}" install "${spec}"`, { stdio: "pipe" });
|
|
997
|
+
spinner.succeed(`Upgraded: ${skill.name}`);
|
|
998
|
+
}
|
|
999
|
+
} catch (err) {
|
|
1000
|
+
spinner.fail(`Failed: ${skill.name}`);
|
|
1001
|
+
verbose(err.message);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
success("Upgrade complete");
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
async function runUninstall(skillName) {
|
|
1009
|
+
if (!skillName) fatal("Missing skill name");
|
|
1010
|
+
|
|
1011
|
+
const scope = resolveScope();
|
|
1012
|
+
const targetDir = path.join(scope, skillName);
|
|
1013
|
+
|
|
1014
|
+
if (!fs.existsSync(targetDir)) {
|
|
1015
|
+
fatal(`Skill not found: ${skillName}`);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const confirmed = await confirm(
|
|
1019
|
+
`${icons.trash} Remove skill "${skillName}"?`
|
|
1020
|
+
);
|
|
1021
|
+
|
|
1022
|
+
if (!confirmed) {
|
|
1023
|
+
info("Cancelled");
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (DRY) {
|
|
1028
|
+
info(`Would remove: ${targetDir}`);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
createBackup(targetDir, skillName);
|
|
1033
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
1034
|
+
|
|
1035
|
+
success(`Removed: ${skillName}`);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
async function runUpdate(skillName) {
|
|
1039
|
+
if (!skillName) fatal("Missing skill name");
|
|
1040
|
+
|
|
1041
|
+
const scope = resolveScope();
|
|
1042
|
+
const targetDir = path.join(scope, skillName);
|
|
1043
|
+
|
|
1044
|
+
if (!fs.existsSync(targetDir)) {
|
|
1045
|
+
fatal(`Skill not found: ${skillName}`);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const metaFile = path.join(targetDir, ".skill-source.json");
|
|
1049
|
+
if (!fs.existsSync(metaFile)) {
|
|
1050
|
+
fatal("Skill metadata not found");
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const meta = JSON.parse(fs.readFileSync(metaFile, "utf-8"));
|
|
1054
|
+
if (!meta.repo || meta.repo === "local") {
|
|
1055
|
+
fatal("Cannot update local skill");
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const spinner = spin(`Updating ${skillName}`);
|
|
1059
|
+
|
|
1060
|
+
try {
|
|
1061
|
+
if (!DRY) {
|
|
1062
|
+
createBackup(targetDir, skillName);
|
|
1063
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
const spec = `${meta.repo}#${meta.skill}${meta.ref ? "@" + meta.ref : ""}`;
|
|
1067
|
+
|
|
1068
|
+
if (DRY) {
|
|
1069
|
+
spinner.succeed(`Would update: ${skillName}`);
|
|
1070
|
+
} else {
|
|
1071
|
+
execSync(`node "${process.argv[1]}" install "${spec}"`, { stdio: "pipe" });
|
|
1072
|
+
spinner.succeed(`Updated: ${skillName}`);
|
|
1073
|
+
}
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
spinner.fail(`Failed to update: ${skillName}`);
|
|
1076
|
+
verbose(err.message);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function runInstall(spec) {
|
|
1081
|
+
// Auto-init local workspace if not global and not already initialized
|
|
1082
|
+
if (!GLOBAL && !fs.existsSync(path.join(cwd, ".agent"))) {
|
|
1083
|
+
info("Initializing local workspace...");
|
|
1084
|
+
runInit();
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (!spec) fatal("Missing skill reference");
|
|
1088
|
+
|
|
1089
|
+
const resolved = resolveSkillSpec(spec);
|
|
1090
|
+
let org, repo, skill, ref;
|
|
1091
|
+
|
|
1092
|
+
if (resolved) {
|
|
1093
|
+
const repoParts = resolved.repo.split("/");
|
|
1094
|
+
org = repoParts[0];
|
|
1095
|
+
repo = repoParts[1];
|
|
1096
|
+
skill = resolved.skill;
|
|
1097
|
+
ref = resolved.ref;
|
|
1098
|
+
verbose(`Resolved from registry: ${resolved.registry}`);
|
|
1099
|
+
} else {
|
|
1100
|
+
const parsed = parseSkillSpec(spec);
|
|
1101
|
+
org = parsed.org;
|
|
1102
|
+
repo = parsed.repo;
|
|
1103
|
+
skill = parsed.skill;
|
|
1104
|
+
ref = parsed.ref;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (!org || !repo || !skill) fatal("Invalid skill reference");
|
|
1108
|
+
|
|
1109
|
+
if (LOCKED) {
|
|
1110
|
+
const lock = loadSkillLock();
|
|
1111
|
+
if (!lock.skills[skill]) fatal(`Skill "${skill}" not in lock`);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const scope = resolveScope();
|
|
1115
|
+
const targetDir = path.join(scope, skill);
|
|
1116
|
+
|
|
1117
|
+
if (fs.existsSync(targetDir) && !FORCE) {
|
|
1118
|
+
fatal(`Skill already installed: ${skill} (use --force to reinstall)`);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
if (fs.existsSync(targetDir)) {
|
|
1122
|
+
createBackup(targetDir, skill);
|
|
1123
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
fs.mkdirSync(scope, { recursive: true });
|
|
1127
|
+
|
|
1128
|
+
const spinner = spin(`Installing ${skill} from ${org}/${repo}`);
|
|
1129
|
+
|
|
1130
|
+
try {
|
|
1131
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "add-skill-"));
|
|
1132
|
+
|
|
1133
|
+
if (DRY) {
|
|
1134
|
+
spinner.succeed(`Would install: ${skill}${ref ? ` @ ${ref}` : ""}`);
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
execSync(`git clone --depth=1 https://github.com/${org}/${repo}.git "${tmp}"`, {
|
|
1139
|
+
stdio: "pipe",
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
if (ref) {
|
|
1143
|
+
execSync(`git -C "${tmp}" checkout ${ref}`, { stdio: "pipe" });
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
const skillSrc = path.join(tmp, skill);
|
|
1147
|
+
if (!fs.existsSync(skillSrc)) {
|
|
1148
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
1149
|
+
spinner.fail(`Skill not found in repo: ${skill}`);
|
|
1150
|
+
fatal(`The skill "${skill}" was not found in ${org}/${repo}`);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
fs.cpSync(skillSrc, targetDir, { recursive: true });
|
|
1154
|
+
|
|
1155
|
+
if (!fs.existsSync(path.join(targetDir, "SKILL.md"))) {
|
|
1156
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
1157
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
1158
|
+
spinner.fail("Invalid skill");
|
|
1159
|
+
fatal("SKILL.md missing");
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const checksum = merkleHash(targetDir);
|
|
1163
|
+
|
|
1164
|
+
fs.writeFileSync(
|
|
1165
|
+
path.join(targetDir, ".skill-source.json"),
|
|
1166
|
+
JSON.stringify(
|
|
1167
|
+
{
|
|
1168
|
+
repo: `${org}/${repo}`,
|
|
1169
|
+
skill,
|
|
1170
|
+
ref: ref || null,
|
|
1171
|
+
checksum,
|
|
1172
|
+
installedAt: new Date().toISOString(),
|
|
1173
|
+
},
|
|
1174
|
+
null,
|
|
1175
|
+
2
|
|
1176
|
+
)
|
|
1177
|
+
);
|
|
1178
|
+
|
|
1179
|
+
if (LOCKED) {
|
|
1180
|
+
const lock = loadSkillLock();
|
|
1181
|
+
const expected = lock.skills[skill].checksum;
|
|
1182
|
+
if (expected !== `sha256:${checksum}`) {
|
|
1183
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
1184
|
+
spinner.fail("Checksum mismatch");
|
|
1185
|
+
fatal("Checksum mismatch in locked mode");
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
1190
|
+
|
|
1191
|
+
spinner.succeed(`Installed: ${skill}${ref ? ` @ ${ref}` : ""}`);
|
|
1192
|
+
outputJSON({ installed: skill, checksum, ref });
|
|
1193
|
+
} catch (err) {
|
|
1194
|
+
spinner.fail(`Failed to install: ${skill}`);
|
|
1195
|
+
fatal(err.message, err);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function runRegistry(sub, url) {
|
|
1200
|
+
if (sub === "add") {
|
|
1201
|
+
if (!url) fatal("Missing registry URL");
|
|
1202
|
+
|
|
1203
|
+
const regs = loadRegistries();
|
|
1204
|
+
if (regs.includes(url)) {
|
|
1205
|
+
info("Registry already added");
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const spinner = spin(`Adding registry`);
|
|
1210
|
+
try {
|
|
1211
|
+
regs.push(url);
|
|
1212
|
+
saveRegistries(regs);
|
|
1213
|
+
fetchRegistry(url);
|
|
1214
|
+
spinner.succeed(`Registry added: ${url}`);
|
|
1215
|
+
} catch (err) {
|
|
1216
|
+
spinner.fail("Failed to add registry");
|
|
1217
|
+
fatal(err.message);
|
|
1218
|
+
}
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
if (sub === "list") {
|
|
1223
|
+
const regs = loadRegistries();
|
|
1224
|
+
if (regs.length === 0) {
|
|
1225
|
+
info("No registries configured");
|
|
1226
|
+
outputJSON({ registries: [] });
|
|
1227
|
+
} else {
|
|
1228
|
+
log(`\n${icons.folder} ${colors.bold}Configured Registries${colors.reset}\n`);
|
|
1229
|
+
regs.forEach((r) => log(` ${icons.arrow} ${r}`));
|
|
1230
|
+
log("");
|
|
1231
|
+
outputJSON({ registries: regs });
|
|
1232
|
+
}
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
if (sub === "remove") {
|
|
1237
|
+
if (!url) fatal("Missing registry URL");
|
|
1238
|
+
|
|
1239
|
+
const regs = loadRegistries();
|
|
1240
|
+
const filtered = regs.filter((r) => r !== url);
|
|
1241
|
+
|
|
1242
|
+
if (filtered.length === regs.length) {
|
|
1243
|
+
info("Registry not found");
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
saveRegistries(filtered);
|
|
1248
|
+
success(`Registry removed: ${url}`);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
if (sub === "refresh") {
|
|
1253
|
+
const regs = loadRegistries();
|
|
1254
|
+
const spinner = spin(`Refreshing ${regs.length} registry(ies)`);
|
|
1255
|
+
|
|
1256
|
+
for (const url of regs) {
|
|
1257
|
+
try {
|
|
1258
|
+
fetchRegistry(url);
|
|
1259
|
+
} catch (err) {
|
|
1260
|
+
warn(`Failed to refresh: ${url}`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
spinner.succeed("Registries refreshed");
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
fatal(`Unknown registry subcommand: ${sub || "(none)"}`);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function runSearch(query) {
|
|
1272
|
+
if (!query) fatal("Missing search query");
|
|
1273
|
+
|
|
1274
|
+
const registries = loadAllRegistries();
|
|
1275
|
+
const results = [];
|
|
1276
|
+
|
|
1277
|
+
for (const r of registries) {
|
|
1278
|
+
for (const [name, meta] of Object.entries(r.skills || {})) {
|
|
1279
|
+
const matches =
|
|
1280
|
+
name.toLowerCase().includes(query.toLowerCase()) ||
|
|
1281
|
+
meta.description?.toLowerCase().includes(query.toLowerCase()) ||
|
|
1282
|
+
meta.tags?.some((t) => t.toLowerCase().includes(query.toLowerCase()));
|
|
1283
|
+
|
|
1284
|
+
if (matches) {
|
|
1285
|
+
results.push({
|
|
1286
|
+
name,
|
|
1287
|
+
publisher: meta.publisher,
|
|
1288
|
+
latest: meta.latest,
|
|
1289
|
+
description: meta.description,
|
|
1290
|
+
tags: meta.tags,
|
|
1291
|
+
registry: r.url,
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
if (results.length === 0) {
|
|
1298
|
+
info("No skills found matching your query");
|
|
1299
|
+
outputJSON({ results: [] });
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
if (JSON_OUTPUT) {
|
|
1304
|
+
outputJSON({ results });
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
log(`\n${icons.package} ${colors.bold}Search Results${colors.reset}\n`);
|
|
1309
|
+
|
|
1310
|
+
for (const r of results) {
|
|
1311
|
+
log(
|
|
1312
|
+
` ${colors.bold}${r.name}${colors.reset} ` +
|
|
1313
|
+
`${colors.dim}v${r.latest}${colors.reset} ` +
|
|
1314
|
+
`${colors.cyan}@${r.publisher}${colors.reset}`
|
|
1315
|
+
);
|
|
1316
|
+
if (r.description) {
|
|
1317
|
+
log(` ${colors.dim}${r.description}${colors.reset}`);
|
|
1318
|
+
}
|
|
1319
|
+
if (r.tags?.length) {
|
|
1320
|
+
log(` ${colors.yellow}${r.tags.map((t) => `#${t}`).join(" ")}${colors.reset}`);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
log(`\n${colors.dim}Found ${results.length} skill(s)${colors.reset}\n`);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function runInfo(name) {
|
|
1328
|
+
if (!name) fatal("Missing skill name");
|
|
1329
|
+
|
|
1330
|
+
// Check if installed locally first
|
|
1331
|
+
const scope = resolveScope();
|
|
1332
|
+
const localDir = path.join(scope, name);
|
|
1333
|
+
|
|
1334
|
+
if (fs.existsSync(localDir)) {
|
|
1335
|
+
const metaFile = path.join(localDir, ".skill-source.json");
|
|
1336
|
+
const skillMd = path.join(localDir, "SKILL.md");
|
|
1337
|
+
|
|
1338
|
+
log(`\n${icons.package} ${colors.bold}${name}${colors.reset} ${colors.green}(installed)${colors.reset}\n`);
|
|
1339
|
+
log(` ${colors.dim}Path: ${localDir}${colors.reset}`);
|
|
1340
|
+
|
|
1341
|
+
if (fs.existsSync(metaFile)) {
|
|
1342
|
+
const meta = JSON.parse(fs.readFileSync(metaFile, "utf-8"));
|
|
1343
|
+
log(` ${colors.dim}Repo: ${meta.repo || "local"}${colors.reset}`);
|
|
1344
|
+
log(` ${colors.dim}Ref: ${meta.ref || "N/A"}${colors.reset}`);
|
|
1345
|
+
log(` ${colors.dim}Installed: ${formatDate(meta.installedAt)}${colors.reset}`);
|
|
1346
|
+
log(` ${colors.dim}Checksum: ${meta.checksum?.substring(0, 12)}...${colors.reset}`);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
if (fs.existsSync(skillMd)) {
|
|
1350
|
+
log(`\n${colors.dim}--- SKILL.md ---${colors.reset}\n`);
|
|
1351
|
+
const content = fs.readFileSync(skillMd, "utf-8");
|
|
1352
|
+
const lines = content.split("\n").slice(0, 20);
|
|
1353
|
+
log(lines.join("\n"));
|
|
1354
|
+
if (content.split("\n").length > 20) {
|
|
1355
|
+
log(`\n${colors.dim}... (truncated)${colors.reset}`);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
log("");
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// Check registries
|
|
1364
|
+
const registries = loadAllRegistries();
|
|
1365
|
+
|
|
1366
|
+
for (const r of registries) {
|
|
1367
|
+
if (r.skills?.[name]) {
|
|
1368
|
+
const s = r.skills[name];
|
|
1369
|
+
|
|
1370
|
+
if (JSON_OUTPUT) {
|
|
1371
|
+
outputJSON({ name, ...s, registry: r.url });
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
log(`\n${icons.package} ${colors.bold}${name}${colors.reset}\n`);
|
|
1376
|
+
log(` Publisher: ${colors.cyan}@${s.publisher}${colors.reset}`);
|
|
1377
|
+
log(` Repo: ${s.repo}`);
|
|
1378
|
+
log(` Latest: ${colors.green}${s.latest}${colors.reset}`);
|
|
1379
|
+
if (s.description) log(` Description: ${s.description}`);
|
|
1380
|
+
if (s.tags?.length) log(` Tags: ${s.tags.map((t) => `#${t}`).join(" ")}`);
|
|
1381
|
+
log(`\n Versions:`);
|
|
1382
|
+
Object.keys(s.versions).forEach((v) => log(` - ${v}`));
|
|
1383
|
+
log("");
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
fatal("Skill not found");
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function runPublish() {
|
|
1392
|
+
const skillDir = params[0];
|
|
1393
|
+
if (!skillDir) fatal("Missing skill directory");
|
|
1394
|
+
|
|
1395
|
+
const keyIndex = args.indexOf("--key");
|
|
1396
|
+
const keyPath = keyIndex !== -1 ? args[keyIndex + 1] : null;
|
|
1397
|
+
if (!keyPath) fatal("Missing --key <private-key>");
|
|
1398
|
+
|
|
1399
|
+
publishSkill(path.resolve(skillDir), keyPath);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function runCache(sub) {
|
|
1403
|
+
if (sub === "clear") {
|
|
1404
|
+
if (DRY) {
|
|
1405
|
+
info(`Would clear cache: ${CACHE_ROOT}`);
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
if (fs.existsSync(CACHE_ROOT)) {
|
|
1410
|
+
const size = getDirSize(CACHE_ROOT);
|
|
1411
|
+
fs.rmSync(CACHE_ROOT, { recursive: true, force: true });
|
|
1412
|
+
success(`Cache cleared (${formatBytes(size)})`);
|
|
1413
|
+
} else {
|
|
1414
|
+
info("Cache already empty");
|
|
1415
|
+
}
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
if (sub === "info" || !sub) {
|
|
1420
|
+
if (!fs.existsSync(CACHE_ROOT)) {
|
|
1421
|
+
info("Cache is empty");
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const registrySize = fs.existsSync(REGISTRY_CACHE) ? getDirSize(REGISTRY_CACHE) : 0;
|
|
1426
|
+
const backupSize = fs.existsSync(BACKUP_DIR) ? getDirSize(BACKUP_DIR) : 0;
|
|
1427
|
+
const totalSize = getDirSize(CACHE_ROOT);
|
|
1428
|
+
|
|
1429
|
+
log(`\n${icons.folder} ${colors.bold}Cache Info${colors.reset}\n`);
|
|
1430
|
+
log(` Location: ${CACHE_ROOT}`);
|
|
1431
|
+
log(` Registries: ${formatBytes(registrySize)}`);
|
|
1432
|
+
log(` Backups: ${formatBytes(backupSize)}`);
|
|
1433
|
+
log(` Total: ${formatBytes(totalSize)}`);
|
|
1434
|
+
|
|
1435
|
+
const backups = listBackups();
|
|
1436
|
+
if (backups.length) {
|
|
1437
|
+
log(`\n Recent backups:`);
|
|
1438
|
+
backups.slice(0, 5).forEach((b) => {
|
|
1439
|
+
log(` - ${b.name} (${formatBytes(b.size)})`);
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
log("");
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if (sub === "backups") {
|
|
1448
|
+
const backups = listBackups();
|
|
1449
|
+
if (backups.length === 0) {
|
|
1450
|
+
info("No backups found");
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
log(`\n${icons.folder} ${colors.bold}Backups${colors.reset}\n`);
|
|
1455
|
+
backups.forEach((b) => {
|
|
1456
|
+
log(` ${b.name} (${formatBytes(b.size)}) - ${formatDate(b.createdAt.toISOString())}`);
|
|
1457
|
+
});
|
|
1458
|
+
log("");
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
fatal(`Unknown cache subcommand: ${sub}`);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/**
|
|
1466
|
+
* Validate skill against Antigravity Skills specification
|
|
1467
|
+
*/
|
|
1468
|
+
function runValidate(skillName) {
|
|
1469
|
+
const scope = resolveScope();
|
|
1470
|
+
let skillsToValidate = [];
|
|
1471
|
+
|
|
1472
|
+
if (skillName) {
|
|
1473
|
+
const skillDir = path.join(scope, skillName);
|
|
1474
|
+
if (!fs.existsSync(skillDir)) {
|
|
1475
|
+
fatal(`Skill not found: ${skillName}`);
|
|
1476
|
+
}
|
|
1477
|
+
skillsToValidate = [{ name: skillName, path: skillDir }];
|
|
1478
|
+
} else {
|
|
1479
|
+
skillsToValidate = getInstalledSkills();
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
if (skillsToValidate.length === 0) {
|
|
1483
|
+
info("No skills to validate");
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
log(`\n${colors.bold}🔍 Antigravity Skills Validation${colors.reset}\n`);
|
|
1488
|
+
|
|
1489
|
+
let totalErrors = 0;
|
|
1490
|
+
let totalWarnings = 0;
|
|
1491
|
+
const results = [];
|
|
1492
|
+
|
|
1493
|
+
for (const skill of skillsToValidate) {
|
|
1494
|
+
const skillDir = skill.path;
|
|
1495
|
+
const errors = [];
|
|
1496
|
+
const warnings = [];
|
|
1497
|
+
const suggestions = [];
|
|
1498
|
+
|
|
1499
|
+
// Check 1: SKILL.md exists
|
|
1500
|
+
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
1501
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
1502
|
+
errors.push("Missing SKILL.md (required)");
|
|
1503
|
+
} else {
|
|
1504
|
+
// Check 2: Valid YAML frontmatter
|
|
1505
|
+
const meta = parseSkillMdFrontmatter(skillMdPath);
|
|
1506
|
+
|
|
1507
|
+
if (!meta.name) {
|
|
1508
|
+
warnings.push("SKILL.md: Missing 'name' in frontmatter");
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// Check 3: Description field (critical for semantic routing)
|
|
1512
|
+
if (!meta.description) {
|
|
1513
|
+
errors.push("SKILL.md: Missing 'description' field (required for semantic routing)");
|
|
1514
|
+
} else if (meta.description.length < 50) {
|
|
1515
|
+
warnings.push("SKILL.md: Description is too short (recommend 50+ chars for better routing)");
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// Check 4: Tags
|
|
1519
|
+
if (!meta.tags || meta.tags.length === 0) {
|
|
1520
|
+
suggestions.push("Consider adding 'tags' for better discoverability");
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// Check 5: Author
|
|
1524
|
+
if (!meta.author) {
|
|
1525
|
+
suggestions.push("Consider adding 'author' field");
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// Check 6: .skill-source.json
|
|
1530
|
+
const metaPath = path.join(skillDir, ".skill-source.json");
|
|
1531
|
+
if (fs.existsSync(metaPath)) {
|
|
1532
|
+
try {
|
|
1533
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
1534
|
+
if (!meta.repo) warnings.push(".skill-source.json: Missing 'repo' field");
|
|
1535
|
+
if (!meta.ref) warnings.push(".skill-source.json: Missing 'ref' (version) field");
|
|
1536
|
+
if (!meta.publisher) suggestions.push(".skill-source.json: Consider adding 'publisher'");
|
|
1537
|
+
} catch (err) {
|
|
1538
|
+
errors.push(".skill-source.json: Invalid JSON");
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// Check 7: Progressive Disclosure structure
|
|
1543
|
+
const structure = skill.structure || detectSkillStructure(skillDir);
|
|
1544
|
+
|
|
1545
|
+
// Resources, examples, scripts are recommended
|
|
1546
|
+
if (!structure.hasResources && !structure.hasExamples && !structure.hasScripts) {
|
|
1547
|
+
suggestions.push("Consider adding resources/, examples/, or scripts/ for Progressive Disclosure");
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// Print results for this skill
|
|
1551
|
+
const hasIssues = errors.length > 0 || warnings.length > 0;
|
|
1552
|
+
const status = errors.length > 0
|
|
1553
|
+
? `${colors.red}❌ FAIL${colors.reset}`
|
|
1554
|
+
: warnings.length > 0
|
|
1555
|
+
? `${colors.yellow}⚠️ WARN${colors.reset}`
|
|
1556
|
+
: `${colors.green}✅ PASS${colors.reset}`;
|
|
1557
|
+
|
|
1558
|
+
log(`${status} ${colors.bold}${skill.name}${colors.reset}`);
|
|
1559
|
+
|
|
1560
|
+
if (VERBOSE || hasIssues) {
|
|
1561
|
+
errors.forEach(e => log(` ${colors.red}ERROR: ${e}${colors.reset}`));
|
|
1562
|
+
warnings.forEach(w => log(` ${colors.yellow}WARN: ${w}${colors.reset}`));
|
|
1563
|
+
if (VERBOSE) {
|
|
1564
|
+
suggestions.forEach(s => log(` ${colors.dim}SUGGEST: ${s}${colors.reset}`));
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
totalErrors += errors.length;
|
|
1569
|
+
totalWarnings += warnings.length;
|
|
1570
|
+
|
|
1571
|
+
results.push({
|
|
1572
|
+
name: skill.name,
|
|
1573
|
+
valid: errors.length === 0,
|
|
1574
|
+
errors,
|
|
1575
|
+
warnings,
|
|
1576
|
+
suggestions,
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
log(`\n${colors.dim}─────────────────────────────────────────${colors.reset}`);
|
|
1581
|
+
log(`Total: ${skillsToValidate.length} skill(s), ${totalErrors} error(s), ${totalWarnings} warning(s)\n`);
|
|
1582
|
+
|
|
1583
|
+
outputJSON({ results, totalErrors, totalWarnings });
|
|
1584
|
+
|
|
1585
|
+
if (STRICT && totalErrors > 0) {
|
|
1586
|
+
process.exit(1);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
/**
|
|
1591
|
+
* Analyze skill metadata and structure in detail
|
|
1592
|
+
*/
|
|
1593
|
+
function runAnalyze(skillName) {
|
|
1594
|
+
if (!skillName) fatal("Missing skill name");
|
|
1595
|
+
|
|
1596
|
+
const scope = resolveScope();
|
|
1597
|
+
const skillDir = path.join(scope, skillName);
|
|
1598
|
+
|
|
1599
|
+
if (!fs.existsSync(skillDir)) {
|
|
1600
|
+
fatal(`Skill not found: ${skillName}`);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
1604
|
+
const metaPath = path.join(skillDir, ".skill-source.json");
|
|
1605
|
+
|
|
1606
|
+
log(`\n${colors.bold}📊 Skill Analysis: ${skillName}${colors.reset}\n`);
|
|
1607
|
+
log(`${colors.dim}Path: ${skillDir}${colors.reset}\n`);
|
|
1608
|
+
|
|
1609
|
+
// Parse SKILL.md
|
|
1610
|
+
if (fs.existsSync(skillMdPath)) {
|
|
1611
|
+
const meta = parseSkillMdFrontmatter(skillMdPath);
|
|
1612
|
+
|
|
1613
|
+
log(`${colors.cyan}SKILL.md Frontmatter:${colors.reset}`);
|
|
1614
|
+
log(` Name: ${meta.name || colors.dim + "(not set)" + colors.reset}`);
|
|
1615
|
+
log(` Version: ${meta.version || colors.dim + "(not set)" + colors.reset}`);
|
|
1616
|
+
log(` Type: ${meta.type || "standard"}`);
|
|
1617
|
+
log(` Authority: ${meta.authority || "normal"}`);
|
|
1618
|
+
log(` Author: ${meta.author || colors.dim + "(not set)" + colors.reset}`);
|
|
1619
|
+
|
|
1620
|
+
if (meta.description) {
|
|
1621
|
+
log(` Description: ${meta.description.substring(0, 100)}${meta.description.length > 100 ? "..." : ""}`);
|
|
1622
|
+
} else {
|
|
1623
|
+
log(` Description: ${colors.red}(MISSING - required for semantic routing)${colors.reset}`);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
if (meta.tags && meta.tags.length > 0) {
|
|
1627
|
+
log(` Tags: ${meta.tags.join(", ")}`);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
if (meta.parent) {
|
|
1631
|
+
log(` Parent: ${meta.parent}`);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
log("");
|
|
1635
|
+
} else {
|
|
1636
|
+
log(`${colors.red}SKILL.md: NOT FOUND${colors.reset}\n`);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
// Parse .skill-source.json
|
|
1640
|
+
if (fs.existsSync(metaPath)) {
|
|
1641
|
+
try {
|
|
1642
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
1643
|
+
log(`${colors.cyan}Source Metadata:${colors.reset}`);
|
|
1644
|
+
log(` Repo: ${meta.repo || "local"}`);
|
|
1645
|
+
log(` Ref: ${meta.ref || "N/A"}`);
|
|
1646
|
+
log(` Publisher: ${meta.publisher || "unknown"}`);
|
|
1647
|
+
log(` Installed: ${formatDate(meta.installedAt)}`);
|
|
1648
|
+
if (meta.checksum) {
|
|
1649
|
+
log(` Checksum: ${meta.checksum.substring(0, 16)}...`);
|
|
1650
|
+
}
|
|
1651
|
+
log("");
|
|
1652
|
+
} catch (err) {
|
|
1653
|
+
log(`${colors.red}.skill-source.json: Invalid${colors.reset}\n`);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// Structure analysis
|
|
1658
|
+
const structure = detectSkillStructure(skillDir);
|
|
1659
|
+
|
|
1660
|
+
log(`${colors.cyan}Progressive Disclosure Structure:${colors.reset}`);
|
|
1661
|
+
|
|
1662
|
+
const structureItems = [
|
|
1663
|
+
{ name: "resources/", has: structure.hasResources, desc: "On-demand heavy content" },
|
|
1664
|
+
{ name: "examples/", has: structure.hasExamples, desc: "Before/after code examples" },
|
|
1665
|
+
{ name: "scripts/", has: structure.hasScripts, desc: "Validation/automation scripts" },
|
|
1666
|
+
{ name: "constitution/", has: structure.hasConstitution, desc: "Supreme authority docs" },
|
|
1667
|
+
{ name: "doctrines/", has: structure.hasDoctrines, desc: "Domain-specific laws" },
|
|
1668
|
+
{ name: "enforcement/", has: structure.hasEnforcement, desc: "Checklists & protocols" },
|
|
1669
|
+
{ name: "proposals/", has: structure.hasProposals, desc: "Evolution & Change Control" },
|
|
1670
|
+
{ name: "assets/", has: structure.hasAssets, desc: "Static assets" },
|
|
1671
|
+
];
|
|
1672
|
+
|
|
1673
|
+
for (const item of structureItems) {
|
|
1674
|
+
const icon = item.has ? colors.green + "✓" + colors.reset : colors.dim + "○" + colors.reset;
|
|
1675
|
+
const name = item.has ? colors.bold + item.name + colors.reset : colors.dim + item.name + colors.reset;
|
|
1676
|
+
log(` ${icon} ${name} ${colors.dim}${item.desc}${colors.reset}`);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
log("");
|
|
1680
|
+
|
|
1681
|
+
// Files list
|
|
1682
|
+
log(`${colors.cyan}Top-level Files:${colors.reset}`);
|
|
1683
|
+
structure.files.forEach(f => {
|
|
1684
|
+
log(` ${colors.dim}•${colors.reset} ${f}`);
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
log("");
|
|
1688
|
+
|
|
1689
|
+
// Directories list
|
|
1690
|
+
if (structure.directories.length > 0) {
|
|
1691
|
+
log(`${colors.cyan}Directories:${colors.reset}`);
|
|
1692
|
+
structure.directories.forEach(d => {
|
|
1693
|
+
const subPath = path.join(skillDir, d);
|
|
1694
|
+
try {
|
|
1695
|
+
const subItems = fs.readdirSync(subPath);
|
|
1696
|
+
log(` ${colors.dim}📁${colors.reset} ${d}/ ${colors.dim}(${subItems.length} items)${colors.reset}`);
|
|
1697
|
+
} catch {
|
|
1698
|
+
log(` ${colors.dim}📁${colors.reset} ${d}/`);
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
log("");
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// Size info
|
|
1705
|
+
const size = getDirSize(skillDir);
|
|
1706
|
+
log(`${colors.cyan}Size:${colors.reset} ${formatBytes(size)}\n`);
|
|
1707
|
+
|
|
1708
|
+
// Antigravity compatibility score
|
|
1709
|
+
let score = 0;
|
|
1710
|
+
if (fs.existsSync(skillMdPath)) score += 20;
|
|
1711
|
+
const meta = parseSkillMdFrontmatter(skillMdPath);
|
|
1712
|
+
if (meta.description) score += 25;
|
|
1713
|
+
if (meta.tags && meta.tags.length > 0) score += 10;
|
|
1714
|
+
if (meta.author) score += 5;
|
|
1715
|
+
if (structure.hasResources || structure.hasExamples || structure.hasScripts) score += 20;
|
|
1716
|
+
if (fs.existsSync(metaPath)) score += 10;
|
|
1717
|
+
if (structure.hasConstitution || structure.hasDoctrines) score += 10;
|
|
1718
|
+
|
|
1719
|
+
const scoreColor = score >= 80 ? colors.green : score >= 50 ? colors.yellow : colors.red;
|
|
1720
|
+
log(`${colors.cyan}Antigravity Compatibility Score:${colors.reset} ${scoreColor}${score}/100${colors.reset}`);
|
|
1721
|
+
|
|
1722
|
+
if (score < 50) {
|
|
1723
|
+
log(`${colors.dim}Tip: Add 'description' to SKILL.md and consider Progressive Disclosure structure${colors.reset}`);
|
|
1724
|
+
} else if (score < 80) {
|
|
1725
|
+
log(`${colors.dim}Tip: Add examples/ or scripts/ to improve Progressive Disclosure${colors.reset}`);
|
|
1726
|
+
} else {
|
|
1727
|
+
log(`${colors.green}✨ Excellent! This skill follows Antigravity best practices${colors.reset}`);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
log("");
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
|
|
1734
|
+
function showHelp() {
|
|
1735
|
+
console.log(`
|
|
1736
|
+
${colors.bold}add-skill${colors.reset} v${pkg.version} - Enterprise Agent Skill Manager
|
|
1737
|
+
|
|
1738
|
+
|
|
1739
|
+
${colors.bold}USAGE${colors.reset}
|
|
1740
|
+
add-skill <command> [options]
|
|
1741
|
+
|
|
1742
|
+
${colors.bold}COMMANDS${colors.reset}
|
|
1743
|
+
${colors.cyan}install${colors.reset} <org/repo#skill@ref> Install a skill from GitHub
|
|
1744
|
+
${colors.cyan}install${colors.reset} <skill-name> Install a skill from registry
|
|
1745
|
+
${colors.cyan}uninstall${colors.reset} <skill-name> Remove an installed skill
|
|
1746
|
+
${colors.cyan}update${colors.reset} <skill-name> Update a single skill
|
|
1747
|
+
${colors.cyan}upgrade-all${colors.reset} Upgrade all installed skills
|
|
1748
|
+
${colors.cyan}list${colors.reset} Show installed skills
|
|
1749
|
+
${colors.cyan}verify${colors.reset} Verify skill checksums
|
|
1750
|
+
${colors.cyan}doctor${colors.reset} Check skill health
|
|
1751
|
+
${colors.cyan}lock${colors.reset} Generate skill-lock.json
|
|
1752
|
+
${colors.cyan}init${colors.reset} Initialize skills directory
|
|
1753
|
+
|
|
1754
|
+
${colors.bold}ANTIGRAVITY SKILLS${colors.reset}
|
|
1755
|
+
${colors.cyan}validate${colors.reset} [skill-name] Validate against Antigravity spec
|
|
1756
|
+
${colors.cyan}analyze${colors.reset} <skill-name> Analyze skill metadata & structure
|
|
1757
|
+
|
|
1758
|
+
${colors.bold}REGISTRY${colors.reset}
|
|
1759
|
+
${colors.cyan}registry add${colors.reset} <url> Add a skill registry
|
|
1760
|
+
${colors.cyan}registry list${colors.reset} List configured registries
|
|
1761
|
+
${colors.cyan}registry remove${colors.reset} <url> Remove a registry
|
|
1762
|
+
${colors.cyan}registry refresh${colors.reset} Refresh registry cache
|
|
1763
|
+
${colors.cyan}search${colors.reset} <query> Search skills in registries
|
|
1764
|
+
${colors.cyan}info${colors.reset} <skill-name> Show skill details
|
|
1765
|
+
|
|
1766
|
+
${colors.bold}PUBLISHING${colors.reset}
|
|
1767
|
+
${colors.cyan}publish${colors.reset} <dir> --key <path> Generate signed manifest
|
|
1768
|
+
|
|
1769
|
+
${colors.bold}CACHE${colors.reset}
|
|
1770
|
+
${colors.cyan}cache info${colors.reset} Show cache statistics
|
|
1771
|
+
${colors.cyan}cache clear${colors.reset} Clear all caches
|
|
1772
|
+
${colors.cyan}cache backups${colors.reset} List skill backups
|
|
1773
|
+
|
|
1774
|
+
${colors.bold}FLAGS${colors.reset}
|
|
1775
|
+
${colors.yellow}--global, -g${colors.reset} Use global skills directory
|
|
1776
|
+
${colors.yellow}--force, -f${colors.reset} Force operation (skip prompts)
|
|
1777
|
+
${colors.yellow}--locked${colors.reset} Enforce skill-lock.json
|
|
1778
|
+
${colors.yellow}--strict${colors.reset} Fail on any violation
|
|
1779
|
+
${colors.yellow}--fix${colors.reset} Auto-fix safe issues
|
|
1780
|
+
${colors.yellow}--dry-run${colors.reset} Preview without changes
|
|
1781
|
+
${colors.yellow}--verbose, -v${colors.reset} Detailed output
|
|
1782
|
+
${colors.yellow}--json${colors.reset} Output as JSON
|
|
1783
|
+
${colors.yellow}--offline${colors.reset} Use cached registries only
|
|
1784
|
+
|
|
1785
|
+
${colors.bold}EXAMPLES${colors.reset}
|
|
1786
|
+
add-skill install dataguruin/skills#browser@v1.0.0
|
|
1787
|
+
add-skill install browser --force
|
|
1788
|
+
add-skill list --verbose
|
|
1789
|
+
add-skill update browser
|
|
1790
|
+
add-skill verify --strict
|
|
1791
|
+
add-skill doctor --fix
|
|
1792
|
+
add-skill registry add https://skills.example.com/index.json
|
|
1793
|
+
add-skill search "automation" --json
|
|
1794
|
+
`);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
/* ===================== MAIN ROUTER ===================== */
|
|
1798
|
+
|
|
1799
|
+
async function main() {
|
|
1800
|
+
try {
|
|
1801
|
+
switch (command) {
|
|
1802
|
+
case "init":
|
|
1803
|
+
runInit();
|
|
1804
|
+
break;
|
|
1805
|
+
|
|
1806
|
+
case "list":
|
|
1807
|
+
case "ls":
|
|
1808
|
+
runList();
|
|
1809
|
+
break;
|
|
1810
|
+
|
|
1811
|
+
case "lock":
|
|
1812
|
+
runLock();
|
|
1813
|
+
break;
|
|
1814
|
+
|
|
1815
|
+
case "verify":
|
|
1816
|
+
runVerify();
|
|
1817
|
+
break;
|
|
1818
|
+
|
|
1819
|
+
case "doctor":
|
|
1820
|
+
runDoctor();
|
|
1821
|
+
break;
|
|
1822
|
+
|
|
1823
|
+
case "upgrade-all":
|
|
1824
|
+
runUpgradeAll();
|
|
1825
|
+
break;
|
|
1826
|
+
|
|
1827
|
+
case "uninstall":
|
|
1828
|
+
case "remove":
|
|
1829
|
+
case "rm":
|
|
1830
|
+
await runUninstall(params[0]);
|
|
1831
|
+
break;
|
|
1832
|
+
|
|
1833
|
+
case "update":
|
|
1834
|
+
await runUpdate(params[0]);
|
|
1835
|
+
break;
|
|
1836
|
+
|
|
1837
|
+
case "install":
|
|
1838
|
+
case "add":
|
|
1839
|
+
case "i":
|
|
1840
|
+
runInstall(params[0]);
|
|
1841
|
+
break;
|
|
1842
|
+
|
|
1843
|
+
case "registry":
|
|
1844
|
+
case "reg":
|
|
1845
|
+
runRegistry(params[0], params[1]);
|
|
1846
|
+
break;
|
|
1847
|
+
|
|
1848
|
+
case "search":
|
|
1849
|
+
case "s":
|
|
1850
|
+
runSearch(params[0]);
|
|
1851
|
+
break;
|
|
1852
|
+
|
|
1853
|
+
case "info":
|
|
1854
|
+
case "show":
|
|
1855
|
+
runInfo(params[0]);
|
|
1856
|
+
break;
|
|
1857
|
+
|
|
1858
|
+
case "publish":
|
|
1859
|
+
runPublish();
|
|
1860
|
+
break;
|
|
1861
|
+
|
|
1862
|
+
case "validate":
|
|
1863
|
+
case "check":
|
|
1864
|
+
runValidate(params[0]);
|
|
1865
|
+
break;
|
|
1866
|
+
|
|
1867
|
+
case "analyze":
|
|
1868
|
+
runAnalyze(params[0]);
|
|
1869
|
+
break;
|
|
1870
|
+
|
|
1871
|
+
case "cache":
|
|
1872
|
+
runCache(params[0]);
|
|
1873
|
+
break;
|
|
1874
|
+
|
|
1875
|
+
|
|
1876
|
+
case "help":
|
|
1877
|
+
case "--help":
|
|
1878
|
+
case "-h":
|
|
1879
|
+
case undefined:
|
|
1880
|
+
showHelp();
|
|
1881
|
+
break;
|
|
1882
|
+
|
|
1883
|
+
case "--version":
|
|
1884
|
+
case "-V":
|
|
1885
|
+
console.log(pkg.version);
|
|
1886
|
+
break;
|
|
1887
|
+
|
|
1888
|
+
default:
|
|
1889
|
+
// Smart Install: If command looks like org/repo, treat as install arg
|
|
1890
|
+
if (command && command.includes("/")) {
|
|
1891
|
+
runInstall(command);
|
|
1892
|
+
break;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
console.error(`${icons.error} Unknown command: ${command}`);
|
|
1896
|
+
showHelp();
|
|
1897
|
+
process.exit(1);
|
|
1898
|
+
}
|
|
1899
|
+
} catch (err) {
|
|
1900
|
+
fatal(err.message, err);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
main();
|