askill-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +254 -0
  3. package/dist/cli.mjs +2816 -0
  4. package/package.json +59 -0
package/dist/cli.mjs ADDED
@@ -0,0 +1,2816 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/constants.ts
4
+ import { homedir } from "os";
5
+ import { join } from "path";
6
+ import { existsSync } from "fs";
7
+ var VERSION = "0.1.0";
8
+ var API_BASE_URL = "https://askill.sh/api/v1";
9
+ var RESET = "\x1B[0m";
10
+ var BOLD = "\x1B[1m";
11
+ var DIM = "\x1B[2m";
12
+ var CYAN = "\x1B[36m";
13
+ var GREEN = "\x1B[32m";
14
+ var YELLOW = "\x1B[33m";
15
+ var RED = "\x1B[31m";
16
+ var home = homedir();
17
+ var configHome = process.env.XDG_CONFIG_HOME || join(home, ".config");
18
+ var codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
19
+ var claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
20
+ var agents = {
21
+ // === Popular Agents ===
22
+ "claude-code": {
23
+ name: "claude-code",
24
+ displayName: "Claude Code",
25
+ skillsDir: ".claude/skills",
26
+ globalSkillsDir: join(claudeHome, "skills"),
27
+ detectInstalled: async () => existsSync(claudeHome)
28
+ },
29
+ cursor: {
30
+ name: "cursor",
31
+ displayName: "Cursor",
32
+ skillsDir: ".cursor/skills",
33
+ globalSkillsDir: join(home, ".cursor/skills"),
34
+ detectInstalled: async () => existsSync(join(home, ".cursor"))
35
+ },
36
+ windsurf: {
37
+ name: "windsurf",
38
+ displayName: "Windsurf",
39
+ skillsDir: ".windsurf/skills",
40
+ globalSkillsDir: join(home, ".codeium/windsurf/skills"),
41
+ detectInstalled: async () => existsSync(join(home, ".codeium/windsurf"))
42
+ },
43
+ opencode: {
44
+ name: "opencode",
45
+ displayName: "OpenCode",
46
+ skillsDir: ".opencode/skills",
47
+ globalSkillsDir: join(configHome, "opencode/skills"),
48
+ detectInstalled: async () => existsSync(join(configHome, "opencode")) || existsSync(join(claudeHome, "skills"))
49
+ },
50
+ codex: {
51
+ name: "codex",
52
+ displayName: "Codex",
53
+ skillsDir: ".codex/skills",
54
+ globalSkillsDir: join(codexHome, "skills"),
55
+ detectInstalled: async () => existsSync(codexHome) || existsSync("/etc/codex")
56
+ },
57
+ cline: {
58
+ name: "cline",
59
+ displayName: "Cline",
60
+ skillsDir: ".cline/skills",
61
+ globalSkillsDir: join(home, ".cline/skills"),
62
+ detectInstalled: async () => existsSync(join(home, ".cline"))
63
+ },
64
+ "gemini-cli": {
65
+ name: "gemini-cli",
66
+ displayName: "Gemini CLI",
67
+ skillsDir: ".gemini/skills",
68
+ globalSkillsDir: join(home, ".gemini/skills"),
69
+ detectInstalled: async () => existsSync(join(home, ".gemini"))
70
+ },
71
+ goose: {
72
+ name: "goose",
73
+ displayName: "Goose",
74
+ skillsDir: ".goose/skills",
75
+ globalSkillsDir: join(configHome, "goose/skills"),
76
+ detectInstalled: async () => existsSync(join(configHome, "goose"))
77
+ },
78
+ amp: {
79
+ name: "amp",
80
+ displayName: "Amp",
81
+ skillsDir: ".agents/skills",
82
+ globalSkillsDir: join(configHome, "agents/skills"),
83
+ detectInstalled: async () => existsSync(join(configHome, "amp"))
84
+ },
85
+ roo: {
86
+ name: "roo",
87
+ displayName: "Roo Code",
88
+ skillsDir: ".roo/skills",
89
+ globalSkillsDir: join(home, ".roo/skills"),
90
+ detectInstalled: async () => existsSync(join(home, ".roo"))
91
+ },
92
+ // === Additional Agents ===
93
+ antigravity: {
94
+ name: "antigravity",
95
+ displayName: "Antigravity",
96
+ skillsDir: ".agent/skills",
97
+ globalSkillsDir: join(home, ".gemini/antigravity/global_skills"),
98
+ detectInstalled: async () => existsSync(join(process.cwd(), ".agent")) || existsSync(join(home, ".gemini/antigravity"))
99
+ },
100
+ augment: {
101
+ name: "augment",
102
+ displayName: "Augment",
103
+ skillsDir: ".augment/rules",
104
+ globalSkillsDir: join(home, ".augment/rules"),
105
+ detectInstalled: async () => existsSync(join(home, ".augment"))
106
+ },
107
+ codebuddy: {
108
+ name: "codebuddy",
109
+ displayName: "CodeBuddy",
110
+ skillsDir: ".codebuddy/skills",
111
+ globalSkillsDir: join(home, ".codebuddy/skills"),
112
+ detectInstalled: async () => existsSync(join(process.cwd(), ".codebuddy")) || existsSync(join(home, ".codebuddy"))
113
+ },
114
+ "command-code": {
115
+ name: "command-code",
116
+ displayName: "Command Code",
117
+ skillsDir: ".commandcode/skills",
118
+ globalSkillsDir: join(home, ".commandcode/skills"),
119
+ detectInstalled: async () => existsSync(join(home, ".commandcode"))
120
+ },
121
+ continue: {
122
+ name: "continue",
123
+ displayName: "Continue",
124
+ skillsDir: ".continue/skills",
125
+ globalSkillsDir: join(home, ".continue/skills"),
126
+ detectInstalled: async () => existsSync(join(process.cwd(), ".continue")) || existsSync(join(home, ".continue"))
127
+ },
128
+ crush: {
129
+ name: "crush",
130
+ displayName: "Crush",
131
+ skillsDir: ".crush/skills",
132
+ globalSkillsDir: join(home, ".config/crush/skills"),
133
+ detectInstalled: async () => existsSync(join(home, ".config/crush"))
134
+ },
135
+ droid: {
136
+ name: "droid",
137
+ displayName: "Droid",
138
+ skillsDir: ".factory/skills",
139
+ globalSkillsDir: join(home, ".factory/skills"),
140
+ detectInstalled: async () => existsSync(join(home, ".factory"))
141
+ },
142
+ "github-copilot": {
143
+ name: "github-copilot",
144
+ displayName: "GitHub Copilot",
145
+ skillsDir: ".github/skills",
146
+ globalSkillsDir: join(home, ".copilot/skills"),
147
+ detectInstalled: async () => existsSync(join(process.cwd(), ".github")) || existsSync(join(home, ".copilot"))
148
+ },
149
+ junie: {
150
+ name: "junie",
151
+ displayName: "Junie",
152
+ skillsDir: ".junie/skills",
153
+ globalSkillsDir: join(home, ".junie/skills"),
154
+ detectInstalled: async () => existsSync(join(home, ".junie"))
155
+ },
156
+ "iflow-cli": {
157
+ name: "iflow-cli",
158
+ displayName: "iFlow CLI",
159
+ skillsDir: ".iflow/skills",
160
+ globalSkillsDir: join(home, ".iflow/skills"),
161
+ detectInstalled: async () => existsSync(join(home, ".iflow"))
162
+ },
163
+ kilo: {
164
+ name: "kilo",
165
+ displayName: "Kilo Code",
166
+ skillsDir: ".kilocode/skills",
167
+ globalSkillsDir: join(home, ".kilocode/skills"),
168
+ detectInstalled: async () => existsSync(join(home, ".kilocode"))
169
+ },
170
+ "kimi-cli": {
171
+ name: "kimi-cli",
172
+ displayName: "Kimi Code CLI",
173
+ skillsDir: ".agents/skills",
174
+ globalSkillsDir: join(home, ".config/agents/skills"),
175
+ detectInstalled: async () => existsSync(join(home, ".kimi"))
176
+ },
177
+ "kiro-cli": {
178
+ name: "kiro-cli",
179
+ displayName: "Kiro CLI",
180
+ skillsDir: ".kiro/skills",
181
+ globalSkillsDir: join(home, ".kiro/skills"),
182
+ detectInstalled: async () => existsSync(join(home, ".kiro"))
183
+ },
184
+ kode: {
185
+ name: "kode",
186
+ displayName: "Kode",
187
+ skillsDir: ".kode/skills",
188
+ globalSkillsDir: join(home, ".kode/skills"),
189
+ detectInstalled: async () => existsSync(join(home, ".kode"))
190
+ },
191
+ mcpjam: {
192
+ name: "mcpjam",
193
+ displayName: "MCPJam",
194
+ skillsDir: ".mcpjam/skills",
195
+ globalSkillsDir: join(home, ".mcpjam/skills"),
196
+ detectInstalled: async () => existsSync(join(home, ".mcpjam"))
197
+ },
198
+ "mistral-vibe": {
199
+ name: "mistral-vibe",
200
+ displayName: "Mistral Vibe",
201
+ skillsDir: ".vibe/skills",
202
+ globalSkillsDir: join(home, ".vibe/skills"),
203
+ detectInstalled: async () => existsSync(join(home, ".vibe"))
204
+ },
205
+ mux: {
206
+ name: "mux",
207
+ displayName: "Mux",
208
+ skillsDir: ".mux/skills",
209
+ globalSkillsDir: join(home, ".mux/skills"),
210
+ detectInstalled: async () => existsSync(join(home, ".mux"))
211
+ },
212
+ openclaw: {
213
+ name: "openclaw",
214
+ displayName: "OpenClaw",
215
+ skillsDir: "skills",
216
+ globalSkillsDir: existsSync(join(home, ".openclaw")) ? join(home, ".openclaw/skills") : existsSync(join(home, ".clawdbot")) ? join(home, ".clawdbot/skills") : join(home, ".moltbot/skills"),
217
+ detectInstalled: async () => existsSync(join(home, ".openclaw")) || existsSync(join(home, ".clawdbot")) || existsSync(join(home, ".moltbot"))
218
+ },
219
+ openclaude: {
220
+ name: "openclaude",
221
+ displayName: "OpenClaude IDE",
222
+ skillsDir: ".openclaude/skills",
223
+ globalSkillsDir: join(home, ".openclaude/skills"),
224
+ detectInstalled: async () => existsSync(join(home, ".openclaude")) || existsSync(join(process.cwd(), ".openclaude"))
225
+ },
226
+ openhands: {
227
+ name: "openhands",
228
+ displayName: "OpenHands",
229
+ skillsDir: ".openhands/skills",
230
+ globalSkillsDir: join(home, ".openhands/skills"),
231
+ detectInstalled: async () => existsSync(join(home, ".openhands"))
232
+ },
233
+ pi: {
234
+ name: "pi",
235
+ displayName: "Pi",
236
+ skillsDir: ".pi/skills",
237
+ globalSkillsDir: join(home, ".pi/agent/skills"),
238
+ detectInstalled: async () => existsSync(join(home, ".pi/agent"))
239
+ },
240
+ qoder: {
241
+ name: "qoder",
242
+ displayName: "Qoder",
243
+ skillsDir: ".qoder/skills",
244
+ globalSkillsDir: join(home, ".qoder/skills"),
245
+ detectInstalled: async () => existsSync(join(home, ".qoder"))
246
+ },
247
+ "qwen-code": {
248
+ name: "qwen-code",
249
+ displayName: "Qwen Code",
250
+ skillsDir: ".qwen/skills",
251
+ globalSkillsDir: join(home, ".qwen/skills"),
252
+ detectInstalled: async () => existsSync(join(home, ".qwen"))
253
+ },
254
+ replit: {
255
+ name: "replit",
256
+ displayName: "Replit",
257
+ skillsDir: ".agent/skills",
258
+ globalSkillsDir: void 0,
259
+ // No global support
260
+ detectInstalled: async () => existsSync(join(process.cwd(), ".agent"))
261
+ },
262
+ trae: {
263
+ name: "trae",
264
+ displayName: "Trae",
265
+ skillsDir: ".trae/skills",
266
+ globalSkillsDir: join(home, ".trae/skills"),
267
+ detectInstalled: async () => existsSync(join(home, ".trae"))
268
+ },
269
+ "trae-cn": {
270
+ name: "trae-cn",
271
+ displayName: "Trae CN",
272
+ skillsDir: ".trae/skills",
273
+ globalSkillsDir: join(home, ".trae-cn/skills"),
274
+ detectInstalled: async () => existsSync(join(home, ".trae-cn"))
275
+ },
276
+ zencoder: {
277
+ name: "zencoder",
278
+ displayName: "Zencoder",
279
+ skillsDir: ".zencoder/skills",
280
+ globalSkillsDir: join(home, ".zencoder/skills"),
281
+ detectInstalled: async () => existsSync(join(home, ".zencoder"))
282
+ },
283
+ neovate: {
284
+ name: "neovate",
285
+ displayName: "Neovate",
286
+ skillsDir: ".neovate/skills",
287
+ globalSkillsDir: join(home, ".neovate/skills"),
288
+ detectInstalled: async () => existsSync(join(home, ".neovate"))
289
+ },
290
+ pochi: {
291
+ name: "pochi",
292
+ displayName: "Pochi",
293
+ skillsDir: ".pochi/skills",
294
+ globalSkillsDir: join(home, ".pochi/skills"),
295
+ detectInstalled: async () => existsSync(join(home, ".pochi"))
296
+ },
297
+ adal: {
298
+ name: "adal",
299
+ displayName: "AdaL",
300
+ skillsDir: ".adal/skills",
301
+ globalSkillsDir: join(home, ".adal/skills"),
302
+ detectInstalled: async () => existsSync(join(home, ".adal"))
303
+ }
304
+ };
305
+ var AGENTS_DIR = ".agents";
306
+ var SKILLS_SUBDIR = "skills";
307
+
308
+ // src/api.ts
309
+ var APIClient = class {
310
+ baseUrl;
311
+ constructor(baseUrl = API_BASE_URL) {
312
+ this.baseUrl = baseUrl;
313
+ }
314
+ async fetch(path, options) {
315
+ const url = `${this.baseUrl}${path}`;
316
+ const response = await fetch(url, {
317
+ ...options,
318
+ headers: {
319
+ "Content-Type": "application/json",
320
+ "User-Agent": "askill-cli",
321
+ ...options?.headers
322
+ }
323
+ });
324
+ if (!response.ok) {
325
+ const error = await response.json().catch(() => ({}));
326
+ throw new APIError(
327
+ response.status,
328
+ error.error?.code || "UNKNOWN_ERROR",
329
+ error.error?.message || `HTTP ${response.status}`
330
+ );
331
+ }
332
+ return response.json();
333
+ }
334
+ /**
335
+ * List skills with pagination and filtering
336
+ */
337
+ async listSkills(options = {}) {
338
+ const params = new URLSearchParams();
339
+ if (options.page) params.set("page", String(options.page));
340
+ if (options.limit) params.set("limit", String(options.limit));
341
+ if (options.q) params.set("q", options.q);
342
+ if (options.tag) params.set("tag", options.tag);
343
+ if (options.owner) params.set("owner", options.owner);
344
+ if (options.repo) params.set("repo", options.repo);
345
+ if (options.sort) params.set("sort", options.sort);
346
+ if (options.order) params.set("order", options.order);
347
+ const query = params.toString();
348
+ return this.fetch(`/skills${query ? `?${query}` : ""}`);
349
+ }
350
+ /**
351
+ * Get all skills in a repository
352
+ */
353
+ async getRepoSkills(owner, repo) {
354
+ return this.fetch(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/skills`);
355
+ }
356
+ /**
357
+ * Get a single skill by slug
358
+ *
359
+ * Supported formats:
360
+ * - ID: "123"
361
+ * - Short name: "extract-errors"
362
+ * - Owner/repo@name: "facebook/react@extract-errors"
363
+ * - Full path: "facebook/react/scripts/error-codes"
364
+ */
365
+ async getSkill(slug) {
366
+ return this.fetch(`/skills/${encodeURIComponent(slug)}`);
367
+ }
368
+ /**
369
+ * Get the raw SKILL.md content
370
+ */
371
+ async getSkillRaw(slug) {
372
+ const url = `${this.baseUrl}/skills/${encodeURIComponent(slug)}/raw`;
373
+ const response = await fetch(url, {
374
+ headers: { "User-Agent": "askill-cli" }
375
+ });
376
+ if (!response.ok) {
377
+ const error = await response.json().catch(() => ({}));
378
+ throw new APIError(
379
+ response.status,
380
+ error.error?.code || "NOT_FOUND",
381
+ error.error?.message || "Skill not found"
382
+ );
383
+ }
384
+ return response.text();
385
+ }
386
+ /**
387
+ * Search for skills (uses listSkills with q parameter)
388
+ */
389
+ async search(q, limit = 10) {
390
+ return this.listSkills({ q, limit });
391
+ }
392
+ /**
393
+ * Check for CLI updates
394
+ */
395
+ async checkCLIVersion() {
396
+ return this.fetch("/cli/version");
397
+ }
398
+ };
399
+ var APIError = class extends Error {
400
+ constructor(status, code, message) {
401
+ super(message);
402
+ this.status = status;
403
+ this.code = code;
404
+ this.name = "APIError";
405
+ }
406
+ };
407
+ var api = new APIClient();
408
+
409
+ // src/installer.ts
410
+ import { mkdir, writeFile, symlink, lstat, rm, readlink, access, readdir, cp } from "fs/promises";
411
+ import { join as join2, dirname, relative, resolve, sep, normalize } from "path";
412
+ import { homedir as homedir2, platform } from "os";
413
+ var EXCLUDE_FILES = /* @__PURE__ */ new Set(["README.md", "metadata.json"]);
414
+ var EXCLUDE_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "__pycache__", "dist", "build"]);
415
+ function sanitizeName(name) {
416
+ const sanitized = name.toLowerCase().replace(/[^a-z0-9._]+/g, "-").replace(/^[.\-]+|[.\-]+$/g, "");
417
+ return sanitized.substring(0, 255) || "unnamed-skill";
418
+ }
419
+ function isPathSafe(basePath, targetPath) {
420
+ const normalizedBase = normalize(resolve(basePath));
421
+ const normalizedTarget = normalize(resolve(targetPath));
422
+ return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
423
+ }
424
+ function getCanonicalSkillsDir(global, cwd) {
425
+ const baseDir = global ? homedir2() : cwd || process.cwd();
426
+ return join2(baseDir, AGENTS_DIR, SKILLS_SUBDIR);
427
+ }
428
+ async function createSymlink(target, linkPath) {
429
+ try {
430
+ const resolvedTarget = resolve(target);
431
+ const resolvedLinkPath = resolve(linkPath);
432
+ if (resolvedTarget === resolvedLinkPath) {
433
+ return true;
434
+ }
435
+ try {
436
+ const stats = await lstat(linkPath);
437
+ if (stats.isSymbolicLink()) {
438
+ const existingTarget = await readlink(linkPath);
439
+ const resolvedExisting = resolve(dirname(linkPath), existingTarget);
440
+ if (resolvedExisting === resolvedTarget) {
441
+ return true;
442
+ }
443
+ await rm(linkPath);
444
+ } else {
445
+ await rm(linkPath, { recursive: true });
446
+ }
447
+ } catch (err) {
448
+ if (err.code === "ELOOP") {
449
+ await rm(linkPath, { force: true }).catch(() => {
450
+ });
451
+ }
452
+ }
453
+ const linkDir = dirname(linkPath);
454
+ await mkdir(linkDir, { recursive: true });
455
+ const relativePath = relative(linkDir, target);
456
+ const symlinkType = platform() === "win32" ? "junction" : void 0;
457
+ await symlink(relativePath, linkPath, symlinkType);
458
+ return true;
459
+ } catch {
460
+ return false;
461
+ }
462
+ }
463
+ async function cleanAndCreateDirectory(path) {
464
+ try {
465
+ await rm(path, { recursive: true, force: true });
466
+ } catch {
467
+ }
468
+ await mkdir(path, { recursive: true });
469
+ }
470
+ async function copySkillDirectory(src, dest) {
471
+ await mkdir(dest, { recursive: true });
472
+ const entries = await readdir(src, { withFileTypes: true });
473
+ await Promise.all(
474
+ entries.filter((entry) => {
475
+ if (entry.name.startsWith("_")) return false;
476
+ if (entry.isDirectory() && EXCLUDE_DIRS.has(entry.name)) return false;
477
+ if (!entry.isDirectory() && EXCLUDE_FILES.has(entry.name)) return false;
478
+ return true;
479
+ }).map(async (entry) => {
480
+ const srcPath = join2(src, entry.name);
481
+ const destPath = join2(dest, entry.name);
482
+ if (entry.isDirectory()) {
483
+ await copySkillDirectory(srcPath, destPath);
484
+ } else {
485
+ await cp(srcPath, destPath, { dereference: true, recursive: true });
486
+ }
487
+ })
488
+ );
489
+ }
490
+ function resolveInstallPaths(skillName, agentType, options) {
491
+ const agent = agents[agentType];
492
+ if (!agent) return null;
493
+ const isGlobal = options.global ?? false;
494
+ const cwd = options.cwd || process.cwd();
495
+ const sanitized = sanitizeName(skillName);
496
+ const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
497
+ const canonicalDir = join2(canonicalBase, sanitized);
498
+ const agentBase = isGlobal ? agent.globalSkillsDir : join2(cwd, agent.skillsDir);
499
+ const agentDir = join2(agentBase, sanitized);
500
+ if (!isPathSafe(canonicalBase, canonicalDir) || !isPathSafe(agentBase, agentDir)) {
501
+ return null;
502
+ }
503
+ return { canonicalBase, canonicalDir, agentBase, agentDir };
504
+ }
505
+ async function installSkillFromDir(skillName, skillDir, agentType, options = {}) {
506
+ const agent = agents[agentType];
507
+ if (!agent) {
508
+ return { success: false, path: "", mode: options.mode ?? "symlink", error: `Unknown agent: ${agentType}` };
509
+ }
510
+ const isGlobal = options.global ?? false;
511
+ if (isGlobal && !agent.globalSkillsDir) {
512
+ return { success: false, path: "", mode: options.mode ?? "symlink", error: `${agent.displayName} does not support global skill installation` };
513
+ }
514
+ const paths = resolveInstallPaths(skillName, agentType, options);
515
+ if (!paths) {
516
+ return { success: false, path: "", mode: options.mode ?? "symlink", error: "Invalid skill name: potential path traversal detected" };
517
+ }
518
+ const { canonicalDir, agentDir } = paths;
519
+ const installMode = options.mode ?? "symlink";
520
+ try {
521
+ if (installMode === "copy") {
522
+ await cleanAndCreateDirectory(agentDir);
523
+ await copySkillDirectory(skillDir, agentDir);
524
+ return { success: true, path: agentDir, mode: "copy" };
525
+ }
526
+ await cleanAndCreateDirectory(canonicalDir);
527
+ await copySkillDirectory(skillDir, canonicalDir);
528
+ const symlinkCreated = await createSymlink(canonicalDir, agentDir);
529
+ if (!symlinkCreated) {
530
+ await cleanAndCreateDirectory(agentDir);
531
+ await copySkillDirectory(skillDir, agentDir);
532
+ return { success: true, path: agentDir, canonicalPath: canonicalDir, mode: "symlink", symlinkFailed: true };
533
+ }
534
+ return { success: true, path: agentDir, canonicalPath: canonicalDir, mode: "symlink" };
535
+ } catch (error) {
536
+ return { success: false, path: agentDir, mode: installMode, error: error instanceof Error ? error.message : "Unknown error" };
537
+ }
538
+ }
539
+ async function installSkill(skillName, content, agentType, options = {}) {
540
+ const agent = agents[agentType];
541
+ if (!agent) {
542
+ return { success: false, path: "", mode: options.mode ?? "symlink", error: `Unknown agent: ${agentType}` };
543
+ }
544
+ const isGlobal = options.global ?? false;
545
+ if (isGlobal && !agent.globalSkillsDir) {
546
+ return { success: false, path: "", mode: options.mode ?? "symlink", error: `${agent.displayName} does not support global skill installation` };
547
+ }
548
+ const paths = resolveInstallPaths(skillName, agentType, options);
549
+ if (!paths) {
550
+ return { success: false, path: "", mode: options.mode ?? "symlink", error: "Invalid skill name: potential path traversal detected" };
551
+ }
552
+ const { canonicalDir, agentDir } = paths;
553
+ const installMode = options.mode ?? "symlink";
554
+ try {
555
+ if (installMode === "copy") {
556
+ await cleanAndCreateDirectory(agentDir);
557
+ await writeFile(join2(agentDir, "SKILL.md"), content, "utf-8");
558
+ return { success: true, path: agentDir, mode: "copy" };
559
+ }
560
+ await cleanAndCreateDirectory(canonicalDir);
561
+ await writeFile(join2(canonicalDir, "SKILL.md"), content, "utf-8");
562
+ const symlinkCreated = await createSymlink(canonicalDir, agentDir);
563
+ if (!symlinkCreated) {
564
+ await cleanAndCreateDirectory(agentDir);
565
+ await writeFile(join2(agentDir, "SKILL.md"), content, "utf-8");
566
+ return { success: true, path: agentDir, canonicalPath: canonicalDir, mode: "symlink", symlinkFailed: true };
567
+ }
568
+ return { success: true, path: agentDir, canonicalPath: canonicalDir, mode: "symlink" };
569
+ } catch (error) {
570
+ return { success: false, path: agentDir, mode: installMode, error: error instanceof Error ? error.message : "Unknown error" };
571
+ }
572
+ }
573
+ async function isSkillInstalled(skillName, agentType, options = {}) {
574
+ const agent = agents[agentType];
575
+ if (!agent) return false;
576
+ const sanitized = sanitizeName(skillName);
577
+ if (options.global && !agent.globalSkillsDir) {
578
+ return false;
579
+ }
580
+ const targetBase = options.global ? agent.globalSkillsDir : join2(options.cwd || process.cwd(), agent.skillsDir);
581
+ const skillDir = join2(targetBase, sanitized);
582
+ if (!isPathSafe(targetBase, skillDir)) {
583
+ return false;
584
+ }
585
+ try {
586
+ await access(skillDir);
587
+ return true;
588
+ } catch {
589
+ return false;
590
+ }
591
+ }
592
+ async function detectInstalledAgents() {
593
+ const results = await Promise.all(
594
+ Object.entries(agents).map(async ([type, config]) => ({
595
+ type,
596
+ installed: await config.detectInstalled()
597
+ }))
598
+ );
599
+ return results.filter((r) => r.installed).map((r) => r.type);
600
+ }
601
+ async function listInstalledSkills(options = {}) {
602
+ const cwd = options.cwd || process.cwd();
603
+ const installedSkills = [];
604
+ const scopes = [];
605
+ if (options.global === void 0) {
606
+ scopes.push({ global: false, path: getCanonicalSkillsDir(false, cwd) });
607
+ scopes.push({ global: true, path: getCanonicalSkillsDir(true, cwd) });
608
+ } else {
609
+ scopes.push({ global: options.global, path: getCanonicalSkillsDir(options.global, cwd) });
610
+ }
611
+ const detectedAgents = await detectInstalledAgents();
612
+ for (const scope of scopes) {
613
+ try {
614
+ const entries = await readdir(scope.path, { withFileTypes: true });
615
+ for (const entry of entries) {
616
+ if (!entry.isDirectory()) continue;
617
+ const skillDir = join2(scope.path, entry.name);
618
+ try {
619
+ await access(join2(skillDir, "SKILL.md"));
620
+ } catch {
621
+ continue;
622
+ }
623
+ const installedAgents = [];
624
+ for (const agentType of detectedAgents) {
625
+ const agent = agents[agentType];
626
+ if (scope.global && !agent.globalSkillsDir) continue;
627
+ const agentBase = scope.global ? agent.globalSkillsDir : join2(cwd, agent.skillsDir);
628
+ const agentSkillDir = join2(agentBase, entry.name);
629
+ try {
630
+ await access(agentSkillDir);
631
+ installedAgents.push(agentType);
632
+ } catch {
633
+ }
634
+ }
635
+ installedSkills.push({
636
+ name: entry.name,
637
+ path: skillDir,
638
+ scope: scope.global ? "global" : "project",
639
+ agents: installedAgents
640
+ });
641
+ }
642
+ } catch {
643
+ }
644
+ }
645
+ return installedSkills;
646
+ }
647
+ async function removeSkill(skillName, agentType, options = {}) {
648
+ const agent = agents[agentType];
649
+ if (!agent) {
650
+ return { success: false, error: `Unknown agent: ${agentType}` };
651
+ }
652
+ const sanitized = sanitizeName(skillName);
653
+ const cwd = options.cwd || process.cwd();
654
+ if (options.global && !agent.globalSkillsDir) {
655
+ return { success: false, error: `${agent.displayName} does not support global skills` };
656
+ }
657
+ const agentBase = options.global ? agent.globalSkillsDir : join2(cwd, agent.skillsDir);
658
+ const skillDir = join2(agentBase, sanitized);
659
+ if (!isPathSafe(agentBase, skillDir)) {
660
+ return { success: false, error: "Invalid skill name" };
661
+ }
662
+ try {
663
+ await rm(skillDir, { recursive: true, force: true });
664
+ return { success: true };
665
+ } catch (error) {
666
+ return {
667
+ success: false,
668
+ error: error instanceof Error ? error.message : "Unknown error"
669
+ };
670
+ }
671
+ }
672
+
673
+ // src/updater.ts
674
+ import { existsSync as existsSync2, createWriteStream, unlinkSync, chmodSync, renameSync } from "fs";
675
+ import { join as join3, dirname as dirname2 } from "path";
676
+ import { homedir as homedir3, platform as platform2, arch } from "os";
677
+ import { pipeline } from "stream/promises";
678
+ import { Readable } from "stream";
679
+ import semver from "semver";
680
+ var UPDATE_CHECK_FILE = join3(homedir3(), ".askill", "last-update-check");
681
+ var UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
682
+ var GITHUB_REPO = "avibe-bot/askill";
683
+ function getPlatformKey() {
684
+ const p2 = platform2();
685
+ const a = arch();
686
+ if (p2 === "darwin") {
687
+ return a === "arm64" ? "darwin-arm64" : "darwin-x64";
688
+ } else if (p2 === "linux") {
689
+ return a === "arm64" ? "linux-arm64" : "linux-x64";
690
+ } else if (p2 === "win32") {
691
+ return "win32-x64";
692
+ }
693
+ return `${p2}-${a}`;
694
+ }
695
+ async function shouldCheckUpdate() {
696
+ try {
697
+ const { readFile: readFile4 } = await import("fs/promises");
698
+ const lastCheck = await readFile4(UPDATE_CHECK_FILE, "utf-8");
699
+ const lastCheckTime = parseInt(lastCheck, 10);
700
+ return Date.now() - lastCheckTime > UPDATE_INTERVAL_MS;
701
+ } catch {
702
+ return true;
703
+ }
704
+ }
705
+ async function saveUpdateCheckTime() {
706
+ try {
707
+ const { mkdir: mkdir4, writeFile: writeFile4 } = await import("fs/promises");
708
+ await mkdir4(dirname2(UPDATE_CHECK_FILE), { recursive: true });
709
+ await writeFile4(UPDATE_CHECK_FILE, String(Date.now()), "utf-8");
710
+ } catch {
711
+ }
712
+ }
713
+ async function fetchVersionInfo() {
714
+ try {
715
+ const response = await fetch("https://askill.sh/api/v1/cli/version", {
716
+ headers: { "User-Agent": `askill/${VERSION}` }
717
+ });
718
+ if (response.ok) {
719
+ return response.json();
720
+ }
721
+ } catch {
722
+ }
723
+ try {
724
+ const response = await fetch(
725
+ `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`,
726
+ {
727
+ headers: {
728
+ "User-Agent": `askill/${VERSION}`,
729
+ "Accept": "application/vnd.github.v3+json"
730
+ }
731
+ }
732
+ );
733
+ if (!response.ok) return null;
734
+ const release = await response.json();
735
+ const latest = release.tag_name.replace(/^v/, "");
736
+ const downloadUrls = {};
737
+ for (const asset of release.assets || []) {
738
+ const platforms = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-x64"];
739
+ for (const p2 of platforms) {
740
+ if (asset.name.includes(p2)) {
741
+ downloadUrls[p2] = asset.browser_download_url;
742
+ }
743
+ }
744
+ }
745
+ return {
746
+ latest,
747
+ minimum: "0.1.0",
748
+ releaseNotes: release.body?.slice(0, 500) || `Release ${latest}`,
749
+ releaseUrl: release.html_url,
750
+ downloadUrls
751
+ };
752
+ } catch {
753
+ return null;
754
+ }
755
+ }
756
+ async function checkForUpdates(force = false) {
757
+ if (!force && !await shouldCheckUpdate()) {
758
+ return;
759
+ }
760
+ await saveUpdateCheckTime();
761
+ const versionInfo = await fetchVersionInfo();
762
+ if (!versionInfo) return;
763
+ const current = VERSION;
764
+ const latest = versionInfo.latest;
765
+ if (semver.lt(current, latest)) {
766
+ console.log();
767
+ console.log(`${YELLOW}\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E${RESET}`);
768
+ console.log(`${YELLOW}\u2502${RESET} Update available: ${DIM}${current}${RESET} \u2192 ${GREEN}${latest}${RESET} ${YELLOW}\u2502${RESET}`);
769
+ console.log(`${YELLOW}\u2502${RESET} Run ${CYAN}askill self-update${RESET} to update ${YELLOW}\u2502${RESET}`);
770
+ console.log(`${YELLOW}\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F${RESET}`);
771
+ console.log();
772
+ }
773
+ if (semver.lt(current, versionInfo.minimum)) {
774
+ console.log(`${RED}Your askill version is too old. Please update to continue.${RESET}`);
775
+ console.log(`Minimum required: ${versionInfo.minimum}`);
776
+ process.exit(1);
777
+ }
778
+ }
779
+ async function selfUpdate() {
780
+ console.log(`${CYAN}Checking for updates...${RESET}`);
781
+ const versionInfo = await fetchVersionInfo();
782
+ if (!versionInfo) {
783
+ console.log(`${RED}Failed to check for updates${RESET}`);
784
+ return false;
785
+ }
786
+ const current = VERSION;
787
+ const latest = versionInfo.latest;
788
+ if (semver.gte(current, latest)) {
789
+ console.log(`${GREEN}You are already on the latest version (${current})${RESET}`);
790
+ return true;
791
+ }
792
+ console.log(`Updating from ${DIM}${current}${RESET} to ${GREEN}${latest}${RESET}...`);
793
+ const platformKey = getPlatformKey();
794
+ const downloadUrl = versionInfo.downloadUrls[platformKey];
795
+ if (!downloadUrl) {
796
+ console.log(`${RED}No download available for your platform (${platformKey})${RESET}`);
797
+ console.log(`Please update manually:`);
798
+ console.log(` ${CYAN}npm install -g @askill/cli@latest${RESET}`);
799
+ console.log(` ${DIM}or${RESET}`);
800
+ console.log(` ${CYAN}curl -fsSL https://askill.sh/install.sh | sh${RESET}`);
801
+ return false;
802
+ }
803
+ try {
804
+ const execPath = process.execPath;
805
+ const isNodeProcess = execPath.includes("node") || execPath.includes("bun");
806
+ if (isNodeProcess) {
807
+ console.log(`${YELLOW}Running via Node.js runtime${RESET}`);
808
+ console.log(`Please update using: ${CYAN}npm install -g @askill/cli@latest${RESET}`);
809
+ return false;
810
+ }
811
+ const tempPath = `${execPath}.new`;
812
+ const backupPath = `${execPath}.backup`;
813
+ console.log(`Downloading ${platformKey} binary...`);
814
+ const response = await fetch(downloadUrl, {
815
+ headers: { "User-Agent": `askill/${VERSION}` },
816
+ redirect: "follow"
817
+ });
818
+ if (!response.ok || !response.body) {
819
+ throw new Error(`Download failed: ${response.status}`);
820
+ }
821
+ const writer = createWriteStream(tempPath);
822
+ await pipeline(Readable.fromWeb(response.body), writer);
823
+ chmodSync(tempPath, 493);
824
+ if (existsSync2(execPath)) {
825
+ renameSync(execPath, backupPath);
826
+ }
827
+ renameSync(tempPath, execPath);
828
+ try {
829
+ unlinkSync(backupPath);
830
+ } catch {
831
+ }
832
+ console.log(`${GREEN}Successfully updated to v${latest}!${RESET}`);
833
+ if (versionInfo.releaseNotes) {
834
+ console.log(`${DIM}Release notes: ${versionInfo.releaseNotes.slice(0, 200)}${RESET}`);
835
+ }
836
+ return true;
837
+ } catch (error) {
838
+ console.log(`${RED}Update failed: ${error instanceof Error ? error.message : "Unknown error"}${RESET}`);
839
+ console.log(`Please update manually:`);
840
+ console.log(` ${CYAN}npm install -g @askill/cli@latest${RESET}`);
841
+ console.log(` ${DIM}or${RESET}`);
842
+ console.log(` ${CYAN}curl -fsSL https://askill.sh/install.sh | sh${RESET}`);
843
+ return false;
844
+ }
845
+ }
846
+
847
+ // src/config.ts
848
+ import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
849
+ import { join as join4 } from "path";
850
+ import { homedir as homedir4 } from "os";
851
+ var CONFIG_DIR = process.env.XDG_CONFIG_HOME || join4(homedir4(), ".config");
852
+ var ASKILL_CONFIG_DIR = join4(CONFIG_DIR, "askill");
853
+ var CONFIG_FILE = join4(ASKILL_CONFIG_DIR, "config.json");
854
+ async function loadConfig() {
855
+ try {
856
+ const content = await readFile(CONFIG_FILE, "utf-8");
857
+ return JSON.parse(content);
858
+ } catch {
859
+ return {};
860
+ }
861
+ }
862
+ async function getPreferredAgents() {
863
+ const config = await loadConfig();
864
+ return config.preferredAgents;
865
+ }
866
+
867
+ // src/parser.ts
868
+ function parseSkillMd(content) {
869
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---/;
870
+ const match = content.match(frontmatterRegex);
871
+ if (!match) {
872
+ return {
873
+ frontmatter: {},
874
+ content: content.trim()
875
+ };
876
+ }
877
+ const yamlContent = match[1];
878
+ const markdownContent = content.slice(match[0].length).trim();
879
+ const frontmatter = parseYaml(yamlContent);
880
+ return {
881
+ frontmatter,
882
+ content: markdownContent
883
+ };
884
+ }
885
+ function parseYaml(yaml) {
886
+ const result = {};
887
+ const lines = yaml.split("\n");
888
+ let currentKey = null;
889
+ let currentArray = null;
890
+ let currentObject = null;
891
+ let inCommandsBlock = false;
892
+ let currentCommand = null;
893
+ let commandsResult = {};
894
+ for (let i = 0; i < lines.length; i++) {
895
+ const line = lines[i];
896
+ const trimmed = line.trim();
897
+ if (!trimmed || trimmed.startsWith("#")) continue;
898
+ const indent = line.search(/\S/);
899
+ if (indent === 0 && trimmed.includes(":")) {
900
+ if (currentKey && currentArray) {
901
+ result[currentKey] = currentArray;
902
+ currentArray = null;
903
+ }
904
+ if (currentKey && currentObject) {
905
+ result[currentKey] = currentObject;
906
+ currentObject = null;
907
+ }
908
+ if (inCommandsBlock) {
909
+ result.commands = commandsResult;
910
+ inCommandsBlock = false;
911
+ }
912
+ const colonIndex = trimmed.indexOf(":");
913
+ const key = trimmed.slice(0, colonIndex).trim();
914
+ const value = trimmed.slice(colonIndex + 1).trim();
915
+ currentKey = key;
916
+ if (key === "commands") {
917
+ inCommandsBlock = true;
918
+ commandsResult = {};
919
+ continue;
920
+ }
921
+ if (value) {
922
+ result[key] = parseValue(value);
923
+ currentKey = null;
924
+ }
925
+ continue;
926
+ }
927
+ if (inCommandsBlock) {
928
+ if (indent === 2 && trimmed.includes(":") && !trimmed.startsWith("-")) {
929
+ const colonIndex = trimmed.indexOf(":");
930
+ const cmdName = trimmed.slice(0, colonIndex).trim();
931
+ const cmdValue = trimmed.slice(colonIndex + 1).trim();
932
+ if (!cmdValue) {
933
+ currentCommand = cmdName;
934
+ commandsResult[cmdName] = { run: "", description: "" };
935
+ }
936
+ continue;
937
+ }
938
+ if (indent === 4 && currentCommand && trimmed.includes(":")) {
939
+ const colonIndex = trimmed.indexOf(":");
940
+ const propKey = trimmed.slice(0, colonIndex).trim();
941
+ const propValue = trimmed.slice(colonIndex + 1).trim();
942
+ if (propKey === "run" || propKey === "description") {
943
+ commandsResult[currentCommand][propKey] = parseValue(propValue);
944
+ }
945
+ continue;
946
+ }
947
+ continue;
948
+ }
949
+ if (trimmed.startsWith("-")) {
950
+ if (!currentArray) currentArray = [];
951
+ const itemValue = trimmed.slice(1).trim();
952
+ currentArray.push(parseValue(itemValue));
953
+ continue;
954
+ }
955
+ if (indent > 0 && currentKey && trimmed.includes(":")) {
956
+ if (!currentObject) currentObject = {};
957
+ const colonIndex = trimmed.indexOf(":");
958
+ const propKey = trimmed.slice(0, colonIndex).trim();
959
+ const propValue = trimmed.slice(colonIndex + 1).trim();
960
+ currentObject[propKey] = parseValue(propValue);
961
+ continue;
962
+ }
963
+ }
964
+ if (currentKey && currentArray) {
965
+ result[currentKey] = currentArray;
966
+ }
967
+ if (currentKey && currentObject) {
968
+ result[currentKey] = currentObject;
969
+ }
970
+ if (inCommandsBlock) {
971
+ result.commands = commandsResult;
972
+ }
973
+ return result;
974
+ }
975
+ function parseValue(value) {
976
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
977
+ return value.slice(1, -1);
978
+ }
979
+ return value;
980
+ }
981
+ function extractDependencies(content) {
982
+ const { frontmatter } = parseSkillMd(content);
983
+ return frontmatter.dependencies || [];
984
+ }
985
+ function parseDependency(dep) {
986
+ if (dep.startsWith("gh:")) {
987
+ const rest = dep.slice(3);
988
+ if (rest.includes("@")) {
989
+ const [repoPath, skillName] = rest.split("@");
990
+ const [owner, repo] = repoPath.split("/");
991
+ return {
992
+ type: "github",
993
+ raw: dep,
994
+ owner,
995
+ repo,
996
+ skill: skillName
997
+ };
998
+ }
999
+ const parts = rest.split("/");
1000
+ if (parts.length >= 3) {
1001
+ return {
1002
+ type: "github",
1003
+ raw: dep,
1004
+ owner: parts[0],
1005
+ repo: parts[1],
1006
+ path: parts.slice(2).join("/")
1007
+ };
1008
+ }
1009
+ return {
1010
+ type: "github",
1011
+ raw: dep,
1012
+ owner: parts[0],
1013
+ repo: parts[1]
1014
+ };
1015
+ }
1016
+ if (dep.startsWith("@")) {
1017
+ const withoutPrefix = dep.slice(1);
1018
+ const slashIndex = withoutPrefix.indexOf("/");
1019
+ if (slashIndex === -1) {
1020
+ return { type: "published", raw: dep };
1021
+ }
1022
+ const scope = withoutPrefix.slice(0, slashIndex);
1023
+ const rest = withoutPrefix.slice(slashIndex + 1);
1024
+ const atIndex = rest.indexOf("@");
1025
+ if (atIndex !== -1) {
1026
+ const name = rest.slice(0, atIndex);
1027
+ const version = rest.slice(atIndex + 1);
1028
+ return { type: "published", raw: dep, scope, name, version };
1029
+ }
1030
+ return { type: "published", raw: dep, scope, name: rest };
1031
+ }
1032
+ return { type: "published", raw: dep };
1033
+ }
1034
+ function dependencyToSlug(dep) {
1035
+ if (dep.type === "github") {
1036
+ if (dep.skill) {
1037
+ return `gh:${dep.owner}/${dep.repo}@${dep.skill}`;
1038
+ }
1039
+ if (dep.path) {
1040
+ return `gh:${dep.owner}/${dep.repo}/${dep.path}`;
1041
+ }
1042
+ return `gh:${dep.owner}/${dep.repo}`;
1043
+ }
1044
+ if (dep.scope && dep.name) {
1045
+ return `@${dep.scope}/${dep.name}`;
1046
+ }
1047
+ return dep.raw;
1048
+ }
1049
+
1050
+ // src/source-parser.ts
1051
+ import { isAbsolute, resolve as resolve2 } from "path";
1052
+ function parseSource(input) {
1053
+ if (isLocalPath(input)) {
1054
+ const resolvedPath = resolve2(input);
1055
+ return {
1056
+ type: "local",
1057
+ url: resolvedPath,
1058
+ localPath: resolvedPath
1059
+ };
1060
+ }
1061
+ const normalized = input.startsWith("gh:") ? input.slice(3) : input;
1062
+ const hadGhPrefix = input.startsWith("gh:");
1063
+ const githubTreeWithPathMatch = normalized.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/);
1064
+ if (githubTreeWithPathMatch) {
1065
+ const [, owner, repo, ref, subpath] = githubTreeWithPathMatch;
1066
+ return {
1067
+ type: "github",
1068
+ url: `https://github.com/${owner}/${repo}.git`,
1069
+ owner,
1070
+ repo,
1071
+ ref,
1072
+ subpath
1073
+ };
1074
+ }
1075
+ const githubTreeMatch = normalized.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)$/);
1076
+ if (githubTreeMatch) {
1077
+ const [, owner, repo, ref] = githubTreeMatch;
1078
+ return {
1079
+ type: "github",
1080
+ url: `https://github.com/${owner}/${repo}.git`,
1081
+ owner,
1082
+ repo,
1083
+ ref
1084
+ };
1085
+ }
1086
+ const githubUrlMatch = normalized.match(/github\.com\/([^/]+)\/([^/]+)/);
1087
+ if (githubUrlMatch) {
1088
+ const [, owner, repo] = githubUrlMatch;
1089
+ const cleanRepo = repo.replace(/\.git$/, "");
1090
+ return {
1091
+ type: "github",
1092
+ url: `https://github.com/${owner}/${cleanRepo}.git`,
1093
+ owner,
1094
+ repo: cleanRepo
1095
+ };
1096
+ }
1097
+ const atSkillMatch = normalized.match(/^([^/]+)\/([^/@]+)@(.+)$/);
1098
+ if (atSkillMatch && !normalized.includes(":")) {
1099
+ const [, owner, repo, skillFilter] = atSkillMatch;
1100
+ return {
1101
+ type: "github",
1102
+ url: `https://github.com/${owner}/${repo}.git`,
1103
+ owner,
1104
+ repo,
1105
+ skillFilter
1106
+ };
1107
+ }
1108
+ const shorthandMatch = normalized.match(/^([^/]+)\/([^/]+)(?:\/(.+))?$/);
1109
+ if (shorthandMatch && !normalized.includes(":") && !normalized.startsWith(".") && !normalized.startsWith("/")) {
1110
+ const [, owner, repo, subpath] = shorthandMatch;
1111
+ return {
1112
+ type: "github",
1113
+ url: `https://github.com/${owner}/${repo}.git`,
1114
+ owner,
1115
+ repo,
1116
+ subpath
1117
+ };
1118
+ }
1119
+ return {
1120
+ type: "git",
1121
+ url: normalized
1122
+ };
1123
+ }
1124
+ function isLocalPath(input) {
1125
+ return isAbsolute(input) || input.startsWith("./") || input.startsWith("../") || input === "." || input === ".." || /^[a-zA-Z]:[/\\]/.test(input);
1126
+ }
1127
+
1128
+ // src/discover.ts
1129
+ import { readdir as readdir2, readFile as readFile2, stat } from "fs/promises";
1130
+ import { join as join5 } from "path";
1131
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "__pycache__"]);
1132
+ async function hasSkillMd(dir) {
1133
+ try {
1134
+ const skillPath = join5(dir, "SKILL.md");
1135
+ const stats = await stat(skillPath);
1136
+ return stats.isFile();
1137
+ } catch {
1138
+ return false;
1139
+ }
1140
+ }
1141
+ async function parseSkillDir(dir) {
1142
+ try {
1143
+ const skillPath = join5(dir, "SKILL.md");
1144
+ const content = await readFile2(skillPath, "utf-8");
1145
+ const parsed = parseSkillMd(content);
1146
+ if (!parsed.frontmatter.name || !parsed.frontmatter.description) {
1147
+ return null;
1148
+ }
1149
+ return {
1150
+ name: parsed.frontmatter.name,
1151
+ description: parsed.frontmatter.description,
1152
+ path: dir,
1153
+ rawContent: content,
1154
+ frontmatter: parsed.frontmatter
1155
+ };
1156
+ } catch {
1157
+ return null;
1158
+ }
1159
+ }
1160
+ async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
1161
+ if (depth > maxDepth) return [];
1162
+ try {
1163
+ const [hasSkill, entries] = await Promise.all([
1164
+ hasSkillMd(dir),
1165
+ readdir2(dir, { withFileTypes: true }).catch(() => [])
1166
+ ]);
1167
+ const currentDir = hasSkill ? [dir] : [];
1168
+ const subDirResults = await Promise.all(
1169
+ entries.filter((entry) => entry.isDirectory() && !SKIP_DIRS.has(entry.name)).map((entry) => findSkillDirs(join5(dir, entry.name), depth + 1, maxDepth))
1170
+ );
1171
+ return [...currentDir, ...subDirResults.flat()];
1172
+ } catch {
1173
+ return [];
1174
+ }
1175
+ }
1176
+ async function discoverSkills(basePath, subpath) {
1177
+ const skills = [];
1178
+ const seenNames = /* @__PURE__ */ new Set();
1179
+ const searchPath = subpath ? join5(basePath, subpath) : basePath;
1180
+ if (await hasSkillMd(searchPath)) {
1181
+ const skill = await parseSkillDir(searchPath);
1182
+ if (skill) {
1183
+ return [skill];
1184
+ }
1185
+ }
1186
+ const prioritySearchDirs = [
1187
+ searchPath,
1188
+ join5(searchPath, "skills"),
1189
+ join5(searchPath, "skills/.curated"),
1190
+ join5(searchPath, "skills/.experimental"),
1191
+ join5(searchPath, ".agents/skills"),
1192
+ join5(searchPath, ".claude/skills"),
1193
+ join5(searchPath, ".opencode/skills"),
1194
+ join5(searchPath, ".cursor/skills"),
1195
+ join5(searchPath, ".codex/skills"),
1196
+ join5(searchPath, ".cline/skills"),
1197
+ join5(searchPath, ".gemini/skills"),
1198
+ join5(searchPath, ".windsurf/skills"),
1199
+ join5(searchPath, ".roo/skills"),
1200
+ join5(searchPath, ".github/skills"),
1201
+ join5(searchPath, ".goose/skills")
1202
+ ];
1203
+ for (const dir of prioritySearchDirs) {
1204
+ try {
1205
+ const entries = await readdir2(dir, { withFileTypes: true });
1206
+ for (const entry of entries) {
1207
+ if (entry.isDirectory()) {
1208
+ const skillDir = join5(dir, entry.name);
1209
+ if (await hasSkillMd(skillDir)) {
1210
+ const skill = await parseSkillDir(skillDir);
1211
+ if (skill && !seenNames.has(skill.name)) {
1212
+ skills.push(skill);
1213
+ seenNames.add(skill.name);
1214
+ }
1215
+ }
1216
+ }
1217
+ }
1218
+ } catch {
1219
+ }
1220
+ }
1221
+ if (skills.length === 0) {
1222
+ const allSkillDirs = await findSkillDirs(searchPath);
1223
+ for (const skillDir of allSkillDirs) {
1224
+ const skill = await parseSkillDir(skillDir);
1225
+ if (skill && !seenNames.has(skill.name)) {
1226
+ skills.push(skill);
1227
+ seenNames.add(skill.name);
1228
+ }
1229
+ }
1230
+ }
1231
+ return skills;
1232
+ }
1233
+ function filterSkills(skills, names) {
1234
+ const normalizedNames = names.map((n) => n.toLowerCase());
1235
+ return skills.filter((skill) => {
1236
+ const name = skill.name.toLowerCase();
1237
+ return normalizedNames.some((input) => input === name);
1238
+ });
1239
+ }
1240
+
1241
+ // src/git.ts
1242
+ import { execFile } from "child_process";
1243
+ import { mkdtemp, rm as rm2 } from "fs/promises";
1244
+ import { join as join6, normalize as normalize2, resolve as resolve3, sep as sep2 } from "path";
1245
+ import { tmpdir } from "os";
1246
+ var CLONE_TIMEOUT_MS = 6e4;
1247
+ var GitCloneError = class extends Error {
1248
+ url;
1249
+ isTimeout;
1250
+ isAuthError;
1251
+ constructor(message, url, isTimeout = false, isAuthError = false) {
1252
+ super(message);
1253
+ this.name = "GitCloneError";
1254
+ this.url = url;
1255
+ this.isTimeout = isTimeout;
1256
+ this.isAuthError = isAuthError;
1257
+ }
1258
+ };
1259
+ async function cloneRepo(url, ref) {
1260
+ const tempDir = await mkdtemp(join6(tmpdir(), "askill-"));
1261
+ const args = ["clone", "--depth", "1"];
1262
+ if (ref) {
1263
+ args.push("--branch", ref);
1264
+ }
1265
+ args.push(url, tempDir);
1266
+ try {
1267
+ await execGit(args);
1268
+ return tempDir;
1269
+ } catch (error) {
1270
+ await rm2(tempDir, { recursive: true, force: true }).catch(() => {
1271
+ });
1272
+ const errorMessage = error instanceof Error ? error.message : String(error);
1273
+ const isTimeout = errorMessage.includes("timed out") || errorMessage.includes("timeout");
1274
+ const isAuthError = errorMessage.includes("Authentication failed") || errorMessage.includes("could not read Username") || errorMessage.includes("Permission denied") || errorMessage.includes("Repository not found");
1275
+ if (isTimeout) {
1276
+ throw new GitCloneError(
1277
+ `Clone timed out after 60s. This may happen with private repos.
1278
+ Ensure SSH keys or credentials are configured.`,
1279
+ url,
1280
+ true,
1281
+ false
1282
+ );
1283
+ }
1284
+ if (isAuthError) {
1285
+ throw new GitCloneError(
1286
+ `Authentication failed for ${url}.
1287
+ For SSH: Check keys with 'ssh -T git@github.com'
1288
+ For HTTPS: Run 'gh auth login'`,
1289
+ url,
1290
+ false,
1291
+ true
1292
+ );
1293
+ }
1294
+ throw new GitCloneError(`Failed to clone ${url}: ${errorMessage}`, url);
1295
+ }
1296
+ }
1297
+ async function cleanupTempDir(dir) {
1298
+ const normalizedDir = normalize2(resolve3(dir));
1299
+ const normalizedTmpDir = normalize2(resolve3(tmpdir()));
1300
+ if (!normalizedDir.startsWith(normalizedTmpDir + sep2) && normalizedDir !== normalizedTmpDir) {
1301
+ throw new Error("Attempted to clean up directory outside of temp directory");
1302
+ }
1303
+ await rm2(dir, { recursive: true, force: true });
1304
+ }
1305
+ function execGit(args) {
1306
+ return new Promise((resolve4, reject) => {
1307
+ const child = execFile("git", args, { timeout: CLONE_TIMEOUT_MS }, (error, stdout, stderr) => {
1308
+ if (error) {
1309
+ reject(new Error(stderr || error.message));
1310
+ } else {
1311
+ resolve4(stdout);
1312
+ }
1313
+ });
1314
+ });
1315
+ }
1316
+
1317
+ // src/lock.ts
1318
+ import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
1319
+ import { join as join7, dirname as dirname5 } from "path";
1320
+ import { homedir as homedir5 } from "os";
1321
+ var AGENTS_DIR2 = ".agents";
1322
+ var LOCK_FILE = ".skill-lock.json";
1323
+ var CURRENT_VERSION = 3;
1324
+ function getSkillLockPath() {
1325
+ return join7(homedir5(), AGENTS_DIR2, LOCK_FILE);
1326
+ }
1327
+ function createEmptyLockFile() {
1328
+ return {
1329
+ version: CURRENT_VERSION,
1330
+ skills: {}
1331
+ };
1332
+ }
1333
+ async function readSkillLock() {
1334
+ const lockPath = getSkillLockPath();
1335
+ try {
1336
+ const content = await readFile3(lockPath, "utf-8");
1337
+ const parsed = JSON.parse(content);
1338
+ if (typeof parsed.version !== "number" || !parsed.skills) {
1339
+ return createEmptyLockFile();
1340
+ }
1341
+ if (parsed.version < CURRENT_VERSION) {
1342
+ return createEmptyLockFile();
1343
+ }
1344
+ return parsed;
1345
+ } catch {
1346
+ return createEmptyLockFile();
1347
+ }
1348
+ }
1349
+ async function writeSkillLock(lock) {
1350
+ const lockPath = getSkillLockPath();
1351
+ await mkdir3(dirname5(lockPath), { recursive: true });
1352
+ const content = JSON.stringify(lock, null, 2);
1353
+ await writeFile3(lockPath, content, "utf-8");
1354
+ }
1355
+ async function addSkillToLock(skillName, entry) {
1356
+ const lock = await readSkillLock();
1357
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1358
+ const existingEntry = lock.skills[skillName];
1359
+ lock.skills[skillName] = {
1360
+ ...entry,
1361
+ installedAt: existingEntry?.installedAt ?? now,
1362
+ updatedAt: now
1363
+ };
1364
+ await writeSkillLock(lock);
1365
+ }
1366
+ async function removeSkillFromLock(skillName) {
1367
+ const lock = await readSkillLock();
1368
+ if (!(skillName in lock.skills)) {
1369
+ return false;
1370
+ }
1371
+ delete lock.skills[skillName];
1372
+ await writeSkillLock(lock);
1373
+ return true;
1374
+ }
1375
+ async function getAllLockedSkills() {
1376
+ const lock = await readSkillLock();
1377
+ return lock.skills;
1378
+ }
1379
+ async function getLastSelectedAgents() {
1380
+ const lock = await readSkillLock();
1381
+ return lock.lastSelectedAgents;
1382
+ }
1383
+ async function saveLastSelectedAgents(agents2) {
1384
+ const lock = await readSkillLock();
1385
+ lock.lastSelectedAgents = agents2;
1386
+ await writeSkillLock(lock);
1387
+ }
1388
+ async function fetchSkillFolderHash(ownerRepo, skillPath) {
1389
+ let folderPath = skillPath.replace(/\\/g, "/");
1390
+ if (folderPath.endsWith("/SKILL.md")) {
1391
+ folderPath = folderPath.slice(0, -9);
1392
+ } else if (folderPath.endsWith("SKILL.md")) {
1393
+ folderPath = folderPath.slice(0, -8);
1394
+ }
1395
+ if (folderPath.endsWith("/")) {
1396
+ folderPath = folderPath.slice(0, -1);
1397
+ }
1398
+ const branches = ["main", "master"];
1399
+ for (const branch of branches) {
1400
+ try {
1401
+ const url = `https://api.github.com/repos/${ownerRepo}/git/trees/${branch}?recursive=1`;
1402
+ const response = await fetch(url, {
1403
+ headers: {
1404
+ Accept: "application/vnd.github.v3+json",
1405
+ "User-Agent": "askill-cli"
1406
+ }
1407
+ });
1408
+ if (!response.ok) continue;
1409
+ const data = await response.json();
1410
+ if (!folderPath) {
1411
+ return data.sha;
1412
+ }
1413
+ const folderEntry = data.tree.find(
1414
+ (entry) => entry.type === "tree" && entry.path === folderPath
1415
+ );
1416
+ if (folderEntry) {
1417
+ return folderEntry.sha;
1418
+ }
1419
+ } catch {
1420
+ continue;
1421
+ }
1422
+ }
1423
+ return "";
1424
+ }
1425
+
1426
+ // src/cli.ts
1427
+ import { join as join8 } from "path";
1428
+ import { homedir as homedir6 } from "os";
1429
+ import * as p from "@clack/prompts";
1430
+ import pc from "picocolors";
1431
+ var LOGO = `
1432
+ \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557
1433
+ \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
1434
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
1435
+ \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
1436
+ \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
1437
+ \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D
1438
+ `.trim();
1439
+ function showLogo() {
1440
+ const lines = LOGO.split("\n");
1441
+ const grays = ["\x1B[38;5;250m", "\x1B[38;5;248m", "\x1B[38;5;245m", "\x1B[38;5;243m", "\x1B[38;5;240m", "\x1B[38;5;238m"];
1442
+ console.log();
1443
+ lines.forEach((line, i) => {
1444
+ console.log(`${grays[i] || grays[grays.length - 1]}${line}${RESET}`);
1445
+ });
1446
+ }
1447
+ function showBanner() {
1448
+ showLogo();
1449
+ console.log();
1450
+ console.log(`${DIM}The Agent Skill Package Manager${RESET}`);
1451
+ console.log();
1452
+ console.log(` ${DIM}$${RESET} askill add ${DIM}<skill>${RESET} ${DIM}Install a skill${RESET}`);
1453
+ console.log(` ${DIM}$${RESET} askill find ${DIM}[query]${RESET} ${DIM}Search for skills${RESET}`);
1454
+ console.log(` ${DIM}$${RESET} askill list${RESET} ${DIM}List installed skills${RESET}`);
1455
+ console.log(` ${DIM}$${RESET} askill remove ${DIM}<skill>${RESET} ${DIM}Remove a skill${RESET}`);
1456
+ console.log(` ${DIM}$${RESET} askill init${RESET} ${DIM}Create a new skill${RESET}`);
1457
+ console.log(` ${DIM}$${RESET} askill run ${DIM}<skill:cmd>${RESET} ${DIM}Run a skill command${RESET}`);
1458
+ console.log();
1459
+ console.log(`${DIM}Browse skills at${RESET} ${CYAN}https://askill.sh${RESET}`);
1460
+ console.log();
1461
+ }
1462
+ function showHelp() {
1463
+ console.log(`
1464
+ ${BOLD}Usage:${RESET} askill <command> [options]
1465
+
1466
+ ${BOLD}Commands:${RESET}
1467
+ add, install, i <skill> Install a skill from askill.sh
1468
+ remove, rm <skill> Remove an installed skill
1469
+ list, ls List installed skills
1470
+ find, search, s [query] Search for skills
1471
+ info <skill> Show skill details
1472
+ init [dir] Create a new SKILL.md template
1473
+ validate [path] Validate a SKILL.md file
1474
+ check Check installed skills for updates
1475
+ update [skill] Update installed skills
1476
+ run <skill:cmd> Run a skill command
1477
+ self-update Update askill CLI
1478
+
1479
+ ${BOLD}Skill Source Formats:${RESET}
1480
+ owner/repo All skills from a GitHub repo
1481
+ owner/repo@skill-name Specific skill by name
1482
+ owner/repo/path/to/skill Specific skill by path
1483
+ https://github.com/owner/repo Full GitHub URL
1484
+ ./local/path Local directory
1485
+ gh:owner/repo@skill-name Explicit GitHub prefix (optional)
1486
+
1487
+ ${BOLD}Install Options:${RESET}
1488
+ -g, --global Install globally (user-level)
1489
+ -a, --agent <agents> Install to specific agents
1490
+ -y, --yes Skip confirmation prompts
1491
+ --copy Copy files instead of symlink
1492
+ -l, --list Preview skills in a repo without installing
1493
+ --all Install all discovered skills (skip selection)
1494
+
1495
+ ${BOLD}Run Options:${RESET}
1496
+ askill run <skill>:<command> Run a skill's command
1497
+
1498
+ ${BOLD}Options:${RESET}
1499
+ --help, -h Show this help message
1500
+ --version, -v Show version number
1501
+
1502
+ ${BOLD}Examples:${RESET}
1503
+ ${DIM}$${RESET} askill add anthropic/courses@prompt-eng
1504
+ ${DIM}$${RESET} askill add anthropic/courses
1505
+ ${DIM}$${RESET} askill add ./my-skills/custom-skill
1506
+ ${DIM}$${RESET} askill find memory
1507
+ ${DIM}$${RESET} askill list -g
1508
+ ${DIM}$${RESET} askill info gh:anthropic/courses@prompt-eng
1509
+
1510
+ ${DIM}Browse more at${RESET} ${CYAN}https://askill.sh${RESET}
1511
+ `);
1512
+ }
1513
+ function parseInstallOptions(args) {
1514
+ const options = {};
1515
+ let skillName = "";
1516
+ for (let i = 0; i < args.length; i++) {
1517
+ const arg = args[i];
1518
+ if (arg === "-g" || arg === "--global") {
1519
+ options.global = true;
1520
+ } else if (arg === "-y" || arg === "--yes") {
1521
+ options.yes = true;
1522
+ } else if (arg === "--copy") {
1523
+ options.copy = true;
1524
+ } else if (arg === "-l" || arg === "--list") {
1525
+ options.list = true;
1526
+ } else if (arg === "--all") {
1527
+ options.all = true;
1528
+ } else if (arg === "-a" || arg === "--agent") {
1529
+ options.agent = [];
1530
+ while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
1531
+ i++;
1532
+ options.agent.push(args[i]);
1533
+ }
1534
+ } else if (!arg.startsWith("-")) {
1535
+ skillName = arg;
1536
+ }
1537
+ }
1538
+ return { skillName, options };
1539
+ }
1540
+ async function resolveSkills(source, spinner2, options) {
1541
+ const parsed = parseSource(source);
1542
+ if (parsed.type === "local") {
1543
+ spinner2.start(`Scanning ${source}...`);
1544
+ const skills = await discoverSkills(parsed.localPath);
1545
+ spinner2.stop(`Found ${skills.length} skill(s) in ${pc.cyan(source)}`);
1546
+ return { skills, parsed };
1547
+ }
1548
+ if (parsed.type === "github" || parsed.type === "git") {
1549
+ spinner2.start(`Cloning ${parsed.owner ? `${parsed.owner}/${parsed.repo}` : parsed.url}...`);
1550
+ try {
1551
+ const tempDir = await cloneRepo(parsed.url, parsed.ref);
1552
+ spinner2.stop("Repository cloned");
1553
+ spinner2.start("Discovering skills...");
1554
+ let skills = await discoverSkills(tempDir, parsed.subpath);
1555
+ if (parsed.skillFilter) {
1556
+ skills = filterSkills(skills, [parsed.skillFilter]);
1557
+ }
1558
+ spinner2.stop(`Found ${skills.length} skill(s)`);
1559
+ return { skills, parsed, tempDir };
1560
+ } catch (error) {
1561
+ if (parsed.type === "github" && parsed.owner && parsed.repo) {
1562
+ const errorMsg = error instanceof GitCloneError ? error.message : "Clone failed";
1563
+ spinner2.stop(pc.yellow(`Git clone failed, trying askill.sh...`));
1564
+ try {
1565
+ return await resolveSkillsViaApi(parsed, spinner2, options);
1566
+ } catch (apiError) {
1567
+ spinner2.stop(pc.red("Failed"));
1568
+ p.log.error(`Git clone: ${errorMsg}`);
1569
+ p.log.error(`API fallback: ${apiError instanceof Error ? apiError.message : "Failed"}`);
1570
+ p.outro(pc.red("Could not resolve skill"));
1571
+ process.exit(1);
1572
+ }
1573
+ }
1574
+ spinner2.stop(pc.red("Clone failed"));
1575
+ if (error instanceof GitCloneError) {
1576
+ p.log.error(error.message);
1577
+ }
1578
+ p.outro(pc.red("Could not clone repository"));
1579
+ process.exit(1);
1580
+ }
1581
+ }
1582
+ return { skills: [], parsed };
1583
+ }
1584
+ async function resolveSkillsViaApi(parsed, spinner2, options) {
1585
+ const { owner, repo, skillFilter, subpath } = parsed;
1586
+ if (skillFilter) {
1587
+ const slug = `${owner}/${repo}@${skillFilter}`;
1588
+ spinner2.start(`Fetching ${slug} from askill.sh...`);
1589
+ const skill = await api.getSkill(slug);
1590
+ const content = await api.getSkillRaw(slug);
1591
+ spinner2.stop(`Found: ${pc.cyan(skill.name)}`);
1592
+ return {
1593
+ skills: [{
1594
+ name: skill.name || "unknown",
1595
+ description: skill.description || "",
1596
+ path: "",
1597
+ // No local path (API-only)
1598
+ rawContent: content,
1599
+ frontmatter: { name: skill.name || void 0, description: skill.description || void 0 }
1600
+ }],
1601
+ parsed
1602
+ };
1603
+ }
1604
+ if (subpath) {
1605
+ const slug = `${owner}/${repo}/${subpath}`;
1606
+ spinner2.start(`Fetching ${slug} from askill.sh...`);
1607
+ const skill = await api.getSkill(slug);
1608
+ const skillSlug = skill.owner && skill.repo && skill.name ? `${skill.owner}/${skill.repo}@${skill.name}` : String(skill.id);
1609
+ const content = await api.getSkillRaw(skillSlug);
1610
+ spinner2.stop(`Found: ${pc.cyan(skill.name)}`);
1611
+ return {
1612
+ skills: [{
1613
+ name: skill.name || "unknown",
1614
+ description: skill.description || "",
1615
+ path: "",
1616
+ rawContent: content,
1617
+ frontmatter: { name: skill.name || void 0, description: skill.description || void 0 }
1618
+ }],
1619
+ parsed
1620
+ };
1621
+ }
1622
+ spinner2.start(`Fetching skills from ${owner}/${repo} on askill.sh...`);
1623
+ const repoData = await api.getRepoSkills(owner, repo);
1624
+ spinner2.stop(`Found ${repoData.skills.length} skill(s)`);
1625
+ const results = [];
1626
+ for (const s of repoData.skills) {
1627
+ const slug = `${owner}/${repo}@${s.name}`;
1628
+ try {
1629
+ const content = await api.getSkillRaw(slug);
1630
+ results.push({
1631
+ name: s.name || "unknown",
1632
+ description: s.description || "",
1633
+ path: "",
1634
+ rawContent: content,
1635
+ frontmatter: { name: s.name || void 0, description: s.description || void 0 }
1636
+ });
1637
+ } catch {
1638
+ }
1639
+ }
1640
+ return { skills: results, parsed };
1641
+ }
1642
+ async function runInstall(args) {
1643
+ const { skillName, options } = parseInstallOptions(args);
1644
+ if (!skillName) {
1645
+ console.log(`${RED}Error: Missing skill identifier${RESET}`);
1646
+ console.log(`Usage: askill add <source>`);
1647
+ console.log(`
1648
+ Formats supported:`);
1649
+ console.log(` askill add owner/repo ${DIM}# all skills from repo${RESET}`);
1650
+ console.log(` askill add owner/repo@skill-name ${DIM}# specific skill${RESET}`);
1651
+ console.log(` askill add owner/repo/path/to/skill ${DIM}# skill by path${RESET}`);
1652
+ console.log(` askill add https://github.com/owner/repo ${DIM}# full GitHub URL${RESET}`);
1653
+ console.log(` askill add ./local/path ${DIM}# local directory${RESET}`);
1654
+ process.exit(1);
1655
+ }
1656
+ console.log();
1657
+ p.intro(pc.bgCyan(pc.black(" askill install ")));
1658
+ const spinner2 = p.spinner();
1659
+ const { skills: discoveredSkills, parsed: sourceParsed, tempDir } = await resolveSkills(skillName, spinner2, options);
1660
+ const cleanup = async () => {
1661
+ if (tempDir) await cleanupTempDir(tempDir).catch(() => {
1662
+ });
1663
+ };
1664
+ try {
1665
+ if (discoveredSkills.length === 0) {
1666
+ p.log.warning("No skills found");
1667
+ p.outro(`Browse skills at ${pc.cyan("https://askill.sh")}`);
1668
+ return;
1669
+ }
1670
+ if (options.list) {
1671
+ console.log();
1672
+ p.log.info(`Found ${discoveredSkills.length} skill(s) in ${pc.cyan(skillName)}:`);
1673
+ console.log();
1674
+ for (const skill of discoveredSkills) {
1675
+ console.log(` ${pc.cyan(skill.name)}`);
1676
+ if (skill.description) {
1677
+ console.log(` ${pc.dim(skill.description.slice(0, 80))}${skill.description.length > 80 ? "..." : ""}`);
1678
+ }
1679
+ if (skill.path) {
1680
+ console.log(` ${pc.dim("path:")} ${skill.path.replace(tempDir || "", "").replace(/^\//, "")}`);
1681
+ }
1682
+ console.log();
1683
+ }
1684
+ p.outro(`Install with: ${pc.cyan(`askill add ${skillName} --all`)}`);
1685
+ return;
1686
+ }
1687
+ let skillsToInstall;
1688
+ if (discoveredSkills.length === 1 || options.yes || options.all) {
1689
+ skillsToInstall = discoveredSkills;
1690
+ if (discoveredSkills.length === 1) {
1691
+ p.log.info(`Installing: ${pc.cyan(discoveredSkills[0].name)}`);
1692
+ } else {
1693
+ p.log.info(`Installing ${discoveredSkills.length} skill(s)`);
1694
+ }
1695
+ } else {
1696
+ const selected = await p.multiselect({
1697
+ message: "Select skills to install",
1698
+ options: discoveredSkills.map((s) => ({
1699
+ value: s,
1700
+ label: s.name,
1701
+ hint: s.description.slice(0, 60) + (s.description.length > 60 ? "..." : "")
1702
+ }))
1703
+ });
1704
+ if (p.isCancel(selected)) {
1705
+ p.cancel("Installation cancelled");
1706
+ return;
1707
+ }
1708
+ skillsToInstall = selected;
1709
+ }
1710
+ let targetAgents;
1711
+ const validAgents = Object.keys(agents);
1712
+ if (options.agent && options.agent.length > 0) {
1713
+ const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
1714
+ if (invalidAgents.length > 0) {
1715
+ p.log.error(`Invalid agents: ${invalidAgents.join(", ")}`);
1716
+ p.log.info(`Valid agents: ${validAgents.slice(0, 10).join(", ")}...`);
1717
+ return;
1718
+ }
1719
+ targetAgents = options.agent;
1720
+ } else {
1721
+ spinner2.start("Detecting installed agents...");
1722
+ const installedAgents = await detectInstalledAgents();
1723
+ const preferredAgents = await getLastSelectedAgents() || await getPreferredAgents();
1724
+ spinner2.stop(`Found ${installedAgents.length} agent(s)`);
1725
+ if (installedAgents.length === 0) {
1726
+ if (options.yes) {
1727
+ targetAgents = validAgents.slice(0, 5);
1728
+ p.log.info("Installing to default agents");
1729
+ } else {
1730
+ const selected = await p.multiselect({
1731
+ message: "Select agents to install to",
1732
+ options: validAgents.slice(0, 15).map((a) => ({
1733
+ value: a,
1734
+ label: agents[a].displayName
1735
+ }))
1736
+ });
1737
+ if (p.isCancel(selected)) {
1738
+ p.cancel("Installation cancelled");
1739
+ return;
1740
+ }
1741
+ targetAgents = selected;
1742
+ }
1743
+ } else if (installedAgents.length === 1) {
1744
+ targetAgents = installedAgents;
1745
+ p.log.info(`Installing to: ${targetAgents.map((a) => pc.cyan(agents[a].displayName)).join(", ")}`);
1746
+ } else if (options.yes) {
1747
+ const effectiveAgents = preferredAgents ? preferredAgents.filter((a) => installedAgents.includes(a)) : [];
1748
+ targetAgents = effectiveAgents.length > 0 ? effectiveAgents : installedAgents;
1749
+ p.log.info(`Installing to: ${targetAgents.map((a) => pc.cyan(agents[a].displayName)).join(", ")}`);
1750
+ } else {
1751
+ const initialSelection = preferredAgents ? preferredAgents.filter((a) => installedAgents.includes(a)) : installedAgents;
1752
+ const selected = await p.multiselect({
1753
+ message: "Select agents to install to",
1754
+ options: installedAgents.map((a) => ({
1755
+ value: a,
1756
+ label: agents[a].displayName
1757
+ })),
1758
+ initialValues: initialSelection.length > 0 ? initialSelection : installedAgents
1759
+ });
1760
+ if (p.isCancel(selected)) {
1761
+ p.cancel("Installation cancelled");
1762
+ return;
1763
+ }
1764
+ targetAgents = selected;
1765
+ }
1766
+ }
1767
+ let installGlobally = options.global ?? false;
1768
+ if (options.global === void 0 && !options.yes) {
1769
+ const scope = await p.select({
1770
+ message: "Installation scope",
1771
+ options: [
1772
+ { value: false, label: "Project", hint: "Install in current directory" },
1773
+ { value: true, label: "Global", hint: "Install in home directory (all projects)" }
1774
+ ]
1775
+ });
1776
+ if (p.isCancel(scope)) {
1777
+ p.cancel("Installation cancelled");
1778
+ return;
1779
+ }
1780
+ installGlobally = scope;
1781
+ }
1782
+ const installMode = options.copy ? "copy" : "symlink";
1783
+ if (!options.yes) {
1784
+ const skillNames = skillsToInstall.map((s) => s.name).join(", ");
1785
+ const confirmed = await p.confirm({
1786
+ message: `Install ${pc.cyan(skillNames)} to ${targetAgents.length} agent(s)?`
1787
+ });
1788
+ if (p.isCancel(confirmed) || !confirmed) {
1789
+ p.cancel("Installation cancelled");
1790
+ return;
1791
+ }
1792
+ }
1793
+ spinner2.start("Installing...");
1794
+ const allResults = [];
1795
+ const installedNames = /* @__PURE__ */ new Set();
1796
+ const normalizeDepKey = (s) => s.replace(/^gh:/, "").toLowerCase();
1797
+ const toApiSlug = (s) => s.replace(/^gh:/, "");
1798
+ async function installOneSkill(skill, isDependency) {
1799
+ spinner2.message(`Installing ${skill.name}...`);
1800
+ for (const agent of targetAgents) {
1801
+ let result;
1802
+ if (skill.path) {
1803
+ result = await installSkillFromDir(skill.name, skill.path, agent, {
1804
+ global: installGlobally,
1805
+ mode: installMode
1806
+ });
1807
+ } else {
1808
+ result = await installSkill(skill.name, skill.rawContent, agent, {
1809
+ global: installGlobally,
1810
+ mode: installMode
1811
+ });
1812
+ }
1813
+ allResults.push({ skill: skill.name, agent, ...result, isDependency });
1814
+ }
1815
+ }
1816
+ async function installDependencies(skill) {
1817
+ const dependencies = extractDependencies(skill.rawContent);
1818
+ if (dependencies.length === 0) return;
1819
+ spinner2.message(`Resolving dependencies for ${skill.name}...`);
1820
+ for (const dep of dependencies) {
1821
+ const parsed = parseDependency(dep);
1822
+ const depSlug = dependencyToSlug(parsed);
1823
+ const depKey = normalizeDepKey(depSlug);
1824
+ if (installedNames.has(depKey)) continue;
1825
+ installedNames.add(depKey);
1826
+ try {
1827
+ const apiSlug = toApiSlug(depSlug);
1828
+ const depSkill = await api.getSkill(apiSlug);
1829
+ const depContent = await api.getSkillRaw(apiSlug);
1830
+ const parsedContent = parseSkillMd(depContent);
1831
+ const name = depSkill.name || parsedContent.frontmatter.name || parsed.skill || parsed.name || dep;
1832
+ const description = depSkill.description || parsedContent.frontmatter.description || "";
1833
+ const depDiscovered = {
1834
+ name,
1835
+ description,
1836
+ path: "",
1837
+ // API-only, no local path
1838
+ rawContent: depContent,
1839
+ frontmatter: parsedContent.frontmatter
1840
+ };
1841
+ installedNames.add(normalizeDepKey(name));
1842
+ await installDependencies(depDiscovered);
1843
+ await installOneSkill(depDiscovered, true);
1844
+ } catch (error) {
1845
+ const errorMsg = error instanceof Error ? error.message : "Failed to resolve dependency";
1846
+ for (const agent of targetAgents) {
1847
+ allResults.push({
1848
+ skill: `${dep} (dependency of ${skill.name})`,
1849
+ agent,
1850
+ success: false,
1851
+ error: errorMsg,
1852
+ isDependency: true
1853
+ });
1854
+ }
1855
+ }
1856
+ }
1857
+ }
1858
+ for (const skill of skillsToInstall) {
1859
+ const key = normalizeDepKey(skill.name);
1860
+ if (installedNames.has(key)) continue;
1861
+ installedNames.add(key);
1862
+ await installDependencies(skill);
1863
+ await installOneSkill(skill, false);
1864
+ }
1865
+ spinner2.stop("Installation complete");
1866
+ const successful = allResults.filter((r) => r.success);
1867
+ const failed = allResults.filter((r) => !r.success);
1868
+ if (successful.length > 0) {
1869
+ await saveLastSelectedAgents(targetAgents);
1870
+ const installedSkillNames = new Set(successful.map((r) => r.skill));
1871
+ for (const skillName2 of installedSkillNames) {
1872
+ const discoveredSkill = skillsToInstall.find((s) => s.name === skillName2);
1873
+ const source = sourceParsed.owner && sourceParsed.repo ? `${sourceParsed.owner}/${sourceParsed.repo}` : sourceParsed.localPath || sourceParsed.url;
1874
+ const sourceType = sourceParsed.type === "local" ? "local" : sourceParsed.type;
1875
+ const sourceUrl = sourceParsed.url;
1876
+ let skillPath = "";
1877
+ if (discoveredSkill?.path && tempDir) {
1878
+ const relative2 = discoveredSkill.path.replace(tempDir, "").replace(/^\//, "");
1879
+ if (relative2) skillPath = relative2;
1880
+ } else if (sourceParsed.subpath) {
1881
+ skillPath = sourceParsed.subpath;
1882
+ } else if (sourceParsed.skillFilter) {
1883
+ skillPath = "";
1884
+ }
1885
+ let skillFolderHash = "";
1886
+ if (sourceType === "github" && sourceParsed.owner && sourceParsed.repo) {
1887
+ try {
1888
+ skillFolderHash = await fetchSkillFolderHash(
1889
+ `${sourceParsed.owner}/${sourceParsed.repo}`,
1890
+ skillPath
1891
+ );
1892
+ } catch {
1893
+ }
1894
+ }
1895
+ await addSkillToLock(skillName2, {
1896
+ source,
1897
+ sourceType,
1898
+ sourceUrl,
1899
+ skillPath: skillPath || void 0,
1900
+ skillFolderHash
1901
+ }).catch(() => {
1902
+ });
1903
+ }
1904
+ console.log();
1905
+ const mainSkills = successful.filter((r) => !r.isDependency);
1906
+ const depSkills = successful.filter((r) => r.isDependency);
1907
+ const skillCount = new Set(mainSkills.map((r) => r.skill)).size;
1908
+ const depCount = new Set(depSkills.map((r) => r.skill)).size;
1909
+ const agentCount = new Set(successful.map((r) => r.agent)).size;
1910
+ let message = `Installed ${skillCount} skill(s)`;
1911
+ if (depCount > 0) {
1912
+ message += ` + ${depCount} dependenc${depCount === 1 ? "y" : "ies"}`;
1913
+ }
1914
+ message += ` to ${agentCount} agent(s)`;
1915
+ p.log.success(pc.green(message));
1916
+ const bySkill = successful.reduce((acc, r) => {
1917
+ if (!acc[r.skill]) acc[r.skill] = { agents: [], isDependency: r.isDependency };
1918
+ acc[r.skill].agents.push(r.agent);
1919
+ return acc;
1920
+ }, {});
1921
+ for (const [skill, info] of Object.entries(bySkill)) {
1922
+ const prefix = info.isDependency ? pc.dim(" (dep) ") : " ";
1923
+ console.log(`${prefix}${pc.green("\u2713")} ${skill}`);
1924
+ for (const agent of info.agents) {
1925
+ const agentName = agents[agent]?.displayName || agent;
1926
+ console.log(` ${pc.dim("\u2192")} ${agentName}`);
1927
+ }
1928
+ }
1929
+ }
1930
+ if (failed.length > 0) {
1931
+ console.log();
1932
+ p.log.error(pc.red(`Failed for ${failed.length} installation(s)`));
1933
+ for (const r of failed) {
1934
+ const agentName = agents[r.agent]?.displayName || r.agent;
1935
+ console.log(` ${pc.red("\u2717")} ${r.skill} \u2192 ${agentName}: ${pc.dim(r.error || "Unknown error")}`);
1936
+ }
1937
+ }
1938
+ console.log();
1939
+ p.outro(pc.green("Done!"));
1940
+ } finally {
1941
+ await cleanup();
1942
+ }
1943
+ }
1944
+ async function runSearch(args) {
1945
+ const query = args.join(" ");
1946
+ console.log();
1947
+ p.intro(pc.bgCyan(pc.black(" askill search ")));
1948
+ const spinner2 = p.spinner();
1949
+ spinner2.start(query ? `Searching for "${query}"...` : "Loading skills...");
1950
+ try {
1951
+ const response = query ? await api.search(query, 20) : await api.listSkills({ limit: 20 });
1952
+ const skills = response.data || [];
1953
+ spinner2.stop(`Found ${skills.length} result(s)`);
1954
+ if (skills.length === 0) {
1955
+ p.log.info("No skills found");
1956
+ p.outro(`Browse all skills at ${pc.cyan("https://askill.sh")}`);
1957
+ return;
1958
+ }
1959
+ console.log();
1960
+ for (const skill of skills) {
1961
+ const displayName = skill.name || "unknown";
1962
+ const owner = skill.owner || "unknown";
1963
+ const description = skill.description || "";
1964
+ console.log(` ${pc.cyan(displayName)} ${pc.dim(`by ${owner}`)}`);
1965
+ if (description) {
1966
+ console.log(` ${pc.dim(description.slice(0, 80))}${description.length > 80 ? "..." : ""}`);
1967
+ }
1968
+ const installCmd = skill.owner && skill.repo ? `gh:${skill.owner}/${skill.repo}@${displayName}` : `gh:${displayName}`;
1969
+ console.log(` ${pc.dim("askill add")} ${installCmd}`);
1970
+ console.log();
1971
+ }
1972
+ p.outro(`Browse more at ${pc.cyan("https://askill.sh")}`);
1973
+ } catch (error) {
1974
+ spinner2.stop(pc.red("Search failed"));
1975
+ if (error instanceof Error) {
1976
+ console.log(pc.red(error.message));
1977
+ }
1978
+ process.exit(1);
1979
+ }
1980
+ }
1981
+ async function runList(args) {
1982
+ const isGlobal = args.includes("-g") || args.includes("--global");
1983
+ console.log();
1984
+ p.intro(pc.bgCyan(pc.black(" askill list ")));
1985
+ const spinner2 = p.spinner();
1986
+ spinner2.start("Loading installed skills...");
1987
+ const skills = await listInstalledSkills({ global: isGlobal ? true : void 0 });
1988
+ spinner2.stop(`Found ${skills.length} skill(s)`);
1989
+ if (skills.length === 0) {
1990
+ p.log.info("No skills installed");
1991
+ p.outro(`Install skills with ${pc.cyan("askill add <skill>")}`);
1992
+ return;
1993
+ }
1994
+ console.log();
1995
+ for (const skill of skills) {
1996
+ const scope = skill.scope === "global" ? pc.yellow("[global]") : pc.dim("[project]");
1997
+ const agentList = skill.agents.map((a) => agents[a]?.displayName || a).join(", ");
1998
+ console.log(` ${pc.cyan(skill.name)} ${scope}`);
1999
+ console.log(` ${pc.dim("Agents:")} ${agentList || pc.dim("none")}`);
2000
+ console.log(` ${pc.dim(skill.path)}`);
2001
+ console.log();
2002
+ }
2003
+ p.outro("");
2004
+ }
2005
+ async function runRemove(args) {
2006
+ const isGlobal = args.includes("-g") || args.includes("--global");
2007
+ const skillName = args.find((a) => !a.startsWith("-"));
2008
+ if (!skillName) {
2009
+ console.log(`${RED}Error: Missing skill name${RESET}`);
2010
+ console.log(`Usage: askill remove <skill-name>`);
2011
+ process.exit(1);
2012
+ }
2013
+ console.log();
2014
+ p.intro(pc.bgCyan(pc.black(" askill remove ")));
2015
+ const spinner2 = p.spinner();
2016
+ spinner2.start("Detecting agents...");
2017
+ const installedAgents = await detectInstalledAgents();
2018
+ spinner2.stop(`Found ${installedAgents.length} agent(s)`);
2019
+ const agentsWithSkill = [];
2020
+ for (const agent of installedAgents) {
2021
+ if (await isSkillInstalled(skillName, agent, { global: isGlobal })) {
2022
+ agentsWithSkill.push(agent);
2023
+ }
2024
+ }
2025
+ if (agentsWithSkill.length === 0) {
2026
+ p.log.info(`Skill "${skillName}" not found`);
2027
+ p.outro("");
2028
+ return;
2029
+ }
2030
+ const confirmed = await p.confirm({
2031
+ message: `Remove ${pc.cyan(skillName)} from ${agentsWithSkill.length} agent(s)?`
2032
+ });
2033
+ if (p.isCancel(confirmed) || !confirmed) {
2034
+ p.cancel("Removal cancelled");
2035
+ process.exit(0);
2036
+ }
2037
+ spinner2.start("Removing...");
2038
+ for (const agent of agentsWithSkill) {
2039
+ await removeSkill(skillName, agent, { global: isGlobal });
2040
+ }
2041
+ await removeSkillFromLock(skillName).catch(() => {
2042
+ });
2043
+ spinner2.stop("Removed");
2044
+ p.outro(pc.green(`Removed ${skillName} from ${agentsWithSkill.length} agent(s)`));
2045
+ }
2046
+ async function runInfo(args) {
2047
+ const skillName = args[0];
2048
+ if (!skillName) {
2049
+ console.log(`${RED}Error: Missing skill name${RESET}`);
2050
+ console.log(`Usage: askill info <skill-name>`);
2051
+ process.exit(1);
2052
+ }
2053
+ console.log();
2054
+ p.intro(pc.bgCyan(pc.black(" askill info ")));
2055
+ const spinner2 = p.spinner();
2056
+ spinner2.start(`Fetching ${skillName}...`);
2057
+ try {
2058
+ const skill = await api.getSkill(skillName);
2059
+ spinner2.stop("");
2060
+ const displayName = skill.name || "unknown";
2061
+ const owner = skill.owner || "unknown";
2062
+ const repo = skill.repo || "";
2063
+ console.log();
2064
+ console.log(` ${pc.bold(displayName)}`);
2065
+ if (skill.description) {
2066
+ console.log(` ${pc.dim(skill.description)}`);
2067
+ }
2068
+ console.log();
2069
+ console.log(` ${pc.dim("Owner:")} ${owner}`);
2070
+ if (repo) {
2071
+ console.log(` ${pc.dim("Repository:")} ${owner}/${repo}`);
2072
+ }
2073
+ if (skill.stars !== null && skill.stars !== void 0) {
2074
+ console.log(` ${pc.dim("Stars:")} ${skill.stars.toLocaleString()}`);
2075
+ }
2076
+ if (skill.tags && skill.tags.length > 0) {
2077
+ console.log(` ${pc.dim("Tags:")} ${skill.tags.join(", ")}`);
2078
+ }
2079
+ if (skill.path) {
2080
+ console.log(` ${pc.dim("Path:")} ${skill.path}`);
2081
+ }
2082
+ if (skill.updatedAt) {
2083
+ console.log(` ${pc.dim("Updated:")} ${new Date(skill.updatedAt).toLocaleDateString()}`);
2084
+ }
2085
+ console.log();
2086
+ const installCmd = skill.owner && skill.repo ? `${skill.owner}/${skill.repo}@${displayName}` : displayName;
2087
+ console.log(` ${pc.dim("Install:")} ${pc.cyan(`askill install gh:${installCmd}`)}`);
2088
+ console.log();
2089
+ p.outro("");
2090
+ } catch (error) {
2091
+ if (error instanceof APIError && error.status === 404) {
2092
+ spinner2.stop(pc.red("Not found"));
2093
+ p.outro(pc.red(`Skill "${skillName}" not found`));
2094
+ process.exit(1);
2095
+ }
2096
+ throw error;
2097
+ }
2098
+ }
2099
+ async function runCheck(_args) {
2100
+ console.log();
2101
+ p.intro(pc.bgCyan(pc.black(" askill check ")));
2102
+ const spinner2 = p.spinner();
2103
+ spinner2.start("Reading lock file...");
2104
+ const skills = await getAllLockedSkills();
2105
+ const skillNames = Object.keys(skills);
2106
+ if (skillNames.length === 0) {
2107
+ spinner2.stop("No skills tracked");
2108
+ p.log.info("No installed skills found in lock file");
2109
+ p.log.info(`Install skills with ${pc.cyan("askill add <skill>")}`);
2110
+ p.outro("");
2111
+ return;
2112
+ }
2113
+ spinner2.stop(`Found ${skillNames.length} tracked skill(s)`);
2114
+ spinner2.start("Checking for updates...");
2115
+ const updatable = [];
2116
+ const upToDate = [];
2117
+ const uncheckable = [];
2118
+ for (const [name, entry] of Object.entries(skills)) {
2119
+ if (entry.sourceType !== "github" || !entry.source) {
2120
+ uncheckable.push({ name, reason: entry.sourceType === "local" ? "local source" : "no source info" });
2121
+ continue;
2122
+ }
2123
+ if (!entry.skillFolderHash) {
2124
+ uncheckable.push({ name, reason: "no hash recorded (reinstall to fix)" });
2125
+ continue;
2126
+ }
2127
+ try {
2128
+ const remoteHash = await fetchSkillFolderHash(entry.source, entry.skillPath || "");
2129
+ if (!remoteHash) {
2130
+ uncheckable.push({ name, reason: "could not fetch remote hash" });
2131
+ continue;
2132
+ }
2133
+ if (remoteHash !== entry.skillFolderHash) {
2134
+ updatable.push({
2135
+ name,
2136
+ source: entry.source,
2137
+ sourceUrl: entry.sourceUrl,
2138
+ skillPath: entry.skillPath,
2139
+ localHash: entry.skillFolderHash,
2140
+ remoteHash
2141
+ });
2142
+ } else {
2143
+ upToDate.push(name);
2144
+ }
2145
+ } catch {
2146
+ uncheckable.push({ name, reason: "failed to check remote" });
2147
+ }
2148
+ }
2149
+ spinner2.stop("Check complete");
2150
+ console.log();
2151
+ if (updatable.length > 0) {
2152
+ p.log.warning(pc.yellow(`${updatable.length} skill(s) have updates available:`));
2153
+ for (const u of updatable) {
2154
+ console.log(` ${pc.yellow("\u2191")} ${pc.cyan(u.name)} ${pc.dim(`from ${u.source}`)}`);
2155
+ console.log(` ${pc.dim(`${u.localHash.slice(0, 8)} \u2192 ${u.remoteHash.slice(0, 8)}`)}`);
2156
+ }
2157
+ console.log();
2158
+ p.log.info(`Run ${pc.cyan("askill update")} to update all`);
2159
+ }
2160
+ if (upToDate.length > 0) {
2161
+ p.log.success(pc.green(`${upToDate.length} skill(s) up to date`));
2162
+ }
2163
+ if (uncheckable.length > 0) {
2164
+ for (const u of uncheckable) {
2165
+ console.log(` ${pc.dim("?")} ${u.name} ${pc.dim(`(${u.reason})`)}`);
2166
+ }
2167
+ }
2168
+ console.log();
2169
+ p.outro(updatable.length > 0 ? pc.yellow(`${updatable.length} update(s) available`) : pc.green("All up to date"));
2170
+ }
2171
+ async function runUpdate(args) {
2172
+ const isYes = args.includes("-y") || args.includes("--yes");
2173
+ const specificSkill = args.find((a) => !a.startsWith("-"));
2174
+ console.log();
2175
+ p.intro(pc.bgCyan(pc.black(" askill update ")));
2176
+ const spinner2 = p.spinner();
2177
+ spinner2.start("Checking for updates...");
2178
+ const skills = await getAllLockedSkills();
2179
+ const skillNames = Object.keys(skills);
2180
+ if (skillNames.length === 0) {
2181
+ spinner2.stop("No skills tracked");
2182
+ p.log.info("No installed skills found in lock file");
2183
+ p.outro("");
2184
+ return;
2185
+ }
2186
+ const updatable = [];
2187
+ for (const [name, entry] of Object.entries(skills)) {
2188
+ if (specificSkill && name !== specificSkill) continue;
2189
+ if (entry.sourceType !== "github" || !entry.source || !entry.skillFolderHash) {
2190
+ continue;
2191
+ }
2192
+ try {
2193
+ const remoteHash = await fetchSkillFolderHash(entry.source, entry.skillPath || "");
2194
+ if (remoteHash && remoteHash !== entry.skillFolderHash) {
2195
+ updatable.push({
2196
+ name,
2197
+ source: entry.source,
2198
+ sourceUrl: entry.sourceUrl,
2199
+ skillPath: entry.skillPath,
2200
+ localHash: entry.skillFolderHash,
2201
+ remoteHash
2202
+ });
2203
+ }
2204
+ } catch {
2205
+ }
2206
+ }
2207
+ if (updatable.length === 0) {
2208
+ spinner2.stop("All skills up to date");
2209
+ p.outro(pc.green("Nothing to update"));
2210
+ return;
2211
+ }
2212
+ spinner2.stop(`${updatable.length} update(s) available`);
2213
+ for (const u of updatable) {
2214
+ console.log(` ${pc.yellow("\u2191")} ${pc.cyan(u.name)} ${pc.dim(`(${u.localHash.slice(0, 8)} \u2192 ${u.remoteHash.slice(0, 8)})`)}`);
2215
+ }
2216
+ if (!isYes) {
2217
+ const confirmed = await p.confirm({
2218
+ message: `Update ${updatable.length} skill(s)?`
2219
+ });
2220
+ if (p.isCancel(confirmed) || !confirmed) {
2221
+ p.cancel("Update cancelled");
2222
+ return;
2223
+ }
2224
+ }
2225
+ const lastAgents = await getLastSelectedAgents();
2226
+ let targetAgents;
2227
+ if (lastAgents && lastAgents.length > 0) {
2228
+ targetAgents = lastAgents;
2229
+ } else {
2230
+ spinner2.start("Detecting agents...");
2231
+ const installedAgents = await detectInstalledAgents();
2232
+ spinner2.stop(`Found ${installedAgents.length} agent(s)`);
2233
+ targetAgents = installedAgents;
2234
+ }
2235
+ if (targetAgents.length === 0) {
2236
+ p.log.error("No agents found");
2237
+ p.outro(pc.red("Cannot update without agents"));
2238
+ return;
2239
+ }
2240
+ spinner2.start("Updating...");
2241
+ let successCount = 0;
2242
+ let failCount = 0;
2243
+ for (const u of updatable) {
2244
+ spinner2.message(`Updating ${u.name}...`);
2245
+ let tempDir;
2246
+ try {
2247
+ tempDir = await cloneRepo(u.sourceUrl);
2248
+ let discovered = await discoverSkills(tempDir, u.skillPath);
2249
+ discovered = discovered.filter((s) => s.name === u.name);
2250
+ if (discovered.length === 0) {
2251
+ discovered = await discoverSkills(tempDir);
2252
+ discovered = discovered.filter((s) => s.name === u.name);
2253
+ }
2254
+ if (discovered.length === 0) {
2255
+ p.log.warning(`Skill "${u.name}" not found in source, skipping`);
2256
+ failCount++;
2257
+ continue;
2258
+ }
2259
+ const skill = discovered[0];
2260
+ for (const agent of targetAgents) {
2261
+ if (skill.path) {
2262
+ await installSkillFromDir(skill.name, skill.path, agent, { mode: "symlink" });
2263
+ } else {
2264
+ await installSkill(skill.name, skill.rawContent, agent, { mode: "symlink" });
2265
+ }
2266
+ }
2267
+ const lockEntry = skills[u.name];
2268
+ await addSkillToLock(u.name, {
2269
+ source: lockEntry.source,
2270
+ sourceType: lockEntry.sourceType,
2271
+ sourceUrl: lockEntry.sourceUrl,
2272
+ skillPath: lockEntry.skillPath,
2273
+ skillFolderHash: u.remoteHash
2274
+ });
2275
+ successCount++;
2276
+ } catch (error) {
2277
+ const msg = error instanceof Error ? error.message : "Unknown error";
2278
+ p.log.error(`Failed to update ${u.name}: ${pc.dim(msg)}`);
2279
+ failCount++;
2280
+ } finally {
2281
+ if (tempDir) {
2282
+ await cleanupTempDir(tempDir).catch(() => {
2283
+ });
2284
+ }
2285
+ }
2286
+ }
2287
+ spinner2.stop("Update complete");
2288
+ if (successCount > 0) {
2289
+ p.log.success(pc.green(`Updated ${successCount} skill(s)`));
2290
+ }
2291
+ if (failCount > 0) {
2292
+ p.log.error(pc.red(`Failed to update ${failCount} skill(s)`));
2293
+ }
2294
+ console.log();
2295
+ p.outro(pc.green("Done!"));
2296
+ }
2297
+ function parseRunTarget(input) {
2298
+ const colonIndex = input.lastIndexOf(":");
2299
+ if (colonIndex <= 0 || colonIndex === input.length - 1) {
2300
+ return null;
2301
+ }
2302
+ return {
2303
+ skill: input.slice(0, colonIndex),
2304
+ command: input.slice(colonIndex + 1)
2305
+ };
2306
+ }
2307
+ async function findSkillDir(skillName) {
2308
+ const { access: fsAccess } = await import("fs/promises");
2309
+ const sanitized = sanitizeName(skillName);
2310
+ const cwd = process.cwd();
2311
+ const projectCanonical = join8(cwd, AGENTS_DIR, SKILLS_SUBDIR, sanitized);
2312
+ try {
2313
+ await fsAccess(join8(projectCanonical, "SKILL.md"));
2314
+ return projectCanonical;
2315
+ } catch {
2316
+ }
2317
+ const commonAgentDirs = [".claude/skills", ".cursor/skills", ".opencode/skills", ".windsurf/skills"];
2318
+ for (const dir of commonAgentDirs) {
2319
+ const agentPath = join8(cwd, dir, sanitized);
2320
+ try {
2321
+ await fsAccess(join8(agentPath, "SKILL.md"));
2322
+ return agentPath;
2323
+ } catch {
2324
+ }
2325
+ }
2326
+ const home2 = homedir6();
2327
+ const globalCanonical = join8(home2, AGENTS_DIR, SKILLS_SUBDIR, sanitized);
2328
+ try {
2329
+ await fsAccess(join8(globalCanonical, "SKILL.md"));
2330
+ return globalCanonical;
2331
+ } catch {
2332
+ }
2333
+ const globalAgentDirs = [".claude/skills", ".cursor/skills", ".opencode/skills"];
2334
+ for (const dir of globalAgentDirs) {
2335
+ const agentPath = join8(home2, dir, sanitized);
2336
+ try {
2337
+ await fsAccess(join8(agentPath, "SKILL.md"));
2338
+ return agentPath;
2339
+ } catch {
2340
+ }
2341
+ }
2342
+ return null;
2343
+ }
2344
+ async function runRun(args) {
2345
+ if (args.length === 0) {
2346
+ console.log(`${RED}Error: Missing run target${RESET}`);
2347
+ console.log(`Usage: askill run <skill>:<command> [args...]`);
2348
+ console.log(`
2349
+ Examples:`);
2350
+ console.log(` askill run my-skill:build`);
2351
+ console.log(` askill run code-stats:analyze -- --path ./src`);
2352
+ console.log(` askill run my-skill:_setup`);
2353
+ process.exit(1);
2354
+ }
2355
+ const target = args[0];
2356
+ const parsed = parseRunTarget(target);
2357
+ if (!parsed) {
2358
+ console.log(`${RED}Error: Invalid run target "${target}"${RESET}`);
2359
+ console.log(`Expected format: ${CYAN}<skill>:<command>${RESET}`);
2360
+ console.log(`Example: askill run my-skill:build`);
2361
+ process.exit(1);
2362
+ }
2363
+ const { skill, command } = parsed;
2364
+ let extraArgs = args.slice(1);
2365
+ if (extraArgs[0] === "--") {
2366
+ extraArgs = extraArgs.slice(1);
2367
+ }
2368
+ const skillDir = await findSkillDir(skill);
2369
+ if (!skillDir) {
2370
+ console.log(`${RED}Error: Skill "${skill}" not found${RESET}`);
2371
+ console.log(`Install it with: ${CYAN}askill add <source>${RESET}`);
2372
+ process.exit(1);
2373
+ }
2374
+ const fs = await import("fs/promises");
2375
+ const skillMdPath = join8(skillDir, "SKILL.md");
2376
+ const content = await fs.readFile(skillMdPath, "utf-8");
2377
+ const { frontmatter } = parseSkillMd(content);
2378
+ if (!frontmatter.commands || Object.keys(frontmatter.commands).length === 0) {
2379
+ console.log(`${RED}Error: Skill "${skill}" does not define any commands${RESET}`);
2380
+ console.log(`${DIM}Check the skill's SKILL.md for available commands${RESET}`);
2381
+ process.exit(1);
2382
+ }
2383
+ const cmdDef = frontmatter.commands[command];
2384
+ if (!cmdDef) {
2385
+ console.log(`${RED}Error: Command "${command}" not found in skill "${skill}"${RESET}`);
2386
+ console.log(`
2387
+ Available commands:`);
2388
+ for (const [name, def] of Object.entries(frontmatter.commands)) {
2389
+ const prefix = name.startsWith("_") ? pc.dim(" (internal) ") : " ";
2390
+ console.log(`${prefix}${pc.cyan(name)} ${pc.dim("\u2014")} ${def.description || "No description"}`);
2391
+ }
2392
+ process.exit(1);
2393
+ }
2394
+ if (!cmdDef.run) {
2395
+ console.log(`${RED}Error: Command "${command}" has no "run" field${RESET}`);
2396
+ process.exit(1);
2397
+ }
2398
+ let shellCmd = cmdDef.run;
2399
+ if (extraArgs.length > 0) {
2400
+ const escapedArgs = extraArgs.map((a) => {
2401
+ if (/[^a-zA-Z0-9_\-.\/=:]/.test(a)) {
2402
+ return `'${a.replace(/'/g, "'\\''")}'`;
2403
+ }
2404
+ return a;
2405
+ });
2406
+ shellCmd += " " + escapedArgs.join(" ");
2407
+ }
2408
+ console.log(`${DIM}$ ${shellCmd}${RESET}`);
2409
+ console.log();
2410
+ const { spawn } = await import("child_process");
2411
+ const child = spawn(shellCmd, {
2412
+ cwd: skillDir,
2413
+ shell: true,
2414
+ stdio: "inherit",
2415
+ env: {
2416
+ ...process.env,
2417
+ ASKILL_SKILL_DIR: skillDir,
2418
+ ASKILL_SKILL_NAME: skill,
2419
+ ASKILL_COMMAND: command
2420
+ }
2421
+ });
2422
+ const exitCode = await new Promise((resolve4) => {
2423
+ child.on("close", (code) => resolve4(code ?? 0));
2424
+ child.on("error", () => resolve4(1));
2425
+ });
2426
+ process.exit(exitCode);
2427
+ }
2428
+ function validateFrontmatter(frontmatter) {
2429
+ const errors = [];
2430
+ const warnings = [];
2431
+ if (!frontmatter.name) {
2432
+ errors.push("Missing required field: name");
2433
+ } else if (typeof frontmatter.name !== "string") {
2434
+ errors.push('Field "name" must be a string');
2435
+ } else if (!/^[a-z0-9-]+$/.test(frontmatter.name)) {
2436
+ errors.push('Field "name" must be lowercase alphanumeric with hyphens only');
2437
+ }
2438
+ if (!frontmatter.description) {
2439
+ errors.push("Missing required field: description");
2440
+ } else if (typeof frontmatter.description !== "string") {
2441
+ errors.push('Field "description" must be a string');
2442
+ } else if (frontmatter.description.length > 200) {
2443
+ warnings.push('Field "description" should be 200 characters or less');
2444
+ }
2445
+ if (frontmatter.version !== void 0) {
2446
+ if (typeof frontmatter.version !== "string") {
2447
+ errors.push('Field "version" must be a string');
2448
+ } else if (!/^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/.test(frontmatter.version)) {
2449
+ errors.push('Field "version" must be valid semver (e.g., 1.0.0, 1.0.0-beta.1)');
2450
+ }
2451
+ } else {
2452
+ warnings.push("Missing optional field: version (recommended)");
2453
+ }
2454
+ if (frontmatter.author !== void 0) {
2455
+ if (typeof frontmatter.author !== "string" && typeof frontmatter.author !== "object") {
2456
+ errors.push('Field "author" must be a string or object');
2457
+ }
2458
+ }
2459
+ if (frontmatter.tags !== void 0) {
2460
+ if (!Array.isArray(frontmatter.tags)) {
2461
+ errors.push('Field "tags" must be an array');
2462
+ } else {
2463
+ for (const tag of frontmatter.tags) {
2464
+ if (typeof tag !== "string") {
2465
+ errors.push("Each tag must be a string");
2466
+ break;
2467
+ }
2468
+ }
2469
+ }
2470
+ }
2471
+ if (frontmatter.dependencies !== void 0) {
2472
+ if (!Array.isArray(frontmatter.dependencies)) {
2473
+ errors.push('Field "dependencies" must be an array');
2474
+ } else {
2475
+ for (const dep of frontmatter.dependencies) {
2476
+ if (typeof dep !== "string") {
2477
+ errors.push("Each dependency must be a string");
2478
+ break;
2479
+ }
2480
+ if (!dep.startsWith("@") && !dep.startsWith("gh:")) {
2481
+ warnings.push(`Dependency "${dep}" should start with @ or gh:`);
2482
+ }
2483
+ }
2484
+ }
2485
+ }
2486
+ if (frontmatter.commands !== void 0) {
2487
+ if (typeof frontmatter.commands !== "object" || frontmatter.commands === null) {
2488
+ errors.push('Field "commands" must be an object');
2489
+ } else {
2490
+ const commands = frontmatter.commands;
2491
+ for (const [cmdName, cmdDef] of Object.entries(commands)) {
2492
+ if (typeof cmdDef !== "object" || cmdDef === null) {
2493
+ errors.push(`Command "${cmdName}" must be an object`);
2494
+ continue;
2495
+ }
2496
+ const def = cmdDef;
2497
+ if (!def.run) {
2498
+ errors.push(`Command "${cmdName}" is missing required field: run`);
2499
+ } else if (typeof def.run !== "string") {
2500
+ errors.push(`Command "${cmdName}.run" must be a string`);
2501
+ }
2502
+ if (!def.description) {
2503
+ warnings.push(`Command "${cmdName}" is missing description`);
2504
+ } else if (typeof def.description !== "string") {
2505
+ errors.push(`Command "${cmdName}.description" must be a string`);
2506
+ }
2507
+ }
2508
+ }
2509
+ }
2510
+ return {
2511
+ valid: errors.length === 0,
2512
+ errors,
2513
+ warnings
2514
+ };
2515
+ }
2516
+ async function runValidate(args) {
2517
+ let targetPath = args.find((a) => !a.startsWith("-")) || "SKILL.md";
2518
+ if (!targetPath.endsWith("SKILL.md")) {
2519
+ targetPath = join8(targetPath, "SKILL.md");
2520
+ }
2521
+ const absolutePath = join8(process.cwd(), targetPath);
2522
+ console.log();
2523
+ p.intro(pc.bgCyan(pc.black(" askill validate ")));
2524
+ const spinner2 = p.spinner();
2525
+ spinner2.start(`Checking ${targetPath}...`);
2526
+ const fs = await import("fs/promises");
2527
+ try {
2528
+ await fs.access(absolutePath);
2529
+ } catch {
2530
+ spinner2.stop(pc.red("File not found"));
2531
+ p.log.error(`Cannot find ${pc.cyan(targetPath)}`);
2532
+ p.outro(pc.red("Validation failed"));
2533
+ process.exit(1);
2534
+ }
2535
+ let content;
2536
+ try {
2537
+ content = await fs.readFile(absolutePath, "utf-8");
2538
+ } catch (error) {
2539
+ spinner2.stop(pc.red("Read error"));
2540
+ p.log.error(`Cannot read file: ${error instanceof Error ? error.message : "Unknown error"}`);
2541
+ p.outro(pc.red("Validation failed"));
2542
+ process.exit(1);
2543
+ }
2544
+ const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
2545
+ if (!frontmatterMatch) {
2546
+ spinner2.stop(pc.red("Invalid format"));
2547
+ p.log.error("SKILL.md must start with YAML frontmatter (--- ... ---)");
2548
+ p.outro(pc.red("Validation failed"));
2549
+ process.exit(1);
2550
+ }
2551
+ spinner2.stop("Parsing...");
2552
+ const { frontmatter } = parseSkillMd(content);
2553
+ const result = validateFrontmatter(frontmatter);
2554
+ console.log();
2555
+ if (result.errors.length > 0) {
2556
+ for (const error of result.errors) {
2557
+ console.log(` ${pc.red("\u2717")} ${error}`);
2558
+ }
2559
+ }
2560
+ if (result.warnings.length > 0) {
2561
+ for (const warning of result.warnings) {
2562
+ console.log(` ${pc.yellow("!")} ${warning}`);
2563
+ }
2564
+ }
2565
+ const checks = [
2566
+ { name: "Frontmatter is valid YAML", passed: true },
2567
+ // Already parsed
2568
+ { name: "Required field: name", passed: !!frontmatter.name && typeof frontmatter.name === "string" },
2569
+ { name: "Required field: description", passed: !!frontmatter.description && typeof frontmatter.description === "string" }
2570
+ ];
2571
+ if (frontmatter.version) {
2572
+ const versionValid = typeof frontmatter.version === "string" && /^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/.test(frontmatter.version);
2573
+ checks.push({ name: `Version format: ${frontmatter.version}`, passed: versionValid });
2574
+ }
2575
+ if (frontmatter.dependencies && Array.isArray(frontmatter.dependencies)) {
2576
+ checks.push({ name: `Dependencies: ${frontmatter.dependencies.length} defined`, passed: true });
2577
+ }
2578
+ if (frontmatter.commands && typeof frontmatter.commands === "object") {
2579
+ const cmdCount = Object.keys(frontmatter.commands).length;
2580
+ checks.push({ name: `Commands: ${cmdCount} defined`, passed: cmdCount > 0 });
2581
+ }
2582
+ if (result.errors.length === 0) {
2583
+ for (const check of checks) {
2584
+ if (check.passed) {
2585
+ console.log(` ${pc.green("\u2713")} ${check.name}`);
2586
+ }
2587
+ }
2588
+ }
2589
+ console.log();
2590
+ if (result.valid) {
2591
+ if (result.warnings.length > 0) {
2592
+ p.outro(pc.yellow(`Valid with ${result.warnings.length} warning(s)`));
2593
+ } else {
2594
+ p.outro(pc.green("Ready to publish!"));
2595
+ }
2596
+ } else {
2597
+ p.outro(pc.red(`Validation failed: ${result.errors.length} error(s)`));
2598
+ process.exit(1);
2599
+ }
2600
+ }
2601
+ async function runInit(args) {
2602
+ const targetDir = args.find((a) => !a.startsWith("-")) || ".";
2603
+ const isYes = args.includes("-y") || args.includes("--yes");
2604
+ console.log();
2605
+ p.intro(pc.bgCyan(pc.black(" askill init ")));
2606
+ const skillPath = join8(process.cwd(), targetDir, "SKILL.md");
2607
+ try {
2608
+ await import("fs").then((fs2) => fs2.promises.access(skillPath));
2609
+ p.log.error(`SKILL.md already exists at ${pc.cyan(skillPath)}`);
2610
+ p.outro(pc.red("Aborted"));
2611
+ return;
2612
+ } catch {
2613
+ }
2614
+ let name;
2615
+ let description;
2616
+ let version;
2617
+ let author;
2618
+ let tags;
2619
+ if (isYes) {
2620
+ const dirName = targetDir === "." ? process.cwd().split("/").pop() || "my-skill" : targetDir;
2621
+ name = dirName;
2622
+ description = "A new askill skill";
2623
+ version = "0.1.0";
2624
+ author = "";
2625
+ tags = [];
2626
+ } else {
2627
+ const dirName = targetDir === "." ? process.cwd().split("/").pop() || "my-skill" : targetDir;
2628
+ const nameResult = await p.text({
2629
+ message: "Skill name",
2630
+ placeholder: dirName,
2631
+ defaultValue: dirName,
2632
+ validate: (value) => {
2633
+ if (!value) return "Name is required";
2634
+ if (!/^[a-z0-9-]+$/.test(value)) return "Name must be lowercase alphanumeric with hyphens";
2635
+ return void 0;
2636
+ }
2637
+ });
2638
+ if (p.isCancel(nameResult)) {
2639
+ p.cancel("Init cancelled");
2640
+ return;
2641
+ }
2642
+ name = nameResult;
2643
+ const descResult = await p.text({
2644
+ message: "Description",
2645
+ placeholder: "What does this skill do?",
2646
+ validate: (value) => {
2647
+ if (!value) return "Description is required";
2648
+ if (value.length > 200) return "Description must be 200 characters or less";
2649
+ return void 0;
2650
+ }
2651
+ });
2652
+ if (p.isCancel(descResult)) {
2653
+ p.cancel("Init cancelled");
2654
+ return;
2655
+ }
2656
+ description = descResult;
2657
+ const versionResult = await p.text({
2658
+ message: "Version",
2659
+ placeholder: "0.1.0",
2660
+ defaultValue: "0.1.0",
2661
+ validate: (value) => {
2662
+ if (!value) return "Version is required";
2663
+ if (!/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(value)) return "Must be valid semver (e.g., 0.1.0)";
2664
+ return void 0;
2665
+ }
2666
+ });
2667
+ if (p.isCancel(versionResult)) {
2668
+ p.cancel("Init cancelled");
2669
+ return;
2670
+ }
2671
+ version = versionResult;
2672
+ const authorResult = await p.text({
2673
+ message: "Author (GitHub username)",
2674
+ placeholder: "your-username"
2675
+ });
2676
+ if (p.isCancel(authorResult)) {
2677
+ p.cancel("Init cancelled");
2678
+ return;
2679
+ }
2680
+ author = authorResult || "";
2681
+ const tagsResult = await p.text({
2682
+ message: "Tags (comma-separated)",
2683
+ placeholder: "automation, productivity"
2684
+ });
2685
+ if (p.isCancel(tagsResult)) {
2686
+ p.cancel("Init cancelled");
2687
+ return;
2688
+ }
2689
+ tags = tagsResult.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
2690
+ }
2691
+ let content = "---\n";
2692
+ content += `name: ${name}
2693
+ `;
2694
+ content += `description: ${description}
2695
+ `;
2696
+ content += `version: ${version}
2697
+ `;
2698
+ if (author) {
2699
+ content += `author: ${author}
2700
+ `;
2701
+ }
2702
+ if (tags.length > 0) {
2703
+ content += `tags:
2704
+ `;
2705
+ for (const tag of tags) {
2706
+ content += ` - ${tag}
2707
+ `;
2708
+ }
2709
+ }
2710
+ content += "---\n\n";
2711
+ content += `# ${name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}
2712
+
2713
+ `;
2714
+ content += `${description}
2715
+
2716
+ `;
2717
+ content += `## Usage
2718
+
2719
+ `;
2720
+ content += `Explain how an AI agent should use this skill...
2721
+
2722
+ `;
2723
+ content += `## Examples
2724
+
2725
+ `;
2726
+ content += `Provide concrete examples of when and how to use this skill.
2727
+ `;
2728
+ const targetPath = join8(process.cwd(), targetDir);
2729
+ if (targetDir !== ".") {
2730
+ const fs2 = await import("fs");
2731
+ await fs2.promises.mkdir(targetPath, { recursive: true });
2732
+ }
2733
+ const fs = await import("fs");
2734
+ await fs.promises.writeFile(skillPath, content, "utf-8");
2735
+ p.log.success(`Created ${pc.cyan(skillPath)}`);
2736
+ console.log();
2737
+ console.log(pc.dim("Next steps:"));
2738
+ console.log(` 1. Edit ${pc.cyan("SKILL.md")} to add instructions`);
2739
+ console.log(` 2. Optionally add ${pc.cyan("scripts/")} for commands`);
2740
+ console.log(` 3. Test locally: ${pc.cyan(`askill add ./${targetDir === "." ? "" : targetDir}`)}`);
2741
+ console.log();
2742
+ p.outro(pc.green("Done!"));
2743
+ }
2744
+ async function main() {
2745
+ const args = process.argv.slice(2);
2746
+ checkForUpdates().catch(() => {
2747
+ });
2748
+ if (args.length === 0) {
2749
+ showBanner();
2750
+ return;
2751
+ }
2752
+ const command = args[0];
2753
+ const restArgs = args.slice(1);
2754
+ switch (command) {
2755
+ case "install":
2756
+ case "i":
2757
+ case "add":
2758
+ await runInstall(restArgs);
2759
+ break;
2760
+ case "search":
2761
+ case "s":
2762
+ case "find":
2763
+ await runSearch(restArgs);
2764
+ break;
2765
+ case "list":
2766
+ case "ls":
2767
+ await runList(restArgs);
2768
+ break;
2769
+ case "remove":
2770
+ case "rm":
2771
+ case "uninstall":
2772
+ await runRemove(restArgs);
2773
+ break;
2774
+ case "info":
2775
+ case "show":
2776
+ await runInfo(restArgs);
2777
+ break;
2778
+ case "check":
2779
+ await runCheck(restArgs);
2780
+ break;
2781
+ case "update":
2782
+ case "upgrade":
2783
+ await runUpdate(restArgs);
2784
+ break;
2785
+ case "self-update":
2786
+ await selfUpdate();
2787
+ break;
2788
+ case "run":
2789
+ await runRun(restArgs);
2790
+ break;
2791
+ case "validate":
2792
+ await runValidate(restArgs);
2793
+ break;
2794
+ case "init":
2795
+ await runInit(restArgs);
2796
+ break;
2797
+ case "--help":
2798
+ case "-h":
2799
+ case "help":
2800
+ showHelp();
2801
+ break;
2802
+ case "--version":
2803
+ case "-v":
2804
+ case "version":
2805
+ console.log(VERSION);
2806
+ break;
2807
+ default:
2808
+ console.log(`${RED}Unknown command: ${command}${RESET}`);
2809
+ console.log(`Run ${CYAN}askill --help${RESET} for usage.`);
2810
+ process.exit(1);
2811
+ }
2812
+ }
2813
+ main().catch((error) => {
2814
+ console.error(`${RED}Error: ${error.message}${RESET}`);
2815
+ process.exit(1);
2816
+ });