@ikas/code-components-mcp 1.4.0-beta.14 → 1.4.0-beta.140
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/framework.json +49 -9
- package/data/migration.json +2 -2
- package/data/storefront-api.json +203 -23
- package/data/storefront-types.json +9334 -6021
- package/dist/index.js +1885 -242
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -98,9 +98,16 @@ function loadStorefrontTypes() {
|
|
|
98
98
|
}
|
|
99
99
|
return null;
|
|
100
100
|
}
|
|
101
|
-
const SUBTREE_KINDS = [
|
|
101
|
+
const SUBTREE_KINDS = [
|
|
102
|
+
"children",
|
|
103
|
+
"components",
|
|
104
|
+
"sub-components",
|
|
105
|
+
];
|
|
102
106
|
function normalizeName(value) {
|
|
103
|
-
return value
|
|
107
|
+
return value
|
|
108
|
+
.trim()
|
|
109
|
+
.replace(/^`+|`+$/g, "")
|
|
110
|
+
.trim();
|
|
104
111
|
}
|
|
105
112
|
const storefrontData = loadStorefrontData();
|
|
106
113
|
const frameworkData = loadJsonFile("../data/framework.json");
|
|
@@ -261,7 +268,11 @@ function searchMigrationTopics(query) {
|
|
|
261
268
|
const descScore = matchScore(topic.description, query) * 2;
|
|
262
269
|
const contentScore = matchScore(topic.content, query);
|
|
263
270
|
const tagScore = topic.tags.some((t) => matchScore(t, query) > 0) ? 5 : 0;
|
|
264
|
-
return {
|
|
271
|
+
return {
|
|
272
|
+
key,
|
|
273
|
+
topic,
|
|
274
|
+
score: titleScore + descScore + contentScore + tagScore,
|
|
275
|
+
};
|
|
265
276
|
})
|
|
266
277
|
.filter((item) => item.score > 0)
|
|
267
278
|
.sort((a, b) => b.score - a.score);
|
|
@@ -288,7 +299,7 @@ function analyzeOldTheme(themeJson) {
|
|
|
288
299
|
parts.push(`> **CRITICAL:** The old system (\`@ikas/storefront\`) and the new code-component system (\`@ikas/bp-storefront\`) are **entirely different packages**. Even when type names look the same (e.g., \`IkasImage\`, \`IkasProduct\`), they are different types with different properties. The prop type systems are also completely separate — old theme.json prop types and new ikas.config.json prop types have different semantics even when names match. **Never assume old-system knowledge applies to the new system.** Always use \`get_type_definition\`, \`get_model_guide\`, and \`get_function_doc\` to look up the correct new-system APIs.\n`);
|
|
289
300
|
parts.push(`## Summary Statistics\n`);
|
|
290
301
|
parts.push(`- **Components:** ${components.length}`);
|
|
291
|
-
parts.push(`- **Custom Data Definitions:** ${customData.filter(cd => cd.isRoot).length}`);
|
|
302
|
+
parts.push(`- **Custom Data Definitions:** ${customData.filter((cd) => cd.isRoot).length}`);
|
|
292
303
|
parts.push(`- **Prop Groups:** ${groups.length}`);
|
|
293
304
|
// Component analysis
|
|
294
305
|
parts.push(`\n## Components (${components.length})\n`);
|
|
@@ -327,7 +338,11 @@ function analyzeOldTheme(themeJson) {
|
|
|
327
338
|
const typesSummary = Object.entries(propTypeCounts)
|
|
328
339
|
.map(([t, c]) => `${t}×${c}`)
|
|
329
340
|
.join(", ");
|
|
330
|
-
const headerFooter = comp.isHeader
|
|
341
|
+
const headerFooter = comp.isHeader
|
|
342
|
+
? " [HEADER]"
|
|
343
|
+
: comp.isFooter
|
|
344
|
+
? " [FOOTER]"
|
|
345
|
+
: "";
|
|
331
346
|
parts.push(`### ${comp.displayName || comp.dir || comp.id}${headerFooter}`);
|
|
332
347
|
parts.push(`- **Dir:** \`${comp.dir || "?"}\` | **Props:** ${props.length} (${typesSummary})`);
|
|
333
348
|
parts.push(`- **Recommended new type:** section`);
|
|
@@ -342,7 +357,7 @@ function analyzeOldTheme(themeJson) {
|
|
|
342
357
|
parts.push("");
|
|
343
358
|
}
|
|
344
359
|
// Custom data analysis
|
|
345
|
-
const rootCustomData = customData.filter(cd => cd.isRoot);
|
|
360
|
+
const rootCustomData = customData.filter((cd) => cd.isRoot);
|
|
346
361
|
if (rootCustomData.length > 0) {
|
|
347
362
|
parts.push(`\n## Custom Data Definitions (${rootCustomData.length})\n`);
|
|
348
363
|
for (const cd of rootCustomData) {
|
|
@@ -352,7 +367,9 @@ function analyzeOldTheme(themeJson) {
|
|
|
352
367
|
const describeNested = (items, indent) => {
|
|
353
368
|
const lines = [];
|
|
354
369
|
for (const item of items) {
|
|
355
|
-
const key = item.key
|
|
370
|
+
const key = item.key
|
|
371
|
+
? `\`${item.key}\``
|
|
372
|
+
: item.typescriptName || item.name || "unnamed";
|
|
356
373
|
lines.push(`${indent}- ${key}: ${item.type}${item.isRequired ? " (required)" : ""}`);
|
|
357
374
|
if (item.nestedData && item.nestedData.length > 0) {
|
|
358
375
|
lines.push(...describeNested(item.nestedData, indent + " "));
|
|
@@ -364,7 +381,7 @@ function analyzeOldTheme(themeJson) {
|
|
|
364
381
|
parts.push(...describeNested(cd.nestedData, " "));
|
|
365
382
|
}
|
|
366
383
|
if (cd.enumOptions && cd.enumOptions.length > 0) {
|
|
367
|
-
parts.push(`- **Enum options:** ${cd.enumOptions.map(o => `"${o.value}"`).join(", ")}`);
|
|
384
|
+
parts.push(`- **Enum options:** ${cd.enumOptions.map((o) => `"${o.value}"`).join(", ")}`);
|
|
368
385
|
}
|
|
369
386
|
// Find which components reference this customData
|
|
370
387
|
const referencingComponents = [];
|
|
@@ -446,7 +463,9 @@ function scanSharedSubcomponents(sourceDir) {
|
|
|
446
463
|
const walk = (dir) => {
|
|
447
464
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
448
465
|
if (entry.isDirectory()) {
|
|
449
|
-
if (entry.name === "node_modules" ||
|
|
466
|
+
if (entry.name === "node_modules" ||
|
|
467
|
+
entry.name === "__generated__" ||
|
|
468
|
+
entry.name.startsWith("."))
|
|
450
469
|
continue;
|
|
451
470
|
walk(path.join(dir, entry.name));
|
|
452
471
|
}
|
|
@@ -481,10 +500,14 @@ function scanSharedSubcomponents(sourceDir) {
|
|
|
481
500
|
if (!importPath.startsWith("."))
|
|
482
501
|
continue;
|
|
483
502
|
// Skip imports of generated types, utils, hooks
|
|
484
|
-
if (importPath.includes("__generated__") ||
|
|
503
|
+
if (importPath.includes("__generated__") ||
|
|
504
|
+
importPath.includes("/utils") ||
|
|
505
|
+
importPath.includes("/hooks"))
|
|
485
506
|
continue;
|
|
486
507
|
// Extract base name from path
|
|
487
|
-
const pathSegments = importPath
|
|
508
|
+
const pathSegments = importPath
|
|
509
|
+
.split("/")
|
|
510
|
+
.filter((s) => s && s !== "." && s !== "..");
|
|
488
511
|
if (pathSegments.length === 0)
|
|
489
512
|
continue;
|
|
490
513
|
const lastSegment = pathSegments[pathSegments.length - 1];
|
|
@@ -495,7 +518,10 @@ function scanSharedSubcomponents(sourceDir) {
|
|
|
495
518
|
continue;
|
|
496
519
|
seenInFile.add(key);
|
|
497
520
|
if (!importUsage.has(key)) {
|
|
498
|
-
importUsage.set(key, {
|
|
521
|
+
importUsage.set(key, {
|
|
522
|
+
usingComponents: new Set(),
|
|
523
|
+
rawImportPath: importPath,
|
|
524
|
+
});
|
|
499
525
|
}
|
|
500
526
|
importUsage.get(key).usingComponents.add(componentDir);
|
|
501
527
|
}
|
|
@@ -504,7 +530,7 @@ function scanSharedSubcomponents(sourceDir) {
|
|
|
504
530
|
const shared = [];
|
|
505
531
|
for (const [name, { usingComponents, rawImportPath }] of importUsage) {
|
|
506
532
|
// Don't flag the component itself (e.g., Navbar imports from ../Navbar/something)
|
|
507
|
-
const users = [...usingComponents].filter(c => c !== name);
|
|
533
|
+
const users = [...usingComponents].filter((c) => c !== name);
|
|
508
534
|
if (users.length >= 3) {
|
|
509
535
|
shared.push({ name, usedBy: users.sort(), importPaths: [rawImportPath] });
|
|
510
536
|
}
|
|
@@ -521,7 +547,7 @@ function toKebabCase(s) {
|
|
|
521
547
|
}
|
|
522
548
|
function classifyComplexity(comp, customDataMap) {
|
|
523
549
|
const props = comp.props || [];
|
|
524
|
-
const customCount = props.filter(p => p.type === "CUSTOM").length;
|
|
550
|
+
const customCount = props.filter((p) => p.type === "CUSTOM").length;
|
|
525
551
|
if (customCount === 0 && props.length < 10)
|
|
526
552
|
return "simple";
|
|
527
553
|
// Check for deeply nested CUSTOM (customData referencing another customData)
|
|
@@ -532,7 +558,9 @@ function classifyComplexity(comp, customDataMap) {
|
|
|
532
558
|
if (cd?.nestedData) {
|
|
533
559
|
const hasNested = (items) => {
|
|
534
560
|
for (const item of items) {
|
|
535
|
-
if (item.type === "DYNAMIC_LIST" ||
|
|
561
|
+
if (item.type === "DYNAMIC_LIST" ||
|
|
562
|
+
item.type === "STATIC_LIST" ||
|
|
563
|
+
item.customDataId)
|
|
536
564
|
return true;
|
|
537
565
|
if (item.nestedData && hasNested(item.nestedData))
|
|
538
566
|
return true;
|
|
@@ -564,7 +592,7 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
564
592
|
parts.push(`# Theme Migration Plan — \`${projectName}\``);
|
|
565
593
|
parts.push("");
|
|
566
594
|
parts.push(`**Generated:** ${new Date().toISOString().slice(0, 10)}`);
|
|
567
|
-
parts.push(`**Source:** ${components.length} old components, ${customData.filter(cd => cd.isRoot).length} custom data types, ${(theme.pages || []).length} pages`);
|
|
595
|
+
parts.push(`**Source:** ${components.length} old components, ${customData.filter((cd) => cd.isRoot).length} custom data types, ${(theme.pages || []).length} pages`);
|
|
568
596
|
parts.push("");
|
|
569
597
|
parts.push(`> ## READ THIS FIRST`);
|
|
570
598
|
parts.push(`>`);
|
|
@@ -654,16 +682,22 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
654
682
|
const cdType = cd.type || "?";
|
|
655
683
|
let shape = "";
|
|
656
684
|
if (cd.type === "ENUM") {
|
|
657
|
-
const opts = (cd.enumOptions || [])
|
|
685
|
+
const opts = (cd.enumOptions || [])
|
|
686
|
+
.map((o) => o.value || o.displayName)
|
|
687
|
+
.filter(Boolean);
|
|
658
688
|
shape = ` — shape: \`enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}\``;
|
|
659
689
|
}
|
|
660
690
|
else if (cd.nestedData && cd.nestedData.length > 0) {
|
|
661
691
|
const first = cd.nestedData[0];
|
|
662
|
-
const fields = (first?.nestedData || cd.nestedData || [])
|
|
692
|
+
const fields = (first?.nestedData || cd.nestedData || [])
|
|
693
|
+
.map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`)
|
|
694
|
+
.slice(0, 8);
|
|
663
695
|
shape = ` — shape: \`{${fields.join(", ")}}\``;
|
|
664
696
|
}
|
|
665
697
|
const usedBy = cd.id ? usageByCustomDataId.get(cd.id) || [] : [];
|
|
666
|
-
const usedByStr = usedBy.length > 0
|
|
698
|
+
const usedByStr = usedBy.length > 0
|
|
699
|
+
? ` — used by: ${usedBy.slice(0, 6).join(", ")}${usedBy.length > 6 ? `, +${usedBy.length - 6} more` : ""}`
|
|
700
|
+
: ` — _not directly referenced by any section's props (may be nested inside another customData)_`;
|
|
667
701
|
parts.push(`- \`${cdName}\` (${cdType})${shape}${usedByStr}`);
|
|
668
702
|
}
|
|
669
703
|
parts.push("");
|
|
@@ -697,7 +731,11 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
697
731
|
const oldName = comp.displayName || comp.dir || comp.id || "Unknown";
|
|
698
732
|
const kebabName = toKebabCase(comp.dir || comp.displayName || comp.id || "unknown");
|
|
699
733
|
const newId = `${projectName}-${kebabName}`;
|
|
700
|
-
const headerFooter = comp.isHeader
|
|
734
|
+
const headerFooter = comp.isHeader
|
|
735
|
+
? " **[HEADER]**"
|
|
736
|
+
: comp.isFooter
|
|
737
|
+
? " **[FOOTER]**"
|
|
738
|
+
: "";
|
|
701
739
|
const propCount = (comp.props || []).length;
|
|
702
740
|
// Detect children from CUSTOM DYNAMIC_LIST props
|
|
703
741
|
const children = [];
|
|
@@ -707,8 +745,13 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
707
745
|
if (cd && (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST")) {
|
|
708
746
|
const itemObj = cd.nestedData?.[0];
|
|
709
747
|
if (itemObj) {
|
|
710
|
-
const childName = itemObj.typescriptName ||
|
|
711
|
-
|
|
748
|
+
const childName = itemObj.typescriptName ||
|
|
749
|
+
(itemObj.name
|
|
750
|
+
? itemObj.name.replace(/[^a-zA-Z0-9]/g, "")
|
|
751
|
+
: `${oldName}Item`);
|
|
752
|
+
const fields = (itemObj.nestedData || [])
|
|
753
|
+
.map((f) => f.key || f.name || "?")
|
|
754
|
+
.slice(0, 8);
|
|
712
755
|
children.push({ propName: p.name || "?", childName, fields });
|
|
713
756
|
}
|
|
714
757
|
}
|
|
@@ -767,11 +810,25 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
767
810
|
}
|
|
768
811
|
// Known libraries we detect in old themes and want to flag for replacement
|
|
769
812
|
const KNOWN_LIBRARIES = [
|
|
770
|
-
"swiper",
|
|
771
|
-
"react
|
|
772
|
-
"react
|
|
773
|
-
"
|
|
774
|
-
"
|
|
813
|
+
"swiper",
|
|
814
|
+
"@headlessui/react",
|
|
815
|
+
"@heroicons/react",
|
|
816
|
+
"recharts",
|
|
817
|
+
"react-player",
|
|
818
|
+
"react-simple-star-rating",
|
|
819
|
+
"react-slider",
|
|
820
|
+
"react-compound-slider",
|
|
821
|
+
"react-zoom-pan-pinch",
|
|
822
|
+
"react-hot-toast",
|
|
823
|
+
"react-fast-marquee",
|
|
824
|
+
"react-indiana-drag-scroll",
|
|
825
|
+
"react-simple-typewriter",
|
|
826
|
+
"react-timer-hook",
|
|
827
|
+
"date-fns",
|
|
828
|
+
"slugify",
|
|
829
|
+
"classnames",
|
|
830
|
+
"clsx",
|
|
831
|
+
"@react-pdf/renderer",
|
|
775
832
|
];
|
|
776
833
|
// Heuristic: member-access patterns on old storefront stores/singletons that likely need new-system equivalents
|
|
777
834
|
const OLD_STOREFRONT_CALL_REGEX = /\b(customerStore|cartStore|productStore|categoryStore|orderStore|searchStore|favoritesStore|i18nStore|Router|useStore)\.\w+/g;
|
|
@@ -790,7 +847,8 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
790
847
|
// Collect all .tsx/.ts files in the component dir
|
|
791
848
|
try {
|
|
792
849
|
for (const entry of fs.readdirSync(componentDir, { withFileTypes: true })) {
|
|
793
|
-
if (entry.isFile() &&
|
|
850
|
+
if (entry.isFile() &&
|
|
851
|
+
(entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
|
|
794
852
|
result.sourceFiles.push(path.join(componentDir, entry.name));
|
|
795
853
|
}
|
|
796
854
|
}
|
|
@@ -807,7 +865,18 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
807
865
|
const subCompSet = new Map();
|
|
808
866
|
const callSet = new Set();
|
|
809
867
|
// Packages we treat as "framework" and don't flag for replacement (but do note in reactPackageUsage)
|
|
810
|
-
const REACT_PACKAGES = new Set([
|
|
868
|
+
const REACT_PACKAGES = new Set([
|
|
869
|
+
"react",
|
|
870
|
+
"react-dom",
|
|
871
|
+
"next",
|
|
872
|
+
"next/link",
|
|
873
|
+
"next/image",
|
|
874
|
+
"next/router",
|
|
875
|
+
"next/head",
|
|
876
|
+
"next/script",
|
|
877
|
+
"mobx-react-lite",
|
|
878
|
+
"mobx",
|
|
879
|
+
]);
|
|
811
880
|
for (const file of result.sourceFiles) {
|
|
812
881
|
let content;
|
|
813
882
|
try {
|
|
@@ -821,7 +890,7 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
821
890
|
while ((m = importRegex.exec(content)) !== null) {
|
|
822
891
|
const p = m[1];
|
|
823
892
|
if (p.startsWith(".")) {
|
|
824
|
-
const segs = p.split("/").filter(s => s && s !== "." && s !== "..");
|
|
893
|
+
const segs = p.split("/").filter((s) => s && s !== "." && s !== "..");
|
|
825
894
|
const last = segs[segs.length - 1];
|
|
826
895
|
if (last && /^[A-Z]/.test(last) && !last.includes("__generated__")) {
|
|
827
896
|
subCompSet.set(last, p);
|
|
@@ -829,7 +898,9 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
829
898
|
}
|
|
830
899
|
else if (!p.startsWith("@ikas/")) {
|
|
831
900
|
// Classify: known library, react-family, or unknown-external
|
|
832
|
-
const base = p.startsWith("@")
|
|
901
|
+
const base = p.startsWith("@")
|
|
902
|
+
? p.split("/").slice(0, 2).join("/")
|
|
903
|
+
: p.split("/")[0];
|
|
833
904
|
if (REACT_PACKAGES.has(p) || REACT_PACKAGES.has(base)) {
|
|
834
905
|
reactSet.add(base);
|
|
835
906
|
}
|
|
@@ -892,7 +963,10 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
892
963
|
}
|
|
893
964
|
}
|
|
894
965
|
}
|
|
895
|
-
result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({
|
|
966
|
+
result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({
|
|
967
|
+
name,
|
|
968
|
+
path: p,
|
|
969
|
+
}));
|
|
896
970
|
result.importedLibraries = [...libSet].sort();
|
|
897
971
|
result.importedUnknownLibraries = [...unknownLibSet].sort();
|
|
898
972
|
result.reactPackageUsage = [...reactSet].sort();
|
|
@@ -908,27 +982,38 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
908
982
|
customDataMap.set(cd.id, cd);
|
|
909
983
|
}
|
|
910
984
|
// Find the component — try match by dir, displayName, id, or new-id
|
|
911
|
-
const target = components.find(c => {
|
|
985
|
+
const target = components.find((c) => {
|
|
912
986
|
if (!c)
|
|
913
987
|
return false;
|
|
914
|
-
if (c.dir === sectionName ||
|
|
988
|
+
if (c.dir === sectionName ||
|
|
989
|
+
c.displayName === sectionName ||
|
|
990
|
+
c.id === sectionName)
|
|
915
991
|
return true;
|
|
916
992
|
const kebab = toKebabCase(c.dir || c.displayName || c.id || "");
|
|
917
993
|
const newId = `${projectName}-${kebab}`;
|
|
918
994
|
return newId === sectionName;
|
|
919
995
|
});
|
|
920
996
|
if (!target) {
|
|
921
|
-
const available = components
|
|
997
|
+
const available = components
|
|
998
|
+
.map((c) => c.dir || c.displayName || c.id)
|
|
999
|
+
.filter(Boolean)
|
|
1000
|
+
.join(", ");
|
|
922
1001
|
return `Section "${sectionName}" not found in theme. Available: ${available}`;
|
|
923
1002
|
}
|
|
924
1003
|
const parts = [];
|
|
925
1004
|
const oldName = target.displayName || target.dir || target.id || "Unknown";
|
|
926
1005
|
const kebabName = toKebabCase(target.dir || target.displayName || target.id || "unknown");
|
|
927
1006
|
const sectionId = `${projectName}-${kebabName}`;
|
|
928
|
-
const sectionPascal = (target.dir || target.displayName || "").replace(/[^a-zA-Z0-9]/g, "") ||
|
|
1007
|
+
const sectionPascal = (target.dir || target.displayName || "").replace(/[^a-zA-Z0-9]/g, "") ||
|
|
1008
|
+
kebabName
|
|
1009
|
+
.split("-")
|
|
1010
|
+
.map((s) => s[0]?.toUpperCase() + s.slice(1))
|
|
1011
|
+
.join("");
|
|
929
1012
|
// Scan the old source for imports, libraries, field usage
|
|
930
|
-
const propNames = (target.props || [])
|
|
931
|
-
|
|
1013
|
+
const propNames = (target.props || [])
|
|
1014
|
+
.map((p) => p.name || "")
|
|
1015
|
+
.filter(Boolean);
|
|
1016
|
+
const sourceScan = oldSourceDir && target.dir
|
|
932
1017
|
? scanSectionSource(path.join(oldSourceDir, target.dir), propNames)
|
|
933
1018
|
: null;
|
|
934
1019
|
parts.push(`# Section Migration Plan: ${oldName}`);
|
|
@@ -974,7 +1059,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
974
1059
|
}
|
|
975
1060
|
if (sourceScan.reactPackageUsage.length > 0) {
|
|
976
1061
|
parts.push("");
|
|
977
|
-
parts.push(`**React/Next.js framework imports detected:** ${sourceScan.reactPackageUsage.map(p => `\`${p}\``).join(", ")}. These do NOT carry over — the new system is Preact. See \`get_migration_guide("react-to-preact")\` for conversion patterns (hooks, observer, event types, routing).`);
|
|
1062
|
+
parts.push(`**React/Next.js framework imports detected:** ${sourceScan.reactPackageUsage.map((p) => `\`${p}\``).join(", ")}. These do NOT carry over — the new system is Preact. See \`get_migration_guide("react-to-preact")\` for conversion patterns (hooks, observer, event types, routing).`);
|
|
978
1063
|
}
|
|
979
1064
|
if (sourceScan.oldStorefrontCalls.length > 0) {
|
|
980
1065
|
parts.push("");
|
|
@@ -1020,7 +1105,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1020
1105
|
if (oldType === "SLIDER") {
|
|
1021
1106
|
newType = "NUMBER";
|
|
1022
1107
|
notes = `Was SLIDER(min=${p.sliderData?.min}, max=${p.sliderData?.max}) — replace \`.value\` access with direct number`;
|
|
1023
|
-
const prop = {
|
|
1108
|
+
const prop = {
|
|
1109
|
+
name: newName,
|
|
1110
|
+
displayName: p.displayName || newName,
|
|
1111
|
+
type: "NUMBER",
|
|
1112
|
+
};
|
|
1024
1113
|
if (p.isRequired)
|
|
1025
1114
|
prop.required = true;
|
|
1026
1115
|
parentPropsJson.push(prop);
|
|
@@ -1028,7 +1117,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1028
1117
|
else if (oldType === "PRODUCT_DETAIL") {
|
|
1029
1118
|
newType = "PRODUCT";
|
|
1030
1119
|
notes = "Renamed — PRODUCT_DETAIL → PRODUCT";
|
|
1031
|
-
const prop = {
|
|
1120
|
+
const prop = {
|
|
1121
|
+
name: newName,
|
|
1122
|
+
displayName: p.displayName || newName,
|
|
1123
|
+
type: "PRODUCT",
|
|
1124
|
+
};
|
|
1032
1125
|
if (p.isRequired)
|
|
1033
1126
|
prop.required = true;
|
|
1034
1127
|
parentPropsJson.push(prop);
|
|
@@ -1039,7 +1132,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1039
1132
|
if (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST") {
|
|
1040
1133
|
// Child component needed
|
|
1041
1134
|
const itemObj = cd.nestedData?.[0];
|
|
1042
|
-
const childName = itemObj?.typescriptName ||
|
|
1135
|
+
const childName = itemObj?.typescriptName ||
|
|
1136
|
+
(itemObj?.name
|
|
1137
|
+
? itemObj.name.replace(/[^a-zA-Z0-9]/g, "")
|
|
1138
|
+
: `${sectionPascal}Item`);
|
|
1043
1139
|
const childProps = [];
|
|
1044
1140
|
const nestedWarnings = [];
|
|
1045
1141
|
for (const f of (itemObj?.nestedData || [])) {
|
|
@@ -1050,11 +1146,18 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1050
1146
|
fType = "NUMBER";
|
|
1051
1147
|
else if (fType === "PRODUCT_DETAIL")
|
|
1052
1148
|
fType = "PRODUCT";
|
|
1053
|
-
else if (fType === "CUSTOM" ||
|
|
1149
|
+
else if (fType === "CUSTOM" ||
|
|
1150
|
+
fType === "DYNAMIC_LIST" ||
|
|
1151
|
+
fType === "STATIC_LIST" ||
|
|
1152
|
+
fType === "OBJECT") {
|
|
1054
1153
|
nestedWarnings.push(`\`${f.key}\` (${fType})`);
|
|
1055
1154
|
fType = "COMPONENT_LIST";
|
|
1056
1155
|
}
|
|
1057
|
-
const prop = {
|
|
1156
|
+
const prop = {
|
|
1157
|
+
name: f.key,
|
|
1158
|
+
displayName: f.name || f.key,
|
|
1159
|
+
type: fType,
|
|
1160
|
+
};
|
|
1058
1161
|
if (f.isRequired)
|
|
1059
1162
|
prop.required = true;
|
|
1060
1163
|
childProps.push(prop);
|
|
@@ -1066,10 +1169,14 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1066
1169
|
if (childProps.length === 0 && sourceScan?.propFieldUsage[oldName]) {
|
|
1067
1170
|
const inferred = sourceScan.propFieldUsage[oldName];
|
|
1068
1171
|
for (const fieldName of inferred) {
|
|
1069
|
-
childProps.push({
|
|
1172
|
+
childProps.push({
|
|
1173
|
+
name: fieldName,
|
|
1174
|
+
displayName: fieldName,
|
|
1175
|
+
type: "TEXT",
|
|
1176
|
+
});
|
|
1070
1177
|
}
|
|
1071
1178
|
if (inferred.length > 0) {
|
|
1072
|
-
notes = `Was CUSTOM → ${cd.type} with empty customData. **Inferred props from source usage:** ${inferred.map(f => `\`${f}\``).join(", ")}. Verify types and required flags — default inferred type is TEXT.`;
|
|
1179
|
+
notes = `Was CUSTOM → ${cd.type} with empty customData. **Inferred props from source usage:** ${inferred.map((f) => `\`${f}\``).join(", ")}. Verify types and required flags — default inferred type is TEXT.`;
|
|
1073
1180
|
}
|
|
1074
1181
|
}
|
|
1075
1182
|
children.push({
|
|
@@ -1098,7 +1205,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1098
1205
|
let fType = f.type;
|
|
1099
1206
|
if (fType === "SLIDER")
|
|
1100
1207
|
fType = "NUMBER";
|
|
1101
|
-
const prop = {
|
|
1208
|
+
const prop = {
|
|
1209
|
+
name: f.key,
|
|
1210
|
+
displayName: f.name || f.key,
|
|
1211
|
+
type: fType,
|
|
1212
|
+
};
|
|
1102
1213
|
if (f.isRequired)
|
|
1103
1214
|
prop.required = true;
|
|
1104
1215
|
parentPropsJson.push(prop);
|
|
@@ -1113,7 +1224,8 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1113
1224
|
}
|
|
1114
1225
|
else if (cd.type === "ENUM") {
|
|
1115
1226
|
newType = "ENUM";
|
|
1116
|
-
const enumName = cd.typescriptName ||
|
|
1227
|
+
const enumName = cd.typescriptName ||
|
|
1228
|
+
(cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
|
|
1117
1229
|
const options = (cd.enumOptions || []).reduce((acc, o) => {
|
|
1118
1230
|
if (o.displayName && o.value)
|
|
1119
1231
|
acc[o.displayName] = o.value;
|
|
@@ -1121,7 +1233,12 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1121
1233
|
}, {});
|
|
1122
1234
|
enumsNeeded.push({ name: enumName, options });
|
|
1123
1235
|
notes = `Was CUSTOM (ENUM) — create enum \`${enumName}\` via \`config add-enum\` first, then reference its enumId here`;
|
|
1124
|
-
const prop = {
|
|
1236
|
+
const prop = {
|
|
1237
|
+
name: newName,
|
|
1238
|
+
displayName: p.displayName || newName,
|
|
1239
|
+
type: "ENUM",
|
|
1240
|
+
enumTypeId: `<ENUM_ID_FROM_add-enum_${enumName}>`,
|
|
1241
|
+
};
|
|
1125
1242
|
if (p.isRequired)
|
|
1126
1243
|
prop.required = true;
|
|
1127
1244
|
parentPropsJson.push(prop);
|
|
@@ -1130,7 +1247,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1130
1247
|
}
|
|
1131
1248
|
else {
|
|
1132
1249
|
// Direct mapping
|
|
1133
|
-
const prop = {
|
|
1250
|
+
const prop = {
|
|
1251
|
+
name: newName,
|
|
1252
|
+
displayName: p.displayName || newName,
|
|
1253
|
+
type: newType,
|
|
1254
|
+
};
|
|
1134
1255
|
if (p.isRequired)
|
|
1135
1256
|
prop.required = true;
|
|
1136
1257
|
parentPropsJson.push(prop);
|
|
@@ -1139,7 +1260,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1139
1260
|
}
|
|
1140
1261
|
parts.push("");
|
|
1141
1262
|
// Custom Data Decision Callouts — per prop referencing a customData type
|
|
1142
|
-
const customDataPropsForCallouts = (target.props || []).filter((p) => p.type === "CUSTOM" &&
|
|
1263
|
+
const customDataPropsForCallouts = (target.props || []).filter((p) => p.type === "CUSTOM" &&
|
|
1264
|
+
p.customDataId &&
|
|
1265
|
+
customDataMap.has(p.customDataId));
|
|
1143
1266
|
if (customDataPropsForCallouts.length > 0) {
|
|
1144
1267
|
parts.push(`## Custom Data Decisions to Make`);
|
|
1145
1268
|
parts.push("");
|
|
@@ -1154,7 +1277,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1154
1277
|
let shape = "";
|
|
1155
1278
|
let shapeKind = "unknown";
|
|
1156
1279
|
if (cd.type === "ENUM") {
|
|
1157
|
-
const opts = (cd.enumOptions || [])
|
|
1280
|
+
const opts = (cd.enumOptions || [])
|
|
1281
|
+
.map((o) => o.value || o.displayName)
|
|
1282
|
+
.filter(Boolean);
|
|
1158
1283
|
shape = `enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}`;
|
|
1159
1284
|
shapeKind = "enum";
|
|
1160
1285
|
}
|
|
@@ -1187,7 +1312,8 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1187
1312
|
if (shapeKind === "enum") {
|
|
1188
1313
|
parts.push(`**Default: enum prop.** Flat scalar set; use \`config add-enum\`.`);
|
|
1189
1314
|
parts.push("");
|
|
1190
|
-
const enumName = cd.typescriptName ||
|
|
1315
|
+
const enumName = cd.typescriptName ||
|
|
1316
|
+
(cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
|
|
1191
1317
|
const enumOptions = (cd.enumOptions || []).reduce((acc, o) => {
|
|
1192
1318
|
if (o.displayName && o.value)
|
|
1193
1319
|
acc[o.displayName] = o.value;
|
|
@@ -1222,7 +1348,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1222
1348
|
parts.push(`> See \`get_migration_guide("component-composition-decision-guide")\` for when \`COMPONENT_LIST\` is overkill.`);
|
|
1223
1349
|
parts.push("");
|
|
1224
1350
|
}
|
|
1225
|
-
const compName = cd.typescriptName ||
|
|
1351
|
+
const compName = cd.typescriptName ||
|
|
1352
|
+
(cd.name
|
|
1353
|
+
? cd.name.replace(/[^a-zA-Z0-9]/g, "")
|
|
1354
|
+
: `${sectionPascal}Item`);
|
|
1226
1355
|
const compPropsForCli = [];
|
|
1227
1356
|
for (const f of fieldSource) {
|
|
1228
1357
|
if (!f.key)
|
|
@@ -1232,7 +1361,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1232
1361
|
fType = "NUMBER";
|
|
1233
1362
|
else if (fType === "PRODUCT_DETAIL")
|
|
1234
1363
|
fType = "PRODUCT";
|
|
1235
|
-
compPropsForCli.push({
|
|
1364
|
+
compPropsForCli.push({
|
|
1365
|
+
name: f.key,
|
|
1366
|
+
displayName: f.name || f.key,
|
|
1367
|
+
type: fType,
|
|
1368
|
+
});
|
|
1236
1369
|
}
|
|
1237
1370
|
parts.push("```bash");
|
|
1238
1371
|
if (isMinimal) {
|
|
@@ -1271,7 +1404,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1271
1404
|
existing.usedByProps.push(ch.propName);
|
|
1272
1405
|
}
|
|
1273
1406
|
else {
|
|
1274
|
-
uniqueChildren.set(ch.childName, {
|
|
1407
|
+
uniqueChildren.set(ch.childName, {
|
|
1408
|
+
child: ch,
|
|
1409
|
+
usedByProps: [ch.propName],
|
|
1410
|
+
});
|
|
1275
1411
|
}
|
|
1276
1412
|
}
|
|
1277
1413
|
if (uniqueChildren.size > 0) {
|
|
@@ -1284,7 +1420,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1284
1420
|
for (const { child: ch, usedByProps } of uniqueChildren.values()) {
|
|
1285
1421
|
parts.push(`### \`${ch.childName}\``);
|
|
1286
1422
|
const propsLabel = usedByProps.length > 1
|
|
1287
|
-
? `Used by parent props: ${usedByProps.map(p => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
|
|
1423
|
+
? `Used by parent props: ${usedByProps.map((p) => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
|
|
1288
1424
|
: `For parent prop: \`${usedByProps[0]}\``;
|
|
1289
1425
|
parts.push(propsLabel);
|
|
1290
1426
|
parts.push(`Old customData: "${ch.customDataName}"`);
|
|
@@ -1330,12 +1466,15 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1330
1466
|
parts.push(`- **Every COMPONENT_LIST slot needs a child component to render individual items.** A product list needs a ProductCard child, a blog list needs a BlogCard child, etc. Check if the child already exists (run \`config list\` to see all components and their opaque ids; reuse the existing id in \`filteredComponentIds\`). If not, create it as a registered component and use the \`componentId\` from the CLI's response.`);
|
|
1331
1467
|
}
|
|
1332
1468
|
// Check if the section itself has data-driven list props (PRODUCT_LIST, BLOG_LIST, CATEGORY_LIST)
|
|
1333
|
-
const dataListProps = (target.props || []).filter(p => p.type === "PRODUCT_LIST" ||
|
|
1469
|
+
const dataListProps = (target.props || []).filter((p) => p.type === "PRODUCT_LIST" ||
|
|
1470
|
+
p.type === "BLOG_LIST" ||
|
|
1471
|
+
p.type === "CATEGORY_LIST" ||
|
|
1472
|
+
p.type === "BRAND_LIST");
|
|
1334
1473
|
if (dataListProps.length > 0) {
|
|
1335
1474
|
parts.push("");
|
|
1336
1475
|
parts.push(`### Data-Driven List Rendering`);
|
|
1337
1476
|
parts.push("");
|
|
1338
|
-
parts.push(`This section has data-driven list props: ${dataListProps.map(p => `\`${p.name}\` (${p.type})`).join(", ")}.`);
|
|
1477
|
+
parts.push(`This section has data-driven list props: ${dataListProps.map((p) => `\`${p.name}\` (${p.type})`).join(", ")}.`);
|
|
1339
1478
|
parts.push(`These are NOT COMPONENT_LIST — the data comes from dynamic queries (filters, categories, search), not hand-picked items. Render items **internally** by mapping over the data:`);
|
|
1340
1479
|
parts.push("");
|
|
1341
1480
|
parts.push("```tsx");
|
|
@@ -1362,9 +1501,22 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1362
1501
|
parts.push(`See \`get_migration_guide("custom-data-conversion")\` → "Two Ways to Render Lists" for the full pattern.`);
|
|
1363
1502
|
}
|
|
1364
1503
|
// Detect form-page sections (0 or few props, name suggests a form/auth page)
|
|
1365
|
-
const formKeywords = [
|
|
1504
|
+
const formKeywords = [
|
|
1505
|
+
"login",
|
|
1506
|
+
"register",
|
|
1507
|
+
"forgot",
|
|
1508
|
+
"recover",
|
|
1509
|
+
"password",
|
|
1510
|
+
"account",
|
|
1511
|
+
"email",
|
|
1512
|
+
"verification",
|
|
1513
|
+
"activate",
|
|
1514
|
+
"contact",
|
|
1515
|
+
"checkout",
|
|
1516
|
+
"address",
|
|
1517
|
+
];
|
|
1366
1518
|
const lowerDir = (target.dir || "").toLowerCase();
|
|
1367
|
-
const isLikelyFormPage = formKeywords.some(kw => lowerDir.includes(kw));
|
|
1519
|
+
const isLikelyFormPage = formKeywords.some((kw) => lowerDir.includes(kw));
|
|
1368
1520
|
if (isLikelyFormPage) {
|
|
1369
1521
|
parts.push("");
|
|
1370
1522
|
parts.push(`### Form Page Pattern`);
|
|
@@ -1393,7 +1545,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1393
1545
|
// Fallback heuristic only when source scan unavailable
|
|
1394
1546
|
const heuristicLibs = [];
|
|
1395
1547
|
const lowerName = oldName.toLowerCase();
|
|
1396
|
-
if (lowerName.includes("slider") ||
|
|
1548
|
+
if (lowerName.includes("slider") ||
|
|
1549
|
+
lowerName.includes("carousel") ||
|
|
1550
|
+
lowerName.includes("banner")) {
|
|
1397
1551
|
heuristicLibs.push("swiper");
|
|
1398
1552
|
}
|
|
1399
1553
|
if (lowerName.includes("marquee"))
|
|
@@ -1402,7 +1556,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1402
1556
|
heuristicLibs.push("react-player");
|
|
1403
1557
|
if (lowerName.includes("chart"))
|
|
1404
1558
|
heuristicLibs.push("recharts");
|
|
1405
|
-
if (lowerName.includes("star") ||
|
|
1559
|
+
if (lowerName.includes("star") ||
|
|
1560
|
+
lowerName.includes("rating") ||
|
|
1561
|
+
lowerName.includes("review"))
|
|
1406
1562
|
heuristicLibs.push("react-simple-star-rating");
|
|
1407
1563
|
if (heuristicLibs.length > 0) {
|
|
1408
1564
|
parts.push(`### Likely Library Replacements (heuristic — source not scanned)`);
|
|
@@ -1474,8 +1630,7 @@ function levenshtein(a, b) {
|
|
|
1474
1630
|
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
1475
1631
|
curr[j] = Math.min(curr[j - 1] + 1, // insertion
|
|
1476
1632
|
prev[j] + 1, // deletion
|
|
1477
|
-
prev[j - 1] + cost
|
|
1478
|
-
);
|
|
1633
|
+
prev[j - 1] + cost);
|
|
1479
1634
|
}
|
|
1480
1635
|
for (let j = 0; j <= n; j++)
|
|
1481
1636
|
prev[j] = curr[j];
|
|
@@ -1506,17 +1661,29 @@ function searchFunctions(query) {
|
|
|
1506
1661
|
const scored = storefrontData.functions
|
|
1507
1662
|
.map((fn) => {
|
|
1508
1663
|
const nameScore = matchScore(fn.name, query) * 3;
|
|
1509
|
-
const displayNameScore = fn.displayName
|
|
1664
|
+
const displayNameScore = fn.displayName
|
|
1665
|
+
? matchScore(fn.displayName, query) * 3
|
|
1666
|
+
: 0;
|
|
1510
1667
|
const descScore = matchScore(fn.description, query);
|
|
1511
|
-
const catScore = fn.categories.some((c) => matchScore(c, query) > 0)
|
|
1668
|
+
const catScore = fn.categories.some((c) => matchScore(c, query) > 0)
|
|
1669
|
+
? 5
|
|
1670
|
+
: 0;
|
|
1512
1671
|
const paramScore = fn.params.some((p) => matchScore(p.name, query) > 0 || matchScore(p.description, query) > 0)
|
|
1513
1672
|
? 2
|
|
1514
1673
|
: 0;
|
|
1515
1674
|
const sigScore = matchScore(fn.signature, query) * 2;
|
|
1516
|
-
const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0)
|
|
1675
|
+
const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0)
|
|
1676
|
+
? 8
|
|
1677
|
+
: 0;
|
|
1517
1678
|
return {
|
|
1518
1679
|
fn,
|
|
1519
|
-
score: nameScore +
|
|
1680
|
+
score: nameScore +
|
|
1681
|
+
displayNameScore +
|
|
1682
|
+
descScore +
|
|
1683
|
+
catScore +
|
|
1684
|
+
paramScore +
|
|
1685
|
+
sigScore +
|
|
1686
|
+
typeScore,
|
|
1520
1687
|
};
|
|
1521
1688
|
})
|
|
1522
1689
|
.filter((item) => item.score > 0)
|
|
@@ -1530,7 +1697,11 @@ function searchFrameworkTopics(query) {
|
|
|
1530
1697
|
const descScore = matchScore(topic.description, query) * 2;
|
|
1531
1698
|
const contentScore = matchScore(topic.content, query);
|
|
1532
1699
|
const tagScore = topic.tags.some((t) => matchScore(t, query) > 0) ? 5 : 0;
|
|
1533
|
-
return {
|
|
1700
|
+
return {
|
|
1701
|
+
key,
|
|
1702
|
+
topic,
|
|
1703
|
+
score: titleScore + descScore + contentScore + tagScore,
|
|
1704
|
+
};
|
|
1534
1705
|
})
|
|
1535
1706
|
.filter((item) => item.score > 0)
|
|
1536
1707
|
.sort((a, b) => b.score - a.score);
|
|
@@ -1545,8 +1716,16 @@ function searchTypes(query) {
|
|
|
1545
1716
|
const propScore = td.properties?.some((p) => matchScore(p.name, query) > 0 || matchScore(p.type, query) > 0)
|
|
1546
1717
|
? 4
|
|
1547
1718
|
: 0;
|
|
1548
|
-
const enumScore = td.enumValues?.some((v) => matchScore(v, query) > 0)
|
|
1549
|
-
|
|
1719
|
+
const enumScore = td.enumValues?.some((v) => matchScore(v, query) > 0)
|
|
1720
|
+
? 4
|
|
1721
|
+
: 0;
|
|
1722
|
+
const unionScore = td.unionValues?.some((v) => matchScore(v, query) > 0)
|
|
1723
|
+
? 4
|
|
1724
|
+
: 0;
|
|
1725
|
+
return {
|
|
1726
|
+
td,
|
|
1727
|
+
score: nameScore + domainScore + propScore + enumScore + unionScore,
|
|
1728
|
+
};
|
|
1550
1729
|
})
|
|
1551
1730
|
.filter((item) => item.score > 0)
|
|
1552
1731
|
.sort((a, b) => b.score - a.score);
|
|
@@ -1587,8 +1766,12 @@ function formatFunctionDoc(fn) {
|
|
|
1587
1766
|
return lines.join("\n");
|
|
1588
1767
|
}
|
|
1589
1768
|
function formatFunctionSummary(fn) {
|
|
1590
|
-
const desc = fn.description
|
|
1591
|
-
|
|
1769
|
+
const desc = fn.description
|
|
1770
|
+
? fn.description.split(".")[0] + "."
|
|
1771
|
+
: "No description.";
|
|
1772
|
+
const alias = fn.displayName && fn.displayName !== fn.name
|
|
1773
|
+
? ` (alias: ${fn.displayName})`
|
|
1774
|
+
: "";
|
|
1592
1775
|
return `- \`${fn.name}\`${alias} - ${desc}`;
|
|
1593
1776
|
}
|
|
1594
1777
|
function formatTypeDefinition(td, opts = {}) {
|
|
@@ -1596,7 +1779,12 @@ function formatTypeDefinition(td, opts = {}) {
|
|
|
1596
1779
|
const lines = [];
|
|
1597
1780
|
lines.push(`## ${td.name}`);
|
|
1598
1781
|
lines.push("", `**Kind:** ${td.kind} | **Domain:** ${td.domain}`);
|
|
1599
|
-
if (td.kind === "type") {
|
|
1782
|
+
if (td.kind === "type" && td.unionValues) {
|
|
1783
|
+
lines.push("", "```typescript");
|
|
1784
|
+
lines.push(`type ${td.name} = ${td.unionValues.map((v) => `"${v}"`).join(" | ")};`);
|
|
1785
|
+
lines.push("```");
|
|
1786
|
+
}
|
|
1787
|
+
else if (td.kind === "type") {
|
|
1600
1788
|
lines.push("", "```typescript");
|
|
1601
1789
|
const extendsStr = td.extends ? ` & ${td.extends.join(" & ")}` : "";
|
|
1602
1790
|
lines.push(`type ${td.name} = {`);
|
|
@@ -1629,7 +1817,9 @@ function formatTypeDefinition(td, opts = {}) {
|
|
|
1629
1817
|
const fn = storefrontData.functions.find((f) => f.name === fnName);
|
|
1630
1818
|
if (fn) {
|
|
1631
1819
|
const desc = fn.description ? fn.description.split(".")[0] + "." : "";
|
|
1632
|
-
const alias = fn.displayName && fn.displayName !== fn.name
|
|
1820
|
+
const alias = fn.displayName && fn.displayName !== fn.name
|
|
1821
|
+
? ` (alias: ${fn.displayName})`
|
|
1822
|
+
: "";
|
|
1633
1823
|
lines.push(`- **\`${fn.name}\`**${alias} — ${desc}`);
|
|
1634
1824
|
lines.push(` \`${fn.signature}\``);
|
|
1635
1825
|
}
|
|
@@ -1638,7 +1828,9 @@ function formatTypeDefinition(td, opts = {}) {
|
|
|
1638
1828
|
}
|
|
1639
1829
|
}
|
|
1640
1830
|
lines.push("");
|
|
1641
|
-
lines.push(
|
|
1831
|
+
lines.push('Use `get_functions_for_type("' +
|
|
1832
|
+
td.name +
|
|
1833
|
+
'")` for full documentation of these functions.');
|
|
1642
1834
|
}
|
|
1643
1835
|
return lines.join("\n");
|
|
1644
1836
|
}
|
|
@@ -1647,6 +1839,10 @@ function formatTypeSummary(td) {
|
|
|
1647
1839
|
const count = td.enumValues?.length ?? 0;
|
|
1648
1840
|
return `- \`${td.name}\` (enum, ${count} values) — domain: ${td.domain}`;
|
|
1649
1841
|
}
|
|
1842
|
+
if (td.unionValues) {
|
|
1843
|
+
const values = td.unionValues.map((v) => `"${v}"`).join(" | ");
|
|
1844
|
+
return `- \`${td.name}\` (union: ${values}) — domain: ${td.domain}`;
|
|
1845
|
+
}
|
|
1650
1846
|
const count = td.properties?.length ?? 0;
|
|
1651
1847
|
const ext = td.extends ? ` extends ${td.extends.join(", ")}` : "";
|
|
1652
1848
|
return `- \`${td.name}\` (${count} properties${ext}) — domain: ${td.domain}`;
|
|
@@ -1656,7 +1852,13 @@ const server = new McpServer({
|
|
|
1656
1852
|
name: "ikas-code-components",
|
|
1657
1853
|
version: "0.1.0",
|
|
1658
1854
|
}, {
|
|
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
|
|
1855
|
+
instructions: "Examples and section templates from this server are API reference only — reuse imports, function calls, and data-access patterns; create your own JSX structure, CSS class names, and visual design.\n\n" +
|
|
1856
|
+
"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" +
|
|
1857
|
+
"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" +
|
|
1858
|
+
"TWO DISTINCT JOBS — do not confuse them, and do not jump to writing code for the second one:\n" +
|
|
1859
|
+
"(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" +
|
|
1860
|
+
"(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" +
|
|
1861
|
+
"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.",
|
|
1660
1862
|
});
|
|
1661
1863
|
// Tool: search_docs
|
|
1662
1864
|
server.tool("search_docs", "Search across all ikas storefront API docs, framework guides, and migration guides. Returns matching functions, framework topics, and migration topics ranked by relevance.", { query: z.string().describe("Search keyword or phrase") }, async ({ query }) => {
|
|
@@ -1697,13 +1899,20 @@ server.tool("search_docs", "Search across all ikas storefront API docs, framewor
|
|
|
1697
1899
|
parts.push("");
|
|
1698
1900
|
parts.push("Use `get_migration_guide(topic)` to get full content for any migration topic.");
|
|
1699
1901
|
}
|
|
1700
|
-
if (functions.length === 0 &&
|
|
1902
|
+
if (functions.length === 0 &&
|
|
1903
|
+
topics.length === 0 &&
|
|
1904
|
+
types.length === 0 &&
|
|
1905
|
+
migrationTopics.length === 0) {
|
|
1701
1906
|
parts.push(`No results found for "${query}". Try different keywords or use \`list_functions()\` to see all available functions.`);
|
|
1702
1907
|
}
|
|
1703
1908
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1704
1909
|
});
|
|
1705
1910
|
// Tool: get_function_doc
|
|
1706
|
-
server.tool("get_function_doc", "Get full documentation for a specific storefront API function including signature, parameters, return type, and example.", {
|
|
1911
|
+
server.tool("get_function_doc", "Get full documentation for a specific storefront API function including signature, parameters, return type, and example.", {
|
|
1912
|
+
name: z
|
|
1913
|
+
.string()
|
|
1914
|
+
.describe("Function name (e.g. 'addItemToCart', 'Router.navigate')"),
|
|
1915
|
+
}, async ({ name }) => {
|
|
1707
1916
|
const nameLower = name.toLowerCase();
|
|
1708
1917
|
// Phase 1: canonical-name match wins. A real function name always outranks
|
|
1709
1918
|
// any displayName alias so aliases can never shadow the function they're
|
|
@@ -1711,7 +1920,9 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1711
1920
|
// [BP-DISPLAY-NAME: hasCustomer] alias).
|
|
1712
1921
|
const byName = storefrontData.functions.find((f) => f.name.toLowerCase() === nameLower);
|
|
1713
1922
|
if (byName) {
|
|
1714
|
-
return {
|
|
1923
|
+
return {
|
|
1924
|
+
content: [{ type: "text", text: formatFunctionDoc(byName) }],
|
|
1925
|
+
};
|
|
1715
1926
|
}
|
|
1716
1927
|
// Phase 2: fall back to displayName aliases.
|
|
1717
1928
|
const byAlias = storefrontData.functions.filter((f) => f.displayName && f.displayName.toLowerCase() === nameLower);
|
|
@@ -1719,7 +1930,9 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1719
1930
|
const fn = byAlias[0];
|
|
1720
1931
|
const note = `> Note: "${name}" is a display alias for \`${fn.name}\`.\n\n`;
|
|
1721
1932
|
return {
|
|
1722
|
-
content: [
|
|
1933
|
+
content: [
|
|
1934
|
+
{ type: "text", text: note + formatFunctionDoc(fn) },
|
|
1935
|
+
],
|
|
1723
1936
|
};
|
|
1724
1937
|
}
|
|
1725
1938
|
if (byAlias.length > 1) {
|
|
@@ -1737,7 +1950,9 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1737
1950
|
(f.displayName && f.displayName.toLowerCase().includes(nameLower)));
|
|
1738
1951
|
if (matches.length > 0) {
|
|
1739
1952
|
const suggestions = matches.slice(0, 5).map((f) => {
|
|
1740
|
-
const alias = f.displayName && f.displayName !== f.name
|
|
1953
|
+
const alias = f.displayName && f.displayName !== f.name
|
|
1954
|
+
? ` (alias: ${f.displayName})`
|
|
1955
|
+
: "";
|
|
1741
1956
|
return ` - ${f.name}${alias}`;
|
|
1742
1957
|
});
|
|
1743
1958
|
return {
|
|
@@ -1750,7 +1965,12 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1750
1965
|
};
|
|
1751
1966
|
}
|
|
1752
1967
|
return {
|
|
1753
|
-
content: [
|
|
1968
|
+
content: [
|
|
1969
|
+
{
|
|
1970
|
+
type: "text",
|
|
1971
|
+
text: `Function "${name}" not found. Use \`list_functions()\` to see all available functions.`,
|
|
1972
|
+
},
|
|
1973
|
+
],
|
|
1754
1974
|
};
|
|
1755
1975
|
});
|
|
1756
1976
|
// Tool: list_functions
|
|
@@ -1800,7 +2020,7 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
|
|
|
1800
2020
|
if (uncategorized > 0) {
|
|
1801
2021
|
lines.push(`- \`Other\` (${uncategorized})`);
|
|
1802
2022
|
}
|
|
1803
|
-
lines.push("",
|
|
2023
|
+
lines.push("", 'Call `list_functions(category: "<name>")` to see one-line summaries for a category.');
|
|
1804
2024
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1805
2025
|
}
|
|
1806
2026
|
const catLower = category.toLowerCase();
|
|
@@ -1832,7 +2052,11 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
|
|
|
1832
2052
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1833
2053
|
});
|
|
1834
2054
|
// Tool: get_code_example
|
|
1835
|
-
server.tool("get_code_example", "Get an API usage reference for a specific task. Shows correct function calls, imports, and data-handling patterns from a real production theme. The JSX layout and CSS are illustrative only — create your own original visual design. Call `list_examples()` to see available example IDs.", {
|
|
2055
|
+
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.", {
|
|
2056
|
+
task: z
|
|
2057
|
+
.string()
|
|
2058
|
+
.describe("Task description or example ID (call `list_examples()` for the full list)"),
|
|
2059
|
+
}, async ({ task }) => {
|
|
1836
2060
|
const taskLower = task.toLowerCase();
|
|
1837
2061
|
// Try exact ID match first
|
|
1838
2062
|
let example = storefrontData.codeExamples.find((e) => e.id === taskLower);
|
|
@@ -1852,7 +2076,9 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
1852
2076
|
}
|
|
1853
2077
|
}
|
|
1854
2078
|
if (!example) {
|
|
1855
|
-
const available = storefrontData.codeExamples
|
|
2079
|
+
const available = storefrontData.codeExamples
|
|
2080
|
+
.map((e) => ` - \`${e.id}\` - ${e.title}`)
|
|
2081
|
+
.join("\n");
|
|
1856
2082
|
return {
|
|
1857
2083
|
content: [
|
|
1858
2084
|
{
|
|
@@ -1875,14 +2101,24 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
1875
2101
|
if (example.files && example.files.length > 0) {
|
|
1876
2102
|
for (const file of example.files) {
|
|
1877
2103
|
const ext = file.filename.split(".").pop() || "text";
|
|
1878
|
-
const lang = ext === "tsx" || ext === "ts"
|
|
2104
|
+
const lang = ext === "tsx" || ext === "ts"
|
|
2105
|
+
? "typescript"
|
|
2106
|
+
: ext === "css"
|
|
2107
|
+
? "css"
|
|
2108
|
+
: ext === "json"
|
|
2109
|
+
? "json"
|
|
2110
|
+
: "text";
|
|
1879
2111
|
// Add inline originality comments to CSS and TSX files
|
|
1880
2112
|
let content = file.content;
|
|
1881
2113
|
if (ext === "css") {
|
|
1882
|
-
content =
|
|
2114
|
+
content =
|
|
2115
|
+
"/* EXAMPLE STYLING — create your own original CSS with different class names and design */\n" +
|
|
2116
|
+
content;
|
|
1883
2117
|
}
|
|
1884
2118
|
else if (ext === "tsx") {
|
|
1885
|
-
content =
|
|
2119
|
+
content =
|
|
2120
|
+
"// EXAMPLE COMPONENT — use the API patterns but create your own JSX structure and layout\n" +
|
|
2121
|
+
content;
|
|
1886
2122
|
}
|
|
1887
2123
|
parts.push(`### ${file.filename}`, "", `\`\`\`${lang}`, content, "```", "");
|
|
1888
2124
|
}
|
|
@@ -1896,63 +2132,88 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
1896
2132
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1897
2133
|
});
|
|
1898
2134
|
// Tool: get_framework_guide
|
|
1899
|
-
server.tool("get_framework_guide", "Get a framework guide on a specific topic (e.g. 'ai-workflow', 'common-pitfalls', 'prop-types', 'css-scoping', 'form-handling'). Call `list_topics()` to see all available topic keys.", {
|
|
2135
|
+
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.", {
|
|
2136
|
+
topic: z
|
|
2137
|
+
.string()
|
|
2138
|
+
.describe("Topic key or keyword (call `list_topics()` for the full list)"),
|
|
2139
|
+
}, async ({ topic }) => {
|
|
1900
2140
|
const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
|
|
1901
2141
|
// Alias mapping for common alternative topic names
|
|
1902
2142
|
const topicAliases = {
|
|
1903
2143
|
"form-handling": "form-patterns",
|
|
1904
|
-
|
|
2144
|
+
forms: "form-patterns",
|
|
1905
2145
|
"data-fetching": "async-data-patterns",
|
|
1906
|
-
|
|
1907
|
-
|
|
2146
|
+
async: "async-data-patterns",
|
|
2147
|
+
loading: "async-data-patterns",
|
|
1908
2148
|
"sub-components": "sub-component-patterns",
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2149
|
+
subcomponents: "sub-component-patterns",
|
|
2150
|
+
routing: "navigation-patterns",
|
|
2151
|
+
router: "navigation-patterns",
|
|
2152
|
+
observer: "component-structure",
|
|
2153
|
+
reactivity: "component-structure",
|
|
2154
|
+
pitfalls: "common-pitfalls",
|
|
2155
|
+
gotchas: "common-pitfalls",
|
|
2156
|
+
mistakes: "common-pitfalls",
|
|
2157
|
+
header: "header-footer-patterns",
|
|
2158
|
+
footer: "header-footer-patterns",
|
|
2159
|
+
blog: "blog-patterns",
|
|
2160
|
+
cart: "cart-patterns",
|
|
2161
|
+
account: "account-patterns",
|
|
1922
2162
|
"product-detail": "product-detail-patterns",
|
|
1923
2163
|
"product-list": "product-list-patterns",
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2164
|
+
filtering: "product-list-patterns",
|
|
2165
|
+
reviews: "review-patterns",
|
|
2166
|
+
slider: "slider-overlay-patterns",
|
|
2167
|
+
overlay: "slider-overlay-patterns",
|
|
2168
|
+
modal: "slider-overlay-patterns",
|
|
2169
|
+
architecture: "real-world-architecture",
|
|
2170
|
+
theme: "real-world-architecture",
|
|
1931
2171
|
"global-styles": "global-css",
|
|
1932
|
-
|
|
2172
|
+
global: "global-css",
|
|
1933
2173
|
"css-variables": "global-css",
|
|
1934
2174
|
"custom-properties": "global-css",
|
|
1935
2175
|
};
|
|
1936
2176
|
const resolvedTopic = topicAliases[topicLower] || topicLower;
|
|
1937
2177
|
// Topics that involve MobX store reads get a reminder about root reactivity
|
|
1938
2178
|
const storeTopics = new Set([
|
|
1939
|
-
"product-detail-patterns",
|
|
1940
|
-
"
|
|
1941
|
-
"
|
|
1942
|
-
"
|
|
2179
|
+
"product-detail-patterns",
|
|
2180
|
+
"product-list-patterns",
|
|
2181
|
+
"cart-patterns",
|
|
2182
|
+
"account-patterns",
|
|
2183
|
+
"header-footer-patterns",
|
|
2184
|
+
"review-patterns",
|
|
2185
|
+
"blog-patterns",
|
|
2186
|
+
"form-handling",
|
|
2187
|
+
"async-data-patterns",
|
|
2188
|
+
"component-structure",
|
|
2189
|
+
"imports",
|
|
1943
2190
|
]);
|
|
1944
2191
|
const observerReminder = "> **IMPORTANT: Do NOT use `observer()` on root component exports.** The ikas runtime wraps root renders in MobX `autorun()`, making them automatically reactive. All store reads (`cartStore`, `customerStore`, etc.) in root components are tracked automatically. Only use `observer()` on extracted sub-components.\n\n";
|
|
1945
2192
|
// Try exact key match (with alias resolution)
|
|
1946
2193
|
if (frameworkData.topics[resolvedTopic]) {
|
|
1947
2194
|
const t = frameworkData.topics[resolvedTopic];
|
|
1948
2195
|
const prefix = storeTopics.has(resolvedTopic) ? observerReminder : "";
|
|
1949
|
-
return {
|
|
2196
|
+
return {
|
|
2197
|
+
content: [
|
|
2198
|
+
{
|
|
2199
|
+
type: "text",
|
|
2200
|
+
text: `## ${t.title}\n\n${prefix}${t.content}`,
|
|
2201
|
+
},
|
|
2202
|
+
],
|
|
2203
|
+
};
|
|
1950
2204
|
}
|
|
1951
2205
|
// Try original topic key (without alias) in case it's a direct key
|
|
1952
2206
|
if (resolvedTopic !== topicLower && frameworkData.topics[topicLower]) {
|
|
1953
2207
|
const t = frameworkData.topics[topicLower];
|
|
1954
2208
|
const prefix = storeTopics.has(topicLower) ? observerReminder : "";
|
|
1955
|
-
return {
|
|
2209
|
+
return {
|
|
2210
|
+
content: [
|
|
2211
|
+
{
|
|
2212
|
+
type: "text",
|
|
2213
|
+
text: `## ${t.title}\n\n${prefix}${t.content}`,
|
|
2214
|
+
},
|
|
2215
|
+
],
|
|
2216
|
+
};
|
|
1956
2217
|
}
|
|
1957
2218
|
// Try keyword search
|
|
1958
2219
|
const matches = searchFrameworkTopics(topic);
|
|
@@ -1960,7 +2221,12 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
|
|
|
1960
2221
|
const best = matches[0];
|
|
1961
2222
|
const prefix = storeTopics.has(best.key) ? observerReminder : "";
|
|
1962
2223
|
return {
|
|
1963
|
-
content: [
|
|
2224
|
+
content: [
|
|
2225
|
+
{
|
|
2226
|
+
type: "text",
|
|
2227
|
+
text: `## ${best.topic.title}\n\n${prefix}${best.topic.content}`,
|
|
2228
|
+
},
|
|
2229
|
+
],
|
|
1964
2230
|
};
|
|
1965
2231
|
}
|
|
1966
2232
|
const available = Object.entries(frameworkData.topics)
|
|
@@ -1976,16 +2242,27 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
|
|
|
1976
2242
|
};
|
|
1977
2243
|
});
|
|
1978
2244
|
// Tool: get_type_definition
|
|
1979
|
-
server.tool("get_type_definition", "Get the full definition of a storefront type or enum by name (e.g. 'IkasProduct', 'IkasOrderStatus')
|
|
2245
|
+
server.tool("get_type_definition", "Get the full definition of a storefront type or enum by name. Shows all properties with types, extends, enum values, or string-union values. Covers runtime models (e.g. 'IkasProduct', 'IkasOrderStatus'), theme-globals tokens returned by the getters (e.g. 'ThemeSetting', 'DesignToken', 'TypographyToken', 'ColorSchemeToken', 'BreakpointToken', 'KeyframeToken'), and style-prop TYPE shapes used by ikas.config.json props (e.g. 'BorderRadiusStyleType', 'PaddingStyleType', 'GapStyleType', 'SizeStyleType' and their '*StyleUnitType' unions). If a type reports as not found, it may not be indexed yet — fall back to reading the package's `.d.ts` (e.g. `@ikas/bp-storefront-config`) rather than typing the value as `any`.", {
|
|
2246
|
+
name: z
|
|
2247
|
+
.string()
|
|
2248
|
+
.describe("Type or enum name (e.g. 'IkasProduct', 'IkasOrderStatus')"),
|
|
2249
|
+
}, async ({ name }) => {
|
|
1980
2250
|
if (!typesData) {
|
|
1981
2251
|
return {
|
|
1982
|
-
content: [
|
|
2252
|
+
content: [
|
|
2253
|
+
{
|
|
2254
|
+
type: "text",
|
|
2255
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2256
|
+
},
|
|
2257
|
+
],
|
|
1983
2258
|
};
|
|
1984
2259
|
}
|
|
1985
2260
|
const nameLower = name.toLowerCase();
|
|
1986
2261
|
const td = typesData.types.find((t) => t.name.toLowerCase() === nameLower);
|
|
1987
2262
|
if (td) {
|
|
1988
|
-
return {
|
|
2263
|
+
return {
|
|
2264
|
+
content: [{ type: "text", text: formatTypeDefinition(td) }],
|
|
2265
|
+
};
|
|
1989
2266
|
}
|
|
1990
2267
|
// Fuzzy match
|
|
1991
2268
|
const matches = typesData.types.filter((t) => t.name.toLowerCase().includes(nameLower));
|
|
@@ -2001,14 +2278,28 @@ server.tool("get_type_definition", "Get the full definition of a storefront type
|
|
|
2001
2278
|
};
|
|
2002
2279
|
}
|
|
2003
2280
|
return {
|
|
2004
|
-
content: [
|
|
2281
|
+
content: [
|
|
2282
|
+
{
|
|
2283
|
+
type: "text",
|
|
2284
|
+
text: `Type "${name}" not found. Use \`list_types()\` to see all available types.`,
|
|
2285
|
+
},
|
|
2286
|
+
],
|
|
2005
2287
|
};
|
|
2006
2288
|
});
|
|
2007
2289
|
// Tool: get_functions_for_type
|
|
2008
|
-
server.tool("get_functions_for_type", "Get full documentation for all utility functions that operate on a given storefront type. For example, get_functions_for_type('IkasImage') returns getSrc, getDefaultSrc, getThumbnailSrc, createMediaSrcset with full signatures, descriptions, and examples.", {
|
|
2290
|
+
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.", {
|
|
2291
|
+
typeName: z
|
|
2292
|
+
.string()
|
|
2293
|
+
.describe("Type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
|
|
2294
|
+
}, async ({ typeName }) => {
|
|
2009
2295
|
if (!typesData) {
|
|
2010
2296
|
return {
|
|
2011
|
-
content: [
|
|
2297
|
+
content: [
|
|
2298
|
+
{
|
|
2299
|
+
type: "text",
|
|
2300
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2301
|
+
},
|
|
2302
|
+
],
|
|
2012
2303
|
};
|
|
2013
2304
|
}
|
|
2014
2305
|
const nameLower = typeName.toLowerCase();
|
|
@@ -2028,7 +2319,12 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
|
|
|
2028
2319
|
};
|
|
2029
2320
|
}
|
|
2030
2321
|
return {
|
|
2031
|
-
content: [
|
|
2322
|
+
content: [
|
|
2323
|
+
{
|
|
2324
|
+
type: "text",
|
|
2325
|
+
text: `Type "${typeName}" not found. Use \`list_types()\` to see all available types.`,
|
|
2326
|
+
},
|
|
2327
|
+
],
|
|
2032
2328
|
};
|
|
2033
2329
|
}
|
|
2034
2330
|
if (!td.relatedFunctions || td.relatedFunctions.length === 0) {
|
|
@@ -2059,7 +2355,10 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
|
|
|
2059
2355
|
});
|
|
2060
2356
|
// Tool: get_model_guide
|
|
2061
2357
|
server.tool("get_model_guide", "Get an overview of a storefront model type. By default returns the type definition, related function names with one-line summaries, matching example titles/IDs, and related type summaries. Pass `mode: 'full'` to inline full function docs and example code.", {
|
|
2062
|
-
model: z
|
|
2358
|
+
model: z
|
|
2359
|
+
.string()
|
|
2360
|
+
.optional()
|
|
2361
|
+
.describe("Model type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
|
|
2063
2362
|
name: z.string().optional().describe("Alias for 'model'"),
|
|
2064
2363
|
mode: z
|
|
2065
2364
|
.enum(["summary", "full"])
|
|
@@ -2070,12 +2369,22 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
2070
2369
|
const model = modelParam || nameParam;
|
|
2071
2370
|
if (!model) {
|
|
2072
2371
|
return {
|
|
2073
|
-
content: [
|
|
2372
|
+
content: [
|
|
2373
|
+
{
|
|
2374
|
+
type: "text",
|
|
2375
|
+
text: "Please provide a model name (e.g. 'IkasProduct', 'IkasOrder'). Use the 'model' or 'name' parameter.",
|
|
2376
|
+
},
|
|
2377
|
+
],
|
|
2074
2378
|
};
|
|
2075
2379
|
}
|
|
2076
2380
|
if (!typesData) {
|
|
2077
2381
|
return {
|
|
2078
|
-
content: [
|
|
2382
|
+
content: [
|
|
2383
|
+
{
|
|
2384
|
+
type: "text",
|
|
2385
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2386
|
+
},
|
|
2387
|
+
],
|
|
2079
2388
|
};
|
|
2080
2389
|
}
|
|
2081
2390
|
const modelLower = model.toLowerCase();
|
|
@@ -2097,7 +2406,12 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
2097
2406
|
};
|
|
2098
2407
|
}
|
|
2099
2408
|
return {
|
|
2100
|
-
content: [
|
|
2409
|
+
content: [
|
|
2410
|
+
{
|
|
2411
|
+
type: "text",
|
|
2412
|
+
text: `Model "${model}" not found. Use \`list_types()\` to see all available types.`,
|
|
2413
|
+
},
|
|
2414
|
+
],
|
|
2101
2415
|
};
|
|
2102
2416
|
}
|
|
2103
2417
|
const parts = [`# Model Guide: ${td.name}\n`];
|
|
@@ -2205,13 +2519,23 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
2205
2519
|
server.tool("search_types", "Search storefront types and enums by keyword (e.g. 'price', 'address', 'status'). Returns top matches ranked by relevance.", { query: z.string().describe("Search keyword or phrase") }, async ({ query }) => {
|
|
2206
2520
|
if (!typesData) {
|
|
2207
2521
|
return {
|
|
2208
|
-
content: [
|
|
2522
|
+
content: [
|
|
2523
|
+
{
|
|
2524
|
+
type: "text",
|
|
2525
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2526
|
+
},
|
|
2527
|
+
],
|
|
2209
2528
|
};
|
|
2210
2529
|
}
|
|
2211
2530
|
const results = searchTypes(query).slice(0, 15);
|
|
2212
2531
|
if (results.length === 0) {
|
|
2213
2532
|
return {
|
|
2214
|
-
content: [
|
|
2533
|
+
content: [
|
|
2534
|
+
{
|
|
2535
|
+
type: "text",
|
|
2536
|
+
text: `No types found matching "${query}". Use \`list_types()\` to see all available types.`,
|
|
2537
|
+
},
|
|
2538
|
+
],
|
|
2215
2539
|
};
|
|
2216
2540
|
}
|
|
2217
2541
|
const parts = [`## Type Search Results for "${query}"\n`];
|
|
@@ -2275,7 +2599,7 @@ server.tool("list_types", "List storefront types and enums. Use `domain` and/or
|
|
|
2275
2599
|
for (const [d, count] of sorted) {
|
|
2276
2600
|
lines.push(`- \`${d}\` (${count})`);
|
|
2277
2601
|
}
|
|
2278
|
-
lines.push("",
|
|
2602
|
+
lines.push("", 'Call `list_types(domain: "<name>")` to see summaries for a domain.');
|
|
2279
2603
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2280
2604
|
}
|
|
2281
2605
|
const domainLower = domain.toLowerCase();
|
|
@@ -2314,15 +2638,25 @@ server.tool("list_types", "List storefront types and enums. Use `domain` and/or
|
|
|
2314
2638
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
2315
2639
|
});
|
|
2316
2640
|
// Tool: get_prop_types
|
|
2317
|
-
server.tool("get_prop_types",
|
|
2641
|
+
server.tool("get_prop_types", 'Get all available ikas.config.json prop types with descriptions, TypeScript types, and examples. NOTE: these are the TypeScript prop-definition types for authoring a component — they are NOT the runtime JSON value shapes you write via `update_section_prop` (e.g. a TEXT prop is typed `string` here but written as `{ "value": "..." }`, and an IMAGE is `IkasImage | null` here but written as `{ "id": "..." }`). For the value shapes to pass to `update_section_prop`, see that tool\'s description. Tip: Use `npx ikas-component config add-component --props \'[...]\'` to create a component with all props in one command, or `add-prop` to add props incrementally. NEVER manually edit types.ts — it is auto-generated by the CLI.', {}, async () => {
|
|
2318
2642
|
const propTypesTopic = frameworkData.topics["prop-types"];
|
|
2319
2643
|
if (propTypesTopic) {
|
|
2320
2644
|
return {
|
|
2321
|
-
content: [
|
|
2645
|
+
content: [
|
|
2646
|
+
{
|
|
2647
|
+
type: "text",
|
|
2648
|
+
text: `## ${propTypesTopic.title}\n\n${propTypesTopic.content}`,
|
|
2649
|
+
},
|
|
2650
|
+
],
|
|
2322
2651
|
};
|
|
2323
2652
|
}
|
|
2324
2653
|
return {
|
|
2325
|
-
content: [
|
|
2654
|
+
content: [
|
|
2655
|
+
{
|
|
2656
|
+
type: "text",
|
|
2657
|
+
text: "Prop types documentation not available.",
|
|
2658
|
+
},
|
|
2659
|
+
],
|
|
2326
2660
|
};
|
|
2327
2661
|
});
|
|
2328
2662
|
// Tool: get_section_template
|
|
@@ -2419,7 +2753,8 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2419
2753
|
// the end of the response in the existing recipe-builder block.
|
|
2420
2754
|
{
|
|
2421
2755
|
const snippetStrForBanner = bundle.rootFiles["ikas-config-snippet.json"];
|
|
2422
|
-
if (snippetStrForBanner &&
|
|
2756
|
+
if (snippetStrForBanner &&
|
|
2757
|
+
/<id-of-[A-Za-z0-9_]+>/.test(snippetStrForBanner)) {
|
|
2423
2758
|
const childMatches = Array.from(snippetStrForBanner.matchAll(/<id-of-([A-Za-z0-9_]+)>/g));
|
|
2424
2759
|
const uniqueChildren = Array.from(new Set(childMatches.map((m) => m[1])));
|
|
2425
2760
|
parts.push(`> 🔧 **CONTAINER SECTION — required wiring.** This template hosts ${uniqueChildren.length} child component${uniqueChildren.length === 1 ? "" : "s"} (${uniqueChildren.map((n) => `\`${n}\``).join(", ")}) via a \`COMPONENT_LIST\` slot. Creating the parent alone produces an empty, unusable section. **You MUST follow the full Setup Recipe below**: create each child → capture its \`componentId\` → wire them into the parent's \`filteredComponentIds\` with \`config update-prop\`. Component ids are opaque random strings (e.g. \`7ojrigep-Eml9n5sN3i\`) — they cannot be derived from names, and the CLI rejects unknown ids.`, "");
|
|
@@ -2543,7 +2878,10 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2543
2878
|
try {
|
|
2544
2879
|
const childSnippet = JSON.parse(fs.readFileSync(childSnippetPath, "utf-8"));
|
|
2545
2880
|
const childProps = (childSnippet.props || []).map((p) => {
|
|
2546
|
-
const out = {
|
|
2881
|
+
const out = {
|
|
2882
|
+
name: p.name,
|
|
2883
|
+
type: p.type,
|
|
2884
|
+
};
|
|
2547
2885
|
if (p.displayName)
|
|
2548
2886
|
out.displayName = p.displayName;
|
|
2549
2887
|
if (p.required)
|
|
@@ -2616,10 +2954,7 @@ server.tool("get_section_child", "Fetch one item's files from a section's childr
|
|
|
2616
2954
|
.string()
|
|
2617
2955
|
.optional()
|
|
2618
2956
|
.describe("The item name as listed in `get_section_template`'s response (Children/Components/Sub-components)"),
|
|
2619
|
-
child: z
|
|
2620
|
-
.string()
|
|
2621
|
-
.optional()
|
|
2622
|
-
.describe("Alias for `name`"),
|
|
2957
|
+
child: z.string().optional().describe("Alias for `name`"),
|
|
2623
2958
|
kind: z
|
|
2624
2959
|
.enum(["children", "components", "sub-components"])
|
|
2625
2960
|
.optional()
|
|
@@ -2782,7 +3117,11 @@ server.tool("list_section_types", "List all available `get_section_template` sec
|
|
|
2782
3117
|
});
|
|
2783
3118
|
// --- Migration tools ---
|
|
2784
3119
|
// Tool: analyze_old_theme
|
|
2785
|
-
server.tool("analyze_old_theme", "Analyze an old ikas storefront theme.json and produce a structured migration report. Shows all components, custom data definitions, prop type breakdown, and migration recommendations. Use this as the first step when converting an old theme.", {
|
|
3120
|
+
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.", {
|
|
3121
|
+
theme_json: z
|
|
3122
|
+
.string()
|
|
3123
|
+
.describe("The raw JSON content of the old theme.json file"),
|
|
3124
|
+
}, async ({ theme_json }) => {
|
|
2786
3125
|
try {
|
|
2787
3126
|
const parsed = JSON.parse(theme_json);
|
|
2788
3127
|
const analysis = analyzeOldTheme(parsed);
|
|
@@ -2790,102 +3129,162 @@ server.tool("analyze_old_theme", "Analyze an old ikas storefront theme.json and
|
|
|
2790
3129
|
}
|
|
2791
3130
|
catch (err) {
|
|
2792
3131
|
return {
|
|
2793
|
-
content: [
|
|
3132
|
+
content: [
|
|
3133
|
+
{
|
|
3134
|
+
type: "text",
|
|
3135
|
+
text: `Error parsing theme.json: ${err instanceof Error ? err.message : String(err)}. Make sure you're passing valid JSON.`,
|
|
3136
|
+
},
|
|
3137
|
+
],
|
|
2794
3138
|
};
|
|
2795
3139
|
}
|
|
2796
3140
|
});
|
|
2797
3141
|
// Tool: get_migration_guide
|
|
2798
3142
|
const migrationTopicAliases = {
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
3143
|
+
overview: "migration-overview",
|
|
3144
|
+
migrate: "migration-overview",
|
|
3145
|
+
custom: "custom-data-conversion",
|
|
2802
3146
|
"custom-data": "custom-data-conversion",
|
|
2803
|
-
|
|
3147
|
+
customdata: "custom-data-conversion",
|
|
2804
3148
|
"dynamic-list": "custom-data-conversion",
|
|
2805
3149
|
"component-list": "custom-data-conversion",
|
|
2806
|
-
|
|
2807
|
-
|
|
3150
|
+
slider: "prop-type-mapping",
|
|
3151
|
+
props: "prop-type-mapping",
|
|
2808
3152
|
"prop-mapping": "prop-type-mapping",
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
3153
|
+
types: "prop-type-mapping",
|
|
3154
|
+
react: "react-to-preact",
|
|
3155
|
+
preact: "react-to-preact",
|
|
3156
|
+
observer: "react-to-preact",
|
|
3157
|
+
libraries: "library-replacements",
|
|
3158
|
+
swiper: "library-replacements",
|
|
3159
|
+
headlessui: "library-replacements",
|
|
3160
|
+
tailwind: "library-replacements",
|
|
3161
|
+
tailwindcss: "library-replacements",
|
|
3162
|
+
recharts: "library-replacements",
|
|
3163
|
+
marquee: "library-replacements",
|
|
3164
|
+
imports: "storefront-import-mapping",
|
|
3165
|
+
storefront: "storefront-import-mapping",
|
|
2822
3166
|
"bp-storefront": "storefront-import-mapping",
|
|
2823
3167
|
"theme-json": "theme-json-anatomy",
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
3168
|
+
anatomy: "theme-json-anatomy",
|
|
3169
|
+
decompose: "component-decomposition-strategy",
|
|
3170
|
+
decomposition: "component-decomposition-strategy",
|
|
3171
|
+
strategy: "component-decomposition-strategy",
|
|
3172
|
+
project: "complete-project-generation",
|
|
3173
|
+
generate: "complete-project-generation",
|
|
3174
|
+
generation: "complete-project-generation",
|
|
3175
|
+
settings: "settings-conversion",
|
|
3176
|
+
colors: "settings-conversion",
|
|
3177
|
+
fonts: "settings-conversion",
|
|
3178
|
+
find: "finding-new-system-equivalents",
|
|
3179
|
+
search: "finding-new-system-equivalents",
|
|
3180
|
+
discover: "finding-new-system-equivalents",
|
|
3181
|
+
equivalent: "finding-new-system-equivalents",
|
|
3182
|
+
equivalents: "finding-new-system-equivalents",
|
|
3183
|
+
replacement: "finding-new-system-equivalents",
|
|
2840
3184
|
};
|
|
2841
3185
|
const migrationTopicKeys = migrationData
|
|
2842
3186
|
? Object.keys(migrationData.topics)
|
|
2843
3187
|
: [];
|
|
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.`, {
|
|
3188
|
+
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.`, {
|
|
3189
|
+
topic: z
|
|
3190
|
+
.string()
|
|
3191
|
+
.describe("Migration topic key, alias, or 'list' to see all topics"),
|
|
3192
|
+
}, async ({ topic }) => {
|
|
2845
3193
|
if (!migrationData) {
|
|
2846
|
-
return {
|
|
3194
|
+
return {
|
|
3195
|
+
content: [
|
|
3196
|
+
{
|
|
3197
|
+
type: "text",
|
|
3198
|
+
text: "Migration data not available. Ensure data/migration.json exists.",
|
|
3199
|
+
},
|
|
3200
|
+
],
|
|
3201
|
+
};
|
|
2847
3202
|
}
|
|
2848
3203
|
if (topic.toLowerCase() === "list") {
|
|
2849
3204
|
const available = Object.entries(migrationData.topics)
|
|
2850
3205
|
.map(([key, t]) => `- \`${key}\` — ${t.title}: ${t.description}`)
|
|
2851
3206
|
.join("\n");
|
|
2852
|
-
return {
|
|
3207
|
+
return {
|
|
3208
|
+
content: [
|
|
3209
|
+
{
|
|
3210
|
+
type: "text",
|
|
3211
|
+
text: `## Available Migration Topics\n\n${available}`,
|
|
3212
|
+
},
|
|
3213
|
+
],
|
|
3214
|
+
};
|
|
2853
3215
|
}
|
|
2854
3216
|
const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
|
|
2855
3217
|
const resolvedTopic = migrationTopicAliases[topicLower] || topicLower;
|
|
2856
3218
|
if (migrationData.topics[resolvedTopic]) {
|
|
2857
3219
|
const t = migrationData.topics[resolvedTopic];
|
|
2858
|
-
return {
|
|
3220
|
+
return {
|
|
3221
|
+
content: [
|
|
3222
|
+
{ type: "text", text: `## ${t.title}\n\n${t.content}` },
|
|
3223
|
+
],
|
|
3224
|
+
};
|
|
2859
3225
|
}
|
|
2860
3226
|
// Try original key
|
|
2861
3227
|
if (resolvedTopic !== topicLower && migrationData.topics[topicLower]) {
|
|
2862
3228
|
const t = migrationData.topics[topicLower];
|
|
2863
|
-
return {
|
|
3229
|
+
return {
|
|
3230
|
+
content: [
|
|
3231
|
+
{ type: "text", text: `## ${t.title}\n\n${t.content}` },
|
|
3232
|
+
],
|
|
3233
|
+
};
|
|
2864
3234
|
}
|
|
2865
3235
|
// Keyword search
|
|
2866
3236
|
const matches = searchMigrationTopics(topic);
|
|
2867
3237
|
if (matches.length > 0) {
|
|
2868
3238
|
const best = matches[0];
|
|
2869
|
-
return {
|
|
3239
|
+
return {
|
|
3240
|
+
content: [
|
|
3241
|
+
{
|
|
3242
|
+
type: "text",
|
|
3243
|
+
text: `## ${best.topic.title}\n\n${best.topic.content}`,
|
|
3244
|
+
},
|
|
3245
|
+
],
|
|
3246
|
+
};
|
|
2870
3247
|
}
|
|
2871
3248
|
const available = Object.entries(migrationData.topics)
|
|
2872
3249
|
.map(([key, t]) => ` - \`${key}\` - ${t.title}`)
|
|
2873
3250
|
.join("\n");
|
|
2874
3251
|
return {
|
|
2875
|
-
content: [
|
|
3252
|
+
content: [
|
|
3253
|
+
{
|
|
3254
|
+
type: "text",
|
|
3255
|
+
text: `Migration topic "${topic}" not found. Available topics:\n${available}`,
|
|
3256
|
+
},
|
|
3257
|
+
],
|
|
2876
3258
|
};
|
|
2877
3259
|
});
|
|
2878
3260
|
// Tool: get_migration_example
|
|
2879
|
-
server.tool("get_migration_example", `Get a concrete before/after migration example showing how to convert an old theme component to the new code-component system.${migrationExampleNames.length > 0 ? ` Available examples: ${migrationExampleNames.join(", ")}.` : ""} Call with example "list" to see all examples.`, {
|
|
3261
|
+
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.`, {
|
|
3262
|
+
example: z.string().describe("Example name or 'list' to see all examples"),
|
|
3263
|
+
}, async ({ example }) => {
|
|
2880
3264
|
if (example.toLowerCase() === "list") {
|
|
2881
3265
|
if (migrationExampleNames.length === 0) {
|
|
2882
|
-
return {
|
|
3266
|
+
return {
|
|
3267
|
+
content: [
|
|
3268
|
+
{ type: "text", text: "No migration examples available." },
|
|
3269
|
+
],
|
|
3270
|
+
};
|
|
2883
3271
|
}
|
|
2884
|
-
const list = migrationExampleNames
|
|
3272
|
+
const list = migrationExampleNames
|
|
3273
|
+
.map((name) => {
|
|
2885
3274
|
const ex = loadMigrationExample(name);
|
|
2886
|
-
return ex
|
|
2887
|
-
|
|
2888
|
-
|
|
3275
|
+
return ex
|
|
3276
|
+
? `- \`${name}\` — ${ex.title}: ${ex.description}`
|
|
3277
|
+
: `- \`${name}\``;
|
|
3278
|
+
})
|
|
3279
|
+
.join("\n");
|
|
3280
|
+
return {
|
|
3281
|
+
content: [
|
|
3282
|
+
{
|
|
3283
|
+
type: "text",
|
|
3284
|
+
text: `## Available Migration Examples\n\n${list}`,
|
|
3285
|
+
},
|
|
3286
|
+
],
|
|
3287
|
+
};
|
|
2889
3288
|
}
|
|
2890
3289
|
const exampleLower = example.toLowerCase();
|
|
2891
3290
|
let exName = migrationExampleNames.find((n) => n === exampleLower);
|
|
@@ -2895,19 +3294,26 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
|
|
|
2895
3294
|
if (!exName) {
|
|
2896
3295
|
const available = migrationExampleNames.join(", ");
|
|
2897
3296
|
return {
|
|
2898
|
-
content: [
|
|
3297
|
+
content: [
|
|
3298
|
+
{
|
|
3299
|
+
type: "text",
|
|
3300
|
+
text: `Migration example "${example}" not found. Available: ${available}`,
|
|
3301
|
+
},
|
|
3302
|
+
],
|
|
2899
3303
|
};
|
|
2900
3304
|
}
|
|
2901
3305
|
const ex = loadMigrationExample(exName);
|
|
2902
3306
|
if (!ex) {
|
|
2903
|
-
return {
|
|
3307
|
+
return {
|
|
3308
|
+
content: [
|
|
3309
|
+
{
|
|
3310
|
+
type: "text",
|
|
3311
|
+
text: `Failed to load migration example "${exName}".`,
|
|
3312
|
+
},
|
|
3313
|
+
],
|
|
3314
|
+
};
|
|
2904
3315
|
}
|
|
2905
|
-
const parts = [
|
|
2906
|
-
`## ${ex.title}`,
|
|
2907
|
-
"",
|
|
2908
|
-
ex.description,
|
|
2909
|
-
"",
|
|
2910
|
-
];
|
|
3316
|
+
const parts = [`## ${ex.title}`, "", ex.description, ""];
|
|
2911
3317
|
for (const [filename, content] of Object.entries(ex.files)) {
|
|
2912
3318
|
const ext = filename.split(".").pop() || "text";
|
|
2913
3319
|
const lang = ext === "tsx" || ext === "ts"
|
|
@@ -2926,13 +3332,31 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
|
|
|
2926
3332
|
});
|
|
2927
3333
|
// Tool: plan_migration
|
|
2928
3334
|
server.tool("plan_migration", "Generate the **initial** migration plan and (when `project_root` is provided) write it to <project_root>/MIGRATION.md. **This is the only time the MCP writes that file.** From here, you own it: tick checkboxes as you finish work, log custom-data decisions, scan the old source for atomic components (Button, Input, Card, etc.) that theme.json doesn't see, and append them to MIGRATION.md yourself. theme.json is incomplete by design — the MCP can only describe what's listed there. Pass `theme_json_path` for large themes (raw `theme_json` string is supported for backward compat but fails on real-world sizes).", {
|
|
2929
|
-
theme_json: z
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3335
|
+
theme_json: z
|
|
3336
|
+
.string()
|
|
3337
|
+
.optional()
|
|
3338
|
+
.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."),
|
|
3339
|
+
theme_json_path: z
|
|
3340
|
+
.string()
|
|
3341
|
+
.optional()
|
|
3342
|
+
.describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
|
|
3343
|
+
project_name: z
|
|
3344
|
+
.string()
|
|
3345
|
+
.optional()
|
|
3346
|
+
.describe("Target new project name, used to prefix migration-tracking IDs (default: 'my-theme')"),
|
|
3347
|
+
old_source_dir: z
|
|
3348
|
+
.string()
|
|
3349
|
+
.optional()
|
|
3350
|
+
.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."),
|
|
3351
|
+
project_root: z
|
|
3352
|
+
.string()
|
|
3353
|
+
.optional()
|
|
3354
|
+
.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."),
|
|
3355
|
+
overwrite: z
|
|
3356
|
+
.boolean()
|
|
3357
|
+
.optional()
|
|
3358
|
+
.describe("If MIGRATION.md already exists at <project_root>/MIGRATION.md and is non-empty, refuse the write unless this is true. Default: false."),
|
|
3359
|
+
}, async ({ theme_json, theme_json_path, project_name, old_source_dir, project_root, overwrite, }) => {
|
|
2936
3360
|
try {
|
|
2937
3361
|
const parsed = resolveThemeJson(theme_json, theme_json_path);
|
|
2938
3362
|
const projectName = project_name || "my-theme";
|
|
@@ -2993,18 +3417,37 @@ server.tool("plan_migration", "Generate the **initial** migration plan and (when
|
|
|
2993
3417
|
}
|
|
2994
3418
|
catch (err) {
|
|
2995
3419
|
return {
|
|
2996
|
-
content: [
|
|
3420
|
+
content: [
|
|
3421
|
+
{
|
|
3422
|
+
type: "text",
|
|
3423
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
3424
|
+
},
|
|
3425
|
+
],
|
|
2997
3426
|
};
|
|
2998
3427
|
}
|
|
2999
3428
|
});
|
|
3000
3429
|
// Tool: get_section_migration_plan
|
|
3001
3430
|
server.tool("get_section_migration_plan", "Returns concrete CLI commands and prop conversions for one section. For each prop that references a customData type, you'll see a 'Decide: enum or component?' callout — log your decision in MIGRATION.md under `## Custom Data Decisions`. Pass `theme_json_path` for large themes.", {
|
|
3002
|
-
theme_json: z
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3431
|
+
theme_json: z
|
|
3432
|
+
.string()
|
|
3433
|
+
.optional()
|
|
3434
|
+
.describe("Raw JSON content of the old theme.json. EITHER this OR theme_json_path is required (not both)."),
|
|
3435
|
+
theme_json_path: z
|
|
3436
|
+
.string()
|
|
3437
|
+
.optional()
|
|
3438
|
+
.describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
|
|
3439
|
+
section_name: z
|
|
3440
|
+
.string()
|
|
3441
|
+
.describe("Old component name (e.g. 'Navbar', 'ProductGrid') or dir name, OR the new section ID (e.g. 'my-theme-navbar')"),
|
|
3442
|
+
project_name: z
|
|
3443
|
+
.string()
|
|
3444
|
+
.optional()
|
|
3445
|
+
.describe("Target new project name (must match what was used in plan_migration). Default: 'my-theme'"),
|
|
3446
|
+
old_source_dir: z
|
|
3447
|
+
.string()
|
|
3448
|
+
.optional()
|
|
3449
|
+
.describe("Absolute path to old src/ directory (used to output exact source file paths to read)"),
|
|
3450
|
+
}, async ({ theme_json, theme_json_path, section_name, project_name, old_source_dir, }) => {
|
|
3008
3451
|
try {
|
|
3009
3452
|
const parsed = resolveThemeJson(theme_json, theme_json_path);
|
|
3010
3453
|
const projectName = project_name || "my-theme";
|
|
@@ -3013,7 +3456,12 @@ server.tool("get_section_migration_plan", "Returns concrete CLI commands and pro
|
|
|
3013
3456
|
}
|
|
3014
3457
|
catch (err) {
|
|
3015
3458
|
return {
|
|
3016
|
-
content: [
|
|
3459
|
+
content: [
|
|
3460
|
+
{
|
|
3461
|
+
type: "text",
|
|
3462
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
3463
|
+
},
|
|
3464
|
+
],
|
|
3017
3465
|
};
|
|
3018
3466
|
}
|
|
3019
3467
|
});
|
|
@@ -3043,13 +3491,22 @@ function resolveIkasComponentBinary(projectRoot) {
|
|
|
3043
3491
|
async function runIkasComponentCli(projectRoot, args) {
|
|
3044
3492
|
const bin = resolveIkasComponentBinary(projectRoot);
|
|
3045
3493
|
return new Promise((resolve) => {
|
|
3046
|
-
execFile(bin, args,
|
|
3494
|
+
execFile(bin, args,
|
|
3495
|
+
// Large editor responses (e.g. list_categories on a big store, or
|
|
3496
|
+
// add_sections_to_page echoing many sections) easily exceed Node's default
|
|
3497
|
+
// 1MB stdout buffer, which truncates output mid-JSON and yields a
|
|
3498
|
+
// "no parseable JSON" error. Give it generous headroom.
|
|
3499
|
+
{ cwd: projectRoot, windowsHide: true, maxBuffer: 64 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
3047
3500
|
const exitCode = err && typeof err.code === "number"
|
|
3048
3501
|
? err.code
|
|
3049
3502
|
: err
|
|
3050
3503
|
? 1
|
|
3051
3504
|
: 0;
|
|
3052
|
-
resolve({
|
|
3505
|
+
resolve({
|
|
3506
|
+
stdout: stdout?.toString() ?? "",
|
|
3507
|
+
stderr: stderr?.toString() ?? "",
|
|
3508
|
+
exitCode,
|
|
3509
|
+
});
|
|
3053
3510
|
});
|
|
3054
3511
|
});
|
|
3055
3512
|
}
|
|
@@ -3057,24 +3514,47 @@ function parseCliJson(stdout) {
|
|
|
3057
3514
|
const trimmed = stdout.trim();
|
|
3058
3515
|
if (!trimmed)
|
|
3059
3516
|
return null;
|
|
3060
|
-
// CLI prints exactly one JSON object on stdout
|
|
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;
|
|
3517
|
+
// Fast path: the CLI prints exactly one JSON object on stdout.
|
|
3065
3518
|
try {
|
|
3066
|
-
return JSON.parse(
|
|
3519
|
+
return JSON.parse(trimmed);
|
|
3067
3520
|
}
|
|
3068
3521
|
catch {
|
|
3522
|
+
// Fallback: if anything leaked onto stdout before/after the JSON, scan the
|
|
3523
|
+
// lines and return the last one that parses as JSON.
|
|
3524
|
+
const lines = trimmed
|
|
3525
|
+
.split("\n")
|
|
3526
|
+
.map((l) => l.trim())
|
|
3527
|
+
.filter(Boolean);
|
|
3528
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3529
|
+
try {
|
|
3530
|
+
return JSON.parse(lines[i]);
|
|
3531
|
+
}
|
|
3532
|
+
catch {
|
|
3533
|
+
// keep scanning
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3069
3536
|
return null;
|
|
3070
3537
|
}
|
|
3071
3538
|
}
|
|
3539
|
+
// Coerce a JSON-ARRAY argument (points / settings / colors) into the single JSON-array string the
|
|
3540
|
+
// CLI expects. Agents sometimes send these PRE-STRINGIFIED (a JSON string instead of a real array);
|
|
3541
|
+
// a blind JSON.stringify would then double-encode it ("[...]" -> "\"[...]\"") and the CLI rejects it
|
|
3542
|
+
// with "must be a JSON array". So pass a string through as-is (the CLI parses + validates it) and
|
|
3543
|
+
// only stringify real arrays/values. NOTE: only for array args — a scalar `value` arg must NOT use
|
|
3544
|
+
// this (a plain string there is a literal value, not stringified JSON).
|
|
3545
|
+
function toJsonArrayArg(value) {
|
|
3546
|
+
return typeof value === "string" ? value : JSON.stringify(value ?? []);
|
|
3547
|
+
}
|
|
3072
3548
|
async function callEditorAction(projectRoot, args) {
|
|
3073
3549
|
try {
|
|
3074
3550
|
const { stdout, stderr, exitCode } = await runIkasComponentCli(projectRoot, args);
|
|
3075
3551
|
const parsed = parseCliJson(stdout);
|
|
3076
3552
|
if (parsed) {
|
|
3077
|
-
return {
|
|
3553
|
+
return {
|
|
3554
|
+
content: [
|
|
3555
|
+
{ type: "text", text: JSON.stringify(parsed, null, 2) },
|
|
3556
|
+
],
|
|
3557
|
+
};
|
|
3078
3558
|
}
|
|
3079
3559
|
return {
|
|
3080
3560
|
content: [
|
|
@@ -3097,38 +3577,77 @@ async function callEditorAction(projectRoot, args) {
|
|
|
3097
3577
|
};
|
|
3098
3578
|
}
|
|
3099
3579
|
}
|
|
3580
|
+
//
|
|
3100
3581
|
// Tool: list_editor_pages
|
|
3101
3582
|
server.tool("list_editor_pages", "List pages in the connected editor's project. Returns id, name, pageType, slug for each page. Requires `ikas-component dev` to be running with the editor connected. Use the returned `id`s as the `pageId` argument to `add_section_to_page`.", {
|
|
3102
|
-
project_root: z
|
|
3103
|
-
|
|
3583
|
+
project_root: z
|
|
3584
|
+
.string()
|
|
3585
|
+
.describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
|
|
3586
|
+
port: z
|
|
3587
|
+
.number()
|
|
3588
|
+
.optional()
|
|
3589
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3104
3590
|
}, async ({ project_root, port }) => {
|
|
3105
3591
|
const args = ["list-pages", ...(port ? ["--port", String(port)] : [])];
|
|
3106
3592
|
return callEditorAction(project_root, args);
|
|
3107
3593
|
});
|
|
3108
3594
|
// Tool: list_imported_sections
|
|
3109
3595
|
server.tool("list_imported_sections", "List section-type code components already imported into the editor's project (theme.codeComponents, filtered to type=section). Use to confirm a component is ready to be placed on a page. The component must be built (`ikas-component build`/`dev`) and imported (`import_section`) before it can be added to a page. The returned `id` is the editor's id for the imported component — normally identical to the id in `ikas.config.json`, but it can differ if the dev component was deleted and re-scaffolded after a previous import. Always use the `id` from this tool, not from `ikas.config.json`, when calling `add_section_to_page`.", {
|
|
3110
|
-
project_root: z
|
|
3111
|
-
|
|
3596
|
+
project_root: z
|
|
3597
|
+
.string()
|
|
3598
|
+
.describe("Absolute path to the code-component project."),
|
|
3599
|
+
port: z
|
|
3600
|
+
.number()
|
|
3601
|
+
.optional()
|
|
3602
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3112
3603
|
}, async ({ project_root, port }) => {
|
|
3113
|
-
const args = [
|
|
3604
|
+
const args = [
|
|
3605
|
+
"list-imported",
|
|
3606
|
+
"--sections-only",
|
|
3607
|
+
...(port ? ["--port", String(port)] : []),
|
|
3608
|
+
];
|
|
3114
3609
|
return callEditorAction(project_root, args);
|
|
3115
3610
|
});
|
|
3116
3611
|
// Tool: import_section
|
|
3117
3612
|
server.tool("import_section", "Import a built section-type code component into the editor's project. This copies the compiled JS/CSS/props into `theme.codeComponents` and auto-creates the wrapper section needed to place it on a page. Idempotent: re-running updates the existing entry in place. Required before `add_section_to_page`. The component must have been built (`ikas-component build`/`dev`).", {
|
|
3118
|
-
project_root: z
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3613
|
+
project_root: z
|
|
3614
|
+
.string()
|
|
3615
|
+
.describe("Absolute path to the code-component project."),
|
|
3616
|
+
component_id: z
|
|
3617
|
+
.string()
|
|
3618
|
+
.describe("Component id from `ikas.config.json` (strict — no name resolution)."),
|
|
3619
|
+
port: z
|
|
3620
|
+
.number()
|
|
3621
|
+
.optional()
|
|
3622
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3623
|
+
}, async ({ project_root, component_id, port }) => {
|
|
3624
|
+
const args = [
|
|
3625
|
+
"import",
|
|
3626
|
+
"--id",
|
|
3627
|
+
component_id,
|
|
3628
|
+
...(port ? ["--port", String(port)] : []),
|
|
3629
|
+
];
|
|
3123
3630
|
return callEditorAction(project_root, args);
|
|
3124
3631
|
});
|
|
3125
3632
|
// Tool: add_section_to_page
|
|
3126
|
-
server.tool("add_section_to_page", "Place
|
|
3127
|
-
project_root: z
|
|
3128
|
-
|
|
3633
|
+
server.tool("add_section_to_page", "Place a SINGLE already-imported section-type code component on a page. Use this ONLY when adding exactly one section. If you are adding more than one section — or building/populating a page — call `add_sections_to_page` instead: it places them all AND fills their props in ONE call. Calling this tool repeatedly (one section per call) is the slow path and should be avoided. Equivalent to right-clicking the section in the dev-components panel and choosing \"Add to Page\". Errors if the component is not imported, is not section-type, or the page id is unknown. Use `list_editor_pages` to discover page ids and `list_imported_sections` to discover component ids. After placing, change the section's prop values with `update_section_prop` / `update_page_sections` (use `list_page_sections` to get the placement's `elementId` and prop names).", {
|
|
3634
|
+
project_root: z
|
|
3635
|
+
.string()
|
|
3636
|
+
.describe("Absolute path to the code-component project."),
|
|
3637
|
+
component_id: z
|
|
3638
|
+
.string()
|
|
3639
|
+
.describe("Imported code component id from `list_imported_sections` — NOT the id in `ikas.config.json` (the two are usually the same but can diverge after a rescaffold)."),
|
|
3129
3640
|
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3130
|
-
index: z
|
|
3131
|
-
|
|
3641
|
+
index: z
|
|
3642
|
+
.number()
|
|
3643
|
+
.int()
|
|
3644
|
+
.nonnegative()
|
|
3645
|
+
.optional()
|
|
3646
|
+
.describe("Zero-based insertion index in the page; appends when omitted."),
|
|
3647
|
+
port: z
|
|
3648
|
+
.number()
|
|
3649
|
+
.optional()
|
|
3650
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3132
3651
|
}, async ({ project_root, component_id, page_id, index, port }) => {
|
|
3133
3652
|
const args = [
|
|
3134
3653
|
"add-to-page",
|
|
@@ -3141,6 +3660,1130 @@ server.tool("add_section_to_page", "Place an already-imported section-type code
|
|
|
3141
3660
|
];
|
|
3142
3661
|
return callEditorAction(project_root, args);
|
|
3143
3662
|
});
|
|
3663
|
+
// Tool: add_sections_to_page
|
|
3664
|
+
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.", {
|
|
3665
|
+
project_root: z
|
|
3666
|
+
.string()
|
|
3667
|
+
.describe("Absolute path to the code-component project."),
|
|
3668
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3669
|
+
sections: z
|
|
3670
|
+
.array(z.object({
|
|
3671
|
+
component_id: z
|
|
3672
|
+
.string()
|
|
3673
|
+
.describe("Imported (or built) section-type code component id."),
|
|
3674
|
+
index: z
|
|
3675
|
+
.number()
|
|
3676
|
+
.int()
|
|
3677
|
+
.nonnegative()
|
|
3678
|
+
.optional()
|
|
3679
|
+
.describe("Zero-based insertion index; appends when omitted."),
|
|
3680
|
+
updates: z
|
|
3681
|
+
.array(z.object({
|
|
3682
|
+
prop_id: z
|
|
3683
|
+
.string()
|
|
3684
|
+
.optional()
|
|
3685
|
+
.describe("Blueprint prop id (provide this or prop_name)."),
|
|
3686
|
+
prop_name: z
|
|
3687
|
+
.string()
|
|
3688
|
+
.optional()
|
|
3689
|
+
.describe("Blueprint prop name (alternative to prop_id)."),
|
|
3690
|
+
value: z
|
|
3691
|
+
.any()
|
|
3692
|
+
.describe("Prop value, same shape update_section_prop expects for that type."),
|
|
3693
|
+
}))
|
|
3694
|
+
.optional()
|
|
3695
|
+
.describe("Optional prop values to set on this section right after placing it."),
|
|
3696
|
+
}))
|
|
3697
|
+
.describe("Non-empty array of sections to place (in order), each with its componentId and optional prop updates."),
|
|
3698
|
+
port: z
|
|
3699
|
+
.number()
|
|
3700
|
+
.optional()
|
|
3701
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3702
|
+
}, async ({ project_root, page_id, sections, port }) => {
|
|
3703
|
+
const normalized = (sections || []).map((s) => ({
|
|
3704
|
+
componentId: s.component_id,
|
|
3705
|
+
...(typeof s.index === "number" ? { index: s.index } : {}),
|
|
3706
|
+
...(s.updates
|
|
3707
|
+
? {
|
|
3708
|
+
updates: s.updates.map((u) => ({
|
|
3709
|
+
...(u.prop_id ? { propId: u.prop_id } : {}),
|
|
3710
|
+
...(u.prop_name ? { propName: u.prop_name } : {}),
|
|
3711
|
+
value: u.value,
|
|
3712
|
+
})),
|
|
3713
|
+
}
|
|
3714
|
+
: {}),
|
|
3715
|
+
}));
|
|
3716
|
+
const args = [
|
|
3717
|
+
"add-sections-to-page",
|
|
3718
|
+
"--page-id",
|
|
3719
|
+
page_id,
|
|
3720
|
+
"--sections",
|
|
3721
|
+
JSON.stringify(normalized),
|
|
3722
|
+
...(port ? ["--port", String(port)] : []),
|
|
3723
|
+
];
|
|
3724
|
+
return callEditorAction(project_root, args);
|
|
3725
|
+
});
|
|
3726
|
+
// Tool: list_page_sections
|
|
3727
|
+
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.", {
|
|
3728
|
+
project_root: z
|
|
3729
|
+
.string()
|
|
3730
|
+
.describe("Absolute path to the code-component project."),
|
|
3731
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3732
|
+
port: z
|
|
3733
|
+
.number()
|
|
3734
|
+
.optional()
|
|
3735
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3736
|
+
}, async ({ project_root, page_id, port }) => {
|
|
3737
|
+
const args = [
|
|
3738
|
+
"list-page-sections",
|
|
3739
|
+
"--page-id",
|
|
3740
|
+
page_id,
|
|
3741
|
+
...(port ? ["--port", String(port)] : []),
|
|
3742
|
+
];
|
|
3743
|
+
return callEditorAction(project_root, args);
|
|
3744
|
+
});
|
|
3745
|
+
// Tool: get_component_props
|
|
3746
|
+
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).", {
|
|
3747
|
+
project_root: z
|
|
3748
|
+
.string()
|
|
3749
|
+
.describe("Absolute path to the code-component project."),
|
|
3750
|
+
component_ids: z
|
|
3751
|
+
.array(z.string())
|
|
3752
|
+
.describe("Section/child code-component ids to resolve (batch). From list_imported_sections or a COMPONENT_LIST prop's allowedComponentIds."),
|
|
3753
|
+
port: z
|
|
3754
|
+
.number()
|
|
3755
|
+
.optional()
|
|
3756
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3757
|
+
}, async ({ project_root, component_ids, port }) => {
|
|
3758
|
+
const args = [
|
|
3759
|
+
"get-component-props",
|
|
3760
|
+
"--component-ids",
|
|
3761
|
+
(component_ids || []).join(","),
|
|
3762
|
+
...(port ? ["--port", String(port)] : []),
|
|
3763
|
+
];
|
|
3764
|
+
return callEditorAction(project_root, args);
|
|
3765
|
+
});
|
|
3766
|
+
// Tool: get_section_values
|
|
3767
|
+
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).", {
|
|
3768
|
+
project_root: z
|
|
3769
|
+
.string()
|
|
3770
|
+
.describe("Absolute path to the code-component project."),
|
|
3771
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3772
|
+
element_ids: z
|
|
3773
|
+
.array(z.string())
|
|
3774
|
+
.describe("Placed-section elementIds to read (batch). From `list_page_sections`."),
|
|
3775
|
+
port: z
|
|
3776
|
+
.number()
|
|
3777
|
+
.optional()
|
|
3778
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3779
|
+
}, async ({ project_root, page_id, element_ids, port }) => {
|
|
3780
|
+
const args = [
|
|
3781
|
+
"get-section-values",
|
|
3782
|
+
"--page-id",
|
|
3783
|
+
page_id,
|
|
3784
|
+
"--element-ids",
|
|
3785
|
+
(element_ids || []).join(","),
|
|
3786
|
+
...(port ? ["--port", String(port)] : []),
|
|
3787
|
+
];
|
|
3788
|
+
return callEditorAction(project_root, args);
|
|
3789
|
+
});
|
|
3790
|
+
// Tool: update_section_prop
|
|
3791
|
+
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). A COLOR prop can ALSO bind to a global theme color instead of a literal hex — pass `{ "patternValueId": "<id>" }` (the color id from `list_theme_globals` → `colors[].id`) and the prop tracks that global color live (resolved to `var(--<id>)` at render); the id is validated and an unknown one is REJECTED. 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>", ... ] }`; TYPE (style props such as BorderRadiusStyleType/PaddingStyleType/GapStyleType) = the type-shaped object ITSELF, e.g. `{ "value": 8, "unit": "px" }` — get the exact field structure from the prop\'s `writeExample`/`typeName` in `get_component_props` or `get_type_definition("<typeName>")`; unknown fields, wrong field types, and invalid union values (e.g. `"unit": "banana"`) are REJECTED; 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": "<id from list_categories>" }` (the field is `category`, NOT `categoryId` — `categoryId` is only the standalone CATEGORY entity prop; no productIds here) or `"ALL"` (no ids). Unknown sub-fields inside a PRODUCT_LIST (e.g. `categoryId`) are REJECTED, not silently stored. 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' +
|
|
3792
|
+
'BIND TO A GLOBAL VARIABLE (Theme Setting): instead of a static `value`, pass `bind_global` = the global\'s `name` (its `variableName`, the `_<id>` form) from `list_theme_globals`. The prop becomes a LIVE dynamic value that always reflects that global\'s current value — editing the global in Theme Settings updates every bound prop. Use this to wire content to a project-wide setting (e.g. a shared announcement text, brand color, or logo image) rather than duplicating a literal. Pick a global whose type matches the prop (TEXT↔TEXT/RICH_TEXT, NUMBER↔NUMBER, BOOLEAN↔BOOLEAN, IMAGE↔IMAGE, COLOR↔COLOR, …). Provide `value` OR `bind_global`, not both. To bind a prop of a COMPONENT_LIST CHILD, also pass `list_path` (the `{ propId, index }` steps down to the child) and set `prop_id` to the child\'s prop id. Works for every section type, including built-in / code-component sections that have no blueprint.\n\n' +
|
|
3793
|
+
'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' +
|
|
3794
|
+
'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): ' +
|
|
3795
|
+
`{ "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" } } } ] } } } ] }`, {
|
|
3796
|
+
project_root: z
|
|
3797
|
+
.string()
|
|
3798
|
+
.describe("Absolute path to the code-component project."),
|
|
3799
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3800
|
+
element_id: z
|
|
3801
|
+
.string()
|
|
3802
|
+
.describe("Placed-section elementId identifying THIS placement on the page (from `list_page_sections`)."),
|
|
3803
|
+
prop_id: z
|
|
3804
|
+
.string()
|
|
3805
|
+
.optional()
|
|
3806
|
+
.describe("Blueprint prop id to update (from `list_page_sections`). Provide this or `prop_name`."),
|
|
3807
|
+
prop_name: z
|
|
3808
|
+
.string()
|
|
3809
|
+
.optional()
|
|
3810
|
+
.describe("Blueprint prop name to update (alternative to `prop_id`)."),
|
|
3811
|
+
value: z
|
|
3812
|
+
.any()
|
|
3813
|
+
.optional()
|
|
3814
|
+
.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. Omit when using `bind_global`.'),
|
|
3815
|
+
bind_global: z
|
|
3816
|
+
.string()
|
|
3817
|
+
.optional()
|
|
3818
|
+
.describe("Bind this prop to a project GLOBAL variable (Theme Setting) as a LIVE dynamic value instead of writing a static `value` — the prop then always reflects that global's current value (edited in Theme Settings). Pass the global's `name` (its `variableName`, the `_<id>` form) from `list_theme_globals`. Provide `value` OR `bind_global`, not both. Bindable globals include TEXT / RICH_TEXT / NUMBER / BOOLEAN / IMAGE / COLOR / BORDER / SHADOW settings; pick a global whose type matches the prop. (This is the general theme-global binding; a COLOR prop can alternatively take an inline `{ \"patternValueId\" }` for theme colors.)"),
|
|
3819
|
+
list_path: z
|
|
3820
|
+
.array(z.object({ propId: z.string(), index: z.number() }))
|
|
3821
|
+
.optional()
|
|
3822
|
+
.describe("Only with `bind_global`: bind a prop of a COMPONENT_LIST child instead of a section-level prop. An array of `{ propId, index }` steps from the section down to the target child; `prop_id` is then the CHILD's prop id (from `get_component_props`)."),
|
|
3823
|
+
port: z
|
|
3824
|
+
.number()
|
|
3825
|
+
.optional()
|
|
3826
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3827
|
+
}, async ({ project_root, page_id, element_id, prop_id, prop_name, value, bind_global, list_path, port, }) => {
|
|
3828
|
+
const args = [
|
|
3829
|
+
"update-section-prop",
|
|
3830
|
+
"--page-id",
|
|
3831
|
+
page_id,
|
|
3832
|
+
"--element-id",
|
|
3833
|
+
element_id,
|
|
3834
|
+
...(prop_id ? ["--prop-id", prop_id] : []),
|
|
3835
|
+
...(prop_name ? ["--prop-name", prop_name] : []),
|
|
3836
|
+
...(bind_global
|
|
3837
|
+
? ["--bind-global", bind_global, ...(list_path ? ["--list-path", JSON.stringify(list_path)] : [])]
|
|
3838
|
+
: [
|
|
3839
|
+
"--value",
|
|
3840
|
+
// The CLI JSON.parses --value. If `value` is already a JSON string, pass
|
|
3841
|
+
// it through verbatim — re-stringifying it would double-encode it and the
|
|
3842
|
+
// editor would store a string instead of the object/array.
|
|
3843
|
+
typeof value === "string" ? value : JSON.stringify(value),
|
|
3844
|
+
]),
|
|
3845
|
+
...(port ? ["--port", String(port)] : []),
|
|
3846
|
+
];
|
|
3847
|
+
return callEditorAction(project_root, args);
|
|
3848
|
+
});
|
|
3849
|
+
// Tool: upload_image
|
|
3850
|
+
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`. This is the ONLY way to obtain a real image id — an id cannot be invented. Use the returned `id`: (1) as an IMAGE prop value for `update_section_prop` (e.g. `{ "id": "<returned-id>", "altText": "...", "isVideo": false }`), or (2) as an IMAGE theme global\'s value for `create_theme_global`/`update_theme_global` (e.g. `{ "id": "<returned-id>" }`). An id cannot be invented — an unreal one is stored as given and renders as a broken image, so always use a `upload_image` id or one already listed by `list_theme_globals`. Neither tool uploads; both only reference 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).', {
|
|
3851
|
+
project_root: z
|
|
3852
|
+
.string()
|
|
3853
|
+
.describe("Absolute path to the code-component project."),
|
|
3854
|
+
file_path: z
|
|
3855
|
+
.string()
|
|
3856
|
+
.optional()
|
|
3857
|
+
.describe("Local image file path (.png, .jpg, .jpeg, .webp, .gif). Provide this or `image_url`."),
|
|
3858
|
+
image_url: z
|
|
3859
|
+
.string()
|
|
3860
|
+
.optional()
|
|
3861
|
+
.describe("Image URL to fetch and upload (alternative to `file_path`)."),
|
|
3862
|
+
alt_text: z
|
|
3863
|
+
.string()
|
|
3864
|
+
.optional()
|
|
3865
|
+
.describe("Alt text to store with the image."),
|
|
3866
|
+
port: z
|
|
3867
|
+
.number()
|
|
3868
|
+
.optional()
|
|
3869
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3870
|
+
}, async ({ project_root, file_path, image_url, alt_text, port }) => {
|
|
3871
|
+
const args = [
|
|
3872
|
+
"upload-image",
|
|
3873
|
+
...(file_path ? ["--file", file_path] : []),
|
|
3874
|
+
...(image_url ? ["--url", image_url] : []),
|
|
3875
|
+
...(alt_text ? ["--alt", alt_text] : []),
|
|
3876
|
+
...(port ? ["--port", String(port)] : []),
|
|
3877
|
+
];
|
|
3878
|
+
return callEditorAction(project_root, args);
|
|
3879
|
+
});
|
|
3880
|
+
// Tool: update_page_sections
|
|
3881
|
+
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. An update item may instead bind a prop to a project GLOBAL variable (Theme Setting) as a live dynamic value: use `{ prop_id|prop_name, bind_global }` where `bind_global` is the global's `name` (its `variableName`) from `list_theme_globals` — omit `value`. Add `list_path` (an array of `{ propId, index }` steps) to bind a prop of a COMPONENT_LIST child (then `prop_id` is the child's prop id). Static value writes are applied first, then bindings. Prefer this over many update_section_prop(s) calls.", {
|
|
3882
|
+
project_root: z
|
|
3883
|
+
.string()
|
|
3884
|
+
.describe("Absolute path to the code-component project."),
|
|
3885
|
+
page_id: z.string().describe("Target page id (from list_editor_pages)."),
|
|
3886
|
+
sections: z
|
|
3887
|
+
.array(z.object({
|
|
3888
|
+
element_id: z
|
|
3889
|
+
.string()
|
|
3890
|
+
.describe("Placed-section elementId (from list_page_sections)."),
|
|
3891
|
+
updates: z
|
|
3892
|
+
.array(z.object({
|
|
3893
|
+
prop_id: z
|
|
3894
|
+
.string()
|
|
3895
|
+
.optional()
|
|
3896
|
+
.describe("Blueprint prop id (provide this or prop_name)."),
|
|
3897
|
+
prop_name: z
|
|
3898
|
+
.string()
|
|
3899
|
+
.optional()
|
|
3900
|
+
.describe("Blueprint prop name (alternative to prop_id)."),
|
|
3901
|
+
value: z
|
|
3902
|
+
.any()
|
|
3903
|
+
.optional()
|
|
3904
|
+
.describe("Prop value, same shape update_section_prop expects for that type. Omit when using bind_global."),
|
|
3905
|
+
bind_global: z
|
|
3906
|
+
.string()
|
|
3907
|
+
.optional()
|
|
3908
|
+
.describe("Bind this prop to a project GLOBAL variable (Theme Setting) as a live dynamic value instead of a static value. The global's `name` (variableName) from list_theme_globals. Provide value OR bind_global."),
|
|
3909
|
+
list_path: z
|
|
3910
|
+
.array(z.object({ propId: z.string(), index: z.number() }))
|
|
3911
|
+
.optional()
|
|
3912
|
+
.describe("Only with bind_global: `{ propId, index }` steps down to a COMPONENT_LIST child; prop_id is then the child's prop id."),
|
|
3913
|
+
}))
|
|
3914
|
+
.describe("Non-empty array of prop updates for this section."),
|
|
3915
|
+
}))
|
|
3916
|
+
.describe("Non-empty array of sections to fill, each with its elementId and prop updates."),
|
|
3917
|
+
port: z
|
|
3918
|
+
.number()
|
|
3919
|
+
.optional()
|
|
3920
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3921
|
+
}, async ({ project_root, page_id, sections, port }) => {
|
|
3922
|
+
const normalized = (sections || []).map((s) => ({
|
|
3923
|
+
elementId: s.element_id,
|
|
3924
|
+
updates: (s.updates || []).map((u) => ({
|
|
3925
|
+
...(u.prop_id ? { propId: u.prop_id } : {}),
|
|
3926
|
+
...(u.prop_name ? { propName: u.prop_name } : {}),
|
|
3927
|
+
...(u.bind_global
|
|
3928
|
+
? { bindGlobal: u.bind_global, ...(u.list_path ? { listPath: u.list_path } : {}) }
|
|
3929
|
+
: { value: u.value }),
|
|
3930
|
+
})),
|
|
3931
|
+
}));
|
|
3932
|
+
const args = [
|
|
3933
|
+
"update-page-sections",
|
|
3934
|
+
"--page-id",
|
|
3935
|
+
page_id,
|
|
3936
|
+
"--sections",
|
|
3937
|
+
JSON.stringify(normalized),
|
|
3938
|
+
...(port ? ["--port", String(port)] : []),
|
|
3939
|
+
];
|
|
3940
|
+
return callEditorAction(project_root, args);
|
|
3941
|
+
});
|
|
3942
|
+
// Tool: upload_images
|
|
3943
|
+
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.", {
|
|
3944
|
+
project_root: z
|
|
3945
|
+
.string()
|
|
3946
|
+
.describe("Absolute path to the code-component project."),
|
|
3947
|
+
images: z
|
|
3948
|
+
.array(z.object({
|
|
3949
|
+
file_path: z
|
|
3950
|
+
.string()
|
|
3951
|
+
.optional()
|
|
3952
|
+
.describe("Local image file path (provide this or image_url)."),
|
|
3953
|
+
image_url: z
|
|
3954
|
+
.string()
|
|
3955
|
+
.optional()
|
|
3956
|
+
.describe("Image URL to fetch (alternative to file_path)."),
|
|
3957
|
+
alt_text: z.string().optional().describe("Alt text for this image."),
|
|
3958
|
+
}))
|
|
3959
|
+
.describe("Non-empty array of images to upload."),
|
|
3960
|
+
port: z
|
|
3961
|
+
.number()
|
|
3962
|
+
.optional()
|
|
3963
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3964
|
+
}, async ({ project_root, images, port }) => {
|
|
3965
|
+
const manifest = (images || []).map((im) => ({
|
|
3966
|
+
...(im.file_path ? { file: im.file_path } : {}),
|
|
3967
|
+
...(im.image_url ? { url: im.image_url } : {}),
|
|
3968
|
+
...(im.alt_text ? { alt: im.alt_text } : {}),
|
|
3969
|
+
}));
|
|
3970
|
+
const args = [
|
|
3971
|
+
"upload-images",
|
|
3972
|
+
"--manifest",
|
|
3973
|
+
JSON.stringify(manifest),
|
|
3974
|
+
...(port ? ["--port", String(port)] : []),
|
|
3975
|
+
];
|
|
3976
|
+
return callEditorAction(project_root, args);
|
|
3977
|
+
});
|
|
3978
|
+
// Tool: search_products
|
|
3979
|
+
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).', {
|
|
3980
|
+
project_root: z
|
|
3981
|
+
.string()
|
|
3982
|
+
.describe("Absolute path to the code-component project."),
|
|
3983
|
+
query: z
|
|
3984
|
+
.string()
|
|
3985
|
+
.optional()
|
|
3986
|
+
.describe("Free-text product search (name, sku, barcode)."),
|
|
3987
|
+
product_ids: z
|
|
3988
|
+
.array(z.string())
|
|
3989
|
+
.optional()
|
|
3990
|
+
.describe("Specific product ids to resolve directly (alternative to `query`)."),
|
|
3991
|
+
per_page: z.number().optional().describe("Results per page."),
|
|
3992
|
+
port: z
|
|
3993
|
+
.number()
|
|
3994
|
+
.optional()
|
|
3995
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3996
|
+
}, async ({ project_root, query, product_ids, per_page, port }) => {
|
|
3997
|
+
const args = [
|
|
3998
|
+
"search-products",
|
|
3999
|
+
...(query ? ["--query", query] : []),
|
|
4000
|
+
...(product_ids && product_ids.length
|
|
4001
|
+
? ["--ids", product_ids.join(",")]
|
|
4002
|
+
: []),
|
|
4003
|
+
...(typeof per_page === "number" ? ["--per-page", String(per_page)] : []),
|
|
4004
|
+
...(port ? ["--port", String(port)] : []),
|
|
4005
|
+
];
|
|
4006
|
+
return callEditorAction(project_root, args);
|
|
4007
|
+
});
|
|
4008
|
+
// Tool: list_categories
|
|
4009
|
+
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.', {
|
|
4010
|
+
project_root: z
|
|
4011
|
+
.string()
|
|
4012
|
+
.describe("Absolute path to the code-component project."),
|
|
4013
|
+
port: z
|
|
4014
|
+
.number()
|
|
4015
|
+
.optional()
|
|
4016
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4017
|
+
}, async ({ project_root, port }) => {
|
|
4018
|
+
const args = [
|
|
4019
|
+
"list-entities",
|
|
4020
|
+
"--kind",
|
|
4021
|
+
"category",
|
|
4022
|
+
...(port ? ["--port", String(port)] : []),
|
|
4023
|
+
];
|
|
4024
|
+
return callEditorAction(project_root, args);
|
|
4025
|
+
});
|
|
4026
|
+
// Tool: list_brands
|
|
4027
|
+
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.', {
|
|
4028
|
+
project_root: z
|
|
4029
|
+
.string()
|
|
4030
|
+
.describe("Absolute path to the code-component project."),
|
|
4031
|
+
port: z
|
|
4032
|
+
.number()
|
|
4033
|
+
.optional()
|
|
4034
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4035
|
+
}, async ({ project_root, port }) => {
|
|
4036
|
+
const args = [
|
|
4037
|
+
"list-entities",
|
|
4038
|
+
"--kind",
|
|
4039
|
+
"brand",
|
|
4040
|
+
...(port ? ["--port", String(port)] : []),
|
|
4041
|
+
];
|
|
4042
|
+
return callEditorAction(project_root, args);
|
|
4043
|
+
});
|
|
4044
|
+
// Tool: list_blogs
|
|
4045
|
+
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.', {
|
|
4046
|
+
project_root: z
|
|
4047
|
+
.string()
|
|
4048
|
+
.describe("Absolute path to the code-component project."),
|
|
4049
|
+
query: z.string().optional().describe("Free-text blog search."),
|
|
4050
|
+
port: z
|
|
4051
|
+
.number()
|
|
4052
|
+
.optional()
|
|
4053
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4054
|
+
}, async ({ project_root, query, port }) => {
|
|
4055
|
+
const args = [
|
|
4056
|
+
"list-entities",
|
|
4057
|
+
"--kind",
|
|
4058
|
+
"blog",
|
|
4059
|
+
...(query ? ["--query", query] : []),
|
|
4060
|
+
...(port ? ["--port", String(port)] : []),
|
|
4061
|
+
];
|
|
4062
|
+
return callEditorAction(project_root, args);
|
|
4063
|
+
});
|
|
4064
|
+
// Tool: list_blog_categories
|
|
4065
|
+
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.', {
|
|
4066
|
+
project_root: z
|
|
4067
|
+
.string()
|
|
4068
|
+
.describe("Absolute path to the code-component project."),
|
|
4069
|
+
query: z.string().optional().describe("Free-text blog-category search."),
|
|
4070
|
+
port: z
|
|
4071
|
+
.number()
|
|
4072
|
+
.optional()
|
|
4073
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4074
|
+
}, async ({ project_root, query, port }) => {
|
|
4075
|
+
const args = [
|
|
4076
|
+
"list-entities",
|
|
4077
|
+
"--kind",
|
|
4078
|
+
"blog-category",
|
|
4079
|
+
...(query ? ["--query", query] : []),
|
|
4080
|
+
...(port ? ["--port", String(port)] : []),
|
|
4081
|
+
];
|
|
4082
|
+
return callEditorAction(project_root, args);
|
|
4083
|
+
});
|
|
4084
|
+
// Tool: get_page_by_type
|
|
4085
|
+
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).', {
|
|
4086
|
+
project_root: z
|
|
4087
|
+
.string()
|
|
4088
|
+
.describe("Absolute path to the code-component project."),
|
|
4089
|
+
page_type: z
|
|
4090
|
+
.string()
|
|
4091
|
+
.describe("Page type to resolve, e.g. CATEGORY, PRODUCT, BRAND, BLOG, BLOG_CATEGORY, INDEX."),
|
|
4092
|
+
port: z
|
|
4093
|
+
.number()
|
|
4094
|
+
.optional()
|
|
4095
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4096
|
+
}, async ({ project_root, page_type, port }) => {
|
|
4097
|
+
const args = [
|
|
4098
|
+
"get-page-by-type",
|
|
4099
|
+
"--page-type",
|
|
4100
|
+
page_type,
|
|
4101
|
+
...(port ? ["--port", String(port)] : []),
|
|
4102
|
+
];
|
|
4103
|
+
return callEditorAction(project_root, args);
|
|
4104
|
+
});
|
|
4105
|
+
// Tool: create_page
|
|
4106
|
+
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.', {
|
|
4107
|
+
project_root: z
|
|
4108
|
+
.string()
|
|
4109
|
+
.describe("Absolute path to the code-component project."),
|
|
4110
|
+
page_type: z
|
|
4111
|
+
.string()
|
|
4112
|
+
.describe("Page type to create, e.g. CATEGORY, PRODUCT, BRAND, BLOG, BLOG_CATEGORY, INDEX, CUSTOM."),
|
|
4113
|
+
name: z
|
|
4114
|
+
.string()
|
|
4115
|
+
.optional()
|
|
4116
|
+
.describe("Page name. Required for CUSTOM pages; ignored otherwise."),
|
|
4117
|
+
slug: z
|
|
4118
|
+
.string()
|
|
4119
|
+
.optional()
|
|
4120
|
+
.describe("Page slug. Required (and unique) for CUSTOM pages; ignored otherwise."),
|
|
4121
|
+
port: z
|
|
4122
|
+
.number()
|
|
4123
|
+
.optional()
|
|
4124
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4125
|
+
}, async ({ project_root, page_type, name, slug, port }) => {
|
|
4126
|
+
const args = [
|
|
4127
|
+
"create-page",
|
|
4128
|
+
"--page-type",
|
|
4129
|
+
page_type,
|
|
4130
|
+
...(name ? ["--name", name] : []),
|
|
4131
|
+
...(slug ? ["--slug", slug] : []),
|
|
4132
|
+
...(port ? ["--port", String(port)] : []),
|
|
4133
|
+
];
|
|
4134
|
+
return callEditorAction(project_root, args);
|
|
4135
|
+
});
|
|
4136
|
+
// Tool: publish_theme
|
|
4137
|
+
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.", {
|
|
4138
|
+
project_root: z
|
|
4139
|
+
.string()
|
|
4140
|
+
.describe("Absolute path to the code-component project."),
|
|
4141
|
+
confirm: z
|
|
4142
|
+
.boolean()
|
|
4143
|
+
.optional()
|
|
4144
|
+
.describe("Set true to actually publish. Omitted/false = dry-run (returns previewUrl + warning, publishes nothing)."),
|
|
4145
|
+
confirm_production: z
|
|
4146
|
+
.boolean()
|
|
4147
|
+
.optional()
|
|
4148
|
+
.describe("Required IN ADDITION to confirm when the target is the MAIN/production theme (live customer-facing site)."),
|
|
4149
|
+
port: z
|
|
4150
|
+
.number()
|
|
4151
|
+
.optional()
|
|
4152
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4153
|
+
}, async ({ project_root, confirm, confirm_production, port }) => {
|
|
4154
|
+
const args = [
|
|
4155
|
+
"publish-theme",
|
|
4156
|
+
...(confirm ? ["--confirm"] : []),
|
|
4157
|
+
...(confirm_production ? ["--confirm-production"] : []),
|
|
4158
|
+
...(port ? ["--port", String(port)] : []),
|
|
4159
|
+
];
|
|
4160
|
+
return callEditorAction(project_root, args);
|
|
4161
|
+
});
|
|
4162
|
+
// Tool: get_editor_workflow
|
|
4163
|
+
const EDITOR_WORKFLOW_GUIDE = [
|
|
4164
|
+
"# Editor workflow: placing sections and filling their content on a page",
|
|
4165
|
+
"",
|
|
4166
|
+
"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).",
|
|
4167
|
+
"",
|
|
4168
|
+
"## Two distinct jobs — do not confuse them",
|
|
4169
|
+
"- (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.",
|
|
4170
|
+
"- (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.",
|
|
4171
|
+
"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.",
|
|
4172
|
+
"",
|
|
4173
|
+
"## Step-by-step (JOB B — fill content)",
|
|
4174
|
+
"1. list_editor_pages → choose the target page_id.",
|
|
4175
|
+
"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.)",
|
|
4176
|
+
"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.",
|
|
4177
|
+
"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.",
|
|
4178
|
+
"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.",
|
|
4179
|
+
"",
|
|
4180
|
+
"## Building a whole page fast (placing + filling)",
|
|
4181
|
+
"- 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.",
|
|
4182
|
+
"- 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.",
|
|
4183
|
+
"",
|
|
4184
|
+
"## Value shapes by prop type (what to pass as `value`)",
|
|
4185
|
+
'- Scalars are WRAPPED: TEXT/RICH_TEXT { "value": "..." }, BOOLEAN { "value": true }, NUMBER { "value": 12 }, COLOR { "value": "#FF0000" }, ENUM { "value": "<key>" }, DATE { "value": ... }.',
|
|
4186
|
+
'- SVG is value-wrapped: { "value": "<svg markup>" }. NUMBER_RANGE: { "value": <number>, "unit": "px"|null }.',
|
|
4187
|
+
'- SVG naming: the editor shows each svg\'s `class` attribute as its display name in the list; add class="my-icon" to name it. SVG_LIST items with no class render as svg-1, svg-2, ….',
|
|
4188
|
+
'- Object props are NOT wrapped (no { "value": ... }):',
|
|
4189
|
+
' - 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>, ... ] }.',
|
|
4190
|
+
' - VIDEO: { "video": { "id": "<asset-id>" }, "thumbnailImage"?: { "id": "..." }, "autoplay"?: false, "controls"?: true, "loop"?: false, "muted"?: false } — NOT a bare { "id": ... }. SVG_LIST: { "svgs": [ "<svg>", ... ] }.',
|
|
4191
|
+
' - 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>, ... ] }.',
|
|
4192
|
+
' - 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.)',
|
|
4193
|
+
" - List/collection props (PRODUCT_LIST/CATEGORY_LIST/BRAND_LIST/BLOG_LIST/…): typed objects keyed by a <entity>ListType discriminator plus initialSort/initialLimit. The id field DEPENDS on the type — for PRODUCT_LIST: STATIC → productIds:[{productId,variantId}] (from search_products); CATEGORY → category:'<categoryId>' (from list_categories) — the field is `category`, NOT `categoryId` (categoryId is ONLY the standalone CATEGORY entity prop above); ALL/DISCOUNTED/RECOMMENDED/etc → no id field. Get the exact field set + writeNote from get_component_props, or read+edit an existing value via get_section_values. Unknown sub-fields (e.g. categoryId inside a PRODUCT_LIST) are REJECTED.",
|
|
4194
|
+
' - 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.',
|
|
4195
|
+
"",
|
|
4196
|
+
"## COMPONENT_LIST rules (the part most often gotten wrong)",
|
|
4197
|
+
"- 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.",
|
|
4198
|
+
"- 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.",
|
|
4199
|
+
'- Reference a code component with "codeComponentId" and a built-in theme component with "componentId" — not the other way around. Use "propValues" (not "props").',
|
|
4200
|
+
"- Child ids are opaque — take them from get_section_template / list_imported_sections; never invent them.",
|
|
4201
|
+
"- 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.",
|
|
4202
|
+
"",
|
|
4203
|
+
"## Reading prop schemas (avoid guessing)",
|
|
4204
|
+
"- 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.",
|
|
4205
|
+
"- For a COMPONENT_LIST: get_component_props(parent) → its allowedComponentIds → get_component_props(child) → the child's props/enums. Then build the components array.",
|
|
4206
|
+
"- 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).",
|
|
4207
|
+
"- 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.",
|
|
4208
|
+
"",
|
|
4209
|
+
"## Validation",
|
|
4210
|
+
"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.",
|
|
4211
|
+
].join("\n");
|
|
4212
|
+
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 () => {
|
|
4213
|
+
return {
|
|
4214
|
+
content: [{ type: "text", text: EDITOR_WORKFLOW_GUIDE }],
|
|
4215
|
+
};
|
|
4216
|
+
});
|
|
4217
|
+
// Tool: list_theme_globals
|
|
4218
|
+
server.tool("list_theme_globals", "List the theme's global settings from the connected editor: global variables (Theme Settings) and design tokens (colors, typography, breakpoints, keyframes, color schemes). Call this BEFORE generating sections/components so you reuse the theme's existing variables and tokens (read them in component code via `getThemeSetting`/`getThemeColors`/... from `@ikas/bp-storefront`). Includes items created manually in the editor UI. Requires `ikas-component dev` running with the editor connected. Two shape gotchas when consuming the result at runtime: (1) color schemes — `colorSchemes.schemes` holds only `{id,name}` slot LABELS; the actual colors live in `colorSchemes.values[].colorsByScheme` keyed by slot id, so iterate `values[].colorsByScheme` (iterating `schemes` renders empty). (2) An IMAGE setting's `value` is an image REFERENCE like `{id:'theme-images/<uuid>'}` with no `.url` — resolve it via `getDefaultSrc`/`getSrc`. (3) Each keyframe now includes its animation `settings` (type-dependent { property, value } entries — duration/iteration/timing/etc. for keyframe, transition for transition) and each point's resolved `styles`, so you can read back the full animation config, not just the `ref`. (4) Each typography entry may include `supportedFontWeights` — the exact numeric weights that text style's font actually ships (e.g. `[400]` for Modak); when present, only set `font_weight` to a value in this list (via `update_text_style`/`create_theme_global`), otherwise the write is rejected. If the field is absent the font's weight set couldn't be determined — fall back to 400 or rely on the validation error. Each typography entry also carries `isDefault` (true for the theme's default/global text style — the editor's \"Make Default Style\"); toggle it via `update_text_style`'s `is_default`. (5) Token NAMES are NOT unique — a theme can contain two tokens with the SAME name (e.g. colors pulled from two different design assets), so NEVER match a token by its human name. ALWAYS identify a token by its `id` (globally unique). IN COMPONENT CODE: prefer embedding the token's id-derived STABLE ref directly — color → cssVar `var(--<id>)`, typography → `className` `_<id>`, keyframe (animation-name) → `ref` `_<id>` — so you don't look anything up at runtime. If you DO need to pick a token out of a `getThemeColors()` / `getThemeTypography()` / `getThemeKeyframes()` / `getThemeColorSchemes()` array, match on `.id` (e.g. `getThemeColors().find(c => c.id === \"<id>\")`), NEVER on `.name`. EXCEPTION — global variables/settings: `getThemeSetting(name)` takes the token's `name`, but here `name` is the unique runtime key (`variableName`, the `_<id>` form returned in this list), which IS safe; do NOT pass a setting's human `displayName`. The update/delete tools also identify a token ONLY by `id` (they do NOT look up by name — on those tools `name` means the NEW name when renaming). Always take the `id` (or, for settings, the `name`/variableName) from THIS list. (6) GROUPING convention: a \"/\" in a token's NAME organizes it into a group in the editor UI — the text before the first \"/\" is the group, the rest is the item label. This applies to COLORS (e.g. \"Brand/Primary\"), TEXT STYLES (e.g. \"Heading/H1\"), and COLOR-SCHEME SLOTS (e.g. \"PrimaryButton/Background\"). When you create globals (via `create_theme_global`/`update_theme_color_scheme`), use \"/\"-prefixed names to keep related tokens grouped and tidy; reuse an existing group's exact prefix so they nest together.", {
|
|
4219
|
+
project_root: z
|
|
4220
|
+
.string()
|
|
4221
|
+
.describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
|
|
4222
|
+
port: z
|
|
4223
|
+
.number()
|
|
4224
|
+
.optional()
|
|
4225
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4226
|
+
}, async ({ project_root, port }) => {
|
|
4227
|
+
const args = [
|
|
4228
|
+
"list-theme-globals",
|
|
4229
|
+
...(port ? ["--port", String(port)] : []),
|
|
4230
|
+
];
|
|
4231
|
+
return callEditorAction(project_root, args);
|
|
4232
|
+
});
|
|
4233
|
+
// Shared text-style (typography) font-property schema — kept in lock-step with the editor's
|
|
4234
|
+
// "Update Text Style" modal fields (no color/text-align, since that modal doesn't expose them).
|
|
4235
|
+
// Each enum prop documents its accepted values so a bad value is caught with a clear error. Spread
|
|
4236
|
+
// into both create_theme_global (typography) and update_text_style so the two never drift apart.
|
|
4237
|
+
const textStyleFontParams = {
|
|
4238
|
+
font_family: z
|
|
4239
|
+
.string()
|
|
4240
|
+
.optional()
|
|
4241
|
+
.describe('A real font family the editor offers — a Google Fonts name with EXACT casing (e.g. "Inter", "Roboto", ' +
|
|
4242
|
+
'"Poppins"), or a font already used by an existing text style in list_theme_globals. An unknown font ' +
|
|
4243
|
+
"(or wrong casing) is rejected; do NOT invent a family name."),
|
|
4244
|
+
font_size: z.string().optional(),
|
|
4245
|
+
font_weight: z
|
|
4246
|
+
.string()
|
|
4247
|
+
.optional()
|
|
4248
|
+
.describe('font-weight as a number (e.g. "600"); append \'i\' for italic (e.g. "600i"). A PLAIN number with no \'i\' ' +
|
|
4249
|
+
'is upright and REMOVES italic if the text style was italic (italic is carried by the weight — re-send the ' +
|
|
4250
|
+
'weight without \'i\' to turn it off). Must be a weight the chosen ' +
|
|
4251
|
+
'font ships — if it isn\'t, the error lists the supported weights; when unsure use "400".'),
|
|
4252
|
+
line_height: z.string().optional(),
|
|
4253
|
+
letter_spacing: z.string().optional(),
|
|
4254
|
+
text_decoration: z
|
|
4255
|
+
.string()
|
|
4256
|
+
.optional()
|
|
4257
|
+
.describe("text-decoration: none | underline | line-through"),
|
|
4258
|
+
text_transform: z
|
|
4259
|
+
.string()
|
|
4260
|
+
.optional()
|
|
4261
|
+
.describe("text-transform: none | uppercase | lowercase | capitalize"),
|
|
4262
|
+
text_overflow: z
|
|
4263
|
+
.string()
|
|
4264
|
+
.optional()
|
|
4265
|
+
.describe("text-overflow: clip | ellipsis"),
|
|
4266
|
+
white_space: z
|
|
4267
|
+
.string()
|
|
4268
|
+
.optional()
|
|
4269
|
+
.describe("white-space: normal | pre | pre-wrap | pre-line | wrap | nowrap | collapse"),
|
|
4270
|
+
line_break: z
|
|
4271
|
+
.string()
|
|
4272
|
+
.optional()
|
|
4273
|
+
.describe("line-break: auto | loose | normal | strict | anywhere"),
|
|
4274
|
+
word_break: z
|
|
4275
|
+
.string()
|
|
4276
|
+
.optional()
|
|
4277
|
+
.describe("word-break: normal | break-all | keep-all"),
|
|
4278
|
+
text_wrap_style: z
|
|
4279
|
+
.string()
|
|
4280
|
+
.optional()
|
|
4281
|
+
.describe("text-wrap-style: auto | balance | pretty | stable"),
|
|
4282
|
+
};
|
|
4283
|
+
// Turn the supplied text-style font params into `ikas-component` CLI flags (omitted ones dropped).
|
|
4284
|
+
// The editor validates each value and rejects an unsupported one with the offending value + allowed set.
|
|
4285
|
+
function textStyleFontArgs(input) {
|
|
4286
|
+
const map = {
|
|
4287
|
+
"--font-family": input.font_family,
|
|
4288
|
+
"--font-size": input.font_size,
|
|
4289
|
+
"--font-weight": input.font_weight,
|
|
4290
|
+
"--line-height": input.line_height,
|
|
4291
|
+
"--letter-spacing": input.letter_spacing,
|
|
4292
|
+
"--text-decoration": input.text_decoration,
|
|
4293
|
+
"--text-transform": input.text_transform,
|
|
4294
|
+
"--text-overflow": input.text_overflow,
|
|
4295
|
+
"--white-space": input.white_space,
|
|
4296
|
+
"--line-break": input.line_break,
|
|
4297
|
+
"--word-break": input.word_break,
|
|
4298
|
+
"--text-wrap-style": input.text_wrap_style,
|
|
4299
|
+
};
|
|
4300
|
+
return Object.entries(map).flatMap(([flag, value]) => value ? [flag, value] : []);
|
|
4301
|
+
}
|
|
4302
|
+
// Tool: create_theme_global
|
|
4303
|
+
server.tool("create_theme_global", "Create a theme global setting in the connected editor. `kind` selects what to create:\n" +
|
|
4304
|
+
'- globalVariable: requires display_name + type (TEXT|RICH_TEXT|IMAGE|COLOR|NUMBER|BOOLEAN|BORDER|SHADOW); value optional. Pass `value` as a REAL JSON value, never a stringified JSON. Value shapes — TEXT/COLOR: string; RICH_TEXT: HTML string; NUMBER: number; BOOLEAN: real JSON boolean true/false (NOT the string "true"); IMAGE: an image REFERENCE object { id: "theme-images/<uuid>" } (NOT { url }) — the id MUST come from a REAL upload: call `upload_image` (file_path or image_url) and use the id it returns, or reuse an id already listed by `list_theme_globals`. NEVER invent an id — it is stored as given and renders as a broken image, so if you have no image, omit `value` and let the merchant set it from the "Theme Settings" panel; BORDER: { width: { value, unit }, style, color } (width.value must be a number); SHADOW: { x, y, blur, spread, color, position: "outside"|"inside" }. For object types (IMAGE/BORDER/SHADOW) pass an actual JSON object, not a JSON-encoded string. Every value is validated server-side and a wrong shape is REJECTED, not silently stored.\n' +
|
|
4305
|
+
'- color: requires name + value — a CONCRETE color (hex e.g. "#ff0000", rgb()/hsl() or a CSS color name). A color token cannot alias another color, so a "var(...)" reference is REJECTED here (to reuse another theme color, reference its cssVar in component code, or in a colorScheme slot). TIP: a "/" in the `name` GROUPS the color in the editor UI (e.g. "Brand/Primary", "Brand/Accent" appear under a "Brand" group); use this to organize related colors. To CHANGE an existing color later, use `update_theme_color` (edits in place, keeps the cssVar stable) — do NOT delete+recreate.\n' +
|
|
4306
|
+
'- typography: requires name + at least one text-style property. These are exactly the fields the editor\'s "Update Text Style" modal exposes — nothing else (in particular there is NO color or text-align on a text style). VALUE FORMATS (all validated; a bad value is rejected with the offending value and the allowed set): font_weight is one of 100,200,300,400,500,600,700,800,900 (append \'i\' for italic, e.g. "600i") AND must be a weight the chosen font actually ships (many fonts provide only a subset, e.g. "Modak" only ships 400) — if unavailable the call fails and lists the supported weights, so when unsure use 400; stored as a numeric weight + a font-style entry; font_size is a number WITH a unit (e.g. "16px", "1.2rem", "80%"); line_height is "normal", a unitless number (e.g. "1.5") or a length; letter_spacing is "normal" or a length (e.g. "0.02em", "0.5px"); font_family is a real font the editor offers (a Google Fonts name with EXACT casing, e.g. "Inter") — an unknown/invented family is rejected; text_decoration is none|underline|line-through; text_transform is none|uppercase|lowercase|capitalize; text_overflow is clip|ellipsis; white_space is normal|pre|pre-wrap|pre-line|wrap|nowrap|collapse; line_break is auto|loose|normal|strict|anywhere; word_break is normal|break-all|keep-all; text_wrap_style is auto|balance|pretty|stable. TIP: a "/" in the `name` GROUPS the text style in the editor UI (e.g. "Heading/H1", "Heading/H2" appear under a "Heading" group); use this to organize related styles. To CHANGE an existing text style later, use `update_text_style` (edits in place, keeps the `className` stable) — do NOT delete+recreate, which changes the id/className.\n' +
|
|
4307
|
+
"- breakpoint: requires name + width (a positive INTEGER, in px — no decimals). To CHANGE an existing breakpoint later, use `update_theme_breakpoint` (edits in place, keeps the id).\n" +
|
|
4308
|
+
"- keyframe: requires name + points (array of { point, styles? }) where each style is { property, value } using a CSS property name (opacity, transform, filter, background, color, …); keyframe_type defaults to \"keyframe\". Optional `settings` = animation-level config as { property, value } entries, TYPE-DEPENDENT: for keyframe — animation-duration, animation-iteration-count, animation-play-state, animation-delay, animation-timing-function, animation-direction, animation-fill-mode, animation-timeline, animation-range, transform-origin, backface-visibility; for transition — transition, backface-visibility. Setting these means the editor drives the animation timing (you don't have to hand-write `animation:` CSS). Use the keyframe's `ref` as the CSS animation-name. For a TRANSITION keyframe, put the target/hover-state styles on the 100% point (points[1].styles) — the 0% point is ignored (the start state is the element's own normal state). To CHANGE an existing keyframe later, use `update_theme_keyframe` (edits in place, keeps the `ref` stable) — do NOT delete+recreate, which changes the ref and breaks references.\n" +
|
|
4309
|
+
'- colorScheme: requires name + colors (array of { slotId? | newSlotName?, value }) — each color targets ONE color SLOT, EITHER by `slotId` (an EXISTING slot\'s id, from `list_theme_globals` → colorSchemes.schemes[].id or a `colorsByScheme` key — the id-keyed path) OR by `newSlotName` (a name to CREATE-or-reuse a slot; write-only, for the standard vocabulary and brand-new slots). Pass EXACTLY ONE of the two per entry. `value` is EITHER a literal color (hex/rgb/hsl or a CSS color name, e.g. "#0b1120") OR a REFERENCE to an existing theme color, passed as that color\'s cssVar from `list_theme_globals` (e.g. "var(--<colorId>)"). A `var(--id)` value is stored as a linked reference (editing the global color updates the scheme too and the dependency is tracked) — do NOT inline a hex you copied from a global color; pass its cssVar. SLOT IDENTITY: `colorSchemes.schemes` lists each slot as `{id, name}`. To set a color on an EXISTING slot, find it there by name and pass its `slotId` (names are NOT unique — two design assets can each have a "Background" — so when ambiguous disambiguate via the scheme\'s existing `colorsByScheme` keys); use `newSlotName` only to add a slot the scheme does not have yet. When creating slots use the theme\'s STANDARD slot names so components resolve them: "Background", "Heading", "Text", "Link", "HoverLink", "Border", and per button "PrimaryButton/Background", "PrimaryButton/Text", "PrimaryButton/Border", "PrimaryButton/HoverBackground", "PrimaryButton/HoverText", "PrimaryButton/HoverBorder" (and the same six under "SecondaryButton/"); a "/" groups the slot in the editor UI. A scheme should normally define EVERY slot it uses; omitted slots fall back to the default scheme\'s color. A mistaken slot can be removed with `delete_theme_global` kind `colorSchemeSlot` (by slot id), or renamed in place (id stays stable) with `rename_theme_color_scheme_slot`. To CHANGE an existing scheme later, use `update_theme_color_scheme` (edits in place, keeps the className stable; merges colors by slot, and can set/clear its default flag via `is_default`).\n' +
|
|
4310
|
+
"Read created items back with `list_theme_globals`. Requires `ikas-component dev` running with the editor connected.", {
|
|
4311
|
+
project_root: z
|
|
4312
|
+
.string()
|
|
4313
|
+
.describe("Absolute path to the code-component project."),
|
|
4314
|
+
kind: z
|
|
4315
|
+
.enum([
|
|
4316
|
+
"globalVariable",
|
|
4317
|
+
"color",
|
|
4318
|
+
"typography",
|
|
4319
|
+
"breakpoint",
|
|
4320
|
+
"keyframe",
|
|
4321
|
+
"colorScheme",
|
|
4322
|
+
])
|
|
4323
|
+
.describe("What to create."),
|
|
4324
|
+
name: z.string().optional().describe("Name (design tokens)."),
|
|
4325
|
+
display_name: z
|
|
4326
|
+
.string()
|
|
4327
|
+
.optional()
|
|
4328
|
+
.describe("Human label (globalVariable)."),
|
|
4329
|
+
type: z.string().optional().describe("globalVariable value type."),
|
|
4330
|
+
value: z
|
|
4331
|
+
.any()
|
|
4332
|
+
.optional()
|
|
4333
|
+
.describe("globalVariable default value (shape depends on type — see tool description), or color hex string."),
|
|
4334
|
+
...textStyleFontParams,
|
|
4335
|
+
width: z
|
|
4336
|
+
.number()
|
|
4337
|
+
.int()
|
|
4338
|
+
.optional()
|
|
4339
|
+
.describe("breakpoint width in px (positive integer, no decimals)."),
|
|
4340
|
+
keyframe_type: z.enum(["keyframe", "transition"]).optional(),
|
|
4341
|
+
points: z.any().optional().describe("keyframe points array."),
|
|
4342
|
+
settings: z
|
|
4343
|
+
.any()
|
|
4344
|
+
.optional()
|
|
4345
|
+
.describe("keyframe animation settings: array of { property, value } (type-dependent — see tool description)."),
|
|
4346
|
+
colors: z
|
|
4347
|
+
.any()
|
|
4348
|
+
.optional()
|
|
4349
|
+
.describe('colorScheme colors: array of { slotId? | newSlotName?, value } — target an EXISTING slot by `slotId` (its id from list_theme_globals → colorSchemes.schemes / colorsByScheme keys) OR create/reuse one by `newSlotName` (use the standard slot names, "/" to group, e.g. "PrimaryButton/Background"). Exactly one per entry. value = a literal color hex/rgb/hsl/name, OR an existing theme color\'s cssVar "var(--<colorId>)" to store a linked reference.'),
|
|
4350
|
+
port: z
|
|
4351
|
+
.number()
|
|
4352
|
+
.optional()
|
|
4353
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4354
|
+
}, async (input) => {
|
|
4355
|
+
const p = input.port ? ["--port", String(input.port)] : [];
|
|
4356
|
+
let args;
|
|
4357
|
+
switch (input.kind) {
|
|
4358
|
+
case "globalVariable":
|
|
4359
|
+
args = [
|
|
4360
|
+
"create-global-variable",
|
|
4361
|
+
"--display-name",
|
|
4362
|
+
String(input.display_name ?? ""),
|
|
4363
|
+
"--type",
|
|
4364
|
+
String(input.type ?? ""),
|
|
4365
|
+
...(input.value !== undefined
|
|
4366
|
+
? ["--value", JSON.stringify(input.value)]
|
|
4367
|
+
: []),
|
|
4368
|
+
...p,
|
|
4369
|
+
];
|
|
4370
|
+
break;
|
|
4371
|
+
case "color":
|
|
4372
|
+
args = [
|
|
4373
|
+
"create-color",
|
|
4374
|
+
"--name",
|
|
4375
|
+
String(input.name ?? ""),
|
|
4376
|
+
"--value",
|
|
4377
|
+
String(input.value ?? ""),
|
|
4378
|
+
...p,
|
|
4379
|
+
];
|
|
4380
|
+
break;
|
|
4381
|
+
case "typography":
|
|
4382
|
+
args = [
|
|
4383
|
+
"create-text-style",
|
|
4384
|
+
"--name",
|
|
4385
|
+
String(input.name ?? ""),
|
|
4386
|
+
...textStyleFontArgs(input),
|
|
4387
|
+
...p,
|
|
4388
|
+
];
|
|
4389
|
+
break;
|
|
4390
|
+
case "breakpoint":
|
|
4391
|
+
args = [
|
|
4392
|
+
"create-breakpoint",
|
|
4393
|
+
"--name",
|
|
4394
|
+
String(input.name ?? ""),
|
|
4395
|
+
"--width",
|
|
4396
|
+
String(input.width ?? ""),
|
|
4397
|
+
...p,
|
|
4398
|
+
];
|
|
4399
|
+
break;
|
|
4400
|
+
case "keyframe":
|
|
4401
|
+
args = [
|
|
4402
|
+
"create-keyframe",
|
|
4403
|
+
"--name",
|
|
4404
|
+
String(input.name ?? ""),
|
|
4405
|
+
...(input.keyframe_type ? ["--type", input.keyframe_type] : []),
|
|
4406
|
+
"--points",
|
|
4407
|
+
toJsonArrayArg(input.points),
|
|
4408
|
+
...(input.settings !== undefined
|
|
4409
|
+
? ["--settings", toJsonArrayArg(input.settings)]
|
|
4410
|
+
: []),
|
|
4411
|
+
...p,
|
|
4412
|
+
];
|
|
4413
|
+
break;
|
|
4414
|
+
case "colorScheme":
|
|
4415
|
+
args = [
|
|
4416
|
+
"create-color-scheme",
|
|
4417
|
+
"--name",
|
|
4418
|
+
String(input.name ?? ""),
|
|
4419
|
+
"--colors",
|
|
4420
|
+
toJsonArrayArg(input.colors),
|
|
4421
|
+
...p,
|
|
4422
|
+
];
|
|
4423
|
+
break;
|
|
4424
|
+
default:
|
|
4425
|
+
return {
|
|
4426
|
+
content: [
|
|
4427
|
+
{
|
|
4428
|
+
type: "text",
|
|
4429
|
+
text: `Error: unknown kind "${input.kind}"`,
|
|
4430
|
+
},
|
|
4431
|
+
],
|
|
4432
|
+
};
|
|
4433
|
+
}
|
|
4434
|
+
return callEditorAction(input.project_root, args);
|
|
4435
|
+
});
|
|
4436
|
+
// Tool: update_theme_global
|
|
4437
|
+
server.tool("update_theme_global", 'Update an existing global variable in the connected editor (e.g. fix its value or change its type). Identify it by `name` (the runtime key from `list_theme_globals`). Only the fields you pass are changed. Value shapes follow `create_theme_global` — pass `value` as REAL JSON (objects/booleans, never stringified); BOOLEAN is a real boolean, BORDER/SHADOW are real objects; IMAGE is an image REFERENCE object { id: "theme-images/<uuid>" } whose id MUST come from `upload_image` or an existing `list_theme_globals` entry — an invented id renders as a broken image. Values are validated server-side; a wrong shape is REJECTED, not silently stored.', {
|
|
4438
|
+
project_root: z
|
|
4439
|
+
.string()
|
|
4440
|
+
.describe("Absolute path to the code-component project."),
|
|
4441
|
+
name: z
|
|
4442
|
+
.string()
|
|
4443
|
+
.describe("The variable's runtime key (from `list_theme_globals`)."),
|
|
4444
|
+
display_name: z.string().optional().describe("New label."),
|
|
4445
|
+
type: z
|
|
4446
|
+
.string()
|
|
4447
|
+
.optional()
|
|
4448
|
+
.describe("New value type (TEXT|RICH_TEXT|IMAGE|COLOR|NUMBER|BOOLEAN|BORDER|SHADOW)."),
|
|
4449
|
+
value: z
|
|
4450
|
+
.any()
|
|
4451
|
+
.optional()
|
|
4452
|
+
.describe('New value as REAL JSON (shape depends on type; e.g. BOOLEAN: true/false, NUMBER: 12, IMAGE: { id: "theme-images/<uuid>" } from upload_image — see create_theme_global). An invented IMAGE id renders as a broken image.'),
|
|
4453
|
+
port: z
|
|
4454
|
+
.number()
|
|
4455
|
+
.optional()
|
|
4456
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4457
|
+
}, async ({ project_root, name, display_name, type, value, port }) => {
|
|
4458
|
+
const args = [
|
|
4459
|
+
"update-global-variable",
|
|
4460
|
+
"--name",
|
|
4461
|
+
name,
|
|
4462
|
+
...(display_name !== undefined ? ["--display-name", display_name] : []),
|
|
4463
|
+
...(type !== undefined ? ["--type", type] : []),
|
|
4464
|
+
...(value !== undefined ? ["--value", JSON.stringify(value)] : []),
|
|
4465
|
+
...(port ? ["--port", String(port)] : []),
|
|
4466
|
+
];
|
|
4467
|
+
return callEditorAction(project_root, args);
|
|
4468
|
+
});
|
|
4469
|
+
// Tool: update_theme_keyframe
|
|
4470
|
+
server.tool("update_theme_keyframe", "Update an existing theme keyframe IN PLACE in the connected editor. Prefer this over delete+recreate: " +
|
|
4471
|
+
"it keeps the keyframe's id, so its runtime `ref` (the CSS animation-name `_<id>`) stays STABLE and every " +
|
|
4472
|
+
"existing reference to it keeps working. Identify the keyframe by `id` (REQUIRED — globally unique, from " +
|
|
4473
|
+
"`list_theme_globals`); name-based lookup is intentionally NOT supported here because names can collide across " +
|
|
4474
|
+
"design assets. To rename, pass `name` (the new name) together with `id`. " +
|
|
4475
|
+
"`points` REPLACES the keyframe's points: an array of { point, styles? } where `point` is a CSS " +
|
|
4476
|
+
'keyframe offset (e.g. "0%", "50%", "100%") and each style is { property, value } using a CSS property ' +
|
|
4477
|
+
"name (opacity, transform, filter, background, color, …). Pass `points` as REAL JSON (not a stringified array). " +
|
|
4478
|
+
"For a TRANSITION keyframe, put the target/hover-state styles on the 100% point (points[1].styles) — the 0% " +
|
|
4479
|
+
"point is ignored (the start state is the element's own normal state). " +
|
|
4480
|
+
"Optional `settings` REPLACES the keyframe's animation settings (type-dependent { property, value } entries — " +
|
|
4481
|
+
"see `create_theme_global`); OMIT it to keep the existing settings, or pass [] to clear them. " +
|
|
4482
|
+
"Requires `ikas-component dev` running with the editor connected.", {
|
|
4483
|
+
project_root: z
|
|
4484
|
+
.string()
|
|
4485
|
+
.describe("Absolute path to the code-component project."),
|
|
4486
|
+
id: z
|
|
4487
|
+
.string()
|
|
4488
|
+
.describe("Keyframe id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
|
|
4489
|
+
name: z
|
|
4490
|
+
.string()
|
|
4491
|
+
.optional()
|
|
4492
|
+
.describe("Optional NEW name to rename the keyframe to. NOT used to find the token — lookup is always by id."),
|
|
4493
|
+
keyframe_type: z
|
|
4494
|
+
.enum(["keyframe", "transition"])
|
|
4495
|
+
.optional()
|
|
4496
|
+
.describe("Optional new keyframe type."),
|
|
4497
|
+
points: z
|
|
4498
|
+
.any()
|
|
4499
|
+
.describe("New keyframe points array of { point, styles? } — REPLACES the existing points."),
|
|
4500
|
+
settings: z
|
|
4501
|
+
.any()
|
|
4502
|
+
.optional()
|
|
4503
|
+
.describe("New animation settings { property, value } array (type-dependent — see create_theme_global). Omit to keep existing; [] to clear."),
|
|
4504
|
+
port: z
|
|
4505
|
+
.number()
|
|
4506
|
+
.optional()
|
|
4507
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4508
|
+
}, async ({ project_root, id, name, keyframe_type, points, settings, port }) => {
|
|
4509
|
+
const args = [
|
|
4510
|
+
"update-keyframe",
|
|
4511
|
+
"--id",
|
|
4512
|
+
id,
|
|
4513
|
+
...(name !== undefined ? ["--name", name] : []),
|
|
4514
|
+
...(keyframe_type ? ["--type", keyframe_type] : []),
|
|
4515
|
+
"--points",
|
|
4516
|
+
toJsonArrayArg(points),
|
|
4517
|
+
...(settings !== undefined
|
|
4518
|
+
? ["--settings", toJsonArrayArg(settings)]
|
|
4519
|
+
: []),
|
|
4520
|
+
...(port ? ["--port", String(port)] : []),
|
|
4521
|
+
];
|
|
4522
|
+
return callEditorAction(project_root, args);
|
|
4523
|
+
});
|
|
4524
|
+
// Tool: update_text_style
|
|
4525
|
+
server.tool("update_text_style", "Update an existing theme typography token (text style) IN PLACE in the connected editor. Prefer this over " +
|
|
4526
|
+
"delete+recreate: it keeps the text style's id, so its `className` (`_<id>`) — and every section bound to it — " +
|
|
4527
|
+
"stays STABLE. Identify the text style by `id` (REQUIRED — globally unique, from `list_theme_globals`); " +
|
|
4528
|
+
"name-based lookup is intentionally NOT supported here because names can collide across design assets. " +
|
|
4529
|
+
"To rename, pass `name` (the new name) together with `id`. Pass only the properties you want to change; omitted " +
|
|
4530
|
+
'ones keep their existing values. The settable properties are exactly the editor\'s "Update Text Style" modal ' +
|
|
4531
|
+
"fields — there is NO color or text-align on a text style. VALUE FORMATS (all validated; a bad value is rejected " +
|
|
4532
|
+
"with the offending value and the allowed set): font_weight is one of 100…900 (append 'i' for italic, e.g. " +
|
|
4533
|
+
'"600i") AND must be a weight the chosen font actually ships — many fonts only provide a subset (e.g. "Modak" ' +
|
|
4534
|
+
"only ships 400); if a weight isn't available the call fails and the error lists the supported weights, so when " +
|
|
4535
|
+
'unsure use 400; font_size is a number with a unit (e.g. "16px"); line_height is "normal", a unitless number ' +
|
|
4536
|
+
'or a length; letter_spacing is "normal" or a length (e.g. "0.02em"); font_family is a real font the editor ' +
|
|
4537
|
+
'offers (a Google Fonts name with EXACT casing, e.g. "Inter") — an unknown/invented family is rejected; ' +
|
|
4538
|
+
"text_decoration is none|underline|" +
|
|
4539
|
+
"line-through; text_transform is none|uppercase|lowercase|capitalize; text_overflow is clip|ellipsis; white_space " +
|
|
4540
|
+
"is normal|pre|pre-wrap|pre-line|wrap|nowrap|collapse; line_break is auto|loose|normal|strict|anywhere; word_break " +
|
|
4541
|
+
"is normal|break-all|keep-all; text_wrap_style is auto|balance|pretty|stable. Set `is_default: true` to make " +
|
|
4542
|
+
"this text style the theme's default/global style (the editor's right-click \"Make Default Style\" / \"Global " +
|
|
4543
|
+
"Stil Yap\") — any other default in the group is cleared; `is_default: false` removes the flag. A text style's " +
|
|
4544
|
+
"current default state is readable from `list_theme_globals` (each typography entry's `isDefault`). Requires " +
|
|
4545
|
+
"`ikas-component dev` running with the editor connected.", {
|
|
4546
|
+
project_root: z
|
|
4547
|
+
.string()
|
|
4548
|
+
.describe("Absolute path to the code-component project."),
|
|
4549
|
+
id: z
|
|
4550
|
+
.string()
|
|
4551
|
+
.describe("Text style id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
|
|
4552
|
+
name: z
|
|
4553
|
+
.string()
|
|
4554
|
+
.optional()
|
|
4555
|
+
.describe("Optional NEW name to rename the text style to. NOT used to find the token — lookup is always by id."),
|
|
4556
|
+
...textStyleFontParams,
|
|
4557
|
+
is_default: z
|
|
4558
|
+
.boolean()
|
|
4559
|
+
.optional()
|
|
4560
|
+
.describe("true → make this the theme's default/global text style (clears any other default); false → remove the " +
|
|
4561
|
+
"default flag. Omit to leave the default state unchanged."),
|
|
4562
|
+
port: z
|
|
4563
|
+
.number()
|
|
4564
|
+
.optional()
|
|
4565
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4566
|
+
}, async (input) => {
|
|
4567
|
+
const { project_root, id, name, is_default, port } = input;
|
|
4568
|
+
const args = [
|
|
4569
|
+
"update-text-style",
|
|
4570
|
+
"--id",
|
|
4571
|
+
id,
|
|
4572
|
+
...(name !== undefined ? ["--name", name] : []),
|
|
4573
|
+
...textStyleFontArgs(input),
|
|
4574
|
+
...(is_default === true ? ["--default"] : is_default === false ? ["--no-default"] : []),
|
|
4575
|
+
...(port ? ["--port", String(port)] : []),
|
|
4576
|
+
];
|
|
4577
|
+
return callEditorAction(project_root, args);
|
|
4578
|
+
});
|
|
4579
|
+
// Tool: update_theme_color
|
|
4580
|
+
server.tool("update_theme_color", "Update an existing theme color token IN PLACE in the connected editor. Prefer this over delete+recreate: " +
|
|
4581
|
+
"it keeps the color's id, so its cssVar (`var(--<id>)`) — and every element style bound to it — stays STABLE. " +
|
|
4582
|
+
"Identify it by `id` (REQUIRED — globally unique, from `list_theme_globals`); name-based lookup is intentionally " +
|
|
4583
|
+
"NOT supported here because a theme can have two colors with the same name (e.g. from two different design assets). " +
|
|
4584
|
+
"To rename, pass `name` (the new name) together with `id`. Pass `value` to change the color — a CONCRETE " +
|
|
4585
|
+
'color (hex/rgb/hsl or a CSS color name), NOT a "var(...)" reference (a color token can\'t alias another ' +
|
|
4586
|
+
"color); omit it to only rename. Requires `ikas-component " +
|
|
4587
|
+
"dev` running with the editor connected.", {
|
|
4588
|
+
project_root: z
|
|
4589
|
+
.string()
|
|
4590
|
+
.describe("Absolute path to the code-component project."),
|
|
4591
|
+
id: z
|
|
4592
|
+
.string()
|
|
4593
|
+
.describe("Color id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
|
|
4594
|
+
name: z
|
|
4595
|
+
.string()
|
|
4596
|
+
.optional()
|
|
4597
|
+
.describe("Optional NEW name to rename the color to. NOT used to find the token — lookup is always by id."),
|
|
4598
|
+
value: z
|
|
4599
|
+
.string()
|
|
4600
|
+
.optional()
|
|
4601
|
+
.describe('New color value — a concrete color (e.g. "#ff0000", rgb/hsl/name), not a "var(...)" reference. Omit to only rename.'),
|
|
4602
|
+
port: z
|
|
4603
|
+
.number()
|
|
4604
|
+
.optional()
|
|
4605
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4606
|
+
}, async ({ project_root, id, name, value, port }) => {
|
|
4607
|
+
const args = [
|
|
4608
|
+
"update-color",
|
|
4609
|
+
"--id",
|
|
4610
|
+
id,
|
|
4611
|
+
...(name !== undefined ? ["--name", name] : []),
|
|
4612
|
+
...(value !== undefined ? ["--value", value] : []),
|
|
4613
|
+
...(port ? ["--port", String(port)] : []),
|
|
4614
|
+
];
|
|
4615
|
+
return callEditorAction(project_root, args);
|
|
4616
|
+
});
|
|
4617
|
+
// Tool: update_theme_breakpoint
|
|
4618
|
+
server.tool("update_theme_breakpoint", "Update an existing theme breakpoint IN PLACE in the connected editor (keeps its id). Identify it by `id` " +
|
|
4619
|
+
"(REQUIRED — globally unique, from `list_theme_globals`); name-based lookup is intentionally NOT supported here " +
|
|
4620
|
+
"because names can collide across design assets. To rename, pass `name` (the new name) together with `id`. " +
|
|
4621
|
+
"Pass `width` (px) to change it; omit it to only rename. " +
|
|
4622
|
+
"Requires `ikas-component dev` running with the editor connected.", {
|
|
4623
|
+
project_root: z
|
|
4624
|
+
.string()
|
|
4625
|
+
.describe("Absolute path to the code-component project."),
|
|
4626
|
+
id: z
|
|
4627
|
+
.string()
|
|
4628
|
+
.describe("Breakpoint id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
|
|
4629
|
+
name: z
|
|
4630
|
+
.string()
|
|
4631
|
+
.optional()
|
|
4632
|
+
.describe("Optional NEW name to rename the breakpoint to. NOT used to find the token — lookup is always by id."),
|
|
4633
|
+
width: z
|
|
4634
|
+
.number()
|
|
4635
|
+
.int()
|
|
4636
|
+
.optional()
|
|
4637
|
+
.describe("New breakpoint width in px (positive integer, no decimals). Omit to only rename."),
|
|
4638
|
+
port: z
|
|
4639
|
+
.number()
|
|
4640
|
+
.optional()
|
|
4641
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4642
|
+
}, async ({ project_root, id, name, width, port }) => {
|
|
4643
|
+
const args = [
|
|
4644
|
+
"update-breakpoint",
|
|
4645
|
+
"--id",
|
|
4646
|
+
id,
|
|
4647
|
+
...(name !== undefined ? ["--name", name] : []),
|
|
4648
|
+
...(width !== undefined ? ["--width", String(width)] : []),
|
|
4649
|
+
...(port ? ["--port", String(port)] : []),
|
|
4650
|
+
];
|
|
4651
|
+
return callEditorAction(project_root, args);
|
|
4652
|
+
});
|
|
4653
|
+
// Tool: update_theme_color_scheme
|
|
4654
|
+
server.tool("update_theme_color_scheme", "Update an existing theme color scheme (palette) IN PLACE in the connected editor. Prefer this over " +
|
|
4655
|
+
"delete+recreate: it keeps the scheme's id, so its className (`_<id>`) stays STABLE. Identify it by `id` " +
|
|
4656
|
+
"(REQUIRED — globally unique, from `list_theme_globals`); name-based lookup is intentionally NOT supported here " +
|
|
4657
|
+
"because names can collide across design assets. To rename, pass `name` (the new name) together with `id`. " +
|
|
4658
|
+
"`colors` is an array of { slotId? | newSlotName?, value } — each entry targets ONE slot, EITHER by " +
|
|
4659
|
+
"`slotId` (an EXISTING slot's id, from list_theme_globals colorsByScheme keys / colorSchemes.schemes — the " +
|
|
4660
|
+
"id-keyed path; slot names ARE listed in colorSchemes.schemes but are not unique, so prefer the scheme's colorsByScheme keys to get the right id) OR by `newSlotName` (create/reuse " +
|
|
4661
|
+
'a slot by name; use the standard slot names, "/" to group, e.g. "PrimaryButton/Background"); value = a ' +
|
|
4662
|
+
'literal color hex/rgb/hsl/name, OR an existing theme color\'s cssVar "var(--<colorId>)" from ' +
|
|
4663
|
+
"list_theme_globals to store it as a LINKED reference — pass the cssVar, not a hex copied from a global color) " +
|
|
4664
|
+
"and MERGES by slot — only the slots you pass change. Pass `colors` as REAL " +
|
|
4665
|
+
"JSON (not a stringified array). Set `is_default: true` to make this the theme's DEFAULT scheme (the " +
|
|
4666
|
+
"fallback whose colors fill any slot a non-default scheme leaves unset); this is exclusive — the editor " +
|
|
4667
|
+
"automatically clears the default flag from any other scheme in the same group, so there is always exactly " +
|
|
4668
|
+
"one default. Set `is_default: false` to remove the default flag from this scheme. Omit all of name/colors/" +
|
|
4669
|
+
"is_default to no-op. Requires `ikas-component dev` running with the editor connected.", {
|
|
4670
|
+
project_root: z
|
|
4671
|
+
.string()
|
|
4672
|
+
.describe("Absolute path to the code-component project."),
|
|
4673
|
+
id: z
|
|
4674
|
+
.string()
|
|
4675
|
+
.describe("Color scheme id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
|
|
4676
|
+
name: z
|
|
4677
|
+
.string()
|
|
4678
|
+
.optional()
|
|
4679
|
+
.describe("Optional NEW name to rename the color scheme to. NOT used to find the token — lookup is always by id."),
|
|
4680
|
+
colors: z
|
|
4681
|
+
.any()
|
|
4682
|
+
.optional()
|
|
4683
|
+
.describe('Array of { slotId? | newSlotName?, value } to set — target an existing slot by `slotId` (from list_theme_globals colorsByScheme keys / colorSchemes.schemes) or create/reuse by `newSlotName`; value = a literal color hex/rgb/hsl/name, OR an existing theme color\'s cssVar "var(--<colorId>)" (linked reference). Merges by slot. Omit to only rename.'),
|
|
4684
|
+
is_default: z
|
|
4685
|
+
.boolean()
|
|
4686
|
+
.optional()
|
|
4687
|
+
.describe("true = make this the default scheme (exclusive — the editor clears the default flag from every other scheme in its group); false = remove the default flag. Omit to leave the default unchanged. The result echoes the persisted `isDefault`."),
|
|
4688
|
+
port: z
|
|
4689
|
+
.number()
|
|
4690
|
+
.optional()
|
|
4691
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4692
|
+
}, async ({ project_root, id, name, colors, is_default, port }) => {
|
|
4693
|
+
const args = [
|
|
4694
|
+
"update-color-scheme",
|
|
4695
|
+
"--id",
|
|
4696
|
+
id,
|
|
4697
|
+
...(name !== undefined ? ["--name", name] : []),
|
|
4698
|
+
...(colors !== undefined ? ["--colors", toJsonArrayArg(colors)] : []),
|
|
4699
|
+
...(is_default === true
|
|
4700
|
+
? ["--default"]
|
|
4701
|
+
: is_default === false
|
|
4702
|
+
? ["--no-default"]
|
|
4703
|
+
: []),
|
|
4704
|
+
...(port ? ["--port", String(port)] : []),
|
|
4705
|
+
];
|
|
4706
|
+
return callEditorAction(project_root, args);
|
|
4707
|
+
});
|
|
4708
|
+
// Tool: rename_theme_color_scheme_slot
|
|
4709
|
+
server.tool("rename_theme_color_scheme_slot", 'Rename a color scheme SLOT (a scheme key like "Background" or "PrimaryButton/Text") IN PLACE in the ' +
|
|
4710
|
+
"connected editor. A slot's id is STABLE, so renaming keeps every runtime reference intact " +
|
|
4711
|
+
"(`var(--<slotId>)` in CSS, `colorsByScheme[slotId]` in component code) — only the human display name " +
|
|
4712
|
+
"changes; nothing rebinds. Identify the slot by `id` (REQUIRED — the slot id from `list_theme_globals` → " +
|
|
4713
|
+
"colorSchemes.schemes[].id or a `colorsByScheme` key); name-based lookup is intentionally NOT supported " +
|
|
4714
|
+
'because slot names can collide across design assets. `name` is the NEW slot name (a "/" groups it in the ' +
|
|
4715
|
+
'editor UI, e.g. "PrimaryButton/Background"). Only non-design-asset (user/theme) slots can be renamed here. ' +
|
|
4716
|
+
"This renames the slot itself across ALL schemes that use it (a slot is shared) — it does NOT rename a single " +
|
|
4717
|
+
"scheme/palette (for that use `update_theme_color_scheme`). Requires `ikas-component dev` running with the " +
|
|
4718
|
+
"editor connected.", {
|
|
4719
|
+
project_root: z
|
|
4720
|
+
.string()
|
|
4721
|
+
.describe("Absolute path to the code-component project."),
|
|
4722
|
+
id: z
|
|
4723
|
+
.string()
|
|
4724
|
+
.min(1, "id is REQUIRED — the slot id from list_theme_globals (colorSchemes.schemes[].id / colorsByScheme keys).")
|
|
4725
|
+
.describe("Slot id (REQUIRED) from `list_theme_globals` → colorSchemes.schemes[].id / colorsByScheme keys. The slot is always identified by id."),
|
|
4726
|
+
name: z
|
|
4727
|
+
.string()
|
|
4728
|
+
.min(1, "name is REQUIRED — the new slot name (non-empty).")
|
|
4729
|
+
.describe('The NEW slot name (non-empty). A "/" groups it in the editor UI (e.g. "PrimaryButton/Background").'),
|
|
4730
|
+
port: z
|
|
4731
|
+
.number()
|
|
4732
|
+
.optional()
|
|
4733
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4734
|
+
}, async ({ project_root, id, name, port }) => {
|
|
4735
|
+
const args = [
|
|
4736
|
+
"rename-color-scheme-slot",
|
|
4737
|
+
"--id",
|
|
4738
|
+
id,
|
|
4739
|
+
"--name",
|
|
4740
|
+
name,
|
|
4741
|
+
...(port ? ["--port", String(port)] : []),
|
|
4742
|
+
];
|
|
4743
|
+
return callEditorAction(project_root, args);
|
|
4744
|
+
});
|
|
4745
|
+
// Tool: delete_theme_global
|
|
4746
|
+
server.tool("delete_theme_global", 'Delete a theme global setting from the connected editor. For `kind: globalVariable` pass `name` (the runtime key); for design-token kinds (color | typography | breakpoint | keyframe | colorScheme) pass the token `id`. For `kind: colorSchemeSlot` pass the color SLOT id (from `list_theme_globals` → colorSchemes.schemes[].id or a `colorsByScheme` key) — this deletes the slot itself across ALL schemes (use it to clean up a junk slot created by a typo); it CASCADES: the slot\'s color is removed from every scheme and any element/component style bound to that slot is cleared, so only delete a slot you are sure is unused. Only non-design-asset (user/theme) slots can be deleted here. Get names/ids from `list_theme_globals`. Deleting a non-existent id fails with a "not found" error (it does NOT report a false success), so a success result means the token really existed and was removed.', {
|
|
4747
|
+
project_root: z
|
|
4748
|
+
.string()
|
|
4749
|
+
.describe("Absolute path to the code-component project."),
|
|
4750
|
+
kind: z
|
|
4751
|
+
.enum([
|
|
4752
|
+
"globalVariable",
|
|
4753
|
+
"color",
|
|
4754
|
+
"typography",
|
|
4755
|
+
"breakpoint",
|
|
4756
|
+
"keyframe",
|
|
4757
|
+
"colorScheme",
|
|
4758
|
+
"colorSchemeSlot",
|
|
4759
|
+
])
|
|
4760
|
+
.describe("What to delete."),
|
|
4761
|
+
name: z
|
|
4762
|
+
.string()
|
|
4763
|
+
.optional()
|
|
4764
|
+
.describe("globalVariable runtime key (from `list_theme_globals`)."),
|
|
4765
|
+
id: z
|
|
4766
|
+
.string()
|
|
4767
|
+
.optional()
|
|
4768
|
+
.describe("design-token id, or for colorSchemeSlot the slot id from `list_theme_globals` → colorSchemes.schemes."),
|
|
4769
|
+
port: z
|
|
4770
|
+
.number()
|
|
4771
|
+
.optional()
|
|
4772
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4773
|
+
}, async ({ project_root, kind, name, id, port }) => {
|
|
4774
|
+
const p = port ? ["--port", String(port)] : [];
|
|
4775
|
+
const args = kind === "globalVariable"
|
|
4776
|
+
? ["delete-global-variable", "--name", String(name ?? ""), ...p]
|
|
4777
|
+
: [
|
|
4778
|
+
"delete-design-token",
|
|
4779
|
+
"--token-type",
|
|
4780
|
+
kind,
|
|
4781
|
+
"--id",
|
|
4782
|
+
String(id ?? ""),
|
|
4783
|
+
...p,
|
|
4784
|
+
];
|
|
4785
|
+
return callEditorAction(project_root, args);
|
|
4786
|
+
});
|
|
3144
4787
|
// --- Start server ---
|
|
3145
4788
|
async function main() {
|
|
3146
4789
|
const transport = new StdioServerTransport();
|