@ikas/component-cli 1.4.0-beta.1 → 1.4.0-beta.100

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 (146) hide show
  1. package/dist/commands/add-sections-to-page.d.ts +3 -0
  2. package/dist/commands/add-sections-to-page.d.ts.map +1 -0
  3. package/dist/commands/add-sections-to-page.js +39 -0
  4. package/dist/commands/add-sections-to-page.js.map +1 -0
  5. package/dist/commands/add-to-page.d.ts +3 -0
  6. package/dist/commands/add-to-page.d.ts.map +1 -0
  7. package/dist/commands/add-to-page.js +41 -0
  8. package/dist/commands/add-to-page.js.map +1 -0
  9. package/dist/commands/add.d.ts.map +1 -1
  10. package/dist/commands/add.js +10 -1
  11. package/dist/commands/add.js.map +1 -1
  12. package/dist/commands/build.d.ts.map +1 -1
  13. package/dist/commands/build.js +5 -165
  14. package/dist/commands/build.js.map +1 -1
  15. package/dist/commands/config.d.ts.map +1 -1
  16. package/dist/commands/config.js +484 -157
  17. package/dist/commands/config.js.map +1 -1
  18. package/dist/commands/create-design-tokens.d.ts +24 -0
  19. package/dist/commands/create-design-tokens.d.ts.map +1 -0
  20. package/dist/commands/create-design-tokens.js +317 -0
  21. package/dist/commands/create-design-tokens.js.map +1 -0
  22. package/dist/commands/create-global-variable.d.ts +3 -0
  23. package/dist/commands/create-global-variable.d.ts.map +1 -0
  24. package/dist/commands/create-global-variable.js +53 -0
  25. package/dist/commands/create-global-variable.js.map +1 -0
  26. package/dist/commands/create-page.d.ts +3 -0
  27. package/dist/commands/create-page.d.ts.map +1 -0
  28. package/dist/commands/create-page.js +31 -0
  29. package/dist/commands/create-page.js.map +1 -0
  30. package/dist/commands/delete-theme-globals.d.ts +4 -0
  31. package/dist/commands/delete-theme-globals.d.ts.map +1 -0
  32. package/dist/commands/delete-theme-globals.js +48 -0
  33. package/dist/commands/delete-theme-globals.js.map +1 -0
  34. package/dist/commands/dev.d.ts.map +1 -1
  35. package/dist/commands/dev.js +299 -32
  36. package/dist/commands/dev.js.map +1 -1
  37. package/dist/commands/get-component-props.d.ts +3 -0
  38. package/dist/commands/get-component-props.d.ts.map +1 -0
  39. package/dist/commands/get-component-props.js +32 -0
  40. package/dist/commands/get-component-props.js.map +1 -0
  41. package/dist/commands/get-page-by-type.d.ts +3 -0
  42. package/dist/commands/get-page-by-type.d.ts.map +1 -0
  43. package/dist/commands/get-page-by-type.js +25 -0
  44. package/dist/commands/get-page-by-type.js.map +1 -0
  45. package/dist/commands/get-section-values.d.ts +3 -0
  46. package/dist/commands/get-section-values.d.ts.map +1 -0
  47. package/dist/commands/get-section-values.js +39 -0
  48. package/dist/commands/get-section-values.js.map +1 -0
  49. package/dist/commands/import.d.ts +3 -0
  50. package/dist/commands/import.d.ts.map +1 -0
  51. package/dist/commands/import.js +25 -0
  52. package/dist/commands/import.js.map +1 -0
  53. package/dist/commands/list-entities.d.ts +3 -0
  54. package/dist/commands/list-entities.d.ts.map +1 -0
  55. package/dist/commands/list-entities.js +32 -0
  56. package/dist/commands/list-entities.js.map +1 -0
  57. package/dist/commands/list-imported.d.ts +3 -0
  58. package/dist/commands/list-imported.d.ts.map +1 -0
  59. package/dist/commands/list-imported.js +25 -0
  60. package/dist/commands/list-imported.js.map +1 -0
  61. package/dist/commands/list-page-sections.d.ts +3 -0
  62. package/dist/commands/list-page-sections.d.ts.map +1 -0
  63. package/dist/commands/list-page-sections.js +25 -0
  64. package/dist/commands/list-page-sections.js.map +1 -0
  65. package/dist/commands/list-pages.d.ts +3 -0
  66. package/dist/commands/list-pages.d.ts.map +1 -0
  67. package/dist/commands/list-pages.js +21 -0
  68. package/dist/commands/list-pages.js.map +1 -0
  69. package/dist/commands/list-theme-globals.d.ts +3 -0
  70. package/dist/commands/list-theme-globals.d.ts.map +1 -0
  71. package/dist/commands/list-theme-globals.js +22 -0
  72. package/dist/commands/list-theme-globals.js.map +1 -0
  73. package/dist/commands/publish-theme.d.ts +3 -0
  74. package/dist/commands/publish-theme.d.ts.map +1 -0
  75. package/dist/commands/publish-theme.js +29 -0
  76. package/dist/commands/publish-theme.js.map +1 -0
  77. package/dist/commands/publish.d.ts.map +1 -1
  78. package/dist/commands/publish.js +23 -2
  79. package/dist/commands/publish.js.map +1 -1
  80. package/dist/commands/search-products.d.ts +3 -0
  81. package/dist/commands/search-products.d.ts.map +1 -0
  82. package/dist/commands/search-products.js +40 -0
  83. package/dist/commands/search-products.js.map +1 -0
  84. package/dist/commands/update-global-variable.d.ts +3 -0
  85. package/dist/commands/update-global-variable.d.ts.map +1 -0
  86. package/dist/commands/update-global-variable.js +47 -0
  87. package/dist/commands/update-global-variable.js.map +1 -0
  88. package/dist/commands/update-page-sections.d.ts +3 -0
  89. package/dist/commands/update-page-sections.d.ts.map +1 -0
  90. package/dist/commands/update-page-sections.js +39 -0
  91. package/dist/commands/update-page-sections.js.map +1 -0
  92. package/dist/commands/update-section-prop.d.ts +3 -0
  93. package/dist/commands/update-section-prop.d.ts.map +1 -0
  94. package/dist/commands/update-section-prop.js +59 -0
  95. package/dist/commands/update-section-prop.js.map +1 -0
  96. package/dist/commands/upload-image.d.ts +3 -0
  97. package/dist/commands/upload-image.d.ts.map +1 -0
  98. package/dist/commands/upload-image.js +38 -0
  99. package/dist/commands/upload-image.js.map +1 -0
  100. package/dist/commands/upload-images.d.ts +3 -0
  101. package/dist/commands/upload-images.d.ts.map +1 -0
  102. package/dist/commands/upload-images.js +48 -0
  103. package/dist/commands/upload-images.js.map +1 -0
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +55 -0
  106. package/dist/index.js.map +1 -1
  107. package/dist/types.d.ts +11 -1
  108. package/dist/types.d.ts.map +1 -1
  109. package/dist/utils/compile.d.ts +4 -1
  110. package/dist/utils/compile.d.ts.map +1 -1
  111. package/dist/utils/compile.js +630 -102
  112. package/dist/utils/compile.js.map +1 -1
  113. package/dist/utils/component-helpers.d.ts +64 -3
  114. package/dist/utils/component-helpers.d.ts.map +1 -1
  115. package/dist/utils/component-helpers.js +151 -4
  116. package/dist/utils/component-helpers.js.map +1 -1
  117. package/dist/utils/editor-action-client.d.ts +28 -0
  118. package/dist/utils/editor-action-client.d.ts.map +1 -0
  119. package/dist/utils/editor-action-client.js +116 -0
  120. package/dist/utils/editor-action-client.js.map +1 -0
  121. package/dist/utils/forbid-deep-imports.d.ts +3 -0
  122. package/dist/utils/forbid-deep-imports.d.ts.map +1 -0
  123. package/dist/utils/forbid-deep-imports.js +54 -0
  124. package/dist/utils/forbid-deep-imports.js.map +1 -0
  125. package/dist/utils/forbid-external-packages.d.ts +16 -0
  126. package/dist/utils/forbid-external-packages.d.ts.map +1 -0
  127. package/dist/utils/forbid-external-packages.js +89 -0
  128. package/dist/utils/forbid-external-packages.js.map +1 -0
  129. package/dist/utils/load-image.d.ts +16 -0
  130. package/dist/utils/load-image.d.ts.map +1 -0
  131. package/dist/utils/load-image.js +50 -0
  132. package/dist/utils/load-image.js.map +1 -0
  133. package/dist/utils/websocket-server.d.ts +52 -2
  134. package/dist/utils/websocket-server.d.ts.map +1 -1
  135. package/dist/utils/websocket-server.js +59 -1
  136. package/dist/utils/websocket-server.js.map +1 -1
  137. package/package.json +1 -1
  138. package/templates/add/section.css +11 -0
  139. package/dist/commands/create.d.ts +0 -9
  140. package/dist/commands/create.d.ts.map +0 -1
  141. package/dist/commands/create.js +0 -9
  142. package/dist/commands/create.js.map +0 -1
  143. package/dist/commands/proxy.d.ts +0 -39
  144. package/dist/commands/proxy.d.ts.map +0 -1
  145. package/dist/commands/proxy.js +0 -212
  146. package/dist/commands/proxy.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
