@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.
package/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # Visual Validation CLI
2
+
3
+ A command-line tool for validating and managing `.canvas` configuration files for the Visual Validation Framework.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @principal-ai/visual-validation-cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ The CLI provides two command aliases: `vv` (primary) and `visual-validation`.
14
+
15
+ ### Commands
16
+
17
+ #### `init` - Initialize Project Structure
18
+
19
+ Set up a new `.vgc` folder with template files:
20
+
21
+ ```bash
22
+ vv init
23
+ vv init --name my-architecture
24
+ vv init --force # Overwrite existing files
25
+ ```
26
+
27
+ #### `validate` - Validate Canvas Files
28
+
29
+ Strict validation of `.canvas` configuration files:
30
+
31
+ ```bash
32
+ vv validate # Validates all .vgc/*.canvas files
33
+ vv validate path/to/file.canvas
34
+ vv validate "**/*.canvas" # Glob pattern
35
+ vv validate --quiet # Only output errors
36
+ vv validate --json # Output as JSON
37
+ ```
38
+
39
+ **Validation checks:**
40
+ - Required `vv` extension with name and version
41
+ - All nodes have required fields (id, type, x, y, width, height)
42
+ - Custom node types must have `vv.nodeType` and valid `vv.shape`
43
+ - Edge references point to existing nodes
44
+ - Edge types reference defined edge type definitions
45
+
46
+ #### `list` (alias: `ls`) - List Canvas Files
47
+
48
+ Display all canvas files in the project with metadata:
49
+
50
+ ```bash
51
+ vv list
52
+ vv ls --all # Search all directories
53
+ vv ls --json # Output as JSON
54
+ ```
55
+
56
+ #### `schema` - Display Format Documentation
57
+
58
+ Show detailed documentation about the canvas format:
59
+
60
+ ```bash
61
+ vv schema # Overview
62
+ vv schema nodes # Node types, shapes, colors
63
+ vv schema edges # Edge properties and styles
64
+ vv schema vv # Visual Validation extension fields
65
+ vv schema examples # Complete examples
66
+ ```
67
+
68
+ #### `doctor` - Configuration Health Check
69
+
70
+ Check configuration staleness and validate source patterns:
71
+
72
+ ```bash
73
+ vv doctor
74
+ vv doctor --quiet # Only show errors and warnings
75
+ vv doctor --errors-only # For pre-commit hooks
76
+ vv doctor --json # Output as JSON
77
+ ```
78
+
79
+ ## Canvas Format
80
+
81
+ Canvas files follow the [JSON Canvas](https://jsoncanvas.org/) specification with Visual Validation extensions that maintain compatibility with standard tools like Obsidian.
82
+
83
+ ### Required Structure
84
+
85
+ ```json
86
+ {
87
+ "nodes": [...],
88
+ "edges": [...],
89
+ "vv": {
90
+ "name": "my-architecture",
91
+ "version": "1.0.0"
92
+ }
93
+ }
94
+ ```
95
+
96
+ ### Node Types
97
+
98
+ **Standard types** (no additional metadata required):
99
+ - `text` - Text content
100
+ - `group` - Container for other nodes
101
+ - `file` - File reference
102
+ - `link` - URL link
103
+
104
+ **Custom types** require `vv` extension:
105
+ ```json
106
+ {
107
+ "id": "node-1",
108
+ "type": "custom",
109
+ "x": 0, "y": 0,
110
+ "width": 200, "height": 100,
111
+ "vv": {
112
+ "nodeType": "service",
113
+ "shape": "rectangle"
114
+ }
115
+ }
116
+ ```
117
+
118
+ **Available shapes:** `circle`, `rectangle`, `hexagon`, `diamond`, `custom`
119
+
120
+ ### Edge Types
121
+
122
+ Define reusable edge styles at the canvas level:
123
+
124
+ ```json
125
+ {
126
+ "vv": {
127
+ "edgeTypes": {
128
+ "data-flow": {
129
+ "style": "dashed",
130
+ "color": "#3498db",
131
+ "width": 2
132
+ }
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ Use in edges:
139
+
140
+ ```json
141
+ {
142
+ "id": "edge-1",
143
+ "fromNode": "node-1",
144
+ "toNode": "node-2",
145
+ "vv": {
146
+ "edgeType": "data-flow"
147
+ }
148
+ }
149
+ ```
150
+
151
+ ## Requirements
152
+
153
+ - Node.js >= 18
154
+
155
+ ## License
156
+
157
+ MIT
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Create command - Create a new canvas file in the .principal-views folder
3
+ */
4
+ import { Command } from 'commander';
5
+ export declare function createCreateCommand(): Command;
6
+ //# sourceMappingURL=create.d.ts.map
@@ -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,CA2C7C"}
@@ -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('privu validate')} to check your configuration`);
42
+ console.log(` 4. Run ${chalk.cyan('privu 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;AA+KpC,wBAAgB,mBAAmB,IAAI,OAAO,CAgK7C"}
@@ -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 "privu 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,9 @@
1
+ /**
2
+ * Hooks command - Manage husky pre-commit hooks for Principal View
3
+ *
4
+ * This command installs/removes pre-commit hooks into a target project
5
+ * that will run `privu doctor` and `privu validate` before each commit.
6
+ */
7
+ import { Command } from 'commander';
8
+ export declare function createHooksCommand(): Command;
9
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +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;AA0NpC,wBAAgB,kBAAkB,IAAI,OAAO,CA6F5C"}