fix(cli): surface package manager command execution failures

This commit is contained in:
root 2026-02-27 01:09:45 +08:00
parent 1177d38087
commit 62e262785f
11 changed files with 95 additions and 58 deletions

View file

@ -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 };
}

View file

@ -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?",
]);
});
});

View file

@ -1,6 +1,7 @@
import { ui } from "../../environment/userInteraction.js"; import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js"; import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
/** /**
* @returns {import("../currentPackageManager.js").PackageManager} * @returns {import("../currentPackageManager.js").PackageManager}
@ -43,11 +44,6 @@ async function runBunCommand(command, args) {
}); });
return { status: result.status }; return { status: result.status };
} catch (/** @type any */ error) { } catch (/** @type any */ error) {
if (error.status) { return reportCommandExecutionFailure(error, command);
return { status: error.status };
} else {
ui.writeError("Error executing command:", error.message);
return { status: 1 };
}
} }
} }

View file

@ -1,6 +1,7 @@
import { ui } from "../../environment/userInteraction.js"; import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js"; import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
/** /**
* @param {string[]} args * @param {string[]} args
@ -15,11 +16,6 @@ export async function runNpm(args) {
}); });
return { status: result.status }; return { status: result.status };
} catch (/** @type any */ error) { } catch (/** @type any */ error) {
if (error.status) { return reportCommandExecutionFailure(error, "npm");
return { status: error.status };
} else {
ui.writeError("Error executing command:", error.message);
return { status: 1 };
}
} }
} }

View file

@ -1,6 +1,7 @@
import { ui } from "../../environment/userInteraction.js"; import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js"; import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
/** /**
* @param {string[]} args * @param {string[]} args
@ -15,11 +16,6 @@ export async function runNpx(args) {
}); });
return { status: result.status }; return { status: result.status };
} catch (/** @type any */ error) { } catch (/** @type any */ error) {
if (error.status) { return reportCommandExecutionFailure(error, "npx");
return { status: error.status };
} else {
ui.writeError("Error executing command:", error.message);
return { status: 1 };
}
} }
} }

View file

@ -9,6 +9,7 @@ import os from "node:os";
import path from "node:path"; import path from "node:path";
import ini from "ini"; import ini from "ini";
import { spawn } from "child_process"; import { spawn } from "child_process";
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
/** /**
* Checks if this pip invocation should bypass safe-chain and spawn directly. * 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 }; return { status: result.status };
} catch (/** @type any */ error) { } catch (/** @type any */ error) {
if (error.status) { return reportCommandExecutionFailure(error, command);
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 };
}
} }
} }

View file

@ -2,6 +2,7 @@ import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js"; import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
/** /**
* Sets CA bundle environment variables used by Python libraries and pipx. * 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 }; return { status: result.status };
} catch (/** @type any */ error) { } catch (/** @type any */ error) {
if (error.status) { return reportCommandExecutionFailure(error, command);
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 };
}
} }
} }

View file

@ -1,6 +1,7 @@
import { ui } from "../../environment/userInteraction.js"; import { ui } from "../../environment/userInteraction.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { safeSpawn } from "../../utils/safeSpawn.js"; import { safeSpawn } from "../../utils/safeSpawn.js";
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
/** /**
* @param {string[]} args * @param {string[]} args
@ -26,11 +27,7 @@ export async function runPnpmCommand(args, toolName = "pnpm") {
return { status: result.status }; return { status: result.status };
} catch (/** @type any */ error) { } catch (/** @type any */ error) {
if (error.status) { const target = toolName === "pnpm" ? "pnpm" : "pnpx";
return { status: error.status }; return reportCommandExecutionFailure(error, target);
} else {
ui.writeError("Error executing command:", error.message);
return { status: 1 };
}
} }
} }

View file

@ -2,6 +2,7 @@ import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js"; import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
/** /**
* @returns {import("../currentPackageManager.js").PackageManager} * @returns {import("../currentPackageManager.js").PackageManager}
@ -66,12 +67,6 @@ async function runPoetryCommand(args) {
return { status: result.status }; return { status: result.status };
} catch (/** @type any */ error) { } catch (/** @type any */ error) {
if (error.status) { return reportCommandExecutionFailure(error, "poetry");
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 };
}
} }
} }

View file

@ -2,6 +2,7 @@ import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js"; import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js"; import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
/** /**
* Sets CA bundle environment variables used by Python libraries and uv. * 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 }; return { status: result.status };
} catch (/** @type any */ error) { } catch (/** @type any */ error) {
if (error.status) { return reportCommandExecutionFailure(error, command);
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 };
}
} }
} }

View file

@ -1,6 +1,7 @@
import { ui } from "../../environment/userInteraction.js"; import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js"; import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
/** /**
* @param {string[]} args * @param {string[]} args
@ -18,12 +19,7 @@ export async function runYarnCommand(args) {
}); });
return { status: result.status }; return { status: result.status };
} catch (/** @type any */ error) { } catch (/** @type any */ error) {
if (error.status) { return reportCommandExecutionFailure(error, "yarn");
return { status: error.status };
} else {
ui.writeError("Error executing command:", error.message);
return { status: 1 };
}
} }
} }