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