add-skill-kit 2.3.2 → 3.2.1

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
+ }
@@ -133,10 +133,11 @@ export async function run(spec) {
133
133
  if (fs.statSync(sp).isDirectory() && fs.existsSync(path.join(sp, "SKILL.md"))) {
134
134
  const m = parseSkillMdFrontmatter(path.join(sp, "SKILL.md"));
135
135
  skillsInRepo.push({
136
- title: e + (m.description ? c.dim(` (${m.description.substring(0, 40)}...)`) : ""),
136
+ title: e, // Only show folder name
137
137
  value: e,
138
+ description: m.description || "",
138
139
  selected: singleSkill ? e === singleSkill : true,
139
- _path: sp // Store actual path for later use
140
+ _path: sp
140
141
  });
141
142
  }
142
143
  }
@@ -167,177 +168,155 @@ export async function run(spec) {
167
168
  stepLine();
168
169
  step(`Auto-selected: ${c.cyan(singleSkill)}`);
169
170
  } else {
170
- // Show selection prompt for multiple skills
171
- stepLine();
171
+ // Group skills by category - expanded keywords for 75+ skills
172
+ const CATEGORY_KEYWORDS = {
173
+ "⚙️ Backend & API": ["backend", "api", "nodejs", "server", "database", "prisma", "mcp", "python", "cache", "input-validator"],
174
+ "🎨 Frontend & UI": ["frontend", "react", "nextjs", "tailwind", "css", "ui", "ux", "design", "visual", "studio", "web-core"],
175
+ "🧪 Testing & Quality": ["test", "tdd", "testing", "lint", "review", "code-review", "clean", "validate", "webapp-testing", "e2e", "integration-tester", "load-tester", "code-quality", "code-craft"],
176
+ "🔒 Security & DevOps": ["security", "vulnerability", "deploy", "git", "docker", "server-management", "red-team", "governance", "offensive", "gitops", "cicd", "pipeline", "incident", "chaos"],
177
+ "📱 Mobile": ["mobile", "react-native", "flutter", "ios", "android"],
178
+ "🧠 AI & Agents": ["agent", "routing", "brainstorm", "behavioral", "intelligent", "parallel", "reasoning", "creative", "idea", "context", "self-evolution", "auto-learn", "smart", "multi-agent"],
179
+ "📦 Architecture": ["architecture", "system", "pattern", "app-builder", "performance", "profiling", "perf", "scaffold", "workflow", "lifecycle", "state", "execution"],
180
+ "📝 Documentation": ["documentation", "plan", "writing", "geo", "seo", "i18n", "localization", "global", "doc-", "requirement", "diagram"],
181
+ "🎮 Game Development": ["game"],
182
+ "🖥️ CLI & Tools": ["bash", "linux", "powershell", "windows", "debugging", "systematic", "debug", "shell", "script", "logging", "tracing", "metrics", "observability"],
183
+ "🔧 Code Tools": ["skill-forge", "code-constitution", "typescript", "problem", "checker"]
184
+ };
185
+
186
+ function categorizeSkill(skillName) {
187
+ const lower = skillName.toLowerCase();
188
+ for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) {
189
+ if (keywords.some(kw => lower.includes(kw))) {
190
+ return category;
191
+ }
192
+ }
193
+ return "📚 Other";
194
+ }
172
195
 
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.
196
+ // Group skills by category
197
+ const grouped = {};
198
+ for (const skill of skillsInRepo) {
199
+ const cat = categorizeSkill(skill.value);
200
+ if (!grouped[cat]) grouped[cat] = [];
201
+ grouped[cat].push(skill);
202
+ }
203
+
204
+ // Custom sort: alphabetical but "Other" always last
205
+ const sortedCategories = Object.keys(grouped).sort((a, b) => {
206
+ if (a.includes("Other")) return 1;
207
+ if (b.includes("Other")) return -1;
208
+ return a.localeCompare(b);
209
+ });
183
210
 
184
- activeStep("Select skills to install");
185
211
 
