@ikas/component-cli 1.4.0-beta.5 → 1.4.0-beta.50

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 (131) 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/build.d.ts.map +1 -1
  10. package/dist/commands/build.js +5 -165
  11. package/dist/commands/build.js.map +1 -1
  12. package/dist/commands/config.d.ts.map +1 -1
  13. package/dist/commands/config.js +514 -114
  14. package/dist/commands/config.js.map +1 -1
  15. package/dist/commands/create-design-tokens.d.ts +7 -0
  16. package/dist/commands/create-design-tokens.d.ts.map +1 -0
  17. package/dist/commands/create-design-tokens.js +127 -0
  18. package/dist/commands/create-design-tokens.js.map +1 -0
  19. package/dist/commands/create-global-variable.d.ts +3 -0
  20. package/dist/commands/create-global-variable.d.ts.map +1 -0
  21. package/dist/commands/create-global-variable.js +53 -0
  22. package/dist/commands/create-global-variable.js.map +1 -0
  23. package/dist/commands/create-page.d.ts +3 -0
  24. package/dist/commands/create-page.d.ts.map +1 -0
  25. package/dist/commands/create-page.js +31 -0
  26. package/dist/commands/create-page.js.map +1 -0
  27. package/dist/commands/delete-theme-globals.d.ts +4 -0
  28. package/dist/commands/delete-theme-globals.d.ts.map +1 -0
  29. package/dist/commands/delete-theme-globals.js +48 -0
  30. package/dist/commands/delete-theme-globals.js.map +1 -0
  31. package/dist/commands/dev.d.ts.map +1 -1
  32. package/dist/commands/dev.js +297 -25
  33. package/dist/commands/dev.js.map +1 -1
  34. package/dist/commands/get-component-props.d.ts +3 -0
  35. package/dist/commands/get-component-props.d.ts.map +1 -0
  36. package/dist/commands/get-component-props.js +32 -0
  37. package/dist/commands/get-component-props.js.map +1 -0
  38. package/dist/commands/get-page-by-type.d.ts +3 -0
  39. package/dist/commands/get-page-by-type.d.ts.map +1 -0
  40. package/dist/commands/get-page-by-type.js +25 -0
  41. package/dist/commands/get-page-by-type.js.map +1 -0
  42. package/dist/commands/get-section-values.d.ts +3 -0
  43. package/dist/commands/get-section-values.d.ts.map +1 -0
  44. package/dist/commands/get-section-values.js +39 -0
  45. package/dist/commands/get-section-values.js.map +1 -0
  46. package/dist/commands/import.d.ts +3 -0
  47. package/dist/commands/import.d.ts.map +1 -0
  48. package/dist/commands/import.js +25 -0
  49. package/dist/commands/import.js.map +1 -0
  50. package/dist/commands/list-entities.d.ts +3 -0
  51. package/dist/commands/list-entities.d.ts.map +1 -0
  52. package/dist/commands/list-entities.js +32 -0
  53. package/dist/commands/list-entities.js.map +1 -0
  54. package/dist/commands/list-imported.d.ts +3 -0
  55. package/dist/commands/list-imported.d.ts.map +1 -0
  56. package/dist/commands/list-imported.js +25 -0
  57. package/dist/commands/list-imported.js.map +1 -0
  58. package/dist/commands/list-page-sections.d.ts +3 -0
  59. package/dist/commands/list-page-sections.d.ts.map +1 -0
  60. package/dist/commands/list-page-sections.js +25 -0
  61. package/dist/commands/list-page-sections.js.map +1 -0
  62. package/dist/commands/list-pages.d.ts +3 -0
  63. package/dist/commands/list-pages.d.ts.map +1 -0
  64. package/dist/commands/list-pages.js +21 -0
  65. package/dist/commands/list-pages.js.map +1 -0
  66. package/dist/commands/list-theme-globals.d.ts +3 -0
  67. package/dist/commands/list-theme-globals.d.ts.map +1 -0
  68. package/dist/commands/list-theme-globals.js +22 -0
  69. package/dist/commands/list-theme-globals.js.map +1 -0
  70. package/dist/commands/publish-theme.d.ts +3 -0
  71. package/dist/commands/publish-theme.d.ts.map +1 -0
  72. package/dist/commands/publish-theme.js +29 -0
  73. package/dist/commands/publish-theme.js.map +1 -0
  74. package/dist/commands/search-products.d.ts +3 -0
  75. package/dist/commands/search-products.d.ts.map +1 -0
  76. package/dist/commands/search-products.js +40 -0
  77. package/dist/commands/search-products.js.map +1 -0
  78. package/dist/commands/update-global-variable.d.ts +3 -0
  79. package/dist/commands/update-global-variable.d.ts.map +1 -0
  80. package/dist/commands/update-global-variable.js +47 -0
  81. package/dist/commands/update-global-variable.js.map +1 -0
  82. package/dist/commands/update-page-sections.d.ts +3 -0
  83. package/dist/commands/update-page-sections.d.ts.map +1 -0
  84. package/dist/commands/update-page-sections.js +39 -0
  85. package/dist/commands/update-page-sections.js.map +1 -0
  86. package/dist/commands/update-section-prop.d.ts +3 -0
  87. package/dist/commands/update-section-prop.d.ts.map +1 -0
  88. package/dist/commands/update-section-prop.js +59 -0
  89. package/dist/commands/update-section-prop.js.map +1 -0
  90. package/dist/commands/upload-image.d.ts +3 -0
  91. package/dist/commands/upload-image.d.ts.map +1 -0
  92. package/dist/commands/upload-image.js +38 -0
  93. package/dist/commands/upload-image.js.map +1 -0
  94. package/dist/commands/upload-images.d.ts +3 -0
  95. package/dist/commands/upload-images.d.ts.map +1 -0
  96. package/dist/commands/upload-images.js +48 -0
  97. package/dist/commands/upload-images.js.map +1 -0
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +49 -0
  100. package/dist/index.js.map +1 -1
  101. package/dist/types.d.ts +28 -1
  102. package/dist/types.d.ts.map +1 -1
  103. package/dist/utils/compile.d.ts +4 -1
  104. package/dist/utils/compile.d.ts.map +1 -1
  105. package/dist/utils/compile.js +517 -48
  106. package/dist/utils/compile.js.map +1 -1
  107. package/dist/utils/component-helpers.d.ts +29 -2
  108. package/dist/utils/component-helpers.d.ts.map +1 -1
  109. package/dist/utils/component-helpers.js +102 -15
  110. package/dist/utils/component-helpers.js.map +1 -1
  111. package/dist/utils/editor-action-client.d.ts +28 -0
  112. package/dist/utils/editor-action-client.d.ts.map +1 -0
  113. package/dist/utils/editor-action-client.js +116 -0
  114. package/dist/utils/editor-action-client.js.map +1 -0
  115. package/dist/utils/load-image.d.ts +16 -0
  116. package/dist/utils/load-image.d.ts.map +1 -0
  117. package/dist/utils/load-image.js +50 -0
  118. package/dist/utils/load-image.js.map +1 -0
  119. package/dist/utils/websocket-server.d.ts +128 -1
  120. package/dist/utils/websocket-server.d.ts.map +1 -1
  121. package/dist/utils/websocket-server.js +116 -0
  122. package/dist/utils/websocket-server.js.map +1 -1
  123. package/package.json +1 -1
  124. package/dist/commands/create.d.ts +0 -9
  125. package/dist/commands/create.d.ts.map +0 -1
  126. package/dist/commands/create.js +0 -9
  127. package/dist/commands/create.js.map +0 -1
  128. package/dist/commands/proxy.d.ts +0 -39
  129. package/dist/commands/proxy.d.ts.map +0 -1
  130. package/dist/commands/proxy.js +0 -212
  131. package/dist/commands/proxy.js.map +0 -1
