fix: preserve zsh prompt in packaged desktop (#544)
This commit is contained in:
@@ -9,6 +9,7 @@ files:
|
|||||||
- dist/**/*
|
- dist/**/*
|
||||||
asarUnpack:
|
asarUnpack:
|
||||||
- dist/daemon/node-entrypoint-runner.js
|
- dist/daemon/node-entrypoint-runner.js
|
||||||
|
- node_modules/@getpaseo/server/dist/server/terminal/shell-integration/**/*
|
||||||
extraResources:
|
extraResources:
|
||||||
- from: ../app/dist
|
- from: ../app/dist
|
||||||
to: app-dist
|
to: app-dist
|
||||||
|
|||||||
@@ -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/**/*",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "./terminal.js";
|
} from "./terminal.js";
|
||||||
import {
|
import {
|
||||||
chmodSync,
|
chmodSync,
|
||||||
|
cpSync,
|
||||||
existsSync,
|
existsSync,
|
||||||
mkdtempSync,
|
mkdtempSync,
|
||||||
mkdirSync,
|
mkdirSync,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
statSync,
|
statSync,
|
||||||
writeFileSync,
|
writeFileSync,
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
|
|
||||||
@@ -208,7 +210,9 @@ describe("createTerminal", () => {
|
|||||||
|
|
||||||
expect(resolvedEnv.TERM).toBe("xterm-256color");
|
expect(resolvedEnv.TERM).toBe("xterm-256color");
|
||||||
expect(resolvedEnv.PASEO_ZSH_ZDOTDIR).toBe("/tmp/paseo-zdotdir");
|
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 () => {
|
it("uses custom name when provided", async () => {
|
||||||
@@ -588,6 +592,58 @@ describe("terminal title", () => {
|
|||||||
await waitForTitle(session, (title) => title === "~/dev/faro", 4000);
|
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 () => {
|
it.skipIf(!hasZsh)("emits zsh shell integration command completion", async () => {
|
||||||
const homeDir = mkdtempSync(join(tmpdir(), "terminal-zsh-command-finished-home-"));
|
const homeDir = mkdtempSync(join(tmpdir(), "terminal-zsh-command-finished-home-"));
|
||||||
temporaryDirs.push(homeDir);
|
temporaryDirs.push(homeDir);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import * as pty from "node-pty";
|
import * as pty from "node-pty";
|
||||||
import xterm, { type Terminal as TerminalType } from "@xterm/headless";
|
import xterm, { type Terminal as TerminalType } from "@xterm/headless";
|
||||||
import { randomUUID } from "crypto";
|
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 { basename, dirname, join } from "node:path";
|
||||||
import { createRequire } from "node:module";
|
import { createRequire } from "node:module";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
@@ -85,6 +86,7 @@ export interface CreateTerminalOptions {
|
|||||||
interface BuildTerminalEnvironmentInput {
|
interface BuildTerminalEnvironmentInput {
|
||||||
shell: string;
|
shell: string;
|
||||||
env: Record<string, string>;
|
env: Record<string, string>;
|
||||||
|
zshShellIntegrationDir?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CaptureTerminalLinesOptions {
|
export interface CaptureTerminalLinesOptions {
|
||||||
@@ -182,6 +184,33 @@ export function resolveZshShellIntegrationDir(): string {
|
|||||||
return fileURLToPath(new URL("./shell-integration/zsh", import.meta.url));
|
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(
|
export function buildTerminalEnvironment(
|
||||||
input: BuildTerminalEnvironmentInput,
|
input: BuildTerminalEnvironmentInput,
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
@@ -199,7 +228,7 @@ export function buildTerminalEnvironment(
|
|||||||
return {
|
return {
|
||||||
...baseEnv,
|
...baseEnv,
|
||||||
PASEO_ZSH_ZDOTDIR: originalZdotdir,
|
PASEO_ZSH_ZDOTDIR: originalZdotdir,
|
||||||
ZDOTDIR: resolveZshShellIntegrationDir(),
|
ZDOTDIR: prepareZshShellIntegrationRuntimeDir(input.zshShellIntegrationDir),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user