add-skill-kit 3.2.0 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,208 @@
1
+ /**
2
+ * @fileoverview Agent definitions and detection
3
+ * Based on Vercel's agent-skills CLI structure
4
+ */
5
+
6
+ import { existsSync } from "fs";
7
+ import { homedir } from "os";
8
+ import { join } from "path";
9
+
10
+ const home = homedir();
11
+
12
+ // Environment-based paths
13
+ const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
14
+ const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
15
+
16
+ /**
17
+ * @typedef {Object} AgentConfig
18
+ * @property {string} name - Internal agent ID
19
+ * @property {string} displayName - Display name for UI
20
+ * @property {string} skillsDir - Project-level skills directory
21
+ * @property {string} globalSkillsDir - Global skills directory
22
+ * @property {() => boolean} detect - Detection function
23
+ */
24
+
25
+ /**
26
+ * All supported agents with detection logic
27
+ * @type {Record<string, AgentConfig>}
28
+ */
29
+ export const AGENTS = {
30
+ antigravity: {
31
+ name: "antigravity",
32
+ displayName: "Antigravity",
33
+ skillsDir: ".agent/skills",
34
+ globalSkillsDir: join(home, ".gemini/antigravity/global_skills"),
35
+ detect: () => existsSync(join(process.cwd(), ".agent")) || existsSync(join(home, ".gemini/antigravity"))
36
+ },
37
+ "claude-code": {
38
+ name: "claude-code",
39
+ displayName: "Claude Code",
40
+ skillsDir: ".claude/skills",
41
+ globalSkillsDir: join(claudeHome, "skills"),
42
+ detect: () => existsSync(claudeHome)
43
+ },
44
+ codex: {
45
+ name: "codex",
46
+ displayName: "Codex",
47
+ skillsDir: ".codex/skills",
48
+ globalSkillsDir: join(codexHome, "skills"),
49
+ detect: () => existsSync(codexHome) || existsSync("/etc/codex")
50
+ },
51
+ "gemini-cli": {
52
+ name: "gemini-cli",
53
+ displayName: "Gemini CLI",
54
+ skillsDir: ".gemini/skills",
55
+ globalSkillsDir: join(home, ".gemini/skills"),
56
+ detect: () => existsSync(join(home, ".gemini"))
57
+ },
58
+ "github-copilot": {
59
+ name: "github-copilot",
60
+ displayName: "GitHub Copilot",
61
+ skillsDir: ".github/skills",
62
+ globalSkillsDir: join(home, ".copilot/skills"),
63
+ detect: () => existsSync(join(process.cwd(), ".github")) || existsSync(join(home, ".copilot"))
64
+ },
65
+ windsurf: {
66
+ name: "windsurf",
67
+ displayName: "Windsurf",
68
+ skillsDir: ".windsurf/skills",
69
+ globalSkillsDir: join(home, ".codeium/windsurf/skills"),
70
+ detect: () => existsSync(join(home, ".codeium/windsurf"))
71
+ },
72
+ cursor: {
73
+ name: "cursor",
74
+ displayName: "Cursor",
75
+ skillsDir: ".cursor/skills",
76
+ globalSkillsDir: join(home, ".cursor/skills"),
77
+ detect: () => existsSync(join(home, ".cursor"))
78
+ },
79
+ cline: {
80
+ name: "cline",
81
+ displayName: "Cline",
82
+ skillsDir: ".cline/skills",
83
+ globalSkillsDir: join(home, ".cline/skills"),
84
+ detect: () => existsSync(join(home, ".cline"))
85
+ },
86
+ roo: {
87
+ name: "roo",
88
+ displayName: "Roo Code",
89
+ skillsDir: ".roo/skills",
90
+ globalSkillsDir: join(home, ".roo/skills"),
91
+ detect: () => existsSync(join(home, ".roo"))
92
+ },
93
+ continue: {
94
+ name: "continue",
95
+ displayName: "Continue",
96
+ skillsDir: ".continue/skills",
97
+ globalSkillsDir: join(home, ".continue/skills"),
98
+ detect: () => existsSync(join(process.cwd(), ".continue")) || existsSync(join(home, ".continue"))
99
+ },
100
+ goose: {
101
+ name: "goose",
102
+ displayName: "Goose",
103
+ skillsDir: ".goose/skills",
104
+ globalSkillsDir: join(home, ".config/goose/skills"),
105
+ detect: () => existsSync(join(home, ".config/goose"))
106
+ },
107
+ trae: {
108
+ name: "trae",
109
+ displayName: "Trae",
110
+ skillsDir: ".trae/skills",
111
+ globalSkillsDir: join(home, ".trae/skills"),
112
+ detect: () => existsSync(join(home, ".trae"))
113
+ },
114
+ kilo: {
115
+ name: "kilo",
116
+ displayName: "Kilo Code",
117
+ skillsDir: ".kilocode/skills",
118
+ globalSkillsDir: join(home, ".kilocode/skills"),
119
+ detect: () => existsSync(join(home, ".kilocode"))
120
+ },
121
+ opencode: {
122
+ name: "opencode",
123
+ displayName: "OpenCode",
124
+ skillsDir: ".opencode/skills",
125
+ globalSkillsDir: join(home, ".config/opencode/skills"),
126
+ detect: () => existsSync(join(home, ".config/opencode"))
127
+ },
128
+ amp: {
129
+ name: "amp",
130
+ displayName: "Amp",
131
+ skillsDir: ".agents/skills",
132
+ globalSkillsDir: join(home, ".config/agents/skills"),
133
+ detect: () => existsSync(join(home, ".config/amp"))
134
+ },
135
+ junie: {
136
+ name: "junie",
137
+ displayName: "Junie",
138
+ skillsDir: ".junie/skills",
139
+ globalSkillsDir: join(home, ".junie/skills"),
140
+ detect: () => existsSync(join(home, ".junie"))
141
+ },
142
+ "kiro-cli": {
143
+ name: "kiro-cli",
144
+ displayName: "Kiro CLI",
145
+ skillsDir: ".kiro/skills",
146
+ globalSkillsDir: join(home, ".kiro/skills"),
147
+ detect: () => existsSync(join(home, ".kiro"))
148
+ },
149
+ zencoder: {
150
+ name: "zencoder",
151
+ displayName: "Zencoder",
152
+ skillsDir: ".zencoder/skills",
153
+ globalSkillsDir: join(home, ".zencoder/skills"),
154
+ detect: () => existsSync(join(home, ".zencoder"))
155
+ },
156
+ openhands: {
157
+ name: "openhands",
158
+ displayName: "OpenHands",
159
+ skillsDir: ".openhands/skills",
160
+ globalSkillsDir: join(home, ".openhands/skills"),
161
+ detect: () => existsSync(join(home, ".openhands"))
162
+ },
163
+ "qwen-code": {
164
+ name: "qwen-code",
165
+ displayName: "Qwen Code",
166
+ skillsDir: ".qwen/skills",
167
+ globalSkillsDir: join(home, ".qwen/skills"),
168
+ detect: () => existsSync(join(home, ".qwen"))
169
+ }
170
+ };
171
+
172
+ /**
173
+ * Detect all installed agents on the system
174
+ * @returns {Array<{name: string, displayName: string, skillsDir: string, globalSkillsDir: string}>}
175
+ */
176
+ export function detectInstalledAgents() {
177
+ const detected = [];
178
+
179
+ for (const [key, config] of Object.entries(AGENTS)) {
180
+ if (config.detect()) {
181
+ detected.push({
182
+ name: config.name,
183
+ displayName: config.displayName,
184
+ skillsDir: config.skillsDir,
185
+ globalSkillsDir: config.globalSkillsDir
186
+ });
187
+ }
188
+ }
189
+
190
+ return detected;
191
+ }
192
+
193
+ /**
194
+ * Get agent config by name
195
+ * @param {string} name - Agent name
196
+ * @returns {AgentConfig | undefined}
197
+ */
198
+ export function getAgentConfig(name) {
199
+ return AGENTS[name];
200
+ }
201
+
202
+ /**
203
+ * Get all agent names
204
+ * @returns {string[]}
205
+ */
206
+ export function getAllAgentNames() {
207
+ return Object.keys(AGENTS);
208
+ }
@@ -130,14 +130,34 @@ export async function run(spec) {
130
130
  if (skillsDir) {
131
131
  for (const e of fs.readdirSync(skillsDir)) {
132
132
  const sp = path.join(skillsDir, e);
133
- if (fs.statSync(sp).isDirectory() && fs.existsSync(path.join(sp, "SKILL.md"))) {
134
- const m = parseSkillMdFrontmatter(path.join(sp, "SKILL.md"));
135
- skillsInRepo.push({
136
- title: e + (m.description ? c.dim(` (${m.description.substring(0, 40)}...)`) : ""),
137
- value: e,
138
- selected: singleSkill ? e === singleSkill : true,
139
- _path: sp // Store actual path for later use
140
- });
133
+ if (fs.statSync(sp).isDirectory()) {
134
+ // Check if this directory has SKILL.md (top-level skill)
135
+ if (fs.existsSync(path.join(sp, "SKILL.md"))) {
136
+ const m = parseSkillMdFrontmatter(path.join(sp, "SKILL.md"));
137
+ skillsInRepo.push({
138
+ title: e, // Only show folder name
139
+ value: e,
140
+ description: m.description || "",
141
+ selected: singleSkill ? e === singleSkill : true,
142
+ _path: sp
143
+ });
144
+ }
145
+
146
+ // Also scan nested directories (2 levels deep) for sub-skills
147
+ for (const sub of fs.readdirSync(sp)) {
148
+ const subPath = path.join(sp, sub);
149
+ if (fs.statSync(subPath).isDirectory() && fs.existsSync(path.join(subPath, "SKILL.md"))) {
150
+ const m = parseSkillMdFrontmatter(path.join(subPath, "SKILL.md"));
151
+ const skillName = `${e}/${sub}`; // e.g. "game-development/2d-games"
152
+ skillsInRepo.push({
153
+ title: skillName,
154
+ value: skillName,
155
+ description: m.description || "",
156
+ selected: singleSkill ? skillName === singleSkill : true,
157
+ _path: subPath
158
+ });
159
+ }
160
+ }
141
161
  }
142
162
  }
143
163
  }
@@ -167,177 +187,167 @@ export async function run(spec) {
167
187
  stepLine();
168
188
  step(`Auto-selected: ${c.cyan(singleSkill)}`);
169
189
  } else {
170
- // Show selection prompt for multiple skills
171
- stepLine();
190
+ // Group skills by category - 5 main categories (all skills covered, no Other)
191
+ // NOTE: Order matters! Mobile is before Security so mobile-security-coder goes to Mobile
192
+ const CATEGORY_KEYWORDS = {
193
+ "⚙️ Backend & API": [
194
+ // Core backend
195
+ "backend", "api", "nodejs", "server", "database", "prisma", "mcp", "python", "cache",
196
+ // Architecture & patterns
197
+ "architecture", "pattern", "app-builder", "performance", "profiling",
198
+ // Testing & quality
199
+ "testing", "test", "tdd", "lint", "validate", "debugging", "systematic", "debug",
200
+ "code-review", "code-quality", "e2e", "integration", "webapp", "checklist", "reviewer", "clean",
201
+ // Documentation & tools
202
+ "documentation", "plan", "writing", "geo", "seo", "i18n", "localization", "template",
203
+ // CLI & scripting
204
+ "bash", "linux", "powershell", "windows", "shell", "script",
205
+ // AI & automation
206
+ "agent", "routing", "brainstorm", "behavioral", "intelligent", "parallel", "modes",
207
+ // Other skills
208
+ "typescript", "problem", "workflow", "builder", "conventions", "fundamentals",
209
+ "best-practices", "procedures", "management"
210
+ ],
211
+ "🎨 Frontend & UI": ["frontend", "nextjs", "tailwind", "css", "ui", "ux", "visual", "studio", "web-core", "design-system", "react-architect", "react"],
212
+ "🎮 Game Development": ["game", "development", "engine", "unity", "unreal", "godot", "phaser", "game-dev"],
213
+ "📱 Mobile": ["mobile", "first", "developer", "react-native", "flutter", "ios", "android", "swift", "kotlin"],
214
+ "🔒 Security & DevOps": ["security", "vulnerability", "deploy", "git", "docker", "red-team", "governance", "offensive", "gitops", "cicd", "pipeline", "incident", "chaos", "scanner"]
215
+ };
216
+
217
+ function categorizeSkill(skillName) {
218
+ const lower = skillName.toLowerCase();
219
+ for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) {
220
+ if (keywords.some(kw => lower.includes(kw))) {
221
+ return category;
222
+ }
223
+ }
224
+ return "⚙️ Backend & API"; // Default fallback (no "Other" category)
225
+ }
226
+
227
+ // Group skills by category
228
+ const grouped = {};
229
+ for (const skill of skillsInRepo) {
230
+ const cat = categorizeSkill(skill.value);
231
+ if (!grouped[cat]) grouped[cat] = [];
232
+ grouped[cat].push(skill);
233
+ }
234
+
235
+ // Custom sort: alphabetical but "Other" always last
236
+ const sortedCategories = Object.keys(grouped).sort((a, b) => {
237
+ if (a.includes("Other")) return 1;
238
+ if (b.includes("Other")) return -1;
239
+ return a.localeCompare(b);
240
+ });
172
241
 
173
- // Active Step: Selection
174
- // Note: activeStep already prints the icon. Clack prompts typically print their own message.
175
- // But we want to enforce the tree style.
176
- // Clack's multiselect prints: "? Message"
177
- // We want: "◆ Select skills to install"
178
- // We can print "activeStep" line, and then let clack print below?
179
- // Or we use clack's message but Clack doesn't support custom icons well in the prompt line itself easily without patches.
180
- // We will print the active step line manually, and set clack message to something minimal or empty if possible, or just repeat it but we want the "◆".
181
- // Let's print activeStep("Select skills to install") and then run multiselect with a simpler message like " " or hidden?
182
- // Clack behaves best with a message. Let's try printing the header and then the prompt.
183
242
 
184
- activeStep("Select skills to install");
243
+ stepLine();
244
+ activeStep("Select skill categories to install");
185
245
 
186
- const skills = await multiselect({
246
+ // Show only categories, not individual skills
247
+ const selectedCategories = await multiselect({
187
248
  message: `${c.cyan("space")} select · ${c.cyan("enter")} confirm`,
188
- options: skillsInRepo.map(s => ({
189
- label: s.title,
190
- value: s.value
249
+ options: sortedCategories.map(cat => ({
250
+ label: `${cat} (${grouped[cat].length} skills)`,
251
+ value: cat,
252
+ hint: grouped[cat].slice(0, 3).map(s => s.value).join(", ") + (grouped[cat].length > 3 ? "..." : "")
191
253
  })),
192
- initialValues: skillsInRepo.map(s => s.value), // Pre-select all
254
+ initialValues: sortedCategories, // Pre-select all
193
255
  required: true
194
256
  });
195
257
 
196
- if (isCancel(skills)) {
258
+ if (isCancel(selectedCategories)) {
197
259
  cancel("Cancelled.");
198
260
  fs.rmSync(tmp, { recursive: true, force: true });
199
261
  return;
200
262
  }
201
263
 
202
- selectedSkills = skills;
264
+ // Get all skills from selected categories
265
+ selectedSkills = selectedCategories.flatMap(cat => grouped[cat].map(s => s.value));
203
266
  }
204
267
 
205
- // Ensure newline after prompt to fix tree alignment
268
+
269
+
206
270
  stepLine();
207
271
  step("Select skills to install");
208
272
  console.log(`${c.gray(S.branch)} ${c.dim(selectedSkills.join(", "))}`);
209
273
 
210
- // --- NEW: Scope Selection ---
274
+ // --- Detect installed agents ---
275
+ stepLine();
276
+ const { detectInstalledAgents } = await import("../agents.js");
277
+ const detectedAgents = detectInstalledAgents();
211
278
 
212
- // Determine installation scope
213
- let isGlobal = false;
279
+ if (detectedAgents.length === 0) {
280
+ step(c.yellow("No agents detected"), S.diamond, "yellow");
281
+ step(c.dim("Please install at least one AI agent (Antigravity, Claude Code, etc.)"), S.branch, "gray");
282
+ fs.rmSync(tmp, { recursive: true, force: true });
283
+ return;
284
+ }
214
285
 
215
- if (GLOBAL) {
216
- isGlobal = true;
217
- } else {
218
- activeStep("Select installation scope");
219
-
220
- const scopeSelection = await select({
221
- message: " ",
222
- options: [
223
- {
224
- label: "Current Project",
225
- value: "local",
226
- hint: "Installs to .agent/ (Best for project-specific skills)"
227
- },
228
- {
229
- label: "Global System",
230
- value: "global",
231
- hint: "Installs to ~/.gemini/ (Available to all projects via 'agent' command)"
232
- }
233
- ],
234
- initialValue: "local"
235
- });
286
+ step(`Detected ${detectedAgents.length} agents`);
236
287
 
237
- if (isCancel(scopeSelection)) {
238
- cancel("Cancelled.");
239
- fs.rmSync(tmp, { recursive: true, force: true });
240
- return;
241
- }
288
+ // --- Select agents (Vercel-style) ---
289
+ const { selectAgentsPrompt, selectScopePrompt, selectMethodPrompt } = await import("../ui.js");
290
+
291
+ stepLine();
292
+ activeStep("Install to");
293
+ const selectedAgents = await selectAgentsPrompt(detectedAgents);
242
294
 
243
- isGlobal = scopeSelection === "global";
295
+ if (!selectedAgents || selectedAgents.length === 0) {
296
+ fs.rmSync(tmp, { recursive: true, force: true });
297
+ return;
244
298
  }
245
- const targetScope = isGlobal ? GLOBAL_DIR : WORKSPACE;
246
299
 
247
300
  stepLine();
248
- step("Installation scope");
249
- console.log(`${c.gray(S.branch)} ${c.dim(isGlobal ? "Global System" : "Current Project")}`);
250
-
251
- // Agent selection - currently only Antigravity supported
252
- const availableAgents = [
253
- { label: "Antigravity (.agent/skills)", value: "antigravity", available: true },
254
- { label: "Claude Code (coming soon)", value: "claude", available: false },
255
- { label: "Codex (coming soon)", value: "codex", available: false },
256
- { label: "Gemini CLI (coming soon)", value: "gemini", available: false },
257
- { label: "Windsurf (coming soon)", value: "windsurf", available: false }
258
- ];
259
- const activeAgentCount = availableAgents.filter(a => a.available).length;
260
- stepLine();
261
- step(`Detected ${availableAgents.length} agents (${activeAgentCount} available)`);
301
+ step("Install to");
302
+ console.log(`${c.gray(S.branch)} ${c.dim(selectedAgents.map(a => a.displayName).join(", "))}`);
262
303
 
263
- let agents;
264
- while (true) {
265
- // Active Step
266
- activeStep("Select agents to install skills to (Antigravity only)");
304
+ // --- Select installation scope ---
305
+ let isGlobal = GLOBAL;
267
306
 
268
- agents = await multiselect({
269
- message: `${c.cyan("space")} select · ${c.cyan("enter")} confirm`,
270
- options: availableAgents.map(a => ({
271
- label: a.available ? a.label : c.dim(a.label),
272
- value: a.value
273
- })),
274
- initialValues: ["antigravity"],
275
- required: true
276
- });
307
+ if (!GLOBAL) {
308
+ stepLine();
309
+ activeStep("Installation scope");
310
+ const scope = await selectScopePrompt();
277
311
 
278
- if (isCancel(agents)) {
279
- cancel("Cancelled.");
312
+ if (!scope) {
280
313
  fs.rmSync(tmp, { recursive: true, force: true });
281
314
  return;
282
315
  }
283
316
 
284
- const invalidAgents = agents.filter(a => ["claude", "codex", "gemini", "windsurf"].includes(a));
285
- if (invalidAgents.length > 0) {
286
- step(`Selection contains coming soon agents. Only Antigravity is currently supported.`, S.cross, "red");
287
- stepLine();
288
- continue;
289
- }
290
-
291
- break;
292
- }
293
-
294
- if (!agents || agents.length === 0) {
295
- console.log(`\n ${c.yellow("No agents selected.")}`);
296
- fs.rmSync(tmp, { recursive: true, force: true });
297
- return;
317
+ isGlobal = scope === "global";
298
318
  }
299
319
 
300
- // Agent summary
301
320
  stepLine();
302
- step("Select agents to install skills to");
303
- console.log(`${c.gray(S.branch)} ${c.dim(Array.isArray(agents) ? agents.join(", ") : agents)}`);
321
+ step("Installation scope");
322
+ console.log(`${c.gray(S.branch)} ${c.dim(isGlobal ? "Global" : "Project")}`);
304
323
 
305
- // Installation method
324
+ // --- Select installation method ---
325
+ stepLine();
306
326
  activeStep("Installation method");
327
+ const installMethod = await selectMethodPrompt();
307
328
 
308
- const installMethod = await select({
309
- message: " ",
310
- options: [
311
- { label: "Symlink (Recommended)", value: "symlink", hint: "Single source of truth, easy updates" },
312
- { label: "Copy to all agents", value: "copy" }
313
- ],
314
- initialValue: "symlink"
315
- });
316
-
317
- if (isCancel(installMethod)) {
318
- cancel("Cancelled.");
329
+ if (!installMethod) {
319
330
  fs.rmSync(tmp, { recursive: true, force: true });
320
331
  return;
321
332
  }
322
333
 
334
+
323
335
  // Installation Summary Box
324
336
  stepLine();
325
- step("Installation Summary"); // Passive header for box
337
+ step("Installation method");
338
+ console.log(`${c.gray(S.branch)} ${c.dim(installMethod === "symlink" ? "Symlink" : "Copy")}`);
339
+
340
+ stepLine();
341
+ step("Installation Summary");
326
342
  stepLine();
327
343
 
328
- const selectedAgentsList = Array.isArray(agents) ? agents : [agents];
329
- const agentsString = selectedAgentsList.map(a =>
330
- a === "antigravity" ? "Antigravity" :
331
- a.charAt(0).toUpperCase() + a.slice(1)
332
- ).join(", ");
344
+ const agentsString = selectedAgents.map(a => a.displayName).join(", ");
333
345
 
334
346
  let summaryContent = "";
335
347
  const methodVerb = installMethod === "symlink" ? "symlink" : "copy";
336
348
 
337
349
  for (const sn of selectedSkills) {
338
- // Show actual target path
339
- const targetPath = path.relative(process.cwd(), path.join(targetScope, sn));
340
- summaryContent += `${c.cyan(targetPath)}\n`;
350
+ summaryContent += `${c.cyan(sn)}\n`;
341
351
  summaryContent += ` ${c.dim(methodVerb)} ${c.gray("→")} ${c.dim(agentsString)}\n\n`;
342
352
  }
343
353
 
@@ -356,7 +366,6 @@ export async function run(spec) {
356
366
  stepLine();
357
367
 
358
368
  // Confirmation
359
- // Active Step
360
369
  activeStep("Proceed with installation?");
361
370
  const shouldProceed = await confirm({ message: " ", initialValue: true });
362
371
 
@@ -366,29 +375,51 @@ export async function run(spec) {
366
375
  return;
367
376
  }
368
377
 
369
- // Install
378
+ // Install skills to multiple agents
370
379
  stepLine();
371
- fs.mkdirSync(targetScope, { recursive: true });
380
+ const { installSkillForAgents } = await import("../installer.js");
372
381
 
373
382
  // Create a map for skill paths
374
383
  const skillPathMap = Object.fromEntries(skillsInRepo.map(s => [s.value, s._path]));
375
384
 
385
+ const installResults = { success: [], failed: [] };
386
+
376
387
  for (const sn of selectedSkills) {
377
388
  const src = skillPathMap[sn] || path.join(skillsDir || tmp, sn);
378
- const dest = path.join(targetScope, sn);
379
389
 
380
- await installSkill(src, dest, installMethod, {
381
- repo: `${org}/${repo}`,
382
- skill: sn,
383
- ref: ref || null
390
+ const is = spinner();
391
+ is.start(`Installing ${sn} to ${selectedAgents.length} agents`);
392
+
393
+ const result = await installSkillForAgents(src, sn, selectedAgents, {
394
+ method: installMethod,
395
+ scope: isGlobal ? "global" : "project",
396
+ metadata: {
397
+ repo: `${org}/${repo}`,
398
+ ref: ref || null
399
+ }
384
400
  });
401
+
402
+ installResults.success.push(...result.success);
403
+ installResults.failed.push(...result.failed);
404
+
405
+ if (result.failed.length === 0) {
406
+ is.stop(`Installed ${sn} (${result.success.length} agents)`);
407
+ } else {
408
+ is.stop(`${sn}: ${result.success.length} success, ${result.failed.length} failed`);
409
+ }
385
410
  }
386
411
 
412
+
413
+ // Derive base .agent directory from skillsDir
414
+ // If skillsDir is .../skills, then baseAgentDir is parent (.agent)
415
+ const baseAgentDir = skillsDir ? path.dirname(skillsDir) : path.join(tmp, ".agent");
416
+
387
417
  // Install workflows if they exist
388
- const workflowsDir = path.join(tmp, ".agent", "workflows");
418
+ const workflowsDir = path.join(baseAgentDir, "workflows");
389
419
  const targetWorkflowsDir = path.join(WORKSPACE, "..", "workflows");
390
420
  let workflowsInstalled = 0;
391
421
 
422
+
392
423
  if (fs.existsSync(workflowsDir)) {
393
424
  stepLine();
394
425
  const ws = spinner();
@@ -411,7 +442,7 @@ export async function run(spec) {
411
442
  }
412
443
 
413
444
  // Install GEMINI.md if it exists
414
- const geminiSrc = path.join(tmp, ".agent", "GEMINI.md");
445
+ const geminiSrc = path.join(baseAgentDir, "GEMINI.md");
415
446
  const geminiDest = path.join(WORKSPACE, "..", "GEMINI.md");
416
447
  let geminiInstalled = false;
417
448
 
@@ -423,7 +454,7 @@ export async function run(spec) {
423
454
  }
424
455
 
425
456
  // Install agents if they exist
426
- const agentsDir = path.join(tmp, ".agent", "agents");
457
+ const agentsDir = path.join(baseAgentDir, "agents");
427
458
  const targetAgentsDir = path.join(WORKSPACE, "..", "agents");
428
459
  let agentsInstalled = 0;
429
460
 
@@ -449,7 +480,7 @@ export async function run(spec) {
449
480
  }
450
481
 
451
482
  // Install ARCHITECTURE.md if it exists
452
- const archSrc = path.join(tmp, ".agent", "ARCHITECTURE.md");
483
+ const archSrc = path.join(baseAgentDir, "ARCHITECTURE.md");
453
484
  const archDest = path.join(WORKSPACE, "..", "ARCHITECTURE.md");
454
485
  let archInstalled = false;
455
486
 
@@ -460,7 +491,7 @@ export async function run(spec) {
460
491
  }
461
492
 
462
493
  // Install knowledge if it exists
463
- const knowledgeDir = path.join(tmp, ".agent", "knowledge");
494
+ const knowledgeDir = path.join(baseAgentDir, "knowledge");
464
495
  const targetKnowledgeDir = path.join(WORKSPACE, "..", "knowledge");
465
496
  let knowledgeInstalled = false;
466
497
 
@@ -471,7 +502,7 @@ export async function run(spec) {
471
502
  }
472
503
 
473
504
  // Install rules if they exist
474
- const rulesDir = path.join(tmp, ".agent", "rules");
505
+ const rulesDir = path.join(baseAgentDir, "rules");
475
506
  const targetRulesDir = path.join(WORKSPACE, "..", "rules");
476
507
  let rulesInstalled = 0;
477
508
 
@@ -1,7 +1,11 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
+ import { homedir } from "os";
3
4
  import { GLOBAL_DIR } from "./config.js";
4
5
  import { merkleHash } from "./helpers.js";
6
+ import { AGENTS } from "./agents.js";
7
+
8
+ const home = homedir();
5
9
 
6
10
  /**
7
11
  * Install a skill to the destination using the specified method.
@@ -47,3 +51,114 @@ export async function installSkill(src, dest, method, metadata) {
47
51
  method: method
48
52
  }, null, 2));
49
53
  }
54
+
55
+ /**
56
+ * Install a skill to multiple agents
57
+ * @param {string} src - Source directory containing the skill
58
+ * @param {string} skillName - Name of the skill
59
+ * @param {Array<{name: string, displayName: string, skillsDir: string, globalSkillsDir: string}>} agents - Agents to install to
60
+ * @param {Object} options - Installation options
61
+ * @param {string} options.method - 'symlink' or 'copy'
62
+ * @param {string} options.scope - 'project' or 'global'
63
+ * @param {Object} options.metadata - Metadata for tracking
64
+ * @returns {Promise<{success: Array, failed: Array}>}
65
+ */
66
+ export async function installSkillForAgents(src, skillName, agents, options = {}) {
67
+ const { method = "symlink", scope = "project", metadata = {} } = options;
68
+ const results = { success: [], failed: [] };
69
+
70
+ // For symlink mode: first copy to canonical location
71
+ let canonicalPath = null;
72
+
73
+ if (method === "symlink") {
74
+ // Canonical: .agents/skills/<skill-name> or ~/.agents/skills/<skill-name>
75
+ const baseDir = scope === "global" ? home : process.cwd();
76
+ canonicalPath = path.join(baseDir, ".agents", "skills", skillName);
77
+
78
+ // Ensure fresh copy in canonical
79
+ if (fs.existsSync(canonicalPath)) {
80
+ fs.rmSync(canonicalPath, { recursive: true, force: true });
81
+ }
82
+ fs.mkdirSync(path.dirname(canonicalPath), { recursive: true });
83
+
84
+ try {
85
+ await fs.promises.cp(src, canonicalPath, { recursive: true });
86
+
87
+ // Write metadata to canonical location
88
+ const hash = merkleHash(canonicalPath);
89
+ const metaFile = path.join(canonicalPath, ".skill-source.json");
90
+ fs.writeFileSync(metaFile, JSON.stringify({
91
+ ...metadata,
92
+ skillName,
93
+ checksum: hash,
94
+ installedAt: new Date().toISOString(),
95
+ method: method,
96
+ scope: scope,
97
+ agents: agents.map(a => a.name)
98
+ }, null, 2));
99
+ } catch (err) {
100
+ // If canonical copy fails, abort
101
+ return {
102
+ success: [],
103
+ failed: agents.map(a => ({ agent: a.displayName, error: `Canonical copy failed: ${err.message}` }))
104
+ };
105
+ }
106
+ }
107
+
108
+ // Install to each agent
109
+ for (const agent of agents) {
110
+ const agentConfig = AGENTS[agent.name];
111
+ if (!agentConfig) {
112
+ results.failed.push({ agent: agent.displayName, error: "Unknown agent" });
113
+ continue;
114
+ }
115
+
116
+ // Determine destination path
117
+ const baseDir = scope === "global" ? agentConfig.globalSkillsDir : path.join(process.cwd(), agentConfig.skillsDir);
118
+ const destPath = path.join(baseDir, skillName);
119
+
120
+ try {
121
+ // Ensure parent directory exists
122
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
123
+
124
+ // Remove existing if any
125
+ if (fs.existsSync(destPath)) {
126
+ fs.rmSync(destPath, { recursive: true, force: true });
127
+ }
128
+
129
+ if (method === "symlink" && canonicalPath) {
130
+ // Create symlink to canonical location
131
+ try {
132
+ fs.symlinkSync(canonicalPath, destPath, "junction");
133
+ results.success.push({ agent: agent.displayName, path: destPath, mode: "symlink" });
134
+ } catch (symlinkErr) {
135
+ // Fallback to copy if symlink fails (Windows permissions)
136
+ await fs.promises.cp(canonicalPath, destPath, { recursive: true });
137
+ results.success.push({ agent: agent.displayName, path: destPath, mode: "copy (symlink failed)" });
138
+ }
139
+ } else {
140
+ // Direct copy
141
+ await fs.promises.cp(src, destPath, { recursive: true });
142
+
143
+ // Write metadata
144
+ const hash = merkleHash(destPath);
145
+ const metaFile = path.join(destPath, ".skill-source.json");
146
+ fs.writeFileSync(metaFile, JSON.stringify({
147
+ ...metadata,
148
+ skillName,
149
+ checksum: hash,
150
+ installedAt: new Date().toISOString(),
151
+ method: "copy",
152
+ scope: scope
153
+ }, null, 2));
154
+
155
+ results.success.push({ agent: agent.displayName, path: destPath, mode: "copy" });
156
+ }
157
+ } catch (err) {
158
+ results.failed.push({ agent: agent.displayName, error: err.message });
159
+ }
160
+ }
161
+
162
+ return results;
163
+ }
164
+
package/bin/lib/ui.js CHANGED
@@ -6,9 +6,22 @@ import kleur from "kleur";
6
6
  import boxen from "boxen";
7
7
  import { intro, outro, multiselect, select, confirm, isCancel, cancel, text } from "@clack/prompts";
8
8
  import ora from "ora";
9
+ import gradient from "gradient-string";
9
10
 
10
11
  export { intro, outro, multiselect, select, confirm, isCancel, cancel, text };
11
12
 
13
+ // --- ASCII Art Banner ---
14
+ const PIKAKIT_BANNER = `
15
+ ____ _ _ _ ___ _
16
+ | _ \\(_) | ____ _| |/ (_) |_
17
+ | |_) | | |/ / _\` | ' /| | __|
18
+ | __/| | < (_| | . \\| | |_
19
+ |_| |_|_|\\_\\__,_|_|\\_\\_|\\__|
20
+ `;
21
+
22
+ // Custom gradient: white → gray (like vercel style)
23
+ const pikaGradient = gradient(['#ffffff', '#bbbbbb', '#888888', '#555555']);
24
+
12
25
  /**
13
26
  * Create a spinner
14
27
  */
@@ -148,15 +161,169 @@ export function box(message, options = {}) {
148
161
  }
149
162
 
150
163
  /**
151
- * Show branded intro with version
164
+ * Show branded intro with version (matches agent CLI style)
152
165
  * @param {string} version - Package version
153
166
  * @param {string} [status] - Optional status text
154
167
  */
155
168
  export function brandedIntro(version, status = "") {
156
- const statusLine = status ? `\n\n ${c.dim(status)}` : "";
157
- console.log(box(
158
- `📦 Add Skill Kit v${version}${statusLine}`,
159
- { borderColor: status.includes("✓") ? "green" : "blue" }
160
- ));
169
+ // Split banner and filter to get content lines only
170
+ const bannerLines = PIKAKIT_BANNER.split('\n').filter(line => line.trim() !== '');
171
+
172
+ // Print all lines except the last with gradient
173
+ for (let i = 0; i < bannerLines.length - 1; i++) {
174
+ console.log(pikaGradient(bannerLines[i]));
175
+ }
176
+
177
+ // Last line: gradient ASCII + dim version (aligned at bottom)
178
+ const lastLine = bannerLines[bannerLines.length - 1];
179
+ console.log(pikaGradient(lastLine) + ` ${c.dim(`v${version}`)}`);
180
+ console.log(''); // Empty line after banner
181
+
182
+ if (status) {
183
+ console.log(`${c.dim(status)}`);
184
+ }
185
+ }
186
+
187
+ // --- Vercel-Style Installation Prompts ---
188
+
189
+ /**
190
+ * Prompt user to select which agents to install to
191
+ * @param {Array<{name: string, displayName: string, skillsDir: string}>} detectedAgents
192
+ * @returns {Promise<Array<{name: string, displayName: string, skillsDir: string}> | null>}
193
+ */
194
+ export async function selectAgentsPrompt(detectedAgents) {
195
+ if (detectedAgents.length === 0) {
196
+ return null;
197
+ }
198
+
199
+ // First ask: All detected or select specific?
200
+ const installChoice = await select({
201
+ message: "Install to",
202
+ options: [
203
+ {
204
+ value: "all",
205
+ label: `All detected agents (Recommended)`,
206
+ hint: `Install to all ${detectedAgents.length} detected agents`
207
+ },
208
+ {
209
+ value: "select",
210
+ label: "Select specific agents",
211
+ hint: "Choose which agents to install to"
212
+ }
213
+ ]
214
+ });
215
+
216
+ if (isCancel(installChoice)) {
217
+ cancel("Installation cancelled");
218
+ return null;
219
+ }
220
+
221
+ if (installChoice === "all") {
222
+ return detectedAgents;
223
+ }
224
+
225
+ // Let user select specific agents
226
+ const selectedAgents = await multiselect({
227
+ message: "Select agents to install skills to",
228
+ options: detectedAgents.map(agent => ({
229
+ value: agent.name,
230
+ label: agent.displayName,
231
+ hint: agent.skillsDir
232
+ })),
233
+ required: true
234
+ });
235
+
236
+ if (isCancel(selectedAgents)) {
237
+ cancel("Installation cancelled");
238
+ return null;
239
+ }
240
+
241
+ return detectedAgents.filter(a => selectedAgents.includes(a.name));
242
+ }
243
+
244
+ /**
245
+ * Prompt user to select installation scope (Project or Global)
246
+ * @returns {Promise<"project" | "global" | null>}
247
+ */
248
+ export async function selectScopePrompt() {
249
+ const scope = await select({
250
+ message: "Installation scope",
251
+ options: [
252
+ {
253
+ value: "project",
254
+ label: "Project",
255
+ hint: "Install in current directory (committed with your project)"
256
+ },
257
+ {
258
+ value: "global",
259
+ label: "Global",
260
+ hint: "Install globally (available across all projects)"
261
+ }
262
+ ]
263
+ });
264
+
265
+ if (isCancel(scope)) {
266
+ cancel("Installation cancelled");
267
+ return null;
268
+ }
269
+
270
+ return scope;
271
+ }
272
+
273
+ /**
274
+ * Prompt user to select installation method (Symlink or Copy)
275
+ * @returns {Promise<"symlink" | "copy" | null>}
276
+ */
277
+ export async function selectMethodPrompt() {
278
+ const method = await select({
279
+ message: "Installation method",
280
+ options: [
281
+ {
282
+ value: "symlink",
283
+ label: "Symlink (Recommended)",
284
+ hint: "Single source of truth, easy updates"
285
+ },
286
+ {
287
+ value: "copy",
288
+ label: "Copy to all agents",
289
+ hint: "Independent copies for each agent"
290
+ }
291
+ ]
292
+ });
293
+
294
+ if (isCancel(method)) {
295
+ cancel("Installation cancelled");
296
+ return null;
297
+ }
298
+
299
+ return method;
300
+ }
301
+
302
+ /**
303
+ * Prompt user to select skills to install (multiselect with descriptions)
304
+ * @param {Array<{name: string, description: string, path: string}>} skills
305
+ * @returns {Promise<Array<{name: string, description: string, path: string}> | null>}
306
+ */
307
+ export async function selectSkillsPrompt(skills) {
308
+ if (skills.length === 0) {
309
+ return null;
310
+ }
311
+
312
+ const selectedNames = await multiselect({
313
+ message: "Select skills to install",
314
+ options: skills.map(skill => ({
315
+ value: skill.name,
316
+ label: skill.name,
317
+ hint: skill.description ? skill.description.substring(0, 60) + "..." : ""
318
+ })),
319
+ required: true
320
+ });
321
+
322
+ if (isCancel(selectedNames)) {
323
+ cancel("Installation cancelled");
324
+ return null;
325
+ }
326
+
327
+ return skills.filter(s => selectedNames.includes(s.name));
161
328
  }
162
329
 
package/package.json CHANGED
@@ -1,64 +1,67 @@
1
- {
2
- "name": "add-skill-kit",
3
- "version": "3.2.0",
4
- "description": "Enterprise-grade Agent Skill Manager with Antigravity Skills support, Progressive Disclosure detection, and semantic routing validation",
5
- "license": "MIT",
6
- "author": "agentskillkit <agentskillkit@gmail.com>",
7
- "homepage": "https://github.com/agentskillkit/add-skill-kit",
8
- "repository": {
9
- "type": "git",
10
- "url": "https://github.com/agentskillkit/add-skill-kit.git"
11
- },
12
- "bugs": {
13
- "url": "https://github.com/agentskillkit/add-skill-kit/issues"
14
- },
15
- "type": "module",
16
- "bin": {
17
- "kit": "./bin/cli.js"
18
- },
19
- "files": [
20
- "bin/",
21
- "specs/",
22
- "README.md",
23
- "LICENSE"
24
- ],
25
- "engines": {
26
- "node": ">=18.0.0"
27
- },
28
- "keywords": [
29
- "agent",
30
- "ai",
31
- "skills",
32
- "cli",
33
- "tooling",
34
- "registry",
35
- "security",
36
- "devops",
37
- "automation",
38
- "antigravity"
39
- ],
40
- "scripts": {
41
- "lint": "eslint bin/",
42
- "lint:fix": "eslint bin/ --fix",
43
- "format": "prettier --write bin/",
44
- "test": "vitest run",
45
- "test:watch": "vitest",
46
- "ci": "npm run lint && npm test && node bin/cli.js verify --strict && node bin/cli.js doctor --strict"
47
- },
48
- "publishConfig": {
49
- "access": "public"
50
- },
51
- "dependencies": {
52
- "@clack/prompts": "^0.9.1",
53
- "boxen": "^8.0.1",
54
- "chalk": "^5.4.1",
55
- "kleur": "^4.1.5",
56
- "ora": "^8.1.0",
57
- "prompts": "^2.4.2"
58
- },
59
- "devDependencies": {
60
- "eslint": "^8.57.0",
61
- "prettier": "^3.2.5",
62
- "vitest": "^1.6.0"
63
- }
64
- }
1
+ {
2
+ "name": "add-skill-kit",
3
+ "version": "3.2.2",
4
+ "description": "Enterprise-grade Agent Skill Manager with Antigravity Skills support, Progressive Disclosure detection, and semantic routing validation",
5
+ "license": "MIT",
6
+ "author": "agentskillkit <agentskillkit@gmail.com>",
7
+ "homepage": "https://github.com/agentskillkit/add-skill-kit",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/agentskillkit/add-skill-kit.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/agentskillkit/add-skill-kit/issues"
14
+ },
15
+ "type": "module",
16
+ "bin": {
17
+ "kit": "./bin/cli.js"
18
+ },
19
+ "files": [
20
+ "bin/",
21
+ "specs/",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ },
28
+ "keywords": [
29
+ "agent",
30
+ "ai",
31
+ "skills",
32
+ "cli",
33
+ "tooling",
34
+ "registry",
35
+ "security",
36
+ "devops",
37
+ "automation",
38
+ "antigravity"
39
+ ],
40
+ "scripts": {
41
+ "lint": "eslint bin/",
42
+ "lint:fix": "eslint bin/ --fix",
43
+ "format": "prettier --write bin/",
44
+ "test": "vitest run",
45
+ "test:watch": "vitest",
46
+ "ci": "npm run lint && npm test && node bin/cli.js verify --strict && node bin/cli.js doctor --strict",
47
+ "agent": "agent"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "dependencies": {
53
+ "@clack/prompts": "^0.9.1",
54
+ "boxen": "^8.0.1",
55
+ "chalk": "^5.4.1",
56
+ "gradient-string": "^2.0.2",
57
+ "kleur": "^4.1.5",
58
+ "ora": "^8.1.0",
59
+ "prompts": "^2.4.2"
60
+ },
61
+ "devDependencies": {
62
+ "agentskillskit-cli": "^3.2.0",
63
+ "eslint": "^8.57.0",
64
+ "prettier": "^3.2.5",
65
+ "vitest": "^1.6.0"
66
+ }
67
+ }