@principal-ai/principal-view-cli 0.3.2 ā 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.cjs +249853 -0
- package/dist/index.cjs.map +7 -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,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage command - Measure telemetry coverage for canvas nodes
|
|
3
|
+
*
|
|
4
|
+
* This command analyzes .otel.canvas files and checks which nodes have
|
|
5
|
+
* OpenTelemetry instrumentation in their linked source files.
|
|
6
|
+
*/
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
export declare function createCoverageCommand(): Command;
|
|
9
|
+
//# sourceMappingURL=coverage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../../src/commands/coverage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgIpC,wBAAgB,qBAAqB,IAAI,OAAO,CAiD/C"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage command - Measure telemetry coverage for canvas nodes
|
|
3
|
+
*
|
|
4
|
+
* This command analyzes .otel.canvas files and checks which nodes have
|
|
5
|
+
* OpenTelemetry instrumentation in their linked source files.
|
|
6
|
+
*/
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { resolve } from 'node:path';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { analyzeCoverage } from '@principal-ai/principal-view-core';
|
|
11
|
+
/**
|
|
12
|
+
* Print coverage report in human-readable format
|
|
13
|
+
*/
|
|
14
|
+
function printCoverageReport(metrics, rootDir, options) {
|
|
15
|
+
const { verbose } = options;
|
|
16
|
+
console.log(chalk.bold('\n' + 'ā'.repeat(70)));
|
|
17
|
+
console.log(chalk.bold('š TELEMETRY COVERAGE REPORT'));
|
|
18
|
+
console.log(chalk.bold('ā'.repeat(70)));
|
|
19
|
+
// Canvas files
|
|
20
|
+
console.log(chalk.bold(`\nš Canvas Files Analyzed: ${metrics.canvasFiles.length}`));
|
|
21
|
+
if (verbose) {
|
|
22
|
+
metrics.canvasFiles.forEach(f => console.log(chalk.dim(` - ${f}`)));
|
|
23
|
+
}
|
|
24
|
+
// Node summary
|
|
25
|
+
console.log(chalk.bold('\nšÆ Node Summary:'));
|
|
26
|
+
console.log(` Total nodes: ${metrics.totalNodes}`);
|
|
27
|
+
console.log(` Nodes with anchors: ${chalk.cyan(metrics.nodesWithFiles.toString())}`);
|
|
28
|
+
console.log(` Nodes without anchors: ${chalk.dim(metrics.totalNodes - metrics.nodesWithFiles)}`);
|
|
29
|
+
console.log(` Nodes with instrumentation: ${chalk.green(metrics.nodesWithInstrumentation.toString())}`);
|
|
30
|
+
const coverageColor = metrics.coveragePercentage >= 80
|
|
31
|
+
? chalk.green
|
|
32
|
+
: metrics.coveragePercentage >= 50
|
|
33
|
+
? chalk.yellow
|
|
34
|
+
: chalk.red;
|
|
35
|
+
console.log(` Coverage: ${coverageColor(metrics.coveragePercentage.toFixed(1) + '%')}`);
|
|
36
|
+
// Nodes WITHOUT anchors
|
|
37
|
+
const noAnchors = metrics.nodeCoverage.filter(n => n.filePaths.length === 0);
|
|
38
|
+
if (noAnchors.length > 0 && verbose) {
|
|
39
|
+
console.log(chalk.yellow(`\nā ļø Nodes Without Anchors (${noAnchors.length}):`));
|
|
40
|
+
const display = verbose ? noAnchors : noAnchors.slice(0, 10);
|
|
41
|
+
display.forEach(node => {
|
|
42
|
+
console.log(chalk.dim(` - ${node.nodeId}`));
|
|
43
|
+
});
|
|
44
|
+
if (!verbose && noAnchors.length > 10) {
|
|
45
|
+
console.log(chalk.dim(` ... and ${noAnchors.length - 10} more (use --verbose to see all)`));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Nodes WITHOUT instrumentation
|
|
49
|
+
const uninstrumented = metrics.nodeCoverage.filter(n => n.filePaths.length > 0 && !n.hasInstrumentation);
|
|
50
|
+
if (uninstrumented.length > 0) {
|
|
51
|
+
console.log(chalk.red(`\nā Nodes Missing Instrumentation (${uninstrumented.length}):`));
|
|
52
|
+
const display = verbose ? uninstrumented : uninstrumented.slice(0, 5);
|
|
53
|
+
display.forEach(node => {
|
|
54
|
+
console.log(chalk.yellow(`\n Node: ${node.nodeId}`));
|
|
55
|
+
console.log(chalk.dim(` Files: ${node.filePaths.join(', ')}`));
|
|
56
|
+
});
|
|
57
|
+
if (!verbose && uninstrumented.length > 5) {
|
|
58
|
+
console.log(chalk.dim(`\n ... and ${uninstrumented.length - 5} more (use --verbose to see all)`));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Nodes WITH instrumentation
|
|
62
|
+
const instrumented = metrics.nodeCoverage.filter(n => n.hasInstrumentation);
|
|
63
|
+
if (instrumented.length > 0) {
|
|
64
|
+
console.log(chalk.green(`\nā
Nodes With Instrumentation (${instrumented.length}):`));
|
|
65
|
+
const display = verbose ? instrumented : instrumented.slice(0, 5);
|
|
66
|
+
display.forEach(node => {
|
|
67
|
+
console.log(chalk.dim(` - ${node.nodeId}: ${node.instrumentedFiles.join(', ')}`));
|
|
68
|
+
});
|
|
69
|
+
if (!verbose && instrumented.length > 5) {
|
|
70
|
+
console.log(chalk.dim(` ... and ${instrumented.length - 5} more (use --verbose to see all)`));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Nodes with missing files
|
|
74
|
+
const withMissingFiles = metrics.nodeCoverage.filter(n => n.missingFiles.length > 0);
|
|
75
|
+
if (withMissingFiles.length > 0) {
|
|
76
|
+
console.log(chalk.yellow(`\nā ļø Nodes Referencing Missing Files (${withMissingFiles.length}):`));
|
|
77
|
+
const display = verbose ? withMissingFiles : withMissingFiles.slice(0, 5);
|
|
78
|
+
display.forEach(node => {
|
|
79
|
+
console.log(chalk.dim(` - ${node.nodeId}: ${node.missingFiles.join(', ')}`));
|
|
80
|
+
});
|
|
81
|
+
if (!verbose && withMissingFiles.length > 5) {
|
|
82
|
+
console.log(chalk.dim(` ... and ${withMissingFiles.length - 5} more (use --verbose to see all)`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Recommendations
|
|
86
|
+
console.log(chalk.bold('\nš” Recommendations:'));
|
|
87
|
+
if (noAnchors.length > 0) {
|
|
88
|
+
console.log(chalk.red(' š“ Add anchors to canvas nodes before measuring coverage'));
|
|
89
|
+
}
|
|
90
|
+
else if (metrics.coveragePercentage < 30) {
|
|
91
|
+
console.log(chalk.red(' š“ Low coverage - add OTEL instrumentation to files referenced in canvas'));
|
|
92
|
+
}
|
|
93
|
+
else if (metrics.coveragePercentage < 60) {
|
|
94
|
+
console.log(chalk.yellow(' š” Moderate coverage - continue instrumenting remaining nodes'));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.log(chalk.green(' š¢ Good coverage - maintain instrumentation quality'));
|
|
98
|
+
}
|
|
99
|
+
// Next steps
|
|
100
|
+
console.log(chalk.bold('\nš Next Steps:'));
|
|
101
|
+
if (noAnchors.length > 0) {
|
|
102
|
+
console.log(chalk.dim(` 1. Add anchors to ${noAnchors.length} node(s) without file references`));
|
|
103
|
+
console.log(chalk.dim(` 2. Example: { "id": "node-id", "anchors": [{ "path": "packages/core/src/File.ts" }] }`));
|
|
104
|
+
console.log(chalk.dim(` 3. Re-run: privu coverage`));
|
|
105
|
+
}
|
|
106
|
+
else if (uninstrumented.length > 0) {
|
|
107
|
+
console.log(chalk.dim(` 1. Review the ${uninstrumented.length} uninstrumented node(s) above`));
|
|
108
|
+
console.log(chalk.dim(` 2. Add OpenTelemetry spans to their source files`));
|
|
109
|
+
console.log(chalk.dim(` 3. Follow patterns from packages/core/src/rules/engine.ts`));
|
|
110
|
+
console.log(chalk.dim(` 4. Re-run: privu coverage`));
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.log(chalk.green(` ā
All canvas nodes have instrumentation!`));
|
|
114
|
+
}
|
|
115
|
+
console.log(chalk.bold('\n' + 'ā'.repeat(70) + '\n'));
|
|
116
|
+
}
|
|
117
|
+
export function createCoverageCommand() {
|
|
118
|
+
const command = new Command('coverage');
|
|
119
|
+
command
|
|
120
|
+
.description('Measure telemetry coverage for canvas nodes')
|
|
121
|
+
.option('-d, --dir <path>', 'Project directory (defaults to current directory)')
|
|
122
|
+
.option('--json', 'Output results as JSON')
|
|
123
|
+
.option('-t, --threshold <percentage>', 'Minimum coverage percentage (exit with error if below)')
|
|
124
|
+
.option('-v, --verbose', 'Show all nodes in output')
|
|
125
|
+
.action(async (options) => {
|
|
126
|
+
try {
|
|
127
|
+
const rootDir = resolve(options.dir || process.cwd());
|
|
128
|
+
// Analyze coverage
|
|
129
|
+
const metrics = await analyzeCoverage(rootDir);
|
|
130
|
+
// Output results
|
|
131
|
+
if (options.json) {
|
|
132
|
+
console.log(JSON.stringify(metrics, null, 2));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
printCoverageReport(metrics, rootDir, options);
|
|
136
|
+
}
|
|
137
|
+
// Check threshold
|
|
138
|
+
if (options.threshold) {
|
|
139
|
+
const threshold = parseFloat(options.threshold);
|
|
140
|
+
if (isNaN(threshold) || threshold < 0 || threshold > 100) {
|
|
141
|
+
console.error(chalk.red('Error: Threshold must be a number between 0 and 100'));
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
if (metrics.coveragePercentage < threshold) {
|
|
145
|
+
if (!options.json) {
|
|
146
|
+
console.error(chalk.red(`\nā Coverage ${metrics.coveragePercentage.toFixed(1)}% is below threshold ${threshold}%`));
|
|
147
|
+
}
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
console.error(chalk.red('Error:'), error.message);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
return command;
|
|
158
|
+
}
|
|
@@ -0,0 +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,CAkD7C"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create command - Create a new canvas file in the .principal-views folder
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
const TEMPLATE_CANVAS = {
|
|
9
|
+
nodes: [],
|
|
10
|
+
edges: [],
|
|
11
|
+
};
|
|
12
|
+
export function createCreateCommand() {
|
|
13
|
+
const command = new Command('create');
|
|
14
|
+
command
|
|
15
|
+
.description('Create a new canvas file in the .principal-views folder')
|
|
16
|
+
.requiredOption('-n, --name <name>', 'Name for the canvas file (e.g., "cache-sync-architecture")')
|
|
17
|
+
.option('-f, --force', 'Overwrite existing file')
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
try {
|
|
20
|
+
const vgcDir = join(process.cwd(), '.principal-views');
|
|
21
|
+
const canvasFile = join(vgcDir, `${options.name}.canvas`);
|
|
22
|
+
// Check if .principal-views directory exists
|
|
23
|
+
if (!existsSync(vgcDir)) {
|
|
24
|
+
mkdirSync(vgcDir, { recursive: true });
|
|
25
|
+
console.log(chalk.green(`Created directory: .principal-views/`));
|
|
26
|
+
}
|
|
27
|
+
// Check if canvas file already exists
|
|
28
|
+
if (existsSync(canvasFile) && !options.force) {
|
|
29
|
+
console.error(chalk.red(`Error: Canvas file already exists: .principal-views/${options.name}.canvas`));
|
|
30
|
+
console.log(chalk.yellow(`Use ${chalk.cyan('--force')} to overwrite`));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
// Create the canvas file
|
|
34
|
+
writeFileSync(canvasFile, JSON.stringify(TEMPLATE_CANVAS, null, 2));
|
|
35
|
+
console.log(chalk.green(`ā Created canvas file: .principal-views/${options.name}.canvas`));
|
|
36
|
+
// Show next steps
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(chalk.bold('Next steps:'));
|
|
39
|
+
console.log(` 1. Open ${chalk.cyan(`.principal-views/${options.name}.canvas`)} in your editor`);
|
|
40
|
+
console.log(` 2. Add nodes and edges to define your architecture`);
|
|
41
|
+
console.log(` 3. Run ${chalk.cyan('npx @principal-ai/principal-view-cli validate')} to check your configuration`);
|
|
42
|
+
console.log(` 4. Run ${chalk.cyan('npx @principal-ai/principal-view-cli doctor')} to verify source mappings`);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error(chalk.red('Error:'), error.message);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return command;
|
|
50
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor command - Check configuration staleness and source pattern validity
|
|
3
|
+
*
|
|
4
|
+
* This command performs two types of checks:
|
|
5
|
+
* 1. Pattern validation: Ensures source patterns in .principal-views/*.yaml configs match actual files
|
|
6
|
+
* 2. Freshness check: Compares config modification times vs source file changes
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
export declare function createDoctorCommand(): Command;
|
|
10
|
+
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -0,0 +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;AAmLpC,wBAAgB,mBAAmB,IAAI,OAAO,CAsK7C"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor command - Check configuration staleness and source pattern validity
|
|
3
|
+
*
|
|
4
|
+
* This command performs two types of checks:
|
|
5
|
+
* 1. Pattern validation: Ensures source patterns in .principal-views/*.yaml configs match actual files
|
|
6
|
+
* 2. Freshness check: Compares config modification times vs source file changes
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
10
|
+
import { resolve, relative } from 'node:path';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { globby } from 'globby';
|
|
13
|
+
import yaml from 'js-yaml';
|
|
14
|
+
/**
|
|
15
|
+
* Format a time difference in human-readable form
|
|
16
|
+
*/
|
|
17
|
+
function formatTimeDiff(ms) {
|
|
18
|
+
const seconds = Math.floor(ms / 1000);
|
|
19
|
+
const minutes = Math.floor(seconds / 60);
|
|
20
|
+
const hours = Math.floor(minutes / 60);
|
|
21
|
+
const days = Math.floor(hours / 24);
|
|
22
|
+
if (days > 0)
|
|
23
|
+
return `${days} day${days > 1 ? 's' : ''}`;
|
|
24
|
+
if (hours > 0)
|
|
25
|
+
return `${hours} hour${hours > 1 ? 's' : ''}`;
|
|
26
|
+
if (minutes > 0)
|
|
27
|
+
return `${minutes} minute${minutes > 1 ? 's' : ''}`;
|
|
28
|
+
return `${seconds} second${seconds !== 1 ? 's' : ''}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Check a single .principal-views config file for staleness issues
|
|
32
|
+
*/
|
|
33
|
+
async function checkConfig(configPath, projectRoot) {
|
|
34
|
+
const absolutePath = resolve(configPath);
|
|
35
|
+
const relativePath = relative(projectRoot, absolutePath);
|
|
36
|
+
const issues = [];
|
|
37
|
+
const stats = {
|
|
38
|
+
nodeTypesChecked: 0,
|
|
39
|
+
patternsChecked: 0,
|
|
40
|
+
filesMatched: 0,
|
|
41
|
+
staleConfigs: 0,
|
|
42
|
+
};
|
|
43
|
+
let configName = 'Unknown';
|
|
44
|
+
try {
|
|
45
|
+
const content = readFileSync(absolutePath, 'utf8');
|
|
46
|
+
const config = yaml.load(content);
|
|
47
|
+
const configStats = statSync(absolutePath);
|
|
48
|
+
const configMtime = configStats.mtime.getTime();
|
|
49
|
+
configName = config.metadata?.name || relativePath;
|
|
50
|
+
// Check if config has nodeTypes with sources
|
|
51
|
+
if (!config.nodeTypes || Object.keys(config.nodeTypes).length === 0) {
|
|
52
|
+
issues.push({
|
|
53
|
+
type: 'info',
|
|
54
|
+
configFile: relativePath,
|
|
55
|
+
message: 'No node types defined in configuration',
|
|
56
|
+
});
|
|
57
|
+
return { configFile: relativePath, configName, issues, stats };
|
|
58
|
+
}
|
|
59
|
+
// Check each node type's source patterns
|
|
60
|
+
for (const [nodeTypeName, nodeType] of Object.entries(config.nodeTypes)) {
|
|
61
|
+
stats.nodeTypesChecked++;
|
|
62
|
+
if (!nodeType.sources || nodeType.sources.length === 0) {
|
|
63
|
+
issues.push({
|
|
64
|
+
type: 'info',
|
|
65
|
+
configFile: relativePath,
|
|
66
|
+
nodeType: nodeTypeName,
|
|
67
|
+
message: `Node type "${nodeTypeName}" has no source patterns defined`,
|
|
68
|
+
});
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
for (const pattern of nodeType.sources) {
|
|
72
|
+
stats.patternsChecked++;
|
|
73
|
+
// Find files matching this pattern
|
|
74
|
+
const matchedFiles = await globby(pattern, {
|
|
75
|
+
cwd: projectRoot,
|
|
76
|
+
gitignore: true,
|
|
77
|
+
ignore: ['node_modules/**', 'dist/**', '.git/**'],
|
|
78
|
+
});
|
|
79
|
+
if (matchedFiles.length === 0) {
|
|
80
|
+
// Pattern doesn't match any files - this is a warning
|
|
81
|
+
issues.push({
|
|
82
|
+
type: 'warning',
|
|
83
|
+
configFile: relativePath,
|
|
84
|
+
nodeType: nodeTypeName,
|
|
85
|
+
message: `Source pattern "${pattern}" doesn't match any files`,
|
|
86
|
+
details: 'This pattern may be outdated or the files may have been moved/deleted',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
stats.filesMatched += matchedFiles.length;
|
|
91
|
+
// Check if any matched files are newer than the config
|
|
92
|
+
let newestFile = null;
|
|
93
|
+
let newestMtime = 0;
|
|
94
|
+
for (const file of matchedFiles) {
|
|
95
|
+
const filePath = resolve(projectRoot, file);
|
|
96
|
+
try {
|
|
97
|
+
const fileStats = statSync(filePath);
|
|
98
|
+
const fileMtime = fileStats.mtime.getTime();
|
|
99
|
+
if (fileMtime > newestMtime) {
|
|
100
|
+
newestMtime = fileMtime;
|
|
101
|
+
newestFile = file;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// File may have been deleted between glob and stat
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Check staleness (with 5-second buffer for build tools)
|
|
109
|
+
const STALE_THRESHOLD_MS = 5000;
|
|
110
|
+
if (newestFile && newestMtime > configMtime + STALE_THRESHOLD_MS) {
|
|
111
|
+
const timeDiff = newestMtime - configMtime;
|
|
112
|
+
stats.staleConfigs++;
|
|
113
|
+
issues.push({
|
|
114
|
+
type: 'warning',
|
|
115
|
+
configFile: relativePath,
|
|
116
|
+
nodeType: nodeTypeName,
|
|
117
|
+
message: `Config may be stale: "${newestFile}" was modified ${formatTimeDiff(timeDiff)} after the config`,
|
|
118
|
+
details: `Pattern: ${pattern} (matched ${matchedFiles.length} file${matchedFiles.length > 1 ? 's' : ''})`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
issues.push({
|
|
127
|
+
type: 'error',
|
|
128
|
+
configFile: relativePath,
|
|
129
|
+
message: `Failed to parse config: ${error.message}`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return { configFile: relativePath, configName, issues, stats };
|
|
133
|
+
}
|
|
134
|
+
export function createDoctorCommand() {
|
|
135
|
+
const command = new Command('doctor');
|
|
136
|
+
command
|
|
137
|
+
.description('Check configuration staleness and source pattern validity')
|
|
138
|
+
.option('-q, --quiet', 'Only show errors and warnings')
|
|
139
|
+
.option('--errors-only', 'Only show errors (for pre-commit hooks)')
|
|
140
|
+
.option('--json', 'Output results as JSON')
|
|
141
|
+
.option('-d, --dir <path>', 'Project directory (defaults to current directory)')
|
|
142
|
+
.action(async (options) => {
|
|
143
|
+
try {
|
|
144
|
+
const projectRoot = resolve(options.dir || process.cwd());
|
|
145
|
+
const vgcDir = resolve(projectRoot, '.principal-views');
|
|
146
|
+
if (!existsSync(vgcDir)) {
|
|
147
|
+
if (options.json) {
|
|
148
|
+
console.log(JSON.stringify({ error: 'No .principal-views directory found', results: [] }));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
console.log(chalk.yellow('No .principal-views directory found.'));
|
|
152
|
+
console.log(chalk.dim('Run "npx @principal-ai/principal-view-cli init" to create a configuration.'));
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Find all .yaml config files
|
|
157
|
+
const configFiles = await globby(['*.yaml', '*.yml'], {
|
|
158
|
+
cwd: vgcDir,
|
|
159
|
+
absolute: true,
|
|
160
|
+
ignore: ['README.md'],
|
|
161
|
+
});
|
|
162
|
+
if (configFiles.length === 0) {
|
|
163
|
+
if (options.json) {
|
|
164
|
+
console.log(JSON.stringify({ error: 'No config files found in .principal-views', results: [] }));
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
console.log(chalk.yellow('No configuration files found in .principal-views/'));
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Check each config
|
|
172
|
+
const results = [];
|
|
173
|
+
for (const configFile of configFiles) {
|
|
174
|
+
const result = await checkConfig(configFile, projectRoot);
|
|
175
|
+
results.push(result);
|
|
176
|
+
}
|
|
177
|
+
// Aggregate stats
|
|
178
|
+
const totalStats = results.reduce((acc, r) => ({
|
|
179
|
+
nodeTypesChecked: acc.nodeTypesChecked + r.stats.nodeTypesChecked,
|
|
180
|
+
patternsChecked: acc.patternsChecked + r.stats.patternsChecked,
|
|
181
|
+
filesMatched: acc.filesMatched + r.stats.filesMatched,
|
|
182
|
+
staleConfigs: acc.staleConfigs + r.stats.staleConfigs,
|
|
183
|
+
}), { nodeTypesChecked: 0, patternsChecked: 0, filesMatched: 0, staleConfigs: 0 });
|
|
184
|
+
// Filter issues based on options
|
|
185
|
+
const filterIssues = (issues) => {
|
|
186
|
+
if (options.errorsOnly) {
|
|
187
|
+
return issues.filter((i) => i.type === 'error');
|
|
188
|
+
}
|
|
189
|
+
if (options.quiet) {
|
|
190
|
+
return issues.filter((i) => i.type === 'error' || i.type === 'warning');
|
|
191
|
+
}
|
|
192
|
+
return issues;
|
|
193
|
+
};
|
|
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;
|
|
198
|
+
// Output results
|
|
199
|
+
if (options.json) {
|
|
200
|
+
console.log(JSON.stringify({
|
|
201
|
+
results: results.map((r) => ({
|
|
202
|
+
...r,
|
|
203
|
+
issues: filterIssues(r.issues),
|
|
204
|
+
})),
|
|
205
|
+
summary: {
|
|
206
|
+
configs: results.length,
|
|
207
|
+
errors: errorCount,
|
|
208
|
+
warnings: warningCount,
|
|
209
|
+
...totalStats,
|
|
210
|
+
},
|
|
211
|
+
}, null, 2));
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
if (!options.quiet && !options.errorsOnly) {
|
|
215
|
+
console.log(chalk.bold(`\nChecking ${results.length} configuration file(s)...\n`));
|
|
216
|
+
}
|
|
217
|
+
for (const result of results) {
|
|
218
|
+
const issues = filterIssues(result.issues);
|
|
219
|
+
if (issues.length === 0 && !options.quiet && !options.errorsOnly) {
|
|
220
|
+
console.log(chalk.green(`ā ${result.configFile}`) + chalk.dim(` (${result.configName})`));
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (issues.length > 0) {
|
|
224
|
+
const hasErrors = issues.some((i) => i.type === 'error');
|
|
225
|
+
const icon = hasErrors ? chalk.red('ā') : chalk.yellow('ā ');
|
|
226
|
+
console.log(`${icon} ${result.configFile}` + chalk.dim(` (${result.configName})`));
|
|
227
|
+
for (const issue of issues) {
|
|
228
|
+
const prefix = issue.nodeType ? `[${issue.nodeType}] ` : '';
|
|
229
|
+
if (issue.type === 'error') {
|
|
230
|
+
console.log(chalk.red(` ā ${prefix}${issue.message}`));
|
|
231
|
+
}
|
|
232
|
+
else if (issue.type === 'warning') {
|
|
233
|
+
console.log(chalk.yellow(` ā ${prefix}${issue.message}`));
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
console.log(chalk.dim(` ā¹ ${prefix}${issue.message}`));
|
|
237
|
+
}
|
|
238
|
+
if (issue.details) {
|
|
239
|
+
console.log(chalk.dim(` ā ${issue.details}`));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
console.log('');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Summary
|
|
246
|
+
if (!options.errorsOnly) {
|
|
247
|
+
console.log(chalk.dim('ā'.repeat(50)));
|
|
248
|
+
console.log(chalk.dim(`Checked ${totalStats.nodeTypesChecked} node types, ` +
|
|
249
|
+
`${totalStats.patternsChecked} patterns, ` +
|
|
250
|
+
`matched ${totalStats.filesMatched} files`));
|
|
251
|
+
}
|
|
252
|
+
if (errorCount > 0) {
|
|
253
|
+
console.log(chalk.red(`\nā ${errorCount} error(s) found`));
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
else if (warningCount > 0 && options.errorsOnly) {
|
|
257
|
+
// In errors-only mode, don't fail on warnings
|
|
258
|
+
process.exit(0);
|
|
259
|
+
}
|
|
260
|
+
else if (warningCount > 0) {
|
|
261
|
+
console.log(chalk.yellow(`\nā ${warningCount} warning(s) found`));
|
|
262
|
+
}
|
|
263
|
+
else if (!options.quiet && !options.errorsOnly) {
|
|
264
|
+
console.log(chalk.green(`\nā All configurations are up to date`));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
console.error(chalk.red('Error:'), error.message);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
return command;
|
|
274
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formats.d.ts","sourceRoot":"","sources":["../../src/commands/formats.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAycpC,wBAAgB,oBAAoB,IAAI,OAAO,CA6B9C"}
|