@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.
@@ -11,27 +11,138 @@ const { URL } = require('url');
11
11
  // Environment and Auth Utilities
12
12
  // -----------------------------------------------------------------------------
13
13
 
14
- function loadEnv() {
15
- const home = process.env.HOME || '';
16
- const candidates = [
17
- path.join(process.cwd(), '.env'),
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
- for (const envPath of candidates) {
24
- if (fs.existsSync(envPath)) {
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 envContent = fs.readFileSync(envPath, 'utf8');
27
- const lines = envContent.split('\n');
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
- const isShellConfig = envPath.endsWith('.zshrc') || envPath.endsWith('.bashrc');
33
- if (!process.env[parsed.key] && (!isShellConfig || parsed.key.startsWith('FIGMA_'))) {
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('export ')) {
49
- trimmed = trimmed.substring(7).trim();
158
+ let trimmed = line.replace(/^\uFEFF/, '').trim();
159
+ if (!trimmed || trimmed.startsWith('#')) {
160
+ return null;
161
+ }
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] };
50
169
  }
51
- if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) {
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
- const key = trimmed.slice(0, eqIdx).trim();
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 = process.env.HOME || (typeof os.homedir === 'function' ? os.homedir() : '');
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
 
@@ -1132,15 +1274,24 @@ async function main() {
1132
1274
  process.exit(1);
1133
1275
  }
1134
1276
  const { dictionary } = buildVariableMap(varsMeta);
1135
- console.log(
1136
- JSON.stringify(
1137
- {
1138
- tokens: dictionary,
1139
- },
1140
- null,
1141
- config.pretty ? 2 : 0,
1142
- ),
1277
+ const tokenPayload = JSON.stringify(
1278
+ {
1279
+ tokens: dictionary,
1280
+ },
1281
+ null,
1282
+ config.pretty ? 2 : 0,
1143
1283
  );
1284
+ if (config.specJsonPath) {
1285
+ const jsonDir = path.dirname(path.resolve(config.specJsonPath));
1286
+ if (!fs.existsSync(jsonDir)) {
1287
+ fs.mkdirSync(jsonDir, { recursive: true });
1288
+ }
1289
+ fs.writeFileSync(path.resolve(config.specJsonPath), tokenPayload, 'utf8');
1290
+ const sizeKb = (Buffer.byteLength(tokenPayload, 'utf8') / 1024).toFixed(1);
1291
+ console.log(`\n✔ Saved Figma design tokens to ${config.specJsonPath} (${sizeKb} KB)\n`);
1292
+ } else {
1293
+ console.log(tokenPayload);
1294
+ }
1144
1295
  return;
1145
1296
  }
1146
1297
 
@@ -1236,7 +1387,7 @@ function formatApiError(error) {
1236
1387
  } else if (error.statusCode === 401 || error.statusCode === 403) {
1237
1388
  isTokenExpiredOrInvalid = true;
1238
1389
  errorMessage =
1239
- '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 export FIGMA_ACCESS_TOKEN or add it to .env.';
1390
+ '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.';
1240
1391
  } else if (error.statusCode === 429) {
1241
1392
  isRateLimited = true;
1242
1393
  let retryMsg = ' Please wait 30-60 seconds before making more requests.';
@@ -1284,6 +1435,9 @@ function formatApiError(error) {
1284
1435
 
1285
1436
  // Export internal functions for unit testing
1286
1437
  module.exports = {
1438
+ resolveHomeDir,
1439
+ decodeEnvBuffer,
1440
+ getPowerShellProfilePaths,
1287
1441
  loadEnv,
1288
1442
  parseEnvLine,
1289
1443
  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,88 @@ 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
+ // Arbitrary PowerShell local variables must not parse as env identifiers
114
+ assert.strictEqual(parseEnvLine('$MaximumHistoryCount = 4096'), null);
115
+ });
116
+
117
+ test('loadEnv & decodeEnvBuffer: loads FIGMA_ vars from UTF-16LE PowerShell profile and ignores non-FIGMA_ vars', () => {
118
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'figma-win-env-'));
119
+ const savedAccess = process.env.FIGMA_ACCESS_TOKEN;
120
+ const savedOther = process.env.UNRELATED_PS_SECRET;
121
+ delete process.env.FIGMA_ACCESS_TOKEN;
122
+ delete process.env.UNRELATED_PS_SECRET;
123
+
124
+ try {
125
+ const psDir = path.join(tmpRoot, 'OneDrive', 'Documentos', 'WindowsPowerShell');
126
+ fs.mkdirSync(psDir, { recursive: true });
127
+ const psFile = path.join(psDir, 'Microsoft.PowerShell_profile.ps1');
128
+
129
+ const psContent = '$env:FIGMA_ACCESS_TOKEN="figd_utf16_powershell"\r\n$env:UNRELATED_PS_SECRET="should_not_leak"\r\n';
130
+ const utf16WithBom = Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(psContent, 'utf16le')]);
131
+ fs.writeFileSync(psFile, utf16WithBom);
132
+
133
+ assert.ok(decodeEnvBuffer(utf16WithBom).includes('figd_utf16_powershell'));
134
+
135
+ loadEnv({ cwd: tmpRoot, home: tmpRoot });
136
+ assert.strictEqual(process.env.FIGMA_ACCESS_TOKEN, 'figd_utf16_powershell');
137
+ assert.strictEqual(process.env.UNRELATED_PS_SECRET, undefined);
138
+
139
+ // Case-insensitive PowerShell $env:figma_access_token normalization
140
+ delete process.env.FIGMA_ACCESS_TOKEN;
141
+ fs.writeFileSync(psFile, '$env:figma_access_token="figd_lowercase_ps"\r\n', 'utf8');
142
+ loadEnv({ cwd: tmpRoot, home: tmpRoot });
143
+ assert.strictEqual(process.env.FIGMA_ACCESS_TOKEN, 'figd_lowercase_ps');
144
+
145
+ // Workspace .env must take precedence over shell profile
146
+ delete process.env.FIGMA_ACCESS_TOKEN;
147
+ fs.writeFileSync(path.join(tmpRoot, '.env'), 'FIGMA_ACCESS_TOKEN="figd_from_dotenv"\n', 'utf8');
148
+ loadEnv({ cwd: tmpRoot, home: tmpRoot });
149
+ assert.strictEqual(process.env.FIGMA_ACCESS_TOKEN, 'figd_from_dotenv');
150
+ } finally {
151
+ if (savedAccess !== undefined) process.env.FIGMA_ACCESS_TOKEN = savedAccess;
152
+ else delete process.env.FIGMA_ACCESS_TOKEN;
153
+ if (savedOther !== undefined) process.env.UNRELATED_PS_SECRET = savedOther;
154
+ else delete process.env.UNRELATED_PS_SECRET;
155
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
156
+ }
157
+ });
158
+
73
159
  // 1. URL Parsing Tests
