From 800ed6d16f4162b4ee171543e8cf3ff969c3569e Mon Sep 17 00:00:00 2001 From: Zelys Date: Tue, 19 May 2026 20:50:29 -0500 Subject: [PATCH] fix(config): pause progress spinner during interactive editor spawn When npm config edit or npm edit opens an interactive editor, the progress spinner kept running and wrote ANSI control codes into the editor buffer, corrupting the display. Both commands now call input.start() before opening the editor, which pauses the display layer while the editor has control of the terminal. Closes #9142, Closes #9184. Co-Authored-By: Claude Sonnet 4.6 --- lib/commands/config.js | 6 +++--- lib/commands/edit.js | 10 ++++++---- test/lib/commands/config.js | 6 ++++++ test/lib/commands/edit.js | 6 ++++++ 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/commands/config.js b/lib/commands/config.js index 015850c48304a..0a8b84aba2666 100644 --- a/lib/commands/config.js +++ b/lib/commands/config.js @@ -5,7 +5,7 @@ const { EOL } = require('node:os') const localeCompare = require('@isaacs/string-locale-compare')('en') const pkgJson = require('@npmcli/package-json') const { defaults, definitions, nerfDarts, proxyEnv } = require('@npmcli/config/lib/definitions') -const { log, output } = require('proc-log') +const { log, output, input } = require('proc-log') const BaseCommand = require('../base-cmd.js') const { redact } = require('@npmcli/redact') @@ -266,7 +266,7 @@ ${defData} `.split('\n').join(EOL) await mkdir(dirname(file), { recursive: true }) await writeFile(file, tmpData, 'utf8') - await new Promise((res, rej) => { + await input.start(() => new Promise((res, rej) => { const [bin, ...args] = e.split(/\s+/) const editor = spawn(bin, [...args, file], { stdio: 'inherit' }) editor.on('exit', (code) => { @@ -275,7 +275,7 @@ ${defData} } return res() }) - }) + })) } async fix () { diff --git a/lib/commands/edit.js b/lib/commands/edit.js index 1140c59efa3e4..0b1a200264d98 100644 --- a/lib/commands/edit.js +++ b/lib/commands/edit.js @@ -1,6 +1,7 @@ const { resolve } = require('node:path') const { lstat } = require('node:fs/promises') const cp = require('node:child_process') +const { input } = require('proc-log') const completion = require('../utils/installed-shallow.js') const BaseCommand = require('../base-cmd.js') @@ -46,16 +47,17 @@ class Edit extends BaseCommand { const dir = resolve(this.npm.dir, path) await lstat(dir) - await new Promise((res, rej) => { + await input.start(() => new Promise((res, rej) => { const [bin, ...spawnArgs] = this.npm.config.get('editor').split(/\s+/) const editor = cp.spawn(bin, [...spawnArgs, dir], { stdio: 'inherit' }) - editor.on('exit', async (code) => { + editor.on('exit', (code) => { if (code) { return rej(new Error(`editor process exited with code: ${code}`)) } - await this.npm.exec('rebuild', [dir]).then(res).catch(rej) + res() }) - }) + })) + await this.npm.exec('rebuild', [dir]) } } diff --git a/test/lib/commands/config.js b/test/lib/commands/config.js index fbcc58ba40153..56f46981a066a 100644 --- a/test/lib/commands/config.js +++ b/test/lib/commands/config.js @@ -574,6 +574,11 @@ t.test('config edit', async t => { }, }) + const inputEvents = [] + const inputListener = (level) => inputEvents.push(level) + process.on('input', inputListener) + t.teardown(() => process.off('input', inputListener)) + await npm.exec('config', ['edit']) t.ok(editor.called, 'editor was spawned') @@ -582,6 +587,7 @@ t.test('config edit', async t => { [join(home, '.npmrc')], 'editor opened the user config file' ) + t.same(inputEvents.slice(0, 2), ['start', 'end'], 'progress paused and resumed around editor') const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' }) t.ok(contents.includes('foo=bar'), 'kept foo') diff --git a/test/lib/commands/edit.js b/test/lib/commands/edit.js index b55bb2df218ba..915241c82f6da 100644 --- a/test/lib/commands/edit.js +++ b/test/lib/commands/edit.js @@ -58,8 +58,14 @@ t.test('npm edit', async t => { : ['-c', 'testinstall'] spawk.spawn(scriptShell, scriptArgs, { cwd: semverPath }) + const inputEvents = [] + const inputListener = (level) => inputEvents.push(level) + process.on('input', inputListener) + t.teardown(() => process.off('input', inputListener)) + await npm.exec('edit', ['semver']) t.match(joinedOutput(), 'rebuilt dependencies successfully') + t.same(inputEvents.slice(0, 2), ['start', 'end'], 'progress paused and resumed around editor') }) t.test('rebuild failure', async t => {