@@ -2,6 +2,16 @@ import { Command } from "commander";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
4
  import { PROP_TYPES, toPascalCase, generateTypesFile, generateGlobalTypesFile, collectUsedEnumIds, generateComponentFile, generateStylesFile, generateProjectId, generateComponentId, generateUniqueId, updateBarrelExport, findPropGroup, collectPropGroupIds, movePropGroupInTree, validateFilteredComponentIds, } from "../utils/component-helpers.js";
5
+ import { runEditorAction, printResultAndExit, printErrorAndExit } from "../utils/editor-action-client.js";
6
+ async function listLocales(port) {
7
+ try {
8
+ const result = await runEditorAction("list-locales", {}, port ? { port } : {});
9
+ printResultAndExit(result);
10
+ }
11
+ catch (e) {
12
+ printErrorAndExit(e);
13
+ }
14
+ }
5
15
  function loadConfig() {
6
16
  const configPath = path.resolve(process.cwd(), "ikas.config.json");
7
17
  if (!fs.existsSync(configPath)) {
@@ -17,9 +27,48 @@ function loadConfig() {
17
27
  function saveConfig(configPath, config) {
18
28
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
19
29
  }
20
- function findComponent(config, name) {
21
- const pascalName = toPascalCase(name);
22
- return config.components.find((c) => c.name === pascalName);
30
+ /**
31
+ * Resolve a component by canonical id (preferred) or exact PascalCase name.
32
+ * No silent normalization: pass --id from `config list-components`, or pass --name
33
+ * exactly as stored in ikas.config.json. Exits with a structured JSON error
34
+ * (listing valid {id, name} pairs) if zero/both/missing.
35
+ */
36
+ function resolveComponent(config, ref) {
37
+ const id = ref.id?.trim() || undefined;
38
+ const name = ref.name?.trim() || undefined;
39
+ if (!id && !name) {
40
+ console.log(JSON.stringify({
41
+ success: false,
42
+ error: "Component reference required: pass --id (preferred) or --name (exact PascalCase). " +
43
+ "Run `ikas-component config list-components` to see canonical ids.",
44
+ validComponents: config.components.map((c) => ({ id: c.id, name: c.name })),
45
+ }));
46
+ process.exit(1);
47
+ }
48
+ if (id && name) {
49
+ console.log(JSON.stringify({
50
+ success: false,
51
+ error: "Specify only one of --id or --name, not both.",
52
+ }));
53
+ process.exit(1);
54
+ }
55
+ const found = id
56
+ ? config.components.find((c) => c.id === id)
57
+ : config.components.find((c) => c.name === name);
58
+ if (!found) {
59
+ console.log(JSON.stringify({
60
+ success: false,
61
+ error: id
62
+ ? `Component not found: id="${id}".`
63
+ : `Component not found: name="${name}". Names are matched exactly (PascalCase, no normalization).`,
64
+ lookup: id ? { id } : { name },
65
+ validComponents: config.components.map((c) => ({ id: c.id, name: c.name })),
66
+ hint: "Use --id (preferred) or exact PascalCase --name. " +
67
+ "Run `ikas-component config list-components` to see canonical ids.",
68
+ }));
69
+ process.exit(1);
70
+ }
71
+ return found;
23
72
  }
24
73
  function getComponentNames(config) {
25
74
  return config.components.map((c) => c.name);
@@ -105,6 +154,19 @@ function camelCaseToDisplayName(name) {
105
154
  .replace(/([a-z])([A-Z])/g, "$1 $2")
106
155
  .replace(/^./, (c) => c.toUpperCase());
107
156
  }
157
+ const ALLOWED_PROP_FIELDS = new Set([
158
+ "name",
159
+ "displayName",
160
+ "type",
161
+ "required",
162
+ "description",
163
+ "defaultValue",
164
+ "groupId",
165
+ "typeId",
166
+ "enumTypeId",
167
+ "filteredComponentIds",
168
+ "privateVarMap",
169
+ ]);
108
170
  /**
109
171
  * Parse and validate the --props JSON flag for add-component.
110
172
  * Returns validated ComponentProp[] or exits with error.
@@ -132,6 +194,16 @@ async function parsePropsFlag(propsJson, config, configPath) {
132
194
  const props = [];
133
195
  for (const raw of rawProps) {
134
196
  const r = raw;
197
+ const unknownFields = Object.keys(r).filter((k) => !ALLOWED_PROP_FIELDS.has(k));
198
+ if (unknownFields.length > 0) {
199
+ console.log(JSON.stringify({
200
+ success: false,
201
+ error: `Unknown field(s) in --props entry: ${unknownFields.join(", ")}. ` +
202
+ `Allowed: ${[...ALLOWED_PROP_FIELDS].join(", ")}. ` +
203
+ `Note: use "groupId" (not "group") and "defaultValue" (not "default") in --props JSON.`,
204
+ }));
205
+ process.exit(1);
206
+ }
135
207
  if (!r.name || typeof r.name !== "string") {
136
208
  console.log(JSON.stringify({
137
209
  success: false,
@@ -172,6 +244,14 @@ async function parsePropsFlag(propsJson, config, configPath) {
172
244
  }));
173
245
  process.exit(1);
174
246
  }
247
+ // Validate LINK / LIST_OF_LINK defaultValue shape (reject JSON strings and legacy { href })
248
+ if ((propType === "LINK" || propType === "LIST_OF_LINK") && r.defaultValue !== undefined) {
249
+ const linkError = validateLinkDefaultValue(propType, r.defaultValue, r.name);
250
+ if (linkError) {
251
+ console.log(JSON.stringify({ success: false, error: linkError }));
252
+ process.exit(1);
253
+ }
254
+ }
175
255
  // Auto-generate displayName if omitted
176
256
  const displayName = typeof r.displayName === "string" && r.displayName
177
257
  ? r.displayName
@@ -293,6 +373,7 @@ async function addComponent(name, options) {
293
373
  const newComponent = {
294
374
  id: componentId,
295
375
  name: pascalName,
376
+ ...(options.displayName ? { displayName: options.displayName } : {}),
296
377
  entry: `./src/components/${pascalName}/index.tsx`,
297
378
  styles: `./src/components/${pascalName}/styles.css`,
298
379
  ...(componentType === "section" ? { type: "section" } : {}),
@@ -322,16 +403,9 @@ async function addComponent(name, options) {
322
403
  ],
323
404
  }));
324
405
  }
325
- async function addProp(componentName, options) {
406
+ async function addProp(ref, options) {
326
407
  const { config, configPath } = loadConfig();
327
- const component = findComponent(config, componentName);
328
- if (!component) {
329
- console.log(JSON.stringify({
330
- success: false,
331
- error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
332
- }));
333
- process.exit(1);
334
- }
408
+ const component = resolveComponent(config, ref);
335
409
  // Validate prop type
336
410
  const propType = options.type.toUpperCase();
337
411
  if (!PROP_TYPES.includes(propType)) {
@@ -434,14 +508,22 @@ async function addProp(componentName, options) {
434
508
  process.exit(1);
435
509
  }
436
510
  }
511
+ const parsedDefaultValue = options.defaultValue !== undefined ? parseDefaultValue(options.defaultValue, propType) : undefined;
512
+ if ((propType === "LINK" || propType === "LIST_OF_LINK") && parsedDefaultValue !== undefined) {
513
+ const linkError = validateLinkDefaultValue(propType, parsedDefaultValue, options.name);
514
+ if (linkError) {
515
+ console.log(JSON.stringify({ success: false, error: linkError }));
516
+ process.exit(1);
517
+ }
518
+ }
437
519
  const newProp = {
438
520
  name: options.name,
439
521
  displayName: options.displayName,
440
522
  type: propType,
441
523
  required: options.required ?? false,
442
524
  ...(options.description ? { description: options.description } : {}),
443
- ...(options.defaultValue !== undefined
444
- ? { defaultValue: parseDefaultValue(options.defaultValue, propType) }
525
+ ...(parsedDefaultValue !== undefined
526
+ ? { defaultValue: parsedDefaultValue }
445
527
  : {}),
446
528
  ...(options.group ? { groupId: options.group } : {}),
447
529
  ...(options.typeId ? { typeId: options.typeId } : {}),
@@ -475,20 +557,73 @@ function parseDefaultValue(value, propType) {
475
557
  return Number(value);
476
558
  case "BOOLEAN":
477
559
  return value === "true";
560
+ case "LINK":
561
+ case "LIST_OF_LINK":
562
+ // The --default-value flag arrives as a string; LINK values must be stored as a
563
+ // typed object. Parse it here; if it is not valid JSON, return the raw string so
564
+ // validateLinkDefaultValue can report a precise error.
565
+ try {
566
+ return JSON.parse(value);
567
+ }
568
+ catch {
569
+ return value;
570
+ }
478
571
  default:
479
572
  return value;
480
573
  }
481
574
  }
482
- function updateProp(componentName, options) {
575
+ const VALID_LINK_TYPES = ["PAGE", "EXTERNAL", "FILE"];
576
+ const LINK_SHAPE_HELP = 'A link must be an object: { "linkType": "EXTERNAL", "label": "Text", "externalLink": "https://...", "subLinks": [] } ' +
577
+ 'for external links, or { "linkType": "PAGE", "label": "Text", "pageType": "INDEX", "subLinks": [] } for store pages. ' +
578
+ "Do NOT pass a JSON string or a { label, href } shape.";
579
+ // Returns the index of the first array element that fails `fn`, as an error string, or null.
580
+ const firstError = (arr, fn) => arr.reduce((acc, item, index) => acc ?? fn(item, index), null);
581
+ const validateSingleLink = (link, propName, path) => {
582
+ const where = `defaultValue for prop "${propName}"${path}`;
583
+ if (typeof link === "string") {
584
+ return `${where} is a JSON string but must be an object. ${LINK_SHAPE_HELP}`;
585
+ }
586
+ if (!link || typeof link !== "object" || Array.isArray(link)) {
587
+ return `${where} must be a link object. ${LINK_SHAPE_HELP}`;
588
+ }
589
+ const l = link;
590
+ if ("href" in l && !("linkType" in l)) {
591
+ return `${where} uses a legacy { href } shape without "linkType". Use "linkType" + "externalLink"/"pageType" instead. ${LINK_SHAPE_HELP}`;
592
+ }
593
+ if (typeof l.linkType !== "string" || !VALID_LINK_TYPES.includes(l.linkType)) {
594
+ return `${where} must have a valid "linkType" (one of ${VALID_LINK_TYPES.join(", ")}). ${LINK_SHAPE_HELP}`;
595
+ }
596
+ if (l.subLinks !== undefined && !Array.isArray(l.subLinks)) {
597
+ return `${where}: "subLinks" must be an array (use [] when there are none).`;
598
+ }
599
+ if (Array.isArray(l.subLinks)) {
600
+ return firstError(l.subLinks, (sl, i) => validateSingleLink(sl, propName, `${path}.subLinks[${i}]`));
601
+ }
602
+ return null;
603
+ };
604
+ // Validates a LINK / LIST_OF_LINK defaultValue. Returns an error message, or null if valid.
605
+ // These defaults are a frequent source of bad data: agents tend to pass a JSON string or a
606
+ // legacy { label, href } shape. Rejecting them at authoring time keeps malformed link values
607
+ // out of published themes.
608
+ function validateLinkDefaultValue(propType, value, propName) {
609
+ if (value === undefined || value === null)
610
+ return null;
611
+ if (propType === "LINK") {
612
+ return validateSingleLink(value, propName, "");
613
+ }
614
+ // LIST_OF_LINK
615
+ if (typeof value === "string") {
616
+ return `defaultValue for prop "${propName}" is a JSON string but must be an object { "links": [...] }. ${LINK_SHAPE_HELP}`;
617
+ }
618
+ if (!value || typeof value !== "object" || Array.isArray(value) || !Array.isArray(value.links)) {
619
+ return `defaultValue for prop "${propName}" must be an object with a "links" array: { "links": [ <link>, ... ] }. ${LINK_SHAPE_HELP}`;
620
+ }
621
+ const links = value.links;
622
+ return firstError(links, (link, i) => validateSingleLink(link, propName, `.links[${i}]`));
623
+ }
624
+ function updateProp(ref, options) {
483
625
  const { config, configPath } = loadConfig();
484
- const component = findComponent(config, componentName);
485
- if (!component) {
486
- console.log(JSON.stringify({
487
- success: false,
488
- error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
489
- }));
490
- process.exit(1);
491
- }
626
+ const component = resolveComponent(config, ref);
492
627
  const propIndex = component.props.findIndex((p) => p.name === options.prop);
493
628
  if (propIndex === -1) {
494
629
  console.log(JSON.stringify({
@@ -519,7 +654,15 @@ function updateProp(componentName, options) {
519
654
  prop.description = options.description || undefined;
520
655
  }
521
656
  if (options.defaultValue !== undefined) {
522
- prop.defaultValue = parseDefaultValue(options.defaultValue, prop.type);
657
+ const parsed = parseDefaultValue(options.defaultValue, prop.type);
658
+ if (prop.type === "LINK" || prop.type === "LIST_OF_LINK") {
659
+ const linkError = validateLinkDefaultValue(prop.type, parsed, prop.name);
660
+ if (linkError) {
661
+ console.log(JSON.stringify({ success: false, error: linkError }));
662
+ process.exit(1);
663
+ }
664
+ }
665
+ prop.defaultValue = parsed;
523
666
  }
524
667
  if (options.group !== undefined) {
525
668
  if (options.group === "" || options.group === "none") {
@@ -622,16 +765,9 @@ function updateProp(componentName, options) {
622
765
  },
623
766
  }));
624
767
  }
625
- function removeProp(componentName, options) {
768
+ function removeProp(ref, options) {
626
769
  const { config, configPath } = loadConfig();
627
- const component = findComponent(config, componentName);
628
- if (!component) {
629
- console.log(JSON.stringify({
630
- success: false,
631
- error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
632
- }));
633
- process.exit(1);
634
- }
770
+ const component = resolveComponent(config, ref);
635
771
  const propIndex = component.props.findIndex((p) => p.name === options.prop);
636
772
  if (propIndex === -1) {
637
773
  console.log(JSON.stringify({
@@ -652,21 +788,30 @@ function removeProp(componentName, options) {
652
788
  remainingProps: component.props.map((p) => p.name),
653
789
  }));
654
790
  }
655
- function removeComponent(options) {
656
- const { config, configPath } = loadConfig();
657
- const pascalName = toPascalCase(options.name);
658
- const componentIndex = config.components.findIndex((c) => c.name === pascalName);
659
- if (componentIndex === -1) {
660
- console.log(JSON.stringify({
661
- success: false,
662
- error: `Component "${options.name}" not found. Available: ${getComponentNames(config).join(", ")}`,
663
- }));
664
- process.exit(1);
791
+ /**
792
+ * Strip a deleted component's ID from every other component's COMPONENT/COMPONENT_LIST
793
+ * `filteredComponentIds` allowlist. Empty arrays are deleted to keep the field optional
794
+ * (mirrors the editor-side convention).
795
+ */
796
+ function pruneFilteredComponentIdRefs(config, removedId) {
797
+ for (const component of config.components) {
798
+ for (const prop of component.props || []) {
799
+ if (!prop.filteredComponentIds)
800
+ continue;
801
+ prop.filteredComponentIds = prop.filteredComponentIds.filter((id) => id !== removedId);
802
+ if (prop.filteredComponentIds.length === 0)
803
+ delete prop.filteredComponentIds;
804
+ }
665
805
  }
666
- const component = config.components[componentIndex];
806
+ }
807
+ function removeComponent(ref) {
808
+ const { config, configPath } = loadConfig();
809
+ const component = resolveComponent(config, ref);
810
+ const componentIndex = config.components.indexOf(component);
667
811
  const componentDir = path.resolve(process.cwd(), path.dirname(component.entry));
668
- // Remove component from config
812
+ // Remove component from config and strip orphaned references from remaining components
669
813
  config.components.splice(componentIndex, 1);
814
+ pruneFilteredComponentIdRefs(config, component.id);
670
815
  saveConfig(configPath, config);
671
816
  // Remove component directory
672
817
  if (fs.existsSync(componentDir)) {
@@ -676,21 +821,15 @@ function removeComponent(options) {
676
821
  updateBarrelExport(process.cwd(), getComponentNames(config));
677
822
  console.log(JSON.stringify({
678
823
  success: true,
679
- removedComponent: pascalName,
824
+ removedComponentId: component.id,
825
+ removedComponentName: component.name,
680
826
  removedDirectory: path.relative(process.cwd(), componentDir),
681
- remainingComponents: getComponentNames(config),
827
+ remainingComponents: config.components.map((c) => ({ id: c.id, name: c.name })),
682
828
  }));
683
829
  }
684
- function addPropGroup(componentName, options) {
830
+ function addPropGroup(ref, options) {
685
831
  const { config, configPath } = loadConfig();
686
- const component = findComponent(config, componentName);
687
- if (!component) {
688
- console.log(JSON.stringify({
689
- success: false,
690
- error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
691
- }));
692
- process.exit(1);
693
- }
832
+ const component = resolveComponent(config, ref);
694
833
  if (!component.propGroups)
695
834
  component.propGroups = [];
696
835
  // Check uniqueness
@@ -741,16 +880,9 @@ function addPropGroup(componentName, options) {
741
880
  },
742
881
  }));
743
882
  }
744
- function updatePropGroup(componentName, options) {
883
+ function updatePropGroup(ref, options) {
745
884
  const { config, configPath } = loadConfig();
746
- const component = findComponent(config, componentName);
747
- if (!component) {
748
- console.log(JSON.stringify({
749
- success: false,
750
- error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
751
- }));
752
- process.exit(1);
753
- }
885
+ const component = resolveComponent(config, ref);
754
886
  if (!component.propGroups || component.propGroups.length === 0) {
755
887
  console.log(JSON.stringify({
756
888
  success: false,
@@ -780,16 +912,9 @@ function updatePropGroup(componentName, options) {
780
912
  },
781
913
  }));
782
914
  }
783
- function removePropGroup(componentName, options) {
915
+ function removePropGroup(ref, options) {
784
916
  const { config, configPath } = loadConfig();
785
- const component = findComponent(config, componentName);
786
- if (!component) {
787
- console.log(JSON.stringify({
788
- success: false,
789
- error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
790
- }));
791
- process.exit(1);
792
- }
917
+ const component = resolveComponent(config, ref);
793
918
  if (!component.propGroups || component.propGroups.length === 0) {
794
919
  console.log(JSON.stringify({
795
920
  success: false,
@@ -825,16 +950,9 @@ function removePropGroup(componentName, options) {
825
950
  remainingPropGroups: component.propGroups.map(g => g.id),
826
951
  }));
827
952
  }
828
- function movePropGroup(componentName, options) {
953
+ function movePropGroup(ref, options) {
829
954
  const { config, configPath } = loadConfig();
830
- const component = findComponent(config, componentName);
831
- if (!component) {
832
- console.log(JSON.stringify({
833
- success: false,
834
- error: `Component "${componentName}" not found. Available: ${getComponentNames(config).join(", ")}`,
835
- }));
836
- process.exit(1);
837
- }
955
+ const component = resolveComponent(config, ref);
838
956
  if (!component.propGroups || component.propGroups.length === 0) {
839
957
  console.log(JSON.stringify({
840
958
  success: false,
@@ -857,16 +975,9 @@ function movePropGroup(componentName, options) {
857
975
  parentGroupId: options.parent || null,
858
976
  }));
859
977
  }
860
- function updateComponent(options) {
978
+ function updateComponent(ref, options) {
861
979
  const { config, configPath } = loadConfig();
862
- const component = findComponent(config, options.name);
863
- if (!component) {
864
- console.log(JSON.stringify({
865
- success: false,
866
- error: `Component "${options.name}" not found. Available: ${getComponentNames(config).join(", ")}`,
867
- }));
868
- process.exit(1);
869
- }
980
+ const component = resolveComponent(config, ref);
870
981
  if (options.isHeader !== undefined) {
871
982
  if (options.isHeader) {
872
983
  component.isHeader = true;
@@ -883,20 +994,217 @@ function updateComponent(options) {
883
994
  delete component.isFooter;
884
995
  }
885
996
  }
997
+ // displayName is the code-owned default-locale display label (may contain spaces).
998
+ // An empty string clears it (falls back to `name`).
999
+ if (options.displayName !== undefined) {
1000
+ if (options.displayName) {
1001
+ component.displayName = options.displayName;
1002
+ }
1003
+ else {
1004
+ delete component.displayName;
1005
+ }
1006
+ }
886
1007
  saveConfig(configPath, config);
887
1008
  console.log(JSON.stringify({
888
1009
  success: true,
889
1010
  componentName: component.name,
890
1011
  type: component.type ?? "component",
1012
+ ...(component.displayName ? { displayName: component.displayName } : {}),
891
1013
  ...(component.isHeader ? { isHeader: true } : {}),
892
1014
  ...(component.isFooter ? { isFooter: true } : {}),
893
1015
  }));
894
1016
  }
1017
+ // Upsert a per-locale translation row in `list` by locale, applying only the
1018
+ // provided fields. Studio keeps one translation per locale, so this matches by
1019
+ // locale rather than appending. A row left with no content (all label fields
1020
+ // empty) is not persisted — a new one is skipped, an existing one is pruned —
1021
+ // so we never write a meaningless `{id, locale}` row. Returns the row, or null
1022
+ // when nothing meaningful remains.
1023
+ function upsertTranslation(list, locale, fields) {
1024
+ const existing = list.find((t) => t.locale === locale);
1025
+ const target = existing ?? { id: generateUniqueId(), locale };
1026
+ if (fields.displayName !== undefined)
1027
+ target.displayName = fields.displayName || undefined;
1028
+ if (fields.description !== undefined)
1029
+ target.description = fields.description || undefined;
1030
+ if (fields.name !== undefined)
1031
+ target.name = fields.name || undefined;
1032
+ if (!target.displayName && !target.description && !target.name) {
1033
+ if (existing)
1034
+ list.splice(list.indexOf(existing), 1);
1035
+ return null;
1036
+ }
1037
+ if (!existing)
1038
+ list.push(target);
1039
+ return target;
1040
+ }
1041
+ function setPropTranslation(ref, options) {
1042
+ const { config, configPath } = loadConfig();
1043
+ const component = resolveComponent(config, ref);
1044
+ const prop = component.props.find((p) => p.name === options.prop);
1045
+ if (!prop) {
1046
+ console.log(JSON.stringify({
1047
+ success: false,
1048
+ error: `Prop "${options.prop}" not found on component "${component.name}". Available: ${component.props.map((p) => p.name).join(", ")}`,
1049
+ }));
1050
+ process.exit(1);
1051
+ }
1052
+ if (options.displayName === undefined && options.description === undefined) {
1053
+ console.log(JSON.stringify({
1054
+ success: false,
1055
+ error: "Nothing to set: provide --displayName and/or --description.",
1056
+ }));
1057
+ process.exit(1);
1058
+ }
1059
+ if (!prop.translations)
1060
+ prop.translations = [];
1061
+ const row = upsertTranslation(prop.translations, options.locale, {
1062
+ displayName: options.displayName,
1063
+ description: options.description,
1064
+ });
1065
+ if (prop.translations.length === 0)
1066
+ prop.translations = undefined;
1067
+ saveConfig(configPath, config);
1068
+ console.log(JSON.stringify({
1069
+ success: true,
1070
+ componentName: component.name,
1071
+ prop: prop.name,
1072
+ translation: row,
1073
+ }));
1074
+ }
1075
+ function setGroupTranslation(ref, options) {
1076
+ const { config, configPath } = loadConfig();
1077
+ const component = resolveComponent(config, ref);
1078
+ if (!component.propGroups || component.propGroups.length === 0) {
1079
+ console.log(JSON.stringify({ success: false, error: `No prop groups on component "${component.name}".` }));
1080
+ process.exit(1);
1081
+ }
1082
+ const found = findPropGroup(component.propGroups, options.group);
1083
+ if (!found) {
1084
+ console.log(JSON.stringify({
1085
+ success: false,
1086
+ error: `Prop group "${options.group}" not found on component "${component.name}". Available: ${Array.from(collectPropGroupIds(component.propGroups)).join(", ")}`,
1087
+ }));
1088
+ process.exit(1);
1089
+ }
1090
+ if (options.name === undefined && options.description === undefined) {
1091
+ console.log(JSON.stringify({
1092
+ success: false,
1093
+ error: "Nothing to set: provide --name and/or --description.",
1094
+ }));
1095
+ process.exit(1);
1096
+ }
1097
+ if (!found.group.translations)
1098
+ found.group.translations = [];
1099
+ const row = upsertTranslation(found.group.translations, options.locale, {
1100
+ name: options.name,
1101
+ description: options.description,
1102
+ });
1103
+ if (found.group.translations.length === 0)
1104
+ found.group.translations = undefined;
1105
+ saveConfig(configPath, config);
1106
+ console.log(JSON.stringify({
1107
+ success: true,
1108
+ componentName: component.name,
1109
+ group: found.group.id,
1110
+ translation: row,
1111
+ }));
1112
+ }
1113
+ function removePropTranslation(ref, options) {
1114
+ const { config, configPath } = loadConfig();
1115
+ const component = resolveComponent(config, ref);
1116
+ const prop = component.props.find((p) => p.name === options.prop);
1117
+ if (!prop) {
1118
+ console.log(JSON.stringify({
1119
+ success: false,
1120
+ error: `Prop "${options.prop}" not found on component "${component.name}". Available: ${component.props.map((p) => p.name).join(", ")}`,
1121
+ }));
1122
+ process.exit(1);
1123
+ }
1124
+ const before = prop.translations?.length ?? 0;
1125
+ if (prop.translations) {
1126
+ prop.translations = prop.translations.filter((t) => t.locale !== options.locale);
1127
+ if (prop.translations.length === 0)
1128
+ prop.translations = undefined;
1129
+ }
1130
+ saveConfig(configPath, config);
1131
+ console.log(JSON.stringify({
1132
+ success: true,
1133
+ componentName: component.name,
1134
+ prop: prop.name,
1135
+ removed: before - (prop.translations?.length ?? 0),
1136
+ }));
1137
+ }
1138
+ function removeGroupTranslation(ref, options) {
1139
+ const { config, configPath } = loadConfig();
1140
+ const component = resolveComponent(config, ref);
1141
+ if (!component.propGroups || component.propGroups.length === 0) {
1142
+ console.log(JSON.stringify({ success: false, error: `No prop groups on component "${component.name}".` }));
1143
+ process.exit(1);
1144
+ }
1145
+ const found = findPropGroup(component.propGroups, options.group);
1146
+ if (!found) {
1147
+ console.log(JSON.stringify({
1148
+ success: false,
1149
+ error: `Prop group "${options.group}" not found on component "${component.name}". Available: ${Array.from(collectPropGroupIds(component.propGroups)).join(", ")}`,
1150
+ }));
1151
+ process.exit(1);
1152
+ }
1153
+ const before = found.group.translations?.length ?? 0;
1154
+ if (found.group.translations) {
1155
+ found.group.translations = found.group.translations.filter((t) => t.locale !== options.locale);
1156
+ if (found.group.translations.length === 0)
1157
+ found.group.translations = undefined;
1158
+ }
1159
+ saveConfig(configPath, config);
1160
+ console.log(JSON.stringify({
1161
+ success: true,
1162
+ componentName: component.name,
1163
+ group: found.group.id,
1164
+ removed: before - (found.group.translations?.length ?? 0),
1165
+ }));
1166
+ }
1167
+ function setComponentTranslation(ref, options) {
1168
+ const { config, configPath } = loadConfig();
1169
+ const component = resolveComponent(config, ref);
1170
+ if (options.displayName === undefined) {
1171
+ console.log(JSON.stringify({ success: false, error: "Nothing to set: provide --displayName." }));
1172
+ process.exit(1);
1173
+ }
1174
+ if (!component.translations)
1175
+ component.translations = [];
1176
+ const row = upsertTranslation(component.translations, options.locale, { displayName: options.displayName });
1177
+ if (component.translations.length === 0)
1178
+ component.translations = undefined;
1179
+ saveConfig(configPath, config);
1180
+ console.log(JSON.stringify({
1181
+ success: true,
1182
+ componentName: component.name,
1183
+ translation: row,
1184
+ }));
1185
+ }
1186
+ function removeComponentTranslation(ref, options) {
1187
+ const { config, configPath } = loadConfig();
1188
+ const component = resolveComponent(config, ref);
1189
+ const before = component.translations?.length ?? 0;
1190
+ if (component.translations) {
1191
+ component.translations = component.translations.filter((t) => t.locale !== options.locale);
1192
+ if (component.translations.length === 0)
1193
+ component.translations = undefined;
1194
+ }
1195
+ saveConfig(configPath, config);
1196
+ console.log(JSON.stringify({
1197
+ success: true,
1198
+ componentName: component.name,
1199
+ removed: before - (component.translations?.length ?? 0),
1200
+ }));
1201
+ }
895
1202
  function listComponents() {
896
1203
  const { config } = loadConfig();
897
1204
  const components = config.components.map((c) => ({
898
1205
  id: c.id,
899
1206
  name: c.name,
1207
+ ...(c.displayName ? { displayName: c.displayName } : {}),
900
1208
  type: c.type ?? "component",
901
1209
  ...(c.isHeader ? { isHeader: true } : {}),
902
1210
  ...(c.isFooter ? { isFooter: true } : {}),
@@ -1116,17 +1424,20 @@ export function createConfigCommand() {
1116
1424
  .command("add-component")
1117
1425
  .description("Add a new component to the project")
1118
1426
  .requiredOption("--name <name>", "Component name (PascalCase or kebab-case)")
1427
+ .option("--displayName <displayName>", "Human-facing display label shown in the editor (may contain spaces; defaults to name)")
1119
1428
  .option("--type <type>", "Component type: section or component", "component")
1120
1429
  .option("--isHeader", "Mark this section as the store header (only for type: section)")
1121
1430
  .option("--isFooter", "Mark this section as the store footer (only for type: section)")
1122
- .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.")
1431
+ .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. " +
1432
+ "Example: '[{\"name\":\"title\",\"type\":\"TEXT\",\"required\":true,\"defaultValue\":\"Hello\",\"groupId\":\"basic\"}]'")
1123
1433
  .action((options) => {
1124
1434
  addComponent(options.name, options);
1125
1435
  });
1126
1436
  config
1127
1437
  .command("add-prop")
1128
1438
  .description("Add a prop to a component")
1129
- .requiredOption("--component <name>", "Component name")
1439
+ .option("--component-id <id>", "Component id (preferred; from `config list-components`)")
1440
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1130
1441
  .requiredOption("--name <name>", "Prop name (camelCase)")
1131
1442
  .requiredOption("--displayName <displayName>", "Display name in editor")
1132
1443
  .requiredOption("--type <type>", `Prop type: ${PROP_TYPES.join(", ")}`)
@@ -1139,12 +1450,13 @@ export function createConfigCommand() {
1139
1450
  .option("--filteredComponentIds <json>", "JSON array of component IDs to restrict selection (for COMPONENT/COMPONENT_LIST)")
1140
1451
  .option("--privateVarMap <json>", 'JSON object mapping variable keys to {id, typeId} (for COMPONENT/COMPONENT_LIST)')
1141
1452
  .action((options) => {
1142
- addProp(options.component, options);
1453
+ addProp({ id: options.componentId, name: options.component }, options);
1143
1454
  });
1144
1455
  config
1145
1456
  .command("update-prop")
1146
1457
  .description("Update a prop on a component")
1147
- .requiredOption("--component <name>", "Component name")
1458
+ .option("--component-id <id>", "Component id (preferred; from `config list-components`)")
1459
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1148
1460
  .requiredOption("--prop <propName>", "Prop name to update")
1149
1461
  .option("--displayName <displayName>", "New display name")
1150
1462
  .option("--type <type>", "New prop type")
@@ -1157,76 +1469,157 @@ export function createConfigCommand() {
1157
1469
  .option("--filteredComponentIds <json>", "JSON array of component IDs (use 'none' to clear)")
1158
1470
  .option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (use 'none' to clear)")
1159
1471
  .action((options) => {
1160
- updateProp(options.component, options);
1472
+ updateProp({ id: options.componentId, name: options.component }, options);
1161
1473
  });
1162
1474
  config
1163
1475
  .command("remove-prop")
1164
1476
  .description("Remove a prop from a component")
1165
- .requiredOption("--component <name>", "Component name")
1477
+ .option("--component-id <id>", "Component id (preferred; from `config list-components`)")
1478
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1166
1479
  .requiredOption("--prop <propName>", "Prop name to remove")
1167
1480
  .action((options) => {
1168
- removeProp(options.component, options);
1481
+ removeProp({ id: options.componentId, name: options.component }, options);
1169
1482
  });
1170
1483
  config
1171
1484
  .command("update-component")
1172
- .description("Update a component's metadata (isHeader, isFooter)")
1173
- .requiredOption("--name <name>", "Component name")
1485
+ .description("Update a component's metadata (displayName, isHeader, isFooter). Identify by --id (preferred) or exact --name.")
1486
+ .option("--id <id>", "Component id (preferred; from `config list-components`)")
1487
+ .option("--name <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1488
+ .option("--displayName <displayName>", "Human-facing display label shown in the editor (may contain spaces; empty string clears it)")
1174
1489
  .option("--isHeader", "Mark as store header")
1175
1490
  .option("--no-isHeader", "Unmark as store header")
1176
1491
  .option("--isFooter", "Mark as store footer")
1177
1492
  .option("--no-isFooter", "Unmark as store footer")
1178
1493
  .action((options) => {
1179
- updateComponent(options);
1494
+ updateComponent({ id: options.id, name: options.name }, options);
1180
1495
  });
1181
1496
  config
1182
1497
  .command("remove-component")
1183
- .description("Remove a component from the project (deletes files)")
1184
- .requiredOption("--name <name>", "Component name to remove")
1498
+ .description("Remove a component from the project (deletes files). Identify by --id (preferred) or exact --name.")
1499
+ .option("--id <id>", "Component id (preferred; from `config list-components`)")
1500
+ .option("--name <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1185
1501
  .action((options) => {
1186
- removeComponent(options);
1502
+ removeComponent({ id: options.id, name: options.name });
1187
1503
  });
1188
1504
  config
1189
1505
  .command("add-prop-group")
1190
1506
  .description("Add a prop group to a component")
1191
- .requiredOption("--component <name>", "Component name")
1507
+ .option("--component-id <componentId>", "Component id (preferred; from `config list-components`)")
1508
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1192
1509
  .requiredOption("--id <id>", "Group ID (unique within component, kebab-case)")
1193
1510
  .requiredOption("--name <name>", "Display name in editor")
1194
1511
  .option("--description <description>", "Group description")
1195
1512
  .option("--parent <parentId>", "Parent group ID (for nesting, max 1 level)")
1196
1513
  .action((options) => {
1197
- addPropGroup(options.component, options);
1514
+ addPropGroup({ id: options.componentId, name: options.component }, options);
1198
1515
  });
1199
1516
  config
1200
1517
  .command("update-prop-group")
1201
1518
  .description("Update a prop group on a component")
1202
- .requiredOption("--component <name>", "Component name")
1519
+ .option("--component-id <componentId>", "Component id (preferred; from `config list-components`)")
1520
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1203
1521
  .requiredOption("--id <id>", "Group ID to update")
1204
1522
  .option("--name <name>", "New display name")
1205
1523
  .option("--description <description>", "New description")
1206
1524
  .action((options) => {
1207
- updatePropGroup(options.component, options);
1525
+ updatePropGroup({ id: options.componentId, name: options.component }, options);
1208
1526
  });
1209
1527
  config
1210
1528
  .command("remove-prop-group")
1211
1529
  .description("Remove a prop group from a component (props become ungrouped)")
1212
- .requiredOption("--component <name>", "Component name")
1530
+ .option("--component-id <componentId>", "Component id (preferred; from `config list-components`)")
1531
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1213
1532
  .requiredOption("--id <id>", "Group ID to remove")
1214
1533
  .action((options) => {
1215
- removePropGroup(options.component, options);
1534
+ removePropGroup({ id: options.componentId, name: options.component }, options);
1216
1535
  });
1217
1536
  config
1218
1537
  .command("move-prop-group")
1219
1538
  .description("Move a prop group to a different parent or position (for drag-and-drop reordering)")
1220
- .requiredOption("--component <name>", "Component name")
1539
+ .option("--component-id <componentId>", "Component id (preferred; from `config list-components`)")
1540
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1221
1541
  .requiredOption("--id <id>", "Group ID to move")
1222
1542
  .option("--parent <parentId>", "Target parent group ID (omit to move to root)")
1223
1543
  .option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", v => parseInt(v, 10))
1224
1544
  .action((options) => {
1225
- movePropGroup(options.component, options);
1545
+ movePropGroup({ id: options.componentId, name: options.component }, options);
1546
+ });
1547
+ config
1548
+ .command("set-prop-translation")
1549
+ .description("Add or update a per-locale translation for a prop's label/description. " +
1550
+ "Seeded into the editor on first import of a new component or a newly added prop; " +
1551
+ "studio-owned translations for already-imported props are not overwritten.")
1552
+ .option("--component-id <id>", "Component id (preferred; from `config list-components`)")
1553
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1554
+ .requiredOption("--prop <propName>", "Prop name to translate")
1555
+ .requiredOption("--locale <locale>", "Target locale (e.g. de, fr, en)")
1556
+ .option("--displayName <text>", "Translated display name")
1557
+ .option("--description <text>", "Translated description")
1558
+ .action((options) => {
1559
+ setPropTranslation({ id: options.componentId, name: options.component }, options);
1560
+ });
1561
+ config
1562
+ .command("set-group-translation")
1563
+ .description("Add or update a per-locale translation for a prop group's name/description. " +
1564
+ "Seeded into the editor only for groups studio has not seen yet (studio wins otherwise).")
1565
+ .option("--component-id <id>", "Component id (preferred; from `config list-components`)")
1566
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1567
+ .requiredOption("--group <groupId>", "Prop group id (local, as in ikas.config.json)")
1568
+ .requiredOption("--locale <locale>", "Target locale (e.g. de, fr, en)")
1569
+ .option("--name <text>", "Translated group name")
1570
+ .option("--description <text>", "Translated description")
1571
+ .action((options) => {
1572
+ setGroupTranslation({ id: options.componentId, name: options.component }, options);
1573
+ });
1574
+ config
1575
+ .command("remove-prop-translation")
1576
+ .description("Remove a prop's translation for a given locale")
1577
+ .option("--component-id <id>", "Component id (preferred; from `config list-components`)")
1578
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1579
+ .requiredOption("--prop <propName>", "Prop name")
1580
+ .requiredOption("--locale <locale>", "Locale to remove")
1581
+ .action((options) => {
1582
+ removePropTranslation({ id: options.componentId, name: options.component }, options);
1583
+ });
1584
+ config
1585
+ .command("remove-group-translation")
1586
+ .description("Remove a prop group's translation for a given locale")
1587
+ .option("--component-id <id>", "Component id (preferred; from `config list-components`)")
1588
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1589
+ .requiredOption("--group <groupId>", "Prop group id (local, as in ikas.config.json)")
1590
+ .requiredOption("--locale <locale>", "Locale to remove")
1591
+ .action((options) => {
1592
+ removeGroupTranslation({ id: options.componentId, name: options.component }, options);
1593
+ });
1594
+ config
1595
+ .command("set-component-translation")
1596
+ .description("Add or update a per-locale translation for a section/component's own display label. " +
1597
+ "Seeded into the editor only for components studio has not seen yet (studio wins otherwise).")
1598
+ .option("--component-id <id>", "Component id (preferred; from `config list-components`)")
1599
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1600
+ .requiredOption("--locale <locale>", "Target locale (e.g. de, fr, en)")
1601
+ .option("--displayName <text>", "Translated component display label")
1602
+ .action((options) => {
1603
+ setComponentTranslation({ id: options.componentId, name: options.component }, options);
1604
+ });
1605
+ config
1606
+ .command("remove-component-translation")
1607
+ .description("Remove a section/component name translation for a given locale")
1608
+ .option("--component-id <id>", "Component id (preferred; from `config list-components`)")
1609
+ .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1610
+ .requiredOption("--locale <locale>", "Locale to remove")
1611
+ .action((options) => {
1612
+ removeComponentTranslation({ id: options.componentId, name: options.component }, options);
1613
+ });
1614
+ config
1615
+ .command("list-components")
1616
+ .description("List all components and their props (with canonical ids for use with --id flags)")
1617
+ .action(() => {
1618
+ listComponents();
1226
1619
  });
1227
1620
  config
1228
1621
  .command("list")
1229
- .description("List all components and their props")
1622
+ .description("Alias for `list-components` (kept for backwards compatibility)")
1230
1623
  .action(() => {
1231
1624
  listComponents();
1232
1625
  });
@@ -1237,6 +1630,13 @@ export function createConfigCommand() {
1237
1630
  .action(async (options) => {
1238
1631
  await listTypes(options.componentType);
1239
1632
  });
1633
+ config
1634
+ .command("list-locales")
1635
+ .description("List the editor's configured locales — the ONLY valid --locale values for set-prop-translation / set-group-translation. Run this before adding translations; never invent a locale. (Requires a running dev server with the editor connected.)")
1636
+ .option("--port <port>", "Dev server WebSocket port", "5201")
1637
+ .action(async (options) => {
1638
+ await listLocales(options.port ? parseInt(options.port, 10) : undefined);
1639
+ });
1240
1640
  config
1241
1641
  .command("add-enum")
1242
1642
  .description("Create a custom enum type (offline, no editor needed)")