add figma-tools skill

This commit is contained in:
2026-03-11 16:30:01 +00:00
parent 76e9ba7a6b
commit 3c3d24ccdb
12 changed files with 1375 additions and 0 deletions
@@ -0,0 +1,311 @@
#!/usr/bin/env node
/**
* Figma Tools CLI
*/
import { FigmaClient, parseFigmaUrl, downloadImage } from './figma-client.ts';
import { simplifyDesign } from './simplify.ts';
import type { FigmaNode, FigmaFileResponse, FigmaNodeResponse } from './types.ts';
const API_KEY = process.env.FIGMA_API_KEY;
function showHelp(): void {
console.log(`
Figma Tools - Fetch and parse Figma design files
Usage:
figma-tools get <url> [options] Fetch and simplify Figma file/node
figma-tools download <url> <dir> Download images/assets
figma-tools parse <file> [options] Parse local JSON file
Commands:
get <figma-url>
Fetch a Figma file or specific node. Outputs simplified JSON.
Options:
--depth N Limit tree depth (1-4)
--raw Output raw API response
--yaml Output as YAML
Examples:
figma-tools get https://www.figma.com/file/ABC123/My-Design
figma-tools get "https://www.figma.com/file/ABC123/My-Design?node-id=123%3A456" --depth 2
figma-tools get https://www.figma.com/file/ABC123/My-Design --raw > raw.json
download <figma-url> <output-dir>
Download images and assets from a Figma file.
Options:
--scale N PNG scale factor (1-4, default: 1)
--format fmt Image format: png, svg, or both (default: png)
Examples:
figma-tools download https://www.figma.com/file/ABC123/My-Design ./assets
figma-tools download https://www.figma.com/file/ABC123/My-Design ./assets --scale 2
parse <json-file>
Simplify a raw Figma API JSON file.
Options:
--yaml Output as YAML
Examples:
figma-tools parse ./raw-figma-response.json
figma-tools parse ./raw.json --yaml > design.yaml
Setup:
export FIGMA_API_KEY="your-token-here"
Get a token from Figma Settings → Personal Access Tokens
`);
}
async function output(data: unknown, useYaml: boolean): Promise<void> {
if (useYaml) {
const yaml = await import('js-yaml');
console.log(yaml.default.dump(data, { indent: 2, lineWidth: 120 }));
} else {
console.log(JSON.stringify(data, null, 2));
}
}
async function getCommand(args: string[]): Promise<void> {
if (!API_KEY) {
console.error('Error: FIGMA_API_KEY environment variable required');
console.error('Get a token from: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens');
process.exit(1);
}
const url = args.find((a) => !a.startsWith('--'));
if (!url) {
console.error('Error: No URL provided');
console.error('Usage: figma-tools get <figma-url> [--depth N] [--raw] [--yaml]');
process.exit(1);
}
const depthArg = args.find((a, i) => a === '--depth' && args[i + 1]);
const depth = depthArg ? parseInt(args[args.indexOf(depthArg) + 1]) : null;
const raw = args.includes('--raw');
const yaml = args.includes('--yaml');
const parsed = parseFigmaUrl(url);
if (!parsed) {
console.error('Error: Invalid Figma URL');
console.error('Expected: https://www.figma.com/file/{fileKey}/{title} or with ?node-id=');
process.exit(1);
}
const client = new FigmaClient(API_KEY);
let data: FigmaFileResponse | FigmaNodeResponse;
if (parsed.nodeId) {
console.error(`Fetching node ${parsed.nodeId} from file ${parsed.fileKey}...`);
data = await client.getNode(parsed.fileKey, parsed.nodeId, depth);
} else {
console.error(`Fetching file ${parsed.fileKey}...`);
data = await client.getFile(parsed.fileKey, depth);
}
if (raw) {
await output(data, yaml);
} else {
console.error('Simplifying response...');
const simplified = simplifyDesign(data);
await output(simplified, yaml);
}
}
async function downloadCommand(args: string[]): Promise<void> {
if (!API_KEY) {
console.error('Error: FIGMA_API_KEY environment variable required');
process.exit(1);
}
const url = args.find((a) => !a.startsWith('--'));
const outputDir = args.filter((a) => !a.startsWith('--'))[1];
if (!url || !outputDir) {
console.error('Error: URL and output directory required');
console.error('Usage: figma-tools download <figma-url> <output-dir> [--scale N] [--format png|svg]');
process.exit(1);
}
const scaleArg = args.find((a, i) => a === '--scale' && args[i + 1]);
const scale = scaleArg ? parseInt(args[args.indexOf(scaleArg) + 1]) : 1;
const formatArg = args.find((a, i) => a === '--format' && args[i + 1]);
const format = formatArg ? args[args.indexOf(formatArg) + 1] : 'png';
const parsed = parseFigmaUrl(url);
if (!parsed) {
console.error('Error: Invalid Figma URL');
process.exit(1);
}
const client = new FigmaClient(API_KEY);
console.error('Fetching file structure...');
const fileData = await client.getFile(parsed.fileKey, 3);
const imageNodes: Array<{ id: string; name: string; imageRef?: string }> = [];
const svgNodes: Array<{ id: string; name: string }> = [];
function sanitizeFilename(name: string): string {
return name
.replace(/[^a-zA-Z0-9\-_./\s]/g, '')
.replace(/\s+/g, '_')
.substring(0, 100);
}
function collectNodes(nodes: FigmaNode[], parentPath = ''): void {
for (const node of nodes) {
const currentPath = parentPath ? `${parentPath}/${node.name}` : node.name;
if (node.fills) {
const fills = Array.isArray(node.fills) ? node.fills : [node.fills];
for (const fill of fills) {
if (fill?.type === 'IMAGE' && fill.imageRef) {
imageNodes.push({
id: node.id,
name: sanitizeFilename(currentPath) + '.png',
imageRef: fill.imageRef,
});
}
}
}
if (
['VECTOR', 'BOOLEAN_OPERATION', 'STAR', 'POLYGON', 'LINE', 'ELLIPSE', 'RECTANGLE'].includes(
node.type
)
) {
svgNodes.push({
id: node.id,
name: sanitizeFilename(currentPath) + '.svg',
});
}
if (node.children) {
collectNodes(node.children, currentPath);
}
}
}
let nodesToProcess: FigmaNode[];
if (parsed.nodeId) {
// fileData is FigmaNodeResponse when we requested a specific node
const nodeResponse = fileData as unknown as { nodes?: Record<string, { document: FigmaNode }> };
nodesToProcess = nodeResponse.nodes?.[parsed.nodeId]?.document?.children || [];
} else {
nodesToProcess = fileData.document?.children || [];
}
collectNodes(nodesToProcess);
console.error(`Found ${imageNodes.length} image fills, ${svgNodes.length} exportable nodes`);
const results: string[] = [];
if ((format === 'png' || format === 'both') && (imageNodes.length > 0 || svgNodes.length > 0)) {
const pngIds = [
...imageNodes.map((n) => n.id),
...svgNodes.map((n) => n.id),
];
console.error(`Downloading ${pngIds.length} PNGs at ${scale}x...`);
const images = await client.getImages(parsed.fileKey, pngIds, 'png', scale);
for (const [nodeId, imageUrl] of Object.entries(images)) {
if (imageUrl) {
const node = [...imageNodes, ...svgNodes].find((n) => n.id === nodeId);
if (node) {
const fileName = node.name.endsWith('.png') ? node.name : `${node.name}.png`;
const path = await downloadImage(imageUrl, outputDir, fileName);
results.push(path);
}
}
}
}
if ((format === 'svg' || format === 'both') && svgNodes.length > 0) {
console.error(`Downloading ${svgNodes.length} SVGs...`);
const images = await client.getImages(
parsed.fileKey,
svgNodes.map((n) => n.id),
'svg'
);
for (const [nodeId, imageUrl] of Object.entries(images)) {
if (imageUrl) {
const node = svgNodes.find((n) => n.id === nodeId);
if (node) {
const path = await downloadImage(imageUrl, outputDir, node.name);
results.push(path);
}
}
}
}
console.error(`\nDownloaded ${results.length} files to: ${outputDir}`);
for (const path of results) {
console.log(path);
}
}
async function parseCommand(args: string[]): Promise<void> {
const fs = await import('fs');
const filePath = args.find((a) => !a.startsWith('--'));
if (!filePath) {
console.error('Error: No file path provided');
console.error('Usage: figma-tools parse <json-file> [--yaml]');
process.exit(1);
}
const yaml = args.includes('--yaml');
if (!fs.existsSync(filePath)) {
console.error(`Error: File not found: ${filePath}`);
process.exit(1);
}
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
console.error('Simplifying...');
const simplified = simplifyDesign(raw);
await output(simplified, yaml);
}
async function main(): Promise<void> {
const args = process.argv.slice(2);
const command = args[0];
if (!command || command === '--help' || command === '-h') {
showHelp();
process.exit(0);
}
const commandArgs = args.slice(1);
try {
switch (command) {
case 'get':
await getCommand(commandArgs);
break;
case 'download':
await downloadCommand(commandArgs);
break;
case 'parse':
await parseCommand(commandArgs);
break;
default:
console.error(`Unknown command: ${command}`);
showHelp();
process.exit(1);
}
} catch (err) {
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
}
}
main();