depfixer 1.1.8 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/commands/check.d.ts +8 -0
  2. package/dist/commands/check.d.ts.map +1 -0
  3. package/dist/commands/check.js +742 -0
  4. package/dist/commands/check.js.map +1 -0
  5. package/dist/commands/migrate.d.ts +1 -7
  6. package/dist/commands/migrate.d.ts.map +1 -1
  7. package/dist/commands/migrate.js +702 -706
  8. package/dist/commands/migrate.js.map +1 -1
  9. package/dist/commands/smart.d.ts.map +1 -1
  10. package/dist/commands/smart.js +954 -911
  11. package/dist/commands/smart.js.map +1 -1
  12. package/dist/constants/analysis.constants.d.ts +2 -0
  13. package/dist/constants/analysis.constants.d.ts.map +1 -1
  14. package/dist/constants/analysis.constants.js +9 -0
  15. package/dist/constants/analysis.constants.js.map +1 -1
  16. package/dist/index.js +57 -17
  17. package/dist/index.js.map +1 -1
  18. package/dist/services/api-client.d.ts +89 -0
  19. package/dist/services/api-client.d.ts.map +1 -1
  20. package/dist/services/api-client.js +95 -1
  21. package/dist/services/api-client.js.map +1 -1
  22. package/dist/services/framework-detector.d.ts +23 -0
  23. package/dist/services/framework-detector.d.ts.map +1 -0
  24. package/dist/services/framework-detector.js +230 -0
  25. package/dist/services/framework-detector.js.map +1 -0
  26. package/dist/services/payment-flow.d.ts +3 -1
  27. package/dist/services/payment-flow.d.ts.map +1 -1
  28. package/dist/services/payment-flow.js +8 -1
  29. package/dist/services/payment-flow.js.map +1 -1
  30. package/dist/utils/framework-utils.d.ts +29 -0
  31. package/dist/utils/framework-utils.d.ts.map +1 -0
  32. package/dist/utils/framework-utils.js +45 -0
  33. package/dist/utils/framework-utils.js.map +1 -0
  34. package/dist/utils/interactive-picker.d.ts +12 -0
  35. package/dist/utils/interactive-picker.d.ts.map +1 -0
  36. package/dist/utils/interactive-picker.js +109 -0
  37. package/dist/utils/interactive-picker.js.map +1 -0
  38. package/dist/utils/package-parser.d.ts +24 -0
  39. package/dist/utils/package-parser.d.ts.map +1 -0
  40. package/dist/utils/package-parser.js +63 -0
  41. package/dist/utils/package-parser.js.map +1 -0
  42. package/dist/utils/prompt.d.ts +13 -0
  43. package/dist/utils/prompt.d.ts.map +1 -0
  44. package/dist/utils/prompt.js +71 -0
  45. package/dist/utils/prompt.js.map +1 -0
  46. package/dist/utils/table-builders.d.ts +40 -0
  47. package/dist/utils/table-builders.d.ts.map +1 -0
  48. package/dist/utils/table-builders.js +175 -0
  49. package/dist/utils/table-builders.js.map +1 -0
  50. package/dist/version.d.ts +1 -1
  51. package/dist/version.js +1 -1
  52. package/package.json +23 -3
