upgrade-interactive 1.0.0 → 1.1.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
@@ -1,9 +1,12 @@
1
1
  # upgrade-interactive
2
2
 
3
- A clone of `yarn upgrade-interactive` (the Yarn Berry / Yarn 4 version, built into
4
- Yarn since v4) for npm projects. It was built by reading Yarn's actual source
5
- (`@yarnpkg/plugin-interactive-tools`) rather than guessing at the UI, so the
6
- keybindings, columns, and version-suggestion logic mirror it closely.
3
+ An interactive dependency upgrader for npm projects, **inspired by** `yarn
4
+ upgrade-interactive` (the Yarn Berry / Yarn 4 version, built into Yarn since v4).
5
+ The three-column layout, keybindings, and version-suggestion logic follow yarn's
6
+ closely they were built by reading Yarn's actual source
7
+ (`@yarnpkg/plugin-interactive-tools`) — but this tool also adds things yarn
8
+ doesn't have, notably built-in **vulnerability warnings** and one-key npm
9
+ **`overrides`**, so it deliberately diverges where that improves the experience.
7
10
 
8
11
  ## Install / run
9
12
 
@@ -16,11 +19,20 @@ nui
16
19
 
17
20
  Requires Node 18+ and an interactive terminal.
18
21
 
19
- ### Using it inside a project (`npm run`)
22
+ ### Using it inside a project
20
23
 
21
- npm has no plugin system for adding real subcommands the way yarn does, so
22
- `npm upgrade-interactive` (no `run`) isn't possible. The closest equivalent,
23
- and the standard way to wire up any custom npm command, is a script entry:
24
+ Because this package ships a `bin`, you don't need `npm run` or any
25
+ `package.json` change to use it in a project. Install it and call it with npx:
26
+
27
+ ```sh
28
+ npm install -D upgrade-interactive
29
+ npx upgrade-interactive # runs the locally-installed copy (npx prefers node_modules/.bin)
30
+ npx nui # same thing, short name
31
+ ```
32
+
33
+ npm has no plugin/subcommand system the way Yarn Berry does, so the literal
34
+ `npm upgrade-interactive` isn't possible — but `npx upgrade-interactive` is the
35
+ native, no-`run` equivalent. If you'd still rather have a named script, add:
24
36
 
25
37
  ```json
26
38
  "scripts": {
@@ -28,7 +40,7 @@ and the standard way to wire up any custom npm command, is a script entry:
28
40
  }
29
41
  ```
30
42
 
31
- Then `npm run upgrade-interactive` works from that project, every time.
43
+ and run `npm run upgrade-interactive`.
32
44
 
33
45
  ## What it does
34
46
 
@@ -43,7 +55,26 @@ Then `npm run upgrade-interactive` works from that project, every time.
43
55
  entirely — same as yarn.
44
56
  3. Lets you pick, per package, whether to stay on **Current**, take the
45
57
  **Range** upgrade, or take the **Latest** upgrade.