186
- const skills = await multiselect({
212
+ stepLine();
213
+ activeStep("Select skill categories to install");
214
+
215
+ // Show only categories, not individual skills
216
+ const selectedCategories = await multiselect({
187
217
  message: `${c.cyan("space")} select · ${c.cyan("enter")} confirm`,
188
- options: skillsInRepo.map(s => ({
189
- label: s.title,
190
- value: s.value
218
+ options: sortedCategories.map(cat => ({
219
+ label: `${cat} (${grouped[cat].length} skills)`,
220
+ value: cat,
221
+ hint: grouped[cat].slice(0, 3).map(s => s.value).join(", ") + (grouped[cat].length > 3 ? "..." : "")
191
222
  })),
192
- initialValues: skillsInRepo.map(s => s.value), // Pre-select all
223
+ initialValues: sortedCategories, // Pre-select all
193
224
  required: true
194
225
  });
195
226
 
196
- if (isCancel(skills)) {
227
+ if (isCancel(selectedCategories)) {
197
228
  cancel("Cancelled.");
198
229
  fs.rmSync(tmp, { recursive: true, force: true });
199
230
  return;
200
231
  }
201
232
 
202
- selectedSkills = skills;
233
+ // Get all skills from selected categories
234
+ selectedSkills = selectedCategories.flatMap(cat => grouped[cat].map(s => s.value));
203
235
  }
204
236
 
205
- // Ensure newline after prompt to fix tree alignment
237
+
238
+
206
239
  stepLine();
207
240
  step("Select skills to install");
208
241
  console.log(`${c.gray(S.branch)} ${c.dim(selectedSkills.join(", "))}`);
209
242
 
210
- // --- NEW: Scope Selection ---
243
+ // --- Detect installed agents ---
244
+ stepLine();
245
+ const { detectInstalledAgents } = await import("../agents.js");
246
+ const detectedAgents = detectInstalledAgents();
247
+
248
+ if (detectedAgents.length === 0) {
249
+ step(c.yellow("No agents detected"), S.diamond, "yellow");
250
+ step(c.dim("Please install at least one AI agent (Antigravity, Claude Code, etc.)"), S.branch, "gray");
251
+ fs.rmSync(tmp, { recursive: true, force: true });
252
+ return;
253
+ }
211
254
 
212
- // Determine installation scope
213
- let isGlobal = false;
255
+ step(`Detected ${detectedAgents.length} agents`);
214
256
 
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
- });
257
+ // --- Select agents (Vercel-style) ---
258
+ const { selectAgentsPrompt, selectScopePrompt, selectMethodPrompt } = await import("../ui.js");
236
259
 
237
- if (isCancel(scopeSelection)) {
238
- cancel("Cancelled.");
239
- fs.rmSync(tmp, { recursive: true, force: true });
240
- return;
241
- }
260
+ stepLine();
261
+ activeStep("Install to");
262
+ const selectedAgents = await selectAgentsPrompt(detectedAgents);
242
263
 
243
- isGlobal = scopeSelection === "global";
264
+ if (!selectedAgents || selectedAgents.length === 0) {
265
+ fs.rmSync(tmp, { recursive: true, force: true });
266
+ return;
244
267
  }
245
- const targetScope = isGlobal ? GLOBAL_DIR : WORKSPACE;
246
268
 
247
269
  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)`);
270
+ step("Install to");
271
+ console.log(`${c.gray(S.branch)} ${c.dim(selectedAgents.map(a => a.displayName).join(", "))}`);
262
272
 
263
- let agents;
264
- while (true) {
265
- // Active Step
266
- activeStep("Select agents to install skills to (Antigravity only)");
273
+ // --- Select installation scope ---
274
+ let isGlobal = GLOBAL;
267
275
 
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
- });
276
+ if (!GLOBAL) {
277
+ stepLine();
278
+ activeStep("Installation scope");
279
+ const scope = await selectScopePrompt();
277
280
 
278
- if (isCancel(agents)) {
279
- cancel("Cancelled.");
281
+ if (!scope) {
280
282
  fs.rmSync(tmp, { recursive: true, force: true });
281
283
  return;
282
284
  }
283
285
 
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;
286
+ isGlobal = scope === "global";
292
287
  }
293
288
 
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;
298
- }
299
-
300
- // Agent summary
301
289
  stepLine();
302
- step("Select agents to install skills to");
303
- console.log(`${c.gray(S.branch)} ${c.dim(Array.isArray(agents) ? agents.join(", ") : agents)}`);
290
+ step("Installation scope");
291
+ console.log(`${c.gray(S.branch)} ${c.dim(isGlobal ? "Global" : "Project")}`);
304
292
 
305
- // Installation method
293
+ // --- Select installation method ---
294
+ stepLine();
306
295
  activeStep("Installation method");
296
+ const installMethod = await selectMethodPrompt();
307
297
 
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.");
298
+ if (!installMethod) {
319
299
  fs.rmSync(tmp, { recursive: true, force: true });
320
300
  return;
321
301
  }
322
302
 
303
+
323
304
  // Installation Summary Box
324
305
  stepLine();
325
- step("Installation Summary"); // Passive header for box
306
+ step("Installation method");
307
+ console.log(`${c.gray(S.branch)} ${c.dim(installMethod === "symlink" ? "Symlink" : "Copy")}`);
308
+
309
+ stepLine();
310
+ step("Installation Summary");
326
311
  stepLine();
327
312
 
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(", ");
313
+ const agentsString = selectedAgents.map(a => a.displayName).join(", ");
333
314
 
334
315
  let summaryContent = "";
335
316
  const methodVerb = installMethod === "symlink" ? "symlink" : "copy";
336
317
 
337
318
  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`;
319
+ summaryContent += `${c.cyan(sn)}\n`;
341
320
  summaryContent += ` ${c.dim(methodVerb)} ${c.gray("→")} ${c.dim(agentsString)}\n\n`;
342
321
  }
343
322
 
@@ -356,7 +335,6 @@ export async function run(spec) {
356
335
  stepLine();
357
336
 
358
337
  // Confirmation
359
- // Active Step
360
338
  activeStep("Proceed with installation?");
361
339
  const shouldProceed = await confirm({ message: " ", initialValue: true });
362
340
 
@@ -366,29 +344,51 @@ export async function run(spec) {
366
344
  return;
367
345
  }
368
346
 
369
- // Install
347
+ // Install skills to multiple agents
370
348
  stepLine();
371
- fs.mkdirSync(targetScope, { recursive: true });
349
+ const { installSkillForAgents } = await import("../installer.js");
372
350
 
373
351
  // Create a map for skill paths
374
352
  const skillPathMap = Object.fromEntries(skillsInRepo.map(s => [s.value, s._path]));
375
353
 
354
+ const installResults = { success: [], failed: [] };
355
+
376
356
  for (const sn of selectedSkills) {
377
357
  const src = skillPathMap[sn] || path.join(skillsDir || tmp, sn);
378
- const dest = path.join(targetScope, sn);
379
358
 
380
- await installSkill(src, dest, installMethod, {
381
- repo: `${org}/${repo}`,
382
- skill: sn,
383
- ref: ref || null
359
+ const is = spinner();
360
+ is.start(`Installing ${sn} to ${selectedAgents.length} agents`);
361
+
362
+ const result = await installSkillForAgents(src, sn, selectedAgents, {
363
+ method: installMethod,
364
+ scope: isGlobal ? "global" : "project",
365
+ metadata: {
366
+ repo: `${org}/${repo}`,
367
+ ref: ref || null
368
+ }
384
369
  });
370
+
371
+ installResults.success.push(...result.success);
372
+ installResults.failed.push(...result.failed);
373
+
374
+ if (result.failed.length === 0) {
375
+ is.stop(`Installed ${sn} (${result.success.length} agents)`);
376
+ } else {
377
+ is.stop(`${sn}: ${result.success.length} success, ${result.failed.length} failed`);
378
+ }
385
379
  }
386
380
 
381
+
382
+ // Derive base .agent directory from skillsDir
383
+ // If skillsDir is .../skills, then baseAgentDir is parent (.agent)
384
+ const baseAgentDir = skillsDir ? path.dirname(skillsDir) : path.join(tmp, ".agent");
385
+
387
386
  // Install workflows if they exist
388
- const workflowsDir = path.join(tmp, ".agent", "workflows");
387
+ const workflowsDir = path.join(baseAgentDir, "workflows");
389
388
  const targetWorkflowsDir = path.join(WORKSPACE, "..", "workflows");
390
389
  let workflowsInstalled = 0;
391
390
 
391
+
392
392
  if (fs.existsSync(workflowsDir)) {
393
393
  stepLine();
394
394
  const ws = spinner();
@@ -411,7 +411,7 @@ export async function run(spec) {
411
411
  }
412
412
 
413
413
  // Install GEMINI.md if it exists
414
- const geminiSrc = path.join(tmp, ".agent", "GEMINI.md");
414
+ const geminiSrc = path.join(baseAgentDir, "GEMINI.md");
415
415
  const geminiDest = path.join(WORKSPACE, "..", "GEMINI.md");
416
416
  let geminiInstalled = false;
417
417
 
@@ -423,7 +423,7 @@ export async function run(spec) {
423
423
  }
424
424
 
425
425
  // Install agents if they exist
426
- const agentsDir = path.join(tmp, ".agent", "agents");
426
+ const agentsDir = path.join(baseAgentDir, "agents");
427
427
  const targetAgentsDir = path.join(WORKSPACE, "..", "agents");
428
428
  let agentsInstalled = 0;
429
429
 
@@ -449,7 +449,7 @@ export async function run(spec) {
449
449
  }
450
450
 
451
451
  // Install ARCHITECTURE.md if it exists
452
- const archSrc = path.join(tmp, ".agent", "ARCHITECTURE.md");
452
+ const archSrc = path.join(baseAgentDir, "ARCHITECTURE.md");
453
453
  const archDest = path.join(WORKSPACE, "..", "ARCHITECTURE.md");
454
454
  let archInstalled = false;
455
455
 
@@ -460,7 +460,7 @@ export async function run(spec) {
460
460
  }
461
461
 
462
462
  // Install knowledge if it exists
463
- const knowledgeDir = path.join(tmp, ".agent", "knowledge");
463
+ const knowledgeDir = path.join(baseAgentDir, "knowledge");
464
464
  const targetKnowledgeDir = path.join(WORKSPACE, "..", "knowledge");
465
465
  let knowledgeInstalled = false;
466
466
 
@@ -471,7 +471,7 @@ export async function run(spec) {
471
471
  }
472
472
 
473
473
  // Install rules if they exist
474
- const rulesDir = path.join(tmp, ".agent", "rules");
474
+ const rulesDir = path.join(baseAgentDir, "rules");
475
475
  const targetRulesDir = path.join(WORKSPACE, "..", "rules");
476
476
  let rulesInstalled = 0;
477
477
 
@@ -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,23 @@ 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
+
23
+ // Custom gradient: white → gray (like vercel style)
24
+ const pikaGradient = gradient(['#ffffff', '#bbbbbb', '#888888', '#555555']);
25
+
12
26
  /**
13
27
  * Create a spinner
14
28
  */
@@ -148,15 +162,162 @@ export function box(message, options = {}) {
148
162
  }
149
163
 
150
164
  /**
151
- * Show branded intro with version
165
+ * Show branded intro with version (matches agent CLI style)
152
166
  * @param {string} version - Package version
153
167
  * @param {string} [status] - Optional status text
154
168
  */
155
169
  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
- ));
170
+ // Print ASCII art banner with gradient
171
+ console.log(pikaGradient(PIKAKIT_BANNER));
172
+
173
+ // Version info
174
+ console.log(c.dim(` v${version}\n`));
175
+
176
+ if (status) {
177
+ console.log(`${c.dim(status)}`);
178
+ }
179
+ }
180
+
181
+ // --- Vercel-Style Installation Prompts ---
182
+
183
+ /**
184
+ * Prompt user to select which agents to install to
185
+ * @param {Array<{name: string, displayName: string, skillsDir: string}>} detectedAgents
186
+ * @returns {Promise<Array<{name: string, displayName: string, skillsDir: string}> | null>}
187
+ */
188
+ export async function selectAgentsPrompt(detectedAgents) {
189
+ if (detectedAgents.length === 0) {
190
+ return null;
191
+ }
192
+
193
+ // First ask: All detected or select specific?
194
+ const installChoice = await select({
195
+ message: "Install to",
196
+ options: [
197
+ {
198
+ value: "all",
199
+ label: `All detected agents (Recommended)`,
200
+ hint: `Install to all ${detectedAgents.length} detected agents`
201
+ },
202
+ {
203
+ value: "select",
204
+ label: "Select specific agents",
205
+ hint: "Choose which agents to install to"
206
+ }
207
+ ]
208
+ });
209
+
210
+ if (isCancel(installChoice)) {
211
+ cancel("Installation cancelled");
212
+ return null;
213
+ }
214
+
215
+ if (installChoice === "all") {
216
+ return detectedAgents;
217
+ }
218
+
219
+ // Let user select specific agents
220
+ const selectedAgents = await multiselect({
221
+ message: "Select agents to install skills to",
222
+ options: detectedAgents.map(agent => ({
223
+ value: agent.name,
224
+ label: agent.displayName,
225
+ hint: agent.skillsDir
226
+ })),
227
+ required: true
228
+ });
229
+
230
+ if (isCancel(selectedAgents)) {
231
+ cancel("Installation cancelled");
232
+ return null;
233
+ }
234
+
235
+ return detectedAgents.filter(a => selectedAgents.includes(a.name));
236
+ }
237
+
238
+ /**
239
+ * Prompt user to select installation scope (Project or Global)
240
+ * @returns {Promise<"project" | "global" | null>}
241
+ */
242
+ export async function selectScopePrompt() {
243
+ const scope = await select({
244
+ message: "Installation scope",
245
+ options: [
246
+ {
247
+ value: "project",
248
+ label: "Project",
249
+ hint: "Install in current directory (committed with your project)"
250
+ },
251
+ {
252
+ value: "global",
253
+ label: "Global",
254
+ hint: "Install globally (available across all projects)"
255
+ }
256
+ ]
257
+ });
258
+
259
+ if (isCancel(scope)) {
260
+ cancel("Installation cancelled");
261
+ return null;
262
+ }
263
+
264
+ return scope;
265
+ }
266
+
267
+ /**
268
+ * Prompt user to select installation method (Symlink or Copy)
269
+ * @returns {Promise<"symlink" | "copy" | null>}
270
+ */
271
+ export async function selectMethodPrompt() {
272
+ const method = await select({
273
+ message: "Installation method",
274
+ options: [
275
+ {
276
+ value: "symlink",
277
+ label: "Symlink (Recommended)",
278
+ hint: "Single source of truth, easy updates"
279
+ },
280
+ {
281
+ value: "copy",
282
+ label: "Copy to all agents",
283
+ hint: "Independent copies for each agent"
284
+ }
285
+ ]
286
+ });
287
+
288
+ if (isCancel(method)) {
289
+ cancel("Installation cancelled");
290
+ return null;
291
+ }
292
+
293
+ return method;
294
+ }
295
+
296
+ /**
297
+ * Prompt user to select skills to install (multiselect with descriptions)
298
+ * @param {Array<{name: string, description: string, path: string}>} skills
299
+ * @returns {Promise<Array<{name: string, description: string, path: string}> | null>}
300
+ */
301
+ export async function selectSkillsPrompt(skills) {
302
+ if (skills.length === 0) {
303
+ return null;
304
+ }
305
+
306
+ const selectedNames = await multiselect({
307
+ message: "Select skills to install",
308
+ options: skills.map(skill => ({
309
+ value: skill.name,
310
+ label: skill.name,
311
+ hint: skill.description ? skill.description.substring(0, 60) + "..." : ""
312
+ })),
313
+ required: true
314
+ });
315
+
316
+ if (isCancel(selectedNames)) {
317
+ cancel("Installation cancelled");
318
+ return null;
319
+ }
320
+
321
+ return skills.filter(s => selectedNames.includes(s.name));
161
322
  }
162
323
 
package/package.json CHANGED
@@ -1,64 +1,67 @@
1
- {
2
- "name": "add-skill-kit",
3
- "version": "2.3.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
- },
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.1",
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
+ }