@spectratools/figma-cli 0.2.0 → 0.3.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/dist/cli.js +1151 -4
- package/dist/index.js +1010 -26
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,32 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { readFileSync, realpathSync } from "fs";
|
|
5
|
-
import { dirname, resolve } from "path";
|
|
5
|
+
import { dirname, resolve as resolve2 } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { initTelemetry, shutdownTelemetry } from "@spectratools/cli-shared/telemetry";
|
|
8
|
-
import { Cli } from "incur";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
version: pkg.version,
|
|
13
|
-
description: "Query Figma REST API data from the command line."
|
|
14
|
-
});
|
|
15
|
-
var isMain = (() => {
|
|
16
|
-
const entrypoint = process.argv[1];
|
|
17
|
-
if (!entrypoint) {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
try {
|
|
21
|
-
return realpathSync(entrypoint) === realpathSync(fileURLToPath(import.meta.url));
|
|
22
|
-
} catch {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
})();
|
|
26
|
-
if (isMain) {
|
|
27
|
-
initTelemetry("figma");
|
|
28
|
-
process.on("beforeExit", () => shutdownTelemetry());
|
|
29
|
-
cli.serve();
|
|
30
|
-
}
|
|
8
|
+
import { Cli as Cli7 } from "incur";
|
|
9
|
+
|
|
10
|
+
// src/commands/comments.ts
|
|
11
|
+
import { Cli, z as z4 } from "incur";
|
|
31
12
|
|
|
32
13
|
// src/api.ts
|
|
33
14
|
import {
|
|
@@ -61,12 +42,18 @@ var styleSchema = z.object({
|
|
|
61
42
|
style_type: z.string(),
|
|
62
43
|
description: z.string().optional()
|
|
63
44
|
});
|
|
45
|
+
var clientMetaSchema = z.object({
|
|
46
|
+
node_id: z.string().optional(),
|
|
47
|
+
node_offset: z.object({ x: z.number(), y: z.number() }).optional()
|
|
48
|
+
}).optional();
|
|
64
49
|
var commentSchema = z.object({
|
|
65
50
|
id: z.string(),
|
|
66
51
|
message: z.string(),
|
|
67
52
|
created_at: z.string(),
|
|
53
|
+
resolved_at: z.string().nullable().optional(),
|
|
68
54
|
user: userSchema,
|
|
69
|
-
order_id: z.union([z.string(), z.number()]).optional()
|
|
55
|
+
order_id: z.union([z.string(), z.number()]).optional(),
|
|
56
|
+
client_meta: clientMetaSchema
|
|
70
57
|
});
|
|
71
58
|
var fileMetaSchema = z.object({
|
|
72
59
|
name: z.string(),
|
|
@@ -189,13 +176,1010 @@ function createFigmaClient(apiKey) {
|
|
|
189
176
|
};
|
|
190
177
|
}
|
|
191
178
|
|
|
179
|
+
// src/commands/_common.ts
|
|
180
|
+
import { z as z3 } from "incur";
|
|
181
|
+
|
|
192
182
|
// src/auth.ts
|
|
193
183
|
import { z as z2 } from "incur";
|
|
194
184
|
var figmaEnv = z2.object({
|
|
195
185
|
FIGMA_API_KEY: z2.string().describe("Figma personal access token")
|
|
196
186
|
});
|
|
187
|
+
|
|
188
|
+
// src/commands/_common.ts
|
|
189
|
+
var fileKeyArg = z3.string().describe("Figma file key (from the file URL)");
|
|
190
|
+
var formatOption = z3.enum(["json", "table"]).default("json").describe("Output format: json or table");
|
|
191
|
+
function outputFormatter(data, format) {
|
|
192
|
+
if (format === "json") {
|
|
193
|
+
return JSON.stringify(data, null, 2);
|
|
194
|
+
}
|
|
195
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
196
|
+
if (rows.length === 0) return "(no data)";
|
|
197
|
+
const keys = Object.keys(rows[0]);
|
|
198
|
+
const widths = keys.map((k) => Math.max(k.length, ...rows.map((r) => String(r[k] ?? "").length)));
|
|
199
|
+
const header = keys.map((k, i) => k.padEnd(widths[i])).join(" ");
|
|
200
|
+
const separator = widths.map((w) => "-".repeat(w)).join(" ");
|
|
201
|
+
const body = rows.map((r) => keys.map((k, i) => String(r[k] ?? "").padEnd(widths[i])).join(" ")).join("\n");
|
|
202
|
+
return `${header}
|
|
203
|
+
${separator}
|
|
204
|
+
${body}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/commands/comments.ts
|
|
208
|
+
var commentsCli = Cli.create("comments", {
|
|
209
|
+
description: "List and post comments on Figma files."
|
|
210
|
+
});
|
|
211
|
+
commentsCli.command("list", {
|
|
212
|
+
description: "List all comments on a Figma file.",
|
|
213
|
+
args: z4.object({ fileKey: fileKeyArg }),
|
|
214
|
+
options: z4.object({ format: formatOption }),
|
|
215
|
+
env: figmaEnv,
|
|
216
|
+
output: z4.unknown(),
|
|
217
|
+
examples: [
|
|
218
|
+
{
|
|
219
|
+
args: { fileKey: "abc123" },
|
|
220
|
+
options: { format: "json" },
|
|
221
|
+
description: "List comments as JSON"
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
args: { fileKey: "abc123" },
|
|
225
|
+
options: { format: "table" },
|
|
226
|
+
description: "List comments as table"
|
|
227
|
+
}
|
|
228
|
+
],
|
|
229
|
+
async run(c) {
|
|
230
|
+
const fileKey = c.args.fileKey;
|
|
231
|
+
const client = createFigmaClient(c.env.FIGMA_API_KEY);
|
|
232
|
+
const response = await client.getComments(fileKey);
|
|
233
|
+
const comments = response.comments.map((comment) => ({
|
|
234
|
+
id: comment.id,
|
|
235
|
+
author: comment.user.handle,
|
|
236
|
+
message: comment.message,
|
|
237
|
+
createdAt: comment.created_at,
|
|
238
|
+
resolved: comment.resolved_at != null,
|
|
239
|
+
nodeId: comment.client_meta?.node_id ?? null
|
|
240
|
+
}));
|
|
241
|
+
if (comments.length === 0) {
|
|
242
|
+
return c.ok(
|
|
243
|
+
c.options.format === "json" ? { comments: [], message: "No comments found." } : "No comments found."
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (c.options.format === "json") {
|
|
247
|
+
return c.ok({ comments, total: comments.length });
|
|
248
|
+
}
|
|
249
|
+
return c.ok(
|
|
250
|
+
outputFormatter(
|
|
251
|
+
comments.map((co) => ({
|
|
252
|
+
id: co.id,
|
|
253
|
+
author: co.author,
|
|
254
|
+
message: co.message.length > 50 ? `${co.message.slice(0, 47)}...` : co.message,
|
|
255
|
+
createdAt: co.createdAt,
|
|
256
|
+
resolved: co.resolved ? "yes" : "no",
|
|
257
|
+
nodeId: co.nodeId ?? "-"
|
|
258
|
+
})),
|
|
259
|
+
"table"
|
|
260
|
+
)
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
commentsCli.command("post", {
|
|
265
|
+
description: "Post a comment on a Figma file.",
|
|
266
|
+
args: z4.object({ fileKey: fileKeyArg }),
|
|
267
|
+
options: z4.object({
|
|
268
|
+
message: z4.string().describe("Comment text (required)"),
|
|
269
|
+
"node-id": z4.string().optional().describe("Pin comment to a specific node ID")
|
|
270
|
+
}),
|
|
271
|
+
env: figmaEnv,
|
|
272
|
+
output: z4.unknown(),
|
|
273
|
+
examples: [
|
|
274
|
+
{
|
|
275
|
+
args: { fileKey: "abc123" },
|
|
276
|
+
options: { message: "Looks good!" },
|
|
277
|
+
description: "Post a general comment"
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
args: { fileKey: "abc123" },
|
|
281
|
+
options: { message: "Check this spacing", "node-id": "1:42" },
|
|
282
|
+
description: "Post a comment pinned to a node"
|
|
283
|
+
}
|
|
284
|
+
],
|
|
285
|
+
async run(c) {
|
|
286
|
+
const fileKey = c.args.fileKey;
|
|
287
|
+
const { message, "node-id": nodeId } = c.options;
|
|
288
|
+
if (!message || message.trim().length === 0) {
|
|
289
|
+
return c.error({
|
|
290
|
+
code: "VALIDATION_ERROR",
|
|
291
|
+
message: "--message is required and cannot be empty"
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
const client = createFigmaClient(c.env.FIGMA_API_KEY);
|
|
295
|
+
const comment = await client.postComment(fileKey, message, nodeId);
|
|
296
|
+
return c.ok({
|
|
297
|
+
id: comment.id,
|
|
298
|
+
message: comment.message,
|
|
299
|
+
author: comment.user.handle,
|
|
300
|
+
createdAt: comment.created_at,
|
|
301
|
+
permalink: `https://www.figma.com/file/${fileKey}?comment=${comment.id}`
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// src/commands/components.ts
|
|
307
|
+
import { Cli as Cli2, z as z5 } from "incur";
|
|
308
|
+
var componentsCli = Cli2.create("components", {
|
|
309
|
+
description: "List and inspect published Figma components."
|
|
310
|
+
});
|
|
311
|
+
var componentOutputSchema = z5.object({
|
|
312
|
+
key: z5.string(),
|
|
313
|
+
name: z5.string(),
|
|
314
|
+
description: z5.string()
|
|
315
|
+
});
|
|
316
|
+
componentsCli.command("list", {
|
|
317
|
+
description: "List published components in a Figma file.",
|
|
318
|
+
args: z5.object({
|
|
319
|
+
fileKey: z5.string().describe("Figma file key")
|
|
320
|
+
}),
|
|
321
|
+
options: z5.object({}),
|
|
322
|
+
env: figmaEnv,
|
|
323
|
+
output: z5.object({
|
|
324
|
+
components: z5.array(componentOutputSchema),
|
|
325
|
+
total: z5.number()
|
|
326
|
+
}),
|
|
327
|
+
examples: [
|
|
328
|
+
{
|
|
329
|
+
args: { fileKey: "abc123FileKey" },
|
|
330
|
+
options: {},
|
|
331
|
+
description: "List all published components"
|
|
332
|
+
}
|
|
333
|
+
],
|
|
334
|
+
async run(c) {
|
|
335
|
+
const { fileKey } = c.args;
|
|
336
|
+
const client = createFigmaClient(c.env.FIGMA_API_KEY);
|
|
337
|
+
const response = await client.getFileComponents(fileKey);
|
|
338
|
+
const components = response.meta.components;
|
|
339
|
+
return c.ok(
|
|
340
|
+
{
|
|
341
|
+
components: components.map((comp) => ({
|
|
342
|
+
key: comp.key,
|
|
343
|
+
name: comp.name,
|
|
344
|
+
description: comp.description
|
|
345
|
+
})),
|
|
346
|
+
total: components.length
|
|
347
|
+
},
|
|
348
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
349
|
+
cta: {
|
|
350
|
+
commands: components.length > 0 ? [
|
|
351
|
+
{
|
|
352
|
+
command: `components get ${fileKey} ${components[0]?.key}`,
|
|
353
|
+
description: `Get details for "${components[0]?.name}"`
|
|
354
|
+
}
|
|
355
|
+
] : [
|
|
356
|
+
{
|
|
357
|
+
command: `tokens export ${fileKey}`,
|
|
358
|
+
description: "Export design tokens from this file"
|
|
359
|
+
}
|
|
360
|
+
]
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
componentsCli.command("get", {
|
|
367
|
+
description: "Get details for a specific published component.",
|
|
368
|
+
args: z5.object({
|
|
369
|
+
fileKey: z5.string().describe("Figma file key"),
|
|
370
|
+
componentKey: z5.string().describe("Component key")
|
|
371
|
+
}),
|
|
372
|
+
options: z5.object({}),
|
|
373
|
+
env: figmaEnv,
|
|
374
|
+
output: z5.object({
|
|
375
|
+
key: z5.string(),
|
|
376
|
+
name: z5.string(),
|
|
377
|
+
description: z5.string(),
|
|
378
|
+
found: z5.boolean()
|
|
379
|
+
}),
|
|
380
|
+
examples: [
|
|
381
|
+
{
|
|
382
|
+
args: { fileKey: "abc123FileKey", componentKey: "comp:456" },
|
|
383
|
+
options: {},
|
|
384
|
+
description: "Get details for a specific component"
|
|
385
|
+
}
|
|
386
|
+
],
|
|
387
|
+
async run(c) {
|
|
388
|
+
const { fileKey, componentKey } = c.args;
|
|
389
|
+
const client = createFigmaClient(c.env.FIGMA_API_KEY);
|
|
390
|
+
const response = await client.getFileComponents(fileKey);
|
|
391
|
+
const match = response.meta.components.find((comp) => comp.key === componentKey);
|
|
392
|
+
if (!match) {
|
|
393
|
+
return c.ok(
|
|
394
|
+
{
|
|
395
|
+
key: componentKey,
|
|
396
|
+
name: "",
|
|
397
|
+
description: "",
|
|
398
|
+
found: false
|
|
399
|
+
},
|
|
400
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
401
|
+
cta: {
|
|
402
|
+
commands: [
|
|
403
|
+
{
|
|
404
|
+
command: `components list ${fileKey}`,
|
|
405
|
+
description: "List all available components"
|
|
406
|
+
}
|
|
407
|
+
]
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
return c.ok(
|
|
413
|
+
{
|
|
414
|
+
key: match.key,
|
|
415
|
+
name: match.name,
|
|
416
|
+
description: match.description,
|
|
417
|
+
found: true
|
|
418
|
+
},
|
|
419
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
420
|
+
cta: {
|
|
421
|
+
commands: [
|
|
422
|
+
{
|
|
423
|
+
command: `components list ${fileKey}`,
|
|
424
|
+
description: "List all components"
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
command: `tokens export ${fileKey}`,
|
|
428
|
+
description: "Export design tokens"
|
|
429
|
+
}
|
|
430
|
+
]
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// src/commands/files.ts
|
|
438
|
+
import { Cli as Cli3, z as z6 } from "incur";
|
|
439
|
+
var pageSchema = z6.object({
|
|
440
|
+
id: z6.string(),
|
|
441
|
+
name: z6.string()
|
|
442
|
+
});
|
|
443
|
+
var fileGetOutputSchema = z6.object({
|
|
444
|
+
name: z6.string(),
|
|
445
|
+
lastModified: z6.string(),
|
|
446
|
+
version: z6.string(),
|
|
447
|
+
thumbnailUrl: z6.string().optional(),
|
|
448
|
+
pages: z6.array(pageSchema)
|
|
449
|
+
});
|
|
450
|
+
var projectFileOutputSchema = z6.object({
|
|
451
|
+
key: z6.string(),
|
|
452
|
+
name: z6.string(),
|
|
453
|
+
lastModified: z6.string(),
|
|
454
|
+
thumbnailUrl: z6.string().optional()
|
|
455
|
+
});
|
|
456
|
+
var filesListOutputSchema = z6.object({
|
|
457
|
+
projectName: z6.string(),
|
|
458
|
+
files: z6.array(projectFileOutputSchema),
|
|
459
|
+
count: z6.number()
|
|
460
|
+
});
|
|
461
|
+
var filesCli = Cli3.create("files", {
|
|
462
|
+
description: "Query Figma file metadata."
|
|
463
|
+
});
|
|
464
|
+
filesCli.command("get", {
|
|
465
|
+
description: "Get metadata for a Figma file.",
|
|
466
|
+
args: z6.object({
|
|
467
|
+
fileKey: fileKeyArg
|
|
468
|
+
}),
|
|
469
|
+
options: z6.object({
|
|
470
|
+
format: formatOption
|
|
471
|
+
}),
|
|
472
|
+
env: figmaEnv,
|
|
473
|
+
output: fileGetOutputSchema,
|
|
474
|
+
examples: [
|
|
475
|
+
{
|
|
476
|
+
args: { fileKey: "abc123XYZ" },
|
|
477
|
+
description: "Get file metadata for a Figma file"
|
|
478
|
+
}
|
|
479
|
+
],
|
|
480
|
+
async run(c) {
|
|
481
|
+
const client = createFigmaClient(c.env.FIGMA_API_KEY);
|
|
482
|
+
const file = await client.getFile(c.args.fileKey, { depth: 1 });
|
|
483
|
+
const doc = file.document;
|
|
484
|
+
const pages = doc?.children?.map((child) => ({ id: child.id, name: child.name })) ?? [];
|
|
485
|
+
const result = {
|
|
486
|
+
name: file.name,
|
|
487
|
+
lastModified: file.lastModified,
|
|
488
|
+
version: file.version,
|
|
489
|
+
thumbnailUrl: void 0,
|
|
490
|
+
pages
|
|
491
|
+
};
|
|
492
|
+
if (c.options.format === "table") {
|
|
493
|
+
process.stdout.write(
|
|
494
|
+
`${outputFormatter(result, "table")}
|
|
495
|
+
`
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
return c.ok(
|
|
499
|
+
result,
|
|
500
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
501
|
+
cta: {
|
|
502
|
+
commands: [
|
|
503
|
+
{
|
|
504
|
+
command: "nodes get",
|
|
505
|
+
args: { fileKey: c.args.fileKey, nodeId: pages[0]?.id ?? "<node-id>" },
|
|
506
|
+
description: "Inspect a node in this file"
|
|
507
|
+
}
|
|
508
|
+
]
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
filesCli.command("list", {
|
|
515
|
+
description: "List files in a Figma project.",
|
|
516
|
+
options: z6.object({
|
|
517
|
+
"project-id": z6.string().describe("Figma project ID"),
|
|
518
|
+
format: formatOption
|
|
519
|
+
}),
|
|
520
|
+
env: figmaEnv,
|
|
521
|
+
output: filesListOutputSchema,
|
|
522
|
+
examples: [
|
|
523
|
+
{
|
|
524
|
+
options: { "project-id": "12345" },
|
|
525
|
+
description: "List files in a Figma project"
|
|
526
|
+
}
|
|
527
|
+
],
|
|
528
|
+
async run(c) {
|
|
529
|
+
const projectId = c.options["project-id"];
|
|
530
|
+
const client = createFigmaClient(c.env.FIGMA_API_KEY);
|
|
531
|
+
const project = await client.getProjectFiles(projectId);
|
|
532
|
+
const files = project.files.map((f) => ({
|
|
533
|
+
key: f.key,
|
|
534
|
+
name: f.name,
|
|
535
|
+
lastModified: f.last_modified,
|
|
536
|
+
thumbnailUrl: f.thumbnail_url
|
|
537
|
+
}));
|
|
538
|
+
const result = {
|
|
539
|
+
projectName: project.name,
|
|
540
|
+
files,
|
|
541
|
+
count: files.length
|
|
542
|
+
};
|
|
543
|
+
if (c.options.format === "table") {
|
|
544
|
+
process.stdout.write(`${outputFormatter(files, "table")}
|
|
545
|
+
`);
|
|
546
|
+
}
|
|
547
|
+
return c.ok(
|
|
548
|
+
result,
|
|
549
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
550
|
+
cta: {
|
|
551
|
+
commands: files[0] ? [
|
|
552
|
+
{
|
|
553
|
+
command: "files get",
|
|
554
|
+
args: { fileKey: files[0].key },
|
|
555
|
+
description: `Get metadata for "${files[0].name}"`
|
|
556
|
+
}
|
|
557
|
+
] : []
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// src/commands/frames.ts
|
|
565
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
566
|
+
import { resolve } from "path";
|
|
567
|
+
import { Cli as Cli4, z as z7 } from "incur";
|
|
568
|
+
var framesCli = Cli4.create("frames", {
|
|
569
|
+
description: "Export frame metadata and render frame images from Figma files."
|
|
570
|
+
});
|
|
571
|
+
function extractFrames(document, pageFilter) {
|
|
572
|
+
const doc = document;
|
|
573
|
+
const frames = [];
|
|
574
|
+
for (const page of doc.children ?? []) {
|
|
575
|
+
if (page.type !== "CANVAS") continue;
|
|
576
|
+
if (pageFilter && page.name !== pageFilter) continue;
|
|
577
|
+
for (const child of page.children ?? []) {
|
|
578
|
+
if (child.type !== "FRAME") continue;
|
|
579
|
+
frames.push({
|
|
580
|
+
name: child.name,
|
|
581
|
+
nodeId: child.id,
|
|
582
|
+
page: page.name,
|
|
583
|
+
width: child.absoluteBoundingBox?.width ?? 0,
|
|
584
|
+
height: child.absoluteBoundingBox?.height ?? 0
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return frames;
|
|
589
|
+
}
|
|
590
|
+
async function downloadFile(url) {
|
|
591
|
+
const res = await fetch(url);
|
|
592
|
+
if (!res.ok) {
|
|
593
|
+
throw new Error(`Download failed (${res.status}): ${url}`);
|
|
594
|
+
}
|
|
595
|
+
return Buffer.from(await res.arrayBuffer());
|
|
596
|
+
}
|
|
597
|
+
framesCli.command("export", {
|
|
598
|
+
description: "List all top-level frames in a Figma file.",
|
|
599
|
+
args: z7.object({ fileKey: fileKeyArg }),
|
|
600
|
+
options: z7.object({
|
|
601
|
+
page: z7.string().optional().describe("Filter to frames on a specific page"),
|
|
602
|
+
format: formatOption
|
|
603
|
+
}),
|
|
604
|
+
env: figmaEnv,
|
|
605
|
+
output: z7.unknown(),
|
|
606
|
+
examples: [
|
|
607
|
+
{
|
|
608
|
+
args: { fileKey: "abc123" },
|
|
609
|
+
options: { format: "json" },
|
|
610
|
+
description: "List frames in a file"
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
args: { fileKey: "abc123" },
|
|
614
|
+
options: { page: "Home", format: "table" },
|
|
615
|
+
description: "List frames on the Home page"
|
|
616
|
+
}
|
|
617
|
+
],
|
|
618
|
+
async run(c) {
|
|
619
|
+
const fileKey = c.args.fileKey;
|
|
620
|
+
const client = createFigmaClient(c.env.FIGMA_API_KEY);
|
|
621
|
+
const file = await client.getFile(fileKey);
|
|
622
|
+
const frames = extractFrames(file.document, c.options.page);
|
|
623
|
+
if (frames.length === 0) {
|
|
624
|
+
const msg = c.options.page ? `No frames found on page "${c.options.page}".` : "No top-level frames found in this file.";
|
|
625
|
+
return c.ok(c.options.format === "json" ? { frames: [], message: msg } : msg);
|
|
626
|
+
}
|
|
627
|
+
if (c.options.format === "json") {
|
|
628
|
+
return c.ok({ frames, total: frames.length });
|
|
629
|
+
}
|
|
630
|
+
return c.ok(outputFormatter(frames, "table"));
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
framesCli.command("render", {
|
|
634
|
+
description: "Download rendered images of specific frames from a Figma file.",
|
|
635
|
+
args: z7.object({ fileKey: fileKeyArg }),
|
|
636
|
+
options: z7.object({
|
|
637
|
+
ids: z7.string().describe("Comma-separated node IDs to render"),
|
|
638
|
+
"image-format": z7.enum(["png", "svg"]).default("png").describe("Image format: png or svg (default: png)"),
|
|
639
|
+
scale: z7.coerce.number().min(1).max(4).default(2).describe("Image scale 1-4 (default: 2)"),
|
|
640
|
+
output: z7.string().default(".").describe("Output directory (default: current directory)")
|
|
641
|
+
}),
|
|
642
|
+
env: figmaEnv,
|
|
643
|
+
output: z7.unknown(),
|
|
644
|
+
examples: [
|
|
645
|
+
{
|
|
646
|
+
args: { fileKey: "abc123" },
|
|
647
|
+
options: {
|
|
648
|
+
ids: "1:2,3:4",
|
|
649
|
+
"image-format": "png",
|
|
650
|
+
scale: 2,
|
|
651
|
+
output: "."
|
|
652
|
+
},
|
|
653
|
+
description: "Render frames as 2x PNG"
|
|
654
|
+
}
|
|
655
|
+
],
|
|
656
|
+
async run(c) {
|
|
657
|
+
const fileKey = c.args.fileKey;
|
|
658
|
+
const { ids, "image-format": imageFormat, scale, output: outputDir } = c.options;
|
|
659
|
+
const client = createFigmaClient(c.env.FIGMA_API_KEY);
|
|
660
|
+
const nodeIds = ids.split(",").map((id) => id.trim());
|
|
661
|
+
if (nodeIds.length === 0 || nodeIds.length === 1 && nodeIds[0] === "") {
|
|
662
|
+
return c.error({
|
|
663
|
+
code: "VALIDATION_ERROR",
|
|
664
|
+
message: "--ids is required and cannot be empty"
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
const imagesResponse = await client.getImages(fileKey, nodeIds, {
|
|
668
|
+
format: imageFormat,
|
|
669
|
+
scale
|
|
670
|
+
});
|
|
671
|
+
if (imagesResponse.err) {
|
|
672
|
+
return c.error({
|
|
673
|
+
code: "FIGMA_API_ERROR",
|
|
674
|
+
message: imagesResponse.err
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
const resolvedDir = resolve(outputDir);
|
|
678
|
+
if (!existsSync(resolvedDir)) {
|
|
679
|
+
mkdirSync(resolvedDir, { recursive: true });
|
|
680
|
+
}
|
|
681
|
+
const results = [];
|
|
682
|
+
for (const nodeId of nodeIds) {
|
|
683
|
+
const imageUrl = imagesResponse.images[nodeId];
|
|
684
|
+
if (!imageUrl) {
|
|
685
|
+
results.push({ nodeId, file: "(no image)", size: 0 });
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
const safeNodeId = nodeId.replace(/:/g, "-");
|
|
689
|
+
const filename = `${safeNodeId}.${imageFormat}`;
|
|
690
|
+
const filepath = resolve(resolvedDir, filename);
|
|
691
|
+
const buffer = await downloadFile(imageUrl);
|
|
692
|
+
writeFileSync(filepath, buffer);
|
|
693
|
+
results.push({ nodeId, file: filepath, size: buffer.length });
|
|
694
|
+
}
|
|
695
|
+
return c.ok({ rendered: results, total: results.length });
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// src/commands/nodes.ts
|
|
700
|
+
import { Cli as Cli5, z as z8 } from "incur";
|
|
701
|
+
var boundingBoxSchema = z8.object({
|
|
702
|
+
x: z8.number(),
|
|
703
|
+
y: z8.number(),
|
|
704
|
+
width: z8.number(),
|
|
705
|
+
height: z8.number()
|
|
706
|
+
}).optional();
|
|
707
|
+
var childSummarySchema = z8.object({
|
|
708
|
+
id: z8.string(),
|
|
709
|
+
name: z8.string(),
|
|
710
|
+
type: z8.string()
|
|
711
|
+
});
|
|
712
|
+
var nodeGetOutputSchema = z8.object({
|
|
713
|
+
id: z8.string(),
|
|
714
|
+
name: z8.string(),
|
|
715
|
+
type: z8.string(),
|
|
716
|
+
boundingBox: boundingBoxSchema,
|
|
717
|
+
children: z8.array(childSummarySchema),
|
|
718
|
+
childCount: z8.number()
|
|
719
|
+
});
|
|
720
|
+
function summarizeChildren(children, depth) {
|
|
721
|
+
if (!children || depth <= 0) return [];
|
|
722
|
+
const result = [];
|
|
723
|
+
for (const child of children) {
|
|
724
|
+
result.push({
|
|
725
|
+
id: child.id ?? "",
|
|
726
|
+
name: child.name ?? "",
|
|
727
|
+
type: child.type ?? "UNKNOWN"
|
|
728
|
+
});
|
|
729
|
+
if (depth > 1 && child.children) {
|
|
730
|
+
result.push(...summarizeChildren(child.children, depth - 1));
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return result;
|
|
734
|
+
}
|
|
735
|
+
var nodesCli = Cli5.create("nodes", {
|
|
736
|
+
description: "Inspect Figma file nodes."
|
|
737
|
+
});
|
|
738
|
+
nodesCli.command("get", {
|
|
739
|
+
description: "Get details for a specific node in a Figma file.",
|
|
740
|
+
args: z8.object({
|
|
741
|
+
fileKey: fileKeyArg,
|
|
742
|
+
nodeId: z8.string().describe('Node ID (e.g. "1:2")')
|
|
743
|
+
}),
|
|
744
|
+
options: z8.object({
|
|
745
|
+
depth: z8.coerce.number().default(1).describe("How deep into the node tree to display children (default: 1)"),
|
|
746
|
+
format: formatOption
|
|
747
|
+
}),
|
|
748
|
+
env: figmaEnv,
|
|
749
|
+
output: nodeGetOutputSchema,
|
|
750
|
+
examples: [
|
|
751
|
+
{
|
|
752
|
+
args: { fileKey: "abc123XYZ", nodeId: "1:2" },
|
|
753
|
+
options: { depth: 2 },
|
|
754
|
+
description: "Inspect a node with 2 levels of children"
|
|
755
|
+
}
|
|
756
|
+
],
|
|
757
|
+
async run(c) {
|
|
758
|
+
const client = createFigmaClient(c.env.FIGMA_API_KEY);
|
|
759
|
+
const response = await client.getFileNodes(c.args.fileKey, [c.args.nodeId]);
|
|
760
|
+
const nodeData = response.nodes[c.args.nodeId];
|
|
761
|
+
if (!nodeData?.document) {
|
|
762
|
+
return c.error({
|
|
763
|
+
code: "NODE_NOT_FOUND",
|
|
764
|
+
message: `Node "${c.args.nodeId}" not found in file "${c.args.fileKey}".`
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
const doc = nodeData.document;
|
|
768
|
+
const bbox = doc.absoluteBoundingBox ?? doc.absoluteRenderBounds;
|
|
769
|
+
const children = summarizeChildren(doc.children, c.options.depth);
|
|
770
|
+
const result = {
|
|
771
|
+
id: doc.id ?? c.args.nodeId,
|
|
772
|
+
name: doc.name ?? "",
|
|
773
|
+
type: doc.type ?? "UNKNOWN",
|
|
774
|
+
boundingBox: bbox ? { x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height } : void 0,
|
|
775
|
+
children,
|
|
776
|
+
childCount: doc.children?.length ?? 0
|
|
777
|
+
};
|
|
778
|
+
if (c.options.format === "table") {
|
|
779
|
+
process.stdout.write(
|
|
780
|
+
`${outputFormatter(result, "table")}
|
|
781
|
+
`
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
return c.ok(
|
|
785
|
+
result,
|
|
786
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
787
|
+
cta: {
|
|
788
|
+
commands: children[0] ? [
|
|
789
|
+
{
|
|
790
|
+
command: "nodes get",
|
|
791
|
+
args: { fileKey: c.args.fileKey, nodeId: children[0].id },
|
|
792
|
+
description: `Inspect child node "${children[0].name}"`
|
|
793
|
+
}
|
|
794
|
+
] : []
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// src/commands/tokens.ts
|
|
802
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
803
|
+
import { Cli as Cli6, z as z9 } from "incur";
|
|
804
|
+
|
|
805
|
+
// src/tokens/dtcg.ts
|
|
806
|
+
function slugify(name) {
|
|
807
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
808
|
+
}
|
|
809
|
+
function colorToDtcgHex(rgba) {
|
|
810
|
+
const r = rgba.r.toString(16).padStart(2, "0");
|
|
811
|
+
const g = rgba.g.toString(16).padStart(2, "0");
|
|
812
|
+
const b = rgba.b.toString(16).padStart(2, "0");
|
|
813
|
+
if (rgba.a < 1) {
|
|
814
|
+
const a = Math.round(rgba.a * 255).toString(16).padStart(2, "0");
|
|
815
|
+
return `#${r}${g}${b}${a}`;
|
|
816
|
+
}
|
|
817
|
+
return `#${r}${g}${b}`;
|
|
818
|
+
}
|
|
819
|
+
function colorToDtcg(token) {
|
|
820
|
+
return {
|
|
821
|
+
$type: "color",
|
|
822
|
+
$value: colorToDtcgHex(token.rgba)
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
function typographyToDtcg(token) {
|
|
826
|
+
const group = {
|
|
827
|
+
fontFamily: {
|
|
828
|
+
$type: "fontFamily",
|
|
829
|
+
$value: token.fontFamily
|
|
830
|
+
},
|
|
831
|
+
fontSize: {
|
|
832
|
+
$type: "dimension",
|
|
833
|
+
$value: `${token.fontSize}px`
|
|
834
|
+
},
|
|
835
|
+
fontWeight: {
|
|
836
|
+
$type: "fontWeight",
|
|
837
|
+
$value: token.fontWeight
|
|
838
|
+
},
|
|
839
|
+
letterSpacing: {
|
|
840
|
+
$type: "dimension",
|
|
841
|
+
$value: `${token.letterSpacing}px`
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
if (token.lineHeight !== null) {
|
|
845
|
+
group.lineHeight = {
|
|
846
|
+
$type: "dimension",
|
|
847
|
+
$value: `${token.lineHeight}px`
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
return group;
|
|
851
|
+
}
|
|
852
|
+
function shadowToDtcg(token) {
|
|
853
|
+
return {
|
|
854
|
+
$type: "shadow",
|
|
855
|
+
$value: {
|
|
856
|
+
color: colorToDtcgHex(token.color),
|
|
857
|
+
offsetX: `${token.offsetX}px`,
|
|
858
|
+
offsetY: `${token.offsetY}px`,
|
|
859
|
+
blur: `${token.radius}px`,
|
|
860
|
+
spread: `${token.spread}px`
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
function blurToDtcg(token) {
|
|
865
|
+
return {
|
|
866
|
+
$type: "dimension",
|
|
867
|
+
$value: `${token.radius}px`
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function effectToDtcg(token) {
|
|
871
|
+
if ("color" in token) {
|
|
872
|
+
return shadowToDtcg(token);
|
|
873
|
+
}
|
|
874
|
+
return blurToDtcg(token);
|
|
875
|
+
}
|
|
876
|
+
function toDtcg(tokens) {
|
|
877
|
+
const output = {};
|
|
878
|
+
if (tokens.colors.length > 0) {
|
|
879
|
+
const group = {};
|
|
880
|
+
for (const c of tokens.colors) {
|
|
881
|
+
group[slugify(c.name)] = colorToDtcg(c);
|
|
882
|
+
}
|
|
883
|
+
output.color = group;
|
|
884
|
+
}
|
|
885
|
+
if (tokens.typography.length > 0) {
|
|
886
|
+
const group = {};
|
|
887
|
+
for (const t of tokens.typography) {
|
|
888
|
+
group[slugify(t.name)] = typographyToDtcg(t);
|
|
889
|
+
}
|
|
890
|
+
output.typography = group;
|
|
891
|
+
}
|
|
892
|
+
if (tokens.effects.length > 0) {
|
|
893
|
+
const group = {};
|
|
894
|
+
for (const e of tokens.effects) {
|
|
895
|
+
group[slugify(e.name)] = effectToDtcg(e);
|
|
896
|
+
}
|
|
897
|
+
output.effect = group;
|
|
898
|
+
}
|
|
899
|
+
return output;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// src/tokens/extractor.ts
|
|
903
|
+
function clamp(v, min, max) {
|
|
904
|
+
return Math.max(min, Math.min(max, v));
|
|
905
|
+
}
|
|
906
|
+
function toHex(r, g, b, a) {
|
|
907
|
+
const r8 = Math.round(clamp(r, 0, 1) * 255);
|
|
908
|
+
const g8 = Math.round(clamp(g, 0, 1) * 255);
|
|
909
|
+
const b8 = Math.round(clamp(b, 0, 1) * 255);
|
|
910
|
+
const hex = `#${r8.toString(16).padStart(2, "0")}${g8.toString(16).padStart(2, "0")}${b8.toString(16).padStart(2, "0")}`;
|
|
911
|
+
if (a < 1) {
|
|
912
|
+
const a8 = Math.round(clamp(a, 0, 1) * 255);
|
|
913
|
+
return `${hex}${a8.toString(16).padStart(2, "0")}`;
|
|
914
|
+
}
|
|
915
|
+
return hex;
|
|
916
|
+
}
|
|
917
|
+
function sanitizeName(name) {
|
|
918
|
+
return name.trim() || "unnamed";
|
|
919
|
+
}
|
|
920
|
+
function extractColorFromPaints(name, fills) {
|
|
921
|
+
const solid = fills.find((f) => f.type === "SOLID" && f.visible !== false && f.color);
|
|
922
|
+
if (!solid?.color) return null;
|
|
923
|
+
const { r, g, b, a: colorA } = solid.color;
|
|
924
|
+
const a = solid.opacity !== void 0 ? solid.opacity * colorA : colorA;
|
|
925
|
+
return {
|
|
926
|
+
name: sanitizeName(name),
|
|
927
|
+
hex: toHex(r, g, b, a),
|
|
928
|
+
rgba: {
|
|
929
|
+
r: Math.round(clamp(r, 0, 1) * 255),
|
|
930
|
+
g: Math.round(clamp(g, 0, 1) * 255),
|
|
931
|
+
b: Math.round(clamp(b, 0, 1) * 255),
|
|
932
|
+
a: Number.parseFloat(clamp(a, 0, 1).toFixed(2))
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function extractTypography(name, style) {
|
|
937
|
+
return {
|
|
938
|
+
name: sanitizeName(name),
|
|
939
|
+
fontFamily: style.fontFamily,
|
|
940
|
+
fontSize: style.fontSize,
|
|
941
|
+
fontWeight: style.fontWeight,
|
|
942
|
+
lineHeight: style.lineHeightPx ?? null,
|
|
943
|
+
letterSpacing: style.letterSpacing ?? 0
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
function extractEffects(name, effects) {
|
|
947
|
+
const tokens = [];
|
|
948
|
+
for (const effect of effects) {
|
|
949
|
+
if (effect.visible === false) continue;
|
|
950
|
+
if (effect.type === "DROP_SHADOW" || effect.type === "INNER_SHADOW") {
|
|
951
|
+
tokens.push({
|
|
952
|
+
name: sanitizeName(name),
|
|
953
|
+
type: effect.type === "DROP_SHADOW" ? "drop-shadow" : "inner-shadow",
|
|
954
|
+
color: effect.color ? {
|
|
955
|
+
r: Math.round(clamp(effect.color.r, 0, 1) * 255),
|
|
956
|
+
g: Math.round(clamp(effect.color.g, 0, 1) * 255),
|
|
957
|
+
b: Math.round(clamp(effect.color.b, 0, 1) * 255),
|
|
958
|
+
a: Number.parseFloat(clamp(effect.color.a, 0, 1).toFixed(2))
|
|
959
|
+
} : { r: 0, g: 0, b: 0, a: 1 },
|
|
960
|
+
offsetX: effect.offset?.x ?? 0,
|
|
961
|
+
offsetY: effect.offset?.y ?? 0,
|
|
962
|
+
radius: effect.radius ?? 0,
|
|
963
|
+
spread: effect.spread ?? 0
|
|
964
|
+
});
|
|
965
|
+
} else if (effect.type === "LAYER_BLUR" || effect.type === "BACKGROUND_BLUR") {
|
|
966
|
+
tokens.push({
|
|
967
|
+
name: sanitizeName(name),
|
|
968
|
+
type: effect.type === "LAYER_BLUR" ? "layer-blur" : "background-blur",
|
|
969
|
+
radius: effect.radius ?? 0
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return tokens;
|
|
974
|
+
}
|
|
975
|
+
function extractTokens(document, styles, filter) {
|
|
976
|
+
const colors = [];
|
|
977
|
+
const typography = [];
|
|
978
|
+
const effects = [];
|
|
979
|
+
if (!styles || Object.keys(styles).length === 0) {
|
|
980
|
+
return { colors, typography, effects };
|
|
981
|
+
}
|
|
982
|
+
const styleByNodeId = /* @__PURE__ */ new Map();
|
|
983
|
+
for (const [nodeId, meta] of Object.entries(styles)) {
|
|
984
|
+
styleByNodeId.set(nodeId, meta);
|
|
985
|
+
}
|
|
986
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
987
|
+
function walk(node) {
|
|
988
|
+
if (!node || typeof node !== "object") return;
|
|
989
|
+
const n = node;
|
|
990
|
+
if (n.id) {
|
|
991
|
+
nodeMap.set(n.id, n);
|
|
992
|
+
}
|
|
993
|
+
const nodeStyles = n.styles;
|
|
994
|
+
if (nodeStyles) {
|
|
995
|
+
for (const [_styleType, styleId] of Object.entries(nodeStyles)) {
|
|
996
|
+
if (styleByNodeId.has(styleId)) {
|
|
997
|
+
nodeMap.set(styleId, n);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
if (Array.isArray(n.children)) {
|
|
1002
|
+
for (const child of n.children) walk(child);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
walk(document);
|
|
1006
|
+
for (const [nodeId, meta] of styleByNodeId) {
|
|
1007
|
+
const node = nodeMap.get(nodeId);
|
|
1008
|
+
const styleName = meta.name;
|
|
1009
|
+
if (meta.style_type === "FILL" && (!filter || filter === "colors")) {
|
|
1010
|
+
if (node?.fills && Array.isArray(node.fills)) {
|
|
1011
|
+
const color = extractColorFromPaints(styleName, node.fills);
|
|
1012
|
+
if (color) colors.push(color);
|
|
1013
|
+
}
|
|
1014
|
+
} else if (meta.style_type === "TEXT" && (!filter || filter === "typography")) {
|
|
1015
|
+
if (node?.style) {
|
|
1016
|
+
typography.push(extractTypography(styleName, node.style));
|
|
1017
|
+
}
|
|
1018
|
+
} else if (meta.style_type === "EFFECT" && (!filter || filter === "effects")) {
|
|
1019
|
+
if (node?.effects && Array.isArray(node.effects)) {
|
|
1020
|
+
const extracted = extractEffects(styleName, node.effects);
|
|
1021
|
+
effects.push(...extracted);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return { colors, typography, effects };
|
|
1026
|
+
}
|
|
1027
|
+
function toFlatTokens(tokens) {
|
|
1028
|
+
const flat = {};
|
|
1029
|
+
for (const c of tokens.colors) {
|
|
1030
|
+
const key = `color.${slugify2(c.name)}`;
|
|
1031
|
+
flat[key] = c.hex;
|
|
1032
|
+
}
|
|
1033
|
+
for (const t of tokens.typography) {
|
|
1034
|
+
const base = `typography.${slugify2(t.name)}`;
|
|
1035
|
+
flat[`${base}.fontFamily`] = t.fontFamily;
|
|
1036
|
+
flat[`${base}.fontSize`] = t.fontSize;
|
|
1037
|
+
flat[`${base}.fontWeight`] = t.fontWeight;
|
|
1038
|
+
if (t.lineHeight !== null) flat[`${base}.lineHeight`] = t.lineHeight;
|
|
1039
|
+
flat[`${base}.letterSpacing`] = t.letterSpacing;
|
|
1040
|
+
}
|
|
1041
|
+
for (const e of tokens.effects) {
|
|
1042
|
+
const base = `effect.${slugify2(e.name)}`;
|
|
1043
|
+
flat[`${base}.type`] = e.type;
|
|
1044
|
+
if ("color" in e) {
|
|
1045
|
+
const s = e;
|
|
1046
|
+
flat[`${base}.offsetX`] = s.offsetX;
|
|
1047
|
+
flat[`${base}.offsetY`] = s.offsetY;
|
|
1048
|
+
flat[`${base}.radius`] = s.radius;
|
|
1049
|
+
flat[`${base}.spread`] = s.spread;
|
|
1050
|
+
flat[`${base}.color`] = `rgba(${s.color.r}, ${s.color.g}, ${s.color.b}, ${s.color.a})`;
|
|
1051
|
+
} else {
|
|
1052
|
+
flat[`${base}.radius`] = e.radius;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return flat;
|
|
1056
|
+
}
|
|
1057
|
+
function slugify2(name) {
|
|
1058
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// src/commands/tokens.ts
|
|
1062
|
+
var tokensCli = Cli6.create("tokens", {
|
|
1063
|
+
description: "Extract design tokens from Figma files."
|
|
1064
|
+
});
|
|
1065
|
+
var formatOption2 = z9.enum(["dtcg", "flat", "json"]).default("dtcg").describe("Output format: dtcg (W3C DTCG), flat (key-value), json (raw intermediate)");
|
|
1066
|
+
var filterOption = z9.enum(["colors", "typography", "effects"]).optional().describe("Extract only specific token types");
|
|
1067
|
+
var outputOption = z9.string().optional().describe("Write output to file instead of stdout");
|
|
1068
|
+
tokensCli.command("export", {
|
|
1069
|
+
description: "Extract design tokens from a Figma file.",
|
|
1070
|
+
args: z9.object({
|
|
1071
|
+
fileKey: z9.string().describe("Figma file key")
|
|
1072
|
+
}),
|
|
1073
|
+
options: z9.object({
|
|
1074
|
+
format: formatOption2,
|
|
1075
|
+
filter: filterOption,
|
|
1076
|
+
output: outputOption
|
|
1077
|
+
}),
|
|
1078
|
+
env: figmaEnv,
|
|
1079
|
+
output: z9.unknown(),
|
|
1080
|
+
examples: [
|
|
1081
|
+
{
|
|
1082
|
+
args: { fileKey: "abc123FileKey" },
|
|
1083
|
+
options: { format: "dtcg" },
|
|
1084
|
+
description: "Export tokens in DTCG format"
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
args: { fileKey: "abc123FileKey" },
|
|
1088
|
+
options: { format: "flat", filter: "colors" },
|
|
1089
|
+
description: "Export only color tokens in flat format"
|
|
1090
|
+
}
|
|
1091
|
+
],
|
|
1092
|
+
async run(c) {
|
|
1093
|
+
const { fileKey } = c.args;
|
|
1094
|
+
const client = createFigmaClient(c.env.FIGMA_API_KEY);
|
|
1095
|
+
const file = await client.getFile(fileKey);
|
|
1096
|
+
const tokens = extractTokens(
|
|
1097
|
+
file.document,
|
|
1098
|
+
file.styles,
|
|
1099
|
+
c.options.filter
|
|
1100
|
+
);
|
|
1101
|
+
let result;
|
|
1102
|
+
if (c.options.format === "dtcg") {
|
|
1103
|
+
result = toDtcg(tokens);
|
|
1104
|
+
} else if (c.options.format === "flat") {
|
|
1105
|
+
result = toFlatTokens(tokens);
|
|
1106
|
+
} else {
|
|
1107
|
+
result = tokens;
|
|
1108
|
+
}
|
|
1109
|
+
if (c.options.output) {
|
|
1110
|
+
writeFileSync2(c.options.output, JSON.stringify(result, null, 2), "utf8");
|
|
1111
|
+
return c.ok(
|
|
1112
|
+
{ written: c.options.output, format: c.options.format },
|
|
1113
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
1114
|
+
cta: {
|
|
1115
|
+
commands: [
|
|
1116
|
+
{
|
|
1117
|
+
command: `tokens export ${fileKey} --format flat`,
|
|
1118
|
+
description: "Export tokens in flat format"
|
|
1119
|
+
}
|
|
1120
|
+
]
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
return c.ok(
|
|
1126
|
+
result,
|
|
1127
|
+
c.format === "json" || c.format === "jsonl" ? void 0 : {
|
|
1128
|
+
cta: {
|
|
1129
|
+
commands: [
|
|
1130
|
+
{
|
|
1131
|
+
command: `tokens export ${fileKey} --format flat`,
|
|
1132
|
+
description: "Export in flat key-value format"
|
|
1133
|
+
},
|
|
1134
|
+
{
|
|
1135
|
+
command: `tokens export ${fileKey} --filter colors`,
|
|
1136
|
+
description: "Export only color tokens"
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
command: `components list ${fileKey}`,
|
|
1140
|
+
description: "List published components"
|
|
1141
|
+
}
|
|
1142
|
+
]
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
// src/cli.ts
|
|
1150
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1151
|
+
var pkg = JSON.parse(readFileSync(resolve2(__dirname, "../package.json"), "utf8"));
|
|
1152
|
+
var cli = Cli7.create("figma", {
|
|
1153
|
+
version: pkg.version,
|
|
1154
|
+
description: "Query Figma REST API data from the command line."
|
|
1155
|
+
});
|
|
1156
|
+
cli.command(tokensCli);
|
|
1157
|
+
cli.command(componentsCli);
|
|
1158
|
+
cli.command(filesCli);
|
|
1159
|
+
cli.command(nodesCli);
|
|
1160
|
+
cli.command(framesCli);
|
|
1161
|
+
cli.command(commentsCli);
|
|
1162
|
+
var isMain = (() => {
|
|
1163
|
+
const entrypoint = process.argv[1];
|
|
1164
|
+
if (!entrypoint) {
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
try {
|
|
1168
|
+
return realpathSync(entrypoint) === realpathSync(fileURLToPath(import.meta.url));
|
|
1169
|
+
} catch {
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
})();
|
|
1173
|
+
if (isMain) {
|
|
1174
|
+
initTelemetry("figma");
|
|
1175
|
+
process.on("beforeExit", () => shutdownTelemetry());
|
|
1176
|
+
cli.serve();
|
|
1177
|
+
}
|
|
197
1178
|
export {
|
|
198
1179
|
cli,
|
|
199
1180
|
createFigmaClient,
|
|
200
|
-
|
|
1181
|
+
extractTokens,
|
|
1182
|
+
figmaEnv,
|
|
1183
|
+
toDtcg,
|
|
1184
|
+
toFlatTokens
|
|
201
1185
|
};
|