pikakit 1.0.28 → 1.0.30

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.
@@ -1,917 +1,902 @@
1
- /**
2
- * @fileoverview Install command - Interactive skill installation
3
- */
4
-
5
- import fs from "fs";
6
- import path from "path";
7
- import os from "os";
8
- import { exec } from "child_process";
9
- import util from "util";
10
- const execAsync = util.promisify(exec);
11
- import boxen from "boxen";
12
- import { parseSkillSpec, merkleHash } from "../helpers.js";
13
- import { parseSkillMdFrontmatter } from "../skills.js";
14
- import { step, activeStep, stepLine, S, c, fatal, spinner, multiselect, select, confirm, isCancel, cancel } from "../ui.js";
15
- import { WORKSPACE, GLOBAL_DIR, OFFLINE, GLOBAL, REPO_CACHE_DIR, CACHE_TTL_MS, FORCE_REFRESH } from "../config.js";
16
- import { installSkill } from "../installer.js";
17
-
18
- /**
19
- * Install skills from repository
20
- * @param {string} spec - Skill spec (org/repo or org/repo#skill)
21
- */
22
- export async function run(spec) {
23
- if (!spec) {
24
- fatal("Missing skill spec. Usage: add-skill <org/repo>");
25
- return;
26
- }
27
-
28
- const { org, repo, skill: singleSkill, ref } = parseSkillSpec(spec);
29
-
30
- if (!org || !repo) {
31
- fatal("Invalid spec. Format: org/repo or org/repo#skill");
32
- return;
33
- }
34
-
35
- // Check offline mode
36
- if (OFFLINE) {
37
- stepLine();
38
- step(c.yellow("Offline mode enabled"), S.diamond, "yellow");
39
- step(c.dim("Cannot install from remote repository in offline mode"), S.branch, "gray");
40
- step(c.dim("Use --locked to install from lockfile instead"), S.branch, "gray");
41
- return;
42
- }
43
-
44
- const url = `https://github.com/${org}/${repo}.git`;
45
-
46
- stepLine();
47
- step("Source: " + c.cyan(url));
48
-
49
- const s = spinner();
50
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "add-skill-"));
51
-
52
- // --- Repository Caching Logic ---
53
- const cacheDir = path.join(REPO_CACHE_DIR, org, repo);
54
- const cacheMetaFile = path.join(cacheDir, ".cache-meta.json");
55
- let useCache = false;
56
- let cacheHit = false;
57
-
58
- // Check if cache exists and is valid
59
- if (!FORCE_REFRESH && fs.existsSync(cacheDir) && fs.existsSync(cacheMetaFile)) {
60
- try {
61
- const meta = JSON.parse(fs.readFileSync(cacheMetaFile, "utf-8"));
62
- const cacheAge = Date.now() - new Date(meta.timestamp).getTime();
63
-
64
- if (cacheAge < CACHE_TTL_MS) {
65
- useCache = true;
66
- cacheHit = true;
67
- s.start("Cloning repository");
68
- }
69
- } catch {
70
- // Invalid cache, will refresh
71
- }
72
- }
73
-
74
- let lastError = null;
75
- const MAX_RETRIES = 3;
76
-
77
- if (useCache) {
78
- // Fast path: Update cache with git fetch (much faster than clone)
79
- try {
80
- // Fast update via git fetch (silent)
81
- await execAsync(`git -C "${cacheDir}" fetch --depth=1 origin HEAD`, { timeout: 30000 });
82
- await execAsync(`git -C "${cacheDir}" reset --hard FETCH_HEAD`, { timeout: 10000 });
83
-
84
- // Copy from cache to tmp
85
- await fs.promises.cp(cacheDir, tmp, { recursive: true });
86
-
87
- // Update cache metadata
88
- fs.writeFileSync(cacheMetaFile, JSON.stringify({
89
- timestamp: new Date().toISOString(),
90
- org, repo, ref: ref || "HEAD"
91
- }));
92
-
93
- s.stop("Repository cloned");
94
- } catch (err) {
95
- // Cache update failed, fall back to fresh clone
96
- useCache = false;
97
- cacheHit = false;
98
- }
99
- }
100
-
101
- if (!useCache) {
102
- // Fresh clone with retry logic
103
- s.start(FORCE_REFRESH ? "Force refreshing repository..." : "Cloning repository");
104
-
105
- for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
106
- try {
107
- await execAsync(`git clone --depth=1 ${url} "${tmp}"`, { timeout: 60000 });
108
- if (ref) await execAsync(`git -C "${tmp}" checkout ${ref}`, { timeout: 30000 });
109
- lastError = null;
110
-
111
- // Save to cache for next time
112
- try {
113
- fs.mkdirSync(path.dirname(cacheDir), { recursive: true });
114
- if (fs.existsSync(cacheDir)) fs.rmSync(cacheDir, { recursive: true, force: true });
115
- await fs.promises.cp(tmp, cacheDir, { recursive: true });
116
- fs.writeFileSync(path.join(cacheDir, ".cache-meta.json"), JSON.stringify({
117
- timestamp: new Date().toISOString(),
118
- org, repo, ref: ref || "HEAD"
119
- }));
120
- } catch { /* Cache write failed, non-fatal */ }
121
-
122
- break;
123
- } catch (err) {
124
- lastError = err;
125
-
126
- if (attempt < MAX_RETRIES) {
127
- const delay = Math.pow(2, attempt) * 1000; // 2s, 4s
128
- s.message(`Retry ${attempt}/${MAX_RETRIES} in ${delay / 1000}s...`);
129
- await new Promise(r => setTimeout(r, delay));
130
-
131
- // Clean up failed attempt
132
- try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { }
133
- fs.mkdirSync(tmp, { recursive: true });
134
- }
135
- }
136
- }
137
- }
138
-
139
- if (lastError) {
140
- s.fail("Failed to clone repository");
141
- stepLine();
142
-
143
- // Provide helpful error messages
144
- const errMsg = lastError.message || "";
145
- if (errMsg.includes("not found") || errMsg.includes("404")) {
146
- step(c.red(`Repository not found: ${org}/${repo}`), S.cross, "red");
147
- step(c.dim("Check if the repository exists and is public"), S.branch, "gray");
148
- } else if (errMsg.includes("timeout")) {
149
- step(c.red("Connection timeout"), S.cross, "red");
150
- step(c.dim("Check your internet connection and try again"), S.branch, "gray");
151
- } else if (errMsg.includes("Could not resolve")) {
152
- step(c.red("Network error: Unable to reach GitHub"), S.cross, "red");
153
- step(c.dim("Check your internet connection"), S.branch, "gray");
154
- } else {
155
- step(c.red("Clone failed: " + errMsg.split("\n")[0]), S.cross, "red");
156
- }
157
-
158
- fs.rmSync(tmp, { recursive: true, force: true });
159
- return;
160
- }
161
-
162
- if (!cacheHit) {
163
- s.stop("Repository cloned");
164
- }
165
-
166
- // Find skills in repo - check multiple possible locations
167
- const skillsInRepo = [];
168
-
169
- // Possible skill locations (in order of priority)
170
- const possibleSkillDirs = [
171
- path.join(tmp, ".agent", "skills"), // Standard location
172
- path.join(tmp, "skills"), // Alternative location
173
- tmp // Root level (legacy)
174
- ];
175
-
176
- let skillsDir = null;
177
- for (const dir of possibleSkillDirs) {
178
- if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
179
- // Check if this directory contains skill folders
180
- const entries = fs.readdirSync(dir);
181
- for (const e of entries) {
182
- const sp = path.join(dir, e);
183
- if (fs.statSync(sp).isDirectory() && fs.existsSync(path.join(sp, "SKILL.md"))) {
184
- skillsDir = dir;
185
- break;
186
- }
187
- }
188
- if (skillsDir) break;
189
- }
190
- }
191
-
192
- if (skillsDir) {
193
- for (const e of fs.readdirSync(skillsDir)) {
194
- const sp = path.join(skillsDir, e);
195
- if (fs.statSync(sp).isDirectory()) {
196
- // Check if this directory has SKILL.md (top-level skill only)
197
- if (fs.existsSync(path.join(sp, "SKILL.md"))) {
198
- const m = parseSkillMdFrontmatter(path.join(sp, "SKILL.md"));
199
- skillsInRepo.push({
200
- title: e, // Only show folder name
201
- value: e,
202
- description: m.description || "",
203
- selected: singleSkill ? e === singleSkill : true,
204
- _path: sp
205
- });
206
- }
207
- // NOTE: Sub-folders are NOT counted as separate skills
208
- // They are part of the parent skill (e.g. game-development/2d-games)
209
- }
210
- }
211
- }
212
-
213
- if (skillsInRepo.length === 0) {
214
- step(c.yellow("No valid skills found"), S.diamond, "yellow");
215
- step(c.dim("Expected skills in .agent/skills/ or skills/ directory"), S.branch, "gray");
216
- fs.rmSync(tmp, { recursive: true, force: true });
217
- return;
218
- }
219
-
220
- stepLine();
221
- step(`Found ${skillsInRepo.length} skill${skillsInRepo.length > 1 ? "s" : ""}`);
222
-
223
- let selectedSkills = [];
224
-
225
- // If single skill specified via #skill_name, auto-select it
226
- if (singleSkill) {
227
- const found = skillsInRepo.find(s => s.value === singleSkill);
228
- if (!found) {
229
- stepLine();
230
- step(c.red(`Skill '${singleSkill}' not found in repository`), S.cross, "red");
231
- fs.rmSync(tmp, { recursive: true, force: true });
232
- return;
233
- }
234
- selectedSkills = [singleSkill];
235
- stepLine();
236
- step(`Auto-selected: ${c.cyan(singleSkill)}`);
237
- } else {
238
- // FAANG-Grade Categories (8 balanced categories)
239
- // NOTE: Order matters! Specialized categories FIRST, Core is fallback
240
- const CATEGORY_KEYWORDS = {
241
- // Specialized domains
242
- "🎨 Frontend & UI": [
243
- "frontend", "nextjs", "tailwind", "css", "ui", "ux", "visual",
244
- "studio", "web-core", "design-system", "react-architect", "react"
245
- ],
246
- "🎮 Game Development": [
247
- "game", "development", "engine", "unity", "unreal", "godot", "phaser"
248
- ],
249
- "📱 Mobile": [
250
- "mobile", "first", "developer", "react-native", "flutter",
251
- "ios", "android", "swift", "kotlin"
252
- ],
253
- "🔒 Security & DevOps": [
254
- "security", "vulnerability", "offensive", "scanner", "red-team", "governance",
255
- "cicd", "pipeline", "gitops", "docker", "deploy", "server-ops"
256
- ],
257
- // Technical domains
258
- "🧪 Testing & Quality": [
259
- "test", "testing", "tdd", "e2e", "debug", "quality", "review",
260
- "lint", "validate", "automation", "problem", "checker"
261
- ],
262
- "🤖 AI & Agents": [
263
- "agent", "pattern", "auto-learn", "execution", "self-evolution",
264
- "lifecycle", "skill-forge", "intelligent", "routing"
265
- ],
266
- "📚 Docs & Planning": [
267
- "doc", "template", "plan", "project", "idea", "brainstorm",
268
- "geo", "seo", "i18n", "writing"
269
- ],
270
- // Fallback (core backend/infra)
271
- "⚙️ Backend & Core": [
272
- "backend", "api", "nodejs", "python", "server", "database",
273
- "prisma", "mcp", "data", "architect", "scaffold", "system",
274
- "typescript", "shell", "bash", "powershell", "git", "code-craft",
275
- "code-constitution", "observability", "perf", "state", "rollback"
276
- ]
277
- };
278
-
279
- function categorizeSkill(skillName) {
280
- const lower = skillName.toLowerCase();
281
- for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) {
282
- if (keywords.some(kw => lower.includes(kw))) {
283
- return category;
284
- }
285
- }
286
- return "⚙️ Backend & Core"; // Default fallback (no "Other" category)
287
- }
288
-
289
- // REQUIRED SKILLS - Always installed, not shown in selection
290
- const REQUIRED_SKILLS = ["auto-learner"];
291
-
292
- // Filter out required skills from selection list
293
- const selectableSkills = skillsInRepo.filter(s => !REQUIRED_SKILLS.includes(s.value));
294
-
295
- // Group skills by category
296
- const grouped = {};
297
- for (const skill of selectableSkills) {
298
- const cat = categorizeSkill(skill.value);
299
- if (!grouped[cat]) grouped[cat] = [];
300
- grouped[cat].push(skill);
301
- }
302
-
303
- // Custom sort: alphabetical but "Other" always last
304
- const sortedCategories = Object.keys(grouped).sort((a, b) => {
305
- if (a.includes("Other")) return 1;
306
- if (b.includes("Other")) return -1;
307
- return a.localeCompare(b);
308
- });
309
-
310
-
311
- stepLine();
312
- activeStep("Select skill categories to install");
313
-
314
- // Show only categories, not individual skills
315
- const selectedCategories = await multiselect({
316
- message: `${c.cyan("space")} select · ${c.cyan("enter")} confirm`,
317
- options: sortedCategories.map(cat => ({
318
- label: `${cat} (${grouped[cat].length} skills)`,
319
- value: cat,
320
- hint: grouped[cat].slice(0, 3).map(s => s.value).join(", ") + (grouped[cat].length > 3 ? "..." : "")
321
- })),
322
- initialValues: sortedCategories, // Pre-select all
323
- required: true
324
- });
325
-
326
- if (isCancel(selectedCategories)) {
327
- cancel("Cancelled.");
328
- fs.rmSync(tmp, { recursive: true, force: true });
329
- return;
330
- }
331
-
332
- // Get all skills from selected categories
333
- selectedSkills = selectedCategories.flatMap(cat => grouped[cat].map(s => s.value));
334
-
335
- // Add required system skills
336
- const requiredInRepo = skillsInRepo.filter(s => REQUIRED_SKILLS.includes(s.value)).map(s => s.value);
337
- selectedSkills = [...new Set([...selectedSkills, ...requiredInRepo])];
338
- }
339
-
340
- // Check for required skills and show info
341
- const CORE_REQUIRED = ["auto-learner"];
342
- const installedRequired = selectedSkills.filter(s => CORE_REQUIRED.includes(s));
343
-
344
- stepLine();
345
- step("Select skills to install");
346
- console.log(`${c.gray(S.branch)} ${c.dim(selectedSkills.filter(s => !CORE_REQUIRED.includes(s)).join(", "))}`);
347
- if (installedRequired.length > 0) {
348
- console.log(`${c.gray(S.branch)} ${c.cyan("+ System required:")} ${c.green(installedRequired.join(", "))}`);
349
- }
350
-
351
- // --- Detect installed agents ---
352
- stepLine();
353
- const { detectInstalledAgents } = await import("../agents.js");
354
- const detectedAgents = detectInstalledAgents();
355
-
356
- if (detectedAgents.length === 0) {
357
- step(c.yellow("No agents detected"), S.diamond, "yellow");
358
- step(c.dim("Please install at least one AI agent (Antigravity, Claude Code, etc.)"), S.branch, "gray");
359
- fs.rmSync(tmp, { recursive: true, force: true });
360
- return;
361
- }
362
-
363
- step(`Detected ${detectedAgents.length} agents`);
364
-
365
- // --- Select agents (Vercel-style) ---
366
- const { selectAgentsPrompt, selectScopePrompt, selectMethodPrompt } = await import("../ui.js");
367
-
368
- stepLine();
369
- activeStep("Install to");
370
- const selectedAgents = await selectAgentsPrompt(detectedAgents);
371
-
372
- if (!selectedAgents || selectedAgents.length === 0) {
373
- fs.rmSync(tmp, { recursive: true, force: true });
374
- return;
375
- }
376
-
377
- stepLine();
378
- step("Install to");
379
- console.log(`${c.gray(S.branch)} ${c.dim(selectedAgents.map(a => a.displayName).join(", "))}`);
380
-
381
- // --- Select installation scope ---
382
- let isGlobal = GLOBAL;
383
-
384
- if (!GLOBAL) {
385
- stepLine();
386
- activeStep("Installation scope");
387
- const scope = await selectScopePrompt();
388
-
389
- if (!scope) {
390
- fs.rmSync(tmp, { recursive: true, force: true });
391
- return;
392
- }
393
-
394
- isGlobal = scope === "global";
395
- }
396
-
397
- stepLine();
398
- step("Installation scope");
399
- console.log(`${c.gray(S.branch)} ${c.dim(isGlobal ? "Global" : "Project")}`);
400
-
401
- // --- Select installation method ---
402
- stepLine();
403
- activeStep("Installation method");
404
- const installMethod = await selectMethodPrompt();
405
-
406
- if (!installMethod) {
407
- fs.rmSync(tmp, { recursive: true, force: true });
408
- return;
409
- }
410
-
411
-
412
- // Installation Summary Box
413
- stepLine();
414
- step("Installation method");
415
- console.log(`${c.gray(S.branch)} ${c.dim(installMethod === "symlink" ? "Symlink" : "Copy")}`);
416
-
417
- stepLine();
418
- step("Installation Summary");
419
- stepLine();
420
-
421
- const agentsString = selectedAgents.map(a => a.displayName).join(", ");
422
-
423
- let summaryContent = "";
424
- const methodVerb = installMethod === "symlink" ? "symlink" : "copy";
425
-
426
- for (const sn of selectedSkills) {
427
- summaryContent += `${c.cyan(sn)}\n`;
428
- summaryContent += ` ${c.dim(methodVerb)} ${c.gray("→")} ${c.dim(agentsString)}\n\n`;
429
- }
430
-
431
- // Remove trailing newlines
432
- summaryContent = summaryContent.trim();
433
-
434
- console.log(boxen(summaryContent, {
435
- padding: 1,
436
- borderColor: "gray",
437
- borderStyle: "round",
438
- dimBorder: true,
439
- title: "Installation Summary",
440
- titleAlignment: "left"
441
- }).split("\n").map(l => `${c.gray(S.branch)} ${l}`).join("\n"));
442
-
443
- stepLine();
444
-
445
- // Confirmation
446
- activeStep("Proceed with installation?");
447
- const shouldProceed = await confirm({ message: " ", initialValue: true });
448
-
449
- if (isCancel(shouldProceed) || !shouldProceed) {
450
- cancel("Cancelled.");
451
- fs.rmSync(tmp, { recursive: true, force: true });
452
- return;
453
- }
454
-
455
- // Install skills to multiple agents
456
- stepLine();
457
- const { installSkillForAgents } = await import("../installer.js");
458
-
459
- // Create a map for skill paths
460
- const skillPathMap = Object.fromEntries(skillsInRepo.map(s => [s.value, s._path]));
461
-
462
- const installResults = { success: [], failed: [] };
463
-
464
- // --- Parallel Skill Installation (Batch processing) ---
465
- const CONCURRENCY_LIMIT = 5; // Process 5 skills at a time
466
- const totalSkills = selectedSkills.length;
467
-
468
- // Create installation function
469
- async function installSingleSkill(sn) {
470
- const src = skillPathMap[sn] || path.join(skillsDir || tmp, sn);
471
- const result = await installSkillForAgents(src, sn, selectedAgents, {
472
- method: installMethod,
473
- scope: isGlobal ? "global" : "project",
474
- metadata: {
475
- repo: `${org}/${repo}`,
476
- ref: ref || null
477
- }
478
- });
479
- return { skill: sn, ...result };
480
- }
481
-
482
- // Progress spinner for batch installation
483
- const batchSpinner = spinner();
484
- batchSpinner.start(`Installing ${totalSkills} skills to ${selectedAgents.length} agents (parallel)...`);
485
-
486
- // Process in batches
487
- let completed = 0;
488
- for (let i = 0; i < selectedSkills.length; i += CONCURRENCY_LIMIT) {
489
- const batch = selectedSkills.slice(i, i + CONCURRENCY_LIMIT);
490
- const batchResults = await Promise.all(batch.map(installSingleSkill));
491
-
492
- for (const r of batchResults) {
493
- installResults.success.push(...r.success);
494
- installResults.failed.push(...r.failed);
495
- completed++;
496
- }
497
-
498
- batchSpinner.message(`Installing skills... ${completed}/${totalSkills}`);
499
- }
500
-
501
- batchSpinner.stop(`Installed ${completed} skills (${installResults.success.length} agents, ${installResults.failed.length} failed)`);
502
-
503
-
504
- // Derive base .agent directory from skillsDir
505
- // If skillsDir is .../skills, then baseAgentDir is parent (.agent)
506
- const baseAgentDir = skillsDir ? path.dirname(skillsDir) : path.join(tmp, ".agent");
507
-
508
- // Install workflows if they exist
509
- const workflowsDir = path.join(baseAgentDir, "workflows");
510
- const targetWorkflowsDir = path.join(WORKSPACE, "..", "workflows");
511
- let workflowsInstalled = 0;
512
-
513
-
514
- if (fs.existsSync(workflowsDir)) {
515
- stepLine();
516
- const ws = spinner();
517
- ws.start("Installing workflows");
518
-
519
- fs.mkdirSync(targetWorkflowsDir, { recursive: true });
520
- const workflows = fs.readdirSync(workflowsDir).filter(f => f.endsWith(".md"));
521
-
522
- for (const wf of workflows) {
523
- const src = path.join(workflowsDir, wf);
524
- const dest = path.join(targetWorkflowsDir, wf);
525
-
526
- if (!fs.existsSync(dest)) {
527
- fs.copyFileSync(src, dest);
528
- workflowsInstalled++;
529
- }
530
- }
531
-
532
- ws.stop(`Installed ${workflowsInstalled} workflows`);
533
- }
534
-
535
- // Install GEMINI.md if it exists
536
- const geminiSrc = path.join(baseAgentDir, "GEMINI.md");
537
- const geminiDest = path.join(WORKSPACE, "..", "GEMINI.md");
538
- let geminiInstalled = false;
539
-
540
- if (fs.existsSync(geminiSrc) && !fs.existsSync(geminiDest)) {
541
- stepLine();
542
- fs.copyFileSync(geminiSrc, geminiDest);
543
- step("Installed GEMINI.md (Agent Rules)");
544
- geminiInstalled = true;
545
- }
546
-
547
- // Install agents if they exist
548
- const agentsDir = path.join(baseAgentDir, "agents");
549
- const targetAgentsDir = path.join(WORKSPACE, "..", "agents");
550
- let agentsInstalled = 0;
551
-
552
- if (fs.existsSync(agentsDir)) {
553
- stepLine();
554
- const as = spinner();
555
- as.start("Installing agents");
556
-
557
- fs.mkdirSync(targetAgentsDir, { recursive: true });
558
- const agents = fs.readdirSync(agentsDir).filter(f => f.endsWith(".md"));
559
-
560
- for (const agent of agents) {
561
- const src = path.join(agentsDir, agent);
562
- const dest = path.join(targetAgentsDir, agent);
563
-
564
- if (!fs.existsSync(dest)) {
565
- fs.copyFileSync(src, dest);
566
- agentsInstalled++;
567
- }
568
- }
569
-
570
- as.stop(`Installed ${agentsInstalled} agents`);
571
- }
572
-
573
- // Install ARCHITECTURE.md if it exists
574
- const archSrc = path.join(baseAgentDir, "ARCHITECTURE.md");
575
- const archDest = path.join(WORKSPACE, "..", "ARCHITECTURE.md");
576
- let archInstalled = false;
577
-
578
- if (fs.existsSync(archSrc) && !fs.existsSync(archDest)) {
579
- fs.copyFileSync(archSrc, archDest);
580
- step("Installed ARCHITECTURE.md");
581
- archInstalled = true;
582
- }
583
-
584
- // Install knowledge if it exists (required for Agent CLI)
585
- const knowledgeDir = path.join(baseAgentDir, "knowledge");
586
- const targetKnowledgeDir = path.join(WORKSPACE, "..", "knowledge");
587
- let knowledgeInstalled = false;
588
-
589
- if (fs.existsSync(knowledgeDir) && !fs.existsSync(targetKnowledgeDir)) {
590
- fs.cpSync(knowledgeDir, targetKnowledgeDir, { recursive: true });
591
- step("Installed knowledge/");
592
- knowledgeInstalled = true;
593
- } else if (!fs.existsSync(targetKnowledgeDir)) {
594
- // Create empty knowledge folder for Agent CLI
595
- fs.mkdirSync(targetKnowledgeDir, { recursive: true });
596
- // Create minimal structure for Agent CLI
597
- fs.writeFileSync(path.join(targetKnowledgeDir, "lessons-learned.yaml"), "# Lessons learned by AI Agent\nlessons: []\n");
598
- step("Created knowledge/ (Agent CLI ready)");
599
- knowledgeInstalled = true;
600
- }
601
-
602
- // Install config/ if it exists (required for skill configuration)
603
- const configDir = path.join(baseAgentDir, "config");
604
- const targetConfigDir = path.join(WORKSPACE, "..", "config");
605
- let configInstalled = false;
606
-
607
- if (fs.existsSync(configDir) && !fs.existsSync(targetConfigDir)) {
608
- fs.cpSync(configDir, targetConfigDir, { recursive: true });
609
- step("Installed config/");
610
- configInstalled = true;
611
- }
612
-
613
- // Install scripts-js/ if it exists (required for skill operations)
614
- const scriptsJsDir = path.join(baseAgentDir, "scripts-js");
615
- const targetScriptsJsDir = path.join(WORKSPACE, "..", "scripts-js");
616
- let scriptsJsInstalled = false;
617
-
618
- if (fs.existsSync(scriptsJsDir) && !fs.existsSync(targetScriptsJsDir)) {
619
- fs.cpSync(scriptsJsDir, targetScriptsJsDir, { recursive: true });
620
- step("Installed scripts-js/");
621
- scriptsJsInstalled = true;
622
- }
623
-
624
- // Install metrics/ if it exists (for agent performance tracking)
625
- const metricsDir = path.join(baseAgentDir, "metrics");
626
- const targetMetricsDir = path.join(WORKSPACE, "..", "metrics");
627
- let metricsInstalled = false;
628
-
629
- if (fs.existsSync(metricsDir) && !fs.existsSync(targetMetricsDir)) {
630
- fs.cpSync(metricsDir, targetMetricsDir, { recursive: true });
631
- step("Installed metrics/");
632
- metricsInstalled = true;
633
- }
634
-
635
- // Install additional policy documents
636
- const policyDocs = [
637
- "CONTINUOUS_EXECUTION_POLICY.md",
638
- "WORKFLOW_CHAINS.md"
639
- ];
640
- let policyDocsInstalled = 0;
641
-
642
- for (const doc of policyDocs) {
643
- const docSrc = path.join(baseAgentDir, doc);
644
- const docDest = path.join(WORKSPACE, "..", doc);
645
- if (fs.existsSync(docSrc) && !fs.existsSync(docDest)) {
646
- fs.copyFileSync(docSrc, docDest);
647
- policyDocsInstalled++;
648
- }
649
- }
650
- if (policyDocsInstalled > 0) {
651
- step(`Installed ${policyDocsInstalled} policy docs`);
652
- }
653
-
654
- // Install rules if they exist
655
- const rulesDir = path.join(baseAgentDir, "rules");
656
- const targetRulesDir = path.join(WORKSPACE, "..", "rules");
657
- let rulesInstalled = 0;
658
-
659
- if (fs.existsSync(rulesDir)) {
660
- fs.mkdirSync(targetRulesDir, { recursive: true });
661
- const rules = fs.readdirSync(rulesDir).filter(f => f.endsWith(".md"));
662
-
663
- for (const rule of rules) {
664
- const src = path.join(rulesDir, rule);
665
- const dest = path.join(targetRulesDir, rule);
666
-
667
- if (!fs.existsSync(dest)) {
668
- fs.copyFileSync(src, dest);
669
- rulesInstalled++;
670
- }
671
- }
672
-
673
- if (rulesInstalled > 0) {
674
- step(`Installed ${rulesInstalled} rules`);
675
- }
676
- }
677
-
678
- // Install .shared if it exists (contains shared resources like ui-ux-pro-max data)
679
- const sharedDir = path.join(tmp, ".agent", ".shared");
680
- const targetSharedDir = path.join(WORKSPACE, "..", ".shared");
681
- let sharedInstalled = false;
682
-
683
- if (fs.existsSync(sharedDir) && !fs.existsSync(targetSharedDir)) {
684
- stepLine();
685
- const ss = spinner();
686
- ss.start("Installing shared resources");
687
-
688
- fs.cpSync(sharedDir, targetSharedDir, { recursive: true });
689
- sharedInstalled = true;
690
-
691
- ss.stop("Installed .shared/ (ui-ux-pro-max data)");
692
- }
693
-
694
- // Installation complete step
695
- stepLine();
696
- step("Installation complete");
697
-
698
- // Final Success Box
699
- stepLine();
700
- console.log(`${c.gray(S.branch)}`); // Extra spacing line
701
-
702
- let successContent = "";
703
-
704
- // Skills summary
705
- for (const sn of selectedSkills) {
706
- const mockPath = `.agent/skills/${sn}`;
707
- successContent += `${c.cyan("✓")} ${c.dim(mockPath)}\n`;
708
- }
709
-
710
- // Workflows summary
711
- if (workflowsInstalled > 0) {
712
- successContent += `${c.cyan("✓")} ${c.dim(`.agent/workflows/ (${workflowsInstalled} files)`)}\n`;
713
- }
714
-
715
- // Agents summary
716
- if (agentsInstalled > 0) {
717
- successContent += `${c.cyan("✓")} ${c.dim(`.agent/agents/ (${agentsInstalled} files)`)}\n`;
718
- }
719
-
720
- // GEMINI.md summary
721
- if (geminiInstalled) {
722
- successContent += `${c.cyan("✓")} ${c.dim(".agent/GEMINI.md (Agent Rules)")}\n`;
723
- }
724
-
725
- // ARCHITECTURE.md summary
726
- if (archInstalled) {
727
- successContent += `${c.cyan("✓")} ${c.dim(".agent/ARCHITECTURE.md")}\n`;
728
- }
729
-
730
- // Knowledge summary
731
- if (knowledgeInstalled) {
732
- successContent += `${c.cyan("✓")} ${c.dim(".agent/knowledge/")}\n`;
733
- }
734
-
735
- // Rules summary
736
- if (rulesInstalled > 0) {
737
- successContent += `${c.cyan("✓")} ${c.dim(`.agent/rules/ (${rulesInstalled} files)`)}\n`;
738
- }
739
-
740
- // Shared resources summary
741
- if (sharedInstalled) {
742
- successContent += `${c.cyan("✓")} ${c.dim(".agent/.shared/ (ui-ux-pro-max data)")}\n`;
743
- }
744
-
745
- // Build title
746
- const parts = [`${selectedSkills.length} skills`];
747
- if (workflowsInstalled > 0) parts.push(`${workflowsInstalled} workflows`);
748
- if (agentsInstalled > 0) parts.push(`${agentsInstalled} agents`);
749
- if (geminiInstalled) parts.push("GEMINI.md");
750
-
751
- console.log(boxen(successContent.trim(), {
752
- padding: 1,
753
- borderColor: "cyan",
754
- borderStyle: "round",
755
- title: c.cyan(`Installed ${parts.join(", ")}`),
756
- titleAlignment: "left"
757
- }).split("\n").map(l => `${c.gray(S.branch)} ${l}`).join("\n"));
758
-
759
- fs.rmSync(tmp, { recursive: true, force: true });
760
-
761
- // Ask user about AutoLearn installation
762
- stepLine();
763
- const installAutoLearn = await confirm({
764
- message: "Install AutoLearn (enables 'agent' command for learning & self-improvement)?",
765
- initialValue: true
766
- });
767
-
768
- if (isCancel(installAutoLearn)) {
769
- cancel("Installation cancelled");
770
- process.exit(0);
771
- }
772
-
773
- // Install CLI package
774
- stepLine();
775
- const cliSpinner = spinner();
776
- const cliPackage = "pikakit";
777
-
778
- if (isGlobal) {
779
- if (installAutoLearn) {
780
- cliSpinner.start(`Installing CLI globally (${cliPackage})`);
781
- } else {
782
- cliSpinner.start(`Installing kit CLI globally (${cliPackage})`);
783
- }
784
- try {
785
- await execAsync(`npm install -g ${cliPackage}`, { timeout: 120000 });
786
- cliSpinner.stop("CLI installed globally");
787
- if (installAutoLearn) {
788
- step(c.dim("Commands: agent, kit"));
789
- } else {
790
- step(c.dim("Command: kit"));
791
- }
792
- } catch (e) {
793
- cliSpinner.stop(c.yellow("Global CLI installation failed"));
794
- step(c.dim(`Try running manually: npm i -g ${cliPackage}`));
795
- }
796
- } else {
797
- cliSpinner.start(`Installing Agent CLI locally (${cliPackage})`);
798
- try {
799
- await execAsync(`npm install -D ${cliPackage}`, { timeout: 120000 });
800
- cliSpinner.stop("CLI installed locally");
801
-
802
- // Add npm scripts to package.json
803
- try {
804
- const pkgPath = path.join(process.cwd(), "package.json");
805
- if (fs.existsSync(pkgPath)) {
806
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
807
- pkg.scripts = pkg.scripts || {};
808
-
809
- // Always add kit script
810
- if (!pkg.scripts.kit) {
811
- pkg.scripts.kit = "kit";
812
- }
813
-
814
- // Add agent script only if AutoLearn enabled
815
- if (installAutoLearn && !pkg.scripts.agent) {
816
- pkg.scripts.agent = "agent";
817
- }
818
-
819
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
820
- if (installAutoLearn) {
821
- step(c.green("✓ Added npm scripts: 'agent', 'kit'"));
822
- } else {
823
- step(c.green("✓ Added npm script: 'kit'"));
824
- }
825
- }
826
- } catch (scriptErr) {
827
- step(c.yellow("âš  Could not add npm scripts automatically"));
828
- }
829
-
830
- // Create wrapper scripts for direct command access (Windows + Unix)
831
- try {
832
- const projectRoot = process.cwd();
833
- const binDir = path.join(projectRoot, "node_modules", ".bin");
834
-
835
- // Always create kit wrappers at project root
836
- const kitCmd = `@echo off\nnode "%~dp0node_modules\\pikakit\\bin\\kit.js" %*`;
837
- const kitSh = `#!/bin/sh\nnode "$(dirname "$0")/node_modules/pikakit/bin/kit.js" "$@"`;
838
- fs.writeFileSync(path.join(projectRoot, "kit.cmd"), kitCmd);
839
- fs.writeFileSync(path.join(projectRoot, "kit"), kitSh, { mode: 0o755 });
840
-
841
- // Also create in node_modules/.bin for npx support
842
- if (fs.existsSync(binDir)) {
843
- const kitBinCmd = `@echo off\nnode "%~dp0..\\pikakit\\bin\\kit.js" %*`;
844
- const kitBinSh = `#!/bin/sh\nnode "$(dirname "$0")/../pikakit/bin/kit.js" "$@"`;
845
- fs.writeFileSync(path.join(binDir, "kit.cmd"), kitBinCmd);
846
- fs.writeFileSync(path.join(binDir, "kit"), kitBinSh, { mode: 0o755 });
847
- }
848
-
849
- if (installAutoLearn) {
850
- // Create agent wrappers only if AutoLearn enabled
851
- const agentCmd = `@echo off\nnode "%~dp0node_modules\\pikakit\\lib\\agent-cli\\bin\\agent.js" %*`;
852
- const agentSh = `#!/bin/sh\nnode "$(dirname "$0")/node_modules/pikakit/lib/agent-cli/bin/agent.js" "$@"`;
853
- fs.writeFileSync(path.join(projectRoot, "agent.cmd"), agentCmd);
854
- fs.writeFileSync(path.join(projectRoot, "agent"), agentSh, { mode: 0o755 });
855
-
856
- // Also create in node_modules/.bin for npx support
857
- if (fs.existsSync(binDir)) {
858
- const agentBinCmd = `@echo off\nnode "%~dp0..\\pikakit\\lib\\agent-cli\\bin\\agent.js" %*`;
859
- const agentBinSh = `#!/bin/sh\nnode "$(dirname "$0")/../pikakit/lib/agent-cli/bin/agent.js" "$@"`;
860
- fs.writeFileSync(path.join(binDir, "agent.cmd"), agentBinCmd);
861
- fs.writeFileSync(path.join(binDir, "agent"), agentBinSh, { mode: 0o755 });
862
- }
863
-
864
- step(c.green("✔ Created wrapper scripts: agent, kit"));
865
- step(c.dim("Run: npx agent | npx kit | .\\agent | .\\kit (Windows)"));
866
- } else {
867
- step(c.green(" Created wrapper script: kit"));
868
- step(c.dim("Run: npx kit | .\\kit (Windows)"));
869
- }
870
- } catch (wrapperErr) {
871
- step(c.dim("Run: npx kit"));
872
- }
873
- } catch (e) {
874
- cliSpinner.stop(c.yellow("Local CLI installation skipped"));
875
- step(c.dim(`Run manually: npm i -D ${cliPackage}`));
876
- }
877
- }
878
-
879
- // Run npm install to ensure all skill dependencies are available
880
- stepLine();
881
- const depsSpinner = spinner();
882
- depsSpinner.start("Installing skill dependencies (csv-parse, etc.)");
883
- try {
884
- await execAsync("npm install", { timeout: 120000 });
885
- depsSpinner.stop("Skill dependencies installed");
886
- } catch (e) {
887
- depsSpinner.stop(c.yellow("Dependencies installation skipped"));
888
- step(c.dim("Run manually: npm install"));
889
- }
890
-
891
- // Python dependencies no longer needed - all scripts migrated to JS
892
-
893
- // Final Quick Start Guide
894
- stepLine();
895
- console.log(boxen(
896
- `${c.cyan("Quick Start Commands:")}\n\n` +
897
- `${c.green("Option 1:")} ${c.white("npm run kit")} ${c.dim("// npm scripts")}\n` +
898
- `${c.green("Option 2:")} ${c.white("npx kit")} ${c.dim("// npx")}\n` +
899
- `${c.green("Option 3:")} ${c.white(".\\\\kit")} ${c.dim("// PowerShell direct")}\n\n` +
900
- (installAutoLearn ?
901
- `${c.cyan("Agent Commands:")}\n` +
902
- `${c.white("npm run agent")} ${c.dim("or")} ${c.white("npx agent")} ${c.dim("or")} ${c.white(".\\\\agent")}\n\n` : "") +
903
- `${c.yellow("Note:")} Windows PowerShell requires ${c.white(".\\\\")}\n` +
904
- `prefix to run scripts from current directory.`,
905
- {
906
- padding: 1,
907
- borderColor: "cyan",
908
- borderStyle: "round",
909
- title: c.cyan("⚡ Get Started"),
910
- titleAlignment: "left"
911
- }
912
- ).split("\n").map(l => `${c.gray(S.branch)} ${l}`).join("\n"));
913
-
914
- stepLine();
915
- console.log(` ${c.cyan("Done!")}`);
916
- console.log();
917
- }
1
+ /**
2
+ * @fileoverview Install command - Interactive skill installation
3
+ */
4
+
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import os from "os";
8
+ import { exec } from "child_process";
9
+ import util from "util";
10
+ const execAsync = util.promisify(exec);
11
+ import boxen from "boxen";
12
+ import { parseSkillSpec, merkleHash } from "../helpers.js";
13
+ import { parseSkillMdFrontmatter } from "../skills.js";
14
+ import { step, activeStep, stepLine, S, c, fatal, spinner, multiselect, select, confirm, isCancel, cancel } from "../ui.js";
15
+ import { WORKSPACE, GLOBAL_DIR, OFFLINE, GLOBAL, REPO_CACHE_DIR, CACHE_TTL_MS, FORCE_REFRESH } from "../config.js";
16
+ import { installSkill } from "../installer.js";
17
+
18
+ /**
19
+ * Install skills from repository
20
+ * @param {string} spec - Skill spec (org/repo or org/repo#skill)
21
+ */
22
+ export async function run(spec) {
23
+ if (!spec) {
24
+ fatal("Missing skill spec. Usage: add-skill <org/repo>");
25
+ return;
26
+ }
27
+
28
+ const { org, repo, skill: singleSkill, ref } = parseSkillSpec(spec);
29
+
30
+ if (!org || !repo) {
31
+ fatal("Invalid spec. Format: org/repo or org/repo#skill");
32
+ return;
33
+ }
34
+
35
+ // Check offline mode
36
+ if (OFFLINE) {
37
+ stepLine();
38
+ step(c.yellow("Offline mode enabled"), S.diamond, "yellow");
39
+ step(c.dim("Cannot install from remote repository in offline mode"), S.branch, "gray");
40
+ step(c.dim("Use --locked to install from lockfile instead"), S.branch, "gray");
41
+ return;
42
+ }
43
+
44
+ const url = `https://github.com/${org}/${repo}.git`;
45
+
46
+ stepLine();
47
+ step("Source: " + c.cyan(url));
48
+
49
+ const s = spinner();
50
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "add-skill-"));
51
+
52
+ // --- Repository Caching Logic ---
53
+ const cacheDir = path.join(REPO_CACHE_DIR, org, repo);
54
+ const cacheMetaFile = path.join(cacheDir, ".cache-meta.json");
55
+ let useCache = false;
56
+ let cacheHit = false;
57
+
58
+ // Check if cache exists and is valid
59
+ if (!FORCE_REFRESH && fs.existsSync(cacheDir) && fs.existsSync(cacheMetaFile)) {
60
+ try {
61
+ const meta = JSON.parse(fs.readFileSync(cacheMetaFile, "utf-8"));
62
+ const cacheAge = Date.now() - new Date(meta.timestamp).getTime();
63
+
64
+ if (cacheAge < CACHE_TTL_MS) {
65
+ useCache = true;
66
+ cacheHit = true;
67
+ s.start("Deploying next-generation skills...");
68
+ }
69
+ } catch {
70
+ // Invalid cache, will refresh
71
+ }
72
+ }
73
+
74
+ let lastError = null;
75
+ const MAX_RETRIES = 3;
76
+
77
+ if (useCache) {
78
+ // Fast path: Update cache with git fetch (much faster than clone)
79
+ try {
80
+ await execAsync(`git -C "${cacheDir}" fetch --depth=1 origin HEAD`, { timeout: 30000 });
81
+ await execAsync(`git -C "${cacheDir}" reset --hard FETCH_HEAD`, { timeout: 10000 });
82
+
83
+ // Copy from cache to tmp
84
+ await fs.promises.cp(cacheDir, tmp, { recursive: true });
85
+
86
+ // Update cache metadata
87
+ fs.writeFileSync(cacheMetaFile, JSON.stringify({
88
+ timestamp: new Date().toISOString(),
89
+ org, repo, ref: ref || "HEAD"
90
+ }));
91
+
92
+ s.stop("Skills deployed successfully");
93
+ } catch (err) {
94
+ // Cache update failed, fall back to fresh clone
95
+ useCache = false;
96
+ cacheHit = false;
97
+ }
98
+ }
99
+
100
+ if (!useCache) {
101
+ // Fresh clone with retry logic
102
+ s.start(FORCE_REFRESH ? "Force refreshing repository..." : "Deploying next-generation skills...");
103
+
104
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
105
+ try {
106
+ await execAsync(`git clone --depth=1 ${url} "${tmp}"`, { timeout: 60000 });
107
+ if (ref) await execAsync(`git -C "${tmp}" checkout ${ref}`, { timeout: 30000 });
108
+ lastError = null;
109
+
110
+ // Save to cache for next time
111
+ try {
112
+ fs.mkdirSync(path.dirname(cacheDir), { recursive: true });
113
+ if (fs.existsSync(cacheDir)) fs.rmSync(cacheDir, { recursive: true, force: true });
114
+ await fs.promises.cp(tmp, cacheDir, { recursive: true });
115
+ fs.writeFileSync(path.join(cacheDir, ".cache-meta.json"), JSON.stringify({
116
+ timestamp: new Date().toISOString(),
117
+ org, repo, ref: ref || "HEAD"
118
+ }));
119
+ } catch { /* Cache write failed, non-fatal */ }
120
+
121
+ break;
122
+ } catch (err) {
123
+ lastError = err;
124
+
125
+ if (attempt < MAX_RETRIES) {
126
+ const delay = Math.pow(2, attempt) * 1000; // 2s, 4s
127
+ s.message(`Retry ${attempt}/${MAX_RETRIES} in ${delay / 1000}s...`);
128
+ await new Promise(r => setTimeout(r, delay));
129
+
130
+ // Clean up failed attempt
131
+ try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { }
132
+ fs.mkdirSync(tmp, { recursive: true });
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ if (lastError) {
139
+ s.fail("Failed to clone repository");
140
+ stepLine();
141
+
142
+ // Provide helpful error messages
143
+ const errMsg = lastError.message || "";
144
+ if (errMsg.includes("not found") || errMsg.includes("404")) {
145
+ step(c.red(`Repository not found: ${org}/${repo}`), S.cross, "red");
146
+ step(c.dim("Check if the repository exists and is public"), S.branch, "gray");
147
+ } else if (errMsg.includes("timeout")) {
148
+ step(c.red("Connection timeout"), S.cross, "red");
149
+ step(c.dim("Check your internet connection and try again"), S.branch, "gray");
150
+ } else if (errMsg.includes("Could not resolve")) {
151
+ step(c.red("Network error: Unable to reach GitHub"), S.cross, "red");
152
+ step(c.dim("Check your internet connection"), S.branch, "gray");
153
+ } else {
154
+ step(c.red("Clone failed: " + errMsg.split("\n")[0]), S.cross, "red");
155
+ }
156
+
157
+ fs.rmSync(tmp, { recursive: true, force: true });
158
+ return;
159
+ }
160
+
161
+ if (!cacheHit) {
162
+ s.stop("Skills deployed successfully");
163
+ }
164
+
165
+ // Find skills in repo - check multiple possible locations
166
+ const skillsInRepo = [];
167
+
168
+ // Possible skill locations (in order of priority)
169
+ const possibleSkillDirs = [
170
+ path.join(tmp, ".agent", "skills"), // Standard location
171
+ path.join(tmp, "skills"), // Alternative location
172
+ tmp // Root level (legacy)
173
+ ];
174
+
175
+ let skillsDir = null;
176
+ for (const dir of possibleSkillDirs) {
177
+ if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
178
+ // Check if this directory contains skill folders
179
+ const entries = fs.readdirSync(dir);
180
+ for (const e of entries) {
181
+ const sp = path.join(dir, e);
182
+ if (fs.statSync(sp).isDirectory() && fs.existsSync(path.join(sp, "SKILL.md"))) {
183
+ skillsDir = dir;
184
+ break;
185
+ }
186
+ }
187
+ if (skillsDir) break;
188
+ }
189
+ }
190
+
191
+ if (skillsDir) {
192
+ for (const e of fs.readdirSync(skillsDir)) {
193
+ const sp = path.join(skillsDir, e);
194
+ if (fs.statSync(sp).isDirectory()) {
195
+ // Check if this directory has SKILL.md (top-level skill only)
196
+ if (fs.existsSync(path.join(sp, "SKILL.md"))) {
197
+ const m = parseSkillMdFrontmatter(path.join(sp, "SKILL.md"));
198
+ skillsInRepo.push({
199
+ title: e, // Only show folder name
200
+ value: e,
201
+ description: m.description || "",
202
+ selected: singleSkill ? e === singleSkill : true,
203
+ _path: sp
204
+ });
205
+ }
206
+ // NOTE: Sub-folders are NOT counted as separate skills
207
+ // They are part of the parent skill (e.g. game-development/2d-games)
208
+ }
209
+ }
210
+ }
211
+
212
+ if (skillsInRepo.length === 0) {
213
+ step(c.yellow("No valid skills found"), S.diamond, "yellow");
214
+ step(c.dim("Expected skills in .agent/skills/ or skills/ directory"), S.branch, "gray");
215
+ fs.rmSync(tmp, { recursive: true, force: true });
216
+ return;
217
+ }
218
+
219
+ stepLine();
220
+ step(`Found ${skillsInRepo.length} skill${skillsInRepo.length > 1 ? "s" : ""}`);
221
+
222
+ let selectedSkills = [];
223
+
224
+ // If single skill specified via #skill_name, auto-select it
225
+ if (singleSkill) {
226
+ const found = skillsInRepo.find(s => s.value === singleSkill);
227
+ if (!found) {
228
+ stepLine();
229
+ step(c.red(`Skill '${singleSkill}' not found in repository`), S.cross, "red");
230
+ fs.rmSync(tmp, { recursive: true, force: true });
231
+ return;
232
+ }
233
+ selectedSkills = [singleSkill];
234
+ stepLine();
235
+ step(`Auto-selected: ${c.cyan(singleSkill)}`);
236
+ } else {
237
+ // FAANG-Grade Categories (8 balanced categories)
238
+ // NOTE: Order matters! Specialized categories FIRST, Core is fallback
239
+ const CATEGORY_KEYWORDS = {
240
+ // Specialized domains
241
+ "🎨 Frontend & UI": [
242
+ "frontend", "nextjs", "tailwind", "css", "ui", "ux", "visual",
243
+ "studio", "web-core", "design-system", "react-architect", "react"
244
+ ],
245
+ "🎮 Game Development": [
246
+ "game", "development", "engine", "unity", "unreal", "godot", "phaser"
247
+ ],
248
+ "📱 Mobile": [
249
+ "mobile", "first", "developer", "react-native", "flutter",
250
+ "ios", "android", "swift", "kotlin"
251
+ ],
252
+ "🔒 Security & DevOps": [
253
+ "security", "vulnerability", "offensive", "scanner", "red-team", "governance",
254
+ "cicd", "pipeline", "gitops", "docker", "deploy", "server-ops"
255
+ ],
256
+ // Technical domains
257
+ "🧪 Testing & Quality": [
258
+ "test", "testing", "tdd", "e2e", "debug", "quality", "review",
259
+ "lint", "validate", "automation", "problem", "checker"
260
+ ],
261
+ "🤖 AI & Agents": [
262
+ "agent", "pattern", "auto-learn", "execution", "self-evolution",
263
+ "lifecycle", "skill-forge", "intelligent", "routing"
264
+ ],
265
+ "📚 Docs & Planning": [
266
+ "doc", "template", "plan", "project", "idea", "brainstorm",
267
+ "geo", "seo", "i18n", "writing"
268
+ ],
269
+ // Fallback (core backend/infra)
270
+ "⚙️ Backend & Core": [
271
+ "backend", "api", "nodejs", "python", "server", "database",
272
+ "prisma", "mcp", "data", "architect", "scaffold", "system",
273
+ "typescript", "shell", "bash", "powershell", "git", "code-craft",
274
+ "code-constitution", "observability", "perf", "state", "rollback"
275
+ ]
276
+ };
277
+
278
+ function categorizeSkill(skillName) {
279
+ const lower = skillName.toLowerCase();
280
+ for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) {
281
+ if (keywords.some(kw => lower.includes(kw))) {
282
+ return category;
283
+ }
284
+ }
285
+ return "⚙️ Backend & Core"; // Default fallback (no "Other" category)
286
+ }
287
+
288
+ // REQUIRED SKILLS - Always installed, not shown in selection
289
+ const REQUIRED_SKILLS = ["auto-learner"];
290
+
291
+ // Filter out required skills from selection list
292
+ const selectableSkills = skillsInRepo.filter(s => !REQUIRED_SKILLS.includes(s.value));
293
+
294
+ // Group skills by category
295
+ const grouped = {};
296
+ for (const skill of selectableSkills) {
297
+ const cat = categorizeSkill(skill.value);
298
+ if (!grouped[cat]) grouped[cat] = [];
299
+ grouped[cat].push(skill);
300
+ }
301
+
302
+ // Custom sort: alphabetical but "Other" always last
303
+ const sortedCategories = Object.keys(grouped).sort((a, b) => {
304
+ if (a.includes("Other")) return 1;
305
+ if (b.includes("Other")) return -1;
306
+ return a.localeCompare(b);
307
+ });
308
+
309
+
310
+ stepLine();
311
+ activeStep("Select skill categories to install");
312
+
313
+ // Show only categories, not individual skills
314
+ const selectedCategories = await multiselect({
315
+ message: `${c.cyan("space")} select · ${c.cyan("enter")} confirm`,
316
+ options: sortedCategories.map(cat => ({
317
+ label: `${cat} (${grouped[cat].length} skills)`,
318
+ value: cat,
319
+ hint: grouped[cat].slice(0, 3).map(s => s.value).join(", ") + (grouped[cat].length > 3 ? "..." : "")
320
+ })),
321
+ initialValues: sortedCategories, // Pre-select all
322
+ required: true
323
+ });
324
+
325
+ if (isCancel(selectedCategories)) {
326
+ cancel("Cancelled.");
327
+ fs.rmSync(tmp, { recursive: true, force: true });
328
+ return;
329
+ }
330
+
331
+ // Get all skills from selected categories
332
+ selectedSkills = selectedCategories.flatMap(cat => grouped[cat].map(s => s.value));
333
+
334
+ // Add required system skills
335
+ const requiredInRepo = skillsInRepo.filter(s => REQUIRED_SKILLS.includes(s.value)).map(s => s.value);
336
+ selectedSkills = [...new Set([...selectedSkills, ...requiredInRepo])];
337
+ }
338
+
339
+ // Check for required skills and show info
340
+ const CORE_REQUIRED = ["auto-learner"];
341
+ const installedRequired = selectedSkills.filter(s => CORE_REQUIRED.includes(s));
342
+
343
+ stepLine();
344
+ step("Select skills to install");
345
+ console.log(`${c.gray(S.branch)} ${c.dim(selectedSkills.filter(s => !CORE_REQUIRED.includes(s)).join(", "))}`);
346
+ if (installedRequired.length > 0) {
347
+ console.log(`${c.gray(S.branch)} ${c.cyan("+ System required:")} ${c.green(installedRequired.join(", "))}`);
348
+ }
349
+
350
+ // --- Detect installed agents ---
351
+ stepLine();
352
+ const { detectInstalledAgents } = await import("../agents.js");
353
+ const detectedAgents = detectInstalledAgents();
354
+
355
+ if (detectedAgents.length === 0) {
356
+ step(c.yellow("No agents detected"), S.diamond, "yellow");
357
+ step(c.dim("Please install at least one AI agent (Antigravity, Claude Code, etc.)"), S.branch, "gray");
358
+ fs.rmSync(tmp, { recursive: true, force: true });
359
+ return;
360
+ }
361
+
362
+ step(`Detected ${detectedAgents.length} agents`);
363
+
364
+ // --- Select agents (Vercel-style) ---
365
+ const { selectAgentsPrompt, selectScopePrompt, selectMethodPrompt } = await import("../ui.js");
366
+
367
+ stepLine();
368
+ activeStep("Install to");
369
+ const selectedAgents = await selectAgentsPrompt(detectedAgents);
370
+
371
+ if (!selectedAgents || selectedAgents.length === 0) {
372
+ fs.rmSync(tmp, { recursive: true, force: true });
373
+ return;
374
+ }
375
+
376
+ stepLine();
377
+ step("Install to");
378
+ console.log(`${c.gray(S.branch)} ${c.dim(selectedAgents.map(a => a.displayName).join(", "))}`);
379
+
380
+ // --- Select installation scope ---
381
+ let isGlobal = GLOBAL;
382
+
383
+ if (!GLOBAL) {
384
+ stepLine();
385
+ activeStep("Installation scope");
386
+ const scope = await selectScopePrompt();
387
+
388
+ if (!scope) {
389
+ fs.rmSync(tmp, { recursive: true, force: true });
390
+ return;
391
+ }
392
+
393
+ isGlobal = scope === "global";
394
+ }
395
+
396
+ stepLine();
397
+ step("Installation scope");
398
+ console.log(`${c.gray(S.branch)} ${c.dim(isGlobal ? "Global" : "Project")}`);
399
+
400
+ // --- Select installation method ---
401
+ stepLine();
402
+ activeStep("Installation method");
403
+ const installMethod = await selectMethodPrompt();
404
+
405
+ if (!installMethod) {
406
+ fs.rmSync(tmp, { recursive: true, force: true });
407
+ return;
408
+ }
409
+
410
+
411
+ // Installation Summary Box
412
+ stepLine();
413
+ step("Installation method");
414
+ console.log(`${c.gray(S.branch)} ${c.dim(installMethod === "symlink" ? "Symlink" : "Copy")}`);
415
+
416
+ stepLine();
417
+ step("Installation Summary");
418
+ stepLine();
419
+
420
+ const agentsString = selectedAgents.map(a => a.displayName).join(", ");
421
+
422
+ let summaryContent = "";
423
+ const methodVerb = installMethod === "symlink" ? "symlink" : "copy";
424
+
425
+ for (const sn of selectedSkills) {
426
+ summaryContent += `${c.cyan(sn)}\n`;
427
+ summaryContent += ` ${c.dim(methodVerb)} ${c.gray("→")} ${c.dim(agentsString)}\n\n`;
428
+ }
429
+
430
+ // Remove trailing newlines
431
+ summaryContent = summaryContent.trim();
432
+
433
+ console.log(boxen(summaryContent, {
434
+ padding: 1,
435
+ borderColor: "gray",
436
+ borderStyle: "round",
437
+ dimBorder: true,
438
+ title: "Installation Summary",
439
+ titleAlignment: "left"
440
+ }).split("\n").map(l => `${c.gray(S.branch)} ${l}`).join("\n"));
441
+
442
+ stepLine();
443
+
444
+ // Confirmation
445
+ activeStep("Proceed with installation?");
446
+ const shouldProceed = await confirm({ message: " ", initialValue: true });
447
+
448
+ if (isCancel(shouldProceed) || !shouldProceed) {
449
+ cancel("Cancelled.");
450
+ fs.rmSync(tmp, { recursive: true, force: true });
451
+ return;
452
+ }
453
+
454
+ // Install skills to multiple agents
455
+ stepLine();
456
+ const { installSkillForAgents } = await import("../installer.js");
457
+
458
+ // Create a map for skill paths
459
+ const skillPathMap = Object.fromEntries(skillsInRepo.map(s => [s.value, s._path]));
460
+
461
+ const installResults = { success: [], failed: [] };
462
+
463
+ for (const sn of selectedSkills) {
464
+ const src = skillPathMap[sn] || path.join(skillsDir || tmp, sn);
465
+
466
+ const is = spinner();
467
+ is.start(`Installing ${sn} to ${selectedAgents.length} agents`);
468
+
469
+ const result = await installSkillForAgents(src, sn, selectedAgents, {
470
+ method: installMethod,
471
+ scope: isGlobal ? "global" : "project",
472
+ metadata: {
473
+ repo: `${org}/${repo}`,
474
+ ref: ref || null
475
+ }
476
+ });
477
+
478
+ installResults.success.push(...result.success);
479
+ installResults.failed.push(...result.failed);
480
+
481
+ if (result.failed.length === 0) {
482
+ is.stop(`Installed ${sn} (${result.success.length} agents)`);
483
+ } else {
484
+ is.stop(`${sn}: ${result.success.length} success, ${result.failed.length} failed`);
485
+ }
486
+ }
487
+
488
+
489
+ // Derive base .agent directory from skillsDir
490
+ // If skillsDir is .../skills, then baseAgentDir is parent (.agent)
491
+ const baseAgentDir = skillsDir ? path.dirname(skillsDir) : path.join(tmp, ".agent");
492
+
493
+ // Install workflows if they exist
494
+ const workflowsDir = path.join(baseAgentDir, "workflows");
495
+ const targetWorkflowsDir = path.join(WORKSPACE, "..", "workflows");
496
+ let workflowsInstalled = 0;
497
+
498
+
499
+ if (fs.existsSync(workflowsDir)) {
500
+ stepLine();
501
+ const ws = spinner();
502
+ ws.start("Installing workflows");
503
+
504
+ fs.mkdirSync(targetWorkflowsDir, { recursive: true });
505
+ const workflows = fs.readdirSync(workflowsDir).filter(f => f.endsWith(".md"));
506
+
507
+ for (const wf of workflows) {
508
+ const src = path.join(workflowsDir, wf);
509
+ const dest = path.join(targetWorkflowsDir, wf);
510
+
511
+ if (!fs.existsSync(dest)) {
512
+ fs.copyFileSync(src, dest);
513
+ workflowsInstalled++;
514
+ }
515
+ }
516
+
517
+ ws.stop(`Installed ${workflowsInstalled} workflows`);
518
+ }
519
+
520
+ // Install GEMINI.md if it exists
521
+ const geminiSrc = path.join(baseAgentDir, "GEMINI.md");
522
+ const geminiDest = path.join(WORKSPACE, "..", "GEMINI.md");
523
+ let geminiInstalled = false;
524
+
525
+ if (fs.existsSync(geminiSrc) && !fs.existsSync(geminiDest)) {
526
+ stepLine();
527
+ fs.copyFileSync(geminiSrc, geminiDest);
528
+ step("Installed GEMINI.md (Agent Rules)");
529
+ geminiInstalled = true;
530
+ }
531
+
532
+ // Install agents if they exist
533
+ const agentsDir = path.join(baseAgentDir, "agents");
534
+ const targetAgentsDir = path.join(WORKSPACE, "..", "agents");
535
+ let agentsInstalled = 0;
536
+
537
+ if (fs.existsSync(agentsDir)) {
538
+ stepLine();
539
+ const as = spinner();
540
+ as.start("Installing agents");
541
+
542
+ fs.mkdirSync(targetAgentsDir, { recursive: true });
543
+ const agents = fs.readdirSync(agentsDir).filter(f => f.endsWith(".md"));
544
+
545
+ for (const agent of agents) {
546
+ const src = path.join(agentsDir, agent);
547
+ const dest = path.join(targetAgentsDir, agent);
548
+
549
+ if (!fs.existsSync(dest)) {
550
+ fs.copyFileSync(src, dest);
551
+ agentsInstalled++;
552
+ }
553
+ }
554
+
555
+ as.stop(`Installed ${agentsInstalled} agents`);
556
+ }
557
+
558
+ // Install ARCHITECTURE.md if it exists
559
+ const archSrc = path.join(baseAgentDir, "ARCHITECTURE.md");
560
+ const archDest = path.join(WORKSPACE, "..", "ARCHITECTURE.md");
561
+ let archInstalled = false;
562
+
563
+ if (fs.existsSync(archSrc) && !fs.existsSync(archDest)) {
564
+ fs.copyFileSync(archSrc, archDest);
565
+ step("Installed ARCHITECTURE.md");
566
+ archInstalled = true;
567
+ }
568
+
569
+ // Install knowledge if it exists (required for Agent CLI)
570
+ const knowledgeDir = path.join(baseAgentDir, "knowledge");
571
+ const targetKnowledgeDir = path.join(WORKSPACE, "..", "knowledge");
572
+ let knowledgeInstalled = false;
573
+
574
+ if (fs.existsSync(knowledgeDir) && !fs.existsSync(targetKnowledgeDir)) {
575
+ fs.cpSync(knowledgeDir, targetKnowledgeDir, { recursive: true });
576
+ step("Installed knowledge/");
577
+ knowledgeInstalled = true;
578
+ } else if (!fs.existsSync(targetKnowledgeDir)) {
579
+ // Create empty knowledge folder for Agent CLI
580
+ fs.mkdirSync(targetKnowledgeDir, { recursive: true });
581
+ // Create minimal structure for Agent CLI
582
+ fs.writeFileSync(path.join(targetKnowledgeDir, "lessons-learned.yaml"), "# Lessons learned by AI Agent\nlessons: []\n");
583
+ step("Created knowledge/ (Agent CLI ready)");
584
+ knowledgeInstalled = true;
585
+ }
586
+
587
+ // Install config/ if it exists (required for skill configuration)
588
+ const configDir = path.join(baseAgentDir, "config");
589
+ const targetConfigDir = path.join(WORKSPACE, "..", "config");
590
+ let configInstalled = false;
591
+
592
+ if (fs.existsSync(configDir) && !fs.existsSync(targetConfigDir)) {
593
+ fs.cpSync(configDir, targetConfigDir, { recursive: true });
594
+ step("Installed config/");
595
+ configInstalled = true;
596
+ }
597
+
598
+ // Install scripts-js/ if it exists (required for skill operations)
599
+ const scriptsJsDir = path.join(baseAgentDir, "scripts-js");
600
+ const targetScriptsJsDir = path.join(WORKSPACE, "..", "scripts-js");
601
+ let scriptsJsInstalled = false;
602
+
603
+ if (fs.existsSync(scriptsJsDir) && !fs.existsSync(targetScriptsJsDir)) {
604
+ fs.cpSync(scriptsJsDir, targetScriptsJsDir, { recursive: true });
605
+ step("Installed scripts-js/");
606
+ scriptsJsInstalled = true;
607
+ }
608
+
609
+ // Install metrics/ if it exists (for agent performance tracking)
610
+ const metricsDir = path.join(baseAgentDir, "metrics");
611
+ const targetMetricsDir = path.join(WORKSPACE, "..", "metrics");
612
+ let metricsInstalled = false;
613
+
614
+ if (fs.existsSync(metricsDir) && !fs.existsSync(targetMetricsDir)) {
615
+ fs.cpSync(metricsDir, targetMetricsDir, { recursive: true });
616
+ step("Installed metrics/");
617
+ metricsInstalled = true;
618
+ }
619
+
620
+ // Install additional policy documents
621
+ const policyDocs = [
622
+ "CONTINUOUS_EXECUTION_POLICY.md",
623
+ "WORKFLOW_CHAINS.md"
624
+ ];
625
+ let policyDocsInstalled = 0;
626
+
627
+ for (const doc of policyDocs) {
628
+ const docSrc = path.join(baseAgentDir, doc);
629
+ const docDest = path.join(WORKSPACE, "..", doc);
630
+ if (fs.existsSync(docSrc) && !fs.existsSync(docDest)) {
631
+ fs.copyFileSync(docSrc, docDest);
632
+ policyDocsInstalled++;
633
+ }
634
+ }
635
+ if (policyDocsInstalled > 0) {
636
+ step(`Installed ${policyDocsInstalled} policy docs`);
637
+ }
638
+
639
+ // Install rules if they exist
640
+ const rulesDir = path.join(baseAgentDir, "rules");
641
+ const targetRulesDir = path.join(WORKSPACE, "..", "rules");
642
+ let rulesInstalled = 0;
643
+
644
+ if (fs.existsSync(rulesDir)) {
645
+ fs.mkdirSync(targetRulesDir, { recursive: true });
646
+ const rules = fs.readdirSync(rulesDir).filter(f => f.endsWith(".md"));
647
+
648
+ for (const rule of rules) {
649
+ const src = path.join(rulesDir, rule);
650
+ const dest = path.join(targetRulesDir, rule);
651
+
652
+ if (!fs.existsSync(dest)) {
653
+ fs.copyFileSync(src, dest);
654
+ rulesInstalled++;
655
+ }
656
+ }
657
+
658
+ if (rulesInstalled > 0) {
659
+ step(`Installed ${rulesInstalled} rules`);
660
+ }
661
+ }
662
+
663
+ // Install .shared if it exists (contains shared resources like ui-ux-pro-max data)
664
+ const sharedDir = path.join(tmp, ".agent", ".shared");
665
+ const targetSharedDir = path.join(WORKSPACE, "..", ".shared");
666
+ let sharedInstalled = false;
667
+
668
+ if (fs.existsSync(sharedDir) && !fs.existsSync(targetSharedDir)) {
669
+ stepLine();
670
+ const ss = spinner();
671
+ ss.start("Installing shared resources");
672
+
673
+ fs.cpSync(sharedDir, targetSharedDir, { recursive: true });
674
+ sharedInstalled = true;
675
+
676
+ ss.stop("Installed .shared/ (ui-ux-pro-max data)");
677
+ }
678
+
679
+ // Installation complete step
680
+ stepLine();
681
+ step("Installation complete");
682
+
683
+ // Final Success Box
684
+ stepLine();
685
+ console.log(`${c.gray(S.branch)}`); // Extra spacing line
686
+
687
+ let successContent = "";
688
+
689
+ // Skills summary
690
+ for (const sn of selectedSkills) {
691
+ const mockPath = `.agent/skills/${sn}`;
692
+ successContent += `${c.cyan("✓")} ${c.dim(mockPath)}\n`;
693
+ }
694
+
695
+ // Workflows summary
696
+ if (workflowsInstalled > 0) {
697
+ successContent += `${c.cyan("✓")} ${c.dim(`.agent/workflows/ (${workflowsInstalled} files)`)}\n`;
698
+ }
699
+
700
+ // Agents summary
701
+ if (agentsInstalled > 0) {
702
+ successContent += `${c.cyan("")} ${c.dim(`.agent/agents/ (${agentsInstalled} files)`)}\n`;
703
+ }
704
+
705
+ // GEMINI.md summary
706
+ if (geminiInstalled) {
707
+ successContent += `${c.cyan("✓")} ${c.dim(".agent/GEMINI.md (Agent Rules)")}\n`;
708
+ }
709
+
710
+ // ARCHITECTURE.md summary
711
+ if (archInstalled) {
712
+ successContent += `${c.cyan("✓")} ${c.dim(".agent/ARCHITECTURE.md")}\n`;
713
+ }
714
+
715
+ // Knowledge summary
716
+ if (knowledgeInstalled) {
717
+ successContent += `${c.cyan("✓")} ${c.dim(".agent/knowledge/")}\n`;
718
+ }
719
+
720
+ // Rules summary
721
+ if (rulesInstalled > 0) {
722
+ successContent += `${c.cyan("✓")} ${c.dim(`.agent/rules/ (${rulesInstalled} files)`)}\n`;
723
+ }
724
+
725
+ // Shared resources summary
726
+ if (sharedInstalled) {
727
+ successContent += `${c.cyan("✓")} ${c.dim(".agent/.shared/ (ui-ux-pro-max data)")}\n`;
728
+ }
729
+
730
+ // Build title
731
+ const parts = [`${selectedSkills.length} skills`];
732
+ if (workflowsInstalled > 0) parts.push(`${workflowsInstalled} workflows`);
733
+ if (agentsInstalled > 0) parts.push(`${agentsInstalled} agents`);
734
+ if (geminiInstalled) parts.push("GEMINI.md");
735
+
736
+ console.log(boxen(successContent.trim(), {
737
+ padding: 1,
738
+ borderColor: "cyan",
739
+ borderStyle: "round",
740
+ title: c.cyan(`Installed ${parts.join(", ")}`),
741
+ titleAlignment: "left"
742
+ }).split("\n").map(l => `${c.gray(S.branch)} ${l}`).join("\n"));
743
+
744
+ fs.rmSync(tmp, { recursive: true, force: true });
745
+
746
+ // Ask user about AutoLearn installation
747
+ stepLine();
748
+ const installAutoLearn = await confirm({
749
+ message: "Install AutoLearn (enables 'agent' command for learning & self-improvement)?",
750
+ initialValue: true
751
+ });
752
+
753
+ if (isCancel(installAutoLearn)) {
754
+ cancel("Installation cancelled");
755
+ process.exit(0);
756
+ }
757
+
758
+ // Install CLI package
759
+ stepLine();
760
+ const cliSpinner = spinner();
761
+ const cliPackage = "pikakit";
762
+
763
+ if (isGlobal) {
764
+ if (installAutoLearn) {
765
+ cliSpinner.start(`Installing CLI globally (${cliPackage})`);
766
+ } else {
767
+ cliSpinner.start(`Installing kit CLI globally (${cliPackage})`);
768
+ }
769
+ try {
770
+ await execAsync(`npm install -g ${cliPackage}`, { timeout: 120000 });
771
+ cliSpinner.stop("CLI installed globally");
772
+ if (installAutoLearn) {
773
+ step(c.dim("Commands: agent, kit"));
774
+ } else {
775
+ step(c.dim("Command: kit"));
776
+ }
777
+ } catch (e) {
778
+ cliSpinner.stop(c.yellow("Global CLI installation failed"));
779
+ step(c.dim(`Try running manually: npm i -g ${cliPackage}`));
780
+ }
781
+ } else {
782
+ cliSpinner.start(`Installing Agent CLI locally (${cliPackage})`);
783
+ try {
784
+ await execAsync(`npm install -D ${cliPackage}`, { timeout: 120000 });
785
+ cliSpinner.stop("CLI installed locally");
786
+
787
+ // Add npm scripts to package.json
788
+ try {
789
+ const pkgPath = path.join(process.cwd(), "package.json");
790
+ if (fs.existsSync(pkgPath)) {
791
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
792
+ pkg.scripts = pkg.scripts || {};
793
+
794
+ // Always add kit script
795
+ if (!pkg.scripts.kit) {
796
+ pkg.scripts.kit = "kit";
797
+ }
798
+
799
+ // Add agent script only if AutoLearn enabled
800
+ if (installAutoLearn && !pkg.scripts.agent) {
801
+ pkg.scripts.agent = "agent";
802
+ }
803
+
804
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
805
+ if (installAutoLearn) {
806
+ step(c.green("✓ Added npm scripts: 'agent', 'kit'"));
807
+ } else {
808
+ step(c.green("✓ Added npm script: 'kit'"));
809
+ }
810
+ }
811
+ } catch (scriptErr) {
812
+ step(c.yellow("âš  Could not add npm scripts automatically"));
813
+ }
814
+
815
+ // Create wrapper scripts for direct command access (Windows + Unix)
816
+ try {
817
+ const projectRoot = process.cwd();
818
+ const binDir = path.join(projectRoot, "node_modules", ".bin");
819
+
820
+ // Always create kit wrappers at project root
821
+ const kitCmd = `@echo off\nnode "%~dp0node_modules\\pikakit\\bin\\kit.js" %*`;
822
+ const kitSh = `#!/bin/sh\nnode "$(dirname "$0")/node_modules/pikakit/bin/kit.js" "$@"`;
823
+ fs.writeFileSync(path.join(projectRoot, "kit.cmd"), kitCmd);
824
+ fs.writeFileSync(path.join(projectRoot, "kit"), kitSh, { mode: 0o755 });
825
+
826
+ // Also create in node_modules/.bin for npx support
827
+ if (fs.existsSync(binDir)) {
828
+ const kitBinCmd = `@echo off\nnode "%~dp0..\\pikakit\\bin\\kit.js" %*`;
829
+ const kitBinSh = `#!/bin/sh\nnode "$(dirname "$0")/../pikakit/bin/kit.js" "$@"`;
830
+ fs.writeFileSync(path.join(binDir, "kit.cmd"), kitBinCmd);
831
+ fs.writeFileSync(path.join(binDir, "kit"), kitBinSh, { mode: 0o755 });
832
+ }
833
+
834
+ if (installAutoLearn) {
835
+ // Create agent wrappers only if AutoLearn enabled
836
+ const agentCmd = `@echo off\nnode "%~dp0node_modules\\pikakit\\lib\\agent-cli\\bin\\agent.js" %*`;
837
+ const agentSh = `#!/bin/sh\nnode "$(dirname "$0")/node_modules/pikakit/lib/agent-cli/bin/agent.js" "$@"`;
838
+ fs.writeFileSync(path.join(projectRoot, "agent.cmd"), agentCmd);
839
+ fs.writeFileSync(path.join(projectRoot, "agent"), agentSh, { mode: 0o755 });
840
+
841
+ // Also create in node_modules/.bin for npx support
842
+ if (fs.existsSync(binDir)) {
843
+ const agentBinCmd = `@echo off\nnode "%~dp0..\\pikakit\\lib\\agent-cli\\bin\\agent.js" %*`;
844
+ const agentBinSh = `#!/bin/sh\nnode "$(dirname "$0")/../pikakit/lib/agent-cli/bin/agent.js" "$@"`;
845
+ fs.writeFileSync(path.join(binDir, "agent.cmd"), agentBinCmd);
846
+ fs.writeFileSync(path.join(binDir, "agent"), agentBinSh, { mode: 0o755 });
847
+ }
848
+
849
+ step(c.green("✔ Created wrapper scripts: agent, kit"));
850
+ step(c.dim("Run: npx agent | npx kit | .\\agent | .\\kit (Windows)"));
851
+ } else {
852
+ step(c.green("✔ Created wrapper script: kit"));
853
+ step(c.dim("Run: npx kit | .\\kit (Windows)"));
854
+ }
855
+ } catch (wrapperErr) {
856
+ step(c.dim("Run: npx kit"));
857
+ }
858
+ } catch (e) {
859
+ cliSpinner.stop(c.yellow("Local CLI installation skipped"));
860
+ step(c.dim(`Run manually: npm i -D ${cliPackage}`));
861
+ }
862
+ }
863
+
864
+ // Run npm install to ensure all skill dependencies are available
865
+ stepLine();
866
+ const depsSpinner = spinner();
867
+ depsSpinner.start("Installing skill dependencies (csv-parse, etc.)");
868
+ try {
869
+ await execAsync("npm install", { timeout: 120000 });
870
+ depsSpinner.stop("Skill dependencies installed");
871
+ } catch (e) {
872
+ depsSpinner.stop(c.yellow("Dependencies installation skipped"));
873
+ step(c.dim("Run manually: npm install"));
874
+ }
875
+
876
+ // Python dependencies no longer needed - all scripts migrated to JS
877
+
878
+ // Final Quick Start Guide
879
+ stepLine();
880
+ console.log(boxen(
881
+ `${c.cyan("Quick Start Commands:")}\n\n` +
882
+ `${c.green("Option 1:")} ${c.white("npm run kit")} ${c.dim("// npm scripts")}\n` +
883
+ `${c.green("Option 2:")} ${c.white("npx kit")} ${c.dim("// npx")}\n` +
884
+ `${c.green("Option 3:")} ${c.white(".\\\\kit")} ${c.dim("// PowerShell direct")}\n\n` +
885
+ (installAutoLearn ?
886
+ `${c.cyan("Agent Commands:")}\n` +
887
+ `${c.white("npm run agent")} ${c.dim("or")} ${c.white("npx agent")} ${c.dim("or")} ${c.white(".\\\\agent")}\n\n` : "") +
888
+ `${c.yellow("Note:")} Windows PowerShell requires ${c.white(".\\\\")}\n` +
889
+ `prefix to run scripts from current directory.`,
890
+ {
891
+ padding: 1,
892
+ borderColor: "cyan",
893
+ borderStyle: "round",
894
+ title: c.cyan("⚡ Get Started"),
895
+ titleAlignment: "left"
896
+ }
897
+ ).split("\n").map(l => `${c.gray(S.branch)} ${l}`).join("\n"));
898
+
899
+ stepLine();
900
+ console.log(` ${c.cyan("Done!")}`);
901
+ console.log();
902
+ }