@ikas/code-components-mcp 2.1.0 → 2.2.0

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.
Files changed (54) hide show
  1. package/data/framework.json +22 -5
  2. package/data/migration-examples/complex-header-migration/_meta.json +4 -0
  3. package/data/migration-examples/complex-header-migration/after-config-snippet.json +55 -0
  4. package/data/migration-examples/complex-header-migration/after-section.tsx +64 -0
  5. package/data/migration-examples/complex-header-migration/before-props-summary.json +42 -0
  6. package/data/migration-examples/custom-dynamic-list-to-component-list/_meta.json +4 -0
  7. package/data/migration-examples/custom-dynamic-list-to-component-list/after-child-styles.css +38 -0
  8. package/data/migration-examples/custom-dynamic-list-to-component-list/after-child.tsx +22 -0
  9. package/data/migration-examples/custom-dynamic-list-to-component-list/after-config-snippet.json +31 -0
  10. package/data/migration-examples/custom-dynamic-list-to-component-list/after-section-styles.css +25 -0
  11. package/data/migration-examples/custom-dynamic-list-to-component-list/after-section.tsx +17 -0
  12. package/data/migration-examples/custom-dynamic-list-to-component-list/before-component.tsx +32 -0
  13. package/data/migration-examples/custom-dynamic-list-to-component-list/before-theme-snippet.json +53 -0
  14. package/data/migration-examples/full-component-with-tailwind/_meta.json +4 -0
  15. package/data/migration-examples/full-component-with-tailwind/after-component.tsx +43 -0
  16. package/data/migration-examples/full-component-with-tailwind/after-config-snippet.json +25 -0
  17. package/data/migration-examples/full-component-with-tailwind/after-styles.css +99 -0
  18. package/data/migration-examples/full-component-with-tailwind/before-component.tsx +60 -0
  19. package/data/migration-examples/object-custom-to-inline-props/_meta.json +4 -0
  20. package/data/migration-examples/object-custom-to-inline-props/after-component.tsx +34 -0
  21. package/data/migration-examples/object-custom-to-inline-props/after-config-snippet.json +23 -0
  22. package/data/migration-examples/object-custom-to-inline-props/after-styles.css +38 -0
  23. package/data/migration-examples/object-custom-to-inline-props/before-component.tsx +30 -0
  24. package/data/migration-examples/object-custom-to-inline-props/before-theme-snippet.json +26 -0
  25. package/data/migration-examples/slider-library-replacement/_meta.json +4 -0
  26. package/data/migration-examples/slider-library-replacement/after-child.tsx +13 -0
  27. package/data/migration-examples/slider-library-replacement/after-config-snippet.json +29 -0
  28. package/data/migration-examples/slider-library-replacement/after-section.tsx +38 -0
  29. package/data/migration-examples/slider-library-replacement/after-styles.css +43 -0
  30. package/data/migration-examples/slider-library-replacement/before-component.tsx +39 -0
  31. package/data/migration-examples/slider-library-replacement/before-types-snippet.ts +14 -0
  32. package/data/migration.json +260 -0
  33. package/data/section-templates/account-info-section/children/AccountFavorites/ikas-config-snippet.json +3 -3
  34. package/data/section-templates/account-info-section/ikas-config-snippet.json +5 -5
  35. package/data/section-templates/category-images-section/ikas-config-snippet.json +1 -1
  36. package/data/section-templates/category-list-section/ikas-config-snippet.json +3 -3
  37. package/data/section-templates/component-renderer/ikas-config-snippet.json +3 -3
  38. package/data/section-templates/features-section/ikas-config-snippet.json +1 -1
  39. package/data/section-templates/footer-section/ikas-config-snippet.json +1 -1
  40. package/data/section-templates/header-section/children/Announcements/ikas-config-snippet.json +1 -1
  41. package/data/section-templates/header-section/children/Navbar/ikas-config-snippet.json +3 -3
  42. package/data/section-templates/header-section/ikas-config-snippet.json +3 -3
  43. package/data/section-templates/hero-slider-section/ikas-config-snippet.json +1 -1
  44. package/data/section-templates/image-handling/ikas-config-snippet.json +13 -13
  45. package/data/section-templates/navigation/ikas-config-snippet.json +3 -3
  46. package/data/section-templates/product-detail-section/children/ProductDetailDescription/ikas-config-snippet.json +1 -1
  47. package/data/section-templates/product-detail-section/children/ProductDetailFeatures/ikas-config-snippet.json +1 -1
  48. package/data/section-templates/product-detail-section/ikas-config-snippet.json +13 -13
  49. package/data/section-templates/product-slider-section/ikas-config-snippet.json +3 -3
  50. package/data/storefront-api.json +27 -1
  51. package/data/storefront-types.json +2 -1
  52. package/dist/index.js +1737 -47
  53. package/dist/index.js.map +1 -1
  54. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { z } from "zod";
5
4
  import * as fs from "fs";
6
5
  import * as path from "path";
7
6
  import { fileURLToPath } from "url";
7
+ import { z } from "zod";
8
8
  const __filename = fileURLToPath(import.meta.url);
9
9
  const __dirname = path.dirname(__filename);
10
10
  // --- Load data ---
@@ -15,6 +15,59 @@ function loadJsonFile(relativePath) {
15
15
  }
16
16
  return JSON.parse(fs.readFileSync(filePath, "utf-8"));
17
17
  }
