depfixer 1.1.8 → 1.2.0

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.
Files changed (52) hide show
  1. package/dist/commands/check.d.ts +8 -0
  2. package/dist/commands/check.d.ts.map +1 -0
  3. package/dist/commands/check.js +742 -0
  4. package/dist/commands/check.js.map +1 -0
  5. package/dist/commands/migrate.d.ts +1 -7
  6. package/dist/commands/migrate.d.ts.map +1 -1
  7. package/dist/commands/migrate.js +702 -706
  8. package/dist/commands/migrate.js.map +1 -1
  9. package/dist/commands/smart.d.ts.map +1 -1
  10. package/dist/commands/smart.js +954 -911
  11. package/dist/commands/smart.js.map +1 -1
  12. package/dist/constants/analysis.constants.d.ts +2 -0
  13. package/dist/constants/analysis.constants.d.ts.map +1 -1
  14. package/dist/constants/analysis.constants.js +9 -0
  15. package/dist/constants/analysis.constants.js.map +1 -1
  16. package/dist/index.js +57 -17
  17. package/dist/index.js.map +1 -1
  18. package/dist/services/api-client.d.ts +89 -0
  19. package/dist/services/api-client.d.ts.map +1 -1
  20. package/dist/services/api-client.js +95 -1
  21. package/dist/services/api-client.js.map +1 -1
  22. package/dist/services/framework-detector.d.ts +23 -0
  23. package/dist/services/framework-detector.d.ts.map +1 -0
  24. package/dist/services/framework-detector.js +230 -0
  25. package/dist/services/framework-detector.js.map +1 -0
  26. package/dist/services/payment-flow.d.ts +3 -1
  27. package/dist/services/payment-flow.d.ts.map +1 -1
  28. package/dist/services/payment-flow.js +8 -1
  29. package/dist/services/payment-flow.js.map +1 -1
  30. package/dist/utils/framework-utils.d.ts +29 -0
  31. package/dist/utils/framework-utils.d.ts.map +1 -0
  32. package/dist/utils/framework-utils.js +45 -0
  33. package/dist/utils/framework-utils.js.map +1 -0
  34. package/dist/utils/interactive-picker.d.ts +12 -0
  35. package/dist/utils/interactive-picker.d.ts.map +1 -0
  36. package/dist/utils/interactive-picker.js +109 -0
  37. package/dist/utils/interactive-picker.js.map +1 -0
  38. package/dist/utils/package-parser.d.ts +24 -0
  39. package/dist/utils/package-parser.d.ts.map +1 -0
  40. package/dist/utils/package-parser.js +63 -0
  41. package/dist/utils/package-parser.js.map +1 -0
  42. package/dist/utils/prompt.d.ts +13 -0
  43. package/dist/utils/prompt.d.ts.map +1 -0
  44. package/dist/utils/prompt.js +71 -0
  45. package/dist/utils/prompt.js.map +1 -0
  46. package/dist/utils/table-builders.d.ts +40 -0
  47. package/dist/utils/table-builders.d.ts.map +1 -0
  48. package/dist/utils/table-builders.js +175 -0
  49. package/dist/utils/table-builders.js.map +1 -0
  50. package/dist/version.d.ts +1 -1
  51. package/dist/version.js +1 -1
  52. package/package.json +23 -3
@@ -1,5 +1,18 @@
1
+ /**
2
+ * Smart Command
3
+ *
4
+ * The main analysis and fix command for DepFixer CLI.
5
+ * Handles both interactive (default) and CI modes.
6
+ *
7
+ * Flow:
8
+ * 1. Run audit analysis (FREE)
9
+ * 2. Show issues with cost
10
+ * 3. Prompt to pay and fix
11
+ * 4. If YES: auth → balance check → pay → apply fix
12
+ * 5. If NO: Save to session, user can run `fix` later
13
+ */
1
14
  import chalk from 'chalk';
2
- import { CLI_AUDIT_SAMPLE_SIZE, CLI_AUDIT_THRESHOLD } from '../constants/analysis.constants.js';
15
+ import { CLI_AUDIT_SAMPLE_SIZE, CLI_AUDIT_THRESHOLD, FIX_STEPS } from '../constants/analysis.constants.js';
3
16
  import { ApiClient } from '../services/api-client.js';
4
17
  import { PackageJsonService } from '../services/package-json.js';
5
18
  import { SessionManager } from '../services/session-manager.js';
@@ -9,6 +22,49 @@ import { analytics } from '../services/analytics.js';
9
22
  import { getDeviceId } from '../services/device-id.js';
10
23
  import { createSpinner, createMigrationTable, printError, printSuccess, printInfo, runStepSequence, sleep, } from '../utils/output.js';
11
24
  import { colors, printCliHeader, renderHealthBar, getHealthStatus, printCostBox, printSuccessBox, printProjectHeader, printDiagnosis, printUserDetails, printCreditCheck, } from '../utils/design-system.js';
