diff --git a/src/main/daemon/shell-ready.test.ts b/src/main/daemon/shell-ready.test.ts index 3e981fb95..7f957072c 100644 --- a/src/main/daemon/shell-ready.test.ts +++ b/src/main/daemon/shell-ready.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { tmpdir } from 'os' import { join } from 'path' -import { existsSync, mkdtempSync, rmSync } from 'fs' +import { existsSync, mkdtempSync, readFileSync, rmSync } from 'fs' import type * as ShellReadyModule from './shell-ready' async function importFreshShellReady(): Promise { @@ -90,6 +90,75 @@ describePosix('daemon shell-ready launch config', () => { } }) + it('uses inherited ORCA_ORIG_ZDOTDIR when ZDOTDIR is an Orca wrapper dir', async () => { + const previousZdotdir = process.env.ZDOTDIR + const previousOrigZdotdir = process.env.ORCA_ORIG_ZDOTDIR + const previousHome = process.env.HOME + process.env.ZDOTDIR = '/some/other/orca/shell-ready/zsh' + process.env.ORCA_ORIG_ZDOTDIR = '/Users/alice/.config/zsh' + process.env.HOME = '/Users/alice' + try { + const { getShellReadyLaunchConfig } = await importFreshShellReady() + const config = getShellReadyLaunchConfig('/bin/zsh') + expect(config.env.ORCA_ORIG_ZDOTDIR).toBe('/Users/alice/.config/zsh') + } finally { + if (previousZdotdir === undefined) { + delete process.env.ZDOTDIR + } else { + process.env.ZDOTDIR = previousZdotdir + } + if (previousOrigZdotdir === undefined) { + delete process.env.ORCA_ORIG_ZDOTDIR + } else { + process.env.ORCA_ORIG_ZDOTDIR = previousOrigZdotdir + } + if (previousHome === undefined) { + delete process.env.HOME + } else { + process.env.HOME = previousHome + } + } + }) + + it('falls back to HOME when inherited ORCA_ORIG_ZDOTDIR points at a wrapper dir', async () => { + const previousZdotdir = process.env.ZDOTDIR + const previousOrigZdotdir = process.env.ORCA_ORIG_ZDOTDIR + const previousHome = process.env.HOME + delete process.env.ZDOTDIR + process.env.ORCA_ORIG_ZDOTDIR = '/some/other/orca/shell-ready/zsh' + process.env.HOME = '/Users/alice' + try { + const { getShellReadyLaunchConfig } = await importFreshShellReady() + const config = getShellReadyLaunchConfig('/bin/zsh') + expect(config.env.ORCA_ORIG_ZDOTDIR).toBe('/Users/alice') + } finally { + if (previousZdotdir === undefined) { + delete process.env.ZDOTDIR + } else { + process.env.ZDOTDIR = previousZdotdir + } + if (previousOrigZdotdir === undefined) { + delete process.env.ORCA_ORIG_ZDOTDIR + } else { + process.env.ORCA_ORIG_ZDOTDIR = previousOrigZdotdir + } + if (previousHome === undefined) { + delete process.env.HOME + } else { + process.env.HOME = previousHome + } + } + }) + + it('writes zsh wrappers that guard against ORCA_ORIG_ZDOTDIR self-loops', async () => { + const { getShellReadyLaunchConfig } = await importFreshShellReady() + + getShellReadyLaunchConfig('/bin/zsh') + + const zshenv = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshenv'), 'utf8') + expect(zshenv).toContain('*/shell-ready/zsh) export ORCA_ORIG_ZDOTDIR="$HOME" ;;') + }) + it('preserves a real inherited ZDOTDIR as ORCA_ORIG_ZDOTDIR', async () => { // Why: users who run a custom zsh dotfiles directory legitimately set // ZDOTDIR before launching Orca. We only want to reject the self-loop diff --git a/src/main/daemon/shell-ready.ts b/src/main/daemon/shell-ready.ts index a78273cd1..f7a2e021f 100644 --- a/src/main/daemon/shell-ready.ts +++ b/src/main/daemon/shell-ready.ts @@ -30,21 +30,29 @@ function getShellReadyWrapperRoot(): string { // (regardless of whether it came from this daemon's userData, a packaged // Orca, or a different dev build). Treat it as if ZDOTDIR were unset so the // caller falls back to HOME for the user's real config root. -function resolveOriginalZdotdir(): string { - const inherited = process.env.ZDOTDIR - if (!inherited) { - return process.env.HOME || '' +function normalizeOriginalZdotdirCandidate(value: string | undefined): string | null { + if (!value) { + return null } // Why: tolerate trailing slashes — some shell startup scripts export // `ZDOTDIR="$dir/"`, and without normalization the suffix check would // miss the self-loop path and restore the recursion bug. Also collapses // a pathological `ZDOTDIR=/` to empty so we fall back to HOME rather than // sourcing `/.zshenv` (which is never the user's real config). - const normalized = inherited.replace(/\/+$/, '') + const normalized = value.replace(/\/+$/, '') if (!normalized || normalized.endsWith('/shell-ready/zsh')) { - return process.env.HOME || '' + return null } - return inherited + return value +} + +function resolveOriginalZdotdir(): string { + return ( + normalizeOriginalZdotdirCandidate(process.env.ZDOTDIR) || + normalizeOriginalZdotdirCandidate(process.env.ORCA_ORIG_ZDOTDIR) || + process.env.HOME || + '' + ) } function getRequiredShellReadyWrapperPaths(root = getShellReadyWrapperRoot()): string[] { @@ -76,15 +84,24 @@ function ensureShellReadyWrappers(): void { const zshEnv = `# Orca daemon zsh shell-ready wrapper export ORCA_ORIG_ZDOTDIR="\${ORCA_ORIG_ZDOTDIR:-$HOME}" +case "\${ORCA_ORIG_ZDOTDIR%/}" in + */shell-ready/zsh) export ORCA_ORIG_ZDOTDIR="$HOME" ;; +esac [[ -f "$ORCA_ORIG_ZDOTDIR/.zshenv" ]] && source "$ORCA_ORIG_ZDOTDIR/.zshenv" export ZDOTDIR=${quotePosixSingle(zshDir)} ` const zshProfile = `# Orca daemon zsh shell-ready wrapper _orca_home="\${ORCA_ORIG_ZDOTDIR:-$HOME}" +case "\${_orca_home%/}" in + */shell-ready/zsh) _orca_home="$HOME" ;; +esac [[ -f "$_orca_home/.zprofile" ]] && source "$_orca_home/.zprofile" ` const zshRc = `# Orca daemon zsh shell-ready wrapper _orca_home="\${ORCA_ORIG_ZDOTDIR:-$HOME}" +case "\${_orca_home%/}" in + */shell-ready/zsh) _orca_home="$HOME" ;; +esac if [[ -o interactive && -f "$_orca_home/.zshrc" ]]; then source "$_orca_home/.zshrc" fi @@ -99,6 +116,9 @@ __orca_restore_attribution_path() { ` const zshLogin = `# Orca daemon zsh shell-ready wrapper _orca_home="\${ORCA_ORIG_ZDOTDIR:-$HOME}" +case "\${_orca_home%/}" in + */shell-ready/zsh) _orca_home="$HOME" ;; +esac if [[ -o interactive && -f "$_orca_home/.zlogin" ]]; then source "$_orca_home/.zlogin" fi diff --git a/src/main/providers/local-pty-shell-ready.test.ts b/src/main/providers/local-pty-shell-ready.test.ts index f4d54c979..f80848027 100644 --- a/src/main/providers/local-pty-shell-ready.test.ts +++ b/src/main/providers/local-pty-shell-ready.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { tmpdir } from 'os' import { join } from 'path' -import { mkdtempSync, rmSync } from 'fs' +import { mkdtempSync, readFileSync, rmSync } from 'fs' import type * as pty from 'node-pty' import type * as LocalPtyShellReadyModule from './local-pty-shell-ready' import { writeStartupCommandWhenShellReady } from './local-pty-shell-ready' @@ -165,6 +165,75 @@ describePosix('local PTY shell-ready launch config', () => { } }) + it('uses inherited ORCA_ORIG_ZDOTDIR when ZDOTDIR is an Orca wrapper dir', async () => { + const previousZdotdir = process.env.ZDOTDIR + const previousOrigZdotdir = process.env.ORCA_ORIG_ZDOTDIR + const previousHome = process.env.HOME + process.env.ZDOTDIR = '/some/other/orca/shell-ready/zsh' + process.env.ORCA_ORIG_ZDOTDIR = '/Users/alice/.config/zsh' + process.env.HOME = '/Users/alice' + try { + const { getShellReadyLaunchConfig } = await importFreshLocalPtyShellReady() + const config = getShellReadyLaunchConfig('/bin/zsh') + expect(config.env.ORCA_ORIG_ZDOTDIR).toBe('/Users/alice/.config/zsh') + } finally { + if (previousZdotdir === undefined) { + delete process.env.ZDOTDIR + } else { + process.env.ZDOTDIR = previousZdotdir + } + if (previousOrigZdotdir === undefined) { + delete process.env.ORCA_ORIG_ZDOTDIR + } else { + process.env.ORCA_ORIG_ZDOTDIR = previousOrigZdotdir + } + if (previousHome === undefined) { + delete process.env.HOME + } else { + process.env.HOME = previousHome + } + } + }) + + it('falls back to HOME when inherited ORCA_ORIG_ZDOTDIR points at a wrapper dir', async () => { + const previousZdotdir = process.env.ZDOTDIR + const previousOrigZdotdir = process.env.ORCA_ORIG_ZDOTDIR + const previousHome = process.env.HOME + delete process.env.ZDOTDIR + process.env.ORCA_ORIG_ZDOTDIR = '/some/other/orca/shell-ready/zsh' + process.env.HOME = '/Users/alice' + try { + const { getShellReadyLaunchConfig } = await importFreshLocalPtyShellReady() + const config = getShellReadyLaunchConfig('/bin/zsh') + expect(config.env.ORCA_ORIG_ZDOTDIR).toBe('/Users/alice') + } finally { + if (previousZdotdir === undefined) { + delete process.env.ZDOTDIR + } else { + process.env.ZDOTDIR = previousZdotdir + } + if (previousOrigZdotdir === undefined) { + delete process.env.ORCA_ORIG_ZDOTDIR + } else { + process.env.ORCA_ORIG_ZDOTDIR = previousOrigZdotdir + } + if (previousHome === undefined) { + delete process.env.HOME + } else { + process.env.HOME = previousHome + } + } + }) + + it('writes zsh wrappers that guard against ORCA_ORIG_ZDOTDIR self-loops', async () => { + const { getShellReadyLaunchConfig } = await importFreshLocalPtyShellReady() + + getShellReadyLaunchConfig('/bin/zsh') + + const zshenv = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshenv'), 'utf8') + expect(zshenv).toContain('*/shell-ready/zsh) export ORCA_ORIG_ZDOTDIR="$HOME" ;;') + }) + it('preserves a real inherited ZDOTDIR as ORCA_ORIG_ZDOTDIR', async () => { const previousZdotdir = process.env.ZDOTDIR process.env.ZDOTDIR = '/Users/alice/.config/zsh' diff --git a/src/main/providers/local-pty-shell-ready.ts b/src/main/providers/local-pty-shell-ready.ts index cf2c9148e..7ad3c09b9 100644 --- a/src/main/providers/local-pty-shell-ready.ts +++ b/src/main/providers/local-pty-shell-ready.ts @@ -86,21 +86,29 @@ function getShellReadyWrapperRoot(): string { // (regardless of whether it came from this app's userData, a packaged Orca, // or a different dev build). Treat it as if ZDOTDIR were unset so the caller // falls back to HOME for the user's real config root. -function resolveOriginalZdotdir(): string { - const inherited = process.env.ZDOTDIR - if (!inherited) { - return process.env.HOME || '' +function normalizeOriginalZdotdirCandidate(value: string | undefined): string | null { + if (!value) { + return null } // Why: tolerate trailing slashes — some shell startup scripts export // `ZDOTDIR="$dir/"`, and without normalization the suffix check would // miss the self-loop path and restore the recursion bug. Also collapses // a pathological `ZDOTDIR=/` to empty so we fall back to HOME rather than // sourcing `/.zshenv` (which is never the user's real config). - const normalized = inherited.replace(/\/+$/, '') + const normalized = value.replace(/\/+$/, '') if (!normalized || normalized.endsWith('/shell-ready/zsh')) { - return process.env.HOME || '' + return null } - return inherited + return value +} + +function resolveOriginalZdotdir(): string { + return ( + normalizeOriginalZdotdirCandidate(process.env.ZDOTDIR) || + normalizeOriginalZdotdirCandidate(process.env.ORCA_ORIG_ZDOTDIR) || + process.env.HOME || + '' + ) } export function getBashShellReadyRcfileContent(): string { @@ -156,15 +164,24 @@ function ensureShellReadyWrappers(): void { const zshEnv = `# Orca zsh shell-ready wrapper export ORCA_ORIG_ZDOTDIR="\${ORCA_ORIG_ZDOTDIR:-$HOME}" +case "\${ORCA_ORIG_ZDOTDIR%/}" in + */shell-ready/zsh) export ORCA_ORIG_ZDOTDIR="$HOME" ;; +esac [[ -f "$ORCA_ORIG_ZDOTDIR/.zshenv" ]] && source "$ORCA_ORIG_ZDOTDIR/.zshenv" export ZDOTDIR=${quotePosixSingle(zshDir)} ` const zshProfile = `# Orca zsh shell-ready wrapper _orca_home="\${ORCA_ORIG_ZDOTDIR:-$HOME}" +case "\${_orca_home%/}" in + */shell-ready/zsh) _orca_home="$HOME" ;; +esac [[ -f "$_orca_home/.zprofile" ]] && source "$_orca_home/.zprofile" ` const zshRc = `# Orca zsh shell-ready wrapper _orca_home="\${ORCA_ORIG_ZDOTDIR:-$HOME}" +case "\${_orca_home%/}" in + */shell-ready/zsh) _orca_home="$HOME" ;; +esac if [[ -o interactive && -f "$_orca_home/.zshrc" ]]; then source "$_orca_home/.zshrc" fi @@ -179,6 +196,9 @@ __orca_restore_attribution_path() { ` const zshLogin = `# Orca zsh shell-ready wrapper _orca_home="\${ORCA_ORIG_ZDOTDIR:-$HOME}" +case "\${_orca_home%/}" in + */shell-ready/zsh) _orca_home="$HOME" ;; +esac if [[ -o interactive && -f "$_orca_home/.zlogin" ]]; then source "$_orca_home/.zlogin" fi