diff --git a/package.json b/package.json index 6f603479..b0f6febf 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "@salesforce/plugin-info": "^4.0.0", "@salesforce/sf-plugins-core": "^13.0.0", "@salesforce/ts-types": "^3.0.0", - "open": "^10.2.0" + "open": "^10.2.0", + "which": "^4" }, "devDependencies": { "@oclif/plugin-command-snapshot": "^5.3.20", @@ -23,6 +24,7 @@ "@salesforce/dev-scripts": "^11.0.4", "@salesforce/plugin-command-reference": "^3.1.101", "@salesforce/ts-sinon": "^1.4.34", + "@types/which": "^3", "eslint-plugin-sf-plugin": "^1.20.33", "oclif": "^4.23.16", "ts-node": "^10.9.2", diff --git a/src/hooks/diagnostics.ts b/src/hooks/diagnostics.ts index 196ade06..82a26eec 100644 --- a/src/hooks/diagnostics.ts +++ b/src/hooks/diagnostics.ts @@ -16,8 +16,9 @@ import childProcess from 'node:child_process'; import { ExecFileOptions } from 'node:child_process'; import util from 'node:util'; -import { join } from 'node:path'; +import { dirname, join, resolve } from 'node:path'; import fs from 'node:fs'; +import which from 'which'; import { Global, Lifecycle, Logger, Messages } from '@salesforce/core'; import { SfDoctor, SfDoctorDiagnosis } from '@salesforce/plugin-info'; import { asString, isString } from '@salesforce/ts-types'; @@ -42,15 +43,29 @@ const pluginName = '@salesforce/plugin-auth'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages(pluginName, 'diagnostics'); -// Use execFile instead of exec to avoid shell interpretation. -// exec invokes cmd.exe on Windows, which resolves commands from CWD before PATH. +// Resolve npm from PATH only, excluding CWD. On Windows, both execFile (CreateProcess) +// and the `which` module resolve executables from CWD before PATH. let execFile: PromisifiedExecFile; +let npmPath: string; export const hook: HookFunction = async (options) => { getLogger().debug(`Running SfDoctor diagnostics for ${pluginName}`); - execFile = util.promisify(childProcess.execFile) as unknown as PromisifiedExecFile; + + const cwd = resolve(process.cwd()); + const resolved = which.sync('npm', { nothrow: true }); + if (!resolved || dirname(resolve(resolved)) === cwd) { + getLogger().warn(`Unable to run SfDoctor diagnostics for ${pluginName}: npm not found on PATH`); + return Promise.resolve([undefined]); + } + npmPath = resolved; + + const useShell = /\.(cmd|bat)$/i.test(npmPath); + const rawExecFile = util.promisify(childProcess.execFile) as unknown as PromisifiedExecFile; + execFile = (cmd: string, args: string[], opts?: ExecFileOptions): Promise<{ stdout: string; stderr: string }> => + rawExecFile(cmd, args, { ...opts, shell: useShell }); + try { - await execFile('npm', ['-v']); + await execFile(npmPath, ['-v']); return await Promise.all([cryptoVersionTest(options.doctor)]); } catch (e: unknown) { const errMsg = e instanceof Error ? e.message : isString(e) ? e : 'unknown'; @@ -159,7 +174,7 @@ const supportsCliV2Crypto = async (doctor: SfDoctor): Promise => { // check core CLI if (root?.length) { try { - const { stdout } = await execFile('npm', ['explain', '@salesforce/core', '--json'], { cwd: root }); + const { stdout } = await execFile(npmPath, ['explain', '@salesforce/core', '--json'], { cwd: root }); const coreExplanation = JSON.parse(stdout) as NpmExplanation[]; coreSupportsV2 = coreExplanation.every((exp) => exp?.version > '6.6.0'); } catch (e: unknown) { @@ -170,7 +185,7 @@ const supportsCliV2Crypto = async (doctor: SfDoctor): Promise => { // check installed plugins if (dataDir?.length) { try { - const { stdout } = await execFile('npm', ['explain', '@salesforce/core', '--json'], { cwd: dataDir }); + const { stdout } = await execFile(npmPath, ['explain', '@salesforce/core', '--json'], { cwd: dataDir }); const pluginsExplanation = JSON.parse(stdout) as NpmExplanation[]; pluginsSupportV2 = pluginsExplanation?.length ? pluginsExplanation.every((exp) => exp?.version > '6.6.0') : true; } catch (e: unknown) { @@ -192,7 +207,7 @@ const supportsCliV2Crypto = async (doctor: SfDoctor): Promise => { // last entry is the path. E.g., "auth 3.3.17 (link) /Users/me/dev/plugin-auth", const pluginPath = pluginEntry.split(' ')?.pop(); if (pluginPath?.length) { - const { stdout } = await execFile('npm', ['explain', '@salesforce/core', '--json'], { cwd: pluginPath }); + const { stdout } = await execFile(npmPath, ['explain', '@salesforce/core', '--json'], { cwd: pluginPath }); const linksExplanation = JSON.parse(stdout) as NpmExplanation[]; return linksExplanation?.every((exp) => exp?.version > '6.6.0'); } diff --git a/yarn.lock b/yarn.lock index ab6306a1..536b3183 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1983,6 +1983,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20" integrity sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ== +"@types/which@^3": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/which/-/which-3.0.4.tgz#2c3a89be70c56a84a6957a7264639f39ae4340a1" + integrity sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w== + "@types/wrap-ansi@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd" @@ -4887,6 +4892,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +isexe@^3.1.1: + version "3.1.5" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.5.tgz#42e368f68d5e10dadfee4fda7b550bc2d8892dc9" + integrity sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w== + istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" @@ -7692,6 +7702,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +which@^4: + version "4.0.0" + resolved "https://registry.yarnpkg.com/which/-/which-4.0.0.tgz#cd60b5e74503a3fbcfbf6cd6b4138a8bae644c1a" + integrity sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg== + dependencies: + isexe "^3.1.1" + widest-line@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca"