@ikas/code-components-mcp 1.4.0-beta.2 → 1.4.0-beta.21
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 +22 -5
- package/data/migration.json +189 -24
- package/data/section-templates/account-info-section/children/AccountFavorites/ikas-config-snippet.json +3 -3
- package/data/section-templates/account-info-section/ikas-config-snippet.json +5 -5
- package/data/section-templates/category-images-section/ikas-config-snippet.json +1 -1
- package/data/section-templates/category-list-section/ikas-config-snippet.json +3 -3
- package/data/section-templates/component-renderer/ikas-config-snippet.json +3 -3
- package/data/section-templates/features-section/ikas-config-snippet.json +1 -1
- package/data/section-templates/footer-section/ikas-config-snippet.json +1 -1
- package/data/section-templates/header-section/children/Announcements/ikas-config-snippet.json +1 -1
- package/data/section-templates/header-section/children/Navbar/ikas-config-snippet.json +3 -3
- package/data/section-templates/header-section/ikas-config-snippet.json +3 -3
- package/data/section-templates/hero-slider-section/ikas-config-snippet.json +1 -1
- package/data/section-templates/image-handling/ikas-config-snippet.json +13 -13
- package/data/section-templates/navigation/ikas-config-snippet.json +3 -3
- package/data/section-templates/product-detail-section/children/ProductDetailDescription/ikas-config-snippet.json +1 -1
- package/data/section-templates/product-detail-section/children/ProductDetailFeatures/ikas-config-snippet.json +1 -1
- package/data/section-templates/product-detail-section/ikas-config-snippet.json +13 -13
- package/data/section-templates/product-slider-section/ikas-config-snippet.json +3 -3
- package/data/storefront-api.json +114 -1381
- package/data/storefront-types.json +32 -124
- package/dist/index.js +1449 -336
- 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,10 +651,61 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
581
651
|
parts.push(`_Old source directory not provided — shared sub-components cannot be detected automatically. If you have access to the old source, call \`plan_migration\` again with \`old_source_dir\` to populate this section._`);
|
|
582
652
|
parts.push("");
|
|
583
653
|
}
|
|
654
|
+
parts.push(`> **This scan is partial.** \`scanSharedSubcomponents\` only detects relative imports used by **3 or more** old components. Atomic components used by fewer sections (often Button/Input/Card/icon primitives) will be missed. Scan \`src/\` yourself and add any others under \`## Source Code Analysis\` below.`);
|
|
655
|
+
parts.push("");
|
|
656
|
+
// Custom Data Types — deferred decisions (reference list, not checkboxes)
|
|
657
|
+
const rootCustomData = customData.filter((cd) => cd.isRoot);
|
|
658
|
+
if (rootCustomData.length > 0) {
|
|
659
|
+
// Build a usage map: which sections reference each customData type
|
|
660
|
+
const usageByCustomDataId = new Map();
|
|
661
|
+
for (const comp of components) {
|
|
662
|
+
const sectionName = comp.displayName || comp.dir || comp.id || "?";
|
|
663
|
+
for (const p of comp.props || []) {
|
|
664
|
+
if (p.type === "CUSTOM" && p.customDataId) {
|
|
665
|
+
const list = usageByCustomDataId.get(p.customDataId) || [];
|
|
666
|
+
if (!list.includes(sectionName))
|
|
667
|
+
list.push(sectionName);
|
|
668
|
+
usageByCustomDataId.set(p.customDataId, list);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
parts.push(`### Custom Data Types — decide during section migration`);
|
|
673
|
+
parts.push("");
|
|
674
|
+
parts.push(`Each entry below is an OLD \`theme.customData[]\` type. Do **not** pre-migrate these as enums. When you migrate the section that uses one, decide:`);
|
|
675
|
+
parts.push(`- **Flat scalar set** (e.g. \`"left" | "right" | "center"\`) → new-system **enum prop** via \`config add-enum\`.`);
|
|
676
|
+
parts.push(`- **Structured record** (e.g. \`{image, link, title}\`, repeated in a list) → new-system **component** via \`config add-component\` + COMPONENT_LIST wiring on the parent.`);
|
|
677
|
+
parts.push("");
|
|
678
|
+
parts.push(`Log every decision in \`## Custom Data Decisions\` below. See \`get_migration_guide("custom-data-conversion")\` for the full heuristic and worked examples (Position, Slide, MenuItem).`);
|
|
679
|
+
parts.push("");
|
|
680
|
+
for (const cd of rootCustomData) {
|
|
681
|
+
const cdName = cd.typescriptName || cd.name || cd.id || "Unknown";
|
|
682
|
+
const cdType = cd.type || "?";
|
|
683
|
+
let shape = "";
|
|
684
|
+
if (cd.type === "ENUM") {
|
|
685
|
+
const opts = (cd.enumOptions || [])
|
|
686
|
+
.map((o) => o.value || o.displayName)
|
|
687
|
+
.filter(Boolean);
|
|
688
|
+
shape = ` — shape: \`enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}\``;
|
|
689
|
+
}
|
|
690
|
+
else if (cd.nestedData && cd.nestedData.length > 0) {
|
|
691
|
+
const first = cd.nestedData[0];
|
|
692
|
+
const fields = (first?.nestedData || cd.nestedData || [])
|
|
693
|
+
.map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`)
|
|
694
|
+
.slice(0, 8);
|
|
695
|
+
shape = ` — shape: \`{${fields.join(", ")}}\``;
|
|
696
|
+
}
|
|
697
|
+
const usedBy = cd.id ? usageByCustomDataId.get(cd.id) || [] : [];
|
|
698
|
+
const usedByStr = usedBy.length > 0
|
|
699
|
+
? ` — used by: ${usedBy.slice(0, 6).join(", ")}${usedBy.length > 6 ? `, +${usedBy.length - 6} more` : ""}`
|
|
700
|
+
: ` — _not directly referenced by any section's props (may be nested inside another customData)_`;
|
|
701
|
+
parts.push(`- \`${cdName}\` (${cdType})${shape}${usedByStr}`);
|
|
702
|
+
}
|
|
703
|
+
parts.push("");
|
|
704
|
+
}
|
|
584
705
|
// Sections queue
|
|
585
706
|
parts.push(`## Sections`);
|
|
586
707
|
parts.push("");
|
|
587
|
-
parts.push(`
|
|
708
|
+
parts.push(`Component **names** are listed below. The CLI auto-generates opaque \`componentId\`s when you run \`config add-component\` — capture each id from the JSON response and use it (or look it up later with \`config list\`) when wiring \`filteredComponentIds\`.`);
|
|
588
709
|
parts.push("");
|
|
589
710
|
const buckets = {
|
|
590
711
|
simple: [],
|
|
@@ -610,7 +731,11 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
610
731
|
const oldName = comp.displayName || comp.dir || comp.id || "Unknown";
|
|
611
732
|
const kebabName = toKebabCase(comp.dir || comp.displayName || comp.id || "unknown");
|
|
612
733
|
const newId = `${projectName}-${kebabName}`;
|
|
613
|
-
const headerFooter = comp.isHeader
|
|
734
|
+
const headerFooter = comp.isHeader
|
|
735
|
+
? " **[HEADER]**"
|
|
736
|
+
: comp.isFooter
|
|
737
|
+
? " **[FOOTER]**"
|
|
738
|
+
: "";
|
|
614
739
|
const propCount = (comp.props || []).length;
|
|
615
740
|
// Detect children from CUSTOM DYNAMIC_LIST props
|
|
616
741
|
const children = [];
|
|
@@ -620,11 +745,14 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
620
745
|
if (cd && (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST")) {
|
|
621
746
|
const itemObj = cd.nestedData?.[0];
|
|
622
747
|
if (itemObj) {
|
|
623
|
-
const childName = itemObj.typescriptName ||
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
748
|
+
const childName = itemObj.typescriptName ||
|
|
749
|
+
(itemObj.name
|
|
750
|
+
? itemObj.name.replace(/[^a-zA-Z0-9]/g, "")
|
|
751
|
+
: `${oldName}Item`);
|
|
752
|
+
const fields = (itemObj.nestedData || [])
|
|
753
|
+
.map((f) => f.key || f.name || "?")
|
|
754
|
+
.slice(0, 8);
|
|
755
|
+
children.push({ propName: p.name || "?", childName, fields });
|
|
628
756
|
}
|
|
629
757
|
}
|
|
630
758
|
}
|
|
@@ -634,65 +762,73 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
|
|
|
634
762
|
if (children.length > 0) {
|
|
635
763
|
parts.push(` - Children:`);
|
|
636
764
|
for (const ch of children) {
|
|
637
|
-
parts.push(` - [ ] \`${ch.
|
|
765
|
+
parts.push(` - [ ] \`${ch.childName}\` — for prop \`${ch.propName}\` — fields: ${ch.fields.join(", ")}`);
|
|
638
766
|
}
|
|
639
767
|
}
|
|
640
768
|
}
|
|
641
769
|
parts.push("");
|
|
642
770
|
}
|
|
643
|
-
//
|
|
644
|
-
parts.push(`##
|
|
771
|
+
// Source Code Analysis — placeholder for the LLM to fill in
|
|
772
|
+
parts.push(`## Source Code Analysis`);
|
|
645
773
|
parts.push("");
|
|
646
|
-
parts.push(
|
|
774
|
+
parts.push(`> **Before starting section migration**, scan the old \`src/\` for components NOT listed above. theme.json does not see atomic primitives — buttons, inputs, cards, icon wrappers, layout helpers, etc. — that are imported by sections but never appear as theme components. Add them here, then decide which become shared sub-components vs which collapse into section bodies.`);
|
|
647
775
|
parts.push("");
|
|
648
|
-
parts.push(
|
|
776
|
+
parts.push(`<!-- Example: \`- Button (src/atoms/Button/) — used by ~all sections → promote to Shared Sub-Component\` -->`);
|
|
777
|
+
parts.push("");
|
|
778
|
+
// Custom Data Decisions — append-only log the LLM fills as it makes per-section decisions
|
|
779
|
+
parts.push(`## Custom Data Decisions`);
|
|
649
780
|
parts.push("");
|
|
650
|
-
|
|
651
|
-
parts.push(
|
|
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\`.`);
|
|
782
|
+
parts.push("");
|
|
783
|
+
parts.push(`<!-- Example: \`- SlideData → component \\\`hero-slide\\\` (2026-05-11) — structured record {image,link,title}; LIST_OF_LINK would drop the image\` -->`);
|
|
784
|
+
parts.push("");
|
|
785
|
+
// Known Environmental Issues (agents fill in during work)
|
|
786
|
+
parts.push(`## Known Environmental Issues`);
|
|
652
787
|
parts.push("");
|
|
653
|
-
parts.push(`
|
|
788
|
+
parts.push(`_Record any non-component build/TS errors here so future sessions don't waste time diagnosing them._`);
|
|
654
789
|
parts.push("");
|
|
655
|
-
parts.push(
|
|
656
|
-
parts.push(`2. Call \`get_section_migration_plan(theme_json, "<old section name>", "${projectName}")\`.`);
|
|
657
|
-
parts.push(`3. Read the old source files listed in the plan.`);
|
|
658
|
-
parts.push(`4. Run the CLI commands in the plan (they create parent + children with auto-generated types.ts).`);
|
|
659
|
-
parts.push(`5. Write \`index.tsx\` and \`styles.css\` using the patterns in the plan. DO NOT manually edit types.ts.`);
|
|
660
|
-
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).`);
|
|
790
|
+
parts.push(`- [ ] _(none recorded yet)_`);
|
|
661
791
|
parts.push("");
|
|
662
|
-
//
|
|
663
|
-
parts.push(`##
|
|
792
|
+
// Notes — append-only log for decisions not captured elsewhere
|
|
793
|
+
parts.push(`## Notes`);
|
|
664
794
|
parts.push("");
|
|
665
|
-
parts.push(`
|
|
795
|
+
parts.push(`_Append a bullet after completing work — library swaps, ad-hoc props, CLI folder-name oddities, user-approved feature drops, etc. Format: \`- [YYYY-MM-DD] <section-id>: <brief note>\`._`);
|
|
666
796
|
parts.push("");
|
|
667
|
-
parts.push(
|
|
668
|
-
parts.push(`<!-- \`- [2026-04-15] Foundation: built Input, SubmitButton, Modal, SectionHeader, StarRating. Skipped: GoogleCaptcha (needs investigation), BlogCard, Pagination (low priority).\` -->`);
|
|
669
|
-
parts.push(`<!-- \`- [2026-04-15] ${projectName}-footer: swapped react-hot-toast for inline status text; newsletter uses form-model pattern (getNewsletterSubscriptionForm). CLI created folder as "Footer/" as expected.\` -->`);
|
|
797
|
+
parts.push(`<!-- Example: \`- [2026-04-15] ${projectName}-footer: swapped react-hot-toast for inline status text; CLI created Footer/ as expected\` -->`);
|
|
670
798
|
parts.push("");
|
|
671
|
-
// Cross-references
|
|
799
|
+
// Cross-references — keep terse; LLM can call `get_migration_guide("list")` or `get_framework_guide("list")` for more
|
|
672
800
|
parts.push(`## Cross-References`);
|
|
673
801
|
parts.push("");
|
|
674
|
-
parts.push(
|
|
802
|
+
parts.push(`- \`get_migration_guide("iterative-workflow")\` — the full per-phase protocol`);
|
|
803
|
+
parts.push(`- \`get_migration_guide("custom-data-conversion")\` — enum-vs-component decisions`);
|
|
804
|
+
parts.push(`- \`get_migration_guide("prop-runtime-shapes")\` — runtime shapes & access patterns`);
|
|
805
|
+
parts.push(`- \`get_framework_guide("common-pitfalls")\` — gotchas & old→new property migrations`);
|
|
675
806
|
parts.push("");
|
|
676
|
-
parts.push(
|
|
677
|
-
parts.push(`- \`get_migration_guide("component-renderer-limitations")\` — critical constraints when using COMPONENT_LIST`);
|
|
678
|
-
parts.push(`- \`get_migration_guide("prop-runtime-shapes")\` — \`.data\` vs \`.links\`, \`.variant\` vs \`.product\`, etc.`);
|
|
679
|
-
parts.push(`- \`get_migration_guide("link-prop-decision-guide")\` — LINK vs LIST_OF_LINK vs COMPONENT_LIST`);
|
|
680
|
-
parts.push(`- \`get_migration_guide("library-replacements")\` — swiper, headlessui, tailwind, etc.`);
|
|
681
|
-
parts.push(`- \`get_migration_guide("react-to-preact")\` — observer rules, imports, IkasSlider removal`);
|
|
682
|
-
parts.push(`- \`get_framework_guide("component-renderer-patterns")\` — full IkasComponentRenderer usage`);
|
|
683
|
-
parts.push(`- \`get_framework_guide("common-pitfalls")\` — general gotchas`);
|
|
684
|
-
parts.push(`- \`get_framework_guide("navigation-patterns")\` — Router.navigate, Router.navigateToPage`);
|
|
685
|
-
parts.push(`- \`get_model_guide("<TypeName>")\` — IkasCart, IkasProduct, IkasCustomer shapes`);
|
|
807
|
+
parts.push(`Call \`get_migration_guide("list")\` or \`get_framework_guide("list")\` for the full topic catalog.`);
|
|
686
808
|
parts.push("");
|
|
687
809
|
return parts.join("\n");
|
|
688
810
|
}
|
|
689
811
|
// Known libraries we detect in old themes and want to flag for replacement
|
|
690
812
|
const KNOWN_LIBRARIES = [
|
|
691
|
-
"swiper",
|
|
692
|
-
"react
|
|
693
|
-
"react
|
|
694
|
-
"
|
|
695
|
-
"
|
|
813
|
+
"swiper",
|
|
814
|
+
"@headlessui/react",
|
|
815
|
+
"@heroicons/react",
|
|
816
|
+
"recharts",
|
|
817
|
+
"react-player",
|
|
818
|
+
"react-simple-star-rating",
|
|
819
|
+
"react-slider",
|
|
820
|
+
"react-compound-slider",
|
|
821
|
+
"react-zoom-pan-pinch",
|
|
822
|
+
"react-hot-toast",
|
|
823
|
+
"react-fast-marquee",
|
|
824
|
+
"react-indiana-drag-scroll",
|
|
825
|
+
"react-simple-typewriter",
|
|
826
|
+
"react-timer-hook",
|
|
827
|
+
"date-fns",
|
|
828
|
+
"slugify",
|
|
829
|
+
"classnames",
|
|
830
|
+
"clsx",
|
|
831
|
+
"@react-pdf/renderer",
|
|
696
832
|
];
|
|
697
833
|
// Heuristic: member-access patterns on old storefront stores/singletons that likely need new-system equivalents
|
|
698
834
|
const OLD_STOREFRONT_CALL_REGEX = /\b(customerStore|cartStore|productStore|categoryStore|orderStore|searchStore|favoritesStore|i18nStore|Router|useStore)\.\w+/g;
|
|
@@ -711,7 +847,8 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
711
847
|
// Collect all .tsx/.ts files in the component dir
|
|
712
848
|
try {
|
|
713
849
|
for (const entry of fs.readdirSync(componentDir, { withFileTypes: true })) {
|
|
714
|
-
if (entry.isFile() &&
|
|
850
|
+
if (entry.isFile() &&
|
|
851
|
+
(entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
|
|
715
852
|
result.sourceFiles.push(path.join(componentDir, entry.name));
|
|
716
853
|
}
|
|
717
854
|
}
|
|
@@ -728,7 +865,18 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
728
865
|
const subCompSet = new Map();
|
|
729
866
|
const callSet = new Set();
|
|
730
867
|
// Packages we treat as "framework" and don't flag for replacement (but do note in reactPackageUsage)
|
|
731
|
-
const REACT_PACKAGES = new Set([
|
|
868
|
+
const REACT_PACKAGES = new Set([
|
|
869
|
+
"react",
|
|
870
|
+
"react-dom",
|
|
871
|
+
"next",
|
|
872
|
+
"next/link",
|
|
873
|
+
"next/image",
|
|
874
|
+
"next/router",
|
|
875
|
+
"next/head",
|
|
876
|
+
"next/script",
|
|
877
|
+
"mobx-react-lite",
|
|
878
|
+
"mobx",
|
|
879
|
+
]);
|
|
732
880
|
for (const file of result.sourceFiles) {
|
|
733
881
|
let content;
|
|
734
882
|
try {
|
|
@@ -742,7 +890,7 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
742
890
|
while ((m = importRegex.exec(content)) !== null) {
|
|
743
891
|
const p = m[1];
|
|
744
892
|
if (p.startsWith(".")) {
|
|
745
|
-
const segs = p.split("/").filter(s => s && s !== "." && s !== "..");
|
|
893
|
+
const segs = p.split("/").filter((s) => s && s !== "." && s !== "..");
|
|
746
894
|
const last = segs[segs.length - 1];
|
|
747
895
|
if (last && /^[A-Z]/.test(last) && !last.includes("__generated__")) {
|
|
748
896
|
subCompSet.set(last, p);
|
|
@@ -750,7 +898,9 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
750
898
|
}
|
|
751
899
|
else if (!p.startsWith("@ikas/")) {
|
|
752
900
|
// Classify: known library, react-family, or unknown-external
|
|
753
|
-
const base = p.startsWith("@")
|
|
901
|
+
const base = p.startsWith("@")
|
|
902
|
+
? p.split("/").slice(0, 2).join("/")
|
|
903
|
+
: p.split("/")[0];
|
|
754
904
|
if (REACT_PACKAGES.has(p) || REACT_PACKAGES.has(base)) {
|
|
755
905
|
reactSet.add(base);
|
|
756
906
|
}
|
|
@@ -813,7 +963,10 @@ function scanSectionSource(componentDir, propNames) {
|
|
|
813
963
|
}
|
|
814
964
|
}
|
|
815
965
|
}
|
|
816
|
-
result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({
|
|
966
|
+
result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({
|
|
967
|
+
name,
|
|
968
|
+
path: p,
|
|
969
|
+
}));
|
|
817
970
|
result.importedLibraries = [...libSet].sort();
|
|
818
971
|
result.importedUnknownLibraries = [...unknownLibSet].sort();
|
|
819
972
|
result.reactPackageUsage = [...reactSet].sort();
|
|
@@ -829,34 +982,45 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
829
982
|
customDataMap.set(cd.id, cd);
|
|
830
983
|
}
|
|
831
984
|
// Find the component — try match by dir, displayName, id, or new-id
|
|
832
|
-
const target = components.find(c => {
|
|
985
|
+
const target = components.find((c) => {
|
|
833
986
|
if (!c)
|
|
834
987
|
return false;
|
|
835
|
-
if (c.dir === sectionName ||
|
|
988
|
+
if (c.dir === sectionName ||
|
|
989
|
+
c.displayName === sectionName ||
|
|
990
|
+
c.id === sectionName)
|
|
836
991
|
return true;
|
|
837
992
|
const kebab = toKebabCase(c.dir || c.displayName || c.id || "");
|
|
838
993
|
const newId = `${projectName}-${kebab}`;
|
|
839
994
|
return newId === sectionName;
|
|
840
995
|
});
|
|
841
996
|
if (!target) {
|
|
842
|
-
const available = components
|
|
997
|
+
const available = components
|
|
998
|
+
.map((c) => c.dir || c.displayName || c.id)
|
|
999
|
+
.filter(Boolean)
|
|
1000
|
+
.join(", ");
|
|
843
1001
|
return `Section "${sectionName}" not found in theme. Available: ${available}`;
|
|
844
1002
|
}
|
|
845
1003
|
const parts = [];
|
|
846
1004
|
const oldName = target.displayName || target.dir || target.id || "Unknown";
|
|
847
1005
|
const kebabName = toKebabCase(target.dir || target.displayName || target.id || "unknown");
|
|
848
1006
|
const sectionId = `${projectName}-${kebabName}`;
|
|
849
|
-
const sectionPascal = (target.dir || target.displayName || "").replace(/[^a-zA-Z0-9]/g, "") ||
|
|
1007
|
+
const sectionPascal = (target.dir || target.displayName || "").replace(/[^a-zA-Z0-9]/g, "") ||
|
|
1008
|
+
kebabName
|
|
1009
|
+
.split("-")
|
|
1010
|
+
.map((s) => s[0]?.toUpperCase() + s.slice(1))
|
|
1011
|
+
.join("");
|
|
850
1012
|
// Scan the old source for imports, libraries, field usage
|
|
851
|
-
const propNames = (target.props || [])
|
|
852
|
-
|
|
1013
|
+
const propNames = (target.props || [])
|
|
1014
|
+
.map((p) => p.name || "")
|
|
1015
|
+
.filter(Boolean);
|
|
1016
|
+
const sourceScan = oldSourceDir && target.dir
|
|
853
1017
|
? scanSectionSource(path.join(oldSourceDir, target.dir), propNames)
|
|
854
1018
|
: null;
|
|
855
1019
|
parts.push(`# Section Migration Plan: ${oldName}`);
|
|
856
1020
|
parts.push("");
|
|
857
1021
|
parts.push(`**Old name:** \`${oldName}\` (dir: \`${target.dir || "?"}\`)`);
|
|
858
|
-
parts.push(`**New section
|
|
859
|
-
parts.push(`**
|
|
1022
|
+
parts.push(`**New section name:** \`${sectionPascal}\` (the CLI will assign an opaque \`componentId\` at write-time — capture it from \`config add-component\`'s JSON response)`);
|
|
1023
|
+
parts.push(`**Migration checkbox label:** \`${sectionId}\` (tracking identifier in \`MIGRATION.md\` only — not the runtime component id)`);
|
|
860
1024
|
if (target.isHeader)
|
|
861
1025
|
parts.push(`**Flags:** HEADER (\`isHeader: true\`)`);
|
|
862
1026
|
if (target.isFooter)
|
|
@@ -895,7 +1059,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
895
1059
|
}
|
|
896
1060
|
if (sourceScan.reactPackageUsage.length > 0) {
|
|
897
1061
|
parts.push("");
|
|
898
|
-
parts.push(`**React/Next.js framework imports detected:** ${sourceScan.reactPackageUsage.map(p => `\`${p}\``).join(", ")}. These do NOT carry over — the new system is Preact. See \`get_migration_guide("react-to-preact")\` for conversion patterns (hooks, observer, event types, routing).`);
|
|
1062
|
+
parts.push(`**React/Next.js framework imports detected:** ${sourceScan.reactPackageUsage.map((p) => `\`${p}\``).join(", ")}. These do NOT carry over — the new system is Preact. See \`get_migration_guide("react-to-preact")\` for conversion patterns (hooks, observer, event types, routing).`);
|
|
899
1063
|
}
|
|
900
1064
|
if (sourceScan.oldStorefrontCalls.length > 0) {
|
|
901
1065
|
parts.push("");
|
|
@@ -941,7 +1105,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
941
1105
|
if (oldType === "SLIDER") {
|
|
942
1106
|
newType = "NUMBER";
|
|
943
1107
|
notes = `Was SLIDER(min=${p.sliderData?.min}, max=${p.sliderData?.max}) — replace \`.value\` access with direct number`;
|
|
944
|
-
const prop = {
|
|
1108
|
+
const prop = {
|
|
1109
|
+
name: newName,
|
|
1110
|
+
displayName: p.displayName || newName,
|
|
1111
|
+
type: "NUMBER",
|
|
1112
|
+
};
|
|
945
1113
|
if (p.isRequired)
|
|
946
1114
|
prop.required = true;
|
|
947
1115
|
parentPropsJson.push(prop);
|
|
@@ -949,7 +1117,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
949
1117
|
else if (oldType === "PRODUCT_DETAIL") {
|
|
950
1118
|
newType = "PRODUCT";
|
|
951
1119
|
notes = "Renamed — PRODUCT_DETAIL → PRODUCT";
|
|
952
|
-
const prop = {
|
|
1120
|
+
const prop = {
|
|
1121
|
+
name: newName,
|
|
1122
|
+
displayName: p.displayName || newName,
|
|
1123
|
+
type: "PRODUCT",
|
|
1124
|
+
};
|
|
953
1125
|
if (p.isRequired)
|
|
954
1126
|
prop.required = true;
|
|
955
1127
|
parentPropsJson.push(prop);
|
|
@@ -960,9 +1132,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
960
1132
|
if (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST") {
|
|
961
1133
|
// Child component needed
|
|
962
1134
|
const itemObj = cd.nestedData?.[0];
|
|
963
|
-
const childName = itemObj?.typescriptName ||
|
|
964
|
-
|
|
965
|
-
|
|
1135
|
+
const childName = itemObj?.typescriptName ||
|
|
1136
|
+
(itemObj?.name
|
|
1137
|
+
? itemObj.name.replace(/[^a-zA-Z0-9]/g, "")
|
|
1138
|
+
: `${sectionPascal}Item`);
|
|
966
1139
|
const childProps = [];
|
|
967
1140
|
const nestedWarnings = [];
|
|
968
1141
|
for (const f of (itemObj?.nestedData || [])) {
|
|
@@ -973,11 +1146,18 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
973
1146
|
fType = "NUMBER";
|
|
974
1147
|
else if (fType === "PRODUCT_DETAIL")
|
|
975
1148
|
fType = "PRODUCT";
|
|
976
|
-
else if (fType === "CUSTOM" ||
|
|
1149
|
+
else if (fType === "CUSTOM" ||
|
|
1150
|
+
fType === "DYNAMIC_LIST" ||
|
|
1151
|
+
fType === "STATIC_LIST" ||
|
|
1152
|
+
fType === "OBJECT") {
|
|
977
1153
|
nestedWarnings.push(`\`${f.key}\` (${fType})`);
|
|
978
1154
|
fType = "COMPONENT_LIST";
|
|
979
1155
|
}
|
|
980
|
-
const prop = {
|
|
1156
|
+
const prop = {
|
|
1157
|
+
name: f.key,
|
|
1158
|
+
displayName: f.name || f.key,
|
|
1159
|
+
type: fType,
|
|
1160
|
+
};
|
|
981
1161
|
if (f.isRequired)
|
|
982
1162
|
prop.required = true;
|
|
983
1163
|
childProps.push(prop);
|
|
@@ -989,26 +1169,29 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
989
1169
|
if (childProps.length === 0 && sourceScan?.propFieldUsage[oldName]) {
|
|
990
1170
|
const inferred = sourceScan.propFieldUsage[oldName];
|
|
991
1171
|
for (const fieldName of inferred) {
|
|
992
|
-
childProps.push({
|
|
1172
|
+
childProps.push({
|
|
1173
|
+
name: fieldName,
|
|
1174
|
+
displayName: fieldName,
|
|
1175
|
+
type: "TEXT",
|
|
1176
|
+
});
|
|
993
1177
|
}
|
|
994
1178
|
if (inferred.length > 0) {
|
|
995
|
-
notes = `Was CUSTOM → ${cd.type} with empty customData. **Inferred props from source usage:** ${inferred.map(f => `\`${f}\``).join(", ")}. Verify types and required flags — default inferred type is TEXT.`;
|
|
1179
|
+
notes = `Was CUSTOM → ${cd.type} with empty customData. **Inferred props from source usage:** ${inferred.map((f) => `\`${f}\``).join(", ")}. Verify types and required flags — default inferred type is TEXT.`;
|
|
996
1180
|
}
|
|
997
1181
|
}
|
|
998
1182
|
children.push({
|
|
999
1183
|
propName: oldName,
|
|
1000
|
-
childId,
|
|
1001
1184
|
childName,
|
|
1002
1185
|
childPropsJson: childProps,
|
|
1003
1186
|
customDataName: cd.name || "unnamed",
|
|
1004
1187
|
});
|
|
1005
1188
|
newType = "COMPONENT_LIST";
|
|
1006
|
-
notes = `Was CUSTOM → ${cd.type} of ${itemObj?.typescriptName || "object"}. Create child component \`${
|
|
1189
|
+
notes = `Was CUSTOM → ${cd.type} of ${itemObj?.typescriptName || "object"}. Create child component \`${childName}\`; the CLI returns the opaque \`componentId\` in its JSON response — substitute it for the \`<id-of-${childName}>\` placeholder below.`;
|
|
1007
1190
|
const prop = {
|
|
1008
1191
|
name: newName,
|
|
1009
1192
|
displayName: p.displayName || newName,
|
|
1010
1193
|
type: "COMPONENT_LIST",
|
|
1011
|
-
filteredComponentIds: [
|
|
1194
|
+
filteredComponentIds: [`<id-of-${childName}>`],
|
|
1012
1195
|
};
|
|
1013
1196
|
parentPropsJson.push(prop);
|
|
1014
1197
|
}
|
|
@@ -1022,7 +1205,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1022
1205
|
let fType = f.type;
|
|
1023
1206
|
if (fType === "SLIDER")
|
|
1024
1207
|
fType = "NUMBER";
|
|
1025
|
-
const prop = {
|
|
1208
|
+
const prop = {
|
|
1209
|
+
name: f.key,
|
|
1210
|
+
displayName: f.name || f.key,
|
|
1211
|
+
type: fType,
|
|
1212
|
+
};
|
|
1026
1213
|
if (f.isRequired)
|
|
1027
1214
|
prop.required = true;
|
|
1028
1215
|
parentPropsJson.push(prop);
|
|
@@ -1037,7 +1224,8 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1037
1224
|
}
|
|
1038
1225
|
else if (cd.type === "ENUM") {
|
|
1039
1226
|
newType = "ENUM";
|
|
1040
|
-
const enumName = cd.typescriptName ||
|
|
1227
|
+
const enumName = cd.typescriptName ||
|
|
1228
|
+
(cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
|
|
1041
1229
|
const options = (cd.enumOptions || []).reduce((acc, o) => {
|
|
1042
1230
|
if (o.displayName && o.value)
|
|
1043
1231
|
acc[o.displayName] = o.value;
|
|
@@ -1045,7 +1233,12 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1045
1233
|
}, {});
|
|
1046
1234
|
enumsNeeded.push({ name: enumName, options });
|
|
1047
1235
|
notes = `Was CUSTOM (ENUM) — create enum \`${enumName}\` via \`config add-enum\` first, then reference its enumId here`;
|
|
1048
|
-
const prop = {
|
|
1236
|
+
const prop = {
|
|
1237
|
+
name: newName,
|
|
1238
|
+
displayName: p.displayName || newName,
|
|
1239
|
+
type: "ENUM",
|
|
1240
|
+
enumTypeId: `<ENUM_ID_FROM_add-enum_${enumName}>`,
|
|
1241
|
+
};
|
|
1049
1242
|
if (p.isRequired)
|
|
1050
1243
|
prop.required = true;
|
|
1051
1244
|
parentPropsJson.push(prop);
|
|
@@ -1054,7 +1247,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1054
1247
|
}
|
|
1055
1248
|
else {
|
|
1056
1249
|
// Direct mapping
|
|
1057
|
-
const prop = {
|
|
1250
|
+
const prop = {
|
|
1251
|
+
name: newName,
|
|
1252
|
+
displayName: p.displayName || newName,
|
|
1253
|
+
type: newType,
|
|
1254
|
+
};
|
|
1058
1255
|
if (p.isRequired)
|
|
1059
1256
|
prop.required = true;
|
|
1060
1257
|
parentPropsJson.push(prop);
|
|
@@ -1062,6 +1259,131 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1062
1259
|
parts.push(`| \`${oldName}\` | ${oldType} | → | \`${newName}\` | ${newType} | ${notes} |`);
|
|
1063
1260
|
}
|
|
1064
1261
|
parts.push("");
|
|
1262
|
+
// Custom Data Decision Callouts — per prop referencing a customData type
|
|
1263
|
+
const customDataPropsForCallouts = (target.props || []).filter((p) => p.type === "CUSTOM" &&
|
|
1264
|
+
p.customDataId &&
|
|
1265
|
+
customDataMap.has(p.customDataId));
|
|
1266
|
+
if (customDataPropsForCallouts.length > 0) {
|
|
1267
|
+
parts.push(`## Custom Data Decisions to Make`);
|
|
1268
|
+
parts.push("");
|
|
1269
|
+
parts.push(`Each prop below references an old \`customData\` type. Verify the MCP's default against the actual data semantics, then run the CLI command. **Log every decision in MIGRATION.md → \`## Custom Data Decisions\`** with reasoning. See \`get_migration_guide("custom-data-conversion")\` for the heuristic and worked examples.`);
|
|
1270
|
+
parts.push("");
|
|
1271
|
+
for (const p of customDataPropsForCallouts) {
|
|
1272
|
+
const cd = customDataMap.get(p.customDataId);
|
|
1273
|
+
if (!cd)
|
|
1274
|
+
continue;
|
|
1275
|
+
const cdName = cd.typescriptName || cd.name || cd.id || "Unknown";
|
|
1276
|
+
const cdType = cd.type || "?";
|
|
1277
|
+
let shape = "";
|
|
1278
|
+
let shapeKind = "unknown";
|
|
1279
|
+
if (cd.type === "ENUM") {
|
|
1280
|
+
const opts = (cd.enumOptions || [])
|
|
1281
|
+
.map((o) => o.value || o.displayName)
|
|
1282
|
+
.filter(Boolean);
|
|
1283
|
+
shape = `enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}`;
|
|
1284
|
+
shapeKind = "enum";
|
|
1285
|
+
}
|
|
1286
|
+
else if (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST") {
|
|
1287
|
+
const first = cd.nestedData?.[0];
|
|
1288
|
+
const fields = (first?.nestedData || []).map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`);
|
|
1289
|
+
shape = `${cdType} of {${fields.slice(0, 6).join(", ")}}`;
|
|
1290
|
+
shapeKind = "list";
|
|
1291
|
+
}
|
|
1292
|
+
else if (cd.type === "OBJECT") {
|
|
1293
|
+
const fields = (cd.nestedData || []).map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`);
|
|
1294
|
+
shape = `OBJECT {${fields.slice(0, 6).join(", ")}}`;
|
|
1295
|
+
shapeKind = "record";
|
|
1296
|
+
}
|
|
1297
|
+
else {
|
|
1298
|
+
shape = cdType;
|
|
1299
|
+
}
|
|
1300
|
+
// Build the field source + names list once so we can use it for the lost-fields enumeration
|
|
1301
|
+
// AND for the component-path CLI template.
|
|
1302
|
+
const fieldSource = cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST"
|
|
1303
|
+
? cd.nestedData?.[0]?.nestedData || []
|
|
1304
|
+
: cd.nestedData || [];
|
|
1305
|
+
const fieldDescriptions = fieldSource
|
|
1306
|
+
.filter((f) => f.key)
|
|
1307
|
+
.map((f) => `\`${f.key}\` (${f.type || "?"})`);
|
|
1308
|
+
parts.push(`### Prop \`${p.name || "?"}\` → customData \`${cdName}\``);
|
|
1309
|
+
parts.push("");
|
|
1310
|
+
parts.push(`**Shape:** \`${shape}\``);
|
|
1311
|
+
parts.push("");
|
|
1312
|
+
if (shapeKind === "enum") {
|
|
1313
|
+
parts.push(`**Default: enum prop.** Flat scalar set; use \`config add-enum\`.`);
|
|
1314
|
+
parts.push("");
|
|
1315
|
+
const enumName = cd.typescriptName ||
|
|
1316
|
+
(cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
|
|
1317
|
+
const enumOptions = (cd.enumOptions || []).reduce((acc, o) => {
|
|
1318
|
+
if (o.displayName && o.value)
|
|
1319
|
+
acc[o.displayName] = o.value;
|
|
1320
|
+
return acc;
|
|
1321
|
+
}, {});
|
|
1322
|
+
parts.push("```bash");
|
|
1323
|
+
parts.push(`npx ikas-component config add-enum --name "${enumName}" --options '${JSON.stringify(enumOptions)}'`);
|
|
1324
|
+
parts.push("```");
|
|
1325
|
+
parts.push("");
|
|
1326
|
+
parts.push(`If you believe this should be a component instead (e.g. each option secretly carries richer data not visible in the customData shape), see \`get_migration_guide("custom-data-conversion")\`.`);
|
|
1327
|
+
parts.push("");
|
|
1328
|
+
}
|
|
1329
|
+
else if (shapeKind === "list" || shapeKind === "record") {
|
|
1330
|
+
const fieldCount = fieldDescriptions.length;
|
|
1331
|
+
const isMinimal = fieldCount > 0 && fieldCount <= 2;
|
|
1332
|
+
if (isMinimal) {
|
|
1333
|
+
parts.push(`⚠️ **This child would have only ${fieldCount} field${fieldCount === 1 ? "" : "s"}** (${fieldDescriptions.join(", ")}). \`COMPONENT_LIST\` is usually overkill at this size. **Prefer one of:**`);
|
|
1334
|
+
parts.push(`- repeated scalar props on the parent (\`title1\`/\`link1\`, \`title2\`/\`link2\`, …) for a small fixed count`);
|
|
1335
|
+
parts.push(`- a domain LIST prop type (\`LIST_OF_LINK\`, \`IMAGE_LIST\`, \`PRODUCT_LIST\`, …) when each item IS one domain object`);
|
|
1336
|
+
parts.push(`- \`COMPONENT_LIST\` (CLI command below) only if reordering in the editor is a real UX win`);
|
|
1337
|
+
parts.push("");
|
|
1338
|
+
parts.push(`See \`get_migration_guide("component-composition-decision-guide")\` for the full tree. Log your choice in MIGRATION.md → \`## Custom Data Decisions\`.`);
|
|
1339
|
+
parts.push("");
|
|
1340
|
+
}
|
|
1341
|
+
else {
|
|
1342
|
+
parts.push(`**Default: component + COMPONENT_LIST.** Multiple fields per item — a single enum value cannot carry this structure.`);
|
|
1343
|
+
parts.push("");
|
|
1344
|
+
if (fieldCount > 0) {
|
|
1345
|
+
parts.push(`⚠️ **Fields you would lose if you flatten this:** ${fieldDescriptions.join(", ")}. Flattening to a simpler prop type drops these from the editor UI permanently. **Do not "simplify for later"** — if the feature genuinely isn't wanted, log that explicitly in MIGRATION.md → \`## Notes\` with reasoning. Otherwise build the component.`);
|
|
1346
|
+
parts.push("");
|
|
1347
|
+
}
|
|
1348
|
+
parts.push(`> See \`get_migration_guide("component-composition-decision-guide")\` for when \`COMPONENT_LIST\` is overkill.`);
|
|
1349
|
+
parts.push("");
|
|
1350
|
+
}
|
|
1351
|
+
const compName = cd.typescriptName ||
|
|
1352
|
+
(cd.name
|
|
1353
|
+
? cd.name.replace(/[^a-zA-Z0-9]/g, "")
|
|
1354
|
+
: `${sectionPascal}Item`);
|
|
1355
|
+
const compPropsForCli = [];
|
|
1356
|
+
for (const f of fieldSource) {
|
|
1357
|
+
if (!f.key)
|
|
1358
|
+
continue;
|
|
1359
|
+
let fType = f.type;
|
|
1360
|
+
if (fType === "SLIDER")
|
|
1361
|
+
fType = "NUMBER";
|
|
1362
|
+
else if (fType === "PRODUCT_DETAIL")
|
|
1363
|
+
fType = "PRODUCT";
|
|
1364
|
+
compPropsForCli.push({
|
|
1365
|
+
name: f.key,
|
|
1366
|
+
displayName: f.name || f.key,
|
|
1367
|
+
type: fType,
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
parts.push("```bash");
|
|
1371
|
+
if (isMinimal) {
|
|
1372
|
+
parts.push(`# Fallback: COMPONENT_LIST (use only if the simpler alternatives above don't fit)`);
|
|
1373
|
+
}
|
|
1374
|
+
parts.push(`npx ikas-component config add-component --name "${compName}" --type component --props '${JSON.stringify(compPropsForCli)}'`);
|
|
1375
|
+
parts.push(`# Then on the parent, set the prop's filteredComponentIds to the new component's id.`);
|
|
1376
|
+
parts.push("```");
|
|
1377
|
+
parts.push("");
|
|
1378
|
+
parts.push(`If you believe this should be an enum despite the structure, see \`get_migration_guide("custom-data-conversion")\`.`);
|
|
1379
|
+
parts.push("");
|
|
1380
|
+
}
|
|
1381
|
+
else {
|
|
1382
|
+
parts.push(`**Unable to classify automatically — you decide.** See \`get_migration_guide("custom-data-conversion")\` for the enum-vs-component heuristic.`);
|
|
1383
|
+
parts.push("");
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1065
1387
|
// Enums to create first
|
|
1066
1388
|
if (enumsNeeded.length > 0) {
|
|
1067
1389
|
parts.push(`## 3. Create Enums FIRST (if not already done)`);
|
|
@@ -1074,15 +1396,18 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1074
1396
|
parts.push(`Save the returned \`enumId\` values and substitute them in the parent config JSON below.`);
|
|
1075
1397
|
parts.push("");
|
|
1076
1398
|
}
|
|
1077
|
-
// Child CLI commands — dedupe by
|
|
1399
|
+
// Child CLI commands — dedupe by childName (same shape may be referenced by multiple parent props)
|
|
1078
1400
|
const uniqueChildren = new Map();
|
|
1079
1401
|
for (const ch of children) {
|
|
1080
|
-
const existing = uniqueChildren.get(ch.
|
|
1402
|
+
const existing = uniqueChildren.get(ch.childName);
|
|
1081
1403
|
if (existing) {
|
|
1082
1404
|
existing.usedByProps.push(ch.propName);
|
|
1083
1405
|
}
|
|
1084
1406
|
else {
|
|
1085
|
-
uniqueChildren.set(ch.
|
|
1407
|
+
uniqueChildren.set(ch.childName, {
|
|
1408
|
+
child: ch,
|
|
1409
|
+
usedByProps: [ch.propName],
|
|
1410
|
+
});
|
|
1086
1411
|
}
|
|
1087
1412
|
}
|
|
1088
1413
|
if (uniqueChildren.size > 0) {
|
|
@@ -1090,17 +1415,19 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1090
1415
|
parts.push("");
|
|
1091
1416
|
parts.push(`These children are referenced by the parent's COMPONENT_LIST props. Create them before the parent.`);
|
|
1092
1417
|
parts.push(`**Deduped:** run each \`add-component\` command ONCE even if multiple parent props reference the same child.`);
|
|
1418
|
+
parts.push(`**Capture each \`componentId\` from the CLI's JSON response** — you'll substitute these ids for the \`<id-of-{ChildName}>\` placeholders in the parent's \`filteredComponentIds\` below.`);
|
|
1093
1419
|
parts.push("");
|
|
1094
1420
|
for (const { child: ch, usedByProps } of uniqueChildren.values()) {
|
|
1095
|
-
parts.push(`### \`${ch.
|
|
1421
|
+
parts.push(`### \`${ch.childName}\``);
|
|
1096
1422
|
const propsLabel = usedByProps.length > 1
|
|
1097
|
-
? `Used by parent props: ${usedByProps.map(p => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
|
|
1423
|
+
? `Used by parent props: ${usedByProps.map((p) => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
|
|
1098
1424
|
: `For parent prop: \`${usedByProps[0]}\``;
|
|
1099
1425
|
parts.push(propsLabel);
|
|
1100
1426
|
parts.push(`Old customData: "${ch.customDataName}"`);
|
|
1101
1427
|
parts.push("");
|
|
1102
1428
|
parts.push(`\`\`\`bash`);
|
|
1103
1429
|
parts.push(`npx ikas-component config add-component --name "${ch.childName}" --type component --props '${JSON.stringify(ch.childPropsJson)}'`);
|
|
1430
|
+
parts.push(`# → { "success": true, "componentId": "<capture-this>", ... }`);
|
|
1104
1431
|
parts.push(`\`\`\``);
|
|
1105
1432
|
parts.push("");
|
|
1106
1433
|
}
|
|
@@ -1116,6 +1443,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1116
1443
|
parts.push(`\`\`\``);
|
|
1117
1444
|
parts.push("");
|
|
1118
1445
|
parts.push(`**IMPORTANT:** The CLI auto-generates \`types.ts\`. DO NOT manually create or edit \`types.ts\`.`);
|
|
1446
|
+
if (children.length > 0) {
|
|
1447
|
+
parts.push("");
|
|
1448
|
+
parts.push(`**Before running the command above:** replace each \`<id-of-{ChildName}>\` placeholder in the \`--props\` JSON with the real \`componentId\` captured from the corresponding child's \`add-component\` response (or from \`npx ikas-component config list\`). Component ids are opaque random strings — the CLI will reject any unknown id with a structured error.`);
|
|
1449
|
+
}
|
|
1119
1450
|
parts.push("");
|
|
1120
1451
|
// Implementation guidance
|
|
1121
1452
|
const nextStep = parentStep + 1;
|
|
@@ -1132,15 +1463,18 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1132
1463
|
if (children.length > 0) {
|
|
1133
1464
|
parts.push(`- For COMPONENT_LIST props, use \`<IkasComponentRenderer id="..." components={list as any[]} parentProps={props} />\``);
|
|
1134
1465
|
parts.push(`- **Remember: the parent cannot read child prop values.** Any per-item logic must live in the child component.`);
|
|
1135
|
-
parts.push(`- **Every COMPONENT_LIST slot needs a child component to render individual items.** A product list needs a ProductCard child, a blog list needs a BlogCard child, etc. Check if the child already exists
|
|
1466
|
+
parts.push(`- **Every COMPONENT_LIST slot needs a child component to render individual items.** A product list needs a ProductCard child, a blog list needs a BlogCard child, etc. Check if the child already exists (run \`config list\` to see all components and their opaque ids; reuse the existing id in \`filteredComponentIds\`). If not, create it as a registered component and use the \`componentId\` from the CLI's response.`);
|
|
1136
1467
|
}
|
|
1137
1468
|
// Check if the section itself has data-driven list props (PRODUCT_LIST, BLOG_LIST, CATEGORY_LIST)
|
|
1138
|
-
const dataListProps = (target.props || []).filter(p => p.type === "PRODUCT_LIST" ||
|
|
1469
|
+
const dataListProps = (target.props || []).filter((p) => p.type === "PRODUCT_LIST" ||
|
|
1470
|
+
p.type === "BLOG_LIST" ||
|
|
1471
|
+
p.type === "CATEGORY_LIST" ||
|
|
1472
|
+
p.type === "BRAND_LIST");
|
|
1139
1473
|
if (dataListProps.length > 0) {
|
|
1140
1474
|
parts.push("");
|
|
1141
1475
|
parts.push(`### Data-Driven List Rendering`);
|
|
1142
1476
|
parts.push("");
|
|
1143
|
-
parts.push(`This section has data-driven list props: ${dataListProps.map(p => `\`${p.name}\` (${p.type})`).join(", ")}.`);
|
|
1477
|
+
parts.push(`This section has data-driven list props: ${dataListProps.map((p) => `\`${p.name}\` (${p.type})`).join(", ")}.`);
|
|
1144
1478
|
parts.push(`These are NOT COMPONENT_LIST — the data comes from dynamic queries (filters, categories, search), not hand-picked items. Render items **internally** by mapping over the data:`);
|
|
1145
1479
|
parts.push("");
|
|
1146
1480
|
parts.push("```tsx");
|
|
@@ -1167,9 +1501,22 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1167
1501
|
parts.push(`See \`get_migration_guide("custom-data-conversion")\` → "Two Ways to Render Lists" for the full pattern.`);
|
|
1168
1502
|
}
|
|
1169
1503
|
// Detect form-page sections (0 or few props, name suggests a form/auth page)
|
|
1170
|
-
const formKeywords = [
|
|
1504
|
+
const formKeywords = [
|
|
1505
|
+
"login",
|
|
1506
|
+
"register",
|
|
1507
|
+
"forgot",
|
|
1508
|
+
"recover",
|
|
1509
|
+
"password",
|
|
1510
|
+
"account",
|
|
1511
|
+
"email",
|
|
1512
|
+
"verification",
|
|
1513
|
+
"activate",
|
|
1514
|
+
"contact",
|
|
1515
|
+
"checkout",
|
|
1516
|
+
"address",
|
|
1517
|
+
];
|
|
1171
1518
|
const lowerDir = (target.dir || "").toLowerCase();
|
|
1172
|
-
const isLikelyFormPage = formKeywords.some(kw => lowerDir.includes(kw));
|
|
1519
|
+
const isLikelyFormPage = formKeywords.some((kw) => lowerDir.includes(kw));
|
|
1173
1520
|
if (isLikelyFormPage) {
|
|
1174
1521
|
parts.push("");
|
|
1175
1522
|
parts.push(`### Form Page Pattern`);
|
|
@@ -1198,7 +1545,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1198
1545
|
// Fallback heuristic only when source scan unavailable
|
|
1199
1546
|
const heuristicLibs = [];
|
|
1200
1547
|
const lowerName = oldName.toLowerCase();
|
|
1201
|
-
if (lowerName.includes("slider") ||
|
|
1548
|
+
if (lowerName.includes("slider") ||
|
|
1549
|
+
lowerName.includes("carousel") ||
|
|
1550
|
+
lowerName.includes("banner")) {
|
|
1202
1551
|
heuristicLibs.push("swiper");
|
|
1203
1552
|
}
|
|
1204
1553
|
if (lowerName.includes("marquee"))
|
|
@@ -1207,7 +1556,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1207
1556
|
heuristicLibs.push("react-player");
|
|
1208
1557
|
if (lowerName.includes("chart"))
|
|
1209
1558
|
heuristicLibs.push("recharts");
|
|
1210
|
-
if (lowerName.includes("star") ||
|
|
1559
|
+
if (lowerName.includes("star") ||
|
|
1560
|
+
lowerName.includes("rating") ||
|
|
1561
|
+
lowerName.includes("review"))
|
|
1211
1562
|
heuristicLibs.push("react-simple-star-rating");
|
|
1212
1563
|
if (heuristicLibs.length > 0) {
|
|
1213
1564
|
parts.push(`### Likely Library Replacements (heuristic — source not scanned)`);
|
|
@@ -1221,30 +1572,19 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
|
|
|
1221
1572
|
parts.push("");
|
|
1222
1573
|
}
|
|
1223
1574
|
}
|
|
1224
|
-
// Relevant guides
|
|
1225
|
-
parts.push(`## ${nextStep + 1}. Relevant Guides
|
|
1575
|
+
// Relevant guides — keep terse; LLM can call get_migration_guide("list") for the full catalog
|
|
1576
|
+
parts.push(`## ${nextStep + 1}. Relevant Guides`);
|
|
1226
1577
|
parts.push("");
|
|
1227
1578
|
parts.push(`- \`get_migration_guide("react-to-preact")\` — code conversion patterns`);
|
|
1228
|
-
parts.push(`- \`get_migration_guide("
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
parts.push(`- \`get_migration_guide("component-renderer-limitations")\` — critical COMPONENT_LIST constraints`);
|
|
1232
|
-
parts.push(`- \`get_framework_guide("component-renderer-patterns")\` — full IkasComponentRenderer usage`);
|
|
1233
|
-
parts.push(`- \`get_migration_example("custom-dynamic-list-to-component-list")\` — concrete example`);
|
|
1234
|
-
}
|
|
1235
|
-
if (target.isHeader || target.isFooter) {
|
|
1236
|
-
parts.push(`- \`get_framework_guide("header-footer-patterns")\` — header/footer specifics`);
|
|
1579
|
+
parts.push(`- \`get_migration_guide("prop-runtime-shapes")\` — exact runtime shapes (\`.data\` vs \`.links\`, etc.)`);
|
|
1580
|
+
if (children.length > 0 || target.isHeader || target.isFooter) {
|
|
1581
|
+
parts.push(`- \`get_framework_guide("header-footer-patterns")\` — COMPONENT_LIST + IkasComponentRenderer wiring`);
|
|
1237
1582
|
}
|
|
1238
|
-
parts.push(`- \`get_framework_guide("common-pitfalls")\` — observer rules, common mistakes`);
|
|
1239
1583
|
parts.push("");
|
|
1240
1584
|
// Completion
|
|
1241
1585
|
parts.push(`## ${nextStep + 2}. Mark Complete`);
|
|
1242
1586
|
parts.push("");
|
|
1243
|
-
parts.push(`Once the section builds cleanly with \`npx ikas-component build
|
|
1244
|
-
parts.push(`1. Edit \`MIGRATION.md\` at the project root`);
|
|
1245
|
-
parts.push(`2. Change the checkbox for \`${sectionId}\` from \`[ ]\` to \`[x]\``);
|
|
1246
|
-
parts.push(`3. Also mark each child component as \`[x]\``);
|
|
1247
|
-
parts.push(`4. Append an entry to the **Session Log** section of \`MIGRATION.md\` noting: libraries replaced, any props added beyond the generated plan, the actual folder name the CLI created (in case it differs from the name you passed — e.g., \`FAQ\` → \`Faq/\`), and any other decisions a future agent should know.`);
|
|
1587
|
+
parts.push(`Once the section builds cleanly with \`npx ikas-component build\`: edit MIGRATION.md → tick \`[x]\` for \`${sectionId}\` and each child component, log any customData decisions under \`## Custom Data Decisions\`, and append a brief entry to \`## Notes\` with anything future sessions should know.`);
|
|
1248
1588
|
parts.push("");
|
|
1249
1589
|
return parts.join("\n");
|
|
1250
1590
|
}
|
|
@@ -1270,18 +1610,81 @@ function matchScore(text, query) {
|
|
|
1270
1610
|
}
|
|
1271
1611
|
return score;
|
|
1272
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
|
+
}
|
|
1273
1660
|
function searchFunctions(query) {
|
|
1274
1661
|
const scored = storefrontData.functions
|
|
1275
1662
|
.map((fn) => {
|
|
1276
1663
|
const nameScore = matchScore(fn.name, query) * 3;
|
|
1664
|
+
const displayNameScore = fn.displayName
|
|
1665
|
+
? matchScore(fn.displayName, query) * 3
|
|
1666
|
+
: 0;
|
|
1277
1667
|
const descScore = matchScore(fn.description, query);
|
|
1278
|
-
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;
|
|
1279
1671
|
const paramScore = fn.params.some((p) => matchScore(p.name, query) > 0 || matchScore(p.description, query) > 0)
|
|
1280
1672
|
? 2
|
|
1281
1673
|
: 0;
|
|
1282
1674
|
const sigScore = matchScore(fn.signature, query) * 2;
|
|
1283
|
-
const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0)
|
|
1284
|
-
|
|
1675
|
+
const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0)
|
|
1676
|
+
? 8
|
|
1677
|
+
: 0;
|
|
1678
|
+
return {
|
|
1679
|
+
fn,
|
|
1680
|
+
score: nameScore +
|
|
1681
|
+
displayNameScore +
|
|
1682
|
+
descScore +
|
|
1683
|
+
catScore +
|
|
1684
|
+
paramScore +
|
|
1685
|
+
sigScore +
|
|
1686
|
+
typeScore,
|
|
1687
|
+
};
|
|
1285
1688
|
})
|
|
1286
1689
|
.filter((item) => item.score > 0)
|
|
1287
1690
|
.sort((a, b) => b.score - a.score);
|
|
@@ -1294,7 +1697,11 @@ function searchFrameworkTopics(query) {
|
|
|
1294
1697
|
const descScore = matchScore(topic.description, query) * 2;
|
|
1295
1698
|
const contentScore = matchScore(topic.content, query);
|
|
1296
1699
|
const tagScore = topic.tags.some((t) => matchScore(t, query) > 0) ? 5 : 0;
|
|
1297
|
-
return {
|
|
1700
|
+
return {
|
|
1701
|
+
key,
|
|
1702
|
+
topic,
|
|
1703
|
+
score: titleScore + descScore + contentScore + tagScore,
|
|
1704
|
+
};
|
|
1298
1705
|
})
|
|
1299
1706
|
.filter((item) => item.score > 0)
|
|
1300
1707
|
.sort((a, b) => b.score - a.score);
|
|
@@ -1309,7 +1716,9 @@ function searchTypes(query) {
|
|
|
1309
1716
|
const propScore = td.properties?.some((p) => matchScore(p.name, query) > 0 || matchScore(p.type, query) > 0)
|
|
1310
1717
|
? 4
|
|
1311
1718
|
: 0;
|
|
1312
|
-
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;
|
|
1313
1722
|
return { td, score: nameScore + domainScore + propScore + enumScore };
|
|
1314
1723
|
})
|
|
1315
1724
|
.filter((item) => item.score > 0)
|
|
@@ -1351,8 +1760,12 @@ function formatFunctionDoc(fn) {
|
|
|
1351
1760
|
return lines.join("\n");
|
|
1352
1761
|
}
|
|
1353
1762
|
function formatFunctionSummary(fn) {
|
|
1354
|
-
const desc = fn.description
|
|
1355
|
-
|
|
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
|
+
: "";
|
|
1356
1769
|
return `- \`${fn.name}\`${alias} - ${desc}`;
|
|
1357
1770
|
}
|
|
1358
1771
|
function formatTypeDefinition(td, opts = {}) {
|
|
@@ -1393,7 +1806,9 @@ function formatTypeDefinition(td, opts = {}) {
|
|
|
1393
1806
|
const fn = storefrontData.functions.find((f) => f.name === fnName);
|
|
1394
1807
|
if (fn) {
|
|
1395
1808
|
const desc = fn.description ? fn.description.split(".")[0] + "." : "";
|
|
1396
|
-
const alias = fn.displayName && fn.displayName !== fn.name
|
|
1809
|
+
const alias = fn.displayName && fn.displayName !== fn.name
|
|
1810
|
+
? ` (alias: ${fn.displayName})`
|
|
1811
|
+
: "";
|
|
1397
1812
|
lines.push(`- **\`${fn.name}\`**${alias} — ${desc}`);
|
|
1398
1813
|
lines.push(` \`${fn.signature}\``);
|
|
1399
1814
|
}
|
|
@@ -1402,7 +1817,9 @@ function formatTypeDefinition(td, opts = {}) {
|
|
|
1402
1817
|
}
|
|
1403
1818
|
}
|
|
1404
1819
|
lines.push("");
|
|
1405
|
-
lines.push(
|
|
1820
|
+
lines.push('Use `get_functions_for_type("' +
|
|
1821
|
+
td.name +
|
|
1822
|
+
'")` for full documentation of these functions.');
|
|
1406
1823
|
}
|
|
1407
1824
|
return lines.join("\n");
|
|
1408
1825
|
}
|
|
@@ -1420,7 +1837,13 @@ const server = new McpServer({
|
|
|
1420
1837
|
name: "ikas-code-components",
|
|
1421
1838
|
version: "0.1.0",
|
|
1422
1839
|
}, {
|
|
1423
|
-
instructions: "Examples and section templates from this server are API reference only — reuse imports, function calls, and data-access patterns; create your own JSX structure, CSS class names, and visual design
|
|
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 a section on a page; `list_page_sections` → the sections already placed on a page, each with its per-placement `elementId`, current `propValues`, and blueprint `props`; `update_section_prop` → change a single prop value of a placed section; `upload_image` → upload an image (file or URL) and get an image id to use in an IMAGE prop value.\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.",
|
|
1424
1847
|
});
|
|
1425
1848
|
// Tool: search_docs
|
|
1426
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 }) => {
|
|
@@ -1461,39 +1884,79 @@ server.tool("search_docs", "Search across all ikas storefront API docs, framewor
|
|
|
1461
1884
|
parts.push("");
|
|
1462
1885
|
parts.push("Use `get_migration_guide(topic)` to get full content for any migration topic.");
|
|
1463
1886
|
}
|
|
1464
|
-
if (functions.length === 0 &&
|
|
1887
|
+
if (functions.length === 0 &&
|
|
1888
|
+
topics.length === 0 &&
|
|
1889
|
+
types.length === 0 &&
|
|
1890
|
+
migrationTopics.length === 0) {
|
|
1465
1891
|
parts.push(`No results found for "${query}". Try different keywords or use \`list_functions()\` to see all available functions.`);
|
|
1466
1892
|
}
|
|
1467
1893
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1468
1894
|
});
|
|
1469
1895
|
// Tool: get_function_doc
|
|
1470
|
-
server.tool("get_function_doc", "Get full documentation for a specific storefront API function including signature, parameters, return type, and example.", {
|
|
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 }) => {
|
|
1471
1901
|
const nameLower = name.toLowerCase();
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1902
|
+
// Phase 1: canonical-name match wins. A real function name always outranks
|
|
1903
|
+
// any displayName alias so aliases can never shadow the function they're
|
|
1904
|
+
// named after (e.g. `hasCustomer` the function vs. `hasIkasOrderCustomer`'s
|
|
1905
|
+
// [BP-DISPLAY-NAME: hasCustomer] alias).
|
|
1906
|
+
const byName = storefrontData.functions.find((f) => f.name.toLowerCase() === nameLower);
|
|
1907
|
+
if (byName) {
|
|
1908
|
+
return {
|
|
1909
|
+
content: [{ type: "text", text: formatFunctionDoc(byName) }],
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
// Phase 2: fall back to displayName aliases.
|
|
1913
|
+
const byAlias = storefrontData.functions.filter((f) => f.displayName && f.displayName.toLowerCase() === nameLower);
|
|
1914
|
+
if (byAlias.length === 1) {
|
|
1915
|
+
const fn = byAlias[0];
|
|
1916
|
+
const note = `> Note: "${name}" is a display alias for \`${fn.name}\`.\n\n`;
|
|
1917
|
+
return {
|
|
1918
|
+
content: [
|
|
1919
|
+
{ type: "text", text: note + formatFunctionDoc(fn) },
|
|
1920
|
+
],
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
if (byAlias.length > 1) {
|
|
1924
|
+
const lines = [];
|
|
1925
|
+
lines.push(`"${name}" is an ambiguous display alias used by ${byAlias.length} functions. Call \`get_function_doc\` again with the canonical name of the one you want:`);
|
|
1926
|
+
lines.push("");
|
|
1927
|
+
for (const fn of byAlias) {
|
|
1928
|
+
lines.push(formatFunctionSummary(fn));
|
|
1929
|
+
lines.push(` \`${fn.signature}\``);
|
|
1491
1930
|
}
|
|
1931
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1932
|
+
}
|
|
1933
|
+
// No exact match. Try fuzzy substring match across name + displayName.
|
|
1934
|
+
const matches = storefrontData.functions.filter((f) => f.name.toLowerCase().includes(nameLower) ||
|
|
1935
|
+
(f.displayName && f.displayName.toLowerCase().includes(nameLower)));
|
|
1936
|
+
if (matches.length > 0) {
|
|
1937
|
+
const suggestions = matches.slice(0, 5).map((f) => {
|
|
1938
|
+
const alias = f.displayName && f.displayName !== f.name
|
|
1939
|
+
? ` (alias: ${f.displayName})`
|
|
1940
|
+
: "";
|
|
1941
|
+
return ` - ${f.name}${alias}`;
|
|
1942
|
+
});
|
|
1492
1943
|
return {
|
|
1493
|
-
content: [
|
|
1944
|
+
content: [
|
|
1945
|
+
{
|
|
1946
|
+
type: "text",
|
|
1947
|
+
text: `Function "${name}" not found. Did you mean:\n${suggestions.join("\n")}`,
|
|
1948
|
+
},
|
|
1949
|
+
],
|
|
1494
1950
|
};
|
|
1495
1951
|
}
|
|
1496
|
-
return {
|
|
1952
|
+
return {
|
|
1953
|
+
content: [
|
|
1954
|
+
{
|
|
1955
|
+
type: "text",
|
|
1956
|
+
text: `Function "${name}" not found. Use \`list_functions()\` to see all available functions.`,
|
|
1957
|
+
},
|
|
1958
|
+
],
|
|
1959
|
+
};
|
|
1497
1960
|
});
|
|
1498
1961
|
// Tool: list_functions
|
|
1499
1962
|
server.tool("list_functions", "List storefront API functions. Without a `category`, returns category names + counts so you can drill in. With a `category`, returns one-line summaries for that category. Use `limit`/`offset` to paginate.", {
|
|
@@ -1542,7 +2005,7 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
|
|
|
1542
2005
|
if (uncategorized > 0) {
|
|
1543
2006
|
lines.push(`- \`Other\` (${uncategorized})`);
|
|
1544
2007
|
}
|
|
1545
|
-
lines.push("",
|
|
2008
|
+
lines.push("", 'Call `list_functions(category: "<name>")` to see one-line summaries for a category.');
|
|
1546
2009
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1547
2010
|
}
|
|
1548
2011
|
const catLower = category.toLowerCase();
|
|
@@ -1574,7 +2037,11 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
|
|
|
1574
2037
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1575
2038
|
});
|
|
1576
2039
|
// Tool: get_code_example
|
|
1577
|
-
server.tool("get_code_example", "Get an API usage reference for a specific task. Shows correct function calls, imports, and data-handling patterns from a real production theme. The JSX layout and CSS are illustrative only — create your own original visual design. Call `list_examples()` to see available example IDs.", {
|
|
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 }) => {
|
|
1578
2045
|
const taskLower = task.toLowerCase();
|
|
1579
2046
|
// Try exact ID match first
|
|
1580
2047
|
let example = storefrontData.codeExamples.find((e) => e.id === taskLower);
|
|
@@ -1594,7 +2061,9 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
1594
2061
|
}
|
|
1595
2062
|
}
|
|
1596
2063
|
if (!example) {
|
|
1597
|
-
const available = storefrontData.codeExamples
|
|
2064
|
+
const available = storefrontData.codeExamples
|
|
2065
|
+
.map((e) => ` - \`${e.id}\` - ${e.title}`)
|
|
2066
|
+
.join("\n");
|
|
1598
2067
|
return {
|
|
1599
2068
|
content: [
|
|
1600
2069
|
{
|
|
@@ -1617,14 +2086,24 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
1617
2086
|
if (example.files && example.files.length > 0) {
|
|
1618
2087
|
for (const file of example.files) {
|
|
1619
2088
|
const ext = file.filename.split(".").pop() || "text";
|
|
1620
|
-
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";
|
|
1621
2096
|
// Add inline originality comments to CSS and TSX files
|
|
1622
2097
|
let content = file.content;
|
|
1623
2098
|
if (ext === "css") {
|
|
1624
|
-
content =
|
|
2099
|
+
content =
|
|
2100
|
+
"/* EXAMPLE STYLING — create your own original CSS with different class names and design */\n" +
|
|
2101
|
+
content;
|
|
1625
2102
|
}
|
|
1626
2103
|
else if (ext === "tsx") {
|
|
1627
|
-
content =
|
|
2104
|
+
content =
|
|
2105
|
+
"// EXAMPLE COMPONENT — use the API patterns but create your own JSX structure and layout\n" +
|
|
2106
|
+
content;
|
|
1628
2107
|
}
|
|
1629
2108
|
parts.push(`### ${file.filename}`, "", `\`\`\`${lang}`, content, "```", "");
|
|
1630
2109
|
}
|
|
@@ -1638,63 +2117,88 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
|
|
|
1638
2117
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
1639
2118
|
});
|
|
1640
2119
|
// Tool: get_framework_guide
|
|
1641
|
-
server.tool("get_framework_guide", "Get a framework guide on a specific topic (e.g. 'ai-workflow', 'common-pitfalls', 'prop-types', 'css-scoping', 'form-handling'). Call `list_topics()` to see all available topic keys.", {
|
|
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 }) => {
|
|
1642
2125
|
const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
|
|
1643
2126
|
// Alias mapping for common alternative topic names
|
|
1644
2127
|
const topicAliases = {
|
|
1645
2128
|
"form-handling": "form-patterns",
|
|
1646
|
-
|
|
2129
|
+
forms: "form-patterns",
|
|
1647
2130
|
"data-fetching": "async-data-patterns",
|
|
1648
|
-
|
|
1649
|
-
|
|
2131
|
+
async: "async-data-patterns",
|
|
2132
|
+
loading: "async-data-patterns",
|
|
1650
2133
|
"sub-components": "sub-component-patterns",
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
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",
|
|
1664
2147
|
"product-detail": "product-detail-patterns",
|
|
1665
2148
|
"product-list": "product-list-patterns",
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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",
|
|
1673
2156
|
"global-styles": "global-css",
|
|
1674
|
-
|
|
2157
|
+
global: "global-css",
|
|
1675
2158
|
"css-variables": "global-css",
|
|
1676
2159
|
"custom-properties": "global-css",
|
|
1677
2160
|
};
|
|
1678
2161
|
const resolvedTopic = topicAliases[topicLower] || topicLower;
|
|
1679
2162
|
// Topics that involve MobX store reads get a reminder about root reactivity
|
|
1680
2163
|
const storeTopics = new Set([
|
|
1681
|
-
"product-detail-patterns",
|
|
1682
|
-
"
|
|
1683
|
-
"
|
|
1684
|
-
"
|
|
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",
|
|
1685
2175
|
]);
|
|
1686
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";
|
|
1687
2177
|
// Try exact key match (with alias resolution)
|
|
1688
2178
|
if (frameworkData.topics[resolvedTopic]) {
|
|
1689
2179
|
const t = frameworkData.topics[resolvedTopic];
|
|
1690
2180
|
const prefix = storeTopics.has(resolvedTopic) ? observerReminder : "";
|
|
1691
|
-
return {
|
|
2181
|
+
return {
|
|
2182
|
+
content: [
|
|
2183
|
+
{
|
|
2184
|
+
type: "text",
|
|
2185
|
+
text: `## ${t.title}\n\n${prefix}${t.content}`,
|
|
2186
|
+
},
|
|
2187
|
+
],
|
|
2188
|
+
};
|
|
1692
2189
|
}
|
|
1693
2190
|
// Try original topic key (without alias) in case it's a direct key
|
|
1694
2191
|
if (resolvedTopic !== topicLower && frameworkData.topics[topicLower]) {
|
|
1695
2192
|
const t = frameworkData.topics[topicLower];
|
|
1696
2193
|
const prefix = storeTopics.has(topicLower) ? observerReminder : "";
|
|
1697
|
-
return {
|
|
2194
|
+
return {
|
|
2195
|
+
content: [
|
|
2196
|
+
{
|
|
2197
|
+
type: "text",
|
|
2198
|
+
text: `## ${t.title}\n\n${prefix}${t.content}`,
|
|
2199
|
+
},
|
|
2200
|
+
],
|
|
2201
|
+
};
|
|
1698
2202
|
}
|
|
1699
2203
|
// Try keyword search
|
|
1700
2204
|
const matches = searchFrameworkTopics(topic);
|
|
@@ -1702,7 +2206,12 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
|
|
|
1702
2206
|
const best = matches[0];
|
|
1703
2207
|
const prefix = storeTopics.has(best.key) ? observerReminder : "";
|
|
1704
2208
|
return {
|
|
1705
|
-
content: [
|
|
2209
|
+
content: [
|
|
2210
|
+
{
|
|
2211
|
+
type: "text",
|
|
2212
|
+
text: `## ${best.topic.title}\n\n${prefix}${best.topic.content}`,
|
|
2213
|
+
},
|
|
2214
|
+
],
|
|
1706
2215
|
};
|
|
1707
2216
|
}
|
|
1708
2217
|
const available = Object.entries(frameworkData.topics)
|
|
@@ -1718,16 +2227,27 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
|
|
|
1718
2227
|
};
|
|
1719
2228
|
});
|
|
1720
2229
|
// Tool: get_type_definition
|
|
1721
|
-
server.tool("get_type_definition", "Get the full definition of a storefront type or enum by name (e.g. 'IkasProduct', 'IkasOrderStatus'). 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 }) => {
|
|
1722
2235
|
if (!typesData) {
|
|
1723
2236
|
return {
|
|
1724
|
-
content: [
|
|
2237
|
+
content: [
|
|
2238
|
+
{
|
|
2239
|
+
type: "text",
|
|
2240
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2241
|
+
},
|
|
2242
|
+
],
|
|
1725
2243
|
};
|
|
1726
2244
|
}
|
|
1727
2245
|
const nameLower = name.toLowerCase();
|
|
1728
2246
|
const td = typesData.types.find((t) => t.name.toLowerCase() === nameLower);
|
|
1729
2247
|
if (td) {
|
|
1730
|
-
return {
|
|
2248
|
+
return {
|
|
2249
|
+
content: [{ type: "text", text: formatTypeDefinition(td) }],
|
|
2250
|
+
};
|
|
1731
2251
|
}
|
|
1732
2252
|
// Fuzzy match
|
|
1733
2253
|
const matches = typesData.types.filter((t) => t.name.toLowerCase().includes(nameLower));
|
|
@@ -1743,14 +2263,28 @@ server.tool("get_type_definition", "Get the full definition of a storefront type
|
|
|
1743
2263
|
};
|
|
1744
2264
|
}
|
|
1745
2265
|
return {
|
|
1746
|
-
content: [
|
|
2266
|
+
content: [
|
|
2267
|
+
{
|
|
2268
|
+
type: "text",
|
|
2269
|
+
text: `Type "${name}" not found. Use \`list_types()\` to see all available types.`,
|
|
2270
|
+
},
|
|
2271
|
+
],
|
|
1747
2272
|
};
|
|
1748
2273
|
});
|
|
1749
2274
|
// Tool: get_functions_for_type
|
|
1750
|
-
server.tool("get_functions_for_type", "Get full documentation for all utility functions that operate on a given storefront type. For example, get_functions_for_type('IkasImage') returns getSrc, getDefaultSrc, getThumbnailSrc, createMediaSrcset with full signatures, descriptions, and examples.", {
|
|
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 }) => {
|
|
1751
2280
|
if (!typesData) {
|
|
1752
2281
|
return {
|
|
1753
|
-
content: [
|
|
2282
|
+
content: [
|
|
2283
|
+
{
|
|
2284
|
+
type: "text",
|
|
2285
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2286
|
+
},
|
|
2287
|
+
],
|
|
1754
2288
|
};
|
|
1755
2289
|
}
|
|
1756
2290
|
const nameLower = typeName.toLowerCase();
|
|
@@ -1770,7 +2304,12 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
|
|
|
1770
2304
|
};
|
|
1771
2305
|
}
|
|
1772
2306
|
return {
|
|
1773
|
-
content: [
|
|
2307
|
+
content: [
|
|
2308
|
+
{
|
|
2309
|
+
type: "text",
|
|
2310
|
+
text: `Type "${typeName}" not found. Use \`list_types()\` to see all available types.`,
|
|
2311
|
+
},
|
|
2312
|
+
],
|
|
1774
2313
|
};
|
|
1775
2314
|
}
|
|
1776
2315
|
if (!td.relatedFunctions || td.relatedFunctions.length === 0) {
|
|
@@ -1801,7 +2340,10 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
|
|
|
1801
2340
|
});
|
|
1802
2341
|
// Tool: get_model_guide
|
|
1803
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.", {
|
|
1804
|
-
model: z
|
|
2343
|
+
model: z
|
|
2344
|
+
.string()
|
|
2345
|
+
.optional()
|
|
2346
|
+
.describe("Model type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
|
|
1805
2347
|
name: z.string().optional().describe("Alias for 'model'"),
|
|
1806
2348
|
mode: z
|
|
1807
2349
|
.enum(["summary", "full"])
|
|
@@ -1812,12 +2354,22 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
1812
2354
|
const model = modelParam || nameParam;
|
|
1813
2355
|
if (!model) {
|
|
1814
2356
|
return {
|
|
1815
|
-
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
|
+
],
|
|
1816
2363
|
};
|
|
1817
2364
|
}
|
|
1818
2365
|
if (!typesData) {
|
|
1819
2366
|
return {
|
|
1820
|
-
content: [
|
|
2367
|
+
content: [
|
|
2368
|
+
{
|
|
2369
|
+
type: "text",
|
|
2370
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2371
|
+
},
|
|
2372
|
+
],
|
|
1821
2373
|
};
|
|
1822
2374
|
}
|
|
1823
2375
|
const modelLower = model.toLowerCase();
|
|
@@ -1839,7 +2391,12 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
1839
2391
|
};
|
|
1840
2392
|
}
|
|
1841
2393
|
return {
|
|
1842
|
-
content: [
|
|
2394
|
+
content: [
|
|
2395
|
+
{
|
|
2396
|
+
type: "text",
|
|
2397
|
+
text: `Model "${model}" not found. Use \`list_types()\` to see all available types.`,
|
|
2398
|
+
},
|
|
2399
|
+
],
|
|
1843
2400
|
};
|
|
1844
2401
|
}
|
|
1845
2402
|
const parts = [`# Model Guide: ${td.name}\n`];
|
|
@@ -1947,13 +2504,23 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
|
|
|
1947
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 }) => {
|
|
1948
2505
|
if (!typesData) {
|
|
1949
2506
|
return {
|
|
1950
|
-
content: [
|
|
2507
|
+
content: [
|
|
2508
|
+
{
|
|
2509
|
+
type: "text",
|
|
2510
|
+
text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
|
|
2511
|
+
},
|
|
2512
|
+
],
|
|
1951
2513
|
};
|
|
1952
2514
|
}
|
|
1953
2515
|
const results = searchTypes(query).slice(0, 15);
|
|
1954
2516
|
if (results.length === 0) {
|
|
1955
2517
|
return {
|
|
1956
|
-
content: [
|
|
2518
|
+
content: [
|
|
2519
|
+
{
|
|
2520
|
+
type: "text",
|
|
2521
|
+
text: `No types found matching "${query}". Use \`list_types()\` to see all available types.`,
|
|
2522
|
+
},
|
|
2523
|
+
],
|
|
1957
2524
|
};
|
|
1958
2525
|
}
|
|
1959
2526
|
const parts = [`## Type Search Results for "${query}"\n`];
|
|
@@ -2017,7 +2584,7 @@ server.tool("list_types", "List storefront types and enums. Use `domain` and/or
|
|
|
2017
2584
|
for (const [d, count] of sorted) {
|
|
2018
2585
|
lines.push(`- \`${d}\` (${count})`);
|
|
2019
2586
|
}
|
|
2020
|
-
lines.push("",
|
|
2587
|
+
lines.push("", 'Call `list_types(domain: "<name>")` to see summaries for a domain.');
|
|
2021
2588
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2022
2589
|
}
|
|
2023
2590
|
const domainLower = domain.toLowerCase();
|
|
@@ -2056,15 +2623,25 @@ server.tool("list_types", "List storefront types and enums. Use `domain` and/or
|
|
|
2056
2623
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
2057
2624
|
});
|
|
2058
2625
|
// Tool: get_prop_types
|
|
2059
|
-
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 () => {
|
|
2060
2627
|
const propTypesTopic = frameworkData.topics["prop-types"];
|
|
2061
2628
|
if (propTypesTopic) {
|
|
2062
2629
|
return {
|
|
2063
|
-
content: [
|
|
2630
|
+
content: [
|
|
2631
|
+
{
|
|
2632
|
+
type: "text",
|
|
2633
|
+
text: `## ${propTypesTopic.title}\n\n${propTypesTopic.content}`,
|
|
2634
|
+
},
|
|
2635
|
+
],
|
|
2064
2636
|
};
|
|
2065
2637
|
}
|
|
2066
2638
|
return {
|
|
2067
|
-
content: [
|
|
2639
|
+
content: [
|
|
2640
|
+
{
|
|
2641
|
+
type: "text",
|
|
2642
|
+
text: "Prop types documentation not available.",
|
|
2643
|
+
},
|
|
2644
|
+
],
|
|
2068
2645
|
};
|
|
2069
2646
|
});
|
|
2070
2647
|
// Tool: get_section_template
|
|
@@ -2072,7 +2649,7 @@ server.tool("get_prop_types", "Get all available ikas.config.json prop types wit
|
|
|
2072
2649
|
const sectionTemplateKeys = sectionTemplateNames.length > 0
|
|
2073
2650
|
? sectionTemplateNames
|
|
2074
2651
|
: null;
|
|
2075
|
-
server.tool("get_section_template", "Get the root files of a starter section template (index.tsx, types.ts, styles.css, ikas-config-snippet.json). Returns ONLY the section's root files plus the NAMES of any children, components, sub-components, utilities, and hooks — their files are NOT included by default. To view one item's full implementation, call `get_section_child(section, name, kind)` where kind is 'children' (default), 'components', or 'sub-components'. To bundle subtrees inline, pass `include`. Call `list_section_types()` for available section types. Use the API patterns shown — create your own JSX structure, CSS class names, and visual design.", {
|
|
2652
|
+
server.tool("get_section_template", "Get the root files of a starter section template (index.tsx, types.ts, styles.css, ikas-config-snippet.json). Returns ONLY the section's root files plus the NAMES of any children, components, sub-components, utilities, and hooks — their files are NOT included by default. To view one item's full implementation, call `get_section_child(section, name, kind)` where kind is 'children' (default), 'components', or 'sub-components'. To bundle subtrees inline, pass `include`. Call `list_section_types()` for available section types. Use the API patterns shown — create your own JSX structure, CSS class names, and visual design. **Container sections** (Header, Footer, ProductDetail, etc.) host child components via a `COMPONENT_LIST` slot — the response emits a complete multi-step Setup Recipe (create children → capture ids → wire `filteredComponentIds` via `config update-prop`). Follow all steps; the parent alone produces an empty section.", {
|
|
2076
2653
|
sectionType: z
|
|
2077
2654
|
.string()
|
|
2078
2655
|
.describe("The section type (call `list_section_types()` for valid values)"),
|
|
@@ -2114,11 +2691,19 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2114
2691
|
}
|
|
2115
2692
|
const normalizedType = normalizeName(sectionType);
|
|
2116
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
|
+
}
|
|
2117
2702
|
return {
|
|
2118
2703
|
content: [
|
|
2119
2704
|
{
|
|
2120
2705
|
type: "text",
|
|
2121
|
-
text: `Unknown section type "${sectionType}"
|
|
2706
|
+
text: `Unknown section type "${sectionType}".${suggestionText} Call \`list_section_types()\` to see all ${sectionTemplateNames.length} valid types.`,
|
|
2122
2707
|
},
|
|
2123
2708
|
],
|
|
2124
2709
|
};
|
|
@@ -2147,6 +2732,19 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2147
2732
|
`## ${bundle.title} — API Integration Pattern Reference`,
|
|
2148
2733
|
"",
|
|
2149
2734
|
];
|
|
2735
|
+
// Detect container-section pattern (COMPONENT_LIST with `<id-of-X>` placeholders).
|
|
2736
|
+
// Surface this BEFORE every other warning so the LLM cannot miss the wiring requirement.
|
|
2737
|
+
// The full recipe (commands, captured-id placeholders, update-prop call) is appended near
|
|
2738
|
+
// the end of the response in the existing recipe-builder block.
|
|
2739
|
+
{
|
|
2740
|
+
const snippetStrForBanner = bundle.rootFiles["ikas-config-snippet.json"];
|
|
2741
|
+
if (snippetStrForBanner &&
|
|
2742
|
+
/<id-of-[A-Za-z0-9_]+>/.test(snippetStrForBanner)) {
|
|
2743
|
+
const childMatches = Array.from(snippetStrForBanner.matchAll(/<id-of-([A-Za-z0-9_]+)>/g));
|
|
2744
|
+
const uniqueChildren = Array.from(new Set(childMatches.map((m) => m[1])));
|
|
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.`, "");
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2150
2748
|
if (anyOutstandingSubtree) {
|
|
2151
2749
|
parts.push("> **PARTIAL TEMPLATE — NOT A COMPLETE IMPLEMENTATION.** The files below are the section's root files only. The section also ships with child components, local components, and/or shared sub-components whose files are **not included** in this response. To view one's full implementation, call `get_section_child(section, name, kind)` for each item you need (kind = `children`, `components`, or `sub-components`). The root `index.tsx` alone is not a complete reference — its imports resolve to files that live in those subtrees.", "");
|
|
2152
2750
|
}
|
|
@@ -2215,7 +2813,10 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2215
2813
|
renderInlineSubtree("children", bundle.childContents);
|
|
2216
2814
|
renderInlineSubtree("components", bundle.componentContents);
|
|
2217
2815
|
renderInlineSubtree("sub-components", bundle.subComponentContents);
|
|
2218
|
-
// Generate a ready-to-run CLI
|
|
2816
|
+
// Generate a ready-to-run CLI recipe from the config snippet. If the parent's
|
|
2817
|
+
// filteredComponentIds reference `<id-of-X>` placeholders, expand into a
|
|
2818
|
+
// multi-step recipe (create children → create parent → wire filteredComponentIds)
|
|
2819
|
+
// so the LLM cannot skip the wiring step.
|
|
2219
2820
|
const configSnippetStr = bundle.rootFiles["ikas-config-snippet.json"];
|
|
2220
2821
|
if (configSnippetStr) {
|
|
2221
2822
|
try {
|
|
@@ -2223,26 +2824,105 @@ server.tool("get_section_template", "Get the root files of a starter section tem
|
|
|
2223
2824
|
const compName = configSnippet.name || normalizedType;
|
|
2224
2825
|
const compType = configSnippet.type || "section";
|
|
2225
2826
|
const propsArr = configSnippet.props || [];
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2827
|
+
// Strip filteredComponentIds for the parent's add-component call (wired
|
|
2828
|
+
// separately below). Preserve name/type/displayName/required so the LLM gets the prop shape.
|
|
2829
|
+
const parentPropsForAdd = propsArr.map((p) => {
|
|
2830
|
+
const out = { name: p.name, type: p.type };
|
|
2831
|
+
if (p.displayName)
|
|
2832
|
+
out.displayName = p.displayName;
|
|
2833
|
+
if (p.required)
|
|
2834
|
+
out.required = true;
|
|
2835
|
+
return out;
|
|
2836
|
+
});
|
|
2837
|
+
const placeholderRe = /^<id-of-(.+)>$/;
|
|
2838
|
+
const slotPlans = [];
|
|
2839
|
+
const allChildNames = new Set();
|
|
2840
|
+
for (const p of propsArr) {
|
|
2841
|
+
if ((p.type === "COMPONENT_LIST" || p.type === "COMPONENT") &&
|
|
2842
|
+
Array.isArray(p.filteredComponentIds)) {
|
|
2843
|
+
const childNames = [];
|
|
2844
|
+
for (const entry of p.filteredComponentIds) {
|
|
2845
|
+
const m = typeof entry === "string" && entry.match(placeholderRe);
|
|
2846
|
+
if (m) {
|
|
2847
|
+
childNames.push(m[1]);
|
|
2848
|
+
allChildNames.add(m[1]);
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
if (childNames.length > 0) {
|
|
2852
|
+
slotPlans.push({ propName: p.name, childNames });
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
const childPlans = [];
|
|
2857
|
+
for (const childName of allChildNames) {
|
|
2858
|
+
const childSnippetPath = path.join(SECTION_TEMPLATES_DIR, normalizedType, "children", childName, "ikas-config-snippet.json");
|
|
2859
|
+
let propsJson = "[]";
|
|
2860
|
+
let hasTemplate = false;
|
|
2861
|
+
if (fs.existsSync(childSnippetPath)) {
|
|
2862
|
+
hasTemplate = true;
|
|
2863
|
+
try {
|
|
2864
|
+
const childSnippet = JSON.parse(fs.readFileSync(childSnippetPath, "utf-8"));
|
|
2865
|
+
const childProps = (childSnippet.props || []).map((p) => {
|
|
2866
|
+
const out = {
|
|
2867
|
+
name: p.name,
|
|
2868
|
+
type: p.type,
|
|
2869
|
+
};
|
|
2870
|
+
if (p.displayName)
|
|
2871
|
+
out.displayName = p.displayName;
|
|
2872
|
+
if (p.required)
|
|
2873
|
+
out.required = true;
|
|
2874
|
+
return out;
|
|
2875
|
+
});
|
|
2876
|
+
propsJson = JSON.stringify(childProps);
|
|
2877
|
+
}
|
|
2878
|
+
catch {
|
|
2879
|
+
// fall through with []
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
childPlans.push({ name: childName, propsJson, hasTemplate });
|
|
2883
|
+
}
|
|
2884
|
+
const baseFlags = (configSnippet.isHeader ? " --isHeader" : "") +
|
|
2885
|
+
(configSnippet.isFooter ? " --isFooter" : "");
|
|
2886
|
+
const parentPropsJsonStr = parentPropsForAdd.length > 0
|
|
2887
|
+
? ` --props '${JSON.stringify(parentPropsForAdd)}'`
|
|
2888
|
+
: "";
|
|
2889
|
+
if (slotPlans.length > 0) {
|
|
2890
|
+
parts.push("---", "");
|
|
2891
|
+
parts.push(`### Setup Recipe (run in order — ${childPlans.length} child component${childPlans.length === 1 ? "" : "s"} + 1 parent + wiring)`);
|
|
2892
|
+
parts.push("");
|
|
2893
|
+
parts.push(`> ⚠️ **This section is a CONTAINER.** It hosts child components via ${slotPlans.length === 1 ? "a COMPONENT_LIST slot" : "COMPONENT_LIST slots"} (\`${slotPlans.map((s) => s.propName).join("`, `")}\`). Creating the parent alone is **not enough** — you MUST also create the child components and wire their opaque ids into the parent's \`filteredComponentIds\`. Skipping the wiring leaves the slot empty and the section unusable.`, "");
|
|
2894
|
+
parts.push("**Step 1 — Create each child component, and capture its `componentId` from the JSON response:**");
|
|
2895
|
+
parts.push("", "```bash");
|
|
2896
|
+
for (const ch of childPlans) {
|
|
2897
|
+
parts.push(`npx ikas-component config add-component --name "${ch.name}" --type component --props '${ch.propsJson}'`);
|
|
2898
|
+
parts.push(`# → { "success": true, "componentId": "<capture as ${ch.name.toUpperCase()}_ID>", ... }`);
|
|
2899
|
+
if (!ch.hasTemplate) {
|
|
2900
|
+
parts.push(`# (no children/${ch.name}/ template in this section bundle — \`--props '[]'\` is a stub; add real props for ${ch.name} as needed)`);
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
parts.push("```", "");
|
|
2904
|
+
parts.push("**Step 2 — Create the parent section (without `filteredComponentIds` yet — those are wired in Step 3):**");
|
|
2905
|
+
parts.push("", "```bash");
|
|
2906
|
+
parts.push(`npx ikas-component config add-component --name "${compName}" --type ${compType}${baseFlags}${parentPropsJsonStr}`);
|
|
2907
|
+
parts.push("```", "");
|
|
2908
|
+
parts.push("**Step 3 — Wire each slot to its allowed children using the ids captured in Step 1:**");
|
|
2909
|
+
parts.push("", "```bash");
|
|
2910
|
+
for (const slot of slotPlans) {
|
|
2911
|
+
const idsArr = slot.childNames.map((n) => `<${n.toUpperCase()}_ID>`);
|
|
2912
|
+
parts.push(`npx ikas-component config update-prop --component "${compName}" --prop ${slot.propName} \\`);
|
|
2913
|
+
parts.push(` --filteredComponentIds '${JSON.stringify(idsArr)}'`);
|
|
2914
|
+
}
|
|
2915
|
+
parts.push("```", "");
|
|
2916
|
+
parts.push("Replace each `<X_ID>` placeholder above with the real `componentId` from the corresponding Step 1 response (or look ids up with `config list`). The CLI rejects unknown ids with a structured error — there is no silent failure mode.");
|
|
2917
|
+
parts.push("");
|
|
2918
|
+
parts.push("**Do NOT manually create or edit `types.ts`** — the CLI commands above regenerate it automatically.");
|
|
2919
|
+
parts.push("");
|
|
2920
|
+
}
|
|
2921
|
+
else {
|
|
2922
|
+
// No child slots — single-step parent command (preserves previous behaviour)
|
|
2923
|
+
const cliCommand = `npx ikas-component config add-component --name "${compName}" --type ${compType}${baseFlags}${parentPropsJsonStr}`;
|
|
2924
|
+
parts.push("---", "", "### CLI Command (run this first)", "", "```bash", cliCommand, "```", "", "**Do NOT manually create or edit `types.ts`** — the CLI command above generates it automatically.", "");
|
|
2244
2925
|
}
|
|
2245
|
-
parts.push("---", "", "### CLI Command (run this first)", "", "```bash", cliCommand, "```", "", "**Do NOT manually create or edit `types.ts`** — the CLI command above generates it automatically.", "");
|
|
2246
2926
|
}
|
|
2247
2927
|
catch {
|
|
2248
2928
|
// ignore parse errors
|
|
@@ -2259,10 +2939,7 @@ server.tool("get_section_child", "Fetch one item's files from a section's childr
|
|
|
2259
2939
|
.string()
|
|
2260
2940
|
.optional()
|
|
2261
2941
|
.describe("The item name as listed in `get_section_template`'s response (Children/Components/Sub-components)"),
|
|
2262
|
-
child: z
|
|
2263
|
-
.string()
|
|
2264
|
-
.optional()
|
|
2265
|
-
.describe("Alias for `name`"),
|
|
2942
|
+
child: z.string().optional().describe("Alias for `name`"),
|
|
2266
2943
|
kind: z
|
|
2267
2944
|
.enum(["children", "components", "sub-components"])
|
|
2268
2945
|
.optional()
|
|
@@ -2425,7 +3102,11 @@ server.tool("list_section_types", "List all available `get_section_template` sec
|
|
|
2425
3102
|
});
|
|
2426
3103
|
// --- Migration tools ---
|
|
2427
3104
|
// Tool: analyze_old_theme
|
|
2428
|
-
server.tool("analyze_old_theme", "Analyze an old ikas storefront theme.json and produce a structured migration report. Shows all components, custom data definitions, prop type breakdown, and migration recommendations. Use this as the first step when converting an old theme.", {
|
|
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 }) => {
|
|
2429
3110
|
try {
|
|
2430
3111
|
const parsed = JSON.parse(theme_json);
|
|
2431
3112
|
const analysis = analyzeOldTheme(parsed);
|
|
@@ -2433,102 +3114,162 @@ server.tool("analyze_old_theme", "Analyze an old ikas storefront theme.json and
|
|
|
2433
3114
|
}
|
|
2434
3115
|
catch (err) {
|
|
2435
3116
|
return {
|
|
2436
|
-
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
|
+
],
|
|
2437
3123
|
};
|
|
2438
3124
|
}
|
|
2439
3125
|
});
|
|
2440
3126
|
// Tool: get_migration_guide
|
|
2441
3127
|
const migrationTopicAliases = {
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
3128
|
+
overview: "migration-overview",
|
|
3129
|
+
migrate: "migration-overview",
|
|
3130
|
+
custom: "custom-data-conversion",
|
|
2445
3131
|
"custom-data": "custom-data-conversion",
|
|
2446
|
-
|
|
3132
|
+
customdata: "custom-data-conversion",
|
|
2447
3133
|
"dynamic-list": "custom-data-conversion",
|
|
2448
3134
|
"component-list": "custom-data-conversion",
|
|
2449
|
-
|
|
2450
|
-
|
|
3135
|
+
slider: "prop-type-mapping",
|
|
3136
|
+
props: "prop-type-mapping",
|
|
2451
3137
|
"prop-mapping": "prop-type-mapping",
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
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",
|
|
2465
3151
|
"bp-storefront": "storefront-import-mapping",
|
|
2466
3152
|
"theme-json": "theme-json-anatomy",
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
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",
|
|
2483
3169
|
};
|
|
2484
3170
|
const migrationTopicKeys = migrationData
|
|
2485
3171
|
? Object.keys(migrationData.topics)
|
|
2486
3172
|
: [];
|
|
2487
|
-
server.tool("get_migration_guide", `Get a migration guide for converting old ikas themes to the new code-component system.${migrationTopicKeys.length > 0 ? ` Available topics: ${migrationTopicKeys.join(", ")}. Also supports aliases like "custom", "slider", "react", "libraries", "imports", "settings".` : ""} Call with topic "list" to see all available topics.`, {
|
|
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 }) => {
|
|
2488
3178
|
if (!migrationData) {
|
|
2489
|
-
return {
|
|
3179
|
+
return {
|
|
3180
|
+
content: [
|
|
3181
|
+
{
|
|
3182
|
+
type: "text",
|
|
3183
|
+
text: "Migration data not available. Ensure data/migration.json exists.",
|
|
3184
|
+
},
|
|
3185
|
+
],
|
|
3186
|
+
};
|
|
2490
3187
|
}
|
|
2491
3188
|
if (topic.toLowerCase() === "list") {
|
|
2492
3189
|
const available = Object.entries(migrationData.topics)
|
|
2493
3190
|
.map(([key, t]) => `- \`${key}\` — ${t.title}: ${t.description}`)
|
|
2494
3191
|
.join("\n");
|
|
2495
|
-
return {
|
|
3192
|
+
return {
|
|
3193
|
+
content: [
|
|
3194
|
+
{
|
|
3195
|
+
type: "text",
|
|
3196
|
+
text: `## Available Migration Topics\n\n${available}`,
|
|
3197
|
+
},
|
|
3198
|
+
],
|
|
3199
|
+
};
|
|
2496
3200
|
}
|
|
2497
3201
|
const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
|
|
2498
3202
|
const resolvedTopic = migrationTopicAliases[topicLower] || topicLower;
|
|
2499
3203
|
if (migrationData.topics[resolvedTopic]) {
|
|
2500
3204
|
const t = migrationData.topics[resolvedTopic];
|
|
2501
|
-
return {
|
|
3205
|
+
return {
|
|
3206
|
+
content: [
|
|
3207
|
+
{ type: "text", text: `## ${t.title}\n\n${t.content}` },
|
|
3208
|
+
],
|
|
3209
|
+
};
|
|
2502
3210
|
}
|
|
2503
3211
|
// Try original key
|
|
2504
3212
|
if (resolvedTopic !== topicLower && migrationData.topics[topicLower]) {
|
|
2505
3213
|
const t = migrationData.topics[topicLower];
|
|
2506
|
-
return {
|
|
3214
|
+
return {
|
|
3215
|
+
content: [
|
|
3216
|
+
{ type: "text", text: `## ${t.title}\n\n${t.content}` },
|
|
3217
|
+
],
|
|
3218
|
+
};
|
|
2507
3219
|
}
|
|
2508
3220
|
// Keyword search
|
|
2509
3221
|
const matches = searchMigrationTopics(topic);
|
|
2510
3222
|
if (matches.length > 0) {
|
|
2511
3223
|
const best = matches[0];
|
|
2512
|
-
return {
|
|
3224
|
+
return {
|
|
3225
|
+
content: [
|
|
3226
|
+
{
|
|
3227
|
+
type: "text",
|
|
3228
|
+
text: `## ${best.topic.title}\n\n${best.topic.content}`,
|
|
3229
|
+
},
|
|
3230
|
+
],
|
|
3231
|
+
};
|
|
2513
3232
|
}
|
|
2514
3233
|
const available = Object.entries(migrationData.topics)
|
|
2515
3234
|
.map(([key, t]) => ` - \`${key}\` - ${t.title}`)
|
|
2516
3235
|
.join("\n");
|
|
2517
3236
|
return {
|
|
2518
|
-
content: [
|
|
3237
|
+
content: [
|
|
3238
|
+
{
|
|
3239
|
+
type: "text",
|
|
3240
|
+
text: `Migration topic "${topic}" not found. Available topics:\n${available}`,
|
|
3241
|
+
},
|
|
3242
|
+
],
|
|
2519
3243
|
};
|
|
2520
3244
|
});
|
|
2521
3245
|
// Tool: get_migration_example
|
|
2522
|
-
server.tool("get_migration_example", `Get a concrete before/after migration example showing how to convert an old theme component to the new code-component system.${migrationExampleNames.length > 0 ? ` Available examples: ${migrationExampleNames.join(", ")}.` : ""} Call with example "list" to see all examples.`, {
|
|
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 }) => {
|
|
2523
3249
|
if (example.toLowerCase() === "list") {
|
|
2524
3250
|
if (migrationExampleNames.length === 0) {
|
|
2525
|
-
return {
|
|
3251
|
+
return {
|
|
3252
|
+
content: [
|
|
3253
|
+
{ type: "text", text: "No migration examples available." },
|
|
3254
|
+
],
|
|
3255
|
+
};
|
|
2526
3256
|
}
|
|
2527
|
-
const list = migrationExampleNames
|
|
3257
|
+
const list = migrationExampleNames
|
|
3258
|
+
.map((name) => {
|
|
2528
3259
|
const ex = loadMigrationExample(name);
|
|
2529
|
-
return ex
|
|
2530
|
-
|
|
2531
|
-
|
|
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
|
+
};
|
|
2532
3273
|
}
|
|
2533
3274
|
const exampleLower = example.toLowerCase();
|
|
2534
3275
|
let exName = migrationExampleNames.find((n) => n === exampleLower);
|
|
@@ -2538,19 +3279,26 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
|
|
|
2538
3279
|
if (!exName) {
|
|
2539
3280
|
const available = migrationExampleNames.join(", ");
|
|
2540
3281
|
return {
|
|
2541
|
-
content: [
|
|
3282
|
+
content: [
|
|
3283
|
+
{
|
|
3284
|
+
type: "text",
|
|
3285
|
+
text: `Migration example "${example}" not found. Available: ${available}`,
|
|
3286
|
+
},
|
|
3287
|
+
],
|
|
2542
3288
|
};
|
|
2543
3289
|
}
|
|
2544
3290
|
const ex = loadMigrationExample(exName);
|
|
2545
3291
|
if (!ex) {
|
|
2546
|
-
return {
|
|
3292
|
+
return {
|
|
3293
|
+
content: [
|
|
3294
|
+
{
|
|
3295
|
+
type: "text",
|
|
3296
|
+
text: `Failed to load migration example "${exName}".`,
|
|
3297
|
+
},
|
|
3298
|
+
],
|
|
3299
|
+
};
|
|
2547
3300
|
}
|
|
2548
|
-
const parts = [
|
|
2549
|
-
`## ${ex.title}`,
|
|
2550
|
-
"",
|
|
2551
|
-
ex.description,
|
|
2552
|
-
"",
|
|
2553
|
-
];
|
|
3301
|
+
const parts = [`## ${ex.title}`, "", ex.description, ""];
|
|
2554
3302
|
for (const [filename, content] of Object.entries(ex.files)) {
|
|
2555
3303
|
const ext = filename.split(".").pop() || "text";
|
|
2556
3304
|
const lang = ext === "tsx" || ext === "ts"
|
|
@@ -2568,42 +3316,407 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
|
|
|
2568
3316
|
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
2569
3317
|
});
|
|
2570
3318
|
// Tool: plan_migration
|
|
2571
|
-
server.tool("plan_migration", "Generate
|
|
2572
|
-
theme_json: z
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
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, }) => {
|
|
2576
3345
|
try {
|
|
2577
|
-
const parsed =
|
|
3346
|
+
const parsed = resolveThemeJson(theme_json, theme_json_path);
|
|
2578
3347
|
const projectName = project_name || "my-theme";
|
|
2579
3348
|
const plan = generateMigrationPlan(parsed, projectName, old_source_dir);
|
|
2580
|
-
|
|
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 }] };
|
|
2581
3402
|
}
|
|
2582
3403
|
catch (err) {
|
|
2583
3404
|
return {
|
|
2584
|
-
content: [
|
|
3405
|
+
content: [
|
|
3406
|
+
{
|
|
3407
|
+
type: "text",
|
|
3408
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
3409
|
+
},
|
|
3410
|
+
],
|
|
2585
3411
|
};
|
|
2586
3412
|
}
|
|
2587
3413
|
});
|
|
2588
3414
|
// Tool: get_section_migration_plan
|
|
2589
|
-
server.tool("get_section_migration_plan", "
|
|
2590
|
-
theme_json: z
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
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, }) => {
|
|
2595
3436
|
try {
|
|
2596
|
-
const parsed =
|
|
3437
|
+
const parsed = resolveThemeJson(theme_json, theme_json_path);
|
|
2597
3438
|
const projectName = project_name || "my-theme";
|
|
2598
3439
|
const plan = generateSectionMigrationPlan(parsed, section_name, projectName, old_source_dir);
|
|
2599
3440
|
return { content: [{ type: "text", text: plan }] };
|
|
2600
3441
|
}
|
|
2601
3442
|
catch (err) {
|
|
2602
3443
|
return {
|
|
2603
|
-
content: [
|
|
3444
|
+
content: [
|
|
3445
|
+
{
|
|
3446
|
+
type: "text",
|
|
3447
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
3448
|
+
},
|
|
3449
|
+
],
|
|
2604
3450
|
};
|
|
2605
3451
|
}
|
|
2606
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, { cwd: projectRoot, windowsHide: true }, (err, stdout, stderr) => {
|
|
3480
|
+
const exitCode = err && typeof err.code === "number"
|
|
3481
|
+
? err.code
|
|
3482
|
+
: err
|
|
3483
|
+
? 1
|
|
3484
|
+
: 0;
|
|
3485
|
+
resolve({
|
|
3486
|
+
stdout: stdout?.toString() ?? "",
|
|
3487
|
+
stderr: stderr?.toString() ?? "",
|
|
3488
|
+
exitCode,
|
|
3489
|
+
});
|
|
3490
|
+
});
|
|
3491
|
+
});
|
|
3492
|
+
}
|
|
3493
|
+
function parseCliJson(stdout) {
|
|
3494
|
+
const trimmed = stdout.trim();
|
|
3495
|
+
if (!trimmed)
|
|
3496
|
+
return null;
|
|
3497
|
+
// CLI prints exactly one JSON object on stdout; if multiple lines appeared
|
|
3498
|
+
// (e.g., warnings on stderr leaked), take the last non-empty line.
|
|
3499
|
+
const last = trimmed
|
|
3500
|
+
.split("\n")
|
|
3501
|
+
.map((l) => l.trim())
|
|
3502
|
+
.filter(Boolean)
|
|
3503
|
+
.pop();
|
|
3504
|
+
if (!last)
|
|
3505
|
+
return null;
|
|
3506
|
+
try {
|
|
3507
|
+
return JSON.parse(last);
|
|
3508
|
+
}
|
|
3509
|
+
catch {
|
|
3510
|
+
return null;
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
async function callEditorAction(projectRoot, args) {
|
|
3514
|
+
try {
|
|
3515
|
+
const { stdout, stderr, exitCode } = await runIkasComponentCli(projectRoot, args);
|
|
3516
|
+
const parsed = parseCliJson(stdout);
|
|
3517
|
+
if (parsed) {
|
|
3518
|
+
return {
|
|
3519
|
+
content: [
|
|
3520
|
+
{ type: "text", text: JSON.stringify(parsed, null, 2) },
|
|
3521
|
+
],
|
|
3522
|
+
};
|
|
3523
|
+
}
|
|
3524
|
+
return {
|
|
3525
|
+
content: [
|
|
3526
|
+
{
|
|
3527
|
+
type: "text",
|
|
3528
|
+
text: `CLI exited with code ${exitCode} and produced no parseable JSON.\n` +
|
|
3529
|
+
`stdout:\n${stdout || "(empty)"}\nstderr:\n${stderr || "(empty)"}`,
|
|
3530
|
+
},
|
|
3531
|
+
],
|
|
3532
|
+
};
|
|
3533
|
+
}
|
|
3534
|
+
catch (err) {
|
|
3535
|
+
return {
|
|
3536
|
+
content: [
|
|
3537
|
+
{
|
|
3538
|
+
type: "text",
|
|
3539
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
3540
|
+
},
|
|
3541
|
+
],
|
|
3542
|
+
};
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
//
|
|
3546
|
+
// Tool: list_editor_pages
|
|
3547
|
+
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`.", {
|
|
3548
|
+
project_root: z
|
|
3549
|
+
.string()
|
|
3550
|
+
.describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
|
|
3551
|
+
port: z
|
|
3552
|
+
.number()
|
|
3553
|
+
.optional()
|
|
3554
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3555
|
+
}, async ({ project_root, port }) => {
|
|
3556
|
+
const args = ["list-pages", ...(port ? ["--port", String(port)] : [])];
|
|
3557
|
+
return callEditorAction(project_root, args);
|
|
3558
|
+
});
|
|
3559
|
+
// Tool: list_imported_sections
|
|
3560
|
+
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`.", {
|
|
3561
|
+
project_root: z
|
|
3562
|
+
.string()
|
|
3563
|
+
.describe("Absolute path to the code-component project."),
|
|
3564
|
+
port: z
|
|
3565
|
+
.number()
|
|
3566
|
+
.optional()
|
|
3567
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3568
|
+
}, async ({ project_root, port }) => {
|
|
3569
|
+
const args = [
|
|
3570
|
+
"list-imported",
|
|
3571
|
+
"--sections-only",
|
|
3572
|
+
...(port ? ["--port", String(port)] : []),
|
|
3573
|
+
];
|
|
3574
|
+
return callEditorAction(project_root, args);
|
|
3575
|
+
});
|
|
3576
|
+
// Tool: import_section
|
|
3577
|
+
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`).", {
|
|
3578
|
+
project_root: z
|
|
3579
|
+
.string()
|
|
3580
|
+
.describe("Absolute path to the code-component project."),
|
|
3581
|
+
component_id: z
|
|
3582
|
+
.string()
|
|
3583
|
+
.describe("Component id from `ikas.config.json` (strict — no name resolution)."),
|
|
3584
|
+
port: z
|
|
3585
|
+
.number()
|
|
3586
|
+
.optional()
|
|
3587
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3588
|
+
}, async ({ project_root, component_id, port }) => {
|
|
3589
|
+
const args = [
|
|
3590
|
+
"import",
|
|
3591
|
+
"--id",
|
|
3592
|
+
component_id,
|
|
3593
|
+
...(port ? ["--port", String(port)] : []),
|
|
3594
|
+
];
|
|
3595
|
+
return callEditorAction(project_root, args);
|
|
3596
|
+
});
|
|
3597
|
+
// Tool: add_section_to_page
|
|
3598
|
+
server.tool("add_section_to_page", 'Place an already-imported section-type code component on a page in the editor. Equivalent to right-clicking the section in the dev-components panel and choosing "Add to Page". Errors if the component is not imported, is not section-type, or the page id is unknown. Use `list_editor_pages` to discover page ids and `list_imported_sections` to discover component ids. After placing, change the section\'s prop values with `update_section_prop` (use `list_page_sections` to get the placement\'s `elementId` and prop names).', {
|
|
3599
|
+
project_root: z
|
|
3600
|
+
.string()
|
|
3601
|
+
.describe("Absolute path to the code-component project."),
|
|
3602
|
+
component_id: z
|
|
3603
|
+
.string()
|
|
3604
|
+
.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)."),
|
|
3605
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3606
|
+
index: z
|
|
3607
|
+
.number()
|
|
3608
|
+
.int()
|
|
3609
|
+
.nonnegative()
|
|
3610
|
+
.optional()
|
|
3611
|
+
.describe("Zero-based insertion index in the page; appends when omitted."),
|
|
3612
|
+
port: z
|
|
3613
|
+
.number()
|
|
3614
|
+
.optional()
|
|
3615
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3616
|
+
}, async ({ project_root, component_id, page_id, index, port }) => {
|
|
3617
|
+
const args = [
|
|
3618
|
+
"add-to-page",
|
|
3619
|
+
"--component-id",
|
|
3620
|
+
component_id,
|
|
3621
|
+
"--page-id",
|
|
3622
|
+
page_id,
|
|
3623
|
+
...(typeof index === "number" ? ["--index", String(index)] : []),
|
|
3624
|
+
...(port ? ["--port", String(port)] : []),
|
|
3625
|
+
];
|
|
3626
|
+
return callEditorAction(project_root, args);
|
|
3627
|
+
});
|
|
3628
|
+
// Tool: list_page_sections
|
|
3629
|
+
server.tool("list_page_sections", "List the sections placed on a page. Each entry includes the per-placement `elementId` (the identity of THIS placement — there can be multiple placements of the same section), `sectionId`, `componentId`, `name`, the current `propValues`, and the blueprint `props` (each with `id`, `name`, `type`). Use the returned `elementId` plus a prop `id`/`name` with `update_section_prop` to change a prop value of that specific placement. Use `list_editor_pages` to discover page ids.", {
|
|
3630
|
+
project_root: z
|
|
3631
|
+
.string()
|
|
3632
|
+
.describe("Absolute path to the code-component project."),
|
|
3633
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3634
|
+
port: z
|
|
3635
|
+
.number()
|
|
3636
|
+
.optional()
|
|
3637
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3638
|
+
}, async ({ project_root, page_id, port }) => {
|
|
3639
|
+
const args = [
|
|
3640
|
+
"list-page-sections",
|
|
3641
|
+
"--page-id",
|
|
3642
|
+
page_id,
|
|
3643
|
+
...(port ? ["--port", String(port)] : []),
|
|
3644
|
+
];
|
|
3645
|
+
return callEditorAction(project_root, args);
|
|
3646
|
+
});
|
|
3647
|
+
// Tool: update_section_prop
|
|
3648
|
+
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: IMAGE/VIDEO/SVG = `{ "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`); 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": [] }`. 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: a wrong shape (e.g. `{ "value": [...] }` for a COMPONENT_LIST, a `{ "value": ... }`-wrapped IMAGE, `componentId`+`props` instead of `codeComponentId`+`propValues`, or a missing entry `id`) is now REJECTED with an explanatory error instead of being silently stored. For any prop type you are unsure about, inspect the existing entry in `list_page_sections` `propValues` and match its exact shape. The change is applied with undo support.\n\n' +
|
|
3649
|
+
'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 `list_page_sections`, 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; the allowed set is the blueprint prop\'s `filteredComponentIds`, which for container sections (Header, Footer, ProductDetail, …) is wired at build/config time per the `get_section_template` setup recipe (`config update-prop`), not here.\n\n' +
|
|
3650
|
+
'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): ' +
|
|
3651
|
+
`{ "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" } } } ] } } } ] }`, {
|
|
3652
|
+
project_root: z
|
|
3653
|
+
.string()
|
|
3654
|
+
.describe("Absolute path to the code-component project."),
|
|
3655
|
+
page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
|
|
3656
|
+
element_id: z
|
|
3657
|
+
.string()
|
|
3658
|
+
.describe("Placed-section elementId identifying THIS placement on the page (from `list_page_sections`)."),
|
|
3659
|
+
prop_id: z
|
|
3660
|
+
.string()
|
|
3661
|
+
.optional()
|
|
3662
|
+
.describe("Blueprint prop id to update (from `list_page_sections`). Provide this or `prop_name`."),
|
|
3663
|
+
prop_name: z
|
|
3664
|
+
.string()
|
|
3665
|
+
.optional()
|
|
3666
|
+
.describe("Blueprint prop name to update (alternative to `prop_id`)."),
|
|
3667
|
+
value: z
|
|
3668
|
+
.any()
|
|
3669
|
+
.describe('The prop value object to store, e.g. `{ "value": "Hello" }`. Match the shape of the existing entry in `list_page_sections` propValues for non-scalar prop types.'),
|
|
3670
|
+
port: z
|
|
3671
|
+
.number()
|
|
3672
|
+
.optional()
|
|
3673
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3674
|
+
}, async ({ project_root, page_id, element_id, prop_id, prop_name, value, port }) => {
|
|
3675
|
+
const args = [
|
|
3676
|
+
"update-section-prop",
|
|
3677
|
+
"--page-id",
|
|
3678
|
+
page_id,
|
|
3679
|
+
"--element-id",
|
|
3680
|
+
element_id,
|
|
3681
|
+
...(prop_id ? ["--prop-id", prop_id] : []),
|
|
3682
|
+
...(prop_name ? ["--prop-name", prop_name] : []),
|
|
3683
|
+
"--value",
|
|
3684
|
+
// The CLI JSON.parses --value. If `value` is already a JSON string, pass
|
|
3685
|
+
// it through verbatim — re-stringifying it would double-encode it and the
|
|
3686
|
+
// editor would store a string instead of the object/array.
|
|
3687
|
+
typeof value === "string" ? value : JSON.stringify(value),
|
|
3688
|
+
...(port ? ["--port", String(port)] : []),
|
|
3689
|
+
];
|
|
3690
|
+
return callEditorAction(project_root, args);
|
|
3691
|
+
});
|
|
3692
|
+
// Tool: upload_image
|
|
3693
|
+
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).", {
|
|
3694
|
+
project_root: z
|
|
3695
|
+
.string()
|
|
3696
|
+
.describe("Absolute path to the code-component project."),
|
|
3697
|
+
file_path: z
|
|
3698
|
+
.string()
|
|
3699
|
+
.optional()
|
|
3700
|
+
.describe("Local image file path (.png, .jpg, .jpeg, .webp, .gif). Provide this or `image_url`."),
|
|
3701
|
+
image_url: z
|
|
3702
|
+
.string()
|
|
3703
|
+
.optional()
|
|
3704
|
+
.describe("Image URL to fetch and upload (alternative to `file_path`)."),
|
|
3705
|
+
alt_text: z.string().optional().describe("Alt text to store with the image."),
|
|
3706
|
+
port: z
|
|
3707
|
+
.number()
|
|
3708
|
+
.optional()
|
|
3709
|
+
.describe("Dev server WebSocket port (default 5201)."),
|
|
3710
|
+
}, async ({ project_root, file_path, image_url, alt_text, port }) => {
|
|
3711
|
+
const args = [
|
|
3712
|
+
"upload-image",
|
|
3713
|
+
...(file_path ? ["--file", file_path] : []),
|
|
3714
|
+
...(image_url ? ["--url", image_url] : []),
|
|
3715
|
+
...(alt_text ? ["--alt", alt_text] : []),
|
|
3716
|
+
...(port ? ["--port", String(port)] : []),
|
|
3717
|
+
];
|
|
3718
|
+
return callEditorAction(project_root, args);
|
|
3719
|
+
});
|
|
2607
3720
|
// --- Start server ---
|
|
2608
3721
|
async function main() {
|
|
2609
3722
|
const transport = new StdioServerTransport();
|