18
+ // Resolve theme.json from either raw string or absolute file path.
19
+ // Exactly one of the two must be provided.
20
+ function resolveThemeJson(themeJson, themeJsonPath) {
21
+ const hasInline = typeof themeJson === "string" && themeJson.length > 0;
22
+ const hasPath = typeof themeJsonPath === "string" && themeJsonPath.length > 0;
23
+ if (hasInline === hasPath) {
24
+ throw new Error("Provide exactly one of `theme_json` (raw JSON string) or `theme_json_path` (absolute path to theme.json).");
25
+ }
26
+ let raw;
27
+ if (hasPath) {
28
+ if (!path.isAbsolute(themeJsonPath)) {
29
+ throw new Error(`theme_json_path must be absolute: ${themeJsonPath}`);
30
+ }
31
+ if (!fs.existsSync(themeJsonPath)) {
32
+ throw new Error(`theme_json_path not found: ${themeJsonPath}`);
33
+ }
34
+ try {
35
+ raw = fs.readFileSync(themeJsonPath, "utf-8");
36
+ }
37
+ catch (e) {
38
+ throw new Error(`Failed to read theme_json_path "${themeJsonPath}": ${e.message}`);
39
+ }
40
+ }
41
+ else {
42
+ raw = themeJson;
43
+ }
44
+ try {
45
+ return JSON.parse(raw);
46
+ }
47
+ catch (e) {
48
+ throw new Error(`Invalid JSON in theme.json: ${e.message}`);
49
+ }
50
+ }
51
+ // Atomic file write: temp file in same dir, then rename. Same-fs rename is atomic on POSIX.
52
+ function writeFileAtomic(targetPath, content) {
53
+ const dir = path.dirname(targetPath);
54
+ const base = path.basename(targetPath);
55
+ const tmp = path.join(dir, `.${base}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
56
+ try {
57
+ fs.writeFileSync(tmp, content, "utf-8");
58
+ fs.renameSync(tmp, targetPath);
59
+ }
60
+ catch (e) {
61
+ try {
62
+ if (fs.existsSync(tmp))
63
+ fs.unlinkSync(tmp);
64
+ }
65
+ catch {
66
+ // ignore cleanup failure
67
+ }
68
+ throw e;
69
+ }
70
+ }
18
71
  // Try multiple paths for storefront data (generated output or local data dir)
19
72
  function loadStorefrontData() {
20
73
  const paths = [
@@ -50,7 +103,15 @@ function normalizeName(value) {
50
103
  const storefrontData = loadStorefrontData();
51
104
  const frameworkData = loadJsonFile("../data/framework.json");
52
105
  const typesData = loadStorefrontTypes();
106
+ let migrationData = null;
107
+ try {
108
+ migrationData = loadJsonFile("../data/migration.json");
109
+ }
110
+ catch {
111
+ migrationData = null;
112
+ }
53
113
  const SECTION_TEMPLATES_DIR = path.resolve(__dirname, "../data/section-templates");
114
+ const MIGRATION_EXAMPLES_DIR = path.resolve(__dirname, "../data/migration-examples");
54
115
  function listSectionTemplateNames() {
55
116
  try {
56
117
  return fs
@@ -159,6 +220,1216 @@ function loadSectionSubtreeItem(section, subtree, name) {
159
220
  return { files };
160
221
  }
161
222
  const sectionTemplateNames = listSectionTemplateNames();
223
+ // --- Migration example helpers ---
224
+ function listMigrationExampleNames() {
225
+ try {
226
+ return fs
227
+ .readdirSync(MIGRATION_EXAMPLES_DIR, { withFileTypes: true })
228
+ .filter((e) => e.isDirectory())
229
+ .map((e) => e.name)
230
+ .sort();
231
+ }
232
+ catch {
233
+ return [];
234
+ }
235
+ }
236
+ function loadMigrationExample(name) {
237
+ const root = path.join(MIGRATION_EXAMPLES_DIR, name);
238
+ if (!fs.existsSync(root))
239
+ return null;
240
+ const metaPath = path.join(root, "_meta.json");
241
+ if (!fs.existsSync(metaPath))
242
+ return null;
243
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
244
+ const files = {};
245
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
246
+ if (entry.name === "_meta.json" || entry.isDirectory())
247
+ continue;
248
+ files[entry.name] = fs.readFileSync(path.join(root, entry.name), "utf-8");
249
+ }
250
+ return { title: meta.title, description: meta.description, files };
251
+ }
252
+ const migrationExampleNames = listMigrationExampleNames();
253
+ function searchMigrationTopics(query) {
254
+ if (!migrationData)
255
+ return [];
256
+ return Object.entries(migrationData.topics)
257
+ .map(([key, topic]) => {
258
+ const titleScore = matchScore(topic.title, query) * 3;
259
+ const descScore = matchScore(topic.description, query) * 2;
260
+ const contentScore = matchScore(topic.content, query);
261
+ const tagScore = topic.tags.some((t) => matchScore(t, query) > 0) ? 5 : 0;
262
+ return { key, topic, score: titleScore + descScore + contentScore + tagScore };
263
+ })
264
+ .filter((item) => item.score > 0)
265
+ .sort((a, b) => b.score - a.score);
266
+ }
267
+ function analyzeOldTheme(themeJson) {
268
+ const parts = [];
269
+ const components = themeJson.components || [];
270
+ const customData = themeJson.customData || [];
271
+ const groups = themeJson.groups || [];
272
+ const settings = themeJson.settings;
273
+ // Build customData lookup
274
+ const customDataMap = new Map();
275
+ for (const cd of customData) {
276
+ if (cd.id)
277
+ customDataMap.set(cd.id, cd);
278
+ }
279
+ // Statistics
280
+ let totalProps = 0;
281
+ let customProps = 0;
282
+ let sliderProps = 0;
283
+ let productDetailProps = 0;
284
+ let estimatedChildComponents = 0;
285
+ parts.push(`# Old Theme Analysis\n`);
286
+ parts.push(`> **CRITICAL:** The old system (\`@ikas/storefront\`) and the new code-component system (\`@ikas/bp-storefront\`) are **entirely different packages**. Even when type names look the same (e.g., \`IkasImage\`, \`IkasProduct\`), they are different types with different properties. The prop type systems are also completely separate — old theme.json prop types and new ikas.config.json prop types have different semantics even when names match. **Never assume old-system knowledge applies to the new system.** Always use \`get_type_definition\`, \`get_model_guide\`, and \`get_function_doc\` to look up the correct new-system APIs.\n`);
287
+ parts.push(`## Summary Statistics\n`);
288
+ parts.push(`- **Components:** ${components.length}`);
289
+ parts.push(`- **Custom Data Definitions:** ${customData.filter(cd => cd.isRoot).length}`);
290
+ parts.push(`- **Prop Groups:** ${groups.length}`);
291
+ // Component analysis
292
+ parts.push(`\n## Components (${components.length})\n`);
293
+ for (const comp of components) {
294
+ const props = comp.props || [];
295
+ totalProps += props.length;
296
+ const propTypeCounts = {};
297
+ const customPropDetails = [];
298
+ const sliderPropDetails = [];
299
+ for (const prop of props) {
300
+ const pType = prop.type || "UNKNOWN";
301
+ propTypeCounts[pType] = (propTypeCounts[pType] || 0) + 1;
302
+ if (pType === "CUSTOM" && prop.customDataId) {
303
+ customProps++;
304
+ const cd = customDataMap.get(prop.customDataId);
305
+ if (cd) {
306
+ const cdType = cd.type || "UNKNOWN";
307
+ customPropDetails.push(` - \`${prop.name}\` → ${cd.name || "unnamed"} (${cdType})`);
308
+ if (cdType === "DYNAMIC_LIST" || cdType === "STATIC_LIST") {
309
+ estimatedChildComponents++;
310
+ }
311
+ }
312
+ else {
313
+ customPropDetails.push(` - \`${prop.name}\` → [unresolved customDataId]`);
314
+ }
315
+ }
316
+ if (pType === "SLIDER") {
317
+ sliderProps++;
318
+ const sd = prop.sliderData;
319
+ sliderPropDetails.push(` - \`${prop.name}\` (${sd?.min ?? "?"}–${sd?.max ?? "?"}, step ${sd?.interval ?? "?"})`);
320
+ }
321
+ if (pType === "PRODUCT_DETAIL") {
322
+ productDetailProps++;
323
+ }
324
+ }
325
+ const typesSummary = Object.entries(propTypeCounts)
326
+ .map(([t, c]) => `${t}×${c}`)
327
+ .join(", ");
328
+ const headerFooter = comp.isHeader ? " [HEADER]" : comp.isFooter ? " [FOOTER]" : "";
329
+ parts.push(`### ${comp.displayName || comp.dir || comp.id}${headerFooter}`);
330
+ parts.push(`- **Dir:** \`${comp.dir || "?"}\` | **Props:** ${props.length} (${typesSummary})`);
331
+ parts.push(`- **Recommended new type:** section`);
332
+ if (customPropDetails.length > 0) {
333
+ parts.push(`- **CUSTOM props** (→ COMPONENT_LIST):`);
334
+ parts.push(customPropDetails.join("\n"));
335
+ }
336
+ if (sliderPropDetails.length > 0) {
337
+ parts.push(`- **SLIDER props** (→ NUMBER):`);
338
+ parts.push(sliderPropDetails.join("\n"));
339
+ }
340
+ parts.push("");
341
+ }
342
+ // Custom data analysis
343
+ const rootCustomData = customData.filter(cd => cd.isRoot);
344
+ if (rootCustomData.length > 0) {
345
+ parts.push(`\n## Custom Data Definitions (${rootCustomData.length})\n`);
346
+ for (const cd of rootCustomData) {
347
+ parts.push(`### ${cd.name || cd.typescriptName || cd.id}`);
348
+ parts.push(`- **Type:** ${cd.type}`);
349
+ if (cd.nestedData && cd.nestedData.length > 0) {
350
+ const describeNested = (items, indent) => {
351
+ const lines = [];
352
+ for (const item of items) {
353
+ const key = item.key ? `\`${item.key}\`` : item.typescriptName || item.name || "unnamed";
354
+ lines.push(`${indent}- ${key}: ${item.type}${item.isRequired ? " (required)" : ""}`);
355
+ if (item.nestedData && item.nestedData.length > 0) {
356
+ lines.push(...describeNested(item.nestedData, indent + " "));
357
+ }
358
+ }
359
+ return lines;
360
+ };
361
+ parts.push("- **Structure:**");
362
+ parts.push(...describeNested(cd.nestedData, " "));
363
+ }
364
+ if (cd.enumOptions && cd.enumOptions.length > 0) {
365
+ parts.push(`- **Enum options:** ${cd.enumOptions.map(o => `"${o.value}"`).join(", ")}`);
366
+ }
367
+ // Find which components reference this customData
368
+ const referencingComponents = [];
369
+ for (const comp of components) {
370
+ for (const prop of comp.props || []) {
371
+ if (prop.type === "CUSTOM" && prop.customDataId === cd.id) {
372
+ referencingComponents.push(`${comp.displayName || comp.dir || comp.id}.${prop.name}`);
373
+ }
374
+ }
375
+ }
376
+ if (referencingComponents.length > 0) {
377
+ parts.push(`- **Referenced by:** ${referencingComponents.join(", ")}`);
378
+ }
379
+ parts.push("");
380
+ }
381
+ }
382
+ // Settings
383
+ if (settings) {
384
+ parts.push(`\n## Settings\n`);
385
+ if (settings.colors && settings.colors.length > 0) {
386
+ parts.push(`### Colors (${settings.colors.length})`);
387
+ for (const c of settings.colors) {
388
+ parts.push(`- \`${c.key}\`: ${c.color} (${c.displayName})`);
389
+ }
390
+ parts.push("");
391
+ }
392
+ if (settings.fontFamily) {
393
+ parts.push(`### Font: ${settings.fontFamily.name} (weights: ${settings.fontFamily.variants?.join(", ")})`);
394
+ parts.push("");
395
+ }
396
+ }
397
+ // Groups
398
+ if (groups.length > 0) {
399
+ parts.push(`\n## Prop Groups (${groups.length})\n`);
400
+ for (const g of groups) {
401
+ parts.push(`- \`${g.id}\`: ${g.name}`);
402
+ }
403
+ parts.push("");
404
+ }
405
+ // Final statistics
406
+ parts.push(`\n## Migration Statistics\n`);
407
+ parts.push(`- **Total props across all components:** ${totalProps}`);
408
+ parts.push(`- **CUSTOM props (need conversion):** ${customProps}`);
409
+ parts.push(`- **SLIDER props (→ NUMBER):** ${sliderProps}`);
410
+ parts.push(`- **PRODUCT_DETAIL props (→ PRODUCT):** ${productDetailProps}`);
411
+ parts.push(`- **Estimated child components needed:** ${estimatedChildComponents}`);
412
+ parts.push(`- **Total components in new system:** ~${components.length + estimatedChildComponents} (${components.length} sections + ${estimatedChildComponents} children)`);
413
+ parts.push(`\n## Recommended Workflow\n`);
414
+ const useIterative = components.length > 5;
415
+ if (useIterative) {
416
+ parts.push(`**This theme has ${components.length} sections — USE THE ITERATIVE WORKFLOW.** A single session cannot migrate this entire theme.\n`);
417
+ parts.push(`1. Call \`plan_migration(theme_json, old_source_dir)\` to generate MIGRATION.md (this is your resumable plan).`);
418
+ parts.push(`2. Save the output to \`<new-project-root>/MIGRATION.md\`.`);
419
+ parts.push(`3. Execute the Foundation checklist (global.css, enums, shared sub-components) in MIGRATION.md.`);
420
+ parts.push(`4. For each section: call \`get_section_migration_plan(theme_json, section_name)\`, implement it, and mark \`[x]\` in MIGRATION.md.`);
421
+ parts.push(`5. In any new session, **read MIGRATION.md first** and resume from the first \`[ ]\` item.\n`);
422
+ parts.push(`Read \`get_migration_guide("iterative-workflow")\` for the full protocol.`);
423
+ }
424
+ else {
425
+ parts.push(`This theme is small enough (${components.length} sections) for a one-pass migration.\n`);
426
+ parts.push(`1. Call \`get_migration_guide("migration-overview")\` for the full workflow`);
427
+ parts.push(`2. Call \`get_migration_guide("prop-type-mapping")\` for prop conversion details`);
428
+ parts.push(`3. Call \`get_migration_guide("custom-data-conversion")\` for CUSTOM → COMPONENT_LIST patterns`);
429
+ parts.push(`4. Call \`get_migration_guide("library-replacements")\` for replacing external libraries`);
430
+ }
431
+ parts.push(`\n## Other Key Guides\n`);
432
+ parts.push(`- \`get_migration_guide("component-renderer-limitations")\` — critical constraints when using COMPONENT_LIST (parent cannot read child props)`);
433
+ parts.push(`- \`get_migration_guide("prop-runtime-shapes")\` — exact runtime shape for each prop type (.data vs .links, etc.)`);
434
+ parts.push(`- \`get_migration_guide("link-prop-decision-guide")\` — when to use LINK vs LIST_OF_LINK vs COMPONENT_LIST`);
435
+ parts.push(`- \`get_framework_guide("common-pitfalls")\` — general gotchas (observer rules, .data access)`);
436
+ parts.push(`- \`get_framework_guide("component-renderer-patterns")\` — full IkasComponentRenderer usage`);
437
+ parts.push(`- \`get_model_guide("<TypeName>")\` — store type definitions (IkasCart, IkasProduct, etc.)`);
438
+ return parts.join("\n");
439
+ }
440
+ function scanSharedSubcomponents(sourceDir) {
441
+ if (!fs.existsSync(sourceDir))
442
+ return [];
443
+ const tsxFiles = [];
444
+ const walk = (dir) => {
445
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
446
+ if (entry.isDirectory()) {
447
+ if (entry.name === "node_modules" || entry.name === "__generated__" || entry.name.startsWith("."))
448
+ continue;
449
+ walk(path.join(dir, entry.name));
450
+ }
451
+ else if (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) {
452
+ const componentDir = path.basename(path.dirname(path.join(dir, entry.name)));
453
+ tsxFiles.push({ componentDir, filePath: path.join(dir, entry.name) });
454
+ }
455
+ }
456
+ };
457
+ try {
458
+ walk(sourceDir);
459
+ }
460
+ catch {
461
+ return [];
462
+ }
463
+ // Collect imports: map from imported path/name → { usingComponents: Set, rawImportPath: string }
464
+ const importUsage = new Map();
465
+ const importRegex = /import\s+(?:{[^}]+}|\w+(?:\s*,\s*{[^}]+})?)\s+from\s+['"]([^'"]+)['"]/g;
466
+ for (const { componentDir, filePath } of tsxFiles) {
467
+ let content;
468
+ try {
469
+ content = fs.readFileSync(filePath, "utf-8");
470
+ }
471
+ catch {
472
+ continue;
473
+ }
474
+ const seenInFile = new Set();
475
+ let match;
476
+ while ((match = importRegex.exec(content)) !== null) {
477
+ const importPath = match[1];
478
+ // Only count relative imports (shared internal components, not npm packages)
479
+ if (!importPath.startsWith("."))
480
+ continue;
481
+ // Skip imports of generated types, utils, hooks
482
+ if (importPath.includes("__generated__") || importPath.includes("/utils") || importPath.includes("/hooks"))
483
+ continue;
484
+ // Extract base name from path
485
+ const pathSegments = importPath.split("/").filter(s => s && s !== "." && s !== "..");
486
+ if (pathSegments.length === 0)
487
+ continue;
488
+ const lastSegment = pathSegments[pathSegments.length - 1];
489
+ if (!lastSegment || !/^[A-Z]/.test(lastSegment))
490
+ continue; // PascalCase only (component-like)
491
+ const key = lastSegment;
492
+ if (seenInFile.has(key))
493
+ continue;
494
+ seenInFile.add(key);
495
+ if (!importUsage.has(key)) {
496
+ importUsage.set(key, { usingComponents: new Set(), rawImportPath: importPath });
497
+ }
498
+ importUsage.get(key).usingComponents.add(componentDir);
499
+ }
500
+ }
501
+ // Filter to candidates used by 3+ distinct components
502
+ const shared = [];
503
+ for (const [name, { usingComponents, rawImportPath }] of importUsage) {
504
+ // Don't flag the component itself (e.g., Navbar imports from ../Navbar/something)
505
+ const users = [...usingComponents].filter(c => c !== name);
506
+ if (users.length >= 3) {
507
+ shared.push({ name, usedBy: users.sort(), importPaths: [rawImportPath] });
508
+ }
509
+ }
510
+ return shared.sort((a, b) => b.usedBy.length - a.usedBy.length);
511
+ }
512
+ // --- Migration plan helpers ---
513
+ function toKebabCase(s) {
514
+ return s
515
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
516
+ .replace(/[\s_]+/g, "-")
517
+ .toLowerCase()
518
+ .replace(/[^a-z0-9-]/g, "");
519
+ }
520
+ function classifyComplexity(comp, customDataMap) {
521
+ const props = comp.props || [];
522
+ const customCount = props.filter(p => p.type === "CUSTOM").length;
523
+ if (customCount === 0 && props.length < 10)
524
+ return "simple";
525
+ // Check for deeply nested CUSTOM (customData referencing another customData)
526
+ let hasDeepNesting = false;
527
+ for (const p of props) {
528
+ if (p.type === "CUSTOM" && p.customDataId) {
529
+ const cd = customDataMap.get(p.customDataId);
530
+ if (cd?.nestedData) {
531
+ const hasNested = (items) => {
532
+ for (const item of items) {
533
+ if (item.type === "DYNAMIC_LIST" || item.type === "STATIC_LIST" || item.customDataId)
534
+ return true;
535
+ if (item.nestedData && hasNested(item.nestedData))
536
+ return true;
537
+ }
538
+ return false;
539
+ };
540
+ if (hasNested(cd.nestedData)) {
541
+ hasDeepNesting = true;
542
+ break;
543
+ }
544
+ }
545
+ }
546
+ }
547
+ if (hasDeepNesting || props.length > 20 || customCount > 3)
548
+ return "complex";
549
+ return "medium";
550
+ }
551
+ function generateMigrationPlan(theme, projectName, oldSourceDir) {
552
+ const components = theme.components || [];
553
+ const customData = theme.customData || [];
554
+ const settings = theme.settings;
555
+ const customDataMap = new Map();
556
+ for (const cd of customData) {
557
+ if (cd.id)
558
+ customDataMap.set(cd.id, cd);
559
+ }
560
+ const sharedSubs = oldSourceDir ? scanSharedSubcomponents(oldSourceDir) : [];
561
+ const parts = [];
562
+ parts.push(`# Theme Migration Plan — \`${projectName}\``);
563
+ parts.push("");
564
+ parts.push(`**Generated:** ${new Date().toISOString().slice(0, 10)}`);
565
+ parts.push(`**Source:** ${components.length} old components, ${customData.filter(cd => cd.isRoot).length} custom data types, ${(theme.pages || []).length} pages`);
566
+ parts.push("");
567
+ parts.push(`> ## READ THIS FIRST`);
568
+ parts.push(`>`);
569
+ parts.push(`> This file is your responsibility from here. The MCP wrote this initial scaffold **once**. You own all updates — no MCP tool will modify this file again.`);
570
+ parts.push(`>`);
571
+ parts.push(`> 1. **Tick checkboxes** as you finish work. Use \`[~]\` for in-progress and \`[x]\` for done. Edit the file directly with your file-editing tools.`);
572
+ parts.push(`> 2. **theme.json is incomplete.** Atomic components (Button, Input, Card, icons, etc.) often live only in \`src/\` and are NOT referenced from theme.json. Before you start section migration, scan the old source directory and ADD entries to this file for anything the initial scan missed — list them under \`## Source Code Analysis\` and add shared ones to \`### Shared Sub-Components\`.`);
573
+ parts.push(`> 3. **Custom data types are NOT pre-converted.** For each customData entry used by a section, decide enum-vs-component when you migrate that section. Log every decision in \`## Custom Data Decisions\` with reasoning. See \`get_migration_guide("custom-data-conversion")\` for the heuristic.`);
574
+ parts.push(`> 4. **Preserve feature parity. Do NOT silently simplify or drop features.** If an old prop has richer fields than a new built-in prop type can carry (e.g. a navigation link with a per-link image), the answer is to build a child component — never to flatten and "add it later." Later doesn't come. If the user explicitly wants a feature removed, log that as an explicit decision in \`## Notes\`; otherwise migrate to functional parity.`);
575
+ parts.push(`> 5. **Per-section work:** call \`get_section_migration_plan({theme_json_path, section_name, project_name: "${projectName}"})\` for each section. The MCP returns concrete CLI commands and flags any customData-referencing props with a "Decide: enum or component?" callout.`);
576
+ parts.push(`> 6. **If you discover new shared sub-components mid-migration**, add them under \`### Shared Sub-Components\` and any notes under \`## Notes\`.`);
577
+ parts.push(`>`);
578
+ parts.push(`> Status legend: \`[ ]\` not started · \`[~]\` in progress · \`[x]\` complete.`);
579
+ parts.push("");
580
+ parts.push(`## Foundation`);
581
+ parts.push("");
582
+ // CSS variables
583
+ if (settings?.colors && settings.colors.length > 0) {
584
+ parts.push(`### Global CSS Variables (→ \`src/global.css\`)`);
585
+ parts.push("");
586
+ for (const c of settings.colors) {
587
+ parts.push(`- [ ] \`${c.key}: ${c.color};\` — ${c.displayName || "color"}`);
588
+ }
589
+ parts.push("");
590
+ }
591
+ // Fonts
592
+ if (settings?.fontFamily?.name) {
593
+ parts.push(`### Fonts (→ \`src/global.css\`)`);
594
+ parts.push("");
595
+ parts.push(`- [ ] ${settings.fontFamily.name} (weights: ${settings.fontFamily.variants?.join(", ") || "default"})`);
596
+ parts.push("");
597
+ }
598
+ // (Custom Enums foundation subsection intentionally removed — see ## Custom Data Types below.
599
+ // Old customData entries are NOT pre-migrated as enums; each is a per-section decision.
600
+ // See get_migration_guide("custom-data-conversion") for the enum-vs-component heuristic.)
601
+ // Shared sub-components
602
+ if (sharedSubs.length > 0) {
603
+ parts.push(`### Shared Sub-Components (→ \`src/sub-components/\`)`);
604
+ parts.push("");
605
+ parts.push(`These are reused across 3+ old sections. Build them BEFORE migrating sections. They are NOT registered in \`ikas.config.json\` — they live in \`src/sub-components/<Name>/{index.tsx,styles.css}\` and are imported by section components.`);
606
+ parts.push("");
607
+ for (const sub of sharedSubs) {
608
+ parts.push(`- [ ] \`${sub.name}\` — used by: ${sub.usedBy.slice(0, 8).join(", ")}${sub.usedBy.length > 8 ? `, +${sub.usedBy.length - 8} more` : ""}`);
609
+ }
610
+ parts.push("");
611
+ }
612
+ else if (oldSourceDir) {
613
+ parts.push(`### Shared Sub-Components`);
614
+ parts.push("");
615
+ parts.push(`No shared sub-components detected (no relative imports reused across 3+ old component files).`);
616
+ parts.push("");
617
+ }
618
+ else {
619
+ parts.push(`### Shared Sub-Components`);
620
+ parts.push("");
621
+ parts.push(`_Old source directory not provided — shared sub-components cannot be detected automatically. If you have access to the old source, call \`plan_migration\` again with \`old_source_dir\` to populate this section._`);
622
+ parts.push("");
623
+ }
624
+ parts.push(`> **This scan is partial.** \`scanSharedSubcomponents\` only detects relative imports used by **3 or more** old components. Atomic components used by fewer sections (often Button/Input/Card/icon primitives) will be missed. Scan \`src/\` yourself and add any others under \`## Source Code Analysis\` below.`);
625
+ parts.push("");
626
+ // Custom Data Types — deferred decisions (reference list, not checkboxes)
627
+ const rootCustomData = customData.filter((cd) => cd.isRoot);
628
+ if (rootCustomData.length > 0) {
629
+ // Build a usage map: which sections reference each customData type
630
+ const usageByCustomDataId = new Map();
631
+ for (const comp of components) {
632
+ const sectionName = comp.displayName || comp.dir || comp.id || "?";
633
+ for (const p of comp.props || []) {
634
+ if (p.type === "CUSTOM" && p.customDataId) {
635
+ const list = usageByCustomDataId.get(p.customDataId) || [];
636
+ if (!list.includes(sectionName))
637
+ list.push(sectionName);
638
+ usageByCustomDataId.set(p.customDataId, list);
639
+ }
640
+ }
641
+ }
642
+ parts.push(`### Custom Data Types — decide during section migration`);
643
+ parts.push("");
644
+ parts.push(`Each entry below is an OLD \`theme.customData[]\` type. Do **not** pre-migrate these as enums. When you migrate the section that uses one, decide:`);
645
+ parts.push(`- **Flat scalar set** (e.g. \`"left" | "right" | "center"\`) → new-system **enum prop** via \`config add-enum\`.`);
646
+ parts.push(`- **Structured record** (e.g. \`{image, link, title}\`, repeated in a list) → new-system **component** via \`config add-component\` + COMPONENT_LIST wiring on the parent.`);
647
+ parts.push("");
648
+ parts.push(`Log every decision in \`## Custom Data Decisions\` below. See \`get_migration_guide("custom-data-conversion")\` for the full heuristic and worked examples (Position, Slide, MenuItem).`);
649
+ parts.push("");
650
+ for (const cd of rootCustomData) {
651
+ const cdName = cd.typescriptName || cd.name || cd.id || "Unknown";
652
+ const cdType = cd.type || "?";
653
+ let shape = "";
654
+ if (cd.type === "ENUM") {
655
+ const opts = (cd.enumOptions || []).map((o) => o.value || o.displayName).filter(Boolean);
656
+ shape = ` — shape: \`enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}\``;
657
+ }
658
+ else if (cd.nestedData && cd.nestedData.length > 0) {
659
+ const first = cd.nestedData[0];
660
+ const fields = (first?.nestedData || cd.nestedData || []).map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`).slice(0, 8);
661
+ shape = ` — shape: \`{${fields.join(", ")}}\``;
662
+ }
663
+ const usedBy = cd.id ? usageByCustomDataId.get(cd.id) || [] : [];
664
+ const usedByStr = usedBy.length > 0 ? ` — used by: ${usedBy.slice(0, 6).join(", ")}${usedBy.length > 6 ? `, +${usedBy.length - 6} more` : ""}` : ` — _not directly referenced by any section's props (may be nested inside another customData)_`;
665
+ parts.push(`- \`${cdName}\` (${cdType})${shape}${usedByStr}`);
666
+ }
667
+ parts.push("");
668
+ }
669
+ // Sections queue
670
+ parts.push(`## Sections`);
671
+ parts.push("");
672
+ parts.push(`Component **names** are listed below. The CLI auto-generates opaque \`componentId\`s when you run \`config add-component\` — capture each id from the JSON response and use it (or look it up later with \`config list\`) when wiring \`filteredComponentIds\`.`);
673
+ parts.push("");
674
+ const buckets = {
675
+ simple: [],
676
+ medium: [],
677
+ complex: [],
678
+ };
679
+ for (const comp of components) {
680
+ const complexity = classifyComplexity(comp, customDataMap);
681
+ buckets[complexity].push({ comp, complexity });
682
+ }
683
+ const labels = {
684
+ simple: "### Simple (migrate first)",
685
+ medium: "### Medium",
686
+ complex: "### Complex (migrate last)",
687
+ };
688
+ for (const key of ["simple", "medium", "complex"]) {
689
+ const items = buckets[key];
690
+ if (items.length === 0)
691
+ continue;
692
+ parts.push(labels[key]);
693
+ parts.push("");
694
+ for (const { comp } of items) {
695
+ const oldName = comp.displayName || comp.dir || comp.id || "Unknown";
696
+ const kebabName = toKebabCase(comp.dir || comp.displayName || comp.id || "unknown");
697
+ const newId = `${projectName}-${kebabName}`;
698
+ const headerFooter = comp.isHeader ? " **[HEADER]**" : comp.isFooter ? " **[FOOTER]**" : "";
699
+ const propCount = (comp.props || []).length;
700
+ // Detect children from CUSTOM DYNAMIC_LIST props
701
+ const children = [];
702
+ for (const p of comp.props || []) {
703
+ if (p.type === "CUSTOM" && p.customDataId) {
704
+ const cd = customDataMap.get(p.customDataId);
705
+ if (cd && (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST")) {
706
+ const itemObj = cd.nestedData?.[0];
707
+ if (itemObj) {
708
+ const childName = itemObj.typescriptName || (itemObj.name ? itemObj.name.replace(/[^a-zA-Z0-9]/g, "") : `${oldName}Item`);
709
+ const fields = (itemObj.nestedData || []).map((f) => f.key || f.name || "?").slice(0, 8);
710
+ children.push({ propName: p.name || "?", childName, fields });
711
+ }
712
+ }
713
+ }
714
+ }
715
+ parts.push(`- [ ] \`${newId}\` — **${oldName}**${headerFooter} (${propCount} props)`);
716
+ parts.push(` - Old dir: \`${comp.dir || "?"}\``);
717
+ if (children.length > 0) {
718
+ parts.push(` - Children:`);
719
+ for (const ch of children) {
720
+ parts.push(` - [ ] \`${ch.childName}\` — for prop \`${ch.propName}\` — fields: ${ch.fields.join(", ")}`);
721
+ }
722
+ }
723
+ }
724
+ parts.push("");
725
+ }
726
+ // Source Code Analysis — placeholder for the LLM to fill in
727
+ parts.push(`## Source Code Analysis`);
728
+ parts.push("");
729
+ parts.push(`> **Before starting section migration**, scan the old \`src/\` for components NOT listed above. theme.json does not see atomic primitives — buttons, inputs, cards, icon wrappers, layout helpers, etc. — that are imported by sections but never appear as theme components. Add them here, then decide which become shared sub-components vs which collapse into section bodies.`);
730
+ parts.push("");
731
+ parts.push(`<!-- Example: \`- Button (src/atoms/Button/) — used by ~all sections → promote to Shared Sub-Component\` -->`);
732
+ parts.push("");
733
+ // Custom Data Decisions — append-only log the LLM fills as it makes per-section decisions
734
+ parts.push(`## Custom Data Decisions`);
735
+ parts.push("");
736
+ parts.push(`> Log each customData enum-vs-component decision here as you encounter it during section migration. Format: \`- \\\`<CustomDataName>\\\` → enum/component \\\`<target name>\\\` (YYYY-MM-DD) — reasoning\`.`);
737
+ parts.push("");
738
+ parts.push(`<!-- Example: \`- SlideData → component \\\`hero-slide\\\` (2026-05-11) — structured record {image,link,title}; LIST_OF_LINK would drop the image\` -->`);
739
+ parts.push("");
740
+ // Known Environmental Issues (agents fill in during work)
741
+ parts.push(`## Known Environmental Issues`);
742
+ parts.push("");
743
+ parts.push(`_Record any non-component build/TS errors here so future sessions don't waste time diagnosing them._`);
744
+ parts.push("");
745
+ parts.push(`- [ ] _(none recorded yet)_`);
746
+ parts.push("");
747
+ // Notes — append-only log for decisions not captured elsewhere
748
+ parts.push(`## Notes`);
749
+ parts.push("");
750
+ parts.push(`_Append a bullet after completing work — library swaps, ad-hoc props, CLI folder-name oddities, user-approved feature drops, etc. Format: \`- [YYYY-MM-DD] <section-id>: <brief note>\`._`);
751
+ parts.push("");
752
+ parts.push(`<!-- Example: \`- [2026-04-15] ${projectName}-footer: swapped react-hot-toast for inline status text; CLI created Footer/ as expected\` -->`);
753
+ parts.push("");
754
+ // Cross-references — keep terse; LLM can call `get_migration_guide("list")` or `get_framework_guide("list")` for more
755
+ parts.push(`## Cross-References`);
756
+ parts.push("");
757
+ parts.push(`- \`get_migration_guide("iterative-workflow")\` — the full per-phase protocol`);
758
+ parts.push(`- \`get_migration_guide("custom-data-conversion")\` — enum-vs-component decisions`);
759
+ parts.push(`- \`get_migration_guide("prop-runtime-shapes")\` — runtime shapes & access patterns`);
760
+ parts.push(`- \`get_framework_guide("common-pitfalls")\` — gotchas & old→new property migrations`);
761
+ parts.push("");
762
+ parts.push(`Call \`get_migration_guide("list")\` or \`get_framework_guide("list")\` for the full topic catalog.`);
763
+ parts.push("");
764
+ return parts.join("\n");
765
+ }
766
+ // Known libraries we detect in old themes and want to flag for replacement
767
+ const KNOWN_LIBRARIES = [
768
+ "swiper", "@headlessui/react", "@heroicons/react", "recharts",
769
+ "react-player", "react-simple-star-rating", "react-slider", "react-compound-slider",
770
+ "react-zoom-pan-pinch", "react-hot-toast", "react-fast-marquee",
771
+ "react-indiana-drag-scroll", "react-simple-typewriter", "react-timer-hook",
772
+ "date-fns", "slugify", "classnames", "clsx", "@react-pdf/renderer",
773
+ ];
774
+ // Heuristic: member-access patterns on old storefront stores/singletons that likely need new-system equivalents
775
+ const OLD_STOREFRONT_CALL_REGEX = /\b(customerStore|cartStore|productStore|categoryStore|orderStore|searchStore|favoritesStore|i18nStore|Router|useStore)\.\w+/g;
776
+ function scanSectionSource(componentDir, propNames) {
777
+ if (!fs.existsSync(componentDir))
778
+ return null;
779
+ const result = {
780
+ sourceFiles: [],
781
+ importedSubComponents: [],
782
+ importedLibraries: [],
783
+ importedUnknownLibraries: [],
784
+ propFieldUsage: {},
785
+ oldStorefrontCalls: [],
786
+ reactPackageUsage: [],
787
+ };
788
+ // Collect all .tsx/.ts files in the component dir
789
+ try {
790
+ for (const entry of fs.readdirSync(componentDir, { withFileTypes: true })) {
791
+ if (entry.isFile() && (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
792
+ result.sourceFiles.push(path.join(componentDir, entry.name));
793
+ }
794
+ }
795
+ }
796
+ catch {
797
+ return null;
798
+ }
799
+ if (result.sourceFiles.length === 0)
800
+ return result;
801
+ const importRegex = /import\s+(?:type\s+)?(?:\{[^}]+\}|\w+(?:\s*,\s*\{[^}]+\})?)\s+from\s+['"]([^'"]+)['"]/g;
802
+ const libSet = new Set();
803
+ const unknownLibSet = new Set();
804
+ const reactSet = new Set();
805
+ const subCompSet = new Map();
806
+ const callSet = new Set();
807
+ // Packages we treat as "framework" and don't flag for replacement (but do note in reactPackageUsage)
808
+ const REACT_PACKAGES = new Set(["react", "react-dom", "next", "next/link", "next/image", "next/router", "next/head", "next/script", "mobx-react-lite", "mobx"]);
809
+ for (const file of result.sourceFiles) {
810
+ let content;
811
+ try {
812
+ content = fs.readFileSync(file, "utf-8");
813
+ }
814
+ catch {
815
+ continue;
816
+ }
817
+ // Parse imports
818
+ let m;
819
+ while ((m = importRegex.exec(content)) !== null) {
820
+ const p = m[1];
821
+ if (p.startsWith(".")) {
822
+ const segs = p.split("/").filter(s => s && s !== "." && s !== "..");
823
+ const last = segs[segs.length - 1];
824
+ if (last && /^[A-Z]/.test(last) && !last.includes("__generated__")) {
825
+ subCompSet.set(last, p);
826
+ }
827
+ }
828
+ else if (!p.startsWith("@ikas/")) {
829
+ // Classify: known library, react-family, or unknown-external
830
+ const base = p.startsWith("@") ? p.split("/").slice(0, 2).join("/") : p.split("/")[0];
831
+ if (REACT_PACKAGES.has(p) || REACT_PACKAGES.has(base)) {
832
+ reactSet.add(base);
833
+ }
834
+ else {
835
+ let matched = false;
836
+ for (const lib of KNOWN_LIBRARIES) {
837
+ if (p === lib || p.startsWith(lib + "/")) {
838
+ libSet.add(lib);
839
+ matched = true;
840
+ break;
841
+ }
842
+ }
843
+ if (!matched) {
844
+ unknownLibSet.add(base);
845
+ }
846
+ }
847
+ }
848
+ }
849
+ // Generic: detect any member call on old storefront stores/singletons
850
+ let cm;
851
+ OLD_STOREFRONT_CALL_REGEX.lastIndex = 0;
852
+ while ((cm = OLD_STOREFRONT_CALL_REGEX.exec(content)) !== null) {
853
+ callSet.add(cm[0]);
854
+ }
855
+ // Detect field access for each prop
856
+ for (const propName of propNames) {
857
+ if (!propName)
858
+ continue;
859
+ // Look for patterns like: propName.map((item) => ... item.FIELD ...)
860
+ // or: propName[0].FIELD, propName.FIELD, destructure: const { field } = propName
861
+ const mapRegex = new RegExp(`\\b${propName}(?:\\?)?\\.(?:map|forEach|filter|find|reduce)\\s*\\(\\s*\\(?(\\w+)`, "g");
862
+ let mm;
863
+ const itemVars = new Set();
864
+ while ((mm = mapRegex.exec(content)) !== null) {
865
+ itemVars.add(mm[1]);
866
+ }
867
+ // Also detect destructuring in map callback: .map(({ title, image }) => ...)
868
+ const destructRegex = new RegExp(`\\b${propName}(?:\\?)?\\.(?:map|forEach|filter|find|reduce)\\s*\\(\\s*\\(?\\s*\\{([^}]+)\\}`, "g");
869
+ const fields = new Set();
870
+ while ((mm = destructRegex.exec(content)) !== null) {
871
+ for (const f of mm[1].split(",")) {
872
+ const name = f.trim().split(/[:=]/)[0].trim();
873
+ if (name && /^\w+$/.test(name))
874
+ fields.add(name);
875
+ }
876
+ }
877
+ // Scan for itemVar.fieldName access
878
+ for (const itemVar of itemVars) {
879
+ const fieldRegex = new RegExp(`\\b${itemVar}(?:\\?)?\\.(\\w+)`, "g");
880
+ while ((mm = fieldRegex.exec(content)) !== null) {
881
+ const f = mm[1];
882
+ // Exclude TS/JS keywords and built-ins
883
+ if (!/^(map|forEach|filter|find|reduce|length|toString|valueOf|constructor|prototype|then|catch|finally)$/.test(f)) {
884
+ fields.add(f);
885
+ }
886
+ }
887
+ }
888
+ if (fields.size > 0) {
889
+ result.propFieldUsage[propName] = [...fields].sort();
890
+ }
891
+ }
892
+ }
893
+ result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({ name, path: p }));
894
+ result.importedLibraries = [...libSet].sort();
895
+ result.importedUnknownLibraries = [...unknownLibSet].sort();
896
+ result.reactPackageUsage = [...reactSet].sort();
897
+ result.oldStorefrontCalls = [...callSet].sort();
898
+ return result;
899
+ }
900
+ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSourceDir) {
901
+ const components = theme.components || [];
902
+ const customData = theme.customData || [];
903
+ const customDataMap = new Map();
904
+ for (const cd of customData) {
905
+ if (cd.id)
906
+ customDataMap.set(cd.id, cd);
907
+ }
908
+ // Find the component — try match by dir, displayName, id, or new-id
909
+ const target = components.find(c => {
910
+ if (!c)
911
+ return false;
912
+ if (c.dir === sectionName || c.displayName === sectionName || c.id === sectionName)
913
+ return true;
914
+ const kebab = toKebabCase(c.dir || c.displayName || c.id || "");
915
+ const newId = `${projectName}-${kebab}`;
916
+ return newId === sectionName;
917
+ });
918
+ if (!target) {
919
+ const available = components.map(c => c.dir || c.displayName || c.id).filter(Boolean).join(", ");
920
+ return `Section "${sectionName}" not found in theme. Available: ${available}`;
921
+ }
922
+ const parts = [];
923
+ const oldName = target.displayName || target.dir || target.id || "Unknown";
924
+ const kebabName = toKebabCase(target.dir || target.displayName || target.id || "unknown");
925
+ const sectionId = `${projectName}-${kebabName}`;
926
+ const sectionPascal = (target.dir || target.displayName || "").replace(/[^a-zA-Z0-9]/g, "") || kebabName.split("-").map(s => s[0]?.toUpperCase() + s.slice(1)).join("");
927
+ // Scan the old source for imports, libraries, field usage
928
+ const propNames = (target.props || []).map(p => p.name || "").filter(Boolean);
929
+ const sourceScan = (oldSourceDir && target.dir)
930
+ ? scanSectionSource(path.join(oldSourceDir, target.dir), propNames)
931
+ : null;
932
+ parts.push(`# Section Migration Plan: ${oldName}`);
933
+ parts.push("");
934
+ parts.push(`**Old name:** \`${oldName}\` (dir: \`${target.dir || "?"}\`)`);
935
+ parts.push(`**New section name:** \`${sectionPascal}\` (the CLI will assign an opaque \`componentId\` at write-time — capture it from \`config add-component\`'s JSON response)`);
936
+ parts.push(`**Migration checkbox label:** \`${sectionId}\` (tracking identifier in \`MIGRATION.md\` only — not the runtime component id)`);
937
+ if (target.isHeader)
938
+ parts.push(`**Flags:** HEADER (\`isHeader: true\`)`);
939
+ if (target.isFooter)
940
+ parts.push(`**Flags:** FOOTER (\`isFooter: true\`)`);
941
+ parts.push("");
942
+ parts.push(`> **CLI casing note:** \`config add-component --name "${sectionPascal}"\` will create the folder as \`src/components/${sectionPascal}/\`. If you pass all-caps names (e.g. \`FAQ\`), the CLI PascalCases them (\`Faq/\`). Use the folder name the CLI actually creates in your imports.`);
943
+ parts.push("");
944
+ // Source files
945
+ parts.push(`## 1. Old Source Files to Read`);
946
+ parts.push("");
947
+ if (sourceScan && sourceScan.sourceFiles.length > 0) {
948
+ parts.push(`Read these files from the old project (detected by scanning the directory):`);
949
+ for (const f of sourceScan.sourceFiles) {
950
+ parts.push(`- \`${f}\``);
951
+ }
952
+ if (sourceScan.importedSubComponents.length > 0) {
953
+ parts.push("");
954
+ parts.push(`**Imported sub-components** (relative PascalCase imports found in the source — read these too if you need their behavior):`);
955
+ for (const sub of sourceScan.importedSubComponents) {
956
+ parts.push(`- \`${sub.name}\` (import path: \`${sub.path}\`)`);
957
+ }
958
+ }
959
+ if (sourceScan.importedLibraries.length > 0) {
960
+ parts.push("");
961
+ parts.push(`**External libraries (recognized) — MUST be replaced with vanilla Preact + CSS:**`);
962
+ for (const lib of sourceScan.importedLibraries) {
963
+ parts.push(`- \`${lib}\` — see \`get_migration_guide("library-replacements")\``);
964
+ }
965
+ }
966
+ if (sourceScan.importedUnknownLibraries.length > 0) {
967
+ parts.push("");
968
+ parts.push(`**External libraries (unrecognized) — verify whether a replacement is needed:**`);
969
+ for (const lib of sourceScan.importedUnknownLibraries) {
970
+ parts.push(`- \`${lib}\` — this library is not in our known-replacement list. Check if the new system provides equivalent functionality via \`search_docs\` / \`list_functions\`. If not, implement in vanilla Preact. See \`get_migration_guide("finding-new-system-equivalents")\`.`);
971
+ }
972
+ }
973
+ if (sourceScan.reactPackageUsage.length > 0) {
974
+ parts.push("");
975
+ parts.push(`**React/Next.js framework imports detected:** ${sourceScan.reactPackageUsage.map(p => `\`${p}\``).join(", ")}. These do NOT carry over — the new system is Preact. See \`get_migration_guide("react-to-preact")\` for conversion patterns (hooks, observer, event types, routing).`);
976
+ }
977
+ if (sourceScan.oldStorefrontCalls.length > 0) {
978
+ parts.push("");
979
+ parts.push(`**Old-system API calls detected — search for new-system equivalents:**`);
980
+ for (const call of sourceScan.oldStorefrontCalls) {
981
+ parts.push(`- \`${call}\``);
982
+ }
983
+ parts.push("");
984
+ parts.push(`Each of these is a call on an old \`@ikas/storefront\` store or singleton. The new system (\`@ikas/bp-storefront\`) has different APIs — never assume the same method exists. Follow the discovery protocol in \`get_migration_guide("finding-new-system-equivalents")\`:`);
985
+ parts.push(`1. Identify the intent (e.g. "login", "add to cart", "newsletter signup", "navigate")`);
986
+ parts.push(`2. \`search_docs("<intent keywords>")\` to find candidates`);
987
+ parts.push(`3. \`list_functions("<category>")\` (Form, Cart, Customer, Navigation, ...) to see all options`);
988
+ parts.push(`4. \`get_function_doc("<name>")\` / \`get_model_guide("<Type>")\` for exact signatures`);
989
+ parts.push(`5. \`get_code_example("<task>")\` for working examples`);
990
+ }
991
+ }
992
+ else if (oldSourceDir && target.dir) {
993
+ const compDir = path.join(oldSourceDir, target.dir);
994
+ parts.push(`Read these files from the old project (if they exist):`);
995
+ parts.push(`- \`${compDir}/index.tsx\` — main component implementation`);
996
+ parts.push(`- \`${compDir}/*.tsx\` — any nested sub-components`);
997
+ parts.push(`- \`${compDir}/*.css\` / \`*.scss\` — styles`);
998
+ }
999
+ else {
1000
+ parts.push(`Read the old component at \`src/components/${target.dir || oldName}/index.tsx\` and any files in that directory.`);
1001
+ }
1002
+ parts.push("");
1003
+ // Prop conversion table
1004
+ parts.push(`## 2. Prop Conversion Table`);
1005
+ parts.push("");
1006
+ parts.push(`| Old prop | Old type | → | New prop | New type | Notes |`);
1007
+ parts.push(`|----------|----------|---|----------|----------|-------|`);
1008
+ const children = [];
1009
+ const enumsNeeded = [];
1010
+ const librariesDetected = new Set();
1011
+ const parentPropsJson = [];
1012
+ for (const p of target.props || []) {
1013
+ const oldName = p.name || "?";
1014
+ const oldType = p.type || "?";
1015
+ let newName = oldName;
1016
+ let newType = oldType;
1017
+ let notes = "Direct";
1018
+ if (oldType === "SLIDER") {
1019
+ newType = "NUMBER";
1020
+ notes = `Was SLIDER(min=${p.sliderData?.min}, max=${p.sliderData?.max}) — replace \`.value\` access with direct number`;
1021
+ const prop = { name: newName, displayName: p.displayName || newName, type: "NUMBER" };
1022
+ if (p.isRequired)
1023
+ prop.required = true;
1024
+ parentPropsJson.push(prop);
1025
+ }
1026
+ else if (oldType === "PRODUCT_DETAIL") {
1027
+ newType = "PRODUCT";
1028
+ notes = "Renamed — PRODUCT_DETAIL → PRODUCT";
1029
+ const prop = { name: newName, displayName: p.displayName || newName, type: "PRODUCT" };
1030
+ if (p.isRequired)
1031
+ prop.required = true;
1032
+ parentPropsJson.push(prop);
1033
+ }
1034
+ else if (oldType === "CUSTOM" && p.customDataId) {
1035
+ const cd = customDataMap.get(p.customDataId);
1036
+ if (cd) {
1037
+ if (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST") {
1038
+ // Child component needed
1039
+ const itemObj = cd.nestedData?.[0];
1040
+ const childName = itemObj?.typescriptName || (itemObj?.name ? itemObj.name.replace(/[^a-zA-Z0-9]/g, "") : `${sectionPascal}Item`);
1041
+ const childProps = [];
1042
+ const nestedWarnings = [];
1043
+ for (const f of (itemObj?.nestedData || [])) {
1044
+ if (!f.key)
1045
+ continue;
1046
+ let fType = f.type;
1047
+ if (fType === "SLIDER")
1048
+ fType = "NUMBER";
1049
+ else if (fType === "PRODUCT_DETAIL")
1050
+ fType = "PRODUCT";
1051
+ else if (fType === "CUSTOM" || fType === "DYNAMIC_LIST" || fType === "STATIC_LIST" || fType === "OBJECT") {
1052
+ nestedWarnings.push(`\`${f.key}\` (${fType})`);
1053
+ fType = "COMPONENT_LIST";
1054
+ }
1055
+ const prop = { name: f.key, displayName: f.name || f.key, type: fType };
1056
+ if (f.isRequired)
1057
+ prop.required = true;
1058
+ childProps.push(prop);
1059
+ }
1060
+ if (nestedWarnings.length > 0) {
1061
+ notes = `⚠️ Child has nested structures (${nestedWarnings.join(", ")}) — these need their OWN child components. After creating this child, decompose each nested structure recursively.`;
1062
+ }
1063
+ // If customData is empty/incomplete, try to infer fields from source usage
1064
+ if (childProps.length === 0 && sourceScan?.propFieldUsage[oldName]) {
1065
+ const inferred = sourceScan.propFieldUsage[oldName];
1066
+ for (const fieldName of inferred) {
1067
+ childProps.push({ name: fieldName, displayName: fieldName, type: "TEXT" });
1068
+ }
1069
+ if (inferred.length > 0) {
1070
+ notes = `Was CUSTOM → ${cd.type} with empty customData. **Inferred props from source usage:** ${inferred.map(f => `\`${f}\``).join(", ")}. Verify types and required flags — default inferred type is TEXT.`;
1071
+ }
1072
+ }
1073
+ children.push({
1074
+ propName: oldName,
1075
+ childName,
1076
+ childPropsJson: childProps,
1077
+ customDataName: cd.name || "unnamed",
1078
+ });
1079
+ newType = "COMPONENT_LIST";
1080
+ notes = `Was CUSTOM → ${cd.type} of ${itemObj?.typescriptName || "object"}. Create child component \`${childName}\`; the CLI returns the opaque \`componentId\` in its JSON response — substitute it for the \`<id-of-${childName}>\` placeholder below.`;
1081
+ const prop = {
1082
+ name: newName,
1083
+ displayName: p.displayName || newName,
1084
+ type: "COMPONENT_LIST",
1085
+ filteredComponentIds: [`<id-of-${childName}>`],
1086
+ };
1087
+ parentPropsJson.push(prop);
1088
+ }
1089
+ else if (cd.type === "OBJECT") {
1090
+ const fieldCount = (cd.nestedData || []).length;
1091
+ if (fieldCount <= 3) {
1092
+ notes = `Was CUSTOM (OBJECT, ${fieldCount} fields) — **flatten into direct props** (${(cd.nestedData || []).map((f) => f.key || f.name).join(", ")})`;
1093
+ for (const f of (cd.nestedData || [])) {
1094
+ if (!f.key)
1095
+ continue;
1096
+ let fType = f.type;
1097
+ if (fType === "SLIDER")
1098
+ fType = "NUMBER";
1099
+ const prop = { name: f.key, displayName: f.name || f.key, type: fType };
1100
+ if (f.isRequired)
1101
+ prop.required = true;
1102
+ parentPropsJson.push(prop);
1103
+ }
1104
+ newType = "(flattened)";
1105
+ newName = (cd.nestedData || []).map((f) => f.key).join(", ");
1106
+ }
1107
+ else {
1108
+ newType = "COMPONENT_LIST";
1109
+ notes = `Was CUSTOM (OBJECT, ${fieldCount} fields) — too complex to flatten, use COMPONENT_LIST with single child`;
1110
+ }
1111
+ }
1112
+ else if (cd.type === "ENUM") {
1113
+ newType = "ENUM";
1114
+ const enumName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
1115
+ const options = (cd.enumOptions || []).reduce((acc, o) => {
1116
+ if (o.displayName && o.value)
1117
+ acc[o.displayName] = o.value;
1118
+ return acc;
1119
+ }, {});
1120
+ enumsNeeded.push({ name: enumName, options });
1121
+ notes = `Was CUSTOM (ENUM) — create enum \`${enumName}\` via \`config add-enum\` first, then reference its enumId here`;
1122
+ const prop = { name: newName, displayName: p.displayName || newName, type: "ENUM", enumTypeId: `<ENUM_ID_FROM_add-enum_${enumName}>` };
1123
+ if (p.isRequired)
1124
+ prop.required = true;
1125
+ parentPropsJson.push(prop);
1126
+ }
1127
+ }
1128
+ }
1129
+ else {
1130
+ // Direct mapping
1131
+ const prop = { name: newName, displayName: p.displayName || newName, type: newType };
1132
+ if (p.isRequired)
1133
+ prop.required = true;
1134
+ parentPropsJson.push(prop);
1135
+ }
1136
+ parts.push(`| \`${oldName}\` | ${oldType} | → | \`${newName}\` | ${newType} | ${notes} |`);
1137
+ }
1138
+ parts.push("");
1139
+ // Custom Data Decision Callouts — per prop referencing a customData type
1140
+ const customDataPropsForCallouts = (target.props || []).filter((p) => p.type === "CUSTOM" && p.customDataId && customDataMap.has(p.customDataId));
1141
+ if (customDataPropsForCallouts.length > 0) {
1142
+ parts.push(`## Custom Data Decisions to Make`);
1143
+ parts.push("");
1144
+ parts.push(`Each prop below references an old \`customData\` type. Verify the MCP's default against the actual data semantics, then run the CLI command. **Log every decision in MIGRATION.md → \`## Custom Data Decisions\`** with reasoning. See \`get_migration_guide("custom-data-conversion")\` for the heuristic and worked examples.`);
1145
+ parts.push("");
1146
+ for (const p of customDataPropsForCallouts) {
1147
+ const cd = customDataMap.get(p.customDataId);
1148
+ if (!cd)
1149
+ continue;
1150
+ const cdName = cd.typescriptName || cd.name || cd.id || "Unknown";
1151
+ const cdType = cd.type || "?";
1152
+ let shape = "";
1153
+ let shapeKind = "unknown";
1154
+ if (cd.type === "ENUM") {
1155
+ const opts = (cd.enumOptions || []).map((o) => o.value || o.displayName).filter(Boolean);
1156
+ shape = `enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}`;
1157
+ shapeKind = "enum";
1158
+ }
1159
+ else if (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST") {
1160
+ const first = cd.nestedData?.[0];
1161
+ const fields = (first?.nestedData || []).map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`);
1162
+ shape = `${cdType} of {${fields.slice(0, 6).join(", ")}}`;
1163
+ shapeKind = "list";
1164
+ }
1165
+ else if (cd.type === "OBJECT") {
1166
+ const fields = (cd.nestedData || []).map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`);
1167
+ shape = `OBJECT {${fields.slice(0, 6).join(", ")}}`;
1168
+ shapeKind = "record";
1169
+ }
1170
+ else {
1171
+ shape = cdType;
1172
+ }
1173
+ // Build the field source + names list once so we can use it for the lost-fields enumeration
1174
+ // AND for the component-path CLI template.
1175
+ const fieldSource = cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST"
1176
+ ? cd.nestedData?.[0]?.nestedData || []
1177
+ : cd.nestedData || [];
1178
+ const fieldDescriptions = fieldSource
1179
+ .filter((f) => f.key)
1180
+ .map((f) => `\`${f.key}\` (${f.type || "?"})`);
1181
+ parts.push(`### Prop \`${p.name || "?"}\` → customData \`${cdName}\``);
1182
+ parts.push("");
1183
+ parts.push(`**Shape:** \`${shape}\``);
1184
+ parts.push("");
1185
+ if (shapeKind === "enum") {
1186
+ parts.push(`**Default: enum prop.** Flat scalar set; use \`config add-enum\`.`);
1187
+ parts.push("");
1188
+ const enumName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
1189
+ const enumOptions = (cd.enumOptions || []).reduce((acc, o) => {
1190
+ if (o.displayName && o.value)
1191
+ acc[o.displayName] = o.value;
1192
+ return acc;
1193
+ }, {});
1194
+ parts.push("```bash");
1195
+ parts.push(`npx ikas-component config add-enum --name "${enumName}" --options '${JSON.stringify(enumOptions)}'`);
1196
+ parts.push("```");
1197
+ parts.push("");
1198
+ parts.push(`If you believe this should be a component instead (e.g. each option secretly carries richer data not visible in the customData shape), see \`get_migration_guide("custom-data-conversion")\`.`);
1199
+ parts.push("");
1200
+ }
1201
+ else if (shapeKind === "list" || shapeKind === "record") {
1202
+ const fieldCount = fieldDescriptions.length;
1203
+ const isMinimal = fieldCount > 0 && fieldCount <= 2;
1204
+ if (isMinimal) {
1205
+ parts.push(`⚠️ **This child would have only ${fieldCount} field${fieldCount === 1 ? "" : "s"}** (${fieldDescriptions.join(", ")}). \`COMPONENT_LIST\` is usually overkill at this size. **Prefer one of:**`);
1206
+ parts.push(`- repeated scalar props on the parent (\`title1\`/\`link1\`, \`title2\`/\`link2\`, …) for a small fixed count`);
1207
+ parts.push(`- a domain LIST prop type (\`LIST_OF_LINK\`, \`IMAGE_LIST\`, \`PRODUCT_LIST\`, …) when each item IS one domain object`);
1208
+ parts.push(`- \`COMPONENT_LIST\` (CLI command below) only if reordering in the editor is a real UX win`);
1209
+ parts.push("");
1210
+ parts.push(`See \`get_migration_guide("component-composition-decision-guide")\` for the full tree. Log your choice in MIGRATION.md → \`## Custom Data Decisions\`.`);
1211
+ parts.push("");
1212
+ }
1213
+ else {
1214
+ parts.push(`**Default: component + COMPONENT_LIST.** Multiple fields per item — a single enum value cannot carry this structure.`);
1215
+ parts.push("");
1216
+ if (fieldCount > 0) {
1217
+ parts.push(`⚠️ **Fields you would lose if you flatten this:** ${fieldDescriptions.join(", ")}. Flattening to a simpler prop type drops these from the editor UI permanently. **Do not "simplify for later"** — if the feature genuinely isn't wanted, log that explicitly in MIGRATION.md → \`## Notes\` with reasoning. Otherwise build the component.`);
1218
+ parts.push("");
1219
+ }
1220
+ parts.push(`> See \`get_migration_guide("component-composition-decision-guide")\` for when \`COMPONENT_LIST\` is overkill.`);
1221
+ parts.push("");
1222
+ }
1223
+ const compName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : `${sectionPascal}Item`);
1224
+ const compPropsForCli = [];
1225
+ for (const f of fieldSource) {
1226
+ if (!f.key)
1227
+ continue;
1228
+ let fType = f.type;
1229
+ if (fType === "SLIDER")
1230
+ fType = "NUMBER";
1231
+ else if (fType === "PRODUCT_DETAIL")
1232
+ fType = "PRODUCT";
1233
+ compPropsForCli.push({ name: f.key, displayName: f.name || f.key, type: fType });
1234
+ }
1235
+ parts.push("```bash");
1236
+ if (isMinimal) {
1237
+ parts.push(`# Fallback: COMPONENT_LIST (use only if the simpler alternatives above don't fit)`);
1238
+ }
1239
+ parts.push(`npx ikas-component config add-component --name "${compName}" --type component --props '${JSON.stringify(compPropsForCli)}'`);
1240
+ parts.push(`# Then on the parent, set the prop's filteredComponentIds to the new component's id.`);
1241
+ parts.push("```");
1242
+ parts.push("");
1243
+ parts.push(`If you believe this should be an enum despite the structure, see \`get_migration_guide("custom-data-conversion")\`.`);
1244
+ parts.push("");
1245
+ }
1246
+ else {
1247
+ parts.push(`**Unable to classify automatically — you decide.** See \`get_migration_guide("custom-data-conversion")\` for the enum-vs-component heuristic.`);
1248
+ parts.push("");
1249
+ }
1250
+ }
1251
+ }
1252
+ // Enums to create first
1253
+ if (enumsNeeded.length > 0) {
1254
+ parts.push(`## 3. Create Enums FIRST (if not already done)`);
1255
+ parts.push("");
1256
+ for (const e of enumsNeeded) {
1257
+ parts.push(`\`\`\`bash`);
1258
+ parts.push(`npx ikas-component config add-enum --name "${e.name}" --options '${JSON.stringify(e.options)}'`);
1259
+ parts.push(`\`\`\``);
1260
+ }
1261
+ parts.push(`Save the returned \`enumId\` values and substitute them in the parent config JSON below.`);
1262
+ parts.push("");
1263
+ }
1264
+ // Child CLI commands — dedupe by childName (same shape may be referenced by multiple parent props)
1265
+ const uniqueChildren = new Map();
1266
+ for (const ch of children) {
1267
+ const existing = uniqueChildren.get(ch.childName);
1268
+ if (existing) {
1269
+ existing.usedByProps.push(ch.propName);
1270
+ }
1271
+ else {
1272
+ uniqueChildren.set(ch.childName, { child: ch, usedByProps: [ch.propName] });
1273
+ }
1274
+ }
1275
+ if (uniqueChildren.size > 0) {
1276
+ parts.push(`## ${enumsNeeded.length > 0 ? "4" : "3"}. Create Child Components FIRST`);
1277
+ parts.push("");
1278
+ parts.push(`These children are referenced by the parent's COMPONENT_LIST props. Create them before the parent.`);
1279
+ parts.push(`**Deduped:** run each \`add-component\` command ONCE even if multiple parent props reference the same child.`);
1280
+ parts.push(`**Capture each \`componentId\` from the CLI's JSON response** — you'll substitute these ids for the \`<id-of-{ChildName}>\` placeholders in the parent's \`filteredComponentIds\` below.`);
1281
+ parts.push("");
1282
+ for (const { child: ch, usedByProps } of uniqueChildren.values()) {
1283
+ parts.push(`### \`${ch.childName}\``);
1284
+ const propsLabel = usedByProps.length > 1
1285
+ ? `Used by parent props: ${usedByProps.map(p => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
1286
+ : `For parent prop: \`${usedByProps[0]}\``;
1287
+ parts.push(propsLabel);
1288
+ parts.push(`Old customData: "${ch.customDataName}"`);
1289
+ parts.push("");
1290
+ parts.push(`\`\`\`bash`);
1291
+ parts.push(`npx ikas-component config add-component --name "${ch.childName}" --type component --props '${JSON.stringify(ch.childPropsJson)}'`);
1292
+ parts.push(`# → { "success": true, "componentId": "<capture-this>", ... }`);
1293
+ parts.push(`\`\`\``);
1294
+ parts.push("");
1295
+ }
1296
+ }
1297
+ // Parent CLI
1298
+ const parentStep = 3 + (enumsNeeded.length > 0 ? 1 : 0) + (children.length > 0 ? 1 : 0);
1299
+ parts.push(`## ${parentStep}. Create Parent Section`);
1300
+ parts.push("");
1301
+ const isHeaderFlag = target.isHeader ? " --isHeader" : "";
1302
+ const isFooterFlag = target.isFooter ? " --isFooter" : "";
1303
+ parts.push(`\`\`\`bash`);
1304
+ parts.push(`npx ikas-component config add-component --name "${sectionPascal}" --type section${isHeaderFlag}${isFooterFlag} --props '${JSON.stringify(parentPropsJson)}'`);
1305
+ parts.push(`\`\`\``);
1306
+ parts.push("");
1307
+ parts.push(`**IMPORTANT:** The CLI auto-generates \`types.ts\`. DO NOT manually create or edit \`types.ts\`.`);
1308
+ if (children.length > 0) {
1309
+ parts.push("");
1310
+ parts.push(`**Before running the command above:** replace each \`<id-of-{ChildName}>\` placeholder in the \`--props\` JSON with the real \`componentId\` captured from the corresponding child's \`add-component\` response (or from \`npx ikas-component config list\`). Component ids are opaque random strings — the CLI will reject any unknown id with a structured error.`);
1311
+ }
1312
+ parts.push("");
1313
+ // Implementation guidance
1314
+ const nextStep = parentStep + 1;
1315
+ parts.push(`## ${nextStep}. Write \`index.tsx\` and \`styles.css\``);
1316
+ parts.push("");
1317
+ parts.push(`After running the CLI commands, write the section's \`index.tsx\`. Key rules:`);
1318
+ parts.push("");
1319
+ parts.push(`- **BOTH named + default export** (the CLI barrel file uses named imports — without both, build fails):`);
1320
+ parts.push(` \`\`\`export function ${sectionPascal}(props: Props) { ... } export default ${sectionPascal};\`\`\``);
1321
+ parts.push(`- No \`observer()\` on root sections — only on sub-components`);
1322
+ parts.push(`- Import from \`@ikas/bp-storefront\` (NOT \`@ikas/storefront\`)`);
1323
+ parts.push(`- Use \`getDefaultSrc(image)\` + native \`<img>\` instead of the old \`<Image>\` component`);
1324
+ parts.push(`- Replace \`IkasSlider\` usage (\`.value\` access) with plain \`number\``);
1325
+ if (children.length > 0) {
1326
+ parts.push(`- For COMPONENT_LIST props, use \`<IkasComponentRenderer id="..." components={list as any[]} parentProps={props} />\``);
1327
+ parts.push(`- **Remember: the parent cannot read child prop values.** Any per-item logic must live in the child component.`);
1328
+ parts.push(`- **Every COMPONENT_LIST slot needs a child component to render individual items.** A product list needs a ProductCard child, a blog list needs a BlogCard child, etc. Check if the child already exists (run \`config list\` to see all components and their opaque ids; reuse the existing id in \`filteredComponentIds\`). If not, create it as a registered component and use the \`componentId\` from the CLI's response.`);
1329
+ }
1330
+ // Check if the section itself has data-driven list props (PRODUCT_LIST, BLOG_LIST, CATEGORY_LIST)
1331
+ const dataListProps = (target.props || []).filter(p => p.type === "PRODUCT_LIST" || p.type === "BLOG_LIST" || p.type === "CATEGORY_LIST" || p.type === "BRAND_LIST");
1332
+ if (dataListProps.length > 0) {
1333
+ parts.push("");
1334
+ parts.push(`### Data-Driven List Rendering`);
1335
+ parts.push("");
1336
+ parts.push(`This section has data-driven list props: ${dataListProps.map(p => `\`${p.name}\` (${p.type})`).join(", ")}.`);
1337
+ parts.push(`These are NOT COMPONENT_LIST — the data comes from dynamic queries (filters, categories, search), not hand-picked items. Render items **internally** by mapping over the data:`);
1338
+ parts.push("");
1339
+ parts.push("```tsx");
1340
+ for (const p of dataListProps) {
1341
+ if (p.type === "PRODUCT_LIST") {
1342
+ parts.push(`// ${p.name}: PRODUCT_LIST — map over .data to render each product`);
1343
+ parts.push(`{${p.name}?.data?.map((product, i) => (`);
1344
+ parts.push(` <ProductCard key={i} product={product} />`);
1345
+ parts.push(`))}`);
1346
+ }
1347
+ else if (p.type === "BLOG_LIST") {
1348
+ parts.push(`// ${p.name}: BLOG_LIST — map over .data to render each blog post`);
1349
+ parts.push(`{${p.name}?.data?.map((blog, i) => (`);
1350
+ parts.push(` <BlogCard key={i} blog={blog} />`);
1351
+ parts.push(`))}`);
1352
+ }
1353
+ else {
1354
+ parts.push(`// ${p.name}: ${p.type} — check shape with get_model_guide, then map over items`);
1355
+ }
1356
+ }
1357
+ parts.push("```");
1358
+ parts.push("");
1359
+ parts.push(`Use a **registered component** (e.g., ProductCard, BlogCard) to render each item. The same component can be reused in both data-driven sections (imported directly) AND COMPONENT_LIST slots (placed by the store owner). Check if it already exists — if so, import it from \`../ProductCard\`. If not, create it as a registered component.`);
1360
+ parts.push(`See \`get_migration_guide("custom-data-conversion")\` → "Two Ways to Render Lists" for the full pattern.`);
1361
+ }
1362
+ // Detect form-page sections (0 or few props, name suggests a form/auth page)
1363
+ const formKeywords = ["login", "register", "forgot", "recover", "password", "account", "email", "verification", "activate", "contact", "checkout", "address"];
1364
+ const lowerDir = (target.dir || "").toLowerCase();
1365
+ const isLikelyFormPage = formKeywords.some(kw => lowerDir.includes(kw));
1366
+ if (isLikelyFormPage) {
1367
+ parts.push("");
1368
+ parts.push(`### Form Page Pattern`);
1369
+ parts.push("");
1370
+ parts.push(`This section looks like a **form/auth page**. The old system typically used imperative \`customerStore\` methods. The new system uses a form-model pattern.`);
1371
+ parts.push(`- Call \`search_docs("<relevant keyword>")\` to find the new-system form functions (e.g., \`search_docs("forgot password")\`, \`search_docs("login")\`, \`search_docs("register")\`)`);
1372
+ parts.push(`- Call \`get_framework_guide("form-handling")\` for the general form-model pattern`);
1373
+ parts.push(`- Call \`get_code_example("<section-type>")\` if available (e.g., \`get_code_example("login-section")\`, \`get_code_example("register-section")\`, \`get_code_example("forgot-password-section")\`)`);
1374
+ parts.push(`- Typical pattern: \`get<Feature>Form()\` → \`set<Feature>Form<Field>(form, value)\` → \`submit<Feature>Form(form)\``);
1375
+ parts.push(`- Replace \`react-hot-toast\` feedback with inline status messages using TEXT props`);
1376
+ }
1377
+ parts.push("");
1378
+ // Library detection — prefer source scan, fall back to heuristic
1379
+ if (sourceScan && sourceScan.importedLibraries.length > 0) {
1380
+ parts.push(`### Library Replacements Required (detected in source)`);
1381
+ parts.push("");
1382
+ parts.push(`These libraries are confirmed imports in the old source. Each MUST be replaced with vanilla Preact + CSS:`);
1383
+ for (const lib of sourceScan.importedLibraries) {
1384
+ parts.push(`- \`${lib}\``);
1385
+ }
1386
+ parts.push("");
1387
+ parts.push(`Call \`get_migration_guide("library-replacements")\` for the specific replacement pattern for each.`);
1388
+ parts.push("");
1389
+ }
1390
+ else if (!sourceScan) {
1391
+ // Fallback heuristic only when source scan unavailable
1392
+ const heuristicLibs = [];
1393
+ const lowerName = oldName.toLowerCase();
1394
+ if (lowerName.includes("slider") || lowerName.includes("carousel") || lowerName.includes("banner")) {
1395
+ heuristicLibs.push("swiper");
1396
+ }
1397
+ if (lowerName.includes("marquee"))
1398
+ heuristicLibs.push("react-fast-marquee");
1399
+ if (lowerName.includes("video") || lowerName.includes("player"))
1400
+ heuristicLibs.push("react-player");
1401
+ if (lowerName.includes("chart"))
1402
+ heuristicLibs.push("recharts");
1403
+ if (lowerName.includes("star") || lowerName.includes("rating") || lowerName.includes("review"))
1404
+ heuristicLibs.push("react-simple-star-rating");
1405
+ if (heuristicLibs.length > 0) {
1406
+ parts.push(`### Likely Library Replacements (heuristic — source not scanned)`);
1407
+ parts.push("");
1408
+ parts.push(`No \`old_source_dir\` was provided so libraries cannot be confirmed. Based on the component name, the old version may use:`);
1409
+ for (const lib of heuristicLibs) {
1410
+ parts.push(`- \`${lib}\``);
1411
+ }
1412
+ parts.push("");
1413
+ parts.push(`Verify by reading the old \`index.tsx\`, then call \`get_migration_guide("library-replacements")\`.`);
1414
+ parts.push("");
1415
+ }
1416
+ }
1417
+ // Relevant guides — keep terse; LLM can call get_migration_guide("list") for the full catalog
1418
+ parts.push(`## ${nextStep + 1}. Relevant Guides`);
1419
+ parts.push("");
1420
+ parts.push(`- \`get_migration_guide("react-to-preact")\` — code conversion patterns`);
1421
+ parts.push(`- \`get_migration_guide("prop-runtime-shapes")\` — exact runtime shapes (\`.data\` vs \`.links\`, etc.)`);
1422
+ if (children.length > 0 || target.isHeader || target.isFooter) {
1423
+ parts.push(`- \`get_framework_guide("header-footer-patterns")\` — COMPONENT_LIST + IkasComponentRenderer wiring`);
1424
+ }
1425
+ parts.push("");
1426
+ // Completion
1427
+ parts.push(`## ${nextStep + 2}. Mark Complete`);
1428
+ parts.push("");
1429
+ parts.push(`Once the section builds cleanly with \`npx ikas-component build\`: edit MIGRATION.md → tick \`[x]\` for \`${sectionId}\` and each child component, log any customData decisions under \`## Custom Data Decisions\`, and append a brief entry to \`## Notes\` with anything future sessions should know.`);
1430
+ parts.push("");
1431
+ return parts.join("\n");
1432
+ }
162
1433
  // --- Search helpers ---
