diff --git a/packages/safe-chain/src/packagemanager/_shared/commandErrors.js b/packages/safe-chain/src/packagemanager/_shared/commandErrors.js new file mode 100644 index 0000000..bee68e4 --- /dev/null +++ b/packages/safe-chain/src/packagemanager/_shared/commandErrors.js @@ -0,0 +1,17 @@ +import { ui } from "../../environment/userInteraction.js"; + +/** + * Centralized logging for package-manager command launch failures. + * + * @param {any} error - Error thrown by safeSpawn while preparing/running the command. + * @param {string} command - Command name that failed to execute. + * @returns {{status: number}} + */ +export function reportCommandExecutionFailure(error, command) { + const message = typeof error?.message === "string" ? error.message : "Unknown error"; + ui.writeError(`Error executing command: ${message}`); + + ui.writeError(`Is '${command}' installed and available on your system?`); + + return { status: typeof error?.status === "number" ? error.status : 1 }; +} diff --git a/packages/safe-chain/src/packagemanager/_shared/commandErrors.spec.js b/packages/safe-chain/src/packagemanager/_shared/commandErrors.spec.js new file mode 100644 index 0000000..350228a --- /dev/null +++ b/packages/safe-chain/src/packagemanager/_shared/commandErrors.spec.js @@ -0,0 +1,59 @@ +import { describe, it, beforeEach, afterEach, mock } from "node:test"; +import assert from "node:assert"; + +describe("reportCommandExecutionFailure", () => { + let errorLines; + + beforeEach(async () => { + errorLines = []; + + mock.module("../../environment/userInteraction.js", { + namedExports: { + ui: { + writeError: (...args) => { + errorLines.push(args.join(" ")); + }, + }, + }, + }); + }); + + afterEach(() => { + mock.reset(); + }); + + it("reports command errors while preserving exit status", async () => { + const { reportCommandExecutionFailure } = await import("./commandErrors.js"); + + const result = reportCommandExecutionFailure( + { + status: 127, + message: "Command failed: command -v bun", + }, + "bun", + ); + + assert.deepStrictEqual(result, { status: 127 }); + assert.deepStrictEqual(errorLines, [ + "Error executing command: Command failed: command -v bun", + "Is 'bun' installed and available on your system?", + ]); + }); + + it("falls back to exit code 1 when status is missing", async () => { + const { reportCommandExecutionFailure } = await import("./commandErrors.js"); + + const result = reportCommandExecutionFailure( + { + message: "Network error", + }, + "npm", + ); + + assert.deepStrictEqual(result, { status: 1 }); + assert.deepStrictEqual(errorLines, [ + "Error executing command: Network error", + "Is 'npm' installed and available on your system?", + ]); + }); +}); diff --git a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js index 037a512..1138203 100644 --- a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js +++ b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js @@ -1,6 +1,7 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; +import { reportCommandExecutionFailure } from "../_shared/commandErrors.js"; /** * @returns {import("../currentPackageManager.js").PackageManager} @@ -43,11 +44,6 @@ async function runBunCommand(command, args) { }); return { status: result.status }; } catch (/** @type any */ error) { - if (error.status) { - return { status: error.status }; - } else { - ui.writeError("Error executing command:", error.message); - return { status: 1 }; - } + return reportCommandExecutionFailure(error, command); } } diff --git a/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js b/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js index af57fad..4a1f0b1 100644 --- a/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js +++ b/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js @@ -1,6 +1,7 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; +import { reportCommandExecutionFailure } from "../_shared/commandErrors.js"; /** * @param {string[]} args @@ -15,11 +16,6 @@ export async function runNpm(args) { }); return { status: result.status }; } catch (/** @type any */ error) { - if (error.status) { - return { status: error.status }; - } else { - ui.writeError("Error executing command:", error.message); - return { status: 1 }; - } + return reportCommandExecutionFailure(error, "npm"); } } diff --git a/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js b/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js index 2501b79..6aebc3e 100644 --- a/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js +++ b/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js @@ -1,6 +1,7 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; +import { reportCommandExecutionFailure } from "../_shared/commandErrors.js"; /** * @param {string[]} args @@ -15,11 +16,6 @@ export async function runNpx(args) { }); return { status: result.status }; } catch (/** @type any */ error) { - if (error.status) { - return { status: error.status }; - } else { - ui.writeError("Error executing command:", error.message); - return { status: 1 }; - } + return reportCommandExecutionFailure(error, "npx"); } } diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 83bc03e..4f4e401 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -9,6 +9,7 @@ import os from "node:os"; import path from "node:path"; import ini from "ini"; import { spawn } from "child_process"; +import { reportCommandExecutionFailure } from "../_shared/commandErrors.js"; /** * Checks if this pip invocation should bypass safe-chain and spawn directly. @@ -203,12 +204,6 @@ export async function runPip(command, args) { return { status: result.status }; } catch (/** @type any */ error) { - if (error.status) { - return { status: error.status }; - } else { - ui.writeError(`Error executing command: ${error.message}`); - ui.writeError(`Is '${command}' installed and available on your system?`); - return { status: 1 }; - } + return reportCommandExecutionFailure(error, command); } } diff --git a/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.js b/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.js index 2f70cfa..c374e2a 100644 --- a/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.js +++ b/packages/safe-chain/src/packagemanager/pipx/runPipXCommand.js @@ -2,6 +2,7 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; +import { reportCommandExecutionFailure } from "../_shared/commandErrors.js"; /** * Sets CA bundle environment variables used by Python libraries and pipx. @@ -54,12 +55,6 @@ export async function runPipX(command, args) { return { status: result.status }; } catch (/** @type any */ error) { - if (error.status) { - return { status: error.status }; - } else { - ui.writeError(`Error executing command: ${error.message}`); - ui.writeError(`Is '${command}' installed and available on your system?`); - return { status: 1 }; - } + return reportCommandExecutionFailure(error, command); } } diff --git a/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js b/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js index d958fb8..cad4afe 100644 --- a/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js +++ b/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js @@ -1,6 +1,7 @@ import { ui } from "../../environment/userInteraction.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; +import { reportCommandExecutionFailure } from "../_shared/commandErrors.js"; /** * @param {string[]} args @@ -26,11 +27,7 @@ export async function runPnpmCommand(args, toolName = "pnpm") { return { status: result.status }; } catch (/** @type any */ error) { - if (error.status) { - return { status: error.status }; - } else { - ui.writeError("Error executing command:", error.message); - return { status: 1 }; - } + const target = toolName === "pnpm" ? "pnpm" : "pnpx"; + return reportCommandExecutionFailure(error, target); } } diff --git a/packages/safe-chain/src/packagemanager/poetry/createPoetryPackageManager.js b/packages/safe-chain/src/packagemanager/poetry/createPoetryPackageManager.js index c8094e5..567fb43 100644 --- a/packages/safe-chain/src/packagemanager/poetry/createPoetryPackageManager.js +++ b/packages/safe-chain/src/packagemanager/poetry/createPoetryPackageManager.js @@ -2,6 +2,7 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; +import { reportCommandExecutionFailure } from "../_shared/commandErrors.js"; /** * @returns {import("../currentPackageManager.js").PackageManager} @@ -66,12 +67,6 @@ async function runPoetryCommand(args) { return { status: result.status }; } catch (/** @type any */ error) { - if (error.status) { - return { status: error.status }; - } else { - ui.writeError("Error executing command:", error.message); - ui.writeError("Is 'poetry' installed and available on your system?"); - return { status: 1 }; - } + return reportCommandExecutionFailure(error, "poetry"); } } diff --git a/packages/safe-chain/src/packagemanager/uv/runUvCommand.js b/packages/safe-chain/src/packagemanager/uv/runUvCommand.js index ed02fe3..7c22518 100644 --- a/packages/safe-chain/src/packagemanager/uv/runUvCommand.js +++ b/packages/safe-chain/src/packagemanager/uv/runUvCommand.js @@ -2,6 +2,7 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; +import { reportCommandExecutionFailure } from "../_shared/commandErrors.js"; /** * Sets CA bundle environment variables used by Python libraries and uv. @@ -60,12 +61,6 @@ export async function runUv(command, args) { return { status: result.status }; } catch (/** @type any */ error) { - if (error.status) { - return { status: error.status }; - } else { - ui.writeError(`Error executing command: ${error.message}`); - ui.writeError(`Is '${command}' installed and available on your system?`); - return { status: 1 }; - } + return reportCommandExecutionFailure(error, command); } } diff --git a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js index 2089551..cdf216f 100644 --- a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js +++ b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js @@ -1,6 +1,7 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; +import { reportCommandExecutionFailure } from "../_shared/commandErrors.js"; /** * @param {string[]} args @@ -18,12 +19,7 @@ export async function runYarnCommand(args) { }); return { status: result.status }; } catch (/** @type any */ error) { - if (error.status) { - return { status: error.status }; - } else { - ui.writeError("Error executing command:", error.message); - return { status: 1 }; - } + return reportCommandExecutionFailure(error, "yarn"); } }