@ikas/code-components-mcp 1.4.0-beta.2 → 1.4.0-beta.200
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 +208 -78
- package/data/migration.json +196 -25
- package/data/section-templates/account-info-section/children/AccountFavorites/ikas-config-snippet.json +16 -3
- package/data/section-templates/account-info-section/children/AccountOrders/index.tsx +1 -1
- package/data/section-templates/account-info-section/ikas-config-snippet.json +24 -5
- package/data/section-templates/add-to-cart/ikas-config-snippet.json +17 -0
- package/data/section-templates/add-to-cart/index.tsx +16 -2
- package/data/section-templates/add-to-cart/sub-components/QuantitySelector/index.tsx +11 -3
- package/data/section-templates/add-to-cart/utils/cartLimits.ts +24 -0
- package/data/section-templates/blog-home-section/ikas-config-snippet.json +2 -0
- package/data/section-templates/blog-home-section/index.tsx +2 -1
- package/data/section-templates/blog-post-section/ikas-config-snippet.json +2 -0
- package/data/section-templates/blog-post-section/index.tsx +3 -2
- package/data/section-templates/bundle-products/hooks/useBundleProducts.ts +7 -6
- package/data/section-templates/bundle-products/ikas-config-snippet.json +20 -0
- package/data/section-templates/cart-section/components/EmptyState/index.tsx +1 -1
- package/data/section-templates/cart-section/sub-components/CartItem/index.tsx +66 -24
- package/data/section-templates/cart-section/sub-components/CartItem/styles.css +14 -0
- package/data/section-templates/cart-section/sub-components/QuantitySelector/index.tsx +11 -3
- package/data/section-templates/cart-section/utils/cartLimits.ts +24 -0
- package/data/section-templates/cart-section/utils/cartLineLimits.ts +100 -0
- package/data/section-templates/category-images-section/children/CategoryImageItem/ikas-config-snippet.json +4 -0
- package/data/section-templates/category-images-section/ikas-config-snippet.json +16 -1
- package/data/section-templates/category-list-section/children/CardProductName/ikas-config-snippet.json +2 -1
- package/data/section-templates/category-list-section/ikas-config-snippet.json +24 -3
- package/data/section-templates/category-list-section/index.tsx +2 -1
- package/data/section-templates/component-renderer/additional/ProductDetail/index.tsx +5 -1
- package/data/section-templates/component-renderer/ikas-config-snippet.json +16 -3
- package/data/section-templates/favorites/ikas-config-snippet.json +18 -1
- package/data/section-templates/features-section/ikas-config-snippet.json +14 -1
- package/data/section-templates/footer-section/ikas-config-snippet.json +16 -1
- package/data/section-templates/forgot-password-section/components/ForgotPasswordForm/index.tsx +1 -1
- package/data/section-templates/header-section/children/Announcements/ikas-config-snippet.json +8 -1
- package/data/section-templates/header-section/children/CookieBar/ikas-config-snippet.json +3 -0
- package/data/section-templates/header-section/children/Navbar/components/CartSidebar/index.tsx +2 -1
- package/data/section-templates/header-section/children/Navbar/components/SearchModal/index.tsx +2 -1
- package/data/section-templates/header-section/children/Navbar/ikas-config-snippet.json +30 -3
- package/data/section-templates/header-section/children/Navbar/index.tsx +6 -5
- package/data/section-templates/header-section/ikas-config-snippet.json +16 -3
- package/data/section-templates/header-section/sub-components/CartItem/index.tsx +66 -24
- package/data/section-templates/header-section/sub-components/CartItem/styles.css +14 -0
- package/data/section-templates/header-section/sub-components/QuantitySelector/index.tsx +11 -3
- package/data/section-templates/header-section/utils/cartLimits.ts +24 -0
- package/data/section-templates/header-section/utils/cartLineLimits.ts +100 -0
- package/data/section-templates/hero-slider-section/children/HeroSliderItem/ikas-config-snippet.json +18 -0
- package/data/section-templates/hero-slider-section/children/HeroSliderItem/index.tsx +12 -7
- package/data/section-templates/hero-slider-section/ikas-config-snippet.json +14 -1
- package/data/section-templates/image-handling/ikas-config-snippet.json +66 -13
- package/data/section-templates/image-handling/index.tsx +5 -1
- package/data/section-templates/login-section/ikas-config-snippet.json +2 -0
- package/data/section-templates/navigation/components/CartSidebar/index.tsx +2 -1
- package/data/section-templates/navigation/components/SearchModal/index.tsx +2 -1
- package/data/section-templates/navigation/ikas-config-snippet.json +30 -3
- package/data/section-templates/navigation/index.tsx +6 -5
- package/data/section-templates/navigation/sub-components/CartItem/index.tsx +66 -24
- package/data/section-templates/navigation/sub-components/CartItem/styles.css +14 -0
- package/data/section-templates/navigation/sub-components/QuantitySelector/index.tsx +11 -3
- package/data/section-templates/navigation/utils/cartLimits.ts +24 -0
- package/data/section-templates/navigation/utils/cartLineLimits.ts +100 -0
- package/data/section-templates/product-detail-section/children/ProductDetailAddToCart/ikas-config-snippet.json +17 -0
- package/data/section-templates/product-detail-section/children/ProductDetailAddToCart/index.tsx +16 -2
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/components/BackInStockForm/index.tsx +92 -0
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/ikas-config-snippet.json +175 -0
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/index.tsx +141 -0
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/styles.css +47 -0
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/types.ts +25 -0
- package/data/section-templates/product-detail-section/children/ProductDetailBundleFurniture/ikas-config-snippet.json +18 -0
- package/data/section-templates/product-detail-section/children/ProductDetailBundleProduct/ikas-config-snippet.json +20 -0
- package/data/section-templates/product-detail-section/children/ProductDetailDescription/ikas-config-snippet.json +28 -1
- package/data/section-templates/product-detail-section/children/ProductDetailFeatures/ikas-config-snippet.json +30 -1
- package/data/section-templates/product-detail-section/children/ProductDetailNameFavorite/ikas-config-snippet.json +18 -1
- package/data/section-templates/product-detail-section/children/ProductDetailOffer/ikas-config-snippet.json +20 -0
- package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/ikas-config-snippet.json +16 -0
- package/data/section-templates/product-detail-section/children/ProductDetailPrices/ikas-config-snippet.json +16 -0
- package/data/section-templates/product-detail-section/children/ProductDetailProductGroup/ikas-config-snippet.json +16 -0
- package/data/section-templates/product-detail-section/children/ProductDetailSku/ikas-config-snippet.json +16 -0
- package/data/section-templates/product-detail-section/children/ProductDetailVariant/ikas-config-snippet.json +17 -0
- package/data/section-templates/product-detail-section/hooks/useBundleProducts.ts +7 -6
- package/data/section-templates/product-detail-section/ikas-config-snippet.json +66 -13
- package/data/section-templates/product-detail-section/index.tsx +5 -1
- package/data/section-templates/product-detail-section/sub-components/Modal/index.tsx +120 -0
- package/data/section-templates/product-detail-section/sub-components/Modal/styles.css +70 -0
- package/data/section-templates/product-detail-section/sub-components/QuantitySelector/index.tsx +11 -3
- package/data/section-templates/product-detail-section/utils/cartLimits.ts +24 -0
- package/data/section-templates/product-pricing/ikas-config-snippet.json +16 -0
- package/data/section-templates/product-slider-section/children/CardProductName/ikas-config-snippet.json +2 -1
- package/data/section-templates/product-slider-section/ikas-config-snippet.json +20 -3
- package/data/section-templates/register-section/ikas-config-snippet.json +2 -0
- package/data/section-templates/variant-selection/ikas-config-snippet.json +17 -0
- package/data/storefront-api.json +819 -1443
- package/data/storefront-types.json +9644 -6132
- package/dist/index.js +2978 -347
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { execFile } from "child_process";
|
|
4
5
|
import * as fs from "fs";
|
|
6
|
+
import * as os from "os";
|
|
5
7
|
import * as path from "path";
|
|
6
8
|
import { fileURLToPath } from "url";
|
|
7
9
|
import { z } from "zod";
|
|
@@ -15,6 +17,59 @@ function loadJsonFile(relativePath) {
|
|
|
15
17
|
}
|
|
16
18
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
17
19
|
}
|
|
20
|
+
// Resolve theme.json from either raw string or absolute file path.
|
|
21
|
+
// Exactly one of the two must be provided.
|
|
22
|
+
function resolveThemeJson(themeJson, themeJsonPath) {
|
|
23
|
+
const hasInline = typeof themeJson === "string" && themeJson.length > 0;
|
|
24
|
+
const hasPath = typeof themeJsonPath === "string" && themeJsonPath.length > 0;
|
|
25
|
+
if (hasInline === hasPath) {
|
|
26
|
+
throw new Error("Provide exactly one of `theme_json` (raw JSON string) or `theme_json_path` (absolute path to theme.json).");
|
|
27
|
+
}
|
|
28
|
+
let raw;
|
|
29
|
+
if (hasPath) {
|
|
30
|
+
if (!path.isAbsolute(themeJsonPath)) {
|
|
31
|
+
throw new Error(`theme_json_path must be absolute: ${themeJsonPath}`);
|
|
32
|
+
}
|
|
33
|
+
if (!fs.existsSync(themeJsonPath)) {
|
|
34
|
+
throw new Error(`theme_json_path not found: ${themeJsonPath}`);
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
raw = fs.readFileSync(themeJsonPath, "utf-8");
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
throw new Error(`Failed to read theme_json_path "${themeJsonPath}": ${e.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
raw = themeJson;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(raw);
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
throw new Error(`Invalid JSON in theme.json: ${e.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Atomic file write: temp file in same dir, then rename. Same-fs rename is atomic on POSIX.
|
|
54
|
+
function writeFileAtomic(targetPath, content) {
|
|
55
|
+
const dir = path.dirname(targetPath);
|
|
56
|
+
const base = path.basename(targetPath);
|
|
57
|
+
const tmp = path.join(dir, `.${base}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
|
|
58
|
+
try {
|
|
59
|
+
fs.writeFileSync(tmp, content, "utf-8");
|
|
60
|
+
fs.renameSync(tmp, targetPath);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
try {
|
|
64
|
+
if (fs.existsSync(tmp))
|
|
65
|
+
fs.unlinkSync(tmp);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// ignore cleanup failure
|
|
69
|
+
}
|
|
70
|
+
throw e;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
18
73
|
// Try multiple paths for storefront data (generated output or local data dir)
|
|
19
74
|
function loadStorefrontData() {
|
|
20
75
|
const paths = [
|
|
@@ -43,9 +98,16 @@ function loadStorefrontTypes() {
|
|
|
43
98
|
}
|
|
44
99
|
return null;
|
|
45
100
|
}
|
|
46
|
-
const SUBTREE_KINDS = [
|
|
101
|
+
const SUBTREE_KINDS = [
|
|
102
|
+
"children",
|
|
103
|
+
"components",
|
|
104
|
+
"sub-components",
|
|
105
|
+
];
|
|
47
106
|
function normalizeName(value) {
|
|
48
|
-
return value
|
|
107
|
+
return value
|
|
108
|
+
.trim()
|
|
109
|
+
.replace(/^`+|`+$/g, "")
|
|
110
|
+
.trim();
|
|
49
111
|
}
|
|
50
112
|
const storefrontData = loadStorefrontData();
|
|
51
113
|
const frameworkData = loadJsonFile("../data/framework.json");
|
|
@@ -206,7 +268,11 @@ function searchMigrationTopics(query) {
|
|
|
206
268
|
const descScore = matchScore(topic.description, query) * 2;
|
|
207
269
|
const contentScore = matchScore(topic.content, query);
|
|
208
270
|
const tagScore = topic.tags.some((t) => matchScore(t, query) > 0) ? 5 : 0;
|
|
209
|
-
return {
|
|
271
|
+
return {
|
|
272
|
+
key,
|
|
273
|
+
topic,
|
|
274
|
+
score: titleScore + descScore + contentScore + tagScore,
|
|
275
|
+
};
|
|
210
276
|
})
|
|
211
277
|
.filter((item) => item.score > 0)
|
|
212
278
|
.sort((a, b) => b.score - a.score);
|
|
@@ -233,7 +299,7 @@ function analyzeOldTheme(themeJson) {
|
|
|
233
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`);
|
|
234
300
|
parts.push(`## Summary Statistics\n`);
|
|
235
301
|
parts.push(`- **Components:** ${components.length}`);
|
|
236
|
-
parts.push(`- **Custom Data Definitions:** ${customData.filter(cd => cd.isRoot).length}`);
|
|
302
|
+
parts.push(`- **Custom Data Definitions:** ${customData.filter((cd) => cd.isRoot).length}`);
|
|
237
303
|
parts.push(`- **Prop Groups:** ${groups.length}`);
|
|
238
304
|
// Component analysis
|
|
239
305
|
parts.push(`\n## Components (${components.length})\n`);
|
|
@@ -272,7 +338,11 @@ function analyzeOldTheme(themeJson) {
|
|
|
272
338
|
const typesSummary = Object.entries(propTypeCounts)
|
|
273
339
|
.map(([t, c]) => `${t}×${c}`)
|
|
274
340
|
.join(", ");
|
|
275
|
-
const headerFooter = comp.isHeader
|
|
341
|
+
const headerFooter = comp.isHeader
|
|
342
|
+
? " [HEADER]"
|
|
343
|
+
: comp.isFooter
|
|
344
|
+
? " [FOOTER]"
|
|
345
|
+
: "";
|
|
276
346
|
parts.push(`### ${comp.displayName || comp.dir || comp.id}${headerFooter}`);
|
|
277
347
|
parts.push(`- **Dir:** \`${comp.dir || "?"}\` | **Props:** ${props.length} (${typesSummary})`);
|
|
278
348
|
parts.push(`- **Recommended new type:** section`);
|
|
@@ -287,7 +357,7 @@ function analyzeOldTheme(themeJson) {
|
|
|
287
357
|
parts.push("");
|
|
288
358
|
}
|
|
289
359
|
// Custom data analysis
|
|
290
|
-
const rootCustomData = customData.filter(cd => cd.isRoot);
|
|
360
|
+
const rootCustomData = customData.filter((cd) => cd.isRoot);
|
|
291
361
|
if (rootCustomData.length > 0) {
|
|
292
362
|
parts.push(`\n## Custom Data Definitions (${rootCustomData.length})\n`);
|
|
293
363
|
for (const cd of rootCustomData) {
|
|
@@ -297,7 +367,9 @@ function analyzeOldTheme(themeJson) {
|
|
|
297
367
|
const describeNested = (items, indent) => {
|
|
298
368
|
const lines = [];
|
|
299
369
|
for (const item of items) {
|
|
300
|
-
const key = item.key
|
|
370
|
+
const key = item.key
|
|
371
|
+
? `\`${item.key}\``
|
|
372
|
+
: item.typescriptName || item.name || "unnamed";
|
|
301
373
|
lines.push(`${indent}- ${key}: ${item.type}${item.isRequired ? " (required)" : ""}`);
|
|
302
374
|
if (item.nestedData && item.nestedData.length > 0) {
|
|
303
375
|
lines.push(...describeNested(item.nestedData, indent + " "));
|
|
@@ -309,7 +381,7 @@ function analyzeOldTheme(themeJson) {
|
|
|
309
381
|
parts.push(...describeNested(cd.nestedData, " "));
|
|
310
382
|
}
|
|
311
383
|
if (cd.enumOptions && cd.enumOptions.length > 0) {
|
|
312
|
-
parts.push(`- **Enum options:** ${cd.enumOptions.map(o => `"${o.value}"`).join(", ")}`);
|
|
384
|
+
parts.push(`- **Enum options:** ${cd.enumOptions.map((o) => `"${o.value}"`).join(", ")}`);
|
|
313
385
|
}
|
|
314
386
|
// Find which components reference this customData
|
|
315
387
|
const referencingComponents = [];
|
|
@@ -391,7 +463,9 @@ function scanSharedSubcomponents(sourceDir) {
|
|
|
391
463
|
const walk = (dir) => {
|
|
392
464
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
393
465
|
if (entry.isDirectory()) {
|
|
394
|
-
if (entry.name === "node_modules" ||
|
|
466
|
+
if (entry.name === "node_modules" ||
|
|
467
|
+
entry.name === "__generated__" ||
|
|
468
|
+
entry.name.startsWith("."))
|
|
395
469
|
continue;
|
|
396
470
|
walk(path.join(dir, entry.name));
|
|
397
471
|
}
|
|
@@ -426,10 +500,14 @@ function scanSharedSubcomponents(sourceDir) {
|
|
|
426
500
|
if (!importPath.startsWith("."))
|
|
427
501
|
continue;
|
|
428
502
|
// Skip imports of generated types, utils, hooks
|
|
429
|
-
if (importPath.includes("__generated__") ||
|
|
503
|
+
if (importPath.includes("__generated__") ||
|
|
504
|
+
importPath.includes("/utils") ||
|
|
505
|
+
importPath.includes("/hooks"))
|
|
430
506
|
continue;
|
|
431
507
|
// Extract base name from path
|
|
432
|
-
const pathSegments = importPath
|
|
508
|
+
const pathSegments = importPath
|
|
509
|
+
.split("/")
|
|
510
|
+
.filter((s) => s && s !== "." && s !== "..");
|
|
433
511
|
if (pathSegments.length === 0)
|
|
434
512
|
continue;
|
|
435
513
|
const lastSegment = pathSegments[pathSegments.length - 1];
|
|
@@ -440,7 +518,10 @@ function scanSharedSubcomponents(sourceDir) {
|
|
|
440
518
|
continue;
|
|
441
519
|
seenInFile.add(key);
|
|
442
520
|
if (!importUsage.has(key)) {
|
|
443
|
-
importUsage.set(key, {
|
|
521
|
+
importUsage.set(key, {
|
|
522
|
+
usingComponents: new Set(),
|
|
523
|
+
rawImportPath: importPath,
|
|
524
|
+
});
|
|
444
525
|
}
|
|
445
526
|
importUsage.get(key).usingComponents.add(componentDir);
|
|
446
527
|
}
|
|
@@ -449,7 +530,7 @@ function scanSharedSubcomponents(sourceDir) {
|
|
|
449
530
|
const shared = [];
|
|
450
531
|
for (const [name, { usingComponents, rawImportPath }] of importUsage) {
|
|
451
532
|
// Don't flag the component itself (e.g., Navbar imports from ../Navbar/something)
|
|
452
|
-
const users = [...usingComponents].filter(c => c !== name);
|
|
533
|
+
const users = [...usingComponents].filter((c) => c !== name);
|
|
453
534
|
if (users.length >= 3) {
|
|
454
535
|
shared.push({ name, usedBy: users.sort(), importPaths: [rawImportPath] });
|
|
455
536
|
}
|
|
@@ -466,7 +547,7 @@ function toKebabCase(s) {
|
|
|
466
547
|
}
|
|
467
548
|
function classifyComplexity(comp, customDataMap) {
|
|
468
549
|
const props = comp.props || [];
|
|
469
|
-
const customCount = props.filter(p => p.type === "CUSTOM").length;
|
|
550
|
+
const customCount = props.filter((p) => p.type === "CUSTOM").length;
|
|
470
551
|
if (customCount === 0 && props.length < 10)
|
|
471
552
|
return "simple";
|
|
472
553
|
// Check for deeply nested CUSTOM (customData referencing another customData)
|
|
@@ -477,7 +558,9 @@ function classifyComplexity(comp, customDataMap) {
|
|
|
477
558
|
if (cd?.nestedData) {
|
|
478
559
|
const hasNested = (items) => {
|
|
479
560
|
for (const item of items) {
|
|
480
|
-
if (item.type === "DYNAMIC_LIST" ||
|
|
561
|
+
if (item.type === "DYNAMIC_LIST" ||
|
|
562
|
+
item.type === "STATIC_LIST" ||
|
|
563
|
+
item.customDataId)
|
|
481
564
|
return true;
|
|
482
565
|
if (item.nestedData && hasNested(item.nestedData))
|
|
483
566
|
return true;
|
|
@@ -506,18 +589,23 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
506
589
|
}
|
|
507
590
|
const sharedSubs = oldSourceDir ? scanSharedSubcomponents(oldSourceDir) : [];
|
|
508
591
|
const parts = [];
|
|
509
|
-
parts.push(`# Theme Migration Plan
|
|
592
|
+
parts.push(`# Theme Migration Plan — \`${projectName}\``);
|
|
510
593
|
parts.push("");
|
|
511
594
|
parts.push(`**Generated:** ${new Date().toISOString().slice(0, 10)}`);
|
|
512
|
-
parts.push(`**
|
|
513
|
-
parts.push(`**Source:** ${components.length} old components, ${customData.filter(cd => cd.isRoot).length} custom data types, ${(theme.pages || []).length} pages`);
|
|
514
|
-
parts.push("");
|
|
515
|
-
parts.push(`This file is the **source of truth** for the migration. All sessions must read it before starting work.`);
|
|
595
|
+
parts.push(`**Source:** ${components.length} old components, ${customData.filter((cd) => cd.isRoot).length} custom data types, ${(theme.pages || []).length} pages`);
|
|
516
596
|
parts.push("");
|
|
517
|
-
parts.push(
|
|
518
|
-
parts.push(
|
|
519
|
-
parts.push(
|
|
520
|
-
parts.push(
|
|
597
|
+
parts.push(`> ## READ THIS FIRST`);
|
|
598
|
+
parts.push(`>`);
|
|
599
|
+
parts.push(`> This file is your responsibility from here. The MCP wrote this initial scaffold **once**. You own all updates — no MCP tool will modify this file again.`);
|
|
600
|
+
parts.push(`>`);
|
|
601
|
+
parts.push(`> 1. **Tick checkboxes** as you finish work. Use \`[~]\` for in-progress and \`[x]\` for done. Edit the file directly with your file-editing tools.`);
|
|
602
|
+
parts.push(`> 2. **theme.json is incomplete.** Atomic components (Button, Input, Card, icons, etc.) often live only in \`src/\` and are NOT referenced from theme.json. Before you start section migration, scan the old source directory and ADD entries to this file for anything the initial scan missed — list them under \`## Source Code Analysis\` and add shared ones to \`### Shared Sub-Components\`.`);
|
|
603
|
+
parts.push(`> 3. **Custom data types are NOT pre-converted.** For each customData entry used by a section, decide enum-vs-component when you migrate that section. Log every decision in \`## Custom Data Decisions\` with reasoning. See \`get_migration_guide("custom-data-conversion")\` for the heuristic.`);
|
|
604
|
+
parts.push(`> 4. **Preserve feature parity. Do NOT silently simplify or drop features.** If an old prop has richer fields than a new built-in prop type can carry (e.g. a navigation link with a per-link image), the answer is to build a child component — never to flatten and "add it later." Later doesn't come. If the user explicitly wants a feature removed, log that as an explicit decision in \`## Notes\`; otherwise migrate to functional parity.`);
|
|
605
|
+
parts.push(`> 5. **Per-section work:** call \`get_section_migration_plan({theme_json_path, section_name, project_name: "${projectName}"})\` for each section. The MCP returns concrete CLI commands and flags any customData-referencing props with a "Decide: enum or component?" callout.`);
|
|
606
|
+
parts.push(`> 6. **If you discover new shared sub-components mid-migration**, add them under \`### Shared Sub-Components\` and any notes under \`## Notes\`.`);
|
|
607
|
+
parts.push(`>`);
|
|
608
|
+
parts.push(`> Status legend: \`[ ]\` not started · \`[~]\` in progress · \`[x]\` complete.`);
|
|
521
609
|
parts.push("");
|
|
522
610
|
parts.push(`## Foundation`);
|
|
523
611
|
parts.push("");
|
|
@@ -537,27 +625,9 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
537
625
|
parts.push(`- [ ] ${settings.fontFamily.name} (weights: ${settings.fontFamily.variants?.join(", ") || "default"})`);
|
|
538
626
|
parts.push("");
|
|
539
627
|
}
|
|
540
|
-
// Custom Enums
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
parts.push(`### Custom Enums (run these BEFORE any \`config add-component\`)`);
|
|
544
|
-
parts.push("");
|
|
545
|
-
for (const cd of enumCustomData) {
|
|
546
|
-
const enumName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
|
|
547
|
-
const options = (cd.enumOptions || []).reduce((acc, o) => {
|
|
548
|
-
if (o.displayName && o.value)
|
|
549
|
-
acc[o.displayName] = o.value;
|
|
550
|
-
return acc;
|
|
551
|
-
}, {});
|
|
552
|
-
const optionsStr = JSON.stringify(options);
|
|
553
|
-
parts.push(`- [ ] **${enumName}**`);
|
|
554
|
-
parts.push(` \`\`\`bash`);
|
|
555
|
-
parts.push(` npx ikas-component config add-enum --name "${enumName}" --options '${optionsStr}'`);
|
|
556
|
-
parts.push(` \`\`\``);
|
|
557
|
-
parts.push(` Save the returned \`enumId\` and update this file with: \`enumId: <id>\``);
|
|
558
|
-
}
|
|
559
|
-
parts.push("");
|
|
560
|
-
}
|
|
628
|
+
// (Custom Enums foundation subsection intentionally removed — see ## Custom Data Types below.
|
|
629
|
+
// Old customData entries are NOT pre-migrated as enums; each is a per-section decision.
|
|
630
|
+
// See get_migration_guide("custom-data-conversion") for the enum-vs-component heuristic.)
|
|
561
631
|
// Shared sub-components
|
|
562
632
|
if (sharedSubs.length > 0) {
|
|
563
633
|
parts.push(`### Shared Sub-Components (→ \`src/sub-components/\`)`);
|
|
@@ -581,10 +651,61 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
581
651
|
parts.push(`_Old source directory not provided — shared sub-components cannot be detected automatically. If you have access to the old source, call \`plan_migration\` again with \`old_source_dir\` to populate this section._`);
|
|
582
652
|
parts.push("");
|
|
583
653
|
}
|
|
654
|
+
parts.push(`> **This scan is partial.** \`scanSharedSubcomponents\` only detects relative imports used by **3 or more** old components. Atomic components used by fewer sections (often Button/Input/Card/icon primitives) will be missed. Scan \`src/\` yourself and add any others under \`## Source Code Analysis\` below.`);
|
|
655
|
+
parts.push("");
|
|
656
|
+
// Custom Data Types — deferred decisions (reference list, not checkboxes)
|
|
657
|
+
const rootCustomData = customData.filter((cd) => cd.isRoot);
|
|
658
|
+
if (rootCustomData.length > 0) {
|
|
659
|
+
// Build a usage map: which sections reference each customData type
|
|
660
|
+
const usageByCustomDataId = new Map();
|
|
661
|
+
for (const comp of components) {
|
|
662
|
+
const sectionName = comp.displayName || comp.dir || comp.id || "?";
|
|
663
|
+
for (const p of comp.props || []) {
|
|
664
|
+
if (p.type === "CUSTOM" && p.customDataId) {
|
|
665
|
+
const list = usageByCustomDataId.get(p.customDataId) || [];
|
|
666
|
+
if (!list.includes(sectionName))
|
|
667
|
+
list.push(sectionName);
|
|
668
|
+
usageByCustomDataId.set(p.customDataId, list);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
parts.push(`### Custom Data Types — decide during section migration`);
|
|
673
|
+
parts.push("");
|
|
674
|
+
parts.push(`Each entry below is an OLD \`theme.customData[]\` type. Do **not** pre-migrate these as enums. When you migrate the section that uses one, decide:`);
|
|
675
|
+
parts.push(`- **Flat scalar set** (e.g. \`"left" | "right" | "center"\`) → new-system **enum prop** via \`config add-enum\`.`);
|
|
676
|
+
parts.push(`- **Structured record** (e.g. \`{image, link, title}\`, repeated in a list) → new-system **component** via \`config add-component\` + COMPONENT_LIST wiring on the parent.`);
|
|
677
|
+
parts.push("");
|
|
678
|
+
parts.push(`Log every decision in \`## Custom Data Decisions\` below. See \`get_migration_guide("custom-data-conversion")\` for the full heuristic and worked examples (Position, Slide, MenuItem).`);
|
|
679
|
+
parts.push("");
|
|
680
|
+
for (const cd of rootCustomData) {
|
|
681
|
+
const cdName = cd.typescriptName || cd.name || cd.id || "Unknown";
|
|
682
|
+
const cdType = cd.type || "?";
|
|
683
|
+
let shape = "";
|
|
684
|
+
if (cd.type === "ENUM") {
|
|
685
|
+
const opts = (cd.enumOptions || [])
|
|
686
|
+
.map((o) => o.value || o.displayName)
|
|
687
|
+
.filter(Boolean);
|
|
688
|
+
shape = ` — shape: \`enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}\``;
|
|
689
|
+
}
|
|
690
|
+
else if (cd.nestedData && cd.nestedData.length > 0) {
|
|
691
|
+
const first = cd.nestedData[0];
|
|
692
|
+
const fields = (first?.nestedData || cd.nestedData || [])
|
|
693
|
+
.map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`)
|
|
694
|
+
.slice(0, 8);
|
|
695
|
+
shape = ` — shape: \`{${fields.join(", ")}}\``;
|
|
696
|
+
}
|
|
697
|
+
const usedBy = cd.id ? usageByCustomDataId.get(cd.id) || [] : [];
|
|
698
|
+
const usedByStr = usedBy.length > 0
|
|
699
|
+
? ` — used by: ${usedBy.slice(0, 6).join(", ")}${usedBy.length > 6 ? `, +${usedBy.length - 6} more` : ""}`
|
|
700
|
+
: ` — _not directly referenced by any section's props (may be nested inside another customData)_`;
|
|
701
|
+
parts.push(`- \`${cdName}\` (${cdType})${shape}${usedByStr}`);
|
|
702
|
+
}
|
|
703
|
+
parts.push("");
|
|
704
|
+
}
|
|
584
705
|
// Sections queue
|
|
585
706
|
parts.push(`## Sections`);
|
|
586
707
|
parts.push("");
|
|
587
|
-
parts.push(`
|
|
708
|
+
parts.push(`Component **names** are listed below. The CLI auto-generates opaque \`componentId\`s when you run \`config add-component\` — capture each id from the JSON response and use it (or look it up later with \`config list\`) when wiring \`filteredComponentIds\`.`);
|
|
588
709
|
parts.push("");
|
|
589
710
|
const buckets = {
|
|
590
711
|
simple: [],
|
|
@@ -610,7 +731,11 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
610
731
|
const oldName = comp.displayName || comp.dir || comp.id || "Unknown";
|
|
611
732
|
const kebabName = toKebabCase(comp.dir || comp.displayName || comp.id || "unknown");
|
|
612
733
|
const newId = `${projectName}-${kebabName}`;
|
|
613
|
-
const headerFooter = comp.isHeader
|
|
734
|
+
const headerFooter = comp.isHeader
|
|
735
|
+
? " **[HEADER]**"
|
|
736
|
+
: comp.isFooter
|
|
737
|
+
? " **[FOOTER]**"
|
|
738
|
+
: "";
|
|
614
739
|
const propCount = (comp.props || []).length;
|
|
615
740
|
// Detect children from CUSTOM DYNAMIC_LIST props
|
|
616
741
|
const children = [];
|
|
@@ -620,11 +745,14 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
620
745
|
if (cd && (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST")) {
|
|
621
746
|
const itemObj = cd.nestedData?.[0];
|
|
622
747
|
if (itemObj) {
|
|
623
|
-
const childName = itemObj.typescriptName ||
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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);
|
|
755
|
+
children.push({ propName: p.name || "?", childName, fields });
|
|
628
756
|
}
|
|
629
757
|
}
|
|
630
758
|
}
|
|
@@ -634,65 +762,73 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
634
762
|
if (children.length > 0) {
|
|
635
763
|
parts.push(` - Children:`);
|
|
636
764
|
for (const ch of children) {
|
|
637
|
-
parts.push(` - [ ] \`${ch.
|
|
765
|
+
parts.push(` - [ ] \`${ch.childName}\` — for prop \`${ch.propName}\` — fields: ${ch.fields.join(", ")}`);
|
|
638
766
|
}
|
|
639
767
|
}
|
|
640
768
|
}
|
|
641
769
|
parts.push("");
|
|
642
770
|
}
|
|
643
|
-
//
|
|
644
|
-
parts.push(`##
|
|
771
|
+
// Source Code Analysis — placeholder for the LLM to fill in
|
|
772
|
+
parts.push(`## Source Code Analysis`);
|
|
645
773
|
parts.push("");
|
|
646
|
-
parts.push(
|
|
774
|
+
parts.push(`> **Before starting section migration**, scan the old \`src/\` for components NOT listed above. theme.json does not see atomic primitives — buttons, inputs, cards, icon wrappers, layout helpers, etc. — that are imported by sections but never appear as theme components. Add them here, then decide which become shared sub-components vs which collapse into section bodies.`);
|
|
647
775
|
parts.push("");
|
|
648
|
-
parts.push(
|
|
776
|
+
parts.push(`<!-- Example: \`- Button (src/atoms/Button/) — used by ~all sections → promote to Shared Sub-Component\` -->`);
|
|
777
|
+
parts.push("");
|
|
778
|
+
// Custom Data Decisions — append-only log the LLM fills as it makes per-section decisions
|
|
779
|
+
parts.push(`## Custom Data Decisions`);
|
|
649
780
|
parts.push("");
|
|
650
|
-
|
|
651
|
-
parts.push(`## Per-Section Usage`);
|
|
781
|
+
parts.push(`> Log each customData enum-vs-component decision here as you encounter it during section migration. Format: \`- \\\`<CustomDataName>\\\` → enum/component \\\`<target name>\\\` (YYYY-MM-DD) — reasoning\`.`);
|
|
652
782
|
parts.push("");
|
|
653
|
-
parts.push(
|
|
783
|
+
parts.push(`<!-- Example: \`- SlideData → component \\\`hero-slide\\\` (2026-05-11) — structured record {image,link,title}; LIST_OF_LINK would drop the image\` -->`);
|
|
654
784
|
parts.push("");
|
|
655
|
-
|
|
656
|
-
parts.push(
|
|
657
|
-
parts.push(
|
|
658
|
-
parts.push(`
|
|
659
|
-
parts.push(
|
|
660
|
-
parts.push(
|
|
785
|
+
// Known Environmental Issues (agents fill in during work)
|
|
786
|
+
parts.push(`## Known Environmental Issues`);
|
|
787
|
+
parts.push("");
|
|
788
|
+
parts.push(`_Record any non-component build/TS errors here so future sessions don't waste time diagnosing them._`);
|
|
789
|
+
parts.push("");
|
|
790
|
+
parts.push(`- [ ] _(none recorded yet)_`);
|
|
661
791
|
parts.push("");
|
|
662
|
-
//
|
|
663
|
-
parts.push(`##
|
|
792
|
+
// Notes — append-only log for decisions not captured elsewhere
|
|
793
|
+
parts.push(`## Notes`);
|
|
664
794
|
parts.push("");
|
|
665
|
-
parts.push(`
|
|
795
|
+
parts.push(`_Append a bullet after completing work — library swaps, ad-hoc props, CLI folder-name oddities, user-approved feature drops, etc. Format: \`- [YYYY-MM-DD] <section-id>: <brief note>\`._`);
|
|
666
796
|
parts.push("");
|
|
667
|
-
parts.push(
|
|
668
|
-
parts.push(`<!-- \`- [2026-04-15] Foundation: built Input, SubmitButton, Modal, SectionHeader, StarRating. Skipped: GoogleCaptcha (needs investigation), BlogCard, Pagination (low priority).\` -->`);
|
|
669
|
-
parts.push(`<!-- \`- [2026-04-15] ${projectName}-footer: swapped react-hot-toast for inline status text; newsletter uses form-model pattern (getNewsletterSubscriptionForm). CLI created folder as "Footer/" as expected.\` -->`);
|
|
797
|
+
parts.push(`<!-- Example: \`- [2026-04-15] ${projectName}-footer: swapped react-hot-toast for inline status text; CLI created Footer/ as expected\` -->`);
|
|
670
798
|
parts.push("");
|
|
671
|
-
// Cross-references
|
|
799
|
+
// Cross-references — keep terse; LLM can call `get_migration_guide("list")` or `get_framework_guide("list")` for more
|
|
672
800
|
parts.push(`## Cross-References`);
|
|
673
801
|
parts.push("");
|
|
674
|
-
parts.push(
|
|
802
|
+
parts.push(`- \`get_migration_guide("iterative-workflow")\` — the full per-phase protocol`);
|
|
803
|
+
parts.push(`- \`get_migration_guide("custom-data-conversion")\` — enum-vs-component decisions`);
|
|
804
|
+
parts.push(`- \`get_migration_guide("prop-runtime-shapes")\` — runtime shapes & access patterns`);
|
|
805
|
+
parts.push(`- \`get_framework_guide("common-pitfalls")\` — gotchas & old→new property migrations`);
|
|
675
806
|
parts.push("");
|
|
676
|
-
parts.push(
|
|
677
|
-
parts.push(`- \`get_migration_guide("component-renderer-limitations")\` — critical constraints when using COMPONENT_LIST`);
|
|
678
|
-
parts.push(`- \`get_migration_guide("prop-runtime-shapes")\` — \`.data\` vs \`.links\`, \`.variant\` vs \`.product\`, etc.`);
|
|
679
|
-
parts.push(`- \`get_migration_guide("link-prop-decision-guide")\` — LINK vs LIST_OF_LINK vs COMPONENT_LIST`);
|
|
680
|
-
parts.push(`- \`get_migration_guide("library-replacements")\` — swiper, headlessui, tailwind, etc.`);
|
|
681
|
-
parts.push(`- \`get_migration_guide("react-to-preact")\` — observer rules, imports, IkasSlider removal`);
|
|
682
|
-
parts.push(`- \`get_framework_guide("component-renderer-patterns")\` — full IkasComponentRenderer usage`);
|
|
683
|
-
parts.push(`- \`get_framework_guide("common-pitfalls")\` — general gotchas`);
|
|
684
|
-
parts.push(`- \`get_framework_guide("navigation-patterns")\` — Router.navigate, Router.navigateToPage`);
|
|
685
|
-
parts.push(`- \`get_model_guide("<TypeName>")\` — IkasCart, IkasProduct, IkasCustomer shapes`);
|
|
807
|
+
parts.push(`Call \`get_migration_guide("list")\` or \`get_framework_guide("list")\` for the full topic catalog.`);
|
|
686
808
|
parts.push("");
|
|
687
809
|
return parts.join("\n");
|
|
688
810
|
}
|
|
689
811
|
// Known libraries we detect in old themes and want to flag for replacement
|
|
690
812
|
const KNOWN_LIBRARIES = [
|
|
691
|
-
"swiper",
|
|
692
|
-
"react
|
|
693
|
-
"react
|
|
694
|
-
"
|
|
695
|
-
"
|
|
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",
|
|
696
832
|
];
|
|
697
833
|
// Heuristic: member-access patterns on old storefront stores/singletons that likely need new-system equivalents
|
|
698
834
|
const OLD_STOREFRONT_CALL_REGEX = /\b(customerStore|cartStore|productStore|categoryStore|orderStore|searchStore|favoritesStore|i18nStore|Router|useStore)\.\w+/g;
|
|
@@ -711,7 +847,8 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
711
847
|
// Collect all .tsx/.ts files in the component dir
|
|
712
848
|
try {
|
|
713
849
|
for (const entry of fs.readdirSync(componentDir, { withFileTypes: true })) {
|
|
714
|
-
if (entry.isFile() &&
|
|
850
|
+
if (entry.isFile() &&
|
|
851
|
+
(entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
|
|
715
852
|
result.sourceFiles.push(path.join(componentDir, entry.name));
|
|
716
853
|
}
|
|
717
854
|
}
|
|
@@ -728,7 +865,18 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
728
865
|
const subCompSet = new Map();
|
|
729
866
|
const callSet = new Set();
|
|
730
867
|
// Packages we treat as "framework" and don't flag for replacement (but do note in reactPackageUsage)
|
|
731
|
-
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
|
+
]);
|
|
732
880
|
for (const file of result.sourceFiles) {
|
|
733
881
|
let content;
|
|
734
882
|
try {
|
|
@@ -742,7 +890,7 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
742
890
|
while ((m = importRegex.exec(content)) !== null) {
|
|
743
891
|
const p = m[1];
|
|
744
892
|
if (p.startsWith(".")) {
|
|
745
|
-
const segs = p.split("/").filter(s => s && s !== "." && s !== "..");
|
|
893
|
+
const segs = p.split("/").filter((s) => s && s !== "." && s !== "..");
|
|
746
894
|
const last = segs[segs.length - 1];
|
|
747
895
|
if (last && /^[A-Z]/.test(last) && !last.includes("__generated__")) {
|
|
748
896
|
subCompSet.set(last, p);
|
|
@@ -750,7 +898,9 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
750
898
|
}
|
|
751
899
|
else if (!p.startsWith("@ikas/")) {
|
|
752
900
|
// Classify: known library, react-family, or unknown-external
|
|
753
|
-
const base = p.startsWith("@")
|
|
901
|
+
const base = p.startsWith("@")
|
|
902
|
+
? p.split("/").slice(0, 2).join("/")
|
|
903
|
+
: p.split("/")[0];
|
|
754
904
|
if (REACT_PACKAGES.has(p) || REACT_PACKAGES.has(base)) {
|
|
755
905
|
reactSet.add(base);
|
|
756
906
|
}
|
|
@@ -813,7 +963,10 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
813
963
|
}
|
|
814
964
|
}
|
|
815
965
|
}
|
|
816
|
-
result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({
|
|
966
|
+
result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({
|
|
967
|
+
name,
|
|
968
|
+
path: p,
|
|
969
|
+
}));
|
|
817
970
|
result.importedLibraries = [...libSet].sort();
|
|
818
971
|
result.importedUnknownLibraries = [...unknownLibSet].sort();
|
|
819
972
|
result.reactPackageUsage = [...reactSet].sort();
|
|
@@ -829,34 +982,45 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
829
982
|
customDataMap.set(cd.id, cd);
|
|
830
983
|
}
|
|
831
984
|
// Find the component — try match by dir, displayName, id, or new-id
|
|
832
|
-
const target = components.find(c => {
|
|
985
|
+
const target = components.find((c) => {
|
|
833
986
|
if (!c)
|
|
834
987
|
return false;
|
|
835
|
-
if (c.dir === sectionName ||
|
|
988
|
+
if (c.dir === sectionName ||
|
|
989
|
+
c.displayName === sectionName ||
|
|
990
|
+
c.id === sectionName)
|
|
836
991
|
return true;
|
|
837
992
|
const kebab = toKebabCase(c.dir || c.displayName || c.id || "");
|
|
838
993
|
const newId = `${projectName}-${kebab}`;
|
|
839
994
|
return newId === sectionName;
|
|
840
995
|
});
|
|
841
996
|
if (!target) {
|
|
842
|
-
const available = components
|
|
997
|
+
const available = components
|
|
998
|
+
.map((c) => c.dir || c.displayName || c.id)
|
|
999
|
+
.filter(Boolean)
|
|
1000
|
+
.join(", ");
|
|
843
1001
|
return `Section "${sectionName}" not found in theme. Available: ${available}`;
|
|
844
1002
|
}
|
|
845
1003
|
const parts = [];
|
|
846
1004
|
const oldName = target.displayName || target.dir || target.id || "Unknown";
|
|
847
1005
|
const kebabName = toKebabCase(target.dir || target.displayName || target.id || "unknown");
|
|
848
1006
|
const sectionId = `${projectName}-${kebabName}`;
|
|
849
|
-
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("");
|
|
850
1012
|
// Scan the old source for imports, libraries, field usage
|
|
851
|
-
const propNames = (target.props || [])
|
|
852
|
-
|
|
1013
|
+
const propNames = (target.props || [])
|
|
1014
|
+
.map((p) => p.name || "")
|
|
1015
|
+
.filter(Boolean);
|
|
1016
|
+
const sourceScan = oldSourceDir && target.dir
|
|
853
1017
|
? scanSectionSource(path.join(oldSourceDir, target.dir), propNames)
|
|
854
1018
|
: null;
|
|
855
1019
|
parts.push(`# Section Migration Plan: ${oldName}`);
|
|
856
1020
|
parts.push("");
|
|
857
1021
|
parts.push(`**Old name:** \`${oldName}\` (dir: \`${target.dir || "?"}\`)`);
|
|
858
|
-
parts.push(`**New section
|
|
859
|
-
parts.push(`**
|
|
1022
|
+
parts.push(`**New section name:** \`${sectionPascal}\` (the CLI will assign an opaque \`componentId\` at write-time — capture it from \`config add-component\`'s JSON response)`);
|
|
1023
|
+
parts.push(`**Migration checkbox label:** \`${sectionId}\` (tracking identifier in \`MIGRATION.md\` only — not the runtime component id)`);
|
|
860
1024
|
if (target.isHeader)
|
|
861
1025
|
parts.push(`**Flags:** HEADER (\`isHeader: true\`)`);
|
|
862
1026
|
if (target.isFooter)
|
|
@@ -895,7 +1059,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
895
1059
|
}
|
|
896
1060
|
if (sourceScan.reactPackageUsage.length > 0) {
|
|
897
1061
|
parts.push("");
|
|
898
|
-
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).`);
|
|
899
1063
|
}
|
|
900
1064
|
if (sourceScan.oldStorefrontCalls.length > 0) {
|
|
901
1065
|
parts.push("");
|
|
@@ -941,7 +1105,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
941
1105
|
if (oldType === "SLIDER") {
|
|
942
1106
|
newType = "NUMBER";
|
|
943
1107
|
notes = `Was SLIDER(min=${p.sliderData?.min}, max=${p.sliderData?.max}) — replace \`.value\` access with direct number`;
|
|
944
|
-
const prop = {
|
|
1108
|
+
const prop = {
|
|
1109
|
+
name: newName,
|
|
1110
|
+
displayName: p.displayName || newName,
|
|
1111
|
+
type: "NUMBER",
|
|
1112
|
+
};
|
|
945
1113
|
if (p.isRequired)
|
|
946
1114
|
prop.required = true;
|
|
947
1115
|
parentPropsJson.push(prop);
|
|
@@ -949,7 +1117,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
949
1117
|
else if (oldType === "PRODUCT_DETAIL") {
|
|
950
1118
|
newType = "PRODUCT";
|
|
951
1119
|
notes = "Renamed — PRODUCT_DETAIL → PRODUCT";
|
|
952
|
-
const prop = {
|
|
1120
|
+
const prop = {
|
|
1121
|
+
name: newName,
|
|
1122
|
+
displayName: p.displayName || newName,
|
|
1123
|
+
type: "PRODUCT",
|
|
1124
|
+
};
|
|
953
1125
|
if (p.isRequired)
|
|
954
1126
|
prop.required = true;
|
|
955
1127
|
parentPropsJson.push(prop);
|
|
@@ -960,9 +1132,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
960
1132
|
if (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST") {
|
|
961
1133
|
// Child component needed
|
|
962
1134
|
const itemObj = cd.nestedData?.[0];
|
|
963
|
-
const childName = itemObj?.typescriptName ||
|
|
964
|
-
|
|
965
|
-
|
|
1135
|
+
const childName = itemObj?.typescriptName ||
|
|
1136
|
+
(itemObj?.name
|
|
1137
|
+
? itemObj.name.replace(/[^a-zA-Z0-9]/g, "")
|
|
1138
|
+
: `${sectionPascal}Item`);
|
|
966
1139
|
const childProps = [];
|
|
967
1140
|
const nestedWarnings = [];
|
|
968
1141
|
for (const f of (itemObj?.nestedData || [])) {
|
|
@@ -973,11 +1146,18 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
973
1146
|
fType = "NUMBER";
|
|
974
1147
|
else if (fType === "PRODUCT_DETAIL")
|
|
975
1148
|
fType = "PRODUCT";
|
|
976
|
-
else if (fType === "CUSTOM" ||
|
|
1149
|
+
else if (fType === "CUSTOM" ||
|
|
1150
|
+
fType === "DYNAMIC_LIST" ||
|
|
1151
|
+
fType === "STATIC_LIST" ||
|
|
1152
|
+
fType === "OBJECT") {
|
|
977
1153
|
nestedWarnings.push(`\`${f.key}\` (${fType})`);
|
|
978
1154
|
fType = "COMPONENT_LIST";
|
|
979
1155
|
}
|
|
980
|
-
const prop = {
|
|
1156
|
+
const prop = {
|
|
1157
|
+
name: f.key,
|
|
1158
|
+
displayName: f.name || f.key,
|
|
1159
|
+
type: fType,
|
|
1160
|
+
};
|
|
981
1161
|
if (f.isRequired)
|
|
982
1162
|
prop.required = true;
|
|
983
1163
|
childProps.push(prop);
|
|
@@ -989,26 +1169,29 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
989
1169
|
if (childProps.length === 0 && sourceScan?.propFieldUsage[oldName]) {
|
|
990
1170
|
const inferred = sourceScan.propFieldUsage[oldName];
|
|
991
1171
|
for (const fieldName of inferred) {
|
|
992
|
-
childProps.push({
|
|
1172
|
+
childProps.push({
|
|
1173
|
+
name: fieldName,
|
|
1174
|
+
displayName: fieldName,
|
|
1175
|
+
type: "TEXT",
|
|
1176
|
+
});
|
|
993
1177
|
}
|
|
994
1178
|
if (inferred.length > 0) {
|
|
995
|
-
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.`;
|
|
996
1180
|
}
|
|
997
1181
|
}
|
|
998
1182
|
children.push({
|
|
999
1183
|
propName: oldName,
|
|
1000
|
-
childId,
|
|
1001
1184
|
childName,
|
|
1002
1185
|
childPropsJson: childProps,
|
|
1003
1186
|
customDataName: cd.name || "unnamed",
|
|
1004
1187
|
});
|
|
1005
1188
|
newType = "COMPONENT_LIST";
|
|
1006
|
-
notes = `Was CUSTOM → ${cd.type} of ${itemObj?.typescriptName || "object"}. Create child component \`${
|
|
1189
|
+
notes = `Was CUSTOM → ${cd.type} of ${itemObj?.typescriptName || "object"}. Create child component \`${childName}\`; the CLI returns the opaque \`componentId\` in its JSON response — substitute it for the \`<id-of-${childName}>\` placeholder below.`;
|
|
1007
1190
|
const prop = {
|
|
1008
1191
|
name: newName,
|
|
1009
1192
|
displayName: p.displayName || newName,
|
|
1010
1193
|
type: "COMPONENT_LIST",
|
|
1011
|
-
filteredComponentIds: [
|
|
1194
|
+
filteredComponentIds: [`<id-of-${childName}>`],
|
|
1012
1195
|
};
|
|
1013
1196
|
parentPropsJson.push(prop);
|
|
1014
1197
|
}
|
|
@@ -1022,7 +1205,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1022
1205
|
let fType = f.type;
|
|
1023
1206
|
if (fType === "SLIDER")
|
|
1024
1207
|
fType = "NUMBER";
|
|
1025
|
-
const prop = {
|
|
1208
|
+
const prop = {
|
|
1209
|
+
name: f.key,
|
|
1210
|
+
displayName: f.name || f.key,
|
|
1211
|
+
type: fType,
|
|
1212
|
+
};
|
|
1026
1213
|
if (f.isRequired)
|
|
1027
1214
|
prop.required = true;
|
|
1028
1215
|
parentPropsJson.push(prop);
|
|
@@ -1037,7 +1224,8 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1037
1224
|
}
|
|
1038
1225
|
else if (cd.type === "ENUM") {
|
|
1039
1226
|
newType = "ENUM";
|
|
1040
|
-
const enumName = cd.typescriptName ||
|
|
1227
|
+
const enumName = cd.typescriptName ||
|
|
1228
|
+
(cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
|
|
1041
1229
|
const options = (cd.enumOptions || []).reduce((acc, o) => {
|
|
1042
1230
|
if (o.displayName && o.value)
|
|
1043
1231
|
acc[o.displayName] = o.value;
|
|
@@ -1045,7 +1233,12 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1045
1233
|
}, {});
|
|
1046
1234
|
enumsNeeded.push({ name: enumName, options });
|
|
1047
1235
|
notes = `Was CUSTOM (ENUM) — create enum \`${enumName}\` via \`config add-enum\` first, then reference its enumId here`;
|
|
1048
|
-
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
|
+
};
|
|
1049
1242
|
if (p.isRequired)
|
|
1050
1243
|
prop.required = true;
|
|
1051
1244
|
parentPropsJson.push(prop);
|
|
@@ -1054,7 +1247,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1054
1247
|
}
|
|
1055
1248
|
else {
|
|
1056
1249
|
// Direct mapping
|
|
1057
|
-
const prop = {
|
|
1250
|
+
const prop = {
|
|
1251
|
+
name: newName,
|
|
1252
|
+
displayName: p.displayName || newName,
|
|
1253
|
+
type: newType,
|
|
1254
|
+
};
|
|
1058
1255
|
if (p.isRequired)
|
|
1059
1256
|
prop.required = true;
|
|
1060
1257
|
parentPropsJson.push(prop);
|
|
@@ -1062,6 +1259,131 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1062
1259
|
parts.push(`| \`${oldName}\` | ${oldType} | → | \`${newName}\` | ${newType} | ${notes} |`);
|
|
1063
1260
|
}
|
|
1064
1261
|
parts.push("");
|
|
1262
|
+
// Custom Data Decision Callouts — per prop referencing a customData type
|
|
1263
|
+
const customDataPropsForCallouts = (target.props || []).filter((p) => p.type === "CUSTOM" &&
|
|
1264
|
+
p.customDataId &&
|
|
1265
|
+
customDataMap.has(p.customDataId));
|
|
1266
|
+
if (customDataPropsForCallouts.length > 0) {
|
|
1267
|
+
parts.push(`## Custom Data Decisions to Make`);
|
|
1268
|
+
parts.push("");
|
|
1269
|
+
parts.push(`Each prop below references an old \`customData\` type. Verify the MCP's default against the actual data semantics, then run the CLI command. **Log every decision in MIGRATION.md → \`## Custom Data Decisions\`** with reasoning. See \`get_migration_guide("custom-data-conversion")\` for the heuristic and worked examples.`);
|
|
1270
|
+
parts.push("");
|
|
1271
|
+
for (const p of customDataPropsForCallouts) {
|
|
1272
|
+
const cd = customDataMap.get(p.customDataId);
|
|
1273
|
+
if (!cd)
|
|
1274
|
+
continue;
|
|
1275
|
+
const cdName = cd.typescriptName || cd.name || cd.id || "Unknown";
|
|
1276
|
+
const cdType = cd.type || "?";
|
|
1277
|
+
let shape = "";
|
|
1278
|
+
let shapeKind = "unknown";
|
|
1279
|
+
if (cd.type === "ENUM") {
|
|
1280
|
+
const opts = (cd.enumOptions || [])
|
|
1281
|
+
.map((o) => o.value || o.displayName)
|
|
1282
|
+
.filter(Boolean);
|
|
1283
|
+
shape = `enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}`;
|
|
1284
|
+
shapeKind = "enum";
|
|
1285
|
+
}
|
|
1286
|
+
else if (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST") {
|
|
1287
|
+
const first = cd.nestedData?.[0];
|
|
1288
|
+
const fields = (first?.nestedData || []).map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`);
|
|
1289
|
+
shape = `${cdType} of {${fields.slice(0, 6).join(", ")}}`;
|
|
1290
|
+
shapeKind = "list";
|
|
1291
|
+
}
|
|
1292
|
+
else if (cd.type === "OBJECT") {
|
|
1293
|
+
const fields = (cd.nestedData || []).map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`);
|
|
1294
|
+
shape = `OBJECT {${fields.slice(0, 6).join(", ")}}`;
|
|
1295
|
+
shapeKind = "record";
|
|
1296
|
+
}
|
|
1297
|
+
else {
|
|
1298
|
+
shape = cdType;
|
|
1299
|
+
}
|
|
1300
|
+
// Build the field source + names list once so we can use it for the lost-fields enumeration
|
|
1301
|
+
// AND for the component-path CLI template.
|
|
1302
|
+
const fieldSource = cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST"
|
|
1303
|
+
? cd.nestedData?.[0]?.nestedData || []
|
|
1304
|
+
: cd.nestedData || [];
|
|
1305
|
+
const fieldDescriptions = fieldSource
|
|
1306
|
+
.filter((f) => f.key)
|
|
1307
|
+
.map((f) => `\`${f.key}\` (${f.type || "?"})`);
|
|
1308
|
+
parts.push(`### Prop \`${p.name || "?"}\` → customData \`${cdName}\``);
|
|
1309
|
+
parts.push("");
|
|
1310
|
+
parts.push(`**Shape:** \`${shape}\``);
|
|
1311
|
+
parts.push("");
|
|
1312
|
+
if (shapeKind === "enum") {
|
|
1313
|
+
parts.push(`**Default: enum prop.** Flat scalar set; use \`config add-enum\`.`);
|
|
1314
|
+
parts.push("");
|
|
1315
|
+
const enumName = cd.typescriptName ||
|
|
1316
|
+
(cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
|
|
1317
|
+
const enumOptions = (cd.enumOptions || []).reduce((acc, o) => {
|
|
1318
|
+
if (o.displayName && o.value)
|
|
1319
|
+
acc[o.displayName] = o.value;
|
|
1320
|
+
return acc;
|
|
1321
|
+
}, {});
|
|
1322
|
+
parts.push("```bash");
|
|
1323
|
+
parts.push(`npx ikas-component config add-enum --name "${enumName}" --options '${JSON.stringify(enumOptions)}'`);
|
|
1324
|
+
parts.push("```");
|
|
1325
|
+
parts.push("");
|
|
1326
|
+
parts.push(`If you believe this should be a component instead (e.g. each option secretly carries richer data not visible in the customData shape), see \`get_migration_guide("custom-data-conversion")\`.`);
|
|
1327
|
+
parts.push("");
|
|
1328
|
+
}
|
|
1329
|
+
else if (shapeKind === "list" || shapeKind === "record") {
|
|
1330
|
+
const fieldCount = fieldDescriptions.length;
|
|
1331
|
+
const isMinimal = fieldCount > 0 && fieldCount <= 2;
|
|
1332
|
+
if (isMinimal) {
|
|
1333
|
+
parts.push(`⚠️ **This child would have only ${fieldCount} field${fieldCount === 1 ? "" : "s"}** (${fieldDescriptions.join(", ")}). \`COMPONENT_LIST\` is usually overkill at this size. **Prefer one of:**`);
|
|
1334
|
+
parts.push(`- repeated scalar props on the parent (\`title1\`/\`link1\`, \`title2\`/\`link2\`, …) for a small fixed count`);
|
|
1335
|
+
parts.push(`- a domain LIST prop type (\`LIST_OF_LINK\`, \`IMAGE_LIST\`, \`PRODUCT_LIST\`, …) when each item IS one domain object`);
|
|
1336
|
+
parts.push(`- \`COMPONENT_LIST\` (CLI command below) only if reordering in the editor is a real UX win`);
|
|
1337
|
+
parts.push("");
|
|
1338
|
+
parts.push(`See \`get_migration_guide("component-composition-decision-guide")\` for the full tree. Log your choice in MIGRATION.md → \`## Custom Data Decisions\`.`);
|
|
1339
|
+
parts.push("");
|
|
1340
|
+
}
|
|
1341
|
+
else {
|
|
1342
|
+
parts.push(`**Default: component + COMPONENT_LIST.** Multiple fields per item — a single enum value cannot carry this structure.`);
|
|
1343
|
+
parts.push("");
|
|
1344
|
+
if (fieldCount > 0) {
|
|
1345
|
+
parts.push(`⚠️ **Fields you would lose if you flatten this:** ${fieldDescriptions.join(", ")}. Flattening to a simpler prop type drops these from the editor UI permanently. **Do not "simplify for later"** — if the feature genuinely isn't wanted, log that explicitly in MIGRATION.md → \`## Notes\` with reasoning. Otherwise build the component.`);
|
|
1346
|
+
parts.push("");
|
|
1347
|
+
}
|
|
1348
|
+
parts.push(`> See \`get_migration_guide("component-composition-decision-guide")\` for when \`COMPONENT_LIST\` is overkill.`);
|
|
1349
|
+
parts.push("");
|
|
1350
|
+
}
|
|
1351
|
+
const compName = cd.typescriptName ||
|
|
1352
|
+
(cd.name
|
|
1353
|
+
? cd.name.replace(/[^a-zA-Z0-9]/g, "")
|
|
1354
|
+
: `${sectionPascal}Item`);
|
|
1355
|
+
const compPropsForCli = [];
|
|
1356
|
+
for (const f of fieldSource) {
|
|
1357
|
+
if (!f.key)
|
|
1358
|
+
continue;
|
|
1359
|
+
let fType = f.type;
|
|
1360
|
+
if (fType === "SLIDER")
|
|
1361
|
+
fType = "NUMBER";
|
|
1362
|
+
else if (fType === "PRODUCT_DETAIL")
|
|
1363
|
+
fType = "PRODUCT";
|
|
1364
|
+
compPropsForCli.push({
|
|
1365
|
+
name: f.key,
|
|
1366
|
+
displayName: f.name || f.key,
|
|
1367
|
+
type: fType,
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
parts.push("```bash");
|
|
1371
|
+
if (isMinimal) {
|
|
1372
|
+
parts.push(`# Fallback: COMPONENT_LIST (use only if the simpler alternatives above don't fit)`);
|
|
1373
|
+
}
|
|
1374
|
+
parts.push(`npx ikas-component config add-component --name "${compName}" --type component --props '${JSON.stringify(compPropsForCli)}'`);
|
|
1375
|
+
parts.push(`# Then on the parent, set the prop's filteredComponentIds to the new component's id.`);
|
|
1376
|
+
parts.push("```");
|
|
1377
|
+
parts.push("");
|
|
1378
|
+
parts.push(`If you believe this should be an enum despite the structure, see \`get_migration_guide("custom-data-conversion")\`.`);
|
|
1379
|
+
parts.push("");
|
|
1380
|
+
}
|
|
1381
|
+
else {
|
|
1382
|
+
parts.push(`**Unable to classify automatically — you decide.** See \`get_migration_guide("custom-data-conversion")\` for the enum-vs-component heuristic.`);
|
|
1383
|
+
parts.push("");
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1065
1387
|
// Enums to create first
|
|
1066
1388
|
if (enumsNeeded.length > 0) {
|
|
1067
1389
|
parts.push(`## 3. Create Enums FIRST (if not already done)`);
|
|
@@ -1074,15 +1396,18 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1074
1396
|
parts.push(`Save the returned \`enumId\` values and substitute them in the parent config JSON below.`);
|
|
1075
1397
|
parts.push("");
|
|
1076
1398
|
}
|
|
1077
|
-
// Child CLI commands — dedupe by
|
|
1399
|
+
// Child CLI commands — dedupe by childName (same shape may be referenced by multiple parent props)
|
|
1078
1400
|
const uniqueChildren = new Map();
|
|
1079
1401
|
for (const ch of children) {
|
|
1080
|
-
const existing = uniqueChildren.get(ch.
|
|
1402
|
+
const existing = uniqueChildren.get(ch.childName);
|
|
1081
1403
|
if (existing) {
|
|
1082
1404
|
existing.usedByProps.push(ch.propName);
|
|
1083
1405
|
}
|
|
1084
1406
|
else {
|
|
1085
|
-
uniqueChildren.set(ch.
|
|
1407
|
+
uniqueChildren.set(ch.childName, {
|
|
1408
|
+
child: ch,
|
|
1409
|
+
usedByProps: [ch.propName],
|
|
1410
|
+
});
|
|
1086
1411
|
}
|
|
1087
1412
|
}
|
|
1088
1413
|
if (uniqueChildren.size > 0) {
|
|
@@ -1090,17 +1415,19 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1090
1415
|
parts.push("");
|
|
1091
1416
|
parts.push(`These children are referenced by the parent's COMPONENT_LIST props. Create them before the parent.`);
|
|
1092
1417
|
parts.push(`**Deduped:** run each \`add-component\` command ONCE even if multiple parent props reference the same child.`);
|
|
1418
|
+
parts.push(`**Capture each \`componentId\` from the CLI's JSON response** — you'll substitute these ids for the \`<id-of-{ChildName}>\` placeholders in the parent's \`filteredComponentIds\` below.`);
|
|
1093
1419
|
parts.push("");
|
|
1094
1420
|
for (const { child: ch, usedByProps } of uniqueChildren.values()) {
|
|
1095
|
-
parts.push(`### \`${ch.
|
|
1421
|
+
parts.push(`### \`${ch.childName}\``);
|
|
1096
1422
|
const propsLabel = usedByProps.length > 1
|
|
1097
|
-
? `Used by parent props: ${usedByProps.map(p => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
|
|
1423
|
+
? `Used by parent props: ${usedByProps.map((p) => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
|
|
1098
1424
|
: `For parent prop: \`${usedByProps[0]}\``;
|
|
1099
1425
|
parts.push(propsLabel);
|
|
1100
1426
|
parts.push(`Old customData: "${ch.customDataName}"`);
|
|
1101
1427
|
parts.push("");
|
|
1102
1428
|
parts.push(`\`\`\`bash`);
|
|
1103
1429
|
parts.push(`npx ikas-component config add-component --name "${ch.childName}" --type component --props '${JSON.stringify(ch.childPropsJson)}'`);
|
|
1430
|
+
parts.push(`# → { "success": true, "componentId": "<capture-this>", ... }`);
|
|
1104
1431
|
parts.push(`\`\`\``);
|
|
1105
1432
|
parts.push("");
|
|
1106
1433
|
}
|
|
@@ -1116,6 +1443,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1116
1443
|
parts.push(`\`\`\``);
|
|
1117
1444
|
parts.push("");
|
|
1118
1445
|
parts.push(`**IMPORTANT:** The CLI auto-generates \`types.ts\`. DO NOT manually create or edit \`types.ts\`.`);
|
|
1446
|
+
if (children.length > 0) {
|
|
1447
|
+
parts.push("");
|
|
1448
|
+
parts.push(`**Before running the command above:** replace each \`<id-of-{ChildName}>\` placeholder in the \`--props\` JSON with the real \`componentId\` captured from the corresponding child's \`add-component\` response (or from \`npx ikas-component config list\`). Component ids are opaque random strings — the CLI will reject any unknown id with a structured error.`);
|
|
1449
|
+
}
|
|
1119
1450
|
parts.push("");
|
|
1120
1451
|
// Implementation guidance
|
|
1121
1452
|
const nextStep = parentStep + 1;
|
|
@@ -1132,15 +1463,18 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1132
1463
|
if (children.length > 0) {
|
|
1133
1464
|
parts.push(`- For COMPONENT_LIST props, use \`<IkasComponentRenderer id="..." components={list as any[]} parentProps={props} />\``);
|
|
1134
1465
|
parts.push(`- **Remember: the parent cannot read child prop values.** Any per-item logic must live in the child component.`);
|
|
1135
|
-
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
|
|
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.`);
|
|
1136
1467
|
}
|
|
1137
1468
|
// Check if the section itself has data-driven list props (PRODUCT_LIST, BLOG_LIST, CATEGORY_LIST)
|
|
1138
|
-
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");
|
|
1139
1473
|
if (dataListProps.length > 0) {
|
|
1140
1474
|
parts.push("");
|
|
1141
1475
|
parts.push(`### Data-Driven List Rendering`);
|
|
1142
1476
|
parts.push("");
|
|
1143
|
-
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(", ")}.`);
|
|
1144
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:`);
|
|
1145
1479
|
parts.push("");
|
|
1146
1480
|
parts.push("```tsx");
|
|
@@ -1167,9 +1501,22 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1167
1501
|
parts.push(`See \`get_migration_guide("custom-data-conversion")\` → "Two Ways to Render Lists" for the full pattern.`);
|
|
1168
1502
|
}
|
|
1169
1503
|
// Detect form-page sections (0 or few props, name suggests a form/auth page)
|
|
1170
|
-
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
|
+
];
|
|
1171
1518
|
const lowerDir = (target.dir || "").toLowerCase();
|
|
1172
|
-
const isLikelyFormPage = formKeywords.some(kw => lowerDir.includes(kw));
|
|
1519
|
+
const isLikelyFormPage = formKeywords.some((kw) => lowerDir.includes(kw));
|
|
1173
1520
|
if (isLikelyFormPage) {
|
|
1174
1521
|
parts.push("");
|
|
1175
1522
|
parts.push(`### Form Page Pattern`);
|
|
@@ -1198,7 +1545,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1198
1545
|
// Fallback heuristic only when source scan unavailable
|
|
1199
1546
|
const heuristicLibs = [];
|
|
1200
1547
|
const lowerName = oldName.toLowerCase();
|
|
1201
|
-
if (lowerName.includes("slider") ||
|
|
1548
|
+
if (lowerName.includes("slider") ||
|
|
1549
|
+
lowerName.includes("carousel") ||
|
|
1550
|
+
lowerName.includes("banner")) {
|
|
1202
1551
|
heuristicLibs.push("swiper");
|
|
1203
1552
|
}
|
|
1204
1553
|
if (lowerName.includes("marquee"))
|
|
@@ -1207,7 +1556,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1207
1556
|
heuristicLibs.push("react-player");
|
|
1208
1557
|
if (lowerName.includes("chart"))
|
|
1209
1558
|
heuristicLibs.push("recharts");
|
|
1210
|
-
if (lowerName.includes("star") ||
|
|
1559
|
+
if (lowerName.includes("star") ||
|
|
1560
|
+
lowerName.includes("rating") ||
|
|
1561
|
+
lowerName.includes("review"))
|
|
1211
1562
|
heuristicLibs.push("react-simple-star-rating");
|
|
1212
1563
|
if (heuristicLibs.length > 0) {
|
|
1213
1564
|
parts.push(`### Likely Library Replacements (heuristic — source not scanned)`);
|
|
@@ -1221,48 +1572,70 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1221
1572
|
parts.push("");
|
|
1222
1573
|
}
|
|
1223
1574
|
}
|
|
1224
|
-
// Relevant guides
|
|
1225
|
-
parts.push(`## ${nextStep + 1}. Relevant Guides
|
|
1575
|
+
// Relevant guides — keep terse; LLM can call get_migration_guide("list") for the full catalog
|
|
1576
|
+
parts.push(`## ${nextStep + 1}. Relevant Guides`);
|
|
1226
1577
|
parts.push("");
|
|
1227
1578
|
parts.push(`- \`get_migration_guide("react-to-preact")\` — code conversion patterns`);
|
|
1228
|
-
parts.push(`- \`get_migration_guide("
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
parts.push(`- \`get_migration_guide("component-renderer-limitations")\` — critical COMPONENT_LIST constraints`);
|
|
1232
|
-
parts.push(`- \`get_framework_guide("component-renderer-patterns")\` — full IkasComponentRenderer usage`);
|
|
1233
|
-
parts.push(`- \`get_migration_example("custom-dynamic-list-to-component-list")\` — concrete example`);
|
|
1234
|
-
}
|
|
1235
|
-
if (target.isHeader || target.isFooter) {
|
|
1236
|
-
parts.push(`- \`get_framework_guide("header-footer-patterns")\` — header/footer specifics`);
|
|
1579
|
+
parts.push(`- \`get_migration_guide("prop-runtime-shapes")\` — exact runtime shapes (\`.data\` vs \`.links\`, etc.)`);
|
|
1580
|
+
if (children.length > 0 || target.isHeader || target.isFooter) {
|
|
1581
|
+
parts.push(`- \`get_framework_guide("header-footer-patterns")\` — COMPONENT_LIST + IkasComponentRenderer wiring`);
|
|
1237
1582
|
}
|
|
1238
|
-
parts.push(`- \`get_framework_guide("common-pitfalls")\` — observer rules, common mistakes`);
|
|
1239
1583
|
parts.push("");
|
|
1240
1584
|
// Completion
|
|
1241
1585
|
parts.push(`## ${nextStep + 2}. Mark Complete`);
|
|
1242
1586
|
parts.push("");
|
|
1243
|
-
parts.push(`Once the section builds cleanly with \`npx ikas-component build
|
|
1244
|
-
parts.push(`1. Edit \`MIGRATION.md\` at the project root`);
|
|
1245
|
-
parts.push(`2. Change the checkbox for \`${sectionId}\` from \`[ ]\` to \`[x]\``);
|
|
1246
|
-
parts.push(`3. Also mark each child component as \`[x]\``);
|
|
1247
|
-
parts.push(`4. Append an entry to the **Session Log** section of \`MIGRATION.md\` noting: libraries replaced, any props added beyond the generated plan, the actual folder name the CLI created (in case it differs from the name you passed — e.g., \`FAQ\` → \`Faq/\`), and any other decisions a future agent should know.`);
|
|
1587
|
+
parts.push(`Once the section builds cleanly with \`npx ikas-component build\`: edit MIGRATION.md → tick \`[x]\` for \`${sectionId}\` and each child component, log any customData decisions under \`## Custom Data Decisions\`, and append a brief entry to \`## Notes\` with anything future sessions should know.`);
|
|
1248
1588
|
parts.push("");
|
|
1249
1589
|
return parts.join("\n");
|
|
1250
1590
|
}
|
|
1251
1591
|
// --- Search helpers ---
|
|
1592
|
+
/**
|
|
1593
|
+
* Strip every non-alphanumeric character and lowercase, so the SAME identifier written in
|
|
1594
|
+
* different conventions collapses to one form: `PRODUCT_ATTRIBUTE`, `IkasProductAttributeDetail`
|
|
1595
|
+
* and the prose "product attribute detail" all become `productattribute…`. Without this a query
|
|
1596
|
+
* for a prop type (which is always SCREAMING_SNAKE) can never reach the function/type docs that
|
|
1597
|
+
* spell the same concept in camelCase or prose — `search_docs("PRODUCT_ATTRIBUTE")` used to miss
|
|
1598
|
+
* `getAttributeDetailValues` entirely.
|
|
1599
|
+
*/
|
|
1600
|
+
function squashIdentifier(text) {
|
|
1601
|
+
return text.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
1602
|
+
}
|
|
1603
|
+
/** A query word worth matching separator-insensitively: an identifier, not prose. */
|
|
1604
|
+
function isIdentifierLike(word) {
|
|
1605
|
+
return (word.length > 3 &&
|
|
1606
|
+
(/[_\-.]/.test(word) ||
|
|
1607
|
+
/[a-z][A-Z]/.test(word) ||
|
|
1608
|
+
/^[A-Z0-9_]+$/.test(word)));
|
|
1609
|
+
}
|
|
1252
1610
|
function matchScore(text, query) {
|
|
1253
1611
|
const lower = text.toLowerCase();
|
|
1254
1612
|
const queryLower = query.toLowerCase();
|
|
1255
|
-
const words =
|
|
1613
|
+
const words = query.split(/\s+/).filter(Boolean);
|
|
1256
1614
|
let score = 0;
|
|
1257
1615
|
// Exact substring match
|
|
1258
1616
|
if (lower.includes(queryLower)) {
|
|
1259
1617
|
score += 10;
|
|
1260
1618
|
}
|
|
1261
|
-
// Word matches
|
|
1619
|
+
// Word matches. Squashing the (potentially guide-sized) text is the expensive half of the
|
|
1620
|
+
// identifier check — compute it lazily, once per call, not once per query word.
|
|
1621
|
+
let squashedText = null;
|
|
1262
1622
|
for (const word of words) {
|
|
1263
|
-
|
|
1623
|
+
const wordLower = word.toLowerCase();
|
|
1624
|
+
if (lower.includes(wordLower)) {
|
|
1264
1625
|
score += 3;
|
|
1265
1626
|
}
|
|
1627
|
+
else if (isIdentifierLike(word)) {
|
|
1628
|
+
// Same identifier, different convention (PRODUCT_ATTRIBUTE ↔ IkasProductAttributeDetail
|
|
1629
|
+
// ↔ "product attribute"). Scored below an exact word hit so literal matches still win.
|
|
1630
|
+
const squashed = squashIdentifier(word);
|
|
1631
|
+
if (squashed) {
|
|
1632
|
+
if (squashedText === null)
|
|
1633
|
+
squashedText = squashIdentifier(text);
|
|
1634
|
+
if (squashedText.includes(squashed)) {
|
|
1635
|
+
score += 2;
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1266
1639
|
}
|
|
1267
1640
|
// Name exact match bonus
|
|
1268
1641
|
if (lower === queryLower) {
|
|
@@ -1270,18 +1643,148 @@ function matchScore(text, query) {
|
|
|
1270
1643
|
}
|
|
1271
1644
|
return score;
|
|
1272
1645
|
}
|
|
1646
|
+
// Levenshtein edit distance — small DP, fine for the 28 short kebab-case section template names.
|
|
1647
|
+
function levenshtein(a, b) {
|
|
1648
|
+
if (a === b)
|
|
1649
|
+
return 0;
|
|
1650
|
+
if (a.length === 0)
|
|
1651
|
+
return b.length;
|
|
1652
|
+
if (b.length === 0)
|
|
1653
|
+
return a.length;
|
|
1654
|
+
const m = a.length;
|
|
1655
|
+
const n = b.length;
|
|
1656
|
+
const prev = new Array(n + 1);
|
|
1657
|
+
const curr = new Array(n + 1);
|
|
1658
|
+
for (let j = 0; j <= n; j++)
|
|
1659
|
+
prev[j] = j;
|
|
1660
|
+
for (let i = 1; i <= m; i++) {
|
|
1661
|
+
curr[0] = i;
|
|
1662
|
+
for (let j = 1; j <= n; j++) {
|
|
1663
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
1664
|
+
curr[j] = Math.min(curr[j - 1] + 1, // insertion
|
|
1665
|
+
prev[j] + 1, // deletion
|
|
1666
|
+
prev[j - 1] + cost);
|
|
1667
|
+
}
|
|
1668
|
+
for (let j = 0; j <= n; j++)
|
|
1669
|
+
prev[j] = curr[j];
|
|
1670
|
+
}
|
|
1671
|
+
return prev[n];
|
|
1672
|
+
}
|
|
1673
|
+
// Suggest up to `max` closest candidates for an unknown input.
|
|
1674
|
+
// Primary: matchScore (substring/word). Fallback: Levenshtein when nothing scores well.
|
|
1675
|
+
function suggestClosestNames(input, candidates, opts) {
|
|
1676
|
+
const min = opts?.min ?? 8;
|
|
1677
|
+
const max = opts?.max ?? 3;
|
|
1678
|
+
const scored = candidates
|
|
1679
|
+
.map((c) => ({ name: c, score: matchScore(c, input) }))
|
|
1680
|
+
.filter((s) => s.score >= min)
|
|
1681
|
+
.sort((a, b) => b.score - a.score);
|
|
1682
|
+
if (scored.length > 0)
|
|
1683
|
+
return scored.slice(0, max).map((s) => s.name);
|
|
1684
|
+
// Levenshtein fallback — tolerate up to ceil(len/3) edits.
|
|
1685
|
+
const maxDistance = Math.max(1, Math.ceil(input.length / 3));
|
|
1686
|
+
const lower = input.toLowerCase();
|
|
1687
|
+
const edits = candidates
|
|
1688
|
+
.map((c) => ({ name: c, d: levenshtein(lower, c.toLowerCase()) }))
|
|
1689
|
+
.filter((s) => s.d <= maxDistance)
|
|
1690
|
+
.sort((a, b) => a.d - b.d);
|
|
1691
|
+
return edits.slice(0, max).map((s) => s.name);
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Formatted ` — matching sections: ...` suffix naming the guide sections that match the query,
|
|
1695
|
+
* so a hit buried mid-guide (e.g. "Quantity Limits" inside cart-patterns) is visible without
|
|
1696
|
+
* opening the full guide. Section bodies are scored on purpose: a query for an identifier that
|
|
1697
|
+
* appears only inside a section (an error code, a prop name) must still surface that section's
|
|
1698
|
+
* heading. Returns "" when the query matches most of the guide's sections — matchScore is
|
|
1699
|
+
* presence-based, so a broad query ("cart" against cart-patterns) ties every section and a
|
|
1700
|
+
* top-3 would just echo document order, hinting nothing.
|
|
1701
|
+
*/
|
|
1702
|
+
// Topic content is loaded once at startup and never mutates, so the section parse is
|
|
1703
|
+
// memoized on the content string — a query only pays for scoring.
|
|
1704
|
+
const parsedSectionsCache = new Map();
|
|
1705
|
+
function parseSections(content) {
|
|
1706
|
+
const cached = parsedSectionsCache.get(content);
|
|
1707
|
+
if (cached)
|
|
1708
|
+
return cached;
|
|
1709
|
+
// Line walk instead of a heading-regex split: a `## ` line inside a fenced code block must
|
|
1710
|
+
// not become a pseudo-section (it would surface a heading that doesn't exist in the guide
|
|
1711
|
+
// and swallow the rest of the real section's body).
|
|
1712
|
+
const sections = [];
|
|
1713
|
+
let fenceOpen = false;
|
|
1714
|
+
for (const line of content.split("\n")) {
|
|
1715
|
+
if (/^\s*```/.test(line))
|
|
1716
|
+
fenceOpen = !fenceOpen;
|
|
1717
|
+
const headingMatch = fenceOpen ? null : line.match(/^#{2,4} (.+)/);
|
|
1718
|
+
if (headingMatch)
|
|
1719
|
+
sections.push({ heading: headingMatch[1].trim(), body: "" });
|
|
1720
|
+
else if (sections.length > 0)
|
|
1721
|
+
sections[sections.length - 1].body += line + "\n";
|
|
1722
|
+
}
|
|
1723
|
+
parsedSectionsCache.set(content, sections);
|
|
1724
|
+
return sections;
|
|
1725
|
+
}
|
|
1726
|
+
function sectionHint(content, query, topicTitle) {
|
|
1727
|
+
const sections = parseSections(content);
|
|
1728
|
+
const scored = [];
|
|
1729
|
+
for (const { heading, body } of sections) {
|
|
1730
|
+
// Heading hits weigh double. The floor of 3 drops sections whose only signal is a single
|
|
1731
|
+
// squashed-identifier body match (worth 2) — a literal word hit (3) or any heading match
|
|
1732
|
+
// clears it.
|
|
1733
|
+
const score = matchScore(heading, query) * 2 + matchScore(body, query);
|
|
1734
|
+
if (score >= 3)
|
|
1735
|
+
scored.push({ heading, score });
|
|
1736
|
+
}
|
|
1737
|
+
if (scored.length === 0)
|
|
1738
|
+
return "";
|
|
1739
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1740
|
+
// The tie-guard below must only fire when the field is genuinely flat. A single common
|
|
1741
|
+
// query word ("cart") pushes every section past the floor, so counting raw matches would
|
|
1742
|
+
// also suppress steep fields where one section clearly won. Sections below 60% of the top
|
|
1743
|
+
// score are noise from such a word, not real matches — drop them before counting.
|
|
1744
|
+
const strong = scored.filter((s) => s.score >= scored[0].score * 0.6);
|
|
1745
|
+
if (strong.length > sections.length / 2)
|
|
1746
|
+
return "";
|
|
1747
|
+
// The guide's own title is usually its first `##` line, so it registers as a section.
|
|
1748
|
+
// Its heading is the exact text the result line already prints — listing it would burn
|
|
1749
|
+
// one of the three slots on a pure echo. Keep it in scoring (its body can be a genuine
|
|
1750
|
+
// hit and it counts toward the tie-guard), drop it from the output only. Headings are
|
|
1751
|
+
// already trimmed at parse time; trim the title too so stray whitespace in the JSON
|
|
1752
|
+
// cannot silently re-admit the echo.
|
|
1753
|
+
const title = topicTitle.trim();
|
|
1754
|
+
const named = strong.filter((s) => s.heading !== title);
|
|
1755
|
+
if (named.length === 0)
|
|
1756
|
+
return "";
|
|
1757
|
+
const top = named.slice(0, 3);
|
|
1758
|
+
return ` — matching sections: ${top.map((s) => `"${s.heading}"`).join(", ")}`;
|
|
1759
|
+
}
|
|
1273
1760
|
function searchFunctions(query) {
|
|
1274
1761
|
const scored = storefrontData.functions
|
|
1275
1762
|
.map((fn) => {
|
|
1276
1763
|
const nameScore = matchScore(fn.name, query) * 3;
|
|
1764
|
+
const displayNameScore = fn.displayName
|
|
1765
|
+
? matchScore(fn.displayName, query) * 3
|
|
1766
|
+
: 0;
|
|
1277
1767
|
const descScore = matchScore(fn.description, query);
|
|
1278
|
-
const catScore = fn.categories.some((c) => matchScore(c, query) > 0)
|
|
1768
|
+
const catScore = fn.categories.some((c) => matchScore(c, query) > 0)
|
|
1769
|
+
? 5
|
|
1770
|
+
: 0;
|
|
1279
1771
|
const paramScore = fn.params.some((p) => matchScore(p.name, query) > 0 || matchScore(p.description, query) > 0)
|
|
1280
1772
|
? 2
|
|
1281
1773
|
: 0;
|
|
1282
1774
|
const sigScore = matchScore(fn.signature, query) * 2;
|
|
1283
|
-
const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0)
|
|
1284
|
-
|
|
1775
|
+
const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0)
|
|
1776
|
+
? 8
|
|
1777
|
+
: 0;
|
|
1778
|
+
return {
|
|
1779
|
+
fn,
|
|
1780
|
+
score: nameScore +
|
|
1781
|
+
displayNameScore +
|
|
1782
|
+
descScore +
|
|
1783
|
+
catScore +
|
|
1784
|
+
paramScore +
|
|
1785
|
+
sigScore +
|
|
1786
|
+
typeScore,
|
|
1787
|
+
};
|
|
1285
1788
|
})
|
|
1286
1789
|
.filter((item) => item.score > 0)
|
|
1287
1790
|
.sort((a, b) => b.score - a.score);
|
|
@@ -1294,7 +1797,11 @@ function searchFrameworkTopics(query) {
|
|
|
1294
1797
|
const descScore = matchScore(topic.description, query) * 2;
|
|
1295
1798
|
const contentScore = matchScore(topic.content, query);
|
|
1296
1799
|
const tagScore = topic.tags.some((t) => matchScore(t, query) > 0) ? 5 : 0;
|
|
1297
|
-
return {
|
|
1800
|
+
return {
|
|
1801
|
+
key,
|
|
1802
|
+
topic,
|
|
1803
|
+
score: titleScore + descScore + contentScore + tagScore,
|
|
1804
|
+
};
|
|
1298
1805
|
})
|
|
1299
1806
|
.filter((item) => item.score > 0)
|
|
1300
1807
|
.sort((a, b) => b.score - a.score);
|
|
@@ -1309,14 +1816,34 @@ function searchTypes(query) {
|
|
|
1309
1816
|
const propScore = td.properties?.some((p) => matchScore(p.name, query) > 0 || matchScore(p.type, query) > 0)
|
|
1310
1817
|
? 4
|
|
1311
1818
|
: 0;
|
|
1312
|
-
const enumScore = td.enumValues?.some((v) => matchScore(v, query) > 0)
|
|
1313
|
-
|
|
1819
|
+
const enumScore = td.enumValues?.some((v) => matchScore(v, query) > 0)
|
|
1820
|
+
? 4
|
|
1821
|
+
: 0;
|
|
1822
|
+
const unionScore = td.unionValues?.some((v) => matchScore(v, query) > 0)
|
|
1823
|
+
? 4
|
|
1824
|
+
: 0;
|
|
1825
|
+
return {
|
|
1826
|
+
td,
|
|
1827
|
+
score: nameScore + domainScore + propScore + enumScore + unionScore,
|
|
1828
|
+
};
|
|
1314
1829
|
})
|
|
1315
1830
|
.filter((item) => item.score > 0)
|
|
1316
1831
|
.sort((a, b) => b.score - a.score);
|
|
1317
1832
|
return scored.map((item) => item.td);
|
|
1318
1833
|
}
|
|
1319
1834
|
// --- Format helpers ---
|
|
1835
|
+
/**
|
|
1836
|
+
* Wrap a code snippet in a fenced block — unless it already carries its own fence. JSDoc `@example`
|
|
1837
|
+
* bodies are indexed verbatim, so every one of them arrives fenced; wrapping them again produced a
|
|
1838
|
+
* double ```` ``` ```` opener, which makes the block unparseable for anything extracting code from
|
|
1839
|
+
* the doc. Bare snippets still get fenced as typescript.
|
|
1840
|
+
*/
|
|
1841
|
+
function asCodeBlock(snippet) {
|
|
1842
|
+
const trimmed = snippet.trim();
|
|
1843
|
+
return trimmed.startsWith("```")
|
|
1844
|
+
? trimmed
|
|
1845
|
+
: ["```typescript", trimmed, "```"].join("\n");
|
|
1846
|
+
}
|
|
1320
1847
|
function formatFunctionDoc(fn) {
|
|
1321
1848
|
const lines = [];
|
|
1322
1849
|
lines.push(`## ${fn.name}`);
|
|
@@ -1343,7 +1870,7 @@ function formatFunctionDoc(fn) {
|
|
|
1343
1870
|
lines.push("", `**Returns:** \`${fn.returnType}\`${fn.returns ? ` - ${fn.returns}` : ""}`);
|
|
1344
1871
|
}
|
|
1345
1872
|
if (fn.example) {
|
|
1346
|
-
lines.push("", "**Example:**",
|
|
1873
|
+
lines.push("", "**Example:**", asCodeBlock(fn.example));
|
|
1347
1874
|
}
|
|
1348
1875
|
if (fn.related.length > 0) {
|
|
1349
1876
|
lines.push("", `**Related:** ${fn.related.map((r) => `\`${r}\``).join(", ")}`);
|
|
@@ -1351,8 +1878,12 @@ function formatFunctionDoc(fn) {
|
|
|
1351
1878
|
return lines.join("\n");
|
|
1352
1879
|
}
|
|
1353
1880
|
function formatFunctionSummary(fn) {
|
|
1354
|
-
const desc = fn.description
|
|
1355
|
-
|
|
1881
|
+
const desc = fn.description
|
|
1882
|
+
? fn.description.split(".")[0] + "."
|
|
1883
|
+
: "No description.";
|
|
1884
|
+
const alias = fn.displayName && fn.displayName !== fn.name
|
|
1885
|
+
? ` (alias: ${fn.displayName})`
|
|
1886
|
+
: "";
|
|
1356
1887
|
return `- \`${fn.name}\`${alias} - ${desc}`;
|
|
1357
1888
|
}
|
|
1358
1889
|
function formatTypeDefinition(td, opts = {}) {
|
|
@@ -1360,13 +1891,24 @@ function formatTypeDefinition(td, opts = {}) {
|
|
|
1360
1891
|
const lines = [];
|
|
1361
1892
|
lines.push(`## ${td.name}`);
|
|
1362
1893
|
lines.push("", `**Kind:** ${td.kind} | **Domain:** ${td.domain}`);
|
|
1363
|
-
if (td.
|
|
1894
|
+
if (td.description) {
|
|
1895
|
+
lines.push("", td.description);
|
|
1896
|
+
}
|
|
1897
|
+
if (td.kind === "type" && td.unionValues) {
|
|
1898
|
+
lines.push("", "```typescript");
|
|
1899
|
+
lines.push(`type ${td.name} = ${td.unionValues.map((v) => `"${v}"`).join(" | ")};`);
|
|
1900
|
+
lines.push("```");
|
|
1901
|
+
}
|
|
1902
|
+
else if (td.kind === "type") {
|
|
1364
1903
|
lines.push("", "```typescript");
|
|
1365
1904
|
const extendsStr = td.extends ? ` & ${td.extends.join(" & ")}` : "";
|
|
1366
1905
|
lines.push(`type ${td.name} = {`);
|
|
1367
1906
|
if (td.properties) {
|
|
1368
1907
|
for (const p of td.properties) {
|
|
1369
1908
|
const opt = p.optional ? "?" : "";
|
|
1909
|
+
if (p.description) {
|
|
1910
|
+
lines.push(" /**", ...p.description.split("\n").map((l) => ` * ${l.trim()}`), " */");
|
|
1911
|
+
}
|
|
1370
1912
|
lines.push(` ${p.name}${opt}: ${p.type};`);
|
|
1371
1913
|
}
|
|
1372
1914
|
}
|
|
@@ -1393,7 +1935,9 @@ function formatTypeDefinition(td, opts = {}) {
|
|
|
1393
1935
|
const fn = storefrontData.functions.find((f) => f.name === fnName);
|
|
1394
1936
|
if (fn) {
|
|
1395
1937
|
const desc = fn.description ? fn.description.split(".")[0] + "." : "";
|
|
1396
|
-
const alias = fn.displayName && fn.displayName !== fn.name
|
|
1938
|
+
const alias = fn.displayName && fn.displayName !== fn.name
|
|
1939
|
+
? ` (alias: ${fn.displayName})`
|
|
1940
|
+
: "";
|
|
1397
1941
|
lines.push(`- **\`${fn.name}\`**${alias} — ${desc}`);
|
|
1398
1942
|
lines.push(` \`${fn.signature}\``);
|
|
1399
1943
|
}
|
|
@@ -1402,7 +1946,9 @@ function formatTypeDefinition(td, opts = {}) {
|
|
|
1402
1946
|
}
|
|
1403
1947
|
}
|
|
1404
1948
|
lines.push("");
|
|
1405
|
-
lines.push(
|
|
1949
|
+
lines.push('Use `get_functions_for_type("' +
|
|
1950
|
+
td.name +
|
|
1951
|
+
'")` for full documentation of these functions.');
|
|
1406
1952
|
}
|
|
1407
1953
|
return lines.join("\n");
|
|
1408
1954
|
}
|
|
@@ -1411,6 +1957,10 @@ function formatTypeSummary(td) {
|
|
|
1411
1957
|
const count = td.enumValues?.length ?? 0;
|
|
1412
1958
|
return `- \`${td.name}\` (enum, ${count} values) — domain: ${td.domain}`;
|
|
1413
1959
|
}
|
|
1960
|
+
if (td.unionValues) {
|
|
1961
|
+
const values = td.unionValues.map((v) => `"${v}"`).join(" | ");
|
|
1962
|
+
return `- \`${td.name}\` (union: ${values}) — domain: ${td.domain}`;
|
|
1963
|
+
}
|
|
1414
1964
|
const count = td.properties?.length ?? 0;
|
|
1415
1965
|
const ext = td.extends ? ` extends ${td.extends.join(", ")}` : "";
|
|
1416
1966
|
return `- \`${td.name}\` (${count} properties${ext}) — domain: ${td.domain}`;
|
|
@@ -1420,7 +1970,15 @@ const server = new McpServer({
|
|
|
1420
1970
|
name: "ikas-code-components",
|
|
1421
1971
|
version: "0.1.0",
|
|
1422
1972
|
}, {
|
|
1423
|
-
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
|
|
1973
|
+
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" +
|
|
1974
|
+
"Live-editor actions (require `ikas theme 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); `move_page_section` → reorder a section already placed on a page (by `direction` up/down or an absolute `to_index`) — the only way to change section order, since the editor UI offers it solely as a drag; `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); `list_ready_made_pages` / `update_ready_made_page_prop` / `enable_ready_made_pages` → the ikas-rendered ready-made account/membership pages (login, register, my account, orders, …) — these are NOT sections and the section tools cannot edit them; use these three to enable the groups, read their prop schemas, and customize/localize them (logo, colors, texts). When the user asks to BUILD a login/register/password/my-account/orders/addresses page, FIRST offer these ready-made pages instead of assembling one from sections — they are fully functional out of the box and customizable (logo, colors, texts); build a custom section page only if the user prefers a custom design. Offer, don't flip: `enable_ready_made_pages` creates/removes pages, so get the user's explicit OK before calling it. 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" +
|
|
1975
|
+
"READY-MADE PAGES FIRST — HARD RULE for account/membership pages: USER-FACING NAME: name these in the USER'S OWN LANGUAGE — English: \"ikas's ready-made pages\", Turkish: \"ikas'ın hazır sayfaları\"; never mix the English term into a sentence in another language — the editor UI lists them as Membership Pages / My Account Pages; \"built-in\" is an internal term — keep it out of everything the user sees, including question/option labels. When the user asks for a login, register, forgot/recover-password, my-account, orders, order-detail, addresses, favorites or email-verification page, do NOT start coding a custom section page. STOP and ask the user FIRST whether to use ikas's ready-made version of that page (`list_ready_made_pages` shows it — fully functional out of the box, customized/localized via `update_ready_made_page_prop`, the whole group turned on with `enable_ready_made_pages`). Only build a custom section page AFTER the user explicitly chooses custom. When asking, list the ready-made option FIRST as the recommended default; the custom section option comes second. Never call `enable_ready_made_pages` without the user's OK — it creates/removes pages. Ready-made pages are born with ONLY their own ikas-rendered element: the theme's header/footer are NOT on them (the page draws its own top bar — logo, back-to-store link — and account nav). If the user wants the theme header/footer on a ready-made page, PLACE it there with add_section_to_page — sections can sit above/below that element — and suggest turning the page's own `showBrandBar` prop off to avoid double chrome.\n\n" +
|
|
1976
|
+
"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" +
|
|
1977
|
+
"TWO DISTINCT JOBS — do not confuse them, and do not jump to writing code for the second one:\n" +
|
|
1978
|
+
"(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" +
|
|
1979
|
+
"(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" +
|
|
1980
|
+
"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.\n\n" +
|
|
1981
|
+
"COMMON (GLOBAL) SECTIONS — HEADER & FOOTER: these render on every page that CARRIES A PLACEMENT of them — pages created in the editor are seeded with one, EXCEPT ikas ready-made (built-in) account/membership pages, which are born WITHOUT the theme header/footer (see the ready-made rule above) — and their prop values are shared theme-wide, stored in ONE record on the INDEX (Home) page's placement. Reads and writes from any page are resolved to that record, and every response about such a section carries a `common` block (`isCommon`, `commonSectionType`, `canonicalPageId`, `canonicalElementId`, `valuesArePageSpecific: false`). Consequences: changing a header/footer prop from, say, a product-detail page changes it on ALL pages; and since the header/footer normally already holds the merchant's content, you must `get_section_values` first and change only what the user asked — never treat a prop as empty and never blind-fill 'missing' props, or you overwrite live content everywhere. If the INDEX page has no placement of the section there is no record at all: reads report nothing and writes fail until you add it to the INDEX page.",
|
|
1424
1982
|
});
|
|
1425
1983
|
// Tool: search_docs
|
|
1426
1984
|
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 }) => {
|
|
@@ -1440,7 +1998,7 @@ server.tool("search_docs", "Search across all ikas storefront API docs, framewor
|
|
|
1440
1998
|
if (topics.length > 0) {
|
|
1441
1999
|
parts.push("\n## Matching Framework Topics\n");
|
|
1442
2000
|
for (const item of topics) {
|
|
1443
|
-
parts.push(`- **${item.topic.title}** (key: \`${item.key}\`) - ${item.topic.description}`);
|
|
2001
|
+
parts.push(`- **${item.topic.title}** (key: \`${item.key}\`) - ${item.topic.description}${sectionHint(item.topic.content, query, item.topic.title)}`);
|
|
1444
2002
|
}
|
|
1445
2003
|
parts.push("");
|
|
1446
2004
|
parts.push("Use `get_framework_guide(topic)` to get full content for any topic.");
|
|
@@ -1456,44 +2014,84 @@ server.tool("search_docs", "Search across all ikas storefront API docs, framewor
|
|
|
1456
2014
|
if (migrationTopics.length > 0) {
|
|
1457
2015
|
parts.push("\n## Matching Migration Topics\n");
|
|
1458
2016
|
for (const item of migrationTopics) {
|
|
1459
|
-
parts.push(`- [migration] **${item.topic.title}** (key: \`${item.key}\`) - ${item.topic.description}`);
|
|
2017
|
+
parts.push(`- [migration] **${item.topic.title}** (key: \`${item.key}\`) - ${item.topic.description}${sectionHint(item.topic.content, query, item.topic.title)}`);
|
|
1460
2018
|
}
|
|
1461
2019
|
parts.push("");
|
|
1462
2020
|
parts.push("Use `get_migration_guide(topic)` to get full content for any migration topic.");
|
|
1463
2021
|
}
|
|
1464
|
-
if (functions.length === 0 &&
|
|
2022
|
+
if (functions.length === 0 &&
|
|
2023
|
+
topics.length === 0 &&
|
|
2024
|
+
types.length === 0 &&
|
|
2025
|
+
migrationTopics.length === 0) {
|
|
1465
2026
|
parts.push(`No results found for "${query}". Try different keywords or use \`list_functions()\` to see all available functions.`);
|
|
1466
2027
|
}
|
|
1467
2028
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1468
2029
|
});
|
|
1469
2030
|
// Tool: get_function_doc
|
|
1470
|
-
server.tool("get_function_doc", "Get full documentation for a specific storefront API function including signature, parameters, return type, and example.", {
|
|
2031
|
+
server.tool("get_function_doc", "Get full documentation for a specific storefront API function including signature, parameters, return type, and example.", {
|
|
2032
|
+
name: z
|
|
2033
|
+
.string()
|
|
2034
|
+
.describe("Function name (e.g. 'addItemToCart', 'Router.navigate')"),
|
|
2035
|
+
}, async ({ name }) => {
|
|
1471
2036
|
const nameLower = name.toLowerCase();
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
2037
|
+
// Phase 1: canonical-name match wins. A real function name always outranks
|
|
2038
|
+
// any displayName alias so aliases can never shadow the function they're
|
|
2039
|
+
// named after (e.g. `hasCustomer` the function vs. `hasIkasOrderCustomer`'s
|
|
2040
|
+
// [BP-DISPLAY-NAME: hasCustomer] alias).
|
|
2041
|
+
const byName = storefrontData.functions.find((f) => f.name.toLowerCase() === nameLower);
|
|
2042
|
+
if (byName) {
|
|
2043
|
+
return {
|
|
2044
|
+
content: [{ type: "text", text: formatFunctionDoc(byName) }],
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
// Phase 2: fall back to displayName aliases.
|
|
2048
|
+
const byAlias = storefrontData.functions.filter((f) => f.displayName && f.displayName.toLowerCase() === nameLower);
|
|
2049
|
+
if (byAlias.length === 1) {
|
|
2050
|
+
const fn = byAlias[0];
|
|
2051
|
+
const note = `> Note: "${name}" is a display alias for \`${fn.name}\`.\n\n`;
|
|
2052
|
+
return {
|
|
2053
|
+
content: [
|
|
2054
|
+
{ type: "text", text: note + formatFunctionDoc(fn) },
|
|
2055
|
+
],
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
if (byAlias.length > 1) {
|
|
2059
|
+
const lines = [];
|
|
2060
|
+
lines.push(`"${name}" is an ambiguous display alias used by ${byAlias.length} functions. Call \`get_function_doc\` again with the canonical name of the one you want:`);
|
|
2061
|
+
lines.push("");
|
|
2062
|
+
for (const fn of byAlias) {
|
|
2063
|
+
lines.push(formatFunctionSummary(fn));
|
|
2064
|
+
lines.push(` \`${fn.signature}\``);
|
|
1491
2065
|
}
|
|
2066
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2067
|
+
}
|
|
2068
|
+
// No exact match. Try fuzzy substring match across name + displayName.
|
|
2069
|
+
const matches = storefrontData.functions.filter((f) => f.name.toLowerCase().includes(nameLower) ||
|
|
2070
|
+
(f.displayName && f.displayName.toLowerCase().includes(nameLower)));
|
|
2071
|
+
if (matches.length > 0) {
|
|
2072
|
+
const suggestions = matches.slice(0, 5).map((f) => {
|
|
2073
|
+
const alias = f.displayName && f.displayName !== f.name
|
|
2074
|
+
? ` (alias: ${f.displayName})`
|
|
2075
|
+
: "";
|
|
2076
|
+
return ` - ${f.name}${alias}`;
|
|
2077
|
+
});
|
|
1492
2078
|
return {
|
|
1493
|
-
content: [
|
|
2079
|
+
content: [
|
|
2080
|
+
{
|
|
2081
|
+
type: "text",
|
|
2082
|
+
text: `Function "${name}" not found. Did you mean:\n${suggestions.join("\n")}`,
|
|
2083
|
+
},
|
|
2084
|
+
],
|
|
1494
2085
|
};
|
|
1495
2086
|
}
|
|
1496
|
-
return {
|
|
2087
|
+
return {
|
|
2088
|
+
content: [
|
|
2089
|
+
{
|
|
2090
|
+
type: "text",
|
|
2091
|
+
text: `Function "${name}" not found. Use \`list_functions()\` to see all available functions.`,
|
|
2092
|
+
},
|
|
2093
|
+
],
|
|
2094
|
+
};
|
|
1497
2095
|
});
|
|
1498
2096
|
// Tool: list_functions
|
|
1499
2097
|
server.tool("list_functions", "List storefront API functions. Without a `category`, returns category names + counts so you can drill in. With a `category`, returns one-line summaries for that category. Use `limit`/`offset` to paginate.", {
|
|
@@ -1542,7 +2140,7 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
|
|
|
1542
2140
|
if (uncategorized > 0) {
|
|
1543
2141
|
lines.push(`- \`Other\` (${uncategorized})`);
|
|
1544
2142
|
}
|
|
1545
|
-
lines.push("",
|
|
2143
|
+
lines.push("", 'Call `list_functions(category: "<name>")` to see one-line summaries for a category.');
|
|
1546
2144
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1547
2145
|
}
|
|
1548
2146
|
const catLower = category.toLowerCase();
|
|
@@ -1574,7 +2172,11 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
|
|
|
1574
2172
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1575
2173
|
});
|
|
1576
2174
|
// Tool: get_code_example
|
|
1577
|
-
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.", {
|
|
2175
|
+
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.", {
|
|
2176
|
+
task: z
|
|
2177
|
+
.string()
|
|
2178
|
+
.describe("Task description or example ID (call `list_examples()` for the full list)"),
|
|
2179
|
+
}, async ({ task }) => {
|
|
1578
2180
|
const taskLower = task.toLowerCase();
|
|
1579
2181
|
// Try exact ID match first
|
|
1580
2182
|
let example = storefrontData.codeExamples.find((e) => e.id === taskLower);
|
|
@@ -1594,7 +2196,9 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
1594
2196
|
}
|
|
1595
2197
|
}
|
|
1596
2198
|
if (!example) {
|
|
1597
|
-
const available = storefrontData.codeExamples
|
|
2199
|
+
const available = storefrontData.codeExamples
|
|
2200
|
+
.map((e) => ` - \`${e.id}\` - ${e.title}`)
|
|
2201
|
+
.join("\n");
|
|
1598
2202
|
return {
|
|
1599
2203
|
content: [
|
|
1600
2204
|
{
|
|
@@ -1617,20 +2221,30 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
1617
2221
|
if (example.files && example.files.length > 0) {
|
|
1618
2222
|
for (const file of example.files) {
|
|
1619
2223
|
const ext = file.filename.split(".").pop() || "text";
|
|
1620
|
-
const lang = ext === "tsx" || ext === "ts"
|
|
2224
|
+
const lang = ext === "tsx" || ext === "ts"
|
|
2225
|
+
? "typescript"
|
|
2226
|
+
: ext === "css"
|
|
2227
|
+
? "css"
|
|
2228
|
+
: ext === "json"
|
|
2229
|
+
? "json"
|
|
2230
|
+
: "text";
|
|
1621
2231
|
// Add inline originality comments to CSS and TSX files
|
|
1622
2232
|
let content = file.content;
|
|
1623
2233
|
if (ext === "css") {
|
|
1624
|
-
content =
|
|
2234
|
+
content =
|
|
2235
|
+
"/* EXAMPLE STYLING — create your own original CSS with different class names and design */\n" +
|
|
2236
|
+
content;
|
|
1625
2237
|
}
|
|
1626
2238
|
else if (ext === "tsx") {
|
|
1627
|
-
content =
|
|
2239
|
+
content =
|
|
2240
|
+
"// EXAMPLE COMPONENT — use the API patterns but create your own JSX structure and layout\n" +
|
|
2241
|
+
content;
|
|
1628
2242
|
}
|
|
1629
2243
|
parts.push(`### ${file.filename}`, "", `\`\`\`${lang}`, content, "```", "");
|
|
1630
2244
|
}
|
|
1631
2245
|
}
|
|
1632
2246
|
else {
|
|
1633
|
-
parts.push(
|
|
2247
|
+
parts.push(asCodeBlock(example.code), "");
|
|
1634
2248
|
}
|
|
1635
2249
|
parts.push(`**Reminder:** Use the API patterns above (function calls, imports, type handling) but create your own original HTML structure and CSS.`);
|
|
1636
2250
|
parts.push("");
|
|
@@ -1638,63 +2252,95 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
1638
2252
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1639
2253
|
});
|
|
1640
2254
|
// Tool: get_framework_guide
|
|
1641
|
-
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.", {
|
|
2255
|
+
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.", {
|
|
2256
|
+
topic: z
|
|
2257
|
+
.string()
|
|
2258
|
+
.describe("Topic key or keyword (call `list_topics()` for the full list)"),
|
|
2259
|
+
}, async ({ topic }) => {
|
|
1642
2260
|
const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
|
|
1643
2261
|
// Alias mapping for common alternative topic names
|
|
1644
2262
|
const topicAliases = {
|
|
1645
2263
|
"form-handling": "form-patterns",
|
|
1646
|
-
|
|
2264
|
+
forms: "form-patterns",
|
|
1647
2265
|
"data-fetching": "async-data-patterns",
|
|
1648
|
-
|
|
1649
|
-
|
|
2266
|
+
async: "async-data-patterns",
|
|
2267
|
+
loading: "async-data-patterns",
|
|
1650
2268
|
"sub-components": "sub-component-patterns",
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
2269
|
+
subcomponents: "sub-component-patterns",
|
|
2270
|
+
routing: "navigation-patterns",
|
|
2271
|
+
router: "navigation-patterns",
|
|
2272
|
+
observer: "component-structure",
|
|
2273
|
+
reactivity: "component-structure",
|
|
2274
|
+
pitfalls: "common-pitfalls",
|
|
2275
|
+
gotchas: "common-pitfalls",
|
|
2276
|
+
mistakes: "common-pitfalls",
|
|
2277
|
+
header: "header-footer-patterns",
|
|
2278
|
+
footer: "header-footer-patterns",
|
|
2279
|
+
blog: "blog-patterns",
|
|
2280
|
+
cart: "cart-patterns",
|
|
2281
|
+
account: "account-patterns",
|
|
1664
2282
|
"product-detail": "product-detail-patterns",
|
|
1665
2283
|
"product-list": "product-list-patterns",
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
2284
|
+
filtering: "product-list-patterns",
|
|
2285
|
+
reviews: "review-patterns",
|
|
2286
|
+
slider: "slider-overlay-patterns",
|
|
2287
|
+
overlay: "slider-overlay-patterns",
|
|
2288
|
+
modal: "slider-overlay-patterns",
|
|
2289
|
+
architecture: "real-world-architecture",
|
|
2290
|
+
theme: "real-world-architecture",
|
|
1673
2291
|
"global-styles": "global-css",
|
|
1674
|
-
|
|
2292
|
+
global: "global-css",
|
|
1675
2293
|
"css-variables": "global-css",
|
|
1676
2294
|
"custom-properties": "global-css",
|
|
2295
|
+
conditional: "conditional-visibility",
|
|
2296
|
+
"conditional-props": "conditional-visibility",
|
|
2297
|
+
visibility: "conditional-visibility",
|
|
2298
|
+
requiredprop: "conditional-visibility",
|
|
2299
|
+
"required-prop": "conditional-visibility",
|
|
2300
|
+
"dependent-props": "conditional-visibility",
|
|
2301
|
+
"show-hide": "conditional-visibility",
|
|
1677
2302
|
};
|
|
1678
2303
|
const resolvedTopic = topicAliases[topicLower] || topicLower;
|
|
1679
2304
|
// Topics that involve MobX store reads get a reminder about root reactivity
|
|
1680
2305
|
const storeTopics = new Set([
|
|
1681
|
-
"product-detail-patterns",
|
|
1682
|
-
"
|
|
1683
|
-
"
|
|
1684
|
-
"
|
|
2306
|
+
"product-detail-patterns",
|
|
2307
|
+
"product-list-patterns",
|
|
2308
|
+
"cart-patterns",
|
|
2309
|
+
"account-patterns",
|
|
2310
|
+
"header-footer-patterns",
|
|
2311
|
+
"review-patterns",
|
|
2312
|
+
"blog-patterns",
|
|
2313
|
+
"form-handling",
|
|
2314
|
+
"async-data-patterns",
|
|
2315
|
+
"component-structure",
|
|
2316
|
+
"imports",
|
|
1685
2317
|
]);
|
|
1686
2318
|
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";
|
|
1687
2319
|
// Try exact key match (with alias resolution)
|
|
1688
2320
|
if (frameworkData.topics[resolvedTopic]) {
|
|
1689
2321
|
const t = frameworkData.topics[resolvedTopic];
|
|
1690
2322
|
const prefix = storeTopics.has(resolvedTopic) ? observerReminder : "";
|
|
1691
|
-
return {
|
|
2323
|
+
return {
|
|
2324
|
+
content: [
|
|
2325
|
+
{
|
|
2326
|
+
type: "text",
|
|
2327
|
+
text: `## ${t.title}\n\n${prefix}${t.content}`,
|
|
2328
|
+
},
|
|
2329
|
+
],
|
|
2330
|
+
};
|
|
1692
2331
|
}
|
|
1693
2332
|
// Try original topic key (without alias) in case it's a direct key
|
|
1694
2333
|
if (resolvedTopic !== topicLower && frameworkData.topics[topicLower]) {
|
|
1695
2334
|
const t = frameworkData.topics[topicLower];
|
|
1696
2335
|
const prefix = storeTopics.has(topicLower) ? observerReminder : "";
|
|
1697
|
-
return {
|
|
2336
|
+
return {
|
|
2337
|
+
content: [
|
|
2338
|
+
{
|
|
2339
|
+
type: "text",
|
|
2340
|
+
text: `## ${t.title}\n\n${prefix}${t.content}`,
|
|
2341
|
+
},
|
|
2342
|
+
],
|
|
2343
|
+
};
|
|
1698
2344
|
}
|
|
1699
2345
|
// Try keyword search
|
|
1700
2346
|
const matches = searchFrameworkTopics(topic);
|
|
@@ -1702,7 +2348,12 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
|
|
|
1702
2348
|
const best = matches[0];
|
|
1703
2349
|
const prefix = storeTopics.has(best.key) ? observerReminder : "";
|
|
1704
2350
|
return {
|
|
1705
|
-
content: [
|
|
2351
|
+
content: [
|
|
2352
|
+
{
|
|
2353
|
+
type: "text",
|
|
2354
|
+
text: `## ${best.topic.title}\n\n${prefix}${best.topic.content}`,
|
|
2355
|
+
},
|
|
2356
|
+
],
|
|
1706
2357
|
};
|
|
1707
2358
|
}
|
|
1708
2359
|
const available = Object.entries(frameworkData.topics)
|
|
@@ -1718,16 +2369,27 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
|
|
|
1718
2369
|
};
|
|
1719
2370
|
});
|
|
1720
2371
|
// Tool: get_type_definition
|
|
1721
|
-
server.tool("get_type_definition", "Get the full definition of a storefront type or enum by name (e.g. 'IkasProduct', 'IkasOrderStatus')
|
|
2372
|
+
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`.", {
|
|
2373
|
+
name: z
|
|
2374
|
+
.string()
|
|
2375
|
+
.describe("Type or enum name (e.g. 'IkasProduct', 'IkasOrderStatus')"),
|
|
2376
|
+
}, async ({ name }) => {
|
|
1722
2377
|
if (!typesData) {
|
|
1723
2378
|
return {
|
|
1724
|
-
content: [
|
|
2379
|
+
content: [
|
|
2380
|
+
{
|
|
2381
|
+
type: "text",
|
|
2382
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2383
|
+
},
|
|
2384
|
+
],
|
|
1725
2385
|
};
|
|
1726
2386
|
}
|
|
1727
2387
|
const nameLower = name.toLowerCase();
|
|
1728
2388
|
const td = typesData.types.find((t) => t.name.toLowerCase() === nameLower);
|
|
1729
2389
|
if (td) {
|
|
1730
|
-
return {
|
|
2390
|
+
return {
|
|
2391
|
+
content: [{ type: "text", text: formatTypeDefinition(td) }],
|
|
2392
|
+
};
|
|
1731
2393
|
}
|
|
1732
2394
|
// Fuzzy match
|
|
1733
2395
|
const matches = typesData.types.filter((t) => t.name.toLowerCase().includes(nameLower));
|
|
@@ -1743,14 +2405,28 @@ server.tool("get_type_definition", "Get the full definition of a storefront type
|
|
|
1743
2405
|
};
|
|
1744
2406
|
}
|
|
1745
2407
|
return {
|
|
1746
|
-
content: [
|
|
2408
|
+
content: [
|
|
2409
|
+
{
|
|
2410
|
+
type: "text",
|
|
2411
|
+
text: `Type "${name}" not found. Use \`list_types()\` to see all available types.`,
|
|
2412
|
+
},
|
|
2413
|
+
],
|
|
1747
2414
|
};
|
|
1748
2415
|
});
|
|
1749
2416
|
// Tool: get_functions_for_type
|
|
1750
|
-
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.", {
|
|
2417
|
+
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.", {
|
|
2418
|
+
typeName: z
|
|
2419
|
+
.string()
|
|
2420
|
+
.describe("Type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
|
|
2421
|
+
}, async ({ typeName }) => {
|
|
1751
2422
|
if (!typesData) {
|
|
1752
2423
|
return {
|
|
1753
|
-
content: [
|
|
2424
|
+
content: [
|
|
2425
|
+
{
|
|
2426
|
+
type: "text",
|
|
2427
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2428
|
+
},
|
|
2429
|
+
],
|
|
1754
2430
|
};
|
|
1755
2431
|
}
|
|
1756
2432
|
const nameLower = typeName.toLowerCase();
|
|
@@ -1770,7 +2446,12 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
|
|
|
1770
2446
|
};
|
|
1771
2447
|
}
|
|
1772
2448
|
return {
|
|
1773
|
-
content: [
|
|
2449
|
+
content: [
|
|
2450
|
+
{
|
|
2451
|
+
type: "text",
|
|
2452
|
+
text: `Type "${typeName}" not found. Use \`list_types()\` to see all available types.`,
|
|
2453
|
+
},
|
|
2454
|
+
],
|
|
1774
2455
|
};
|
|
1775
2456
|
}
|
|
1776
2457
|
if (!td.relatedFunctions || td.relatedFunctions.length === 0) {
|
|
@@ -1801,7 +2482,10 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
|
|
|
1801
2482
|
});
|
|
1802
2483
|
// Tool: get_model_guide
|
|
1803
2484
|
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.", {
|
|
1804
|
-
model: z
|
|
2485
|
+
model: z
|
|
2486
|
+
.string()
|
|
2487
|
+
.optional()
|
|
2488
|
+
.describe("Model type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
|
|
1805
2489
|
name: z.string().optional().describe("Alias for 'model'"),
|
|
1806
2490
|
mode: z
|
|
1807
2491
|
.enum(["summary", "full"])
|
|
@@ -1812,12 +2496,22 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
1812
2496
|
const model = modelParam || nameParam;
|
|
1813
2497
|
if (!model) {
|
|
1814
2498
|
return {
|
|
1815
|
-
content: [
|
|
2499
|
+
content: [
|
|
2500
|
+
{
|
|
2501
|
+
type: "text",
|
|
2502
|
+
text: "Please provide a model name (e.g. 'IkasProduct', 'IkasOrder'). Use the 'model' or 'name' parameter.",
|
|
2503
|
+
},
|
|
2504
|
+
],
|
|
1816
2505
|
};
|
|
1817
2506
|
}
|
|
1818
2507
|
if (!typesData) {
|
|
1819
2508
|
return {
|
|
1820
|
-
content: [
|
|
2509
|
+
content: [
|
|
2510
|
+
{
|
|
2511
|
+
type: "text",
|
|
2512
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2513
|
+
},
|
|
2514
|
+
],
|
|
1821
2515
|
};
|
|
1822
2516
|
}
|
|
1823
2517
|
const modelLower = model.toLowerCase();
|
|
@@ -1839,7 +2533,12 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
1839
2533
|
};
|
|
1840
2534
|
}
|
|
1841
2535
|
return {
|
|
1842
|
-
content: [
|
|
2536
|
+
content: [
|
|
2537
|
+
{
|
|
2538
|
+
type: "text",
|
|
2539
|
+
text: `Model "${model}" not found. Use \`list_types()\` to see all available types.`,
|
|
2540
|
+
},
|
|
2541
|
+
],
|
|
1843
2542
|
};
|
|
1844
2543
|
}
|
|
1845
2544
|
const parts = [`# Model Guide: ${td.name}\n`];
|
|
@@ -1947,13 +2646,23 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
1947
2646
|
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 }) => {
|
|
1948
2647
|
if (!typesData) {
|
|
1949
2648
|
return {
|
|
1950
|
-
content: [
|
|
2649
|
+
content: [
|
|
2650
|
+
{
|
|
2651
|
+
type: "text",
|
|
2652
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2653
|
+
},
|
|
2654
|
+
],
|
|
1951
2655
|
};
|
|
1952
2656
|
}
|
|
1953
2657
|
const results = searchTypes(query).slice(0, 15);
|
|
1954
2658
|
if (results.length === 0) {
|
|
1955
2659
|
return {
|
|
1956
|
-
content: [
|
|
2660
|
+
content: [
|
|
2661
|
+
{
|
|
2662
|
+
type: "text",
|
|
2663
|
+
text: `No types found matching "${query}". Use \`list_types()\` to see all available types.`,
|
|
2664
|
+
},
|
|
2665
|
+
],
|
|
1957
2666
|
};
|
|
1958
2667
|
}
|
|
1959
2668
|
const parts = [`## Type Search Results for "${query}"\n`];
|
|
@@ -2017,7 +2726,7 @@ server.tool("list_types", "List storefront types and enums. Use `domain` and/or
|
|
|
2017
2726
|
for (const [d, count] of sorted) {
|
|
2018
2727
|
lines.push(`- \`${d}\` (${count})`);
|
|
2019
2728
|
}
|
|
2020
|
-
lines.push("",
|
|
2729
|
+
lines.push("", 'Call `list_types(domain: "<name>")` to see summaries for a domain.');
|
|
2021
2730
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2022
2731
|
}
|
|
2023
2732
|
const domainLower = domain.toLowerCase();
|
|
@@ -2056,23 +2765,50 @@ server.tool("list_types", "List storefront types and enums. Use `domain` and/or
|
|
|
2056
2765
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
2057
2766
|
});
|
|
2058
2767
|
// Tool: get_prop_types
|
|
2059
|
-
server.tool("get_prop_types",
|
|
2768
|
+
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 () => {
|
|
2060
2769
|
const propTypesTopic = frameworkData.topics["prop-types"];
|
|
2061
2770
|
if (propTypesTopic) {
|
|
2062
2771
|
return {
|
|
2063
|
-
content: [
|
|
2772
|
+
content: [
|
|
2773
|
+
{
|
|
2774
|
+
type: "text",
|
|
2775
|
+
text: `## ${propTypesTopic.title}\n\n${propTypesTopic.content}`,
|
|
2776
|
+
},
|
|
2777
|
+
],
|
|
2064
2778
|
};
|
|
2065
2779
|
}
|
|
2066
2780
|
return {
|
|
2067
|
-
content: [
|
|
2781
|
+
content: [
|
|
2782
|
+
{
|
|
2783
|
+
type: "text",
|
|
2784
|
+
text: "Prop types documentation not available.",
|
|
2785
|
+
},
|
|
2786
|
+
],
|
|
2068
2787
|
};
|
|
2069
2788
|
});
|
|
2070
2789
|
// Tool: get_section_template
|
|
2790
|
+
// Starter templates that rebuild a page ikas already ships as a ready-made BUILT-IN page. Asked to
|
|
2791
|
+
// "build a login page", the agent reaches for the template before any other tool — so the
|
|
2792
|
+
// ask-the-user gate rides this response, where it cannot be skimmed past.
|
|
2793
|
+
const BUILTIN_COVERED_TEMPLATES = new Set([
|
|
2794
|
+
"login-section",
|
|
2795
|
+
"register-section",
|
|
2796
|
+
"forgot-password-section",
|
|
2797
|
+
"recover-password-section",
|
|
2798
|
+
"email-verification-section",
|
|
2799
|
+
"account-info-section",
|
|
2800
|
+
"favorites",
|
|
2801
|
+
]);
|
|
2802
|
+
const BUILTIN_COVERED_BANNER = [
|
|
2803
|
+
"STOP — ASK THE USER BEFORE CODING THIS.",
|
|
2804
|
+
"This template rebuilds a page ikas already ships as a ready-made page: fully functional auth/account flows out of the box, customized and localized via `update_ready_made_page_prop`, the whole group enabled with `enable_ready_made_pages` (see `list_ready_made_pages`).",
|
|
2805
|
+
"If the user has not EXPLICITLY chosen a custom-coded page, do not write code yet. Ask them now: (a) ikas's ready-made page — instant, maintained by ikas — or (b) a custom section with full design freedom. List the ready-made option FIRST as the recommended default; custom comes second. Continue with this template ONLY after they explicitly choose custom.",
|
|
2806
|
+
].join("\n");
|
|
2071
2807
|
// Build the enum dynamically from the on-disk section templates directory
|
|
2072
2808
|
const sectionTemplateKeys = sectionTemplateNames.length > 0
|
|
2073
2809
|
? sectionTemplateNames
|
|
2074
2810
|
: null;
|
|
2075
|
-
server.tool("get_section_template", "Get the root files of a starter section template (index.tsx, types.ts, styles.css, ikas-config-snippet.json). Returns ONLY the section's root files plus the NAMES of any children, components, sub-components, utilities, and hooks — their files are NOT included by default. To view one item's full implementation, call `get_section_child(section, name, kind)` where kind is 'children' (default), 'components', or 'sub-components'. To bundle subtrees inline, pass `include`. Call `list_section_types()` for available section types. Use the API patterns shown — create your own JSX structure, CSS class names, and visual design.", {
|
|
2811
|
+
server.tool("get_section_template", "Get the root files of a starter section template (index.tsx, types.ts, styles.css, ikas-config-snippet.json). Returns ONLY the section's root files plus the NAMES of any children, components, sub-components, utilities, and hooks — their files are NOT included by default. To view one item's full implementation, call `get_section_child(section, name, kind)` where kind is 'children' (default), 'components', or 'sub-components'. To bundle subtrees inline, pass `include`. Call `list_section_types()` for available section types. Use the API patterns shown — create your own JSX structure, CSS class names, and visual design. **Container sections** (Header, Footer, ProductDetail, etc.) host child components via a `COMPONENT_LIST` slot — the response emits a complete multi-step Setup Recipe (create children → capture ids → wire `filteredComponentIds` via `config update-prop`). Follow all steps; the parent alone produces an empty section.", {
|
|
2076
2812
|
sectionType: z
|
|
2077
2813
|
.string()
|
|
2078
2814
|
.describe("The section type (call `list_section_types()` for valid values)"),
|
|
@@ -2114,11 +2850,19 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2114
2850
|
}
|
|
2115
2851
|
const normalizedType = normalizeName(sectionType);
|
|
2116
2852
|
if (!sectionTemplateNames.includes(normalizedType)) {
|
|
2117
|
-
|
|
2853
|
+
const suggestions = suggestClosestNames(normalizedType, sectionTemplateNames);
|
|
2854
|
+
let suggestionText = "";
|
|
2855
|
+
if (suggestions.length === 1) {
|
|
2856
|
+
suggestionText = ` Did you mean "${suggestions[0]}"?`;
|
|
2857
|
+
}
|
|
2858
|
+
else if (suggestions.length > 1) {
|
|
2859
|
+
suggestionText = ` Did you mean one of: ${suggestions.map((s) => `"${s}"`).join(", ")}?`;
|
|
2860
|
+
}
|
|
2861
|
+
return {
|
|
2118
2862
|
content: [
|
|
2119
2863
|
{
|
|
2120
2864
|
type: "text",
|
|
2121
|
-
text: `Unknown section type "${sectionType}"
|
|
2865
|
+
text: `Unknown section type "${sectionType}".${suggestionText} Call \`list_section_types()\` to see all ${sectionTemplateNames.length} valid types.`,
|
|
2122
2866
|
},
|
|
2123
2867
|
],
|
|
2124
2868
|
};
|
|
@@ -2144,9 +2888,25 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2144
2888
|
(bundle.componentNames.length > 0 && !componentsFullyIncluded) ||
|
|
2145
2889
|
(bundle.subComponentNames.length > 0 && !subComponentsFullyIncluded);
|
|
2146
2890
|
const parts = [
|
|
2891
|
+
...(BUILTIN_COVERED_TEMPLATES.has(normalizedType)
|
|
2892
|
+
? [BUILTIN_COVERED_BANNER, ""]
|
|
2893
|
+
: []),
|
|
2147
2894
|
`## ${bundle.title} — API Integration Pattern Reference`,
|
|
2148
2895
|
"",
|
|
2149
2896
|
];
|
|
2897
|
+
// Detect container-section pattern (COMPONENT_LIST with `<id-of-X>` placeholders).
|
|
2898
|
+
// Surface this BEFORE every other warning so the LLM cannot miss the wiring requirement.
|
|
2899
|
+
// The full recipe (commands, captured-id placeholders, update-prop call) is appended near
|
|
2900
|
+
// the end of the response in the existing recipe-builder block.
|
|
2901
|
+
{
|
|
2902
|
+
const snippetStrForBanner = bundle.rootFiles["ikas-config-snippet.json"];
|
|
2903
|
+
if (snippetStrForBanner &&
|
|
2904
|
+
/<id-of-[A-Za-z0-9_]+>/.test(snippetStrForBanner)) {
|
|
2905
|
+
const childMatches = Array.from(snippetStrForBanner.matchAll(/<id-of-([A-Za-z0-9_]+)>/g));
|
|
2906
|
+
const uniqueChildren = Array.from(new Set(childMatches.map((m) => m[1])));
|
|
2907
|
+
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.`, "");
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2150
2910
|
if (anyOutstandingSubtree) {
|
|
2151
2911
|
parts.push("> **PARTIAL TEMPLATE — NOT A COMPLETE IMPLEMENTATION.** The files below are the section's root files only. The section also ships with child components, local components, and/or shared sub-components whose files are **not included** in this response. To view one's full implementation, call `get_section_child(section, name, kind)` for each item you need (kind = `children`, `components`, or `sub-components`). The root `index.tsx` alone is not a complete reference — its imports resolve to files that live in those subtrees.", "");
|
|
2152
2912
|
}
|
|
@@ -2215,7 +2975,10 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2215
2975
|
renderInlineSubtree("children", bundle.childContents);
|
|
2216
2976
|
renderInlineSubtree("components", bundle.componentContents);
|
|
2217
2977
|
renderInlineSubtree("sub-components", bundle.subComponentContents);
|
|
2218
|
-
// Generate a ready-to-run CLI
|
|
2978
|
+
// Generate a ready-to-run CLI recipe from the config snippet. If the parent's
|
|
2979
|
+
// filteredComponentIds reference `<id-of-X>` placeholders, expand into a
|
|
2980
|
+
// multi-step recipe (create children → create parent → wire filteredComponentIds)
|
|
2981
|
+
// so the LLM cannot skip the wiring step.
|
|
2219
2982
|
const configSnippetStr = bundle.rootFiles["ikas-config-snippet.json"];
|
|
2220
2983
|
if (configSnippetStr) {
|
|
2221
2984
|
try {
|
|
@@ -2223,26 +2986,105 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2223
2986
|
const compName = configSnippet.name || normalizedType;
|
|
2224
2987
|
const compType = configSnippet.type || "section";
|
|
2225
2988
|
const propsArr = configSnippet.props || [];
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2989
|
+
// Strip filteredComponentIds for the parent's add-component call (wired
|
|
2990
|
+
// separately below). Preserve name/type/displayName/required so the LLM gets the prop shape.
|
|
2991
|
+
const parentPropsForAdd = propsArr.map((p) => {
|
|
2992
|
+
const out = { name: p.name, type: p.type };
|
|
2993
|
+
if (p.displayName)
|
|
2994
|
+
out.displayName = p.displayName;
|
|
2995
|
+
if (p.required)
|
|
2996
|
+
out.required = true;
|
|
2997
|
+
return out;
|
|
2998
|
+
});
|
|
2999
|
+
const placeholderRe = /^<id-of-(.+)>$/;
|
|
3000
|
+
const slotPlans = [];
|
|
3001
|
+
const allChildNames = new Set();
|
|
3002
|
+
for (const p of propsArr) {
|
|
3003
|
+
if ((p.type === "COMPONENT_LIST" || p.type === "COMPONENT") &&
|
|
3004
|
+
Array.isArray(p.filteredComponentIds)) {
|
|
3005
|
+
const childNames = [];
|
|
3006
|
+
for (const entry of p.filteredComponentIds) {
|
|
3007
|
+
const m = typeof entry === "string" && entry.match(placeholderRe);
|
|
3008
|
+
if (m) {
|
|
3009
|
+
childNames.push(m[1]);
|
|
3010
|
+
allChildNames.add(m[1]);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
if (childNames.length > 0) {
|
|
3014
|
+
slotPlans.push({ propName: p.name, childNames });
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
const childPlans = [];
|
|
3019
|
+
for (const childName of allChildNames) {
|
|
3020
|
+
const childSnippetPath = path.join(SECTION_TEMPLATES_DIR, normalizedType, "children", childName, "ikas-config-snippet.json");
|
|
3021
|
+
let propsJson = "[]";
|
|
3022
|
+
let hasTemplate = false;
|
|
3023
|
+
if (fs.existsSync(childSnippetPath)) {
|
|
3024
|
+
hasTemplate = true;
|
|
3025
|
+
try {
|
|
3026
|
+
const childSnippet = JSON.parse(fs.readFileSync(childSnippetPath, "utf-8"));
|
|
3027
|
+
const childProps = (childSnippet.props || []).map((p) => {
|
|
3028
|
+
const out = {
|
|
3029
|
+
name: p.name,
|
|
3030
|
+
type: p.type,
|
|
3031
|
+
};
|
|
3032
|
+
if (p.displayName)
|
|
3033
|
+
out.displayName = p.displayName;
|
|
3034
|
+
if (p.required)
|
|
3035
|
+
out.required = true;
|
|
3036
|
+
return out;
|
|
3037
|
+
});
|
|
3038
|
+
propsJson = JSON.stringify(childProps);
|
|
3039
|
+
}
|
|
3040
|
+
catch {
|
|
3041
|
+
// fall through with []
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
childPlans.push({ name: childName, propsJson, hasTemplate });
|
|
3045
|
+
}
|
|
3046
|
+
const baseFlags = (configSnippet.isHeader ? " --isHeader" : "") +
|
|
3047
|
+
(configSnippet.isFooter ? " --isFooter" : "");
|
|
3048
|
+
const parentPropsJsonStr = parentPropsForAdd.length > 0
|
|
3049
|
+
? ` --props '${JSON.stringify(parentPropsForAdd)}'`
|
|
3050
|
+
: "";
|
|
3051
|
+
if (slotPlans.length > 0) {
|
|
3052
|
+
parts.push("---", "");
|
|
3053
|
+
parts.push(`### Setup Recipe (run in order — ${childPlans.length} child component${childPlans.length === 1 ? "" : "s"} + 1 parent + wiring)`);
|
|
3054
|
+
parts.push("");
|
|
3055
|
+
parts.push(`> ⚠️ **This section is a CONTAINER.** It hosts child components via ${slotPlans.length === 1 ? "a COMPONENT_LIST slot" : "COMPONENT_LIST slots"} (\`${slotPlans.map((s) => s.propName).join("`, `")}\`). Creating the parent alone is **not enough** — you MUST also create the child components and wire their opaque ids into the parent's \`filteredComponentIds\`. Skipping the wiring leaves the slot empty and the section unusable.`, "");
|
|
3056
|
+
parts.push("**Step 1 — Create each child component, and capture its `componentId` from the JSON response:**");
|
|
3057
|
+
parts.push("", "```bash");
|
|
3058
|
+
for (const ch of childPlans) {
|
|
3059
|
+
parts.push(`npx ikas-component config add-component --name "${ch.name}" --type component --props '${ch.propsJson}'`);
|
|
3060
|
+
parts.push(`# → { "success": true, "componentId": "<capture as ${ch.name.toUpperCase()}_ID>", ... }`);
|
|
3061
|
+
if (!ch.hasTemplate) {
|
|
3062
|
+
parts.push(`# (no children/${ch.name}/ template in this section bundle — \`--props '[]'\` is a stub; add real props for ${ch.name} as needed)`);
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
parts.push("```", "");
|
|
3066
|
+
parts.push("**Step 2 — Create the parent section (without `filteredComponentIds` yet — those are wired in Step 3):**");
|
|
3067
|
+
parts.push("", "```bash");
|
|
3068
|
+
parts.push(`npx ikas-component config add-component --name "${compName}" --type ${compType}${baseFlags}${parentPropsJsonStr}`);
|
|
3069
|
+
parts.push("```", "");
|
|
3070
|
+
parts.push("**Step 3 — Wire each slot to its allowed children using the ids captured in Step 1:**");
|
|
3071
|
+
parts.push("", "```bash");
|
|
3072
|
+
for (const slot of slotPlans) {
|
|
3073
|
+
const idsArr = slot.childNames.map((n) => `<${n.toUpperCase()}_ID>`);
|
|
3074
|
+
parts.push(`npx ikas-component config update-prop --component "${compName}" --prop ${slot.propName} \\`);
|
|
3075
|
+
parts.push(` --filteredComponentIds '${JSON.stringify(idsArr)}'`);
|
|
3076
|
+
}
|
|
3077
|
+
parts.push("```", "");
|
|
3078
|
+
parts.push("Replace each `<X_ID>` placeholder above with the real `componentId` from the corresponding Step 1 response (or look ids up with `config list`). The CLI rejects unknown ids with a structured error — there is no silent failure mode.");
|
|
3079
|
+
parts.push("");
|
|
3080
|
+
parts.push("**Do NOT manually create or edit `types.ts`** — the CLI commands above regenerate it automatically.");
|
|
3081
|
+
parts.push("");
|
|
3082
|
+
}
|
|
3083
|
+
else {
|
|
3084
|
+
// No child slots — single-step parent command (preserves previous behaviour)
|
|
3085
|
+
const cliCommand = `npx ikas-component config add-component --name "${compName}" --type ${compType}${baseFlags}${parentPropsJsonStr}`;
|
|
3086
|
+
parts.push("---", "", "### CLI Command (run this first)", "", "```bash", cliCommand, "```", "", "**Do NOT manually create or edit `types.ts`** — the CLI command above generates it automatically.", "");
|
|
2244
3087
|
}
|
|
2245
|
-
parts.push("---", "", "### CLI Command (run this first)", "", "```bash", cliCommand, "```", "", "**Do NOT manually create or edit `types.ts`** — the CLI command above generates it automatically.", "");
|
|
2246
3088
|
}
|
|
2247
3089
|
catch {
|
|
2248
3090
|
// ignore parse errors
|
|
@@ -2259,10 +3101,7 @@ server.tool("get_section_child", "Fetch one item's files from a section's childr
|
|
|
2259
3101
|
.string()
|
|
2260
3102
|
.optional()
|
|
2261
3103
|
.describe("The item name as listed in `get_section_template`'s response (Children/Components/Sub-components)"),
|
|
2262
|
-
child: z
|
|
2263
|
-
.string()
|
|
2264
|
-
.optional()
|
|
2265
|
-
.describe("Alias for `name`"),
|
|
3104
|
+
child: z.string().optional().describe("Alias for `name`"),
|
|
2266
3105
|
kind: z
|
|
2267
3106
|
.enum(["children", "components", "sub-components"])
|
|
2268
3107
|
.optional()
|
|
@@ -2418,14 +3257,21 @@ server.tool("list_section_types", "List all available `get_section_template` sec
|
|
|
2418
3257
|
];
|
|
2419
3258
|
for (const name of sectionTemplateNames) {
|
|
2420
3259
|
const meta = loadSectionMeta(name);
|
|
2421
|
-
|
|
3260
|
+
const builtinMark = BUILTIN_COVERED_TEMPLATES.has(name)
|
|
3261
|
+
? " [ikas ships a ready-made page for this — ask the user before coding a custom one; see list_ready_made_pages]"
|
|
3262
|
+
: "";
|
|
3263
|
+
lines.push(`- \`${name}\`${meta ? ` — ${meta.title}` : ""}${builtinMark}`);
|
|
2422
3264
|
}
|
|
2423
3265
|
lines.push("", "Call `get_section_template(sectionType)` to fetch one.");
|
|
2424
3266
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2425
3267
|
});
|
|
2426
3268
|
// --- Migration tools ---
|
|
2427
3269
|
// Tool: analyze_old_theme
|
|
2428
|
-
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.", {
|
|
3270
|
+
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.", {
|
|
3271
|
+
theme_json: z
|
|
3272
|
+
.string()
|
|
3273
|
+
.describe("The raw JSON content of the old theme.json file"),
|
|
3274
|
+
}, async ({ theme_json }) => {
|
|
2429
3275
|
try {
|
|
2430
3276
|
const parsed = JSON.parse(theme_json);
|
|
2431
3277
|
const analysis = analyzeOldTheme(parsed);
|
|
@@ -2433,102 +3279,162 @@ server.tool("analyze_old_theme", "Analyze an old ikas storefront theme.json and
|
|
|
2433
3279
|
}
|
|
2434
3280
|
catch (err) {
|
|
2435
3281
|
return {
|
|
2436
|
-
content: [
|
|
3282
|
+
content: [
|
|
3283
|
+
{
|
|
3284
|
+
type: "text",
|
|
3285
|
+
text: `Error parsing theme.json: ${err instanceof Error ? err.message : String(err)}. Make sure you're passing valid JSON.`,
|
|
3286
|
+
},
|
|
3287
|
+
],
|
|
2437
3288
|
};
|
|
2438
3289
|
}
|
|
2439
3290
|
});
|
|
2440
3291
|
// Tool: get_migration_guide
|
|
2441
3292
|
const migrationTopicAliases = {
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
3293
|
+
overview: "migration-overview",
|
|
3294
|
+
migrate: "migration-overview",
|
|
3295
|
+
custom: "custom-data-conversion",
|
|
2445
3296
|
"custom-data": "custom-data-conversion",
|
|
2446
|
-
|
|
3297
|
+
customdata: "custom-data-conversion",
|
|
2447
3298
|
"dynamic-list": "custom-data-conversion",
|
|
2448
3299
|
"component-list": "custom-data-conversion",
|
|
2449
|
-
|
|
2450
|
-
|
|
3300
|
+
slider: "prop-type-mapping",
|
|
3301
|
+
props: "prop-type-mapping",
|
|
2451
3302
|
"prop-mapping": "prop-type-mapping",
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
3303
|
+
types: "prop-type-mapping",
|
|
3304
|
+
react: "react-to-preact",
|
|
3305
|
+
preact: "react-to-preact",
|
|
3306
|
+
observer: "react-to-preact",
|
|
3307
|
+
libraries: "library-replacements",
|
|
3308
|
+
swiper: "library-replacements",
|
|
3309
|
+
headlessui: "library-replacements",
|
|
3310
|
+
tailwind: "library-replacements",
|
|
3311
|
+
tailwindcss: "library-replacements",
|
|
3312
|
+
recharts: "library-replacements",
|
|
3313
|
+
marquee: "library-replacements",
|
|
3314
|
+
imports: "storefront-import-mapping",
|
|
3315
|
+
storefront: "storefront-import-mapping",
|
|
2465
3316
|
"bp-storefront": "storefront-import-mapping",
|
|
2466
3317
|
"theme-json": "theme-json-anatomy",
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
3318
|
+
anatomy: "theme-json-anatomy",
|
|
3319
|
+
decompose: "component-decomposition-strategy",
|
|
3320
|
+
decomposition: "component-decomposition-strategy",
|
|
3321
|
+
strategy: "component-decomposition-strategy",
|
|
3322
|
+
project: "complete-project-generation",
|
|
3323
|
+
generate: "complete-project-generation",
|
|
3324
|
+
generation: "complete-project-generation",
|
|
3325
|
+
settings: "settings-conversion",
|
|
3326
|
+
colors: "settings-conversion",
|
|
3327
|
+
fonts: "settings-conversion",
|
|
3328
|
+
find: "finding-new-system-equivalents",
|
|
3329
|
+
search: "finding-new-system-equivalents",
|
|
3330
|
+
discover: "finding-new-system-equivalents",
|
|
3331
|
+
equivalent: "finding-new-system-equivalents",
|
|
3332
|
+
equivalents: "finding-new-system-equivalents",
|
|
3333
|
+
replacement: "finding-new-system-equivalents",
|
|
2483
3334
|
};
|
|
2484
3335
|
const migrationTopicKeys = migrationData
|
|
2485
3336
|
? Object.keys(migrationData.topics)
|
|
2486
3337
|
: [];
|
|
2487
|
-
server.tool("get_migration_guide", `Get a migration guide for converting old ikas themes to the new code-component system.${migrationTopicKeys.length > 0 ? ` Available topics: ${migrationTopicKeys.join(", ")}. Also supports aliases like "custom", "slider", "react", "libraries", "imports", "settings".` : ""} Call with topic "list" to see all available topics.`, {
|
|
3338
|
+
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.`, {
|
|
3339
|
+
topic: z
|
|
3340
|
+
.string()
|
|
3341
|
+
.describe("Migration topic key, alias, or 'list' to see all topics"),
|
|
3342
|
+
}, async ({ topic }) => {
|
|
2488
3343
|
if (!migrationData) {
|
|
2489
|
-
return {
|
|
3344
|
+
return {
|
|
3345
|
+
content: [
|
|
3346
|
+
{
|
|
3347
|
+
type: "text",
|
|
3348
|
+
text: "Migration data not available. Ensure data/migration.json exists.",
|
|
3349
|
+
},
|
|
3350
|
+
],
|
|
3351
|
+
};
|
|
2490
3352
|
}
|
|
2491
3353
|
if (topic.toLowerCase() === "list") {
|
|
2492
3354
|
const available = Object.entries(migrationData.topics)
|
|
2493
3355
|
.map(([key, t]) => `- \`${key}\` — ${t.title}: ${t.description}`)
|
|
2494
3356
|
.join("\n");
|
|
2495
|
-
return {
|
|
3357
|
+
return {
|
|
3358
|
+
content: [
|
|
3359
|
+
{
|
|
3360
|
+
type: "text",
|
|
3361
|
+
text: `## Available Migration Topics\n\n${available}`,
|
|
3362
|
+
},
|
|
3363
|
+
],
|
|
3364
|
+
};
|
|
2496
3365
|
}
|
|
2497
3366
|
const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
|
|
2498
3367
|
const resolvedTopic = migrationTopicAliases[topicLower] || topicLower;
|
|
2499
3368
|
if (migrationData.topics[resolvedTopic]) {
|
|
2500
3369
|
const t = migrationData.topics[resolvedTopic];
|
|
2501
|
-
return {
|
|
3370
|
+
return {
|
|
3371
|
+
content: [
|
|
3372
|
+
{ type: "text", text: `## ${t.title}\n\n${t.content}` },
|
|
3373
|
+
],
|
|
3374
|
+
};
|
|
2502
3375
|
}
|
|
2503
3376
|
// Try original key
|
|
2504
3377
|
if (resolvedTopic !== topicLower && migrationData.topics[topicLower]) {
|
|
2505
3378
|
const t = migrationData.topics[topicLower];
|
|
2506
|
-
return {
|
|
3379
|
+
return {
|
|
3380
|
+
content: [
|
|
3381
|
+
{ type: "text", text: `## ${t.title}\n\n${t.content}` },
|
|
3382
|
+
],
|
|
3383
|
+
};
|
|
2507
3384
|
}
|
|
2508
3385
|
// Keyword search
|
|
2509
3386
|
const matches = searchMigrationTopics(topic);
|
|
2510
3387
|
if (matches.length > 0) {
|
|
2511
3388
|
const best = matches[0];
|
|
2512
|
-
return {
|
|
3389
|
+
return {
|
|
3390
|
+
content: [
|
|
3391
|
+
{
|
|
3392
|
+
type: "text",
|
|
3393
|
+
text: `## ${best.topic.title}\n\n${best.topic.content}`,
|
|
3394
|
+
},
|
|
3395
|
+
],
|
|
3396
|
+
};
|
|
2513
3397
|
}
|
|
2514
3398
|
const available = Object.entries(migrationData.topics)
|
|
2515
3399
|
.map(([key, t]) => ` - \`${key}\` - ${t.title}`)
|
|
2516
3400
|
.join("\n");
|
|
2517
3401
|
return {
|
|
2518
|
-
content: [
|
|
3402
|
+
content: [
|
|
3403
|
+
{
|
|
3404
|
+
type: "text",
|
|
3405
|
+
text: `Migration topic "${topic}" not found. Available topics:\n${available}`,
|
|
3406
|
+
},
|
|
3407
|
+
],
|
|
2519
3408
|
};
|
|
2520
3409
|
});
|
|
2521
3410
|
// Tool: get_migration_example
|
|
2522
|
-
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.`, {
|
|
3411
|
+
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.`, {
|
|
3412
|
+
example: z.string().describe("Example name or 'list' to see all examples"),
|
|
3413
|
+
}, async ({ example }) => {
|
|
2523
3414
|
if (example.toLowerCase() === "list") {
|
|
2524
3415
|
if (migrationExampleNames.length === 0) {
|
|
2525
|
-
return {
|
|
3416
|
+
return {
|
|
3417
|
+
content: [
|
|
3418
|
+
{ type: "text", text: "No migration examples available." },
|
|
3419
|
+
],
|
|
3420
|
+
};
|
|
2526
3421
|
}
|
|
2527
|
-
const list = migrationExampleNames
|
|
3422
|
+
const list = migrationExampleNames
|
|
3423
|
+
.map((name) => {
|
|
2528
3424
|
const ex = loadMigrationExample(name);
|
|
2529
|
-
return ex
|
|
2530
|
-
|
|
2531
|
-
|
|
3425
|
+
return ex
|
|
3426
|
+
? `- \`${name}\` — ${ex.title}: ${ex.description}`
|
|
3427
|
+
: `- \`${name}\``;
|
|
3428
|
+
})
|
|
3429
|
+
.join("\n");
|
|
3430
|
+
return {
|
|
3431
|
+
content: [
|
|
3432
|
+
{
|
|
3433
|
+
type: "text",
|
|
3434
|
+
text: `## Available Migration Examples\n\n${list}`,
|
|
3435
|
+
},
|
|
3436
|
+
],
|
|
3437
|
+
};
|
|
2532
3438
|
}
|
|
2533
3439
|
const exampleLower = example.toLowerCase();
|
|
2534
3440
|
let exName = migrationExampleNames.find((n) => n === exampleLower);
|
|
@@ -2538,19 +3444,26 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
|
|
|
2538
3444
|
if (!exName) {
|
|
2539
3445
|
const available = migrationExampleNames.join(", ");
|
|
2540
3446
|
return {
|
|
2541
|
-
content: [
|
|
3447
|
+
content: [
|
|
3448
|
+
{
|
|
3449
|
+
type: "text",
|
|
3450
|
+
text: `Migration example "${example}" not found. Available: ${available}`,
|
|
3451
|
+
},
|
|
3452
|
+
],
|
|
2542
3453
|
};
|
|
2543
3454
|
}
|
|
2544
3455
|
const ex = loadMigrationExample(exName);
|
|
2545
3456
|
if (!ex) {
|
|
2546
|
-
return {
|
|
3457
|
+
return {
|
|
3458
|
+
content: [
|
|
3459
|
+
{
|
|
3460
|
+
type: "text",
|
|
3461
|
+
text: `Failed to load migration example "${exName}".`,
|
|
3462
|
+
},
|
|
3463
|
+
],
|
|
3464
|
+
};
|
|
2547
3465
|
}
|
|
2548
|
-
const parts = [
|
|
2549
|
-
`## ${ex.title}`,
|
|
2550
|
-
"",
|
|
2551
|
-
ex.description,
|
|
2552
|
-
"",
|
|
2553
|
-
];
|
|
3466
|
+
const parts = [`## ${ex.title}`, "", ex.description, ""];
|
|
2554
3467
|
for (const [filename, content] of Object.entries(ex.files)) {
|
|
2555
3468
|
const ext = filename.split(".").pop() || "text";
|
|
2556
3469
|
const lang = ext === "tsx" || ext === "ts"
|
|
@@ -2568,41 +3481,1759 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
|
|
|
2568
3481
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
2569
3482
|
});
|
|
2570
3483
|
// Tool: plan_migration
|
|
2571
|
-
server.tool("plan_migration", "Generate
|
|
2572
|
-
theme_json: z
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
3484
|
+
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).", {
|
|
3485
|
+
theme_json: z
|
|
3486
|
+
.string()
|
|
3487
|
+
.optional()
|
|
3488
|
+
.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."),
|
|
3489
|
+
theme_json_path: z
|
|
3490
|
+
.string()
|
|
3491
|
+
.optional()
|
|
3492
|
+
.describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
|
|
3493
|
+
project_name: z
|
|
3494
|
+
.string()
|
|
3495
|
+
.optional()
|
|
3496
|
+
.describe("Target new project name, used to prefix migration-tracking IDs (default: 'my-theme')"),
|
|
3497
|
+
old_source_dir: z
|
|
3498
|
+
.string()
|
|
3499
|
+
.optional()
|
|
3500
|
+
.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."),
|
|
3501
|
+
project_root: z
|
|
3502
|
+
.string()
|
|
3503
|
+
.optional()
|
|
3504
|
+
.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."),
|
|
3505
|
+
overwrite: z
|
|
3506
|
+
.boolean()
|
|
3507
|
+
.optional()
|
|
3508
|
+
.describe("If MIGRATION.md already exists at <project_root>/MIGRATION.md and is non-empty, refuse the write unless this is true. Default: false."),
|
|
3509
|
+
}, async ({ theme_json, theme_json_path, project_name, old_source_dir, project_root, overwrite, }) => {
|
|
2576
3510
|
try {
|
|
2577
|
-
const parsed =
|
|
3511
|
+
const parsed = resolveThemeJson(theme_json, theme_json_path);
|
|
2578
3512
|
const projectName = project_name || "my-theme";
|
|
2579
3513
|
const plan = generateMigrationPlan(parsed, projectName, old_source_dir);
|
|
2580
|
-
|
|
3514
|
+
if (!project_root) {
|
|
3515
|
+
return { content: [{ type: "text", text: plan }] };
|
|
3516
|
+
}
|
|
3517
|
+
// Write MIGRATION.md to project_root
|
|
3518
|
+
if (!path.isAbsolute(project_root)) {
|
|
3519
|
+
throw new Error(`project_root must be absolute: ${project_root}`);
|
|
3520
|
+
}
|
|
3521
|
+
if (!fs.existsSync(project_root)) {
|
|
3522
|
+
throw new Error(`project_root not found: ${project_root}`);
|
|
3523
|
+
}
|
|
3524
|
+
if (!fs.statSync(project_root).isDirectory()) {
|
|
3525
|
+
throw new Error(`project_root is not a directory: ${project_root}`);
|
|
3526
|
+
}
|
|
3527
|
+
const targetPath = path.join(project_root, "MIGRATION.md");
|
|
3528
|
+
if (fs.existsSync(targetPath)) {
|
|
3529
|
+
const existing = fs.readFileSync(targetPath, "utf-8").trim();
|
|
3530
|
+
if (existing.length > 0 && !overwrite) {
|
|
3531
|
+
return {
|
|
3532
|
+
content: [
|
|
3533
|
+
{
|
|
3534
|
+
type: "text",
|
|
3535
|
+
text: `Refusing to overwrite existing non-empty MIGRATION.md at ${targetPath}. ` +
|
|
3536
|
+
`Pass overwrite: true to replace it, or delete the file first. ` +
|
|
3537
|
+
`If you intended to RESUME an in-progress migration, do not call plan_migration again — read the existing MIGRATION.md and continue from the first unchecked item.`,
|
|
3538
|
+
},
|
|
3539
|
+
],
|
|
3540
|
+
};
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
writeFileAtomic(targetPath, plan);
|
|
3544
|
+
const components = parsed.components || [];
|
|
3545
|
+
const customData = parsed.customData || [];
|
|
3546
|
+
const cssVarCount = parsed.settings?.colors?.length || 0;
|
|
3547
|
+
const fontCount = parsed.settings?.fontFamily?.name ? 1 : 0;
|
|
3548
|
+
const customDataCount = customData.filter((cd) => cd.isRoot).length;
|
|
3549
|
+
const sectionCount = components.length;
|
|
3550
|
+
const summary = [
|
|
3551
|
+
`Wrote initial migration plan to ${targetPath}`,
|
|
3552
|
+
"",
|
|
3553
|
+
`**Summary:**`,
|
|
3554
|
+
`- Sections to migrate: ${sectionCount}`,
|
|
3555
|
+
`- CSS variables: ${cssVarCount}`,
|
|
3556
|
+
`- Fonts: ${fontCount}`,
|
|
3557
|
+
`- Custom data types (deferred decisions, not pre-migrated): ${customDataCount}`,
|
|
3558
|
+
"",
|
|
3559
|
+
`**Next steps for the LLM:**`,
|
|
3560
|
+
`1. Read \`${targetPath}\` start-to-finish. The "READ THIS FIRST" preamble explains your responsibilities.`,
|
|
3561
|
+
`2. Scan the old source directory (\`${old_source_dir || "<old-src>"}\`) for atomic components (Button, Input, Card, icons, etc.) NOT referenced from theme.json. Add them to \`## Source Code Analysis\` in MIGRATION.md.`,
|
|
3562
|
+
`3. Start the Foundation work (CSS variables, fonts, shared sub-components).`,
|
|
3563
|
+
`4. For each section, call \`get_section_migration_plan({theme_json_path, section_name, project_name})\`. The MCP will tell you which props need enum-vs-component decisions.`,
|
|
3564
|
+
`5. Log every custom-data decision in \`## Custom Data Decisions\`. Tick checkboxes as you finish.`,
|
|
3565
|
+
].join("\n");
|
|
3566
|
+
return { content: [{ type: "text", text: summary }] };
|
|
2581
3567
|
}
|
|
2582
3568
|
catch (err) {
|
|
2583
3569
|
return {
|
|
2584
|
-
content: [
|
|
3570
|
+
content: [
|
|
3571
|
+
{
|
|
3572
|
+
type: "text",
|
|
3573
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
3574
|
+
},
|
|
3575
|
+
],
|
|
2585
3576
|
};
|
|
2586
3577
|
}
|
|
2587
3578
|
});
|
|
2588
3579
|
// Tool: get_section_migration_plan
|
|
2589
|
-
server.tool("get_section_migration_plan", "
|
|
2590
|
-
theme_json: z
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
3580
|
+
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.", {
|
|
3581
|
+
theme_json: z
|
|
3582
|
+
.string()
|
|
3583
|
+
.optional()
|
|
3584
|
+
.describe("Raw JSON content of the old theme.json. EITHER this OR theme_json_path is required (not both)."),
|
|
3585
|
+
theme_json_path: z
|
|
3586
|
+
.string()
|
|
3587
|
+
.optional()
|
|
3588
|
+
.describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
|
|
3589
|
+
section_name: z
|
|
3590
|
+
.string()
|
|
3591
|
+
.describe("Old component name (e.g. 'Navbar', 'ProductGrid') or dir name, OR the new section ID (e.g. 'my-theme-navbar')"),
|
|
3592
|
+
project_name: z
|
|
3593
|
+
.string()
|
|
3594
|
+
.optional()
|
|
3595
|
+
.describe("Target new project name (must match what was used in plan_migration). Default: 'my-theme'"),
|
|
3596
|
+
old_source_dir: z
|
|
3597
|
+
.string()
|
|
3598
|
+
.optional()
|
|
3599
|
+
.describe("Absolute path to old src/ directory (used to output exact source file paths to read)"),
|
|
3600
|
+
}, async ({ theme_json, theme_json_path, section_name, project_name, old_source_dir, }) => {
|
|
2595
3601
|
try {
|
|
2596
|
-
const parsed =
|
|
3602
|
+
const parsed = resolveThemeJson(theme_json, theme_json_path);
|
|
2597
3603
|
const projectName = project_name || "my-theme";
|
|
2598
3604
|
const plan = generateSectionMigrationPlan(parsed, section_name, projectName, old_source_dir);
|
|
2599
3605
|
return { content: [{ type: "text", text: plan }] };
|
|
2600
3606
|
}
|
|
2601
3607
|
catch (err) {
|
|
2602
3608
|
return {
|
|
2603
|
-
content: [
|
|
3609
|
+
content: [
|
|
3610
|
+
{
|
|
3611
|
+
type: "text",
|
|
3612
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
3613
|
+
},
|
|
3614
|
+
],
|
|
3615
|
+
};
|
|
3616
|
+
}
|
|
3617
|
+
});
|
|
3618
|
+
// =============================================================================
|
|
3619
|
+
// Editor-action tools — drive the connected editor over the dev server's WS
|
|
3620
|
+
// (port 5201) by shelling out to `ikas-component <cmd>`. The CLI is the
|
|
3621
|
+
// canonical implementation; these tools are thin advertisements for the LLM.
|
|
3622
|
+
// =============================================================================
|
|
3623
|
+
function resolveIkasComponentBinary(projectRoot) {
|
|
3624
|
+
if (!path.isAbsolute(projectRoot)) {
|
|
3625
|
+
throw new Error(`project_root must be absolute: ${projectRoot}`);
|
|
3626
|
+
}
|
|
3627
|
+
if (!fs.existsSync(projectRoot) || !fs.statSync(projectRoot).isDirectory()) {
|
|
3628
|
+
throw new Error(`project_root is not a directory: ${projectRoot}`);
|
|
3629
|
+
}
|
|
3630
|
+
const binDir = path.join(projectRoot, "node_modules", ".bin");
|
|
3631
|
+
const candidates = os.platform() === "win32"
|
|
3632
|
+
? ["ikas-component.cmd", "ikas-component.exe", "ikas-component"]
|
|
3633
|
+
: ["ikas-component"];
|
|
3634
|
+
for (const name of candidates) {
|
|
3635
|
+
const full = path.join(binDir, name);
|
|
3636
|
+
if (fs.existsSync(full))
|
|
3637
|
+
return full;
|
|
3638
|
+
}
|
|
3639
|
+
throw new Error(`ikas-component CLI not found at ${binDir}. Run \`npm install\` (or \`pnpm install\`) in ${projectRoot} first.`);
|
|
3640
|
+
}
|
|
3641
|
+
async function runIkasComponentCli(projectRoot, args) {
|
|
3642
|
+
const bin = resolveIkasComponentBinary(projectRoot);
|
|
3643
|
+
return new Promise((resolve) => {
|
|
3644
|
+
execFile(bin, args,
|
|
3645
|
+
// Large editor responses (e.g. list_categories on a big store, or
|
|
3646
|
+
// add_sections_to_page echoing many sections) easily exceed Node's default
|
|
3647
|
+
// 1MB stdout buffer, which truncates output mid-JSON and yields a
|
|
3648
|
+
// "no parseable JSON" error. Give it generous headroom.
|
|
3649
|
+
{ cwd: projectRoot, windowsHide: true, maxBuffer: 64 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
3650
|
+
const exitCode = err && typeof err.code === "number"
|
|
3651
|
+
? err.code
|
|
3652
|
+
: err
|
|
3653
|
+
? 1
|
|
3654
|
+
: 0;
|
|
3655
|
+
resolve({
|
|
3656
|
+
stdout: stdout?.toString() ?? "",
|
|
3657
|
+
stderr: stderr?.toString() ?? "",
|
|
3658
|
+
exitCode,
|
|
3659
|
+
});
|
|
3660
|
+
});
|
|
3661
|
+
});
|
|
3662
|
+
}
|
|
3663
|
+
function parseCliJson(stdout) {
|
|
3664
|
+
const trimmed = stdout.trim();
|
|
3665
|
+
if (!trimmed)
|
|
3666
|
+
return null;
|
|
3667
|
+
// Fast path: the CLI prints exactly one JSON object on stdout.
|
|
3668
|
+
try {
|
|
3669
|
+
return JSON.parse(trimmed);
|
|
3670
|
+
}
|
|
3671
|
+
catch {
|
|
3672
|
+
// Fallback: if anything leaked onto stdout before/after the JSON, scan the
|
|
3673
|
+
// lines and return the last one that parses as JSON.
|
|
3674
|
+
const lines = trimmed
|
|
3675
|
+
.split("\n")
|
|
3676
|
+
.map((l) => l.trim())
|
|
3677
|
+
.filter(Boolean);
|
|
3678
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3679
|
+
try {
|
|
3680
|
+
return JSON.parse(lines[i]);
|
|
3681
|
+
}
|
|
3682
|
+
catch {
|
|
3683
|
+
// keep scanning
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
return null;
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
// Coerce a JSON-ARRAY argument (points / settings / colors) into the single JSON-array string the
|
|
3690
|
+
// CLI expects. Agents sometimes send these PRE-STRINGIFIED (a JSON string instead of a real array);
|
|
3691
|
+
// a blind JSON.stringify would then double-encode it ("[...]" -> "\"[...]\"") and the CLI rejects it
|
|
3692
|
+
// with "must be a JSON array". So pass a string through as-is (the CLI parses + validates it) and
|
|
3693
|
+
// only stringify real arrays/values. NOTE: only for array args — a scalar `value` arg must NOT use
|
|
3694
|
+
// this (a plain string there is a literal value, not stringified JSON).
|
|
3695
|
+
function toJsonArrayArg(value) {
|
|
3696
|
+
return typeof value === "string" ? value : JSON.stringify(value ?? []);
|
|
3697
|
+
}
|
|
3698
|
+
/**
|
|
3699
|
+
* The `confirm` parameter shared by the destructive editor tools.
|
|
3700
|
+
*
|
|
3701
|
+
* Spelled out at length on purpose: a one-word "confirmation" flag reads to a model as a
|
|
3702
|
+
* formality to fill in, and the whole point is that it stands for a human's answer.
|
|
3703
|
+
*/
|
|
3704
|
+
const CONFIRM_PARAM_DESCRIPTION = "Must be `true`, and only after the USER has explicitly agreed to this specific deletion. It stands for their consent, not for your own confidence \u2014 never set it to clear up something you merely believe is unused, and never re-run with it after a refusal without asking first. The merchant's content goes with the deletion and only the editor's undo stack can bring it back.";
|
|
3705
|
+
/** Reply for a destructive tool called without consent. The dev-server action refuses too. */
|
|
3706
|
+
function destructiveConfirmRefusal() {
|
|
3707
|
+
return {
|
|
3708
|
+
content: [
|
|
3709
|
+
{
|
|
3710
|
+
type: "text",
|
|
3711
|
+
text: JSON.stringify({
|
|
3712
|
+
success: false,
|
|
3713
|
+
error: "Refused: `confirm` was not true. This deletes the merchant's content and can only be undone from the editor's undo stack. Ask the user whether to proceed, and pass confirm: true only after they agree.",
|
|
3714
|
+
}, null, 2),
|
|
3715
|
+
},
|
|
3716
|
+
],
|
|
3717
|
+
};
|
|
3718
|
+
}
|
|
3719
|
+
async function callEditorAction(projectRoot, args) {
|
|
3720
|
+
try {
|
|
3721
|
+
const { stdout, stderr, exitCode } = await runIkasComponentCli(projectRoot, args);
|
|
3722
|
+
const parsed = parseCliJson(stdout);
|
|
3723
|
+
if (parsed) {
|
|
3724
|
+
return {
|
|
3725
|
+
content: [
|
|
3726
|
+
{ type: "text", text: JSON.stringify(parsed, null, 2) },
|
|
3727
|
+
],
|
|
3728
|
+
};
|
|
3729
|
+
}
|
|
3730
|
+
return {
|
|
3731
|
+
content: [
|
|
3732
|
+
{
|
|
3733
|
+
type: "text",
|
|
3734
|
+
text: `CLI exited with code ${exitCode} and produced no parseable JSON.\n` +
|
|
3735
|
+
`stdout:\n${stdout || "(empty)"}\nstderr:\n${stderr || "(empty)"}`,
|
|
3736
|
+
},
|
|
3737
|
+
],
|
|
3738
|
+
};
|
|
3739
|
+
}
|
|
3740
|
+
catch (err) {
|
|
3741
|
+
return {
|
|
3742
|
+
content: [
|
|
3743
|
+
{
|
|
3744
|
+
type: "text",
|
|
3745
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
3746
|
+
},
|
|
3747
|
+
],
|
|
2604
3748
|
};
|
|
2605
3749
|
}
|
|
3750
|
+
}
|
|
3751
|
+
//
|
|
3752
|
+
// Tool: list_editor_pages
|
|
3753
|
+
server.tool("list_editor_pages", "List pages in the connected editor's project. Returns id, name, pageType, slug for each page. Requires `ikas theme dev` to be running with the editor connected. Use the returned `id`s as the `pageId` argument to `add_section_to_page`.", {
|
|
3754
|
+
project_root: z
|
|
3755
|
+
.string()
|
|
3756
|
+
.describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
|
|
3757
|
+
port: z
|
|
3758
|
+
.number()
|
|
3759
|
+
.optional()
|
|
3760
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3761
|
+
}, async ({ project_root, port }) => {
|
|
3762
|
+
const args = ["list-pages", ...(port ? ["--port", String(port)] : [])];
|
|
3763
|
+
return callEditorAction(project_root, args);
|
|
3764
|
+
});
|
|
3765
|
+
// Tool: list_imported_sections
|
|
3766
|
+
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`.", {
|
|
3767
|
+
project_root: z
|
|
3768
|
+
.string()
|
|
3769
|
+
.describe("Absolute path to the code-component project."),
|
|
3770
|
+
port: z
|
|
3771
|
+
.number()
|
|
3772
|
+
.optional()
|
|
3773
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3774
|
+
}, async ({ project_root, port }) => {
|
|
3775
|
+
const args = [
|
|
3776
|
+
"list-imported",
|
|
3777
|
+
"--sections-only",
|
|
3778
|
+
...(port ? ["--port", String(port)] : []),
|
|
3779
|
+
];
|
|
3780
|
+
return callEditorAction(project_root, args);
|
|
3781
|
+
});
|
|
3782
|
+
// Tool: import_section
|
|
3783
|
+
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`).", {
|
|
3784
|
+
project_root: z
|
|
3785
|
+
.string()
|
|
3786
|
+
.describe("Absolute path to the code-component project."),
|
|
3787
|
+
component_id: z
|
|
3788
|
+
.string()
|
|
3789
|
+
.describe("Component id from `ikas.config.json` (strict — no name resolution)."),
|
|
3790
|
+
port: z
|
|
3791
|
+
.number()
|
|
3792
|
+
.optional()
|
|
3793
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3794
|
+
}, async ({ project_root, component_id, port }) => {
|
|
3795
|
+
const args = [
|
|
3796
|
+
"import",
|
|
3797
|
+
"--id",
|
|
3798
|
+
component_id,
|
|
3799
|
+
...(port ? ["--port", String(port)] : []),
|
|
3800
|
+
];
|
|
3801
|
+
return callEditorAction(project_root, args);
|
|
3802
|
+
});
|
|
3803
|
+
// Tool: add_section_to_page
|
|
3804
|
+
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).", {
|
|
3805
|
+
project_root: z
|
|
3806
|
+
.string()
|
|
3807
|
+
.describe("Absolute path to the code-component project."),
|
|
3808
|
+
component_id: z
|
|
3809
|
+
.string()
|
|
3810
|
+
.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)."),
|
|
3811
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3812
|
+
index: z
|
|
3813
|
+
.number()
|
|
3814
|
+
.int()
|
|
3815
|
+
.nonnegative()
|
|
3816
|
+
.optional()
|
|
3817
|
+
.describe("Zero-based insertion index in the page; appends when omitted."),
|
|
3818
|
+
port: z
|
|
3819
|
+
.number()
|
|
3820
|
+
.optional()
|
|
3821
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3822
|
+
}, async ({ project_root, component_id, page_id, index, port }) => {
|
|
3823
|
+
const args = [
|
|
3824
|
+
"add-to-page",
|
|
3825
|
+
"--component-id",
|
|
3826
|
+
component_id,
|
|
3827
|
+
"--page-id",
|
|
3828
|
+
page_id,
|
|
3829
|
+
...(typeof index === "number" ? ["--index", String(index)] : []),
|
|
3830
|
+
...(port ? ["--port", String(port)] : []),
|
|
3831
|
+
];
|
|
3832
|
+
return callEditorAction(project_root, args);
|
|
3833
|
+
});
|
|
3834
|
+
// Tool: add_sections_to_page
|
|
3835
|
+
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. Placing a COMMON (global) section — the theme's header/footer — is special: it has ONE shared set of prop values stored on the INDEX (Home) page's placement, so `updates` land there and change that section on every page (the result carries a `common` block), and placing it with `updates` on a non-INDEX page while the INDEX page does not have it is rejected — place and fill it on the INDEX page instead.", {
|
|
3836
|
+
project_root: z
|
|
3837
|
+
.string()
|
|
3838
|
+
.describe("Absolute path to the code-component project."),
|
|
3839
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3840
|
+
sections: z
|
|
3841
|
+
.array(z.object({
|
|
3842
|
+
component_id: z
|
|
3843
|
+
.string()
|
|
3844
|
+
.describe("Imported (or built) section-type code component id."),
|
|
3845
|
+
index: z
|
|
3846
|
+
.number()
|
|
3847
|
+
.int()
|
|
3848
|
+
.nonnegative()
|
|
3849
|
+
.optional()
|
|
3850
|
+
.describe("Zero-based insertion index; appends when omitted."),
|
|
3851
|
+
updates: z
|
|
3852
|
+
.array(z.object({
|
|
3853
|
+
prop_id: z
|
|
3854
|
+
.string()
|
|
3855
|
+
.optional()
|
|
3856
|
+
.describe("Blueprint prop id (provide this or prop_name)."),
|
|
3857
|
+
prop_name: z
|
|
3858
|
+
.string()
|
|
3859
|
+
.optional()
|
|
3860
|
+
.describe("Blueprint prop name (alternative to prop_id)."),
|
|
3861
|
+
value: z
|
|
3862
|
+
.any()
|
|
3863
|
+
.describe("Prop value, same shape update_section_prop expects for that type."),
|
|
3864
|
+
}))
|
|
3865
|
+
.optional()
|
|
3866
|
+
.describe("Optional prop values to set on this section right after placing it."),
|
|
3867
|
+
}))
|
|
3868
|
+
.describe("Non-empty array of sections to place (in order), each with its componentId and optional prop updates."),
|
|
3869
|
+
port: z
|
|
3870
|
+
.number()
|
|
3871
|
+
.optional()
|
|
3872
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3873
|
+
}, async ({ project_root, page_id, sections, port }) => {
|
|
3874
|
+
const normalized = (sections || []).map((s) => ({
|
|
3875
|
+
componentId: s.component_id,
|
|
3876
|
+
...(typeof s.index === "number" ? { index: s.index } : {}),
|
|
3877
|
+
...(s.updates
|
|
3878
|
+
? {
|
|
3879
|
+
updates: s.updates.map((u) => ({
|
|
3880
|
+
...(u.prop_id ? { propId: u.prop_id } : {}),
|
|
3881
|
+
...(u.prop_name ? { propName: u.prop_name } : {}),
|
|
3882
|
+
value: u.value,
|
|
3883
|
+
})),
|
|
3884
|
+
}
|
|
3885
|
+
: {}),
|
|
3886
|
+
}));
|
|
3887
|
+
const args = [
|
|
3888
|
+
"add-sections-to-page",
|
|
3889
|
+
"--page-id",
|
|
3890
|
+
page_id,
|
|
3891
|
+
"--sections",
|
|
3892
|
+
JSON.stringify(normalized),
|
|
3893
|
+
...(port ? ["--port", String(port)] : []),
|
|
3894
|
+
];
|
|
3895
|
+
return callEditorAction(project_root, args);
|
|
3896
|
+
});
|
|
3897
|
+
// Tool: list_page_sections
|
|
3898
|
+
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. COMMON (global) sections — the theme's header and footer — carry an extra `common` field (`isCommon`, `commonSectionType`, `canonicalPageId`/`canonicalElementId`, `valuesArePageSpecific: false`): their prop values are NOT page-specific, they are shared by every page and stored on the INDEX (Home) page's placement, so `setPropNames` for them reflects that one shared record and any write from any page changes the header/footer everywhere. A page customized per storefront-routing/specification holds a SEPARATE set of placements with its own section order in each state: this lists the BASE layer by default and reports the available states in `pageStates` (plus a `scope` field) — pass `sf_routing_id`/`specification_id` to list a state's own placements instead, which is what `move_page_section` needs when reordering that state.", {
|
|
3899
|
+
project_root: z
|
|
3900
|
+
.string()
|
|
3901
|
+
.describe("Absolute path to the code-component project."),
|
|
3902
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3903
|
+
sf_routing_id: z
|
|
3904
|
+
.string()
|
|
3905
|
+
.optional()
|
|
3906
|
+
.describe("List a routing-customized state of the page instead of the base layer. Ids come from the `pageStates` block this tool returns for a customized page."),
|
|
3907
|
+
specification_id: z
|
|
3908
|
+
.string()
|
|
3909
|
+
.optional()
|
|
3910
|
+
.describe("Specification of the customized state to list (used together with `sf_routing_id`)."),
|
|
3911
|
+
port: z
|
|
3912
|
+
.number()
|
|
3913
|
+
.optional()
|
|
3914
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3915
|
+
}, async ({ project_root, page_id, sf_routing_id, specification_id, port }) => {
|
|
3916
|
+
const args = [
|
|
3917
|
+
"list-page-sections",
|
|
3918
|
+
"--page-id",
|
|
3919
|
+
page_id,
|
|
3920
|
+
...(sf_routing_id ? ["--sf-routing-id", sf_routing_id] : []),
|
|
3921
|
+
...(specification_id ? ["--specification-id", specification_id] : []),
|
|
3922
|
+
...(port ? ["--port", String(port)] : []),
|
|
3923
|
+
];
|
|
3924
|
+
return callEditorAction(project_root, args);
|
|
3925
|
+
});
|
|
3926
|
+
// Tool: move_page_section
|
|
3927
|
+
server.tool("move_page_section", "Reorder a section that is ALREADY placed on a page — the only way to change section order, since the editor UI offers it solely as a drag in the layers tree. Target the placement by its `element_id` from `list_page_sections` (NOT the sectionId/componentId — a page can hold several placements of the same section and each moves independently). Give EITHER `direction` (\"up\"/\"down\", one position — use this for a simple nudge) OR `to_index` (absolute 0-based position), never both. Indices are 0-based and count ONLY placed sections, in exactly the order `list_page_sections` returns them, so you can read that list, pick a target position from it, and pass it straight back. The result returns `fromIndex`, `resultIndex` (where the section ACTUALLY ended up) and `sections` (the full resulting order) — assert on `resultIndex`/`sections` rather than assuming the move landed where you asked; if it lands somewhere other than requested a `note` says so. A reorder that does not take effect comes back as an ERROR, never as a success that changed nothing, so a successful result means the section really moved. Out-of-range indices are rejected with the valid range (and the section's current index) instead of being clamped. Passing a `sectionId`/`componentId` where an `element_id` belongs is detected and the error names the elementId you actually wanted. COMMON (global) sections — the theme's header and footer — are REJECTED: their position is fixed by the theme, so there is nothing to reorder. SCOPE: by default this reorders the page's BASE layer. A page customized per storefront-routing/specification keeps a SEPARATE set of placements and its own section order in each of those states — pass `sf_routing_id` (and `specification_id`) to reorder one of them instead, and read that state's current order with `list_page_sections` using the SAME ids. Whenever the page has any state the result carries `scope`, `pageStates` and `pageStatesNote`: do not report a reorder as verified on such a page without checking them, since reordering the base layer leaves every state exactly as it was. The state must ALREADY exist — creating one duplicates the whole page and is a deliberate editor action, so an unknown routing/specification is rejected with the list of existing states rather than created. To place a NEW section at a specific position use `add_sections_to_page` with its `index` instead; this tool only moves existing placements.", {
|
|
3928
|
+
project_root: z
|
|
3929
|
+
.string()
|
|
3930
|
+
.describe("Absolute path to the code-component project."),
|
|
3931
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3932
|
+
element_id: z
|
|
3933
|
+
.string()
|
|
3934
|
+
.describe("Placed-section elementId to move (from `list_page_sections`)."),
|
|
3935
|
+
direction: z
|
|
3936
|
+
.enum(["up", "down"])
|
|
3937
|
+
.optional()
|
|
3938
|
+
.describe("Move one position up or down. Provide this or `to_index`, not both."),
|
|
3939
|
+
to_index: z
|
|
3940
|
+
.number()
|
|
3941
|
+
.int()
|
|
3942
|
+
.min(0)
|
|
3943
|
+
.optional()
|
|
3944
|
+
.describe("Absolute 0-based target position among the page's placed sections. Provide this or `direction`, not both."),
|
|
3945
|
+
sf_routing_id: z
|
|
3946
|
+
.string()
|
|
3947
|
+
.optional()
|
|
3948
|
+
.describe("Reorder a routing-customized state of the page instead of the base layer. Ids come from the `pageStates` block returned by `list_page_sections`. The state must already exist."),
|
|
3949
|
+
specification_id: z
|
|
3950
|
+
.string()
|
|
3951
|
+
.optional()
|
|
3952
|
+
.describe("Specification of the customized state to reorder (used together with `sf_routing_id`)."),
|
|
3953
|
+
port: z
|
|
3954
|
+
.number()
|
|
3955
|
+
.optional()
|
|
3956
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3957
|
+
}, async ({ project_root, page_id, element_id, direction, to_index, sf_routing_id, specification_id, port, }) => {
|
|
3958
|
+
const args = [
|
|
3959
|
+
"move-page-section",
|
|
3960
|
+
"--page-id",
|
|
3961
|
+
page_id,
|
|
3962
|
+
"--element-id",
|
|
3963
|
+
element_id,
|
|
3964
|
+
...(direction ? ["--direction", direction] : []),
|
|
3965
|
+
...(to_index !== undefined ? ["--to-index", String(to_index)] : []),
|
|
3966
|
+
...(sf_routing_id ? ["--sf-routing-id", sf_routing_id] : []),
|
|
3967
|
+
...(specification_id ? ["--specification-id", specification_id] : []),
|
|
3968
|
+
...(port ? ["--port", String(port)] : []),
|
|
3969
|
+
];
|
|
3970
|
+
return callEditorAction(project_root, args);
|
|
3971
|
+
});
|
|
3972
|
+
// Tool: get_component_props
|
|
3973
|
+
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).", {
|
|
3974
|
+
project_root: z
|
|
3975
|
+
.string()
|
|
3976
|
+
.describe("Absolute path to the code-component project."),
|
|
3977
|
+
component_ids: z
|
|
3978
|
+
.array(z.string())
|
|
3979
|
+
.describe("Section/child code-component ids to resolve (batch). From list_imported_sections or a COMPONENT_LIST prop's allowedComponentIds."),
|
|
3980
|
+
port: z
|
|
3981
|
+
.number()
|
|
3982
|
+
.optional()
|
|
3983
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3984
|
+
}, async ({ project_root, component_ids, port }) => {
|
|
3985
|
+
const args = [
|
|
3986
|
+
"get-component-props",
|
|
3987
|
+
"--component-ids",
|
|
3988
|
+
(component_ids || []).join(","),
|
|
3989
|
+
...(port ? ["--port", String(port)] : []),
|
|
3990
|
+
];
|
|
3991
|
+
return callEditorAction(project_root, args);
|
|
3992
|
+
});
|
|
3993
|
+
// Tool: get_section_values
|
|
3994
|
+
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). COMMON (global) sections — the theme's header and footer — keep ONE shared set of prop values for the whole theme, stored on the INDEX (Home) page's placement; for those this tool reads the values FROM that canonical placement (a non-INDEX placement stores nothing of its own) and returns a `common` block naming `canonicalPageId`/`canonicalElementId`. So the values you get for a header/footer are the live ones the page actually renders no matter which page_id you ask from — do NOT read defaults as \"empty\" and blind-fill them, and remember any write goes to that same shared record and changes all pages.", {
|
|
3995
|
+
project_root: z
|
|
3996
|
+
.string()
|
|
3997
|
+
.describe("Absolute path to the code-component project."),
|
|
3998
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3999
|
+
element_ids: z
|
|
4000
|
+
.array(z.string())
|
|
4001
|
+
.describe("Placed-section elementIds to read (batch). From `list_page_sections`."),
|
|
4002
|
+
port: z
|
|
4003
|
+
.number()
|
|
4004
|
+
.optional()
|
|
4005
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4006
|
+
}, async ({ project_root, page_id, element_ids, port }) => {
|
|
4007
|
+
const args = [
|
|
4008
|
+
"get-section-values",
|
|
4009
|
+
"--page-id",
|
|
4010
|
+
page_id,
|
|
4011
|
+
"--element-ids",
|
|
4012
|
+
(element_ids || []).join(","),
|
|
4013
|
+
...(port ? ["--port", String(port)] : []),
|
|
4014
|
+
];
|
|
4015
|
+
return callEditorAction(project_root, args);
|
|
4016
|
+
});
|
|
4017
|
+
// Tool: update_section_prop
|
|
4018
|
+
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`; PRODUCT_ATTRIBUTE = `{ "productId": "...", "variantId": "...", "attributeId": "..." }` — `variantId` is REQUIRED alongside `productId` (without it the value resolves with `product: null` and the component renders nothing), or bind to the page product with `{ "attributeId": "...", "usePageData": true }` on a product detail page; PRODUCT_ATTRIBUTE_LIST = the same with `"attributeIds": [ "..." ]`, and note the component receives a SINGLE wrapper object, never an array (read both with `getAttributeDetailValues()` / `getAttributeListValues()` — see `get_framework_guide("prop-types")`); 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' +
|
|
4019
|
+
"COMMON (global) sections — the theme's HEADER and FOOTER: these render on every page that carries a placement of them (ikas ready-made account/membership pages are born without one) and have ONE shared set of prop values, stored on the INDEX (Home) page's placement. Whichever page_id you write from, the value lands on that single record and changes the header/footer on ALL pages; the response carries a `common` block (`commonSectionType`, `canonicalPageId`, `canonicalElementId`) saying so, and the echoed `value` is read back from that canonical placement. Therefore: ALWAYS `get_section_values` first and change only the prop the user asked about — the props are already filled with the merchant's content even when you are on a page you have not inspected, so blind-filling \"empty-looking\" props overwrites the live header/footer everywhere. If the INDEX page has no placement of the section, there is no record to write to and the call fails telling you to add it to the INDEX page first.\n\n" +
|
|
4020
|
+
"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, ENUM↔ENUM of the SAME enum type — the prop's `enumTypeId` must equal the setting's `enumTypeId`; a mismatch is REJECTED, …). 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" +
|
|
4021
|
+
'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' +
|
|
4022
|
+
'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): ' +
|
|
4023
|
+
`{ "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" } } } ] } } } ] }`, {
|
|
4024
|
+
project_root: z
|
|
4025
|
+
.string()
|
|
4026
|
+
.describe("Absolute path to the code-component project."),
|
|
4027
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
4028
|
+
element_id: z
|
|
4029
|
+
.string()
|
|
4030
|
+
.describe("Placed-section elementId identifying THIS placement on the page (from `list_page_sections`)."),
|
|
4031
|
+
prop_id: z
|
|
4032
|
+
.string()
|
|
4033
|
+
.optional()
|
|
4034
|
+
.describe("Blueprint prop id to update (from `list_page_sections`). Provide this or `prop_name`."),
|
|
4035
|
+
prop_name: z
|
|
4036
|
+
.string()
|
|
4037
|
+
.optional()
|
|
4038
|
+
.describe("Blueprint prop name to update (alternative to `prop_id`)."),
|
|
4039
|
+
value: z
|
|
4040
|
+
.any()
|
|
4041
|
+
.optional()
|
|
4042
|
+
.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`.'),
|
|
4043
|
+
bind_global: z
|
|
4044
|
+
.string()
|
|
4045
|
+
.optional()
|
|
4046
|
+
.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 / ENUM settings; pick a global whose type matches the prop (an ENUM prop only binds an ENUM setting of the same enum type). (This is the general theme-global binding; a COLOR prop can alternatively take an inline `{ \"patternValueId\" }` for theme colors.)"),
|
|
4047
|
+
list_path: z
|
|
4048
|
+
.array(z.object({ propId: z.string(), index: z.number() }))
|
|
4049
|
+
.optional()
|
|
4050
|
+
.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`)."),
|
|
4051
|
+
port: z
|
|
4052
|
+
.number()
|
|
4053
|
+
.optional()
|
|
4054
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4055
|
+
}, async ({ project_root, page_id, element_id, prop_id, prop_name, value, bind_global, list_path, port, }) => {
|
|
4056
|
+
const args = [
|
|
4057
|
+
"update-section-prop",
|
|
4058
|
+
"--page-id",
|
|
4059
|
+
page_id,
|
|
4060
|
+
"--element-id",
|
|
4061
|
+
element_id,
|
|
4062
|
+
...(prop_id ? ["--prop-id", prop_id] : []),
|
|
4063
|
+
...(prop_name ? ["--prop-name", prop_name] : []),
|
|
4064
|
+
...(bind_global
|
|
4065
|
+
? [
|
|
4066
|
+
"--bind-global",
|
|
4067
|
+
bind_global,
|
|
4068
|
+
...(list_path ? ["--list-path", JSON.stringify(list_path)] : []),
|
|
4069
|
+
]
|
|
4070
|
+
: [
|
|
4071
|
+
"--value",
|
|
4072
|
+
// The CLI JSON.parses --value. If `value` is already a JSON string, pass
|
|
4073
|
+
// it through verbatim — re-stringifying it would double-encode it and the
|
|
4074
|
+
// editor would store a string instead of the object/array.
|
|
4075
|
+
typeof value === "string" ? value : JSON.stringify(value),
|
|
4076
|
+
]),
|
|
4077
|
+
...(port ? ["--port", String(port)] : []),
|
|
4078
|
+
];
|
|
4079
|
+
return callEditorAction(project_root, args);
|
|
4080
|
+
});
|
|
4081
|
+
// Tool: upload_image
|
|
4082
|
+
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).', {
|
|
4083
|
+
project_root: z
|
|
4084
|
+
.string()
|
|
4085
|
+
.describe("Absolute path to the code-component project."),
|
|
4086
|
+
file_path: z
|
|
4087
|
+
.string()
|
|
4088
|
+
.optional()
|
|
4089
|
+
.describe("Local image file path (.png, .jpg, .jpeg, .webp, .gif). Provide this or `image_url`."),
|
|
4090
|
+
image_url: z
|
|
4091
|
+
.string()
|
|
4092
|
+
.optional()
|
|
4093
|
+
.describe("Image URL to fetch and upload (alternative to `file_path`)."),
|
|
4094
|
+
alt_text: z
|
|
4095
|
+
.string()
|
|
4096
|
+
.optional()
|
|
4097
|
+
.describe("Alt text to store with the image."),
|
|
4098
|
+
port: z
|
|
4099
|
+
.number()
|
|
4100
|
+
.optional()
|
|
4101
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4102
|
+
}, async ({ project_root, file_path, image_url, alt_text, port }) => {
|
|
4103
|
+
const args = [
|
|
4104
|
+
"upload-image",
|
|
4105
|
+
...(file_path ? ["--file", file_path] : []),
|
|
4106
|
+
...(image_url ? ["--url", image_url] : []),
|
|
4107
|
+
...(alt_text ? ["--alt", alt_text] : []),
|
|
4108
|
+
...(port ? ["--port", String(port)] : []),
|
|
4109
|
+
];
|
|
4110
|
+
return callEditorAction(project_root, args);
|
|
4111
|
+
});
|
|
4112
|
+
// Tool: update_page_sections
|
|
4113
|
+
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. If any targeted section is a COMMON (global) one — the theme's header/footer — its values are shared by every page and stored on the INDEX (Home) page's placement: the write goes there and changes that section on ALL pages, the result's `common` block says so, and the echoed values are read back from that canonical placement. Read those sections with `get_section_values` first and only change what was asked; a header/footer missing from the INDEX page has no record to write to and rejects the whole call.", {
|
|
4114
|
+
project_root: z
|
|
4115
|
+
.string()
|
|
4116
|
+
.describe("Absolute path to the code-component project."),
|
|
4117
|
+
page_id: z.string().describe("Target page id (from list_editor_pages)."),
|
|
4118
|
+
sections: z
|
|
4119
|
+
.array(z.object({
|
|
4120
|
+
element_id: z
|
|
4121
|
+
.string()
|
|
4122
|
+
.describe("Placed-section elementId (from list_page_sections)."),
|
|
4123
|
+
updates: z
|
|
4124
|
+
.array(z.object({
|
|
4125
|
+
prop_id: z
|
|
4126
|
+
.string()
|
|
4127
|
+
.optional()
|
|
4128
|
+
.describe("Blueprint prop id (provide this or prop_name)."),
|
|
4129
|
+
prop_name: z
|
|
4130
|
+
.string()
|
|
4131
|
+
.optional()
|
|
4132
|
+
.describe("Blueprint prop name (alternative to prop_id)."),
|
|
4133
|
+
value: z
|
|
4134
|
+
.any()
|
|
4135
|
+
.optional()
|
|
4136
|
+
.describe("Prop value, same shape update_section_prop expects for that type. Omit when using bind_global."),
|
|
4137
|
+
bind_global: z
|
|
4138
|
+
.string()
|
|
4139
|
+
.optional()
|
|
4140
|
+
.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."),
|
|
4141
|
+
list_path: z
|
|
4142
|
+
.array(z.object({ propId: z.string(), index: z.number() }))
|
|
4143
|
+
.optional()
|
|
4144
|
+
.describe("Only with bind_global: `{ propId, index }` steps down to a COMPONENT_LIST child; prop_id is then the child's prop id."),
|
|
4145
|
+
}))
|
|
4146
|
+
.describe("Non-empty array of prop updates for this section."),
|
|
4147
|
+
}))
|
|
4148
|
+
.describe("Non-empty array of sections to fill, each with its elementId and prop updates."),
|
|
4149
|
+
port: z
|
|
4150
|
+
.number()
|
|
4151
|
+
.optional()
|
|
4152
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4153
|
+
}, async ({ project_root, page_id, sections, port }) => {
|
|
4154
|
+
const normalized = (sections || []).map((s) => ({
|
|
4155
|
+
elementId: s.element_id,
|
|
4156
|
+
updates: (s.updates || []).map((u) => ({
|
|
4157
|
+
...(u.prop_id ? { propId: u.prop_id } : {}),
|
|
4158
|
+
...(u.prop_name ? { propName: u.prop_name } : {}),
|
|
4159
|
+
...(u.bind_global
|
|
4160
|
+
? {
|
|
4161
|
+
bindGlobal: u.bind_global,
|
|
4162
|
+
...(u.list_path ? { listPath: u.list_path } : {}),
|
|
4163
|
+
}
|
|
4164
|
+
: { value: u.value }),
|
|
4165
|
+
})),
|
|
4166
|
+
}));
|
|
4167
|
+
const args = [
|
|
4168
|
+
"update-page-sections",
|
|
4169
|
+
"--page-id",
|
|
4170
|
+
page_id,
|
|
4171
|
+
"--sections",
|
|
4172
|
+
JSON.stringify(normalized),
|
|
4173
|
+
...(port ? ["--port", String(port)] : []),
|
|
4174
|
+
];
|
|
4175
|
+
return callEditorAction(project_root, args);
|
|
4176
|
+
});
|
|
4177
|
+
// Tool: upload_images
|
|
4178
|
+
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.", {
|
|
4179
|
+
project_root: z
|
|
4180
|
+
.string()
|
|
4181
|
+
.describe("Absolute path to the code-component project."),
|
|
4182
|
+
images: z
|
|
4183
|
+
.array(z.object({
|
|
4184
|
+
file_path: z
|
|
4185
|
+
.string()
|
|
4186
|
+
.optional()
|
|
4187
|
+
.describe("Local image file path (provide this or image_url)."),
|
|
4188
|
+
image_url: z
|
|
4189
|
+
.string()
|
|
4190
|
+
.optional()
|
|
4191
|
+
.describe("Image URL to fetch (alternative to file_path)."),
|
|
4192
|
+
alt_text: z.string().optional().describe("Alt text for this image."),
|
|
4193
|
+
}))
|
|
4194
|
+
.describe("Non-empty array of images to upload."),
|
|
4195
|
+
port: z
|
|
4196
|
+
.number()
|
|
4197
|
+
.optional()
|
|
4198
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4199
|
+
}, async ({ project_root, images, port }) => {
|
|
4200
|
+
const manifest = (images || []).map((im) => ({
|
|
4201
|
+
...(im.file_path ? { file: im.file_path } : {}),
|
|
4202
|
+
...(im.image_url ? { url: im.image_url } : {}),
|
|
4203
|
+
...(im.alt_text ? { alt: im.alt_text } : {}),
|
|
4204
|
+
}));
|
|
4205
|
+
const args = [
|
|
4206
|
+
"upload-images",
|
|
4207
|
+
"--manifest",
|
|
4208
|
+
JSON.stringify(manifest),
|
|
4209
|
+
...(port ? ["--port", String(port)] : []),
|
|
4210
|
+
];
|
|
4211
|
+
return callEditorAction(project_root, args);
|
|
4212
|
+
});
|
|
4213
|
+
// Tool: search_products
|
|
4214
|
+
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).', {
|
|
4215
|
+
project_root: z
|
|
4216
|
+
.string()
|
|
4217
|
+
.describe("Absolute path to the code-component project."),
|
|
4218
|
+
query: z
|
|
4219
|
+
.string()
|
|
4220
|
+
.optional()
|
|
4221
|
+
.describe("Free-text product search (name, sku, barcode)."),
|
|
4222
|
+
product_ids: z
|
|
4223
|
+
.array(z.string())
|
|
4224
|
+
.optional()
|
|
4225
|
+
.describe("Specific product ids to resolve directly (alternative to `query`)."),
|
|
4226
|
+
per_page: z.number().optional().describe("Results per page."),
|
|
4227
|
+
port: z
|
|
4228
|
+
.number()
|
|
4229
|
+
.optional()
|
|
4230
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4231
|
+
}, async ({ project_root, query, product_ids, per_page, port }) => {
|
|
4232
|
+
const args = [
|
|
4233
|
+
"search-products",
|
|
4234
|
+
...(query ? ["--query", query] : []),
|
|
4235
|
+
...(product_ids && product_ids.length
|
|
4236
|
+
? ["--ids", product_ids.join(",")]
|
|
4237
|
+
: []),
|
|
4238
|
+
...(typeof per_page === "number" ? ["--per-page", String(per_page)] : []),
|
|
4239
|
+
...(port ? ["--port", String(port)] : []),
|
|
4240
|
+
];
|
|
4241
|
+
return callEditorAction(project_root, args);
|
|
4242
|
+
});
|
|
4243
|
+
// Tool: list_categories
|
|
4244
|
+
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.', {
|
|
4245
|
+
project_root: z
|
|
4246
|
+
.string()
|
|
4247
|
+
.describe("Absolute path to the code-component project."),
|
|
4248
|
+
port: z
|
|
4249
|
+
.number()
|
|
4250
|
+
.optional()
|
|
4251
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4252
|
+
}, async ({ project_root, port }) => {
|
|
4253
|
+
const args = [
|
|
4254
|
+
"list-entities",
|
|
4255
|
+
"--kind",
|
|
4256
|
+
"category",
|
|
4257
|
+
...(port ? ["--port", String(port)] : []),
|
|
4258
|
+
];
|
|
4259
|
+
return callEditorAction(project_root, args);
|
|
4260
|
+
});
|
|
4261
|
+
// Tool: list_brands
|
|
4262
|
+
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.', {
|
|
4263
|
+
project_root: z
|
|
4264
|
+
.string()
|
|
4265
|
+
.describe("Absolute path to the code-component project."),
|
|
4266
|
+
port: z
|
|
4267
|
+
.number()
|
|
4268
|
+
.optional()
|
|
4269
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4270
|
+
}, async ({ project_root, port }) => {
|
|
4271
|
+
const args = [
|
|
4272
|
+
"list-entities",
|
|
4273
|
+
"--kind",
|
|
4274
|
+
"brand",
|
|
4275
|
+
...(port ? ["--port", String(port)] : []),
|
|
4276
|
+
];
|
|
4277
|
+
return callEditorAction(project_root, args);
|
|
4278
|
+
});
|
|
4279
|
+
// Tool: list_blogs
|
|
4280
|
+
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.', {
|
|
4281
|
+
project_root: z
|
|
4282
|
+
.string()
|
|
4283
|
+
.describe("Absolute path to the code-component project."),
|
|
4284
|
+
query: z.string().optional().describe("Free-text blog search."),
|
|
4285
|
+
port: z
|
|
4286
|
+
.number()
|
|
4287
|
+
.optional()
|
|
4288
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4289
|
+
}, async ({ project_root, query, port }) => {
|
|
4290
|
+
const args = [
|
|
4291
|
+
"list-entities",
|
|
4292
|
+
"--kind",
|
|
4293
|
+
"blog",
|
|
4294
|
+
...(query ? ["--query", query] : []),
|
|
4295
|
+
...(port ? ["--port", String(port)] : []),
|
|
4296
|
+
];
|
|
4297
|
+
return callEditorAction(project_root, args);
|
|
4298
|
+
});
|
|
4299
|
+
// Tool: list_blog_categories
|
|
4300
|
+
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.', {
|
|
4301
|
+
project_root: z
|
|
4302
|
+
.string()
|
|
4303
|
+
.describe("Absolute path to the code-component project."),
|
|
4304
|
+
query: z.string().optional().describe("Free-text blog-category search."),
|
|
4305
|
+
port: z
|
|
4306
|
+
.number()
|
|
4307
|
+
.optional()
|
|
4308
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4309
|
+
}, async ({ project_root, query, port }) => {
|
|
4310
|
+
const args = [
|
|
4311
|
+
"list-entities",
|
|
4312
|
+
"--kind",
|
|
4313
|
+
"blog-category",
|
|
4314
|
+
...(query ? ["--query", query] : []),
|
|
4315
|
+
...(port ? ["--port", String(port)] : []),
|
|
4316
|
+
];
|
|
4317
|
+
return callEditorAction(project_root, args);
|
|
4318
|
+
});
|
|
4319
|
+
// Tool: get_page_by_type
|
|
4320
|
+
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).', {
|
|
4321
|
+
project_root: z
|
|
4322
|
+
.string()
|
|
4323
|
+
.describe("Absolute path to the code-component project."),
|
|
4324
|
+
page_type: z
|
|
4325
|
+
.string()
|
|
4326
|
+
.describe("Page type to resolve, e.g. CATEGORY, PRODUCT, BRAND, BLOG, BLOG_CATEGORY, INDEX."),
|
|
4327
|
+
port: z
|
|
4328
|
+
.number()
|
|
4329
|
+
.optional()
|
|
4330
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4331
|
+
}, async ({ project_root, page_type, port }) => {
|
|
4332
|
+
const args = [
|
|
4333
|
+
"get-page-by-type",
|
|
4334
|
+
"--page-type",
|
|
4335
|
+
page_type,
|
|
4336
|
+
...(port ? ["--port", String(port)] : []),
|
|
4337
|
+
];
|
|
4338
|
+
return callEditorAction(project_root, args);
|
|
4339
|
+
});
|
|
4340
|
+
// Tool: create_page
|
|
4341
|
+
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 theme dev` running with the editor connected.', {
|
|
4342
|
+
project_root: z
|
|
4343
|
+
.string()
|
|
4344
|
+
.describe("Absolute path to the code-component project."),
|
|
4345
|
+
page_type: z
|
|
4346
|
+
.string()
|
|
4347
|
+
.describe("Page type to create, e.g. CATEGORY, PRODUCT, BRAND, BLOG, BLOG_CATEGORY, INDEX, CUSTOM."),
|
|
4348
|
+
name: z
|
|
4349
|
+
.string()
|
|
4350
|
+
.optional()
|
|
4351
|
+
.describe("Page name. Required for CUSTOM pages; ignored otherwise."),
|
|
4352
|
+
slug: z
|
|
4353
|
+
.string()
|
|
4354
|
+
.optional()
|
|
4355
|
+
.describe("Page slug. Required (and unique) for CUSTOM pages; ignored otherwise."),
|
|
4356
|
+
port: z
|
|
4357
|
+
.number()
|
|
4358
|
+
.optional()
|
|
4359
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4360
|
+
}, async ({ project_root, page_type, name, slug, port }) => {
|
|
4361
|
+
const args = [
|
|
4362
|
+
"create-page",
|
|
4363
|
+
"--page-type",
|
|
4364
|
+
page_type,
|
|
4365
|
+
...(name ? ["--name", name] : []),
|
|
4366
|
+
...(slug ? ["--slug", slug] : []),
|
|
4367
|
+
...(port ? ["--port", String(port)] : []),
|
|
4368
|
+
];
|
|
4369
|
+
return callEditorAction(project_root, args);
|
|
4370
|
+
});
|
|
4371
|
+
// Tool: delete_page
|
|
4372
|
+
server.tool("delete_page", "Delete a theme page and every section placed on it. DESTRUCTIVE and effectively one-way from here (only the editor's own undo stack can bring it back) — ALWAYS get the user's explicit OK before calling it, and never call it to \"clean up\" a page you merely think is unused. Takes the page id from `list_editor_pages` / `get_page_by_type`; there is no name resolution. Refused for the pages a theme cannot run without — INDEX (home), CHECKOUT and NOT_FOUND (404) — and for an ikas-rendered ready-made account page, which must be turned off by its group with `enable_ready_made_pages` so the cleanup that flow does actually runs. Deleting the page currently open in the editor moves it to the home page first. Returns the deleted page's `{ pageId, pageType, name, slug }` plus `remainingPages`. Requires `ikas theme dev` running with the editor connected.", {
|
|
4373
|
+
project_root: z
|
|
4374
|
+
.string()
|
|
4375
|
+
.describe("Absolute path to the code-component project."),
|
|
4376
|
+
page_id: z
|
|
4377
|
+
.string()
|
|
4378
|
+
.describe("Id of the page to delete (from `list_editor_pages` or `get_page_by_type`). Strict — no name resolution."),
|
|
4379
|
+
confirm: z.boolean().describe(CONFIRM_PARAM_DESCRIPTION),
|
|
4380
|
+
port: z
|
|
4381
|
+
.number()
|
|
4382
|
+
.optional()
|
|
4383
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4384
|
+
}, async ({ project_root, page_id, confirm, port }) => {
|
|
4385
|
+
if (confirm !== true)
|
|
4386
|
+
return destructiveConfirmRefusal();
|
|
4387
|
+
const args = [
|
|
4388
|
+
"delete-page",
|
|
4389
|
+
"--id",
|
|
4390
|
+
page_id,
|
|
4391
|
+
"--yes",
|
|
4392
|
+
...(port ? ["--port", String(port)] : []),
|
|
4393
|
+
];
|
|
4394
|
+
return callEditorAction(project_root, args);
|
|
4395
|
+
});
|
|
4396
|
+
// Tool: remove_page_section
|
|
4397
|
+
server.tool("remove_page_section", "Remove ONE section placement from a page — the delete counterpart of `add_section_to_page` / `move_page_section`. DESTRUCTIVE: it takes the merchant's content in that section with it, so get the user's explicit OK first. The checkout section on the CHECKOUT page is refused — the checkout page requires it, so never offer to remove it. Target the PLACEMENT by `element_id` from `list_page_sections`, never the `sectionId` or `componentId` — the same section placed twice has two independent placements and each is removed on its own (passing the wrong id is rejected with the elementId you meant). A common (header/footer) section on the home page is HIDDEN rather than deleted, because the INDEX placement is where a common section's prop values live for EVERY page; the reply says so with `hidden: true`, and `add_section_to_page` brings it back with its values intact. A page customized per routing/specification holds its own placements per state: pass `sf_routing_id`/`specification_id` to remove from that state, otherwise only the BASE layer is touched (the reply lists the states it left alone). Returns the removed placement plus the page's remaining `sections`. Requires `ikas theme dev` running with the editor connected.", {
|
|
4398
|
+
project_root: z
|
|
4399
|
+
.string()
|
|
4400
|
+
.describe("Absolute path to the code-component project."),
|
|
4401
|
+
page_id: z
|
|
4402
|
+
.string()
|
|
4403
|
+
.describe("Id of the page holding the placement (from `list_editor_pages`)."),
|
|
4404
|
+
element_id: z
|
|
4405
|
+
.string()
|
|
4406
|
+
.describe("The PLACEMENT's elementId from `list_page_sections` — not the sectionId and not the componentId."),
|
|
4407
|
+
sf_routing_id: z
|
|
4408
|
+
.string()
|
|
4409
|
+
.optional()
|
|
4410
|
+
.describe("Remove from this customized routing state instead of the page's base layer. The state must already exist."),
|
|
4411
|
+
specification_id: z
|
|
4412
|
+
.string()
|
|
4413
|
+
.optional()
|
|
4414
|
+
.describe("Remove from this customized specification state instead of the page's base layer. The state must already exist."),
|
|
4415
|
+
confirm: z.boolean().describe(CONFIRM_PARAM_DESCRIPTION),
|
|
4416
|
+
port: z
|
|
4417
|
+
.number()
|
|
4418
|
+
.optional()
|
|
4419
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4420
|
+
}, async ({ project_root, page_id, element_id, sf_routing_id, specification_id, confirm, port, }) => {
|
|
4421
|
+
if (confirm !== true)
|
|
4422
|
+
return destructiveConfirmRefusal();
|
|
4423
|
+
const args = [
|
|
4424
|
+
"remove-page-section",
|
|
4425
|
+
"--page-id",
|
|
4426
|
+
page_id,
|
|
4427
|
+
"--element-id",
|
|
4428
|
+
element_id,
|
|
4429
|
+
"--yes",
|
|
4430
|
+
...(sf_routing_id ? ["--sf-routing-id", sf_routing_id] : []),
|
|
4431
|
+
...(specification_id ? ["--specification-id", specification_id] : []),
|
|
4432
|
+
...(port ? ["--port", String(port)] : []),
|
|
4433
|
+
];
|
|
4434
|
+
return callEditorAction(project_root, args);
|
|
4435
|
+
});
|
|
4436
|
+
// Tool: publish_theme
|
|
4437
|
+
// Tool: list_ready_made_pages
|
|
4438
|
+
server.tool("list_ready_made_pages", "List the ikas-rendered ready-made (\"built-in\") account/membership pages (login, register, forgot/recover password, my account, orders, addresses, favorites, email verification) and how to customize them. These pages are rendered by ikas's own components, NOT by sections: a ready-made page carries no sections, so `list_page_sections` shows nothing and `update_section_prop` cannot touch it — `update_ready_made_page_prop` is the write tool. Without arguments returns the roster: per-group (`membership` = login/register/password flows, `account` = my-account pages) enabled state, every ready-made page type with its theme pageId and which props the merchant already edited, plus the SHARED SCOPES (logo, palette, account-menu labels — values edited once that reach every page in the scope's group). Pass `page_type` (e.g. LOGIN) or `scope_id` for that page's/scope's FULL prop schema (types, defaults, ENUM options, translations) and current values (`values` = what the page renders with; `ownValues` = what this page's own layer stores). Pass `sf_routing_id` to read what one storefront routing/locale sees — the no-argument call lists the store's routings (id, path, locale; `isDefault: true` = the base, needing no sf_routing_id). Typical branding flow: read the theme's palette via `list_theme_globals`, then set the shared scope's colors as `{ \"patternValueId\": \"<global color id>\" }` bindings so the pages follow the theme's colors live.", {
|
|
4439
|
+
project_root: z
|
|
4440
|
+
.string()
|
|
4441
|
+
.describe("Absolute path to the code-component project."),
|
|
4442
|
+
page_type: z
|
|
4443
|
+
.string()
|
|
4444
|
+
.optional()
|
|
4445
|
+
.describe("One ready-made page's full prop schema and values (e.g. LOGIN, ACCOUNT, ORDERS)."),
|
|
4446
|
+
scope_id: z
|
|
4447
|
+
.string()
|
|
4448
|
+
.optional()
|
|
4449
|
+
.describe("One shared scope's full prop schema and values (scope ids come from the no-argument call)."),
|
|
4450
|
+
sf_routing_id: z
|
|
4451
|
+
.string()
|
|
4452
|
+
.optional()
|
|
4453
|
+
.describe("Read the values one storefront routing/locale sees (default: the base routing)."),
|
|
4454
|
+
port: z
|
|
4455
|
+
.number()
|
|
4456
|
+
.optional()
|
|
4457
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4458
|
+
}, async ({ project_root, page_type, scope_id, sf_routing_id, port }) => {
|
|
4459
|
+
const args = [
|
|
4460
|
+
"list-builtin-pages",
|
|
4461
|
+
...(page_type ? ["--page-type", page_type] : []),
|
|
4462
|
+
...(scope_id ? ["--scope-id", scope_id] : []),
|
|
4463
|
+
...(sf_routing_id ? ["--sf-routing-id", sf_routing_id] : []),
|
|
4464
|
+
...(port ? ["--port", String(port)] : []),
|
|
4465
|
+
];
|
|
4466
|
+
return callEditorAction(project_root, args);
|
|
4467
|
+
});
|
|
4468
|
+
// Tool: update_ready_made_page_prop
|
|
4469
|
+
server.tool("update_ready_made_page_prop", 'Set, bind, or clear ONE prop of an ikas-rendered ready-made account/membership page — the write half of `list_ready_made_pages`, and the ONLY way to customize those pages (they have no sections, so `update_section_prop` cannot reach them). Target a page\'s own prop with `page_type` (e.g. LOGIN) or a SHARED prop — logo, palette, menu labels, edited once for every page in the scope\'s group — with `scope_id`. Identify the prop by `prop_id` or `prop_name` from `list_ready_made_pages`. VALUES ARE RAW (no `{ "value": ... }` envelope — an envelope is tolerated and unwrapped): TEXT = the plain string; BOOLEAN = true/false; NUMBER = a number; ENUM = one of the prop\'s `enumOptions` VALUES; COLOR = "#rrggbb" OR `{ "patternValueId": "<global color id>" }` to follow a theme color live (ids from `list_theme_globals` → colors[].id — prefer the binding when branding, so a palette change updates the pages); IMAGE = `{ "id": "<image id>" }` (id from `upload_image`); LINK/LIST_OF_LINK = copy the shape from `list_ready_made_pages` values. `bind_global` (a global\'s `name` from `list_theme_globals`) makes the prop follow a Theme Setting instead of holding a value; `clear: true` deletes the stored value so the prop falls back to the shared value or the manifest default. `sf_routing_id` writes one routing\'s override — use it to localize texts per language; without it you write the base value every routing inherits. Writes are undoable in the editor.', {
|
|
4470
|
+
project_root: z
|
|
4471
|
+
.string()
|
|
4472
|
+
.describe("Absolute path to the code-component project."),
|
|
4473
|
+
page_type: z
|
|
4474
|
+
.string()
|
|
4475
|
+
.optional()
|
|
4476
|
+
.describe("Page type whose OWN prop to write (e.g. LOGIN). Pass this or scope_id."),
|
|
4477
|
+
scope_id: z
|
|
4478
|
+
.string()
|
|
4479
|
+
.optional()
|
|
4480
|
+
.describe("Shared scope whose prop to write — reaches every ready-made page in its group."),
|
|
4481
|
+
prop_id: z
|
|
4482
|
+
.string()
|
|
4483
|
+
.optional()
|
|
4484
|
+
.describe("Prop id from list_ready_made_pages. Provide this or prop_name."),
|
|
4485
|
+
prop_name: z
|
|
4486
|
+
.string()
|
|
4487
|
+
.optional()
|
|
4488
|
+
.describe("Prop name (alternative to prop_id)."),
|
|
4489
|
+
value: z
|
|
4490
|
+
.any()
|
|
4491
|
+
.optional()
|
|
4492
|
+
.describe("The RAW value (see the tool description for per-type shapes). Omit when using bind_global or clear."),
|
|
4493
|
+
bind_global: z
|
|
4494
|
+
.string()
|
|
4495
|
+
.optional()
|
|
4496
|
+
.describe("Bind the prop to a global variable (Theme Setting) — pass the global's `name` from list_theme_globals."),
|
|
4497
|
+
clear: z
|
|
4498
|
+
.boolean()
|
|
4499
|
+
.optional()
|
|
4500
|
+
.describe("true deletes the stored value; the prop falls back to the shared value or the manifest default."),
|
|
4501
|
+
sf_routing_id: z
|
|
4502
|
+
.string()
|
|
4503
|
+
.optional()
|
|
4504
|
+
.describe("Write one storefront routing's override (localization). Omit to write the base value."),
|
|
4505
|
+
port: z
|
|
4506
|
+
.number()
|
|
4507
|
+
.optional()
|
|
4508
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4509
|
+
}, async ({ project_root, page_type, scope_id, prop_id, prop_name, value, bind_global, clear, sf_routing_id, port, }) => {
|
|
4510
|
+
const args = [
|
|
4511
|
+
"update-builtin-page-prop",
|
|
4512
|
+
...(page_type ? ["--page-type", page_type] : []),
|
|
4513
|
+
...(scope_id ? ["--scope-id", scope_id] : []),
|
|
4514
|
+
...(prop_id ? ["--prop-id", prop_id] : []),
|
|
4515
|
+
...(prop_name ? ["--prop-name", prop_name] : []),
|
|
4516
|
+
...(bind_global
|
|
4517
|
+
? ["--bind-global", bind_global]
|
|
4518
|
+
: clear
|
|
4519
|
+
? ["--clear"]
|
|
4520
|
+
: value !== undefined
|
|
4521
|
+
? [
|
|
4522
|
+
"--value",
|
|
4523
|
+
// ALWAYS JSON-encode: the CLI JSON.parses --value (raw-string fallback), so a
|
|
4524
|
+
// verbatim TEXT string that happens to parse — "2024", "true", "null" — would
|
|
4525
|
+
// arrive as a number/boolean/clear. Encoded, it round-trips as the string it is.
|
|
4526
|
+
JSON.stringify(value),
|
|
4527
|
+
]
|
|
4528
|
+
: []),
|
|
4529
|
+
...(sf_routing_id ? ["--sf-routing-id", sf_routing_id] : []),
|
|
4530
|
+
...(port ? ["--port", String(port)] : []),
|
|
4531
|
+
];
|
|
4532
|
+
return callEditorAction(project_root, args);
|
|
4533
|
+
});
|
|
4534
|
+
// Tool: enable_ready_made_pages
|
|
4535
|
+
server.tool("enable_ready_made_pages", 'Turn one GROUP of ikas-rendered ready-made pages on or off: "membership" (login, register, forgot/recover password, email verification) or "account" (my account, orders, order detail, addresses, favorites). Enabling records the choice and creates any page of the group the theme does not have yet — each born rendered by ikas\'s own component; customize them right after with `update_ready_made_page_prop`. Disabling removes ONLY the untouched pages the toggle itself created — a page the merchant built on is never deleted. The flip is one undo step in the editor. Check the current state first with `list_ready_made_pages`.', {
|
|
4536
|
+
project_root: z
|
|
4537
|
+
.string()
|
|
4538
|
+
.describe("Absolute path to the code-component project."),
|
|
4539
|
+
group: z
|
|
4540
|
+
.enum(["membership", "account"])
|
|
4541
|
+
.describe('"membership" = login/register/password flows; "account" = my-account pages.'),
|
|
4542
|
+
enabled: z
|
|
4543
|
+
.boolean()
|
|
4544
|
+
.describe("true to enable the group, false to disable it."),
|
|
4545
|
+
port: z
|
|
4546
|
+
.number()
|
|
4547
|
+
.optional()
|
|
4548
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4549
|
+
}, async ({ project_root, group, enabled, port }) => {
|
|
4550
|
+
const args = [
|
|
4551
|
+
"enable-builtin-pages",
|
|
4552
|
+
"--group",
|
|
4553
|
+
group,
|
|
4554
|
+
"--enabled",
|
|
4555
|
+
String(enabled),
|
|
4556
|
+
...(port ? ["--port", String(port)] : []),
|
|
4557
|
+
];
|
|
4558
|
+
return callEditorAction(project_root, args);
|
|
4559
|
+
});
|
|
4560
|
+
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 theme dev` running with the editor connected.", {
|
|
4561
|
+
project_root: z
|
|
4562
|
+
.string()
|
|
4563
|
+
.describe("Absolute path to the code-component project."),
|
|
4564
|
+
confirm: z
|
|
4565
|
+
.boolean()
|
|
4566
|
+
.optional()
|
|
4567
|
+
.describe("Set true to actually publish. Omitted/false = dry-run (returns previewUrl + warning, publishes nothing)."),
|
|
4568
|
+
confirm_production: z
|
|
4569
|
+
.boolean()
|
|
4570
|
+
.optional()
|
|
4571
|
+
.describe("Required IN ADDITION to confirm when the target is the MAIN/production theme (live customer-facing site)."),
|
|
4572
|
+
port: z
|
|
4573
|
+
.number()
|
|
4574
|
+
.optional()
|
|
4575
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4576
|
+
}, async ({ project_root, confirm, confirm_production, port }) => {
|
|
4577
|
+
const args = [
|
|
4578
|
+
"publish-theme",
|
|
4579
|
+
...(confirm ? ["--confirm"] : []),
|
|
4580
|
+
...(confirm_production ? ["--confirm-production"] : []),
|
|
4581
|
+
...(port ? ["--port", String(port)] : []),
|
|
4582
|
+
];
|
|
4583
|
+
return callEditorAction(project_root, args);
|
|
4584
|
+
});
|
|
4585
|
+
// Tool: get_editor_workflow
|
|
4586
|
+
const EDITOR_WORKFLOW_GUIDE = [
|
|
4587
|
+
"# Editor workflow: placing sections and filling their content on a page",
|
|
4588
|
+
"",
|
|
4589
|
+
"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 theme 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).",
|
|
4590
|
+
"",
|
|
4591
|
+
"## Account & membership pages: ASK before you build",
|
|
4592
|
+
"If the page the user wants is a login, register, forgot/recover-password, my-account, orders, order-detail, addresses, favorites or email-verification page: do NOT code a custom section page yet. ikas ships ready-made versions of exactly these pages — fully functional (real auth/order/address flows), customizable (logo, colors, texts) and localizable via update_ready_made_page_prop, the whole group enabled at once with enable_ready_made_pages. FIRST ask the user which they want: the ready-made page (fast, maintained by ikas) or a custom section page (full design freedom) — list the ready-made option FIRST as the recommended default, custom second. Only start coding after the user explicitly chooses custom — and never call enable_ready_made_pages without the user's OK, it creates/removes pages. Start discovery with list_ready_made_pages. USER-FACING NAME: name these in the USER'S OWN LANGUAGE — English: \"ikas's ready-made pages\", Turkish: \"ikas'ın hazır sayfaları\"; never mix the English term into a sentence in another language — the editor UI lists them as Membership Pages / My Account Pages; \"built-in\" is an internal term — keep it out of everything the user sees, including question/option labels. Note: a ready-made page is born with ONLY its own ikas-rendered element — the theme's header/footer are NOT on it (the page draws its own top bar — logo, back-to-store link — and account nav). If the user wants the theme header/footer there, place it with add_section_to_page (sections can sit above/below that element) and suggest turning the page's `showBrandBar` prop off to avoid double chrome.",
|
|
4593
|
+
"",
|
|
4594
|
+
"## Two distinct jobs — do not confuse them",
|
|
4595
|
+
"- (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.",
|
|
4596
|
+
"- (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.",
|
|
4597
|
+
"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.",
|
|
4598
|
+
"",
|
|
4599
|
+
"## Step-by-step (JOB B — fill content)",
|
|
4600
|
+
"1. list_editor_pages → choose the target page_id.",
|
|
4601
|
+
"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). A section returned with a `common` field is the theme's header/footer — its values are theme-wide, not this page's; read the 'COMMON (global) sections' section below BEFORE touching it. (If the section is not on the page yet, see 'Placing a section' below.)",
|
|
4602
|
+
"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.",
|
|
4603
|
+
"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.",
|
|
4604
|
+
"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.",
|
|
4605
|
+
"",
|
|
4606
|
+
"## COMMON (global) sections: the header and the footer",
|
|
4607
|
+
"The theme's header and footer are COMMON sections: they render on every page that carries a placement of them (pages created in the editor are seeded with one — EXCEPT ikas ready-made (built-in) account/membership pages, which are born without the theme header/footer) and their prop values are SHARED by the whole theme — there is exactly ONE record, kept on the INDEX (Home) page's placement. A placement of the same section on any other page stores NOTHING of its own.",
|
|
4608
|
+
'- How to recognize one: list_page_sections / get_section_values / the write results return a `common` block for it — `isCommon: true`, `commonSectionType: "header"|"footer"`, `valuesArePageSpecific: false`, plus `canonicalPageId`/`canonicalElementId` (the INDEX placement that holds the values). A page containing one also gets a top-level `commonSectionsNote`.',
|
|
4609
|
+
"- READS are redirected for you: get_section_values / list_page_sections report a common section's values FROM the INDEX placement, whatever page_id you pass. So what you see is what the page actually renders — no need to re-read from the INDEX page.",
|
|
4610
|
+
"- WRITES are redirected too: update_section_prop / update_page_sections / add_sections_to_page store the value on that INDEX record, so the change applies to the header/footer on ALL pages. The echoed value is read back from the canonical placement, so it reflects what was really stored.",
|
|
4611
|
+
'- THE TRAP to avoid: a header/footer is normally already filled with the merchant\'s content. Never conclude a prop is empty and never blind-fill "missing" props — you would overwrite live content on every page, including pages you never opened. Always get_section_values first and change ONLY the prop the user asked about.',
|
|
4612
|
+
"- If the INDEX page has no placement of the section, there is no record: reads return nothing and writes are REJECTED (\"Index page does not have the common section!\"). Fix it by placing the section on the INDEX page (add_section_to_page with the INDEX page id — get it from list_editor_pages / get_page_by_type('INDEX')) and setting its props there; it then renders on the other pages too.",
|
|
4613
|
+
"",
|
|
4614
|
+
"## Building a whole page fast (placing + filling)",
|
|
4615
|
+
"- 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.",
|
|
4616
|
+
"- 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.",
|
|
4617
|
+
"- REORDERING an already-placed section is a different tool: move_page_section(page_id, element_id, direction:'up'|'down' | to_index:<n>). The `index` on add_* only positions a NEW placement; it cannot move an existing one, and the editor UI exposes reordering solely as a drag, so this tool is the only way. Indices are 0-based over the placed sections in list_page_sections order; the result returns `resultIndex` + the full resulting `sections` order — assert on those. Header/footer (COMMON) sections cannot be reordered and are rejected.",
|
|
4618
|
+
"",
|
|
4619
|
+
"## Value shapes by prop type (what to pass as `value`)",
|
|
4620
|
+
'- Scalars are WRAPPED: TEXT/RICH_TEXT { "value": "..." }, BOOLEAN { "value": true }, NUMBER { "value": 12 }, COLOR { "value": "#FF0000" }, ENUM { "value": "<key>" }, DATE { "value": ... }.',
|
|
4621
|
+
'- SVG is value-wrapped: { "value": "<svg markup>" }. NUMBER_RANGE: { "value": <number>, "unit": "px"|null }.',
|
|
4622
|
+
'- 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, ….',
|
|
4623
|
+
'- Object props are NOT wrapped (no { "value": ... }):',
|
|
4624
|
+
' - 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>, ... ] }.',
|
|
4625
|
+
' - VIDEO: { "video": { "id": "<asset-id>" }, "thumbnailImage"?: { "id": "..." }, "autoplay"?: false, "controls"?: true, "loop"?: false, "muted"?: false } — NOT a bare { "id": ... }. SVG_LIST: { "svgs": [ "<svg>", ... ] }.',
|
|
4626
|
+
' - 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>, ... ] }.',
|
|
4627
|
+
' - 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.)',
|
|
4628
|
+
" - 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.",
|
|
4629
|
+
' - 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.',
|
|
4630
|
+
"",
|
|
4631
|
+
"## COMPONENT_LIST rules (the part most often gotten wrong)",
|
|
4632
|
+
"- 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.",
|
|
4633
|
+
"- 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.",
|
|
4634
|
+
'- Reference a code component with "codeComponentId" and a built-in theme component with "componentId" — not the other way around. Use "propValues" (not "props").',
|
|
4635
|
+
"- Child ids are opaque — take them from get_section_template / list_imported_sections; never invent them.",
|
|
4636
|
+
"- 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.",
|
|
4637
|
+
"",
|
|
4638
|
+
"## Reading prop schemas (avoid guessing)",
|
|
4639
|
+
"- 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.",
|
|
4640
|
+
"- For a COMPONENT_LIST: get_component_props(parent) → its allowedComponentIds → get_component_props(child) → the child's props/enums. Then build the components array.",
|
|
4641
|
+
"- 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).",
|
|
4642
|
+
"- 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.",
|
|
4643
|
+
"",
|
|
4644
|
+
"## Validation",
|
|
4645
|
+
"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.",
|
|
4646
|
+
].join("\n");
|
|
4647
|
+
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, how COMMON (global) header/footer sections keep one theme-wide set of values on the INDEX page (and how not to overwrite them), 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 () => {
|
|
4648
|
+
return {
|
|
4649
|
+
content: [{ type: "text", text: EDITOR_WORKFLOW_GUIDE }],
|
|
4650
|
+
};
|
|
4651
|
+
});
|
|
4652
|
+
// Tool: list_theme_globals
|
|
4653
|
+
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 theme 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. (7) ENUM settings: a setting with `type: \"ENUM\"` selects one option of a PROJECT enum and carries `enumTypeId` (the enum's id — the same one an ENUM prop declares as `enumTypeId`) and `options` (`{ label: value }`); its `value` is always one of those option VALUES (never a label), or null when unset. The response's top-level `enumTypes` array lists the project enums a new ENUM setting may select from (`{ id, name, options }`) — use an `id` from it as `enum_type_id` in `create_theme_global`. An ENUM prop can be bound (`bind_global`) only to an ENUM setting of the SAME enum.", {
|
|
4654
|
+
project_root: z
|
|
4655
|
+
.string()
|
|
4656
|
+
.describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
|
|
4657
|
+
port: z
|
|
4658
|
+
.number()
|
|
4659
|
+
.optional()
|
|
4660
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4661
|
+
}, async ({ project_root, port }) => {
|
|
4662
|
+
const args = [
|
|
4663
|
+
"list-theme-globals",
|
|
4664
|
+
...(port ? ["--port", String(port)] : []),
|
|
4665
|
+
];
|
|
4666
|
+
return callEditorAction(project_root, args);
|
|
4667
|
+
});
|
|
4668
|
+
// Shared text-style (typography) font-property schema — kept in lock-step with the editor's
|
|
4669
|
+
// "Update Text Style" modal fields (no color/text-align, since that modal doesn't expose them).
|
|
4670
|
+
// Each enum prop documents its accepted values so a bad value is caught with a clear error. Spread
|
|
4671
|
+
// into both create_theme_global (typography) and update_text_style so the two never drift apart.
|
|
4672
|
+
const textStyleFontParams = {
|
|
4673
|
+
font_family: z
|
|
4674
|
+
.string()
|
|
4675
|
+
.optional()
|
|
4676
|
+
.describe('A real font family the editor offers — a Google Fonts name with EXACT casing (e.g. "Inter", "Roboto", ' +
|
|
4677
|
+
'"Poppins"), or a font already used by an existing text style in list_theme_globals. An unknown font ' +
|
|
4678
|
+
"(or wrong casing) is rejected; do NOT invent a family name."),
|
|
4679
|
+
font_size: z.string().optional(),
|
|
4680
|
+
font_weight: z
|
|
4681
|
+
.string()
|
|
4682
|
+
.optional()
|
|
4683
|
+
.describe("font-weight as a number (e.g. \"600\"); append 'i' for italic (e.g. \"600i\"). A PLAIN number with no 'i' " +
|
|
4684
|
+
"is upright and REMOVES italic if the text style was italic (italic is carried by the weight — re-send the " +
|
|
4685
|
+
"weight without 'i' to turn it off). Must be a weight the chosen " +
|
|
4686
|
+
'font ships — if it isn\'t, the error lists the supported weights; when unsure use "400".'),
|
|
4687
|
+
line_height: z.string().optional(),
|
|
4688
|
+
letter_spacing: z.string().optional(),
|
|
4689
|
+
text_decoration: z
|
|
4690
|
+
.string()
|
|
4691
|
+
.optional()
|
|
4692
|
+
.describe("text-decoration: none | underline | line-through"),
|
|
4693
|
+
text_transform: z
|
|
4694
|
+
.string()
|
|
4695
|
+
.optional()
|
|
4696
|
+
.describe("text-transform: none | uppercase | lowercase | capitalize"),
|
|
4697
|
+
text_overflow: z
|
|
4698
|
+
.string()
|
|
4699
|
+
.optional()
|
|
4700
|
+
.describe("text-overflow: clip | ellipsis"),
|
|
4701
|
+
white_space: z
|
|
4702
|
+
.string()
|
|
4703
|
+
.optional()
|
|
4704
|
+
.describe("white-space: normal | pre | pre-wrap | pre-line | wrap | nowrap | collapse"),
|
|
4705
|
+
line_break: z
|
|
4706
|
+
.string()
|
|
4707
|
+
.optional()
|
|
4708
|
+
.describe("line-break: auto | loose | normal | strict | anywhere"),
|
|
4709
|
+
word_break: z
|
|
4710
|
+
.string()
|
|
4711
|
+
.optional()
|
|
4712
|
+
.describe("word-break: normal | break-all | keep-all"),
|
|
4713
|
+
text_wrap_style: z
|
|
4714
|
+
.string()
|
|
4715
|
+
.optional()
|
|
4716
|
+
.describe("text-wrap-style: auto | balance | pretty | stable"),
|
|
4717
|
+
};
|
|
4718
|
+
// Turn the supplied text-style font params into `ikas-component` CLI flags (omitted ones dropped).
|
|
4719
|
+
// The editor validates each value and rejects an unsupported one with the offending value + allowed set.
|
|
4720
|
+
function textStyleFontArgs(input) {
|
|
4721
|
+
const map = {
|
|
4722
|
+
"--font-family": input.font_family,
|
|
4723
|
+
"--font-size": input.font_size,
|
|
4724
|
+
"--font-weight": input.font_weight,
|
|
4725
|
+
"--line-height": input.line_height,
|
|
4726
|
+
"--letter-spacing": input.letter_spacing,
|
|
4727
|
+
"--text-decoration": input.text_decoration,
|
|
4728
|
+
"--text-transform": input.text_transform,
|
|
4729
|
+
"--text-overflow": input.text_overflow,
|
|
4730
|
+
"--white-space": input.white_space,
|
|
4731
|
+
"--line-break": input.line_break,
|
|
4732
|
+
"--word-break": input.word_break,
|
|
4733
|
+
"--text-wrap-style": input.text_wrap_style,
|
|
4734
|
+
};
|
|
4735
|
+
return Object.entries(map).flatMap(([flag, value]) => value ? [flag, value] : []);
|
|
4736
|
+
}
|
|
4737
|
+
// Tool: create_theme_global
|
|
4738
|
+
server.tool("create_theme_global", "Create a theme global setting in the connected editor. `kind` selects what to create:\n" +
|
|
4739
|
+
'- globalVariable: requires display_name + type (TEXT|RICH_TEXT|IMAGE|COLOR|NUMBER|BOOLEAN|BORDER|SHADOW|ENUM); 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" }; ENUM: a setting that selects ONE option of a PROJECT enum — pass `enum_type_id` (an enum id from `list_theme_globals` → `enumTypes[]`; the same enum an ENUM prop declares as `enumTypeId`; built-in @ikas/* enums are REJECTED) and `value` = one of that enum\'s option VALUES (the strings in its `options`, e.g. "bottom"), NOT a label; omit `value` to leave it unset. Once created, an ENUM prop of the SAME enum can be bound to it with `update_section_prop` `bind_global`, and in component code `themeValue("_<id>")` from `./global-types` narrows to the enum\'s union type. 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' +
|
|
4740
|
+
'- 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' +
|
|
4741
|
+
'- 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' +
|
|
4742
|
+
"- 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" +
|
|
4743
|
+
"- 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" +
|
|
4744
|
+
'- 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' +
|
|
4745
|
+
"Read created items back with `list_theme_globals`. Requires `ikas theme dev` running with the editor connected.", {
|
|
4746
|
+
project_root: z
|
|
4747
|
+
.string()
|
|
4748
|
+
.describe("Absolute path to the code-component project."),
|
|
4749
|
+
kind: z
|
|
4750
|
+
.enum([
|
|
4751
|
+
"globalVariable",
|
|
4752
|
+
"color",
|
|
4753
|
+
"typography",
|
|
4754
|
+
"breakpoint",
|
|
4755
|
+
"keyframe",
|
|
4756
|
+
"colorScheme",
|
|
4757
|
+
])
|
|
4758
|
+
.describe("What to create."),
|
|
4759
|
+
name: z.string().optional().describe("Name (design tokens)."),
|
|
4760
|
+
display_name: z
|
|
4761
|
+
.string()
|
|
4762
|
+
.optional()
|
|
4763
|
+
.describe("Human label (globalVariable)."),
|
|
4764
|
+
type: z.string().optional().describe("globalVariable value type."),
|
|
4765
|
+
value: z
|
|
4766
|
+
.any()
|
|
4767
|
+
.optional()
|
|
4768
|
+
.describe("globalVariable default value (shape depends on type — see tool description), or color hex string."),
|
|
4769
|
+
enum_type_id: z
|
|
4770
|
+
.string()
|
|
4771
|
+
.optional()
|
|
4772
|
+
.describe("globalVariable of type ENUM only: id of the project enum the setting selects from (from `list_theme_globals` → `enumTypes[]`). Required for ENUM, invalid for other types."),
|
|
4773
|
+
...textStyleFontParams,
|
|
4774
|
+
width: z
|
|
4775
|
+
.number()
|
|
4776
|
+
.int()
|
|
4777
|
+
.optional()
|
|
4778
|
+
.describe("breakpoint width in px (positive integer, no decimals)."),
|
|
4779
|
+
keyframe_type: z.enum(["keyframe", "transition"]).optional(),
|
|
4780
|
+
points: z.any().optional().describe("keyframe points array."),
|
|
4781
|
+
settings: z
|
|
4782
|
+
.any()
|
|
4783
|
+
.optional()
|
|
4784
|
+
.describe("keyframe animation settings: array of { property, value } (type-dependent — see tool description)."),
|
|
4785
|
+
colors: z
|
|
4786
|
+
.any()
|
|
4787
|
+
.optional()
|
|
4788
|
+
.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.'),
|
|
4789
|
+
port: z
|
|
4790
|
+
.number()
|
|
4791
|
+
.optional()
|
|
4792
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4793
|
+
}, async (input) => {
|
|
4794
|
+
const p = input.port ? ["--port", String(input.port)] : [];
|
|
4795
|
+
let args;
|
|
4796
|
+
switch (input.kind) {
|
|
4797
|
+
case "globalVariable":
|
|
4798
|
+
args = [
|
|
4799
|
+
"create-global-variable",
|
|
4800
|
+
"--display-name",
|
|
4801
|
+
String(input.display_name ?? ""),
|
|
4802
|
+
"--type",
|
|
4803
|
+
String(input.type ?? ""),
|
|
4804
|
+
...(input.value !== undefined
|
|
4805
|
+
? ["--value", JSON.stringify(input.value)]
|
|
4806
|
+
: []),
|
|
4807
|
+
...(input.enum_type_id
|
|
4808
|
+
? ["--enum-type-id", String(input.enum_type_id)]
|
|
4809
|
+
: []),
|
|
4810
|
+
...p,
|
|
4811
|
+
];
|
|
4812
|
+
break;
|
|
4813
|
+
case "color":
|
|
4814
|
+
args = [
|
|
4815
|
+
"create-color",
|
|
4816
|
+
"--name",
|
|
4817
|
+
String(input.name ?? ""),
|
|
4818
|
+
"--value",
|
|
4819
|
+
String(input.value ?? ""),
|
|
4820
|
+
...p,
|
|
4821
|
+
];
|
|
4822
|
+
break;
|
|
4823
|
+
case "typography":
|
|
4824
|
+
args = [
|
|
4825
|
+
"create-text-style",
|
|
4826
|
+
"--name",
|
|
4827
|
+
String(input.name ?? ""),
|
|
4828
|
+
...textStyleFontArgs(input),
|
|
4829
|
+
...p,
|
|
4830
|
+
];
|
|
4831
|
+
break;
|
|
4832
|
+
case "breakpoint":
|
|
4833
|
+
args = [
|
|
4834
|
+
"create-breakpoint",
|
|
4835
|
+
"--name",
|
|
4836
|
+
String(input.name ?? ""),
|
|
4837
|
+
"--width",
|
|
4838
|
+
String(input.width ?? ""),
|
|
4839
|
+
...p,
|
|
4840
|
+
];
|
|
4841
|
+
break;
|
|
4842
|
+
case "keyframe":
|
|
4843
|
+
args = [
|
|
4844
|
+
"create-keyframe",
|
|
4845
|
+
"--name",
|
|
4846
|
+
String(input.name ?? ""),
|
|
4847
|
+
...(input.keyframe_type ? ["--type", input.keyframe_type] : []),
|
|
4848
|
+
"--points",
|
|
4849
|
+
toJsonArrayArg(input.points),
|
|
4850
|
+
...(input.settings !== undefined
|
|
4851
|
+
? ["--settings", toJsonArrayArg(input.settings)]
|
|
4852
|
+
: []),
|
|
4853
|
+
...p,
|
|
4854
|
+
];
|
|
4855
|
+
break;
|
|
4856
|
+
case "colorScheme":
|
|
4857
|
+
args = [
|
|
4858
|
+
"create-color-scheme",
|
|
4859
|
+
"--name",
|
|
4860
|
+
String(input.name ?? ""),
|
|
4861
|
+
"--colors",
|
|
4862
|
+
toJsonArrayArg(input.colors),
|
|
4863
|
+
...p,
|
|
4864
|
+
];
|
|
4865
|
+
break;
|
|
4866
|
+
default:
|
|
4867
|
+
return {
|
|
4868
|
+
content: [
|
|
4869
|
+
{
|
|
4870
|
+
type: "text",
|
|
4871
|
+
text: `Error: unknown kind "${input.kind}"`,
|
|
4872
|
+
},
|
|
4873
|
+
],
|
|
4874
|
+
};
|
|
4875
|
+
}
|
|
4876
|
+
return callEditorAction(input.project_root, args);
|
|
4877
|
+
});
|
|
4878
|
+
// Tool: update_theme_global
|
|
4879
|
+
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; ENUM `value` is one of the enum's option VALUES (see the setting's `options` in `list_theme_globals`), and `enum_type_id` re-points the setting at another project enum. Changing `type` or `enum_type_id` of a global that props are bound to is REJECTED (the error names the places) — unbind those props first. Values are validated server-side; a wrong shape is REJECTED, not silently stored.", {
|
|
4880
|
+
project_root: z
|
|
4881
|
+
.string()
|
|
4882
|
+
.describe("Absolute path to the code-component project."),
|
|
4883
|
+
name: z
|
|
4884
|
+
.string()
|
|
4885
|
+
.describe("The variable's runtime key (from `list_theme_globals`)."),
|
|
4886
|
+
display_name: z.string().optional().describe("New label."),
|
|
4887
|
+
type: z
|
|
4888
|
+
.string()
|
|
4889
|
+
.optional()
|
|
4890
|
+
.describe("New value type (TEXT|RICH_TEXT|IMAGE|COLOR|NUMBER|BOOLEAN|BORDER|SHADOW|ENUM)."),
|
|
4891
|
+
value: z
|
|
4892
|
+
.any()
|
|
4893
|
+
.optional()
|
|
4894
|
+
.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, ENUM: an option VALUE string — see create_theme_global). An invented IMAGE id renders as a broken image.'),
|
|
4895
|
+
enum_type_id: z
|
|
4896
|
+
.string()
|
|
4897
|
+
.optional()
|
|
4898
|
+
.describe('ENUM only: the project enum to select from (from `list_theme_globals` → `enumTypes[]`); may be passed alone to re-point an unbound ENUM global, or together with `type: "ENUM"`.'),
|
|
4899
|
+
port: z
|
|
4900
|
+
.number()
|
|
4901
|
+
.optional()
|
|
4902
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4903
|
+
}, async ({ project_root, name, display_name, type, value, enum_type_id, port, }) => {
|
|
4904
|
+
const args = [
|
|
4905
|
+
"update-global-variable",
|
|
4906
|
+
"--name",
|
|
4907
|
+
name,
|
|
4908
|
+
...(display_name !== undefined ? ["--display-name", display_name] : []),
|
|
4909
|
+
...(type !== undefined ? ["--type", type] : []),
|
|
4910
|
+
...(value !== undefined ? ["--value", JSON.stringify(value)] : []),
|
|
4911
|
+
...(enum_type_id !== undefined ? ["--enum-type-id", enum_type_id] : []),
|
|
4912
|
+
...(port ? ["--port", String(port)] : []),
|
|
4913
|
+
];
|
|
4914
|
+
return callEditorAction(project_root, args);
|
|
4915
|
+
});
|
|
4916
|
+
// Tool: update_theme_keyframe
|
|
4917
|
+
server.tool("update_theme_keyframe", "Update an existing theme keyframe IN PLACE in the connected editor. Prefer this over delete+recreate: " +
|
|
4918
|
+
"it keeps the keyframe's id, so its runtime `ref` (the CSS animation-name `_<id>`) stays STABLE and every " +
|
|
4919
|
+
"existing reference to it keeps working. Identify the keyframe by `id` (REQUIRED — globally unique, from " +
|
|
4920
|
+
"`list_theme_globals`); name-based lookup is intentionally NOT supported here because names can collide across " +
|
|
4921
|
+
"design assets. To rename, pass `name` (the new name) together with `id`. " +
|
|
4922
|
+
"`points` REPLACES the keyframe's points: an array of { point, styles? } where `point` is a CSS " +
|
|
4923
|
+
'keyframe offset (e.g. "0%", "50%", "100%") and each style is { property, value } using a CSS property ' +
|
|
4924
|
+
"name (opacity, transform, filter, background, color, …). Pass `points` as REAL JSON (not a stringified array). " +
|
|
4925
|
+
"For a TRANSITION keyframe, put the target/hover-state styles on the 100% point (points[1].styles) — the 0% " +
|
|
4926
|
+
"point is ignored (the start state is the element's own normal state). " +
|
|
4927
|
+
"Optional `settings` REPLACES the keyframe's animation settings (type-dependent { property, value } entries — " +
|
|
4928
|
+
"see `create_theme_global`); OMIT it to keep the existing settings, or pass [] to clear them. " +
|
|
4929
|
+
"Requires `ikas theme dev` running with the editor connected.", {
|
|
4930
|
+
project_root: z
|
|
4931
|
+
.string()
|
|
4932
|
+
.describe("Absolute path to the code-component project."),
|
|
4933
|
+
id: z
|
|
4934
|
+
.string()
|
|
4935
|
+
.describe("Keyframe id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
|
|
4936
|
+
name: z
|
|
4937
|
+
.string()
|
|
4938
|
+
.optional()
|
|
4939
|
+
.describe("Optional NEW name to rename the keyframe to. NOT used to find the token — lookup is always by id."),
|
|
4940
|
+
keyframe_type: z
|
|
4941
|
+
.enum(["keyframe", "transition"])
|
|
4942
|
+
.optional()
|
|
4943
|
+
.describe("Optional new keyframe type."),
|
|
4944
|
+
points: z
|
|
4945
|
+
.any()
|
|
4946
|
+
.describe("New keyframe points array of { point, styles? } — REPLACES the existing points."),
|
|
4947
|
+
settings: z
|
|
4948
|
+
.any()
|
|
4949
|
+
.optional()
|
|
4950
|
+
.describe("New animation settings { property, value } array (type-dependent — see create_theme_global). Omit to keep existing; [] to clear."),
|
|
4951
|
+
port: z
|
|
4952
|
+
.number()
|
|
4953
|
+
.optional()
|
|
4954
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
4955
|
+
}, async ({ project_root, id, name, keyframe_type, points, settings, port }) => {
|
|
4956
|
+
const args = [
|
|
4957
|
+
"update-keyframe",
|
|
4958
|
+
"--id",
|
|
4959
|
+
id,
|
|
4960
|
+
...(name !== undefined ? ["--name", name] : []),
|
|
4961
|
+
...(keyframe_type ? ["--type", keyframe_type] : []),
|
|
4962
|
+
"--points",
|
|
4963
|
+
toJsonArrayArg(points),
|
|
4964
|
+
...(settings !== undefined
|
|
4965
|
+
? ["--settings", toJsonArrayArg(settings)]
|
|
4966
|
+
: []),
|
|
4967
|
+
...(port ? ["--port", String(port)] : []),
|
|
4968
|
+
];
|
|
4969
|
+
return callEditorAction(project_root, args);
|
|
4970
|
+
});
|
|
4971
|
+
// Tool: update_text_style
|
|
4972
|
+
server.tool("update_text_style", "Update an existing theme typography token (text style) IN PLACE in the connected editor. Prefer this over " +
|
|
4973
|
+
"delete+recreate: it keeps the text style's id, so its `className` (`_<id>`) — and every section bound to it — " +
|
|
4974
|
+
"stays STABLE. Identify the text style by `id` (REQUIRED — globally unique, from `list_theme_globals`); " +
|
|
4975
|
+
"name-based lookup is intentionally NOT supported here because names can collide across design assets. " +
|
|
4976
|
+
"To rename, pass `name` (the new name) together with `id`. Pass only the properties you want to change; omitted " +
|
|
4977
|
+
'ones keep their existing values. The settable properties are exactly the editor\'s "Update Text Style" modal ' +
|
|
4978
|
+
"fields — there is NO color or text-align on a text style. VALUE FORMATS (all validated; a bad value is rejected " +
|
|
4979
|
+
"with the offending value and the allowed set): font_weight is one of 100…900 (append 'i' for italic, e.g. " +
|
|
4980
|
+
'"600i") AND must be a weight the chosen font actually ships — many fonts only provide a subset (e.g. "Modak" ' +
|
|
4981
|
+
"only ships 400); if a weight isn't available the call fails and the error lists the supported weights, so when " +
|
|
4982
|
+
'unsure use 400; font_size is a number with a unit (e.g. "16px"); line_height is "normal", a unitless number ' +
|
|
4983
|
+
'or a length; letter_spacing is "normal" or a length (e.g. "0.02em"); font_family is a real font the editor ' +
|
|
4984
|
+
'offers (a Google Fonts name with EXACT casing, e.g. "Inter") — an unknown/invented family is rejected; ' +
|
|
4985
|
+
"text_decoration is none|underline|" +
|
|
4986
|
+
"line-through; text_transform is none|uppercase|lowercase|capitalize; text_overflow is clip|ellipsis; white_space " +
|
|
4987
|
+
"is normal|pre|pre-wrap|pre-line|wrap|nowrap|collapse; line_break is auto|loose|normal|strict|anywhere; word_break " +
|
|
4988
|
+
"is normal|break-all|keep-all; text_wrap_style is auto|balance|pretty|stable. Set `is_default: true` to make " +
|
|
4989
|
+
'this text style the theme\'s default/global style (the editor\'s right-click "Make Default Style" / "Global ' +
|
|
4990
|
+
"Stil Yap\") — any other default in the group is cleared; `is_default: false` removes the flag. A text style's " +
|
|
4991
|
+
"current default state is readable from `list_theme_globals` (each typography entry's `isDefault`). Requires " +
|
|
4992
|
+
"`ikas theme dev` running with the editor connected.", {
|
|
4993
|
+
project_root: z
|
|
4994
|
+
.string()
|
|
4995
|
+
.describe("Absolute path to the code-component project."),
|
|
4996
|
+
id: z
|
|
4997
|
+
.string()
|
|
4998
|
+
.describe("Text style id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
|
|
4999
|
+
name: z
|
|
5000
|
+
.string()
|
|
5001
|
+
.optional()
|
|
5002
|
+
.describe("Optional NEW name to rename the text style to. NOT used to find the token — lookup is always by id."),
|
|
5003
|
+
...textStyleFontParams,
|
|
5004
|
+
is_default: z
|
|
5005
|
+
.boolean()
|
|
5006
|
+
.optional()
|
|
5007
|
+
.describe("true → make this the theme's default/global text style (clears any other default); false → remove the " +
|
|
5008
|
+
"default flag. Omit to leave the default state unchanged."),
|
|
5009
|
+
port: z
|
|
5010
|
+
.number()
|
|
5011
|
+
.optional()
|
|
5012
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
5013
|
+
}, async (input) => {
|
|
5014
|
+
const { project_root, id, name, is_default, port } = input;
|
|
5015
|
+
const args = [
|
|
5016
|
+
"update-text-style",
|
|
5017
|
+
"--id",
|
|
5018
|
+
id,
|
|
5019
|
+
...(name !== undefined ? ["--name", name] : []),
|
|
5020
|
+
...textStyleFontArgs(input),
|
|
5021
|
+
...(is_default === true
|
|
5022
|
+
? ["--default"]
|
|
5023
|
+
: is_default === false
|
|
5024
|
+
? ["--no-default"]
|
|
5025
|
+
: []),
|
|
5026
|
+
...(port ? ["--port", String(port)] : []),
|
|
5027
|
+
];
|
|
5028
|
+
return callEditorAction(project_root, args);
|
|
5029
|
+
});
|
|
5030
|
+
// Tool: update_theme_color
|
|
5031
|
+
server.tool("update_theme_color", "Update an existing theme color token IN PLACE in the connected editor. Prefer this over delete+recreate: " +
|
|
5032
|
+
"it keeps the color's id, so its cssVar (`var(--<id>)`) — and every element style bound to it — stays STABLE. " +
|
|
5033
|
+
"Identify it by `id` (REQUIRED — globally unique, from `list_theme_globals`); name-based lookup is intentionally " +
|
|
5034
|
+
"NOT supported here because a theme can have two colors with the same name (e.g. from two different design assets). " +
|
|
5035
|
+
"To rename, pass `name` (the new name) together with `id`. Pass `value` to change the color — a CONCRETE " +
|
|
5036
|
+
'color (hex/rgb/hsl or a CSS color name), NOT a "var(...)" reference (a color token can\'t alias another ' +
|
|
5037
|
+
"color); omit it to only rename. Requires `ikas-component " +
|
|
5038
|
+
"dev` running with the editor connected.", {
|
|
5039
|
+
project_root: z
|
|
5040
|
+
.string()
|
|
5041
|
+
.describe("Absolute path to the code-component project."),
|
|
5042
|
+
id: z
|
|
5043
|
+
.string()
|
|
5044
|
+
.describe("Color id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
|
|
5045
|
+
name: z
|
|
5046
|
+
.string()
|
|
5047
|
+
.optional()
|
|
5048
|
+
.describe("Optional NEW name to rename the color to. NOT used to find the token — lookup is always by id."),
|
|
5049
|
+
value: z
|
|
5050
|
+
.string()
|
|
5051
|
+
.optional()
|
|
5052
|
+
.describe('New color value — a concrete color (e.g. "#ff0000", rgb/hsl/name), not a "var(...)" reference. Omit to only rename.'),
|
|
5053
|
+
port: z
|
|
5054
|
+
.number()
|
|
5055
|
+
.optional()
|
|
5056
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
5057
|
+
}, async ({ project_root, id, name, value, port }) => {
|
|
5058
|
+
const args = [
|
|
5059
|
+
"update-color",
|
|
5060
|
+
"--id",
|
|
5061
|
+
id,
|
|
5062
|
+
...(name !== undefined ? ["--name", name] : []),
|
|
5063
|
+
...(value !== undefined ? ["--value", value] : []),
|
|
5064
|
+
...(port ? ["--port", String(port)] : []),
|
|
5065
|
+
];
|
|
5066
|
+
return callEditorAction(project_root, args);
|
|
5067
|
+
});
|
|
5068
|
+
// Tool: update_theme_breakpoint
|
|
5069
|
+
server.tool("update_theme_breakpoint", "Update an existing theme breakpoint IN PLACE in the connected editor (keeps its id). Identify it by `id` " +
|
|
5070
|
+
"(REQUIRED — globally unique, from `list_theme_globals`); name-based lookup is intentionally NOT supported here " +
|
|
5071
|
+
"because names can collide across design assets. To rename, pass `name` (the new name) together with `id`. " +
|
|
5072
|
+
"Pass `width` (px) to change it; omit it to only rename. " +
|
|
5073
|
+
"Requires `ikas theme dev` running with the editor connected.", {
|
|
5074
|
+
project_root: z
|
|
5075
|
+
.string()
|
|
5076
|
+
.describe("Absolute path to the code-component project."),
|
|
5077
|
+
id: z
|
|
5078
|
+
.string()
|
|
5079
|
+
.describe("Breakpoint id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
|
|
5080
|
+
name: z
|
|
5081
|
+
.string()
|
|
5082
|
+
.optional()
|
|
5083
|
+
.describe("Optional NEW name to rename the breakpoint to. NOT used to find the token — lookup is always by id."),
|
|
5084
|
+
width: z
|
|
5085
|
+
.number()
|
|
5086
|
+
.int()
|
|
5087
|
+
.optional()
|
|
5088
|
+
.describe("New breakpoint width in px (positive integer, no decimals). Omit to only rename."),
|
|
5089
|
+
port: z
|
|
5090
|
+
.number()
|
|
5091
|
+
.optional()
|
|
5092
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
5093
|
+
}, async ({ project_root, id, name, width, port }) => {
|
|
5094
|
+
const args = [
|
|
5095
|
+
"update-breakpoint",
|
|
5096
|
+
"--id",
|
|
5097
|
+
id,
|
|
5098
|
+
...(name !== undefined ? ["--name", name] : []),
|
|
5099
|
+
...(width !== undefined ? ["--width", String(width)] : []),
|
|
5100
|
+
...(port ? ["--port", String(port)] : []),
|
|
5101
|
+
];
|
|
5102
|
+
return callEditorAction(project_root, args);
|
|
5103
|
+
});
|
|
5104
|
+
// Tool: update_theme_color_scheme
|
|
5105
|
+
server.tool("update_theme_color_scheme", "Update an existing theme color scheme (palette) IN PLACE in the connected editor. Prefer this over " +
|
|
5106
|
+
"delete+recreate: it keeps the scheme's id, so its className (`_<id>`) stays STABLE. Identify it by `id` " +
|
|
5107
|
+
"(REQUIRED — globally unique, from `list_theme_globals`); name-based lookup is intentionally NOT supported here " +
|
|
5108
|
+
"because names can collide across design assets. To rename, pass `name` (the new name) together with `id`. " +
|
|
5109
|
+
"`colors` is an array of { slotId? | newSlotName?, value } — each entry targets ONE slot, EITHER by " +
|
|
5110
|
+
"`slotId` (an EXISTING slot's id, from list_theme_globals colorsByScheme keys / colorSchemes.schemes — the " +
|
|
5111
|
+
"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 " +
|
|
5112
|
+
'a slot by name; use the standard slot names, "/" to group, e.g. "PrimaryButton/Background"); value = a ' +
|
|
5113
|
+
'literal color hex/rgb/hsl/name, OR an existing theme color\'s cssVar "var(--<colorId>)" from ' +
|
|
5114
|
+
"list_theme_globals to store it as a LINKED reference — pass the cssVar, not a hex copied from a global color) " +
|
|
5115
|
+
"and MERGES by slot — only the slots you pass change. Pass `colors` as REAL " +
|
|
5116
|
+
"JSON (not a stringified array). Set `is_default: true` to make this the theme's DEFAULT scheme (the " +
|
|
5117
|
+
"fallback whose colors fill any slot a non-default scheme leaves unset); this is exclusive — the editor " +
|
|
5118
|
+
"automatically clears the default flag from any other scheme in the same group, so there is always exactly " +
|
|
5119
|
+
"one default. Set `is_default: false` to remove the default flag from this scheme. Omit all of name/colors/" +
|
|
5120
|
+
"is_default to no-op. Requires `ikas theme dev` running with the editor connected.", {
|
|
5121
|
+
project_root: z
|
|
5122
|
+
.string()
|
|
5123
|
+
.describe("Absolute path to the code-component project."),
|
|
5124
|
+
id: z
|
|
5125
|
+
.string()
|
|
5126
|
+
.describe("Color scheme id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
|
|
5127
|
+
name: z
|
|
5128
|
+
.string()
|
|
5129
|
+
.optional()
|
|
5130
|
+
.describe("Optional NEW name to rename the color scheme to. NOT used to find the token — lookup is always by id."),
|
|
5131
|
+
colors: z
|
|
5132
|
+
.any()
|
|
5133
|
+
.optional()
|
|
5134
|
+
.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.'),
|
|
5135
|
+
is_default: z
|
|
5136
|
+
.boolean()
|
|
5137
|
+
.optional()
|
|
5138
|
+
.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`."),
|
|
5139
|
+
port: z
|
|
5140
|
+
.number()
|
|
5141
|
+
.optional()
|
|
5142
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
5143
|
+
}, async ({ project_root, id, name, colors, is_default, port }) => {
|
|
5144
|
+
const args = [
|
|
5145
|
+
"update-color-scheme",
|
|
5146
|
+
"--id",
|
|
5147
|
+
id,
|
|
5148
|
+
...(name !== undefined ? ["--name", name] : []),
|
|
5149
|
+
...(colors !== undefined ? ["--colors", toJsonArrayArg(colors)] : []),
|
|
5150
|
+
...(is_default === true
|
|
5151
|
+
? ["--default"]
|
|
5152
|
+
: is_default === false
|
|
5153
|
+
? ["--no-default"]
|
|
5154
|
+
: []),
|
|
5155
|
+
...(port ? ["--port", String(port)] : []),
|
|
5156
|
+
];
|
|
5157
|
+
return callEditorAction(project_root, args);
|
|
5158
|
+
});
|
|
5159
|
+
// Tool: rename_theme_color_scheme_slot
|
|
5160
|
+
server.tool("rename_theme_color_scheme_slot", 'Rename a color scheme SLOT (a scheme key like "Background" or "PrimaryButton/Text") IN PLACE in the ' +
|
|
5161
|
+
"connected editor. A slot's id is STABLE, so renaming keeps every runtime reference intact " +
|
|
5162
|
+
"(`var(--<slotId>)` in CSS, `colorsByScheme[slotId]` in component code) — only the human display name " +
|
|
5163
|
+
"changes; nothing rebinds. Identify the slot by `id` (REQUIRED — the slot id from `list_theme_globals` → " +
|
|
5164
|
+
"colorSchemes.schemes[].id or a `colorsByScheme` key); name-based lookup is intentionally NOT supported " +
|
|
5165
|
+
'because slot names can collide across design assets. `name` is the NEW slot name (a "/" groups it in the ' +
|
|
5166
|
+
'editor UI, e.g. "PrimaryButton/Background"). Only non-design-asset (user/theme) slots can be renamed here. ' +
|
|
5167
|
+
"This renames the slot itself across ALL schemes that use it (a slot is shared) — it does NOT rename a single " +
|
|
5168
|
+
"scheme/palette (for that use `update_theme_color_scheme`). Requires `ikas theme dev` running with the " +
|
|
5169
|
+
"editor connected.", {
|
|
5170
|
+
project_root: z
|
|
5171
|
+
.string()
|
|
5172
|
+
.describe("Absolute path to the code-component project."),
|
|
5173
|
+
id: z
|
|
5174
|
+
.string()
|
|
5175
|
+
.min(1, "id is REQUIRED — the slot id from list_theme_globals (colorSchemes.schemes[].id / colorsByScheme keys).")
|
|
5176
|
+
.describe("Slot id (REQUIRED) from `list_theme_globals` → colorSchemes.schemes[].id / colorsByScheme keys. The slot is always identified by id."),
|
|
5177
|
+
name: z
|
|
5178
|
+
.string()
|
|
5179
|
+
.min(1, "name is REQUIRED — the new slot name (non-empty).")
|
|
5180
|
+
.describe('The NEW slot name (non-empty). A "/" groups it in the editor UI (e.g. "PrimaryButton/Background").'),
|
|
5181
|
+
port: z
|
|
5182
|
+
.number()
|
|
5183
|
+
.optional()
|
|
5184
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
5185
|
+
}, async ({ project_root, id, name, port }) => {
|
|
5186
|
+
const args = [
|
|
5187
|
+
"rename-color-scheme-slot",
|
|
5188
|
+
"--id",
|
|
5189
|
+
id,
|
|
5190
|
+
"--name",
|
|
5191
|
+
name,
|
|
5192
|
+
...(port ? ["--port", String(port)] : []),
|
|
5193
|
+
];
|
|
5194
|
+
return callEditorAction(project_root, args);
|
|
5195
|
+
});
|
|
5196
|
+
// Tool: delete_theme_global
|
|
5197
|
+
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.', {
|
|
5198
|
+
project_root: z
|
|
5199
|
+
.string()
|
|
5200
|
+
.describe("Absolute path to the code-component project."),
|
|
5201
|
+
kind: z
|
|
5202
|
+
.enum([
|
|
5203
|
+
"globalVariable",
|
|
5204
|
+
"color",
|
|
5205
|
+
"typography",
|
|
5206
|
+
"breakpoint",
|
|
5207
|
+
"keyframe",
|
|
5208
|
+
"colorScheme",
|
|
5209
|
+
"colorSchemeSlot",
|
|
5210
|
+
])
|
|
5211
|
+
.describe("What to delete."),
|
|
5212
|
+
name: z
|
|
5213
|
+
.string()
|
|
5214
|
+
.optional()
|
|
5215
|
+
.describe("globalVariable runtime key (from `list_theme_globals`)."),
|
|
5216
|
+
id: z
|
|
5217
|
+
.string()
|
|
5218
|
+
.optional()
|
|
5219
|
+
.describe("design-token id, or for colorSchemeSlot the slot id from `list_theme_globals` → colorSchemes.schemes."),
|
|
5220
|
+
port: z
|
|
5221
|
+
.number()
|
|
5222
|
+
.optional()
|
|
5223
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
5224
|
+
}, async ({ project_root, kind, name, id, port }) => {
|
|
5225
|
+
const p = port ? ["--port", String(port)] : [];
|
|
5226
|
+
const args = kind === "globalVariable"
|
|
5227
|
+
? ["delete-global-variable", "--name", String(name ?? ""), ...p]
|
|
5228
|
+
: [
|
|
5229
|
+
"delete-design-token",
|
|
5230
|
+
"--token-type",
|
|
5231
|
+
kind,
|
|
5232
|
+
"--id",
|
|
5233
|
+
String(id ?? ""),
|
|
5234
|
+
...p,
|
|
5235
|
+
];
|
|
5236
|
+
return callEditorAction(project_root, args);
|
|
2606
5237
|
});
|
|
2607
5238
|
// --- Start server ---
|
|
2608
5239
|
async function main() {
|