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,889 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @dataguruin/add-skill
4
+ * Vercel-Style CLI - Exact Match with @clack/prompts
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 * as p from "@clack/prompts";
13
+ import chalk from "chalk";
14
+ import { createRequire } from "module";
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const pkg = (() => {
18
+ try { return require("../package.json"); } catch { return { version: "4.1.0" }; }
19
+ })();
20
+
21
+ // --- CONFIG & CONSTANTS ---
22
+ const cwd = process.cwd();
23
+ const WORKSPACE = path.join(cwd, ".agent", "skills");
24
+ const GLOBAL_DIR = path.join(os.homedir(), ".gemini", "antigravity", "skills");
25
+ const CACHE_ROOT = process.env.ADD_SKILL_CACHE_DIR || path.join(os.homedir(), ".cache", "add-skill");
26
+ const BACKUP_DIR = path.join(CACHE_ROOT, "backups");
27
+
28
+ // Args
29
+ const args = process.argv.slice(2);
30
+ const command = args[0] || "help";
31
+ const flags = new Set(args.filter((a) => a.startsWith("--")));
32
+ const params = args.filter((a) => !a.startsWith("--")).slice(1);
33
+
34
+ const GLOBAL = flags.has("--global") || flags.has("-g");
35
+ const VERBOSE = flags.has("--verbose") || flags.has("-v");
36
+ const JSON_OUTPUT = flags.has("--json");
37
+ const FORCE = flags.has("--force") || flags.has("-f");
38
+ const DRY = flags.has("--dry-run");
39
+ const STRICT = flags.has("--strict");
40
+ const FIX = flags.has("--fix");
41
+ const YES = flags.has("--yes") || flags.has("-y");
42
+
43
+ // --- Helper Functions ---
44
+ function resolveScope() {
45
+ if (GLOBAL) return GLOBAL_DIR;
46
+ if (fs.existsSync(path.join(cwd, ".agent"))) return WORKSPACE;
47
+ return GLOBAL_DIR;
48
+ }
49
+
50
+ function getDirSize(dir) {
51
+ let size = 0;
52
+ try {
53
+ const walk = (p) => {
54
+ for (const f of fs.readdirSync(p)) {
55
+ const full = path.join(p, f);
56
+ const stat = fs.statSync(full);
57
+ if (stat.isDirectory()) walk(full);
58
+ else size += stat.size;
59
+ }
60
+ };
61
+ walk(dir);
62
+ } catch { }
63
+ return size;
64
+ }
65
+
66
+ function formatBytes(bytes) {
67
+ if (bytes < 1024) return bytes + " B";
68
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
69
+ return (bytes / 1024 / 1024).toFixed(1) + " MB";
70
+ }
71
+
72
+ function formatDate(iso) { return iso ? new Date(iso).toLocaleDateString() : "unknown"; }
73
+
74
+ function shortenPath(fullPath, fromCwd = cwd) {
75
+ const home = os.homedir();
76
+ if (fullPath.startsWith(home)) return fullPath.replace(home, "~");
77
+ if (fullPath.startsWith(fromCwd)) return "." + fullPath.slice(fromCwd.length);
78
+ return fullPath;
79
+ }
80
+
81
+ function parseSkillMdFrontmatter(filePath) {
82
+ try {
83
+ const content = fs.readFileSync(filePath, "utf-8");
84
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
85
+ if (!match) return {};
86
+ const meta = {};
87
+ for (const line of match[1].split(/\r?\n/)) {
88
+ const i = line.indexOf(":");
89
+ if (i === -1) continue;
90
+ const key = line.substring(0, i).trim();
91
+ const val = line.substring(i + 1).trim();
92
+ if (key === "tags") meta[key] = val.split(",").map(t => t.trim()).filter(Boolean);
93
+ else if (key && val) meta[key] = val;
94
+ }
95
+ return meta;
96
+ } catch { return {}; }
97
+ }
98
+
99
+ function detectSkillStructure(dir) {
100
+ const s = { hasResources: false, hasExamples: false, hasScripts: false, hasConstitution: false, hasDoctrines: false, hasEnforcement: false, hasProposals: false, directories: [], files: [] };
101
+ try {
102
+ for (const item of fs.readdirSync(dir)) {
103
+ const full = path.join(dir, item);
104
+ if (fs.statSync(full).isDirectory()) {
105
+ s.directories.push(item);
106
+ const l = item.toLowerCase();
107
+ if (l === "resources") s.hasResources = true;
108
+ if (l === "examples") s.hasExamples = true;
109
+ if (l === "scripts") s.hasScripts = true;
110
+ if (l === "constitution") s.hasConstitution = true;
111
+ if (l === "doctrines") s.hasDoctrines = true;
112
+ if (l === "enforcement") s.hasEnforcement = true;
113
+ if (l === "proposals") s.hasProposals = true;
114
+ } else s.files.push(item);
115
+ }
116
+ } catch { }
117
+ return s;
118
+ }
119
+
120
+ function getInstalledSkills() {
121
+ const scope = resolveScope();
122
+ const skills = [];
123
+ if (!fs.existsSync(scope)) return skills;
124
+ for (const name of fs.readdirSync(scope)) {
125
+ const dir = path.join(scope, name);
126
+ if (!fs.statSync(dir).isDirectory()) continue;
127
+ const metaFile = path.join(dir, ".skill-source.json");
128
+ const skillFile = path.join(dir, "SKILL.md");
129
+ if (fs.existsSync(metaFile) || fs.existsSync(skillFile)) {
130
+ const meta = fs.existsSync(metaFile) ? JSON.parse(fs.readFileSync(metaFile, "utf-8")) : {};
131
+ const hasSkillMd = fs.existsSync(skillFile);
132
+ const skillMeta = hasSkillMd ? parseSkillMdFrontmatter(skillFile) : {};
133
+ 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) });
134
+ }
135
+ }
136
+ return skills;
137
+ }
138
+
139
+ function merkleHash(dir) {
140
+ const files = [];
141
+ const walk = (p) => {
142
+ for (const f of fs.readdirSync(p)) {
143
+ if (f === ".skill-source.json") continue;
144
+ const full = path.join(p, f);
145
+ const stat = fs.statSync(full);
146
+ if (stat.isDirectory()) walk(full);
147
+ else { const h = crypto.createHash("sha256").update(fs.readFileSync(full)).digest("hex"); files.push(`${path.relative(dir, full)}:${h}`); }
148
+ }
149
+ };
150
+ walk(dir);
151
+ files.sort();
152
+ return crypto.createHash("sha256").update(files.join("|")).digest("hex");
153
+ }
154
+
155
+ function parseSkillSpec(spec) {
156
+ const [repoPart, skillPart] = spec.split("#");
157
+ const [org, repo] = repoPart.split("/");
158
+ const [skill, ref] = (skillPart || "").split("@");
159
+ return { org, repo, skill, ref };
160
+ }
161
+
162
+ function createBackup(skillDir, skillName) {
163
+ if (DRY) return null;
164
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
165
+ const bp = path.join(BACKUP_DIR, `${skillName}_${ts}`);
166
+ fs.mkdirSync(BACKUP_DIR, { recursive: true });
167
+ fs.cpSync(skillDir, bp, { recursive: true });
168
+ return bp;
169
+ }
170
+
171
+ function listBackups(skillName = null) {
172
+ if (!fs.existsSync(BACKUP_DIR)) return [];
173
+ const backups = [];
174
+ for (const name of fs.readdirSync(BACKUP_DIR)) {
175
+ if (skillName && !name.startsWith(skillName + "_")) continue;
176
+ const bp = path.join(BACKUP_DIR, name);
177
+ backups.push({ name, path: bp, createdAt: fs.statSync(bp).mtime, size: getDirSize(bp) });
178
+ }
179
+ return backups.sort((a, b) => b.createdAt - a.createdAt);
180
+ }
181
+
182
+ // --- COMMANDS ---
183
+
184
+ async function runInit() {
185
+ console.log();
186
+ p.intro(chalk.bgCyan.black(" add-skill "));
187
+
188
+ const targetDir = GLOBAL ? GLOBAL_DIR : WORKSPACE;
189
+ if (fs.existsSync(targetDir)) {
190
+ p.log.success(`Skills directory already exists: ${shortenPath(targetDir)}`);
191
+ p.outro(chalk.green("Done!"));
192
+ return;
193
+ }
194
+
195
+ if (DRY) {
196
+ p.log.info(`Would create: ${shortenPath(targetDir)}`);
197
+ p.outro(chalk.dim("Dry run complete"));
198
+ return;
199
+ }
200
+
201
+ fs.mkdirSync(targetDir, { recursive: true });
202
+ if (!GLOBAL) {
203
+ const gi = path.join(cwd, ".agent", ".gitignore");
204
+ if (!fs.existsSync(gi)) fs.writeFileSync(gi, "# Skill caches\nskills/*/.skill-source.json\n");
205
+ }
206
+
207
+ p.log.success(`Initialized skills directory: ${shortenPath(targetDir)}`);
208
+ p.outro(chalk.green("Done!"));
209
+ }
210
+
211
+ async function runList() {
212
+ console.log();
213
+ p.intro(chalk.bgCyan.black(" add-skill "));
214
+
215
+ const skills = getInstalledSkills();
216
+ const scope = resolveScope();
217
+
218
+ p.log.info(`Location: ${chalk.dim(shortenPath(scope))}`);
219
+
220
+ if (skills.length === 0) {
221
+ p.log.message(chalk.dim("No skills installed."));
222
+ p.outro(chalk.dim("Run add-skill <org/repo> to install skills"));
223
+ return;
224
+ }
225
+
226
+ if (JSON_OUTPUT) {
227
+ console.log(JSON.stringify({ skills }, null, 2));
228
+ return;
229
+ }
230
+
231
+ const lines = [];
232
+ for (const s of skills) {
233
+ const icon = s.hasSkillMd ? chalk.green("✓") : chalk.yellow("○");
234
+ lines.push(`${icon} ${chalk.bold(s.name)} ${chalk.dim("v" + s.version)} ${chalk.dim("(" + formatBytes(s.size) + ")")}`);
235
+ if (s.description && VERBOSE) {
236
+ lines.push(` ${chalk.dim(s.description.substring(0, 60))}`);
237
+ }
238
+ }
239
+
240
+ p.note(lines.join("\n"), `Installed Skills (${skills.length})`);
241
+ p.outro(chalk.green("Done!"));
242
+ }
243
+
244
+ async function runInstall(spec) {
245
+ console.log();
246
+ p.intro(chalk.bgCyan.black(" skills "));
247
+
248
+ if (!spec) {
249
+ p.log.error("Missing skill spec. Usage: add-skill <org/repo>");
250
+ p.outro(chalk.red("Installation failed"));
251
+ process.exit(1);
252
+ }
253
+
254
+ const { org, repo, skill: singleSkill, ref } = parseSkillSpec(spec);
255
+ if (!org || !repo) {
256
+ p.log.error("Invalid spec. Format: org/repo or org/repo#skill");
257
+ p.outro(chalk.red("Installation failed"));
258
+ process.exit(1);
259
+ }
260
+
261
+ const url = `https://github.com/${org}/${repo}.git`;
262
+ const spinner = p.spinner();
263
+
264
+ spinner.start("Cloning repository");
265
+
266
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "add-skill-"));
267
+ try {
268
+ execSync(`git clone --depth=1 ${url} "${tmp}"`, { stdio: "pipe" });
269
+ if (ref) execSync(`git -C "${tmp}" checkout ${ref}`, { stdio: "pipe" });
270
+ } catch {
271
+ spinner.stop(chalk.red("Failed to clone repository"));
272
+ p.outro(chalk.red("Installation failed"));
273
+ fs.rmSync(tmp, { recursive: true, force: true });
274
+ process.exit(1);
275
+ }
276
+
277
+ spinner.stop(`Source: ${chalk.cyan(url)}`);
278
+
279
+ // Find skills in repo
280
+ const skillsInRepo = [];
281
+ for (const e of fs.readdirSync(tmp)) {
282
+ const sp = path.join(tmp, e);
283
+ if (fs.statSync(sp).isDirectory() && fs.existsSync(path.join(sp, "SKILL.md"))) {
284
+ const m = parseSkillMdFrontmatter(path.join(sp, "SKILL.md"));
285
+ skillsInRepo.push({
286
+ value: e,
287
+ label: e,
288
+ hint: m.description ? m.description.substring(0, 50) + "..." : undefined
289
+ });
290
+ }
291
+ }
292
+
293
+ if (skillsInRepo.length === 0) {
294
+ p.log.warn("No valid skills found in repository");
295
+ p.outro(chalk.yellow("No skills to install"));
296
+ fs.rmSync(tmp, { recursive: true, force: true });
297
+ return;
298
+ }
299
+
300
+ p.log.success(`Repository cloned`);
301
+ p.log.info(`Found ${skillsInRepo.length} skills`);
302
+
303
+ // Select skills
304
+ let selectedSkills;
305
+ if (singleSkill) {
306
+ selectedSkills = [singleSkill];
307
+ } else if (YES) {
308
+ selectedSkills = skillsInRepo.map(s => s.value);
309
+ } else {
310
+ const selected = await p.multiselect({
311
+ message: "Select skills to install",
312
+ options: skillsInRepo,
313
+ required: true,
314
+ initialValues: skillsInRepo.map(s => s.value)
315
+ });
316
+
317
+ if (p.isCancel(selected)) {
318
+ p.cancel("Installation cancelled");
319
+ fs.rmSync(tmp, { recursive: true, force: true });
320
+ process.exit(0);
321
+ }
322
+ selectedSkills = selected;
323
+ }
324
+
325
+ // Select scope
326
+ let installGlobally = GLOBAL;
327
+ if (!GLOBAL && !YES) {
328
+ const scope = await p.select({
329
+ message: "Installation scope",
330
+ options: [
331
+ { value: false, label: "Project", hint: "Install in current directory" },
332
+ { value: true, label: "Global", hint: "Install in home directory" }
333
+ ]
334
+ });
335
+
336
+ if (p.isCancel(scope)) {
337
+ p.cancel("Installation cancelled");
338
+ fs.rmSync(tmp, { recursive: true, force: true });
339
+ process.exit(0);
340
+ }
341
+ installGlobally = scope;
342
+ }
343
+
344
+ const targetScope = installGlobally ? GLOBAL_DIR : WORKSPACE;
345
+
346
+ // Show summary
347
+ const summaryLines = [];
348
+ for (const sn of selectedSkills) {
349
+ const shortPath = shortenPath(path.join(targetScope, sn));
350
+ summaryLines.push(`${chalk.cyan(sn)}`);
351
+ summaryLines.push(` ${chalk.dim("→")} ${shortPath}`);
352
+ }
353
+
354
+ p.note(summaryLines.join("\n"), "Installation Summary");
355
+
356
+ // Confirm
357
+ if (!YES) {
358
+ const confirmed = await p.confirm({
359
+ message: "Proceed with installation?"
360
+ });
361
+
362
+ if (p.isCancel(confirmed) || !confirmed) {
363
+ p.cancel("Installation cancelled");
364
+ fs.rmSync(tmp, { recursive: true, force: true });
365
+ process.exit(0);
366
+ }
367
+ }
368
+
369
+ // Install
370
+ spinner.start("Installing skills...");
371
+ fs.mkdirSync(targetScope, { recursive: true });
372
+
373
+ const installed = [];
374
+ for (const sn of selectedSkills) {
375
+ const src = path.join(tmp, sn);
376
+ const dest = path.join(targetScope, sn);
377
+ if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
378
+ fs.cpSync(src, dest, { recursive: true });
379
+ const hash = merkleHash(dest);
380
+ 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));
381
+ installed.push(sn);
382
+ }
383
+
384
+ spinner.stop("Installation complete");
385
+ fs.rmSync(tmp, { recursive: true, force: true });
386
+
387
+ // Show result
388
+ const resultLines = installed.map(s => `${chalk.green("✓")} ${s}`);
389
+ p.note(resultLines.join("\n"), chalk.green(`Installed ${installed.length} skill${installed.length !== 1 ? "s" : ""}`));
390
+
391
+ console.log();
392
+ p.outro(chalk.green("Done!"));
393
+ }
394
+
395
+ async function runUninstall(skillName) {
396
+ console.log();
397
+ p.intro(chalk.bgCyan.black(" add-skill "));
398
+
399
+ if (!skillName) {
400
+ p.log.error("Missing skill name");
401
+ p.outro(chalk.red("Uninstall failed"));
402
+ process.exit(1);
403
+ }
404
+
405
+ const scope = resolveScope();
406
+ const targetDir = path.join(scope, skillName);
407
+
408
+ if (!fs.existsSync(targetDir)) {
409
+ p.log.error(`Skill not found: ${skillName}`);
410
+ p.outro(chalk.red("Uninstall failed"));
411
+ process.exit(1);
412
+ }
413
+
414
+ if (!YES) {
415
+ const confirmed = await p.confirm({
416
+ message: `Remove skill "${skillName}"?`,
417
+ initialValue: false
418
+ });
419
+
420
+ if (p.isCancel(confirmed) || !confirmed) {
421
+ p.cancel("Uninstall cancelled");
422
+ process.exit(0);
423
+ }
424
+ }
425
+
426
+ if (DRY) {
427
+ p.log.info(`Would remove: ${shortenPath(targetDir)}`);
428
+ p.outro(chalk.dim("Dry run complete"));
429
+ return;
430
+ }
431
+
432
+ createBackup(targetDir, skillName);
433
+ fs.rmSync(targetDir, { recursive: true, force: true });
434
+
435
+ p.log.success(`Removed: ${skillName}`);
436
+ p.outro(chalk.green("Done!"));
437
+ }
438
+
439
+ async function runUpdate(skillName) {
440
+ console.log();
441
+ p.intro(chalk.bgCyan.black(" add-skill "));
442
+
443
+ if (!skillName) {
444
+ p.log.error("Missing skill name");
445
+ p.outro(chalk.red("Update failed"));
446
+ process.exit(1);
447
+ }
448
+
449
+ const scope = resolveScope();
450
+ const targetDir = path.join(scope, skillName);
451
+
452
+ if (!fs.existsSync(targetDir)) {
453
+ p.log.error(`Skill not found: ${skillName}`);
454
+ p.outro(chalk.red("Update failed"));
455
+ process.exit(1);
456
+ }
457
+
458
+ const metaFile = path.join(targetDir, ".skill-source.json");
459
+ if (!fs.existsSync(metaFile)) {
460
+ p.log.error("Skill metadata not found");
461
+ p.outro(chalk.red("Update failed"));
462
+ process.exit(1);
463
+ }
464
+
465
+ const meta = JSON.parse(fs.readFileSync(metaFile, "utf-8"));
466
+ if (!meta.repo || meta.repo === "local") {
467
+ p.log.error("Cannot update local skill");
468
+ p.outro(chalk.red("Update failed"));
469
+ process.exit(1);
470
+ }
471
+
472
+ const spinner = p.spinner();
473
+ spinner.start(`Updating ${skillName}...`);
474
+
475
+ try {
476
+ if (!DRY) {
477
+ createBackup(targetDir, skillName);
478
+ fs.rmSync(targetDir, { recursive: true, force: true });
479
+ }
480
+ const spec = `${meta.repo}#${meta.skill}${meta.ref ? "@" + meta.ref : ""}`;
481
+ spinner.stop(`Updated: ${skillName}`);
482
+ if (!DRY) await runInstall(spec);
483
+ } catch (err) {
484
+ spinner.stop(chalk.red(`Failed: ${err.message}`));
485
+ p.outro(chalk.red("Update failed"));
486
+ }
487
+ }
488
+
489
+ function runLock() {
490
+ console.log();
491
+ p.intro(chalk.bgCyan.black(" add-skill "));
492
+
493
+ if (!fs.existsSync(WORKSPACE)) {
494
+ p.log.error("No .agent/skills directory");
495
+ p.outro(chalk.red("Lock failed"));
496
+ process.exit(1);
497
+ }
498
+
499
+ const skills = {};
500
+ for (const name of fs.readdirSync(WORKSPACE)) {
501
+ const dir = path.join(WORKSPACE, name);
502
+ if (!fs.statSync(dir).isDirectory()) continue;
503
+ const mf = path.join(dir, ".skill-source.json");
504
+ if (!fs.existsSync(mf)) continue;
505
+ const m = JSON.parse(fs.readFileSync(mf, "utf-8"));
506
+ skills[name] = { repo: m.repo, skill: m.skill, ref: m.ref, checksum: `sha256:${m.checksum}`, publisher: m.publisher || null };
507
+ }
508
+
509
+ const lock = { lockVersion: 1, generatedAt: new Date().toISOString(), generator: `@dataguruin/add-skill@${pkg.version}`, skills };
510
+
511
+ if (DRY) {
512
+ p.log.info("Would generate skill-lock.json");
513
+ console.log(JSON.stringify(lock, null, 2));
514
+ p.outro(chalk.dim("Dry run complete"));
515
+ return;
516
+ }
517
+
518
+ fs.mkdirSync(path.join(cwd, ".agent"), { recursive: true });
519
+ fs.writeFileSync(path.join(cwd, ".agent", "skill-lock.json"), JSON.stringify(lock, null, 2));
520
+
521
+ p.log.success("Generated skill-lock.json");
522
+ p.outro(chalk.green("Done!"));
523
+ }
524
+
525
+ function runVerify() {
526
+ console.log();
527
+ p.intro(chalk.bgCyan.black(" add-skill "));
528
+
529
+ const scope = resolveScope();
530
+ if (!fs.existsSync(scope)) {
531
+ p.log.warn("No skills directory found");
532
+ p.outro(chalk.dim("Nothing to verify"));
533
+ return;
534
+ }
535
+
536
+ p.log.step("Verifying skills...");
537
+
538
+ let issues = 0;
539
+ const results = [];
540
+
541
+ for (const name of fs.readdirSync(scope)) {
542
+ const dir = path.join(scope, name);
543
+ if (!fs.statSync(dir).isDirectory()) continue;
544
+ const mf = path.join(dir, ".skill-source.json");
545
+ if (!fs.existsSync(mf)) {
546
+ results.push(`${chalk.red("✗")} ${name}: ${chalk.red("missing metadata")}`);
547
+ issues++;
548
+ continue;
549
+ }
550
+ const m = JSON.parse(fs.readFileSync(mf, "utf-8"));
551
+ const actual = merkleHash(dir);
552
+ if (actual !== m.checksum) {
553
+ results.push(`${chalk.red("✗")} ${name}: ${chalk.red("checksum mismatch")}`);
554
+ issues++;
555
+ } else {
556
+ results.push(`${chalk.green("✓")} ${name}: ${chalk.green("OK")}`);
557
+ }
558
+ }
559
+
560
+ p.note(results.join("\n"), "Verification Results");
561
+
562
+ if (issues > 0) {
563
+ p.log.error(`${issues} issue(s) found`);
564
+ if (STRICT) process.exit(1);
565
+ } else {
566
+ p.log.success("All skills verified");
567
+ }
568
+
569
+ p.outro(chalk.green("Done!"));
570
+ }
571
+
572
+ function runDoctor() {
573
+ console.log();
574
+ p.intro(chalk.bgCyan.black(" add-skill "));
575
+
576
+ const scope = resolveScope();
577
+ if (!fs.existsSync(scope)) {
578
+ p.log.warn("No skills directory found");
579
+ p.outro(chalk.dim("Nothing to check"));
580
+ return;
581
+ }
582
+
583
+ p.log.step("Running health check...");
584
+
585
+ let errors = 0, warnings = 0;
586
+ const results = [];
587
+ const lockFile = path.join(cwd, ".agent", "skill-lock.json");
588
+ const lock = fs.existsSync(lockFile) ? JSON.parse(fs.readFileSync(lockFile, "utf-8")) : null;
589
+
590
+ for (const name of fs.readdirSync(scope)) {
591
+ const dir = path.join(scope, name);
592
+ if (!fs.statSync(dir).isDirectory()) continue;
593
+
594
+ if (!fs.existsSync(path.join(dir, "SKILL.md"))) {
595
+ results.push(`${chalk.red("✗")} ${name}: ${chalk.red("missing SKILL.md")}`);
596
+ errors++;
597
+ continue;
598
+ }
599
+
600
+ const mf = path.join(dir, ".skill-source.json");
601
+ if (!fs.existsSync(mf)) {
602
+ results.push(`${chalk.red("✗")} ${name}: ${chalk.red("missing metadata")}`);
603
+ errors++;
604
+ continue;
605
+ }
606
+
607
+ const m = JSON.parse(fs.readFileSync(mf, "utf-8"));
608
+ const actual = merkleHash(dir);
609
+
610
+ if (actual !== m.checksum) {
611
+ if (FIX && !DRY) {
612
+ m.checksum = actual;
613
+ fs.writeFileSync(mf, JSON.stringify(m, null, 2));
614
+ results.push(`${chalk.yellow("○")} ${name}: ${chalk.yellow("checksum fixed")}`);
615
+ } else {
616
+ results.push(`${chalk.yellow("○")} ${name}: ${chalk.yellow("checksum drift")}`);
617
+ STRICT ? errors++ : warnings++;
618
+ }
619
+ } else if (lock && !lock.skills[name]) {
620
+ results.push(`${chalk.yellow("○")} ${name}: ${chalk.yellow("not in lock")}`);
621
+ STRICT ? errors++ : warnings++;
622
+ } else {
623
+ results.push(`${chalk.green("✓")} ${name}: ${chalk.green("healthy")}`);
624
+ }
625
+ }
626
+
627
+ p.note(results.join("\n"), "Health Check Results");
628
+ p.log.info(`Errors: ${errors}, Warnings: ${warnings}`);
629
+
630
+ if (STRICT && errors) process.exit(1);
631
+ p.outro(chalk.green("Done!"));
632
+ }
633
+
634
+ function runCache(sub) {
635
+ console.log();
636
+ p.intro(chalk.bgCyan.black(" add-skill "));
637
+
638
+ if (sub === "clear") {
639
+ if (DRY) {
640
+ p.log.info(`Would clear: ${shortenPath(CACHE_ROOT)}`);
641
+ p.outro(chalk.dim("Dry run complete"));
642
+ return;
643
+ }
644
+ if (fs.existsSync(CACHE_ROOT)) {
645
+ const size = getDirSize(CACHE_ROOT);
646
+ fs.rmSync(CACHE_ROOT, { recursive: true, force: true });
647
+ p.log.success(`Cache cleared (${formatBytes(size)})`);
648
+ } else {
649
+ p.log.info("Cache already empty");
650
+ }
651
+ p.outro(chalk.green("Done!"));
652
+ return;
653
+ }
654
+
655
+ if (sub === "info" || !sub) {
656
+ if (!fs.existsSync(CACHE_ROOT)) {
657
+ p.log.info("Cache is empty");
658
+ p.outro(chalk.dim("Nothing cached"));
659
+ return;
660
+ }
661
+ const bs = fs.existsSync(BACKUP_DIR) ? getDirSize(BACKUP_DIR) : 0;
662
+ const lines = [
663
+ `Location: ${shortenPath(CACHE_ROOT)}`,
664
+ `Backups: ${formatBytes(bs)}`,
665
+ `Total: ${formatBytes(getDirSize(CACHE_ROOT))}`
666
+ ];
667
+ p.note(lines.join("\n"), "Cache Info");
668
+ p.outro(chalk.green("Done!"));
669
+ return;
670
+ }
671
+
672
+ if (sub === "backups") {
673
+ const backups = listBackups();
674
+ if (backups.length === 0) {
675
+ p.log.info("No backups found");
676
+ p.outro(chalk.dim("Nothing backed up"));
677
+ return;
678
+ }
679
+ const lines = backups.map(b => `${b.name} (${formatBytes(b.size)})`);
680
+ p.note(lines.join("\n"), `Backups (${backups.length})`);
681
+ p.outro(chalk.green("Done!"));
682
+ return;
683
+ }
684
+
685
+ p.log.error(`Unknown cache subcommand: ${sub}`);
686
+ p.outro(chalk.red("Failed"));
687
+ process.exit(1);
688
+ }
689
+
690
+ function runValidate(skillName) {
691
+ console.log();
692
+ p.intro(chalk.bgCyan.black(" add-skill "));
693
+
694
+ const scope = resolveScope();
695
+ let skillsToValidate = [];
696
+
697
+ if (skillName) {
698
+ const sd = path.join(scope, skillName);
699
+ if (!fs.existsSync(sd)) {
700
+ p.log.error(`Skill not found: ${skillName}`);
701
+ p.outro(chalk.red("Validation failed"));
702
+ process.exit(1);
703
+ }
704
+ skillsToValidate = [{ name: skillName, path: sd }];
705
+ } else {
706
+ skillsToValidate = getInstalledSkills();
707
+ }
708
+
709
+ if (skillsToValidate.length === 0) {
710
+ p.log.warn("No skills to validate");
711
+ p.outro(chalk.dim("Nothing to validate"));
712
+ return;
713
+ }
714
+
715
+ p.log.step("Running Antigravity validation...");
716
+
717
+ let totalErrors = 0, totalWarnings = 0;
718
+ const results = [];
719
+
720
+ for (const skill of skillsToValidate) {
721
+ const errors = [], warnings = [];
722
+ const smp = path.join(skill.path, "SKILL.md");
723
+
724
+ if (!fs.existsSync(smp)) {
725
+ errors.push("Missing SKILL.md");
726
+ } else {
727
+ const m = parseSkillMdFrontmatter(smp);
728
+ if (!m.description) errors.push("Missing description");
729
+ else if (m.description.length < 50) warnings.push("Description too short");
730
+ }
731
+
732
+ const status = errors.length > 0 ? chalk.red("FAIL") : warnings.length > 0 ? chalk.yellow("WARN") : chalk.green("PASS");
733
+ results.push(`${status} ${chalk.bold(skill.name)}`);
734
+
735
+ if (VERBOSE || errors.length || warnings.length) {
736
+ errors.forEach(e => results.push(` ${chalk.red("ERROR: " + e)}`));
737
+ warnings.forEach(w => results.push(` ${chalk.yellow("WARN: " + w)}`));
738
+ }
739
+
740
+ totalErrors += errors.length;
741
+ totalWarnings += warnings.length;
742
+ }
743
+
744
+ p.note(results.join("\n"), "Validation Results");
745
+ p.log.info(`Total: ${skillsToValidate.length}, Errors: ${totalErrors}, Warnings: ${totalWarnings}`);
746
+
747
+ if (STRICT && totalErrors > 0) process.exit(1);
748
+ p.outro(chalk.green("Done!"));
749
+ }
750
+
751
+ function runAnalyze(skillName) {
752
+ console.log();
753
+ p.intro(chalk.bgCyan.black(" add-skill "));
754
+
755
+ if (!skillName) {
756
+ p.log.error("Missing skill name");
757
+ p.outro(chalk.red("Analysis failed"));
758
+ process.exit(1);
759
+ }
760
+
761
+ const scope = resolveScope();
762
+ const skillDir = path.join(scope, skillName);
763
+
764
+ if (!fs.existsSync(skillDir)) {
765
+ p.log.error(`Skill not found: ${skillName}`);
766
+ p.outro(chalk.red("Analysis failed"));
767
+ process.exit(1);
768
+ }
769
+
770
+ p.log.info(`Path: ${chalk.dim(shortenPath(skillDir))}`);
771
+
772
+ const smp = path.join(skillDir, "SKILL.md");
773
+ const lines = [];
774
+
775
+ if (fs.existsSync(smp)) {
776
+ const m = parseSkillMdFrontmatter(smp);
777
+ lines.push(chalk.cyan("SKILL.md Frontmatter:"));
778
+ lines.push(` Name: ${m.name || chalk.dim("(not set)")}`);
779
+ lines.push(` Description: ${m.description ? m.description.substring(0, 60) : chalk.red("(MISSING)")}`);
780
+ if (m.tags) lines.push(` Tags: ${m.tags.join(", ")}`);
781
+ lines.push("");
782
+ }
783
+
784
+ const structure = detectSkillStructure(skillDir);
785
+ lines.push(chalk.cyan("Structure:"));
786
+ const items = [["resources", structure.hasResources], ["examples", structure.hasExamples], ["scripts", structure.hasScripts], ["constitution", structure.hasConstitution], ["doctrines", structure.hasDoctrines]];
787
+ items.forEach(([n, has]) => lines.push(` ${has ? chalk.green("✓") : chalk.dim("○")} ${has ? chalk.bold(n) : chalk.dim(n)}`));
788
+ lines.push("");
789
+
790
+ let score = 0;
791
+ if (fs.existsSync(smp)) score += 20;
792
+ const m = parseSkillMdFrontmatter(smp);
793
+ if (m.description) score += 25;
794
+ if (m.tags && m.tags.length > 0) score += 10;
795
+ if (structure.hasResources || structure.hasExamples || structure.hasScripts) score += 20;
796
+ if (fs.existsSync(path.join(skillDir, ".skill-source.json"))) score += 10;
797
+ if (structure.hasConstitution || structure.hasDoctrines) score += 15;
798
+
799
+ const scoreColor = score >= 80 ? chalk.green : score >= 50 ? chalk.yellow : chalk.red;
800
+ lines.push(`${chalk.cyan("Antigravity Score:")} ${scoreColor(score + "/100")}`);
801
+
802
+ p.note(lines.join("\n"), `Skill Analysis: ${skillName}`);
803
+ p.outro(chalk.green("Done!"));
804
+ }
805
+
806
+ function runInfo(name) {
807
+ console.log();
808
+ p.intro(chalk.bgCyan.black(" add-skill "));
809
+
810
+ if (!name) {
811
+ p.log.error("Missing skill name");
812
+ p.outro(chalk.red("Info failed"));
813
+ process.exit(1);
814
+ }
815
+
816
+ const scope = resolveScope();
817
+ const localDir = path.join(scope, name);
818
+
819
+ if (fs.existsSync(localDir)) {
820
+ const lines = [`Path: ${shortenPath(localDir)}`];
821
+ const mf = path.join(localDir, ".skill-source.json");
822
+ if (fs.existsSync(mf)) {
823
+ const m = JSON.parse(fs.readFileSync(mf, "utf-8"));
824
+ lines.push(`Repo: ${m.repo || "local"}`);
825
+ lines.push(`Installed: ${formatDate(m.installedAt)}`);
826
+ }
827
+ p.note(lines.join("\n"), `${name} ${chalk.green("(installed)")}`);
828
+ p.outro(chalk.green("Done!"));
829
+ return;
830
+ }
831
+
832
+ p.log.warn(`Skill not installed: ${name}`);
833
+ p.outro(chalk.dim("Run add-skill <org/repo> to install"));
834
+ }
835
+
836
+ async function runHelp() {
837
+ console.log(`
838
+ ${chalk.bold("add-skill")} ${chalk.dim("v" + pkg.version)}
839
+
840
+ ${chalk.bold("Usage")}
841
+ $ add-skill <command> [options]
842
+
843
+ ${chalk.bold("Commands")}
844
+ <org/repo> Install all skills from repository
845
+ <org/repo#skill> Install specific skill
846
+ list List installed skills
847
+ uninstall <skill> Remove a skill
848
+ update <skill> Update a skill
849
+ verify Verify checksums
850
+ doctor Check health
851
+ lock Generate skill-lock.json
852
+ init Initialize skills directory
853
+ validate [skill] Validate against Antigravity spec
854
+ analyze <skill> Analyze skill structure
855
+ cache [info|clear] Manage cache
856
+
857
+ ${chalk.bold("Options")}
858
+ --global, -g Use global scope
859
+ --force, -f Force operation
860
+ --strict Fail on violations
861
+ --fix Auto-fix issues
862
+ --dry-run Preview only
863
+ --verbose, -v Detailed output
864
+ --json JSON output
865
+ --yes, -y Skip confirmation prompts
866
+ `);
867
+ }
868
+
869
+ // --- MAIN ---
870
+ async function main() {
871
+ if (command === "list" || command === "ls") await runList();
872
+ else if (command === "init") await runInit();
873
+ else if (command === "install" || command === "add" || command === "i") await runInstall(params[0]);
874
+ else if (command === "uninstall" || command === "remove" || command === "rm") await runUninstall(params[0]);
875
+ else if (command === "update") await runUpdate(params[0]);
876
+ else if (command === "lock") runLock();
877
+ else if (command === "verify") runVerify();
878
+ else if (command === "doctor") runDoctor();
879
+ else if (command === "cache") runCache(params[0]);
880
+ else if (command === "validate" || command === "check") runValidate(params[0]);
881
+ else if (command === "analyze") runAnalyze(params[0]);
882
+ else if (command === "info" || command === "show") runInfo(params[0]);
883
+ else if (command === "help" || command === "--help" || command === "-h") await runHelp();
884
+ else if (command === "--version" || command === "-V") console.log(pkg.version);
885
+ else if (command.includes("/")) await runInstall(command);
886
+ else { console.log(`Unknown command: ${command}`); await runHelp(); }
887
+ }
888
+
889
+ main().catch(err => { console.error(chalk.red("\nError: " + err.message)); process.exit(1); });