163
1434
  function matchScore(text, query) {
164
1435
  const lower = text.toLowerCase();
@@ -181,10 +1452,59 @@ function matchScore(text, query) {
181
1452
  }
182
1453
  return score;
183
1454
  }
1455
+ // Levenshtein edit distance — small DP, fine for the 28 short kebab-case section template names.
1456
+ function levenshtein(a, b) {
1457
+ if (a === b)
1458
+ return 0;
1459
+ if (a.length === 0)
1460
+ return b.length;
1461
+ if (b.length === 0)
1462
+ return a.length;
1463
+ const m = a.length;
1464
+ const n = b.length;
1465
+ const prev = new Array(n + 1);
1466
+ const curr = new Array(n + 1);
1467
+ for (let j = 0; j <= n; j++)
1468
+ prev[j] = j;
1469
+ for (let i = 1; i <= m; i++) {
1470
+ curr[0] = i;
1471
+ for (let j = 1; j <= n; j++) {
1472
+ const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
1473
+ curr[j] = Math.min(curr[j - 1] + 1, // insertion
1474
+ prev[j] + 1, // deletion
1475
+ prev[j - 1] + cost // substitution
1476
+ );
1477
+ }
1478
+ for (let j = 0; j <= n; j++)
1479
+ prev[j] = curr[j];
1480
+ }
1481
+ return prev[n];
1482
+ }
1483
+ // Suggest up to `max` closest candidates for an unknown input.
1484
+ // Primary: matchScore (substring/word). Fallback: Levenshtein when nothing scores well.
1485
+ function suggestClosestNames(input, candidates, opts) {
1486
+ const min = opts?.min ?? 8;
1487
+ const max = opts?.max ?? 3;
1488
+ const scored = candidates
1489
+ .map((c) => ({ name: c, score: matchScore(c, input) }))
1490
+ .filter((s) => s.score >= min)
1491
+ .sort((a, b) => b.score - a.score);
1492
+ if (scored.length > 0)
1493
+ return scored.slice(0, max).map((s) => s.name);
1494
+ // Levenshtein fallback — tolerate up to ceil(len/3) edits.
1495
+ const maxDistance = Math.max(1, Math.ceil(input.length / 3));
1496
+ const lower = input.toLowerCase();
1497
+ const edits = candidates
1498
+ .map((c) => ({ name: c, d: levenshtein(lower, c.toLowerCase()) }))
1499
+ .filter((s) => s.d <= maxDistance)
1500
+ .sort((a, b) => a.d - b.d);
1501
+ return edits.slice(0, max).map((s) => s.name);
1502
+ }
184
1503
  function searchFunctions(query) {
185
1504
  const scored = storefrontData.functions
186
1505
  .map((fn) => {
187
1506
  const nameScore = matchScore(fn.name, query) * 3;
1507
+ const displayNameScore = fn.displayName ? matchScore(fn.displayName, query) * 3 : 0;
188
1508
  const descScore = matchScore(fn.description, query);
189
1509
  const catScore = fn.categories.some((c) => matchScore(c, query) > 0) ? 5 : 0;
190
1510
  const paramScore = fn.params.some((p) => matchScore(p.name, query) > 0 || matchScore(p.description, query) > 0)
@@ -192,7 +1512,10 @@ function searchFunctions(query) {
192
1512
  : 0;
193
1513
  const sigScore = matchScore(fn.signature, query) * 2;
194
1514
  const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0) ? 8 : 0;
195
- return { fn, score: nameScore + descScore + catScore + paramScore + sigScore + typeScore };
1515
+ return {
1516
+ fn,
1517
+ score: nameScore + displayNameScore + descScore + catScore + paramScore + sigScore + typeScore,
1518
+ };
196
1519
  })
197
1520
  .filter((item) => item.score > 0)
198
1521
  .sort((a, b) => b.score - a.score);
@@ -334,10 +1657,11 @@ const server = new McpServer({
334
1657
  instructions: "Examples and section templates from this server are API reference only — reuse imports, function calls, and data-access patterns; create your own JSX structure, CSS class names, and visual design.",
335
1658
  });
336
1659
  // Tool: search_docs
337
- server.tool("search_docs", "Search across all ikas storefront API docs and framework guides. Returns matching functions and framework topics ranked by relevance.", { query: z.string().describe("Search keyword or phrase") }, async ({ query }) => {
1660
+ server.tool("search_docs", "Search across all ikas storefront API docs, framework guides, and migration guides. Returns matching functions, framework topics, and migration topics ranked by relevance.", { query: z.string().describe("Search keyword or phrase") }, async ({ query }) => {
338
1661
  const functions = searchFunctions(query).slice(0, 10);
339
1662
  const topics = searchFrameworkTopics(query).slice(0, 5);
340
1663
  const types = searchTypes(query).slice(0, 5);
1664
+ const migrationTopics = searchMigrationTopics(query).slice(0, 3);
341
1665
  const parts = [];
342
1666
  if (functions.length > 0) {
343
1667
  parts.push("## Matching Storefront Functions\n");
@@ -363,7 +1687,15 @@ server.tool("search_docs", "Search across all ikas storefront API docs and frame
363
1687
  parts.push("");
364
1688
  parts.push("Use `get_type_definition(name)` to get full type details, or `search_types(query)` for more type results.");
365
1689
  }
366
- if (functions.length === 0 && topics.length === 0 && types.length === 0) {
1690
+ if (migrationTopics.length > 0) {
1691
+ parts.push("\n## Matching Migration Topics\n");
1692
+ for (const item of migrationTopics) {
1693
+ parts.push(`- [migration] **${item.topic.title}** (key: \`${item.key}\`) - ${item.topic.description}`);
1694
+ }
1695
+ parts.push("");
1696
+ parts.push("Use `get_migration_guide(topic)` to get full content for any migration topic.");
1697
+ }
1698
+ if (functions.length === 0 && topics.length === 0 && types.length === 0 && migrationTopics.length === 0) {
367
1699
  parts.push(`No results found for "${query}". Try different keywords or use \`list_functions()\` to see all available functions.`);
368
1700
  }
369
1701
  return { content: [{ type: "text", text: parts.join("\n") }] };
@@ -371,31 +1703,53 @@ server.tool("search_docs", "Search across all ikas storefront API docs and frame
371
1703
  // Tool: get_function_doc
372
1704
  server.tool("get_function_doc", "Get full documentation for a specific storefront API function including signature, parameters, return type, and example.", { name: z.string().describe("Function name (e.g. 'addItemToCart', 'Router.navigate')") }, async ({ name }) => {
373
1705
  const nameLower = name.toLowerCase();
374
- const fn = storefrontData.functions.find((f) => f.name.toLowerCase() === nameLower ||
375
- (f.displayName && f.displayName.toLowerCase() === nameLower));
376
- if (!fn) {
377
- // Try fuzzy match
378
- const matches = storefrontData.functions.filter((f) => f.name.toLowerCase().includes(nameLower) ||
379
- (f.displayName && f.displayName.toLowerCase().includes(nameLower)));
380
- if (matches.length > 0) {
381
- const suggestions = matches.slice(0, 5).map((f) => {
382
- const alias = f.displayName && f.displayName !== f.name ? ` (alias: ${f.displayName})` : "";
383
- return ` - ${f.name}${alias}`;
384
- });
385
- return {
386
- content: [
387
- {
388
- type: "text",
389
- text: `Function "${name}" not found. Did you mean:\n${suggestions.join("\n")}`,
390
- },
391
- ],
392
- };
1706
+ // Phase 1: canonical-name match wins. A real function name always outranks
1707
+ // any displayName alias so aliases can never shadow the function they're
1708
+ // named after (e.g. `hasCustomer` the function vs. `hasIkasOrderCustomer`'s
1709
+ // [BP-DISPLAY-NAME: hasCustomer] alias).
1710
+ const byName = storefrontData.functions.find((f) => f.name.toLowerCase() === nameLower);
1711
+ if (byName) {
1712
+ return { content: [{ type: "text", text: formatFunctionDoc(byName) }] };
1713
+ }
1714
+ // Phase 2: fall back to displayName aliases.
1715
+ const byAlias = storefrontData.functions.filter((f) => f.displayName && f.displayName.toLowerCase() === nameLower);
1716
+ if (byAlias.length === 1) {
1717
+ const fn = byAlias[0];
1718
+ const note = `> Note: "${name}" is a display alias for \`${fn.name}\`.\n\n`;
1719
+ return {
1720
+ content: [{ type: "text", text: note + formatFunctionDoc(fn) }],
1721
+ };
1722
+ }
1723
+ if (byAlias.length > 1) {
1724
+ const lines = [];
1725
+ lines.push(`"${name}" is an ambiguous display alias used by ${byAlias.length} functions. Call \`get_function_doc\` again with the canonical name of the one you want:`);
1726
+ lines.push("");
1727
+ for (const fn of byAlias) {
1728
+ lines.push(formatFunctionSummary(fn));
1729
+ lines.push(` \`${fn.signature}\``);
393
1730
  }
1731
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1732
+ }
1733
+ // No exact match. Try fuzzy substring match across name + displayName.
1734
+ const matches = storefrontData.functions.filter((f) => f.name.toLowerCase().includes(nameLower) ||
1735
+ (f.displayName && f.displayName.toLowerCase().includes(nameLower)));
1736
+ if (matches.length > 0) {
1737
+ const suggestions = matches.slice(0, 5).map((f) => {
1738
+ const alias = f.displayName && f.displayName !== f.name ? ` (alias: ${f.displayName})` : "";
1739
+ return ` - ${f.name}${alias}`;
1740
+ });
394
1741
  return {
395
- content: [{ type: "text", text: `Function "${name}" not found. Use \`list_functions()\` to see all available functions.` }],
1742
+ content: [
1743
+ {
1744
+ type: "text",
1745
+ text: `Function "${name}" not found. Did you mean:\n${suggestions.join("\n")}`,
1746
+ },
1747
+ ],
396
1748
  };
397
1749
  }
398
- return { content: [{ type: "text", text: formatFunctionDoc(fn) }] };
1750
+ return {
1751
+ content: [{ type: "text", text: `Function "${name}" not found. Use \`list_functions()\` to see all available functions.` }],
1752
+ };
399
1753
  });
400
1754
  // Tool: list_functions
401
1755
  server.tool("list_functions", "List storefront API functions. Without a `category`, returns category names + counts so you can drill in. With a `category`, returns one-line summaries for that category. Use `limit`/`offset` to paginate.", {
@@ -974,7 +2328,7 @@ server.tool("get_prop_types", "Get all available ikas.config.json prop types wit
974
2328
  const sectionTemplateKeys = sectionTemplateNames.length > 0
975
2329
  ? sectionTemplateNames
976
2330
  : null;
977
- server.tool("get_section_template", "Get the root files of a starter section template (index.tsx, types.ts, styles.css, ikas-config-snippet.json). Returns ONLY the section's root files plus the NAMES of any children, components, sub-components, utilities, and hooks — their files are NOT included by default. To view one item's full implementation, call `get_section_child(section, name, kind)` where kind is 'children' (default), 'components', or 'sub-components'. To bundle subtrees inline, pass `include`. Call `list_section_types()` for available section types. Use the API patterns shown — create your own JSX structure, CSS class names, and visual design.", {
2331
+ server.tool("get_section_template", "Get the root files of a starter section template (index.tsx, types.ts, styles.css, ikas-config-snippet.json). Returns ONLY the section's root files plus the NAMES of any children, components, sub-components, utilities, and hooks — their files are NOT included by default. To view one item's full implementation, call `get_section_child(section, name, kind)` where kind is 'children' (default), 'components', or 'sub-components'. To bundle subtrees inline, pass `include`. Call `list_section_types()` for available section types. Use the API patterns shown — create your own JSX structure, CSS class names, and visual design. **Container sections** (Header, Footer, ProductDetail, etc.) host child components via a `COMPONENT_LIST` slot — the response emits a complete multi-step Setup Recipe (create children → capture ids → wire `filteredComponentIds` via `config update-prop`). Follow all steps; the parent alone produces an empty section.", {
978
2332
  sectionType: z
979
2333
  .string()
980
2334
  .describe("The section type (call `list_section_types()` for valid values)"),
@@ -1016,11 +2370,19 @@ server.tool("get_section_template", "Get the root files of a starter section tem
1016
2370
  }
1017
2371
  const normalizedType = normalizeName(sectionType);
1018
2372
  if (!sectionTemplateNames.includes(normalizedType)) {
2373
+ const suggestions = suggestClosestNames(normalizedType, sectionTemplateNames);
2374
+ let suggestionText = "";
2375
+ if (suggestions.length === 1) {
2376
+ suggestionText = ` Did you mean "${suggestions[0]}"?`;
2377
+ }
2378
+ else if (suggestions.length > 1) {
2379
+ suggestionText = ` Did you mean one of: ${suggestions.map((s) => `"${s}"`).join(", ")}?`;
2380
+ }
1019
2381
  return {
1020
2382
  content: [
1021
2383
  {
1022
2384
  type: "text",
1023
- text: `Unknown section type "${sectionType}". Call \`list_section_types()\` to see all ${sectionTemplateNames.length} valid types.`,
2385
+ text: `Unknown section type "${sectionType}".${suggestionText} Call \`list_section_types()\` to see all ${sectionTemplateNames.length} valid types.`,
1024
2386
  },
1025
2387
  ],
1026
2388
  };
@@ -1049,6 +2411,18 @@ server.tool("get_section_template", "Get the root files of a starter section tem
1049
2411
  `## ${bundle.title} — API Integration Pattern Reference`,
1050
2412
  "",
1051
2413
  ];
2414
+ // Detect container-section pattern (COMPONENT_LIST with `<id-of-X>` placeholders).
2415
+ // Surface this BEFORE every other warning so the LLM cannot miss the wiring requirement.
2416
+ // The full recipe (commands, captured-id placeholders, update-prop call) is appended near
2417
+ // the end of the response in the existing recipe-builder block.
2418
+ {
2419
+ const snippetStrForBanner = bundle.rootFiles["ikas-config-snippet.json"];
2420
+ if (snippetStrForBanner && /<id-of-[A-Za-z0-9_]+>/.test(snippetStrForBanner)) {
2421
+ const childMatches = Array.from(snippetStrForBanner.matchAll(/<id-of-([A-Za-z0-9_]+)>/g));
2422
+ const uniqueChildren = Array.from(new Set(childMatches.map((m) => m[1])));
2423
+ parts.push(`> 🔧 **CONTAINER SECTION — required wiring.** This template hosts ${uniqueChildren.length} child component${uniqueChildren.length === 1 ? "" : "s"} (${uniqueChildren.map((n) => `\`${n}\``).join(", ")}) via a \`COMPONENT_LIST\` slot. Creating the parent alone produces an empty, unusable section. **You MUST follow the full Setup Recipe below**: create each child → capture its \`componentId\` → wire them into the parent's \`filteredComponentIds\` with \`config update-prop\`. Component ids are opaque random strings (e.g. \`7ojrigep-Eml9n5sN3i\`) — they cannot be derived from names, and the CLI rejects unknown ids.`, "");
2424
+ }
2425
+ }
1052
2426
  if (anyOutstandingSubtree) {
1053
2427
  parts.push("> **PARTIAL TEMPLATE — NOT A COMPLETE IMPLEMENTATION.** The files below are the section's root files only. The section also ships with child components, local components, and/or shared sub-components whose files are **not included** in this response. To view one's full implementation, call `get_section_child(section, name, kind)` for each item you need (kind = `children`, `components`, or `sub-components`). The root `index.tsx` alone is not a complete reference — its imports resolve to files that live in those subtrees.", "");
1054
2428
  }
@@ -1117,7 +2491,10 @@ server.tool("get_section_template", "Get the root files of a starter section tem
1117
2491
  renderInlineSubtree("children", bundle.childContents);
1118
2492
  renderInlineSubtree("components", bundle.componentContents);
1119
2493
  renderInlineSubtree("sub-components", bundle.subComponentContents);
1120
- // Generate a ready-to-run CLI command from the config snippet
2494
+ // Generate a ready-to-run CLI recipe from the config snippet. If the parent's
2495
+ // filteredComponentIds reference `<id-of-X>` placeholders, expand into a
2496
+ // multi-step recipe (create children → create parent → wire filteredComponentIds)
2497
+ // so the LLM cannot skip the wiring step.
1121
2498
  const configSnippetStr = bundle.rootFiles["ikas-config-snippet.json"];
1122
2499
  if (configSnippetStr) {
1123
2500
  try {
@@ -1125,26 +2502,102 @@ server.tool("get_section_template", "Get the root files of a starter section tem
1125
2502
  const compName = configSnippet.name || normalizedType;
1126
2503
  const compType = configSnippet.type || "section";
1127
2504
  const propsArr = configSnippet.props || [];
1128
- let cliCommand = `npx ikas-component config add-component --name "${compName}" --type ${compType}`;
1129
- if (configSnippet.isHeader)
1130
- cliCommand += " --isHeader";
1131
- if (configSnippet.isFooter)
1132
- cliCommand += " --isFooter";
1133
- if (propsArr.length > 0) {
1134
- const propsJson = JSON.stringify(propsArr.map((p) => {
1135
- const prop = {
1136
- name: p.name,
1137
- type: p.type,
1138
- };
1139
- if (p.displayName)
1140
- prop.displayName = p.displayName;
1141
- if (p.required)
1142
- prop.required = true;
1143
- return prop;
1144
- }));
1145
- cliCommand += ` --props '${propsJson}'`;
2505
+ // Strip filteredComponentIds for the parent's add-component call (wired
2506
+ // separately below). Preserve name/type/displayName/required so the LLM gets the prop shape.
2507
+ const parentPropsForAdd = propsArr.map((p) => {
2508
+ const out = { name: p.name, type: p.type };
2509
+ if (p.displayName)
2510
+ out.displayName = p.displayName;
2511
+ if (p.required)
2512
+ out.required = true;
2513
+ return out;
2514
+ });
2515
+ const placeholderRe = /^<id-of-(.+)>$/;
2516
+ const slotPlans = [];
2517
+ const allChildNames = new Set();
2518
+ for (const p of propsArr) {
2519
+ if ((p.type === "COMPONENT_LIST" || p.type === "COMPONENT") &&
2520
+ Array.isArray(p.filteredComponentIds)) {
2521
+ const childNames = [];
2522
+ for (const entry of p.filteredComponentIds) {
2523
+ const m = typeof entry === "string" && entry.match(placeholderRe);
2524
+ if (m) {
2525
+ childNames.push(m[1]);
2526
+ allChildNames.add(m[1]);
2527
+ }
2528
+ }
2529
+ if (childNames.length > 0) {
2530
+ slotPlans.push({ propName: p.name, childNames });
2531
+ }
2532
+ }
2533
+ }
2534
+ const childPlans = [];
2535
+ for (const childName of allChildNames) {
2536
+ const childSnippetPath = path.join(SECTION_TEMPLATES_DIR, normalizedType, "children", childName, "ikas-config-snippet.json");
2537
+ let propsJson = "[]";
2538
+ let hasTemplate = false;
2539
+ if (fs.existsSync(childSnippetPath)) {
2540
+ hasTemplate = true;
2541
+ try {
2542
+ const childSnippet = JSON.parse(fs.readFileSync(childSnippetPath, "utf-8"));
2543
+ const childProps = (childSnippet.props || []).map((p) => {
2544
+ const out = { name: p.name, type: p.type };
2545
+ if (p.displayName)
2546
+ out.displayName = p.displayName;
2547
+ if (p.required)
2548
+ out.required = true;
2549
+ return out;
2550
+ });
2551
+ propsJson = JSON.stringify(childProps);
2552
+ }
2553
+ catch {
2554
+ // fall through with []
2555
+ }
2556
+ }
2557
+ childPlans.push({ name: childName, propsJson, hasTemplate });
2558
+ }
2559
+ const baseFlags = (configSnippet.isHeader ? " --isHeader" : "") +
2560
+ (configSnippet.isFooter ? " --isFooter" : "");
2561
+ const parentPropsJsonStr = parentPropsForAdd.length > 0
2562
+ ? ` --props '${JSON.stringify(parentPropsForAdd)}'`
2563
+ : "";
2564
+ if (slotPlans.length > 0) {
2565
+ parts.push("---", "");
2566
+ parts.push(`### Setup Recipe (run in order — ${childPlans.length} child component${childPlans.length === 1 ? "" : "s"} + 1 parent + wiring)`);
2567
+ parts.push("");
2568
+ parts.push(`> ⚠️ **This section is a CONTAINER.** It hosts child components via ${slotPlans.length === 1 ? "a COMPONENT_LIST slot" : "COMPONENT_LIST slots"} (\`${slotPlans.map((s) => s.propName).join("`, `")}\`). Creating the parent alone is **not enough** — you MUST also create the child components and wire their opaque ids into the parent's \`filteredComponentIds\`. Skipping the wiring leaves the slot empty and the section unusable.`, "");
2569
+ parts.push("**Step 1 — Create each child component, and capture its `componentId` from the JSON response:**");
2570
+ parts.push("", "```bash");
2571
+ for (const ch of childPlans) {
2572
+ parts.push(`npx ikas-component config add-component --name "${ch.name}" --type component --props '${ch.propsJson}'`);
2573
+ parts.push(`# → { "success": true, "componentId": "<capture as ${ch.name.toUpperCase()}_ID>", ... }`);
2574
+ if (!ch.hasTemplate) {
2575
+ parts.push(`# (no children/${ch.name}/ template in this section bundle — \`--props '[]'\` is a stub; add real props for ${ch.name} as needed)`);
2576
+ }
2577
+ }
2578
+ parts.push("```", "");
2579
+ parts.push("**Step 2 — Create the parent section (without `filteredComponentIds` yet — those are wired in Step 3):**");
2580
+ parts.push("", "```bash");
2581
+ parts.push(`npx ikas-component config add-component --name "${compName}" --type ${compType}${baseFlags}${parentPropsJsonStr}`);
2582
+ parts.push("```", "");
2583
+ parts.push("**Step 3 — Wire each slot to its allowed children using the ids captured in Step 1:**");
2584
+ parts.push("", "```bash");
2585
+ for (const slot of slotPlans) {
2586
+ const idsArr = slot.childNames.map((n) => `<${n.toUpperCase()}_ID>`);
2587
+ parts.push(`npx ikas-component config update-prop --component "${compName}" --prop ${slot.propName} \\`);
2588
+ parts.push(` --filteredComponentIds '${JSON.stringify(idsArr)}'`);
2589
+ }
2590
+ parts.push("```", "");
2591
+ parts.push("Replace each `<X_ID>` placeholder above with the real `componentId` from the corresponding Step 1 response (or look ids up with `config list`). The CLI rejects unknown ids with a structured error — there is no silent failure mode.");
2592
+ parts.push("");
2593
+ parts.push("**Do NOT manually create or edit `types.ts`** — the CLI commands above regenerate it automatically.");
2594
+ parts.push("");
2595
+ }
2596
+ else {
2597
+ // No child slots — single-step parent command (preserves previous behaviour)
2598
+ const cliCommand = `npx ikas-component config add-component --name "${compName}" --type ${compType}${baseFlags}${parentPropsJsonStr}`;
2599
+ parts.push("---", "", "### CLI Command (run this first)", "", "```bash", cliCommand, "```", "", "**Do NOT manually create or edit `types.ts`** — the CLI command above generates it automatically.", "");
1146
2600
  }
1147
- parts.push("---", "", "### CLI Command (run this first)", "", "```bash", cliCommand, "```", "", "**Do NOT manually create or edit `types.ts`** — the CLI command above generates it automatically.", "");
1148
2601
  }
1149
2602
  catch {
1150
2603
  // ignore parse errors
@@ -1325,6 +2778,243 @@ server.tool("list_section_types", "List all available `get_section_template` sec
1325
2778
  lines.push("", "Call `get_section_template(sectionType)` to fetch one.");
1326
2779
  return { content: [{ type: "text", text: lines.join("\n") }] };
1327
2780
  });
2781
+ // --- Migration tools ---
2782
+ // Tool: analyze_old_theme
2783
+ server.tool("analyze_old_theme", "Analyze an old ikas storefront theme.json and produce a structured migration report. Shows all components, custom data definitions, prop type breakdown, and migration recommendations. Use this as the first step when converting an old theme.", { theme_json: z.string().describe("The raw JSON content of the old theme.json file") }, async ({ theme_json }) => {
2784
+ try {
2785
+ const parsed = JSON.parse(theme_json);
2786
+ const analysis = analyzeOldTheme(parsed);
2787
+ return { content: [{ type: "text", text: analysis }] };
2788
+ }
2789
+ catch (err) {
2790
+ return {
2791
+ content: [{ type: "text", text: `Error parsing theme.json: ${err instanceof Error ? err.message : String(err)}. Make sure you're passing valid JSON.` }],
2792
+ };
2793
+ }
2794
+ });
2795
+ // Tool: get_migration_guide
2796
+ const migrationTopicAliases = {
2797
+ "overview": "migration-overview",
2798
+ "migrate": "migration-overview",
2799
+ "custom": "custom-data-conversion",
2800
+ "custom-data": "custom-data-conversion",
2801
+ "customdata": "custom-data-conversion",
2802
+ "dynamic-list": "custom-data-conversion",
2803
+ "component-list": "custom-data-conversion",
2804
+ "slider": "prop-type-mapping",
2805
+ "props": "prop-type-mapping",
2806
+ "prop-mapping": "prop-type-mapping",
2807
+ "types": "prop-type-mapping",
2808
+ "react": "react-to-preact",
2809
+ "preact": "react-to-preact",
2810
+ "observer": "react-to-preact",
2811
+ "libraries": "library-replacements",
2812
+ "swiper": "library-replacements",
2813
+ "headlessui": "library-replacements",
2814
+ "tailwind": "library-replacements",
2815
+ "tailwindcss": "library-replacements",
2816
+ "recharts": "library-replacements",
2817
+ "marquee": "library-replacements",
2818
+ "imports": "storefront-import-mapping",
2819
+ "storefront": "storefront-import-mapping",
2820
+ "bp-storefront": "storefront-import-mapping",
2821
+ "theme-json": "theme-json-anatomy",
2822
+ "anatomy": "theme-json-anatomy",
2823
+ "decompose": "component-decomposition-strategy",
2824
+ "decomposition": "component-decomposition-strategy",
2825
+ "strategy": "component-decomposition-strategy",
2826
+ "project": "complete-project-generation",
2827
+ "generate": "complete-project-generation",
2828
+ "generation": "complete-project-generation",
2829
+ "settings": "settings-conversion",
2830
+ "colors": "settings-conversion",
2831
+ "fonts": "settings-conversion",
2832
+ "find": "finding-new-system-equivalents",
2833
+ "search": "finding-new-system-equivalents",
2834
+ "discover": "finding-new-system-equivalents",
2835
+ "equivalent": "finding-new-system-equivalents",
2836
+ "equivalents": "finding-new-system-equivalents",
2837
+ "replacement": "finding-new-system-equivalents",
2838
+ };
2839
+ const migrationTopicKeys = migrationData
2840
+ ? Object.keys(migrationData.topics)
2841
+ : [];
2842
+ server.tool("get_migration_guide", `Get a migration guide for converting old ikas themes to the new code-component system. **Start with \`get_migration_guide("iterative-workflow")\` if you're new to this MCP** — it explains the MCP-vs-LLM responsibility split and the four phases.${migrationTopicKeys.length > 0 ? ` Available topics: ${migrationTopicKeys.join(", ")}. Also supports aliases like "custom", "slider", "react", "libraries", "imports", "settings".` : ""} Call with topic "list" to see all available topics.`, { topic: z.string().describe("Migration topic key, alias, or 'list' to see all topics") }, async ({ topic }) => {
2843
+ if (!migrationData) {
2844
+ return { content: [{ type: "text", text: "Migration data not available. Ensure data/migration.json exists." }] };
2845
+ }
2846
+ if (topic.toLowerCase() === "list") {
2847
+ const available = Object.entries(migrationData.topics)
2848
+ .map(([key, t]) => `- \`${key}\` — ${t.title}: ${t.description}`)
2849
+ .join("\n");
2850
+ return { content: [{ type: "text", text: `## Available Migration Topics\n\n${available}` }] };
2851
+ }
2852
+ const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
2853
+ const resolvedTopic = migrationTopicAliases[topicLower] || topicLower;
2854
+ if (migrationData.topics[resolvedTopic]) {
2855
+ const t = migrationData.topics[resolvedTopic];
2856
+ return { content: [{ type: "text", text: `## ${t.title}\n\n${t.content}` }] };
2857
+ }
2858
+ // Try original key
2859
+ if (resolvedTopic !== topicLower && migrationData.topics[topicLower]) {
2860
+ const t = migrationData.topics[topicLower];
2861
+ return { content: [{ type: "text", text: `## ${t.title}\n\n${t.content}` }] };
2862
+ }
2863
+ // Keyword search
2864
+ const matches = searchMigrationTopics(topic);
2865
+ if (matches.length > 0) {
2866
+ const best = matches[0];
2867
+ return { content: [{ type: "text", text: `## ${best.topic.title}\n\n${best.topic.content}` }] };
2868
+ }
2869
+ const available = Object.entries(migrationData.topics)
2870
+ .map(([key, t]) => ` - \`${key}\` - ${t.title}`)
2871
+ .join("\n");
2872
+ return {
2873
+ content: [{ type: "text", text: `Migration topic "${topic}" not found. Available topics:\n${available}` }],
2874
+ };
2875
+ });
2876
+ // Tool: get_migration_example
2877
+ server.tool("get_migration_example", `Get a concrete before/after migration example showing how to convert an old theme component to the new code-component system.${migrationExampleNames.length > 0 ? ` Available examples: ${migrationExampleNames.join(", ")}.` : ""} Call with example "list" to see all examples.`, { example: z.string().describe("Example name or 'list' to see all examples") }, async ({ example }) => {
2878
+ if (example.toLowerCase() === "list") {
2879
+ if (migrationExampleNames.length === 0) {
2880
+ return { content: [{ type: "text", text: "No migration examples available." }] };
2881
+ }
2882
+ const list = migrationExampleNames.map((name) => {
2883
+ const ex = loadMigrationExample(name);
2884
+ return ex ? `- \`${name}\` — ${ex.title}: ${ex.description}` : `- \`${name}\``;
2885
+ }).join("\n");
2886
+ return { content: [{ type: "text", text: `## Available Migration Examples\n\n${list}` }] };
2887
+ }
2888
+ const exampleLower = example.toLowerCase();
2889
+ let exName = migrationExampleNames.find((n) => n === exampleLower);
2890
+ if (!exName) {
2891
+ exName = migrationExampleNames.find((n) => n.includes(exampleLower) || exampleLower.includes(n));
2892
+ }
2893
+ if (!exName) {
2894
+ const available = migrationExampleNames.join(", ");
2895
+ return {
2896
+ content: [{ type: "text", text: `Migration example "${example}" not found. Available: ${available}` }],
2897
+ };
2898
+ }
2899
+ const ex = loadMigrationExample(exName);
2900
+ if (!ex) {
2901
+ return { content: [{ type: "text", text: `Failed to load migration example "${exName}".` }] };
2902
+ }
2903
+ const parts = [
2904
+ `## ${ex.title}`,
2905
+ "",
2906
+ ex.description,
2907
+ "",
2908
+ ];
2909
+ for (const [filename, content] of Object.entries(ex.files)) {
2910
+ const ext = filename.split(".").pop() || "text";
2911
+ const lang = ext === "tsx" || ext === "ts"
2912
+ ? "typescript"
2913
+ : ext === "css"
2914
+ ? "css"
2915
+ : ext === "json"
2916
+ ? "json"
2917
+ : "text";
2918
+ const isAfter = filename.startsWith("after-");
2919
+ const isBefore = filename.startsWith("before-");
2920
+ const label = isBefore ? "📋 BEFORE" : isAfter ? "✅ AFTER" : "";
2921
+ parts.push(`### ${label} ${filename}`, "", `\`\`\`${lang}`, content, "```", "");
2922
+ }
2923
+ return { content: [{ type: "text", text: parts.join("\n") }] };
2924
+ });
2925
+ // Tool: plan_migration
2926
+ server.tool("plan_migration", "Generate the **initial** migration plan and (when `project_root` is provided) write it to <project_root>/MIGRATION.md. **This is the only time the MCP writes that file.** From here, you own it: tick checkboxes as you finish work, log custom-data decisions, scan the old source for atomic components (Button, Input, Card, etc.) that theme.json doesn't see, and append them to MIGRATION.md yourself. theme.json is incomplete by design — the MCP can only describe what's listed there. Pass `theme_json_path` for large themes (raw `theme_json` string is supported for backward compat but fails on real-world sizes).", {
2927
+ theme_json: z.string().optional().describe("Raw JSON content of the old theme.json. EITHER this OR theme_json_path is required (not both). For real themes use theme_json_path — raw strings exceed tool/context limits at production sizes."),
2928
+ theme_json_path: z.string().optional().describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
2929
+ project_name: z.string().optional().describe("Target new project name, used to prefix migration-tracking IDs (default: 'my-theme')"),
2930
+ old_source_dir: z.string().optional().describe("Absolute path to the old project's src/ directory. When provided, the tool scans .tsx files to detect shared sub-components used across 3+ components. This scan is partial — atomic components used by only 1-2 sections will be missed and must be added by the LLM."),
2931
+ project_root: z.string().optional().describe("Absolute path to the new project root. When provided, the MCP writes MIGRATION.md to <project_root>/MIGRATION.md and returns a short summary instead of the full markdown body."),
2932
+ overwrite: z.boolean().optional().describe("If MIGRATION.md already exists at <project_root>/MIGRATION.md and is non-empty, refuse the write unless this is true. Default: false."),
2933
+ }, async ({ theme_json, theme_json_path, project_name, old_source_dir, project_root, overwrite }) => {
2934
+ try {
2935
+ const parsed = resolveThemeJson(theme_json, theme_json_path);
2936
+ const projectName = project_name || "my-theme";
2937
+ const plan = generateMigrationPlan(parsed, projectName, old_source_dir);
2938
+ if (!project_root) {
2939
+ return { content: [{ type: "text", text: plan }] };
2940
+ }
2941
+ // Write MIGRATION.md to project_root
2942
+ if (!path.isAbsolute(project_root)) {
2943
+ throw new Error(`project_root must be absolute: ${project_root}`);
2944
+ }
2945
+ if (!fs.existsSync(project_root)) {
2946
+ throw new Error(`project_root not found: ${project_root}`);
2947
+ }
2948
+ if (!fs.statSync(project_root).isDirectory()) {
2949
+ throw new Error(`project_root is not a directory: ${project_root}`);
2950
+ }
2951
+ const targetPath = path.join(project_root, "MIGRATION.md");
2952
+ if (fs.existsSync(targetPath)) {
2953
+ const existing = fs.readFileSync(targetPath, "utf-8").trim();
2954
+ if (existing.length > 0 && !overwrite) {
2955
+ return {
2956
+ content: [
2957
+ {
2958
+ type: "text",
2959
+ text: `Refusing to overwrite existing non-empty MIGRATION.md at ${targetPath}. ` +
2960
+ `Pass overwrite: true to replace it, or delete the file first. ` +
2961
+ `If you intended to RESUME an in-progress migration, do not call plan_migration again — read the existing MIGRATION.md and continue from the first unchecked item.`,
2962
+ },
2963
+ ],
2964
+ };
2965
+ }
2966
+ }
2967
+ writeFileAtomic(targetPath, plan);
2968
+ const components = parsed.components || [];
2969
+ const customData = parsed.customData || [];
2970
+ const cssVarCount = parsed.settings?.colors?.length || 0;
2971
+ const fontCount = parsed.settings?.fontFamily?.name ? 1 : 0;
2972
+ const customDataCount = customData.filter((cd) => cd.isRoot).length;
2973
+ const sectionCount = components.length;
2974
+ const summary = [
2975
+ `Wrote initial migration plan to ${targetPath}`,
2976
+ "",
2977
+ `**Summary:**`,
2978
+ `- Sections to migrate: ${sectionCount}`,
2979
+ `- CSS variables: ${cssVarCount}`,
2980
+ `- Fonts: ${fontCount}`,
2981
+ `- Custom data types (deferred decisions, not pre-migrated): ${customDataCount}`,
2982
+ "",
2983
+ `**Next steps for the LLM:**`,
2984
+ `1. Read \`${targetPath}\` start-to-finish. The "READ THIS FIRST" preamble explains your responsibilities.`,
2985
+ `2. Scan the old source directory (\`${old_source_dir || "<old-src>"}\`) for atomic components (Button, Input, Card, icons, etc.) NOT referenced from theme.json. Add them to \`## Source Code Analysis\` in MIGRATION.md.`,
2986
+ `3. Start the Foundation work (CSS variables, fonts, shared sub-components).`,
2987
+ `4. For each section, call \`get_section_migration_plan({theme_json_path, section_name, project_name})\`. The MCP will tell you which props need enum-vs-component decisions.`,
2988
+ `5. Log every custom-data decision in \`## Custom Data Decisions\`. Tick checkboxes as you finish.`,
2989
+ ].join("\n");
2990
+ return { content: [{ type: "text", text: summary }] };
2991
+ }
2992
+ catch (err) {
2993
+ return {
2994
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
2995
+ };
2996
+ }
2997
+ });
2998
+ // Tool: get_section_migration_plan
2999
+ server.tool("get_section_migration_plan", "Returns concrete CLI commands and prop conversions for one section. For each prop that references a customData type, you'll see a 'Decide: enum or component?' callout — log your decision in MIGRATION.md under `## Custom Data Decisions`. Pass `theme_json_path` for large themes.", {
3000
+ theme_json: z.string().optional().describe("Raw JSON content of the old theme.json. EITHER this OR theme_json_path is required (not both)."),
3001
+ theme_json_path: z.string().optional().describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
3002
+ section_name: z.string().describe("Old component name (e.g. 'Navbar', 'ProductGrid') or dir name, OR the new section ID (e.g. 'my-theme-navbar')"),
3003
+ project_name: z.string().optional().describe("Target new project name (must match what was used in plan_migration). Default: 'my-theme'"),
3004
+ old_source_dir: z.string().optional().describe("Absolute path to old src/ directory (used to output exact source file paths to read)"),
3005
+ }, async ({ theme_json, theme_json_path, section_name, project_name, old_source_dir }) => {
3006
+ try {
3007
+ const parsed = resolveThemeJson(theme_json, theme_json_path);
3008
+ const projectName = project_name || "my-theme";
3009
+ const plan = generateSectionMigrationPlan(parsed, section_name, projectName, old_source_dir);
3010
+ return { content: [{ type: "text", text: plan }] };
3011
+ }
3012
+ catch (err) {
3013
+ return {
3014
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
3015
+ };
3016
+ }
3017
+ });
1328
3018
  // --- Start server ---
1329
3019
  async function main() {
1330
3020
  const transport = new StdioServerTransport();