mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 16:03:47 +00:00
fix(shell): stop the zsh wrapper colliding on REPLY and degrade under sh emulation
Widening wrapping from overlay/startup panes to every zsh pane turned three latent wrapper defects into user-visible ones. - The config-dir resolver used `REPLY`, zsh's shared scratch global, as its out-parameter. `typeset -r REPLY` in a user config made the wrapper's first executable assignment fatal, `typeset -i REPLY` silently resolved every path to 0; both left HISTFILE inside Orca's wrapper dir. It now writes an Orca-private `_orca_resolved_config_dir`, declared `typeset -g` so the rename introduces no `warn_create_global` noise. A new rule test fails on any generated wrapper file that writes a global outside Orca's namespace. - The daemon dropped an inherited HISTFILE but never an inherited ORCA_HISTFILE, which now both wraps a pane the client scoped nothing for and re-exports another worktree's history path. The relay had the same gap on its isolation-off and revive paths. Both now mirror the desktop. - A user .zshenv or .zprofile ending in `emulate sh` makes zsh ignore ZDOTDIR, so no later wrapper file is read and the epilogue never runs. Nothing can repair HISTFILE from there, so the wrapper now detects the emulation and hands the pane back unwrapped instead of leaving history somewhere invisible. Also `typeset -g __orca_in_command` so the OSC 133 preexec hook prints no warning under `setopt warn_create_global`.
This commit is contained in:
@@ -250,6 +250,46 @@ describe('createPtySubprocess', () => {
|
||||
expect(spawnMock.mock.calls.at(-1)?.[2].env.HISTFILE).toBe(expected)
|
||||
})
|
||||
|
||||
it.each([
|
||||
// ORCA_HISTFILE is exported into every pane, so a daemon started from an
|
||||
// Orca pane inherits one. Left in place it BOTH re-scopes the pane to
|
||||
// another worktree's history file (#11146) and wraps a zsh pane nothing
|
||||
// asked to wrap, since `history` is selected on its presence.
|
||||
['drops an inherited Orca ORCA_HISTFILE', undefined, undefined],
|
||||
[
|
||||
'keeps the path this spawn injected',
|
||||
'/fake/userData/terminal-history/00112233445566aa/zsh_history',
|
||||
'/fake/userData/terminal-history/00112233445566aa/zsh_history'
|
||||
],
|
||||
['keeps a caller-supplied value', '/home/me/.zsh_history', '/home/me/.zsh_history']
|
||||
])('%s', async (_name, requested, expected) => {
|
||||
spawnMock.mockReturnValue(mockPtyProcess())
|
||||
const saved = process.env.ORCA_HISTFILE
|
||||
process.env.ORCA_HISTFILE = '/fake/userData/terminal-history/aabbccddeeff0011/zsh_history'
|
||||
|
||||
try {
|
||||
await createPtySubprocess({
|
||||
sessionId: 'test',
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
shellOverride: '/bin/zsh',
|
||||
...(requested === undefined ? {} : { env: { ORCA_HISTFILE: requested } })
|
||||
})
|
||||
} finally {
|
||||
if (saved === undefined) {
|
||||
delete process.env.ORCA_HISTFILE
|
||||
} else {
|
||||
process.env.ORCA_HISTFILE = saved
|
||||
}
|
||||
}
|
||||
|
||||
const env = spawnMock.mock.calls.at(-1)?.[2].env
|
||||
expect(env.ORCA_HISTFILE).toBe(expected)
|
||||
// The wrapping consequence: no inherited value may point a pane at Orca's
|
||||
// ZDOTDIR that the client scoped no history for.
|
||||
expect(env.ORCA_SHELL_FEATURES).toBe(expected === undefined ? undefined : 'history')
|
||||
})
|
||||
|
||||
it('does not inherit ELECTRON_RUN_AS_NODE from the daemon process env', async () => {
|
||||
// Why: the daemon is forked with ELECTRON_RUN_AS_NODE=1. If that flag
|
||||
// reaches user shells, nested Electron commands run as plain Node.
|
||||
|
||||
@@ -665,6 +665,14 @@ export async function createPtySubprocess(opts: PtySubprocessOptions): Promise<S
|
||||
if (opts.env?.HISTFILE === undefined) {
|
||||
dropInheritedOrcaHistFile(env)
|
||||
}
|
||||
// Why ORCA_HISTFILE too: the desktop drops an inherited one on both of its
|
||||
// branches, and the daemon inherits one from the pane that launched it just
|
||||
// as readily. Left in place it now both WRAPS a pane the client scoped
|
||||
// nothing for (shell-startup-features selects `history` on its presence) and
|
||||
// makes the wrapper re-export another worktree's history path (#11146).
|
||||
if (opts.env?.ORCA_HISTFILE === undefined) {
|
||||
delete env.ORCA_HISTFILE
|
||||
}
|
||||
removeInheritedDevAgentHookEndpoint(env, opts.env)
|
||||
removeInheritedElectronRunAsNode(env)
|
||||
removeAppImageRuntimeEnv(env)
|
||||
|
||||
@@ -186,7 +186,7 @@ function expectZdotdirSourceContext(content: string, fileName: '.zprofile' | '.z
|
||||
|
||||
function expectFinalZdotdirRestoreContext(content: string) {
|
||||
expect(content).toContain("after Orca's last wrapper file has loaded")
|
||||
expect(content).toContain('export ZDOTDIR="$REPLY"')
|
||||
expect(content).toContain('export ZDOTDIR="$_orca_resolved_config_dir"')
|
||||
}
|
||||
|
||||
describePosix('daemon shell-ready launch config', () => {
|
||||
@@ -498,8 +498,8 @@ describePosix('daemon shell-ready launch config', () => {
|
||||
const zlogin = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zlogin'), 'utf8')
|
||||
expect(zshenv).toContain('__orca_resolve_inherited_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"')
|
||||
expect(zshenv).toContain('printf "\\033]777;orca-shell-start:%s\\007" "$$"')
|
||||
expect(zshenv).toContain('"$REPLY" == */shell-ready/zsh ]]; then')
|
||||
expect(zshenv).toContain('export ORCA_ORIG_ZDOTDIR="$REPLY"')
|
||||
expect(zshenv).toContain('"$_orca_resolved_config_dir" == */shell-ready/zsh ]]; then')
|
||||
expect(zshenv).toContain('export ORCA_ORIG_ZDOTDIR="$_orca_resolved_config_dir"')
|
||||
expectZdotdirSourceContext(zprofile, '.zprofile')
|
||||
expectZdotdirSourceContext(zshrc, '.zshrc')
|
||||
expectZdotdirSourceContext(zlogin, '.zlogin')
|
||||
@@ -931,7 +931,7 @@ describePosix('daemon shell-ready launch config', () => {
|
||||
const zshenv = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshenv'), 'utf8')
|
||||
|
||||
// Save spawn-env value before sourcing user .zshenv
|
||||
expect(zshenv).toContain('_orca_user_zdotdir="$REPLY"')
|
||||
expect(zshenv).toContain('_orca_user_zdotdir="$_orca_resolved_config_dir"')
|
||||
|
||||
// Fallback chain: discovered → normalized spawn-env path → HOME
|
||||
expect(zshenv).toContain('${_orca_discovered_zdotdir:-${_orca_user_zdotdir:-$HOME}}')
|
||||
|
||||
@@ -96,7 +96,7 @@ function expectZdotdirSourceContext(content: string, fileName: '.zprofile' | '.z
|
||||
|
||||
function expectFinalZdotdirRestoreContext(content: string) {
|
||||
expect(content).toContain("after Orca's last wrapper file has loaded")
|
||||
expect(content).toContain('export ZDOTDIR="$REPLY"')
|
||||
expect(content).toContain('export ZDOTDIR="$_orca_resolved_config_dir"')
|
||||
}
|
||||
|
||||
describePosix('local PTY shell-ready launch config', () => {
|
||||
@@ -246,8 +246,8 @@ describePosix('local PTY shell-ready launch config', () => {
|
||||
const zlogin = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zlogin'), 'utf8')
|
||||
expect(zshenv).toContain('__orca_resolve_inherited_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"')
|
||||
expect(zshenv).toContain('printf "\\033]777;orca-shell-start:%s\\007" "$$"')
|
||||
expect(zshenv).toContain('"$REPLY" == */shell-ready/zsh ]]; then')
|
||||
expect(zshenv).toContain('export ORCA_ORIG_ZDOTDIR="$REPLY"')
|
||||
expect(zshenv).toContain('"$_orca_resolved_config_dir" == */shell-ready/zsh ]]; then')
|
||||
expect(zshenv).toContain('export ORCA_ORIG_ZDOTDIR="$_orca_resolved_config_dir"')
|
||||
expectZdotdirSourceContext(zprofile, '.zprofile')
|
||||
expectZdotdirSourceContext(zshrc, '.zshrc')
|
||||
expectZdotdirSourceContext(zlogin, '.zlogin')
|
||||
@@ -538,7 +538,7 @@ describePosix('local PTY shell-ready launch config', () => {
|
||||
const zshenv = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshenv'), 'utf8')
|
||||
|
||||
// Save spawn-env value before sourcing user .zshenv
|
||||
expect(zshenv.indexOf('_orca_user_zdotdir="$REPLY"')).toBeLessThan(
|
||||
expect(zshenv.indexOf('_orca_user_zdotdir="$_orca_resolved_config_dir"')).toBeLessThan(
|
||||
zshenv.indexOf('source "${_orca_zshenv_path}"')
|
||||
)
|
||||
|
||||
|
||||
+85
-17
@@ -50,13 +50,24 @@ export const SHELL_STARTUP_IDENTITY_MARKER_BLOCK = `__orca_has_feature identity
|
||||
* wrapper dir can be rewritten by two concurrently installed builds, so a shell
|
||||
* can read one build's .zshenv and another's .zshrc. A .zshrc that called a
|
||||
* function only the newer .zshenv defines printed `command not found` AND left
|
||||
* $REPLY empty, which skipped sourcing the user's own startup file entirely.
|
||||
* the out-parameter empty, which skipped sourcing the user's own startup file.
|
||||
*
|
||||
* Why an Orca-private out-parameter and not `REPLY`: `REPLY` is zsh's shared
|
||||
* scratch global, so a user config is entitled to constrain it. `typeset -r
|
||||
* REPLY` made the very first assignment a fatal error and aborted the wrapper
|
||||
* file; `typeset -i REPLY` silently turned every resolved path into `0`. Both
|
||||
* left HISTFILE inside Orca's wrapper dir. Harmless while Orca wrapped a few
|
||||
* percent of panes; not harmless now that it wraps every zsh pane.
|
||||
*
|
||||
* Why `typeset -g`: .zshrc/.zlogin call this AFTER the user's own config, so
|
||||
* creating a plain global inside a function would print a warning per call
|
||||
* under `setopt warn_create_global`.
|
||||
*/
|
||||
export const ZSH_USER_CONFIG_DIR_RESOLVER_BLOCK = `__orca_resolve_user_config_dir() {
|
||||
REPLY="\${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="\${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/${ZSH_WRAPPER_DIR_MARKER_FILE}" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
typeset -g _orca_resolved_config_dir="\${1:-}"
|
||||
while [[ "$_orca_resolved_config_dir" == */ ]]; do _orca_resolved_config_dir="\${_orca_resolved_config_dir%/}"; done
|
||||
if [[ -z "$_orca_resolved_config_dir" || -f "$_orca_resolved_config_dir/${ZSH_WRAPPER_DIR_MARKER_FILE}" || "$_orca_resolved_config_dir" == */shell-ready/zsh ]]; then
|
||||
_orca_resolved_config_dir="$HOME"
|
||||
fi
|
||||
}`
|
||||
|
||||
@@ -71,17 +82,61 @@ export const ZSH_INHERITED_CONFIG_DIR_RESOLVER_BLOCK = `# Why stricter for an in
|
||||
# startup file at all is not the user's config root whoever wrote it.
|
||||
__orca_resolve_inherited_config_dir() {
|
||||
__orca_resolve_user_config_dir "\${1:-}"
|
||||
[[ "$REPLY" == "$HOME" ]] && return 0
|
||||
[[ "$_orca_resolved_config_dir" == "$HOME" ]] && return 0
|
||||
local _orca_startup_file
|
||||
for _orca_startup_file in .zshenv .zshrc .zprofile .zlogin; do
|
||||
[[ -r "$REPLY/$_orca_startup_file" ]] && return 0
|
||||
[[ -r "$_orca_resolved_config_dir/$_orca_startup_file" ]] && return 0
|
||||
done
|
||||
REPLY="$HOME"
|
||||
_orca_resolved_config_dir="$HOME"
|
||||
}`
|
||||
|
||||
// Why: daemon, local, and relay wrappers must preserve one Bash prompt-hook contract.
|
||||
export { BASH_PROMPT_COMMAND_COMPOSITION_BLOCK } from './bash-prompt-command-composition'
|
||||
|
||||
/**
|
||||
* Hands the pane back to the user unwrapped when zsh has entered sh/ksh
|
||||
* emulation, and stops reading the rest of the current wrapper file.
|
||||
*
|
||||
* Why: zsh's `sourcehome()` ignores ZDOTDIR entirely once the shell is in sh or
|
||||
* ksh emulation, so a user .zshenv (or .zprofile) ending in `emulate sh` means
|
||||
* NO later wrapper file is ever read — the epilogue runs zero times, while the
|
||||
* user's own $HOME startup files load normally. The pane looks fine and writes
|
||||
* its history inside Orca's wrapper dir, where the user will never find it.
|
||||
*
|
||||
* Nothing can repair that from here (`/etc/zshrc` assigns HISTFILE after
|
||||
* .zshenv and .zprofile both), so the wrapper does the next best thing: it
|
||||
* restores the user's own ZDOTDIR and consumes Orca's variables, leaving
|
||||
* exactly the shell an unwrapped pane would have produced. Degrading to the
|
||||
* pre-wrapping behaviour is the bar; degrading to something worse is not.
|
||||
*
|
||||
* Why a positive `sh|ksh` match rather than `!= zsh`: a zsh too old for the
|
||||
* query form of `emulate` prints nothing, and must not unwrap every pane.
|
||||
*
|
||||
* Why `sourcedUserFileTest` gates the probe rather than it running
|
||||
* unconditionally: `$(emulate)` forks, and every zsh pane now pays for it. The
|
||||
* gate loses no coverage — this wrapper file is itself read through ZDOTDIR, so
|
||||
* anything that had already entered emulation (a system /etc/zshenv or
|
||||
* /etc/zprofile) would have hidden this very file too. The only thing that can
|
||||
* have entered emulation by this line is the user file this file just sourced.
|
||||
*/
|
||||
export function getZshEmulationDegradeBlock(options: {
|
||||
userZdotdirExpression: string
|
||||
sourcedUserFileTest: string
|
||||
}): string {
|
||||
return `if [[ ${options.sourcedUserFileTest} ]]; then
|
||||
case "$(emulate 2>/dev/null)" in
|
||||
sh|ksh)
|
||||
export ZDOTDIR=${options.userZdotdirExpression}
|
||||
# Why unset: an ORCA_HISTFILE no wrapper file will ever consume is
|
||||
# inherited by everything this pane spawns, including a nested Orca.
|
||||
builtin unset ORCA_HISTFILE _orca_shell_features _orca_home _orca_resolved_config_dir _orca_wrapper_zdotdir_self
|
||||
unfunction __orca_shell_epilogue __orca_has_feature __orca_resolve_user_config_dir __orca_resolve_inherited_config_dir 2>/dev/null
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi`
|
||||
}
|
||||
|
||||
/** The ZDOTDIR-discovery body of the wrapper .zshenv (no header, no epilogue). */
|
||||
export function getZshEnvDiscoveryBody(zshDir: string): string {
|
||||
return `# Why: capture the runtime wrapper dir before it is unset below. On WSL this
|
||||
@@ -106,9 +161,9 @@ _orca_zshenv_path=""
|
||||
# Normalize fallback and source roots before reading user .zshenv so nested
|
||||
# Orca PTYs never source another Orca wrapper recursively.
|
||||
__orca_resolve_inherited_config_dir "\${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
_orca_user_zdotdir="$REPLY"
|
||||
_orca_user_zdotdir="$_orca_resolved_config_dir"
|
||||
__orca_resolve_inherited_config_dir "\${ORCA_ZSHENV_SOURCE_DIR:-$HOME}"
|
||||
_orca_zshenv_source_dir="$REPLY"
|
||||
_orca_zshenv_source_dir="$_orca_resolved_config_dir"
|
||||
unset ORCA_ZSHENV_SOURCE_DIR
|
||||
|
||||
# Why: source at wrapper top level, not in a function/subshell, so .zshenv
|
||||
@@ -140,7 +195,14 @@ fi
|
||||
# Why only the ownership check here: a ZDOTDIR the user's own .zshenv just
|
||||
# exported is the user's by construction, whatever it happens to contain.
|
||||
__orca_resolve_user_config_dir "\${_orca_discovered_zdotdir:-\${_orca_user_zdotdir:-$HOME}}"
|
||||
export ORCA_ORIG_ZDOTDIR="$REPLY"
|
||||
export ORCA_ORIG_ZDOTDIR="$_orca_resolved_config_dir"
|
||||
unset _orca_user_zdotdir _orca_zshenv_source_dir _orca_discovered_zdotdir
|
||||
|
||||
${getZshEmulationDegradeBlock({
|
||||
userZdotdirExpression: '"$ORCA_ORIG_ZDOTDIR"',
|
||||
sourcedUserFileTest: '-n "${_orca_zshenv_path:-}"'
|
||||
})}
|
||||
unset _orca_zshenv_path
|
||||
|
||||
# Why: use :- after user .zshenv — a pathological unset under set -u must not
|
||||
# abort the wrapper; empty falls through to the baked-literal branch.
|
||||
@@ -149,7 +211,7 @@ if [[ -n "\${_orca_wrapper_zdotdir_self:-}" && -f "\${_orca_wrapper_zdotdir_self
|
||||
else
|
||||
export ZDOTDIR=${quotePosixSingle(zshDir)}
|
||||
fi
|
||||
unset _orca_user_zdotdir _orca_zshenv_source_dir _orca_zshenv_path _orca_discovered_zdotdir _orca_wrapper_zdotdir_self
|
||||
unset _orca_wrapper_zdotdir_self
|
||||
`
|
||||
}
|
||||
|
||||
@@ -163,10 +225,16 @@ unset _orca_user_zdotdir _orca_zshenv_source_dir _orca_zshenv_path _orca_discove
|
||||
*/
|
||||
export function getZshOverlayEnvBody(zshDir: string): string {
|
||||
return `__orca_resolve_inherited_config_dir "\${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
export ORCA_ORIG_ZDOTDIR="$REPLY"
|
||||
export ORCA_ORIG_ZDOTDIR="$_orca_resolved_config_dir"
|
||||
[[ -f "$ORCA_ORIG_ZDOTDIR/.zshenv" ]] && source "$ORCA_ORIG_ZDOTDIR/.zshenv"
|
||||
__orca_resolve_user_config_dir "\${ZDOTDIR:-\${ORCA_ORIG_ZDOTDIR:-$HOME}}"
|
||||
export ORCA_USER_ZDOTDIR="$REPLY"
|
||||
export ORCA_USER_ZDOTDIR="$_orca_resolved_config_dir"
|
||||
|
||||
${getZshEmulationDegradeBlock({
|
||||
userZdotdirExpression: '"$ORCA_USER_ZDOTDIR"',
|
||||
sourcedUserFileTest: '-f "$ORCA_ORIG_ZDOTDIR/.zshenv"'
|
||||
})}
|
||||
|
||||
export ZDOTDIR=${quotePosixSingle(zshDir)}
|
||||
`
|
||||
}
|
||||
@@ -210,7 +278,7 @@ export function getZshStartupFileSourceBlock(options: {
|
||||
].filter(Boolean)
|
||||
|
||||
return `__orca_resolve_user_config_dir ${homeExpression}
|
||||
_orca_home="$REPLY"
|
||||
_orca_home="$_orca_resolved_config_dir"
|
||||
if [[ ${checks.join(' && ')} ]]; then
|
||||
_orca_wrapper_zdotdir="$ZDOTDIR"
|
||||
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
|
||||
@@ -277,7 +345,7 @@ export function getZshFinalZdotdirRestoreBlock(homeExpression = '"${ORCA_ORIG_ZD
|
||||
return `__orca_resolve_user_config_dir ${homeExpression}
|
||||
# Why: after Orca's last wrapper file has loaded, the interactive shell should
|
||||
# expose the same ZDOTDIR a normal zsh startup would expose.
|
||||
export ZDOTDIR="$REPLY"
|
||||
unset _orca_home REPLY
|
||||
export ZDOTDIR="$_orca_resolved_config_dir"
|
||||
unset _orca_home _orca_resolved_config_dir
|
||||
`
|
||||
}
|
||||
|
||||
@@ -52,6 +52,49 @@ async function expectWrapperFiles(transport: string, root: string): Promise<void
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Every shell name the wrapper is allowed to write that is not Orca-namespaced.
|
||||
*
|
||||
* Each is a deliberate contract with the shell or with Orca's own features, not
|
||||
* scratch space: the history path, the config dir, the two PATH-shaped exports
|
||||
* agent overlays need, and the prompt-hook arrays the readiness and OSC 133
|
||||
* markers register through.
|
||||
*/
|
||||
const CONTRACT_GLOBALS = new Set([
|
||||
'CODEX_HOME',
|
||||
'HISTFILE',
|
||||
'MIMOCODE_HOME',
|
||||
'OPENCODE_CONFIG_DIR',
|
||||
'PATH',
|
||||
'PROMPT_COMMAND',
|
||||
'ZDOTDIR',
|
||||
'precmd_functions',
|
||||
'preexec_functions'
|
||||
])
|
||||
|
||||
// `local`/`local -a` declarations are function-scoped and cannot collide.
|
||||
const LINE_START_ASSIGNMENT =
|
||||
/^[ \t]*(?:builtin[ \t]+)?(?:export[ \t]+|typeset[ \t]+-[a-zA-Z]+[ \t]+|declare[ \t]+-[a-zA-Z]+[ \t]+)?([A-Za-z_][A-Za-z0-9_]*)\+?=/
|
||||
const INLINE_EXPORT = /\bexport[ \t]+([A-Za-z_][A-Za-z0-9_]*)\+?=/g
|
||||
|
||||
function foreignGlobalsWritten(content: string): string[] {
|
||||
const names = new Set<string>()
|
||||
for (const line of content.split('\n')) {
|
||||
if (/^[ \t]*#/.test(line) || /^[ \t]*local\b/.test(line)) {
|
||||
continue
|
||||
}
|
||||
for (const name of [
|
||||
LINE_START_ASSIGNMENT.exec(line)?.[1],
|
||||
...[...line.matchAll(INLINE_EXPORT)].map((match) => match[1])
|
||||
]) {
|
||||
if (name && !/^_{0,2}orca_/i.test(name) && !CONTRACT_GLOBALS.has(name)) {
|
||||
names.add(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...names].sort()
|
||||
}
|
||||
|
||||
// Why: all three generators are POSIX-only (the launch configs skip wrapping on
|
||||
// win32), and native Windows path separators would make the fixtures unstable.
|
||||
const describePosix = process.platform === 'win32' ? describe.skip : describe
|
||||
@@ -90,6 +133,31 @@ describePosix('generated shell wrapper files', () => {
|
||||
await expectWrapperFiles('relay', root)
|
||||
})
|
||||
|
||||
// Why a rule and not another fixture: `REPLY`, zsh's shared scratch global,
|
||||
// was the wrapper's resolver out-parameter. A user config that constrained it
|
||||
// (`typeset -r REPLY`) aborted the wrapper at its first executable line — on
|
||||
// every zsh pane, once wrapping widened past overlay/startup panes. The
|
||||
// fixtures above would have shown that only to a reader who knew to look.
|
||||
it.each([
|
||||
['local', (): void => void ensureShellReadyWrappersAt(root), (): string => root],
|
||||
[
|
||||
'daemon',
|
||||
(): void => {
|
||||
process.env.ORCA_USER_DATA_PATH = root
|
||||
getDaemonShellLaunchConfig('/bin/zsh', STARTUP_COMMAND_FEATURES)
|
||||
},
|
||||
(): string => join(root, 'shell-ready')
|
||||
],
|
||||
['relay', (): void => void ensureOverlayRestoreWrappers(root), (): string => root]
|
||||
])('%s wrappers write no shell global outside Orca’s namespace', (_transport, generate, dir) => {
|
||||
generate()
|
||||
|
||||
for (const [, relativePath] of WRAPPER_FILES) {
|
||||
const content = readFileSync(join(dir(), relativePath), 'utf8')
|
||||
expect({ [relativePath]: foreignGlobalsWritten(content) }).toEqual({ [relativePath]: [] })
|
||||
}
|
||||
})
|
||||
|
||||
it('fish shell-ready init commands', async () => {
|
||||
process.env.ORCA_USER_DATA_PATH = root
|
||||
const local = getLocalShellLaunchConfig('/usr/bin/fish', STARTUP_COMMAND_FEATURES)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Orca daemon zsh shell-ready wrapper
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
typeset -g _orca_resolved_config_dir="${1:-}"
|
||||
while [[ "$_orca_resolved_config_dir" == */ ]]; do _orca_resolved_config_dir="${_orca_resolved_config_dir%/}"; done
|
||||
if [[ -z "$_orca_resolved_config_dir" || -f "$_orca_resolved_config_dir/.orca-shell-wrapper" || "$_orca_resolved_config_dir" == */shell-ready/zsh ]]; then
|
||||
_orca_resolved_config_dir="$HOME"
|
||||
fi
|
||||
}
|
||||
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
_orca_home="$REPLY"
|
||||
_orca_home="$_orca_resolved_config_dir"
|
||||
if [[ -o interactive && -f "$_orca_home/.zlogin" ]]; then
|
||||
_orca_wrapper_zdotdir="$ZDOTDIR"
|
||||
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Orca daemon zsh shell-ready wrapper
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
typeset -g _orca_resolved_config_dir="${1:-}"
|
||||
while [[ "$_orca_resolved_config_dir" == */ ]]; do _orca_resolved_config_dir="${_orca_resolved_config_dir%/}"; done
|
||||
if [[ -z "$_orca_resolved_config_dir" || -f "$_orca_resolved_config_dir/.orca-shell-wrapper" || "$_orca_resolved_config_dir" == */shell-ready/zsh ]]; then
|
||||
_orca_resolved_config_dir="$HOME"
|
||||
fi
|
||||
}
|
||||
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
_orca_home="$REPLY"
|
||||
_orca_home="$_orca_resolved_config_dir"
|
||||
if [[ -f "$_orca_home/.zprofile" ]]; then
|
||||
_orca_wrapper_zdotdir="$ZDOTDIR"
|
||||
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
|
||||
@@ -18,3 +18,15 @@ if [[ -f "$_orca_home/.zprofile" ]]; then
|
||||
unset _orca_wrapper_zdotdir
|
||||
fi
|
||||
|
||||
if [[ -f "$_orca_home/.zprofile" ]]; then
|
||||
case "$(emulate 2>/dev/null)" in
|
||||
sh|ksh)
|
||||
export ZDOTDIR="$_orca_home"
|
||||
# Why unset: an ORCA_HISTFILE no wrapper file will ever consume is
|
||||
# inherited by everything this pane spawns, including a nested Orca.
|
||||
builtin unset ORCA_HISTFILE _orca_shell_features _orca_home _orca_resolved_config_dir _orca_wrapper_zdotdir_self
|
||||
unfunction __orca_shell_epilogue __orca_has_feature __orca_resolve_user_config_dir __orca_resolve_inherited_config_dir 2>/dev/null
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
@@ -5,10 +5,10 @@ builtin unset ORCA_SHELL_FEATURES
|
||||
__orca_has_feature() { (( ${_orca_shell_features[(Ie)$1]} )) }
|
||||
__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$"
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
typeset -g _orca_resolved_config_dir="${1:-}"
|
||||
while [[ "$_orca_resolved_config_dir" == */ ]]; do _orca_resolved_config_dir="${_orca_resolved_config_dir%/}"; done
|
||||
if [[ -z "$_orca_resolved_config_dir" || -f "$_orca_resolved_config_dir/.orca-shell-wrapper" || "$_orca_resolved_config_dir" == */shell-ready/zsh ]]; then
|
||||
_orca_resolved_config_dir="$HOME"
|
||||
fi
|
||||
}
|
||||
# Why stricter for an inherited value: Orca can be launched from a terminal that
|
||||
@@ -16,12 +16,12 @@ __orca_resolve_user_config_dir() {
|
||||
# startup file at all is not the user's config root whoever wrote it.
|
||||
__orca_resolve_inherited_config_dir() {
|
||||
__orca_resolve_user_config_dir "${1:-}"
|
||||
[[ "$REPLY" == "$HOME" ]] && return 0
|
||||
[[ "$_orca_resolved_config_dir" == "$HOME" ]] && return 0
|
||||
local _orca_startup_file
|
||||
for _orca_startup_file in .zshenv .zshrc .zprofile .zlogin; do
|
||||
[[ -r "$REPLY/$_orca_startup_file" ]] && return 0
|
||||
[[ -r "$_orca_resolved_config_dir/$_orca_startup_file" ]] && return 0
|
||||
done
|
||||
REPLY="$HOME"
|
||||
_orca_resolved_config_dir="$HOME"
|
||||
}
|
||||
# Why: capture the runtime wrapper dir before it is unset below. On WSL this
|
||||
# file is generated with a Windows path but sourced via /mnt/c, so the baked
|
||||
@@ -45,9 +45,9 @@ _orca_zshenv_path=""
|
||||
# Normalize fallback and source roots before reading user .zshenv so nested
|
||||
# Orca PTYs never source another Orca wrapper recursively.
|
||||
__orca_resolve_inherited_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
_orca_user_zdotdir="$REPLY"
|
||||
_orca_user_zdotdir="$_orca_resolved_config_dir"
|
||||
__orca_resolve_inherited_config_dir "${ORCA_ZSHENV_SOURCE_DIR:-$HOME}"
|
||||
_orca_zshenv_source_dir="$REPLY"
|
||||
_orca_zshenv_source_dir="$_orca_resolved_config_dir"
|
||||
unset ORCA_ZSHENV_SOURCE_DIR
|
||||
|
||||
# Why: source at wrapper top level, not in a function/subshell, so .zshenv
|
||||
@@ -79,7 +79,22 @@ fi
|
||||
# Why only the ownership check here: a ZDOTDIR the user's own .zshenv just
|
||||
# exported is the user's by construction, whatever it happens to contain.
|
||||
__orca_resolve_user_config_dir "${_orca_discovered_zdotdir:-${_orca_user_zdotdir:-$HOME}}"
|
||||
export ORCA_ORIG_ZDOTDIR="$REPLY"
|
||||
export ORCA_ORIG_ZDOTDIR="$_orca_resolved_config_dir"
|
||||
unset _orca_user_zdotdir _orca_zshenv_source_dir _orca_discovered_zdotdir
|
||||
|
||||
if [[ -n "${_orca_zshenv_path:-}" ]]; then
|
||||
case "$(emulate 2>/dev/null)" in
|
||||
sh|ksh)
|
||||
export ZDOTDIR="$ORCA_ORIG_ZDOTDIR"
|
||||
# Why unset: an ORCA_HISTFILE no wrapper file will ever consume is
|
||||
# inherited by everything this pane spawns, including a nested Orca.
|
||||
builtin unset ORCA_HISTFILE _orca_shell_features _orca_home _orca_resolved_config_dir _orca_wrapper_zdotdir_self
|
||||
unfunction __orca_shell_epilogue __orca_has_feature __orca_resolve_user_config_dir __orca_resolve_inherited_config_dir 2>/dev/null
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
unset _orca_zshenv_path
|
||||
|
||||
# Why: use :- after user .zshenv — a pathological unset under set -u must not
|
||||
# abort the wrapper; empty falls through to the baked-literal branch.
|
||||
@@ -88,7 +103,7 @@ if [[ -n "${_orca_wrapper_zdotdir_self:-}" && -f "${_orca_wrapper_zdotdir_self:-
|
||||
else
|
||||
export ZDOTDIR='<WRAPPER_ROOT>/zsh'
|
||||
fi
|
||||
unset _orca_user_zdotdir _orca_zshenv_source_dir _orca_zshenv_path _orca_discovered_zdotdir _orca_wrapper_zdotdir_self
|
||||
unset _orca_wrapper_zdotdir_self
|
||||
|
||||
__orca_shell_epilogue() {
|
||||
# Why first: this body runs after the user's own config, so it would otherwise
|
||||
@@ -173,7 +188,9 @@ __orca_shell_epilogue() {
|
||||
}
|
||||
__orca_osc133_preexec() {
|
||||
printf "\033]133;C\007"
|
||||
__orca_in_command=1
|
||||
# Why typeset -g: a plain assignment here creates a global inside a function,
|
||||
# which prints a warning above every command under warn_create_global.
|
||||
typeset -g __orca_in_command=1
|
||||
}
|
||||
# Why: prepend so Orca captures $? before user prompt hooks can overwrite it.
|
||||
precmd_functions=(__orca_osc133_precmd ${precmd_functions[@]})
|
||||
@@ -206,8 +223,8 @@ __orca_shell_epilogue() {
|
||||
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
# Why: after Orca's last wrapper file has loaded, the interactive shell should
|
||||
# expose the same ZDOTDIR a normal zsh startup would expose.
|
||||
export ZDOTDIR="$REPLY"
|
||||
unset _orca_home REPLY
|
||||
export ZDOTDIR="$_orca_resolved_config_dir"
|
||||
unset _orca_home _orca_resolved_config_dir
|
||||
unset _orca_shell_features
|
||||
unfunction __orca_shell_epilogue __orca_has_feature __orca_resolve_user_config_dir __orca_resolve_inherited_config_dir
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Orca daemon zsh shell-ready wrapper
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
typeset -g _orca_resolved_config_dir="${1:-}"
|
||||
while [[ "$_orca_resolved_config_dir" == */ ]]; do _orca_resolved_config_dir="${_orca_resolved_config_dir%/}"; done
|
||||
if [[ -z "$_orca_resolved_config_dir" || -f "$_orca_resolved_config_dir/.orca-shell-wrapper" || "$_orca_resolved_config_dir" == */shell-ready/zsh ]]; then
|
||||
_orca_resolved_config_dir="$HOME"
|
||||
fi
|
||||
}
|
||||
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
_orca_home="$REPLY"
|
||||
_orca_home="$_orca_resolved_config_dir"
|
||||
if [[ "$_orca_home" != "$ZDOTDIR" && -o interactive && -f "$_orca_home/.zshrc" ]]; then
|
||||
_orca_wrapper_zdotdir="$ZDOTDIR"
|
||||
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Orca zsh shell-ready wrapper
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
typeset -g _orca_resolved_config_dir="${1:-}"
|
||||
while [[ "$_orca_resolved_config_dir" == */ ]]; do _orca_resolved_config_dir="${_orca_resolved_config_dir%/}"; done
|
||||
if [[ -z "$_orca_resolved_config_dir" || -f "$_orca_resolved_config_dir/.orca-shell-wrapper" || "$_orca_resolved_config_dir" == */shell-ready/zsh ]]; then
|
||||
_orca_resolved_config_dir="$HOME"
|
||||
fi
|
||||
}
|
||||
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
_orca_home="$REPLY"
|
||||
_orca_home="$_orca_resolved_config_dir"
|
||||
if [[ -o interactive && -f "$_orca_home/.zlogin" ]]; then
|
||||
_orca_wrapper_zdotdir="$ZDOTDIR"
|
||||
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Orca zsh shell-ready wrapper
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
typeset -g _orca_resolved_config_dir="${1:-}"
|
||||
while [[ "$_orca_resolved_config_dir" == */ ]]; do _orca_resolved_config_dir="${_orca_resolved_config_dir%/}"; done
|
||||
if [[ -z "$_orca_resolved_config_dir" || -f "$_orca_resolved_config_dir/.orca-shell-wrapper" || "$_orca_resolved_config_dir" == */shell-ready/zsh ]]; then
|
||||
_orca_resolved_config_dir="$HOME"
|
||||
fi
|
||||
}
|
||||
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
_orca_home="$REPLY"
|
||||
_orca_home="$_orca_resolved_config_dir"
|
||||
if [[ -f "$_orca_home/.zprofile" ]]; then
|
||||
_orca_wrapper_zdotdir="$ZDOTDIR"
|
||||
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
|
||||
@@ -18,3 +18,15 @@ if [[ -f "$_orca_home/.zprofile" ]]; then
|
||||
unset _orca_wrapper_zdotdir
|
||||
fi
|
||||
|
||||
if [[ -f "$_orca_home/.zprofile" ]]; then
|
||||
case "$(emulate 2>/dev/null)" in
|
||||
sh|ksh)
|
||||
export ZDOTDIR="$_orca_home"
|
||||
# Why unset: an ORCA_HISTFILE no wrapper file will ever consume is
|
||||
# inherited by everything this pane spawns, including a nested Orca.
|
||||
builtin unset ORCA_HISTFILE _orca_shell_features _orca_home _orca_resolved_config_dir _orca_wrapper_zdotdir_self
|
||||
unfunction __orca_shell_epilogue __orca_has_feature __orca_resolve_user_config_dir __orca_resolve_inherited_config_dir 2>/dev/null
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
@@ -5,10 +5,10 @@ builtin unset ORCA_SHELL_FEATURES
|
||||
__orca_has_feature() { (( ${_orca_shell_features[(Ie)$1]} )) }
|
||||
__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$"
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
typeset -g _orca_resolved_config_dir="${1:-}"
|
||||
while [[ "$_orca_resolved_config_dir" == */ ]]; do _orca_resolved_config_dir="${_orca_resolved_config_dir%/}"; done
|
||||
if [[ -z "$_orca_resolved_config_dir" || -f "$_orca_resolved_config_dir/.orca-shell-wrapper" || "$_orca_resolved_config_dir" == */shell-ready/zsh ]]; then
|
||||
_orca_resolved_config_dir="$HOME"
|
||||
fi
|
||||
}
|
||||
# Why stricter for an inherited value: Orca can be launched from a terminal that
|
||||
@@ -16,12 +16,12 @@ __orca_resolve_user_config_dir() {
|
||||
# startup file at all is not the user's config root whoever wrote it.
|
||||
__orca_resolve_inherited_config_dir() {
|
||||
__orca_resolve_user_config_dir "${1:-}"
|
||||
[[ "$REPLY" == "$HOME" ]] && return 0
|
||||
[[ "$_orca_resolved_config_dir" == "$HOME" ]] && return 0
|
||||
local _orca_startup_file
|
||||
for _orca_startup_file in .zshenv .zshrc .zprofile .zlogin; do
|
||||
[[ -r "$REPLY/$_orca_startup_file" ]] && return 0
|
||||
[[ -r "$_orca_resolved_config_dir/$_orca_startup_file" ]] && return 0
|
||||
done
|
||||
REPLY="$HOME"
|
||||
_orca_resolved_config_dir="$HOME"
|
||||
}
|
||||
# Why: capture the runtime wrapper dir before it is unset below. On WSL this
|
||||
# file is generated with a Windows path but sourced via /mnt/c, so the baked
|
||||
@@ -45,9 +45,9 @@ _orca_zshenv_path=""
|
||||
# Normalize fallback and source roots before reading user .zshenv so nested
|
||||
# Orca PTYs never source another Orca wrapper recursively.
|
||||
__orca_resolve_inherited_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
_orca_user_zdotdir="$REPLY"
|
||||
_orca_user_zdotdir="$_orca_resolved_config_dir"
|
||||
__orca_resolve_inherited_config_dir "${ORCA_ZSHENV_SOURCE_DIR:-$HOME}"
|
||||
_orca_zshenv_source_dir="$REPLY"
|
||||
_orca_zshenv_source_dir="$_orca_resolved_config_dir"
|
||||
unset ORCA_ZSHENV_SOURCE_DIR
|
||||
|
||||
# Why: source at wrapper top level, not in a function/subshell, so .zshenv
|
||||
@@ -79,7 +79,22 @@ fi
|
||||
# Why only the ownership check here: a ZDOTDIR the user's own .zshenv just
|
||||
# exported is the user's by construction, whatever it happens to contain.
|
||||
__orca_resolve_user_config_dir "${_orca_discovered_zdotdir:-${_orca_user_zdotdir:-$HOME}}"
|
||||
export ORCA_ORIG_ZDOTDIR="$REPLY"
|
||||
export ORCA_ORIG_ZDOTDIR="$_orca_resolved_config_dir"
|
||||
unset _orca_user_zdotdir _orca_zshenv_source_dir _orca_discovered_zdotdir
|
||||
|
||||
if [[ -n "${_orca_zshenv_path:-}" ]]; then
|
||||
case "$(emulate 2>/dev/null)" in
|
||||
sh|ksh)
|
||||
export ZDOTDIR="$ORCA_ORIG_ZDOTDIR"
|
||||
# Why unset: an ORCA_HISTFILE no wrapper file will ever consume is
|
||||
# inherited by everything this pane spawns, including a nested Orca.
|
||||
builtin unset ORCA_HISTFILE _orca_shell_features _orca_home _orca_resolved_config_dir _orca_wrapper_zdotdir_self
|
||||
unfunction __orca_shell_epilogue __orca_has_feature __orca_resolve_user_config_dir __orca_resolve_inherited_config_dir 2>/dev/null
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
unset _orca_zshenv_path
|
||||
|
||||
# Why: use :- after user .zshenv — a pathological unset under set -u must not
|
||||
# abort the wrapper; empty falls through to the baked-literal branch.
|
||||
@@ -88,7 +103,7 @@ if [[ -n "${_orca_wrapper_zdotdir_self:-}" && -f "${_orca_wrapper_zdotdir_self:-
|
||||
else
|
||||
export ZDOTDIR='<WRAPPER_ROOT>/zsh'
|
||||
fi
|
||||
unset _orca_user_zdotdir _orca_zshenv_source_dir _orca_zshenv_path _orca_discovered_zdotdir _orca_wrapper_zdotdir_self
|
||||
unset _orca_wrapper_zdotdir_self
|
||||
|
||||
__orca_shell_epilogue() {
|
||||
# Why first: this body runs after the user's own config, so it would otherwise
|
||||
@@ -173,7 +188,9 @@ __orca_shell_epilogue() {
|
||||
}
|
||||
__orca_osc133_preexec() {
|
||||
printf "\033]133;C\007"
|
||||
__orca_in_command=1
|
||||
# Why typeset -g: a plain assignment here creates a global inside a function,
|
||||
# which prints a warning above every command under warn_create_global.
|
||||
typeset -g __orca_in_command=1
|
||||
}
|
||||
# Why: prepend so Orca captures $? before user prompt hooks can overwrite it.
|
||||
precmd_functions=(__orca_osc133_precmd ${precmd_functions[@]})
|
||||
@@ -206,8 +223,8 @@ __orca_shell_epilogue() {
|
||||
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
# Why: after Orca's last wrapper file has loaded, the interactive shell should
|
||||
# expose the same ZDOTDIR a normal zsh startup would expose.
|
||||
export ZDOTDIR="$REPLY"
|
||||
unset _orca_home REPLY
|
||||
export ZDOTDIR="$_orca_resolved_config_dir"
|
||||
unset _orca_home _orca_resolved_config_dir
|
||||
unset _orca_shell_features
|
||||
unfunction __orca_shell_epilogue __orca_has_feature __orca_resolve_user_config_dir __orca_resolve_inherited_config_dir
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Orca zsh shell-ready wrapper
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
typeset -g _orca_resolved_config_dir="${1:-}"
|
||||
while [[ "$_orca_resolved_config_dir" == */ ]]; do _orca_resolved_config_dir="${_orca_resolved_config_dir%/}"; done
|
||||
if [[ -z "$_orca_resolved_config_dir" || -f "$_orca_resolved_config_dir/.orca-shell-wrapper" || "$_orca_resolved_config_dir" == */shell-ready/zsh ]]; then
|
||||
_orca_resolved_config_dir="$HOME"
|
||||
fi
|
||||
}
|
||||
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
_orca_home="$REPLY"
|
||||
_orca_home="$_orca_resolved_config_dir"
|
||||
if [[ "$_orca_home" != "$ZDOTDIR" && -o interactive && -f "$_orca_home/.zshrc" ]]; then
|
||||
_orca_wrapper_zdotdir="$ZDOTDIR"
|
||||
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Orca relay zsh overlay wrapper
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
typeset -g _orca_resolved_config_dir="${1:-}"
|
||||
while [[ "$_orca_resolved_config_dir" == */ ]]; do _orca_resolved_config_dir="${_orca_resolved_config_dir%/}"; done
|
||||
if [[ -z "$_orca_resolved_config_dir" || -f "$_orca_resolved_config_dir/.orca-shell-wrapper" || "$_orca_resolved_config_dir" == */shell-ready/zsh ]]; then
|
||||
_orca_resolved_config_dir="$HOME"
|
||||
fi
|
||||
}
|
||||
__orca_resolve_user_config_dir "${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"
|
||||
_orca_home="$REPLY"
|
||||
_orca_home="$_orca_resolved_config_dir"
|
||||
if [[ -o interactive && -f "$_orca_home/.zlogin" ]]; then
|
||||
_orca_wrapper_zdotdir="$ZDOTDIR"
|
||||
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Orca relay zsh overlay wrapper
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
typeset -g _orca_resolved_config_dir="${1:-}"
|
||||
while [[ "$_orca_resolved_config_dir" == */ ]]; do _orca_resolved_config_dir="${_orca_resolved_config_dir%/}"; done
|
||||
if [[ -z "$_orca_resolved_config_dir" || -f "$_orca_resolved_config_dir/.orca-shell-wrapper" || "$_orca_resolved_config_dir" == */shell-ready/zsh ]]; then
|
||||
_orca_resolved_config_dir="$HOME"
|
||||
fi
|
||||
}
|
||||
__orca_resolve_user_config_dir "${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"
|
||||
_orca_home="$REPLY"
|
||||
_orca_home="$_orca_resolved_config_dir"
|
||||
if [[ -f "$_orca_home/.zprofile" ]]; then
|
||||
_orca_wrapper_zdotdir="$ZDOTDIR"
|
||||
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
|
||||
@@ -18,3 +18,15 @@ if [[ -f "$_orca_home/.zprofile" ]]; then
|
||||
unset _orca_wrapper_zdotdir
|
||||
fi
|
||||
|
||||
if [[ -f "$_orca_home/.zprofile" ]]; then
|
||||
case "$(emulate 2>/dev/null)" in
|
||||
sh|ksh)
|
||||
export ZDOTDIR="$_orca_home"
|
||||
# Why unset: an ORCA_HISTFILE no wrapper file will ever consume is
|
||||
# inherited by everything this pane spawns, including a nested Orca.
|
||||
builtin unset ORCA_HISTFILE _orca_shell_features _orca_home _orca_resolved_config_dir _orca_wrapper_zdotdir_self
|
||||
unfunction __orca_shell_epilogue __orca_has_feature __orca_resolve_user_config_dir __orca_resolve_inherited_config_dir 2>/dev/null
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
@@ -5,10 +5,10 @@ builtin unset ORCA_SHELL_FEATURES
|
||||
__orca_has_feature() { (( ${_orca_shell_features[(Ie)$1]} )) }
|
||||
__orca_has_feature identity && printf "\033]777;orca-shell-start:%s\007" "$$"
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
typeset -g _orca_resolved_config_dir="${1:-}"
|
||||
while [[ "$_orca_resolved_config_dir" == */ ]]; do _orca_resolved_config_dir="${_orca_resolved_config_dir%/}"; done
|
||||
if [[ -z "$_orca_resolved_config_dir" || -f "$_orca_resolved_config_dir/.orca-shell-wrapper" || "$_orca_resolved_config_dir" == */shell-ready/zsh ]]; then
|
||||
_orca_resolved_config_dir="$HOME"
|
||||
fi
|
||||
}
|
||||
# Why stricter for an inherited value: Orca can be launched from a terminal that
|
||||
@@ -16,18 +16,32 @@ __orca_resolve_user_config_dir() {
|
||||
# startup file at all is not the user's config root whoever wrote it.
|
||||
__orca_resolve_inherited_config_dir() {
|
||||
__orca_resolve_user_config_dir "${1:-}"
|
||||
[[ "$REPLY" == "$HOME" ]] && return 0
|
||||
[[ "$_orca_resolved_config_dir" == "$HOME" ]] && return 0
|
||||
local _orca_startup_file
|
||||
for _orca_startup_file in .zshenv .zshrc .zprofile .zlogin; do
|
||||
[[ -r "$REPLY/$_orca_startup_file" ]] && return 0
|
||||
[[ -r "$_orca_resolved_config_dir/$_orca_startup_file" ]] && return 0
|
||||
done
|
||||
REPLY="$HOME"
|
||||
_orca_resolved_config_dir="$HOME"
|
||||
}
|
||||
__orca_resolve_inherited_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
export ORCA_ORIG_ZDOTDIR="$REPLY"
|
||||
export ORCA_ORIG_ZDOTDIR="$_orca_resolved_config_dir"
|
||||
[[ -f "$ORCA_ORIG_ZDOTDIR/.zshenv" ]] && source "$ORCA_ORIG_ZDOTDIR/.zshenv"
|
||||
__orca_resolve_user_config_dir "${ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"
|
||||
export ORCA_USER_ZDOTDIR="$REPLY"
|
||||
export ORCA_USER_ZDOTDIR="$_orca_resolved_config_dir"
|
||||
|
||||
if [[ -f "$ORCA_ORIG_ZDOTDIR/.zshenv" ]]; then
|
||||
case "$(emulate 2>/dev/null)" in
|
||||
sh|ksh)
|
||||
export ZDOTDIR="$ORCA_USER_ZDOTDIR"
|
||||
# Why unset: an ORCA_HISTFILE no wrapper file will ever consume is
|
||||
# inherited by everything this pane spawns, including a nested Orca.
|
||||
builtin unset ORCA_HISTFILE _orca_shell_features _orca_home _orca_resolved_config_dir _orca_wrapper_zdotdir_self
|
||||
unfunction __orca_shell_epilogue __orca_has_feature __orca_resolve_user_config_dir __orca_resolve_inherited_config_dir 2>/dev/null
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
export ZDOTDIR='<WRAPPER_ROOT>/zsh'
|
||||
|
||||
__orca_shell_epilogue() {
|
||||
@@ -110,8 +124,8 @@ __orca_shell_epilogue() {
|
||||
__orca_resolve_user_config_dir "${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"
|
||||
# Why: after Orca's last wrapper file has loaded, the interactive shell should
|
||||
# expose the same ZDOTDIR a normal zsh startup would expose.
|
||||
export ZDOTDIR="$REPLY"
|
||||
unset _orca_home REPLY
|
||||
export ZDOTDIR="$_orca_resolved_config_dir"
|
||||
unset _orca_home _orca_resolved_config_dir
|
||||
unset _orca_shell_features
|
||||
unfunction __orca_shell_epilogue __orca_has_feature __orca_resolve_user_config_dir __orca_resolve_inherited_config_dir
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Orca relay zsh overlay wrapper
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
typeset -g _orca_resolved_config_dir="${1:-}"
|
||||
while [[ "$_orca_resolved_config_dir" == */ ]]; do _orca_resolved_config_dir="${_orca_resolved_config_dir%/}"; done
|
||||
if [[ -z "$_orca_resolved_config_dir" || -f "$_orca_resolved_config_dir/.orca-shell-wrapper" || "$_orca_resolved_config_dir" == */shell-ready/zsh ]]; then
|
||||
_orca_resolved_config_dir="$HOME"
|
||||
fi
|
||||
}
|
||||
__orca_resolve_user_config_dir "${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"
|
||||
_orca_home="$REPLY"
|
||||
_orca_home="$_orca_resolved_config_dir"
|
||||
if [[ -o interactive && -f "$_orca_home/.zshrc" ]]; then
|
||||
_orca_wrapper_zdotdir="$ZDOTDIR"
|
||||
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
|
||||
|
||||
@@ -48,6 +48,16 @@ function runZsh(
|
||||
})
|
||||
}
|
||||
|
||||
/** Both streams, because a wrapper that breaks does it on stderr. */
|
||||
function runZshCapturingStderr(args: string[], env: Record<string, string>, probe: string): string {
|
||||
const result = spawnSync(ZSH_PATH, [...args, '-i', '-c', probe], {
|
||||
encoding: 'utf8',
|
||||
timeout: 20_000,
|
||||
env: { PATH: '/usr/bin:/bin', ...env }
|
||||
})
|
||||
return `${result.stdout ?? ''}${result.stderr ?? ''}`
|
||||
}
|
||||
|
||||
/**
|
||||
* True when this host's system zshrc is the thing that destroys HISTFILE.
|
||||
*
|
||||
@@ -267,3 +277,69 @@ describe('worktree-scoped HISTFILE survives zsh startup', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('the wrapper survives a hostile user zsh config', () => {
|
||||
const PROBE =
|
||||
'echo "RESULT=$HISTFILE"; print -r -- "ZDOTDIR=[$ZDOTDIR]"; ' +
|
||||
'print -r -- "ORCA_HISTFILE=[${ORCA_HISTFILE:-unset}]"'
|
||||
|
||||
// Why both forms: `REPLY` is zsh's shared scratch global, and the two ways a
|
||||
// user config constrains it fail differently. `typeset -r` makes the
|
||||
// wrapper's first assignment fatal and prints into the pane; `typeset -i`
|
||||
// turns every resolved path into 0 with no error text at all. Both aborted
|
||||
// the wrapper before the epilogue, leaving history inside Orca's own wrapper
|
||||
// dir — on 100% of zsh panes, not the few percent wrapped before.
|
||||
it.each(['typeset -r REPLY', 'typeset -i REPLY'])(
|
||||
'resolves the user config dir when the user .zshrc runs `%s`',
|
||||
(declaration) => {
|
||||
if (!hasZsh) {
|
||||
return
|
||||
}
|
||||
withTempHome((home) => {
|
||||
const scoped = join(home, 'orca-history', 'zsh_history')
|
||||
const { launch, env } = launchPlainHistoryPane(home, scoped)
|
||||
writeFileSync(join(home, '.zshrc'), `${declaration}\n`)
|
||||
|
||||
const output = runZshCapturingStderr(launch.args ?? ['-l'], env, PROBE)
|
||||
|
||||
expect(histfileOf(output)).toBe(scoped)
|
||||
expect(output).toContain(`ZDOTDIR=[${home}]`)
|
||||
expect(output).toContain('ORCA_HISTFILE=[unset]')
|
||||
expect(output).not.toContain('__orca_resolve_user_config_dir:')
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// Why these two files and not .zshrc: zsh's sourcehome() ignores ZDOTDIR once
|
||||
// the shell is in sh/ksh emulation, so emulation entered from .zshenv or
|
||||
// .zprofile hides EVERY later wrapper file — the epilogue runs zero times and
|
||||
// can repair nothing. A .zshrc that does it is already covered by running the
|
||||
// epilogue from the wrapper .zshrc, which /etc/zshrc has run before.
|
||||
it.each([
|
||||
['.zshenv', 'emulate sh'],
|
||||
['.zshenv', 'emulate ksh'],
|
||||
['.zprofile', 'emulate sh']
|
||||
])('degrades to an unwrapped pane when the user %s runs `%s`', (file, statement) => {
|
||||
if (!hasZsh) {
|
||||
return
|
||||
}
|
||||
withTempHome((home) => {
|
||||
const scoped = join(home, 'orca-history', 'zsh_history')
|
||||
const { launch, env } = launchPlainHistoryPane(home, scoped)
|
||||
writeFileSync(join(home, file), `${statement}\n`)
|
||||
|
||||
const wrapped = runZshCapturingStderr(launch.args ?? ['-l'], env, PROBE)
|
||||
const unwrapped = runZsh(['-l'], { HOME: home, HISTFILE: scoped })
|
||||
|
||||
// The bar: never worse off than the pane Orca did not wrap. What that is
|
||||
// differs per host (macOS /etc/zshrc clobbers HISTFILE, stock Ubuntu does
|
||||
// not), so it is read from an unwrapped run rather than hardcoded.
|
||||
expect(histfileOf(wrapped)).toBe(histfileOf(unwrapped))
|
||||
expect(histfileOf(wrapped)).not.toContain(launch.env.ZDOTDIR)
|
||||
expect(wrapped).toContain(`ZDOTDIR=[${home}]`)
|
||||
// Nothing this pane spawns may inherit a history path no wrapper file
|
||||
// will ever consume.
|
||||
expect(wrapped).toContain('ORCA_HISTFILE=[unset]')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -22,6 +22,7 @@ import { getPosixOmpShellWrapper } from './pty/omp-shell-wrapper'
|
||||
import { getPosixCodexShellLaunchPreflight } from './pty/codex-shell-launch-preflight'
|
||||
import {
|
||||
getZshEnvDiscoveryBody,
|
||||
getZshEmulationDegradeBlock,
|
||||
getZshFinalZdotdirRestoreBlock,
|
||||
getZshOverlayEnvBody,
|
||||
getZshShellReadyMarkerRegistrationBlock,
|
||||
@@ -100,7 +101,9 @@ const ZSH_OSC133_COMMAND_MARKER_BLOCK = `__orca_osc133_precmd() {
|
||||
}
|
||||
__orca_osc133_preexec() {
|
||||
printf "\\033]133;C\\007"
|
||||
__orca_in_command=1
|
||||
# Why typeset -g: a plain assignment here creates a global inside a function,
|
||||
# which prints a warning above every command under warn_create_global.
|
||||
typeset -g __orca_in_command=1
|
||||
}
|
||||
# Why: prepend so Orca captures $? before user prompt hooks can overwrite it.
|
||||
precmd_functions=(__orca_osc133_precmd \${precmd_functions[@]})
|
||||
@@ -227,6 +230,14 @@ export function buildZshStartupWrapperFiles(spec: ZshStartupWrapperSpec): ZshSta
|
||||
getZshStartupFileSourceBlock({
|
||||
fileName: '.zprofile',
|
||||
homeExpression: spec.homeExpression
|
||||
}),
|
||||
// Why here too: a user .zprofile ending in `emulate sh` hides .zshrc and
|
||||
// .zlogin from the wrapper exactly as a user .zshenv does, and .zprofile
|
||||
// is the last wrapper file that still runs before /etc/zshrc clobbers
|
||||
// HISTFILE — so this is the last point where degrading cleanly is possible.
|
||||
getZshEmulationDegradeBlock({
|
||||
userZdotdirExpression: '"$_orca_home"',
|
||||
sourcedUserFileTest: '-f "$_orca_home/.zprofile"'
|
||||
})
|
||||
])}\n`,
|
||||
zshrc: `${joinBlocks([
|
||||
|
||||
@@ -220,6 +220,50 @@ describe('PtyHandler', () => {
|
||||
}
|
||||
)
|
||||
|
||||
// Why unconditionally, not only with isolation on: injectRelayHistoryEnv is
|
||||
// what normally mints (and first clears) ORCA_HISTFILE, and it runs only
|
||||
// with isolation on. An inherited one — the relay can be launched from an
|
||||
// Orca pane — would otherwise reach the remote wrapper on the disabled and
|
||||
// revive paths, re-exporting another worktree's history path (#11146) and
|
||||
// wrapping a zsh pane nothing asked to wrap.
|
||||
it.each([
|
||||
[
|
||||
'a relay-minted path',
|
||||
`${process.env.HOME ?? ''}/.orca-remote/terminal-history/aabbccddeeff0011-zsh_history`
|
||||
],
|
||||
['a desktop-minted path', '/fake/userData/terminal-history/aabbccddeeff0011/zsh_history'],
|
||||
['a user value', '/home/me/.zsh_history']
|
||||
])(
|
||||
'drops %s inherited as ORCA_HISTFILE from the relay process env',
|
||||
async (_kind, inherited) => {
|
||||
const previous = process.env.ORCA_HISTFILE
|
||||
process.env.ORCA_HISTFILE = inherited
|
||||
try {
|
||||
await dispatcher.callRequest('pty.spawn', { cols: 80, rows: 24 })
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.ORCA_HISTFILE
|
||||
} else {
|
||||
process.env.ORCA_HISTFILE = previous
|
||||
}
|
||||
}
|
||||
|
||||
const spawnEnv = mockPtySpawn.mock.calls.at(-1)?.[2]?.env as Record<string, string>
|
||||
expect(spawnEnv.ORCA_HISTFILE).toBeUndefined()
|
||||
}
|
||||
)
|
||||
|
||||
it('drops an ORCA_HISTFILE handed over in the client env', async () => {
|
||||
await dispatcher.callRequest('pty.spawn', {
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
env: { ORCA_HISTFILE: '/fake/userData/terminal-history/aabbccddeeff0011/zsh_history' }
|
||||
})
|
||||
|
||||
const spawnEnv = mockPtySpawn.mock.calls.at(-1)?.[2]?.env as Record<string, string>
|
||||
expect(spawnEnv.ORCA_HISTFILE).toBeUndefined()
|
||||
})
|
||||
|
||||
it('drops a desktop-minted session handed over in the client env', async () => {
|
||||
await dispatcher.callRequest('pty.spawn', {
|
||||
cols: 80,
|
||||
|
||||
@@ -641,6 +641,13 @@ export class PtyHandler {
|
||||
// on, yet an inherited Orca HISTFILE must not scope a pane to someone else's
|
||||
// worktree on the disabled and revive paths either.
|
||||
dropInheritedOrcaHistFile(result)
|
||||
// Why unconditionally: ORCA_HISTFILE is Orca-owned and minted below by
|
||||
// injectRelayHistoryEnv, which also runs only with isolation on. An
|
||||
// inherited one (the relay can be launched from an Orca pane) would
|
||||
// otherwise reach the wrapper on the disabled and revive paths, scoping the
|
||||
// pane to another worktree's history file — and wrapping a zsh pane that
|
||||
// nothing asked to wrap, since `history` is selected on its presence.
|
||||
delete result.ORCA_HISTFILE
|
||||
// Why: match local/daemon precedence so defaults/augmenters can't resurrect explicitly-removed values.
|
||||
for (const key of envToDelete) {
|
||||
delete result[key]
|
||||
|
||||
@@ -67,7 +67,7 @@ function expectZdotdirSourceContext(content: string, fileName: '.zprofile' | '.z
|
||||
|
||||
function expectFinalZdotdirRestoreContext(content: string) {
|
||||
expect(content).toContain("after Orca's last wrapper file has loaded")
|
||||
expect(content).toContain('export ZDOTDIR="$REPLY"')
|
||||
expect(content).toContain('export ZDOTDIR="$_orca_resolved_config_dir"')
|
||||
}
|
||||
|
||||
describe('isRelayWslShell', () => {
|
||||
@@ -113,7 +113,7 @@ describe('getRelayShellLaunchConfig', () => {
|
||||
const zshenv = readFileSync(join(zshRoot, '.zshenv'), 'utf8')
|
||||
const userZdotdirResolution =
|
||||
'__orca_resolve_user_config_dir "${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"'
|
||||
expect(zshenv).toContain('export ORCA_USER_ZDOTDIR="$REPLY"')
|
||||
expect(zshenv).toContain('export ORCA_USER_ZDOTDIR="$_orca_resolved_config_dir"')
|
||||
const zprofile = readFileSync(join(zshRoot, '.zprofile'), 'utf8')
|
||||
const zshrc = readFileSync(join(zshRoot, '.zshrc'), 'utf8')
|
||||
const zlogin = readFileSync(join(zshRoot, '.zlogin'), 'utf8')
|
||||
@@ -222,7 +222,7 @@ describe('getRelayShellLaunchConfig', () => {
|
||||
})
|
||||
|
||||
expect(readFileSync(join(zshRoot, '.zshenv'), 'utf8')).toContain(
|
||||
'export ORCA_USER_ZDOTDIR="$REPLY"'
|
||||
'export ORCA_USER_ZDOTDIR="$_orca_resolved_config_dir"'
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user