@pedroab0/skills 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js ADDED
@@ -0,0 +1,748 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Skills Installer CLI
5
+ * https://github.com/pedroab0/skills
6
+ *
7
+ * Install curated AI agent skills for Claude Code, Cursor, Antigravity, and OpenCode.
8
+ * Usage: npx @pedroab0/skills [options]
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+ const readline = require('readline');
15
+
16
+ // ANSI colors & styling
17
+ const isColorSupported = !process.env.NO_COLOR && (process.stdout.isTTY || process.env.FORCE_COLOR);
18
+ const style = {
19
+ bold: (t) => (isColorSupported ? `\x1b[1m${t}\x1b[0m` : t),
20
+ dim: (t) => (isColorSupported ? `\x1b[2m${t}\x1b[0m` : t),
21
+ green: (t) => (isColorSupported ? `\x1b[32m${t}\x1b[0m` : t),
22
+ cyan: (t) => (isColorSupported ? `\x1b[36m${t}\x1b[0m` : t),
23
+ yellow: (t) => (isColorSupported ? `\x1b[33m${t}\x1b[0m` : t),
24
+ red: (t) => (isColorSupported ? `\x1b[31m${t}\x1b[0m` : t),
25
+ };
26
+
27
+ const ROOT_DIR = path.resolve(__dirname, '..');
28
+
29
+ // Visual Tree / Clack-style Icons
30
+ const ui = {
31
+ header: (title) => console.log(`\n${style.bold(style.cyan('◆'))} ${style.bold(title)}`),
32
+ bar: () => console.log(style.dim('│')),
33
+ step: (title) => console.log(`${style.cyan('◇')} ${style.bold(title)}`),
34
+ item: (text) => console.log(`${style.dim('│')} ${text}`),
35
+ itemActive: (text) => console.log(`${style.dim('│')} ${style.cyan('●')} ${text}`),
36
+ itemInactive: (text) => console.log(`${style.dim('│')} ${style.dim('○')} ${text}`),
37
+ confirmed: (text) => console.log(`${style.dim('│')}\n${style.green('✔')} ${text}\n${style.dim('│')}`),
38
+ done: (text) => console.log(`${style.green('✔')} ${style.bold(text)}\n${style.dim('└')} ${style.dim('https://github.com/pedroab0/skills')}\n`),
39
+ cancel: () => console.log(`${style.dim('│')}\n${style.yellow('✖')} ${style.yellow('Installation cancelled.')}\n${style.dim('└')}\n`),
40
+ };
41
+
42
+ function printBanner() {
43
+ ui.header(`Skills Installer ${style.dim('(by @pedroab0)')}`);
44
+ ui.item(style.dim('Curated AI Agent Skills for Claude Code, Antigravity, Cursor & OpenCode'));
45
+ ui.item(style.dim('https://github.com/pedroab0/skills'));
46
+ ui.bar();
47
+ }
48
+
49
+ function printUsage() {
50
+ console.log(`
51
+ ${style.bold('Usage:')}
52
+ npx @pedroab0/skills [options]
53
+ (or: npx github:pedroab0/skills [options])
54
+
55
+ ${style.bold('Options:')}
56
+ --agent <name> Target agent: claude, antigravity, cursor, opencode, all
57
+ --skill <name> Skill to install (or comma-separated, or "all", "sdd")
58
+ --global Install globally (available across all projects on your machine)
59
+ --local Install in current project workspace only
60
+ --dest <path> Custom destination directory
61
+ --symlink Create symlinks instead of copying (recommended for local repos)
62
+ --list, -l List available skills and exit
63
+ --help, -h Show this help message
64
+
65
+ ${style.bold('Examples:')}
66
+ npx @pedroab0/skills
67
+ npx @pedroab0/skills --agent claude --skill all
68
+ npx @pedroab0/skills --agent antigravity --skill figma-extractor,figma-node-builder
69
+ npx @pedroab0/skills --dest ~/.claude/skills --skill figma-to-code
70
+ `);
71
+ }
72
+
73
+ // -----------------------------------------------------------------------------
74
+ // Skill Discovery
75
+ // -----------------------------------------------------------------------------
76
+
77
+ function getAvailableSkills() {
78
+ const skills = [];
79
+ try {
80
+ const entries = fs.readdirSync(ROOT_DIR, { withFileTypes: true });
81
+ for (const entry of entries) {
82
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'bin' && entry.name !== 'node_modules') {
83
+ const skillPath = path.join(ROOT_DIR, entry.name);
84
+ const skillMdPath = path.join(skillPath, 'SKILL.md');
85
+ if (fs.existsSync(skillMdPath)) {
86
+ let description = '';
87
+ try {
88
+ const content = fs.readFileSync(skillMdPath, 'utf8');
89
+ const match = content.match(/description:\s*['"]?([^'"\n\r]+)['"]?/);
90
+ if (match) {
91
+ description = match[1].trim();
92
+ }
93
+ } catch (_) {}
94
+ skills.push({
95
+ name: entry.name,
96
+ path: skillPath,
97
+ description: description || 'AI agent skill',
98
+ });
99
+ }
100
+ }
101
+ }
102
+ } catch (err) {
103
+ console.error('Error scanning skills:', err.message);
104
+ }
105
+ return skills;
106
+ }
107
+
108
+ // -----------------------------------------------------------------------------
109
+ // Agent Detection & Configurations
110
+ // -----------------------------------------------------------------------------
111
+
112
+ function getAgentConfigs(cwd) {
113
+ const home = os.homedir();
114
+ return {
115
+ claude: {
116
+ name: 'Claude Code',
117
+ globalPath: path.join(home, '.claude', 'skills'),
118
+ localPath: path.join(cwd, '.claude', 'skills'),
119
+ isDetected: fs.existsSync(path.join(home, '.claude')),
120
+ },
121
+ antigravity: {
122
+ name: 'Google Antigravity',
123
+ globalPath: path.join(home, '.gemini', 'config', 'skills'),
124
+ fallbackPath: path.join(home, '.gemini', 'antigravity-cli', 'skills'),
125
+ localPath: path.join(cwd, '.gemini', 'skills'),
126
+ isDetected: fs.existsSync(path.join(home, '.gemini', 'config')) || fs.existsSync(path.join(home, '.gemini', 'antigravity-cli')),
127
+ },
128
+ cursor: {
129
+ name: 'Cursor',
130
+ globalPath: path.join(home, '.cursor', 'rules'),
131
+ localPath: path.join(cwd, '.cursor', 'rules'),
132
+ isDetected: fs.existsSync(path.join(cwd, '.cursor')) || fs.existsSync(path.join(home, '.cursor')),
133
+ isCursorRule: true,
134
+ },
135
+ opencode: {
136
+ name: 'OpenCode',
137
+ globalPath: path.join(home, '.config', 'opencode', 'skills'),
138
+ localPath: path.join(cwd, '.opencode', 'skills'),
139
+ isDetected: fs.existsSync(path.join(home, '.config', 'opencode')),
140
+ },
141
+ };
142
+ }
143
+
144
+ // -----------------------------------------------------------------------------
145
+ // Interactive Selection & Keypress Handling
146
+ // -----------------------------------------------------------------------------
147
+
148
+ let nonTTYLineIterator = null;
149
+ let nonTTYInterface = null;
150
+ async function readLineNonTTY(promptQuery) {
151
+ process.stdout.write(`${style.cyan('◆')} ${promptQuery}`);
152
+ if (!nonTTYLineIterator) {
153
+ nonTTYInterface = readline.createInterface({
154
+ input: process.stdin,
155
+ terminal: false,
156
+ });
157
+ nonTTYLineIterator = nonTTYInterface[Symbol.asyncIterator]();
158
+ }
159
+ const next = await nonTTYLineIterator.next();
160
+ return next.done ? '' : next.value;
161
+ }
162
+
163
+ function getTerminalRows(text) {
164
+ const cols = process.stdout.columns || 80;
165
+ const clean = text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
166
+ return Math.max(1, Math.ceil(clean.length / cols));
167
+ }
168
+
169
+ // Ensure terminal cursor is restored on exit
170
+ process.on('exit', () => {
171
+ process.stdout.write('\x1b[?25h');
172
+ });
173
+ process.on('SIGINT', () => {
174
+ process.stdout.write('\x1b[?25h');
175
+ ui.cancel();
176
+ process.exit(0);
177
+ });
178
+
179
+ /**
180
+ * Interactive select prompt with keyboard navigation (↑/↓, numbers 1-9, Enter to confirm, ESC to cancel).
181
+ * Automatically falls back to line-based input when running in non-TTY (CI / piped) environments.
182
+ */
183
+ async function selectPrompt({ title, items, defaultIndex = 0, onConfirm }) {
184
+ // Non-TTY fallback (piped input / CI environments)
185
+ if (!process.stdin.isTTY) {
186
+ ui.step(title);
187
+ items.forEach((item, idx) => {
188
+ const hintStr = item.hint ? ` ${style.dim(item.hint)}` : '';
189
+ const badgeStr = item.badge ? ` ${item.badge}` : '';
190
+ if (idx === defaultIndex) {
191
+ ui.itemActive(`${idx + 1}. ${style.bold(item.label)}${hintStr}${badgeStr}`);
192
+ } else {
193
+ ui.itemInactive(`${idx + 1}. ${item.label}${hintStr}${badgeStr}`);
194
+ }
195
+ });
196
+ ui.bar();
197
+
198
+ const ans = await readLineNonTTY(`Enter choice [1-${items.length}, or ESC to exit, default: ${defaultIndex + 1}]: `);
199
+ const trimmed = (ans || '').trim();
200
+ if (['0', 'q', 'exit', 'quit'].includes(trimmed.toLowerCase())) {
201
+ ui.cancel();
202
+ process.exit(0);
203
+ }
204
+
205
+ let pickedIdx = defaultIndex;
206
+ const parsed = parseInt(trimmed, 10);
207
+ if (!isNaN(parsed) && parsed >= 1 && parsed <= items.length) {
208
+ pickedIdx = parsed - 1;
209
+ } else if (trimmed) {
210
+ const matchIdx = items.findIndex((it) => String(it.value).toLowerCase() === trimmed.toLowerCase());
211
+ if (matchIdx !== -1) pickedIdx = matchIdx;
212
+ }
213
+
214
+ const picked = items[pickedIdx];
215
+ if (onConfirm) {
216
+ onConfirm(picked);
217
+ } else {
218
+ ui.confirmed(`Selected: ${style.bold(picked.label)}`);
219
+ }
220
+ return picked.value;
221
+ }
222
+
223
+ // Interactive TTY mode with arrow keys and raw mode
224
+ return new Promise((resolve) => {
225
+ let activeIndex = defaultIndex;
226
+ let rowsRendered = 0;
227
+
228
+ readline.emitKeypressEvents(process.stdin);
229
+ if (process.stdin.setRawMode) {
230
+ process.stdin.setRawMode(true);
231
+ }
232
+ process.stdin.resume();
233
+
234
+ // Hide terminal cursor
235
+ process.stdout.write('\x1b[?25l');
236
+
237
+ const restoreTerminal = () => {
238
+ process.stdout.write('\x1b[?25h');
239
+ if (process.stdin.setRawMode) {
240
+ try {
241
+ process.stdin.setRawMode(false);
242
+ } catch (_) {}
243
+ }
244
+ process.stdin.removeListener('keypress', onKeypress);
245
+ try {
246
+ process.stdin.pause();
247
+ } catch (_) {}
248
+ };
249
+
250
+ const cleanupAndExit = () => {
251
+ restoreTerminal();
252
+ ui.cancel();
253
+ process.exit(0);
254
+ };
255
+
256
+ const render = (isInitial = false) => {
257
+ if (isInitial) {
258
+ ui.step(title);
259
+ } else if (rowsRendered > 0) {
260
+ readline.moveCursor(process.stdout, 0, -rowsRendered);
261
+ readline.cursorTo(process.stdout, 0);
262
+ readline.clearScreenDown(process.stdout);
263
+ }
264
+
265
+ const lines = [];
266
+ items.forEach((item, idx) => {
267
+ const isActive = idx === activeIndex;
268
+ const num = `${idx + 1}.`;
269
+ const hintStr = item.hint ? ` ${style.dim(item.hint)}` : '';
270
+ const badgeStr = item.badge ? ` ${item.badge}` : '';
271
+ if (isActive) {
272
+ lines.push(`${style.dim('│')} ${style.cyan('●')} ${style.bold(style.cyan(`${num} ${item.label}`))}${hintStr}${badgeStr}`);
273
+ } else {
274
+ lines.push(`${style.dim('│')} ${style.dim('○')} ${style.dim(`${num} ${item.label}`)}${hintStr}${badgeStr}`);
275
+ }
276
+ });
277
+
278
+ lines.push(style.dim('│'));
279
+ lines.push(
280
+ `${style.dim('│')} ${style.dim('Use ')}${style.cyan('↑/↓')}${style.dim(' to navigate, ')}${style.cyan('[Enter]')}${style.dim(' or ')}${style.cyan('1-' + items.length)}${style.dim(' to select, ')}${style.dim('[ESC] to exit')}`
281
+ );
282
+
283
+ process.stdout.write(lines.join('\n') + '\n');
284
+ rowsRendered = lines.reduce((acc, line) => acc + getTerminalRows(line), 0);
285
+ };
286
+
287
+ const confirmSelection = (selectedIndex) => {
288
+ activeIndex = selectedIndex;
289
+
290
+ // Erase the options + tip line
291
+ if (rowsRendered > 0) {
292
+ readline.moveCursor(process.stdout, 0, -rowsRendered);
293
+ readline.cursorTo(process.stdout, 0);
294
+ readline.clearScreenDown(process.stdout);
295
+ }
296
+
297
+ // Re-render the frozen list without the tip line
298
+ const frozenLines = [];
299
+ items.forEach((item, idx) => {
300
+ const isActive = idx === activeIndex;
301
+ const num = `${idx + 1}.`;
302
+ const hintStr = item.hint ? ` ${style.dim(item.hint)}` : '';
303
+ const badgeStr = item.badge ? ` ${item.badge}` : '';
304
+ if (isActive) {
305
+ frozenLines.push(`${style.dim('│')} ${style.cyan('●')} ${style.bold(style.cyan(`${num} ${item.label}`))}${hintStr}${badgeStr}`);
306
+ } else {
307
+ frozenLines.push(`${style.dim('│')} ${style.dim('○')} ${style.dim(`${num} ${item.label}`)}${hintStr}${badgeStr}`);
308
+ }
309
+ });
310
+ process.stdout.write(frozenLines.join('\n') + '\n');
311
+
312
+ restoreTerminal();
313
+
314
+ const selected = items[activeIndex];
315
+ if (onConfirm) {
316
+ onConfirm(selected);
317
+ } else {
318
+ ui.confirmed(`Selected: ${style.bold(selected.label)}`);
319
+ }
320
+ resolve(selected.value);
321
+ };
322
+
323
+ const onKeypress = (char, key) => {
324
+ if (!key) {
325
+ if (char >= '1' && char <= String(items.length)) {
326
+ confirmSelection(parseInt(char, 10) - 1);
327
+ }
328
+ return;
329
+ }
330
+
331
+ // ESC, Ctrl+C, or 'q'
332
+ if (key.name === 'escape' || (key.ctrl && key.name === 'c') || char === 'q') {
333
+ cleanupAndExit();
334
+ return;
335
+ }
336
+
337
+ // Up arrow or 'k'
338
+ if (key.name === 'up' || char === 'k') {
339
+ activeIndex = (activeIndex - 1 + items.length) % items.length;
340
+ render(false);
341
+ return;
342
+ }
343
+
344
+ // Down arrow or 'j'
345
+ if (key.name === 'down' || char === 'j') {
346
+ activeIndex = (activeIndex + 1) % items.length;
347
+ render(false);
348
+ return;
349
+ }
350
+
351
+ // Direct number key selection
352
+ if (char >= '1' && char <= String(items.length)) {
353
+ confirmSelection(parseInt(char, 10) - 1);
354
+ return;
355
+ }
356
+
357
+ // Enter / Return
358
+ if (key.name === 'return' || key.name === 'enter') {
359
+ confirmSelection(activeIndex);
360
+ return;
361
+ }
362
+ };
363
+
364
+ process.stdin.on('keypress', onKeypress);
365
+ render(true);
366
+ });
367
+ }
368
+
369
+ // -----------------------------------------------------------------------------
370
+ // Token Verification
371
+ // -----------------------------------------------------------------------------
372
+
373
+ function checkFigmaToken() {
374
+ if (process.env.FIGMA_ACCESS_TOKEN || process.env.FIGMA_TOKEN) {
375
+ return { found: true, source: 'environment variable' };
376
+ }
377
+ const home = os.homedir();
378
+ const candidates = [
379
+ path.join(process.cwd(), '.env'),
380
+ path.join(process.cwd(), '.env.local'),
381
+ path.join(home, '.zshrc'),
382
+ path.join(home, '.bashrc'),
383
+ path.join(home, '.profile'),
384
+ ];
385
+ for (const c of candidates) {
386
+ if (fs.existsSync(c)) {
387
+ try {
388
+ const text = fs.readFileSync(c, 'utf8');
389
+ if (/FIGMA_ACCESS_TOKEN|FIGMA_TOKEN/.test(text)) {
390
+ return { found: true, source: path.basename(c) };
391
+ }
392
+ } catch (_) {}
393
+ }
394
+ }
395
+ return { found: false };
396
+ }
397
+
398
+ // -----------------------------------------------------------------------------
399
+ // Skill Installation Logic
400
+ // -----------------------------------------------------------------------------
401
+
402
+ function installSkill(skill, destDir, useSymlink = false, isCursor = false) {
403
+ if (!fs.existsSync(destDir)) {
404
+ fs.mkdirSync(destDir, { recursive: true });
405
+ }
406
+
407
+ const targetPath = path.join(destDir, skill.name);
408
+
409
+ if (isCursor) {
410
+ const cursorSkillsDir = path.join(path.dirname(destDir), 'skills', skill.name);
411
+ fs.mkdirSync(path.dirname(cursorSkillsDir), { recursive: true });
412
+
413
+ if (fs.existsSync(cursorSkillsDir)) {
414
+ fs.rmSync(cursorSkillsDir, { recursive: true, force: true });
415
+ }
416
+ fs.cpSync(skill.path, cursorSkillsDir, { recursive: true });
417
+
418
+ const ruleFile = path.join(destDir, `${skill.name}.mdc`);
419
+ const ruleContent = `---
420
+ description: ${skill.description}
421
+ globs: *
422
+ ---
423
+
424
+ ${fs.readFileSync(path.join(skill.path, 'SKILL.md'), 'utf8')}
425
+ `;
426
+ fs.writeFileSync(ruleFile, ruleContent, 'utf8');
427
+ return { targetPath: ruleFile, isCursorRule: true };
428
+ }
429
+
430
+ try {
431
+ fs.rmSync(targetPath, { recursive: true, force: true });
432
+ } catch (_) {}
433
+
434
+ if (useSymlink) {
435
+ try {
436
+ fs.symlinkSync(skill.path, targetPath, 'dir');
437
+ return { targetPath, isSymlink: true };
438
+ } catch (e) {
439
+ fs.cpSync(skill.path, targetPath, { recursive: true });
440
+ return { targetPath, isSymlink: false };
441
+ }
442
+ } else {
443
+ fs.cpSync(skill.path, targetPath, { recursive: true });
444
+ const scriptsDir = path.join(targetPath, 'scripts');
445
+ if (fs.existsSync(scriptsDir)) {
446
+ try {
447
+ const scriptFiles = fs.readdirSync(scriptsDir);
448
+ for (const f of scriptFiles) {
449
+ if (f.endsWith('.js')) {
450
+ fs.chmodSync(path.join(scriptsDir, f), 0o755);
451
+ }
452
+ }
453
+ } catch (_) {}
454
+ }
455
+ return { targetPath, isSymlink: false };
456
+ }
457
+ }
458
+
459
+ // -----------------------------------------------------------------------------
460
+ // Main Execution
461
+ // -----------------------------------------------------------------------------
462
+
463
+ async function main() {
464
+ const args = process.argv.slice(2);
465
+
466
+ if (args.includes('--help') || args.includes('-h')) {
467
+ printBanner();
468
+ printUsage();
469
+ process.exit(0);
470
+ }
471
+
472
+ const availableSkills = getAvailableSkills();
473
+
474
+ if (args.includes('--list') || args.includes('-l')) {
475
+ printBanner();
476
+ console.log(style.bold('Available Skills:\n'));
477
+ for (const s of availableSkills) {
478
+ console.log(` • ${style.bold(style.cyan(s.name))}`);
479
+ console.log(` ${style.dim(s.description)}\n`);
480
+ }
481
+ process.exit(0);
482
+ }
483
+
484
+ printBanner();
485
+
486
+ let flagAgent = null;
487
+ let flagSkill = null;
488
+ let flagScope = null;
489
+ let flagDest = null;
490
+ let flagSymlink = args.includes('--symlink');
491
+
492
+ for (let i = 0; i < args.length; i++) {
493
+ const a = args[i];
494
+ if (a === '--agent' && args[i + 1]) {
495
+ flagAgent = args[++i].toLowerCase();
496
+ } else if (a === '--skill' && args[i + 1]) {
497
+ flagSkill = args[++i];
498
+ } else if (a === '--dest' && args[i + 1]) {
499
+ flagDest = args[++i];
500
+ } else if (a === '--global') {
501
+ flagScope = 'global';
502
+ } else if (a === '--local') {
503
+ flagScope = 'local';
504
+ }
505
+ }
506
+
507
+ // 1. SELECT SKILLS
508
+ let selectedSkills = [];
509
+ const figmaExtractor = availableSkills.find((s) => s.name === 'figma-extractor');
510
+ const figmaNodeBuilder = availableSkills.find((s) => s.name === 'figma-node-builder');
511
+ const figmaToCode = availableSkills.find((s) => s.name === 'figma-to-code');
512
+
513
+ if (flagSkill) {
514
+ const lower = flagSkill.toLowerCase();
515
+ if (lower === 'all') {
516
+ selectedSkills = availableSkills;
517
+ } else if (lower === 'sdd' || lower === 'suite') {
518
+ selectedSkills = [figmaExtractor, figmaNodeBuilder].filter(Boolean);
519
+ } else {
520
+ const names = flagSkill.split(',').map((x) => x.trim().toLowerCase());
521
+ selectedSkills = availableSkills.filter((s) => names.includes(s.name.toLowerCase()));
522
+ if (selectedSkills.length === 0) {
523
+ console.error(style.yellow(`⚠ No matching skills found for "${flagSkill}".`));
524
+ console.log(`Available skills: ${availableSkills.map((s) => s.name).join(', ')}`);
525
+ process.exit(1);
526
+ }
527
+ }
528
+ } else {
529
+ const skillOptions = [
530
+ {
531
+ label: 'All skills',
532
+ hint: '(recommended: installs figma-to-code, figma-extractor & figma-node-builder)',
533
+ value: 'all',
534
+ },
535
+ {
536
+ label: 'figma-extractor and figma-node-builder',
537
+ hint: '- Spec-Driven Development (SDD) Suite',
538
+ value: 'sdd',
539
+ },
540
+ {
541
+ label: 'figma-to-code',
542
+ hint: '- fast prototyping (URL ──► UI Code)',
543
+ value: 'figma-to-code',
544
+ },
545
+ {
546
+ label: 'figma-extractor',
547
+ hint: '- Figma design scraper (extracts layout AST, tokens & preview images to .specs/)',
548
+ value: 'figma-extractor',
549
+ },
550
+ {
551
+ label: 'figma-node-builder',
552
+ hint: '- Builds UI components from extracted Figma JSON specs (.specs/*.json)',
553
+ value: 'figma-node-builder',
554
+ },
555
+ ];
556
+
557
+ const chosenValue = await selectPrompt({
558
+ title: 'Select skills to install:',
559
+ items: skillOptions,
560
+ defaultIndex: 0,
561
+ onConfirm: (item) => {
562
+ let skillsForChoice = [];
563
+ if (item.value === 'all') skillsForChoice = availableSkills;
564
+ else if (item.value === 'sdd') skillsForChoice = [figmaExtractor, figmaNodeBuilder].filter(Boolean);
565
+ else if (item.value === 'figma-to-code') skillsForChoice = [figmaToCode].filter(Boolean);
566
+ else if (item.value === 'figma-extractor') skillsForChoice = [figmaExtractor].filter(Boolean);
567
+ else if (item.value === 'figma-node-builder') skillsForChoice = [figmaNodeBuilder].filter(Boolean);
568
+
569
+ ui.confirmed(`Selected ${style.bold(skillsForChoice.length.toString())} skill(s): ${skillsForChoice.map((s) => style.cyan(s.name)).join(', ')}`);
570
+ },
571
+ });
572
+
573
+ if (chosenValue === 'all') {
574
+ selectedSkills = availableSkills;
575
+ } else if (chosenValue === 'sdd') {
576
+ selectedSkills = [figmaExtractor, figmaNodeBuilder].filter(Boolean);
577
+ } else if (chosenValue === 'figma-to-code') {
578
+ selectedSkills = [figmaToCode].filter(Boolean);
579
+ } else if (chosenValue === 'figma-extractor') {
580
+ selectedSkills = [figmaExtractor].filter(Boolean);
581
+ } else if (chosenValue === 'figma-node-builder') {
582
+ selectedSkills = [figmaNodeBuilder].filter(Boolean);
583
+ }
584
+ }
585
+
586
+ // 2. SELECT AGENT / DESTINATION
587
+ const cwd = process.cwd();
588
+ const agentConfigs = getAgentConfigs(cwd);
589
+ let targetDestinations = [];
590
+
591
+ if (flagDest) {
592
+ targetDestinations.push({
593
+ name: 'Custom Directory',
594
+ path: path.resolve(cwd, flagDest),
595
+ isCursor: false,
596
+ });
597
+ } else if (flagAgent) {
598
+ const isGlobal = flagScope !== 'local';
599
+ if (flagAgent === 'all') {
600
+ for (const [key, cfg] of Object.entries(agentConfigs)) {
601
+ targetDestinations.push({
602
+ name: cfg.name,
603
+ path: isGlobal ? cfg.globalPath : cfg.localPath,
604
+ isCursor: !!cfg.isCursorRule,
605
+ });
606
+ }
607
+ } else if (agentConfigs[flagAgent]) {
608
+ const cfg = agentConfigs[flagAgent];
609
+ targetDestinations.push({
610
+ name: cfg.name,
611
+ path: isGlobal ? cfg.globalPath : cfg.localPath,
612
+ isCursor: !!cfg.isCursorRule,
613
+ });
614
+ } else {
615
+ console.error(style.yellow(`⚠ Unknown agent "${flagAgent}". Options: claude, antigravity, cursor, opencode, all`));
616
+ process.exit(1);
617
+ }
618
+ } else {
619
+ const agentsList = [
620
+ { id: 'claude', ...agentConfigs.claude },
621
+ { id: 'antigravity', ...agentConfigs.antigravity },
622
+ { id: 'cursor', ...agentConfigs.cursor },
623
+ { id: 'opencode', ...agentConfigs.opencode },
624
+ { id: 'all', name: 'All detected agents' },
625
+ ];
626
+
627
+ const agentOptions = agentsList.map((ag) => ({
628
+ label: ag.name,
629
+ badge: ag.isDetected ? style.green('[Detected on this machine]') : '',
630
+ value: ag,
631
+ }));
632
+
633
+ const selectedAgent = await selectPrompt({
634
+ title: 'Which AI coding agent do you use?',
635
+ items: agentOptions,
636
+ defaultIndex: 0,
637
+ onConfirm: (item) => {
638
+ ui.confirmed(`Target agent: ${style.bold(item.label)}${item.value.id === 'cursor' ? ' (.cursor/rules)' : ''}`);
639
+ },
640
+ });
641
+
642
+ // Scope selection (Global vs Local)
643
+ let isGlobal = true;
644
+ if (selectedAgent.id !== 'cursor') {
645
+ const scopeOptions = [
646
+ {
647
+ label: 'Global',
648
+ hint: '(recommended: available across all projects on this machine)',
649
+ value: true,
650
+ },
651
+ {
652
+ label: 'Local',
653
+ hint: '(in current project workspace only)',
654
+ value: false,
655
+ },
656
+ ];
657
+
658
+ isGlobal = await selectPrompt({
659
+ title: 'Where should skills be installed?',
660
+ items: scopeOptions,
661
+ defaultIndex: 0,
662
+ onConfirm: (item) => {
663
+ ui.confirmed(`Scope: ${style.bold(item.label)}`);
664
+ },
665
+ });
666
+ }
667
+
668
+ if (selectedAgent.id === 'all') {
669
+ for (const [key, cfg] of Object.entries(agentConfigs)) {
670
+ if (cfg.isDetected) {
671
+ targetDestinations.push({
672
+ name: cfg.name,
673
+ path: isGlobal ? cfg.globalPath : cfg.localPath,
674
+ isCursor: !!cfg.isCursorRule,
675
+ });
676
+ }
677
+ }
678
+ if (targetDestinations.length === 0) {
679
+ targetDestinations.push({ name: 'Claude Code', path: agentConfigs.claude.globalPath, isCursor: false });
680
+ targetDestinations.push({ name: 'Antigravity', path: agentConfigs.antigravity.globalPath, isCursor: false });
681
+ }
682
+ } else {
683
+ const cfg = agentConfigs[selectedAgent.id];
684
+ targetDestinations.push({
685
+ name: cfg.name,
686
+ path: isGlobal ? cfg.globalPath : cfg.localPath,
687
+ isCursor: !!cfg.isCursorRule,
688
+ });
689
+ }
690
+ }
691
+
692
+ // 3. EXECUTE INSTALLATION
693
+ ui.step('Installing skills...');
694
+ for (const dest of targetDestinations) {
695
+ ui.item(`${style.dim('→')} Installing for ${style.bold(dest.name)} at ${style.dim(dest.path)}`);
696
+ for (const skill of selectedSkills) {
697
+ try {
698
+ const res = installSkill(skill, dest.path, flagSymlink, dest.isCursor);
699
+ const detail = res.isCursorRule ? '(rule created)' : res.isSymlink ? '(symlinked)' : '(copied)';
700
+ ui.item(`${style.green('✓')} ${style.bold(skill.name)} ${style.dim(detail)}`);
701
+ } catch (err) {
702
+ ui.item(`${style.yellow('✗')} Failed to install ${skill.name}: ${err.message}`);
703
+ }
704
+ }
705
+ }
706
+ ui.bar();
707
+
708
+ // 4. ENVIRONMENT CHECK
709
+ ui.step('Environment check:');
710
+ const tokenStatus = checkFigmaToken();
711
+ if (tokenStatus.found) {
712
+ ui.item(`${style.green('✓')} ${style.bold('FIGMA_ACCESS_TOKEN')} detected via ${tokenStatus.source}`);
713
+ } else {
714
+ ui.item(`${style.yellow('⚠')} ${style.bold('FIGMA_ACCESS_TOKEN')} not detected.`);
715
+ ui.item(`${style.dim('To scrape designs from Figma, set your token:')}`);
716
+ ui.item(`${style.cyan('export FIGMA_ACCESS_TOKEN="figd_your_token_here"')}`);
717
+ ui.item(`${style.dim('or add it to your project\'s .env file.')}`);
718
+ }
719
+ ui.bar();
720
+
721
+ ui.done('Done! Skills are installed and ready to use.');
722
+
723
+ if (nonTTYInterface) {
724
+ try {
725
+ nonTTYInterface.close();
726
+ } catch (_) {}
727
+ }
728
+ try {
729
+ process.stdin.pause();
730
+ } catch (_) {}
731
+ process.exit(0);
732
+ }
733
+
734
+ if (require.main === module) {
735
+ main().catch((err) => {
736
+ console.error('\nFatal error:', err.message);
737
+ process.exit(1);
738
+ });
739
+ }
740
+
741
+ module.exports = {
742
+ selectPrompt,
743
+ getAvailableSkills,
744
+ getAgentConfigs,
745
+ checkFigmaToken,
746
+ installSkill,
747
+ main,
748
+ };