@principal-ai/principal-view-cli 0.1.13

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.
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Validate command - Validate .canvas configuration files
3
+ */
4
+ import { Command } from 'commander';
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { resolve, relative } from 'node:path';
7
+ import chalk from 'chalk';
8
+ import { globby } from 'globby';
9
+ import yaml from 'js-yaml';
10
+ /**
11
+ * Load the library.yaml file from the .principal-views directory
12
+ */
13
+ function loadLibrary(principalViewsDir) {
14
+ const libraryFiles = ['library.yaml', 'library.yml', 'library.json'];
15
+ for (const fileName of libraryFiles) {
16
+ const libraryPath = resolve(principalViewsDir, fileName);
17
+ if (existsSync(libraryPath)) {
18
+ try {
19
+ const content = readFileSync(libraryPath, 'utf8');
20
+ const library = fileName.endsWith('.json')
21
+ ? JSON.parse(content)
22
+ : yaml.load(content);
23
+ if (library && typeof library === 'object') {
24
+ return {
25
+ nodeComponents: library.nodeComponents || {},
26
+ edgeComponents: library.edgeComponents || {},
27
+ };
28
+ }
29
+ }
30
+ catch {
31
+ // Library exists but failed to parse - return empty to avoid false positives
32
+ return { nodeComponents: {}, edgeComponents: {} };
33
+ }
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+ /**
39
+ * Standard JSON Canvas node types that don't require pv metadata
40
+ */
41
+ const STANDARD_CANVAS_TYPES = ['text', 'group', 'file', 'link'];
42
+ /**
43
+ * Valid node shapes for pv.shape
44
+ */
45
+ const VALID_NODE_SHAPES = ['circle', 'rectangle', 'hexagon', 'diamond', 'custom'];
46
+ /**
47
+ * Validate an ExtendedCanvas object with strict validation
48
+ *
49
+ * Strict validation ensures:
50
+ * - All required fields are present
51
+ * - Custom node types have proper pv metadata
52
+ * - Edge types reference defined types in pv.edgeTypes or library.edgeComponents
53
+ * - Node types reference defined types in pv.nodeTypes or library.nodeComponents
54
+ * - Canvas has pv extension with name and version
55
+ */
56
+ function validateCanvas(canvas, filePath, library) {
57
+ const issues = [];
58
+ if (!canvas || typeof canvas !== 'object') {
59
+ issues.push({ type: 'error', message: 'Canvas must be an object' });
60
+ return issues;
61
+ }
62
+ const c = canvas;
63
+ // Collect library-defined types
64
+ const libraryNodeTypes = library ? Object.keys(library.nodeComponents) : [];
65
+ const libraryEdgeTypes = library ? Object.keys(library.edgeComponents) : [];
66
+ // Check pv extension (REQUIRED for strict validation)
67
+ let canvasEdgeTypes = [];
68
+ let canvasNodeTypes = [];
69
+ if (c.pv === undefined) {
70
+ issues.push({
71
+ type: 'error',
72
+ message: 'Canvas must have a "pv" extension with name and version',
73
+ path: 'pv',
74
+ suggestion: 'Add: "pv": { "name": "My Graph", "version": "1.0.0" }',
75
+ });
76
+ }
77
+ else if (typeof c.pv !== 'object') {
78
+ issues.push({ type: 'error', message: '"pv" extension must be an object' });
79
+ }
80
+ else {
81
+ const pv = c.pv;
82
+ if (typeof pv.version !== 'string' || !pv.version) {
83
+ issues.push({
84
+ type: 'error',
85
+ message: 'pv.version is required',
86
+ path: 'pv.version',
87
+ suggestion: 'Add: "version": "1.0.0"',
88
+ });
89
+ }
90
+ if (typeof pv.name !== 'string' || !pv.name) {
91
+ issues.push({
92
+ type: 'error',
93
+ message: 'pv.name is required',
94
+ path: 'pv.name',
95
+ suggestion: 'Add: "name": "My Graph"',
96
+ });
97
+ }
98
+ // Collect defined edge types for later validation
99
+ if (pv.edgeTypes && typeof pv.edgeTypes === 'object') {
100
+ canvasEdgeTypes = Object.keys(pv.edgeTypes);
101
+ }
102
+ // Collect defined node types for later validation
103
+ if (pv.nodeTypes && typeof pv.nodeTypes === 'object') {
104
+ canvasNodeTypes = Object.keys(pv.nodeTypes);
105
+ }
106
+ }
107
+ // Combined types from canvas + library
108
+ const allDefinedNodeTypes = [...new Set([...canvasNodeTypes, ...libraryNodeTypes])];
109
+ const allDefinedEdgeTypes = [...new Set([...canvasEdgeTypes, ...libraryEdgeTypes])];
110
+ // Check nodes
111
+ if (!Array.isArray(c.nodes)) {
112
+ issues.push({ type: 'error', message: 'Canvas must have a "nodes" array' });
113
+ }
114
+ else {
115
+ c.nodes.forEach((node, index) => {
116
+ if (!node || typeof node !== 'object') {
117
+ issues.push({ type: 'error', message: `Node at index ${index} must be an object`, path: `nodes[${index}]` });
118
+ return;
119
+ }
120
+ const n = node;
121
+ if (typeof n.id !== 'string' || !n.id) {
122
+ issues.push({ type: 'error', message: `Node at index ${index} must have a string "id"`, path: `nodes[${index}].id` });
123
+ }
124
+ if (typeof n.type !== 'string') {
125
+ issues.push({ type: 'error', message: `Node "${n.id || index}" must have a string "type"`, path: `nodes[${index}].type` });
126
+ }
127
+ if (typeof n.x !== 'number') {
128
+ issues.push({ type: 'error', message: `Node "${n.id || index}" must have a numeric "x" position`, path: `nodes[${index}].x` });
129
+ }
130
+ if (typeof n.y !== 'number') {
131
+ issues.push({ type: 'error', message: `Node "${n.id || index}" must have a numeric "y" position`, path: `nodes[${index}].y` });
132
+ }
133
+ // Width and height are now REQUIRED (was warning)
134
+ if (typeof n.width !== 'number') {
135
+ issues.push({ type: 'error', message: `Node "${n.id || index}" must have a numeric "width"`, path: `nodes[${index}].width` });
136
+ }
137
+ if (typeof n.height !== 'number') {
138
+ issues.push({ type: 'error', message: `Node "${n.id || index}" must have a numeric "height"`, path: `nodes[${index}].height` });
139
+ }
140
+ // Validate node type - must be standard canvas type OR have pv metadata
141
+ const nodeType = n.type;
142
+ const isStandardType = STANDARD_CANVAS_TYPES.includes(nodeType);
143
+ if (!isStandardType) {
144
+ // Custom type - must have pv.nodeType with shape
145
+ if (!n.pv || typeof n.pv !== 'object') {
146
+ issues.push({
147
+ type: 'error',
148
+ message: `Node "${n.id || index}" uses custom type "${nodeType}" but has no "pv" extension`,
149
+ path: `nodes[${index}].pv`,
150
+ suggestion: `Use a standard type (${STANDARD_CANVAS_TYPES.join(', ')}) or add pv.nodeType and pv.shape`,
151
+ });
152
+ }
153
+ else {
154
+ const nodePv = n.pv;
155
+ if (typeof nodePv.nodeType !== 'string' || !nodePv.nodeType) {
156
+ issues.push({
157
+ type: 'error',
158
+ message: `Node "${n.id || index}" with custom type must have "pv.nodeType"`,
159
+ path: `nodes[${index}].pv.nodeType`,
160
+ });
161
+ }
162
+ if (typeof nodePv.shape !== 'string' || !VALID_NODE_SHAPES.includes(nodePv.shape)) {
163
+ issues.push({
164
+ type: 'error',
165
+ message: `Node "${n.id || index}" must have a valid "pv.shape"`,
166
+ path: `nodes[${index}].pv.shape`,
167
+ suggestion: `Valid shapes: ${VALID_NODE_SHAPES.join(', ')}`,
168
+ });
169
+ }
170
+ }
171
+ }
172
+ // Validate pv.nodeType references a defined nodeType (for any node with pv.nodeType)
173
+ if (n.pv && typeof n.pv === 'object') {
174
+ const nodePv = n.pv;
175
+ if (typeof nodePv.nodeType === 'string' && nodePv.nodeType) {
176
+ if (allDefinedNodeTypes.length === 0) {
177
+ issues.push({
178
+ type: 'error',
179
+ message: `Node "${n.id || index}" uses nodeType "${nodePv.nodeType}" but no node types are defined`,
180
+ path: `nodes[${index}].pv.nodeType`,
181
+ suggestion: 'Define node types in canvas pv.nodeTypes or library.yaml nodeComponents',
182
+ });
183
+ }
184
+ else if (!allDefinedNodeTypes.includes(nodePv.nodeType)) {
185
+ // Build a helpful suggestion showing where types can be defined
186
+ const sources = [];
187
+ if (canvasNodeTypes.length > 0) {
188
+ sources.push(`canvas pv.nodeTypes: ${canvasNodeTypes.join(', ')}`);
189
+ }
190
+ if (libraryNodeTypes.length > 0) {
191
+ sources.push(`library.yaml nodeComponents: ${libraryNodeTypes.join(', ')}`);
192
+ }
193
+ const suggestion = sources.length > 0
194
+ ? `Available types from ${sources.join(' | ')}`
195
+ : 'Define node types in canvas pv.nodeTypes or library.yaml nodeComponents';
196
+ issues.push({
197
+ type: 'error',
198
+ message: `Node "${n.id || index}" uses undefined nodeType "${nodePv.nodeType}"`,
199
+ path: `nodes[${index}].pv.nodeType`,
200
+ suggestion,
201
+ });
202
+ }
203
+ }
204
+ }
205
+ });
206
+ }
207
+ // Check edges (optional but validated strictly if present)
208
+ if (c.edges !== undefined && !Array.isArray(c.edges)) {
209
+ issues.push({ type: 'error', message: '"edges" must be an array if present' });
210
+ }
211
+ else if (Array.isArray(c.edges)) {
212
+ const nodeIds = new Set(c.nodes?.map(n => n.id) || []);
213
+ c.edges.forEach((edge, index) => {
214
+ if (!edge || typeof edge !== 'object') {
215
+ issues.push({ type: 'error', message: `Edge at index ${index} must be an object`, path: `edges[${index}]` });
216
+ return;
217
+ }
218
+ const e = edge;
219
+ if (typeof e.id !== 'string' || !e.id) {
220
+ issues.push({ type: 'error', message: `Edge at index ${index} must have a string "id"`, path: `edges[${index}].id` });
221
+ }
222
+ if (typeof e.fromNode !== 'string') {
223
+ issues.push({ type: 'error', message: `Edge "${e.id || index}" must have a string "fromNode"`, path: `edges[${index}].fromNode` });
224
+ }
225
+ else if (!nodeIds.has(e.fromNode)) {
226
+ issues.push({ type: 'error', message: `Edge "${e.id || index}" references unknown node "${e.fromNode}"`, path: `edges[${index}].fromNode` });
227
+ }
228
+ if (typeof e.toNode !== 'string') {
229
+ issues.push({ type: 'error', message: `Edge "${e.id || index}" must have a string "toNode"`, path: `edges[${index}].toNode` });
230
+ }
231
+ else if (!nodeIds.has(e.toNode)) {
232
+ issues.push({ type: 'error', message: `Edge "${e.id || index}" references unknown node "${e.toNode}"`, path: `edges[${index}].toNode` });
233
+ }
234
+ // Validate edge type if pv.edgeType is specified
235
+ if (e.pv && typeof e.pv === 'object') {
236
+ const edgePv = e.pv;
237
+ if (edgePv.edgeType && typeof edgePv.edgeType === 'string') {
238
+ if (allDefinedEdgeTypes.length === 0) {
239
+ issues.push({
240
+ type: 'error',
241
+ message: `Edge "${e.id || index}" uses edgeType "${edgePv.edgeType}" but no edge types are defined`,
242
+ path: `edges[${index}].pv.edgeType`,
243
+ suggestion: 'Define edge types in canvas pv.edgeTypes or library.yaml edgeComponents',
244
+ });
245
+ }
246
+ else if (!allDefinedEdgeTypes.includes(edgePv.edgeType)) {
247
+ // Build a helpful suggestion showing where types can be defined
248
+ const sources = [];
249
+ if (canvasEdgeTypes.length > 0) {
250
+ sources.push(`canvas pv.edgeTypes: ${canvasEdgeTypes.join(', ')}`);
251
+ }
252
+ if (libraryEdgeTypes.length > 0) {
253
+ sources.push(`library.yaml edgeComponents: ${libraryEdgeTypes.join(', ')}`);
254
+ }
255
+ const suggestion = sources.length > 0
256
+ ? `Available types from ${sources.join(' | ')}`
257
+ : 'Define edge types in canvas pv.edgeTypes or library.yaml edgeComponents';
258
+ issues.push({
259
+ type: 'error',
260
+ message: `Edge "${e.id || index}" uses undefined edgeType "${edgePv.edgeType}"`,
261
+ path: `edges[${index}].pv.edgeType`,
262
+ suggestion,
263
+ });
264
+ }
265
+ }
266
+ }
267
+ });
268
+ }
269
+ return issues;
270
+ }
271
+ /**
272
+ * Validate a single .canvas file
273
+ */
274
+ function validateFile(filePath, library) {
275
+ const absolutePath = resolve(filePath);
276
+ const relativePath = relative(process.cwd(), absolutePath);
277
+ if (!existsSync(absolutePath)) {
278
+ return {
279
+ file: relativePath,
280
+ isValid: false,
281
+ issues: [{ type: 'error', message: `File not found: ${filePath}` }],
282
+ };
283
+ }
284
+ try {
285
+ const content = readFileSync(absolutePath, 'utf8');
286
+ const canvas = JSON.parse(content);
287
+ const issues = validateCanvas(canvas, relativePath, library);
288
+ const hasErrors = issues.some(i => i.type === 'error');
289
+ return {
290
+ file: relativePath,
291
+ isValid: !hasErrors,
292
+ issues,
293
+ canvas: hasErrors ? undefined : canvas,
294
+ };
295
+ }
296
+ catch (error) {
297
+ return {
298
+ file: relativePath,
299
+ isValid: false,
300
+ issues: [{ type: 'error', message: `Failed to parse JSON: ${error.message}` }],
301
+ };
302
+ }
303
+ }
304
+ export function createValidateCommand() {
305
+ const command = new Command('validate');
306
+ command
307
+ .description('Validate .canvas configuration files')
308
+ .argument('[files...]', 'Files or glob patterns to validate (defaults to .principal-views/*.canvas)')
309
+ .option('-q, --quiet', 'Only output errors')
310
+ .option('--json', 'Output results as JSON')
311
+ .action(async (files, options) => {
312
+ try {
313
+ // Default to .principal-views/*.canvas if no files specified
314
+ const patterns = files.length > 0 ? files : ['.principal-views/*.canvas'];
315
+ // Find all matching files
316
+ const matchedFiles = await globby(patterns, {
317
+ expandDirectories: false,
318
+ });
319
+ if (matchedFiles.length === 0) {
320
+ if (options.json) {
321
+ console.log(JSON.stringify({ files: [], summary: { total: 0, valid: 0, invalid: 0 } }));
322
+ }
323
+ else {
324
+ console.log(chalk.yellow('No .canvas files found matching the specified patterns.'));
325
+ console.log(chalk.dim(`Patterns searched: ${patterns.join(', ')}`));
326
+ console.log(chalk.dim('\nTo create a new .principal-views folder, run: privu init'));
327
+ }
328
+ return;
329
+ }
330
+ // Load library from .principal-views directory (used for type validation)
331
+ const principalViewsDir = resolve(process.cwd(), '.principal-views');
332
+ const library = loadLibrary(principalViewsDir);
333
+ // Validate all files
334
+ const results = matchedFiles.map(f => validateFile(f, library));
335
+ const validCount = results.filter(r => r.isValid).length;
336
+ const invalidCount = results.length - validCount;
337
+ // Output results
338
+ if (options.json) {
339
+ console.log(JSON.stringify({
340
+ files: results,
341
+ summary: { total: results.length, valid: validCount, invalid: invalidCount },
342
+ }, null, 2));
343
+ }
344
+ else {
345
+ if (!options.quiet) {
346
+ console.log(chalk.bold(`\nValidating ${results.length} canvas file(s)...\n`));
347
+ }
348
+ for (const result of results) {
349
+ if (result.isValid) {
350
+ if (!options.quiet) {
351
+ console.log(chalk.green(`✓ ${result.file}`));
352
+ const warnings = result.issues.filter(i => i.type === 'warning');
353
+ if (warnings.length > 0) {
354
+ warnings.forEach(w => {
355
+ console.log(chalk.yellow(` ⚠ ${w.message}`));
356
+ });
357
+ }
358
+ }
359
+ }
360
+ else {
361
+ console.log(chalk.red(`✗ ${result.file}`));
362
+ result.issues.forEach(issue => {
363
+ const icon = issue.type === 'error' ? '✗' : '⚠';
364
+ const color = issue.type === 'error' ? chalk.red : chalk.yellow;
365
+ console.log(color(` ${icon} ${issue.message}`));
366
+ if (issue.suggestion) {
367
+ console.log(chalk.dim(` → ${issue.suggestion}`));
368
+ }
369
+ });
370
+ }
371
+ }
372
+ // Summary
373
+ console.log('');
374
+ if (invalidCount === 0) {
375
+ console.log(chalk.green(`✓ All ${validCount} file(s) are valid`));
376
+ }
377
+ else {
378
+ console.log(chalk.red(`✗ ${invalidCount} of ${results.length} file(s) failed validation`));
379
+ process.exit(1);
380
+ }
381
+ }
382
+ }
383
+ catch (error) {
384
+ console.error(chalk.red('Error:'), error.message);
385
+ process.exit(1);
386
+ }
387
+ });
388
+ return command;
389
+ }