From 60fbb2f06eedcd1f5170226a5118e956a6fc9dd0 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 11 Oct 2026 06:41:51 +0800 Subject: [PATCH] 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 --- install.sh | 11 +++-- .../boundary/filesystem/installScript.test.ts | 40 +++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index d059694844..1fa70b9c11 100755 --- a/install.sh +++ b/install.sh @@ -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." diff --git a/tests/boundary/filesystem/installScript.test.ts b/tests/boundary/filesystem/installScript.test.ts index 57450fee4d..4984d37ee9 100644 --- a/tests/boundary/filesystem/installScript.test.ts +++ b/tests/boundary/filesystem/installScript.test.ts @@ -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"), + "rea-agents@0.3.0", + "", + ]); + 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 rea-agents@0.3.0\n", + ); expect(result.stdout).toContain("REA 0.3.0 is installed"); expect(await readFile(fixture.npmLog, "utf8")).toBe( "prefix --global\ninstall --global rea-agents@0.3.0\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"