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.
@@ -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();