@ikas/code-components-mcp 1.4.0-beta.4 → 1.4.0-beta.41
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 +39 -5
- package/data/migration.json +189 -24
- package/data/storefront-api.json +184 -1352
- package/data/storefront-types.json +32 -124
- package/dist/index.js +1743 -285
- 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`);
|
|
595
|
+
parts.push(`**Source:** ${components.length} old components, ${customData.filter((cd) => cd.isRoot).length} custom data types, ${(theme.pages || []).length} pages`);
|
|
514
596
|
parts.push("");
|
|
515
|
-
parts.push(
|
|
516
|
-
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,6 +651,57 @@ 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("");
|
|
@@ -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,8 +745,13 @@ 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
|
-
|
|
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);
|
|
625
755
|
children.push({ propName: p.name || "?", childName, fields });
|
|
626
756
|
}
|
|
627
757
|
}
|
|
@@ -638,59 +768,67 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
638
768
|
}
|
|
639
769
|
parts.push("");
|
|
640
770
|
}
|
|
641
|
-
//
|
|
642
|
-
parts.push(`##
|
|
771
|
+
// Source Code Analysis — placeholder for the LLM to fill in
|
|
772
|
+
parts.push(`## Source Code Analysis`);
|
|
643
773
|
parts.push("");
|
|
644
|
-
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.`);
|
|
645
775
|
parts.push("");
|
|
646
|
-
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`);
|
|
780
|
+
parts.push("");
|
|
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\`.`);
|
|
647
782
|
parts.push("");
|
|
648
|
-
|
|
649
|
-
parts.push(`## Per-Section Usage`);
|
|
783
|
+
parts.push(`<!-- Example: \`- SlideData → component \\\`hero-slide\\\` (2026-05-11) — structured record {image,link,title}; LIST_OF_LINK would drop the image\` -->`);
|
|
650
784
|
parts.push("");
|
|
651
|
-
|
|
785
|
+
// Known Environmental Issues (agents fill in during work)
|
|
786
|
+
parts.push(`## Known Environmental Issues`);
|
|
652
787
|
parts.push("");
|
|
653
|
-
parts.push(`
|
|
654
|
-
parts.push(
|
|
655
|
-
parts.push(
|
|
656
|
-
parts.push(`4. Run the CLI commands in the plan (they create parent + children with auto-generated types.ts).`);
|
|
657
|
-
parts.push(`5. Write \`index.tsx\` and \`styles.css\` using the patterns in the plan. DO NOT manually edit types.ts.`);
|
|
658
|
-
parts.push(`6. Mark the section \`[x]\` when it builds cleanly. Append a short note to the **Session Log** below (what libraries you replaced, any ad-hoc props you had to add, the actual folder name the CLI created if different from the name you passed).`);
|
|
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)_`);
|
|
659
791
|
parts.push("");
|
|
660
|
-
//
|
|
661
|
-
parts.push(`##
|
|
792
|
+
// Notes — append-only log for decisions not captured elsewhere
|
|
793
|
+
parts.push(`## Notes`);
|
|
662
794
|
parts.push("");
|
|
663
|
-
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>\`._`);
|
|
664
796
|
parts.push("");
|
|
665
|
-
parts.push(
|
|
666
|
-
parts.push(`<!-- \`- [2026-04-15] Foundation: built Input, SubmitButton, Modal, SectionHeader, StarRating. Skipped: GoogleCaptcha (needs investigation), BlogCard, Pagination (low priority).\` -->`);
|
|
667
|
-
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\` -->`);
|
|
668
798
|
parts.push("");
|
|
669
|
-
// Cross-references
|
|
799
|
+
// Cross-references — keep terse; LLM can call `get_migration_guide("list")` or `get_framework_guide("list")` for more
|
|
670
800
|
parts.push(`## Cross-References`);
|
|
671
801
|
parts.push("");
|
|
672
|
-
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`);
|
|
673
806
|
parts.push("");
|
|
674
|
-
parts.push(
|
|
675
|
-
parts.push(`- \`get_migration_guide("component-renderer-limitations")\` — critical constraints when using COMPONENT_LIST`);
|
|
676
|
-
parts.push(`- \`get_migration_guide("prop-runtime-shapes")\` — \`.data\` vs \`.links\`, \`.variant\` vs \`.product\`, etc.`);
|
|
677
|
-
parts.push(`- \`get_migration_guide("link-prop-decision-guide")\` — LINK vs LIST_OF_LINK vs COMPONENT_LIST`);
|
|
678
|
-
parts.push(`- \`get_migration_guide("library-replacements")\` — swiper, headlessui, tailwind, etc.`);
|
|
679
|
-
parts.push(`- \`get_migration_guide("react-to-preact")\` — observer rules, imports, IkasSlider removal`);
|
|
680
|
-
parts.push(`- \`get_framework_guide("component-renderer-patterns")\` — full IkasComponentRenderer usage`);
|
|
681
|
-
parts.push(`- \`get_framework_guide("common-pitfalls")\` — general gotchas`);
|
|
682
|
-
parts.push(`- \`get_framework_guide("navigation-patterns")\` — Router.navigate, Router.navigateToPage`);
|
|
683
|
-
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.`);
|
|
684
808
|
parts.push("");
|
|
685
809
|
return parts.join("\n");
|
|
686
810
|
}
|
|
687
811
|
// Known libraries we detect in old themes and want to flag for replacement
|
|
688
812
|
const KNOWN_LIBRARIES = [
|
|
689
|
-
"swiper",
|
|
690
|
-
"react
|
|
691
|
-
"react
|
|
692
|
-
"
|
|
693
|
-
"
|
|
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",
|
|
694
832
|
];
|
|
695
833
|
// Heuristic: member-access patterns on old storefront stores/singletons that likely need new-system equivalents
|
|
696
834
|
const OLD_STOREFRONT_CALL_REGEX = /\b(customerStore|cartStore|productStore|categoryStore|orderStore|searchStore|favoritesStore|i18nStore|Router|useStore)\.\w+/g;
|
|
@@ -709,7 +847,8 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
709
847
|
// Collect all .tsx/.ts files in the component dir
|
|
710
848
|
try {
|
|
711
849
|
for (const entry of fs.readdirSync(componentDir, { withFileTypes: true })) {
|
|
712
|
-
if (entry.isFile() &&
|
|
850
|
+
if (entry.isFile() &&
|
|
851
|
+
(entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
|
|
713
852
|
result.sourceFiles.push(path.join(componentDir, entry.name));
|
|
714
853
|
}
|
|
715
854
|
}
|
|
@@ -726,7 +865,18 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
726
865
|
const subCompSet = new Map();
|
|
727
866
|
const callSet = new Set();
|
|
728
867
|
// Packages we treat as "framework" and don't flag for replacement (but do note in reactPackageUsage)
|
|
729
|
-
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
|
+
]);
|
|
730
880
|
for (const file of result.sourceFiles) {
|
|
731
881
|
let content;
|
|
732
882
|
try {
|
|
@@ -740,7 +890,7 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
740
890
|
while ((m = importRegex.exec(content)) !== null) {
|
|
741
891
|
const p = m[1];
|
|
742
892
|
if (p.startsWith(".")) {
|
|
743
|
-
const segs = p.split("/").filter(s => s && s !== "." && s !== "..");
|
|
893
|
+
const segs = p.split("/").filter((s) => s && s !== "." && s !== "..");
|
|
744
894
|
const last = segs[segs.length - 1];
|
|
745
895
|
if (last && /^[A-Z]/.test(last) && !last.includes("__generated__")) {
|
|
746
896
|
subCompSet.set(last, p);
|
|
@@ -748,7 +898,9 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
748
898
|
}
|
|
749
899
|
else if (!p.startsWith("@ikas/")) {
|
|
750
900
|
// Classify: known library, react-family, or unknown-external
|
|
751
|
-
const base = p.startsWith("@")
|
|
901
|
+
const base = p.startsWith("@")
|
|
902
|
+
? p.split("/").slice(0, 2).join("/")
|
|
903
|
+
: p.split("/")[0];
|
|
752
904
|
if (REACT_PACKAGES.has(p) || REACT_PACKAGES.has(base)) {
|
|
753
905
|
reactSet.add(base);
|
|
754
906
|
}
|
|
@@ -811,7 +963,10 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
811
963
|
}
|
|
812
964
|
}
|
|
813
965
|
}
|
|
814
|
-
result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({
|
|
966
|
+
result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({
|
|
967
|
+
name,
|
|
968
|
+
path: p,
|
|
969
|
+
}));
|
|
815
970
|
result.importedLibraries = [...libSet].sort();
|
|
816
971
|
result.importedUnknownLibraries = [...unknownLibSet].sort();
|
|
817
972
|
result.reactPackageUsage = [...reactSet].sort();
|
|
@@ -827,27 +982,38 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
827
982
|
customDataMap.set(cd.id, cd);
|
|
828
983
|
}
|
|
829
984
|
// Find the component — try match by dir, displayName, id, or new-id
|
|
830
|
-
const target = components.find(c => {
|
|
985
|
+
const target = components.find((c) => {
|
|
831
986
|
if (!c)
|
|
832
987
|
return false;
|
|
833
|
-
if (c.dir === sectionName ||
|
|
988
|
+
if (c.dir === sectionName ||
|
|
989
|
+
c.displayName === sectionName ||
|
|
990
|
+
c.id === sectionName)
|
|
834
991
|
return true;
|
|
835
992
|
const kebab = toKebabCase(c.dir || c.displayName || c.id || "");
|
|
836
993
|
const newId = `${projectName}-${kebab}`;
|
|
837
994
|
return newId === sectionName;
|
|
838
995
|
});
|
|
839
996
|
if (!target) {
|
|
840
|
-
const available = components
|
|
997
|
+
const available = components
|
|
998
|
+
.map((c) => c.dir || c.displayName || c.id)
|
|
999
|
+
.filter(Boolean)
|
|
1000
|
+
.join(", ");
|
|
841
1001
|
return `Section "${sectionName}" not found in theme. Available: ${available}`;
|
|
842
1002
|
}
|
|
843
1003
|
const parts = [];
|
|
844
1004
|
const oldName = target.displayName || target.dir || target.id || "Unknown";
|
|
845
1005
|
const kebabName = toKebabCase(target.dir || target.displayName || target.id || "unknown");
|
|
846
1006
|
const sectionId = `${projectName}-${kebabName}`;
|
|
847
|
-
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("");
|
|
848
1012
|
// Scan the old source for imports, libraries, field usage
|
|
849
|
-
const propNames = (target.props || [])
|
|
850
|
-
|
|
1013
|
+
const propNames = (target.props || [])
|
|
1014
|
+
.map((p) => p.name || "")
|
|
1015
|
+
.filter(Boolean);
|
|
1016
|
+
const sourceScan = oldSourceDir && target.dir
|
|
851
1017
|
? scanSectionSource(path.join(oldSourceDir, target.dir), propNames)
|
|
852
1018
|
: null;
|
|
853
1019
|
parts.push(`# Section Migration Plan: ${oldName}`);
|
|
@@ -893,7 +1059,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
893
1059
|
}
|
|
894
1060
|
if (sourceScan.reactPackageUsage.length > 0) {
|
|
895
1061
|
parts.push("");
|
|
896
|
-
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).`);
|
|
897
1063
|
}
|
|
898
1064
|
if (sourceScan.oldStorefrontCalls.length > 0) {
|
|
899
1065
|
parts.push("");
|
|
@@ -939,7 +1105,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
939
1105
|
if (oldType === "SLIDER") {
|
|
940
1106
|
newType = "NUMBER";
|
|
941
1107
|
notes = `Was SLIDER(min=${p.sliderData?.min}, max=${p.sliderData?.max}) — replace \`.value\` access with direct number`;
|
|
942
|
-
const prop = {
|
|
1108
|
+
const prop = {
|
|
1109
|
+
name: newName,
|
|
1110
|
+
displayName: p.displayName || newName,
|
|
1111
|
+
type: "NUMBER",
|
|
1112
|
+
};
|
|
943
1113
|
if (p.isRequired)
|
|
944
1114
|
prop.required = true;
|
|
945
1115
|
parentPropsJson.push(prop);
|
|
@@ -947,7 +1117,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
947
1117
|
else if (oldType === "PRODUCT_DETAIL") {
|
|
948
1118
|
newType = "PRODUCT";
|
|
949
1119
|
notes = "Renamed — PRODUCT_DETAIL → PRODUCT";
|
|
950
|
-
const prop = {
|
|
1120
|
+
const prop = {
|
|
1121
|
+
name: newName,
|
|
1122
|
+
displayName: p.displayName || newName,
|
|
1123
|
+
type: "PRODUCT",
|
|
1124
|
+
};
|
|
951
1125
|
if (p.isRequired)
|
|
952
1126
|
prop.required = true;
|
|
953
1127
|
parentPropsJson.push(prop);
|
|
@@ -958,7 +1132,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
958
1132
|
if (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST") {
|
|
959
1133
|
// Child component needed
|
|
960
1134
|
const itemObj = cd.nestedData?.[0];
|
|
961
|
-
const childName = itemObj?.typescriptName ||
|
|
1135
|
+
const childName = itemObj?.typescriptName ||
|
|
1136
|
+
(itemObj?.name
|
|
1137
|
+
? itemObj.name.replace(/[^a-zA-Z0-9]/g, "")
|
|
1138
|
+
: `${sectionPascal}Item`);
|
|
962
1139
|
const childProps = [];
|
|
963
1140
|
const nestedWarnings = [];
|
|
964
1141
|
for (const f of (itemObj?.nestedData || [])) {
|
|
@@ -969,11 +1146,18 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
969
1146
|
fType = "NUMBER";
|
|
970
1147
|
else if (fType === "PRODUCT_DETAIL")
|
|
971
1148
|
fType = "PRODUCT";
|
|
972
|
-
else if (fType === "CUSTOM" ||
|
|
1149
|
+
else if (fType === "CUSTOM" ||
|
|
1150
|
+
fType === "DYNAMIC_LIST" ||
|
|
1151
|
+
fType === "STATIC_LIST" ||
|
|
1152
|
+
fType === "OBJECT") {
|
|
973
1153
|
nestedWarnings.push(`\`${f.key}\` (${fType})`);
|
|
974
1154
|
fType = "COMPONENT_LIST";
|
|
975
1155
|
}
|
|
976
|
-
const prop = {
|
|
1156
|
+
const prop = {
|
|
1157
|
+
name: f.key,
|
|
1158
|
+
displayName: f.name || f.key,
|
|
1159
|
+
type: fType,
|
|
1160
|
+
};
|
|
977
1161
|
if (f.isRequired)
|
|
978
1162
|
prop.required = true;
|
|
979
1163
|
childProps.push(prop);
|
|
@@ -985,10 +1169,14 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
985
1169
|
if (childProps.length === 0 && sourceScan?.propFieldUsage[oldName]) {
|
|
986
1170
|
const inferred = sourceScan.propFieldUsage[oldName];
|
|
987
1171
|
for (const fieldName of inferred) {
|
|
988
|
-
childProps.push({
|
|
1172
|
+
childProps.push({
|
|
1173
|
+
name: fieldName,
|
|
1174
|
+
displayName: fieldName,
|
|
1175
|
+
type: "TEXT",
|
|
1176
|
+
});
|
|
989
1177
|
}
|
|
990
1178
|
if (inferred.length > 0) {
|
|
991
|
-
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.`;
|
|
992
1180
|
}
|
|
993
1181
|
}
|
|
994
1182
|
children.push({
|
|
@@ -1017,7 +1205,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1017
1205
|
let fType = f.type;
|
|
1018
1206
|
if (fType === "SLIDER")
|
|
1019
1207
|
fType = "NUMBER";
|
|
1020
|
-
const prop = {
|
|
1208
|
+
const prop = {
|
|
1209
|
+
name: f.key,
|
|
1210
|
+
displayName: f.name || f.key,
|
|
1211
|
+
type: fType,
|
|
1212
|
+
};
|
|
1021
1213
|
if (f.isRequired)
|
|
1022
1214
|
prop.required = true;
|
|
1023
1215
|
parentPropsJson.push(prop);
|
|
@@ -1032,7 +1224,8 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1032
1224
|
}
|
|
1033
1225
|
else if (cd.type === "ENUM") {
|
|
1034
1226
|
newType = "ENUM";
|
|
1035
|
-
const enumName = cd.typescriptName ||
|
|
1227
|
+
const enumName = cd.typescriptName ||
|
|
1228
|
+
(cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
|
|
1036
1229
|
const options = (cd.enumOptions || []).reduce((acc, o) => {
|
|
1037
1230
|
if (o.displayName && o.value)
|
|
1038
1231
|
acc[o.displayName] = o.value;
|
|
@@ -1040,7 +1233,12 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1040
1233
|
}, {});
|
|
1041
1234
|
enumsNeeded.push({ name: enumName, options });
|
|
1042
1235
|
notes = `Was CUSTOM (ENUM) — create enum \`${enumName}\` via \`config add-enum\` first, then reference its enumId here`;
|
|
1043
|
-
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
|
+
};
|
|
1044
1242
|
if (p.isRequired)
|
|
1045
1243
|
prop.required = true;
|
|
1046
1244
|
parentPropsJson.push(prop);
|
|
@@ -1049,7 +1247,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1049
1247
|
}
|
|
1050
1248
|
else {
|
|
1051
1249
|
// Direct mapping
|
|
1052
|
-
const prop = {
|
|
1250
|
+
const prop = {
|
|
1251
|
+
name: newName,
|
|
1252
|
+
displayName: p.displayName || newName,
|
|
1253
|
+
type: newType,
|
|
1254
|
+
};
|
|
1053
1255
|
if (p.isRequired)
|
|
1054
1256
|
prop.required = true;
|
|
1055
1257
|
parentPropsJson.push(prop);
|
|
@@ -1057,6 +1259,131 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1057
1259
|
parts.push(`| \`${oldName}\` | ${oldType} | → | \`${newName}\` | ${newType} | ${notes} |`);
|
|
1058
1260
|
}
|
|
1059
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
|
+
}
|
|
1060
1387
|
// Enums to create first
|
|
1061
1388
|
if (enumsNeeded.length > 0) {
|
|
1062
1389
|
parts.push(`## 3. Create Enums FIRST (if not already done)`);
|
|
@@ -1077,7 +1404,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1077
1404
|
existing.usedByProps.push(ch.propName);
|
|
1078
1405
|
}
|
|
1079
1406
|
else {
|
|
1080
|
-
uniqueChildren.set(ch.childName, {
|
|
1407
|
+
uniqueChildren.set(ch.childName, {
|
|
1408
|
+
child: ch,
|
|
1409
|
+
usedByProps: [ch.propName],
|
|
1410
|
+
});
|
|
1081
1411
|
}
|
|
1082
1412
|
}
|
|
1083
1413
|
if (uniqueChildren.size > 0) {
|
|
@@ -1090,7 +1420,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1090
1420
|
for (const { child: ch, usedByProps } of uniqueChildren.values()) {
|
|
1091
1421
|
parts.push(`### \`${ch.childName}\``);
|
|
1092
1422
|
const propsLabel = usedByProps.length > 1
|
|
1093
|
-
? `Used by parent props: ${usedByProps.map(p => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
|
|
1423
|
+
? `Used by parent props: ${usedByProps.map((p) => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
|
|
1094
1424
|
: `For parent prop: \`${usedByProps[0]}\``;
|
|
1095
1425
|
parts.push(propsLabel);
|
|
1096
1426
|
parts.push(`Old customData: "${ch.customDataName}"`);
|
|
@@ -1136,12 +1466,15 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1136
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.`);
|
|
1137
1467
|
}
|
|
1138
1468
|
// Check if the section itself has data-driven list props (PRODUCT_LIST, BLOG_LIST, CATEGORY_LIST)
|
|
1139
|
-
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");
|
|
1140
1473
|
if (dataListProps.length > 0) {
|
|
1141
1474
|
parts.push("");
|
|
1142
1475
|
parts.push(`### Data-Driven List Rendering`);
|
|
1143
1476
|
parts.push("");
|
|
1144
|
-
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(", ")}.`);
|
|
1145
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:`);
|
|
1146
1479
|
parts.push("");
|
|
1147
1480
|
parts.push("```tsx");
|
|
@@ -1168,9 +1501,22 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1168
1501
|
parts.push(`See \`get_migration_guide("custom-data-conversion")\` → "Two Ways to Render Lists" for the full pattern.`);
|
|
1169
1502
|
}
|
|
1170
1503
|
// Detect form-page sections (0 or few props, name suggests a form/auth page)
|
|
1171
|
-
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
|
+
];
|
|
1172
1518
|
const lowerDir = (target.dir || "").toLowerCase();
|
|
1173
|
-
const isLikelyFormPage = formKeywords.some(kw => lowerDir.includes(kw));
|
|
1519
|
+
const isLikelyFormPage = formKeywords.some((kw) => lowerDir.includes(kw));
|
|
1174
1520
|
if (isLikelyFormPage) {
|
|
1175
1521
|
parts.push("");
|
|
1176
1522
|
parts.push(`### Form Page Pattern`);
|
|
@@ -1199,7 +1545,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1199
1545
|
// Fallback heuristic only when source scan unavailable
|
|
1200
1546
|
const heuristicLibs = [];
|
|
1201
1547
|
const lowerName = oldName.toLowerCase();
|
|
1202
|
-
if (lowerName.includes("slider") ||
|
|
1548
|
+
if (lowerName.includes("slider") ||
|
|
1549
|
+
lowerName.includes("carousel") ||
|
|
1550
|
+
lowerName.includes("banner")) {
|
|
1203
1551
|
heuristicLibs.push("swiper");
|
|
1204
1552
|
}
|
|
1205
1553
|
if (lowerName.includes("marquee"))
|
|
@@ -1208,7 +1556,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1208
1556
|
heuristicLibs.push("react-player");
|
|
1209
1557
|
if (lowerName.includes("chart"))
|
|
1210
1558
|
heuristicLibs.push("recharts");
|
|
1211
|
-
if (lowerName.includes("star") ||
|
|
1559
|
+
if (lowerName.includes("star") ||
|
|
1560
|
+
lowerName.includes("rating") ||
|
|
1561
|
+
lowerName.includes("review"))
|
|
1212
1562
|
heuristicLibs.push("react-simple-star-rating");
|
|
1213
1563
|
if (heuristicLibs.length > 0) {
|
|
1214
1564
|
parts.push(`### Likely Library Replacements (heuristic — source not scanned)`);
|
|
@@ -1222,30 +1572,19 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1222
1572
|
parts.push("");
|
|
1223
1573
|
}
|
|
1224
1574
|
}
|
|
1225
|
-
// Relevant guides
|
|
1226
|
-
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`);
|
|
1227
1577
|
parts.push("");
|
|
1228
1578
|
parts.push(`- \`get_migration_guide("react-to-preact")\` — code conversion patterns`);
|
|
1229
|
-
parts.push(`- \`get_migration_guide("
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
parts.push(`- \`get_migration_guide("component-renderer-limitations")\` — critical COMPONENT_LIST constraints`);
|
|
1233
|
-
parts.push(`- \`get_framework_guide("component-renderer-patterns")\` — full IkasComponentRenderer usage`);
|
|
1234
|
-
parts.push(`- \`get_migration_example("custom-dynamic-list-to-component-list")\` — concrete example`);
|
|
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`);
|
|
1235
1582
|
}
|
|
1236
|
-
if (target.isHeader || target.isFooter) {
|
|
1237
|
-
parts.push(`- \`get_framework_guide("header-footer-patterns")\` — header/footer specifics`);
|
|
1238
|
-
}
|
|
1239
|
-
parts.push(`- \`get_framework_guide("common-pitfalls")\` — observer rules, common mistakes`);
|
|
1240
1583
|
parts.push("");
|
|
1241
1584
|
// Completion
|
|
1242
1585
|
parts.push(`## ${nextStep + 2}. Mark Complete`);
|
|
1243
1586
|
parts.push("");
|
|
1244
|
-
parts.push(`Once the section builds cleanly with \`npx ikas-component build
|
|
1245
|
-
parts.push(`1. Edit \`MIGRATION.md\` at the project root`);
|
|
1246
|
-
parts.push(`2. Change the checkbox for \`${sectionId}\` from \`[ ]\` to \`[x]\``);
|
|
1247
|
-
parts.push(`3. Also mark each child component as \`[x]\``);
|
|
1248
|
-
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.`);
|
|
1249
1588
|
parts.push("");
|
|
1250
1589
|
return parts.join("\n");
|
|
1251
1590
|
}
|
|
@@ -1271,21 +1610,80 @@ function matchScore(text, query) {
|
|
|
1271
1610
|
}
|
|
1272
1611
|
return score;
|
|
1273
1612
|
}
|
|
1613
|
+
// Levenshtein edit distance — small DP, fine for the 28 short kebab-case section template names.
|
|
1614
|
+
function levenshtein(a, b) {
|
|
1615
|
+
if (a === b)
|
|
1616
|
+
return 0;
|
|
1617
|
+
if (a.length === 0)
|
|
1618
|
+
return b.length;
|
|
1619
|
+
if (b.length === 0)
|
|
1620
|
+
return a.length;
|
|
1621
|
+
const m = a.length;
|
|
1622
|
+
const n = b.length;
|
|
1623
|
+
const prev = new Array(n + 1);
|
|
1624
|
+
const curr = new Array(n + 1);
|
|
1625
|
+
for (let j = 0; j <= n; j++)
|
|
1626
|
+
prev[j] = j;
|
|
1627
|
+
for (let i = 1; i <= m; i++) {
|
|
1628
|
+
curr[0] = i;
|
|
1629
|
+
for (let j = 1; j <= n; j++) {
|
|
1630
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
1631
|
+
curr[j] = Math.min(curr[j - 1] + 1, // insertion
|
|
1632
|
+
prev[j] + 1, // deletion
|
|
1633
|
+
prev[j - 1] + cost);
|
|
1634
|
+
}
|
|
1635
|
+
for (let j = 0; j <= n; j++)
|
|
1636
|
+
prev[j] = curr[j];
|
|
1637
|
+
}
|
|
1638
|
+
return prev[n];
|
|
1639
|
+
}
|
|
1640
|
+
// Suggest up to `max` closest candidates for an unknown input.
|
|
1641
|
+
// Primary: matchScore (substring/word). Fallback: Levenshtein when nothing scores well.
|
|
1642
|
+
function suggestClosestNames(input, candidates, opts) {
|
|
1643
|
+
const min = opts?.min ?? 8;
|
|
1644
|
+
const max = opts?.max ?? 3;
|
|
1645
|
+
const scored = candidates
|
|
1646
|
+
.map((c) => ({ name: c, score: matchScore(c, input) }))
|
|
1647
|
+
.filter((s) => s.score >= min)
|
|
1648
|
+
.sort((a, b) => b.score - a.score);
|
|
1649
|
+
if (scored.length > 0)
|
|
1650
|
+
return scored.slice(0, max).map((s) => s.name);
|
|
1651
|
+
// Levenshtein fallback — tolerate up to ceil(len/3) edits.
|
|
1652
|
+
const maxDistance = Math.max(1, Math.ceil(input.length / 3));
|
|
1653
|
+
const lower = input.toLowerCase();
|
|
1654
|
+
const edits = candidates
|
|
1655
|
+
.map((c) => ({ name: c, d: levenshtein(lower, c.toLowerCase()) }))
|
|
1656
|
+
.filter((s) => s.d <= maxDistance)
|
|
1657
|
+
.sort((a, b) => a.d - b.d);
|
|
1658
|
+
return edits.slice(0, max).map((s) => s.name);
|
|
1659
|
+
}
|
|
1274
1660
|
function searchFunctions(query) {
|
|
1275
1661
|
const scored = storefrontData.functions
|
|
1276
1662
|
.map((fn) => {
|
|
1277
1663
|
const nameScore = matchScore(fn.name, query) * 3;
|
|
1278
|
-
const displayNameScore = fn.displayName
|
|
1664
|
+
const displayNameScore = fn.displayName
|
|
1665
|
+
? matchScore(fn.displayName, query) * 3
|
|
1666
|
+
: 0;
|
|
1279
1667
|
const descScore = matchScore(fn.description, query);
|
|
1280
|
-
const catScore = fn.categories.some((c) => matchScore(c, query) > 0)
|
|
1668
|
+
const catScore = fn.categories.some((c) => matchScore(c, query) > 0)
|
|
1669
|
+
? 5
|
|
1670
|
+
: 0;
|
|
1281
1671
|
const paramScore = fn.params.some((p) => matchScore(p.name, query) > 0 || matchScore(p.description, query) > 0)
|
|
1282
1672
|
? 2
|
|
1283
1673
|
: 0;
|
|
1284
1674
|
const sigScore = matchScore(fn.signature, query) * 2;
|
|
1285
|
-
const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0)
|
|
1675
|
+
const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0)
|
|
1676
|
+
? 8
|
|
1677
|
+
: 0;
|
|
1286
1678
|
return {
|
|
1287
1679
|
fn,
|
|
1288
|
-
score: nameScore +
|
|
1680
|
+
score: nameScore +
|
|
1681
|
+
displayNameScore +
|
|
1682
|
+
descScore +
|
|
1683
|
+
catScore +
|
|
1684
|
+
paramScore +
|
|
1685
|
+
sigScore +
|
|
1686
|
+
typeScore,
|
|
1289
1687
|
};
|
|
1290
1688
|
})
|
|
1291
1689
|
.filter((item) => item.score > 0)
|
|
@@ -1299,7 +1697,11 @@ function searchFrameworkTopics(query) {
|
|
|
1299
1697
|
const descScore = matchScore(topic.description, query) * 2;
|
|
1300
1698
|
const contentScore = matchScore(topic.content, query);
|
|
1301
1699
|
const tagScore = topic.tags.some((t) => matchScore(t, query) > 0) ? 5 : 0;
|
|
1302
|
-
return {
|
|
1700
|
+
return {
|
|
1701
|
+
key,
|
|
1702
|
+
topic,
|
|
1703
|
+
score: titleScore + descScore + contentScore + tagScore,
|
|
1704
|
+
};
|
|
1303
1705
|
})
|
|
1304
1706
|
.filter((item) => item.score > 0)
|
|
1305
1707
|
.sort((a, b) => b.score - a.score);
|
|
@@ -1314,7 +1716,9 @@ function searchTypes(query) {
|
|
|
1314
1716
|
const propScore = td.properties?.some((p) => matchScore(p.name, query) > 0 || matchScore(p.type, query) > 0)
|
|
1315
1717
|
? 4
|
|
1316
1718
|
: 0;
|
|
1317
|
-
const enumScore = td.enumValues?.some((v) => matchScore(v, query) > 0)
|
|
1719
|
+
const enumScore = td.enumValues?.some((v) => matchScore(v, query) > 0)
|
|
1720
|
+
? 4
|
|
1721
|
+
: 0;
|
|
1318
1722
|
return { td, score: nameScore + domainScore + propScore + enumScore };
|
|
1319
1723
|
})
|
|
1320
1724
|
.filter((item) => item.score > 0)
|
|
@@ -1356,8 +1760,12 @@ function formatFunctionDoc(fn) {
|
|
|
1356
1760
|
return lines.join("\n");
|
|
1357
1761
|
}
|
|
1358
1762
|
function formatFunctionSummary(fn) {
|
|
1359
|
-
const desc = fn.description
|
|
1360
|
-
|
|
1763
|
+
const desc = fn.description
|
|
1764
|
+
? fn.description.split(".")[0] + "."
|
|
1765
|
+
: "No description.";
|
|
1766
|
+
const alias = fn.displayName && fn.displayName !== fn.name
|
|
1767
|
+
? ` (alias: ${fn.displayName})`
|
|
1768
|
+
: "";
|
|
1361
1769
|
return `- \`${fn.name}\`${alias} - ${desc}`;
|
|
1362
1770
|
}
|
|
1363
1771
|
function formatTypeDefinition(td, opts = {}) {
|
|
@@ -1398,7 +1806,9 @@ function formatTypeDefinition(td, opts = {}) {
|
|
|
1398
1806
|
const fn = storefrontData.functions.find((f) => f.name === fnName);
|
|
1399
1807
|
if (fn) {
|
|
1400
1808
|
const desc = fn.description ? fn.description.split(".")[0] + "." : "";
|
|
1401
|
-
const alias = fn.displayName && fn.displayName !== fn.name
|
|
1809
|
+
const alias = fn.displayName && fn.displayName !== fn.name
|
|
1810
|
+
? ` (alias: ${fn.displayName})`
|
|
1811
|
+
: "";
|
|
1402
1812
|
lines.push(`- **\`${fn.name}\`**${alias} — ${desc}`);
|
|
1403
1813
|
lines.push(` \`${fn.signature}\``);
|
|
1404
1814
|
}
|
|
@@ -1407,7 +1817,9 @@ function formatTypeDefinition(td, opts = {}) {
|
|
|
1407
1817
|
}
|
|
1408
1818
|
}
|
|
1409
1819
|
lines.push("");
|
|
1410
|
-
lines.push(
|
|
1820
|
+
lines.push('Use `get_functions_for_type("' +
|
|
1821
|
+
td.name +
|
|
1822
|
+
'")` for full documentation of these functions.');
|
|
1411
1823
|
}
|
|
1412
1824
|
return lines.join("\n");
|
|
1413
1825
|
}
|
|
@@ -1425,7 +1837,13 @@ const server = new McpServer({
|
|
|
1425
1837
|
name: "ikas-code-components",
|
|
1426
1838
|
version: "0.1.0",
|
|
1427
1839
|
}, {
|
|
1428
|
-
instructions: "Examples and section templates from this server are API reference only — reuse imports, function calls, and data-access patterns; create your own JSX structure, CSS class names, and visual design
|
|
1840
|
+
instructions: "Examples and section templates from this server are API reference only — reuse imports, function calls, and data-access patterns; create your own JSX structure, CSS class names, and visual design.\n\n" +
|
|
1841
|
+
"Live-editor actions (require `ikas-component dev` running with the editor connected): `list_editor_pages` → page ids; `list_imported_sections` → imported component ids; `import_section` → import a built component; `add_section_to_page` → place one section (`add_sections_to_page` places MANY at once and can set their props in the same call — fastest way to build a page); `list_page_sections` → the sections placed on a page (LEAN roster: per-placement `elementId`, `componentId`, `propCount`, which props are filled — NO schema/values, so it never truncates); `get_component_props` → a component's prop blueprint (types, ENUM valid `options`, COMPONENT_LIST `allowedComponentIds`) for any section OR child id; `get_section_values` → one section's current prop values (for read-modify-write); `get_page_by_type` → a theme page id by pageType (e.g. CATEGORY), to build PAGE links to entities (`create_page` adds that page if it does not exist yet, so you never guess a slug); `update_section_prop` → change a single prop value of a placed section (`update_page_sections` fills MANY sections/props of a page in ONE call — strongly prefer it to cut round-trips when filling content); `upload_image` → upload one image (file or URL) and get an image id (`upload_images` uploads many in one call — prefer it for several); `search_products` / `list_categories` / `list_brands` / `list_blogs` / `list_blog_categories` → find real entity ids for PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY prop values; `publish_theme` → publish the theme LIVE and get back the preview URL to review (guarded — needs `confirm:true`, and `confirm_production:true` for the main theme; only call it when the user explicitly asks to publish). For the full end-to-end process (placing a section and filling its content, with per-prop value shapes and COMPONENT_LIST rules), call `get_editor_workflow` first.\n\n" +
|
|
1842
|
+
"To CHANGE/EDIT a prop value (text, color, boolean, number, etc.) of a section that is already on a page, this IS supported: call `list_page_sections(page_id)` to get the placement `elementId` and the prop names/ids, then `update_section_prop(page_id, element_id, prop_name|prop_id, value)`. Do not assume prop editing is unavailable.\n\n" +
|
|
1843
|
+
"TWO DISTINCT JOBS — do not confuse them, and do not jump to writing code for the second one:\n" +
|
|
1844
|
+
"(A) DEFINING props / authoring a component = changing which props a component HAS. This edits the component source/config (types.ts via `ikas-component config add-prop`/`add-component`, JSX in index.tsx, etc.) and requires a rebuild. Use this ONLY when the needed prop does not exist yet, or the user explicitly asks to build/modify the component's code or prop schema.\n" +
|
|
1845
|
+
"(B) SETTING prop VALUES / filling content = giving values to props that ALREADY exist on a section placed on a page. This is pure data entry via `list_page_sections` + `update_section_prop` (and `upload_image` for images). It writes NO code and needs NO rebuild.\n" +
|
|
1846
|
+
"When the user says things like 'fill this section', 'set the heading/title/text/image/link', 'populate', 'change the content/value', or 'enter this data', that is JOB (B): use `update_section_prop`. First call `list_page_sections` and check the existing `props` — if the prop is already there (it usually is), just set its value. Only fall back to JOB (A) if the required prop genuinely does not exist in that section's `props`. Writing a new component or adding a prop to fill in a value the section already supports is the wrong move.",
|
|
1429
1847
|
});
|
|
1430
1848
|
// Tool: search_docs
|
|
1431
1849
|
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 }) => {
|
|
@@ -1466,13 +1884,20 @@ server.tool("search_docs", "Search across all ikas storefront API docs, framewor
|
|
|
1466
1884
|
parts.push("");
|
|
1467
1885
|
parts.push("Use `get_migration_guide(topic)` to get full content for any migration topic.");
|
|
1468
1886
|
}
|
|
1469
|
-
if (functions.length === 0 &&
|
|
1887
|
+
if (functions.length === 0 &&
|
|
1888
|
+
topics.length === 0 &&
|
|
1889
|
+
types.length === 0 &&
|
|
1890
|
+
migrationTopics.length === 0) {
|
|
1470
1891
|
parts.push(`No results found for "${query}". Try different keywords or use \`list_functions()\` to see all available functions.`);
|
|
1471
1892
|
}
|
|
1472
1893
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1473
1894
|
});
|
|
1474
1895
|
// Tool: get_function_doc
|
|
1475
|
-
server.tool("get_function_doc", "Get full documentation for a specific storefront API function including signature, parameters, return type, and example.", {
|
|
1896
|
+
server.tool("get_function_doc", "Get full documentation for a specific storefront API function including signature, parameters, return type, and example.", {
|
|
1897
|
+
name: z
|
|
1898
|
+
.string()
|
|
1899
|
+
.describe("Function name (e.g. 'addItemToCart', 'Router.navigate')"),
|
|
1900
|
+
}, async ({ name }) => {
|
|
1476
1901
|
const nameLower = name.toLowerCase();
|
|
1477
1902
|
// Phase 1: canonical-name match wins. A real function name always outranks
|
|
1478
1903
|
// any displayName alias so aliases can never shadow the function they're
|
|
@@ -1480,7 +1905,9 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1480
1905
|
// [BP-DISPLAY-NAME: hasCustomer] alias).
|
|
1481
1906
|
const byName = storefrontData.functions.find((f) => f.name.toLowerCase() === nameLower);
|
|
1482
1907
|
if (byName) {
|
|
1483
|
-
return {
|
|
1908
|
+
return {
|
|
1909
|
+
content: [{ type: "text", text: formatFunctionDoc(byName) }],
|
|
1910
|
+
};
|
|
1484
1911
|
}
|
|
1485
1912
|
// Phase 2: fall back to displayName aliases.
|
|
1486
1913
|
const byAlias = storefrontData.functions.filter((f) => f.displayName && f.displayName.toLowerCase() === nameLower);
|
|
@@ -1488,7 +1915,9 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1488
1915
|
const fn = byAlias[0];
|
|
1489
1916
|
const note = `> Note: "${name}" is a display alias for \`${fn.name}\`.\n\n`;
|
|
1490
1917
|
return {
|
|
1491
|
-
content: [
|
|
1918
|
+
content: [
|
|
1919
|
+
{ type: "text", text: note + formatFunctionDoc(fn) },
|
|
1920
|
+
],
|
|
1492
1921
|
};
|
|
1493
1922
|
}
|
|
1494
1923
|
if (byAlias.length > 1) {
|
|
@@ -1506,7 +1935,9 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1506
1935
|
(f.displayName && f.displayName.toLowerCase().includes(nameLower)));
|
|
1507
1936
|
if (matches.length > 0) {
|
|
1508
1937
|
const suggestions = matches.slice(0, 5).map((f) => {
|
|
1509
|
-
const alias = f.displayName && f.displayName !== f.name
|
|
1938
|
+
const alias = f.displayName && f.displayName !== f.name
|
|
1939
|
+
? ` (alias: ${f.displayName})`
|
|
1940
|
+
: "";
|
|
1510
1941
|
return ` - ${f.name}${alias}`;
|
|
1511
1942
|
});
|
|
1512
1943
|
return {
|
|
@@ -1519,7 +1950,12 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
|
|
|
1519
1950
|
};
|
|
1520
1951
|
}
|
|
1521
1952
|
return {
|
|
1522
|
-
content: [
|
|
1953
|
+
content: [
|
|
1954
|
+
{
|
|
1955
|
+
type: "text",
|
|
1956
|
+
text: `Function "${name}" not found. Use \`list_functions()\` to see all available functions.`,
|
|
1957
|
+
},
|
|
1958
|
+
],
|
|
1523
1959
|
};
|
|
1524
1960
|
});
|
|
1525
1961
|
// Tool: list_functions
|
|
@@ -1569,7 +2005,7 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
|
|
|
1569
2005
|
if (uncategorized > 0) {
|
|
1570
2006
|
lines.push(`- \`Other\` (${uncategorized})`);
|
|
1571
2007
|
}
|
|
1572
|
-
lines.push("",
|
|
2008
|
+
lines.push("", 'Call `list_functions(category: "<name>")` to see one-line summaries for a category.');
|
|
1573
2009
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1574
2010
|
}
|
|
1575
2011
|
const catLower = category.toLowerCase();
|
|
@@ -1601,7 +2037,11 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
|
|
|
1601
2037
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1602
2038
|
});
|
|
1603
2039
|
// Tool: get_code_example
|
|
1604
|
-
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.", {
|
|
2040
|
+
server.tool("get_code_example", "Get an API usage reference for a specific task. Shows correct function calls, imports, and data-handling patterns from a real production theme. The JSX layout and CSS are illustrative only — create your own original visual design. Call `list_examples()` to see available example IDs.", {
|
|
2041
|
+
task: z
|
|
2042
|
+
.string()
|
|
2043
|
+
.describe("Task description or example ID (call `list_examples()` for the full list)"),
|
|
2044
|
+
}, async ({ task }) => {
|
|
1605
2045
|
const taskLower = task.toLowerCase();
|
|
1606
2046
|
// Try exact ID match first
|
|
1607
2047
|
let example = storefrontData.codeExamples.find((e) => e.id === taskLower);
|
|
@@ -1621,7 +2061,9 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
1621
2061
|
}
|
|
1622
2062
|
}
|
|
1623
2063
|
if (!example) {
|
|
1624
|
-
const available = storefrontData.codeExamples
|
|
2064
|
+
const available = storefrontData.codeExamples
|
|
2065
|
+
.map((e) => ` - \`${e.id}\` - ${e.title}`)
|
|
2066
|
+
.join("\n");
|
|
1625
2067
|
return {
|
|
1626
2068
|
content: [
|
|
1627
2069
|
{
|
|
@@ -1644,14 +2086,24 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
1644
2086
|
if (example.files && example.files.length > 0) {
|
|
1645
2087
|
for (const file of example.files) {
|
|
1646
2088
|
const ext = file.filename.split(".").pop() || "text";
|
|
1647
|
-
const lang = ext === "tsx" || ext === "ts"
|
|
2089
|
+
const lang = ext === "tsx" || ext === "ts"
|
|
2090
|
+
? "typescript"
|
|
2091
|
+
: ext === "css"
|
|
2092
|
+
? "css"
|
|
2093
|
+
: ext === "json"
|
|
2094
|
+
? "json"
|
|
2095
|
+
: "text";
|
|
1648
2096
|
// Add inline originality comments to CSS and TSX files
|
|
1649
2097
|
let content = file.content;
|
|
1650
2098
|
if (ext === "css") {
|
|
1651
|
-
content =
|
|
2099
|
+
content =
|
|
2100
|
+
"/* EXAMPLE STYLING — create your own original CSS with different class names and design */\n" +
|
|
2101
|
+
content;
|
|
1652
2102
|
}
|
|
1653
2103
|
else if (ext === "tsx") {
|
|
1654
|
-
content =
|
|
2104
|
+
content =
|
|
2105
|
+
"// EXAMPLE COMPONENT — use the API patterns but create your own JSX structure and layout\n" +
|
|
2106
|
+
content;
|
|
1655
2107
|
}
|
|
1656
2108
|
parts.push(`### ${file.filename}`, "", `\`\`\`${lang}`, content, "```", "");
|
|
1657
2109
|
}
|
|
@@ -1665,63 +2117,88 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
1665
2117
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1666
2118
|
});
|
|
1667
2119
|
// Tool: get_framework_guide
|
|
1668
|
-
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.", {
|
|
2120
|
+
server.tool("get_framework_guide", "Get a framework guide on a specific topic (e.g. 'ai-workflow', 'common-pitfalls', 'prop-types', 'css-scoping', 'form-handling'). Call `list_topics()` to see all available topic keys.", {
|
|
2121
|
+
topic: z
|
|
2122
|
+
.string()
|
|
2123
|
+
.describe("Topic key or keyword (call `list_topics()` for the full list)"),
|
|
2124
|
+
}, async ({ topic }) => {
|
|
1669
2125
|
const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
|
|
1670
2126
|
// Alias mapping for common alternative topic names
|
|
1671
2127
|
const topicAliases = {
|
|
1672
2128
|
"form-handling": "form-patterns",
|
|
1673
|
-
|
|
2129
|
+
forms: "form-patterns",
|
|
1674
2130
|
"data-fetching": "async-data-patterns",
|
|
1675
|
-
|
|
1676
|
-
|
|
2131
|
+
async: "async-data-patterns",
|
|
2132
|
+
loading: "async-data-patterns",
|
|
1677
2133
|
"sub-components": "sub-component-patterns",
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
2134
|
+
subcomponents: "sub-component-patterns",
|
|
2135
|
+
routing: "navigation-patterns",
|
|
2136
|
+
router: "navigation-patterns",
|
|
2137
|
+
observer: "component-structure",
|
|
2138
|
+
reactivity: "component-structure",
|
|
2139
|
+
pitfalls: "common-pitfalls",
|
|
2140
|
+
gotchas: "common-pitfalls",
|
|
2141
|
+
mistakes: "common-pitfalls",
|
|
2142
|
+
header: "header-footer-patterns",
|
|
2143
|
+
footer: "header-footer-patterns",
|
|
2144
|
+
blog: "blog-patterns",
|
|
2145
|
+
cart: "cart-patterns",
|
|
2146
|
+
account: "account-patterns",
|
|
1691
2147
|
"product-detail": "product-detail-patterns",
|
|
1692
2148
|
"product-list": "product-list-patterns",
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
2149
|
+
filtering: "product-list-patterns",
|
|
2150
|
+
reviews: "review-patterns",
|
|
2151
|
+
slider: "slider-overlay-patterns",
|
|
2152
|
+
overlay: "slider-overlay-patterns",
|
|
2153
|
+
modal: "slider-overlay-patterns",
|
|
2154
|
+
architecture: "real-world-architecture",
|
|
2155
|
+
theme: "real-world-architecture",
|
|
1700
2156
|
"global-styles": "global-css",
|
|
1701
|
-
|
|
2157
|
+
global: "global-css",
|
|
1702
2158
|
"css-variables": "global-css",
|
|
1703
2159
|
"custom-properties": "global-css",
|
|
1704
2160
|
};
|
|
1705
2161
|
const resolvedTopic = topicAliases[topicLower] || topicLower;
|
|
1706
2162
|
// Topics that involve MobX store reads get a reminder about root reactivity
|
|
1707
2163
|
const storeTopics = new Set([
|
|
1708
|
-
"product-detail-patterns",
|
|
1709
|
-
"
|
|
1710
|
-
"
|
|
1711
|
-
"
|
|
2164
|
+
"product-detail-patterns",
|
|
2165
|
+
"product-list-patterns",
|
|
2166
|
+
"cart-patterns",
|
|
2167
|
+
"account-patterns",
|
|
2168
|
+
"header-footer-patterns",
|
|
2169
|
+
"review-patterns",
|
|
2170
|
+
"blog-patterns",
|
|
2171
|
+
"form-handling",
|
|
2172
|
+
"async-data-patterns",
|
|
2173
|
+
"component-structure",
|
|
2174
|
+
"imports",
|
|
1712
2175
|
]);
|
|
1713
2176
|
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";
|
|
1714
2177
|
// Try exact key match (with alias resolution)
|
|
1715
2178
|
if (frameworkData.topics[resolvedTopic]) {
|
|
1716
2179
|
const t = frameworkData.topics[resolvedTopic];
|
|
1717
2180
|
const prefix = storeTopics.has(resolvedTopic) ? observerReminder : "";
|
|
1718
|
-
return {
|
|
2181
|
+
return {
|
|
2182
|
+
content: [
|
|
2183
|
+
{
|
|
2184
|
+
type: "text",
|
|
2185
|
+
text: `## ${t.title}\n\n${prefix}${t.content}`,
|
|
2186
|
+
},
|
|
2187
|
+
],
|
|
2188
|
+
};
|
|
1719
2189
|
}
|
|
1720
2190
|
// Try original topic key (without alias) in case it's a direct key
|
|
1721
2191
|
if (resolvedTopic !== topicLower && frameworkData.topics[topicLower]) {
|
|
1722
2192
|
const t = frameworkData.topics[topicLower];
|
|
1723
2193
|
const prefix = storeTopics.has(topicLower) ? observerReminder : "";
|
|
1724
|
-
return {
|
|
2194
|
+
return {
|
|
2195
|
+
content: [
|
|
2196
|
+
{
|
|
2197
|
+
type: "text",
|
|
2198
|
+
text: `## ${t.title}\n\n${prefix}${t.content}`,
|
|
2199
|
+
},
|
|
2200
|
+
],
|
|
2201
|
+
};
|
|
1725
2202
|
}
|
|
1726
2203
|
// Try keyword search
|
|
1727
2204
|
const matches = searchFrameworkTopics(topic);
|
|
@@ -1729,7 +2206,12 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
|
|
|
1729
2206
|
const best = matches[0];
|
|
1730
2207
|
const prefix = storeTopics.has(best.key) ? observerReminder : "";
|
|
1731
2208
|
return {
|
|
1732
|
-
content: [
|
|
2209
|
+
content: [
|
|
2210
|
+
{
|
|
2211
|
+
type: "text",
|
|
2212
|
+
text: `## ${best.topic.title}\n\n${prefix}${best.topic.content}`,
|
|
2213
|
+
},
|
|
2214
|
+
],
|
|
1733
2215
|
};
|
|
1734
2216
|
}
|
|
1735
2217
|
const available = Object.entries(frameworkData.topics)
|
|
@@ -1745,16 +2227,27 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
|
|
|
1745
2227
|
};
|
|
1746
2228
|
});
|
|
1747
2229
|
// Tool: get_type_definition
|
|
1748
|
-
server.tool("get_type_definition", "Get the full definition of a storefront type or enum by name (e.g. 'IkasProduct', 'IkasOrderStatus'). Shows all properties with types, extends, or enum values.", {
|
|
2230
|
+
server.tool("get_type_definition", "Get the full definition of a storefront type or enum by name (e.g. 'IkasProduct', 'IkasOrderStatus'). Shows all properties with types, extends, or enum values.", {
|
|
2231
|
+
name: z
|
|
2232
|
+
.string()
|
|
2233
|
+
.describe("Type or enum name (e.g. 'IkasProduct', 'IkasOrderStatus')"),
|
|
2234
|
+
}, async ({ name }) => {
|
|
1749
2235
|
if (!typesData) {
|
|
1750
2236
|
return {
|
|
1751
|
-
content: [
|
|
2237
|
+
content: [
|
|
2238
|
+
{
|
|
2239
|
+
type: "text",
|
|
2240
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2241
|
+
},
|
|
2242
|
+
],
|
|
1752
2243
|
};
|
|
1753
2244
|
}
|
|
1754
2245
|
const nameLower = name.toLowerCase();
|
|
1755
2246
|
const td = typesData.types.find((t) => t.name.toLowerCase() === nameLower);
|
|
1756
2247
|
if (td) {
|
|
1757
|
-
return {
|
|
2248
|
+
return {
|
|
2249
|
+
content: [{ type: "text", text: formatTypeDefinition(td) }],
|
|
2250
|
+
};
|
|
1758
2251
|
}
|
|
1759
2252
|
// Fuzzy match
|
|
1760
2253
|
const matches = typesData.types.filter((t) => t.name.toLowerCase().includes(nameLower));
|
|
@@ -1770,14 +2263,28 @@ server.tool("get_type_definition", "Get the full definition of a storefront type
|
|
|
1770
2263
|
};
|
|
1771
2264
|
}
|
|
1772
2265
|
return {
|
|
1773
|
-
content: [
|
|
2266
|
+
content: [
|
|
2267
|
+
{
|
|
2268
|
+
type: "text",
|
|
2269
|
+
text: `Type "${name}" not found. Use \`list_types()\` to see all available types.`,
|
|
2270
|
+
},
|
|
2271
|
+
],
|
|
1774
2272
|
};
|
|
1775
2273
|
});
|
|
1776
2274
|
// Tool: get_functions_for_type
|
|
1777
|
-
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.", {
|
|
2275
|
+
server.tool("get_functions_for_type", "Get full documentation for all utility functions that operate on a given storefront type. For example, get_functions_for_type('IkasImage') returns getSrc, getDefaultSrc, getThumbnailSrc, createMediaSrcset with full signatures, descriptions, and examples.", {
|
|
2276
|
+
typeName: z
|
|
2277
|
+
.string()
|
|
2278
|
+
.describe("Type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
|
|
2279
|
+
}, async ({ typeName }) => {
|
|
1778
2280
|
if (!typesData) {
|
|
1779
2281
|
return {
|
|
1780
|
-
content: [
|
|
2282
|
+
content: [
|
|
2283
|
+
{
|
|
2284
|
+
type: "text",
|
|
2285
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2286
|
+
},
|
|
2287
|
+
],
|
|
1781
2288
|
};
|
|
1782
2289
|
}
|
|
1783
2290
|
const nameLower = typeName.toLowerCase();
|
|
@@ -1797,7 +2304,12 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
|
|
|
1797
2304
|
};
|
|
1798
2305
|
}
|
|
1799
2306
|
return {
|
|
1800
|
-
content: [
|
|
2307
|
+
content: [
|
|
2308
|
+
{
|
|
2309
|
+
type: "text",
|
|
2310
|
+
text: `Type "${typeName}" not found. Use \`list_types()\` to see all available types.`,
|
|
2311
|
+
},
|
|
2312
|
+
],
|
|
1801
2313
|
};
|
|
1802
2314
|
}
|
|
1803
2315
|
if (!td.relatedFunctions || td.relatedFunctions.length === 0) {
|
|
@@ -1828,7 +2340,10 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
|
|
|
1828
2340
|
});
|
|
1829
2341
|
// Tool: get_model_guide
|
|
1830
2342
|
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.", {
|
|
1831
|
-
model: z
|
|
2343
|
+
model: z
|
|
2344
|
+
.string()
|
|
2345
|
+
.optional()
|
|
2346
|
+
.describe("Model type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
|
|
1832
2347
|
name: z.string().optional().describe("Alias for 'model'"),
|
|
1833
2348
|
mode: z
|
|
1834
2349
|
.enum(["summary", "full"])
|
|
@@ -1839,12 +2354,22 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
1839
2354
|
const model = modelParam || nameParam;
|
|
1840
2355
|
if (!model) {
|
|
1841
2356
|
return {
|
|
1842
|
-
content: [
|
|
2357
|
+
content: [
|
|
2358
|
+
{
|
|
2359
|
+
type: "text",
|
|
2360
|
+
text: "Please provide a model name (e.g. 'IkasProduct', 'IkasOrder'). Use the 'model' or 'name' parameter.",
|
|
2361
|
+
},
|
|
2362
|
+
],
|
|
1843
2363
|
};
|
|
1844
2364
|
}
|
|
1845
2365
|
if (!typesData) {
|
|
1846
2366
|
return {
|
|
1847
|
-
content: [
|
|
2367
|
+
content: [
|
|
2368
|
+
{
|
|
2369
|
+
type: "text",
|
|
2370
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2371
|
+
},
|
|
2372
|
+
],
|
|
1848
2373
|
};
|
|
1849
2374
|
}
|
|
1850
2375
|
const modelLower = model.toLowerCase();
|
|
@@ -1866,7 +2391,12 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
1866
2391
|
};
|
|
1867
2392
|
}
|
|
1868
2393
|
return {
|
|
1869
|
-
content: [
|
|
2394
|
+
content: [
|
|
2395
|
+
{
|
|
2396
|
+
type: "text",
|
|
2397
|
+
text: `Model "${model}" not found. Use \`list_types()\` to see all available types.`,
|
|
2398
|
+
},
|
|
2399
|
+
],
|
|
1870
2400
|
};
|
|
1871
2401
|
}
|
|
1872
2402
|
const parts = [`# Model Guide: ${td.name}\n`];
|
|
@@ -1974,13 +2504,23 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
1974
2504
|
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 }) => {
|
|
1975
2505
|
if (!typesData) {
|
|
1976
2506
|
return {
|
|
1977
|
-
content: [
|
|
2507
|
+
content: [
|
|
2508
|
+
{
|
|
2509
|
+
type: "text",
|
|
2510
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2511
|
+
},
|
|
2512
|
+
],
|
|
1978
2513
|
};
|
|
1979
2514
|
}
|
|
1980
2515
|
const results = searchTypes(query).slice(0, 15);
|
|
1981
2516
|
if (results.length === 0) {
|
|
1982
2517
|
return {
|
|
1983
|
-
content: [
|
|
2518
|
+
content: [
|
|
2519
|
+
{
|
|
2520
|
+
type: "text",
|
|
2521
|
+
text: `No types found matching "${query}". Use \`list_types()\` to see all available types.`,
|
|
2522
|
+
},
|
|
2523
|
+
],
|
|
1984
2524
|
};
|
|
1985
2525
|
}
|
|
1986
2526
|
const parts = [`## Type Search Results for "${query}"\n`];
|
|
@@ -2044,7 +2584,7 @@ server.tool("list_types", "List storefront types and enums. Use `domain` and/or
|
|
|
2044
2584
|
for (const [d, count] of sorted) {
|
|
2045
2585
|
lines.push(`- \`${d}\` (${count})`);
|
|
2046
2586
|
}
|
|
2047
|
-
lines.push("",
|
|
2587
|
+
lines.push("", 'Call `list_types(domain: "<name>")` to see summaries for a domain.');
|
|
2048
2588
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2049
2589
|
}
|
|
2050
2590
|
const domainLower = domain.toLowerCase();
|
|
@@ -2083,15 +2623,25 @@ server.tool("list_types", "List storefront types and enums. Use `domain` and/or
|
|
|
2083
2623
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
2084
2624
|
});
|
|
2085
2625
|
// Tool: get_prop_types
|
|
2086
|
-
server.tool("get_prop_types", "Get all available ikas.config.json prop types with descriptions, TypeScript types, and examples. Tip: Use `npx ikas-component config add-component --props '[...]'` to create a component with all props in one command, or `add-prop` to add props incrementally. NEVER manually edit types.ts — it is auto-generated by the CLI.", {}, async () => {
|
|
2626
|
+
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 () => {
|
|
2087
2627
|
const propTypesTopic = frameworkData.topics["prop-types"];
|
|
2088
2628
|
if (propTypesTopic) {
|
|
2089
2629
|
return {
|
|
2090
|
-
content: [
|
|
2630
|
+
content: [
|
|
2631
|
+
{
|
|
2632
|
+
type: "text",
|
|
2633
|
+
text: `## ${propTypesTopic.title}\n\n${propTypesTopic.content}`,
|
|
2634
|
+
},
|
|
2635
|
+
],
|
|
2091
2636
|
};
|
|
2092
2637
|
}
|
|
2093
2638
|
return {
|
|
2094
|
-
content: [
|
|
2639
|
+
content: [
|
|
2640
|
+
{
|
|
2641
|
+
type: "text",
|
|
2642
|
+
text: "Prop types documentation not available.",
|
|
2643
|
+
},
|
|
2644
|
+
],
|
|
2095
2645
|
};
|
|
2096
2646
|
});
|
|
2097
2647
|
// Tool: get_section_template
|
|
@@ -2141,11 +2691,19 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2141
2691
|
}
|
|
2142
2692
|
const normalizedType = normalizeName(sectionType);
|
|
2143
2693
|
if (!sectionTemplateNames.includes(normalizedType)) {
|
|
2694
|
+
const suggestions = suggestClosestNames(normalizedType, sectionTemplateNames);
|
|
2695
|
+
let suggestionText = "";
|
|
2696
|
+
if (suggestions.length === 1) {
|
|
2697
|
+
suggestionText = ` Did you mean "${suggestions[0]}"?`;
|
|
2698
|
+
}
|
|
2699
|
+
else if (suggestions.length > 1) {
|
|
2700
|
+
suggestionText = ` Did you mean one of: ${suggestions.map((s) => `"${s}"`).join(", ")}?`;
|
|
2701
|
+
}
|
|
2144
2702
|
return {
|
|
2145
2703
|
content: [
|
|
2146
2704
|
{
|
|
2147
2705
|
type: "text",
|
|
2148
|
-
text: `Unknown section type "${sectionType}"
|
|
2706
|
+
text: `Unknown section type "${sectionType}".${suggestionText} Call \`list_section_types()\` to see all ${sectionTemplateNames.length} valid types.`,
|
|
2149
2707
|
},
|
|
2150
2708
|
],
|
|
2151
2709
|
};
|
|
@@ -2180,7 +2738,8 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2180
2738
|
// the end of the response in the existing recipe-builder block.
|
|
2181
2739
|
{
|
|
2182
2740
|
const snippetStrForBanner = bundle.rootFiles["ikas-config-snippet.json"];
|
|
2183
|
-
if (snippetStrForBanner &&
|
|
2741
|
+
if (snippetStrForBanner &&
|
|
2742
|
+
/<id-of-[A-Za-z0-9_]+>/.test(snippetStrForBanner)) {
|
|
2184
2743
|
const childMatches = Array.from(snippetStrForBanner.matchAll(/<id-of-([A-Za-z0-9_]+)>/g));
|
|
2185
2744
|
const uniqueChildren = Array.from(new Set(childMatches.map((m) => m[1])));
|
|
2186
2745
|
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.`, "");
|
|
@@ -2304,7 +2863,10 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2304
2863
|
try {
|
|
2305
2864
|
const childSnippet = JSON.parse(fs.readFileSync(childSnippetPath, "utf-8"));
|
|
2306
2865
|
const childProps = (childSnippet.props || []).map((p) => {
|
|
2307
|
-
const out = {
|
|
2866
|
+
const out = {
|
|
2867
|
+
name: p.name,
|
|
2868
|
+
type: p.type,
|
|
2869
|
+
};
|
|
2308
2870
|
if (p.displayName)
|
|
2309
2871
|
out.displayName = p.displayName;
|
|
2310
2872
|
if (p.required)
|
|
@@ -2377,10 +2939,7 @@ server.tool("get_section_child", "Fetch one item's files from a section's childr
|
|
|
2377
2939
|
.string()
|
|
2378
2940
|
.optional()
|
|
2379
2941
|
.describe("The item name as listed in `get_section_template`'s response (Children/Components/Sub-components)"),
|
|
2380
|
-
child: z
|
|
2381
|
-
.string()
|
|
2382
|
-
.optional()
|
|
2383
|
-
.describe("Alias for `name`"),
|
|
2942
|
+
child: z.string().optional().describe("Alias for `name`"),
|
|
2384
2943
|
kind: z
|
|
2385
2944
|
.enum(["children", "components", "sub-components"])
|
|
2386
2945
|
.optional()
|
|
@@ -2543,7 +3102,11 @@ server.tool("list_section_types", "List all available `get_section_template` sec
|
|
|
2543
3102
|
});
|
|
2544
3103
|
// --- Migration tools ---
|
|
2545
3104
|
// Tool: analyze_old_theme
|
|
2546
|
-
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.", {
|
|
3105
|
+
server.tool("analyze_old_theme", "Analyze an old ikas storefront theme.json and produce a structured migration report. Shows all components, custom data definitions, prop type breakdown, and migration recommendations. Use this as the first step when converting an old theme.", {
|
|
3106
|
+
theme_json: z
|
|
3107
|
+
.string()
|
|
3108
|
+
.describe("The raw JSON content of the old theme.json file"),
|
|
3109
|
+
}, async ({ theme_json }) => {
|
|
2547
3110
|
try {
|
|
2548
3111
|
const parsed = JSON.parse(theme_json);
|
|
2549
3112
|
const analysis = analyzeOldTheme(parsed);
|
|
@@ -2551,102 +3114,162 @@ server.tool("analyze_old_theme", "Analyze an old ikas storefront theme.json and
|
|
|
2551
3114
|
}
|
|
2552
3115
|
catch (err) {
|
|
2553
3116
|
return {
|
|
2554
|
-
content: [
|
|
3117
|
+
content: [
|
|
3118
|
+
{
|
|
3119
|
+
type: "text",
|
|
3120
|
+
text: `Error parsing theme.json: ${err instanceof Error ? err.message : String(err)}. Make sure you're passing valid JSON.`,
|
|
3121
|
+
},
|
|
3122
|
+
],
|
|
2555
3123
|
};
|
|
2556
3124
|
}
|
|
2557
3125
|
});
|
|
2558
3126
|
// Tool: get_migration_guide
|
|
2559
3127
|
const migrationTopicAliases = {
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
3128
|
+
overview: "migration-overview",
|
|
3129
|
+
migrate: "migration-overview",
|
|
3130
|
+
custom: "custom-data-conversion",
|
|
2563
3131
|
"custom-data": "custom-data-conversion",
|
|
2564
|
-
|
|
3132
|
+
customdata: "custom-data-conversion",
|
|
2565
3133
|
"dynamic-list": "custom-data-conversion",
|
|
2566
3134
|
"component-list": "custom-data-conversion",
|
|
2567
|
-
|
|
2568
|
-
|
|
3135
|
+
slider: "prop-type-mapping",
|
|
3136
|
+
props: "prop-type-mapping",
|
|
2569
3137
|
"prop-mapping": "prop-type-mapping",
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
3138
|
+
types: "prop-type-mapping",
|
|
3139
|
+
react: "react-to-preact",
|
|
3140
|
+
preact: "react-to-preact",
|
|
3141
|
+
observer: "react-to-preact",
|
|
3142
|
+
libraries: "library-replacements",
|
|
3143
|
+
swiper: "library-replacements",
|
|
3144
|
+
headlessui: "library-replacements",
|
|
3145
|
+
tailwind: "library-replacements",
|
|
3146
|
+
tailwindcss: "library-replacements",
|
|
3147
|
+
recharts: "library-replacements",
|
|
3148
|
+
marquee: "library-replacements",
|
|
3149
|
+
imports: "storefront-import-mapping",
|
|
3150
|
+
storefront: "storefront-import-mapping",
|
|
2583
3151
|
"bp-storefront": "storefront-import-mapping",
|
|
2584
3152
|
"theme-json": "theme-json-anatomy",
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
3153
|
+
anatomy: "theme-json-anatomy",
|
|
3154
|
+
decompose: "component-decomposition-strategy",
|
|
3155
|
+
decomposition: "component-decomposition-strategy",
|
|
3156
|
+
strategy: "component-decomposition-strategy",
|
|
3157
|
+
project: "complete-project-generation",
|
|
3158
|
+
generate: "complete-project-generation",
|
|
3159
|
+
generation: "complete-project-generation",
|
|
3160
|
+
settings: "settings-conversion",
|
|
3161
|
+
colors: "settings-conversion",
|
|
3162
|
+
fonts: "settings-conversion",
|
|
3163
|
+
find: "finding-new-system-equivalents",
|
|
3164
|
+
search: "finding-new-system-equivalents",
|
|
3165
|
+
discover: "finding-new-system-equivalents",
|
|
3166
|
+
equivalent: "finding-new-system-equivalents",
|
|
3167
|
+
equivalents: "finding-new-system-equivalents",
|
|
3168
|
+
replacement: "finding-new-system-equivalents",
|
|
2601
3169
|
};
|
|
2602
3170
|
const migrationTopicKeys = migrationData
|
|
2603
3171
|
? Object.keys(migrationData.topics)
|
|
2604
3172
|
: [];
|
|
2605
|
-
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.`, {
|
|
3173
|
+
server.tool("get_migration_guide", `Get a migration guide for converting old ikas themes to the new code-component system. **Start with \`get_migration_guide("iterative-workflow")\` if you're new to this MCP** — it explains the MCP-vs-LLM responsibility split and the four phases.${migrationTopicKeys.length > 0 ? ` Available topics: ${migrationTopicKeys.join(", ")}. Also supports aliases like "custom", "slider", "react", "libraries", "imports", "settings".` : ""} Call with topic "list" to see all available topics.`, {
|
|
3174
|
+
topic: z
|
|
3175
|
+
.string()
|
|
3176
|
+
.describe("Migration topic key, alias, or 'list' to see all topics"),
|
|
3177
|
+
}, async ({ topic }) => {
|
|
2606
3178
|
if (!migrationData) {
|
|
2607
|
-
return {
|
|
3179
|
+
return {
|
|
3180
|
+
content: [
|
|
3181
|
+
{
|
|
3182
|
+
type: "text",
|
|
3183
|
+
text: "Migration data not available. Ensure data/migration.json exists.",
|
|
3184
|
+
},
|
|
3185
|
+
],
|
|
3186
|
+
};
|
|
2608
3187
|
}
|
|
2609
3188
|
if (topic.toLowerCase() === "list") {
|
|
2610
3189
|
const available = Object.entries(migrationData.topics)
|
|
2611
3190
|
.map(([key, t]) => `- \`${key}\` — ${t.title}: ${t.description}`)
|
|
2612
3191
|
.join("\n");
|
|
2613
|
-
return {
|
|
3192
|
+
return {
|
|
3193
|
+
content: [
|
|
3194
|
+
{
|
|
3195
|
+
type: "text",
|
|
3196
|
+
text: `## Available Migration Topics\n\n${available}`,
|
|
3197
|
+
},
|
|
3198
|
+
],
|
|
3199
|
+
};
|
|
2614
3200
|
}
|
|
2615
3201
|
const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
|
|
2616
3202
|
const resolvedTopic = migrationTopicAliases[topicLower] || topicLower;
|
|
2617
3203
|
if (migrationData.topics[resolvedTopic]) {
|
|
2618
3204
|
const t = migrationData.topics[resolvedTopic];
|
|
2619
|
-
return {
|
|
3205
|
+
return {
|
|
3206
|
+
content: [
|
|
3207
|
+
{ type: "text", text: `## ${t.title}\n\n${t.content}` },
|
|
3208
|
+
],
|
|
3209
|
+
};
|
|
2620
3210
|
}
|
|
2621
3211
|
// Try original key
|
|
2622
3212
|
if (resolvedTopic !== topicLower && migrationData.topics[topicLower]) {
|
|
2623
3213
|
const t = migrationData.topics[topicLower];
|
|
2624
|
-
return {
|
|
3214
|
+
return {
|
|
3215
|
+
content: [
|
|
3216
|
+
{ type: "text", text: `## ${t.title}\n\n${t.content}` },
|
|
3217
|
+
],
|
|
3218
|
+
};
|
|
2625
3219
|
}
|
|
2626
3220
|
// Keyword search
|
|
2627
3221
|
const matches = searchMigrationTopics(topic);
|
|
2628
3222
|
if (matches.length > 0) {
|
|
2629
3223
|
const best = matches[0];
|
|
2630
|
-
return {
|
|
3224
|
+
return {
|
|
3225
|
+
content: [
|
|
3226
|
+
{
|
|
3227
|
+
type: "text",
|
|
3228
|
+
text: `## ${best.topic.title}\n\n${best.topic.content}`,
|
|
3229
|
+
},
|
|
3230
|
+
],
|
|
3231
|
+
};
|
|
2631
3232
|
}
|
|
2632
3233
|
const available = Object.entries(migrationData.topics)
|
|
2633
3234
|
.map(([key, t]) => ` - \`${key}\` - ${t.title}`)
|
|
2634
3235
|
.join("\n");
|
|
2635
3236
|
return {
|
|
2636
|
-
content: [
|
|
3237
|
+
content: [
|
|
3238
|
+
{
|
|
3239
|
+
type: "text",
|
|
3240
|
+
text: `Migration topic "${topic}" not found. Available topics:\n${available}`,
|
|
3241
|
+
},
|
|
3242
|
+
],
|
|
2637
3243
|
};
|
|
2638
3244
|
});
|
|
2639
3245
|
// Tool: get_migration_example
|
|
2640
|
-
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.`, {
|
|
3246
|
+
server.tool("get_migration_example", `Get a concrete before/after migration example showing how to convert an old theme component to the new code-component system.${migrationExampleNames.length > 0 ? ` Available examples: ${migrationExampleNames.join(", ")}.` : ""} Call with example "list" to see all examples.`, {
|
|
3247
|
+
example: z.string().describe("Example name or 'list' to see all examples"),
|
|
3248
|
+
}, async ({ example }) => {
|
|
2641
3249
|
if (example.toLowerCase() === "list") {
|
|
2642
3250
|
if (migrationExampleNames.length === 0) {
|
|
2643
|
-
return {
|
|
3251
|
+
return {
|
|
3252
|
+
content: [
|
|
3253
|
+
{ type: "text", text: "No migration examples available." },
|
|
3254
|
+
],
|
|
3255
|
+
};
|
|
2644
3256
|
}
|
|
2645
|
-
const list = migrationExampleNames
|
|
3257
|
+
const list = migrationExampleNames
|
|
3258
|
+
.map((name) => {
|
|
2646
3259
|
const ex = loadMigrationExample(name);
|
|
2647
|
-
return ex
|
|
2648
|
-
|
|
2649
|
-
|
|
3260
|
+
return ex
|
|
3261
|
+
? `- \`${name}\` — ${ex.title}: ${ex.description}`
|
|
3262
|
+
: `- \`${name}\``;
|
|
3263
|
+
})
|
|
3264
|
+
.join("\n");
|
|
3265
|
+
return {
|
|
3266
|
+
content: [
|
|
3267
|
+
{
|
|
3268
|
+
type: "text",
|
|
3269
|
+
text: `## Available Migration Examples\n\n${list}`,
|
|
3270
|
+
},
|
|
3271
|
+
],
|
|
3272
|
+
};
|
|
2650
3273
|
}
|
|
2651
3274
|
const exampleLower = example.toLowerCase();
|
|
2652
3275
|
let exName = migrationExampleNames.find((n) => n === exampleLower);
|
|
@@ -2656,19 +3279,26 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
|
|
|
2656
3279
|
if (!exName) {
|
|
2657
3280
|
const available = migrationExampleNames.join(", ");
|
|
2658
3281
|
return {
|
|
2659
|
-
content: [
|
|
3282
|
+
content: [
|
|
3283
|
+
{
|
|
3284
|
+
type: "text",
|
|
3285
|
+
text: `Migration example "${example}" not found. Available: ${available}`,
|
|
3286
|
+
},
|
|
3287
|
+
],
|
|
2660
3288
|
};
|
|
2661
3289
|
}
|
|
2662
3290
|
const ex = loadMigrationExample(exName);
|
|
2663
3291
|
if (!ex) {
|
|
2664
|
-
return {
|
|
3292
|
+
return {
|
|
3293
|
+
content: [
|
|
3294
|
+
{
|
|
3295
|
+
type: "text",
|
|
3296
|
+
text: `Failed to load migration example "${exName}".`,
|
|
3297
|
+
},
|
|
3298
|
+
],
|
|
3299
|
+
};
|
|
2665
3300
|
}
|
|
2666
|
-
const parts = [
|
|
2667
|
-
`## ${ex.title}`,
|
|
2668
|
-
"",
|
|
2669
|
-
ex.description,
|
|
2670
|
-
"",
|
|
2671
|
-
];
|
|
3301
|
+
const parts = [`## ${ex.title}`, "", ex.description, ""];
|
|
2672
3302
|
for (const [filename, content] of Object.entries(ex.files)) {
|
|
2673
3303
|
const ext = filename.split(".").pop() || "text";
|
|
2674
3304
|
const lang = ext === "tsx" || ext === "ts"
|
|
@@ -2686,42 +3316,870 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
|
|
|
2686
3316
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
2687
3317
|
});
|
|
2688
3318
|
// Tool: plan_migration
|
|
2689
|
-
server.tool("plan_migration", "Generate
|
|
2690
|
-
theme_json: z
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
3319
|
+
server.tool("plan_migration", "Generate the **initial** migration plan and (when `project_root` is provided) write it to <project_root>/MIGRATION.md. **This is the only time the MCP writes that file.** From here, you own it: tick checkboxes as you finish work, log custom-data decisions, scan the old source for atomic components (Button, Input, Card, etc.) that theme.json doesn't see, and append them to MIGRATION.md yourself. theme.json is incomplete by design — the MCP can only describe what's listed there. Pass `theme_json_path` for large themes (raw `theme_json` string is supported for backward compat but fails on real-world sizes).", {
|
|
3320
|
+
theme_json: z
|
|
3321
|
+
.string()
|
|
3322
|
+
.optional()
|
|
3323
|
+
.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."),
|
|
3324
|
+
theme_json_path: z
|
|
3325
|
+
.string()
|
|
3326
|
+
.optional()
|
|
3327
|
+
.describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
|
|
3328
|
+
project_name: z
|
|
3329
|
+
.string()
|
|
3330
|
+
.optional()
|
|
3331
|
+
.describe("Target new project name, used to prefix migration-tracking IDs (default: 'my-theme')"),
|
|
3332
|
+
old_source_dir: z
|
|
3333
|
+
.string()
|
|
3334
|
+
.optional()
|
|
3335
|
+
.describe("Absolute path to the old project's src/ directory. When provided, the tool scans .tsx files to detect shared sub-components used across 3+ components. This scan is partial — atomic components used by only 1-2 sections will be missed and must be added by the LLM."),
|
|
3336
|
+
project_root: z
|
|
3337
|
+
.string()
|
|
3338
|
+
.optional()
|
|
3339
|
+
.describe("Absolute path to the new project root. When provided, the MCP writes MIGRATION.md to <project_root>/MIGRATION.md and returns a short summary instead of the full markdown body."),
|
|
3340
|
+
overwrite: z
|
|
3341
|
+
.boolean()
|
|
3342
|
+
.optional()
|
|
3343
|
+
.describe("If MIGRATION.md already exists at <project_root>/MIGRATION.md and is non-empty, refuse the write unless this is true. Default: false."),
|
|
3344
|
+
}, async ({ theme_json, theme_json_path, project_name, old_source_dir, project_root, overwrite, }) => {
|
|
2694
3345
|
try {
|
|
2695
|
-
const parsed =
|
|
3346
|
+
const parsed = resolveThemeJson(theme_json, theme_json_path);
|
|
2696
3347
|
const projectName = project_name || "my-theme";
|
|
2697
3348
|
const plan = generateMigrationPlan(parsed, projectName, old_source_dir);
|
|
2698
|
-
|
|
3349
|
+
if (!project_root) {
|
|
3350
|
+
return { content: [{ type: "text", text: plan }] };
|
|
3351
|
+
}
|
|
3352
|
+
// Write MIGRATION.md to project_root
|
|
3353
|
+
if (!path.isAbsolute(project_root)) {
|
|
3354
|
+
throw new Error(`project_root must be absolute: ${project_root}`);
|
|
3355
|
+
}
|
|
3356
|
+
if (!fs.existsSync(project_root)) {
|
|
3357
|
+
throw new Error(`project_root not found: ${project_root}`);
|
|
3358
|
+
}
|
|
3359
|
+
if (!fs.statSync(project_root).isDirectory()) {
|
|
3360
|
+
throw new Error(`project_root is not a directory: ${project_root}`);
|
|
3361
|
+
}
|
|
3362
|
+
const targetPath = path.join(project_root, "MIGRATION.md");
|
|
3363
|
+
if (fs.existsSync(targetPath)) {
|
|
3364
|
+
const existing = fs.readFileSync(targetPath, "utf-8").trim();
|
|
3365
|
+
if (existing.length > 0 && !overwrite) {
|
|
3366
|
+
return {
|
|
3367
|
+
content: [
|
|
3368
|
+
{
|
|
3369
|
+
type: "text",
|
|
3370
|
+
text: `Refusing to overwrite existing non-empty MIGRATION.md at ${targetPath}. ` +
|
|
3371
|
+
`Pass overwrite: true to replace it, or delete the file first. ` +
|
|
3372
|
+
`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.`,
|
|
3373
|
+
},
|
|
3374
|
+
],
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
writeFileAtomic(targetPath, plan);
|
|
3379
|
+
const components = parsed.components || [];
|
|
3380
|
+
const customData = parsed.customData || [];
|
|
3381
|
+
const cssVarCount = parsed.settings?.colors?.length || 0;
|
|
3382
|
+
const fontCount = parsed.settings?.fontFamily?.name ? 1 : 0;
|
|
3383
|
+
const customDataCount = customData.filter((cd) => cd.isRoot).length;
|
|
3384
|
+
const sectionCount = components.length;
|
|
3385
|
+
const summary = [
|
|
3386
|
+
`Wrote initial migration plan to ${targetPath}`,
|
|
3387
|
+
"",
|
|
3388
|
+
`**Summary:**`,
|
|
3389
|
+
`- Sections to migrate: ${sectionCount}`,
|
|
3390
|
+
`- CSS variables: ${cssVarCount}`,
|
|
3391
|
+
`- Fonts: ${fontCount}`,
|
|
3392
|
+
`- Custom data types (deferred decisions, not pre-migrated): ${customDataCount}`,
|
|
3393
|
+
"",
|
|
3394
|
+
`**Next steps for the LLM:**`,
|
|
3395
|
+
`1. Read \`${targetPath}\` start-to-finish. The "READ THIS FIRST" preamble explains your responsibilities.`,
|
|
3396
|
+
`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.`,
|
|
3397
|
+
`3. Start the Foundation work (CSS variables, fonts, shared sub-components).`,
|
|
3398
|
+
`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.`,
|
|
3399
|
+
`5. Log every custom-data decision in \`## Custom Data Decisions\`. Tick checkboxes as you finish.`,
|
|
3400
|
+
].join("\n");
|
|
3401
|
+
return { content: [{ type: "text", text: summary }] };
|
|
2699
3402
|
}
|
|
2700
3403
|
catch (err) {
|
|
2701
3404
|
return {
|
|
2702
|
-
content: [
|
|
3405
|
+
content: [
|
|
3406
|
+
{
|
|
3407
|
+
type: "text",
|
|
3408
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
3409
|
+
},
|
|
3410
|
+
],
|
|
2703
3411
|
};
|
|
2704
3412
|
}
|
|
2705
3413
|
});
|
|
2706
3414
|
// Tool: get_section_migration_plan
|
|
2707
|
-
server.tool("get_section_migration_plan", "
|
|
2708
|
-
theme_json: z
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
3415
|
+
server.tool("get_section_migration_plan", "Returns concrete CLI commands and prop conversions for one section. For each prop that references a customData type, you'll see a 'Decide: enum or component?' callout — log your decision in MIGRATION.md under `## Custom Data Decisions`. Pass `theme_json_path` for large themes.", {
|
|
3416
|
+
theme_json: z
|
|
3417
|
+
.string()
|
|
3418
|
+
.optional()
|
|
3419
|
+
.describe("Raw JSON content of the old theme.json. EITHER this OR theme_json_path is required (not both)."),
|
|
3420
|
+
theme_json_path: z
|
|
3421
|
+
.string()
|
|
3422
|
+
.optional()
|
|
3423
|
+
.describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
|
|
3424
|
+
section_name: z
|
|
3425
|
+
.string()
|
|
3426
|
+
.describe("Old component name (e.g. 'Navbar', 'ProductGrid') or dir name, OR the new section ID (e.g. 'my-theme-navbar')"),
|
|
3427
|
+
project_name: z
|
|
3428
|
+
.string()
|
|
3429
|
+
.optional()
|
|
3430
|
+
.describe("Target new project name (must match what was used in plan_migration). Default: 'my-theme'"),
|
|
3431
|
+
old_source_dir: z
|
|
3432
|
+
.string()
|
|
3433
|
+
.optional()
|
|
3434
|
+
.describe("Absolute path to old src/ directory (used to output exact source file paths to read)"),
|
|
3435
|
+
}, async ({ theme_json, theme_json_path, section_name, project_name, old_source_dir, }) => {
|
|
2713
3436
|
try {
|
|
2714
|
-
const parsed =
|
|
3437
|
+
const parsed = resolveThemeJson(theme_json, theme_json_path);
|
|
2715
3438
|
const projectName = project_name || "my-theme";
|
|
2716
3439
|
const plan = generateSectionMigrationPlan(parsed, section_name, projectName, old_source_dir);
|
|
2717
3440
|
return { content: [{ type: "text", text: plan }] };
|
|
2718
3441
|
}
|
|
2719
3442
|
catch (err) {
|
|
2720
3443
|
return {
|
|
2721
|
-
content: [
|
|
3444
|
+
content: [
|
|
3445
|
+
{
|
|
3446
|
+
type: "text",
|
|
3447
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
3448
|
+
},
|
|
3449
|
+
],
|
|
2722
3450
|
};
|
|
2723
3451
|
}
|
|
2724
3452
|
});
|
|
3453
|
+
// =============================================================================
|
|
3454
|
+
// Editor-action tools — drive the connected editor over the dev server's WS
|
|
3455
|
+
// (port 5201) by shelling out to `ikas-component <cmd>`. The CLI is the
|
|
3456
|
+
// canonical implementation; these tools are thin advertisements for the LLM.
|
|
3457
|
+
// =============================================================================
|
|
3458
|
+
function resolveIkasComponentBinary(projectRoot) {
|
|
3459
|
+
if (!path.isAbsolute(projectRoot)) {
|
|
3460
|
+
throw new Error(`project_root must be absolute: ${projectRoot}`);
|
|
3461
|
+
}
|
|
3462
|
+
if (!fs.existsSync(projectRoot) || !fs.statSync(projectRoot).isDirectory()) {
|
|
3463
|
+
throw new Error(`project_root is not a directory: ${projectRoot}`);
|
|
3464
|
+
}
|
|
3465
|
+
const binDir = path.join(projectRoot, "node_modules", ".bin");
|
|
3466
|
+
const candidates = os.platform() === "win32"
|
|
3467
|
+
? ["ikas-component.cmd", "ikas-component.exe", "ikas-component"]
|
|
3468
|
+
: ["ikas-component"];
|
|
3469
|
+
for (const name of candidates) {
|
|
3470
|
+
const full = path.join(binDir, name);
|
|
3471
|
+
if (fs.existsSync(full))
|
|
3472
|
+
return full;
|
|
3473
|
+
}
|
|
3474
|
+
throw new Error(`ikas-component CLI not found at ${binDir}. Run \`npm install\` (or \`pnpm install\`) in ${projectRoot} first.`);
|
|
3475
|
+
}
|
|
3476
|
+
async function runIkasComponentCli(projectRoot, args) {
|
|
3477
|
+
const bin = resolveIkasComponentBinary(projectRoot);
|
|
3478
|
+
return new Promise((resolve) => {
|
|
3479
|
+
execFile(bin, args,
|
|
3480
|
+
// Large editor responses (e.g. list_categories on a big store, or
|
|
3481
|
+
// add_sections_to_page echoing many sections) easily exceed Node's default
|
|
3482
|
+
// 1MB stdout buffer, which truncates output mid-JSON and yields a
|
|
3483
|
+
// "no parseable JSON" error. Give it generous headroom.
|
|
3484
|
+
{ cwd: projectRoot, windowsHide: true, maxBuffer: 64 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
3485
|
+
const exitCode = err && typeof err.code === "number"
|
|
3486
|
+
? err.code
|
|
3487
|
+
: err
|
|
3488
|
+
? 1
|
|
3489
|
+
: 0;
|
|
3490
|
+
resolve({
|
|
3491
|
+
stdout: stdout?.toString() ?? "",
|
|
3492
|
+
stderr: stderr?.toString() ?? "",
|
|
3493
|
+
exitCode,
|
|
3494
|
+
});
|
|
3495
|
+
});
|
|
3496
|
+
});
|
|
3497
|
+
}
|
|
3498
|
+
function parseCliJson(stdout) {
|
|
3499
|
+
const trimmed = stdout.trim();
|
|
3500
|
+
if (!trimmed)
|
|
3501
|
+
return null;
|
|
3502
|
+
// Fast path: the CLI prints exactly one JSON object on stdout.
|
|
3503
|
+
try {
|
|
3504
|
+
return JSON.parse(trimmed);
|
|
3505
|
+
}
|
|
3506
|
+
catch {
|
|
3507
|
+
// Fallback: if anything leaked onto stdout before/after the JSON, scan the
|
|
3508
|
+
// lines and return the last one that parses as JSON.
|
|
3509
|
+
const lines = trimmed
|
|
3510
|
+
.split("\n")
|
|
3511
|
+
.map((l) => l.trim())
|
|
3512
|
+
.filter(Boolean);
|
|
3513
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3514
|
+
try {
|
|
3515
|
+
return JSON.parse(lines[i]);
|
|
3516
|
+
}
|
|
3517
|
+
catch {
|
|
3518
|
+
// keep scanning
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
return null;
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
async function callEditorAction(projectRoot, args) {
|
|
3525
|
+
try {
|
|
3526
|
+
const { stdout, stderr, exitCode } = await runIkasComponentCli(projectRoot, args);
|
|
3527
|
+
const parsed = parseCliJson(stdout);
|
|
3528
|
+
if (parsed) {
|
|
3529
|
+
return {
|
|
3530
|
+
content: [
|
|
3531
|
+
{ type: "text", text: JSON.stringify(parsed, null, 2) },
|
|
3532
|
+
],
|
|
3533
|
+
};
|
|
3534
|
+
}
|
|
3535
|
+
return {
|
|
3536
|
+
content: [
|
|
3537
|
+
{
|
|
3538
|
+
type: "text",
|
|
3539
|
+
text: `CLI exited with code ${exitCode} and produced no parseable JSON.\n` +
|
|
3540
|
+
`stdout:\n${stdout || "(empty)"}\nstderr:\n${stderr || "(empty)"}`,
|
|
3541
|
+
},
|
|
3542
|
+
],
|
|
3543
|
+
};
|
|
3544
|
+
}
|
|
3545
|
+
catch (err) {
|
|
3546
|
+
return {
|
|
3547
|
+
content: [
|
|
3548
|
+
{
|
|
3549
|
+
type: "text",
|
|
3550
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
3551
|
+
},
|
|
3552
|
+
],
|
|
3553
|
+
};
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
//
|
|
3557
|
+
// Tool: list_editor_pages
|
|
3558
|
+
server.tool("list_editor_pages", "List pages in the connected editor's project. Returns id, name, pageType, slug for each page. Requires `ikas-component dev` to be running with the editor connected. Use the returned `id`s as the `pageId` argument to `add_section_to_page`.", {
|
|
3559
|
+
project_root: z
|
|
3560
|
+
.string()
|
|
3561
|
+
.describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
|
|
3562
|
+
port: z
|
|
3563
|
+
.number()
|
|
3564
|
+
.optional()
|
|
3565
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3566
|
+
}, async ({ project_root, port }) => {
|
|
3567
|
+
const args = ["list-pages", ...(port ? ["--port", String(port)] : [])];
|
|
3568
|
+
return callEditorAction(project_root, args);
|
|
3569
|
+
});
|
|
3570
|
+
// Tool: list_imported_sections
|
|
3571
|
+
server.tool("list_imported_sections", "List section-type code components already imported into the editor's project (theme.codeComponents, filtered to type=section). Use to confirm a component is ready to be placed on a page. The component must be built (`ikas-component build`/`dev`) and imported (`import_section`) before it can be added to a page. The returned `id` is the editor's id for the imported component — normally identical to the id in `ikas.config.json`, but it can differ if the dev component was deleted and re-scaffolded after a previous import. Always use the `id` from this tool, not from `ikas.config.json`, when calling `add_section_to_page`.", {
|
|
3572
|
+
project_root: z
|
|
3573
|
+
.string()
|
|
3574
|
+
.describe("Absolute path to the code-component project."),
|
|
3575
|
+
port: z
|
|
3576
|
+
.number()
|
|
3577
|
+
.optional()
|
|
3578
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3579
|
+
}, async ({ project_root, port }) => {
|
|
3580
|
+
const args = [
|
|
3581
|
+
"list-imported",
|
|
3582
|
+
"--sections-only",
|
|
3583
|
+
...(port ? ["--port", String(port)] : []),
|
|
3584
|
+
];
|
|
3585
|
+
return callEditorAction(project_root, args);
|
|
3586
|
+
});
|
|
3587
|
+
// Tool: import_section
|
|
3588
|
+
server.tool("import_section", "Import a built section-type code component into the editor's project. This copies the compiled JS/CSS/props into `theme.codeComponents` and auto-creates the wrapper section needed to place it on a page. Idempotent: re-running updates the existing entry in place. Required before `add_section_to_page`. The component must have been built (`ikas-component build`/`dev`).", {
|
|
3589
|
+
project_root: z
|
|
3590
|
+
.string()
|
|
3591
|
+
.describe("Absolute path to the code-component project."),
|
|
3592
|
+
component_id: z
|
|
3593
|
+
.string()
|
|
3594
|
+
.describe("Component id from `ikas.config.json` (strict — no name resolution)."),
|
|
3595
|
+
port: z
|
|
3596
|
+
.number()
|
|
3597
|
+
.optional()
|
|
3598
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3599
|
+
}, async ({ project_root, component_id, port }) => {
|
|
3600
|
+
const args = [
|
|
3601
|
+
"import",
|
|
3602
|
+
"--id",
|
|
3603
|
+
component_id,
|
|
3604
|
+
...(port ? ["--port", String(port)] : []),
|
|
3605
|
+
];
|
|
3606
|
+
return callEditorAction(project_root, args);
|
|
3607
|
+
});
|
|
3608
|
+
// Tool: add_section_to_page
|
|
3609
|
+
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).', {
|
|
3610
|
+
project_root: z
|
|
3611
|
+
.string()
|
|
3612
|
+
.describe("Absolute path to the code-component project."),
|
|
3613
|
+
component_id: z
|
|
3614
|
+
.string()
|
|
3615
|
+
.describe("Imported code component id from `list_imported_sections` — NOT the id in `ikas.config.json` (the two are usually the same but can diverge after a rescaffold)."),
|
|
3616
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3617
|
+
index: z
|
|
3618
|
+
.number()
|
|
3619
|
+
.int()
|
|
3620
|
+
.nonnegative()
|
|
3621
|
+
.optional()
|
|
3622
|
+
.describe("Zero-based insertion index in the page; appends when omitted."),
|
|
3623
|
+
port: z
|
|
3624
|
+
.number()
|
|
3625
|
+
.optional()
|
|
3626
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3627
|
+
}, async ({ project_root, component_id, page_id, index, port }) => {
|
|
3628
|
+
const args = [
|
|
3629
|
+
"add-to-page",
|
|
3630
|
+
"--component-id",
|
|
3631
|
+
component_id,
|
|
3632
|
+
"--page-id",
|
|
3633
|
+
page_id,
|
|
3634
|
+
...(typeof index === "number" ? ["--index", String(index)] : []),
|
|
3635
|
+
...(port ? ["--port", String(port)] : []),
|
|
3636
|
+
];
|
|
3637
|
+
return callEditorAction(project_root, args);
|
|
3638
|
+
});
|
|
3639
|
+
// Tool: add_sections_to_page
|
|
3640
|
+
server.tool("add_sections_to_page", "Place MANY sections on a page in a SINGLE call — and optionally set each section's prop values AT placement time, so you skip the separate fill step. The fastest way to build a page: one round-trip instead of one add (and one fill) per section. Pass `sections`, each `{ component_id, index?, updates?: [{ prop_name|prop_id, value }] }`; values use the same per-type shapes as update_section_prop / get_editor_workflow. Returns the new `elementId` for each placement (plus written prop results). Built-but-unimported section AND child code components are auto-imported. All update values are validated before anything is placed. Use `list_editor_pages` for page ids and `list_imported_sections` for component ids.", {
|
|
3641
|
+
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3642
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3643
|
+
sections: z
|
|
3644
|
+
.array(z.object({
|
|
3645
|
+
component_id: z.string().describe("Imported (or built) section-type code component id."),
|
|
3646
|
+
index: z
|
|
3647
|
+
.number()
|
|
3648
|
+
.int()
|
|
3649
|
+
.nonnegative()
|
|
3650
|
+
.optional()
|
|
3651
|
+
.describe("Zero-based insertion index; appends when omitted."),
|
|
3652
|
+
updates: z
|
|
3653
|
+
.array(z.object({
|
|
3654
|
+
prop_id: z.string().optional().describe("Blueprint prop id (provide this or prop_name)."),
|
|
3655
|
+
prop_name: z.string().optional().describe("Blueprint prop name (alternative to prop_id)."),
|
|
3656
|
+
value: z.any().describe("Prop value, same shape update_section_prop expects for that type."),
|
|
3657
|
+
}))
|
|
3658
|
+
.optional()
|
|
3659
|
+
.describe("Optional prop values to set on this section right after placing it."),
|
|
3660
|
+
}))
|
|
3661
|
+
.describe("Non-empty array of sections to place (in order), each with its componentId and optional prop updates."),
|
|
3662
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3663
|
+
}, async ({ project_root, page_id, sections, port }) => {
|
|
3664
|
+
const normalized = (sections || []).map(s => ({
|
|
3665
|
+
componentId: s.component_id,
|
|
3666
|
+
...(typeof s.index === "number" ? { index: s.index } : {}),
|
|
3667
|
+
...(s.updates
|
|
3668
|
+
? {
|
|
3669
|
+
updates: s.updates.map(u => ({
|
|
3670
|
+
...(u.prop_id ? { propId: u.prop_id } : {}),
|
|
3671
|
+
...(u.prop_name ? { propName: u.prop_name } : {}),
|
|
3672
|
+
value: u.value,
|
|
3673
|
+
})),
|
|
3674
|
+
}
|
|
3675
|
+
: {}),
|
|
3676
|
+
}));
|
|
3677
|
+
const args = [
|
|
3678
|
+
"add-sections-to-page",
|
|
3679
|
+
"--page-id",
|
|
3680
|
+
page_id,
|
|
3681
|
+
"--sections",
|
|
3682
|
+
JSON.stringify(normalized),
|
|
3683
|
+
...(port ? ["--port", String(port)] : []),
|
|
3684
|
+
];
|
|
3685
|
+
return callEditorAction(project_root, args);
|
|
3686
|
+
});
|
|
3687
|
+
// Tool: list_page_sections
|
|
3688
|
+
server.tool("list_page_sections", "List the sections placed on a page — a LEAN ROSTER that never truncates: per-placement `elementId` (the identity of THIS placement — there can be multiple of the same section), `sectionId`, `componentId`, `name`, `propCount`, and `setPropNames` (which props already have a value). It deliberately omits the prop SCHEMA and the prop VALUES (both can be large — Header alone can have ~80 props). To get a section's prop schema (types, ENUM options, allowed children) call `get_component_props(componentId)`; to read its current values call `get_section_values(element_id)`. Use the returned `elementId` with `update_section_prop` / `update_page_sections`. Use `list_editor_pages` for page ids.", {
|
|
3689
|
+
project_root: z
|
|
3690
|
+
.string()
|
|
3691
|
+
.describe("Absolute path to the code-component project."),
|
|
3692
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3693
|
+
port: z
|
|
3694
|
+
.number()
|
|
3695
|
+
.optional()
|
|
3696
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3697
|
+
}, async ({ project_root, page_id, port }) => {
|
|
3698
|
+
const args = [
|
|
3699
|
+
"list-page-sections",
|
|
3700
|
+
"--page-id",
|
|
3701
|
+
page_id,
|
|
3702
|
+
...(port ? ["--port", String(port)] : []),
|
|
3703
|
+
];
|
|
3704
|
+
return callEditorAction(project_root, args);
|
|
3705
|
+
});
|
|
3706
|
+
// Tool: get_component_props
|
|
3707
|
+
server.tool("get_component_props", "Get prop blueprints straight from the editor (the source of truth) for one or MANY components in a single call — for any section OR child code component ids. Returns `{ components: [...] }`, each prop carrying its `id`, `name`, `type`, `required`, `default`, and crucially: a ready-to-use `writeExample` (the EXACT value shape to pass to update_section_prop / update_page_sections — e.g. PRODUCT_LIST, BLOG_LIST, COMPONENT_LIST, LINK) plus a `writeNote` for variants, so you do NOT have to guess or trial-write the JSON; for ENUM props the `options` (valid `value`s); and for COMPONENT/COMPONENT_LIST props the `allowedComponentIds` (which children the slot permits — when empty, an `allowedComponentsNote` says any imported code component is allowed). Use this (its `writeExample`/`options`/`allowedComponentIds`) instead of guessing or reading ikas.config.json. Pass ALL the section/child ids you need at once (batch). Child component ids come from a parent prop's `allowedComponentIds` (or get_section_template).", {
|
|
3708
|
+
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3709
|
+
component_ids: z
|
|
3710
|
+
.array(z.string())
|
|
3711
|
+
.describe("Section/child code-component ids to resolve (batch). From list_imported_sections or a COMPONENT_LIST prop's allowedComponentIds."),
|
|
3712
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3713
|
+
}, async ({ project_root, component_ids, port }) => {
|
|
3714
|
+
const args = [
|
|
3715
|
+
"get-component-props",
|
|
3716
|
+
"--component-ids",
|
|
3717
|
+
(component_ids || []).join(","),
|
|
3718
|
+
...(port ? ["--port", String(port)] : []),
|
|
3719
|
+
];
|
|
3720
|
+
return callEditorAction(project_root, args);
|
|
3721
|
+
});
|
|
3722
|
+
// Tool: get_section_values
|
|
3723
|
+
server.tool("get_section_values", "Get the CURRENT prop values of one or MANY placed sections (each with its full `propValues` plus blueprint `props` — every prop carrying a `writeExample`/`writeNote` for the exact value shape, plus ENUM `options` and COMPONENT_LIST `allowedComponentIds`). `list_page_sections` is lean and omits values to avoid truncation; use this to read sections' values — needed for read-modify-write of a COMPONENT_LIST, or to inspect the exact stored shape of an existing value. Returns `{ sections: [...] }`. Pass all the elementIds you need at once (batch).", {
|
|
3724
|
+
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3725
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3726
|
+
element_ids: z
|
|
3727
|
+
.array(z.string())
|
|
3728
|
+
.describe("Placed-section elementIds to read (batch). From `list_page_sections`."),
|
|
3729
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3730
|
+
}, async ({ project_root, page_id, element_ids, port }) => {
|
|
3731
|
+
const args = [
|
|
3732
|
+
"get-section-values",
|
|
3733
|
+
"--page-id",
|
|
3734
|
+
page_id,
|
|
3735
|
+
"--element-ids",
|
|
3736
|
+
(element_ids || []).join(","),
|
|
3737
|
+
...(port ? ["--port", String(port)] : []),
|
|
3738
|
+
];
|
|
3739
|
+
return callEditorAction(project_root, args);
|
|
3740
|
+
});
|
|
3741
|
+
// Tool: update_section_prop
|
|
3742
|
+
server.tool("update_section_prop", 'Change a single prop value of a section placed on a page. This is the tool for FILLING/SETTING content (heading, text, image, link, etc.) on an existing section — it is pure data entry: it writes NO component code and needs NO rebuild. Do not author a new prop or component to set a value the section already supports; first check the section\'s existing `props` via `list_page_sections`. Target the specific placement by its `element_id` (from `list_page_sections`) and the prop by `prop_id` or `prop_name` (also from `list_page_sections`). `value` is the prop value object as stored by the editor — for scalar props it is wrapped as `{ "value": <scalar> }` (e.g. `{ "value": "Hello" }` for TEXT, `{ "value": true }` for BOOLEAN, `{ "value": 12 }` for NUMBER, `{ "value": "#FF0000" }` for COLOR, `{ "value": "<enum-key>" }` for ENUM). Richer object prop types use their own object shape and are NOT `{ "value": ... }`-wrapped (full catalog in `get_editor_workflow`): IMAGE = `{ "id": "<asset-id>", "altText"?: "<alt>", "isVideo"?: false }` (the `id` MUST reference an already-uploaded asset — call `upload_image` with a `file_path`/`image_url` to get one, or reuse an existing `id` from another section\'s `propValues`); IMAGE_LIST = `{ "images": [ <image>, ... ] }`; VIDEO = `{ "video": { "id": "<asset-id>" }, "thumbnailImage"?: { "id": "..." }, "autoplay"?: false, "controls"?: true, "loop"?: false, "muted"?: false }` (NOT a bare `{ "id": ... }`); SVG = `{ "value": "<svg markup>" }` (value-wrapped, like a scalar); SVG_LIST = `{ "svgs": [ "<svg>", ... ] }`; entity props PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY/RAFFLE = `{ "<entity>Id": "...", "usePageData"?: false }` (e.g. `{ "categoryId": "..." }`) — get real ids from `search_products` (PRODUCT), `list_categories`, `list_brands`, `list_blogs`, `list_blog_categories`; list/collection props are typed objects keyed by a `<entity>ListType` discriminator: PRODUCT_LIST = `{ "id": "<unique>", "productListType": "STATIC", "productIds": [ { "productId": "...", "variantId": "..." } ], "initialLimit"?: 12 }` — note productIds is an array of {productId, variantId} PAIRS (from search_products), not bare ids; productListType ∈ ALL|STATIC|DISCOUNTED|RECOMMENDED|CATEGORY|SEARCH|LAST_VIEWED|RELATED_PRODUCTS|VIEWED_TOGETHER|PURCHASED_TOGETHER, and for a dynamic type use e.g. `{ "id": "<unique>", "productListType": "CATEGORY", "category": "<categoryId>" }` (no productIds) or `"ALL"` (no ids). CATEGORY_LIST = `{ "categoryListType": "STATIC", "categoryIds": ["..."] }` or `{ "categoryListType": "ALL" }`. BRAND_LIST = `{ "brandListType": "STATIC", "brandIds": ["..."] }` or ALL. BLOG_LIST = `{ "blogListType": "STATIC", "blogIds": ["..."] }`, `{ "blogListType": "CATEGORY", "categoryId": "..." }`, or ALL. BLOG_CATEGORY_LIST = `{ "blogCategoryListType": "STATIC", "blogCategoryIds": ["..."] }` or ALL. (Get the entity ids from search_products / list_*; you can also read an existing value with `get_section_values` to copy its exact shape.) LINK = a single link object `{ "id": "<unique-short-id>", "linkType": "PAGE"|"EXTERNAL"|"FILE", "label": "...", "openInNewTab"?: false, "subLinks": [] }` plus the target for its type — for PAGE add `"pageId"` AND `"pageType"` (both from `list_editor_pages`, e.g. pageType "INDEX"; for dynamic page types like PRODUCT/CATEGORY/BLOG also add the target `"itemId"`), for EXTERNAL add `"externalLink": "https://..."`. Examples: PAGE → `{ "id": "k3p9x", "linkType": "PAGE", "label": "Home", "pageId": "<page-id>", "pageType": "INDEX", "openInNewTab": false, "subLinks": [] }`; EXTERNAL → `{ "id": "m7q2z", "linkType": "EXTERNAL", "label": "Docs", "externalLink": "https://example.com", "openInNewTab": true, "subLinks": [] }`. To link to an ENTITY (category/brand/blog/blog-category) do NOT guess a slug — get the entity id from `list_categories`/`list_brands`/`list_blogs`/`list_blog_categories` and the page id from `get_page_by_type("<TYPE>")`, then build `{ "id": "<unique>", "linkType": "PAGE", "label": "...", "pageId": <pageId>, "pageType": "<TYPE>", "itemId": <entityId>, "subLinks": [] }`. LIST_OF_LINK = `{ "links": [ <link>, ... ] }` (each item is a link object as above). PRODUCT = `{ "productId": "...", "variantId"?: "..." }`. This SAME unwrapped shape applies whether the prop sits at section level OR nested inside a COMPONENT_LIST entry\'s `propValues` — there is no section-vs-nested difference. IMPORTANT: pass `value` as the parsed JSON object/array itself, NEVER as a JSON string (a stringified value is double-encoded and stored as a useless string). Values are validated server-side (deeply, including nested COMPONENT_LIST children): wrong shapes AND wrong semantics are REJECTED with an explanatory error instead of being silently stored. This includes: a `{ "value": [...] }` wrapper for a COMPONENT_LIST; a `{ "value": ... }`-wrapped IMAGE; `props` instead of `propValues`; a missing/duplicate entry `id`; a child referenced by the wrong key (a code component MUST use `codeComponentId`, a theme component `componentId`) or by an id that does not exist (child ids are opaque — take them from `get_section_template` / `list_imported_sections`, never invent them); and any `propValues` key that is not a real prop of that child component (the error lists the valid prop names). Auto-import: a referenced child code component that has been BUILT but not yet imported into the theme is imported automatically before the value is set (the response lists any such ids under `autoImported`); only children that are not built at all fail with a "no imported code component" error — build them first (`ikas-component build`/`dev`). For any prop type you are unsure about, inspect the existing value with `get_section_values` and match its exact shape. The change is applied with undo support.\n\n' +
|
|
3743
|
+
'COMPONENT_LIST / COMPONENT props: the value is NOT wrapped in `{ "value": ... }`. A COMPONENT_LIST value is `{ "components": [ <entry>, ... ] }`; a single COMPONENT value is one `<entry>`. Each entry = `{ "id": "<unique-id>", "codeComponentId": "<child-id>" | "componentId": "<child-id>", "propValues": { "<childPropName>": <wrapped-value>, ... } }`. Rules: (1) `id` is a unique short alphanumeric string identifying THIS entry — generate a fresh one per new entry and NEVER reuse an existing entry\'s id. (2) Use `codeComponentId` for code components, `componentId` for built-in theme components. (3) `propValues` is keyed by the CHILD component\'s prop NAMES, each using the same scalar wrappers as above (nesting is recursive — a child may itself hold a COMPONENT_LIST). (4) `update_section_prop` REPLACES the whole prop value — there is NO partial/deep merge. ALWAYS send the COMPLETE, fully-nested value for the prop: every existing entry (with its id), every nested `propValues`, and every nested COMPONENT_LIST at every depth. To change anything (even one deeply-nested scalar, or to add/remove/reorder a child), READ-MODIFY-WRITE: take the current `{ "components": [...] }` from `get_section_values`, edit it in place (preserving all other entries, ids, and nested structures), then send the ENTIRE updated object back. Sending only the changed part — a single entry, or a child without its siblings — wipes everything you omitted. (5) You can only add child component types the prop permits — call `get_component_props(parentComponentId)` to read the COMPONENT_LIST prop\'s `allowedComponentIds`, then `get_component_props(childId)` to learn that child\'s props (names, types, ENUM `options`/valid values) instead of guessing or reading ikas.config.json. The allowed set is wired at build/config time per the `get_section_template` setup recipe (`config update-prop`), not here.\n\n' +
|
|
3744
|
+
'Example — a full COMPONENT_LIST `value` with two children, where the second child itself nests another COMPONENT_LIST (note: scalar leaves are `{ "value": ... }`-wrapped, COMPONENT_LIST values are `{ "components": [...] }` and are NOT wrapped, every entry has a unique `id`, and this is the COMPLETE value you would send even to change just one field): ' +
|
|
3745
|
+
`{ "components": [ { "id": "a1b2c", "codeComponentId": "7ojrigep-Eml9n5sN3i", "propValues": { "heading": { "value": "Featured" }, "visible": { "value": true } } }, { "id": "d3e4f", "codeComponentId": "x2plk9zq-Qw8rt", "propValues": { "title": { "value": "Sub group" }, "cards": { "components": [ { "id": "g5h6i", "codeComponentId": "card01ab-Zz12", "propValues": { "label": { "value": "Card 1" } } }, { "id": "j7k8l", "codeComponentId": "card01ab-Zz12", "propValues": { "label": { "value": "Card 2" } } } ] } } } ] }`, {
|
|
3746
|
+
project_root: z
|
|
3747
|
+
.string()
|
|
3748
|
+
.describe("Absolute path to the code-component project."),
|
|
3749
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3750
|
+
element_id: z
|
|
3751
|
+
.string()
|
|
3752
|
+
.describe("Placed-section elementId identifying THIS placement on the page (from `list_page_sections`)."),
|
|
3753
|
+
prop_id: z
|
|
3754
|
+
.string()
|
|
3755
|
+
.optional()
|
|
3756
|
+
.describe("Blueprint prop id to update (from `list_page_sections`). Provide this or `prop_name`."),
|
|
3757
|
+
prop_name: z
|
|
3758
|
+
.string()
|
|
3759
|
+
.optional()
|
|
3760
|
+
.describe("Blueprint prop name to update (alternative to `prop_id`)."),
|
|
3761
|
+
value: z
|
|
3762
|
+
.any()
|
|
3763
|
+
.describe('The prop value object to store, e.g. `{ "value": "Hello" }`. Match the shape of the existing value from `get_section_values` for non-scalar prop types.'),
|
|
3764
|
+
port: z
|
|
3765
|
+
.number()
|
|
3766
|
+
.optional()
|
|
3767
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3768
|
+
}, async ({ project_root, page_id, element_id, prop_id, prop_name, value, port }) => {
|
|
3769
|
+
const args = [
|
|
3770
|
+
"update-section-prop",
|
|
3771
|
+
"--page-id",
|
|
3772
|
+
page_id,
|
|
3773
|
+
"--element-id",
|
|
3774
|
+
element_id,
|
|
3775
|
+
...(prop_id ? ["--prop-id", prop_id] : []),
|
|
3776
|
+
...(prop_name ? ["--prop-name", prop_name] : []),
|
|
3777
|
+
"--value",
|
|
3778
|
+
// The CLI JSON.parses --value. If `value` is already a JSON string, pass
|
|
3779
|
+
// it through verbatim — re-stringifying it would double-encode it and the
|
|
3780
|
+
// editor would store a string instead of the object/array.
|
|
3781
|
+
typeof value === "string" ? value : JSON.stringify(value),
|
|
3782
|
+
...(port ? ["--port", String(port)] : []),
|
|
3783
|
+
];
|
|
3784
|
+
return callEditorAction(project_root, args);
|
|
3785
|
+
});
|
|
3786
|
+
// Tool: upload_image
|
|
3787
|
+
server.tool("upload_image", "Upload an image to the connected editor's project and get back its image `id`. Provide the image via `file_path` (a local path) OR `image_url`. Use the returned `id` as the `id` of an IMAGE prop value passed to `update_section_prop` (e.g. `{ \"id\": \"<returned-id>\", \"altText\": \"...\", \"isVideo\": false }`). This is the way to set an image prop — `update_section_prop` does not upload, it only references an existing image id. Supported types: png, jpg, jpeg, webp, gif; max 10MB. Requires the editor to be embedded in the ikas admin panel (the admin panel performs the actual upload; a standalone editor cannot upload and will error/time out).", {
|
|
3788
|
+
project_root: z
|
|
3789
|
+
.string()
|
|
3790
|
+
.describe("Absolute path to the code-component project."),
|
|
3791
|
+
file_path: z
|
|
3792
|
+
.string()
|
|
3793
|
+
.optional()
|
|
3794
|
+
.describe("Local image file path (.png, .jpg, .jpeg, .webp, .gif). Provide this or `image_url`."),
|
|
3795
|
+
image_url: z
|
|
3796
|
+
.string()
|
|
3797
|
+
.optional()
|
|
3798
|
+
.describe("Image URL to fetch and upload (alternative to `file_path`)."),
|
|
3799
|
+
alt_text: z.string().optional().describe("Alt text to store with the image."),
|
|
3800
|
+
port: z
|
|
3801
|
+
.number()
|
|
3802
|
+
.optional()
|
|
3803
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3804
|
+
}, async ({ project_root, file_path, image_url, alt_text, port }) => {
|
|
3805
|
+
const args = [
|
|
3806
|
+
"upload-image",
|
|
3807
|
+
...(file_path ? ["--file", file_path] : []),
|
|
3808
|
+
...(image_url ? ["--url", image_url] : []),
|
|
3809
|
+
...(alt_text ? ["--alt", alt_text] : []),
|
|
3810
|
+
...(port ? ["--port", String(port)] : []),
|
|
3811
|
+
];
|
|
3812
|
+
return callEditorAction(project_root, args);
|
|
3813
|
+
});
|
|
3814
|
+
// Tool: update_page_sections
|
|
3815
|
+
server.tool("update_page_sections", "Fill MANY placed sections of a page in a SINGLE call — the fastest way to populate a whole page (one round-trip for the entire page instead of one call per section/prop). Takes `sections`, each `{ element_id, updates: [{ prop_name|prop_id, value }] }`; values use the same per-type shapes as update_section_prop / get_editor_workflow. All-or-nothing across the whole page: every value is resolved and deeply validated (and built-but-unimported child code components auto-imported) before anything is written — one invalid value rejects the entire page update. Prefer this over many update_section_prop(s) calls.", {
|
|
3816
|
+
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3817
|
+
page_id: z.string().describe("Target page id (from list_editor_pages)."),
|
|
3818
|
+
sections: z
|
|
3819
|
+
.array(z.object({
|
|
3820
|
+
element_id: z.string().describe("Placed-section elementId (from list_page_sections)."),
|
|
3821
|
+
updates: z
|
|
3822
|
+
.array(z.object({
|
|
3823
|
+
prop_id: z.string().optional().describe("Blueprint prop id (provide this or prop_name)."),
|
|
3824
|
+
prop_name: z.string().optional().describe("Blueprint prop name (alternative to prop_id)."),
|
|
3825
|
+
value: z.any().describe("Prop value, same shape update_section_prop expects for that type."),
|
|
3826
|
+
}))
|
|
3827
|
+
.describe("Non-empty array of prop updates for this section."),
|
|
3828
|
+
}))
|
|
3829
|
+
.describe("Non-empty array of sections to fill, each with its elementId and prop updates."),
|
|
3830
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3831
|
+
}, async ({ project_root, page_id, sections, port }) => {
|
|
3832
|
+
const normalized = (sections || []).map(s => ({
|
|
3833
|
+
elementId: s.element_id,
|
|
3834
|
+
updates: (s.updates || []).map(u => ({
|
|
3835
|
+
...(u.prop_id ? { propId: u.prop_id } : {}),
|
|
3836
|
+
...(u.prop_name ? { propName: u.prop_name } : {}),
|
|
3837
|
+
value: u.value,
|
|
3838
|
+
})),
|
|
3839
|
+
}));
|
|
3840
|
+
const args = [
|
|
3841
|
+
"update-page-sections",
|
|
3842
|
+
"--page-id",
|
|
3843
|
+
page_id,
|
|
3844
|
+
"--sections",
|
|
3845
|
+
JSON.stringify(normalized),
|
|
3846
|
+
...(port ? ["--port", String(port)] : []),
|
|
3847
|
+
];
|
|
3848
|
+
return callEditorAction(project_root, args);
|
|
3849
|
+
});
|
|
3850
|
+
// Tool: upload_images
|
|
3851
|
+
server.tool("upload_images", "Upload MANY images in one call (batch upload_image) and get their ids back in order — the host uploads them all in a single round-trip. Pass `images`, each `{ file_path?, image_url?, alt_text? }`. Returns an array of { id, fileName, altText, isVideo } in the same order; use each id in an IMAGE prop value. Prefer this over multiple upload_image calls when a section/page needs several images. Supported: png/jpg/jpeg/webp/gif, max 10MB each. Requires the editor embedded in the ikas admin panel.", {
|
|
3852
|
+
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3853
|
+
images: z
|
|
3854
|
+
.array(z.object({
|
|
3855
|
+
file_path: z.string().optional().describe("Local image file path (provide this or image_url)."),
|
|
3856
|
+
image_url: z.string().optional().describe("Image URL to fetch (alternative to file_path)."),
|
|
3857
|
+
alt_text: z.string().optional().describe("Alt text for this image."),
|
|
3858
|
+
}))
|
|
3859
|
+
.describe("Non-empty array of images to upload."),
|
|
3860
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3861
|
+
}, async ({ project_root, images, port }) => {
|
|
3862
|
+
const manifest = (images || []).map(im => ({
|
|
3863
|
+
...(im.file_path ? { file: im.file_path } : {}),
|
|
3864
|
+
...(im.image_url ? { url: im.image_url } : {}),
|
|
3865
|
+
...(im.alt_text ? { alt: im.alt_text } : {}),
|
|
3866
|
+
}));
|
|
3867
|
+
const args = [
|
|
3868
|
+
"upload-images",
|
|
3869
|
+
"--manifest",
|
|
3870
|
+
JSON.stringify(manifest),
|
|
3871
|
+
...(port ? ["--port", String(port)] : []),
|
|
3872
|
+
];
|
|
3873
|
+
return callEditorAction(project_root, args);
|
|
3874
|
+
});
|
|
3875
|
+
// Tool: search_products
|
|
3876
|
+
server.tool("search_products", "Search the connected store's products to obtain a real productId (and first variantId) for a PRODUCT prop value. Returns products with productId, variantId, name, slug, variantCount, and `imageSrc` (the full-resolution CDN URL of the product's main image). To use that product image in an IMAGE prop, you CANNOT reuse a product image id directly (it is not a theme asset) — pass the `imageSrc` URL to `upload_image(image_url: <imageSrc>)` (or `upload_images`), then use the returned id in the IMAGE value. This gives real product imagery instead of external/placeholder URLs. Use the result in update_section_prop as `{ \"productId\": \"<productId>\", \"variantId\": \"<variantId>\" }`. Pass `query` for free-text search, or `product_ids` to resolve specific ids. Requires the editor embedded in the ikas admin panel (the admin panel runs the search; a standalone editor cannot).", {
|
|
3877
|
+
project_root: z
|
|
3878
|
+
.string()
|
|
3879
|
+
.describe("Absolute path to the code-component project."),
|
|
3880
|
+
query: z.string().optional().describe("Free-text product search (name, sku, barcode)."),
|
|
3881
|
+
product_ids: z
|
|
3882
|
+
.array(z.string())
|
|
3883
|
+
.optional()
|
|
3884
|
+
.describe("Specific product ids to resolve directly (alternative to `query`)."),
|
|
3885
|
+
per_page: z.number().optional().describe("Results per page."),
|
|
3886
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3887
|
+
}, async ({ project_root, query, product_ids, per_page, port }) => {
|
|
3888
|
+
const args = [
|
|
3889
|
+
"search-products",
|
|
3890
|
+
...(query ? ["--query", query] : []),
|
|
3891
|
+
...(product_ids && product_ids.length ? ["--ids", product_ids.join(",")] : []),
|
|
3892
|
+
...(typeof per_page === "number" ? ["--per-page", String(per_page)] : []),
|
|
3893
|
+
...(port ? ["--port", String(port)] : []),
|
|
3894
|
+
];
|
|
3895
|
+
return callEditorAction(project_root, args);
|
|
3896
|
+
});
|
|
3897
|
+
// Tool: list_categories
|
|
3898
|
+
server.tool("list_categories", "List the store's product categories and their ids, to fill a CATEGORY prop value `{ \"categoryId\": \"<id>\" }`. Returns categoryId, name, slug, parentId for each. Returns all categories (no query). To link to a category from a LINK prop, also call `get_page_by_type(\"CATEGORY\")` for the page id, then build `{ \"linkType\": \"PAGE\", \"pageId\": <pageId>, \"pageType\": \"CATEGORY\", \"itemId\": <categoryId>, \"id\": <unique>, \"label\": \"...\" }`. Requires the editor embedded in the ikas admin panel.", {
|
|
3899
|
+
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3900
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3901
|
+
}, async ({ project_root, port }) => {
|
|
3902
|
+
const args = ["list-entities", "--kind", "category", ...(port ? ["--port", String(port)] : [])];
|
|
3903
|
+
return callEditorAction(project_root, args);
|
|
3904
|
+
});
|
|
3905
|
+
// Tool: list_brands
|
|
3906
|
+
server.tool("list_brands", "List the store's product brands and their ids, to fill a BRAND prop value `{ \"brandId\": \"<id>\" }`. Returns brandId, name, slug for each. Returns all brands (no query). To link to a brand from a LINK prop, also call `get_page_by_type(\"BRAND\")` for the page id, then build `{ \"linkType\": \"PAGE\", \"pageId\": <pageId>, \"pageType\": \"BRAND\", \"itemId\": <brandId>, \"id\": <unique>, \"label\": \"...\" }`. Requires the editor embedded in the ikas admin panel.", {
|
|
3907
|
+
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3908
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3909
|
+
}, async ({ project_root, port }) => {
|
|
3910
|
+
const args = ["list-entities", "--kind", "brand", ...(port ? ["--port", String(port)] : [])];
|
|
3911
|
+
return callEditorAction(project_root, args);
|
|
3912
|
+
});
|
|
3913
|
+
// Tool: list_blogs
|
|
3914
|
+
server.tool("list_blogs", "List the store's blogs and their ids, to fill a BLOG prop value `{ \"blogId\": \"<id>\" }`. Returns blogId, name, slug for each. Accepts an optional `query` for free-text search. To link to a blog from a LINK prop, also call `get_page_by_type(\"BLOG\")` for the page id, then build `{ \"linkType\": \"PAGE\", \"pageId\": <pageId>, \"pageType\": \"BLOG\", \"itemId\": <blogId>, \"id\": <unique>, \"label\": \"...\" }`. Requires the editor embedded in the ikas admin panel.", {
|
|
3915
|
+
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3916
|
+
query: z.string().optional().describe("Free-text blog search."),
|
|
3917
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3918
|
+
}, async ({ project_root, query, port }) => {
|
|
3919
|
+
const args = [
|
|
3920
|
+
"list-entities",
|
|
3921
|
+
"--kind",
|
|
3922
|
+
"blog",
|
|
3923
|
+
...(query ? ["--query", query] : []),
|
|
3924
|
+
...(port ? ["--port", String(port)] : []),
|
|
3925
|
+
];
|
|
3926
|
+
return callEditorAction(project_root, args);
|
|
3927
|
+
});
|
|
3928
|
+
// Tool: list_blog_categories
|
|
3929
|
+
server.tool("list_blog_categories", "List the store's blog categories and their ids, to fill a BLOG_CATEGORY prop value `{ \"blogCategoryId\": \"<id>\" }`. Returns blogCategoryId, name, slug for each. Accepts an optional `query` for free-text search. To link to a blog category from a LINK prop, also call `get_page_by_type(\"BLOG_CATEGORY\")` for the page id, then build `{ \"linkType\": \"PAGE\", \"pageId\": <pageId>, \"pageType\": \"BLOG_CATEGORY\", \"itemId\": <blogCategoryId>, \"id\": <unique>, \"label\": \"...\" }`. Requires the editor embedded in the ikas admin panel.", {
|
|
3930
|
+
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3931
|
+
query: z.string().optional().describe("Free-text blog-category search."),
|
|
3932
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3933
|
+
}, async ({ project_root, query, port }) => {
|
|
3934
|
+
const args = [
|
|
3935
|
+
"list-entities",
|
|
3936
|
+
"--kind",
|
|
3937
|
+
"blog-category",
|
|
3938
|
+
...(query ? ["--query", query] : []),
|
|
3939
|
+
...(port ? ["--port", String(port)] : []),
|
|
3940
|
+
];
|
|
3941
|
+
return callEditorAction(project_root, args);
|
|
3942
|
+
});
|
|
3943
|
+
// Tool: get_page_by_type
|
|
3944
|
+
server.tool("get_page_by_type", "Resolve a theme page's id from its pageType (CATEGORY, PRODUCT, BRAND, BLOG, BLOG_CATEGORY, INDEX, …). Use this to get the `pageId` needed to build a PAGE link to an entity: pair it with the entity id from list_categories/list_brands/list_blogs/list_blog_categories — `{ linkType:\"PAGE\", pageId:<this pageId>, pageType:<type>, itemId:<entityId>, id:<unique>, label:... }`. Returns `pageId: null` (with a note) if no page of that type exists yet — in that case call `create_page(page_type)` to add it, then use the returned pageId (do not guess a slug/EXTERNAL URL).", {
|
|
3945
|
+
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3946
|
+
page_type: z
|
|
3947
|
+
.string()
|
|
3948
|
+
.describe("Page type to resolve, e.g. CATEGORY, PRODUCT, BRAND, BLOG, BLOG_CATEGORY, INDEX."),
|
|
3949
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3950
|
+
}, async ({ project_root, page_type, port }) => {
|
|
3951
|
+
const args = [
|
|
3952
|
+
"get-page-by-type",
|
|
3953
|
+
"--page-type",
|
|
3954
|
+
page_type,
|
|
3955
|
+
...(port ? ["--port", String(port)] : []),
|
|
3956
|
+
];
|
|
3957
|
+
return callEditorAction(project_root, args);
|
|
3958
|
+
});
|
|
3959
|
+
// Tool: create_page
|
|
3960
|
+
server.tool("create_page", "Create a theme page of a given pageType in the connected editor. Use this when get_page_by_type returns `pageId: null` for a dynamic page (CATEGORY/PRODUCT/BRAND/BLOG/BLOG_CATEGORY) you need to link to: create the page, then build a PAGE link `{ linkType:\"PAGE\", pageId:<this pageId>, pageType:<type>, itemId:<entityId>, id:<unique>, label:... }` — do NOT guess a slug/EXTERNAL URL. Non-CUSTOM page types are unique per theme: if one already exists it is returned unchanged (`alreadyExisted: true`). CUSTOM pages require `name` and a unique `slug`. Account sub-pages (ADDRESSES/ORDERS/ORDER_DETAIL/FAVORITE_PRODUCTS/RAFFLE_ACCOUNT) need an ACCOUNT page first. Returns `{ pageType, pageId, name, slug }`. Requires `ikas-component dev` running with the editor connected.", {
|
|
3961
|
+
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3962
|
+
page_type: z
|
|
3963
|
+
.string()
|
|
3964
|
+
.describe("Page type to create, e.g. CATEGORY, PRODUCT, BRAND, BLOG, BLOG_CATEGORY, INDEX, CUSTOM."),
|
|
3965
|
+
name: z.string().optional().describe("Page name. Required for CUSTOM pages; ignored otherwise."),
|
|
3966
|
+
slug: z.string().optional().describe("Page slug. Required (and unique) for CUSTOM pages; ignored otherwise."),
|
|
3967
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3968
|
+
}, async ({ project_root, page_type, name, slug, port }) => {
|
|
3969
|
+
const args = [
|
|
3970
|
+
"create-page",
|
|
3971
|
+
"--page-type",
|
|
3972
|
+
page_type,
|
|
3973
|
+
...(name ? ["--name", name] : []),
|
|
3974
|
+
...(slug ? ["--slug", slug] : []),
|
|
3975
|
+
...(port ? ["--port", String(port)] : []),
|
|
3976
|
+
];
|
|
3977
|
+
return callEditorAction(project_root, args);
|
|
3978
|
+
});
|
|
3979
|
+
// Tool: publish_theme
|
|
3980
|
+
server.tool("publish_theme", "Publish the current theme LIVE — the same action as the editor's Publish button (uploads the whole project to the storefront). This is REAL and customer-facing: the published site changes. SAFETY: it publishes nothing unless `confirm: true` is passed; if you omit it you get a dry-run with the `previewUrl` and a warning describing the impact. The MAIN/production theme additionally requires `confirm_production: true` (its URL is the live customer-facing domain). Use this when the user explicitly asks to publish/go live — do NOT publish on your own initiative. On success returns `{ published: true, previewUrl, isMainTheme }`; open `previewUrl` to review the result. Requires `ikas-component dev` running with the editor connected.", {
|
|
3981
|
+
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
3982
|
+
confirm: z
|
|
3983
|
+
.boolean()
|
|
3984
|
+
.optional()
|
|
3985
|
+
.describe("Set true to actually publish. Omitted/false = dry-run (returns previewUrl + warning, publishes nothing)."),
|
|
3986
|
+
confirm_production: z
|
|
3987
|
+
.boolean()
|
|
3988
|
+
.optional()
|
|
3989
|
+
.describe("Required IN ADDITION to confirm when the target is the MAIN/production theme (live customer-facing site)."),
|
|
3990
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
3991
|
+
}, async ({ project_root, confirm, confirm_production, port }) => {
|
|
3992
|
+
const args = [
|
|
3993
|
+
"publish-theme",
|
|
3994
|
+
...(confirm ? ["--confirm"] : []),
|
|
3995
|
+
...(confirm_production ? ["--confirm-production"] : []),
|
|
3996
|
+
...(port ? ["--port", String(port)] : []),
|
|
3997
|
+
];
|
|
3998
|
+
return callEditorAction(project_root, args);
|
|
3999
|
+
});
|
|
4000
|
+
// Tool: get_editor_workflow
|
|
4001
|
+
const EDITOR_WORKFLOW_GUIDE = [
|
|
4002
|
+
"# Editor workflow: placing sections and filling their content on a page",
|
|
4003
|
+
"",
|
|
4004
|
+
"Use this when the user wants to add a section to a page and/or fill in its content (heading, text, image, link, slides, etc.) in the live editor. These are LIVE-EDITOR actions and require `ikas-component dev` running with the editor connected; image upload additionally requires the editor to be embedded in the ikas admin panel (the admin panel performs the upload — a standalone editor cannot upload).",
|
|
4005
|
+
"",
|
|
4006
|
+
"## Two distinct jobs — do not confuse them",
|
|
4007
|
+
"- (A) DEFINE props / author component code — changing WHICH props a component has. Edits component source/config (config add-prop/add-component, index.tsx) and needs a rebuild. Do this ONLY when a needed prop does not exist yet, or the user explicitly asks to build/modify the component.",
|
|
4008
|
+
"- (B) SET prop VALUES / fill content — giving values to props that ALREADY exist on a placed section. Pure data entry via list_page_sections + update_section_prop (+ upload_image). No code, no rebuild.",
|
|
4009
|
+
"When the user says 'fill this section', 'set the title/text/image/link', 'populate', 'change the content' → that is JOB (B). Do NOT write a component or add a prop to fill a value the section already supports.",
|
|
4010
|
+
"",
|
|
4011
|
+
"## Step-by-step (JOB B — fill content)",
|
|
4012
|
+
"1. list_editor_pages → choose the target page_id.",
|
|
4013
|
+
"2. list_page_sections(page_id) → for each placed section: its per-placement elementId, blueprint props (id, name, type), and setPropNames (which props already have a value). It is LEAN — no prop VALUES, so it never truncates. To read a section's current values (e.g. to read-modify-write a COMPONENT_LIST), call get_section_values(page_id, element_id). (If the section is not on the page yet, see 'Placing a section' below.)",
|
|
4014
|
+
"3. To set props (fewest round-trips = fastest): update_page_sections(page_id, sections:[{element_id, updates:[{prop_name,value}]}]) fills the WHOLE page (or one section, or just one prop — it scales to all cases) in ONE call (best). update_section_prop sets a single prop, for quick one-offs. Pass each value as a parsed JSON object/array, NEVER a JSON string.",
|
|
4015
|
+
"4. For image props: upload_images(images:[{file_path|image_url, alt_text}]) uploads many in ONE call (preferred); upload_image for a single one. Use the returned id(s) in IMAGE values. Do uploads BEFORE the update call so you have the ids.",
|
|
4016
|
+
"Speed tip: each tool call is a round-trip — gather first (one list_page_sections), batch your uploads (upload_images) and your writes (update_page_sections), rather than calling per-prop/per-image.",
|
|
4017
|
+
"",
|
|
4018
|
+
"## Building a whole page fast (placing + filling)",
|
|
4019
|
+
"- add_sections_to_page(page_id, sections:[{component_id, index?, updates:[{prop_name,value}]}]) places MANY sections AND sets their props in ONE call, returning each new elementId. This is the fastest path — prefer it over add_section_to_page per section followed by separate fills. Built-but-unimported section/child components are auto-imported.",
|
|
4020
|
+
"- For a single section: list_imported_sections (confirm imported; else import_section, after building) → add_section_to_page(component_id, page_id, index?) → then list_page_sections to get its elementId.",
|
|
4021
|
+
"",
|
|
4022
|
+
"## Value shapes by prop type (what to pass as `value`)",
|
|
4023
|
+
"- Scalars are WRAPPED: TEXT/RICH_TEXT { \"value\": \"...\" }, BOOLEAN { \"value\": true }, NUMBER { \"value\": 12 }, COLOR { \"value\": \"#FF0000\" }, ENUM { \"value\": \"<key>\" }, DATE { \"value\": ... }.",
|
|
4024
|
+
"- SVG is value-wrapped: { \"value\": \"<svg markup>\" }. NUMBER_RANGE: { \"value\": <number>, \"unit\": \"px\"|null }.",
|
|
4025
|
+
"- Object props are NOT wrapped (no { \"value\": ... }):",
|
|
4026
|
+
" - IMAGE: { \"id\": \"<asset-id>\", \"altText\"?: \"...\", \"isVideo\"?: false } — get the id from upload_image. Same shape at section level AND nested in a component list. IMAGE_LIST: { \"images\": [ <image>, ... ] }.",
|
|
4027
|
+
" - VIDEO: { \"video\": { \"id\": \"<asset-id>\" }, \"thumbnailImage\"?: { \"id\": \"...\" }, \"autoplay\"?: false, \"controls\"?: true, \"loop\"?: false, \"muted\"?: false } — NOT a bare { \"id\": ... }. SVG_LIST: { \"svgs\": [ \"<svg>\", ... ] }.",
|
|
4028
|
+
" - LINK: { \"id\": \"<unique>\", \"linkType\": \"PAGE\"|\"EXTERNAL\"|\"FILE\", \"label\": \"...\", \"subLinks\": [] } plus target — for a static page: pageId + pageType (from list_editor_pages); for EXTERNAL: externalLink. To link to a CATEGORY/BRAND/BLOG/BLOG_CATEGORY entity, do NOT guess a slug — get the entity id from list_categories/list_brands/list_blogs/list_blog_categories and the page id from get_page_by_type('<TYPE>'), then build { id, linkType:'PAGE', label, pageId:<pageId>, pageType:'<TYPE>', itemId:<entityId>, subLinks:[] }. LIST_OF_LINK = { \"links\": [ <link>, ... ] }.",
|
|
4029
|
+
" - Entity props PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY/RAFFLE: { \"<entity>Id\": \"...\", \"usePageData\"?: false } — e.g. { \"productId\": \"...\", \"variantId\"?: \"...\" }, { \"categoryId\": \"...\" }. Get real ids (never invent them) from: search_products (PRODUCT), list_categories (CATEGORY), list_brands (BRAND), list_blogs (BLOG), list_blog_categories (BLOG_CATEGORY). (RAFFLE has no lookup tool yet — read an existing value or ask the user.)",
|
|
4030
|
+
" - List/collection props (PRODUCT_LIST/CATEGORY_LIST/BRAND_LIST/BLOG_LIST/…): complex typed objects (a <entity>ListType plus initialSort/initialLimit and static ids). Do NOT build from scratch — read the current value with get_section_values and edit it.",
|
|
4031
|
+
" - COMPONENT_LIST: { \"components\": [ { \"id\": \"<unique>\", \"codeComponentId\": \"<child-id>\", \"propValues\": { \"<childProp>\": <value>, ... } }, ... ] }. A single COMPONENT prop is one such entry. childProp values use these same per-type shapes, recursively.",
|
|
4032
|
+
"",
|
|
4033
|
+
"## COMPONENT_LIST rules (the part most often gotten wrong)",
|
|
4034
|
+
"- READ-MODIFY-WRITE: update_section_prop REPLACES the whole prop value. To add/edit/remove one child, take the CURRENT { components: [...] } from get_section_values, edit it, and resend the ENTIRE array (with all existing entries and their ids). Sending only your new entry wipes the rest.",
|
|
4035
|
+
"- Each entry needs a UNIQUE id (a short string you generate; it does not need to match anything). Do not reuse another entry's id.",
|
|
4036
|
+
"- Reference a code component with \"codeComponentId\" and a built-in theme component with \"componentId\" — not the other way around. Use \"propValues\" (not \"props\").",
|
|
4037
|
+
"- Child ids are opaque — take them from get_section_template / list_imported_sections; never invent them.",
|
|
4038
|
+
"- Auto-import: a referenced child that is BUILT but not yet imported is imported automatically (returned under autoImported). A child that is not built at all errors — build it first.",
|
|
4039
|
+
"",
|
|
4040
|
+
"## Reading prop schemas (avoid guessing)",
|
|
4041
|
+
"- get_component_props(componentId) → a component's props with types, required, default, ENUM `options` (the valid values to set), and COMPONENT_LIST `allowedComponentIds` (valid children). Works for any section OR child id. Use it BEFORE filling — to know prop names, enum values, and which children a slot allows — instead of guessing or reading ikas.config.json.",
|
|
4042
|
+
"- For a COMPONENT_LIST: get_component_props(parent) → its allowedComponentIds → get_component_props(child) → the child's props/enums. Then build the components array.",
|
|
4043
|
+
"- get_section_values(element_id) → one placed section's current values (read-modify-write of a COMPONENT_LIST, or to copy an existing value's exact shape).",
|
|
4044
|
+
"- Real entity ids/images: search_products returns productId + variantId + imageSrc (the product image URL). To use that image in an IMAGE prop, you can't reuse a product image id — upload it: upload_image(image_url: imageSrc) → use the returned id. list_categories/list_brands/list_blogs/list_blog_categories return the entity ids; to LINK to one, also call get_page_by_type('<TYPE>') for the page id and build a PAGE link { linkType:'PAGE', pageId, pageType, itemId:<entityId>, id, label }. IMPORTANT: if get_page_by_type returns pageId:null, that dynamic page does NOT exist in the theme yet — do NOT guess a slug/EXTERNAL URL. Call create_page('<TYPE>') to add it (non-CUSTOM types are unique and returned as-is if already present), then use the returned pageId to build the PAGE link. Always have a real pageId before building a PAGE link.",
|
|
4045
|
+
"",
|
|
4046
|
+
"## Validation",
|
|
4047
|
+
"Values are validated server-side (deeply, including nested children). Wrong shape or wrong semantics (double-encoded string, { value } wrapper on an object prop, props vs propValues, missing/duplicate id, wrong reference key, nonexistent component id, unknown prop name) are REJECTED with an explanatory error instead of being stored silently. Read the error and fix the value; if unsure of a shape, read it with get_section_values and match it.",
|
|
4048
|
+
].join("\n");
|
|
4049
|
+
server.tool("get_editor_workflow", "Read this FIRST when the user wants to place a section on a page or fill/edit a section's content (text, image, link, slides, component lists) in the live editor. Returns the complete step-by-step workflow for the live-editor tools (list_editor_pages, list_imported_sections, import_section, add_section_to_page, list_page_sections, update_section_prop, upload_image), the per-prop-type value shapes, the COMPONENT_LIST read-modify-write rules, and how validation/auto-import behave. Use it to avoid the common mistake of writing component code to set a value a section already supports.", {}, async () => {
|
|
4050
|
+
return { content: [{ type: "text", text: EDITOR_WORKFLOW_GUIDE }] };
|
|
4051
|
+
});
|
|
4052
|
+
// Tool: list_theme_globals
|
|
4053
|
+
server.tool("list_theme_globals", "List the theme's global settings from the connected editor: global variables (Theme Settings) and design tokens (colors, typography, breakpoints, keyframes, color schemes). Call this BEFORE generating sections/components so you reuse the theme's existing variables and tokens (read them in component code via `getThemeSetting`/`getThemeColors`/... from `@ikas/bp-storefront`). Includes items created manually in the editor UI. Requires `ikas-component dev` running with the editor connected.", {
|
|
4054
|
+
project_root: z.string().describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
|
|
4055
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
4056
|
+
}, async ({ project_root, port }) => {
|
|
4057
|
+
const args = ["list-theme-globals", ...(port ? ["--port", String(port)] : [])];
|
|
4058
|
+
return callEditorAction(project_root, args);
|
|
4059
|
+
});
|
|
4060
|
+
// Tool: create_theme_global
|
|
4061
|
+
server.tool("create_theme_global", "Create a theme global setting in the connected editor. `kind` selects what to create:\n" +
|
|
4062
|
+
"- globalVariable: requires display_name + type (TEXT|RICH_TEXT|IMAGE|COLOR|NUMBER|BOOLEAN|BORDER|SHADOW); value optional. Value shapes — TEXT/COLOR: string; RICH_TEXT: HTML string; NUMBER: number; BOOLEAN: boolean; IMAGE: { url } object; BORDER: { width: { value, unit }, style, color }; SHADOW: { x, y, blur, spread, color, position: \"outside\"|\"inside\" }.\n" +
|
|
4063
|
+
"- color: requires name + value (hex, e.g. \"#ff0000\").\n" +
|
|
4064
|
+
"- typography: requires name + at least one of font_family/font_size/font_weight/line_height/letter_spacing.\n" +
|
|
4065
|
+
"- breakpoint: requires name + width (px).\n" +
|
|
4066
|
+
"- 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\". Use the keyframe's `ref` as a CSS animation-name and set timing (duration/iteration) where you apply it.\n" +
|
|
4067
|
+
"- colorScheme: requires name + colors (array of { key, value }) — `key` is the color slot name (e.g. Background, Text, Primary); slots are created automatically if missing.\n" +
|
|
4068
|
+
"Read created items back with `list_theme_globals`. Requires `ikas-component dev` running with the editor connected.", {
|
|
4069
|
+
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
4070
|
+
kind: z
|
|
4071
|
+
.enum(["globalVariable", "color", "typography", "breakpoint", "keyframe", "colorScheme"])
|
|
4072
|
+
.describe("What to create."),
|
|
4073
|
+
name: z.string().optional().describe("Name (design tokens)."),
|
|
4074
|
+
display_name: z.string().optional().describe("Human label (globalVariable)."),
|
|
4075
|
+
type: z.string().optional().describe("globalVariable value type."),
|
|
4076
|
+
value: z.any().optional().describe("globalVariable default value (shape depends on type — see tool description), or color hex string."),
|
|
4077
|
+
font_family: z.string().optional(),
|
|
4078
|
+
font_size: z.string().optional(),
|
|
4079
|
+
font_weight: z.string().optional(),
|
|
4080
|
+
line_height: z.string().optional(),
|
|
4081
|
+
letter_spacing: z.string().optional(),
|
|
4082
|
+
width: z.number().optional().describe("breakpoint width in px."),
|
|
4083
|
+
keyframe_type: z.enum(["keyframe", "transition"]).optional(),
|
|
4084
|
+
points: z.any().optional().describe("keyframe points array."),
|
|
4085
|
+
colors: z.any().optional().describe("colorScheme colors: array of { key, value } (key = color slot name)."),
|
|
4086
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
4087
|
+
}, async (input) => {
|
|
4088
|
+
const p = input.port ? ["--port", String(input.port)] : [];
|
|
4089
|
+
let args;
|
|
4090
|
+
switch (input.kind) {
|
|
4091
|
+
case "globalVariable":
|
|
4092
|
+
args = [
|
|
4093
|
+
"create-global-variable",
|
|
4094
|
+
"--display-name",
|
|
4095
|
+
String(input.display_name ?? ""),
|
|
4096
|
+
"--type",
|
|
4097
|
+
String(input.type ?? ""),
|
|
4098
|
+
...(input.value !== undefined ? ["--value", JSON.stringify(input.value)] : []),
|
|
4099
|
+
...p,
|
|
4100
|
+
];
|
|
4101
|
+
break;
|
|
4102
|
+
case "color":
|
|
4103
|
+
args = ["create-color", "--name", String(input.name ?? ""), "--value", String(input.value ?? ""), ...p];
|
|
4104
|
+
break;
|
|
4105
|
+
case "typography":
|
|
4106
|
+
args = [
|
|
4107
|
+
"create-text-style",
|
|
4108
|
+
"--name",
|
|
4109
|
+
String(input.name ?? ""),
|
|
4110
|
+
...(input.font_family ? ["--font-family", input.font_family] : []),
|
|
4111
|
+
...(input.font_size ? ["--font-size", input.font_size] : []),
|
|
4112
|
+
...(input.font_weight ? ["--font-weight", input.font_weight] : []),
|
|
4113
|
+
...(input.line_height ? ["--line-height", input.line_height] : []),
|
|
4114
|
+
...(input.letter_spacing ? ["--letter-spacing", input.letter_spacing] : []),
|
|
4115
|
+
...p,
|
|
4116
|
+
];
|
|
4117
|
+
break;
|
|
4118
|
+
case "breakpoint":
|
|
4119
|
+
args = ["create-breakpoint", "--name", String(input.name ?? ""), "--width", String(input.width ?? ""), ...p];
|
|
4120
|
+
break;
|
|
4121
|
+
case "keyframe":
|
|
4122
|
+
args = [
|
|
4123
|
+
"create-keyframe",
|
|
4124
|
+
"--name",
|
|
4125
|
+
String(input.name ?? ""),
|
|
4126
|
+
...(input.keyframe_type ? ["--type", input.keyframe_type] : []),
|
|
4127
|
+
"--points",
|
|
4128
|
+
JSON.stringify(input.points ?? []),
|
|
4129
|
+
...p,
|
|
4130
|
+
];
|
|
4131
|
+
break;
|
|
4132
|
+
case "colorScheme":
|
|
4133
|
+
args = [
|
|
4134
|
+
"create-color-scheme",
|
|
4135
|
+
"--name",
|
|
4136
|
+
String(input.name ?? ""),
|
|
4137
|
+
"--colors",
|
|
4138
|
+
JSON.stringify(input.colors ?? []),
|
|
4139
|
+
...p,
|
|
4140
|
+
];
|
|
4141
|
+
break;
|
|
4142
|
+
default:
|
|
4143
|
+
return { content: [{ type: "text", text: `Error: unknown kind "${input.kind}"` }] };
|
|
4144
|
+
}
|
|
4145
|
+
return callEditorAction(input.project_root, args);
|
|
4146
|
+
});
|
|
4147
|
+
// Tool: update_theme_global
|
|
4148
|
+
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`.", {
|
|
4149
|
+
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
4150
|
+
name: z.string().describe("The variable's runtime key (from `list_theme_globals`)."),
|
|
4151
|
+
display_name: z.string().optional().describe("New label."),
|
|
4152
|
+
type: z.string().optional().describe("New value type (TEXT|RICH_TEXT|IMAGE|COLOR|NUMBER|BOOLEAN|BORDER|SHADOW)."),
|
|
4153
|
+
value: z.any().optional().describe("New value (shape depends on type)."),
|
|
4154
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
4155
|
+
}, async ({ project_root, name, display_name, type, value, port }) => {
|
|
4156
|
+
const args = [
|
|
4157
|
+
"update-global-variable",
|
|
4158
|
+
"--name",
|
|
4159
|
+
name,
|
|
4160
|
+
...(display_name !== undefined ? ["--display-name", display_name] : []),
|
|
4161
|
+
...(type !== undefined ? ["--type", type] : []),
|
|
4162
|
+
...(value !== undefined ? ["--value", JSON.stringify(value)] : []),
|
|
4163
|
+
...(port ? ["--port", String(port)] : []),
|
|
4164
|
+
];
|
|
4165
|
+
return callEditorAction(project_root, args);
|
|
4166
|
+
});
|
|
4167
|
+
// Tool: delete_theme_global
|
|
4168
|
+
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`. Get names/ids from `list_theme_globals`.", {
|
|
4169
|
+
project_root: z.string().describe("Absolute path to the code-component project."),
|
|
4170
|
+
kind: z
|
|
4171
|
+
.enum(["globalVariable", "color", "typography", "breakpoint", "keyframe", "colorScheme"])
|
|
4172
|
+
.describe("What to delete."),
|
|
4173
|
+
name: z.string().optional().describe("globalVariable runtime key (from `list_theme_globals`)."),
|
|
4174
|
+
id: z.string().optional().describe("design-token id (from `list_theme_globals`)."),
|
|
4175
|
+
port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
|
|
4176
|
+
}, async ({ project_root, kind, name, id, port }) => {
|
|
4177
|
+
const p = port ? ["--port", String(port)] : [];
|
|
4178
|
+
const args = kind === "globalVariable"
|
|
4179
|
+
? ["delete-global-variable", "--name", String(name ?? ""), ...p]
|
|
4180
|
+
: ["delete-design-token", "--token-type", kind, "--id", String(id ?? ""), ...p];
|
|
4181
|
+
return callEditorAction(project_root, args);
|
|
4182
|
+
});
|
|
2725
4183
|
// --- Start server ---
|
|
2726
4184
|
async function main() {
|
|
2727
4185
|
const transport = new StdioServerTransport();
|