@ikas/component-cli 1.4.0-beta.22 → 1.4.0-beta.220

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 (204) hide show
  1. package/dist/build-tools.d.ts +37 -0
  2. package/dist/build-tools.d.ts.map +1 -0
  3. package/dist/build-tools.js +36 -0
  4. package/dist/build-tools.js.map +1 -0
  5. package/dist/commands/add-sections-to-page.d.ts +3 -0
  6. package/dist/commands/add-sections-to-page.d.ts.map +1 -0
  7. package/dist/commands/add-sections-to-page.js +39 -0
  8. package/dist/commands/add-sections-to-page.js.map +1 -0
  9. package/dist/commands/add.d.ts.map +1 -1
  10. package/dist/commands/add.js +11 -2
  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 +15 -167
  14. package/dist/commands/build.js.map +1 -1
  15. package/dist/commands/config-texts.d.ts +38 -0
  16. package/dist/commands/config-texts.d.ts.map +1 -0
  17. package/dist/commands/config-texts.js +186 -0
  18. package/dist/commands/config-texts.js.map +1 -0
  19. package/dist/commands/config.d.ts +45 -0
  20. package/dist/commands/config.d.ts.map +1 -1
  21. package/dist/commands/config.js +1148 -154
  22. package/dist/commands/config.js.map +1 -1
  23. package/dist/commands/create-design-tokens.d.ts +24 -0
  24. package/dist/commands/create-design-tokens.d.ts.map +1 -0
  25. package/dist/commands/create-design-tokens.js +388 -0
  26. package/dist/commands/create-design-tokens.js.map +1 -0
  27. package/dist/commands/create-global-variable.d.ts +3 -0
  28. package/dist/commands/create-global-variable.d.ts.map +1 -0
  29. package/dist/commands/create-global-variable.js +73 -0
  30. package/dist/commands/create-global-variable.js.map +1 -0
  31. package/dist/commands/create-page.d.ts +3 -0
  32. package/dist/commands/create-page.d.ts.map +1 -0
  33. package/dist/commands/create-page.js +31 -0
  34. package/dist/commands/create-page.js.map +1 -0
  35. package/dist/commands/delete-page.d.ts +3 -0
  36. package/dist/commands/delete-page.d.ts.map +1 -0
  37. package/dist/commands/delete-page.js +28 -0
  38. package/dist/commands/delete-page.js.map +1 -0
  39. package/dist/commands/delete-theme-globals.d.ts +4 -0
  40. package/dist/commands/delete-theme-globals.d.ts.map +1 -0
  41. package/dist/commands/delete-theme-globals.js +50 -0
  42. package/dist/commands/delete-theme-globals.js.map +1 -0
  43. package/dist/commands/dev.d.ts.map +1 -1
  44. package/dist/commands/dev.js +308 -39
  45. package/dist/commands/dev.js.map +1 -1
  46. package/dist/commands/enable-builtin-pages.d.ts +3 -0
  47. package/dist/commands/enable-builtin-pages.d.ts.map +1 -0
  48. package/dist/commands/enable-builtin-pages.js +28 -0
  49. package/dist/commands/enable-builtin-pages.js.map +1 -0
  50. package/dist/commands/get-component-props.d.ts +3 -0
  51. package/dist/commands/get-component-props.d.ts.map +1 -0
  52. package/dist/commands/get-component-props.js +32 -0
  53. package/dist/commands/get-component-props.js.map +1 -0
  54. package/dist/commands/get-page-by-type.d.ts +3 -0
  55. package/dist/commands/get-page-by-type.d.ts.map +1 -0
  56. package/dist/commands/get-page-by-type.js +25 -0
  57. package/dist/commands/get-page-by-type.js.map +1 -0
  58. package/dist/commands/get-section-values.d.ts +3 -0
  59. package/dist/commands/get-section-values.d.ts.map +1 -0
  60. package/dist/commands/get-section-values.js +39 -0
  61. package/dist/commands/get-section-values.js.map +1 -0
  62. package/dist/commands/list-builtin-pages.d.ts +3 -0
  63. package/dist/commands/list-builtin-pages.d.ts.map +1 -0
  64. package/dist/commands/list-builtin-pages.js +28 -0
  65. package/dist/commands/list-builtin-pages.js.map +1 -0
  66. package/dist/commands/list-entities.d.ts +3 -0
  67. package/dist/commands/list-entities.d.ts.map +1 -0
  68. package/dist/commands/list-entities.js +32 -0
  69. package/dist/commands/list-entities.js.map +1 -0
  70. package/dist/commands/list-page-sections.d.ts.map +1 -1
  71. package/dist/commands/list-page-sections.js +8 -2
  72. package/dist/commands/list-page-sections.js.map +1 -1
  73. package/dist/commands/list-theme-globals.d.ts +3 -0
  74. package/dist/commands/list-theme-globals.d.ts.map +1 -0
  75. package/dist/commands/list-theme-globals.js +22 -0
  76. package/dist/commands/list-theme-globals.js.map +1 -0
  77. package/dist/commands/list-translations.d.ts +3 -0
  78. package/dist/commands/list-translations.d.ts.map +1 -0
  79. package/dist/commands/list-translations.js +33 -0
  80. package/dist/commands/list-translations.js.map +1 -0
  81. package/dist/commands/move-page-section.d.ts +3 -0
  82. package/dist/commands/move-page-section.d.ts.map +1 -0
  83. package/dist/commands/move-page-section.js +62 -0
  84. package/dist/commands/move-page-section.js.map +1 -0
  85. package/dist/commands/publish-theme.d.ts +3 -0
  86. package/dist/commands/publish-theme.d.ts.map +1 -0
  87. package/dist/commands/publish-theme.js +29 -0
  88. package/dist/commands/publish-theme.js.map +1 -0
  89. package/dist/commands/publish.d.ts.map +1 -1
  90. package/dist/commands/publish.js +65 -3
  91. package/dist/commands/publish.js.map +1 -1
  92. package/dist/commands/remove-page-section.d.ts +3 -0
  93. package/dist/commands/remove-page-section.d.ts.map +1 -0
  94. package/dist/commands/remove-page-section.js +40 -0
  95. package/dist/commands/remove-page-section.js.map +1 -0
  96. package/dist/commands/search-products.d.ts +3 -0
  97. package/dist/commands/search-products.d.ts.map +1 -0
  98. package/dist/commands/search-products.js +40 -0
  99. package/dist/commands/search-products.js.map +1 -0
  100. package/dist/commands/translation-languages.d.ts +3 -0
  101. package/dist/commands/translation-languages.d.ts.map +1 -0
  102. package/dist/commands/translation-languages.js +43 -0
  103. package/dist/commands/translation-languages.js.map +1 -0
  104. package/dist/commands/update-builtin-page-prop.d.ts +3 -0
  105. package/dist/commands/update-builtin-page-prop.d.ts.map +1 -0
  106. package/dist/commands/update-builtin-page-prop.js +49 -0
  107. package/dist/commands/update-builtin-page-prop.js.map +1 -0
  108. package/dist/commands/update-global-variable.d.ts +3 -0
  109. package/dist/commands/update-global-variable.d.ts.map +1 -0
  110. package/dist/commands/update-global-variable.js +70 -0
  111. package/dist/commands/update-global-variable.js.map +1 -0
  112. package/dist/commands/update-page-sections.d.ts +3 -0
  113. package/dist/commands/update-page-sections.d.ts.map +1 -0
  114. package/dist/commands/update-page-sections.js +39 -0
  115. package/dist/commands/update-page-sections.js.map +1 -0
  116. package/dist/commands/update-section-prop.d.ts.map +1 -1
  117. package/dist/commands/update-section-prop.js +25 -8
  118. package/dist/commands/update-section-prop.js.map +1 -1
  119. package/dist/commands/update-translations.d.ts +3 -0
  120. package/dist/commands/update-translations.d.ts.map +1 -0
  121. package/dist/commands/update-translations.js +37 -0
  122. package/dist/commands/update-translations.js.map +1 -0
  123. package/dist/commands/upload-image.d.ts.map +1 -1
  124. package/dist/commands/upload-image.js +5 -46
  125. package/dist/commands/upload-image.js.map +1 -1
  126. package/dist/commands/upload-images.d.ts +3 -0
  127. package/dist/commands/upload-images.d.ts.map +1 -0
  128. package/dist/commands/upload-images.js +48 -0
  129. package/dist/commands/upload-images.js.map +1 -0
  130. package/dist/index.d.ts.map +1 -1
  131. package/dist/index.js +60 -1
  132. package/dist/index.js.map +1 -1
  133. package/dist/types.d.ts +77 -0
  134. package/dist/types.d.ts.map +1 -1
  135. package/dist/utils/archive-project.d.ts +50 -0
  136. package/dist/utils/archive-project.d.ts.map +1 -0
  137. package/dist/utils/archive-project.js +178 -0
  138. package/dist/utils/archive-project.js.map +1 -0
  139. package/dist/utils/compile.d.ts +17 -2
  140. package/dist/utils/compile.d.ts.map +1 -1
  141. package/dist/utils/compile.js +759 -117
  142. package/dist/utils/compile.js.map +1 -1
  143. package/dist/utils/component-helpers.d.ts +222 -4
  144. package/dist/utils/component-helpers.d.ts.map +1 -1
  145. package/dist/utils/component-helpers.js +558 -37
  146. package/dist/utils/component-helpers.js.map +1 -1
  147. package/dist/utils/css-import-resolver.d.ts +4 -1
  148. package/dist/utils/css-import-resolver.d.ts.map +1 -1
  149. package/dist/utils/css-import-resolver.js +9 -1
  150. package/dist/utils/css-import-resolver.js.map +1 -1
  151. package/dist/utils/dev-server-auth.d.ts +19 -0
  152. package/dist/utils/dev-server-auth.d.ts.map +1 -0
  153. package/dist/utils/dev-server-auth.js +60 -0
  154. package/dist/utils/dev-server-auth.js.map +1 -0
  155. package/dist/utils/editor-action-client.d.ts +4 -3
  156. package/dist/utils/editor-action-client.d.ts.map +1 -1
  157. package/dist/utils/editor-action-client.js +56 -19
  158. package/dist/utils/editor-action-client.js.map +1 -1
  159. package/dist/utils/esm-transform.d.ts +30 -0
  160. package/dist/utils/esm-transform.d.ts.map +1 -1
  161. package/dist/utils/esm-transform.js +169 -2
  162. package/dist/utils/esm-transform.js.map +1 -1
  163. package/dist/utils/find-component-for-file.d.ts +12 -0
  164. package/dist/utils/find-component-for-file.d.ts.map +1 -0
  165. package/dist/utils/find-component-for-file.js +24 -0
  166. package/dist/utils/find-component-for-file.js.map +1 -0
  167. package/dist/utils/forbid-deep-imports.d.ts +3 -0
  168. package/dist/utils/forbid-deep-imports.d.ts.map +1 -0
  169. package/dist/utils/forbid-deep-imports.js +54 -0
  170. package/dist/utils/forbid-deep-imports.js.map +1 -0
  171. package/dist/utils/forbid-external-packages.d.ts +26 -0
  172. package/dist/utils/forbid-external-packages.d.ts.map +1 -0
  173. package/dist/utils/forbid-external-packages.js +99 -0
  174. package/dist/utils/forbid-external-packages.js.map +1 -0
  175. package/dist/utils/i18n.d.ts +80 -0
  176. package/dist/utils/i18n.d.ts.map +1 -0
  177. package/dist/utils/i18n.js +265 -0
  178. package/dist/utils/i18n.js.map +1 -0
  179. package/dist/utils/load-image.d.ts +16 -0
  180. package/dist/utils/load-image.d.ts.map +1 -0
  181. package/dist/utils/load-image.js +50 -0
  182. package/dist/utils/load-image.js.map +1 -0
  183. package/dist/utils/storefront-t.d.ts +17 -0
  184. package/dist/utils/storefront-t.d.ts.map +1 -0
  185. package/dist/utils/storefront-t.js +186 -0
  186. package/dist/utils/storefront-t.js.map +1 -0
  187. package/dist/utils/vendor-storefront.d.ts +13 -0
  188. package/dist/utils/vendor-storefront.d.ts.map +1 -0
  189. package/dist/utils/vendor-storefront.js +91 -0
  190. package/dist/utils/vendor-storefront.js.map +1 -0
  191. package/dist/utils/websocket-server.d.ts +161 -7
  192. package/dist/utils/websocket-server.d.ts.map +1 -1
  193. package/dist/utils/websocket-server.js +324 -45
  194. package/dist/utils/websocket-server.js.map +1 -1
  195. package/package.json +19 -2
  196. package/templates/add/section.css +11 -0
  197. package/dist/commands/create.d.ts +0 -4
  198. package/dist/commands/create.d.ts.map +0 -1
  199. package/dist/commands/create.js +0 -228
  200. package/dist/commands/create.js.map +0 -1
  201. package/dist/commands/proxy.d.ts +0 -39
  202. package/dist/commands/proxy.d.ts.map +0 -1
  203. package/dist/commands/proxy.js +0 -210
  204. package/dist/commands/proxy.js.map +0 -1
@@ -1,7 +1,9 @@
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, validateFilteredComponentIds, } from "../utils/component-helpers.js";
4
+ import { runEditorAction } from "../utils/editor-action-client.js";
5
+ import { listTexts, removeText, renameText, setText } from "./config-texts.js";
6
+ import { PROP_TYPES, toPascalCase, generateTypesFile, isBuiltInEnumTypeId, normalizeEnumOptionsInput, generateGlobalTypesFile, collectUsedEnumIds, generateComponentFile, generateStylesFile, generateProjectId, isValidProjectId, generateComponentId, generateUniqueId, updateBarrelExport, findPropGroup, collectPropGroupIds, movePropGroupInTree, validateFilteredComponentIds, visibilityGateError, } from "../utils/component-helpers.js";
5
7
  function loadConfig() {
6
8
  const configPath = path.resolve(process.cwd(), "ikas.config.json");
7
9
  if (!fs.existsSync(configPath)) {
@@ -31,7 +33,10 @@ function resolveComponent(config, ref) {
31
33
  success: false,
32
34
  error: "Component reference required: pass --id (preferred) or --name (exact PascalCase). " +
33
35
  "Run `ikas-component config list-components` to see canonical ids.",
34
- validComponents: config.components.map((c) => ({ id: c.id, name: c.name })),
36
+ validComponents: config.components.map((c) => ({
37
+ id: c.id,
38
+ name: c.name,
39
+ })),
35
40
  }));
