depfixer 1.0.0 → 1.1.1

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