@ikas/code-components-mcp 1.4.0-beta.5 → 1.4.0-beta.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
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 { execFile } from "child_process";
4
5
  import * as fs from "fs";
6
+ import * as os from "os";
5
7
  import * as path from "path";
6
8
  import { fileURLToPath } from "url";
7
9
  import { z } from "zod";
@@ -96,9 +98,16 @@ function loadStorefrontTypes() {
96
98
  }
97
99
  return null;
98
100
  }
99
- const SUBTREE_KINDS = ["children", "components", "sub-components"];
101
+ const SUBTREE_KINDS = [
102
+ "children",
103
+ "components",
104
+ "sub-components",
105
+ ];
100
106
  function normalizeName(value) {
101
- return value.trim().replace(/^`+|`+$/g, "").trim();
107
+ return value
108
+ .trim()
109
+ .replace(/^`+|`+$/g, "")
110
+ .trim();
102
111
  }
103
112
  const storefrontData = loadStorefrontData();
104
113
  const frameworkData = loadJsonFile("../data/framework.json");
@@ -259,7 +268,11 @@ function searchMigrationTopics(query) {
259
268
  const descScore = matchScore(topic.description, query) * 2;
260
269
  const contentScore = matchScore(topic.content, query);
261
270
  const tagScore = topic.tags.some((t) => matchScore(t, query) > 0) ? 5 : 0;
262
- return { key, topic, score: titleScore + descScore + contentScore + tagScore };
271
+ return {
272
+ key,
273
+ topic,
274
+ score: titleScore + descScore + contentScore + tagScore,
275
+ };
263
276
  })
264
277
  .filter((item) => item.score > 0)
265
278
  .sort((a, b) => b.score - a.score);
