@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;
50
161
  }
51
- if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) {
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
- 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
 
@@ -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 export FIGMA_ACCESS_TOKEN or add it to .env.';
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.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",