mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 16:02:15 +00:00
* fix(terminal): give plain fish tabs Orca's codex function without changing fish's startup A `codex` typed into a plain fish tab ran without --no-daemon because only wrapped fish tabs (startup command / ready marker) got Orca's codex function. Plain fish spawns now prepend an Orca data dir to XDG_DATA_DIRS and record the exact prefix in ORCA_FISH_XDG_DATA_DIRS_PREFIX. Fish sources the dir's fish/vendor_conf.d snippet, which first restores XDG_DATA_DIRS (unset again if it was unset), erases the marker, drops its dir from fish's derived vendor/function/ completion paths, then defines the shared fish codex function at the first prompt so the user's config.fish still wins. fish argv is unchanged; wrapped tabs keep their existing -C path. A local fallback to another shell restores the user's XDG_DATA_DIRS instead of deleting it. Bumps the terminal daemon protocol to v39 so new tabs move to a daemon that injects the env; v38 owners stay attachable. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(fish): skip the XDG handoff for -N/--no-config and empty XDG_DATA_DIRS fish never reads vendor_conf.d under -N/--no-config (also abbreviated or clustered), so the snippet could not undo the prefix; and the restore cannot tell an empty XDG_DATA_DIRS from an unset one. Both now launch untouched. Run the real-fish handoff tests in the shell contracts job, where fish is required, so they no longer skip in CI. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(fish): compare the unset-restore case against a fish without Orca Ubuntu runners ship snapd's fish vendor snippet, which sets XDG_DATA_DIRS on every fish start, so "unset" was never the right oracle there. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(fish): treat an empty XDG_DATA_DIRS like unset so the tab still gets the codex hook Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(pty): put back the user's own launch env on a shell fallback The primary shell's launch config now records the pre-launch value of each key it writes. A fallback shell restores those values (unsetting keys that had none) instead of deleting the keys, which hands back an inherited XDG_DATA_DIRS after a fish fallback and an inherited ZDOTDIR after a zsh->bash fallback, with no per-shell special case. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(fish): drop the Node restore twin and simplify the vendor snippet - Remove restoreFishXdgDataDirs; the generic fallback restore covers it. - Snippet: read ":$XDG_DATA_DIRS:" directly and filter Orca's vendor dirs with one string match per variable. - Require inheritedXdgDataDirs in both getShellLaunchConfig option shapes. - Drop the test-only FISH_XDG_DATA_DIRS_HANDOFF_DAEMON_PROTOCOL_VERSION. - Fix stale fish comments and trim redundant -N launch cases. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(fish): stop scrubbing fish's lookup paths after the handoff Only XDG_DATA_DIRS is restored, by exact prefix; Orca's dir holds nothing but this snippet, so leaving it on fish's derived paths loads nothing else and drops the glob match. * docs(fish): drop the comment for the removed vendor-dir cleanup --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
215 lines
7.5 KiB
TypeScript
215 lines
7.5 KiB
TypeScript
import { tmpdir } from 'node:os'
|
|
import { basename, join, win32 as pathWin32 } from 'node:path'
|
|
import { statSync } from 'node:fs'
|
|
import {
|
|
encodePowerShellCommand,
|
|
getPowerShellOsc133Bootstrap,
|
|
isPowerShellExecutableName
|
|
} from '../powershell-osc133-bootstrap'
|
|
import { getFishCodexShellLaunchPreflight } from '../../shared/codex-shell-function'
|
|
import { getFishXdgDataDirsLaunchEnv } from '../fish-xdg-data-dirs-handoff'
|
|
import { getFishShellReadyInitCommand } from '../shell-templates'
|
|
import {
|
|
encodeShellStartupFeatures,
|
|
SHELL_STARTUP_FEATURE_ENV,
|
|
type ShellStartupFeature
|
|
} from '../shell-startup-features'
|
|
import { resolveShellWrapperRoot } from '../shell-wrapper-content-address'
|
|
import { writeShellWrapperFiles } from '../shell-wrapper-file-writer'
|
|
import {
|
|
buildDaemonShellReadyWrapperFiles,
|
|
getDaemonShellReadyWrapperPaths
|
|
} from './daemon-shell-ready-wrapper-fileset'
|
|
import { inheritedZdotdirEnv, resolveInheritedZdotdir } from '../zsh-wrapper-dir-ownership'
|
|
import { SHELL_READY_MARKER } from './daemon-shell-ready-marker'
|
|
|
|
const ORCA_USER_DATA_PATH_ENV = 'ORCA_USER_DATA_PATH'
|
|
|
|
function getShellReadyWrapperBaseDir(): string {
|
|
const userDataPath = process.env[ORCA_USER_DATA_PATH_ENV]
|
|
// Why a base dir of its own rather than the legacy `shell-ready/`: daemons of
|
|
// older builds still write that path unconditionally, so leaving it to them
|
|
// keeps this build's content-addressed trees out of their reach.
|
|
// Why the tmpdir fallback: older/test launchers may not seed
|
|
// ORCA_USER_DATA_PATH, and daemon startup must not fail before the parent can
|
|
// be fixed. It is dev/test-only -- daemon-init always passes the real path --
|
|
// which matters because the presence check is size-only, so a complete tree
|
|
// pre-planted under a shared /tmp would be trusted rather than overwritten.
|
|
return join(userDataPath || tmpdir(), userDataPath ? 'shell-wrappers' : 'orca-shell-wrappers')
|
|
}
|
|
|
|
// Why memoized and keyed on the base dir: the digest is stable for a given base
|
|
// dir, every shell launch asks for it, and the key self-invalidates if
|
|
// ORCA_USER_DATA_PATH is ever re-pointed mid-process.
|
|
let cachedShellReadyWrapperRoot: { baseDir: string; root: string } | null = null
|
|
|
|
export function getShellReadyWrapperRoot(): string {
|
|
const baseDir = getShellReadyWrapperBaseDir()
|
|
if (cachedShellReadyWrapperRoot?.baseDir !== baseDir) {
|
|
cachedShellReadyWrapperRoot = {
|
|
baseDir,
|
|
root: resolveShellWrapperRoot(baseDir, buildDaemonShellReadyWrapperFiles)
|
|
}
|
|
}
|
|
return cachedShellReadyWrapperRoot.root
|
|
}
|
|
|
|
function getRequiredShellReadyWrapperPaths(root = getShellReadyWrapperRoot()): readonly string[] {
|
|
return getDaemonShellReadyWrapperPaths(root)
|
|
}
|
|
|
|
// Why non-empty and not just present: a partial write leaves a zero-byte
|
|
// .zshenv, and pointing ZDOTDIR at that dir makes zsh skip the user's config.
|
|
function shellReadyWrappersExist(): boolean {
|
|
return getRequiredShellReadyWrapperPaths().every((path) => {
|
|
try {
|
|
return statSync(path).size > 0
|
|
} catch {
|
|
return false
|
|
}
|
|
})
|
|
}
|
|
|
|
/** True when every wrapper file is present and non-empty afterwards. */
|
|
function ensureShellReadyWrappers(): boolean {
|
|
if (process.platform === 'win32') {
|
|
return false
|
|
}
|
|
const root = getShellReadyWrapperRoot()
|
|
// Why existence alone decides, with no per-process flag: the root is keyed by
|
|
// a hash of the exact bytes we would write, so a tree that is present and
|
|
// non-empty is a tree this build wrote. Rewriting it would replace a live file
|
|
// on the terminal-spawn path for no gain.
|
|
if (!shellReadyWrappersExist()) {
|
|
const written = writeShellWrapperFiles(
|
|
buildDaemonShellReadyWrapperFiles(root),
|
|
'[daemon/shell-ready]'
|
|
)
|
|
if (!written || !shellReadyWrappersExist()) {
|
|
// Why no flag to reset: the next launch re-checks the files themselves, so
|
|
// a half-written tree is retried without any extra bookkeeping.
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
export function resolvePtyShellPath(env: Record<string, string>): string {
|
|
if (process.platform === 'win32') {
|
|
return env.ORCA_TERMINAL_WINDOWS_SHELL || 'powershell.exe'
|
|
}
|
|
return env.SHELL || process.env.SHELL || '/bin/zsh'
|
|
}
|
|
|
|
export function shellPathSupportsPtyStartupBarrier(shellPath: string): boolean {
|
|
const shellName = pathWin32.basename(basename(shellPath)).toLowerCase()
|
|
// Why fish: markerless, its startup command is written before fish's reader owns
|
|
// the PTY and the launch is lost under slow prompts like Starship (STA-3417).
|
|
return shellName === 'zsh' || shellName === 'bash' || shellName === 'fish'
|
|
}
|
|
|
|
export type ShellLaunchConfig = {
|
|
args: string[] | null
|
|
env: Record<string, string>
|
|
supportsReadyMarker: boolean
|
|
}
|
|
|
|
const UNWRAPPED: ShellLaunchConfig = {
|
|
args: null,
|
|
env: {},
|
|
supportsReadyMarker: false
|
|
}
|
|
|
|
/**
|
|
* The one launch-config entry point: args + env for a shell that should start
|
|
* with exactly `features` enabled. An empty selection is never wrapped.
|
|
*/
|
|
export function getShellLaunchConfig(
|
|
shellPath: string,
|
|
features: readonly ShellStartupFeature[],
|
|
options?: {
|
|
hasStartupCommand?: boolean
|
|
inheritedXdgDataDirs: string | undefined
|
|
shellArgs?: readonly string[]
|
|
}
|
|
): ShellLaunchConfig {
|
|
const shellName = pathWin32.basename(basename(shellPath)).toLowerCase()
|
|
|
|
if (shellName === 'zsh') {
|
|
if (features.length === 0) {
|
|
return UNWRAPPED
|
|
}
|
|
if (!ensureShellReadyWrappers()) {
|
|
// Why plain login zsh: ZDOTDIR pointed at an incomplete wrapper dir makes
|
|
// zsh skip the user's whole config. Losing Orca's features is recoverable.
|
|
return { args: ['-l'], env: {}, supportsReadyMarker: false }
|
|
}
|
|
return {
|
|
args: ['-l'],
|
|
env: {
|
|
...inheritedZdotdirEnv(resolveInheritedZdotdir(process.env)),
|
|
ZDOTDIR: join(getShellReadyWrapperRoot(), 'zsh'),
|
|
[SHELL_STARTUP_FEATURE_ENV]: encodeShellStartupFeatures(features)
|
|
},
|
|
supportsReadyMarker: features.includes('ready')
|
|
}
|
|
}
|
|
|
|
if (shellName === 'bash') {
|
|
if (features.length === 0 || !ensureShellReadyWrappers()) {
|
|
return UNWRAPPED
|
|
}
|
|
return {
|
|
args: ['--rcfile', join(getShellReadyWrapperRoot(), 'bash', 'rcfile')],
|
|
env: {
|
|
[SHELL_STARTUP_FEATURE_ENV]: encodeShellStartupFeatures(features)
|
|
},
|
|
supportsReadyMarker: features.includes('ready')
|
|
}
|
|
}
|
|
|
|
if (isPowerShellExecutableName(shellName)) {
|
|
return {
|
|
args: [
|
|
'-NoLogo',
|
|
'-NoExit',
|
|
// Why base64 and not -Command: see powershell-osc133-bootstrap.ts (MDE review).
|
|
'-EncodedCommand',
|
|
encodePowerShellCommand(getPowerShellOsc133Bootstrap())
|
|
],
|
|
env: {},
|
|
supportsReadyMarker: false
|
|
}
|
|
}
|
|
|
|
// Why: mirrors local-pty-shell-ready.ts; only these need the -C init, plain fish gets
|
|
// the env-only handoff below. The selection is baked in, so no feature env var.
|
|
if (shellName === 'fish' && (features.includes('ready') || options?.hasStartupCommand)) {
|
|
return {
|
|
args: [
|
|
'-l',
|
|
'-C',
|
|
`${getFishShellReadyInitCommand(SHELL_READY_MARKER, features.includes('ready'))}\n${getFishCodexShellLaunchPreflight()}`
|
|
],
|
|
env: {},
|
|
supportsReadyMarker: features.includes('ready')
|
|
}
|
|
}
|
|
|
|
// Why env only: mirrors local-pty-shell-ready.ts; a plain fish pane keeps fish's own argv.
|
|
if (shellName === 'fish' && ensureShellReadyWrappers()) {
|
|
return {
|
|
args: null,
|
|
env: getFishXdgDataDirsLaunchEnv(
|
|
getShellReadyWrapperRoot(),
|
|
options?.inheritedXdgDataDirs,
|
|
options?.shellArgs
|
|
),
|
|
supportsReadyMarker: false
|
|
}
|
|
}
|
|
|
|
return UNWRAPPED
|
|
}
|