node-power-user 2.1.8 → 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 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:
@@ -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
- if (hasMajorUpdates) {
151
- logger.log(chalk.dim('Legend: ') + chalk.magenta('⚠ = major version (breaking changes)'));
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 upgrades = action === 'patch' ? patchUpgrades
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.1.8",
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.4.3",
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.5",
57
+ "mocha": "^11.7.6",
58
58
  "prepare-package": "^2.1.0"
59
59
  }
60
60
  }