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