@aiready/cli 0.14.9 → 0.14.11

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.
@@ -28,29 +28,24 @@ describe('initAction', () => {
28
28
  }
29
29
  });
30
30
 
31
- it('should generate aiready.json without output field by default', async () => {
31
+ it('should generate aiready.json with output field by default', async () => {
32
32
  await initAction({});
33
33
 
34
34
  expect(existsSync(configPath)).toBe(true);
35
35
  const config = JSON.parse(readFileSync(configPath, 'utf8'));
36
- expect(config).not.toHaveProperty('output');
36
+ expect(config).toHaveProperty('output');
37
+ expect(config.output.format).toBe('console');
37
38
  });
38
39
 
39
- it('should generate aiready.json without output field even with --full flag', async () => {
40
- await initAction({ full: true });
41
-
42
- expect(existsSync(configPath)).toBe(true);
43
- const config = JSON.parse(readFileSync(configPath, 'utf8'));
44
- expect(config).not.toHaveProperty('output');
45
- });
46
-
47
- it('should include scan, tools, and scoring sections', async () => {
40
+ it('should include scan, tools, scoring, and visualizer sections', async () => {
48
41
  await initAction({ full: true });
49
42
 
50
43
  const config = JSON.parse(readFileSync(configPath, 'utf8'));
51
44
  expect(config).toHaveProperty('scan');
52
45
  expect(config).toHaveProperty('tools');
53
46
  expect(config).toHaveProperty('scoring');
47
+ expect(config).toHaveProperty('output');
54
48
  expect(config).toHaveProperty('visualizer');
49
+ expect(config).toHaveProperty('threshold');
55
50
  });
56
51
  });
@@ -11,7 +11,7 @@ export async function bugAction(message: string | undefined, options: any) {
11
11
 
12
12
  if (message) {
13
13
  // Agent-assisted pre-filled issue
14
- const type = options.type || 'bug';
14
+ const type = options.type ?? 'bug';
15
15
  const title = `[${type.toUpperCase()}] ${message}`;
16
16
  const label =
17
17
  type === 'bug' ? 'bug' : type === 'feature' ? 'enhancement' : 'metric';
@@ -34,7 +34,7 @@ export async function consistencyAction(
34
34
  console.log(chalk.blue('🔍 Analyzing consistency...\n'));
35
35
 
36
36
  const startTime = Date.now();
37
- const resolvedDir = resolvePath(process.cwd(), directory || '.');
37
+ const resolvedDir = resolvePath(process.cwd(), directory ?? '.');
38
38
 
39
39
  try {
40
40
  // Define defaults
@@ -69,7 +69,7 @@ export async function consistencyAction(
69
69
  // Calculate score if requested
70
70
  let consistencyScore: ToolScoringOutput | undefined;
71
71
  if (options.score) {
72
- const issues = report.results?.flatMap((r: any) => r.issues) || [];
72
+ const issues = report.results?.flatMap((r: any) => r.issues) ?? [];
73
73
  consistencyScore = calculateConsistencyScore(
74
74
  issues,
75
75
  report.summary.filesAnalyzed
@@ -77,8 +77,8 @@ export async function consistencyAction(
77
77
  }
78
78
 
79
79
  const outputFormat =
80
- options.output || finalOptions.output?.format || 'console';
81
- const userOutputFile = options.outputFile || finalOptions.output?.file;
80
+ options.output ?? finalOptions.output?.format ?? 'console';
81
+ const userOutputFile = options.outputFile ?? finalOptions.output?.file;
82
82
 
83
83
  if (outputFormat === 'json') {
84
84
  const outputData = {
@@ -121,7 +121,7 @@ export async function consistencyAction(
121
121
  console.log(` Naming: ${chalk.yellow(report.summary.namingIssues)}`);
122
122
  console.log(` Patterns: ${chalk.yellow(report.summary.patternIssues)}`);
123
123
  console.log(
124
- ` Architecture: ${chalk.yellow(report.summary.architectureIssues || 0)}`
124
+ ` Architecture: ${chalk.yellow(report.summary.architectureIssues ?? 0)}`
125
125
  );
126
126
  console.log(`Analysis Time: ${chalk.gray(elapsedTime + 's')}\n`);
127
127
 
@@ -32,7 +32,7 @@ export async function contextAction(
32
32
  console.log(chalk.blue('🧠 Analyzing context costs...\n'));
33
33
 
34
34
  const startTime = Date.now();
35
- const resolvedDir = resolvePath(process.cwd(), directory || '.');
35
+ const resolvedDir = resolvePath(process.cwd(), directory ?? '.');
36
36
 
37
37
  try {
38
38
  // Define defaults
@@ -94,8 +94,8 @@ export async function contextAction(
94
94
  }
95
95
 
96
96
  const outputFormat =
97
- options.output || finalOptions.output?.format || 'console';
98
- const userOutputFile = options.outputFile || finalOptions.output?.file;
97
+ options.output ?? finalOptions.output?.format ?? 'console';
98
+ const userOutputFile = options.outputFile ?? finalOptions.output?.file;
99
99
 
100
100
  if (outputFormat === 'json') {
101
101
  const outputData = {
@@ -117,7 +117,7 @@ export async function contextAction(
117
117
  );
118
118
  } else {
119
119
  // Console output - format the results nicely
120
- const terminalWidth = process.stdout.columns || 80;
120
+ const terminalWidth = process.stdout.columns ?? 80;
121
121
  const dividerWidth = Math.min(60, terminalWidth - 2);
122
122
  const divider = '━'.repeat(dividerWidth);
123
123
 
@@ -20,6 +20,10 @@ export async function initAction(options: {
20
20
  }
21
21
 
22
22
  const baseConfig = {
23
+ // Target quality score threshold (0-100)
24
+ threshold: 75,
25
+
26
+ // Global scan settings
23
27
  scan: {
24
28
  include: [
25
29
  'src/**/*.ts',
@@ -46,6 +50,20 @@ export async function initAction(options: {
46
50
  ToolName.ChangeAmplification,
47
51
  ],
48
52
  },
53
+
54
+ // Output preferences
55
+ output: {
56
+ format: 'console',
57
+ showBreakdown: true,
58
+ saveTo: 'aiready-report.json',
59
+ },
60
+
61
+ // Scoring profile and weights
62
+ scoring: {
63
+ profile: 'balanced',
64
+ },
65
+
66
+ // Tool-specific configurations
49
67
  tools: {
50
68
  [ToolName.PatternDetect]: {
51
69
  minSimilarity: 0.8,
@@ -73,9 +91,33 @@ export async function initAction(options: {
73
91
  },
74
92
  [ToolName.NamingConsistency]: {
75
93
  shortWords: ['id', 'db', 'ui', 'ai'],
76
- ...(options.full
77
- ? { acceptedAbbreviations: [], disableChecks: [] }
78
- : {}),
94
+ acceptedAbbreviations: [
95
+ 'API',
96
+ 'JSON',
97
+ 'CSV',
98
+ 'HTML',
99
+ 'CSS',
100
+ 'HTTP',
101
+ 'URL',
102
+ 'SDK',
103
+ 'CLI',
104
+ 'AI',
105
+ 'ML',
106
+ 'ID',
107
+ 'DB',
108
+ 'UI',
109
+ 'UX',
110
+ 'DOM',
111
+ 'UUID',
112
+ 'GUID',
113
+ 'DEFAULT',
114
+ 'MAX',
115
+ 'MIN',
116
+ 'config',
117
+ 'INIT',
118
+ 'SKILL',
119
+ ],
120
+ ...(options.full ? { disableChecks: [] } : {}),
79
121
  },
80
122
  [ToolName.AiSignalClarity]: {
81
123
  checkMagicLiterals: true,
@@ -86,53 +128,45 @@ export async function initAction(options: {
86
128
  ? { checkImplicitSideEffects: false, checkDeepCallbacks: false }
87
129
  : {}),
88
130
  },
89
- ...(options.full
90
- ? {
91
- [ToolName.AgentGrounding]: {
92
- maxRecommendedDepth: 5,
93
- readmeStaleDays: 30,
94
- },
95
- [ToolName.TestabilityIndex]: {
96
- minCoverageRatio: 0.7,
97
- testPatterns: ['**/*.test.ts', '**/__tests__/**'],
98
- },
99
- [ToolName.DocDrift]: {
100
- maxCommits: 50,
101
- staleMonths: 3,
102
- },
103
- [ToolName.DependencyHealth]: {
104
- trainingCutoffYear: 2023,
105
- },
106
- }
107
- : {}),
131
+ [ToolName.AgentGrounding]: {
132
+ maxRecommendedDepth: 5,
133
+ readmeStaleDays: 30,
134
+ },
135
+ [ToolName.TestabilityIndex]: {
136
+ minCoverageRatio: 0.7,
137
+ testPatterns: ['**/*.test.ts', '**/__tests__/**'],
138
+ },
139
+ [ToolName.DocDrift]: {
140
+ maxCommits: 50,
141
+ staleMonths: 3,
142
+ },
143
+ [ToolName.DependencyHealth]: {
144
+ trainingCutoffYear: 2023,
145
+ },
146
+ [ToolName.ChangeAmplification]: {
147
+ // No specific options yet, uses global scan settings
148
+ },
108
149
  },
109
- scoring: {
110
- threshold: 70,
111
- showBreakdown: true,
112
- ...(options.full ? { profile: 'default' } : {}),
150
+
151
+ // Visualizer settings (interactive graph)
152
+ visualizer: {
153
+ groupingDirs: ['packages', 'src', 'lib'],
154
+ graph: {
155
+ maxNodes: 5000,
156
+ maxEdges: 10000,
157
+ },
113
158
  },
114
- ...(options.full
115
- ? {
116
- visualizer: {
117
- groupingDirs: ['packages', 'src', 'lib'],
118
- graph: {
119
- maxNodes: 5000,
120
- maxEdges: 10000,
121
- },
122
- },
123
- }
124
- : {}),
125
159
  };
126
160
 
127
161
  const defaultConfig = baseConfig;
128
162
 
129
163
  let content: string;
130
164
  if (fileExt === 'js') {
131
- content = `/** @type {import('@aiready/core').AIReadyConfig} */\nmodule.exports = ${JSON.stringify(
132
- defaultConfig,
133
- null,
134
- 2
135
- )};\n`;
165
+ content = `/**
166
+ * AIReady Configuration
167
+ * @type {import('@aiready/core').AIReadyConfig}
168
+ */
169
+ module.exports = ${JSON.stringify(defaultConfig, null, 2)};\n`;
136
170
  } else {
137
171
  content = JSON.stringify(defaultConfig, null, 2);
138
172
  }
@@ -35,7 +35,7 @@ export async function patternsAction(
35
35
  console.log(chalk.blue('🔍 Analyzing patterns...\n'));
36
36
 
37
37
  const startTime = Date.now();
38
- const resolvedDir = resolvePath(process.cwd(), directory || '.');
38
+ const resolvedDir = resolvePath(process.cwd(), directory ?? '.');
39
39
 
40
40
  try {
41
41
  // Determine if smart defaults should be used
@@ -100,8 +100,8 @@ export async function patternsAction(
100
100
  }
101
101
 
102
102
  const outputFormat =
103
- options.output || finalOptions.output?.format || 'console';
104
- const userOutputFile = options.outputFile || finalOptions.output?.file;
103
+ options.output ?? finalOptions.output?.format ?? 'console';
104
+ const userOutputFile = options.outputFile ?? finalOptions.output?.file;
105
105
 
106
106
  if (outputFormat === 'json') {
107
107
  const outputData = {
@@ -7,6 +7,95 @@ import {
7
7
  getRatingDisplay,
8
8
  } from '@aiready/core';
9
9
 
10
+ /**
11
+ * Generate a visual progress bar for a score.
12
+ *
13
+ * @param score - The score value (0-100)
14
+ * @param width - The width of the progress bar
15
+ * @returns A colored progress bar string
16
+ */
17
+ function generateProgressBar(score: number, width: number = 20): string {
18
+ const filled = Math.round((score / 100) * width);
19
+ const empty = width - filled;
20
+
21
+ let color = chalk.red;
22
+ if (score >= 90) color = chalk.green;
23
+ else if (score >= 75) color = chalk.cyan;
24
+ else if (score >= 60) color = chalk.yellow;
25
+
26
+ const bar = '█'.repeat(filled) + '░'.repeat(empty);
27
+ return color(bar);
28
+ }
29
+
30
+ /**
31
+ * Count issues by severity level from all tool results.
32
+ *
33
+ * @param results - The unified results object
34
+ * @returns Object with counts for each severity level
35
+ */
36
+ function countIssuesBySeverity(results: any): {
37
+ critical: number;
38
+ major: number;
39
+ minor: number;
40
+ } {
41
+ let critical = 0;
42
+ let major = 0;
43
+ let minor = 0;
44
+
45
+ if (results.summary?.toolsRun) {
46
+ for (const toolId of results.summary.toolsRun) {
47
+ const toolRes = results[toolId];
48
+ if (toolRes?.results) {
49
+ for (const fileRes of toolRes.results) {
50
+ if (fileRes.issues) {
51
+ for (const issue of fileRes.issues) {
52
+ const sev = issue.severity?.toLowerCase();
53
+ if (sev === 'critical') critical++;
54
+ else if (sev === 'major') major++;
55
+ else minor++;
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ return { critical, major, minor };
64
+ }
65
+
66
+ /**
67
+ * Get top files with most issues.
68
+ *
69
+ * @param results - The unified results object
70
+ * @param limit - Maximum number of files to return
71
+ * @returns Array of files with issue counts
72
+ */
73
+ function getTopFilesWithIssues(
74
+ results: any,
75
+ limit: number = 5
76
+ ): Array<{ file: string; count: number }> {
77
+ const fileCounts = new Map<string, number>();
78
+
79
+ if (results.summary?.toolsRun) {
80
+ for (const toolId of results.summary.toolsRun) {
81
+ const toolRes = results[toolId];
82
+ if (toolRes?.results) {
83
+ for (const fileRes of toolRes.results) {
84
+ if (fileRes.issues?.length > 0) {
85
+ const current = fileCounts.get(fileRes.fileName) || 0;
86
+ fileCounts.set(fileRes.fileName, current + fileRes.issues.length);
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ return Array.from(fileCounts.entries())
94
+ .map(([file, count]) => ({ file, count }))
95
+ .sort((a, b) => b.count - a.count)
96
+ .slice(0, limit);
97
+ }
98
+
10
99
  /**
11
100
  * Handle console output for the scan results.
12
101
  *
@@ -14,12 +103,48 @@ import {
14
103
  * @param startTime - The timestamp when the scan started.
15
104
  */
16
105
  export function printScanSummary(results: any, startTime: number) {
106
+ // Count issues by severity
107
+ const severity = countIssuesBySeverity(results);
108
+ const totalIssues = severity.critical + severity.major + severity.minor;
109
+
110
+ // Get top files with issues
111
+ const topFiles = getTopFilesWithIssues(results);
112
+
17
113
  console.log(chalk.cyan('\n=== AIReady Run Summary ==='));
114
+ console.log(` Total issues: ${chalk.bold(String(totalIssues))}`);
115
+
116
+ // Severity breakdown
117
+ if (totalIssues > 0) {
118
+ console.log(chalk.dim(' Severity breakdown:'));
119
+ if (severity.critical > 0) {
120
+ console.log(
121
+ ` ${chalk.red('●')} Critical: ${chalk.bold(severity.critical)}`
122
+ );
123
+ }
124
+ if (severity.major > 0) {
125
+ console.log(
126
+ ` ${chalk.yellow('●')} Major: ${chalk.bold(severity.major)}`
127
+ );
128
+ }
129
+ if (severity.minor > 0) {
130
+ console.log(
131
+ ` ${chalk.blue('●')} Minor: ${chalk.bold(severity.minor)}`
132
+ );
133
+ }
134
+ }
135
+
136
+ // Top files with issues
137
+ if (topFiles.length > 0) {
138
+ console.log(chalk.dim('\n Top files with issues:'));
139
+ topFiles.forEach((item) => {
140
+ console.log(
141
+ ` ${chalk.yellow('→')} ${item.file}: ${chalk.bold(item.count)} issues`
142
+ );
143
+ });
144
+ }
145
+
18
146
  console.log(
19
- ` Total issues (all tools): ${chalk.bold(String(results.summary.totalIssues || 0))}`
20
- );
21
- console.log(
22
- ` Execution time: ${chalk.bold(((Date.now() - startTime) / 1000).toFixed(2) + 's')}`
147
+ `\n Execution time: ${chalk.bold(((Date.now() - startTime) / 1000).toFixed(2) + 's')}`
23
148
  );
24
149
  }
25
150
 
@@ -61,25 +186,26 @@ export function printScoring(
61
186
 
62
187
  if (scoringResult.breakdown) {
63
188
  console.log(chalk.bold('\nTool breakdown:'));
64
- scoringResult.breakdown.forEach((tool) => {
189
+ scoringResult.breakdown.forEach((tool: any) => {
65
190
  const rating = getRating(tool.score);
66
191
  const emoji = getRatingDisplay(rating).emoji;
192
+ const progressBar = generateProgressBar(tool.score, 15);
67
193
  console.log(
68
- ` - ${tool.toolName}: ${tool.score}/100 (${rating}) ${emoji}`
194
+ ` ${progressBar} ${tool.score}/100 (${rating}) ${emoji} ${tool.toolName}`
69
195
  );
70
196
  });
71
197
 
72
- // Top Actionable Recommendations
198
+ // Top Actionable Recommendations - increased from 3 to 5
73
199
  const allRecs = scoringResult.breakdown
74
- .flatMap((t) =>
75
- (t.recommendations || []).map((r) => ({ ...r, tool: t.toolName }))
200
+ .flatMap((t: any) =>
201
+ (t.recommendations ?? []).map((r: any) => ({ ...r, tool: t.toolName }))
76
202
  )
77
- .sort((a, b) => b.estimatedImpact - a.estimatedImpact)
78
- .slice(0, 3);
203
+ .sort((a: any, b: any) => b.estimatedImpact - a.estimatedImpact)
204
+ .slice(0, 5); // Increased from 3 to 5
79
205
 
80
206
  if (allRecs.length > 0) {
81
207
  console.log(chalk.bold('\n🎯 Top Actionable Recommendations:'));
82
- allRecs.forEach((rec, i) => {
208
+ allRecs.forEach((rec: any, i: number) => {
83
209
  const priorityIcon =
84
210
  rec.priority === 'high'
85
211
  ? '🔴'
@@ -57,7 +57,7 @@ export async function scanAction(directory: string, options: ScanOptions) {
57
57
  console.log(chalk.blue('🚀 Starting AIReady unified analysis...\n'));
58
58
 
59
59
  const startTime = Date.now();
60
- const resolvedDir = resolvePath(process.cwd(), directory || '.');
60
+ const resolvedDir = resolvePath(process.cwd(), directory ?? '.');
61
61
  const repoMetadata = getRepoMetadata(resolvedDir);
62
62
 
63
63
  try {
@@ -159,7 +159,7 @@ export async function scanAction(directory: string, options: ScanOptions) {
159
159
  const { getSmartDefaults } = await import('@aiready/pattern-detect');
160
160
  const patternSmartDefaults = await getSmartDefaults(
161
161
  resolvedDir,
162
- finalOptions.toolConfigs?.[ToolName.PatternDetect] || {}
162
+ finalOptions.toolConfigs?.[ToolName.PatternDetect] ?? {}
163
163
  );
164
164
 
165
165
  // Merge smart defaults into toolConfigs instead of root level
@@ -173,7 +173,7 @@ export async function scanAction(directory: string, options: ScanOptions) {
173
173
  console.log(chalk.cyan('\n=== AIReady Run Preview ==='));
174
174
  console.log(
175
175
  chalk.white('Tools to run:'),
176
- (finalOptions.tools || []).join(', ')
176
+ (finalOptions.tools ?? []).join(', ')
177
177
  );
178
178
 
179
179
  // Dynamic progress callback
@@ -202,7 +202,7 @@ export async function scanAction(directory: string, options: ScanOptions) {
202
202
 
203
203
  // Determine scoring profile for project-type-aware weighting
204
204
  const scoringProfile =
205
- options.profile || baseOptions.scoring?.profile || 'default';
205
+ options.profile ?? baseOptions.scoring?.profile ?? 'default';
206
206
 
207
207
  const results = await analyzeUnified({
208
208
  ...finalOptions,
@@ -233,7 +233,7 @@ export async function scanAction(directory: string, options: ScanOptions) {
233
233
  readFileSync(resolvePath(process.cwd(), options.compareTo), 'utf8')
234
234
  );
235
235
  const prevScore =
236
- prevReport.scoring?.overall || prevReport.scoring?.score;
236
+ prevReport.scoring?.overall ?? prevReport.scoring?.score;
237
237
  if (typeof prevScore === 'number') {
238
238
  const diff = scoringResult.overall - prevScore;
239
239
  const diffStr = diff > 0 ? `+${diff}` : String(diff);
@@ -262,19 +262,19 @@ export async function scanAction(directory: string, options: ScanOptions) {
262
262
  }
263
263
 
264
264
  // Token Budget & Cost Logic
265
- const totalWastedDuplication = (scoringResult.breakdown || []).reduce(
266
- (sum, s) =>
267
- sum + (s.tokenBudget?.wastedTokens.bySource.duplication || 0),
265
+ const totalWastedDuplication = (scoringResult.breakdown ?? []).reduce(
266
+ (sum: number, s: any) =>
267
+ sum + (s.tokenBudget?.wastedTokens?.bySource?.duplication ?? 0),
268
268
  0
269
269
  );
270
- const totalWastedFragmentation = (scoringResult.breakdown || []).reduce(
271
- (sum, s) =>
272
- sum + (s.tokenBudget?.wastedTokens.bySource.fragmentation || 0),
270
+ const totalWastedFragmentation = (scoringResult.breakdown ?? []).reduce(
271
+ (sum: number, s: any) =>
272
+ sum + (s.tokenBudget?.wastedTokens?.bySource?.fragmentation ?? 0),
273
273
  0
274
274
  );
275
275
  const totalContext = Math.max(
276
- ...(scoringResult.breakdown || []).map(
277
- (s) => s.tokenBudget?.totalContextTokens || 0
276
+ ...(scoringResult.breakdown ?? []).map(
277
+ (s: any) => s.tokenBudget?.totalContextTokens ?? 0
278
278
  ),
279
279
  0
280
280
  );
@@ -300,7 +300,7 @@ export async function scanAction(directory: string, options: ScanOptions) {
300
300
  }
301
301
  }
302
302
 
303
- const modelId = options.model || 'claude-3-5-sonnet';
303
+ const modelId = options.model ?? 'gpt-5.4-mini';
304
304
  const roi = (await import('@aiready/core')).calculateBusinessROI({
305
305
  tokenWaste: unifiedBudget.wastedTokens.total,
306
306
  issues: allIssues,
@@ -345,9 +345,9 @@ export async function scanAction(directory: string, options: ScanOptions) {
345
345
 
346
346
  // Output persistence
347
347
  const outputFormat =
348
- options.output || finalOptions.output?.format || 'console';
348
+ options.output ?? finalOptions.output?.format ?? 'console';
349
349
  const outputPath = resolveOutputPath(
350
- options.outputFile || finalOptions.output?.file,
350
+ options.outputFile ?? finalOptions.output?.file,
351
351
  `aiready-report-${getReportTimestamp()}.json`,
352
352
  resolvedDir
353
353
  );
@@ -380,8 +380,8 @@ export async function scanAction(directory: string, options: ScanOptions) {
380
380
  const threshold = options.threshold
381
381
  ? parseInt(options.threshold)
382
382
  : undefined;
383
- const failOnLevel = options.failOn || 'critical';
384
- const isCI = options.ci || process.env.CI === 'true';
383
+ const failOnLevel = options.failOn ?? 'critical';
384
+ const isCI = options.ci ?? process.env.CI === 'true';
385
385
 
386
386
  let shouldFail = false;
387
387
  let failReason = '';
@@ -13,10 +13,10 @@ export async function uploadAction(file: string, options: UploadOptions) {
13
13
  const startTime = Date.now();
14
14
  const filePath = resolvePath(process.cwd(), file);
15
15
  const serverUrl =
16
- options.server ||
17
- process.env.AIREADY_SERVER ||
16
+ options.server ??
17
+ process.env.AIREADY_SERVER ??
18
18
  'https://dev.platform.getaiready.dev';
19
- const apiKey = options.apiKey || process.env.AIREADY_API_KEY;
19
+ const apiKey = options.apiKey ?? process.env.AIREADY_API_KEY;
20
20
 
21
21
  if (!apiKey) {
22
22
  console.error(chalk.red('❌ API Key is required for upload.'));
@@ -49,7 +49,7 @@ export async function uploadAction(file: string, options: UploadOptions) {
49
49
 
50
50
  // Prepare upload payload
51
51
  // Note: repoId is optional if the metadata contains it, but for now we'll require it or infer from metadata
52
- const repoId = options.repoId || reportData.repository?.repoId;
52
+ const repoId = options.repoId ?? reportData.repository?.repoId;
53
53
 
54
54
  const response = await fetch(`${serverUrl}/api/analysis/upload`, {
55
55
  method: 'POST',
@@ -70,13 +70,13 @@ export async function uploadAction(file: string, options: UploadOptions) {
70
70
  uploadResult = await response.json();
71
71
  } else {
72
72
  const text = await response.text();
73
- uploadResult = { error: text || response.statusText };
73
+ uploadResult = { error: text ?? response.statusText };
74
74
  }
75
75
 
76
76
  if (!response.ok) {
77
77
  console.error(
78
78
  chalk.red(
79
- `❌ Upload failed: ${uploadResult.error || response.statusText}`
79
+ `❌ Upload failed: ${uploadResult.error ?? response.statusText}`
80
80
  )
81
81
  );
82
82
 
@@ -30,7 +30,7 @@ export async function visualizeAction(
30
30
  options: VisualizeOptions
31
31
  ) {
32
32
  try {
33
- const dirPath = resolvePath(process.cwd(), directory || '.');
33
+ const dirPath = resolvePath(process.cwd(), directory ?? '.');
34
34
  let reportPath = options.report
35
35
  ? resolvePath(dirPath, options.report)
36
36
  : null;
@@ -87,7 +87,7 @@ export async function visualizeAction(
87
87
  const graph = GraphBuilder.buildFromReport(report, dirPath);
88
88
 
89
89
  // Check if --dev mode is requested and available
90
- let useDevMode = options.dev || false;
90
+ let useDevMode = options.dev ?? false;
91
91
  let devServerStarted = false;
92
92
 
93
93
  if (useDevMode) {
@@ -230,7 +230,7 @@ export async function visualizeAction(
230
230
  console.log('Generating HTML...');
231
231
  const html = generateHTML(graph as any);
232
232
  const defaultOutput = 'visualization.html';
233
- const outPath = resolvePath(dirPath, options.output || defaultOutput);
233
+ const outPath = resolvePath(dirPath, options.output ?? defaultOutput);
234
234
  writeFileSync(outPath, html, 'utf8');
235
235
  console.log(chalk.green(`✅ Visualization written to: ${outPath}`));
236
236
 
@@ -250,7 +250,7 @@ export async function visualizeAction(
250
250
 
251
251
  const server = http.createServer(async (req, res) => {
252
252
  try {
253
- const urlPath = req.url || '/';
253
+ const urlPath = req.url ?? '/';
254
254
  if (urlPath === '/' || urlPath === '/index.html') {
255
255
  const content = await fsp.readFile(outPath, 'utf8');
256
256
  res.writeHead(200, {