From 03b7b575208dd6ef654c48f441235ba107607de3 Mon Sep 17 00:00:00 2001 From: Richard Green Date: Sat, 11 Apr 2026 21:42:42 +0100 Subject: [PATCH 1/2] New config option for terminal/shell title prefixes of package name --- lib/npm.js | 14 +++++- test/lib/npm.js | 43 +++++++++++++++++++ .../config/lib/definitions/definitions.js | 8 ++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/lib/npm.js b/lib/npm.js index b2ab377e95d71..ae945d730ca8a 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -1,5 +1,6 @@ const { resolve, dirname, join } = require('node:path') const Config = require('@npmcli/config') +const pkgJson = require("@npmcli/package-json"); const which = require('which') const fs = require('node:fs/promises') const { definitions, flatten, nerfDarts, shorthands } = require('@npmcli/config/lib/definitions') @@ -143,13 +144,24 @@ class Npm { .catch((e) => log.verbose('logfile', `could not create logs-dir: ${e}`))) } + // configuration to enable the terminal title to be prefixed with the project's name. + let titlePrefix = [] + if (this.config.get('prefix-package-name-in-title')) { + const pkg = await pkgJson + .normalize(this.localPrefix) + .then((p) => p.content); + if (pkg.name) { + titlePrefix = [pkg.name, '::'] + } + } + // note: this MUST be shorter than the actual argv length, because it uses the same memory, so node will truncate it if it's too long. // We time this because setting process.title is slow sometimes but we have to do it for security reasons. But still helpful to know how slow it is. time.start('npm:load:setTitle', () => { const { parsedArgv: { cooked, remain } } = this.config // Secrets are mostly in configs, so title is set using only the positional args to keep those from being leaked. // We still do a best effort replaceInfo. - this.#title = ['npm'].concat(replaceInfo(remain)).join(' ').trim() + this.#title = [...titlePrefix, 'npm'].concat(replaceInfo(remain)).join(' ').trim() process.title = this.#title // The cooked argv is also logged separately for debugging purposes. // It is cleaned as a best effort by replacing known secrets like basic auth password and strings that look like npm tokens. diff --git a/test/lib/npm.js b/test/lib/npm.js index c4c0d720e86e8..e75582edb280f 100644 --- a/test/lib/npm.js +++ b/test/lib/npm.js @@ -295,6 +295,49 @@ t.test('set process.title', async t => { t.equal(npm.title, 'npm token revoke notatoken') t.equal(process.title, 'npm token revoke notatoken') }) + + t.test('show project name as a prefix if config enabled and project name set', async t => { + const packageJson = { + name: 'my-project', + description: 'npm test package', + version: '1.0.0', + scripts: { + dev: 'nodemon --exec ts-node src/index.ts', + }, + } + const { npm } = await loadMockNpm(t, { + config: { + 'prefix-package-name-in-title': true, + }, + prefixDir: { + 'package.json': JSON.stringify(packageJson, null, 2), + }, + argv: ['run', 'dev'], + }) + t.equal(npm.title, 'my-project :: npm run dev') + t.equal(process.title, 'my-project :: npm run dev') + }) + + t.test('show project name as a prefix if config enabled and project name set', async t => { + const packageJson = { + description: 'npm test package', + version: '1.0.0', + scripts: { + dev: 'nodemon --exec ts-node src/index.ts', + }, + } + const { npm } = await loadMockNpm(t, { + config: { + 'prefix-package-name-in-title': true, + }, + prefixDir: { + 'package.json': JSON.stringify(packageJson, null, 2), + }, + argv: ['run', 'dev'], + }) + t.equal(npm.title, 'npm run dev') + t.equal(process.title, 'npm run dev') + }) }) t.test('debug log', async t => { diff --git a/workspaces/config/lib/definitions/definitions.js b/workspaces/config/lib/definitions/definitions.js index 7c5b2ce170d89..207802b3a650e 100644 --- a/workspaces/config/lib/definitions/definitions.js +++ b/workspaces/config/lib/definitions/definitions.js @@ -1655,6 +1655,14 @@ const definitions = { it forces non-global commands to run in the specified folder. `, }), + 'prefix-package-name-in-title': new Definition('prefix-package-name-in-title', { + default: false, + type: Boolean, + description: ` + If true, terminal/shell titles will be prefixed with the package name. + `, + flatten, + }), preid: new Definition('preid', { default: '', hint: 'prerelease-id', From 42ab45f315639ec53a1943cfbfeac78825e2609e Mon Sep 17 00:00:00 2001 From: Richard Green Date: Sat, 11 Apr 2026 22:35:56 +0100 Subject: [PATCH 2/2] Update snapshots --- .../test/lib/commands/config.js.test.cjs | 2 ++ .../test/lib/commands/install.js.test.cjs | 12 +++++------ .../test/lib/commands/publish.js.test.cjs | 21 ------------------- tap-snapshots/test/lib/docs.js.test.cjs | 12 +++++++++++ 4 files changed, 20 insertions(+), 27 deletions(-) diff --git a/tap-snapshots/test/lib/commands/config.js.test.cjs b/tap-snapshots/test/lib/commands/config.js.test.cjs index 6617b3a0827f7..cc4f797943d6d 100644 --- a/tap-snapshots/test/lib/commands/config.js.test.cjs +++ b/tap-snapshots/test/lib/commands/config.js.test.cjs @@ -128,6 +128,7 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna "prefer-offline": false, "prefer-online": false, "prefix": "{CWD}/global", + "prefix-package-name-in-title": false, "preid": "", "production": null, "progress": {PROGRESS}, @@ -311,6 +312,7 @@ prefer-dedupe = false prefer-offline = false prefer-online = false prefix = "{CWD}/global" +prefix-package-name-in-title = false preid = "" production = null progress = {PROGRESS} diff --git a/tap-snapshots/test/lib/commands/install.js.test.cjs b/tap-snapshots/test/lib/commands/install.js.test.cjs index dd618ee3688f1..1e0cff2413633 100644 --- a/tap-snapshots/test/lib/commands/install.js.test.cjs +++ b/tap-snapshots/test/lib/commands/install.js.test.cjs @@ -135,8 +135,8 @@ verbose stack Error: The developer of this package has specified the following t verbose stack Invalid devEngines.runtime verbose stack Invalid name "nondescript" does not match "node" for "runtime" verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:247:27) -verbose stack at MockNpm.execCommandClass ({CWD}/lib/npm.js:292:7) -verbose stack at MockNpm.exec ({CWD}/lib/npm.js:193:9) +verbose stack at MockNpm.execCommandClass ({CWD}/lib/npm.js:304:7) +verbose stack at MockNpm.exec ({CWD}/lib/npm.js:205:9) error code EBADDEVENGINES error EBADDEVENGINES The developer of this package has specified the following through devEngines error EBADDEVENGINES Invalid devEngines.runtime @@ -200,8 +200,8 @@ verbose stack Error: The developer of this package has specified the following t verbose stack Invalid devEngines.runtime verbose stack Invalid name "nondescript" does not match "node" for "runtime" verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:247:27) -verbose stack at MockNpm.execCommandClass ({CWD}/lib/npm.js:292:7) -verbose stack at MockNpm.exec ({CWD}/lib/npm.js:193:9) +verbose stack at MockNpm.execCommandClass ({CWD}/lib/npm.js:304:7) +verbose stack at MockNpm.exec ({CWD}/lib/npm.js:205:9) error code EBADDEVENGINES error EBADDEVENGINES The developer of this package has specified the following through devEngines error EBADDEVENGINES Invalid devEngines.runtime @@ -226,8 +226,8 @@ verbose stack Error: The developer of this package has specified the following t verbose stack Invalid devEngines.runtime verbose stack Invalid name "nondescript" does not match "node" for "runtime" verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:247:27) -verbose stack at MockNpm.execCommandClass ({CWD}/lib/npm.js:292:7) -verbose stack at MockNpm.exec ({CWD}/lib/npm.js:193:9) +verbose stack at MockNpm.execCommandClass ({CWD}/lib/npm.js:304:7) +verbose stack at MockNpm.exec ({CWD}/lib/npm.js:205:9) error code EBADDEVENGINES error EBADDEVENGINES The developer of this package has specified the following through devEngines error EBADDEVENGINES Invalid devEngines.runtime diff --git a/tap-snapshots/test/lib/commands/publish.js.test.cjs b/tap-snapshots/test/lib/commands/publish.js.test.cjs index e7507118a28f5..8fc57f8458ef4 100644 --- a/tap-snapshots/test/lib/commands/publish.js.test.cjs +++ b/tap-snapshots/test/lib/commands/publish.js.test.cjs @@ -262,10 +262,6 @@ Array [ ] ` -exports[`test/lib/commands/publish.js TAP prioritize CLI flags over publishConfig > new package version 1`] = ` -+ @npmcli/test-package@1.0.0 -` - exports[`test/lib/commands/publish.js TAP public access > must match snapshot 1`] = ` Array [ "package: @npm/test-package@1.0.0", @@ -288,23 +284,6 @@ exports[`test/lib/commands/publish.js TAP public access > new package version 1` + @npm/test-package@1.0.0 ` -exports[`test/lib/commands/publish.js TAP re-loads publishConfig.registry if added during script process > new package version 1`] = ` -+ @npmcli/test-package@1.0.0 -` - -exports[`test/lib/commands/publish.js TAP respects publishConfig.registry, runs appropriate scripts > new package version 1`] = ` - -> @npmcli/test-package@1.0.0 prepublishOnly -> touch scripts-prepublishonly - -> @npmcli/test-package@1.0.0 publish -> touch scripts-publish - -> @npmcli/test-package@1.0.0 postpublish -> touch scripts-postpublish -+ @npmcli/test-package@1.0.0 -` - exports[`test/lib/commands/publish.js TAP restricted access > must match snapshot 1`] = ` Array [ "package: @npm/test-package@1.0.0", diff --git a/tap-snapshots/test/lib/docs.js.test.cjs b/tap-snapshots/test/lib/docs.js.test.cjs index 2f8891f886263..0f8eb72f5c3bb 100644 --- a/tap-snapshots/test/lib/docs.js.test.cjs +++ b/tap-snapshots/test/lib/docs.js.test.cjs @@ -1420,6 +1420,15 @@ forces non-global commands to run in the specified folder. +#### \`prefix-package-name-in-title\` + +* Default: false +* Type: Boolean + +If true, terminal/shell titles will be prefixed with the package name. + + + #### \`preid\` * Default: "" @@ -2371,6 +2380,7 @@ Array [ "prefer-offline", "prefer-online", "prefix", + "prefix-package-name-in-title", "preid", "production", "progress", @@ -2527,6 +2537,7 @@ Array [ "prefer-dedupe", "prefer-offline", "prefer-online", + "prefix-package-name-in-title", "preid", "production", "progress", @@ -2703,6 +2714,7 @@ Object { "preferDedupe": false, "preferOffline": false, "preferOnline": false, + "prefixPackageNameInTitle": false, "preid": "", "progress": false, "projectScope": "",