import { z } from 'zod';
import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import axios from 'axios';
import { findAssets, buildAssetName, buildAssetPath, getSectionFromPath, isCompositeGroup } from '../../utils/assetHelpers.js';
export const name = 'extract_assets';
export const description = 'Extract all assets from frame with progress tracking. Detects composite groups (isCompositeAsset:true) and exports as single PNG. Categorizes into icons/, images/, composites/.';
export const inputSchema = {
file_key: z.string().describe('Figma file key'),
page_name: z.string().describe('Page name (partial match)'),
frame_name: z.string().describe('Frame name (partial match)'),
output_dir: z.string().default('./figma-assets').describe('Output directory (default: ./figma-assets)'),
};
export async function handler(args, ctx) {
const { chunker, figmaClient } = ctx;
const { file_key: fileKey, page_name: pageName, frame_name: frameName, output_dir: outputDir } = args;
const file = await figmaClient.getFile(fileKey, 2);
const page = figmaClient.findPageByName(file, pageName);
if (!page) throw new Error(`Page "${pageName}" not found`);
const frameRef = figmaClient.findFrameByName(page, frameName);
if (!frameRef) throw new Error(`Frame "${frameName}" not found`);
const frame = await figmaClient.getNode(fileKey, frameRef.id);
const assets = findAssets(frame, { collectBounds: true });
const iconsDir = join(outputDir, 'icons');
const imagesDir = join(outputDir, 'images');
const compositeDir = join(imagesDir, 'composites');
await mkdir(iconsDir, { recursive: true });
await mkdir(imagesDir, { recursive: true });
await mkdir(compositeDir, { recursive: true });
const results = { icons: [], images: [], composites: [], failed: [] };
const assetMap = {};
const batchSize = 10;
for (let i = 0; i < assets.length; i += batchSize) {
const batch = assets.slice(i, i + batchSize);
const ids = batch.map((a) => a.id).join(',');
try {
const svgData = await figmaClient.getImage(fileKey, ids, 'svg');
const pngData = await figmaClient.getImage(fileKey, ids, 'png', 2);
for (const asset of batch) {
try {
if (asset.isComposite) {
if (pngData.images[asset.id]) {
const pngResponse = await axios.get(pngData.images[asset.id], { responseType: 'arraybuffer' });
const filePath = join(compositeDir, `${asset.name}.png`);
await writeFile(filePath, Buffer.from(pngResponse.data));
results.composites.push({
path: filePath,
uniqueName: asset.name,
originalName: asset.originalName,
section: getSectionFromPath(asset.path),
bounds: asset.bounds,
isComposite: true,
});
assetMap[asset.name] = filePath;
}
} else if (asset.category === 'icon' && svgData.images[asset.id]) {
const svgResponse = await axios.get(svgData.images[asset.id]);
const filePath = join(iconsDir, `${asset.name}.svg`);
await writeFile(filePath, svgResponse.data);
results.icons.push({
path: filePath,
uniqueName: asset.name,
originalName: asset.originalName,
section: getSectionFromPath(asset.path),
bounds: asset.bounds,
});
assetMap[asset.name] = filePath;
} else if (pngData.images[asset.id]) {
const pngResponse = await axios.get(pngData.images[asset.id], { responseType: 'arraybuffer' });
const filePath = join(imagesDir, `${asset.name}.png`);
await writeFile(filePath, Buffer.from(pngResponse.data));
results.images.push({
path: filePath,
uniqueName: asset.name,
originalName: asset.originalName,
section: getSectionFromPath(asset.path),
bounds: asset.bounds,
});
assetMap[asset.name] = filePath;
}
} catch (err) {
results.failed.push({
name: asset.name,
originalName: asset.originalName,
error: err.message,
});
}
}
} catch (err) {
batch.forEach((a) => results.failed.push({
name: a.name,
originalName: a.originalName,
error: err.message,
}));
}
}
const response = chunker.wrapResponse(
{
frame: frame.name,
outputDir,
summary: {
icons: results.icons.length,
images: results.images.length,
composites: results.composites.length,
failed: results.failed.length,
},
icons: results.icons,
images: results.images,
composites: results.composites,
assetMap,
failed: results.failed,
},
{
step: 'Asset extraction complete',
progress: `${results.icons.length} icons, ${results.images.length} images, ${results.composites.length} composite groups`,
nextStep: 'Assets saved to disk. Use extract_styles for design tokens.',
}
);
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
}