From 9d7d1e8500bd5f17b65425593f4e5a3db3e791e1 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 13 May 2026 15:58:20 +0800 Subject: [PATCH] Fix zsh integration runtime file modes (#976) --- .../src/terminal/terminal.posix.test.ts | 30 ++++++++++++++++++- packages/server/src/terminal/terminal.ts | 12 +++++--- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/packages/server/src/terminal/terminal.posix.test.ts b/packages/server/src/terminal/terminal.posix.test.ts index 4666d9f94..7c61b4fe9 100644 --- a/packages/server/src/terminal/terminal.posix.test.ts +++ b/packages/server/src/terminal/terminal.posix.test.ts @@ -20,7 +20,7 @@ import { } from "node:fs"; import { spawnSync } from "node:child_process"; import { join } from "node:path"; -import { tmpdir } from "node:os"; +import { tmpdir, userInfo } from "node:os"; const hasZsh = existsSync("/bin/zsh"); @@ -216,6 +216,13 @@ function lastNonEmptyLineIsPrompt(state: ReturnType return last === "$"; } +function removeZshShellIntegrationRuntimeDir(): void { + rmSync(join(tmpdir(), `${userInfo().username || "unknown"}-paseo-zsh`), { + recursive: true, + force: true, + }); +} + describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => { it("sets zsh wrapper env when spawning zsh", () => { const resolvedEnv = buildTerminalEnvironment({ @@ -233,6 +240,27 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => { expect(existsSync(join(resolvedEnv.ZDOTDIR, "paseo-integration.zsh"))).toBe(true); }); + it("reuses zsh shell integration copied from read-only source files", () => { + const integrationSourceDir = mkdtempSync(join(tmpdir(), "paseo-zsh-readonly-source-")); + const tmpHome = mkdtempSync(join(tmpdir(), "paseo-zsh-readonly-home-")); + temporaryDirs.push(integrationSourceDir, tmpHome); + cpSync(resolveZshShellIntegrationDir(), integrationSourceDir, { recursive: true }); + chmodSync(join(integrationSourceDir, ".zshenv"), 0o444); + chmodSync(join(integrationSourceDir, "paseo-integration.zsh"), 0o444); + removeZshShellIntegrationRuntimeDir(); + + const buildEnvironment = () => + buildTerminalEnvironment({ + shell: "/bin/zsh", + env: { HOME: tmpHome }, + zshShellIntegrationDir: integrationSourceDir, + }); + + buildEnvironment(); + + expect(buildEnvironment).not.toThrow(); + }); + describe("send input", () => { it("executes a simple echo command", async () => { const session = trackSession( diff --git a/packages/server/src/terminal/terminal.ts b/packages/server/src/terminal/terminal.ts index c83c1f279..0f761954e 100644 --- a/packages/server/src/terminal/terminal.ts +++ b/packages/server/src/terminal/terminal.ts @@ -1,12 +1,13 @@ import * as pty from "node-pty"; import xterm, { type Terminal as TerminalType } from "@xterm/headless"; import { randomUUID } from "crypto"; -import { chmodSync, copyFileSync, existsSync, mkdirSync, statSync } from "node:fs"; +import { chmodSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs"; import { tmpdir, userInfo } from "node:os"; import { basename, dirname, join } from "node:path"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; import { createExternalProcessEnv } from "../server/paseo-env.js"; +import { writePrivateFileAtomicSync } from "../server/private-files.js"; import type { TerminalCell, TerminalState } from "../shared/messages.js"; const { Terminal } = xterm; @@ -202,10 +203,13 @@ function prepareZshShellIntegrationRuntimeDir(sourceDir = resolveZshShellIntegra const runtimeDir = resolveZshShellIntegrationRuntimeDir(); mkdirSync(runtimeDir, { recursive: true, mode: 0o700 }); chmodSync(runtimeDir, 0o700); - copyFileSync(join(readableSourceDir, ".zshenv"), join(runtimeDir, ".zshenv")); - copyFileSync( - join(readableSourceDir, "paseo-integration.zsh"), + writePrivateFileAtomicSync( + join(runtimeDir, ".zshenv"), + readFileSync(join(readableSourceDir, ".zshenv")), + ); + writePrivateFileAtomicSync( join(runtimeDir, "paseo-integration.zsh"), + readFileSync(join(readableSourceDir, "paseo-integration.zsh")), ); return runtimeDir; }