@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.
- package/companion/local-llm-generator.cjs +2 -0
- package/companion/mcp-server.mjs +273 -16
- package/companion/setup.cjs +85 -1
- package/companion/xcode-writer.cjs +58 -0
- package/package.json +1 -1
package/companion/mcp-server.mjs
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
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
|
|
786
|
+
const semanticResult = await tryHostedSemanticGeneration({
|
|
782
787
|
nodeIds,
|
|
783
788
|
generationMode,
|
|
784
789
|
includeOverflow,
|
|
785
|
-
analyze:
|
|
790
|
+
analyze: false,
|
|
786
791
|
});
|
|
787
|
-
if (
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
|
797
|
-
console.error('[figma-swiftui-mcp]
|
|
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:
|
|
835
|
+
code: finalCode,
|
|
809
836
|
selectionNames: generated.selection?.names ?? [],
|
|
810
837
|
});
|
|
811
838
|
|
|
812
839
|
const result = writeSwiftUIScreen({
|
|
813
840
|
targetDir,
|
|
814
|
-
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
|
// ---------------------------------------------------------------------------
|
package/companion/setup.cjs
CHANGED
|
@@ -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