@aiready/cli 0.7.19 → 0.7.21

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
@@ -14,6 +14,7 @@ import {
14
14
  calculateOverallScore,
15
15
  formatScore,
16
16
  formatToolScore,
17
+ getRating,
17
18
  getRatingDisplay,
18
19
  parseWeightString,
19
20
  type AIReadyConfig,
@@ -27,22 +28,64 @@ const program = new Command();
27
28
 
28
29
  program
29
30
  .name('aiready')
30
- .description('AIReady - Unified AI-readiness analysis tools')
31
+ .description('AIReady - Assess and improve AI-readiness of codebases')
31
32
  .version(packageJson.version)
32
- .addHelpText('after', '\nCONFIGURATION:\n Supports config files: aiready.json, aiready.config.json, .aiready.json, .aireadyrc.json, aiready.config.js, .aireadyrc.js\n CLI options override config file settings');
33
+ .addHelpText('after', `
34
+ AI READINESS SCORING:
35
+ Get a 0-100 score indicating how AI-ready your codebase is.
36
+ Use --score flag with any analysis command for detailed breakdown.
37
+
38
+ EXAMPLES:
39
+ $ aiready scan # Quick analysis of current directory
40
+ $ aiready scan --score # Get AI Readiness Score (0-100)
41
+ $ aiready scan --tools patterns # Run only pattern detection
42
+ $ aiready patterns --similarity 0.6 # Custom similarity threshold
43
+ $ aiready scan --output json --output-file results.json
44
+
45
+ GETTING STARTED:
46
+ 1. Run 'aiready scan' to analyze your codebase
47
+ 2. Use 'aiready scan --score' for AI readiness assessment
48
+ 3. Create aiready.json for persistent configuration
49
+ 4. Set up CI/CD with '--threshold' for quality gates
50
+
51
+ CONFIGURATION:
52
+ Config files (searched upward): aiready.json, .aiready.json, aiready.config.*
53
+ CLI options override config file settings
54
+
55
+ Example aiready.json:
56
+ {
57
+ "scan": { "exclude": ["**/dist/**", "**/node_modules/**"] },
58
+ "tools": {
59
+ "pattern-detect": { "minSimilarity": 0.5 },
60
+ "context-analyzer": { "maxContextBudget": 15000 }
61
+ },
62
+ "output": { "format": "json", "directory": ".aiready" }
63
+ }
64
+
65
+ VERSION: ${packageJson.version}
66
+ DOCUMENTATION: https://aiready.dev/docs/cli
67
+ GITHUB: https://github.com/caopengau/aiready-cli
68
+ LANDING: https://github.com/caopengau/aiready-landing`);
33
69
 
34
70
  program
35
71
  .command('scan')
36
- .description('Run unified analysis on a codebase')
72
+ .description('Run comprehensive AI-readiness analysis (patterns + context + consistency)')
37
73
  .argument('[directory]', 'Directory to analyze', '.')
38
74
  .option('-t, --tools <tools>', 'Tools to run (comma-separated: patterns,context,consistency)', 'patterns,context,consistency')
39
75
  .option('--include <patterns>', 'File patterns to include (comma-separated)')
40
76
  .option('--exclude <patterns>', 'File patterns to exclude (comma-separated)')
41
77
  .option('-o, --output <format>', 'Output format: console, json', 'console')
42
78
  .option('--output-file <path>', 'Output file path (for json)')
43
- .option('--score', 'Calculate and display AI Readiness Score (0-100)')
44
- .option('--weights <weights>', 'Override tool weights for scoring (e.g., "patterns:50,context:30,consistency:20")')
45
- .option('--threshold <score>', 'Minimum passing score for CI/CD (exits with code 1 if below)')
79
+ .option('--score', 'Calculate and display AI Readiness Score (0-100) with breakdown')
80
+ .option('--weights <weights>', 'Custom scoring weights (patterns:40,context:35,consistency:25)')
81
+ .option('--threshold <score>', 'Fail CI/CD if score below threshold (0-100)')
82
+ .addHelpText('after', `
83
+ EXAMPLES:
84
+ $ aiready scan # Analyze all tools
85
+ $ aiready scan --tools patterns,context # Skip consistency
86
+ $ aiready scan --score --threshold 75 # CI/CD with threshold
87
+ $ aiready scan --output json --output-file report.json
88
+ `)
46
89
  .action(async (directory, options) => {
47
90
  console.log(chalk.blue('🚀 Starting AIReady unified analysis...\n'));
48
91
 
@@ -67,142 +110,270 @@ program
67
110
  exclude: options.exclude?.split(','),
68
111
  }) as any;
69
112
 
113
+
70
114
  // Apply smart defaults for pattern detection if patterns tool is enabled
71
115
  let finalOptions = { ...baseOptions };
72
116
  if (baseOptions.tools.includes('patterns')) {
73
117
  const { getSmartDefaults } = await import('@aiready/pattern-detect');
74
118
  const patternSmartDefaults = await getSmartDefaults(directory, baseOptions);
75
- finalOptions = { ...patternSmartDefaults, ...finalOptions };
119
+ // Merge deeply to preserve nested config
120
+ finalOptions = { ...patternSmartDefaults, ...finalOptions, ...baseOptions };
121
+ }
122
+
123
+ // Print pre-run summary with expanded settings (truncate long arrays)
124
+ console.log(chalk.cyan('\n=== AIReady Run Preview ==='));
125
+ console.log(chalk.white('Tools to run:'), (finalOptions.tools || ['patterns', 'context', 'consistency']).join(', '));
126
+ console.log(chalk.white('Will use settings from config and defaults.'));
127
+
128
+ const truncate = (arr: any[] | undefined, cap = 8) => {
129
+ if (!Array.isArray(arr)) return '';
130
+ const shown = arr.slice(0, cap).map((v) => String(v));
131
+ const more = arr.length - shown.length;
132
+ return shown.join(', ') + (more > 0 ? `, ... (+${more} more)` : '');
133
+ };
134
+
135
+ // Common top-level settings
136
+ console.log(chalk.white('\nGeneral settings:'));
137
+ if (finalOptions.rootDir) console.log(` rootDir: ${chalk.bold(String(finalOptions.rootDir))}`);
138
+ if (finalOptions.include) console.log(` include: ${chalk.bold(truncate(finalOptions.include, 6))}`);
139
+ if (finalOptions.exclude) console.log(` exclude: ${chalk.bold(truncate(finalOptions.exclude, 6))}`);
140
+
141
+ if (finalOptions['pattern-detect'] || finalOptions.minSimilarity) {
142
+ const pd = finalOptions['pattern-detect'] || {
143
+ minSimilarity: finalOptions.minSimilarity,
144
+ minLines: finalOptions.minLines,
145
+ approx: finalOptions.approx,
146
+ minSharedTokens: finalOptions.minSharedTokens,
147
+ maxCandidatesPerBlock: finalOptions.maxCandidatesPerBlock,
148
+ batchSize: finalOptions.batchSize,
149
+ streamResults: finalOptions.streamResults,
150
+ severity: (finalOptions as any).severity,
151
+ includeTests: (finalOptions as any).includeTests,
152
+ };
153
+ console.log(chalk.white('\nPattern-detect settings:'));
154
+ console.log(` minSimilarity: ${chalk.bold(pd.minSimilarity ?? 'default')}`);
155
+ console.log(` minLines: ${chalk.bold(pd.minLines ?? 'default')}`);
156
+ if (pd.approx !== undefined) console.log(` approx: ${chalk.bold(String(pd.approx))}`);
157
+ if (pd.minSharedTokens !== undefined) console.log(` minSharedTokens: ${chalk.bold(String(pd.minSharedTokens))}`);
158
+ if (pd.maxCandidatesPerBlock !== undefined) console.log(` maxCandidatesPerBlock: ${chalk.bold(String(pd.maxCandidatesPerBlock))}`);
159
+ if (pd.batchSize !== undefined) console.log(` batchSize: ${chalk.bold(String(pd.batchSize))}`);
160
+ if (pd.streamResults !== undefined) console.log(` streamResults: ${chalk.bold(String(pd.streamResults))}`);
161
+ if (pd.severity !== undefined) console.log(` severity: ${chalk.bold(String(pd.severity))}`);
162
+ if (pd.includeTests !== undefined) console.log(` includeTests: ${chalk.bold(String(pd.includeTests))}`);
163
+ }
164
+
165
+ if (finalOptions['context-analyzer'] || finalOptions.maxDepth) {
166
+ const ca = finalOptions['context-analyzer'] || {
167
+ maxDepth: finalOptions.maxDepth,
168
+ maxContextBudget: finalOptions.maxContextBudget,
169
+ minCohesion: (finalOptions as any).minCohesion,
170
+ maxFragmentation: (finalOptions as any).maxFragmentation,
171
+ includeNodeModules: (finalOptions as any).includeNodeModules,
172
+ };
173
+ console.log(chalk.white('\nContext-analyzer settings:'));
174
+ console.log(` maxDepth: ${chalk.bold(ca.maxDepth ?? 'default')}`);
175
+ console.log(` maxContextBudget: ${chalk.bold(ca.maxContextBudget ?? 'default')}`);
176
+ if (ca.minCohesion !== undefined) console.log(` minCohesion: ${chalk.bold(String(ca.minCohesion))}`);
177
+ if (ca.maxFragmentation !== undefined) console.log(` maxFragmentation: ${chalk.bold(String(ca.maxFragmentation))}`);
178
+ if (ca.includeNodeModules !== undefined) console.log(` includeNodeModules: ${chalk.bold(String(ca.includeNodeModules))}`);
179
+ }
180
+
181
+ if (finalOptions.consistency) {
182
+ const c = finalOptions.consistency;
183
+ console.log(chalk.white('\nConsistency settings:'));
184
+ console.log(` checkNaming: ${chalk.bold(String(c.checkNaming ?? true))}`);
185
+ console.log(` checkPatterns: ${chalk.bold(String(c.checkPatterns ?? true))}`);
186
+ console.log(` checkArchitecture: ${chalk.bold(String(c.checkArchitecture ?? false))}`);
187
+ if (c.minSeverity) console.log(` minSeverity: ${chalk.bold(c.minSeverity)}`);
188
+ if (c.acceptedAbbreviations) console.log(` acceptedAbbreviations: ${chalk.bold(truncate(c.acceptedAbbreviations, 8))}`);
189
+ if (c.shortWords) console.log(` shortWords: ${chalk.bold(truncate(c.shortWords, 8))}`);
76
190
  }
77
191
 
78
- const results = await analyzeUnified(finalOptions);
192
+ console.log(chalk.white('\nStarting analysis...'));
193
+
194
+ // Progress callback to surface per-tool output as each tool finishes
195
+ const progressCallback = (event: { tool: string; data: any }) => {
196
+ console.log(chalk.cyan(`\n--- ${event.tool.toUpperCase()} RESULTS ---`));
197
+ try {
198
+ if (event.tool === 'patterns') {
199
+ const pr = event.data as any;
200
+ console.log(` Duplicate patterns: ${chalk.bold(String(pr.duplicates?.length || 0))}`);
201
+ console.log(` Files with pattern issues: ${chalk.bold(String(pr.results?.length || 0))}`);
202
+ // show top duplicate summaries
203
+ if (pr.duplicates && pr.duplicates.length > 0) {
204
+ pr.duplicates.slice(0, 5).forEach((d: any, i: number) => {
205
+ console.log(` ${i + 1}. ${d.file1.split('/').pop()} ↔ ${d.file2.split('/').pop()} (sim=${(d.similarity * 100).toFixed(1)}%)`);
206
+ });
207
+ }
208
+
209
+ // show top files with pattern issues (sorted by issue count desc)
210
+ if (pr.results && pr.results.length > 0) {
211
+ console.log(` Top files with pattern issues:`);
212
+ const sortedByIssues = [...pr.results].sort((a: any, b: any) => (b.issues?.length || 0) - (a.issues?.length || 0));
213
+ sortedByIssues.slice(0, 5).forEach((r: any, i: number) => {
214
+ console.log(` ${i + 1}. ${r.fileName.split('/').pop()} - ${r.issues.length} issue(s)`);
215
+ });
216
+ }
217
+
218
+ // Grouping and clusters summary (if available) — show after detailed findings
219
+ if (pr.groups && pr.groups.length >= 0) {
220
+ console.log(` ✅ Grouped ${chalk.bold(String(pr.duplicates?.length || 0))} duplicates into ${chalk.bold(String(pr.groups.length))} file pairs`);
221
+ }
222
+ if (pr.clusters && pr.clusters.length >= 0) {
223
+ console.log(` ✅ Created ${chalk.bold(String(pr.clusters.length))} refactor clusters`);
224
+ // show brief cluster summaries
225
+ pr.clusters.slice(0, 3).forEach((cl: any, idx: number) => {
226
+ const files = (cl.files || []).map((f: any) => f.path.split('/').pop()).join(', ');
227
+ console.log(` ${idx + 1}. ${files} (${cl.tokenCost || 'n/a'} tokens)`);
228
+ });
229
+ }
230
+ } else if (event.tool === 'context') {
231
+ const cr = event.data as any[];
232
+ console.log(` Context issues found: ${chalk.bold(String(cr.length || 0))}`);
233
+ cr.slice(0, 5).forEach((c: any, i: number) => {
234
+ const msg = c.message ? ` - ${c.message}` : '';
235
+ console.log(` ${i + 1}. ${c.file} (${c.severity || 'n/a'})${msg}`);
236
+ });
237
+ } else if (event.tool === 'consistency') {
238
+ const rep = event.data as any;
239
+ console.log(` Consistency totalIssues: ${chalk.bold(String(rep.summary?.totalIssues || 0))}`);
240
+
241
+ if (rep.results && rep.results.length > 0) {
242
+ // Group issues by file
243
+ const fileMap = new Map<string, any[]>();
244
+ rep.results.forEach((r: any) => {
245
+ (r.issues || []).forEach((issue: any) => {
246
+ const file = issue.location?.file || r.file || 'unknown';
247
+ if (!fileMap.has(file)) fileMap.set(file, []);
248
+ fileMap.get(file)!.push(issue);
249
+ });
250
+ });
251
+
252
+ // Sort files by number of issues desc
253
+ const files = Array.from(fileMap.entries()).sort((a, b) => b[1].length - a[1].length);
254
+ const topFiles = files.slice(0, 10);
255
+
256
+ topFiles.forEach(([file, issues], idx) => {
257
+ // Count severities
258
+ const counts = issues.reduce((acc: any, it: any) => {
259
+ const s = (it.severity || 'info').toLowerCase();
260
+ acc[s] = (acc[s] || 0) + 1;
261
+ return acc;
262
+ }, {} as Record<string, number>);
263
+
264
+ const sample = issues.find((it: any) => it.severity === 'critical' || it.severity === 'major') || issues[0];
265
+ const sampleMsg = sample ? ` — ${sample.message}` : '';
266
+
267
+ 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}`);
268
+ });
269
+
270
+ const remaining = files.length - topFiles.length;
271
+ if (remaining > 0) {
272
+ console.log(chalk.dim(` ... and ${remaining} more files with issues (use --output json for full details)`));
273
+ }
274
+ }
275
+ }
276
+ } catch (err) {
277
+ // don't crash the run for progress printing errors
278
+ }
279
+ };
280
+
281
+ const results = await analyzeUnified({ ...finalOptions, progressCallback, suppressToolConfig: true });
282
+
283
+ // Summarize tools and results to console
284
+ console.log(chalk.cyan('\n=== AIReady Run Summary ==='));
285
+ console.log(chalk.white('Tools run:'), (finalOptions.tools || ['patterns', 'context', 'consistency']).join(', '));
286
+
287
+ // Results summary
288
+ console.log(chalk.cyan('\nResults summary:'));
289
+ console.log(` Total issues (all tools): ${chalk.bold(String(results.summary.totalIssues || 0))}`);
290
+ if (results.duplicates) console.log(` Duplicate patterns found: ${chalk.bold(String(results.duplicates.length || 0))}`);
291
+ if (results.patterns) console.log(` Pattern files with issues: ${chalk.bold(String(results.patterns.length || 0))}`);
292
+ if (results.context) console.log(` Context issues: ${chalk.bold(String(results.context.length || 0))}`);
293
+ if (results.consistency) console.log(` Consistency issues: ${chalk.bold(String(results.consistency.summary.totalIssues || 0))}`);
294
+ console.log(chalk.cyan('===========================\n'));
79
295
 
80
296
  const elapsedTime = getElapsedTime(startTime);
81
297
 
82
- // Calculate score if requested
298
+ // Calculate score if requested: assemble per-tool scoring outputs
83
299
  let scoringResult: ReturnType<typeof calculateOverallScore> | undefined;
84
300
  if (options.score || finalOptions.scoring?.showBreakdown) {
85
301
  const toolScores: Map<string, ToolScoringOutput> = new Map();
86
-
87
- // Collect scores from each tool that was run
88
- if (results.patterns && baseOptions.tools.includes('patterns')) {
302
+
303
+ // Patterns score
304
+ if (results.duplicates) {
89
305
  const { calculatePatternScore } = await import('@aiready/pattern-detect');
90
- // Use the actual duplicates array which has tokenCost field
91
- const duplicates = results.duplicates || [];
92
- const score = calculatePatternScore(duplicates, results.patterns.length);
93
- toolScores.set('pattern-detect', score);
306
+ try {
307
+ const patternScore = calculatePatternScore(results.duplicates, results.patterns?.length || 0);
308
+ toolScores.set('pattern-detect', patternScore);
309
+ } catch (err) {
310
+ // ignore scoring failures for a single tool
311
+ }
94
312
  }
95
-
96
- if (results.context && baseOptions.tools.includes('context')) {
97
- const { calculateContextScore } = await import('@aiready/context-analyzer');
98
- // Calculate summary from context results
99
- const summary = {
100
- avgContextBudget: results.context.reduce((sum, r) => sum + r.contextBudget, 0) / results.context.length,
101
- maxContextBudget: Math.max(...results.context.map(r => r.contextBudget)),
102
- avgImportDepth: results.context.reduce((sum, r) => sum + r.importDepth, 0) / results.context.length,
103
- maxImportDepth: Math.max(...results.context.map(r => r.importDepth)),
104
- avgFragmentation: results.context.reduce((sum, r) => sum + r.fragmentationScore, 0) / results.context.length,
105
- criticalIssues: results.context.filter(r => r.severity === 'critical').length,
106
- majorIssues: results.context.filter(r => r.severity === 'major').length,
107
- };
108
- const score = calculateContextScore(summary as any);
109
- toolScores.set('context-analyzer', score);
313
+
314
+ // Context score
315
+ if (results.context) {
316
+ const { generateSummary: genContextSummary, calculateContextScore } = await import('@aiready/context-analyzer');
317
+ try {
318
+ const ctxSummary = genContextSummary(results.context);
319
+ const contextScore = calculateContextScore(ctxSummary);
320
+ toolScores.set('context-analyzer', contextScore);
321
+ } catch (err) {
322
+ // ignore
323
+ }
110
324
  }
111
-
112
- if (results.consistency && baseOptions.tools.includes('consistency')) {
325
+
326
+ // Consistency score
327
+ if (results.consistency) {
113
328
  const { calculateConsistencyScore } = await import('@aiready/consistency');
114
- const issues = results.consistency.results?.flatMap((r: any) => r.issues) || [];
115
- const score = calculateConsistencyScore(issues, results.consistency.summary.filesAnalyzed);
116
- toolScores.set('consistency', score);
117
- }
118
-
119
- // Parse weight overrides from CLI
120
- const cliWeights = options.weights ? parseWeightString(options.weights) : undefined;
121
-
122
- // Calculate overall score
123
- scoringResult = calculateOverallScore(toolScores, finalOptions as AIReadyConfig, cliWeights);
124
-
125
- // Check threshold
126
- if (options.threshold) {
127
- const threshold = parseFloat(options.threshold);
128
- if (scoringResult.overall < threshold) {
129
- console.error(chalk.red(`\n❌ Score ${scoringResult.overall} is below threshold ${threshold}\n`));
130
- process.exit(1);
329
+ try {
330
+ const issues = results.consistency.results?.flatMap((r: any) => r.issues) || [];
331
+ const totalFiles = results.consistency.summary?.filesAnalyzed || 0;
332
+ const consistencyScore = calculateConsistencyScore(issues, totalFiles);
333
+ toolScores.set('consistency', consistencyScore);
334
+ } catch (err) {
335
+ // ignore
131
336
  }
132
337
  }
133
- }
134
338
 
135
- const outputFormat = options.output || finalOptions.output?.format || 'console';
136
- const userOutputFile = options.outputFile || finalOptions.output?.file;
339
+ // Parse CLI weight overrides (if any)
340
+ const cliWeights = parseWeightString((options as any).weights);
137
341
 
138
- if (outputFormat === 'json') {
139
- const outputData = {
140
- ...results,
141
- summary: {
142
- ...results.summary,
143
- executionTime: parseFloat(elapsedTime),
144
- },
145
- ...(scoringResult && { scoring: scoringResult }),
146
- };
342
+ // Only calculate overall score if we have at least one tool score
343
+ if (toolScores.size > 0) {
344
+ scoringResult = calculateOverallScore(toolScores, finalOptions, cliWeights.size ? cliWeights : undefined);
147
345
 
148
- const outputPath = resolveOutputPath(
149
- userOutputFile,
150
- `aiready-scan-${new Date().toISOString().split('T')[0]}.json`,
151
- directory
152
- );
153
-
154
- handleJSONOutput(outputData, outputPath, `✅ Results saved to ${outputPath}`);
155
- } else {
156
- // Console output
157
- console.log(generateUnifiedSummary(results));
158
-
159
- // Display score if calculated
160
- if (scoringResult) {
161
- const terminalWidth = process.stdout.columns || 80;
162
- const dividerWidth = Math.min(60, terminalWidth - 2);
163
- const divider = '━'.repeat(dividerWidth);
164
-
165
- console.log(chalk.cyan('\n' + divider));
166
- console.log(chalk.bold.white(' AI READINESS SCORE'));
167
- console.log(chalk.cyan(divider) + '\n');
168
-
169
- const { emoji, color } = getRatingDisplay(scoringResult.rating);
170
- const scoreColor = color === 'green' ? chalk.green :
171
- color === 'blue' ? chalk.blue :
172
- color === 'yellow' ? chalk.yellow : chalk.red;
173
-
174
- console.log(` ${emoji} Overall Score: ${scoreColor.bold(scoringResult.overall + '/100')} (${chalk.bold(scoringResult.rating)})`); console.log(` ${chalk.dim('Timestamp:')} ${new Date(scoringResult.timestamp).toLocaleString()}\n`);
175
-
176
- // Show breakdown by tool
177
- if (scoringResult.breakdown.length > 0) {
178
- console.log(chalk.bold(' Component Scores:\n'));
179
- scoringResult.breakdown.forEach(tool => {
180
- const toolEmoji = tool.toolName === 'pattern-detect' ? '🔍' :
181
- tool.toolName === 'context-analyzer' ? '🧠' : '🏷️';
182
- const weight = scoringResult.calculation.weights[tool.toolName];
183
- console.log(` ${toolEmoji} ${chalk.white(tool.toolName.padEnd(20))} ${scoreColor(tool.score + '/100')} ${chalk.dim(`(weight: ${weight})`)}`);
346
+ console.log(chalk.bold('\n📊 AI Readiness Overall Score'));
347
+ console.log(` ${formatScore(scoringResult)}`);
348
+
349
+ // Show concise breakdown; detailed breakdown only if config requests it
350
+ if (scoringResult.breakdown && scoringResult.breakdown.length > 0) {
351
+ console.log(chalk.bold('\nTool breakdown:'));
352
+ scoringResult.breakdown.forEach((tool) => {
353
+ const rating = getRating(tool.score);
354
+ const rd = getRatingDisplay(rating);
355
+ console.log(` - ${tool.toolName}: ${tool.score}/100 (${rating}) ${rd.emoji}`);
184
356
  });
185
357
  console.log();
358
+
359
+ if (finalOptions.scoring?.showBreakdown) {
360
+ console.log(chalk.bold('Detailed tool breakdown:'));
361
+ scoringResult.breakdown.forEach((tool) => {
362
+ console.log(formatToolScore(tool));
363
+ });
364
+ console.log();
365
+ }
186
366
  }
187
-
188
- // Show calculation
189
- console.log(chalk.dim(` Weighted Formula: ${scoringResult.calculation.formula}`));
190
- console.log(chalk.dim(` Normalized Score: ${scoringResult.calculation.normalized}\n`));
191
-
192
- // Show top recommendations across all tools
193
- const allRecommendations = scoringResult.breakdown
194
- .flatMap(tool => tool.recommendations || [])
195
- .sort((a, b) => b.estimatedImpact - a.estimatedImpact)
196
- .slice(0, 5);
197
-
198
- if (allRecommendations.length > 0) {
199
- console.log(chalk.bold(' Top Recommendations:\n'));
200
- allRecommendations.forEach((rec, i) => {
201
- const priorityIcon = rec.priority === 'high' ? '🔴' :
202
- rec.priority === 'medium' ? '🟡' : '🔵';
203
- console.log(` ${i + 1}. ${priorityIcon} ${rec.action}`);
204
- console.log(` ${chalk.dim(`Impact: +${rec.estimatedImpact} points`)}\n`);
205
- });
367
+
368
+ // Persist JSON summary by default when output is json or when config directory present
369
+ const outputFormat = options.output || finalOptions.output?.format || 'console';
370
+ const userOutputFile = options.outputFile || finalOptions.output?.file;
371
+ if (outputFormat === 'json') {
372
+ const dateStr = new Date().toISOString().split('T')[0];
373
+ const defaultFilename = `aiready-scan-${dateStr}.json`;
374
+ const outputPath = resolveOutputPath(userOutputFile, defaultFilename, directory);
375
+ const outputData = { ...results, scoring: scoringResult };
376
+ handleJSONOutput(outputData, outputPath, `✅ Summary saved to ${outputPath}`);
206
377
  }
207
378
  }
208
379
  }
@@ -214,7 +385,7 @@ program
214
385
  // Individual tool commands for convenience
215
386
  program
216
387
  .command('patterns')
217
- .description('Run pattern detection analysis')
388
+ .description('Detect duplicate code patterns that confuse AI models')
218
389
  .argument('[directory]', 'Directory to analyze', '.')
219
390
  .option('-s, --similarity <number>', 'Minimum similarity score (0-1)', '0.40')
220
391
  .option('-l, --min-lines <number>', 'Minimum lines to consider', '5')
@@ -226,6 +397,12 @@ program
226
397
  .option('-o, --output <format>', 'Output format: console, json', 'console')
227
398
  .option('--output-file <path>', 'Output file path (for json)')
228
399
  .option('--score', 'Calculate and display AI Readiness Score for patterns (0-100)')
400
+ .addHelpText('after', `
401
+ EXAMPLES:
402
+ $ aiready patterns # Default analysis
403
+ $ aiready patterns --similarity 0.6 # Stricter matching
404
+ $ aiready patterns --min-lines 10 # Larger patterns only
405
+ `)
229
406
  .action(async (directory, options) => {
230
407
  console.log(chalk.blue('🔍 Analyzing patterns...\n'));
231
408
 
@@ -370,7 +547,7 @@ program
370
547
 
371
548
  program
372
549
  .command('context')
373
- .description('Run context window cost analysis')
550
+ .description('Analyze context window costs and dependency fragmentation')
374
551
  .argument('[directory]', 'Directory to analyze', '.')
375
552
  .option('--max-depth <number>', 'Maximum acceptable import depth', '5')
376
553
  .option('--max-context <number>', 'Maximum acceptable context budget (tokens)', '10000')
@@ -546,7 +723,7 @@ program
546
723
 
547
724
  program
548
725
  .command('consistency')
549
- .description('Check naming, patterns, and architecture consistency')
726
+ .description('Check naming conventions and architectural consistency')
550
727
  .argument('[directory]', 'Directory to analyze', '.')
551
728
  .option('--naming', 'Check naming conventions (default: true)')
552
729
  .option('--no-naming', 'Skip naming analysis')
package/src/index.ts CHANGED
@@ -6,6 +6,8 @@ import type { ContextAnalysisResult } from '@aiready/context-analyzer';
6
6
  import type { PatternDetectOptions, DuplicatePattern } from '@aiready/pattern-detect';
7
7
  import type { ConsistencyReport } from '@aiready/consistency';
8
8
 
9
+ import type { ConsistencyOptions } from '@aiready/consistency';
10
+
9
11
  export interface UnifiedAnalysisOptions extends ScanOptions {
10
12
  tools?: ('patterns' | 'context' | 'consistency')[];
11
13
  minSimilarity?: number;
@@ -13,6 +15,8 @@ export interface UnifiedAnalysisOptions extends ScanOptions {
13
15
  maxCandidatesPerBlock?: number;
14
16
  minSharedTokens?: number;
15
17
  useSmartDefaults?: boolean;
18
+ consistency?: Partial<ConsistencyOptions>;
19
+ progressCallback?: (event: { tool: string; data: any }) => void;
16
20
  }
17
21
 
18
22
  export interface UnifiedAnalysisResult {
@@ -68,6 +72,7 @@ export async function analyzeUnified(
68
72
  ): Promise<UnifiedAnalysisResult> {
69
73
  const startTime = Date.now();
70
74
  const tools = options.tools || ['patterns', 'context', 'consistency'];
75
+ // Tools requested and effective options are used from `options`
71
76
  const result: UnifiedAnalysisResult = {
72
77
  summary: {
73
78
  totalIssues: 0,
@@ -79,6 +84,10 @@ export async function analyzeUnified(
79
84
  // Run pattern detection
80
85
  if (tools.includes('patterns')) {
81
86
  const patternResult = await analyzePatterns(options);
87
+ // Emit progress for patterns
88
+ if (options.progressCallback) {
89
+ options.progressCallback({ tool: 'patterns', data: patternResult });
90
+ }
82
91
  // Sort results by severity
83
92
  result.patterns = sortBySeverity(patternResult.results);
84
93
  // Store duplicates for scoring
@@ -93,6 +102,9 @@ export async function analyzeUnified(
93
102
  // Run context analysis
94
103
  if (tools.includes('context')) {
95
104
  const contextResults = await analyzeContext(options);
105
+ if (options.progressCallback) {
106
+ options.progressCallback({ tool: 'context', data: contextResults });
107
+ }
96
108
  // Sort context results by severity (most severe first)
97
109
  result.context = contextResults.sort((a, b) => {
98
110
  const severityDiff = (severityOrder[b.severity] || 0) - (severityOrder[a.severity] || 0);
@@ -107,14 +119,17 @@ export async function analyzeUnified(
107
119
 
108
120
  // Run consistency analysis
109
121
  if (tools.includes('consistency')) {
110
- const report = await analyzeConsistency({
122
+ // Use config fields if present, fallback to defaults
123
+ const consistencyOptions = {
111
124
  rootDir: options.rootDir,
112
125
  include: options.include,
113
126
  exclude: options.exclude,
114
- checkNaming: true,
115
- checkPatterns: true,
116
- minSeverity: 'info',
117
- });
127
+ ...(options.consistency || {}),
128
+ };
129
+ const report = await analyzeConsistency(consistencyOptions);
130
+ if (options.progressCallback) {
131
+ options.progressCallback({ tool: 'consistency', data: report });
132
+ }
118
133
  // Sort consistency results by severity
119
134
  if (report.results) {
120
135
  report.results = sortBySeverity(report.results);
@@ -135,11 +150,11 @@ export function generateUnifiedSummary(result: UnifiedAnalysisResult): string {
135
150
  output += ` Total issues found: ${summary.totalIssues}\n`;
136
151
  output += ` Execution time: ${(summary.executionTime / 1000).toFixed(2)}s\n\n`;
137
152
 
138
- if (result.patterns?.length) {
153
+ if (result.patterns) {
139
154
  output += `🔍 Pattern Analysis: ${result.patterns.length} issues\n`;
140
155
  }
141
156
 
142
- if (result.context?.length) {
157
+ if (result.context) {
143
158
  output += `🧠 Context Analysis: ${result.context.length} issues\n`;
144
159
  }
145
160
 
@@ -1,54 +0,0 @@
1
- // src/index.ts
2
- import { analyzePatterns } from "@aiready/pattern-detect";
3
- import { analyzeContext } from "@aiready/context-analyzer";
4
- async function analyzeUnified(options) {
5
- const startTime = Date.now();
6
- const tools = options.tools || ["patterns", "context"];
7
- const result = {
8
- summary: {
9
- totalIssues: 0,
10
- toolsRun: tools,
11
- executionTime: 0
12
- }
13
- };
14
- if (tools.includes("patterns")) {
15
- const patternResult = await analyzePatterns(options);
16
- result.patterns = patternResult.results;
17
- result.summary.totalIssues += patternResult.results.length;
18
- }
19
- if (tools.includes("context")) {
20
- result.context = await analyzeContext(options);
21
- result.summary.totalIssues += result.context?.length || 0;
22
- }
23
- result.summary.executionTime = Date.now() - startTime;
24
- return result;
25
- }
26
- function generateUnifiedSummary(result) {
27
- const { summary } = result;
28
- let output = `\u{1F680} AIReady Analysis Complete
29
-
30
- `;
31
- output += `\u{1F4CA} Summary:
32
- `;
33
- output += ` Tools run: ${summary.toolsRun.join(", ")}
34
- `;
35
- output += ` Total issues found: ${summary.totalIssues}
36
- `;
37
- output += ` Execution time: ${(summary.executionTime / 1e3).toFixed(2)}s
38
-
39
- `;
40
- if (result.patterns?.length) {
41
- output += `\u{1F50D} Pattern Analysis: ${result.patterns.length} issues
42
- `;
43
- }
44
- if (result.context?.length) {
45
- output += `\u{1F9E0} Context Analysis: ${result.context.length} issues
46
- `;
47
- }
48
- return output;
49
- }
50
-
51
- export {
52
- analyzeUnified,
53
- generateUnifiedSummary
54
- };