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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/dist/commands/add-sections-to-page.d.ts +3 -0
  2. package/dist/commands/add-sections-to-page.d.ts.map +1 -0
  3. package/dist/commands/add-sections-to-page.js +39 -0
  4. package/dist/commands/add-sections-to-page.js.map +1 -0
  5. package/dist/commands/add-to-page.d.ts +3 -0
  6. package/dist/commands/add-to-page.d.ts.map +1 -0
  7. package/dist/commands/add-to-page.js +41 -0
  8. package/dist/commands/add-to-page.js.map +1 -0
  9. package/dist/commands/add.d.ts.map +1 -1
  10. package/dist/commands/add.js +10 -1
  11. package/dist/commands/add.js.map +1 -1
  12. package/dist/commands/build.d.ts.map +1 -1
  13. package/dist/commands/build.js +5 -165
  14. package/dist/commands/build.js.map +1 -1
  15. package/dist/commands/config.d.ts.map +1 -1
  16. package/dist/commands/config.js +330 -44
  17. package/dist/commands/config.js.map +1 -1
  18. package/dist/commands/create-design-tokens.d.ts +24 -0
  19. package/dist/commands/create-design-tokens.d.ts.map +1 -0
  20. package/dist/commands/create-design-tokens.js +317 -0
  21. package/dist/commands/create-design-tokens.js.map +1 -0
  22. package/dist/commands/create-global-variable.d.ts +3 -0
  23. package/dist/commands/create-global-variable.d.ts.map +1 -0
  24. package/dist/commands/create-global-variable.js +53 -0
  25. package/dist/commands/create-global-variable.js.map +1 -0
  26. package/dist/commands/create-page.d.ts +3 -0
  27. package/dist/commands/create-page.d.ts.map +1 -0
  28. package/dist/commands/create-page.js +31 -0
  29. package/dist/commands/create-page.js.map +1 -0
  30. package/dist/commands/delete-theme-globals.d.ts +4 -0
  31. package/dist/commands/delete-theme-globals.d.ts.map +1 -0
  32. package/dist/commands/delete-theme-globals.js +48 -0
  33. package/dist/commands/delete-theme-globals.js.map +1 -0
  34. package/dist/commands/dev.d.ts.map +1 -1
  35. package/dist/commands/dev.js +88 -13
  36. package/dist/commands/dev.js.map +1 -1
  37. package/dist/commands/get-component-props.d.ts +3 -0
  38. package/dist/commands/get-component-props.d.ts.map +1 -0
  39. package/dist/commands/get-component-props.js +32 -0
  40. package/dist/commands/get-component-props.js.map +1 -0
  41. package/dist/commands/get-page-by-type.d.ts +3 -0
  42. package/dist/commands/get-page-by-type.d.ts.map +1 -0
  43. package/dist/commands/get-page-by-type.js +25 -0
  44. package/dist/commands/get-page-by-type.js.map +1 -0
  45. package/dist/commands/get-section-values.d.ts +3 -0
  46. package/dist/commands/get-section-values.d.ts.map +1 -0
  47. package/dist/commands/get-section-values.js +39 -0
  48. package/dist/commands/get-section-values.js.map +1 -0
  49. package/dist/commands/import.d.ts +3 -0
  50. package/dist/commands/import.d.ts.map +1 -0
  51. package/dist/commands/import.js +25 -0
  52. package/dist/commands/import.js.map +1 -0
  53. package/dist/commands/list-entities.d.ts +3 -0
  54. package/dist/commands/list-entities.d.ts.map +1 -0
  55. package/dist/commands/list-entities.js +32 -0
  56. package/dist/commands/list-entities.js.map +1 -0
  57. package/dist/commands/list-imported.d.ts +3 -0
  58. package/dist/commands/list-imported.d.ts.map +1 -0
  59. package/dist/commands/list-imported.js +25 -0
  60. package/dist/commands/list-imported.js.map +1 -0
  61. package/dist/commands/list-page-sections.d.ts +3 -0
  62. package/dist/commands/list-page-sections.d.ts.map +1 -0
  63. package/dist/commands/list-page-sections.js +25 -0
  64. package/dist/commands/list-page-sections.js.map +1 -0
  65. package/dist/commands/list-pages.d.ts +3 -0
  66. package/dist/commands/list-pages.d.ts.map +1 -0
  67. package/dist/commands/list-pages.js +21 -0
  68. package/dist/commands/list-pages.js.map +1 -0
  69. package/dist/commands/list-theme-globals.d.ts +3 -0
  70. package/dist/commands/list-theme-globals.d.ts.map +1 -0
  71. package/dist/commands/list-theme-globals.js +22 -0
  72. package/dist/commands/list-theme-globals.js.map +1 -0
  73. package/dist/commands/publish-theme.d.ts +3 -0
  74. package/dist/commands/publish-theme.d.ts.map +1 -0
  75. package/dist/commands/publish-theme.js +29 -0
  76. package/dist/commands/publish-theme.js.map +1 -0
  77. package/dist/commands/publish.d.ts.map +1 -1
  78. package/dist/commands/publish.js +23 -2
  79. package/dist/commands/publish.js.map +1 -1
  80. package/dist/commands/search-products.d.ts +3 -0
  81. package/dist/commands/search-products.d.ts.map +1 -0
  82. package/dist/commands/search-products.js +40 -0
  83. package/dist/commands/search-products.js.map +1 -0
  84. package/dist/commands/update-global-variable.d.ts +3 -0
  85. package/dist/commands/update-global-variable.d.ts.map +1 -0
  86. package/dist/commands/update-global-variable.js +47 -0
  87. package/dist/commands/update-global-variable.js.map +1 -0
  88. package/dist/commands/update-page-sections.d.ts +3 -0
  89. package/dist/commands/update-page-sections.d.ts.map +1 -0
  90. package/dist/commands/update-page-sections.js +39 -0
  91. package/dist/commands/update-page-sections.js.map +1 -0
  92. package/dist/commands/update-section-prop.d.ts +3 -0
  93. package/dist/commands/update-section-prop.d.ts.map +1 -0
  94. package/dist/commands/update-section-prop.js +59 -0
  95. package/dist/commands/update-section-prop.js.map +1 -0
  96. package/dist/commands/upload-image.d.ts +3 -0
  97. package/dist/commands/upload-image.d.ts.map +1 -0
  98. package/dist/commands/upload-image.js +38 -0
  99. package/dist/commands/upload-image.js.map +1 -0
  100. package/dist/commands/upload-images.d.ts +3 -0
  101. package/dist/commands/upload-images.d.ts.map +1 -0
  102. package/dist/commands/upload-images.js +48 -0
  103. package/dist/commands/upload-images.js.map +1 -0
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +55 -0
  106. package/dist/index.js.map +1 -1
  107. package/dist/types.d.ts +11 -1
  108. package/dist/types.d.ts.map +1 -1
  109. package/dist/utils/compile.d.ts +4 -1
  110. package/dist/utils/compile.d.ts.map +1 -1
  111. package/dist/utils/compile.js +630 -102
  112. package/dist/utils/compile.js.map +1 -1
  113. package/dist/utils/component-helpers.d.ts +51 -3
  114. package/dist/utils/component-helpers.d.ts.map +1 -1
  115. package/dist/utils/component-helpers.js +139 -4
  116. package/dist/utils/component-helpers.js.map +1 -1
  117. package/dist/utils/editor-action-client.d.ts +28 -0
  118. package/dist/utils/editor-action-client.d.ts.map +1 -0
  119. package/dist/utils/editor-action-client.js +116 -0
  120. package/dist/utils/editor-action-client.js.map +1 -0
  121. package/dist/utils/forbid-deep-imports.d.ts +3 -0
  122. package/dist/utils/forbid-deep-imports.d.ts.map +1 -0
  123. package/dist/utils/forbid-deep-imports.js +54 -0
  124. package/dist/utils/forbid-deep-imports.js.map +1 -0
  125. package/dist/utils/forbid-external-packages.d.ts +16 -0
  126. package/dist/utils/forbid-external-packages.d.ts.map +1 -0
  127. package/dist/utils/forbid-external-packages.js +89 -0
  128. package/dist/utils/forbid-external-packages.js.map +1 -0
  129. package/dist/utils/load-image.d.ts +16 -0
  130. package/dist/utils/load-image.d.ts.map +1 -0
  131. package/dist/utils/load-image.js +50 -0
  132. package/dist/utils/load-image.js.map +1 -0
  133. package/dist/utils/websocket-server.d.ts +40 -2
  134. package/dist/utils/websocket-server.d.ts.map +1 -1
  135. package/dist/utils/websocket-server.js +59 -1
  136. package/dist/utils/websocket-server.js.map +1 -1
  137. package/package.json +1 -1
  138. package/templates/add/section.css +11 -0
  139. package/dist/commands/create.d.ts +0 -9
  140. package/dist/commands/create.d.ts.map +0 -1
  141. package/dist/commands/create.js +0 -9
  142. package/dist/commands/create.js.map +0 -1
  143. package/dist/commands/proxy.d.ts +0 -39
  144. package/dist/commands/proxy.d.ts.map +0 -1
  145. package/dist/commands/proxy.js +0 -212
  146. package/dist/commands/proxy.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
