depfixer 1.1.9 → 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 +94 -0
  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 +7 -0
  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,3 +1,17 @@
1
+ /**
2
+ * Migrate Command
3
+ *
4
+ * Interactive migration planner for framework upgrades.
5
+ * Supports Angular, React (web), and Vue framework migrations.
6
+ *
7
+ * Flow:
8
+ * 1. Detect framework via API (server-side rules)
9
+ * 2. Fetch available versions from server
10
+ * 3. Show version selector (minor patches vs major upgrades)
11
+ * 4. User selects target version
12
+ * 5. Show migration plan with cost
13
+ * 6. Confirm and execute
14
+ */
1
15
  import chalk from 'chalk';
2
16
  import { ApiClient } from '../services/api-client.js';
3
17
  import { PackageJsonService } from '../services/package-json.js';
@@ -7,16 +21,52 @@ import { analytics } from '../services/analytics.js';
7
21
  import { getDeviceId } from '../services/device-id.js';
8
22
  import { createSpinner, createMigrationTable, printError, printSuccess, printInfo, runStepSequence, sleep, } from '../utils/output.js';
9
23
  import { colors, printCliHeader, printMigrationPlanHeader, printProjectionStats, printCostBox, printSuccessBox, renderHealthBar, getHealthStatus, printUserDetails, } from '../utils/design-system.js';
