@ikas/code-components-mcp 1.4.0-beta.2 → 1.4.0-beta.4

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.
Files changed (23) hide show
  1. package/data/framework.json +4 -4
  2. package/data/section-templates/account-info-section/children/AccountFavorites/ikas-config-snippet.json +3 -3
  3. package/data/section-templates/account-info-section/ikas-config-snippet.json +5 -5
  4. package/data/section-templates/category-images-section/ikas-config-snippet.json +1 -1
  5. package/data/section-templates/category-list-section/ikas-config-snippet.json +3 -3
  6. package/data/section-templates/component-renderer/ikas-config-snippet.json +3 -3
  7. package/data/section-templates/features-section/ikas-config-snippet.json +1 -1
  8. package/data/section-templates/footer-section/ikas-config-snippet.json +1 -1
  9. package/data/section-templates/header-section/children/Announcements/ikas-config-snippet.json +1 -1
  10. package/data/section-templates/header-section/children/Navbar/ikas-config-snippet.json +3 -3
  11. package/data/section-templates/header-section/ikas-config-snippet.json +3 -3
  12. package/data/section-templates/hero-slider-section/ikas-config-snippet.json +1 -1
  13. package/data/section-templates/image-handling/ikas-config-snippet.json +13 -13
  14. package/data/section-templates/navigation/ikas-config-snippet.json +3 -3
  15. package/data/section-templates/product-detail-section/children/ProductDetailDescription/ikas-config-snippet.json +1 -1
  16. package/data/section-templates/product-detail-section/children/ProductDetailFeatures/ikas-config-snippet.json +1 -1
  17. package/data/section-templates/product-detail-section/ikas-config-snippet.json +13 -13
  18. package/data/section-templates/product-slider-section/ikas-config-snippet.json +3 -3
  19. package/data/storefront-api.json +1 -1
  20. package/data/storefront-types.json +1 -1
  21. package/dist/index.js +178 -60
  22. package/dist/index.js.map +1 -1
  23. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -584,7 +584,7 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
584
584
  // Sections queue
585
585
  parts.push(`## Sections`);
586
586
  parts.push("");
587
- parts.push(`Canonical component IDs are listed below **use these exactly** when running \`config add-component\` and in \`filteredComponentIds\`. Do not change them mid-migration.`);
587
+ 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
588
  parts.push("");
589
589
  const buckets = {
590
590
  simple: [],
@@ -621,10 +621,8 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
621
621
  const itemObj = cd.nestedData?.[0];
622
622
  if (itemObj) {
623
623
  const childName = itemObj.typescriptName || (itemObj.name ? itemObj.name.replace(/[^a-zA-Z0-9]/g, "") : `${oldName}Item`);
624
- const childKebab = toKebabCase(childName);
625
- const childId = `${projectName}-${childKebab}`;
626
624
  const fields = (itemObj.nestedData || []).map((f) => f.key || f.name || "?").slice(0, 8);
627
- children.push({ propName: p.name || "?", childId, childName, fields });
625
+ children.push({ propName: p.name || "?", childName, fields });
628
626
  }
629
627
  }
630
628
  }
@@ -634,7 +632,7 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
634
632
  if (children.length > 0) {
635
633
  parts.push(` - Children:`);
636
634
  for (const ch of children) {
637
- parts.push(` - [ ] \`${ch.childId}\` (\`${ch.childName}\`) — for prop \`${ch.propName}\` — fields: ${ch.fields.join(", ")}`);
635
+ parts.push(` - [ ] \`${ch.childName}\` — for prop \`${ch.propName}\` — fields: ${ch.fields.join(", ")}`);
638
636
  }
639
637
  }
640
638
  }
@@ -855,8 +853,8 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
855
853
  parts.push(`# Section Migration Plan: ${oldName}`);
856
854
  parts.push("");
857
855
  parts.push(`**Old name:** \`${oldName}\` (dir: \`${target.dir || "?"}\`)`);
