@pedroab0/skills 1.0.1 → 1.1.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 +19 -3
- package/bin/cli.js +152 -32
- package/figma-extractor/README.md +14 -6
- package/figma-extractor/SKILL.md +18 -16
- package/figma-extractor/figma-plugin/README.md +10 -5
- package/figma-extractor/scripts/fetch_figma.js +180 -26
- package/figma-extractor/scripts/test_fetch_figma.js +90 -4
- package/figma-node-builder/README.md +1 -1
- package/figma-node-builder/SKILL.md +7 -8
- package/figma-node-builder/scripts/inspect_spec.js +15 -2
- package/figma-node-builder/scripts/test_inspect_spec.js +16 -1
- package/figma-to-code/README.md +10 -4
- package/figma-to-code/SKILL.md +18 -16
- package/figma-to-code/scripts/fetch_figma.js +163 -18
- package/figma-to-code/scripts/test_fetch_figma.js +79 -0
- package/package.json +6 -2
|
@@ -11,27 +11,138 @@ const { URL } = require('url');
|
|
|
11
11
|
// Environment and Auth Utilities
|
|
12
12
|
// -----------------------------------------------------------------------------
|
|
13
13
|
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
function resolveHomeDir() {
|
|
15
|
+
return (
|
|
16
|
+
process.env.HOME ||
|
|
17
|
+
process.env.USERPROFILE ||
|
|
18
|
+
(typeof os.homedir === 'function' ? os.homedir() : '') ||
|
|
19
|
+
''
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function decodeEnvBuffer(buf) {
|
|
24
|
+
if (!Buffer.isBuffer(buf)) {
|
|
25
|
+
const str = String(buf || '');
|
|
26
|
+
return str.charCodeAt(0) === 0xfeff ? str.slice(1) : str;
|
|
27
|
+
}
|
|
28
|
+
if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) {
|
|
29
|
+
return buf.toString('utf16le', 2);
|
|
30
|
+
}
|
|
31
|
+
const str = buf.toString('utf8');
|
|
32
|
+
return str.charCodeAt(0) === 0xfeff ? str.slice(1) : str;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getPowerShellProfilePaths(home) {
|
|
36
|
+
const paths = [];
|
|
37
|
+
if (process.env.PROFILE && process.env.PROFILE.toLowerCase().endsWith('.ps1')) {
|
|
38
|
+
paths.push(process.env.PROFILE);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const docRoots = [];
|
|
42
|
+
if (home) {
|
|
43
|
+
docRoots.push(
|
|
44
|
+
path.join(home, 'Documents'),
|
|
45
|
+
path.join(home, 'Documentos'),
|
|
46
|
+
path.join(home, 'OneDrive', 'Documents'),
|
|
47
|
+
path.join(home, 'OneDrive', 'Documentos'),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const oneDriveEnvs = [
|
|
52
|
+
process.env.OneDrive,
|
|
53
|
+
process.env.OneDriveConsumer,
|
|
54
|
+
process.env.OneDriveCommercial,
|
|
55
|
+
].filter(Boolean);
|
|
56
|
+
|
|
57
|
+
for (const od of oneDriveEnvs) {
|
|
58
|
+
docRoots.push(path.join(od, 'Documents'), path.join(od, 'Documentos'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const shellDirs = ['PowerShell', 'WindowsPowerShell'];
|
|
62
|
+
const profileFiles = [
|
|
63
|
+
'Microsoft.PowerShell_profile.ps1',
|
|
64
|
+
'Microsoft.VSCode_profile.ps1',
|
|
65
|
+
'profile.ps1',
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
for (const docRoot of docRoots) {
|
|
69
|
+
if (!fs.existsSync(docRoot)) continue;
|
|
70
|
+
for (const shellDir of shellDirs) {
|
|
71
|
+
const fullShellDir = path.join(docRoot, shellDir);
|
|
72
|
+
if (!fs.existsSync(fullShellDir)) continue;
|
|
73
|
+
for (const profileFile of profileFiles) {
|
|
74
|
+
paths.push(path.join(fullShellDir, profileFile));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (home) {
|
|
80
|
+
const xdgPsDir = path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'powershell');
|
|
81
|
+
if (fs.existsSync(xdgPsDir)) {
|
|
82
|
+
for (const profileFile of profileFiles) {
|
|
83
|
+
paths.push(path.join(xdgPsDir, profileFile));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return Array.from(new Set(paths));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function loadEnv(options = {}) {
|
|
92
|
+
const cwd = options.cwd || process.cwd();
|
|
93
|
+
const home = options.home !== undefined ? options.home : resolveHomeDir();
|
|
94
|
+
|
|
95
|
+
const dotEnvCandidates = [
|
|
96
|
+
path.join(cwd, '.env'),
|
|
97
|
+
path.join(cwd, '.env.local'),
|
|
18
98
|
home ? path.join(home, '.env') : null,
|
|
99
|
+
].filter(Boolean);
|
|
100
|
+
|
|
101
|
+
const shellConfigCandidates = [
|
|
19
102
|
home ? path.join(home, '.zshrc') : null,
|
|
103
|
+
home ? path.join(home, '.zprofile') : null,
|
|
104
|
+
home ? path.join(home, '.zshenv') : null,
|
|
20
105
|
home ? path.join(home, '.bashrc') : null,
|
|
106
|
+
home ? path.join(home, '.bash_profile') : null,
|
|
107
|
+
home ? path.join(home, '.profile') : null,
|
|
108
|
+
...getPowerShellProfilePaths(home),
|
|
21
109
|
].filter(Boolean);
|
|
22
110
|
|
|
23
|
-
|
|
24
|
-
|
|
111
|
+
const seen = new Set();
|
|
112
|
+
const candidates = [];
|
|
113
|
+
for (const p of dotEnvCandidates) {
|
|
114
|
+
if (!seen.has(p)) {
|
|
115
|
+
seen.add(p);
|
|
116
|
+
candidates.push({ filePath: p, isShellConfig: false });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const p of shellConfigCandidates) {
|
|
120
|
+
if (!seen.has(p)) {
|
|
121
|
+
seen.add(p);
|
|
122
|
+
candidates.push({ filePath: p, isShellConfig: true });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const { filePath, isShellConfig } of candidates) {
|
|
127
|
+
if (fs.existsSync(filePath)) {
|
|
25
128
|
try {
|
|
26
|
-
const
|
|
27
|
-
const
|
|
129
|
+
const rawBuf = fs.readFileSync(filePath);
|
|
130
|
+
const envContent = decodeEnvBuffer(rawBuf);
|
|
131
|
+
if (isShellConfig && !/figma_/i.test(envContent)) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const lines = envContent.split(/\r?\n/);
|
|
28
135
|
for (const line of lines) {
|
|
136
|
+
if (isShellConfig && !/figma_/i.test(line)) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
29
139
|
const parsed = parseEnvLine(line);
|
|
30
140
|
if (parsed && parsed.key) {
|
|
141
|
+
const isFigmaVar = /^FIGMA_/i.test(parsed.key);
|
|
142
|
+
const envKey = isFigmaVar ? parsed.key.toUpperCase() : parsed.key;
|
|
31
143
|
// For dot-env files, load any variable; for shell configs, only load FIGMA_ variables
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
process.env[parsed.key] = parsed.val;
|
|
144
|
+
if (!process.env[envKey] && (!isShellConfig || isFigmaVar)) {
|
|
145
|
+
process.env[envKey] = parsed.val;
|
|
35
146
|
}
|
|
36
147
|
}
|
|
37
148
|
}
|
|
@@ -44,16 +155,47 @@ function loadEnv() {
|
|
|
44
155
|
|
|
45
156
|
function parseEnvLine(line) {
|
|
46
157
|
if (!line || typeof line !== 'string') return null;
|
|
47
|
-
let trimmed = line.trim();
|
|
48
|
-
if (trimmed.startsWith('
|
|
49
|
-
|
|
158
|
+
let trimmed = line.replace(/^\uFEFF/, '').trim();
|
|
159
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
160
|
+
return null;
|
|
50
161
|
}
|
|
51
|
-
|
|
162
|
+
|
|
163
|
+
// Handle PowerShell [Environment]::SetEnvironmentVariable("KEY", "VAL", ...)
|
|
164
|
+
const setEnvMatch = trimmed.match(
|
|
165
|
+
/^\[(?:System\.)?Environment\]::SetEnvironmentVariable\(\s*(['"])([A-Za-z_][A-Za-z0-9_]*)\1\s*,\s*(['"])([^'"]*)\3/i,
|
|
166
|
+
);
|
|
167
|
+
if (setEnvMatch) {
|
|
168
|
+
return { key: setEnvMatch[2], val: setEnvMatch[4] };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Strip Unix 'export ' or Windows CMD 'set ' prefix (requires whitespace after keyword)
|
|
172
|
+
if (/^export\s+/i.test(trimmed)) {
|
|
173
|
+
trimmed = trimmed.replace(/^export\s+/i, '').trim();
|
|
174
|
+
} else if (/^set\s+/i.test(trimmed)) {
|
|
175
|
+
trimmed = trimmed.replace(/^set\s+/i, '').trim();
|
|
176
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.includes('=')) {
|
|
177
|
+
trimmed = trimmed.slice(1, -1).trim();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!trimmed.includes('=')) {
|
|
52
182
|
return null;
|
|
53
183
|
}
|
|
54
184
|
|
|
55
185
|
const eqIdx = trimmed.indexOf('=');
|
|
56
|
-
|
|
186
|
+
let key = trimmed.slice(0, eqIdx).trim();
|
|
187
|
+
|
|
188
|
+
// Strip PowerShell $env:KEY or ${env:KEY} syntax
|
|
189
|
+
if (/^\$env:/i.test(key)) {
|
|
190
|
+
key = key.replace(/^\$env:/i, '').trim();
|
|
191
|
+
} else if (/^\$\{env:([^}]+)\}$/i.test(key)) {
|
|
192
|
+
key = key.replace(/^\$\{env:([^}]+)\}$/i, '$1').trim();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
57
199
|
let rest = trimmed.slice(eqIdx + 1).trim();
|
|
58
200
|
|
|
59
201
|
let val = '';
|
|
@@ -566,7 +708,7 @@ const MAX_REQUESTS_PER_WINDOW = 5; // Max 5 calls per minute (well below Figma 1
|
|
|
566
708
|
const MIN_REQUEST_INTERVAL_MS = 2500; // Minimum 2.5s between consecutive requests
|
|
567
709
|
|
|
568
710
|
function getRateGateFilePath() {
|
|
569
|
-
const home =
|
|
711
|
+
const home = resolveHomeDir();
|
|
570
712
|
return home ? path.join(home, '.figma_ratelimit.json') : path.join(process.cwd(), '.figma_ratelimit.json');
|
|
571
713
|
}
|
|
572
714
|
|
|
@@ -1173,7 +1315,7 @@ function formatApiError(error) {
|
|
|
1173
1315
|
} else if (error.statusCode === 401 || error.statusCode === 403) {
|
|
1174
1316
|
isTokenExpiredOrInvalid = true;
|
|
1175
1317
|
errorMessage =
|
|
1176
|
-
'FIGMA_TOKEN_EXPIRED_OR_INVALID: The provided Figma access token is expired, invalid, or unauthorized. Create a new token in Figma Settings -> Personal Access Tokens and
|
|
1318
|
+
'FIGMA_TOKEN_EXPIRED_OR_INVALID: The provided Figma access token is expired, invalid, or unauthorized. Create a new token in Figma Settings -> Personal Access Tokens and set FIGMA_ACCESS_TOKEN in your environment or workspace .env file.';
|
|
1177
1319
|
} else if (error.statusCode === 429) {
|
|
1178
1320
|
isRateLimited = true;
|
|
1179
1321
|
let retryMsg = ' Please wait 30-60 seconds before making more requests.';
|
|
@@ -1221,6 +1363,9 @@ function formatApiError(error) {
|
|
|
1221
1363
|
|
|
1222
1364
|
// Export internal functions for unit testing
|
|
1223
1365
|
module.exports = {
|
|
1366
|
+
resolveHomeDir,
|
|
1367
|
+
decodeEnvBuffer,
|
|
1368
|
+
getPowerShellProfilePaths,
|
|
1224
1369
|
loadEnv,
|
|
1225
1370
|
parseEnvLine,
|
|
1226
1371
|
getFigmaToken,
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const assert = require('assert');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
4
7
|
const {
|
|
8
|
+
decodeEnvBuffer,
|
|
5
9
|
loadEnv,
|
|
6
10
|
parseEnvLine,
|
|
7
11
|
parseFigmaUrl,
|
|
@@ -70,6 +74,81 @@ test('parseEnvLine: returns null for comments, empty lines, and lines without eq
|
|
|
70
74
|
assert.strictEqual(parseEnvLine('export FIGMA_TOKEN'), null);
|
|
71
75
|
});
|
|
72
76
|
|
|
77
|
+
test('parseEnvLine: parses Windows PowerShell $env: syntax, CRLF, and SetEnvironmentVariable', () => {
|
|
78
|
+
assert.deepStrictEqual(
|
|
79
|
+
parseEnvLine('$env:FIGMA_ACCESS_TOKEN="figd_win_ps1"\r\n'),
|
|
80
|
+
{ key: 'FIGMA_ACCESS_TOKEN', val: 'figd_win_ps1' }
|
|
81
|
+
);
|
|
82
|
+
assert.deepStrictEqual(
|
|
83
|
+
parseEnvLine("$Env:FIGMA_ACCESS_TOKEN = 'figd_win_single' # ps comment\r\n"),
|
|
84
|
+
{ key: 'FIGMA_ACCESS_TOKEN', val: 'figd_win_single' }
|
|
85
|
+
);
|
|
86
|
+
assert.deepStrictEqual(
|
|
87
|
+
parseEnvLine('${env:FIGMA_TOKEN}="figd_braced"'),
|
|
88
|
+
{ key: 'FIGMA_TOKEN', val: 'figd_braced' }
|
|
89
|
+
);
|
|
90
|
+
assert.deepStrictEqual(
|
|
91
|
+
parseEnvLine('[System.Environment]::SetEnvironmentVariable("FIGMA_ACCESS_TOKEN", "figd_sys_env", "User")'),
|
|
92
|
+
{ key: 'FIGMA_ACCESS_TOKEN', val: 'figd_sys_env' }
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('parseEnvLine: parses Windows CMD set syntax without corrupting SETTINGS/EXPORT keys', () => {
|
|
97
|
+
assert.deepStrictEqual(
|
|
98
|
+
parseEnvLine('set FIGMA_ACCESS_TOKEN=figd_cmd_plain\r\n'),
|
|
99
|
+
{ key: 'FIGMA_ACCESS_TOKEN', val: 'figd_cmd_plain' }
|
|
100
|
+
);
|
|
101
|
+
assert.deepStrictEqual(
|
|
102
|
+
parseEnvLine('set "FIGMA_ACCESS_TOKEN=figd_cmd_quoted"'),
|
|
103
|
+
{ key: 'FIGMA_ACCESS_TOKEN', val: 'figd_cmd_quoted' }
|
|
104
|
+
);
|
|
105
|
+
assert.deepStrictEqual(
|
|
106
|
+
parseEnvLine('SETTINGS=enabled'),
|
|
107
|
+
{ key: 'SETTINGS', val: 'enabled' }
|
|
108
|
+
);
|
|
109
|
+
assert.deepStrictEqual(
|
|
110
|
+
parseEnvLine('EXPORT_MODE=fast'),
|
|
111
|
+
{ key: 'EXPORT_MODE', val: 'fast' }
|
|
112
|
+
);
|
|
113
|
+
assert.strictEqual(parseEnvLine('$MaximumHistoryCount = 4096'), null);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('loadEnv & decodeEnvBuffer: loads FIGMA_ vars from UTF-16LE PowerShell profile and ignores non-FIGMA_ vars', () => {
|
|
117
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'figma-to-code-win-env-'));
|
|
118
|
+
const savedAccess = process.env.FIGMA_ACCESS_TOKEN;
|
|
119
|
+
const savedOther = process.env.UNRELATED_PS_SECRET;
|
|
120
|
+
delete process.env.FIGMA_ACCESS_TOKEN;
|
|
121
|
+
delete process.env.UNRELATED_PS_SECRET;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const psDir = path.join(tmpRoot, 'Documents', 'PowerShell');
|
|
125
|
+
fs.mkdirSync(psDir, { recursive: true });
|
|
126
|
+
const psFile = path.join(psDir, 'Microsoft.PowerShell_profile.ps1');
|
|
127
|
+
|
|
128
|
+
const psContent = '$env:FIGMA_ACCESS_TOKEN="figd_utf16_ps7"\r\n$env:UNRELATED_PS_SECRET="should_not_leak"\r\n';
|
|
129
|
+
const utf16WithBom = Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(psContent, 'utf16le')]);
|
|
130
|
+
fs.writeFileSync(psFile, utf16WithBom);
|
|
131
|
+
|
|
132
|
+
assert.ok(decodeEnvBuffer(utf16WithBom).includes('figd_utf16_ps7'));
|
|
133
|
+
|
|
134
|
+
loadEnv({ cwd: tmpRoot, home: tmpRoot });
|
|
135
|
+
assert.strictEqual(process.env.FIGMA_ACCESS_TOKEN, 'figd_utf16_ps7');
|
|
136
|
+
assert.strictEqual(process.env.UNRELATED_PS_SECRET, undefined);
|
|
137
|
+
|
|
138
|
+
// Case-insensitive PowerShell $env:figma_access_token normalization
|
|
139
|
+
delete process.env.FIGMA_ACCESS_TOKEN;
|
|
140
|
+
fs.writeFileSync(psFile, '$env:figma_access_token="figd_lowercase_ps7"\r\n', 'utf8');
|
|
141
|
+
loadEnv({ cwd: tmpRoot, home: tmpRoot });
|
|
142
|
+
assert.strictEqual(process.env.FIGMA_ACCESS_TOKEN, 'figd_lowercase_ps7');
|
|
143
|
+
} finally {
|
|
144
|
+
if (savedAccess !== undefined) process.env.FIGMA_ACCESS_TOKEN = savedAccess;
|
|
145
|
+
else delete process.env.FIGMA_ACCESS_TOKEN;
|
|
146
|
+
if (savedOther !== undefined) process.env.UNRELATED_PS_SECRET = savedOther;
|
|
147
|
+
else delete process.env.UNRELATED_PS_SECRET;
|
|
148
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
73
152
|
// 1. URL Parsing Tests
|
|
74
153
|
test('parseFigmaUrl: extracts fileKey and nodeId with hyphens', () => {
|
|
75
154
|
const url = 'https://www.figma.com/design/1KP9pVQ1ptZzHa3Gcpr6ta/Consulta-e-pagamento?node-id=4023-474&t=abcd';
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pedroab0/skills",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Curated collection of modular, production-ready skills for AI coding agents (Claude Code, Cursor, OpenCode, Antigravity)",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Curated collection of modular, production-ready skills for AI coding agents (Claude Code, Cursor, Kiro, OpenCode, Antigravity)",
|
|
5
5
|
"bin": {
|
|
6
6
|
"skills": "bin/cli.js",
|
|
7
7
|
"skills-installer": "bin/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "./bin/cli.js",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=16.7.0"
|
|
12
|
+
},
|
|
10
13
|
"scripts": {
|
|
11
14
|
"test": "node figma-extractor/scripts/test_fetch_figma.js && node figma-to-code/scripts/test_fetch_figma.js && node figma-node-builder/scripts/test_inspect_spec.js && node test/test_cli.js",
|
|
12
15
|
"install-skills": "node ./bin/cli.js"
|
|
@@ -20,6 +23,7 @@
|
|
|
20
23
|
"skills",
|
|
21
24
|
"claude-code",
|
|
22
25
|
"cursor",
|
|
26
|
+
"kiro",
|
|
23
27
|
"antigravity",
|
|
24
28
|
"opencode",
|
|
25
29
|
"figma",
|