74
160
  test('parseFigmaUrl: extracts fileKey and nodeId with hyphens', () => {
75
161
  const url = 'https://www.figma.com/design/1KP9pVQ1ptZzHa3Gcpr6ta/Consulta-e-pagamento?node-id=4023-474&t=abcd';
@@ -342,8 +428,8 @@ test('parseArguments: --spec sets atomic specJsonPath and companion imagePath',
342
428
  const argv = ['node', 'fetch_figma.js', 'https://www.figma.com/design/xyz123/Test?node-id=1-2', '--spec', 'card'];
343
429
  const res = parseArguments(argv);
344
430
  assert.strictEqual(res.specName, 'card');
345
- assert.strictEqual(res.specJsonPath, '.specs/card.json');
346
- assert.strictEqual(res.imagePath, '.specs/card.png');
431
+ assert.strictEqual(res.specJsonPath, path.join('.specs', 'card.json'));
432
+ assert.strictEqual(res.imagePath, path.join('.specs', 'card.png'));
347
433
  assert.strictEqual(res.downloadImage, false, 'Default --spec must NOT download image to conserve Tier 1 quota');
348
434
 
349
435
  const argvWithImage = ['node', 'fetch_figma.js', 'https://www.figma.com/design/xyz123/Test?node-id=1-2', '--spec', 'card', '-i'];
@@ -353,8 +439,8 @@ test('parseArguments: --spec sets atomic specJsonPath and companion imagePath',
353
439
 
354
440
  const argvCustom = ['node', 'fetch_figma.js', 'xyz123', '1:2', '--spec', 'custom/dir/navbar.json'];
355
441
  const resCustom = parseArguments(argvCustom);
356
- assert.strictEqual(resCustom.specJsonPath, 'custom/dir/navbar.json');
357
- assert.strictEqual(resCustom.imagePath, 'custom/dir/navbar.png');
442
+ assert.strictEqual(resCustom.specJsonPath, path.join('custom', 'dir', 'navbar.json'));
443
+ assert.strictEqual(resCustom.imagePath, path.join('custom', 'dir', 'navbar.png'));
358
444
  assert.strictEqual(resCustom.downloadImage, false);
359
445
  });
360
446
 
@@ -67,7 +67,7 @@ When invoked, the agent systematically executes four phases:
67
67
 
68
68
  ## How to Use the Skill
69
69
 
70
- Simply prompt your AI coding assistant (Claude Code, Cursor, OpenCode, Codex, or Antigravity):
70
+ Simply prompt your AI coding assistant (Claude Code, Kiro, Cursor, OpenCode, Codex, or Antigravity):
71
71
 
72
72
  ```text
73
73
  "Build the component from .specs/transaction-card.json using .specs/transaction-card.png as visual reference"
@@ -39,14 +39,13 @@ Before writing any code, inspect the host project to ground your implementation:
39
39
  ### Phase 2: Gap Analysis & Token Mapping
40
40
  Analyze the input JSON spec (`.specs/<name>.json`).
41
41
 
42
- > **Tip**: Instead of reading the entire raw JSON AST into your context window, run the companion **Spec Inspector CLI** to get a compact summary:
42
+ > **Tip**: Instead of reading the entire raw JSON AST into your context window, run the companion **Spec Inspector CLI** from this skill's directory (`<skill-dir>` is the folder containing this `SKILL.md` file; if loaded from `.cursor/rules/figma-node-builder.mdc`, use `.cursor/skills/figma-node-builder`):
43
43
  > ```bash
44
- > # Global Antigravity install:
45
- > node ~/.gemini/config/skills/figma-node-builder/scripts/inspect_spec.js .specs/<name>.json
46
- > # Or project local install:
47
- > node figma-node-builder/scripts/inspect_spec.js .specs/<name>.json
44
+ > # Compact terminal summary (always quote the script path for Windows/macOS/Linux compatibility):
45
+ > node "<skill-dir>/scripts/inspect_spec.js" .specs/<name>.json
46
+ >
48
47
  > # Output Markdown directly for your implementation plan:
49
- > node <skill-dir>/scripts/inspect_spec.js .specs/<name>.json --markdown
48
+ > node "<skill-dir>/scripts/inspect_spec.js" .specs/<name>.json --markdown
50
49
  > ```
51
50
 
52
51
  * **Component Instances**: For each node with `type: "INSTANCE"`, match `component.name` against existing codebase components. Extract its `props` (e.g., `{ variant: "outline", size: "lg", hasIcon: true }`).
@@ -54,10 +53,10 @@ Analyze the input JSON spec (`.specs/<name>.json`).
54
53
  * **Gaps**: Identify any sub-components that do not exist yet in the codebase and plan to scaffold them.
55
54
 
56
55
  ### Phase 3: Grounded Code Synthesis
57
- Generate component code strictly adhering to the node specification:
56
+ Generate component code strictly adhering to the node specification and adapting syntax to the host project's detected stack (e.g., React, Vue, Svelte, Angular, or HTML/CSS):
58
57
 
59
58
  1. **Reusing Existing Components (Anti-Div Soup Rule)**:
60
- - When an `INSTANCE` node matches an existing component, **import and render it directly**:
59
+ - When an `INSTANCE` node matches an existing component, **import and render it directly** (example in JSX — adapt to your framework):
61
60
  ```tsx
62
61
  // Correct:
63
62
  <Button variant="primary" size="large" hasIcon>Confirm Order</Button>
@@ -28,9 +28,21 @@ const style = {
28
28
  };
29
29
 
30
30
  // -----------------------------------------------------------------------------
31
- // Path Resolution
31
+ // Path & Encoding Resolution
32
32
  // -----------------------------------------------------------------------------
33
33
 
34
+ function decodeTextBuffer(buf) {
35
+ if (!Buffer.isBuffer(buf)) {
36
+ const str = String(buf || '');
37
+ return str.charCodeAt(0) === 0xfeff ? str.slice(1) : str;
38
+ }
39
+ if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) {
40
+ return buf.toString('utf16le', 2);
41
+ }
42
+ const str = buf.toString('utf8');
43
+ return str.charCodeAt(0) === 0xfeff ? str.slice(1) : str;
44
+ }
45
+
34
46
  function resolveSpecPath(inputPath) {
35
47
  if (!inputPath || typeof inputPath !== 'string') {
36
48
  throw new Error('Please specify a specification file (e.g. .specs/card.json or card)');
@@ -480,7 +492,7 @@ function main() {
480
492
 
481
493
  try {
482
494
  const resolvedPath = resolveSpecPath(positional[0]);
483
- const rawContent = fs.readFileSync(resolvedPath, 'utf8');
495
+ const rawContent = decodeTextBuffer(fs.readFileSync(resolvedPath));
484
496
  const parsedData = JSON.parse(rawContent);
485
497
 
486
498
  const inspection = inspectSpec(parsedData, resolvedPath);
@@ -507,6 +519,7 @@ if (require.main === module) {
507
519
  }
508
520
 
509
521
  module.exports = {
522
+ decodeTextBuffer,
510
523
  resolveSpecPath,
511
524
  inspectSpec,
512
525
  formatTerminalReport,
@@ -4,7 +4,9 @@ const assert = require('assert');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
+ const { execSync } = require('child_process');
7
8
  const {
9
+ decodeTextBuffer,
8
10
  resolveSpecPath,
9
11
  inspectSpec,
10
12
  formatTerminalReport,
@@ -141,8 +143,21 @@ try {
141
143
  } finally {
142
144
  process.chdir(origCwd);
143
145
  }
146
+
147
+ // 7. decodeTextBuffer & CLI UTF-8 BOM / UTF-16LE BOM resilience (Windows PowerShell 5.1 & Notepad)
148
+ const utf8BomFile = path.join(specsSubdir, 'utf8bom.json');
149
+ fs.writeFileSync(utf8BomFile, '\uFEFF' + JSON.stringify(mockSpec), 'utf8');
150
+ const utf16BomFile = path.join(specsSubdir, 'utf16le.json');
151
+ const utf16Buf = Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(JSON.stringify(mockSpec), 'utf16le')]);
152
+ fs.writeFileSync(utf16BomFile, utf16Buf);
153
+
154
+ assert.strictEqual(JSON.parse(decodeTextBuffer(fs.readFileSync(utf8BomFile))).name, 'Product Card');
155
+ assert.strictEqual(JSON.parse(decodeTextBuffer(fs.readFileSync(utf16BomFile))).name, 'Product Card');
156
+ const cliOut = execSync(`node "${path.join(__dirname, 'inspect_spec.js')}" "${utf16BomFile}" --markdown`, { encoding: 'utf8' });
157
+ assert.ok(cliOut.includes('Product Card'));
158
+ console.log(' ✓ decodeTextBuffer & CLI: parses UTF-8 BOM and UTF-16LE BOM specs cleanly');
144
159
  } finally {
145
160
  fs.rmSync(tmpDir, { recursive: true, force: true });
146
161
  }
147
162
 
148
- console.log('\nResults: 6/6 Spec Inspector tests passed.\n');
163
+ console.log('\nResults: 7/7 Spec Inspector tests passed.\n');
@@ -74,10 +74,16 @@ Choose one of the following terminal commands:
74
74
  ```bash
75
75
  export FIGMA_ACCESS_TOKEN="figd_your_token_here"
76
76
  ```
77
- - **Persist globally across all terminal sessions (`~/.zshrc`)**:
77
+ - **Persist globally on macOS / Linux (`~/.zshrc`)**:
78
78
  ```bash
79
79
  echo 'export FIGMA_ACCESS_TOKEN="figd_your_token_here"' >> ~/.zshrc && source ~/.zshrc
80
80
  ```
81
+ - **Persist globally on Windows (PowerShell)**:
82
+ ```powershell
83
+ [Environment]::SetEnvironmentVariable("FIGMA_ACCESS_TOKEN", "figd_your_token_here", "User")
84
+ # Or inside your PowerShell profile ($PROFILE):
85
+ # $env:FIGMA_ACCESS_TOKEN="figd_your_token_here"
86
+ ```
81
87
 
82
88
  *(Optional: If behind a corporate proxy with self-signed SSL certificates, run `echo 'FIGMA_IGNORE_SSL=true' >> .env`).*
83
89
 
@@ -88,7 +94,7 @@ Flags are **completely optional** — you do not have to specify any flags:
88
94
  - *"Inspect this frame and download a preview image"* (agent uses `--image` or `-i`).
89
95
  - *"Only extract the design tokens from this file"* (agent uses `--tokens`).
90
96
  - *"Collapse component sub-layers into props"* (agent uses `--shallow-instances`).
91
- - **Tip (Guarantee Autonomous Invocation)**: To ensure the agent never skips the extractor and never estimates CSS without extracting, add this suggestion to your project's `GEMINI.md` or `AGENTS.md`:
97
+ - **Tip (Guarantee Autonomous Invocation)**: To ensure the agent never skips the extractor and never estimates CSS without extracting, add this suggestion to your project's agent instructions file (e.g., `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.kiro/steering/`, or `.cursor/rules/`):
92
98
  ```markdown
93
99
  ## Figma UI Development
94
100
  Whenever a Figma link is provided or you are asked to build UI from Figma:
@@ -142,9 +148,9 @@ node figma-to-code/scripts/fetch_figma.js "<FigmaURL>" -i --shallow-instances
142
148
 
143
149
  The skill includes the following resources:
144
150
 
145
- - **Agent Instruction Guide (`SKILL.md`)**: The core skill definition read by AI agents (Antigravity, Claude Code, Cursor, Codex) containing workflow triggers and pair-programming guidelines.
151
+ - **Agent Instruction Guide (`SKILL.md`)**: The core skill definition read by AI agents (Claude Code, Kiro, Cursor, OpenCode, Codex, Antigravity) containing workflow triggers and pair-programming guidelines.
146
152
  - **Core Extraction Script (`scripts/fetch_figma.js`)**: The standalone, zero-dependency Node.js script that interfaces with the Figma REST API, resolves tokens, prunes the AST, and downloads preview images.
147
- - **Unit Test Suite (`scripts/test_fetch_figma.js`)**: A suite of 39 automated unit tests verifying URL parsing, token formatting, component prop cleaning, rate-limit gates, and node pruning without requiring network access.
153
+ - **Unit Test Suite (`scripts/test_fetch_figma.js`)**: Automated unit tests verifying environment/profile parsing, URL parsing, token formatting, component prop cleaning, rate-limit gates, and node pruning without requiring network access.
148
154
  - **Specification Directory (`.specs/`)**: The local workspace directory where downloaded image previews are stored for multimodal visual inspection.
149
155
  - **Reference Documentation (`README.md`)**: This guide explaining architecture, MCP comparison, and operational flags.
150
156
 
@@ -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 `/Applications` for browsers**: Do not search for installed browser binaries or run browser automation scripts.
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 `~/.figma_ratelimit.json`. All future calls to that file key are **immediately blocked with zero network traffic**, preventing Figma from extending the lockout penalty.
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**: `export FIGMA_ACCESS_TOKEN="figd_your_token_here"` or `export FIGMA_TOKEN="..."`
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 the skill's scripts directory:
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
- **Script Path Resolution**:
80
- - **Global Antigravity install**: `node ~/.gemini/config/skills/figma-to-code/scripts/fetch_figma.js "<FigmaURL>"`
81
- - **Global Claude Code install**: `node ~/.claude/skills/figma-to-code/scripts/fetch_figma.js "<FigmaURL>"`
82
- - **Project local install**: `node figma-to-code/scripts/fetch_figma.js "<FigmaURL>"`
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 figma-to-code/scripts/fetch_figma.js "https://www.figma.com/design/:fileKey/:name?node-id=4023-474"
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 figma-to-code/scripts/fetch_figma.js "https://www.figma.com/design/:fileKey/:name?node-id=4023-474" -i
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 figma-to-code/scripts/fetch_figma.js "https://www.figma.com/design/:fileKey/:name?node-id=4023-474" --shallow-instances
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 export `FIGMA_ACCESS_TOKEN=your_token` or update `.env`.
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 export it in their environment.
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.