@principal-ai/principal-view-cli 0.1.19 → 0.1.20
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/README.md +7 -2
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +7 -7
- package/dist/commands/hooks.d.ts.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +4 -2
- package/dist/commands/lint.d.ts.map +1 -1
- package/dist/commands/lint.js +14 -9
- package/dist/commands/schema.d.ts.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +174 -34
- package/dist/index.cjs +443 -97
- package/dist/index.cjs.map +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@ vv validate --json # Output as JSON
|
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
**Validation checks:**
|
|
40
|
+
|
|
40
41
|
- Required `vv` extension with name and version
|
|
41
42
|
- All nodes have required fields (id, type, x, y, width, height)
|
|
42
43
|
- Custom node types must have `vv.nodeType` and valid `vv.shape`
|
|
@@ -96,18 +97,22 @@ Canvas files follow the [JSON Canvas](https://jsoncanvas.org/) specification wit
|
|
|
96
97
|
### Node Types
|
|
97
98
|
|
|
98
99
|
**Standard types** (no additional metadata required):
|
|
100
|
+
|
|
99
101
|
- `text` - Text content
|
|
100
102
|
- `group` - Container for other nodes
|
|
101
103
|
- `file` - File reference
|
|
102
104
|
- `link` - URL link
|
|
103
105
|
|
|
104
106
|
**Custom types** require `vv` extension:
|
|
107
|
+
|
|
105
108
|
```json
|
|
106
109
|
{
|
|
107
110
|
"id": "node-1",
|
|
108
111
|
"type": "custom",
|
|
109
|
-
"x": 0,
|
|
110
|
-
"
|
|
112
|
+
"x": 0,
|
|
113
|
+
"y": 0,
|
|
114
|
+
"width": 200,
|
|
115
|
+
"height": 100,
|
|
111
116
|
"vv": {
|
|
112
117
|
"nodeType": "service",
|
|
113
118
|
"shape": "rectangle"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,wBAAgB,mBAAmB,IAAI,OAAO,
|
|
1
|
+
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,wBAAgB,mBAAmB,IAAI,OAAO,CAkD7C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmLpC,wBAAgB,mBAAmB,IAAI,OAAO,CAsK7C"}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -184,21 +184,21 @@ export function createDoctorCommand() {
|
|
|
184
184
|
// Filter issues based on options
|
|
185
185
|
const filterIssues = (issues) => {
|
|
186
186
|
if (options.errorsOnly) {
|
|
187
|
-
return issues.filter(i => i.type === 'error');
|
|
187
|
+
return issues.filter((i) => i.type === 'error');
|
|
188
188
|
}
|
|
189
189
|
if (options.quiet) {
|
|
190
|
-
return issues.filter(i => i.type === 'error' || i.type === 'warning');
|
|
190
|
+
return issues.filter((i) => i.type === 'error' || i.type === 'warning');
|
|
191
191
|
}
|
|
192
192
|
return issues;
|
|
193
193
|
};
|
|
194
194
|
// Count issues
|
|
195
|
-
const allIssues = results.flatMap(r => filterIssues(r.issues));
|
|
196
|
-
const errorCount = allIssues.filter(i => i.type === 'error').length;
|
|
197
|
-
const warningCount = allIssues.filter(i => i.type === 'warning').length;
|
|
195
|
+
const allIssues = results.flatMap((r) => filterIssues(r.issues));
|
|
196
|
+
const errorCount = allIssues.filter((i) => i.type === 'error').length;
|
|
197
|
+
const warningCount = allIssues.filter((i) => i.type === 'warning').length;
|
|
198
198
|
// Output results
|
|
199
199
|
if (options.json) {
|
|
200
200
|
console.log(JSON.stringify({
|
|
201
|
-
results: results.map(r => ({
|
|
201
|
+
results: results.map((r) => ({
|
|
202
202
|
...r,
|
|
203
203
|
issues: filterIssues(r.issues),
|
|
204
204
|
})),
|
|
@@ -221,7 +221,7 @@ export function createDoctorCommand() {
|
|
|
221
221
|
continue;
|
|
222
222
|
}
|
|
223
223
|
if (issues.length > 0) {
|
|
224
|
-
const hasErrors = issues.some(i => i.type === 'error');
|
|
224
|
+
const hasErrors = issues.some((i) => i.type === 'error');
|
|
225
225
|
const icon = hasErrors ? chalk.red('✗') : chalk.yellow('⚠');
|
|
226
226
|
console.log(`${icon} ${result.configFile}` + chalk.dim(` (${result.configName})`));
|
|
227
227
|
for (const issue of issues) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/commands/hooks.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/commands/hooks.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA4NpC,wBAAgB,kBAAkB,IAAI,OAAO,CA+F5C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkIpC,wBAAgB,iBAAiB,IAAI,OAAO,
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkIpC,wBAAgB,iBAAiB,IAAI,OAAO,CA6L3C"}
|
package/dist/commands/init.js
CHANGED
|
@@ -198,12 +198,14 @@ edgeComponents: {}
|
|
|
198
198
|
if (existsSync(preCommitFile)) {
|
|
199
199
|
// Check if our hook is already in the file
|
|
200
200
|
const existingContent = readFileSync(preCommitFile, 'utf8');
|
|
201
|
-
if (existingContent.includes('principal-view-cli lint') ||
|
|
201
|
+
if (existingContent.includes('principal-view-cli lint') ||
|
|
202
|
+
existingContent.includes('privu lint')) {
|
|
202
203
|
console.log(chalk.yellow(`Husky pre-commit hook already includes principal view linting`));
|
|
203
204
|
}
|
|
204
205
|
else {
|
|
205
206
|
// Append our lint command to existing pre-commit
|
|
206
|
-
const updatedContent = existingContent.trimEnd() +
|
|
207
|
+
const updatedContent = existingContent.trimEnd() +
|
|
208
|
+
'\n\n# Run principal view linting\nnpx @principal-ai/principal-view-cli lint --quiet\n';
|
|
207
209
|
writeFileSync(preCommitFile, updatedContent);
|
|
208
210
|
console.log(chalk.green(`Updated Husky pre-commit hook with principal view linting`));
|
|
209
211
|
huskySetup = true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../../src/commands/lint.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../../src/commands/lint.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0PpC,wBAAgB,iBAAiB,IAAI,OAAO,CA8L3C"}
|
package/dist/commands/lint.js
CHANGED
|
@@ -14,11 +14,7 @@ import { createDefaultRulesEngine, validatePrivuConfig, mergeConfigs, getDefault
|
|
|
14
14
|
/**
|
|
15
15
|
* Config file names in resolution order
|
|
16
16
|
*/
|
|
17
|
-
const CONFIG_FILE_NAMES = [
|
|
18
|
-
'.privurc.yaml',
|
|
19
|
-
'.privurc.yml',
|
|
20
|
-
'.privurc.json',
|
|
21
|
-
];
|
|
17
|
+
const CONFIG_FILE_NAMES = ['.privurc.yaml', '.privurc.yml', '.privurc.json'];
|
|
22
18
|
/**
|
|
23
19
|
* Find and load privurc config file
|
|
24
20
|
*/
|
|
@@ -277,7 +273,11 @@ export function createLintCommand() {
|
|
|
277
273
|
patterns = privuConfig.include;
|
|
278
274
|
}
|
|
279
275
|
else {
|
|
280
|
-
patterns = [
|
|
276
|
+
patterns = [
|
|
277
|
+
'.principal-views/**/*.yaml',
|
|
278
|
+
'.principal-views/**/*.yml',
|
|
279
|
+
'.principal-views/**/*.json',
|
|
280
|
+
];
|
|
281
281
|
}
|
|
282
282
|
// Find matching files
|
|
283
283
|
const matchedFiles = await globby(patterns, {
|
|
@@ -294,7 +294,10 @@ export function createLintCommand() {
|
|
|
294
294
|
});
|
|
295
295
|
if (configFiles.length === 0) {
|
|
296
296
|
if (options.json) {
|
|
297
|
-
console.log(JSON.stringify({
|
|
297
|
+
console.log(JSON.stringify({
|
|
298
|
+
files: [],
|
|
299
|
+
summary: { totalFiles: 0, totalErrors: 0, totalWarnings: 0, totalFixable: 0 },
|
|
300
|
+
}));
|
|
298
301
|
}
|
|
299
302
|
else {
|
|
300
303
|
console.log(chalk.yellow('No configuration files found matching the specified patterns.'));
|
|
@@ -323,14 +326,16 @@ export function createLintCommand() {
|
|
|
323
326
|
if (!loaded) {
|
|
324
327
|
// File couldn't be loaded - report as error
|
|
325
328
|
results.set(relativePath, {
|
|
326
|
-
violations: [
|
|
329
|
+
violations: [
|
|
330
|
+
{
|
|
327
331
|
ruleId: 'parse-error',
|
|
328
332
|
severity: 'error',
|
|
329
333
|
file: relativePath,
|
|
330
334
|
message: `Could not parse file: ${filePath}`,
|
|
331
335
|
impact: 'File cannot be validated',
|
|
332
336
|
fixable: false,
|
|
333
|
-
}
|
|
337
|
+
},
|
|
338
|
+
],
|
|
334
339
|
errorCount: 1,
|
|
335
340
|
warningCount: 0,
|
|
336
341
|
fixableCount: 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/commands/schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/commands/schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2UpC,wBAAgB,mBAAmB,IAAI,OAAO,CA0B7C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA26BpC,wBAAgB,qBAAqB,IAAI,OAAO,CAsH/C"}
|
|
@@ -17,13 +17,13 @@ function loadLibrary(principalViewsDir) {
|
|
|
17
17
|
if (existsSync(libraryPath)) {
|
|
18
18
|
try {
|
|
19
19
|
const content = readFileSync(libraryPath, 'utf8');
|
|
20
|
-
const library = fileName.endsWith('.json')
|
|
21
|
-
? JSON.parse(content)
|
|
22
|
-
: yaml.load(content);
|
|
20
|
+
const library = fileName.endsWith('.json') ? JSON.parse(content) : yaml.load(content);
|
|
23
21
|
if (library && typeof library === 'object') {
|
|
24
22
|
return {
|
|
25
|
-
nodeComponents: library.nodeComponents ||
|
|
26
|
-
|
|
23
|
+
nodeComponents: library.nodeComponents ||
|
|
24
|
+
{},
|
|
25
|
+
edgeComponents: library.edgeComponents ||
|
|
26
|
+
{},
|
|
27
27
|
raw: library,
|
|
28
28
|
path: libraryPath,
|
|
29
29
|
};
|
|
@@ -129,12 +129,27 @@ const VALID_NODE_SHAPES = ['circle', 'rectangle', 'hexagon', 'diamond', 'custom'
|
|
|
129
129
|
const ALLOWED_CANVAS_FIELDS = {
|
|
130
130
|
root: ['nodes', 'edges', 'pv'],
|
|
131
131
|
pv: ['version', 'name', 'description', 'nodeTypes', 'edgeTypes', 'pathConfig', 'display'],
|
|
132
|
-
pvPathConfig: [
|
|
132
|
+
pvPathConfig: [
|
|
133
|
+
'projectRoot',
|
|
134
|
+
'captureSource',
|
|
135
|
+
'enableActionPatterns',
|
|
136
|
+
'logLevel',
|
|
137
|
+
'ignoreUnsourced',
|
|
138
|
+
],
|
|
133
139
|
pvDisplay: ['layout', 'theme', 'animations'],
|
|
134
140
|
pvDisplayTheme: ['primary', 'success', 'warning', 'danger', 'info'],
|
|
135
141
|
pvDisplayAnimations: ['enabled', 'speed'],
|
|
136
142
|
pvNodeType: ['label', 'description', 'color', 'icon', 'shape'],
|
|
137
|
-
pvEdgeType: [
|
|
143
|
+
pvEdgeType: [
|
|
144
|
+
'label',
|
|
145
|
+
'style',
|
|
146
|
+
'color',
|
|
147
|
+
'width',
|
|
148
|
+
'directed',
|
|
149
|
+
'animation',
|
|
150
|
+
'labelConfig',
|
|
151
|
+
'activatedBy',
|
|
152
|
+
],
|
|
138
153
|
pvEdgeTypeAnimation: ['type', 'duration', 'color'],
|
|
139
154
|
pvEdgeTypeLabelConfig: ['field', 'position'],
|
|
140
155
|
// Base node fields from JSON Canvas spec
|
|
@@ -145,13 +160,36 @@ const ALLOWED_CANVAS_FIELDS = {
|
|
|
145
160
|
nodeLink: ['url'],
|
|
146
161
|
nodeGroup: ['label', 'background', 'backgroundStyle'],
|
|
147
162
|
// Node pv extension
|
|
148
|
-
nodePv: [
|
|
163
|
+
nodePv: [
|
|
164
|
+
'nodeType',
|
|
165
|
+
'description',
|
|
166
|
+
'shape',
|
|
167
|
+
'icon',
|
|
168
|
+
'fill',
|
|
169
|
+
'stroke',
|
|
170
|
+
'states',
|
|
171
|
+
'sources',
|
|
172
|
+
'actions',
|
|
173
|
+
'dataSchema',
|
|
174
|
+
'layout',
|
|
175
|
+
],
|
|
149
176
|
nodePvState: ['color', 'icon', 'label'],
|
|
150
177
|
nodePvAction: ['pattern', 'event', 'state', 'metadata', 'triggerEdges'],
|
|
151
178
|
nodePvDataSchemaField: ['type', 'required', 'displayInLabel'],
|
|
152
179
|
nodePvLayout: ['layer', 'cluster'],
|
|
153
180
|
// Edge fields
|
|
154
|
-
edge: [
|
|
181
|
+
edge: [
|
|
182
|
+
'id',
|
|
183
|
+
'fromNode',
|
|
184
|
+
'toNode',
|
|
185
|
+
'fromSide',
|
|
186
|
+
'toSide',
|
|
187
|
+
'fromEnd',
|
|
188
|
+
'toEnd',
|
|
189
|
+
'color',
|
|
190
|
+
'label',
|
|
191
|
+
'pv',
|
|
192
|
+
],
|
|
155
193
|
edgePv: ['edgeType', 'style', 'width', 'animation', 'activatedBy'],
|
|
156
194
|
edgePvAnimation: ['type', 'duration', 'color'],
|
|
157
195
|
edgePvActivatedBy: ['action', 'animation', 'direction', 'duration'],
|
|
@@ -161,13 +199,35 @@ const ALLOWED_CANVAS_FIELDS = {
|
|
|
161
199
|
*/
|
|
162
200
|
const ALLOWED_LIBRARY_FIELDS = {
|
|
163
201
|
root: ['version', 'name', 'description', 'nodeComponents', 'edgeComponents', 'connectionRules'],
|
|
164
|
-
nodeComponent: [
|
|
202
|
+
nodeComponent: [
|
|
203
|
+
'description',
|
|
204
|
+
'tags',
|
|
205
|
+
'defaultLabel',
|
|
206
|
+
'shape',
|
|
207
|
+
'icon',
|
|
208
|
+
'color',
|
|
209
|
+
'size',
|
|
210
|
+
'states',
|
|
211
|
+
'sources',
|
|
212
|
+
'actions',
|
|
213
|
+
'dataSchema',
|
|
214
|
+
'layout',
|
|
215
|
+
],
|
|
165
216
|
nodeComponentSize: ['width', 'height'],
|
|
166
217
|
nodeComponentState: ['color', 'icon', 'label'],
|
|
167
218
|
nodeComponentAction: ['pattern', 'event', 'state', 'metadata', 'triggerEdges'],
|
|
168
219
|
nodeComponentDataSchemaField: ['type', 'required', 'displayInLabel', 'label', 'displayInInfo'],
|
|
169
220
|
nodeComponentLayout: ['layer', 'cluster'],
|
|
170
|
-
edgeComponent: [
|
|
221
|
+
edgeComponent: [
|
|
222
|
+
'description',
|
|
223
|
+
'tags',
|
|
224
|
+
'style',
|
|
225
|
+
'color',
|
|
226
|
+
'width',
|
|
227
|
+
'directed',
|
|
228
|
+
'animation',
|
|
229
|
+
'label',
|
|
230
|
+
],
|
|
171
231
|
edgeComponentAnimation: ['type', 'duration', 'color'],
|
|
172
232
|
edgeComponentLabel: ['field', 'position'],
|
|
173
233
|
connectionRule: ['from', 'to', 'via', 'constraints'],
|
|
@@ -323,7 +383,11 @@ function validateCanvas(canvas, filePath, library) {
|
|
|
323
383
|
else {
|
|
324
384
|
c.nodes.forEach((node, index) => {
|
|
325
385
|
if (!node || typeof node !== 'object') {
|
|
326
|
-
issues.push({
|
|
386
|
+
issues.push({
|
|
387
|
+
type: 'error',
|
|
388
|
+
message: `Node at index ${index} must be an object`,
|
|
389
|
+
path: `nodes[${index}]`,
|
|
390
|
+
});
|
|
327
391
|
return;
|
|
328
392
|
}
|
|
329
393
|
const n = node;
|
|
@@ -347,23 +411,72 @@ function validateCanvas(canvas, filePath, library) {
|
|
|
347
411
|
// Custom types can have any base fields
|
|
348
412
|
checkUnknownFields(n, allowedNodeFields, nodePath, issues);
|
|
349
413
|
if (typeof n.id !== 'string' || !n.id) {
|
|
350
|
-
issues.push({
|
|
414
|
+
issues.push({
|
|
415
|
+
type: 'error',
|
|
416
|
+
message: `Node at index ${index} must have a string "id"`,
|
|
417
|
+
path: `${nodePath}.id`,
|
|
418
|
+
});
|
|
351
419
|
}
|
|
352
420
|
if (typeof n.type !== 'string') {
|
|
353
|
-
issues.push({
|
|
421
|
+
issues.push({
|
|
422
|
+
type: 'error',
|
|
423
|
+
message: `Node "${nodeLabel}" must have a string "type"`,
|
|
424
|
+
path: `${nodePath}.type`,
|
|
425
|
+
});
|
|
354
426
|
}
|
|
355
427
|
if (typeof n.x !== 'number') {
|
|
356
|
-
issues.push({
|
|
428
|
+
issues.push({
|
|
429
|
+
type: 'error',
|
|
430
|
+
message: `Node "${nodeLabel}" must have a numeric "x" position`,
|
|
431
|
+
path: `${nodePath}.x`,
|
|
432
|
+
});
|
|
357
433
|
}
|
|
358
434
|
if (typeof n.y !== 'number') {
|
|
359
|
-
issues.push({
|
|
435
|
+
issues.push({
|
|
436
|
+
type: 'error',
|
|
437
|
+
message: `Node "${nodeLabel}" must have a numeric "y" position`,
|
|
438
|
+
path: `${nodePath}.y`,
|
|
439
|
+
});
|
|
360
440
|
}
|
|
361
441
|
// Width and height are now REQUIRED (was warning)
|
|
362
442
|
if (typeof n.width !== 'number') {
|
|
363
|
-
issues.push({
|
|
443
|
+
issues.push({
|
|
444
|
+
type: 'error',
|
|
445
|
+
message: `Node "${nodeLabel}" must have a numeric "width"`,
|
|
446
|
+
path: `${nodePath}.width`,
|
|
447
|
+
});
|
|
364
448
|
}
|
|
365
449
|
if (typeof n.height !== 'number') {
|
|
366
|
-
issues.push({
|
|
450
|
+
issues.push({
|
|
451
|
+
type: 'error',
|
|
452
|
+
message: `Node "${nodeLabel}" must have a numeric "height"`,
|
|
453
|
+
path: `${nodePath}.height`,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
// Validate required fields for standard canvas types
|
|
457
|
+
if (nodeType === 'text' && (typeof n.text !== 'string' || !n.text)) {
|
|
458
|
+
issues.push({
|
|
459
|
+
type: 'error',
|
|
460
|
+
message: `Node "${nodeLabel}" has type "text" but is missing required "text" field`,
|
|
461
|
+
path: `${nodePath}.text`,
|
|
462
|
+
suggestion: 'Add a "text" field with markdown content, or change the node type',
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
if (nodeType === 'file' && (typeof n.file !== 'string' || !n.file)) {
|
|
466
|
+
issues.push({
|
|
467
|
+
type: 'error',
|
|
468
|
+
message: `Node "${nodeLabel}" has type "file" but is missing required "file" field`,
|
|
469
|
+
path: `${nodePath}.file`,
|
|
470
|
+
suggestion: 'Add a "file" field with a file path, or change the node type',
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
if (nodeType === 'link' && (typeof n.url !== 'string' || !n.url)) {
|
|
474
|
+
issues.push({
|
|
475
|
+
type: 'error',
|
|
476
|
+
message: `Node "${nodeLabel}" has type "link" but is missing required "url" field`,
|
|
477
|
+
path: `${nodePath}.url`,
|
|
478
|
+
suggestion: 'Add a "url" field with a URL, or change the node type',
|
|
479
|
+
});
|
|
367
480
|
}
|
|
368
481
|
// Validate node type - must be standard canvas type OR have pv metadata
|
|
369
482
|
const isStandardType = STANDARD_CANVAS_TYPES.includes(nodeType);
|
|
@@ -386,7 +499,8 @@ function validateCanvas(canvas, filePath, library) {
|
|
|
386
499
|
path: `nodes[${index}].pv.nodeType`,
|
|
387
500
|
});
|
|
388
501
|
}
|
|
389
|
-
if (typeof nodePv.shape !== 'string' ||
|
|
502
|
+
if (typeof nodePv.shape !== 'string' ||
|
|
503
|
+
!VALID_NODE_SHAPES.includes(nodePv.shape)) {
|
|
390
504
|
issues.push({
|
|
391
505
|
type: 'error',
|
|
392
506
|
message: `Node "${n.id || index}" must have a valid "pv.shape"`,
|
|
@@ -464,10 +578,14 @@ function validateCanvas(canvas, filePath, library) {
|
|
|
464
578
|
issues.push({ type: 'error', message: '"edges" must be an array if present' });
|
|
465
579
|
}
|
|
466
580
|
else if (Array.isArray(c.edges)) {
|
|
467
|
-
const nodeIds = new Set(c.nodes?.map(n => n.id) || []);
|
|
581
|
+
const nodeIds = new Set(c.nodes?.map((n) => n.id) || []);
|
|
468
582
|
c.edges.forEach((edge, index) => {
|
|
469
583
|
if (!edge || typeof edge !== 'object') {
|
|
470
|
-
issues.push({
|
|
584
|
+
issues.push({
|
|
585
|
+
type: 'error',
|
|
586
|
+
message: `Edge at index ${index} must be an object`,
|
|
587
|
+
path: `edges[${index}]`,
|
|
588
|
+
});
|
|
471
589
|
return;
|
|
472
590
|
}
|
|
473
591
|
const e = edge;
|
|
@@ -476,19 +594,39 @@ function validateCanvas(canvas, filePath, library) {
|
|
|
476
594
|
// Check unknown fields on edge
|
|
477
595
|
checkUnknownFields(e, ALLOWED_CANVAS_FIELDS.edge, edgePath, issues);
|
|
478
596
|
if (typeof e.id !== 'string' || !e.id) {
|
|
479
|
-
issues.push({
|
|
597
|
+
issues.push({
|
|
598
|
+
type: 'error',
|
|
599
|
+
message: `Edge at index ${index} must have a string "id"`,
|
|
600
|
+
path: `${edgePath}.id`,
|
|
601
|
+
});
|
|
480
602
|
}
|
|
481
603
|
if (typeof e.fromNode !== 'string') {
|
|
482
|
-
issues.push({
|
|
604
|
+
issues.push({
|
|
605
|
+
type: 'error',
|
|
606
|
+
message: `Edge "${edgeLabel}" must have a string "fromNode"`,
|
|
607
|
+
path: `${edgePath}.fromNode`,
|
|
608
|
+
});
|
|
483
609
|
}
|
|
484
610
|
else if (!nodeIds.has(e.fromNode)) {
|
|
485
|
-
issues.push({
|
|
611
|
+
issues.push({
|
|
612
|
+
type: 'error',
|
|
613
|
+
message: `Edge "${edgeLabel}" references unknown node "${e.fromNode}"`,
|
|
614
|
+
path: `${edgePath}.fromNode`,
|
|
615
|
+
});
|
|
486
616
|
}
|
|
487
617
|
if (typeof e.toNode !== 'string') {
|
|
488
|
-
issues.push({
|
|
618
|
+
issues.push({
|
|
619
|
+
type: 'error',
|
|
620
|
+
message: `Edge "${edgeLabel}" must have a string "toNode"`,
|
|
621
|
+
path: `${edgePath}.toNode`,
|
|
622
|
+
});
|
|
489
623
|
}
|
|
490
624
|
else if (!nodeIds.has(e.toNode)) {
|
|
491
|
-
issues.push({
|
|
625
|
+
issues.push({
|
|
626
|
+
type: 'error',
|
|
627
|
+
message: `Edge "${edgeLabel}" references unknown node "${e.toNode}"`,
|
|
628
|
+
path: `${edgePath}.toNode`,
|
|
629
|
+
});
|
|
492
630
|
}
|
|
493
631
|
// Validate edge pv extension fields
|
|
494
632
|
if (e.pv && typeof e.pv === 'object') {
|
|
@@ -558,7 +696,7 @@ function validateFile(filePath, library) {
|
|
|
558
696
|
const content = readFileSync(absolutePath, 'utf8');
|
|
559
697
|
const canvas = JSON.parse(content);
|
|
560
698
|
const issues = validateCanvas(canvas, relativePath, library);
|
|
561
|
-
const hasErrors = issues.some(i => i.type === 'error');
|
|
699
|
+
const hasErrors = issues.some((i) => i.type === 'error');
|
|
562
700
|
return {
|
|
563
701
|
file: relativePath,
|
|
564
702
|
isValid: !hasErrors,
|
|
@@ -607,7 +745,7 @@ export function createValidateCommand() {
|
|
|
607
745
|
let libraryResult = null;
|
|
608
746
|
if (library && Object.keys(library.raw).length > 0) {
|
|
609
747
|
const libraryIssues = validateLibrary(library);
|
|
610
|
-
const libraryHasErrors = libraryIssues.some(i => i.type === 'error');
|
|
748
|
+
const libraryHasErrors = libraryIssues.some((i) => i.type === 'error');
|
|
611
749
|
libraryResult = {
|
|
612
750
|
file: relative(process.cwd(), library.path),
|
|
613
751
|
isValid: !libraryHasErrors,
|
|
@@ -615,10 +753,10 @@ export function createValidateCommand() {
|
|
|
615
753
|
};
|
|
616
754
|
}
|
|
617
755
|
// Validate all canvas files
|
|
618
|
-
const results = matchedFiles.map(f => validateFile(f, library));
|
|
756
|
+
const results = matchedFiles.map((f) => validateFile(f, library));
|
|
619
757
|
// Combine results
|
|
620
758
|
const allResults = libraryResult ? [libraryResult, ...results] : results;
|
|
621
|
-
const validCount = allResults.filter(r => r.isValid).length;
|
|
759
|
+
const validCount = allResults.filter((r) => r.isValid).length;
|
|
622
760
|
const invalidCount = allResults.length - validCount;
|
|
623
761
|
// Output results
|
|
624
762
|
if (options.json) {
|
|
@@ -629,16 +767,18 @@ export function createValidateCommand() {
|
|
|
629
767
|
}
|
|
630
768
|
else {
|
|
631
769
|
if (!options.quiet) {
|
|
632
|
-
const fileCount = libraryResult
|
|
770
|
+
const fileCount = libraryResult
|
|
771
|
+
? `${results.length} canvas file(s) + library`
|
|
772
|
+
: `${results.length} canvas file(s)`;
|
|
633
773
|
console.log(chalk.bold(`\nValidating ${fileCount}...\n`));
|
|
634
774
|
}
|
|
635
775
|
for (const result of allResults) {
|
|
636
776
|
if (result.isValid) {
|
|
637
777
|
if (!options.quiet) {
|
|
638
778
|
console.log(chalk.green(`✓ ${result.file}`));
|
|
639
|
-
const warnings = result.issues.filter(i => i.type === 'warning');
|
|
779
|
+
const warnings = result.issues.filter((i) => i.type === 'warning');
|
|
640
780
|
if (warnings.length > 0) {
|
|
641
|
-
warnings.forEach(w => {
|
|
781
|
+
warnings.forEach((w) => {
|
|
642
782
|
console.log(chalk.yellow(` ⚠ ${w.message}`));
|
|
643
783
|
});
|
|
644
784
|
}
|
|
@@ -646,7 +786,7 @@ export function createValidateCommand() {
|
|
|
646
786
|
}
|
|
647
787
|
else {
|
|
648
788
|
console.log(chalk.red(`✗ ${result.file}`));
|
|
649
|
-
result.issues.forEach(issue => {
|
|
789
|
+
result.issues.forEach((issue) => {
|
|
650
790
|
const icon = issue.type === 'error' ? '✗' : '⚠';
|
|
651
791
|
const color = issue.type === 'error' ? chalk.red : chalk.yellow;
|
|
652
792
|
console.log(color(` ${icon} ${issue.message}`));
|