858
- parts.push(`**New section ID:** \`${sectionId}\``);
859
- parts.push(`**New section name:** \`${sectionPascal}\``);
856
+ 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)`);
857
+ parts.push(`**Migration checkbox label:** \`${sectionId}\` (tracking identifier in \`MIGRATION.md\` only — not the runtime component id)`);
860
858
  if (target.isHeader)
861
859
  parts.push(`**Flags:** HEADER (\`isHeader: true\`)`);
862
860
  if (target.isFooter)
@@ -961,8 +959,6 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
961
959
  // Child component needed
962
960
  const itemObj = cd.nestedData?.[0];
963
961
  const childName = itemObj?.typescriptName || (itemObj?.name ? itemObj.name.replace(/[^a-zA-Z0-9]/g, "") : `${sectionPascal}Item`);
964
- const childKebab = toKebabCase(childName);
965
- const childId = `${projectName}-${childKebab}`;
966
962
  const childProps = [];
967
963
  const nestedWarnings = [];
968
964
  for (const f of (itemObj?.nestedData || [])) {
@@ -997,18 +993,17 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
997
993
  }
998
994
  children.push({
999
995
  propName: oldName,
1000
- childId,
1001
996
  childName,
1002
997
  childPropsJson: childProps,
1003
998
  customDataName: cd.name || "unnamed",
1004
999
  });
1005
1000
  newType = "COMPONENT_LIST";
1006
- notes = `Was CUSTOM → ${cd.type} of ${itemObj?.typescriptName || "object"}. Create child component \`${childId}\`.`;
1001
+ 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
1002
  const prop = {
1008
1003
  name: newName,
1009
1004
  displayName: p.displayName || newName,
1010
1005
  type: "COMPONENT_LIST",
1011
- filteredComponentIds: [childId],
1006
+ filteredComponentIds: [`<id-of-${childName}>`],
1012
1007
  };
1013
1008
  parentPropsJson.push(prop);
1014
1009
  }
@@ -1074,15 +1069,15 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1074
1069
  parts.push(`Save the returned \`enumId\` values and substitute them in the parent config JSON below.`);
1075
1070
  parts.push("");
1076
1071
  }
1077
- // Child CLI commands — dedupe by childId (same shape may be referenced by multiple parent props)
1072
+ // Child CLI commands — dedupe by childName (same shape may be referenced by multiple parent props)
1078
1073
  const uniqueChildren = new Map();
1079
1074
  for (const ch of children) {
1080
- const existing = uniqueChildren.get(ch.childId);
1075
+ const existing = uniqueChildren.get(ch.childName);
1081
1076
  if (existing) {
1082
1077
  existing.usedByProps.push(ch.propName);
1083
1078
  }
1084
1079
  else {
1085
- uniqueChildren.set(ch.childId, { child: ch, usedByProps: [ch.propName] });
1080
+ uniqueChildren.set(ch.childName, { child: ch, usedByProps: [ch.propName] });
1086
1081
  }
1087
1082
  }
