@createlex/figma-swiftui-mcp 1.3.1 → 1.4.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.
@@ -431,5 +431,7 @@ async function generateWithLocalKey(context, generationMode = 'editable') {
431
431
  module.exports = {
432
432
  generateWithLocalKey,
433
433
  buildGenerationPrompt,
434
+ parseClaudeResponse,
435
+ buildPromptContext,
434
436
  SWIFTUI_SYSTEM_PROMPT,
435
437
  };
@@ -16,6 +16,7 @@ const {
16
16
  resolveWritableProjectPath,
17
17
  setSavedProjectPath,
18
18
  writeAssetCatalogEntries,
19
+ writeMultiScreenProject,
19
20
  writeSwiftUIScreen,
20
21
  } = require('./xcode-writer.cjs');
21
22
  const { startBridgeServer } = require('./bridge-server.cjs');
@@ -28,6 +29,8 @@ const {
28
29
  const {
29
30
  generateWithLocalKey,
30
31
  buildGenerationPrompt,
32
+ parseClaudeResponse,
33
+ buildPromptContext,
31
34
  } = require('./local-llm-generator.cjs');
32
35
 
33
36
  const BRIDGE_HTTP_URL = process.env.FIGMA_SWIFTUI_BRIDGE_HTTP_URL || 'http://localhost:7765';
@@ -420,7 +423,7 @@ server.registerTool('get_metadata', {
420
423
  });
421
424
 
422
425
  server.registerTool('get_design_context', {
423
- description: 'Return node metadata, asset export candidates, and generation hints for the current Figma selection. PREFERRED WORKFLOW: call write_selection_to_xcode instead — it generates correct SwiftUI with Image("name") asset refs AND writes PNGs to Assets.xcassets in one step, no planning needed. Only call get_design_context if you need to inspect the raw node tree before generating.',
426
+ description: 'Return node metadata, asset export candidates, and generation hints for the current Figma selection. PREFERRED WORKFLOW: call figma_to_swiftui instead — it handles LLM generation, Image("name") asset refs, PNG export, DesignTokens, and reusable components in one shot. Only call get_design_context if you need to inspect the raw node tree before generating.',
424
427
  inputSchema: {
425
428
  nodeIds: z.array(z.string()).optional().describe('Optional list of Figma node ids. If omitted, uses the current selection'),
426
429
  nodeId: z.string().optional().describe('Optional single Figma node id'),
@@ -438,7 +441,7 @@ server.registerTool('get_design_context', {
438
441
  });
439
442
 
440
443
  server.registerTool('get_swiftui_generation_prompt', {
441
- description: 'Return a ready-to-use SwiftUI system prompt and user message for AI-native generation. WARNING: prefer write_selection_to_xcode — it handles Image() asset refs and PNG export automatically. Only use get_swiftui_generation_prompt when you must generate code yourself. If you do use this: (a) use .font(.system(size:weight:)) — never hardcode custom font names like Inter or Roboto, (b) reference every assetExportPlan entry as Image("name") not Rectangle().',
444
+ description: 'Return a ready-to-use SwiftUI system prompt and user message for AI-native generation. PREFERRED: use figma_to_swiftui instead — it pre-writes assets and handles LLM generation automatically. Only use get_swiftui_generation_prompt when you need the raw prompt. If you do: (a) use .font(.system(size:weight:)) — never hardcode custom font names like Inter or Roboto, (b) reference every assetExportPlan entry as Image("name") not Rectangle().',
442
445
  inputSchema: {
443
446
  nodeIds: z.array(z.string()).optional().describe('Optional list of Figma node ids. If omitted, uses the current selection'),
444
447
  nodeId: z.string().optional().describe('Optional single Figma node id'),
@@ -775,26 +778,47 @@ server.registerTool('write_selection_to_xcode', {
775
778
  }, async ({ nodeIds, includeOverflow, generationMode, projectPath }) => {
776
779
  const targetDir = resolveTargetProjectPath(projectPath);
777
780
 
778
- // Try hosted analysis first to get refinement hints alongside code
781
+ // Try LLM generation (Tier 2/3) to get better code than the template generator
782
+ let llmCode = null;
783
+ let llmAdditionalFiles = [];
779
784
  let analysisHints = null;
780
785
  try {
781
- const analysis = await tryHostedSemanticGeneration({
786
+ const semanticResult = await tryHostedSemanticGeneration({
782
787
  nodeIds,
783
788
  generationMode,
784
789
  includeOverflow,
785
- analyze: true,
790
+ analyze: false,
786
791
  });
787
- if (analysis) {
788
- analysisHints = {
789
- generationHints: analysis.generationHints,
790
- manualRefinementHints: analysis.manualRefinementHints,
791
- reusableComponents: analysis.reusableComponents,
792
- assetExportPlan: analysis.assetExportPlan,
793
- };
792
+ if (semanticResult?.code) {
793
+ const parsed = parseClaudeResponse(semanticResult.code);
794
+ llmCode = parsed.code || semanticResult.code;
795
+ if (parsed.designTokensCode) {
796
+ llmAdditionalFiles.push({ name: 'DesignTokens.swift', code: parsed.designTokensCode, dir: 'shared' });
797
+ }
798
+ for (const comp of (parsed.componentFiles || [])) {
799
+ llmAdditionalFiles.push({ name: comp.name, code: comp.code, dir: 'components' });
800
+ }
801
+ }
802
+ // Also try to get analysis hints
803
+ if (!semanticResult) {
804
+ const analysis = await tryHostedSemanticGeneration({
805
+ nodeIds,
806
+ generationMode,
807
+ includeOverflow,
808
+ analyze: true,
809
+ });
810
+ if (analysis) {
811
+ analysisHints = {
812
+ generationHints: analysis.generationHints,
813
+ manualRefinementHints: analysis.manualRefinementHints,
814
+ reusableComponents: analysis.reusableComponents,
815
+ assetExportPlan: analysis.assetExportPlan,
816
+ };
817
+ }
794
818
  }
795
819
  } catch (err) {
796
- // Non-fatal — continue with generation even if analysis fails
797
- console.error('[figma-swiftui-mcp] Hosted analysis failed (non-fatal):', err?.message ?? err);
820
+ // Non-fatal — continue with plugin generation
821
+ console.error('[figma-swiftui-mcp] Semantic generation failed (non-fatal):', err?.message ?? err);
798
822
  }
799
823
 
800
824
  const generated = await callBridge('generate_swiftui', {
@@ -804,16 +828,20 @@ server.registerTool('write_selection_to_xcode', {
804
828
  includeImages: true,
805
829
  });
806
830
 
831
+ // Prefer LLM-generated code over template-generated code
832
+ const finalCode = llmCode || generated.code;
833
+
807
834
  const effectiveStructName = inferStructName({
808
- code: generated.code,
835
+ code: finalCode,
809
836
  selectionNames: generated.selection?.names ?? [],
810
837
  });
811
838
 
812
839
  const result = writeSwiftUIScreen({
813
840
  targetDir,
814
- code: generated.code,
841
+ code: finalCode,
815
842
  structName: effectiveStructName,
816
843
  images: Array.isArray(generated.images) ? generated.images : [],
844
+ additionalFiles: llmAdditionalFiles,
817
845
  });
818
846
 
819
847
  if (!result.ok) {
@@ -852,6 +880,235 @@ server.registerTool('write_selection_to_xcode', {
852
880
  }));
853
881
  });
854
882
 
883
+ // ---------------------------------------------------------------------------
884
+ // figma_to_swiftui — true one-shot Figma-to-SwiftUI generation
885
+ // ---------------------------------------------------------------------------
886
+
887
+ server.registerTool('figma_to_swiftui', {
888
+ description:
889
+ 'One-shot Figma-to-SwiftUI: generates production-ready SwiftUI code from the current Figma selection (or all page frames) and writes it to the Xcode project — including PNG assets, DesignTokens.swift, and reusable components. ' +
890
+ 'With BYOK keys (ANTHROPIC_API_KEY / OPENAI_API_KEY) or a CreateLex subscription this is a TRUE single-call operation. ' +
891
+ 'Without those, it pre-writes assets and returns a structured prompt so the calling AI can generate code and finish with one more write_generated_swiftui_to_xcode call. ' +
892
+ 'Use scope="page" to batch-generate every top-level frame on the current page.',
893
+ inputSchema: {
894
+ nodeIds: z.array(z.string()).optional().describe('Optional list of Figma node ids. If omitted, uses the current selection'),
895
+ scope: z.enum(['selection', 'page']).default('selection').describe('selection = current selection; page = every top-level frame on the active page'),
896
+ generationMode: z.enum(['editable', 'fidelity']).default('editable').describe('editable (default) keeps native SwiftUI elements; fidelity rasterizes the whole frame'),
897
+ includeOverflow: z.boolean().default(false).describe('Ignore Figma clipping when generating layout'),
898
+ projectPath: z.string().optional().describe('Optional Xcode source folder override'),
899
+ },
900
+ }, async ({ nodeIds, scope, generationMode, includeOverflow, projectPath }) => {
901
+ const targetDir = resolveTargetProjectPath(projectPath);
902
+
903
+ // -------------------------------------------------------------------------
904
+ // 1. Resolve target node ids
905
+ // -------------------------------------------------------------------------
906
+ let effectiveNodeIds = nodeIds;
907
+
908
+ if (scope === 'page') {
909
+ const pageSnapshot = await callBridge('get_page_snapshot', { maxDepth: 1 });
910
+ const topFrames = (pageSnapshot?.children ?? []).filter(
911
+ (c) => c.type === 'FRAME' || c.type === 'COMPONENT' || c.type === 'COMPONENT_SET'
912
+ );
913
+ if (topFrames.length === 0) {
914
+ throw new Error('No top-level frames found on the current page');
915
+ }
916
+ effectiveNodeIds = topFrames.map((f) => f.id);
917
+ }
918
+
919
+ // -------------------------------------------------------------------------
920
+ // 2. Get design context (node tree, tokens, asset plan, components)
921
+ // -------------------------------------------------------------------------
922
+ const context = await callBridge('get_design_context', {
923
+ nodeIds: effectiveNodeIds,
924
+ maxDepth: 4,
925
+ includeScreenshot: false,
926
+ });
927
+
928
+ // -------------------------------------------------------------------------
929
+ // 3. Get images from plugin generator
930
+ // -------------------------------------------------------------------------
931
+ const generated = await callBridge('generate_swiftui', {
932
+ nodeIds: effectiveNodeIds,
933
+ includeOverflow,
934
+ generationMode,
935
+ includeImages: true,
936
+ });
937
+
938
+ const images = Array.isArray(generated.images) ? generated.images : [];
939
+ const imageNames = images.map((img) => img.name);
940
+
941
+ // -------------------------------------------------------------------------
942
+ // 4. Attempt LLM generation (Tier 2 BYOK → Tier 3 Hosted)
943
+ // -------------------------------------------------------------------------
944
+ let llmResult = null;
945
+
946
+ // Tier 2: BYOK
947
+ if (process.env.ANTHROPIC_API_KEY || process.env.HF_API_TOKEN || process.env.OPENAI_API_KEY) {
948
+ try {
949
+ const byokResult = await generateWithLocalKey(context, generationMode);
950
+ if (byokResult?.handled) {
951
+ llmResult = byokResult;
952
+ }
953
+ } catch (err) {
954
+ console.error('[figma-swiftui-mcp] BYOK generation failed:', err?.message ?? err);
955
+ }
956
+ }
957
+
958
+ // Tier 3: Hosted (only for editable single-root)
959
+ if (!llmResult && generationMode === 'editable') {
960
+ try {
961
+ const metadata = context?.metadata;
962
+ const isSingleRoot = !!metadata && (!Array.isArray(metadata?.nodes) || metadata.nodes.length === 1);
963
+ if (isSingleRoot && runtimeAuthState.authorized) {
964
+ const { response, data } = await postAuthorizedApi(runtimeAuthState, '/mcp/figma-swiftui/generate', {
965
+ context,
966
+ generationMode,
967
+ includeOverflow,
968
+ });
969
+ if (response.ok && data?.handled) {
970
+ llmResult = {
971
+ handled: true,
972
+ provider: data.provider || 'createlex-hosted',
973
+ code: data.code,
974
+ designTokensCode: null,
975
+ componentFiles: [],
976
+ diagnostics: data.diagnostics || [],
977
+ };
978
+ }
979
+ }
980
+ } catch (err) {
981
+ console.error('[figma-swiftui-mcp] Hosted generation failed (non-fatal):', err?.message ?? err);
982
+ }
983
+ }
984
+
985
+ // -------------------------------------------------------------------------
986
+ // 5a. Tier 2/3 succeeded — parse, write everything, return success
987
+ // -------------------------------------------------------------------------
988
+ if (llmResult) {
989
+ const parsed = parseClaudeResponse(llmResult.code || '');
990
+ const finalCode = parsed.code || llmResult.code || generated.code;
991
+ const effectiveStructName = inferStructName({
992
+ code: finalCode,
993
+ selectionNames: generated.selection?.names ?? [],
994
+ });
995
+
996
+ // Build additional files
997
+ const additionalFiles = [];
998
+ if (parsed.designTokensCode || llmResult.designTokensCode) {
999
+ additionalFiles.push({
1000
+ name: 'DesignTokens.swift',
1001
+ code: parsed.designTokensCode || llmResult.designTokensCode,
1002
+ dir: 'shared',
1003
+ });
1004
+ }
1005
+ for (const comp of (parsed.componentFiles || llmResult.componentFiles || [])) {
1006
+ additionalFiles.push({ name: comp.name, code: comp.code, dir: 'components' });
1007
+ }
1008
+
1009
+ const result = writeSwiftUIScreen({
1010
+ targetDir,
1011
+ code: finalCode,
1012
+ structName: effectiveStructName,
1013
+ images,
1014
+ additionalFiles,
1015
+ });
1016
+
1017
+ if (!result.ok) {
1018
+ throw new Error(result.results.errors.join(' | ') || 'Failed to write generated SwiftUI to Xcode');
1019
+ }
1020
+
1021
+ return jsonResult({
1022
+ tier: llmResult.provider?.startsWith('local-') || llmResult.provider?.startsWith('huggingface:') ? 'byok' : 'hosted',
1023
+ provider: llmResult.provider,
1024
+ oneShot: true,
1025
+ structName: effectiveStructName,
1026
+ swiftFile: result.results?.swiftFile ?? null,
1027
+ additionalFiles: result.results?.additionalSwiftFiles ?? [],
1028
+ imagesWritten: imageNames.length,
1029
+ imageNames,
1030
+ generatedRoot: result.generatedRoot,
1031
+ projectPath: result.projectPath,
1032
+ _critical: [
1033
+ `✅ SwiftUI generated via ${llmResult.provider} and written to ${result.results?.swiftFile ?? 'Xcode'}.`,
1034
+ imageNames.length > 0
1035
+ ? `🖼 ${imageNames.length} PNG asset(s) written: ${imageNames.join(', ')}.`
1036
+ : null,
1037
+ additionalFiles.length > 0
1038
+ ? `📦 ${additionalFiles.length} additional file(s): ${additionalFiles.map((f) => f.name).join(', ')}.`
1039
+ : null,
1040
+ '🚫 DO NOT rewrite the generated Swift file. DO NOT replace Image("name") calls with shapes or colors.',
1041
+ '✏️ You may ONLY: add GeometryReader for responsiveness, wire @State interactions, extract reusable sub-views.',
1042
+ ].filter(Boolean),
1043
+ });
1044
+ }
1045
+
1046
+ // -------------------------------------------------------------------------
1047
+ // 5b. Tier 1 — AI-native: pre-write assets, return structured prompt
1048
+ // -------------------------------------------------------------------------
1049
+
1050
+ // Pre-write images to Assets.xcassets so Claude only needs to generate code
1051
+ let assetsPreWritten = false;
1052
+ if (images.length > 0) {
1053
+ const assetResult = writeAssetCatalogEntries({
1054
+ targetDir,
1055
+ assets: images.map((img) => ({
1056
+ name: img.name,
1057
+ format: img.format || 'png',
1058
+ base64: img.base64,
1059
+ svg: img.svg,
1060
+ })),
1061
+ });
1062
+ assetsPreWritten = assetResult.ok || assetResult.files.length > 0;
1063
+ }
1064
+
1065
+ // Build the generation prompt
1066
+ const prompt = buildGenerationPrompt(context);
1067
+ const effectiveStructName = inferStructName({
1068
+ code: generated.code,
1069
+ selectionNames: generated.selection?.names ?? [],
1070
+ });
1071
+
1072
+ // Build asset manifest for the prompt so the AI knows exactly which Image() names are valid
1073
+ const assetManifest = (context?.assetExportPlan?.candidates ?? []).map((c) => ({
1074
+ name: c.assetName || c.name,
1075
+ blendMode: c.blendModeSwiftUI || null,
1076
+ type: c.suggestedType || c.type || 'image',
1077
+ }));
1078
+
1079
+ return jsonResult({
1080
+ tier: 'ai-native',
1081
+ oneShot: false,
1082
+ assetsPreWritten,
1083
+ imagesWritten: assetsPreWritten ? imageNames.length : 0,
1084
+ imageNames,
1085
+ structName: effectiveStructName,
1086
+ projectPath: targetDir,
1087
+ generationPrompt: prompt
1088
+ ? {
1089
+ systemPrompt: prompt.systemPrompt,
1090
+ userMessage: prompt.userMessage + (assetManifest.length > 0
1091
+ ? `\n\nASSET MANIFEST (already written to Assets.xcassets — use these exact names):\n${JSON.stringify(assetManifest, null, 2)}`
1092
+ : ''),
1093
+ outputStructName: prompt.outputStructName || effectiveStructName,
1094
+ }
1095
+ : null,
1096
+ designContext: prompt ? null : context,
1097
+ assetExportPlan: context?.assetExportPlan ?? null,
1098
+ reusableComponents: context?.reusableComponents ?? null,
1099
+ _critical: [
1100
+ assetsPreWritten
1101
+ ? `🖼 ${imageNames.length} PNG asset(s) pre-written to Assets.xcassets: ${imageNames.join(', ')}.`
1102
+ : null,
1103
+ 'Generate SwiftUI code using the generationPrompt above (systemPrompt + userMessage).',
1104
+ 'Output code using <file name="StructName.swift"> XML tags.',
1105
+ 'Then call write_generated_swiftui_to_xcode with the generated code and images:[] (assets are already on disk).',
1106
+ '⚠️ Use .font(.system(size:weight:)) ONLY — never custom font names.',
1107
+ '⚠️ Reference every asset as Image("name") — never Rectangle() or Color() placeholders.',
1108
+ ].filter(Boolean),
1109
+ });
1110
+ });
1111
+
855
1112
  // ---------------------------------------------------------------------------
856
1113
  // Response-size mitigation helpers
857
1114
  // ---------------------------------------------------------------------------
@@ -6,10 +6,13 @@
6
6
  *
7
7
  * Supported targets:
8
8
  * - Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json)
9
- * - Claude Code (~/.claude.json → mcpServers)
9
+ * - Claude Code (~/.claude.json → mcpServers) (covers CLI + desktop + web)
10
10
  * - Cursor (~/.cursor/mcp.json)
11
11
  * - Windsurf (~/.codeium/windsurf/mcp_config.json)
12
12
  * - VS Code (~/.vscode/mcp.json — user-level)
13
+ * - OpenCode (~/.config/opencode/opencode.json → mcp)
14
+ * - Codex CLI (~/.codex/config.toml → [mcp_servers.figma-swiftui])
15
+ * - Gemini CLI (~/.gemini/settings.json)
13
16
  * - Antigravity (~/.gemini/antigravity/mcp_config.json)
14
17
  */
15
18
 
@@ -118,6 +121,26 @@ function getTargets({ nodePath, scriptPath }) {
118
121
  entry: stdioEntry,
119
122
  wrapKey: null, // VS Code uses { servers: { ... } } at top level
120
123
  },
124
+ {
125
+ name: 'OpenCode',
126
+ path: path.join(home, '.config', 'opencode', 'opencode.json'),
127
+ key: 'mcp',
128
+ entry: scriptPath
129
+ ? { type: 'local', command: [nodePath, scriptPath, 'start'], enabled: true }
130
+ : { type: 'local', command: [resolveNpxPath(), '-y', '@createlex/figma-swiftui-mcp', 'start'], enabled: true },
131
+ },
132
+ {
133
+ name: 'Codex CLI',
134
+ path: path.join(home, '.codex', 'config.toml'),
135
+ format: 'toml',
136
+ entry: { command: stdioEntry.command, args: stdioEntry.args },
137
+ },
138
+ {
139
+ name: 'Gemini CLI',
140
+ path: path.join(home, '.gemini', 'settings.json'),
141
+ key: 'mcpServers',
142
+ entry: stdioEntry,
143
+ },
121
144
  {
122
145
  name: 'Antigravity (Gemini)',
123
146
  path: path.join(home, '.gemini', 'antigravity', 'mcp_config.json'),
@@ -145,6 +168,48 @@ function writeJsonSafe(filePath, data) {
145
168
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
146
169
  }
147
170
 
171
+ // ── TOML helpers (minimal, for Codex config.toml) ────────────────────
172
+ // We only need to append/check a [mcp_servers.<name>] section — no full
173
+ // TOML parser required.
174
+
175
+ function tomlHasServer(raw, serverName) {
176
+ const pattern = new RegExp(`^\\[mcp_servers\\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]`, 'm');
177
+ return pattern.test(raw);
178
+ }
179
+
180
+ function tomlFormatValue(v) {
181
+ if (typeof v === 'string') return `"${v.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
182
+ if (typeof v === 'boolean') return v ? 'true' : 'false';
183
+ if (typeof v === 'number') return String(v);
184
+ if (Array.isArray(v)) return `[${v.map(tomlFormatValue).join(', ')}]`;
185
+ return `"${v}"`;
186
+ }
187
+
188
+ function buildTomlSection(serverName, entry) {
189
+ const lines = [`[mcp_servers.${serverName}]`];
190
+ for (const [k, v] of Object.entries(entry)) {
191
+ if (k === 'env' && typeof v === 'object') continue; // handle env as sub-table
192
+ lines.push(`${k} = ${tomlFormatValue(v)}`);
193
+ }
194
+ if (entry.env && Object.keys(entry.env).length > 0) {
195
+ lines.push(`[mcp_servers.${serverName}.env]`);
196
+ for (const [ek, ev] of Object.entries(entry.env)) {
197
+ lines.push(`${ek} = ${tomlFormatValue(ev)}`);
198
+ }
199
+ }
200
+ return lines.join('\n') + '\n';
201
+ }
202
+
203
+ function appendTomlServer(filePath, serverName, entry, dryRun) {
204
+ const raw = fs.readFileSync(filePath, 'utf-8');
205
+ if (tomlHasServer(raw, serverName)) return false; // already present
206
+ const section = '\n' + buildTomlSection(serverName, entry);
207
+ if (!dryRun) {
208
+ fs.appendFileSync(filePath, section, 'utf-8');
209
+ }
210
+ return true;
211
+ }
212
+
148
213
  // ── Main ──────────────────────────────────────────────────────────────
149
214
  function runSetup(flags = {}) {
150
215
  const force = flags.force || false;
@@ -177,6 +242,25 @@ function runSetup(flags = {}) {
177
242
  continue;
178
243
  }
179
244
 
245
+ // ── TOML targets (Codex) ──────────────────────────────────────
246
+ if (target.format === 'toml') {
247
+ const raw = fs.readFileSync(target.path, 'utf-8');
248
+ if (tomlHasServer(raw, MCP_KEY) && !force) {
249
+ console.log(` ✅ ${target.name} — already configured`);
250
+ skipped++;
251
+ continue;
252
+ }
253
+ if (dryRun) {
254
+ console.log(` 🟡 ${target.name} — would configure (dry run)`);
255
+ } else {
256
+ appendTomlServer(target.path, MCP_KEY, target.entry, false);
257
+ console.log(` ✅ ${target.name} — configured!`);
258
+ }
259
+ configured++;
260
+ continue;
261
+ }
262
+
263
+ // ── JSON targets ────────────────────────────────────────────
180
264
  const config = readJsonSafe(target.path);
181
265
  if (!config) {
182
266
  console.log(` ⚠️ ${target.name} — could not parse config`);
@@ -443,6 +443,63 @@ function scoreSourceDirectory(dirPath, preferredNames = new Set()) {
443
443
  return score;
444
444
  }
445
445
 
446
+ /**
447
+ * Write multiple screens from a single generation batch.
448
+ * Shares DesignTokens.swift and Components across all screens.
449
+ *
450
+ * @param {Object} opts
451
+ * @param {string} opts.targetDir - Xcode source folder
452
+ * @param {Array<{structName: string, code: string, images?: Array}>} opts.screens
453
+ * @param {string|null} opts.designTokensCode - Shared DesignTokens.swift
454
+ * @param {Array<{name: string, code: string}>} opts.componentFiles - Shared components
455
+ * @returns {{ok: boolean, screens: Array, errors: string[]}}
456
+ */
457
+ function writeMultiScreenProject({ targetDir, screens = [], designTokensCode = null, componentFiles = [] }) {
458
+ const allErrors = [];
459
+ const screenResults = [];
460
+
461
+ // Build shared additional files list
462
+ const sharedFiles = [];
463
+ if (designTokensCode) {
464
+ sharedFiles.push({ name: 'DesignTokens.swift', code: designTokensCode, dir: 'shared' });
465
+ }
466
+ for (const comp of componentFiles) {
467
+ sharedFiles.push({ name: comp.name, code: comp.code, dir: 'components' });
468
+ }
469
+
470
+ for (let i = 0; i < screens.length; i++) {
471
+ const screen = screens[i];
472
+ // Only include shared files on the first screen write to avoid duplicates
473
+ const additionalFiles = i === 0 ? sharedFiles : [];
474
+
475
+ const result = writeSwiftUIScreen({
476
+ targetDir,
477
+ code: screen.code,
478
+ structName: screen.structName,
479
+ images: Array.isArray(screen.images) ? screen.images : [],
480
+ additionalFiles,
481
+ });
482
+
483
+ screenResults.push({
484
+ structName: screen.structName,
485
+ ok: result.ok,
486
+ swiftFile: result.results?.swiftFile ?? null,
487
+ imageCount: (screen.images ?? []).length,
488
+ });
489
+
490
+ if (!result.ok) {
491
+ allErrors.push(...result.results.errors);
492
+ }
493
+ }
494
+
495
+ return {
496
+ ok: allErrors.length === 0,
497
+ screens: screenResults,
498
+ errors: allErrors,
499
+ projectPath: targetDir,
500
+ };
501
+ }
502
+
446
503
  module.exports = {
447
504
  GENERATED_ROOT_DIRNAME,
448
505
  GENERATED_LAYOUT_VERSION,
@@ -454,5 +511,6 @@ module.exports = {
454
511
  resolveWritableProjectPath,
455
512
  setSavedProjectPath,
456
513
  writeAssetCatalogEntries,
514
+ writeMultiScreenProject,
457
515
  writeSwiftUIScreen,
458
516
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@createlex/figma-swiftui-mcp",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
4
4
  "description": "CreateLex MCP runtime for Figma-to-SwiftUI generation and Xcode export",
5
5
  "bin": {
6
6
  "figma-swiftui-mcp": "bin/figma-swiftui-mcp.js"