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