@aiready/cli 0.9.27 → 0.9.29

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,257 @@
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
+ // Check if --dev mode is requested and available
69
+ let useDevMode = options.dev || false;
70
+ let devServerStarted = false;
71
+
72
+ if (useDevMode) {
73
+ try {
74
+ const monorepoWebDir = resolvePath(dirPath, 'packages/visualizer');
75
+ let webDir = '';
76
+ let visualizerAvailable = false;
77
+
78
+ if (existsSync(monorepoWebDir)) {
79
+ webDir = monorepoWebDir;
80
+ visualizerAvailable = true;
81
+ } else {
82
+ // Try to resolve installed @aiready/visualizer package from node_modules
83
+ const nodemodulesLocations: string[] = [
84
+ resolvePath(dirPath, 'node_modules', '@aiready', 'visualizer'),
85
+ resolvePath(process.cwd(), 'node_modules', '@aiready', 'visualizer'),
86
+ ];
87
+
88
+ // Walk up directory tree to find node_modules in parent directories
89
+ let currentDir = dirPath;
90
+ while (currentDir !== '/' && currentDir !== '.') {
91
+ nodemodulesLocations.push(resolvePath(currentDir, 'node_modules', '@aiready', 'visualizer'));
92
+ const parent = resolvePath(currentDir, '..');
93
+ if (parent === currentDir) break;
94
+ currentDir = parent;
95
+ }
96
+
97
+ for (const location of nodemodulesLocations) {
98
+ if (existsSync(location) && existsSync(resolvePath(location, 'package.json'))) {
99
+ webDir = location;
100
+ visualizerAvailable = true;
101
+ break;
102
+ }
103
+ }
104
+
105
+ // Fallback: try require.resolve
106
+ if (!visualizerAvailable) {
107
+ try {
108
+ const vizPkgPath = require.resolve('@aiready/visualizer/package.json');
109
+ webDir = resolvePath(vizPkgPath, '..');
110
+ visualizerAvailable = true;
111
+ } catch (e) {
112
+ // Visualizer not found
113
+ }
114
+ }
115
+ }
116
+
117
+ // Check if web directory with vite config exists (required for dev mode)
118
+ const webViteConfigExists = webDir && existsSync(resolvePath(webDir, 'web', 'vite.config.ts'));
119
+
120
+ if (visualizerAvailable && webViteConfigExists) {
121
+ // Dev mode is available - start Vite dev server
122
+ const spawnCwd = webDir!;
123
+
124
+ // Inline report watcher: copy report to web/report-data.json and watch for changes
125
+ const { watch } = await import('fs');
126
+ const copyReportToViz = () => {
127
+ try {
128
+ const destPath = resolvePath(spawnCwd, 'web', 'report-data.json');
129
+ copyFileSync(reportPath!, destPath);
130
+ console.log(`📋 Report synced to ${destPath}`);
131
+ } catch (e) {
132
+ console.error('Failed to sync report:', e);
133
+ }
134
+ };
135
+
136
+ // Initial copy
137
+ copyReportToViz();
138
+
139
+ // Watch source report for changes
140
+ let watchTimeout: NodeJS.Timeout | null = null;
141
+ const reportWatcher = watch(reportPath, () => {
142
+ if (watchTimeout) clearTimeout(watchTimeout);
143
+ watchTimeout = setTimeout(copyReportToViz, 100);
144
+ });
145
+
146
+ const envForSpawn = {
147
+ ...process.env,
148
+ AIREADY_REPORT_PATH: reportPath,
149
+ AIREADY_VISUALIZER_CONFIG: envVisualizerConfig
150
+ };
151
+ const vite = spawn("pnpm", ["run", "dev:web"], { cwd: spawnCwd, stdio: "inherit", shell: true, env: envForSpawn });
152
+ const onExit = () => {
153
+ try { reportWatcher.close(); } catch (e) {}
154
+ try { vite.kill(); } catch (e) {}
155
+ process.exit(0);
156
+ };
157
+ process.on("SIGINT", onExit);
158
+ process.on("SIGTERM", onExit);
159
+ devServerStarted = true;
160
+ return;
161
+ } else {
162
+ console.log(chalk.yellow('⚠️ Dev server not available (requires local @aiready/visualizer with web assets).'));
163
+ console.log(chalk.cyan(' Falling back to static HTML generation...\n'));
164
+ useDevMode = false;
165
+ }
166
+ } catch (err) {
167
+ console.error("Failed to start dev server:", err);
168
+ console.log(chalk.cyan(' Falling back to static HTML generation...\n'));
169
+ useDevMode = false;
170
+ }
171
+ }
172
+
173
+ // Generate static HTML (default behavior or fallback from failed --dev)
174
+ console.log("Generating HTML...");
175
+ const html = generateHTML(graph);
176
+ const defaultOutput = 'visualization.html';
177
+ const outPath = resolvePath(dirPath, options.output || defaultOutput);
178
+ writeFileSync(outPath, html, 'utf8');
179
+ console.log(chalk.green(`✅ Visualization written to: ${outPath}`));
180
+
181
+
182
+ if (options.open || options.serve) {
183
+ const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
184
+
185
+ if (options.serve) {
186
+ try {
187
+ const port = typeof options.serve === 'number' ? options.serve : 5173;
188
+ const http = await import('http');
189
+ const fsp = await import('fs/promises');
190
+
191
+ const server = http.createServer(async (req, res) => {
192
+ try {
193
+ const urlPath = req.url || '/';
194
+ if (urlPath === '/' || urlPath === '/index.html') {
195
+ const content = await fsp.readFile(outPath, 'utf8');
196
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
197
+ res.end(content);
198
+ return;
199
+ }
200
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
201
+ res.end('Not found');
202
+ } catch (e: any) {
203
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
204
+ res.end('Server error');
205
+ }
206
+ });
207
+
208
+ server.listen(port, () => {
209
+ const addr = `http://localhost:${port}/`;
210
+ console.log(chalk.cyan(`🌐 Local visualization server running at ${addr}`));
211
+ spawn(opener, [`"${addr}"`], { shell: true });
212
+ });
213
+
214
+ process.on('SIGINT', () => {
215
+ server.close();
216
+ process.exit(0);
217
+ });
218
+ } catch (err) {
219
+ console.error('Failed to start local server:', err);
220
+ }
221
+ } else if (options.open) {
222
+ spawn(opener, [`"${outPath}"`], { shell: true });
223
+ }
224
+ }
225
+
226
+ } catch (err: any) {
227
+ handleCLIError(err, 'Visualization');
228
+ }
229
+ }
230
+
231
+ export const visualizeHelpText = `
232
+ EXAMPLES:
233
+ $ aiready visualize . # Auto-detects latest report, generates HTML
234
+ $ aiready visualize . --report .aiready/aiready-report-20260217-143022.json
235
+ $ aiready visualize . --report report.json -o out/visualization.html --open
236
+ $ aiready visualize . --report report.json --serve
237
+ $ aiready visualize . --report report.json --serve 8080
238
+ $ aiready visualize . --report report.json --dev
239
+
240
+ NOTES:
241
+ - The value passed to --report is interpreted relative to the directory argument (first positional).
242
+ If the report is not found, the CLI will suggest running 'aiready scan' to generate it.
243
+ - Default output path: visualization.html (in the current directory).
244
+ - --serve starts a tiny single-file HTTP server (default port: 5173) and opens your browser.
245
+ - --dev starts a Vite dev server with live reload (requires local @aiready/visualizer installation).
246
+ When --dev is not available, it falls back to static HTML generation.
247
+ `;
248
+
249
+ export const visualiseHelpText = `
250
+ EXAMPLES:
251
+ $ aiready visualise . # Auto-detects latest report
252
+ $ aiready visualise . --report .aiready/aiready-report-20260217-143022.json
253
+ $ aiready visualise . --report report.json --serve 8080
254
+
255
+ NOTES:
256
+ - Same options as 'visualize'. Use --serve to host the static HTML, or --dev for live reload.
257
+ `;
@@ -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
@@ -2,7 +2,10 @@
2
2
  "extends": "../core/tsconfig.json",
3
3
  "compilerOptions": {
4
4
  "outDir": "./dist",
5
- "rootDir": "./src"
5
+ "rootDir": "./src",
6
+ "types": ["node"],
7
+ "lib": ["ES2020"],
8
+ "moduleResolution": "bundler"
6
9
  },
7
10
  "include": ["src/**/*"]
8
- }
11
+ }