36
41
  process.exit(1);
37
42
  }
@@ -52,7 +57,10 @@ function resolveComponent(config, ref) {
52
57
  ? `Component not found: id="${id}".`
53
58
  : `Component not found: name="${name}". Names are matched exactly (PascalCase, no normalization).`,
54
59
  lookup: id ? { id } : { name },
55
- validComponents: config.components.map((c) => ({ id: c.id, name: c.name })),
60
+ validComponents: config.components.map((c) => ({
61
+ id: c.id,
62
+ name: c.name,
63
+ })),
56
64
  hint: "Use --id (preferred) or exact PascalCase --name. " +
57
65
  "Run `ikas-component config list-components` to see canonical ids.",
58
66
  }));
@@ -60,9 +68,6 @@ function resolveComponent(config, ref) {
60
68
  }
61
69
  return found;
62
70
  }
63
- function getComponentNames(config) {
64
- return config.components.map((c) => c.name);
65
- }
66
71
  /**
67
72
  * Emit a structured JSON error for unknown filteredComponentIds entries and exit non-zero.
68
73
  * Lists valid `{ id, name }` pairs so the LLM/caller can recover.
@@ -90,6 +95,11 @@ function assertKnownComponentIds(input, config) {
90
95
  }
91
96
  return input;
92
97
  }
98
+ // The set of component ids that currently exist in the project, for validateComponentDefaultValue's
99
+ // existence check. Undefined when no config is loaded, which makes the check a no-op (back-compat).
100
+ function existingComponentIdSet(config) {
101
+ return config ? new Set(config.components.map((c) => c.id)) : undefined;
102
+ }
93
103
  function regenerateTypes(component, componentType, config) {
94
104
  const componentDir = path.resolve(process.cwd(), path.dirname(component.entry));
95
105
  const typesPath = path.join(componentDir, "types.ts");
@@ -156,7 +166,118 @@ const ALLOWED_PROP_FIELDS = new Set([
156
166
  "enumTypeId",
157
167
  "filteredComponentIds",
158
168
  "privateVarMap",
169
+ "numberRangeData",
170
+ "requiredProp",
171
+ "requiredPropValue",
172
+ "requiredEnumOptionId",
173
+ "requiredEnumOptionValue",
174
+ "translations",
159
175
  ]);
176
+ // Emit the standard CLI error envelope and exit(1) when a validator returned a message; a no-op
177
+ // when the message is null. Collapses the repeated `if (err) { console.log(...); process.exit(1) }`
178
+ // tail that follows every default-value validator call.
179
+ function failIf(error) {
180
+ if (error) {
181
+ console.log(JSON.stringify({ success: false, error }));
182
+ process.exit(1);
183
+ }
184
+ }
185
+ /** Refusal of a removal run without `--yes`: the user's consent, never the agent's own call. */
186
+ const unconfirmedRemoval = (what, yes) => yes
187
+ ? null
188
+ : `Refused: this would ${what}. Ask the user first and re-run with --yes once they have agreed. Do NOT add it on your own.`;
189
+ /**
190
+ * A `--requiredPropValue` flag as a boolean. Commander hands every flag over as a string, and this is
191
+ * the one place that turns it back; anything other than true/false is refused rather than coerced,
192
+ * since "0" or "off" silently becoming `true` is exactly the kind of gate nobody would think to
193
+ * check.
194
+ */
195
+ function parseRequiredPropValue(raw, where) {
196
+ if (raw === undefined)
197
+ return undefined;
198
+ if (typeof raw === "boolean")
199
+ return raw;
200
+ if (raw === "true")
201
+ return true;
202
+ if (raw === "false")
203
+ return false;
204
+ failIf(`--requiredPropValue for ${where} must be "true" or "false" (got "${raw}").`);
205
+ return undefined;
206
+ }
207
+ /**
208
+ * Validate/sanitize a numberRangeData object for a NUMBER_RANGE prop. Accepts an object
209
+ * or a JSON string. min/max/interval are truncated to integers; only a positive interval
210
+ * is kept (0/negative would yield an invalid slider step). Exits with a structured error
211
+ * on malformed input.
212
+ */
213
+ function parseNumberRangeData(raw, propName) {
214
+ const fail = (error) => {
215
+ console.log(JSON.stringify({ success: false, error }));
216
+ process.exit(1);
217
+ };
218
+ let obj = raw;
219
+ if (typeof raw === "string") {
220
+ try {
221
+ obj = JSON.parse(raw);
222
+ }
223
+ catch {
224
+ fail(`Invalid --numberRangeData JSON for prop "${propName}": ${raw}`);
225
+ }
226
+ }
227
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
228
+ fail(`numberRangeData for prop "${propName}" must be an object { min, max, interval?, unit? }.`);
229
+ }
230
+ const o = obj;
231
+ const asInt = (v, field) => {
232
+ if (v === undefined || v === null)
233
+ return undefined;
234
+ if (typeof v !== "number" || !Number.isFinite(v)) {
235
+ fail(`numberRangeData.${field} for prop "${propName}" must be a finite number.`);
236
+ }
237
+ return Math.trunc(v);
238
+ };
239
+ const min = asInt(o.min, "min");
240
+ const max = asInt(o.max, "max");
241
+ const interval = asInt(o.interval, "interval");
242
+ const unit = typeof o.unit === "string" && o.unit.trim() ? o.unit.trim() : undefined;
243
+ if (min !== undefined && max !== undefined && min >= max) {
244
+ fail(`numberRangeData for prop "${propName}": min (${min}) must be less than max (${max}).`);
245
+ }
246
+ return {
247
+ ...(min !== undefined ? { min } : {}),
248
+ ...(max !== undefined ? { max } : {}),
249
+ ...(interval !== undefined && interval > 0 ? { interval } : {}),
250
+ ...(unit ? { unit } : {}),
251
+ };
252
+ }
253
+ const numberRangeDataRequiredError = (propName) => `numberRangeData with min and max is required for NUMBER_RANGE prop "${propName}". ` +
254
+ `Provide it, e.g. --numberRangeData '{"min":0,"max":100,"interval":5,"unit":"px"}' ` +
255
+ `(or a "numberRangeData" field inside --props). interval and unit are optional.`;
256
+ // A NUMBER_RANGE slider config is complete only with both bounds; interval/unit stay optional.
257
+ export const isNumberRangeComplete = (data) => data.min !== undefined && data.max !== undefined;
258
+ /**
259
+ * A NUMBER_RANGE prop must carry a slider config with at least min and max — the editor UI marks
260
+ * Minimum/Maksimum Değer as required, so a prop authored without them yields a half-configured
261
+ * slider. Enforce it at creation time: reject when numberRangeData is absent or missing min/max.
262
+ */
263
+ export function requireNumberRangeData(raw, propName) {
264
+ if (raw === undefined) {
265
+ console.log(JSON.stringify({
266
+ success: false,
267
+ error: numberRangeDataRequiredError(propName),
268
+ }));
269
+ process.exit(1);
270
+ }
271
+ const parsed = parseNumberRangeData(raw, propName);
272
+ if (!isNumberRangeComplete(parsed)) {
273
+ console.log(JSON.stringify({
274
+ success: false,
275
+ error: numberRangeDataRequiredError(propName),
276
+ }));
277
+ process.exit(1);
278
+ }
279
+ return parsed;
280
+ }
160
281
  /**
161
282
  * Parse and validate the --props JSON flag for add-component.
162
283
  * Returns validated ComponentProp[] or exits with error.
@@ -182,6 +303,8 @@ async function parsePropsFlag(propsJson, config, configPath) {
182
303
  }
183
304
  const seenNames = new Set();
184
305
  const props = [];
306
+ // Loop-invariant: build the known-id set once for the whole batch, not per prop.
307
+ const existingComponentIds = existingComponentIdSet(config);
185
308
  for (const raw of rawProps) {
186
309
  const r = raw;
187
310
  const unknownFields = Object.keys(r).filter((k) => !ALLOWED_PROP_FIELDS.has(k));
@@ -234,6 +357,36 @@ async function parsePropsFlag(propsJson, config, configPath) {
234
357
  }));
235
358
  process.exit(1);
236
359
  }
360
+ // Dynamic (merchant-data reference) props can't carry a static default.
361
+ rejectDynamicDefault(r.name, propType, r.defaultValue);
362
+ // Validate LINK / LIST_OF_LINK defaultValue shape (reject JSON strings and legacy { href })
363
+ if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
364
+ r.defaultValue !== undefined) {
365
+ const linkError = validateLinkDefaultValue(propType, r.defaultValue, r.name);
366
+ failIf(linkError);
367
+ }
368
+ // Validate SVG / SVG_LIST defaultValue (reject malformed/oversized/unsafe SVG)
369
+ if ((propType === "SVG" || propType === "SVG_LIST") &&
370
+ r.defaultValue !== undefined) {
371
+ const svgError = validateSvgDefaultValue(propType, r.defaultValue, r.name);
372
+ failIf(svgError);
373
+ }
374
+ // Validate COMPONENT / COMPONENT_LIST defaultValue (reference-only child code components)
375
+ if ((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
376
+ r.defaultValue !== undefined) {
377
+ failIf(validateComponentDefaultValue(propType, r.defaultValue, r.name, r.filteredComponentIds, existingComponentIds));
378
+ }
379
+ // Validate TYPE (structured/style) defaultValue shape — must be an object (or an array for an
380
+ // _array typeId), never a bare string that would be emitted as a wrong-shaped default.
381
+ if (propType === "TYPE" && r.defaultValue !== undefined) {
382
+ const typeError = validateTypeDefaultValue(r.defaultValue, r.name, r.typeId);
383
+ failIf(typeError);
384
+ }
385
+ // Validate NUMBER defaultValue — reject empty/non-finite (Number("") silently becomes 0).
386
+ if (propType === "NUMBER" && r.defaultValue !== undefined) {
387
+ const numError = validateNumberDefaultValue(r.defaultValue, r.name);
388
+ failIf(numError);
389
+ }
237
390
  // Auto-generate displayName if omitted
238
391
  const displayName = typeof r.displayName === "string" && r.displayName
239
392
  ? r.displayName
@@ -258,7 +411,10 @@ async function parsePropsFlag(propsJson, config, configPath) {
258
411
  process.exit(1);
259
412
  }
260
413
  // Validate enum type exists (checks config then live dev server)
261
- if (config && configPath && typeof r.enumTypeId === "string" && !r.enumTypeId.startsWith("@ikas/")) {
414
+ if (config &&
415
+ configPath &&
416
+ typeof r.enumTypeId === "string" &&
417
+ !isBuiltInEnumTypeId(r.enumTypeId)) {
262
418
  const resolved = await resolveEnumType(r.enumTypeId, config, configPath);
263
419
  if (!resolved) {
264
420
  console.log(JSON.stringify({
@@ -277,9 +433,7 @@ async function parsePropsFlag(propsJson, config, configPath) {
277
433
  ...(typeof r.description === "string" && r.description
278
434
  ? { description: r.description }
279
435
  : {}),
280
- ...(r.defaultValue !== undefined
281
- ? { defaultValue: r.defaultValue }
282
- : {}),
436
+ ...(r.defaultValue !== undefined ? { defaultValue: r.defaultValue } : {}),
283
437
  ...(typeof r.groupId === "string" && r.groupId
284
438
  ? { groupId: r.groupId }
285
439
  : {}),
@@ -289,15 +443,47 @@ async function parsePropsFlag(propsJson, config, configPath) {
289
443
  ...(propType === "ENUM" && typeof r.enumTypeId === "string"
290
444
  ? { enumTypeId: r.enumTypeId }
291
445
  : {}),
292
- ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") && r.filteredComponentIds && Array.isArray(r.filteredComponentIds)
293
- ? { filteredComponentIds: assertKnownComponentIds(r.filteredComponentIds, config) }
446
+ ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
447
+ r.filteredComponentIds &&
448
+ Array.isArray(r.filteredComponentIds)
449
+ ? {
450
+ filteredComponentIds: assertKnownComponentIds(r.filteredComponentIds, config),
451
+ }
452
+ : {}),
453
+ ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
454
+ r.privateVarMap &&
455
+ typeof r.privateVarMap === "object"
456
+ ? {
457
+ privateVarMap: r.privateVarMap,
458
+ }
459
+ : {}),
460
+ ...(propType === "NUMBER_RANGE"
461
+ ? { numberRangeData: requireNumberRangeData(r.numberRangeData, r.name) }
462
+ : {}),
463
+ ...(r.requiredProp !== undefined
464
+ ? { requiredProp: r.requiredProp }
465
+ : {}),
466
+ ...(r.requiredPropValue !== undefined
467
+ ? { requiredPropValue: r.requiredPropValue }
294
468
  : {}),
295
- ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") && r.privateVarMap && typeof r.privateVarMap === "object"
296
- ? { privateVarMap: r.privateVarMap }
469
+ ...(r.requiredEnumOptionId !== undefined
470
+ ? { requiredEnumOptionId: r.requiredEnumOptionId }
471
+ : {}),
472
+ ...(r.requiredEnumOptionValue !== undefined
473
+ ? { requiredEnumOptionValue: r.requiredEnumOptionValue }
297
474
  : {}),
298
475
  };
299
476
  props.push(prop);
300
477
  }
478
+ // Gates are checked once the whole batch is parsed: a prop may name one declared after it, and
479
+ // cycle detection needs every prop in hand. The batch IS the component's prop list here — the
480
+ // component itself does not exist yet — so it stands in as one.
481
+ const batch = { name: "the new component", props };
482
+ for (const prop of props) {
483
+ failIf(visibilityGateError(batch, prop, `prop "${prop.name}"`, {
484
+ selfPropName: prop.name,
485
+ }));
486
+ }
301
487
  return props;
302
488
  }
303
489
  // --- Subcommands ---
@@ -325,11 +511,25 @@ async function addComponent(name, options) {
325
511
  if (!config.projectId) {
326
512
  config.projectId = generateProjectId();
327
513
  }
514
+ else if (!isValidProjectId(config.projectId)) {
515
+ // A malformed projectId (hand-edited config) breaks the editor's republish
516
+ // stability: published/local id prefixes stop matching and every publish
517
+ // INSERTs duplicate design elements. Fail fast instead of corrupting data.
518
+ console.log(JSON.stringify({
519
+ success: false,
520
+ error: `Invalid projectId "${config.projectId}" in ikas.config.json. ` +
521
+ "projectId must be at least 6 alphanumeric characters (no '-' or '_'). " +
522
+ "Restore the original generated value — do not hand-edit it.",
523
+ }));
524
+ process.exit(1);
525
+ }
328
526
  // Parse --props (and validate filteredComponentIds) BEFORE any filesystem side-effects
329
527
  // so a bad payload doesn't leave an empty component directory behind.
330
528
  const props = options.props
331
529
  ? await parsePropsFlag(options.props, config, configPath)
332
530
  : [];
531
+ // Reject section-disallowed TYPE props at definition time (best-effort, needs dev server)
532
+ await assertTypeIdsUsable(componentType, props.flatMap((p) => (p.type === "TYPE" && p.typeId ? [p.typeId] : [])));
333
533
  const componentId = generateComponentId(config.projectId);
334
534
  const componentDir = path.resolve(process.cwd(), `src/components/${pascalName}`);
335
535
  // Create directory
@@ -366,7 +566,7 @@ async function addComponent(name, options) {
366
566
  config.components.push(newComponent);
367
567
  saveConfig(configPath, config);
368
568
  // Update barrel export
369
- updateBarrelExport(process.cwd(), getComponentNames(config));
569
+ updateBarrelExport(process.cwd(), config.components);
370
570
  // Regenerate global types
371
571
  regenerateGlobalTypes(config);
372
572
  console.log(JSON.stringify({
@@ -420,6 +620,10 @@ async function addProp(ref, options) {
420
620
  }));
421
621
  process.exit(1);
422
622
  }
623
+ // Reject section-disallowed types at definition time (best-effort, needs dev server)
624
+ if (propType === "TYPE" && options.typeId) {
625
+ await assertTypeIdsUsable(component.type, [options.typeId]);
626
+ }
423
627
  // Validate enumTypeId for ENUM props
424
628
  if (propType === "ENUM" && !options.enumTypeId) {
425
629
  console.log(JSON.stringify({
@@ -430,7 +634,9 @@ async function addProp(ref, options) {
430
634
  }
431
635
  // Validate that enumTypeId references an existing enum type
432
636
  // (checks config.customTypes first, then falls back to the live dev server)
433
- if (propType === "ENUM" && options.enumTypeId && !options.enumTypeId.startsWith("@ikas/")) {
637
+ if (propType === "ENUM" &&
638
+ options.enumTypeId &&
639
+ !isBuiltInEnumTypeId(options.enumTypeId)) {
434
640
  const resolved = await resolveEnumType(options.enumTypeId, config, configPath);
435
641
  if (!resolved) {
436
642
  console.log(JSON.stringify({
@@ -460,50 +666,117 @@ async function addProp(ref, options) {
460
666
  }
461
667
  // Parse filteredComponentIds JSON for COMPONENT/COMPONENT_LIST
462
668
  let parsedFilteredIds;
463
- if (options.filteredComponentIds && (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
669
+ if (options.filteredComponentIds &&
670
+ (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
464
671
  try {
465
672
  parsedFilteredIds = JSON.parse(options.filteredComponentIds);
466
673
  if (!Array.isArray(parsedFilteredIds)) {
467
- console.log(JSON.stringify({ success: false, error: "--filteredComponentIds must be a JSON array of strings." }));
674
+ console.log(JSON.stringify({
675
+ success: false,
676
+ error: "--filteredComponentIds must be a JSON array of strings.",
677
+ }));
468
678
  process.exit(1);
469
679
  }
470
680
  }
471
681
  catch {
472
- console.log(JSON.stringify({ success: false, error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}` }));
682
+ console.log(JSON.stringify({
683
+ success: false,
684
+ error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
685
+ }));
473
686
  process.exit(1);
474
687
  }
475
688
  assertKnownComponentIds(parsedFilteredIds, config);
476
689
  }
477
690
  // Parse privateVarMap JSON for COMPONENT/COMPONENT_LIST
478
691
  let parsedPrivateVarMap;
479
- if (options.privateVarMap && (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
692
+ if (options.privateVarMap &&
693
+ (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
480
694
  try {
481
695
  parsedPrivateVarMap = JSON.parse(options.privateVarMap);
482
- if (typeof parsedPrivateVarMap !== "object" || Array.isArray(parsedPrivateVarMap)) {
483
- console.log(JSON.stringify({ success: false, error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}." }));
696
+ if (typeof parsedPrivateVarMap !== "object" ||
697
+ Array.isArray(parsedPrivateVarMap)) {
698
+ console.log(JSON.stringify({
699
+ success: false,
700
+ error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
701
+ }));
484
702
  process.exit(1);
485
703
  }
486
704
  }
487
705
  catch {
488
- console.log(JSON.stringify({ success: false, error: `Invalid --privateVarMap JSON: ${options.privateVarMap}` }));
706
+ console.log(JSON.stringify({
707
+ success: false,
708
+ error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
709
+ }));
489
710
  process.exit(1);
490
711
  }
491
712
  }
713
+ rejectDynamicDefault(options.name, propType, options.defaultValue);
714
+ const parsedDefaultValue = options.defaultValue !== undefined
715
+ ? parseDefaultValue(options.defaultValue, propType)
716
+ : undefined;
717
+ if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
718
+ parsedDefaultValue !== undefined) {
719
+ const linkError = validateLinkDefaultValue(propType, parsedDefaultValue, options.name);
720
+ failIf(linkError);
721
+ }
722
+ if ((propType === "SVG" || propType === "SVG_LIST") &&
723
+ parsedDefaultValue !== undefined) {
724
+ const svgError = validateSvgDefaultValue(propType, parsedDefaultValue, options.name);
725
+ failIf(svgError);
726
+ }
727
+ if ((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
728
+ parsedDefaultValue !== undefined) {
729
+ failIf(validateComponentDefaultValue(propType, parsedDefaultValue, options.name, parsedFilteredIds, existingComponentIdSet(config)));
730
+ }
731
+ if (propType === "TYPE" && parsedDefaultValue !== undefined) {
732
+ const typeError = validateTypeDefaultValue(parsedDefaultValue, options.name, options.typeId);
733
+ failIf(typeError);
734
+ }
735
+ // Validate against the raw flag string so an empty --defaultValue (Number("") → 0) is caught.
736
+ if (propType === "NUMBER" && options.defaultValue !== undefined) {
737
+ const numError = validateNumberDefaultValue(options.defaultValue, options.name);
738
+ failIf(numError);
739
+ }
740
+ const parsedVisibilityValue = parseRequiredPropValue(options.requiredPropValue, `prop "${options.name}"`);
741
+ const parsedNumberRangeData = propType === "NUMBER_RANGE"
742
+ ? requireNumberRangeData(options.numberRangeData, options.name)
743
+ : undefined;
744
+ if (propType === "NUMBER_RANGE" && parsedDefaultValue !== undefined) {
745
+ const nrError = validateNumberRangeDefaultValue(parsedDefaultValue, parsedNumberRangeData, options.name);
746
+ failIf(nrError);
747
+ }
492
748
  const newProp = {
493
749
  name: options.name,
494
750
  displayName: options.displayName,
495
751
  type: propType,
496
752
  required: options.required ?? false,
497
753
  ...(options.description ? { description: options.description } : {}),
498
- ...(options.defaultValue !== undefined
499
- ? { defaultValue: parseDefaultValue(options.defaultValue, propType) }
754
+ ...(parsedDefaultValue !== undefined
755
+ ? { defaultValue: parsedDefaultValue }
500
756
  : {}),
501
757
  ...(options.group ? { groupId: options.group } : {}),
502
758
  ...(options.typeId ? { typeId: options.typeId } : {}),
503
759
  ...(options.enumTypeId ? { enumTypeId: options.enumTypeId } : {}),
504
760
  ...(parsedFilteredIds ? { filteredComponentIds: parsedFilteredIds } : {}),
505
761
  ...(parsedPrivateVarMap ? { privateVarMap: parsedPrivateVarMap } : {}),
762
+ ...(parsedNumberRangeData
763
+ ? { numberRangeData: parsedNumberRangeData }
764
+ : {}),
765
+ ...(options.requiredProp ? { requiredProp: options.requiredProp } : {}),
766
+ ...(parsedVisibilityValue !== undefined
767
+ ? { requiredPropValue: parsedVisibilityValue }
768
+ : {}),
769
+ ...(options.requiredProp && options.requiredEnumOptionValue !== undefined
770
+ ? {
771
+ // The editor has no separate option id; the value doubles as one.
772
+ requiredEnumOptionId: options.requiredEnumOptionValue,
773
+ requiredEnumOptionValue: options.requiredEnumOptionValue,
774
+ }
775
+ : {}),
506
776
  };
777
+ failIf(visibilityGateError(component, newProp, `prop "${newProp.name}"`, {
778
+ selfPropName: newProp.name,
779
+ }));
507
780
  component.props.push(newProp);
508
781
  saveConfig(configPath, config);
509
782
  // Regenerate types.ts and global types
@@ -516,25 +789,446 @@ async function addProp(ref, options) {
516
789
  name: newProp.name,
517
790
  displayName: newProp.displayName,
518
791
  type: newProp.type,
519
- required: newProp.required,
520
792
  ...(newProp.groupId ? { groupId: newProp.groupId } : {}),
521
793
  ...(newProp.enumTypeId ? { enumTypeId: newProp.enumTypeId } : {}),
522
- ...(newProp.filteredComponentIds ? { filteredComponentIds: newProp.filteredComponentIds } : {}),
523
- ...(newProp.privateVarMap ? { privateVarMap: newProp.privateVarMap } : {}),
794
+ ...(newProp.filteredComponentIds
795
+ ? { filteredComponentIds: newProp.filteredComponentIds }
796
+ : {}),
797
+ ...(newProp.privateVarMap
798
+ ? { privateVarMap: newProp.privateVarMap }
799
+ : {}),
800
+ ...(newProp.numberRangeData
801
+ ? { numberRangeData: newProp.numberRangeData }
802
+ : {}),
803
+ ...(newProp.requiredProp
804
+ ? {
805
+ requiredProp: newProp.requiredProp,
806
+ ...(newProp.requiredEnumOptionValue !== undefined
807
+ ? { requiredEnumOptionValue: newProp.requiredEnumOptionValue }
808
+ : { requiredPropValue: newProp.requiredPropValue ?? true }),
809
+ }
810
+ : {}),
524
811
  },
525
812
  }));
526
813
  }
527
- function parseDefaultValue(value, propType) {
814
+ export function parseDefaultValue(value, propType) {
528
815
  switch (propType) {
529
816
  case "NUMBER":
530
817
  return Number(value);
531
818
  case "BOOLEAN":
532
819
  return value === "true";
820
+ case "COMPONENT":
821
+ case "COMPONENT_LIST":
822
+ case "LINK":
823
+ case "LIST_OF_LINK":
824
+ case "SVG_LIST":
825
+ case "NUMBER_RANGE":
826
+ case "TYPE":
827
+ // These all carry a typed value (object/array), not a scalar. The --defaultValue flag arrives
828
+ // as a string, so parse it here; if it is not valid JSON, return the raw string so the type's
829
+ // validator (validateLinkDefaultValue / validateSvgDefaultValue / validateNumberRangeDefaultValue
830
+ // / validateTypeDefaultValue / validateComponentDefaultValue) can report a precise error instead
831
+ // of silently storing a bad shape.
832
+ try {
833
+ return JSON.parse(value);
834
+ }
835
+ catch {
836
+ return value;
837
+ }
533
838
  default:
534
839
  return value;
535
840
  }
536
841
  }
537
- function updateProp(ref, options) {
842
+ // Prop types whose value is a dynamic reference to merchant storefront data (products, media,
843
+ // categories, brands, blogs, raffles). They resolve at storefront render time, so a static config
844
+ // default is invalid and is rejected on add/update-prop. Keep in sync with @ikas/editor-models
845
+ // DYNAMIC_PROP_TYPES (the CLI must not depend on that private package).
846
+ export const DYNAMIC_PROP_TYPES = [
847
+ "IMAGE",
848
+ "IMAGE_LIST",
849
+ "VIDEO",
850
+ "PRODUCT",
851
+ "PRODUCT_LIST",
852
+ "PRODUCT_ATTRIBUTE",
853
+ "PRODUCT_ATTRIBUTE_LIST",
854
+ "BRAND",
855
+ "BRAND_LIST",
856
+ "CATEGORY",
857
+ "CATEGORY_LIST",
858
+ "BLOG",
859
+ "BLOG_LIST",
860
+ "BLOG_CATEGORY",
861
+ "BLOG_CATEGORY_LIST",
862
+ "RAFFLE",
863
+ "RAFFLE_LIST",
864
+ ];
865
+ const dynamicPropDefaultError = (propName, propType) => `defaultValue is not allowed for prop "${propName}" of type ${propType}. ${propType} resolves to merchant storefront data at render time, so it can't carry a static default — omit defaultValue.`;
866
+ // Reject a static default on a dynamic (merchant-data reference) prop type: print the standard
867
+ // error and exit. No-op when the type isn't dynamic or no default was supplied.
868
+ export const rejectDynamicDefault = (propName, propType, defaultValue) => {
869
+ if (DYNAMIC_PROP_TYPES.includes(propType) && defaultValue !== undefined) {
870
+ console.log(JSON.stringify({
871
+ success: false,
872
+ error: dynamicPropDefaultError(propName, propType),
873
+ }));
874
+ process.exit(1);
875
+ }
876
+ };
877
+ const nonEmpty = (v) => typeof v === "string" && v.trim().length > 0;
878
+ // FILE is intentionally excluded from CLI-authored defaults: a code-component / AI-authored default
879
+ // can't reference a store-specific uploaded file, so a "FILE" link default is always rejected. Store
880
+ // owners can still create FILE links manually in the editor (editor-core keeps FILE in its own set).
881
+ const VALID_LINK_TYPES = ["PAGE", "EXTERNAL"];
882
+ const LINK_SHAPE_HELP = 'A link must be an object: { "linkType": "EXTERNAL", "label": "Text", "externalLink": "https://...", "subLinks": [] } ' +
883
+ 'for external links, or { "linkType": "PAGE", "label": "Text", "pageType": "INDEX", "subLinks": [] } for store pages. ' +
884
+ "Do NOT pass a JSON string or a { label, href } shape.";
885
+ // Returns the index of the first array element that fails `fn`, as an error string, or null.
886
+ const firstError = (arr, fn) => arr.reduce((acc, item, index) => acc ?? fn(item, index), null);
887
+ const validateSingleLink = (link, propName, path) => {
888
+ const where = `defaultValue for prop "${propName}"${path}`;
889
+ if (typeof link === "string") {
890
+ return `${where} is a JSON string but must be an object. ${LINK_SHAPE_HELP}`;
891
+ }
892
+ if (!link || typeof link !== "object" || Array.isArray(link)) {
893
+ return `${where} must be a link object. ${LINK_SHAPE_HELP}`;
894
+ }
895
+ const l = link;
896
+ if ("href" in l && !("linkType" in l)) {
897
+ return `${where} uses a legacy { href } shape without "linkType". Use "linkType" + "externalLink"/"pageType" instead. ${LINK_SHAPE_HELP}`;
898
+ }
899
+ if (typeof l.linkType !== "string" ||
900
+ !VALID_LINK_TYPES.includes(l.linkType)) {
901
+ return `${where} must have a valid "linkType" (one of ${VALID_LINK_TYPES.join(", ")}). ${LINK_SHAPE_HELP}`;
902
+ }
903
+ // Require the destination field for the declared linkType, so a link that points nowhere never
904
+ // reaches a published theme. A PAGE default carries a pageType (pageId is per-store, filled on
905
+ // materialization); pageId alone is also accepted for a specific page.
906
+ if (l.linkType === "PAGE" && !nonEmpty(l.pageType) && !nonEmpty(l.pageId)) {
907
+ return `${where}: a "PAGE" link needs a "pageType" (e.g. "INDEX") — or a "pageId" for a specific page. ${LINK_SHAPE_HELP}`;
908
+ }
909
+ if (l.linkType === "EXTERNAL" && !nonEmpty(l.externalLink)) {
910
+ return `${where}: an "EXTERNAL" link needs a non-empty "externalLink" URL. ${LINK_SHAPE_HELP}`;
911
+ }
912
+ if (l.subLinks !== undefined && !Array.isArray(l.subLinks)) {
913
+ return `${where}: "subLinks" must be an array (use [] when there are none).`;
914
+ }
915
+ if (Array.isArray(l.subLinks)) {
916
+ return firstError(l.subLinks, (sl, i) => validateSingleLink(sl, propName, `${path}.subLinks[${i}]`));
917
+ }
918
+ return null;
919
+ };
920
+ // Validates a LINK / LIST_OF_LINK defaultValue. Returns an error message, or null if valid.
921
+ // These defaults are a frequent source of bad data: agents tend to pass a JSON string or a
922
+ // legacy { label, href } shape. Rejecting them at authoring time keeps malformed link values
923
+ // out of published themes.
924
+ export function validateLinkDefaultValue(propType, value, propName) {
925
+ if (value === undefined || value === null)
926
+ return null;
927
+ if (propType === "LINK") {
928
+ return validateSingleLink(value, propName, "");
929
+ }
930
+ // LIST_OF_LINK
931
+ if (typeof value === "string") {
932
+ return `defaultValue for prop "${propName}" is a JSON string but must be an object { "links": [...] }. ${LINK_SHAPE_HELP}`;
933
+ }
934
+ if (!value ||
935
+ typeof value !== "object" ||
936
+ Array.isArray(value) ||
937
+ !Array.isArray(value.links)) {
938
+ return `defaultValue for prop "${propName}" must be an object with a "links" array: { "links": [ <link>, ... ] }. ${LINK_SHAPE_HELP}`;
939
+ }
940
+ const links = value.links;
941
+ return firstError(links, (link, i) => validateSingleLink(link, propName, `.links[${i}]`));
942
+ }
943
+ const COMPONENT_SHAPE_HELP = "A COMPONENT / COMPONENT_LIST default must be an object listing child code components: " +
944
+ '{ "components": [ { "codeComponentId": "<id>" } ] }. Only code components are allowed; each entry ' +
945
+ 'needs a string "codeComponentId" (no componentId, no inline propValues).';
946
+ /**
947
+ * Validate a COMPONENT / COMPONENT_LIST defaultValue. Reference-only: each entry must name a code
948
+ * component via `codeComponentId`. Rejects non-CC (`componentId`) children, missing ids, more than
949
+ * one entry for a single COMPONENT, and ids outside the prop's filteredComponentIds allowlist.
950
+ * Returns an error message, or null if valid.
951
+ */
952
+ export function validateComponentDefaultValue(propType, value, propName, filteredComponentIds, existingComponentIds) {
953
+ if (value === undefined || value === null)
954
+ return null;
955
+ const where = `defaultValue for prop "${propName}"`;
956
+ if (typeof value === "string") {
957
+ return `${where} is a JSON string but must be an object. ${COMPONENT_SHAPE_HELP}`;
958
+ }
959
+ if (!value ||
960
+ typeof value !== "object" ||
961
+ Array.isArray(value) ||
962
+ !Array.isArray(value.components)) {
963
+ return `${where} must be an object with a "components" array. ${COMPONENT_SHAPE_HELP}`;
964
+ }
965
+ const components = value.components;
966
+ if (propType === "COMPONENT" && components.length > 1) {
967
+ return `${where}: a COMPONENT prop holds a single child — provide at most one entry (use COMPONENT_LIST for many).`;
968
+ }
969
+ const allow = filteredComponentIds && filteredComponentIds.length
970
+ ? new Set(filteredComponentIds)
971
+ : null;
972
+ return firstError(components, (entry, i) => {
973
+ const at = `${where}.components[${i}]`;
974
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
975
+ return `${at} must be an object { "codeComponentId": "<id>" }. ${COMPONENT_SHAPE_HELP}`;
976
+ }
977
+ const e = entry;
978
+ if ("componentId" in e && !("codeComponentId" in e)) {
979
+ return `${at} references a non-code component (componentId). Only code components are allowed as defaults. ${COMPONENT_SHAPE_HELP}`;
980
+ }
981
+ if (typeof e.codeComponentId !== "string" ||
982
+ e.codeComponentId.trim().length === 0) {
983
+ return `${at} must have a non-empty string "codeComponentId". ${COMPONENT_SHAPE_HELP}`;
984
+ }
985
+ if (allow && !allow.has(e.codeComponentId)) {
986
+ return `${at}: "${e.codeComponentId}" is not in this prop's filteredComponentIds allowlist.`;
987
+ }
988
+ // Reject a reference to a component that does not exist in the project (deleted, renamed, or
989
+ // never created). A dangling default would seed a ghost entry on every placement that renders
990
+ // nothing. Only enforced when the caller supplies the known-component set (the config-backed
991
+ // add-prop / update-prop / batch sites); pure-shape callers pass no set and skip this check.
992
+ if (existingComponentIds && !existingComponentIds.has(e.codeComponentId)) {
993
+ return `${at}: no component with id "${e.codeComponentId}" exists in this project. Create it first (config add-component) or reference an existing component id (config list).`;
994
+ }
995
+ return null;
996
+ });
997
+ }
998
+ const SVG_DEFAULT_HELP = "An SVG default must be a single well-formed <svg>…</svg> string ≤ 64 KB with no embedded base64 raster images. " +
999
+ 'Tip: the editor uses each svg\'s `class` attribute as its display name — set class="my-icon" to name it; ' +
1000
+ "SVG_LIST items with no class show as svg-1, svg-2, ….";
1001
+ // Keep the cap and base64 pattern in sync with @ikas/editor-models validate-svg.ts (the
1002
+ // authoritative validator). Duplicated here on purpose — see validateSvgShape below.
1003
+ const SVG_MAX_SIZE_KB = 64;
1004
+ const SVG_BASE64_RASTER_RE = /data:image\/[a-z0-9.+-]+;base64/i;
1005
+ const SVG_SHAPE_RE = /^\s*(?:<\?xml[^>]*\?>\s*)?(?:<!DOCTYPE[^>]*>\s*)?<svg[\s>][\s\S]*<\/svg>\s*$/i;
1006
+ // Count TOP-LEVEL <svg> roots without a DOM: scan svg open/close tags in order and track
1007
+ // nesting depth. A root is any <svg> opened at depth 0. This matches the editor's DOM-based
1008
+ // single-root check — it rejects siblings (`<svg/><svg/>`) while allowing a nested <svg>.
1009
+ // Self-closing <svg .../> counts as a root but opens no nesting level.
1010
+ function svgRootCount(input) {
1011
+ const tags = input.match(/<svg\b[^>]*>|<\/svg\s*>/gi) || [];
1012
+ return tags.reduce((acc, tag) => {
1013
+ if (tag.startsWith("</"))
1014
+ return { depth: Math.max(0, acc.depth - 1), roots: acc.roots };
1015
+ const roots = acc.depth === 0 ? acc.roots + 1 : acc.roots;
1016
+ const depth = /\/>\s*$/.test(tag) ? acc.depth : acc.depth + 1;
1017
+ return { depth, roots };
1018
+ }, { depth: 0, roots: 0 }).roots;
1019
+ }
1020
+ // Lightweight, dependency-free SVG structure check. This standalone CLI is publishable and
1021
+ // must not depend on the private @ikas/editor-models package (nor pull a DOM/DOMPurify into a
1022
+ // CLI), so it mirrors the RULES of the editor's validateSvg (size, base64, single <svg> root)
1023
+ // for fast authoring feedback. The authoritative sanitize runs in the editor/upload path.
1024
+ // Returns a rejection reason, or null if the shape is acceptable.
1025
+ function validateSvgShape(input) {
1026
+ if (typeof input !== "string" || input.trim() === "")
1027
+ return "empty or not a string";
1028
+ if (Buffer.byteLength(input, "utf8") > SVG_MAX_SIZE_KB * 1024)
1029
+ return `exceeds ${SVG_MAX_SIZE_KB} KB`;
1030
+ if (SVG_BASE64_RASTER_RE.test(input))
1031
+ return "embedded base64 raster image not allowed";
1032
+ if (!SVG_SHAPE_RE.test(input))
1033
+ return "not a single-root <svg> document";
1034
+ if (svgRootCount(input) !== 1)
1035
+ return "must be exactly one <svg> root";
1036
+ return null;
1037
+ }
1038
+ // Validates an SVG / SVG_LIST defaultValue. Returns an error message, or null if valid.
1039
+ // Authoring an SVG default is a common mistake source for agents; rejecting here (with an
1040
+ // instructive message) keeps malformed/oversized SVGs out of published components.
1041
+ export function validateSvgDefaultValue(propType, value, propName) {
1042
+ if (value === undefined || value === null)
1043
+ return null;
1044
+ if (propType === "SVG") {
1045
+ const reason = validateSvgShape(value);
1046
+ return reason
1047
+ ? `defaultValue for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
1048
+ : null;
1049
+ }
1050
+ // SVG_LIST
1051
+ if (!Array.isArray(value)) {
1052
+ return `defaultValue for prop "${propName}" must be an array of SVG strings: [ "<svg>…</svg>", ... ]. ${SVG_DEFAULT_HELP}`;
1053
+ }
1054
+ return firstError(value, (svg, i) => {
1055
+ const reason = validateSvgShape(svg);
1056
+ return reason
1057
+ ? `defaultValue[${i}] for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
1058
+ : null;
1059
+ });
1060
+ }
1061
+ const NUMBER_RANGE_DEFAULT_HELP = 'A NUMBER_RANGE default must be an object { "value": <number>, "unit": "px" | null }. ' +
1062
+ "When the prop defines numberRangeData, value must be within [min, max] and on the interval grid (min + k·interval).";
1063
+ // Validates a NUMBER_RANGE defaultValue. Returns an error message, or null if valid.
1064
+ // Checks the { value, unit } shape and — when the prop carries numberRangeData — that the
1065
+ // value is within [min, max] and lands on the interval grid, mirroring the editor's slider
1066
+ // so an agent can't author an out-of-range or off-step default.
1067
+ export function validateNumberRangeDefaultValue(value, numberRangeData, propName) {
1068
+ if (value === undefined || value === null)
1069
+ return null;
1070
+ const where = `defaultValue for prop "${propName}"`;
1071
+ if (typeof value === "string") {
1072
+ return `${where} is a JSON string but must be an object. ${NUMBER_RANGE_DEFAULT_HELP}`;
1073
+ }
1074
+ if (typeof value !== "object" || Array.isArray(value)) {
1075
+ return `${where} must be an object. ${NUMBER_RANGE_DEFAULT_HELP}`;
1076
+ }
1077
+ const v = value;
1078
+ if (typeof v.value !== "number" || !Number.isFinite(v.value)) {
1079
+ return `${where}: "value" must be a finite number. ${NUMBER_RANGE_DEFAULT_HELP}`;
1080
+ }
1081
+ if (v.unit !== undefined && v.unit !== null && typeof v.unit !== "string") {
1082
+ return `${where}: "unit" must be a string or null. ${NUMBER_RANGE_DEFAULT_HELP}`;
1083
+ }
1084
+ if (numberRangeData) {
1085
+ const { min, max, interval } = numberRangeData;
1086
+ if (min !== undefined && v.value < min) {
1087
+ return `${where}: value ${v.value} is below the configured min (${min}).`;
1088
+ }
1089
+ if (max !== undefined && v.value > max) {
1090
+ return `${where}: value ${v.value} is above the configured max (${max}).`;
1091
+ }
1092
+ if (interval !== undefined &&
1093
+ interval > 0 &&
1094
+ min !== undefined &&
1095
+ (v.value - min) % interval !== 0) {
1096
+ return `${where}: value ${v.value} is not on the interval grid (min ${min} + k·${interval}).`;
1097
+ }
1098
+ }
1099
+ return null;
1100
+ }
1101
+ const NUMBER_DEFAULT_HELP = "A NUMBER default must be a finite number, e.g. --defaultValue 42.";
1102
+ // Validates a NUMBER defaultValue. Returns an error message, or null if valid. Guards two footguns:
1103
+ // an empty/whitespace flag string (Number("") === 0 would silently store 0) and a non-numeric or
1104
+ // non-finite value (e.g. "42px" → NaN, "Infinity"). The flag path passes the raw --defaultValue
1105
+ // string; the --props path passes the already-parsed JSON value — both are handled here.
1106
+ export function validateNumberDefaultValue(value, propName) {
1107
+ if (value === undefined || value === null)
1108
+ return null;
1109
+ const where = `defaultValue for prop "${propName}"`;
1110
+ if (typeof value === "string") {
1111
+ if (value.trim() === "") {
1112
+ return `${where} is empty, but NUMBER requires a finite number. ${NUMBER_DEFAULT_HELP}`;
1113
+ }
1114
+ if (!Number.isFinite(Number(value))) {
1115
+ return `${where} ("${value}") is not a finite number. ${NUMBER_DEFAULT_HELP}`;
1116
+ }
1117
+ return null;
1118
+ }
1119
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1120
+ return `${where} must be a finite number. ${NUMBER_DEFAULT_HELP}`;
1121
+ }
1122
+ return null;
1123
+ }
1124
+ const STYLE_TYPE_DEFAULT_HELP = "A size/style TYPE default must be an object matching the type's shape: " +
1125
+ '{ "value": <number>, "unit": "px" | "rem" | "%" | "vh" | "vw" } (or { "css": "<raw css>" }). ' +
1126
+ "Author it via add-component --props '[…]' or --defaultValue '<json>'.";
1127
+ const BP_STOREFRONT_PREFIX = "@ikas/bp-storefront-models-";
1128
+ // The size/style types that share the { css?, value?, unit? } shape. This is EXACTLY the set of
1129
+ // TYPE typeIds whose default the generator emits (mirror of DYNAMIC_STYLE_TYPE_IDS in
1130
+ // @ikas/blueprint-modules design-generator/generate-helpers/styles.ts — the standalone CLI must not
1131
+ // depend on that package, so it is hand-maintained here and kept in sync by a parity test). Every
1132
+ // OTHER TYPE typeId (BorderStyleType, BoxShadowStyleType, GridTemplateColumnsStyleType, string-union
1133
+ // styles like TextAlignStyleType, IkasProduct, custom types) has a DIFFERENT value shape, so their
1134
+ // defaults are not shape-checked (and are dead code in the generator anyway).
1135
+ const SIZE_STYLE_TYPE_NAMES = [
1136
+ "SizeStyleType",
1137
+ "PaddingStyleType",
1138
+ "PaddingTopStyleType",
1139
+ "PaddingRightStyleType",
1140
+ "PaddingBottomStyleType",
1141
+ "PaddingLeftStyleType",
1142
+ "MarginStyleType",
1143
+ "MarginTopStyleType",
1144
+ "MarginRightStyleType",
1145
+ "MarginBottomStyleType",
1146
+ "MarginLeftStyleType",
1147
+ "BorderRadiusStyleType",
1148
+ "BorderRadiusTopLeftStyleType",
1149
+ "BorderRadiusTopRightStyleType",
1150
+ "BorderRadiusBottomRightStyleType",
1151
+ "BorderRadiusBottomLeftStyleType",
1152
+ "FontSizeStyleType",
1153
+ "LineHeightStyleType",
1154
+ "LetterSpacingStyleType",
1155
+ "HeightStyleType",
1156
+ "MinHeightStyleType",
1157
+ "MaxHeightStyleType",
1158
+ "WidthStyleType",
1159
+ "MinWidthStyleType",
1160
+ "MaxWidthStyleType",
1161
+ "GapStyleType",
1162
+ "BorderWidthStyleType",
1163
+ "TopStyleType",
1164
+ "RightStyleType",
1165
+ "BottomStyleType",
1166
+ "LeftStyleType",
1167
+ ];
1168
+ // Validates a single style-type default object. Every size style type shares the optional
1169
+ // { css?, value?, unit? } shape, so inner fields are checked loosely — the key rejection is a
1170
+ // non-object (e.g. a bare JSON string, the common failure when a --defaultValue flag is not parsed).
1171
+ function styleDefaultObjectError(value, where) {
1172
+ if (typeof value === "string") {
1173
+ return `${where} is a JSON string but must be an object. ${STYLE_TYPE_DEFAULT_HELP}`;
1174
+ }
1175
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1176
+ return `${where} must be an object. ${STYLE_TYPE_DEFAULT_HELP}`;
1177
+ }
1178
+ const v = value;
1179
+ // css/value/unit are all optional, and the editor treats an explicit null as "unset" (it accepts
1180
+ // { value: null }). So only type-check a field that is present AND non-null — otherwise the CLI
1181
+ // would be stricter than the authoritative editor and reject a value it would happily accept.
1182
+ // Unknown fields ARE rejected below: every size-style type shares exactly this { css?, value?, unit? }
1183
+ // shape, so an extra key is always a mistake (this matches the editor). The per-type value-DOMAIN
1184
+ // (which units a given type allows — BorderWidth also permits em/vmin/vmax while the rest are
1185
+ // px/rem/%/vh/vw) stays editor-side: it is per-type and can't be mirrored offline without drift.
1186
+ if (v.css !== undefined && v.css !== null && typeof v.css !== "string") {
1187
+ return `${where}: "css" must be a string. ${STYLE_TYPE_DEFAULT_HELP}`;
1188
+ }
1189
+ if (v.value !== undefined &&
1190
+ v.value !== null &&
1191
+ (typeof v.value !== "number" || !Number.isFinite(v.value))) {
1192
+ return `${where}: "value" must be a finite number. ${STYLE_TYPE_DEFAULT_HELP}`;
1193
+ }
1194
+ if (v.unit !== undefined && v.unit !== null && typeof v.unit !== "string") {
1195
+ return `${where}: "unit" must be a string. ${STYLE_TYPE_DEFAULT_HELP}`;
1196
+ }
1197
+ const unknownKey = Object.keys(v).find((k) => k !== "css" && k !== "value" && k !== "unit");
1198
+ if (unknownKey) {
1199
+ return `${where}: unknown field "${unknownKey}". A size/style default allows only "css", "value", "unit". ${STYLE_TYPE_DEFAULT_HELP}`;
1200
+ }
1201
+ return null;
1202
+ }
1203
+ // Validates a TYPE prop defaultValue — but ONLY for the size/style types whose shape we know
1204
+ // ({ css?, value?, unit? }). Any other TYPE typeId (color/border/box-shadow/grid/string-union
1205
+ // styles, IkasProduct, custom types) has a different value shape we can't verify offline, so its
1206
+ // default is passed through unchecked. The generator only emits a default for these size/style
1207
+ // types anyway. Returns an error message, or null if valid / not a size-style type.
1208
+ export function validateTypeDefaultValue(value, propName, typeId) {
1209
+ if (value === undefined || value === null)
1210
+ return null;
1211
+ if (typeof typeId !== "string" || !typeId.startsWith(BP_STOREFRONT_PREFIX)) {
1212
+ return null;
1213
+ }
1214
+ // Strip the prefix and an optional _array suffix to get the base type name.
1215
+ const withoutPrefix = typeId.slice(BP_STOREFRONT_PREFIX.length);
1216
+ const isArray = withoutPrefix.endsWith("_array");
1217
+ const baseName = isArray
1218
+ ? withoutPrefix.slice(0, -"_array".length)
1219
+ : withoutPrefix;
1220
+ if (!SIZE_STYLE_TYPE_NAMES.includes(baseName))
1221
+ return null;
1222
+ const where = `defaultValue for prop "${propName}"`;
1223
+ if (isArray) {
1224
+ if (!Array.isArray(value)) {
1225
+ return `${where} must be an array for an _array TYPE prop: [ { "value": …, "unit": … }, … ]. ${STYLE_TYPE_DEFAULT_HELP}`;
1226
+ }
1227
+ return firstError(value, (el, i) => styleDefaultObjectError(el, `${where}[${i}]`));
1228
+ }
1229
+ return styleDefaultObjectError(value, where);
1230
+ }
1231
+ async function updateProp(ref, options) {
538
1232
  const { config, configPath } = loadConfig();
539
1233
  const component = resolveComponent(config, ref);
540
1234
  const propIndex = component.props.findIndex((p) => p.name === options.prop);
@@ -566,8 +1260,87 @@ function updateProp(ref, options) {
566
1260
  if (options.description !== undefined) {
567
1261
  prop.description = options.description || undefined;
568
1262
  }
1263
+ // Resolve numberRangeData before defaultValue so a NUMBER_RANGE default can be validated
1264
+ // against the (possibly just-updated) slider config in the same call.
1265
+ if (options.numberRangeData !== undefined) {
1266
+ if (options.numberRangeData === "" || options.numberRangeData === "none") {
1267
+ delete prop.numberRangeData;
1268
+ }
1269
+ else {
1270
+ prop.numberRangeData = parseNumberRangeData(options.numberRangeData, prop.name);
1271
+ }
1272
+ }
1273
+ // A NUMBER_RANGE prop must keep a valid slider config (min + max). Enforce the invariant only when
1274
+ // this update touches the type or numberRangeData — a benign update (e.g. displayName) on an
1275
+ // existing prop must still pass. This catches switching a prop TO NUMBER_RANGE without a config, or
1276
+ // clearing/blanking numberRangeData on a NUMBER_RANGE prop.
1277
+ if ((options.type !== undefined || options.numberRangeData !== undefined) &&
1278
+ prop.type === "NUMBER_RANGE" &&
1279
+ (!prop.numberRangeData || !isNumberRangeComplete(prop.numberRangeData))) {
1280
+ console.log(JSON.stringify({
1281
+ success: false,
1282
+ error: numberRangeDataRequiredError(prop.name),
1283
+ }));
1284
+ process.exit(1);
1285
+ }
1286
+ // Resolve filteredComponentIds before defaultValue so a COMPONENT / COMPONENT_LIST default can be
1287
+ // validated against the (possibly just-updated) allowlist in the same call.
1288
+ if (options.filteredComponentIds !== undefined) {
1289
+ if (options.filteredComponentIds === "" ||
1290
+ options.filteredComponentIds === "none") {
1291
+ delete prop.filteredComponentIds;
1292
+ }
1293
+ else {
1294
+ try {
1295
+ const parsed = JSON.parse(options.filteredComponentIds);
1296
+ if (!Array.isArray(parsed)) {
1297
+ console.log(JSON.stringify({
1298
+ success: false,
1299
+ error: "--filteredComponentIds must be a JSON array of strings.",
1300
+ }));
1301
+ process.exit(1);
1302
+ }
1303
+ assertKnownComponentIds(parsed, config);
1304
+ prop.filteredComponentIds = parsed;
1305
+ }
1306
+ catch (err) {
1307
+ if (err instanceof SyntaxError) {
1308
+ console.log(JSON.stringify({
1309
+ success: false,
1310
+ error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
1311
+ }));
1312
+ process.exit(1);
1313
+ }
1314
+ throw err;
1315
+ }
1316
+ }
1317
+ }
569
1318
  if (options.defaultValue !== undefined) {
570
- prop.defaultValue = parseDefaultValue(options.defaultValue, prop.type);
1319
+ rejectDynamicDefault(prop.name, prop.type, options.defaultValue);
1320
+ const parsed = parseDefaultValue(options.defaultValue, prop.type);
1321
+ // Validate against the raw flag string so an empty --defaultValue (Number("") → 0) is caught.
1322
+ if (prop.type === "NUMBER") {
1323
+ const numError = validateNumberDefaultValue(options.defaultValue, prop.name);
1324
+ failIf(numError);
1325
+ }
1326
+ if (prop.type === "LINK" || prop.type === "LIST_OF_LINK") {
1327
+ const linkError = validateLinkDefaultValue(prop.type, parsed, prop.name);
1328
+ failIf(linkError);
1329
+ }
1330
+ if ((prop.type === "COMPONENT" || prop.type === "COMPONENT_LIST") &&
1331
+ parsed !== undefined) {
1332
+ failIf(validateComponentDefaultValue(prop.type, parsed, prop.name, prop.filteredComponentIds, existingComponentIdSet(config)));
1333
+ }
1334
+ if (prop.type === "NUMBER_RANGE") {
1335
+ const nrError = validateNumberRangeDefaultValue(parsed, prop.numberRangeData, prop.name);
1336
+ failIf(nrError);
1337
+ }
1338
+ if (prop.type === "TYPE") {
1339
+ // Prefer a typeId supplied in this same update over the stored one.
1340
+ const typeError = validateTypeDefaultValue(parsed, prop.name, options.typeId ?? prop.typeId);
1341
+ failIf(typeError);
1342
+ }
1343
+ prop.defaultValue = parsed;
571
1344
  }
572
1345
  if (options.group !== undefined) {
573
1346
  if (options.group === "" || options.group === "none") {
@@ -600,6 +1373,13 @@ function updateProp(ref, options) {
600
1373
  prop.typeId = options.typeId;
601
1374
  }
602
1375
  }
1376
+ // Reject section-disallowed types when the caller changes type/typeId;
1377
+ // untouched legacy props stay editable (displayName, group, ...).
1378
+ if ((options.type !== undefined || options.typeId !== undefined) &&
1379
+ prop.type === "TYPE" &&
1380
+ prop.typeId) {
1381
+ await assertTypeIdsUsable(component.type, [prop.typeId]);
1382
+ }
603
1383
  if (options.enumTypeId !== undefined) {
604
1384
  if (options.enumTypeId === "" || options.enumTypeId === "none") {
605
1385
  delete prop.enumTypeId;
@@ -608,29 +1388,6 @@ function updateProp(ref, options) {
608
1388
  prop.enumTypeId = options.enumTypeId;
609
1389
  }
610
1390
  }
611
- if (options.filteredComponentIds !== undefined) {
612
- if (options.filteredComponentIds === "" || options.filteredComponentIds === "none") {
613
- delete prop.filteredComponentIds;
614
- }
615
- else {
616
- try {
617
- const parsed = JSON.parse(options.filteredComponentIds);
618
- if (!Array.isArray(parsed)) {
619
- console.log(JSON.stringify({ success: false, error: "--filteredComponentIds must be a JSON array of strings." }));
620
- process.exit(1);
621
- }
622
- assertKnownComponentIds(parsed, config);
623
- prop.filteredComponentIds = parsed;
624
- }
625
- catch (err) {
626
- if (err instanceof SyntaxError) {
627
- console.log(JSON.stringify({ success: false, error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}` }));
628
- process.exit(1);
629
- }
630
- throw err;
631
- }
632
- }
633
- }
634
1391
  if (options.privateVarMap !== undefined) {
635
1392
  if (options.privateVarMap === "" || options.privateVarMap === "none") {
636
1393
  delete prop.privateVarMap;
@@ -639,17 +1396,50 @@ function updateProp(ref, options) {
639
1396
  try {
640
1397
  const parsed = JSON.parse(options.privateVarMap);
641
1398
  if (typeof parsed !== "object" || Array.isArray(parsed)) {
642
- console.log(JSON.stringify({ success: false, error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}." }));
1399
+ console.log(JSON.stringify({
1400
+ success: false,
1401
+ error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
1402
+ }));
643
1403
  process.exit(1);
644
1404
  }
645
1405
  prop.privateVarMap = parsed;
646
1406
  }
647
1407
  catch {
648
- console.log(JSON.stringify({ success: false, error: `Invalid --privateVarMap JSON: ${options.privateVarMap}` }));
1408
+ console.log(JSON.stringify({
1409
+ success: false,
1410
+ error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
1411
+ }));
649
1412
  process.exit(1);
650
1413
  }
651
1414
  }
652
1415
  }
1416
+ if (options.requiredProp !== undefined) {
1417
+ if (options.requiredProp === "" || options.requiredProp === "none") {
1418
+ // Every field goes together — see `IProp.requiredPropId`.
1419
+ delete prop.requiredProp;
1420
+ delete prop.requiredPropValue;
1421
+ delete prop.requiredEnumOptionId;
1422
+ delete prop.requiredEnumOptionValue;
1423
+ }
1424
+ else {
1425
+ prop.requiredProp = options.requiredProp;
1426
+ }
1427
+ }
1428
+ if (options.requiredEnumOptionValue !== undefined && prop.requiredProp) {
1429
+ prop.requiredEnumOptionId = options.requiredEnumOptionValue;
1430
+ prop.requiredEnumOptionValue = options.requiredEnumOptionValue;
1431
+ delete prop.requiredPropValue;
1432
+ }
1433
+ if (options.requiredPropValue !== undefined && prop.requiredProp) {
1434
+ delete prop.requiredEnumOptionId;
1435
+ delete prop.requiredEnumOptionValue;
1436
+ prop.requiredPropValue = parseRequiredPropValue(options.requiredPropValue, `prop "${prop.name}"`);
1437
+ }
1438
+ // Re-checked on every update, not only when a visibility flag is passed: flipping `required`, or
1439
+ // renaming the prop this one hangs on, can invalidate a gate that was sound when it was written.
1440
+ failIf(visibilityGateError(component, prop, `prop "${prop.name}"`, {
1441
+ selfPropName: prop.name,
1442
+ }));
653
1443
  saveConfig(configPath, config);
654
1444
  // Regenerate types.ts and global types
655
1445
  regenerateTypes(component, undefined, config);
@@ -661,16 +1451,29 @@ function updateProp(ref, options) {
661
1451
  name: prop.name,
662
1452
  displayName: prop.displayName,
663
1453
  type: prop.type,
664
- required: prop.required,
665
1454
  ...(prop.groupId ? { groupId: prop.groupId } : {}),
666
1455
  ...(prop.typeId ? { typeId: prop.typeId } : {}),
667
1456
  ...(prop.enumTypeId ? { enumTypeId: prop.enumTypeId } : {}),
668
- ...(prop.filteredComponentIds ? { filteredComponentIds: prop.filteredComponentIds } : {}),
1457
+ ...(prop.filteredComponentIds
1458
+ ? { filteredComponentIds: prop.filteredComponentIds }
1459
+ : {}),
669
1460
  ...(prop.privateVarMap ? { privateVarMap: prop.privateVarMap } : {}),
1461
+ ...(prop.numberRangeData
1462
+ ? { numberRangeData: prop.numberRangeData }
1463
+ : {}),
1464
+ ...(prop.requiredProp
1465
+ ? {
1466
+ requiredProp: prop.requiredProp,
1467
+ ...(prop.requiredEnumOptionValue !== undefined
1468
+ ? { requiredEnumOptionValue: prop.requiredEnumOptionValue }
1469
+ : { requiredPropValue: prop.requiredPropValue ?? true }),
1470
+ }
1471
+ : {}),
670
1472
  },
671
1473
  }));
672
1474
  }
673
1475
  function removeProp(ref, options) {
1476
+ failIf(unconfirmedRemoval(`remove the prop "${options.prop}" and the values set for it on every page`, options.yes));
674
1477
  const { config, configPath } = loadConfig();
675
1478
  const component = resolveComponent(config, ref);
676
1479
  const propIndex = component.props.findIndex((p) => p.name === options.prop);
@@ -709,11 +1512,30 @@ function pruneFilteredComponentIdRefs(config, removedId) {
709
1512
  }
710
1513
  }
711
1514
  }
712
- function removeComponent(ref) {
1515
+ /**
1516
+ * Why `component`'s folder may not be deleted, or null: only its own folder directly under src/components
1517
+ * (where components are created), never one holding another component — so a hand-edited entry can not
1518
+ * point the removal at the project, a folder outside it, or any other project folder.
1519
+ */
1520
+ export function componentDirRemovalError(components, component, root) {
1521
+ const dir = path.resolve(root, path.dirname(component.entry));
1522
+ const holdsOther = components.some((other) => {
1523
+ if (other === component)
1524
+ return false;
1525
+ const otherDir = path.resolve(root, path.dirname(other.entry));
1526
+ return otherDir === dir || otherDir.startsWith(dir + path.sep);
1527
+ });
1528
+ return path.dirname(dir) !== path.resolve(root, "src/components") || holdsOther
1529
+ ? `Refused: "${path.relative(root, dir) || "."}" (from the component's entry) is not a folder of its own under src/components; nothing was removed.`
1530
+ : null;
1531
+ }
1532
+ function removeComponent(ref, yes) {
713
1533
  const { config, configPath } = loadConfig();
714
1534
  const component = resolveComponent(config, ref);
1535
+ failIf(unconfirmedRemoval(`delete the component "${component.name}" and its folder`, yes));
715
1536
  const componentIndex = config.components.indexOf(component);
716
1537
  const componentDir = path.resolve(process.cwd(), path.dirname(component.entry));
1538
+ failIf(componentDirRemovalError(config.components, component, process.cwd()));
717
1539
  // Remove component from config and strip orphaned references from remaining components
718
1540
  config.components.splice(componentIndex, 1);
719
1541
  pruneFilteredComponentIdRefs(config, component.id);
@@ -723,13 +1545,16 @@ function removeComponent(ref) {
723
1545
  fs.rmSync(componentDir, { recursive: true });
724
1546
  }
725
1547
  // Update barrel export
726
- updateBarrelExport(process.cwd(), getComponentNames(config));
1548
+ updateBarrelExport(process.cwd(), config.components);
727
1549
  console.log(JSON.stringify({
728
1550
  success: true,
729
1551
  removedComponentId: component.id,
730
1552
  removedComponentName: component.name,
731
1553
  removedDirectory: path.relative(process.cwd(), componentDir),
732
- remainingComponents: config.components.map((c) => ({ id: c.id, name: c.name })),
1554
+ remainingComponents: config.components.map((c) => ({
1555
+ id: c.id,
1556
+ name: c.name,
1557
+ })),
733
1558
  }));
734
1559
  }
735
1560
  function addPropGroup(ref, options) {
@@ -750,7 +1575,24 @@ function addPropGroup(ref, options) {
750
1575
  id: options.id,
751
1576
  name: options.name,
752
1577
  ...(options.description ? { description: options.description } : {}),
1578
+ ...(options.requiredProp ? { requiredProp: options.requiredProp } : {}),
1579
+ ...(options.requiredProp && options.requiredEnumOptionValue !== undefined
1580
+ ? {
1581
+ requiredEnumOptionId: options.requiredEnumOptionValue,
1582
+ requiredEnumOptionValue: options.requiredEnumOptionValue,
1583
+ }
1584
+ : {}),
1585
+ ...(options.requiredProp &&
1586
+ options.requiredEnumOptionValue === undefined &&
1587
+ options.requiredPropValue !== undefined
1588
+ ? {
1589
+ requiredPropValue: parseRequiredPropValue(options.requiredPropValue, `group "${options.id}"`),
1590
+ }
1591
+ : {}),
753
1592
  };
1593
+ failIf(visibilityGateError(component, newGroup, `group "${newGroup.id}"`, {
1594
+ group: newGroup,
1595
+ }));
754
1596
  if (options.parent) {
755
1597
  const found = findPropGroup(component.propGroups, options.parent);
756
1598
  if (!found) {
@@ -782,6 +1624,14 @@ function addPropGroup(ref, options) {
782
1624
  id: newGroup.id,
783
1625
  name: newGroup.name,
784
1626
  ...(options.parent ? { parentId: options.parent } : {}),
1627
+ ...(newGroup.requiredProp
1628
+ ? {
1629
+ requiredProp: newGroup.requiredProp,
1630
+ ...(newGroup.requiredEnumOptionValue !== undefined
1631
+ ? { requiredEnumOptionValue: newGroup.requiredEnumOptionValue }
1632
+ : { requiredPropValue: newGroup.requiredPropValue ?? true }),
1633
+ }
1634
+ : {}),
785
1635
  },
786
1636
  }));
787
1637
  }
@@ -807,6 +1657,34 @@ function updatePropGroup(ref, options) {
807
1657
  found.group.name = options.name;
808
1658
  if (options.description !== undefined)
809
1659
  found.group.description = options.description || undefined;
1660
+ if (options.requiredProp !== undefined) {
1661
+ if (options.requiredProp === "" || options.requiredProp === "none") {
1662
+ // Every field goes together — see `IThemePropGroup.requiredPropId`.
1663
+ delete found.group.requiredProp;
1664
+ delete found.group.requiredPropValue;
1665
+ delete found.group.requiredEnumOptionId;
1666
+ delete found.group.requiredEnumOptionValue;
1667
+ }
1668
+ else {
1669
+ found.group.requiredProp = options.requiredProp;
1670
+ }
1671
+ }
1672
+ if (options.requiredEnumOptionValue !== undefined &&
1673
+ found.group.requiredProp) {
1674
+ found.group.requiredEnumOptionId = options.requiredEnumOptionValue;
1675
+ found.group.requiredEnumOptionValue = options.requiredEnumOptionValue;
1676
+ delete found.group.requiredPropValue;
1677
+ }
1678
+ if (options.requiredPropValue !== undefined && found.group.requiredProp) {
1679
+ delete found.group.requiredEnumOptionId;
1680
+ delete found.group.requiredEnumOptionValue;
1681
+ found.group.requiredPropValue = parseRequiredPropValue(options.requiredPropValue, `group "${found.group.id}"`);
1682
+ }
1683
+ // Re-checked on every update: moving a prop into this group can invalidate a gate that was sound
1684
+ // when it was written.
1685
+ failIf(visibilityGateError(component, found.group, `group "${found.group.id}"`, {
1686
+ group: found.group,
1687
+ }));
810
1688
  saveConfig(configPath, config);
811
1689
  console.log(JSON.stringify({
812
1690
  success: true,
@@ -814,6 +1692,16 @@ function updatePropGroup(ref, options) {
814
1692
  updatedPropGroup: {
815
1693
  id: found.group.id,
816
1694
  name: found.group.name,
1695
+ ...(found.group.requiredProp
1696
+ ? {
1697
+ requiredProp: found.group.requiredProp,
1698
+ ...(found.group.requiredEnumOptionValue !== undefined
1699
+ ? {
1700
+ requiredEnumOptionValue: found.group.requiredEnumOptionValue,
1701
+ }
1702
+ : { requiredPropValue: found.group.requiredPropValue ?? true }),
1703
+ }
1704
+ : {}),
817
1705
  },
818
1706
  }));
819
1707
  }
@@ -836,10 +1724,10 @@ function removePropGroup(ref, options) {
836
1724
  process.exit(1);
837
1725
  }
838
1726
  if (found.parent) {
839
- found.parent.children = found.parent.children.filter(c => c.id !== options.id);
1727
+ found.parent.children = found.parent.children.filter((c) => c.id !== options.id);
840
1728
  }
841
1729
  else {
842
- component.propGroups = component.propGroups.filter(g => g.id !== options.id);
1730
+ component.propGroups = component.propGroups.filter((g) => g.id !== options.id);
843
1731
  }
844
1732
  // Clear groupId from props referencing the deleted group
845
1733
  for (const prop of component.props) {
@@ -852,7 +1740,7 @@ function removePropGroup(ref, options) {
852
1740
  success: true,
853
1741
  componentName: component.name,
854
1742
  removedPropGroup: options.id,
855
- remainingPropGroups: component.propGroups.map(g => g.id),
1743
+ remainingPropGroups: component.propGroups.map((g) => g.id),
856
1744
  }));
857
1745
  }
858
1746
  function movePropGroup(ref, options) {
@@ -869,7 +1757,10 @@ function movePropGroup(ref, options) {
869
1757
  component.propGroups = movePropGroupInTree(component.propGroups, options.id, options.parent || null, options.index);
870
1758
  }
871
1759
  catch (e) {
872
- console.log(JSON.stringify({ success: false, error: e.message || "Failed to move prop group" }));
1760
+ console.log(JSON.stringify({
1761
+ success: false,
1762
+ error: e.message || "Failed to move prop group",
1763
+ }));
873
1764
  process.exit(1);
874
1765
  }
875
1766
  saveConfig(configPath, config);
@@ -925,7 +1816,9 @@ function listComponents() {
925
1816
  required: p.required,
926
1817
  ...(p.groupId ? { groupId: p.groupId } : {}),
927
1818
  })),
928
- ...(c.propGroups && c.propGroups.length > 0 ? { propGroups: c.propGroups } : {}),
1819
+ ...(c.propGroups && c.propGroups.length > 0
1820
+ ? { propGroups: c.propGroups }
1821
+ : {}),
929
1822
  }));
930
1823
  console.log(JSON.stringify({
931
1824
  success: true,
@@ -939,7 +1832,9 @@ function listComponents() {
939
1832
  async function addEnum(options) {
940
1833
  const { config, configPath } = loadConfig();
941
1834
  // Validate name
942
- if (!options.name || typeof options.name !== "string" || !options.name.trim()) {
1835
+ if (!options.name ||
1836
+ typeof options.name !== "string" ||
1837
+ !options.name.trim()) {
943
1838
  console.log(JSON.stringify({ success: false, error: "Enum name is required." }));
944
1839
  process.exit(1);
945
1840
  }
@@ -967,10 +1862,10 @@ async function addEnum(options) {
967
1862
  }));
968
1863
  process.exit(1);
969
1864
  }
970
- // Parse and validate options JSON
971
- let enumOptions;
1865
+ // Parse and validate options JSON (object form, or the `[{ name, value }]` array form)
1866
+ let parsedOptions;
972
1867
  try {
973
- enumOptions = JSON.parse(options.options);
1868
+ parsedOptions = JSON.parse(options.options);
974
1869
  }
975
1870
  catch {
976
1871
  console.log(JSON.stringify({
@@ -979,15 +1874,12 @@ async function addEnum(options) {
979
1874
  }));
980
1875
  process.exit(1);
981
1876
  }
982
- if (typeof enumOptions !== "object" ||
983
- Array.isArray(enumOptions) ||
984
- Object.keys(enumOptions).length === 0) {
985
- console.log(JSON.stringify({
986
- success: false,
987
- error: 'Options must be a non-empty JSON object, e.g. \'{"Small":"sm","Medium":"md","Large":"lg"}\'.',
988
- }));
1877
+ const normalizedOptions = normalizeEnumOptionsInput(parsedOptions);
1878
+ if ("error" in normalizedOptions) {
1879
+ console.log(JSON.stringify({ success: false, error: normalizedOptions.error }));
989
1880
  process.exit(1);
990
1881
  }
1882
+ const enumOptions = normalizedOptions.options;
991
1883
  const enumId = generateUniqueId();
992
1884
  config.customTypes.push({
993
1885
  id: enumId,
@@ -1037,9 +1929,9 @@ function updateEnum(options) {
1037
1929
  enumType.name = options.name;
1038
1930
  }
1039
1931
  if (options.options !== undefined) {
1040
- let enumOptions;
1932
+ let parsedOptions;
1041
1933
  try {
1042
- enumOptions = JSON.parse(options.options);
1934
+ parsedOptions = JSON.parse(options.options);
1043
1935
  }
1044
1936
  catch {
1045
1937
  console.log(JSON.stringify({
@@ -1048,16 +1940,12 @@ function updateEnum(options) {
1048
1940
  }));
1049
1941
  process.exit(1);
1050
1942
  }
1051
- if (typeof enumOptions !== "object" ||
1052
- Array.isArray(enumOptions) ||
1053
- Object.keys(enumOptions).length === 0) {
1054
- console.log(JSON.stringify({
1055
- success: false,
1056
- error: 'Options must be a non-empty JSON object.',
1057
- }));
1943
+ const normalizedOptions = normalizeEnumOptionsInput(parsedOptions);
1944
+ if ("error" in normalizedOptions) {
1945
+ console.log(JSON.stringify({ success: false, error: normalizedOptions.error }));
1058
1946
  process.exit(1);
1059
1947
  }
1060
- enumType.enumOptions = enumOptions;
1948
+ enumType.enumOptions = normalizedOptions.options;
1061
1949
  }
1062
1950
  saveConfig(configPath, config);
1063
1951
  // Regenerate global types and all component types
@@ -1072,7 +1960,7 @@ function updateEnum(options) {
1072
1960
  enumOptions: enumType.enumOptions,
1073
1961
  }));
1074
1962
  }
1075
- function removeEnum(options) {
1963
+ async function removeEnum(options) {
1076
1964
  const { config, configPath } = loadConfig();
1077
1965
  if (!config.customTypes)
1078
1966
  config.customTypes = [];
@@ -1096,6 +1984,26 @@ function removeEnum(options) {
1096
1984
  }
1097
1985
  }
1098
1986
  }
1987
+ // A theme ENUM global variable can select from this enum too. Those live in the editor's
1988
+ // project, not in ikas.config.json, so ask the connected editor (best effort: with no dev
1989
+ // server / editor the removal proceeds and the result carries a warning). The editor itself
1990
+ // refuses to delete a type its globals reference, so this mirrors that guard for the CLI path.
1991
+ const liveGlobals = await fetchLiveThemeGlobals();
1992
+ const referencingGlobals = liveGlobals
1993
+ ? Object.values(liveGlobals).filter((s) => s.type === "ENUM" && s.enumTypeId === options.id)
1994
+ : [];
1995
+ if (referencingGlobals.length > 0) {
1996
+ const names = referencingGlobals
1997
+ .map((s) => `"${s.displayName}" (${s.name})`)
1998
+ .join(", ");
1999
+ console.log(JSON.stringify({
2000
+ success: false,
2001
+ error: `Cannot remove enum "${config.customTypes[enumIndex].name}": theme global variable(s) ${names} ` +
2002
+ `in the connected editor select from it. Change or delete those globals first ` +
2003
+ `(update-global-variable --enum-type-id / delete-global-variable).`,
2004
+ }));
2005
+ process.exit(1);
2006
+ }
1099
2007
  const removed = config.customTypes.splice(enumIndex, 1)[0];
1100
2008
  saveConfig(configPath, config);
1101
2009
  // Regenerate global types and all component types
@@ -1107,6 +2015,11 @@ function removeEnum(options) {
1107
2015
  success: true,
1108
2016
  removedEnumId: removed.id,
1109
2017
  removedEnumName: removed.name,
2018
+ ...(liveGlobals === null
2019
+ ? {
2020
+ warning: "Editor not connected — could not verify that no theme global variable selects from this enum.",
2021
+ }
2022
+ : {}),
1110
2023
  }));
1111
2024
  }
1112
2025
  function listEnums() {
@@ -1126,8 +2039,7 @@ function listEnums() {
1126
2039
  }
1127
2040
  // --- Command registration ---
1128
2041
  export function createConfigCommand() {
1129
- const config = new Command("config")
1130
- .description("Non-interactive commands for managing ikas.config.json (designed for AI agents and scripting)");
2042
+ const config = new Command("config").description("Non-interactive commands for managing ikas.config.json (designed for AI agents and scripting)");
1131
2043
  config
1132
2044
  .command("add-component")
1133
2045
  .description("Add a new component to the project")
@@ -1136,7 +2048,7 @@ export function createConfigCommand() {
1136
2048
  .option("--isHeader", "Mark this section as the store header (only for type: section)")
1137
2049
  .option("--isFooter", "Mark this section as the store footer (only for type: section)")
1138
2050
  .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. " +
1139
- "Example: '[{\"name\":\"title\",\"type\":\"TEXT\",\"required\":true,\"defaultValue\":\"Hello\",\"groupId\":\"basic\"}]'")
2051
+ 'Example: \'[{"name":"title","type":"TEXT","required":true,"defaultValue":"Hello","groupId":"basic"}]\'')
1140
2052
  .action((options) => {
1141
2053
  addComponent(options.name, options);
1142
2054
  });
@@ -1155,7 +2067,11 @@ export function createConfigCommand() {
1155
2067
  .option("--typeId <typeId>", "Type ID for TYPE props (required when type is TYPE)")
1156
2068
  .option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (required when type is ENUM)")
1157
2069
  .option("--filteredComponentIds <json>", "JSON array of component IDs to restrict selection (for COMPONENT/COMPONENT_LIST)")
1158
- .option("--privateVarMap <json>", 'JSON object mapping variable keys to {id, typeId} (for COMPONENT/COMPONENT_LIST)')
2070
+ .option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (for COMPONENT/COMPONENT_LIST)")
2071
+ .option("--numberRangeData <json>", 'JSON slider config for NUMBER_RANGE props, e.g. \'{"min":0,"max":100,"interval":5,"unit":"px"}\'')
2072
+ .option("--requiredProp <propName>", 'Show this only while the named BOOLEAN or ENUM prop satisfies the condition ("Koşullu Prop")')
2073
+ .option("--requiredPropValue <boolean>", "For a BOOLEAN --requiredProp: which state shows it, true (default) or false")
2074
+ .option("--requiredEnumOptionValue <value>", "For an ENUM --requiredProp: the option value that shows it")
1159
2075
  .action((options) => {
1160
2076
  addProp({ id: options.componentId, name: options.component }, options);
1161
2077
  });
@@ -1175,6 +2091,10 @@ export function createConfigCommand() {
1175
2091
  .option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (use 'none' to clear)")
1176
2092
  .option("--filteredComponentIds <json>", "JSON array of component IDs (use 'none' to clear)")
1177
2093
  .option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (use 'none' to clear)")
2094
+ .option("--numberRangeData <json>", "JSON slider config for NUMBER_RANGE props (use 'none' to clear)")
2095
+ .option("--requiredProp <propName>", "BOOLEAN or ENUM prop that gates this prop in the editor panel (use 'none' to clear)")
2096
+ .option("--requiredPropValue <boolean>", "For a BOOLEAN --requiredProp: which state shows it, true (default) or false")
2097
+ .option("--requiredEnumOptionValue <value>", "For an ENUM --requiredProp: the option value that shows it")
1178
2098
  .action((options) => {
1179
2099
  updateProp({ id: options.componentId, name: options.component }, options);
1180
2100
  });
@@ -1184,6 +2104,7 @@ export function createConfigCommand() {
1184
2104
  .option("--component-id <id>", "Component id (preferred; from `config list-components`)")
1185
2105
  .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1186
2106
  .requiredOption("--prop <propName>", "Prop name to remove")
2107
+ .option("--yes", "Confirm the removal. Required: the command refuses to run without it.")
1187
2108
  .action((options) => {
1188
2109
  removeProp({ id: options.componentId, name: options.component }, options);
1189
2110
  });
@@ -1199,13 +2120,48 @@ export function createConfigCommand() {
1199
2120
  .action((options) => {
1200
2121
  updateComponent({ id: options.id, name: options.name }, options);
1201
2122
  });
2123
+ // The project's UI texts (`src/i18n/default.json` + a `<locale>.json` per language)
2124
+ const textsCommand = (run) => {
2125
+ const { config: projectConfig, configPath } = loadConfig();
2126
+ const result = run(projectConfig, path.dirname(configPath));
2127
+ if (result.configChanged)
2128
+ saveConfig(configPath, projectConfig);
2129
+ console.log(JSON.stringify(result, null, 2));
2130
+ if (!result.success)
2131
+ process.exit(1);
2132
+ };
2133
+ config
2134
+ .command("set-text")
2135
+ .description("Add or update a UI text of the project (a fixed text of the components' UI, read with t(key, params) imported from @ikas/bp-storefront). " +
2136
+ "Keys are dotted groups: common.addToCart. Without --locale the base text (default.json); with --locale a language's. {{param}} placeholders must match the base text's.")
2137
+ .requiredOption("--key <key>", "Dotted key, e.g. common.addToCart")
2138
+ .requiredOption("--text <text>", 'The text, e.g. "{{count}} adet kaldı"')
2139
+ .option("--locale <code>", "A language code (en, de, ...): set that language's text instead of the base text")
2140
+ .action((options) => textsCommand((projectConfig, root) => setText(projectConfig, options, root)));
2141
+ config
2142
+ .command("remove-text")
2143
+ .description("Remove a UI text: from the base and every language, or with --locale only that language's text")
2144
+ .requiredOption("--key <key>", "Dotted key")
2145
+ .option("--locale <code>", "Remove only this language's text")
2146
+ .action((options) => textsCommand((projectConfig, root) => removeText(projectConfig, options, root)));
2147
+ config
2148
+ .command("rename-text")
2149
+ .description("Rename a UI text, or a group with every text under it (product → item), in the base and every language. Change the t() calls after: the type check reports the old keys.")
2150
+ .requiredOption("--key <key>", "Dotted key or group, e.g. common.addToCart")
2151
+ .requiredOption("--to <key>", "The new dotted key or group")
2152
+ .action((options) => textsCommand((projectConfig, root) => renameText(projectConfig, options, root)));
2153
+ config
2154
+ .command("list-texts")
2155
+ .description("List the project's UI texts: the base texts, each language's, and the keys a language lacks")
2156
+ .action(() => textsCommand((projectConfig, root) => listTexts(projectConfig, root)));
1202
2157
  config
1203
2158
  .command("remove-component")
1204
2159
  .description("Remove a component from the project (deletes files). Identify by --id (preferred) or exact --name.")
1205
2160
  .option("--id <id>", "Component id (preferred; from `config list-components`)")
1206
2161
  .option("--name <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
2162
+ .option("--yes", "Confirm the removal. Required: the command refuses to run without it.")
1207
2163
  .action((options) => {
1208
- removeComponent({ id: options.id, name: options.name });
2164
+ removeComponent({ id: options.id, name: options.name }, options.yes);
1209
2165
  });
1210
2166
  config
1211
2167
  .command("add-prop-group")
@@ -1216,6 +2172,9 @@ export function createConfigCommand() {
1216
2172
  .requiredOption("--name <name>", "Display name in editor")
1217
2173
  .option("--description <description>", "Group description")
1218
2174
  .option("--parent <parentId>", "Parent group ID (for nesting, max 1 level)")
2175
+ .option("--requiredProp <propName>", 'Show this only while the named BOOLEAN or ENUM prop satisfies the condition ("Koşullu Prop")')
2176
+ .option("--requiredPropValue <boolean>", "For a BOOLEAN --requiredProp: which state shows it, true (default) or false")
2177
+ .option("--requiredEnumOptionValue <value>", "For an ENUM --requiredProp: the option value that shows it")
1219
2178
  .action((options) => {
1220
2179
  addPropGroup({ id: options.componentId, name: options.component }, options);
1221
2180
  });
@@ -1227,6 +2186,9 @@ export function createConfigCommand() {
1227
2186
  .requiredOption("--id <id>", "Group ID to update")
1228
2187
  .option("--name <name>", "New display name")
1229
2188
  .option("--description <description>", "New description")
2189
+ .option("--requiredProp <propName>", "BOOLEAN or ENUM prop that gates this group in the editor panel (use 'none' to clear)")
2190
+ .option("--requiredPropValue <boolean>", "For a BOOLEAN --requiredProp: which state shows it, true (default) or false")
2191
+ .option("--requiredEnumOptionValue <value>", "For an ENUM --requiredProp: the option value that shows it")
1230
2192
  .action((options) => {
1231
2193
  updatePropGroup({ id: options.componentId, name: options.component }, options);
1232
2194
  });
@@ -1246,7 +2208,7 @@ export function createConfigCommand() {
1246
2208
  .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1247
2209
  .requiredOption("--id <id>", "Group ID to move")
1248
2210
  .option("--parent <parentId>", "Target parent group ID (omit to move to root)")
1249
- .option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", v => parseInt(v, 10))
2211
+ .option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", (v) => parseInt(v, 10))
1250
2212
  .action((options) => {
1251
2213
  movePropGroup({ id: options.componentId, name: options.component }, options);
1252
2214
  });
@@ -1273,7 +2235,7 @@ export function createConfigCommand() {
1273
2235
  .command("add-enum")
1274
2236
  .description("Create a custom enum type (offline, no editor needed)")
1275
2237
  .requiredOption("--name <name>", "Enum name (e.g. Size, AspectRatio)")
1276
- .requiredOption("--options <json>", 'JSON object of label→value pairs, e.g. \'{"Small":"sm","Medium":"md","Large":"lg"}\'')
2238
+ .requiredOption("--options <json>", 'JSON object of label→value pairs, e.g. \'{"Small":"sm","Medium":"md","Large":"lg"}\' (an array \'[{"name":"Small","value":"sm"}]\' is also accepted)')
1277
2239
  .action(async (options) => {
1278
2240
  await addEnum(options);
1279
2241
  });
@@ -1282,16 +2244,16 @@ export function createConfigCommand() {
1282
2244
  .description("Update a custom enum type")
1283
2245
  .requiredOption("--id <id>", "Enum ID to update")
1284
2246
  .option("--name <name>", "New enum name")
1285
- .option("--options <json>", "New JSON object of label→value pairs")
2247
+ .option("--options <json>", "New JSON object of label→value pairs (an array of { name, value } is also accepted)")
1286
2248
  .action((options) => {
1287
2249
  updateEnum(options);
1288
2250
  });
1289
2251
  config
1290
2252
  .command("remove-enum")
1291
- .description("Remove a custom enum type (fails if any prop references it)")
2253
+ .description("Remove a custom enum type (fails if any prop references it, or — while the editor is connected — if a theme ENUM global variable selects from it)")
1292
2254
  .requiredOption("--id <id>", "Enum ID to remove")
1293
- .action((options) => {
1294
- removeEnum(options);
2255
+ .action(async (options) => {
2256
+ await removeEnum(options);
1295
2257
  });
1296
2258
  config
1297
2259
  .command("list-enums")
@@ -1306,54 +2268,15 @@ export function createConfigCommand() {
1306
2268
  * Returns the matching type (with enumOptions) if found, null otherwise.
1307
2269
  */
1308
2270
  async function fetchLiveEditorEnumById(enumId) {
1309
- const { WebSocket } = await import("ws");
1310
- const WS_URL = "ws://localhost:5201";
1311
- const TIMEOUT_MS = 1500;
1312
- return new Promise((resolve) => {
1313
- let ws;
1314
- let settled = false;
1315
- const finish = (value) => {
1316
- if (settled)
1317
- return;
1318
- settled = true;
1319
- try {
1320
- ws?.close();
1321
- }
1322
- catch {
1323
- /* ignore */
1324
- }
1325
- resolve(value);
1326
- };
1327
- try {
1328
- ws = new WebSocket(WS_URL);
2271
+ const types = await fetchLiveEditorTypes();
2272
+ const match = types?.find((t) => t.category === "enum" && t.id === enumId);
2273
+ return match
2274
+ ? {
2275
+ id: match.id,
2276
+ name: match.name,
2277
+ ...(match.enumOptions ? { enumOptions: match.enumOptions } : {}),
1329
2278
  }
1330
- catch {
1331
- resolve(null);
1332
- return;
1333
- }
1334
- const timeout = setTimeout(() => finish(null), TIMEOUT_MS);
1335
- ws.on("open", () => {
1336
- ws.send(JSON.stringify({ type: "request-types" }));
1337
- });
1338
- ws.on("message", (data) => {
1339
- try {
1340
- const message = JSON.parse(data.toString());
1341
- if (message.type === "types-list") {
1342
- clearTimeout(timeout);
1343
- const types = message.payload?.types || [];
1344
- const match = types.find((t) => t.category === "enum" && t.id === enumId);
1345
- finish(match ? { id: match.id, name: match.name, ...(match.enumOptions ? { enumOptions: match.enumOptions } : {}) } : null);
1346
- }
1347
- }
1348
- catch {
1349
- // Ignore non-JSON messages
1350
- }
1351
- });
1352
- ws.on("error", () => {
1353
- clearTimeout(timeout);
1354
- finish(null);
1355
- });
1356
- });
2279
+ : null;
1357
2280
  }
1358
2281
  /**
1359
2282
  * Best-effort lookup of an enum by PascalCase name in the live editor session.
@@ -1363,8 +2286,37 @@ async function fetchLiveEditorEnumById(enumId) {
1363
2286
  * must treat null as "could not verify, fall back to on-disk check only".
1364
2287
  */
1365
2288
  async function fetchLiveEditorEnumByName(pascalName) {
2289
+ const types = await fetchLiveEditorTypes();
2290
+ const match = types?.find((t) => t.category === "enum" && toPascalCase(t.name) === pascalName);
2291
+ return match ? { id: match.id, name: match.name } : null;
2292
+ }
2293
+ /**
2294
+ * Best-effort read of the connected editor's theme global variables (the same editor action
2295
+ * `list-theme-globals` runs). Returns null on any failure — no dev server, no editor connected,
2296
+ * timeout — and callers must treat null as "could not verify, allow".
2297
+ */
2298
+ async function fetchLiveThemeGlobals() {
2299
+ try {
2300
+ const result = await runEditorAction("list-theme-globals", {}, { timeoutMs: 1500 });
2301
+ if (!result.success)
2302
+ return null;
2303
+ const settings = result.payload?.settings;
2304
+ return settings && typeof settings === "object" ? settings : null;
2305
+ }
2306
+ catch {
2307
+ return null;
2308
+ }
2309
+ }
2310
+ /**
2311
+ * Best-effort fetch of the editor's synced type list (with category /
2312
+ * sectionAllowed / enumOptions metadata). Returns null on any failure (no dev
2313
+ * server, timeout) — callers must treat null as "could not verify, allow".
2314
+ */
2315
+ async function fetchLiveEditorTypes() {
1366
2316
  const { WebSocket } = await import("ws");
1367
- const WS_URL = "ws://localhost:5201";
2317
+ // `client=control` keeps this short-lived socket out of the dev server's
2318
+ // `editor-action` target pool — it only reads types, it can't apply commands.
2319
+ const WS_URL = "ws://localhost:5201/?client=control";
1368
2320
  const TIMEOUT_MS = 1500;
1369
2321
  return new Promise((resolve) => {
1370
2322
  let ws;
@@ -1397,9 +2349,7 @@ async function fetchLiveEditorEnumByName(pascalName) {
1397
2349
  const message = JSON.parse(data.toString());
1398
2350
  if (message.type === "types-list") {
1399
2351
  clearTimeout(timeout);
1400
- const types = message.payload?.types || [];
1401
- const match = types.find((t) => t.category === "enum" && toPascalCase(t.name) === pascalName);
1402
- finish(match ? { id: match.id, name: match.name } : null);
2352
+ finish(message.payload?.types || []);
1403
2353
  }
1404
2354
  }
1405
2355
  catch {
@@ -1412,9 +2362,52 @@ async function fetchLiveEditorEnumByName(pascalName) {
1412
2362
  });
1413
2363
  });
1414
2364
  }
2365
+ /**
2366
+ * Definition-time guard for TYPE-prop typeIds, verified against the live
2367
+ * editor's type list (skipped when the dev server is unreachable or the type
2368
+ * is unknown to the editor). Two rules:
2369
+ * - deprecated types are rejected on ANY component: they are legacy
2370
+ * render-only types — old themes may still carry values, but new props must
2371
+ * not be defined with them.
2372
+ * - on section components, only section-allowed (style) types pass — any other
2373
+ * type renders an empty placeholder merchants cannot fill, and section-level
2374
+ * bindings cannot supply a value either, so the prop would be dead weight.
2375
+ * On violation prints the standard { success: false } envelope and exits.
2376
+ */
2377
+ async function assertTypeIdsUsable(componentType, typeIds) {
2378
+ if (typeIds.length === 0)
2379
+ return;
2380
+ const types = await fetchLiveEditorTypes();
2381
+ if (!types)
2382
+ return;
2383
+ const matches = typeIds.flatMap((typeId) => {
2384
+ const match = types.find((t) => t.id === typeId);
2385
+ return match ? [match] : [];
2386
+ });
2387
+ const legacy = matches.find((t) => t.deprecated === true);
2388
+ if (legacy) {
2389
+ console.log(JSON.stringify({
2390
+ success: false,
2391
+ error: `Type "${legacy.id}" is a legacy render-only type — values of it may still exist in old themes, but NEW props must not use it. Pick a current style type instead (run "config list-types" to see them).`,
2392
+ }));
2393
+ process.exit(1);
2394
+ }
2395
+ if (componentType !== "section")
2396
+ return;
2397
+ const disallowed = matches.find((t) => t.sectionAllowed === false);
2398
+ if (!disallowed)
2399
+ return;
2400
+ console.log(JSON.stringify({
2401
+ success: false,
2402
+ error: `Type "${disallowed.id}" is not allowed for TYPE props on section components — merchants cannot fill it in the editor (sections only support style types). Run "config list-types --component-type section" to see allowed types, or use a dedicated prop type (e.g. PRODUCT, CATEGORY) for domain data.`,
2403
+ }));
2404
+ process.exit(1);
2405
+ }
1415
2406
  async function listTypes(componentType) {
1416
2407
  const { WebSocket } = await import("ws");
1417
- const WS_URL = "ws://localhost:5201";
2408
+ // `client=control` keeps this short-lived socket out of the dev server's
2409
+ // `editor-action` target pool — it only reads types, it can't apply commands.
2410
+ const WS_URL = "ws://localhost:5201/?client=control";
1418
2411
  const TIMEOUT_MS = 5000;
1419
2412
  await new Promise((resolve) => {
1420
2413
  let ws;
@@ -1424,7 +2417,7 @@ async function listTypes(componentType) {
1424
2417
  catch {
1425
2418
  console.log(JSON.stringify({
1426
2419
  success: false,
1427
- error: "Failed to connect to dev server. Make sure 'ikas-component dev' is running.",
2420
+ error: "Failed to connect to dev server. Make sure 'ikas theme dev' is running.",
1428
2421
  }));
1429
2422
  process.exit(1);
1430
2423
  return;
@@ -1445,7 +2438,8 @@ async function listTypes(componentType) {
1445
2438
  const message = JSON.parse(data.toString());
1446
2439
  if (message.type === "types-list") {
1447
2440
  clearTimeout(timeout);
1448
- let types = message.payload.types;
2441
+ // Legacy render-only types are never offered for new props.
2442
+ let types = message.payload.types.filter((t) => !t.deprecated);
1449
2443
  if (componentType === "section") {
1450
2444
  types = types.filter((t) => t.sectionAllowed);
1451
2445
  }
@@ -1465,7 +2459,7 @@ async function listTypes(componentType) {
1465
2459
  clearTimeout(timeout);
1466
2460
  console.log(JSON.stringify({
1467
2461
  success: false,
1468
- error: "Failed to connect to dev server. Make sure 'ikas-component dev' is running.",
2462
+ error: "Failed to connect to dev server. Make sure 'ikas theme dev' is running.",
1469
2463
  }));
1470
2464
  process.exit(1);
1471
2465
  });