@mstrathman/figma 0.1.0 → 0.2.1

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/main.js CHANGED
@@ -60,9 +60,9 @@ var init_client = __esm({
60
60
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
61
61
  this.maxRetries = options.maxRetries ?? MAX_RETRIES;
62
62
  }
63
- async request(method, path5, options = {}, retryCount = 0) {
63
+ async request(method, path8, options = {}, retryCount = 0) {
64
64
  const baseWithSlash = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
65
- const pathWithoutLeadingSlash = path5.startsWith("/") ? path5.slice(1) : path5;
65
+ const pathWithoutLeadingSlash = path8.startsWith("/") ? path8.slice(1) : path8;
66
66
  const url = new URL(pathWithoutLeadingSlash, baseWithSlash);
67
67
  if (options.params) {
68
68
  for (const [key, value] of Object.entries(options.params)) {
@@ -110,12 +110,12 @@ var init_client = __esm({
110
110
  const retryAfter = response.headers.get("Retry-After");
111
111
  const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount);
112
112
  await this.sleep(delayMs);
113
- return this.request(method, path5, options, retryCount + 1);
113
+ return this.request(method, path8, options, retryCount + 1);
114
114
  }
115
115
  if (response.status >= 500 && retryCount < this.maxRetries) {
116
116
  const delayMs = INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount);
117
117
  await this.sleep(delayMs);
118
- return this.request(method, path5, options, retryCount + 1);
118
+ return this.request(method, path8, options, retryCount + 1);
119
119
  }
120
120
  if (!response.ok) {
121
121
  let body;
@@ -133,7 +133,7 @@ var init_client = __esm({
133
133
  return response.json();
134
134
  }
135
135
  sleep(ms) {
136
- return new Promise((resolve) => setTimeout(resolve, ms));
136
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
137
137
  }
138
138
  // User endpoints
139
139
  async getMe() {
@@ -281,8 +281,8 @@ var init_client = __esm({
281
281
  );
282
282
  }
283
283
  // Generic API method for raw access
284
- async api(method, path5, options) {
285
- const normalizedPath = path5.startsWith("/") ? path5 : `/${path5}`;
284
+ async api(method, path8, options) {
285
+ const normalizedPath = path8.startsWith("/") ? path8 : `/${path8}`;
286
286
  return this.request(method.toUpperCase(), normalizedPath, options);
287
287
  }
288
288
  };
@@ -578,19 +578,19 @@ async function findAvailablePort(startPort, maxPort = MAX_PORT) {
578
578
  if (startPort > maxPort) {
579
579
  throw new Error(`No available ports found between 8585 and ${maxPort}`);
580
580
  }
581
- return new Promise((resolve, reject) => {
581
+ return new Promise((resolve3, reject) => {
582
582
  const server = http.createServer();
583
583
  server.listen(startPort, "127.0.0.1", () => {
584
584
  const address = server.address();
585
585
  if (address && typeof address === "object") {
586
586
  const port = address.port;
587
- server.close(() => resolve(port));
587
+ server.close(() => resolve3(port));
588
588
  } else {
589
589
  reject(new Error("Could not get server address"));
590
590
  }
591
591
  });
592
592
  server.on("error", () => {
593
- findAvailablePort(startPort + 1, maxPort).then(resolve).catch(reject);
593
+ findAvailablePort(startPort + 1, maxPort).then(resolve3).catch(reject);
594
594
  });
595
595
  });
596
596
  }
@@ -599,7 +599,7 @@ async function startOAuthFlow(config, onOpenUrl) {
599
599
  const redirectUri = config.redirectUri || `http://127.0.0.1:${port}/callback`;
600
600
  const state = generateState();
601
601
  const scopes = config.scopes || ["files:read"];
602
- return new Promise((resolve, reject) => {
602
+ return new Promise((resolve3, reject) => {
603
603
  let serverClosed = false;
604
604
  const closeServer = () => {
605
605
  if (!serverClosed) {
@@ -656,7 +656,7 @@ async function startOAuthFlow(config, onOpenUrl) {
656
656
  res.writeHead(200, { "Content-Type": "text/html" });
657
657
  res.end(getSuccessHtml());
658
658
  closeServer();
659
- resolve({
659
+ resolve3({
660
660
  accessToken: tokenData.access_token,
661
661
  refreshToken: tokenData.refresh_token,
662
662
  expiresIn: tokenData.expires_in,
@@ -876,7 +876,7 @@ function createFactory() {
876
876
  }
877
877
 
878
878
  // src/cmd/root.ts
879
- import { Command as Command26 } from "commander";
879
+ import { Command as Command30 } from "commander";
880
880
 
881
881
  // src/cmd/auth/index.ts
882
882
  import { Command as Command4 } from "commander";
@@ -1163,7 +1163,7 @@ function createAuthCommand(factory) {
1163
1163
  }
1164
1164
 
1165
1165
  // src/cmd/file/index.ts
1166
- import { Command as Command8 } from "commander";
1166
+ import { Command as Command9 } from "commander";
1167
1167
 
1168
1168
  // src/cmd/file/get.ts
1169
1169
  import { Command as Command5 } from "commander";
@@ -1228,25 +1228,78 @@ function formatValue(value) {
1228
1228
  return String(value);
1229
1229
  }
1230
1230
 
1231
+ // src/internal/url/parser.ts
1232
+ import {
1233
+ parseFileAndNodeId,
1234
+ parseFileId,
1235
+ formatNodeId
1236
+ } from "@design-sdk/figma-url";
1237
+ function parseFigmaInput(input) {
1238
+ const trimmed = input.trim();
1239
+ if (trimmed.includes("figma.com/")) {
1240
+ const result = parseFileAndNodeId(trimmed);
1241
+ if (result) {
1242
+ return {
1243
+ fileKey: result.file,
1244
+ nodeId: result.node || void 0
1245
+ };
1246
+ }
1247
+ try {
1248
+ const fileKey = parseFileId(trimmed);
1249
+ return { fileKey };
1250
+ } catch (error) {
1251
+ const message = error instanceof Error ? error.message : String(error);
1252
+ throw new Error(`Could not parse Figma URL: ${trimmed}. ${message}`);
1253
+ }
1254
+ }
1255
+ return { fileKey: trimmed };
1256
+ }
1257
+ function parseNodeIds(input) {
1258
+ const rawIds = input.split(",").map((id) => id.trim()).filter(Boolean);
1259
+ const validIds = [];
1260
+ const invalidIds = [];
1261
+ for (const id of rawIds) {
1262
+ const formatted = formatNodeId(id);
1263
+ if (formatted) {
1264
+ validIds.push(formatted);
1265
+ } else {
1266
+ invalidIds.push(id);
1267
+ }
1268
+ }
1269
+ if (invalidIds.length > 0) {
1270
+ throw new Error(
1271
+ `Invalid node ID(s): ${invalidIds.join(", ")}. Node IDs should be in format "1:2" or "1-2".`
1272
+ );
1273
+ }
1274
+ return validIds;
1275
+ }
1276
+
1231
1277
  // src/cmd/file/get.ts
1232
1278
  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(
1279
+ const cmd = new Command5("get").description("Get file JSON structure").argument("<url>", "Figma URL or file key").option("-v, --version <version>", "File version to get").option(
1234
1280
  "-d, --depth <depth>",
1235
1281
  "Depth of node tree to return (default: full tree)",
1236
1282
  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) => {
1283
+ ).option("--id <ids>", "Comma-separated node IDs (overrides URL node-id)").option("--geometry <paths>", "Include geometry data (paths)").option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
1284
+ async (url, options) => {
1239
1285
  const { io } = factory;
1240
1286
  try {
1241
- const token = await factory.getToken(options.token);
1287
+ const parsed = parseFigmaInput(url);
1288
+ const fileKey = parsed.fileKey;
1289
+ let nodeIds;
1290
+ if (options.id) {
1291
+ nodeIds = parseNodeIds(options.id);
1292
+ } else if (parsed.nodeId) {
1293
+ nodeIds = [parsed.nodeId];
1294
+ }
1242
1295
  const client = await factory.getClient();
1243
1296
  if (options.token) {
1244
1297
  const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
1245
- const overrideClient = new FigmaClient2({ token });
1298
+ const overrideClient = new FigmaClient2({ token: options.token });
1246
1299
  const file2 = await overrideClient.getFile(fileKey, {
1247
1300
  version: options.version,
1248
1301
  depth: options.depth,
1249
- ids: options.ids?.split(","),
1302
+ ids: nodeIds,
1250
1303
  geometry: options.geometry
1251
1304
  });
1252
1305
  io.out.write(
@@ -1258,7 +1311,7 @@ function createFileGetCommand(factory) {
1258
1311
  const file = await client.getFile(fileKey, {
1259
1312
  version: options.version,
1260
1313
  depth: options.depth,
1261
- ids: options.ids?.split(","),
1314
+ ids: nodeIds,
1262
1315
  geometry: options.geometry
1263
1316
  });
1264
1317
  io.out.write(
@@ -1282,13 +1335,27 @@ function createFileGetCommand(factory) {
1282
1335
  import { Command as Command6 } from "commander";
1283
1336
  import chalk6 from "chalk";
1284
1337
  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) => {
1338
+ const cmd = new Command6("nodes").description("Get specific nodes from a file").argument("<url>", "Figma URL (with node-id) or file key").option("--id <ids>", "Comma-separated node IDs (overrides URL node-id)").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(
1339
+ async (url, options) => {
1287
1340
  const { io } = factory;
1288
1341
  try {
1342
+ const parsed = parseFigmaInput(url);
1343
+ const fileKey = parsed.fileKey;
1344
+ let nodeIds;
1345
+ if (options.id) {
1346
+ nodeIds = parseNodeIds(options.id);
1347
+ } else if (parsed.nodeId) {
1348
+ nodeIds = [parsed.nodeId];
1349
+ } else {
1350
+ io.err.write(
1351
+ chalk6.red(
1352
+ "Error: node ID required. Use a URL with node-id or --id flag.\n"
1353
+ )
1354
+ );
1355
+ process.exit(1);
1356
+ }
1289
1357
  const token = await factory.getToken(options.token);
1290
1358
  const client = await factory.getClient();
1291
- const nodeIds = options.ids.split(",").map((id) => id.trim());
1292
1359
  if (options.token) {
1293
1360
  const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
1294
1361
  const overrideClient = new FigmaClient2({ token });
@@ -1402,208 +1469,894 @@ function sanitizeFilename(name) {
1402
1469
  return name.replace(/[:/\\?*"<>|]/g, "-");
1403
1470
  }
1404
1471
 
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;
1472
+ // src/internal/transform/tokens.ts
1473
+ function figmaColorToCss(color) {
1474
+ const r = Math.round(color.r * 255);
1475
+ const g = Math.round(color.g * 255);
1476
+ const b = Math.round(color.b * 255);
1477
+ const a = color.a ?? 1;
1478
+ if (a === 1) {
1479
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
1480
+ }
1481
+ return `rgba(${r}, ${g}, ${b}, ${a.toFixed(2)})`;
1483
1482
  }
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;
1483
+ function toKebabCase(str) {
1484
+ return str.replace(/\s+/g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").toLowerCase();
1492
1485
  }
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" }));
1486
+ function shadowToCss(shadow) {
1487
+ const inset = shadow.inset ? "inset " : "";
1488
+ return `${inset}${shadow.x}px ${shadow.y}px ${shadow.blur}px ${shadow.spread}px ${shadow.color}`;
1489
+ }
1490
+ function extractStylesFromFile(file, styleMetadata) {
1491
+ const tokens = {
1492
+ colors: {},
1493
+ typography: {},
1494
+ shadows: {},
1495
+ radii: {},
1496
+ spacing: {},
1497
+ effects: {}
1498
+ };
1499
+ const seenRadii = /* @__PURE__ */ new Set();
1500
+ const seenSpacing = /* @__PURE__ */ new Set();
1501
+ const styleNodeMap = /* @__PURE__ */ new Map();
1502
+ for (const style of styleMetadata) {
1503
+ const nodeId = style.node_id;
1504
+ if (nodeId) {
1505
+ styleNodeMap.set(nodeId, style);
1506
+ }
1507
+ }
1508
+ function traverseNode(node) {
1509
+ const nodeWithStyles = node;
1510
+ if ("styles" in nodeWithStyles && nodeWithStyles.styles) {
1511
+ const styles = nodeWithStyles.styles;
1512
+ if (styles.fill && "fills" in nodeWithStyles) {
1513
+ const fills = nodeWithStyles.fills;
1514
+ if (fills && fills.length > 0 && fills[0].type === "SOLID") {
1515
+ const fill = fills[0];
1516
+ if (fill.color) {
1517
+ const styleInfo = styleNodeMap.get(node.id);
1518
+ const styleName = styleInfo?.name || node.name;
1519
+ const tokenName = toKebabCase(styleName);
1520
+ tokens.colors[tokenName] = {
1521
+ name: styleName,
1522
+ type: "color",
1523
+ value: figmaColorToCss(fill.color),
1524
+ description: styleInfo?.description
1525
+ };
1526
+ }
1525
1527
  }
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
- `));
1528
+ }
1529
+ if (styles.text && "style" in nodeWithStyles && nodeWithStyles.style) {
1530
+ const textStyle = nodeWithStyles.style;
1531
+ const styleInfo = styleNodeMap.get(node.id);
1532
+ const styleName = styleInfo?.name || node.name;
1533
+ const tokenName = toKebabCase(styleName);
1534
+ tokens.typography[tokenName] = {
1535
+ name: styleName,
1536
+ type: "typography",
1537
+ value: {
1538
+ fontFamily: textStyle.fontFamily || "sans-serif",
1539
+ fontSize: `${textStyle.fontSize || 16}px`,
1540
+ fontWeight: textStyle.fontWeight || 400,
1541
+ lineHeight: textStyle.lineHeightPx !== void 0 ? `${textStyle.lineHeightPx}px` : "normal",
1542
+ letterSpacing: textStyle.letterSpacing !== void 0 ? `${textStyle.letterSpacing}px` : "normal"
1543
+ },
1544
+ description: styleInfo?.description
1545
+ };
1546
+ }
1547
+ if (styles.effect && "effects" in nodeWithStyles && nodeWithStyles.effects) {
1548
+ const effects = nodeWithStyles.effects;
1549
+ const styleInfo = styleNodeMap.get(node.id);
1550
+ const styleName = styleInfo?.name || node.name;
1551
+ const baseTokenName = toKebabCase(styleName);
1552
+ let shadowIndex = 0;
1553
+ for (const effect of effects) {
1554
+ if ((effect.type === "DROP_SHADOW" || effect.type === "INNER_SHADOW") && effect.visible !== false) {
1555
+ const shadowEffect = effect;
1556
+ const colorValue = shadowEffect.color ? figmaColorToCss(shadowEffect.color) : "rgba(0, 0, 0, 0.25)";
1557
+ const tokenName = shadowIndex === 0 ? baseTokenName : `${baseTokenName}-${shadowIndex}`;
1558
+ tokens.shadows[tokenName] = {
1559
+ name: shadowIndex === 0 ? styleName : `${styleName} ${shadowIndex + 1}`,
1560
+ type: "shadow",
1561
+ value: {
1562
+ x: shadowEffect.offset?.x ?? 0,
1563
+ y: shadowEffect.offset?.y ?? 0,
1564
+ blur: shadowEffect.radius ?? 0,
1565
+ spread: shadowEffect.spread ?? 0,
1566
+ color: colorValue,
1567
+ inset: effect.type === "INNER_SHADOW"
1568
+ },
1569
+ description: styleInfo?.description
1570
+ };
1571
+ shadowIndex++;
1572
+ }
1532
1573
  }
1533
- process.exit(1);
1534
1574
  }
1535
1575
  }
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" }));
1576
+ if ("cornerRadius" in nodeWithStyles && nodeWithStyles.cornerRadius !== void 0) {
1577
+ const radius = nodeWithStyles.cornerRadius;
1578
+ if (radius > 0) {
1579
+ const radiusValue = `${radius}px`;
1580
+ if (!seenRadii.has(radiusValue)) {
1581
+ seenRadii.add(radiusValue);
1582
+ const tokenName = `radius-${radius}`;
1583
+ tokens.radii[tokenName] = {
1584
+ name: `Radius ${radius}`,
1585
+ type: "radius",
1586
+ value: radiusValue
1587
+ };
1588
+ }
1589
+ }
1590
+ }
1591
+ if ("rectangleCornerRadii" in nodeWithStyles && nodeWithStyles.rectangleCornerRadii) {
1592
+ const [topLeft, topRight, bottomRight, bottomLeft] = nodeWithStyles.rectangleCornerRadii;
1593
+ for (const radius of [topLeft, topRight, bottomRight, bottomLeft]) {
1594
+ if (radius > 0) {
1595
+ const radiusValue = `${radius}px`;
1596
+ if (!seenRadii.has(radiusValue)) {
1597
+ seenRadii.add(radiusValue);
1598
+ const tokenName = `radius-${radius}`;
1599
+ tokens.radii[tokenName] = {
1600
+ name: `Radius ${radius}`,
1601
+ type: "radius",
1602
+ value: radiusValue
1603
+ };
1604
+ }
1571
1605
  }
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
- `));
1606
+ }
1607
+ }
1608
+ if ("layoutMode" in nodeWithStyles && nodeWithStyles.layoutMode !== "NONE") {
1609
+ if (nodeWithStyles.itemSpacing !== void 0 && nodeWithStyles.itemSpacing > 0) {
1610
+ const spacingValue = `${nodeWithStyles.itemSpacing}px`;
1611
+ if (!seenSpacing.has(spacingValue)) {
1612
+ seenSpacing.add(spacingValue);
1613
+ const tokenName = `spacing-${nodeWithStyles.itemSpacing}`;
1614
+ tokens.spacing[tokenName] = {
1615
+ name: `Spacing ${nodeWithStyles.itemSpacing}`,
1616
+ type: "spacing",
1617
+ value: spacingValue
1618
+ };
1619
+ }
1620
+ }
1621
+ const paddings = [
1622
+ nodeWithStyles.paddingTop,
1623
+ nodeWithStyles.paddingRight,
1624
+ nodeWithStyles.paddingBottom,
1625
+ nodeWithStyles.paddingLeft
1626
+ ];
1627
+ for (const padding of paddings) {
1628
+ if (padding !== void 0 && padding > 0) {
1629
+ const spacingValue = `${padding}px`;
1630
+ if (!seenSpacing.has(spacingValue)) {
1631
+ seenSpacing.add(spacingValue);
1632
+ const tokenName = `spacing-${padding}`;
1633
+ tokens.spacing[tokenName] = {
1634
+ name: `Spacing ${padding}`,
1635
+ type: "spacing",
1636
+ value: spacingValue
1637
+ };
1638
+ }
1578
1639
  }
1579
- process.exit(1);
1580
1640
  }
1581
1641
  }
1582
- );
1583
- return cmd;
1584
- }
1585
-
1642
+ if ("children" in node) {
1643
+ const parent = node;
1644
+ for (const child of parent.children || []) {
1645
+ traverseNode(child);
1646
+ }
1647
+ }
1648
+ }
1649
+ if (file.document) {
1650
+ traverseNode(file.document);
1651
+ }
1652
+ return tokens;
1653
+ }
1654
+ function tokensToCss(tokens) {
1655
+ const lines = [":root {"];
1656
+ if (tokens.colors && Object.keys(tokens.colors).length > 0) {
1657
+ lines.push(" /* Colors */");
1658
+ for (const [name, token] of Object.entries(tokens.colors)) {
1659
+ lines.push(` --color-${name}: ${token.value};`);
1660
+ }
1661
+ lines.push("");
1662
+ }
1663
+ if (tokens.typography && Object.keys(tokens.typography).length > 0) {
1664
+ lines.push(" /* Typography */");
1665
+ for (const [name, token] of Object.entries(tokens.typography)) {
1666
+ const value = token.value;
1667
+ lines.push(` --font-${name}-family: ${value.fontFamily};`);
1668
+ lines.push(` --font-${name}-size: ${value.fontSize};`);
1669
+ lines.push(` --font-${name}-weight: ${value.fontWeight};`);
1670
+ lines.push(` --font-${name}-line-height: ${value.lineHeight};`);
1671
+ lines.push(` --font-${name}-letter-spacing: ${value.letterSpacing};`);
1672
+ }
1673
+ lines.push("");
1674
+ }
1675
+ if (tokens.shadows && Object.keys(tokens.shadows).length > 0) {
1676
+ lines.push(" /* Shadows */");
1677
+ for (const [name, token] of Object.entries(tokens.shadows)) {
1678
+ const value = token.value;
1679
+ const shadowCss = shadowToCss(value);
1680
+ lines.push(` --shadow-${name}: ${shadowCss};`);
1681
+ }
1682
+ lines.push("");
1683
+ }
1684
+ if (tokens.radii && Object.keys(tokens.radii).length > 0) {
1685
+ lines.push(" /* Border Radii */");
1686
+ for (const [name, token] of Object.entries(tokens.radii)) {
1687
+ lines.push(` --${name}: ${token.value};`);
1688
+ }
1689
+ lines.push("");
1690
+ }
1691
+ if (tokens.spacing && Object.keys(tokens.spacing).length > 0) {
1692
+ lines.push(" /* Spacing */");
1693
+ for (const [name, token] of Object.entries(tokens.spacing)) {
1694
+ lines.push(` --${name}: ${token.value};`);
1695
+ }
1696
+ }
1697
+ lines.push("}");
1698
+ return lines.join("\n");
1699
+ }
1700
+ function tokensToScss(tokens) {
1701
+ const lines = [];
1702
+ if (tokens.colors && Object.keys(tokens.colors).length > 0) {
1703
+ lines.push("// Colors");
1704
+ for (const [name, token] of Object.entries(tokens.colors)) {
1705
+ lines.push(`$color-${name}: ${token.value};`);
1706
+ }
1707
+ lines.push("");
1708
+ }
1709
+ if (tokens.typography && Object.keys(tokens.typography).length > 0) {
1710
+ lines.push("// Typography");
1711
+ for (const [name, token] of Object.entries(tokens.typography)) {
1712
+ const value = token.value;
1713
+ lines.push(`$font-${name}-family: ${value.fontFamily};`);
1714
+ lines.push(`$font-${name}-size: ${value.fontSize};`);
1715
+ lines.push(`$font-${name}-weight: ${value.fontWeight};`);
1716
+ lines.push(`$font-${name}-line-height: ${value.lineHeight};`);
1717
+ lines.push(`$font-${name}-letter-spacing: ${value.letterSpacing};`);
1718
+ }
1719
+ lines.push("");
1720
+ }
1721
+ if (tokens.shadows && Object.keys(tokens.shadows).length > 0) {
1722
+ lines.push("// Shadows");
1723
+ for (const [name, token] of Object.entries(tokens.shadows)) {
1724
+ const value = token.value;
1725
+ const shadowCss = shadowToCss(value);
1726
+ lines.push(`$shadow-${name}: ${shadowCss};`);
1727
+ }
1728
+ lines.push("");
1729
+ }
1730
+ if (tokens.radii && Object.keys(tokens.radii).length > 0) {
1731
+ lines.push("// Border Radii");
1732
+ for (const [name, token] of Object.entries(tokens.radii)) {
1733
+ lines.push(`$${name}: ${token.value};`);
1734
+ }
1735
+ lines.push("");
1736
+ }
1737
+ if (tokens.spacing && Object.keys(tokens.spacing).length > 0) {
1738
+ lines.push("// Spacing");
1739
+ for (const [name, token] of Object.entries(tokens.spacing)) {
1740
+ lines.push(`$${name}: ${token.value};`);
1741
+ }
1742
+ }
1743
+ return lines.join("\n");
1744
+ }
1745
+ function tokensToStyleDictionary(tokens) {
1746
+ const output = {};
1747
+ if (tokens.colors && Object.keys(tokens.colors).length > 0) {
1748
+ output.color = {};
1749
+ for (const [name, token] of Object.entries(tokens.colors)) {
1750
+ output.color[name] = {
1751
+ value: token.value,
1752
+ type: "color",
1753
+ description: token.description
1754
+ };
1755
+ }
1756
+ }
1757
+ if (tokens.typography && Object.keys(tokens.typography).length > 0) {
1758
+ output.typography = {};
1759
+ for (const [name, token] of Object.entries(tokens.typography)) {
1760
+ output.typography[name] = {
1761
+ value: token.value,
1762
+ type: "typography",
1763
+ description: token.description
1764
+ };
1765
+ }
1766
+ }
1767
+ if (tokens.shadows && Object.keys(tokens.shadows).length > 0) {
1768
+ output.shadow = {};
1769
+ for (const [name, token] of Object.entries(tokens.shadows)) {
1770
+ output.shadow[name] = {
1771
+ value: token.value,
1772
+ type: "shadow",
1773
+ description: token.description
1774
+ };
1775
+ }
1776
+ }
1777
+ if (tokens.radii && Object.keys(tokens.radii).length > 0) {
1778
+ output.borderRadius = {};
1779
+ for (const [name, token] of Object.entries(tokens.radii)) {
1780
+ output.borderRadius[name] = {
1781
+ value: token.value,
1782
+ type: "dimension",
1783
+ description: token.description
1784
+ };
1785
+ }
1786
+ }
1787
+ if (tokens.spacing && Object.keys(tokens.spacing).length > 0) {
1788
+ output.spacing = {};
1789
+ for (const [name, token] of Object.entries(tokens.spacing)) {
1790
+ output.spacing[name] = {
1791
+ value: token.value,
1792
+ type: "dimension",
1793
+ description: token.description
1794
+ };
1795
+ }
1796
+ }
1797
+ return JSON.stringify(output, null, 2);
1798
+ }
1799
+ function tokensToTailwind(tokens) {
1800
+ const config = {
1801
+ theme: {
1802
+ extend: {}
1803
+ }
1804
+ };
1805
+ if (tokens.colors && Object.keys(tokens.colors).length > 0) {
1806
+ config.theme.extend.colors = {};
1807
+ for (const [name, token] of Object.entries(tokens.colors)) {
1808
+ config.theme.extend.colors[name] = token.value;
1809
+ }
1810
+ }
1811
+ if (tokens.typography && Object.keys(tokens.typography).length > 0) {
1812
+ config.theme.extend.fontFamily = {};
1813
+ config.theme.extend.fontSize = {};
1814
+ for (const [name, token] of Object.entries(tokens.typography)) {
1815
+ const value = token.value;
1816
+ config.theme.extend.fontFamily[name] = [value.fontFamily];
1817
+ config.theme.extend.fontSize[name] = value.fontSize;
1818
+ }
1819
+ }
1820
+ if (tokens.shadows && Object.keys(tokens.shadows).length > 0) {
1821
+ config.theme.extend.boxShadow = {};
1822
+ for (const [name, token] of Object.entries(tokens.shadows)) {
1823
+ config.theme.extend.boxShadow[name] = shadowToCss(token.value);
1824
+ }
1825
+ }
1826
+ if (tokens.radii && Object.keys(tokens.radii).length > 0) {
1827
+ config.theme.extend.borderRadius = {};
1828
+ for (const [name, token] of Object.entries(tokens.radii)) {
1829
+ config.theme.extend.borderRadius[name] = token.value;
1830
+ }
1831
+ }
1832
+ if (tokens.spacing && Object.keys(tokens.spacing).length > 0) {
1833
+ config.theme.extend.spacing = {};
1834
+ for (const [name, token] of Object.entries(tokens.spacing)) {
1835
+ config.theme.extend.spacing[name] = token.value;
1836
+ }
1837
+ }
1838
+ return `/** @type {import('tailwindcss').Config} */
1839
+ module.exports = ${JSON.stringify(config, null, 2)};`;
1840
+ }
1841
+
1842
+ // src/cmd/file/images.ts
1843
+ function getNodeInfoMap(nodes, nodeIds) {
1844
+ const nodeInfoMap = /* @__PURE__ */ new Map();
1845
+ for (const nodeId of nodeIds) {
1846
+ const nodeData = nodes[nodeId];
1847
+ if (nodeData && nodeData.document) {
1848
+ const node = nodeData.document;
1849
+ const nameParts = node.name.split("/");
1850
+ const name = nameParts[nameParts.length - 1];
1851
+ const parentNames = nameParts.slice(0, -1);
1852
+ nodeInfoMap.set(nodeId, {
1853
+ name,
1854
+ parentNames
1855
+ });
1856
+ }
1857
+ }
1858
+ return nodeInfoMap;
1859
+ }
1860
+ function generateFilename(nodeId, nodeInfo, namingStrategy, format, isVariant, variantSuffix) {
1861
+ let baseName;
1862
+ switch (namingStrategy) {
1863
+ case "name":
1864
+ if (nodeInfo) {
1865
+ baseName = toKebabCase(nodeInfo.name);
1866
+ } else {
1867
+ baseName = sanitizeFilename(nodeId);
1868
+ }
1869
+ break;
1870
+ case "path":
1871
+ if (nodeInfo && nodeInfo.parentNames.length > 0) {
1872
+ const pathParts = [...nodeInfo.parentNames, nodeInfo.name];
1873
+ baseName = pathParts.map((p) => toKebabCase(p)).join("--");
1874
+ } else if (nodeInfo) {
1875
+ baseName = toKebabCase(nodeInfo.name);
1876
+ } else {
1877
+ baseName = sanitizeFilename(nodeId);
1878
+ }
1879
+ break;
1880
+ case "id":
1881
+ default:
1882
+ baseName = sanitizeFilename(nodeId);
1883
+ break;
1884
+ }
1885
+ if (isVariant && variantSuffix) {
1886
+ baseName = `${baseName}--${toKebabCase(variantSuffix)}`;
1887
+ }
1888
+ return `${baseName}.${format}`;
1889
+ }
1890
+ function createFileImagesCommand(factory) {
1891
+ const cmd = new Command7("images").description("Export images from a file").argument("<url>", "Figma URL (with node-id) or file key").option("--id <ids>", "Comma-separated node IDs (overrides URL node-id)").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(
1892
+ "-O, --output-format <format>",
1893
+ "Output format (json, table)",
1894
+ "json"
1895
+ ).option(
1896
+ "--naming <strategy>",
1897
+ "Naming strategy: id, name, or path (default: id)",
1898
+ "id"
1899
+ ).option("-t, --token <token>", "Override authentication token").action(
1900
+ async (url, options) => {
1901
+ const { io } = factory;
1902
+ const validFormats = ["jpg", "png", "svg", "pdf"];
1903
+ if (!validFormats.includes(options.format)) {
1904
+ io.err.write(
1905
+ chalk7.red(
1906
+ `Invalid format "${options.format}". Valid formats: ${validFormats.join(", ")}
1907
+ `
1908
+ )
1909
+ );
1910
+ process.exit(1);
1911
+ }
1912
+ const imageFormat = options.format;
1913
+ if (isNaN(options.scale) || options.scale < 0.01 || options.scale > 4) {
1914
+ io.err.write(
1915
+ chalk7.red(
1916
+ `Invalid scale "${options.scale}". Scale must be between 0.01 and 4.
1917
+ `
1918
+ )
1919
+ );
1920
+ process.exit(1);
1921
+ }
1922
+ const validNamingStrategies = ["id", "name", "path"];
1923
+ if (!validNamingStrategies.includes(options.naming)) {
1924
+ io.err.write(
1925
+ chalk7.red(
1926
+ `Invalid naming strategy "${options.naming}". Valid strategies: ${validNamingStrategies.join(", ")}
1927
+ `
1928
+ )
1929
+ );
1930
+ process.exit(1);
1931
+ }
1932
+ const namingStrategy = options.naming;
1933
+ try {
1934
+ const parsed = parseFigmaInput(url);
1935
+ const fileKey = parsed.fileKey;
1936
+ let nodeIds;
1937
+ if (options.id) {
1938
+ nodeIds = parseNodeIds(options.id);
1939
+ } else if (parsed.nodeId) {
1940
+ nodeIds = [parsed.nodeId];
1941
+ } else {
1942
+ io.err.write(
1943
+ chalk7.red(
1944
+ "Error: node ID required. Use a URL with node-id or --id flag.\n"
1945
+ )
1946
+ );
1947
+ process.exit(1);
1948
+ }
1949
+ const client = await factory.getClient();
1950
+ let activeClient = client;
1951
+ if (options.token) {
1952
+ const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
1953
+ activeClient = new FigmaClient2({ token: options.token });
1954
+ }
1955
+ const images = await activeClient.getImages(fileKey, nodeIds, {
1956
+ scale: options.scale,
1957
+ format: imageFormat,
1958
+ svg_include_id: options.svgIncludeId,
1959
+ svg_simplify_stroke: options.svgSimplifyStroke
1960
+ });
1961
+ if (options.output) {
1962
+ const imageUrls = images.images || {};
1963
+ if (Object.keys(imageUrls).length === 0) {
1964
+ io.err.write(
1965
+ chalk7.yellow(
1966
+ "Warning: No image URLs returned from Figma API.\n"
1967
+ )
1968
+ );
1969
+ }
1970
+ let nodeInfoMap = /* @__PURE__ */ new Map();
1971
+ if (namingStrategy === "name" || namingStrategy === "path") {
1972
+ try {
1973
+ const nodesResponse = await activeClient.getFileNodes(
1974
+ fileKey,
1975
+ nodeIds
1976
+ );
1977
+ nodeInfoMap = getNodeInfoMap(nodesResponse.nodes, nodeIds);
1978
+ } catch (fetchError) {
1979
+ const errorMsg = fetchError instanceof Error ? fetchError.message : String(fetchError);
1980
+ io.err.write(
1981
+ chalk7.yellow(
1982
+ `Warning: Could not fetch node names (${errorMsg}), falling back to ID naming.
1983
+ `
1984
+ )
1985
+ );
1986
+ }
1987
+ }
1988
+ const downloadItems = Object.entries(imageUrls).map(
1989
+ ([nodeId, url2]) => {
1990
+ const nodeInfo = nodeInfoMap.get(nodeId);
1991
+ const filename = generateFilename(
1992
+ nodeId,
1993
+ nodeInfo,
1994
+ namingStrategy,
1995
+ imageFormat,
1996
+ false
1997
+ );
1998
+ return {
1999
+ id: nodeId,
2000
+ url: url2 ?? null,
2001
+ filename
2002
+ };
2003
+ }
2004
+ );
2005
+ const results = await downloadFiles(downloadItems, options.output);
2006
+ const failures = results.filter((r) => r.status === "error");
2007
+ const downloadResults = results.map((r) => ({
2008
+ nodeId: r.id,
2009
+ file: r.file,
2010
+ status: r.status === "success" ? "downloaded" : r.error ?? "error"
2011
+ }));
2012
+ io.out.write(
2013
+ formatOutput(downloadResults, {
2014
+ format: options.outputFormat
2015
+ })
2016
+ );
2017
+ io.out.write("\n");
2018
+ if (failures.length > 0) {
2019
+ io.err.write(
2020
+ chalk7.yellow(
2021
+ `${failures.length} image(s) failed to download.
2022
+ `
2023
+ )
2024
+ );
2025
+ process.exit(1);
2026
+ }
2027
+ } else {
2028
+ io.out.write(
2029
+ formatOutput(images, {
2030
+ format: options.outputFormat
2031
+ })
2032
+ );
2033
+ io.out.write("\n");
2034
+ }
2035
+ } catch (error) {
2036
+ io.err.write(chalk7.red("Error exporting images.\n"));
2037
+ if (error instanceof Error) {
2038
+ io.err.write(chalk7.dim(`${error.message}
2039
+ `));
2040
+ }
2041
+ process.exit(1);
2042
+ }
2043
+ }
2044
+ );
2045
+ return cmd;
2046
+ }
2047
+
2048
+ // src/cmd/file/structure.ts
2049
+ import { Command as Command8 } from "commander";
2050
+ import chalk8 from "chalk";
2051
+ import * as fs4 from "fs";
2052
+ import * as path4 from "path";
2053
+ function extractFrameInfo(node, currentDepth, maxDepth, includeHidden) {
2054
+ const nodeWithBounds = node;
2055
+ if (!includeHidden && nodeWithBounds.visible === false) {
2056
+ return null;
2057
+ }
2058
+ const relevantTypes = [
2059
+ "FRAME",
2060
+ "COMPONENT",
2061
+ "COMPONENT_SET",
2062
+ "SECTION",
2063
+ "GROUP",
2064
+ "INSTANCE"
2065
+ ];
2066
+ if (!relevantTypes.includes(node.type)) {
2067
+ return null;
2068
+ }
2069
+ const frame = {
2070
+ id: node.id,
2071
+ name: node.name,
2072
+ type: node.type
2073
+ };
2074
+ if (nodeWithBounds.absoluteBoundingBox) {
2075
+ frame.width = Math.round(nodeWithBounds.absoluteBoundingBox.width);
2076
+ frame.height = Math.round(nodeWithBounds.absoluteBoundingBox.height);
2077
+ }
2078
+ if (currentDepth < maxDepth && "children" in node) {
2079
+ const parent = node;
2080
+ const children = (parent.children || []).map(
2081
+ (child) => extractFrameInfo(child, currentDepth + 1, maxDepth, includeHidden)
2082
+ ).filter((child) => child !== null);
2083
+ if (children.length > 0) {
2084
+ frame.children = children;
2085
+ }
2086
+ }
2087
+ return frame;
2088
+ }
2089
+ function extractStructure(file, maxDepth, includeHidden) {
2090
+ const pages = [];
2091
+ if (file.document && "children" in file.document) {
2092
+ const doc = file.document;
2093
+ for (const pageNode of doc.children || []) {
2094
+ if (pageNode.type !== "CANVAS") continue;
2095
+ const page = {
2096
+ id: pageNode.id,
2097
+ name: pageNode.name,
2098
+ frames: []
2099
+ };
2100
+ if ("children" in pageNode) {
2101
+ const pageWithChildren = pageNode;
2102
+ for (const child of pageWithChildren.children || []) {
2103
+ const frame = extractFrameInfo(child, 1, maxDepth, includeHidden);
2104
+ if (frame) {
2105
+ page.frames.push(frame);
2106
+ }
2107
+ }
2108
+ }
2109
+ pages.push(page);
2110
+ }
2111
+ }
2112
+ return {
2113
+ name: file.name,
2114
+ lastModified: file.lastModified,
2115
+ version: file.version,
2116
+ pages
2117
+ };
2118
+ }
2119
+ function formatTree(structure) {
2120
+ const lines = [];
2121
+ lines.push(`\u{1F4C1} ${structure.name}`);
2122
+ lines.push(` Last Modified: ${structure.lastModified}`);
2123
+ lines.push("");
2124
+ for (let i = 0; i < structure.pages.length; i++) {
2125
+ const page = structure.pages[i];
2126
+ const isLastPage = i === structure.pages.length - 1;
2127
+ const pagePrefix = isLastPage ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
2128
+ lines.push(`${pagePrefix} \u{1F4C4} ${page.name} (${page.id})`);
2129
+ const indent = isLastPage ? " " : "\u2502 ";
2130
+ formatFramesTree(page.frames, indent, lines);
2131
+ }
2132
+ return lines.join("\n");
2133
+ }
2134
+ function formatFramesTree(frames, indent, lines) {
2135
+ for (let i = 0; i < frames.length; i++) {
2136
+ const frame = frames[i];
2137
+ const isLast = i === frames.length - 1;
2138
+ const prefix = isLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
2139
+ const icon = getTypeIcon(frame.type);
2140
+ const sizeInfo = frame.width && frame.height ? ` (${frame.width}\xD7${frame.height})` : "";
2141
+ lines.push(`${indent}${prefix} ${icon} ${frame.name}${sizeInfo}`);
2142
+ if (frame.children && frame.children.length > 0) {
2143
+ const childIndent = indent + (isLast ? " " : "\u2502 ");
2144
+ formatFramesTree(frame.children, childIndent, lines);
2145
+ }
2146
+ }
2147
+ }
2148
+ function getTypeIcon(type) {
2149
+ switch (type) {
2150
+ case "COMPONENT":
2151
+ return "\u{1F537}";
2152
+ case "COMPONENT_SET":
2153
+ return "\u{1F536}";
2154
+ case "FRAME":
2155
+ return "\u2B1C";
2156
+ case "SECTION":
2157
+ return "\u{1F4C2}";
2158
+ case "GROUP":
2159
+ return "\u{1F4E6}";
2160
+ case "INSTANCE":
2161
+ return "\u25C7";
2162
+ default:
2163
+ return "\u25AB";
2164
+ }
2165
+ }
2166
+ function createFileStructureCommand(factory) {
2167
+ const cmd = new Command8("structure").description("Get simplified file structure").argument("<url>", "Figma URL or file key").option("-d, --depth <depth>", "Max depth of hierarchy (default: 2)", "2").option("--include-hidden", "Include hidden nodes", false).option("-f, --format <format>", "Output format (json, tree)", "json").option("-o, --output <file>", "Write output to file").option("-t, --token <token>", "Override authentication token").action(
2168
+ async (url, options) => {
2169
+ const { io } = factory;
2170
+ try {
2171
+ const parsed = parseFigmaInput(url);
2172
+ const fileKey = parsed.fileKey;
2173
+ const maxDepth = parseInt(options.depth, 10);
2174
+ if (isNaN(maxDepth) || maxDepth < 1) {
2175
+ io.err.write(
2176
+ chalk8.red(
2177
+ `Invalid depth "${options.depth}". Depth must be a positive integer.
2178
+ `
2179
+ )
2180
+ );
2181
+ process.exit(1);
2182
+ }
2183
+ const validFormats = ["json", "tree"];
2184
+ if (!validFormats.includes(
2185
+ options.format
2186
+ )) {
2187
+ io.err.write(
2188
+ chalk8.red(
2189
+ `Invalid format "${options.format}". Valid formats: ${validFormats.join(", ")}
2190
+ `
2191
+ )
2192
+ );
2193
+ process.exit(1);
2194
+ }
2195
+ const client = await factory.getClient();
2196
+ let activeClient = client;
2197
+ if (options.token) {
2198
+ const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
2199
+ activeClient = new FigmaClient2({ token: options.token });
2200
+ }
2201
+ const file = await activeClient.getFile(fileKey, {
2202
+ depth: maxDepth + 1
2203
+ // +1 to include top-level frames
2204
+ });
2205
+ const structure = extractStructure(
2206
+ file,
2207
+ maxDepth,
2208
+ options.includeHidden
2209
+ );
2210
+ let output;
2211
+ if (options.format === "tree") {
2212
+ output = formatTree(structure);
2213
+ } else {
2214
+ output = JSON.stringify(structure, null, 2);
2215
+ }
2216
+ if (options.output) {
2217
+ const outputPath = path4.resolve(options.output);
2218
+ fs4.writeFileSync(outputPath, output + "\n");
2219
+ io.out.write(chalk8.green(`Structure written to ${outputPath}
2220
+ `));
2221
+ } else {
2222
+ io.out.write(output + "\n");
2223
+ }
2224
+ } catch (error) {
2225
+ io.err.write(chalk8.red("Error fetching file structure.\n"));
2226
+ if (error instanceof Error) {
2227
+ io.err.write(chalk8.dim(`${error.message}
2228
+ `));
2229
+ }
2230
+ process.exit(1);
2231
+ }
2232
+ }
2233
+ );
2234
+ return cmd;
2235
+ }
2236
+
2237
+ // src/cmd/file/index.ts
2238
+ function createFileCommand(factory) {
2239
+ const cmd = new Command9("file").description("Work with Figma files");
2240
+ cmd.addCommand(createFileGetCommand(factory));
2241
+ cmd.addCommand(createFileNodesCommand(factory));
2242
+ cmd.addCommand(createFileImagesCommand(factory));
2243
+ cmd.addCommand(createFileStructureCommand(factory));
2244
+ return cmd;
2245
+ }
2246
+
2247
+ // src/cmd/project/index.ts
2248
+ import { Command as Command12 } from "commander";
2249
+
2250
+ // src/cmd/project/list.ts
2251
+ import { Command as Command10 } from "commander";
2252
+ import chalk9 from "chalk";
2253
+ function createProjectListCommand(factory) {
2254
+ const cmd = new Command10("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(
2255
+ async (options) => {
2256
+ const { io } = factory;
2257
+ try {
2258
+ const token = await factory.getToken(options.token);
2259
+ let client = await factory.getClient();
2260
+ if (options.token) {
2261
+ const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
2262
+ client = new FigmaClient2({ token });
2263
+ }
2264
+ const projects = await client.getTeamProjects(options.team);
2265
+ if (options.format === "table") {
2266
+ const tableData = projects.projects.map((p) => ({
2267
+ id: p.id,
2268
+ name: p.name
2269
+ }));
2270
+ io.out.write(
2271
+ formatOutput(tableData, {
2272
+ format: "table",
2273
+ columns: ["id", "name"]
2274
+ })
2275
+ );
2276
+ } else {
2277
+ io.out.write(formatOutput(projects, { format: "json" }));
2278
+ }
2279
+ io.out.write("\n");
2280
+ } catch (error) {
2281
+ io.err.write(chalk9.red("Error fetching projects.\n"));
2282
+ if (error instanceof Error) {
2283
+ io.err.write(chalk9.dim(`${error.message}
2284
+ `));
2285
+ }
2286
+ process.exit(1);
2287
+ }
2288
+ }
2289
+ );
2290
+ return cmd;
2291
+ }
2292
+
2293
+ // src/cmd/project/files.ts
2294
+ import { Command as Command11 } from "commander";
2295
+ import chalk10 from "chalk";
2296
+ function createProjectFilesCommand(factory) {
2297
+ const cmd = new Command11("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(
2298
+ async (projectId, options) => {
2299
+ const { io } = factory;
2300
+ try {
2301
+ const token = await factory.getToken(options.token);
2302
+ let client = await factory.getClient();
2303
+ if (options.token) {
2304
+ const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
2305
+ client = new FigmaClient2({ token });
2306
+ }
2307
+ const files = await client.getProjectFiles(projectId, {
2308
+ branch_data: options.branchData
2309
+ });
2310
+ if (options.format === "table") {
2311
+ const tableData = files.files.map((f) => ({
2312
+ key: f.key,
2313
+ name: f.name,
2314
+ last_modified: f.last_modified
2315
+ }));
2316
+ io.out.write(
2317
+ formatOutput(tableData, {
2318
+ format: "table",
2319
+ columns: ["key", "name", "last_modified"]
2320
+ })
2321
+ );
2322
+ } else {
2323
+ io.out.write(formatOutput(files, { format: "json" }));
2324
+ }
2325
+ io.out.write("\n");
2326
+ } catch (error) {
2327
+ io.err.write(chalk10.red("Error fetching project files.\n"));
2328
+ if (error instanceof Error) {
2329
+ io.err.write(chalk10.dim(`${error.message}
2330
+ `));
2331
+ }
2332
+ process.exit(1);
2333
+ }
2334
+ }
2335
+ );
2336
+ return cmd;
2337
+ }
2338
+
1586
2339
  // src/cmd/project/index.ts
1587
2340
  function createProjectCommand(factory) {
1588
- const cmd = new Command11("project").description("Work with Figma projects");
2341
+ const cmd = new Command12("project").description("Work with Figma projects");
1589
2342
  cmd.addCommand(createProjectListCommand(factory));
1590
2343
  cmd.addCommand(createProjectFilesCommand(factory));
1591
2344
  return cmd;
1592
2345
  }
1593
2346
 
1594
2347
  // src/cmd/component/index.ts
1595
- import { Command as Command14 } from "commander";
2348
+ import { Command as Command15 } from "commander";
1596
2349
 
1597
2350
  // src/cmd/component/list.ts
1598
- import { Command as Command12 } from "commander";
1599
- import chalk10 from "chalk";
2351
+ import { Command as Command13 } from "commander";
2352
+ import chalk11 from "chalk";
1600
2353
  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(
2354
+ const cmd = new Command13("list").description("List components (from team or file)").option("--team <id>", "Team ID to list components from").option("--file <url>", "Figma URL or file key").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
2355
  async (options) => {
1603
2356
  const { io } = factory;
1604
2357
  if (!options.team && !options.file) {
1605
2358
  io.err.write(
1606
- chalk10.red("Error: Either --team or --file must be specified.\n")
2359
+ chalk11.red("Error: Either --team or --file must be specified.\n")
1607
2360
  );
1608
2361
  process.exit(1);
1609
2362
  }
@@ -1634,7 +2387,8 @@ function createComponentListCommand(factory) {
1634
2387
  io.out.write(formatOutput(components, { format: "json" }));
1635
2388
  }
1636
2389
  } else if (options.file) {
1637
- const components = await client.getFileComponents(options.file);
2390
+ const parsed = parseFigmaInput(options.file);
2391
+ const components = await client.getFileComponents(parsed.fileKey);
1638
2392
  if (options.format === "table") {
1639
2393
  const tableData = components.meta?.components?.map((c) => ({
1640
2394
  key: c.key,
@@ -1653,9 +2407,9 @@ function createComponentListCommand(factory) {
1653
2407
  }
1654
2408
  io.out.write("\n");
1655
2409
  } catch (error) {
1656
- io.err.write(chalk10.red("Error fetching components.\n"));
2410
+ io.err.write(chalk11.red("Error fetching components.\n"));
1657
2411
  if (error instanceof Error) {
1658
- io.err.write(chalk10.dim(`${error.message}
2412
+ io.err.write(chalk11.dim(`${error.message}
1659
2413
  `));
1660
2414
  }
1661
2415
  process.exit(1);
@@ -1666,10 +2420,10 @@ function createComponentListCommand(factory) {
1666
2420
  }
1667
2421
 
1668
2422
  // src/cmd/component/get.ts
1669
- import { Command as Command13 } from "commander";
1670
- import chalk11 from "chalk";
2423
+ import { Command as Command14 } from "commander";
2424
+ import chalk12 from "chalk";
1671
2425
  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(
2426
+ const cmd = new Command14("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
2427
  async (componentKey, options) => {
1674
2428
  const { io } = factory;
1675
2429
  try {
@@ -1687,9 +2441,9 @@ function createComponentGetCommand(factory) {
1687
2441
  );
1688
2442
  io.out.write("\n");
1689
2443
  } catch (error) {
1690
- io.err.write(chalk11.red("Error fetching component.\n"));
2444
+ io.err.write(chalk12.red("Error fetching component.\n"));
1691
2445
  if (error instanceof Error) {
1692
- io.err.write(chalk11.dim(`${error.message}
2446
+ io.err.write(chalk12.dim(`${error.message}
1693
2447
  `));
1694
2448
  }
1695
2449
  process.exit(1);
@@ -1701,7 +2455,7 @@ function createComponentGetCommand(factory) {
1701
2455
 
1702
2456
  // src/cmd/component/index.ts
1703
2457
  function createComponentCommand(factory) {
1704
- const cmd = new Command14("component").description(
2458
+ const cmd = new Command15("component").description(
1705
2459
  "Work with Figma components"
1706
2460
  );
1707
2461
  cmd.addCommand(createComponentListCommand(factory));
@@ -1710,18 +2464,18 @@ function createComponentCommand(factory) {
1710
2464
  }
1711
2465
 
1712
2466
  // src/cmd/style/index.ts
1713
- import { Command as Command17 } from "commander";
2467
+ import { Command as Command18 } from "commander";
1714
2468
 
1715
2469
  // src/cmd/style/list.ts
1716
- import { Command as Command15 } from "commander";
1717
- import chalk12 from "chalk";
2470
+ import { Command as Command16 } from "commander";
2471
+ import chalk13 from "chalk";
1718
2472
  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(
2473
+ const cmd = new Command16("list").description("List styles (from team or file)").option("--team <id>", "Team ID to list styles from").option("--file <url>", "Figma URL or file key").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
2474
  async (options) => {
1721
2475
  const { io } = factory;
1722
2476
  if (!options.team && !options.file) {
1723
2477
  io.err.write(
1724
- chalk12.red("Error: Either --team or --file must be specified.\n")
2478
+ chalk13.red("Error: Either --team or --file must be specified.\n")
1725
2479
  );
1726
2480
  process.exit(1);
1727
2481
  }
@@ -1753,7 +2507,8 @@ function createStyleListCommand(factory) {
1753
2507
  io.out.write(formatOutput(styles, { format: "json" }));
1754
2508
  }
1755
2509
  } else if (options.file) {
1756
- const styles = await client.getFileStyles(options.file);
2510
+ const parsed = parseFigmaInput(options.file);
2511
+ const styles = await client.getFileStyles(parsed.fileKey);
1757
2512
  if (options.format === "table") {
1758
2513
  const tableData = styles.meta?.styles?.map((s) => ({
1759
2514
  key: s.key,
@@ -1773,9 +2528,9 @@ function createStyleListCommand(factory) {
1773
2528
  }
1774
2529
  io.out.write("\n");
1775
2530
  } catch (error) {
1776
- io.err.write(chalk12.red("Error fetching styles.\n"));
2531
+ io.err.write(chalk13.red("Error fetching styles.\n"));
1777
2532
  if (error instanceof Error) {
1778
- io.err.write(chalk12.dim(`${error.message}
2533
+ io.err.write(chalk13.dim(`${error.message}
1779
2534
  `));
1780
2535
  }
1781
2536
  process.exit(1);
@@ -1786,10 +2541,10 @@ function createStyleListCommand(factory) {
1786
2541
  }
1787
2542
 
1788
2543
  // src/cmd/style/get.ts
1789
- import { Command as Command16 } from "commander";
1790
- import chalk13 from "chalk";
2544
+ import { Command as Command17 } from "commander";
2545
+ import chalk14 from "chalk";
1791
2546
  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(
2547
+ const cmd = new Command17("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
2548
  async (styleKey, options) => {
1794
2549
  const { io } = factory;
1795
2550
  try {
@@ -1807,9 +2562,9 @@ function createStyleGetCommand(factory) {
1807
2562
  );
1808
2563
  io.out.write("\n");
1809
2564
  } catch (error) {
1810
- io.err.write(chalk13.red("Error fetching style.\n"));
2565
+ io.err.write(chalk14.red("Error fetching style.\n"));
1811
2566
  if (error instanceof Error) {
1812
- io.err.write(chalk13.dim(`${error.message}
2567
+ io.err.write(chalk14.dim(`${error.message}
1813
2568
  `));
1814
2569
  }
1815
2570
  process.exit(1);
@@ -1821,23 +2576,25 @@ function createStyleGetCommand(factory) {
1821
2576
 
1822
2577
  // src/cmd/style/index.ts
1823
2578
  function createStyleCommand(factory) {
1824
- const cmd = new Command17("style").description("Work with Figma styles");
2579
+ const cmd = new Command18("style").description("Work with Figma styles");
1825
2580
  cmd.addCommand(createStyleListCommand(factory));
1826
2581
  cmd.addCommand(createStyleGetCommand(factory));
1827
2582
  return cmd;
1828
2583
  }
1829
2584
 
1830
2585
  // src/cmd/variable/index.ts
1831
- import { Command as Command20 } from "commander";
2586
+ import { Command as Command21 } from "commander";
1832
2587
 
1833
2588
  // src/cmd/variable/list.ts
1834
- import { Command as Command18 } from "commander";
1835
- import chalk14 from "chalk";
2589
+ import { Command as Command19 } from "commander";
2590
+ import chalk15 from "chalk";
1836
2591
  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) => {
2592
+ const cmd = new Command19("list").description("List variables from a file (Enterprise)").argument("<url>", "Figma URL or 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(
2593
+ async (url, options) => {
1839
2594
  const { io } = factory;
1840
2595
  try {
2596
+ const parsed = parseFigmaInput(url);
2597
+ const fileKey = parsed.fileKey;
1841
2598
  const token = await factory.getToken(options.token);
1842
2599
  let client = await factory.getClient();
1843
2600
  if (options.token) {
@@ -1865,13 +2622,13 @@ function createVariableListCommand(factory) {
1865
2622
  }
1866
2623
  io.out.write("\n");
1867
2624
  } catch (error) {
1868
- io.err.write(chalk14.red("Error fetching variables.\n"));
2625
+ io.err.write(chalk15.red("Error fetching variables.\n"));
1869
2626
  if (error instanceof Error) {
1870
- io.err.write(chalk14.dim(`${error.message}
2627
+ io.err.write(chalk15.dim(`${error.message}
1871
2628
  `));
1872
2629
  }
1873
2630
  io.err.write(
1874
- chalk14.dim("\nNote: Variables API requires Figma Enterprise plan.\n")
2631
+ chalk15.dim("\nNote: Variables API requires Figma Enterprise plan.\n")
1875
2632
  );
1876
2633
  process.exit(1);
1877
2634
  }
@@ -1881,28 +2638,115 @@ function createVariableListCommand(factory) {
1881
2638
  }
1882
2639
 
1883
2640
  // src/cmd/variable/export.ts
1884
- import { Command as Command19 } from "commander";
1885
- import chalk15 from "chalk";
1886
- import * as fs4 from "fs";
2641
+ import { Command as Command20 } from "commander";
2642
+ import chalk16 from "chalk";
2643
+ import * as fs5 from "fs";
2644
+ var SCOPE_CATEGORIES = {
2645
+ colors: [
2646
+ "ALL_FILLS",
2647
+ "FRAME_FILL",
2648
+ "SHAPE_FILL",
2649
+ "TEXT_FILL",
2650
+ "STROKE_COLOR"
2651
+ ],
2652
+ spacing: ["GAP", "WIDTH_HEIGHT"],
2653
+ radius: ["CORNER_RADIUS"],
2654
+ effects: ["EFFECT_COLOR"],
2655
+ typography: [
2656
+ "FONT_FAMILY",
2657
+ "FONT_SIZE",
2658
+ "FONT_WEIGHT",
2659
+ "LINE_HEIGHT",
2660
+ "LETTER_SPACING"
2661
+ ],
2662
+ other: []
2663
+ };
2664
+ function categorizeVariables(variables) {
2665
+ const categorized = {
2666
+ colors: [],
2667
+ spacing: [],
2668
+ radius: [],
2669
+ effects: [],
2670
+ typography: [],
2671
+ other: []
2672
+ };
2673
+ for (const variable of Object.values(variables)) {
2674
+ const scopes = variable.scopes || [];
2675
+ let placed = false;
2676
+ for (const [category, categoryScopes] of Object.entries(SCOPE_CATEGORIES)) {
2677
+ if (category === "other") continue;
2678
+ if (scopes.some((scope) => categoryScopes.includes(scope))) {
2679
+ categorized[category].push(variable);
2680
+ placed = true;
2681
+ break;
2682
+ }
2683
+ }
2684
+ if (!placed) {
2685
+ switch (variable.resolvedType) {
2686
+ case "COLOR":
2687
+ categorized.colors.push(variable);
2688
+ break;
2689
+ case "FLOAT": {
2690
+ const nameLower = variable.name.toLowerCase();
2691
+ if (nameLower.includes("radius") || nameLower.includes("corner")) {
2692
+ categorized.radius.push(variable);
2693
+ } else if (nameLower.includes("spacing") || nameLower.includes("gap") || nameLower.includes("padding") || nameLower.includes("margin")) {
2694
+ categorized.spacing.push(variable);
2695
+ } else {
2696
+ categorized.other.push(variable);
2697
+ }
2698
+ break;
2699
+ }
2700
+ default:
2701
+ categorized.other.push(variable);
2702
+ }
2703
+ }
2704
+ }
2705
+ return Object.fromEntries(
2706
+ Object.entries(categorized).filter(([, vars]) => vars.length > 0)
2707
+ );
2708
+ }
1887
2709
  function createVariableExportCommand(factory) {
1888
- const cmd = new Command19("export").description("Export variables as design tokens (Enterprise)").argument("<file-key>", "File key").option(
2710
+ const cmd = new Command20("export").description("Export variables as design tokens (Enterprise)").argument("<url>", "Figma URL or file key").option(
1889
2711
  "--format <format>",
1890
2712
  "Output format (json, css, scss, style-dictionary)",
1891
2713
  "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) => {
2714
+ ).option("--mode <mode>", "Specific mode to export").option(
2715
+ "--categorize",
2716
+ "Group variables by scope (CORNER_RADIUS, GAP, etc.)",
2717
+ false
2718
+ ).option("-o, --output <file>", "Output file path").option("-t, --token <token>", "Override authentication token").action(
2719
+ async (url, options) => {
1894
2720
  const { io } = factory;
2721
+ const validFormats = [
2722
+ "json",
2723
+ "css",
2724
+ "scss",
2725
+ "style-dictionary"
2726
+ ];
2727
+ if (!validFormats.includes(
2728
+ options.format
2729
+ )) {
2730
+ io.err.write(
2731
+ chalk16.red(
2732
+ `Invalid format "${options.format}". Valid formats: ${validFormats.join(", ")}
2733
+ `
2734
+ )
2735
+ );
2736
+ process.exit(1);
2737
+ }
1895
2738
  try {
1896
- const token = await factory.getToken(options.token);
2739
+ const parsed = parseFigmaInput(url);
2740
+ const fileKey = parsed.fileKey;
1897
2741
  let client = await factory.getClient();
1898
2742
  if (options.token) {
1899
2743
  const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
1900
- client = new FigmaClient2({ token });
2744
+ client = new FigmaClient2({ token: options.token });
1901
2745
  }
1902
2746
  const response = await client.getLocalVariables(fileKey);
1903
2747
  const meta = response.meta;
1904
2748
  if (!meta) {
1905
- io.err.write(chalk15.red("No variables found in file.\n"));
2749
+ io.err.write(chalk16.red("No variables found in file.\n"));
1906
2750
  process.exit(1);
1907
2751
  }
1908
2752
  const variables = meta.variables;
@@ -1919,13 +2763,13 @@ function createVariableExportCommand(factory) {
1919
2763
  }
1920
2764
  }
1921
2765
  if (!targetModeId) {
1922
- io.err.write(chalk15.red(`Mode "${options.mode}" not found.
2766
+ io.err.write(chalk16.red(`Mode "${options.mode}" not found.
1923
2767
  `));
1924
2768
  const allModes = Object.values(collections).flatMap(
1925
2769
  (c) => c.modes.map((m) => m.name)
1926
2770
  );
1927
2771
  io.err.write(
1928
- chalk15.dim(`Available modes: ${allModes.join(", ")}
2772
+ chalk16.dim(`Available modes: ${allModes.join(", ")}
1929
2773
  `)
1930
2774
  );
1931
2775
  process.exit(1);
@@ -1934,37 +2778,53 @@ function createVariableExportCommand(factory) {
1934
2778
  let output;
1935
2779
  switch (options.format) {
1936
2780
  case "css":
1937
- output = exportToCss(variables, collections, targetModeId);
2781
+ output = exportToCss(
2782
+ variables,
2783
+ collections,
2784
+ targetModeId,
2785
+ options.categorize
2786
+ );
1938
2787
  break;
1939
2788
  case "scss":
1940
- output = exportToScss(variables, collections, targetModeId);
2789
+ output = exportToScss(
2790
+ variables,
2791
+ collections,
2792
+ targetModeId,
2793
+ options.categorize
2794
+ );
1941
2795
  break;
1942
2796
  case "style-dictionary":
1943
2797
  output = exportToStyleDictionary(
1944
2798
  variables,
1945
2799
  collections,
1946
- targetModeId
2800
+ targetModeId,
2801
+ options.categorize
1947
2802
  );
1948
2803
  break;
1949
2804
  default:
1950
- output = JSON.stringify({ variables, collections }, null, 2);
2805
+ if (options.categorize) {
2806
+ const categorized = categorizeVariables(variables);
2807
+ output = JSON.stringify({ categorized, collections }, null, 2);
2808
+ } else {
2809
+ output = JSON.stringify({ variables, collections }, null, 2);
2810
+ }
1951
2811
  }
1952
2812
  if (options.output) {
1953
- fs4.writeFileSync(options.output, output);
1954
- io.out.write(chalk15.green(`\u2713 Exported to ${options.output}
2813
+ fs5.writeFileSync(options.output, output);
2814
+ io.out.write(chalk16.green(`\u2713 Exported to ${options.output}
1955
2815
  `));
1956
2816
  } else {
1957
2817
  io.out.write(output);
1958
2818
  io.out.write("\n");
1959
2819
  }
1960
2820
  } catch (error) {
1961
- io.err.write(chalk15.red("Error exporting variables.\n"));
2821
+ io.err.write(chalk16.red("Error exporting variables.\n"));
1962
2822
  if (error instanceof Error) {
1963
- io.err.write(chalk15.dim(`${error.message}
2823
+ io.err.write(chalk16.dim(`${error.message}
1964
2824
  `));
1965
2825
  }
1966
2826
  io.err.write(
1967
- chalk15.dim("\nNote: Variables API requires Figma Enterprise plan.\n")
2827
+ chalk16.dim("\nNote: Variables API requires Figma Enterprise plan.\n")
1968
2828
  );
1969
2829
  process.exit(1);
1970
2830
  }
@@ -1972,9 +2832,6 @@ function createVariableExportCommand(factory) {
1972
2832
  );
1973
2833
  return cmd;
1974
2834
  }
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
2835
  function resolveValue(value, variables, _modeId) {
1979
2836
  if (typeof value === "object" && value !== null) {
1980
2837
  if ("r" in value && "g" in value && "b" in value) {
@@ -2001,47 +2858,90 @@ function resolveValue(value, variables, _modeId) {
2001
2858
  }
2002
2859
  return String(value);
2003
2860
  }
2004
- function exportToCss(variables, collections, modeId) {
2861
+ function exportToCss(variables, collections, modeId, categorize) {
2005
2862
  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};`);
2863
+ if (categorize) {
2864
+ const categorized = categorizeVariables(variables);
2865
+ for (const [category, categoryVars] of Object.entries(categorized)) {
2866
+ if (categoryVars.length === 0) continue;
2867
+ lines.push(
2868
+ ` /* ${category.charAt(0).toUpperCase() + category.slice(1)} */`
2869
+ );
2870
+ for (const variable of categoryVars) {
2871
+ const collection = collections[variable.variableCollectionId];
2872
+ const targetMode = modeId || collection?.defaultModeId;
2873
+ const value = variable.valuesByMode[targetMode];
2874
+ if (value !== void 0) {
2875
+ const cssName = toKebabCase(variable.name);
2876
+ const cssValue = resolveValue(value, variables, targetMode);
2877
+ lines.push(` --${cssName}: ${cssValue};`);
2878
+ }
2879
+ }
2880
+ lines.push("");
2881
+ }
2882
+ } else {
2883
+ for (const variable of Object.values(variables)) {
2884
+ const collection = collections[variable.variableCollectionId];
2885
+ const targetMode = modeId || collection?.defaultModeId;
2886
+ const value = variable.valuesByMode[targetMode];
2887
+ if (value !== void 0) {
2888
+ const cssName = toKebabCase(variable.name);
2889
+ const cssValue = resolveValue(value, variables, targetMode);
2890
+ lines.push(` --${cssName}: ${cssValue};`);
2891
+ }
2014
2892
  }
2015
2893
  }
2016
2894
  lines.push("}");
2017
2895
  return lines.join("\n");
2018
2896
  }
2019
- function exportToScss(variables, collections, modeId) {
2897
+ function exportToScss(variables, collections, modeId, categorize) {
2020
2898
  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};`);
2899
+ if (categorize) {
2900
+ const categorized = categorizeVariables(variables);
2901
+ for (const [category, categoryVars] of Object.entries(categorized)) {
2902
+ if (categoryVars.length === 0) continue;
2903
+ lines.push(`// ${category.charAt(0).toUpperCase() + category.slice(1)}`);
2904
+ for (const variable of categoryVars) {
2905
+ const collection = collections[variable.variableCollectionId];
2906
+ const targetMode = modeId || collection?.defaultModeId;
2907
+ const value = variable.valuesByMode[targetMode];
2908
+ if (value !== void 0) {
2909
+ const scssName = toKebabCase(variable.name);
2910
+ const scssValue = resolveValue(value, variables, targetMode).replace(
2911
+ /var\(--([^)]+)\)/g,
2912
+ "$$$1"
2913
+ );
2914
+ lines.push(`$${scssName}: ${scssValue};`);
2915
+ }
2916
+ }
2917
+ lines.push("");
2918
+ }
2919
+ } else {
2920
+ for (const variable of Object.values(variables)) {
2921
+ const collection = collections[variable.variableCollectionId];
2922
+ const targetMode = modeId || collection?.defaultModeId;
2923
+ const value = variable.valuesByMode[targetMode];
2924
+ if (value !== void 0) {
2925
+ const scssName = toKebabCase(variable.name);
2926
+ const scssValue = resolveValue(value, variables, targetMode).replace(
2927
+ /var\(--([^)]+)\)/g,
2928
+ "$$$1"
2929
+ );
2930
+ lines.push(`$${scssName}: ${scssValue};`);
2931
+ }
2032
2932
  }
2033
2933
  }
2034
2934
  return lines.join("\n");
2035
2935
  }
2036
- function exportToStyleDictionary(variables, collections, modeId) {
2936
+ function exportToStyleDictionary(variables, collections, modeId, categorize) {
2037
2937
  const tokens = {};
2038
- for (const variable of Object.values(variables)) {
2938
+ const processVariable = (variable, targetTokens) => {
2039
2939
  const collection = collections[variable.variableCollectionId];
2040
2940
  const targetMode = modeId || collection?.defaultModeId;
2041
2941
  const value = variable.valuesByMode[targetMode];
2042
- if (value === void 0) continue;
2942
+ if (value === void 0) return;
2043
2943
  const pathParts = variable.name.split("/").map((p) => p.trim());
2044
- let current = tokens;
2944
+ let current = targetTokens;
2045
2945
  for (let i = 0; i < pathParts.length - 1; i++) {
2046
2946
  const part = pathParts[i];
2047
2947
  if (!current[part]) {
@@ -2055,6 +2955,20 @@ function exportToStyleDictionary(variables, collections, modeId) {
2055
2955
  type: mapTypeToStyleDictionary(variable.resolvedType),
2056
2956
  description: variable.description || void 0
2057
2957
  };
2958
+ };
2959
+ if (categorize) {
2960
+ const categorized = categorizeVariables(variables);
2961
+ for (const [category, categoryVars] of Object.entries(categorized)) {
2962
+ if (categoryVars.length === 0) continue;
2963
+ tokens[category] = {};
2964
+ for (const variable of categoryVars) {
2965
+ processVariable(variable, tokens[category]);
2966
+ }
2967
+ }
2968
+ } else {
2969
+ for (const variable of Object.values(variables)) {
2970
+ processVariable(variable, tokens);
2971
+ }
2058
2972
  }
2059
2973
  return JSON.stringify(tokens, null, 2);
2060
2974
  }
@@ -2096,266 +3010,89 @@ function mapTypeToStyleDictionary(figmaType) {
2096
3010
  return figmaType.toLowerCase();
2097
3011
  }
2098
3012
  }
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)};`;
3013
+
3014
+ // src/cmd/variable/index.ts
3015
+ function createVariableCommand(factory) {
3016
+ const cmd = new Command21("variable").description(
3017
+ "Work with Figma variables (Enterprise)"
3018
+ );
3019
+ cmd.addCommand(createVariableListCommand(factory));
3020
+ cmd.addCommand(createVariableExportCommand(factory));
3021
+ return cmd;
2320
3022
  }
2321
3023
 
3024
+ // src/cmd/export/index.ts
3025
+ import { Command as Command25 } from "commander";
3026
+
2322
3027
  // src/cmd/export/tokens.ts
3028
+ import { Command as Command22 } from "commander";
3029
+ import chalk17 from "chalk";
3030
+ import * as fs6 from "fs";
2323
3031
  function createExportTokensCommand(factory) {
2324
- const cmd = new Command21("tokens").description("Export design tokens from a Figma file").argument("<file-key>", "File key").option(
3032
+ const cmd = new Command22("tokens").description("Export design tokens from a Figma file").argument("<url>", "Figma URL or file key").option(
2325
3033
  "--format <format>",
2326
3034
  "Output format (json, css, scss, style-dictionary, tailwind)",
2327
3035
  "json"
2328
3036
  ).option("-o, --output <file>", "Output file path").option("-t, --token <token>", "Override authentication token").action(
2329
- async (fileKey, options) => {
3037
+ async (url, options) => {
2330
3038
  const { io } = factory;
3039
+ const validFormats = [
3040
+ "json",
3041
+ "css",
3042
+ "scss",
3043
+ "style-dictionary",
3044
+ "tailwind"
3045
+ ];
3046
+ if (!validFormats.includes(
3047
+ options.format
3048
+ )) {
3049
+ io.err.write(
3050
+ chalk17.red(
3051
+ `Invalid format "${options.format}". Valid formats: ${validFormats.join(", ")}
3052
+ `
3053
+ )
3054
+ );
3055
+ process.exit(1);
3056
+ }
2331
3057
  try {
2332
- const token = await factory.getToken(options.token);
3058
+ const parsed = parseFigmaInput(url);
3059
+ const fileKey = parsed.fileKey;
2333
3060
  let client = await factory.getClient();
2334
3061
  if (options.token) {
2335
3062
  const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
2336
- client = new FigmaClient2({ token });
3063
+ client = new FigmaClient2({ token: options.token });
2337
3064
  }
2338
- io.err.write(chalk16.dim("Fetching file styles...\n"));
3065
+ io.err.write(chalk17.dim("Fetching file styles...\n"));
2339
3066
  const stylesResponse = await client.getFileStyles(fileKey);
2340
3067
  const styles = stylesResponse.meta?.styles || [];
2341
3068
  if (styles.length === 0) {
2342
3069
  io.err.write(
2343
- chalk16.yellow("No styles found in file. Trying variables...\n")
3070
+ chalk17.yellow("No styles found in file. Trying variables...\n")
2344
3071
  );
2345
3072
  try {
2346
3073
  const variablesResponse = await client.getLocalVariables(fileKey);
2347
3074
  if (variablesResponse.meta?.variables) {
2348
3075
  io.err.write(
2349
- chalk16.dim(
3076
+ chalk17.dim(
2350
3077
  "Found variables. Use 'figma variable export' for variable-based tokens.\n"
2351
3078
  )
2352
3079
  );
2353
3080
  }
2354
- } catch {
3081
+ } catch (error) {
3082
+ const message = error instanceof Error ? error.message.toLowerCase() : "";
3083
+ const isPermissionError = message.includes("403") || message.includes("enterprise") || message.includes("permission") || message.includes("forbidden");
3084
+ if (!isPermissionError) {
3085
+ io.err.write(
3086
+ chalk17.dim(
3087
+ `Note: Could not check for variables: ${error instanceof Error ? error.message : String(error)}
3088
+ `
3089
+ )
3090
+ );
3091
+ }
2355
3092
  }
2356
3093
  process.exit(0);
2357
3094
  }
2358
- io.err.write(chalk16.dim("Extracting token values...\n"));
3095
+ io.err.write(chalk17.dim("Extracting token values...\n"));
2359
3096
  const file = await client.getFile(fileKey);
2360
3097
  const tokens = extractStylesFromFile(file, styles);
2361
3098
  let output;
@@ -2376,17 +3113,17 @@ function createExportTokensCommand(factory) {
2376
3113
  output = JSON.stringify(tokens, null, 2);
2377
3114
  }
2378
3115
  if (options.output) {
2379
- fs5.writeFileSync(options.output, output);
2380
- io.out.write(chalk16.green(`\u2713 Exported to ${options.output}
3116
+ fs6.writeFileSync(options.output, output);
3117
+ io.out.write(chalk17.green(`\u2713 Exported to ${options.output}
2381
3118
  `));
2382
3119
  } else {
2383
3120
  io.out.write(output);
2384
3121
  io.out.write("\n");
2385
3122
  }
2386
3123
  } catch (error) {
2387
- io.err.write(chalk16.red("Error exporting tokens.\n"));
3124
+ io.err.write(chalk17.red("Error exporting tokens.\n"));
2388
3125
  if (error instanceof Error) {
2389
- io.err.write(chalk16.dim(`${error.message}
3126
+ io.err.write(chalk17.dim(`${error.message}
2390
3127
  `));
2391
3128
  }
2392
3129
  process.exit(1);
@@ -2397,19 +3134,67 @@ function createExportTokensCommand(factory) {
2397
3134
  }
2398
3135
 
2399
3136
  // src/cmd/export/icons.ts
2400
- import { Command as Command22 } from "commander";
2401
- import chalk17 from "chalk";
3137
+ import { Command as Command23 } from "commander";
3138
+ import chalk18 from "chalk";
3139
+ import * as path5 from "path";
3140
+ import * as fs7 from "fs";
3141
+ function parseVariantName(name) {
3142
+ const parts = name.split("/");
3143
+ const variants = [];
3144
+ let baseName = "";
3145
+ for (const part of parts) {
3146
+ if (part.includes("=")) {
3147
+ const [, value] = part.split("=");
3148
+ if (value) {
3149
+ variants.push(value.trim());
3150
+ }
3151
+ } else {
3152
+ baseName = baseName ? `${baseName}-${part}` : part;
3153
+ }
3154
+ }
3155
+ return { baseName: baseName || name, variants };
3156
+ }
3157
+ function generateVariantFilename(name, variantFormat, prefix, imageFormat) {
3158
+ const { baseName, variants } = parseVariantName(name);
3159
+ const kebabBase = toKebabCase(baseName);
3160
+ switch (variantFormat) {
3161
+ case "suffix":
3162
+ if (variants.length > 0) {
3163
+ const variantSuffix = variants.map((v) => toKebabCase(v)).join("-");
3164
+ return {
3165
+ filename: `${prefix}${kebabBase}--${variantSuffix}.${imageFormat}`
3166
+ };
3167
+ }
3168
+ return { filename: `${prefix}${kebabBase}.${imageFormat}` };
3169
+ case "folder":
3170
+ if (variants.length > 0) {
3171
+ const variantSuffix = variants.map((v) => toKebabCase(v)).join("-");
3172
+ return {
3173
+ filename: `${variantSuffix}.${imageFormat}`,
3174
+ subdir: `${prefix}${kebabBase}`
3175
+ };
3176
+ }
3177
+ return { filename: `${prefix}${kebabBase}.${imageFormat}` };
3178
+ case "flat":
3179
+ default:
3180
+ return { filename: `${prefix}${toKebabCase(name)}.${imageFormat}` };
3181
+ }
3182
+ }
2402
3183
  function createExportIconsCommand(factory) {
2403
- const cmd = new Command22("icons").description("Export icons from a Figma file").argument("<file-key>", "File key").option(
3184
+ const cmd = new Command23("icons").description("Export icons from a Figma file").argument("<url>", "Figma URL or file key").option(
2404
3185
  "--frame <name>",
2405
3186
  "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) => {
3187
+ ).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(
3188
+ "--variant-format <format>",
3189
+ "How to handle variants: flat, suffix, or folder (default: flat)",
3190
+ "flat"
3191
+ ).option("-t, --token <token>", "Override authentication token").action(
3192
+ async (url, options) => {
2408
3193
  const { io } = factory;
2409
3194
  const validFormats = ["svg", "png"];
2410
3195
  if (!validFormats.includes(options.format)) {
2411
3196
  io.err.write(
2412
- chalk17.red(
3197
+ chalk18.red(
2413
3198
  `Invalid format "${options.format}". Valid formats: ${validFormats.join(", ")}
2414
3199
  `
2415
3200
  )
@@ -2445,30 +3230,32 @@ function createExportIconsCommand(factory) {
2445
3230
  );
2446
3231
  };
2447
3232
  var findIcons = findIcons2, hasDeepChildren = hasDeepChildren2;
3233
+ const parsed = parseFigmaInput(url);
3234
+ const fileKey = parsed.fileKey;
2448
3235
  const token = await factory.getToken(options.token);
2449
3236
  let client = await factory.getClient();
2450
3237
  if (options.token) {
2451
3238
  const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
2452
3239
  client = new FigmaClient2({ token });
2453
3240
  }
2454
- io.err.write(chalk17.dim("Fetching file structure...\n"));
3241
+ io.err.write(chalk18.dim("Fetching file structure...\n"));
2455
3242
  const file = await client.getFile(fileKey, { depth: 2 });
2456
3243
  const iconNodes = [];
2457
3244
  if (file.document) {
2458
3245
  findIcons2(file.document, false);
2459
3246
  }
2460
3247
  if (iconNodes.length === 0) {
2461
- io.err.write(chalk17.yellow("No icons found in file.\n"));
3248
+ io.err.write(chalk18.yellow("No icons found in file.\n"));
2462
3249
  if (options.frame) {
2463
3250
  io.err.write(
2464
- chalk17.dim(`Looking for frame: "${options.frame}"
3251
+ chalk18.dim(`Looking for frame: "${options.frame}"
2465
3252
  `)
2466
3253
  );
2467
3254
  }
2468
3255
  process.exit(0);
2469
3256
  }
2470
3257
  io.err.write(
2471
- chalk17.dim(`Found ${iconNodes.length} icons. Exporting...
3258
+ chalk18.dim(`Found ${iconNodes.length} icons. Exporting...
2472
3259
  `)
2473
3260
  );
2474
3261
  const nodeIds = iconNodes.map((n) => n.id);
@@ -2479,12 +3266,51 @@ function createExportIconsCommand(factory) {
2479
3266
  svg_simplify_stroke: true
2480
3267
  });
2481
3268
  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
- }));
3269
+ const validVariantFormats = ["flat", "suffix", "folder"];
3270
+ if (!validVariantFormats.includes(
3271
+ options.variantFormat
3272
+ )) {
3273
+ io.err.write(
3274
+ chalk18.red(
3275
+ `Invalid variant format "${options.variantFormat}". Valid formats: ${validVariantFormats.join(", ")}
3276
+ `
3277
+ )
3278
+ );
3279
+ process.exit(1);
3280
+ }
3281
+ const variantFormat = options.variantFormat;
3282
+ const subdirs = /* @__PURE__ */ new Set();
3283
+ for (const node of iconNodes) {
3284
+ const { subdir } = generateVariantFilename(
3285
+ node.name,
3286
+ variantFormat,
3287
+ options.prefix,
3288
+ imageFormat
3289
+ );
3290
+ if (subdir) {
3291
+ subdirs.add(subdir);
3292
+ }
3293
+ }
3294
+ fs7.mkdirSync(options.output, { recursive: true });
3295
+ for (const subdir of subdirs) {
3296
+ fs7.mkdirSync(path5.join(options.output, subdir), {
3297
+ recursive: true
3298
+ });
3299
+ }
3300
+ const downloadItems = iconNodes.map((node) => {
3301
+ const { filename, subdir } = generateVariantFilename(
3302
+ node.name,
3303
+ variantFormat,
3304
+ options.prefix,
3305
+ imageFormat
3306
+ );
3307
+ return {
3308
+ id: node.id,
3309
+ name: node.name,
3310
+ url: imageUrls[node.id] ?? null,
3311
+ filename: subdir ? path5.join(subdir, filename) : filename
3312
+ };
3313
+ });
2488
3314
  const results = await downloadFiles(downloadItems, options.output);
2489
3315
  const successCount = results.filter(
2490
3316
  (r) => r.status === "success"
@@ -2492,25 +3318,25 @@ function createExportIconsCommand(factory) {
2492
3318
  const failures = results.filter((r) => r.status === "error");
2493
3319
  for (const failure of failures) {
2494
3320
  io.err.write(
2495
- chalk17.yellow(
3321
+ chalk18.yellow(
2496
3322
  `Warning: Failed to download "${failure.name ?? failure.id}": ${failure.error}
2497
3323
  `
2498
3324
  )
2499
3325
  );
2500
3326
  }
2501
3327
  io.err.write(
2502
- chalk17.green(`Exported ${successCount} icons to ${options.output}
3328
+ chalk18.green(`Exported ${successCount} icons to ${options.output}
2503
3329
  `)
2504
3330
  );
2505
3331
  if (failures.length > 0) {
2506
- io.err.write(chalk17.yellow(`${failures.length} icons failed
3332
+ io.err.write(chalk18.yellow(`${failures.length} icons failed
2507
3333
  `));
2508
3334
  process.exit(1);
2509
3335
  }
2510
3336
  } catch (error) {
2511
- io.err.write(chalk17.red("Error exporting icons.\n"));
3337
+ io.err.write(chalk18.red("Error exporting icons.\n"));
2512
3338
  if (error instanceof Error) {
2513
- io.err.write(chalk17.dim(`${error.message}
3339
+ io.err.write(chalk18.dim(`${error.message}
2514
3340
  `));
2515
3341
  }
2516
3342
  process.exit(1);
@@ -2521,33 +3347,35 @@ function createExportIconsCommand(factory) {
2521
3347
  }
2522
3348
 
2523
3349
  // 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";
3350
+ import { Command as Command24 } from "commander";
3351
+ import chalk19 from "chalk";
3352
+ import * as fs8 from "fs";
3353
+ import * as path6 from "path";
2528
3354
  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(
3355
+ const cmd = new Command24("theme").description("Export complete theme package from a Figma file").argument("<url>", "Figma URL or file key").option("-o, --output <dir>", "Output directory", "./theme").option(
2530
3356
  "--formats <formats>",
2531
3357
  "Comma-separated list of formats (json, css, scss, style-dictionary, tailwind)",
2532
3358
  "json,css"
2533
3359
  ).option("-t, --token <token>", "Override authentication token").action(
2534
- async (fileKey, options) => {
3360
+ async (url, options) => {
2535
3361
  const { io } = factory;
2536
3362
  try {
3363
+ const parsed = parseFigmaInput(url);
3364
+ const fileKey = parsed.fileKey;
2537
3365
  const token = await factory.getToken(options.token);
2538
3366
  let client = await factory.getClient();
2539
3367
  if (options.token) {
2540
3368
  const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
2541
3369
  client = new FigmaClient2({ token });
2542
3370
  }
2543
- io.err.write(chalk18.dim("Fetching file styles...\n"));
3371
+ io.err.write(chalk19.dim("Fetching file styles...\n"));
2544
3372
  const stylesResponse = await client.getFileStyles(fileKey);
2545
3373
  const styles = stylesResponse.meta?.styles || [];
2546
- io.err.write(chalk18.dim("Extracting token values...\n"));
3374
+ io.err.write(chalk19.dim("Extracting token values...\n"));
2547
3375
  const file = await client.getFile(fileKey);
2548
3376
  const tokens = extractStylesFromFile(file, styles);
2549
- if (!fs6.existsSync(options.output)) {
2550
- fs6.mkdirSync(options.output, { recursive: true });
3377
+ if (!fs8.existsSync(options.output)) {
3378
+ fs8.mkdirSync(options.output, { recursive: true });
2551
3379
  }
2552
3380
  const formats = options.formats.split(",").map((f) => f.trim());
2553
3381
  const outputs = [];
@@ -2576,8 +3404,8 @@ function createExportThemeCommand(factory) {
2576
3404
  output = JSON.stringify(tokens, null, 2);
2577
3405
  filename = "tokens.json";
2578
3406
  }
2579
- const filepath = path4.join(options.output, filename);
2580
- fs6.writeFileSync(filepath, output);
3407
+ const filepath = path6.join(options.output, filename);
3408
+ fs8.writeFileSync(filepath, output);
2581
3409
  outputs.push(filename);
2582
3410
  }
2583
3411
  const metadata = {
@@ -2587,22 +3415,22 @@ function createExportThemeCommand(factory) {
2587
3415
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
2588
3416
  formats
2589
3417
  };
2590
- fs6.writeFileSync(
2591
- path4.join(options.output, "metadata.json"),
3418
+ fs8.writeFileSync(
3419
+ path6.join(options.output, "metadata.json"),
2592
3420
  JSON.stringify(metadata, null, 2)
2593
3421
  );
2594
- io.out.write(chalk18.green(`\u2713 Theme exported to ${options.output}
3422
+ io.out.write(chalk19.green(`\u2713 Theme exported to ${options.output}
2595
3423
  `));
2596
- io.out.write(chalk18.dim(" Files:\n"));
3424
+ io.out.write(chalk19.dim(" Files:\n"));
2597
3425
  for (const filename of outputs) {
2598
- io.out.write(chalk18.dim(` - ${filename}
3426
+ io.out.write(chalk19.dim(` - ${filename}
2599
3427
  `));
2600
3428
  }
2601
- io.out.write(chalk18.dim(" - metadata.json\n"));
3429
+ io.out.write(chalk19.dim(" - metadata.json\n"));
2602
3430
  } catch (error) {
2603
- io.err.write(chalk18.red("Error exporting theme.\n"));
3431
+ io.err.write(chalk19.red("Error exporting theme.\n"));
2604
3432
  if (error instanceof Error) {
2605
- io.err.write(chalk18.dim(`${error.message}
3433
+ io.err.write(chalk19.dim(`${error.message}
2606
3434
  `));
2607
3435
  }
2608
3436
  process.exit(1);
@@ -2614,7 +3442,7 @@ function createExportThemeCommand(factory) {
2614
3442
 
2615
3443
  // src/cmd/export/index.ts
2616
3444
  function createExportCommand(factory) {
2617
- const cmd = new Command24("export").description(
3445
+ const cmd = new Command25("export").description(
2618
3446
  "Design-to-code export commands"
2619
3447
  );
2620
3448
  cmd.addCommand(createExportTokensCommand(factory));
@@ -2625,10 +3453,10 @@ function createExportCommand(factory) {
2625
3453
 
2626
3454
  // src/cmd/api/index.ts
2627
3455
  init_client();
2628
- import { Command as Command25 } from "commander";
2629
- import chalk19 from "chalk";
3456
+ import { Command as Command26 } from "commander";
3457
+ import chalk20 from "chalk";
2630
3458
  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(
3459
+ const cmd = new Command26("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
3460
  async (apiPath, options) => {
2633
3461
  const { io } = factory;
2634
3462
  try {
@@ -2653,7 +3481,7 @@ function createApiCommand(factory) {
2653
3481
  try {
2654
3482
  body = JSON.parse(options.data);
2655
3483
  } catch {
2656
- io.err.write(chalk19.red("Error: Invalid JSON in --data\n"));
3484
+ io.err.write(chalk20.red("Error: Invalid JSON in --data\n"));
2657
3485
  process.exit(1);
2658
3486
  }
2659
3487
  }
@@ -2670,15 +3498,15 @@ function createApiCommand(factory) {
2670
3498
  } catch (error) {
2671
3499
  if (error instanceof FigmaApiError) {
2672
3500
  io.err.write(
2673
- chalk19.red(`API Error: ${error.status} ${error.statusText}
3501
+ chalk20.red(`API Error: ${error.status} ${error.statusText}
2674
3502
  `)
2675
3503
  );
2676
3504
  if (error.body) {
2677
- io.err.write(chalk19.dim(JSON.stringify(error.body, null, 2)));
3505
+ io.err.write(chalk20.dim(JSON.stringify(error.body, null, 2)));
2678
3506
  io.err.write("\n");
2679
3507
  }
2680
3508
  } else if (error instanceof Error) {
2681
- io.err.write(chalk19.red(`Error: ${error.message}
3509
+ io.err.write(chalk20.red(`Error: ${error.message}
2682
3510
  `));
2683
3511
  }
2684
3512
  process.exit(1);
@@ -2699,15 +3527,515 @@ Examples:
2699
3527
  return cmd;
2700
3528
  }
2701
3529
 
3530
+ // src/cmd/frame/index.ts
3531
+ import { Command as Command28 } from "commander";
3532
+
3533
+ // src/cmd/frame/list.ts
3534
+ import { Command as Command27 } from "commander";
3535
+ import chalk21 from "chalk";
3536
+ var FRAME_TYPES = [
3537
+ "FRAME",
3538
+ "COMPONENT",
3539
+ "COMPONENT_SET",
3540
+ "SECTION",
3541
+ "GROUP",
3542
+ "INSTANCE"
3543
+ ];
3544
+ function extractFrames(file, options) {
3545
+ const frames = [];
3546
+ const allowedTypes = options.types || FRAME_TYPES;
3547
+ if (!file.document || !("children" in file.document)) {
3548
+ return frames;
3549
+ }
3550
+ const doc = file.document;
3551
+ for (const pageNode of doc.children || []) {
3552
+ let traverseNode2 = function(node, isTopLevel) {
3553
+ const nodeWithBounds = node;
3554
+ if (allowedTypes.includes(node.type)) {
3555
+ if (!options.topLevel || isTopLevel) {
3556
+ const frame = {
3557
+ id: node.id,
3558
+ name: node.name,
3559
+ type: node.type,
3560
+ page: pageNode.name,
3561
+ pageId: pageNode.id
3562
+ };
3563
+ if (nodeWithBounds.absoluteBoundingBox) {
3564
+ frame.width = Math.round(nodeWithBounds.absoluteBoundingBox.width);
3565
+ frame.height = Math.round(
3566
+ nodeWithBounds.absoluteBoundingBox.height
3567
+ );
3568
+ }
3569
+ frames.push(frame);
3570
+ }
3571
+ }
3572
+ if (!options.topLevel && "children" in node) {
3573
+ const parent = node;
3574
+ for (const child of parent.children || []) {
3575
+ traverseNode2(child, false);
3576
+ }
3577
+ }
3578
+ };
3579
+ var traverseNode = traverseNode2;
3580
+ if (pageNode.type !== "CANVAS") continue;
3581
+ if (options.page && pageNode.name !== options.page) continue;
3582
+ if (!("children" in pageNode)) continue;
3583
+ const page = pageNode;
3584
+ for (const child of page.children || []) {
3585
+ traverseNode2(child, true);
3586
+ }
3587
+ }
3588
+ return frames;
3589
+ }
3590
+ function formatTable2(frames) {
3591
+ if (frames.length === 0) {
3592
+ return "No frames found.";
3593
+ }
3594
+ const headers = ["ID", "Name", "Type", "Page", "Size"];
3595
+ const rows = frames.map((f) => [
3596
+ f.id,
3597
+ f.name,
3598
+ f.type,
3599
+ f.page,
3600
+ f.width && f.height ? `${f.width}\xD7${f.height}` : "-"
3601
+ ]);
3602
+ const widths = headers.map(
3603
+ (h, i) => Math.max(h.length, ...rows.map((r) => r[i].length))
3604
+ );
3605
+ const separator = widths.map((w) => "-".repeat(w)).join("-+-");
3606
+ const headerLine = headers.map((h, i) => h.padEnd(widths[i])).join(" | ");
3607
+ const rowLines = rows.map(
3608
+ (r) => r.map((cell, i) => cell.padEnd(widths[i])).join(" | ")
3609
+ );
3610
+ return [headerLine, separator, ...rowLines].join("\n");
3611
+ }
3612
+ function createFrameListCommand(factory) {
3613
+ const cmd = new Command27("list").description("List frames in a file").requiredOption("--file <url>", "Figma URL or file key").option("--page <name>", "Filter to specific page").option(
3614
+ "--type <types>",
3615
+ "Comma-separated types: frame,component,component_set,section,group,instance"
3616
+ ).option("--top-level", "Only top-level frames", false).option("-f, --format <format>", "Output format (json, table)", "json").option("-t, --token <token>", "Override authentication token").action(
3617
+ async (options) => {
3618
+ const { io } = factory;
3619
+ const validFormats = ["json", "table"];
3620
+ if (!validFormats.includes(
3621
+ options.format
3622
+ )) {
3623
+ io.err.write(
3624
+ chalk21.red(
3625
+ `Invalid format "${options.format}". Valid formats: ${validFormats.join(", ")}
3626
+ `
3627
+ )
3628
+ );
3629
+ process.exit(1);
3630
+ }
3631
+ try {
3632
+ const parsed = parseFigmaInput(options.file);
3633
+ const fileKey = parsed.fileKey;
3634
+ let types;
3635
+ if (options.type) {
3636
+ const typeMap = {
3637
+ frame: "FRAME",
3638
+ component: "COMPONENT",
3639
+ component_set: "COMPONENT_SET",
3640
+ section: "SECTION",
3641
+ group: "GROUP",
3642
+ instance: "INSTANCE"
3643
+ };
3644
+ types = options.type.split(",").map((t) => {
3645
+ const mapped = typeMap[t.trim().toLowerCase()];
3646
+ if (!mapped) {
3647
+ throw new Error(
3648
+ `Invalid type "${t}". Valid types: ${Object.keys(typeMap).join(", ")}`
3649
+ );
3650
+ }
3651
+ return mapped;
3652
+ });
3653
+ }
3654
+ const client = await factory.getClient();
3655
+ let activeClient = client;
3656
+ if (options.token) {
3657
+ const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
3658
+ activeClient = new FigmaClient2({ token: options.token });
3659
+ }
3660
+ const file = await activeClient.getFile(fileKey, {
3661
+ depth: options.topLevel ? 2 : void 0
3662
+ });
3663
+ const frames = extractFrames(file, {
3664
+ page: options.page,
3665
+ types,
3666
+ topLevel: options.topLevel
3667
+ });
3668
+ if (options.format === "table") {
3669
+ io.out.write(formatTable2(frames) + "\n");
3670
+ } else {
3671
+ io.out.write(
3672
+ formatOutput(frames, { format: options.format })
3673
+ );
3674
+ io.out.write("\n");
3675
+ }
3676
+ } catch (error) {
3677
+ io.err.write(chalk21.red("Error listing frames.\n"));
3678
+ if (error instanceof Error) {
3679
+ io.err.write(chalk21.dim(`${error.message}
3680
+ `));
3681
+ }
3682
+ process.exit(1);
3683
+ }
3684
+ }
3685
+ );
3686
+ return cmd;
3687
+ }
3688
+
3689
+ // src/cmd/frame/index.ts
3690
+ function createFrameCommand(factory) {
3691
+ const cmd = new Command28("frame").description("Work with Figma frames");
3692
+ cmd.addCommand(createFrameListCommand(factory));
3693
+ return cmd;
3694
+ }
3695
+
3696
+ // src/cmd/handoff/index.ts
3697
+ import { Command as Command29 } from "commander";
3698
+ import chalk22 from "chalk";
3699
+ import * as fs9 from "fs";
3700
+ import * as path7 from "path";
3701
+ function countTokens(tokens) {
3702
+ return {
3703
+ colors: Object.keys(tokens.colors || {}).length,
3704
+ typography: Object.keys(tokens.typography || {}).length,
3705
+ shadows: Object.keys(tokens.shadows || {}).length,
3706
+ radii: Object.keys(tokens.radii || {}).length,
3707
+ spacing: Object.keys(tokens.spacing || {}).length
3708
+ };
3709
+ }
3710
+ function extractComponents(file) {
3711
+ const components = [];
3712
+ function traverseNode(node) {
3713
+ if (node.type === "COMPONENT" || node.type === "COMPONENT_SET") {
3714
+ components.push({
3715
+ id: node.id,
3716
+ name: node.name,
3717
+ type: node.type
3718
+ });
3719
+ }
3720
+ if ("children" in node) {
3721
+ const parent = node;
3722
+ for (const child of parent.children || []) {
3723
+ traverseNode(child);
3724
+ }
3725
+ }
3726
+ }
3727
+ if (file.document) {
3728
+ traverseNode(file.document);
3729
+ }
3730
+ return components;
3731
+ }
3732
+ function countPagesAndFrames(file) {
3733
+ let pages = 0;
3734
+ let frames = 0;
3735
+ if (file.document && "children" in file.document) {
3736
+ const doc = file.document;
3737
+ for (const pageNode of doc.children || []) {
3738
+ if (pageNode.type === "CANVAS") {
3739
+ pages++;
3740
+ if ("children" in pageNode) {
3741
+ const page = pageNode;
3742
+ frames += (page.children || []).length;
3743
+ }
3744
+ }
3745
+ }
3746
+ }
3747
+ return { pages, frames };
3748
+ }
3749
+ function generateHandoffReadme(summary, tokenFormats) {
3750
+ const lines = [];
3751
+ lines.push(`# Design Handoff: ${summary.name}`);
3752
+ lines.push("");
3753
+ lines.push(`Source: ${summary.url}`);
3754
+ lines.push(`Generated: ${summary.generatedAt}`);
3755
+ lines.push("");
3756
+ lines.push("## Tokens Summary");
3757
+ lines.push(`- Colors: ${summary.tokens.colors} tokens`);
3758
+ lines.push(`- Typography: ${summary.tokens.typography} tokens`);
3759
+ lines.push(`- Shadows: ${summary.tokens.shadows} tokens`);
3760
+ lines.push(`- Radii: ${summary.tokens.radii} tokens`);
3761
+ lines.push(`- Spacing: ${summary.tokens.spacing} tokens`);
3762
+ lines.push("");
3763
+ if (summary.components.length > 0) {
3764
+ lines.push("## Component Inventory");
3765
+ lines.push("| ID | Name | Type | Asset |");
3766
+ lines.push("|----|------|------|-------|");
3767
+ for (const comp of summary.components) {
3768
+ lines.push(
3769
+ `| ${comp.id} | ${comp.name} | ${comp.type} | ${comp.asset || "-"} |`
3770
+ );
3771
+ }
3772
+ lines.push("");
3773
+ }
3774
+ lines.push("## File Structure");
3775
+ lines.push(
3776
+ `${summary.pages} pages, ${summary.frames} frames, ${summary.components.length} components`
3777
+ );
3778
+ lines.push("");
3779
+ lines.push("## Implementation Notes");
3780
+ if (tokenFormats.includes("css")) {
3781
+ lines.push("- Import tokens: `@import './tokens/tokens.css'`");
3782
+ }
3783
+ if (tokenFormats.includes("scss")) {
3784
+ lines.push("- SCSS import: `@import './tokens/tokens.scss'`");
3785
+ }
3786
+ if (summary.components.some((c) => c.asset)) {
3787
+ lines.push("- Asset path: `./assets/{name}.svg`");
3788
+ }
3789
+ lines.push("");
3790
+ return lines.join("\n");
3791
+ }
3792
+ function extractSimplifiedStructure(file) {
3793
+ const structure = {
3794
+ name: file.name,
3795
+ lastModified: file.lastModified,
3796
+ pages: []
3797
+ };
3798
+ if (file.document && "children" in file.document) {
3799
+ const doc = file.document;
3800
+ for (const pageNode of doc.children || []) {
3801
+ if (pageNode.type !== "CANVAS") continue;
3802
+ const page = {
3803
+ id: pageNode.id,
3804
+ name: pageNode.name,
3805
+ frames: []
3806
+ };
3807
+ if ("children" in pageNode) {
3808
+ const pageWithChildren = pageNode;
3809
+ for (const child of pageWithChildren.children || []) {
3810
+ const childWithBounds = child;
3811
+ const frame = {
3812
+ id: child.id,
3813
+ name: child.name,
3814
+ type: child.type
3815
+ };
3816
+ if (childWithBounds.absoluteBoundingBox) {
3817
+ frame.width = Math.round(childWithBounds.absoluteBoundingBox.width);
3818
+ frame.height = Math.round(
3819
+ childWithBounds.absoluteBoundingBox.height
3820
+ );
3821
+ }
3822
+ page.frames.push(frame);
3823
+ }
3824
+ }
3825
+ structure.pages.push(page);
3826
+ }
3827
+ }
3828
+ return structure;
3829
+ }
3830
+ function createHandoffCommand(factory) {
3831
+ const cmd = new Command29("handoff").description("Export complete design handoff package").argument("<url>", "Figma URL or file key").option("-o, --output <dir>", "Output directory", "./figma-handoff").option(
3832
+ "--tokens <formats>",
3833
+ "Token formats to export (comma-separated: css,json,scss)",
3834
+ "css,json"
3835
+ ).option("--assets", "Export component assets", false).option("--asset-format <format>", "Asset format (svg, png)", "svg").option("--structure", "Include structure.json", false).option("--readme", "Generate HANDOFF.md", false).option("--all", "Export all (tokens, assets, structure, readme)", false).option("-t, --token <token>", "Override authentication token").action(
3836
+ async (url, options) => {
3837
+ const { io } = factory;
3838
+ try {
3839
+ const parsed = parseFigmaInput(url);
3840
+ const fileKey = parsed.fileKey;
3841
+ const exportAssets = options.all || options.assets;
3842
+ const exportStructure = options.all || options.structure;
3843
+ const exportReadme = options.all || options.readme;
3844
+ const tokenFormats = options.tokens.split(",").map((f) => f.trim());
3845
+ const validTokenFormats = ["css", "scss", "json"];
3846
+ for (const format of tokenFormats) {
3847
+ if (!validTokenFormats.includes(
3848
+ format
3849
+ )) {
3850
+ io.err.write(
3851
+ chalk22.red(
3852
+ `Invalid token format "${format}". Valid formats: ${validTokenFormats.join(", ")}
3853
+ `
3854
+ )
3855
+ );
3856
+ process.exit(1);
3857
+ }
3858
+ }
3859
+ const validAssetFormats = ["svg", "png"];
3860
+ if (!validAssetFormats.includes(
3861
+ options.assetFormat
3862
+ )) {
3863
+ io.err.write(
3864
+ chalk22.red(
3865
+ `Invalid asset format "${options.assetFormat}". Valid formats: ${validAssetFormats.join(", ")}
3866
+ `
3867
+ )
3868
+ );
3869
+ process.exit(1);
3870
+ }
3871
+ const assetFormat = options.assetFormat;
3872
+ const outputDir = path7.resolve(options.output);
3873
+ const cwd = process.cwd();
3874
+ const homeDir = process.env.HOME || "";
3875
+ if (!outputDir.startsWith(cwd) && !outputDir.startsWith(homeDir)) {
3876
+ io.err.write(
3877
+ chalk22.red(
3878
+ `Error: Output directory "${outputDir}" is outside your project and home directory. Use a relative path or a path within your home directory.
3879
+ `
3880
+ )
3881
+ );
3882
+ process.exit(1);
3883
+ }
3884
+ const tokensDir = path7.join(outputDir, "tokens");
3885
+ const assetsDir = path7.join(outputDir, "assets");
3886
+ fs9.mkdirSync(tokensDir, { recursive: true });
3887
+ if (exportAssets) {
3888
+ fs9.mkdirSync(assetsDir, { recursive: true });
3889
+ }
3890
+ io.err.write(chalk22.dim("Fetching Figma file...\n"));
3891
+ const client = await factory.getClient();
3892
+ let activeClient = client;
3893
+ if (options.token) {
3894
+ const { FigmaClient: FigmaClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
3895
+ activeClient = new FigmaClient2({ token: options.token });
3896
+ }
3897
+ const [file, stylesResponse] = await Promise.all([
3898
+ activeClient.getFile(fileKey),
3899
+ activeClient.getFileStyles(fileKey)
3900
+ ]);
3901
+ const styles = stylesResponse.meta?.styles || [];
3902
+ io.err.write(chalk22.dim("Extracting design tokens...\n"));
3903
+ const tokens = extractStylesFromFile(file, styles);
3904
+ for (const format of tokenFormats) {
3905
+ let content;
3906
+ let filename;
3907
+ switch (format) {
3908
+ case "css":
3909
+ content = tokensToCss(tokens);
3910
+ filename = "tokens.css";
3911
+ break;
3912
+ case "scss":
3913
+ content = tokensToScss(tokens);
3914
+ filename = "tokens.scss";
3915
+ break;
3916
+ case "json":
3917
+ default:
3918
+ content = JSON.stringify(tokens, null, 2);
3919
+ filename = "tokens.json";
3920
+ break;
3921
+ }
3922
+ fs9.writeFileSync(path7.join(tokensDir, filename), content + "\n");
3923
+ io.err.write(chalk22.green(` \u2713 ${filename}
3924
+ `));
3925
+ }
3926
+ const components = extractComponents(file);
3927
+ const { pages, frames } = countPagesAndFrames(file);
3928
+ if (exportAssets && components.length > 0) {
3929
+ io.err.write(chalk22.dim("Exporting component assets...\n"));
3930
+ const componentIds = components.map((c) => c.id);
3931
+ const images = await activeClient.getImages(fileKey, componentIds, {
3932
+ format: assetFormat
3933
+ });
3934
+ const imageUrls = images.images || {};
3935
+ if (Object.keys(imageUrls).length === 0) {
3936
+ io.err.write(
3937
+ chalk22.yellow(
3938
+ " Warning: No image URLs returned from Figma API.\n"
3939
+ )
3940
+ );
3941
+ }
3942
+ const downloadItems = Object.entries(imageUrls).map(
3943
+ ([nodeId, imageUrl]) => {
3944
+ const component = components.find((c) => c.id === nodeId);
3945
+ const filename = component ? `${toKebabCase(component.name)}.${assetFormat}` : `${sanitizeFilename(nodeId)}.${assetFormat}`;
3946
+ if (component) {
3947
+ component.asset = filename;
3948
+ }
3949
+ return {
3950
+ id: nodeId,
3951
+ url: imageUrl ?? null,
3952
+ filename
3953
+ };
3954
+ }
3955
+ );
3956
+ const downloadResults = await downloadFiles(
3957
+ downloadItems,
3958
+ assetsDir
3959
+ );
3960
+ const successCount = downloadResults.filter(
3961
+ (r) => r.status === "success"
3962
+ ).length;
3963
+ const failures = downloadResults.filter(
3964
+ (r) => r.status === "error"
3965
+ );
3966
+ io.err.write(chalk22.green(` \u2713 ${successCount} assets exported
3967
+ `));
3968
+ if (failures.length > 0) {
3969
+ io.err.write(
3970
+ chalk22.yellow(
3971
+ ` Warning: ${failures.length} asset(s) failed to download:
3972
+ `
3973
+ )
3974
+ );
3975
+ for (const failure of failures) {
3976
+ io.err.write(
3977
+ chalk22.dim(
3978
+ ` - ${failure.name ?? failure.id}: ${failure.error}
3979
+ `
3980
+ )
3981
+ );
3982
+ }
3983
+ process.exit(1);
3984
+ }
3985
+ }
3986
+ if (exportStructure) {
3987
+ io.err.write(chalk22.dim("Generating structure.json...\n"));
3988
+ const structure = extractSimplifiedStructure(file);
3989
+ fs9.writeFileSync(
3990
+ path7.join(outputDir, "structure.json"),
3991
+ JSON.stringify(structure, null, 2) + "\n"
3992
+ );
3993
+ io.err.write(chalk22.green(" \u2713 structure.json\n"));
3994
+ }
3995
+ if (exportReadme) {
3996
+ io.err.write(chalk22.dim("Generating HANDOFF.md...\n"));
3997
+ const summary = {
3998
+ name: file.name,
3999
+ url: `https://www.figma.com/file/${fileKey}`,
4000
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4001
+ tokens: countTokens(tokens),
4002
+ components,
4003
+ pages,
4004
+ frames
4005
+ };
4006
+ const readme = generateHandoffReadme(summary, tokenFormats);
4007
+ fs9.writeFileSync(path7.join(outputDir, "HANDOFF.md"), readme);
4008
+ io.err.write(chalk22.green(" \u2713 HANDOFF.md\n"));
4009
+ }
4010
+ io.out.write(
4011
+ chalk22.bold.green(`
4012
+ \u2713 Handoff package created at ${outputDir}
4013
+ `)
4014
+ );
4015
+ } catch (error) {
4016
+ io.err.write(chalk22.red("Error creating handoff package.\n"));
4017
+ if (error instanceof Error) {
4018
+ io.err.write(chalk22.dim(`${error.message}
4019
+ `));
4020
+ }
4021
+ process.exit(1);
4022
+ }
4023
+ }
4024
+ );
4025
+ return cmd;
4026
+ }
4027
+
2702
4028
  // src/cmd/root.ts
2703
4029
  function createRootCommand(factory) {
2704
- const program = new Command26();
4030
+ const program = new Command30();
2705
4031
  program.name("figma").description("CLI for the Figma API").version("1.0.0").configureHelp({
2706
4032
  sortSubcommands: true,
2707
4033
  sortOptions: true
2708
4034
  });
2709
4035
  program.addCommand(createAuthCommand(factory));
2710
4036
  program.addCommand(createFileCommand(factory));
4037
+ program.addCommand(createFrameCommand(factory));
4038
+ program.addCommand(createHandoffCommand(factory));
2711
4039
  program.addCommand(createProjectCommand(factory));
2712
4040
  program.addCommand(createComponentCommand(factory));
2713
4041
  program.addCommand(createStyleCommand(factory));