fix(install): share arguments between the plan and npm execution

Include the Linux user prefix in the planned command. Build the install
arguments once and shell-escape the printed arguments so paths with spaces
or metacharacters remain reusable. Cover dry-run admission, exact argument
round-trip and the unprefixed macOS command with native Bash 3.2.

Refs: #1417
This commit is contained in:
morluto
2026-10-11 06:41:51 +08:00
parent 8b4c96e6c6
commit 60fbb2f06e
2 changed files with 47 additions and 4 deletions
+7 -4
View File
@@ -85,19 +85,22 @@ if [[ "$version" =~ \+ ]]; then
fail "npm cannot install an exact version with build metadata. Pass a version without build metadata, then retry."
fi
prefix_args=()
install_args=(install --global)
if [[ "$platform" == "Linux" ]]; then
prefix_args=(--prefix "$HOME/.local")
install_args+=(--prefix "$HOME/.local")
install_bin="$HOME/.local/bin/rea"
else
npm_prefix="$(npm prefix --global)" || fail "the npm global prefix could not be read. Repair the npm configuration, then retry."
install_bin="$npm_prefix/bin/rea"
fi
install_args+=("$PACKAGE@$version")
printf 'REA install plan\n'
printf ' Version: %s\n' "$version"
printf ' Runtime: Node.js %s\n' "$node_version"
printf ' Command: npm install --global %s@%s\n' "$PACKAGE" "$version"
printf ' Command:'
printf ' %q' npm "${install_args[@]}"
printf '\n'
printf ' Binary: %s\n' "$install_bin"
printf ' Setup: %s\n' "$([[ "$start_setup" == true ]] && printf 'start when a terminal is available' || printf 'skipped')"
@@ -107,7 +110,7 @@ if [[ "$dry_run" == true ]]; then
fi
printf 'Installing %s@%s...\n' "$PACKAGE" "$version"
npm install --global ${prefix_args[@]+"${prefix_args[@]}"} "$PACKAGE@$version" || fail "npm could not install REA. Check registry access and npm permissions, then retry."
npm "${install_args[@]}" || fail "npm could not install REA. Check registry access and npm permissions, then retry."
[[ -x "$install_bin" ]] || fail "npm completed without installing the rea command. Check the npm global bin directory and PATH, then retry."
installed_version="$("$install_bin" --version 2>/dev/null | tr -d '[:space:]')" || fail "the installed REA version could not be read. Reinstall the requested version, then retry."
[[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version. Reinstall the requested version, then retry."
@@ -172,6 +172,42 @@ describe("curl installer scenarios", { timeout: 20_000 }, () => {
});
});
describe("Linux user-prefix installer", { timeout: 20_000 }, () => {
it("prints the executed arguments as a reusable command with shell metacharacters", async () => {
const fixture = await createFixture();
const home = join(fixture.home, "home with 'quotes' $cash; [glob]");
const argvLog = join(fixture.home, "npm-argv.log");
const overrides = { HOME: home, FAKE_NPM_ARGV_LOG: argvLog };
const plan = await runInstaller(
fixture,
["--version", "0.3.0", "--dry-run"],
overrides,
);
await expect(readFile(argvLog)).rejects.toMatchObject({ code: "ENOENT" });
const command = /^ Command: (.+)$/mu.exec(plan.stdout)?.[1];
expect(command).toBeDefined();
const printed = await execFileAsync("/bin/bash", [
"-c",
`npm() { printf '%s\\0' "$@"; }\n${command}`,
]);
const installed = await runInstaller(
fixture,
["--version", "0.3.0"],
overrides,
);
expect(installed.stdout).toContain(` Command: ${command}\n`);
expect(printed.stdout.split("\0")).toEqual([
"install",
"--global",
"--prefix",
join(home, ".local"),
"[email protected]",
"",
]);
expect((await readFile(argvLog)).toString()).toBe(printed.stdout);
});
});
describe("native macOS installer", { timeout: 20_000 }, () => {
it("installs on Darwin with an empty prefix argument array", async () => {
const fixture = await createFixture();
@@ -179,6 +215,9 @@ describe("native macOS installer", { timeout: 20_000 }, () => {
FAKE_PLATFORM: "Darwin",
FAKE_NPM_PREFIX: join(fixture.home, "npm global prefix"),
});
expect(result.stdout).toContain(
" Command: npm install --global [email protected]\n",
);
expect(result.stdout).toContain("REA 0.3.0 is installed");
expect(await readFile(fixture.npmLog, "utf8")).toBe(
"prefix --global\ninstall --global [email protected]\n",
@@ -322,6 +361,7 @@ if [ "$1" = "-p" ]; then printf '%s\n' "\${FAKE_NODE_VERSION:-24.18.0}"; else ex
join(bin, "npm"),
`#!/bin/sh
printf '%s\n' "$*" >> "$FAKE_NPM_LOG"
if [ -n "\${FAKE_NPM_ARGV_LOG:-}" ]; then printf '%s\\0' "$@" > "$FAKE_NPM_ARGV_LOG"; fi
[ "$1" = "prefix" ] && { [ "\${FAKE_NPM_PREFIX_FAIL:-}" = "1" ] && exit 1; printf '%s\n' "$FAKE_NPM_PREFIX"; exit 0; }
[ "\${FAKE_NPM_FAIL:-}" = "1" ] && exit 1
prefix="$HOME/.local"