@ikas/code-components-mcp 1.4.0-beta.1 → 1.4.0-beta.2

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 (36) hide show
  1. package/data/migration-examples/complex-header-migration/_meta.json +4 -0
  2. package/data/migration-examples/complex-header-migration/after-config-snippet.json +55 -0
  3. package/data/migration-examples/complex-header-migration/after-section.tsx +64 -0
  4. package/data/migration-examples/complex-header-migration/before-props-summary.json +42 -0
  5. package/data/migration-examples/custom-dynamic-list-to-component-list/_meta.json +4 -0
  6. package/data/migration-examples/custom-dynamic-list-to-component-list/after-child-styles.css +38 -0
  7. package/data/migration-examples/custom-dynamic-list-to-component-list/after-child.tsx +22 -0
  8. package/data/migration-examples/custom-dynamic-list-to-component-list/after-config-snippet.json +31 -0
  9. package/data/migration-examples/custom-dynamic-list-to-component-list/after-section-styles.css +25 -0
  10. package/data/migration-examples/custom-dynamic-list-to-component-list/after-section.tsx +17 -0
  11. package/data/migration-examples/custom-dynamic-list-to-component-list/before-component.tsx +32 -0
  12. package/data/migration-examples/custom-dynamic-list-to-component-list/before-theme-snippet.json +53 -0
  13. package/data/migration-examples/full-component-with-tailwind/_meta.json +4 -0
  14. package/data/migration-examples/full-component-with-tailwind/after-component.tsx +43 -0
  15. package/data/migration-examples/full-component-with-tailwind/after-config-snippet.json +25 -0
  16. package/data/migration-examples/full-component-with-tailwind/after-styles.css +99 -0
  17. package/data/migration-examples/full-component-with-tailwind/before-component.tsx +60 -0
  18. package/data/migration-examples/object-custom-to-inline-props/_meta.json +4 -0
  19. package/data/migration-examples/object-custom-to-inline-props/after-component.tsx +34 -0
  20. package/data/migration-examples/object-custom-to-inline-props/after-config-snippet.json +23 -0
  21. package/data/migration-examples/object-custom-to-inline-props/after-styles.css +38 -0
  22. package/data/migration-examples/object-custom-to-inline-props/before-component.tsx +30 -0
  23. package/data/migration-examples/object-custom-to-inline-props/before-theme-snippet.json +26 -0
  24. package/data/migration-examples/slider-library-replacement/_meta.json +4 -0
  25. package/data/migration-examples/slider-library-replacement/after-child.tsx +13 -0
  26. package/data/migration-examples/slider-library-replacement/after-config-snippet.json +29 -0
  27. package/data/migration-examples/slider-library-replacement/after-section.tsx +38 -0
  28. package/data/migration-examples/slider-library-replacement/after-styles.css +43 -0
  29. package/data/migration-examples/slider-library-replacement/before-component.tsx +39 -0
  30. package/data/migration-examples/slider-library-replacement/before-types-snippet.ts +14 -0
  31. package/data/migration.json +95 -0
  32. package/data/storefront-api.json +1 -1
  33. package/data/storefront-types.json +1 -1
  34. package/dist/index.js +1282 -3
  35. package/dist/index.js.map +1 -1
  36. 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 ---