- import { PROP_TYPES, toPascalCase, generateTypesFile, generateGlobalTypesFile, collectUsedEnumIds, generateComponentFile, generateStylesFile, generateProjectId, generateComponentId, generateUniqueId, updateBarrelExport, findPropGroup, collectPropGroupIds, movePropGroupInTree, } from "../utils/component-helpers.js";
4
+ import { PROP_TYPES, toPascalCase, generateTypesFile, generateGlobalTypesFile, collectUsedEnumIds, generateComponentFile, generateStylesFile, generateProjectId, isValidProjectId, generateComponentId, generateUniqueId, updateBarrelExport, findPropGroup, collectPropGroupIds, movePropGroupInTree, validateFilteredComponentIds, } from "../utils/component-helpers.js";
5
5
  function loadConfig() {
6
6
  const configPath = path.resolve(process.cwd(), "ikas.config.json");
7
7
  if (!fs.existsSync(configPath)) {
@@ -17,13 +17,85 @@ function loadConfig() {
17
17
  function saveConfig(configPath, config) {
18
18
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
19
19
  }
20
- function findComponent(config, name) {
21
- const pascalName = toPascalCase(name);
22
- return config.components.find((c) => c.name === pascalName);
20
+ /**
21
+ * Resolve a component by canonical id (preferred) or exact PascalCase name.
22
+ * No silent normalization: pass --id from `config list-components`, or pass --name
23
+ * exactly as stored in ikas.config.json. Exits with a structured JSON error
24
+ * (listing valid {id, name} pairs) if zero/both/missing.
25
+ */
26
+ function resolveComponent(config, ref) {
27
+ const id = ref.id?.trim() || undefined;
28
+ const name = ref.name?.trim() || undefined;
29
+ if (!id && !name) {
30
+ console.log(JSON.stringify({
31
+ success: false,
32
+ error: "Component reference required: pass --id (preferred) or --name (exact PascalCase). " +
33
+ "Run `ikas-component config list-components` to see canonical ids.",
34
+ validComponents: config.components.map((c) => ({
35
+ id: c.id,
36
+ name: c.name,
37
+ })),
38
+ }));
39
+ process.exit(1);
40
+ }
41
+ if (id && name) {
42
+ console.log(JSON.stringify({
43
+ success: false,
44
+ error: "Specify only one of --id or --name, not both.",
45
+ }));
46
+ process.exit(1);
47
+ }
48
+ const found = id
49
+ ? config.components.find((c) => c.id === id)
50
+ : config.components.find((c) => c.name === name);
51
+ if (!found) {
52
+ console.log(JSON.stringify({
53
+ success: false,
54
+ error: id
55
+ ? `Component not found: id="${id}".`
56
+ : `Component not found: name="${name}". Names are matched exactly (PascalCase, no normalization).`,
57
+ lookup: id ? { id } : { name },
58
+ validComponents: config.components.map((c) => ({
59
+ id: c.id,
60
+ name: c.name,
61
+ })),
62
+ hint: "Use --id (preferred) or exact PascalCase --name. " +
63
+ "Run `ikas-component config list-components` to see canonical ids.",
64
+ }));
65
+ process.exit(1);
66
+ }
67
+ return found;
23
68
  }
24
69
  function getComponentNames(config) {
25
70
  return config.components.map((c) => c.name);
26
71
  }
72
+ /**
73
+ * Emit a structured JSON error for unknown filteredComponentIds entries and exit non-zero.
74
+ * Lists valid `{ id, name }` pairs so the LLM/caller can recover.
75
+ */
76
+ function emitFilteredComponentIdsError(unknown, components) {
77
+ console.log(JSON.stringify({
78
+ success: false,
79
+ error: `Unknown filteredComponentIds: ${unknown.join(", ")}. ` +
80
+ `Component ids are opaque and cannot be derived from names — they must come from the ` +
81
+ `componentId returned by 'config add-component' or from 'config list'.`,
82
+ validComponents: components.map((c) => ({ id: c.id, name: c.name })),
83
+ }));
84
+ process.exit(1);
85
+ }
86
+ /**
87
+ * Validate `input` ids against `config.components` and return the same array on success.
88
+ * Exits with a JSON error if any id is unknown. Use at every write site that accepts
89
+ * filteredComponentIds from user/LLM input.
90
+ */
91
+ function assertKnownComponentIds(input, config) {
92
+ const components = config?.components ?? [];
93
+ const { unknown } = validateFilteredComponentIds(input, components);
94
+ if (unknown.length > 0) {
95
+ emitFilteredComponentIdsError(unknown, components);
96
+ }
97
+ return input;
98
+ }
27
99
  function regenerateTypes(component, componentType, config) {
28
100
  const componentDir = path.resolve(process.cwd(), path.dirname(component.entry));
29
101
  const typesPath = path.join(componentDir, "types.ts");
@@ -78,6 +150,19 @@ function camelCaseToDisplayName(name) {
78
150
  .replace(/([a-z])([A-Z])/g, "$1 $2")
79
151
  .replace(/^./, (c) => c.toUpperCase());
80
152
  }
153
+ const ALLOWED_PROP_FIELDS = new Set([
154
+ "name",
155
+ "displayName",
156
+ "type",
157
+ "required",
158
+ "description",
159
+ "defaultValue",
160
+ "groupId",
161
+ "typeId",
162
+ "enumTypeId",
163
+ "filteredComponentIds",
164
+ "privateVarMap",
165
+ ]);
81
166
  /**
82
167
  * Parse and validate the --props JSON flag for add-component.
83
168
  * Returns validated ComponentProp[] or exits with error.
@@ -105,6 +190,16 @@ async function parsePropsFlag(propsJson, config, configPath) {
105
190
  const props = [];
106
191
  for (const raw of rawProps) {
107
192
  const r = raw;
193
+ const unknownFields = Object.keys(r).filter((k) => !ALLOWED_PROP_FIELDS.has(k));
194
+ if (unknownFields.length > 0) {
195
+ console.log(JSON.stringify({
196
+ success: false,
197
+ error: `Unknown field(s) in --props entry: ${unknownFields.join(", ")}. ` +
198
+ `Allowed: ${[...ALLOWED_PROP_FIELDS].join(", ")}. ` +
199
+ `Note: use "groupId" (not "group") and "defaultValue" (not "default") in --props JSON.`,
200
+ }));
201
+ process.exit(1);
202
+ }
108
203
  if (!r.name || typeof r.name !== "string") {
109
204
  console.log(JSON.stringify({
110
205
  success: false,
@@ -145,6 +240,24 @@ async function parsePropsFlag(propsJson, config, configPath) {
145
240
  }));
146
241
  process.exit(1);
147
242
  }
243
+ // Validate LINK / LIST_OF_LINK defaultValue shape (reject JSON strings and legacy { href })
244
+ if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
245
+ r.defaultValue !== undefined) {
246
+ const linkError = validateLinkDefaultValue(propType, r.defaultValue, r.name);
247
+ if (linkError) {
248
+ console.log(JSON.stringify({ success: false, error: linkError }));
249
+ process.exit(1);
250
+ }
251
+ }
252
+ // Validate SVG / SVG_LIST defaultValue (reject malformed/oversized/unsafe SVG)
253
+ if ((propType === "SVG" || propType === "SVG_LIST") &&
254
+ r.defaultValue !== undefined) {
255
+ const svgError = validateSvgDefaultValue(propType, r.defaultValue, r.name);
256
+ if (svgError) {
257
+ console.log(JSON.stringify({ success: false, error: svgError }));
258
+ process.exit(1);
259
+ }
260
+ }
148
261
  // Auto-generate displayName if omitted
149
262
  const displayName = typeof r.displayName === "string" && r.displayName
150
263
  ? r.displayName
@@ -169,7 +282,10 @@ async function parsePropsFlag(propsJson, config, configPath) {
169
282
  process.exit(1);
170
283
  }
171
284
  // Validate enum type exists (checks config then live dev server)
172
- if (config && configPath && typeof r.enumTypeId === "string" && !r.enumTypeId.startsWith("@ikas/")) {
285
+ if (config &&
286
+ configPath &&
287
+ typeof r.enumTypeId === "string" &&
288
+ !r.enumTypeId.startsWith("@ikas/")) {
173
289
  const resolved = await resolveEnumType(r.enumTypeId, config, configPath);
174
290
  if (!resolved) {
175
291
  console.log(JSON.stringify({
@@ -188,9 +304,7 @@ async function parsePropsFlag(propsJson, config, configPath) {
188
304
  ...(typeof r.description === "string" && r.description
189
305
  ? { description: r.description }
190
306
  : {}),
191
- ...(r.defaultValue !== undefined
192
- ? { defaultValue: r.defaultValue }
193
- : {}),
307
+ ...(r.defaultValue !== undefined ? { defaultValue: r.defaultValue } : {}),
194
308
  ...(typeof r.groupId === "string" && r.groupId
195
309
  ? { groupId: r.groupId }
196
310
  : {}),
@@ -200,11 +314,19 @@ async function parsePropsFlag(propsJson, config, configPath) {
200
314
  ...(propType === "ENUM" && typeof r.enumTypeId === "string"
201
315
  ? { enumTypeId: r.enumTypeId }
202
316
  : {}),
203
- ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") && r.filteredComponentIds && Array.isArray(r.filteredComponentIds)
204
- ? { filteredComponentIds: r.filteredComponentIds }
317
+ ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
318
+ r.filteredComponentIds &&
319
+ Array.isArray(r.filteredComponentIds)
320
+ ? {
321
+ filteredComponentIds: assertKnownComponentIds(r.filteredComponentIds, config),
322
+ }
205
323
  : {}),
206
- ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") && r.privateVarMap && typeof r.privateVarMap === "object"
207
- ? { privateVarMap: r.privateVarMap }
324
+ ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
325
+ r.privateVarMap &&
326
+ typeof r.privateVarMap === "object"
327
+ ? {
328
+ privateVarMap: r.privateVarMap,
329
+ }
208
330
  : {}),
209
331
  };
210
332
  props.push(prop);
@@ -236,14 +358,27 @@ async function addComponent(name, options) {
236
358
  if (!config.projectId) {
237
359
  config.projectId = generateProjectId();
238
360
  }
361
+ else if (!isValidProjectId(config.projectId)) {
362
+ // A malformed projectId (hand-edited config) breaks the editor's republish
363
+ // stability: published/local id prefixes stop matching and every publish
364
+ // INSERTs duplicate design elements. Fail fast instead of corrupting data.
365
+ console.log(JSON.stringify({
366
+ success: false,
367
+ error: `Invalid projectId "${config.projectId}" in ikas.config.json. ` +
368
+ "projectId must be at least 6 alphanumeric characters (no '-' or '_'). " +
369
+ "Restore the original generated value — do not hand-edit it.",
370
+ }));
371
+ process.exit(1);
372
+ }
373
+ // Parse --props (and validate filteredComponentIds) BEFORE any filesystem side-effects
374
+ // so a bad payload doesn't leave an empty component directory behind.
375
+ const props = options.props
376
+ ? await parsePropsFlag(options.props, config, configPath)
377
+ : [];
239
378
  const componentId = generateComponentId(config.projectId);
240
379
  const componentDir = path.resolve(process.cwd(), `src/components/${pascalName}`);
241
380
  // Create directory
242
381
  fs.mkdirSync(componentDir, { recursive: true });
243
- // Parse --props if provided, otherwise empty
244
- const props = options.props
245
- ? await parsePropsFlag(options.props, config, configPath)
246
- : [];
247
382
  // Auto-create prop groups from groupId references in props
248
383
  const referencedGroupIds = new Set();
249
384
  for (const prop of props) {
@@ -294,16 +429,9 @@ async function addComponent(name, options) {
294
429
  ],
295
430
  }));
296
431
  }
297
- async function addProp(componentName, options) {
432
+ async function addProp(ref, options) {
298
433
  const { config, configPath } = loadConfig();
299
- const component = findComponent(config, componentName);
300
- if (!component) {
301
- console.log(JSON.stringify({
302
- success: false,
303
- error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
304
- }));
305
- process.exit(1);
306
- }
434
+ const component = resolveComponent(config, ref);
307
435
  // Validate prop type
308
436
  const propType = options.type.toUpperCase();
309
437
  if (!PROP_TYPES.includes(propType)) {
@@ -347,7 +475,9 @@ async function addProp(componentName, options) {
347
475
  }
348
476
  // Validate that enumTypeId references an existing enum type
349
477
  // (checks config.customTypes first, then falls back to the live dev server)
350
- if (propType === "ENUM" && options.enumTypeId && !options.enumTypeId.startsWith("@ikas/")) {
478
+ if (propType === "ENUM" &&
479
+ options.enumTypeId &&
480
+ !options.enumTypeId.startsWith("@ikas/")) {
351
481
  const resolved = await resolveEnumType(options.enumTypeId, config, configPath);
352
482
  if (!resolved) {
353
483
  console.log(JSON.stringify({
@@ -377,31 +507,66 @@ async function addProp(componentName, options) {
377
507
  }
378
508
  // Parse filteredComponentIds JSON for COMPONENT/COMPONENT_LIST
379
509
  let parsedFilteredIds;
380
- if (options.filteredComponentIds && (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
510
+ if (options.filteredComponentIds &&
511
+ (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
381
512
  try {
382
513
  parsedFilteredIds = JSON.parse(options.filteredComponentIds);
383
514
  if (!Array.isArray(parsedFilteredIds)) {
384
- console.log(JSON.stringify({ success: false, error: "--filteredComponentIds must be a JSON array of strings." }));
515
+ console.log(JSON.stringify({
516
+ success: false,
517
+ error: "--filteredComponentIds must be a JSON array of strings.",
518
+ }));
385
519
  process.exit(1);
386
520
  }
387
521
  }
388
522
  catch {
389
- console.log(JSON.stringify({ success: false, error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}` }));
523
+ console.log(JSON.stringify({
524
+ success: false,
525
+ error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
526
+ }));
390
527
  process.exit(1);
391
528
  }
529
+ assertKnownComponentIds(parsedFilteredIds, config);
392
530
  }
393
531
  // Parse privateVarMap JSON for COMPONENT/COMPONENT_LIST
394
532
  let parsedPrivateVarMap;
395
- if (options.privateVarMap && (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
533
+ if (options.privateVarMap &&
534
+ (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
396
535
  try {
397
536
  parsedPrivateVarMap = JSON.parse(options.privateVarMap);
398
- if (typeof parsedPrivateVarMap !== "object" || Array.isArray(parsedPrivateVarMap)) {
399
- console.log(JSON.stringify({ success: false, error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}." }));
537
+ if (typeof parsedPrivateVarMap !== "object" ||
538
+ Array.isArray(parsedPrivateVarMap)) {
539
+ console.log(JSON.stringify({
540
+ success: false,
541
+ error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
542
+ }));
400
543
  process.exit(1);
401
544
  }
402
545
  }
403
546
  catch {
404
- console.log(JSON.stringify({ success: false, error: `Invalid --privateVarMap JSON: ${options.privateVarMap}` }));
547
+ console.log(JSON.stringify({
548
+ success: false,
549
+ error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
550
+ }));
551
+ process.exit(1);
552
+ }
553
+ }
554
+ const parsedDefaultValue = options.defaultValue !== undefined
555
+ ? parseDefaultValue(options.defaultValue, propType)
556
+ : undefined;
557
+ if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
558
+ parsedDefaultValue !== undefined) {
559
+ const linkError = validateLinkDefaultValue(propType, parsedDefaultValue, options.name);
560
+ if (linkError) {
561
+ console.log(JSON.stringify({ success: false, error: linkError }));
562
+ process.exit(1);
563
+ }
564
+ }
565
+ if ((propType === "SVG" || propType === "SVG_LIST") &&
566
+ parsedDefaultValue !== undefined) {
567
+ const svgError = validateSvgDefaultValue(propType, parsedDefaultValue, options.name);
568
+ if (svgError) {
569
+ console.log(JSON.stringify({ success: false, error: svgError }));
405
570
  process.exit(1);
406
571
  }
407
572
  }
@@ -411,8 +576,8 @@ async function addProp(componentName, options) {
411
576
  type: propType,
412
577
  required: options.required ?? false,
413
578
  ...(options.description ? { description: options.description } : {}),
414
- ...(options.defaultValue !== undefined
415
- ? { defaultValue: parseDefaultValue(options.defaultValue, propType) }
579
+ ...(parsedDefaultValue !== undefined
580
+ ? { defaultValue: parsedDefaultValue }
416
581
  : {}),
417
582
  ...(options.group ? { groupId: options.group } : {}),
418
583
  ...(options.typeId ? { typeId: options.typeId } : {}),
@@ -435,8 +600,12 @@ async function addProp(componentName, options) {
435
600
  required: newProp.required,
436
601
  ...(newProp.groupId ? { groupId: newProp.groupId } : {}),
437
602
  ...(newProp.enumTypeId ? { enumTypeId: newProp.enumTypeId } : {}),
438
- ...(newProp.filteredComponentIds ? { filteredComponentIds: newProp.filteredComponentIds } : {}),
439
- ...(newProp.privateVarMap ? { privateVarMap: newProp.privateVarMap } : {}),
603
+ ...(newProp.filteredComponentIds
604
+ ? { filteredComponentIds: newProp.filteredComponentIds }
605
+ : {}),
606
+ ...(newProp.privateVarMap
607
+ ? { privateVarMap: newProp.privateVarMap }
608
+ : {}),
440
609
  },
441
610
  }));
442
611
  }
@@ -446,20 +615,150 @@ function parseDefaultValue(value, propType) {
446
615
  return Number(value);
447
616
  case "BOOLEAN":
448
617
  return value === "true";
618
+ case "LINK":
619
+ case "LIST_OF_LINK":
620
+ // The --default-value flag arrives as a string; LINK values must be stored as a
621
+ // typed object. Parse it here; if it is not valid JSON, return the raw string so
622
+ // validateLinkDefaultValue can report a precise error.
623
+ try {
624
+ return JSON.parse(value);
625
+ }
626
+ catch {
627
+ return value;
628
+ }
629
+ case "SVG_LIST":
630
+ // Same reasoning as LINK_LIST: SVG_LIST must be stored as an array of SVG strings.
631
+ // Parse it here; if it is not valid JSON, return the raw string so
632
+ // validateSvgDefaultValue can report a precise "must be an array" error.
633
+ try {
634
+ return JSON.parse(value);
635
+ }
636
+ catch {
637
+ return value;
638
+ }
449
639
  default:
450
640
  return value;
451
641
  }
452
642
  }
453
- function updateProp(componentName, options) {
643
+ const VALID_LINK_TYPES = ["PAGE", "EXTERNAL", "FILE"];
644
+ const LINK_SHAPE_HELP = 'A link must be an object: { "linkType": "EXTERNAL", "label": "Text", "externalLink": "https://...", "subLinks": [] } ' +
645
+ 'for external links, or { "linkType": "PAGE", "label": "Text", "pageType": "INDEX", "subLinks": [] } for store pages. ' +
646
+ "Do NOT pass a JSON string or a { label, href } shape.";
647
+ // Returns the index of the first array element that fails `fn`, as an error string, or null.
648
+ const firstError = (arr, fn) => arr.reduce((acc, item, index) => acc ?? fn(item, index), null);
649
+ const validateSingleLink = (link, propName, path) => {
650
+ const where = `defaultValue for prop "${propName}"${path}`;
651
+ if (typeof link === "string") {
652
+ return `${where} is a JSON string but must be an object. ${LINK_SHAPE_HELP}`;
653
+ }
654
+ if (!link || typeof link !== "object" || Array.isArray(link)) {
655
+ return `${where} must be a link object. ${LINK_SHAPE_HELP}`;
656
+ }
657
+ const l = link;
658
+ if ("href" in l && !("linkType" in l)) {
659
+ return `${where} uses a legacy { href } shape without "linkType". Use "linkType" + "externalLink"/"pageType" instead. ${LINK_SHAPE_HELP}`;
660
+ }
661
+ if (typeof l.linkType !== "string" ||
662
+ !VALID_LINK_TYPES.includes(l.linkType)) {
663
+ return `${where} must have a valid "linkType" (one of ${VALID_LINK_TYPES.join(", ")}). ${LINK_SHAPE_HELP}`;
664
+ }
665
+ if (l.subLinks !== undefined && !Array.isArray(l.subLinks)) {
666
+ return `${where}: "subLinks" must be an array (use [] when there are none).`;
667
+ }
668
+ if (Array.isArray(l.subLinks)) {
669
+ return firstError(l.subLinks, (sl, i) => validateSingleLink(sl, propName, `${path}.subLinks[${i}]`));
670
+ }
671
+ return null;
672
+ };
673
+ // Validates a LINK / LIST_OF_LINK defaultValue. Returns an error message, or null if valid.
674
+ // These defaults are a frequent source of bad data: agents tend to pass a JSON string or a
675
+ // legacy { label, href } shape. Rejecting them at authoring time keeps malformed link values
676
+ // out of published themes.
677
+ function validateLinkDefaultValue(propType, value, propName) {
678
+ if (value === undefined || value === null)
679
+ return null;
680
+ if (propType === "LINK") {
681
+ return validateSingleLink(value, propName, "");
682
+ }
683
+ // LIST_OF_LINK
684
+ if (typeof value === "string") {
685
+ return `defaultValue for prop "${propName}" is a JSON string but must be an object { "links": [...] }. ${LINK_SHAPE_HELP}`;
686
+ }
687
+ if (!value ||
688
+ typeof value !== "object" ||
689
+ Array.isArray(value) ||
690
+ !Array.isArray(value.links)) {
691
+ return `defaultValue for prop "${propName}" must be an object with a "links" array: { "links": [ <link>, ... ] }. ${LINK_SHAPE_HELP}`;
692
+ }
693
+ const links = value.links;
694
+ return firstError(links, (link, i) => validateSingleLink(link, propName, `.links[${i}]`));
695
+ }
696
+ const SVG_DEFAULT_HELP = "An SVG default must be a single well-formed <svg>…</svg> string ≤ 64 KB with no embedded base64 raster images. " +
697
+ "Tip: the editor uses each svg's `class` attribute as its display name — set class=\"my-icon\" to name it; " +
698
+ "SVG_LIST items with no class show as svg-1, svg-2, ….";
699
+ // Keep the cap and base64 pattern in sync with @ikas/editor-models validate-svg.ts (the
700
+ // authoritative validator). Duplicated here on purpose — see validateSvgShape below.
701
+ const SVG_MAX_SIZE_KB = 64;
702
+ const SVG_BASE64_RASTER_RE = /data:image\/[a-z0-9.+-]+;base64/i;
703
+ const SVG_SHAPE_RE = /^\s*(?:<\?xml[^>]*\?>\s*)?(?:<!DOCTYPE[^>]*>\s*)?<svg[\s>][\s\S]*<\/svg>\s*$/i;
704
+ // Count TOP-LEVEL <svg> roots without a DOM: scan svg open/close tags in order and track
705
+ // nesting depth. A root is any <svg> opened at depth 0. This matches the editor's DOM-based
706
+ // single-root check — it rejects siblings (`<svg/><svg/>`) while allowing a nested <svg>.
707
+ // Self-closing <svg .../> counts as a root but opens no nesting level.
708
+ function svgRootCount(input) {
709
+ const tags = input.match(/<svg\b[^>]*>|<\/svg\s*>/gi) || [];
710
+ return tags.reduce((acc, tag) => {
711
+ if (tag.startsWith("</"))
712
+ return { depth: Math.max(0, acc.depth - 1), roots: acc.roots };
713
+ const roots = acc.depth === 0 ? acc.roots + 1 : acc.roots;
714
+ const depth = /\/>\s*$/.test(tag) ? acc.depth : acc.depth + 1;
715
+ return { depth, roots };
716
+ }, { depth: 0, roots: 0 }).roots;
717
+ }
718
+ // Lightweight, dependency-free SVG structure check. This standalone CLI is publishable and
719
+ // must not depend on the private @ikas/editor-models package (nor pull a DOM/DOMPurify into a
720
+ // CLI), so it mirrors the RULES of the editor's validateSvg (size, base64, single <svg> root)
721
+ // for fast authoring feedback. The authoritative sanitize runs in the editor/upload path.
722
+ // Returns a rejection reason, or null if the shape is acceptable.
723
+ function validateSvgShape(input) {
724
+ if (typeof input !== "string" || input.trim() === "")
725
+ return "empty or not a string";
726
+ if (Buffer.byteLength(input, "utf8") > SVG_MAX_SIZE_KB * 1024)
727
+ return `exceeds ${SVG_MAX_SIZE_KB} KB`;
728
+ if (SVG_BASE64_RASTER_RE.test(input))
729
+ return "embedded base64 raster image not allowed";
730
+ if (!SVG_SHAPE_RE.test(input))
731
+ return "not a single-root <svg> document";
732
+ if (svgRootCount(input) !== 1)
733
+ return "must be exactly one <svg> root";
734
+ return null;
735
+ }
736
+ // Validates an SVG / SVG_LIST defaultValue. Returns an error message, or null if valid.
737
+ // Authoring an SVG default is a common mistake source for agents; rejecting here (with an
738
+ // instructive message) keeps malformed/oversized SVGs out of published components.
739
+ function validateSvgDefaultValue(propType, value, propName) {
740
+ if (value === undefined || value === null)
741
+ return null;
742
+ if (propType === "SVG") {
743
+ const reason = validateSvgShape(value);
744
+ return reason
745
+ ? `defaultValue for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
746
+ : null;
747
+ }
748
+ // SVG_LIST
749
+ if (!Array.isArray(value)) {
750
+ return `defaultValue for prop "${propName}" must be an array of SVG strings: [ "<svg>…</svg>", ... ]. ${SVG_DEFAULT_HELP}`;
751
+ }
752
+ return firstError(value, (svg, i) => {
753
+ const reason = validateSvgShape(svg);
754
+ return reason
755
+ ? `defaultValue[${i}] for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
756
+ : null;
757
+ });
758
+ }
759
+ function updateProp(ref, options) {
454
760
  const { config, configPath } = loadConfig();
455
- const component = findComponent(config, componentName);
456
- if (!component) {
457
- console.log(JSON.stringify({
458
- success: false,
459
- error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
460
- }));
461
- process.exit(1);
462
- }
761
+ const component = resolveComponent(config, ref);
463
762
  const propIndex = component.props.findIndex((p) => p.name === options.prop);
464
763
  if (propIndex === -1) {
465
764
  console.log(JSON.stringify({
@@ -490,7 +789,15 @@ function updateProp(componentName, options) {
490
789
  prop.description = options.description || undefined;
491
790
  }
492
791
  if (options.defaultValue !== undefined) {
493
- prop.defaultValue = parseDefaultValue(options.defaultValue, prop.type);
792
+ const parsed = parseDefaultValue(options.defaultValue, prop.type);
793
+ if (prop.type === "LINK" || prop.type === "LIST_OF_LINK") {
794
+ const linkError = validateLinkDefaultValue(prop.type, parsed, prop.name);
795
+ if (linkError) {
796
+ console.log(JSON.stringify({ success: false, error: linkError }));
797
+ process.exit(1);
798
+ }
799
+ }
800
+ prop.defaultValue = parsed;
494
801
  }
495
802
  if (options.group !== undefined) {
496
803
  if (options.group === "" || options.group === "none") {
@@ -532,21 +839,32 @@ function updateProp(componentName, options) {
532
839
  }
533
840
  }
534
841
  if (options.filteredComponentIds !== undefined) {
535
- if (options.filteredComponentIds === "" || options.filteredComponentIds === "none") {
842
+ if (options.filteredComponentIds === "" ||
843
+ options.filteredComponentIds === "none") {
536
844
  delete prop.filteredComponentIds;
537
845
  }
538
846
  else {
539
847
  try {
540
848
  const parsed = JSON.parse(options.filteredComponentIds);
541
849
  if (!Array.isArray(parsed)) {
542
- console.log(JSON.stringify({ success: false, error: "--filteredComponentIds must be a JSON array of strings." }));
850
+ console.log(JSON.stringify({
851
+ success: false,
852
+ error: "--filteredComponentIds must be a JSON array of strings.",
853
+ }));
543
854
  process.exit(1);
544
855
  }
856
+ assertKnownComponentIds(parsed, config);
545
857
  prop.filteredComponentIds = parsed;
546
858
  }
547
- catch {
548
- console.log(JSON.stringify({ success: false, error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}` }));
549
- process.exit(1);
859
+ catch (err) {
860
+ if (err instanceof SyntaxError) {
861
+ console.log(JSON.stringify({
862
+ success: false,
863
+ error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
864
+ }));
865
+ process.exit(1);
866
+ }
867
+ throw err;
550
868
  }
551
869
  }
552
870
  }
@@ -558,13 +876,19 @@ function updateProp(componentName, options) {
558
876
  try {
559
877
  const parsed = JSON.parse(options.privateVarMap);
560
878
  if (typeof parsed !== "object" || Array.isArray(parsed)) {
561
- console.log(JSON.stringify({ success: false, error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}." }));
879
+ console.log(JSON.stringify({
880
+ success: false,
881
+ error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
882
+ }));
562
883
  process.exit(1);
563
884
  }
564
885
  prop.privateVarMap = parsed;
565
886
  }
566
887
  catch {
567
- console.log(JSON.stringify({ success: false, error: `Invalid --privateVarMap JSON: ${options.privateVarMap}` }));
888
+ console.log(JSON.stringify({
889
+ success: false,
890
+ error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
891
+ }));
568
892
  process.exit(1);
569
893
  }
570
894
  }
@@ -584,21 +908,16 @@ function updateProp(componentName, options) {
584
908
  ...(prop.groupId ? { groupId: prop.groupId } : {}),
585
909
  ...(prop.typeId ? { typeId: prop.typeId } : {}),
586
910
  ...(prop.enumTypeId ? { enumTypeId: prop.enumTypeId } : {}),
587
- ...(prop.filteredComponentIds ? { filteredComponentIds: prop.filteredComponentIds } : {}),
911
+ ...(prop.filteredComponentIds
912
+ ? { filteredComponentIds: prop.filteredComponentIds }
913
+ : {}),
588
914
  ...(prop.privateVarMap ? { privateVarMap: prop.privateVarMap } : {}),
589
915
  },
590
916
  }));
591
917
  }
592
- function removeProp(componentName, options) {
918
+ function removeProp(ref, options) {
593
919
  const { config, configPath } = loadConfig();
594
- const component = findComponent(config, componentName);
595
- if (!component) {
596
- console.log(JSON.stringify({
597
- success: false,
598
- error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
599
- }));
600
- process.exit(1);
601
- }
920
+ const component = resolveComponent(config, ref);
602
921
  const propIndex = component.props.findIndex((p) => p.name === options.prop);
603
922
  if (propIndex === -1) {
604
923
  console.log(JSON.stringify({
@@ -619,21 +938,30 @@ function removeProp(componentName, options) {
619
938
  remainingProps: component.props.map((p) => p.name),
620
939
  }));
621
940
  }
622
- function removeComponent(options) {
623
- const { config, configPath } = loadConfig();
624
- const pascalName = toPascalCase(options.name);
625
- const componentIndex = config.components.findIndex((c) => c.name === pascalName);
626
- if (componentIndex === -1) {
627
- console.log(JSON.stringify({
628
- success: false,
629
- error: `Component "${options.name}" not found. Available: ${getComponentNames(config).join(", ")}`,
630
- }));
631
- process.exit(1);
941
+ /**
942
+ * Strip a deleted component's ID from every other component's COMPONENT/COMPONENT_LIST
943
+ * `filteredComponentIds` allowlist. Empty arrays are deleted to keep the field optional
944
+ * (mirrors the editor-side convention).
945
+ */
946
+ function pruneFilteredComponentIdRefs(config, removedId) {
947
+ for (const component of config.components) {
948
+ for (const prop of component.props || []) {
949
+ if (!prop.filteredComponentIds)
950
+ continue;
951
+ prop.filteredComponentIds = prop.filteredComponentIds.filter((id) => id !== removedId);
952
+ if (prop.filteredComponentIds.length === 0)
953
+ delete prop.filteredComponentIds;
954
+ }
632
955
  }
633
- const component = config.components[componentIndex];
956
+ }
957
+ function removeComponent(ref) {
958
+ const { config, configPath } = loadConfig();
959
+ const component = resolveComponent(config, ref);
960
+ const componentIndex = config.components.indexOf(component);
634
961
  const componentDir = path.resolve(process.cwd(), path.dirname(component.entry));
635
- // Remove component from config
962
+ // Remove component from config and strip orphaned references from remaining components
636
963
  config.components.splice(componentIndex, 1);
964
+ pruneFilteredComponentIdRefs(config, component.id);
637
965
  saveConfig(configPath, config);
638
966
  // Remove component directory
639
967
  if (fs.existsSync(componentDir)) {
@@ -643,21 +971,18 @@ function removeComponent(options) {
643
971
  updateBarrelExport(process.cwd(), getComponentNames(config));
644
972
  console.log(JSON.stringify({
645
973
  success: true,
646
- removedComponent: pascalName,
974
+ removedComponentId: component.id,
975
+ removedComponentName: component.name,
647
976
  removedDirectory: path.relative(process.cwd(), componentDir),
648
- remainingComponents: getComponentNames(config),
977
+ remainingComponents: config.components.map((c) => ({
978
+ id: c.id,
979
+ name: c.name,
980
+ })),
649
981
  }));
650
982
  }
651
- function addPropGroup(componentName, options) {
983
+ function addPropGroup(ref, options) {
652
984
  const { config, configPath } = loadConfig();
653
- const component = findComponent(config, componentName);
654
- if (!component) {
655
- console.log(JSON.stringify({
656
- success: false,
657
- error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
658
- }));
659
- process.exit(1);
660
- }
985
+ const component = resolveComponent(config, ref);
661
986
  if (!component.propGroups)
662
987
  component.propGroups = [];
663
988
  // Check uniqueness
@@ -708,16 +1033,9 @@ function addPropGroup(componentName, options) {
708
1033
  },
709
1034
  }));
710
1035
  }
711
- function updatePropGroup(componentName, options) {
1036
+ function updatePropGroup(ref, options) {
712
1037
  const { config, configPath } = loadConfig();
713
- const component = findComponent(config, componentName);
714
- if (!component) {
715
- console.log(JSON.stringify({
716
- success: false,
717
- error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
718
- }));
719
- process.exit(1);
720
- }
1038
+ const component = resolveComponent(config, ref);
721
1039
  if (!component.propGroups || component.propGroups.length === 0) {
722
1040
  console.log(JSON.stringify({
723
1041
  success: false,
@@ -747,16 +1065,9 @@ function updatePropGroup(componentName, options) {
747
1065
  },
748
1066
  }));
749
1067
  }
750
- function removePropGroup(componentName, options) {
1068
+ function removePropGroup(ref, options) {
751
1069
  const { config, configPath } = loadConfig();
752
- const component = findComponent(config, componentName);
753
- if (!component) {
754
- console.log(JSON.stringify({
755
- success: false,
756
- error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
757
- }));
758
- process.exit(1);
759
- }
1070
+ const component = resolveComponent(config, ref);
760
1071
  if (!component.propGroups || component.propGroups.length === 0) {
761
1072
  console.log(JSON.stringify({
762
1073
  success: false,
@@ -773,10 +1084,10 @@ function removePropGroup(componentName, options) {
773
1084
  process.exit(1);
774
1085
  }
775
1086
  if (found.parent) {
776
- found.parent.children = found.parent.children.filter(c => c.id !== options.id);
1087
+ found.parent.children = found.parent.children.filter((c) => c.id !== options.id);
777
1088
  }
778
1089
  else {
779
- component.propGroups = component.propGroups.filter(g => g.id !== options.id);
1090
+ component.propGroups = component.propGroups.filter((g) => g.id !== options.id);
780
1091
  }
781
1092
  // Clear groupId from props referencing the deleted group
782
1093
  for (const prop of component.props) {
@@ -789,19 +1100,12 @@ function removePropGroup(componentName, options) {
789
1100
  success: true,
790
1101
  componentName: component.name,
791
1102
  removedPropGroup: options.id,
792
- remainingPropGroups: component.propGroups.map(g => g.id),
1103
+ remainingPropGroups: component.propGroups.map((g) => g.id),
793
1104
  }));
794
1105
  }
795
- function movePropGroup(componentName, options) {
1106
+ function movePropGroup(ref, options) {
796
1107
  const { config, configPath } = loadConfig();
797
- const component = findComponent(config, componentName);
798
- if (!component) {
799
- console.log(JSON.stringify({
800
- success: false,
801
- error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
802
- }));
803
- process.exit(1);
804
- }
1108
+ const component = resolveComponent(config, ref);
805
1109
  if (!component.propGroups || component.propGroups.length === 0) {
806
1110
  console.log(JSON.stringify({
807
1111
  success: false,
@@ -813,7 +1117,10 @@ function movePropGroup(componentName, options) {
813
1117
  component.propGroups = movePropGroupInTree(component.propGroups, options.id, options.parent || null, options.index);
814
1118
  }
815
1119
  catch (e) {
816
- console.log(JSON.stringify({ success: false, error: e.message || "Failed to move prop group" }));
1120
+ console.log(JSON.stringify({
1121
+ success: false,
1122
+ error: e.message || "Failed to move prop group",
1123
+ }));
817
1124
  process.exit(1);
818
1125
  }
819
1126
  saveConfig(configPath, config);
@@ -824,16 +1131,9 @@ function movePropGroup(componentName, options) {
824
1131
  parentGroupId: options.parent || null,
825
1132
  }));
826
1133
  }
827
- function updateComponent(options) {
1134
+ function updateComponent(ref, options) {
828
1135
  const { config, configPath } = loadConfig();
829
- const component = findComponent(config, options.name);
830
- if (!component) {
831
- console.log(JSON.stringify({
832
- success: false,
833
- error: `Component "${options.name}" not found. Available: ${getComponentNames(config).join(", ")}`,
834
- }));
835
- process.exit(1);
836
- }
1136
+ const component = resolveComponent(config, ref);
837
1137
  if (options.isHeader !== undefined) {
838
1138
  if (options.isHeader) {
839
1139
  component.isHeader = true;
@@ -876,7 +1176,9 @@ function listComponents() {
876
1176
  required: p.required,
877
1177
  ...(p.groupId ? { groupId: p.groupId } : {}),
878
1178
  })),
879
- ...(c.propGroups && c.propGroups.length > 0 ? { propGroups: c.propGroups } : {}),
1179
+ ...(c.propGroups && c.propGroups.length > 0
1180
+ ? { propGroups: c.propGroups }
1181
+ : {}),
880
1182
  }));
881
1183
  console.log(JSON.stringify({
882
1184
  success: true,
@@ -890,7 +1192,9 @@ function listComponents() {
890
1192
  async function addEnum(options) {
891
1193
  const { config, configPath } = loadConfig();
892
1194
  // Validate name
893
- if (!options.name || typeof options.name !== "string" || !options.name.trim()) {
1195
+ if (!options.name ||
1196
+ typeof options.name !== "string" ||
1197
+ !options.name.trim()) {
894
1198
  console.log(JSON.stringify({ success: false, error: "Enum name is required." }));
895
1199
  process.exit(1);
896
1200
  }
@@ -1004,7 +1308,7 @@ function updateEnum(options) {
1004
1308
  Object.keys(enumOptions).length === 0) {
1005
1309
  console.log(JSON.stringify({
1006
1310
  success: false,
1007
- error: 'Options must be a non-empty JSON object.',
1311
+ error: "Options must be a non-empty JSON object.",
1008
1312
  }));
1009
1313
  process.exit(1);
1010
1314
  }
@@ -1077,8 +1381,7 @@ function listEnums() {
1077
1381
  }
1078
1382
  // --- Command registration ---
1079
1383
  export function createConfigCommand() {
1080
- const config = new Command("config")
1081
- .description("Non-interactive commands for managing ikas.config.json (designed for AI agents and scripting)");
1384
+ const config = new Command("config").description("Non-interactive commands for managing ikas.config.json (designed for AI agents and scripting)");
1082
1385
  config
1083
1386
  .command("add-component")
1084
1387
  .description("Add a new component to the project")
@@ -1086,14 +1389,16 @@ export function createConfigCommand() {
1086
1389
  .option("--type <type>", "Component type: section or component", "component")
1087
1390
  .option("--isHeader", "Mark this section as the store header (only for type: section)")
1088
1391
  .option("--isFooter", "Mark this section as the store footer (only for type: section)")
1089
- .option("--props <json>", "JSON array of props, e.g. '[{\"name\":\"title\",\"type\":\"TEXT\",\"required\":true}]'. Each prop needs name+type; displayName is auto-generated from name if omitted.")
1392
+ .option("--props <json>", "JSON array of props. Required per entry: name, type. Optional: displayName (auto from name), required, description, defaultValue, groupId, typeId (TYPE props), enumTypeId (ENUM props), filteredComponentIds, privateVarMap. " +
1393
+ 'Example: \'[{"name":"title","type":"TEXT","required":true,"defaultValue":"Hello","groupId":"basic"}]\'')
1090
1394
  .action((options) => {
1091
1395
  addComponent(options.name, options);
1092
1396
  });
1093
1397
  config
1094
1398
  .command("add-prop")
1095
1399
  .description("Add a prop to a component")
1096
- .requiredOption("--component <name>", "Component name")
1400
+ .option("--component-id <id>", "Component id (preferred; from `config list-components`)")
1401
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1097
1402
  .requiredOption("--name <name>", "Prop name (camelCase)")
1098
1403
  .requiredOption("--displayName <displayName>", "Display name in editor")
1099
1404
  .requiredOption("--type <type>", `Prop type: ${PROP_TYPES.join(", ")}`)
@@ -1104,14 +1409,15 @@ export function createConfigCommand() {
1104
1409
  .option("--typeId <typeId>", "Type ID for TYPE props (required when type is TYPE)")
1105
1410
  .option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (required when type is ENUM)")
1106
1411
  .option("--filteredComponentIds <json>", "JSON array of component IDs to restrict selection (for COMPONENT/COMPONENT_LIST)")
1107
- .option("--privateVarMap <json>", 'JSON object mapping variable keys to {id, typeId} (for COMPONENT/COMPONENT_LIST)')
1412
+ .option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (for COMPONENT/COMPONENT_LIST)")
1108
1413
  .action((options) => {
1109
- addProp(options.component, options);
1414
+ addProp({ id: options.componentId, name: options.component }, options);
1110
1415
  });
1111
1416
  config
1112
1417
  .command("update-prop")
1113
1418
  .description("Update a prop on a component")
1114
- .requiredOption("--component <name>", "Component name")
1419
+ .option("--component-id <id>", "Component id (preferred; from `config list-components`)")
1420
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1115
1421
  .requiredOption("--prop <propName>", "Prop name to update")
1116
1422
  .option("--displayName <displayName>", "New display name")
1117
1423
  .option("--type <type>", "New prop type")
@@ -1124,76 +1430,89 @@ export function createConfigCommand() {
1124
1430
  .option("--filteredComponentIds <json>", "JSON array of component IDs (use 'none' to clear)")
1125
1431
  .option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (use 'none' to clear)")
1126
1432
  .action((options) => {
1127
- updateProp(options.component, options);
1433
+ updateProp({ id: options.componentId, name: options.component }, options);
1128
1434
  });
1129
1435
  config
1130
1436
  .command("remove-prop")
1131
1437
  .description("Remove a prop from a component")
1132
- .requiredOption("--component <name>", "Component name")
1438
+ .option("--component-id <id>", "Component id (preferred; from `config list-components`)")
1439
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1133
1440
  .requiredOption("--prop <propName>", "Prop name to remove")
1134
1441
  .action((options) => {
1135
- removeProp(options.component, options);
1442
+ removeProp({ id: options.componentId, name: options.component }, options);
1136
1443
  });
1137
1444
  config
1138
1445
  .command("update-component")
1139
- .description("Update a component's metadata (isHeader, isFooter)")
1140
- .requiredOption("--name <name>", "Component name")
1446
+ .description("Update a component's metadata (isHeader, isFooter). Identify by --id (preferred) or exact --name.")
1447
+ .option("--id <id>", "Component id (preferred; from `config list-components`)")
1448
+ .option("--name <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1141
1449
  .option("--isHeader", "Mark as store header")
1142
1450
  .option("--no-isHeader", "Unmark as store header")
1143
1451
  .option("--isFooter", "Mark as store footer")
1144
1452
  .option("--no-isFooter", "Unmark as store footer")
1145
1453
  .action((options) => {
1146
- updateComponent(options);
1454
+ updateComponent({ id: options.id, name: options.name }, options);
1147
1455
  });
1148
1456
  config
1149
1457
  .command("remove-component")
1150
- .description("Remove a component from the project (deletes files)")
1151
- .requiredOption("--name <name>", "Component name to remove")
1458
+ .description("Remove a component from the project (deletes files). Identify by --id (preferred) or exact --name.")
1459
+ .option("--id <id>", "Component id (preferred; from `config list-components`)")
1460
+ .option("--name <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1152
1461
  .action((options) => {
1153
- removeComponent(options);
1462
+ removeComponent({ id: options.id, name: options.name });
1154
1463
  });
1155
1464
  config
1156
1465
  .command("add-prop-group")
1157
1466
  .description("Add a prop group to a component")
1158
- .requiredOption("--component <name>", "Component name")
1467
+ .option("--component-id <componentId>", "Component id (preferred; from `config list-components`)")
1468
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1159
1469
  .requiredOption("--id <id>", "Group ID (unique within component, kebab-case)")
1160
1470
  .requiredOption("--name <name>", "Display name in editor")
1161
1471
  .option("--description <description>", "Group description")
1162
1472
  .option("--parent <parentId>", "Parent group ID (for nesting, max 1 level)")
1163
1473
  .action((options) => {
1164
- addPropGroup(options.component, options);
1474
+ addPropGroup({ id: options.componentId, name: options.component }, options);
1165
1475
  });
1166
1476
  config
1167
1477
  .command("update-prop-group")
1168
1478
  .description("Update a prop group on a component")
1169
- .requiredOption("--component <name>", "Component name")
1479
+ .option("--component-id <componentId>", "Component id (preferred; from `config list-components`)")
1480
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1170
1481
  .requiredOption("--id <id>", "Group ID to update")
1171
1482
  .option("--name <name>", "New display name")
1172
1483
  .option("--description <description>", "New description")
1173
1484
  .action((options) => {
1174
- updatePropGroup(options.component, options);
1485
+ updatePropGroup({ id: options.componentId, name: options.component }, options);
1175
1486
  });
1176
1487
  config
1177
1488
  .command("remove-prop-group")
1178
1489
  .description("Remove a prop group from a component (props become ungrouped)")
1179
- .requiredOption("--component <name>", "Component name")
1490
+ .option("--component-id <componentId>", "Component id (preferred; from `config list-components`)")
1491
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1180
1492
  .requiredOption("--id <id>", "Group ID to remove")
1181
1493
  .action((options) => {
1182
- removePropGroup(options.component, options);
1494
+ removePropGroup({ id: options.componentId, name: options.component }, options);
1183
1495
  });
1184
1496
  config
1185
1497
  .command("move-prop-group")
1186
1498
  .description("Move a prop group to a different parent or position (for drag-and-drop reordering)")
1187
- .requiredOption("--component <name>", "Component name")
1499
+ .option("--component-id <componentId>", "Component id (preferred; from `config list-components`)")
1500
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1188
1501
  .requiredOption("--id <id>", "Group ID to move")
1189
1502
  .option("--parent <parentId>", "Target parent group ID (omit to move to root)")
1190
- .option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", v => parseInt(v, 10))
1503
+ .option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", (v) => parseInt(v, 10))
1191
1504
  .action((options) => {
1192
- movePropGroup(options.component, options);
1505
+ movePropGroup({ id: options.componentId, name: options.component }, options);
1506
+ });
1507
+ config
1508
+ .command("list-components")
1509
+ .description("List all components and their props (with canonical ids for use with --id flags)")
1510
+ .action(() => {
1511
+ listComponents();
1193
1512
  });
1194
1513
  config
1195
1514
  .command("list")
1196
- .description("List all components and their props")
1515
+ .description("Alias for `list-components` (kept for backwards compatibility)")
1197
1516
  .action(() => {
1198
1517
  listComponents();
1199
1518
  });
@@ -1277,7 +1596,15 @@ async function fetchLiveEditorEnumById(enumId) {
1277
1596
  clearTimeout(timeout);
1278
1597
  const types = message.payload?.types || [];
1279
1598
  const match = types.find((t) => t.category === "enum" && t.id === enumId);
1280
- finish(match ? { id: match.id, name: match.name, ...(match.enumOptions ? { enumOptions: match.enumOptions } : {}) } : null);
1599
+ finish(match
1600
+ ? {
1601
+ id: match.id,
1602
+ name: match.name,
1603
+ ...(match.enumOptions
1604
+ ? { enumOptions: match.enumOptions }
1605
+ : {}),
1606
+ }
1607
+ : null);
1281
1608
  }
1282
1609
  }
1283
1610
  catch {