1088
1083
  if (uniqueChildren.size > 0) {
@@ -1090,9 +1085,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1090
1085
  parts.push("");
1091
1086
  parts.push(`These children are referenced by the parent's COMPONENT_LIST props. Create them before the parent.`);
1092
1087
  parts.push(`**Deduped:** run each \`add-component\` command ONCE even if multiple parent props reference the same child.`);
1088
+ 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
1089
  parts.push("");
1094
1090
  for (const { child: ch, usedByProps } of uniqueChildren.values()) {
1095
- parts.push(`### \`${ch.childId}\` (\`${ch.childName}\`)`);
1091
+ parts.push(`### \`${ch.childName}\``);
1096
1092
  const propsLabel = usedByProps.length > 1
1097
1093
  ? `Used by parent props: ${usedByProps.map(p => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
1098
1094
  : `For parent prop: \`${usedByProps[0]}\``;
@@ -1101,6 +1097,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1101
1097
  parts.push("");
1102
1098
  parts.push(`\`\`\`bash`);
1103
1099
  parts.push(`npx ikas-component config add-component --name "${ch.childName}" --type component --props '${JSON.stringify(ch.childPropsJson)}'`);
1100
+ parts.push(`# → { "success": true, "componentId": "<capture-this>", ... }`);
1104
1101
  parts.push(`\`\`\``);
1105
1102
  parts.push("");
1106
1103
  }
@@ -1116,6 +1113,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1116
1113
  parts.push(`\`\`\``);
1117
1114
  parts.push("");
1118
1115
  parts.push(`**IMPORTANT:** The CLI auto-generates \`types.ts\`. DO NOT manually create or edit \`types.ts\`.`);
1116
+ if (children.length > 0) {
1117
+ parts.push("");
1118
+ 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.`);
1119
+ }
1119
1120
  parts.push("");
1120
1121
  // Implementation guidance
1121
1122
  const nextStep = parentStep + 1;
@@ -1132,7 +1133,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1132
1133
  if (children.length > 0) {
1133
1134
  parts.push(`- For COMPONENT_LIST props, use \`<IkasComponentRenderer id="..." components={list as any[]} parentProps={props} />\``);
1134
1135
  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 from a previous migration (reuse its ID in \`filteredComponentIds\`). If not, create it as a registered component.`);
1136
+ 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
1137
  }
1137
1138
  // Check if the section itself has data-driven list props (PRODUCT_LIST, BLOG_LIST, CATEGORY_LIST)
1138
1139
  const dataListProps = (target.props || []).filter(p => p.type === "PRODUCT_LIST" || p.type === "BLOG_LIST" || p.type === "CATEGORY_LIST" || p.type === "BRAND_LIST");
