Skip to content

Commit 372923f

Browse files
felixtrzmeta-codesync[bot]
authored andcommitted
fix(ai-scaffolding): fix MCP config merge bugs and add multi-tool recipe generation
Summary: Address code review issues in MCP config merge implementation: - Fix race condition: await in-flight config writes before cleanup on server close - Add TOML marker ordering validation (startIdx < endIdx guard) - Use Promise.allSettled in writeMcpConfigs so one tool failure doesn't block others - Add missing cancelled check in prompts.ts between gitInit and aiTools prompts - Fix vite.config template to use valid TS placeholder with chef:mcp anchor - Add bracket tracking in build-assets.mjs for chef:mcp array replacement - Add 4 new test cases for merge/unmerge edge cases (sibling keys, wrong marker order, user content preservation) Also includes the multi-tool AI scaffolding feature: CLI --ai-tools flag, per-tool recipe generation (cursor, copilot, codex, agents), and bundle support. Reviewed By: zjm-meta Differential Revision: D94132897 fbshipit-source-id: e28c62edde51b3a6cc2ce94f4d17d7952d23f466
1 parent 355cb1d commit 372923f

File tree

14 files changed

+2033
-62
lines changed

14 files changed

+2033
-62
lines changed

packages/create/src/cli.ts

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
import { MSE_MIN_VERSION } from './mse-config.js';
3131
import { scaffoldProject } from './scaffold.js';
3232
import { resolveSource, SDK_PACKAGES_DIR } from './source.js';
33-
import { MSEInstallResult, PromptResult, TriState, VariantId } from './types.js';
33+
import { MSEInstallResult, PromptResult, TriState, VariantId, AiTool } from './types.js';
3434
import { VERSION, NODE_ENGINE } from './version.js';
3535

3636
type CliOptions = {
@@ -46,6 +46,7 @@ type CliOptions = {
4646
physics?: boolean;
4747
sceneUnderstanding?: boolean;
4848
environmentRaycast?: boolean;
49+
aiTools?: string;
4950
};
5051

5152
async function main() {
@@ -112,6 +113,7 @@ IWSDK Create CLI v${VERSION}\nNode ${process.version}`;
112113
.option('--no-scene-understanding', 'Disable scene understanding')
113114
.option('--environment-raycast', 'Enable environment raycast (AR mode)', true)
114115
.option('--no-environment-raycast', 'Disable environment raycast')
116+
.option('--ai-tools <tools>', 'AI tools to configure (comma-separated: claude,cursor,copilot,codex; or "none")', 'claude,cursor,copilot,codex')
115117
.action((n: string | undefined, opts: CliOptions) => {
116118
nameArg = n;
117119
cliOpts = opts;
@@ -169,6 +171,7 @@ IWSDK Create CLI v${VERSION}\nNode ${process.version}`;
169171
'--physics', '--no-physics',
170172
'--scene-understanding', '--no-scene-understanding',
171173
'--environment-raycast', '--no-environment-raycast',
174+
'--ai-tools',
172175
];
173176
const hasExplicitFlags = process.argv.some((arg) =>
174177
explicitFlags.includes(arg),
@@ -202,6 +205,12 @@ IWSDK Create CLI v${VERSION}\nNode ${process.version}`;
202205
const workflow = metaspatial ? 'metaspatial' : 'manual';
203206
const variantId = `${mode}-${workflow}-${language}` as VariantId;
204207

208+
const validAiTools = ['claude', 'cursor', 'copilot', 'codex'] as const;
209+
const rawAiTools = (cliOpts.aiTools || 'claude,cursor,copilot,codex').split(',').map((t) => t.trim());
210+
const aiTools = rawAiTools.includes('none')
211+
? []
212+
: rawAiTools.filter((t): t is AiTool => validAiTools.includes(t as AiTool));
213+
205214
const locomotionEnabled =
206215
mode === 'vr' ? (cliOpts.locomotion ?? true) : false;
207216

@@ -255,6 +264,7 @@ IWSDK Create CLI v${VERSION}\nNode ${process.version}`;
255264
: false,
256265
},
257266
gitInit: cliOpts.git ?? true,
267+
aiTools,
258268
xrFeatureStates:
259269
mode === 'ar'
260270
? {
@@ -356,6 +366,13 @@ IWSDK Create CLI v${VERSION}\nNode ${process.version}`;
356366
}
357367
const xrLiteral = `{ ${entries.join(', ')} }`;
358368
resolvedRecipe.edits['@xrFeaturesStr'] = xrLiteral;
369+
370+
// MCP tool selection for vite.config.ts
371+
const mcpToolsLiteral = res.aiTools.length > 0
372+
? `[${res.aiTools.map((t) => `'${t}'`).join(', ')}]`
373+
: `['claude', 'cursor', 'copilot', 'codex']`;
374+
resolvedRecipe.edits['@mcpToolsStr'] = mcpToolsLiteral;
375+
359376
const outDir = join(process.cwd(), res.name);
360377

361378
// Check if target directory already exists and is non-empty
@@ -369,20 +386,70 @@ IWSDK Create CLI v${VERSION}\nNode ${process.version}`;
369386
);
370387
}
371388

