@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/README.md +173 -2
- package/bin/cli.js +748 -0
- package/figma-extractor/README.md +170 -0
- package/figma-extractor/SKILL.md +199 -0
- package/figma-extractor/demo/desktop_home.json +2434 -0
- package/figma-extractor/demo/dt_money_desktop_home.png +0 -0
- package/figma-extractor/demo/dt_money_preview.png +0 -0
- package/figma-extractor/demo/dt_money_selection.json +472 -0
- package/figma-extractor/demo/index.html +188 -0
- package/figma-extractor/demo/style.css +360 -0
- package/figma-extractor/figma-plugin/README.md +70 -0
- package/figma-extractor/figma-plugin/code.js +407 -0
- package/figma-extractor/figma-plugin/manifest.json +10 -0
- package/figma-extractor/figma-plugin/ui.html +470 -0
- package/figma-extractor/scripts/fetch_figma.js +1314 -0
- package/figma-extractor/scripts/test_fetch_figma.js +601 -0
- package/figma-node-builder/README.md +107 -0
- package/figma-node-builder/SKILL.md +113 -0
- package/figma-node-builder/scripts/inspect_spec.js +514 -0
- package/figma-node-builder/scripts/test_inspect_spec.js +148 -0
- package/figma-to-code/README.md +160 -0
- package/figma-to-code/SKILL.md +211 -0
- package/figma-to-code/scripts/fetch_figma.js +1251 -0
- package/figma-to-code/scripts/test_fetch_figma.js +581 -0
- package/package.json +39 -4
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
|
+
};
|