@ikas/code-components-mcp 1.4.0-beta.14 → 1.4.0-beta.16
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 +1 -1
- package/data/storefront-api.json +1 -1
- package/data/storefront-types.json +1 -1
- package/dist/index.js +767 -231
- 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
|
}
|
|
@@ -1697,13 +1878,20 @@ server.tool("search_docs", "Search across all ikas storefront API docs, framewor
|
|
|
1697
1878
|
parts.push("");
|
|
1698
1879
|
parts.push("Use `get_migration_guide(topic)` to get full content for any migration topic.");
|
|
1699
1880
|
}
|
|
1700
|
-
if (functions.length === 0 &&
|
|
1881
|
+
if (functions.length === 0 &&
|
|
1882
|
+
topics.length === 0 &&
|
|
1883
|
+
types.length === 0 &&
|
|
1884
|
+
migrationTopics.length === 0) {
|
|
1701
1885
|
parts.push(`No results found for "${query}". Try different keywords or use \`list_functions()\` to see all available functions.`);
|
|
1702
1886
|
}
|
|
1703
1887
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1704
1888
|
});
|
|
1705
1889
|
// 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.", {
|
|
1890
|
+
server.tool("get_function_doc", "Get full documentation for a specific storefront API function including signature, parameters, return type, and example.", {
|
|
1891
|
+
name: z
|
|
1892
|
+
.string()
|
|
1893
|
+
.describe("Function name (e.g. 'addItemToCart', 'Router.navigate')"),
|
|
1894
|
+
}, async ({ name }) => {
|
|
1707
1895
|
const nameLower = name.toLowerCase();
|
|
1708
1896
|
// Phase 1: canonical-name match wins. A real function name always outranks
|
|
1709
1897
|
// any displayName alias so aliases can never shadow the function they're
|
|
@@ -1711,7 +1899,9 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1711
1899
|
// [BP-DISPLAY-NAME: hasCustomer] alias).
|
|
1712
1900
|
const byName = storefrontData.functions.find((f) => f.name.toLowerCase() === nameLower);
|
|
1713
1901
|
if (byName) {
|
|
1714
|
-
return {
|
|
1902
|
+
return {
|
|
1903
|
+
content: [{ type: "text", text: formatFunctionDoc(byName) }],
|
|
1904
|
+
};
|
|
1715
1905
|
}
|
|
1716
1906
|
// Phase 2: fall back to displayName aliases.
|
|
1717
1907
|
const byAlias = storefrontData.functions.filter((f) => f.displayName && f.displayName.toLowerCase() === nameLower);
|
|
@@ -1719,7 +1909,9 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1719
1909
|
const fn = byAlias[0];
|
|
1720
1910
|
const note = `> Note: "${name}" is a display alias for \`${fn.name}\`.\n\n`;
|
|
1721
1911
|
return {
|
|
1722
|
-
content: [
|
|
1912
|
+
content: [
|
|
1913
|
+
{ type: "text", text: note + formatFunctionDoc(fn) },
|
|
1914
|
+
],
|
|
1723
1915
|
};
|
|
1724
1916
|
}
|
|
1725
1917
|
if (byAlias.length > 1) {
|
|
@@ -1737,7 +1929,9 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1737
1929
|
(f.displayName && f.displayName.toLowerCase().includes(nameLower)));
|
|
1738
1930
|
if (matches.length > 0) {
|
|
1739
1931
|
const suggestions = matches.slice(0, 5).map((f) => {
|
|
1740
|
-
const alias = f.displayName && f.displayName !== f.name
|
|
1932
|
+
const alias = f.displayName && f.displayName !== f.name
|
|
1933
|
+
? ` (alias: ${f.displayName})`
|
|
1934
|
+
: "";
|
|
1741
1935
|
return ` - ${f.name}${alias}`;
|
|
1742
1936
|
});
|
|
1743
1937
|
return {
|
|
@@ -1750,7 +1944,12 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1750
1944
|
};
|
|
1751
1945
|
}
|
|
1752
1946
|
return {
|
|
1753
|
-
content: [
|
|
1947
|
+
content: [
|
|
1948
|
+
{
|
|
1949
|
+
type: "text",
|
|
1950
|
+
text: `Function "${name}" not found. Use \`list_functions()\` to see all available functions.`,
|
|
1951
|
+
},
|
|
1952
|
+
],
|
|
1754
1953
|
};
|
|
1755
1954
|
});
|
|
1756
1955
|
// Tool: list_functions
|
|
@@ -1800,7 +1999,7 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
|
|
|
1800
1999
|
if (uncategorized > 0) {
|
|
1801
2000
|
lines.push(`- \`Other\` (${uncategorized})`);
|
|
1802
2001
|
}
|
|
1803
|
-
lines.push("",
|
|
2002
|
+
lines.push("", 'Call `list_functions(category: "<name>")` to see one-line summaries for a category.');
|
|
1804
2003
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1805
2004
|
}
|
|
1806
2005
|
const catLower = category.toLowerCase();
|
|
@@ -1832,7 +2031,11 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
|
|
|
1832
2031
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1833
2032
|
});
|
|
1834
2033
|
// 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.", {
|
|
2034
|
+
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.", {
|
|
2035
|
+
task: z
|
|
2036
|
+
.string()
|
|
2037
|
+
.describe("Task description or example ID (call `list_examples()` for the full list)"),
|
|
2038
|
+
}, async ({ task }) => {
|
|
1836
2039
|
const taskLower = task.toLowerCase();
|
|
1837
2040
|
// Try exact ID match first
|
|
1838
2041
|
let example = storefrontData.codeExamples.find((e) => e.id === taskLower);
|
|
@@ -1852,7 +2055,9 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
1852
2055
|
}
|
|
1853
2056
|
}
|
|
1854
2057
|
if (!example) {
|
|
1855
|
-
const available = storefrontData.codeExamples
|
|
2058
|
+
const available = storefrontData.codeExamples
|
|
2059
|
+
.map((e) => ` - \`${e.id}\` - ${e.title}`)
|
|
2060
|
+
.join("\n");
|
|
1856
2061
|
return {
|
|
1857
2062
|
content: [
|
|
1858
2063
|
{
|
|
@@ -1875,14 +2080,24 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
1875
2080
|
if (example.files && example.files.length > 0) {
|
|
1876
2081
|
for (const file of example.files) {
|
|
1877
2082
|
const ext = file.filename.split(".").pop() || "text";
|
|
1878
|
-
const lang = ext === "tsx" || ext === "ts"
|
|
2083
|
+
const lang = ext === "tsx" || ext === "ts"
|
|
2084
|
+
? "typescript"
|
|
2085
|
+
: ext === "css"
|
|
2086
|
+
? "css"
|
|
2087
|
+
: ext === "json"
|
|
2088
|
+
? "json"
|
|
2089
|
+
: "text";
|
|
1879
2090
|
// Add inline originality comments to CSS and TSX files
|
|
1880
2091
|
let content = file.content;
|
|
1881
2092
|
if (ext === "css") {
|
|
1882
|
-
content =
|
|
2093
|
+
content =
|
|
2094
|
+
"/* EXAMPLE STYLING — create your own original CSS with different class names and design */\n" +
|
|
2095
|
+
content;
|
|
1883
2096
|
}
|
|
1884
2097
|
else if (ext === "tsx") {
|
|
1885
|
-
content =
|
|
2098
|
+
content =
|
|
2099
|
+
"// EXAMPLE COMPONENT — use the API patterns but create your own JSX structure and layout\n" +
|
|
2100
|
+
content;
|
|
1886
2101
|
}
|
|
1887
2102
|
parts.push(`### ${file.filename}`, "", `\`\`\`${lang}`, content, "```", "");
|
|
1888
2103
|
}
|
|
@@ -1896,63 +2111,88 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
1896
2111
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1897
2112
|
});
|
|
1898
2113
|
// 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.", {
|
|
2114
|
+
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.", {
|
|
2115
|
+
topic: z
|
|
2116
|
+
.string()
|
|
2117
|
+
.describe("Topic key or keyword (call `list_topics()` for the full list)"),
|
|
2118
|
+
}, async ({ topic }) => {
|
|
1900
2119
|
const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
|
|
1901
2120
|
// Alias mapping for common alternative topic names
|
|
1902
2121
|
const topicAliases = {
|
|
1903
2122
|
"form-handling": "form-patterns",
|
|
1904
|
-
|
|
2123
|
+
forms: "form-patterns",
|
|
1905
2124
|
"data-fetching": "async-data-patterns",
|
|
1906
|
-
|
|
1907
|
-
|
|
2125
|
+
async: "async-data-patterns",
|
|
2126
|
+
loading: "async-data-patterns",
|
|
1908
2127
|
"sub-components": "sub-component-patterns",
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2128
|
+
subcomponents: "sub-component-patterns",
|
|
2129
|
+
routing: "navigation-patterns",
|
|
2130
|
+
router: "navigation-patterns",
|
|
2131
|
+
observer: "component-structure",
|
|
2132
|
+
reactivity: "component-structure",
|
|
2133
|
+
pitfalls: "common-pitfalls",
|
|
2134
|
+
gotchas: "common-pitfalls",
|
|
2135
|
+
mistakes: "common-pitfalls",
|
|
2136
|
+
header: "header-footer-patterns",
|
|
2137
|
+
footer: "header-footer-patterns",
|
|
2138
|
+
blog: "blog-patterns",
|
|
2139
|
+
cart: "cart-patterns",
|
|
2140
|
+
account: "account-patterns",
|
|
1922
2141
|
"product-detail": "product-detail-patterns",
|
|
1923
2142
|
"product-list": "product-list-patterns",
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2143
|
+
filtering: "product-list-patterns",
|
|
2144
|
+
reviews: "review-patterns",
|
|
2145
|
+
slider: "slider-overlay-patterns",
|
|
2146
|
+
overlay: "slider-overlay-patterns",
|
|
2147
|
+
modal: "slider-overlay-patterns",
|
|
2148
|
+
architecture: "real-world-architecture",
|
|
2149
|
+
theme: "real-world-architecture",
|
|
1931
2150
|
"global-styles": "global-css",
|
|
1932
|
-
|
|
2151
|
+
global: "global-css",
|
|
1933
2152
|
"css-variables": "global-css",
|
|
1934
2153
|
"custom-properties": "global-css",
|
|
1935
2154
|
};
|
|
1936
2155
|
const resolvedTopic = topicAliases[topicLower] || topicLower;
|
|
1937
2156
|
// Topics that involve MobX store reads get a reminder about root reactivity
|
|
1938
2157
|
const storeTopics = new Set([
|
|
1939
|
-
"product-detail-patterns",
|
|
1940
|
-
"
|
|
1941
|
-
"
|
|
1942
|
-
"
|
|
2158
|
+
"product-detail-patterns",
|
|
2159
|
+
"product-list-patterns",
|
|
2160
|
+
"cart-patterns",
|
|
2161
|
+
"account-patterns",
|
|
2162
|
+
"header-footer-patterns",
|
|
2163
|
+
"review-patterns",
|
|
2164
|
+
"blog-patterns",
|
|
2165
|
+
"form-handling",
|
|
2166
|
+
"async-data-patterns",
|
|
2167
|
+
"component-structure",
|
|
2168
|
+
"imports",
|
|
1943
2169
|
]);
|
|
1944
2170
|
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
2171
|
// Try exact key match (with alias resolution)
|
|
1946
2172
|
if (frameworkData.topics[resolvedTopic]) {
|
|
1947
2173
|
const t = frameworkData.topics[resolvedTopic];
|
|
1948
2174
|
const prefix = storeTopics.has(resolvedTopic) ? observerReminder : "";
|
|
1949
|
-
return {
|
|
2175
|
+
return {
|
|
2176
|
+
content: [
|
|
2177
|
+
{
|
|
2178
|
+
type: "text",
|
|
2179
|
+
text: `## ${t.title}\n\n${prefix}${t.content}`,
|
|
2180
|
+
},
|
|
2181
|
+
],
|
|
2182
|
+
};
|
|
1950
2183
|
}
|
|
1951
2184
|
// Try original topic key (without alias) in case it's a direct key
|
|
1952
2185
|
if (resolvedTopic !== topicLower && frameworkData.topics[topicLower]) {
|
|
1953
2186
|
const t = frameworkData.topics[topicLower];
|
|
1954
2187
|
const prefix = storeTopics.has(topicLower) ? observerReminder : "";
|
|
1955
|
-
return {
|
|
2188
|
+
return {
|
|
2189
|
+
content: [
|
|
2190
|
+
{
|
|
2191
|
+
type: "text",
|
|
2192
|
+
text: `## ${t.title}\n\n${prefix}${t.content}`,
|
|
2193
|
+
},
|
|
2194
|
+
],
|
|
2195
|
+
};
|
|
1956
2196
|
}
|
|
1957
2197
|
// Try keyword search
|
|
1958
2198
|
const matches = searchFrameworkTopics(topic);
|
|
@@ -1960,7 +2200,12 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
|
|
|
1960
2200
|
const best = matches[0];
|
|
1961
2201
|
const prefix = storeTopics.has(best.key) ? observerReminder : "";
|
|
1962
2202
|
return {
|
|
1963
|
-
content: [
|
|
2203
|
+
content: [
|
|
2204
|
+
{
|
|
2205
|
+
type: "text",
|
|
2206
|
+
text: `## ${best.topic.title}\n\n${prefix}${best.topic.content}`,
|
|
2207
|
+
},
|
|
2208
|
+
],
|
|
1964
2209
|
};
|
|
1965
2210
|
}
|
|
1966
2211
|
const available = Object.entries(frameworkData.topics)
|
|
@@ -1976,16 +2221,27 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
|
|
|
1976
2221
|
};
|
|
1977
2222
|
});
|
|
1978
2223
|
// 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.", {
|
|
2224
|
+
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.", {
|
|
2225
|
+
name: z
|
|
2226
|
+
.string()
|
|
2227
|
+
.describe("Type or enum name (e.g. 'IkasProduct', 'IkasOrderStatus')"),
|
|
2228
|
+
}, async ({ name }) => {
|
|
1980
2229
|
if (!typesData) {
|
|
1981
2230
|
return {
|
|
1982
|
-
content: [
|
|
2231
|
+
content: [
|
|
2232
|
+
{
|
|
2233
|
+
type: "text",
|
|
2234
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2235
|
+
},
|
|
2236
|
+
],
|
|
1983
2237
|
};
|
|
1984
2238
|
}
|
|
1985
2239
|
const nameLower = name.toLowerCase();
|
|
1986
2240
|
const td = typesData.types.find((t) => t.name.toLowerCase() === nameLower);
|
|
1987
2241
|
if (td) {
|
|
1988
|
-
return {
|
|
2242
|
+
return {
|
|
2243
|
+
content: [{ type: "text", text: formatTypeDefinition(td) }],
|
|
2244
|
+
};
|
|
1989
2245
|
}
|
|
1990
2246
|
// Fuzzy match
|
|
1991
2247
|
const matches = typesData.types.filter((t) => t.name.toLowerCase().includes(nameLower));
|
|
@@ -2001,14 +2257,28 @@ server.tool("get_type_definition", "Get the full definition of a storefront type
|
|
|
2001
2257
|
};
|
|
2002
2258
|
}
|
|
2003
2259
|
return {
|
|
2004
|
-
content: [
|
|
2260
|
+
content: [
|
|
2261
|
+
{
|
|
2262
|
+
type: "text",
|
|
2263
|
+
text: `Type "${name}" not found. Use \`list_types()\` to see all available types.`,
|
|
2264
|
+
},
|
|
2265
|
+
],
|
|
2005
2266
|
};
|
|
2006
2267
|
});
|
|
2007
2268
|
// 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.", {
|
|
2269
|
+
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.", {
|
|
2270
|
+
typeName: z
|
|
2271
|
+
.string()
|
|
2272
|
+
.describe("Type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
|
|
2273
|
+
}, async ({ typeName }) => {
|
|
2009
2274
|
if (!typesData) {
|
|
2010
2275
|
return {
|
|
2011
|
-
content: [
|
|
2276
|
+
content: [
|
|
2277
|
+
{
|
|
2278
|
+
type: "text",
|
|
2279
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2280
|
+
},
|
|
2281
|
+
],
|
|
2012
2282
|
};
|
|
2013
2283
|
}
|
|
2014
2284
|
const nameLower = typeName.toLowerCase();
|
|
@@ -2028,7 +2298,12 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
|
|
|
2028
2298
|
};
|
|
2029
2299
|
}
|
|
2030
2300
|
return {
|
|
2031
|
-
content: [
|
|
2301
|
+
content: [
|
|
2302
|
+
{
|
|
2303
|
+
type: "text",
|
|
2304
|
+
text: `Type "${typeName}" not found. Use \`list_types()\` to see all available types.`,
|
|
2305
|
+
},
|
|
2306
|
+
],
|
|
2032
2307
|
};
|
|
2033
2308
|
}
|
|
2034
2309
|
if (!td.relatedFunctions || td.relatedFunctions.length === 0) {
|
|
@@ -2059,7 +2334,10 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
|
|
|
2059
2334
|
});
|
|
2060
2335
|
// Tool: get_model_guide
|
|
2061
2336
|
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
|
|
2337
|
+
model: z
|
|
2338
|
+
.string()
|
|
2339
|
+
.optional()
|
|
2340
|
+
.describe("Model type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
|
|
2063
2341
|
name: z.string().optional().describe("Alias for 'model'"),
|
|
2064
2342
|
mode: z
|
|
2065
2343
|
.enum(["summary", "full"])
|
|
@@ -2070,12 +2348,22 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
2070
2348
|
const model = modelParam || nameParam;
|
|
2071
2349
|
if (!model) {
|
|
2072
2350
|
return {
|
|
2073
|
-
content: [
|
|
2351
|
+
content: [
|
|
2352
|
+
{
|
|
2353
|
+
type: "text",
|
|
2354
|
+
text: "Please provide a model name (e.g. 'IkasProduct', 'IkasOrder'). Use the 'model' or 'name' parameter.",
|
|
2355
|
+
},
|
|
2356
|
+
],
|
|
2074
2357
|
};
|
|
2075
2358
|
}
|
|
2076
2359
|
if (!typesData) {
|
|
2077
2360
|
return {
|
|
2078
|
-
content: [
|
|
2361
|
+
content: [
|
|
2362
|
+
{
|
|
2363
|
+
type: "text",
|
|
2364
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2365
|
+
},
|
|
2366
|
+
],
|
|
2079
2367
|
};
|
|
2080
2368
|
}
|
|
2081
2369
|
const modelLower = model.toLowerCase();
|
|
@@ -2097,7 +2385,12 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
2097
2385
|
};
|
|
2098
2386
|
}
|
|
2099
2387
|
return {
|
|
2100
|
-
content: [
|
|
2388
|
+
content: [
|
|
2389
|
+
{
|
|
2390
|
+
type: "text",
|
|
2391
|
+
text: `Model "${model}" not found. Use \`list_types()\` to see all available types.`,
|
|
2392
|
+
},
|
|
2393
|
+
],
|
|
2101
2394
|
};
|
|
2102
2395
|
}
|
|
2103
2396
|
const parts = [`# Model Guide: ${td.name}\n`];
|
|
@@ -2205,13 +2498,23 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
2205
2498
|
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
2499
|
if (!typesData) {
|
|
2207
2500
|
return {
|
|
2208
|
-
content: [
|
|
2501
|
+
content: [
|
|
2502
|
+
{
|
|
2503
|
+
type: "text",
|
|
2504
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2505
|
+
},
|
|
2506
|
+
],
|
|
2209
2507
|
};
|
|
2210
2508
|
}
|
|
2211
2509
|
const results = searchTypes(query).slice(0, 15);
|
|
2212
2510
|
if (results.length === 0) {
|
|
2213
2511
|
return {
|
|
2214
|
-
content: [
|
|
2512
|
+
content: [
|
|
2513
|
+
{
|
|
2514
|
+
type: "text",
|
|
2515
|
+
text: `No types found matching "${query}". Use \`list_types()\` to see all available types.`,
|
|
2516
|
+
},
|
|
2517
|
+
],
|
|
2215
2518
|
};
|
|
2216
2519
|
}
|
|
2217
2520
|
const parts = [`## Type Search Results for "${query}"\n`];
|
|
@@ -2275,7 +2578,7 @@ server.tool("list_types", "List storefront types and enums. Use `domain` and/or
|
|
|
2275
2578
|
for (const [d, count] of sorted) {
|
|
2276
2579
|
lines.push(`- \`${d}\` (${count})`);
|
|
2277
2580
|
}
|
|
2278
|
-
lines.push("",
|
|
2581
|
+
lines.push("", 'Call `list_types(domain: "<name>")` to see summaries for a domain.');
|
|
2279
2582
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2280
2583
|
}
|
|
2281
2584
|
const domainLower = domain.toLowerCase();
|
|
@@ -2318,11 +2621,21 @@ server.tool("get_prop_types", "Get all available ikas.config.json prop types wit
|
|
|
2318
2621
|
const propTypesTopic = frameworkData.topics["prop-types"];
|
|
2319
2622
|
if (propTypesTopic) {
|
|
2320
2623
|
return {
|
|
2321
|
-
content: [
|
|
2624
|
+
content: [
|
|
2625
|
+
{
|
|
2626
|
+
type: "text",
|
|
2627
|
+
text: `## ${propTypesTopic.title}\n\n${propTypesTopic.content}`,
|
|
2628
|
+
},
|
|
2629
|
+
],
|
|
2322
2630
|
};
|
|
2323
2631
|
}
|
|
2324
2632
|
return {
|
|
2325
|
-
content: [
|
|
2633
|
+
content: [
|
|
2634
|
+
{
|
|
2635
|
+
type: "text",
|
|
2636
|
+
text: "Prop types documentation not available.",
|
|
2637
|
+
},
|
|
2638
|
+
],
|
|
2326
2639
|
};
|
|
2327
2640
|
});
|
|
2328
2641
|
// Tool: get_section_template
|
|
@@ -2419,7 +2732,8 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2419
2732
|
// the end of the response in the existing recipe-builder block.
|
|
2420
2733
|
{
|
|
2421
2734
|
const snippetStrForBanner = bundle.rootFiles["ikas-config-snippet.json"];
|
|
2422
|
-
if (snippetStrForBanner &&
|
|
2735
|
+
if (snippetStrForBanner &&
|
|
2736
|
+
/<id-of-[A-Za-z0-9_]+>/.test(snippetStrForBanner)) {
|
|
2423
2737
|
const childMatches = Array.from(snippetStrForBanner.matchAll(/<id-of-([A-Za-z0-9_]+)>/g));
|
|
2424
2738
|
const uniqueChildren = Array.from(new Set(childMatches.map((m) => m[1])));
|
|
2425
2739
|
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 +2857,10 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2543
2857
|
try {
|
|
2544
2858
|
const childSnippet = JSON.parse(fs.readFileSync(childSnippetPath, "utf-8"));
|
|
2545
2859
|
const childProps = (childSnippet.props || []).map((p) => {
|
|
2546
|
-
const out = {
|
|
2860
|
+
const out = {
|
|
2861
|
+
name: p.name,
|
|
2862
|
+
type: p.type,
|
|
2863
|
+
};
|
|
2547
2864
|
if (p.displayName)
|
|
2548
2865
|
out.displayName = p.displayName;
|
|
2549
2866
|
if (p.required)
|
|
@@ -2616,10 +2933,7 @@ server.tool("get_section_child", "Fetch one item's files from a section's childr
|
|
|
2616
2933
|
.string()
|
|
2617
2934
|
.optional()
|
|
2618
2935
|
.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`"),
|
|
2936
|
+
child: z.string().optional().describe("Alias for `name`"),
|
|
2623
2937
|
kind: z
|
|
2624
2938
|
.enum(["children", "components", "sub-components"])
|
|
2625
2939
|
.optional()
|
|
@@ -2782,7 +3096,11 @@ server.tool("list_section_types", "List all available `get_section_template` sec
|
|
|
2782
3096
|
});
|
|
2783
3097
|
// --- Migration tools ---
|
|
2784
3098
|
// 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.", {
|
|
3099
|
+
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.", {
|
|
3100
|
+
theme_json: z
|
|
3101
|
+
.string()
|
|
3102
|
+
.describe("The raw JSON content of the old theme.json file"),
|
|
3103
|
+
}, async ({ theme_json }) => {
|
|
2786
3104
|
try {
|
|
2787
3105
|
const parsed = JSON.parse(theme_json);
|
|
2788
3106
|
const analysis = analyzeOldTheme(parsed);
|
|
@@ -2790,102 +3108,162 @@ server.tool("analyze_old_theme", "Analyze an old ikas storefront theme.json and
|
|
|
2790
3108
|
}
|
|
2791
3109
|
catch (err) {
|
|
2792
3110
|
return {
|
|
2793
|
-
content: [
|
|
3111
|
+
content: [
|
|
3112
|
+
{
|
|
3113
|
+
type: "text",
|
|
3114
|
+
text: `Error parsing theme.json: ${err instanceof Error ? err.message : String(err)}. Make sure you're passing valid JSON.`,
|
|
3115
|
+
},
|
|
3116
|
+
],
|
|
2794
3117
|
};
|
|
2795
3118
|
}
|
|
2796
3119
|
});
|
|
2797
3120
|
// Tool: get_migration_guide
|
|
2798
3121
|
const migrationTopicAliases = {
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
3122
|
+
overview: "migration-overview",
|
|
3123
|
+
migrate: "migration-overview",
|
|
3124
|
+
custom: "custom-data-conversion",
|
|
2802
3125
|
"custom-data": "custom-data-conversion",
|
|
2803
|
-
|
|
3126
|
+
customdata: "custom-data-conversion",
|
|
2804
3127
|
"dynamic-list": "custom-data-conversion",
|
|
2805
3128
|
"component-list": "custom-data-conversion",
|
|
2806
|
-
|
|
2807
|
-
|
|
3129
|
+
slider: "prop-type-mapping",
|
|
3130
|
+
props: "prop-type-mapping",
|
|
2808
3131
|
"prop-mapping": "prop-type-mapping",
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
3132
|
+
types: "prop-type-mapping",
|
|
3133
|
+
react: "react-to-preact",
|
|
3134
|
+
preact: "react-to-preact",
|
|
3135
|
+
observer: "react-to-preact",
|
|
3136
|
+
libraries: "library-replacements",
|
|
3137
|
+
swiper: "library-replacements",
|
|
3138
|
+
headlessui: "library-replacements",
|
|
3139
|
+
tailwind: "library-replacements",
|
|
3140
|
+
tailwindcss: "library-replacements",
|
|
3141
|
+
recharts: "library-replacements",
|
|
3142
|
+
marquee: "library-replacements",
|
|
3143
|
+
imports: "storefront-import-mapping",
|
|
3144
|
+
storefront: "storefront-import-mapping",
|
|
2822
3145
|
"bp-storefront": "storefront-import-mapping",
|
|
2823
3146
|
"theme-json": "theme-json-anatomy",
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
3147
|
+
anatomy: "theme-json-anatomy",
|
|
3148
|
+
decompose: "component-decomposition-strategy",
|
|
3149
|
+
decomposition: "component-decomposition-strategy",
|
|
3150
|
+
strategy: "component-decomposition-strategy",
|
|
3151
|
+
project: "complete-project-generation",
|
|
3152
|
+
generate: "complete-project-generation",
|
|
3153
|
+
generation: "complete-project-generation",
|
|
3154
|
+
settings: "settings-conversion",
|
|
3155
|
+
colors: "settings-conversion",
|
|
3156
|
+
fonts: "settings-conversion",
|
|
3157
|
+
find: "finding-new-system-equivalents",
|
|
3158
|
+
search: "finding-new-system-equivalents",
|
|
3159
|
+
discover: "finding-new-system-equivalents",
|
|
3160
|
+
equivalent: "finding-new-system-equivalents",
|
|
3161
|
+
equivalents: "finding-new-system-equivalents",
|
|
3162
|
+
replacement: "finding-new-system-equivalents",
|
|
2840
3163
|
};
|
|
2841
3164
|
const migrationTopicKeys = migrationData
|
|
2842
3165
|
? Object.keys(migrationData.topics)
|
|
2843
3166
|
: [];
|
|
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.`, {
|
|
3167
|
+
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.`, {
|
|
3168
|
+
topic: z
|
|
3169
|
+
.string()
|
|
3170
|
+
.describe("Migration topic key, alias, or 'list' to see all topics"),
|
|
3171
|
+
}, async ({ topic }) => {
|
|
2845
3172
|
if (!migrationData) {
|
|
2846
|
-
return {
|
|
3173
|
+
return {
|
|
3174
|
+
content: [
|
|
3175
|
+
{
|
|
3176
|
+
type: "text",
|
|
3177
|
+
text: "Migration data not available. Ensure data/migration.json exists.",
|
|
3178
|
+
},
|
|
3179
|
+
],
|
|
3180
|
+
};
|
|
2847
3181
|
}
|
|
2848
3182
|
if (topic.toLowerCase() === "list") {
|
|
2849
3183
|
const available = Object.entries(migrationData.topics)
|
|
2850
3184
|
.map(([key, t]) => `- \`${key}\` — ${t.title}: ${t.description}`)
|
|
2851
3185
|
.join("\n");
|
|
2852
|
-
return {
|
|
3186
|
+
return {
|
|
3187
|
+
content: [
|
|
3188
|
+
{
|
|
3189
|
+
type: "text",
|
|
3190
|
+
text: `## Available Migration Topics\n\n${available}`,
|
|
3191
|
+
},
|
|
3192
|
+
],
|
|
3193
|
+
};
|
|
2853
3194
|
}
|
|
2854
3195
|
const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
|
|
2855
3196
|
const resolvedTopic = migrationTopicAliases[topicLower] || topicLower;
|
|
2856
3197
|
if (migrationData.topics[resolvedTopic]) {
|
|
2857
3198
|
const t = migrationData.topics[resolvedTopic];
|
|
2858
|
-
return {
|
|
3199
|
+
return {
|
|
3200
|
+
content: [
|
|
3201
|
+
{ type: "text", text: `## ${t.title}\n\n${t.content}` },
|
|
3202
|
+
],
|
|
3203
|
+
};
|
|
2859
3204
|
}
|
|
2860
3205
|
// Try original key
|
|
2861
3206
|
if (resolvedTopic !== topicLower && migrationData.topics[topicLower]) {
|
|
2862
3207
|
const t = migrationData.topics[topicLower];
|
|
2863
|
-
return {
|
|
3208
|
+
return {
|
|
3209
|
+
content: [
|
|
3210
|
+
{ type: "text", text: `## ${t.title}\n\n${t.content}` },
|
|
3211
|
+
],
|
|
3212
|
+
};
|
|
2864
3213
|
}
|
|
2865
3214
|
// Keyword search
|
|
2866
3215
|
const matches = searchMigrationTopics(topic);
|
|
2867
3216
|
if (matches.length > 0) {
|
|
2868
3217
|
const best = matches[0];
|
|
2869
|
-
return {
|
|
3218
|
+
return {
|
|
3219
|
+
content: [
|
|
3220
|
+
{
|
|
3221
|
+
type: "text",
|
|
3222
|
+
text: `## ${best.topic.title}\n\n${best.topic.content}`,
|
|
3223
|
+
},
|
|
3224
|
+
],
|
|
3225
|
+
};
|
|
2870
3226
|
}
|
|
2871
3227
|
const available = Object.entries(migrationData.topics)
|
|
2872
3228
|
.map(([key, t]) => ` - \`${key}\` - ${t.title}`)
|
|
2873
3229
|
.join("\n");
|
|
2874
3230
|
return {
|
|
2875
|
-
content: [
|
|
3231
|
+
content: [
|
|
3232
|
+
{
|
|
3233
|
+
type: "text",
|
|
3234
|
+
text: `Migration topic "${topic}" not found. Available topics:\n${available}`,
|
|
3235
|
+
},
|
|
3236
|
+
],
|
|
2876
3237
|
};
|
|
2877
3238
|
});
|
|
2878
3239
|
// 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.`, {
|
|
3240
|
+
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.`, {
|
|
3241
|
+
example: z.string().describe("Example name or 'list' to see all examples"),
|
|
3242
|
+
}, async ({ example }) => {
|
|
2880
3243
|
if (example.toLowerCase() === "list") {
|
|
2881
3244
|
if (migrationExampleNames.length === 0) {
|
|
2882
|
-
return {
|
|
3245
|
+
return {
|
|
3246
|
+
content: [
|
|
3247
|
+
{ type: "text", text: "No migration examples available." },
|
|
3248
|
+
],
|
|
3249
|
+
};
|
|
2883
3250
|
}
|
|
2884
|
-
const list = migrationExampleNames
|
|
3251
|
+
const list = migrationExampleNames
|
|
3252
|
+
.map((name) => {
|
|
2885
3253
|
const ex = loadMigrationExample(name);
|
|
2886
|
-
return ex
|
|
2887
|
-
|
|
2888
|
-
|
|
3254
|
+
return ex
|
|
3255
|
+
? `- \`${name}\` — ${ex.title}: ${ex.description}`
|
|
3256
|
+
: `- \`${name}\``;
|
|
3257
|
+
})
|
|
3258
|
+
.join("\n");
|
|
3259
|
+
return {
|
|
3260
|
+
content: [
|
|
3261
|
+
{
|
|
3262
|
+
type: "text",
|
|
3263
|
+
text: `## Available Migration Examples\n\n${list}`,
|
|
3264
|
+
},
|
|
3265
|
+
],
|
|
3266
|
+
};
|
|
2889
3267
|
}
|
|
2890
3268
|
const exampleLower = example.toLowerCase();
|
|
2891
3269
|
let exName = migrationExampleNames.find((n) => n === exampleLower);
|
|
@@ -2895,19 +3273,26 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
|
|
|
2895
3273
|
if (!exName) {
|
|
2896
3274
|
const available = migrationExampleNames.join(", ");
|
|
2897
3275
|
return {
|
|
2898
|
-
content: [
|
|
3276
|
+
content: [
|
|
3277
|
+
{
|
|
3278
|
+
type: "text",
|
|
3279
|
+
text: `Migration example "${example}" not found. Available: ${available}`,
|
|
3280
|
+
},
|
|
3281
|
+
],
|
|
2899
3282
|
};
|
|
2900
3283
|
}
|
|
2901
3284
|
const ex = loadMigrationExample(exName);
|
|
2902
3285
|
if (!ex) {
|
|
2903
|
-
return {
|
|
3286
|
+
return {
|
|
3287
|
+
content: [
|
|
3288
|
+
{
|
|
3289
|
+
type: "text",
|
|
3290
|
+
text: `Failed to load migration example "${exName}".`,
|
|
3291
|
+
},
|
|
3292
|
+
],
|
|
3293
|
+
};
|
|
2904
3294
|
}
|
|
2905
|
-
const parts = [
|
|
2906
|
-
`## ${ex.title}`,
|
|
2907
|
-
"",
|
|
2908
|
-
ex.description,
|
|
2909
|
-
"",
|
|
2910
|
-
];
|
|
3295
|
+
const parts = [`## ${ex.title}`, "", ex.description, ""];
|
|
2911
3296
|
for (const [filename, content] of Object.entries(ex.files)) {
|
|
2912
3297
|
const ext = filename.split(".").pop() || "text";
|
|
2913
3298
|
const lang = ext === "tsx" || ext === "ts"
|
|
@@ -2926,13 +3311,31 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
|
|
|
2926
3311
|
});
|
|
2927
3312
|
// Tool: plan_migration
|
|
2928
3313
|
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
|
-
|
|
3314
|
+
theme_json: z
|
|
3315
|
+
.string()
|
|
3316
|
+
.optional()
|
|
3317
|
+
.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."),
|
|
3318
|
+
theme_json_path: z
|
|
3319
|
+
.string()
|
|
3320
|
+
.optional()
|
|
3321
|
+
.describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
|
|
3322
|
+
project_name: z
|
|
3323
|
+
.string()
|
|
3324
|
+
.optional()
|
|
3325
|
+
.describe("Target new project name, used to prefix migration-tracking IDs (default: 'my-theme')"),
|
|
3326
|
+
old_source_dir: z
|
|
3327
|
+
.string()
|
|
3328
|
+
.optional()
|
|
3329
|
+
.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."),
|
|
3330
|
+
project_root: z
|
|
3331
|
+
.string()
|
|
3332
|
+
.optional()
|
|
3333
|
+
.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."),
|
|
3334
|
+
overwrite: z
|
|
3335
|
+
.boolean()
|
|
3336
|
+
.optional()
|
|
3337
|
+
.describe("If MIGRATION.md already exists at <project_root>/MIGRATION.md and is non-empty, refuse the write unless this is true. Default: false."),
|
|
3338
|
+
}, async ({ theme_json, theme_json_path, project_name, old_source_dir, project_root, overwrite, }) => {
|
|
2936
3339
|
try {
|
|
2937
3340
|
const parsed = resolveThemeJson(theme_json, theme_json_path);
|
|
2938
3341
|
const projectName = project_name || "my-theme";
|
|
@@ -2993,18 +3396,37 @@ server.tool("plan_migration", "Generate the **initial** migration plan and (when
|
|
|
2993
3396
|
}
|
|
2994
3397
|
catch (err) {
|
|
2995
3398
|
return {
|
|
2996
|
-
content: [
|
|
3399
|
+
content: [
|
|
3400
|
+
{
|
|
3401
|
+
type: "text",
|
|
3402
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
3403
|
+
},
|
|
3404
|
+
],
|
|
2997
3405
|
};
|
|
2998
3406
|
}
|
|
2999
3407
|
});
|
|
3000
3408
|
// Tool: get_section_migration_plan
|
|
3001
3409
|
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
|
-
|
|
3410
|
+
theme_json: z
|
|
3411
|
+
.string()
|
|
3412
|
+
.optional()
|
|
3413
|
+
.describe("Raw JSON content of the old theme.json. EITHER this OR theme_json_path is required (not both)."),
|
|
3414
|
+
theme_json_path: z
|
|
3415
|
+
.string()
|
|
3416
|
+
.optional()
|
|
3417
|
+
.describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
|
|
3418
|
+
section_name: z
|
|
3419
|
+
.string()
|
|
3420
|
+
.describe("Old component name (e.g. 'Navbar', 'ProductGrid') or dir name, OR the new section ID (e.g. 'my-theme-navbar')"),
|
|
3421
|
+
project_name: z
|
|
3422
|
+
.string()
|
|
3423
|
+
.optional()
|
|
3424
|
+
.describe("Target new project name (must match what was used in plan_migration). Default: 'my-theme'"),
|
|
3425
|
+
old_source_dir: z
|
|
3426
|
+
.string()
|
|
3427
|
+
.optional()
|
|
3428
|
+
.describe("Absolute path to old src/ directory (used to output exact source file paths to read)"),
|
|
3429
|
+
}, async ({ theme_json, theme_json_path, section_name, project_name, old_source_dir, }) => {
|
|
3008
3430
|
try {
|
|
3009
3431
|
const parsed = resolveThemeJson(theme_json, theme_json_path);
|
|
3010
3432
|
const projectName = project_name || "my-theme";
|
|
@@ -3013,7 +3435,12 @@ server.tool("get_section_migration_plan", "Returns concrete CLI commands and pro
|
|
|
3013
3435
|
}
|
|
3014
3436
|
catch (err) {
|
|
3015
3437
|
return {
|
|
3016
|
-
content: [
|
|
3438
|
+
content: [
|
|
3439
|
+
{
|
|
3440
|
+
type: "text",
|
|
3441
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
3442
|
+
},
|
|
3443
|
+
],
|
|
3017
3444
|
};
|
|
3018
3445
|
}
|
|
3019
3446
|
});
|
|
@@ -3049,7 +3476,11 @@ async function runIkasComponentCli(projectRoot, args) {
|
|
|
3049
3476
|
: err
|
|
3050
3477
|
? 1
|
|
3051
3478
|
: 0;
|
|
3052
|
-
resolve({
|
|
3479
|
+
resolve({
|
|
3480
|
+
stdout: stdout?.toString() ?? "",
|
|
3481
|
+
stderr: stderr?.toString() ?? "",
|
|
3482
|
+
exitCode,
|
|
3483
|
+
});
|
|
3053
3484
|
});
|
|
3054
3485
|
});
|
|
3055
3486
|
}
|
|
@@ -3059,7 +3490,11 @@ function parseCliJson(stdout) {
|
|
|
3059
3490
|
return null;
|
|
3060
3491
|
// CLI prints exactly one JSON object on stdout; if multiple lines appeared
|
|
3061
3492
|
// (e.g., warnings on stderr leaked), take the last non-empty line.
|
|
3062
|
-
const last = trimmed
|
|
3493
|
+
const last = trimmed
|
|
3494
|
+
.split("\n")
|
|
3495
|
+
.map((l) => l.trim())
|
|
3496
|
+
.filter(Boolean)
|
|
3497
|
+
.pop();
|
|
3063
3498
|
if (!last)
|
|
3064
3499
|
return null;
|
|
3065
3500
|
try {
|
|
@@ -3074,7 +3509,11 @@ async function callEditorAction(projectRoot, args) {
|
|
|
3074
3509
|
const { stdout, stderr, exitCode } = await runIkasComponentCli(projectRoot, args);
|
|
3075
3510
|
const parsed = parseCliJson(stdout);
|
|
3076
3511
|
if (parsed) {
|
|
3077
|
-
return {
|
|
3512
|
+
return {
|
|
3513
|
+
content: [
|
|
3514
|
+
{ type: "text", text: JSON.stringify(parsed, null, 2) },
|
|
3515
|
+
],
|
|
3516
|
+
};
|
|
3078
3517
|
}
|
|
3079
3518
|
return {
|
|
3080
3519
|
content: [
|
|
@@ -3097,38 +3536,77 @@ async function callEditorAction(projectRoot, args) {
|
|
|
3097
3536
|
};
|
|
3098
3537
|
}
|
|
3099
3538
|
}
|
|
3539
|
+
//
|
|
3100
3540
|
// Tool: list_editor_pages
|
|
3101
3541
|
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
|
-
|
|
3542
|
+
project_root: z
|
|
3543
|
+
.string()
|
|
3544
|
+
.describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
|
|
3545
|
+
port: z
|
|
3546
|
+
.number()
|
|
3547
|
+
.optional()
|
|
3548
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3104
3549
|
}, async ({ project_root, port }) => {
|
|
3105
3550
|
const args = ["list-pages", ...(port ? ["--port", String(port)] : [])];
|
|
3106
3551
|
return callEditorAction(project_root, args);
|
|
3107
3552
|
});
|
|
3108
3553
|
// Tool: list_imported_sections
|
|
3109
3554
|
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
|
-
|
|
3555
|
+
project_root: z
|
|
3556
|
+
.string()
|
|
3557
|
+
.describe("Absolute path to the code-component project."),
|
|
3558
|
+
port: z
|
|
3559
|
+
.number()
|
|
3560
|
+
.optional()
|
|
3561
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3112
3562
|
}, async ({ project_root, port }) => {
|
|
3113
|
-
const args = [
|
|
3563
|
+
const args = [
|
|
3564
|
+
"list-imported",
|
|
3565
|
+
"--sections-only",
|
|
3566
|
+
...(port ? ["--port", String(port)] : []),
|
|
3567
|
+
];
|
|
3114
3568
|
return callEditorAction(project_root, args);
|
|
3115
3569
|
});
|
|
3116
3570
|
// Tool: import_section
|
|
3117
3571
|
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
|
-
|
|
3572
|
+
project_root: z
|
|
3573
|
+
.string()
|
|
3574
|
+
.describe("Absolute path to the code-component project."),
|
|
3575
|
+
component_id: z
|
|
3576
|
+
.string()
|
|
3577
|
+
.describe("Component id from `ikas.config.json` (strict — no name resolution)."),
|
|
3578
|
+
port: z
|
|
3579
|
+
.number()
|
|
3580
|
+
.optional()
|
|
3581
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3121
3582
|
}, async ({ project_root, component_id, port }) => {
|
|
3122
|
-
const args = [
|
|
3583
|
+
const args = [
|
|
3584
|
+
"import",
|
|
3585
|
+
"--id",
|
|
3586
|
+
component_id,
|
|
3587
|
+
...(port ? ["--port", String(port)] : []),
|
|
3588
|
+
];
|
|
3123
3589
|
return callEditorAction(project_root, args);
|
|
3124
3590
|
});
|
|
3125
3591
|
// Tool: add_section_to_page
|
|
3126
|
-
server.tool("add_section_to_page",
|
|
3127
|
-
project_root: z
|
|
3128
|
-
|
|
3592
|
+
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.', {
|
|
3593
|
+
project_root: z
|
|
3594
|
+
.string()
|
|
3595
|
+
.describe("Absolute path to the code-component project."),
|
|
3596
|
+
component_id: z
|
|
3597
|
+
.string()
|
|
3598
|
+
.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
3599
|
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3130
|
-
index: z
|
|
3131
|
-
|
|
3600
|
+
index: z
|
|
3601
|
+
.number()
|
|
3602
|
+
.int()
|
|
3603
|
+
.nonnegative()
|
|
3604
|
+
.optional()
|
|
3605
|
+
.describe("Zero-based insertion index in the page; appends when omitted."),
|
|
3606
|
+
port: z
|
|
3607
|
+
.number()
|
|
3608
|
+
.optional()
|
|
3609
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3132
3610
|
}, async ({ project_root, component_id, page_id, index, port }) => {
|
|
3133
3611
|
const args = [
|
|
3134
3612
|
"add-to-page",
|
|
@@ -3141,6 +3619,64 @@ server.tool("add_section_to_page", "Place an already-imported section-type code
|
|
|
3141
3619
|
];
|
|
3142
3620
|
return callEditorAction(project_root, args);
|
|
3143
3621
|
});
|
|
3622
|
+
// Tool: list_page_sections
|
|
3623
|
+
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.", {
|
|
3624
|
+
project_root: z
|
|
3625
|
+
.string()
|
|
3626
|
+
.describe("Absolute path to the code-component project."),
|
|
3627
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3628
|
+
port: z
|
|
3629
|
+
.number()
|
|
3630
|
+
.optional()
|
|
3631
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3632
|
+
}, async ({ project_root, page_id, port }) => {
|
|
3633
|
+
const args = [
|
|
3634
|
+
"list-page-sections",
|
|
3635
|
+
"--page-id",
|
|
3636
|
+
page_id,
|
|
3637
|
+
...(port ? ["--port", String(port)] : []),
|
|
3638
|
+
];
|
|
3639
|
+
return callEditorAction(project_root, args);
|
|
3640
|
+
});
|
|
3641
|
+
// Tool: update_section_prop
|
|
3642
|
+
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.', {
|
|
3643
|
+
project_root: z
|
|
3644
|
+
.string()
|
|
3645
|
+
.describe("Absolute path to the code-component project."),
|
|
3646
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3647
|
+
element_id: z
|
|
3648
|
+
.string()
|
|
3649
|
+
.describe("Placed-section elementId identifying THIS placement on the page (from `list_page_sections`)."),
|
|
3650
|
+
prop_id: z
|
|
3651
|
+
.string()
|
|
3652
|
+
.optional()
|
|
3653
|
+
.describe("Blueprint prop id to update (from `list_page_sections`). Provide this or `prop_name`."),
|
|
3654
|
+
prop_name: z
|
|
3655
|
+
.string()
|
|
3656
|
+
.optional()
|
|
3657
|
+
.describe("Blueprint prop name to update (alternative to `prop_id`)."),
|
|
3658
|
+
value: z
|
|
3659
|
+
.any()
|
|
3660
|
+
.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.'),
|
|
3661
|
+
port: z
|
|
3662
|
+
.number()
|
|
3663
|
+
.optional()
|
|
3664
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3665
|
+
}, async ({ project_root, page_id, element_id, prop_id, prop_name, value, port }) => {
|
|
3666
|
+
const args = [
|
|
3667
|
+
"update-section-prop",
|
|
3668
|
+
"--page-id",
|
|
3669
|
+
page_id,
|
|
3670
|
+
"--element-id",
|
|
3671
|
+
element_id,
|
|
3672
|
+
...(prop_id ? ["--prop-id", prop_id] : []),
|
|
3673
|
+
...(prop_name ? ["--prop-name", prop_name] : []),
|
|
3674
|
+
"--value",
|
|
3675
|
+
JSON.stringify(value),
|
|
3676
|
+
...(port ? ["--port", String(port)] : []),
|
|
3677
|
+
];
|
|
3678
|
+
return callEditorAction(project_root, args);
|
|
3679
|
+
});
|
|
3144
3680
|
// --- Start server ---
|
|
3145
3681
|
async function main() {
|
|
3146
3682
|
const transport = new StdioServerTransport();
|