372-
// Try to load the Claude Code configuration recipe
373-
let claudeRecipe: Recipe | null = null;
374-
try {
375-
const rawClaudeRecipe = await source.fetchRecipe(
376-
'base-claude-config.recipe.json',
377-
);
378-
claudeRecipe = source.resolveRecipeUrls(rawClaudeRecipe);
379-
} catch {
380-
// Not yet published — skip silently
389+
// Load AI tool configuration recipes based on user selection
390+
const aiRecipes: Recipe[] = [];
391+
392+
if (res.aiTools.length > 0) {
393+
// AGENTS.md recipe (loaded when any AI tool is selected — universal baseline)
394+
try {
395+
const rawAgentsRecipe = await source.fetchRecipe(
396+
'base-agents-config.recipe.json',
397+
);
398+
aiRecipes.push(source.resolveRecipeUrls(rawAgentsRecipe));
399+
} catch {
400+
// Not yet published — skip silently
401+
}
402+
}
403+
404+
// Claude Code recipe (conditional)
405+
if (res.aiTools.includes('claude')) {
406+
try {
407+
const rawClaudeRecipe = await source.fetchRecipe(
408+
'base-claude-config.recipe.json',
409+
);
410+
aiRecipes.push(source.resolveRecipeUrls(rawClaudeRecipe));
411+
} catch {
412+
// Not yet published — skip silently
413+
}
414+
}
415+
416+
// Cursor recipe (conditional)
417+
if (res.aiTools.includes('cursor')) {
418+
try {
419+
const rawCursorRecipe = await source.fetchRecipe(
420+
'base-cursor-config.recipe.json',
421+
);
422+
aiRecipes.push(source.resolveRecipeUrls(rawCursorRecipe));
423+
} catch {
424+
// Not yet published — skip silently
425+
}
426+
}
427+
428+
// Copilot recipe (conditional)
429+
if (res.aiTools.includes('copilot')) {
430+
try {
431+
const rawCopilotRecipe = await source.fetchRecipe(
432+
'base-copilot-config.recipe.json',
433+
);
434+
aiRecipes.push(source.resolveRecipeUrls(rawCopilotRecipe));
435+
} catch {
436+
// Not yet published — skip silently
437+
}
438+
}
439+
440+
// Codex recipe (conditional)
441+
if (res.aiTools.includes('codex')) {
442+
try {
443+
const rawCodexRecipe = await source.fetchRecipe(
444+
'base-codex-config.recipe.json',
445+
);
446+
aiRecipes.push(source.resolveRecipeUrls(rawCodexRecipe));
447+
} catch {
448+
// Not yet published — skip silently
449+
}
381450
}
382451

383-
const recipes: Recipe[] = claudeRecipe
384-
? [resolvedRecipe, claudeRecipe]
385-
: [resolvedRecipe];
452+
const recipes: Recipe[] = [resolvedRecipe, ...aiRecipes];
386453
await scaffoldProject(recipes, outDir);
387454

388455
// Git init

packages/create/src/prompts.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import prompts from 'prompts';
99
import { installMSE } from './mse-installer.js';
10-
import { MSEInstallResult, PromptResult, TriState, VariantId } from './types.js';
10+
import { MSEInstallResult, PromptResult, TriState, VariantId, AiTool } from './types.js';
1111

1212
export async function promptFlow(nameArg?: string): Promise<PromptResult> {
1313
let cancelled = false;
@@ -40,6 +40,31 @@ export async function promptFlow(nameArg?: string): Promise<PromptResult> {
4040
throw new Error('Input cancelled');
4141
}
4242

43+
const { aiTools } = await prompts(
44+
{
45+
type: 'multiselect',
46+
name: 'aiTools',
47+
message: 'Which AI coding tools do you use?',
48+
choices: [
49+
{ title: 'Claude Code (Anthropic)', value: 'claude', selected: true },
50+
{ title: 'Cursor', value: 'cursor' },
51+
{ title: 'GitHub Copilot', value: 'copilot' },
52+
{ title: 'OpenAI Codex', value: 'codex' },
53+
{ title: 'None', value: 'none' },
54+
],
55+
hint: '- Space to select, Enter to confirm',
56+
},
57+
{ onCancel },
58+
);
59+
if (cancelled) {
60+
throw new Error('Input cancelled');
61+
}
62+
63+
// If "none" is selected, clear all other selections
64+
const resolvedAiTools: AiTool[] = (aiTools as string[])?.includes('none')
65+
? []
66+
: ((aiTools as AiTool[]) || []);
67+
4368
const { language, mode } = await prompts(
4469
[
4570
{
@@ -254,9 +279,9 @@ export async function promptFlow(nameArg?: string): Promise<PromptResult> {
254279
title: 'Yes (Install Meta Spatial Editor if needed)',
255280
value: true,
256281
},
257-
{ title: 'No (Can change later)', value: false },
282+
{ title: 'Skip for now (can change later)', value: false },
258283
],
259-
initial: 0,
284+
initial: 1,
260285
},
261286
{ onCancel },
262287
);
@@ -300,6 +325,10 @@ export async function promptFlow(nameArg?: string): Promise<PromptResult> {
300325
{ onCancel },
301326
);
302327

328+
if (cancelled) {
329+
throw new Error('Input cancelled');
330+
}
331+
303332
const kind = metaspatial ? 'metaspatial' : 'manual';
304333
const id = `${mode}-${kind}-${language}` as VariantId;
305334
return {
@@ -319,6 +348,7 @@ export async function promptFlow(nameArg?: string): Promise<PromptResult> {
319348
environmentRaycastEnabled: !!environmentRaycastEnabled,
320349
},
321350
gitInit,
351+
aiTools: resolvedAiTools,
322352
xrFeatureStates,
323353
actionItems,
324354
prerequisites,

packages/create/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
export type VariantId =
99
`${'vr' | 'ar'}-${'manual' | 'metaspatial'}-${'ts' | 'js'}`;
1010
export type TriState = 'no' | 'optional' | 'required';
11+
export type AiTool = 'claude' | 'cursor' | 'copilot' | 'codex';
1112

1213
/**
1314
* Platform type for MSE installation detection
@@ -54,6 +55,7 @@ export type PromptResult = {
5455
environmentRaycastEnabled: boolean; // AR-relevant, no room scanning required
5556
};
5657
gitInit: boolean;
58+
aiTools: AiTool[];
5759
xrFeatureStates: Record<string, TriState>;
5860
actionItems?: ActionItem[];
5961
prerequisites?: ActionItem[];

0 commit comments

Comments
 (0)