From 34d13bb0d3f1a7f08338133a0cdea7a394ec372c Mon Sep 17 00:00:00 2001 From: Ashish Vaghela Date: Tue, 4 Aug 2026 16:40:08 +0530 Subject: [PATCH] fix(install): repeat specs in global allow-scripts suggestion The blocked-install-scripts warning suggested `npm install -g --allow-scripts=`, which has no install targets, so the command falls back to installing the current directory and fails with ENOENT reading package.json for anyone not sitting in a project. Build the suggestion from the command that was actually run and its positional specs, so `npm install -g esbuild` now suggests `npm install -g esbuild --allow-scripts=esbuild`. Commands invoked without specs (`npm update -g`) keep the bare form, which works. Fixes: https://github.com/npm/cli/issues/9835 --- .../content/commands/npm-approve-scripts.md | 4 +- .../content/commands/npm-install-scripts.md | 4 +- lib/utils/allow-scripts-remediation.js | 13 ++++- lib/utils/reify-output.js | 5 +- test/lib/utils/reify-output.js | 52 +++++++++++++++++++ 5 files changed, 70 insertions(+), 8 deletions(-) diff --git a/docs/lib/content/commands/npm-approve-scripts.md b/docs/lib/content/commands/npm-approve-scripts.md index 55c892fbf96b1..d73c052286e8d 100644 --- a/docs/lib/content/commands/npm-approve-scripts.md +++ b/docs/lib/content/commands/npm-approve-scripts.md @@ -25,8 +25,8 @@ it with `--global` (`-g`) fails with an `EGLOBAL` error, since global installs (`npm install -g`) and one-off executions (`npm exec` / `npx`) have no project `package.json` to write to. To allow install scripts in those contexts, use the `--allow-scripts` flag at install time (for example -`npm install -g --allow-scripts=canvas,sharp`) or persist the setting with -`npm config set allow-scripts=canvas,sharp --location=user`. +`npm install -g canvas sharp --allow-scripts=canvas,sharp`) or persist the +setting with `npm config set allow-scripts=canvas,sharp --location=user`. There are three modes: diff --git a/docs/lib/content/commands/npm-install-scripts.md b/docs/lib/content/commands/npm-install-scripts.md index 32f05a8577039..e83a72025f821 100644 --- a/docs/lib/content/commands/npm-install-scripts.md +++ b/docs/lib/content/commands/npm-install-scripts.md @@ -25,8 +25,8 @@ it with `--global` (`-g`) fails with an `EGLOBAL` error, since global installs (`npm install -g`) and one-off executions (`npm exec` / `npx`) have no project `package.json` to write to. To allow install scripts in those contexts, use the `--allow-scripts` flag at install time (for example -`npm install -g --allow-scripts=canvas,sharp`) or persist the setting with -`npm config set allow-scripts=canvas,sharp --location=user`. +`npm install -g canvas sharp --allow-scripts=canvas,sharp`) or persist the +setting with `npm config set allow-scripts=canvas,sharp --location=user`. There are four subcommands: diff --git a/lib/utils/allow-scripts-remediation.js b/lib/utils/allow-scripts-remediation.js index ff8c9b75a81fe..c3c99316b6f0e 100644 --- a/lib/utils/allow-scripts-remediation.js +++ b/lib/utils/allow-scripts-remediation.js @@ -6,4 +6,15 @@ const configSetAllowScripts = (names) => `npm config set allow-scripts=${names.join(',')} --location=user` -module.exports = { configSetAllowScripts } +// Builds the one-off `npm -g ... --allow-scripts=` command +// suggested to global users. The specs the user asked for have to be +// repeated: `npm install -g --allow-scripts=foo` with no specs installs the +// current directory, which global users usually are not sitting in, so the +// suggestion would fail with ENOENT reading package.json. +const globalAllowScripts = (npm, names) => { + const command = npm.command || 'install' + const specs = npm.argv?.length ? ` ${npm.argv.join(' ')}` : '' + return `npm ${command} -g${specs} --allow-scripts=${names.join(',')}` +} + +module.exports = { configSetAllowScripts, globalAllowScripts } diff --git a/lib/utils/reify-output.js b/lib/utils/reify-output.js index e50d4ac72d967..aadc2da58ed82 100644 --- a/lib/utils/reify-output.js +++ b/lib/utils/reify-output.js @@ -16,7 +16,7 @@ const npmAuditReport = require('npm-audit-report') const { readTree: getFundingInfo } = require('libnpmfund') const { trustedDisplay } = require('@npmcli/arborist/lib/script-allowed.js') const auditError = require('./audit-error.js') -const { configSetAllowScripts } = require('./allow-scripts-remediation.js') +const { configSetAllowScripts, globalAllowScripts } = require('./allow-scripts-remediation.js') const reifyOutput = (npm, arb, extras = {}) => { const { diff, actualTree } = arb @@ -276,9 +276,8 @@ const unreviewedScriptsMessage = (npm, unreviewedScripts) => { // one-off, or `npm config set allow-scripts` to persist it. const remediationLines = (npm, names) => { if (npm.global) { - const list = names.join(',') return [ - `Run \`npm install -g --allow-scripts=${list}\` to allow these scripts ` + + `Run \`${globalAllowScripts(npm, names)}\` to allow these scripts ` + `once, or \`${configSetAllowScripts(names)}\` to allow them for ` + 'all global installs.', ] diff --git a/test/lib/utils/reify-output.js b/test/lib/utils/reify-output.js index 0678e9cabeb28..7d559fe027221 100644 --- a/test/lib/utils/reify-output.js +++ b/test/lib/utils/reify-output.js @@ -539,6 +539,58 @@ t.test('global install suggests --allow-scripts, not approve-scripts', async t = t.notMatch(warn, /approve-scripts/) }) +t.test('global install repeats the requested specs in the suggestion', async t => { + const mock = await mockNpm(t, { + command: 'install', + argv: ['esbuild', 'canvas@2'], + config: { global: true }, + }) + Object.defineProperty(mock.npm, 'command', { + get () { + return 'install' + }, + enumerable: true, + }) + + reifyOutput(mock.npm, { + actualTree: { name: 'host', inventory: { has: () => false } }, + diff: { children: [] }, + }, { + unreviewedScripts: [{ + node: { packageName: 'esbuild', name: 'esbuild', version: '0.28.1', path: '/x/esbuild' }, + scripts: { postinstall: 'node install.js' }, + }], + }) + mock.npm.finish() + + const warn = mock.logs.warn.byTitle('install-scripts').join('\n') + t.match(warn, /npm install -g esbuild canvas@2 --allow-scripts=esbuild/) +}) + +t.test('global command without specs suggests that command', async t => { + const mock = await mockNpm(t, { command: 'update', config: { global: true } }) + Object.defineProperty(mock.npm, 'command', { + get () { + return 'update' + }, + enumerable: true, + }) + + reifyOutput(mock.npm, { + actualTree: { name: 'host', inventory: { has: () => false } }, + diff: { children: [] }, + }, { + unreviewedScripts: [{ + node: { packageName: 'esbuild', name: 'esbuild', version: '0.28.1', path: '/x/esbuild' }, + scripts: { postinstall: 'node install.js' }, + }], + }) + mock.npm.finish() + + const warn = mock.logs.warn.byTitle('install-scripts').join('\n') + t.match(warn, /npm update -g --allow-scripts=esbuild/) +}) + t.test('single unreviewed script uses singular wording', async t => { const mockReifyWithExtras = async (t, reify, extras) => { const mock = await mockNpm(t, {})