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.
- package/dist/commands/check.d.ts +8 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +742 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/migrate.d.ts +1 -7
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +702 -706
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/smart.d.ts.map +1 -1
- package/dist/commands/smart.js +954 -911
- package/dist/commands/smart.js.map +1 -1
- package/dist/constants/analysis.constants.d.ts +2 -0
- package/dist/constants/analysis.constants.d.ts.map +1 -1
- package/dist/constants/analysis.constants.js +9 -0
- package/dist/constants/analysis.constants.js.map +1 -1
- package/dist/index.js +57 -17
- package/dist/index.js.map +1 -1
- package/dist/services/api-client.d.ts +89 -0
- package/dist/services/api-client.d.ts.map +1 -1
- package/dist/services/api-client.js +95 -1
- package/dist/services/api-client.js.map +1 -1
- package/dist/services/framework-detector.d.ts +23 -0
- package/dist/services/framework-detector.d.ts.map +1 -0
- package/dist/services/framework-detector.js +230 -0
- package/dist/services/framework-detector.js.map +1 -0
- package/dist/services/payment-flow.d.ts +3 -1
- package/dist/services/payment-flow.d.ts.map +1 -1
- package/dist/services/payment-flow.js +8 -1
- package/dist/services/payment-flow.js.map +1 -1
- package/dist/utils/framework-utils.d.ts +29 -0
- package/dist/utils/framework-utils.d.ts.map +1 -0
- package/dist/utils/framework-utils.js +45 -0
- package/dist/utils/framework-utils.js.map +1 -0
- package/dist/utils/interactive-picker.d.ts +12 -0
- package/dist/utils/interactive-picker.d.ts.map +1 -0
- package/dist/utils/interactive-picker.js +109 -0
- package/dist/utils/interactive-picker.js.map +1 -0
- package/dist/utils/package-parser.d.ts +24 -0
- package/dist/utils/package-parser.d.ts.map +1 -0
- package/dist/utils/package-parser.js +63 -0
- package/dist/utils/package-parser.js.map +1 -0
- package/dist/utils/prompt.d.ts +13 -0
- package/dist/utils/prompt.d.ts.map +1 -0
- package/dist/utils/prompt.js +71 -0
- package/dist/utils/prompt.js.map +1 -0
- package/dist/utils/table-builders.d.ts +40 -0
- package/dist/utils/table-builders.d.ts.map +1 -0
- package/dist/utils/table-builders.js +175 -0
- package/dist/utils/table-builders.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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
|