- import { PROP_TYPES, toPascalCase, generateTypesFile, generateGlobalTypesFile, collectUsedEnumIds, generateComponentFile, generateStylesFile, generateProjectId, generateComponentId, generateUniqueId, updateBarrelExport, findPropGroup, collectPropGroupIds, movePropGroupInTree, validateFilteredComponentIds, } from "../utils/component-helpers.js";
4
+ import { PROP_TYPES, toPascalCase, generateTypesFile, generateGlobalTypesFile, collectUsedEnumIds, generateComponentFile, generateStylesFile, generateProjectId, isValidProjectId, generateComponentId, generateUniqueId, updateBarrelExport, findPropGroup, collectPropGroupIds, movePropGroupInTree, validateFilteredComponentIds, } from "../utils/component-helpers.js";
5
5
  function loadConfig() {
6
6
  const configPath = path.resolve(process.cwd(), "ikas.config.json");
7
7
  if (!fs.existsSync(configPath)) {
@@ -31,7 +31,10 @@ function resolveComponent(config, ref) {
31
31
  success: false,
32
32
  error: "Component reference required: pass --id (preferred) or --name (exact PascalCase). " +
33
33
  "Run `ikas-component config list-components` to see canonical ids.",
34
- validComponents: config.components.map((c) => ({ id: c.id, name: c.name })),
34
+ validComponents: config.components.map((c) => ({
35
+ id: c.id,
36
+ name: c.name,
37
+ })),
35
38
  }));
36
39
  process.exit(1);
37
40
  }
