@aiready/cli 0.12.11 → 0.12.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { analyzeUnified } from '../index';
3
+ import { ToolRegistry, ToolName, SpokeOutputSchema } from '@aiready/core';
4
+
5
+ describe('CLI Configuration Shape', () => {
6
+ beforeEach(() => {
7
+ ToolRegistry.clear();
8
+
9
+ // Register a mock provider that returns its input config in metadata
10
+ ToolRegistry.register({
11
+ id: ToolName.PatternDetect,
12
+ alias: ['patterns'],
13
+ analyze: async (options) =>
14
+ SpokeOutputSchema.parse({
15
+ results: [],
16
+ summary: { config: options },
17
+ metadata: {
18
+ toolName: ToolName.PatternDetect,
19
+ version: '1.0.0',
20
+ config: options,
21
+ },
22
+ }),
23
+ score: () => ({
24
+ toolName: ToolName.PatternDetect,
25
+ score: 80,
26
+ factors: [],
27
+ recommendations: [],
28
+ rawMetrics: {},
29
+ }),
30
+ defaultWeight: 10,
31
+ });
32
+ });
33
+
34
+ afterEach(() => {
35
+ ToolRegistry.clear();
36
+ });
37
+
38
+ it('should generate a strictly portable AIReadyConfig in summary', async () => {
39
+ const results = await analyzeUnified({
40
+ rootDir: '/tmp/fake-repo',
41
+ tools: [ToolName.PatternDetect],
42
+ exclude: ['**/node_modules/**'],
43
+ // Pass a tool-specific override
44
+ toolConfigs: {
45
+ [ToolName.PatternDetect]: {
46
+ minSimilarity: 0.9,
47
+ // This should be stripped
48
+ rootDir: '/tmp/fake-repo',
49
+ },
50
+ },
51
+ });
52
+
53
+ const config = results.summary.config;
54
+
55
+ // 1. Check top-level structure
56
+ expect(config).toHaveProperty('scan');
57
+ expect(config).toHaveProperty('tools');
58
+
59
+ // 2. Ensure rootDir is STRIPPED from top level
60
+ expect(config).not.toHaveProperty('rootDir');
61
+
62
+ // 3. Ensure internal keys are stripped from scan section
63
+ expect(config.scan).toHaveProperty('tools');
64
+ expect(config.scan).toHaveProperty('exclude');
65
+ expect(config.scan).not.toHaveProperty('rootDir');
66
+
67
+ // 4. Ensure recursive stripping in tools section
68
+ const patternConfig = config.tools[ToolName.PatternDetect];
69
+ expect(patternConfig).toHaveProperty('minSimilarity', 0.9);
70
+ expect(patternConfig).not.toHaveProperty('rootDir');
71
+ expect(patternConfig).not.toHaveProperty('onProgress');
72
+ });
73
+
74
+ it('should strip internal keys like useSmartDefaults and batchSize', async () => {
75
+ const results = await analyzeUnified({
76
+ rootDir: '/test',
77
+ tools: [ToolName.PatternDetect],
78
+ useSmartDefaults: true,
79
+ // @ts-ignore - testing internal key stripping
80
+ batchSize: 50,
81
+ });
82
+
83
+ const config = results.summary.config;
84
+
85
+ expect(config).not.toHaveProperty('useSmartDefaults');
86
+ expect(config.scan).not.toHaveProperty('useSmartDefaults');
87
+
88
+ // Check tool level too
89
+ const patternConfig = config.tools[ToolName.PatternDetect];
90
+ expect(patternConfig).not.toHaveProperty('useSmartDefaults');
91
+ expect(patternConfig).not.toHaveProperty('batchSize');
92
+ });
93
+
94
+ it('should produce a config that is compatible with tool specific collection', async () => {
95
+ // This test ensures that the toolConfigs collected from individual tools
96
+ // are also sanitized before being merged into the final report.
97
+ const results = await analyzeUnified({
98
+ rootDir: '/test',
99
+ tools: [ToolName.PatternDetect],
100
+ });
101
+
102
+ const toolConfigs = results.summary.toolConfigs;
103
+ expect(toolConfigs).toBeDefined();
104
+ expect(toolConfigs![ToolName.PatternDetect]).not.toHaveProperty('rootDir');
105
+ });
106
+ });
@@ -184,7 +184,7 @@ export async function scanAction(directory: string, options: ScanOptions) {
184
184
  const results = await analyzeUnified({
185
185
  ...finalOptions,
186
186
  progressCallback,
187
- onProgress: () => { },
187
+ onProgress: () => {},
188
188
  suppressToolConfig: true,
189
189
  });
190
190
 
@@ -192,6 +192,9 @@ export async function scanAction(directory: string, options: ScanOptions) {
192
192
  console.log(
193
193
  ` Total issues (all tools): ${chalk.bold(String(results.summary.totalIssues || 0))}`
194
194
  );
195
+ console.log(
196
+ ` Execution time: ${chalk.bold(((Date.now() - startTime) / 1000).toFixed(2) + 's')}`
197
+ );
195
198
 
196
199
  let scoringResult: ScoringResult | undefined;
197
200
  if (options.score || finalOptions.scoring?.showBreakdown) {
package/src/index.ts CHANGED
@@ -92,19 +92,44 @@ const TOOL_PACKAGE_MAP: Record<string, string> = {
92
92
  };
93
93
 
94
94
  /**
95
- * Sanitize tool configuration by removing global options (except rootDir)
95
+ * Deeply sanitizes a configuration object by removing infrastructure keys like rootDir.
96
+ * Works recursively to clean up nested tool configurations to ensure compatibility
97
+ * with the AIReadyConfig schema for portable configuration files.
96
98
  */
97
- function sanitizeToolConfig(config: any): any {
98
- if (!config || typeof config !== 'object') return config;
99
- const sanitized = { ...config };
100
- GLOBAL_INFRA_OPTIONS.forEach((key: string) => {
101
- if (key !== 'rootDir') {
102
- delete (sanitized as any)[key];
99
+ function sanitizeConfigRecursive(obj: any): any {
100
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj;
101
+
102
+ const sanitized: any = {};
103
+ const infraToStrip = [
104
+ 'rootDir',
105
+ 'onProgress',
106
+ 'progressCallback',
107
+ 'streamResults',
108
+ 'batchSize',
109
+ 'useSmartDefaults',
110
+ ];
111
+
112
+ for (const [key, value] of Object.entries(obj)) {
113
+ // Skip infrastructure keys that shouldn't be in a portable config file
114
+ if (infraToStrip.includes(key)) continue;
115
+
116
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
117
+ sanitized[key] = sanitizeConfigRecursive(value);
118
+ } else {
119
+ sanitized[key] = value;
103
120
  }
104
- });
121
+ }
122
+
105
123
  return sanitized;
106
124
  }
107
125
 
126
+ /**
127
+ * Sanitize tool configuration by removing global options to match AIReadyConfig schema
128
+ */
129
+ function sanitizeToolConfig(config: any): any {
130
+ return sanitizeConfigRecursive(config);
131
+ }
132
+
108
133
  /**
109
134
  * AIReady Unified Analysis
110
135
  * Orchestrates all registered tools via the ToolRegistry.
@@ -169,9 +194,9 @@ export async function analyzeUnified(
169
194
 
170
195
  try {
171
196
  // Sanitize options for metadata tracking (remove functions/internal keys)
172
- const sanitizedConfig = { ...options };
173
- delete (sanitizedConfig as any).onProgress;
174
- delete (sanitizedConfig as any).progressCallback;
197
+ const sanitizedOptions = { ...options };
198
+ delete (sanitizedOptions as any).onProgress;
199
+ delete (sanitizedOptions as any).progressCallback;
175
200
 
176
201
  // 1. Start with sanitized global subset
177
202
  const toolOptions: any = {
@@ -181,15 +206,26 @@ export async function analyzeUnified(
181
206
  // 1. Pass through all known infra and fine-tuning options from root level
182
207
  [...GLOBAL_INFRA_OPTIONS, ...COMMON_FINE_TUNING_OPTIONS].forEach(
183
208
  (key) => {
184
- if (key in options && key !== 'toolConfigs') {
209
+ if (key in options && key !== 'toolConfigs' && key !== 'tools') {
185
210
  toolOptions[key] = (options as any)[key];
186
211
  }
187
212
  }
188
213
  );
189
214
 
190
215
  // 3. Add tool-specific overrides
216
+ // Check priority:
217
+ // a) options.toolConfigs[toolId]
218
+ // b) options.tools[toolId] (if tools is an object, not the array of tool names)
219
+ // c) options[toolId] (legacy)
191
220
  if (options.toolConfigs?.[provider.id]) {
192
221
  Object.assign(toolOptions, options.toolConfigs[provider.id]);
222
+ } else if (
223
+ options.tools &&
224
+ !Array.isArray(options.tools) &&
225
+ typeof options.tools === 'object' &&
226
+ (options.tools as any)[provider.id]
227
+ ) {
228
+ Object.assign(toolOptions, (options.tools as any)[provider.id]);
193
229
  } else if ((options as any)[provider.id]) {
194
230
  // Fallback for legacy tool-specific keys
195
231
  Object.assign(toolOptions, (options as any)[provider.id]);
@@ -275,11 +311,17 @@ export async function analyzeUnified(
275
311
  }
276
312
  }
277
313
 
278
- // Finalize configuration for metadata
279
- result.summary.config = {
280
- ...options,
281
- toolConfigs: result.summary.toolConfigs,
282
- };
314
+ // Finalize configuration for metadata to match AIReadyConfig schema
315
+ // We use sanitizeConfigRecursive to ensure no internal keys (like rootDir) remain
316
+ result.summary.config = sanitizeConfigRecursive({
317
+ scan: {
318
+ tools: requestedTools,
319
+ include: options.include,
320
+ exclude: options.exclude,
321
+ },
322
+ // Use 'tools' for tool-specific configurations to match AIReadyConfig
323
+ tools: result.summary.toolConfigs,
324
+ });
283
325
 
284
326
  result.summary.executionTime = Date.now() - startTime;
285
327
  return result;