@@ -1274,6 +1275,7 @@ function searchFunctions(query) {
1274
1275
  const scored = storefrontData.functions
1275
1276
  .map((fn) => {
1276
1277
  const nameScore = matchScore(fn.name, query) * 3;
1278
+ const displayNameScore = fn.displayName ? matchScore(fn.displayName, query) * 3 : 0;
1277
1279
  const descScore = matchScore(fn.description, query);
1278
1280
  const catScore = fn.categories.some((c) => matchScore(c, query) > 0) ? 5 : 0;
1279
1281
  const paramScore = fn.params.some((p) => matchScore(p.name, query) > 0 || matchScore(p.description, query) > 0)
@@ -1281,7 +1283,10 @@ function searchFunctions(query) {
1281
1283
  : 0;
1282
1284
  const sigScore = matchScore(fn.signature, query) * 2;
1283
1285
  const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0) ? 8 : 0;
1284
- return { fn, score: nameScore + descScore + catScore + paramScore + sigScore + typeScore };
1286
+ return {
1287
+ fn,
1288
+ score: nameScore + displayNameScore + descScore + catScore + paramScore + sigScore + typeScore,
1289
+ };
1285
1290
  })
1286
1291
  .filter((item) => item.score > 0)
1287
1292
  .sort((a, b) => b.score - a.score);
@@ -1469,31 +1474,53 @@ server.tool("search_docs", "Search across all ikas storefront API docs, framewor
1469
1474
  // Tool: get_function_doc
1470
1475
  server.tool("get_function_doc", "Get full documentation for a specific storefront API function including signature, parameters, return type, and example.", { name: z.string().describe("Function name (e.g. 'addItemToCart', 'Router.navigate')") }, async ({ name }) => {
1471
1476
  const nameLower = name.toLowerCase();
1472
- const fn = storefrontData.functions.find((f) => f.name.toLowerCase() === nameLower ||
1473
- (f.displayName && f.displayName.toLowerCase() === nameLower));
1474
- if (!fn) {
1475
- // Try fuzzy match
1476
- const matches = storefrontData.functions.filter((f) => f.name.toLowerCase().includes(nameLower) ||
1477
- (f.displayName && f.displayName.toLowerCase().includes(nameLower)));
1478
- if (matches.length > 0) {
1479
- const suggestions = matches.slice(0, 5).map((f) => {
1480
- const alias = f.displayName && f.displayName !== f.name ? ` (alias: ${f.displayName})` : "";
1481
- return ` - ${f.name}${alias}`;
1482
- });
1483
- return {
1484
- content: [
1485
- {
1486
- type: "text",
1487
- text: `Function "${name}" not found. Did you mean:\n${suggestions.join("\n")}`,
1488
- },
1489
- ],
1490
- };
1477
+ // Phase 1: canonical-name match wins. A real function name always outranks
1478
+ // any displayName alias so aliases can never shadow the function they're
1479
+ // named after (e.g. `hasCustomer` the function vs. `hasIkasOrderCustomer`'s
1480
+ // [BP-DISPLAY-NAME: hasCustomer] alias).
1481
+ const byName = storefrontData.functions.find((f) => f.name.toLowerCase() === nameLower);
1482
+ if (byName) {
1483
+ return { content: [{ type: "text", text: formatFunctionDoc(byName) }] };
1484
+ }
1485
+ // Phase 2: fall back to displayName aliases.
1486
+ const byAlias = storefrontData.functions.filter((f) => f.displayName && f.displayName.toLowerCase() === nameLower);
1487
+ if (byAlias.length === 1) {
1488
+ const fn = byAlias[0];
1489
+ const note = `> Note: "${name}" is a display alias for \`${fn.name}\`.\n\n`;
1490
+ return {
1491
+ content: [{ type: "text", text: note + formatFunctionDoc(fn) }],
1492
+ };
1493
+ }
1494
+ if (byAlias.length > 1) {
1495
+ const lines = [];
1496
+ 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:`);
1497
+ lines.push("");
1498
+ for (const fn of byAlias) {
1499
+ lines.push(formatFunctionSummary(fn));
1500
+ lines.push(` \`${fn.signature}\``);
1491
1501
  }
1502
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1503
+ }
1504
+ // No exact match. Try fuzzy substring match across name + displayName.
1505
+ const matches = storefrontData.functions.filter((f) => f.name.toLowerCase().includes(nameLower) ||
1506
+ (f.displayName && f.displayName.toLowerCase().includes(nameLower)));
1507
+ if (matches.length > 0) {
1508
+ const suggestions = matches.slice(0, 5).map((f) => {
1509
+ const alias = f.displayName && f.displayName !== f.name ? ` (alias: ${f.displayName})` : "";
1510
+ return ` - ${f.name}${alias}`;
1511
+ });
1492
1512
  return {
1493
- content: [{ type: "text", text: `Function "${name}" not found. Use \`list_functions()\` to see all available functions.` }],
1513
+ content: [
1514
+ {
1515
+ type: "text",
1516
+ text: `Function "${name}" not found. Did you mean:\n${suggestions.join("\n")}`,
1517
+ },
1518
+ ],
1494
1519
  };
1495
1520
  }
1496
- return { content: [{ type: "text", text: formatFunctionDoc(fn) }] };
1521
+ return {
1522
+ content: [{ type: "text", text: `Function "${name}" not found. Use \`list_functions()\` to see all available functions.` }],
1523
+ };
1497
1524
  });
1498
1525
  // Tool: list_functions
1499
1526
  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.", {
@@ -2072,7 +2099,7 @@ server.tool("get_prop_types", "Get all available ikas.config.json prop types wit
2072
2099
  const sectionTemplateKeys = sectionTemplateNames.length > 0
2073
2100
  ? sectionTemplateNames
2074
2101
  : 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.", {
2102
+ 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
2103
  sectionType: z
2077
2104
  .string()
2078
2105
  .describe("The section type (call `list_section_types()` for valid values)"),
@@ -2147,6 +2174,18 @@ server.tool("get_section_template", "Get the root files of a starter section tem
2147
2174
  `## ${bundle.title} — API Integration Pattern Reference`,
2148
2175
  "",
2149
2176
  ];
2177
+ // Detect container-section pattern (COMPONENT_LIST with `<id-of-X>` placeholders).
2178
+ // Surface this BEFORE every other warning so the LLM cannot miss the wiring requirement.
2179
+ // The full recipe (commands, captured-id placeholders, update-prop call) is appended near
2180
+ // the end of the response in the existing recipe-builder block.
2181
+ {
2182
+ const snippetStrForBanner = bundle.rootFiles["ikas-config-snippet.json"];
2183
+ if (snippetStrForBanner && /<id-of-[A-Za-z0-9_]+>/.test(snippetStrForBanner)) {
2184
+ const childMatches = Array.from(snippetStrForBanner.matchAll(/<id-of-([A-Za-z0-9_]+)>/g));
2185
+ const uniqueChildren = Array.from(new Set(childMatches.map((m) => m[1])));
2186
+ 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.`, "");
2187
+ }
2188
+ }
2150
2189
  if (anyOutstandingSubtree) {
2151
2190
  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
2191
  }
@@ -2215,7 +2254,10 @@ server.tool("get_section_template", "Get the root files of a starter section tem
2215
2254
  renderInlineSubtree("children", bundle.childContents);
2216
2255
  renderInlineSubtree("components", bundle.componentContents);
2217
2256
  renderInlineSubtree("sub-components", bundle.subComponentContents);
2218
- // Generate a ready-to-run CLI command from the config snippet
2257
+ // Generate a ready-to-run CLI recipe from the config snippet. If the parent's
2258
+ // filteredComponentIds reference `<id-of-X>` placeholders, expand into a
2259
+ // multi-step recipe (create children → create parent → wire filteredComponentIds)
2260
+ // so the LLM cannot skip the wiring step.
2219
2261
  const configSnippetStr = bundle.rootFiles["ikas-config-snippet.json"];
2220
2262
  if (configSnippetStr) {
2221
2263
  try {
@@ -2223,26 +2265,102 @@ server.tool("get_section_template", "Get the root files of a starter section tem
2223
2265
  const compName = configSnippet.name || normalizedType;
2224
2266
  const compType = configSnippet.type || "section";
2225
2267
  const propsArr = configSnippet.props || [];
2226
- let cliCommand = `npx ikas-component config add-component --name "${compName}" --type ${compType}`;
2227
- if (configSnippet.isHeader)
2228
- cliCommand += " --isHeader";
2229
- if (configSnippet.isFooter)
2230
- cliCommand += " --isFooter";
2231
- if (propsArr.length > 0) {
2232
- const propsJson = JSON.stringify(propsArr.map((p) => {
2233
- const prop = {
2234
- name: p.name,
2235
- type: p.type,
2236
- };
2237
- if (p.displayName)
2238
- prop.displayName = p.displayName;
2239
- if (p.required)
2240
- prop.required = true;
2241
- return prop;
2242
- }));
2243
- cliCommand += ` --props '${propsJson}'`;
2268
+ // Strip filteredComponentIds for the parent's add-component call (wired
2269
+ // separately below). Preserve name/type/displayName/required so the LLM gets the prop shape.
2270
+ const parentPropsForAdd = propsArr.map((p) => {
2271
+ const out = { name: p.name, type: p.type };
2272
+ if (p.displayName)
2273
+ out.displayName = p.displayName;
2274
+ if (p.required)
2275
+ out.required = true;
2276
+ return out;
2277
+ });
2278
+ const placeholderRe = /^<id-of-(.+)>$/;
2279
+ const slotPlans = [];
2280
+ const allChildNames = new Set();
2281
+ for (const p of propsArr) {
2282
+ if ((p.type === "COMPONENT_LIST" || p.type === "COMPONENT") &&
2283
+ Array.isArray(p.filteredComponentIds)) {
2284
+ const childNames = [];
2285
+ for (const entry of p.filteredComponentIds) {
2286
+ const m = typeof entry === "string" && entry.match(placeholderRe);
2287
+ if (m) {
2288
+ childNames.push(m[1]);
2289
+ allChildNames.add(m[1]);
2290
+ }
2291
+ }
2292
+ if (childNames.length > 0) {
2293
+ slotPlans.push({ propName: p.name, childNames });
2294
+ }
2295
+ }
2296
+ }
2297
+ const childPlans = [];
2298
+ for (const childName of allChildNames) {
2299
+ const childSnippetPath = path.join(SECTION_TEMPLATES_DIR, normalizedType, "children", childName, "ikas-config-snippet.json");
2300
+ let propsJson = "[]";
2301
+ let hasTemplate = false;
2302
+ if (fs.existsSync(childSnippetPath)) {
2303
+ hasTemplate = true;
2304
+ try {
2305
+ const childSnippet = JSON.parse(fs.readFileSync(childSnippetPath, "utf-8"));
2306
+ const childProps = (childSnippet.props || []).map((p) => {
2307
+ const out = { name: p.name, type: p.type };
2308
+ if (p.displayName)
2309
+ out.displayName = p.displayName;
2310
+ if (p.required)
2311
+ out.required = true;
2312
+ return out;
2313
+ });
2314
+ propsJson = JSON.stringify(childProps);
2315
+ }
2316
+ catch {
2317
+ // fall through with []
2318
+ }
2319
+ }
2320
+ childPlans.push({ name: childName, propsJson, hasTemplate });
2321
+ }
2322
+ const baseFlags = (configSnippet.isHeader ? " --isHeader" : "") +
2323
+ (configSnippet.isFooter ? " --isFooter" : "");
2324
+ const parentPropsJsonStr = parentPropsForAdd.length > 0
2325
+ ? ` --props '${JSON.stringify(parentPropsForAdd)}'`
2326
+ : "";
2327
+ if (slotPlans.length > 0) {
2328
+ parts.push("---", "");
2329
+ parts.push(`### Setup Recipe (run in order — ${childPlans.length} child component${childPlans.length === 1 ? "" : "s"} + 1 parent + wiring)`);
2330
+ parts.push("");
2331
+ 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.`, "");
2332
+ parts.push("**Step 1 — Create each child component, and capture its `componentId` from the JSON response:**");
2333
+ parts.push("", "```bash");
2334
+ for (const ch of childPlans) {
2335
+ parts.push(`npx ikas-component config add-component --name "${ch.name}" --type component --props '${ch.propsJson}'`);
2336
+ parts.push(`# → { "success": true, "componentId": "<capture as ${ch.name.toUpperCase()}_ID>", ... }`);
2337
+ if (!ch.hasTemplate) {
2338
+ parts.push(`# (no children/${ch.name}/ template in this section bundle — \`--props '[]'\` is a stub; add real props for ${ch.name} as needed)`);
2339
+ }
2340
+ }
2341
+ parts.push("```", "");
2342
+ parts.push("**Step 2 — Create the parent section (without `filteredComponentIds` yet — those are wired in Step 3):**");
2343
+ parts.push("", "```bash");
2344
+ parts.push(`npx ikas-component config add-component --name "${compName}" --type ${compType}${baseFlags}${parentPropsJsonStr}`);
2345
+ parts.push("```", "");
2346
+ parts.push("**Step 3 — Wire each slot to its allowed children using the ids captured in Step 1:**");
2347
+ parts.push("", "```bash");
2348
+ for (const slot of slotPlans) {
2349
+ const idsArr = slot.childNames.map((n) => `<${n.toUpperCase()}_ID>`);
2350
+ parts.push(`npx ikas-component config update-prop --component "${compName}" --prop ${slot.propName} \\`);
2351
+ parts.push(` --filteredComponentIds '${JSON.stringify(idsArr)}'`);
2352
+ }
2353
+ parts.push("```", "");
2354
+ 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.");
2355
+ parts.push("");
2356
+ parts.push("**Do NOT manually create or edit `types.ts`** — the CLI commands above regenerate it automatically.");
2357
+ parts.push("");
2358
+ }
2359
+ else {
2360
+ // No child slots — single-step parent command (preserves previous behaviour)
2361
+ const cliCommand = `npx ikas-component config add-component --name "${compName}" --type ${compType}${baseFlags}${parentPropsJsonStr}`;
2362
+ 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
2363
  }
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
2364
  }
2247
2365
  catch {
2248
2366
  // ignore parse errors