@@ -50,7 +50,15 @@ function normalizeName(value) {
50
50
  const storefrontData = loadStorefrontData();
51
51
  const frameworkData = loadJsonFile("../data/framework.json");
52
52
  const typesData = loadStorefrontTypes();
53
+ let migrationData = null;
54
+ try {
55
+ migrationData = loadJsonFile("../data/migration.json");
56
+ }
57
+ catch {
58
+ migrationData = null;
59
+ }
53
60
  const SECTION_TEMPLATES_DIR = path.resolve(__dirname, "../data/section-templates");
61
+ const MIGRATION_EXAMPLES_DIR = path.resolve(__dirname, "../data/migration-examples");
54
62
  function listSectionTemplateNames() {
55
63
  try {
56
64
  return fs
@@ -159,6 +167,1087 @@ function loadSectionSubtreeItem(section, subtree, name) {
159
167
  return { files };
160
168
  }
161
169
  const sectionTemplateNames = listSectionTemplateNames();
170
+ // --- Migration example helpers ---
171
+ function listMigrationExampleNames() {
172
+ try {
173
+ return fs
174
+ .readdirSync(MIGRATION_EXAMPLES_DIR, { withFileTypes: true })
175
+ .filter((e) => e.isDirectory())
176
+ .map((e) => e.name)
177
+ .sort();
178
+ }
179
+ catch {
180
+ return [];
181
+ }
182
+ }
183
+ function loadMigrationExample(name) {
184
+ const root = path.join(MIGRATION_EXAMPLES_DIR, name);
185
+ if (!fs.existsSync(root))
186
+ return null;
187
+ const metaPath = path.join(root, "_meta.json");
188
+ if (!fs.existsSync(metaPath))
189
+ return null;
190
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
191
+ const files = {};
192
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
193
+ if (entry.name === "_meta.json" || entry.isDirectory())
194
+ continue;
195
+ files[entry.name] = fs.readFileSync(path.join(root, entry.name), "utf-8");
196
+ }
197
+ return { title: meta.title, description: meta.description, files };
198
+ }
199
+ const migrationExampleNames = listMigrationExampleNames();
200
+ function searchMigrationTopics(query) {
201
+ if (!migrationData)
202
+ return [];
203
+ return Object.entries(migrationData.topics)
204
+ .map(([key, topic]) => {
205
+ const titleScore = matchScore(topic.title, query) * 3;
206
+ const descScore = matchScore(topic.description, query) * 2;
207
+ const contentScore = matchScore(topic.content, query);
208
+ const tagScore = topic.tags.some((t) => matchScore(t, query) > 0) ? 5 : 0;
209
+ return { key, topic, score: titleScore + descScore + contentScore + tagScore };
210
+ })
211
+ .filter((item) => item.score > 0)
212
+ .sort((a, b) => b.score - a.score);
213
+ }
214
+ function analyzeOldTheme(themeJson) {
215
+ const parts = [];
216
+ const components = themeJson.components || [];
217
+ const customData = themeJson.customData || [];
218
+ const groups = themeJson.groups || [];
219
+ const settings = themeJson.settings;
220
+ // Build customData lookup
221
+ const customDataMap = new Map();
222
+ for (const cd of customData) {
223
+ if (cd.id)
224
+ customDataMap.set(cd.id, cd);
225
+ }
226
+ // Statistics
227
+ let totalProps = 0;
228
+ let customProps = 0;
229
+ let sliderProps = 0;
230
+ let productDetailProps = 0;
231
+ let estimatedChildComponents = 0;
232
+ parts.push(`# Old Theme Analysis\n`);
233
+ 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`);
234
+ parts.push(`## Summary Statistics\n`);
235
+ parts.push(`- **Components:** ${components.length}`);
236
+ parts.push(`- **Custom Data Definitions:** ${customData.filter(cd => cd.isRoot).length}`);
237
+ parts.push(`- **Prop Groups:** ${groups.length}`);
238
+ // Component analysis
239
+ parts.push(`\n## Components (${components.length})\n`);
240
+ for (const comp of components) {
241
+ const props = comp.props || [];
242
+ totalProps += props.length;
243
+ const propTypeCounts = {};
244
+ const customPropDetails = [];
245
+ const sliderPropDetails = [];
246
+ for (const prop of props) {
247
+ const pType = prop.type || "UNKNOWN";
248
+ propTypeCounts[pType] = (propTypeCounts[pType] || 0) + 1;
249
+ if (pType === "CUSTOM" && prop.customDataId) {
250
+ customProps++;
251
+ const cd = customDataMap.get(prop.customDataId);
252
+ if (cd) {
253
+ const cdType = cd.type || "UNKNOWN";
254
+ customPropDetails.push(` - \`${prop.name}\` → ${cd.name || "unnamed"} (${cdType})`);
255
+ if (cdType === "DYNAMIC_LIST" || cdType === "STATIC_LIST") {
256
+ estimatedChildComponents++;
257
+ }
258
+ }
259
+ else {
260
+ customPropDetails.push(` - \`${prop.name}\` → [unresolved customDataId]`);
261
+ }
262
+ }
263
+ if (pType === "SLIDER") {
264
+ sliderProps++;
265
+ const sd = prop.sliderData;
266
+ sliderPropDetails.push(` - \`${prop.name}\` (${sd?.min ?? "?"}–${sd?.max ?? "?"}, step ${sd?.interval ?? "?"})`);
267
+ }
268
+ if (pType === "PRODUCT_DETAIL") {
269
+ productDetailProps++;
270
+ }
271
+ }
272
+ const typesSummary = Object.entries(propTypeCounts)
273
+ .map(([t, c]) => `${t}×${c}`)
274
+ .join(", ");
275
+ const headerFooter = comp.isHeader ? " [HEADER]" : comp.isFooter ? " [FOOTER]" : "";
276
+ parts.push(`### ${comp.displayName || comp.dir || comp.id}${headerFooter}`);
277
+ parts.push(`- **Dir:** \`${comp.dir || "?"}\` | **Props:** ${props.length} (${typesSummary})`);
278
+ parts.push(`- **Recommended new type:** section`);
279
+ if (customPropDetails.length > 0) {
280
+ parts.push(`- **CUSTOM props** (→ COMPONENT_LIST):`);
281
+ parts.push(customPropDetails.join("\n"));
282
+ }
283
+ if (sliderPropDetails.length > 0) {
284
+ parts.push(`- **SLIDER props** (→ NUMBER):`);
285
+ parts.push(sliderPropDetails.join("\n"));
286
+ }
287
+ parts.push("");
288
+ }
289
+ // Custom data analysis
290
+ const rootCustomData = customData.filter(cd => cd.isRoot);
291
+ if (rootCustomData.length > 0) {
292
+ parts.push(`\n## Custom Data Definitions (${rootCustomData.length})\n`);
293
+ for (const cd of rootCustomData) {
294
+ parts.push(`### ${cd.name || cd.typescriptName || cd.id}`);
295
+ parts.push(`- **Type:** ${cd.type}`);
296
+ if (cd.nestedData && cd.nestedData.length > 0) {
297
+ const describeNested = (items, indent) => {
298
+ const lines = [];
299
+ for (const item of items) {
300
+ const key = item.key ? `\`${item.key}\`` : item.typescriptName || item.name || "unnamed";
301
+ lines.push(`${indent}- ${key}: ${item.type}${item.isRequired ? " (required)" : ""}`);
302
+ if (item.nestedData && item.nestedData.length > 0) {
303
+ lines.push(...describeNested(item.nestedData, indent + " "));
304
+ }
305
+ }
306
+ return lines;
307
+ };
308
+ parts.push("- **Structure:**");
309
+ parts.push(...describeNested(cd.nestedData, " "));
310
+ }
311
+ if (cd.enumOptions && cd.enumOptions.length > 0) {
312
+ parts.push(`- **Enum options:** ${cd.enumOptions.map(o => `"${o.value}"`).join(", ")}`);
313
+ }
314
+ // Find which components reference this customData
315
+ const referencingComponents = [];
316
+ for (const comp of components) {
317
+ for (const prop of comp.props || []) {
318
+ if (prop.type === "CUSTOM" && prop.customDataId === cd.id) {
319
+ referencingComponents.push(`${comp.displayName || comp.dir || comp.id}.${prop.name}`);
320
+ }
321
+ }
322
+ }
323
+ if (referencingComponents.length > 0) {
324
+ parts.push(`- **Referenced by:** ${referencingComponents.join(", ")}`);
325
+ }
326
+ parts.push("");
327
+ }
328
+ }
329
+ // Settings
330
+ if (settings) {
331
+ parts.push(`\n## Settings\n`);
332
+ if (settings.colors && settings.colors.length > 0) {
333
+ parts.push(`### Colors (${settings.colors.length})`);
334
+ for (const c of settings.colors) {
335
+ parts.push(`- \`${c.key}\`: ${c.color} (${c.displayName})`);
336
+ }
337
+ parts.push("");
338
+ }
339
+ if (settings.fontFamily) {
340
+ parts.push(`### Font: ${settings.fontFamily.name} (weights: ${settings.fontFamily.variants?.join(", ")})`);
341
+ parts.push("");
342
+ }
343
+ }
344
+ // Groups
345
+ if (groups.length > 0) {
346
+ parts.push(`\n## Prop Groups (${groups.length})\n`);
347
+ for (const g of groups) {
348
+ parts.push(`- \`${g.id}\`: ${g.name}`);
349
+ }
350
+ parts.push("");
351
+ }
352
+ // Final statistics
353
+ parts.push(`\n## Migration Statistics\n`);
354
+ parts.push(`- **Total props across all components:** ${totalProps}`);
355
+ parts.push(`- **CUSTOM props (need conversion):** ${customProps}`);
356
+ parts.push(`- **SLIDER props (→ NUMBER):** ${sliderProps}`);
357
+ parts.push(`- **PRODUCT_DETAIL props (→ PRODUCT):** ${productDetailProps}`);
358
+ parts.push(`- **Estimated child components needed:** ${estimatedChildComponents}`);
359
+ parts.push(`- **Total components in new system:** ~${components.length + estimatedChildComponents} (${components.length} sections + ${estimatedChildComponents} children)`);
360
+ parts.push(`\n## Recommended Workflow\n`);
361
+ const useIterative = components.length > 5;
362
+ if (useIterative) {
363
+ parts.push(`**This theme has ${components.length} sections — USE THE ITERATIVE WORKFLOW.** A single session cannot migrate this entire theme.\n`);
364
+ parts.push(`1. Call \`plan_migration(theme_json, old_source_dir)\` to generate MIGRATION.md (this is your resumable plan).`);
365
+ parts.push(`2. Save the output to \`<new-project-root>/MIGRATION.md\`.`);
366
+ parts.push(`3. Execute the Foundation checklist (global.css, enums, shared sub-components) in MIGRATION.md.`);
367
+ parts.push(`4. For each section: call \`get_section_migration_plan(theme_json, section_name)\`, implement it, and mark \`[x]\` in MIGRATION.md.`);
368
+ parts.push(`5. In any new session, **read MIGRATION.md first** and resume from the first \`[ ]\` item.\n`);
369
+ parts.push(`Read \`get_migration_guide("iterative-workflow")\` for the full protocol.`);
370
+ }
371
+ else {
372
+ parts.push(`This theme is small enough (${components.length} sections) for a one-pass migration.\n`);
373
+ parts.push(`1. Call \`get_migration_guide("migration-overview")\` for the full workflow`);
374
+ parts.push(`2. Call \`get_migration_guide("prop-type-mapping")\` for prop conversion details`);
375
+ parts.push(`3. Call \`get_migration_guide("custom-data-conversion")\` for CUSTOM → COMPONENT_LIST patterns`);
376
+ parts.push(`4. Call \`get_migration_guide("library-replacements")\` for replacing external libraries`);
377
+ }
378
+ parts.push(`\n## Other Key Guides\n`);
379
+ parts.push(`- \`get_migration_guide("component-renderer-limitations")\` — critical constraints when using COMPONENT_LIST (parent cannot read child props)`);
380
+ parts.push(`- \`get_migration_guide("prop-runtime-shapes")\` — exact runtime shape for each prop type (.data vs .links, etc.)`);
381
+ parts.push(`- \`get_migration_guide("link-prop-decision-guide")\` — when to use LINK vs LIST_OF_LINK vs COMPONENT_LIST`);
382
+ parts.push(`- \`get_framework_guide("common-pitfalls")\` — general gotchas (observer rules, .data access)`);
383
+ parts.push(`- \`get_framework_guide("component-renderer-patterns")\` — full IkasComponentRenderer usage`);
384
+ parts.push(`- \`get_model_guide("<TypeName>")\` — store type definitions (IkasCart, IkasProduct, etc.)`);
385
+ return parts.join("\n");
386
+ }
387
+ function scanSharedSubcomponents(sourceDir) {
388
+ if (!fs.existsSync(sourceDir))
389
+ return [];
390
+ const tsxFiles = [];
391
+ const walk = (dir) => {
392
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
393
+ if (entry.isDirectory()) {
394
+ if (entry.name === "node_modules" || entry.name === "__generated__" || entry.name.startsWith("."))
395
+ continue;
396
+ walk(path.join(dir, entry.name));
397
+ }
398
+ else if (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) {
399
+ const componentDir = path.basename(path.dirname(path.join(dir, entry.name)));
400
+ tsxFiles.push({ componentDir, filePath: path.join(dir, entry.name) });
401
+ }
402
+ }
403
+ };
404
+ try {
405
+ walk(sourceDir);
406
+ }
407
+ catch {
408
+ return [];
409
+ }
410
+ // Collect imports: map from imported path/name → { usingComponents: Set, rawImportPath: string }
411
+ const importUsage = new Map();
412
+ const importRegex = /import\s+(?:{[^}]+}|\w+(?:\s*,\s*{[^}]+})?)\s+from\s+['"]([^'"]+)['"]/g;
413
+ for (const { componentDir, filePath } of tsxFiles) {
414
+ let content;
415
+ try {
416
+ content = fs.readFileSync(filePath, "utf-8");
417
+ }
418
+ catch {
419
+ continue;
420
+ }
421
+ const seenInFile = new Set();
422
+ let match;
423
+ while ((match = importRegex.exec(content)) !== null) {
424
+ const importPath = match[1];
425
+ // Only count relative imports (shared internal components, not npm packages)
426
+ if (!importPath.startsWith("."))
427
+ continue;
428
+ // Skip imports of generated types, utils, hooks
429
+ if (importPath.includes("__generated__") || importPath.includes("/utils") || importPath.includes("/hooks"))
430
+ continue;
431
+ // Extract base name from path
432
+ const pathSegments = importPath.split("/").filter(s => s && s !== "." && s !== "..");
433
+ if (pathSegments.length === 0)
434
+ continue;
435
+ const lastSegment = pathSegments[pathSegments.length - 1];
436
+ if (!lastSegment || !/^[A-Z]/.test(lastSegment))
437
+ continue; // PascalCase only (component-like)
438
+ const key = lastSegment;
439
+ if (seenInFile.has(key))
440
+ continue;
441
+ seenInFile.add(key);
442
+ if (!importUsage.has(key)) {
443
+ importUsage.set(key, { usingComponents: new Set(), rawImportPath: importPath });
444
+ }
445
+ importUsage.get(key).usingComponents.add(componentDir);
446
+ }
447
+ }
448
+ // Filter to candidates used by 3+ distinct components
449
+ const shared = [];
450
+ for (const [name, { usingComponents, rawImportPath }] of importUsage) {
451
+ // Don't flag the component itself (e.g., Navbar imports from ../Navbar/something)
452
+ const users = [...usingComponents].filter(c => c !== name);
453
+ if (users.length >= 3) {
454
+ shared.push({ name, usedBy: users.sort(), importPaths: [rawImportPath] });
455
+ }
456
+ }
457
+ return shared.sort((a, b) => b.usedBy.length - a.usedBy.length);
458
+ }
459
+ // --- Migration plan helpers ---
460
+ function toKebabCase(s) {
461
+ return s
462
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
463
+ .replace(/[\s_]+/g, "-")
464
+ .toLowerCase()
465
+ .replace(/[^a-z0-9-]/g, "");
466
+ }
467
+ function classifyComplexity(comp, customDataMap) {
468
+ const props = comp.props || [];
469
+ const customCount = props.filter(p => p.type === "CUSTOM").length;
470
+ if (customCount === 0 && props.length < 10)
471
+ return "simple";
472
+ // Check for deeply nested CUSTOM (customData referencing another customData)
473
+ let hasDeepNesting = false;
474
+ for (const p of props) {
475
+ if (p.type === "CUSTOM" && p.customDataId) {
476
+ const cd = customDataMap.get(p.customDataId);
477
+ if (cd?.nestedData) {
478
+ const hasNested = (items) => {
479
+ for (const item of items) {
480
+ if (item.type === "DYNAMIC_LIST" || item.type === "STATIC_LIST" || item.customDataId)
481
+ return true;
482
+ if (item.nestedData && hasNested(item.nestedData))
483
+ return true;
484
+ }
485
+ return false;
486
+ };
487
+ if (hasNested(cd.nestedData)) {
488
+ hasDeepNesting = true;
489
+ break;
490
+ }
491
+ }
492
+ }
493
+ }
494
+ if (hasDeepNesting || props.length > 20 || customCount > 3)
495
+ return "complex";
496
+ return "medium";
497
+ }
498
+ function generateMigrationPlan(theme, projectName, oldSourceDir) {
499
+ const components = theme.components || [];
500
+ const customData = theme.customData || [];
501
+ const settings = theme.settings;
502
+ const customDataMap = new Map();
503
+ for (const cd of customData) {
504
+ if (cd.id)
505
+ customDataMap.set(cd.id, cd);
506
+ }
507
+ const sharedSubs = oldSourceDir ? scanSharedSubcomponents(oldSourceDir) : [];
508
+ const parts = [];
509
+ parts.push(`# Theme Migration Plan`);
510
+ parts.push("");
511
+ parts.push(`**Generated:** ${new Date().toISOString().slice(0, 10)}`);
512
+ parts.push(`**Project ID:** \`${projectName}\``);
513
+ parts.push(`**Source:** ${components.length} old components, ${customData.filter(cd => cd.isRoot).length} custom data types, ${(theme.pages || []).length} pages`);
514
+ parts.push("");
515
+ parts.push(`This file is the **source of truth** for the migration. All sessions must read it before starting work.`);
516
+ parts.push("");
517
+ parts.push(`## Status Legend`);
518
+ parts.push(`- \`[ ]\` Not started`);
519
+ parts.push(`- \`[~]\` In progress`);
520
+ parts.push(`- \`[x]\` Complete`);
521
+ parts.push("");
522
+ parts.push(`## Foundation`);
523
+ parts.push("");
524
+ // CSS variables
525
+ if (settings?.colors && settings.colors.length > 0) {
526
+ parts.push(`### Global CSS Variables (→ \`src/global.css\`)`);
527
+ parts.push("");
528
+ for (const c of settings.colors) {
529
+ parts.push(`- [ ] \`${c.key}: ${c.color};\` — ${c.displayName || "color"}`);
530
+ }
531
+ parts.push("");
532
+ }
533
+ // Fonts
534
+ if (settings?.fontFamily?.name) {
535
+ parts.push(`### Fonts (→ \`src/global.css\`)`);
536
+ parts.push("");
537
+ parts.push(`- [ ] ${settings.fontFamily.name} (weights: ${settings.fontFamily.variants?.join(", ") || "default"})`);
538
+ parts.push("");
539
+ }
540
+ // Custom Enums
541
+ const enumCustomData = customData.filter(cd => cd.type === "ENUM" && cd.isRoot);
542
+ if (enumCustomData.length > 0) {
543
+ parts.push(`### Custom Enums (run these BEFORE any \`config add-component\`)`);
544
+ parts.push("");
545
+ for (const cd of enumCustomData) {
546
+ const enumName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
547
+ const options = (cd.enumOptions || []).reduce((acc, o) => {
548
+ if (o.displayName && o.value)
549
+ acc[o.displayName] = o.value;
550
+ return acc;
551
+ }, {});
552
+ const optionsStr = JSON.stringify(options);
553
+ parts.push(`- [ ] **${enumName}**`);
554
+ parts.push(` \`\`\`bash`);
555
+ parts.push(` npx ikas-component config add-enum --name "${enumName}" --options '${optionsStr}'`);
556
+ parts.push(` \`\`\``);
557
+ parts.push(` Save the returned \`enumId\` and update this file with: \`enumId: <id>\``);
558
+ }
559
+ parts.push("");
560
+ }
561
+ // Shared sub-components
562
+ if (sharedSubs.length > 0) {
563
+ parts.push(`### Shared Sub-Components (→ \`src/sub-components/\`)`);
564
+ parts.push("");
565
+ 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.`);
566
+ parts.push("");
567
+ for (const sub of sharedSubs) {
568
+ parts.push(`- [ ] \`${sub.name}\` — used by: ${sub.usedBy.slice(0, 8).join(", ")}${sub.usedBy.length > 8 ? `, +${sub.usedBy.length - 8} more` : ""}`);
569
+ }
570
+ parts.push("");
571
+ }
572
+ else if (oldSourceDir) {
573
+ parts.push(`### Shared Sub-Components`);
574
+ parts.push("");
575
+ parts.push(`No shared sub-components detected (no relative imports reused across 3+ old component files).`);
576
+ parts.push("");
577
+ }
578
+ else {
579
+ parts.push(`### Shared Sub-Components`);
580
+ parts.push("");
581
+ 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._`);
582
+ parts.push("");
583
+ }
584
+ // Sections queue
585
+ parts.push(`## Sections`);
586
+ parts.push("");
587
+ parts.push(`Canonical component IDs are listed below — **use these exactly** when running \`config add-component\` and in \`filteredComponentIds\`. Do not change them mid-migration.`);
588
+ parts.push("");
589
+ const buckets = {
590
+ simple: [],
591
+ medium: [],
592
+ complex: [],
593
+ };
594
+ for (const comp of components) {
595
+ const complexity = classifyComplexity(comp, customDataMap);
596
+ buckets[complexity].push({ comp, complexity });
597
+ }
598
+ const labels = {
599
+ simple: "### Simple (migrate first)",
600
+ medium: "### Medium",
601
+ complex: "### Complex (migrate last)",
602
+ };
603
+ for (const key of ["simple", "medium", "complex"]) {
604
+ const items = buckets[key];
605
+ if (items.length === 0)
606
+ continue;
607
+ parts.push(labels[key]);
608
+ parts.push("");
609
+ for (const { comp } of items) {
610
+ const oldName = comp.displayName || comp.dir || comp.id || "Unknown";
611
+ const kebabName = toKebabCase(comp.dir || comp.displayName || comp.id || "unknown");
612
+ const newId = `${projectName}-${kebabName}`;
613
+ const headerFooter = comp.isHeader ? " **[HEADER]**" : comp.isFooter ? " **[FOOTER]**" : "";
614
+ const propCount = (comp.props || []).length;
615
+ // Detect children from CUSTOM DYNAMIC_LIST props
616
+ const children = [];
617
+ for (const p of comp.props || []) {
618
+ if (p.type === "CUSTOM" && p.customDataId) {
619
+ const cd = customDataMap.get(p.customDataId);
620
+ if (cd && (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST")) {
621
+ const itemObj = cd.nestedData?.[0];
622
+ if (itemObj) {
623
+ const childName = itemObj.typescriptName || (itemObj.name ? itemObj.name.replace(/[^a-zA-Z0-9]/g, "") : `${oldName}Item`);
624
+ const childKebab = toKebabCase(childName);
625
+ const childId = `${projectName}-${childKebab}`;
626
+ const fields = (itemObj.nestedData || []).map((f) => f.key || f.name || "?").slice(0, 8);
627
+ children.push({ propName: p.name || "?", childId, childName, fields });
628
+ }
629
+ }
630
+ }
631
+ }
632
+ parts.push(`- [ ] \`${newId}\` — **${oldName}**${headerFooter} (${propCount} props)`);
633
+ parts.push(` - Old dir: \`${comp.dir || "?"}\``);
634
+ if (children.length > 0) {
635
+ parts.push(` - Children:`);
636
+ for (const ch of children) {
637
+ parts.push(` - [ ] \`${ch.childId}\` (\`${ch.childName}\`) — for prop \`${ch.propName}\` — fields: ${ch.fields.join(", ")}`);
638
+ }
639
+ }
640
+ }
641
+ parts.push("");
642
+ }
643
+ // Known Environmental Issues (agents fill in during work)
644
+ parts.push(`## Known Environmental Issues`);
645
+ parts.push("");
646
+ parts.push(`_Agents working on this migration: record any non-component build/TS errors here so future sessions don't waste time diagnosing them._`);
647
+ parts.push("");
648
+ parts.push(`- [ ] _(none recorded yet)_`);
649
+ parts.push("");
650
+ // Usage section
651
+ parts.push(`## Per-Section Usage`);
652
+ parts.push("");
653
+ parts.push(`Once the Foundation is complete, for each section above:`);
654
+ parts.push("");
655
+ parts.push(`1. Find the first unchecked \`[ ]\` section (start with Simple).`);
656
+ parts.push(`2. Call \`get_section_migration_plan(theme_json, "<old section name>", "${projectName}")\`.`);
657
+ parts.push(`3. Read the old source files listed in the plan.`);
658
+ parts.push(`4. Run the CLI commands in the plan (they create parent + children with auto-generated types.ts).`);
659
+ parts.push(`5. Write \`index.tsx\` and \`styles.css\` using the patterns in the plan. DO NOT manually edit types.ts.`);
660
+ parts.push(`6. Mark the section \`[x]\` when it builds cleanly. Append a short note to the **Session Log** below (what libraries you replaced, any ad-hoc props you had to add, the actual folder name the CLI created if different from the name you passed).`);
661
+ parts.push("");
662
+ // Session Log — agents append notes here
663
+ parts.push(`## Session Log`);
664
+ parts.push("");
665
+ parts.push(`_Each agent: append a bullet after completing work. Format: \`- [YYYY-MM-DD] <section-id>: <brief note>\`. This is how future sessions learn about decisions not encoded in the plan structure._`);
666
+ parts.push("");
667
+ parts.push(`_After completing Foundation, also record which sub-components were built and which were skipped, e.g.:_`);
668
+ parts.push(`<!-- \`- [2026-04-15] Foundation: built Input, SubmitButton, Modal, SectionHeader, StarRating. Skipped: GoogleCaptcha (needs investigation), BlogCard, Pagination (low priority).\` -->`);
669
+ parts.push(`<!-- \`- [2026-04-15] ${projectName}-footer: swapped react-hot-toast for inline status text; newsletter uses form-model pattern (getNewsletterSubscriptionForm). CLI created folder as "Footer/" as expected.\` -->`);
670
+ parts.push("");
671
+ // Cross-references
672
+ parts.push(`## Cross-References`);
673
+ parts.push("");
674
+ parts.push(`Essential MCP tools to call during migration:`);
675
+ parts.push("");
676
+ parts.push(`- \`get_migration_guide("iterative-workflow")\` — full protocol for resumable migrations`);
677
+ parts.push(`- \`get_migration_guide("component-renderer-limitations")\` — critical constraints when using COMPONENT_LIST`);
678
+ parts.push(`- \`get_migration_guide("prop-runtime-shapes")\` — \`.data\` vs \`.links\`, \`.variant\` vs \`.product\`, etc.`);
679
+ parts.push(`- \`get_migration_guide("link-prop-decision-guide")\` — LINK vs LIST_OF_LINK vs COMPONENT_LIST`);
680
+ parts.push(`- \`get_migration_guide("library-replacements")\` — swiper, headlessui, tailwind, etc.`);
681
+ parts.push(`- \`get_migration_guide("react-to-preact")\` — observer rules, imports, IkasSlider removal`);
682
+ parts.push(`- \`get_framework_guide("component-renderer-patterns")\` — full IkasComponentRenderer usage`);
683
+ parts.push(`- \`get_framework_guide("common-pitfalls")\` — general gotchas`);
684
+ parts.push(`- \`get_framework_guide("navigation-patterns")\` — Router.navigate, Router.navigateToPage`);
685
+ parts.push(`- \`get_model_guide("<TypeName>")\` — IkasCart, IkasProduct, IkasCustomer shapes`);
686
+ parts.push("");
687
+ return parts.join("\n");
688
+ }
689
+ // Known libraries we detect in old themes and want to flag for replacement
690
+ const KNOWN_LIBRARIES = [
691
+ "swiper", "@headlessui/react", "@heroicons/react", "recharts",
692
+ "react-player", "react-simple-star-rating", "react-slider", "react-compound-slider",
693
+ "react-zoom-pan-pinch", "react-hot-toast", "react-fast-marquee",
694
+ "react-indiana-drag-scroll", "react-simple-typewriter", "react-timer-hook",
695
+ "date-fns", "slugify", "classnames", "clsx", "@react-pdf/renderer",
696
+ ];
697
+ // Heuristic: member-access patterns on old storefront stores/singletons that likely need new-system equivalents
698
+ const OLD_STOREFRONT_CALL_REGEX = /\b(customerStore|cartStore|productStore|categoryStore|orderStore|searchStore|favoritesStore|i18nStore|Router|useStore)\.\w+/g;
699
+ function scanSectionSource(componentDir, propNames) {
700
+ if (!fs.existsSync(componentDir))
701
+ return null;
702
+ const result = {
703
+ sourceFiles: [],
704
+ importedSubComponents: [],
705
+ importedLibraries: [],
706
+ importedUnknownLibraries: [],
707
+ propFieldUsage: {},
708
+ oldStorefrontCalls: [],
709
+ reactPackageUsage: [],
710
+ };
711
+ // Collect all .tsx/.ts files in the component dir
712
+ try {
713
+ for (const entry of fs.readdirSync(componentDir, { withFileTypes: true })) {
714
+ if (entry.isFile() && (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
715
+ result.sourceFiles.push(path.join(componentDir, entry.name));
716
+ }
717
+ }
718
+ }
719
+ catch {
720
+ return null;
721
+ }
722
+ if (result.sourceFiles.length === 0)
723
+ return result;
724
+ const importRegex = /import\s+(?:type\s+)?(?:\{[^}]+\}|\w+(?:\s*,\s*\{[^}]+\})?)\s+from\s+['"]([^'"]+)['"]/g;
725
+ const libSet = new Set();
726
+ const unknownLibSet = new Set();
727
+ const reactSet = new Set();
728
+ const subCompSet = new Map();
729
+ const callSet = new Set();
730
+ // Packages we treat as "framework" and don't flag for replacement (but do note in reactPackageUsage)
731
+ const REACT_PACKAGES = new Set(["react", "react-dom", "next", "next/link", "next/image", "next/router", "next/head", "next/script", "mobx-react-lite", "mobx"]);
732
+ for (const file of result.sourceFiles) {
733
+ let content;
734
+ try {
735
+ content = fs.readFileSync(file, "utf-8");
736
+ }
737
+ catch {
738
+ continue;
739
+ }
740
+ // Parse imports
741
+ let m;
742
+ while ((m = importRegex.exec(content)) !== null) {
743
+ const p = m[1];
744
+ if (p.startsWith(".")) {
745
+ const segs = p.split("/").filter(s => s && s !== "." && s !== "..");
746
+ const last = segs[segs.length - 1];
747
+ if (last && /^[A-Z]/.test(last) && !last.includes("__generated__")) {
748
+ subCompSet.set(last, p);
749
+ }
750
+ }
751
+ else if (!p.startsWith("@ikas/")) {
752
+ // Classify: known library, react-family, or unknown-external
753
+ const base = p.startsWith("@") ? p.split("/").slice(0, 2).join("/") : p.split("/")[0];
754
+ if (REACT_PACKAGES.has(p) || REACT_PACKAGES.has(base)) {
755
+ reactSet.add(base);
756
+ }
757
+ else {
758
+ let matched = false;
759
+ for (const lib of KNOWN_LIBRARIES) {
760
+ if (p === lib || p.startsWith(lib + "/")) {
761
+ libSet.add(lib);
762
+ matched = true;
763
+ break;
764
+ }
765
+ }
766
+ if (!matched) {
767
+ unknownLibSet.add(base);
768
+ }
769
+ }
770
+ }
771
+ }
772
+ // Generic: detect any member call on old storefront stores/singletons
773
+ let cm;
774
+ OLD_STOREFRONT_CALL_REGEX.lastIndex = 0;
775
+ while ((cm = OLD_STOREFRONT_CALL_REGEX.exec(content)) !== null) {
776
+ callSet.add(cm[0]);
777
+ }
778
+ // Detect field access for each prop
779
+ for (const propName of propNames) {
780
+ if (!propName)
781
+ continue;
782
+ // Look for patterns like: propName.map((item) => ... item.FIELD ...)
783
+ // or: propName[0].FIELD, propName.FIELD, destructure: const { field } = propName
784
+ const mapRegex = new RegExp(`\\b${propName}(?:\\?)?\\.(?:map|forEach|filter|find|reduce)\\s*\\(\\s*\\(?(\\w+)`, "g");
785
+ let mm;
786
+ const itemVars = new Set();
787
+ while ((mm = mapRegex.exec(content)) !== null) {
788
+ itemVars.add(mm[1]);
789
+ }
790
+ // Also detect destructuring in map callback: .map(({ title, image }) => ...)
791
+ const destructRegex = new RegExp(`\\b${propName}(?:\\?)?\\.(?:map|forEach|filter|find|reduce)\\s*\\(\\s*\\(?\\s*\\{([^}]+)\\}`, "g");
792
+ const fields = new Set();
793
+ while ((mm = destructRegex.exec(content)) !== null) {
794
+ for (const f of mm[1].split(",")) {
795
+ const name = f.trim().split(/[:=]/)[0].trim();
796
+ if (name && /^\w+$/.test(name))
797
+ fields.add(name);
798
+ }
799
+ }
800
+ // Scan for itemVar.fieldName access
801
+ for (const itemVar of itemVars) {
802
+ const fieldRegex = new RegExp(`\\b${itemVar}(?:\\?)?\\.(\\w+)`, "g");
803
+ while ((mm = fieldRegex.exec(content)) !== null) {
804
+ const f = mm[1];
805
+ // Exclude TS/JS keywords and built-ins
806
+ if (!/^(map|forEach|filter|find|reduce|length|toString|valueOf|constructor|prototype|then|catch|finally)$/.test(f)) {
807
+ fields.add(f);
808
+ }
809
+ }
810
+ }
811
+ if (fields.size > 0) {
812
+ result.propFieldUsage[propName] = [...fields].sort();
813
+ }
814
+ }
815
+ }
816
+ result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({ name, path: p }));
817
+ result.importedLibraries = [...libSet].sort();
818
+ result.importedUnknownLibraries = [...unknownLibSet].sort();
819
+ result.reactPackageUsage = [...reactSet].sort();
820
+ result.oldStorefrontCalls = [...callSet].sort();
821
+ return result;
822
+ }
823
+ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSourceDir) {
824
+ const components = theme.components || [];
825
+ const customData = theme.customData || [];
826
+ const customDataMap = new Map();
827
+ for (const cd of customData) {
828
+ if (cd.id)
829
+ customDataMap.set(cd.id, cd);
830
+ }
831
+ // Find the component — try match by dir, displayName, id, or new-id
832
+ const target = components.find(c => {
833
+ if (!c)
834
+ return false;
835
+ if (c.dir === sectionName || c.displayName === sectionName || c.id === sectionName)
836
+ return true;
837
+ const kebab = toKebabCase(c.dir || c.displayName || c.id || "");
838
+ const newId = `${projectName}-${kebab}`;
839
+ return newId === sectionName;
840
+ });
841
+ if (!target) {
842
+ const available = components.map(c => c.dir || c.displayName || c.id).filter(Boolean).join(", ");
843
+ return `Section "${sectionName}" not found in theme. Available: ${available}`;
844
+ }
845
+ const parts = [];
846
+ const oldName = target.displayName || target.dir || target.id || "Unknown";
847
+ const kebabName = toKebabCase(target.dir || target.displayName || target.id || "unknown");
848
+ const sectionId = `${projectName}-${kebabName}`;
849
+ const sectionPascal = (target.dir || target.displayName || "").replace(/[^a-zA-Z0-9]/g, "") || kebabName.split("-").map(s => s[0]?.toUpperCase() + s.slice(1)).join("");
850
+ // Scan the old source for imports, libraries, field usage
851
+ const propNames = (target.props || []).map(p => p.name || "").filter(Boolean);
852
+ const sourceScan = (oldSourceDir && target.dir)
853
+ ? scanSectionSource(path.join(oldSourceDir, target.dir), propNames)
854
+ : null;
855
+ parts.push(`# Section Migration Plan: ${oldName}`);
856
+ parts.push("");
857
+ parts.push(`**Old name:** \`${oldName}\` (dir: \`${target.dir || "?"}\`)`);
858
+ parts.push(`**New section ID:** \`${sectionId}\``);
859
+ parts.push(`**New section name:** \`${sectionPascal}\``);
860
+ if (target.isHeader)
861
+ parts.push(`**Flags:** HEADER (\`isHeader: true\`)`);
862
+ if (target.isFooter)
863
+ parts.push(`**Flags:** FOOTER (\`isFooter: true\`)`);
864
+ parts.push("");
865
+ 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.`);
866
+ parts.push("");
867
+ // Source files
868
+ parts.push(`## 1. Old Source Files to Read`);
869
+ parts.push("");
870
+ if (sourceScan && sourceScan.sourceFiles.length > 0) {
871
+ parts.push(`Read these files from the old project (detected by scanning the directory):`);
872
+ for (const f of sourceScan.sourceFiles) {
873
+ parts.push(`- \`${f}\``);
874
+ }
875
+ if (sourceScan.importedSubComponents.length > 0) {
876
+ parts.push("");
877
+ parts.push(`**Imported sub-components** (relative PascalCase imports found in the source — read these too if you need their behavior):`);
878
+ for (const sub of sourceScan.importedSubComponents) {
879
+ parts.push(`- \`${sub.name}\` (import path: \`${sub.path}\`)`);
880
+ }
881
+ }
882
+ if (sourceScan.importedLibraries.length > 0) {
883
+ parts.push("");
884
+ parts.push(`**External libraries (recognized) — MUST be replaced with vanilla Preact + CSS:**`);
885
+ for (const lib of sourceScan.importedLibraries) {
886
+ parts.push(`- \`${lib}\` — see \`get_migration_guide("library-replacements")\``);
887
+ }
888
+ }
889
+ if (sourceScan.importedUnknownLibraries.length > 0) {
890
+ parts.push("");
891
+ parts.push(`**External libraries (unrecognized) — verify whether a replacement is needed:**`);
892
+ for (const lib of sourceScan.importedUnknownLibraries) {
893
+ 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")\`.`);
894
+ }
895
+ }
896
+ if (sourceScan.reactPackageUsage.length > 0) {
897
+ parts.push("");
898
+ 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).`);
899
+ }
900
+ if (sourceScan.oldStorefrontCalls.length > 0) {
901
+ parts.push("");
902
+ parts.push(`**Old-system API calls detected — search for new-system equivalents:**`);
903
+ for (const call of sourceScan.oldStorefrontCalls) {
904
+ parts.push(`- \`${call}\``);
905
+ }
906
+ parts.push("");
907
+ 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")\`:`);
908
+ parts.push(`1. Identify the intent (e.g. "login", "add to cart", "newsletter signup", "navigate")`);
909
+ parts.push(`2. \`search_docs("<intent keywords>")\` to find candidates`);
910
+ parts.push(`3. \`list_functions("<category>")\` (Form, Cart, Customer, Navigation, ...) to see all options`);
911
+ parts.push(`4. \`get_function_doc("<name>")\` / \`get_model_guide("<Type>")\` for exact signatures`);
912
+ parts.push(`5. \`get_code_example("<task>")\` for working examples`);
913
+ }
914
+ }
915
+ else if (oldSourceDir && target.dir) {
916
+ const compDir = path.join(oldSourceDir, target.dir);
917
+ parts.push(`Read these files from the old project (if they exist):`);
918
+ parts.push(`- \`${compDir}/index.tsx\` — main component implementation`);
919
+ parts.push(`- \`${compDir}/*.tsx\` — any nested sub-components`);
920
+ parts.push(`- \`${compDir}/*.css\` / \`*.scss\` — styles`);
921
+ }
922
+ else {
923
+ parts.push(`Read the old component at \`src/components/${target.dir || oldName}/index.tsx\` and any files in that directory.`);
924
+ }
925
+ parts.push("");
926
+ // Prop conversion table
927
+ parts.push(`## 2. Prop Conversion Table`);
928
+ parts.push("");
929
+ parts.push(`| Old prop | Old type | → | New prop | New type | Notes |`);
930
+ parts.push(`|----------|----------|---|----------|----------|-------|`);
931
+ const children = [];
932
+ const enumsNeeded = [];
933
+ const librariesDetected = new Set();
934
+ const parentPropsJson = [];
935
+ for (const p of target.props || []) {
936
+ const oldName = p.name || "?";
937
+ const oldType = p.type || "?";
938
+ let newName = oldName;
939
+ let newType = oldType;
940
+ let notes = "Direct";
941
+ if (oldType === "SLIDER") {
942
+ newType = "NUMBER";
943
+ notes = `Was SLIDER(min=${p.sliderData?.min}, max=${p.sliderData?.max}) — replace \`.value\` access with direct number`;
944
+ const prop = { name: newName, displayName: p.displayName || newName, type: "NUMBER" };
945
+ if (p.isRequired)
946
+ prop.required = true;
947
+ parentPropsJson.push(prop);
948
+ }
949
+ else if (oldType === "PRODUCT_DETAIL") {
950
+ newType = "PRODUCT";
951
+ notes = "Renamed — PRODUCT_DETAIL → PRODUCT";
952
+ const prop = { name: newName, displayName: p.displayName || newName, type: "PRODUCT" };
953
+ if (p.isRequired)
954
+ prop.required = true;
955
+ parentPropsJson.push(prop);
956
+ }
957
+ else if (oldType === "CUSTOM" && p.customDataId) {
958
+ const cd = customDataMap.get(p.customDataId);
959
+ if (cd) {
960
+ if (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST") {
961
+ // Child component needed
962
+ const itemObj = cd.nestedData?.[0];
963
+ const childName = itemObj?.typescriptName || (itemObj?.name ? itemObj.name.replace(/[^a-zA-Z0-9]/g, "") : `${sectionPascal}Item`);
964
+ const childKebab = toKebabCase(childName);
965
+ const childId = `${projectName}-${childKebab}`;
966
+ const childProps = [];
967
+ const nestedWarnings = [];
968
+ for (const f of (itemObj?.nestedData || [])) {
969
+ if (!f.key)
970
+ continue;
971
+ let fType = f.type;
972
+ if (fType === "SLIDER")
973
+ fType = "NUMBER";
974
+ else if (fType === "PRODUCT_DETAIL")
975
+ fType = "PRODUCT";
976
+ else if (fType === "CUSTOM" || fType === "DYNAMIC_LIST" || fType === "STATIC_LIST" || fType === "OBJECT") {
977
+ nestedWarnings.push(`\`${f.key}\` (${fType})`);
978
+ fType = "COMPONENT_LIST";
979
+ }
980
+ const prop = { name: f.key, displayName: f.name || f.key, type: fType };
981
+ if (f.isRequired)
982
+ prop.required = true;
983
+ childProps.push(prop);
984
+ }
985
+ if (nestedWarnings.length > 0) {
986
+ notes = `⚠️ Child has nested structures (${nestedWarnings.join(", ")}) — these need their OWN child components. After creating this child, decompose each nested structure recursively.`;
987
+ }
988
+ // If customData is empty/incomplete, try to infer fields from source usage
989
+ if (childProps.length === 0 && sourceScan?.propFieldUsage[oldName]) {
990
+ const inferred = sourceScan.propFieldUsage[oldName];
991
+ for (const fieldName of inferred) {
992
+ childProps.push({ name: fieldName, displayName: fieldName, type: "TEXT" });
993
+ }
994
+ if (inferred.length > 0) {
995
+ 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.`;
996
+ }
997
+ }
998
+ children.push({
999
+ propName: oldName,
1000
+ childId,
1001
+ childName,
1002
+ childPropsJson: childProps,
1003
+ customDataName: cd.name || "unnamed",
1004
+ });
1005
+ newType = "COMPONENT_LIST";
1006
+ notes = `Was CUSTOM → ${cd.type} of ${itemObj?.typescriptName || "object"}. Create child component \`${childId}\`.`;
1007
+ const prop = {
1008
+ name: newName,
1009
+ displayName: p.displayName || newName,
1010
+ type: "COMPONENT_LIST",
1011
+ filteredComponentIds: [childId],
1012
+ };
1013
+ parentPropsJson.push(prop);
1014
+ }
1015
+ else if (cd.type === "OBJECT") {
1016
+ const fieldCount = (cd.nestedData || []).length;
1017
+ if (fieldCount <= 3) {
1018
+ notes = `Was CUSTOM (OBJECT, ${fieldCount} fields) — **flatten into direct props** (${(cd.nestedData || []).map((f) => f.key || f.name).join(", ")})`;
1019
+ for (const f of (cd.nestedData || [])) {
1020
+ if (!f.key)
1021
+ continue;
1022
+ let fType = f.type;
1023
+ if (fType === "SLIDER")
1024
+ fType = "NUMBER";
1025
+ const prop = { name: f.key, displayName: f.name || f.key, type: fType };
1026
+ if (f.isRequired)
1027
+ prop.required = true;
1028
+ parentPropsJson.push(prop);
1029
+ }
1030
+ newType = "(flattened)";
1031
+ newName = (cd.nestedData || []).map((f) => f.key).join(", ");
1032
+ }
1033
+ else {
1034
+ newType = "COMPONENT_LIST";
1035
+ notes = `Was CUSTOM (OBJECT, ${fieldCount} fields) — too complex to flatten, use COMPONENT_LIST with single child`;
1036
+ }
1037
+ }
1038
+ else if (cd.type === "ENUM") {
1039
+ newType = "ENUM";
1040
+ const enumName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
1041
+ const options = (cd.enumOptions || []).reduce((acc, o) => {
1042
+ if (o.displayName && o.value)
1043
+ acc[o.displayName] = o.value;
1044
+ return acc;
1045
+ }, {});
1046
+ enumsNeeded.push({ name: enumName, options });
1047
+ notes = `Was CUSTOM (ENUM) — create enum \`${enumName}\` via \`config add-enum\` first, then reference its enumId here`;
1048
+ const prop = { name: newName, displayName: p.displayName || newName, type: "ENUM", enumTypeId: `<ENUM_ID_FROM_add-enum_${enumName}>` };
1049
+ if (p.isRequired)
1050
+ prop.required = true;
1051
+ parentPropsJson.push(prop);
1052
+ }
1053
+ }
1054
+ }
1055
+ else {
1056
+ // Direct mapping
1057
+ const prop = { name: newName, displayName: p.displayName || newName, type: newType };
1058
+ if (p.isRequired)
1059
+ prop.required = true;
1060
+ parentPropsJson.push(prop);
1061
+ }
1062
+ parts.push(`| \`${oldName}\` | ${oldType} | → | \`${newName}\` | ${newType} | ${notes} |`);
1063
+ }
1064
+ parts.push("");
1065
+ // Enums to create first
1066
+ if (enumsNeeded.length > 0) {
1067
+ parts.push(`## 3. Create Enums FIRST (if not already done)`);
1068
+ parts.push("");
1069
+ for (const e of enumsNeeded) {
1070
+ parts.push(`\`\`\`bash`);
1071
+ parts.push(`npx ikas-component config add-enum --name "${e.name}" --options '${JSON.stringify(e.options)}'`);
1072
+ parts.push(`\`\`\``);
1073
+ }
1074
+ parts.push(`Save the returned \`enumId\` values and substitute them in the parent config JSON below.`);
1075
+ parts.push("");
1076
+ }
1077
+ // Child CLI commands — dedupe by childId (same shape may be referenced by multiple parent props)
1078
+ const uniqueChildren = new Map();
1079
+ for (const ch of children) {
1080
+ const existing = uniqueChildren.get(ch.childId);
1081
+ if (existing) {
1082
+ existing.usedByProps.push(ch.propName);
1083
+ }
1084
+ else {
1085
+ uniqueChildren.set(ch.childId, { child: ch, usedByProps: [ch.propName] });
1086
+ }
1087
+ }
1088
+ if (uniqueChildren.size > 0) {
1089
+ parts.push(`## ${enumsNeeded.length > 0 ? "4" : "3"}. Create Child Components FIRST`);
1090
+ parts.push("");
1091
+ parts.push(`These children are referenced by the parent's COMPONENT_LIST props. Create them before the parent.`);
1092
+ parts.push(`**Deduped:** run each \`add-component\` command ONCE even if multiple parent props reference the same child.`);
1093
+ parts.push("");
1094
+ for (const { child: ch, usedByProps } of uniqueChildren.values()) {
1095
+ parts.push(`### \`${ch.childId}\` (\`${ch.childName}\`)`);
1096
+ const propsLabel = usedByProps.length > 1
1097
+ ? `Used by parent props: ${usedByProps.map(p => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
1098
+ : `For parent prop: \`${usedByProps[0]}\``;
1099
+ parts.push(propsLabel);
1100
+ parts.push(`Old customData: "${ch.customDataName}"`);
1101
+ parts.push("");
1102
+ parts.push(`\`\`\`bash`);
1103
+ parts.push(`npx ikas-component config add-component --name "${ch.childName}" --type component --props '${JSON.stringify(ch.childPropsJson)}'`);
1104
+ parts.push(`\`\`\``);
1105
+ parts.push("");
1106
+ }
1107
+ }
1108
+ // Parent CLI
1109
+ const parentStep = 3 + (enumsNeeded.length > 0 ? 1 : 0) + (children.length > 0 ? 1 : 0);
1110
+ parts.push(`## ${parentStep}. Create Parent Section`);
1111
+ parts.push("");
1112
+ const isHeaderFlag = target.isHeader ? " --isHeader" : "";
1113
+ const isFooterFlag = target.isFooter ? " --isFooter" : "";
1114
+ parts.push(`\`\`\`bash`);
1115
+ parts.push(`npx ikas-component config add-component --name "${sectionPascal}" --type section${isHeaderFlag}${isFooterFlag} --props '${JSON.stringify(parentPropsJson)}'`);
1116
+ parts.push(`\`\`\``);
1117
+ parts.push("");
1118
+ parts.push(`**IMPORTANT:** The CLI auto-generates \`types.ts\`. DO NOT manually create or edit \`types.ts\`.`);
1119
+ parts.push("");
1120
+ // Implementation guidance
1121
+ const nextStep = parentStep + 1;
1122
+ parts.push(`## ${nextStep}. Write \`index.tsx\` and \`styles.css\``);
1123
+ parts.push("");
1124
+ parts.push(`After running the CLI commands, write the section's \`index.tsx\`. Key rules:`);
1125
+ parts.push("");
1126
+ parts.push(`- **BOTH named + default export** (the CLI barrel file uses named imports — without both, build fails):`);
1127
+ parts.push(` \`\`\`export function ${sectionPascal}(props: Props) { ... } export default ${sectionPascal};\`\`\``);
1128
+ parts.push(`- No \`observer()\` on root sections — only on sub-components`);
1129
+ parts.push(`- Import from \`@ikas/bp-storefront\` (NOT \`@ikas/storefront\`)`);
1130
+ parts.push(`- Use \`getDefaultSrc(image)\` + native \`<img>\` instead of the old \`<Image>\` component`);
1131
+ parts.push(`- Replace \`IkasSlider\` usage (\`.value\` access) with plain \`number\``);
1132
+ if (children.length > 0) {
1133
+ parts.push(`- For COMPONENT_LIST props, use \`<IkasComponentRenderer id="..." components={list as any[]} parentProps={props} />\``);
1134
+ parts.push(`- **Remember: the parent cannot read child prop values.** Any per-item logic must live in the child component.`);
1135
+ 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 from a previous migration (reuse its ID in \`filteredComponentIds\`). If not, create it as a registered component.`);
1136
+ }
1137
+ // Check if the section itself has data-driven list props (PRODUCT_LIST, BLOG_LIST, CATEGORY_LIST)
1138
+ const dataListProps = (target.props || []).filter(p => p.type === "PRODUCT_LIST" || p.type === "BLOG_LIST" || p.type === "CATEGORY_LIST" || p.type === "BRAND_LIST");
1139
+ if (dataListProps.length > 0) {
1140
+ parts.push("");
1141
+ parts.push(`### Data-Driven List Rendering`);
1142
+ parts.push("");
1143
+ parts.push(`This section has data-driven list props: ${dataListProps.map(p => `\`${p.name}\` (${p.type})`).join(", ")}.`);
1144
+ 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:`);
1145
+ parts.push("");
1146
+ parts.push("```tsx");
1147
+ for (const p of dataListProps) {
1148
+ if (p.type === "PRODUCT_LIST") {
1149
+ parts.push(`// ${p.name}: PRODUCT_LIST — map over .data to render each product`);
1150
+ parts.push(`{${p.name}?.data?.map((product, i) => (`);
1151
+ parts.push(` <ProductCard key={i} product={product} />`);
1152
+ parts.push(`))}`);
1153
+ }
1154
+ else if (p.type === "BLOG_LIST") {
1155
+ parts.push(`// ${p.name}: BLOG_LIST — map over .data to render each blog post`);
1156
+ parts.push(`{${p.name}?.data?.map((blog, i) => (`);
1157
+ parts.push(` <BlogCard key={i} blog={blog} />`);
1158
+ parts.push(`))}`);
1159
+ }
1160
+ else {
1161
+ parts.push(`// ${p.name}: ${p.type} — check shape with get_model_guide, then map over items`);
1162
+ }
1163
+ }
1164
+ parts.push("```");
1165
+ parts.push("");
1166
+ 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.`);
1167
+ parts.push(`See \`get_migration_guide("custom-data-conversion")\` → "Two Ways to Render Lists" for the full pattern.`);
1168
+ }
1169
+ // Detect form-page sections (0 or few props, name suggests a form/auth page)
1170
+ const formKeywords = ["login", "register", "forgot", "recover", "password", "account", "email", "verification", "activate", "contact", "checkout", "address"];
1171
+ const lowerDir = (target.dir || "").toLowerCase();
1172
+ const isLikelyFormPage = formKeywords.some(kw => lowerDir.includes(kw));
1173
+ if (isLikelyFormPage) {
1174
+ parts.push("");
1175
+ parts.push(`### Form Page Pattern`);
1176
+ parts.push("");
1177
+ 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.`);
1178
+ 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")\`)`);
1179
+ parts.push(`- Call \`get_framework_guide("form-handling")\` for the general form-model pattern`);
1180
+ 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")\`)`);
1181
+ parts.push(`- Typical pattern: \`get<Feature>Form()\` → \`set<Feature>Form<Field>(form, value)\` → \`submit<Feature>Form(form)\``);
1182
+ parts.push(`- Replace \`react-hot-toast\` feedback with inline status messages using TEXT props`);
1183
+ }
1184
+ parts.push("");
1185
+ // Library detection — prefer source scan, fall back to heuristic
1186
+ if (sourceScan && sourceScan.importedLibraries.length > 0) {
1187
+ parts.push(`### Library Replacements Required (detected in source)`);
1188
+ parts.push("");
1189
+ parts.push(`These libraries are confirmed imports in the old source. Each MUST be replaced with vanilla Preact + CSS:`);
1190
+ for (const lib of sourceScan.importedLibraries) {
1191
+ parts.push(`- \`${lib}\``);
1192
+ }
1193
+ parts.push("");
1194
+ parts.push(`Call \`get_migration_guide("library-replacements")\` for the specific replacement pattern for each.`);
1195
+ parts.push("");
1196
+ }
1197
+ else if (!sourceScan) {
1198
+ // Fallback heuristic only when source scan unavailable
1199
+ const heuristicLibs = [];
1200
+ const lowerName = oldName.toLowerCase();
1201
+ if (lowerName.includes("slider") || lowerName.includes("carousel") || lowerName.includes("banner")) {
1202
+ heuristicLibs.push("swiper");
1203
+ }
1204
+ if (lowerName.includes("marquee"))
1205
+ heuristicLibs.push("react-fast-marquee");
1206
+ if (lowerName.includes("video") || lowerName.includes("player"))
1207
+ heuristicLibs.push("react-player");
1208
+ if (lowerName.includes("chart"))
1209
+ heuristicLibs.push("recharts");
1210
+ if (lowerName.includes("star") || lowerName.includes("rating") || lowerName.includes("review"))
1211
+ heuristicLibs.push("react-simple-star-rating");
1212
+ if (heuristicLibs.length > 0) {
1213
+ parts.push(`### Likely Library Replacements (heuristic — source not scanned)`);
1214
+ parts.push("");
1215
+ parts.push(`No \`old_source_dir\` was provided so libraries cannot be confirmed. Based on the component name, the old version may use:`);
1216
+ for (const lib of heuristicLibs) {
1217
+ parts.push(`- \`${lib}\``);
1218
+ }
1219
+ parts.push("");
1220
+ parts.push(`Verify by reading the old \`index.tsx\`, then call \`get_migration_guide("library-replacements")\`.`);
1221
+ parts.push("");
1222
+ }
1223
+ }
1224
+ // Relevant guides
1225
+ parts.push(`## ${nextStep + 1}. Relevant Guides (call these for details)`);
1226
+ parts.push("");
1227
+ parts.push(`- \`get_migration_guide("react-to-preact")\` — code conversion patterns`);
1228
+ parts.push(`- \`get_migration_guide("library-replacements")\` — library → vanilla Preact patterns`);
1229
+ parts.push(`- \`get_migration_guide("prop-runtime-shapes")\` — exact runtime shapes (.data vs .links)`);
1230
+ if (children.length > 0) {
1231
+ parts.push(`- \`get_migration_guide("component-renderer-limitations")\` — critical COMPONENT_LIST constraints`);
1232
+ parts.push(`- \`get_framework_guide("component-renderer-patterns")\` — full IkasComponentRenderer usage`);
1233
+ parts.push(`- \`get_migration_example("custom-dynamic-list-to-component-list")\` — concrete example`);
1234
+ }
1235
+ if (target.isHeader || target.isFooter) {
1236
+ parts.push(`- \`get_framework_guide("header-footer-patterns")\` — header/footer specifics`);
1237
+ }
1238
+ parts.push(`- \`get_framework_guide("common-pitfalls")\` — observer rules, common mistakes`);
1239
+ parts.push("");
1240
+ // Completion
1241
+ parts.push(`## ${nextStep + 2}. Mark Complete`);
1242
+ parts.push("");
1243
+ parts.push(`Once the section builds cleanly with \`npx ikas-component build\`:`);
1244
+ parts.push(`1. Edit \`MIGRATION.md\` at the project root`);
1245
+ parts.push(`2. Change the checkbox for \`${sectionId}\` from \`[ ]\` to \`[x]\``);
1246
+ parts.push(`3. Also mark each child component as \`[x]\``);
1247
+ parts.push(`4. Append an entry to the **Session Log** section of \`MIGRATION.md\` noting: libraries replaced, any props added beyond the generated plan, the actual folder name the CLI created (in case it differs from the name you passed — e.g., \`FAQ\` → \`Faq/\`), and any other decisions a future agent should know.`);
1248
+ parts.push("");
1249
+ return parts.join("\n");
1250
+ }
162
1251
  // --- Search helpers ---