46
- 4. Writes your choices back into `package.json` and runs `npm install`.
58
+ 4. **Checks for known vulnerabilities** (on by default) against npm's advisory
59
+ database, covering both your **direct and transitive** dependencies. A
60
+ flagged row shows a ⚠ icon, the severity (`low` / `moderate` / `high` /
61
+ `critical`), and the CVE id as a clickable link to the advisory — with the
62
+ plain URL printed alongside it when your terminal can't render clickable
63
+ links. The affected range and first fixed version are shown inline.
64
+ 5. Lets you press `o` on a vulnerable package to **pin it to a safe version via
65
+ an npm `overrides` entry** — the main way to patch a *transitive* dependency
66
+ you don't directly control.
67
+ 6. **Flags existing `overrides` that are no longer needed** — either because
68
+ nothing in the tree depends on that package anymore, or because your deps
69
+ would now resolve to a non-vulnerable version without the pin. Press `x` to
70
+ remove one. (It never removes an override that's still doing something, and
71
+ only ever removes one you explicitly select.)
72
+ 7. Writes your choices (overrides added and removed) back into `package.json`
73
+ and runs `npm install`.
74
+
75
+ By default the list is grouped into **Dependencies**, **Dev dependencies**, and
76
+ **Overrides** (transitive packages you've flagged for an override) sections. Pass
77
+ `--no-section` for a single flat list.
47
78
 
48
79
  ## Controls
49
80
 
@@ -52,6 +83,8 @@ Then `npm run upgrade-interactive` works from that project, every time.
52
83
  | `↑` / `↓` | Move between packages |
53
84
  | `←` / `→` | Move between Current / Range / Latest for that package |
54
85
  | `c` / `r` / `l` | Select **c**urrent / **r**ange / **l**atest for *every* package at once |
86
+ | `o` | Override the focused vulnerable package to a safe version (audit mode) |
87
+ | `x` | Remove the focused override when it's no longer needed (audit mode) |
55
88
  | `Enter` | Apply the selected upgrades and run `npm install` |
56
89
  | `Ctrl+C` / `Esc` | Abort — nothing is written |
57
90
 
@@ -62,11 +95,34 @@ changed highlighted — same idea as yarn's diff highlighting.
62
95
  ## Flags
63
96
 
64
97
  - `--no-install` — update `package.json` only, skip the `npm install` step
98
+ - `--audit` / `--no-audit` — enable/disable the vulnerability check (default: on)
99
+ - `--section` / `--no-section` — grouped sections vs a flat list (default: on)
65
100
  - `-h, --help`, `-v, --version`
66
101
 
102
+ ### Persisting audit / section preferences
103
+
104
+ Audit and sectioning are both on by default. To change the default permanently,
105
+ set an environment variable or a `package.json` config block:
106
+
107
+ ```json
108
+ "upgrade-interactive": { "audit": false, "section": true }
109
+ ```
110
+
111
+ ```sh
112
+ NUI_AUDIT=0 npx upgrade-interactive # disable auditing for this run
113
+ ```
114
+
115
+ Precedence, highest first: command-line flag → `NUI_AUDIT` / `NUI_SECTION`
116
+ environment variable → `package.json` config → default (on).
117
+
118
+ > Vulnerability data comes from npm's advisory endpoint, so auditing needs
119
+ > network access. When it can't reach the network the tool says so
120
+ > (`no network — couldn't check for vulnerable packages`) rather than pretending
121
+ > everything is clean, and the upgrade flow works as normal.
122
+
67
123
  ## How closely does this match yarn?
68
124
 
69
- Matched exactly:
125
+ Follows yarn closely:
70
126
  - The three-column Current/Range/Latest layout and the help text wording
71
127
  - The up/down/left/right navigation model (selection = which column is
72
128
  highlighted per row, not a separate checkbox)
@@ -76,7 +132,14 @@ Matched exactly:
76
132
  - The version-diff coloring algorithm (segment-by-segment: modifier → major
77
133
  → minor → patch)
78
134
 
79
- Intentional differences:
135
+ Deliberate additions / differences (this is *inspired by* yarn, not a clone):
136
+ - **Vulnerability warnings + `overrides`** — flags vulnerable direct and
137
+ transitive packages, lets you pin a safe version via npm `overrides`, and
138
+ flags existing overrides that are no longer needed so you can remove them.
139
+ Yarn's command has no equivalent.
140
+ - **Sectioned layout** — the list is grouped into Dependencies / Dev
141
+ dependencies / Overrides by default (yarn shows one flat list; use
142
+ `--no-section` to match that).
80
143
  - Only plain semver ranges are resolved (git/file/link/workspace ranges,
81
144
  and compound ranges like `>=1.0.0 <2.0.0`, are skipped — yarn handles
82
145
  these through its pluggable resolvers, which is out of scope here).
@@ -92,12 +155,16 @@ Intentional differences:
92
155
 
93
156
  ```
94
157
  src/
95
- cli.js entry point, arg parsing, apply + npm install
96
- registry.js npm registry client
158
+ cli.js entry point, arg/flag parsing, apply + npm install
159
+ registry.js npm registry client + bulk advisory lookup
97
160
  semver-suggest.js Current/Range/Latest suggestion + diff coloring
98
- package-file.js package.json read/write
161
+ package-file.js package.json read/write (+ overrides)
162
+ lockfile.js installed versions from package-lock.json
163
+ vulnerabilities.js advisory orchestration + severity/safe-version logic
164
+ links.js OSC 8 terminal hyperlinks (with fallback)
99
165
  components/
100
166
  App.js state machine + keybindings
167
+ OverridePicker.js safe-version chooser overlay
101
168
  Header.js, Prompt.js, Row.js presentation
102
169
  test/
103
170
  app.test.mjs simulated-keypress smoke tests (ink-testing-library)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "upgrade-interactive",
3
- "version": "1.0.0",
4
- "description": "A faithful clone of `yarn upgrade-interactive` (Yarn Berry / Yarn 4) for npm projects.",
3
+ "version": "1.1.0",
4
+ "description": "An interactive dependency upgrader for npm projects, inspired by `yarn upgrade-interactive` (Yarn Berry / Yarn 4), with built-in vulnerability warnings.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "upgrade-interactive": "./src/cli.js",
@@ -9,7 +9,9 @@
9
9
  },
10
10
  "main": "./src/cli.js",
11
11
  "scripts": {
12
- "test": "node test/app.test.mjs"
12
+ "test": "node --test \"test/unit/**/*.test.mjs\"",
13
+ "test:integration": "node test/app.test.mjs",
14
+ "test:all": "npm run test && npm run test:integration"
13
15
  },
14
16
  "engines": {
15
17
  "node": ">=18"
package/src/cli.js CHANGED
@@ -15,24 +15,50 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
15
  const HELP = `
16
16
  upgrade-interactive (nui)
17
17
 
18
- A faithful clone of "yarn upgrade-interactive" (Yarn Berry / Yarn 4) for npm projects.
18
+ An interactive dependency upgrader for npm projects, inspired by yarn's
19
+ "upgrade-interactive" (Yarn Berry / Yarn 4).
19
20
 
20
21
  Usage
21
22
  $ npx upgrade-interactive [options]
22
23
 
23
24
  Options
24
- --no-install Update package.json only, skip running "npm install" afterwards
25
- -h, --help Show this help message
26
- -v, --version Show the version number
25
+ --no-install Update package.json only, skip running "npm install" afterwards
26
+ --audit Flag vulnerable packages (default: on)
27
+ --no-audit Skip the vulnerability check (no advisory network calls)
28
+ --section Group the list into Dependencies / Dev dependencies / Overrides (default: on)
29
+ --no-section Show one flat list instead
30
+ -h, --help Show this help message
31
+ -v, --version Show the version number
32
+
33
+ Audit and sectioning are on by default. Persist a preference either way with the
34
+ NUI_AUDIT / NUI_SECTION environment variables, or a package.json config block:
35
+
36
+ "upgrade-interactive": { "audit": false, "section": true }
37
+
38
+ Precedence: command-line flag > environment variable > package.json config > default (on).
27
39
 
28
40
  Controls (inside the interactive UI)
29
41
  <up>/<down> select a package
30
42
  <left>/<right> select which version to apply (Current / Range / Latest)
31
43
  c / r / l select all packages' Current / Range / Latest column at once
44
+ o override a vulnerable package to a safe version (audit mode)
45
+ x remove an existing override that's no longer needed (audit mode)
32
46
  <enter> apply the selected upgrades (and run npm install)
33
47
  <ctrl+c> / esc abort without changing anything
34
48
  `;
35
49
 
50
+ // Resolve a boolean toggle from flags > env var > package.json config > default(true).
51
+ function resolveToggle({ args, env, config, onFlag, offFlag, envVar, configKey }) {
52
+ if (args.includes(offFlag)) return false;
53
+ if (args.includes(onFlag)) return true;
54
+ const envVal = env[envVar];
55
+ if (envVal != null && envVal !== '') {
56
+ return !/^(0|false|no|off)$/i.test(envVal.trim());
57
+ }
58
+ if (config && typeof config[configKey] === 'boolean') return config[configKey];
59
+ return true;
60
+ }
61
+
36
62
  async function main() {
37
63
  const args = process.argv.slice(2);
38
64
 
@@ -65,11 +91,23 @@ async function main() {
65
91
  return;
66
92
  }
67
93
 
94
+ const config = manifest.json['upgrade-interactive'];
95
+ const audit = resolveToggle({
96
+ args, env: process.env, config, onFlag: '--audit', offFlag: '--no-audit', envVar: 'NUI_AUDIT', configKey: 'audit',
97
+ });
98
+ const section = resolveToggle({
99
+ args, env: process.env, config, onFlag: '--section', offFlag: '--no-section', envVar: 'NUI_SECTION', configKey: 'section',
100
+ });
101
+
68
102
  const result = await new Promise((resolve) => {
69
103
  const { waitUntilExit } = render(
70
104
  e(App, {
71
105
  descriptors: manifest.descriptors,
72
- onSubmit: (selections) => resolve({ type: 'submit', selections }),
106
+ audit,
107
+ section,
108
+ cwd,
109
+ overrides: manifest.json.overrides || {},
110
+ onSubmit: (selections, overrides, removals) => resolve({ type: 'submit', selections, overrides, removals }),
73
111
  onAbort: () => resolve({ type: 'abort' }),
74
112
  }),
75
113
  { exitOnCtrlC: false }
@@ -83,12 +121,23 @@ async function main() {
83
121
  return;
84
122
  }
85
123
 
86
- if (result.selections.size === 0) {
124
+ const overrideSelections = result.overrides || {};
125
+ const overrideRemovals = result.removals || [];
126
+ if (
127
+ result.selections.size === 0 &&
128
+ Object.keys(overrideSelections).length === 0 &&
129
+ overrideRemovals.length === 0
130
+ ) {
87
131
  process.stdout.write('\nNo changes selected.\n');
88
132
  return;
89
133
  }
90
134
 
91
- const applied = await applyUpgrades(manifest, result.selections);
135
+ const { applied, overrides, removed } = await applyUpgrades(
136
+ manifest,
137
+ result.selections,
138
+ overrideSelections,
139
+ overrideRemovals
140
+ );
92
141
 
93
142
  process.stdout.write('\n');
94
143
  const byField = { dependencies: [], devDependencies: [] };
@@ -102,6 +151,21 @@ async function main() {
102
151
  }
103
152
  }
104
153
 
154
+ if (overrides.length > 0 || removed.length > 0) {
155
+ process.stdout.write('overrides\n');
156
+ for (const change of overrides) {
157
+ process.stdout.write(` ${change.name} \u2192 ${change.to}\n`);
158
+ }
159
+ for (const change of removed) {
160
+ process.stdout.write(` ${change.name} removed\n`);
161
+ }
162
+ }
163
+
164
+ if (applied.length === 0 && overrides.length === 0 && removed.length === 0) {
165
+ process.stdout.write('No effective changes.\n');
166
+ return;
167
+ }
168
+
105
169
  if (skipInstall) {
106
170
  process.stdout.write('\nUpdated package.json. Run npm install to apply.\n');
107
171
  return;