@ikas/code-components-mcp 1.4.0-beta.15 → 1.4.0-beta.17
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/storefront-api.json +1 -1
- package/data/storefront-types.json +1 -1
- package/dist/index.js +770 -232
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -98,9 +98,16 @@ function loadStorefrontTypes() {
|
|
|
98
98
|
}
|
|
99
99
|
return null;
|
|
100
100
|
}
|
|
101
|
-
const SUBTREE_KINDS = [
|
|
101
|
+
const SUBTREE_KINDS = [
|
|
102
|
+
"children",
|
|
103
|
+
"components",
|
|
104
|
+
"sub-components",
|
|
105
|
+
];
|
|
102
106
|
function normalizeName(value) {
|
|
103
|
-
return value
|
|
107
|
+
return value
|
|
108
|
+
.trim()
|
|
109
|
+
.replace(/^`+|`+$/g, "")
|
|
110
|
+
.trim();
|
|
104
111
|
}
|
|
105
112
|
const storefrontData = loadStorefrontData();
|
|
106
113
|
const frameworkData = loadJsonFile("../data/framework.json");
|
|
@@ -261,7 +268,11 @@ function searchMigrationTopics(query) {
|
|
|
261
268
|
const descScore = matchScore(topic.description, query) * 2;
|
|
262
269
|
const contentScore = matchScore(topic.content, query);
|
|
263
270
|
const tagScore = topic.tags.some((t) => matchScore(t, query) > 0) ? 5 : 0;
|
|
264
|
-
return {
|
|
271
|
+
return {
|
|
272
|
+
key,
|
|
273
|
+
topic,
|
|
274
|
+
score: titleScore + descScore + contentScore + tagScore,
|
|
275
|
+
};
|
|
265
276
|
})
|
|
266
277
|
.filter((item) => item.score > 0)
|
|
267
278
|
.sort((a, b) => b.score - a.score);
|
|
@@ -288,7 +299,7 @@ function analyzeOldTheme(themeJson) {
|
|
|
288
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`);
|
|
289
300
|
parts.push(`## Summary Statistics\n`);
|
|
290
301
|
parts.push(`- **Components:** ${components.length}`);
|
|
291
|
-
parts.push(`- **Custom Data Definitions:** ${customData.filter(cd => cd.isRoot).length}`);
|
|
302
|
+
parts.push(`- **Custom Data Definitions:** ${customData.filter((cd) => cd.isRoot).length}`);
|
|
292
303
|
parts.push(`- **Prop Groups:** ${groups.length}`);
|
|
293
304
|
// Component analysis
|
|
294
305
|
parts.push(`\n## Components (${components.length})\n`);
|
|
@@ -327,7 +338,11 @@ function analyzeOldTheme(themeJson) {
|
|
|
327
338
|
const typesSummary = Object.entries(propTypeCounts)
|
|
328
339
|
.map(([t, c]) => `${t}×${c}`)
|
|
329
340
|
.join(", ");
|
|
330
|
-
const headerFooter = comp.isHeader
|
|
341
|
+
const headerFooter = comp.isHeader
|
|
342
|
+
? " [HEADER]"
|
|
343
|
+
: comp.isFooter
|
|
344
|
+
? " [FOOTER]"
|
|
345
|
+
: "";
|
|
331
346
|
parts.push(`### ${comp.displayName || comp.dir || comp.id}${headerFooter}`);
|
|
332
347
|
parts.push(`- **Dir:** \`${comp.dir || "?"}\` | **Props:** ${props.length} (${typesSummary})`);
|
|
333
348
|
parts.push(`- **Recommended new type:** section`);
|
|
@@ -342,7 +357,7 @@ function analyzeOldTheme(themeJson) {
|
|
|
342
357
|
parts.push("");
|
|
343
358
|
}
|
|
344
359
|
// Custom data analysis
|
|
345
|
-
const rootCustomData = customData.filter(cd => cd.isRoot);
|
|
360
|
+
const rootCustomData = customData.filter((cd) => cd.isRoot);
|
|
346
361
|
if (rootCustomData.length > 0) {
|
|
347
362
|
parts.push(`\n## Custom Data Definitions (${rootCustomData.length})\n`);
|
|
348
363
|
for (const cd of rootCustomData) {
|
|
@@ -352,7 +367,9 @@ function analyzeOldTheme(themeJson) {
|
|
|
352
367
|
const describeNested = (items, indent) => {
|
|
353
368
|
const lines = [];
|
|
354
369
|
for (const item of items) {
|
|
355
|
-
const key = item.key
|
|
370
|
+
const key = item.key
|
|
371
|
+
? `\`${item.key}\``
|
|
372
|
+
: item.typescriptName || item.name || "unnamed";
|
|
356
373
|
lines.push(`${indent}- ${key}: ${item.type}${item.isRequired ? " (required)" : ""}`);
|
|
357
374
|
if (item.nestedData && item.nestedData.length > 0) {
|
|
358
375
|
lines.push(...describeNested(item.nestedData, indent + " "));
|
|
@@ -364,7 +381,7 @@ function analyzeOldTheme(themeJson) {
|
|
|
364
381
|
parts.push(...describeNested(cd.nestedData, " "));
|
|
365
382
|
}
|
|
366
383
|
if (cd.enumOptions && cd.enumOptions.length > 0) {
|
|
367
|
-
parts.push(`- **Enum options:** ${cd.enumOptions.map(o => `"${o.value}"`).join(", ")}`);
|
|
384
|
+
parts.push(`- **Enum options:** ${cd.enumOptions.map((o) => `"${o.value}"`).join(", ")}`);
|
|
368
385
|
}
|
|
369
386
|
// Find which components reference this customData
|
|
370
387
|
const referencingComponents = [];
|
|
@@ -446,7 +463,9 @@ function scanSharedSubcomponents(sourceDir) {
|
|
|
446
463
|
const walk = (dir) => {
|
|
447
464
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
448
465
|
if (entry.isDirectory()) {
|
|
449
|
-
if (entry.name === "node_modules" ||
|
|
466
|
+
if (entry.name === "node_modules" ||
|
|
467
|
+
entry.name === "__generated__" ||
|
|
468
|
+
entry.name.startsWith("."))
|
|
450
469
|
continue;
|
|
451
470
|
walk(path.join(dir, entry.name));
|
|
452
471
|
}
|
|
@@ -481,10 +500,14 @@ function scanSharedSubcomponents(sourceDir) {
|
|
|
481
500
|
if (!importPath.startsWith("."))
|
|
482
501
|
continue;
|
|
483
502
|
// Skip imports of generated types, utils, hooks
|
|
484
|
-
if (importPath.includes("__generated__") ||
|
|
503
|
+
if (importPath.includes("__generated__") ||
|
|
504
|
+
importPath.includes("/utils") ||
|
|
505
|
+
importPath.includes("/hooks"))
|
|
485
506
|
continue;
|
|
486
507
|
// Extract base name from path
|
|
487
|
-
const pathSegments = importPath
|
|
508
|
+
const pathSegments = importPath
|
|
509
|
+
.split("/")
|
|
510
|
+
.filter((s) => s && s !== "." && s !== "..");
|
|
488
511
|
if (pathSegments.length === 0)
|
|
489
512
|
continue;
|
|
490
513
|
const lastSegment = pathSegments[pathSegments.length - 1];
|
|
@@ -495,7 +518,10 @@ function scanSharedSubcomponents(sourceDir) {
|
|
|
495
518
|
continue;
|
|
496
519
|
seenInFile.add(key);
|
|
497
520
|
if (!importUsage.has(key)) {
|
|
498
|
-
importUsage.set(key, {
|
|
521
|
+
importUsage.set(key, {
|
|
522
|
+
usingComponents: new Set(),
|
|
523
|
+
rawImportPath: importPath,
|
|
524
|
+
});
|
|
499
525
|
}
|
|
500
526
|
importUsage.get(key).usingComponents.add(componentDir);
|
|
501
527
|
}
|
|
@@ -504,7 +530,7 @@ function scanSharedSubcomponents(sourceDir) {
|
|
|
504
530
|
const shared = [];
|
|
505
531
|
for (const [name, { usingComponents, rawImportPath }] of importUsage) {
|
|
506
532
|
// Don't flag the component itself (e.g., Navbar imports from ../Navbar/something)
|
|
507
|
-
const users = [...usingComponents].filter(c => c !== name);
|
|
533
|
+
const users = [...usingComponents].filter((c) => c !== name);
|
|
508
534
|
if (users.length >= 3) {
|
|
509
535
|
shared.push({ name, usedBy: users.sort(), importPaths: [rawImportPath] });
|
|
510
536
|
}
|
|
@@ -521,7 +547,7 @@ function toKebabCase(s) {
|
|
|
521
547
|
}
|
|
522
548
|
function classifyComplexity(comp, customDataMap) {
|
|
523
549
|
const props = comp.props || [];
|
|
524
|
-
const customCount = props.filter(p => p.type === "CUSTOM").length;
|
|
550
|
+
const customCount = props.filter((p) => p.type === "CUSTOM").length;
|
|
525
551
|
if (customCount === 0 && props.length < 10)
|
|
526
552
|
return "simple";
|
|
527
553
|
// Check for deeply nested CUSTOM (customData referencing another customData)
|
|
@@ -532,7 +558,9 @@ function classifyComplexity(comp, customDataMap) {
|
|
|
532
558
|
if (cd?.nestedData) {
|
|
533
559
|
const hasNested = (items) => {
|
|
534
560
|
for (const item of items) {
|
|
535
|
-
if (item.type === "DYNAMIC_LIST" ||
|
|
561
|
+
if (item.type === "DYNAMIC_LIST" ||
|
|
562
|
+
item.type === "STATIC_LIST" ||
|
|
563
|
+
item.customDataId)
|
|
536
564
|
return true;
|
|
537
565
|
if (item.nestedData && hasNested(item.nestedData))
|
|
538
566
|
return true;
|
|
@@ -564,7 +592,7 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
564
592
|
parts.push(`# Theme Migration Plan — \`${projectName}\``);
|
|
565
593
|
parts.push("");
|
|
566
594
|
parts.push(`**Generated:** ${new Date().toISOString().slice(0, 10)}`);
|
|
567
|
-
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`);
|
|
568
596
|
parts.push("");
|
|
569
597
|
parts.push(`> ## READ THIS FIRST`);
|
|
570
598
|
parts.push(`>`);
|
|
@@ -654,16 +682,22 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
654
682
|
const cdType = cd.type || "?";
|
|
655
683
|
let shape = "";
|
|
656
684
|
if (cd.type === "ENUM") {
|
|
657
|
-
const opts = (cd.enumOptions || [])
|
|
685
|
+
const opts = (cd.enumOptions || [])
|
|
686
|
+
.map((o) => o.value || o.displayName)
|
|
687
|
+
.filter(Boolean);
|
|
658
688
|
shape = ` — shape: \`enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}\``;
|
|
659
689
|
}
|
|
660
690
|
else if (cd.nestedData && cd.nestedData.length > 0) {
|
|
661
691
|
const first = cd.nestedData[0];
|
|
662
|
-
const fields = (first?.nestedData || cd.nestedData || [])
|
|
692
|
+
const fields = (first?.nestedData || cd.nestedData || [])
|
|
693
|
+
.map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`)
|
|
694
|
+
.slice(0, 8);
|
|
663
695
|
shape = ` — shape: \`{${fields.join(", ")}}\``;
|
|
664
696
|
}
|
|
665
697
|
const usedBy = cd.id ? usageByCustomDataId.get(cd.id) || [] : [];
|
|
666
|
-
const usedByStr = usedBy.length > 0
|
|
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)_`;
|
|
667
701
|
parts.push(`- \`${cdName}\` (${cdType})${shape}${usedByStr}`);
|
|
668
702
|
}
|
|
669
703
|
parts.push("");
|
|
@@ -697,7 +731,11 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
697
731
|
const oldName = comp.displayName || comp.dir || comp.id || "Unknown";
|
|
698
732
|
const kebabName = toKebabCase(comp.dir || comp.displayName || comp.id || "unknown");
|
|
699
733
|
const newId = `${projectName}-${kebabName}`;
|
|
700
|
-
const headerFooter = comp.isHeader
|
|
734
|
+
const headerFooter = comp.isHeader
|
|
735
|
+
? " **[HEADER]**"
|
|
736
|
+
: comp.isFooter
|
|
737
|
+
? " **[FOOTER]**"
|
|
738
|
+
: "";
|
|
701
739
|
const propCount = (comp.props || []).length;
|
|
702
740
|
// Detect children from CUSTOM DYNAMIC_LIST props
|
|
703
741
|
const children = [];
|
|
@@ -707,8 +745,13 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
707
745
|
if (cd && (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST")) {
|
|
708
746
|
const itemObj = cd.nestedData?.[0];
|
|
709
747
|
if (itemObj) {
|
|
710
|
-
const childName = itemObj.typescriptName ||
|
|
711
|
-
|
|
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);
|
|
712
755
|
children.push({ propName: p.name || "?", childName, fields });
|
|
713
756
|
}
|
|
714
757
|
}
|
|
@@ -767,11 +810,25 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
767
810
|
}
|
|
768
811
|
// Known libraries we detect in old themes and want to flag for replacement
|
|
769
812
|
const KNOWN_LIBRARIES = [
|
|
770
|
-
"swiper",
|
|
771
|
-
"react
|
|
772
|
-
"react
|
|
773
|
-
"
|
|
774
|
-
"
|
|
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",
|
|
775
832
|
];
|
|
776
833
|
// Heuristic: member-access patterns on old storefront stores/singletons that likely need new-system equivalents
|
|
777
834
|
const OLD_STOREFRONT_CALL_REGEX = /\b(customerStore|cartStore|productStore|categoryStore|orderStore|searchStore|favoritesStore|i18nStore|Router|useStore)\.\w+/g;
|
|
@@ -790,7 +847,8 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
790
847
|
// Collect all .tsx/.ts files in the component dir
|
|
791
848
|
try {
|
|
792
849
|
for (const entry of fs.readdirSync(componentDir, { withFileTypes: true })) {
|
|
793
|
-
if (entry.isFile() &&
|
|
850
|
+
if (entry.isFile() &&
|
|
851
|
+
(entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
|
|
794
852
|
result.sourceFiles.push(path.join(componentDir, entry.name));
|
|
795
853
|
}
|
|
796
854
|
}
|
|
@@ -807,7 +865,18 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
807
865
|
const subCompSet = new Map();
|
|
808
866
|
const callSet = new Set();
|
|
809
867
|
// Packages we treat as "framework" and don't flag for replacement (but do note in reactPackageUsage)
|
|
810
|
-
const REACT_PACKAGES = new Set([
|
|
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
|
+
]);
|
|
811
880
|
for (const file of result.sourceFiles) {
|
|
812
881
|
let content;
|
|
813
882
|
try {
|
|
@@ -821,7 +890,7 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
821
890
|
while ((m = importRegex.exec(content)) !== null) {
|
|
822
891
|
const p = m[1];
|
|
823
892
|
if (p.startsWith(".")) {
|
|
824
|
-
const segs = p.split("/").filter(s => s && s !== "." && s !== "..");
|
|
893
|
+
const segs = p.split("/").filter((s) => s && s !== "." && s !== "..");
|
|
825
894
|
const last = segs[segs.length - 1];
|
|
826
895
|
if (last && /^[A-Z]/.test(last) && !last.includes("__generated__")) {
|
|
827
896
|
subCompSet.set(last, p);
|
|
@@ -829,7 +898,9 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
829
898
|
}
|
|
830
899
|
else if (!p.startsWith("@ikas/")) {
|
|
831
900
|
// Classify: known library, react-family, or unknown-external
|
|
832
|
-
const base = p.startsWith("@")
|
|
901
|
+
const base = p.startsWith("@")
|
|
902
|
+
? p.split("/").slice(0, 2).join("/")
|
|
903
|
+
: p.split("/")[0];
|
|
833
904
|
if (REACT_PACKAGES.has(p) || REACT_PACKAGES.has(base)) {
|
|
834
905
|
reactSet.add(base);
|
|
835
906
|
}
|
|
@@ -892,7 +963,10 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
892
963
|
}
|
|
893
964
|
}
|
|
894
965
|
}
|
|
895
|
-
result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({
|
|
966
|
+
result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({
|
|
967
|
+
name,
|
|
968
|
+
path: p,
|
|
969
|
+
}));
|
|
896
970
|
result.importedLibraries = [...libSet].sort();
|
|
897
971
|
result.importedUnknownLibraries = [...unknownLibSet].sort();
|
|
898
972
|
result.reactPackageUsage = [...reactSet].sort();
|
|
@@ -908,27 +982,38 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
908
982
|
customDataMap.set(cd.id, cd);
|
|
909
983
|
}
|
|
910
984
|
// Find the component — try match by dir, displayName, id, or new-id
|
|
911
|
-
const target = components.find(c => {
|
|
985
|
+
const target = components.find((c) => {
|
|
912
986
|
if (!c)
|
|
913
987
|
return false;
|
|
914
|
-
if (c.dir === sectionName ||
|
|
988
|
+
if (c.dir === sectionName ||
|
|
989
|
+
c.displayName === sectionName ||
|
|
990
|
+
c.id === sectionName)
|
|
915
991
|
return true;
|
|
916
992
|
const kebab = toKebabCase(c.dir || c.displayName || c.id || "");
|
|
917
993
|
const newId = `${projectName}-${kebab}`;
|
|
918
994
|
return newId === sectionName;
|
|
919
995
|
});
|
|
920
996
|
if (!target) {
|
|
921
|
-
const available = components
|
|
997
|
+
const available = components
|
|
998
|
+
.map((c) => c.dir || c.displayName || c.id)
|
|
999
|
+
.filter(Boolean)
|
|
1000
|
+
.join(", ");
|
|
922
1001
|
return `Section "${sectionName}" not found in theme. Available: ${available}`;
|
|
923
1002
|
}
|
|
924
1003
|
const parts = [];
|
|
925
1004
|
const oldName = target.displayName || target.dir || target.id || "Unknown";
|
|
926
1005
|
const kebabName = toKebabCase(target.dir || target.displayName || target.id || "unknown");
|
|
927
1006
|
const sectionId = `${projectName}-${kebabName}`;
|
|
928
|
-
const sectionPascal = (target.dir || target.displayName || "").replace(/[^a-zA-Z0-9]/g, "") ||
|
|
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("");
|
|
929
1012
|
// Scan the old source for imports, libraries, field usage
|
|
930
|
-
const propNames = (target.props || [])
|
|
931
|
-
|
|
1013
|
+
const propNames = (target.props || [])
|
|
1014
|
+
.map((p) => p.name || "")
|
|
1015
|
+
.filter(Boolean);
|
|
1016
|
+
const sourceScan = oldSourceDir && target.dir
|
|
932
1017
|
? scanSectionSource(path.join(oldSourceDir, target.dir), propNames)
|
|
933
1018
|
: null;
|
|
934
1019
|
parts.push(`# Section Migration Plan: ${oldName}`);
|
|
@@ -974,7 +1059,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
974
1059
|
}
|
|
975
1060
|
if (sourceScan.reactPackageUsage.length > 0) {
|
|
976
1061
|
parts.push("");
|
|
977
|
-
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).`);
|
|
978
1063
|
}
|
|
979
1064
|
if (sourceScan.oldStorefrontCalls.length > 0) {
|
|
980
1065
|
parts.push("");
|
|
@@ -1020,7 +1105,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1020
1105
|
if (oldType === "SLIDER") {
|
|
1021
1106
|
newType = "NUMBER";
|
|
1022
1107
|
notes = `Was SLIDER(min=${p.sliderData?.min}, max=${p.sliderData?.max}) — replace \`.value\` access with direct number`;
|
|
1023
|
-
const prop = {
|
|
1108
|
+
const prop = {
|
|
1109
|
+
name: newName,
|
|
1110
|
+
displayName: p.displayName || newName,
|
|
1111
|
+
type: "NUMBER",
|
|
1112
|
+
};
|
|
1024
1113
|
if (p.isRequired)
|
|
1025
1114
|
prop.required = true;
|
|
1026
1115
|
parentPropsJson.push(prop);
|
|
@@ -1028,7 +1117,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1028
1117
|
else if (oldType === "PRODUCT_DETAIL") {
|
|
1029
1118
|
newType = "PRODUCT";
|
|
1030
1119
|
notes = "Renamed — PRODUCT_DETAIL → PRODUCT";
|
|
1031
|
-
const prop = {
|
|
1120
|
+
const prop = {
|
|
1121
|
+
name: newName,
|
|
1122
|
+
displayName: p.displayName || newName,
|
|
1123
|
+
type: "PRODUCT",
|
|
1124
|
+
};
|
|
1032
1125
|
if (p.isRequired)
|
|
1033
1126
|
prop.required = true;
|
|
1034
1127
|
parentPropsJson.push(prop);
|
|
@@ -1039,7 +1132,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1039
1132
|
if (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST") {
|
|
1040
1133
|
// Child component needed
|
|
1041
1134
|
const itemObj = cd.nestedData?.[0];
|
|
1042
|
-
const childName = itemObj?.typescriptName ||
|
|
1135
|
+
const childName = itemObj?.typescriptName ||
|
|
1136
|
+
(itemObj?.name
|
|
1137
|
+
? itemObj.name.replace(/[^a-zA-Z0-9]/g, "")
|
|
1138
|
+
: `${sectionPascal}Item`);
|
|
1043
1139
|
const childProps = [];
|
|
1044
1140
|
const nestedWarnings = [];
|
|
1045
1141
|
for (const f of (itemObj?.nestedData || [])) {
|
|
@@ -1050,11 +1146,18 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1050
1146
|
fType = "NUMBER";
|
|
1051
1147
|
else if (fType === "PRODUCT_DETAIL")
|
|
1052
1148
|
fType = "PRODUCT";
|
|
1053
|
-
else if (fType === "CUSTOM" ||
|
|
1149
|
+
else if (fType === "CUSTOM" ||
|
|
1150
|
+
fType === "DYNAMIC_LIST" ||
|
|
1151
|
+
fType === "STATIC_LIST" ||
|
|
1152
|
+
fType === "OBJECT") {
|
|
1054
1153
|
nestedWarnings.push(`\`${f.key}\` (${fType})`);
|
|
1055
1154
|
fType = "COMPONENT_LIST";
|
|
1056
1155
|
}
|
|
1057
|
-
const prop = {
|
|
1156
|
+
const prop = {
|
|
1157
|
+
name: f.key,
|
|
1158
|
+
displayName: f.name || f.key,
|
|
1159
|
+
type: fType,
|
|
1160
|
+
};
|
|
1058
1161
|
if (f.isRequired)
|
|
1059
1162
|
prop.required = true;
|
|
1060
1163
|
childProps.push(prop);
|
|
@@ -1066,10 +1169,14 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1066
1169
|
if (childProps.length === 0 && sourceScan?.propFieldUsage[oldName]) {
|
|
1067
1170
|
const inferred = sourceScan.propFieldUsage[oldName];
|
|
1068
1171
|
for (const fieldName of inferred) {
|
|
1069
|
-
childProps.push({
|
|
1172
|
+
childProps.push({
|
|
1173
|
+
name: fieldName,
|
|
1174
|
+
displayName: fieldName,
|
|
1175
|
+
type: "TEXT",
|
|
1176
|
+
});
|
|
1070
1177
|
}
|
|
1071
1178
|
if (inferred.length > 0) {
|
|
1072
|
-
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.`;
|
|
1073
1180
|
}
|
|
1074
1181
|
}
|
|
1075
1182
|
children.push({
|
|
@@ -1098,7 +1205,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1098
1205
|
let fType = f.type;
|
|
1099
1206
|
if (fType === "SLIDER")
|
|
1100
1207
|
fType = "NUMBER";
|
|
1101
|
-
const prop = {
|
|
1208
|
+
const prop = {
|
|
1209
|
+
name: f.key,
|
|
1210
|
+
displayName: f.name || f.key,
|
|
1211
|
+
type: fType,
|
|
1212
|
+
};
|
|
1102
1213
|
if (f.isRequired)
|
|
1103
1214
|
prop.required = true;
|
|
1104
1215
|
parentPropsJson.push(prop);
|
|
@@ -1113,7 +1224,8 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1113
1224
|
}
|
|
1114
1225
|
else if (cd.type === "ENUM") {
|
|
1115
1226
|
newType = "ENUM";
|
|
1116
|
-
const enumName = cd.typescriptName ||
|
|
1227
|
+
const enumName = cd.typescriptName ||
|
|
1228
|
+
(cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
|
|
1117
1229
|
const options = (cd.enumOptions || []).reduce((acc, o) => {
|
|
1118
1230
|
if (o.displayName && o.value)
|
|
1119
1231
|
acc[o.displayName] = o.value;
|
|
@@ -1121,7 +1233,12 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1121
1233
|
}, {});
|
|
1122
1234
|
enumsNeeded.push({ name: enumName, options });
|
|
1123
1235
|
notes = `Was CUSTOM (ENUM) — create enum \`${enumName}\` via \`config add-enum\` first, then reference its enumId here`;
|
|
1124
|
-
const prop = {
|
|
1236
|
+
const prop = {
|
|
1237
|
+
name: newName,
|
|
1238
|
+
displayName: p.displayName || newName,
|
|
1239
|
+
type: "ENUM",
|
|
1240
|
+
enumTypeId: `<ENUM_ID_FROM_add-enum_${enumName}>`,
|
|
1241
|
+
};
|
|
1125
1242
|
if (p.isRequired)
|
|
1126
1243
|
prop.required = true;
|
|
1127
1244
|
parentPropsJson.push(prop);
|
|
@@ -1130,7 +1247,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1130
1247
|
}
|
|
1131
1248
|
else {
|
|
1132
1249
|
// Direct mapping
|
|
1133
|
-
const prop = {
|
|
1250
|
+
const prop = {
|
|
1251
|
+
name: newName,
|
|
1252
|
+
displayName: p.displayName || newName,
|
|
1253
|
+
type: newType,
|
|
1254
|
+
};
|
|
1134
1255
|
if (p.isRequired)
|
|
1135
1256
|
prop.required = true;
|
|
1136
1257
|
parentPropsJson.push(prop);
|
|
@@ -1139,7 +1260,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1139
1260
|
}
|
|
1140
1261
|
parts.push("");
|
|
1141
1262
|
// Custom Data Decision Callouts — per prop referencing a customData type
|
|
1142
|
-
const customDataPropsForCallouts = (target.props || []).filter((p) => p.type === "CUSTOM" &&
|
|
1263
|
+
const customDataPropsForCallouts = (target.props || []).filter((p) => p.type === "CUSTOM" &&
|
|
1264
|
+
p.customDataId &&
|
|
1265
|
+
customDataMap.has(p.customDataId));
|
|
1143
1266
|
if (customDataPropsForCallouts.length > 0) {
|
|
1144
1267
|
parts.push(`## Custom Data Decisions to Make`);
|
|
1145
1268
|
parts.push("");
|
|
@@ -1154,7 +1277,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1154
1277
|
let shape = "";
|
|
1155
1278
|
let shapeKind = "unknown";
|
|
1156
1279
|
if (cd.type === "ENUM") {
|
|
1157
|
-
const opts = (cd.enumOptions || [])
|
|
1280
|
+
const opts = (cd.enumOptions || [])
|
|
1281
|
+
.map((o) => o.value || o.displayName)
|
|
1282
|
+
.filter(Boolean);
|
|
1158
1283
|
shape = `enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}`;
|
|
1159
1284
|
shapeKind = "enum";
|
|
1160
1285
|
}
|
|
@@ -1187,7 +1312,8 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1187
1312
|
if (shapeKind === "enum") {
|
|
1188
1313
|
parts.push(`**Default: enum prop.** Flat scalar set; use \`config add-enum\`.`);
|
|
1189
1314
|
parts.push("");
|
|
1190
|
-
const enumName = cd.typescriptName ||
|
|
1315
|
+
const enumName = cd.typescriptName ||
|
|
1316
|
+
(cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
|
|
1191
1317
|
const enumOptions = (cd.enumOptions || []).reduce((acc, o) => {
|
|
1192
1318
|
if (o.displayName && o.value)
|
|
1193
1319
|
acc[o.displayName] = o.value;
|
|
@@ -1222,7 +1348,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1222
1348
|
parts.push(`> See \`get_migration_guide("component-composition-decision-guide")\` for when \`COMPONENT_LIST\` is overkill.`);
|
|
1223
1349
|
parts.push("");
|
|
1224
1350
|
}
|
|
1225
|
-
const compName = cd.typescriptName ||
|
|
1351
|
+
const compName = cd.typescriptName ||
|
|
1352
|
+
(cd.name
|
|
1353
|
+
? cd.name.replace(/[^a-zA-Z0-9]/g, "")
|
|
1354
|
+
: `${sectionPascal}Item`);
|
|
1226
1355
|
const compPropsForCli = [];
|
|
1227
1356
|
for (const f of fieldSource) {
|
|
1228
1357
|
if (!f.key)
|
|
@@ -1232,7 +1361,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1232
1361
|
fType = "NUMBER";
|
|
1233
1362
|
else if (fType === "PRODUCT_DETAIL")
|
|
1234
1363
|
fType = "PRODUCT";
|
|
1235
|
-
compPropsForCli.push({
|
|
1364
|
+
compPropsForCli.push({
|
|
1365
|
+
name: f.key,
|
|
1366
|
+
displayName: f.name || f.key,
|
|
1367
|
+
type: fType,
|
|
1368
|
+
});
|
|
1236
1369
|
}
|
|
1237
1370
|
parts.push("```bash");
|
|
1238
1371
|
if (isMinimal) {
|
|
@@ -1271,7 +1404,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1271
1404
|
existing.usedByProps.push(ch.propName);
|
|
1272
1405
|
}
|
|
1273
1406
|
else {
|
|
1274
|
-
uniqueChildren.set(ch.childName, {
|
|
1407
|
+
uniqueChildren.set(ch.childName, {
|
|
1408
|
+
child: ch,
|
|
1409
|
+
usedByProps: [ch.propName],
|
|
1410
|
+
});
|
|
1275
1411
|
}
|
|
1276
1412
|
}
|
|
1277
1413
|
if (uniqueChildren.size > 0) {
|
|
@@ -1284,7 +1420,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1284
1420
|
for (const { child: ch, usedByProps } of uniqueChildren.values()) {
|
|
1285
1421
|
parts.push(`### \`${ch.childName}\``);
|
|
1286
1422
|
const propsLabel = usedByProps.length > 1
|
|
1287
|
-
? `Used by parent props: ${usedByProps.map(p => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
|
|
1423
|
+
? `Used by parent props: ${usedByProps.map((p) => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
|
|
1288
1424
|
: `For parent prop: \`${usedByProps[0]}\``;
|
|
1289
1425
|
parts.push(propsLabel);
|
|
1290
1426
|
parts.push(`Old customData: "${ch.customDataName}"`);
|
|
@@ -1330,12 +1466,15 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1330
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.`);
|
|
1331
1467
|
}
|
|
1332
1468
|
// Check if the section itself has data-driven list props (PRODUCT_LIST, BLOG_LIST, CATEGORY_LIST)
|
|
1333
|
-
const dataListProps = (target.props || []).filter(p => p.type === "PRODUCT_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");
|
|
1334
1473
|
if (dataListProps.length > 0) {
|
|
1335
1474
|
parts.push("");
|
|
1336
1475
|
parts.push(`### Data-Driven List Rendering`);
|
|
1337
1476
|
parts.push("");
|
|
1338
|
-
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(", ")}.`);
|
|
1339
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:`);
|
|
1340
1479
|
parts.push("");
|
|
1341
1480
|
parts.push("```tsx");
|
|
@@ -1362,9 +1501,22 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1362
1501
|
parts.push(`See \`get_migration_guide("custom-data-conversion")\` → "Two Ways to Render Lists" for the full pattern.`);
|
|
1363
1502
|
}
|
|
1364
1503
|
// Detect form-page sections (0 or few props, name suggests a form/auth page)
|
|
1365
|
-
const formKeywords = [
|
|
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
|
+
];
|
|
1366
1518
|
const lowerDir = (target.dir || "").toLowerCase();
|
|
1367
|
-
const isLikelyFormPage = formKeywords.some(kw => lowerDir.includes(kw));
|
|
1519
|
+
const isLikelyFormPage = formKeywords.some((kw) => lowerDir.includes(kw));
|
|
1368
1520
|
if (isLikelyFormPage) {
|
|
1369
1521
|
parts.push("");
|
|
1370
1522
|
parts.push(`### Form Page Pattern`);
|
|
@@ -1393,7 +1545,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1393
1545
|
// Fallback heuristic only when source scan unavailable
|
|
1394
1546
|
const heuristicLibs = [];
|
|
1395
1547
|
const lowerName = oldName.toLowerCase();
|
|
1396
|
-
if (lowerName.includes("slider") ||
|
|
1548
|
+
if (lowerName.includes("slider") ||
|
|
1549
|
+
lowerName.includes("carousel") ||
|
|
1550
|
+
lowerName.includes("banner")) {
|
|
1397
1551
|
heuristicLibs.push("swiper");
|
|
1398
1552
|
}
|
|
1399
1553
|
if (lowerName.includes("marquee"))
|
|
@@ -1402,7 +1556,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1402
1556
|
heuristicLibs.push("react-player");
|
|
1403
1557
|
if (lowerName.includes("chart"))
|
|
1404
1558
|
heuristicLibs.push("recharts");
|
|
1405
|
-
if (lowerName.includes("star") ||
|
|
1559
|
+
if (lowerName.includes("star") ||
|
|
1560
|
+
lowerName.includes("rating") ||
|
|
1561
|
+
lowerName.includes("review"))
|
|
1406
1562
|
heuristicLibs.push("react-simple-star-rating");
|
|
1407
1563
|
if (heuristicLibs.length > 0) {
|
|
1408
1564
|
parts.push(`### Likely Library Replacements (heuristic — source not scanned)`);
|
|
@@ -1474,8 +1630,7 @@ function levenshtein(a, b) {
|
|
|
1474
1630
|
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
1475
1631
|
curr[j] = Math.min(curr[j - 1] + 1, // insertion
|
|
1476
1632
|
prev[j] + 1, // deletion
|
|
1477
|
-
prev[j - 1] + cost
|
|
1478
|
-
);
|
|
1633
|
+
prev[j - 1] + cost);
|
|
1479
1634
|
}
|
|
1480
1635
|
for (let j = 0; j <= n; j++)
|
|
1481
1636
|
prev[j] = curr[j];
|
|
@@ -1506,17 +1661,29 @@ function searchFunctions(query) {
|
|
|
1506
1661
|
const scored = storefrontData.functions
|
|
1507
1662
|
.map((fn) => {
|
|
1508
1663
|
const nameScore = matchScore(fn.name, query) * 3;
|
|
1509
|
-
const displayNameScore = fn.displayName
|
|
1664
|
+
const displayNameScore = fn.displayName
|
|
1665
|
+
? matchScore(fn.displayName, query) * 3
|
|
1666
|
+
: 0;
|
|
1510
1667
|
const descScore = matchScore(fn.description, query);
|
|
1511
|
-
const catScore = fn.categories.some((c) => matchScore(c, query) > 0)
|
|
1668
|
+
const catScore = fn.categories.some((c) => matchScore(c, query) > 0)
|
|
1669
|
+
? 5
|
|
1670
|
+
: 0;
|
|
1512
1671
|
const paramScore = fn.params.some((p) => matchScore(p.name, query) > 0 || matchScore(p.description, query) > 0)
|
|
1513
1672
|
? 2
|
|
1514
1673
|
: 0;
|
|
1515
1674
|
const sigScore = matchScore(fn.signature, query) * 2;
|
|
1516
|
-
const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0)
|
|
1675
|
+
const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0)
|
|
1676
|
+
? 8
|
|
1677
|
+
: 0;
|
|
1517
1678
|
return {
|
|
1518
1679
|
fn,
|
|
1519
|
-
score: nameScore +
|
|
1680
|
+
score: nameScore +
|
|
1681
|
+
displayNameScore +
|
|
1682
|
+
descScore +
|
|
1683
|
+
catScore +
|
|
1684
|
+
paramScore +
|
|
1685
|
+
sigScore +
|
|
1686
|
+
typeScore,
|
|
1520
1687
|
};
|
|
1521
1688
|
})
|
|
1522
1689
|
.filter((item) => item.score > 0)
|
|
@@ -1530,7 +1697,11 @@ function searchFrameworkTopics(query) {
|
|
|
1530
1697
|
const descScore = matchScore(topic.description, query) * 2;
|
|
1531
1698
|
const contentScore = matchScore(topic.content, query);
|
|
1532
1699
|
const tagScore = topic.tags.some((t) => matchScore(t, query) > 0) ? 5 : 0;
|
|
1533
|
-
return {
|
|
1700
|
+
return {
|
|
1701
|
+
key,
|
|
1702
|
+
topic,
|
|
1703
|
+
score: titleScore + descScore + contentScore + tagScore,
|
|
1704
|
+
};
|
|
1534
1705
|
})
|
|
1535
1706
|
.filter((item) => item.score > 0)
|
|
1536
1707
|
.sort((a, b) => b.score - a.score);
|
|
@@ -1545,7 +1716,9 @@ function searchTypes(query) {
|
|
|
1545
1716
|
const propScore = td.properties?.some((p) => matchScore(p.name, query) > 0 || matchScore(p.type, query) > 0)
|
|
1546
1717
|
? 4
|
|
1547
1718
|
: 0;
|
|
1548
|
-
const enumScore = td.enumValues?.some((v) => matchScore(v, query) > 0)
|
|
1719
|
+
const enumScore = td.enumValues?.some((v) => matchScore(v, query) > 0)
|
|
1720
|
+
? 4
|
|
1721
|
+
: 0;
|
|
1549
1722
|
return { td, score: nameScore + domainScore + propScore + enumScore };
|
|
1550
1723
|
})
|
|
1551
1724
|
.filter((item) => item.score > 0)
|
|
@@ -1587,8 +1760,12 @@ function formatFunctionDoc(fn) {
|
|
|
1587
1760
|
return lines.join("\n");
|
|
1588
1761
|
}
|
|
1589
1762
|
function formatFunctionSummary(fn) {
|
|
1590
|
-
const desc = fn.description
|
|
1591
|
-
|
|
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
|
+
: "";
|
|
1592
1769
|
return `- \`${fn.name}\`${alias} - ${desc}`;
|
|
1593
1770
|
}
|
|
1594
1771
|
function formatTypeDefinition(td, opts = {}) {
|
|
@@ -1629,7 +1806,9 @@ function formatTypeDefinition(td, opts = {}) {
|
|
|
1629
1806
|
const fn = storefrontData.functions.find((f) => f.name === fnName);
|
|
1630
1807
|
if (fn) {
|
|
1631
1808
|
const desc = fn.description ? fn.description.split(".")[0] + "." : "";
|
|
1632
|
-
const alias = fn.displayName && fn.displayName !== fn.name
|
|
1809
|
+
const alias = fn.displayName && fn.displayName !== fn.name
|
|
1810
|
+
? ` (alias: ${fn.displayName})`
|
|
1811
|
+
: "";
|
|
1633
1812
|
lines.push(`- **\`${fn.name}\`**${alias} — ${desc}`);
|
|
1634
1813
|
lines.push(` \`${fn.signature}\``);
|
|
1635
1814
|
}
|
|
@@ -1638,7 +1817,9 @@ function formatTypeDefinition(td, opts = {}) {
|
|
|
1638
1817
|
}
|
|
1639
1818
|
}
|
|
1640
1819
|
lines.push("");
|
|
1641
|
-
lines.push(
|
|
1820
|
+
lines.push('Use `get_functions_for_type("' +
|
|
1821
|
+
td.name +
|
|
1822
|
+
'")` for full documentation of these functions.');
|
|
1642
1823
|
}
|
|
1643
1824
|
return lines.join("\n");
|
|
1644
1825
|
}
|
|
@@ -1656,7 +1837,9 @@ const server = new McpServer({
|
|
|
1656
1837
|
name: "ikas-code-components",
|
|
1657
1838
|
version: "0.1.0",
|
|
1658
1839
|
}, {
|
|
1659
|
-
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 a section on a page; `list_page_sections` → the sections already placed on a page, each with its per-placement `elementId`, current `propValues`, and blueprint `props`; `update_section_prop` → change a single prop value of a placed section.\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.",
|
|
1660
1843
|
});
|
|
1661
1844
|
// Tool: search_docs
|
|
1662
1845
|
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 }) => {
|
|
@@ -1697,13 +1880,20 @@ server.tool("search_docs", "Search across all ikas storefront API docs, framewor
|
|
|
1697
1880
|
parts.push("");
|
|
1698
1881
|
parts.push("Use `get_migration_guide(topic)` to get full content for any migration topic.");
|
|
1699
1882
|
}
|
|
1700
|
-
if (functions.length === 0 &&
|
|
1883
|
+
if (functions.length === 0 &&
|
|
1884
|
+
topics.length === 0 &&
|
|
1885
|
+
types.length === 0 &&
|
|
1886
|
+
migrationTopics.length === 0) {
|
|
1701
1887
|
parts.push(`No results found for "${query}". Try different keywords or use \`list_functions()\` to see all available functions.`);
|
|
1702
1888
|
}
|
|
1703
1889
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1704
1890
|
});
|
|
1705
1891
|
// Tool: get_function_doc
|
|
1706
|
-
server.tool("get_function_doc", "Get full documentation for a specific storefront API function including signature, parameters, return type, and example.", {
|
|
1892
|
+
server.tool("get_function_doc", "Get full documentation for a specific storefront API function including signature, parameters, return type, and example.", {
|
|
1893
|
+
name: z
|
|
1894
|
+
.string()
|
|
1895
|
+
.describe("Function name (e.g. 'addItemToCart', 'Router.navigate')"),
|
|
1896
|
+
}, async ({ name }) => {
|
|
1707
1897
|
const nameLower = name.toLowerCase();
|
|
1708
1898
|
// Phase 1: canonical-name match wins. A real function name always outranks
|
|
1709
1899
|
// any displayName alias so aliases can never shadow the function they're
|
|
@@ -1711,7 +1901,9 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1711
1901
|
// [BP-DISPLAY-NAME: hasCustomer] alias).
|
|
1712
1902
|
const byName = storefrontData.functions.find((f) => f.name.toLowerCase() === nameLower);
|
|
1713
1903
|
if (byName) {
|
|
1714
|
-
return {
|
|
1904
|
+
return {
|
|
1905
|
+
content: [{ type: "text", text: formatFunctionDoc(byName) }],
|
|
1906
|
+
};
|
|
1715
1907
|
}
|
|
1716
1908
|
// Phase 2: fall back to displayName aliases.
|
|
1717
1909
|
const byAlias = storefrontData.functions.filter((f) => f.displayName && f.displayName.toLowerCase() === nameLower);
|
|
@@ -1719,7 +1911,9 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1719
1911
|
const fn = byAlias[0];
|
|
1720
1912
|
const note = `> Note: "${name}" is a display alias for \`${fn.name}\`.\n\n`;
|
|
1721
1913
|
return {
|
|
1722
|
-
content: [
|
|
1914
|
+
content: [
|
|
1915
|
+
{ type: "text", text: note + formatFunctionDoc(fn) },
|
|
1916
|
+
],
|
|
1723
1917
|
};
|
|
1724
1918
|
}
|
|
1725
1919
|
if (byAlias.length > 1) {
|
|
@@ -1737,7 +1931,9 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1737
1931
|
(f.displayName && f.displayName.toLowerCase().includes(nameLower)));
|
|
1738
1932
|
if (matches.length > 0) {
|
|
1739
1933
|
const suggestions = matches.slice(0, 5).map((f) => {
|
|
1740
|
-
const alias = f.displayName && f.displayName !== f.name
|
|
1934
|
+
const alias = f.displayName && f.displayName !== f.name
|
|
1935
|
+
? ` (alias: ${f.displayName})`
|
|
1936
|
+
: "";
|
|
1741
1937
|
return ` - ${f.name}${alias}`;
|
|
1742
1938
|
});
|
|
1743
1939
|
return {
|
|
@@ -1750,7 +1946,12 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1750
1946
|
};
|
|
1751
1947
|
}
|
|
1752
1948
|
return {
|
|
1753
|
-
content: [
|
|
1949
|
+
content: [
|
|
1950
|
+
{
|
|
1951
|
+
type: "text",
|
|
1952
|
+
text: `Function "${name}" not found. Use \`list_functions()\` to see all available functions.`,
|
|
1953
|
+
},
|
|
1954
|
+
],
|
|
1754
1955
|
};
|
|
1755
1956
|
});
|
|
1756
1957
|
// Tool: list_functions
|
|
@@ -1800,7 +2001,7 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
|
|
|
1800
2001
|
if (uncategorized > 0) {
|
|
1801
2002
|
lines.push(`- \`Other\` (${uncategorized})`);
|
|
1802
2003
|
}
|
|
1803
|
-
lines.push("",
|
|
2004
|
+
lines.push("", 'Call `list_functions(category: "<name>")` to see one-line summaries for a category.');
|
|
1804
2005
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1805
2006
|
}
|
|
1806
2007
|
const catLower = category.toLowerCase();
|
|
@@ -1832,7 +2033,11 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
|
|
|
1832
2033
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1833
2034
|
});
|
|
1834
2035
|
// Tool: get_code_example
|
|
1835
|
-
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.", {
|
|
2036
|
+
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.", {
|
|
2037
|
+
task: z
|
|
2038
|
+
.string()
|
|
2039
|
+
.describe("Task description or example ID (call `list_examples()` for the full list)"),
|
|
2040
|
+
}, async ({ task }) => {
|
|
1836
2041
|
const taskLower = task.toLowerCase();
|
|
1837
2042
|
// Try exact ID match first
|
|
1838
2043
|
let example = storefrontData.codeExamples.find((e) => e.id === taskLower);
|
|
@@ -1852,7 +2057,9 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
1852
2057
|
}
|
|
1853
2058
|
}
|
|
1854
2059
|
if (!example) {
|
|
1855
|
-
const available = storefrontData.codeExamples
|
|
2060
|
+
const available = storefrontData.codeExamples
|
|
2061
|
+
.map((e) => ` - \`${e.id}\` - ${e.title}`)
|
|
2062
|
+
.join("\n");
|
|
1856
2063
|
return {
|
|
1857
2064
|
content: [
|
|
1858
2065
|
{
|
|
@@ -1875,14 +2082,24 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
1875
2082
|
if (example.files && example.files.length > 0) {
|
|
1876
2083
|
for (const file of example.files) {
|
|
1877
2084
|
const ext = file.filename.split(".").pop() || "text";
|
|
1878
|
-
const lang = ext === "tsx" || ext === "ts"
|
|
2085
|
+
const lang = ext === "tsx" || ext === "ts"
|
|
2086
|
+
? "typescript"
|
|
2087
|
+
: ext === "css"
|
|
2088
|
+
? "css"
|
|
2089
|
+
: ext === "json"
|
|
2090
|
+
? "json"
|
|
2091
|
+
: "text";
|
|
1879
2092
|
// Add inline originality comments to CSS and TSX files
|
|
1880
2093
|
let content = file.content;
|
|
1881
2094
|
if (ext === "css") {
|
|
1882
|
-
content =
|
|
2095
|
+
content =
|
|
2096
|
+
"/* EXAMPLE STYLING — create your own original CSS with different class names and design */\n" +
|
|
2097
|
+
content;
|
|
1883
2098
|
}
|
|
1884
2099
|
else if (ext === "tsx") {
|
|
1885
|
-
content =
|
|
2100
|
+
content =
|
|
2101
|
+
"// EXAMPLE COMPONENT — use the API patterns but create your own JSX structure and layout\n" +
|
|
2102
|
+
content;
|
|
1886
2103
|
}
|
|
1887
2104
|
parts.push(`### ${file.filename}`, "", `\`\`\`${lang}`, content, "```", "");
|
|
1888
2105
|
}
|
|
@@ -1896,63 +2113,88 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
1896
2113
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1897
2114
|
});
|
|
1898
2115
|
// Tool: get_framework_guide
|
|
1899
|
-
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.", {
|
|
2116
|
+
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.", {
|
|
2117
|
+
topic: z
|
|
2118
|
+
.string()
|
|
2119
|
+
.describe("Topic key or keyword (call `list_topics()` for the full list)"),
|
|
2120
|
+
}, async ({ topic }) => {
|
|
1900
2121
|
const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
|
|
1901
2122
|
// Alias mapping for common alternative topic names
|
|
1902
2123
|
const topicAliases = {
|
|
1903
2124
|
"form-handling": "form-patterns",
|
|
1904
|
-
|
|
2125
|
+
forms: "form-patterns",
|
|
1905
2126
|
"data-fetching": "async-data-patterns",
|
|
1906
|
-
|
|
1907
|
-
|
|
2127
|
+
async: "async-data-patterns",
|
|
2128
|
+
loading: "async-data-patterns",
|
|
1908
2129
|
"sub-components": "sub-component-patterns",
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2130
|
+
subcomponents: "sub-component-patterns",
|
|
2131
|
+
routing: "navigation-patterns",
|
|
2132
|
+
router: "navigation-patterns",
|
|
2133
|
+
observer: "component-structure",
|
|
2134
|
+
reactivity: "component-structure",
|
|
2135
|
+
pitfalls: "common-pitfalls",
|
|
2136
|
+
gotchas: "common-pitfalls",
|
|
2137
|
+
mistakes: "common-pitfalls",
|
|
2138
|
+
header: "header-footer-patterns",
|
|
2139
|
+
footer: "header-footer-patterns",
|
|
2140
|
+
blog: "blog-patterns",
|
|
2141
|
+
cart: "cart-patterns",
|
|
2142
|
+
account: "account-patterns",
|
|
1922
2143
|
"product-detail": "product-detail-patterns",
|
|
1923
2144
|
"product-list": "product-list-patterns",
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2145
|
+
filtering: "product-list-patterns",
|
|
2146
|
+
reviews: "review-patterns",
|
|
2147
|
+
slider: "slider-overlay-patterns",
|
|
2148
|
+
overlay: "slider-overlay-patterns",
|
|
2149
|
+
modal: "slider-overlay-patterns",
|
|
2150
|
+
architecture: "real-world-architecture",
|
|
2151
|
+
theme: "real-world-architecture",
|
|
1931
2152
|
"global-styles": "global-css",
|
|
1932
|
-
|
|
2153
|
+
global: "global-css",
|
|
1933
2154
|
"css-variables": "global-css",
|
|
1934
2155
|
"custom-properties": "global-css",
|
|
1935
2156
|
};
|
|
1936
2157
|
const resolvedTopic = topicAliases[topicLower] || topicLower;
|
|
1937
2158
|
// Topics that involve MobX store reads get a reminder about root reactivity
|
|
1938
2159
|
const storeTopics = new Set([
|
|
1939
|
-
"product-detail-patterns",
|
|
1940
|
-
"
|
|
1941
|
-
"
|
|
1942
|
-
"
|
|
2160
|
+
"product-detail-patterns",
|
|
2161
|
+
"product-list-patterns",
|
|
2162
|
+
"cart-patterns",
|
|
2163
|
+
"account-patterns",
|
|
2164
|
+
"header-footer-patterns",
|
|
2165
|
+
"review-patterns",
|
|
2166
|
+
"blog-patterns",
|
|
2167
|
+
"form-handling",
|
|
2168
|
+
"async-data-patterns",
|
|
2169
|
+
"component-structure",
|
|
2170
|
+
"imports",
|
|
1943
2171
|
]);
|
|
1944
2172
|
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";
|
|
1945
2173
|
// Try exact key match (with alias resolution)
|
|
1946
2174
|
if (frameworkData.topics[resolvedTopic]) {
|
|
1947
2175
|
const t = frameworkData.topics[resolvedTopic];
|
|
1948
2176
|
const prefix = storeTopics.has(resolvedTopic) ? observerReminder : "";
|
|
1949
|
-
return {
|
|
2177
|
+
return {
|
|
2178
|
+
content: [
|
|
2179
|
+
{
|
|
2180
|
+
type: "text",
|
|
2181
|
+
text: `## ${t.title}\n\n${prefix}${t.content}`,
|
|
2182
|
+
},
|
|
2183
|
+
],
|
|
2184
|
+
};
|
|
1950
2185
|
}
|
|
1951
2186
|
// Try original topic key (without alias) in case it's a direct key
|
|
1952
2187
|
if (resolvedTopic !== topicLower && frameworkData.topics[topicLower]) {
|
|
1953
2188
|
const t = frameworkData.topics[topicLower];
|
|
1954
2189
|
const prefix = storeTopics.has(topicLower) ? observerReminder : "";
|
|
1955
|
-
return {
|
|
2190
|
+
return {
|
|
2191
|
+
content: [
|
|
2192
|
+
{
|
|
2193
|
+
type: "text",
|
|
2194
|
+
text: `## ${t.title}\n\n${prefix}${t.content}`,
|
|
2195
|
+
},
|
|
2196
|
+
],
|
|
2197
|
+
};
|
|
1956
2198
|
}
|
|
1957
2199
|
// Try keyword search
|
|
1958
2200
|
const matches = searchFrameworkTopics(topic);
|
|
@@ -1960,7 +2202,12 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
|
|
|
1960
2202
|
const best = matches[0];
|
|
1961
2203
|
const prefix = storeTopics.has(best.key) ? observerReminder : "";
|
|
1962
2204
|
return {
|
|
1963
|
-
content: [
|
|
2205
|
+
content: [
|
|
2206
|
+
{
|
|
2207
|
+
type: "text",
|
|
2208
|
+
text: `## ${best.topic.title}\n\n${prefix}${best.topic.content}`,
|
|
2209
|
+
},
|
|
2210
|
+
],
|
|
1964
2211
|
};
|
|
1965
2212
|
}
|
|
1966
2213
|
const available = Object.entries(frameworkData.topics)
|
|
@@ -1976,16 +2223,27 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
|
|
|
1976
2223
|
};
|
|
1977
2224
|
});
|
|
1978
2225
|
// Tool: get_type_definition
|
|
1979
|
-
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.", {
|
|
2226
|
+
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.", {
|
|
2227
|
+
name: z
|
|
2228
|
+
.string()
|
|
2229
|
+
.describe("Type or enum name (e.g. 'IkasProduct', 'IkasOrderStatus')"),
|
|
2230
|
+
}, async ({ name }) => {
|
|
1980
2231
|
if (!typesData) {
|
|
1981
2232
|
return {
|
|
1982
|
-
content: [
|
|
2233
|
+
content: [
|
|
2234
|
+
{
|
|
2235
|
+
type: "text",
|
|
2236
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2237
|
+
},
|
|
2238
|
+
],
|
|
1983
2239
|
};
|
|
1984
2240
|
}
|
|
1985
2241
|
const nameLower = name.toLowerCase();
|
|
1986
2242
|
const td = typesData.types.find((t) => t.name.toLowerCase() === nameLower);
|
|
1987
2243
|
if (td) {
|
|
1988
|
-
return {
|
|
2244
|
+
return {
|
|
2245
|
+
content: [{ type: "text", text: formatTypeDefinition(td) }],
|
|
2246
|
+
};
|
|
1989
2247
|
}
|
|
1990
2248
|
// Fuzzy match
|
|
1991
2249
|
const matches = typesData.types.filter((t) => t.name.toLowerCase().includes(nameLower));
|
|
@@ -2001,14 +2259,28 @@ server.tool("get_type_definition", "Get the full definition of a storefront type
|
|
|
2001
2259
|
};
|
|
2002
2260
|
}
|
|
2003
2261
|
return {
|
|
2004
|
-
content: [
|
|
2262
|
+
content: [
|
|
2263
|
+
{
|
|
2264
|
+
type: "text",
|
|
2265
|
+
text: `Type "${name}" not found. Use \`list_types()\` to see all available types.`,
|
|
2266
|
+
},
|
|
2267
|
+
],
|
|
2005
2268
|
};
|
|
2006
2269
|
});
|
|
2007
2270
|
// Tool: get_functions_for_type
|
|
2008
|
-
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.", {
|
|
2271
|
+
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.", {
|
|
2272
|
+
typeName: z
|
|
2273
|
+
.string()
|
|
2274
|
+
.describe("Type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
|
|
2275
|
+
}, async ({ typeName }) => {
|
|
2009
2276
|
if (!typesData) {
|
|
2010
2277
|
return {
|
|
2011
|
-
content: [
|
|
2278
|
+
content: [
|
|
2279
|
+
{
|
|
2280
|
+
type: "text",
|
|
2281
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2282
|
+
},
|
|
2283
|
+
],
|
|
2012
2284
|
};
|
|
2013
2285
|
}
|
|
2014
2286
|
const nameLower = typeName.toLowerCase();
|
|
@@ -2028,7 +2300,12 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
|
|
|
2028
2300
|
};
|
|
2029
2301
|
}
|
|
2030
2302
|
return {
|
|
2031
|
-
content: [
|
|
2303
|
+
content: [
|
|
2304
|
+
{
|
|
2305
|
+
type: "text",
|
|
2306
|
+
text: `Type "${typeName}" not found. Use \`list_types()\` to see all available types.`,
|
|
2307
|
+
},
|
|
2308
|
+
],
|
|
2032
2309
|
};
|
|
2033
2310
|
}
|
|
2034
2311
|
if (!td.relatedFunctions || td.relatedFunctions.length === 0) {
|
|
@@ -2059,7 +2336,10 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
|
|
|
2059
2336
|
});
|
|
2060
2337
|
// Tool: get_model_guide
|
|
2061
2338
|
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.", {
|
|
2062
|
-
model: z
|
|
2339
|
+
model: z
|
|
2340
|
+
.string()
|
|
2341
|
+
.optional()
|
|
2342
|
+
.describe("Model type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
|
|
2063
2343
|
name: z.string().optional().describe("Alias for 'model'"),
|
|
2064
2344
|
mode: z
|
|
2065
2345
|
.enum(["summary", "full"])
|
|
@@ -2070,12 +2350,22 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
2070
2350
|
const model = modelParam || nameParam;
|
|
2071
2351
|
if (!model) {
|
|
2072
2352
|
return {
|
|
2073
|
-
content: [
|
|
2353
|
+
content: [
|
|
2354
|
+
{
|
|
2355
|
+
type: "text",
|
|
2356
|
+
text: "Please provide a model name (e.g. 'IkasProduct', 'IkasOrder'). Use the 'model' or 'name' parameter.",
|
|
2357
|
+
},
|
|
2358
|
+
],
|
|
2074
2359
|
};
|
|
2075
2360
|
}
|
|
2076
2361
|
if (!typesData) {
|
|
2077
2362
|
return {
|
|
2078
|
-
content: [
|
|
2363
|
+
content: [
|
|
2364
|
+
{
|
|
2365
|
+
type: "text",
|
|
2366
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2367
|
+
},
|
|
2368
|
+
],
|
|
2079
2369
|
};
|
|
2080
2370
|
}
|
|
2081
2371
|
const modelLower = model.toLowerCase();
|
|
@@ -2097,7 +2387,12 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
2097
2387
|
};
|
|
2098
2388
|
}
|
|
2099
2389
|
return {
|
|
2100
|
-
content: [
|
|
2390
|
+
content: [
|
|
2391
|
+
{
|
|
2392
|
+
type: "text",
|
|
2393
|
+
text: `Model "${model}" not found. Use \`list_types()\` to see all available types.`,
|
|
2394
|
+
},
|
|
2395
|
+
],
|
|
2101
2396
|
};
|
|
2102
2397
|
}
|
|
2103
2398
|
const parts = [`# Model Guide: ${td.name}\n`];
|
|
@@ -2205,13 +2500,23 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
2205
2500
|
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 }) => {
|
|
2206
2501
|
if (!typesData) {
|
|
2207
2502
|
return {
|
|
2208
|
-
content: [
|
|
2503
|
+
content: [
|
|
2504
|
+
{
|
|
2505
|
+
type: "text",
|
|
2506
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2507
|
+
},
|
|
2508
|
+
],
|
|
2209
2509
|
};
|
|
2210
2510
|
}
|
|
2211
2511
|
const results = searchTypes(query).slice(0, 15);
|
|
2212
2512
|
if (results.length === 0) {
|
|
2213
2513
|
return {
|
|
2214
|
-
content: [
|
|
2514
|
+
content: [
|
|
2515
|
+
{
|
|
2516
|
+
type: "text",
|
|
2517
|
+
text: `No types found matching "${query}". Use \`list_types()\` to see all available types.`,
|
|
2518
|
+
},
|
|
2519
|
+
],
|
|
2215
2520
|
};
|
|
2216
2521
|
}
|
|
2217
2522
|
const parts = [`## Type Search Results for "${query}"\n`];
|
|
@@ -2275,7 +2580,7 @@ server.tool("list_types", "List storefront types and enums. Use `domain` and/or
|
|
|
2275
2580
|
for (const [d, count] of sorted) {
|
|
2276
2581
|
lines.push(`- \`${d}\` (${count})`);
|
|
2277
2582
|
}
|
|
2278
|
-
lines.push("",
|
|
2583
|
+
lines.push("", 'Call `list_types(domain: "<name>")` to see summaries for a domain.');
|
|
2279
2584
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2280
2585
|
}
|
|
2281
2586
|
const domainLower = domain.toLowerCase();
|
|
@@ -2318,11 +2623,21 @@ server.tool("get_prop_types", "Get all available ikas.config.json prop types wit
|
|
|
2318
2623
|
const propTypesTopic = frameworkData.topics["prop-types"];
|
|
2319
2624
|
if (propTypesTopic) {
|
|
2320
2625
|
return {
|
|
2321
|
-
content: [
|
|
2626
|
+
content: [
|
|
2627
|
+
{
|
|
2628
|
+
type: "text",
|
|
2629
|
+
text: `## ${propTypesTopic.title}\n\n${propTypesTopic.content}`,
|
|
2630
|
+
},
|
|
2631
|
+
],
|
|
2322
2632
|
};
|
|
2323
2633
|
}
|
|
2324
2634
|
return {
|
|
2325
|
-
content: [
|
|
2635
|
+
content: [
|
|
2636
|
+
{
|
|
2637
|
+
type: "text",
|
|
2638
|
+
text: "Prop types documentation not available.",
|
|
2639
|
+
},
|
|
2640
|
+
],
|
|
2326
2641
|
};
|
|
2327
2642
|
});
|
|
2328
2643
|
// Tool: get_section_template
|
|
@@ -2419,7 +2734,8 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2419
2734
|
// the end of the response in the existing recipe-builder block.
|
|
2420
2735
|
{
|
|
2421
2736
|
const snippetStrForBanner = bundle.rootFiles["ikas-config-snippet.json"];
|
|
2422
|
-
if (snippetStrForBanner &&
|
|
2737
|
+
if (snippetStrForBanner &&
|
|
2738
|
+
/<id-of-[A-Za-z0-9_]+>/.test(snippetStrForBanner)) {
|
|
2423
2739
|
const childMatches = Array.from(snippetStrForBanner.matchAll(/<id-of-([A-Za-z0-9_]+)>/g));
|
|
2424
2740
|
const uniqueChildren = Array.from(new Set(childMatches.map((m) => m[1])));
|
|
2425
2741
|
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.`, "");
|
|
@@ -2543,7 +2859,10 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2543
2859
|
try {
|
|
2544
2860
|
const childSnippet = JSON.parse(fs.readFileSync(childSnippetPath, "utf-8"));
|
|
2545
2861
|
const childProps = (childSnippet.props || []).map((p) => {
|
|
2546
|
-
const out = {
|
|
2862
|
+
const out = {
|
|
2863
|
+
name: p.name,
|
|
2864
|
+
type: p.type,
|
|
2865
|
+
};
|
|
2547
2866
|
if (p.displayName)
|
|
2548
2867
|
out.displayName = p.displayName;
|
|
2549
2868
|
if (p.required)
|
|
@@ -2616,10 +2935,7 @@ server.tool("get_section_child", "Fetch one item's files from a section's childr
|
|
|
2616
2935
|
.string()
|
|
2617
2936
|
.optional()
|
|
2618
2937
|
.describe("The item name as listed in `get_section_template`'s response (Children/Components/Sub-components)"),
|
|
2619
|
-
child: z
|
|
2620
|
-
.string()
|
|
2621
|
-
.optional()
|
|
2622
|
-
.describe("Alias for `name`"),
|
|
2938
|
+
child: z.string().optional().describe("Alias for `name`"),
|
|
2623
2939
|
kind: z
|
|
2624
2940
|
.enum(["children", "components", "sub-components"])
|
|
2625
2941
|
.optional()
|
|
@@ -2782,7 +3098,11 @@ server.tool("list_section_types", "List all available `get_section_template` sec
|
|
|
2782
3098
|
});
|
|
2783
3099
|
// --- Migration tools ---
|
|
2784
3100
|
// Tool: analyze_old_theme
|
|
2785
|
-
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.", {
|
|
3101
|
+
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.", {
|
|
3102
|
+
theme_json: z
|
|
3103
|
+
.string()
|
|
3104
|
+
.describe("The raw JSON content of the old theme.json file"),
|
|
3105
|
+
}, async ({ theme_json }) => {
|
|
2786
3106
|
try {
|
|
2787
3107
|
const parsed = JSON.parse(theme_json);
|
|
2788
3108
|
const analysis = analyzeOldTheme(parsed);
|
|
@@ -2790,102 +3110,162 @@ server.tool("analyze_old_theme", "Analyze an old ikas storefront theme.json and
|
|
|
2790
3110
|
}
|
|
2791
3111
|
catch (err) {
|
|
2792
3112
|
return {
|
|
2793
|
-
content: [
|
|
3113
|
+
content: [
|
|
3114
|
+
{
|
|
3115
|
+
type: "text",
|
|
3116
|
+
text: `Error parsing theme.json: ${err instanceof Error ? err.message : String(err)}. Make sure you're passing valid JSON.`,
|
|
3117
|
+
},
|
|
3118
|
+
],
|
|
2794
3119
|
};
|
|
2795
3120
|
}
|
|
2796
3121
|
});
|
|
2797
3122
|
// Tool: get_migration_guide
|
|
2798
3123
|
const migrationTopicAliases = {
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
3124
|
+
overview: "migration-overview",
|
|
3125
|
+
migrate: "migration-overview",
|
|
3126
|
+
custom: "custom-data-conversion",
|
|
2802
3127
|
"custom-data": "custom-data-conversion",
|
|
2803
|
-
|
|
3128
|
+
customdata: "custom-data-conversion",
|
|
2804
3129
|
"dynamic-list": "custom-data-conversion",
|
|
2805
3130
|
"component-list": "custom-data-conversion",
|
|
2806
|
-
|
|
2807
|
-
|
|
3131
|
+
slider: "prop-type-mapping",
|
|
3132
|
+
props: "prop-type-mapping",
|
|
2808
3133
|
"prop-mapping": "prop-type-mapping",
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
3134
|
+
types: "prop-type-mapping",
|
|
3135
|
+
react: "react-to-preact",
|
|
3136
|
+
preact: "react-to-preact",
|
|
3137
|
+
observer: "react-to-preact",
|
|
3138
|
+
libraries: "library-replacements",
|
|
3139
|
+
swiper: "library-replacements",
|
|
3140
|
+
headlessui: "library-replacements",
|
|
3141
|
+
tailwind: "library-replacements",
|
|
3142
|
+
tailwindcss: "library-replacements",
|
|
3143
|
+
recharts: "library-replacements",
|
|
3144
|
+
marquee: "library-replacements",
|
|
3145
|
+
imports: "storefront-import-mapping",
|
|
3146
|
+
storefront: "storefront-import-mapping",
|
|
2822
3147
|
"bp-storefront": "storefront-import-mapping",
|
|
2823
3148
|
"theme-json": "theme-json-anatomy",
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
3149
|
+
anatomy: "theme-json-anatomy",
|
|
3150
|
+
decompose: "component-decomposition-strategy",
|
|
3151
|
+
decomposition: "component-decomposition-strategy",
|
|
3152
|
+
strategy: "component-decomposition-strategy",
|
|
3153
|
+
project: "complete-project-generation",
|
|
3154
|
+
generate: "complete-project-generation",
|
|
3155
|
+
generation: "complete-project-generation",
|
|
3156
|
+
settings: "settings-conversion",
|
|
3157
|
+
colors: "settings-conversion",
|
|
3158
|
+
fonts: "settings-conversion",
|
|
3159
|
+
find: "finding-new-system-equivalents",
|
|
3160
|
+
search: "finding-new-system-equivalents",
|
|
3161
|
+
discover: "finding-new-system-equivalents",
|
|
3162
|
+
equivalent: "finding-new-system-equivalents",
|
|
3163
|
+
equivalents: "finding-new-system-equivalents",
|
|
3164
|
+
replacement: "finding-new-system-equivalents",
|
|
2840
3165
|
};
|
|
2841
3166
|
const migrationTopicKeys = migrationData
|
|
2842
3167
|
? Object.keys(migrationData.topics)
|
|
2843
3168
|
: [];
|
|
2844
|
-
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.`, {
|
|
3169
|
+
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.`, {
|
|
3170
|
+
topic: z
|
|
3171
|
+
.string()
|
|
3172
|
+
.describe("Migration topic key, alias, or 'list' to see all topics"),
|
|
3173
|
+
}, async ({ topic }) => {
|
|
2845
3174
|
if (!migrationData) {
|
|
2846
|
-
return {
|
|
3175
|
+
return {
|
|
3176
|
+
content: [
|
|
3177
|
+
{
|
|
3178
|
+
type: "text",
|
|
3179
|
+
text: "Migration data not available. Ensure data/migration.json exists.",
|
|
3180
|
+
},
|
|
3181
|
+
],
|
|
3182
|
+
};
|
|
2847
3183
|
}
|
|
2848
3184
|
if (topic.toLowerCase() === "list") {
|
|
2849
3185
|
const available = Object.entries(migrationData.topics)
|
|
2850
3186
|
.map(([key, t]) => `- \`${key}\` — ${t.title}: ${t.description}`)
|
|
2851
3187
|
.join("\n");
|
|
2852
|
-
return {
|
|
3188
|
+
return {
|
|
3189
|
+
content: [
|
|
3190
|
+
{
|
|
3191
|
+
type: "text",
|
|
3192
|
+
text: `## Available Migration Topics\n\n${available}`,
|
|
3193
|
+
},
|
|
3194
|
+
],
|
|
3195
|
+
};
|
|
2853
3196
|
}
|
|
2854
3197
|
const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
|
|
2855
3198
|
const resolvedTopic = migrationTopicAliases[topicLower] || topicLower;
|
|
2856
3199
|
if (migrationData.topics[resolvedTopic]) {
|
|
2857
3200
|
const t = migrationData.topics[resolvedTopic];
|
|
2858
|
-
return {
|
|
3201
|
+
return {
|
|
3202
|
+
content: [
|
|
3203
|
+
{ type: "text", text: `## ${t.title}\n\n${t.content}` },
|
|
3204
|
+
],
|
|
3205
|
+
};
|
|
2859
3206
|
}
|
|
2860
3207
|
// Try original key
|
|
2861
3208
|
if (resolvedTopic !== topicLower && migrationData.topics[topicLower]) {
|
|
2862
3209
|
const t = migrationData.topics[topicLower];
|
|
2863
|
-
return {
|
|
3210
|
+
return {
|
|
3211
|
+
content: [
|
|
3212
|
+
{ type: "text", text: `## ${t.title}\n\n${t.content}` },
|
|
3213
|
+
],
|
|
3214
|
+
};
|
|
2864
3215
|
}
|
|
2865
3216
|
// Keyword search
|
|
2866
3217
|
const matches = searchMigrationTopics(topic);
|
|
2867
3218
|
if (matches.length > 0) {
|
|
2868
3219
|
const best = matches[0];
|
|
2869
|
-
return {
|
|
3220
|
+
return {
|
|
3221
|
+
content: [
|
|
3222
|
+
{
|
|
3223
|
+
type: "text",
|
|
3224
|
+
text: `## ${best.topic.title}\n\n${best.topic.content}`,
|
|
3225
|
+
},
|
|
3226
|
+
],
|
|
3227
|
+
};
|
|
2870
3228
|
}
|
|
2871
3229
|
const available = Object.entries(migrationData.topics)
|
|
2872
3230
|
.map(([key, t]) => ` - \`${key}\` - ${t.title}`)
|
|
2873
3231
|
.join("\n");
|
|
2874
3232
|
return {
|
|
2875
|
-
content: [
|
|
3233
|
+
content: [
|
|
3234
|
+
{
|
|
3235
|
+
type: "text",
|
|
3236
|
+
text: `Migration topic "${topic}" not found. Available topics:\n${available}`,
|
|
3237
|
+
},
|
|
3238
|
+
],
|
|
2876
3239
|
};
|
|
2877
3240
|
});
|
|
2878
3241
|
// Tool: get_migration_example
|
|
2879
|
-
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.`, {
|
|
3242
|
+
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.`, {
|
|
3243
|
+
example: z.string().describe("Example name or 'list' to see all examples"),
|
|
3244
|
+
}, async ({ example }) => {
|
|
2880
3245
|
if (example.toLowerCase() === "list") {
|
|
2881
3246
|
if (migrationExampleNames.length === 0) {
|
|
2882
|
-
return {
|
|
3247
|
+
return {
|
|
3248
|
+
content: [
|
|
3249
|
+
{ type: "text", text: "No migration examples available." },
|
|
3250
|
+
],
|
|
3251
|
+
};
|
|
2883
3252
|
}
|
|
2884
|
-
const list = migrationExampleNames
|
|
3253
|
+
const list = migrationExampleNames
|
|
3254
|
+
.map((name) => {
|
|
2885
3255
|
const ex = loadMigrationExample(name);
|
|
2886
|
-
return ex
|
|
2887
|
-
|
|
2888
|
-
|
|
3256
|
+
return ex
|
|
3257
|
+
? `- \`${name}\` — ${ex.title}: ${ex.description}`
|
|
3258
|
+
: `- \`${name}\``;
|
|
3259
|
+
})
|
|
3260
|
+
.join("\n");
|
|
3261
|
+
return {
|
|
3262
|
+
content: [
|
|
3263
|
+
{
|
|
3264
|
+
type: "text",
|
|
3265
|
+
text: `## Available Migration Examples\n\n${list}`,
|
|
3266
|
+
},
|
|
3267
|
+
],
|
|
3268
|
+
};
|
|
2889
3269
|
}
|
|
2890
3270
|
const exampleLower = example.toLowerCase();
|
|
2891
3271
|
let exName = migrationExampleNames.find((n) => n === exampleLower);
|
|
@@ -2895,19 +3275,26 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
|
|
|
2895
3275
|
if (!exName) {
|
|
2896
3276
|
const available = migrationExampleNames.join(", ");
|
|
2897
3277
|
return {
|
|
2898
|
-
content: [
|
|
3278
|
+
content: [
|
|
3279
|
+
{
|
|
3280
|
+
type: "text",
|
|
3281
|
+
text: `Migration example "${example}" not found. Available: ${available}`,
|
|
3282
|
+
},
|
|
3283
|
+
],
|
|
2899
3284
|
};
|
|
2900
3285
|
}
|
|
2901
3286
|
const ex = loadMigrationExample(exName);
|
|
2902
3287
|
if (!ex) {
|
|
2903
|
-
return {
|
|
3288
|
+
return {
|
|
3289
|
+
content: [
|
|
3290
|
+
{
|
|
3291
|
+
type: "text",
|
|
3292
|
+
text: `Failed to load migration example "${exName}".`,
|
|
3293
|
+
},
|
|
3294
|
+
],
|
|
3295
|
+
};
|
|
2904
3296
|
}
|
|
2905
|
-
const parts = [
|
|
2906
|
-
`## ${ex.title}`,
|
|
2907
|
-
"",
|
|
2908
|
-
ex.description,
|
|
2909
|
-
"",
|
|
2910
|
-
];
|
|
3297
|
+
const parts = [`## ${ex.title}`, "", ex.description, ""];
|
|
2911
3298
|
for (const [filename, content] of Object.entries(ex.files)) {
|
|
2912
3299
|
const ext = filename.split(".").pop() || "text";
|
|
2913
3300
|
const lang = ext === "tsx" || ext === "ts"
|
|
@@ -2926,13 +3313,31 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
|
|
|
2926
3313
|
});
|
|
2927
3314
|
// Tool: plan_migration
|
|
2928
3315
|
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).", {
|
|
2929
|
-
theme_json: z
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3316
|
+
theme_json: z
|
|
3317
|
+
.string()
|
|
3318
|
+
.optional()
|
|
3319
|
+
.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."),
|
|
3320
|
+
theme_json_path: z
|
|
3321
|
+
.string()
|
|
3322
|
+
.optional()
|
|
3323
|
+
.describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
|
|
3324
|
+
project_name: z
|
|
3325
|
+
.string()
|
|
3326
|
+
.optional()
|
|
3327
|
+
.describe("Target new project name, used to prefix migration-tracking IDs (default: 'my-theme')"),
|
|
3328
|
+
old_source_dir: z
|
|
3329
|
+
.string()
|
|
3330
|
+
.optional()
|
|
3331
|
+
.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."),
|
|
3332
|
+
project_root: z
|
|
3333
|
+
.string()
|
|
3334
|
+
.optional()
|
|
3335
|
+
.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."),
|
|
3336
|
+
overwrite: z
|
|
3337
|
+
.boolean()
|
|
3338
|
+
.optional()
|
|
3339
|
+
.describe("If MIGRATION.md already exists at <project_root>/MIGRATION.md and is non-empty, refuse the write unless this is true. Default: false."),
|
|
3340
|
+
}, async ({ theme_json, theme_json_path, project_name, old_source_dir, project_root, overwrite, }) => {
|
|
2936
3341
|
try {
|
|
2937
3342
|
const parsed = resolveThemeJson(theme_json, theme_json_path);
|
|
2938
3343
|
const projectName = project_name || "my-theme";
|
|
@@ -2993,18 +3398,37 @@ server.tool("plan_migration", "Generate the **initial** migration plan and (when
|
|
|
2993
3398
|
}
|
|
2994
3399
|
catch (err) {
|
|
2995
3400
|
return {
|
|
2996
|
-
content: [
|
|
3401
|
+
content: [
|
|
3402
|
+
{
|
|
3403
|
+
type: "text",
|
|
3404
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
3405
|
+
},
|
|
3406
|
+
],
|
|
2997
3407
|
};
|
|
2998
3408
|
}
|
|
2999
3409
|
});
|
|
3000
3410
|
// Tool: get_section_migration_plan
|
|
3001
3411
|
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.", {
|
|
3002
|
-
theme_json: z
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3412
|
+
theme_json: z
|
|
3413
|
+
.string()
|
|
3414
|
+
.optional()
|
|
3415
|
+
.describe("Raw JSON content of the old theme.json. EITHER this OR theme_json_path is required (not both)."),
|
|
3416
|
+
theme_json_path: z
|
|
3417
|
+
.string()
|
|
3418
|
+
.optional()
|
|
3419
|
+
.describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
|
|
3420
|
+
section_name: z
|
|
3421
|
+
.string()
|
|
3422
|
+
.describe("Old component name (e.g. 'Navbar', 'ProductGrid') or dir name, OR the new section ID (e.g. 'my-theme-navbar')"),
|
|
3423
|
+
project_name: z
|
|
3424
|
+
.string()
|
|
3425
|
+
.optional()
|
|
3426
|
+
.describe("Target new project name (must match what was used in plan_migration). Default: 'my-theme'"),
|
|
3427
|
+
old_source_dir: z
|
|
3428
|
+
.string()
|
|
3429
|
+
.optional()
|
|
3430
|
+
.describe("Absolute path to old src/ directory (used to output exact source file paths to read)"),
|
|
3431
|
+
}, async ({ theme_json, theme_json_path, section_name, project_name, old_source_dir, }) => {
|
|
3008
3432
|
try {
|
|
3009
3433
|
const parsed = resolveThemeJson(theme_json, theme_json_path);
|
|
3010
3434
|
const projectName = project_name || "my-theme";
|
|
@@ -3013,7 +3437,12 @@ server.tool("get_section_migration_plan", "Returns concrete CLI commands and pro
|
|
|
3013
3437
|
}
|
|
3014
3438
|
catch (err) {
|
|
3015
3439
|
return {
|
|
3016
|
-
content: [
|
|
3440
|
+
content: [
|
|
3441
|
+
{
|
|
3442
|
+
type: "text",
|
|
3443
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
3444
|
+
},
|
|
3445
|
+
],
|
|
3017
3446
|
};
|
|
3018
3447
|
}
|
|
3019
3448
|
});
|
|
@@ -3049,7 +3478,11 @@ async function runIkasComponentCli(projectRoot, args) {
|
|
|
3049
3478
|
: err
|
|
3050
3479
|
? 1
|
|
3051
3480
|
: 0;
|
|
3052
|
-
resolve({
|
|
3481
|
+
resolve({
|
|
3482
|
+
stdout: stdout?.toString() ?? "",
|
|
3483
|
+
stderr: stderr?.toString() ?? "",
|
|
3484
|
+
exitCode,
|
|
3485
|
+
});
|
|
3053
3486
|
});
|
|
3054
3487
|
});
|
|
3055
3488
|
}
|
|
@@ -3059,7 +3492,11 @@ function parseCliJson(stdout) {
|
|
|
3059
3492
|
return null;
|
|
3060
3493
|
// CLI prints exactly one JSON object on stdout; if multiple lines appeared
|
|
3061
3494
|
// (e.g., warnings on stderr leaked), take the last non-empty line.
|
|
3062
|
-
const last = trimmed
|
|
3495
|
+
const last = trimmed
|
|
3496
|
+
.split("\n")
|
|
3497
|
+
.map((l) => l.trim())
|
|
3498
|
+
.filter(Boolean)
|
|
3499
|
+
.pop();
|
|
3063
3500
|
if (!last)
|
|
3064
3501
|
return null;
|
|
3065
3502
|
try {
|
|
@@ -3074,7 +3511,11 @@ async function callEditorAction(projectRoot, args) {
|
|
|
3074
3511
|
const { stdout, stderr, exitCode } = await runIkasComponentCli(projectRoot, args);
|
|
3075
3512
|
const parsed = parseCliJson(stdout);
|
|
3076
3513
|
if (parsed) {
|
|
3077
|
-
return {
|
|
3514
|
+
return {
|
|
3515
|
+
content: [
|
|
3516
|
+
{ type: "text", text: JSON.stringify(parsed, null, 2) },
|
|
3517
|
+
],
|
|
3518
|
+
};
|
|
3078
3519
|
}
|
|
3079
3520
|
return {
|
|
3080
3521
|
content: [
|
|
@@ -3097,38 +3538,77 @@ async function callEditorAction(projectRoot, args) {
|
|
|
3097
3538
|
};
|
|
3098
3539
|
}
|
|
3099
3540
|
}
|
|
3541
|
+
//
|
|
3100
3542
|
// Tool: list_editor_pages
|
|
3101
3543
|
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`.", {
|
|
3102
|
-
project_root: z
|
|
3103
|
-
|
|
3544
|
+
project_root: z
|
|
3545
|
+
.string()
|
|
3546
|
+
.describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
|
|
3547
|
+
port: z
|
|
3548
|
+
.number()
|
|
3549
|
+
.optional()
|
|
3550
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3104
3551
|
}, async ({ project_root, port }) => {
|
|
3105
3552
|
const args = ["list-pages", ...(port ? ["--port", String(port)] : [])];
|
|
3106
3553
|
return callEditorAction(project_root, args);
|
|
3107
3554
|
});
|
|
3108
3555
|
// Tool: list_imported_sections
|
|
3109
3556
|
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`.", {
|
|
3110
|
-
project_root: z
|
|
3111
|
-
|
|
3557
|
+
project_root: z
|
|
3558
|
+
.string()
|
|
3559
|
+
.describe("Absolute path to the code-component project."),
|
|
3560
|
+
port: z
|
|
3561
|
+
.number()
|
|
3562
|
+
.optional()
|
|
3563
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3112
3564
|
}, async ({ project_root, port }) => {
|
|
3113
|
-
const args = [
|
|
3565
|
+
const args = [
|
|
3566
|
+
"list-imported",
|
|
3567
|
+
"--sections-only",
|
|
3568
|
+
...(port ? ["--port", String(port)] : []),
|
|
3569
|
+
];
|
|
3114
3570
|
return callEditorAction(project_root, args);
|
|
3115
3571
|
});
|
|
3116
3572
|
// Tool: import_section
|
|
3117
3573
|
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`).", {
|
|
3118
|
-
project_root: z
|
|
3119
|
-
|
|
3120
|
-
|
|
3574
|
+
project_root: z
|
|
3575
|
+
.string()
|
|
3576
|
+
.describe("Absolute path to the code-component project."),
|
|
3577
|
+
component_id: z
|
|
3578
|
+
.string()
|
|
3579
|
+
.describe("Component id from `ikas.config.json` (strict — no name resolution)."),
|
|
3580
|
+
port: z
|
|
3581
|
+
.number()
|
|
3582
|
+
.optional()
|
|
3583
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3121
3584
|
}, async ({ project_root, component_id, port }) => {
|
|
3122
|
-
const args = [
|
|
3585
|
+
const args = [
|
|
3586
|
+
"import",
|
|
3587
|
+
"--id",
|
|
3588
|
+
component_id,
|
|
3589
|
+
...(port ? ["--port", String(port)] : []),
|
|
3590
|
+
];
|
|
3123
3591
|
return callEditorAction(project_root, args);
|
|
3124
3592
|
});
|
|
3125
3593
|
// Tool: add_section_to_page
|
|
3126
|
-
server.tool("add_section_to_page",
|
|
3127
|
-
project_root: z
|
|
3128
|
-
|
|
3594
|
+
server.tool("add_section_to_page", 'Place an already-imported section-type code component on a page in the editor. 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` (use `list_page_sections` to get the placement\'s `elementId` and prop names).', {
|
|
3595
|
+
project_root: z
|
|
3596
|
+
.string()
|
|
3597
|
+
.describe("Absolute path to the code-component project."),
|
|
3598
|
+
component_id: z
|
|
3599
|
+
.string()
|
|
3600
|
+
.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)."),
|
|
3129
3601
|
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3130
|
-
index: z
|
|
3131
|
-
|
|
3602
|
+
index: z
|
|
3603
|
+
.number()
|
|
3604
|
+
.int()
|
|
3605
|
+
.nonnegative()
|
|
3606
|
+
.optional()
|
|
3607
|
+
.describe("Zero-based insertion index in the page; appends when omitted."),
|
|
3608
|
+
port: z
|
|
3609
|
+
.number()
|
|
3610
|
+
.optional()
|
|
3611
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3132
3612
|
}, async ({ project_root, component_id, page_id, index, port }) => {
|
|
3133
3613
|
const args = [
|
|
3134
3614
|
"add-to-page",
|
|
@@ -3141,6 +3621,64 @@ server.tool("add_section_to_page", "Place an already-imported section-type code
|
|
|
3141
3621
|
];
|
|
3142
3622
|
return callEditorAction(project_root, args);
|
|
3143
3623
|
});
|
|
3624
|
+
// Tool: list_page_sections
|
|
3625
|
+
server.tool("list_page_sections", "List the sections placed on a page. Each entry includes the per-placement `elementId` (the identity of THIS placement — there can be multiple placements of the same section), `sectionId`, `componentId`, `name`, the current `propValues`, and the blueprint `props` (each with `id`, `name`, `type`). Use the returned `elementId` plus a prop `id`/`name` with `update_section_prop` to change a prop value of that specific placement. Use `list_editor_pages` to discover page ids.", {
|
|
3626
|
+
project_root: z
|
|
3627
|
+
.string()
|
|
3628
|
+
.describe("Absolute path to the code-component project."),
|
|
3629
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3630
|
+
port: z
|
|
3631
|
+
.number()
|
|
3632
|
+
.optional()
|
|
3633
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3634
|
+
}, async ({ project_root, page_id, port }) => {
|
|
3635
|
+
const args = [
|
|
3636
|
+
"list-page-sections",
|
|
3637
|
+
"--page-id",
|
|
3638
|
+
page_id,
|
|
3639
|
+
...(port ? ["--port", String(port)] : []),
|
|
3640
|
+
];
|
|
3641
|
+
return callEditorAction(project_root, args);
|
|
3642
|
+
});
|
|
3643
|
+
// Tool: update_section_prop
|
|
3644
|
+
server.tool("update_section_prop", 'Change a single prop value of a section placed on a page. 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 prop types (image, link, list types) use their own object shape — inspect the existing entry in `list_page_sections` `propValues` to match it. The change is applied with undo support.', {
|
|
3645
|
+
project_root: z
|
|
3646
|
+
.string()
|
|
3647
|
+
.describe("Absolute path to the code-component project."),
|
|
3648
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3649
|
+
element_id: z
|
|
3650
|
+
.string()
|
|
3651
|
+
.describe("Placed-section elementId identifying THIS placement on the page (from `list_page_sections`)."),
|
|
3652
|
+
prop_id: z
|
|
3653
|
+
.string()
|
|
3654
|
+
.optional()
|
|
3655
|
+
.describe("Blueprint prop id to update (from `list_page_sections`). Provide this or `prop_name`."),
|
|
3656
|
+
prop_name: z
|
|
3657
|
+
.string()
|
|
3658
|
+
.optional()
|
|
3659
|
+
.describe("Blueprint prop name to update (alternative to `prop_id`)."),
|
|
3660
|
+
value: z
|
|
3661
|
+
.any()
|
|
3662
|
+
.describe('The prop value object to store, e.g. `{ "value": "Hello" }`. Match the shape of the existing entry in `list_page_sections` propValues for non-scalar prop types.'),
|
|
3663
|
+
port: z
|
|
3664
|
+
.number()
|
|
3665
|
+
.optional()
|
|
3666
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3667
|
+
}, async ({ project_root, page_id, element_id, prop_id, prop_name, value, port }) => {
|
|
3668
|
+
const args = [
|
|
3669
|
+
"update-section-prop",
|
|
3670
|
+
"--page-id",
|
|
3671
|
+
page_id,
|
|
3672
|
+
"--element-id",
|
|
3673
|
+
element_id,
|
|
3674
|
+
...(prop_id ? ["--prop-id", prop_id] : []),
|
|
3675
|
+
...(prop_name ? ["--prop-name", prop_name] : []),
|
|
3676
|
+
"--value",
|
|
3677
|
+
JSON.stringify(value),
|
|
3678
|
+
...(port ? ["--port", String(port)] : []),
|
|
3679
|
+
];
|
|
3680
|
+
return callEditorAction(project_root, args);
|
|
3681
|
+
});
|
|
3144
3682
|
// --- Start server ---
|
|
3145
3683
|
async function main() {
|
|
3146
3684
|
const transport = new StdioServerTransport();
|