24
+ import { promptYesNo } from '../utils/prompt.js';
25
+ import { getChangeType } from '../utils/framework-utils.js';
26
+ // ============================================================================
27
+ // DEV MODE HELPERS
28
+ // ============================================================================
29
+ /**
30
+ * Check if auto-confirm is enabled (dev mode only)
31
+ */
32
+ function isAutoConfirmEnabled() {
33
+ const isDevMode = process.env.NODE_ENV === 'development' ||
34
+ (process.env.DEPFIXER_API_URL?.includes('localhost') ?? false);
35
+ return isDevMode && process.env.DEPFIXER_AUTO_CONFIRM === 'true';
36
+ }
37
+ /**
38
+ * Prompt for confirmation with dev mode auto-confirm support
39
+ */
40
+ async function confirmPrompt(question, options) {
41
+ // --yes flag overrides
42
+ if (options.yes) {
43
+ if (question) {
44
+ console.log(`${question} ${colors.dim('(Enter/Esc)')} ${colors.success('Yes')} ${colors.dim('[--yes]')}`);
45
+ }
46
+ else {
47
+ console.log(`${colors.success('Yes')} ${colors.dim('[--yes]')}`);
48
+ }
49
+ return true;
50
+ }
51
+ // Dev mode auto-confirm
52
+ if (isAutoConfirmEnabled()) {
53
+ if (question) {
54
+ console.log(`${question} ${colors.dim('(Enter/Esc)')} ${colors.success('Yes')} ${colors.dim('[auto]')}`);
55
+ }
56
+ else {
57
+ console.log(`${colors.success('Yes')} ${colors.dim('[auto]')}`);
58
+ }
59
+ return true;
60
+ }
61
+ return promptYesNo(question);
62
+ }
63
+ // ============================================================================
64
+ // MAIN COMMAND
65
+ // ============================================================================
10
66
  /**
11
67
  * Migrate command
12
68
  *
13
- * Interactive migration with version selection:
14
- * 1. Detect framework and current version
15
- * 2. Fetch available versions from server
16
- * 3. Show version selector (minor patches vs major upgrades)
17
- * 4. User selects target version
18
- * 5. Show migration plan with cost
19
- * 6. Confirm and execute
69
+ * Interactive migration with version selection.
20
70
  *
21
71
  * Usage:
22
72
  * npx depfixer migrate
@@ -27,506 +77,669 @@ export async function migrateCommand(options) {
27
77
  // Create device ID early (for anonymous user tracking before login)
28
78
  getDeviceId();
29
79
  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
- if (!framework) {
39
- printError('Could not detect framework. Migration requires Angular or React project.');
40
- process.exit(1);
41
- }
42
- // Detect current version
43
- const currentVersion = detectCurrentVersion(sanitized, framework);
44
- const currentMajor = currentVersion ? parseInt(currentVersion.split('.')[0], 10) : 0;
45
- const frameworkName = framework.charAt(0).toUpperCase() + framework.slice(1);
80
+ // Initialize context with API-based framework detection
81
+ const ctx = await initializeMigrationContext(projectDir, options);
46
82
  // Print CLI header
47
83
  printCliHeader('migrate');
48
84
  // Track: migrate_started
49
85
  analytics.migrateStarted({ command: 'migrate' });
86
+ // Show project info
50
87
  console.log();
51
- console.log(colors.whiteBold(`📦 Project: ${colors.brand(sanitized.name || 'unnamed')}`));
52
- console.log(colors.dim(` Framework: ${frameworkName} ${currentVersion || 'unknown'}`));
88
+ console.log(colors.whiteBold(`📦 Project: ${colors.brand(ctx.sanitized.name || 'unnamed')}`));
89
+ console.log(colors.dim(` Framework: ${ctx.frameworkName} ${ctx.currentVersion || 'unknown'}`));
53
90
  console.log();
54
- let targetVersion;
55
- // Interactive version selector
56
- {
57
- const spinner = createSpinner('Fetching available versions...').start();
58
- try {
59
- const versionsResponse = await apiClient.getFrameworkVersions(framework, currentMajor);
60
- // Check if already on latest version (no newer versions available)
61
- if (!versionsResponse.success) {
62
- const errorMsg = versionsResponse.error || '';
63
- // If error indicates no newer versions, user is on latest
64
- if (errorMsg.includes('No newer versions') || errorMsg.includes('No supported versions')) {
65
- spinner.succeed('Version check complete');
66
- await showLatestVersionInfo(apiClient, sanitized, framework, frameworkName, currentVersion);
67
- return;
68
- }
69
- spinner.fail('Failed to fetch versions');
70
- throw new Error(errorMsg || 'Could not fetch available versions');
71
- }
72
- spinner.succeed('Versions loaded');
73
- // Track: versions_loaded
74
- analytics.versionsLoaded({
75
- framework,
76
- currentVersion,
77
- groupCount: versionsResponse.data?.groups?.length || 0,
78
- });
79
- const groups = versionsResponse.data?.groups;
80
- if (!groups || groups.length === 0) {
81
- await showLatestVersionInfo(apiClient, sanitized, framework, frameworkName, currentVersion);
82
- return;
83
- }
84
- // Build version options from groups - get latest version from each major
85
- const versionOptions = [];
86
- for (const group of groups) {
87
- // Get the first (latest) option from each group
88
- const latestInGroup = group.options[0];
89
- if (latestInGroup) {
90
- const major = parseInt(latestInGroup.value.split('.')[0], 10);
91
- const isMajorUpgrade = major > currentMajor;
92
- versionOptions.push({
93
- version: latestInGroup.value,
94
- major,
95
- // Show full version number like "Angular 20.3.15 (Latest)"
96
- label: `${frameworkName} ${latestInGroup.value}` + (latestInGroup.badge ? ` (${latestInGroup.badge})` : ''),
97
- type: isMajorUpgrade ? 'major' : 'minor',
98
- isLatest: latestInGroup.badge === 'Latest',
99
- });
100
- }
101
- }
102
- // Show version selector
103
- console.log();
104
- console.log(colors.whiteBold('🎯 Select target version:'));
105
- console.log();
106
- // Group by type
107
- const minorOptions = versionOptions.filter(v => v.type === 'minor');
108
- const majorOptions = versionOptions.filter(v => v.type === 'major');
109
- if (minorOptions.length > 0) {
110
- console.log(colors.dim(' Minor Patches:'));
111
- for (const opt of minorOptions) {
112
- console.log(colors.brand(` ${opt.label}`));
113
- }
114
- console.log();
115
- }
116
- if (majorOptions.length > 0) {
117
- console.log(colors.dim(' Major Upgrades:'));
118
- for (const opt of majorOptions) {
119
- console.log(colors.action(` ${opt.label}`));
120
- }
121
- console.log();
122
- }
123
- // Interactive selection
124
- const selectedVersion = await selectVersion(versionOptions, frameworkName);
125
- if (!selectedVersion) {
126
- console.log();
127
- printInfo('Migration cancelled.');
128
- return;
129
- }
130
- targetVersion = selectedVersion.version;
131
- // Track: version_selected
132
- analytics.versionSelected({
133
- framework,
134
- currentVersion,
135
- targetVersion,
136
- isMajorUpgrade: selectedVersion.type === 'major',
137
- });
138
- console.log();
139
- console.log(colors.success(`✓ Selected: ${frameworkName} ${targetVersion}`));
140
- }
141
- catch (error) {
142
- spinner.fail('Failed to fetch versions');
143
- throw error;
144
- }
145
- }
146
- // Validate version format if provided directly
147
- if (!/^\d+(\.\d+)?(\.\d+)?$/.test(targetVersion)) {
148
- printError('Invalid version format. Use major version (e.g., "19") or semver (e.g., "19.0.0")');
149
- process.exit(1);
150
- }
151
- // Step 1: Get cost estimate from server (uses free audit endpoint)
152
- const costSpinner = createSpinner('Getting cost estimate...').start();
153
- let cost;
154
- let tierName;
155
- let packageCount;
156
- let auditData;
157
- try {
158
- const auditResponse = await apiClient.analyzeAudit(sanitized, framework);
159
- if (!auditResponse.success || !auditResponse.data) {
160
- costSpinner.fail('Failed to get cost estimate');
161
- throw new Error(auditResponse.error || 'Could not get cost estimate');
162
- }
163
- auditData = auditResponse.data;
164
- cost = auditData.cost;
165
- tierName = auditData.tierName;
166
- packageCount = auditData.totalPackages;
167
- costSpinner.succeed('Cost estimate ready');
168
- // Set project context for analytics
169
- analytics.setProjectContext({
170
- packageCount,
171
- framework,
172
- frameworkVersion: currentVersion,
173
- projectHash: analytics.hashProject(sanitized),
174
- });
175
- // Track: project_detected
176
- analytics.projectDetected({
177
- packageCount,
178
- framework,
179
- currentVersion,
180
- });
181
- }
182
- catch (error) {
183
- costSpinner.fail('Failed to get cost estimate');
184
- throw error;
91
+ // Fetch versions and let user select target
92
+ const targetVersion = await fetchAndSelectVersion(ctx);
93
+ if (!targetVersion) {
94
+ return; // User cancelled or already on latest
95
+ }
96
+ // Get cost estimate
97
+ const costEstimate = await getCostEstimate(ctx);
98
+ // Show project overview with migration plan
99
+ await showProjectOverview(ctx, costEstimate, targetVersion);
100
+ // Handle payment flow
101
+ const paymentFlow = new PaymentFlowService();
102
+ const paymentReady = await handleMigrationPaymentFlow(ctx, costEstimate, paymentFlow, targetVersion);
103
+ if (!paymentReady.success) {
104
+ return;
185
105
  }
186
- // Show project overview with packages to migrate - smooth reveal
187
- const healthScore = auditData.healthScore || 0;
188
- const healthInfo = getHealthStatus(healthScore);
189
- console.log();
190
- await sleep(100);
191
- console.log(colors.whiteBold('📊 PROJECT OVERVIEW'));
192
- await sleep(80);
193
- console.log(colors.gray('─'.repeat(50)));
194
- await sleep(120);
195
- console.log(`${colors.whiteBold('🏥 Health:')} ${renderHealthBar(healthScore)} ${healthInfo.color.bold(`${healthScore}/100`)} (${healthInfo.color(healthInfo.text)})`);
196
- await sleep(100);
197
- console.log(`${colors.whiteBold('📦 Packages:')} ${colors.brand(`${packageCount}`)} to migrate`);
198
- await sleep(150);
199
- console.log();
200
- console.log(colors.whiteBold('🚀 MIGRATION PLAN:'));
201
- await sleep(80);
202
- console.log(colors.dim(` ${frameworkName} ${currentVersion || '?'} → ${targetVersion}`));
203
- await sleep(60);
204
- console.log(colors.dim(' All dependencies will be aligned to the target version.'));
205
- // Migration highlight box - smooth reveal
206
- const targetMajor = parseInt(targetVersion.split('.')[0], 10);
207
- const majorJump = targetMajor - currentMajor;
208
- await sleep(150);
209
- console.log();
210
- // Box with 49 visible chars between borders
211
- // Using simple ASCII to avoid emoji width issues
212
- console.log(colors.brandBold('┌─────────────────────────────────────────────────┐'));
213
- await sleep(40);
214
- console.log(colors.brandBold('│') + colors.whiteBold(' MIGRATION HIGHLIGHT ') + colors.brandBold('│'));
215
- await sleep(40);
216
- console.log(colors.brandBold('├─────────────────────────────────────────────────┤'));
217
- await sleep(60);
218
- if (majorJump > 1) {
219
- console.log(colors.brandBold('│') + colors.warning(` ${majorJump} major versions jump - Full ecosystem update `.padEnd(49)) + colors.brandBold('│'));
106
+ // Run migration analysis
107
+ const migrationResult = await runMigrationAnalysis(ctx, targetVersion, costEstimate);
108
+ // Deduct credits and get solution
109
+ const fixResult = await paymentFlow.deductCredits(migrationResult.analysisId, paymentReady.hasActivePass);
110
+ if (!fixResult.success || !fixResult.solution) {
111
+ throw new Error(fixResult.error || 'Unknown error');
220
112
  }
221
- else if (majorJump === 1) {
222
- console.log(colors.brandBold('│') + colors.success(' Single major version upgrade ') + colors.brandBold('│'));
113
+ // Save session as PAID
114
+ await saveMigrationSession(ctx, migrationResult.analysisId, targetVersion, costEstimate);
115
+ // Show full migration plan
116
+ const changes = ctx.packageJsonService.getChanges(ctx.parsed, fixResult.solution);
117
+ const removals = mapRemovals(fixResult.solution.removals);
118
+ await showMigrationPlan(ctx, costEstimate, targetVersion, migrationResult.data, fixResult.solution, changes, removals);
119
+ // Ask to apply and execute
120
+ await applyMigrationFix(ctx, changes, removals, fixResult.solution, targetVersion);
121
+ }
122
+ catch (error) {
123
+ await handleMigrationError(error);
124
+ }
125
+ }
126
+ // ============================================================================
127
+ // INITIALIZATION
128
+ // ============================================================================
129
+ /**
130
+ * Initialize migration context with API-based framework detection
131
+ */
132
+ async function initializeMigrationContext(projectDir, options) {
133
+ const packageJsonService = new PackageJsonService();
134
+ const apiClient = new ApiClient();
135
+ const sessionManager = new SessionManager(projectDir);
136
+ // Read and sanitize package.json
137
+ const { content: packageJsonContent, parsed } = await packageJsonService.read(projectDir);
138
+ const sanitized = packageJsonService.sanitize(parsed);
139
+ // Detect framework using server API (more accurate than local detection)
140
+ let framework;
141
+ let currentVersion;
142
+ let currentMajor = 0;
143
+ const spinner = createSpinner('Detecting framework...').start();
144
+ try {
145
+ const detectResponse = await apiClient.detectFramework(sanitized);
146
+ if (detectResponse.success && detectResponse.data) {
147
+ framework = detectResponse.data.name;
148
+ currentVersion = detectResponse.data.version;
149
+ currentMajor = detectResponse.data.majorVersion || 0;
150
+ spinner.succeed(`Detected ${framework} ${currentVersion}`);
223
151
  }
224
152
  else {
225
- console.log(colors.brandBold('│') + colors.success(' Minor/patch update - Low risk ') + colors.brandBold('│'));
226
- }
227
- await sleep(50);
228
- console.log(colors.brandBold('│') + colors.dim(' - TypeScript alignment included ') + colors.brandBold('│'));
229
- await sleep(50);
230
- console.log(colors.brandBold('│') + colors.dim(' - Peer dependency conflicts auto-resolved ') + colors.brandBold('│'));
231
- await sleep(50);
232
- console.log(colors.brandBold('│') + colors.dim(' - Deprecated packages flagged for removal ') + colors.brandBold('│'));
233
- await sleep(40);
234
- console.log(colors.brandBold('└─────────────────────────────────────────────────┘'));
235
- // Payment flow - check auth and balance BEFORE running migration analysis
236
- const paymentFlow = new PaymentFlowService();
237
- // Step 2: Check authentication
238
- console.log();
239
- const authResult = await paymentFlow.ensureAuthenticated();
240
- if (!authResult.success) {
241
- console.log();
242
- printInfo('Run `npx depfixer migrate` when ready to continue.');
243
- return;
153
+ spinner.fail('Framework detection failed');
244
154
  }
245
- // Get balance info (needed for pass check and user display)
246
- const balanceInfo = await paymentFlow.getBalanceInfo();
247
- // Show user details if already logged in (if not already shown during login)
248
- if (authResult.wasAlreadyLoggedIn && balanceInfo) {
249
- printUserDetails({
250
- name: balanceInfo.name,
251
- email: balanceInfo.email,
252
- credits: balanceInfo.credits,
253
- hasActivePass: balanceInfo.hasActivePass,
254
- showHeader: false,
255
- });
256
- }
257
- console.log();
258
- // Step 3: Show cost and confirm payment
259
- const hasPass = balanceInfo?.hasActivePass;
260
- printCostBox({
261
- cost,
262
- tierName,
263
- prompt: hasPass ? 'Execute migration? (Enter/Esc)' : 'Execute migration? (Enter/Esc)',
264
- isMigration: true,
265
- hasActivePass: hasPass,
266
- });
267
- // Track: migration_prompt_shown
268
- analytics.migrationPromptShown({
269
- creditsNeeded: cost,
270
- creditsAvailable: balanceInfo?.credits || 0,
271
- tier: tierName,
272
- hasActivePass: hasPass,
273
- targetVersion,
155
+ }
156
+ catch (error) {
157
+ spinner.fail('Framework detection failed');
158
+ // If API fails, framework will be undefined
159
+ }
160
+ if (!framework) {
161
+ printError('Could not detect framework. Migration requires Angular, React, or Vue project.');
162
+ printInfo('Note: Next.js, React Native, Expo, and Svelte are not supported for migration.');
163
+ process.exit(1);
164
+ }
165
+ const frameworkName = framework.charAt(0).toUpperCase() + framework.slice(1);
166
+ // Calculate package.json hash
167
+ const packageJsonHash = sessionManager.calculateHash(packageJsonContent);
168
+ return {
169
+ projectDir,
170
+ options,
171
+ packageJsonService,
172
+ apiClient,
173
+ sessionManager,
174
+ packageJsonContent,
175
+ parsed,
176
+ sanitized,
177
+ framework,
178
+ frameworkName,
179
+ currentVersion,
180
+ currentMajor,
181
+ packageJsonHash,
182
+ };
183
+ }
184
+ // ============================================================================
185
+ // VERSION SELECTION
186
+ // ============================================================================
187
+ /**
188
+ * Fetch available versions and let user select target
189
+ */
190
+ async function fetchAndSelectVersion(ctx) {
191
+ const { apiClient, sanitized, framework, frameworkName, currentVersion, currentMajor, options } = ctx;
192
+ const spinner = createSpinner('Fetching available versions...').start();
193
+ try {
194
+ const versionsResponse = await apiClient.getFrameworkVersions(framework, currentMajor);
195
+ // Check if already on latest version
196
+ if (!versionsResponse.success) {
197
+ const errorMsg = versionsResponse.error || '';
198
+ if (errorMsg.includes('No newer versions') || errorMsg.includes('No supported versions')) {
199
+ spinner.succeed('Version check complete');
200
+ await showLatestVersionInfo(apiClient, sanitized, framework, frameworkName, currentVersion);
201
+ return null;
202
+ }
203
+ spinner.fail('Failed to fetch versions');
204
+ throw new Error(errorMsg || 'Could not fetch available versions');
205
+ }
206
+ spinner.succeed('Versions loaded');
207
+ // Track: versions_loaded
208
+ analytics.versionsLoaded({
209
+ framework,
210
+ currentVersion,
211
+ groupCount: versionsResponse.data?.groups?.length || 0,
274
212
  });
275
- const shouldExecute = options.yes || await promptYesNo('');
276
- if (!shouldExecute) {
277
- // Track: migration_rejected
278
- analytics.migrationRejected({ reason: 'user_cancelled' });
213
+ const groups = versionsResponse.data?.groups;
214
+ if (!groups || groups.length === 0) {
215
+ await showLatestVersionInfo(apiClient, sanitized, framework, frameworkName, currentVersion);
216
+ return null;
217
+ }
218
+ // Build version options
219
+ const versionOptions = buildVersionOptions(groups, frameworkName, currentMajor);
220
+ // Show version selector
221
+ displayVersionOptions(versionOptions);
222
+ // Interactive selection
223
+ const selectedVersion = await selectVersion(versionOptions, frameworkName, options);
224
+ if (!selectedVersion) {
279
225
  console.log();
280
226
  printInfo('Migration cancelled.');
281
- return;
282
- }
283
- // Track: migration_accepted
284
- analytics.migrationAccepted({ creditsDeducted: hasPass ? 0 : cost });
285
- // Step 4: Check balance
286
- const readyToPay = await paymentFlow.ensureSufficientBalance(cost);
287
- if (!readyToPay) {
288
- console.log();
289
- printInfo('Run `npx depfixer migrate` when ready to continue.');
290
- return;
291
- }
292
- // Step 5: Run migration analysis
293
- console.log();
294
- console.log(colors.whiteBold(`🔄 Analyzing Migration → ${frameworkName} ${targetVersion}`));
295
- console.log(colors.gray('─'.repeat(50)));
296
- const packageJsonHash = sessionManager.calculateHash(packageJsonContent);
297
- const migrationSteps = [
298
- 'Analyzing current state...',
299
- `Mapping migration path: ${currentVersion || '?'} → ${targetVersion}`,
300
- 'Checking ecosystem compatibility...',
301
- 'Calculating optimal versions...',
302
- 'Resolving dependency conflicts...',
303
- 'Building migration plan...',
304
- ];
305
- let response;
306
- await runStepSequence(migrationSteps, async () => {
307
- response = await apiClient.analyzeMigrate(sanitized, targetVersion, framework);
308
- if (!response.success || !response.data) {
309
- throw new Error(response.error || 'Unknown error');
310
- }
311
- }, { successMessage: 'Migration plan ready', minStepDuration: 200 });
312
- const data = response.data;
313
- const { analysisId } = data;
314
- // Track: migration_plan_ready
315
- analytics.migrationPlanReady({
316
- analysisId,
317
- targetVersion,
318
- conflictCount: (data.conflicts || []).length,
227
+ return null;
228
+ }
229
+ // Track: version_selected
230
+ analytics.versionSelected({
231
+ framework,
232
+ currentVersion,
233
+ targetVersion: selectedVersion.version,
234
+ isMajorUpgrade: selectedVersion.type === 'major',
319
235
  });
320
- // Step 6: Deduct credits and get solution (payment happens HERE)
321
- const fixResult = await paymentFlow.deductCredits(analysisId, hasPass);
322
- if (!fixResult.success || !fixResult.solution) {
323
- throw new Error(fixResult.error || 'Unknown error');
236
+ console.log();
237
+ console.log(colors.success(`✓ Selected: ${frameworkName} ${selectedVersion.version}`));
238
+ // Validate version format
239
+ if (!/^\d+(\.\d+)?(\.\d+)?$/.test(selectedVersion.version)) {
240
+ printError('Invalid version format. Use major version (e.g., "19") or semver (e.g., "19.0.0")');
241
+ process.exit(1);
324
242
  }
325
- // Extract solution for type safety in closures
326
- const solution = fixResult.solution;
327
- // Save session as PAID
328
- await sessionManager.saveSession({
329
- analysisId,
330
- intent: 'MIGRATE',
331
- args: { target: targetVersion },
332
- originalFileHash: packageJsonHash,
333
- cost,
334
- status: 'PAID',
335
- projectName: sanitized.name || 'unnamed',
336
- packageCount,
337
- tierName,
338
- });
339
- // Step 7: Show FULL migration plan (user already paid)
340
- // Get package changes first
341
- const changes = packageJsonService.getChanges(parsed, solution);
342
- const removals = (solution.removals || []).map(r => ({
343
- package: r.package,
344
- reason: r.reason || 'Deprecated',
345
- type: r.type,
346
- }));
347
- // Calculate health scores
348
- // Use audit healthScore for "before" (already shown in PROJECT OVERVIEW)
349
- // Use migration API response for "after" (projected)
350
- const currentHealthScore = healthScore; // From auditData (line 215)
351
- const projectedHealthScore = typeof data.healthScore === 'object'
352
- ? (data.healthScore.after || data.healthScore.projected || 0)
353
- : (typeof data.healthScore === 'number' ? data.healthScore : 0);
354
- const breakingChanges = (data.conflicts || []).filter((c) => c.severity === 'critical' || c.severity === 'high').length;
355
- // Show migration plan header - smooth reveal
356
- await sleep(100);
357
- printMigrationPlanHeader(frameworkName, currentVersion || '?', targetVersion);
358
- // Show projection stats
359
- await sleep(150);
360
- printProjectionStats({
361
- currentHealth: currentHealthScore,
362
- projectedHealth: projectedHealthScore,
363
- packageCount: changes.length,
364
- breakingChanges,
365
- });
366
- if (changes.length > 0) {
367
- await sleep(150);
368
- console.log();
369
- console.log(colors.whiteBold('📦 Package Updates:'));
370
- // Map and sort changes: major at top, then minor, then patch
371
- const mappedChanges = changes.map((c) => ({
372
- package: c.package,
373
- currentVersion: c.from,
374
- targetVersion: c.to,
375
- changeType: getChangeType(c.from, c.to),
376
- }));
377
- // Sort by change type: major > minor > patch > none
378
- const changeTypeOrder = { major: 0, minor: 1, patch: 2, none: 3 };
379
- mappedChanges.sort((a, b) => {
380
- const orderA = changeTypeOrder[a.changeType] ?? 3;
381
- const orderB = changeTypeOrder[b.changeType] ?? 3;
382
- return orderA - orderB;
243
+ return selectedVersion.version;
244
+ }
245
+ catch (error) {
246
+ spinner.fail('Failed to fetch versions');
247
+ throw error;
248
+ }
249
+ }
250
+ /**
251
+ * Build version options from API response groups
252
+ */
253
+ function buildVersionOptions(groups, frameworkName, currentMajor) {
254
+ const versionOptions = [];
255
+ for (const group of groups) {
256
+ const latestInGroup = group.options[0];
257
+ if (latestInGroup) {
258
+ const major = parseInt(latestInGroup.value.split('.')[0], 10);
259
+ const isMajorUpgrade = major > currentMajor;
260
+ versionOptions.push({
261
+ version: latestInGroup.value,
262
+ major,
263
+ label: `${frameworkName} ${latestInGroup.value}` + (latestInGroup.badge ? ` (${latestInGroup.badge})` : ''),
264
+ type: isMajorUpgrade ? 'major' : 'minor',
265
+ isLatest: latestInGroup.badge === 'Latest',
383
266
  });
384
- // Show table rows with smooth reveal
385
- const tableLines = createMigrationTable(mappedChanges).split('\n');
386
- for (const line of tableLines) {
387
- console.log(line);
388
- await sleep(30);
389
- }
390
267
  }
391
- // Show engine requirements (from solution.engines)
392
- if (solution.engines && Object.keys(solution.engines).length > 0) {
393
- await sleep(100);
394
- console.log();
395
- console.log(colors.whiteBold('⚙️ Engine Requirements:'));
396
- if (solution.engines.node) {
397
- console.log(` ${colors.dim('•')} Node.js: ${colors.brand(solution.engines.node)}`);
398
- }
399
- if (solution.engines.npm) {
400
- console.log(` ${colors.dim('•')} npm: ${colors.brand(solution.engines.npm)}`);
401
- }
402
- }
403
- // Show packages to add (missing peer dependencies)
404
- const packagesToAdd = (data.conflicts || []).filter((c) => !c.currentVersion || c.currentVersion.toLowerCase() === 'not installed');
405
- if (packagesToAdd.length > 0) {
406
- await sleep(100);
407
- console.log();
408
- console.log(colors.whiteBold('📦 Packages to Add:'));
409
- for (const pkg of packagesToAdd) {
410
- await sleep(60);
411
- // Build message from requiredBy if available
412
- let message;
413
- if (pkg.requiredBy && Array.isArray(pkg.requiredBy) && pkg.requiredBy.length > 0) {
414
- const requiredRange = pkg.requiredRange || 'required version';
415
- message = `Required as peer dependency by ${pkg.requiredBy.join(', ')} (${requiredRange})`;
416
- }
417
- else if (pkg.isPeerDependency && pkg.requiredRange) {
418
- message = `Missing peer dependency (${pkg.requiredRange})`;
419
- }
420
- else {
421
- message = pkg.description || 'Required dependency';
422
- }
423
- console.log(` ${colors.brand('+')} ${pkg.package}`);
424
- console.log(` ${colors.dim(message)}`);
425
- }
426
- }
427
- // Show packages to remove (using table format)
428
- if (removals.length > 0) {
429
- await sleep(100);
430
- console.log();
431
- console.log(colors.whiteBold('🗑 Packages to Remove:'));
432
- const removalLines = createMigrationTable(removals.map(r => ({
433
- package: r.package,
434
- currentVersion: '*',
435
- targetVersion: 'REMOVE',
436
- changeType: 'deprec',
437
- isRemoval: true,
438
- }))).split('\n');
439
- for (const line of removalLines) {
440
- console.log(line);
441
- await sleep(30);
442
- }
268
+ }
269
+ return versionOptions;
270
+ }
271
+ /**
272
+ * Display version options grouped by type
273
+ */
274
+ function displayVersionOptions(versionOptions) {
275
+ console.log();
276
+ console.log(colors.whiteBold('🎯 Select target version:'));
277
+ console.log();
278
+ const minorOptions = versionOptions.filter(v => v.type === 'minor');
279
+ const majorOptions = versionOptions.filter(v => v.type === 'major');
280
+ if (minorOptions.length > 0) {
281
+ console.log(colors.dim(' Minor Patches:'));
282
+ for (const opt of minorOptions) {
283
+ console.log(colors.brand(` ${opt.label}`));
443
284
  }
444
- // Step 8: Ask to apply (FREE - already paid)
445
- await sleep(200);
446
- console.log();
447
- // Explain what will be changed
448
- console.log(colors.gray('────────────────────────────────────────'));
449
- await sleep(50);
450
- console.log(colors.gray(' 📝 Only ') + colors.white('package.json') + colors.gray(' will be modified'));
451
- await sleep(50);
452
- console.log(colors.gray(' 💾 A backup (') + colors.white('package.json.bak') + colors.gray(') will be created'));
453
- await sleep(50);
454
- console.log(colors.gray('────────────────────────────────────────'));
455
- await sleep(100);
456
285
  console.log();
457
- // Track: migration_apply_prompt
458
- analytics.migrationApplyPrompt({
459
- changeCount: changes.length,
460
- removalCount: removals.length,
461
- });
462
- console.log(colors.gray('[?] Apply changes now? (Enter/Esc)'));
463
- const shouldApplyFix = options.yes || await promptYesNo('');
464
- if (!shouldApplyFix) {
465
- // Track: migration_deferred
466
- analytics.migrationDeferred({ reason: 'user_declined_apply' });
467
- console.log();
468
- printInfo('Solution saved. Run `npx depfixer fix` anytime to apply.');
469
- return;
286
+ }
287
+ if (majorOptions.length > 0) {
288
+ console.log(colors.dim(' Major Upgrades:'));
289
+ for (const opt of majorOptions) {
290
+ console.log(colors.action(` ${opt.label}`));
470
291
  }
471
- // Step 9: Apply fix (FREE) - smooth reveal
472
- await sleep(150);
473
292
  console.log();
474
- console.log(colors.whiteBold(`🔧 Applying Migration...`));
475
- await sleep(80);
476
- console.log(colors.gray('─'.repeat(50)));
477
- const upgradeCount = changes.length;
478
- const removalCount = removals.length;
479
- const migrationFixSteps = [
480
- 'Reading package.json...',
481
- `Applying ${upgradeCount} upgrade${upgradeCount !== 1 ? 's' : ''}...`,
482
- 'Resolving peer conflicts...',
483
- removalCount > 0 ? `Removing ${removalCount} deprecated package${removalCount !== 1 ? 's' : ''}...` : 'Checking for deprecated packages...',
484
- 'Validating final state...',
485
- 'Writing package.json...',
486
- ];
487
- let applyResult;
488
- await sleep(100);
489
- await runStepSequence(migrationFixSteps, async () => {
490
- applyResult = await packageJsonService.applySurgicalFixes(projectDir, changes, removals, solution.engines);
491
- }, { successMessage: 'Migration complete', minStepDuration: 150 });
492
- const { backupPath, applied, removed, enginesUpdated } = applyResult;
493
- // Show success using design system - smooth reveal
494
- await sleep(200);
495
- printSuccessBox({
496
- updated: applied,
497
- removed,
498
- backupPath,
499
- enginesUpdated,
500
- });
501
- // Track: migration_applied
502
- analytics.migrationApplied({
503
- updatedCount: applied,
504
- removedCount: removed,
505
- enginesUpdated,
506
- targetVersion,
293
+ }
294
+ }
295
+ // ============================================================================
296
+ // COST ESTIMATION
297
+ // ============================================================================
298
+ /**
299
+ * Get cost estimate from server
300
+ */
301
+ async function getCostEstimate(ctx) {
302
+ const { apiClient, sanitized, framework, currentVersion } = ctx;
303
+ const costSpinner = createSpinner('Getting cost estimate...').start();
304
+ try {
305
+ const auditResponse = await apiClient.analyzeAudit(sanitized, framework);
306
+ if (!auditResponse.success || !auditResponse.data) {
307
+ costSpinner.fail('Failed to get cost estimate');
308
+ throw new Error(auditResponse.error || 'Could not get cost estimate');
309
+ }
310
+ const auditData = auditResponse.data;
311
+ costSpinner.succeed('Cost estimate ready');
312
+ // Set project context for analytics
313
+ analytics.setProjectContext({
314
+ packageCount: auditData.totalPackages,
315
+ framework,
316
+ frameworkVersion: currentVersion,
317
+ projectHash: analytics.hashProject(sanitized),
507
318
  });
508
- // Track: session_ended (successful migration)
509
- await analytics.sessionEnded({
510
- outcome: 'migration_applied',
511
- targetVersion,
319
+ // Track: project_detected
320
+ analytics.projectDetected({
321
+ packageCount: auditData.totalPackages,
322
+ framework,
323
+ currentVersion,
512
324
  });
325
+ return {
326
+ cost: auditData.cost,
327
+ tierName: auditData.tierName,
328
+ packageCount: auditData.totalPackages,
329
+ healthScore: auditData.healthScore || 0,
330
+ auditData,
331
+ };
513
332
  }
514
333
  catch (error) {
515
- // Track: session_ended (error)
516
- await analytics.sessionEnded({
517
- outcome: 'error',
518
- error: error.message,
334
+ costSpinner.fail('Failed to get cost estimate');
335
+ throw error;
336
+ }
337
+ }
338
+ // ============================================================================
339
+ // PROJECT OVERVIEW
340
+ // ============================================================================
341
+ /**
342
+ * Show project overview with migration plan
343
+ */
344
+ async function showProjectOverview(ctx, costEstimate, targetVersion) {
345
+ const { frameworkName, currentVersion, currentMajor } = ctx;
346
+ const { packageCount, healthScore } = costEstimate;
347
+ const healthInfo = getHealthStatus(healthScore);
348
+ console.log();
349
+ await sleep(100);
350
+ console.log(colors.whiteBold('📊 PROJECT OVERVIEW'));
351
+ await sleep(80);
352
+ console.log(colors.gray('─'.repeat(50)));
353
+ await sleep(120);
354
+ console.log(`${colors.whiteBold('🏥 Health:')} ${renderHealthBar(healthScore)} ${healthInfo.color.bold(`${healthScore}/100`)} (${healthInfo.color(healthInfo.text)})`);
355
+ await sleep(100);
356
+ console.log(`${colors.whiteBold('📦 Packages:')} ${colors.brand(`${packageCount}`)} to migrate`);
357
+ await sleep(150);
358
+ console.log();
359
+ console.log(colors.whiteBold('🚀 MIGRATION PLAN:'));
360
+ await sleep(80);
361
+ console.log(colors.dim(` ${frameworkName} ${currentVersion || '?'} → ${targetVersion}`));
362
+ await sleep(60);
363
+ console.log(colors.dim(' All dependencies will be aligned to the target version.'));
364
+ // Migration highlight box
365
+ const targetMajor = parseInt(targetVersion.split('.')[0], 10);
366
+ const majorJump = targetMajor - currentMajor;
367
+ await displayMigrationHighlight(majorJump);
368
+ }
369
+ /**
370
+ * Display migration highlight box
371
+ */
372
+ async function displayMigrationHighlight(majorJump) {
373
+ await sleep(150);
374
+ console.log();
375
+ console.log(colors.brandBold('┌─────────────────────────────────────────────────┐'));
376
+ await sleep(40);
377
+ console.log(colors.brandBold('│') + colors.whiteBold(' MIGRATION HIGHLIGHT ') + colors.brandBold('│'));
378
+ await sleep(40);
379
+ console.log(colors.brandBold('├─────────────────────────────────────────────────┤'));
380
+ await sleep(60);
381
+ if (majorJump > 1) {
382
+ console.log(colors.brandBold('│') + colors.warning(` ${majorJump} major versions jump - Full ecosystem update `.padEnd(49)) + colors.brandBold('│'));
383
+ }
384
+ else if (majorJump === 1) {
385
+ console.log(colors.brandBold('│') + colors.success(' Single major version upgrade ') + colors.brandBold('│'));
386
+ }
387
+ else {
388
+ console.log(colors.brandBold('│') + colors.success(' Minor/patch update - Low risk ') + colors.brandBold('│'));
389
+ }
390
+ await sleep(50);
391
+ console.log(colors.brandBold('│') + colors.dim(' - TypeScript alignment included ') + colors.brandBold('│'));
392
+ await sleep(50);
393
+ console.log(colors.brandBold('│') + colors.dim(' - Peer dependency conflicts auto-resolved ') + colors.brandBold('│'));
394
+ await sleep(50);
395
+ console.log(colors.brandBold('│') + colors.dim(' - Deprecated packages flagged for removal ') + colors.brandBold('│'));
396
+ await sleep(40);
397
+ console.log(colors.brandBold('└─────────────────────────────────────────────────┘'));
398
+ }
399
+ /**
400
+ * Handle migration payment flow
401
+ */
402
+ async function handleMigrationPaymentFlow(ctx, costEstimate, paymentFlow, targetVersion) {
403
+ const { cost, tierName } = costEstimate;
404
+ const { options } = ctx;
405
+ // Check authentication
406
+ console.log();
407
+ const authResult = await paymentFlow.ensureAuthenticated();
408
+ if (!authResult.success) {
409
+ console.log();
410
+ printInfo('Run `npx depfixer migrate` when ready to continue.');
411
+ return { success: false, hasActivePass: false };
412
+ }
413
+ // Get balance info
414
+ const balanceInfo = await paymentFlow.getBalanceInfo();
415
+ // Show user details if already logged in
416
+ if (authResult.wasAlreadyLoggedIn && balanceInfo) {
417
+ printUserDetails({
418
+ name: balanceInfo.name,
419
+ email: balanceInfo.email,
420
+ credits: balanceInfo.credits,
421
+ hasActivePass: balanceInfo.hasActivePass,
422
+ showHeader: false,
519
423
  });
520
- printError(error.message);
521
- process.exit(1);
522
424
  }
425
+ console.log();
426
+ // Show cost and confirm payment
427
+ const hasPass = balanceInfo?.hasActivePass;
428
+ printCostBox({
429
+ cost,
430
+ tierName,
431
+ prompt: hasPass ? 'Execute migration? (Enter/Esc)' : 'Execute migration? (Enter/Esc)',
432
+ isMigration: true,
433
+ hasActivePass: hasPass,
434
+ });
435
+ // Track: migration_prompt_shown
436
+ analytics.migrationPromptShown({
437
+ creditsNeeded: cost,
438
+ creditsAvailable: balanceInfo?.credits || 0,
439
+ tier: tierName,
440
+ hasActivePass: hasPass,
441
+ targetVersion,
442
+ });
443
+ const shouldExecute = await confirmPrompt('', options);
444
+ if (!shouldExecute) {
445
+ analytics.migrationRejected({ reason: 'user_cancelled' });
446
+ console.log();
447
+ printInfo('Migration cancelled.');
448
+ return { success: false, hasActivePass: hasPass || false };
449
+ }
450
+ analytics.migrationAccepted({ creditsDeducted: hasPass ? 0 : cost });
451
+ // Check balance
452
+ const readyToPay = await paymentFlow.ensureSufficientBalance(cost);
453
+ if (!readyToPay) {
454
+ console.log();
455
+ printInfo('Run `npx depfixer migrate` when ready to continue.');
456
+ return { success: false, hasActivePass: hasPass || false };
457
+ }
458
+ return { success: true, hasActivePass: hasPass || false };
459
+ }
460
+ /**
461
+ * Run migration analysis
462
+ */
463
+ async function runMigrationAnalysis(ctx, targetVersion, costEstimate) {
464
+ const { apiClient, sanitized, framework, frameworkName, currentVersion } = ctx;
465
+ console.log();
466
+ console.log(colors.whiteBold(`🔄 Analyzing Migration → ${frameworkName} ${targetVersion}`));
467
+ console.log(colors.gray('─'.repeat(50)));
468
+ const migrationSteps = [
469
+ 'Analyzing current state...',
470
+ `Mapping migration path: ${currentVersion || '?'} → ${targetVersion}`,
471
+ 'Checking ecosystem compatibility...',
472
+ 'Calculating optimal versions...',
473
+ 'Resolving dependency conflicts...',
474
+ 'Building migration plan...',
475
+ ];
476
+ let response;
477
+ await runStepSequence(migrationSteps, async () => {
478
+ response = await apiClient.analyzeMigrate(sanitized, targetVersion, framework);
479
+ if (!response.success || !response.data) {
480
+ throw new Error(response.error || 'Unknown error');
481
+ }
482
+ }, { successMessage: 'Migration plan ready', minStepDuration: 200 });
483
+ const data = response.data;
484
+ // Track: migration_plan_ready
485
+ analytics.migrationPlanReady({
486
+ analysisId: data.analysisId,
487
+ targetVersion,
488
+ conflictCount: (data.conflicts || []).length,
489
+ });
490
+ return {
491
+ analysisId: data.analysisId,
492
+ data,
493
+ };
494
+ }
495
+ // ============================================================================
496
+ // SESSION MANAGEMENT
497
+ // ============================================================================
498
+ /**
499
+ * Save migration session
500
+ */
501
+ async function saveMigrationSession(ctx, analysisId, targetVersion, costEstimate) {
502
+ const { sessionManager, sanitized, packageJsonHash } = ctx;
503
+ const { cost, tierName, packageCount } = costEstimate;
504
+ await sessionManager.saveSession({
505
+ analysisId,
506
+ intent: 'MIGRATE',
507
+ args: { target: targetVersion },
508
+ originalFileHash: packageJsonHash,
509
+ cost,
510
+ status: 'PAID',
511
+ projectName: sanitized.name || 'unnamed',
512
+ packageCount,
513
+ tierName,
514
+ });
515
+ }
516
+ // ============================================================================
517
+ // MIGRATION PLAN DISPLAY
518
+ // ============================================================================
519
+ /**
520
+ * Map removals to expected format
521
+ */
522
+ function mapRemovals(removals) {
523
+ return (removals || []).map(r => ({
524
+ package: r.package,
525
+ reason: r.reason || 'Deprecated',
526
+ type: r.type,
527
+ }));
523
528
  }
529
+ /**
530
+ * Show full migration plan
531
+ */
532
+ async function showMigrationPlan(ctx, costEstimate, targetVersion, migrationData, solution, changes, removals) {
533
+ const { frameworkName, currentVersion } = ctx;
534
+ const { healthScore } = costEstimate;
535
+ // Calculate projected health score
536
+ const projectedHealthScore = typeof migrationData.healthScore === 'object'
537
+ ? (migrationData.healthScore.after || migrationData.healthScore.projected || 0)
538
+ : (typeof migrationData.healthScore === 'number' ? migrationData.healthScore : 0);
539
+ const breakingChanges = (migrationData.conflicts || []).filter((c) => c.severity === 'critical' || c.severity === 'high').length;
540
+ // Show migration plan header
541
+ await sleep(100);
542
+ printMigrationPlanHeader(frameworkName, currentVersion || '?', targetVersion);
543
+ // Show projection stats
544
+ await sleep(150);
545
+ printProjectionStats({
546
+ currentHealth: healthScore,
547
+ projectedHealth: projectedHealthScore,
548
+ packageCount: changes.length,
549
+ breakingChanges,
550
+ });
551
+ // Show package updates
552
+ if (changes.length > 0) {
553
+ await displayPackageUpdates(changes);
554
+ }
555
+ // Show engine requirements
556
+ await displayEngineRequirements(solution);
557
+ // Show packages to add
558
+ await displayPackagesToAdd(migrationData);
559
+ // Show packages to remove
560
+ await displayRemovals(removals);
561
+ }
562
+ /**
563
+ * Display package updates table
564
+ */
565
+ async function displayPackageUpdates(changes) {
566
+ await sleep(150);
567
+ console.log();
568
+ console.log(colors.whiteBold('📦 Package Updates:'));
569
+ // Map and sort changes
570
+ const mappedChanges = changes.map((c) => ({
571
+ package: c.package,
572
+ currentVersion: c.from,
573
+ targetVersion: c.to,
574
+ changeType: getChangeType(c.from, c.to),
575
+ }));
576
+ const changeTypeOrder = { major: 0, minor: 1, patch: 2, none: 3 };
577
+ mappedChanges.sort((a, b) => {
578
+ const orderA = changeTypeOrder[a.changeType] ?? 3;
579
+ const orderB = changeTypeOrder[b.changeType] ?? 3;
580
+ return orderA - orderB;
581
+ });
582
+ const tableLines = createMigrationTable(mappedChanges).split('\n');
583
+ for (const line of tableLines) {
584
+ console.log(line);
585
+ await sleep(30);
586
+ }
587
+ }
588
+ /**
589
+ * Display engine requirements
590
+ */
591
+ async function displayEngineRequirements(solution) {
592
+ if (!solution.engines || Object.keys(solution.engines).length === 0)
593
+ return;
594
+ await sleep(100);
595
+ console.log();
596
+ console.log(colors.whiteBold('⚙️ Engine Requirements:'));
597
+ if (solution.engines.node) {
598
+ console.log(` ${colors.dim('•')} Node.js: ${colors.brand(solution.engines.node)}`);
599
+ }
600
+ if (solution.engines.npm) {
601
+ console.log(` ${colors.dim('•')} npm: ${colors.brand(solution.engines.npm)}`);
602
+ }
603
+ }
604
+ /**
605
+ * Display packages to add
606
+ */
607
+ async function displayPackagesToAdd(migrationData) {
608
+ const packagesToAdd = (migrationData.conflicts || []).filter((c) => !c.currentVersion || c.currentVersion.toLowerCase() === 'not installed');
609
+ if (packagesToAdd.length === 0)
610
+ return;
611
+ await sleep(100);
612
+ console.log();
613
+ console.log(colors.whiteBold('📦 Packages to Add:'));
614
+ for (const pkg of packagesToAdd) {
615
+ await sleep(60);
616
+ let message;
617
+ if (pkg.requiredBy && Array.isArray(pkg.requiredBy) && pkg.requiredBy.length > 0) {
618
+ const requiredRange = pkg.requiredRange || 'required version';
619
+ message = `Required as peer dependency by ${pkg.requiredBy.join(', ')} (${requiredRange})`;
620
+ }
621
+ else if (pkg.isPeerDependency && pkg.requiredRange) {
622
+ message = `Missing peer dependency (${pkg.requiredRange})`;
623
+ }
624
+ else {
625
+ message = pkg.description || 'Required dependency';
626
+ }
627
+ console.log(` ${colors.brand('+')} ${pkg.package}`);
628
+ console.log(` ${colors.dim(message)}`);
629
+ }
630
+ }
631
+ /**
632
+ * Display removals
633
+ */
634
+ async function displayRemovals(removals) {
635
+ if (removals.length === 0)
636
+ return;
637
+ await sleep(100);
638
+ console.log();
639
+ console.log(colors.whiteBold('🗑 Packages to Remove:'));
640
+ const removalLines = createMigrationTable(removals.map(r => ({
641
+ package: r.package,
642
+ currentVersion: '*',
643
+ targetVersion: 'REMOVE',
644
+ changeType: 'deprec',
645
+ isRemoval: true,
646
+ }))).split('\n');
647
+ for (const line of removalLines) {
648
+ console.log(line);
649
+ await sleep(30);
650
+ }
651
+ }
652
+ // ============================================================================
653
+ // FIX APPLICATION
654
+ // ============================================================================
655
+ /**
656
+ * Apply migration fix to package.json
657
+ */
658
+ async function applyMigrationFix(ctx, changes, removals, solution, targetVersion) {
659
+ const { projectDir, packageJsonService, options } = ctx;
660
+ // Show what will be changed
661
+ await sleep(200);
662
+ console.log();
663
+ console.log(colors.gray('────────────────────────────────────────'));
664
+ await sleep(50);
665
+ console.log(colors.gray(' 📝 Only ') + colors.white('package.json') + colors.gray(' will be modified'));
666
+ await sleep(50);
667
+ console.log(colors.gray(' 💾 A backup (') + colors.white('package.json.bak') + colors.gray(') will be created'));
668
+ await sleep(50);
669
+ console.log(colors.gray('────────────────────────────────────────'));
670
+ await sleep(100);
671
+ console.log();
672
+ // Track: migration_apply_prompt
673
+ analytics.migrationApplyPrompt({
674
+ changeCount: changes.length,
675
+ removalCount: removals.length,
676
+ });
677
+ console.log(colors.gray('[?] Apply changes now? (Enter/Esc)'));
678
+ const shouldApplyFix = await confirmPrompt('', options);
679
+ if (!shouldApplyFix) {
680
+ analytics.migrationDeferred({ reason: 'user_declined_apply' });
681
+ console.log();
682
+ printInfo('Solution saved. Run `npx depfixer fix` anytime to apply.');
683
+ return;
684
+ }
685
+ // Apply fix
686
+ await executeMigrationFix(projectDir, packageJsonService, changes, removals, solution, targetVersion);
687
+ }
688
+ /**
689
+ * Execute migration fix
690
+ */
691
+ async function executeMigrationFix(projectDir, packageJsonService, changes, removals, solution, targetVersion) {
692
+ await sleep(150);
693
+ console.log();
694
+ console.log(colors.whiteBold(`🔧 Applying Migration...`));
695
+ await sleep(80);
696
+ console.log(colors.gray('─'.repeat(50)));
697
+ const upgradeCount = changes.length;
698
+ const removalCount = removals.length;
699
+ const migrationFixSteps = [
700
+ 'Reading package.json...',
701
+ `Applying ${upgradeCount} upgrade${upgradeCount !== 1 ? 's' : ''}...`,
702
+ 'Resolving peer conflicts...',
703
+ removalCount > 0 ? `Removing ${removalCount} deprecated package${removalCount !== 1 ? 's' : ''}...` : 'Checking for deprecated packages...',
704
+ 'Validating final state...',
705
+ 'Writing package.json...',
706
+ ];
707
+ let applyResult;
708
+ await sleep(100);
709
+ await runStepSequence(migrationFixSteps, async () => {
710
+ applyResult = await packageJsonService.applySurgicalFixes(projectDir, changes, removals, solution.engines);
711
+ }, { successMessage: 'Migration complete', minStepDuration: 150 });
712
+ const { backupPath, applied, removed, enginesUpdated } = applyResult;
713
+ // Show success
714
+ await sleep(200);
715
+ printSuccessBox({
716
+ updated: applied,
717
+ removed,
718
+ backupPath,
719
+ enginesUpdated,
720
+ });
721
+ // Track: migration_applied
722
+ analytics.migrationApplied({
723
+ updatedCount: applied,
724
+ removedCount: removed,
725
+ enginesUpdated,
726
+ targetVersion,
727
+ });
728
+ // Track: session_ended
729
+ await analytics.sessionEnded({
730
+ outcome: 'migration_applied',
731
+ targetVersion,
732
+ });
733
+ }
734
+ // ============================================================================
735
+ // VERSION SELECTOR
736
+ // ============================================================================
524
737
  /**
525
738
  * Interactive version selector using arrow keys
526
739
  */
527
- async function selectVersion(options, frameworkName) {
528
- // Auto-select first option in dev mode when env var is set
529
- if (isAutoConfirmEnabled() && options.length > 0) {
740
+ async function selectVersion(options, frameworkName, migrateOptions) {
741
+ // Auto-select first option in dev mode or with --yes flag
742
+ if ((migrateOptions.yes || isAutoConfirmEnabled()) && options.length > 0) {
530
743
  console.log(`${colors.dim(' Auto-selecting:')} ${colors.brand(options[0].label)} ${colors.dim('[auto]')}`);
531
744
  return options[0];
532
745
  }
@@ -601,240 +814,13 @@ async function selectVersion(options, frameworkName) {
601
814
  process.stdin.on('data', onKeyPress);
602
815
  });
603
816
  }
604
- /**
605
- * Check if auto-confirm is enabled (dev mode only)
606
- */
607
- function isAutoConfirmEnabled() {
608
- const isDevMode = process.env.NODE_ENV === 'development' ||
609
- (process.env.DEPFIXER_API_URL?.includes('localhost') ?? false);
610
- return isDevMode && process.env.DEPFIXER_AUTO_CONFIRM === 'true';
611
- }
612
- /**
613
- * Simple Enter/Esc prompt (Enter = Yes, Esc = No)
614
- */
615
- async function promptYesNo(question) {
616
- // Auto-confirm in dev mode when env var is set
617
- if (isAutoConfirmEnabled()) {
618
- if (question) {
619
- console.log(`${question} ${colors.dim('(Enter/Esc)')} ${colors.success('Yes')} ${colors.dim('[auto]')}`);
620
- }
621
- else {
622
- console.log(`${colors.success('Yes')} ${colors.dim('[auto]')}`);
623
- }
624
- return true;
625
- }
626
- return new Promise((resolve) => {
627
- if (question) {
628
- process.stdout.write(`${question} ${colors.dim('(Enter/Esc)')} `);
629
- }
630
- if (process.stdin.isTTY) {
631
- process.stdin.setRawMode(true);
632
- }
633
- process.stdin.resume();
634
- const onKeyPress = (key) => {
635
- const char = key.toString();
636
- if (char === '\r' || char === '\n') {
637
- cleanup();
638
- console.log(colors.success('Yes'));
639
- resolve(true);
640
- }
641
- else if (char === '\x1b') {
642
- cleanup();
643
- console.log(colors.danger('No'));
644
- resolve(false);
645
- }
646
- else if (char.toLowerCase() === 'y') {
647
- cleanup();
648
- console.log(colors.success('Yes'));
649
- resolve(true);
650
- }
651
- else if (char.toLowerCase() === 'n') {
652
- cleanup();
653
- console.log(colors.danger('No'));
654
- resolve(false);
655
- }
656
- else if (char === '\x03') {
657
- cleanup();
658
- process.exit(0);
659
- }
660
- };
661
- const cleanup = () => {
662
- process.stdin.removeListener('data', onKeyPress);
663
- if (process.stdin.isTTY) {
664
- process.stdin.setRawMode(false);
665
- }
666
- process.stdin.pause();
667
- };
668
- process.stdin.on('data', onKeyPress);
669
- });
670
- }
671
- /**
672
- * Simple client-side framework detection
673
- */
674
- function detectFramework(pkg) {
675
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
676
- if (deps['@angular/core'])
677
- return 'angular';
678
- if (deps['react'])
679
- return 'react';
680
- if (deps['vue'])
681
- return 'vue';
682
- if (deps['svelte'])
683
- return 'svelte';
684
- if (deps['next'])
685
- return 'react';
686
- if (deps['nuxt'])
687
- return 'vue';
688
- return undefined;
689
- }
690
- /**
691
- * Detect current framework version
692
- */
693
- function detectCurrentVersion(pkg, framework) {
694
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
695
- switch (framework) {
696
- case 'angular':
697
- return extractVersion(deps['@angular/core']);
698
- case 'react':
699
- return extractVersion(deps['react']);
700
- case 'vue':
701
- return extractVersion(deps['vue']);
702
- default:
703
- return undefined;
704
- }
705
- }
706
- /**
707
- * Extract clean version from semver string
708
- */
709
- function extractVersion(version) {
710
- if (!version)
711
- return undefined;
712
- const cleaned = version.replace(/[~^]/g, '');
713
- return cleaned;
714
- }
715
- /**
716
- * Determine change type for migration display
717
- */
718
- function getChangeType(current, target) {
719
- if (!current || !target)
720
- return 'none';
721
- const currentMajor = parseInt(current.replace(/[~^]/g, '').split('.')[0], 10);
722
- const targetMajor = parseInt(target.replace(/[~^]/g, '').split('.')[0], 10);
723
- if (isNaN(currentMajor) || isNaN(targetMajor))
724
- return 'none';
725
- if (targetMajor > currentMajor)
726
- return 'major';
727
- const currentMinor = parseInt(current.replace(/[~^]/g, '').split('.')[1] || '0', 10);
728
- const targetMinor = parseInt(target.replace(/[~^]/g, '').split('.')[1] || '0', 10);
729
- if (targetMinor > currentMinor)
730
- return 'minor';
731
- return 'patch';
732
- }
733
- /**
734
- * Get meaningful reason for migration
735
- */
736
- function getMigrationReason(packageName, framework) {
737
- // Core framework packages
738
- if (framework === 'angular') {
739
- if (packageName.startsWith('@angular/')) {
740
- return 'Core framework';
741
- }
742
- if (packageName === 'zone.js') {
743
- return 'Angular requirement';
744
- }
745
- if (packageName === 'rxjs') {
746
- return 'Angular dependency';
747
- }
748
- if (packageName === 'typescript') {
749
- return 'Build requirement';
750
- }
751
- }
752
- if (framework === 'react') {
753
- if (packageName === 'react' || packageName === 'react-dom') {
754
- return 'Core framework';
755
- }
756
- if (packageName.startsWith('@types/react')) {
757
- return 'Type definitions';
758
- }
759
- }
760
- // Type definitions
761
- if (packageName.startsWith('@types/')) {
762
- return 'Type definitions';
763
- }
764
- // Build tools
765
- if (['typescript', 'webpack', 'esbuild', 'vite', 'rollup'].includes(packageName)) {
766
- return 'Build tool';
767
- }
768
- // Testing
769
- if (['jest', 'karma', 'jasmine', 'mocha', 'vitest'].some(t => packageName.includes(t))) {
770
- return 'Testing';
771
- }
772
- // Linting
773
- if (packageName.includes('eslint') || packageName.includes('prettier')) {
774
- return 'Code quality';
775
- }
776
- return 'Compatibility';
777
- }
778
- /**
779
- * Wrap text at specified width
780
- */
781
- function wrapText(text, maxWidth) {
782
- if (!text)
783
- return [];
784
- const words = text.split(' ');
785
- const lines = [];
786
- let currentLine = '';
787
- for (const word of words) {
788
- if (currentLine.length + word.length + 1 <= maxWidth) {
789
- currentLine += (currentLine ? ' ' : '') + word;
790
- }
791
- else {
792
- if (currentLine)
793
- lines.push(currentLine);
794
- currentLine = word;
795
- }
796
- }
797
- if (currentLine)
798
- lines.push(currentLine);
799
- return lines;
800
- }
801
- /**
802
- * Create teaser table (limited info, no solutions)
803
- * Shows only severity and package name with generic issue type
804
- */
805
- function createTeaserTable(conflicts) {
806
- const lines = [];
807
- lines.push(colors.gray('┌────────────┬─────────────────────────┬────────────────┐'));
808
- lines.push(colors.gray('│') + colors.whiteBold(' SEVERITY ') + colors.gray('│') + colors.whiteBold(' PACKAGE ') + colors.gray('│') + colors.whiteBold(' ISSUE ') + colors.gray('│'));
809
- lines.push(colors.gray('├────────────┼─────────────────────────┼────────────────┤'));
810
- for (const conflict of conflicts) {
811
- const severity = conflict.severity?.toUpperCase() || 'UNKNOWN';
812
- const severityColor = severity === 'CRITICAL' ? colors.dangerBold :
813
- severity === 'HIGH' ? colors.danger :
814
- severity === 'MEDIUM' ? colors.warning :
815
- colors.dim;
816
- // Truncate package name if too long
817
- const pkg = (conflict.package || '').substring(0, 23).padEnd(23);
818
- // Generic issue description (no details)
819
- const issueType = severity === 'CRITICAL' ? 'Peer Clash' :
820
- severity === 'HIGH' ? 'Version Gap' :
821
- severity === 'MEDIUM' ? 'Conflict' : 'Minor';
822
- lines.push(colors.gray('│') +
823
- ` ${severityColor(severity.padEnd(10))}` +
824
- colors.gray('│') +
825
- ` ${pkg}` +
826
- colors.gray('│') +
827
- ` ${issueType.padEnd(14)}` +
828
- colors.gray('│'));
829
- }
830
- lines.push(colors.gray('└────────────┴─────────────────────────┴────────────────┘'));
831
- return lines.join('\n');
832
- }
817
+ // ============================================================================
818
+ // HELPER FUNCTIONS
819
+ // ============================================================================
833
820
  /**
834
821
  * Show project info when already on latest version
835
822
  */
836
823
  async function showLatestVersionInfo(apiClient, sanitized, framework, frameworkName, currentVersion) {
837
- // Get audit data for health info
838
824
  try {
839
825
  const auditResponse = await apiClient.analyzeAudit(sanitized, framework);
840
826
  if (auditResponse.success && auditResponse.data) {
@@ -852,7 +838,6 @@ async function showLatestVersionInfo(apiClient, sanitized, framework, frameworkN
852
838
  console.log();
853
839
  console.log(colors.successBold('✓ You\'re already on the latest supported version!'));
854
840
  console.log(colors.dim(' No migration needed.'));
855
- // If there are issues, suggest running analyze
856
841
  if (issueCount > 0) {
857
842
  console.log();
858
843
  console.log(colors.warningBold(`⚠️ ${issueCount} issue${issueCount !== 1 ? 's' : ''} detected in your dependencies`));
@@ -871,4 +856,15 @@ async function showLatestVersionInfo(apiClient, sanitized, framework, frameworkN
871
856
  printSuccess(`You're already on the latest supported version of ${frameworkName}!`);
872
857
  }
873
858
  }
859
+ /**
860
+ * Handle migration error
861
+ */
862
+ async function handleMigrationError(error) {
863
+ await analytics.sessionEnded({
864
+ outcome: 'error',
865
+ error: error.message,
866
+ });
867
+ printError(error.message);
868
+ process.exit(1);
869
+ }
874
870
  //# sourceMappingURL=migrate.js.map