25
+ import { promptYesNo } from '../utils/prompt.js';
26
+ import { calculateSummary } from '../utils/framework-utils.js';
27
+ import { createTeaserTable, createFullSolutionTable } from '../utils/table-builders.js';
28
+ // ============================================================================
29
+ // DEV MODE HELPERS
30
+ // ============================================================================
31
+ /**
32
+ * Check if auto-confirm is enabled (dev mode only)
33
+ */
34
+ function isAutoConfirmEnabled() {
35
+ const isDevMode = process.env.NODE_ENV === 'development' ||
36
+ (process.env.DEPFIXER_API_URL?.includes('localhost') ?? false);
37
+ return isDevMode && process.env.DEPFIXER_AUTO_CONFIRM === 'true';
38
+ }
39
+ /**
40
+ * Prompt with auto-confirm support for dev mode
41
+ */
42
+ async function confirmPrompt(question, options) {
43
+ // Auto-confirm with --yes flag
44
+ if (options.yes) {
45
+ if (question) {
46
+ console.log(`${question} ${colors.dim('(Enter/Esc)')} ${colors.success('Yes')} ${colors.dim('[--yes]')}`);
47
+ }
48
+ else {
49
+ console.log(`${colors.success('Yes')} ${colors.dim('[--yes]')}`);
50
+ }
51
+ return true;
52
+ }
53
+ // Auto-confirm in dev mode when env var is set
54
+ if (isAutoConfirmEnabled()) {
55
+ if (question) {
56
+ console.log(`${question} ${colors.dim('(Enter/Esc)')} ${colors.success('Yes')} ${colors.dim('[auto]')}`);
57
+ }
58
+ else {
59
+ console.log(`${colors.success('Yes')} ${colors.dim('[auto]')}`);
60
+ }
61
+ return true;
62
+ }
63
+ return promptYesNo(question);
64
+ }
65
+ // ============================================================================
66
+ // MAIN COMMAND
67
+ // ============================================================================
12
68
  /**
13
69
  * Smart command (DEFAULT)
14
70
  *
@@ -28,118 +84,13 @@ export async function smartCommand(options) {
28
84
  // Create device ID early (for anonymous user tracking before login)
29
85
  getDeviceId();
30
86
  try {
31
- const packageJsonService = new PackageJsonService();
32
- const apiClient = new ApiClient();
33
- const sessionManager = new SessionManager(projectDir);
34
- // Read and sanitize package.json
35
- const { content: packageJsonContent, parsed } = await packageJsonService.read(projectDir);
36
- const sanitized = packageJsonService.sanitize(parsed);
37
- // Detect framework
38
- const framework = detectFramework(sanitized);
39
- const frameworkInfo = framework ? `${framework.charAt(0).toUpperCase() + framework.slice(1)}` : '';
40
- // ================== CI MODE ==================
41
- // CI mode uses dedicated endpoint with API key or JWT auth
42
- // Returns complete analysis for pass/fail pipeline decisions
87
+ // Initialize context
88
+ const ctx = await initializeContext(projectDir, options);
89
+ // Handle CI mode separately (non-interactive)
43
90
  if (options.ci) {
44
- const authManager = new AuthManager();
45
- const authHeader = await authManager.getAuthHeader();
46
- if (!authHeader) {
47
- // No authentication available
48
- if (options.json) {
49
- console.log(JSON.stringify({
50
- success: false,
51
- error: 'Authentication required for CI mode',
52
- help: 'Set DEPFIXER_TOKEN environment variable',
53
- docs: 'https://depfixer.com/docs/ci-setup',
54
- }, null, 2));
55
- }
56
- else {
57
- console.log();
58
- console.log(chalk.red(' Authentication required for CI mode'));
59
- console.log();
60
- console.log(chalk.bold(' Setup:'));
61
- console.log(' 1. Get API token: https://app.depfixer.com/dashboard/api-keys');
62
- console.log(' 2. Add to GitHub Secrets as DEPFIXER_TOKEN');
63
- console.log(' 3. Use in workflow:');
64
- console.log();
65
- console.log(chalk.dim(' - run: npx depfixer --ci'));
66
- console.log(chalk.dim(' env:'));
67
- console.log(chalk.dim(' DEPFIXER_TOKEN: ${{ secrets.DEPFIXER_TOKEN }}'));
68
- console.log();
69
- }
70
- process.exit(2);
71
- }
72
- // Call CI endpoint (full analysis with API key auth)
73
- try {
74
- const ciResponse = await apiClient.analyzeForCi(sanitized, framework);
75
- if (!ciResponse.success || !ciResponse.data) {
76
- if (options.json) {
77
- console.log(JSON.stringify({ success: false, error: ciResponse.error || 'CI analysis failed' }, null, 2));
78
- }
79
- else {
80
- console.log(chalk.red(` ${ciResponse.error || 'CI analysis failed'}`));
81
- }
82
- process.exit(2);
83
- }
84
- const ciData = ciResponse.data;
85
- const issueCount = ciData.summary.critical + ciData.summary.high + ciData.summary.medium + ciData.summary.low;
86
- // JSON output for CI pipelines (machine-readable)
87
- if (options.json) {
88
- console.log(JSON.stringify({
89
- success: true,
90
- mode: 'ci',
91
- analysisId: ciData.analysisId,
92
- healthScore: ciData.healthScore,
93
- totalPackages: ciData.totalPackages,
94
- summary: ciData.summary,
95
- issueCount,
96
- conflicts: ciData.conflicts,
97
- framework: ciData.framework,
98
- requiresAttention: ciData.requiresAttention,
99
- }, null, 2));
100
- process.exit(ciData.requiresAttention ? 1 : 0);
101
- }
102
- // Human-readable output for CI logs
103
- console.log();
104
- console.log(chalk.bold(' CI Mode - Dependency Analysis'));
105
- console.log(chalk.dim(' ' + '─'.repeat(40)));
106
- console.log(` Health Score: ${ciData.healthScore}/100`);
107
- console.log(` Total Packages: ${ciData.totalPackages}`);
108
- console.log(` Issues Found: ${issueCount}`);
109
- if (ciData.summary.critical > 0)
110
- console.log(chalk.red(` Critical: ${ciData.summary.critical}`));
111
- if (ciData.summary.high > 0)
112
- console.log(chalk.yellow(` High: ${ciData.summary.high}`));
113
- if (ciData.summary.medium > 0)
114
- console.log(chalk.blue(` Medium: ${ciData.summary.medium}`));
115
- if (ciData.summary.low > 0)
116
- console.log(chalk.dim(` Low: ${ciData.summary.low}`));
117
- console.log();
118
- if (ciData.requiresAttention) {
119
- console.log(chalk.red(' Pipeline should fail - critical/high issues detected'));
120
- process.exit(1);
121
- }
122
- else if (issueCount > 0) {
123
- console.log(chalk.yellow(' Minor issues detected (medium/low severity)'));
124
- process.exit(0);
125
- }
126
- else {
127
- console.log(chalk.green(' No dependency issues found'));
128
- process.exit(0);
129
- }
130
- }
131
- catch (ciError) {
132
- if (options.json) {
133
- console.log(JSON.stringify({ success: false, error: ciError.message }, null, 2));
134
- }
135
- else {
136
- console.log(chalk.red(` CI analysis failed: ${ciError.message}`));
137
- }
138
- process.exit(2);
139
- }
140
- return; // CI mode exits above, this is just for type safety
91
+ await handleCiMode(ctx);
92
+ return;
141
93
  }
142
- // ================== END CI MODE ==================
143
94
  // Print CLI header (skip for JSON output)
144
95
  if (!options.json) {
145
96
  printCliHeader();
@@ -148,878 +99,970 @@ export async function smartCommand(options) {
148
99
  analytics.analyzeStarted({ command: 'smart' });
149
100
  // Print project info (skip for JSON output)
150
101
  if (!options.json) {
151
- printProjectHeader(sanitized.name || 'unnamed', frameworkInfo);
102
+ printProjectHeader(ctx.sanitized.name || 'unnamed', ctx.frameworkInfo);
152
103
  }
153
- // Calculate package.json hash for integrity check
154
- const packageJsonHash = sessionManager.calculateHash(packageJsonContent);
155
- // Analysis steps - enough to cover ~3-4 seconds before showing elapsed time
156
- const analysisSteps = [
157
- 'Parsing dependency tree...',
158
- `Detecting framework...${frameworkInfo ? ` ${frameworkInfo}` : ''}`,
159
- 'Loading compatibility matrix...',
160
- 'Scanning package versions...',
161
- 'Resolving peer dependencies...',
162
- 'Checking version constraints...',
163
- 'Analyzing transitive dependencies...',
164
- 'Detecting breaking changes...',
165
- 'Evaluating deprecation status...',
166
- 'Calculating version intersections...',
167
- 'Checking cross-package rules...',
168
- 'Generating recommendations...',
169
- ];
170
- let response;
171
- // First phase: Initial API call
104
+ // Run audit analysis
105
+ const auditResult = await runAuditAnalysis(ctx);
106
+ // JSON output mode - just output and return
172
107
  if (options.json) {
173
- // Silent mode for JSON output
174
- response = await apiClient.analyzeAudit(sanitized, framework);
175
- if (!response.success || !response.data) {
176
- throw new Error(response.error || 'Unknown error');
177
- }
178
- }
179
- else {
180
- // Steps cycle through at 300ms each (~3.6s total), then show elapsed time for large projects
181
- await runStepSequence(analysisSteps, async () => {
182
- response = await apiClient.analyzeAudit(sanitized, framework);
183
- if (!response.success || !response.data) {
184
- throw new Error(response.error || 'Unknown error');
185
- }
186
- }, { successMessage: null, minStepDuration: 300 });
187
- }
188
- let data = response.data;
189
- const { analysisId, prefetchId, hasPendingPackages } = data;
190
- // Set project context for analytics
191
- analytics.setProjectContext({
192
- packageCount: data.totalPackages,
193
- framework: data.framework?.name,
194
- frameworkVersion: data.framework?.version,
195
- projectHash: analytics.hashProject(sanitized),
196
- });
197
- // Track: project_detected
198
- analytics.projectDetected({
199
- packageCount: data.totalPackages,
200
- framework: data.framework?.name,
201
- });
202
- // Audit mode: instant results from cache (no polling)
203
- // Background re-analysis happens server-side automatically
204
- // Polling will happen AFTER unlock when user is authenticated
205
- if (!options.json) {
206
- console.log(chalk.green(' ✓ Analysis complete'));
207
- }
208
- // Get cost from server response (database-driven tier pricing)
209
- const cost = data.cost;
210
- const tierName = data.tierName;
211
- const packageCount = data.totalPackages;
212
- // JSON output mode (no interactive prompts, clean output only)
213
- // Note: CI mode with --json is handled earlier in the function
214
- if (options.json) {
215
- const issueCount = data.summary.critical + data.summary.high + data.summary.medium + data.summary.low;
216
- const output = {
217
- mode: 'audit',
218
- analysisId,
219
- healthScore: data.healthScore,
220
- totalPackages: data.totalPackages,
221
- summary: data.summary,
222
- issueCount,
223
- conflicts: data.conflicts,
224
- framework: data.framework,
225
- cost,
226
- tierName,
227
- hasCriticalIssues: data.summary.critical > 0,
228
- hasHighIssues: data.summary.high > 0,
229
- requiresAttention: data.summary.critical > 0 || data.summary.high > 0,
230
- };
231
- console.log(JSON.stringify(output, null, 2));
108
+ outputJsonResult(auditResult);
232
109
  return;
233
110
  }
234
- // Pretty output - TEASER MODE (locked until payment)
235
- const issueCount = data.summary.critical + data.summary.high + data.summary.medium + data.summary.low;
236
- const healthInfo = getHealthStatus(data.healthScore);
237
- // Smooth reveal of analysis report
238
- console.log();
239
- await sleep(100);
240
- console.log(colors.whiteBold('📊 ANALYSIS REPORT'));
241
- await sleep(80);
242
- console.log(colors.gray('─'.repeat(50)));
243
- await sleep(120);
244
- console.log(`${colors.whiteBold('🏥 Health:')} ${renderHealthBar(data.healthScore)} ${healthInfo.color.bold(`${data.healthScore}/100`)} (${healthInfo.color(healthInfo.text)})`);
245
- // Track: analysis_completed
246
- analytics.analysisCompleted({
247
- healthScore: data.healthScore,
248
- issueCount,
249
- criticalCount: data.summary.critical,
250
- highCount: data.summary.high,
251
- });
252
- // Check if there are issues to fix FIRST
253
- if (issueCount === 0) {
254
- await sleep(100);
255
- console.log();
256
- printSuccess('No issues found! Your dependencies are healthy.');
257
- // Check if migration is available (not on latest version)
258
- if (data.framework?.name && data.framework?.version) {
259
- try {
260
- const currentMajor = parseInt(data.framework.version.split('.')[0], 10);
261
- const versionsResponse = await apiClient.getFrameworkVersions(data.framework.name, currentMajor);
262
- if (versionsResponse.success && versionsResponse.data) {
263
- // Find the recommended/latest version
264
- const recommended = versionsResponse.data.quickOptions?.find(opt => opt.isRecommended);
265
- const latestMajor = recommended ? parseInt(recommended.value, 10) : null;
266
- if (latestMajor && latestMajor > currentMajor) {
267
- const versionsBehind = latestMajor - currentMajor;
268
- console.log();
269
- console.log(colors.whiteBold('💡 WHY NOT 100%?'));
270
- console.log(colors.dim(` Your ${data.framework.name} version is `) + colors.warning(`${versionsBehind} major version${versionsBehind > 1 ? 's' : ''} behind`) + colors.dim(' the latest.'));
271
- console.log(colors.dim(` This affects your health score even without dependency conflicts.`));
272
- console.log();
273
- console.log(colors.whiteBold(' 👉 UPGRADE:'));
274
- console.log(` Run ${colors.brand('npx depfixer migrate')} to upgrade to ${data.framework.name} ${latestMajor}.`);
275
- }
276
- }
277
- }
278
- catch {
279
- // Silently ignore - migration suggestion is optional
280
- }
281
- }
282
- console.log();
111
+ // Check if there are issues to fix
112
+ if (auditResult.issueCount === 0) {
113
+ await showNoIssuesResult(ctx, auditResult);
283
114
  return;
284
115
  }
285
- await sleep(100);
286
- console.log(`${colors.whiteBold('⚠️ Issues:')} ${colors.dangerBold(`${issueCount}`)} Conflicts Found`);
287
- await sleep(150);
288
- console.log();
289
- // Show LIMITED preview - protect small conflict counts from bypass
290
- if (data.conflicts && data.conflicts.length > 0) {
291
- // Filter out "not installed" packages from audit display
292
- const installedConflicts = data.conflicts.filter((c) => c.currentVersion && c.currentVersion.toLowerCase() !== 'not installed');
293
- const missingPackagesCount = data.conflicts.length - installedConflicts.length;
294
- if (installedConflicts.length <= CLI_AUDIT_THRESHOLD) {
295
- // For small conflict counts, only show severity summary - no package names
296
- // This prevents users from easily bypassing with AI
297
- await sleep(80);
298
- const W = 50; // Inner width
299
- const row = (label, colorFn, count, desc) => {
300
- const issueWord = count > 1 ? 'issues' : 'issue';
301
- const content = ` ${label.padEnd(10)}${count} ${issueWord} ${desc}`;
302
- console.log(colors.gray('│') + colorFn(content.padEnd(W)) + colors.gray('│'));
303
- };
304
- console.log(colors.gray('┌' + '─'.repeat(W) + '┐'));
305
- console.log(colors.gray('│') + colors.whiteBold(' SEVERITY BREAKDOWN'.padEnd(W)) + colors.gray('│'));
306
- console.log(colors.gray('├' + '─'.repeat(W) + '┤'));
307
- if (data.summary.critical > 0)
308
- row('CRITICAL', colors.dangerBold, data.summary.critical, 'require attention');
309
- if (data.summary.high > 0)
310
- row('HIGH', colors.danger, data.summary.high, 'with compatibility problems');
311
- if (data.summary.medium > 0)
312
- row('MEDIUM', colors.warning, data.summary.medium, 'with version conflicts');
313
- if (data.summary.low > 0)
314
- row('LOW', colors.dim, data.summary.low, 'to review');
315
- console.log(colors.gray('└' + '─'.repeat(W) + '┘'));
316
- await sleep(80);
317
- // Show hint about missing packages if any
318
- if (missingPackagesCount > 0) {
319
- console.log(colors.dim(` + ${missingPackagesCount} missing peer ${missingPackagesCount > 1 ? 'dependencies' : 'dependency'} to install.`));
320
- }
321
- console.log(colors.dim(' Unlock to see details and recommended fixes.'));
322
- }
323
- else {
324
- // For larger counts, show first CLI_AUDIT_SAMPLE_SIZE installed packages only
325
- const tableLines = createTeaserTable(installedConflicts.slice(0, CLI_AUDIT_SAMPLE_SIZE)).split('\n');
326
- for (const line of tableLines) {
327
- console.log(line);
328
- await sleep(40);
329
- }
330
- await sleep(80);
331
- const hiddenCount = installedConflicts.length - CLI_AUDIT_SAMPLE_SIZE;
332
- if (hiddenCount > 0) {
333
- console.log(colors.dim(` + ${hiddenCount} other conflicts hidden.`));
334
- }
335
- // Show hint about missing packages if any
336
- if (missingPackagesCount > 0) {
337
- console.log(colors.dim(` + ${missingPackagesCount} missing peer ${missingPackagesCount > 1 ? 'dependencies' : 'dependency'} to install.`));
338
- }
339
- }
340
- }
341
- // Diagnosis section with smooth reveal
342
- await sleep(150);
343
- printDiagnosis(issueCount);
116
+ // Show audit results (teaser mode)
117
+ await displayAuditResults(auditResult);
344
118
  // Save session for potential later fix
345
- await sessionManager.saveSession({
346
- analysisId,
347
- intent: 'ANALYZE',
348
- originalFileHash: packageJsonHash,
349
- cost,
350
- status: 'UNPAID',
351
- projectName: sanitized.name || 'unnamed',
352
- packageCount,
353
- tierName,
354
- });
355
- // Check if user is already logged in
356
- const authManager = new AuthManager();
357
- const isAlreadyLoggedIn = await authManager.isAuthenticated();
358
- const paymentFlow = new PaymentFlowService();
359
- // Track if user has active pass (set in both branches)
360
- let userHasActivePass = false;
361
- if (isAlreadyLoggedIn) {
362
- // User is logged in - show account details and credit check
363
- const balanceInfo = await paymentFlow.getBalanceInfo();
364
- userHasActivePass = balanceInfo?.hasActivePass || false;
365
- // Show user details
366
- printUserDetails({
367
- name: balanceInfo?.name,
368
- email: balanceInfo?.email,
369
- credits: balanceInfo?.credits || 0,
370
- hasActivePass: userHasActivePass,
371
- showHeader: false,
372
- });
373
- // Show credit check (needed vs available)
374
- printCreditCheck({
375
- needed: cost,
376
- available: balanceInfo?.credits || 0,
377
- hasActivePass: userHasActivePass,
378
- });
379
- // Cost box with prompt
380
- printCostBox({
381
- cost,
382
- tierName,
383
- prompt: userHasActivePass ? 'Continue? (Enter/Esc)' : `Deduct ${cost} credit${cost > 1 ? 's' : ''} to unlock? (Enter/Esc)`,
384
- hasActivePass: userHasActivePass,
385
- });
386
- // Track: unlock_prompt_shown
387
- analytics.unlockPromptShown({
388
- creditsNeeded: cost,
389
- creditsAvailable: balanceInfo?.credits || 0,
390
- tier: tierName,
391
- hasActivePass: userHasActivePass,
392
- });
393
- // Single prompt with cost info
394
- const confirmUnlock = options.yes || await promptYesNo('' // Prompt already shown in cost box
395
- );
396
- if (!confirmUnlock) {
397
- // Track: unlock_rejected
398
- analytics.unlockRejected({ reason: 'user_cancelled' });
399
- console.log();
400
- printInfo('Solution saved. Run `npx depfixer fix` anytime to resume.');
401
- return;
402
- }
403
- // Track: unlock_accepted
404
- analytics.unlockAccepted({ creditsDeducted: userHasActivePass ? 0 : cost });
405
- // Check balance is sufficient
406
- if (!userHasActivePass && (balanceInfo?.credits || 0) < cost) {
407
- // Track: unlock_failed (insufficient credits, but user will try to top up)
408
- analytics.unlockFailed({
409
- reason: 'insufficient_credits',
410
- needed: cost,
411
- available: balanceInfo?.credits || 0,
412
- });
413
- const hasBalance = await paymentFlow.ensureSufficientBalance(cost);
414
- if (!hasBalance) {
415
- console.log();
416
- printInfo('Run `npx depfixer fix` when ready to continue.');
417
- return;
418
- }
419
- // After top-up, show cost box again and confirm
420
- printCostBox({
421
- cost,
422
- tierName,
423
- prompt: `Deduct ${cost} credit${cost > 1 ? 's' : ''} to unlock? (Enter/Esc)`,
424
- });
425
- const confirmAfterTopUp = options.yes || await promptYesNo('');
426
- if (!confirmAfterTopUp) {
427
- console.log();
428
- printInfo('Solution saved. Run `npx depfixer fix` anytime to resume.');
429
- return;
430
- }
431
- }
432
- }
433
- else {
434
- // User not logged in - show cost box and login prompt
435
- printCostBox({
436
- cost,
437
- tierName,
438
- prompt: '',
439
- });
440
- // Track: unlock_prompt_shown (anonymous user)
441
- analytics.unlockPromptShown({
442
- creditsNeeded: cost,
443
- creditsAvailable: 0,
444
- tier: tierName,
445
- isAnonymous: true,
446
- });
447
- // Track: auth_required
448
- analytics.authRequired({ creditsNeeded: cost });
449
- console.log();
450
- console.log(colors.warning('⚠️ Login required to unlock'));
451
- console.log(colors.gray('[?] Continue to login? (Enter/Esc)'));
452
- const wantsToUnlock = options.yes || await promptYesNo('');
453
- if (!wantsToUnlock) {
454
- // Track: auth_abandoned
455
- analytics.authAbandoned({ reason: 'user_cancelled' });
456
- console.log();
457
- printInfo('Solution saved. Run `npx depfixer fix` anytime to resume.');
458
- return;
459
- }
460
- // Run full auth + balance flow (shows user details after login)
461
- const paymentResult = await paymentFlow.ensureReadyToPay(cost);
462
- if (!paymentResult.ready) {
463
- // Track: auth_abandoned (failed to complete auth/balance)
464
- analytics.authAbandoned({ reason: 'auth_flow_incomplete' });
465
- console.log();
466
- printInfo('Run `npx depfixer fix` when ready to continue.');
467
- return;
468
- }
469
- userHasActivePass = paymentResult.hasActivePass || false;
470
- // Confirm after login (user details already shown by ensureReadyToPay)
471
- printCostBox({
472
- cost,
473
- tierName,
474
- prompt: userHasActivePass ? 'Continue? (Enter/Esc)' : `Confirm: Deduct ${cost} credit${cost > 1 ? 's' : ''}? (Enter/Esc)`,
475
- hasActivePass: userHasActivePass,
476
- });
477
- const confirmUnlock = options.yes || await promptYesNo('');
478
- if (!confirmUnlock) {
479
- // Track: unlock_rejected (after login)
480
- analytics.unlockRejected({ reason: 'user_cancelled_after_login' });
481
- console.log();
482
- printInfo('Solution saved. Run `npx depfixer fix` anytime to resume.');
483
- return;
484
- }
485
- // Track: unlock_accepted (after login flow)
486
- analytics.unlockAccepted({ creditsDeducted: userHasActivePass ? 0 : cost });
487
- }
488
- // Step 5: Deduct credits and get solution
489
- const fixResult = await paymentFlow.deductCredits(analysisId, userHasActivePass);
490
- if (!fixResult.success || !fixResult.solution) {
491
- throw new Error(fixResult.error || 'Unknown error');
119
+ await saveSession(ctx, auditResult);
120
+ // Handle payment flow
121
+ const paymentResult = await handlePaymentFlow(ctx, auditResult);
122
+ if (!paymentResult.success) {
123
+ return;
492
124
  }
493
- // Update session status
494
- await sessionManager.updateStatus('PAID');
495
- // If there were pending packages, poll until re-analysis completes
496
- // User is now authenticated, so polling works
497
- if (hasPendingPackages && prefetchId) {
498
- const spinner = createSpinner('Finalizing complete analysis...').start();
499
- let pollCount = 0;
500
- const maxPolls = 120; // 2 minutes max
501
- while (pollCount < maxPolls) {
502
- try {
503
- const status = await apiClient.pollPrefetchStatus(prefetchId);
504
- if (status.isComplete && status.reanalysisStatus === 'completed') {
505
- // Re-fetch complete data from DB
506
- const updatedResponse = await apiClient.getAnalysisById(analysisId);
507
- if (updatedResponse.success && updatedResponse.data) {
508
- const updated = updatedResponse.data;
509
- const analysisResult = updated.analysisResult || {};
510
- // Update fixResult with fresh data from re-analysis
511
- fixResult.conflicts = analysisResult.conflicts || fixResult.conflicts;
512
- fixResult.missingDependencies = analysisResult.missingDependencies || fixResult.missingDependencies;
513
- fixResult.deprecations = analysisResult.deprecations || fixResult.deprecations;
514
- fixResult.healthScore = analysisResult.healthScore || fixResult.healthScore;
515
- if (analysisResult.conflicts) {
516
- fixResult.summary = calculateSummary(analysisResult.conflicts);
517
- }
518
- }
519
- break;
520
- }
521
- // Update spinner with progress
522
- if (status.percentage !== undefined) {
523
- const percent = Math.round(status.percentage);
524
- spinner.text = `Finalizing complete analysis... ${percent}%`;
525
- }
526
- await sleep(1000);
527
- pollCount++;
528
- }
529
- catch (err) {
530
- // Network error - retry
531
- await sleep(2000);
532
- pollCount++;
533
- }
534
- }
535
- spinner.succeed('Complete analysis ready');
125
+ // Poll for prefetch completion if needed
126
+ const finalData = await pollPrefetchIfNeeded(ctx, auditResult, paymentResult);
127
+ // Show full analysis with solutions
128
+ await displayFullAnalysis(finalData, paymentResult.solution);
129
+ // Ask to apply fix and handle result
130
+ await handleFixApplication(ctx, finalData, paymentResult.solution);
131
+ }
132
+ catch (error) {
133
+ await handleError(error, options);
134
+ }
135
+ }
136
+ // ============================================================================
137
+ // INITIALIZATION
138
+ // ============================================================================
139
+ /**
140
+ * Initialize context with all required services and data
141
+ */
142
+ async function initializeContext(projectDir, options) {
143
+ const packageJsonService = new PackageJsonService();
144
+ const apiClient = new ApiClient();
145
+ const sessionManager = new SessionManager(projectDir);
146
+ // Read and sanitize package.json
147
+ const { content: packageJsonContent, parsed } = await packageJsonService.read(projectDir);
148
+ const sanitized = packageJsonService.sanitize(parsed);
149
+ // Calculate package.json hash for integrity check
150
+ const packageJsonHash = sessionManager.calculateHash(packageJsonContent);
151
+ // Detect framework using server API (more accurate than local detection)
152
+ let framework;
153
+ let frameworkInfo = '';
154
+ let creditInfo;
155
+ try {
156
+ const detectResponse = await apiClient.detectFramework(sanitized);
157
+ if (detectResponse.success && detectResponse.data) {
158
+ // Use detected framework name (lowercase for consistency)
159
+ framework = detectResponse.data.name;
160
+ frameworkInfo = framework ? `${framework.charAt(0).toUpperCase() + framework.slice(1)}` : '';
536
161
  }
537
- // Update data with full unfiltered results from DB
538
- if (fixResult.conflicts) {
539
- data = {
540
- ...data,
541
- conflicts: fixResult.conflicts,
542
- missingDependencies: fixResult.missingDependencies,
543
- deprecations: fixResult.deprecations,
544
- healthScore: fixResult.healthScore,
545
- summary: fixResult.summary,
162
+ // Credit info is at the top level of the response
163
+ if (detectResponse.packageCount !== undefined && detectResponse.creditInfo) {
164
+ creditInfo = {
165
+ packageCount: detectResponse.packageCount,
166
+ requiredCredits: detectResponse.creditInfo.requiredCredits,
167
+ tierName: detectResponse.creditInfo.tierName,
546
168
  };
547
169
  }
548
- // Extract solution for type safety in closures
549
- const solution = fixResult.solution;
550
- // Step 6: Show FULL analysis with solutions (recommended versions) - smooth reveal
551
- console.log();
552
- await sleep(100);
553
- console.log(chalk.bold.green('🔓 FULL ANALYSIS'));
554
- await sleep(80);
555
- console.log(chalk.dim('─'.repeat(50)));
556
- await sleep(120);
557
- console.log();
558
- // Separate package conflicts from engine conflicts
559
- const packageConflicts = data.conflicts.filter((c) => c.package !== 'Node.js' && c.package !== 'npm' && c.category !== 'engine');
560
- const engineConflicts = data.conflicts.filter((c) => c.package === 'Node.js' || c.package === 'npm' || c.category === 'engine');
561
- // Show full table with recommended versions - row by row (only if there are package conflicts)
562
- if (packageConflicts.length > 0) {
563
- const fullTableLines = createFullSolutionTable(packageConflicts, solution).split('\n');
564
- for (const line of fullTableLines) {
565
- console.log(line);
566
- await sleep(35);
567
- }
568
- console.log();
569
- }
570
- // 1. Show engine requirements first (from solution.engines)
571
- if (solution.engines && Object.keys(solution.engines).length > 0) {
572
- console.log(colors.whiteBold('⚙️ Engine Requirements:'));
573
- if (solution.engines.node) {
574
- console.log(` ${colors.dim('•')} Node.js: ${colors.brand(solution.engines.node)}`);
575
- }
576
- if (solution.engines.npm) {
577
- console.log(` ${colors.dim('•')} npm: ${colors.brand(solution.engines.npm)}`);
578
- }
579
- console.log();
580
- }
581
- else if (engineConflicts.length > 0) {
582
- // Fallback to conflict data if solution.engines is empty
583
- console.log(colors.whiteBold('⚙️ Engine Requirements:'));
584
- for (const ec of engineConflicts) {
585
- const engine = ec.package || 'Unknown';
586
- let required = ec.recommendedVersion || ec.requiredVersion;
587
- if (!required && ec.engineDetails?.requiredVersion) {
588
- required = ec.engineDetails.requiredVersion.replace(/^>=/, '');
589
- }
590
- if (required) {
591
- console.log(` ${colors.dim('•')} ${engine}: ${colors.brand('>=' + required)}`);
592
- }
593
- }
594
- console.log();
595
- }
596
- // 2. Show packages to add (not installed)
597
- const packagesToAdd = packageConflicts.filter((c) => !c.currentVersion || c.currentVersion.toLowerCase() === 'not installed');
598
- if (packagesToAdd.length > 0) {
599
- console.log(colors.whiteBold('📦 Packages to Add:'));
600
- for (const pkg of packagesToAdd) {
601
- await sleep(60);
602
- // Build message from requiredBy if available, otherwise fallback
603
- let message;
604
- if (pkg.requiredBy && Array.isArray(pkg.requiredBy) && pkg.requiredBy.length > 0) {
605
- const requiredRange = pkg.requiredRange || 'required version';
606
- message = `Required as peer dependency by ${pkg.requiredBy.join(', ')} (${requiredRange})`;
607
- }
608
- else if (pkg.isPeerDependency && pkg.requiredRange) {
609
- message = `Missing peer dependency (${pkg.requiredRange})`;
610
- }
611
- else {
612
- message = pkg.description || 'Required dependency';
613
- }
614
- console.log(` ${colors.brand('+')} ${pkg.package}`);
615
- console.log(` ${colors.dim(message)}`);
170
+ }
171
+ catch {
172
+ // If API fails, continue without framework detection
173
+ // The analysis will still work, just without framework-specific info
174
+ }
175
+ return {
176
+ projectDir,
177
+ options,
178
+ packageJsonService,
179
+ apiClient,
180
+ sessionManager,
181
+ packageJsonContent,
182
+ parsed,
183
+ sanitized,
184
+ framework,
185
+ frameworkInfo,
186
+ packageJsonHash,
187
+ creditInfo,
188
+ };
189
+ }
190
+ // ============================================================================
191
+ // CI MODE
192
+ // ============================================================================
193
+ /**
194
+ * Handle CI mode analysis (non-interactive, for pipelines)
195
+ */
196
+ async function handleCiMode(ctx) {
197
+ const { options, apiClient, sanitized, framework } = ctx;
198
+ const authManager = new AuthManager();
199
+ const authHeader = await authManager.getAuthHeader();
200
+ if (!authHeader) {
201
+ outputCiAuthError(options);
202
+ process.exit(2);
203
+ }
204
+ try {
205
+ const ciResponse = await apiClient.analyzeForCi(sanitized, framework);
206
+ if (!ciResponse.success || !ciResponse.data) {
207
+ if (options.json) {
208
+ console.log(JSON.stringify({ success: false, error: ciResponse.error || 'CI analysis failed' }, null, 2));
616
209
  }
617
- console.log();
618
- }
619
- // 3. Show removals
620
- if (solution.removals && solution.removals.length > 0) {
621
- await sleep(100);
622
- console.log(colors.whiteBold('🗑 Packages to Remove:'));
623
- const removalLines = createMigrationTable(solution.removals.map(r => ({
624
- package: r.package,
625
- currentVersion: '*',
626
- targetVersion: 'REMOVE',
627
- changeType: 'deprec',
628
- isRemoval: true,
629
- }))).split('\n');
630
- for (const line of removalLines) {
631
- console.log(line);
632
- await sleep(30);
210
+ else {
211
+ console.log(chalk.red(` ${ciResponse.error || 'CI analysis failed'}`));
633
212
  }
634
- console.log();
213
+ process.exit(2);
635
214
  }
636
- // If no package conflicts and no removals, show a message
637
- if (packageConflicts.length === 0 && (!solution.removals || solution.removals.length === 0)) {
638
- console.log(colors.dim(' No package version changes needed.'));
639
- console.log();
215
+ const ciData = ciResponse.data;
216
+ const issueCount = ciData.summary.critical + ciData.summary.high + ciData.summary.medium + ciData.summary.low;
217
+ if (options.json) {
218
+ outputCiJsonResult(ciData, issueCount);
640
219
  }
641
- // Track: results_shown (full solution visible)
642
- analytics.resultsShown({
643
- conflictCount: data.conflicts.length,
644
- removalsCount: solution.removals?.length || 0,
645
- });
646
- // Step 7: Ask to apply fix
647
- // Explain what will be changed
648
- await sleep(100);
649
- console.log(colors.gray('────────────────────────────────────────'));
650
- await sleep(50);
651
- console.log(colors.gray(' 📝 Only ') + colors.white('package.json') + colors.gray(' will be modified'));
652
- await sleep(50);
653
- console.log(colors.gray(' 💾 A backup (') + colors.white('package.json.bak') + colors.gray(') will be created'));
654
- await sleep(50);
655
- console.log(colors.gray('────────────────────────────────────────'));
656
- await sleep(100);
657
- console.log();
658
- // Track: fix_prompt_shown
659
- analytics.fixPromptShown({
660
- updatesCount: Object.keys(solution.dependencies || {}).length + Object.keys(solution.devDependencies || {}).length,
661
- removalsCount: solution.removals?.length || 0,
662
- });
663
- const shouldApply = options.yes || await promptYesNo('Apply fix to package.json?');
664
- if (!shouldApply) {
665
- // Track: fix_deferred
666
- analytics.fixDeferred({ reason: 'user_declined' });
667
- console.log();
668
- printInfo('Solution unlocked but not applied. Run `npx depfixer fix` to apply later.');
669
- return;
220
+ else {
221
+ outputCiHumanResult(ciData, issueCount);
670
222
  }
671
- // Track: fix_accepted
672
- analytics.fixAccepted();
673
- // Step 8: Apply the solution surgically (preserves formatting)
674
- console.log();
675
- console.log(chalk.bold('🔧 Applying fixes...'));
676
- console.log();
677
- // Get changes list for surgical update
678
- const changes = packageJsonService.getChanges(parsed, solution);
679
- // Map removals to the format expected by applySurgicalFixes
680
- const removals = (solution.removals || []).map(r => ({
681
- package: r.package,
682
- reason: r.reason || 'Deprecated',
683
- type: r.type,
684
- }));
685
- // Fix steps
686
- const fixSteps = [
687
- 'Reading package.json...',
688
- 'Calculating safe versions...',
689
- `Updating dependencies...`,
690
- `Updating devDependencies...`,
691
- 'Validating changes...',
692
- 'Writing package.json...',
693
- ];
694
- let fixResult2;
695
- await runStepSequence(fixSteps, async () => {
696
- fixResult2 = await packageJsonService.applySurgicalFixes(projectDir, changes, removals, solution.engines);
697
- }, { successMessage: 'Fix complete', minStepDuration: 100 });
698
- const { backupPath, applied, removed, enginesUpdated } = fixResult2;
699
- // Show success
700
- printSuccessBox({
701
- updated: applied,
702
- removed,
703
- backupPath,
704
- enginesUpdated,
705
- });
706
- // Track: fix_applied
707
- analytics.fixApplied({
708
- updatedCount: applied,
709
- removedCount: removed,
710
- enginesUpdated,
711
- });
712
- // Track: session_ended (successful completion)
713
- await analytics.sessionEnded({
714
- outcome: 'fix_applied',
715
- healthScore: data.healthScore,
716
- });
717
- // Note: No exit(1) here - fixes were applied successfully
718
- // CI mode already exits early before interactive prompts
719
223
  }
720
- catch (error) {
721
- // Track: session_ended (error)
722
- await analytics.sessionEnded({
723
- outcome: 'error',
724
- error: error.message,
725
- });
224
+ catch (ciError) {
726
225
  if (options.json) {
727
- console.log(JSON.stringify({ error: error.message }, null, 2));
226
+ console.log(JSON.stringify({ success: false, error: ciError.message }, null, 2));
728
227
  }
729
228
  else {
730
- printError(error.message);
229
+ console.log(chalk.red(` CI analysis failed: ${ciError.message}`));
731
230
  }
732
- process.exit(options.ci ? 2 : 1);
231
+ process.exit(2);
733
232
  }
734
233
  }
735
234
  /**
736
- * Check if auto-confirm is enabled (dev mode only)
235
+ * Output CI authentication error
737
236
  */
738
- function isAutoConfirmEnabled() {
739
- const isDevMode = process.env.NODE_ENV === 'development' ||
740
- (process.env.DEPFIXER_API_URL?.includes('localhost') ?? false);
741
- return isDevMode && process.env.DEPFIXER_AUTO_CONFIRM === 'true';
237
+ function outputCiAuthError(options) {
238
+ if (options.json) {
239
+ console.log(JSON.stringify({
240
+ success: false,
241
+ error: 'Authentication required for CI mode',
242
+ help: 'Set DEPFIXER_TOKEN environment variable',
243
+ docs: 'https://depfixer.com/docs/ci-setup',
244
+ }, null, 2));
245
+ }
246
+ else {
247
+ console.log();
248
+ console.log(chalk.red(' Authentication required for CI mode'));
249
+ console.log();
250
+ console.log(chalk.bold(' Setup:'));
251
+ console.log(' 1. Get API token: https://app.depfixer.com/dashboard/api-keys');
252
+ console.log(' 2. Add to GitHub Secrets as DEPFIXER_TOKEN');
253
+ console.log(' 3. Use in workflow:');
254
+ console.log();
255
+ console.log(chalk.dim(' - run: npx depfixer --ci'));
256
+ console.log(chalk.dim(' env:'));
257
+ console.log(chalk.dim(' DEPFIXER_TOKEN: ${{ secrets.DEPFIXER_TOKEN }}'));
258
+ console.log();
259
+ }
742
260
  }
743
261
  /**
744
- * Simple Enter/Esc prompt (Enter = Yes, Esc = No)
262
+ * Output CI result as JSON
745
263
  */
746
- async function promptYesNo(question) {
747
- // Auto-confirm in dev mode when env var is set
748
- if (isAutoConfirmEnabled()) {
749
- if (question) {
750
- console.log(`${question} ${colors.dim('(Enter/Esc)')} ${colors.success('Yes')} ${colors.dim('[auto]')}`);
751
- }
752
- else {
753
- console.log(`${colors.success('Yes')} ${colors.dim('[auto]')}`);
754
- }
755
- return true;
264
+ function outputCiJsonResult(ciData, issueCount) {
265
+ console.log(JSON.stringify({
266
+ success: true,
267
+ mode: 'ci',
268
+ analysisId: ciData.analysisId,
269
+ healthScore: ciData.healthScore,
270
+ totalPackages: ciData.totalPackages,
271
+ summary: ciData.summary,
272
+ issueCount,
273
+ conflicts: ciData.conflicts,
274
+ framework: ciData.framework,
275
+ requiresAttention: ciData.requiresAttention,
276
+ }, null, 2));
277
+ process.exit(ciData.requiresAttention ? 1 : 0);
278
+ }
279
+ /**
280
+ * Output CI result for human reading
281
+ */
282
+ function outputCiHumanResult(ciData, issueCount) {
283
+ console.log();
284
+ console.log(chalk.bold(' CI Mode - Dependency Analysis'));
285
+ console.log(chalk.dim(' ' + '─'.repeat(40)));
286
+ console.log(` Health Score: ${ciData.healthScore}/100`);
287
+ console.log(` Total Packages: ${ciData.totalPackages}`);
288
+ console.log(` Issues Found: ${issueCount}`);
289
+ if (ciData.summary.critical > 0)
290
+ console.log(chalk.red(` Critical: ${ciData.summary.critical}`));
291
+ if (ciData.summary.high > 0)
292
+ console.log(chalk.yellow(` High: ${ciData.summary.high}`));
293
+ if (ciData.summary.medium > 0)
294
+ console.log(chalk.blue(` Medium: ${ciData.summary.medium}`));
295
+ if (ciData.summary.low > 0)
296
+ console.log(chalk.dim(` Low: ${ciData.summary.low}`));
297
+ console.log();
298
+ if (ciData.requiresAttention) {
299
+ console.log(chalk.red(' Pipeline should fail - critical/high issues detected'));
300
+ process.exit(1);
756
301
  }
757
- return new Promise((resolve) => {
758
- if (question) {
759
- process.stdout.write(`${question} ${colors.dim('(Enter/Esc)')} `);
760
- }
761
- if (process.stdin.isTTY) {
762
- process.stdin.setRawMode(true);
302
+ else if (issueCount > 0) {
303
+ console.log(chalk.yellow(' Minor issues detected (medium/low severity)'));
304
+ process.exit(0);
305
+ }
306
+ else {
307
+ console.log(chalk.green(' No dependency issues found'));
308
+ process.exit(0);
309
+ }
310
+ }
311
+ // ============================================================================
312
+ // AUDIT ANALYSIS
313
+ // ============================================================================
314
+ /**
315
+ * Run audit analysis with step sequence animation
316
+ */
317
+ async function runAuditAnalysis(ctx) {
318
+ const { options, apiClient, sanitized, framework, frameworkInfo } = ctx;
319
+ const analysisSteps = [
320
+ 'Parsing dependency tree...',
321
+ `Detecting framework...${frameworkInfo ? ` ${frameworkInfo}` : ''}`,
322
+ 'Loading compatibility matrix...',
323
+ 'Scanning package versions...',
324
+ 'Resolving peer dependencies...',
325
+ 'Checking version constraints...',
326
+ 'Analyzing transitive dependencies...',
327
+ 'Detecting breaking changes...',
328
+ 'Evaluating deprecation status...',
329
+ 'Calculating version intersections...',
330
+ 'Checking cross-package rules...',
331
+ 'Generating recommendations...',
332
+ ];
333
+ let response;
334
+ if (options.json) {
335
+ // Silent mode for JSON output
336
+ response = await apiClient.analyzeAudit(sanitized, framework);
337
+ if (!response.success || !response.data) {
338
+ throw new Error(response.error || 'Unknown error');
763
339
  }
764
- process.stdin.resume();
765
- const onKeyPress = (key) => {
766
- const char = key.toString();
767
- // Enter key
768
- if (char === '\r' || char === '\n') {
769
- cleanup();
770
- console.log(chalk.green('Yes'));
771
- resolve(true);
772
- }
773
- // Escape key
774
- else if (char === '\x1b') {
775
- cleanup();
776
- console.log(chalk.red('No'));
777
- resolve(false);
778
- }
779
- // 'y' or 'Y'
780
- else if (char.toLowerCase() === 'y') {
781
- cleanup();
782
- console.log(chalk.green('Yes'));
783
- resolve(true);
784
- }
785
- // 'n' or 'N'
786
- else if (char.toLowerCase() === 'n') {
787
- cleanup();
788
- console.log(chalk.red('No'));
789
- resolve(false);
790
- }
791
- // Ctrl+C
792
- else if (char === '\x03') {
793
- cleanup();
794
- process.exit(0);
795
- }
796
- };
797
- const cleanup = () => {
798
- process.stdin.removeListener('data', onKeyPress);
799
- if (process.stdin.isTTY) {
800
- process.stdin.setRawMode(false);
340
+ }
341
+ else {
342
+ await runStepSequence(analysisSteps, async () => {
343
+ response = await apiClient.analyzeAudit(sanitized, framework);
344
+ if (!response.success || !response.data) {
345
+ throw new Error(response.error || 'Unknown error');
801
346
  }
802
- process.stdin.pause();
803
- };
804
- process.stdin.on('data', onKeyPress);
347
+ }, { successMessage: null, minStepDuration: 300 });
348
+ }
349
+ const data = response.data;
350
+ const { analysisId, prefetchId, hasPendingPackages } = data;
351
+ // Set project context for analytics
352
+ analytics.setProjectContext({
353
+ packageCount: data.totalPackages,
354
+ framework: data.framework?.name,
355
+ frameworkVersion: data.framework?.version,
356
+ projectHash: analytics.hashProject(sanitized),
357
+ });
358
+ // Track: project_detected
359
+ analytics.projectDetected({
360
+ packageCount: data.totalPackages,
361
+ framework: data.framework?.name,
805
362
  });
363
+ if (!options.json) {
364
+ console.log(chalk.green(' ✓ Analysis complete'));
365
+ }
366
+ const issueCount = data.summary.critical + data.summary.high + data.summary.medium + data.summary.low;
367
+ // Track: analysis_completed
368
+ analytics.analysisCompleted({
369
+ healthScore: data.healthScore,
370
+ issueCount,
371
+ criticalCount: data.summary.critical,
372
+ highCount: data.summary.high,
373
+ });
374
+ return {
375
+ response,
376
+ data,
377
+ analysisId,
378
+ prefetchId,
379
+ hasPendingPackages,
380
+ cost: data.cost,
381
+ tierName: data.tierName,
382
+ packageCount: data.totalPackages,
383
+ issueCount,
384
+ };
806
385
  }
807
386
  /**
808
- * Apply fixes to package.json
387
+ * Output JSON result for audit mode
388
+ */
389
+ function outputJsonResult(auditResult) {
390
+ const { data, analysisId, cost, tierName, issueCount } = auditResult;
391
+ const output = {
392
+ mode: 'audit',
393
+ analysisId,
394
+ healthScore: data.healthScore,
395
+ totalPackages: data.totalPackages,
396
+ summary: data.summary,
397
+ issueCount,
398
+ conflicts: data.conflicts,
399
+ framework: data.framework,
400
+ cost,
401
+ tierName,
402
+ hasCriticalIssues: data.summary.critical > 0,
403
+ hasHighIssues: data.summary.high > 0,
404
+ requiresAttention: data.summary.critical > 0 || data.summary.high > 0,
405
+ };
406
+ console.log(JSON.stringify(output, null, 2));
407
+ }
408
+ // ============================================================================
409
+ // DISPLAY FUNCTIONS
410
+ // ============================================================================
411
+ /**
412
+ * Show result when no issues are found
413
+ */
414
+ async function showNoIssuesResult(ctx, auditResult) {
415
+ const { apiClient } = ctx;
416
+ const { data } = auditResult;
417
+ const healthInfo = getHealthStatus(data.healthScore);
418
+ console.log();
419
+ await sleep(100);
420
+ console.log(colors.whiteBold('📊 ANALYSIS REPORT'));
421
+ await sleep(80);
422
+ console.log(colors.gray('─'.repeat(50)));
423
+ await sleep(120);
424
+ console.log(`${colors.whiteBold('🏥 Health:')} ${renderHealthBar(data.healthScore)} ${healthInfo.color.bold(`${data.healthScore}/100`)} (${healthInfo.color(healthInfo.text)})`);
425
+ await sleep(100);
426
+ console.log();
427
+ printSuccess('No issues found! Your dependencies are healthy.');
428
+ // Check if migration is available (not on latest version)
429
+ if (data.framework?.name && data.framework?.version) {
430
+ await suggestMigrationIfAvailable(apiClient, data);
431
+ }
432
+ console.log();
433
+ }
434
+ /**
435
+ * Suggest migration if user is not on latest version
809
436
  */
810
- function applyFixes(original, solution) {
811
- const updated = JSON.parse(JSON.stringify(original)); // Deep clone
812
- // Update dependencies
813
- if (solution.dependencies) {
814
- for (const [pkg, version] of Object.entries(solution.dependencies)) {
815
- if (updated.dependencies?.[pkg]) {
816
- updated.dependencies[pkg] = version;
437
+ async function suggestMigrationIfAvailable(apiClient, data) {
438
+ try {
439
+ const currentMajor = parseInt(data.framework.version.split('.')[0], 10);
440
+ const versionsResponse = await apiClient.getFrameworkVersions(data.framework.name, currentMajor);
441
+ if (versionsResponse.success && versionsResponse.data) {
442
+ const recommended = versionsResponse.data.quickOptions?.find(opt => opt.isRecommended);
443
+ const latestMajor = recommended ? parseInt(recommended.value, 10) : null;
444
+ if (latestMajor && latestMajor > currentMajor) {
445
+ const versionsBehind = latestMajor - currentMajor;
446
+ console.log();
447
+ console.log(colors.whiteBold('💡 WHY NOT 100%?'));
448
+ console.log(colors.dim(` Your ${data.framework.name} version is `) + colors.warning(`${versionsBehind} major version${versionsBehind > 1 ? 's' : ''} behind`) + colors.dim(' the latest.'));
449
+ console.log(colors.dim(` This affects your health score even without dependency conflicts.`));
450
+ console.log();
451
+ console.log(colors.whiteBold(' 👉 UPGRADE:'));
452
+ console.log(` Run ${colors.brand('npx depfixer migrate')} to upgrade to ${data.framework.name} ${latestMajor}.`);
817
453
  }
818
454
  }
819
455
  }
820
- // Update devDependencies
821
- if (solution.devDependencies) {
822
- for (const [pkg, version] of Object.entries(solution.devDependencies)) {
823
- if (updated.devDependencies?.[pkg]) {
824
- updated.devDependencies[pkg] = version;
825
- }
456
+ catch {
457
+ // Silently ignore - migration suggestion is optional
458
+ }
459
+ }
460
+ /**
461
+ * Display audit results in teaser mode (locked)
462
+ */
463
+ async function displayAuditResults(auditResult) {
464
+ const { data, issueCount } = auditResult;
465
+ const healthInfo = getHealthStatus(data.healthScore);
466
+ console.log();
467
+ await sleep(100);
468
+ console.log(colors.whiteBold('📊 ANALYSIS REPORT'));
469
+ await sleep(80);
470
+ console.log(colors.gray('─'.repeat(50)));
471
+ await sleep(120);
472
+ console.log(`${colors.whiteBold('🏥 Health:')} ${renderHealthBar(data.healthScore)} ${healthInfo.color.bold(`${data.healthScore}/100`)} (${healthInfo.color(healthInfo.text)})`);
473
+ await sleep(100);
474
+ console.log(`${colors.whiteBold('⚠️ Issues:')} ${colors.dangerBold(`${issueCount}`)} Conflicts Found`);
475
+ await sleep(150);
476
+ console.log();
477
+ // Show LIMITED preview - protect small conflict counts from bypass
478
+ await displayConflictPreview(data);
479
+ // Diagnosis section with smooth reveal
480
+ await sleep(150);
481
+ printDiagnosis(issueCount);
482
+ }
483
+ /**
484
+ * Display conflict preview (teaser table or summary)
485
+ */
486
+ async function displayConflictPreview(data) {
487
+ if (!data.conflicts || data.conflicts.length === 0)
488
+ return;
489
+ // Filter out "not installed" packages from audit display
490
+ const installedConflicts = data.conflicts.filter((c) => c.currentVersion && c.currentVersion.toLowerCase() !== 'not installed');
491
+ const missingPackagesCount = data.conflicts.length - installedConflicts.length;
492
+ if (installedConflicts.length <= CLI_AUDIT_THRESHOLD) {
493
+ // For small conflict counts, only show severity summary - no package names
494
+ await displaySeveritySummary(data, missingPackagesCount);
495
+ }
496
+ else {
497
+ // For larger counts, show first CLI_AUDIT_SAMPLE_SIZE installed packages only
498
+ await displayTeaserTable(installedConflicts, missingPackagesCount);
499
+ }
500
+ }
501
+ /**
502
+ * Display severity summary box (for small conflict counts)
503
+ */
504
+ async function displaySeveritySummary(data, missingPackagesCount) {
505
+ await sleep(80);
506
+ const W = 50; // Inner width
507
+ const row = (label, colorFn, count, desc) => {
508
+ const issueWord = count > 1 ? 'issues' : 'issue';
509
+ const content = ` ${label.padEnd(10)}${count} ${issueWord} ${desc}`;
510
+ console.log(colors.gray('│') + colorFn(content.padEnd(W)) + colors.gray('│'));
511
+ };
512
+ console.log(colors.gray('┌' + '─'.repeat(W) + '┐'));
513
+ console.log(colors.gray('│') + colors.whiteBold(' SEVERITY BREAKDOWN'.padEnd(W)) + colors.gray('│'));
514
+ console.log(colors.gray('├' + '─'.repeat(W) + '┤'));
515
+ if (data.summary.critical > 0)
516
+ row('CRITICAL', colors.dangerBold, data.summary.critical, 'require attention');
517
+ if (data.summary.high > 0)
518
+ row('HIGH', colors.danger, data.summary.high, 'with compatibility problems');
519
+ if (data.summary.medium > 0)
520
+ row('MEDIUM', colors.warning, data.summary.medium, 'with version conflicts');
521
+ if (data.summary.low > 0)
522
+ row('LOW', colors.dim, data.summary.low, 'to review');
523
+ console.log(colors.gray('└' + '─'.repeat(W) + '┘'));
524
+ await sleep(80);
525
+ if (missingPackagesCount > 0) {
526
+ console.log(colors.dim(` + ${missingPackagesCount} missing peer ${missingPackagesCount > 1 ? 'dependencies' : 'dependency'} to install.`));
527
+ }
528
+ console.log(colors.dim(' Unlock to see details and recommended fixes.'));
529
+ }
530
+ /**
531
+ * Display teaser table (for larger conflict counts)
532
+ */
533
+ async function displayTeaserTable(installedConflicts, missingPackagesCount) {
534
+ const tableLines = createTeaserTable(installedConflicts.slice(0, CLI_AUDIT_SAMPLE_SIZE)).split('\n');
535
+ for (const line of tableLines) {
536
+ console.log(line);
537
+ await sleep(40);
538
+ }
539
+ await sleep(80);
540
+ const hiddenCount = installedConflicts.length - CLI_AUDIT_SAMPLE_SIZE;
541
+ if (hiddenCount > 0) {
542
+ console.log(colors.dim(` + ${hiddenCount} other conflicts hidden.`));
543
+ }
544
+ if (missingPackagesCount > 0) {
545
+ console.log(colors.dim(` + ${missingPackagesCount} missing peer ${missingPackagesCount > 1 ? 'dependencies' : 'dependency'} to install.`));
546
+ }
547
+ }
548
+ /**
549
+ * Display full analysis with solutions (after unlock)
550
+ */
551
+ async function displayFullAnalysis(data, solution) {
552
+ console.log();
553
+ await sleep(100);
554
+ console.log(chalk.bold.green('🔓 FULL ANALYSIS'));
555
+ await sleep(80);
556
+ console.log(chalk.dim('─'.repeat(50)));
557
+ await sleep(120);
558
+ console.log();
559
+ // Separate package conflicts from engine conflicts
560
+ const packageConflicts = data.conflicts.filter((c) => c.package !== 'Node.js' && c.package !== 'npm' && c.category !== 'engine');
561
+ const engineConflicts = data.conflicts.filter((c) => c.package === 'Node.js' || c.package === 'npm' || c.category === 'engine');
562
+ // Show full table with recommended versions
563
+ if (packageConflicts.length > 0) {
564
+ await displayPackageConflictsTable(packageConflicts, solution);
565
+ }
566
+ // Show engine requirements
567
+ await displayEngineRequirements(solution, engineConflicts);
568
+ // Show packages to add
569
+ await displayPackagesToAdd(packageConflicts);
570
+ // Show removals
571
+ await displayRemovals(solution);
572
+ // If no package conflicts and no removals
573
+ if (packageConflicts.length === 0 && (!solution.removals || solution.removals.length === 0)) {
574
+ console.log(colors.dim(' No package version changes needed.'));
575
+ console.log();
576
+ }
577
+ // Track: results_shown
578
+ analytics.resultsShown({
579
+ conflictCount: data.conflicts.length,
580
+ removalsCount: solution.removals?.length || 0,
581
+ });
582
+ }
583
+ /**
584
+ * Display package conflicts table
585
+ */
586
+ async function displayPackageConflictsTable(packageConflicts, solution) {
587
+ const fullTableLines = createFullSolutionTable(packageConflicts, solution).split('\n');
588
+ for (const line of fullTableLines) {
589
+ console.log(line);
590
+ await sleep(35);
591
+ }
592
+ console.log();
593
+ }
594
+ /**
595
+ * Display engine requirements
596
+ */
597
+ async function displayEngineRequirements(solution, engineConflicts) {
598
+ if (solution.engines && Object.keys(solution.engines).length > 0) {
599
+ console.log(colors.whiteBold('⚙️ Engine Requirements:'));
600
+ if (solution.engines.node) {
601
+ console.log(` ${colors.dim('•')} Node.js: ${colors.brand(solution.engines.node)}`);
826
602
  }
603
+ if (solution.engines.npm) {
604
+ console.log(` ${colors.dim('•')} npm: ${colors.brand(solution.engines.npm)}`);
605
+ }
606
+ console.log();
827
607
  }
828
- // Remove deprecated packages
829
- if (solution.removals) {
830
- for (const removal of solution.removals) {
831
- if (removal.type === 'dependency') {
832
- delete updated.dependencies?.[removal.package];
608
+ else if (engineConflicts.length > 0) {
609
+ console.log(colors.whiteBold('⚙️ Engine Requirements:'));
610
+ for (const ec of engineConflicts) {
611
+ const engine = ec.package || 'Unknown';
612
+ let required = ec.recommendedVersion || ec.requiredVersion;
613
+ if (!required && ec.engineDetails?.requiredVersion) {
614
+ required = ec.engineDetails.requiredVersion.replace(/^>=/, '');
833
615
  }
834
- else {
835
- delete updated.devDependencies?.[removal.package];
616
+ if (required) {
617
+ console.log(` ${colors.dim('•')} ${engine}: ${colors.brand('>=' + required)}`);
836
618
  }
837
619
  }
620
+ console.log();
838
621
  }
839
- return updated;
840
622
  }
841
623
  /**
842
- * Simple client-side framework detection
624
+ * Display packages to add
843
625
  */
844
- function detectFramework(pkg) {
845
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
846
- if (deps['@angular/core'])
847
- return 'angular';
848
- if (deps['react'])
849
- return 'react';
850
- if (deps['vue'])
851
- return 'vue';
852
- if (deps['svelte'])
853
- return 'svelte';
854
- if (deps['next'])
855
- return 'react';
856
- if (deps['nuxt'])
857
- return 'vue';
858
- return undefined;
626
+ async function displayPackagesToAdd(packageConflicts) {
627
+ const packagesToAdd = packageConflicts.filter((c) => !c.currentVersion || c.currentVersion.toLowerCase() === 'not installed');
628
+ if (packagesToAdd.length === 0)
629
+ return;
630
+ console.log(colors.whiteBold('📦 Packages to Add:'));
631
+ for (const pkg of packagesToAdd) {
632
+ await sleep(60);
633
+ let message;
634
+ if (pkg.requiredBy && Array.isArray(pkg.requiredBy) && pkg.requiredBy.length > 0) {
635
+ const requiredRange = pkg.requiredRange || 'required version';
636
+ message = `Required as peer dependency by ${pkg.requiredBy.join(', ')} (${requiredRange})`;
637
+ }
638
+ else if (pkg.isPeerDependency && pkg.requiredRange) {
639
+ message = `Missing peer dependency (${pkg.requiredRange})`;
640
+ }
641
+ else {
642
+ message = pkg.description || 'Required dependency';
643
+ }
644
+ console.log(` ${colors.brand('+')} ${pkg.package}`);
645
+ console.log(` ${colors.dim(message)}`);
646
+ }
647
+ console.log();
859
648
  }
860
649
  /**
861
- * Calculate severity summary from conflicts array
650
+ * Display removals
862
651
  */
863
- function calculateSummary(conflicts) {
652
+ async function displayRemovals(solution) {
653
+ if (!solution.removals || solution.removals.length === 0)
654
+ return;
655
+ await sleep(100);
656
+ console.log(colors.whiteBold('🗑 Packages to Remove:'));
657
+ const removalLines = createMigrationTable(solution.removals.map((r) => ({
658
+ package: r.package,
659
+ currentVersion: '*',
660
+ targetVersion: 'REMOVE',
661
+ changeType: 'deprec',
662
+ isRemoval: true,
663
+ }))).split('\n');
664
+ for (const line of removalLines) {
665
+ console.log(line);
666
+ await sleep(30);
667
+ }
668
+ console.log();
669
+ }
670
+ // ============================================================================
671
+ // SESSION MANAGEMENT
672
+ // ============================================================================
673
+ /**
674
+ * Save session for potential later fix
675
+ */
676
+ async function saveSession(ctx, auditResult) {
677
+ const { sessionManager, sanitized, packageJsonHash } = ctx;
678
+ const { analysisId, cost, tierName, packageCount } = auditResult;
679
+ await sessionManager.saveSession({
680
+ analysisId,
681
+ intent: 'ANALYZE',
682
+ originalFileHash: packageJsonHash,
683
+ cost,
684
+ status: 'UNPAID',
685
+ projectName: sanitized.name || 'unnamed',
686
+ packageCount,
687
+ tierName,
688
+ });
689
+ }
690
+ /**
691
+ * Handle the complete payment flow (auth check, balance check, deduct credits)
692
+ */
693
+ async function handlePaymentFlow(ctx, auditResult) {
694
+ const { options, sessionManager } = ctx;
695
+ const { analysisId, cost, tierName } = auditResult;
696
+ const authManager = new AuthManager();
697
+ const isAlreadyLoggedIn = await authManager.isAuthenticated();
698
+ const paymentFlow = new PaymentFlowService();
699
+ let userHasActivePass = false;
700
+ if (isAlreadyLoggedIn) {
701
+ const result = await handleLoggedInPaymentFlow(paymentFlow, cost, tierName, options);
702
+ if (!result.success) {
703
+ return { success: false };
704
+ }
705
+ userHasActivePass = result.hasActivePass;
706
+ }
707
+ else {
708
+ const result = await handleAnonymousPaymentFlow(paymentFlow, cost, tierName, options);
709
+ if (!result.success) {
710
+ return { success: false };
711
+ }
712
+ userHasActivePass = result.hasActivePass;
713
+ }
714
+ // Deduct credits and get solution
715
+ const fixResult = await paymentFlow.deductCredits(analysisId, userHasActivePass);
716
+ if (!fixResult.success || !fixResult.solution) {
717
+ throw new Error(fixResult.error || 'Unknown error');
718
+ }
719
+ // Update session status
720
+ await sessionManager.updateStatus('PAID');
864
721
  return {
865
- critical: conflicts.filter(c => c.severity === 'critical').length,
866
- high: conflicts.filter(c => c.severity === 'high').length,
867
- medium: conflicts.filter(c => c.severity === 'medium').length,
868
- low: conflicts.filter(c => c.severity === 'low').length,
722
+ success: true,
723
+ solution: fixResult.solution,
724
+ hasActivePass: userHasActivePass,
869
725
  };
870
726
  }
871
727
  /**
872
- * Wrap text to specified width
728
+ * Handle payment flow for logged-in users
873
729
  */
874
- function wrapText(text, maxWidth) {
875
- const words = text.split(' ');
876
- const lines = [];
877
- let currentLine = '';
878
- for (const word of words) {
879
- if (currentLine.length + word.length + 1 <= maxWidth) {
880
- currentLine += (currentLine ? ' ' : '') + word;
730
+ async function handleLoggedInPaymentFlow(paymentFlow, cost, tierName, options) {
731
+ const balanceInfo = await paymentFlow.getBalanceInfo();
732
+ // If balance info is null, the token is likely expired
733
+ // Treat as not logged in and trigger login flow
734
+ if (!balanceInfo) {
735
+ return handleExpiredAuthFlow(paymentFlow, cost, tierName, options);
736
+ }
737
+ const userHasActivePass = balanceInfo.hasActivePass || false;
738
+ // Show user details
739
+ printUserDetails({
740
+ name: balanceInfo.name,
741
+ email: balanceInfo.email,
742
+ credits: balanceInfo.credits || 0,
743
+ hasActivePass: userHasActivePass,
744
+ showHeader: false,
745
+ });
746
+ // Show credit check
747
+ printCreditCheck({
748
+ needed: cost,
749
+ available: balanceInfo?.credits || 0,
750
+ hasActivePass: userHasActivePass,
751
+ });
752
+ // Cost box with prompt
753
+ printCostBox({
754
+ cost,
755
+ tierName,
756
+ prompt: userHasActivePass ? 'Continue? (Enter/Esc)' : `Deduct ${cost} credit${cost > 1 ? 's' : ''} to unlock? (Enter/Esc)`,
757
+ hasActivePass: userHasActivePass,
758
+ });
759
+ // Track: unlock_prompt_shown
760
+ analytics.unlockPromptShown({
761
+ creditsNeeded: cost,
762
+ creditsAvailable: balanceInfo?.credits || 0,
763
+ tier: tierName,
764
+ hasActivePass: userHasActivePass,
765
+ });
766
+ const confirmUnlock = await confirmPrompt('', options);
767
+ if (!confirmUnlock) {
768
+ analytics.unlockRejected({ reason: 'user_cancelled' });
769
+ console.log();
770
+ printInfo('Solution saved. Run `npx depfixer fix` anytime to resume.');
771
+ return { success: false, hasActivePass: userHasActivePass };
772
+ }
773
+ analytics.unlockAccepted({ creditsDeducted: userHasActivePass ? 0 : cost });
774
+ // Check balance is sufficient
775
+ if (!userHasActivePass && (balanceInfo?.credits || 0) < cost) {
776
+ analytics.unlockFailed({
777
+ reason: 'insufficient_credits',
778
+ needed: cost,
779
+ available: balanceInfo?.credits || 0,
780
+ });
781
+ const hasBalance = await paymentFlow.ensureSufficientBalance(cost);
782
+ // If auth expired during balance check, trigger re-login flow
783
+ if (hasBalance === 'auth_expired') {
784
+ return handleExpiredAuthFlow(paymentFlow, cost, tierName, options);
881
785
  }
882
- else {
883
- if (currentLine) {
884
- lines.push(currentLine);
885
- }
886
- currentLine = word;
786
+ if (!hasBalance) {
787
+ console.log();
788
+ printInfo('Run `npx depfixer fix` when ready to continue.');
789
+ return { success: false, hasActivePass: userHasActivePass };
790
+ }
791
+ // After top-up, confirm again
792
+ printCostBox({
793
+ cost,
794
+ tierName,
795
+ prompt: `Deduct ${cost} credit${cost > 1 ? 's' : ''} to unlock? (Enter/Esc)`,
796
+ });
797
+ const confirmAfterTopUp = await confirmPrompt('', options);
798
+ if (!confirmAfterTopUp) {
799
+ console.log();
800
+ printInfo('Solution saved. Run `npx depfixer fix` anytime to resume.');
801
+ return { success: false, hasActivePass: userHasActivePass };
887
802
  }
888
803
  }
889
- if (currentLine) {
890
- lines.push(currentLine);
804
+ return { success: true, hasActivePass: userHasActivePass };
805
+ }
806
+ /**
807
+ * Handle payment flow for anonymous users
808
+ */
809
+ async function handleAnonymousPaymentFlow(paymentFlow, cost, tierName, options) {
810
+ printCostBox({
811
+ cost,
812
+ tierName,
813
+ prompt: '',
814
+ });
815
+ analytics.unlockPromptShown({
816
+ creditsNeeded: cost,
817
+ creditsAvailable: 0,
818
+ tier: tierName,
819
+ isAnonymous: true,
820
+ });
821
+ analytics.authRequired({ creditsNeeded: cost });
822
+ console.log();
823
+ console.log(colors.warning('⚠️ Login required to unlock'));
824
+ console.log(colors.gray('[?] Continue to login? (Enter/Esc)'));
825
+ const wantsToUnlock = await confirmPrompt('', options);
826
+ if (!wantsToUnlock) {
827
+ analytics.authAbandoned({ reason: 'user_cancelled' });
828
+ console.log();
829
+ printInfo('Solution saved. Run `npx depfixer fix` anytime to resume.');
830
+ return { success: false, hasActivePass: false };
831
+ }
832
+ // Run full auth + balance flow
833
+ const paymentResult = await paymentFlow.ensureReadyToPay(cost);
834
+ if (!paymentResult.ready) {
835
+ analytics.authAbandoned({ reason: 'auth_flow_incomplete' });
836
+ console.log();
837
+ printInfo('Run `npx depfixer fix` when ready to continue.');
838
+ return { success: false, hasActivePass: false };
891
839
  }
892
- return lines;
840
+ const userHasActivePass = paymentResult.hasActivePass || false;
841
+ // Confirm after login
842
+ printCostBox({
843
+ cost,
844
+ tierName,
845
+ prompt: userHasActivePass ? 'Continue? (Enter/Esc)' : `Confirm: Deduct ${cost} credit${cost > 1 ? 's' : ''}? (Enter/Esc)`,
846
+ hasActivePass: userHasActivePass,
847
+ });
848
+ const confirmUnlock = await confirmPrompt('', options);
849
+ if (!confirmUnlock) {
850
+ analytics.unlockRejected({ reason: 'user_cancelled_after_login' });
851
+ console.log();
852
+ printInfo('Solution saved. Run `npx depfixer fix` anytime to resume.');
853
+ return { success: false, hasActivePass: userHasActivePass };
854
+ }
855
+ analytics.unlockAccepted({ creditsDeducted: userHasActivePass ? 0 : cost });
856
+ return { success: true, hasActivePass: userHasActivePass };
893
857
  }
894
858
  /**
895
- * Create teaser table (limited info, no solutions)
896
- * Shows only severity and package name with generic issue type
859
+ * Handle expired auth flow - token exists but is invalid/expired
860
+ * Clear stored tokens and trigger fresh login flow
897
861
  */
898
- function createTeaserTable(conflicts) {
899
- // Column widths
900
- const COL1 = 12; // SEVERITY
901
- const COL2 = 30; // PACKAGE
902
- const COL3 = 14; // ISSUE
903
- const TOTAL_WIDTH = COL1 + COL2 + COL3;
904
- const lines = [];
905
- // Header row
906
- lines.push(colors.whiteBold('SEVERITY'.padEnd(COL1)) +
907
- colors.whiteBold('PACKAGE'.padEnd(COL2)) +
908
- colors.whiteBold('ISSUE'.padEnd(COL3)));
909
- // Single separator line
910
- lines.push(colors.gray('─'.repeat(TOTAL_WIDTH)));
911
- for (const conflict of conflicts) {
912
- const severity = conflict.severity?.toUpperCase() || 'UNKNOWN';
913
- const severityColor = severity === 'CRITICAL' ? colors.dangerBold :
914
- severity === 'HIGH' ? colors.danger :
915
- severity === 'MEDIUM' ? colors.warning :
916
- colors.dim;
917
- // Truncate package name if too long
918
- const pkg = (conflict.package || '').substring(0, COL2 - 1).padEnd(COL2);
919
- // Generic issue description (no details)
920
- const issueType = severity === 'CRITICAL' ? 'Peer Clash' :
921
- severity === 'HIGH' ? 'Version Gap' :
922
- severity === 'MEDIUM' ? 'Conflict' : 'Minor';
923
- lines.push(severityColor(severity.padEnd(COL1)) +
924
- pkg +
925
- issueType);
862
+ async function handleExpiredAuthFlow(paymentFlow, cost, tierName, options) {
863
+ // Clear expired tokens
864
+ const authManager = new AuthManager();
865
+ await authManager.clearCredentials();
866
+ // Show expired message
867
+ console.log();
868
+ console.log(colors.warning('⚠️ Session expired'));
869
+ console.log(colors.dim(' Your previous login has expired.'));
870
+ console.log();
871
+ // Show cost box first
872
+ printCostBox({
873
+ cost,
874
+ tierName,
875
+ prompt: '',
876
+ });
877
+ console.log();
878
+ console.log(colors.gray('[?] Continue to login? (Enter/Esc)'));
879
+ const wantsToLogin = await confirmPrompt('', options);
880
+ if (!wantsToLogin) {
881
+ analytics.authAbandoned({ reason: 'user_cancelled_expired_session' });
882
+ console.log();
883
+ printInfo('Solution saved. Run `npx depfixer fix` anytime to resume.');
884
+ return { success: false, hasActivePass: false };
926
885
  }
927
- return lines.join('\n');
886
+ // Run full auth + balance flow
887
+ const paymentResult = await paymentFlow.ensureReadyToPay(cost);
888
+ if (!paymentResult.ready) {
889
+ analytics.authAbandoned({ reason: 'auth_flow_incomplete' });
890
+ console.log();
891
+ printInfo('Run `npx depfixer fix` when ready to continue.');
892
+ return { success: false, hasActivePass: false };
893
+ }
894
+ const userHasActivePass = paymentResult.hasActivePass || false;
895
+ // Confirm after login
896
+ printCostBox({
897
+ cost,
898
+ tierName,
899
+ prompt: userHasActivePass ? 'Continue? (Enter/Esc)' : `Confirm: Deduct ${cost} credit${cost > 1 ? 's' : ''}? (Enter/Esc)`,
900
+ hasActivePass: userHasActivePass,
901
+ });
902
+ const confirmUnlock = await confirmPrompt('', options);
903
+ if (!confirmUnlock) {
904
+ analytics.unlockRejected({ reason: 'user_cancelled_after_relogin' });
905
+ console.log();
906
+ printInfo('Solution saved. Run `npx depfixer fix` anytime to resume.');
907
+ return { success: false, hasActivePass: userHasActivePass };
908
+ }
909
+ analytics.unlockAccepted({ creditsDeducted: userHasActivePass ? 0 : cost });
910
+ return { success: true, hasActivePass: userHasActivePass };
928
911
  }
912
+ // ============================================================================
913
+ // PREFETCH POLLING
914
+ // ============================================================================
929
915
  /**
930
- * Create full solution table (shows recommended versions and removals)
931
- * Clean design: simple headers with dash separator line
916
+ * Poll prefetch status if there are pending packages
932
917
  */
933
- function createFullSolutionTable(conflicts, solution) {
934
- // Column widths
935
- const COL1 = 35; // PACKAGE
936
- const COL2 = 14; // CURRENT
937
- const COL3 = 14; // TARGET
938
- const COL4 = 10; // TYPE
939
- const TOTAL_WIDTH = COL1 + COL2 + COL3 + COL4;
940
- const lines = [];
941
- // Header row
942
- lines.push(colors.whiteBold('PACKAGE'.padEnd(COL1)) +
943
- colors.whiteBold('CURRENT'.padEnd(COL2)) +
944
- colors.whiteBold('TARGET'.padEnd(COL3)) +
945
- colors.whiteBold('TYPE'.padEnd(COL4)));
946
- // Single separator line
947
- lines.push(colors.gray('─'.repeat(TOTAL_WIDTH)));
948
- // Merge solution deps
949
- const allSolutions = { ...solution.dependencies, ...solution.devDependencies };
950
- // Separate conflicts into updates vs adds
951
- const updates = [];
952
- const adds = [];
953
- for (const conflict of conflicts) {
954
- const recommended = allSolutions[conflict.package] || conflict.recommendedVersion || '';
955
- const currentRaw = conflict.currentVersion || '';
956
- const isNotInstalled = !currentRaw || currentRaw.toLowerCase() === 'not installed';
957
- if (isNotInstalled) {
958
- // Package to add
959
- adds.push({ conflict, recommended });
960
- }
961
- else if (recommended) {
962
- // Package to update - determine change type
963
- let changeType = 'patch';
964
- const cleanCurrent = currentRaw.replace(/[~^]/g, '');
965
- const cleanRec = recommended.replace(/[~^]/g, '');
966
- const currMajor = parseInt(cleanCurrent.split('.')[0], 10);
967
- const recMajor = parseInt(cleanRec.split('.')[0], 10);
968
- if (!isNaN(currMajor) && !isNaN(recMajor)) {
969
- if (recMajor > currMajor)
970
- changeType = 'major';
971
- else if (recMajor === currMajor) {
972
- const currMinor = parseInt(cleanCurrent.split('.')[1] || '0', 10);
973
- const recMinor = parseInt(cleanRec.split('.')[1] || '0', 10);
974
- if (recMinor > currMinor)
975
- changeType = 'minor';
918
+ async function pollPrefetchIfNeeded(ctx, auditResult, paymentResult) {
919
+ const { apiClient } = ctx;
920
+ const { hasPendingPackages, prefetchId, analysisId, data } = auditResult;
921
+ if (!hasPendingPackages || !prefetchId) {
922
+ return updateDataWithSolution(data, paymentResult.solution);
923
+ }
924
+ const spinner = createSpinner('Finalizing complete analysis...').start();
925
+ let pollCount = 0;
926
+ const maxPolls = 120; // 2 minutes max
927
+ let updatedData = data;
928
+ while (pollCount < maxPolls) {
929
+ try {
930
+ const status = await apiClient.pollPrefetchStatus(prefetchId);
931
+ if (status.isComplete && status.reanalysisStatus === 'completed') {
932
+ const updatedResponse = await apiClient.getAnalysisById(analysisId);
933
+ if (updatedResponse.success && updatedResponse.data) {
934
+ const updated = updatedResponse.data;
935
+ const analysisResult = updated.analysisResult || {};
936
+ updatedData = {
937
+ ...data,
938
+ conflicts: analysisResult.conflicts || data.conflicts,
939
+ missingDependencies: analysisResult.missingDependencies || data.missingDependencies,
940
+ deprecations: analysisResult.deprecations || data.deprecations,
941
+ healthScore: analysisResult.healthScore || data.healthScore,
942
+ summary: analysisResult.conflicts ? calculateSummary(analysisResult.conflicts) : data.summary,
943
+ };
976
944
  }
945
+ break;
946
+ }
947
+ if (status.percentage !== undefined) {
948
+ const percent = Math.round(status.percentage);
949
+ spinner.text = `Finalizing complete analysis... ${percent}%`;
977
950
  }
978
- updates.push({ conflict, recommended, changeType });
951
+ await sleep(1000);
952
+ pollCount++;
953
+ }
954
+ catch (err) {
955
+ await sleep(2000);
956
+ pollCount++;
979
957
  }
980
- // Skip if no recommendation and already installed
981
958
  }
982
- // Render updates first
983
- for (const { conflict, recommended, changeType } of updates) {
984
- const pkg = (conflict.package || '').substring(0, COL1 - 1).padEnd(COL1);
985
- let currentRaw = (conflict.currentVersion || '').replace(/[~^]/g, '').substring(0, COL2 - 1);
986
- if (currentRaw.toLowerCase() === 'installed')
987
- currentRaw = '—';
988
- const currentPadded = currentRaw.padEnd(COL2);
989
- const recommendedClean = recommended.replace(/[~^]/g, '').substring(0, COL3 - 1);
990
- const recommendedPadded = recommendedClean.padEnd(COL3);
991
- const typeLabel = changeType === 'major' ? 'Major' :
992
- changeType === 'minor' ? 'Minor' : 'Patch';
993
- const typeColor = changeType === 'major' ? colors.danger :
994
- changeType === 'minor' ? colors.warning : colors.success;
995
- lines.push(pkg +
996
- colors.versionOld(currentPadded) +
997
- colors.version(recommendedPadded) +
998
- typeColor(typeLabel));
959
+ spinner.succeed('Complete analysis ready');
960
+ return updatedData;
961
+ }
962
+ /**
963
+ * Update data with solution results
964
+ */
965
+ function updateDataWithSolution(data, solution) {
966
+ if (!solution)
967
+ return data;
968
+ return {
969
+ ...data,
970
+ conflicts: solution.conflicts || data.conflicts,
971
+ missingDependencies: solution.missingDependencies || data.missingDependencies,
972
+ deprecations: solution.deprecations || data.deprecations,
973
+ healthScore: solution.healthScore || data.healthScore,
974
+ summary: solution.summary || data.summary,
975
+ };
976
+ }
977
+ // ============================================================================
978
+ // FIX APPLICATION
979
+ // ============================================================================
980
+ /**
981
+ * Handle fix application prompt and execution
982
+ */
983
+ async function handleFixApplication(ctx, data, solution) {
984
+ const { projectDir, packageJsonService, parsed, options } = ctx;
985
+ // Explain what will be changed
986
+ await sleep(100);
987
+ console.log(colors.gray('────────────────────────────────────────'));
988
+ await sleep(50);
989
+ console.log(colors.gray(' 📝 Only ') + colors.white('package.json') + colors.gray(' will be modified'));
990
+ await sleep(50);
991
+ console.log(colors.gray(' 💾 A backup (') + colors.white('package.json.bak') + colors.gray(') will be created'));
992
+ await sleep(50);
993
+ console.log(colors.gray('────────────────────────────────────────'));
994
+ await sleep(100);
995
+ console.log();
996
+ // Track: fix_prompt_shown
997
+ analytics.fixPromptShown({
998
+ updatesCount: Object.keys(solution.dependencies || {}).length + Object.keys(solution.devDependencies || {}).length,
999
+ removalsCount: solution.removals?.length || 0,
1000
+ });
1001
+ const shouldApply = await confirmPrompt('Apply fix to package.json?', options);
1002
+ if (!shouldApply) {
1003
+ analytics.fixDeferred({ reason: 'user_declined' });
1004
+ console.log();
1005
+ printInfo('Solution unlocked but not applied. Run `npx depfixer fix` to apply later.');
1006
+ return;
999
1007
  }
1000
- // Render adds
1001
- for (const { conflict, recommended } of adds) {
1002
- const pkg = (conflict.package || '').substring(0, COL1 - 1).padEnd(COL1);
1003
- const currentPadded = '—'.padEnd(COL2);
1004
- const recommendedClean = recommended ? recommended.replace(/[~^]/g, '').substring(0, COL3 - 1) : 'Add';
1005
- const recommendedPadded = recommendedClean.padEnd(COL3);
1006
- lines.push(pkg +
1007
- colors.dim(currentPadded) +
1008
- colors.version(recommendedPadded) +
1009
- colors.brand('+ Add'));
1008
+ analytics.fixAccepted();
1009
+ // Apply the solution
1010
+ await applyFixes(projectDir, packageJsonService, parsed, solution, data);
1011
+ }
1012
+ /**
1013
+ * Apply fixes to package.json
1014
+ */
1015
+ async function applyFixes(projectDir, packageJsonService, parsed, solution, data) {
1016
+ console.log();
1017
+ console.log(chalk.bold('🔧 Applying fixes...'));
1018
+ console.log();
1019
+ const changes = packageJsonService.getChanges(parsed, solution);
1020
+ const removals = (solution.removals || []).map((r) => ({
1021
+ package: r.package,
1022
+ reason: r.reason || 'Deprecated',
1023
+ type: r.type,
1024
+ }));
1025
+ let fixResult;
1026
+ await runStepSequence([...FIX_STEPS], async () => {
1027
+ fixResult = await packageJsonService.applySurgicalFixes(projectDir, changes, removals, solution.engines);
1028
+ }, { successMessage: 'Fix complete', minStepDuration: 100 });
1029
+ const { backupPath, applied, removed, enginesUpdated } = fixResult;
1030
+ // Show success
1031
+ printSuccessBox({
1032
+ updated: applied,
1033
+ removed,
1034
+ backupPath,
1035
+ enginesUpdated,
1036
+ });
1037
+ // Track: fix_applied
1038
+ analytics.fixApplied({
1039
+ updatedCount: applied,
1040
+ removedCount: removed,
1041
+ enginesUpdated,
1042
+ });
1043
+ // Track: session_ended
1044
+ await analytics.sessionEnded({
1045
+ outcome: 'fix_applied',
1046
+ healthScore: data.healthScore,
1047
+ });
1048
+ }
1049
+ // ============================================================================
1050
+ // ERROR HANDLING
1051
+ // ============================================================================
1052
+ /**
1053
+ * Handle errors and track session end
1054
+ */
1055
+ async function handleError(error, options) {
1056
+ await analytics.sessionEnded({
1057
+ outcome: 'error',
1058
+ error: error.message,
1059
+ });
1060
+ if (options.json) {
1061
+ console.log(JSON.stringify({ error: error.message }, null, 2));
1010
1062
  }
1011
- // Render removals
1012
- if (solution.removals && solution.removals.length > 0) {
1013
- for (const removal of solution.removals) {
1014
- const pkg = (removal.package || '').substring(0, COL1 - 1).padEnd(COL1);
1015
- const currentPadded = 'installed'.padEnd(COL2);
1016
- const targetPadded = '—'.padEnd(COL3);
1017
- lines.push(pkg +
1018
- colors.versionOld(currentPadded) +
1019
- colors.dim(targetPadded) +
1020
- colors.danger('Remove'));
1021
- }
1063
+ else {
1064
+ printError(error.message);
1022
1065
  }
1023
- return lines.join('\n');
1066
+ process.exit(options.ci ? 2 : 1);
1024
1067
  }
1025
1068
  //# sourceMappingURL=smart.js.map