@@ -286,7 +299,7 @@ function analyzeOldTheme(themeJson) {
286
299
  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
300
  parts.push(`## Summary Statistics\n`);
288
301
  parts.push(`- **Components:** ${components.length}`);
289
- parts.push(`- **Custom Data Definitions:** ${customData.filter(cd => cd.isRoot).length}`);
302
+ parts.push(`- **Custom Data Definitions:** ${customData.filter((cd) => cd.isRoot).length}`);
290
303
  parts.push(`- **Prop Groups:** ${groups.length}`);
291
304
  // Component analysis
292
305
  parts.push(`\n## Components (${components.length})\n`);
@@ -325,7 +338,11 @@ function analyzeOldTheme(themeJson) {
325
338
  const typesSummary = Object.entries(propTypeCounts)
326
339
  .map(([t, c]) => `${t}×${c}`)
327
340
  .join(", ");
328
- const headerFooter = comp.isHeader ? " [HEADER]" : comp.isFooter ? " [FOOTER]" : "";
341
+ const headerFooter = comp.isHeader
342
+ ? " [HEADER]"
343
+ : comp.isFooter
344
+ ? " [FOOTER]"
345
+ : "";
329
346
  parts.push(`### ${comp.displayName || comp.dir || comp.id}${headerFooter}`);
330
347
  parts.push(`- **Dir:** \`${comp.dir || "?"}\` | **Props:** ${props.length} (${typesSummary})`);
331
348
  parts.push(`- **Recommended new type:** section`);
@@ -340,7 +357,7 @@ function analyzeOldTheme(themeJson) {
340
357
  parts.push("");
341
358
  }
342
359
  // Custom data analysis
343
- const rootCustomData = customData.filter(cd => cd.isRoot);
360
+ const rootCustomData = customData.filter((cd) => cd.isRoot);
344
361
  if (rootCustomData.length > 0) {
345
362
  parts.push(`\n## Custom Data Definitions (${rootCustomData.length})\n`);
346
363
  for (const cd of rootCustomData) {
@@ -350,7 +367,9 @@ function analyzeOldTheme(themeJson) {
350
367
  const describeNested = (items, indent) => {
351
368
  const lines = [];
352
369
  for (const item of items) {
353
- const key = item.key ? `\`${item.key}\`` : item.typescriptName || item.name || "unnamed";
370
+ const key = item.key
371
+ ? `\`${item.key}\``
372
+ : item.typescriptName || item.name || "unnamed";
354
373
  lines.push(`${indent}- ${key}: ${item.type}${item.isRequired ? " (required)" : ""}`);
355
374
  if (item.nestedData && item.nestedData.length > 0) {
356
375
  lines.push(...describeNested(item.nestedData, indent + " "));
@@ -362,7 +381,7 @@ function analyzeOldTheme(themeJson) {
362
381
  parts.push(...describeNested(cd.nestedData, " "));
363
382
  }
364
383
  if (cd.enumOptions && cd.enumOptions.length > 0) {
365
- parts.push(`- **Enum options:** ${cd.enumOptions.map(o => `"${o.value}"`).join(", ")}`);
384
+ parts.push(`- **Enum options:** ${cd.enumOptions.map((o) => `"${o.value}"`).join(", ")}`);
366
385
  }
367
386
  // Find which components reference this customData
368
387
  const referencingComponents = [];
@@ -444,7 +463,9 @@ function scanSharedSubcomponents(sourceDir) {
444
463
  const walk = (dir) => {
445
464
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
446
465
  if (entry.isDirectory()) {
447
- if (entry.name === "node_modules" || entry.name === "__generated__" || entry.name.startsWith("."))
466
+ if (entry.name === "node_modules" ||
467
+ entry.name === "__generated__" ||
468
+ entry.name.startsWith("."))
448
469
  continue;
449
470
  walk(path.join(dir, entry.name));
450
471
  }
@@ -479,10 +500,14 @@ function scanSharedSubcomponents(sourceDir) {
479
500
  if (!importPath.startsWith("."))
480
501
  continue;
481
502
  // Skip imports of generated types, utils, hooks
482
- if (importPath.includes("__generated__") || importPath.includes("/utils") || importPath.includes("/hooks"))
503
+ if (importPath.includes("__generated__") ||
504
+ importPath.includes("/utils") ||
505
+ importPath.includes("/hooks"))
483
506
  continue;
484
507
  // Extract base name from path
485
- const pathSegments = importPath.split("/").filter(s => s && s !== "." && s !== "..");
508
+ const pathSegments = importPath
509
+ .split("/")
510
+ .filter((s) => s && s !== "." && s !== "..");
486
511
  if (pathSegments.length === 0)
487
512
  continue;
488
513
  const lastSegment = pathSegments[pathSegments.length - 1];
@@ -493,7 +518,10 @@ function scanSharedSubcomponents(sourceDir) {
493
518
  continue;
494
519
  seenInFile.add(key);
495
520
  if (!importUsage.has(key)) {
496
- importUsage.set(key, { usingComponents: new Set(), rawImportPath: importPath });
521
+ importUsage.set(key, {
522
+ usingComponents: new Set(),
523
+ rawImportPath: importPath,
524
+ });
497
525
  }
498
526
  importUsage.get(key).usingComponents.add(componentDir);
499
527
  }
@@ -502,7 +530,7 @@ function scanSharedSubcomponents(sourceDir) {
502
530
  const shared = [];
503
531
  for (const [name, { usingComponents, rawImportPath }] of importUsage) {
504
532
  // Don't flag the component itself (e.g., Navbar imports from ../Navbar/something)
505
- const users = [...usingComponents].filter(c => c !== name);
533
+ const users = [...usingComponents].filter((c) => c !== name);
506
534
  if (users.length >= 3) {
507
535
  shared.push({ name, usedBy: users.sort(), importPaths: [rawImportPath] });
508
536
  }
@@ -519,7 +547,7 @@ function toKebabCase(s) {
519
547
  }
520
548
  function classifyComplexity(comp, customDataMap) {
521
549
  const props = comp.props || [];
522
- const customCount = props.filter(p => p.type === "CUSTOM").length;
550
+ const customCount = props.filter((p) => p.type === "CUSTOM").length;
523
551
  if (customCount === 0 && props.length < 10)
524
552
  return "simple";
525
553
  // Check for deeply nested CUSTOM (customData referencing another customData)
@@ -530,7 +558,9 @@ function classifyComplexity(comp, customDataMap) {
530
558
  if (cd?.nestedData) {
531
559
  const hasNested = (items) => {
532
560
  for (const item of items) {
533
- if (item.type === "DYNAMIC_LIST" || item.type === "STATIC_LIST" || item.customDataId)
561
+ if (item.type === "DYNAMIC_LIST" ||
562
+ item.type === "STATIC_LIST" ||
563
+ item.customDataId)
534
564
  return true;
535
565
  if (item.nestedData && hasNested(item.nestedData))
536
566
  return true;
@@ -562,7 +592,7 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
562
592
  parts.push(`# Theme Migration Plan — \`${projectName}\``);
563
593
  parts.push("");
564
594
  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`);
595
+ parts.push(`**Source:** ${components.length} old components, ${customData.filter((cd) => cd.isRoot).length} custom data types, ${(theme.pages || []).length} pages`);
566
596
  parts.push("");
567
597
  parts.push(`> ## READ THIS FIRST`);
568
598
  parts.push(`>`);
@@ -571,8 +601,9 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
571
601
  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
602
  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
603
  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. **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.`);
575
- parts.push(`> 5. **If you discover new shared sub-components mid-migration**, add them under \`### Shared Sub-Components\` and any notes under \`## Notes\`.`);
604
+ 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.`);
605
+ 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.`);
606
+ parts.push(`> 6. **If you discover new shared sub-components mid-migration**, add them under \`### Shared Sub-Components\` and any notes under \`## Notes\`.`);
576
607
  parts.push(`>`);
577
608
  parts.push(`> Status legend: \`[ ]\` not started · \`[~]\` in progress · \`[x]\` complete.`);
578
609
  parts.push("");
@@ -651,16 +682,22 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
651
682
  const cdType = cd.type || "?";
652
683
  let shape = "";
653
684
  if (cd.type === "ENUM") {
654
- const opts = (cd.enumOptions || []).map((o) => o.value || o.displayName).filter(Boolean);
685
+ const opts = (cd.enumOptions || [])
686
+ .map((o) => o.value || o.displayName)
687
+ .filter(Boolean);
655
688
  shape = ` — shape: \`enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}\``;
656
689
  }
657
690
  else if (cd.nestedData && cd.nestedData.length > 0) {
658
691
  const first = cd.nestedData[0];
659
- const fields = (first?.nestedData || cd.nestedData || []).map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`).slice(0, 8);
692
+ const fields = (first?.nestedData || cd.nestedData || [])
693
+ .map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`)
694
+ .slice(0, 8);
660
695
  shape = ` — shape: \`{${fields.join(", ")}}\``;
661
696
  }
662
697
  const usedBy = cd.id ? usageByCustomDataId.get(cd.id) || [] : [];
663
- 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)_`;
698
+ const usedByStr = usedBy.length > 0
699
+ ? ` — used by: ${usedBy.slice(0, 6).join(", ")}${usedBy.length > 6 ? `, +${usedBy.length - 6} more` : ""}`
700
+ : ` — _not directly referenced by any section's props (may be nested inside another customData)_`;
664
701
  parts.push(`- \`${cdName}\` (${cdType})${shape}${usedByStr}`);
665
702
  }
666
703
  parts.push("");
@@ -694,7 +731,11 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
694
731
  const oldName = comp.displayName || comp.dir || comp.id || "Unknown";
695
732
  const kebabName = toKebabCase(comp.dir || comp.displayName || comp.id || "unknown");
696
733
  const newId = `${projectName}-${kebabName}`;
697
- const headerFooter = comp.isHeader ? " **[HEADER]**" : comp.isFooter ? " **[FOOTER]**" : "";
734
+ const headerFooter = comp.isHeader
735
+ ? " **[HEADER]**"
736
+ : comp.isFooter
737
+ ? " **[FOOTER]**"
738
+ : "";
698
739
  const propCount = (comp.props || []).length;
699
740
  // Detect children from CUSTOM DYNAMIC_LIST props
700
741
  const children = [];
@@ -704,8 +745,13 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
704
745
  if (cd && (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST")) {
705
746
  const itemObj = cd.nestedData?.[0];
706
747
  if (itemObj) {
707
- const childName = itemObj.typescriptName || (itemObj.name ? itemObj.name.replace(/[^a-zA-Z0-9]/g, "") : `${oldName}Item`);
708
- const fields = (itemObj.nestedData || []).map((f) => f.key || f.name || "?").slice(0, 8);
748
+ const childName = itemObj.typescriptName ||
749
+ (itemObj.name
750
+ ? itemObj.name.replace(/[^a-zA-Z0-9]/g, "")
751
+ : `${oldName}Item`);
752
+ const fields = (itemObj.nestedData || [])
753
+ .map((f) => f.key || f.name || "?")
754
+ .slice(0, 8);
709
755
  children.push({ propName: p.name || "?", childName, fields });
710
756
  }
711
757
  }
@@ -727,22 +773,14 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
727
773
  parts.push("");
728
774
  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.`);
729
775
  parts.push("");
730
- parts.push(`<!-- Example: list each atomic component you find here.`);
731
- parts.push(`- \`Button\` — found in src/atoms/Button/. Used by ~all sections. → add as Shared Sub-Component.`);
732
- parts.push(`- \`PriceLabel\` — found in src/atoms/. Used only by ProductCard. → inline into ProductCard during migration.`);
733
- parts.push(`- \`Icon\` — found in src/atoms/Icon/. Used everywhere; replace with inline SVG per section.`);
734
- parts.push(`-->`);
776
+ parts.push(`<!-- Example: \`- Button (src/atoms/Button/) used by ~all sections → promote to Shared Sub-Component\` -->`);
735
777
  parts.push("");
736
778
  // Custom Data Decisions — append-only log the LLM fills as it makes per-section decisions
737
779
  parts.push(`## Custom Data Decisions`);
738
780
  parts.push("");
739
781
  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\`.`);
740
782
  parts.push("");
741
- parts.push(`<!-- Example entries:`);
742
- parts.push(`- \`SlideData\` → component \`hero-slide\` (2026-05-11) — structured record {image, link, title} repeated per slide; wired into HeroSlider via COMPONENT_LIST.`);
743
- parts.push(`- \`Position\` → enum \`Position\` (2026-05-11) — flat scalar set left/right/center; enumId: enm_abc123.`);
744
- parts.push(`- \`MenuItemData\` → component \`menu-item\` (2026-05-12) — mega-menu links with image + title fields; LIST_OF_LINK would have dropped the image.`);
745
- parts.push(`-->`);
783
+ parts.push(`<!-- Example: \`- SlideData → component \\\`hero-slide\\\` (2026-05-11) — structured record {image,link,title}; LIST_OF_LINK would drop the image\` -->`);
746
784
  parts.push("");
747
785
  // Known Environmental Issues (agents fill in during work)
748
786
  parts.push(`## Known Environmental Issues`);
@@ -751,54 +789,46 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
751
789
  parts.push("");
752
790
  parts.push(`- [ ] _(none recorded yet)_`);
753
791
  parts.push("");
754
- // Usage section
755
- parts.push(`## Per-Section Usage`);
756
- parts.push("");
757
- parts.push(`Once the Foundation is complete, for each section above:`);
758
- parts.push("");
759
- parts.push(`1. Find the first unchecked \`[ ]\` section (start with Simple).`);
760
- parts.push(`2. Call \`get_section_migration_plan({theme_json_path, section_name: "<old section name>", project_name: "${projectName}"})\`.`);
761
- parts.push(`3. Read the old source files listed in the plan.`);
762
- parts.push(`4. Make any customData enum-vs-component decisions surfaced in the plan and log them under \`## Custom Data Decisions\` above.`);
763
- parts.push(`5. Run the CLI commands in the plan (they create parent + children with auto-generated types.ts).`);
764
- parts.push(`6. Write \`index.tsx\` and \`styles.css\` using the patterns in the plan. DO NOT manually edit types.ts.`);
765
- parts.push(`7. Mark the section \`[x]\` when it builds cleanly. Append a short note to the **Notes** section 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).`);
766
- parts.push("");
767
792
  // Notes — append-only log for decisions not captured elsewhere
768
793
  parts.push(`## Notes`);
769
794
  parts.push("");
770
- parts.push(`_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 (library swaps, ad-hoc props, CLI folder-name oddities, etc.)._`);
795
+ 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>\`._`);
771
796
  parts.push("");
772
- parts.push(`<!-- Examples:`);
773
- parts.push(`- [2026-04-15] Foundation: built Input, SubmitButton, Modal, SectionHeader, StarRating. Skipped: GoogleCaptcha (needs investigation), BlogCard, Pagination (low priority).`);
774
- 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.`);
775
- parts.push(`-->`);
797
+ parts.push(`<!-- Example: \`- [2026-04-15] ${projectName}-footer: swapped react-hot-toast for inline status text; CLI created Footer/ as expected\` -->`);
776
798
  parts.push("");
777
- // Cross-references
799
+ // Cross-references — keep terse; LLM can call `get_migration_guide("list")` or `get_framework_guide("list")` for more
778
800
  parts.push(`## Cross-References`);
779
801
  parts.push("");
780
- parts.push(`Essential MCP tools to call during migration:`);
802
+ parts.push(`- \`get_migration_guide("iterative-workflow")\` the full per-phase protocol`);
803
+ parts.push(`- \`get_migration_guide("custom-data-conversion")\` — enum-vs-component decisions`);
804
+ parts.push(`- \`get_migration_guide("prop-runtime-shapes")\` — runtime shapes & access patterns`);
805
+ parts.push(`- \`get_framework_guide("common-pitfalls")\` — gotchas & old→new property migrations`);
781
806
  parts.push("");
782
- parts.push(`- \`get_migration_guide("iterative-workflow")\` full protocol for resumable migrations`);
783
- parts.push(`- \`get_migration_guide("component-renderer-limitations")\` — critical constraints when using COMPONENT_LIST`);
784
- parts.push(`- \`get_migration_guide("prop-runtime-shapes")\` — \`.data\` vs \`.links\`, \`.variant\` vs \`.product\`, etc.`);
785
- parts.push(`- \`get_migration_guide("link-prop-decision-guide")\` — LINK vs LIST_OF_LINK vs COMPONENT_LIST`);
786
- parts.push(`- \`get_migration_guide("library-replacements")\` — swiper, headlessui, tailwind, etc.`);
787
- parts.push(`- \`get_migration_guide("react-to-preact")\` — observer rules, imports, IkasSlider removal`);
788
- parts.push(`- \`get_framework_guide("component-renderer-patterns")\` — full IkasComponentRenderer usage`);
789
- parts.push(`- \`get_framework_guide("common-pitfalls")\` — general gotchas`);
790
- parts.push(`- \`get_framework_guide("navigation-patterns")\` — Router.navigate, Router.navigateToPage`);
791
- parts.push(`- \`get_model_guide("<TypeName>")\` — IkasCart, IkasProduct, IkasCustomer shapes`);
807
+ parts.push(`Call \`get_migration_guide("list")\` or \`get_framework_guide("list")\` for the full topic catalog.`);
792
808
  parts.push("");
793
809
  return parts.join("\n");
794
810
  }
795
811
  // Known libraries we detect in old themes and want to flag for replacement
796
812
  const KNOWN_LIBRARIES = [
797
- "swiper", "@headlessui/react", "@heroicons/react", "recharts",
798
- "react-player", "react-simple-star-rating", "react-slider", "react-compound-slider",
799
- "react-zoom-pan-pinch", "react-hot-toast", "react-fast-marquee",
800
- "react-indiana-drag-scroll", "react-simple-typewriter", "react-timer-hook",
801
- "date-fns", "slugify", "classnames", "clsx", "@react-pdf/renderer",
813
+ "swiper",
814
+ "@headlessui/react",
815
+ "@heroicons/react",
816
+ "recharts",
817
+ "react-player",
818
+ "react-simple-star-rating",
819
+ "react-slider",
820
+ "react-compound-slider",
821
+ "react-zoom-pan-pinch",
822
+ "react-hot-toast",
823
+ "react-fast-marquee",
824
+ "react-indiana-drag-scroll",
825
+ "react-simple-typewriter",
826
+ "react-timer-hook",
827
+ "date-fns",
828
+ "slugify",
829
+ "classnames",
830
+ "clsx",
831
+ "@react-pdf/renderer",
802
832
  ];
803
833
  // Heuristic: member-access patterns on old storefront stores/singletons that likely need new-system equivalents
804
834
  const OLD_STOREFRONT_CALL_REGEX = /\b(customerStore|cartStore|productStore|categoryStore|orderStore|searchStore|favoritesStore|i18nStore|Router|useStore)\.\w+/g;
@@ -817,7 +847,8 @@ function scanSectionSource(componentDir, propNames) {
817
847
  // Collect all .tsx/.ts files in the component dir
818
848
  try {
819
849
  for (const entry of fs.readdirSync(componentDir, { withFileTypes: true })) {
820
- if (entry.isFile() && (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
850
+ if (entry.isFile() &&
851
+ (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
821
852
  result.sourceFiles.push(path.join(componentDir, entry.name));
822
853
  }
823
854
  }
@@ -834,7 +865,18 @@ function scanSectionSource(componentDir, propNames) {
834
865
  const subCompSet = new Map();
835
866
  const callSet = new Set();
836
867
  // Packages we treat as "framework" and don't flag for replacement (but do note in reactPackageUsage)
837
- const REACT_PACKAGES = new Set(["react", "react-dom", "next", "next/link", "next/image", "next/router", "next/head", "next/script", "mobx-react-lite", "mobx"]);
868
+ const REACT_PACKAGES = new Set([
869
+ "react",
870
+ "react-dom",
871
+ "next",
872
+ "next/link",
873
+ "next/image",
874
+ "next/router",
875
+ "next/head",
876
+ "next/script",
877
+ "mobx-react-lite",
878
+ "mobx",
879
+ ]);
838
880
  for (const file of result.sourceFiles) {
839
881
  let content;
840
882
  try {
@@ -848,7 +890,7 @@ function scanSectionSource(componentDir, propNames) {
848
890
  while ((m = importRegex.exec(content)) !== null) {
849
891
  const p = m[1];
850
892
  if (p.startsWith(".")) {
851
- const segs = p.split("/").filter(s => s && s !== "." && s !== "..");
893
+ const segs = p.split("/").filter((s) => s && s !== "." && s !== "..");
852
894
  const last = segs[segs.length - 1];
853
895
  if (last && /^[A-Z]/.test(last) && !last.includes("__generated__")) {
854
896
  subCompSet.set(last, p);
@@ -856,7 +898,9 @@ function scanSectionSource(componentDir, propNames) {
856
898
  }
857
899
  else if (!p.startsWith("@ikas/")) {
858
900
  // Classify: known library, react-family, or unknown-external
859
- const base = p.startsWith("@") ? p.split("/").slice(0, 2).join("/") : p.split("/")[0];
901
+ const base = p.startsWith("@")
902
+ ? p.split("/").slice(0, 2).join("/")
903
+ : p.split("/")[0];
860
904
  if (REACT_PACKAGES.has(p) || REACT_PACKAGES.has(base)) {
861
905
  reactSet.add(base);
862
906
  }
@@ -919,7 +963,10 @@ function scanSectionSource(componentDir, propNames) {
919
963
  }
920
964
  }
921
965
  }
922
- result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({ name, path: p }));
966
+ result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({
967
+ name,
968
+ path: p,
969
+ }));
923
970
  result.importedLibraries = [...libSet].sort();
924
971
  result.importedUnknownLibraries = [...unknownLibSet].sort();
925
972
  result.reactPackageUsage = [...reactSet].sort();
@@ -935,27 +982,38 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
935
982
  customDataMap.set(cd.id, cd);
936
983
  }
937
984
  // Find the component — try match by dir, displayName, id, or new-id
938
- const target = components.find(c => {
985
+ const target = components.find((c) => {
939
986
  if (!c)
940
987
  return false;
941
- if (c.dir === sectionName || c.displayName === sectionName || c.id === sectionName)
988
+ if (c.dir === sectionName ||
989
+ c.displayName === sectionName ||
990
+ c.id === sectionName)
942
991
  return true;
943
992
  const kebab = toKebabCase(c.dir || c.displayName || c.id || "");
944
993
  const newId = `${projectName}-${kebab}`;
945
994
  return newId === sectionName;
946
995
  });
947
996
  if (!target) {
948
- const available = components.map(c => c.dir || c.displayName || c.id).filter(Boolean).join(", ");
997
+ const available = components
998
+ .map((c) => c.dir || c.displayName || c.id)
999
+ .filter(Boolean)
1000
+ .join(", ");
949
1001
  return `Section "${sectionName}" not found in theme. Available: ${available}`;
950
1002
  }
951
1003
  const parts = [];
952
1004
  const oldName = target.displayName || target.dir || target.id || "Unknown";
953
1005
  const kebabName = toKebabCase(target.dir || target.displayName || target.id || "unknown");
954
1006
  const sectionId = `${projectName}-${kebabName}`;
955
- const sectionPascal = (target.dir || target.displayName || "").replace(/[^a-zA-Z0-9]/g, "") || kebabName.split("-").map(s => s[0]?.toUpperCase() + s.slice(1)).join("");
1007
+ const sectionPascal = (target.dir || target.displayName || "").replace(/[^a-zA-Z0-9]/g, "") ||
1008
+ kebabName
1009
+ .split("-")
1010
+ .map((s) => s[0]?.toUpperCase() + s.slice(1))
1011
+ .join("");
956
1012
  // Scan the old source for imports, libraries, field usage
957
- const propNames = (target.props || []).map(p => p.name || "").filter(Boolean);
958
- const sourceScan = (oldSourceDir && target.dir)
1013
+ const propNames = (target.props || [])
1014
+ .map((p) => p.name || "")
1015
+ .filter(Boolean);
1016
+ const sourceScan = oldSourceDir && target.dir
959
1017
  ? scanSectionSource(path.join(oldSourceDir, target.dir), propNames)
960
1018
  : null;
961
1019
  parts.push(`# Section Migration Plan: ${oldName}`);
@@ -1001,7 +1059,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1001
1059
  }
1002
1060
  if (sourceScan.reactPackageUsage.length > 0) {
1003
1061
  parts.push("");
1004
- 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).`);
1062
+ 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).`);
1005
1063
  }
1006
1064
  if (sourceScan.oldStorefrontCalls.length > 0) {
1007
1065
  parts.push("");
@@ -1047,7 +1105,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1047
1105
  if (oldType === "SLIDER") {
1048
1106
  newType = "NUMBER";
1049
1107
  notes = `Was SLIDER(min=${p.sliderData?.min}, max=${p.sliderData?.max}) — replace \`.value\` access with direct number`;
1050
- const prop = { name: newName, displayName: p.displayName || newName, type: "NUMBER" };
1108
+ const prop = {
1109
+ name: newName,
1110
+ displayName: p.displayName || newName,
1111
+ type: "NUMBER",
1112
+ };
1051
1113
  if (p.isRequired)
1052
1114
  prop.required = true;
1053
1115
  parentPropsJson.push(prop);
@@ -1055,7 +1117,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1055
1117
  else if (oldType === "PRODUCT_DETAIL") {
1056
1118
  newType = "PRODUCT";
1057
1119
  notes = "Renamed — PRODUCT_DETAIL → PRODUCT";
1058
- const prop = { name: newName, displayName: p.displayName || newName, type: "PRODUCT" };
1120
+ const prop = {
1121
+ name: newName,
1122
+ displayName: p.displayName || newName,
1123
+ type: "PRODUCT",
1124
+ };
1059
1125
  if (p.isRequired)
1060
1126
  prop.required = true;
1061
1127
  parentPropsJson.push(prop);
@@ -1066,7 +1132,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1066
1132
  if (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST") {
1067
1133
  // Child component needed
1068
1134
  const itemObj = cd.nestedData?.[0];
1069
- const childName = itemObj?.typescriptName || (itemObj?.name ? itemObj.name.replace(/[^a-zA-Z0-9]/g, "") : `${sectionPascal}Item`);
1135
+ const childName = itemObj?.typescriptName ||
1136
+ (itemObj?.name
1137
+ ? itemObj.name.replace(/[^a-zA-Z0-9]/g, "")
1138
+ : `${sectionPascal}Item`);
1070
1139
  const childProps = [];
1071
1140
  const nestedWarnings = [];
1072
1141
  for (const f of (itemObj?.nestedData || [])) {
@@ -1077,11 +1146,18 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1077
1146
  fType = "NUMBER";
1078
1147
  else if (fType === "PRODUCT_DETAIL")
1079
1148
  fType = "PRODUCT";
1080
- else if (fType === "CUSTOM" || fType === "DYNAMIC_LIST" || fType === "STATIC_LIST" || fType === "OBJECT") {
1149
+ else if (fType === "CUSTOM" ||
1150
+ fType === "DYNAMIC_LIST" ||
1151
+ fType === "STATIC_LIST" ||
1152
+ fType === "OBJECT") {
1081
1153
  nestedWarnings.push(`\`${f.key}\` (${fType})`);
1082
1154
  fType = "COMPONENT_LIST";
1083
1155
  }
1084
- const prop = { name: f.key, displayName: f.name || f.key, type: fType };
1156
+ const prop = {
1157
+ name: f.key,
1158
+ displayName: f.name || f.key,
1159
+ type: fType,
1160
+ };
1085
1161
  if (f.isRequired)
1086
1162
  prop.required = true;
1087
1163
  childProps.push(prop);
@@ -1093,10 +1169,14 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1093
1169
  if (childProps.length === 0 && sourceScan?.propFieldUsage[oldName]) {
1094
1170
  const inferred = sourceScan.propFieldUsage[oldName];
1095
1171
  for (const fieldName of inferred) {
1096
- childProps.push({ name: fieldName, displayName: fieldName, type: "TEXT" });
1172
+ childProps.push({
1173
+ name: fieldName,
1174
+ displayName: fieldName,
1175
+ type: "TEXT",
1176
+ });
1097
1177
  }
1098
1178
  if (inferred.length > 0) {
1099
- 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.`;
1179
+ 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.`;
1100
1180
  }
1101
1181
  }
1102
1182
  children.push({
@@ -1125,7 +1205,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1125
1205
  let fType = f.type;
1126
1206
  if (fType === "SLIDER")
1127
1207
  fType = "NUMBER";
1128
- const prop = { name: f.key, displayName: f.name || f.key, type: fType };
1208
+ const prop = {
1209
+ name: f.key,
1210
+ displayName: f.name || f.key,
1211
+ type: fType,
1212
+ };
1129
1213
  if (f.isRequired)
1130
1214
  prop.required = true;
1131
1215
  parentPropsJson.push(prop);
@@ -1140,7 +1224,8 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1140
1224
  }
1141
1225
  else if (cd.type === "ENUM") {
1142
1226
  newType = "ENUM";
1143
- const enumName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
1227
+ const enumName = cd.typescriptName ||
1228
+ (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
1144
1229
  const options = (cd.enumOptions || []).reduce((acc, o) => {
1145
1230
  if (o.displayName && o.value)
1146
1231
  acc[o.displayName] = o.value;
@@ -1148,7 +1233,12 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1148
1233
  }, {});
1149
1234
  enumsNeeded.push({ name: enumName, options });
1150
1235
  notes = `Was CUSTOM (ENUM) — create enum \`${enumName}\` via \`config add-enum\` first, then reference its enumId here`;
1151
- const prop = { name: newName, displayName: p.displayName || newName, type: "ENUM", enumTypeId: `<ENUM_ID_FROM_add-enum_${enumName}>` };
1236
+ const prop = {
1237
+ name: newName,
1238
+ displayName: p.displayName || newName,
1239
+ type: "ENUM",
1240
+ enumTypeId: `<ENUM_ID_FROM_add-enum_${enumName}>`,
1241
+ };
1152
1242
  if (p.isRequired)
1153
1243
  prop.required = true;
1154
1244
  parentPropsJson.push(prop);
@@ -1157,7 +1247,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1157
1247
  }
1158
1248
  else {
1159
1249
  // Direct mapping
1160
- const prop = { name: newName, displayName: p.displayName || newName, type: newType };
1250
+ const prop = {
1251
+ name: newName,
1252
+ displayName: p.displayName || newName,
1253
+ type: newType,
1254
+ };
1161
1255
  if (p.isRequired)
1162
1256
  prop.required = true;
1163
1257
  parentPropsJson.push(prop);
@@ -1166,18 +1260,13 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1166
1260
  }
1167
1261
  parts.push("");
1168
1262
  // Custom Data Decision Callouts — per prop referencing a customData type
1169
- const customDataPropsForCallouts = (target.props || []).filter((p) => p.type === "CUSTOM" && p.customDataId && customDataMap.has(p.customDataId));
1263
+ const customDataPropsForCallouts = (target.props || []).filter((p) => p.type === "CUSTOM" &&
1264
+ p.customDataId &&
1265
+ customDataMap.has(p.customDataId));
1170
1266
  if (customDataPropsForCallouts.length > 0) {
1171
1267
  parts.push(`## Custom Data Decisions to Make`);
1172
1268
  parts.push("");
1173
- parts.push(`Each prop below references an old \`customData\` type. The conversion table above shows the MCP's **default** choice based on shape but you should verify it matches the data's actual semantics. **Heuristic:**`);
1174
- parts.push("");
1175
- parts.push(`- **Flat scalar set** (e.g. \`"left" | "right" | "center"\`) → new-system **enum prop** via \`config add-enum\`.`);
1176
- parts.push(`- **Structured record** (\`{image, link, title}\`, repeated in a list) → new-system **component** via \`config add-component\` + COMPONENT_LIST wiring.`);
1177
- parts.push("");
1178
- parts.push(`See \`get_migration_guide("custom-data-conversion")\` for worked examples (Position → enum; Slide → component; MenuItem → component).`);
1179
- parts.push("");
1180
- parts.push(`> **For each decision below, log it in MIGRATION.md under \`## Custom Data Decisions\`** with the format: \`- \\\`<CustomDataName>\\\` → enum/component \\\`<target name>\\\` (${new Date().toISOString().slice(0, 10)}) — reasoning\`.`);
1269
+ 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.`);
1181
1270
  parts.push("");
1182
1271
  for (const p of customDataPropsForCallouts) {
1183
1272
  const cd = customDataMap.get(p.customDataId);
@@ -1188,7 +1277,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1188
1277
  let shape = "";
1189
1278
  let shapeKind = "unknown";
1190
1279
  if (cd.type === "ENUM") {
1191
- const opts = (cd.enumOptions || []).map((o) => o.value || o.displayName).filter(Boolean);
1280
+ const opts = (cd.enumOptions || [])
1281
+ .map((o) => o.value || o.displayName)
1282
+ .filter(Boolean);
1192
1283
  shape = `enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}`;
1193
1284
  shapeKind = "enum";
1194
1285
  }
@@ -1206,56 +1297,91 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1206
1297
  else {
1207
1298
  shape = cdType;
1208
1299
  }
1300
+ // Build the field source + names list once so we can use it for the lost-fields enumeration
1301
+ // AND for the component-path CLI template.
1302
+ const fieldSource = cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST"
1303
+ ? cd.nestedData?.[0]?.nestedData || []
1304
+ : cd.nestedData || [];
1305
+ const fieldDescriptions = fieldSource
1306
+ .filter((f) => f.key)
1307
+ .map((f) => `\`${f.key}\` (${f.type || "?"})`);
1209
1308
  parts.push(`### Prop \`${p.name || "?"}\` → customData \`${cdName}\``);
1210
1309
  parts.push("");
1211
1310
  parts.push(`**Shape:** \`${shape}\``);
1212
1311
  parts.push("");
1213
- let recommendation;
1214
1312
  if (shapeKind === "enum") {
1215
- recommendation = `**MCP default: enum prop.** This looks like a flat scalar set. Use \`config add-enum\` unless the data is actually used as a richer object (e.g. each "option" carries an image — in which case it's a component).`;
1313
+ parts.push(`**Default: enum prop.** Flat scalar set; use \`config add-enum\`.`);
1314
+ parts.push("");
1315
+ const enumName = cd.typescriptName ||
1316
+ (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
1317
+ const enumOptions = (cd.enumOptions || []).reduce((acc, o) => {
1318
+ if (o.displayName && o.value)
1319
+ acc[o.displayName] = o.value;
1320
+ return acc;
1321
+ }, {});
1322
+ parts.push("```bash");
1323
+ parts.push(`npx ikas-component config add-enum --name "${enumName}" --options '${JSON.stringify(enumOptions)}'`);
1324
+ parts.push("```");
1325
+ parts.push("");
1326
+ 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")\`.`);
1327
+ parts.push("");
1216
1328
  }
1217
1329
  else if (shapeKind === "list" || shapeKind === "record") {
1218
- recommendation = `**MCP default: component + COMPONENT_LIST.** This has multiple fields per item — a single enum value can't carry the structure. Use \`config add-component\` for a child component, then wire it via \`filteredComponentIds\` on the parent.`;
1330
+ const fieldCount = fieldDescriptions.length;
1331
+ const isMinimal = fieldCount > 0 && fieldCount <= 2;
1332
+ if (isMinimal) {
1333
+ 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:**`);
1334
+ parts.push(`- repeated scalar props on the parent (\`title1\`/\`link1\`, \`title2\`/\`link2\`, …) for a small fixed count`);
1335
+ parts.push(`- a domain LIST prop type (\`LIST_OF_LINK\`, \`IMAGE_LIST\`, \`PRODUCT_LIST\`, …) when each item IS one domain object`);
1336
+ parts.push(`- \`COMPONENT_LIST\` (CLI command below) only if reordering in the editor is a real UX win`);
1337
+ parts.push("");
1338
+ parts.push(`See \`get_migration_guide("component-composition-decision-guide")\` for the full tree. Log your choice in MIGRATION.md → \`## Custom Data Decisions\`.`);
1339
+ parts.push("");
1340
+ }
1341
+ else {
1342
+ parts.push(`**Default: component + COMPONENT_LIST.** Multiple fields per item — a single enum value cannot carry this structure.`);
1343
+ parts.push("");
1344
+ if (fieldCount > 0) {
1345
+ 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.`);
1346
+ parts.push("");
1347
+ }
1348
+ parts.push(`> See \`get_migration_guide("component-composition-decision-guide")\` for when \`COMPONENT_LIST\` is overkill.`);
1349
+ parts.push("");
1350
+ }
1351
+ const compName = cd.typescriptName ||
1352
+ (cd.name
1353
+ ? cd.name.replace(/[^a-zA-Z0-9]/g, "")
1354
+ : `${sectionPascal}Item`);
1355
+ const compPropsForCli = [];
1356
+ for (const f of fieldSource) {
1357
+ if (!f.key)
1358
+ continue;
1359
+ let fType = f.type;
1360
+ if (fType === "SLIDER")
1361
+ fType = "NUMBER";
1362
+ else if (fType === "PRODUCT_DETAIL")
1363
+ fType = "PRODUCT";
1364
+ compPropsForCli.push({
1365
+ name: f.key,
1366
+ displayName: f.name || f.key,
1367
+ type: fType,
1368
+ });
1369
+ }
1370
+ parts.push("```bash");
1371
+ if (isMinimal) {
1372
+ parts.push(`# Fallback: COMPONENT_LIST (use only if the simpler alternatives above don't fit)`);
1373
+ }
1374
+ parts.push(`npx ikas-component config add-component --name "${compName}" --type component --props '${JSON.stringify(compPropsForCli)}'`);
1375
+ parts.push(`# Then on the parent, set the prop's filteredComponentIds to the new component's id.`);
1376
+ parts.push("```");
1377
+ parts.push("");
1378
+ parts.push(`If you believe this should be an enum despite the structure, see \`get_migration_guide("custom-data-conversion")\`.`);
1379
+ parts.push("");
1219
1380
  }
1220
1381
  else {
1221
- recommendation = `**MCP default: unable to classify automatically — you decide.**`;
1382
+ parts.push(`**Unable to classify automatically — you decide.** See \`get_migration_guide("custom-data-conversion")\` for the enum-vs-component heuristic.`);
1383
+ parts.push("");
1222
1384
  }
1223
- parts.push(recommendation);
1224
- parts.push("");
1225
- parts.push(`**Both CLI templates** (pick the one that matches your decision):`);
1226
- parts.push("");
1227
- parts.push(`Enum path:`);
1228
- parts.push("```bash");
1229
- const enumName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
1230
- const enumOptions = (cd.enumOptions || []).reduce((acc, o) => {
1231
- if (o.displayName && o.value)
1232
- acc[o.displayName] = o.value;
1233
- return acc;
1234
- }, {});
1235
- parts.push(`npx ikas-component config add-enum --name "${enumName}" --options '${JSON.stringify(enumOptions)}'`);
1236
- parts.push("```");
1237
- parts.push("");
1238
- parts.push(`Component path:`);
1239
- parts.push("```bash");
1240
- const compName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : `${sectionPascal}Item`);
1241
- const compPropsForCli = [];
1242
- const fieldSource = cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST"
1243
- ? cd.nestedData?.[0]?.nestedData || []
1244
- : cd.nestedData || [];
1245
- for (const f of fieldSource) {
1246
- if (!f.key)
1247
- continue;
1248
- let fType = f.type;
1249
- if (fType === "SLIDER")
1250
- fType = "NUMBER";
1251
- else if (fType === "PRODUCT_DETAIL")
1252
- fType = "PRODUCT";
1253
- compPropsForCli.push({ name: f.key, displayName: f.name || f.key, type: fType });
1254
- }
1255
- parts.push(`npx ikas-component config add-component --name "${compName}" --type component --props '${JSON.stringify(compPropsForCli)}'`);
1256
- parts.push(`# Then on the parent, set the prop's filteredComponentIds to the new component's id.`);
1257
- parts.push("```");
1258
- parts.push("");
1259
1385
  }
1260
1386
  }
1261
1387
  // Enums to create first
@@ -1278,7 +1404,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1278
1404
  existing.usedByProps.push(ch.propName);
1279
1405
  }
1280
1406
  else {
1281
- uniqueChildren.set(ch.childName, { child: ch, usedByProps: [ch.propName] });
1407
+ uniqueChildren.set(ch.childName, {
1408
+ child: ch,
1409
+ usedByProps: [ch.propName],
1410
+ });
1282
1411
  }
1283
1412
  }
1284
1413
  if (uniqueChildren.size > 0) {
@@ -1291,7 +1420,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1291
1420
  for (const { child: ch, usedByProps } of uniqueChildren.values()) {
1292
1421
  parts.push(`### \`${ch.childName}\``);
1293
1422
  const propsLabel = usedByProps.length > 1
1294
- ? `Used by parent props: ${usedByProps.map(p => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
1423
+ ? `Used by parent props: ${usedByProps.map((p) => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
1295
1424
  : `For parent prop: \`${usedByProps[0]}\``;
1296
1425
  parts.push(propsLabel);
1297
1426
  parts.push(`Old customData: "${ch.customDataName}"`);
@@ -1337,12 +1466,15 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1337
1466
  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.`);
1338
1467
  }
1339
1468
  // Check if the section itself has data-driven list props (PRODUCT_LIST, BLOG_LIST, CATEGORY_LIST)
1340
- const dataListProps = (target.props || []).filter(p => p.type === "PRODUCT_LIST" || p.type === "BLOG_LIST" || p.type === "CATEGORY_LIST" || p.type === "BRAND_LIST");
1469
+ const dataListProps = (target.props || []).filter((p) => p.type === "PRODUCT_LIST" ||
1470
+ p.type === "BLOG_LIST" ||
1471
+ p.type === "CATEGORY_LIST" ||
1472
+ p.type === "BRAND_LIST");
1341
1473
  if (dataListProps.length > 0) {
1342
1474
  parts.push("");
1343
1475
  parts.push(`### Data-Driven List Rendering`);
1344
1476
  parts.push("");
1345
- parts.push(`This section has data-driven list props: ${dataListProps.map(p => `\`${p.name}\` (${p.type})`).join(", ")}.`);
1477
+ parts.push(`This section has data-driven list props: ${dataListProps.map((p) => `\`${p.name}\` (${p.type})`).join(", ")}.`);
1346
1478
  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:`);
1347
1479
  parts.push("");
1348
1480
  parts.push("```tsx");
@@ -1369,9 +1501,22 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1369
1501
  parts.push(`See \`get_migration_guide("custom-data-conversion")\` → "Two Ways to Render Lists" for the full pattern.`);
1370
1502
  }
1371
1503
  // Detect form-page sections (0 or few props, name suggests a form/auth page)
1372
- const formKeywords = ["login", "register", "forgot", "recover", "password", "account", "email", "verification", "activate", "contact", "checkout", "address"];
1504
+ const formKeywords = [
1505
+ "login",
1506
+ "register",
1507
+ "forgot",
1508
+ "recover",
1509
+ "password",
1510
+ "account",
1511
+ "email",
1512
+ "verification",
1513
+ "activate",
1514
+ "contact",
1515
+ "checkout",
1516
+ "address",
1517
+ ];
1373
1518
  const lowerDir = (target.dir || "").toLowerCase();
1374
- const isLikelyFormPage = formKeywords.some(kw => lowerDir.includes(kw));
1519
+ const isLikelyFormPage = formKeywords.some((kw) => lowerDir.includes(kw));
1375
1520
  if (isLikelyFormPage) {
1376
1521
  parts.push("");
1377
1522
  parts.push(`### Form Page Pattern`);
@@ -1400,7 +1545,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1400
1545
  // Fallback heuristic only when source scan unavailable
1401
1546
  const heuristicLibs = [];
1402
1547
  const lowerName = oldName.toLowerCase();
1403
- if (lowerName.includes("slider") || lowerName.includes("carousel") || lowerName.includes("banner")) {
1548
+ if (lowerName.includes("slider") ||
1549
+ lowerName.includes("carousel") ||
1550
+ lowerName.includes("banner")) {
1404
1551
  heuristicLibs.push("swiper");
1405
1552
  }
1406
1553
  if (lowerName.includes("marquee"))
@@ -1409,7 +1556,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1409
1556
  heuristicLibs.push("react-player");
1410
1557
  if (lowerName.includes("chart"))
1411
1558
  heuristicLibs.push("recharts");
1412
- if (lowerName.includes("star") || lowerName.includes("rating") || lowerName.includes("review"))
1559
+ if (lowerName.includes("star") ||
1560
+ lowerName.includes("rating") ||
1561
+ lowerName.includes("review"))
1413
1562
  heuristicLibs.push("react-simple-star-rating");
1414
1563
  if (heuristicLibs.length > 0) {
1415
1564
  parts.push(`### Likely Library Replacements (heuristic — source not scanned)`);
@@ -1423,31 +1572,19 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1423
1572
  parts.push("");
1424
1573
  }
1425
1574
  }
1426
- // Relevant guides
1427
- parts.push(`## ${nextStep + 1}. Relevant Guides (call these for details)`);
1575
+ // Relevant guides — keep terse; LLM can call get_migration_guide("list") for the full catalog
1576
+ parts.push(`## ${nextStep + 1}. Relevant Guides`);
1428
1577
  parts.push("");
1429
1578
  parts.push(`- \`get_migration_guide("react-to-preact")\` — code conversion patterns`);
1430
- parts.push(`- \`get_migration_guide("library-replacements")\` — library vanilla Preact patterns`);
1431
- parts.push(`- \`get_migration_guide("prop-runtime-shapes")\` exact runtime shapes (.data vs .links)`);
1432
- if (children.length > 0) {
1433
- parts.push(`- \`get_migration_guide("component-renderer-limitations")\` — critical COMPONENT_LIST constraints`);
1434
- parts.push(`- \`get_framework_guide("component-renderer-patterns")\` — full IkasComponentRenderer usage`);
1435
- parts.push(`- \`get_migration_example("custom-dynamic-list-to-component-list")\` — concrete example`);
1579
+ parts.push(`- \`get_migration_guide("prop-runtime-shapes")\` — exact runtime shapes (\`.data\` vs \`.links\`, etc.)`);
1580
+ if (children.length > 0 || target.isHeader || target.isFooter) {
1581
+ parts.push(`- \`get_framework_guide("header-footer-patterns")\` COMPONENT_LIST + IkasComponentRenderer wiring`);
1436
1582
  }
1437
- if (target.isHeader || target.isFooter) {
1438
- parts.push(`- \`get_framework_guide("header-footer-patterns")\` — header/footer specifics`);
1439
- }
1440
- parts.push(`- \`get_framework_guide("common-pitfalls")\` — observer rules, common mistakes`);
1441
1583
  parts.push("");
1442
1584
  // Completion
1443
1585
  parts.push(`## ${nextStep + 2}. Mark Complete`);
1444
1586
  parts.push("");
1445
- parts.push(`Once the section builds cleanly with \`npx ikas-component build\`:`);
1446
- parts.push(`1. Edit \`MIGRATION.md\` at the project root with your file-editing tool (no MCP tool needed)`);
1447
- parts.push(`2. Change the checkbox for \`${sectionId}\` from \`[ ]\` to \`[x]\``);
1448
- parts.push(`3. Also mark each child component as \`[x]\``);
1449
- parts.push(`4. If you made customData enum-vs-component decisions, log them under \`## Custom Data Decisions\` (one bullet per decision with reasoning)`);
1450
- parts.push(`5. Append an entry to the **Notes** 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.`);
1587
+ 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.`);
1451
1588
  parts.push("");
1452
1589
  return parts.join("\n");
1453
1590
  }
@@ -1493,8 +1630,7 @@ function levenshtein(a, b) {
1493
1630
  const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
1494
1631
  curr[j] = Math.min(curr[j - 1] + 1, // insertion
1495
1632
  prev[j] + 1, // deletion
1496
- prev[j - 1] + cost // substitution
1497
- );
1633
+ prev[j - 1] + cost);
1498
1634
  }
1499
1635
  for (let j = 0; j <= n; j++)
1500
1636
  prev[j] = curr[j];
@@ -1525,17 +1661,29 @@ function searchFunctions(query) {
1525
1661
  const scored = storefrontData.functions
1526
1662
  .map((fn) => {
1527
1663
  const nameScore = matchScore(fn.name, query) * 3;
1528
- const displayNameScore = fn.displayName ? matchScore(fn.displayName, query) * 3 : 0;
1664
+ const displayNameScore = fn.displayName
1665
+ ? matchScore(fn.displayName, query) * 3
1666
+ : 0;
1529
1667
  const descScore = matchScore(fn.description, query);
1530
- const catScore = fn.categories.some((c) => matchScore(c, query) > 0) ? 5 : 0;
1668
+ const catScore = fn.categories.some((c) => matchScore(c, query) > 0)
1669
+ ? 5
1670
+ : 0;
1531
1671
  const paramScore = fn.params.some((p) => matchScore(p.name, query) > 0 || matchScore(p.description, query) > 0)
1532
1672
  ? 2
1533
1673
  : 0;
1534
1674
  const sigScore = matchScore(fn.signature, query) * 2;
1535
- const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0) ? 8 : 0;
1675
+ const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0)
1676
+ ? 8
1677
+ : 0;
1536
1678
  return {
1537
1679
  fn,
1538
- score: nameScore + displayNameScore + descScore + catScore + paramScore + sigScore + typeScore,
1680
+ score: nameScore +
1681
+ displayNameScore +
1682
+ descScore +
1683
+ catScore +
1684
+ paramScore +
1685
+ sigScore +
1686
+ typeScore,
1539
1687
  };
1540
1688
  })
1541
1689
  .filter((item) => item.score > 0)
@@ -1549,7 +1697,11 @@ function searchFrameworkTopics(query) {
1549
1697
  const descScore = matchScore(topic.description, query) * 2;
1550
1698
  const contentScore = matchScore(topic.content, query);
1551
1699
  const tagScore = topic.tags.some((t) => matchScore(t, query) > 0) ? 5 : 0;
1552
- return { key, topic, score: titleScore + descScore + contentScore + tagScore };
1700
+ return {
1701
+ key,
1702
+ topic,
1703
+ score: titleScore + descScore + contentScore + tagScore,
1704
+ };
1553
1705
  })
1554
1706
  .filter((item) => item.score > 0)
1555
1707
  .sort((a, b) => b.score - a.score);
@@ -1564,7 +1716,9 @@ function searchTypes(query) {
1564
1716
  const propScore = td.properties?.some((p) => matchScore(p.name, query) > 0 || matchScore(p.type, query) > 0)
1565
1717
  ? 4
1566
1718
  : 0;
1567
- const enumScore = td.enumValues?.some((v) => matchScore(v, query) > 0) ? 4 : 0;
1719
+ const enumScore = td.enumValues?.some((v) => matchScore(v, query) > 0)
1720
+ ? 4
1721
+ : 0;
1568
1722
  return { td, score: nameScore + domainScore + propScore + enumScore };
1569
1723
  })
1570
1724
  .filter((item) => item.score > 0)
@@ -1606,8 +1760,12 @@ function formatFunctionDoc(fn) {
1606
1760
  return lines.join("\n");
1607
1761
  }
1608
1762
  function formatFunctionSummary(fn) {
1609
- const desc = fn.description ? fn.description.split(".")[0] + "." : "No description.";
1610
- const alias = fn.displayName && fn.displayName !== fn.name ? ` (alias: ${fn.displayName})` : "";
1763
+ const desc = fn.description
1764
+ ? fn.description.split(".")[0] + "."
1765
+ : "No description.";
1766
+ const alias = fn.displayName && fn.displayName !== fn.name
1767
+ ? ` (alias: ${fn.displayName})`
1768
+ : "";
1611
1769
  return `- \`${fn.name}\`${alias} - ${desc}`;
1612
1770
  }
1613
1771
  function formatTypeDefinition(td, opts = {}) {
@@ -1648,7 +1806,9 @@ function formatTypeDefinition(td, opts = {}) {
1648
1806
  const fn = storefrontData.functions.find((f) => f.name === fnName);
1649
1807
  if (fn) {
1650
1808
  const desc = fn.description ? fn.description.split(".")[0] + "." : "";
1651
- const alias = fn.displayName && fn.displayName !== fn.name ? ` (alias: ${fn.displayName})` : "";
1809
+ const alias = fn.displayName && fn.displayName !== fn.name
1810
+ ? ` (alias: ${fn.displayName})`
1811
+ : "";
1652
1812
  lines.push(`- **\`${fn.name}\`**${alias} — ${desc}`);
1653
1813
  lines.push(` \`${fn.signature}\``);
1654
1814
  }
@@ -1657,7 +1817,9 @@ function formatTypeDefinition(td, opts = {}) {
1657
1817
  }
1658
1818
  }
1659
1819
  lines.push("");
1660
- lines.push("Use `get_functions_for_type(\"" + td.name + "\")` for full documentation of these functions.");
1820
+ lines.push('Use `get_functions_for_type("' +
1821
+ td.name +
1822
+ '")` for full documentation of these functions.');
1661
1823
  }
1662
1824
  return lines.join("\n");
1663
1825
  }
@@ -1675,7 +1837,13 @@ const server = new McpServer({
1675
1837
  name: "ikas-code-components",
1676
1838
  version: "0.1.0",
1677
1839
  }, {
1678
- 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.",
1840
+ 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.\n\n" +
1841
+ "Live-editor actions (require `ikas-component dev` running with the editor connected): `list_editor_pages` → page ids; `list_imported_sections` → imported component ids; `import_section` → import a built component; `add_section_to_page` → place one section (`add_sections_to_page` places MANY at once and can set their props in the same call — fastest way to build a page); `list_page_sections` → the sections placed on a page (LEAN roster: per-placement `elementId`, `componentId`, `propCount`, which props are filled — NO schema/values, so it never truncates); `get_component_props` → a component's prop blueprint (types, ENUM valid `options`, COMPONENT_LIST `allowedComponentIds`) for any section OR child id; `get_section_values` → one section's current prop values (for read-modify-write); `get_page_by_type` → a theme page id by pageType (e.g. CATEGORY), to build PAGE links to entities (`create_page` adds that page if it does not exist yet, so you never guess a slug); `update_section_prop` → change a single prop value of a placed section (`update_page_sections` fills MANY sections/props of a page in ONE call — strongly prefer it to cut round-trips when filling content); `upload_image` → upload one image (file or URL) and get an image id (`upload_images` uploads many in one call — prefer it for several); `search_products` / `list_categories` / `list_brands` / `list_blogs` / `list_blog_categories` → find real entity ids for PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY prop values; `publish_theme` → publish the theme LIVE and get back the preview URL to review (guarded — needs `confirm:true`, and `confirm_production:true` for the main theme; only call it when the user explicitly asks to publish). For the full end-to-end process (placing a section and filling its content, with per-prop value shapes and COMPONENT_LIST rules), call `get_editor_workflow` first.\n\n" +
1842
+ "To CHANGE/EDIT a prop value (text, color, boolean, number, etc.) of a section that is already on a page, this IS supported: call `list_page_sections(page_id)` to get the placement `elementId` and the prop names/ids, then `update_section_prop(page_id, element_id, prop_name|prop_id, value)`. Do not assume prop editing is unavailable.\n\n" +
1843
+ "TWO DISTINCT JOBS — do not confuse them, and do not jump to writing code for the second one:\n" +
1844
+ "(A) DEFINING props / authoring a component = changing which props a component HAS. This edits the component source/config (types.ts via `ikas-component config add-prop`/`add-component`, JSX in index.tsx, etc.) and requires a rebuild. Use this ONLY when the needed prop does not exist yet, or the user explicitly asks to build/modify the component's code or prop schema.\n" +
1845
+ "(B) SETTING prop VALUES / filling content = giving values to props that ALREADY exist on a section placed on a page. This is pure data entry via `list_page_sections` + `update_section_prop` (and `upload_image` for images). It writes NO code and needs NO rebuild.\n" +
1846
+ "When the user says things like 'fill this section', 'set the heading/title/text/image/link', 'populate', 'change the content/value', or 'enter this data', that is JOB (B): use `update_section_prop`. First call `list_page_sections` and check the existing `props` — if the prop is already there (it usually is), just set its value. Only fall back to JOB (A) if the required prop genuinely does not exist in that section's `props`. Writing a new component or adding a prop to fill in a value the section already supports is the wrong move.",
1679
1847
  });
1680
1848
  // Tool: search_docs
1681
1849
  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 }) => {
@@ -1716,13 +1884,20 @@ server.tool("search_docs", "Search across all ikas storefront API docs, framewor
1716
1884
  parts.push("");
1717
1885
  parts.push("Use `get_migration_guide(topic)` to get full content for any migration topic.");
1718
1886
  }
1719
- if (functions.length === 0 && topics.length === 0 && types.length === 0 && migrationTopics.length === 0) {
1887
+ if (functions.length === 0 &&
1888
+ topics.length === 0 &&
1889
+ types.length === 0 &&
1890
+ migrationTopics.length === 0) {
1720
1891
  parts.push(`No results found for "${query}". Try different keywords or use \`list_functions()\` to see all available functions.`);
1721
1892
  }
1722
1893
  return { content: [{ type: "text", text: parts.join("\n") }] };
1723
1894
  });
1724
1895
  // Tool: get_function_doc
1725
- 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 }) => {
1896
+ server.tool("get_function_doc", "Get full documentation for a specific storefront API function including signature, parameters, return type, and example.", {
1897
+ name: z
1898
+ .string()
1899
+ .describe("Function name (e.g. 'addItemToCart', 'Router.navigate')"),
1900
+ }, async ({ name }) => {
1726
1901
  const nameLower = name.toLowerCase();
1727
1902
  // Phase 1: canonical-name match wins. A real function name always outranks
1728
1903
  // any displayName alias so aliases can never shadow the function they're
@@ -1730,7 +1905,9 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
1730
1905
  // [BP-DISPLAY-NAME: hasCustomer] alias).
1731
1906
  const byName = storefrontData.functions.find((f) => f.name.toLowerCase() === nameLower);
1732
1907
  if (byName) {
1733
- return { content: [{ type: "text", text: formatFunctionDoc(byName) }] };
1908
+ return {
1909
+ content: [{ type: "text", text: formatFunctionDoc(byName) }],
1910
+ };
1734
1911
  }
1735
1912
  // Phase 2: fall back to displayName aliases.
1736
1913
  const byAlias = storefrontData.functions.filter((f) => f.displayName && f.displayName.toLowerCase() === nameLower);
@@ -1738,7 +1915,9 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
1738
1915
  const fn = byAlias[0];
1739
1916
  const note = `> Note: "${name}" is a display alias for \`${fn.name}\`.\n\n`;
1740
1917
  return {
1741
- content: [{ type: "text", text: note + formatFunctionDoc(fn) }],
1918
+ content: [
1919
+ { type: "text", text: note + formatFunctionDoc(fn) },
1920
+ ],
1742
1921
  };
1743
1922
  }
1744
1923
  if (byAlias.length > 1) {
@@ -1756,7 +1935,9 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
1756
1935
  (f.displayName && f.displayName.toLowerCase().includes(nameLower)));
1757
1936
  if (matches.length > 0) {
1758
1937
  const suggestions = matches.slice(0, 5).map((f) => {
1759
- const alias = f.displayName && f.displayName !== f.name ? ` (alias: ${f.displayName})` : "";
1938
+ const alias = f.displayName && f.displayName !== f.name
1939
+ ? ` (alias: ${f.displayName})`
1940
+ : "";
1760
1941
  return ` - ${f.name}${alias}`;
1761
1942
  });
1762
1943
  return {
@@ -1769,7 +1950,12 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
1769
1950
  };
1770
1951
  }
1771
1952
  return {
1772
- content: [{ type: "text", text: `Function "${name}" not found. Use \`list_functions()\` to see all available functions.` }],
1953
+ content: [
1954
+ {
1955
+ type: "text",
1956
+ text: `Function "${name}" not found. Use \`list_functions()\` to see all available functions.`,
1957
+ },
1958
+ ],
1773
1959
  };
1774
1960
  });
1775
1961
  // Tool: list_functions
@@ -1819,7 +2005,7 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
1819
2005
  if (uncategorized > 0) {
1820
2006
  lines.push(`- \`Other\` (${uncategorized})`);
1821
2007
  }
1822
- lines.push("", "Call `list_functions(category: \"<name>\")` to see one-line summaries for a category.");
2008
+ lines.push("", 'Call `list_functions(category: "<name>")` to see one-line summaries for a category.');
1823
2009
  return { content: [{ type: "text", text: lines.join("\n") }] };
1824
2010
  }
1825
2011
  const catLower = category.toLowerCase();
@@ -1851,7 +2037,11 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
1851
2037
  return { content: [{ type: "text", text: parts.join("\n") }] };
1852
2038
  });
1853
2039
  // Tool: get_code_example
1854
- server.tool("get_code_example", "Get an API usage reference for a specific task. Shows correct function calls, imports, and data-handling patterns from a real production theme. The JSX layout and CSS are illustrative only — create your own original visual design. Call `list_examples()` to see available example IDs.", { task: z.string().describe("Task description or example ID (call `list_examples()` for the full list)") }, async ({ task }) => {
2040
+ server.tool("get_code_example", "Get an API usage reference for a specific task. Shows correct function calls, imports, and data-handling patterns from a real production theme. The JSX layout and CSS are illustrative only — create your own original visual design. Call `list_examples()` to see available example IDs.", {
2041
+ task: z
2042
+ .string()
2043
+ .describe("Task description or example ID (call `list_examples()` for the full list)"),
2044
+ }, async ({ task }) => {
1855
2045
  const taskLower = task.toLowerCase();
1856
2046
  // Try exact ID match first
1857
2047
  let example = storefrontData.codeExamples.find((e) => e.id === taskLower);
@@ -1871,7 +2061,9 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
1871
2061
  }
1872
2062
  }
1873
2063
  if (!example) {
1874
- const available = storefrontData.codeExamples.map((e) => ` - \`${e.id}\` - ${e.title}`).join("\n");
2064
+ const available = storefrontData.codeExamples
2065
+ .map((e) => ` - \`${e.id}\` - ${e.title}`)
2066
+ .join("\n");
1875
2067
  return {
1876
2068
  content: [
1877
2069
  {
@@ -1894,14 +2086,24 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
1894
2086
  if (example.files && example.files.length > 0) {
1895
2087
  for (const file of example.files) {
1896
2088
  const ext = file.filename.split(".").pop() || "text";
1897
- const lang = ext === "tsx" || ext === "ts" ? "typescript" : ext === "css" ? "css" : ext === "json" ? "json" : "text";
2089
+ const lang = ext === "tsx" || ext === "ts"
2090
+ ? "typescript"
2091
+ : ext === "css"
2092
+ ? "css"
2093
+ : ext === "json"
2094
+ ? "json"
2095
+ : "text";
1898
2096
  // Add inline originality comments to CSS and TSX files
1899
2097
  let content = file.content;
1900
2098
  if (ext === "css") {
1901
- content = "/* EXAMPLE STYLING — create your own original CSS with different class names and design */\n" + content;
2099
+ content =
2100
+ "/* EXAMPLE STYLING — create your own original CSS with different class names and design */\n" +
2101
+ content;
1902
2102
  }
1903
2103
  else if (ext === "tsx") {
1904
- content = "// EXAMPLE COMPONENT — use the API patterns but create your own JSX structure and layout\n" + content;
2104
+ content =
2105
+ "// EXAMPLE COMPONENT — use the API patterns but create your own JSX structure and layout\n" +
2106
+ content;
1905
2107
  }
1906
2108
  parts.push(`### ${file.filename}`, "", `\`\`\`${lang}`, content, "```", "");
1907
2109
  }
@@ -1915,63 +2117,94 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
1915
2117
  return { content: [{ type: "text", text: parts.join("\n") }] };
1916
2118
  });
1917
2119
  // Tool: get_framework_guide
1918
- server.tool("get_framework_guide", "Get a framework guide on a specific topic (e.g. 'ai-workflow', 'common-pitfalls', 'prop-types', 'css-scoping', 'form-handling'). Call `list_topics()` to see all available topic keys.", { topic: z.string().describe("Topic key or keyword (call `list_topics()` for the full list)") }, async ({ topic }) => {
2120
+ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e.g. 'ai-workflow', 'common-pitfalls', 'prop-types', 'css-scoping', 'form-handling'). Call `list_topics()` to see all available topic keys.", {
2121
+ topic: z
2122
+ .string()
2123
+ .describe("Topic key or keyword (call `list_topics()` for the full list)"),
2124
+ }, async ({ topic }) => {
1919
2125
  const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
1920
2126
  // Alias mapping for common alternative topic names
1921
2127
  const topicAliases = {
1922
2128
  "form-handling": "form-patterns",
1923
- "forms": "form-patterns",
2129
+ forms: "form-patterns",
1924
2130
  "data-fetching": "async-data-patterns",
1925
- "async": "async-data-patterns",
1926
- "loading": "async-data-patterns",
2131
+ async: "async-data-patterns",
2132
+ loading: "async-data-patterns",
1927
2133
  "sub-components": "sub-component-patterns",
1928
- "subcomponents": "sub-component-patterns",
1929
- "routing": "navigation-patterns",
1930
- "router": "navigation-patterns",
1931
- "observer": "component-structure",
1932
- "reactivity": "component-structure",
1933
- "pitfalls": "common-pitfalls",
1934
- "gotchas": "common-pitfalls",
1935
- "mistakes": "common-pitfalls",
1936
- "header": "header-footer-patterns",
1937
- "footer": "header-footer-patterns",
1938
- "blog": "blog-patterns",
1939
- "cart": "cart-patterns",
1940
- "account": "account-patterns",
2134
+ subcomponents: "sub-component-patterns",
2135
+ routing: "navigation-patterns",
2136
+ router: "navigation-patterns",
2137
+ observer: "component-structure",
2138
+ reactivity: "component-structure",
2139
+ pitfalls: "common-pitfalls",
2140
+ gotchas: "common-pitfalls",
2141
+ mistakes: "common-pitfalls",
2142
+ header: "header-footer-patterns",
2143
+ footer: "header-footer-patterns",
2144
+ blog: "blog-patterns",
2145
+ cart: "cart-patterns",
2146
+ account: "account-patterns",
1941
2147
  "product-detail": "product-detail-patterns",
1942
2148
  "product-list": "product-list-patterns",
1943
- "filtering": "product-list-patterns",
1944
- "reviews": "review-patterns",
1945
- "slider": "slider-overlay-patterns",
1946
- "overlay": "slider-overlay-patterns",
1947
- "modal": "slider-overlay-patterns",
1948
- "architecture": "real-world-architecture",
1949
- "theme": "real-world-architecture",
2149
+ filtering: "product-list-patterns",
2150
+ reviews: "review-patterns",
2151
+ slider: "slider-overlay-patterns",
2152
+ overlay: "slider-overlay-patterns",
2153
+ modal: "slider-overlay-patterns",
2154
+ architecture: "real-world-architecture",
2155
+ theme: "real-world-architecture",
1950
2156
  "global-styles": "global-css",
1951
- "global": "global-css",
2157
+ global: "global-css",
1952
2158
  "css-variables": "global-css",
1953
2159
  "custom-properties": "global-css",
2160
+ "translation": "translations",
2161
+ "localization": "translations",
2162
+ "localize": "translations",
2163
+ "locale": "translations",
2164
+ "i18n": "translations",
2165
+ "çeviri": "translations",
1954
2166
  };
1955
2167
  const resolvedTopic = topicAliases[topicLower] || topicLower;
1956
2168
  // Topics that involve MobX store reads get a reminder about root reactivity
1957
2169
  const storeTopics = new Set([
1958
- "product-detail-patterns", "product-list-patterns", "cart-patterns",
1959
- "account-patterns", "header-footer-patterns", "review-patterns",
1960
- "blog-patterns", "form-handling", "async-data-patterns",
1961
- "component-structure", "imports",
2170
+ "product-detail-patterns",
2171
+ "product-list-patterns",
2172
+ "cart-patterns",
2173
+ "account-patterns",
2174
+ "header-footer-patterns",
2175
+ "review-patterns",
2176
+ "blog-patterns",
2177
+ "form-handling",
2178
+ "async-data-patterns",
2179
+ "component-structure",
2180
+ "imports",
1962
2181
  ]);
1963
2182
  const observerReminder = "> **IMPORTANT: Do NOT use `observer()` on root component exports.** The ikas runtime wraps root renders in MobX `autorun()`, making them automatically reactive. All store reads (`cartStore`, `customerStore`, etc.) in root components are tracked automatically. Only use `observer()` on extracted sub-components.\n\n";
1964
2183
  // Try exact key match (with alias resolution)
1965
2184
  if (frameworkData.topics[resolvedTopic]) {
1966
2185
  const t = frameworkData.topics[resolvedTopic];
1967
2186
  const prefix = storeTopics.has(resolvedTopic) ? observerReminder : "";
1968
- return { content: [{ type: "text", text: `## ${t.title}\n\n${prefix}${t.content}` }] };
2187
+ return {
2188
+ content: [
2189
+ {
2190
+ type: "text",
2191
+ text: `## ${t.title}\n\n${prefix}${t.content}`,
2192
+ },
2193
+ ],
2194
+ };
1969
2195
  }
1970
2196
  // Try original topic key (without alias) in case it's a direct key
1971
2197
  if (resolvedTopic !== topicLower && frameworkData.topics[topicLower]) {
1972
2198
  const t = frameworkData.topics[topicLower];
1973
2199
  const prefix = storeTopics.has(topicLower) ? observerReminder : "";
1974
- return { content: [{ type: "text", text: `## ${t.title}\n\n${prefix}${t.content}` }] };
2200
+ return {
2201
+ content: [
2202
+ {
2203
+ type: "text",
2204
+ text: `## ${t.title}\n\n${prefix}${t.content}`,
2205
+ },
2206
+ ],
2207
+ };
1975
2208
  }
1976
2209
  // Try keyword search
1977
2210
  const matches = searchFrameworkTopics(topic);
@@ -1979,7 +2212,12 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
1979
2212
  const best = matches[0];
1980
2213
  const prefix = storeTopics.has(best.key) ? observerReminder : "";
1981
2214
  return {
1982
- content: [{ type: "text", text: `## ${best.topic.title}\n\n${prefix}${best.topic.content}` }],
2215
+ content: [
2216
+ {
2217
+ type: "text",
2218
+ text: `## ${best.topic.title}\n\n${prefix}${best.topic.content}`,
2219
+ },
2220
+ ],
1983
2221
  };
1984
2222
  }
1985
2223
  const available = Object.entries(frameworkData.topics)
@@ -1995,16 +2233,27 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
1995
2233
  };
1996
2234
  });
1997
2235
  // Tool: get_type_definition
1998
- server.tool("get_type_definition", "Get the full definition of a storefront type or enum by name (e.g. 'IkasProduct', 'IkasOrderStatus'). Shows all properties with types, extends, or enum values.", { name: z.string().describe("Type or enum name (e.g. 'IkasProduct', 'IkasOrderStatus')") }, async ({ name }) => {
2236
+ server.tool("get_type_definition", "Get the full definition of a storefront type or enum by name (e.g. 'IkasProduct', 'IkasOrderStatus'). Shows all properties with types, extends, or enum values.", {
2237
+ name: z
2238
+ .string()
2239
+ .describe("Type or enum name (e.g. 'IkasProduct', 'IkasOrderStatus')"),
2240
+ }, async ({ name }) => {
1999
2241
  if (!typesData) {
2000
2242
  return {
2001
- content: [{ type: "text", text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first." }],
2243
+ content: [
2244
+ {
2245
+ type: "text",
2246
+ text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
2247
+ },
2248
+ ],
2002
2249
  };
2003
2250
  }
2004
2251
  const nameLower = name.toLowerCase();
2005
2252
  const td = typesData.types.find((t) => t.name.toLowerCase() === nameLower);
2006
2253
  if (td) {
2007
- return { content: [{ type: "text", text: formatTypeDefinition(td) }] };
2254
+ return {
2255
+ content: [{ type: "text", text: formatTypeDefinition(td) }],
2256
+ };
2008
2257
  }
2009
2258
  // Fuzzy match
2010
2259
  const matches = typesData.types.filter((t) => t.name.toLowerCase().includes(nameLower));
@@ -2020,14 +2269,28 @@ server.tool("get_type_definition", "Get the full definition of a storefront type
2020
2269
  };
2021
2270
  }
2022
2271
  return {
2023
- content: [{ type: "text", text: `Type "${name}" not found. Use \`list_types()\` to see all available types.` }],
2272
+ content: [
2273
+ {
2274
+ type: "text",
2275
+ text: `Type "${name}" not found. Use \`list_types()\` to see all available types.`,
2276
+ },
2277
+ ],
2024
2278
  };
2025
2279
  });
2026
2280
  // Tool: get_functions_for_type
2027
- server.tool("get_functions_for_type", "Get full documentation for all utility functions that operate on a given storefront type. For example, get_functions_for_type('IkasImage') returns getSrc, getDefaultSrc, getThumbnailSrc, createMediaSrcset with full signatures, descriptions, and examples.", { typeName: z.string().describe("Type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')") }, async ({ typeName }) => {
2281
+ server.tool("get_functions_for_type", "Get full documentation for all utility functions that operate on a given storefront type. For example, get_functions_for_type('IkasImage') returns getSrc, getDefaultSrc, getThumbnailSrc, createMediaSrcset with full signatures, descriptions, and examples.", {
2282
+ typeName: z
2283
+ .string()
2284
+ .describe("Type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
2285
+ }, async ({ typeName }) => {
2028
2286
  if (!typesData) {
2029
2287
  return {
2030
- content: [{ type: "text", text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first." }],
2288
+ content: [
2289
+ {
2290
+ type: "text",
2291
+ text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
2292
+ },
2293
+ ],
2031
2294
  };
2032
2295
  }
2033
2296
  const nameLower = typeName.toLowerCase();
@@ -2047,7 +2310,12 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
2047
2310
  };
2048
2311
  }
2049
2312
  return {
2050
- content: [{ type: "text", text: `Type "${typeName}" not found. Use \`list_types()\` to see all available types.` }],
2313
+ content: [
2314
+ {
2315
+ type: "text",
2316
+ text: `Type "${typeName}" not found. Use \`list_types()\` to see all available types.`,
2317
+ },
2318
+ ],
2051
2319
  };
2052
2320
  }
2053
2321
  if (!td.relatedFunctions || td.relatedFunctions.length === 0) {
@@ -2078,7 +2346,10 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
2078
2346
  });
2079
2347
  // Tool: get_model_guide
2080
2348
  server.tool("get_model_guide", "Get an overview of a storefront model type. By default returns the type definition, related function names with one-line summaries, matching example titles/IDs, and related type summaries. Pass `mode: 'full'` to inline full function docs and example code.", {
2081
- model: z.string().optional().describe("Model type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
2349
+ model: z
2350
+ .string()
2351
+ .optional()
2352
+ .describe("Model type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
2082
2353
  name: z.string().optional().describe("Alias for 'model'"),
2083
2354
  mode: z
2084
2355
  .enum(["summary", "full"])
@@ -2089,12 +2360,22 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
2089
2360
  const model = modelParam || nameParam;
2090
2361
  if (!model) {
2091
2362
  return {
2092
- content: [{ type: "text", text: "Please provide a model name (e.g. 'IkasProduct', 'IkasOrder'). Use the 'model' or 'name' parameter." }],
2363
+ content: [
2364
+ {
2365
+ type: "text",
2366
+ text: "Please provide a model name (e.g. 'IkasProduct', 'IkasOrder'). Use the 'model' or 'name' parameter.",
2367
+ },
2368
+ ],
2093
2369
  };
2094
2370
  }
2095
2371
  if (!typesData) {
2096
2372
  return {
2097
- content: [{ type: "text", text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first." }],
2373
+ content: [
2374
+ {
2375
+ type: "text",
2376
+ text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
2377
+ },
2378
+ ],
2098
2379
  };
2099
2380
  }
2100
2381
  const modelLower = model.toLowerCase();
@@ -2116,7 +2397,12 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
2116
2397
  };
2117
2398
  }
2118
2399
  return {
2119
- content: [{ type: "text", text: `Model "${model}" not found. Use \`list_types()\` to see all available types.` }],
2400
+ content: [
2401
+ {
2402
+ type: "text",
2403
+ text: `Model "${model}" not found. Use \`list_types()\` to see all available types.`,
2404
+ },
2405
+ ],
2120
2406
  };
2121
2407
  }
2122
2408
  const parts = [`# Model Guide: ${td.name}\n`];
@@ -2224,13 +2510,23 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
2224
2510
  server.tool("search_types", "Search storefront types and enums by keyword (e.g. 'price', 'address', 'status'). Returns top matches ranked by relevance.", { query: z.string().describe("Search keyword or phrase") }, async ({ query }) => {
2225
2511
  if (!typesData) {
2226
2512
  return {
2227
- content: [{ type: "text", text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first." }],
2513
+ content: [
2514
+ {
2515
+ type: "text",
2516
+ text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
2517
+ },
2518
+ ],
2228
2519
  };
2229
2520
  }
2230
2521
  const results = searchTypes(query).slice(0, 15);
2231
2522
  if (results.length === 0) {
2232
2523
  return {
2233
- content: [{ type: "text", text: `No types found matching "${query}". Use \`list_types()\` to see all available types.` }],
2524
+ content: [
2525
+ {
2526
+ type: "text",
2527
+ text: `No types found matching "${query}". Use \`list_types()\` to see all available types.`,
2528
+ },
2529
+ ],
2234
2530
  };
2235
2531
  }
2236
2532
  const parts = [`## Type Search Results for "${query}"\n`];
@@ -2294,7 +2590,7 @@ server.tool("list_types", "List storefront types and enums. Use `domain` and/or
2294
2590
  for (const [d, count] of sorted) {
2295
2591
  lines.push(`- \`${d}\` (${count})`);
2296
2592
  }
2297
- lines.push("", "Call `list_types(domain: \"<name>\")` to see summaries for a domain.");
2593
+ lines.push("", 'Call `list_types(domain: "<name>")` to see summaries for a domain.');
2298
2594
  return { content: [{ type: "text", text: lines.join("\n") }] };
2299
2595
  }
2300
2596
  const domainLower = domain.toLowerCase();
@@ -2333,15 +2629,25 @@ server.tool("list_types", "List storefront types and enums. Use `domain` and/or
2333
2629
  return { content: [{ type: "text", text: parts.join("\n") }] };
2334
2630
  });
2335
2631
  // Tool: get_prop_types
2336
- server.tool("get_prop_types", "Get all available ikas.config.json prop types with descriptions, TypeScript types, and examples. Tip: Use `npx ikas-component config add-component --props '[...]'` to create a component with all props in one command, or `add-prop` to add props incrementally. NEVER manually edit types.ts — it is auto-generated by the CLI.", {}, async () => {
2632
+ server.tool("get_prop_types", "Get all available ikas.config.json prop types with descriptions, TypeScript types, and examples. NOTE: these are the TypeScript prop-definition types for authoring a component — they are NOT the runtime JSON value shapes you write via `update_section_prop` (e.g. a TEXT prop is typed `string` here but written as `{ \"value\": \"...\" }`, and an IMAGE is `IkasImage | null` here but written as `{ \"id\": \"...\" }`). For the value shapes to pass to `update_section_prop`, see that tool's description. Tip: Use `npx ikas-component config add-component --props '[...]'` to create a component with all props in one command, or `add-prop` to add props incrementally. NEVER manually edit types.ts — it is auto-generated by the CLI.", {}, async () => {
2337
2633
  const propTypesTopic = frameworkData.topics["prop-types"];
2338
2634
  if (propTypesTopic) {
2339
2635
  return {
2340
- content: [{ type: "text", text: `## ${propTypesTopic.title}\n\n${propTypesTopic.content}` }],
2636
+ content: [
2637
+ {
2638
+ type: "text",
2639
+ text: `## ${propTypesTopic.title}\n\n${propTypesTopic.content}`,
2640
+ },
2641
+ ],
2341
2642
  };
2342
2643
  }
2343
2644
  return {
2344
- content: [{ type: "text", text: "Prop types documentation not available." }],
2645
+ content: [
2646
+ {
2647
+ type: "text",
2648
+ text: "Prop types documentation not available.",
2649
+ },
2650
+ ],
2345
2651
  };
2346
2652
  });
2347
2653
  // Tool: get_section_template
@@ -2438,7 +2744,8 @@ server.tool("get_section_template", "Get the root files of a starter section tem
2438
2744
  // the end of the response in the existing recipe-builder block.
2439
2745
  {
2440
2746
  const snippetStrForBanner = bundle.rootFiles["ikas-config-snippet.json"];
2441
- if (snippetStrForBanner && /<id-of-[A-Za-z0-9_]+>/.test(snippetStrForBanner)) {
2747
+ if (snippetStrForBanner &&
2748
+ /<id-of-[A-Za-z0-9_]+>/.test(snippetStrForBanner)) {
2442
2749
  const childMatches = Array.from(snippetStrForBanner.matchAll(/<id-of-([A-Za-z0-9_]+)>/g));
2443
2750
  const uniqueChildren = Array.from(new Set(childMatches.map((m) => m[1])));
2444
2751
  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.`, "");
@@ -2562,7 +2869,10 @@ server.tool("get_section_template", "Get the root files of a starter section tem
2562
2869
  try {
2563
2870
  const childSnippet = JSON.parse(fs.readFileSync(childSnippetPath, "utf-8"));
2564
2871
  const childProps = (childSnippet.props || []).map((p) => {
2565
- const out = { name: p.name, type: p.type };
2872
+ const out = {
2873
+ name: p.name,
2874
+ type: p.type,
2875
+ };
2566
2876
  if (p.displayName)
2567
2877
  out.displayName = p.displayName;
2568
2878
  if (p.required)
@@ -2635,10 +2945,7 @@ server.tool("get_section_child", "Fetch one item's files from a section's childr
2635
2945
  .string()
2636
2946
  .optional()
2637
2947
  .describe("The item name as listed in `get_section_template`'s response (Children/Components/Sub-components)"),
2638
- child: z
2639
- .string()
2640
- .optional()
2641
- .describe("Alias for `name`"),
2948
+ child: z.string().optional().describe("Alias for `name`"),
2642
2949
  kind: z
2643
2950
  .enum(["children", "components", "sub-components"])
2644
2951
  .optional()
@@ -2801,7 +3108,11 @@ server.tool("list_section_types", "List all available `get_section_template` sec
2801
3108
  });
2802
3109
  // --- Migration tools ---
2803
3110
  // Tool: analyze_old_theme
2804
- 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 }) => {
3111
+ 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.", {
3112
+ theme_json: z
3113
+ .string()
3114
+ .describe("The raw JSON content of the old theme.json file"),
3115
+ }, async ({ theme_json }) => {
2805
3116
  try {
2806
3117
  const parsed = JSON.parse(theme_json);
2807
3118
  const analysis = analyzeOldTheme(parsed);
@@ -2809,102 +3120,162 @@ server.tool("analyze_old_theme", "Analyze an old ikas storefront theme.json and
2809
3120
  }
2810
3121
  catch (err) {
2811
3122
  return {
2812
- content: [{ type: "text", text: `Error parsing theme.json: ${err instanceof Error ? err.message : String(err)}. Make sure you're passing valid JSON.` }],
3123
+ content: [
3124
+ {
3125
+ type: "text",
3126
+ text: `Error parsing theme.json: ${err instanceof Error ? err.message : String(err)}. Make sure you're passing valid JSON.`,
3127
+ },
3128
+ ],
2813
3129
  };
2814
3130
  }
2815
3131
  });
2816
3132
  // Tool: get_migration_guide
2817
3133
  const migrationTopicAliases = {
2818
- "overview": "migration-overview",
2819
- "migrate": "migration-overview",
2820
- "custom": "custom-data-conversion",
3134
+ overview: "migration-overview",
3135
+ migrate: "migration-overview",
3136
+ custom: "custom-data-conversion",
2821
3137
  "custom-data": "custom-data-conversion",
2822
- "customdata": "custom-data-conversion",
3138
+ customdata: "custom-data-conversion",
2823
3139
  "dynamic-list": "custom-data-conversion",
2824
3140
  "component-list": "custom-data-conversion",
2825
- "slider": "prop-type-mapping",
2826
- "props": "prop-type-mapping",
3141
+ slider: "prop-type-mapping",
3142
+ props: "prop-type-mapping",
2827
3143
  "prop-mapping": "prop-type-mapping",
2828
- "types": "prop-type-mapping",
2829
- "react": "react-to-preact",
2830
- "preact": "react-to-preact",
2831
- "observer": "react-to-preact",
2832
- "libraries": "library-replacements",
2833
- "swiper": "library-replacements",
2834
- "headlessui": "library-replacements",
2835
- "tailwind": "library-replacements",
2836
- "tailwindcss": "library-replacements",
2837
- "recharts": "library-replacements",
2838
- "marquee": "library-replacements",
2839
- "imports": "storefront-import-mapping",
2840
- "storefront": "storefront-import-mapping",
3144
+ types: "prop-type-mapping",
3145
+ react: "react-to-preact",
3146
+ preact: "react-to-preact",
3147
+ observer: "react-to-preact",
3148
+ libraries: "library-replacements",
3149
+ swiper: "library-replacements",
3150
+ headlessui: "library-replacements",
3151
+ tailwind: "library-replacements",
3152
+ tailwindcss: "library-replacements",
3153
+ recharts: "library-replacements",
3154
+ marquee: "library-replacements",
3155
+ imports: "storefront-import-mapping",
3156
+ storefront: "storefront-import-mapping",
2841
3157
  "bp-storefront": "storefront-import-mapping",
2842
3158
  "theme-json": "theme-json-anatomy",
2843
- "anatomy": "theme-json-anatomy",
2844
- "decompose": "component-decomposition-strategy",
2845
- "decomposition": "component-decomposition-strategy",
2846
- "strategy": "component-decomposition-strategy",
2847
- "project": "complete-project-generation",
2848
- "generate": "complete-project-generation",
2849
- "generation": "complete-project-generation",
2850
- "settings": "settings-conversion",
2851
- "colors": "settings-conversion",
2852
- "fonts": "settings-conversion",
2853
- "find": "finding-new-system-equivalents",
2854
- "search": "finding-new-system-equivalents",
2855
- "discover": "finding-new-system-equivalents",
2856
- "equivalent": "finding-new-system-equivalents",
2857
- "equivalents": "finding-new-system-equivalents",
2858
- "replacement": "finding-new-system-equivalents",
3159
+ anatomy: "theme-json-anatomy",
3160
+ decompose: "component-decomposition-strategy",
3161
+ decomposition: "component-decomposition-strategy",
3162
+ strategy: "component-decomposition-strategy",
3163
+ project: "complete-project-generation",
3164
+ generate: "complete-project-generation",
3165
+ generation: "complete-project-generation",
3166
+ settings: "settings-conversion",
3167
+ colors: "settings-conversion",
3168
+ fonts: "settings-conversion",
3169
+ find: "finding-new-system-equivalents",
3170
+ search: "finding-new-system-equivalents",
3171
+ discover: "finding-new-system-equivalents",
3172
+ equivalent: "finding-new-system-equivalents",
3173
+ equivalents: "finding-new-system-equivalents",
3174
+ replacement: "finding-new-system-equivalents",
2859
3175
  };
2860
3176
  const migrationTopicKeys = migrationData
2861
3177
  ? Object.keys(migrationData.topics)
2862
3178
  : [];
2863
- 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 }) => {
3179
+ 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.`, {
3180
+ topic: z
3181
+ .string()
3182
+ .describe("Migration topic key, alias, or 'list' to see all topics"),
3183
+ }, async ({ topic }) => {
2864
3184
  if (!migrationData) {
2865
- return { content: [{ type: "text", text: "Migration data not available. Ensure data/migration.json exists." }] };
3185
+ return {
3186
+ content: [
3187
+ {
3188
+ type: "text",
3189
+ text: "Migration data not available. Ensure data/migration.json exists.",
3190
+ },
3191
+ ],
3192
+ };
2866
3193
  }
2867
3194
  if (topic.toLowerCase() === "list") {
2868
3195
  const available = Object.entries(migrationData.topics)
2869
3196
  .map(([key, t]) => `- \`${key}\` — ${t.title}: ${t.description}`)
2870
3197
  .join("\n");
2871
- return { content: [{ type: "text", text: `## Available Migration Topics\n\n${available}` }] };
3198
+ return {
3199
+ content: [
3200
+ {
3201
+ type: "text",
3202
+ text: `## Available Migration Topics\n\n${available}`,
3203
+ },
3204
+ ],
3205
+ };
2872
3206
  }
2873
3207
  const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
2874
3208
  const resolvedTopic = migrationTopicAliases[topicLower] || topicLower;
2875
3209
  if (migrationData.topics[resolvedTopic]) {
2876
3210
  const t = migrationData.topics[resolvedTopic];
2877
- return { content: [{ type: "text", text: `## ${t.title}\n\n${t.content}` }] };
3211
+ return {
3212
+ content: [
3213
+ { type: "text", text: `## ${t.title}\n\n${t.content}` },
3214
+ ],
3215
+ };
2878
3216
  }
2879
3217
  // Try original key
2880
3218
  if (resolvedTopic !== topicLower && migrationData.topics[topicLower]) {
2881
3219
  const t = migrationData.topics[topicLower];
2882
- return { content: [{ type: "text", text: `## ${t.title}\n\n${t.content}` }] };
3220
+ return {
3221
+ content: [
3222
+ { type: "text", text: `## ${t.title}\n\n${t.content}` },
3223
+ ],
3224
+ };
2883
3225
  }
2884
3226
  // Keyword search
2885
3227
  const matches = searchMigrationTopics(topic);
2886
3228
  if (matches.length > 0) {
2887
3229
  const best = matches[0];
2888
- return { content: [{ type: "text", text: `## ${best.topic.title}\n\n${best.topic.content}` }] };
3230
+ return {
3231
+ content: [
3232
+ {
3233
+ type: "text",
3234
+ text: `## ${best.topic.title}\n\n${best.topic.content}`,
3235
+ },
3236
+ ],
3237
+ };
2889
3238
  }
2890
3239
  const available = Object.entries(migrationData.topics)
2891
3240
  .map(([key, t]) => ` - \`${key}\` - ${t.title}`)
2892
3241
  .join("\n");
2893
3242
  return {
2894
- content: [{ type: "text", text: `Migration topic "${topic}" not found. Available topics:\n${available}` }],
3243
+ content: [
3244
+ {
3245
+ type: "text",
3246
+ text: `Migration topic "${topic}" not found. Available topics:\n${available}`,
3247
+ },
3248
+ ],
2895
3249
  };
2896
3250
  });
2897
3251
  // Tool: get_migration_example
2898
- 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 }) => {
3252
+ 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.`, {
3253
+ example: z.string().describe("Example name or 'list' to see all examples"),
3254
+ }, async ({ example }) => {
2899
3255
  if (example.toLowerCase() === "list") {
2900
3256
  if (migrationExampleNames.length === 0) {
2901
- return { content: [{ type: "text", text: "No migration examples available." }] };
3257
+ return {
3258
+ content: [
3259
+ { type: "text", text: "No migration examples available." },
3260
+ ],
3261
+ };
2902
3262
  }
2903
- const list = migrationExampleNames.map((name) => {
3263
+ const list = migrationExampleNames
3264
+ .map((name) => {
2904
3265
  const ex = loadMigrationExample(name);
2905
- return ex ? `- \`${name}\` — ${ex.title}: ${ex.description}` : `- \`${name}\``;
2906
- }).join("\n");
2907
- return { content: [{ type: "text", text: `## Available Migration Examples\n\n${list}` }] };
3266
+ return ex
3267
+ ? `- \`${name}\` — ${ex.title}: ${ex.description}`
3268
+ : `- \`${name}\``;
3269
+ })
3270
+ .join("\n");
3271
+ return {
3272
+ content: [
3273
+ {
3274
+ type: "text",
3275
+ text: `## Available Migration Examples\n\n${list}`,
3276
+ },
3277
+ ],
3278
+ };
2908
3279
  }
2909
3280
  const exampleLower = example.toLowerCase();
2910
3281
  let exName = migrationExampleNames.find((n) => n === exampleLower);
@@ -2914,19 +3285,26 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
2914
3285
  if (!exName) {
2915
3286
  const available = migrationExampleNames.join(", ");
2916
3287
  return {
2917
- content: [{ type: "text", text: `Migration example "${example}" not found. Available: ${available}` }],
3288
+ content: [
3289
+ {
3290
+ type: "text",
3291
+ text: `Migration example "${example}" not found. Available: ${available}`,
3292
+ },
3293
+ ],
2918
3294
  };
2919
3295
  }
2920
3296
  const ex = loadMigrationExample(exName);
2921
3297
  if (!ex) {
2922
- return { content: [{ type: "text", text: `Failed to load migration example "${exName}".` }] };
3298
+ return {
3299
+ content: [
3300
+ {
3301
+ type: "text",
3302
+ text: `Failed to load migration example "${exName}".`,
3303
+ },
3304
+ ],
3305
+ };
2923
3306
  }
2924
- const parts = [
2925
- `## ${ex.title}`,
2926
- "",
2927
- ex.description,
2928
- "",
2929
- ];
3307
+ const parts = [`## ${ex.title}`, "", ex.description, ""];
2930
3308
  for (const [filename, content] of Object.entries(ex.files)) {
2931
3309
  const ext = filename.split(".").pop() || "text";
2932
3310
  const lang = ext === "tsx" || ext === "ts"
@@ -2945,13 +3323,31 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
2945
3323
  });
2946
3324
  // Tool: plan_migration
2947
3325
  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).", {
2948
- 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."),
2949
- theme_json_path: z.string().optional().describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
2950
- project_name: z.string().optional().describe("Target new project name, used to prefix migration-tracking IDs (default: 'my-theme')"),
2951
- 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 partialatomic components used by only 1-2 sections will be missed and must be added by the LLM."),
2952
- 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."),
2953
- 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."),
2954
- }, async ({ theme_json, theme_json_path, project_name, old_source_dir, project_root, overwrite }) => {
3326
+ theme_json: z
3327
+ .string()
3328
+ .optional()
3329
+ .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."),
3330
+ theme_json_path: z
3331
+ .string()
3332
+ .optional()
3333
+ .describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
3334
+ project_name: z
3335
+ .string()
3336
+ .optional()
3337
+ .describe("Target new project name, used to prefix migration-tracking IDs (default: 'my-theme')"),
3338
+ old_source_dir: z
3339
+ .string()
3340
+ .optional()
3341
+ .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."),
3342
+ project_root: z
3343
+ .string()
3344
+ .optional()
3345
+ .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."),
3346
+ overwrite: z
3347
+ .boolean()
3348
+ .optional()
3349
+ .describe("If MIGRATION.md already exists at <project_root>/MIGRATION.md and is non-empty, refuse the write unless this is true. Default: false."),
3350
+ }, async ({ theme_json, theme_json_path, project_name, old_source_dir, project_root, overwrite, }) => {
2955
3351
  try {
2956
3352
  const parsed = resolveThemeJson(theme_json, theme_json_path);
2957
3353
  const projectName = project_name || "my-theme";
@@ -3012,18 +3408,37 @@ server.tool("plan_migration", "Generate the **initial** migration plan and (when
3012
3408
  }
3013
3409
  catch (err) {
3014
3410
  return {
3015
- content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
3411
+ content: [
3412
+ {
3413
+ type: "text",
3414
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
3415
+ },
3416
+ ],
3016
3417
  };
3017
3418
  }
3018
3419
  });
3019
3420
  // Tool: get_section_migration_plan
3020
3421
  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.", {
3021
- theme_json: z.string().optional().describe("Raw JSON content of the old theme.json. EITHER this OR theme_json_path is required (not both)."),
3022
- theme_json_path: z.string().optional().describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
3023
- 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')"),
3024
- project_name: z.string().optional().describe("Target new project name (must match what was used in plan_migration). Default: 'my-theme'"),
3025
- old_source_dir: z.string().optional().describe("Absolute path to old src/ directory (used to output exact source file paths to read)"),
3026
- }, async ({ theme_json, theme_json_path, section_name, project_name, old_source_dir }) => {
3422
+ theme_json: z
3423
+ .string()
3424
+ .optional()
3425
+ .describe("Raw JSON content of the old theme.json. EITHER this OR theme_json_path is required (not both)."),
3426
+ theme_json_path: z
3427
+ .string()
3428
+ .optional()
3429
+ .describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
3430
+ section_name: z
3431
+ .string()
3432
+ .describe("Old component name (e.g. 'Navbar', 'ProductGrid') or dir name, OR the new section ID (e.g. 'my-theme-navbar')"),
3433
+ project_name: z
3434
+ .string()
3435
+ .optional()
3436
+ .describe("Target new project name (must match what was used in plan_migration). Default: 'my-theme'"),
3437
+ old_source_dir: z
3438
+ .string()
3439
+ .optional()
3440
+ .describe("Absolute path to old src/ directory (used to output exact source file paths to read)"),
3441
+ }, async ({ theme_json, theme_json_path, section_name, project_name, old_source_dir, }) => {
3027
3442
  try {
3028
3443
  const parsed = resolveThemeJson(theme_json, theme_json_path);
3029
3444
  const projectName = project_name || "my-theme";
@@ -3032,9 +3447,744 @@ server.tool("get_section_migration_plan", "Returns concrete CLI commands and pro
3032
3447
  }
3033
3448
  catch (err) {
3034
3449
  return {
3035
- content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
3450
+ content: [
3451
+ {
3452
+ type: "text",
3453
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
3454
+ },
3455
+ ],
3456
+ };
3457
+ }
3458
+ });
3459
+ // =============================================================================
3460
+ // Editor-action tools — drive the connected editor over the dev server's WS
3461
+ // (port 5201) by shelling out to `ikas-component <cmd>`. The CLI is the
3462
+ // canonical implementation; these tools are thin advertisements for the LLM.
3463
+ // =============================================================================
3464
+ function resolveIkasComponentBinary(projectRoot) {
3465
+ if (!path.isAbsolute(projectRoot)) {
3466
+ throw new Error(`project_root must be absolute: ${projectRoot}`);
3467
+ }
3468
+ if (!fs.existsSync(projectRoot) || !fs.statSync(projectRoot).isDirectory()) {
3469
+ throw new Error(`project_root is not a directory: ${projectRoot}`);
3470
+ }
3471
+ const binDir = path.join(projectRoot, "node_modules", ".bin");
3472
+ const candidates = os.platform() === "win32"
3473
+ ? ["ikas-component.cmd", "ikas-component.exe", "ikas-component"]
3474
+ : ["ikas-component"];
3475
+ for (const name of candidates) {
3476
+ const full = path.join(binDir, name);
3477
+ if (fs.existsSync(full))
3478
+ return full;
3479
+ }
3480
+ throw new Error(`ikas-component CLI not found at ${binDir}. Run \`npm install\` (or \`pnpm install\`) in ${projectRoot} first.`);
3481
+ }
3482
+ async function runIkasComponentCli(projectRoot, args) {
3483
+ const bin = resolveIkasComponentBinary(projectRoot);
3484
+ return new Promise((resolve) => {
3485
+ execFile(bin, args,
3486
+ // Large editor responses (e.g. list_categories on a big store, or
3487
+ // add_sections_to_page echoing many sections) easily exceed Node's default
3488
+ // 1MB stdout buffer, which truncates output mid-JSON and yields a
3489
+ // "no parseable JSON" error. Give it generous headroom.
3490
+ { cwd: projectRoot, windowsHide: true, maxBuffer: 64 * 1024 * 1024 }, (err, stdout, stderr) => {
3491
+ const exitCode = err && typeof err.code === "number"
3492
+ ? err.code
3493
+ : err
3494
+ ? 1
3495
+ : 0;
3496
+ resolve({
3497
+ stdout: stdout?.toString() ?? "",
3498
+ stderr: stderr?.toString() ?? "",
3499
+ exitCode,
3500
+ });
3501
+ });
3502
+ });
3503
+ }
3504
+ function parseCliJson(stdout) {
3505
+ const trimmed = stdout.trim();
3506
+ if (!trimmed)
3507
+ return null;
3508
+ // Fast path: the CLI prints exactly one JSON object on stdout.
3509
+ try {
3510
+ return JSON.parse(trimmed);
3511
+ }
3512
+ catch {
3513
+ // Fallback: if anything leaked onto stdout before/after the JSON, scan the
3514
+ // lines and return the last one that parses as JSON.
3515
+ const lines = trimmed
3516
+ .split("\n")
3517
+ .map((l) => l.trim())
3518
+ .filter(Boolean);
3519
+ for (let i = lines.length - 1; i >= 0; i--) {
3520
+ try {
3521
+ return JSON.parse(lines[i]);
3522
+ }
3523
+ catch {
3524
+ // keep scanning
3525
+ }
3526
+ }
3527
+ return null;
3528
+ }
3529
+ }
3530
+ async function callEditorAction(projectRoot, args) {
3531
+ try {
3532
+ const { stdout, stderr, exitCode } = await runIkasComponentCli(projectRoot, args);
3533
+ const parsed = parseCliJson(stdout);
3534
+ if (parsed) {
3535
+ return {
3536
+ content: [
3537
+ { type: "text", text: JSON.stringify(parsed, null, 2) },
3538
+ ],
3539
+ };
3540
+ }
3541
+ return {
3542
+ content: [
3543
+ {
3544
+ type: "text",
3545
+ text: `CLI exited with code ${exitCode} and produced no parseable JSON.\n` +
3546
+ `stdout:\n${stdout || "(empty)"}\nstderr:\n${stderr || "(empty)"}`,
3547
+ },
3548
+ ],
3549
+ };
3550
+ }
3551
+ catch (err) {
3552
+ return {
3553
+ content: [
3554
+ {
3555
+ type: "text",
3556
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
3557
+ },
3558
+ ],
3036
3559
  };
3037
3560
  }
3561
+ }
3562
+ //
3563
+ // Tool: list_editor_pages
3564
+ server.tool("list_editor_pages", "List pages in the connected editor's project. Returns id, name, pageType, slug for each page. Requires `ikas-component dev` to be running with the editor connected. Use the returned `id`s as the `pageId` argument to `add_section_to_page`.", {
3565
+ project_root: z
3566
+ .string()
3567
+ .describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
3568
+ port: z
3569
+ .number()
3570
+ .optional()
3571
+ .describe("Dev server WebSocket port (default 5201)."),
3572
+ }, async ({ project_root, port }) => {
3573
+ const args = ["list-pages", ...(port ? ["--port", String(port)] : [])];
3574
+ return callEditorAction(project_root, args);
3575
+ });
3576
+ // Tool: list_imported_sections
3577
+ server.tool("list_imported_sections", "List section-type code components already imported into the editor's project (theme.codeComponents, filtered to type=section). Use to confirm a component is ready to be placed on a page. The component must be built (`ikas-component build`/`dev`) and imported (`import_section`) before it can be added to a page. The returned `id` is the editor's id for the imported component — normally identical to the id in `ikas.config.json`, but it can differ if the dev component was deleted and re-scaffolded after a previous import. Always use the `id` from this tool, not from `ikas.config.json`, when calling `add_section_to_page`.", {
3578
+ project_root: z
3579
+ .string()
3580
+ .describe("Absolute path to the code-component project."),
3581
+ port: z
3582
+ .number()
3583
+ .optional()
3584
+ .describe("Dev server WebSocket port (default 5201)."),
3585
+ }, async ({ project_root, port }) => {
3586
+ const args = [
3587
+ "list-imported",
3588
+ "--sections-only",
3589
+ ...(port ? ["--port", String(port)] : []),
3590
+ ];
3591
+ return callEditorAction(project_root, args);
3592
+ });
3593
+ // Tool: import_section
3594
+ server.tool("import_section", "Import a built section-type code component into the editor's project. This copies the compiled JS/CSS/props into `theme.codeComponents` and auto-creates the wrapper section needed to place it on a page. Idempotent: re-running updates the existing entry in place. Required before `add_section_to_page`. The component must have been built (`ikas-component build`/`dev`).", {
3595
+ project_root: z
3596
+ .string()
3597
+ .describe("Absolute path to the code-component project."),
3598
+ component_id: z
3599
+ .string()
3600
+ .describe("Component id from `ikas.config.json` (strict — no name resolution)."),
3601
+ port: z
3602
+ .number()
3603
+ .optional()
3604
+ .describe("Dev server WebSocket port (default 5201)."),
3605
+ }, async ({ project_root, component_id, port }) => {
3606
+ const args = [
3607
+ "import",
3608
+ "--id",
3609
+ component_id,
3610
+ ...(port ? ["--port", String(port)] : []),
3611
+ ];
3612
+ return callEditorAction(project_root, args);
3613
+ });
3614
+ // Tool: add_section_to_page
3615
+ server.tool("add_section_to_page", 'Place a SINGLE already-imported section-type code component on a page. Use this ONLY when adding exactly one section. If you are adding more than one section — or building/populating a page — call `add_sections_to_page` instead: it places them all AND fills their props in ONE call. Calling this tool repeatedly (one section per call) is the slow path and should be avoided. Equivalent to right-clicking the section in the dev-components panel and choosing "Add to Page". Errors if the component is not imported, is not section-type, or the page id is unknown. Use `list_editor_pages` to discover page ids and `list_imported_sections` to discover component ids. After placing, change the section\'s prop values with `update_section_prop` / `update_page_sections` (use `list_page_sections` to get the placement\'s `elementId` and prop names).', {
3616
+ project_root: z
3617
+ .string()
3618
+ .describe("Absolute path to the code-component project."),
3619
+ component_id: z
3620
+ .string()
3621
+ .describe("Imported code component id from `list_imported_sections` — NOT the id in `ikas.config.json` (the two are usually the same but can diverge after a rescaffold)."),
3622
+ page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
3623
+ index: z
3624
+ .number()
3625
+ .int()
3626
+ .nonnegative()
3627
+ .optional()
3628
+ .describe("Zero-based insertion index in the page; appends when omitted."),
3629
+ port: z
3630
+ .number()
3631
+ .optional()
3632
+ .describe("Dev server WebSocket port (default 5201)."),
3633
+ }, async ({ project_root, component_id, page_id, index, port }) => {
3634
+ const args = [
3635
+ "add-to-page",
3636
+ "--component-id",
3637
+ component_id,
3638
+ "--page-id",
3639
+ page_id,
3640
+ ...(typeof index === "number" ? ["--index", String(index)] : []),
3641
+ ...(port ? ["--port", String(port)] : []),
3642
+ ];
3643
+ return callEditorAction(project_root, args);
3644
+ });
3645
+ // Tool: add_sections_to_page
3646
+ server.tool("add_sections_to_page", "Place MANY sections on a page in a SINGLE call — and optionally set each section's prop values AT placement time, so you skip the separate fill step. The fastest way to build a page: one round-trip instead of one add (and one fill) per section. Pass `sections`, each `{ component_id, index?, updates?: [{ prop_name|prop_id, value }] }`; values use the same per-type shapes as update_section_prop / get_editor_workflow. Returns the new `elementId` for each placement (plus written prop results). Built-but-unimported section AND child code components are auto-imported. All update values are validated before anything is placed. Use `list_editor_pages` for page ids and `list_imported_sections` for component ids.", {
3647
+ project_root: z.string().describe("Absolute path to the code-component project."),
3648
+ page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
3649
+ sections: z
3650
+ .array(z.object({
3651
+ component_id: z.string().describe("Imported (or built) section-type code component id."),
3652
+ index: z
3653
+ .number()
3654
+ .int()
3655
+ .nonnegative()
3656
+ .optional()
3657
+ .describe("Zero-based insertion index; appends when omitted."),
3658
+ updates: z
3659
+ .array(z.object({
3660
+ prop_id: z.string().optional().describe("Blueprint prop id (provide this or prop_name)."),
3661
+ prop_name: z.string().optional().describe("Blueprint prop name (alternative to prop_id)."),
3662
+ value: z.any().describe("Prop value, same shape update_section_prop expects for that type."),
3663
+ }))
3664
+ .optional()
3665
+ .describe("Optional prop values to set on this section right after placing it."),
3666
+ }))
3667
+ .describe("Non-empty array of sections to place (in order), each with its componentId and optional prop updates."),
3668
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3669
+ }, async ({ project_root, page_id, sections, port }) => {
3670
+ const normalized = (sections || []).map(s => ({
3671
+ componentId: s.component_id,
3672
+ ...(typeof s.index === "number" ? { index: s.index } : {}),
3673
+ ...(s.updates
3674
+ ? {
3675
+ updates: s.updates.map(u => ({
3676
+ ...(u.prop_id ? { propId: u.prop_id } : {}),
3677
+ ...(u.prop_name ? { propName: u.prop_name } : {}),
3678
+ value: u.value,
3679
+ })),
3680
+ }
3681
+ : {}),
3682
+ }));
3683
+ const args = [
3684
+ "add-sections-to-page",
3685
+ "--page-id",
3686
+ page_id,
3687
+ "--sections",
3688
+ JSON.stringify(normalized),
3689
+ ...(port ? ["--port", String(port)] : []),
3690
+ ];
3691
+ return callEditorAction(project_root, args);
3692
+ });
3693
+ // Tool: list_page_sections
3694
+ server.tool("list_page_sections", "List the sections placed on a page — a LEAN ROSTER that never truncates: per-placement `elementId` (the identity of THIS placement — there can be multiple of the same section), `sectionId`, `componentId`, `name`, `propCount`, and `setPropNames` (which props already have a value). It deliberately omits the prop SCHEMA and the prop VALUES (both can be large — Header alone can have ~80 props). To get a section's prop schema (types, ENUM options, allowed children) call `get_component_props(componentId)`; to read its current values call `get_section_values(element_id)`. Use the returned `elementId` with `update_section_prop` / `update_page_sections`. Use `list_editor_pages` for page ids.", {
3695
+ project_root: z
3696
+ .string()
3697
+ .describe("Absolute path to the code-component project."),
3698
+ page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
3699
+ port: z
3700
+ .number()
3701
+ .optional()
3702
+ .describe("Dev server WebSocket port (default 5201)."),
3703
+ }, async ({ project_root, page_id, port }) => {
3704
+ const args = [
3705
+ "list-page-sections",
3706
+ "--page-id",
3707
+ page_id,
3708
+ ...(port ? ["--port", String(port)] : []),
3709
+ ];
3710
+ return callEditorAction(project_root, args);
3711
+ });
3712
+ // Tool: get_component_props
3713
+ server.tool("get_component_props", "Get prop blueprints straight from the editor (the source of truth) for one or MANY components in a single call — for any section OR child code component ids. Returns `{ components: [...] }`, each prop carrying its `id`, `name`, `type`, `required`, `default`, and crucially: a ready-to-use `writeExample` (the EXACT value shape to pass to update_section_prop / update_page_sections — e.g. PRODUCT_LIST, BLOG_LIST, COMPONENT_LIST, LINK) plus a `writeNote` for variants, so you do NOT have to guess or trial-write the JSON; for ENUM props the `options` (valid `value`s); and for COMPONENT/COMPONENT_LIST props the `allowedComponentIds` (which children the slot permits — when empty, an `allowedComponentsNote` says any imported code component is allowed). Use this (its `writeExample`/`options`/`allowedComponentIds`) instead of guessing or reading ikas.config.json. Pass ALL the section/child ids you need at once (batch). Child component ids come from a parent prop's `allowedComponentIds` (or get_section_template).", {
3714
+ project_root: z.string().describe("Absolute path to the code-component project."),
3715
+ component_ids: z
3716
+ .array(z.string())
3717
+ .describe("Section/child code-component ids to resolve (batch). From list_imported_sections or a COMPONENT_LIST prop's allowedComponentIds."),
3718
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3719
+ }, async ({ project_root, component_ids, port }) => {
3720
+ const args = [
3721
+ "get-component-props",
3722
+ "--component-ids",
3723
+ (component_ids || []).join(","),
3724
+ ...(port ? ["--port", String(port)] : []),
3725
+ ];
3726
+ return callEditorAction(project_root, args);
3727
+ });
3728
+ // Tool: get_section_values
3729
+ server.tool("get_section_values", "Get the CURRENT prop values of one or MANY placed sections (each with its full `propValues` plus blueprint `props` — every prop carrying a `writeExample`/`writeNote` for the exact value shape, plus ENUM `options` and COMPONENT_LIST `allowedComponentIds`). `list_page_sections` is lean and omits values to avoid truncation; use this to read sections' values — needed for read-modify-write of a COMPONENT_LIST, or to inspect the exact stored shape of an existing value. Returns `{ sections: [...] }`. Pass all the elementIds you need at once (batch).", {
3730
+ project_root: z.string().describe("Absolute path to the code-component project."),
3731
+ page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
3732
+ element_ids: z
3733
+ .array(z.string())
3734
+ .describe("Placed-section elementIds to read (batch). From `list_page_sections`."),
3735
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3736
+ }, async ({ project_root, page_id, element_ids, port }) => {
3737
+ const args = [
3738
+ "get-section-values",
3739
+ "--page-id",
3740
+ page_id,
3741
+ "--element-ids",
3742
+ (element_ids || []).join(","),
3743
+ ...(port ? ["--port", String(port)] : []),
3744
+ ];
3745
+ return callEditorAction(project_root, args);
3746
+ });
3747
+ // Tool: update_section_prop
3748
+ server.tool("update_section_prop", 'Change a single prop value of a section placed on a page. This is the tool for FILLING/SETTING content (heading, text, image, link, etc.) on an existing section — it is pure data entry: it writes NO component code and needs NO rebuild. Do not author a new prop or component to set a value the section already supports; first check the section\'s existing `props` via `list_page_sections`. Target the specific placement by its `element_id` (from `list_page_sections`) and the prop by `prop_id` or `prop_name` (also from `list_page_sections`). `value` is the prop value object as stored by the editor — for scalar props it is wrapped as `{ "value": <scalar> }` (e.g. `{ "value": "Hello" }` for TEXT, `{ "value": true }` for BOOLEAN, `{ "value": 12 }` for NUMBER, `{ "value": "#FF0000" }` for COLOR, `{ "value": "<enum-key>" }` for ENUM). Richer object prop types use their own object shape and are NOT `{ "value": ... }`-wrapped (full catalog in `get_editor_workflow`): IMAGE = `{ "id": "<asset-id>", "altText"?: "<alt>", "isVideo"?: false }` (the `id` MUST reference an already-uploaded asset — call `upload_image` with a `file_path`/`image_url` to get one, or reuse an existing `id` from another section\'s `propValues`); IMAGE_LIST = `{ "images": [ <image>, ... ] }`; VIDEO = `{ "video": { "id": "<asset-id>" }, "thumbnailImage"?: { "id": "..." }, "autoplay"?: false, "controls"?: true, "loop"?: false, "muted"?: false }` (NOT a bare `{ "id": ... }`); SVG = `{ "value": "<svg markup>" }` (value-wrapped, like a scalar); SVG_LIST = `{ "svgs": [ "<svg>", ... ] }`; entity props PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY/RAFFLE = `{ "<entity>Id": "...", "usePageData"?: false }` (e.g. `{ "categoryId": "..." }`) — get real ids from `search_products` (PRODUCT), `list_categories`, `list_brands`, `list_blogs`, `list_blog_categories`; list/collection props are typed objects keyed by a `<entity>ListType` discriminator: PRODUCT_LIST = `{ "id": "<unique>", "productListType": "STATIC", "productIds": [ { "productId": "...", "variantId": "..." } ], "initialLimit"?: 12 }` — note productIds is an array of {productId, variantId} PAIRS (from search_products), not bare ids; productListType ∈ ALL|STATIC|DISCOUNTED|RECOMMENDED|CATEGORY|SEARCH|LAST_VIEWED|RELATED_PRODUCTS|VIEWED_TOGETHER|PURCHASED_TOGETHER, and for a dynamic type use e.g. `{ "id": "<unique>", "productListType": "CATEGORY", "category": "<categoryId>" }` (no productIds) or `"ALL"` (no ids). CATEGORY_LIST = `{ "categoryListType": "STATIC", "categoryIds": ["..."] }` or `{ "categoryListType": "ALL" }`. BRAND_LIST = `{ "brandListType": "STATIC", "brandIds": ["..."] }` or ALL. BLOG_LIST = `{ "blogListType": "STATIC", "blogIds": ["..."] }`, `{ "blogListType": "CATEGORY", "categoryId": "..." }`, or ALL. BLOG_CATEGORY_LIST = `{ "blogCategoryListType": "STATIC", "blogCategoryIds": ["..."] }` or ALL. (Get the entity ids from search_products / list_*; you can also read an existing value with `get_section_values` to copy its exact shape.) LINK = a single link object `{ "id": "<unique-short-id>", "linkType": "PAGE"|"EXTERNAL"|"FILE", "label": "...", "openInNewTab"?: false, "subLinks": [] }` plus the target for its type — for PAGE add `"pageId"` AND `"pageType"` (both from `list_editor_pages`, e.g. pageType "INDEX"; for dynamic page types like PRODUCT/CATEGORY/BLOG also add the target `"itemId"`), for EXTERNAL add `"externalLink": "https://..."`. Examples: PAGE → `{ "id": "k3p9x", "linkType": "PAGE", "label": "Home", "pageId": "<page-id>", "pageType": "INDEX", "openInNewTab": false, "subLinks": [] }`; EXTERNAL → `{ "id": "m7q2z", "linkType": "EXTERNAL", "label": "Docs", "externalLink": "https://example.com", "openInNewTab": true, "subLinks": [] }`. To link to an ENTITY (category/brand/blog/blog-category) do NOT guess a slug — get the entity id from `list_categories`/`list_brands`/`list_blogs`/`list_blog_categories` and the page id from `get_page_by_type("<TYPE>")`, then build `{ "id": "<unique>", "linkType": "PAGE", "label": "...", "pageId": <pageId>, "pageType": "<TYPE>", "itemId": <entityId>, "subLinks": [] }`. LIST_OF_LINK = `{ "links": [ <link>, ... ] }` (each item is a link object as above). PRODUCT = `{ "productId": "...", "variantId"?: "..." }`. This SAME unwrapped shape applies whether the prop sits at section level OR nested inside a COMPONENT_LIST entry\'s `propValues` — there is no section-vs-nested difference. IMPORTANT: pass `value` as the parsed JSON object/array itself, NEVER as a JSON string (a stringified value is double-encoded and stored as a useless string). Values are validated server-side (deeply, including nested COMPONENT_LIST children): wrong shapes AND wrong semantics are REJECTED with an explanatory error instead of being silently stored. This includes: a `{ "value": [...] }` wrapper for a COMPONENT_LIST; a `{ "value": ... }`-wrapped IMAGE; `props` instead of `propValues`; a missing/duplicate entry `id`; a child referenced by the wrong key (a code component MUST use `codeComponentId`, a theme component `componentId`) or by an id that does not exist (child ids are opaque — take them from `get_section_template` / `list_imported_sections`, never invent them); and any `propValues` key that is not a real prop of that child component (the error lists the valid prop names). Auto-import: a referenced child code component that has been BUILT but not yet imported into the theme is imported automatically before the value is set (the response lists any such ids under `autoImported`); only children that are not built at all fail with a "no imported code component" error — build them first (`ikas-component build`/`dev`). For any prop type you are unsure about, inspect the existing value with `get_section_values` and match its exact shape. The change is applied with undo support.\n\n' +
3749
+ 'COMPONENT_LIST / COMPONENT props: the value is NOT wrapped in `{ "value": ... }`. A COMPONENT_LIST value is `{ "components": [ <entry>, ... ] }`; a single COMPONENT value is one `<entry>`. Each entry = `{ "id": "<unique-id>", "codeComponentId": "<child-id>" | "componentId": "<child-id>", "propValues": { "<childPropName>": <wrapped-value>, ... } }`. Rules: (1) `id` is a unique short alphanumeric string identifying THIS entry — generate a fresh one per new entry and NEVER reuse an existing entry\'s id. (2) Use `codeComponentId` for code components, `componentId` for built-in theme components. (3) `propValues` is keyed by the CHILD component\'s prop NAMES, each using the same scalar wrappers as above (nesting is recursive — a child may itself hold a COMPONENT_LIST). (4) `update_section_prop` REPLACES the whole prop value — there is NO partial/deep merge. ALWAYS send the COMPLETE, fully-nested value for the prop: every existing entry (with its id), every nested `propValues`, and every nested COMPONENT_LIST at every depth. To change anything (even one deeply-nested scalar, or to add/remove/reorder a child), READ-MODIFY-WRITE: take the current `{ "components": [...] }` from `get_section_values`, edit it in place (preserving all other entries, ids, and nested structures), then send the ENTIRE updated object back. Sending only the changed part — a single entry, or a child without its siblings — wipes everything you omitted. (5) You can only add child component types the prop permits — call `get_component_props(parentComponentId)` to read the COMPONENT_LIST prop\'s `allowedComponentIds`, then `get_component_props(childId)` to learn that child\'s props (names, types, ENUM `options`/valid values) instead of guessing or reading ikas.config.json. The allowed set is wired at build/config time per the `get_section_template` setup recipe (`config update-prop`), not here.\n\n' +
3750
+ 'Example — a full COMPONENT_LIST `value` with two children, where the second child itself nests another COMPONENT_LIST (note: scalar leaves are `{ "value": ... }`-wrapped, COMPONENT_LIST values are `{ "components": [...] }` and are NOT wrapped, every entry has a unique `id`, and this is the COMPLETE value you would send even to change just one field): ' +
3751
+ `{ "components": [ { "id": "a1b2c", "codeComponentId": "7ojrigep-Eml9n5sN3i", "propValues": { "heading": { "value": "Featured" }, "visible": { "value": true } } }, { "id": "d3e4f", "codeComponentId": "x2plk9zq-Qw8rt", "propValues": { "title": { "value": "Sub group" }, "cards": { "components": [ { "id": "g5h6i", "codeComponentId": "card01ab-Zz12", "propValues": { "label": { "value": "Card 1" } } }, { "id": "j7k8l", "codeComponentId": "card01ab-Zz12", "propValues": { "label": { "value": "Card 2" } } } ] } } } ] }`, {
3752
+ project_root: z
3753
+ .string()
3754
+ .describe("Absolute path to the code-component project."),
3755
+ page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
3756
+ element_id: z
3757
+ .string()
3758
+ .describe("Placed-section elementId identifying THIS placement on the page (from `list_page_sections`)."),
3759
+ prop_id: z
3760
+ .string()
3761
+ .optional()
3762
+ .describe("Blueprint prop id to update (from `list_page_sections`). Provide this or `prop_name`."),
3763
+ prop_name: z
3764
+ .string()
3765
+ .optional()
3766
+ .describe("Blueprint prop name to update (alternative to `prop_id`)."),
3767
+ value: z
3768
+ .any()
3769
+ .describe('The prop value object to store, e.g. `{ "value": "Hello" }`. Match the shape of the existing value from `get_section_values` for non-scalar prop types.'),
3770
+ port: z
3771
+ .number()
3772
+ .optional()
3773
+ .describe("Dev server WebSocket port (default 5201)."),
3774
+ }, async ({ project_root, page_id, element_id, prop_id, prop_name, value, port }) => {
3775
+ const args = [
3776
+ "update-section-prop",
3777
+ "--page-id",
3778
+ page_id,
3779
+ "--element-id",
3780
+ element_id,
3781
+ ...(prop_id ? ["--prop-id", prop_id] : []),
3782
+ ...(prop_name ? ["--prop-name", prop_name] : []),
3783
+ "--value",
3784
+ // The CLI JSON.parses --value. If `value` is already a JSON string, pass
3785
+ // it through verbatim — re-stringifying it would double-encode it and the
3786
+ // editor would store a string instead of the object/array.
3787
+ typeof value === "string" ? value : JSON.stringify(value),
3788
+ ...(port ? ["--port", String(port)] : []),
3789
+ ];
3790
+ return callEditorAction(project_root, args);
3791
+ });
3792
+ // Tool: upload_image
3793
+ server.tool("upload_image", "Upload an image to the connected editor's project and get back its image `id`. Provide the image via `file_path` (a local path) OR `image_url`. Use the returned `id` as the `id` of an IMAGE prop value passed to `update_section_prop` (e.g. `{ \"id\": \"<returned-id>\", \"altText\": \"...\", \"isVideo\": false }`). This is the way to set an image prop — `update_section_prop` does not upload, it only references an existing image id. Supported types: png, jpg, jpeg, webp, gif; max 10MB. Requires the editor to be embedded in the ikas admin panel (the admin panel performs the actual upload; a standalone editor cannot upload and will error/time out).", {
3794
+ project_root: z
3795
+ .string()
3796
+ .describe("Absolute path to the code-component project."),
3797
+ file_path: z
3798
+ .string()
3799
+ .optional()
3800
+ .describe("Local image file path (.png, .jpg, .jpeg, .webp, .gif). Provide this or `image_url`."),
3801
+ image_url: z
3802
+ .string()
3803
+ .optional()
3804
+ .describe("Image URL to fetch and upload (alternative to `file_path`)."),
3805
+ alt_text: z.string().optional().describe("Alt text to store with the image."),
3806
+ port: z
3807
+ .number()
3808
+ .optional()
3809
+ .describe("Dev server WebSocket port (default 5201)."),
3810
+ }, async ({ project_root, file_path, image_url, alt_text, port }) => {
3811
+ const args = [
3812
+ "upload-image",
3813
+ ...(file_path ? ["--file", file_path] : []),
3814
+ ...(image_url ? ["--url", image_url] : []),
3815
+ ...(alt_text ? ["--alt", alt_text] : []),
3816
+ ...(port ? ["--port", String(port)] : []),
3817
+ ];
3818
+ return callEditorAction(project_root, args);
3819
+ });
3820
+ // Tool: update_page_sections
3821
+ server.tool("update_page_sections", "Fill MANY placed sections of a page in a SINGLE call — the fastest way to populate a whole page (one round-trip for the entire page instead of one call per section/prop). Takes `sections`, each `{ element_id, updates: [{ prop_name|prop_id, value }] }`; values use the same per-type shapes as update_section_prop / get_editor_workflow. All-or-nothing across the whole page: every value is resolved and deeply validated (and built-but-unimported child code components auto-imported) before anything is written — one invalid value rejects the entire page update. Prefer this over many update_section_prop(s) calls.", {
3822
+ project_root: z.string().describe("Absolute path to the code-component project."),
3823
+ page_id: z.string().describe("Target page id (from list_editor_pages)."),
3824
+ sections: z
3825
+ .array(z.object({
3826
+ element_id: z.string().describe("Placed-section elementId (from list_page_sections)."),
3827
+ updates: z
3828
+ .array(z.object({
3829
+ prop_id: z.string().optional().describe("Blueprint prop id (provide this or prop_name)."),
3830
+ prop_name: z.string().optional().describe("Blueprint prop name (alternative to prop_id)."),
3831
+ value: z.any().describe("Prop value, same shape update_section_prop expects for that type."),
3832
+ }))
3833
+ .describe("Non-empty array of prop updates for this section."),
3834
+ }))
3835
+ .describe("Non-empty array of sections to fill, each with its elementId and prop updates."),
3836
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3837
+ }, async ({ project_root, page_id, sections, port }) => {
3838
+ const normalized = (sections || []).map(s => ({
3839
+ elementId: s.element_id,
3840
+ updates: (s.updates || []).map(u => ({
3841
+ ...(u.prop_id ? { propId: u.prop_id } : {}),
3842
+ ...(u.prop_name ? { propName: u.prop_name } : {}),
3843
+ value: u.value,
3844
+ })),
3845
+ }));
3846
+ const args = [
3847
+ "update-page-sections",
3848
+ "--page-id",
3849
+ page_id,
3850
+ "--sections",
3851
+ JSON.stringify(normalized),
3852
+ ...(port ? ["--port", String(port)] : []),
3853
+ ];
3854
+ return callEditorAction(project_root, args);
3855
+ });
3856
+ // Tool: upload_images
3857
+ server.tool("upload_images", "Upload MANY images in one call (batch upload_image) and get their ids back in order — the host uploads them all in a single round-trip. Pass `images`, each `{ file_path?, image_url?, alt_text? }`. Returns an array of { id, fileName, altText, isVideo } in the same order; use each id in an IMAGE prop value. Prefer this over multiple upload_image calls when a section/page needs several images. Supported: png/jpg/jpeg/webp/gif, max 10MB each. Requires the editor embedded in the ikas admin panel.", {
3858
+ project_root: z.string().describe("Absolute path to the code-component project."),
3859
+ images: z
3860
+ .array(z.object({
3861
+ file_path: z.string().optional().describe("Local image file path (provide this or image_url)."),
3862
+ image_url: z.string().optional().describe("Image URL to fetch (alternative to file_path)."),
3863
+ alt_text: z.string().optional().describe("Alt text for this image."),
3864
+ }))
3865
+ .describe("Non-empty array of images to upload."),
3866
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3867
+ }, async ({ project_root, images, port }) => {
3868
+ const manifest = (images || []).map(im => ({
3869
+ ...(im.file_path ? { file: im.file_path } : {}),
3870
+ ...(im.image_url ? { url: im.image_url } : {}),
3871
+ ...(im.alt_text ? { alt: im.alt_text } : {}),
3872
+ }));
3873
+ const args = [
3874
+ "upload-images",
3875
+ "--manifest",
3876
+ JSON.stringify(manifest),
3877
+ ...(port ? ["--port", String(port)] : []),
3878
+ ];
3879
+ return callEditorAction(project_root, args);
3880
+ });
3881
+ // Tool: search_products
3882
+ server.tool("search_products", "Search the connected store's products to obtain a real productId (and first variantId) for a PRODUCT prop value. Returns products with productId, variantId, name, slug, variantCount, and `imageSrc` (the full-resolution CDN URL of the product's main image). To use that product image in an IMAGE prop, you CANNOT reuse a product image id directly (it is not a theme asset) — pass the `imageSrc` URL to `upload_image(image_url: <imageSrc>)` (or `upload_images`), then use the returned id in the IMAGE value. This gives real product imagery instead of external/placeholder URLs. Use the result in update_section_prop as `{ \"productId\": \"<productId>\", \"variantId\": \"<variantId>\" }`. Pass `query` for free-text search, or `product_ids` to resolve specific ids. Requires the editor embedded in the ikas admin panel (the admin panel runs the search; a standalone editor cannot).", {
3883
+ project_root: z
3884
+ .string()
3885
+ .describe("Absolute path to the code-component project."),
3886
+ query: z.string().optional().describe("Free-text product search (name, sku, barcode)."),
3887
+ product_ids: z
3888
+ .array(z.string())
3889
+ .optional()
3890
+ .describe("Specific product ids to resolve directly (alternative to `query`)."),
3891
+ per_page: z.number().optional().describe("Results per page."),
3892
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3893
+ }, async ({ project_root, query, product_ids, per_page, port }) => {
3894
+ const args = [
3895
+ "search-products",
3896
+ ...(query ? ["--query", query] : []),
3897
+ ...(product_ids && product_ids.length ? ["--ids", product_ids.join(",")] : []),
3898
+ ...(typeof per_page === "number" ? ["--per-page", String(per_page)] : []),
3899
+ ...(port ? ["--port", String(port)] : []),
3900
+ ];
3901
+ return callEditorAction(project_root, args);
3902
+ });
3903
+ // Tool: list_categories
3904
+ server.tool("list_categories", "List the store's product categories and their ids, to fill a CATEGORY prop value `{ \"categoryId\": \"<id>\" }`. Returns categoryId, name, slug, parentId for each. Returns all categories (no query). To link to a category from a LINK prop, also call `get_page_by_type(\"CATEGORY\")` for the page id, then build `{ \"linkType\": \"PAGE\", \"pageId\": <pageId>, \"pageType\": \"CATEGORY\", \"itemId\": <categoryId>, \"id\": <unique>, \"label\": \"...\" }`. Requires the editor embedded in the ikas admin panel.", {
3905
+ project_root: z.string().describe("Absolute path to the code-component project."),
3906
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3907
+ }, async ({ project_root, port }) => {
3908
+ const args = ["list-entities", "--kind", "category", ...(port ? ["--port", String(port)] : [])];
3909
+ return callEditorAction(project_root, args);
3910
+ });
3911
+ // Tool: list_brands
3912
+ server.tool("list_brands", "List the store's product brands and their ids, to fill a BRAND prop value `{ \"brandId\": \"<id>\" }`. Returns brandId, name, slug for each. Returns all brands (no query). To link to a brand from a LINK prop, also call `get_page_by_type(\"BRAND\")` for the page id, then build `{ \"linkType\": \"PAGE\", \"pageId\": <pageId>, \"pageType\": \"BRAND\", \"itemId\": <brandId>, \"id\": <unique>, \"label\": \"...\" }`. Requires the editor embedded in the ikas admin panel.", {
3913
+ project_root: z.string().describe("Absolute path to the code-component project."),
3914
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3915
+ }, async ({ project_root, port }) => {
3916
+ const args = ["list-entities", "--kind", "brand", ...(port ? ["--port", String(port)] : [])];
3917
+ return callEditorAction(project_root, args);
3918
+ });
3919
+ // Tool: list_blogs
3920
+ server.tool("list_blogs", "List the store's blogs and their ids, to fill a BLOG prop value `{ \"blogId\": \"<id>\" }`. Returns blogId, name, slug for each. Accepts an optional `query` for free-text search. To link to a blog from a LINK prop, also call `get_page_by_type(\"BLOG\")` for the page id, then build `{ \"linkType\": \"PAGE\", \"pageId\": <pageId>, \"pageType\": \"BLOG\", \"itemId\": <blogId>, \"id\": <unique>, \"label\": \"...\" }`. Requires the editor embedded in the ikas admin panel.", {
3921
+ project_root: z.string().describe("Absolute path to the code-component project."),
3922
+ query: z.string().optional().describe("Free-text blog search."),
3923
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3924
+ }, async ({ project_root, query, port }) => {
3925
+ const args = [
3926
+ "list-entities",
3927
+ "--kind",
3928
+ "blog",
3929
+ ...(query ? ["--query", query] : []),
3930
+ ...(port ? ["--port", String(port)] : []),
3931
+ ];
3932
+ return callEditorAction(project_root, args);
3933
+ });
3934
+ // Tool: list_blog_categories
3935
+ server.tool("list_blog_categories", "List the store's blog categories and their ids, to fill a BLOG_CATEGORY prop value `{ \"blogCategoryId\": \"<id>\" }`. Returns blogCategoryId, name, slug for each. Accepts an optional `query` for free-text search. To link to a blog category from a LINK prop, also call `get_page_by_type(\"BLOG_CATEGORY\")` for the page id, then build `{ \"linkType\": \"PAGE\", \"pageId\": <pageId>, \"pageType\": \"BLOG_CATEGORY\", \"itemId\": <blogCategoryId>, \"id\": <unique>, \"label\": \"...\" }`. Requires the editor embedded in the ikas admin panel.", {
3936
+ project_root: z.string().describe("Absolute path to the code-component project."),
3937
+ query: z.string().optional().describe("Free-text blog-category search."),
3938
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3939
+ }, async ({ project_root, query, port }) => {
3940
+ const args = [
3941
+ "list-entities",
3942
+ "--kind",
3943
+ "blog-category",
3944
+ ...(query ? ["--query", query] : []),
3945
+ ...(port ? ["--port", String(port)] : []),
3946
+ ];
3947
+ return callEditorAction(project_root, args);
3948
+ });
3949
+ // Tool: get_page_by_type
3950
+ server.tool("get_page_by_type", "Resolve a theme page's id from its pageType (CATEGORY, PRODUCT, BRAND, BLOG, BLOG_CATEGORY, INDEX, …). Use this to get the `pageId` needed to build a PAGE link to an entity: pair it with the entity id from list_categories/list_brands/list_blogs/list_blog_categories — `{ linkType:\"PAGE\", pageId:<this pageId>, pageType:<type>, itemId:<entityId>, id:<unique>, label:... }`. Returns `pageId: null` (with a note) if no page of that type exists yet — in that case call `create_page(page_type)` to add it, then use the returned pageId (do not guess a slug/EXTERNAL URL).", {
3951
+ project_root: z.string().describe("Absolute path to the code-component project."),
3952
+ page_type: z
3953
+ .string()
3954
+ .describe("Page type to resolve, e.g. CATEGORY, PRODUCT, BRAND, BLOG, BLOG_CATEGORY, INDEX."),
3955
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3956
+ }, async ({ project_root, page_type, port }) => {
3957
+ const args = [
3958
+ "get-page-by-type",
3959
+ "--page-type",
3960
+ page_type,
3961
+ ...(port ? ["--port", String(port)] : []),
3962
+ ];
3963
+ return callEditorAction(project_root, args);
3964
+ });
3965
+ // Tool: create_page
3966
+ server.tool("create_page", "Create a theme page of a given pageType in the connected editor. Use this when get_page_by_type returns `pageId: null` for a dynamic page (CATEGORY/PRODUCT/BRAND/BLOG/BLOG_CATEGORY) you need to link to: create the page, then build a PAGE link `{ linkType:\"PAGE\", pageId:<this pageId>, pageType:<type>, itemId:<entityId>, id:<unique>, label:... }` — do NOT guess a slug/EXTERNAL URL. Non-CUSTOM page types are unique per theme: if one already exists it is returned unchanged (`alreadyExisted: true`). CUSTOM pages require `name` and a unique `slug`. Account sub-pages (ADDRESSES/ORDERS/ORDER_DETAIL/FAVORITE_PRODUCTS/RAFFLE_ACCOUNT) need an ACCOUNT page first. Returns `{ pageType, pageId, name, slug }`. Requires `ikas-component dev` running with the editor connected.", {
3967
+ project_root: z.string().describe("Absolute path to the code-component project."),
3968
+ page_type: z
3969
+ .string()
3970
+ .describe("Page type to create, e.g. CATEGORY, PRODUCT, BRAND, BLOG, BLOG_CATEGORY, INDEX, CUSTOM."),
3971
+ name: z.string().optional().describe("Page name. Required for CUSTOM pages; ignored otherwise."),
3972
+ slug: z.string().optional().describe("Page slug. Required (and unique) for CUSTOM pages; ignored otherwise."),
3973
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3974
+ }, async ({ project_root, page_type, name, slug, port }) => {
3975
+ const args = [
3976
+ "create-page",
3977
+ "--page-type",
3978
+ page_type,
3979
+ ...(name ? ["--name", name] : []),
3980
+ ...(slug ? ["--slug", slug] : []),
3981
+ ...(port ? ["--port", String(port)] : []),
3982
+ ];
3983
+ return callEditorAction(project_root, args);
3984
+ });
3985
+ // Tool: publish_theme
3986
+ server.tool("publish_theme", "Publish the current theme LIVE — the same action as the editor's Publish button (uploads the whole project to the storefront). This is REAL and customer-facing: the published site changes. SAFETY: it publishes nothing unless `confirm: true` is passed; if you omit it you get a dry-run with the `previewUrl` and a warning describing the impact. The MAIN/production theme additionally requires `confirm_production: true` (its URL is the live customer-facing domain). Use this when the user explicitly asks to publish/go live — do NOT publish on your own initiative. On success returns `{ published: true, previewUrl, isMainTheme }`; open `previewUrl` to review the result. Requires `ikas-component dev` running with the editor connected.", {
3987
+ project_root: z.string().describe("Absolute path to the code-component project."),
3988
+ confirm: z
3989
+ .boolean()
3990
+ .optional()
3991
+ .describe("Set true to actually publish. Omitted/false = dry-run (returns previewUrl + warning, publishes nothing)."),
3992
+ confirm_production: z
3993
+ .boolean()
3994
+ .optional()
3995
+ .describe("Required IN ADDITION to confirm when the target is the MAIN/production theme (live customer-facing site)."),
3996
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3997
+ }, async ({ project_root, confirm, confirm_production, port }) => {
3998
+ const args = [
3999
+ "publish-theme",
4000
+ ...(confirm ? ["--confirm"] : []),
4001
+ ...(confirm_production ? ["--confirm-production"] : []),
4002
+ ...(port ? ["--port", String(port)] : []),
4003
+ ];
4004
+ return callEditorAction(project_root, args);
4005
+ });
4006
+ // Tool: get_editor_workflow
4007
+ const EDITOR_WORKFLOW_GUIDE = [
4008
+ "# Editor workflow: placing sections and filling their content on a page",
4009
+ "",
4010
+ "Use this when the user wants to add a section to a page and/or fill in its content (heading, text, image, link, slides, etc.) in the live editor. These are LIVE-EDITOR actions and require `ikas-component dev` running with the editor connected; image upload additionally requires the editor to be embedded in the ikas admin panel (the admin panel performs the upload — a standalone editor cannot upload).",
4011
+ "",
4012
+ "## Two distinct jobs — do not confuse them",
4013
+ "- (A) DEFINE props / author component code — changing WHICH props a component has. Edits component source/config (config add-prop/add-component, index.tsx) and needs a rebuild. Do this ONLY when a needed prop does not exist yet, or the user explicitly asks to build/modify the component.",
4014
+ "- (B) SET prop VALUES / fill content — giving values to props that ALREADY exist on a placed section. Pure data entry via list_page_sections + update_section_prop (+ upload_image). No code, no rebuild.",
4015
+ "When the user says 'fill this section', 'set the title/text/image/link', 'populate', 'change the content' → that is JOB (B). Do NOT write a component or add a prop to fill a value the section already supports.",
4016
+ "",
4017
+ "## Step-by-step (JOB B — fill content)",
4018
+ "1. list_editor_pages → choose the target page_id.",
4019
+ "2. list_page_sections(page_id) → for each placed section: its per-placement elementId, blueprint props (id, name, type), and setPropNames (which props already have a value). It is LEAN — no prop VALUES, so it never truncates. To read a section's current values (e.g. to read-modify-write a COMPONENT_LIST), call get_section_values(page_id, element_id). (If the section is not on the page yet, see 'Placing a section' below.)",
4020
+ "3. To set props (fewest round-trips = fastest): update_page_sections(page_id, sections:[{element_id, updates:[{prop_name,value}]}]) fills the WHOLE page (or one section, or just one prop — it scales to all cases) in ONE call (best). update_section_prop sets a single prop, for quick one-offs. Pass each value as a parsed JSON object/array, NEVER a JSON string.",
4021
+ "4. For image props: upload_images(images:[{file_path|image_url, alt_text}]) uploads many in ONE call (preferred); upload_image for a single one. Use the returned id(s) in IMAGE values. Do uploads BEFORE the update call so you have the ids.",
4022
+ "Speed tip: each tool call is a round-trip — gather first (one list_page_sections), batch your uploads (upload_images) and your writes (update_page_sections), rather than calling per-prop/per-image.",
4023
+ "",
4024
+ "## Building a whole page fast (placing + filling)",
4025
+ "- add_sections_to_page(page_id, sections:[{component_id, index?, updates:[{prop_name,value}]}]) places MANY sections AND sets their props in ONE call, returning each new elementId. This is the fastest path — prefer it over add_section_to_page per section followed by separate fills. Built-but-unimported section/child components are auto-imported.",
4026
+ "- For a single section: list_imported_sections (confirm imported; else import_section, after building) → add_section_to_page(component_id, page_id, index?) → then list_page_sections to get its elementId.",
4027
+ "",
4028
+ "## Value shapes by prop type (what to pass as `value`)",
4029
+ "- Scalars are WRAPPED: TEXT/RICH_TEXT { \"value\": \"...\" }, BOOLEAN { \"value\": true }, NUMBER { \"value\": 12 }, COLOR { \"value\": \"#FF0000\" }, ENUM { \"value\": \"<key>\" }, DATE { \"value\": ... }.",
4030
+ "- SVG is value-wrapped: { \"value\": \"<svg markup>\" }. NUMBER_RANGE: { \"value\": <number>, \"unit\": \"px\"|null }.",
4031
+ "- Object props are NOT wrapped (no { \"value\": ... }):",
4032
+ " - IMAGE: { \"id\": \"<asset-id>\", \"altText\"?: \"...\", \"isVideo\"?: false } — get the id from upload_image. Same shape at section level AND nested in a component list. IMAGE_LIST: { \"images\": [ <image>, ... ] }.",
4033
+ " - VIDEO: { \"video\": { \"id\": \"<asset-id>\" }, \"thumbnailImage\"?: { \"id\": \"...\" }, \"autoplay\"?: false, \"controls\"?: true, \"loop\"?: false, \"muted\"?: false } — NOT a bare { \"id\": ... }. SVG_LIST: { \"svgs\": [ \"<svg>\", ... ] }.",
4034
+ " - LINK: { \"id\": \"<unique>\", \"linkType\": \"PAGE\"|\"EXTERNAL\"|\"FILE\", \"label\": \"...\", \"subLinks\": [] } plus target — for a static page: pageId + pageType (from list_editor_pages); for EXTERNAL: externalLink. To link to a CATEGORY/BRAND/BLOG/BLOG_CATEGORY entity, do NOT guess a slug — get the entity id from list_categories/list_brands/list_blogs/list_blog_categories and the page id from get_page_by_type('<TYPE>'), then build { id, linkType:'PAGE', label, pageId:<pageId>, pageType:'<TYPE>', itemId:<entityId>, subLinks:[] }. LIST_OF_LINK = { \"links\": [ <link>, ... ] }.",
4035
+ " - Entity props PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY/RAFFLE: { \"<entity>Id\": \"...\", \"usePageData\"?: false } — e.g. { \"productId\": \"...\", \"variantId\"?: \"...\" }, { \"categoryId\": \"...\" }. Get real ids (never invent them) from: search_products (PRODUCT), list_categories (CATEGORY), list_brands (BRAND), list_blogs (BLOG), list_blog_categories (BLOG_CATEGORY). (RAFFLE has no lookup tool yet — read an existing value or ask the user.)",
4036
+ " - List/collection props (PRODUCT_LIST/CATEGORY_LIST/BRAND_LIST/BLOG_LIST/…): complex typed objects (a <entity>ListType plus initialSort/initialLimit and static ids). Do NOT build from scratch — read the current value with get_section_values and edit it.",
4037
+ " - COMPONENT_LIST: { \"components\": [ { \"id\": \"<unique>\", \"codeComponentId\": \"<child-id>\", \"propValues\": { \"<childProp>\": <value>, ... } }, ... ] }. A single COMPONENT prop is one such entry. childProp values use these same per-type shapes, recursively.",
4038
+ "",
4039
+ "## COMPONENT_LIST rules (the part most often gotten wrong)",
4040
+ "- READ-MODIFY-WRITE: update_section_prop REPLACES the whole prop value. To add/edit/remove one child, take the CURRENT { components: [...] } from get_section_values, edit it, and resend the ENTIRE array (with all existing entries and their ids). Sending only your new entry wipes the rest.",
4041
+ "- Each entry needs a UNIQUE id (a short string you generate; it does not need to match anything). Do not reuse another entry's id.",
4042
+ "- Reference a code component with \"codeComponentId\" and a built-in theme component with \"componentId\" — not the other way around. Use \"propValues\" (not \"props\").",
4043
+ "- Child ids are opaque — take them from get_section_template / list_imported_sections; never invent them.",
4044
+ "- Auto-import: a referenced child that is BUILT but not yet imported is imported automatically (returned under autoImported). A child that is not built at all errors — build it first.",
4045
+ "",
4046
+ "## Reading prop schemas (avoid guessing)",
4047
+ "- get_component_props(componentId) → a component's props with types, required, default, ENUM `options` (the valid values to set), and COMPONENT_LIST `allowedComponentIds` (valid children). Works for any section OR child id. Use it BEFORE filling — to know prop names, enum values, and which children a slot allows — instead of guessing or reading ikas.config.json.",
4048
+ "- For a COMPONENT_LIST: get_component_props(parent) → its allowedComponentIds → get_component_props(child) → the child's props/enums. Then build the components array.",
4049
+ "- get_section_values(element_id) → one placed section's current values (read-modify-write of a COMPONENT_LIST, or to copy an existing value's exact shape).",
4050
+ "- Real entity ids/images: search_products returns productId + variantId + imageSrc (the product image URL). To use that image in an IMAGE prop, you can't reuse a product image id — upload it: upload_image(image_url: imageSrc) → use the returned id. list_categories/list_brands/list_blogs/list_blog_categories return the entity ids; to LINK to one, also call get_page_by_type('<TYPE>') for the page id and build a PAGE link { linkType:'PAGE', pageId, pageType, itemId:<entityId>, id, label }. IMPORTANT: if get_page_by_type returns pageId:null, that dynamic page does NOT exist in the theme yet — do NOT guess a slug/EXTERNAL URL. Call create_page('<TYPE>') to add it (non-CUSTOM types are unique and returned as-is if already present), then use the returned pageId to build the PAGE link. Always have a real pageId before building a PAGE link.",
4051
+ "",
4052
+ "## Validation",
4053
+ "Values are validated server-side (deeply, including nested children). Wrong shape or wrong semantics (double-encoded string, { value } wrapper on an object prop, props vs propValues, missing/duplicate id, wrong reference key, nonexistent component id, unknown prop name) are REJECTED with an explanatory error instead of being stored silently. Read the error and fix the value; if unsure of a shape, read it with get_section_values and match it.",
4054
+ ].join("\n");
4055
+ server.tool("get_editor_workflow", "Read this FIRST when the user wants to place a section on a page or fill/edit a section's content (text, image, link, slides, component lists) in the live editor. Returns the complete step-by-step workflow for the live-editor tools (list_editor_pages, list_imported_sections, import_section, add_section_to_page, list_page_sections, update_section_prop, upload_image), the per-prop-type value shapes, the COMPONENT_LIST read-modify-write rules, and how validation/auto-import behave. Use it to avoid the common mistake of writing component code to set a value a section already supports.", {}, async () => {
4056
+ return { content: [{ type: "text", text: EDITOR_WORKFLOW_GUIDE }] };
4057
+ });
4058
+ // Tool: list_theme_globals
4059
+ server.tool("list_theme_globals", "List the theme's global settings from the connected editor: global variables (Theme Settings) and design tokens (colors, typography, breakpoints, keyframes, color schemes). Call this BEFORE generating sections/components so you reuse the theme's existing variables and tokens (read them in component code via `getThemeSetting`/`getThemeColors`/... from `@ikas/bp-storefront`). Includes items created manually in the editor UI. Requires `ikas-component dev` running with the editor connected.", {
4060
+ project_root: z.string().describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
4061
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4062
+ }, async ({ project_root, port }) => {
4063
+ const args = ["list-theme-globals", ...(port ? ["--port", String(port)] : [])];
4064
+ return callEditorAction(project_root, args);
4065
+ });
4066
+ // Tool: create_theme_global
4067
+ server.tool("create_theme_global", "Create a theme global setting in the connected editor. `kind` selects what to create:\n" +
4068
+ "- globalVariable: requires display_name + type (TEXT|RICH_TEXT|IMAGE|COLOR|NUMBER|BOOLEAN|BORDER|SHADOW); value optional. Value shapes — TEXT/COLOR: string; RICH_TEXT: HTML string; NUMBER: number; BOOLEAN: boolean; IMAGE: { url } object; BORDER: { width: { value, unit }, style, color }; SHADOW: { x, y, blur, spread, color, position: \"outside\"|\"inside\" }.\n" +
4069
+ "- color: requires name + value (hex, e.g. \"#ff0000\").\n" +
4070
+ "- typography: requires name + at least one of font_family/font_size/font_weight/line_height/letter_spacing.\n" +
4071
+ "- breakpoint: requires name + width (px).\n" +
4072
+ "- keyframe: requires name + points (array of { point, styles? }) where each style is { property, value } using a CSS property name (opacity, transform, filter, background, color, …); keyframe_type defaults to \"keyframe\". Use the keyframe's `ref` as a CSS animation-name and set timing (duration/iteration) where you apply it.\n" +
4073
+ "- colorScheme: requires name + colors (array of { key, value }) — `key` is the color slot name (e.g. Background, Text, Primary); slots are created automatically if missing.\n" +
4074
+ "Read created items back with `list_theme_globals`. Requires `ikas-component dev` running with the editor connected.", {
4075
+ project_root: z.string().describe("Absolute path to the code-component project."),
4076
+ kind: z
4077
+ .enum(["globalVariable", "color", "typography", "breakpoint", "keyframe", "colorScheme"])
4078
+ .describe("What to create."),
4079
+ name: z.string().optional().describe("Name (design tokens)."),
4080
+ display_name: z.string().optional().describe("Human label (globalVariable)."),
4081
+ type: z.string().optional().describe("globalVariable value type."),
4082
+ value: z.any().optional().describe("globalVariable default value (shape depends on type — see tool description), or color hex string."),
4083
+ font_family: z.string().optional(),
4084
+ font_size: z.string().optional(),
4085
+ font_weight: z.string().optional(),
4086
+ line_height: z.string().optional(),
4087
+ letter_spacing: z.string().optional(),
4088
+ width: z.number().optional().describe("breakpoint width in px."),
4089
+ keyframe_type: z.enum(["keyframe", "transition"]).optional(),
4090
+ points: z.any().optional().describe("keyframe points array."),
4091
+ colors: z.any().optional().describe("colorScheme colors: array of { key, value } (key = color slot name)."),
4092
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4093
+ }, async (input) => {
4094
+ const p = input.port ? ["--port", String(input.port)] : [];
4095
+ let args;
4096
+ switch (input.kind) {
4097
+ case "globalVariable":
4098
+ args = [
4099
+ "create-global-variable",
4100
+ "--display-name",
4101
+ String(input.display_name ?? ""),
4102
+ "--type",
4103
+ String(input.type ?? ""),
4104
+ ...(input.value !== undefined ? ["--value", JSON.stringify(input.value)] : []),
4105
+ ...p,
4106
+ ];
4107
+ break;
4108
+ case "color":
4109
+ args = ["create-color", "--name", String(input.name ?? ""), "--value", String(input.value ?? ""), ...p];
4110
+ break;
4111
+ case "typography":
4112
+ args = [
4113
+ "create-text-style",
4114
+ "--name",
4115
+ String(input.name ?? ""),
4116
+ ...(input.font_family ? ["--font-family", input.font_family] : []),
4117
+ ...(input.font_size ? ["--font-size", input.font_size] : []),
4118
+ ...(input.font_weight ? ["--font-weight", input.font_weight] : []),
4119
+ ...(input.line_height ? ["--line-height", input.line_height] : []),
4120
+ ...(input.letter_spacing ? ["--letter-spacing", input.letter_spacing] : []),
4121
+ ...p,
4122
+ ];
4123
+ break;
4124
+ case "breakpoint":
4125
+ args = ["create-breakpoint", "--name", String(input.name ?? ""), "--width", String(input.width ?? ""), ...p];
4126
+ break;
4127
+ case "keyframe":
4128
+ args = [
4129
+ "create-keyframe",
4130
+ "--name",
4131
+ String(input.name ?? ""),
4132
+ ...(input.keyframe_type ? ["--type", input.keyframe_type] : []),
4133
+ "--points",
4134
+ JSON.stringify(input.points ?? []),
4135
+ ...p,
4136
+ ];
4137
+ break;
4138
+ case "colorScheme":
4139
+ args = [
4140
+ "create-color-scheme",
4141
+ "--name",
4142
+ String(input.name ?? ""),
4143
+ "--colors",
4144
+ JSON.stringify(input.colors ?? []),
4145
+ ...p,
4146
+ ];
4147
+ break;
4148
+ default:
4149
+ return { content: [{ type: "text", text: `Error: unknown kind "${input.kind}"` }] };
4150
+ }
4151
+ return callEditorAction(input.project_root, args);
4152
+ });
4153
+ // Tool: update_theme_global
4154
+ server.tool("update_theme_global", "Update an existing global variable in the connected editor (e.g. fix its value or change its type). Identify it by `name` (the runtime key from `list_theme_globals`). Only the fields you pass are changed. Value shapes follow `create_theme_global`.", {
4155
+ project_root: z.string().describe("Absolute path to the code-component project."),
4156
+ name: z.string().describe("The variable's runtime key (from `list_theme_globals`)."),
4157
+ display_name: z.string().optional().describe("New label."),
4158
+ type: z.string().optional().describe("New value type (TEXT|RICH_TEXT|IMAGE|COLOR|NUMBER|BOOLEAN|BORDER|SHADOW)."),
4159
+ value: z.any().optional().describe("New value (shape depends on type)."),
4160
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4161
+ }, async ({ project_root, name, display_name, type, value, port }) => {
4162
+ const args = [
4163
+ "update-global-variable",
4164
+ "--name",
4165
+ name,
4166
+ ...(display_name !== undefined ? ["--display-name", display_name] : []),
4167
+ ...(type !== undefined ? ["--type", type] : []),
4168
+ ...(value !== undefined ? ["--value", JSON.stringify(value)] : []),
4169
+ ...(port ? ["--port", String(port)] : []),
4170
+ ];
4171
+ return callEditorAction(project_root, args);
4172
+ });
4173
+ // Tool: delete_theme_global
4174
+ server.tool("delete_theme_global", "Delete a theme global setting from the connected editor. For `kind: globalVariable` pass `name` (the runtime key); for design-token kinds (color | typography | breakpoint | keyframe | colorScheme) pass the token `id`. Get names/ids from `list_theme_globals`.", {
4175
+ project_root: z.string().describe("Absolute path to the code-component project."),
4176
+ kind: z
4177
+ .enum(["globalVariable", "color", "typography", "breakpoint", "keyframe", "colorScheme"])
4178
+ .describe("What to delete."),
4179
+ name: z.string().optional().describe("globalVariable runtime key (from `list_theme_globals`)."),
4180
+ id: z.string().optional().describe("design-token id (from `list_theme_globals`)."),
4181
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4182
+ }, async ({ project_root, kind, name, id, port }) => {
4183
+ const p = port ? ["--port", String(port)] : [];
4184
+ const args = kind === "globalVariable"
4185
+ ? ["delete-global-variable", "--name", String(name ?? ""), ...p]
4186
+ : ["delete-design-token", "--token-type", kind, "--id", String(id ?? ""), ...p];
4187
+ return callEditorAction(project_root, args);
3038
4188
  });
3039
4189
  // --- Start server ---
3040
4190
  async function main() {