@principal-ai/principal-view-cli 0.3.3 → 0.3.4
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/coverage.d.ts +9 -0
- package/dist/commands/coverage.d.ts.map +1 -0
- package/dist/commands/coverage.js +158 -0
- package/dist/commands/create.d.ts +6 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +50 -0
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +274 -0
- package/dist/commands/formats.d.ts +6 -0
- package/dist/commands/formats.d.ts.map +1 -0
- package/dist/commands/formats.js +475 -0
- package/dist/commands/hooks.d.ts +9 -0
- package/dist/commands/hooks.d.ts.map +1 -0
- package/dist/commands/hooks.js +295 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +271 -0
- package/dist/commands/lint.d.ts +6 -0
- package/dist/commands/lint.d.ts.map +1 -0
- package/dist/commands/lint.js +506 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +80 -0
- package/dist/commands/narrative/index.d.ts +3 -0
- package/dist/commands/narrative/index.d.ts.map +1 -0
- package/dist/commands/narrative/index.js +17 -0
- package/dist/commands/narrative/inspect.d.ts +3 -0
- package/dist/commands/narrative/inspect.d.ts.map +1 -0
- package/dist/commands/narrative/inspect.js +109 -0
- package/dist/commands/narrative/list.d.ts +3 -0
- package/dist/commands/narrative/list.d.ts.map +1 -0
- package/dist/commands/narrative/list.js +101 -0
- package/dist/commands/narrative/render.d.ts +3 -0
- package/dist/commands/narrative/render.d.ts.map +1 -0
- package/dist/commands/narrative/render.js +99 -0
- package/dist/commands/narrative/test.d.ts +3 -0
- package/dist/commands/narrative/test.d.ts.map +1 -0
- package/dist/commands/narrative/test.js +150 -0
- package/dist/commands/narrative/utils.d.ts +49 -0
- package/dist/commands/narrative/utils.d.ts.map +1 -0
- package/dist/commands/narrative/utils.js +164 -0
- package/dist/commands/narrative/validate.d.ts +3 -0
- package/dist/commands/narrative/validate.d.ts.map +1 -0
- package/dist/commands/narrative/validate.js +149 -0
- package/dist/commands/schema.d.ts +6 -0
- package/dist/commands/schema.d.ts.map +1 -0
- package/dist/commands/schema.js +336 -0
- package/dist/commands/validate-execution.d.ts +11 -0
- package/dist/commands/validate-execution.d.ts.map +1 -0
- package/dist/commands/validate-execution.js +223 -0
- package/dist/commands/validate.d.ts +6 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +1065 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/package.json +2 -2
|
@@ -0,0 +1,506 @@
|
|
|
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, createNarrativeValidator, } 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 = ['.privurc.yaml', '.privurc.yml', '.privurc.json'];
|
|
18
|
+
/**
|
|
19
|
+
* Find and load privurc config file
|
|
20
|
+
*/
|
|
21
|
+
function findConfig(startDir) {
|
|
22
|
+
let currentDir = resolve(startDir);
|
|
23
|
+
// eslint-disable-next-line no-constant-condition
|
|
24
|
+
while (true) {
|
|
25
|
+
// Check for config files
|
|
26
|
+
for (const fileName of CONFIG_FILE_NAMES) {
|
|
27
|
+
const configPath = resolve(currentDir, fileName);
|
|
28
|
+
if (existsSync(configPath)) {
|
|
29
|
+
const config = loadConfigFile(configPath);
|
|
30
|
+
if (config) {
|
|
31
|
+
return { config, path: configPath };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Check package.json for "privu" key
|
|
36
|
+
const packageJsonPath = resolve(currentDir, 'package.json');
|
|
37
|
+
if (existsSync(packageJsonPath)) {
|
|
38
|
+
try {
|
|
39
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
40
|
+
if (packageJson.privu && typeof packageJson.privu === 'object') {
|
|
41
|
+
return { config: packageJson.privu, path: packageJsonPath };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Ignore parse errors
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Check for root flag or filesystem root
|
|
49
|
+
const parentDir = dirname(currentDir);
|
|
50
|
+
if (parentDir === currentDir) {
|
|
51
|
+
break; // Reached filesystem root
|
|
52
|
+
}
|
|
53
|
+
currentDir = parentDir;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Load a config file (JSON or YAML)
|
|
59
|
+
*/
|
|
60
|
+
function loadConfigFile(filePath) {
|
|
61
|
+
try {
|
|
62
|
+
const content = readFileSync(filePath, 'utf8');
|
|
63
|
+
const ext = filePath.toLowerCase();
|
|
64
|
+
if (ext.endsWith('.json')) {
|
|
65
|
+
return JSON.parse(content);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
return yaml.load(content);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Load a component library file
|
|
77
|
+
*/
|
|
78
|
+
function loadLibrary(libraryPath) {
|
|
79
|
+
if (!existsSync(libraryPath)) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const content = readFileSync(libraryPath, 'utf8');
|
|
84
|
+
const ext = libraryPath.toLowerCase();
|
|
85
|
+
if (ext.endsWith('.json')) {
|
|
86
|
+
return JSON.parse(content);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
return yaml.load(content);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Load a graph configuration file (YAML or JSON)
|
|
98
|
+
*/
|
|
99
|
+
function loadGraphConfig(filePath) {
|
|
100
|
+
if (!existsSync(filePath)) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
105
|
+
const ext = filePath.toLowerCase();
|
|
106
|
+
let config;
|
|
107
|
+
if (ext.endsWith('.json')) {
|
|
108
|
+
config = JSON.parse(raw);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
config = yaml.load(raw);
|
|
112
|
+
}
|
|
113
|
+
return { config, raw };
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Load a narrative template file
|
|
121
|
+
*/
|
|
122
|
+
function loadNarrativeTemplate(filePath) {
|
|
123
|
+
if (!existsSync(filePath)) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
128
|
+
const narrative = JSON.parse(raw);
|
|
129
|
+
return { narrative, raw };
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Load a canvas file for narrative validation
|
|
137
|
+
*/
|
|
138
|
+
function loadCanvas(filePath) {
|
|
139
|
+
if (!existsSync(filePath)) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const content = readFileSync(filePath, 'utf8');
|
|
144
|
+
const ext = filePath.toLowerCase();
|
|
145
|
+
if (ext.endsWith('.json')) {
|
|
146
|
+
return JSON.parse(content);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
return yaml.load(content);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Determine file type
|
|
158
|
+
*/
|
|
159
|
+
function getFileType(filePath) {
|
|
160
|
+
const name = basename(filePath).toLowerCase();
|
|
161
|
+
if (name.endsWith('.narrative.json')) {
|
|
162
|
+
return 'narrative';
|
|
163
|
+
}
|
|
164
|
+
if (name.endsWith('.canvas') || name.endsWith('.otel.canvas')) {
|
|
165
|
+
return 'canvas';
|
|
166
|
+
}
|
|
167
|
+
return 'config';
|
|
168
|
+
}
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// Output Formatting
|
|
171
|
+
// ============================================================================
|
|
172
|
+
/**
|
|
173
|
+
* Format violations for pretty console output
|
|
174
|
+
*/
|
|
175
|
+
function formatPrettyOutput(results, quiet) {
|
|
176
|
+
const lines = [];
|
|
177
|
+
let totalErrors = 0;
|
|
178
|
+
let totalWarnings = 0;
|
|
179
|
+
let totalFixable = 0;
|
|
180
|
+
for (const [filePath, result] of results) {
|
|
181
|
+
totalErrors += result.errorCount;
|
|
182
|
+
totalWarnings += result.warningCount;
|
|
183
|
+
totalFixable += result.fixableCount;
|
|
184
|
+
if (result.violations.length === 0) {
|
|
185
|
+
if (!quiet) {
|
|
186
|
+
lines.push(chalk.green(`✓ ${filePath}`));
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
lines.push('');
|
|
191
|
+
lines.push(chalk.cyan(filePath));
|
|
192
|
+
lines.push('');
|
|
193
|
+
for (const violation of result.violations) {
|
|
194
|
+
const severityColor = violation.severity === 'error' ? chalk.red : chalk.yellow;
|
|
195
|
+
const severityLabel = violation.severity === 'error' ? 'error' : 'warn ';
|
|
196
|
+
// Format: " error rule-id message"
|
|
197
|
+
const ruleId = chalk.dim(violation.ruleId.padEnd(30));
|
|
198
|
+
lines.push(` ${severityColor(severityLabel)} ${ruleId} ${violation.message}`);
|
|
199
|
+
if (violation.suggestion) {
|
|
200
|
+
lines.push(chalk.dim(` ${''.padEnd(30)} → ${violation.suggestion}`));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Summary
|
|
205
|
+
lines.push('');
|
|
206
|
+
if (totalErrors === 0 && totalWarnings === 0) {
|
|
207
|
+
lines.push(chalk.green(`✓ All files passed linting`));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
const parts = [];
|
|
211
|
+
if (totalErrors > 0) {
|
|
212
|
+
parts.push(chalk.red(`${totalErrors} error${totalErrors === 1 ? '' : 's'}`));
|
|
213
|
+
}
|
|
214
|
+
if (totalWarnings > 0) {
|
|
215
|
+
parts.push(chalk.yellow(`${totalWarnings} warning${totalWarnings === 1 ? '' : 's'}`));
|
|
216
|
+
}
|
|
217
|
+
lines.push(`✖ ${parts.join(', ')}`);
|
|
218
|
+
if (totalFixable > 0) {
|
|
219
|
+
lines.push(chalk.dim(` ${totalFixable} fixable with --fix (coming soon)`));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
output: lines.join('\n'),
|
|
224
|
+
hasErrors: totalErrors > 0,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Format violations for JSON output
|
|
229
|
+
*/
|
|
230
|
+
function formatJsonOutput(results) {
|
|
231
|
+
const files = [];
|
|
232
|
+
let totalErrors = 0;
|
|
233
|
+
let totalWarnings = 0;
|
|
234
|
+
let totalFixable = 0;
|
|
235
|
+
for (const [filePath, result] of results) {
|
|
236
|
+
totalErrors += result.errorCount;
|
|
237
|
+
totalWarnings += result.warningCount;
|
|
238
|
+
totalFixable += result.fixableCount;
|
|
239
|
+
files.push({
|
|
240
|
+
file: filePath,
|
|
241
|
+
errorCount: result.errorCount,
|
|
242
|
+
warningCount: result.warningCount,
|
|
243
|
+
fixableCount: result.fixableCount,
|
|
244
|
+
violations: result.violations,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
files,
|
|
249
|
+
summary: {
|
|
250
|
+
totalFiles: files.length,
|
|
251
|
+
totalErrors,
|
|
252
|
+
totalWarnings,
|
|
253
|
+
totalFixable,
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
// ============================================================================
|
|
258
|
+
// Helper Functions
|
|
259
|
+
// ============================================================================
|
|
260
|
+
/**
|
|
261
|
+
* Count violations by rule ID
|
|
262
|
+
*/
|
|
263
|
+
function countByRule(violations) {
|
|
264
|
+
const counts = {};
|
|
265
|
+
for (const v of violations) {
|
|
266
|
+
counts[v.ruleId] = (counts[v.ruleId] || 0) + 1;
|
|
267
|
+
}
|
|
268
|
+
return counts;
|
|
269
|
+
}
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// Command Implementation
|
|
272
|
+
// ============================================================================
|
|
273
|
+
export function createLintCommand() {
|
|
274
|
+
const command = new Command('lint');
|
|
275
|
+
command
|
|
276
|
+
.description('Lint graph configuration files')
|
|
277
|
+
.argument('[files...]', 'Files or glob patterns to lint (defaults to .principal-views/**/*.yaml)')
|
|
278
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
279
|
+
.option('--library <path>', 'Path to component library file')
|
|
280
|
+
.option('-q, --quiet', 'Only output errors')
|
|
281
|
+
.option('--json', 'Output results as JSON')
|
|
282
|
+
.option('--rule <rules...>', 'Only run specific rules')
|
|
283
|
+
.option('--ignore-rule <rules...>', 'Skip specific rules')
|
|
284
|
+
.action(async (files, options) => {
|
|
285
|
+
try {
|
|
286
|
+
const cwd = process.cwd();
|
|
287
|
+
// Load privurc config
|
|
288
|
+
let privuConfig = getDefaultConfig();
|
|
289
|
+
if (options.config) {
|
|
290
|
+
// Use specified config file
|
|
291
|
+
const loadedConfig = loadConfigFile(resolve(cwd, options.config));
|
|
292
|
+
if (!loadedConfig) {
|
|
293
|
+
console.error(chalk.red(`Error: Could not load config file: ${options.config}`));
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
// Validate config
|
|
297
|
+
const validation = validatePrivuConfig(loadedConfig);
|
|
298
|
+
if (!validation.valid) {
|
|
299
|
+
console.error(chalk.red('Configuration Error:'), options.config);
|
|
300
|
+
for (const error of validation.errors) {
|
|
301
|
+
console.error(chalk.red(` ${error.path}: ${error.message}`));
|
|
302
|
+
if (error.suggestion) {
|
|
303
|
+
console.error(chalk.dim(` → ${error.suggestion}`));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
privuConfig = mergeConfigs(privuConfig, loadedConfig);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
// Search for config file
|
|
312
|
+
const found = findConfig(cwd);
|
|
313
|
+
if (found) {
|
|
314
|
+
const validation = validatePrivuConfig(found.config);
|
|
315
|
+
if (!validation.valid) {
|
|
316
|
+
console.error(chalk.red('Configuration Error:'), found.path);
|
|
317
|
+
for (const error of validation.errors) {
|
|
318
|
+
console.error(chalk.red(` ${error.path}: ${error.message}`));
|
|
319
|
+
if (error.suggestion) {
|
|
320
|
+
console.error(chalk.dim(` → ${error.suggestion}`));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
privuConfig = mergeConfigs(privuConfig, found.config);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// Determine files to lint
|
|
329
|
+
let patterns;
|
|
330
|
+
if (files.length > 0) {
|
|
331
|
+
patterns = files;
|
|
332
|
+
}
|
|
333
|
+
else if (privuConfig.include && privuConfig.include.length > 0) {
|
|
334
|
+
patterns = privuConfig.include;
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
patterns = [
|
|
338
|
+
'.principal-views/**/*.yaml',
|
|
339
|
+
'.principal-views/**/*.yml',
|
|
340
|
+
'.principal-views/**/*.json',
|
|
341
|
+
];
|
|
342
|
+
}
|
|
343
|
+
// Find matching files
|
|
344
|
+
const matchedFiles = await globby(patterns, {
|
|
345
|
+
ignore: privuConfig.exclude || ['**/node_modules/**'],
|
|
346
|
+
expandDirectories: false,
|
|
347
|
+
});
|
|
348
|
+
// Filter out library files, config files, and execution artifacts
|
|
349
|
+
// INCLUDE both canvas files and narrative templates for linting
|
|
350
|
+
const configFiles = matchedFiles.filter((f) => {
|
|
351
|
+
const name = basename(f).toLowerCase();
|
|
352
|
+
const isLibraryFile = name.startsWith('library.');
|
|
353
|
+
const isConfigFile = name.startsWith('.privurc');
|
|
354
|
+
const isExecutionArtifact = f.includes('__executions__/');
|
|
355
|
+
return !isLibraryFile && !isConfigFile && !isExecutionArtifact;
|
|
356
|
+
});
|
|
357
|
+
if (configFiles.length === 0) {
|
|
358
|
+
if (options.json) {
|
|
359
|
+
console.log(JSON.stringify({
|
|
360
|
+
files: [],
|
|
361
|
+
summary: { totalFiles: 0, totalErrors: 0, totalWarnings: 0, totalFixable: 0 },
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
console.log(chalk.yellow('No configuration files found matching the specified patterns.'));
|
|
366
|
+
console.log(chalk.dim(`Patterns searched: ${patterns.join(', ')}`));
|
|
367
|
+
}
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
// Load library if specified
|
|
371
|
+
let library;
|
|
372
|
+
const libraryPath = options.library || privuConfig.library;
|
|
373
|
+
if (libraryPath) {
|
|
374
|
+
const resolvedLibraryPath = resolve(cwd, libraryPath);
|
|
375
|
+
library = loadLibrary(resolvedLibraryPath) ?? undefined;
|
|
376
|
+
if (!library && !options.quiet) {
|
|
377
|
+
console.log(chalk.yellow(`Warning: Could not load library from ${libraryPath}`));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Create validators
|
|
381
|
+
const engine = createDefaultRulesEngine();
|
|
382
|
+
const narrativeValidator = createNarrativeValidator();
|
|
383
|
+
// Lint each file
|
|
384
|
+
const results = new Map();
|
|
385
|
+
for (const filePath of configFiles) {
|
|
386
|
+
const absolutePath = resolve(cwd, filePath);
|
|
387
|
+
const relativePath = relative(cwd, absolutePath);
|
|
388
|
+
const fileType = getFileType(absolutePath);
|
|
389
|
+
if (fileType === 'narrative') {
|
|
390
|
+
// Validate narrative template
|
|
391
|
+
const loaded = loadNarrativeTemplate(absolutePath);
|
|
392
|
+
if (!loaded) {
|
|
393
|
+
// File couldn't be loaded - report as error
|
|
394
|
+
results.set(relativePath, {
|
|
395
|
+
violations: [
|
|
396
|
+
{
|
|
397
|
+
ruleId: 'parse-error',
|
|
398
|
+
severity: 'error',
|
|
399
|
+
file: relativePath,
|
|
400
|
+
message: `Could not parse narrative file: ${filePath}`,
|
|
401
|
+
impact: 'File cannot be validated',
|
|
402
|
+
fixable: false,
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
errorCount: 1,
|
|
406
|
+
warningCount: 0,
|
|
407
|
+
fixableCount: 0,
|
|
408
|
+
byCategory: { schema: 1, reference: 0, structure: 0, pattern: 0, library: 0 },
|
|
409
|
+
byRule: { 'parse-error': 1 },
|
|
410
|
+
});
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
// Load the referenced canvas if it exists
|
|
414
|
+
const canvasPath = loaded.narrative.canvas
|
|
415
|
+
? resolve(dirname(absolutePath), loaded.narrative.canvas)
|
|
416
|
+
: undefined;
|
|
417
|
+
const canvas = canvasPath ? loadCanvas(canvasPath) : undefined;
|
|
418
|
+
// Run narrative validation
|
|
419
|
+
const narrativeResult = await narrativeValidator.validate({
|
|
420
|
+
narrative: loaded.narrative,
|
|
421
|
+
narrativePath: relativePath,
|
|
422
|
+
canvas: canvas ?? undefined,
|
|
423
|
+
canvasPath: canvasPath ? relative(cwd, canvasPath) : undefined,
|
|
424
|
+
basePath: dirname(absolutePath),
|
|
425
|
+
rawContent: loaded.raw,
|
|
426
|
+
});
|
|
427
|
+
// Convert narrative violations to graph violations format
|
|
428
|
+
const violations = narrativeResult.violations.map((v) => ({
|
|
429
|
+
ruleId: v.ruleId,
|
|
430
|
+
severity: v.severity,
|
|
431
|
+
file: v.file,
|
|
432
|
+
line: v.line,
|
|
433
|
+
path: v.path,
|
|
434
|
+
message: v.message,
|
|
435
|
+
impact: v.impact,
|
|
436
|
+
suggestion: v.suggestion,
|
|
437
|
+
fixable: v.fixable,
|
|
438
|
+
}));
|
|
439
|
+
results.set(relativePath, {
|
|
440
|
+
violations,
|
|
441
|
+
errorCount: narrativeResult.errorCount,
|
|
442
|
+
warningCount: narrativeResult.warningCount,
|
|
443
|
+
fixableCount: narrativeResult.fixableCount,
|
|
444
|
+
byCategory: { schema: 0, reference: 0, structure: 0, pattern: 0, library: 0 }, // Could categorize narrative rules
|
|
445
|
+
byRule: countByRule(violations),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
// Validate canvas/graph configuration
|
|
450
|
+
const loaded = loadGraphConfig(absolutePath);
|
|
451
|
+
if (!loaded) {
|
|
452
|
+
// File couldn't be loaded - report as error
|
|
453
|
+
results.set(relativePath, {
|
|
454
|
+
violations: [
|
|
455
|
+
{
|
|
456
|
+
ruleId: 'parse-error',
|
|
457
|
+
severity: 'error',
|
|
458
|
+
file: relativePath,
|
|
459
|
+
message: `Could not parse file: ${filePath}`,
|
|
460
|
+
impact: 'File cannot be validated',
|
|
461
|
+
fixable: false,
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
errorCount: 1,
|
|
465
|
+
warningCount: 0,
|
|
466
|
+
fixableCount: 0,
|
|
467
|
+
byCategory: { schema: 1, reference: 0, structure: 0, pattern: 0, library: 0 },
|
|
468
|
+
byRule: { 'parse-error': 1 },
|
|
469
|
+
});
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
// Run linting
|
|
473
|
+
const result = await engine.lintWithConfig(loaded.config, privuConfig, {
|
|
474
|
+
library,
|
|
475
|
+
configPath: relativePath,
|
|
476
|
+
rawContent: loaded.raw,
|
|
477
|
+
enabledRules: options.rule,
|
|
478
|
+
disabledRules: options.ignoreRule,
|
|
479
|
+
});
|
|
480
|
+
results.set(relativePath, result);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// Output results
|
|
484
|
+
if (options.json) {
|
|
485
|
+
console.log(JSON.stringify(formatJsonOutput(results), null, 2));
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
const { output, hasErrors } = formatPrettyOutput(results, options.quiet);
|
|
489
|
+
console.log(output);
|
|
490
|
+
if (hasErrors) {
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
if (options.json) {
|
|
497
|
+
console.log(JSON.stringify({ error: error.message }));
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
console.error(chalk.red('Error:'), error.message);
|
|
501
|
+
}
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
return command;
|
|
506
|
+
}
|
|
@@ -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: npx @principal-ai/principal-view-cli 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 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/narrative/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,wBAAgB,sBAAsB,IAAI,OAAO,CAYhD"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createValidateCommand } from './validate.js';
|
|
3
|
+
import { createInspectCommand } from './inspect.js';
|
|
4
|
+
import { createRenderCommand } from './render.js';
|
|
5
|
+
import { createTestCommand } from './test.js';
|
|
6
|
+
import { createListCommand } from './list.js';
|
|
7
|
+
export function createNarrativeCommand() {
|
|
8
|
+
const command = new Command('narrative');
|
|
9
|
+
command
|
|
10
|
+
.description('Validate, test, and debug narrative templates')
|
|
11
|
+
.addCommand(createValidateCommand())
|
|
12
|
+
.addCommand(createInspectCommand())
|
|
13
|
+
.addCommand(createRenderCommand())
|
|
14
|
+
.addCommand(createTestCommand())
|
|
15
|
+
.addCommand(createListCommand());
|
|
16
|
+
return command;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../../src/commands/narrative/inspect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuBpC,wBAAgB,oBAAoB,IAAI,OAAO,CAiI9C"}
|