@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,375 @@
1
+ /**
2
+ * Lint command - Lint graph configuration files using the rules engine
3
+ */
4
+ import { Command } from 'commander';
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { resolve, relative, dirname, basename } from 'node:path';
7
+ import chalk from 'chalk';
8
+ import { globby } from 'globby';
9
+ import yaml from 'js-yaml';
10
+ import { createDefaultRulesEngine, validatePrivuConfig, mergeConfigs, getDefaultConfig, } from '@principal-ai/principal-view-core';
11
+ // ============================================================================
12
+ // Config File Loading
13
+ // ============================================================================
14
+ /**
15
+ * Config file names in resolution order
16
+ */
17
+ const CONFIG_FILE_NAMES = [
18
+ '.principal-viewsrc.json',
19
+ '.principal-viewsrc.yaml',
20
+ '.principal-viewsrc.yml',
21
+ 'privu.config.json',
22
+ 'privu.config.yaml',
23
+ 'privu.config.yml',
24
+ ];
25
+ /**
26
+ * Find and load VGC config file
27
+ */
28
+ function findConfig(startDir) {
29
+ let currentDir = resolve(startDir);
30
+ while (true) {
31
+ // Check for config files
32
+ for (const fileName of CONFIG_FILE_NAMES) {
33
+ const configPath = resolve(currentDir, fileName);
34
+ if (existsSync(configPath)) {
35
+ const config = loadConfigFile(configPath);
36
+ if (config) {
37
+ return { config, path: configPath };
38
+ }
39
+ }
40
+ }
41
+ // Check package.json for "privu" key
42
+ const packageJsonPath = resolve(currentDir, 'package.json');
43
+ if (existsSync(packageJsonPath)) {
44
+ try {
45
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
46
+ if (packageJson.privu && typeof packageJson.privu === 'object') {
47
+ return { config: packageJson.privu, path: packageJsonPath };
48
+ }
49
+ }
50
+ catch {
51
+ // Ignore parse errors
52
+ }
53
+ }
54
+ // Check for root flag or filesystem root
55
+ const parentDir = dirname(currentDir);
56
+ if (parentDir === currentDir) {
57
+ break; // Reached filesystem root
58
+ }
59
+ currentDir = parentDir;
60
+ }
61
+ return null;
62
+ }
63
+ /**
64
+ * Load a config file (JSON or YAML)
65
+ */
66
+ function loadConfigFile(filePath) {
67
+ try {
68
+ const content = readFileSync(filePath, 'utf8');
69
+ const ext = filePath.toLowerCase();
70
+ if (ext.endsWith('.json')) {
71
+ return JSON.parse(content);
72
+ }
73
+ else {
74
+ return yaml.load(content);
75
+ }
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ /**
82
+ * Load a component library file
83
+ */
84
+ function loadLibrary(libraryPath) {
85
+ if (!existsSync(libraryPath)) {
86
+ return null;
87
+ }
88
+ try {
89
+ const content = readFileSync(libraryPath, 'utf8');
90
+ const ext = libraryPath.toLowerCase();
91
+ if (ext.endsWith('.json')) {
92
+ return JSON.parse(content);
93
+ }
94
+ else {
95
+ return yaml.load(content);
96
+ }
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ }
102
+ /**
103
+ * Load a graph configuration file (YAML or JSON)
104
+ */
105
+ function loadGraphConfig(filePath) {
106
+ if (!existsSync(filePath)) {
107
+ return null;
108
+ }
109
+ try {
110
+ const raw = readFileSync(filePath, 'utf8');
111
+ const ext = filePath.toLowerCase();
112
+ let config;
113
+ if (ext.endsWith('.json')) {
114
+ config = JSON.parse(raw);
115
+ }
116
+ else {
117
+ config = yaml.load(raw);
118
+ }
119
+ return { config, raw };
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ }
125
+ // ============================================================================
126
+ // Output Formatting
127
+ // ============================================================================
128
+ /**
129
+ * Format violations for pretty console output
130
+ */
131
+ function formatPrettyOutput(results, quiet) {
132
+ const lines = [];
133
+ let totalErrors = 0;
134
+ let totalWarnings = 0;
135
+ let totalFixable = 0;
136
+ for (const [filePath, result] of results) {
137
+ totalErrors += result.errorCount;
138
+ totalWarnings += result.warningCount;
139
+ totalFixable += result.fixableCount;
140
+ if (result.violations.length === 0) {
141
+ if (!quiet) {
142
+ lines.push(chalk.green(`✓ ${filePath}`));
143
+ }
144
+ continue;
145
+ }
146
+ lines.push('');
147
+ lines.push(chalk.cyan(filePath));
148
+ lines.push('');
149
+ for (const violation of result.violations) {
150
+ const severityColor = violation.severity === 'error' ? chalk.red : chalk.yellow;
151
+ const severityLabel = violation.severity === 'error' ? 'error' : 'warn ';
152
+ // Format: " error rule-id message"
153
+ const ruleId = chalk.dim(violation.ruleId.padEnd(30));
154
+ lines.push(` ${severityColor(severityLabel)} ${ruleId} ${violation.message}`);
155
+ if (violation.suggestion) {
156
+ lines.push(chalk.dim(` ${''.padEnd(30)} → ${violation.suggestion}`));
157
+ }
158
+ }
159
+ }
160
+ // Summary
161
+ lines.push('');
162
+ if (totalErrors === 0 && totalWarnings === 0) {
163
+ lines.push(chalk.green(`✓ All files passed linting`));
164
+ }
165
+ else {
166
+ const parts = [];
167
+ if (totalErrors > 0) {
168
+ parts.push(chalk.red(`${totalErrors} error${totalErrors === 1 ? '' : 's'}`));
169
+ }
170
+ if (totalWarnings > 0) {
171
+ parts.push(chalk.yellow(`${totalWarnings} warning${totalWarnings === 1 ? '' : 's'}`));
172
+ }
173
+ lines.push(`✖ ${parts.join(', ')}`);
174
+ if (totalFixable > 0) {
175
+ lines.push(chalk.dim(` ${totalFixable} fixable with --fix (coming soon)`));
176
+ }
177
+ }
178
+ return {
179
+ output: lines.join('\n'),
180
+ hasErrors: totalErrors > 0,
181
+ };
182
+ }
183
+ /**
184
+ * Format violations for JSON output
185
+ */
186
+ function formatJsonOutput(results) {
187
+ const files = [];
188
+ let totalErrors = 0;
189
+ let totalWarnings = 0;
190
+ let totalFixable = 0;
191
+ for (const [filePath, result] of results) {
192
+ totalErrors += result.errorCount;
193
+ totalWarnings += result.warningCount;
194
+ totalFixable += result.fixableCount;
195
+ files.push({
196
+ file: filePath,
197
+ errorCount: result.errorCount,
198
+ warningCount: result.warningCount,
199
+ fixableCount: result.fixableCount,
200
+ violations: result.violations,
201
+ });
202
+ }
203
+ return {
204
+ files,
205
+ summary: {
206
+ totalFiles: files.length,
207
+ totalErrors,
208
+ totalWarnings,
209
+ totalFixable,
210
+ },
211
+ };
212
+ }
213
+ // ============================================================================
214
+ // Command Implementation
215
+ // ============================================================================
216
+ export function createLintCommand() {
217
+ const command = new Command('lint');
218
+ command
219
+ .description('Lint graph configuration files')
220
+ .argument('[files...]', 'Files or glob patterns to lint (defaults to .principal-views/**/*.yaml)')
221
+ .option('-c, --config <path>', 'Path to config file')
222
+ .option('--library <path>', 'Path to component library file')
223
+ .option('-q, --quiet', 'Only output errors')
224
+ .option('--json', 'Output results as JSON')
225
+ .option('--rule <rules...>', 'Only run specific rules')
226
+ .option('--ignore-rule <rules...>', 'Skip specific rules')
227
+ .action(async (files, options) => {
228
+ try {
229
+ const cwd = process.cwd();
230
+ // Load VGC config
231
+ let privuConfig = getDefaultConfig();
232
+ let configPath;
233
+ if (options.config) {
234
+ // Use specified config file
235
+ const loadedConfig = loadConfigFile(resolve(cwd, options.config));
236
+ if (!loadedConfig) {
237
+ console.error(chalk.red(`Error: Could not load config file: ${options.config}`));
238
+ process.exit(1);
239
+ }
240
+ // Validate config
241
+ const validation = validatePrivuConfig(loadedConfig);
242
+ if (!validation.valid) {
243
+ console.error(chalk.red('Configuration Error:'), options.config);
244
+ for (const error of validation.errors) {
245
+ console.error(chalk.red(` ${error.path}: ${error.message}`));
246
+ if (error.suggestion) {
247
+ console.error(chalk.dim(` → ${error.suggestion}`));
248
+ }
249
+ }
250
+ process.exit(1);
251
+ }
252
+ privuConfig = mergeConfigs(privuConfig, loadedConfig);
253
+ configPath = resolve(cwd, options.config);
254
+ }
255
+ else {
256
+ // Search for config file
257
+ const found = findConfig(cwd);
258
+ if (found) {
259
+ const validation = validatePrivuConfig(found.config);
260
+ if (!validation.valid) {
261
+ console.error(chalk.red('Configuration Error:'), found.path);
262
+ for (const error of validation.errors) {
263
+ console.error(chalk.red(` ${error.path}: ${error.message}`));
264
+ if (error.suggestion) {
265
+ console.error(chalk.dim(` → ${error.suggestion}`));
266
+ }
267
+ }
268
+ process.exit(1);
269
+ }
270
+ privuConfig = mergeConfigs(privuConfig, found.config);
271
+ configPath = found.path;
272
+ }
273
+ }
274
+ // Determine files to lint
275
+ let patterns;
276
+ if (files.length > 0) {
277
+ patterns = files;
278
+ }
279
+ else if (privuConfig.include && privuConfig.include.length > 0) {
280
+ patterns = privuConfig.include;
281
+ }
282
+ else {
283
+ patterns = ['.principal-views/**/*.yaml', '.principal-views/**/*.yml', '.principal-views/**/*.json'];
284
+ }
285
+ // Find matching files
286
+ const matchedFiles = await globby(patterns, {
287
+ ignore: privuConfig.exclude || ['**/node_modules/**'],
288
+ expandDirectories: false,
289
+ });
290
+ // Filter out library files and config files
291
+ const configFiles = matchedFiles.filter((f) => {
292
+ const name = basename(f).toLowerCase();
293
+ return !name.startsWith('library.') && !name.startsWith('.principal-viewsrc') && !name.startsWith('privu.config');
294
+ });
295
+ if (configFiles.length === 0) {
296
+ if (options.json) {
297
+ console.log(JSON.stringify({ files: [], summary: { totalFiles: 0, totalErrors: 0, totalWarnings: 0, totalFixable: 0 } }));
298
+ }
299
+ else {
300
+ console.log(chalk.yellow('No configuration files found matching the specified patterns.'));
301
+ console.log(chalk.dim(`Patterns searched: ${patterns.join(', ')}`));
302
+ }
303
+ return;
304
+ }
305
+ // Load library if specified
306
+ let library;
307
+ const libraryPath = options.library || privuConfig.library;
308
+ if (libraryPath) {
309
+ const resolvedLibraryPath = resolve(cwd, libraryPath);
310
+ library = loadLibrary(resolvedLibraryPath) ?? undefined;
311
+ if (!library && !options.quiet) {
312
+ console.log(chalk.yellow(`Warning: Could not load library from ${libraryPath}`));
313
+ }
314
+ }
315
+ // Create rules engine
316
+ const engine = createDefaultRulesEngine();
317
+ // Lint each file
318
+ const results = new Map();
319
+ for (const filePath of configFiles) {
320
+ const absolutePath = resolve(cwd, filePath);
321
+ const relativePath = relative(cwd, absolutePath);
322
+ const loaded = loadGraphConfig(absolutePath);
323
+ if (!loaded) {
324
+ // File couldn't be loaded - report as error
325
+ results.set(relativePath, {
326
+ violations: [{
327
+ ruleId: 'parse-error',
328
+ severity: 'error',
329
+ file: relativePath,
330
+ message: `Could not parse file: ${filePath}`,
331
+ impact: 'File cannot be validated',
332
+ fixable: false,
333
+ }],
334
+ errorCount: 1,
335
+ warningCount: 0,
336
+ fixableCount: 0,
337
+ byCategory: { schema: 1, reference: 0, structure: 0, pattern: 0, library: 0 },
338
+ byRule: { 'parse-error': 1 },
339
+ });
340
+ continue;
341
+ }
342
+ // Run linting
343
+ const result = await engine.lintWithConfig(loaded.config, privuConfig, {
344
+ library,
345
+ configPath: relativePath,
346
+ rawContent: loaded.raw,
347
+ enabledRules: options.rule,
348
+ disabledRules: options.ignoreRule,
349
+ });
350
+ results.set(relativePath, result);
351
+ }
352
+ // Output results
353
+ if (options.json) {
354
+ console.log(JSON.stringify(formatJsonOutput(results), null, 2));
355
+ }
356
+ else {
357
+ const { output, hasErrors } = formatPrettyOutput(results, options.quiet);
358
+ console.log(output);
359
+ if (hasErrors) {
360
+ process.exit(1);
361
+ }
362
+ }
363
+ }
364
+ catch (error) {
365
+ if (options.json) {
366
+ console.log(JSON.stringify({ error: error.message }));
367
+ }
368
+ else {
369
+ console.error(chalk.red('Error:'), error.message);
370
+ }
371
+ process.exit(1);
372
+ }
373
+ });
374
+ return command;
375
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * List command - List all .canvas files in a project
3
+ */
4
+ import { Command } from 'commander';
5
+ export declare function createListCommand(): Command;
6
+ //# sourceMappingURL=list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/commands/list.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmCpC,wBAAgB,iBAAiB,IAAI,OAAO,CAwD3C"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * List command - List all .canvas files in a project
3
+ */
4
+ import { Command } from 'commander';
5
+ import { readFileSync, statSync } from 'node:fs';
6
+ import { resolve, relative } from 'node:path';
7
+ import chalk from 'chalk';
8
+ import { globby } from 'globby';
9
+ function getCanvasInfo(filePath) {
10
+ try {
11
+ const absolutePath = resolve(filePath);
12
+ const content = readFileSync(absolutePath, 'utf8');
13
+ const canvas = JSON.parse(content);
14
+ const stats = statSync(absolutePath);
15
+ return {
16
+ file: relative(process.cwd(), absolutePath),
17
+ name: canvas.pv?.name || 'Untitled',
18
+ version: canvas.pv?.version,
19
+ nodeCount: Array.isArray(canvas.nodes) ? canvas.nodes.length : 0,
20
+ edgeCount: Array.isArray(canvas.edges) ? canvas.edges.length : 0,
21
+ modified: stats.mtime,
22
+ };
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ export function createListCommand() {
29
+ const command = new Command('list');
30
+ command
31
+ .alias('ls')
32
+ .description('List all .canvas files in the project')
33
+ .option('-a, --all', 'Search all directories (not just .principal-views)')
34
+ .option('--json', 'Output as JSON')
35
+ .action(async (options) => {
36
+ try {
37
+ const patterns = options.all
38
+ ? ['**/*.canvas', '!node_modules/**']
39
+ : ['.principal-views/*.canvas'];
40
+ const files = await globby(patterns, {
41
+ expandDirectories: false,
42
+ });
43
+ if (files.length === 0) {
44
+ if (options.json) {
45
+ console.log(JSON.stringify({ files: [] }));
46
+ }
47
+ else {
48
+ console.log(chalk.yellow('No .canvas files found.'));
49
+ if (!options.all) {
50
+ console.log(chalk.dim('Run with --all to search all directories'));
51
+ }
52
+ console.log(chalk.dim('\nTo create a new canvas, run: privu init'));
53
+ }
54
+ return;
55
+ }
56
+ const canvasInfos = files
57
+ .map(getCanvasInfo)
58
+ .filter((info) => info !== null)
59
+ .sort((a, b) => b.modified.getTime() - a.modified.getTime());
60
+ if (options.json) {
61
+ console.log(JSON.stringify({ files: canvasInfos }, null, 2));
62
+ }
63
+ else {
64
+ console.log(chalk.bold(`\nFound ${canvasInfos.length} canvas file(s):\n`));
65
+ for (const info of canvasInfos) {
66
+ const version = info.version ? chalk.dim(` v${info.version}`) : '';
67
+ console.log(` ${chalk.cyan(info.file)}`);
68
+ console.log(` ${chalk.bold(info.name)}${version}`);
69
+ console.log(chalk.dim(` ${info.nodeCount} nodes, ${info.edgeCount} edges`));
70
+ console.log('');
71
+ }
72
+ }
73
+ }
74
+ catch (error) {
75
+ console.error(chalk.red('Error:'), error.message);
76
+ process.exit(1);
77
+ }
78
+ });
79
+ return command;
80
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Schema command - Display documentation about the canvas format
3
+ */
4
+ import { Command } from 'commander';
5
+ export declare function createSchemaCommand(): Command;
6
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +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;AA2TpC,wBAAgB,mBAAmB,IAAI,OAAO,CA0B7C"}