@@ -0,0 +1,742 @@
1
+ /**
2
+ * Check Command
3
+ *
4
+ * Pre-install compatibility check: verify if packages are compatible
5
+ * with your project's framework before installing them.
6
+ *
7
+ * Usage:
8
+ * npx depfixer check react # interactive: pick major, then exact
9
+ * npx depfixer check react@19 # interactive: pick exact 19.x
10
+ * npx depfixer check react@19.1.0 # direct check (no prompts)
11
+ * npx depfixer check react@19 react-dom@19 # multi-package: auto-resolve + confirm
12
+ * npx depfixer check @emotion/react@11 --json
13
+ * npx depfixer check react@19 --path ./my-app
14
+ * npx depfixer check react --yes # skip all prompts, pick latest
15
+ */
16
+ import chalk from 'chalk';
17
+ import Table from 'cli-table3';
18
+ import { ApiClient, NetworkError } from '../services/api-client.js';
19
+ import { PackageJsonService } from '../services/package-json.js';
20
+ import { detectFramework } from '../services/framework-detector.js';
21
+ import { parsePackageSpec } from '../utils/package-parser.js';
22
+ import { createSpinner, printError, printSuccess, printInfo, printWarning, } from '../utils/output.js';
23
+ import { colors, printCliHeader, printSectionHeader, } from '../utils/design-system.js';
24
+ import { pickOne } from '../utils/interactive-picker.js';
25
+ import { promptYesNo } from '../utils/prompt.js';
26
+ export async function checkCommand(packages, options, command) {
27
+ // Merge global options (--json, --path, --yes declared on program) into local options
28
+ // so users can pass these flags naturally: `depfixer check pkg@1 --json`
29
+ if (command?.parent) {
30
+ const globalOpts = command.parent.opts() || {};
31
+ options = { ...globalOpts, ...options };
32
+ }
33
+ const projectDir = options.path || process.cwd();
34
+ // Show header unless JSON mode
35
+ if (!options.json) {
36
+ printCliHeader();
37
+ console.log();
38
+ }
39
+ try {
40
+ // 1. Parse each package spec
41
+ const pendingSpecs = parsePackageSpecs(packages, options.json);
42
+ if (pendingSpecs.length === 0) {
43
+ return;
44
+ }
45
+ // 2. Read local package.json
46
+ const packageJsonService = new PackageJsonService();
47
+ let packageJson;
48
+ try {
49
+ const { parsed } = await packageJsonService.read(projectDir);
50
+ packageJson = parsed;
51
+ }
52
+ catch (err) {
53
+ if (options.json) {
54
+ console.log(JSON.stringify({
55
+ success: false,
56
+ error: err.message,
57
+ }));
58
+ }
59
+ else {
60
+ printError(err.message);
61
+ printInfo('Run this command from a directory with a package.json, or use --path <dir>');
62
+ }
63
+ process.exit(1);
64
+ return;
65
+ }
66
+ // 3. Detect framework + version from package.json
67
+ const framework = detectFramework(packageJson);
68
+ if (!framework) {
69
+ if (options.json) {
70
+ console.log(JSON.stringify({
71
+ success: false,
72
+ error: 'Could not detect a supported framework (Angular, React, or Vue) in your package.json',
73
+ }));
74
+ }
75
+ else {
76
+ printError('Could not detect a supported framework in your package.json');
77
+ printInfo('Supported frameworks: Angular, React, Vue');
78
+ printInfo('Make sure your package.json has the framework as a dependency');
79
+ }
80
+ process.exit(1);
81
+ return;
82
+ }
83
+ if (!options.json) {
84
+ printInfo(`Detected ${chalk.bold(framework.name)} v${chalk.bold(framework.version)} from ${chalk.dim(framework.detectedFrom)}`);
85
+ console.log();
86
+ }
87
+ // 3b. Framework-self check (only if a version was explicitly provided).
88
+ // If no version was provided, the user is asking us to resolve — fall through to resolution.
89
+ const frameworkSelfCheck = pendingSpecs.find(p => isFrameworkPackage(p.name, framework.name) &&
90
+ p.version && !isSameMajor(p.version, framework.version));
91
+ if (frameworkSelfCheck) {
92
+ if (options.json) {
93
+ console.log(JSON.stringify({
94
+ success: false,
95
+ error: `You're checking ${frameworkSelfCheck.name}@${frameworkSelfCheck.version} against your own ${framework.name} ${framework.version}. Use 'depfixer migrate ${framework.name}@${frameworkSelfCheck.version}' for a full migration analysis.`,
96
+ suggestion: `depfixer migrate ${framework.name}@${frameworkSelfCheck.version}`,
97
+ }));
98
+ }
99
+ else {
100
+ printWarning(`You're checking ${chalk.bold(frameworkSelfCheck.name + '@' + frameworkSelfCheck.version)} against your own ${framework.name} ${framework.version}`);
101
+ console.log();
102
+ printInfo(`For framework upgrades, use ${chalk.bold('migrate')} — it ${chalk.bold('previews')} the upgrade and shows:`);
103
+ console.log(` ${chalk.dim('• Which packages will break')}`);
104
+ console.log(` ${chalk.dim('• Breaking changes & migration steps')}`);
105
+ console.log(` ${chalk.dim('• Recommended versions')}`);
106
+ console.log();
107
+ console.log(` ${colors.brand('npx depfixer migrate')} ${chalk.dim('# read-only analysis — does NOT modify your code')}`);
108
+ console.log(` ${colors.brand('npx depfixer fix')} ${chalk.dim('# applies the changes after you review them')}`);
109
+ console.log();
110
+ printInfo(`The ${chalk.bold('check')} command is for third-party packages (e.g. ${chalk.dim('react-router, @emotion/react')}).`);
111
+ }
112
+ process.exit(1);
113
+ return;
114
+ }
115
+ // 4. Version resolution phase
116
+ const apiClient = new ApiClient();
117
+ let resolved;
118
+ try {
119
+ resolved = await resolveVersions(pendingSpecs, apiClient, options);
120
+ }
121
+ catch (err) {
122
+ if (err instanceof NetworkError) {
123
+ if (options.json) {
124
+ console.log(JSON.stringify({ success: false, error: err.message }));
125
+ }
126
+ else {
127
+ printError(err.message);
128
+ }
129
+ }
130
+ else {
131
+ if (options.json) {
132
+ console.log(JSON.stringify({ success: false, error: err.message }));
133
+ }
134
+ else {
135
+ printError(err.message || 'Failed to resolve package versions');
136
+ }
137
+ }
138
+ process.exit(1);
139
+ return;
140
+ }
141
+ if (resolved.length === 0) {
142
+ // Cancelled by user
143
+ if (!options.json) {
144
+ printInfo('Cancelled.');
145
+ }
146
+ process.exit(1);
147
+ return;
148
+ }
149
+ // 5. Call API: POST /compatibility/check-exact
150
+ const spinner = options.json ? null : createSpinner('Checking compatibility...').start();
151
+ let result;
152
+ try {
153
+ result = await apiClient.checkCompatibility(resolved.map(p => ({ name: p.name, version: p.version })), framework.name, framework.version);
154
+ }
155
+ catch (err) {
156
+ if (spinner)
157
+ spinner.fail('Compatibility check failed');
158
+ if (err instanceof NetworkError) {
159
+ if (options.json) {
160
+ console.log(JSON.stringify({ success: false, error: err.message }));
161
+ }
162
+ else {
163
+ printError(err.message);
164
+ }
165
+ }
166
+ else {
167
+ if (options.json) {
168
+ console.log(JSON.stringify({ success: false, error: err.message }));
169
+ }
170
+ else {
171
+ printError(err.message || 'Failed to check compatibility');
172
+ }
173
+ }
174
+ process.exit(1);
175
+ return;
176
+ }
177
+ if (spinner) {
178
+ spinner.stop();
179
+ }
180
+ if (!result.success) {
181
+ if (options.json) {
182
+ console.log(JSON.stringify(result));
183
+ }
184
+ else {
185
+ printError(result.reason || result.error || 'Compatibility check failed');
186
+ }
187
+ process.exit(1);
188
+ return;
189
+ }
190
+ // 6. Display results
191
+ const finalPackages = resolved.map(r => ({ name: r.name, version: r.version }));
192
+ if (options.json) {
193
+ printJsonOutput(result, framework, finalPackages);
194
+ }
195
+ else {
196
+ printHumanOutput(result, framework, finalPackages);
197
+ }
198
+ // Exit with code 1 if any package is incompatible (useful for CI)
199
+ if (!result.compatible) {
200
+ process.exit(1);
201
+ }
202
+ }
203
+ catch (err) {
204
+ if (options.json) {
205
+ console.log(JSON.stringify({ success: false, error: err.message }));
206
+ }
207
+ else {
208
+ printError(err.message);
209
+ }
210
+ process.exit(1);
211
+ }
212
+ }
213
+ /**
214
+ * Resolve pending specs into exact versions.
215
+ *
216
+ * Single package:
217
+ * - no version → pick major, pick exact
218
+ * - partial (no patch) → pick exact within major
219
+ * - exact → pass through
220
+ *
221
+ * Multi package:
222
+ * - auto-resolve partials to latest matching
223
+ * - show resolution summary + confirmation (unless --yes / --json)
224
+ */
225
+ async function resolveVersions(pendingSpecs, apiClient, options) {
226
+ const isSingle = pendingSpecs.length === 1;
227
+ if (isSingle) {
228
+ return resolveSinglePackage(pendingSpecs[0], apiClient, options);
229
+ }
230
+ return resolveMultiPackage(pendingSpecs, apiClient, options);
231
+ }
232
+ async function resolveSinglePackage(spec, apiClient, options) {
233
+ // Exact version → pass through
234
+ if (spec.version && isExactVersion(spec.version)) {
235
+ return [{
236
+ name: spec.name,
237
+ version: normalizeVersion(spec.version),
238
+ originalVersion: spec.version,
239
+ wasResolved: false,
240
+ }];
241
+ }
242
+ // --json mode: must have exact version
243
+ if (options.json) {
244
+ throw new Error(`Version required in --json mode for ${spec.name}. Provide an exact version like ${spec.name}@1.2.3`);
245
+ }
246
+ // --yes mode: auto-pick latest matching (or overall latest)
247
+ if (options.yes) {
248
+ const resolved = await autoResolveLatest(spec, apiClient);
249
+ printInfo(`Auto-resolved ${chalk.bold(spec.name + '@' + (spec.version || 'latest'))} → ${chalk.bold(resolved)}`);
250
+ console.log();
251
+ return [{
252
+ name: spec.name,
253
+ version: resolved,
254
+ originalVersion: spec.version,
255
+ wasResolved: true,
256
+ }];
257
+ }
258
+ // Interactive path
259
+ let major;
260
+ if (spec.version) {
261
+ // Partial version: extract major, skip major picker
262
+ const parsedMajor = parseMajor(spec.version);
263
+ if (parsedMajor === null) {
264
+ throw new Error(`Invalid version "${spec.version}" for ${spec.name}`);
265
+ }
266
+ major = parsedMajor;
267
+ }
268
+ else {
269
+ // No version: show major picker
270
+ const majorResponse = await apiClient.getPackageMajors(spec.name);
271
+ if (!majorResponse.success || !majorResponse.data) {
272
+ throw new Error(majorResponse.error || `No versions found for ${spec.name}`);
273
+ }
274
+ const majors = majorResponse.data.majors;
275
+ if (majors.length === 0) {
276
+ throw new Error(`No versions available for ${spec.name}`);
277
+ }
278
+ // Auto-skip major picker when only one major exists
279
+ if (majors.length === 1) {
280
+ major = majors[0].major;
281
+ printInfo(`Only one major version available: ${chalk.bold(major)}`);
282
+ console.log();
283
+ }
284
+ else {
285
+ const choices = majors.map((m) => {
286
+ const parts = [];
287
+ if (m.latest)
288
+ parts.push(`latest: ${m.latest}`);
289
+ parts.push(`${m.count} version${m.count !== 1 ? 's' : ''}`);
290
+ if (m.deprecated)
291
+ parts.push(chalk.red('deprecated'));
292
+ if (m.isLatest)
293
+ parts.push(chalk.green('current'));
294
+ return {
295
+ label: `${m.major}`,
296
+ value: m.major,
297
+ hint: `(${parts.join(', ')})`,
298
+ };
299
+ });
300
+ const picked = await pickOne(`Pick major version for ${chalk.bold(spec.name)}:`, choices);
301
+ if (picked === null)
302
+ return [];
303
+ major = picked;
304
+ }
305
+ }
306
+ // Now pick exact version within the major
307
+ const versionsResponse = await apiClient.getPackageVersionsForMajor(spec.name, major);
308
+ if (!versionsResponse.success || !versionsResponse.data) {
309
+ throw new Error(versionsResponse.error || `No ${major}.x versions found for ${spec.name}`);
310
+ }
311
+ const versions = versionsResponse.data.versions;
312
+ if (versions.length === 0) {
313
+ throw new Error(`No ${major}.x versions available for ${spec.name}`);
314
+ }
315
+ // Auto-skip version picker when only one version exists
316
+ if (versions.length === 1) {
317
+ const only = versions[0].version;
318
+ printInfo(`Only one ${major}.x version available: ${chalk.bold(only)}`);
319
+ console.log();
320
+ return [{
321
+ name: spec.name,
322
+ version: only,
323
+ originalVersion: spec.version,
324
+ wasResolved: true,
325
+ }];
326
+ }
327
+ const versionChoices = versions.map((v) => {
328
+ const hints = [];
329
+ if (v.isLatest)
330
+ hints.push(chalk.green('latest'));
331
+ if (v.deprecated)
332
+ hints.push(chalk.red('deprecated'));
333
+ if (v.publishedAt) {
334
+ const date = v.publishedAt.slice(0, 10);
335
+ hints.push(colors.dim(date));
336
+ }
337
+ return {
338
+ label: v.version,
339
+ value: v.version,
340
+ hint: hints.length > 0 ? `(${hints.join(', ')})` : undefined,
341
+ };
342
+ });
343
+ const pickedVersion = await pickOne(`Pick exact ${major}.x version for ${chalk.bold(spec.name)}:`, versionChoices);
344
+ if (pickedVersion === null)
345
+ return [];
346
+ return [{
347
+ name: spec.name,
348
+ version: pickedVersion,
349
+ originalVersion: spec.version,
350
+ wasResolved: true,
351
+ }];
352
+ }
353
+ async function resolveMultiPackage(pendingSpecs, apiClient, options) {
354
+ const resolvedList = [];
355
+ const resolutionLog = [];
356
+ for (const spec of pendingSpecs) {
357
+ if (spec.version && isExactVersion(spec.version)) {
358
+ resolvedList.push({
359
+ name: spec.name,
360
+ version: normalizeVersion(spec.version),
361
+ originalVersion: spec.version,
362
+ wasResolved: false,
363
+ });
364
+ continue;
365
+ }
366
+ if (options.json) {
367
+ throw new Error(`Version required in --json mode for ${spec.name}. Provide an exact version like ${spec.name}@1.2.3`);
368
+ }
369
+ const resolved = await autoResolveLatest(spec, apiClient);
370
+ resolvedList.push({
371
+ name: spec.name,
372
+ version: resolved,
373
+ originalVersion: spec.version,
374
+ wasResolved: true,
375
+ });
376
+ resolutionLog.push({
377
+ original: `${spec.name}@${spec.version || 'latest'}`,
378
+ resolved: `${spec.name}@${resolved}`,
379
+ });
380
+ }
381
+ // If nothing was auto-resolved, no need to show summary or confirm
382
+ if (resolutionLog.length === 0) {
383
+ return resolvedList;
384
+ }
385
+ if (options.json) {
386
+ // Already threw above for missing versions; exact-only multi falls through here.
387
+ return resolvedList;
388
+ }
389
+ // Show resolution summary
390
+ console.log(colors.brand('Auto-resolving partial versions:'));
391
+ for (const entry of resolutionLog) {
392
+ console.log(` ${chalk.green('✓')} ${entry.original} → ${chalk.bold(entry.resolved)}`);
393
+ }
394
+ console.log();
395
+ // Skip confirmation in --yes mode
396
+ if (options.yes) {
397
+ return resolvedList;
398
+ }
399
+ const confirmed = await promptYesNo('Check with these versions?');
400
+ if (confirmed) {
401
+ return resolvedList;
402
+ }
403
+ // User declined auto-resolve → let them pick per package
404
+ console.log();
405
+ printInfo('Customize versions for each package (press Enter to keep auto-resolved version):');
406
+ console.log();
407
+ const customized = [];
408
+ for (const spec of pendingSpecs) {
409
+ // Keep exact versions as-is
410
+ if (spec.version && isExactVersion(spec.version)) {
411
+ customized.push({
412
+ name: spec.name,
413
+ version: normalizeVersion(spec.version),
414
+ originalVersion: spec.version,
415
+ wasResolved: false,
416
+ });
417
+ continue;
418
+ }
419
+ const existing = resolvedList.find(r => r.name === spec.name);
420
+ const defaultVersion = existing?.version;
421
+ const picked = await pickVersionInteractive(spec, apiClient, defaultVersion);
422
+ if (picked === null) {
423
+ return []; // user cancelled
424
+ }
425
+ customized.push({
426
+ name: spec.name,
427
+ version: picked,
428
+ originalVersion: spec.version,
429
+ wasResolved: true,
430
+ });
431
+ }
432
+ return customized;
433
+ }
434
+ /**
435
+ * Interactive version picker for a single package (used in multi-package customize flow).
436
+ * If `defaultVersion` provided, it's highlighted as the default choice.
437
+ */
438
+ async function pickVersionInteractive(spec, apiClient, defaultVersion) {
439
+ let major;
440
+ if (spec.version) {
441
+ const parsedMajor = parseMajor(spec.version);
442
+ if (parsedMajor === null) {
443
+ throw new Error(`Invalid version "${spec.version}" for ${spec.name}`);
444
+ }
445
+ major = parsedMajor;
446
+ }
447
+ else {
448
+ // No version — pick major first
449
+ const majorResponse = await apiClient.getPackageMajors(spec.name);
450
+ if (!majorResponse.success || !majorResponse.data) {
451
+ throw new Error(majorResponse.error || `No versions found for ${spec.name}`);
452
+ }
453
+ const majors = majorResponse.data.majors;
454
+ if (majors.length === 0) {
455
+ throw new Error(`No versions available for ${spec.name}`);
456
+ }
457
+ if (majors.length === 1) {
458
+ major = majors[0].major;
459
+ }
460
+ else {
461
+ const majorChoices = majors.map(m => {
462
+ const parts = [];
463
+ if (m.latest)
464
+ parts.push(`latest: ${m.latest}`);
465
+ parts.push(`${m.count} version${m.count !== 1 ? 's' : ''}`);
466
+ if (m.deprecated)
467
+ parts.push(chalk.red('deprecated'));
468
+ if (m.isLatest)
469
+ parts.push(chalk.green('current'));
470
+ return { label: `${m.major}`, value: m.major, hint: `(${parts.join(', ')})` };
471
+ });
472
+ const picked = await pickOne(`Pick major for ${chalk.bold(spec.name)}:`, majorChoices);
473
+ if (picked === null)
474
+ return null;
475
+ major = picked;
476
+ }
477
+ }
478
+ const versionsResponse = await apiClient.getPackageVersionsForMajor(spec.name, major);
479
+ if (!versionsResponse.success || !versionsResponse.data) {
480
+ throw new Error(versionsResponse.error || `No ${major}.x versions found for ${spec.name}`);
481
+ }
482
+ const versions = versionsResponse.data.versions;
483
+ if (versions.length === 0) {
484
+ throw new Error(`No ${major}.x versions available for ${spec.name}`);
485
+ }
486
+ if (versions.length === 1) {
487
+ return versions[0].version;
488
+ }
489
+ const versionChoices = versions.map(v => {
490
+ const hints = [];
491
+ if (v.version === defaultVersion)
492
+ hints.push(chalk.cyan('auto-resolved'));
493
+ if (v.isLatest)
494
+ hints.push(chalk.green('latest'));
495
+ if (v.deprecated)
496
+ hints.push(chalk.red('deprecated'));
497
+ if (v.publishedAt)
498
+ hints.push(colors.dim(v.publishedAt.slice(0, 10)));
499
+ return {
500
+ label: v.version,
501
+ value: v.version,
502
+ hint: hints.length > 0 ? `(${hints.join(', ')})` : undefined,
503
+ };
504
+ });
505
+ // Find index of defaultVersion to pre-select it
506
+ const defaultIdx = defaultVersion
507
+ ? versionChoices.findIndex(c => c.value === defaultVersion)
508
+ : 0;
509
+ return pickOne(`Pick version for ${chalk.bold(spec.name + '@' + major + '.x')}:`, versionChoices, defaultIdx >= 0 ? defaultIdx : 0);
510
+ }
511
+ /**
512
+ * Auto-resolve to latest matching version.
513
+ * If spec has a partial version → latest within that major.
514
+ * If spec has no version → overall latest (most recent major's latest).
515
+ */
516
+ async function autoResolveLatest(spec, apiClient) {
517
+ if (spec.version && !isExactVersion(spec.version)) {
518
+ const major = parseMajor(spec.version);
519
+ if (major === null) {
520
+ throw new Error(`Invalid version "${spec.version}" for ${spec.name}`);
521
+ }
522
+ const versionsResponse = await apiClient.getPackageVersionsForMajor(spec.name, major);
523
+ if (!versionsResponse.success || !versionsResponse.data || versionsResponse.data.versions.length === 0) {
524
+ throw new Error(versionsResponse.error || `No ${major}.x versions found for ${spec.name}`);
525
+ }
526
+ return versionsResponse.data.versions[0].version; // sorted desc → latest
527
+ }
528
+ // No version: overall latest
529
+ const majorResponse = await apiClient.getPackageMajors(spec.name);
530
+ if (!majorResponse.success || !majorResponse.data || majorResponse.data.majors.length === 0) {
531
+ throw new Error(majorResponse.error || `No versions found for ${spec.name}`);
532
+ }
533
+ const latestMajor = majorResponse.data.majors[0]; // sorted desc
534
+ if (!latestMajor.latest) {
535
+ throw new Error(`No stable version found for ${spec.name}`);
536
+ }
537
+ return latestMajor.latest;
538
+ }
539
+ /**
540
+ * A version is "exact" if it has major.minor.patch (no ranges, no missing parts).
541
+ * We strip leading ^/~/= since those narrow to exact for our purposes when a full triplet follows.
542
+ * Actually — we treat anything with all three numeric parts as exact.
543
+ */
544
+ function isExactVersion(version) {
545
+ const cleaned = version.replace(/^[\^~=v]/, '').trim();
546
+ return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(cleaned);
547
+ }
548
+ function normalizeVersion(version) {
549
+ return version.replace(/^[\^~=v]/, '').trim();
550
+ }
551
+ function parseMajor(version) {
552
+ const cleaned = version.replace(/^[\^~=v]/, '').trim();
553
+ const match = cleaned.match(/^(\d+)/);
554
+ if (!match)
555
+ return null;
556
+ const n = parseInt(match[1], 10);
557
+ return isNaN(n) ? null : n;
558
+ }
559
+ /**
560
+ * Check if a package is the framework itself (e.g. "react" for React, "@angular/core" for Angular).
561
+ */
562
+ function isFrameworkPackage(packageName, frameworkName) {
563
+ const fw = frameworkName.toLowerCase();
564
+ const pkg = packageName.toLowerCase();
565
+ if (fw === 'react')
566
+ return pkg === 'react' || pkg === 'react-dom';
567
+ if (fw === 'vue')
568
+ return pkg === 'vue';
569
+ if (fw === 'angular')
570
+ return pkg === '@angular/core' || pkg === '@angular/cli';
571
+ if (fw === 'next' || fw === 'nextjs')
572
+ return pkg === 'next';
573
+ return false;
574
+ }
575
+ /**
576
+ * Compare major versions. Returns true if both versions have the same major.
577
+ */
578
+ function isSameMajor(version1, version2) {
579
+ const major1 = parseInt(version1.split('.')[0].replace(/[\^~]/, ''), 10);
580
+ const major2 = parseInt(version2.split('.')[0].replace(/[\^~]/, ''), 10);
581
+ if (isNaN(major1) || isNaN(major2))
582
+ return false;
583
+ return major1 === major2;
584
+ }
585
+ /**
586
+ * Parse and validate package specs from CLI arguments.
587
+ * Packages without a version are allowed — they will be resolved interactively.
588
+ */
589
+ function parsePackageSpecs(packages, jsonMode) {
590
+ const parsed = [];
591
+ const errors = [];
592
+ for (const spec of packages) {
593
+ try {
594
+ const result = parsePackageSpec(spec);
595
+ parsed.push({ name: result.name, version: result.version, raw: spec });
596
+ }
597
+ catch (err) {
598
+ errors.push(`${spec}: ${err.message}`);
599
+ }
600
+ }
601
+ if (errors.length > 0) {
602
+ if (jsonMode) {
603
+ console.log(JSON.stringify({
604
+ success: false,
605
+ error: 'Invalid package specs',
606
+ details: errors,
607
+ }));
608
+ }
609
+ else {
610
+ for (const error of errors) {
611
+ printError(error);
612
+ }
613
+ console.log();
614
+ printInfo('Usage: npx depfixer check <package>[@<version>] [<package>[@<version>] ...]');
615
+ printInfo('Examples:');
616
+ printInfo(' npx depfixer check react # interactive version picker');
617
+ printInfo(' npx depfixer check react@19 # pick exact 19.x version');
618
+ printInfo(' npx depfixer check react@19.1.0 # check exact version');
619
+ }
620
+ if (errors.length === packages.length) {
621
+ process.exit(1);
622
+ return [];
623
+ }
624
+ }
625
+ return parsed;
626
+ }
627
+ /**
628
+ * Print JSON output for --json mode
629
+ */
630
+ function printJsonOutput(result, framework, packages) {
631
+ const output = {
632
+ success: true,
633
+ framework: {
634
+ name: framework.name,
635
+ version: framework.version,
636
+ },
637
+ compatible: result.compatible,
638
+ packages: [],
639
+ executionTimeMs: result.executionTimeMs,
640
+ };
641
+ if (result.results && Array.isArray(result.results)) {
642
+ output.packages = result.results.map((r) => ({
643
+ name: r.packageName,
644
+ version: r.packageVersion,
645
+ compatible: r.compatible,
646
+ reason: r.reason,
647
+ details: r.details || null,
648
+ }));
649
+ }
650
+ console.log(JSON.stringify(output, null, 2));
651
+ }
652
+ /**
653
+ * Print human-readable output with colored indicators
654
+ */
655
+ function printHumanOutput(result, framework, packages) {
656
+ const frameworkLabel = `${framework.name}@${framework.version}`;
657
+ printSectionHeader(`Compatibility Check vs ${frameworkLabel}`, '');
658
+ const results = result.results || [];
659
+ if (results.length === 0) {
660
+ printWarning('No results returned from compatibility check');
661
+ return;
662
+ }
663
+ // Build results table
664
+ const table = new Table({
665
+ head: [
666
+ chalk.bold('Status'),
667
+ chalk.bold('Package'),
668
+ chalk.bold('Version'),
669
+ chalk.bold('Result'),
670
+ ],
671
+ colWidths: [10, 30, 14, 45],
672
+ wordWrap: true,
673
+ style: {
674
+ head: [],
675
+ border: ['gray'],
676
+ },
677
+ chars: {
678
+ 'mid': '', 'left-mid': '', 'mid-mid': '', 'right-mid': '',
679
+ },
680
+ });
681
+ let compatibleCount = 0;
682
+ let incompatibleCount = 0;
683
+ let unknownCount = 0;
684
+ for (const pkg of results) {
685
+ let statusIcon;
686
+ let reasonText;
687
+ if (pkg.compatible) {
688
+ statusIcon = chalk.green(' OK ');
689
+ reasonText = chalk.green(pkg.reason || 'Compatible');
690
+ compatibleCount++;
691
+ }
692
+ else if (pkg.reason && (pkg.reason.includes('Unknown') || pkg.reason.includes('No data'))) {
693
+ statusIcon = chalk.yellow(' ?? ');
694
+ reasonText = chalk.yellow(pkg.reason);
695
+ unknownCount++;
696
+ }
697
+ else {
698
+ statusIcon = chalk.red(' NO ');
699
+ reasonText = chalk.red(pkg.reason || 'Incompatible');
700
+ incompatibleCount++;
701
+ }
702
+ table.push([
703
+ statusIcon,
704
+ pkg.packageName,
705
+ pkg.packageVersion,
706
+ reasonText,
707
+ ]);
708
+ }
709
+ console.log(table.toString());
710
+ console.log();
711
+ // Summary line
712
+ const totalChecked = results.length;
713
+ if (result.compatible) {
714
+ printSuccess(`All ${totalChecked} package${totalChecked !== 1 ? 's' : ''} compatible with ${frameworkLabel}`);
715
+ console.log();
716
+ console.log(colors.success(' Safe to install:'));
717
+ console.log(colors.dim(` npm install ${packages.map(p => `${p.name}@${p.version}`).join(' ')}`));
718
+ }
719
+ else {
720
+ const parts = [];
721
+ if (compatibleCount > 0)
722
+ parts.push(chalk.green(`${compatibleCount} compatible`));
723
+ if (incompatibleCount > 0)
724
+ parts.push(chalk.red(`${incompatibleCount} incompatible`));
725
+ if (unknownCount > 0)
726
+ parts.push(chalk.yellow(`${unknownCount} unknown`));
727
+ printWarning(`${parts.join(', ')} with ${frameworkLabel}`);
728
+ if (incompatibleCount > 0) {
729
+ console.log();
730
+ console.log(chalk.yellow(' Review incompatible packages before installing.'));
731
+ console.log(chalk.dim(' Run a full analysis for recommended versions:'));
732
+ console.log(chalk.dim(' npx depfixer'));
733
+ }
734
+ }
735
+ // Execution time
736
+ if (result.executionTimeMs) {
737
+ console.log();
738
+ console.log(colors.dim(` Checked in ${result.executionTimeMs}ms`));
739
+ }
740
+ console.log();
741
+ }
742
+ //# sourceMappingURL=check.js.map