@mstrathman/figma 0.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/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +2734 -0
- package/dist/main.js.map +1 -0
- package/package.json +82 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,2734 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/api/client.ts
|
|
13
|
+
var client_exports = {};
|
|
14
|
+
__export(client_exports, {
|
|
15
|
+
FigmaApiError: () => FigmaApiError,
|
|
16
|
+
FigmaClient: () => FigmaClient,
|
|
17
|
+
FigmaNetworkError: () => FigmaNetworkError
|
|
18
|
+
});
|
|
19
|
+
var FIGMA_API_BASE, DEFAULT_TIMEOUT_MS, MAX_RETRIES, INITIAL_RETRY_DELAY_MS, FigmaApiError, FigmaNetworkError, FigmaClient;
|
|
20
|
+
var init_client = __esm({
|
|
21
|
+
"src/api/client.ts"() {
|
|
22
|
+
"use strict";
|
|
23
|
+
FIGMA_API_BASE = "https://api.figma.com/v1";
|
|
24
|
+
DEFAULT_TIMEOUT_MS = 3e4;
|
|
25
|
+
MAX_RETRIES = 3;
|
|
26
|
+
INITIAL_RETRY_DELAY_MS = 1e3;
|
|
27
|
+
FigmaApiError = class extends Error {
|
|
28
|
+
constructor(status, statusText, body) {
|
|
29
|
+
super(`Figma API error: ${status} ${statusText}`);
|
|
30
|
+
this.status = status;
|
|
31
|
+
this.statusText = statusText;
|
|
32
|
+
this.body = body;
|
|
33
|
+
this.name = "FigmaApiError";
|
|
34
|
+
}
|
|
35
|
+
isRateLimited() {
|
|
36
|
+
return this.status === 429;
|
|
37
|
+
}
|
|
38
|
+
isServerError() {
|
|
39
|
+
return this.status >= 500;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
FigmaNetworkError = class extends Error {
|
|
43
|
+
constructor(message, cause) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.cause = cause;
|
|
46
|
+
this.name = "FigmaNetworkError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
FigmaClient = class {
|
|
50
|
+
token;
|
|
51
|
+
baseUrl;
|
|
52
|
+
timeout;
|
|
53
|
+
maxRetries;
|
|
54
|
+
constructor(options) {
|
|
55
|
+
if (!options.token || options.token.trim() === "") {
|
|
56
|
+
throw new Error("Figma API token is required");
|
|
57
|
+
}
|
|
58
|
+
this.token = options.token;
|
|
59
|
+
this.baseUrl = options.baseUrl ?? FIGMA_API_BASE;
|
|
60
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
61
|
+
this.maxRetries = options.maxRetries ?? MAX_RETRIES;
|
|
62
|
+
}
|
|
63
|
+
async request(method, path5, options = {}, retryCount = 0) {
|
|
64
|
+
const baseWithSlash = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
65
|
+
const pathWithoutLeadingSlash = path5.startsWith("/") ? path5.slice(1) : path5;
|
|
66
|
+
const url = new URL(pathWithoutLeadingSlash, baseWithSlash);
|
|
67
|
+
if (options.params) {
|
|
68
|
+
for (const [key, value] of Object.entries(options.params)) {
|
|
69
|
+
if (value !== void 0) {
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
const filtered = value.filter((v) => v !== "");
|
|
72
|
+
if (filtered.length > 0) {
|
|
73
|
+
url.searchParams.set(key, filtered.join(","));
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
url.searchParams.set(key, String(value));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
83
|
+
let response;
|
|
84
|
+
try {
|
|
85
|
+
response = await fetch(url.toString(), {
|
|
86
|
+
method,
|
|
87
|
+
headers: {
|
|
88
|
+
"X-Figma-Token": this.token,
|
|
89
|
+
"Content-Type": "application/json"
|
|
90
|
+
},
|
|
91
|
+
body: options.body ? JSON.stringify(options.body) : void 0,
|
|
92
|
+
signal: controller.signal
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
clearTimeout(timeoutId);
|
|
96
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
97
|
+
throw new FigmaNetworkError(
|
|
98
|
+
`Request timed out after ${this.timeout}ms`,
|
|
99
|
+
error
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
throw new FigmaNetworkError(
|
|
103
|
+
`Network error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
104
|
+
error instanceof Error ? error : void 0
|
|
105
|
+
);
|
|
106
|
+
} finally {
|
|
107
|
+
clearTimeout(timeoutId);
|
|
108
|
+
}
|
|
109
|
+
if (response.status === 429 && retryCount < this.maxRetries) {
|
|
110
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
111
|
+
const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount);
|
|
112
|
+
await this.sleep(delayMs);
|
|
113
|
+
return this.request(method, path5, options, retryCount + 1);
|
|
114
|
+
}
|
|
115
|
+
if (response.status >= 500 && retryCount < this.maxRetries) {
|
|
116
|
+
const delayMs = INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount);
|
|
117
|
+
await this.sleep(delayMs);
|
|
118
|
+
return this.request(method, path5, options, retryCount + 1);
|
|
119
|
+
}
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
let body;
|
|
122
|
+
try {
|
|
123
|
+
body = await response.json();
|
|
124
|
+
} catch {
|
|
125
|
+
try {
|
|
126
|
+
body = await response.text();
|
|
127
|
+
} catch {
|
|
128
|
+
body = null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw new FigmaApiError(response.status, response.statusText, body);
|
|
132
|
+
}
|
|
133
|
+
return response.json();
|
|
134
|
+
}
|
|
135
|
+
sleep(ms) {
|
|
136
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
137
|
+
}
|
|
138
|
+
// User endpoints
|
|
139
|
+
async getMe() {
|
|
140
|
+
return this.request("GET", "/me");
|
|
141
|
+
}
|
|
142
|
+
// File endpoints
|
|
143
|
+
async getFile(fileKey, options) {
|
|
144
|
+
return this.request("GET", `/files/${fileKey}`, {
|
|
145
|
+
params: options
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async getFileNodes(fileKey, nodeIds, options) {
|
|
149
|
+
return this.request(
|
|
150
|
+
"GET",
|
|
151
|
+
`/files/${fileKey}/nodes`,
|
|
152
|
+
{
|
|
153
|
+
params: {
|
|
154
|
+
ids: nodeIds,
|
|
155
|
+
...options
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
async getImages(fileKey, nodeIds, options) {
|
|
161
|
+
return this.request("GET", `/images/${fileKey}`, {
|
|
162
|
+
params: {
|
|
163
|
+
ids: nodeIds,
|
|
164
|
+
...options
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async getImageFills(fileKey) {
|
|
169
|
+
return this.request(
|
|
170
|
+
"GET",
|
|
171
|
+
`/files/${fileKey}/images`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
// Project endpoints
|
|
175
|
+
async getTeamProjects(teamId) {
|
|
176
|
+
return this.request(
|
|
177
|
+
"GET",
|
|
178
|
+
`/teams/${teamId}/projects`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
async getProjectFiles(projectId, options) {
|
|
182
|
+
return this.request(
|
|
183
|
+
"GET",
|
|
184
|
+
`/projects/${projectId}/files`,
|
|
185
|
+
{ params: options }
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
// Component endpoints
|
|
189
|
+
async getTeamComponents(teamId, options) {
|
|
190
|
+
return this.request(
|
|
191
|
+
"GET",
|
|
192
|
+
`/teams/${teamId}/components`,
|
|
193
|
+
{ params: options }
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
async getFileComponents(fileKey) {
|
|
197
|
+
return this.request(
|
|
198
|
+
"GET",
|
|
199
|
+
`/files/${fileKey}/components`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
async getComponent(componentKey) {
|
|
203
|
+
return this.request(
|
|
204
|
+
"GET",
|
|
205
|
+
`/components/${componentKey}`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
async getTeamComponentSets(teamId, options) {
|
|
209
|
+
return this.request(
|
|
210
|
+
"GET",
|
|
211
|
+
`/teams/${teamId}/component_sets`,
|
|
212
|
+
{ params: options }
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
async getFileComponentSets(fileKey) {
|
|
216
|
+
return this.request(
|
|
217
|
+
"GET",
|
|
218
|
+
`/files/${fileKey}/component_sets`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
// Style endpoints
|
|
222
|
+
async getTeamStyles(teamId, options) {
|
|
223
|
+
return this.request(
|
|
224
|
+
"GET",
|
|
225
|
+
`/teams/${teamId}/styles`,
|
|
226
|
+
{ params: options }
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
async getFileStyles(fileKey) {
|
|
230
|
+
return this.request(
|
|
231
|
+
"GET",
|
|
232
|
+
`/files/${fileKey}/styles`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
async getStyle(styleKey) {
|
|
236
|
+
return this.request("GET", `/styles/${styleKey}`);
|
|
237
|
+
}
|
|
238
|
+
// Variable endpoints (Enterprise)
|
|
239
|
+
async getLocalVariables(fileKey) {
|
|
240
|
+
return this.request(
|
|
241
|
+
"GET",
|
|
242
|
+
`/files/${fileKey}/variables/local`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
async getPublishedVariables(fileKey) {
|
|
246
|
+
return this.request(
|
|
247
|
+
"GET",
|
|
248
|
+
`/files/${fileKey}/variables/published`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
// Comments endpoints
|
|
252
|
+
async getComments(fileKey) {
|
|
253
|
+
return this.request(
|
|
254
|
+
"GET",
|
|
255
|
+
`/files/${fileKey}/comments`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
async postComment(fileKey, message, options) {
|
|
259
|
+
return this.request(
|
|
260
|
+
"POST",
|
|
261
|
+
`/files/${fileKey}/comments`,
|
|
262
|
+
{
|
|
263
|
+
body: {
|
|
264
|
+
message,
|
|
265
|
+
...options
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
// Webhooks endpoints
|
|
271
|
+
async getTeamWebhooks(teamId) {
|
|
272
|
+
return this.request(
|
|
273
|
+
"GET",
|
|
274
|
+
`/webhooks/team/${teamId}`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
async getWebhook(webhookId) {
|
|
278
|
+
return this.request(
|
|
279
|
+
"GET",
|
|
280
|
+
`/webhooks/${webhookId}`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
// Generic API method for raw access
|
|
284
|
+
async api(method, path5, options) {
|
|
285
|
+
const normalizedPath = path5.startsWith("/") ? path5 : `/${path5}`;
|
|
286
|
+
return this.request(method.toUpperCase(), normalizedPath, options);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// src/internal/config/config.ts
|
|
293
|
+
import * as fs from "fs";
|
|
294
|
+
import * as path from "path";
|
|
295
|
+
import * as os from "os";
|
|
296
|
+
var CONFIG_DIR = path.join(os.homedir(), ".config", "figma");
|
|
297
|
+
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
298
|
+
var LOCAL_CONFIG_FILE = ".figmarc";
|
|
299
|
+
var ConfigManager = class {
|
|
300
|
+
config = {};
|
|
301
|
+
loaded = false;
|
|
302
|
+
constructor() {
|
|
303
|
+
}
|
|
304
|
+
load() {
|
|
305
|
+
if (this.loaded) {
|
|
306
|
+
return this.config;
|
|
307
|
+
}
|
|
308
|
+
this.config = {};
|
|
309
|
+
const userConfig = this.loadUserConfig();
|
|
310
|
+
this.config = { ...this.config, ...userConfig };
|
|
311
|
+
const localConfig = this.loadLocalConfig();
|
|
312
|
+
this.config = { ...this.config, ...localConfig };
|
|
313
|
+
this.applyEnvVars();
|
|
314
|
+
this.loaded = true;
|
|
315
|
+
return this.config;
|
|
316
|
+
}
|
|
317
|
+
loadUserConfig() {
|
|
318
|
+
try {
|
|
319
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
320
|
+
const content = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
321
|
+
return JSON.parse(content);
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
return {};
|
|
326
|
+
}
|
|
327
|
+
loadLocalConfig() {
|
|
328
|
+
try {
|
|
329
|
+
if (fs.existsSync(LOCAL_CONFIG_FILE)) {
|
|
330
|
+
const content = fs.readFileSync(LOCAL_CONFIG_FILE, "utf-8");
|
|
331
|
+
return JSON.parse(content);
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
}
|
|
335
|
+
return {};
|
|
336
|
+
}
|
|
337
|
+
applyEnvVars() {
|
|
338
|
+
if (process.env.FIGMA_TOKEN) {
|
|
339
|
+
this.config.token = process.env.FIGMA_TOKEN;
|
|
340
|
+
}
|
|
341
|
+
if (process.env.FIGMA_TEAM_ID) {
|
|
342
|
+
this.config.teamId = process.env.FIGMA_TEAM_ID;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
get(key) {
|
|
346
|
+
if (!this.loaded) {
|
|
347
|
+
this.load();
|
|
348
|
+
}
|
|
349
|
+
return this.config[key];
|
|
350
|
+
}
|
|
351
|
+
set(key, value) {
|
|
352
|
+
if (!this.loaded) {
|
|
353
|
+
this.load();
|
|
354
|
+
}
|
|
355
|
+
this.config[key] = value;
|
|
356
|
+
this.save();
|
|
357
|
+
}
|
|
358
|
+
getAll() {
|
|
359
|
+
if (!this.loaded) {
|
|
360
|
+
this.load();
|
|
361
|
+
}
|
|
362
|
+
return { ...this.config };
|
|
363
|
+
}
|
|
364
|
+
save() {
|
|
365
|
+
try {
|
|
366
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
367
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
368
|
+
}
|
|
369
|
+
const { token: _token, ...safeConfig } = this.config;
|
|
370
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(safeConfig, null, 2));
|
|
371
|
+
} catch {
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
getConfigDir() {
|
|
375
|
+
return CONFIG_DIR;
|
|
376
|
+
}
|
|
377
|
+
getConfigFile() {
|
|
378
|
+
return CONFIG_FILE;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// src/internal/auth/keyring.ts
|
|
383
|
+
import * as fs2 from "fs";
|
|
384
|
+
import * as path2 from "path";
|
|
385
|
+
import * as os2 from "os";
|
|
386
|
+
var SERVICE_NAME = "figma";
|
|
387
|
+
var ACCOUNT_NAME = "default";
|
|
388
|
+
var CREDENTIALS_DIR = path2.join(os2.homedir(), ".config", "figma");
|
|
389
|
+
var CREDENTIALS_FILE = path2.join(CREDENTIALS_DIR, "credentials.json");
|
|
390
|
+
var KeytarAuthStore = class {
|
|
391
|
+
keytar = null;
|
|
392
|
+
async getKeytar() {
|
|
393
|
+
if (this.keytar) {
|
|
394
|
+
return this.keytar;
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
this.keytar = await import("keytar");
|
|
398
|
+
return this.keytar;
|
|
399
|
+
} catch {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async getToken() {
|
|
404
|
+
const keytar = await this.getKeytar();
|
|
405
|
+
if (!keytar) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
return await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
410
|
+
} catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async setToken(token) {
|
|
415
|
+
const keytar = await this.getKeytar();
|
|
416
|
+
if (!keytar) {
|
|
417
|
+
throw new Error("Keytar not available");
|
|
418
|
+
}
|
|
419
|
+
await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, token);
|
|
420
|
+
}
|
|
421
|
+
async deleteToken() {
|
|
422
|
+
const keytar = await this.getKeytar();
|
|
423
|
+
if (!keytar) {
|
|
424
|
+
throw new Error("Keytar not available");
|
|
425
|
+
}
|
|
426
|
+
await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
427
|
+
}
|
|
428
|
+
async isAvailable() {
|
|
429
|
+
const keytar = await this.getKeytar();
|
|
430
|
+
return keytar !== null;
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
var FileAuthStore = class {
|
|
434
|
+
async getToken() {
|
|
435
|
+
try {
|
|
436
|
+
if (!fs2.existsSync(CREDENTIALS_FILE)) {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
const content = fs2.readFileSync(CREDENTIALS_FILE, "utf-8");
|
|
440
|
+
const credentials = JSON.parse(content);
|
|
441
|
+
return credentials.token ?? null;
|
|
442
|
+
} catch {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async setToken(token) {
|
|
447
|
+
if (!fs2.existsSync(CREDENTIALS_DIR)) {
|
|
448
|
+
fs2.mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 448 });
|
|
449
|
+
}
|
|
450
|
+
let credentials = {};
|
|
451
|
+
try {
|
|
452
|
+
if (fs2.existsSync(CREDENTIALS_FILE)) {
|
|
453
|
+
const content = fs2.readFileSync(CREDENTIALS_FILE, "utf-8");
|
|
454
|
+
credentials = JSON.parse(content);
|
|
455
|
+
}
|
|
456
|
+
} catch {
|
|
457
|
+
}
|
|
458
|
+
credentials.token = token;
|
|
459
|
+
fs2.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), {
|
|
460
|
+
mode: 384
|
|
461
|
+
// Only owner can read/write
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
async deleteToken() {
|
|
465
|
+
try {
|
|
466
|
+
if (fs2.existsSync(CREDENTIALS_FILE)) {
|
|
467
|
+
const content = fs2.readFileSync(CREDENTIALS_FILE, "utf-8");
|
|
468
|
+
const credentials = JSON.parse(content);
|
|
469
|
+
delete credentials.token;
|
|
470
|
+
if (Object.keys(credentials).length === 0) {
|
|
471
|
+
fs2.unlinkSync(CREDENTIALS_FILE);
|
|
472
|
+
} else {
|
|
473
|
+
fs2.writeFileSync(
|
|
474
|
+
CREDENTIALS_FILE,
|
|
475
|
+
JSON.stringify(credentials, null, 2),
|
|
476
|
+
{ mode: 384 }
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
} catch {
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
var AuthManager = class {
|
|
485
|
+
keytarStore;
|
|
486
|
+
fileStore;
|
|
487
|
+
useKeytar = null;
|
|
488
|
+
onFallback = null;
|
|
489
|
+
fallbackWarningShown = false;
|
|
490
|
+
constructor(onFallback) {
|
|
491
|
+
this.keytarStore = new KeytarAuthStore();
|
|
492
|
+
this.fileStore = new FileAuthStore();
|
|
493
|
+
this.onFallback = onFallback ?? null;
|
|
494
|
+
}
|
|
495
|
+
async shouldUseKeytar() {
|
|
496
|
+
if (this.useKeytar !== null) {
|
|
497
|
+
return this.useKeytar;
|
|
498
|
+
}
|
|
499
|
+
this.useKeytar = await this.keytarStore.isAvailable();
|
|
500
|
+
return this.useKeytar;
|
|
501
|
+
}
|
|
502
|
+
notifyFallback(reason) {
|
|
503
|
+
if (this.onFallback && !this.fallbackWarningShown) {
|
|
504
|
+
this.fallbackWarningShown = true;
|
|
505
|
+
this.onFallback(
|
|
506
|
+
`Warning: Using file-based credential storage (${reason}). Token stored in ${CREDENTIALS_FILE}`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async getToken() {
|
|
511
|
+
if (await this.shouldUseKeytar()) {
|
|
512
|
+
const token = await this.keytarStore.getToken();
|
|
513
|
+
if (token) {
|
|
514
|
+
return token;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return this.fileStore.getToken();
|
|
518
|
+
}
|
|
519
|
+
async setToken(token) {
|
|
520
|
+
if (await this.shouldUseKeytar()) {
|
|
521
|
+
try {
|
|
522
|
+
await this.keytarStore.setToken(token);
|
|
523
|
+
return;
|
|
524
|
+
} catch (error) {
|
|
525
|
+
const reason = error instanceof Error ? error.message : "keytar failed";
|
|
526
|
+
this.notifyFallback(reason);
|
|
527
|
+
}
|
|
528
|
+
} else {
|
|
529
|
+
this.notifyFallback("system keyring not available");
|
|
530
|
+
}
|
|
531
|
+
await this.fileStore.setToken(token);
|
|
532
|
+
}
|
|
533
|
+
async deleteToken() {
|
|
534
|
+
const useKeytar = await this.shouldUseKeytar();
|
|
535
|
+
if (useKeytar) {
|
|
536
|
+
try {
|
|
537
|
+
await this.keytarStore.deleteToken();
|
|
538
|
+
} catch {
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
await this.fileStore.deleteToken();
|
|
542
|
+
}
|
|
543
|
+
async getStorageType() {
|
|
544
|
+
if (await this.shouldUseKeytar()) {
|
|
545
|
+
const token = await this.keytarStore.getToken();
|
|
546
|
+
if (token) {
|
|
547
|
+
return "keyring";
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return "file";
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// src/internal/auth/oauth.ts
|
|
555
|
+
import * as http from "http";
|
|
556
|
+
import * as crypto from "crypto";
|
|
557
|
+
import { URL as URL2 } from "url";
|
|
558
|
+
var FIGMA_OAUTH_URL = "https://www.figma.com/oauth";
|
|
559
|
+
var FIGMA_TOKEN_URL = "https://www.figma.com/api/oauth/token";
|
|
560
|
+
var MAX_PORT = 65535;
|
|
561
|
+
function generateState() {
|
|
562
|
+
return crypto.randomBytes(16).toString("hex");
|
|
563
|
+
}
|
|
564
|
+
function verifyState(received, expected) {
|
|
565
|
+
if (received.length !== expected.length) {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
return crypto.timingSafeEqual(
|
|
570
|
+
Buffer.from(received, "utf8"),
|
|
571
|
+
Buffer.from(expected, "utf8")
|
|
572
|
+
);
|
|
573
|
+
} catch {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
async function findAvailablePort(startPort, maxPort = MAX_PORT) {
|
|
578
|
+
if (startPort > maxPort) {
|
|
579
|
+
throw new Error(`No available ports found between 8585 and ${maxPort}`);
|
|
580
|
+
}
|
|
581
|
+
return new Promise((resolve, reject) => {
|
|
582
|
+
const server = http.createServer();
|
|
583
|
+
server.listen(startPort, "127.0.0.1", () => {
|
|
584
|
+
const address = server.address();
|
|
585
|
+
if (address && typeof address === "object") {
|
|
586
|
+
const port = address.port;
|
|
587
|
+
server.close(() => resolve(port));
|
|
588
|
+
} else {
|
|
589
|
+
reject(new Error("Could not get server address"));
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
server.on("error", () => {
|
|
593
|
+
findAvailablePort(startPort + 1, maxPort).then(resolve).catch(reject);
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
async function startOAuthFlow(config, onOpenUrl) {
|
|
598
|
+
const port = await findAvailablePort(8585);
|
|
599
|
+
const redirectUri = config.redirectUri || `http://127.0.0.1:${port}/callback`;
|
|
600
|
+
const state = generateState();
|
|
601
|
+
const scopes = config.scopes || ["files:read"];
|
|
602
|
+
return new Promise((resolve, reject) => {
|
|
603
|
+
let serverClosed = false;
|
|
604
|
+
const closeServer = () => {
|
|
605
|
+
if (!serverClosed) {
|
|
606
|
+
serverClosed = true;
|
|
607
|
+
server.close();
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
const server = http.createServer(async (req, res) => {
|
|
611
|
+
const url = new URL2(req.url || "/", `http://127.0.0.1:${port}`);
|
|
612
|
+
if (url.pathname === "/callback") {
|
|
613
|
+
const code = url.searchParams.get("code");
|
|
614
|
+
const returnedState = url.searchParams.get("state");
|
|
615
|
+
const error = url.searchParams.get("error");
|
|
616
|
+
if (error) {
|
|
617
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
618
|
+
res.end(getErrorHtml(error));
|
|
619
|
+
closeServer();
|
|
620
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (!returnedState || !verifyState(returnedState, state)) {
|
|
624
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
625
|
+
res.end(getErrorHtml("State mismatch - possible CSRF attack"));
|
|
626
|
+
closeServer();
|
|
627
|
+
reject(new Error("OAuth state mismatch"));
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (!code) {
|
|
631
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
632
|
+
res.end(getErrorHtml("No authorization code received"));
|
|
633
|
+
closeServer();
|
|
634
|
+
reject(new Error("No authorization code"));
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
const tokenResponse = await fetch(FIGMA_TOKEN_URL, {
|
|
639
|
+
method: "POST",
|
|
640
|
+
headers: {
|
|
641
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
642
|
+
},
|
|
643
|
+
body: new URLSearchParams({
|
|
644
|
+
client_id: config.clientId,
|
|
645
|
+
client_secret: config.clientSecret,
|
|
646
|
+
redirect_uri: redirectUri,
|
|
647
|
+
code,
|
|
648
|
+
grant_type: "authorization_code"
|
|
649
|
+
})
|
|
650
|
+
});
|
|
651
|
+
if (!tokenResponse.ok) {
|
|
652
|
+
const errorText = await tokenResponse.text();
|
|
653
|
+
throw new Error(`Token exchange failed: ${errorText}`);
|
|
654
|
+
}
|
|
655
|
+
const tokenData = await tokenResponse.json();
|
|
656
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
657
|
+
res.end(getSuccessHtml());
|
|
658
|
+
closeServer();
|
|
659
|
+
resolve({
|
|
660
|
+
accessToken: tokenData.access_token,
|
|
661
|
+
refreshToken: tokenData.refresh_token,
|
|
662
|
+
expiresIn: tokenData.expires_in,
|
|
663
|
+
userId: tokenData.user_id?.toString()
|
|
664
|
+
});
|
|
665
|
+
} catch (err) {
|
|
666
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
667
|
+
res.end(
|
|
668
|
+
getErrorHtml(err instanceof Error ? err.message : "Unknown error")
|
|
669
|
+
);
|
|
670
|
+
closeServer();
|
|
671
|
+
reject(err);
|
|
672
|
+
}
|
|
673
|
+
} else {
|
|
674
|
+
res.writeHead(404);
|
|
675
|
+
res.end("Not found");
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
server.listen(port, "127.0.0.1", () => {
|
|
679
|
+
const authUrl = new URL2(FIGMA_OAUTH_URL);
|
|
680
|
+
authUrl.searchParams.set("client_id", config.clientId);
|
|
681
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
682
|
+
authUrl.searchParams.set("scope", scopes.join(","));
|
|
683
|
+
authUrl.searchParams.set("state", state);
|
|
684
|
+
authUrl.searchParams.set("response_type", "code");
|
|
685
|
+
onOpenUrl(authUrl.toString());
|
|
686
|
+
});
|
|
687
|
+
setTimeout(
|
|
688
|
+
() => {
|
|
689
|
+
closeServer();
|
|
690
|
+
reject(new Error("OAuth flow timed out"));
|
|
691
|
+
},
|
|
692
|
+
5 * 60 * 1e3
|
|
693
|
+
);
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
function getSuccessHtml() {
|
|
697
|
+
return `<!DOCTYPE html>
|
|
698
|
+
<html>
|
|
699
|
+
<head>
|
|
700
|
+
<title>Figma CLI - Authentication Successful</title>
|
|
701
|
+
<style>
|
|
702
|
+
body {
|
|
703
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
704
|
+
display: flex;
|
|
705
|
+
justify-content: center;
|
|
706
|
+
align-items: center;
|
|
707
|
+
min-height: 100vh;
|
|
708
|
+
margin: 0;
|
|
709
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
710
|
+
}
|
|
711
|
+
.card {
|
|
712
|
+
background: white;
|
|
713
|
+
padding: 3rem;
|
|
714
|
+
border-radius: 16px;
|
|
715
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
716
|
+
text-align: center;
|
|
717
|
+
max-width: 400px;
|
|
718
|
+
}
|
|
719
|
+
.checkmark {
|
|
720
|
+
width: 80px;
|
|
721
|
+
height: 80px;
|
|
722
|
+
background: #10b981;
|
|
723
|
+
border-radius: 50%;
|
|
724
|
+
display: flex;
|
|
725
|
+
align-items: center;
|
|
726
|
+
justify-content: center;
|
|
727
|
+
margin: 0 auto 1.5rem;
|
|
728
|
+
}
|
|
729
|
+
.checkmark svg {
|
|
730
|
+
width: 40px;
|
|
731
|
+
height: 40px;
|
|
732
|
+
stroke: white;
|
|
733
|
+
stroke-width: 3;
|
|
734
|
+
}
|
|
735
|
+
h1 { color: #1f2937; margin: 0 0 0.5rem; }
|
|
736
|
+
p { color: #6b7280; margin: 0; }
|
|
737
|
+
</style>
|
|
738
|
+
</head>
|
|
739
|
+
<body>
|
|
740
|
+
<div class="card">
|
|
741
|
+
<div class="checkmark">
|
|
742
|
+
<svg viewBox="0 0 24 24" fill="none">
|
|
743
|
+
<path d="M5 13l4 4L19 7" stroke-linecap="round" stroke-linejoin="round"/>
|
|
744
|
+
</svg>
|
|
745
|
+
</div>
|
|
746
|
+
<h1>Authentication Successful!</h1>
|
|
747
|
+
<p>You can close this window and return to your terminal.</p>
|
|
748
|
+
</div>
|
|
749
|
+
</body>
|
|
750
|
+
</html>`;
|
|
751
|
+
}
|
|
752
|
+
function getErrorHtml(error) {
|
|
753
|
+
return `<!DOCTYPE html>
|
|
754
|
+
<html>
|
|
755
|
+
<head>
|
|
756
|
+
<title>Figma CLI - Authentication Failed</title>
|
|
757
|
+
<style>
|
|
758
|
+
body {
|
|
759
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
760
|
+
display: flex;
|
|
761
|
+
justify-content: center;
|
|
762
|
+
align-items: center;
|
|
763
|
+
min-height: 100vh;
|
|
764
|
+
margin: 0;
|
|
765
|
+
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
766
|
+
}
|
|
767
|
+
.card {
|
|
768
|
+
background: white;
|
|
769
|
+
padding: 3rem;
|
|
770
|
+
border-radius: 16px;
|
|
771
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
772
|
+
text-align: center;
|
|
773
|
+
max-width: 400px;
|
|
774
|
+
}
|
|
775
|
+
.error-icon {
|
|
776
|
+
width: 80px;
|
|
777
|
+
height: 80px;
|
|
778
|
+
background: #ef4444;
|
|
779
|
+
border-radius: 50%;
|
|
780
|
+
display: flex;
|
|
781
|
+
align-items: center;
|
|
782
|
+
justify-content: center;
|
|
783
|
+
margin: 0 auto 1.5rem;
|
|
784
|
+
}
|
|
785
|
+
.error-icon svg {
|
|
786
|
+
width: 40px;
|
|
787
|
+
height: 40px;
|
|
788
|
+
stroke: white;
|
|
789
|
+
stroke-width: 3;
|
|
790
|
+
}
|
|
791
|
+
h1 { color: #1f2937; margin: 0 0 0.5rem; }
|
|
792
|
+
p { color: #6b7280; margin: 0; }
|
|
793
|
+
.error-detail {
|
|
794
|
+
background: #fef2f2;
|
|
795
|
+
color: #991b1b;
|
|
796
|
+
padding: 1rem;
|
|
797
|
+
border-radius: 8px;
|
|
798
|
+
margin-top: 1rem;
|
|
799
|
+
font-size: 0.875rem;
|
|
800
|
+
}
|
|
801
|
+
</style>
|
|
802
|
+
</head>
|
|
803
|
+
<body>
|
|
804
|
+
<div class="card">
|
|
805
|
+
<div class="error-icon">
|
|
806
|
+
<svg viewBox="0 0 24 24" fill="none">
|
|
807
|
+
<path d="M6 18L18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/>
|
|
808
|
+
</svg>
|
|
809
|
+
</div>
|
|
810
|
+
<h1>Authentication Failed</h1>
|
|
811
|
+
<p>Something went wrong during authentication.</p>
|
|
812
|
+
<div class="error-detail">${escapeHtml(error)}</div>
|
|
813
|
+
</div>
|
|
814
|
+
</body>
|
|
815
|
+
</html>`;
|
|
816
|
+
}
|
|
817
|
+
function escapeHtml(str) {
|
|
818
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// src/pkg/cmdutil/factory.ts
|
|
822
|
+
init_client();
|
|
823
|
+
|
|
824
|
+
// src/pkg/iostreams/iostreams.ts
|
|
825
|
+
function createIOStreams() {
|
|
826
|
+
const isTTY = process.stdout.isTTY ?? false;
|
|
827
|
+
return {
|
|
828
|
+
in: process.stdin,
|
|
829
|
+
out: process.stdout,
|
|
830
|
+
err: process.stderr,
|
|
831
|
+
isInteractive: isTTY && process.env.CI !== "true",
|
|
832
|
+
isTTY
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/pkg/cmdutil/factory.ts
|
|
837
|
+
import chalk from "chalk";
|
|
838
|
+
function createFactory() {
|
|
839
|
+
const config = new ConfigManager();
|
|
840
|
+
const io = createIOStreams();
|
|
841
|
+
const auth = new AuthManager((message) => {
|
|
842
|
+
io.err.write(chalk.yellow(`${message}
|
|
843
|
+
`));
|
|
844
|
+
});
|
|
845
|
+
let clientCache = null;
|
|
846
|
+
return {
|
|
847
|
+
config,
|
|
848
|
+
auth,
|
|
849
|
+
io,
|
|
850
|
+
async getToken(tokenOption) {
|
|
851
|
+
if (tokenOption) {
|
|
852
|
+
return tokenOption;
|
|
853
|
+
}
|
|
854
|
+
config.load();
|
|
855
|
+
const envToken = config.get("token");
|
|
856
|
+
if (envToken) {
|
|
857
|
+
return envToken;
|
|
858
|
+
}
|
|
859
|
+
const storedToken = await auth.getToken();
|
|
860
|
+
if (storedToken) {
|
|
861
|
+
return storedToken;
|
|
862
|
+
}
|
|
863
|
+
throw new Error(
|
|
864
|
+
`No Figma token found. Please run ${chalk.cyan("figma auth login")} or set the ${chalk.cyan("FIGMA_TOKEN")} environment variable.`
|
|
865
|
+
);
|
|
866
|
+
},
|
|
867
|
+
async getClient() {
|
|
868
|
+
if (clientCache) {
|
|
869
|
+
return clientCache;
|
|
870
|
+
}
|
|
871
|
+
const token = await this.getToken();
|
|
872
|
+
clientCache = new FigmaClient({ token });
|
|
873
|
+
return clientCache;
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/cmd/root.ts
|
|
879
|
+
import { Command as Command26 } from "commander";
|
|
880
|
+
|
|
881
|
+
// src/cmd/auth/index.ts
|
|
882
|
+
import { Command as Command4 } from "commander";
|
|
883
|
+
|
|
884
|
+
// src/cmd/auth/login.ts
|
|
885
|
+
init_client();
|
|
886
|
+
import { Command } from "commander";
|
|
887
|
+
import chalk2 from "chalk";
|
|
888
|
+
import { spawn } from "child_process";
|
|
889
|
+
import { platform } from "os";
|
|
890
|
+
function openBrowser(url) {
|
|
891
|
+
const plat = platform();
|
|
892
|
+
if (plat === "darwin") {
|
|
893
|
+
spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
894
|
+
} else if (plat === "win32") {
|
|
895
|
+
spawn("cmd", ["/c", "start", "", url], {
|
|
896
|
+
detached: true,
|
|
897
|
+
stdio: "ignore"
|
|
898
|
+
}).unref();
|
|
899
|
+
} else {
|
|
900
|
+
spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function displayUserInfo(io, user) {
|
|
904
|
+
io.out.write(chalk2.green("\u2713 Logged in as "));
|
|
905
|
+
io.out.write(chalk2.bold(user.handle || user.email));
|
|
906
|
+
io.out.write("\n");
|
|
907
|
+
}
|
|
908
|
+
async function performOAuthFlow(io, auth, clientId, clientSecret, scopes) {
|
|
909
|
+
io.out.write(chalk2.cyan("Opening browser for authentication...\n"));
|
|
910
|
+
io.out.write(chalk2.dim("Waiting for authorization...\n\n"));
|
|
911
|
+
const result = await startOAuthFlow(
|
|
912
|
+
{ clientId, clientSecret, scopes },
|
|
913
|
+
(url) => {
|
|
914
|
+
io.out.write(chalk2.dim(`If browser doesn't open, visit:
|
|
915
|
+
${url}
|
|
916
|
+
|
|
917
|
+
`));
|
|
918
|
+
openBrowser(url);
|
|
919
|
+
}
|
|
920
|
+
);
|
|
921
|
+
await auth.setToken(result.accessToken);
|
|
922
|
+
const client = new FigmaClient({ token: result.accessToken });
|
|
923
|
+
const user = await client.getMe();
|
|
924
|
+
displayUserInfo(io, user);
|
|
925
|
+
}
|
|
926
|
+
function createLoginCommand(factory) {
|
|
927
|
+
const cmd = new Command("login").description("Authenticate with Figma").option("-t, --token <token>", "Personal access token").option("-w, --web", "Login via browser (OAuth)").option("--client-id <id>", "OAuth client ID (or set FIGMA_CLIENT_ID)").option("--scopes <scopes>", "OAuth scopes (comma-separated)", "files:read").action(
|
|
928
|
+
async (options) => {
|
|
929
|
+
const { io, auth } = factory;
|
|
930
|
+
if (options.web) {
|
|
931
|
+
const clientId = options.clientId || process.env.FIGMA_CLIENT_ID;
|
|
932
|
+
const clientSecret = process.env.FIGMA_CLIENT_SECRET;
|
|
933
|
+
if (!clientId || !clientSecret) {
|
|
934
|
+
io.err.write(
|
|
935
|
+
chalk2.red("Error: OAuth requires client ID and secret.\n\n")
|
|
936
|
+
);
|
|
937
|
+
io.err.write(chalk2.dim("Set them via environment variables:\n"));
|
|
938
|
+
io.err.write(
|
|
939
|
+
chalk2.dim(" export FIGMA_CLIENT_ID=your_client_id\n")
|
|
940
|
+
);
|
|
941
|
+
io.err.write(
|
|
942
|
+
chalk2.dim(" export FIGMA_CLIENT_SECRET=your_client_secret\n\n")
|
|
943
|
+
);
|
|
944
|
+
io.err.write(
|
|
945
|
+
chalk2.dim("You can also pass --client-id as a flag.\n")
|
|
946
|
+
);
|
|
947
|
+
io.err.write(
|
|
948
|
+
chalk2.yellow(
|
|
949
|
+
"\nNote: Client secret must be set via FIGMA_CLIENT_SECRET env var for security.\n"
|
|
950
|
+
)
|
|
951
|
+
);
|
|
952
|
+
io.err.write(
|
|
953
|
+
chalk2.dim(
|
|
954
|
+
"\nCreate an OAuth app at: https://www.figma.com/developers/apps\n"
|
|
955
|
+
)
|
|
956
|
+
);
|
|
957
|
+
process.exit(1);
|
|
958
|
+
}
|
|
959
|
+
try {
|
|
960
|
+
await performOAuthFlow(
|
|
961
|
+
io,
|
|
962
|
+
auth,
|
|
963
|
+
clientId,
|
|
964
|
+
clientSecret,
|
|
965
|
+
options.scopes?.split(",")
|
|
966
|
+
);
|
|
967
|
+
} catch (error) {
|
|
968
|
+
io.err.write(chalk2.red("Error: OAuth authentication failed.\n"));
|
|
969
|
+
if (error instanceof Error) {
|
|
970
|
+
io.err.write(chalk2.dim(`${error.message}
|
|
971
|
+
`));
|
|
972
|
+
}
|
|
973
|
+
process.exit(1);
|
|
974
|
+
}
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
let token = options.token;
|
|
978
|
+
if (!token) {
|
|
979
|
+
if (!io.isInteractive) {
|
|
980
|
+
io.err.write(
|
|
981
|
+
chalk2.red(
|
|
982
|
+
"Error: No token provided. Use --token, --web, or run interactively.\n"
|
|
983
|
+
)
|
|
984
|
+
);
|
|
985
|
+
io.err.write(
|
|
986
|
+
chalk2.dim(
|
|
987
|
+
"\nGet a personal access token from: https://www.figma.com/developers/api#access-tokens\n"
|
|
988
|
+
)
|
|
989
|
+
);
|
|
990
|
+
process.exit(1);
|
|
991
|
+
}
|
|
992
|
+
const { default: Enquirer } = await import("enquirer");
|
|
993
|
+
const enquirer = new Enquirer();
|
|
994
|
+
const methodResponse = await enquirer.prompt({
|
|
995
|
+
type: "select",
|
|
996
|
+
name: "method",
|
|
997
|
+
message: "How would you like to authenticate?",
|
|
998
|
+
choices: [
|
|
999
|
+
{ name: "token", message: "Paste a personal access token" },
|
|
1000
|
+
{
|
|
1001
|
+
name: "web",
|
|
1002
|
+
message: "Login with browser (requires OAuth app)"
|
|
1003
|
+
}
|
|
1004
|
+
]
|
|
1005
|
+
});
|
|
1006
|
+
if (methodResponse.method === "web") {
|
|
1007
|
+
const clientIdResponse = await enquirer.prompt({
|
|
1008
|
+
type: "input",
|
|
1009
|
+
name: "clientId",
|
|
1010
|
+
message: "OAuth Client ID:",
|
|
1011
|
+
initial: process.env.FIGMA_CLIENT_ID
|
|
1012
|
+
});
|
|
1013
|
+
const clientSecretResponse = await enquirer.prompt({
|
|
1014
|
+
type: "password",
|
|
1015
|
+
name: "clientSecret",
|
|
1016
|
+
message: "OAuth Client Secret:"
|
|
1017
|
+
});
|
|
1018
|
+
if (!clientIdResponse.clientId || !clientSecretResponse.clientSecret) {
|
|
1019
|
+
io.err.write(
|
|
1020
|
+
chalk2.red("Error: Client ID and secret are required.\n")
|
|
1021
|
+
);
|
|
1022
|
+
process.exit(1);
|
|
1023
|
+
}
|
|
1024
|
+
try {
|
|
1025
|
+
await performOAuthFlow(
|
|
1026
|
+
io,
|
|
1027
|
+
auth,
|
|
1028
|
+
clientIdResponse.clientId,
|
|
1029
|
+
clientSecretResponse.clientSecret
|
|
1030
|
+
);
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
io.err.write(chalk2.red("Error: OAuth authentication failed.\n"));
|
|
1033
|
+
if (error instanceof Error) {
|
|
1034
|
+
io.err.write(chalk2.dim(`${error.message}
|
|
1035
|
+
`));
|
|
1036
|
+
}
|
|
1037
|
+
process.exit(1);
|
|
1038
|
+
}
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
io.out.write(
|
|
1042
|
+
chalk2.cyan(
|
|
1043
|
+
"\nGet a personal access token from: https://www.figma.com/developers/api#access-tokens\n\n"
|
|
1044
|
+
)
|
|
1045
|
+
);
|
|
1046
|
+
const tokenResponse = await enquirer.prompt({
|
|
1047
|
+
type: "password",
|
|
1048
|
+
name: "token",
|
|
1049
|
+
message: "Paste your token:"
|
|
1050
|
+
});
|
|
1051
|
+
token = tokenResponse.token;
|
|
1052
|
+
}
|
|
1053
|
+
if (!token || token.trim() === "") {
|
|
1054
|
+
io.err.write(chalk2.red("Error: Token cannot be empty.\n"));
|
|
1055
|
+
process.exit(1);
|
|
1056
|
+
}
|
|
1057
|
+
io.out.write(chalk2.dim("Verifying token...\n"));
|
|
1058
|
+
try {
|
|
1059
|
+
const client = new FigmaClient({ token });
|
|
1060
|
+
const user = await client.getMe();
|
|
1061
|
+
await auth.setToken(token);
|
|
1062
|
+
displayUserInfo(io, user);
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
io.err.write(chalk2.red("Error: Invalid token or API error.\n"));
|
|
1065
|
+
if (error instanceof Error) {
|
|
1066
|
+
io.err.write(chalk2.dim(`${error.message}
|
|
1067
|
+
`));
|
|
1068
|
+
}
|
|
1069
|
+
process.exit(1);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
);
|
|
1073
|
+
return cmd;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// src/cmd/auth/logout.ts
|
|
1077
|
+
import { Command as Command2 } from "commander";
|
|
1078
|
+
import chalk3 from "chalk";
|
|
1079
|
+
function createLogoutCommand(factory) {
|
|
1080
|
+
const cmd = new Command2("logout").description("Remove stored Figma credentials").action(async () => {
|
|
1081
|
+
const { io, auth } = factory;
|
|
1082
|
+
const existingToken = await auth.getToken();
|
|
1083
|
+
if (!existingToken) {
|
|
1084
|
+
io.out.write(chalk3.yellow("No stored credentials found.\n"));
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
await auth.deleteToken();
|
|
1088
|
+
io.out.write(chalk3.green("\u2713 Logged out successfully.\n"));
|
|
1089
|
+
});
|
|
1090
|
+
return cmd;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/cmd/auth/status.ts
|
|
1094
|
+
init_client();
|
|
1095
|
+
import { Command as Command3 } from "commander";
|
|
1096
|
+
import chalk4 from "chalk";
|
|
1097
|
+
function createStatusCommand(factory) {
|
|
1098
|
+
const cmd = new Command3("status").description("Show authentication status").action(async () => {
|
|
1099
|
+
const { io, auth, config } = factory;
|
|
1100
|
+
config.load();
|
|
1101
|
+
const envToken = config.get("token");
|
|
1102
|
+
const storedToken = await auth.getToken();
|
|
1103
|
+
const token = envToken || storedToken;
|
|
1104
|
+
if (!token) {
|
|
1105
|
+
io.out.write(chalk4.yellow("Not logged in.\n"));
|
|
1106
|
+
io.out.write(
|
|
1107
|
+
chalk4.dim(`Run ${chalk4.cyan("figma auth login")} to authenticate.
|
|
1108
|
+
`)
|
|
1109
|
+
);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const source = envToken ? "environment variable" : "stored credential";
|
|
1113
|
+
try {
|
|
1114
|
+
const client = new FigmaClient({ token });
|
|
1115
|
+
const user = await client.getMe();
|
|
1116
|
+
io.out.write(chalk4.green("\u2713 Logged in to Figma\n\n"));
|
|
1117
|
+
io.out.write(
|
|
1118
|
+
` ${chalk4.dim("User:")} ${user.handle || user.email}
|
|
1119
|
+
`
|
|
1120
|
+
);
|
|
1121
|
+
io.out.write(` ${chalk4.dim("Email:")} ${user.email}
|
|
1122
|
+
`);
|
|
1123
|
+
io.out.write(` ${chalk4.dim("User ID:")} ${user.id}
|
|
1124
|
+
`);
|
|
1125
|
+
io.out.write(` ${chalk4.dim("Source:")} ${source}
|
|
1126
|
+
`);
|
|
1127
|
+
if (envToken) {
|
|
1128
|
+
io.out.write(
|
|
1129
|
+
chalk4.dim("\n Token from FIGMA_TOKEN environment variable.\n")
|
|
1130
|
+
);
|
|
1131
|
+
} else {
|
|
1132
|
+
const storageType = await auth.getStorageType();
|
|
1133
|
+
io.out.write(chalk4.dim(`
|
|
1134
|
+
Token stored in ${storageType}.
|
|
1135
|
+
`));
|
|
1136
|
+
}
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
io.err.write(chalk4.red("Error: Token is invalid or expired.\n"));
|
|
1139
|
+
if (error instanceof Error) {
|
|
1140
|
+
io.err.write(chalk4.dim(`${error.message}
|
|
1141
|
+
`));
|
|
1142
|
+
}
|
|
1143
|
+
io.out.write(
|
|
1144
|
+
chalk4.dim(
|
|
1145
|
+
`
|
|
1146
|
+
Run ${chalk4.cyan("figma auth login")} to re-authenticate.
|
|
1147
|
+
`
|
|
1148
|
+
)
|
|
1149
|
+
);
|
|
1150
|
+
process.exit(1);
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
return cmd;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// src/cmd/auth/index.ts
|
|
1157
|
+
function createAuthCommand(factory) {
|
|
1158
|
+
const cmd = new Command4("auth").description("Manage Figma authentication");
|
|
1159
|
+
cmd.addCommand(createLoginCommand(factory));
|
|
1160
|
+
cmd.addCommand(createLogoutCommand(factory));
|
|
1161
|
+
cmd.addCommand(createStatusCommand(factory));
|
|
1162
|
+
return cmd;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/cmd/file/index.ts
|
|
1166
|
+
import { Command as Command8 } from "commander";
|
|
1167
|
+
|
|
1168
|
+
// src/cmd/file/get.ts
|
|
1169
|
+
import { Command as Command5 } from "commander";
|
|
1170
|
+
import chalk5 from "chalk";
|
|
1171
|
+
|
|
1172
|
+
// src/pkg/output/formatter.ts
|
|
1173
|
+
import Table from "cli-table3";
|
|
1174
|
+
function formatOutput(data, options = { format: "json" }) {
|
|
1175
|
+
switch (options.format) {
|
|
1176
|
+
case "json":
|
|
1177
|
+
return JSON.stringify(data, null, 2);
|
|
1178
|
+
case "table":
|
|
1179
|
+
return formatTable(data, options.columns);
|
|
1180
|
+
default:
|
|
1181
|
+
return JSON.stringify(data, null, 2);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
function formatTable(data, columns) {
|
|
1185
|
+
if (!Array.isArray(data)) {
|
|
1186
|
+
return formatKeyValueTable(data);
|
|
1187
|
+
}
|
|
1188
|
+
if (data.length === 0) {
|
|
1189
|
+
return "No data";
|
|
1190
|
+
}
|
|
1191
|
+
const firstItem = data[0];
|
|
1192
|
+
const headers = columns ?? Object.keys(firstItem);
|
|
1193
|
+
const table = new Table({
|
|
1194
|
+
head: headers,
|
|
1195
|
+
style: {
|
|
1196
|
+
head: ["cyan"],
|
|
1197
|
+
border: ["gray"]
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
for (const item of data) {
|
|
1201
|
+
const row = headers.map((col) => {
|
|
1202
|
+
const value = item[col];
|
|
1203
|
+
return formatValue(value);
|
|
1204
|
+
});
|
|
1205
|
+
table.push(row);
|
|
1206
|
+
}
|
|
1207
|
+
return table.toString();
|
|
1208
|
+
}
|
|
1209
|
+
function formatKeyValueTable(obj) {
|
|
1210
|
+
const table = new Table({
|
|
1211
|
+
style: {
|
|
1212
|
+
head: ["cyan"],
|
|
1213
|
+
border: ["gray"]
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1217
|
+
table.push({ [key]: formatValue(value) });
|
|
1218
|
+
}
|
|
1219
|
+
return table.toString();
|
|
1220
|
+
}
|
|
1221
|
+
function formatValue(value) {
|
|
1222
|
+
if (value === null || value === void 0) {
|
|
1223
|
+
return "";
|
|
1224
|
+
}
|
|
1225
|
+
if (typeof value === "object") {
|
|
1226
|
+
return JSON.stringify(value);
|
|
1227
|
+
}
|
|
1228
|
+
return String(value);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// src/cmd/file/get.ts
|
|
1232
|
+
function createFileGetCommand(factory) {
|
|
1233
|
+
const cmd = new Command5("get").description("Get file JSON structure").argument("<key>", "File key (from Figma URL)").option("-v, --version <version>", "File version to get").option(
|
|
1234
|
+
"-d, --depth <depth>",
|
|
1235
|
+
"Depth of node tree to return (default: full tree)",
|
|
1236
|
+
parseInt
|
|
1237
|
+
).option("-i, --ids <ids>", "Comma-separated list of node IDs to include").option("--geometry <paths>", "Include geometry data (paths)").option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
1238
|
+
async (fileKey, options) => {
|
|
1239
|
+
const { io } = factory;
|
|
1240
|
+
try {
|
|
1241
|
+
const token = await factory.getToken(options.token);
|
|
1242
|
+
const client = await factory.getClient();
|
|
1243
|
+
if (options.token) {
|
|
1244
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1245
|
+
const overrideClient = new FigmaClient2({ token });
|
|
1246
|
+
const file2 = await overrideClient.getFile(fileKey, {
|
|
1247
|
+
version: options.version,
|
|
1248
|
+
depth: options.depth,
|
|
1249
|
+
ids: options.ids?.split(","),
|
|
1250
|
+
geometry: options.geometry
|
|
1251
|
+
});
|
|
1252
|
+
io.out.write(
|
|
1253
|
+
formatOutput(file2, { format: options.format })
|
|
1254
|
+
);
|
|
1255
|
+
io.out.write("\n");
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
const file = await client.getFile(fileKey, {
|
|
1259
|
+
version: options.version,
|
|
1260
|
+
depth: options.depth,
|
|
1261
|
+
ids: options.ids?.split(","),
|
|
1262
|
+
geometry: options.geometry
|
|
1263
|
+
});
|
|
1264
|
+
io.out.write(
|
|
1265
|
+
formatOutput(file, { format: options.format })
|
|
1266
|
+
);
|
|
1267
|
+
io.out.write("\n");
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
io.err.write(chalk5.red("Error fetching file.\n"));
|
|
1270
|
+
if (error instanceof Error) {
|
|
1271
|
+
io.err.write(chalk5.dim(`${error.message}
|
|
1272
|
+
`));
|
|
1273
|
+
}
|
|
1274
|
+
process.exit(1);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
);
|
|
1278
|
+
return cmd;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// src/cmd/file/nodes.ts
|
|
1282
|
+
import { Command as Command6 } from "commander";
|
|
1283
|
+
import chalk6 from "chalk";
|
|
1284
|
+
function createFileNodesCommand(factory) {
|
|
1285
|
+
const cmd = new Command6("nodes").description("Get specific nodes from a file").argument("<key>", "File key (from Figma URL)").requiredOption("-i, --ids <ids>", "Comma-separated list of node IDs").option("-v, --version <version>", "File version to get").option("-d, --depth <depth>", "Depth of node tree to return", parseInt).option("--geometry <paths>", "Include geometry data (paths)").option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
1286
|
+
async (fileKey, options) => {
|
|
1287
|
+
const { io } = factory;
|
|
1288
|
+
try {
|
|
1289
|
+
const token = await factory.getToken(options.token);
|
|
1290
|
+
const client = await factory.getClient();
|
|
1291
|
+
const nodeIds = options.ids.split(",").map((id) => id.trim());
|
|
1292
|
+
if (options.token) {
|
|
1293
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1294
|
+
const overrideClient = new FigmaClient2({ token });
|
|
1295
|
+
const nodes2 = await overrideClient.getFileNodes(fileKey, nodeIds, {
|
|
1296
|
+
version: options.version,
|
|
1297
|
+
depth: options.depth,
|
|
1298
|
+
geometry: options.geometry
|
|
1299
|
+
});
|
|
1300
|
+
io.out.write(
|
|
1301
|
+
formatOutput(nodes2, { format: options.format })
|
|
1302
|
+
);
|
|
1303
|
+
io.out.write("\n");
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
const nodes = await client.getFileNodes(fileKey, nodeIds, {
|
|
1307
|
+
version: options.version,
|
|
1308
|
+
depth: options.depth,
|
|
1309
|
+
geometry: options.geometry
|
|
1310
|
+
});
|
|
1311
|
+
io.out.write(
|
|
1312
|
+
formatOutput(nodes, { format: options.format })
|
|
1313
|
+
);
|
|
1314
|
+
io.out.write("\n");
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
io.err.write(chalk6.red("Error fetching nodes.\n"));
|
|
1317
|
+
if (error instanceof Error) {
|
|
1318
|
+
io.err.write(chalk6.dim(`${error.message}
|
|
1319
|
+
`));
|
|
1320
|
+
}
|
|
1321
|
+
process.exit(1);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
);
|
|
1325
|
+
return cmd;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// src/cmd/file/images.ts
|
|
1329
|
+
import { Command as Command7 } from "commander";
|
|
1330
|
+
import chalk7 from "chalk";
|
|
1331
|
+
|
|
1332
|
+
// src/internal/download/index.ts
|
|
1333
|
+
import * as fs3 from "fs";
|
|
1334
|
+
import * as path3 from "path";
|
|
1335
|
+
var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
1336
|
+
async function downloadFile(url, options) {
|
|
1337
|
+
const controller = new AbortController();
|
|
1338
|
+
const timeoutId = setTimeout(
|
|
1339
|
+
() => controller.abort(),
|
|
1340
|
+
options.timeoutMs ?? DEFAULT_TIMEOUT_MS2
|
|
1341
|
+
);
|
|
1342
|
+
try {
|
|
1343
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
1344
|
+
if (!response.ok) {
|
|
1345
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
1346
|
+
}
|
|
1347
|
+
const buffer = await response.arrayBuffer();
|
|
1348
|
+
const filepath = path3.join(options.outputDir, options.filename);
|
|
1349
|
+
fs3.writeFileSync(filepath, Buffer.from(buffer));
|
|
1350
|
+
} finally {
|
|
1351
|
+
clearTimeout(timeoutId);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
async function downloadFiles(items, outputDir, options) {
|
|
1355
|
+
if (!fs3.existsSync(outputDir)) {
|
|
1356
|
+
fs3.mkdirSync(outputDir, { recursive: true });
|
|
1357
|
+
}
|
|
1358
|
+
const results = [];
|
|
1359
|
+
const total = items.length;
|
|
1360
|
+
let completed = 0;
|
|
1361
|
+
for (const item of items) {
|
|
1362
|
+
if (!item.url) {
|
|
1363
|
+
results.push({
|
|
1364
|
+
id: item.id,
|
|
1365
|
+
name: item.name,
|
|
1366
|
+
file: "",
|
|
1367
|
+
status: "error",
|
|
1368
|
+
error: "No URL provided"
|
|
1369
|
+
});
|
|
1370
|
+
completed++;
|
|
1371
|
+
options?.onProgress?.(completed, total, item.name ?? item.id);
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
try {
|
|
1375
|
+
const filepath = path3.join(outputDir, item.filename);
|
|
1376
|
+
await downloadFile(item.url, {
|
|
1377
|
+
outputDir,
|
|
1378
|
+
filename: item.filename,
|
|
1379
|
+
timeoutMs: options?.timeoutMs
|
|
1380
|
+
});
|
|
1381
|
+
results.push({
|
|
1382
|
+
id: item.id,
|
|
1383
|
+
name: item.name,
|
|
1384
|
+
file: filepath,
|
|
1385
|
+
status: "success"
|
|
1386
|
+
});
|
|
1387
|
+
} catch (err) {
|
|
1388
|
+
results.push({
|
|
1389
|
+
id: item.id,
|
|
1390
|
+
name: item.name,
|
|
1391
|
+
file: "",
|
|
1392
|
+
status: "error",
|
|
1393
|
+
error: err instanceof Error ? err.message : "Download failed"
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
completed++;
|
|
1397
|
+
options?.onProgress?.(completed, total, item.name ?? item.id);
|
|
1398
|
+
}
|
|
1399
|
+
return results;
|
|
1400
|
+
}
|
|
1401
|
+
function sanitizeFilename(name) {
|
|
1402
|
+
return name.replace(/[:/\\?*"<>|]/g, "-");
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// src/cmd/file/images.ts
|
|
1406
|
+
function createFileImagesCommand(factory) {
|
|
1407
|
+
const cmd = new Command7("images").description("Export images from a file").argument("<key>", "File key (from Figma URL)").requiredOption(
|
|
1408
|
+
"-i, --ids <ids>",
|
|
1409
|
+
"Comma-separated list of node IDs to export"
|
|
1410
|
+
).option("-s, --scale <scale>", "Scale factor (0.01-4)", parseFloat, 1).option("--format <format>", "Image format (jpg, png, svg, pdf)", "png").option("--svg-include-id", "Include node ID as an attribute in SVGs").option("--svg-simplify-stroke", "Simplify strokes in SVG export").option("-o, --output <dir>", "Output directory for downloaded images").option(
|
|
1411
|
+
"-O, --output-format <format>",
|
|
1412
|
+
"Output format (json, table)",
|
|
1413
|
+
"json"
|
|
1414
|
+
).option("-t, --token <token>", "Override authentication token").action(
|
|
1415
|
+
async (fileKey, options) => {
|
|
1416
|
+
const { io } = factory;
|
|
1417
|
+
const validFormats = ["jpg", "png", "svg", "pdf"];
|
|
1418
|
+
if (!validFormats.includes(options.format)) {
|
|
1419
|
+
io.err.write(
|
|
1420
|
+
chalk7.red(
|
|
1421
|
+
`Invalid format "${options.format}". Valid formats: ${validFormats.join(", ")}
|
|
1422
|
+
`
|
|
1423
|
+
)
|
|
1424
|
+
);
|
|
1425
|
+
process.exit(1);
|
|
1426
|
+
}
|
|
1427
|
+
const imageFormat = options.format;
|
|
1428
|
+
try {
|
|
1429
|
+
const token = await factory.getToken(options.token);
|
|
1430
|
+
const client = await factory.getClient();
|
|
1431
|
+
const nodeIds = options.ids.split(",").map((id) => id.trim());
|
|
1432
|
+
let activeClient = client;
|
|
1433
|
+
if (options.token) {
|
|
1434
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1435
|
+
activeClient = new FigmaClient2({ token });
|
|
1436
|
+
}
|
|
1437
|
+
const images = await activeClient.getImages(fileKey, nodeIds, {
|
|
1438
|
+
scale: options.scale,
|
|
1439
|
+
format: imageFormat,
|
|
1440
|
+
svg_include_id: options.svgIncludeId,
|
|
1441
|
+
svg_simplify_stroke: options.svgSimplifyStroke
|
|
1442
|
+
});
|
|
1443
|
+
if (options.output) {
|
|
1444
|
+
const imageUrls = images.images || {};
|
|
1445
|
+
const downloadItems = Object.entries(imageUrls).map(
|
|
1446
|
+
([nodeId, url]) => ({
|
|
1447
|
+
id: nodeId,
|
|
1448
|
+
url: url ?? null,
|
|
1449
|
+
filename: `${sanitizeFilename(nodeId)}.${imageFormat}`
|
|
1450
|
+
})
|
|
1451
|
+
);
|
|
1452
|
+
const results = await downloadFiles(downloadItems, options.output);
|
|
1453
|
+
const downloadResults = results.map((r) => ({
|
|
1454
|
+
nodeId: r.id,
|
|
1455
|
+
file: r.file,
|
|
1456
|
+
status: r.status === "success" ? "downloaded" : r.error ?? "error"
|
|
1457
|
+
}));
|
|
1458
|
+
io.out.write(
|
|
1459
|
+
formatOutput(downloadResults, {
|
|
1460
|
+
format: options.outputFormat
|
|
1461
|
+
})
|
|
1462
|
+
);
|
|
1463
|
+
io.out.write("\n");
|
|
1464
|
+
} else {
|
|
1465
|
+
io.out.write(
|
|
1466
|
+
formatOutput(images, {
|
|
1467
|
+
format: options.outputFormat
|
|
1468
|
+
})
|
|
1469
|
+
);
|
|
1470
|
+
io.out.write("\n");
|
|
1471
|
+
}
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
io.err.write(chalk7.red("Error exporting images.\n"));
|
|
1474
|
+
if (error instanceof Error) {
|
|
1475
|
+
io.err.write(chalk7.dim(`${error.message}
|
|
1476
|
+
`));
|
|
1477
|
+
}
|
|
1478
|
+
process.exit(1);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
);
|
|
1482
|
+
return cmd;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// src/cmd/file/index.ts
|
|
1486
|
+
function createFileCommand(factory) {
|
|
1487
|
+
const cmd = new Command8("file").description("Work with Figma files");
|
|
1488
|
+
cmd.addCommand(createFileGetCommand(factory));
|
|
1489
|
+
cmd.addCommand(createFileNodesCommand(factory));
|
|
1490
|
+
cmd.addCommand(createFileImagesCommand(factory));
|
|
1491
|
+
return cmd;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// src/cmd/project/index.ts
|
|
1495
|
+
import { Command as Command11 } from "commander";
|
|
1496
|
+
|
|
1497
|
+
// src/cmd/project/list.ts
|
|
1498
|
+
import { Command as Command9 } from "commander";
|
|
1499
|
+
import chalk8 from "chalk";
|
|
1500
|
+
function createProjectListCommand(factory) {
|
|
1501
|
+
const cmd = new Command9("list").description("List team projects").requiredOption("--team <id>", "Team ID").option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
1502
|
+
async (options) => {
|
|
1503
|
+
const { io } = factory;
|
|
1504
|
+
try {
|
|
1505
|
+
const token = await factory.getToken(options.token);
|
|
1506
|
+
let client = await factory.getClient();
|
|
1507
|
+
if (options.token) {
|
|
1508
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1509
|
+
client = new FigmaClient2({ token });
|
|
1510
|
+
}
|
|
1511
|
+
const projects = await client.getTeamProjects(options.team);
|
|
1512
|
+
if (options.format === "table") {
|
|
1513
|
+
const tableData = projects.projects.map((p) => ({
|
|
1514
|
+
id: p.id,
|
|
1515
|
+
name: p.name
|
|
1516
|
+
}));
|
|
1517
|
+
io.out.write(
|
|
1518
|
+
formatOutput(tableData, {
|
|
1519
|
+
format: "table",
|
|
1520
|
+
columns: ["id", "name"]
|
|
1521
|
+
})
|
|
1522
|
+
);
|
|
1523
|
+
} else {
|
|
1524
|
+
io.out.write(formatOutput(projects, { format: "json" }));
|
|
1525
|
+
}
|
|
1526
|
+
io.out.write("\n");
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
io.err.write(chalk8.red("Error fetching projects.\n"));
|
|
1529
|
+
if (error instanceof Error) {
|
|
1530
|
+
io.err.write(chalk8.dim(`${error.message}
|
|
1531
|
+
`));
|
|
1532
|
+
}
|
|
1533
|
+
process.exit(1);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
);
|
|
1537
|
+
return cmd;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// src/cmd/project/files.ts
|
|
1541
|
+
import { Command as Command10 } from "commander";
|
|
1542
|
+
import chalk9 from "chalk";
|
|
1543
|
+
function createProjectFilesCommand(factory) {
|
|
1544
|
+
const cmd = new Command10("files").description("List files in a project").argument("<project-id>", "Project ID").option("--branch-data", "Include branch metadata").option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
1545
|
+
async (projectId, options) => {
|
|
1546
|
+
const { io } = factory;
|
|
1547
|
+
try {
|
|
1548
|
+
const token = await factory.getToken(options.token);
|
|
1549
|
+
let client = await factory.getClient();
|
|
1550
|
+
if (options.token) {
|
|
1551
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1552
|
+
client = new FigmaClient2({ token });
|
|
1553
|
+
}
|
|
1554
|
+
const files = await client.getProjectFiles(projectId, {
|
|
1555
|
+
branch_data: options.branchData
|
|
1556
|
+
});
|
|
1557
|
+
if (options.format === "table") {
|
|
1558
|
+
const tableData = files.files.map((f) => ({
|
|
1559
|
+
key: f.key,
|
|
1560
|
+
name: f.name,
|
|
1561
|
+
last_modified: f.last_modified
|
|
1562
|
+
}));
|
|
1563
|
+
io.out.write(
|
|
1564
|
+
formatOutput(tableData, {
|
|
1565
|
+
format: "table",
|
|
1566
|
+
columns: ["key", "name", "last_modified"]
|
|
1567
|
+
})
|
|
1568
|
+
);
|
|
1569
|
+
} else {
|
|
1570
|
+
io.out.write(formatOutput(files, { format: "json" }));
|
|
1571
|
+
}
|
|
1572
|
+
io.out.write("\n");
|
|
1573
|
+
} catch (error) {
|
|
1574
|
+
io.err.write(chalk9.red("Error fetching project files.\n"));
|
|
1575
|
+
if (error instanceof Error) {
|
|
1576
|
+
io.err.write(chalk9.dim(`${error.message}
|
|
1577
|
+
`));
|
|
1578
|
+
}
|
|
1579
|
+
process.exit(1);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
);
|
|
1583
|
+
return cmd;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// src/cmd/project/index.ts
|
|
1587
|
+
function createProjectCommand(factory) {
|
|
1588
|
+
const cmd = new Command11("project").description("Work with Figma projects");
|
|
1589
|
+
cmd.addCommand(createProjectListCommand(factory));
|
|
1590
|
+
cmd.addCommand(createProjectFilesCommand(factory));
|
|
1591
|
+
return cmd;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// src/cmd/component/index.ts
|
|
1595
|
+
import { Command as Command14 } from "commander";
|
|
1596
|
+
|
|
1597
|
+
// src/cmd/component/list.ts
|
|
1598
|
+
import { Command as Command12 } from "commander";
|
|
1599
|
+
import chalk10 from "chalk";
|
|
1600
|
+
function createComponentListCommand(factory) {
|
|
1601
|
+
const cmd = new Command12("list").description("List components (from team or file)").option("--team <id>", "Team ID to list components from").option("--file <key>", "File key to list components from").option("--page-size <size>", "Number of results per page", parseInt).option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
1602
|
+
async (options) => {
|
|
1603
|
+
const { io } = factory;
|
|
1604
|
+
if (!options.team && !options.file) {
|
|
1605
|
+
io.err.write(
|
|
1606
|
+
chalk10.red("Error: Either --team or --file must be specified.\n")
|
|
1607
|
+
);
|
|
1608
|
+
process.exit(1);
|
|
1609
|
+
}
|
|
1610
|
+
try {
|
|
1611
|
+
const token = await factory.getToken(options.token);
|
|
1612
|
+
let client = await factory.getClient();
|
|
1613
|
+
if (options.token) {
|
|
1614
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1615
|
+
client = new FigmaClient2({ token });
|
|
1616
|
+
}
|
|
1617
|
+
if (options.team) {
|
|
1618
|
+
const components = await client.getTeamComponents(options.team, {
|
|
1619
|
+
page_size: options.pageSize
|
|
1620
|
+
});
|
|
1621
|
+
if (options.format === "table") {
|
|
1622
|
+
const tableData = components.meta?.components?.map((c) => ({
|
|
1623
|
+
key: c.key,
|
|
1624
|
+
name: c.name,
|
|
1625
|
+
description: c.description || ""
|
|
1626
|
+
})) || [];
|
|
1627
|
+
io.out.write(
|
|
1628
|
+
formatOutput(tableData, {
|
|
1629
|
+
format: "table",
|
|
1630
|
+
columns: ["key", "name", "description"]
|
|
1631
|
+
})
|
|
1632
|
+
);
|
|
1633
|
+
} else {
|
|
1634
|
+
io.out.write(formatOutput(components, { format: "json" }));
|
|
1635
|
+
}
|
|
1636
|
+
} else if (options.file) {
|
|
1637
|
+
const components = await client.getFileComponents(options.file);
|
|
1638
|
+
if (options.format === "table") {
|
|
1639
|
+
const tableData = components.meta?.components?.map((c) => ({
|
|
1640
|
+
key: c.key,
|
|
1641
|
+
name: c.name,
|
|
1642
|
+
description: c.description || ""
|
|
1643
|
+
})) || [];
|
|
1644
|
+
io.out.write(
|
|
1645
|
+
formatOutput(tableData, {
|
|
1646
|
+
format: "table",
|
|
1647
|
+
columns: ["key", "name", "description"]
|
|
1648
|
+
})
|
|
1649
|
+
);
|
|
1650
|
+
} else {
|
|
1651
|
+
io.out.write(formatOutput(components, { format: "json" }));
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
io.out.write("\n");
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
io.err.write(chalk10.red("Error fetching components.\n"));
|
|
1657
|
+
if (error instanceof Error) {
|
|
1658
|
+
io.err.write(chalk10.dim(`${error.message}
|
|
1659
|
+
`));
|
|
1660
|
+
}
|
|
1661
|
+
process.exit(1);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
);
|
|
1665
|
+
return cmd;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// src/cmd/component/get.ts
|
|
1669
|
+
import { Command as Command13 } from "commander";
|
|
1670
|
+
import chalk11 from "chalk";
|
|
1671
|
+
function createComponentGetCommand(factory) {
|
|
1672
|
+
const cmd = new Command13("get").description("Get component details").argument("<key>", "Component key").option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
1673
|
+
async (componentKey, options) => {
|
|
1674
|
+
const { io } = factory;
|
|
1675
|
+
try {
|
|
1676
|
+
const token = await factory.getToken(options.token);
|
|
1677
|
+
let client = await factory.getClient();
|
|
1678
|
+
if (options.token) {
|
|
1679
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1680
|
+
client = new FigmaClient2({ token });
|
|
1681
|
+
}
|
|
1682
|
+
const component = await client.getComponent(componentKey);
|
|
1683
|
+
io.out.write(
|
|
1684
|
+
formatOutput(component, {
|
|
1685
|
+
format: options.format
|
|
1686
|
+
})
|
|
1687
|
+
);
|
|
1688
|
+
io.out.write("\n");
|
|
1689
|
+
} catch (error) {
|
|
1690
|
+
io.err.write(chalk11.red("Error fetching component.\n"));
|
|
1691
|
+
if (error instanceof Error) {
|
|
1692
|
+
io.err.write(chalk11.dim(`${error.message}
|
|
1693
|
+
`));
|
|
1694
|
+
}
|
|
1695
|
+
process.exit(1);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
);
|
|
1699
|
+
return cmd;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// src/cmd/component/index.ts
|
|
1703
|
+
function createComponentCommand(factory) {
|
|
1704
|
+
const cmd = new Command14("component").description(
|
|
1705
|
+
"Work with Figma components"
|
|
1706
|
+
);
|
|
1707
|
+
cmd.addCommand(createComponentListCommand(factory));
|
|
1708
|
+
cmd.addCommand(createComponentGetCommand(factory));
|
|
1709
|
+
return cmd;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// src/cmd/style/index.ts
|
|
1713
|
+
import { Command as Command17 } from "commander";
|
|
1714
|
+
|
|
1715
|
+
// src/cmd/style/list.ts
|
|
1716
|
+
import { Command as Command15 } from "commander";
|
|
1717
|
+
import chalk12 from "chalk";
|
|
1718
|
+
function createStyleListCommand(factory) {
|
|
1719
|
+
const cmd = new Command15("list").description("List styles (from team or file)").option("--team <id>", "Team ID to list styles from").option("--file <key>", "File key to list styles from").option("--page-size <size>", "Number of results per page", parseInt).option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
1720
|
+
async (options) => {
|
|
1721
|
+
const { io } = factory;
|
|
1722
|
+
if (!options.team && !options.file) {
|
|
1723
|
+
io.err.write(
|
|
1724
|
+
chalk12.red("Error: Either --team or --file must be specified.\n")
|
|
1725
|
+
);
|
|
1726
|
+
process.exit(1);
|
|
1727
|
+
}
|
|
1728
|
+
try {
|
|
1729
|
+
const token = await factory.getToken(options.token);
|
|
1730
|
+
let client = await factory.getClient();
|
|
1731
|
+
if (options.token) {
|
|
1732
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1733
|
+
client = new FigmaClient2({ token });
|
|
1734
|
+
}
|
|
1735
|
+
if (options.team) {
|
|
1736
|
+
const styles = await client.getTeamStyles(options.team, {
|
|
1737
|
+
page_size: options.pageSize
|
|
1738
|
+
});
|
|
1739
|
+
if (options.format === "table") {
|
|
1740
|
+
const tableData = styles.meta?.styles?.map((s) => ({
|
|
1741
|
+
key: s.key,
|
|
1742
|
+
name: s.name,
|
|
1743
|
+
style_type: s.style_type,
|
|
1744
|
+
description: s.description || ""
|
|
1745
|
+
})) || [];
|
|
1746
|
+
io.out.write(
|
|
1747
|
+
formatOutput(tableData, {
|
|
1748
|
+
format: "table",
|
|
1749
|
+
columns: ["key", "name", "style_type", "description"]
|
|
1750
|
+
})
|
|
1751
|
+
);
|
|
1752
|
+
} else {
|
|
1753
|
+
io.out.write(formatOutput(styles, { format: "json" }));
|
|
1754
|
+
}
|
|
1755
|
+
} else if (options.file) {
|
|
1756
|
+
const styles = await client.getFileStyles(options.file);
|
|
1757
|
+
if (options.format === "table") {
|
|
1758
|
+
const tableData = styles.meta?.styles?.map((s) => ({
|
|
1759
|
+
key: s.key,
|
|
1760
|
+
name: s.name,
|
|
1761
|
+
style_type: s.style_type,
|
|
1762
|
+
description: s.description || ""
|
|
1763
|
+
})) || [];
|
|
1764
|
+
io.out.write(
|
|
1765
|
+
formatOutput(tableData, {
|
|
1766
|
+
format: "table",
|
|
1767
|
+
columns: ["key", "name", "style_type", "description"]
|
|
1768
|
+
})
|
|
1769
|
+
);
|
|
1770
|
+
} else {
|
|
1771
|
+
io.out.write(formatOutput(styles, { format: "json" }));
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
io.out.write("\n");
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
io.err.write(chalk12.red("Error fetching styles.\n"));
|
|
1777
|
+
if (error instanceof Error) {
|
|
1778
|
+
io.err.write(chalk12.dim(`${error.message}
|
|
1779
|
+
`));
|
|
1780
|
+
}
|
|
1781
|
+
process.exit(1);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
);
|
|
1785
|
+
return cmd;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// src/cmd/style/get.ts
|
|
1789
|
+
import { Command as Command16 } from "commander";
|
|
1790
|
+
import chalk13 from "chalk";
|
|
1791
|
+
function createStyleGetCommand(factory) {
|
|
1792
|
+
const cmd = new Command16("get").description("Get style details").argument("<key>", "Style key").option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
1793
|
+
async (styleKey, options) => {
|
|
1794
|
+
const { io } = factory;
|
|
1795
|
+
try {
|
|
1796
|
+
const token = await factory.getToken(options.token);
|
|
1797
|
+
let client = await factory.getClient();
|
|
1798
|
+
if (options.token) {
|
|
1799
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1800
|
+
client = new FigmaClient2({ token });
|
|
1801
|
+
}
|
|
1802
|
+
const style = await client.getStyle(styleKey);
|
|
1803
|
+
io.out.write(
|
|
1804
|
+
formatOutput(style, {
|
|
1805
|
+
format: options.format
|
|
1806
|
+
})
|
|
1807
|
+
);
|
|
1808
|
+
io.out.write("\n");
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
io.err.write(chalk13.red("Error fetching style.\n"));
|
|
1811
|
+
if (error instanceof Error) {
|
|
1812
|
+
io.err.write(chalk13.dim(`${error.message}
|
|
1813
|
+
`));
|
|
1814
|
+
}
|
|
1815
|
+
process.exit(1);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
);
|
|
1819
|
+
return cmd;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// src/cmd/style/index.ts
|
|
1823
|
+
function createStyleCommand(factory) {
|
|
1824
|
+
const cmd = new Command17("style").description("Work with Figma styles");
|
|
1825
|
+
cmd.addCommand(createStyleListCommand(factory));
|
|
1826
|
+
cmd.addCommand(createStyleGetCommand(factory));
|
|
1827
|
+
return cmd;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// src/cmd/variable/index.ts
|
|
1831
|
+
import { Command as Command20 } from "commander";
|
|
1832
|
+
|
|
1833
|
+
// src/cmd/variable/list.ts
|
|
1834
|
+
import { Command as Command18 } from "commander";
|
|
1835
|
+
import chalk14 from "chalk";
|
|
1836
|
+
function createVariableListCommand(factory) {
|
|
1837
|
+
const cmd = new Command18("list").description("List variables from a file (Enterprise)").argument("<file-key>", "File key").option("--published", "List published variables only").option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
|
|
1838
|
+
async (fileKey, options) => {
|
|
1839
|
+
const { io } = factory;
|
|
1840
|
+
try {
|
|
1841
|
+
const token = await factory.getToken(options.token);
|
|
1842
|
+
let client = await factory.getClient();
|
|
1843
|
+
if (options.token) {
|
|
1844
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1845
|
+
client = new FigmaClient2({ token });
|
|
1846
|
+
}
|
|
1847
|
+
const variables = options.published ? await client.getPublishedVariables(fileKey) : await client.getLocalVariables(fileKey);
|
|
1848
|
+
if (options.format === "table") {
|
|
1849
|
+
const variableMap = "meta" in variables ? variables.meta?.variables : {};
|
|
1850
|
+
const tableData = Object.entries(variableMap || {}).map(
|
|
1851
|
+
([id, v]) => ({
|
|
1852
|
+
id,
|
|
1853
|
+
name: v.name || "",
|
|
1854
|
+
resolvedType: v.resolvedType || ""
|
|
1855
|
+
})
|
|
1856
|
+
);
|
|
1857
|
+
io.out.write(
|
|
1858
|
+
formatOutput(tableData, {
|
|
1859
|
+
format: "table",
|
|
1860
|
+
columns: ["id", "name", "resolvedType"]
|
|
1861
|
+
})
|
|
1862
|
+
);
|
|
1863
|
+
} else {
|
|
1864
|
+
io.out.write(formatOutput(variables, { format: "json" }));
|
|
1865
|
+
}
|
|
1866
|
+
io.out.write("\n");
|
|
1867
|
+
} catch (error) {
|
|
1868
|
+
io.err.write(chalk14.red("Error fetching variables.\n"));
|
|
1869
|
+
if (error instanceof Error) {
|
|
1870
|
+
io.err.write(chalk14.dim(`${error.message}
|
|
1871
|
+
`));
|
|
1872
|
+
}
|
|
1873
|
+
io.err.write(
|
|
1874
|
+
chalk14.dim("\nNote: Variables API requires Figma Enterprise plan.\n")
|
|
1875
|
+
);
|
|
1876
|
+
process.exit(1);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
);
|
|
1880
|
+
return cmd;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// src/cmd/variable/export.ts
|
|
1884
|
+
import { Command as Command19 } from "commander";
|
|
1885
|
+
import chalk15 from "chalk";
|
|
1886
|
+
import * as fs4 from "fs";
|
|
1887
|
+
function createVariableExportCommand(factory) {
|
|
1888
|
+
const cmd = new Command19("export").description("Export variables as design tokens (Enterprise)").argument("<file-key>", "File key").option(
|
|
1889
|
+
"--format <format>",
|
|
1890
|
+
"Output format (json, css, scss, style-dictionary)",
|
|
1891
|
+
"json"
|
|
1892
|
+
).option("--mode <mode>", "Specific mode to export").option("-o, --output <file>", "Output file path").option("-t, --token <token>", "Override authentication token").action(
|
|
1893
|
+
async (fileKey, options) => {
|
|
1894
|
+
const { io } = factory;
|
|
1895
|
+
try {
|
|
1896
|
+
const token = await factory.getToken(options.token);
|
|
1897
|
+
let client = await factory.getClient();
|
|
1898
|
+
if (options.token) {
|
|
1899
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1900
|
+
client = new FigmaClient2({ token });
|
|
1901
|
+
}
|
|
1902
|
+
const response = await client.getLocalVariables(fileKey);
|
|
1903
|
+
const meta = response.meta;
|
|
1904
|
+
if (!meta) {
|
|
1905
|
+
io.err.write(chalk15.red("No variables found in file.\n"));
|
|
1906
|
+
process.exit(1);
|
|
1907
|
+
}
|
|
1908
|
+
const variables = meta.variables;
|
|
1909
|
+
const collections = meta.variableCollections;
|
|
1910
|
+
let targetModeId;
|
|
1911
|
+
if (options.mode) {
|
|
1912
|
+
for (const collection of Object.values(collections)) {
|
|
1913
|
+
const mode = collection.modes.find(
|
|
1914
|
+
(m) => m.name.toLowerCase() === options.mode?.toLowerCase()
|
|
1915
|
+
);
|
|
1916
|
+
if (mode) {
|
|
1917
|
+
targetModeId = mode.modeId;
|
|
1918
|
+
break;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
if (!targetModeId) {
|
|
1922
|
+
io.err.write(chalk15.red(`Mode "${options.mode}" not found.
|
|
1923
|
+
`));
|
|
1924
|
+
const allModes = Object.values(collections).flatMap(
|
|
1925
|
+
(c) => c.modes.map((m) => m.name)
|
|
1926
|
+
);
|
|
1927
|
+
io.err.write(
|
|
1928
|
+
chalk15.dim(`Available modes: ${allModes.join(", ")}
|
|
1929
|
+
`)
|
|
1930
|
+
);
|
|
1931
|
+
process.exit(1);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
let output;
|
|
1935
|
+
switch (options.format) {
|
|
1936
|
+
case "css":
|
|
1937
|
+
output = exportToCss(variables, collections, targetModeId);
|
|
1938
|
+
break;
|
|
1939
|
+
case "scss":
|
|
1940
|
+
output = exportToScss(variables, collections, targetModeId);
|
|
1941
|
+
break;
|
|
1942
|
+
case "style-dictionary":
|
|
1943
|
+
output = exportToStyleDictionary(
|
|
1944
|
+
variables,
|
|
1945
|
+
collections,
|
|
1946
|
+
targetModeId
|
|
1947
|
+
);
|
|
1948
|
+
break;
|
|
1949
|
+
default:
|
|
1950
|
+
output = JSON.stringify({ variables, collections }, null, 2);
|
|
1951
|
+
}
|
|
1952
|
+
if (options.output) {
|
|
1953
|
+
fs4.writeFileSync(options.output, output);
|
|
1954
|
+
io.out.write(chalk15.green(`\u2713 Exported to ${options.output}
|
|
1955
|
+
`));
|
|
1956
|
+
} else {
|
|
1957
|
+
io.out.write(output);
|
|
1958
|
+
io.out.write("\n");
|
|
1959
|
+
}
|
|
1960
|
+
} catch (error) {
|
|
1961
|
+
io.err.write(chalk15.red("Error exporting variables.\n"));
|
|
1962
|
+
if (error instanceof Error) {
|
|
1963
|
+
io.err.write(chalk15.dim(`${error.message}
|
|
1964
|
+
`));
|
|
1965
|
+
}
|
|
1966
|
+
io.err.write(
|
|
1967
|
+
chalk15.dim("\nNote: Variables API requires Figma Enterprise plan.\n")
|
|
1968
|
+
);
|
|
1969
|
+
process.exit(1);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
);
|
|
1973
|
+
return cmd;
|
|
1974
|
+
}
|
|
1975
|
+
function toKebabCase(str) {
|
|
1976
|
+
return str.replace(/\s+/g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").toLowerCase();
|
|
1977
|
+
}
|
|
1978
|
+
function resolveValue(value, variables, _modeId) {
|
|
1979
|
+
if (typeof value === "object" && value !== null) {
|
|
1980
|
+
if ("r" in value && "g" in value && "b" in value) {
|
|
1981
|
+
const rgba = value;
|
|
1982
|
+
const r = Math.round(rgba.r * 255);
|
|
1983
|
+
const g = Math.round(rgba.g * 255);
|
|
1984
|
+
const b = Math.round(rgba.b * 255);
|
|
1985
|
+
const a = rgba.a ?? 1;
|
|
1986
|
+
if (a === 1) {
|
|
1987
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
1988
|
+
}
|
|
1989
|
+
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
1990
|
+
}
|
|
1991
|
+
if ("type" in value && value.type === "VARIABLE_ALIAS") {
|
|
1992
|
+
const aliasId = value.id;
|
|
1993
|
+
const aliasVar = variables[aliasId];
|
|
1994
|
+
if (aliasVar) {
|
|
1995
|
+
return `var(--${toKebabCase(aliasVar.name)})`;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
if (typeof value === "number") {
|
|
2000
|
+
return `${value}px`;
|
|
2001
|
+
}
|
|
2002
|
+
return String(value);
|
|
2003
|
+
}
|
|
2004
|
+
function exportToCss(variables, collections, modeId) {
|
|
2005
|
+
const lines = [":root {"];
|
|
2006
|
+
for (const variable of Object.values(variables)) {
|
|
2007
|
+
const collection = collections[variable.variableCollectionId];
|
|
2008
|
+
const targetMode = modeId || collection?.defaultModeId;
|
|
2009
|
+
const value = variable.valuesByMode[targetMode];
|
|
2010
|
+
if (value !== void 0) {
|
|
2011
|
+
const cssName = toKebabCase(variable.name);
|
|
2012
|
+
const cssValue = resolveValue(value, variables, targetMode);
|
|
2013
|
+
lines.push(` --${cssName}: ${cssValue};`);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
lines.push("}");
|
|
2017
|
+
return lines.join("\n");
|
|
2018
|
+
}
|
|
2019
|
+
function exportToScss(variables, collections, modeId) {
|
|
2020
|
+
const lines = [];
|
|
2021
|
+
for (const variable of Object.values(variables)) {
|
|
2022
|
+
const collection = collections[variable.variableCollectionId];
|
|
2023
|
+
const targetMode = modeId || collection?.defaultModeId;
|
|
2024
|
+
const value = variable.valuesByMode[targetMode];
|
|
2025
|
+
if (value !== void 0) {
|
|
2026
|
+
const scssName = toKebabCase(variable.name);
|
|
2027
|
+
const scssValue = resolveValue(value, variables, targetMode).replace(
|
|
2028
|
+
/var\(--([^)]+)\)/g,
|
|
2029
|
+
"$$$1"
|
|
2030
|
+
);
|
|
2031
|
+
lines.push(`$${scssName}: ${scssValue};`);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
return lines.join("\n");
|
|
2035
|
+
}
|
|
2036
|
+
function exportToStyleDictionary(variables, collections, modeId) {
|
|
2037
|
+
const tokens = {};
|
|
2038
|
+
for (const variable of Object.values(variables)) {
|
|
2039
|
+
const collection = collections[variable.variableCollectionId];
|
|
2040
|
+
const targetMode = modeId || collection?.defaultModeId;
|
|
2041
|
+
const value = variable.valuesByMode[targetMode];
|
|
2042
|
+
if (value === void 0) continue;
|
|
2043
|
+
const pathParts = variable.name.split("/").map((p) => p.trim());
|
|
2044
|
+
let current = tokens;
|
|
2045
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
2046
|
+
const part = pathParts[i];
|
|
2047
|
+
if (!current[part]) {
|
|
2048
|
+
current[part] = {};
|
|
2049
|
+
}
|
|
2050
|
+
current = current[part];
|
|
2051
|
+
}
|
|
2052
|
+
const finalKey = pathParts[pathParts.length - 1];
|
|
2053
|
+
current[finalKey] = {
|
|
2054
|
+
value: resolveValueForStyleDictionary(value, variables, targetMode),
|
|
2055
|
+
type: mapTypeToStyleDictionary(variable.resolvedType),
|
|
2056
|
+
description: variable.description || void 0
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
return JSON.stringify(tokens, null, 2);
|
|
2060
|
+
}
|
|
2061
|
+
function resolveValueForStyleDictionary(value, variables, _modeId) {
|
|
2062
|
+
if (typeof value === "object" && value !== null) {
|
|
2063
|
+
if ("r" in value && "g" in value && "b" in value) {
|
|
2064
|
+
const rgba = value;
|
|
2065
|
+
const r = Math.round(rgba.r * 255).toString(16).padStart(2, "0");
|
|
2066
|
+
const g = Math.round(rgba.g * 255).toString(16).padStart(2, "0");
|
|
2067
|
+
const b = Math.round(rgba.b * 255).toString(16).padStart(2, "0");
|
|
2068
|
+
const a = rgba.a ?? 1;
|
|
2069
|
+
if (a === 1) {
|
|
2070
|
+
return `#${r}${g}${b}`;
|
|
2071
|
+
}
|
|
2072
|
+
const aHex = Math.round(a * 255).toString(16).padStart(2, "0");
|
|
2073
|
+
return `#${r}${g}${b}${aHex}`;
|
|
2074
|
+
}
|
|
2075
|
+
if ("type" in value && value.type === "VARIABLE_ALIAS") {
|
|
2076
|
+
const aliasId = value.id;
|
|
2077
|
+
const aliasVar = variables[aliasId];
|
|
2078
|
+
if (aliasVar) {
|
|
2079
|
+
return `{${aliasVar.name.replace(/\//g, ".")}}`;
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
return value;
|
|
2084
|
+
}
|
|
2085
|
+
function mapTypeToStyleDictionary(figmaType) {
|
|
2086
|
+
switch (figmaType) {
|
|
2087
|
+
case "COLOR":
|
|
2088
|
+
return "color";
|
|
2089
|
+
case "FLOAT":
|
|
2090
|
+
return "dimension";
|
|
2091
|
+
case "STRING":
|
|
2092
|
+
return "string";
|
|
2093
|
+
case "BOOLEAN":
|
|
2094
|
+
return "boolean";
|
|
2095
|
+
default:
|
|
2096
|
+
return figmaType.toLowerCase();
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
// src/cmd/variable/index.ts
|
|
2101
|
+
function createVariableCommand(factory) {
|
|
2102
|
+
const cmd = new Command20("variable").description(
|
|
2103
|
+
"Work with Figma variables (Enterprise)"
|
|
2104
|
+
);
|
|
2105
|
+
cmd.addCommand(createVariableListCommand(factory));
|
|
2106
|
+
cmd.addCommand(createVariableExportCommand(factory));
|
|
2107
|
+
return cmd;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
// src/cmd/export/index.ts
|
|
2111
|
+
import { Command as Command24 } from "commander";
|
|
2112
|
+
|
|
2113
|
+
// src/cmd/export/tokens.ts
|
|
2114
|
+
import { Command as Command21 } from "commander";
|
|
2115
|
+
import chalk16 from "chalk";
|
|
2116
|
+
import * as fs5 from "fs";
|
|
2117
|
+
|
|
2118
|
+
// src/internal/transform/tokens.ts
|
|
2119
|
+
function figmaColorToCss(color) {
|
|
2120
|
+
const r = Math.round(color.r * 255);
|
|
2121
|
+
const g = Math.round(color.g * 255);
|
|
2122
|
+
const b = Math.round(color.b * 255);
|
|
2123
|
+
const a = color.a ?? 1;
|
|
2124
|
+
if (a === 1) {
|
|
2125
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
2126
|
+
}
|
|
2127
|
+
return `rgba(${r}, ${g}, ${b}, ${a.toFixed(2)})`;
|
|
2128
|
+
}
|
|
2129
|
+
function toKebabCase2(str) {
|
|
2130
|
+
return str.replace(/\s+/g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").toLowerCase();
|
|
2131
|
+
}
|
|
2132
|
+
function extractStylesFromFile(file, styleMetadata) {
|
|
2133
|
+
const tokens = {
|
|
2134
|
+
colors: {},
|
|
2135
|
+
typography: {},
|
|
2136
|
+
spacing: {},
|
|
2137
|
+
effects: {}
|
|
2138
|
+
};
|
|
2139
|
+
const styleNodeMap = /* @__PURE__ */ new Map();
|
|
2140
|
+
for (const style of styleMetadata) {
|
|
2141
|
+
const nodeId = style.node_id;
|
|
2142
|
+
if (nodeId) {
|
|
2143
|
+
styleNodeMap.set(nodeId, style);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
function traverseNode(node) {
|
|
2147
|
+
const nodeWithStyles = node;
|
|
2148
|
+
if ("styles" in nodeWithStyles && nodeWithStyles.styles) {
|
|
2149
|
+
const styles = nodeWithStyles.styles;
|
|
2150
|
+
if (styles.fill && "fills" in nodeWithStyles) {
|
|
2151
|
+
const fills = nodeWithStyles.fills;
|
|
2152
|
+
if (fills && fills.length > 0 && fills[0].type === "SOLID") {
|
|
2153
|
+
const fill = fills[0];
|
|
2154
|
+
if (fill.color) {
|
|
2155
|
+
const styleInfo = styleNodeMap.get(node.id);
|
|
2156
|
+
const styleName = styleInfo?.name || node.name;
|
|
2157
|
+
const tokenName = toKebabCase2(styleName);
|
|
2158
|
+
tokens.colors[tokenName] = {
|
|
2159
|
+
name: styleName,
|
|
2160
|
+
type: "color",
|
|
2161
|
+
value: figmaColorToCss(fill.color),
|
|
2162
|
+
description: styleInfo?.description
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
if (styles.text && "style" in nodeWithStyles && nodeWithStyles.style) {
|
|
2168
|
+
const textStyle = nodeWithStyles.style;
|
|
2169
|
+
const styleInfo = styleNodeMap.get(node.id);
|
|
2170
|
+
const styleName = styleInfo?.name || node.name;
|
|
2171
|
+
const tokenName = toKebabCase2(styleName);
|
|
2172
|
+
tokens.typography[tokenName] = {
|
|
2173
|
+
name: styleName,
|
|
2174
|
+
type: "typography",
|
|
2175
|
+
value: {
|
|
2176
|
+
fontFamily: textStyle.fontFamily || "sans-serif",
|
|
2177
|
+
fontSize: `${textStyle.fontSize || 16}px`,
|
|
2178
|
+
fontWeight: textStyle.fontWeight || 400,
|
|
2179
|
+
lineHeight: textStyle.lineHeightPx !== void 0 ? `${textStyle.lineHeightPx}px` : "normal",
|
|
2180
|
+
letterSpacing: textStyle.letterSpacing !== void 0 ? `${textStyle.letterSpacing}px` : "normal"
|
|
2181
|
+
},
|
|
2182
|
+
description: styleInfo?.description
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
if ("children" in node) {
|
|
2187
|
+
const parent = node;
|
|
2188
|
+
for (const child of parent.children || []) {
|
|
2189
|
+
traverseNode(child);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
if (file.document) {
|
|
2194
|
+
traverseNode(file.document);
|
|
2195
|
+
}
|
|
2196
|
+
return tokens;
|
|
2197
|
+
}
|
|
2198
|
+
function tokensToCss(tokens) {
|
|
2199
|
+
const lines = [":root {"];
|
|
2200
|
+
if (tokens.colors) {
|
|
2201
|
+
lines.push(" /* Colors */");
|
|
2202
|
+
for (const [name, token] of Object.entries(tokens.colors)) {
|
|
2203
|
+
lines.push(` --color-${name}: ${token.value};`);
|
|
2204
|
+
}
|
|
2205
|
+
lines.push("");
|
|
2206
|
+
}
|
|
2207
|
+
if (tokens.typography) {
|
|
2208
|
+
lines.push(" /* Typography */");
|
|
2209
|
+
for (const [name, token] of Object.entries(tokens.typography)) {
|
|
2210
|
+
const value = token.value;
|
|
2211
|
+
lines.push(` --font-${name}-family: ${value.fontFamily};`);
|
|
2212
|
+
lines.push(` --font-${name}-size: ${value.fontSize};`);
|
|
2213
|
+
lines.push(` --font-${name}-weight: ${value.fontWeight};`);
|
|
2214
|
+
lines.push(` --font-${name}-line-height: ${value.lineHeight};`);
|
|
2215
|
+
lines.push(` --font-${name}-letter-spacing: ${value.letterSpacing};`);
|
|
2216
|
+
}
|
|
2217
|
+
lines.push("");
|
|
2218
|
+
}
|
|
2219
|
+
if (tokens.spacing) {
|
|
2220
|
+
lines.push(" /* Spacing */");
|
|
2221
|
+
for (const [name, token] of Object.entries(tokens.spacing)) {
|
|
2222
|
+
lines.push(` --spacing-${name}: ${token.value};`);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
lines.push("}");
|
|
2226
|
+
return lines.join("\n");
|
|
2227
|
+
}
|
|
2228
|
+
function tokensToScss(tokens) {
|
|
2229
|
+
const lines = [];
|
|
2230
|
+
if (tokens.colors) {
|
|
2231
|
+
lines.push("// Colors");
|
|
2232
|
+
for (const [name, token] of Object.entries(tokens.colors)) {
|
|
2233
|
+
lines.push(`$color-${name}: ${token.value};`);
|
|
2234
|
+
}
|
|
2235
|
+
lines.push("");
|
|
2236
|
+
}
|
|
2237
|
+
if (tokens.typography) {
|
|
2238
|
+
lines.push("// Typography");
|
|
2239
|
+
for (const [name, token] of Object.entries(tokens.typography)) {
|
|
2240
|
+
const value = token.value;
|
|
2241
|
+
lines.push(`$font-${name}-family: ${value.fontFamily};`);
|
|
2242
|
+
lines.push(`$font-${name}-size: ${value.fontSize};`);
|
|
2243
|
+
lines.push(`$font-${name}-weight: ${value.fontWeight};`);
|
|
2244
|
+
lines.push(`$font-${name}-line-height: ${value.lineHeight};`);
|
|
2245
|
+
lines.push(`$font-${name}-letter-spacing: ${value.letterSpacing};`);
|
|
2246
|
+
}
|
|
2247
|
+
lines.push("");
|
|
2248
|
+
}
|
|
2249
|
+
if (tokens.spacing) {
|
|
2250
|
+
lines.push("// Spacing");
|
|
2251
|
+
for (const [name, token] of Object.entries(tokens.spacing)) {
|
|
2252
|
+
lines.push(`$spacing-${name}: ${token.value};`);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
return lines.join("\n");
|
|
2256
|
+
}
|
|
2257
|
+
function tokensToStyleDictionary(tokens) {
|
|
2258
|
+
const output = {};
|
|
2259
|
+
if (tokens.colors && Object.keys(tokens.colors).length > 0) {
|
|
2260
|
+
output.color = {};
|
|
2261
|
+
for (const [name, token] of Object.entries(tokens.colors)) {
|
|
2262
|
+
output.color[name] = {
|
|
2263
|
+
value: token.value,
|
|
2264
|
+
type: "color",
|
|
2265
|
+
description: token.description
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
if (tokens.typography && Object.keys(tokens.typography).length > 0) {
|
|
2270
|
+
output.typography = {};
|
|
2271
|
+
for (const [name, token] of Object.entries(tokens.typography)) {
|
|
2272
|
+
output.typography[name] = {
|
|
2273
|
+
value: token.value,
|
|
2274
|
+
type: "typography",
|
|
2275
|
+
description: token.description
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
if (tokens.spacing && Object.keys(tokens.spacing).length > 0) {
|
|
2280
|
+
output.spacing = {};
|
|
2281
|
+
for (const [name, token] of Object.entries(tokens.spacing)) {
|
|
2282
|
+
output.spacing[name] = {
|
|
2283
|
+
value: token.value,
|
|
2284
|
+
type: "dimension",
|
|
2285
|
+
description: token.description
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
return JSON.stringify(output, null, 2);
|
|
2290
|
+
}
|
|
2291
|
+
function tokensToTailwind(tokens) {
|
|
2292
|
+
const config = {
|
|
2293
|
+
theme: {
|
|
2294
|
+
extend: {}
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
if (tokens.colors && Object.keys(tokens.colors).length > 0) {
|
|
2298
|
+
config.theme.extend.colors = {};
|
|
2299
|
+
for (const [name, token] of Object.entries(tokens.colors)) {
|
|
2300
|
+
config.theme.extend.colors[name] = token.value;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
if (tokens.typography && Object.keys(tokens.typography).length > 0) {
|
|
2304
|
+
config.theme.extend.fontFamily = {};
|
|
2305
|
+
config.theme.extend.fontSize = {};
|
|
2306
|
+
for (const [name, token] of Object.entries(tokens.typography)) {
|
|
2307
|
+
const value = token.value;
|
|
2308
|
+
config.theme.extend.fontFamily[name] = [value.fontFamily];
|
|
2309
|
+
config.theme.extend.fontSize[name] = value.fontSize;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
if (tokens.spacing && Object.keys(tokens.spacing).length > 0) {
|
|
2313
|
+
config.theme.extend.spacing = {};
|
|
2314
|
+
for (const [name, token] of Object.entries(tokens.spacing)) {
|
|
2315
|
+
config.theme.extend.spacing[name] = token.value;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
return `/** @type {import('tailwindcss').Config} */
|
|
2319
|
+
module.exports = ${JSON.stringify(config, null, 2)};`;
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
// src/cmd/export/tokens.ts
|
|
2323
|
+
function createExportTokensCommand(factory) {
|
|
2324
|
+
const cmd = new Command21("tokens").description("Export design tokens from a Figma file").argument("<file-key>", "File key").option(
|
|
2325
|
+
"--format <format>",
|
|
2326
|
+
"Output format (json, css, scss, style-dictionary, tailwind)",
|
|
2327
|
+
"json"
|
|
2328
|
+
).option("-o, --output <file>", "Output file path").option("-t, --token <token>", "Override authentication token").action(
|
|
2329
|
+
async (fileKey, options) => {
|
|
2330
|
+
const { io } = factory;
|
|
2331
|
+
try {
|
|
2332
|
+
const token = await factory.getToken(options.token);
|
|
2333
|
+
let client = await factory.getClient();
|
|
2334
|
+
if (options.token) {
|
|
2335
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
2336
|
+
client = new FigmaClient2({ token });
|
|
2337
|
+
}
|
|
2338
|
+
io.err.write(chalk16.dim("Fetching file styles...\n"));
|
|
2339
|
+
const stylesResponse = await client.getFileStyles(fileKey);
|
|
2340
|
+
const styles = stylesResponse.meta?.styles || [];
|
|
2341
|
+
if (styles.length === 0) {
|
|
2342
|
+
io.err.write(
|
|
2343
|
+
chalk16.yellow("No styles found in file. Trying variables...\n")
|
|
2344
|
+
);
|
|
2345
|
+
try {
|
|
2346
|
+
const variablesResponse = await client.getLocalVariables(fileKey);
|
|
2347
|
+
if (variablesResponse.meta?.variables) {
|
|
2348
|
+
io.err.write(
|
|
2349
|
+
chalk16.dim(
|
|
2350
|
+
"Found variables. Use 'figma variable export' for variable-based tokens.\n"
|
|
2351
|
+
)
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
} catch {
|
|
2355
|
+
}
|
|
2356
|
+
process.exit(0);
|
|
2357
|
+
}
|
|
2358
|
+
io.err.write(chalk16.dim("Extracting token values...\n"));
|
|
2359
|
+
const file = await client.getFile(fileKey);
|
|
2360
|
+
const tokens = extractStylesFromFile(file, styles);
|
|
2361
|
+
let output;
|
|
2362
|
+
switch (options.format) {
|
|
2363
|
+
case "css":
|
|
2364
|
+
output = tokensToCss(tokens);
|
|
2365
|
+
break;
|
|
2366
|
+
case "scss":
|
|
2367
|
+
output = tokensToScss(tokens);
|
|
2368
|
+
break;
|
|
2369
|
+
case "style-dictionary":
|
|
2370
|
+
output = tokensToStyleDictionary(tokens);
|
|
2371
|
+
break;
|
|
2372
|
+
case "tailwind":
|
|
2373
|
+
output = tokensToTailwind(tokens);
|
|
2374
|
+
break;
|
|
2375
|
+
default:
|
|
2376
|
+
output = JSON.stringify(tokens, null, 2);
|
|
2377
|
+
}
|
|
2378
|
+
if (options.output) {
|
|
2379
|
+
fs5.writeFileSync(options.output, output);
|
|
2380
|
+
io.out.write(chalk16.green(`\u2713 Exported to ${options.output}
|
|
2381
|
+
`));
|
|
2382
|
+
} else {
|
|
2383
|
+
io.out.write(output);
|
|
2384
|
+
io.out.write("\n");
|
|
2385
|
+
}
|
|
2386
|
+
} catch (error) {
|
|
2387
|
+
io.err.write(chalk16.red("Error exporting tokens.\n"));
|
|
2388
|
+
if (error instanceof Error) {
|
|
2389
|
+
io.err.write(chalk16.dim(`${error.message}
|
|
2390
|
+
`));
|
|
2391
|
+
}
|
|
2392
|
+
process.exit(1);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
);
|
|
2396
|
+
return cmd;
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
// src/cmd/export/icons.ts
|
|
2400
|
+
import { Command as Command22 } from "commander";
|
|
2401
|
+
import chalk17 from "chalk";
|
|
2402
|
+
function createExportIconsCommand(factory) {
|
|
2403
|
+
const cmd = new Command22("icons").description("Export icons from a Figma file").argument("<file-key>", "File key").option(
|
|
2404
|
+
"--frame <name>",
|
|
2405
|
+
"Name of frame/page containing icons (searches all pages if not specified)"
|
|
2406
|
+
).option("--format <format>", "Export format (svg, png)", "svg").option("-s, --scale <scale>", "Scale factor for PNG export", parseFloat, 1).option("-o, --output <dir>", "Output directory", "./icons").option("--prefix <prefix>", "Prefix for icon filenames", "icon-").option("-t, --token <token>", "Override authentication token").action(
|
|
2407
|
+
async (fileKey, options) => {
|
|
2408
|
+
const { io } = factory;
|
|
2409
|
+
const validFormats = ["svg", "png"];
|
|
2410
|
+
if (!validFormats.includes(options.format)) {
|
|
2411
|
+
io.err.write(
|
|
2412
|
+
chalk17.red(
|
|
2413
|
+
`Invalid format "${options.format}". Valid formats: ${validFormats.join(", ")}
|
|
2414
|
+
`
|
|
2415
|
+
)
|
|
2416
|
+
);
|
|
2417
|
+
process.exit(1);
|
|
2418
|
+
}
|
|
2419
|
+
const imageFormat = options.format;
|
|
2420
|
+
try {
|
|
2421
|
+
let findIcons2 = function(node, inTargetFrame) {
|
|
2422
|
+
if (options.frame) {
|
|
2423
|
+
if (node.name.toLowerCase() === options.frame.toLowerCase() && (node.type === "FRAME" || node.type === "CANVAS" || node.type === "COMPONENT")) {
|
|
2424
|
+
inTargetFrame = true;
|
|
2425
|
+
}
|
|
2426
|
+
} else {
|
|
2427
|
+
inTargetFrame = true;
|
|
2428
|
+
}
|
|
2429
|
+
if (inTargetFrame) {
|
|
2430
|
+
if (node.type === "COMPONENT" || node.type === "INSTANCE" || node.type === "FRAME" && !("children" in node && hasDeepChildren2(node))) {
|
|
2431
|
+
iconNodes.push({ id: node.id, name: node.name });
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
if ("children" in node) {
|
|
2435
|
+
const parent = node;
|
|
2436
|
+
for (const child of parent.children || []) {
|
|
2437
|
+
findIcons2(child, inTargetFrame);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
}, hasDeepChildren2 = function(node) {
|
|
2441
|
+
if (!("children" in node)) return false;
|
|
2442
|
+
const parent = node;
|
|
2443
|
+
return (parent.children || []).some(
|
|
2444
|
+
(child) => child.type === "FRAME" || child.type === "COMPONENT" || child.type === "GROUP"
|
|
2445
|
+
);
|
|
2446
|
+
};
|
|
2447
|
+
var findIcons = findIcons2, hasDeepChildren = hasDeepChildren2;
|
|
2448
|
+
const token = await factory.getToken(options.token);
|
|
2449
|
+
let client = await factory.getClient();
|
|
2450
|
+
if (options.token) {
|
|
2451
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
2452
|
+
client = new FigmaClient2({ token });
|
|
2453
|
+
}
|
|
2454
|
+
io.err.write(chalk17.dim("Fetching file structure...\n"));
|
|
2455
|
+
const file = await client.getFile(fileKey, { depth: 2 });
|
|
2456
|
+
const iconNodes = [];
|
|
2457
|
+
if (file.document) {
|
|
2458
|
+
findIcons2(file.document, false);
|
|
2459
|
+
}
|
|
2460
|
+
if (iconNodes.length === 0) {
|
|
2461
|
+
io.err.write(chalk17.yellow("No icons found in file.\n"));
|
|
2462
|
+
if (options.frame) {
|
|
2463
|
+
io.err.write(
|
|
2464
|
+
chalk17.dim(`Looking for frame: "${options.frame}"
|
|
2465
|
+
`)
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
process.exit(0);
|
|
2469
|
+
}
|
|
2470
|
+
io.err.write(
|
|
2471
|
+
chalk17.dim(`Found ${iconNodes.length} icons. Exporting...
|
|
2472
|
+
`)
|
|
2473
|
+
);
|
|
2474
|
+
const nodeIds = iconNodes.map((n) => n.id);
|
|
2475
|
+
const imagesResponse = await client.getImages(fileKey, nodeIds, {
|
|
2476
|
+
format: imageFormat,
|
|
2477
|
+
scale: options.scale,
|
|
2478
|
+
svg_include_id: false,
|
|
2479
|
+
svg_simplify_stroke: true
|
|
2480
|
+
});
|
|
2481
|
+
const imageUrls = imagesResponse.images || {};
|
|
2482
|
+
const downloadItems = iconNodes.map((node) => ({
|
|
2483
|
+
id: node.id,
|
|
2484
|
+
name: node.name,
|
|
2485
|
+
url: imageUrls[node.id] ?? null,
|
|
2486
|
+
filename: `${options.prefix}${toKebabCase2(node.name)}.${imageFormat}`
|
|
2487
|
+
}));
|
|
2488
|
+
const results = await downloadFiles(downloadItems, options.output);
|
|
2489
|
+
const successCount = results.filter(
|
|
2490
|
+
(r) => r.status === "success"
|
|
2491
|
+
).length;
|
|
2492
|
+
const failures = results.filter((r) => r.status === "error");
|
|
2493
|
+
for (const failure of failures) {
|
|
2494
|
+
io.err.write(
|
|
2495
|
+
chalk17.yellow(
|
|
2496
|
+
`Warning: Failed to download "${failure.name ?? failure.id}": ${failure.error}
|
|
2497
|
+
`
|
|
2498
|
+
)
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
io.err.write(
|
|
2502
|
+
chalk17.green(`Exported ${successCount} icons to ${options.output}
|
|
2503
|
+
`)
|
|
2504
|
+
);
|
|
2505
|
+
if (failures.length > 0) {
|
|
2506
|
+
io.err.write(chalk17.yellow(`${failures.length} icons failed
|
|
2507
|
+
`));
|
|
2508
|
+
process.exit(1);
|
|
2509
|
+
}
|
|
2510
|
+
} catch (error) {
|
|
2511
|
+
io.err.write(chalk17.red("Error exporting icons.\n"));
|
|
2512
|
+
if (error instanceof Error) {
|
|
2513
|
+
io.err.write(chalk17.dim(`${error.message}
|
|
2514
|
+
`));
|
|
2515
|
+
}
|
|
2516
|
+
process.exit(1);
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
);
|
|
2520
|
+
return cmd;
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
// src/cmd/export/theme.ts
|
|
2524
|
+
import { Command as Command23 } from "commander";
|
|
2525
|
+
import chalk18 from "chalk";
|
|
2526
|
+
import * as fs6 from "fs";
|
|
2527
|
+
import * as path4 from "path";
|
|
2528
|
+
function createExportThemeCommand(factory) {
|
|
2529
|
+
const cmd = new Command23("theme").description("Export complete theme package from a Figma file").argument("<file-key>", "File key").option("-o, --output <dir>", "Output directory", "./theme").option(
|
|
2530
|
+
"--formats <formats>",
|
|
2531
|
+
"Comma-separated list of formats (json, css, scss, style-dictionary, tailwind)",
|
|
2532
|
+
"json,css"
|
|
2533
|
+
).option("-t, --token <token>", "Override authentication token").action(
|
|
2534
|
+
async (fileKey, options) => {
|
|
2535
|
+
const { io } = factory;
|
|
2536
|
+
try {
|
|
2537
|
+
const token = await factory.getToken(options.token);
|
|
2538
|
+
let client = await factory.getClient();
|
|
2539
|
+
if (options.token) {
|
|
2540
|
+
const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
2541
|
+
client = new FigmaClient2({ token });
|
|
2542
|
+
}
|
|
2543
|
+
io.err.write(chalk18.dim("Fetching file styles...\n"));
|
|
2544
|
+
const stylesResponse = await client.getFileStyles(fileKey);
|
|
2545
|
+
const styles = stylesResponse.meta?.styles || [];
|
|
2546
|
+
io.err.write(chalk18.dim("Extracting token values...\n"));
|
|
2547
|
+
const file = await client.getFile(fileKey);
|
|
2548
|
+
const tokens = extractStylesFromFile(file, styles);
|
|
2549
|
+
if (!fs6.existsSync(options.output)) {
|
|
2550
|
+
fs6.mkdirSync(options.output, { recursive: true });
|
|
2551
|
+
}
|
|
2552
|
+
const formats = options.formats.split(",").map((f) => f.trim());
|
|
2553
|
+
const outputs = [];
|
|
2554
|
+
for (const format of formats) {
|
|
2555
|
+
let output;
|
|
2556
|
+
let filename;
|
|
2557
|
+
switch (format) {
|
|
2558
|
+
case "css":
|
|
2559
|
+
output = tokensToCss(tokens);
|
|
2560
|
+
filename = "tokens.css";
|
|
2561
|
+
break;
|
|
2562
|
+
case "scss":
|
|
2563
|
+
output = tokensToScss(tokens);
|
|
2564
|
+
filename = "_tokens.scss";
|
|
2565
|
+
break;
|
|
2566
|
+
case "style-dictionary":
|
|
2567
|
+
output = tokensToStyleDictionary(tokens);
|
|
2568
|
+
filename = "tokens.style-dictionary.json";
|
|
2569
|
+
break;
|
|
2570
|
+
case "tailwind":
|
|
2571
|
+
output = tokensToTailwind(tokens);
|
|
2572
|
+
filename = "tailwind.tokens.js";
|
|
2573
|
+
break;
|
|
2574
|
+
case "json":
|
|
2575
|
+
default:
|
|
2576
|
+
output = JSON.stringify(tokens, null, 2);
|
|
2577
|
+
filename = "tokens.json";
|
|
2578
|
+
}
|
|
2579
|
+
const filepath = path4.join(options.output, filename);
|
|
2580
|
+
fs6.writeFileSync(filepath, output);
|
|
2581
|
+
outputs.push(filename);
|
|
2582
|
+
}
|
|
2583
|
+
const metadata = {
|
|
2584
|
+
name: file.name,
|
|
2585
|
+
lastModified: file.lastModified,
|
|
2586
|
+
version: file.version,
|
|
2587
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2588
|
+
formats
|
|
2589
|
+
};
|
|
2590
|
+
fs6.writeFileSync(
|
|
2591
|
+
path4.join(options.output, "metadata.json"),
|
|
2592
|
+
JSON.stringify(metadata, null, 2)
|
|
2593
|
+
);
|
|
2594
|
+
io.out.write(chalk18.green(`\u2713 Theme exported to ${options.output}
|
|
2595
|
+
`));
|
|
2596
|
+
io.out.write(chalk18.dim(" Files:\n"));
|
|
2597
|
+
for (const filename of outputs) {
|
|
2598
|
+
io.out.write(chalk18.dim(` - ${filename}
|
|
2599
|
+
`));
|
|
2600
|
+
}
|
|
2601
|
+
io.out.write(chalk18.dim(" - metadata.json\n"));
|
|
2602
|
+
} catch (error) {
|
|
2603
|
+
io.err.write(chalk18.red("Error exporting theme.\n"));
|
|
2604
|
+
if (error instanceof Error) {
|
|
2605
|
+
io.err.write(chalk18.dim(`${error.message}
|
|
2606
|
+
`));
|
|
2607
|
+
}
|
|
2608
|
+
process.exit(1);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
);
|
|
2612
|
+
return cmd;
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
// src/cmd/export/index.ts
|
|
2616
|
+
function createExportCommand(factory) {
|
|
2617
|
+
const cmd = new Command24("export").description(
|
|
2618
|
+
"Design-to-code export commands"
|
|
2619
|
+
);
|
|
2620
|
+
cmd.addCommand(createExportTokensCommand(factory));
|
|
2621
|
+
cmd.addCommand(createExportIconsCommand(factory));
|
|
2622
|
+
cmd.addCommand(createExportThemeCommand(factory));
|
|
2623
|
+
return cmd;
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
// src/cmd/api/index.ts
|
|
2627
|
+
init_client();
|
|
2628
|
+
import { Command as Command25 } from "commander";
|
|
2629
|
+
import chalk19 from "chalk";
|
|
2630
|
+
function createApiCommand(factory) {
|
|
2631
|
+
const cmd = new Command25("api").description("Make direct API requests to Figma").argument("<path>", "API path (e.g., /v1/files/:key)").option("-X, --method <method>", "HTTP method", "GET").option("-d, --data <json>", "Request body as JSON").option("-q, --query <params>", "Query parameters (key=value,key2=value2)").option("-t, --token <token>", "Override authentication token").option("--raw", "Output raw response without formatting").action(
|
|
2632
|
+
async (apiPath, options) => {
|
|
2633
|
+
const { io } = factory;
|
|
2634
|
+
try {
|
|
2635
|
+
const token = await factory.getToken(options.token);
|
|
2636
|
+
const client = new FigmaClient({
|
|
2637
|
+
token,
|
|
2638
|
+
baseUrl: "https://api.figma.com"
|
|
2639
|
+
});
|
|
2640
|
+
let params;
|
|
2641
|
+
if (options.query) {
|
|
2642
|
+
params = {};
|
|
2643
|
+
const pairs = options.query.split(",");
|
|
2644
|
+
for (const pair of pairs) {
|
|
2645
|
+
const [key, value] = pair.split("=");
|
|
2646
|
+
if (key && value !== void 0) {
|
|
2647
|
+
params[key.trim()] = value.trim();
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
let body;
|
|
2652
|
+
if (options.data) {
|
|
2653
|
+
try {
|
|
2654
|
+
body = JSON.parse(options.data);
|
|
2655
|
+
} catch {
|
|
2656
|
+
io.err.write(chalk19.red("Error: Invalid JSON in --data\n"));
|
|
2657
|
+
process.exit(1);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
const response = await client.api(options.method, apiPath, {
|
|
2661
|
+
params,
|
|
2662
|
+
body
|
|
2663
|
+
});
|
|
2664
|
+
if (options.raw) {
|
|
2665
|
+
io.out.write(JSON.stringify(response));
|
|
2666
|
+
} else {
|
|
2667
|
+
io.out.write(JSON.stringify(response, null, 2));
|
|
2668
|
+
}
|
|
2669
|
+
io.out.write("\n");
|
|
2670
|
+
} catch (error) {
|
|
2671
|
+
if (error instanceof FigmaApiError) {
|
|
2672
|
+
io.err.write(
|
|
2673
|
+
chalk19.red(`API Error: ${error.status} ${error.statusText}
|
|
2674
|
+
`)
|
|
2675
|
+
);
|
|
2676
|
+
if (error.body) {
|
|
2677
|
+
io.err.write(chalk19.dim(JSON.stringify(error.body, null, 2)));
|
|
2678
|
+
io.err.write("\n");
|
|
2679
|
+
}
|
|
2680
|
+
} else if (error instanceof Error) {
|
|
2681
|
+
io.err.write(chalk19.red(`Error: ${error.message}
|
|
2682
|
+
`));
|
|
2683
|
+
}
|
|
2684
|
+
process.exit(1);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
);
|
|
2688
|
+
cmd.addHelpText(
|
|
2689
|
+
"after",
|
|
2690
|
+
`
|
|
2691
|
+
Examples:
|
|
2692
|
+
$ figma api /v1/me
|
|
2693
|
+
$ figma api /v1/files/abc123
|
|
2694
|
+
$ figma api /v1/teams/12345/projects
|
|
2695
|
+
$ figma api /v1/files/abc123/comments -X POST -d '{"message":"Hello"}'
|
|
2696
|
+
$ figma api /v1/files/abc123/images -q "ids=1:2,format=svg"
|
|
2697
|
+
`
|
|
2698
|
+
);
|
|
2699
|
+
return cmd;
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
// src/cmd/root.ts
|
|
2703
|
+
function createRootCommand(factory) {
|
|
2704
|
+
const program = new Command26();
|
|
2705
|
+
program.name("figma").description("CLI for the Figma API").version("1.0.0").configureHelp({
|
|
2706
|
+
sortSubcommands: true,
|
|
2707
|
+
sortOptions: true
|
|
2708
|
+
});
|
|
2709
|
+
program.addCommand(createAuthCommand(factory));
|
|
2710
|
+
program.addCommand(createFileCommand(factory));
|
|
2711
|
+
program.addCommand(createProjectCommand(factory));
|
|
2712
|
+
program.addCommand(createComponentCommand(factory));
|
|
2713
|
+
program.addCommand(createStyleCommand(factory));
|
|
2714
|
+
program.addCommand(createVariableCommand(factory));
|
|
2715
|
+
program.addCommand(createExportCommand(factory));
|
|
2716
|
+
program.addCommand(createApiCommand(factory));
|
|
2717
|
+
return program;
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
// src/main.ts
|
|
2721
|
+
async function main() {
|
|
2722
|
+
const factory = createFactory();
|
|
2723
|
+
const program = createRootCommand(factory);
|
|
2724
|
+
try {
|
|
2725
|
+
await program.parseAsync(process.argv);
|
|
2726
|
+
} catch (error) {
|
|
2727
|
+
if (error instanceof Error) {
|
|
2728
|
+
console.error(error.message);
|
|
2729
|
+
}
|
|
2730
|
+
process.exit(1);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
main();
|
|
2734
|
+
//# sourceMappingURL=main.js.map
|