node-power-user 2.1.4 → 2.1.6
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/README.md +12 -3
- package/bin/node-power-user +8 -1
- package/dist/commands/install.js +6 -7
- package/dist/commands/outdated.js +239 -53
- package/dist/lib/npm.js +102 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -103,12 +103,21 @@ npu audit
|
|
|
103
103
|
Compare the versions of installed modules to those in your package.json. When you choose to update, the install step and a full post-install audit are both wrapped with Socket for supply chain protection.
|
|
104
104
|
```shell
|
|
105
105
|
npu outdated
|
|
106
|
-
npu out --
|
|
106
|
+
npu out --heal # skip the menu: reinstall copies that don't match package-lock.json
|
|
107
|
+
npu out --sync # skip the menu: install packages to match package.json
|
|
108
|
+
npu out -r # skip the menu: reconcile package.json to installed versions
|
|
109
|
+
npu out -P | -m | -M # skip the menu: apply patch / minor / major updates
|
|
110
|
+
npu out --force # bypass Socket protection
|
|
107
111
|
```
|
|
108
112
|
|
|
109
|
-
|
|
113
|
+
Every run starts with an integrity check: npu compares what `node_modules/.package-lock.json` claims is installed against the packages physically on disk — including transitive deps the table can't show. Desynced copies (stale or partially-extracted installs, typically left behind by an interrupted or Socket-blocked install) make npm silently no-op (`npm install` trusts the lockfile over the disk), so npu warns about them and offers to heal.
|
|
114
|
+
|
|
115
|
+
When problems are found, the menu offers context-aware actions:
|
|
116
|
+
- **Heal** — when disk copies don't match `package-lock.json`, removes them and reinstalls so reality matches the lockfile again.
|
|
110
117
|
- **Sync** — when `node_modules` is *behind* `package.json`, installs packages to match what `package.json` declares.
|
|
111
|
-
- **Reconcile** — when `node_modules` is *ahead* of `package.json`, updates `package.json` to match installed versions.
|
|
118
|
+
- **Reconcile** — when `node_modules` is *ahead* of `package.json`, updates `package.json` to match installed versions. Strictly ahead-only — it never downgrades `package.json` to match a stale install.
|
|
119
|
+
|
|
120
|
+
Installs remove the targeted `node_modules` copies first so npm actually re-fetches them (instead of trusting a stale lockfile and reporting "up to date"), then npu verifies the new versions physically landed in `node_modules`. If an install fails or Socket blocks it, both `package.json` and `package-lock.json` are restored — npu never leaves the lockfile advanced past the files on disk.
|
|
112
121
|
|
|
113
122
|
### List Packages
|
|
114
123
|
List all packages in your project.
|
package/bin/node-power-user
CHANGED
|
@@ -11,5 +11,12 @@ if (targetDir) {
|
|
|
11
11
|
const cli = new (require('../dist/cli.js'))();
|
|
12
12
|
(async function() {
|
|
13
13
|
'use strict';
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
// cli.process logs errors itself — exit non-zero without an unhandled
|
|
16
|
+
// rejection crash (which dumps noise from libraries with global handlers)
|
|
17
|
+
try {
|
|
18
|
+
await cli.process(argv);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
}
|
|
15
22
|
}());
|
package/dist/commands/install.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// Libraries
|
|
2
2
|
const logger = new (require('../lib/logger'))('node-power-user');
|
|
3
3
|
const socket = require('../lib/socket');
|
|
4
|
-
const
|
|
5
|
-
const path = require('path');
|
|
4
|
+
const npm = require('../lib/npm');
|
|
6
5
|
|
|
7
6
|
// Module
|
|
8
7
|
module.exports = async function (options) {
|
|
@@ -28,6 +27,8 @@ module.exports = async function (options) {
|
|
|
28
27
|
// remove existing node_modules copies first so npm actually re-fetches them
|
|
29
28
|
// instead of reporting "up to date" with a stale cached version.
|
|
30
29
|
if (packages.length > 0 && !flags.includes('--global')) {
|
|
30
|
+
const names = [];
|
|
31
|
+
|
|
31
32
|
for (const pkg of packages) {
|
|
32
33
|
// For scoped packages like @scope/name@version, skip the leading @
|
|
33
34
|
const searchFrom = pkg.startsWith('@') ? 1 : 0;
|
|
@@ -36,12 +37,10 @@ module.exports = async function (options) {
|
|
|
36
37
|
continue;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
const pkgDir = path.join(process.cwd(), 'node_modules', pkgName);
|
|
41
|
-
if (jetpack.exists(pkgDir)) {
|
|
42
|
-
jetpack.remove(pkgDir);
|
|
43
|
-
}
|
|
40
|
+
names.push(pkg.substring(0, versionIdx));
|
|
44
41
|
}
|
|
42
|
+
|
|
43
|
+
npm.removeInstalledCopies(names);
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
const command = packages.length > 0
|
|
@@ -8,6 +8,7 @@ const version = require('wonderful-version');
|
|
|
8
8
|
const inquirer = require('@inquirer/prompts');
|
|
9
9
|
const ncu = require('npm-check-updates');
|
|
10
10
|
const socket = require('../lib/socket');
|
|
11
|
+
const npm = require('../lib/npm');
|
|
11
12
|
const { execute } = require('node-powertools');
|
|
12
13
|
|
|
13
14
|
// Load package.json
|
|
@@ -16,6 +17,7 @@ const projectPath = process.cwd();
|
|
|
16
17
|
// Module
|
|
17
18
|
module.exports = async function (options) {
|
|
18
19
|
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
20
|
+
const lockfilePath = path.join(projectPath, 'package-lock.json');
|
|
19
21
|
let projectJson = jetpack.read(packageJsonPath, 'json');
|
|
20
22
|
|
|
21
23
|
// Check if package.json exists
|
|
@@ -34,6 +36,11 @@ module.exports = async function (options) {
|
|
|
34
36
|
// Log start
|
|
35
37
|
logger.log(`Checking packages for ${logger.format.bold(projectJson.name || 'Unknown Project')}...`);
|
|
36
38
|
|
|
39
|
+
// Detect desync between the hidden lockfile and the physical node_modules —
|
|
40
|
+
// desynced copies make npm silently no-op installs (it trusts the lockfile
|
|
41
|
+
// over the disk), which is what traps projects in reconcile/update loops
|
|
42
|
+
const desynced = npm.findDesynced(projectPath);
|
|
43
|
+
|
|
37
44
|
// Parse --ignore flag (comma-separated list of package names to skip)
|
|
38
45
|
const ignoreList = (options.ignore || '')
|
|
39
46
|
.split(',')
|
|
@@ -96,51 +103,64 @@ module.exports = async function (options) {
|
|
|
96
103
|
latestVersion,
|
|
97
104
|
type: getDependencyType(projectJson, dep),
|
|
98
105
|
hasDiscrepancy,
|
|
99
|
-
hasMajorUpdate: latestVersion &&
|
|
106
|
+
hasMajorUpdate: latestVersion && latestVersion.split('.')[0] !== packageVersion.split('.')[0],
|
|
100
107
|
});
|
|
101
108
|
}
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
// Display unified table
|
|
105
|
-
if (allPackages.size === 0) {
|
|
112
|
+
if (allPackages.size === 0 && desynced.length === 0) {
|
|
106
113
|
logger.log(logger.format.green('\nAll packages are up to date!'));
|
|
107
|
-
return { allPackages };
|
|
114
|
+
return { allPackages, desynced };
|
|
108
115
|
}
|
|
109
116
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (pkg.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
if (allPackages.size > 0) {
|
|
118
|
+
const dataTable = [['Package', 'package.json', 'Installed', 'Latest', 'Type']];
|
|
119
|
+
|
|
120
|
+
for (const pkg of allPackages.values()) {
|
|
121
|
+
const pkgVersionColor = pkg.hasDiscrepancy ? 'red' : 'green';
|
|
122
|
+
|
|
123
|
+
// Format latest version - show major updates in magenta
|
|
124
|
+
let latestDisplay;
|
|
125
|
+
if (pkg.latestVersion) {
|
|
126
|
+
if (pkg.hasMajorUpdate) {
|
|
127
|
+
latestDisplay = chalk.magenta(pkg.latestVersion + ' ⚠');
|
|
128
|
+
} else if (pkg.latestVersion !== pkg.installedVersion) {
|
|
129
|
+
latestDisplay = chalk.cyan(pkg.latestVersion);
|
|
130
|
+
} else {
|
|
131
|
+
latestDisplay = chalk.green(pkg.latestVersion);
|
|
132
|
+
}
|
|
122
133
|
} else {
|
|
123
|
-
latestDisplay = chalk.
|
|
134
|
+
latestDisplay = chalk.dim('-');
|
|
124
135
|
}
|
|
125
|
-
|
|
126
|
-
|
|
136
|
+
|
|
137
|
+
dataTable.push([
|
|
138
|
+
pkg.name,
|
|
139
|
+
chalk[pkgVersionColor](pkg.packageVersion),
|
|
140
|
+
chalk.green(pkg.installedVersion),
|
|
141
|
+
latestDisplay,
|
|
142
|
+
pkg.type,
|
|
143
|
+
]);
|
|
127
144
|
}
|
|
128
145
|
|
|
129
|
-
|
|
130
|
-
pkg.name,
|
|
131
|
-
chalk[pkgVersionColor](pkg.packageVersion),
|
|
132
|
-
chalk.green(pkg.installedVersion),
|
|
133
|
-
latestDisplay,
|
|
134
|
-
pkg.type,
|
|
135
|
-
]);
|
|
136
|
-
}
|
|
146
|
+
console.log(table(dataTable));
|
|
137
147
|
|
|
138
|
-
|
|
148
|
+
// Only show legend if there are major updates
|
|
149
|
+
const hasMajorUpdates = [...allPackages.values()].some(pkg => pkg.hasMajorUpdate);
|
|
150
|
+
if (hasMajorUpdates) {
|
|
151
|
+
logger.log(chalk.dim('Legend: ') + chalk.magenta('⚠ = major version (breaking changes)'));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
139
154
|
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
155
|
+
// Warn about desynced copies — includes transitive deps the table can't show
|
|
156
|
+
if (desynced.length > 0) {
|
|
157
|
+
logger.warn(`${desynced.length} package(s) on disk don't match what package-lock.json claims is installed (stale or partial installs):`);
|
|
158
|
+
for (const d of desynced.slice(0, 10)) {
|
|
159
|
+
logger.warn(` • ${d.loc.replace(/^node_modules\//, '')} — lockfile has ${d.lockfileVersion}, disk has ${d.diskVersion || 'nothing'}`);
|
|
160
|
+
}
|
|
161
|
+
if (desynced.length > 10) {
|
|
162
|
+
logger.warn(` …and ${desynced.length - 10} more`);
|
|
163
|
+
}
|
|
144
164
|
}
|
|
145
165
|
|
|
146
166
|
// Separate discrepancies into two categories:
|
|
@@ -166,18 +186,24 @@ module.exports = async function (options) {
|
|
|
166
186
|
);
|
|
167
187
|
|
|
168
188
|
// Check if major/latest offers any versions beyond what minor gives
|
|
169
|
-
const hasMajorBeyondMinor = Object.keys(latestUpgrades).some(dep =>
|
|
170
|
-
|
|
171
|
-
|
|
189
|
+
const hasMajorBeyondMinor = Object.keys(latestUpgrades).some(dep => {
|
|
190
|
+
const latestMajor = version.clean(latestUpgrades[dep]).split('.')[0];
|
|
191
|
+
const currentMajor = version.clean(allDependencies[dep]).split('.')[0];
|
|
192
|
+
return latestMajor !== currentMajor;
|
|
193
|
+
});
|
|
172
194
|
|
|
173
195
|
// If noPrompt, return data without showing menu (used by tests)
|
|
174
196
|
if (options.noPrompt) {
|
|
175
|
-
return { allPackages };
|
|
197
|
+
return { allPackages, desynced };
|
|
176
198
|
}
|
|
177
199
|
|
|
178
200
|
// Check for shortcut flags (skip menu)
|
|
179
201
|
let action = null;
|
|
180
|
-
if (options.
|
|
202
|
+
if (options.heal) {
|
|
203
|
+
action = 'heal';
|
|
204
|
+
} else if (options.sync) {
|
|
205
|
+
action = 'sync';
|
|
206
|
+
} else if (options.r || options.reconcile) {
|
|
181
207
|
action = 'reconcile';
|
|
182
208
|
} else if (options.P || options.patch) {
|
|
183
209
|
action = 'patch';
|
|
@@ -191,6 +217,14 @@ module.exports = async function (options) {
|
|
|
191
217
|
if (!action) {
|
|
192
218
|
const choices = [];
|
|
193
219
|
|
|
220
|
+
// "Heal" reinstalls copies whose disk state doesn't match the lockfile
|
|
221
|
+
if (desynced.length > 0) {
|
|
222
|
+
choices.push({
|
|
223
|
+
name: `Heal (${desynced.length}) - reinstall copies that don't match package-lock.json`,
|
|
224
|
+
value: 'heal',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
194
228
|
// "Sync" installs packages where node_modules is behind package.json
|
|
195
229
|
const syncCount = behindPackages.length + unknownPackages.length;
|
|
196
230
|
if (syncCount > 0) {
|
|
@@ -238,24 +272,101 @@ module.exports = async function (options) {
|
|
|
238
272
|
}
|
|
239
273
|
|
|
240
274
|
if (action === 'exit') {
|
|
241
|
-
return { allPackages };
|
|
275
|
+
return { allPackages, desynced };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Handle heal — reinstall node_modules copies that don't match the hidden
|
|
279
|
+
// lockfile. Desynced copies make npm silently no-op installs, which traps
|
|
280
|
+
// projects in reconcile/update loops where nothing ever actually changes.
|
|
281
|
+
if (action === 'heal') {
|
|
282
|
+
if (desynced.length === 0) {
|
|
283
|
+
logger.log(logger.format.green('\nNothing to heal — node_modules matches package-lock.json.'));
|
|
284
|
+
return { allPackages, desynced, healed: false };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const lockfileBackup = jetpack.read(lockfilePath);
|
|
288
|
+
|
|
289
|
+
logger.log(logger.format.cyan(`\nRemoving ${desynced.length} desynced copies and reinstalling...`));
|
|
290
|
+
npm.removeLocations(desynced.map(d => d.loc), projectPath);
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
await socket.wrap('npm install', { force: options.force });
|
|
294
|
+
} catch (e) {
|
|
295
|
+
// Restore the lockfile so the failed run can't advance it further
|
|
296
|
+
if (lockfileBackup) {
|
|
297
|
+
jetpack.write(lockfilePath, lockfileBackup);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (e.reason === 'npm-failed') {
|
|
301
|
+
logger.log('');
|
|
302
|
+
logger.log('Fix the npm error above and re-run with --heal — the removed copies will reinstall then.');
|
|
303
|
+
return { allPackages, desynced, healed: false };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
reportSocketBlock(e, 'npu out --heal --force');
|
|
307
|
+
return { allPackages, desynced, healed: false };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Confirm disk and lockfile are actually back in sync
|
|
311
|
+
const remaining = npm.findDesynced(projectPath);
|
|
312
|
+
if (remaining.length > 0) {
|
|
313
|
+
logger.error(`\n${remaining.length} package(s) are still desynced after reinstalling:`);
|
|
314
|
+
remaining.slice(0, 10).forEach(d => logger.error(` • ${d.loc.replace(/^node_modules\//, '')} — lockfile has ${d.lockfileVersion}, disk has ${d.diskVersion || 'nothing'}`));
|
|
315
|
+
logger.log(`Try a clean install: ${logger.format.cyan('rm -rf node_modules && npm install')}`);
|
|
316
|
+
return { allPackages, desynced, healed: false };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
await socket.audit({ force: options.force });
|
|
321
|
+
} catch (e) {
|
|
322
|
+
logger.error(`Audit warning: ${e.message}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
logger.log(logger.format.green(`\nHealed ${desynced.length} package(s) — node_modules now matches package-lock.json.`));
|
|
326
|
+
return { allPackages, desynced, healed: true };
|
|
242
327
|
}
|
|
243
328
|
|
|
244
329
|
// Handle sync — run npm install for packages where node_modules is behind package.json
|
|
245
330
|
if (action === 'sync') {
|
|
246
331
|
const toSync = [...behindPackages, ...unknownPackages];
|
|
332
|
+
|
|
333
|
+
if (toSync.length === 0) {
|
|
334
|
+
logger.log(logger.format.green('\nNothing to sync — node_modules already matches package.json.'));
|
|
335
|
+
return { allPackages, desynced, synced: false };
|
|
336
|
+
}
|
|
337
|
+
|
|
247
338
|
const installCmd = `npm install ${toSync.map(pkg => `${pkg.name}@${pkg.packageVersion}`).join(' ')}`;
|
|
248
339
|
logger.log(logger.format.cyan(`\nRunning ${installCmd}...`));
|
|
249
340
|
|
|
341
|
+
const lockfileBackup = jetpack.read(lockfilePath);
|
|
342
|
+
|
|
343
|
+
// Remove stale node_modules copies first so npm actually re-fetches them
|
|
344
|
+
// instead of trusting the hidden lockfile and reporting "up to date"
|
|
345
|
+
npm.removeInstalledCopies(toSync.map(pkg => pkg.name), projectPath);
|
|
346
|
+
|
|
250
347
|
try {
|
|
251
348
|
await socket.wrap(installCmd, { force: options.force });
|
|
252
349
|
} catch (e) {
|
|
350
|
+
// Restore the lockfile so a blocked/failed install can't leave it
|
|
351
|
+
// advanced past the physical files (that desync is what mints loops)
|
|
352
|
+
if (lockfileBackup) {
|
|
353
|
+
jetpack.write(lockfilePath, lockfileBackup);
|
|
354
|
+
}
|
|
355
|
+
|
|
253
356
|
if (e.reason === 'npm-failed') {
|
|
254
357
|
logger.log('');
|
|
255
358
|
logger.log('Fix the npm error above and retry.');
|
|
256
|
-
return { allPackages,
|
|
359
|
+
return { allPackages, desynced, synced: false };
|
|
257
360
|
}
|
|
258
|
-
|
|
361
|
+
|
|
362
|
+
reportSocketBlock(e, 'npu out --sync --force');
|
|
363
|
+
return { allPackages, desynced, synced: false };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Confirm the installs physically landed in node_modules
|
|
367
|
+
const expected = Object.fromEntries(toSync.map(pkg => [pkg.name, pkg.packageVersion]));
|
|
368
|
+
if (reportMismatches(npm.verifyInstalled(expected, projectPath))) {
|
|
369
|
+
return { allPackages, desynced, synced: false };
|
|
259
370
|
}
|
|
260
371
|
|
|
261
372
|
try {
|
|
@@ -265,15 +376,27 @@ module.exports = async function (options) {
|
|
|
265
376
|
}
|
|
266
377
|
|
|
267
378
|
logger.log(logger.format.green(`\nSynced ${toSync.length} package(s) to match package.json.`));
|
|
268
|
-
return { allPackages, synced: true };
|
|
379
|
+
return { allPackages, desynced, synced: true };
|
|
269
380
|
}
|
|
270
381
|
|
|
271
|
-
// Handle reconcile — update package.json for packages where node_modules is ahead
|
|
382
|
+
// Handle reconcile — update package.json for packages where node_modules is ahead.
|
|
383
|
+
// Strictly ahead-only: packages where node_modules is BEHIND are a sync problem,
|
|
384
|
+
// and downgrading package.json to match them would mask a stale install.
|
|
272
385
|
if (action === 'reconcile') {
|
|
386
|
+
if (aheadPackages.length === 0) {
|
|
387
|
+
logger.log('\nNothing to reconcile — no installed packages are ahead of package.json.');
|
|
388
|
+
|
|
389
|
+
const syncCount = behindPackages.length + unknownPackages.length;
|
|
390
|
+
if (syncCount > 0) {
|
|
391
|
+
logger.log(`${syncCount} package(s) are behind package.json — run ${logger.format.cyan('npu out --sync')} to install them.`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return { allPackages, desynced, reconciled: false };
|
|
395
|
+
}
|
|
396
|
+
|
|
273
397
|
projectJson = jetpack.read(packageJsonPath, 'json');
|
|
274
398
|
|
|
275
|
-
const
|
|
276
|
-
for (const pkg of toReconcile) {
|
|
399
|
+
for (const pkg of aheadPackages) {
|
|
277
400
|
const depType = pkg.type === 'dev' ? 'devDependencies'
|
|
278
401
|
: pkg.type === 'peer' ? 'peerDependencies'
|
|
279
402
|
: 'dependencies';
|
|
@@ -282,18 +405,28 @@ module.exports = async function (options) {
|
|
|
282
405
|
}
|
|
283
406
|
|
|
284
407
|
jetpack.write(packageJsonPath, projectJson);
|
|
285
|
-
logger.log(logger.format.green(`\nReconciled ${
|
|
408
|
+
logger.log(logger.format.green(`\nReconciled ${aheadPackages.length} package(s) in package.json.`));
|
|
286
409
|
|
|
287
|
-
return { allPackages, reconciled: true };
|
|
410
|
+
return { allPackages, desynced, reconciled: true };
|
|
288
411
|
}
|
|
289
412
|
|
|
290
413
|
// Handle patch/minor/major updates
|
|
291
414
|
const upgrades = action === 'patch' ? patchUpgrades
|
|
292
415
|
: action === 'minor' ? minorUpgrades
|
|
293
416
|
: latestUpgrades;
|
|
417
|
+
const packageNames = Object.keys(upgrades);
|
|
418
|
+
|
|
419
|
+
if (packageNames.length === 0) {
|
|
420
|
+
logger.log(logger.format.green(`\nNothing to update — package.json is already at the requested versions.`));
|
|
421
|
+
return { allPackages, desynced, updated: false, target: action };
|
|
422
|
+
}
|
|
294
423
|
|
|
295
|
-
// Back up package.json before modifying
|
|
424
|
+
// Back up package.json and package-lock.json before modifying, so a failed
|
|
425
|
+
// install can restore BOTH — restoring only package.json leaves the lockfile
|
|
426
|
+
// advanced past the physical files, which is exactly the desync that mints
|
|
427
|
+
// silent no-op installs
|
|
296
428
|
const packageJsonBackup = jetpack.read(packageJsonPath);
|
|
429
|
+
const lockfileBackup = jetpack.read(lockfilePath);
|
|
297
430
|
|
|
298
431
|
await ncu.run({
|
|
299
432
|
packageFile: packageJsonPath,
|
|
@@ -301,27 +434,34 @@ module.exports = async function (options) {
|
|
|
301
434
|
upgrade: true,
|
|
302
435
|
});
|
|
303
436
|
|
|
304
|
-
logger.log(logger.format.green(`\nUpdated ${
|
|
437
|
+
logger.log(logger.format.green(`\nUpdated ${packageNames.length} package(s) in package.json.`));
|
|
305
438
|
|
|
306
439
|
// Install the specific upgraded packages so npm actually pulls them in
|
|
307
440
|
// (plain `npm install` won't upgrade packages that already satisfy the range)
|
|
308
|
-
const packageNames = Object.keys(upgrades);
|
|
309
441
|
const installCmd = `npm install ${packageNames.map(name => `${name}@${version.clean(upgrades[name])}`).join(' ')}`;
|
|
310
442
|
logger.log(logger.format.cyan(`\nRunning ${installCmd}...`));
|
|
311
443
|
|
|
444
|
+
// Remove stale node_modules copies first so npm actually re-fetches them
|
|
445
|
+
// instead of trusting the hidden lockfile and reporting "up to date"
|
|
446
|
+
npm.removeInstalledCopies(packageNames, projectPath);
|
|
447
|
+
|
|
312
448
|
try {
|
|
313
449
|
await socket.wrap(installCmd, { force: options.force });
|
|
314
450
|
} catch (e) {
|
|
315
|
-
// Restore
|
|
451
|
+
// Restore both manifests since the bulk install failed
|
|
316
452
|
jetpack.write(packageJsonPath, packageJsonBackup);
|
|
317
|
-
|
|
453
|
+
if (lockfileBackup) {
|
|
454
|
+
jetpack.write(lockfilePath, lockfileBackup);
|
|
455
|
+
}
|
|
456
|
+
logger.log('package.json and package-lock.json have been restored to their original state.');
|
|
457
|
+
logger.log('The removed package copies will show as missing in the next check until reinstalled.');
|
|
318
458
|
|
|
319
459
|
// npm itself failed (ERESOLVE, network, peer-dep conflict) — not a Socket block.
|
|
320
460
|
// The npm error was already printed above; just acknowledge and stop.
|
|
321
461
|
if (e.reason === 'npm-failed') {
|
|
322
462
|
logger.log('');
|
|
323
463
|
logger.log('Fix the npm error above (e.g. resolve peer-dep conflicts) and retry.');
|
|
324
|
-
return { allPackages, updated: false, target: action };
|
|
464
|
+
return { allPackages, desynced, updated: false, target: action };
|
|
325
465
|
}
|
|
326
466
|
|
|
327
467
|
const flaggedPackages = e.flaggedPackages || [];
|
|
@@ -376,7 +516,13 @@ module.exports = async function (options) {
|
|
|
376
516
|
logger.log('To bypass Socket for this install only:');
|
|
377
517
|
logger.log(logger.format.cyan(` SOCKET_CLI_ACCEPT_RISKS=1 npm install ${packageNames.map(name => `${name}@${version.clean(upgrades[name])}`).join(' ')}`));
|
|
378
518
|
|
|
379
|
-
return { allPackages, updated: false, target: action };
|
|
519
|
+
return { allPackages, desynced, updated: false, target: action };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Confirm the installs physically landed in node_modules
|
|
523
|
+
const expectedUpgrades = Object.fromEntries(packageNames.map(name => [name, version.clean(upgrades[name])]));
|
|
524
|
+
if (reportMismatches(npm.verifyInstalled(expectedUpgrades, projectPath))) {
|
|
525
|
+
return { allPackages, desynced, updated: false, target: action };
|
|
380
526
|
}
|
|
381
527
|
|
|
382
528
|
// Run full audit after install
|
|
@@ -386,9 +532,49 @@ module.exports = async function (options) {
|
|
|
386
532
|
logger.error(`Audit warning: ${e.message}`);
|
|
387
533
|
}
|
|
388
534
|
|
|
389
|
-
return { allPackages, updated: true, target: action };
|
|
535
|
+
return { allPackages, desynced, updated: true, target: action };
|
|
390
536
|
};
|
|
391
537
|
|
|
538
|
+
// Helper to explain a Socket risk-block after stale copies were already removed.
|
|
539
|
+
// Deliberately does NOT reinstall the removed copies — that would silently
|
|
540
|
+
// bypass the block Socket just raised; removed is safer than stale-and-risky.
|
|
541
|
+
function reportSocketBlock(e, retryCommand) {
|
|
542
|
+
const flaggedPackages = e.flaggedPackages || [];
|
|
543
|
+
|
|
544
|
+
if (flaggedPackages.length > 0) {
|
|
545
|
+
logger.log('');
|
|
546
|
+
logger.error('Socket flagged the following dependencies:');
|
|
547
|
+
flaggedPackages.forEach(pkg => logger.error(` • ${pkg}`));
|
|
548
|
+
|
|
549
|
+
const flaggedNames = flaggedPackages.map(pkg => pkg.replace(/@[^@]+$/, ''));
|
|
550
|
+
logger.log('');
|
|
551
|
+
logger.log('If these are fixable CVEs in transitive deps pinned by package-lock.json, re-resolve them with:');
|
|
552
|
+
logger.log(logger.format.cyan(` socket npm update ${flaggedNames.join(' ')}`));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
logger.log('');
|
|
556
|
+
logger.log('To retry with Socket protection bypassed:');
|
|
557
|
+
logger.log(logger.format.cyan(` ${retryCommand}`));
|
|
558
|
+
logger.log('');
|
|
559
|
+
logger.log('The risky copies stay removed from node_modules (safer than leaving stale versions) and will show as missing until reinstalled.');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Helper to report packages that npm claimed to install but didn't physically land.
|
|
563
|
+
// Returns true if there were mismatches (callers should bail).
|
|
564
|
+
function reportMismatches(mismatches) {
|
|
565
|
+
if (mismatches.length === 0) {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
logger.error('\nnpm reported success but these packages did not actually update in node_modules:');
|
|
570
|
+
for (const m of mismatches) {
|
|
571
|
+
logger.error(` • ${m.name} — expected ${m.expected}, found ${m.actual}`);
|
|
572
|
+
}
|
|
573
|
+
logger.log(`Try a clean install: ${logger.format.cyan('rm -rf node_modules && npm install')}`);
|
|
574
|
+
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
|
|
392
578
|
// Helper to determine dependency type
|
|
393
579
|
function getDependencyType(packageJson, dep) {
|
|
394
580
|
if (packageJson.devDependencies?.[dep]) {
|
package/dist/lib/npm.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Libraries
|
|
2
|
+
const jetpack = require('fs-jetpack');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const version = require('wonderful-version');
|
|
5
|
+
|
|
6
|
+
// Remove installed node_modules copies for the given package names so npm
|
|
7
|
+
// actually re-fetches them. npm trusts the hidden lockfile
|
|
8
|
+
// (node_modules/.package-lock.json) over the physical files — if the lockfile
|
|
9
|
+
// already records the requested version, `npm install pkg@version` reports
|
|
10
|
+
// "up to date" without installing anything, even when the physical copy is
|
|
11
|
+
// stale. Deleting the copy forces npm to reify it from the registry/cache.
|
|
12
|
+
function removeInstalledCopies(names, cwd) {
|
|
13
|
+
const base = cwd || process.cwd();
|
|
14
|
+
|
|
15
|
+
for (const name of names) {
|
|
16
|
+
const pkgDir = path.join(base, 'node_modules', name);
|
|
17
|
+
|
|
18
|
+
if (jetpack.exists(pkgDir)) {
|
|
19
|
+
jetpack.remove(pkgDir);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Remove node_modules entries by their lockfile location (e.g.
|
|
25
|
+
// "node_modules/mocha/node_modules/brace-expansion") — unlike
|
|
26
|
+
// removeInstalledCopies, this handles nested copies.
|
|
27
|
+
function removeLocations(locations, cwd) {
|
|
28
|
+
const base = cwd || process.cwd();
|
|
29
|
+
|
|
30
|
+
for (const location of locations) {
|
|
31
|
+
jetpack.remove(path.join(base, location));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Find packages where the physical node_modules copy doesn't match what the
|
|
36
|
+
// hidden lockfile (node_modules/.package-lock.json) claims is installed.
|
|
37
|
+
// This desync is what makes npm silently no-op installs: npm trusts the hidden
|
|
38
|
+
// lockfile over the physical files, so a stale or partially-extracted copy is
|
|
39
|
+
// never repaired by `npm install`. Created by interrupted or Socket-blocked
|
|
40
|
+
// installs that advance the lockfiles without extracting files.
|
|
41
|
+
// Returns [{ loc, lockfileVersion, diskVersion }] — diskVersion null = missing.
|
|
42
|
+
function findDesynced(cwd) {
|
|
43
|
+
const base = cwd || process.cwd();
|
|
44
|
+
const hidden = jetpack.read(path.join(base, 'node_modules', '.package-lock.json'), 'json');
|
|
45
|
+
|
|
46
|
+
if (!hidden) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const desynced = [];
|
|
51
|
+
|
|
52
|
+
for (const [loc, info] of Object.entries(hidden.packages || {})) {
|
|
53
|
+
if (!loc.startsWith('node_modules/') || !info.version || info.link) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const pkgJson = jetpack.read(path.join(base, loc, 'package.json'), 'json');
|
|
58
|
+
const diskVersion = pkgJson?.version || null;
|
|
59
|
+
|
|
60
|
+
if (!diskVersion) {
|
|
61
|
+
// Missing optional packages are normal (platform-specific binaries
|
|
62
|
+
// like @esbuild/* and fsevents are skipped on non-matching systems)
|
|
63
|
+
if (info.optional) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
desynced.push({ loc, lockfileVersion: info.version, diskVersion: null });
|
|
68
|
+
} else if (diskVersion !== info.version) {
|
|
69
|
+
desynced.push({ loc, lockfileVersion: info.version, diskVersion });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return desynced;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Verify that the physical node_modules copies match the expected versions.
|
|
77
|
+
// expected is a map of { name: version }. Returns an array of mismatches:
|
|
78
|
+
// [{ name, expected, actual }] — empty when everything landed correctly.
|
|
79
|
+
function verifyInstalled(expected, cwd) {
|
|
80
|
+
const base = cwd || process.cwd();
|
|
81
|
+
const mismatches = [];
|
|
82
|
+
|
|
83
|
+
for (const [name, expectedVersion] of Object.entries(expected)) {
|
|
84
|
+
const cleanExpected = version.clean(expectedVersion);
|
|
85
|
+
const pkgJson = jetpack.read(path.join(base, 'node_modules', name, 'package.json'), 'json');
|
|
86
|
+
const actual = pkgJson?.version ? version.clean(pkgJson.version) : null;
|
|
87
|
+
|
|
88
|
+
if (!actual || !version.is(actual, '==', cleanExpected)) {
|
|
89
|
+
mismatches.push({ name, expected: cleanExpected, actual: actual || 'missing' });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return mismatches;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Export
|
|
97
|
+
module.exports = {
|
|
98
|
+
removeInstalledCopies,
|
|
99
|
+
removeLocations,
|
|
100
|
+
findDesynced,
|
|
101
|
+
verifyInstalled,
|
|
102
|
+
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-power-user",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.6",
|
|
4
4
|
"description": "Easy tools for every Node.js developer!",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "npm run prepare &&
|
|
7
|
+
"test": "npm run prepare && mocha test/ --recursive --timeout=10000",
|
|
8
8
|
"start": "npm run prepare && ./bin/node-power-user",
|
|
9
9
|
"help": "echo 'npm start -- -v'",
|
|
10
10
|
"prepare": "node -e \"require('prepare-package')()\"",
|