install-agent-skill 1.2.3

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.
@@ -0,0 +1,512 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * add-agent-skill
4
+ * Vercel-Style CLI - Full Port v2.0
5
+ */
6
+
7
+ import fs from "fs";
8
+ import path from "path";
9
+ import os from "os";
10
+ import { execSync } from "child_process";
11
+ import crypto from "crypto";
12
+ import prompts from "prompts";
13
+ import kleur from "kleur";
14
+ import ora from "ora";
15
+ import boxen from "boxen";
16
+ import { createRequire } from "module";
17
+
18
+ const require = createRequire(import.meta.url);
19
+ const pkg = (() => {
20
+ try { return require("../package.json"); } catch { return { version: "4.0.0" }; }
21
+ })();
22
+
23
+ // --- CONFIG & CONSTANTS ---
24
+ const cwd = process.cwd();
25
+ const WORKSPACE = path.join(cwd, ".agent", "skills");
26
+ const GLOBAL_DIR = path.join(os.homedir(), ".gemini", "antigravity", "skills");
27
+ const CACHE_ROOT = process.env.ADD_SKILL_CACHE_DIR || path.join(os.homedir(), ".cache", "add-skill");
28
+ const REGISTRY_CACHE = path.join(CACHE_ROOT, "registries");
29
+ const REGISTRIES_FILE = path.join(CACHE_ROOT, "registries.json");
30
+ const BACKUP_DIR = path.join(CACHE_ROOT, "backups");
31
+
32
+ // Args
33
+ const args = process.argv.slice(2);
34
+ const command = args[0] || "help";
35
+ const flags = new Set(args.filter((a) => a.startsWith("--")));
36
+ const params = args.filter((a) => !a.startsWith("--")).slice(1);
37
+
38
+ const GLOBAL = flags.has("--global") || flags.has("-g");
39
+ const VERBOSE = flags.has("--verbose") || flags.has("-v");
40
+ const JSON_OUTPUT = flags.has("--json");
41
+ const FORCE = flags.has("--force") || flags.has("-f");
42
+ const DRY = flags.has("--dry-run");
43
+ const STRICT = flags.has("--strict");
44
+ const FIX = flags.has("--fix");
45
+ const LOCKED = flags.has("--locked");
46
+ const OFFLINE = flags.has("--offline");
47
+
48
+ // --- THEME & SYMBOLS ---
49
+ const S = { branch: "│", diamond: "◇", diamondFilled: "◆", check: "✓", cross: "x", arrow: "→" };
50
+ const c = { cyan: kleur.cyan, gray: kleur.gray, green: kleur.green, red: kleur.red, yellow: kleur.yellow, magenta: kleur.magenta, bold: kleur.bold, dim: kleur.dim };
51
+
52
+ // --- UI Helpers ---
53
+ function step(text, icon = S.diamond, color = "gray") {
54
+ console.log(` ${c.gray(S.branch)} ${c[color](icon)} ${text}`);
55
+ }
56
+ function stepLine() { console.log(` ${c.gray(S.branch)}`); }
57
+ function fatal(msg) { console.log(` ${c.gray(S.branch)} ${c.red(S.cross)} ${c.red(msg)}`); process.exit(1); }
58
+ function success(msg) { console.log(` ${c.gray(S.branch)} ${c.green(S.check)} ${c.green(msg)}`); }
59
+ function outputJSON(data) { if (JSON_OUTPUT) console.log(JSON.stringify(data, null, 2)); }
60
+
61
+ // --- Core Helpers ---
62
+ function resolveScope() {
63
+ if (GLOBAL) return GLOBAL_DIR;
64
+ if (fs.existsSync(path.join(cwd, ".agent"))) return WORKSPACE;
65
+ return GLOBAL_DIR;
66
+ }
67
+
68
+ function getDirSize(dir) {
69
+ let size = 0;
70
+ try {
71
+ const walk = (p) => {
72
+ for (const f of fs.readdirSync(p)) {
73
+ const full = path.join(p, f);
74
+ const stat = fs.statSync(full);
75
+ if (stat.isDirectory()) walk(full);
76
+ else size += stat.size;
77
+ }
78
+ };
79
+ walk(dir);
80
+ } catch { }
81
+ return size;
82
+ }
83
+
84
+ function formatBytes(bytes) {
85
+ if (bytes < 1024) return bytes + " B";
86
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
87
+ return (bytes / 1024 / 1024).toFixed(1) + " MB";
88
+ }
89
+
90
+ function formatDate(iso) { return iso ? new Date(iso).toLocaleDateString() : "unknown"; }
91
+
92
+ function parseSkillMdFrontmatter(p) {
93
+ try {
94
+ const content = fs.readFileSync(p, "utf-8");
95
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
96
+ if (!match) return {};
97
+ const meta = {};
98
+ for (const line of match[1].split(/\r?\n/)) {
99
+ const i = line.indexOf(":");
100
+ if (i === -1) continue;
101
+ const key = line.substring(0, i).trim();
102
+ const val = line.substring(i + 1).trim();
103
+ if (key === "tags") meta[key] = val.split(",").map(t => t.trim()).filter(Boolean);
104
+ else if (key && val) meta[key] = val;
105
+ }
106
+ return meta;
107
+ } catch { return {}; }
108
+ }
109
+
110
+ function detectSkillStructure(dir) {
111
+ const s = { hasResources: false, hasExamples: false, hasScripts: false, hasConstitution: false, hasDoctrines: false, hasEnforcement: false, hasProposals: false, directories: [], files: [] };
112
+ try {
113
+ for (const item of fs.readdirSync(dir)) {
114
+ const full = path.join(dir, item);
115
+ if (fs.statSync(full).isDirectory()) {
116
+ s.directories.push(item);
117
+ const l = item.toLowerCase();
118
+ if (l === "resources") s.hasResources = true;
119
+ if (l === "examples") s.hasExamples = true;
120
+ if (l === "scripts") s.hasScripts = true;
121
+ if (l === "constitution") s.hasConstitution = true;
122
+ if (l === "doctrines") s.hasDoctrines = true;
123
+ if (l === "enforcement") s.hasEnforcement = true;
124
+ if (l === "proposals") s.hasProposals = true;
125
+ } else s.files.push(item);
126
+ }
127
+ } catch { }
128
+ return s;
129
+ }
130
+
131
+ function getInstalledSkills() {
132
+ const scope = resolveScope();
133
+ const skills = [];
134
+ if (!fs.existsSync(scope)) return skills;
135
+ for (const name of fs.readdirSync(scope)) {
136
+ const dir = path.join(scope, name);
137
+ if (!fs.statSync(dir).isDirectory()) continue;
138
+ const metaFile = path.join(dir, ".skill-source.json");
139
+ const skillFile = path.join(dir, "SKILL.md");
140
+ if (fs.existsSync(metaFile) || fs.existsSync(skillFile)) {
141
+ const meta = fs.existsSync(metaFile) ? JSON.parse(fs.readFileSync(metaFile, "utf-8")) : {};
142
+ const hasSkillMd = fs.existsSync(skillFile);
143
+ const skillMeta = hasSkillMd ? parseSkillMdFrontmatter(skillFile) : {};
144
+ skills.push({ name, path: dir, ...meta, hasSkillMd, description: skillMeta.description || meta.description || "", tags: skillMeta.tags || [], author: skillMeta.author || meta.publisher || "", version: skillMeta.version || meta.ref || "unknown", structure: detectSkillStructure(dir), size: getDirSize(dir) });
145
+ }
146
+ }
147
+ return skills;
148
+ }
149
+
150
+ function merkleHash(dir) {
151
+ const files = [];
152
+ const walk = (p) => {
153
+ for (const f of fs.readdirSync(p)) {
154
+ if (f === ".skill-source.json") continue;
155
+ const full = path.join(p, f);
156
+ const stat = fs.statSync(full);
157
+ if (stat.isDirectory()) walk(full);
158
+ else { const h = crypto.createHash("sha256").update(fs.readFileSync(full)).digest("hex"); files.push(`${path.relative(dir, full)}:${h}`); }
159
+ }
160
+ };
161
+ walk(dir);
162
+ files.sort();
163
+ return crypto.createHash("sha256").update(files.join("|")).digest("hex");
164
+ }
165
+
166
+ function parseSkillSpec(spec) {
167
+ const [repoPart, skillPart] = spec.split("#");
168
+ const [org, repo] = repoPart.split("/");
169
+ const [skill, ref] = (skillPart || "").split("@");
170
+ return { org, repo, skill, ref };
171
+ }
172
+
173
+ function loadRegistries() { try { return fs.existsSync(REGISTRIES_FILE) ? JSON.parse(fs.readFileSync(REGISTRIES_FILE, "utf-8")) : []; } catch { return []; } }
174
+ function saveRegistries(regs) { fs.mkdirSync(path.dirname(REGISTRIES_FILE), { recursive: true }); fs.writeFileSync(REGISTRIES_FILE, JSON.stringify(regs, null, 2)); }
175
+ function loadSkillLock() { const f = path.join(cwd, ".agent", "skill-lock.json"); if (!fs.existsSync(f)) fatal("skill-lock.json not found"); return JSON.parse(fs.readFileSync(f, "utf-8")); }
176
+
177
+ function createBackup(skillDir, skillName) {
178
+ if (DRY) return null;
179
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
180
+ const bp = path.join(BACKUP_DIR, `${skillName}_${ts}`);
181
+ fs.mkdirSync(BACKUP_DIR, { recursive: true });
182
+ fs.cpSync(skillDir, bp, { recursive: true });
183
+ return bp;
184
+ }
185
+
186
+ function listBackups(skillName = null) {
187
+ if (!fs.existsSync(BACKUP_DIR)) return [];
188
+ const backups = [];
189
+ for (const name of fs.readdirSync(BACKUP_DIR)) {
190
+ if (skillName && !name.startsWith(skillName + "_")) continue;
191
+ const bp = path.join(BACKUP_DIR, name);
192
+ backups.push({ name, path: bp, createdAt: fs.statSync(bp).mtime, size: getDirSize(bp) });
193
+ }
194
+ return backups.sort((a, b) => b.createdAt - a.createdAt);
195
+ }
196
+
197
+ // --- COMMANDS ---
198
+
199
+ async function runInit() {
200
+ stepLine();
201
+ const targetDir = GLOBAL ? GLOBAL_DIR : WORKSPACE;
202
+ if (fs.existsSync(targetDir)) { step(`Skills directory already exists: ${targetDir}`, S.check, "green"); return; }
203
+ if (DRY) { step(`Would create: ${targetDir}`, S.diamond); return; }
204
+ fs.mkdirSync(targetDir, { recursive: true });
205
+ if (!GLOBAL) {
206
+ const gi = path.join(cwd, ".agent", ".gitignore");
207
+ if (!fs.existsSync(gi)) fs.writeFileSync(gi, "# Skill caches\nskills/*/.skill-source.json\n");
208
+ }
209
+ success(`Initialized: ${targetDir}`);
210
+ }
211
+
212
+ async function runList() {
213
+ stepLine();
214
+ step(c.bold("Installed Skills"), S.diamondFilled, "cyan");
215
+ console.log(` ${c.gray(S.branch)} ${c.dim("Location: " + resolveScope())}`);
216
+ stepLine();
217
+ const skills = getInstalledSkills();
218
+ if (skills.length === 0) { step(c.dim("No skills installed."), S.diamond); stepLine(); return; }
219
+ if (JSON_OUTPUT) { outputJSON({ skills }); return; }
220
+ for (const s of skills) {
221
+ const icon = s.hasSkillMd ? c.green(S.check) : c.yellow(S.diamond);
222
+ console.log(` ${c.gray(S.branch)} ${icon} ${c.bold(s.name)} ${c.dim("v" + s.version)} ${c.dim("(" + formatBytes(s.size) + ")")}`);
223
+ if (s.description && VERBOSE) console.log(` ${c.gray(S.branch)} ${c.dim(s.description.substring(0, 60))}`);
224
+ }
225
+ stepLine();
226
+ console.log(` ${c.gray(S.branch)} ${c.dim("Total: " + skills.length + " skill(s)")}`);
227
+ stepLine();
228
+ }
229
+
230
+ async function runInstall(spec) {
231
+ if (!spec) { fatal("Missing skill spec. Usage: add-skill <org/repo>"); return; }
232
+ const { org, repo, skill: singleSkill, ref } = parseSkillSpec(spec);
233
+ if (!org || !repo) { fatal("Invalid spec. Format: org/repo or org/repo#skill"); return; }
234
+ const url = `https://github.com/${org}/${repo}.git`;
235
+ stepLine();
236
+ console.log(` ${c.bgCyan().black(" skills ")}`);
237
+ console.log();
238
+ console.log(` ${c.green(S.diamond)} Source: ${c.cyan(url)}`);
239
+ console.log(` ${c.gray(S.branch)}`);
240
+ const spinner = ora({ text: "Cloning repository", prefixText: ` ${c.gray(S.branch)} ${c.cyan(S.diamondFilled)} `, color: "cyan" }).start();
241
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "add-skill-"));
242
+ try { execSync(`git clone --depth=1 ${url} "${tmp}"`, { stdio: "pipe" }); if (ref) execSync(`git -C "${tmp}" checkout ${ref}`, { stdio: "pipe" }); } catch { spinner.stop(); console.log(` ${c.gray(S.branch)} ${c.red(S.cross)} ${c.red("Failed to clone")}`); fs.rmSync(tmp, { recursive: true, force: true }); return; }
243
+ spinner.stop(); console.log(` ${c.gray(S.branch)} ${c.green(S.check)} Repository cloned`);
244
+ const skillsInRepo = [];
245
+ for (const e of fs.readdirSync(tmp)) { const sp = path.join(tmp, e); if (fs.statSync(sp).isDirectory() && fs.existsSync(path.join(sp, "SKILL.md"))) { const m = parseSkillMdFrontmatter(path.join(sp, "SKILL.md")); skillsInRepo.push({ title: e + (m.description ? c.dim(` (${m.description.substring(0, 40)}...)`) : ""), value: e, selected: singleSkill ? e === singleSkill : true }); } }
246
+ if (skillsInRepo.length === 0) { step(c.yellow("No valid skills found"), S.diamond, "yellow"); fs.rmSync(tmp, { recursive: true, force: true }); return; }
247
+ stepLine(); step(`Found ${skillsInRepo.length} skills`, S.diamond); stepLine();
248
+ const skillsR = await prompts({ type: "multiselect", name: "skills", message: "Select skills to install", choices: skillsInRepo, hint: "- Space to select. Return to submit", instructions: false });
249
+ if (!skillsR.skills || skillsR.skills.length === 0) { console.log(`\n ${c.yellow("Cancelled.")}`); fs.rmSync(tmp, { recursive: true, force: true }); return; }
250
+ stepLine();
251
+ const scopeR = await prompts({ type: "select", name: "scope", message: "Installation scope", choices: [{ title: "Project (current directory)", value: "project" }, { title: "Global", value: "global" }], initial: 0 });
252
+ const targetScope = scopeR.scope === "global" ? GLOBAL_DIR : WORKSPACE;
253
+ stepLine(); step("Installation Summary", S.diamond); stepLine();
254
+ let boxContent = ""; for (const sn of skillsR.skills) boxContent += `${c.cyan(sn)}\n ${c.dim(targetScope)}\n\n`;
255
+ const box = boxen(boxContent.trim(), { padding: 1, borderStyle: "round", borderColor: "gray", dimBorder: true });
256
+ box.split("\n").forEach(l => console.log(` ${c.gray(S.branch)} ${l}`));
257
+
258
+ stepLine(); fs.mkdirSync(targetScope, { recursive: true });
259
+ for (const sn of skillsR.skills) {
260
+ const src = path.join(tmp, sn), dest = path.join(targetScope, sn);
261
+ if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
262
+ fs.cpSync(src, dest, { recursive: true });
263
+ const hash = merkleHash(dest);
264
+ fs.writeFileSync(path.join(dest, ".skill-source.json"), JSON.stringify({ repo: `${org}/${repo}`, skill: sn, ref: ref || null, checksum: hash, installedAt: new Date().toISOString() }, null, 2));
265
+ console.log(` ${c.gray(S.branch)} ${c.green(S.check)} Installed: ${c.bold(sn)}`);
266
+ }
267
+ fs.rmSync(tmp, { recursive: true, force: true });
268
+ stepLine(); console.log(` ${c.cyan("Done!")}`); console.log();
269
+ }
270
+
271
+ async function runUninstall(skillName) {
272
+ if (!skillName) fatal("Missing skill name");
273
+ const scope = resolveScope(), targetDir = path.join(scope, skillName);
274
+ if (!fs.existsSync(targetDir)) fatal(`Skill not found: ${skillName}`);
275
+ stepLine();
276
+ const confirm = await prompts({ type: "confirm", name: "value", message: `Remove skill "${skillName}"?`, initial: false });
277
+ if (!confirm.value) { step("Cancelled", S.diamond); return; }
278
+ if (DRY) { step(`Would remove: ${targetDir}`, S.diamond); return; }
279
+ createBackup(targetDir, skillName);
280
+ fs.rmSync(targetDir, { recursive: true, force: true });
281
+ success(`Removed: ${skillName}`); stepLine();
282
+ }
283
+
284
+ async function runUpdate(skillName) {
285
+ if (!skillName) fatal("Missing skill name");
286
+ const scope = resolveScope(), targetDir = path.join(scope, skillName);
287
+ if (!fs.existsSync(targetDir)) fatal(`Skill not found: ${skillName}`);
288
+ const metaFile = path.join(targetDir, ".skill-source.json");
289
+ if (!fs.existsSync(metaFile)) fatal("Skill metadata not found");
290
+ const meta = JSON.parse(fs.readFileSync(metaFile, "utf-8"));
291
+ if (!meta.repo || meta.repo === "local") fatal("Cannot update local skill");
292
+ stepLine();
293
+ const spinner = ora({ text: `Updating ${skillName}`, prefixText: ` ${c.gray(S.branch)} ${c.cyan(S.diamondFilled)} `, color: "cyan" }).start();
294
+ try {
295
+ if (!DRY) { createBackup(targetDir, skillName); fs.rmSync(targetDir, { recursive: true, force: true }); }
296
+ const spec = `${meta.repo}#${meta.skill}${meta.ref ? "@" + meta.ref : ""}`;
297
+ if (DRY) { spinner.stop(); step(`Would update: ${skillName}`, S.diamond); } else { await runInstall(spec); }
298
+ } catch (err) { spinner.stop(); step(`Failed: ${err.message}`, S.cross, "red"); }
299
+ }
300
+
301
+ function runLock() {
302
+ if (!fs.existsSync(WORKSPACE)) fatal("No .agent/skills directory");
303
+ stepLine();
304
+ const skills = {};
305
+ for (const name of fs.readdirSync(WORKSPACE)) {
306
+ const dir = path.join(WORKSPACE, name);
307
+ if (!fs.statSync(dir).isDirectory()) continue;
308
+ const mf = path.join(dir, ".skill-source.json");
309
+ if (!fs.existsSync(mf)) continue;
310
+ const m = JSON.parse(fs.readFileSync(mf, "utf-8"));
311
+ skills[name] = { repo: m.repo, skill: m.skill, ref: m.ref, checksum: `sha256:${m.checksum}`, publisher: m.publisher || null };
312
+ }
313
+ const lock = { lockVersion: 1, generatedAt: new Date().toISOString(), generator: `add-agent-skill@${pkg.version}`, skills };
314
+ if (DRY) { step("Would generate skill-lock.json", S.diamond); outputJSON(lock); return; }
315
+ fs.mkdirSync(path.join(cwd, ".agent"), { recursive: true });
316
+ fs.writeFileSync(path.join(cwd, ".agent", "skill-lock.json"), JSON.stringify(lock, null, 2));
317
+ success("skill-lock.json generated"); stepLine();
318
+ outputJSON(lock);
319
+ }
320
+
321
+ function runVerify() {
322
+ const scope = resolveScope();
323
+ if (!fs.existsSync(scope)) { stepLine(); step("No skills directory found", S.diamond); return; }
324
+ stepLine(); step(c.bold("Verifying Skills"), S.diamondFilled, "cyan"); stepLine();
325
+ let issues = 0;
326
+ for (const name of fs.readdirSync(scope)) {
327
+ const dir = path.join(scope, name);
328
+ if (!fs.statSync(dir).isDirectory()) continue;
329
+ const mf = path.join(dir, ".skill-source.json");
330
+ if (!fs.existsSync(mf)) { step(`${name}: ${c.red("missing metadata")}`, S.cross, "red"); issues++; continue; }
331
+ const m = JSON.parse(fs.readFileSync(mf, "utf-8")), actual = merkleHash(dir);
332
+ if (actual !== m.checksum) { step(`${name}: ${c.red("checksum mismatch")}`, S.cross, "red"); issues++; }
333
+ else step(`${name}: ${c.green("OK")}`, S.check, "green");
334
+ }
335
+ stepLine(); console.log(` ${c.gray(S.branch)} ${issues ? c.red(issues + " issue(s)") : c.green("All verified")}`); stepLine();
336
+ if (issues && STRICT) process.exit(1);
337
+ }
338
+
339
+ function runDoctor() {
340
+ const scope = resolveScope();
341
+ if (!fs.existsSync(scope)) { stepLine(); step("No skills directory found", S.diamond); return; }
342
+ stepLine(); step(c.bold("Health Check"), S.diamondFilled, "cyan"); stepLine();
343
+ let errors = 0, warnings = 0;
344
+ const lock = fs.existsSync(path.join(cwd, ".agent", "skill-lock.json")) ? loadSkillLock() : null;
345
+ for (const name of fs.readdirSync(scope)) {
346
+ const dir = path.join(scope, name);
347
+ if (!fs.statSync(dir).isDirectory()) continue;
348
+ if (!fs.existsSync(path.join(dir, "SKILL.md"))) { step(`${name}: ${c.red("missing SKILL.md")}`, S.cross, "red"); errors++; continue; }
349
+ const mf = path.join(dir, ".skill-source.json");
350
+ if (!fs.existsSync(mf)) { step(`${name}: ${c.red("missing metadata")}`, S.cross, "red"); errors++; continue; }
351
+ const m = JSON.parse(fs.readFileSync(mf, "utf-8")), actual = merkleHash(dir);
352
+ if (actual !== m.checksum) {
353
+ if (FIX && !DRY) { m.checksum = actual; fs.writeFileSync(mf, JSON.stringify(m, null, 2)); step(`${name}: ${c.yellow("checksum fixed")}`, S.diamond, "yellow"); }
354
+ else { step(`${name}: ${c.yellow("checksum drift")}`, S.diamond, "yellow"); STRICT ? errors++ : warnings++; }
355
+ } else if (lock && !lock.skills[name]) { step(`${name}: ${c.yellow("not in lock")}`, S.diamond, "yellow"); STRICT ? errors++ : warnings++; }
356
+ else step(`${name}: ${c.green("healthy")}`, S.check, "green");
357
+ }
358
+ stepLine(); console.log(` ${c.gray(S.branch)} Errors: ${errors}, Warnings: ${warnings}`); stepLine();
359
+ if (STRICT && errors) process.exit(1);
360
+ }
361
+
362
+ function runCache(sub) {
363
+ stepLine();
364
+ if (sub === "clear") {
365
+ if (DRY) { step(`Would clear: ${CACHE_ROOT}`, S.diamond); return; }
366
+ if (fs.existsSync(CACHE_ROOT)) { const size = getDirSize(CACHE_ROOT); fs.rmSync(CACHE_ROOT, { recursive: true, force: true }); success(`Cache cleared (${formatBytes(size)})`); }
367
+ else step("Cache already empty", S.diamond);
368
+ return;
369
+ }
370
+ if (sub === "info" || !sub) {
371
+ if (!fs.existsSync(CACHE_ROOT)) { step("Cache is empty", S.diamond); return; }
372
+ const rs = fs.existsSync(REGISTRY_CACHE) ? getDirSize(REGISTRY_CACHE) : 0;
373
+ const bs = fs.existsSync(BACKUP_DIR) ? getDirSize(BACKUP_DIR) : 0;
374
+ step(c.bold("Cache Info"), S.diamondFilled, "cyan");
375
+ console.log(` ${c.gray(S.branch)} Location: ${CACHE_ROOT}`);
376
+ console.log(` ${c.gray(S.branch)} Registries: ${formatBytes(rs)}`);
377
+ console.log(` ${c.gray(S.branch)} Backups: ${formatBytes(bs)}`);
378
+ console.log(` ${c.gray(S.branch)} Total: ${formatBytes(getDirSize(CACHE_ROOT))}`);
379
+ stepLine(); return;
380
+ }
381
+ if (sub === "backups") {
382
+ const backups = listBackups();
383
+ if (backups.length === 0) { step("No backups found", S.diamond); return; }
384
+ step(c.bold("Backups"), S.diamondFilled, "cyan"); stepLine();
385
+ backups.forEach(b => console.log(` ${c.gray(S.branch)} ${b.name} (${formatBytes(b.size)})`));
386
+ stepLine(); return;
387
+ }
388
+ fatal(`Unknown cache subcommand: ${sub}`);
389
+ }
390
+
391
+ function runValidate(skillName) {
392
+ const scope = resolveScope();
393
+ let skillsToValidate = [];
394
+ if (skillName) { const sd = path.join(scope, skillName); if (!fs.existsSync(sd)) fatal(`Skill not found: ${skillName}`); skillsToValidate = [{ name: skillName, path: sd }]; }
395
+ else skillsToValidate = getInstalledSkills();
396
+ if (skillsToValidate.length === 0) { stepLine(); step("No skills to validate", S.diamond); return; }
397
+ stepLine(); step(c.bold("Antigravity Validation"), S.diamondFilled, "cyan"); stepLine();
398
+ let totalErrors = 0, totalWarnings = 0;
399
+ for (const skill of skillsToValidate) {
400
+ const errors = [], warnings = [];
401
+ const smp = path.join(skill.path, "SKILL.md");
402
+ if (!fs.existsSync(smp)) errors.push("Missing SKILL.md");
403
+ else { const m = parseSkillMdFrontmatter(smp); if (!m.description) errors.push("Missing description"); else if (m.description.length < 50) warnings.push("Description too short"); }
404
+ const status = errors.length > 0 ? c.red("FAIL") : warnings.length > 0 ? c.yellow("WARN") : c.green("PASS");
405
+ console.log(` ${c.gray(S.branch)} ${status} ${c.bold(skill.name)}`);
406
+ if (VERBOSE || errors.length || warnings.length) { errors.forEach(e => console.log(` ${c.gray(S.branch)} ${c.red("ERROR: " + e)}`)); warnings.forEach(w => console.log(` ${c.gray(S.branch)} ${c.yellow("WARN: " + w)}`)); }
407
+ totalErrors += errors.length; totalWarnings += warnings.length;
408
+ }
409
+ stepLine(); console.log(` ${c.gray(S.branch)} Total: ${skillsToValidate.length}, Errors: ${totalErrors}, Warnings: ${totalWarnings}`); stepLine();
410
+ if (STRICT && totalErrors > 0) process.exit(1);
411
+ }
412
+
413
+ function runAnalyze(skillName) {
414
+ if (!skillName) fatal("Missing skill name");
415
+ const scope = resolveScope(), skillDir = path.join(scope, skillName);
416
+ if (!fs.existsSync(skillDir)) fatal(`Skill not found: ${skillName}`);
417
+ stepLine(); step(c.bold(`Skill Analysis: ${skillName}`), S.diamondFilled, "cyan");
418
+ console.log(` ${c.gray(S.branch)} ${c.dim("Path: " + skillDir)}`); stepLine();
419
+ const smp = path.join(skillDir, "SKILL.md");
420
+ if (fs.existsSync(smp)) {
421
+ const m = parseSkillMdFrontmatter(smp);
422
+ console.log(` ${c.gray(S.branch)} ${c.cyan("SKILL.md Frontmatter:")}`);
423
+ console.log(` ${c.gray(S.branch)} Name: ${m.name || c.dim("(not set)")}`);
424
+ console.log(` ${c.gray(S.branch)} Description: ${m.description ? m.description.substring(0, 60) : c.red("(MISSING)")}`);
425
+ if (m.tags) console.log(` ${c.gray(S.branch)} Tags: ${m.tags.join(", ")}`);
426
+ stepLine();
427
+ }
428
+ const structure = detectSkillStructure(skillDir);
429
+ console.log(` ${c.gray(S.branch)} ${c.cyan("Structure:")}`);
430
+ const items = [["resources", structure.hasResources], ["examples", structure.hasExamples], ["scripts", structure.hasScripts], ["constitution", structure.hasConstitution], ["doctrines", structure.hasDoctrines]];
431
+ items.forEach(([n, has]) => console.log(` ${c.gray(S.branch)} ${has ? c.green(S.check) : c.dim("○")} ${has ? c.bold(n) : c.dim(n)}`));
432
+ stepLine();
433
+ let score = 0;
434
+ if (fs.existsSync(smp)) score += 20;
435
+ const m = parseSkillMdFrontmatter(smp);
436
+ if (m.description) score += 25;
437
+ if (m.tags && m.tags.length > 0) score += 10;
438
+ if (structure.hasResources || structure.hasExamples || structure.hasScripts) score += 20;
439
+ if (fs.existsSync(path.join(skillDir, ".skill-source.json"))) score += 10;
440
+ if (structure.hasConstitution || structure.hasDoctrines) score += 15;
441
+ const scoreColor = score >= 80 ? c.green : score >= 50 ? c.yellow : c.red;
442
+ console.log(` ${c.gray(S.branch)} ${c.cyan("Antigravity Score:")} ${scoreColor(score + "/100")}`);
443
+ stepLine();
444
+ }
445
+
446
+ function runInfo(name) {
447
+ if (!name) fatal("Missing skill name");
448
+ const scope = resolveScope(), localDir = path.join(scope, name);
449
+ stepLine();
450
+ if (fs.existsSync(localDir)) {
451
+ step(`${c.bold(name)} ${c.green("(installed)")}`, S.diamondFilled, "cyan");
452
+ console.log(` ${c.gray(S.branch)} ${c.dim("Path: " + localDir)}`);
453
+ const mf = path.join(localDir, ".skill-source.json");
454
+ if (fs.existsSync(mf)) { const m = JSON.parse(fs.readFileSync(mf, "utf-8")); console.log(` ${c.gray(S.branch)} Repo: ${m.repo || "local"}`); console.log(` ${c.gray(S.branch)} Installed: ${formatDate(m.installedAt)}`); }
455
+ stepLine(); return;
456
+ }
457
+ step(`Skill not installed: ${name}`, S.diamond, "yellow"); stepLine();
458
+ }
459
+
460
+ async function runHelp() {
461
+ console.log(`
462
+ ${c.bold("add-skill")} ${c.dim("v" + pkg.version)}
463
+
464
+ ${c.bold("Usage")}
465
+ $ add-skill <command> [options]
466
+
467
+ ${c.bold("Commands")}
468
+ <org/repo> Install all skills from repository
469
+ <org/repo#skill> Install specific skill
470
+ list List installed skills
471
+ uninstall <skill> Remove a skill
472
+ update <skill> Update a skill
473
+ verify Verify checksums
474
+ doctor Check health
475
+ lock Generate skill-lock.json
476
+ init Initialize skills directory
477
+ validate [skill] Validate against Antigravity spec
478
+ analyze <skill> Analyze skill structure
479
+ cache [info|clear] Manage cache
480
+
481
+ ${c.bold("Options")}
482
+ --global, -g Use global scope
483
+ --force, -f Force operation
484
+ --strict Fail on violations
485
+ --fix Auto-fix issues
486
+ --dry-run Preview only
487
+ --verbose, -v Detailed output
488
+ --json JSON output
489
+ `);
490
+ }
491
+
492
+ // --- MAIN ---
493
+ async function main() {
494
+ if (command === "list" || command === "ls") await runList();
495
+ else if (command === "init") await runInit();
496
+ else if (command === "install" || command === "add" || command === "i") await runInstall(params[0]);
497
+ else if (command === "uninstall" || command === "remove" || command === "rm") await runUninstall(params[0]);
498
+ else if (command === "update") await runUpdate(params[0]);
499
+ else if (command === "lock") runLock();
500
+ else if (command === "verify") runVerify();
501
+ else if (command === "doctor") runDoctor();
502
+ else if (command === "cache") runCache(params[0]);
503
+ else if (command === "validate" || command === "check") runValidate(params[0]);
504
+ else if (command === "analyze") runAnalyze(params[0]);
505
+ else if (command === "info" || command === "show") runInfo(params[0]);
506
+ else if (command === "help" || command === "--help" || command === "-h") await runHelp();
507
+ else if (command === "--version" || command === "-V") console.log(pkg.version);
508
+ else if (command.includes("/")) await runInstall(command);
509
+ else { console.log(`Unknown command: ${command}`); await runHelp(); }
510
+ }
511
+
512
+ main().catch(err => { console.error(c.red("\nError: " + err.message)); process.exit(1); });
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @fileoverview Analyze command - Skill structure analysis
3
+ */
4
+
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { resolveScope, getDirSize } from "../helpers.js";
8
+ import { parseSkillMdFrontmatter, detectSkillStructure } from "../skills.js";
9
+ import { step, stepLine, S, c, fatal } from "../ui.js";
10
+
11
+ /**
12
+ * Analyze skill structure
13
+ * @param {string} skillName - Skill to analyze
14
+ */
15
+ export async function run(skillName) {
16
+ if (!skillName) fatal("Missing skill name");
17
+
18
+ const scope = resolveScope();
19
+ const skillDir = path.join(scope, skillName);
20
+
21
+ if (!fs.existsSync(skillDir)) fatal(`Skill not found: ${skillName}`);
22
+
23
+ stepLine();
24
+ step(c.bold(`Skill Analysis: ${skillName}`), S.diamondFilled, "cyan");
25
+ console.log(` ${c.gray(S.branch)} ${c.dim("Path: " + skillDir)}`);
26
+ stepLine();
27
+
28
+ // SKILL.md frontmatter
29
+ const smp = path.join(skillDir, "SKILL.md");
30
+ if (fs.existsSync(smp)) {
31
+ const m = parseSkillMdFrontmatter(smp);
32
+ console.log(` ${c.gray(S.branch)} ${c.cyan("SKILL.md Frontmatter:")}`);
33
+ console.log(` ${c.gray(S.branch)} Name: ${m.name || c.dim("(not set)")}`);
34
+ console.log(` ${c.gray(S.branch)} Description: ${m.description ? m.description.substring(0, 60) : c.red("(MISSING)")}`);
35
+ if (m.tags) console.log(` ${c.gray(S.branch)} Tags: ${m.tags.join(", ")}`);
36
+ stepLine();
37
+ }
38
+
39
+ // Structure
40
+ const structure = detectSkillStructure(skillDir);
41
+ console.log(` ${c.gray(S.branch)} ${c.cyan("Structure:")}`);
42
+
43
+ const items = [
44
+ ["resources", structure.hasResources],
45
+ ["examples", structure.hasExamples],
46
+ ["scripts", structure.hasScripts],
47
+ ["constitution", structure.hasConstitution],
48
+ ["doctrines", structure.hasDoctrines]
49
+ ];
50
+
51
+ items.forEach(([n, has]) => {
52
+ console.log(` ${c.gray(S.branch)} ${has ? c.green(S.check) : c.dim("○")} ${has ? c.bold(n) : c.dim(n)}`);
53
+ });
54
+
55
+ stepLine();
56
+
57
+ // Antigravity Score
58
+ let score = 0;
59
+ if (fs.existsSync(smp)) score += 20;
60
+ const m = parseSkillMdFrontmatter(smp);
61
+ if (m.description) score += 25;
62
+ if (m.tags && m.tags.length > 0) score += 10;
63
+ if (structure.hasResources || structure.hasExamples || structure.hasScripts) score += 20;
64
+ if (fs.existsSync(path.join(skillDir, ".skill-source.json"))) score += 10;
65
+ if (structure.hasConstitution || structure.hasDoctrines) score += 15;
66
+
67
+ const scoreColor = score >= 80 ? c.green : score >= 50 ? c.yellow : c.red;
68
+ console.log(` ${c.gray(S.branch)} ${c.cyan("Antigravity Score:")} ${scoreColor(score + "/100")}`);
69
+ stepLine();
70
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @fileoverview Cache command
3
+ */
4
+
5
+ import fs from "fs";
6
+ import { step, stepLine, S, c, fatal, success } from "../ui.js";
7
+ import { getDirSize, formatBytes, listBackups } from "../helpers.js";
8
+ import { CACHE_ROOT, REGISTRY_CACHE, BACKUP_DIR, DRY } from "../config.js";
9
+
10
+ /**
11
+ * Manage cache
12
+ * @param {string} [sub] - Subcommand: info, clear, backups
13
+ */
14
+ export async function run(sub) {
15
+ stepLine();
16
+
17
+ if (sub === "clear") {
18
+ if (DRY) {
19
+ step(`Would clear: ${CACHE_ROOT}`, S.diamond);
20
+ return;
21
+ }
22
+ if (fs.existsSync(CACHE_ROOT)) {
23
+ const size = getDirSize(CACHE_ROOT);
24
+ fs.rmSync(CACHE_ROOT, { recursive: true, force: true });
25
+ success(`Cache cleared (${formatBytes(size)})`);
26
+ } else {
27
+ step("Cache already empty", S.diamond);
28
+ }
29
+ return;
30
+ }
31
+
32
+ if (sub === "info" || !sub) {
33
+ if (!fs.existsSync(CACHE_ROOT)) {
34
+ step("Cache is empty", S.diamond);
35
+ return;
36
+ }
37
+
38
+ const rs = fs.existsSync(REGISTRY_CACHE) ? getDirSize(REGISTRY_CACHE) : 0;
39
+ const bs = fs.existsSync(BACKUP_DIR) ? getDirSize(BACKUP_DIR) : 0;
40
+
41
+ step(c.bold("Cache Info"), S.diamondFilled, "cyan");
42
+ console.log(` ${c.gray(S.branch)} Location: ${CACHE_ROOT}`);
43
+ console.log(` ${c.gray(S.branch)} Registries: ${formatBytes(rs)}`);
44
+ console.log(` ${c.gray(S.branch)} Backups: ${formatBytes(bs)}`);
45
+ console.log(` ${c.gray(S.branch)} Total: ${formatBytes(getDirSize(CACHE_ROOT))}`);
46
+ stepLine();
47
+ return;
48
+ }
49
+
50
+ if (sub === "backups") {
51
+ const backups = listBackups();
52
+ if (backups.length === 0) {
53
+ step("No backups found", S.diamond);
54
+ return;
55
+ }
56
+
57
+ step(c.bold("Backups"), S.diamondFilled, "cyan");
58
+ stepLine();
59
+ backups.forEach(b => console.log(` ${c.gray(S.branch)} ${b.name} (${formatBytes(b.size)})`));
60
+ stepLine();
61
+ return;
62
+ }
63
+
64
+ fatal(`Unknown cache subcommand: ${sub}`);
65
+ }