@pedroab0/skills 1.0.0 → 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/LICENSE +21 -0
- package/README.md +44 -7
- package/bin/cli.js +152 -32
- package/figma-extractor/README.md +25 -7
- package/figma-extractor/SKILL.md +19 -17
- package/figma-extractor/figma-plugin/README.md +25 -9
- package/figma-extractor/figma-plugin/assets/cover-1920.png +0 -0
- package/figma-extractor/figma-plugin/assets/cover.jpg +0 -0
- package/figma-extractor/figma-plugin/assets/cover.png +0 -0
- package/figma-extractor/figma-plugin/assets/icon-128.png +0 -0
- package/figma-extractor/figma-plugin/assets/icon.png +0 -0
- package/figma-extractor/figma-plugin/manifest.json +8 -2
- 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 +2 -2
- 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 +8 -3
package/figma-to-code/SKILL.md
CHANGED
|
@@ -17,7 +17,7 @@ This skill connects to the Figma REST API, extracts a compact design-system-awar
|
|
|
17
17
|
> **NEVER use browser tools (Playwright, Puppeteer, Chrome DevTools, `browser_navigate`, `read_browser_page`) on Figma URLs.**
|
|
18
18
|
>
|
|
19
19
|
> 1. **Figma Canvas is WebGL/Wasm**: Figma renders design canvas elements inside an HTML5 `<canvas>` via WebAssembly. There are NO inspectable HTML DOM elements for frames, layers, texts, or styles. Browser automation cannot extract Figma nodes or properties.
|
|
20
|
-
> 2. **Never search
|
|
20
|
+
> 2. **Never search for local browser binaries**: Do not search OS application directories (e.g., `/Applications`, `Program Files`, `/usr/bin`) or run browser automation scripts.
|
|
21
21
|
> 3. **API Errors Are Final**: If `fetch_figma.js` returns an API error (e.g., 401 Unauthorized, 403 Forbidden, or 429 Rate Limited), **report the error directly to the user**. Guide them to check their `FIGMA_ACCESS_TOKEN`, permissions, or wait out rate limits. Do NOT attempt browser scraping fallbacks.
|
|
22
22
|
|
|
23
23
|
---
|
|
@@ -27,7 +27,7 @@ This skill connects to the Figma REST API, extracts a compact design-system-awar
|
|
|
27
27
|
To protect your Figma file quota and prevent lockout penalties, `fetch_figma.js` enforces an automatic safety gate:
|
|
28
28
|
* **Max 5 requests per minute** per file key (sliding window limit).
|
|
29
29
|
* **2.5s inter-request pacing** between consecutive requests.
|
|
30
|
-
* **Persistent Circuit Breaker**: If Figma returns HTTP 429, the script records the lockout time locally in
|
|
30
|
+
* **Persistent Circuit Breaker**: If Figma returns HTTP 429, the script records the lockout time locally in `.figma_ratelimit.json` inside the user's home directory. All future calls to that file key are **immediately blocked with zero network traffic**, preventing Figma from extending the lockout penalty.
|
|
31
31
|
|
|
32
32
|
### Agent Behavioral Rules (Zero-Hammer Protocol)
|
|
33
33
|
1. **Single-Shot Execution**: Run `fetch_figma.js` **once** per target frame or component.
|
|
@@ -41,7 +41,9 @@ To protect your Figma file quota and prevent lockout penalties, `fetch_figma.js`
|
|
|
41
41
|
A Figma Access Token (Personal Access Token or OAuth Token) is required.
|
|
42
42
|
|
|
43
43
|
Configure it using either:
|
|
44
|
-
1. **Environment Variable**:
|
|
44
|
+
1. **Environment Variable**:
|
|
45
|
+
- **macOS / Linux**: `export FIGMA_ACCESS_TOKEN="figd_your_token_here"` (or `FIGMA_TOKEN`)
|
|
46
|
+
- **Windows (PowerShell)**: `[Environment]::SetEnvironmentVariable("FIGMA_ACCESS_TOKEN", "figd_your_token_here", "User")` or `$env:FIGMA_ACCESS_TOKEN="figd_your_token_here"` in `$PROFILE`
|
|
45
47
|
2. **Workspace `.env` File**:
|
|
46
48
|
```env
|
|
47
49
|
FIGMA_ACCESS_TOKEN=figd_your_token_here
|
|
@@ -66,20 +68,20 @@ FIGMA_IGNORE_SSL=true
|
|
|
66
68
|
|
|
67
69
|
## Agent Execution Instructions
|
|
68
70
|
|
|
69
|
-
To extract and build UI from a Figma URL, run `fetch_figma.js` from
|
|
71
|
+
To extract and build UI from a Figma URL, run `fetch_figma.js` from this skill's `scripts/` directory:
|
|
70
72
|
|
|
71
73
|
```bash
|
|
72
74
|
# Recommended: Extract compact AST (1 Tier 1 call)
|
|
73
|
-
node <skill-dir>/scripts/fetch_figma.js "<FigmaURL>"
|
|
75
|
+
node "<skill-dir>/scripts/fetch_figma.js" "<FigmaURL>"
|
|
74
76
|
|
|
75
77
|
# Optional: With companion preview image (2 Tier 1 calls)
|
|
76
|
-
node <skill-dir>/scripts/fetch_figma.js "<FigmaURL>" -i
|
|
78
|
+
node "<skill-dir>/scripts/fetch_figma.js" "<FigmaURL>" -i
|
|
77
79
|
```
|
|
78
80
|
|
|
79
|
-
**
|
|
80
|
-
- **
|
|
81
|
-
- **
|
|
82
|
-
- **
|
|
81
|
+
**Resolving `<skill-dir>` (Agent-Agnostic)**:
|
|
82
|
+
Substitute `<skill-dir>` with the directory where this skill is installed — **never guess another agent's folder** (always keep double quotes around the script path for Windows/macOS/Linux compatibility):
|
|
83
|
+
- **Standard Skills (`SKILL.md`)**: `<skill-dir>` is the exact parent directory of this `SKILL.md` file.
|
|
84
|
+
- **Cursor Rule (`.cursor/rules/figma-to-code.mdc`)**: `<skill-dir>` is the sibling `.cursor/skills/figma-to-code` directory (replace `rules/figma-to-code.mdc` with `skills/figma-to-code`).
|
|
83
85
|
|
|
84
86
|
---
|
|
85
87
|
|
|
@@ -89,13 +91,13 @@ node <skill-dir>/scripts/fetch_figma.js "<FigmaURL>" -i
|
|
|
89
91
|
|
|
90
92
|
```bash
|
|
91
93
|
# Standard extraction (1 Tier 1 call - recommended)
|
|
92
|
-
node
|
|
94
|
+
node "<skill-dir>/scripts/fetch_figma.js" "https://www.figma.com/design/:fileKey/:name?node-id=4023-474"
|
|
93
95
|
|
|
94
96
|
# Optional: With preview image (2 Tier 1 calls)
|
|
95
|
-
node
|
|
97
|
+
node "<skill-dir>/scripts/fetch_figma.js" "https://www.figma.com/design/:fileKey/:name?node-id=4023-474" -i
|
|
96
98
|
|
|
97
99
|
# Collapse internal layers of component instances into props (guides agent to reuse existing components)
|
|
98
|
-
node
|
|
100
|
+
node "<skill-dir>/scripts/fetch_figma.js" "https://www.figma.com/design/:fileKey/:name?node-id=4023-474" --shallow-instances
|
|
99
101
|
```
|
|
100
102
|
|
|
101
103
|
### 2. CLI Flags Reference
|
|
@@ -169,7 +171,7 @@ When an `INSTANCE` node is present, internal property hashes are converted into
|
|
|
169
171
|
|
|
170
172
|
## How to Build Components from the Extracted Data
|
|
171
173
|
|
|
172
|
-
When synthesizing code from the output:
|
|
174
|
+
When synthesizing code from the output (always adapting syntax, attributes, and styling conventions to the project's detected stack — e.g., React, Vue, Svelte, Angular, or plain HTML/CSS):
|
|
173
175
|
|
|
174
176
|
1. **Using Existing UI Components**:
|
|
175
177
|
- For `INSTANCE` nodes with resolved `component.name` and `props`:
|
|
@@ -202,10 +204,10 @@ When synthesizing code from the output:
|
|
|
202
204
|
|
|
203
205
|
- **Token Expired / Missing (`FIGMA_TOKEN_EXPIRED_OR_INVALID` / `isTokenExpiredOrInvalid`)**:
|
|
204
206
|
- Alert the user that their Figma access token is missing, invalid, or expired.
|
|
205
|
-
- Instruct the user to create a token in **Figma Settings $\rightarrow$ Personal Access Tokens** and
|
|
207
|
+
- Instruct the user to create a token in **Figma Settings $\rightarrow$ Personal Access Tokens** and configure `FIGMA_ACCESS_TOKEN` in their environment or workspace `.env` file.
|
|
206
208
|
- **Node Not Found**:
|
|
207
209
|
- Alert the user that the node ID could not be found in the specified file.
|
|
208
210
|
- **Corporate Proxy / Self-Signed SSL (`FIGMA_SSL_CERTIFICATE_ERROR`)**:
|
|
209
|
-
- Instruct the user to add `FIGMA_IGNORE_SSL=true` to `.env` or
|
|
211
|
+
- Instruct the user to add `FIGMA_IGNORE_SSL=true` to `.env` or set it in their environment.
|
|
210
212
|
- **Rate Limited (`FIGMA_RATE_LIMIT_EXCEEDED` / `FIGMA_CIRCUIT_BREAKER_ACTIVE`)**:
|
|
211
213
|
- Figma REST API has strict per-minute and monthly rate limits. If rate limited or the circuit breaker trips, report the cooldown to the user and suggest duplicating the file in Figma Drafts to get a fresh URL.
|
|
@@ -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.
|
|
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",
|
|
@@ -36,6 +40,7 @@
|
|
|
36
40
|
"figma-to-code",
|
|
37
41
|
"figma-extractor",
|
|
38
42
|
"figma-node-builder",
|
|
39
|
-
"README.md"
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE"
|
|
40
45
|
]
|
|
41
46
|
}
|