node-power-user 2.1.7 → 2.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/README.md +4 -0
- package/dist/cli.js +5 -0
- package/dist/commands/npx.js +42 -0
- package/dist/commands/outdated.js +64 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -107,9 +107,13 @@ npu out --heal # skip the menu: reinstall copies that don't match package
|
|
|
107
107
|
npu out --sync # skip the menu: install packages to match package.json
|
|
108
108
|
npu out -r # skip the menu: reconcile package.json to installed versions
|
|
109
109
|
npu out -P | -m | -M # skip the menu: apply patch / minor / major updates
|
|
110
|
+
npu out --min-age 14 # only update packages published ≥ 14 days ago (default: 7)
|
|
111
|
+
npu out --min-age 0 # disable age filtering — update everything
|
|
110
112
|
npu out --force # bypass Socket protection
|
|
111
113
|
```
|
|
112
114
|
|
|
115
|
+
The table includes a **Released** column showing when each package's latest version was published and how many days ago. Packages published less than `--min-age` days ago (default: 7) are flagged with ⚠ and **automatically skipped** during updates — unless that version is already installed. This protects against supply chain attacks that target brand-new releases.
|
|
116
|
+
|
|
113
117
|
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
118
|
|
|
115
119
|
When problems are found, the menu offers context-aware actions:
|
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ const ALIASES = {
|
|
|
10
10
|
global: ['-g', '--global'],
|
|
11
11
|
audit: ['--audit'],
|
|
12
12
|
install: ['-i', '--install', 'i'],
|
|
13
|
+
npx: ['--npx'],
|
|
13
14
|
open: ['--open', 'repo', '--repo'],
|
|
14
15
|
outdated: ['-o', 'out', '--outdated', '-u', '--update', 'up', 'update'],
|
|
15
16
|
packages: ['-p', 'pack', '--packages'],
|
|
@@ -46,6 +47,10 @@ function resolveCommand(options) {
|
|
|
46
47
|
function Main() {}
|
|
47
48
|
|
|
48
49
|
Main.prototype.process = async function (options) {
|
|
50
|
+
// Prevent recursion: zsh/PATH wrappers route npm/npx → npu. When npu
|
|
51
|
+
// spawns subprocesses (sfw, npm, npx), they must NOT loop back to npu.
|
|
52
|
+
process.env._NPU_GUARD = '1';
|
|
53
|
+
|
|
49
54
|
// Fix options
|
|
50
55
|
options = options || {};
|
|
51
56
|
options._ = options._ || [];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Libraries
|
|
2
|
+
const logger = new (require('../lib/logger'))('node-power-user');
|
|
3
|
+
const socket = require('../lib/socket');
|
|
4
|
+
|
|
5
|
+
// Module
|
|
6
|
+
module.exports = async function (options) {
|
|
7
|
+
// Get everything after "npx" as the command to run
|
|
8
|
+
const args = (options._ || []).slice(1);
|
|
9
|
+
|
|
10
|
+
if (args.length === 0) {
|
|
11
|
+
logger.error('Usage: npu npx <command> [args...]');
|
|
12
|
+
logger.log(`Example: ${logger.format.cyan('npu npx create-react-app my-app')}`);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const command = `npx ${args.join(' ')}`;
|
|
17
|
+
|
|
18
|
+
// Check firewall status upfront (blocks if not installed unless --force)
|
|
19
|
+
await socket.check({ force: options.force });
|
|
20
|
+
|
|
21
|
+
// Log
|
|
22
|
+
logger.log(`Running: ${logger.format.cyan(command)}`);
|
|
23
|
+
|
|
24
|
+
// Wrap with Socket Firewall
|
|
25
|
+
try {
|
|
26
|
+
await socket.wrap(command, { force: options.force });
|
|
27
|
+
} catch (e) {
|
|
28
|
+
if (e.reason === 'npm-failed') {
|
|
29
|
+
logger.log('');
|
|
30
|
+
logger.log('npx command failed. See the error output above.');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
logger.log('');
|
|
35
|
+
logger.log('Command blocked. See the output above for details.');
|
|
36
|
+
logger.log('To retry without firewall protection:');
|
|
37
|
+
logger.log(logger.format.cyan(` npu npx ${args.join(' ')} --force`));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
logger.log(logger.format.green('Done.'));
|
|
42
|
+
};
|
|
@@ -51,6 +51,8 @@ module.exports = async function (options) {
|
|
|
51
51
|
logger.log(chalk.dim(`Ignoring: ${ignoreList.join(', ')}`));
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
const minAge = options.minAge != null ? Number(options.minAge) : 7;
|
|
55
|
+
|
|
54
56
|
// Run ncu for patch, minor, and latest (include peer dependencies)
|
|
55
57
|
const ncuOptions = {
|
|
56
58
|
packageFile: packageJsonPath,
|
|
@@ -108,6 +110,25 @@ module.exports = async function (options) {
|
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
|
|
113
|
+
// Fetch publish dates for packages with updates
|
|
114
|
+
if (allPackages.size > 0) {
|
|
115
|
+
await Promise.all([...allPackages.values()].map(async (pkg) => {
|
|
116
|
+
if (!pkg.latestVersion) return;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const output = await execute(`npm view ${pkg.name} time --json`, { log: false });
|
|
120
|
+
const times = JSON.parse(output);
|
|
121
|
+
const publishDate = times[pkg.latestVersion];
|
|
122
|
+
if (publishDate) {
|
|
123
|
+
pkg.publishedDate = new Date(publishDate);
|
|
124
|
+
pkg.daysSincePublish = Math.floor((Date.now() - pkg.publishedDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// Release date is supplementary — skip on failure
|
|
128
|
+
}
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
|
|
111
132
|
// Display unified table
|
|
112
133
|
if (allPackages.size === 0 && desynced.length === 0) {
|
|
113
134
|
logger.log(logger.format.green('\nAll packages are up to date!'));
|
|
@@ -115,7 +136,7 @@ module.exports = async function (options) {
|
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
if (allPackages.size > 0) {
|
|
118
|
-
const dataTable = [['Package', 'package.json', 'Installed', 'Latest', 'Type']];
|
|
139
|
+
const dataTable = [['Package', 'package.json', 'Installed', 'Latest', 'Released', 'Type']];
|
|
119
140
|
|
|
120
141
|
for (const pkg of allPackages.values()) {
|
|
121
142
|
const pkgVersionColor = pkg.hasDiscrepancy ? 'red' : 'green';
|
|
@@ -134,21 +155,34 @@ module.exports = async function (options) {
|
|
|
134
155
|
latestDisplay = chalk.dim('-');
|
|
135
156
|
}
|
|
136
157
|
|
|
158
|
+
let releasedDisplay;
|
|
159
|
+
if (pkg.daysSincePublish != null) {
|
|
160
|
+
const dateStr = pkg.publishedDate.toISOString().split('T')[0];
|
|
161
|
+
const display = `${dateStr} (${pkg.daysSincePublish}d)`;
|
|
162
|
+
releasedDisplay = pkg.daysSincePublish < minAge ? chalk.magenta(display + ' ⚠') : chalk.dim(display);
|
|
163
|
+
} else {
|
|
164
|
+
releasedDisplay = chalk.dim('-');
|
|
165
|
+
}
|
|
166
|
+
|
|
137
167
|
dataTable.push([
|
|
138
168
|
pkg.name,
|
|
139
169
|
chalk[pkgVersionColor](pkg.packageVersion),
|
|
140
170
|
chalk.green(pkg.installedVersion),
|
|
141
171
|
latestDisplay,
|
|
172
|
+
releasedDisplay,
|
|
142
173
|
pkg.type,
|
|
143
174
|
]);
|
|
144
175
|
}
|
|
145
176
|
|
|
146
177
|
console.log(table(dataTable));
|
|
147
178
|
|
|
148
|
-
// Only show legend if there are major updates
|
|
149
179
|
const hasMajorUpdates = [...allPackages.values()].some(pkg => pkg.hasMajorUpdate);
|
|
150
|
-
|
|
151
|
-
|
|
180
|
+
const hasRecentReleases = [...allPackages.values()].some(pkg => pkg.daysSincePublish != null && pkg.daysSincePublish < minAge);
|
|
181
|
+
if (hasMajorUpdates || hasRecentReleases) {
|
|
182
|
+
const legends = [];
|
|
183
|
+
if (hasMajorUpdates) legends.push(chalk.magenta('⚠ = major version (breaking changes)'));
|
|
184
|
+
if (hasRecentReleases) legends.push(chalk.magenta(`⚠ = published < ${minAge} days ago (auto-skipped)`));
|
|
185
|
+
logger.log(chalk.dim('Legend: ') + legends.join(chalk.dim(' · ')));
|
|
152
186
|
}
|
|
153
187
|
}
|
|
154
188
|
|
|
@@ -411,9 +445,32 @@ module.exports = async function (options) {
|
|
|
411
445
|
}
|
|
412
446
|
|
|
413
447
|
// Handle patch/minor/major updates
|
|
414
|
-
const
|
|
448
|
+
const allUpgrades = action === 'patch' ? patchUpgrades
|
|
415
449
|
: action === 'minor' ? minorUpgrades
|
|
416
450
|
: latestUpgrades;
|
|
451
|
+
|
|
452
|
+
// Filter out packages whose target version was published too recently
|
|
453
|
+
const tooNew = [...allPackages.values()]
|
|
454
|
+
.filter(pkg => pkg.daysSincePublish != null && pkg.daysSincePublish < minAge)
|
|
455
|
+
.filter(pkg => allUpgrades[pkg.name] && pkg.installedVersion !== version.clean(allUpgrades[pkg.name]))
|
|
456
|
+
.map(pkg => pkg.name);
|
|
457
|
+
|
|
458
|
+
const upgrades = {};
|
|
459
|
+
for (const [name, ver] of Object.entries(allUpgrades)) {
|
|
460
|
+
if (!tooNew.includes(name)) {
|
|
461
|
+
upgrades[name] = ver;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (tooNew.length > 0) {
|
|
466
|
+
logger.warn(`\nSkipped ${tooNew.length} package(s) published < ${minAge} days ago:`);
|
|
467
|
+
for (const name of tooNew) {
|
|
468
|
+
const pkg = allPackages.get(name);
|
|
469
|
+
logger.warn(` ${chalk.magenta('⚠')} ${name}@${pkg.latestVersion} (${pkg.daysSincePublish}d old)`);
|
|
470
|
+
}
|
|
471
|
+
logger.log(chalk.dim(`Use --min-age 0 to include all packages regardless of age.`));
|
|
472
|
+
}
|
|
473
|
+
|
|
417
474
|
const packageNames = Object.keys(upgrades);
|
|
418
475
|
|
|
419
476
|
if (packageNames.length === 0) {
|
|
@@ -428,10 +485,12 @@ module.exports = async function (options) {
|
|
|
428
485
|
const packageJsonBackup = jetpack.read(packageJsonPath);
|
|
429
486
|
const lockfileBackup = jetpack.read(lockfilePath);
|
|
430
487
|
|
|
488
|
+
const rejectList = [...ignoreList, ...tooNew];
|
|
431
489
|
await ncu.run({
|
|
432
490
|
packageFile: packageJsonPath,
|
|
433
491
|
target: action,
|
|
434
492
|
upgrade: true,
|
|
493
|
+
...(rejectList.length > 0 ? { reject: rejectList } : {}),
|
|
435
494
|
});
|
|
436
495
|
|
|
437
496
|
logger.log(logger.format.green(`\nUpdated ${packageNames.length} package(s) in package.json.`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-power-user",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Easy tools for every Node.js developer!",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"homepage": "https://itwcreativeworks.com",
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@inquirer/prompts": "^8.
|
|
44
|
+
"@inquirer/prompts": "^8.5.2",
|
|
45
45
|
"chalk": "^5.6.2",
|
|
46
46
|
"cli-progress": "^3.12.0",
|
|
47
47
|
"fs-jetpack": "^5.1.0",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"yargs": "^18.0.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"mocha": "^11.7.
|
|
57
|
+
"mocha": "^11.7.6",
|
|
58
58
|
"prepare-package": "^2.1.0"
|
|
59
59
|
}
|
|
60
60
|
}
|