163
1252
  function matchScore(text, query) {
164
1253
  const lower = text.toLowerCase();
@@ -334,10 +1423,11 @@ const server = new McpServer({
334
1423
  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
1424
  });
336
1425
  // 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 }) => {
1426
+ 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
1427
  const functions = searchFunctions(query).slice(0, 10);
339
1428
  const topics = searchFrameworkTopics(query).slice(0, 5);
340
1429
  const types = searchTypes(query).slice(0, 5);
1430
+ const migrationTopics = searchMigrationTopics(query).slice(0, 3);
341
1431
  const parts = [];
342
1432
  if (functions.length > 0) {
343
1433
  parts.push("## Matching Storefront Functions\n");
@@ -363,7 +1453,15 @@ server.tool("search_docs", "Search across all ikas storefront API docs and frame
363
1453
  parts.push("");
364
1454
  parts.push("Use `get_type_definition(name)` to get full type details, or `search_types(query)` for more type results.");
365
1455
  }
366
- if (functions.length === 0 && topics.length === 0 && types.length === 0) {
1456
+ if (migrationTopics.length > 0) {
1457
+ parts.push("\n## Matching Migration Topics\n");
1458
+ for (const item of migrationTopics) {
1459
+ parts.push(`- [migration] **${item.topic.title}** (key: \`${item.key}\`) - ${item.topic.description}`);
1460
+ }
1461
+ parts.push("");
1462
+ parts.push("Use `get_migration_guide(topic)` to get full content for any migration topic.");
1463
+ }
1464
+ if (functions.length === 0 && topics.length === 0 && types.length === 0 && migrationTopics.length === 0) {
367
1465
  parts.push(`No results found for "${query}". Try different keywords or use \`list_functions()\` to see all available functions.`);
368
1466
  }
369
1467
  return { content: [{ type: "text", text: parts.join("\n") }] };
@@ -1325,6 +2423,187 @@ server.tool("list_section_types", "List all available `get_section_template` sec
1325
2423
  lines.push("", "Call `get_section_template(sectionType)` to fetch one.");
1326
2424
  return { content: [{ type: "text", text: lines.join("\n") }] };
1327
2425
  });
2426
+ // --- Migration tools ---
2427
+ // Tool: analyze_old_theme
2428
+ 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 }) => {
2429
+ try {
2430
+ const parsed = JSON.parse(theme_json);
2431
+ const analysis = analyzeOldTheme(parsed);
2432
+ return { content: [{ type: "text", text: analysis }] };
2433
+ }
2434
+ catch (err) {
2435
+ return {
2436
+ content: [{ type: "text", text: `Error parsing theme.json: ${err instanceof Error ? err.message : String(err)}. Make sure you're passing valid JSON.` }],
2437
+ };
2438
+ }
2439
+ });
2440
+ // Tool: get_migration_guide
2441
+ const migrationTopicAliases = {
2442
+ "overview": "migration-overview",
2443
+ "migrate": "migration-overview",
2444
+ "custom": "custom-data-conversion",
2445
+ "custom-data": "custom-data-conversion",
2446
+ "customdata": "custom-data-conversion",
2447
+ "dynamic-list": "custom-data-conversion",
2448
+ "component-list": "custom-data-conversion",
2449
+ "slider": "prop-type-mapping",
2450
+ "props": "prop-type-mapping",
2451
+ "prop-mapping": "prop-type-mapping",
2452
+ "types": "prop-type-mapping",
2453
+ "react": "react-to-preact",
2454
+ "preact": "react-to-preact",
2455
+ "observer": "react-to-preact",
2456
+ "libraries": "library-replacements",
2457
+ "swiper": "library-replacements",
2458
+ "headlessui": "library-replacements",
2459
+ "tailwind": "library-replacements",
2460
+ "tailwindcss": "library-replacements",
2461
+ "recharts": "library-replacements",
2462
+ "marquee": "library-replacements",
2463
+ "imports": "storefront-import-mapping",
2464
+ "storefront": "storefront-import-mapping",
2465
+ "bp-storefront": "storefront-import-mapping",
2466
+ "theme-json": "theme-json-anatomy",
2467
+ "anatomy": "theme-json-anatomy",
2468
+ "decompose": "component-decomposition-strategy",
2469
+ "decomposition": "component-decomposition-strategy",
2470
+ "strategy": "component-decomposition-strategy",
2471
+ "project": "complete-project-generation",
2472
+ "generate": "complete-project-generation",
2473
+ "generation": "complete-project-generation",
2474
+ "settings": "settings-conversion",
2475
+ "colors": "settings-conversion",
2476
+ "fonts": "settings-conversion",
2477
+ "find": "finding-new-system-equivalents",
2478
+ "search": "finding-new-system-equivalents",
2479
+ "discover": "finding-new-system-equivalents",
2480
+ "equivalent": "finding-new-system-equivalents",
2481
+ "equivalents": "finding-new-system-equivalents",
2482
+ "replacement": "finding-new-system-equivalents",
2483
+ };
2484
+ const migrationTopicKeys = migrationData
2485
+ ? Object.keys(migrationData.topics)
2486
+ : [];
2487
+ server.tool("get_migration_guide", `Get a migration guide for converting old ikas themes to the new code-component system.${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 }) => {
2488
+ if (!migrationData) {
2489
+ return { content: [{ type: "text", text: "Migration data not available. Ensure data/migration.json exists." }] };
2490
+ }
2491
+ if (topic.toLowerCase() === "list") {
2492
+ const available = Object.entries(migrationData.topics)
2493
+ .map(([key, t]) => `- \`${key}\` — ${t.title}: ${t.description}`)
2494
+ .join("\n");
2495
+ return { content: [{ type: "text", text: `## Available Migration Topics\n\n${available}` }] };
2496
+ }
2497
+ const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
2498
+ const resolvedTopic = migrationTopicAliases[topicLower] || topicLower;
2499
+ if (migrationData.topics[resolvedTopic]) {
2500
+ const t = migrationData.topics[resolvedTopic];
2501
+ return { content: [{ type: "text", text: `## ${t.title}\n\n${t.content}` }] };
2502
+ }
2503
+ // Try original key
2504
+ if (resolvedTopic !== topicLower && migrationData.topics[topicLower]) {
2505
+ const t = migrationData.topics[topicLower];
2506
+ return { content: [{ type: "text", text: `## ${t.title}\n\n${t.content}` }] };
2507
+ }
2508
+ // Keyword search
2509
+ const matches = searchMigrationTopics(topic);
2510
+ if (matches.length > 0) {
2511
+ const best = matches[0];
2512
+ return { content: [{ type: "text", text: `## ${best.topic.title}\n\n${best.topic.content}` }] };
2513
+ }
2514
+ const available = Object.entries(migrationData.topics)
2515
+ .map(([key, t]) => ` - \`${key}\` - ${t.title}`)
2516
+ .join("\n");
2517
+ return {
2518
+ content: [{ type: "text", text: `Migration topic "${topic}" not found. Available topics:\n${available}` }],
2519
+ };
2520
+ });
2521
+ // Tool: get_migration_example
2522
+ 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 }) => {
2523
+ if (example.toLowerCase() === "list") {
2524
+ if (migrationExampleNames.length === 0) {
2525
+ return { content: [{ type: "text", text: "No migration examples available." }] };
2526
+ }
2527
+ const list = migrationExampleNames.map((name) => {
2528
+ const ex = loadMigrationExample(name);
2529
+ return ex ? `- \`${name}\` — ${ex.title}: ${ex.description}` : `- \`${name}\``;
2530
+ }).join("\n");
2531
+ return { content: [{ type: "text", text: `## Available Migration Examples\n\n${list}` }] };
2532
+ }
2533
+ const exampleLower = example.toLowerCase();
2534
+ let exName = migrationExampleNames.find((n) => n === exampleLower);
2535
+ if (!exName) {
2536
+ exName = migrationExampleNames.find((n) => n.includes(exampleLower) || exampleLower.includes(n));
2537
+ }
2538
+ if (!exName) {
2539
+ const available = migrationExampleNames.join(", ");
2540
+ return {
2541
+ content: [{ type: "text", text: `Migration example "${example}" not found. Available: ${available}` }],
2542
+ };
2543
+ }
2544
+ const ex = loadMigrationExample(exName);
2545
+ if (!ex) {
2546
+ return { content: [{ type: "text", text: `Failed to load migration example "${exName}".` }] };
2547
+ }
2548
+ const parts = [
2549
+ `## ${ex.title}`,
2550
+ "",
2551
+ ex.description,
2552
+ "",
2553
+ ];
2554
+ for (const [filename, content] of Object.entries(ex.files)) {
2555
+ const ext = filename.split(".").pop() || "text";
2556
+ const lang = ext === "tsx" || ext === "ts"
2557
+ ? "typescript"
2558
+ : ext === "css"
2559
+ ? "css"
2560
+ : ext === "json"
2561
+ ? "json"
2562
+ : "text";
2563
+ const isAfter = filename.startsWith("after-");
2564
+ const isBefore = filename.startsWith("before-");
2565
+ const label = isBefore ? "📋 BEFORE" : isAfter ? "✅ AFTER" : "";
2566
+ parts.push(`### ${label} ${filename}`, "", `\`\`\`${lang}`, content, "```", "");
2567
+ }
2568
+ return { content: [{ type: "text", text: parts.join("\n") }] };
2569
+ });
2570
+ // Tool: plan_migration
2571
+ server.tool("plan_migration", "Generate a complete, resumable migration plan (MIGRATION.md) for converting an old ikas theme to the new code-component system. The LLM should save the returned markdown to <new-project-root>/MIGRATION.md as the source of truth for the entire migration. This is the FIRST tool to call for any multi-section (>5 sections) theme migration. Output includes: extracted CSS variables, font setup, custom enums with CLI commands, shared sub-components (when old_source_dir provided), and an ordered section queue grouped by complexity with canonical component IDs.", {
2572
+ theme_json: z.string().describe("Raw JSON content of the old theme.json"),
2573
+ project_name: z.string().optional().describe("Target new project name, used to prefix component IDs (default: 'my-theme')"),
2574
+ 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."),
2575
+ }, async ({ theme_json, project_name, old_source_dir }) => {
2576
+ try {
2577
+ const parsed = JSON.parse(theme_json);
2578
+ const projectName = project_name || "my-theme";
2579
+ const plan = generateMigrationPlan(parsed, projectName, old_source_dir);
2580
+ return { content: [{ type: "text", text: plan }] };
2581
+ }
2582
+ catch (err) {
2583
+ return {
2584
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}. Make sure theme_json is valid JSON.` }],
2585
+ };
2586
+ }
2587
+ });
2588
+ // Tool: get_section_migration_plan
2589
+ server.tool("get_section_migration_plan", "Get a concrete, actionable migration plan for a single section/component from the old theme. Returns: old source file paths to read, a prop-by-prop conversion table, CLI commands to create children and parent components (with auto-generated types.ts), library replacement hints, and cross-references to relevant framework/migration guides. Use this tool once per section in Phase C of the iterative workflow. Call after `plan_migration` has generated MIGRATION.md.", {
2590
+ theme_json: z.string().describe("Raw JSON content of the old theme.json"),
2591
+ 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')"),
2592
+ project_name: z.string().optional().describe("Target new project name (must match what was used in plan_migration). Default: 'my-theme'"),
2593
+ old_source_dir: z.string().optional().describe("Absolute path to old src/ directory (used to output exact source file paths to read)"),
2594
+ }, async ({ theme_json, section_name, project_name, old_source_dir }) => {
2595
+ try {
2596
+ const parsed = JSON.parse(theme_json);
2597
+ const projectName = project_name || "my-theme";
2598
+ const plan = generateSectionMigrationPlan(parsed, section_name, projectName, old_source_dir);
2599
+ return { content: [{ type: "text", text: plan }] };
2600
+ }
2601
+ catch (err) {
2602
+ return {
2603
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}. Make sure theme_json is valid JSON.` }],
2604
+ };
2605
+ }
2606
+ });
1328
2607
  // --- Start server ---
1329
2608
  async function main() {
1330
2609
  const transport = new StdioServerTransport();