@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.
- package/dist/cli.js +503 -455
- package/dist/cli.mjs +523 -453
- 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 +257 -0
- package/src/utils/helpers.ts +133 -0
- package/tsconfig.json +5 -2
package/src/cli.ts
CHANGED
|
@@ -1,30 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
-
import {
|
|
5
|
-
import chalk from 'chalk';
|
|
6
|
-
import { writeFileSync } from 'fs';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
7
5
|
import { join } from 'path';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
generateHTML,
|
|
21
|
-
type AIReadyConfig,
|
|
22
|
-
type ToolScoringOutput,
|
|
23
|
-
} from '@aiready/core';
|
|
24
|
-
import { readFileSync, existsSync, copyFileSync } from 'fs';
|
|
25
|
-
import { resolve as resolvePath } from 'path';
|
|
26
|
-
import { GraphBuilder } from '@aiready/visualizer/graph';
|
|
27
|
-
import type { GraphData } from '@aiready/visualizer';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
scanAction,
|
|
9
|
+
scanHelpText,
|
|
10
|
+
patternsAction,
|
|
11
|
+
patternsHelpText,
|
|
12
|
+
contextAction,
|
|
13
|
+
consistencyAction,
|
|
14
|
+
visualizeAction,
|
|
15
|
+
visualizeHelpText,
|
|
16
|
+
visualiseHelpText,
|
|
17
|
+
} from './commands';
|
|
28
18
|
|
|
29
19
|
const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
|
|
30
20
|
|
|
@@ -71,6 +61,7 @@ DOCUMENTATION: https://aiready.dev/docs/cli
|
|
|
71
61
|
GITHUB: https://github.com/caopengau/aiready-cli
|
|
72
62
|
LANDING: https://github.com/caopengau/aiready-landing`);
|
|
73
63
|
|
|
64
|
+
// Scan command - Run comprehensive AI-readiness analysis
|
|
74
65
|
program
|
|
75
66
|
.command('scan')
|
|
76
67
|
.description('Run comprehensive AI-readiness analysis (patterns + context + consistency)')
|
|
@@ -85,430 +76,12 @@ program
|
|
|
85
76
|
.option('--threshold <score>', 'Fail CI/CD if score below threshold (0-100)')
|
|
86
77
|
.option('--ci', 'CI mode: GitHub Actions annotations, no colors, fail on threshold')
|
|
87
78
|
.option('--fail-on <level>', 'Fail on issues: critical, major, any', 'critical')
|
|
88
|
-
.addHelpText('after',
|
|
89
|
-
EXAMPLES:
|
|
90
|
-
$ aiready scan # Analyze all tools
|
|
91
|
-
$ aiready scan --tools patterns,context # Skip consistency
|
|
92
|
-
$ aiready scan --score --threshold 75 # CI/CD with threshold
|
|
93
|
-
$ aiready scan --ci --threshold 70 # GitHub Actions gatekeeper
|
|
94
|
-
$ aiready scan --ci --fail-on major # Fail on major+ issues
|
|
95
|
-
$ aiready scan --output json --output-file report.json
|
|
96
|
-
|
|
97
|
-
CI/CD INTEGRATION (Gatekeeper Mode):
|
|
98
|
-
Use --ci for GitHub Actions integration:
|
|
99
|
-
- Outputs GitHub Actions annotations for PR checks
|
|
100
|
-
- Fails with exit code 1 if threshold not met
|
|
101
|
-
- Shows clear "blocked" message with remediation steps
|
|
102
|
-
|
|
103
|
-
Example GitHub Actions workflow:
|
|
104
|
-
- name: AI Readiness Check
|
|
105
|
-
run: aiready scan --ci --threshold 70
|
|
106
|
-
`)
|
|
79
|
+
.addHelpText('after', scanHelpText)
|
|
107
80
|
.action(async (directory, options) => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const startTime = Date.now();
|
|
111
|
-
// Resolve directory to absolute path to ensure .aiready/ is created in the right location
|
|
112
|
-
const resolvedDir = resolvePath(process.cwd(), directory || '.');
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
// Define defaults
|
|
116
|
-
const defaults = {
|
|
117
|
-
tools: ['patterns', 'context', 'consistency'],
|
|
118
|
-
include: undefined,
|
|
119
|
-
exclude: undefined,
|
|
120
|
-
output: {
|
|
121
|
-
format: 'json',
|
|
122
|
-
file: undefined,
|
|
123
|
-
},
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
// Load and merge config with CLI options
|
|
127
|
-
const baseOptions = await loadMergedConfig(resolvedDir, defaults, {
|
|
128
|
-
tools: options.tools ? options.tools.split(',').map((t: string) => t.trim()) as ('patterns' | 'context' | 'consistency')[] : undefined,
|
|
129
|
-
include: options.include?.split(','),
|
|
130
|
-
exclude: options.exclude?.split(','),
|
|
131
|
-
}) as any;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// Apply smart defaults for pattern detection if patterns tool is enabled
|
|
135
|
-
let finalOptions = { ...baseOptions };
|
|
136
|
-
if (baseOptions.tools.includes('patterns')) {
|
|
137
|
-
const { getSmartDefaults } = await import('@aiready/pattern-detect');
|
|
138
|
-
const patternSmartDefaults = await getSmartDefaults(resolvedDir, baseOptions);
|
|
139
|
-
// Merge deeply to preserve nested config
|
|
140
|
-
finalOptions = { ...patternSmartDefaults, ...finalOptions, ...baseOptions };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Print pre-run summary with expanded settings (truncate long arrays)
|
|
144
|
-
console.log(chalk.cyan('\n=== AIReady Run Preview ==='));
|
|
145
|
-
console.log(chalk.white('Tools to run:'), (finalOptions.tools || ['patterns', 'context', 'consistency']).join(', '));
|
|
146
|
-
console.log(chalk.white('Will use settings from config and defaults.'));
|
|
147
|
-
|
|
148
|
-
const truncate = (arr: any[] | undefined, cap = 8) => {
|
|
149
|
-
if (!Array.isArray(arr)) return '';
|
|
150
|
-
const shown = arr.slice(0, cap).map((v) => String(v));
|
|
151
|
-
const more = arr.length - shown.length;
|
|
152
|
-
return shown.join(', ') + (more > 0 ? `, ... (+${more} more)` : '');
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
// Common top-level settings
|
|
156
|
-
console.log(chalk.white('\nGeneral settings:'));
|
|
157
|
-
if (finalOptions.rootDir) console.log(` rootDir: ${chalk.bold(String(finalOptions.rootDir))}`);
|
|
158
|
-
if (finalOptions.include) console.log(` include: ${chalk.bold(truncate(finalOptions.include, 6))}`);
|
|
159
|
-
if (finalOptions.exclude) console.log(` exclude: ${chalk.bold(truncate(finalOptions.exclude, 6))}`);
|
|
160
|
-
|
|
161
|
-
if (finalOptions['pattern-detect'] || finalOptions.minSimilarity) {
|
|
162
|
-
const patternDetectConfig = finalOptions['pattern-detect'] || {
|
|
163
|
-
minSimilarity: finalOptions.minSimilarity,
|
|
164
|
-
minLines: finalOptions.minLines,
|
|
165
|
-
approx: finalOptions.approx,
|
|
166
|
-
minSharedTokens: finalOptions.minSharedTokens,
|
|
167
|
-
maxCandidatesPerBlock: finalOptions.maxCandidatesPerBlock,
|
|
168
|
-
batchSize: finalOptions.batchSize,
|
|
169
|
-
streamResults: finalOptions.streamResults,
|
|
170
|
-
severity: (finalOptions as any).severity,
|
|
171
|
-
includeTests: (finalOptions as any).includeTests,
|
|
172
|
-
};
|
|
173
|
-
console.log(chalk.white('\nPattern-detect settings:'));
|
|
174
|
-
console.log(` minSimilarity: ${chalk.bold(patternDetectConfig.minSimilarity ?? 'default')}`);
|
|
175
|
-
console.log(` minLines: ${chalk.bold(patternDetectConfig.minLines ?? 'default')}`);
|
|
176
|
-
if (patternDetectConfig.approx !== undefined) console.log(` approx: ${chalk.bold(String(patternDetectConfig.approx))}`);
|
|
177
|
-
if (patternDetectConfig.minSharedTokens !== undefined) console.log(` minSharedTokens: ${chalk.bold(String(patternDetectConfig.minSharedTokens))}`);
|
|
178
|
-
if (patternDetectConfig.maxCandidatesPerBlock !== undefined) console.log(` maxCandidatesPerBlock: ${chalk.bold(String(patternDetectConfig.maxCandidatesPerBlock))}`);
|
|
179
|
-
if (patternDetectConfig.batchSize !== undefined) console.log(` batchSize: ${chalk.bold(String(patternDetectConfig.batchSize))}`);
|
|
180
|
-
if (patternDetectConfig.streamResults !== undefined) console.log(` streamResults: ${chalk.bold(String(patternDetectConfig.streamResults))}`);
|
|
181
|
-
if (patternDetectConfig.severity !== undefined) console.log(` severity: ${chalk.bold(String(patternDetectConfig.severity))}`);
|
|
182
|
-
if (patternDetectConfig.includeTests !== undefined) console.log(` includeTests: ${chalk.bold(String(patternDetectConfig.includeTests))}`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (finalOptions['context-analyzer'] || finalOptions.maxDepth) {
|
|
186
|
-
const ca = finalOptions['context-analyzer'] || {
|
|
187
|
-
maxDepth: finalOptions.maxDepth,
|
|
188
|
-
maxContextBudget: finalOptions.maxContextBudget,
|
|
189
|
-
minCohesion: (finalOptions as any).minCohesion,
|
|
190
|
-
maxFragmentation: (finalOptions as any).maxFragmentation,
|
|
191
|
-
includeNodeModules: (finalOptions as any).includeNodeModules,
|
|
192
|
-
};
|
|
193
|
-
console.log(chalk.white('\nContext-analyzer settings:'));
|
|
194
|
-
console.log(` maxDepth: ${chalk.bold(ca.maxDepth ?? 'default')}`);
|
|
195
|
-
console.log(` maxContextBudget: ${chalk.bold(ca.maxContextBudget ?? 'default')}`);
|
|
196
|
-
if (ca.minCohesion !== undefined) console.log(` minCohesion: ${chalk.bold(String(ca.minCohesion))}`);
|
|
197
|
-
if (ca.maxFragmentation !== undefined) console.log(` maxFragmentation: ${chalk.bold(String(ca.maxFragmentation))}`);
|
|
198
|
-
if (ca.includeNodeModules !== undefined) console.log(` includeNodeModules: ${chalk.bold(String(ca.includeNodeModules))}`);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (finalOptions.consistency) {
|
|
202
|
-
const c = finalOptions.consistency;
|
|
203
|
-
console.log(chalk.white('\nConsistency settings:'));
|
|
204
|
-
console.log(` checkNaming: ${chalk.bold(String(c.checkNaming ?? true))}`);
|
|
205
|
-
console.log(` checkPatterns: ${chalk.bold(String(c.checkPatterns ?? true))}`);
|
|
206
|
-
console.log(` checkArchitecture: ${chalk.bold(String(c.checkArchitecture ?? false))}`);
|
|
207
|
-
if (c.minSeverity) console.log(` minSeverity: ${chalk.bold(c.minSeverity)}`);
|
|
208
|
-
if (c.acceptedAbbreviations) console.log(` acceptedAbbreviations: ${chalk.bold(truncate(c.acceptedAbbreviations, 8))}`);
|
|
209
|
-
if (c.shortWords) console.log(` shortWords: ${chalk.bold(truncate(c.shortWords, 8))}`);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
console.log(chalk.white('\nStarting analysis...'));
|
|
213
|
-
|
|
214
|
-
// Progress callback to surface per-tool output as each tool finishes
|
|
215
|
-
const progressCallback = (event: { tool: string; data: any }) => {
|
|
216
|
-
console.log(chalk.cyan(`\n--- ${event.tool.toUpperCase()} RESULTS ---`));
|
|
217
|
-
try {
|
|
218
|
-
if (event.tool === 'patterns') {
|
|
219
|
-
const pr = event.data as any;
|
|
220
|
-
console.log(` Duplicate patterns: ${chalk.bold(String(pr.duplicates?.length || 0))}`);
|
|
221
|
-
console.log(` Files with pattern issues: ${chalk.bold(String(pr.results?.length || 0))}`);
|
|
222
|
-
// show top duplicate summaries
|
|
223
|
-
if (pr.duplicates && pr.duplicates.length > 0) {
|
|
224
|
-
pr.duplicates.slice(0, 5).forEach((d: any, i: number) => {
|
|
225
|
-
console.log(` ${i + 1}. ${d.file1.split('/').pop()} ↔ ${d.file2.split('/').pop()} (sim=${(d.similarity * 100).toFixed(1)}%)`);
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// show top files with pattern issues (sorted by issue count desc)
|
|
230
|
-
if (pr.results && pr.results.length > 0) {
|
|
231
|
-
console.log(` Top files with pattern issues:`);
|
|
232
|
-
const sortedByIssues = [...pr.results].sort((a: any, b: any) => (b.issues?.length || 0) - (a.issues?.length || 0));
|
|
233
|
-
sortedByIssues.slice(0, 5).forEach((r: any, i: number) => {
|
|
234
|
-
console.log(` ${i + 1}. ${r.fileName.split('/').pop()} - ${r.issues.length} issue(s)`);
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Grouping and clusters summary (if available) — show after detailed findings
|
|
239
|
-
if (pr.groups && pr.groups.length >= 0) {
|
|
240
|
-
console.log(` ✅ Grouped ${chalk.bold(String(pr.duplicates?.length || 0))} duplicates into ${chalk.bold(String(pr.groups.length))} file pairs`);
|
|
241
|
-
}
|
|
242
|
-
if (pr.clusters && pr.clusters.length >= 0) {
|
|
243
|
-
console.log(` ✅ Created ${chalk.bold(String(pr.clusters.length))} refactor clusters`);
|
|
244
|
-
// show brief cluster summaries
|
|
245
|
-
pr.clusters.slice(0, 3).forEach((cl: any, idx: number) => {
|
|
246
|
-
const files = (cl.files || []).map((f: any) => f.path.split('/').pop()).join(', ');
|
|
247
|
-
console.log(` ${idx + 1}. ${files} (${cl.tokenCost || 'n/a'} tokens)`);
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
} else if (event.tool === 'context') {
|
|
251
|
-
const cr = event.data as any[];
|
|
252
|
-
console.log(` Context issues found: ${chalk.bold(String(cr.length || 0))}`);
|
|
253
|
-
cr.slice(0, 5).forEach((c: any, i: number) => {
|
|
254
|
-
const msg = c.message ? ` - ${c.message}` : '';
|
|
255
|
-
console.log(` ${i + 1}. ${c.file} (${c.severity || 'n/a'})${msg}`);
|
|
256
|
-
});
|
|
257
|
-
} else if (event.tool === 'consistency') {
|
|
258
|
-
const rep = event.data as any;
|
|
259
|
-
console.log(` Consistency totalIssues: ${chalk.bold(String(rep.summary?.totalIssues || 0))}`);
|
|
260
|
-
|
|
261
|
-
if (rep.results && rep.results.length > 0) {
|
|
262
|
-
// Group issues by file
|
|
263
|
-
const fileMap = new Map<string, any[]>();
|
|
264
|
-
rep.results.forEach((r: any) => {
|
|
265
|
-
(r.issues || []).forEach((issue: any) => {
|
|
266
|
-
const file = issue.location?.file || r.file || 'unknown';
|
|
267
|
-
if (!fileMap.has(file)) fileMap.set(file, []);
|
|
268
|
-
fileMap.get(file)!.push(issue);
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
// Sort files by number of issues desc
|
|
273
|
-
const files = Array.from(fileMap.entries()).sort((a, b) => b[1].length - a[1].length);
|
|
274
|
-
const topFiles = files.slice(0, 10);
|
|
275
|
-
|
|
276
|
-
topFiles.forEach(([file, issues], idx) => {
|
|
277
|
-
// Count severities
|
|
278
|
-
const counts = issues.reduce((acc: any, it: any) => {
|
|
279
|
-
const s = (it.severity || 'info').toLowerCase();
|
|
280
|
-
acc[s] = (acc[s] || 0) + 1;
|
|
281
|
-
return acc;
|
|
282
|
-
}, {} as Record<string, number>);
|
|
283
|
-
|
|
284
|
-
const sample = issues.find((it: any) => it.severity === 'critical' || it.severity === 'major') || issues[0];
|
|
285
|
-
const sampleMsg = sample ? ` — ${sample.message}` : '';
|
|
286
|
-
|
|
287
|
-
console.log(` ${idx + 1}. ${file} — ${issues.length} issue(s) (critical:${counts.critical||0} major:${counts.major||0} minor:${counts.minor||0} info:${counts.info||0})${sampleMsg}`);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
const remaining = files.length - topFiles.length;
|
|
291
|
-
if (remaining > 0) {
|
|
292
|
-
console.log(chalk.dim(` ... and ${remaining} more files with issues (use --output json for full details)`));
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
} catch (err) {
|
|
297
|
-
// don't crash the run for progress printing errors
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
const results = await analyzeUnified({ ...finalOptions, progressCallback, suppressToolConfig: true });
|
|
302
|
-
|
|
303
|
-
// Summarize tools and results to console
|
|
304
|
-
console.log(chalk.cyan('\n=== AIReady Run Summary ==='));
|
|
305
|
-
console.log(chalk.white('Tools run:'), (finalOptions.tools || ['patterns', 'context', 'consistency']).join(', '));
|
|
306
|
-
|
|
307
|
-
// Results summary
|
|
308
|
-
console.log(chalk.cyan('\nResults summary:'));
|
|
309
|
-
console.log(` Total issues (all tools): ${chalk.bold(String(results.summary.totalIssues || 0))}`);
|
|
310
|
-
if (results.duplicates) console.log(` Duplicate patterns found: ${chalk.bold(String(results.duplicates.length || 0))}`);
|
|
311
|
-
if (results.patterns) console.log(` Pattern files with issues: ${chalk.bold(String(results.patterns.length || 0))}`);
|
|
312
|
-
if (results.context) console.log(` Context issues: ${chalk.bold(String(results.context.length || 0))}`);
|
|
313
|
-
if (results.consistency) console.log(` Consistency issues: ${chalk.bold(String(results.consistency.summary.totalIssues || 0))}`);
|
|
314
|
-
console.log(chalk.cyan('===========================\n'));
|
|
315
|
-
|
|
316
|
-
const elapsedTime = getElapsedTime(startTime);
|
|
317
|
-
|
|
318
|
-
// Calculate score if requested: assemble per-tool scoring outputs
|
|
319
|
-
let scoringResult: ReturnType<typeof calculateOverallScore> | undefined;
|
|
320
|
-
if (options.score || finalOptions.scoring?.showBreakdown) {
|
|
321
|
-
const toolScores: Map<string, ToolScoringOutput> = new Map();
|
|
322
|
-
|
|
323
|
-
// Patterns score
|
|
324
|
-
if (results.duplicates) {
|
|
325
|
-
const { calculatePatternScore } = await import('@aiready/pattern-detect');
|
|
326
|
-
try {
|
|
327
|
-
const patternScore = calculatePatternScore(results.duplicates, results.patterns?.length || 0);
|
|
328
|
-
toolScores.set('pattern-detect', patternScore);
|
|
329
|
-
} catch (err) {
|
|
330
|
-
// ignore scoring failures for a single tool
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Context score
|
|
335
|
-
if (results.context) {
|
|
336
|
-
const { generateSummary: genContextSummary, calculateContextScore } = await import('@aiready/context-analyzer');
|
|
337
|
-
try {
|
|
338
|
-
const ctxSummary = genContextSummary(results.context);
|
|
339
|
-
const contextScore = calculateContextScore(ctxSummary);
|
|
340
|
-
toolScores.set('context-analyzer', contextScore);
|
|
341
|
-
} catch (err) {
|
|
342
|
-
// ignore
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Consistency score
|
|
347
|
-
if (results.consistency) {
|
|
348
|
-
const { calculateConsistencyScore } = await import('@aiready/consistency');
|
|
349
|
-
try {
|
|
350
|
-
const issues = results.consistency.results?.flatMap((r: any) => r.issues) || [];
|
|
351
|
-
const totalFiles = results.consistency.summary?.filesAnalyzed || 0;
|
|
352
|
-
const consistencyScore = calculateConsistencyScore(issues, totalFiles);
|
|
353
|
-
toolScores.set('consistency', consistencyScore);
|
|
354
|
-
} catch (err) {
|
|
355
|
-
// ignore
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Parse CLI weight overrides (if any)
|
|
360
|
-
const cliWeights = parseWeightString((options as any).weights);
|
|
361
|
-
|
|
362
|
-
// Only calculate overall score if we have at least one tool score
|
|
363
|
-
if (toolScores.size > 0) {
|
|
364
|
-
scoringResult = calculateOverallScore(toolScores, finalOptions, cliWeights.size ? cliWeights : undefined);
|
|
365
|
-
|
|
366
|
-
console.log(chalk.bold('\n📊 AI Readiness Overall Score'));
|
|
367
|
-
console.log(` ${formatScore(scoringResult)}`);
|
|
368
|
-
|
|
369
|
-
// Show concise breakdown; detailed breakdown only if config requests it
|
|
370
|
-
if (scoringResult.breakdown && scoringResult.breakdown.length > 0) {
|
|
371
|
-
console.log(chalk.bold('\nTool breakdown:'));
|
|
372
|
-
scoringResult.breakdown.forEach((tool) => {
|
|
373
|
-
const rating = getRating(tool.score);
|
|
374
|
-
const rd = getRatingDisplay(rating);
|
|
375
|
-
console.log(` - ${tool.toolName}: ${tool.score}/100 (${rating}) ${rd.emoji}`);
|
|
376
|
-
});
|
|
377
|
-
console.log();
|
|
378
|
-
|
|
379
|
-
if (finalOptions.scoring?.showBreakdown) {
|
|
380
|
-
console.log(chalk.bold('Detailed tool breakdown:'));
|
|
381
|
-
scoringResult.breakdown.forEach((tool) => {
|
|
382
|
-
console.log(formatToolScore(tool));
|
|
383
|
-
});
|
|
384
|
-
console.log();
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Persist JSON summary when output format is json
|
|
391
|
-
const outputFormat = options.output || finalOptions.output?.format || 'console';
|
|
392
|
-
const userOutputFile = options.outputFile || finalOptions.output?.file;
|
|
393
|
-
if (outputFormat === 'json') {
|
|
394
|
-
const timestamp = getReportTimestamp();
|
|
395
|
-
const defaultFilename = `aiready-report-${timestamp}.json`;
|
|
396
|
-
const outputPath = resolveOutputPath(userOutputFile, defaultFilename, resolvedDir);
|
|
397
|
-
const outputData = { ...results, scoring: scoringResult };
|
|
398
|
-
handleJSONOutput(outputData, outputPath, `✅ Report saved to ${outputPath}`);
|
|
399
|
-
|
|
400
|
-
// Warn if graph caps may be exceeded
|
|
401
|
-
warnIfGraphCapExceeded(outputData, resolvedDir);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// CI/CD Gatekeeper Mode
|
|
405
|
-
const isCI = options.ci || process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
|
|
406
|
-
if (isCI && scoringResult) {
|
|
407
|
-
const threshold = options.threshold ? parseInt(options.threshold) : undefined;
|
|
408
|
-
const failOnLevel = options.failOn || 'critical';
|
|
409
|
-
|
|
410
|
-
// Output GitHub Actions annotations
|
|
411
|
-
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
412
|
-
console.log(`\n::group::AI Readiness Score`);
|
|
413
|
-
console.log(`score=${scoringResult.overallScore}`);
|
|
414
|
-
if (scoringResult.breakdown) {
|
|
415
|
-
scoringResult.breakdown.forEach(tool => {
|
|
416
|
-
console.log(`${tool.toolName}=${tool.score}`);
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
console.log('::endgroup::');
|
|
420
|
-
|
|
421
|
-
// Output annotation for score
|
|
422
|
-
if (threshold && scoringResult.overallScore < threshold) {
|
|
423
|
-
console.log(`::error::AI Readiness Score ${scoringResult.overallScore} is below threshold ${threshold}`);
|
|
424
|
-
} else if (threshold) {
|
|
425
|
-
console.log(`::notice::AI Readiness Score: ${scoringResult.overallScore}/100 (threshold: ${threshold})`);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Output annotations for critical issues
|
|
429
|
-
if (results.patterns) {
|
|
430
|
-
const criticalPatterns = results.patterns.flatMap((p: any) =>
|
|
431
|
-
p.issues.filter((i: any) => i.severity === 'critical')
|
|
432
|
-
);
|
|
433
|
-
criticalPatterns.slice(0, 10).forEach((issue: any) => {
|
|
434
|
-
console.log(`::warning file=${issue.location?.file || 'unknown'},line=${issue.location?.line || 1}::${issue.message}`);
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Determine if we should fail
|
|
440
|
-
let shouldFail = false;
|
|
441
|
-
let failReason = '';
|
|
442
|
-
|
|
443
|
-
// Check threshold
|
|
444
|
-
if (threshold && scoringResult.overallScore < threshold) {
|
|
445
|
-
shouldFail = true;
|
|
446
|
-
failReason = `AI Readiness Score ${scoringResult.overallScore} is below threshold ${threshold}`;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Check fail-on severity
|
|
450
|
-
if (failOnLevel !== 'none') {
|
|
451
|
-
const severityLevels = { critical: 4, major: 3, minor: 2, any: 1 };
|
|
452
|
-
const minSeverity = severityLevels[failOnLevel as keyof typeof severityLevels] || 4;
|
|
453
|
-
|
|
454
|
-
let criticalCount = 0;
|
|
455
|
-
let majorCount = 0;
|
|
456
|
-
|
|
457
|
-
if (results.patterns) {
|
|
458
|
-
results.patterns.forEach((p: any) => {
|
|
459
|
-
p.issues.forEach((i: any) => {
|
|
460
|
-
if (i.severity === 'critical') criticalCount++;
|
|
461
|
-
if (i.severity === 'major') majorCount++;
|
|
462
|
-
});
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
if (results.context) {
|
|
466
|
-
results.context.forEach((c: any) => {
|
|
467
|
-
if (c.severity === 'critical') criticalCount++;
|
|
468
|
-
if (c.severity === 'major') majorCount++;
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
-
if (results.consistency?.results) {
|
|
472
|
-
results.consistency.results.forEach((r: any) => {
|
|
473
|
-
r.issues?.forEach((i: any) => {
|
|
474
|
-
if (i.severity === 'critical') criticalCount++;
|
|
475
|
-
if (i.severity === 'major') majorCount++;
|
|
476
|
-
});
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (minSeverity >= 4 && criticalCount > 0) {
|
|
481
|
-
shouldFail = true;
|
|
482
|
-
failReason = `Found ${criticalCount} critical issues`;
|
|
483
|
-
} else if (minSeverity >= 3 && (criticalCount + majorCount) > 0) {
|
|
484
|
-
shouldFail = true;
|
|
485
|
-
failReason = `Found ${criticalCount} critical and ${majorCount} major issues`;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Output result
|
|
490
|
-
if (shouldFail) {
|
|
491
|
-
console.log(chalk.red('\n🚫 PR BLOCKED: AI Readiness Check Failed'));
|
|
492
|
-
console.log(chalk.red(` Reason: ${failReason}`));
|
|
493
|
-
console.log(chalk.dim('\n Remediation steps:'));
|
|
494
|
-
console.log(chalk.dim(' 1. Run `aiready scan` locally to see detailed issues'));
|
|
495
|
-
console.log(chalk.dim(' 2. Fix the critical issues before merging'));
|
|
496
|
-
console.log(chalk.dim(' 3. Consider upgrading to Team plan for historical tracking: https://getaiready.dev/pricing'));
|
|
497
|
-
process.exit(1);
|
|
498
|
-
} else {
|
|
499
|
-
console.log(chalk.green('\n✅ PR PASSED: AI Readiness Check'));
|
|
500
|
-
if (threshold) {
|
|
501
|
-
console.log(chalk.green(` Score: ${scoringResult.overallScore}/100 (threshold: ${threshold})`));
|
|
502
|
-
}
|
|
503
|
-
console.log(chalk.dim('\n 💡 Track historical trends: https://getaiready.dev — Team plan $99/mo'));
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
} catch (error) {
|
|
507
|
-
handleCLIError(error, 'Analysis');
|
|
508
|
-
}
|
|
81
|
+
await scanAction(directory, options);
|
|
509
82
|
});
|
|
510
83
|
|
|
511
|
-
//
|
|
84
|
+
// Patterns command - Detect duplicate code patterns
|
|
512
85
|
program
|
|
513
86
|
.command('patterns')
|
|
514
87
|
.description('Detect duplicate code patterns that confuse AI models')
|
|
@@ -523,155 +96,12 @@ program
|
|
|
523
96
|
.option('-o, --output <format>', 'Output format: console, json', 'console')
|
|
524
97
|
.option('--output-file <path>', 'Output file path (for json)')
|
|
525
98
|
.option('--score', 'Calculate and display AI Readiness Score for patterns (0-100)')
|
|
526
|
-
.addHelpText('after',
|
|
527
|
-
EXAMPLES:
|
|
528
|
-
$ aiready patterns # Default analysis
|
|
529
|
-
$ aiready patterns --similarity 0.6 # Stricter matching
|
|
530
|
-
$ aiready patterns --min-lines 10 # Larger patterns only
|
|
531
|
-
`)
|
|
99
|
+
.addHelpText('after', patternsHelpText)
|
|
532
100
|
.action(async (directory, options) => {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const startTime = Date.now();
|
|
536
|
-
const resolvedDir = resolvePath(process.cwd(), directory || '.');
|
|
537
|
-
|
|
538
|
-
try {
|
|
539
|
-
// Determine if smart defaults should be used
|
|
540
|
-
const useSmartDefaults = !options.fullScan;
|
|
541
|
-
|
|
542
|
-
// Define defaults (only for options not handled by smart defaults)
|
|
543
|
-
const defaults = {
|
|
544
|
-
useSmartDefaults,
|
|
545
|
-
include: undefined,
|
|
546
|
-
exclude: undefined,
|
|
547
|
-
output: {
|
|
548
|
-
format: 'console',
|
|
549
|
-
file: undefined,
|
|
550
|
-
},
|
|
551
|
-
};
|
|
552
|
-
|
|
553
|
-
// Set fallback defaults only if smart defaults are disabled
|
|
554
|
-
if (!useSmartDefaults) {
|
|
555
|
-
(defaults as any).minSimilarity = 0.4;
|
|
556
|
-
(defaults as any).minLines = 5;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Load and merge config with CLI options
|
|
560
|
-
const cliOptions: any = {
|
|
561
|
-
minSimilarity: options.similarity ? parseFloat(options.similarity) : undefined,
|
|
562
|
-
minLines: options.minLines ? parseInt(options.minLines) : undefined,
|
|
563
|
-
useSmartDefaults,
|
|
564
|
-
include: options.include?.split(','),
|
|
565
|
-
exclude: options.exclude?.split(','),
|
|
566
|
-
};
|
|
567
|
-
|
|
568
|
-
// Only include performance tuning options if explicitly specified
|
|
569
|
-
if (options.maxCandidates) {
|
|
570
|
-
cliOptions.maxCandidatesPerBlock = parseInt(options.maxCandidates);
|
|
571
|
-
}
|
|
572
|
-
if (options.minSharedTokens) {
|
|
573
|
-
cliOptions.minSharedTokens = parseInt(options.minSharedTokens);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const finalOptions = await loadMergedConfig(resolvedDir, defaults, cliOptions);
|
|
577
|
-
|
|
578
|
-
const { analyzePatterns, generateSummary, calculatePatternScore } = await import('@aiready/pattern-detect');
|
|
579
|
-
|
|
580
|
-
const { results, duplicates } = await analyzePatterns(finalOptions);
|
|
581
|
-
|
|
582
|
-
const elapsedTime = getElapsedTime(startTime);
|
|
583
|
-
const summary = generateSummary(results);
|
|
584
|
-
|
|
585
|
-
// Calculate score if requested
|
|
586
|
-
let patternScore: ToolScoringOutput | undefined;
|
|
587
|
-
if (options.score) {
|
|
588
|
-
patternScore = calculatePatternScore(duplicates, results.length);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const outputFormat = options.output || finalOptions.output?.format || 'console';
|
|
592
|
-
const userOutputFile = options.outputFile || finalOptions.output?.file;
|
|
593
|
-
|
|
594
|
-
if (outputFormat === 'json') {
|
|
595
|
-
const outputData = {
|
|
596
|
-
results,
|
|
597
|
-
summary: { ...summary, executionTime: parseFloat(elapsedTime) },
|
|
598
|
-
...(patternScore && { scoring: patternScore }),
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
const outputPath = resolveOutputPath(
|
|
602
|
-
userOutputFile,
|
|
603
|
-
`aiready-report-${getReportTimestamp()}.json`,
|
|
604
|
-
resolvedDir
|
|
605
|
-
);
|
|
606
|
-
|
|
607
|
-
handleJSONOutput(outputData, outputPath, `✅ Results saved to ${outputPath}`);
|
|
608
|
-
} else {
|
|
609
|
-
// Console output - format to match standalone CLI
|
|
610
|
-
const terminalWidth = process.stdout.columns || 80;
|
|
611
|
-
const dividerWidth = Math.min(60, terminalWidth - 2);
|
|
612
|
-
const divider = '━'.repeat(dividerWidth);
|
|
613
|
-
|
|
614
|
-
console.log(chalk.cyan(divider));
|
|
615
|
-
console.log(chalk.bold.white(' PATTERN ANALYSIS SUMMARY'));
|
|
616
|
-
console.log(chalk.cyan(divider) + '\n');
|
|
617
|
-
|
|
618
|
-
console.log(chalk.white(`📁 Files analyzed: ${chalk.bold(results.length)}`));
|
|
619
|
-
console.log(chalk.yellow(`⚠ Duplicate patterns found: ${chalk.bold(summary.totalPatterns)}`));
|
|
620
|
-
console.log(chalk.red(`💰 Token cost (wasted): ${chalk.bold(summary.totalTokenCost.toLocaleString())}`));
|
|
621
|
-
console.log(chalk.gray(`⏱ Analysis time: ${chalk.bold(elapsedTime + 's')}`));
|
|
622
|
-
|
|
623
|
-
// Show breakdown by pattern type
|
|
624
|
-
const sortedTypes = Object.entries(summary.patternsByType || {})
|
|
625
|
-
.filter(([, count]) => count > 0)
|
|
626
|
-
.sort(([, a], [, b]) => (b as number) - (a as number));
|
|
627
|
-
|
|
628
|
-
if (sortedTypes.length > 0) {
|
|
629
|
-
console.log(chalk.cyan('\n' + divider));
|
|
630
|
-
console.log(chalk.bold.white(' PATTERNS BY TYPE'));
|
|
631
|
-
console.log(chalk.cyan(divider) + '\n');
|
|
632
|
-
sortedTypes.forEach(([type, count]) => {
|
|
633
|
-
console.log(` ${chalk.white(type.padEnd(15))} ${chalk.bold(count)}`);
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// Show top duplicates
|
|
638
|
-
if (summary.totalPatterns > 0 && duplicates.length > 0) {
|
|
639
|
-
console.log(chalk.cyan('\n' + divider));
|
|
640
|
-
console.log(chalk.bold.white(' TOP DUPLICATE PATTERNS'));
|
|
641
|
-
console.log(chalk.cyan(divider) + '\n');
|
|
642
|
-
|
|
643
|
-
// Sort by similarity and take top 10
|
|
644
|
-
const topDuplicates = [...duplicates]
|
|
645
|
-
.sort((a, b) => b.similarity - a.similarity)
|
|
646
|
-
.slice(0, 10);
|
|
647
|
-
|
|
648
|
-
topDuplicates.forEach((dup) => {
|
|
649
|
-
const severity = dup.similarity > 0.95 ? 'CRITICAL' : dup.similarity > 0.9 ? 'HIGH' : 'MEDIUM';
|
|
650
|
-
const severityIcon = dup.similarity > 0.95 ? '🔴' : dup.similarity > 0.9 ? '🟡' : '🔵';
|
|
651
|
-
const file1Name = dup.file1.split('/').pop() || dup.file1;
|
|
652
|
-
const file2Name = dup.file2.split('/').pop() || dup.file2;
|
|
653
|
-
console.log(`${severityIcon} ${severity}: ${chalk.bold(file1Name)} ↔ ${chalk.bold(file2Name)}`);
|
|
654
|
-
console.log(` Similarity: ${chalk.bold(Math.round(dup.similarity * 100) + '%')} | Wasted: ${chalk.bold(dup.tokenCost.toLocaleString())} tokens each`);
|
|
655
|
-
console.log(` Lines: ${chalk.cyan(dup.line1 + '-' + dup.endLine1)} ↔ ${chalk.cyan(dup.line2 + '-' + dup.endLine2)}\n`);
|
|
656
|
-
});
|
|
657
|
-
} else {
|
|
658
|
-
console.log(chalk.green('\n✨ Great! No duplicate patterns detected.\n'));
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// Display score if calculated
|
|
662
|
-
if (patternScore) {
|
|
663
|
-
console.log(chalk.cyan(divider));
|
|
664
|
-
console.log(chalk.bold.white(' AI READINESS SCORE (Patterns)'));
|
|
665
|
-
console.log(chalk.cyan(divider) + '\n');
|
|
666
|
-
console.log(formatToolScore(patternScore));
|
|
667
|
-
console.log();
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
} catch (error) {
|
|
671
|
-
handleCLIError(error, 'Pattern analysis');
|
|
672
|
-
}
|
|
101
|
+
await patternsAction(directory, options);
|
|
673
102
|
});
|
|
674
103
|
|
|
104
|
+
// Context command - Analyze context window costs
|
|
675
105
|
program
|
|
676
106
|
.command('context')
|
|
677
107
|
.description('Analyze context window costs and dependency fragmentation')
|
|
@@ -684,671 +114,29 @@ program
|
|
|
684
114
|
.option('--output-file <path>', 'Output file path (for json)')
|
|
685
115
|
.option('--score', 'Calculate and display AI Readiness Score for context (0-100)')
|
|
686
116
|
.action(async (directory, options) => {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const startTime = Date.now();
|
|
690
|
-
const resolvedDir = resolvePath(process.cwd(), directory || '.');
|
|
691
|
-
|
|
692
|
-
try {
|
|
693
|
-
// Define defaults
|
|
694
|
-
const defaults = {
|
|
695
|
-
maxDepth: 5,
|
|
696
|
-
maxContextBudget: 10000,
|
|
697
|
-
include: undefined,
|
|
698
|
-
exclude: undefined,
|
|
699
|
-
output: {
|
|
700
|
-
format: 'console',
|
|
701
|
-
file: undefined,
|
|
702
|
-
},
|
|
703
|
-
};
|
|
704
|
-
|
|
705
|
-
// Load and merge config with CLI options
|
|
706
|
-
let baseOptions = await loadMergedConfig(resolvedDir, defaults, {
|
|
707
|
-
maxDepth: options.maxDepth ? parseInt(options.maxDepth) : undefined,
|
|
708
|
-
maxContextBudget: options.maxContext ? parseInt(options.maxContext) : undefined,
|
|
709
|
-
include: options.include?.split(','),
|
|
710
|
-
exclude: options.exclude?.split(','),
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
// Apply smart defaults for context analysis (always for individual context command)
|
|
714
|
-
let finalOptions: any = { ...baseOptions };
|
|
715
|
-
const { getSmartDefaults } = await import('@aiready/context-analyzer');
|
|
716
|
-
const contextSmartDefaults = await getSmartDefaults(resolvedDir, baseOptions);
|
|
717
|
-
finalOptions = { ...contextSmartDefaults, ...finalOptions };
|
|
718
|
-
|
|
719
|
-
// Display configuration
|
|
720
|
-
console.log('📋 Configuration:');
|
|
721
|
-
console.log(` Max depth: ${finalOptions.maxDepth}`);
|
|
722
|
-
console.log(` Max context budget: ${finalOptions.maxContextBudget}`);
|
|
723
|
-
console.log(` Min cohesion: ${(finalOptions.minCohesion * 100).toFixed(1)}%`);
|
|
724
|
-
console.log(` Max fragmentation: ${(finalOptions.maxFragmentation * 100).toFixed(1)}%`);
|
|
725
|
-
console.log(` Analysis focus: ${finalOptions.focus}`);
|
|
726
|
-
console.log('');
|
|
727
|
-
|
|
728
|
-
const { analyzeContext, generateSummary, calculateContextScore } = await import('@aiready/context-analyzer');
|
|
729
|
-
|
|
730
|
-
const results = await analyzeContext(finalOptions);
|
|
731
|
-
|
|
732
|
-
const elapsedTime = getElapsedTime(startTime);
|
|
733
|
-
const summary = generateSummary(results);
|
|
734
|
-
|
|
735
|
-
// Calculate score if requested
|
|
736
|
-
let contextScore: ToolScoringOutput | undefined;
|
|
737
|
-
if (options.score) {
|
|
738
|
-
contextScore = calculateContextScore(summary as any);
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
const outputFormat = options.output || finalOptions.output?.format || 'console';
|
|
742
|
-
const userOutputFile = options.outputFile || finalOptions.output?.file;
|
|
743
|
-
|
|
744
|
-
if (outputFormat === 'json') {
|
|
745
|
-
const outputData = {
|
|
746
|
-
results,
|
|
747
|
-
summary: { ...summary, executionTime: parseFloat(elapsedTime) },
|
|
748
|
-
...(contextScore && { scoring: contextScore }),
|
|
749
|
-
};
|
|
750
|
-
|
|
751
|
-
const outputPath = resolveOutputPath(
|
|
752
|
-
userOutputFile,
|
|
753
|
-
`aiready-report-${getReportTimestamp()}.json`,
|
|
754
|
-
resolvedDir
|
|
755
|
-
);
|
|
756
|
-
|
|
757
|
-
handleJSONOutput(outputData, outputPath, `✅ Results saved to ${outputPath}`);
|
|
758
|
-
} else {
|
|
759
|
-
// Console output - format the results nicely
|
|
760
|
-
const terminalWidth = process.stdout.columns || 80;
|
|
761
|
-
const dividerWidth = Math.min(60, terminalWidth - 2);
|
|
762
|
-
const divider = '━'.repeat(dividerWidth);
|
|
763
|
-
|
|
764
|
-
console.log(chalk.cyan(divider));
|
|
765
|
-
console.log(chalk.bold.white(' CONTEXT ANALYSIS SUMMARY'));
|
|
766
|
-
console.log(chalk.cyan(divider) + '\n');
|
|
767
|
-
|
|
768
|
-
console.log(chalk.white(`📁 Files analyzed: ${chalk.bold(summary.totalFiles)}`));
|
|
769
|
-
console.log(chalk.white(`📊 Total tokens: ${chalk.bold(summary.totalTokens.toLocaleString())}`));
|
|
770
|
-
console.log(chalk.yellow(`💰 Avg context budget: ${chalk.bold(summary.avgContextBudget.toFixed(0))} tokens/file`));
|
|
771
|
-
console.log(chalk.white(`⏱ Analysis time: ${chalk.bold(elapsedTime + 's')}\n`));
|
|
772
|
-
|
|
773
|
-
// Issues summary
|
|
774
|
-
const totalIssues = summary.criticalIssues + summary.majorIssues + summary.minorIssues;
|
|
775
|
-
if (totalIssues > 0) {
|
|
776
|
-
console.log(chalk.bold('⚠️ Issues Found:\n'));
|
|
777
|
-
if (summary.criticalIssues > 0) {
|
|
778
|
-
console.log(chalk.red(` 🔴 Critical: ${chalk.bold(summary.criticalIssues)}`));
|
|
779
|
-
}
|
|
780
|
-
if (summary.majorIssues > 0) {
|
|
781
|
-
console.log(chalk.yellow(` 🟡 Major: ${chalk.bold(summary.majorIssues)}`));
|
|
782
|
-
}
|
|
783
|
-
if (summary.minorIssues > 0) {
|
|
784
|
-
console.log(chalk.blue(` 🔵 Minor: ${chalk.bold(summary.minorIssues)}`));
|
|
785
|
-
}
|
|
786
|
-
console.log(chalk.green(`\n 💡 Potential savings: ${chalk.bold(summary.totalPotentialSavings.toLocaleString())} tokens\n`));
|
|
787
|
-
} else {
|
|
788
|
-
console.log(chalk.green('✅ No significant issues found!\n'));
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// Deep import chains
|
|
792
|
-
if (summary.deepFiles.length > 0) {
|
|
793
|
-
console.log(chalk.bold('📏 Deep Import Chains:\n'));
|
|
794
|
-
console.log(chalk.gray(` Average depth: ${summary.avgImportDepth.toFixed(1)}`));
|
|
795
|
-
console.log(chalk.gray(` Maximum depth: ${summary.maxImportDepth}\n`));
|
|
796
|
-
summary.deepFiles.slice(0, 10).forEach((item) => {
|
|
797
|
-
const fileName = item.file.split('/').slice(-2).join('/');
|
|
798
|
-
console.log(` ${chalk.cyan('→')} ${chalk.white(fileName)} ${chalk.dim(`(depth: ${item.depth})`)}`);
|
|
799
|
-
});
|
|
800
|
-
console.log();
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
// Fragmented modules
|
|
804
|
-
if (summary.fragmentedModules.length > 0) {
|
|
805
|
-
console.log(chalk.bold('🧩 Fragmented Modules:\n'));
|
|
806
|
-
console.log(chalk.gray(` Average fragmentation: ${(summary.avgFragmentation * 100).toFixed(0)}%\n`));
|
|
807
|
-
summary.fragmentedModules.slice(0, 10).forEach((module) => {
|
|
808
|
-
console.log(` ${chalk.yellow('●')} ${chalk.white(module.domain)} - ${chalk.dim(`${module.files.length} files, ${(module.fragmentationScore * 100).toFixed(0)}% scattered`)}`);
|
|
809
|
-
console.log(chalk.dim(` Token cost: ${module.totalTokens.toLocaleString()}, Cohesion: ${(module.avgCohesion * 100).toFixed(0)}%`));
|
|
810
|
-
});
|
|
811
|
-
console.log();
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// Low cohesion files
|
|
815
|
-
if (summary.lowCohesionFiles.length > 0) {
|
|
816
|
-
console.log(chalk.bold('🔀 Low Cohesion Files:\n'));
|
|
817
|
-
console.log(chalk.gray(` Average cohesion: ${(summary.avgCohesion * 100).toFixed(0)}%\n`));
|
|
818
|
-
summary.lowCohesionFiles.slice(0, 10).forEach((item) => {
|
|
819
|
-
const fileName = item.file.split('/').slice(-2).join('/');
|
|
820
|
-
const scorePercent = (item.score * 100).toFixed(0);
|
|
821
|
-
const color = item.score < 0.4 ? chalk.red : chalk.yellow;
|
|
822
|
-
console.log(` ${color('○')} ${chalk.white(fileName)} ${chalk.dim(`(${scorePercent}% cohesion)`)}`);
|
|
823
|
-
});
|
|
824
|
-
console.log();
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// Top expensive files
|
|
828
|
-
if (summary.topExpensiveFiles.length > 0) {
|
|
829
|
-
console.log(chalk.bold('💸 Most Expensive Files (Context Budget):\n'));
|
|
830
|
-
summary.topExpensiveFiles.slice(0, 10).forEach((item) => {
|
|
831
|
-
const fileName = item.file.split('/').slice(-2).join('/');
|
|
832
|
-
const severityColor = item.severity === 'critical' ? chalk.red : item.severity === 'major' ? chalk.yellow : chalk.blue;
|
|
833
|
-
console.log(` ${severityColor('●')} ${chalk.white(fileName)} ${chalk.dim(`(${item.contextBudget.toLocaleString()} tokens)`)}`);
|
|
834
|
-
});
|
|
835
|
-
console.log();
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// Display score if calculated
|
|
839
|
-
if (contextScore) {
|
|
840
|
-
console.log(chalk.cyan(divider));
|
|
841
|
-
console.log(chalk.bold.white(' AI READINESS SCORE (Context)'));
|
|
842
|
-
console.log(chalk.cyan(divider) + '\n');
|
|
843
|
-
console.log(formatToolScore(contextScore));
|
|
844
|
-
console.log();
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
} catch (error) {
|
|
848
|
-
handleCLIError(error, 'Context analysis');
|
|
849
|
-
}
|
|
117
|
+
await contextAction(directory, options);
|
|
850
118
|
});
|
|
851
119
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
const resolvedDir = resolvePath(process.cwd(), directory || '.');
|
|
871
|
-
|
|
872
|
-
try {
|
|
873
|
-
// Define defaults
|
|
874
|
-
const defaults = {
|
|
875
|
-
checkNaming: true,
|
|
876
|
-
checkPatterns: true,
|
|
877
|
-
minSeverity: 'info' as const,
|
|
878
|
-
include: undefined,
|
|
879
|
-
exclude: undefined,
|
|
880
|
-
output: {
|
|
881
|
-
format: 'console',
|
|
882
|
-
file: undefined,
|
|
883
|
-
},
|
|
884
|
-
};
|
|
885
|
-
|
|
886
|
-
// Load and merge config with CLI options
|
|
887
|
-
const finalOptions = await loadMergedConfig(resolvedDir, defaults, {
|
|
888
|
-
checkNaming: options.naming !== false,
|
|
889
|
-
checkPatterns: options.patterns !== false,
|
|
890
|
-
minSeverity: options.minSeverity,
|
|
891
|
-
include: options.include?.split(','),
|
|
892
|
-
exclude: options.exclude?.split(','),
|
|
893
|
-
});
|
|
894
|
-
|
|
895
|
-
const { analyzeConsistency, calculateConsistencyScore } = await import('@aiready/consistency');
|
|
896
|
-
|
|
897
|
-
const report = await analyzeConsistency(finalOptions);
|
|
898
|
-
|
|
899
|
-
const elapsedTime = getElapsedTime(startTime);
|
|
900
|
-
|
|
901
|
-
// Calculate score if requested
|
|
902
|
-
let consistencyScore: ToolScoringOutput | undefined;
|
|
903
|
-
if (options.score) {
|
|
904
|
-
const issues = report.results?.flatMap((r: any) => r.issues) || [];
|
|
905
|
-
consistencyScore = calculateConsistencyScore(issues, report.summary.filesAnalyzed);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
const outputFormat = options.output || finalOptions.output?.format || 'console';
|
|
909
|
-
const userOutputFile = options.outputFile || finalOptions.output?.file;
|
|
910
|
-
|
|
911
|
-
if (outputFormat === 'json') {
|
|
912
|
-
const outputData = {
|
|
913
|
-
...report,
|
|
914
|
-
summary: {
|
|
915
|
-
...report.summary,
|
|
916
|
-
executionTime: parseFloat(elapsedTime),
|
|
917
|
-
},
|
|
918
|
-
...(consistencyScore && { scoring: consistencyScore }),
|
|
919
|
-
};
|
|
920
|
-
|
|
921
|
-
const outputPath = resolveOutputPath(
|
|
922
|
-
userOutputFile,
|
|
923
|
-
`aiready-report-${getReportTimestamp()}.json`,
|
|
924
|
-
resolvedDir
|
|
925
|
-
);
|
|
926
|
-
|
|
927
|
-
handleJSONOutput(outputData, outputPath, `✅ Results saved to ${outputPath}`);
|
|
928
|
-
} else if (outputFormat === 'markdown') {
|
|
929
|
-
// Markdown output
|
|
930
|
-
const markdown = generateMarkdownReport(report, elapsedTime);
|
|
931
|
-
const outputPath = resolveOutputPath(
|
|
932
|
-
userOutputFile,
|
|
933
|
-
`aiready-report-${getReportTimestamp()}.md`,
|
|
934
|
-
resolvedDir
|
|
935
|
-
);
|
|
936
|
-
writeFileSync(outputPath, markdown);
|
|
937
|
-
console.log(chalk.green(`✅ Report saved to ${outputPath}`));
|
|
938
|
-
} else {
|
|
939
|
-
// Console output - format to match standalone CLI
|
|
940
|
-
console.log(chalk.bold('\n📊 Summary\n'));
|
|
941
|
-
console.log(`Files Analyzed: ${chalk.cyan(report.summary.filesAnalyzed)}`);
|
|
942
|
-
console.log(`Total Issues: ${chalk.yellow(report.summary.totalIssues)}`);
|
|
943
|
-
console.log(` Naming: ${chalk.yellow(report.summary.namingIssues)}`);
|
|
944
|
-
console.log(` Patterns: ${chalk.yellow(report.summary.patternIssues)}`);
|
|
945
|
-
console.log(` Architecture: ${chalk.yellow(report.summary.architectureIssues || 0)}`);
|
|
946
|
-
console.log(`Analysis Time: ${chalk.gray(elapsedTime + 's')}\n`);
|
|
947
|
-
|
|
948
|
-
if (report.summary.totalIssues === 0) {
|
|
949
|
-
console.log(chalk.green('✨ No consistency issues found! Your codebase is well-maintained.\n'));
|
|
950
|
-
} else {
|
|
951
|
-
// Group and display issues by category
|
|
952
|
-
const namingResults = report.results.filter((r: any) =>
|
|
953
|
-
r.issues.some((i: any) => i.category === 'naming')
|
|
954
|
-
);
|
|
955
|
-
const patternResults = report.results.filter((r: any) =>
|
|
956
|
-
r.issues.some((i: any) => i.category === 'patterns')
|
|
957
|
-
);
|
|
958
|
-
|
|
959
|
-
if (namingResults.length > 0) {
|
|
960
|
-
console.log(chalk.bold('🏷️ Naming Issues\n'));
|
|
961
|
-
let shown = 0;
|
|
962
|
-
for (const result of namingResults) {
|
|
963
|
-
if (shown >= 5) break;
|
|
964
|
-
for (const issue of result.issues) {
|
|
965
|
-
if (shown >= 5) break;
|
|
966
|
-
const severityColor = issue.severity === 'critical' ? chalk.red :
|
|
967
|
-
issue.severity === 'major' ? chalk.yellow :
|
|
968
|
-
issue.severity === 'minor' ? chalk.blue : chalk.gray;
|
|
969
|
-
console.log(`${severityColor(issue.severity.toUpperCase())} ${chalk.dim(`${issue.location.file}:${issue.location.line}`)}`);
|
|
970
|
-
console.log(` ${issue.message}`);
|
|
971
|
-
if (issue.suggestion) {
|
|
972
|
-
console.log(` ${chalk.dim('→')} ${chalk.italic(issue.suggestion)}`);
|
|
973
|
-
}
|
|
974
|
-
console.log();
|
|
975
|
-
shown++;
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
const remaining = namingResults.reduce((sum, r) => sum + r.issues.length, 0) - shown;
|
|
979
|
-
if (remaining > 0) {
|
|
980
|
-
console.log(chalk.dim(` ... and ${remaining} more issues\n`));
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
if (patternResults.length > 0) {
|
|
985
|
-
console.log(chalk.bold('🔄 Pattern Issues\n'));
|
|
986
|
-
let shown = 0;
|
|
987
|
-
for (const result of patternResults) {
|
|
988
|
-
if (shown >= 5) break;
|
|
989
|
-
for (const issue of result.issues) {
|
|
990
|
-
if (shown >= 5) break;
|
|
991
|
-
const severityColor = issue.severity === 'critical' ? chalk.red :
|
|
992
|
-
issue.severity === 'major' ? chalk.yellow :
|
|
993
|
-
issue.severity === 'minor' ? chalk.blue : chalk.gray;
|
|
994
|
-
console.log(`${severityColor(issue.severity.toUpperCase())} ${chalk.dim(`${issue.location.file}:${issue.location.line}`)}`);
|
|
995
|
-
console.log(` ${issue.message}`);
|
|
996
|
-
if (issue.suggestion) {
|
|
997
|
-
console.log(` ${chalk.dim('→')} ${chalk.italic(issue.suggestion)}`);
|
|
998
|
-
}
|
|
999
|
-
console.log();
|
|
1000
|
-
shown++;
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
const remaining = patternResults.reduce((sum, r) => sum + r.issues.length, 0) - shown;
|
|
1004
|
-
if (remaining > 0) {
|
|
1005
|
-
console.log(chalk.dim(` ... and ${remaining} more issues\n`));
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
if (report.recommendations.length > 0) {
|
|
1010
|
-
console.log(chalk.bold('💡 Recommendations\n'));
|
|
1011
|
-
report.recommendations.forEach((rec: string, i: number) => {
|
|
1012
|
-
console.log(`${i + 1}. ${rec}`);
|
|
1013
|
-
});
|
|
1014
|
-
console.log();
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// Display score if calculated
|
|
1019
|
-
if (consistencyScore) {
|
|
1020
|
-
console.log(chalk.bold('\n📊 AI Readiness Score (Consistency)\n'));
|
|
1021
|
-
console.log(formatToolScore(consistencyScore));
|
|
1022
|
-
console.log();
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
} catch (error) {
|
|
1026
|
-
handleCLIError(error, 'Consistency analysis');
|
|
1027
|
-
}
|
|
1028
|
-
});
|
|
1029
|
-
|
|
1030
|
-
function generateMarkdownReport(report: any, elapsedTime: string): string {
|
|
1031
|
-
let markdown = `# Consistency Analysis Report\n\n`;
|
|
1032
|
-
markdown += `**Generated:** ${new Date().toISOString()}\n`;
|
|
1033
|
-
markdown += `**Analysis Time:** ${elapsedTime}s\n\n`;
|
|
1034
|
-
|
|
1035
|
-
markdown += `## Summary\n\n`;
|
|
1036
|
-
markdown += `- **Files Analyzed:** ${report.summary.filesAnalyzed}\n`;
|
|
1037
|
-
markdown += `- **Total Issues:** ${report.summary.totalIssues}\n`;
|
|
1038
|
-
markdown += ` - Naming: ${report.summary.namingIssues}\n`;
|
|
1039
|
-
markdown += ` - Patterns: ${report.summary.patternIssues}\n\n`;
|
|
1040
|
-
|
|
1041
|
-
if (report.recommendations.length > 0) {
|
|
1042
|
-
markdown += `## Recommendations\n\n`;
|
|
1043
|
-
report.recommendations.forEach((rec: string, i: number) => {
|
|
1044
|
-
markdown += `${i + 1}. ${rec}\n`;
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
return markdown;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
/**
|
|
1053
|
-
* Generate timestamp for report filenames (YYYYMMDD-HHMMSS)
|
|
1054
|
-
* Provides better granularity than date-only filenames
|
|
1055
|
-
*/
|
|
1056
|
-
function getReportTimestamp(): string {
|
|
1057
|
-
const now = new Date();
|
|
1058
|
-
const pad = (n: number) => String(n).padStart(2, '0');
|
|
1059
|
-
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
/**
|
|
1063
|
-
* Find the latest aiready report in the .aiready directory
|
|
1064
|
-
* Searches for both new format (aiready-report-*) and legacy format (aiready-scan-*)
|
|
1065
|
-
*/
|
|
1066
|
-
function findLatestScanReport(dirPath: string): string | null {
|
|
1067
|
-
const aireadyDir = resolvePath(dirPath, '.aiready');
|
|
1068
|
-
if (!existsSync(aireadyDir)) {
|
|
1069
|
-
return null;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
const { readdirSync, statSync } = require('fs');
|
|
1073
|
-
// Search for new format first, then legacy format
|
|
1074
|
-
let files = readdirSync(aireadyDir).filter(f => f.startsWith('aiready-report-') && f.endsWith('.json'));
|
|
1075
|
-
if (files.length === 0) {
|
|
1076
|
-
files = readdirSync(aireadyDir).filter(f => f.startsWith('aiready-scan-') && f.endsWith('.json'));
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
if (files.length === 0) {
|
|
1080
|
-
return null;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
// Sort by modification time, most recent first
|
|
1084
|
-
const sortedFiles = files
|
|
1085
|
-
.map(f => ({ name: f, path: resolvePath(aireadyDir, f), mtime: statSync(resolvePath(aireadyDir, f)).mtime }))
|
|
1086
|
-
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
1087
|
-
|
|
1088
|
-
return sortedFiles[0].path;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
function warnIfGraphCapExceeded(report: any, dirPath: string) {
|
|
1092
|
-
try {
|
|
1093
|
-
// Use dynamic import and loadConfig to get the raw visualizer config
|
|
1094
|
-
const { loadConfig } = require('@aiready/core');
|
|
1095
|
-
|
|
1096
|
-
// Sync file system check since we can't easily call loadConfig synchronously
|
|
1097
|
-
const { existsSync, readFileSync } = require('fs');
|
|
1098
|
-
const { resolve } = require('path');
|
|
1099
|
-
|
|
1100
|
-
let graphConfig = { maxNodes: 400, maxEdges: 600 };
|
|
1101
|
-
|
|
1102
|
-
// Try to read aiready.json synchronously
|
|
1103
|
-
const configPath = resolve(dirPath, 'aiready.json');
|
|
1104
|
-
if (existsSync(configPath)) {
|
|
1105
|
-
try {
|
|
1106
|
-
const rawConfig = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
1107
|
-
if (rawConfig.visualizer?.graph) {
|
|
1108
|
-
graphConfig = {
|
|
1109
|
-
maxNodes: rawConfig.visualizer.graph.maxNodes ?? graphConfig.maxNodes,
|
|
1110
|
-
maxEdges: rawConfig.visualizer.graph.maxEdges ?? graphConfig.maxEdges,
|
|
1111
|
-
};
|
|
1112
|
-
}
|
|
1113
|
-
} catch (e) {
|
|
1114
|
-
// Silently ignore parse errors and use defaults
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
const nodeCount = (report.context?.length || 0) + (report.patterns?.length || 0);
|
|
1119
|
-
const edgeCount = report.context?.reduce((sum: number, ctx: any) => {
|
|
1120
|
-
const relCount = ctx.relatedFiles?.length || 0;
|
|
1121
|
-
const depCount = ctx.dependencies?.length || 0;
|
|
1122
|
-
return sum + relCount + depCount;
|
|
1123
|
-
}, 0) || 0;
|
|
1124
|
-
|
|
1125
|
-
if (nodeCount > graphConfig.maxNodes || edgeCount > graphConfig.maxEdges) {
|
|
1126
|
-
console.log('');
|
|
1127
|
-
console.log(chalk.yellow(`⚠️ Graph may be truncated at visualization time:`));
|
|
1128
|
-
if (nodeCount > graphConfig.maxNodes) {
|
|
1129
|
-
console.log(chalk.dim(` • Nodes: ${nodeCount} > limit ${graphConfig.maxNodes}`));
|
|
1130
|
-
}
|
|
1131
|
-
if (edgeCount > graphConfig.maxEdges) {
|
|
1132
|
-
console.log(chalk.dim(` • Edges: ${edgeCount} > limit ${graphConfig.maxEdges}`));
|
|
1133
|
-
}
|
|
1134
|
-
console.log(chalk.dim(` To increase limits, add to aiready.json:`));
|
|
1135
|
-
console.log(chalk.dim(` {`));
|
|
1136
|
-
console.log(chalk.dim(` "visualizer": {`));
|
|
1137
|
-
console.log(chalk.dim(` "graph": { "maxNodes": 2000, "maxEdges": 5000 }`));
|
|
1138
|
-
console.log(chalk.dim(` }`));
|
|
1139
|
-
console.log(chalk.dim(` }`));
|
|
1140
|
-
}
|
|
1141
|
-
} catch (e) {
|
|
1142
|
-
// Silently fail on config read errors
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
async function handleVisualize(directory: string | undefined, options: any) {
|
|
1147
|
-
try {
|
|
1148
|
-
const dirPath = resolvePath(process.cwd(), directory || '.');
|
|
1149
|
-
let reportPath = options.report ? resolvePath(dirPath, options.report) : null;
|
|
1150
|
-
|
|
1151
|
-
// If report not provided or not found, try to find latest scan report
|
|
1152
|
-
if (!reportPath || !existsSync(reportPath)) {
|
|
1153
|
-
const latestScan = findLatestScanReport(dirPath);
|
|
1154
|
-
if (latestScan) {
|
|
1155
|
-
reportPath = latestScan;
|
|
1156
|
-
console.log(chalk.dim(`Found latest report: ${latestScan.split('/').pop()}`));
|
|
1157
|
-
} else {
|
|
1158
|
-
console.error(chalk.red('❌ No AI readiness report found'));
|
|
1159
|
-
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>`));
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
const raw = readFileSync(reportPath, 'utf8');
|
|
1165
|
-
const report = JSON.parse(raw);
|
|
1166
|
-
|
|
1167
|
-
// Load config to extract graph caps
|
|
1168
|
-
const configPath = resolvePath(dirPath, 'aiready.json');
|
|
1169
|
-
let graphConfig = { maxNodes: 400, maxEdges: 600 };
|
|
1170
|
-
|
|
1171
|
-
if (existsSync(configPath)) {
|
|
1172
|
-
try {
|
|
1173
|
-
const rawConfig = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
1174
|
-
if (rawConfig.visualizer?.graph) {
|
|
1175
|
-
graphConfig = {
|
|
1176
|
-
maxNodes: rawConfig.visualizer.graph.maxNodes ?? graphConfig.maxNodes,
|
|
1177
|
-
maxEdges: rawConfig.visualizer.graph.maxEdges ?? graphConfig.maxEdges,
|
|
1178
|
-
};
|
|
1179
|
-
}
|
|
1180
|
-
} catch (e) {
|
|
1181
|
-
// Silently ignore parse errors and use defaults
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
// Store config in env for vite middleware to pass to client
|
|
1186
|
-
const envVisualizerConfig = JSON.stringify(graphConfig);
|
|
1187
|
-
process.env.AIREADY_VISUALIZER_CONFIG = envVisualizerConfig;
|
|
1188
|
-
|
|
1189
|
-
console.log("Building graph from report...");
|
|
1190
|
-
const { GraphBuilder } = await import('@aiready/visualizer/graph');
|
|
1191
|
-
const graph = GraphBuilder.buildFromReport(report, dirPath);
|
|
1192
|
-
|
|
1193
|
-
if (options.dev) {
|
|
1194
|
-
try {
|
|
1195
|
-
const { spawn } = await import("child_process");
|
|
1196
|
-
const monorepoWebDir = resolvePath(dirPath, 'packages/visualizer');
|
|
1197
|
-
let webDir = '';
|
|
1198
|
-
let visualizerAvailable = false;
|
|
1199
|
-
if (existsSync(monorepoWebDir)) {
|
|
1200
|
-
webDir = monorepoWebDir;
|
|
1201
|
-
visualizerAvailable = true;
|
|
1202
|
-
} else {
|
|
1203
|
-
// Try to resolve installed @aiready/visualizer package from node_modules
|
|
1204
|
-
// Check multiple locations to support pnpm, npm, yarn, etc.
|
|
1205
|
-
const nodemodulesLocations: string[] = [
|
|
1206
|
-
resolvePath(dirPath, 'node_modules', '@aiready', 'visualizer'),
|
|
1207
|
-
resolvePath(process.cwd(), 'node_modules', '@aiready', 'visualizer'),
|
|
1208
|
-
];
|
|
1209
|
-
|
|
1210
|
-
// Walk up directory tree to find node_modules in parent directories
|
|
1211
|
-
let currentDir = dirPath;
|
|
1212
|
-
while (currentDir !== '/' && currentDir !== '.') {
|
|
1213
|
-
nodemodulesLocations.push(resolvePath(currentDir, 'node_modules', '@aiready', 'visualizer'));
|
|
1214
|
-
const parent = resolvePath(currentDir, '..');
|
|
1215
|
-
if (parent === currentDir) break; // Reached filesystem root
|
|
1216
|
-
currentDir = parent;
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
for (const location of nodemodulesLocations) {
|
|
1220
|
-
if (existsSync(location) && existsSync(resolvePath(location, 'package.json'))) {
|
|
1221
|
-
webDir = location;
|
|
1222
|
-
visualizerAvailable = true;
|
|
1223
|
-
break;
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
// Fallback: try require.resolve
|
|
1228
|
-
if (!visualizerAvailable) {
|
|
1229
|
-
try {
|
|
1230
|
-
const vizPkgPath = require.resolve('@aiready/visualizer/package.json');
|
|
1231
|
-
webDir = resolvePath(vizPkgPath, '..');
|
|
1232
|
-
visualizerAvailable = true;
|
|
1233
|
-
} catch (e) {
|
|
1234
|
-
// Visualizer not found
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
const spawnCwd = webDir || process.cwd();
|
|
1239
|
-
const nodeBinCandidate = process.execPath;
|
|
1240
|
-
const nodeBin = existsSync(nodeBinCandidate) ? nodeBinCandidate : 'node';
|
|
1241
|
-
if (!visualizerAvailable) {
|
|
1242
|
-
console.error(chalk.red('❌ Cannot start dev server: @aiready/visualizer not available.'));
|
|
1243
|
-
console.log(chalk.dim('Install @aiready/visualizer in your project with:\n npm install @aiready/visualizer'));
|
|
1244
|
-
return;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
// Inline report watcher: copy report to web/report-data.json and watch for changes
|
|
1248
|
-
const { watch } = await import('fs');
|
|
1249
|
-
const copyReportToViz = () => {
|
|
1250
|
-
try {
|
|
1251
|
-
const destPath = resolvePath(spawnCwd, 'web', 'report-data.json');
|
|
1252
|
-
copyFileSync(reportPath, destPath);
|
|
1253
|
-
console.log(`📋 Report synced to ${destPath}`);
|
|
1254
|
-
} catch (e) {
|
|
1255
|
-
console.error('Failed to sync report:', e);
|
|
1256
|
-
}
|
|
1257
|
-
};
|
|
1258
|
-
|
|
1259
|
-
// Initial copy
|
|
1260
|
-
copyReportToViz();
|
|
1261
|
-
|
|
1262
|
-
// Watch source report for changes
|
|
1263
|
-
let watchTimeout: NodeJS.Timeout | null = null;
|
|
1264
|
-
const reportWatcher = watch(reportPath, () => {
|
|
1265
|
-
// Debounce to avoid multiple copies during file write
|
|
1266
|
-
if (watchTimeout) clearTimeout(watchTimeout);
|
|
1267
|
-
watchTimeout = setTimeout(copyReportToViz, 100);
|
|
1268
|
-
});
|
|
1269
|
-
|
|
1270
|
-
const envForSpawn = {
|
|
1271
|
-
...process.env,
|
|
1272
|
-
AIREADY_REPORT_PATH: reportPath,
|
|
1273
|
-
AIREADY_VISUALIZER_CONFIG: envVisualizerConfig
|
|
1274
|
-
};
|
|
1275
|
-
const vite = spawn("pnpm", ["run", "dev:web"], { cwd: spawnCwd, stdio: "inherit", shell: true, env: envForSpawn });
|
|
1276
|
-
const onExit = () => {
|
|
1277
|
-
try {
|
|
1278
|
-
reportWatcher.close();
|
|
1279
|
-
} catch (e) {}
|
|
1280
|
-
try {
|
|
1281
|
-
vite.kill();
|
|
1282
|
-
} catch (e) {}
|
|
1283
|
-
process.exit(0);
|
|
1284
|
-
};
|
|
1285
|
-
process.on("SIGINT", onExit);
|
|
1286
|
-
process.on("SIGTERM", onExit);
|
|
1287
|
-
return;
|
|
1288
|
-
} catch (err) {
|
|
1289
|
-
console.error("Failed to start dev server:", err);
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
console.log("Generating HTML...");
|
|
1294
|
-
const html = generateHTML(graph);
|
|
1295
|
-
const outPath = resolvePath(dirPath, options.output || 'packages/visualizer/visualization.html');
|
|
1296
|
-
writeFileSync(outPath, html, 'utf8');
|
|
1297
|
-
console.log("Visualization written to:", outPath);
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
if (options.open) {
|
|
1301
|
-
const { exec } = await import('child_process');
|
|
1302
|
-
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
1303
|
-
exec(`${opener} "${outPath}"`);
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
if (options.serve) {
|
|
1307
|
-
try {
|
|
1308
|
-
const port = typeof options.serve === 'number' ? options.serve : 5173;
|
|
1309
|
-
const http = await import('http');
|
|
1310
|
-
const fsp = await import('fs/promises');
|
|
1311
|
-
const { exec } = await import('child_process');
|
|
1312
|
-
|
|
1313
|
-
const server = http.createServer(async (req, res) => {
|
|
1314
|
-
try {
|
|
1315
|
-
const urlPath = req.url || '/';
|
|
1316
|
-
if (urlPath === '/' || urlPath === '/index.html') {
|
|
1317
|
-
const content = await fsp.readFile(outPath, 'utf8');
|
|
1318
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1319
|
-
res.end(content);
|
|
1320
|
-
return;
|
|
1321
|
-
}
|
|
1322
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
1323
|
-
res.end('Not found');
|
|
1324
|
-
} catch (e: any) {
|
|
1325
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1326
|
-
res.end('Server error');
|
|
1327
|
-
}
|
|
1328
|
-
});
|
|
1329
|
-
|
|
1330
|
-
server.listen(port, () => {
|
|
1331
|
-
const addr = `http://localhost:${port}/`;
|
|
1332
|
-
console.log(`Local visualization server running at ${addr}`);
|
|
1333
|
-
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
1334
|
-
exec(`${opener} "${addr}"`);
|
|
1335
|
-
});
|
|
1336
|
-
|
|
1337
|
-
process.on('SIGINT', () => {
|
|
1338
|
-
server.close();
|
|
1339
|
-
process.exit(0);
|
|
1340
|
-
});
|
|
1341
|
-
} catch (err) {
|
|
1342
|
-
console.error('Failed to start local server:', err);
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
} catch (err: any) {
|
|
1347
|
-
handleCLIError(err, 'Visualization');
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
120
|
+
// Consistency command - Check naming conventions
|
|
121
|
+
program
|
|
122
|
+
.command('consistency')
|
|
123
|
+
.description('Check naming conventions and architectural consistency')
|
|
124
|
+
.argument('[directory]', 'Directory to analyze', '.')
|
|
125
|
+
.option('--naming', 'Check naming conventions (default: true)')
|
|
126
|
+
.option('--no-naming', 'Skip naming analysis')
|
|
127
|
+
.option('--patterns', 'Check code patterns (default: true)')
|
|
128
|
+
.option('--no-patterns', 'Skip pattern analysis')
|
|
129
|
+
.option('--min-severity <level>', 'Minimum severity: info|minor|major|critical', 'info')
|
|
130
|
+
.option('--include <patterns>', 'File patterns to include (comma-separated)')
|
|
131
|
+
.option('--exclude <patterns>', 'File patterns to exclude (comma-separated)')
|
|
132
|
+
.option('-o, --output <format>', 'Output format: console, json, markdown', 'console')
|
|
133
|
+
.option('--output-file <path>', 'Output file path (for json/markdown)')
|
|
134
|
+
.option('--score', 'Calculate and display AI Readiness Score for consistency (0-100)')
|
|
135
|
+
.action(async (directory, options) => {
|
|
136
|
+
await consistencyAction(directory, options);
|
|
137
|
+
});
|
|
1350
138
|
|
|
1351
|
-
//
|
|
139
|
+
// Visualise command (British spelling alias)
|
|
1352
140
|
program
|
|
1353
141
|
.command('visualise')
|
|
1354
142
|
.description('Alias for visualize (British spelling)')
|
|
@@ -1358,18 +146,12 @@ program
|
|
|
1358
146
|
.option('--open', 'Open generated HTML in default browser')
|
|
1359
147
|
.option('--serve [port]', 'Start a local static server to serve the visualization (optional port number)', false)
|
|
1360
148
|
.option('--dev', 'Start Vite dev server (live reload) for interactive development', true)
|
|
1361
|
-
.addHelpText('after',
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
$ aiready visualise . --report report.json --dev
|
|
1366
|
-
$ aiready visualise . --report report.json --serve 8080
|
|
1367
|
-
|
|
1368
|
-
NOTES:
|
|
1369
|
-
- Same options as \'visualize\'. Use --dev for live reload and --serve to host a static HTML.
|
|
1370
|
-
`)
|
|
1371
|
-
.action(async (directory, options) => await handleVisualize(directory, options));
|
|
149
|
+
.addHelpText('after', visualiseHelpText)
|
|
150
|
+
.action(async (directory, options) => {
|
|
151
|
+
await visualizeAction(directory, options);
|
|
152
|
+
});
|
|
1372
153
|
|
|
154
|
+
// Visualize command - Generate interactive visualization
|
|
1373
155
|
program
|
|
1374
156
|
.command('visualize')
|
|
1375
157
|
.description('Generate interactive visualization from an AIReady report')
|
|
@@ -1379,26 +161,9 @@ program
|
|
|
1379
161
|
.option('--open', 'Open generated HTML in default browser')
|
|
1380
162
|
.option('--serve [port]', 'Start a local static server to serve the visualization (optional port number)', false)
|
|
1381
163
|
.option('--dev', 'Start Vite dev server (live reload) for interactive development', false)
|
|
1382
|
-
.addHelpText('after',
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
$ aiready visualize . --report report.json -o out/visualization.html --open
|
|
1387
|
-
$ aiready visualize . --report report.json --serve
|
|
1388
|
-
$ aiready visualize . --report report.json --serve 8080
|
|
1389
|
-
$ aiready visualize . --report report.json --dev
|
|
1390
|
-
|
|
1391
|
-
NOTES:
|
|
1392
|
-
- The value passed to --report is interpreted relative to the directory argument (first positional).
|
|
1393
|
-
If the report is not found, the CLI will suggest running 'aiready scan' to generate it.
|
|
1394
|
-
- Default output path: packages/visualizer/visualization.html (relative to the directory argument).
|
|
1395
|
-
- --serve starts a tiny single-file HTTP server (default port: 5173) and opens your browser.
|
|
1396
|
-
It serves only the generated HTML (no additional asset folders).
|
|
1397
|
-
- Relatedness is represented by node proximity and size; explicit 'related' edges are not drawn to
|
|
1398
|
-
reduce clutter and improve interactivity on large graphs.
|
|
1399
|
-
- For very large graphs, consider narrowing the input with --include/--exclude or use --serve and
|
|
1400
|
-
allow the browser a moment to stabilize after load.
|
|
1401
|
-
`)
|
|
1402
|
-
.action(async (directory, options) => await handleVisualize(directory, options));
|
|
164
|
+
.addHelpText('after', visualizeHelpText)
|
|
165
|
+
.action(async (directory, options) => {
|
|
166
|
+
await visualizeAction(directory, options);
|
|
167
|
+
});
|
|
1403
168
|
|
|
1404
169
|
program.parse();
|