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.
- package/LICENSE +21 -0
- package/README.md +254 -0
- package/dist/cli.mjs +2816 -0
- 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
|
+
});
|