fix: preserve zsh prompt in packaged desktop (#544)

This commit is contained in:
Mohamed Boudra
2026-04-24 13:54:09 +07:00
parent ef418d5292
commit 231ebdcfb1
4 changed files with 108 additions and 3 deletions

View File

@@ -9,6 +9,7 @@ files:
- dist/**/*
asarUnpack:
- dist/daemon/node-entrypoint-runner.js
- node_modules/@getpaseo/server/dist/server/terminal/shell-integration/**/*
extraResources:
- from: ../app/dist
to: app-dist

View File

@@ -0,0 +1,19 @@
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
describe("desktop packaging", () => {
it("unpacks server zsh shell integration files for external shells", () => {
const config = readFileSync(join(packageRoot, "electron-builder.yml"), "utf8");
expect(config).toContain(
"node_modules/@getpaseo/server/dist/server/terminal/shell-integration/**/*",
);
expect(config).not.toContain(
"node_modules/@getpaseo/server/dist/src/terminal/shell-integration/**/*",
);
});
});

View File

@@ -11,6 +11,7 @@ import {
} from "./terminal.js";
import {
chmodSync,
cpSync,
existsSync,
mkdtempSync,
mkdirSync,
@@ -18,6 +19,7 @@ import {
statSync,
writeFileSync,
} from "node:fs";
import { spawnSync } from "node:child_process";
import { join } from "node:path";
import { tmpdir } from "node:os";
@@ -208,7 +210,9 @@ describe("createTerminal", () => {
expect(resolvedEnv.TERM).toBe("xterm-256color");
expect(resolvedEnv.PASEO_ZSH_ZDOTDIR).toBe("/tmp/paseo-zdotdir");
expect(resolvedEnv.ZDOTDIR).toBe(resolveZshShellIntegrationDir());
expect(resolvedEnv.ZDOTDIR).not.toBe("/tmp/paseo-zdotdir");
expect(existsSync(join(resolvedEnv.ZDOTDIR, ".zshenv"))).toBe(true);
expect(existsSync(join(resolvedEnv.ZDOTDIR, "paseo-integration.zsh"))).toBe(true);
});
it("uses custom name when provided", async () => {
@@ -588,6 +592,58 @@ describe("terminal title", () => {
await waitForTitle(session, (title) => title === "~/dev/faro", 4000);
});
it.skipIf(!hasZsh)("loads the user's zsh prompt when the integration dir is packaged", () => {
const homeDir = mkdtempSync(join(tmpdir(), "terminal-zsh-packaged-home-"));
temporaryDirs.push(homeDir);
writeFileSync(join(homeDir, ".zshrc"), "PS1='PASEO_CUSTOM_PROMPT> '\n");
const fakeAppRoot = join(homeDir, "Paseo.app", "Contents", "Resources");
const inaccessiblePackagedIntegrationDir = join(
fakeAppRoot,
"app.asar",
"node_modules",
"@getpaseo",
"server",
"dist",
"server",
"terminal",
"shell-integration",
"zsh",
);
const unpackedIntegrationDir = join(
fakeAppRoot,
"app.asar.unpacked",
"node_modules",
"@getpaseo",
"server",
"dist",
"server",
"terminal",
"shell-integration",
"zsh",
);
mkdirSync(unpackedIntegrationDir, { recursive: true });
cpSync(resolveZshShellIntegrationDir(), unpackedIntegrationDir, { recursive: true });
writeFileSync(join(fakeAppRoot, "app.asar"), "asar archive placeholder");
const env = buildTerminalEnvironment({
shell: "/bin/zsh",
env: {
HOME: homeDir,
},
zshShellIntegrationDir: inaccessiblePackagedIntegrationDir,
});
const result = spawnSync("/bin/zsh", ["-i", "-c", "print -r -- ${PROMPT}"], {
cwd: homeDir,
env,
encoding: "utf8",
});
expect(result.status).toBe(0);
expect(result.stdout.split(/\r?\n/)).toContain("PASEO_CUSTOM_PROMPT> ");
});
it.skipIf(!hasZsh)("emits zsh shell integration command completion", async () => {
const homeDir = mkdtempSync(join(tmpdir(), "terminal-zsh-command-finished-home-"));
temporaryDirs.push(homeDir);

View File

@@ -1,7 +1,8 @@
import * as pty from "node-pty";
import xterm, { type Terminal as TerminalType } from "@xterm/headless";
import { randomUUID } from "crypto";
import { chmodSync, existsSync, statSync } from "node:fs";
import { chmodSync, copyFileSync, existsSync, mkdirSync, 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";
@@ -85,6 +86,7 @@ export interface CreateTerminalOptions {
interface BuildTerminalEnvironmentInput {
shell: string;
env: Record<string, string>;
zshShellIntegrationDir?: string;
}
export interface CaptureTerminalLinesOptions {
@@ -182,6 +184,33 @@ export function resolveZshShellIntegrationDir(): string {
return fileURLToPath(new URL("./shell-integration/zsh", import.meta.url));
}
function resolveExternalProcessPath(filePath: string): string {
return filePath.replace(/\.asar(?=\/|$)/, ".asar.unpacked");
}
function resolveZshShellIntegrationRuntimeDir(): string {
let username = "unknown";
try {
username = userInfo().username || username;
} catch {
// keep fallback
}
return join(tmpdir(), `${username}-paseo-zsh`);
}
function prepareZshShellIntegrationRuntimeDir(sourceDir = resolveZshShellIntegrationDir()): string {
const readableSourceDir = resolveExternalProcessPath(sourceDir);
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"),
join(runtimeDir, "paseo-integration.zsh"),
);
return runtimeDir;
}
export function buildTerminalEnvironment(
input: BuildTerminalEnvironmentInput,
): Record<string, string> {
@@ -199,7 +228,7 @@ export function buildTerminalEnvironment(
return {
...baseEnv,
PASEO_ZSH_ZDOTDIR: originalZdotdir,
ZDOTDIR: resolveZshShellIntegrationDir(),
ZDOTDIR: prepareZshShellIntegrationRuntimeDir(input.zshShellIntegrationDir),
};
}