@ikas/code-components-mcp 1.4.0-beta.3 → 1.4.0-beta.5
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/data/framework.json +5 -5
- package/data/migration.json +171 -20
- package/data/section-templates/account-info-section/children/AccountFavorites/ikas-config-snippet.json +3 -3
- package/data/section-templates/account-info-section/ikas-config-snippet.json +5 -5
- package/data/section-templates/category-images-section/ikas-config-snippet.json +1 -1
- package/data/section-templates/category-list-section/ikas-config-snippet.json +3 -3
- package/data/section-templates/component-renderer/ikas-config-snippet.json +3 -3
- package/data/section-templates/features-section/ikas-config-snippet.json +1 -1
- package/data/section-templates/footer-section/ikas-config-snippet.json +1 -1
- package/data/section-templates/header-section/children/Announcements/ikas-config-snippet.json +1 -1
- package/data/section-templates/header-section/children/Navbar/ikas-config-snippet.json +3 -3
- package/data/section-templates/header-section/ikas-config-snippet.json +3 -3
- package/data/section-templates/hero-slider-section/ikas-config-snippet.json +1 -1
- package/data/section-templates/image-handling/ikas-config-snippet.json +13 -13
- package/data/section-templates/navigation/ikas-config-snippet.json +3 -3
- package/data/section-templates/product-detail-section/children/ProductDetailDescription/ikas-config-snippet.json +1 -1
- package/data/section-templates/product-detail-section/children/ProductDetailFeatures/ikas-config-snippet.json +1 -1
- package/data/section-templates/product-detail-section/ikas-config-snippet.json +13 -13
- package/data/section-templates/product-slider-section/ikas-config-snippet.json +3 -3
- package/data/storefront-api.json +1 -1
- package/data/storefront-types.json +1 -1
- package/dist/index.js +483 -78
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15,6 +15,59 @@ function loadJsonFile(relativePath) {
|
|
|
15
15
|
}
|
|
16
16
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
17
17
|
}
|
|
18
|
+
// Resolve theme.json from either raw string or absolute file path.
|
|
19
|
+
// Exactly one of the two must be provided.
|
|
20
|
+
function resolveThemeJson(themeJson, themeJsonPath) {
|
|
21
|
+
const hasInline = typeof themeJson === "string" && themeJson.length > 0;
|
|
22
|
+
const hasPath = typeof themeJsonPath === "string" && themeJsonPath.length > 0;
|
|
23
|
+
if (hasInline === hasPath) {
|
|
24
|
+
throw new Error("Provide exactly one of `theme_json` (raw JSON string) or `theme_json_path` (absolute path to theme.json).");
|
|
25
|
+
}
|
|
26
|
+
let raw;
|
|
27
|
+
if (hasPath) {
|
|
28
|
+
if (!path.isAbsolute(themeJsonPath)) {
|
|
29
|
+
throw new Error(`theme_json_path must be absolute: ${themeJsonPath}`);
|
|
30
|
+
}
|
|
31
|
+
if (!fs.existsSync(themeJsonPath)) {
|
|
32
|
+
throw new Error(`theme_json_path not found: ${themeJsonPath}`);
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
raw = fs.readFileSync(themeJsonPath, "utf-8");
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
throw new Error(`Failed to read theme_json_path "${themeJsonPath}": ${e.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
raw = themeJson;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(raw);
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
throw new Error(`Invalid JSON in theme.json: ${e.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Atomic file write: temp file in same dir, then rename. Same-fs rename is atomic on POSIX.
|
|
52
|
+
function writeFileAtomic(targetPath, content) {
|
|
53
|
+
const dir = path.dirname(targetPath);
|
|
54
|
+
const base = path.basename(targetPath);
|
|
55
|
+
const tmp = path.join(dir, `.${base}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
|
|
56
|
+
try {
|
|
57
|
+
fs.writeFileSync(tmp, content, "utf-8");
|
|
58
|
+
fs.renameSync(tmp, targetPath);
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
try {
|
|
62
|
+
if (fs.existsSync(tmp))
|
|
63
|
+
fs.unlinkSync(tmp);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// ignore cleanup failure
|
|
67
|
+
}
|
|
68
|
+
throw e;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
18
71
|
// Try multiple paths for storefront data (generated output or local data dir)
|
|
19
72
|
function loadStorefrontData() {
|
|
20
73
|
const paths = [
|
|
@@ -506,18 +559,22 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
506
559
|
}
|
|
507
560
|
const sharedSubs = oldSourceDir ? scanSharedSubcomponents(oldSourceDir) : [];
|
|
508
561
|
const parts = [];
|
|
509
|
-
parts.push(`# Theme Migration Plan
|
|
562
|
+
parts.push(`# Theme Migration Plan — \`${projectName}\``);
|
|
510
563
|
parts.push("");
|
|
511
564
|
parts.push(`**Generated:** ${new Date().toISOString().slice(0, 10)}`);
|
|
512
|
-
parts.push(`**Project ID:** \`${projectName}\``);
|
|
513
565
|
parts.push(`**Source:** ${components.length} old components, ${customData.filter(cd => cd.isRoot).length} custom data types, ${(theme.pages || []).length} pages`);
|
|
514
566
|
parts.push("");
|
|
515
|
-
parts.push(
|
|
516
|
-
parts.push(
|
|
517
|
-
parts.push(
|
|
518
|
-
parts.push(
|
|
519
|
-
parts.push(
|
|
520
|
-
parts.push(
|
|
567
|
+
parts.push(`> ## READ THIS FIRST`);
|
|
568
|
+
parts.push(`>`);
|
|
569
|
+
parts.push(`> This file is your responsibility from here. The MCP wrote this initial scaffold **once**. You own all updates — no MCP tool will modify this file again.`);
|
|
570
|
+
parts.push(`>`);
|
|
571
|
+
parts.push(`> 1. **Tick checkboxes** as you finish work. Use \`[~]\` for in-progress and \`[x]\` for done. Edit the file directly with your file-editing tools.`);
|
|
572
|
+
parts.push(`> 2. **theme.json is incomplete.** Atomic components (Button, Input, Card, icons, etc.) often live only in \`src/\` and are NOT referenced from theme.json. Before you start section migration, scan the old source directory and ADD entries to this file for anything the initial scan missed — list them under \`## Source Code Analysis\` and add shared ones to \`### Shared Sub-Components\`.`);
|
|
573
|
+
parts.push(`> 3. **Custom data types are NOT pre-converted.** For each customData entry used by a section, decide enum-vs-component when you migrate that section. Log every decision in \`## Custom Data Decisions\` with reasoning. See \`get_migration_guide("custom-data-conversion")\` for the heuristic.`);
|
|
574
|
+
parts.push(`> 4. **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\`.`);
|
|
576
|
+
parts.push(`>`);
|
|
577
|
+
parts.push(`> Status legend: \`[ ]\` not started · \`[~]\` in progress · \`[x]\` complete.`);
|
|
521
578
|
parts.push("");
|
|
522
579
|
parts.push(`## Foundation`);
|
|
523
580
|
parts.push("");
|
|
@@ -537,27 +594,9 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
537
594
|
parts.push(`- [ ] ${settings.fontFamily.name} (weights: ${settings.fontFamily.variants?.join(", ") || "default"})`);
|
|
538
595
|
parts.push("");
|
|
539
596
|
}
|
|
540
|
-
// Custom Enums
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
parts.push(`### Custom Enums (run these BEFORE any \`config add-component\`)`);
|
|
544
|
-
parts.push("");
|
|
545
|
-
for (const cd of enumCustomData) {
|
|
546
|
-
const enumName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
|
|
547
|
-
const options = (cd.enumOptions || []).reduce((acc, o) => {
|
|
548
|
-
if (o.displayName && o.value)
|
|
549
|
-
acc[o.displayName] = o.value;
|
|
550
|
-
return acc;
|
|
551
|
-
}, {});
|
|
552
|
-
const optionsStr = JSON.stringify(options);
|
|
553
|
-
parts.push(`- [ ] **${enumName}**`);
|
|
554
|
-
parts.push(` \`\`\`bash`);
|
|
555
|
-
parts.push(` npx ikas-component config add-enum --name "${enumName}" --options '${optionsStr}'`);
|
|
556
|
-
parts.push(` \`\`\``);
|
|
557
|
-
parts.push(` Save the returned \`enumId\` and update this file with: \`enumId: <id>\``);
|
|
558
|
-
}
|
|
559
|
-
parts.push("");
|
|
560
|
-
}
|
|
597
|
+
// (Custom Enums foundation subsection intentionally removed — see ## Custom Data Types below.
|
|
598
|
+
// Old customData entries are NOT pre-migrated as enums; each is a per-section decision.
|
|
599
|
+
// See get_migration_guide("custom-data-conversion") for the enum-vs-component heuristic.)
|
|
561
600
|
// Shared sub-components
|
|
562
601
|
if (sharedSubs.length > 0) {
|
|
563
602
|
parts.push(`### Shared Sub-Components (→ \`src/sub-components/\`)`);
|
|
@@ -581,6 +620,51 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
581
620
|
parts.push(`_Old source directory not provided — shared sub-components cannot be detected automatically. If you have access to the old source, call \`plan_migration\` again with \`old_source_dir\` to populate this section._`);
|
|
582
621
|
parts.push("");
|
|
583
622
|
}
|
|
623
|
+
parts.push(`> **This scan is partial.** \`scanSharedSubcomponents\` only detects relative imports used by **3 or more** old components. Atomic components used by fewer sections (often Button/Input/Card/icon primitives) will be missed. Scan \`src/\` yourself and add any others under \`## Source Code Analysis\` below.`);
|
|
624
|
+
parts.push("");
|
|
625
|
+
// Custom Data Types — deferred decisions (reference list, not checkboxes)
|
|
626
|
+
const rootCustomData = customData.filter((cd) => cd.isRoot);
|
|
627
|
+
if (rootCustomData.length > 0) {
|
|
628
|
+
// Build a usage map: which sections reference each customData type
|
|
629
|
+
const usageByCustomDataId = new Map();
|
|
630
|
+
for (const comp of components) {
|
|
631
|
+
const sectionName = comp.displayName || comp.dir || comp.id || "?";
|
|
632
|
+
for (const p of comp.props || []) {
|
|
633
|
+
if (p.type === "CUSTOM" && p.customDataId) {
|
|
634
|
+
const list = usageByCustomDataId.get(p.customDataId) || [];
|
|
635
|
+
if (!list.includes(sectionName))
|
|
636
|
+
list.push(sectionName);
|
|
637
|
+
usageByCustomDataId.set(p.customDataId, list);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
parts.push(`### Custom Data Types — decide during section migration`);
|
|
642
|
+
parts.push("");
|
|
643
|
+
parts.push(`Each entry below is an OLD \`theme.customData[]\` type. Do **not** pre-migrate these as enums. When you migrate the section that uses one, decide:`);
|
|
644
|
+
parts.push(`- **Flat scalar set** (e.g. \`"left" | "right" | "center"\`) → new-system **enum prop** via \`config add-enum\`.`);
|
|
645
|
+
parts.push(`- **Structured record** (e.g. \`{image, link, title}\`, repeated in a list) → new-system **component** via \`config add-component\` + COMPONENT_LIST wiring on the parent.`);
|
|
646
|
+
parts.push("");
|
|
647
|
+
parts.push(`Log every decision in \`## Custom Data Decisions\` below. See \`get_migration_guide("custom-data-conversion")\` for the full heuristic and worked examples (Position, Slide, MenuItem).`);
|
|
648
|
+
parts.push("");
|
|
649
|
+
for (const cd of rootCustomData) {
|
|
650
|
+
const cdName = cd.typescriptName || cd.name || cd.id || "Unknown";
|
|
651
|
+
const cdType = cd.type || "?";
|
|
652
|
+
let shape = "";
|
|
653
|
+
if (cd.type === "ENUM") {
|
|
654
|
+
const opts = (cd.enumOptions || []).map((o) => o.value || o.displayName).filter(Boolean);
|
|
655
|
+
shape = ` — shape: \`enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}\``;
|
|
656
|
+
}
|
|
657
|
+
else if (cd.nestedData && cd.nestedData.length > 0) {
|
|
658
|
+
const first = cd.nestedData[0];
|
|
659
|
+
const fields = (first?.nestedData || cd.nestedData || []).map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`).slice(0, 8);
|
|
660
|
+
shape = ` — shape: \`{${fields.join(", ")}}\``;
|
|
661
|
+
}
|
|
662
|
+
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)_`;
|
|
664
|
+
parts.push(`- \`${cdName}\` (${cdType})${shape}${usedByStr}`);
|
|
665
|
+
}
|
|
666
|
+
parts.push("");
|
|
667
|
+
}
|
|
584
668
|
// Sections queue
|
|
585
669
|
parts.push(`## Sections`);
|
|
586
670
|
parts.push("");
|
|
@@ -638,10 +722,32 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
638
722
|
}
|
|
639
723
|
parts.push("");
|
|
640
724
|
}
|
|
725
|
+
// Source Code Analysis — placeholder for the LLM to fill in
|
|
726
|
+
parts.push(`## Source Code Analysis`);
|
|
727
|
+
parts.push("");
|
|
728
|
+
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
|
+
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(`-->`);
|
|
735
|
+
parts.push("");
|
|
736
|
+
// Custom Data Decisions — append-only log the LLM fills as it makes per-section decisions
|
|
737
|
+
parts.push(`## Custom Data Decisions`);
|
|
738
|
+
parts.push("");
|
|
739
|
+
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
|
+
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(`-->`);
|
|
746
|
+
parts.push("");
|
|
641
747
|
// Known Environmental Issues (agents fill in during work)
|
|
642
748
|
parts.push(`## Known Environmental Issues`);
|
|
643
749
|
parts.push("");
|
|
644
|
-
parts.push(`
|
|
750
|
+
parts.push(`_Record any non-component build/TS errors here so future sessions don't waste time diagnosing them._`);
|
|
645
751
|
parts.push("");
|
|
646
752
|
parts.push(`- [ ] _(none recorded yet)_`);
|
|
647
753
|
parts.push("");
|
|
@@ -651,20 +757,22 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
651
757
|
parts.push(`Once the Foundation is complete, for each section above:`);
|
|
652
758
|
parts.push("");
|
|
653
759
|
parts.push(`1. Find the first unchecked \`[ ]\` section (start with Simple).`);
|
|
654
|
-
parts.push(`2. Call \`get_section_migration_plan(
|
|
760
|
+
parts.push(`2. Call \`get_section_migration_plan({theme_json_path, section_name: "<old section name>", project_name: "${projectName}"})\`.`);
|
|
655
761
|
parts.push(`3. Read the old source files listed in the plan.`);
|
|
656
|
-
parts.push(`4.
|
|
657
|
-
parts.push(`5.
|
|
658
|
-
parts.push(`6.
|
|
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).`);
|
|
659
766
|
parts.push("");
|
|
660
|
-
//
|
|
661
|
-
parts.push(`##
|
|
767
|
+
// Notes — append-only log for decisions not captured elsewhere
|
|
768
|
+
parts.push(`## Notes`);
|
|
662
769
|
parts.push("");
|
|
663
|
-
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.)._`);
|
|
664
771
|
parts.push("");
|
|
665
|
-
parts.push(
|
|
666
|
-
parts.push(
|
|
667
|
-
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(`-->`);
|
|
668
776
|
parts.push("");
|
|
669
777
|
// Cross-references
|
|
670
778
|
parts.push(`## Cross-References`);
|
|
@@ -1057,6 +1165,99 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1057
1165
|
parts.push(`| \`${oldName}\` | ${oldType} | → | \`${newName}\` | ${newType} | ${notes} |`);
|
|
1058
1166
|
}
|
|
1059
1167
|
parts.push("");
|
|
1168
|
+
// 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));
|
|
1170
|
+
if (customDataPropsForCallouts.length > 0) {
|
|
1171
|
+
parts.push(`## Custom Data Decisions to Make`);
|
|
1172
|
+
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\`.`);
|
|
1181
|
+
parts.push("");
|
|
1182
|
+
for (const p of customDataPropsForCallouts) {
|
|
1183
|
+
const cd = customDataMap.get(p.customDataId);
|
|
1184
|
+
if (!cd)
|
|
1185
|
+
continue;
|
|
1186
|
+
const cdName = cd.typescriptName || cd.name || cd.id || "Unknown";
|
|
1187
|
+
const cdType = cd.type || "?";
|
|
1188
|
+
let shape = "";
|
|
1189
|
+
let shapeKind = "unknown";
|
|
1190
|
+
if (cd.type === "ENUM") {
|
|
1191
|
+
const opts = (cd.enumOptions || []).map((o) => o.value || o.displayName).filter(Boolean);
|
|
1192
|
+
shape = `enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}`;
|
|
1193
|
+
shapeKind = "enum";
|
|
1194
|
+
}
|
|
1195
|
+
else if (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST") {
|
|
1196
|
+
const first = cd.nestedData?.[0];
|
|
1197
|
+
const fields = (first?.nestedData || []).map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`);
|
|
1198
|
+
shape = `${cdType} of {${fields.slice(0, 6).join(", ")}}`;
|
|
1199
|
+
shapeKind = "list";
|
|
1200
|
+
}
|
|
1201
|
+
else if (cd.type === "OBJECT") {
|
|
1202
|
+
const fields = (cd.nestedData || []).map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`);
|
|
1203
|
+
shape = `OBJECT {${fields.slice(0, 6).join(", ")}}`;
|
|
1204
|
+
shapeKind = "record";
|
|
1205
|
+
}
|
|
1206
|
+
else {
|
|
1207
|
+
shape = cdType;
|
|
1208
|
+
}
|
|
1209
|
+
parts.push(`### Prop \`${p.name || "?"}\` → customData \`${cdName}\``);
|
|
1210
|
+
parts.push("");
|
|
1211
|
+
parts.push(`**Shape:** \`${shape}\``);
|
|
1212
|
+
parts.push("");
|
|
1213
|
+
let recommendation;
|
|
1214
|
+
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).`;
|
|
1216
|
+
}
|
|
1217
|
+
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.`;
|
|
1219
|
+
}
|
|
1220
|
+
else {
|
|
1221
|
+
recommendation = `**MCP default: unable to classify automatically — you decide.**`;
|
|
1222
|
+
}
|
|
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
|
+
}
|
|
1260
|
+
}
|
|
1060
1261
|
// Enums to create first
|
|
1061
1262
|
if (enumsNeeded.length > 0) {
|
|
1062
1263
|
parts.push(`## 3. Create Enums FIRST (if not already done)`);
|
|
@@ -1242,10 +1443,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1242
1443
|
parts.push(`## ${nextStep + 2}. Mark Complete`);
|
|
1243
1444
|
parts.push("");
|
|
1244
1445
|
parts.push(`Once the section builds cleanly with \`npx ikas-component build\`:`);
|
|
1245
|
-
parts.push(`1. Edit \`MIGRATION.md\` at the project root`);
|
|
1446
|
+
parts.push(`1. Edit \`MIGRATION.md\` at the project root with your file-editing tool (no MCP tool needed)`);
|
|
1246
1447
|
parts.push(`2. Change the checkbox for \`${sectionId}\` from \`[ ]\` to \`[x]\``);
|
|
1247
1448
|
parts.push(`3. Also mark each child component as \`[x]\``);
|
|
1248
|
-
parts.push(`4.
|
|
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.`);
|
|
1249
1451
|
parts.push("");
|
|
1250
1452
|
return parts.join("\n");
|
|
1251
1453
|
}
|
|
@@ -1271,6 +1473,54 @@ function matchScore(text, query) {
|
|
|
1271
1473
|
}
|
|
1272
1474
|
return score;
|
|
1273
1475
|
}
|
|
1476
|
+
// Levenshtein edit distance — small DP, fine for the 28 short kebab-case section template names.
|
|
1477
|
+
function levenshtein(a, b) {
|
|
1478
|
+
if (a === b)
|
|
1479
|
+
return 0;
|
|
1480
|
+
if (a.length === 0)
|
|
1481
|
+
return b.length;
|
|
1482
|
+
if (b.length === 0)
|
|
1483
|
+
return a.length;
|
|
1484
|
+
const m = a.length;
|
|
1485
|
+
const n = b.length;
|
|
1486
|
+
const prev = new Array(n + 1);
|
|
1487
|
+
const curr = new Array(n + 1);
|
|
1488
|
+
for (let j = 0; j <= n; j++)
|
|
1489
|
+
prev[j] = j;
|
|
1490
|
+
for (let i = 1; i <= m; i++) {
|
|
1491
|
+
curr[0] = i;
|
|
1492
|
+
for (let j = 1; j <= n; j++) {
|
|
1493
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
1494
|
+
curr[j] = Math.min(curr[j - 1] + 1, // insertion
|
|
1495
|
+
prev[j] + 1, // deletion
|
|
1496
|
+
prev[j - 1] + cost // substitution
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
for (let j = 0; j <= n; j++)
|
|
1500
|
+
prev[j] = curr[j];
|
|
1501
|
+
}
|
|
1502
|
+
return prev[n];
|
|
1503
|
+
}
|
|
1504
|
+
// Suggest up to `max` closest candidates for an unknown input.
|
|
1505
|
+
// Primary: matchScore (substring/word). Fallback: Levenshtein when nothing scores well.
|
|
1506
|
+
function suggestClosestNames(input, candidates, opts) {
|
|
1507
|
+
const min = opts?.min ?? 8;
|
|
1508
|
+
const max = opts?.max ?? 3;
|
|
1509
|
+
const scored = candidates
|
|
1510
|
+
.map((c) => ({ name: c, score: matchScore(c, input) }))
|
|
1511
|
+
.filter((s) => s.score >= min)
|
|
1512
|
+
.sort((a, b) => b.score - a.score);
|
|
1513
|
+
if (scored.length > 0)
|
|
1514
|
+
return scored.slice(0, max).map((s) => s.name);
|
|
1515
|
+
// Levenshtein fallback — tolerate up to ceil(len/3) edits.
|
|
1516
|
+
const maxDistance = Math.max(1, Math.ceil(input.length / 3));
|
|
1517
|
+
const lower = input.toLowerCase();
|
|
1518
|
+
const edits = candidates
|
|
1519
|
+
.map((c) => ({ name: c, d: levenshtein(lower, c.toLowerCase()) }))
|
|
1520
|
+
.filter((s) => s.d <= maxDistance)
|
|
1521
|
+
.sort((a, b) => a.d - b.d);
|
|
1522
|
+
return edits.slice(0, max).map((s) => s.name);
|
|
1523
|
+
}
|
|
1274
1524
|
function searchFunctions(query) {
|
|
1275
1525
|
const scored = storefrontData.functions
|
|
1276
1526
|
.map((fn) => {
|
|
@@ -2099,7 +2349,7 @@ server.tool("get_prop_types", "Get all available ikas.config.json prop types wit
|
|
|
2099
2349
|
const sectionTemplateKeys = sectionTemplateNames.length > 0
|
|
2100
2350
|
? sectionTemplateNames
|
|
2101
2351
|
: null;
|
|
2102
|
-
server.tool("get_section_template", "Get the root files of a starter section template (index.tsx, types.ts, styles.css, ikas-config-snippet.json). Returns ONLY the section's root files plus the NAMES of any children, components, sub-components, utilities, and hooks — their files are NOT included by default. To view one item's full implementation, call `get_section_child(section, name, kind)` where kind is 'children' (default), 'components', or 'sub-components'. To bundle subtrees inline, pass `include`. Call `list_section_types()` for available section types. Use the API patterns shown — create your own JSX structure, CSS class names, and visual design.", {
|
|
2352
|
+
server.tool("get_section_template", "Get the root files of a starter section template (index.tsx, types.ts, styles.css, ikas-config-snippet.json). Returns ONLY the section's root files plus the NAMES of any children, components, sub-components, utilities, and hooks — their files are NOT included by default. To view one item's full implementation, call `get_section_child(section, name, kind)` where kind is 'children' (default), 'components', or 'sub-components'. To bundle subtrees inline, pass `include`. Call `list_section_types()` for available section types. Use the API patterns shown — create your own JSX structure, CSS class names, and visual design. **Container sections** (Header, Footer, ProductDetail, etc.) host child components via a `COMPONENT_LIST` slot — the response emits a complete multi-step Setup Recipe (create children → capture ids → wire `filteredComponentIds` via `config update-prop`). Follow all steps; the parent alone produces an empty section.", {
|
|
2103
2353
|
sectionType: z
|
|
2104
2354
|
.string()
|
|
2105
2355
|
.describe("The section type (call `list_section_types()` for valid values)"),
|
|
@@ -2141,11 +2391,19 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2141
2391
|
}
|
|
2142
2392
|
const normalizedType = normalizeName(sectionType);
|
|
2143
2393
|
if (!sectionTemplateNames.includes(normalizedType)) {
|
|
2394
|
+
const suggestions = suggestClosestNames(normalizedType, sectionTemplateNames);
|
|
2395
|
+
let suggestionText = "";
|
|
2396
|
+
if (suggestions.length === 1) {
|
|
2397
|
+
suggestionText = ` Did you mean "${suggestions[0]}"?`;
|
|
2398
|
+
}
|
|
2399
|
+
else if (suggestions.length > 1) {
|
|
2400
|
+
suggestionText = ` Did you mean one of: ${suggestions.map((s) => `"${s}"`).join(", ")}?`;
|
|
2401
|
+
}
|
|
2144
2402
|
return {
|
|
2145
2403
|
content: [
|
|
2146
2404
|
{
|
|
2147
2405
|
type: "text",
|
|
2148
|
-
text: `Unknown section type "${sectionType}"
|
|
2406
|
+
text: `Unknown section type "${sectionType}".${suggestionText} Call \`list_section_types()\` to see all ${sectionTemplateNames.length} valid types.`,
|
|
2149
2407
|
},
|
|
2150
2408
|
],
|
|
2151
2409
|
};
|
|
@@ -2174,6 +2432,18 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2174
2432
|
`## ${bundle.title} — API Integration Pattern Reference`,
|
|
2175
2433
|
"",
|
|
2176
2434
|
];
|
|
2435
|
+
// Detect container-section pattern (COMPONENT_LIST with `<id-of-X>` placeholders).
|
|
2436
|
+
// Surface this BEFORE every other warning so the LLM cannot miss the wiring requirement.
|
|
2437
|
+
// The full recipe (commands, captured-id placeholders, update-prop call) is appended near
|
|
2438
|
+
// the end of the response in the existing recipe-builder block.
|
|
2439
|
+
{
|
|
2440
|
+
const snippetStrForBanner = bundle.rootFiles["ikas-config-snippet.json"];
|
|
2441
|
+
if (snippetStrForBanner && /<id-of-[A-Za-z0-9_]+>/.test(snippetStrForBanner)) {
|
|
2442
|
+
const childMatches = Array.from(snippetStrForBanner.matchAll(/<id-of-([A-Za-z0-9_]+)>/g));
|
|
2443
|
+
const uniqueChildren = Array.from(new Set(childMatches.map((m) => m[1])));
|
|
2444
|
+
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.`, "");
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2177
2447
|
if (anyOutstandingSubtree) {
|
|
2178
2448
|
parts.push("> **PARTIAL TEMPLATE — NOT A COMPLETE IMPLEMENTATION.** The files below are the section's root files only. The section also ships with child components, local components, and/or shared sub-components whose files are **not included** in this response. To view one's full implementation, call `get_section_child(section, name, kind)` for each item you need (kind = `children`, `components`, or `sub-components`). The root `index.tsx` alone is not a complete reference — its imports resolve to files that live in those subtrees.", "");
|
|
2179
2449
|
}
|
|
@@ -2242,7 +2512,10 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2242
2512
|
renderInlineSubtree("children", bundle.childContents);
|
|
2243
2513
|
renderInlineSubtree("components", bundle.componentContents);
|
|
2244
2514
|
renderInlineSubtree("sub-components", bundle.subComponentContents);
|
|
2245
|
-
// Generate a ready-to-run CLI
|
|
2515
|
+
// Generate a ready-to-run CLI recipe from the config snippet. If the parent's
|
|
2516
|
+
// filteredComponentIds reference `<id-of-X>` placeholders, expand into a
|
|
2517
|
+
// multi-step recipe (create children → create parent → wire filteredComponentIds)
|
|
2518
|
+
// so the LLM cannot skip the wiring step.
|
|
2246
2519
|
const configSnippetStr = bundle.rootFiles["ikas-config-snippet.json"];
|
|
2247
2520
|
if (configSnippetStr) {
|
|
2248
2521
|
try {
|
|
@@ -2250,26 +2523,102 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2250
2523
|
const compName = configSnippet.name || normalizedType;
|
|
2251
2524
|
const compType = configSnippet.type || "section";
|
|
2252
2525
|
const propsArr = configSnippet.props || [];
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2526
|
+
// Strip filteredComponentIds for the parent's add-component call (wired
|
|
2527
|
+
// separately below). Preserve name/type/displayName/required so the LLM gets the prop shape.
|
|
2528
|
+
const parentPropsForAdd = propsArr.map((p) => {
|
|
2529
|
+
const out = { name: p.name, type: p.type };
|
|
2530
|
+
if (p.displayName)
|
|
2531
|
+
out.displayName = p.displayName;
|
|
2532
|
+
if (p.required)
|
|
2533
|
+
out.required = true;
|
|
2534
|
+
return out;
|
|
2535
|
+
});
|
|
2536
|
+
const placeholderRe = /^<id-of-(.+)>$/;
|
|
2537
|
+
const slotPlans = [];
|
|
2538
|
+
const allChildNames = new Set();
|
|
2539
|
+
for (const p of propsArr) {
|
|
2540
|
+
if ((p.type === "COMPONENT_LIST" || p.type === "COMPONENT") &&
|
|
2541
|
+
Array.isArray(p.filteredComponentIds)) {
|
|
2542
|
+
const childNames = [];
|
|
2543
|
+
for (const entry of p.filteredComponentIds) {
|
|
2544
|
+
const m = typeof entry === "string" && entry.match(placeholderRe);
|
|
2545
|
+
if (m) {
|
|
2546
|
+
childNames.push(m[1]);
|
|
2547
|
+
allChildNames.add(m[1]);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
if (childNames.length > 0) {
|
|
2551
|
+
slotPlans.push({ propName: p.name, childNames });
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
const childPlans = [];
|
|
2556
|
+
for (const childName of allChildNames) {
|
|
2557
|
+
const childSnippetPath = path.join(SECTION_TEMPLATES_DIR, normalizedType, "children", childName, "ikas-config-snippet.json");
|
|
2558
|
+
let propsJson = "[]";
|
|
2559
|
+
let hasTemplate = false;
|
|
2560
|
+
if (fs.existsSync(childSnippetPath)) {
|
|
2561
|
+
hasTemplate = true;
|
|
2562
|
+
try {
|
|
2563
|
+
const childSnippet = JSON.parse(fs.readFileSync(childSnippetPath, "utf-8"));
|
|
2564
|
+
const childProps = (childSnippet.props || []).map((p) => {
|
|
2565
|
+
const out = { name: p.name, type: p.type };
|
|
2566
|
+
if (p.displayName)
|
|
2567
|
+
out.displayName = p.displayName;
|
|
2568
|
+
if (p.required)
|
|
2569
|
+
out.required = true;
|
|
2570
|
+
return out;
|
|
2571
|
+
});
|
|
2572
|
+
propsJson = JSON.stringify(childProps);
|
|
2573
|
+
}
|
|
2574
|
+
catch {
|
|
2575
|
+
// fall through with []
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
childPlans.push({ name: childName, propsJson, hasTemplate });
|
|
2579
|
+
}
|
|
2580
|
+
const baseFlags = (configSnippet.isHeader ? " --isHeader" : "") +
|
|
2581
|
+
(configSnippet.isFooter ? " --isFooter" : "");
|
|
2582
|
+
const parentPropsJsonStr = parentPropsForAdd.length > 0
|
|
2583
|
+
? ` --props '${JSON.stringify(parentPropsForAdd)}'`
|
|
2584
|
+
: "";
|
|
2585
|
+
if (slotPlans.length > 0) {
|
|
2586
|
+
parts.push("---", "");
|
|
2587
|
+
parts.push(`### Setup Recipe (run in order — ${childPlans.length} child component${childPlans.length === 1 ? "" : "s"} + 1 parent + wiring)`);
|
|
2588
|
+
parts.push("");
|
|
2589
|
+
parts.push(`> ⚠️ **This section is a CONTAINER.** It hosts child components via ${slotPlans.length === 1 ? "a COMPONENT_LIST slot" : "COMPONENT_LIST slots"} (\`${slotPlans.map((s) => s.propName).join("`, `")}\`). Creating the parent alone is **not enough** — you MUST also create the child components and wire their opaque ids into the parent's \`filteredComponentIds\`. Skipping the wiring leaves the slot empty and the section unusable.`, "");
|
|
2590
|
+
parts.push("**Step 1 — Create each child component, and capture its `componentId` from the JSON response:**");
|
|
2591
|
+
parts.push("", "```bash");
|
|
2592
|
+
for (const ch of childPlans) {
|
|
2593
|
+
parts.push(`npx ikas-component config add-component --name "${ch.name}" --type component --props '${ch.propsJson}'`);
|
|
2594
|
+
parts.push(`# → { "success": true, "componentId": "<capture as ${ch.name.toUpperCase()}_ID>", ... }`);
|
|
2595
|
+
if (!ch.hasTemplate) {
|
|
2596
|
+
parts.push(`# (no children/${ch.name}/ template in this section bundle — \`--props '[]'\` is a stub; add real props for ${ch.name} as needed)`);
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
parts.push("```", "");
|
|
2600
|
+
parts.push("**Step 2 — Create the parent section (without `filteredComponentIds` yet — those are wired in Step 3):**");
|
|
2601
|
+
parts.push("", "```bash");
|
|
2602
|
+
parts.push(`npx ikas-component config add-component --name "${compName}" --type ${compType}${baseFlags}${parentPropsJsonStr}`);
|
|
2603
|
+
parts.push("```", "");
|
|
2604
|
+
parts.push("**Step 3 — Wire each slot to its allowed children using the ids captured in Step 1:**");
|
|
2605
|
+
parts.push("", "```bash");
|
|
2606
|
+
for (const slot of slotPlans) {
|
|
2607
|
+
const idsArr = slot.childNames.map((n) => `<${n.toUpperCase()}_ID>`);
|
|
2608
|
+
parts.push(`npx ikas-component config update-prop --component "${compName}" --prop ${slot.propName} \\`);
|
|
2609
|
+
parts.push(` --filteredComponentIds '${JSON.stringify(idsArr)}'`);
|
|
2610
|
+
}
|
|
2611
|
+
parts.push("```", "");
|
|
2612
|
+
parts.push("Replace each `<X_ID>` placeholder above with the real `componentId` from the corresponding Step 1 response (or look ids up with `config list`). The CLI rejects unknown ids with a structured error — there is no silent failure mode.");
|
|
2613
|
+
parts.push("");
|
|
2614
|
+
parts.push("**Do NOT manually create or edit `types.ts`** — the CLI commands above regenerate it automatically.");
|
|
2615
|
+
parts.push("");
|
|
2616
|
+
}
|
|
2617
|
+
else {
|
|
2618
|
+
// No child slots — single-step parent command (preserves previous behaviour)
|
|
2619
|
+
const cliCommand = `npx ikas-component config add-component --name "${compName}" --type ${compType}${baseFlags}${parentPropsJsonStr}`;
|
|
2620
|
+
parts.push("---", "", "### CLI Command (run this first)", "", "```bash", cliCommand, "```", "", "**Do NOT manually create or edit `types.ts`** — the CLI command above generates it automatically.", "");
|
|
2271
2621
|
}
|
|
2272
|
-
parts.push("---", "", "### CLI Command (run this first)", "", "```bash", cliCommand, "```", "", "**Do NOT manually create or edit `types.ts`** — the CLI command above generates it automatically.", "", "**`filteredComponentIds` placeholders:** Any entry like `\"<id-of-Navbar>\"` in `filteredComponentIds` is a placeholder, not a literal value. Component ids are opaque random strings and cannot be derived from names. Create each referenced child component first with `config add-component` (which returns the new `componentId` in its JSON response), or read existing ids with `config list`, then substitute the real id into the parent's `filteredComponentIds` via `config add-prop`/`config update-prop`.", "");
|
|
2273
2622
|
}
|
|
2274
2623
|
catch {
|
|
2275
2624
|
// ignore parse errors
|
|
@@ -2511,7 +2860,7 @@ const migrationTopicAliases = {
|
|
|
2511
2860
|
const migrationTopicKeys = migrationData
|
|
2512
2861
|
? Object.keys(migrationData.topics)
|
|
2513
2862
|
: [];
|
|
2514
|
-
server.tool("get_migration_guide", `Get a migration guide for converting old ikas themes to the new code-component system.${migrationTopicKeys.length > 0 ? ` Available topics: ${migrationTopicKeys.join(", ")}. Also supports aliases like "custom", "slider", "react", "libraries", "imports", "settings".` : ""} Call with topic "list" to see all available topics.`, { topic: z.string().describe("Migration topic key, alias, or 'list' to see all topics") }, async ({ topic }) => {
|
|
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 }) => {
|
|
2515
2864
|
if (!migrationData) {
|
|
2516
2865
|
return { content: [{ type: "text", text: "Migration data not available. Ensure data/migration.json exists." }] };
|
|
2517
2866
|
}
|
|
@@ -2595,39 +2944,95 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
|
|
|
2595
2944
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
2596
2945
|
});
|
|
2597
2946
|
// Tool: plan_migration
|
|
2598
|
-
server.tool("plan_migration", "Generate
|
|
2599
|
-
theme_json: z.string().describe("Raw JSON content of the old theme.json"),
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2947
|
+
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 partial — atomic 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 }) => {
|
|
2603
2955
|
try {
|
|
2604
|
-
const parsed =
|
|
2956
|
+
const parsed = resolveThemeJson(theme_json, theme_json_path);
|
|
2605
2957
|
const projectName = project_name || "my-theme";
|
|
2606
2958
|
const plan = generateMigrationPlan(parsed, projectName, old_source_dir);
|
|
2607
|
-
|
|
2959
|
+
if (!project_root) {
|
|
2960
|
+
return { content: [{ type: "text", text: plan }] };
|
|
2961
|
+
}
|
|
2962
|
+
// Write MIGRATION.md to project_root
|
|
2963
|
+
if (!path.isAbsolute(project_root)) {
|
|
2964
|
+
throw new Error(`project_root must be absolute: ${project_root}`);
|
|
2965
|
+
}
|
|
2966
|
+
if (!fs.existsSync(project_root)) {
|
|
2967
|
+
throw new Error(`project_root not found: ${project_root}`);
|
|
2968
|
+
}
|
|
2969
|
+
if (!fs.statSync(project_root).isDirectory()) {
|
|
2970
|
+
throw new Error(`project_root is not a directory: ${project_root}`);
|
|
2971
|
+
}
|
|
2972
|
+
const targetPath = path.join(project_root, "MIGRATION.md");
|
|
2973
|
+
if (fs.existsSync(targetPath)) {
|
|
2974
|
+
const existing = fs.readFileSync(targetPath, "utf-8").trim();
|
|
2975
|
+
if (existing.length > 0 && !overwrite) {
|
|
2976
|
+
return {
|
|
2977
|
+
content: [
|
|
2978
|
+
{
|
|
2979
|
+
type: "text",
|
|
2980
|
+
text: `Refusing to overwrite existing non-empty MIGRATION.md at ${targetPath}. ` +
|
|
2981
|
+
`Pass overwrite: true to replace it, or delete the file first. ` +
|
|
2982
|
+
`If you intended to RESUME an in-progress migration, do not call plan_migration again — read the existing MIGRATION.md and continue from the first unchecked item.`,
|
|
2983
|
+
},
|
|
2984
|
+
],
|
|
2985
|
+
};
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
writeFileAtomic(targetPath, plan);
|
|
2989
|
+
const components = parsed.components || [];
|
|
2990
|
+
const customData = parsed.customData || [];
|
|
2991
|
+
const cssVarCount = parsed.settings?.colors?.length || 0;
|
|
2992
|
+
const fontCount = parsed.settings?.fontFamily?.name ? 1 : 0;
|
|
2993
|
+
const customDataCount = customData.filter((cd) => cd.isRoot).length;
|
|
2994
|
+
const sectionCount = components.length;
|
|
2995
|
+
const summary = [
|
|
2996
|
+
`Wrote initial migration plan to ${targetPath}`,
|
|
2997
|
+
"",
|
|
2998
|
+
`**Summary:**`,
|
|
2999
|
+
`- Sections to migrate: ${sectionCount}`,
|
|
3000
|
+
`- CSS variables: ${cssVarCount}`,
|
|
3001
|
+
`- Fonts: ${fontCount}`,
|
|
3002
|
+
`- Custom data types (deferred decisions, not pre-migrated): ${customDataCount}`,
|
|
3003
|
+
"",
|
|
3004
|
+
`**Next steps for the LLM:**`,
|
|
3005
|
+
`1. Read \`${targetPath}\` start-to-finish. The "READ THIS FIRST" preamble explains your responsibilities.`,
|
|
3006
|
+
`2. Scan the old source directory (\`${old_source_dir || "<old-src>"}\`) for atomic components (Button, Input, Card, icons, etc.) NOT referenced from theme.json. Add them to \`## Source Code Analysis\` in MIGRATION.md.`,
|
|
3007
|
+
`3. Start the Foundation work (CSS variables, fonts, shared sub-components).`,
|
|
3008
|
+
`4. For each section, call \`get_section_migration_plan({theme_json_path, section_name, project_name})\`. The MCP will tell you which props need enum-vs-component decisions.`,
|
|
3009
|
+
`5. Log every custom-data decision in \`## Custom Data Decisions\`. Tick checkboxes as you finish.`,
|
|
3010
|
+
].join("\n");
|
|
3011
|
+
return { content: [{ type: "text", text: summary }] };
|
|
2608
3012
|
}
|
|
2609
3013
|
catch (err) {
|
|
2610
3014
|
return {
|
|
2611
|
-
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}
|
|
3015
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
2612
3016
|
};
|
|
2613
3017
|
}
|
|
2614
3018
|
});
|
|
2615
3019
|
// Tool: get_section_migration_plan
|
|
2616
|
-
server.tool("get_section_migration_plan", "
|
|
2617
|
-
theme_json: z.string().describe("Raw JSON content of the old theme.json"),
|
|
3020
|
+
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."),
|
|
2618
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')"),
|
|
2619
3024
|
project_name: z.string().optional().describe("Target new project name (must match what was used in plan_migration). Default: 'my-theme'"),
|
|
2620
3025
|
old_source_dir: z.string().optional().describe("Absolute path to old src/ directory (used to output exact source file paths to read)"),
|
|
2621
|
-
}, async ({ theme_json, section_name, project_name, old_source_dir }) => {
|
|
3026
|
+
}, async ({ theme_json, theme_json_path, section_name, project_name, old_source_dir }) => {
|
|
2622
3027
|
try {
|
|
2623
|
-
const parsed =
|
|
3028
|
+
const parsed = resolveThemeJson(theme_json, theme_json_path);
|
|
2624
3029
|
const projectName = project_name || "my-theme";
|
|
2625
3030
|
const plan = generateSectionMigrationPlan(parsed, section_name, projectName, old_source_dir);
|
|
2626
3031
|
return { content: [{ type: "text", text: plan }] };
|
|
2627
3032
|
}
|
|
2628
3033
|
catch (err) {
|
|
2629
3034
|
return {
|
|
2630
|
-
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}
|
|
3035
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
2631
3036
|
};
|
|
2632
3037
|
}
|
|
2633
3038
|
});
|