@ikas/code-components-mcp 1.4.0-beta.31 → 1.4.0-beta.32
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 +239 -1147
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -98,16 +98,9 @@ function loadStorefrontTypes() {
|
|
|
98
98
|
}
|
|
99
99
|
return null;
|
|
100
100
|
}
|
|
101
|
-
const SUBTREE_KINDS = [
|
|
102
|
-
"children",
|
|
103
|
-
"components",
|
|
104
|
-
"sub-components",
|
|
105
|
-
];
|
|
101
|
+
const SUBTREE_KINDS = ["children", "components", "sub-components"];
|
|
106
102
|
function normalizeName(value) {
|
|
107
|
-
return value
|
|
108
|
-
.trim()
|
|
109
|
-
.replace(/^`+|`+$/g, "")
|
|
110
|
-
.trim();
|
|
103
|
+
return value.trim().replace(/^`+|`+$/g, "").trim();
|
|
111
104
|
}
|
|
112
105
|
const storefrontData = loadStorefrontData();
|
|
113
106
|
const frameworkData = loadJsonFile("../data/framework.json");
|
|
@@ -268,11 +261,7 @@ function searchMigrationTopics(query) {
|
|
|
268
261
|
const descScore = matchScore(topic.description, query) * 2;
|
|
269
262
|
const contentScore = matchScore(topic.content, query);
|
|
270
263
|
const tagScore = topic.tags.some((t) => matchScore(t, query) > 0) ? 5 : 0;
|
|
271
|
-
return {
|
|
272
|
-
key,
|
|
273
|
-
topic,
|
|
274
|
-
score: titleScore + descScore + contentScore + tagScore,
|
|
275
|
-
};
|
|
264
|
+
return { key, topic, score: titleScore + descScore + contentScore + tagScore };
|
|
276
265
|
})
|
|
277
266
|
.filter((item) => item.score > 0)
|
|
278
267
|
.sort((a, b) => b.score - a.score);
|
|
@@ -299,7 +288,7 @@ function analyzeOldTheme(themeJson) {
|
|
|
299
288
|
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`);
|
|
300
289
|
parts.push(`## Summary Statistics\n`);
|
|
301
290
|
parts.push(`- **Components:** ${components.length}`);
|
|
302
|
-
parts.push(`- **Custom Data Definitions:** ${customData.filter(
|
|
291
|
+
parts.push(`- **Custom Data Definitions:** ${customData.filter(cd => cd.isRoot).length}`);
|
|
303
292
|
parts.push(`- **Prop Groups:** ${groups.length}`);
|
|
304
293
|
// Component analysis
|
|
305
294
|
parts.push(`\n## Components (${components.length})\n`);
|
|
@@ -338,11 +327,7 @@ function analyzeOldTheme(themeJson) {
|
|
|
338
327
|
const typesSummary = Object.entries(propTypeCounts)
|
|
339
328
|
.map(([t, c]) => `${t}×${c}`)
|
|
340
329
|
.join(", ");
|
|
341
|
-
const headerFooter = comp.isHeader
|
|
342
|
-
? " [HEADER]"
|
|
343
|
-
: comp.isFooter
|
|
344
|
-
? " [FOOTER]"
|
|
345
|
-
: "";
|
|
330
|
+
const headerFooter = comp.isHeader ? " [HEADER]" : comp.isFooter ? " [FOOTER]" : "";
|
|
346
331
|
parts.push(`### ${comp.displayName || comp.dir || comp.id}${headerFooter}`);
|
|
347
332
|
parts.push(`- **Dir:** \`${comp.dir || "?"}\` | **Props:** ${props.length} (${typesSummary})`);
|
|
348
333
|
parts.push(`- **Recommended new type:** section`);
|
|
@@ -357,7 +342,7 @@ function analyzeOldTheme(themeJson) {
|
|
|
357
342
|
parts.push("");
|
|
358
343
|
}
|
|
359
344
|
// Custom data analysis
|
|
360
|
-
const rootCustomData = customData.filter(
|
|
345
|
+
const rootCustomData = customData.filter(cd => cd.isRoot);
|
|
361
346
|
if (rootCustomData.length > 0) {
|
|
362
347
|
parts.push(`\n## Custom Data Definitions (${rootCustomData.length})\n`);
|
|
363
348
|
for (const cd of rootCustomData) {
|
|
@@ -367,9 +352,7 @@ function analyzeOldTheme(themeJson) {
|
|
|
367
352
|
const describeNested = (items, indent) => {
|
|
368
353
|
const lines = [];
|
|
369
354
|
for (const item of items) {
|
|
370
|
-
const key = item.key
|
|
371
|
-
? `\`${item.key}\``
|
|
372
|
-
: item.typescriptName || item.name || "unnamed";
|
|
355
|
+
const key = item.key ? `\`${item.key}\`` : item.typescriptName || item.name || "unnamed";
|
|
373
356
|
lines.push(`${indent}- ${key}: ${item.type}${item.isRequired ? " (required)" : ""}`);
|
|
374
357
|
if (item.nestedData && item.nestedData.length > 0) {
|
|
375
358
|
lines.push(...describeNested(item.nestedData, indent + " "));
|
|
@@ -381,7 +364,7 @@ function analyzeOldTheme(themeJson) {
|
|
|
381
364
|
parts.push(...describeNested(cd.nestedData, " "));
|
|
382
365
|
}
|
|
383
366
|
if (cd.enumOptions && cd.enumOptions.length > 0) {
|
|
384
|
-
parts.push(`- **Enum options:** ${cd.enumOptions.map(
|
|
367
|
+
parts.push(`- **Enum options:** ${cd.enumOptions.map(o => `"${o.value}"`).join(", ")}`);
|
|
385
368
|
}
|
|
386
369
|
// Find which components reference this customData
|
|
387
370
|
const referencingComponents = [];
|
|
@@ -463,9 +446,7 @@ function scanSharedSubcomponents(sourceDir) {
|
|
|
463
446
|
const walk = (dir) => {
|
|
464
447
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
465
448
|
if (entry.isDirectory()) {
|
|
466
|
-
if (entry.name === "node_modules" ||
|
|
467
|
-
entry.name === "__generated__" ||
|
|
468
|
-
entry.name.startsWith("."))
|
|
449
|
+
if (entry.name === "node_modules" || entry.name === "__generated__" || entry.name.startsWith("."))
|
|
469
450
|
continue;
|
|
470
451
|
walk(path.join(dir, entry.name));
|
|
471
452
|
}
|
|
@@ -500,14 +481,10 @@ function scanSharedSubcomponents(sourceDir) {
|
|
|
500
481
|
if (!importPath.startsWith("."))
|
|
501
482
|
continue;
|
|
502
483
|
// Skip imports of generated types, utils, hooks
|
|
503
|
-
if (importPath.includes("__generated__") ||
|
|
504
|
-
importPath.includes("/utils") ||
|
|
505
|
-
importPath.includes("/hooks"))
|
|
484
|
+
if (importPath.includes("__generated__") || importPath.includes("/utils") || importPath.includes("/hooks"))
|
|
506
485
|
continue;
|
|
507
486
|
// Extract base name from path
|
|
508
|
-
const pathSegments = importPath
|
|
509
|
-
.split("/")
|
|
510
|
-
.filter((s) => s && s !== "." && s !== "..");
|
|
487
|
+
const pathSegments = importPath.split("/").filter(s => s && s !== "." && s !== "..");
|
|
511
488
|
if (pathSegments.length === 0)
|
|
512
489
|
continue;
|
|
513
490
|
const lastSegment = pathSegments[pathSegments.length - 1];
|
|
@@ -518,10 +495,7 @@ function scanSharedSubcomponents(sourceDir) {
|
|
|
518
495
|
continue;
|
|
519
496
|
seenInFile.add(key);
|
|
520
497
|
if (!importUsage.has(key)) {
|
|
521
|
-
importUsage.set(key, {
|
|
522
|
-
usingComponents: new Set(),
|
|
523
|
-
rawImportPath: importPath,
|
|
524
|
-
});
|
|
498
|
+
importUsage.set(key, { usingComponents: new Set(), rawImportPath: importPath });
|
|
525
499
|
}
|
|
526
500
|
importUsage.get(key).usingComponents.add(componentDir);
|
|
527
501
|
}
|
|
@@ -530,7 +504,7 @@ function scanSharedSubcomponents(sourceDir) {
|
|
|
530
504
|
const shared = [];
|
|
531
505
|
for (const [name, { usingComponents, rawImportPath }] of importUsage) {
|
|
532
506
|
// Don't flag the component itself (e.g., Navbar imports from ../Navbar/something)
|
|
533
|
-
const users = [...usingComponents].filter(
|
|
507
|
+
const users = [...usingComponents].filter(c => c !== name);
|
|
534
508
|
if (users.length >= 3) {
|
|
535
509
|
shared.push({ name, usedBy: users.sort(), importPaths: [rawImportPath] });
|
|
536
510
|
}
|
|
@@ -547,7 +521,7 @@ function toKebabCase(s) {
|
|
|
547
521
|
}
|
|
548
522
|
function classifyComplexity(comp, customDataMap) {
|
|
549
523
|
const props = comp.props || [];
|
|
550
|
-
const customCount = props.filter(
|
|
524
|
+
const customCount = props.filter(p => p.type === "CUSTOM").length;
|
|
551
525
|
if (customCount === 0 && props.length < 10)
|
|
552
526
|
return "simple";
|
|
553
527
|
// Check for deeply nested CUSTOM (customData referencing another customData)
|
|
@@ -558,9 +532,7 @@ function classifyComplexity(comp, customDataMap) {
|
|
|
558
532
|
if (cd?.nestedData) {
|
|
559
533
|
const hasNested = (items) => {
|
|
560
534
|
for (const item of items) {
|
|
561
|
-
if (item.type === "DYNAMIC_LIST" ||
|
|
562
|
-
item.type === "STATIC_LIST" ||
|
|
563
|
-
item.customDataId)
|
|
535
|
+
if (item.type === "DYNAMIC_LIST" || item.type === "STATIC_LIST" || item.customDataId)
|
|
564
536
|
return true;
|
|
565
537
|
if (item.nestedData && hasNested(item.nestedData))
|
|
566
538
|
return true;
|
|
@@ -592,7 +564,7 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
592
564
|
parts.push(`# Theme Migration Plan — \`${projectName}\``);
|
|
593
565
|
parts.push("");
|
|
594
566
|
parts.push(`**Generated:** ${new Date().toISOString().slice(0, 10)}`);
|
|
595
|
-
parts.push(`**Source:** ${components.length} old components, ${customData.filter(
|
|
567
|
+
parts.push(`**Source:** ${components.length} old components, ${customData.filter(cd => cd.isRoot).length} custom data types, ${(theme.pages || []).length} pages`);
|
|
596
568
|
parts.push("");
|
|
597
569
|
parts.push(`> ## READ THIS FIRST`);
|
|
598
570
|
parts.push(`>`);
|
|
@@ -682,22 +654,16 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
682
654
|
const cdType = cd.type || "?";
|
|
683
655
|
let shape = "";
|
|
684
656
|
if (cd.type === "ENUM") {
|
|
685
|
-
const opts = (cd.enumOptions || [])
|
|
686
|
-
.map((o) => o.value || o.displayName)
|
|
687
|
-
.filter(Boolean);
|
|
657
|
+
const opts = (cd.enumOptions || []).map((o) => o.value || o.displayName).filter(Boolean);
|
|
688
658
|
shape = ` — shape: \`enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}\``;
|
|
689
659
|
}
|
|
690
660
|
else if (cd.nestedData && cd.nestedData.length > 0) {
|
|
691
661
|
const first = cd.nestedData[0];
|
|
692
|
-
const fields = (first?.nestedData || cd.nestedData || [])
|
|
693
|
-
.map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`)
|
|
694
|
-
.slice(0, 8);
|
|
662
|
+
const fields = (first?.nestedData || cd.nestedData || []).map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`).slice(0, 8);
|
|
695
663
|
shape = ` — shape: \`{${fields.join(", ")}}\``;
|
|
696
664
|
}
|
|
697
665
|
const usedBy = cd.id ? usageByCustomDataId.get(cd.id) || [] : [];
|
|
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)_`;
|
|
666
|
+
const usedByStr = usedBy.length > 0 ? ` — used by: ${usedBy.slice(0, 6).join(", ")}${usedBy.length > 6 ? `, +${usedBy.length - 6} more` : ""}` : ` — _not directly referenced by any section's props (may be nested inside another customData)_`;
|
|
701
667
|
parts.push(`- \`${cdName}\` (${cdType})${shape}${usedByStr}`);
|
|
702
668
|
}
|
|
703
669
|
parts.push("");
|
|
@@ -731,11 +697,7 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
731
697
|
const oldName = comp.displayName || comp.dir || comp.id || "Unknown";
|
|
732
698
|
const kebabName = toKebabCase(comp.dir || comp.displayName || comp.id || "unknown");
|
|
733
699
|
const newId = `${projectName}-${kebabName}`;
|
|
734
|
-
const headerFooter = comp.isHeader
|
|
735
|
-
? " **[HEADER]**"
|
|
736
|
-
: comp.isFooter
|
|
737
|
-
? " **[FOOTER]**"
|
|
738
|
-
: "";
|
|
700
|
+
const headerFooter = comp.isHeader ? " **[HEADER]**" : comp.isFooter ? " **[FOOTER]**" : "";
|
|
739
701
|
const propCount = (comp.props || []).length;
|
|
740
702
|
// Detect children from CUSTOM DYNAMIC_LIST props
|
|
741
703
|
const children = [];
|
|
@@ -745,13 +707,8 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
745
707
|
if (cd && (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST")) {
|
|
746
708
|
const itemObj = cd.nestedData?.[0];
|
|
747
709
|
if (itemObj) {
|
|
748
|
-
const childName = itemObj.typescriptName ||
|
|
749
|
-
|
|
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);
|
|
710
|
+
const childName = itemObj.typescriptName || (itemObj.name ? itemObj.name.replace(/[^a-zA-Z0-9]/g, "") : `${oldName}Item`);
|
|
711
|
+
const fields = (itemObj.nestedData || []).map((f) => f.key || f.name || "?").slice(0, 8);
|
|
755
712
|
children.push({ propName: p.name || "?", childName, fields });
|
|
756
713
|
}
|
|
757
714
|
}
|
|
@@ -810,25 +767,11 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
810
767
|
}
|
|
811
768
|
// Known libraries we detect in old themes and want to flag for replacement
|
|
812
769
|
const KNOWN_LIBRARIES = [
|
|
813
|
-
"swiper",
|
|
814
|
-
"
|
|
815
|
-
"
|
|
816
|
-
"
|
|
817
|
-
"react-
|
|
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",
|
|
770
|
+
"swiper", "@headlessui/react", "@heroicons/react", "recharts",
|
|
771
|
+
"react-player", "react-simple-star-rating", "react-slider", "react-compound-slider",
|
|
772
|
+
"react-zoom-pan-pinch", "react-hot-toast", "react-fast-marquee",
|
|
773
|
+
"react-indiana-drag-scroll", "react-simple-typewriter", "react-timer-hook",
|
|
774
|
+
"date-fns", "slugify", "classnames", "clsx", "@react-pdf/renderer",
|
|
832
775
|
];
|
|
833
776
|
// Heuristic: member-access patterns on old storefront stores/singletons that likely need new-system equivalents
|
|
834
777
|
const OLD_STOREFRONT_CALL_REGEX = /\b(customerStore|cartStore|productStore|categoryStore|orderStore|searchStore|favoritesStore|i18nStore|Router|useStore)\.\w+/g;
|
|
@@ -847,8 +790,7 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
847
790
|
// Collect all .tsx/.ts files in the component dir
|
|
848
791
|
try {
|
|
849
792
|
for (const entry of fs.readdirSync(componentDir, { withFileTypes: true })) {
|
|
850
|
-
if (entry.isFile() &&
|
|
851
|
-
(entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
|
|
793
|
+
if (entry.isFile() && (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
|
|
852
794
|
result.sourceFiles.push(path.join(componentDir, entry.name));
|
|
853
795
|
}
|
|
854
796
|
}
|
|
@@ -865,18 +807,7 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
865
807
|
const subCompSet = new Map();
|
|
866
808
|
const callSet = new Set();
|
|
867
809
|
// Packages we treat as "framework" and don't flag for replacement (but do note in reactPackageUsage)
|
|
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
|
-
]);
|
|
810
|
+
const REACT_PACKAGES = new Set(["react", "react-dom", "next", "next/link", "next/image", "next/router", "next/head", "next/script", "mobx-react-lite", "mobx"]);
|
|
880
811
|
for (const file of result.sourceFiles) {
|
|
881
812
|
let content;
|
|
882
813
|
try {
|
|
@@ -890,7 +821,7 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
890
821
|
while ((m = importRegex.exec(content)) !== null) {
|
|
891
822
|
const p = m[1];
|
|
892
823
|
if (p.startsWith(".")) {
|
|
893
|
-
const segs = p.split("/").filter(
|
|
824
|
+
const segs = p.split("/").filter(s => s && s !== "." && s !== "..");
|
|
894
825
|
const last = segs[segs.length - 1];
|
|
895
826
|
if (last && /^[A-Z]/.test(last) && !last.includes("__generated__")) {
|
|
896
827
|
subCompSet.set(last, p);
|
|
@@ -898,9 +829,7 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
898
829
|
}
|
|
899
830
|
else if (!p.startsWith("@ikas/")) {
|
|
900
831
|
// Classify: known library, react-family, or unknown-external
|
|
901
|
-
const base = p.startsWith("@")
|
|
902
|
-
? p.split("/").slice(0, 2).join("/")
|
|
903
|
-
: p.split("/")[0];
|
|
832
|
+
const base = p.startsWith("@") ? p.split("/").slice(0, 2).join("/") : p.split("/")[0];
|
|
904
833
|
if (REACT_PACKAGES.has(p) || REACT_PACKAGES.has(base)) {
|
|
905
834
|
reactSet.add(base);
|
|
906
835
|
}
|
|
@@ -963,10 +892,7 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
963
892
|
}
|
|
964
893
|
}
|
|
965
894
|
}
|
|
966
|
-
result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({
|
|
967
|
-
name,
|
|
968
|
-
path: p,
|
|
969
|
-
}));
|
|
895
|
+
result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({ name, path: p }));
|
|
970
896
|
result.importedLibraries = [...libSet].sort();
|
|
971
897
|
result.importedUnknownLibraries = [...unknownLibSet].sort();
|
|
972
898
|
result.reactPackageUsage = [...reactSet].sort();
|
|
@@ -982,38 +908,27 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
982
908
|
customDataMap.set(cd.id, cd);
|
|
983
909
|
}
|
|
984
910
|
// Find the component — try match by dir, displayName, id, or new-id
|
|
985
|
-
const target = components.find(
|
|
911
|
+
const target = components.find(c => {
|
|
986
912
|
if (!c)
|
|
987
913
|
return false;
|
|
988
|
-
if (c.dir === sectionName ||
|
|
989
|
-
c.displayName === sectionName ||
|
|
990
|
-
c.id === sectionName)
|
|
914
|
+
if (c.dir === sectionName || c.displayName === sectionName || c.id === sectionName)
|
|
991
915
|
return true;
|
|
992
916
|
const kebab = toKebabCase(c.dir || c.displayName || c.id || "");
|
|
993
917
|
const newId = `${projectName}-${kebab}`;
|
|
994
918
|
return newId === sectionName;
|
|
995
919
|
});
|
|
996
920
|
if (!target) {
|
|
997
|
-
const available = components
|
|
998
|
-
.map((c) => c.dir || c.displayName || c.id)
|
|
999
|
-
.filter(Boolean)
|
|
1000
|
-
.join(", ");
|
|
921
|
+
const available = components.map(c => c.dir || c.displayName || c.id).filter(Boolean).join(", ");
|
|
1001
922
|
return `Section "${sectionName}" not found in theme. Available: ${available}`;
|
|
1002
923
|
}
|
|
1003
924
|
const parts = [];
|
|
1004
925
|
const oldName = target.displayName || target.dir || target.id || "Unknown";
|
|
1005
926
|
const kebabName = toKebabCase(target.dir || target.displayName || target.id || "unknown");
|
|
1006
927
|
const sectionId = `${projectName}-${kebabName}`;
|
|
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("");
|
|
928
|
+
const sectionPascal = (target.dir || target.displayName || "").replace(/[^a-zA-Z0-9]/g, "") || kebabName.split("-").map(s => s[0]?.toUpperCase() + s.slice(1)).join("");
|
|
1012
929
|
// Scan the old source for imports, libraries, field usage
|
|
1013
|
-
const propNames = (target.props || [])
|
|
1014
|
-
|
|
1015
|
-
.filter(Boolean);
|
|
1016
|
-
const sourceScan = oldSourceDir && target.dir
|
|
930
|
+
const propNames = (target.props || []).map(p => p.name || "").filter(Boolean);
|
|
931
|
+
const sourceScan = (oldSourceDir && target.dir)
|
|
1017
932
|
? scanSectionSource(path.join(oldSourceDir, target.dir), propNames)
|
|
1018
933
|
: null;
|
|
1019
934
|
parts.push(`# Section Migration Plan: ${oldName}`);
|
|
@@ -1059,7 +974,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1059
974
|
}
|
|
1060
975
|
if (sourceScan.reactPackageUsage.length > 0) {
|
|
1061
976
|
parts.push("");
|
|
1062
|
-
parts.push(`**React/Next.js framework imports detected:** ${sourceScan.reactPackageUsage.map(
|
|
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).`);
|
|
1063
978
|
}
|
|
1064
979
|
if (sourceScan.oldStorefrontCalls.length > 0) {
|
|
1065
980
|
parts.push("");
|
|
@@ -1105,11 +1020,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1105
1020
|
if (oldType === "SLIDER") {
|
|
1106
1021
|
newType = "NUMBER";
|
|
1107
1022
|
notes = `Was SLIDER(min=${p.sliderData?.min}, max=${p.sliderData?.max}) — replace \`.value\` access with direct number`;
|
|
1108
|
-
const prop = {
|
|
1109
|
-
name: newName,
|
|
1110
|
-
displayName: p.displayName || newName,
|
|
1111
|
-
type: "NUMBER",
|
|
1112
|
-
};
|
|
1023
|
+
const prop = { name: newName, displayName: p.displayName || newName, type: "NUMBER" };
|
|
1113
1024
|
if (p.isRequired)
|
|
1114
1025
|
prop.required = true;
|
|
1115
1026
|
parentPropsJson.push(prop);
|
|
@@ -1117,11 +1028,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1117
1028
|
else if (oldType === "PRODUCT_DETAIL") {
|
|
1118
1029
|
newType = "PRODUCT";
|
|
1119
1030
|
notes = "Renamed — PRODUCT_DETAIL → PRODUCT";
|
|
1120
|
-
const prop = {
|
|
1121
|
-
name: newName,
|
|
1122
|
-
displayName: p.displayName || newName,
|
|
1123
|
-
type: "PRODUCT",
|
|
1124
|
-
};
|
|
1031
|
+
const prop = { name: newName, displayName: p.displayName || newName, type: "PRODUCT" };
|
|
1125
1032
|
if (p.isRequired)
|
|
1126
1033
|
prop.required = true;
|
|
1127
1034
|
parentPropsJson.push(prop);
|
|
@@ -1132,10 +1039,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1132
1039
|
if (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST") {
|
|
1133
1040
|
// Child component needed
|
|
1134
1041
|
const itemObj = cd.nestedData?.[0];
|
|
1135
|
-
const childName = itemObj?.typescriptName ||
|
|
1136
|
-
(itemObj?.name
|
|
1137
|
-
? itemObj.name.replace(/[^a-zA-Z0-9]/g, "")
|
|
1138
|
-
: `${sectionPascal}Item`);
|
|
1042
|
+
const childName = itemObj?.typescriptName || (itemObj?.name ? itemObj.name.replace(/[^a-zA-Z0-9]/g, "") : `${sectionPascal}Item`);
|
|
1139
1043
|
const childProps = [];
|
|
1140
1044
|
const nestedWarnings = [];
|
|
1141
1045
|
for (const f of (itemObj?.nestedData || [])) {
|
|
@@ -1146,18 +1050,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1146
1050
|
fType = "NUMBER";
|
|
1147
1051
|
else if (fType === "PRODUCT_DETAIL")
|
|
1148
1052
|
fType = "PRODUCT";
|
|
1149
|
-
else if (fType === "CUSTOM" ||
|
|
1150
|
-
fType === "DYNAMIC_LIST" ||
|
|
1151
|
-
fType === "STATIC_LIST" ||
|
|
1152
|
-
fType === "OBJECT") {
|
|
1053
|
+
else if (fType === "CUSTOM" || fType === "DYNAMIC_LIST" || fType === "STATIC_LIST" || fType === "OBJECT") {
|
|
1153
1054
|
nestedWarnings.push(`\`${f.key}\` (${fType})`);
|
|
1154
1055
|
fType = "COMPONENT_LIST";
|
|
1155
1056
|
}
|
|
1156
|
-
const prop = {
|
|
1157
|
-
name: f.key,
|
|
1158
|
-
displayName: f.name || f.key,
|
|
1159
|
-
type: fType,
|
|
1160
|
-
};
|
|
1057
|
+
const prop = { name: f.key, displayName: f.name || f.key, type: fType };
|
|
1161
1058
|
if (f.isRequired)
|
|
1162
1059
|
prop.required = true;
|
|
1163
1060
|
childProps.push(prop);
|
|
@@ -1169,14 +1066,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1169
1066
|
if (childProps.length === 0 && sourceScan?.propFieldUsage[oldName]) {
|
|
1170
1067
|
const inferred = sourceScan.propFieldUsage[oldName];
|
|
1171
1068
|
for (const fieldName of inferred) {
|
|
1172
|
-
childProps.push({
|
|
1173
|
-
name: fieldName,
|
|
1174
|
-
displayName: fieldName,
|
|
1175
|
-
type: "TEXT",
|
|
1176
|
-
});
|
|
1069
|
+
childProps.push({ name: fieldName, displayName: fieldName, type: "TEXT" });
|
|
1177
1070
|
}
|
|
1178
1071
|
if (inferred.length > 0) {
|
|
1179
|
-
notes = `Was CUSTOM → ${cd.type} with empty customData. **Inferred props from source usage:** ${inferred.map(
|
|
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.`;
|
|
1180
1073
|
}
|
|
1181
1074
|
}
|
|
1182
1075
|
children.push({
|
|
@@ -1205,11 +1098,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1205
1098
|
let fType = f.type;
|
|
1206
1099
|
if (fType === "SLIDER")
|
|
1207
1100
|
fType = "NUMBER";
|
|
1208
|
-
const prop = {
|
|
1209
|
-
name: f.key,
|
|
1210
|
-
displayName: f.name || f.key,
|
|
1211
|
-
type: fType,
|
|
1212
|
-
};
|
|
1101
|
+
const prop = { name: f.key, displayName: f.name || f.key, type: fType };
|
|
1213
1102
|
if (f.isRequired)
|
|
1214
1103
|
prop.required = true;
|
|
1215
1104
|
parentPropsJson.push(prop);
|
|
@@ -1224,8 +1113,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1224
1113
|
}
|
|
1225
1114
|
else if (cd.type === "ENUM") {
|
|
1226
1115
|
newType = "ENUM";
|
|
1227
|
-
const enumName = cd.typescriptName ||
|
|
1228
|
-
(cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
|
|
1116
|
+
const enumName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
|
|
1229
1117
|
const options = (cd.enumOptions || []).reduce((acc, o) => {
|
|
1230
1118
|
if (o.displayName && o.value)
|
|
1231
1119
|
acc[o.displayName] = o.value;
|
|
@@ -1233,12 +1121,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1233
1121
|
}, {});
|
|
1234
1122
|
enumsNeeded.push({ name: enumName, options });
|
|
1235
1123
|
notes = `Was CUSTOM (ENUM) — create enum \`${enumName}\` via \`config add-enum\` first, then reference its enumId here`;
|
|
1236
|
-
const prop = {
|
|
1237
|
-
name: newName,
|
|
1238
|
-
displayName: p.displayName || newName,
|
|
1239
|
-
type: "ENUM",
|
|
1240
|
-
enumTypeId: `<ENUM_ID_FROM_add-enum_${enumName}>`,
|
|
1241
|
-
};
|
|
1124
|
+
const prop = { name: newName, displayName: p.displayName || newName, type: "ENUM", enumTypeId: `<ENUM_ID_FROM_add-enum_${enumName}>` };
|
|
1242
1125
|
if (p.isRequired)
|
|
1243
1126
|
prop.required = true;
|
|
1244
1127
|
parentPropsJson.push(prop);
|
|
@@ -1247,11 +1130,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1247
1130
|
}
|
|
1248
1131
|
else {
|
|
1249
1132
|
// Direct mapping
|
|
1250
|
-
const prop = {
|
|
1251
|
-
name: newName,
|
|
1252
|
-
displayName: p.displayName || newName,
|
|
1253
|
-
type: newType,
|
|
1254
|
-
};
|
|
1133
|
+
const prop = { name: newName, displayName: p.displayName || newName, type: newType };
|
|
1255
1134
|
if (p.isRequired)
|
|
1256
1135
|
prop.required = true;
|
|
1257
1136
|
parentPropsJson.push(prop);
|
|
@@ -1260,9 +1139,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1260
1139
|
}
|
|
1261
1140
|
parts.push("");
|
|
1262
1141
|
// Custom Data Decision Callouts — per prop referencing a customData type
|
|
1263
|
-
const customDataPropsForCallouts = (target.props || []).filter((p) => p.type === "CUSTOM" &&
|
|
1264
|
-
p.customDataId &&
|
|
1265
|
-
customDataMap.has(p.customDataId));
|
|
1142
|
+
const customDataPropsForCallouts = (target.props || []).filter((p) => p.type === "CUSTOM" && p.customDataId && customDataMap.has(p.customDataId));
|
|
1266
1143
|
if (customDataPropsForCallouts.length > 0) {
|
|
1267
1144
|
parts.push(`## Custom Data Decisions to Make`);
|
|
1268
1145
|
parts.push("");
|
|
@@ -1277,9 +1154,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1277
1154
|
let shape = "";
|
|
1278
1155
|
let shapeKind = "unknown";
|
|
1279
1156
|
if (cd.type === "ENUM") {
|
|
1280
|
-
const opts = (cd.enumOptions || [])
|
|
1281
|
-
.map((o) => o.value || o.displayName)
|
|
1282
|
-
.filter(Boolean);
|
|
1157
|
+
const opts = (cd.enumOptions || []).map((o) => o.value || o.displayName).filter(Boolean);
|
|
1283
1158
|
shape = `enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}`;
|
|
1284
1159
|
shapeKind = "enum";
|
|
1285
1160
|
}
|
|
@@ -1312,8 +1187,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1312
1187
|
if (shapeKind === "enum") {
|
|
1313
1188
|
parts.push(`**Default: enum prop.** Flat scalar set; use \`config add-enum\`.`);
|
|
1314
1189
|
parts.push("");
|
|
1315
|
-
const enumName = cd.typescriptName ||
|
|
1316
|
-
(cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
|
|
1190
|
+
const enumName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
|
|
1317
1191
|
const enumOptions = (cd.enumOptions || []).reduce((acc, o) => {
|
|
1318
1192
|
if (o.displayName && o.value)
|
|
1319
1193
|
acc[o.displayName] = o.value;
|
|
@@ -1348,10 +1222,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1348
1222
|
parts.push(`> See \`get_migration_guide("component-composition-decision-guide")\` for when \`COMPONENT_LIST\` is overkill.`);
|
|
1349
1223
|
parts.push("");
|
|
1350
1224
|
}
|
|
1351
|
-
const compName = cd.typescriptName ||
|
|
1352
|
-
(cd.name
|
|
1353
|
-
? cd.name.replace(/[^a-zA-Z0-9]/g, "")
|
|
1354
|
-
: `${sectionPascal}Item`);
|
|
1225
|
+
const compName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : `${sectionPascal}Item`);
|
|
1355
1226
|
const compPropsForCli = [];
|
|
1356
1227
|
for (const f of fieldSource) {
|
|
1357
1228
|
if (!f.key)
|
|
@@ -1361,11 +1232,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1361
1232
|
fType = "NUMBER";
|
|
1362
1233
|
else if (fType === "PRODUCT_DETAIL")
|
|
1363
1234
|
fType = "PRODUCT";
|
|
1364
|
-
compPropsForCli.push({
|
|
1365
|
-
name: f.key,
|
|
1366
|
-
displayName: f.name || f.key,
|
|
1367
|
-
type: fType,
|
|
1368
|
-
});
|
|
1235
|
+
compPropsForCli.push({ name: f.key, displayName: f.name || f.key, type: fType });
|
|
1369
1236
|
}
|
|
1370
1237
|
parts.push("```bash");
|
|
1371
1238
|
if (isMinimal) {
|
|
@@ -1404,10 +1271,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1404
1271
|
existing.usedByProps.push(ch.propName);
|
|
1405
1272
|
}
|
|
1406
1273
|
else {
|
|
1407
|
-
uniqueChildren.set(ch.childName, {
|
|
1408
|
-
child: ch,
|
|
1409
|
-
usedByProps: [ch.propName],
|
|
1410
|
-
});
|
|
1274
|
+
uniqueChildren.set(ch.childName, { child: ch, usedByProps: [ch.propName] });
|
|
1411
1275
|
}
|
|
1412
1276
|
}
|
|
1413
1277
|
if (uniqueChildren.size > 0) {
|
|
@@ -1420,7 +1284,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1420
1284
|
for (const { child: ch, usedByProps } of uniqueChildren.values()) {
|
|
1421
1285
|
parts.push(`### \`${ch.childName}\``);
|
|
1422
1286
|
const propsLabel = usedByProps.length > 1
|
|
1423
|
-
? `Used by parent props: ${usedByProps.map(
|
|
1287
|
+
? `Used by parent props: ${usedByProps.map(p => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
|
|
1424
1288
|
: `For parent prop: \`${usedByProps[0]}\``;
|
|
1425
1289
|
parts.push(propsLabel);
|
|
1426
1290
|
parts.push(`Old customData: "${ch.customDataName}"`);
|
|
@@ -1466,15 +1330,12 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1466
1330
|
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.`);
|
|
1467
1331
|
}
|
|
1468
1332
|
// Check if the section itself has data-driven list props (PRODUCT_LIST, BLOG_LIST, CATEGORY_LIST)
|
|
1469
|
-
const dataListProps = (target.props || []).filter(
|
|
1470
|
-
p.type === "BLOG_LIST" ||
|
|
1471
|
-
p.type === "CATEGORY_LIST" ||
|
|
1472
|
-
p.type === "BRAND_LIST");
|
|
1333
|
+
const dataListProps = (target.props || []).filter(p => p.type === "PRODUCT_LIST" || p.type === "BLOG_LIST" || p.type === "CATEGORY_LIST" || p.type === "BRAND_LIST");
|
|
1473
1334
|
if (dataListProps.length > 0) {
|
|
1474
1335
|
parts.push("");
|
|
1475
1336
|
parts.push(`### Data-Driven List Rendering`);
|
|
1476
1337
|
parts.push("");
|
|
1477
|
-
parts.push(`This section has data-driven list props: ${dataListProps.map(
|
|
1338
|
+
parts.push(`This section has data-driven list props: ${dataListProps.map(p => `\`${p.name}\` (${p.type})`).join(", ")}.`);
|
|
1478
1339
|
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:`);
|
|
1479
1340
|
parts.push("");
|
|
1480
1341
|
parts.push("```tsx");
|
|
@@ -1501,22 +1362,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1501
1362
|
parts.push(`See \`get_migration_guide("custom-data-conversion")\` → "Two Ways to Render Lists" for the full pattern.`);
|
|
1502
1363
|
}
|
|
1503
1364
|
// Detect form-page sections (0 or few props, name suggests a form/auth page)
|
|
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
|
-
];
|
|
1365
|
+
const formKeywords = ["login", "register", "forgot", "recover", "password", "account", "email", "verification", "activate", "contact", "checkout", "address"];
|
|
1518
1366
|
const lowerDir = (target.dir || "").toLowerCase();
|
|
1519
|
-
const isLikelyFormPage = formKeywords.some(
|
|
1367
|
+
const isLikelyFormPage = formKeywords.some(kw => lowerDir.includes(kw));
|
|
1520
1368
|
if (isLikelyFormPage) {
|
|
1521
1369
|
parts.push("");
|
|
1522
1370
|
parts.push(`### Form Page Pattern`);
|
|
@@ -1545,9 +1393,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1545
1393
|
// Fallback heuristic only when source scan unavailable
|
|
1546
1394
|
const heuristicLibs = [];
|
|
1547
1395
|
const lowerName = oldName.toLowerCase();
|
|
1548
|
-
if (lowerName.includes("slider") ||
|
|
1549
|
-
lowerName.includes("carousel") ||
|
|
1550
|
-
lowerName.includes("banner")) {
|
|
1396
|
+
if (lowerName.includes("slider") || lowerName.includes("carousel") || lowerName.includes("banner")) {
|
|
1551
1397
|
heuristicLibs.push("swiper");
|
|
1552
1398
|
}
|
|
1553
1399
|
if (lowerName.includes("marquee"))
|
|
@@ -1556,9 +1402,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1556
1402
|
heuristicLibs.push("react-player");
|
|
1557
1403
|
if (lowerName.includes("chart"))
|
|
1558
1404
|
heuristicLibs.push("recharts");
|
|
1559
|
-
if (lowerName.includes("star") ||
|
|
1560
|
-
lowerName.includes("rating") ||
|
|
1561
|
-
lowerName.includes("review"))
|
|
1405
|
+
if (lowerName.includes("star") || lowerName.includes("rating") || lowerName.includes("review"))
|
|
1562
1406
|
heuristicLibs.push("react-simple-star-rating");
|
|
1563
1407
|
if (heuristicLibs.length > 0) {
|
|
1564
1408
|
parts.push(`### Likely Library Replacements (heuristic — source not scanned)`);
|
|
@@ -1630,7 +1474,8 @@ function levenshtein(a, b) {
|
|
|
1630
1474
|
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
1631
1475
|
curr[j] = Math.min(curr[j - 1] + 1, // insertion
|
|
1632
1476
|
prev[j] + 1, // deletion
|
|
1633
|
-
prev[j - 1] + cost
|
|
1477
|
+
prev[j - 1] + cost // substitution
|
|
1478
|
+
);
|
|
1634
1479
|
}
|
|
1635
1480
|
for (let j = 0; j <= n; j++)
|
|
1636
1481
|
prev[j] = curr[j];
|
|
@@ -1661,29 +1506,17 @@ function searchFunctions(query) {
|
|
|
1661
1506
|
const scored = storefrontData.functions
|
|
1662
1507
|
.map((fn) => {
|
|
1663
1508
|
const nameScore = matchScore(fn.name, query) * 3;
|
|
1664
|
-
const displayNameScore = fn.displayName
|
|
1665
|
-
? matchScore(fn.displayName, query) * 3
|
|
1666
|
-
: 0;
|
|
1509
|
+
const displayNameScore = fn.displayName ? matchScore(fn.displayName, query) * 3 : 0;
|
|
1667
1510
|
const descScore = matchScore(fn.description, query);
|
|
1668
|
-
const catScore = fn.categories.some((c) => matchScore(c, query) > 0)
|
|
1669
|
-
? 5
|
|
1670
|
-
: 0;
|
|
1511
|
+
const catScore = fn.categories.some((c) => matchScore(c, query) > 0) ? 5 : 0;
|
|
1671
1512
|
const paramScore = fn.params.some((p) => matchScore(p.name, query) > 0 || matchScore(p.description, query) > 0)
|
|
1672
1513
|
? 2
|
|
1673
1514
|
: 0;
|
|
1674
1515
|
const sigScore = matchScore(fn.signature, query) * 2;
|
|
1675
|
-
const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0)
|
|
1676
|
-
? 8
|
|
1677
|
-
: 0;
|
|
1516
|
+
const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0) ? 8 : 0;
|
|
1678
1517
|
return {
|
|
1679
1518
|
fn,
|
|
1680
|
-
score: nameScore +
|
|
1681
|
-
displayNameScore +
|
|
1682
|
-
descScore +
|
|
1683
|
-
catScore +
|
|
1684
|
-
paramScore +
|
|
1685
|
-
sigScore +
|
|
1686
|
-
typeScore,
|
|
1519
|
+
score: nameScore + displayNameScore + descScore + catScore + paramScore + sigScore + typeScore,
|
|
1687
1520
|
};
|
|
1688
1521
|
})
|
|
1689
1522
|
.filter((item) => item.score > 0)
|
|
@@ -1697,11 +1530,7 @@ function searchFrameworkTopics(query) {
|
|
|
1697
1530
|
const descScore = matchScore(topic.description, query) * 2;
|
|
1698
1531
|
const contentScore = matchScore(topic.content, query);
|
|
1699
1532
|
const tagScore = topic.tags.some((t) => matchScore(t, query) > 0) ? 5 : 0;
|
|
1700
|
-
return {
|
|
1701
|
-
key,
|
|
1702
|
-
topic,
|
|
1703
|
-
score: titleScore + descScore + contentScore + tagScore,
|
|
1704
|
-
};
|
|
1533
|
+
return { key, topic, score: titleScore + descScore + contentScore + tagScore };
|
|
1705
1534
|
})
|
|
1706
1535
|
.filter((item) => item.score > 0)
|
|
1707
1536
|
.sort((a, b) => b.score - a.score);
|
|
@@ -1716,9 +1545,7 @@ function searchTypes(query) {
|
|
|
1716
1545
|
const propScore = td.properties?.some((p) => matchScore(p.name, query) > 0 || matchScore(p.type, query) > 0)
|
|
1717
1546
|
? 4
|
|
1718
1547
|
: 0;
|
|
1719
|
-
const enumScore = td.enumValues?.some((v) => matchScore(v, query) > 0)
|
|
1720
|
-
? 4
|
|
1721
|
-
: 0;
|
|
1548
|
+
const enumScore = td.enumValues?.some((v) => matchScore(v, query) > 0) ? 4 : 0;
|
|
1722
1549
|
return { td, score: nameScore + domainScore + propScore + enumScore };
|
|
1723
1550
|
})
|
|
1724
1551
|
.filter((item) => item.score > 0)
|
|
@@ -1760,12 +1587,8 @@ function formatFunctionDoc(fn) {
|
|
|
1760
1587
|
return lines.join("\n");
|
|
1761
1588
|
}
|
|
1762
1589
|
function formatFunctionSummary(fn) {
|
|
1763
|
-
const desc = fn.description
|
|
1764
|
-
|
|
1765
|
-
: "No description.";
|
|
1766
|
-
const alias = fn.displayName && fn.displayName !== fn.name
|
|
1767
|
-
? ` (alias: ${fn.displayName})`
|
|
1768
|
-
: "";
|
|
1590
|
+
const desc = fn.description ? fn.description.split(".")[0] + "." : "No description.";
|
|
1591
|
+
const alias = fn.displayName && fn.displayName !== fn.name ? ` (alias: ${fn.displayName})` : "";
|
|
1769
1592
|
return `- \`${fn.name}\`${alias} - ${desc}`;
|
|
1770
1593
|
}
|
|
1771
1594
|
function formatTypeDefinition(td, opts = {}) {
|
|
@@ -1806,9 +1629,7 @@ function formatTypeDefinition(td, opts = {}) {
|
|
|
1806
1629
|
const fn = storefrontData.functions.find((f) => f.name === fnName);
|
|
1807
1630
|
if (fn) {
|
|
1808
1631
|
const desc = fn.description ? fn.description.split(".")[0] + "." : "";
|
|
1809
|
-
const alias = fn.displayName && fn.displayName !== fn.name
|
|
1810
|
-
? ` (alias: ${fn.displayName})`
|
|
1811
|
-
: "";
|
|
1632
|
+
const alias = fn.displayName && fn.displayName !== fn.name ? ` (alias: ${fn.displayName})` : "";
|
|
1812
1633
|
lines.push(`- **\`${fn.name}\`**${alias} — ${desc}`);
|
|
1813
1634
|
lines.push(` \`${fn.signature}\``);
|
|
1814
1635
|
}
|
|
@@ -1817,9 +1638,7 @@ function formatTypeDefinition(td, opts = {}) {
|
|
|
1817
1638
|
}
|
|
1818
1639
|
}
|
|
1819
1640
|
lines.push("");
|
|
1820
|
-
lines.push(
|
|
1821
|
-
td.name +
|
|
1822
|
-
'")` for full documentation of these functions.');
|
|
1641
|
+
lines.push("Use `get_functions_for_type(\"" + td.name + "\")` for full documentation of these functions.");
|
|
1823
1642
|
}
|
|
1824
1643
|
return lines.join("\n");
|
|
1825
1644
|
}
|
|
@@ -1837,13 +1656,7 @@ const server = new McpServer({
|
|
|
1837
1656
|
name: "ikas-code-components",
|
|
1838
1657
|
version: "0.1.0",
|
|
1839
1658
|
}, {
|
|
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
|
|
1841
|
-
"Live-editor actions (require `ikas-component dev` running with the editor connected): `list_editor_pages` → page ids; `list_imported_sections` → imported component ids; `import_section` → import a built component; `add_section_to_page` → place one section (`add_sections_to_page` places MANY at once and can set their props in the same call — fastest way to build a page); `list_page_sections` → the sections placed on a page (LEAN roster: per-placement `elementId`, `componentId`, `propCount`, which props are filled — NO schema/values, so it never truncates); `get_component_props` → a component's prop blueprint (types, ENUM valid `options`, COMPONENT_LIST `allowedComponentIds`) for any section OR child id; `get_section_values` → one section's current prop values (for read-modify-write); `get_page_by_type` → a theme page id by pageType (e.g. CATEGORY), to build PAGE links to entities (`create_page` adds that page if it does not exist yet, so you never guess a slug); `update_section_prop` → change a single prop value of a placed section (`update_page_sections` fills MANY sections/props of a page in ONE call — strongly prefer it to cut round-trips when filling content); `upload_image` → upload one image (file or URL) and get an image id (`upload_images` uploads many in one call — prefer it for several); `search_products` / `list_categories` / `list_brands` / `list_blogs` / `list_blog_categories` → find real entity ids for PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY prop values; `publish_theme` → publish the theme LIVE and get back the preview URL to review (guarded — needs `confirm:true`, and `confirm_production:true` for the main theme; only call it when the user explicitly asks to publish). For the full end-to-end process (placing a section and filling its content, with per-prop value shapes and COMPONENT_LIST rules), call `get_editor_workflow` first.\n\n" +
|
|
1842
|
-
"To CHANGE/EDIT a prop value (text, color, boolean, number, etc.) of a section that is already on a page, this IS supported: call `list_page_sections(page_id)` to get the placement `elementId` and the prop names/ids, then `update_section_prop(page_id, element_id, prop_name|prop_id, value)`. Do not assume prop editing is unavailable.\n\n" +
|
|
1843
|
-
"TWO DISTINCT JOBS — do not confuse them, and do not jump to writing code for the second one:\n" +
|
|
1844
|
-
"(A) DEFINING props / authoring a component = changing which props a component HAS. This edits the component source/config (types.ts via `ikas-component config add-prop`/`add-component`, JSX in index.tsx, etc.) and requires a rebuild. Use this ONLY when the needed prop does not exist yet, or the user explicitly asks to build/modify the component's code or prop schema.\n" +
|
|
1845
|
-
"(B) SETTING prop VALUES / filling content = giving values to props that ALREADY exist on a section placed on a page. This is pure data entry via `list_page_sections` + `update_section_prop` (and `upload_image` for images). It writes NO code and needs NO rebuild.\n" +
|
|
1846
|
-
"When the user says things like 'fill this section', 'set the heading/title/text/image/link', 'populate', 'change the content/value', or 'enter this data', that is JOB (B): use `update_section_prop`. First call `list_page_sections` and check the existing `props` — if the prop is already there (it usually is), just set its value. Only fall back to JOB (A) if the required prop genuinely does not exist in that section's `props`. Writing a new component or adding a prop to fill in a value the section already supports is the wrong move.",
|
|
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.",
|
|
1847
1660
|
});
|
|
1848
1661
|
// Tool: search_docs
|
|
1849
1662
|
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 }) => {
|
|
@@ -1884,20 +1697,13 @@ server.tool("search_docs", "Search across all ikas storefront API docs, framewor
|
|
|
1884
1697
|
parts.push("");
|
|
1885
1698
|
parts.push("Use `get_migration_guide(topic)` to get full content for any migration topic.");
|
|
1886
1699
|
}
|
|
1887
|
-
if (functions.length === 0 &&
|
|
1888
|
-
topics.length === 0 &&
|
|
1889
|
-
types.length === 0 &&
|
|
1890
|
-
migrationTopics.length === 0) {
|
|
1700
|
+
if (functions.length === 0 && topics.length === 0 && types.length === 0 && migrationTopics.length === 0) {
|
|
1891
1701
|
parts.push(`No results found for "${query}". Try different keywords or use \`list_functions()\` to see all available functions.`);
|
|
1892
1702
|
}
|
|
1893
1703
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1894
1704
|
});
|
|
1895
1705
|
// Tool: get_function_doc
|
|
1896
|
-
server.tool("get_function_doc", "Get full documentation for a specific storefront API function including signature, parameters, return type, and example.", {
|
|
1897
|
-
name: z
|
|
1898
|
-
.string()
|
|
1899
|
-
.describe("Function name (e.g. 'addItemToCart', 'Router.navigate')"),
|
|
1900
|
-
}, async ({ name }) => {
|
|
1706
|
+
server.tool("get_function_doc", "Get full documentation for a specific storefront API function including signature, parameters, return type, and example.", { name: z.string().describe("Function name (e.g. 'addItemToCart', 'Router.navigate')") }, async ({ name }) => {
|
|
1901
1707
|
const nameLower = name.toLowerCase();
|
|
1902
1708
|
// Phase 1: canonical-name match wins. A real function name always outranks
|
|
1903
1709
|
// any displayName alias so aliases can never shadow the function they're
|
|
@@ -1905,9 +1711,7 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1905
1711
|
// [BP-DISPLAY-NAME: hasCustomer] alias).
|
|
1906
1712
|
const byName = storefrontData.functions.find((f) => f.name.toLowerCase() === nameLower);
|
|
1907
1713
|
if (byName) {
|
|
1908
|
-
return {
|
|
1909
|
-
content: [{ type: "text", text: formatFunctionDoc(byName) }],
|
|
1910
|
-
};
|
|
1714
|
+
return { content: [{ type: "text", text: formatFunctionDoc(byName) }] };
|
|
1911
1715
|
}
|
|
1912
1716
|
// Phase 2: fall back to displayName aliases.
|
|
1913
1717
|
const byAlias = storefrontData.functions.filter((f) => f.displayName && f.displayName.toLowerCase() === nameLower);
|
|
@@ -1915,9 +1719,7 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1915
1719
|
const fn = byAlias[0];
|
|
1916
1720
|
const note = `> Note: "${name}" is a display alias for \`${fn.name}\`.\n\n`;
|
|
1917
1721
|
return {
|
|
1918
|
-
content: [
|
|
1919
|
-
{ type: "text", text: note + formatFunctionDoc(fn) },
|
|
1920
|
-
],
|
|
1722
|
+
content: [{ type: "text", text: note + formatFunctionDoc(fn) }],
|
|
1921
1723
|
};
|
|
1922
1724
|
}
|
|
1923
1725
|
if (byAlias.length > 1) {
|
|
@@ -1935,9 +1737,7 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1935
1737
|
(f.displayName && f.displayName.toLowerCase().includes(nameLower)));
|
|
1936
1738
|
if (matches.length > 0) {
|
|
1937
1739
|
const suggestions = matches.slice(0, 5).map((f) => {
|
|
1938
|
-
const alias = f.displayName && f.displayName !== f.name
|
|
1939
|
-
? ` (alias: ${f.displayName})`
|
|
1940
|
-
: "";
|
|
1740
|
+
const alias = f.displayName && f.displayName !== f.name ? ` (alias: ${f.displayName})` : "";
|
|
1941
1741
|
return ` - ${f.name}${alias}`;
|
|
1942
1742
|
});
|
|
1943
1743
|
return {
|
|
@@ -1950,12 +1750,7 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1950
1750
|
};
|
|
1951
1751
|
}
|
|
1952
1752
|
return {
|
|
1953
|
-
content: [
|
|
1954
|
-
{
|
|
1955
|
-
type: "text",
|
|
1956
|
-
text: `Function "${name}" not found. Use \`list_functions()\` to see all available functions.`,
|
|
1957
|
-
},
|
|
1958
|
-
],
|
|
1753
|
+
content: [{ type: "text", text: `Function "${name}" not found. Use \`list_functions()\` to see all available functions.` }],
|
|
1959
1754
|
};
|
|
1960
1755
|
});
|
|
1961
1756
|
// Tool: list_functions
|
|
@@ -2005,7 +1800,7 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
|
|
|
2005
1800
|
if (uncategorized > 0) {
|
|
2006
1801
|
lines.push(`- \`Other\` (${uncategorized})`);
|
|
2007
1802
|
}
|
|
2008
|
-
lines.push("",
|
|
1803
|
+
lines.push("", "Call `list_functions(category: \"<name>\")` to see one-line summaries for a category.");
|
|
2009
1804
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2010
1805
|
}
|
|
2011
1806
|
const catLower = category.toLowerCase();
|
|
@@ -2037,11 +1832,7 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
|
|
|
2037
1832
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
2038
1833
|
});
|
|
2039
1834
|
// Tool: get_code_example
|
|
2040
|
-
server.tool("get_code_example", "Get an API usage reference for a specific task. Shows correct function calls, imports, and data-handling patterns from a real production theme. The JSX layout and CSS are illustrative only — create your own original visual design. Call `list_examples()` to see available example IDs.", {
|
|
2041
|
-
task: z
|
|
2042
|
-
.string()
|
|
2043
|
-
.describe("Task description or example ID (call `list_examples()` for the full list)"),
|
|
2044
|
-
}, async ({ task }) => {
|
|
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.", { task: z.string().describe("Task description or example ID (call `list_examples()` for the full list)") }, async ({ task }) => {
|
|
2045
1836
|
const taskLower = task.toLowerCase();
|
|
2046
1837
|
// Try exact ID match first
|
|
2047
1838
|
let example = storefrontData.codeExamples.find((e) => e.id === taskLower);
|
|
@@ -2061,9 +1852,7 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
2061
1852
|
}
|
|
2062
1853
|
}
|
|
2063
1854
|
if (!example) {
|
|
2064
|
-
const available = storefrontData.codeExamples
|
|
2065
|
-
.map((e) => ` - \`${e.id}\` - ${e.title}`)
|
|
2066
|
-
.join("\n");
|
|
1855
|
+
const available = storefrontData.codeExamples.map((e) => ` - \`${e.id}\` - ${e.title}`).join("\n");
|
|
2067
1856
|
return {
|
|
2068
1857
|
content: [
|
|
2069
1858
|
{
|
|
@@ -2086,24 +1875,14 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
2086
1875
|
if (example.files && example.files.length > 0) {
|
|
2087
1876
|
for (const file of example.files) {
|
|
2088
1877
|
const ext = file.filename.split(".").pop() || "text";
|
|
2089
|
-
const lang = ext === "tsx" || ext === "ts"
|
|
2090
|
-
? "typescript"
|
|
2091
|
-
: ext === "css"
|
|
2092
|
-
? "css"
|
|
2093
|
-
: ext === "json"
|
|
2094
|
-
? "json"
|
|
2095
|
-
: "text";
|
|
1878
|
+
const lang = ext === "tsx" || ext === "ts" ? "typescript" : ext === "css" ? "css" : ext === "json" ? "json" : "text";
|
|
2096
1879
|
// Add inline originality comments to CSS and TSX files
|
|
2097
1880
|
let content = file.content;
|
|
2098
1881
|
if (ext === "css") {
|
|
2099
|
-
content =
|
|
2100
|
-
"/* EXAMPLE STYLING — create your own original CSS with different class names and design */\n" +
|
|
2101
|
-
content;
|
|
1882
|
+
content = "/* EXAMPLE STYLING — create your own original CSS with different class names and design */\n" + content;
|
|
2102
1883
|
}
|
|
2103
1884
|
else if (ext === "tsx") {
|
|
2104
|
-
content =
|
|
2105
|
-
"// EXAMPLE COMPONENT — use the API patterns but create your own JSX structure and layout\n" +
|
|
2106
|
-
content;
|
|
1885
|
+
content = "// EXAMPLE COMPONENT — use the API patterns but create your own JSX structure and layout\n" + content;
|
|
2107
1886
|
}
|
|
2108
1887
|
parts.push(`### ${file.filename}`, "", `\`\`\`${lang}`, content, "```", "");
|
|
2109
1888
|
}
|
|
@@ -2117,88 +1896,63 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
2117
1896
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
2118
1897
|
});
|
|
2119
1898
|
// Tool: get_framework_guide
|
|
2120
|
-
server.tool("get_framework_guide", "Get a framework guide on a specific topic (e.g. 'ai-workflow', 'common-pitfalls', 'prop-types', 'css-scoping', 'form-handling'). Call `list_topics()` to see all available topic keys.", {
|
|
2121
|
-
topic: z
|
|
2122
|
-
.string()
|
|
2123
|
-
.describe("Topic key or keyword (call `list_topics()` for the full list)"),
|
|
2124
|
-
}, async ({ topic }) => {
|
|
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.", { topic: z.string().describe("Topic key or keyword (call `list_topics()` for the full list)") }, async ({ topic }) => {
|
|
2125
1900
|
const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
|
|
2126
1901
|
// Alias mapping for common alternative topic names
|
|
2127
1902
|
const topicAliases = {
|
|
2128
1903
|
"form-handling": "form-patterns",
|
|
2129
|
-
forms: "form-patterns",
|
|
1904
|
+
"forms": "form-patterns",
|
|
2130
1905
|
"data-fetching": "async-data-patterns",
|
|
2131
|
-
async: "async-data-patterns",
|
|
2132
|
-
loading: "async-data-patterns",
|
|
1906
|
+
"async": "async-data-patterns",
|
|
1907
|
+
"loading": "async-data-patterns",
|
|
2133
1908
|
"sub-components": "sub-component-patterns",
|
|
2134
|
-
subcomponents: "sub-component-patterns",
|
|
2135
|
-
routing: "navigation-patterns",
|
|
2136
|
-
router: "navigation-patterns",
|
|
2137
|
-
observer: "component-structure",
|
|
2138
|
-
reactivity: "component-structure",
|
|
2139
|
-
pitfalls: "common-pitfalls",
|
|
2140
|
-
gotchas: "common-pitfalls",
|
|
2141
|
-
mistakes: "common-pitfalls",
|
|
2142
|
-
header: "header-footer-patterns",
|
|
2143
|
-
footer: "header-footer-patterns",
|
|
2144
|
-
blog: "blog-patterns",
|
|
2145
|
-
cart: "cart-patterns",
|
|
2146
|
-
account: "account-patterns",
|
|
1909
|
+
"subcomponents": "sub-component-patterns",
|
|
1910
|
+
"routing": "navigation-patterns",
|
|
1911
|
+
"router": "navigation-patterns",
|
|
1912
|
+
"observer": "component-structure",
|
|
1913
|
+
"reactivity": "component-structure",
|
|
1914
|
+
"pitfalls": "common-pitfalls",
|
|
1915
|
+
"gotchas": "common-pitfalls",
|
|
1916
|
+
"mistakes": "common-pitfalls",
|
|
1917
|
+
"header": "header-footer-patterns",
|
|
1918
|
+
"footer": "header-footer-patterns",
|
|
1919
|
+
"blog": "blog-patterns",
|
|
1920
|
+
"cart": "cart-patterns",
|
|
1921
|
+
"account": "account-patterns",
|
|
2147
1922
|
"product-detail": "product-detail-patterns",
|
|
2148
1923
|
"product-list": "product-list-patterns",
|
|
2149
|
-
filtering: "product-list-patterns",
|
|
2150
|
-
reviews: "review-patterns",
|
|
2151
|
-
slider: "slider-overlay-patterns",
|
|
2152
|
-
overlay: "slider-overlay-patterns",
|
|
2153
|
-
modal: "slider-overlay-patterns",
|
|
2154
|
-
architecture: "real-world-architecture",
|
|
2155
|
-
theme: "real-world-architecture",
|
|
1924
|
+
"filtering": "product-list-patterns",
|
|
1925
|
+
"reviews": "review-patterns",
|
|
1926
|
+
"slider": "slider-overlay-patterns",
|
|
1927
|
+
"overlay": "slider-overlay-patterns",
|
|
1928
|
+
"modal": "slider-overlay-patterns",
|
|
1929
|
+
"architecture": "real-world-architecture",
|
|
1930
|
+
"theme": "real-world-architecture",
|
|
2156
1931
|
"global-styles": "global-css",
|
|
2157
|
-
global: "global-css",
|
|
1932
|
+
"global": "global-css",
|
|
2158
1933
|
"css-variables": "global-css",
|
|
2159
1934
|
"custom-properties": "global-css",
|
|
2160
1935
|
};
|
|
2161
1936
|
const resolvedTopic = topicAliases[topicLower] || topicLower;
|
|
2162
1937
|
// Topics that involve MobX store reads get a reminder about root reactivity
|
|
2163
1938
|
const storeTopics = new Set([
|
|
2164
|
-
"product-detail-patterns",
|
|
2165
|
-
"
|
|
2166
|
-
"
|
|
2167
|
-
"
|
|
2168
|
-
"header-footer-patterns",
|
|
2169
|
-
"review-patterns",
|
|
2170
|
-
"blog-patterns",
|
|
2171
|
-
"form-handling",
|
|
2172
|
-
"async-data-patterns",
|
|
2173
|
-
"component-structure",
|
|
2174
|
-
"imports",
|
|
1939
|
+
"product-detail-patterns", "product-list-patterns", "cart-patterns",
|
|
1940
|
+
"account-patterns", "header-footer-patterns", "review-patterns",
|
|
1941
|
+
"blog-patterns", "form-handling", "async-data-patterns",
|
|
1942
|
+
"component-structure", "imports",
|
|
2175
1943
|
]);
|
|
2176
1944
|
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";
|
|
2177
1945
|
// Try exact key match (with alias resolution)
|
|
2178
1946
|
if (frameworkData.topics[resolvedTopic]) {
|
|
2179
1947
|
const t = frameworkData.topics[resolvedTopic];
|
|
2180
1948
|
const prefix = storeTopics.has(resolvedTopic) ? observerReminder : "";
|
|
2181
|
-
return {
|
|
2182
|
-
content: [
|
|
2183
|
-
{
|
|
2184
|
-
type: "text",
|
|
2185
|
-
text: `## ${t.title}\n\n${prefix}${t.content}`,
|
|
2186
|
-
},
|
|
2187
|
-
],
|
|
2188
|
-
};
|
|
1949
|
+
return { content: [{ type: "text", text: `## ${t.title}\n\n${prefix}${t.content}` }] };
|
|
2189
1950
|
}
|
|
2190
1951
|
// Try original topic key (without alias) in case it's a direct key
|
|
2191
1952
|
if (resolvedTopic !== topicLower && frameworkData.topics[topicLower]) {
|
|
2192
1953
|
const t = frameworkData.topics[topicLower];
|
|
2193
1954
|
const prefix = storeTopics.has(topicLower) ? observerReminder : "";
|
|
2194
|
-
return {
|
|
2195
|
-
content: [
|
|
2196
|
-
{
|
|
2197
|
-
type: "text",
|
|
2198
|
-
text: `## ${t.title}\n\n${prefix}${t.content}`,
|
|
2199
|
-
},
|
|
2200
|
-
],
|
|
2201
|
-
};
|
|
1955
|
+
return { content: [{ type: "text", text: `## ${t.title}\n\n${prefix}${t.content}` }] };
|
|
2202
1956
|
}
|
|
2203
1957
|
// Try keyword search
|
|
2204
1958
|
const matches = searchFrameworkTopics(topic);
|
|
@@ -2206,12 +1960,7 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
|
|
|
2206
1960
|
const best = matches[0];
|
|
2207
1961
|
const prefix = storeTopics.has(best.key) ? observerReminder : "";
|
|
2208
1962
|
return {
|
|
2209
|
-
content: [
|
|
2210
|
-
{
|
|
2211
|
-
type: "text",
|
|
2212
|
-
text: `## ${best.topic.title}\n\n${prefix}${best.topic.content}`,
|
|
2213
|
-
},
|
|
2214
|
-
],
|
|
1963
|
+
content: [{ type: "text", text: `## ${best.topic.title}\n\n${prefix}${best.topic.content}` }],
|
|
2215
1964
|
};
|
|
2216
1965
|
}
|
|
2217
1966
|
const available = Object.entries(frameworkData.topics)
|
|
@@ -2227,27 +1976,16 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
|
|
|
2227
1976
|
};
|
|
2228
1977
|
});
|
|
2229
1978
|
// Tool: get_type_definition
|
|
2230
|
-
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.", {
|
|
2231
|
-
name: z
|
|
2232
|
-
.string()
|
|
2233
|
-
.describe("Type or enum name (e.g. 'IkasProduct', 'IkasOrderStatus')"),
|
|
2234
|
-
}, async ({ name }) => {
|
|
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.", { name: z.string().describe("Type or enum name (e.g. 'IkasProduct', 'IkasOrderStatus')") }, async ({ name }) => {
|
|
2235
1980
|
if (!typesData) {
|
|
2236
1981
|
return {
|
|
2237
|
-
content: [
|
|
2238
|
-
{
|
|
2239
|
-
type: "text",
|
|
2240
|
-
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2241
|
-
},
|
|
2242
|
-
],
|
|
1982
|
+
content: [{ type: "text", text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first." }],
|
|
2243
1983
|
};
|
|
2244
1984
|
}
|
|
2245
1985
|
const nameLower = name.toLowerCase();
|
|
2246
1986
|
const td = typesData.types.find((t) => t.name.toLowerCase() === nameLower);
|
|
2247
1987
|
if (td) {
|
|
2248
|
-
return {
|
|
2249
|
-
content: [{ type: "text", text: formatTypeDefinition(td) }],
|
|
2250
|
-
};
|
|
1988
|
+
return { content: [{ type: "text", text: formatTypeDefinition(td) }] };
|
|
2251
1989
|
}
|
|
2252
1990
|
// Fuzzy match
|
|
2253
1991
|
const matches = typesData.types.filter((t) => t.name.toLowerCase().includes(nameLower));
|
|
@@ -2263,28 +2001,14 @@ server.tool("get_type_definition", "Get the full definition of a storefront type
|
|
|
2263
2001
|
};
|
|
2264
2002
|
}
|
|
2265
2003
|
return {
|
|
2266
|
-
content: [
|
|
2267
|
-
{
|
|
2268
|
-
type: "text",
|
|
2269
|
-
text: `Type "${name}" not found. Use \`list_types()\` to see all available types.`,
|
|
2270
|
-
},
|
|
2271
|
-
],
|
|
2004
|
+
content: [{ type: "text", text: `Type "${name}" not found. Use \`list_types()\` to see all available types.` }],
|
|
2272
2005
|
};
|
|
2273
2006
|
});
|
|
2274
2007
|
// Tool: get_functions_for_type
|
|
2275
|
-
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.", {
|
|
2276
|
-
typeName: z
|
|
2277
|
-
.string()
|
|
2278
|
-
.describe("Type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
|
|
2279
|
-
}, async ({ typeName }) => {
|
|
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.", { typeName: z.string().describe("Type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')") }, async ({ typeName }) => {
|
|
2280
2009
|
if (!typesData) {
|
|
2281
2010
|
return {
|
|
2282
|
-
content: [
|
|
2283
|
-
{
|
|
2284
|
-
type: "text",
|
|
2285
|
-
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2286
|
-
},
|
|
2287
|
-
],
|
|
2011
|
+
content: [{ type: "text", text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first." }],
|
|
2288
2012
|
};
|
|
2289
2013
|
}
|
|
2290
2014
|
const nameLower = typeName.toLowerCase();
|
|
@@ -2304,12 +2028,7 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
|
|
|
2304
2028
|
};
|
|
2305
2029
|
}
|
|
2306
2030
|
return {
|
|
2307
|
-
content: [
|
|
2308
|
-
{
|
|
2309
|
-
type: "text",
|
|
2310
|
-
text: `Type "${typeName}" not found. Use \`list_types()\` to see all available types.`,
|
|
2311
|
-
},
|
|
2312
|
-
],
|
|
2031
|
+
content: [{ type: "text", text: `Type "${typeName}" not found. Use \`list_types()\` to see all available types.` }],
|
|
2313
2032
|
};
|
|
2314
2033
|
}
|
|
2315
2034
|
if (!td.relatedFunctions || td.relatedFunctions.length === 0) {
|
|
@@ -2340,10 +2059,7 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
|
|
|
2340
2059
|
});
|
|
2341
2060
|
// Tool: get_model_guide
|
|
2342
2061
|
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.", {
|
|
2343
|
-
model: z
|
|
2344
|
-
.string()
|
|
2345
|
-
.optional()
|
|
2346
|
-
.describe("Model type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
|
|
2062
|
+
model: z.string().optional().describe("Model type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
|
|
2347
2063
|
name: z.string().optional().describe("Alias for 'model'"),
|
|
2348
2064
|
mode: z
|
|
2349
2065
|
.enum(["summary", "full"])
|
|
@@ -2354,22 +2070,12 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
2354
2070
|
const model = modelParam || nameParam;
|
|
2355
2071
|
if (!model) {
|
|
2356
2072
|
return {
|
|
2357
|
-
content: [
|
|
2358
|
-
{
|
|
2359
|
-
type: "text",
|
|
2360
|
-
text: "Please provide a model name (e.g. 'IkasProduct', 'IkasOrder'). Use the 'model' or 'name' parameter.",
|
|
2361
|
-
},
|
|
2362
|
-
],
|
|
2073
|
+
content: [{ type: "text", text: "Please provide a model name (e.g. 'IkasProduct', 'IkasOrder'). Use the 'model' or 'name' parameter." }],
|
|
2363
2074
|
};
|
|
2364
2075
|
}
|
|
2365
2076
|
if (!typesData) {
|
|
2366
2077
|
return {
|
|
2367
|
-
content: [
|
|
2368
|
-
{
|
|
2369
|
-
type: "text",
|
|
2370
|
-
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2371
|
-
},
|
|
2372
|
-
],
|
|
2078
|
+
content: [{ type: "text", text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first." }],
|
|
2373
2079
|
};
|
|
2374
2080
|
}
|
|
2375
2081
|
const modelLower = model.toLowerCase();
|
|
@@ -2391,12 +2097,7 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
2391
2097
|
};
|
|
2392
2098
|
}
|
|
2393
2099
|
return {
|
|
2394
|
-
content: [
|
|
2395
|
-
{
|
|
2396
|
-
type: "text",
|
|
2397
|
-
text: `Model "${model}" not found. Use \`list_types()\` to see all available types.`,
|
|
2398
|
-
},
|
|
2399
|
-
],
|
|
2100
|
+
content: [{ type: "text", text: `Model "${model}" not found. Use \`list_types()\` to see all available types.` }],
|
|
2400
2101
|
};
|
|
2401
2102
|
}
|
|
2402
2103
|
const parts = [`# Model Guide: ${td.name}\n`];
|
|
@@ -2504,23 +2205,13 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
2504
2205
|
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 }) => {
|
|
2505
2206
|
if (!typesData) {
|
|
2506
2207
|
return {
|
|
2507
|
-
content: [
|
|
2508
|
-
{
|
|
2509
|
-
type: "text",
|
|
2510
|
-
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2511
|
-
},
|
|
2512
|
-
],
|
|
2208
|
+
content: [{ type: "text", text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first." }],
|
|
2513
2209
|
};
|
|
2514
2210
|
}
|
|
2515
2211
|
const results = searchTypes(query).slice(0, 15);
|
|
2516
2212
|
if (results.length === 0) {
|
|
2517
2213
|
return {
|
|
2518
|
-
content: [
|
|
2519
|
-
{
|
|
2520
|
-
type: "text",
|
|
2521
|
-
text: `No types found matching "${query}". Use \`list_types()\` to see all available types.`,
|
|
2522
|
-
},
|
|
2523
|
-
],
|
|
2214
|
+
content: [{ type: "text", text: `No types found matching "${query}". Use \`list_types()\` to see all available types.` }],
|
|
2524
2215
|
};
|
|
2525
2216
|
}
|
|
2526
2217
|
const parts = [`## Type Search Results for "${query}"\n`];
|
|
@@ -2584,7 +2275,7 @@ server.tool("list_types", "List storefront types and enums. Use `domain` and/or
|
|
|
2584
2275
|
for (const [d, count] of sorted) {
|
|
2585
2276
|
lines.push(`- \`${d}\` (${count})`);
|
|
2586
2277
|
}
|
|
2587
|
-
lines.push("",
|
|
2278
|
+
lines.push("", "Call `list_types(domain: \"<name>\")` to see summaries for a domain.");
|
|
2588
2279
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2589
2280
|
}
|
|
2590
2281
|
const domainLower = domain.toLowerCase();
|
|
@@ -2623,25 +2314,15 @@ server.tool("list_types", "List storefront types and enums. Use `domain` and/or
|
|
|
2623
2314
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
2624
2315
|
});
|
|
2625
2316
|
// Tool: get_prop_types
|
|
2626
|
-
server.tool("get_prop_types", "Get all available ikas.config.json prop types with descriptions, TypeScript types, and examples.
|
|
2317
|
+
server.tool("get_prop_types", "Get all available ikas.config.json prop types with descriptions, TypeScript types, and examples. Tip: Use `npx ikas-component config add-component --props '[...]'` to create a component with all props in one command, or `add-prop` to add props incrementally. NEVER manually edit types.ts — it is auto-generated by the CLI.", {}, async () => {
|
|
2627
2318
|
const propTypesTopic = frameworkData.topics["prop-types"];
|
|
2628
2319
|
if (propTypesTopic) {
|
|
2629
2320
|
return {
|
|
2630
|
-
content: [
|
|
2631
|
-
{
|
|
2632
|
-
type: "text",
|
|
2633
|
-
text: `## ${propTypesTopic.title}\n\n${propTypesTopic.content}`,
|
|
2634
|
-
},
|
|
2635
|
-
],
|
|
2321
|
+
content: [{ type: "text", text: `## ${propTypesTopic.title}\n\n${propTypesTopic.content}` }],
|
|
2636
2322
|
};
|
|
2637
2323
|
}
|
|
2638
2324
|
return {
|
|
2639
|
-
content: [
|
|
2640
|
-
{
|
|
2641
|
-
type: "text",
|
|
2642
|
-
text: "Prop types documentation not available.",
|
|
2643
|
-
},
|
|
2644
|
-
],
|
|
2325
|
+
content: [{ type: "text", text: "Prop types documentation not available." }],
|
|
2645
2326
|
};
|
|
2646
2327
|
});
|
|
2647
2328
|
// Tool: get_section_template
|
|
@@ -2738,8 +2419,7 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2738
2419
|
// the end of the response in the existing recipe-builder block.
|
|
2739
2420
|
{
|
|
2740
2421
|
const snippetStrForBanner = bundle.rootFiles["ikas-config-snippet.json"];
|
|
2741
|
-
if (snippetStrForBanner &&
|
|
2742
|
-
/<id-of-[A-Za-z0-9_]+>/.test(snippetStrForBanner)) {
|
|
2422
|
+
if (snippetStrForBanner && /<id-of-[A-Za-z0-9_]+>/.test(snippetStrForBanner)) {
|
|
2743
2423
|
const childMatches = Array.from(snippetStrForBanner.matchAll(/<id-of-([A-Za-z0-9_]+)>/g));
|
|
2744
2424
|
const uniqueChildren = Array.from(new Set(childMatches.map((m) => m[1])));
|
|
2745
2425
|
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.`, "");
|
|
@@ -2863,10 +2543,7 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2863
2543
|
try {
|
|
2864
2544
|
const childSnippet = JSON.parse(fs.readFileSync(childSnippetPath, "utf-8"));
|
|
2865
2545
|
const childProps = (childSnippet.props || []).map((p) => {
|
|
2866
|
-
const out = {
|
|
2867
|
-
name: p.name,
|
|
2868
|
-
type: p.type,
|
|
2869
|
-
};
|
|
2546
|
+
const out = { name: p.name, type: p.type };
|
|
2870
2547
|
if (p.displayName)
|
|
2871
2548
|
out.displayName = p.displayName;
|
|
2872
2549
|
if (p.required)
|
|
@@ -2939,7 +2616,10 @@ server.tool("get_section_child", "Fetch one item's files from a section's childr
|
|
|
2939
2616
|
.string()
|
|
2940
2617
|
.optional()
|
|
2941
2618
|
.describe("The item name as listed in `get_section_template`'s response (Children/Components/Sub-components)"),
|
|
2942
|
-
child: z
|
|
2619
|
+
child: z
|
|
2620
|
+
.string()
|
|
2621
|
+
.optional()
|
|
2622
|
+
.describe("Alias for `name`"),
|
|
2943
2623
|
kind: z
|
|
2944
2624
|
.enum(["children", "components", "sub-components"])
|
|
2945
2625
|
.optional()
|
|
@@ -3102,11 +2782,7 @@ server.tool("list_section_types", "List all available `get_section_template` sec
|
|
|
3102
2782
|
});
|
|
3103
2783
|
// --- Migration tools ---
|
|
3104
2784
|
// Tool: analyze_old_theme
|
|
3105
|
-
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.", {
|
|
3106
|
-
theme_json: z
|
|
3107
|
-
.string()
|
|
3108
|
-
.describe("The raw JSON content of the old theme.json file"),
|
|
3109
|
-
}, async ({ theme_json }) => {
|
|
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.", { theme_json: z.string().describe("The raw JSON content of the old theme.json file") }, async ({ theme_json }) => {
|
|
3110
2786
|
try {
|
|
3111
2787
|
const parsed = JSON.parse(theme_json);
|
|
3112
2788
|
const analysis = analyzeOldTheme(parsed);
|
|
@@ -3114,162 +2790,102 @@ server.tool("analyze_old_theme", "Analyze an old ikas storefront theme.json and
|
|
|
3114
2790
|
}
|
|
3115
2791
|
catch (err) {
|
|
3116
2792
|
return {
|
|
3117
|
-
content: [
|
|
3118
|
-
{
|
|
3119
|
-
type: "text",
|
|
3120
|
-
text: `Error parsing theme.json: ${err instanceof Error ? err.message : String(err)}. Make sure you're passing valid JSON.`,
|
|
3121
|
-
},
|
|
3122
|
-
],
|
|
2793
|
+
content: [{ type: "text", text: `Error parsing theme.json: ${err instanceof Error ? err.message : String(err)}. Make sure you're passing valid JSON.` }],
|
|
3123
2794
|
};
|
|
3124
2795
|
}
|
|
3125
2796
|
});
|
|
3126
2797
|
// Tool: get_migration_guide
|
|
3127
2798
|
const migrationTopicAliases = {
|
|
3128
|
-
overview: "migration-overview",
|
|
3129
|
-
migrate: "migration-overview",
|
|
3130
|
-
custom: "custom-data-conversion",
|
|
2799
|
+
"overview": "migration-overview",
|
|
2800
|
+
"migrate": "migration-overview",
|
|
2801
|
+
"custom": "custom-data-conversion",
|
|
3131
2802
|
"custom-data": "custom-data-conversion",
|
|
3132
|
-
customdata: "custom-data-conversion",
|
|
2803
|
+
"customdata": "custom-data-conversion",
|
|
3133
2804
|
"dynamic-list": "custom-data-conversion",
|
|
3134
2805
|
"component-list": "custom-data-conversion",
|
|
3135
|
-
slider: "prop-type-mapping",
|
|
3136
|
-
props: "prop-type-mapping",
|
|
2806
|
+
"slider": "prop-type-mapping",
|
|
2807
|
+
"props": "prop-type-mapping",
|
|
3137
2808
|
"prop-mapping": "prop-type-mapping",
|
|
3138
|
-
types: "prop-type-mapping",
|
|
3139
|
-
react: "react-to-preact",
|
|
3140
|
-
preact: "react-to-preact",
|
|
3141
|
-
observer: "react-to-preact",
|
|
3142
|
-
libraries: "library-replacements",
|
|
3143
|
-
swiper: "library-replacements",
|
|
3144
|
-
headlessui: "library-replacements",
|
|
3145
|
-
tailwind: "library-replacements",
|
|
3146
|
-
tailwindcss: "library-replacements",
|
|
3147
|
-
recharts: "library-replacements",
|
|
3148
|
-
marquee: "library-replacements",
|
|
3149
|
-
imports: "storefront-import-mapping",
|
|
3150
|
-
storefront: "storefront-import-mapping",
|
|
2809
|
+
"types": "prop-type-mapping",
|
|
2810
|
+
"react": "react-to-preact",
|
|
2811
|
+
"preact": "react-to-preact",
|
|
2812
|
+
"observer": "react-to-preact",
|
|
2813
|
+
"libraries": "library-replacements",
|
|
2814
|
+
"swiper": "library-replacements",
|
|
2815
|
+
"headlessui": "library-replacements",
|
|
2816
|
+
"tailwind": "library-replacements",
|
|
2817
|
+
"tailwindcss": "library-replacements",
|
|
2818
|
+
"recharts": "library-replacements",
|
|
2819
|
+
"marquee": "library-replacements",
|
|
2820
|
+
"imports": "storefront-import-mapping",
|
|
2821
|
+
"storefront": "storefront-import-mapping",
|
|
3151
2822
|
"bp-storefront": "storefront-import-mapping",
|
|
3152
2823
|
"theme-json": "theme-json-anatomy",
|
|
3153
|
-
anatomy: "theme-json-anatomy",
|
|
3154
|
-
decompose: "component-decomposition-strategy",
|
|
3155
|
-
decomposition: "component-decomposition-strategy",
|
|
3156
|
-
strategy: "component-decomposition-strategy",
|
|
3157
|
-
project: "complete-project-generation",
|
|
3158
|
-
generate: "complete-project-generation",
|
|
3159
|
-
generation: "complete-project-generation",
|
|
3160
|
-
settings: "settings-conversion",
|
|
3161
|
-
colors: "settings-conversion",
|
|
3162
|
-
fonts: "settings-conversion",
|
|
3163
|
-
find: "finding-new-system-equivalents",
|
|
3164
|
-
search: "finding-new-system-equivalents",
|
|
3165
|
-
discover: "finding-new-system-equivalents",
|
|
3166
|
-
equivalent: "finding-new-system-equivalents",
|
|
3167
|
-
equivalents: "finding-new-system-equivalents",
|
|
3168
|
-
replacement: "finding-new-system-equivalents",
|
|
2824
|
+
"anatomy": "theme-json-anatomy",
|
|
2825
|
+
"decompose": "component-decomposition-strategy",
|
|
2826
|
+
"decomposition": "component-decomposition-strategy",
|
|
2827
|
+
"strategy": "component-decomposition-strategy",
|
|
2828
|
+
"project": "complete-project-generation",
|
|
2829
|
+
"generate": "complete-project-generation",
|
|
2830
|
+
"generation": "complete-project-generation",
|
|
2831
|
+
"settings": "settings-conversion",
|
|
2832
|
+
"colors": "settings-conversion",
|
|
2833
|
+
"fonts": "settings-conversion",
|
|
2834
|
+
"find": "finding-new-system-equivalents",
|
|
2835
|
+
"search": "finding-new-system-equivalents",
|
|
2836
|
+
"discover": "finding-new-system-equivalents",
|
|
2837
|
+
"equivalent": "finding-new-system-equivalents",
|
|
2838
|
+
"equivalents": "finding-new-system-equivalents",
|
|
2839
|
+
"replacement": "finding-new-system-equivalents",
|
|
3169
2840
|
};
|
|
3170
2841
|
const migrationTopicKeys = migrationData
|
|
3171
2842
|
? Object.keys(migrationData.topics)
|
|
3172
2843
|
: [];
|
|
3173
|
-
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.`, {
|
|
3174
|
-
topic: z
|
|
3175
|
-
.string()
|
|
3176
|
-
.describe("Migration topic key, alias, or 'list' to see all topics"),
|
|
3177
|
-
}, async ({ topic }) => {
|
|
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.`, { topic: z.string().describe("Migration topic key, alias, or 'list' to see all topics") }, async ({ topic }) => {
|
|
3178
2845
|
if (!migrationData) {
|
|
3179
|
-
return {
|
|
3180
|
-
content: [
|
|
3181
|
-
{
|
|
3182
|
-
type: "text",
|
|
3183
|
-
text: "Migration data not available. Ensure data/migration.json exists.",
|
|
3184
|
-
},
|
|
3185
|
-
],
|
|
3186
|
-
};
|
|
2846
|
+
return { content: [{ type: "text", text: "Migration data not available. Ensure data/migration.json exists." }] };
|
|
3187
2847
|
}
|
|
3188
2848
|
if (topic.toLowerCase() === "list") {
|
|
3189
2849
|
const available = Object.entries(migrationData.topics)
|
|
3190
2850
|
.map(([key, t]) => `- \`${key}\` — ${t.title}: ${t.description}`)
|
|
3191
2851
|
.join("\n");
|
|
3192
|
-
return {
|
|
3193
|
-
content: [
|
|
3194
|
-
{
|
|
3195
|
-
type: "text",
|
|
3196
|
-
text: `## Available Migration Topics\n\n${available}`,
|
|
3197
|
-
},
|
|
3198
|
-
],
|
|
3199
|
-
};
|
|
2852
|
+
return { content: [{ type: "text", text: `## Available Migration Topics\n\n${available}` }] };
|
|
3200
2853
|
}
|
|
3201
2854
|
const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
|
|
3202
2855
|
const resolvedTopic = migrationTopicAliases[topicLower] || topicLower;
|
|
3203
2856
|
if (migrationData.topics[resolvedTopic]) {
|
|
3204
2857
|
const t = migrationData.topics[resolvedTopic];
|
|
3205
|
-
return {
|
|
3206
|
-
content: [
|
|
3207
|
-
{ type: "text", text: `## ${t.title}\n\n${t.content}` },
|
|
3208
|
-
],
|
|
3209
|
-
};
|
|
2858
|
+
return { content: [{ type: "text", text: `## ${t.title}\n\n${t.content}` }] };
|
|
3210
2859
|
}
|
|
3211
2860
|
// Try original key
|
|
3212
2861
|
if (resolvedTopic !== topicLower && migrationData.topics[topicLower]) {
|
|
3213
2862
|
const t = migrationData.topics[topicLower];
|
|
3214
|
-
return {
|
|
3215
|
-
content: [
|
|
3216
|
-
{ type: "text", text: `## ${t.title}\n\n${t.content}` },
|
|
3217
|
-
],
|
|
3218
|
-
};
|
|
2863
|
+
return { content: [{ type: "text", text: `## ${t.title}\n\n${t.content}` }] };
|
|
3219
2864
|
}
|
|
3220
2865
|
// Keyword search
|
|
3221
2866
|
const matches = searchMigrationTopics(topic);
|
|
3222
2867
|
if (matches.length > 0) {
|
|
3223
2868
|
const best = matches[0];
|
|
3224
|
-
return {
|
|
3225
|
-
content: [
|
|
3226
|
-
{
|
|
3227
|
-
type: "text",
|
|
3228
|
-
text: `## ${best.topic.title}\n\n${best.topic.content}`,
|
|
3229
|
-
},
|
|
3230
|
-
],
|
|
3231
|
-
};
|
|
2869
|
+
return { content: [{ type: "text", text: `## ${best.topic.title}\n\n${best.topic.content}` }] };
|
|
3232
2870
|
}
|
|
3233
2871
|
const available = Object.entries(migrationData.topics)
|
|
3234
2872
|
.map(([key, t]) => ` - \`${key}\` - ${t.title}`)
|
|
3235
2873
|
.join("\n");
|
|
3236
2874
|
return {
|
|
3237
|
-
content: [
|
|
3238
|
-
{
|
|
3239
|
-
type: "text",
|
|
3240
|
-
text: `Migration topic "${topic}" not found. Available topics:\n${available}`,
|
|
3241
|
-
},
|
|
3242
|
-
],
|
|
2875
|
+
content: [{ type: "text", text: `Migration topic "${topic}" not found. Available topics:\n${available}` }],
|
|
3243
2876
|
};
|
|
3244
2877
|
});
|
|
3245
2878
|
// Tool: get_migration_example
|
|
3246
|
-
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.`, {
|
|
3247
|
-
example: z.string().describe("Example name or 'list' to see all examples"),
|
|
3248
|
-
}, async ({ 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.`, { example: z.string().describe("Example name or 'list' to see all examples") }, async ({ example }) => {
|
|
3249
2880
|
if (example.toLowerCase() === "list") {
|
|
3250
2881
|
if (migrationExampleNames.length === 0) {
|
|
3251
|
-
return {
|
|
3252
|
-
content: [
|
|
3253
|
-
{ type: "text", text: "No migration examples available." },
|
|
3254
|
-
],
|
|
3255
|
-
};
|
|
2882
|
+
return { content: [{ type: "text", text: "No migration examples available." }] };
|
|
3256
2883
|
}
|
|
3257
|
-
const list = migrationExampleNames
|
|
3258
|
-
.map((name) => {
|
|
2884
|
+
const list = migrationExampleNames.map((name) => {
|
|
3259
2885
|
const ex = loadMigrationExample(name);
|
|
3260
|
-
return ex
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
})
|
|
3264
|
-
.join("\n");
|
|
3265
|
-
return {
|
|
3266
|
-
content: [
|
|
3267
|
-
{
|
|
3268
|
-
type: "text",
|
|
3269
|
-
text: `## Available Migration Examples\n\n${list}`,
|
|
3270
|
-
},
|
|
3271
|
-
],
|
|
3272
|
-
};
|
|
2886
|
+
return ex ? `- \`${name}\` — ${ex.title}: ${ex.description}` : `- \`${name}\``;
|
|
2887
|
+
}).join("\n");
|
|
2888
|
+
return { content: [{ type: "text", text: `## Available Migration Examples\n\n${list}` }] };
|
|
3273
2889
|
}
|
|
3274
2890
|
const exampleLower = example.toLowerCase();
|
|
3275
2891
|
let exName = migrationExampleNames.find((n) => n === exampleLower);
|
|
@@ -3279,26 +2895,19 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
|
|
|
3279
2895
|
if (!exName) {
|
|
3280
2896
|
const available = migrationExampleNames.join(", ");
|
|
3281
2897
|
return {
|
|
3282
|
-
content: [
|
|
3283
|
-
{
|
|
3284
|
-
type: "text",
|
|
3285
|
-
text: `Migration example "${example}" not found. Available: ${available}`,
|
|
3286
|
-
},
|
|
3287
|
-
],
|
|
2898
|
+
content: [{ type: "text", text: `Migration example "${example}" not found. Available: ${available}` }],
|
|
3288
2899
|
};
|
|
3289
2900
|
}
|
|
3290
2901
|
const ex = loadMigrationExample(exName);
|
|
3291
2902
|
if (!ex) {
|
|
3292
|
-
return {
|
|
3293
|
-
content: [
|
|
3294
|
-
{
|
|
3295
|
-
type: "text",
|
|
3296
|
-
text: `Failed to load migration example "${exName}".`,
|
|
3297
|
-
},
|
|
3298
|
-
],
|
|
3299
|
-
};
|
|
2903
|
+
return { content: [{ type: "text", text: `Failed to load migration example "${exName}".` }] };
|
|
3300
2904
|
}
|
|
3301
|
-
const parts = [
|
|
2905
|
+
const parts = [
|
|
2906
|
+
`## ${ex.title}`,
|
|
2907
|
+
"",
|
|
2908
|
+
ex.description,
|
|
2909
|
+
"",
|
|
2910
|
+
];
|
|
3302
2911
|
for (const [filename, content] of Object.entries(ex.files)) {
|
|
3303
2912
|
const ext = filename.split(".").pop() || "text";
|
|
3304
2913
|
const lang = ext === "tsx" || ext === "ts"
|
|
@@ -3317,31 +2926,13 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
|
|
|
3317
2926
|
});
|
|
3318
2927
|
// Tool: plan_migration
|
|
3319
2928
|
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).", {
|
|
3320
|
-
theme_json: z
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
.describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
|
|
3328
|
-
project_name: z
|
|
3329
|
-
.string()
|
|
3330
|
-
.optional()
|
|
3331
|
-
.describe("Target new project name, used to prefix migration-tracking IDs (default: 'my-theme')"),
|
|
3332
|
-
old_source_dir: z
|
|
3333
|
-
.string()
|
|
3334
|
-
.optional()
|
|
3335
|
-
.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."),
|
|
3336
|
-
project_root: z
|
|
3337
|
-
.string()
|
|
3338
|
-
.optional()
|
|
3339
|
-
.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."),
|
|
3340
|
-
overwrite: z
|
|
3341
|
-
.boolean()
|
|
3342
|
-
.optional()
|
|
3343
|
-
.describe("If MIGRATION.md already exists at <project_root>/MIGRATION.md and is non-empty, refuse the write unless this is true. Default: false."),
|
|
3344
|
-
}, async ({ theme_json, theme_json_path, project_name, old_source_dir, project_root, overwrite, }) => {
|
|
2929
|
+
theme_json: z.string().optional().describe("Raw JSON content of the old theme.json. EITHER this OR theme_json_path is required (not both). For real themes use theme_json_path — raw strings exceed tool/context limits at production sizes."),
|
|
2930
|
+
theme_json_path: z.string().optional().describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
|
|
2931
|
+
project_name: z.string().optional().describe("Target new project name, used to prefix migration-tracking IDs (default: 'my-theme')"),
|
|
2932
|
+
old_source_dir: z.string().optional().describe("Absolute path to the old project's src/ directory. When provided, the tool scans .tsx files to detect shared sub-components used across 3+ components. This scan is partial — atomic components used by only 1-2 sections will be missed and must be added by the LLM."),
|
|
2933
|
+
project_root: z.string().optional().describe("Absolute path to the new project root. When provided, the MCP writes MIGRATION.md to <project_root>/MIGRATION.md and returns a short summary instead of the full markdown body."),
|
|
2934
|
+
overwrite: z.boolean().optional().describe("If MIGRATION.md already exists at <project_root>/MIGRATION.md and is non-empty, refuse the write unless this is true. Default: false."),
|
|
2935
|
+
}, async ({ theme_json, theme_json_path, project_name, old_source_dir, project_root, overwrite }) => {
|
|
3345
2936
|
try {
|
|
3346
2937
|
const parsed = resolveThemeJson(theme_json, theme_json_path);
|
|
3347
2938
|
const projectName = project_name || "my-theme";
|
|
@@ -3402,37 +2993,18 @@ server.tool("plan_migration", "Generate the **initial** migration plan and (when
|
|
|
3402
2993
|
}
|
|
3403
2994
|
catch (err) {
|
|
3404
2995
|
return {
|
|
3405
|
-
content: [
|
|
3406
|
-
{
|
|
3407
|
-
type: "text",
|
|
3408
|
-
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
3409
|
-
},
|
|
3410
|
-
],
|
|
2996
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
3411
2997
|
};
|
|
3412
2998
|
}
|
|
3413
2999
|
});
|
|
3414
3000
|
// Tool: get_section_migration_plan
|
|
3415
3001
|
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.", {
|
|
3416
|
-
theme_json: z
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
.optional()
|
|
3423
|
-
.describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
|
|
3424
|
-
section_name: z
|
|
3425
|
-
.string()
|
|
3426
|
-
.describe("Old component name (e.g. 'Navbar', 'ProductGrid') or dir name, OR the new section ID (e.g. 'my-theme-navbar')"),
|
|
3427
|
-
project_name: z
|
|
3428
|
-
.string()
|
|
3429
|
-
.optional()
|
|
3430
|
-
.describe("Target new project name (must match what was used in plan_migration). Default: 'my-theme'"),
|
|
3431
|
-
old_source_dir: z
|
|
3432
|
-
.string()
|
|
3433
|
-
.optional()
|
|
3434
|
-
.describe("Absolute path to old src/ directory (used to output exact source file paths to read)"),
|
|
3435
|
-
}, async ({ theme_json, theme_json_path, section_name, project_name, old_source_dir, }) => {
|
|
3002
|
+
theme_json: z.string().optional().describe("Raw JSON content of the old theme.json. EITHER this OR theme_json_path is required (not both)."),
|
|
3003
|
+
theme_json_path: z.string().optional().describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
|
|
3004
|
+
section_name: z.string().describe("Old component name (e.g. 'Navbar', 'ProductGrid') or dir name, OR the new section ID (e.g. 'my-theme-navbar')"),
|
|
3005
|
+
project_name: z.string().optional().describe("Target new project name (must match what was used in plan_migration). Default: 'my-theme'"),
|
|
3006
|
+
old_source_dir: z.string().optional().describe("Absolute path to old src/ directory (used to output exact source file paths to read)"),
|
|
3007
|
+
}, async ({ theme_json, theme_json_path, section_name, project_name, old_source_dir }) => {
|
|
3436
3008
|
try {
|
|
3437
3009
|
const parsed = resolveThemeJson(theme_json, theme_json_path);
|
|
3438
3010
|
const projectName = project_name || "my-theme";
|
|
@@ -3441,12 +3013,7 @@ server.tool("get_section_migration_plan", "Returns concrete CLI commands and pro
|
|
|
3441
3013
|
}
|
|
3442
3014
|
catch (err) {
|
|
3443
3015
|
return {
|
|
3444
|
-
content: [
|
|
3445
|
-
{
|
|
3446
|
-
type: "text",
|
|
3447
|
-
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
3448
|
-
},
|
|
3449
|
-
],
|
|
3016
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
3450
3017
|
};
|
|
3451
3018
|
}
|
|
3452
3019
|
});
|
|
@@ -3476,22 +3043,13 @@ function resolveIkasComponentBinary(projectRoot) {
|
|
|
3476
3043
|
async function runIkasComponentCli(projectRoot, args) {
|
|
3477
3044
|
const bin = resolveIkasComponentBinary(projectRoot);
|
|
3478
3045
|
return new Promise((resolve) => {
|
|
3479
|
-
execFile(bin, args,
|
|
3480
|
-
// Large editor responses (e.g. list_categories on a big store, or
|
|
3481
|
-
// add_sections_to_page echoing many sections) easily exceed Node's default
|
|
3482
|
-
// 1MB stdout buffer, which truncates output mid-JSON and yields a
|
|
3483
|
-
// "no parseable JSON" error. Give it generous headroom.
|
|
3484
|
-
{ cwd: projectRoot, windowsHide: true, maxBuffer: 64 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
3046
|
+
execFile(bin, args, { cwd: projectRoot, windowsHide: true }, (err, stdout, stderr) => {
|
|
3485
3047
|
const exitCode = err && typeof err.code === "number"
|
|
3486
3048
|
? err.code
|
|
3487
3049
|
: err
|
|
3488
3050
|
? 1
|
|
3489
3051
|
: 0;
|
|
3490
|
-
resolve({
|
|
3491
|
-
stdout: stdout?.toString() ?? "",
|
|
3492
|
-
stderr: stderr?.toString() ?? "",
|
|
3493
|
-
exitCode,
|
|
3494
|
-
});
|
|
3052
|
+
resolve({ stdout: stdout?.toString() ?? "", stderr: stderr?.toString() ?? "", exitCode });
|
|
3495
3053
|
});
|
|
3496
3054
|
});
|
|
3497
3055
|
}
|
|
@@ -3499,25 +3057,15 @@ function parseCliJson(stdout) {
|
|
|
3499
3057
|
const trimmed = stdout.trim();
|
|
3500
3058
|
if (!trimmed)
|
|
3501
3059
|
return null;
|
|
3502
|
-
//
|
|
3060
|
+
// CLI prints exactly one JSON object on stdout; if multiple lines appeared
|
|
3061
|
+
// (e.g., warnings on stderr leaked), take the last non-empty line.
|
|
3062
|
+
const last = trimmed.split("\n").map(l => l.trim()).filter(Boolean).pop();
|
|
3063
|
+
if (!last)
|
|
3064
|
+
return null;
|
|
3503
3065
|
try {
|
|
3504
|
-
return JSON.parse(
|
|
3066
|
+
return JSON.parse(last);
|
|
3505
3067
|
}
|
|
3506
3068
|
catch {
|
|
3507
|
-
// Fallback: if anything leaked onto stdout before/after the JSON, scan the
|
|
3508
|
-
// lines and return the last one that parses as JSON.
|
|
3509
|
-
const lines = trimmed
|
|
3510
|
-
.split("\n")
|
|
3511
|
-
.map((l) => l.trim())
|
|
3512
|
-
.filter(Boolean);
|
|
3513
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3514
|
-
try {
|
|
3515
|
-
return JSON.parse(lines[i]);
|
|
3516
|
-
}
|
|
3517
|
-
catch {
|
|
3518
|
-
// keep scanning
|
|
3519
|
-
}
|
|
3520
|
-
}
|
|
3521
3069
|
return null;
|
|
3522
3070
|
}
|
|
3523
3071
|
}
|
|
@@ -3526,11 +3074,7 @@ async function callEditorAction(projectRoot, args) {
|
|
|
3526
3074
|
const { stdout, stderr, exitCode } = await runIkasComponentCli(projectRoot, args);
|
|
3527
3075
|
const parsed = parseCliJson(stdout);
|
|
3528
3076
|
if (parsed) {
|
|
3529
|
-
return {
|
|
3530
|
-
content: [
|
|
3531
|
-
{ type: "text", text: JSON.stringify(parsed, null, 2) },
|
|
3532
|
-
],
|
|
3533
|
-
};
|
|
3077
|
+
return { content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }] };
|
|
3534
3078
|
}
|
|
3535
3079
|
return {
|
|
3536
3080
|
content: [
|
|
@@ -3553,77 +3097,38 @@ async function callEditorAction(projectRoot, args) {
|
|
|
3553
3097
|
};
|
|
3554
3098
|
}
|
|
3555
3099
|
}
|
|
3556
|
-
//
|
|
3557
3100
|
// Tool: list_editor_pages
|
|
3558
3101
|
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`.", {
|
|
3559
|
-
project_root: z
|
|
3560
|
-
|
|
3561
|
-
.describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
|
|
3562
|
-
port: z
|
|
3563
|
-
.number()
|
|
3564
|
-
.optional()
|
|
3565
|
-
.describe("Dev server WebSocket port (default 5201)."),
|
|
3102
|
+
project_root: z.string().describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
|
|
3103
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3566
3104
|
}, async ({ project_root, port }) => {
|
|
3567
3105
|
const args = ["list-pages", ...(port ? ["--port", String(port)] : [])];
|
|
3568
3106
|
return callEditorAction(project_root, args);
|
|
3569
3107
|
});
|
|
3570
3108
|
// Tool: list_imported_sections
|
|
3571
3109
|
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`.", {
|
|
3572
|
-
project_root: z
|
|
3573
|
-
|
|
3574
|
-
.describe("Absolute path to the code-component project."),
|
|
3575
|
-
port: z
|
|
3576
|
-
.number()
|
|
3577
|
-
.optional()
|
|
3578
|
-
.describe("Dev server WebSocket port (default 5201)."),
|
|
3110
|
+
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3111
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3579
3112
|
}, async ({ project_root, port }) => {
|
|
3580
|
-
const args = [
|
|
3581
|
-
"list-imported",
|
|
3582
|
-
"--sections-only",
|
|
3583
|
-
...(port ? ["--port", String(port)] : []),
|
|
3584
|
-
];
|
|
3113
|
+
const args = ["list-imported", "--sections-only", ...(port ? ["--port", String(port)] : [])];
|
|
3585
3114
|
return callEditorAction(project_root, args);
|
|
3586
3115
|
});
|
|
3587
3116
|
// Tool: import_section
|
|
3588
3117
|
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`).", {
|
|
3589
|
-
project_root: z
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
component_id: z
|
|
3593
|
-
.string()
|
|
3594
|
-
.describe("Component id from `ikas.config.json` (strict — no name resolution)."),
|
|
3595
|
-
port: z
|
|
3596
|
-
.number()
|
|
3597
|
-
.optional()
|
|
3598
|
-
.describe("Dev server WebSocket port (default 5201)."),
|
|
3118
|
+
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3119
|
+
component_id: z.string().describe("Component id from `ikas.config.json` (strict — no name resolution)."),
|
|
3120
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3599
3121
|
}, async ({ project_root, component_id, port }) => {
|
|
3600
|
-
const args = [
|
|
3601
|
-
"import",
|
|
3602
|
-
"--id",
|
|
3603
|
-
component_id,
|
|
3604
|
-
...(port ? ["--port", String(port)] : []),
|
|
3605
|
-
];
|
|
3122
|
+
const args = ["import", "--id", component_id, ...(port ? ["--port", String(port)] : [])];
|
|
3606
3123
|
return callEditorAction(project_root, args);
|
|
3607
3124
|
});
|
|
3608
3125
|
// Tool: add_section_to_page
|
|
3609
|
-
server.tool("add_section_to_page",
|
|
3610
|
-
project_root: z
|
|
3611
|
-
|
|
3612
|
-
.describe("Absolute path to the code-component project."),
|
|
3613
|
-
component_id: z
|
|
3614
|
-
.string()
|
|
3615
|
-
.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)."),
|
|
3126
|
+
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.", {
|
|
3127
|
+
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3128
|
+
component_id: z.string().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)."),
|
|
3616
3129
|
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3617
|
-
index: z
|
|
3618
|
-
|
|
3619
|
-
.int()
|
|
3620
|
-
.nonnegative()
|
|
3621
|
-
.optional()
|
|
3622
|
-
.describe("Zero-based insertion index in the page; appends when omitted."),
|
|
3623
|
-
port: z
|
|
3624
|
-
.number()
|
|
3625
|
-
.optional()
|
|
3626
|
-
.describe("Dev server WebSocket port (default 5201)."),
|
|
3130
|
+
index: z.number().int().nonnegative().optional().describe("Zero-based insertion index in the page; appends when omitted."),
|
|
3131
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3627
3132
|
}, async ({ project_root, component_id, page_id, index, port }) => {
|
|
3628
3133
|
const args = [
|
|
3629
3134
|
"add-to-page",
|
|
@@ -3636,419 +3141,6 @@ server.tool("add_section_to_page", 'Place a SINGLE already-imported section-type
|
|
|
3636
3141
|
];
|
|
3637
3142
|
return callEditorAction(project_root, args);
|
|
3638
3143
|
});
|
|
3639
|
-
// Tool: add_sections_to_page
|
|
3640
|
-
server.tool("add_sections_to_page", "Place MANY sections on a page in a SINGLE call — and optionally set each section's prop values AT placement time, so you skip the separate fill step. The fastest way to build a page: one round-trip instead of one add (and one fill) per section. Pass `sections`, each `{ component_id, index?, updates?: [{ prop_name|prop_id, value }] }`; values use the same per-type shapes as update_section_prop / get_editor_workflow. Returns the new `elementId` for each placement (plus written prop results). Built-but-unimported section AND child code components are auto-imported. All update values are validated before anything is placed. Use `list_editor_pages` for page ids and `list_imported_sections` for component ids.", {
|
|
3641
|
-
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3642
|
-
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3643
|
-
sections: z
|
|
3644
|
-
.array(z.object({
|
|
3645
|
-
component_id: z.string().describe("Imported (or built) section-type code component id."),
|
|
3646
|
-
index: z
|
|
3647
|
-
.number()
|
|
3648
|
-
.int()
|
|
3649
|
-
.nonnegative()
|
|
3650
|
-
.optional()
|
|
3651
|
-
.describe("Zero-based insertion index; appends when omitted."),
|
|
3652
|
-
updates: z
|
|
3653
|
-
.array(z.object({
|
|
3654
|
-
prop_id: z.string().optional().describe("Blueprint prop id (provide this or prop_name)."),
|
|
3655
|
-
prop_name: z.string().optional().describe("Blueprint prop name (alternative to prop_id)."),
|
|
3656
|
-
value: z.any().describe("Prop value, same shape update_section_prop expects for that type."),
|
|
3657
|
-
}))
|
|
3658
|
-
.optional()
|
|
3659
|
-
.describe("Optional prop values to set on this section right after placing it."),
|
|
3660
|
-
}))
|
|
3661
|
-
.describe("Non-empty array of sections to place (in order), each with its componentId and optional prop updates."),
|
|
3662
|
-
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3663
|
-
}, async ({ project_root, page_id, sections, port }) => {
|
|
3664
|
-
const normalized = (sections || []).map(s => ({
|
|
3665
|
-
componentId: s.component_id,
|
|
3666
|
-
...(typeof s.index === "number" ? { index: s.index } : {}),
|
|
3667
|
-
...(s.updates
|
|
3668
|
-
? {
|
|
3669
|
-
updates: s.updates.map(u => ({
|
|
3670
|
-
...(u.prop_id ? { propId: u.prop_id } : {}),
|
|
3671
|
-
...(u.prop_name ? { propName: u.prop_name } : {}),
|
|
3672
|
-
value: u.value,
|
|
3673
|
-
})),
|
|
3674
|
-
}
|
|
3675
|
-
: {}),
|
|
3676
|
-
}));
|
|
3677
|
-
const args = [
|
|
3678
|
-
"add-sections-to-page",
|
|
3679
|
-
"--page-id",
|
|
3680
|
-
page_id,
|
|
3681
|
-
"--sections",
|
|
3682
|
-
JSON.stringify(normalized),
|
|
3683
|
-
...(port ? ["--port", String(port)] : []),
|
|
3684
|
-
];
|
|
3685
|
-
return callEditorAction(project_root, args);
|
|
3686
|
-
});
|
|
3687
|
-
// Tool: list_page_sections
|
|
3688
|
-
server.tool("list_page_sections", "List the sections placed on a page — a LEAN ROSTER that never truncates: per-placement `elementId` (the identity of THIS placement — there can be multiple of the same section), `sectionId`, `componentId`, `name`, `propCount`, and `setPropNames` (which props already have a value). It deliberately omits the prop SCHEMA and the prop VALUES (both can be large — Header alone can have ~80 props). To get a section's prop schema (types, ENUM options, allowed children) call `get_component_props(componentId)`; to read its current values call `get_section_values(element_id)`. Use the returned `elementId` with `update_section_prop` / `update_page_sections`. Use `list_editor_pages` for page ids.", {
|
|
3689
|
-
project_root: z
|
|
3690
|
-
.string()
|
|
3691
|
-
.describe("Absolute path to the code-component project."),
|
|
3692
|
-
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3693
|
-
port: z
|
|
3694
|
-
.number()
|
|
3695
|
-
.optional()
|
|
3696
|
-
.describe("Dev server WebSocket port (default 5201)."),
|
|
3697
|
-
}, async ({ project_root, page_id, port }) => {
|
|
3698
|
-
const args = [
|
|
3699
|
-
"list-page-sections",
|
|
3700
|
-
"--page-id",
|
|
3701
|
-
page_id,
|
|
3702
|
-
...(port ? ["--port", String(port)] : []),
|
|
3703
|
-
];
|
|
3704
|
-
return callEditorAction(project_root, args);
|
|
3705
|
-
});
|
|
3706
|
-
// Tool: get_component_props
|
|
3707
|
-
server.tool("get_component_props", "Get prop blueprints straight from the editor (the source of truth) for one or MANY components in a single call — for any section OR child code component ids. Returns `{ components: [...] }`, each prop carrying its `id`, `name`, `type`, `required`, `default`, and crucially: a ready-to-use `writeExample` (the EXACT value shape to pass to update_section_prop / update_page_sections — e.g. PRODUCT_LIST, BLOG_LIST, COMPONENT_LIST, LINK) plus a `writeNote` for variants, so you do NOT have to guess or trial-write the JSON; for ENUM props the `options` (valid `value`s); and for COMPONENT/COMPONENT_LIST props the `allowedComponentIds` (which children the slot permits — when empty, an `allowedComponentsNote` says any imported code component is allowed). Use this (its `writeExample`/`options`/`allowedComponentIds`) instead of guessing or reading ikas.config.json. Pass ALL the section/child ids you need at once (batch). Child component ids come from a parent prop's `allowedComponentIds` (or get_section_template).", {
|
|
3708
|
-
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3709
|
-
component_ids: z
|
|
3710
|
-
.array(z.string())
|
|
3711
|
-
.describe("Section/child code-component ids to resolve (batch). From list_imported_sections or a COMPONENT_LIST prop's allowedComponentIds."),
|
|
3712
|
-
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3713
|
-
}, async ({ project_root, component_ids, port }) => {
|
|
3714
|
-
const args = [
|
|
3715
|
-
"get-component-props",
|
|
3716
|
-
"--component-ids",
|
|
3717
|
-
(component_ids || []).join(","),
|
|
3718
|
-
...(port ? ["--port", String(port)] : []),
|
|
3719
|
-
];
|
|
3720
|
-
return callEditorAction(project_root, args);
|
|
3721
|
-
});
|
|
3722
|
-
// Tool: get_section_values
|
|
3723
|
-
server.tool("get_section_values", "Get the CURRENT prop values of one or MANY placed sections (each with its full `propValues` plus blueprint `props` — every prop carrying a `writeExample`/`writeNote` for the exact value shape, plus ENUM `options` and COMPONENT_LIST `allowedComponentIds`). `list_page_sections` is lean and omits values to avoid truncation; use this to read sections' values — needed for read-modify-write of a COMPONENT_LIST, or to inspect the exact stored shape of an existing value. Returns `{ sections: [...] }`. Pass all the elementIds you need at once (batch).", {
|
|
3724
|
-
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3725
|
-
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3726
|
-
element_ids: z
|
|
3727
|
-
.array(z.string())
|
|
3728
|
-
.describe("Placed-section elementIds to read (batch). From `list_page_sections`."),
|
|
3729
|
-
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3730
|
-
}, async ({ project_root, page_id, element_ids, port }) => {
|
|
3731
|
-
const args = [
|
|
3732
|
-
"get-section-values",
|
|
3733
|
-
"--page-id",
|
|
3734
|
-
page_id,
|
|
3735
|
-
"--element-ids",
|
|
3736
|
-
(element_ids || []).join(","),
|
|
3737
|
-
...(port ? ["--port", String(port)] : []),
|
|
3738
|
-
];
|
|
3739
|
-
return callEditorAction(project_root, args);
|
|
3740
|
-
});
|
|
3741
|
-
// Tool: update_section_prop
|
|
3742
|
-
server.tool("update_section_prop", 'Change a single prop value of a section placed on a page. This is the tool for FILLING/SETTING content (heading, text, image, link, etc.) on an existing section — it is pure data entry: it writes NO component code and needs NO rebuild. Do not author a new prop or component to set a value the section already supports; first check the section\'s existing `props` via `list_page_sections`. Target the specific placement by its `element_id` (from `list_page_sections`) and the prop by `prop_id` or `prop_name` (also from `list_page_sections`). `value` is the prop value object as stored by the editor — for scalar props it is wrapped as `{ "value": <scalar> }` (e.g. `{ "value": "Hello" }` for TEXT, `{ "value": true }` for BOOLEAN, `{ "value": 12 }` for NUMBER, `{ "value": "#FF0000" }` for COLOR, `{ "value": "<enum-key>" }` for ENUM). Richer object prop types use their own object shape and are NOT `{ "value": ... }`-wrapped (full catalog in `get_editor_workflow`): IMAGE = `{ "id": "<asset-id>", "altText"?: "<alt>", "isVideo"?: false }` (the `id` MUST reference an already-uploaded asset — call `upload_image` with a `file_path`/`image_url` to get one, or reuse an existing `id` from another section\'s `propValues`); IMAGE_LIST = `{ "images": [ <image>, ... ] }`; VIDEO = `{ "video": { "id": "<asset-id>" }, "thumbnailImage"?: { "id": "..." }, "autoplay"?: false, "controls"?: true, "loop"?: false, "muted"?: false }` (NOT a bare `{ "id": ... }`); SVG = `{ "value": "<svg markup>" }` (value-wrapped, like a scalar); SVG_LIST = `{ "svgs": [ "<svg>", ... ] }`; entity props PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY/RAFFLE = `{ "<entity>Id": "...", "usePageData"?: false }` (e.g. `{ "categoryId": "..." }`) — get real ids from `search_products` (PRODUCT), `list_categories`, `list_brands`, `list_blogs`, `list_blog_categories`; list/collection props are typed objects keyed by a `<entity>ListType` discriminator: PRODUCT_LIST = `{ "id": "<unique>", "productListType": "STATIC", "productIds": [ { "productId": "...", "variantId": "..." } ], "initialLimit"?: 12 }` — note productIds is an array of {productId, variantId} PAIRS (from search_products), not bare ids; productListType ∈ ALL|STATIC|DISCOUNTED|RECOMMENDED|CATEGORY|SEARCH|LAST_VIEWED|RELATED_PRODUCTS|VIEWED_TOGETHER|PURCHASED_TOGETHER, and for a dynamic type use e.g. `{ "id": "<unique>", "productListType": "CATEGORY", "category": "<categoryId>" }` (no productIds) or `"ALL"` (no ids). CATEGORY_LIST = `{ "categoryListType": "STATIC", "categoryIds": ["..."] }` or `{ "categoryListType": "ALL" }`. BRAND_LIST = `{ "brandListType": "STATIC", "brandIds": ["..."] }` or ALL. BLOG_LIST = `{ "blogListType": "STATIC", "blogIds": ["..."] }`, `{ "blogListType": "CATEGORY", "categoryId": "..." }`, or ALL. BLOG_CATEGORY_LIST = `{ "blogCategoryListType": "STATIC", "blogCategoryIds": ["..."] }` or ALL. (Get the entity ids from search_products / list_*; you can also read an existing value with `get_section_values` to copy its exact shape.) LINK = a single link object `{ "id": "<unique-short-id>", "linkType": "PAGE"|"EXTERNAL"|"FILE", "label": "...", "openInNewTab"?: false, "subLinks": [] }` plus the target for its type — for PAGE add `"pageId"` AND `"pageType"` (both from `list_editor_pages`, e.g. pageType "INDEX"; for dynamic page types like PRODUCT/CATEGORY/BLOG also add the target `"itemId"`), for EXTERNAL add `"externalLink": "https://..."`. Examples: PAGE → `{ "id": "k3p9x", "linkType": "PAGE", "label": "Home", "pageId": "<page-id>", "pageType": "INDEX", "openInNewTab": false, "subLinks": [] }`; EXTERNAL → `{ "id": "m7q2z", "linkType": "EXTERNAL", "label": "Docs", "externalLink": "https://example.com", "openInNewTab": true, "subLinks": [] }`. To link to an ENTITY (category/brand/blog/blog-category) do NOT guess a slug — get the entity id from `list_categories`/`list_brands`/`list_blogs`/`list_blog_categories` and the page id from `get_page_by_type("<TYPE>")`, then build `{ "id": "<unique>", "linkType": "PAGE", "label": "...", "pageId": <pageId>, "pageType": "<TYPE>", "itemId": <entityId>, "subLinks": [] }`. LIST_OF_LINK = `{ "links": [ <link>, ... ] }` (each item is a link object as above). PRODUCT = `{ "productId": "...", "variantId"?: "..." }`. This SAME unwrapped shape applies whether the prop sits at section level OR nested inside a COMPONENT_LIST entry\'s `propValues` — there is no section-vs-nested difference. IMPORTANT: pass `value` as the parsed JSON object/array itself, NEVER as a JSON string (a stringified value is double-encoded and stored as a useless string). Values are validated server-side (deeply, including nested COMPONENT_LIST children): wrong shapes AND wrong semantics are REJECTED with an explanatory error instead of being silently stored. This includes: a `{ "value": [...] }` wrapper for a COMPONENT_LIST; a `{ "value": ... }`-wrapped IMAGE; `props` instead of `propValues`; a missing/duplicate entry `id`; a child referenced by the wrong key (a code component MUST use `codeComponentId`, a theme component `componentId`) or by an id that does not exist (child ids are opaque — take them from `get_section_template` / `list_imported_sections`, never invent them); and any `propValues` key that is not a real prop of that child component (the error lists the valid prop names). Auto-import: a referenced child code component that has been BUILT but not yet imported into the theme is imported automatically before the value is set (the response lists any such ids under `autoImported`); only children that are not built at all fail with a "no imported code component" error — build them first (`ikas-component build`/`dev`). For any prop type you are unsure about, inspect the existing value with `get_section_values` and match its exact shape. The change is applied with undo support.\n\n' +
|
|
3743
|
-
'COMPONENT_LIST / COMPONENT props: the value is NOT wrapped in `{ "value": ... }`. A COMPONENT_LIST value is `{ "components": [ <entry>, ... ] }`; a single COMPONENT value is one `<entry>`. Each entry = `{ "id": "<unique-id>", "codeComponentId": "<child-id>" | "componentId": "<child-id>", "propValues": { "<childPropName>": <wrapped-value>, ... } }`. Rules: (1) `id` is a unique short alphanumeric string identifying THIS entry — generate a fresh one per new entry and NEVER reuse an existing entry\'s id. (2) Use `codeComponentId` for code components, `componentId` for built-in theme components. (3) `propValues` is keyed by the CHILD component\'s prop NAMES, each using the same scalar wrappers as above (nesting is recursive — a child may itself hold a COMPONENT_LIST). (4) `update_section_prop` REPLACES the whole prop value — there is NO partial/deep merge. ALWAYS send the COMPLETE, fully-nested value for the prop: every existing entry (with its id), every nested `propValues`, and every nested COMPONENT_LIST at every depth. To change anything (even one deeply-nested scalar, or to add/remove/reorder a child), READ-MODIFY-WRITE: take the current `{ "components": [...] }` from `get_section_values`, edit it in place (preserving all other entries, ids, and nested structures), then send the ENTIRE updated object back. Sending only the changed part — a single entry, or a child without its siblings — wipes everything you omitted. (5) You can only add child component types the prop permits — call `get_component_props(parentComponentId)` to read the COMPONENT_LIST prop\'s `allowedComponentIds`, then `get_component_props(childId)` to learn that child\'s props (names, types, ENUM `options`/valid values) instead of guessing or reading ikas.config.json. The allowed set is wired at build/config time per the `get_section_template` setup recipe (`config update-prop`), not here.\n\n' +
|
|
3744
|
-
'Example — a full COMPONENT_LIST `value` with two children, where the second child itself nests another COMPONENT_LIST (note: scalar leaves are `{ "value": ... }`-wrapped, COMPONENT_LIST values are `{ "components": [...] }` and are NOT wrapped, every entry has a unique `id`, and this is the COMPLETE value you would send even to change just one field): ' +
|
|
3745
|
-
`{ "components": [ { "id": "a1b2c", "codeComponentId": "7ojrigep-Eml9n5sN3i", "propValues": { "heading": { "value": "Featured" }, "visible": { "value": true } } }, { "id": "d3e4f", "codeComponentId": "x2plk9zq-Qw8rt", "propValues": { "title": { "value": "Sub group" }, "cards": { "components": [ { "id": "g5h6i", "codeComponentId": "card01ab-Zz12", "propValues": { "label": { "value": "Card 1" } } }, { "id": "j7k8l", "codeComponentId": "card01ab-Zz12", "propValues": { "label": { "value": "Card 2" } } } ] } } } ] }`, {
|
|
3746
|
-
project_root: z
|
|
3747
|
-
.string()
|
|
3748
|
-
.describe("Absolute path to the code-component project."),
|
|
3749
|
-
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3750
|
-
element_id: z
|
|
3751
|
-
.string()
|
|
3752
|
-
.describe("Placed-section elementId identifying THIS placement on the page (from `list_page_sections`)."),
|
|
3753
|
-
prop_id: z
|
|
3754
|
-
.string()
|
|
3755
|
-
.optional()
|
|
3756
|
-
.describe("Blueprint prop id to update (from `list_page_sections`). Provide this or `prop_name`."),
|
|
3757
|
-
prop_name: z
|
|
3758
|
-
.string()
|
|
3759
|
-
.optional()
|
|
3760
|
-
.describe("Blueprint prop name to update (alternative to `prop_id`)."),
|
|
3761
|
-
value: z
|
|
3762
|
-
.any()
|
|
3763
|
-
.describe('The prop value object to store, e.g. `{ "value": "Hello" }`. Match the shape of the existing value from `get_section_values` for non-scalar prop types.'),
|
|
3764
|
-
port: z
|
|
3765
|
-
.number()
|
|
3766
|
-
.optional()
|
|
3767
|
-
.describe("Dev server WebSocket port (default 5201)."),
|
|
3768
|
-
}, async ({ project_root, page_id, element_id, prop_id, prop_name, value, port }) => {
|
|
3769
|
-
const args = [
|
|
3770
|
-
"update-section-prop",
|
|
3771
|
-
"--page-id",
|
|
3772
|
-
page_id,
|
|
3773
|
-
"--element-id",
|
|
3774
|
-
element_id,
|
|
3775
|
-
...(prop_id ? ["--prop-id", prop_id] : []),
|
|
3776
|
-
...(prop_name ? ["--prop-name", prop_name] : []),
|
|
3777
|
-
"--value",
|
|
3778
|
-
// The CLI JSON.parses --value. If `value` is already a JSON string, pass
|
|
3779
|
-
// it through verbatim — re-stringifying it would double-encode it and the
|
|
3780
|
-
// editor would store a string instead of the object/array.
|
|
3781
|
-
typeof value === "string" ? value : JSON.stringify(value),
|
|
3782
|
-
...(port ? ["--port", String(port)] : []),
|
|
3783
|
-
];
|
|
3784
|
-
return callEditorAction(project_root, args);
|
|
3785
|
-
});
|
|
3786
|
-
// Tool: upload_image
|
|
3787
|
-
server.tool("upload_image", "Upload an image to the connected editor's project and get back its image `id`. Provide the image via `file_path` (a local path) OR `image_url`. Use the returned `id` as the `id` of an IMAGE prop value passed to `update_section_prop` (e.g. `{ \"id\": \"<returned-id>\", \"altText\": \"...\", \"isVideo\": false }`). This is the way to set an image prop — `update_section_prop` does not upload, it only references an existing image id. Supported types: png, jpg, jpeg, webp, gif; max 10MB. Requires the editor to be embedded in the ikas admin panel (the admin panel performs the actual upload; a standalone editor cannot upload and will error/time out).", {
|
|
3788
|
-
project_root: z
|
|
3789
|
-
.string()
|
|
3790
|
-
.describe("Absolute path to the code-component project."),
|
|
3791
|
-
file_path: z
|
|
3792
|
-
.string()
|
|
3793
|
-
.optional()
|
|
3794
|
-
.describe("Local image file path (.png, .jpg, .jpeg, .webp, .gif). Provide this or `image_url`."),
|
|
3795
|
-
image_url: z
|
|
3796
|
-
.string()
|
|
3797
|
-
.optional()
|
|
3798
|
-
.describe("Image URL to fetch and upload (alternative to `file_path`)."),
|
|
3799
|
-
alt_text: z.string().optional().describe("Alt text to store with the image."),
|
|
3800
|
-
port: z
|
|
3801
|
-
.number()
|
|
3802
|
-
.optional()
|
|
3803
|
-
.describe("Dev server WebSocket port (default 5201)."),
|
|
3804
|
-
}, async ({ project_root, file_path, image_url, alt_text, port }) => {
|
|
3805
|
-
const args = [
|
|
3806
|
-
"upload-image",
|
|
3807
|
-
...(file_path ? ["--file", file_path] : []),
|
|
3808
|
-
...(image_url ? ["--url", image_url] : []),
|
|
3809
|
-
...(alt_text ? ["--alt", alt_text] : []),
|
|
3810
|
-
...(port ? ["--port", String(port)] : []),
|
|
3811
|
-
];
|
|
3812
|
-
return callEditorAction(project_root, args);
|
|
3813
|
-
});
|
|
3814
|
-
// Tool: update_page_sections
|
|
3815
|
-
server.tool("update_page_sections", "Fill MANY placed sections of a page in a SINGLE call — the fastest way to populate a whole page (one round-trip for the entire page instead of one call per section/prop). Takes `sections`, each `{ element_id, updates: [{ prop_name|prop_id, value }] }`; values use the same per-type shapes as update_section_prop / get_editor_workflow. All-or-nothing across the whole page: every value is resolved and deeply validated (and built-but-unimported child code components auto-imported) before anything is written — one invalid value rejects the entire page update. Prefer this over many update_section_prop(s) calls.", {
|
|
3816
|
-
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3817
|
-
page_id: z.string().describe("Target page id (from list_editor_pages)."),
|
|
3818
|
-
sections: z
|
|
3819
|
-
.array(z.object({
|
|
3820
|
-
element_id: z.string().describe("Placed-section elementId (from list_page_sections)."),
|
|
3821
|
-
updates: z
|
|
3822
|
-
.array(z.object({
|
|
3823
|
-
prop_id: z.string().optional().describe("Blueprint prop id (provide this or prop_name)."),
|
|
3824
|
-
prop_name: z.string().optional().describe("Blueprint prop name (alternative to prop_id)."),
|
|
3825
|
-
value: z.any().describe("Prop value, same shape update_section_prop expects for that type."),
|
|
3826
|
-
}))
|
|
3827
|
-
.describe("Non-empty array of prop updates for this section."),
|
|
3828
|
-
}))
|
|
3829
|
-
.describe("Non-empty array of sections to fill, each with its elementId and prop updates."),
|
|
3830
|
-
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3831
|
-
}, async ({ project_root, page_id, sections, port }) => {
|
|
3832
|
-
const normalized = (sections || []).map(s => ({
|
|
3833
|
-
elementId: s.element_id,
|
|
3834
|
-
updates: (s.updates || []).map(u => ({
|
|
3835
|
-
...(u.prop_id ? { propId: u.prop_id } : {}),
|
|
3836
|
-
...(u.prop_name ? { propName: u.prop_name } : {}),
|
|
3837
|
-
value: u.value,
|
|
3838
|
-
})),
|
|
3839
|
-
}));
|
|
3840
|
-
const args = [
|
|
3841
|
-
"update-page-sections",
|
|
3842
|
-
"--page-id",
|
|
3843
|
-
page_id,
|
|
3844
|
-
"--sections",
|
|
3845
|
-
JSON.stringify(normalized),
|
|
3846
|
-
...(port ? ["--port", String(port)] : []),
|
|
3847
|
-
];
|
|
3848
|
-
return callEditorAction(project_root, args);
|
|
3849
|
-
});
|
|
3850
|
-
// Tool: upload_images
|
|
3851
|
-
server.tool("upload_images", "Upload MANY images in one call (batch upload_image) and get their ids back in order — the host uploads them all in a single round-trip. Pass `images`, each `{ file_path?, image_url?, alt_text? }`. Returns an array of { id, fileName, altText, isVideo } in the same order; use each id in an IMAGE prop value. Prefer this over multiple upload_image calls when a section/page needs several images. Supported: png/jpg/jpeg/webp/gif, max 10MB each. Requires the editor embedded in the ikas admin panel.", {
|
|
3852
|
-
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3853
|
-
images: z
|
|
3854
|
-
.array(z.object({
|
|
3855
|
-
file_path: z.string().optional().describe("Local image file path (provide this or image_url)."),
|
|
3856
|
-
image_url: z.string().optional().describe("Image URL to fetch (alternative to file_path)."),
|
|
3857
|
-
alt_text: z.string().optional().describe("Alt text for this image."),
|
|
3858
|
-
}))
|
|
3859
|
-
.describe("Non-empty array of images to upload."),
|
|
3860
|
-
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3861
|
-
}, async ({ project_root, images, port }) => {
|
|
3862
|
-
const manifest = (images || []).map(im => ({
|
|
3863
|
-
...(im.file_path ? { file: im.file_path } : {}),
|
|
3864
|
-
...(im.image_url ? { url: im.image_url } : {}),
|
|
3865
|
-
...(im.alt_text ? { alt: im.alt_text } : {}),
|
|
3866
|
-
}));
|
|
3867
|
-
const args = [
|
|
3868
|
-
"upload-images",
|
|
3869
|
-
"--manifest",
|
|
3870
|
-
JSON.stringify(manifest),
|
|
3871
|
-
...(port ? ["--port", String(port)] : []),
|
|
3872
|
-
];
|
|
3873
|
-
return callEditorAction(project_root, args);
|
|
3874
|
-
});
|
|
3875
|
-
// Tool: search_products
|
|
3876
|
-
server.tool("search_products", "Search the connected store's products to obtain a real productId (and first variantId) for a PRODUCT prop value. Returns products with productId, variantId, name, slug, variantCount, and `imageSrc` (the full-resolution CDN URL of the product's main image). To use that product image in an IMAGE prop, you CANNOT reuse a product image id directly (it is not a theme asset) — pass the `imageSrc` URL to `upload_image(image_url: <imageSrc>)` (or `upload_images`), then use the returned id in the IMAGE value. This gives real product imagery instead of external/placeholder URLs. Use the result in update_section_prop as `{ \"productId\": \"<productId>\", \"variantId\": \"<variantId>\" }`. Pass `query` for free-text search, or `product_ids` to resolve specific ids. Requires the editor embedded in the ikas admin panel (the admin panel runs the search; a standalone editor cannot).", {
|
|
3877
|
-
project_root: z
|
|
3878
|
-
.string()
|
|
3879
|
-
.describe("Absolute path to the code-component project."),
|
|
3880
|
-
query: z.string().optional().describe("Free-text product search (name, sku, barcode)."),
|
|
3881
|
-
product_ids: z
|
|
3882
|
-
.array(z.string())
|
|
3883
|
-
.optional()
|
|
3884
|
-
.describe("Specific product ids to resolve directly (alternative to `query`)."),
|
|
3885
|
-
per_page: z.number().optional().describe("Results per page."),
|
|
3886
|
-
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3887
|
-
}, async ({ project_root, query, product_ids, per_page, port }) => {
|
|
3888
|
-
const args = [
|
|
3889
|
-
"search-products",
|
|
3890
|
-
...(query ? ["--query", query] : []),
|
|
3891
|
-
...(product_ids && product_ids.length ? ["--ids", product_ids.join(",")] : []),
|
|
3892
|
-
...(typeof per_page === "number" ? ["--per-page", String(per_page)] : []),
|
|
3893
|
-
...(port ? ["--port", String(port)] : []),
|
|
3894
|
-
];
|
|
3895
|
-
return callEditorAction(project_root, args);
|
|
3896
|
-
});
|
|
3897
|
-
// Tool: list_categories
|
|
3898
|
-
server.tool("list_categories", "List the store's product categories and their ids, to fill a CATEGORY prop value `{ \"categoryId\": \"<id>\" }`. Returns categoryId, name, slug, parentId for each. Returns all categories (no query). To link to a category from a LINK prop, also call `get_page_by_type(\"CATEGORY\")` for the page id, then build `{ \"linkType\": \"PAGE\", \"pageId\": <pageId>, \"pageType\": \"CATEGORY\", \"itemId\": <categoryId>, \"id\": <unique>, \"label\": \"...\" }`. Requires the editor embedded in the ikas admin panel.", {
|
|
3899
|
-
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3900
|
-
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3901
|
-
}, async ({ project_root, port }) => {
|
|
3902
|
-
const args = ["list-entities", "--kind", "category", ...(port ? ["--port", String(port)] : [])];
|
|
3903
|
-
return callEditorAction(project_root, args);
|
|
3904
|
-
});
|
|
3905
|
-
// Tool: list_brands
|
|
3906
|
-
server.tool("list_brands", "List the store's product brands and their ids, to fill a BRAND prop value `{ \"brandId\": \"<id>\" }`. Returns brandId, name, slug for each. Returns all brands (no query). To link to a brand from a LINK prop, also call `get_page_by_type(\"BRAND\")` for the page id, then build `{ \"linkType\": \"PAGE\", \"pageId\": <pageId>, \"pageType\": \"BRAND\", \"itemId\": <brandId>, \"id\": <unique>, \"label\": \"...\" }`. Requires the editor embedded in the ikas admin panel.", {
|
|
3907
|
-
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3908
|
-
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3909
|
-
}, async ({ project_root, port }) => {
|
|
3910
|
-
const args = ["list-entities", "--kind", "brand", ...(port ? ["--port", String(port)] : [])];
|
|
3911
|
-
return callEditorAction(project_root, args);
|
|
3912
|
-
});
|
|
3913
|
-
// Tool: list_blogs
|
|
3914
|
-
server.tool("list_blogs", "List the store's blogs and their ids, to fill a BLOG prop value `{ \"blogId\": \"<id>\" }`. Returns blogId, name, slug for each. Accepts an optional `query` for free-text search. To link to a blog from a LINK prop, also call `get_page_by_type(\"BLOG\")` for the page id, then build `{ \"linkType\": \"PAGE\", \"pageId\": <pageId>, \"pageType\": \"BLOG\", \"itemId\": <blogId>, \"id\": <unique>, \"label\": \"...\" }`. Requires the editor embedded in the ikas admin panel.", {
|
|
3915
|
-
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3916
|
-
query: z.string().optional().describe("Free-text blog search."),
|
|
3917
|
-
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3918
|
-
}, async ({ project_root, query, port }) => {
|
|
3919
|
-
const args = [
|
|
3920
|
-
"list-entities",
|
|
3921
|
-
"--kind",
|
|
3922
|
-
"blog",
|
|
3923
|
-
...(query ? ["--query", query] : []),
|
|
3924
|
-
...(port ? ["--port", String(port)] : []),
|
|
3925
|
-
];
|
|
3926
|
-
return callEditorAction(project_root, args);
|
|
3927
|
-
});
|
|
3928
|
-
// Tool: list_blog_categories
|
|
3929
|
-
server.tool("list_blog_categories", "List the store's blog categories and their ids, to fill a BLOG_CATEGORY prop value `{ \"blogCategoryId\": \"<id>\" }`. Returns blogCategoryId, name, slug for each. Accepts an optional `query` for free-text search. To link to a blog category from a LINK prop, also call `get_page_by_type(\"BLOG_CATEGORY\")` for the page id, then build `{ \"linkType\": \"PAGE\", \"pageId\": <pageId>, \"pageType\": \"BLOG_CATEGORY\", \"itemId\": <blogCategoryId>, \"id\": <unique>, \"label\": \"...\" }`. Requires the editor embedded in the ikas admin panel.", {
|
|
3930
|
-
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3931
|
-
query: z.string().optional().describe("Free-text blog-category search."),
|
|
3932
|
-
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3933
|
-
}, async ({ project_root, query, port }) => {
|
|
3934
|
-
const args = [
|
|
3935
|
-
"list-entities",
|
|
3936
|
-
"--kind",
|
|
3937
|
-
"blog-category",
|
|
3938
|
-
...(query ? ["--query", query] : []),
|
|
3939
|
-
...(port ? ["--port", String(port)] : []),
|
|
3940
|
-
];
|
|
3941
|
-
return callEditorAction(project_root, args);
|
|
3942
|
-
});
|
|
3943
|
-
// Tool: get_page_by_type
|
|
3944
|
-
server.tool("get_page_by_type", "Resolve a theme page's id from its pageType (CATEGORY, PRODUCT, BRAND, BLOG, BLOG_CATEGORY, INDEX, …). Use this to get the `pageId` needed to build a PAGE link to an entity: pair it with the entity id from list_categories/list_brands/list_blogs/list_blog_categories — `{ linkType:\"PAGE\", pageId:<this pageId>, pageType:<type>, itemId:<entityId>, id:<unique>, label:... }`. Returns `pageId: null` (with a note) if no page of that type exists yet — in that case call `create_page(page_type)` to add it, then use the returned pageId (do not guess a slug/EXTERNAL URL).", {
|
|
3945
|
-
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3946
|
-
page_type: z
|
|
3947
|
-
.string()
|
|
3948
|
-
.describe("Page type to resolve, e.g. CATEGORY, PRODUCT, BRAND, BLOG, BLOG_CATEGORY, INDEX."),
|
|
3949
|
-
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3950
|
-
}, async ({ project_root, page_type, port }) => {
|
|
3951
|
-
const args = [
|
|
3952
|
-
"get-page-by-type",
|
|
3953
|
-
"--page-type",
|
|
3954
|
-
page_type,
|
|
3955
|
-
...(port ? ["--port", String(port)] : []),
|
|
3956
|
-
];
|
|
3957
|
-
return callEditorAction(project_root, args);
|
|
3958
|
-
});
|
|
3959
|
-
// Tool: create_page
|
|
3960
|
-
server.tool("create_page", "Create a theme page of a given pageType in the connected editor. Use this when get_page_by_type returns `pageId: null` for a dynamic page (CATEGORY/PRODUCT/BRAND/BLOG/BLOG_CATEGORY) you need to link to: create the page, then build a PAGE link `{ linkType:\"PAGE\", pageId:<this pageId>, pageType:<type>, itemId:<entityId>, id:<unique>, label:... }` — do NOT guess a slug/EXTERNAL URL. Non-CUSTOM page types are unique per theme: if one already exists it is returned unchanged (`alreadyExisted: true`). CUSTOM pages require `name` and a unique `slug`. Account sub-pages (ADDRESSES/ORDERS/ORDER_DETAIL/FAVORITE_PRODUCTS/RAFFLE_ACCOUNT) need an ACCOUNT page first. Returns `{ pageType, pageId, name, slug }`. Requires `ikas-component dev` running with the editor connected.", {
|
|
3961
|
-
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3962
|
-
page_type: z
|
|
3963
|
-
.string()
|
|
3964
|
-
.describe("Page type to create, e.g. CATEGORY, PRODUCT, BRAND, BLOG, BLOG_CATEGORY, INDEX, CUSTOM."),
|
|
3965
|
-
name: z.string().optional().describe("Page name. Required for CUSTOM pages; ignored otherwise."),
|
|
3966
|
-
slug: z.string().optional().describe("Page slug. Required (and unique) for CUSTOM pages; ignored otherwise."),
|
|
3967
|
-
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3968
|
-
}, async ({ project_root, page_type, name, slug, port }) => {
|
|
3969
|
-
const args = [
|
|
3970
|
-
"create-page",
|
|
3971
|
-
"--page-type",
|
|
3972
|
-
page_type,
|
|
3973
|
-
...(name ? ["--name", name] : []),
|
|
3974
|
-
...(slug ? ["--slug", slug] : []),
|
|
3975
|
-
...(port ? ["--port", String(port)] : []),
|
|
3976
|
-
];
|
|
3977
|
-
return callEditorAction(project_root, args);
|
|
3978
|
-
});
|
|
3979
|
-
// Tool: publish_theme
|
|
3980
|
-
server.tool("publish_theme", "Publish the current theme LIVE — the same action as the editor's Publish button (uploads the whole project to the storefront). This is REAL and customer-facing: the published site changes. SAFETY: it publishes nothing unless `confirm: true` is passed; if you omit it you get a dry-run with the `previewUrl` and a warning describing the impact. The MAIN/production theme additionally requires `confirm_production: true` (its URL is the live customer-facing domain). Use this when the user explicitly asks to publish/go live — do NOT publish on your own initiative. On success returns `{ published: true, previewUrl, isMainTheme }`; open `previewUrl` to review the result. Requires `ikas-component dev` running with the editor connected.", {
|
|
3981
|
-
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3982
|
-
confirm: z
|
|
3983
|
-
.boolean()
|
|
3984
|
-
.optional()
|
|
3985
|
-
.describe("Set true to actually publish. Omitted/false = dry-run (returns previewUrl + warning, publishes nothing)."),
|
|
3986
|
-
confirm_production: z
|
|
3987
|
-
.boolean()
|
|
3988
|
-
.optional()
|
|
3989
|
-
.describe("Required IN ADDITION to confirm when the target is the MAIN/production theme (live customer-facing site)."),
|
|
3990
|
-
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3991
|
-
}, async ({ project_root, confirm, confirm_production, port }) => {
|
|
3992
|
-
const args = [
|
|
3993
|
-
"publish-theme",
|
|
3994
|
-
...(confirm ? ["--confirm"] : []),
|
|
3995
|
-
...(confirm_production ? ["--confirm-production"] : []),
|
|
3996
|
-
...(port ? ["--port", String(port)] : []),
|
|
3997
|
-
];
|
|
3998
|
-
return callEditorAction(project_root, args);
|
|
3999
|
-
});
|
|
4000
|
-
// Tool: get_editor_workflow
|
|
4001
|
-
const EDITOR_WORKFLOW_GUIDE = [
|
|
4002
|
-
"# Editor workflow: placing sections and filling their content on a page",
|
|
4003
|
-
"",
|
|
4004
|
-
"Use this when the user wants to add a section to a page and/or fill in its content (heading, text, image, link, slides, etc.) in the live editor. These are LIVE-EDITOR actions and require `ikas-component dev` running with the editor connected; image upload additionally requires the editor to be embedded in the ikas admin panel (the admin panel performs the upload — a standalone editor cannot upload).",
|
|
4005
|
-
"",
|
|
4006
|
-
"## Two distinct jobs — do not confuse them",
|
|
4007
|
-
"- (A) DEFINE props / author component code — changing WHICH props a component has. Edits component source/config (config add-prop/add-component, index.tsx) and needs a rebuild. Do this ONLY when a needed prop does not exist yet, or the user explicitly asks to build/modify the component.",
|
|
4008
|
-
"- (B) SET prop VALUES / fill content — giving values to props that ALREADY exist on a placed section. Pure data entry via list_page_sections + update_section_prop (+ upload_image). No code, no rebuild.",
|
|
4009
|
-
"When the user says 'fill this section', 'set the title/text/image/link', 'populate', 'change the content' → that is JOB (B). Do NOT write a component or add a prop to fill a value the section already supports.",
|
|
4010
|
-
"",
|
|
4011
|
-
"## Step-by-step (JOB B — fill content)",
|
|
4012
|
-
"1. list_editor_pages → choose the target page_id.",
|
|
4013
|
-
"2. list_page_sections(page_id) → for each placed section: its per-placement elementId, blueprint props (id, name, type), and setPropNames (which props already have a value). It is LEAN — no prop VALUES, so it never truncates. To read a section's current values (e.g. to read-modify-write a COMPONENT_LIST), call get_section_values(page_id, element_id). (If the section is not on the page yet, see 'Placing a section' below.)",
|
|
4014
|
-
"3. To set props (fewest round-trips = fastest): update_page_sections(page_id, sections:[{element_id, updates:[{prop_name,value}]}]) fills the WHOLE page (or one section, or just one prop — it scales to all cases) in ONE call (best). update_section_prop sets a single prop, for quick one-offs. Pass each value as a parsed JSON object/array, NEVER a JSON string.",
|
|
4015
|
-
"4. For image props: upload_images(images:[{file_path|image_url, alt_text}]) uploads many in ONE call (preferred); upload_image for a single one. Use the returned id(s) in IMAGE values. Do uploads BEFORE the update call so you have the ids.",
|
|
4016
|
-
"Speed tip: each tool call is a round-trip — gather first (one list_page_sections), batch your uploads (upload_images) and your writes (update_page_sections), rather than calling per-prop/per-image.",
|
|
4017
|
-
"",
|
|
4018
|
-
"## Building a whole page fast (placing + filling)",
|
|
4019
|
-
"- add_sections_to_page(page_id, sections:[{component_id, index?, updates:[{prop_name,value}]}]) places MANY sections AND sets their props in ONE call, returning each new elementId. This is the fastest path — prefer it over add_section_to_page per section followed by separate fills. Built-but-unimported section/child components are auto-imported.",
|
|
4020
|
-
"- For a single section: list_imported_sections (confirm imported; else import_section, after building) → add_section_to_page(component_id, page_id, index?) → then list_page_sections to get its elementId.",
|
|
4021
|
-
"",
|
|
4022
|
-
"## Value shapes by prop type (what to pass as `value`)",
|
|
4023
|
-
"- Scalars are WRAPPED: TEXT/RICH_TEXT { \"value\": \"...\" }, BOOLEAN { \"value\": true }, NUMBER { \"value\": 12 }, COLOR { \"value\": \"#FF0000\" }, ENUM { \"value\": \"<key>\" }, DATE { \"value\": ... }.",
|
|
4024
|
-
"- SVG is value-wrapped: { \"value\": \"<svg markup>\" }. NUMBER_RANGE: { \"value\": <number>, \"unit\": \"px\"|null }.",
|
|
4025
|
-
"- Object props are NOT wrapped (no { \"value\": ... }):",
|
|
4026
|
-
" - IMAGE: { \"id\": \"<asset-id>\", \"altText\"?: \"...\", \"isVideo\"?: false } — get the id from upload_image. Same shape at section level AND nested in a component list. IMAGE_LIST: { \"images\": [ <image>, ... ] }.",
|
|
4027
|
-
" - VIDEO: { \"video\": { \"id\": \"<asset-id>\" }, \"thumbnailImage\"?: { \"id\": \"...\" }, \"autoplay\"?: false, \"controls\"?: true, \"loop\"?: false, \"muted\"?: false } — NOT a bare { \"id\": ... }. SVG_LIST: { \"svgs\": [ \"<svg>\", ... ] }.",
|
|
4028
|
-
" - LINK: { \"id\": \"<unique>\", \"linkType\": \"PAGE\"|\"EXTERNAL\"|\"FILE\", \"label\": \"...\", \"subLinks\": [] } plus target — for a static page: pageId + pageType (from list_editor_pages); for EXTERNAL: externalLink. To link to a CATEGORY/BRAND/BLOG/BLOG_CATEGORY entity, do NOT guess a slug — get the entity id from list_categories/list_brands/list_blogs/list_blog_categories and the page id from get_page_by_type('<TYPE>'), then build { id, linkType:'PAGE', label, pageId:<pageId>, pageType:'<TYPE>', itemId:<entityId>, subLinks:[] }. LIST_OF_LINK = { \"links\": [ <link>, ... ] }.",
|
|
4029
|
-
" - Entity props PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY/RAFFLE: { \"<entity>Id\": \"...\", \"usePageData\"?: false } — e.g. { \"productId\": \"...\", \"variantId\"?: \"...\" }, { \"categoryId\": \"...\" }. Get real ids (never invent them) from: search_products (PRODUCT), list_categories (CATEGORY), list_brands (BRAND), list_blogs (BLOG), list_blog_categories (BLOG_CATEGORY). (RAFFLE has no lookup tool yet — read an existing value or ask the user.)",
|
|
4030
|
-
" - List/collection props (PRODUCT_LIST/CATEGORY_LIST/BRAND_LIST/BLOG_LIST/…): complex typed objects (a <entity>ListType plus initialSort/initialLimit and static ids). Do NOT build from scratch — read the current value with get_section_values and edit it.",
|
|
4031
|
-
" - COMPONENT_LIST: { \"components\": [ { \"id\": \"<unique>\", \"codeComponentId\": \"<child-id>\", \"propValues\": { \"<childProp>\": <value>, ... } }, ... ] }. A single COMPONENT prop is one such entry. childProp values use these same per-type shapes, recursively.",
|
|
4032
|
-
"",
|
|
4033
|
-
"## COMPONENT_LIST rules (the part most often gotten wrong)",
|
|
4034
|
-
"- READ-MODIFY-WRITE: update_section_prop REPLACES the whole prop value. To add/edit/remove one child, take the CURRENT { components: [...] } from get_section_values, edit it, and resend the ENTIRE array (with all existing entries and their ids). Sending only your new entry wipes the rest.",
|
|
4035
|
-
"- Each entry needs a UNIQUE id (a short string you generate; it does not need to match anything). Do not reuse another entry's id.",
|
|
4036
|
-
"- Reference a code component with \"codeComponentId\" and a built-in theme component with \"componentId\" — not the other way around. Use \"propValues\" (not \"props\").",
|
|
4037
|
-
"- Child ids are opaque — take them from get_section_template / list_imported_sections; never invent them.",
|
|
4038
|
-
"- Auto-import: a referenced child that is BUILT but not yet imported is imported automatically (returned under autoImported). A child that is not built at all errors — build it first.",
|
|
4039
|
-
"",
|
|
4040
|
-
"## Reading prop schemas (avoid guessing)",
|
|
4041
|
-
"- get_component_props(componentId) → a component's props with types, required, default, ENUM `options` (the valid values to set), and COMPONENT_LIST `allowedComponentIds` (valid children). Works for any section OR child id. Use it BEFORE filling — to know prop names, enum values, and which children a slot allows — instead of guessing or reading ikas.config.json.",
|
|
4042
|
-
"- For a COMPONENT_LIST: get_component_props(parent) → its allowedComponentIds → get_component_props(child) → the child's props/enums. Then build the components array.",
|
|
4043
|
-
"- get_section_values(element_id) → one placed section's current values (read-modify-write of a COMPONENT_LIST, or to copy an existing value's exact shape).",
|
|
4044
|
-
"- Real entity ids/images: search_products returns productId + variantId + imageSrc (the product image URL). To use that image in an IMAGE prop, you can't reuse a product image id — upload it: upload_image(image_url: imageSrc) → use the returned id. list_categories/list_brands/list_blogs/list_blog_categories return the entity ids; to LINK to one, also call get_page_by_type('<TYPE>') for the page id and build a PAGE link { linkType:'PAGE', pageId, pageType, itemId:<entityId>, id, label }. IMPORTANT: if get_page_by_type returns pageId:null, that dynamic page does NOT exist in the theme yet — do NOT guess a slug/EXTERNAL URL. Call create_page('<TYPE>') to add it (non-CUSTOM types are unique and returned as-is if already present), then use the returned pageId to build the PAGE link. Always have a real pageId before building a PAGE link.",
|
|
4045
|
-
"",
|
|
4046
|
-
"## Validation",
|
|
4047
|
-
"Values are validated server-side (deeply, including nested children). Wrong shape or wrong semantics (double-encoded string, { value } wrapper on an object prop, props vs propValues, missing/duplicate id, wrong reference key, nonexistent component id, unknown prop name) are REJECTED with an explanatory error instead of being stored silently. Read the error and fix the value; if unsure of a shape, read it with get_section_values and match it.",
|
|
4048
|
-
].join("\n");
|
|
4049
|
-
server.tool("get_editor_workflow", "Read this FIRST when the user wants to place a section on a page or fill/edit a section's content (text, image, link, slides, component lists) in the live editor. Returns the complete step-by-step workflow for the live-editor tools (list_editor_pages, list_imported_sections, import_section, add_section_to_page, list_page_sections, update_section_prop, upload_image), the per-prop-type value shapes, the COMPONENT_LIST read-modify-write rules, and how validation/auto-import behave. Use it to avoid the common mistake of writing component code to set a value a section already supports.", {}, async () => {
|
|
4050
|
-
return { content: [{ type: "text", text: EDITOR_WORKFLOW_GUIDE }] };
|
|
4051
|
-
});
|
|
4052
3144
|
// --- Start server ---
|
|
4053
3145
|
async function main() {
|
|
4054
3146
|
const transport = new StdioServerTransport();
|