diff --git a/scripts/install-cli.sh b/scripts/install-cli.sh index ee8dd0956d78..b6ae8bb93025 100755 --- a/scripts/install-cli.sh +++ b/scripts/install-cli.sh @@ -29,6 +29,18 @@ ensure_home_env() { ensure_home_env +# Track temp paths so fail/exit paths do not leak mktemp dirs/files. +# Register paths in the caller: command substitutions run in a subshell, so +# array mutations inside a helper would not reach this shell. +TMPFILES=() +cleanup_tmpfiles() { + local f + for f in "${TMPFILES[@]:-}"; do + rm -rf "$f" 2>/dev/null || true + done +} +trap cleanup_tmpfiles EXIT + resolve_openclaw_effective_home() { local openclaw_home="${OPENCLAW_HOME:-}" if [[ -z "$openclaw_home" ]]; then @@ -819,6 +831,7 @@ install_node() { mkdir -p "${PREFIX}/tools" tmp="$(mktemp -d)" + TMPFILES+=("$tmp") base_url="https://nodejs.org/dist/v${NODE_VERSION}" tarball="node-v${NODE_VERSION}-${os}-${arch}.tar.gz" url="${base_url}/${tarball}" @@ -1055,6 +1068,7 @@ ensure_pnpm_git_prepare_allowlist() { if [[ -f "$workspace_file" ]] && ! grep -Fq "\"${dep}\"" "$workspace_file" && ! grep -Fq "${dep}:" "$workspace_file" && ! grep -Fq -- "- ${dep}" "$workspace_file"; then tmp="$(mktemp)" + TMPFILES+=("$tmp") if grep -q '^allowBuilds:[[:space:]]*$' "$workspace_file"; then awk -v dep="$dep" ' BEGIN { inserted = 0 } diff --git a/test/scripts/install-cli.test.ts b/test/scripts/install-cli.test.ts index 0fad992b8e49..d4d82b06eaaf 100644 --- a/test/scripts/install-cli.test.ts +++ b/test/scripts/install-cli.test.ts @@ -757,6 +757,67 @@ describe("install-cli.sh", () => { } }); + it("removes the Node staging directory when download fails", () => { + const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-node-cleanup-")); + const prefix = join(tmp, "prefix"); + const stagingDir = join(tmp, "node-staging"); + + try { + const result = runInstallCliShell( + [ + "set -euo pipefail", + `cd ${JSON.stringify(process.cwd())}`, + `source ${JSON.stringify(SCRIPT_PATH)}`, + "os_detect() { printf 'linux\\n'; }", + "arch_detect() { printf 'x64\\n'; }", + "is_musl_linux() { return 1; }", + "linked_node_is_usable() { return 1; }", + "detect_downloader() { :; }", + "require_bin() { :; }", + `mktemp() { mkdir -p ${JSON.stringify(stagingDir)}; printf '%s\\n' ${JSON.stringify(stagingDir)}; }`, + "download_file() { return 42; }", + `PREFIX=${JSON.stringify(prefix)}`, + "NODE_VERSION=22.22.0", + "install_node", + ].join("\n"), + ); + + expect(result.status).toBe(42); + expect(() => lstatSync(stagingDir)).toThrow(); + } finally { + rmSync(tmp, { force: true, recursive: true }); + } + }); + + it("removes the workspace rewrite temp file when rewriting fails", () => { + const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-workspace-cleanup-")); + const repo = join(tmp, "repo"); + const workspaceFile = join(repo, "pnpm-workspace.yaml"); + const rewriteTemp = join(tmp, "workspace-rewrite"); + const workspace = 'packages:\n - "packages/*"\n\nallowBuilds:\n'; + mkdirSync(repo, { recursive: true }); + writeFileSync(workspaceFile, workspace); + + try { + const result = runInstallCliShell( + [ + "set -euo pipefail", + `cd ${JSON.stringify(process.cwd())}`, + `source ${JSON.stringify(SCRIPT_PATH)}`, + `mktemp() { : > ${JSON.stringify(rewriteTemp)}; printf '%s\\n' ${JSON.stringify(rewriteTemp)}; }`, + "awk() { return 43; }", + `ensure_pnpm_git_prepare_allowlist ${JSON.stringify(repo)}`, + ].join("\n"), + ); + + expect(result.status).toBe(43); + expect(() => lstatSync(rewriteTemp)).toThrow(); + expect(readFileSync(workspaceFile, "utf8")).toBe(workspace); + } finally { + rmSync(tmp, { force: true, recursive: true }); + } + }); + it("clears npm freshness filters for package installs", () => { expect(script).toContain('freshness_flag="--min-release-age=0"'); expect(script).toContain('npm_config_has_raw_key "$(npm_bin)" "min-release-age"');