@@ -52,7 +55,10 @@ function resolveComponent(config, ref) {
52
55
  ? `Component not found: id="${id}".`
53
56
  : `Component not found: name="${name}". Names are matched exactly (PascalCase, no normalization).`,
54
57
  lookup: id ? { id } : { name },
55
- validComponents: config.components.map((c) => ({ id: c.id, name: c.name })),
58
+ validComponents: config.components.map((c) => ({
59
+ id: c.id,
60
+ name: c.name,
61
+ })),
56
62
  hint: "Use --id (preferred) or exact PascalCase --name. " +
57
63
  "Run `ikas-component config list-components` to see canonical ids.",
58
64
  }));
@@ -144,6 +150,19 @@ function camelCaseToDisplayName(name) {
144
150
  .replace(/([a-z])([A-Z])/g, "$1 $2")
145
151
  .replace(/^./, (c) => c.toUpperCase());
146
152
  }
153
+ const ALLOWED_PROP_FIELDS = new Set([
154
+ "name",
155
+ "displayName",
156
+ "type",
157
+ "required",
158
+ "description",
159
+ "defaultValue",
160
+ "groupId",
161
+ "typeId",
162
+ "enumTypeId",
163
+ "filteredComponentIds",
164
+ "privateVarMap",
165
+ ]);
147
166
  /**
148
167
  * Parse and validate the --props JSON flag for add-component.
149
168
  * Returns validated ComponentProp[] or exits with error.
@@ -171,6 +190,16 @@ async function parsePropsFlag(propsJson, config, configPath) {
171
190
  const props = [];
172
191
  for (const raw of rawProps) {
173
192
  const r = raw;
193
+ const unknownFields = Object.keys(r).filter((k) => !ALLOWED_PROP_FIELDS.has(k));
194
+ if (unknownFields.length > 0) {
195
+ console.log(JSON.stringify({
196
+ success: false,
197
+ error: `Unknown field(s) in --props entry: ${unknownFields.join(", ")}. ` +
198
+ `Allowed: ${[...ALLOWED_PROP_FIELDS].join(", ")}. ` +
199
+ `Note: use "groupId" (not "group") and "defaultValue" (not "default") in --props JSON.`,
200
+ }));
201
+ process.exit(1);
202
+ }
174
203
  if (!r.name || typeof r.name !== "string") {
175
204
  console.log(JSON.stringify({
176
205
  success: false,
@@ -211,6 +240,24 @@ async function parsePropsFlag(propsJson, config, configPath) {
211
240
  }));
212
241
  process.exit(1);
213
242
  }
243
+ // Validate LINK / LIST_OF_LINK defaultValue shape (reject JSON strings and legacy { href })
244
+ if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
245
+ r.defaultValue !== undefined) {
246
+ const linkError = validateLinkDefaultValue(propType, r.defaultValue, r.name);
247
+ if (linkError) {
248
+ console.log(JSON.stringify({ success: false, error: linkError }));
249
+ process.exit(1);
250
+ }
251
+ }
252
+ // Validate SVG / SVG_LIST defaultValue (reject malformed/oversized/unsafe SVG)
253
+ if ((propType === "SVG" || propType === "SVG_LIST") &&
254
+ r.defaultValue !== undefined) {
255
+ const svgError = validateSvgDefaultValue(propType, r.defaultValue, r.name);
256
+ if (svgError) {
257
+ console.log(JSON.stringify({ success: false, error: svgError }));
258
+ process.exit(1);
259
+ }
260
+ }
214
261
  // Auto-generate displayName if omitted
215
262
  const displayName = typeof r.displayName === "string" && r.displayName
216
263
  ? r.displayName
@@ -235,7 +282,10 @@ async function parsePropsFlag(propsJson, config, configPath) {
235
282
  process.exit(1);
236
283
  }
237
284
  // Validate enum type exists (checks config then live dev server)
238
- if (config && configPath && typeof r.enumTypeId === "string" && !r.enumTypeId.startsWith("@ikas/")) {
285
+ if (config &&
286
+ configPath &&
287
+ typeof r.enumTypeId === "string" &&
288
+ !r.enumTypeId.startsWith("@ikas/")) {
239
289
  const resolved = await resolveEnumType(r.enumTypeId, config, configPath);
240
290
  if (!resolved) {
241
291
  console.log(JSON.stringify({
@@ -254,9 +304,7 @@ async function parsePropsFlag(propsJson, config, configPath) {
254
304
  ...(typeof r.description === "string" && r.description
255
305
  ? { description: r.description }
256
306
  : {}),
257
- ...(r.defaultValue !== undefined
258
- ? { defaultValue: r.defaultValue }
259
- : {}),
307
+ ...(r.defaultValue !== undefined ? { defaultValue: r.defaultValue } : {}),
260
308
  ...(typeof r.groupId === "string" && r.groupId
261
309
  ? { groupId: r.groupId }
262
310
  : {}),
@@ -266,11 +314,19 @@ async function parsePropsFlag(propsJson, config, configPath) {
266
314
  ...(propType === "ENUM" && typeof r.enumTypeId === "string"
267
315
  ? { enumTypeId: r.enumTypeId }
268
316
  : {}),
269
- ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") && r.filteredComponentIds && Array.isArray(r.filteredComponentIds)
270
- ? { filteredComponentIds: assertKnownComponentIds(r.filteredComponentIds, config) }
317
+ ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
318
+ r.filteredComponentIds &&
319
+ Array.isArray(r.filteredComponentIds)
320
+ ? {
321
+ filteredComponentIds: assertKnownComponentIds(r.filteredComponentIds, config),
322
+ }
271
323
  : {}),
272
- ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") && r.privateVarMap && typeof r.privateVarMap === "object"
273
- ? { privateVarMap: r.privateVarMap }
324
+ ...((propType === "COMPONENT" || propType === "COMPONENT_LIST") &&
325
+ r.privateVarMap &&
326
+ typeof r.privateVarMap === "object"
327
+ ? {
328
+ privateVarMap: r.privateVarMap,
329
+ }
274
330
  : {}),
275
331
  };
276
332
  props.push(prop);
@@ -302,6 +358,18 @@ async function addComponent(name, options) {
302
358
  if (!config.projectId) {
303
359
  config.projectId = generateProjectId();
304
360
  }
361
+ else if (!isValidProjectId(config.projectId)) {
362
+ // A malformed projectId (hand-edited config) breaks the editor's republish
363
+ // stability: published/local id prefixes stop matching and every publish
364
+ // INSERTs duplicate design elements. Fail fast instead of corrupting data.
365
+ console.log(JSON.stringify({
366
+ success: false,
367
+ error: `Invalid projectId "${config.projectId}" in ikas.config.json. ` +
368
+ "projectId must be at least 6 alphanumeric characters (no '-' or '_'). " +
369
+ "Restore the original generated value — do not hand-edit it.",
370
+ }));
371
+ process.exit(1);
372
+ }
305
373
  // Parse --props (and validate filteredComponentIds) BEFORE any filesystem side-effects
306
374
  // so a bad payload doesn't leave an empty component directory behind.
307
375
  const props = options.props
@@ -407,7 +475,9 @@ async function addProp(ref, options) {
407
475
  }
408
476
  // Validate that enumTypeId references an existing enum type
409
477
  // (checks config.customTypes first, then falls back to the live dev server)
410
- if (propType === "ENUM" && options.enumTypeId && !options.enumTypeId.startsWith("@ikas/")) {
478
+ if (propType === "ENUM" &&
479
+ options.enumTypeId &&
480
+ !options.enumTypeId.startsWith("@ikas/")) {
411
481
  const resolved = await resolveEnumType(options.enumTypeId, config, configPath);
412
482
  if (!resolved) {
413
483
  console.log(JSON.stringify({
@@ -437,32 +507,66 @@ async function addProp(ref, options) {
437
507
  }
438
508
  // Parse filteredComponentIds JSON for COMPONENT/COMPONENT_LIST
439
509
  let parsedFilteredIds;
440
- if (options.filteredComponentIds && (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
510
+ if (options.filteredComponentIds &&
511
+ (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
441
512
  try {
442
513
  parsedFilteredIds = JSON.parse(options.filteredComponentIds);
443
514
  if (!Array.isArray(parsedFilteredIds)) {
444
- console.log(JSON.stringify({ success: false, error: "--filteredComponentIds must be a JSON array of strings." }));
515
+ console.log(JSON.stringify({
516
+ success: false,
517
+ error: "--filteredComponentIds must be a JSON array of strings.",
518
+ }));
445
519
  process.exit(1);
446
520
  }
447
521
  }
448
522
  catch {
449
- console.log(JSON.stringify({ success: false, error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}` }));
523
+ console.log(JSON.stringify({
524
+ success: false,
525
+ error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
526
+ }));
450
527
  process.exit(1);
451
528
  }
452
529
  assertKnownComponentIds(parsedFilteredIds, config);
453
530
  }
454
531
  // Parse privateVarMap JSON for COMPONENT/COMPONENT_LIST
455
532
  let parsedPrivateVarMap;
456
- if (options.privateVarMap && (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
533
+ if (options.privateVarMap &&
534
+ (propType === "COMPONENT" || propType === "COMPONENT_LIST")) {
457
535
  try {
458
536
  parsedPrivateVarMap = JSON.parse(options.privateVarMap);
459
- if (typeof parsedPrivateVarMap !== "object" || Array.isArray(parsedPrivateVarMap)) {
460
- console.log(JSON.stringify({ success: false, error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}." }));
537
+ if (typeof parsedPrivateVarMap !== "object" ||
538
+ Array.isArray(parsedPrivateVarMap)) {
539
+ console.log(JSON.stringify({
540
+ success: false,
541
+ error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
542
+ }));
461
543
  process.exit(1);
462
544
  }
463
545
  }
464
546
  catch {
465
- console.log(JSON.stringify({ success: false, error: `Invalid --privateVarMap JSON: ${options.privateVarMap}` }));
547
+ console.log(JSON.stringify({
548
+ success: false,
549
+ error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
550
+ }));
551
+ process.exit(1);
552
+ }
553
+ }
554
+ const parsedDefaultValue = options.defaultValue !== undefined
555
+ ? parseDefaultValue(options.defaultValue, propType)
556
+ : undefined;
557
+ if ((propType === "LINK" || propType === "LIST_OF_LINK") &&
558
+ parsedDefaultValue !== undefined) {
559
+ const linkError = validateLinkDefaultValue(propType, parsedDefaultValue, options.name);
560
+ if (linkError) {
561
+ console.log(JSON.stringify({ success: false, error: linkError }));
562
+ process.exit(1);
563
+ }
564
+ }
565
+ if ((propType === "SVG" || propType === "SVG_LIST") &&
566
+ parsedDefaultValue !== undefined) {
567
+ const svgError = validateSvgDefaultValue(propType, parsedDefaultValue, options.name);
568
+ if (svgError) {
569
+ console.log(JSON.stringify({ success: false, error: svgError }));
466
570
  process.exit(1);
467
571
  }
468
572
  }
@@ -472,8 +576,8 @@ async function addProp(ref, options) {
472
576
  type: propType,
473
577
  required: options.required ?? false,
474
578
  ...(options.description ? { description: options.description } : {}),
475
- ...(options.defaultValue !== undefined
476
- ? { defaultValue: parseDefaultValue(options.defaultValue, propType) }
579
+ ...(parsedDefaultValue !== undefined
580
+ ? { defaultValue: parsedDefaultValue }
477
581
  : {}),
478
582
  ...(options.group ? { groupId: options.group } : {}),
479
583
  ...(options.typeId ? { typeId: options.typeId } : {}),
@@ -496,8 +600,12 @@ async function addProp(ref, options) {
496
600
  required: newProp.required,
497
601
  ...(newProp.groupId ? { groupId: newProp.groupId } : {}),
498
602
  ...(newProp.enumTypeId ? { enumTypeId: newProp.enumTypeId } : {}),
499
- ...(newProp.filteredComponentIds ? { filteredComponentIds: newProp.filteredComponentIds } : {}),
500
- ...(newProp.privateVarMap ? { privateVarMap: newProp.privateVarMap } : {}),
603
+ ...(newProp.filteredComponentIds
604
+ ? { filteredComponentIds: newProp.filteredComponentIds }
605
+ : {}),
606
+ ...(newProp.privateVarMap
607
+ ? { privateVarMap: newProp.privateVarMap }
608
+ : {}),
501
609
  },
502
610
  }));
503
611
  }
@@ -507,10 +615,147 @@ function parseDefaultValue(value, propType) {
507
615
  return Number(value);
508
616
  case "BOOLEAN":
509
617
  return value === "true";
618
+ case "LINK":
619
+ case "LIST_OF_LINK":
620
+ // The --default-value flag arrives as a string; LINK values must be stored as a
621
+ // typed object. Parse it here; if it is not valid JSON, return the raw string so
622
+ // validateLinkDefaultValue can report a precise error.
623
+ try {
624
+ return JSON.parse(value);
625
+ }
626
+ catch {
627
+ return value;
628
+ }
629
+ case "SVG_LIST":
630
+ // Same reasoning as LINK_LIST: SVG_LIST must be stored as an array of SVG strings.
631
+ // Parse it here; if it is not valid JSON, return the raw string so
632
+ // validateSvgDefaultValue can report a precise "must be an array" error.
633
+ try {
634
+ return JSON.parse(value);
635
+ }
636
+ catch {
637
+ return value;
638
+ }
510
639
  default:
511
640
  return value;
512
641
  }
513
642
  }
643
+ const VALID_LINK_TYPES = ["PAGE", "EXTERNAL", "FILE"];
644
+ const LINK_SHAPE_HELP = 'A link must be an object: { "linkType": "EXTERNAL", "label": "Text", "externalLink": "https://...", "subLinks": [] } ' +
645
+ 'for external links, or { "linkType": "PAGE", "label": "Text", "pageType": "INDEX", "subLinks": [] } for store pages. ' +
646
+ "Do NOT pass a JSON string or a { label, href } shape.";
647
+ // Returns the index of the first array element that fails `fn`, as an error string, or null.
648
+ const firstError = (arr, fn) => arr.reduce((acc, item, index) => acc ?? fn(item, index), null);
649
+ const validateSingleLink = (link, propName, path) => {
650
+ const where = `defaultValue for prop "${propName}"${path}`;
651
+ if (typeof link === "string") {
652
+ return `${where} is a JSON string but must be an object. ${LINK_SHAPE_HELP}`;
653
+ }
654
+ if (!link || typeof link !== "object" || Array.isArray(link)) {
655
+ return `${where} must be a link object. ${LINK_SHAPE_HELP}`;
656
+ }
657
+ const l = link;
658
+ if ("href" in l && !("linkType" in l)) {
659
+ return `${where} uses a legacy { href } shape without "linkType". Use "linkType" + "externalLink"/"pageType" instead. ${LINK_SHAPE_HELP}`;
660
+ }
661
+ if (typeof l.linkType !== "string" ||
662
+ !VALID_LINK_TYPES.includes(l.linkType)) {
663
+ return `${where} must have a valid "linkType" (one of ${VALID_LINK_TYPES.join(", ")}). ${LINK_SHAPE_HELP}`;
664
+ }
665
+ if (l.subLinks !== undefined && !Array.isArray(l.subLinks)) {
666
+ return `${where}: "subLinks" must be an array (use [] when there are none).`;
667
+ }
668
+ if (Array.isArray(l.subLinks)) {
669
+ return firstError(l.subLinks, (sl, i) => validateSingleLink(sl, propName, `${path}.subLinks[${i}]`));
670
+ }
671
+ return null;
672
+ };
673
+ // Validates a LINK / LIST_OF_LINK defaultValue. Returns an error message, or null if valid.
674
+ // These defaults are a frequent source of bad data: agents tend to pass a JSON string or a
675
+ // legacy { label, href } shape. Rejecting them at authoring time keeps malformed link values
676
+ // out of published themes.
677
+ function validateLinkDefaultValue(propType, value, propName) {
678
+ if (value === undefined || value === null)
679
+ return null;
680
+ if (propType === "LINK") {
681
+ return validateSingleLink(value, propName, "");
682
+ }
683
+ // LIST_OF_LINK
684
+ if (typeof value === "string") {
685
+ return `defaultValue for prop "${propName}" is a JSON string but must be an object { "links": [...] }. ${LINK_SHAPE_HELP}`;
686
+ }
687
+ if (!value ||
688
+ typeof value !== "object" ||
689
+ Array.isArray(value) ||
690
+ !Array.isArray(value.links)) {
691
+ return `defaultValue for prop "${propName}" must be an object with a "links" array: { "links": [ <link>, ... ] }. ${LINK_SHAPE_HELP}`;
692
+ }
693
+ const links = value.links;
694
+ return firstError(links, (link, i) => validateSingleLink(link, propName, `.links[${i}]`));
695
+ }
696
+ const SVG_DEFAULT_HELP = "An SVG default must be a single well-formed <svg>…</svg> string ≤ 64 KB with no embedded base64 raster images. " +
697
+ "Tip: the editor uses each svg's `class` attribute as its display name — set class=\"my-icon\" to name it; " +
698
+ "SVG_LIST items with no class show as svg-1, svg-2, ….";
699
+ // Keep the cap and base64 pattern in sync with @ikas/editor-models validate-svg.ts (the
700
+ // authoritative validator). Duplicated here on purpose — see validateSvgShape below.
701
+ const SVG_MAX_SIZE_KB = 64;
702
+ const SVG_BASE64_RASTER_RE = /data:image\/[a-z0-9.+-]+;base64/i;
703
+ const SVG_SHAPE_RE = /^\s*(?:<\?xml[^>]*\?>\s*)?(?:<!DOCTYPE[^>]*>\s*)?<svg[\s>][\s\S]*<\/svg>\s*$/i;
704
+ // Count TOP-LEVEL <svg> roots without a DOM: scan svg open/close tags in order and track
705
+ // nesting depth. A root is any <svg> opened at depth 0. This matches the editor's DOM-based
706
+ // single-root check — it rejects siblings (`<svg/><svg/>`) while allowing a nested <svg>.
707
+ // Self-closing <svg .../> counts as a root but opens no nesting level.
708
+ function svgRootCount(input) {
709
+ const tags = input.match(/<svg\b[^>]*>|<\/svg\s*>/gi) || [];
710
+ return tags.reduce((acc, tag) => {
711
+ if (tag.startsWith("</"))
712
+ return { depth: Math.max(0, acc.depth - 1), roots: acc.roots };
713
+ const roots = acc.depth === 0 ? acc.roots + 1 : acc.roots;
714
+ const depth = /\/>\s*$/.test(tag) ? acc.depth : acc.depth + 1;
715
+ return { depth, roots };
716
+ }, { depth: 0, roots: 0 }).roots;
717
+ }
718
+ // Lightweight, dependency-free SVG structure check. This standalone CLI is publishable and
719
+ // must not depend on the private @ikas/editor-models package (nor pull a DOM/DOMPurify into a
720
+ // CLI), so it mirrors the RULES of the editor's validateSvg (size, base64, single <svg> root)
721
+ // for fast authoring feedback. The authoritative sanitize runs in the editor/upload path.
722
+ // Returns a rejection reason, or null if the shape is acceptable.
723
+ function validateSvgShape(input) {
724
+ if (typeof input !== "string" || input.trim() === "")
725
+ return "empty or not a string";
726
+ if (Buffer.byteLength(input, "utf8") > SVG_MAX_SIZE_KB * 1024)
727
+ return `exceeds ${SVG_MAX_SIZE_KB} KB`;
728
+ if (SVG_BASE64_RASTER_RE.test(input))
729
+ return "embedded base64 raster image not allowed";
730
+ if (!SVG_SHAPE_RE.test(input))
731
+ return "not a single-root <svg> document";
732
+ if (svgRootCount(input) !== 1)
733
+ return "must be exactly one <svg> root";
734
+ return null;
735
+ }
736
+ // Validates an SVG / SVG_LIST defaultValue. Returns an error message, or null if valid.
737
+ // Authoring an SVG default is a common mistake source for agents; rejecting here (with an
738
+ // instructive message) keeps malformed/oversized SVGs out of published components.
739
+ function validateSvgDefaultValue(propType, value, propName) {
740
+ if (value === undefined || value === null)
741
+ return null;
742
+ if (propType === "SVG") {
743
+ const reason = validateSvgShape(value);
744
+ return reason
745
+ ? `defaultValue for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
746
+ : null;
747
+ }
748
+ // SVG_LIST
749
+ if (!Array.isArray(value)) {
750
+ return `defaultValue for prop "${propName}" must be an array of SVG strings: [ "<svg>…</svg>", ... ]. ${SVG_DEFAULT_HELP}`;
751
+ }
752
+ return firstError(value, (svg, i) => {
753
+ const reason = validateSvgShape(svg);
754
+ return reason
755
+ ? `defaultValue[${i}] for prop "${propName}" is not a valid SVG: ${reason}. ${SVG_DEFAULT_HELP}`
756
+ : null;
757
+ });
758
+ }
514
759
  function updateProp(ref, options) {
515
760
  const { config, configPath } = loadConfig();
516
761
  const component = resolveComponent(config, ref);
@@ -544,7 +789,15 @@ function updateProp(ref, options) {
544
789
  prop.description = options.description || undefined;
545
790
  }
546
791
  if (options.defaultValue !== undefined) {
547
- prop.defaultValue = parseDefaultValue(options.defaultValue, prop.type);
792
+ const parsed = parseDefaultValue(options.defaultValue, prop.type);
793
+ if (prop.type === "LINK" || prop.type === "LIST_OF_LINK") {
794
+ const linkError = validateLinkDefaultValue(prop.type, parsed, prop.name);
795
+ if (linkError) {
796
+ console.log(JSON.stringify({ success: false, error: linkError }));
797
+ process.exit(1);
798
+ }
799
+ }
800
+ prop.defaultValue = parsed;
548
801
  }
549
802
  if (options.group !== undefined) {
550
803
  if (options.group === "" || options.group === "none") {
@@ -586,14 +839,18 @@ function updateProp(ref, options) {
586
839
  }
587
840
  }
588
841
  if (options.filteredComponentIds !== undefined) {
589
- if (options.filteredComponentIds === "" || options.filteredComponentIds === "none") {
842
+ if (options.filteredComponentIds === "" ||
843
+ options.filteredComponentIds === "none") {
590
844
  delete prop.filteredComponentIds;
591
845
  }
592
846
  else {
593
847
  try {
594
848
  const parsed = JSON.parse(options.filteredComponentIds);
595
849
  if (!Array.isArray(parsed)) {
596
- console.log(JSON.stringify({ success: false, error: "--filteredComponentIds must be a JSON array of strings." }));
850
+ console.log(JSON.stringify({
851
+ success: false,
852
+ error: "--filteredComponentIds must be a JSON array of strings.",
853
+ }));
597
854
  process.exit(1);
598
855
  }
599
856
  assertKnownComponentIds(parsed, config);
@@ -601,7 +858,10 @@ function updateProp(ref, options) {
601
858
  }
602
859
  catch (err) {
603
860
  if (err instanceof SyntaxError) {
604
- console.log(JSON.stringify({ success: false, error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}` }));
861
+ console.log(JSON.stringify({
862
+ success: false,
863
+ error: `Invalid --filteredComponentIds JSON: ${options.filteredComponentIds}`,
864
+ }));
605
865
  process.exit(1);
606
866
  }
607
867
  throw err;
@@ -616,13 +876,19 @@ function updateProp(ref, options) {
616
876
  try {
617
877
  const parsed = JSON.parse(options.privateVarMap);
618
878
  if (typeof parsed !== "object" || Array.isArray(parsed)) {
619
- console.log(JSON.stringify({ success: false, error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}." }));
879
+ console.log(JSON.stringify({
880
+ success: false,
881
+ error: "--privateVarMap must be a JSON object mapping keys to {id, typeId}.",
882
+ }));
620
883
  process.exit(1);
621
884
  }
622
885
  prop.privateVarMap = parsed;
623
886
  }
624
887
  catch {
625
- console.log(JSON.stringify({ success: false, error: `Invalid --privateVarMap JSON: ${options.privateVarMap}` }));
888
+ console.log(JSON.stringify({
889
+ success: false,
890
+ error: `Invalid --privateVarMap JSON: ${options.privateVarMap}`,
891
+ }));
626
892
  process.exit(1);
627
893
  }
628
894
  }
@@ -642,7 +908,9 @@ function updateProp(ref, options) {
642
908
  ...(prop.groupId ? { groupId: prop.groupId } : {}),
643
909
  ...(prop.typeId ? { typeId: prop.typeId } : {}),
644
910
  ...(prop.enumTypeId ? { enumTypeId: prop.enumTypeId } : {}),
645
- ...(prop.filteredComponentIds ? { filteredComponentIds: prop.filteredComponentIds } : {}),
911
+ ...(prop.filteredComponentIds
912
+ ? { filteredComponentIds: prop.filteredComponentIds }
913
+ : {}),
646
914
  ...(prop.privateVarMap ? { privateVarMap: prop.privateVarMap } : {}),
647
915
  },
648
916
  }));
@@ -706,7 +974,10 @@ function removeComponent(ref) {
706
974
  removedComponentId: component.id,
707
975
  removedComponentName: component.name,
708
976
  removedDirectory: path.relative(process.cwd(), componentDir),
709
- remainingComponents: config.components.map((c) => ({ id: c.id, name: c.name })),
977
+ remainingComponents: config.components.map((c) => ({
978
+ id: c.id,
979
+ name: c.name,
980
+ })),
710
981
  }));
711
982
  }
712
983
  function addPropGroup(ref, options) {
@@ -813,10 +1084,10 @@ function removePropGroup(ref, options) {
813
1084
  process.exit(1);
814
1085
  }
815
1086
  if (found.parent) {
816
- found.parent.children = found.parent.children.filter(c => c.id !== options.id);
1087
+ found.parent.children = found.parent.children.filter((c) => c.id !== options.id);
817
1088
  }
818
1089
  else {
819
- component.propGroups = component.propGroups.filter(g => g.id !== options.id);
1090
+ component.propGroups = component.propGroups.filter((g) => g.id !== options.id);
820
1091
  }
821
1092
  // Clear groupId from props referencing the deleted group
822
1093
  for (const prop of component.props) {
@@ -829,7 +1100,7 @@ function removePropGroup(ref, options) {
829
1100
  success: true,
830
1101
  componentName: component.name,
831
1102
  removedPropGroup: options.id,
832
- remainingPropGroups: component.propGroups.map(g => g.id),
1103
+ remainingPropGroups: component.propGroups.map((g) => g.id),
833
1104
  }));
834
1105
  }
835
1106
  function movePropGroup(ref, options) {
@@ -846,7 +1117,10 @@ function movePropGroup(ref, options) {
846
1117
  component.propGroups = movePropGroupInTree(component.propGroups, options.id, options.parent || null, options.index);
847
1118
  }
848
1119
  catch (e) {
849
- console.log(JSON.stringify({ success: false, error: e.message || "Failed to move prop group" }));
1120
+ console.log(JSON.stringify({
1121
+ success: false,
1122
+ error: e.message || "Failed to move prop group",
1123
+ }));
850
1124
  process.exit(1);
851
1125
  }
852
1126
  saveConfig(configPath, config);
@@ -902,7 +1176,9 @@ function listComponents() {
902
1176
  required: p.required,
903
1177
  ...(p.groupId ? { groupId: p.groupId } : {}),
904
1178
  })),
905
- ...(c.propGroups && c.propGroups.length > 0 ? { propGroups: c.propGroups } : {}),
1179
+ ...(c.propGroups && c.propGroups.length > 0
1180
+ ? { propGroups: c.propGroups }
1181
+ : {}),
906
1182
  }));
907
1183
  console.log(JSON.stringify({
908
1184
  success: true,
@@ -916,7 +1192,9 @@ function listComponents() {
916
1192
  async function addEnum(options) {
917
1193
  const { config, configPath } = loadConfig();
918
1194
  // Validate name
919
- if (!options.name || typeof options.name !== "string" || !options.name.trim()) {
1195
+ if (!options.name ||
1196
+ typeof options.name !== "string" ||
1197
+ !options.name.trim()) {
920
1198
  console.log(JSON.stringify({ success: false, error: "Enum name is required." }));
921
1199
  process.exit(1);
922
1200
  }
@@ -1030,7 +1308,7 @@ function updateEnum(options) {
1030
1308
  Object.keys(enumOptions).length === 0) {
1031
1309
  console.log(JSON.stringify({
1032
1310
  success: false,
1033
- error: 'Options must be a non-empty JSON object.',
1311
+ error: "Options must be a non-empty JSON object.",
1034
1312
  }));
1035
1313
  process.exit(1);
1036
1314
  }
@@ -1103,8 +1381,7 @@ function listEnums() {
1103
1381
  }
1104
1382
  // --- Command registration ---
1105
1383
  export function createConfigCommand() {
1106
- const config = new Command("config")
1107
- .description("Non-interactive commands for managing ikas.config.json (designed for AI agents and scripting)");
1384
+ const config = new Command("config").description("Non-interactive commands for managing ikas.config.json (designed for AI agents and scripting)");
1108
1385
  config
1109
1386
  .command("add-component")
1110
1387
  .description("Add a new component to the project")
@@ -1112,7 +1389,8 @@ export function createConfigCommand() {
1112
1389
  .option("--type <type>", "Component type: section or component", "component")
1113
1390
  .option("--isHeader", "Mark this section as the store header (only for type: section)")
1114
1391
  .option("--isFooter", "Mark this section as the store footer (only for type: section)")
1115
- .option("--props <json>", "JSON array of props, e.g. '[{\"name\":\"title\",\"type\":\"TEXT\",\"required\":true}]'. Each prop needs name+type; displayName is auto-generated from name if omitted.")
1392
+ .option("--props <json>", "JSON array of props. Required per entry: name, type. Optional: displayName (auto from name), required, description, defaultValue, groupId, typeId (TYPE props), enumTypeId (ENUM props), filteredComponentIds, privateVarMap. " +
1393
+ 'Example: \'[{"name":"title","type":"TEXT","required":true,"defaultValue":"Hello","groupId":"basic"}]\'')
1116
1394
  .action((options) => {
1117
1395
  addComponent(options.name, options);
1118
1396
  });
@@ -1131,7 +1409,7 @@ export function createConfigCommand() {
1131
1409
  .option("--typeId <typeId>", "Type ID for TYPE props (required when type is TYPE)")
1132
1410
  .option("--enumTypeId <enumTypeId>", "Enum type ID for ENUM props (required when type is ENUM)")
1133
1411
  .option("--filteredComponentIds <json>", "JSON array of component IDs to restrict selection (for COMPONENT/COMPONENT_LIST)")
1134
- .option("--privateVarMap <json>", 'JSON object mapping variable keys to {id, typeId} (for COMPONENT/COMPONENT_LIST)')
1412
+ .option("--privateVarMap <json>", "JSON object mapping variable keys to {id, typeId} (for COMPONENT/COMPONENT_LIST)")
1135
1413
  .action((options) => {
1136
1414
  addProp({ id: options.componentId, name: options.component }, options);
1137
1415
  });
@@ -1222,7 +1500,7 @@ export function createConfigCommand() {
1222
1500
  .option("--component <name>", "Component name — exact match (PascalCase, as stored in ikas.config.json)")
1223
1501
  .requiredOption("--id <id>", "Group ID to move")
1224
1502
  .option("--parent <parentId>", "Target parent group ID (omit to move to root)")
1225
- .option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", v => parseInt(v, 10))
1503
+ .option("--index <index>", "Zero-based insertion index within the target parent (appends when omitted)", (v) => parseInt(v, 10))
1226
1504
  .action((options) => {
1227
1505
  movePropGroup({ id: options.componentId, name: options.component }, options);
1228
1506
  });
@@ -1318,7 +1596,15 @@ async function fetchLiveEditorEnumById(enumId) {
1318
1596
  clearTimeout(timeout);
1319
1597
  const types = message.payload?.types || [];
1320
1598
  const match = types.find((t) => t.category === "enum" && t.id === enumId);
1321
- finish(match ? { id: match.id, name: match.name, ...(match.enumOptions ? { enumOptions: match.enumOptions } : {}) } : null);
1599
+ finish(match
1600
+ ? {
1601
+ id: match.id,
1602
+ name: match.name,
1603
+ ...(match.enumOptions
1604
+ ? { enumOptions: match.enumOptions }
1605
+ : {}),
1606
+ }
1607
+ : null);
1322
1608
  }
1323
1609
  }
1324
1610
  catch {