@aiready/cli 0.9.27 → 0.9.28
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/cli.js +423 -377
- package/dist/cli.mjs +442 -374
- package/package.json +3 -3
- package/src/cli.ts +49 -1284
- package/src/commands/consistency.ts +192 -0
- package/src/commands/context.ts +192 -0
- package/src/commands/index.ts +9 -0
- package/src/commands/patterns.ts +179 -0
- package/src/commands/scan.ts +455 -0
- package/src/commands/visualize.ts +253 -0
- package/src/utils/helpers.ts +133 -0
- package/tsconfig.json +5 -2
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visualize command - Generate interactive visualization from an AIReady report
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { writeFileSync, readFileSync, existsSync, copyFileSync } from 'fs';
|
|
7
|
+
import { resolve as resolvePath } from 'path';
|
|
8
|
+
import { spawn } from 'child_process';
|
|
9
|
+
import { handleCLIError } from '@aiready/core';
|
|
10
|
+
import { generateHTML } from '@aiready/core';
|
|
11
|
+
import { findLatestScanReport } from '../utils/helpers';
|
|
12
|
+
|
|
13
|
+
interface VisualizeOptions {
|
|
14
|
+
report?: string;
|
|
15
|
+
output?: string;
|
|
16
|
+
open?: boolean;
|
|
17
|
+
serve?: boolean | number;
|
|
18
|
+
dev?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function visualizeAction(directory: string, options: VisualizeOptions) {
|
|
22
|
+
try {
|
|
23
|
+
const dirPath = resolvePath(process.cwd(), directory || '.');
|
|
24
|
+
let reportPath = options.report ? resolvePath(dirPath, options.report) : null;
|
|
25
|
+
|
|
26
|
+
// If report not provided or not found, try to find latest scan report
|
|
27
|
+
if (!reportPath || !existsSync(reportPath)) {
|
|
28
|
+
const latestScan = findLatestScanReport(dirPath);
|
|
29
|
+
if (latestScan) {
|
|
30
|
+
reportPath = latestScan;
|
|
31
|
+
console.log(chalk.dim(`Found latest report: ${latestScan.split('/').pop()}`));
|
|
32
|
+
} else {
|
|
33
|
+
console.error(chalk.red('❌ No AI readiness report found'));
|
|
34
|
+
console.log(chalk.dim(`\nGenerate a report with:\n aiready scan --output json\n\nOr specify a custom report:\n aiready visualise --report <path-to-report.json>`));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const raw = readFileSync(reportPath, 'utf8');
|
|
40
|
+
const report = JSON.parse(raw);
|
|
41
|
+
|
|
42
|
+
// Load config to extract graph caps
|
|
43
|
+
const configPath = resolvePath(dirPath, 'aiready.json');
|
|
44
|
+
let graphConfig = { maxNodes: 400, maxEdges: 600 };
|
|
45
|
+
|
|
46
|
+
if (existsSync(configPath)) {
|
|
47
|
+
try {
|
|
48
|
+
const rawConfig = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
49
|
+
if (rawConfig.visualizer?.graph) {
|
|
50
|
+
graphConfig = {
|
|
51
|
+
maxNodes: rawConfig.visualizer.graph.maxNodes ?? graphConfig.maxNodes,
|
|
52
|
+
maxEdges: rawConfig.visualizer.graph.maxEdges ?? graphConfig.maxEdges,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
// Silently ignore parse errors and use defaults
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Store config in env for vite middleware to pass to client
|
|
61
|
+
const envVisualizerConfig = JSON.stringify(graphConfig);
|
|
62
|
+
process.env.AIREADY_VISUALIZER_CONFIG = envVisualizerConfig;
|
|
63
|
+
|
|
64
|
+
console.log("Building graph from report...");
|
|
65
|
+
const { GraphBuilder } = await import('@aiready/visualizer/graph');
|
|
66
|
+
const graph = GraphBuilder.buildFromReport(report, dirPath);
|
|
67
|
+
|
|
68
|
+
if (options.dev) {
|
|
69
|
+
try {
|
|
70
|
+
const monorepoWebDir = resolvePath(dirPath, 'packages/visualizer');
|
|
71
|
+
let webDir = '';
|
|
72
|
+
let visualizerAvailable = false;
|
|
73
|
+
if (existsSync(monorepoWebDir)) {
|
|
74
|
+
webDir = monorepoWebDir;
|
|
75
|
+
visualizerAvailable = true;
|
|
76
|
+
} else {
|
|
77
|
+
// Try to resolve installed @aiready/visualizer package from node_modules
|
|
78
|
+
// Check multiple locations to support pnpm, npm, yarn, etc.
|
|
79
|
+
const nodemodulesLocations: string[] = [
|
|
80
|
+
resolvePath(dirPath, 'node_modules', '@aiready', 'visualizer'),
|
|
81
|
+
resolvePath(process.cwd(), 'node_modules', '@aiready', 'visualizer'),
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// Walk up directory tree to find node_modules in parent directories
|
|
85
|
+
let currentDir = dirPath;
|
|
86
|
+
while (currentDir !== '/' && currentDir !== '.') {
|
|
87
|
+
nodemodulesLocations.push(resolvePath(currentDir, 'node_modules', '@aiready', 'visualizer'));
|
|
88
|
+
const parent = resolvePath(currentDir, '..');
|
|
89
|
+
if (parent === currentDir) break; // Reached filesystem root
|
|
90
|
+
currentDir = parent;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const location of nodemodulesLocations) {
|
|
94
|
+
if (existsSync(location) && existsSync(resolvePath(location, 'package.json'))) {
|
|
95
|
+
webDir = location;
|
|
96
|
+
visualizerAvailable = true;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Fallback: try require.resolve
|
|
102
|
+
if (!visualizerAvailable) {
|
|
103
|
+
try {
|
|
104
|
+
const vizPkgPath = require.resolve('@aiready/visualizer/package.json');
|
|
105
|
+
webDir = resolvePath(vizPkgPath, '..');
|
|
106
|
+
visualizerAvailable = true;
|
|
107
|
+
} catch (e) {
|
|
108
|
+
// Visualizer not found
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const spawnCwd = webDir || process.cwd();
|
|
113
|
+
const nodeBinCandidate = process.execPath;
|
|
114
|
+
const nodeBin = existsSync(nodeBinCandidate) ? nodeBinCandidate : 'node';
|
|
115
|
+
if (!visualizerAvailable) {
|
|
116
|
+
console.error(chalk.red('❌ Cannot start dev server: @aiready/visualizer not available.'));
|
|
117
|
+
console.log(chalk.dim('Install @aiready/visualizer in your project with:\n npm install @aiready/visualizer'));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Inline report watcher: copy report to web/report-data.json and watch for changes
|
|
122
|
+
const { watch } = await import('fs');
|
|
123
|
+
const copyReportToViz = () => {
|
|
124
|
+
try {
|
|
125
|
+
const destPath = resolvePath(spawnCwd, 'web', 'report-data.json');
|
|
126
|
+
copyFileSync(reportPath!, destPath);
|
|
127
|
+
console.log(`📋 Report synced to ${destPath}`);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
console.error('Failed to sync report:', e);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Initial copy
|
|
134
|
+
copyReportToViz();
|
|
135
|
+
|
|
136
|
+
// Watch source report for changes
|
|
137
|
+
let watchTimeout: NodeJS.Timeout | null = null;
|
|
138
|
+
const reportWatcher = watch(reportPath, () => {
|
|
139
|
+
// Debounce to avoid multiple copies during file write
|
|
140
|
+
if (watchTimeout) clearTimeout(watchTimeout);
|
|
141
|
+
watchTimeout = setTimeout(copyReportToViz, 100);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const envForSpawn = {
|
|
145
|
+
...process.env,
|
|
146
|
+
AIREADY_REPORT_PATH: reportPath,
|
|
147
|
+
AIREADY_VISUALIZER_CONFIG: envVisualizerConfig
|
|
148
|
+
};
|
|
149
|
+
const vite = spawn("pnpm", ["run", "dev:web"], { cwd: spawnCwd, stdio: "inherit", shell: true, env: envForSpawn });
|
|
150
|
+
const onExit = () => {
|
|
151
|
+
try {
|
|
152
|
+
reportWatcher.close();
|
|
153
|
+
} catch (e) {}
|
|
154
|
+
try {
|
|
155
|
+
vite.kill();
|
|
156
|
+
} catch (e) {}
|
|
157
|
+
process.exit(0);
|
|
158
|
+
};
|
|
159
|
+
process.on("SIGINT", onExit);
|
|
160
|
+
process.on("SIGTERM", onExit);
|
|
161
|
+
return;
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error("Failed to start dev server:", err);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log("Generating HTML...");
|
|
168
|
+
const html = generateHTML(graph);
|
|
169
|
+
const outPath = resolvePath(dirPath, options.output || 'packages/visualizer/visualization.html');
|
|
170
|
+
writeFileSync(outPath, html, 'utf8');
|
|
171
|
+
console.log("Visualization written to:", outPath);
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if (options.open) {
|
|
175
|
+
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
176
|
+
spawn(opener, [`"${outPath}"`], { shell: true });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (options.serve) {
|
|
180
|
+
try {
|
|
181
|
+
const port = typeof options.serve === 'number' ? options.serve : 5173;
|
|
182
|
+
const http = await import('http');
|
|
183
|
+
const fsp = await import('fs/promises');
|
|
184
|
+
|
|
185
|
+
const server = http.createServer(async (req, res) => {
|
|
186
|
+
try {
|
|
187
|
+
const urlPath = req.url || '/';
|
|
188
|
+
if (urlPath === '/' || urlPath === '/index.html') {
|
|
189
|
+
const content = await fsp.readFile(outPath, 'utf8');
|
|
190
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
191
|
+
res.end(content);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
195
|
+
res.end('Not found');
|
|
196
|
+
} catch (e: any) {
|
|
197
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
198
|
+
res.end('Server error');
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
server.listen(port, () => {
|
|
203
|
+
const addr = `http://localhost:${port}/`;
|
|
204
|
+
console.log(`Local visualization server running at ${addr}`);
|
|
205
|
+
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
206
|
+
spawn(opener, [`"${addr}"`], { shell: true });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
process.on('SIGINT', () => {
|
|
210
|
+
server.close();
|
|
211
|
+
process.exit(0);
|
|
212
|
+
});
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.error('Failed to start local server:', err);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
} catch (err: any) {
|
|
219
|
+
handleCLIError(err, 'Visualization');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export const visualizeHelpText = `
|
|
224
|
+
EXAMPLES:
|
|
225
|
+
$ aiready visualize . # Auto-detects latest report
|
|
226
|
+
$ aiready visualize . --report .aiready/aiready-report-20260217-143022.json
|
|
227
|
+
$ aiready visualize . --report report.json -o out/visualization.html --open
|
|
228
|
+
$ aiready visualize . --report report.json --serve
|
|
229
|
+
$ aiready visualize . --report report.json --serve 8080
|
|
230
|
+
$ aiready visualize . --report report.json --dev
|
|
231
|
+
|
|
232
|
+
NOTES:
|
|
233
|
+
- The value passed to --report is interpreted relative to the directory argument (first positional).
|
|
234
|
+
If the report is not found, the CLI will suggest running 'aiready scan' to generate it.
|
|
235
|
+
- Default output path: packages/visualizer/visualization.html (relative to the directory argument).
|
|
236
|
+
- --serve starts a tiny single-file HTTP server (default port: 5173) and opens your browser.
|
|
237
|
+
It serves only the generated HTML (no additional asset folders).
|
|
238
|
+
- Relatedness is represented by node proximity and size; explicit 'related' edges are not drawn to
|
|
239
|
+
reduce clutter and improve interactivity on large graphs.
|
|
240
|
+
- For very large graphs, consider narrowing the input with --include/--exclude or use --serve and
|
|
241
|
+
allow the browser a moment to stabilize after load.
|
|
242
|
+
`;
|
|
243
|
+
|
|
244
|
+
export const visualiseHelpText = `
|
|
245
|
+
EXAMPLES:
|
|
246
|
+
$ aiready visualise . # Auto-detects latest report
|
|
247
|
+
$ aiready visualise . --report .aiready/aiready-report-20260217-143022.json
|
|
248
|
+
$ aiready visualise . --report report.json --dev
|
|
249
|
+
$ aiready visualise . --report report.json --serve 8080
|
|
250
|
+
|
|
251
|
+
NOTES:
|
|
252
|
+
- Same options as 'visualize'. Use --dev for live reload and --serve to host a static HTML.
|
|
253
|
+
`;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper functions for CLI commands
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve as resolvePath } from 'path';
|
|
6
|
+
import { existsSync, readdirSync, statSync, readFileSync, copyFileSync } from 'fs';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate timestamp for report filenames (YYYYMMDD-HHMMSS)
|
|
11
|
+
* Provides better granularity than date-only filenames
|
|
12
|
+
*/
|
|
13
|
+
export function getReportTimestamp(): string {
|
|
14
|
+
const now = new Date();
|
|
15
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
16
|
+
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Find the latest aiready report in the .aiready directory
|
|
21
|
+
* Searches for both new format (aiready-report-*) and legacy format (aiready-scan-*)
|
|
22
|
+
*/
|
|
23
|
+
export function findLatestScanReport(dirPath: string): string | null {
|
|
24
|
+
const aireadyDir = resolvePath(dirPath, '.aiready');
|
|
25
|
+
if (!existsSync(aireadyDir)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Search for new format first, then legacy format
|
|
30
|
+
let files = readdirSync(aireadyDir).filter(f => f.startsWith('aiready-report-') && f.endsWith('.json'));
|
|
31
|
+
if (files.length === 0) {
|
|
32
|
+
files = readdirSync(aireadyDir).filter(f => f.startsWith('aiready-scan-') && f.endsWith('.json'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (files.length === 0) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Sort by modification time, most recent first
|
|
40
|
+
const sortedFiles = files
|
|
41
|
+
.map(f => ({ name: f, path: resolvePath(aireadyDir, f), mtime: statSync(resolvePath(aireadyDir, f)).mtime }))
|
|
42
|
+
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
43
|
+
|
|
44
|
+
return sortedFiles[0].path;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Warn if graph caps may be exceeded
|
|
49
|
+
*/
|
|
50
|
+
export function warnIfGraphCapExceeded(report: any, dirPath: string) {
|
|
51
|
+
try {
|
|
52
|
+
// Use dynamic import and loadConfig to get the raw visualizer config
|
|
53
|
+
const { loadConfig } = require('@aiready/core');
|
|
54
|
+
|
|
55
|
+
let graphConfig = { maxNodes: 400, maxEdges: 600 };
|
|
56
|
+
|
|
57
|
+
// Try to read aiready.json synchronously
|
|
58
|
+
const configPath = resolvePath(dirPath, 'aiready.json');
|
|
59
|
+
if (existsSync(configPath)) {
|
|
60
|
+
try {
|
|
61
|
+
const rawConfig = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
62
|
+
if (rawConfig.visualizer?.graph) {
|
|
63
|
+
graphConfig = {
|
|
64
|
+
maxNodes: rawConfig.visualizer.graph.maxNodes ?? graphConfig.maxNodes,
|
|
65
|
+
maxEdges: rawConfig.visualizer.graph.maxEdges ?? graphConfig.maxEdges,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Silently ignore parse errors and use defaults
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const nodeCount = (report.context?.length || 0) + (report.patterns?.length || 0);
|
|
74
|
+
const edgeCount = report.context?.reduce((sum: number, ctx: any) => {
|
|
75
|
+
const relCount = ctx.relatedFiles?.length || 0;
|
|
76
|
+
const depCount = ctx.dependencies?.length || 0;
|
|
77
|
+
return sum + relCount + depCount;
|
|
78
|
+
}, 0) || 0;
|
|
79
|
+
|
|
80
|
+
if (nodeCount > graphConfig.maxNodes || edgeCount > graphConfig.maxEdges) {
|
|
81
|
+
console.log('');
|
|
82
|
+
console.log(chalk.yellow(`⚠️ Graph may be truncated at visualization time:`));
|
|
83
|
+
if (nodeCount > graphConfig.maxNodes) {
|
|
84
|
+
console.log(chalk.dim(` • Nodes: ${nodeCount} > limit ${graphConfig.maxNodes}`));
|
|
85
|
+
}
|
|
86
|
+
if (edgeCount > graphConfig.maxEdges) {
|
|
87
|
+
console.log(chalk.dim(` • Edges: ${edgeCount} > limit ${graphConfig.maxEdges}`));
|
|
88
|
+
}
|
|
89
|
+
console.log(chalk.dim(` To increase limits, add to aiready.json:`));
|
|
90
|
+
console.log(chalk.dim(` {`));
|
|
91
|
+
console.log(chalk.dim(` "visualizer": {`));
|
|
92
|
+
console.log(chalk.dim(` "graph": { "maxNodes": 2000, "maxEdges": 5000 }`));
|
|
93
|
+
console.log(chalk.dim(` }`));
|
|
94
|
+
console.log(chalk.dim(` }`));
|
|
95
|
+
}
|
|
96
|
+
} catch (e) {
|
|
97
|
+
// Silently fail on config read errors
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generate markdown report for consistency command
|
|
103
|
+
*/
|
|
104
|
+
export function generateMarkdownReport(report: any, elapsedTime: string): string {
|
|
105
|
+
let markdown = `# Consistency Analysis Report\n\n`;
|
|
106
|
+
markdown += `**Generated:** ${new Date().toISOString()}\n`;
|
|
107
|
+
markdown += `**Analysis Time:** ${elapsedTime}s\n\n`;
|
|
108
|
+
|
|
109
|
+
markdown += `## Summary\n\n`;
|
|
110
|
+
markdown += `- **Files Analyzed:** ${report.summary.filesAnalyzed}\n`;
|
|
111
|
+
markdown += `- **Total Issues:** ${report.summary.totalIssues}\n`;
|
|
112
|
+
markdown += ` - Naming: ${report.summary.namingIssues}\n`;
|
|
113
|
+
markdown += ` - Patterns: ${report.summary.patternIssues}\n\n`;
|
|
114
|
+
|
|
115
|
+
if (report.recommendations.length > 0) {
|
|
116
|
+
markdown += `## Recommendations\n\n`;
|
|
117
|
+
report.recommendations.forEach((rec: string, i: number) => {
|
|
118
|
+
markdown += `${i + 1}. ${rec}\n`;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return markdown;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Truncate array for display (show first N items with "... +N more")
|
|
127
|
+
*/
|
|
128
|
+
export function truncateArray(arr: any[] | undefined, cap = 8): string {
|
|
129
|
+
if (!Array.isArray(arr)) return '';
|
|
130
|
+
const shown = arr.slice(0, cap).map((v) => String(v));
|
|
131
|
+
const more = arr.length - shown.length;
|
|
132
|
+
return shown.join(', ') + (more > 0 ? `, ... (+${more} more)` : '');
|
|
133
|
+
}
|
package/tsconfig.json
CHANGED