/** * Decides where a generated shell wrapper tree lives, by hashing its contents. * * Why: every writer that shares a userData dir -- main's local PTY path, the * daemon fork, and the daemons of other builds that outlive the app that * spawned them -- wrote one fixed `shell-ready/` tree. Last writer won, and the * guard only re-checked that the files were present, never that they were this * build's. A daemon whose spawn env no longer agreed with the wrapper on disk * then kept launching shells it could not read: the ready marker never fired * and every startup command waited out the full readiness timeout, silently. * * Naming the directory after a hash of its own contents makes that impossible: * different bytes are a different directory, so "present" means "written by * this build" again. Writing stays in shell-wrapper-file-writer.ts. * * Why nothing here collects old trees: a tree is ~48KB and a new one appears * only when the wrapper templates themselves change, so an install accrues a * couple of megabytes a year against a userData directory that runs to tens of * gigabytes. Reclaiming that is not worth putting an `rm -rf` on the * terminal-spawn path. */ import { createHash } from 'node:crypto' import { join } from 'node:path' import type { ShellWrapperFile } from './shell-wrapper-file-writer' /** Builds the wrapper set for a tree rooted at `root`, as the writer takes it. */ export type ShellWrapperFileBuilder = (root: string) => readonly ShellWrapperFile[] // Why a placeholder root: .zshenv bakes in the tree path, so hashing against the // real root would make the digest depend on the very path it is choosing. A // fixed stand-in keeps it stable across machines and user data dirs while still // covering every difference that matters. // // Note this string is itself part of the digest, because it appears in the baked // content. Renaming it re-keys every tree -- which costs one extra ~48KB // directory per install and nothing else, so it is not worth normalizing out. // // The digest therefore covers the probe build, not the literal bytes on disk. // That holds only while the sole path-dependent thing in a wrapper is the root // itself. A future template that varies on something else about its location // would need that input folded in here, or two genuinely different trees would // collide on one directory. const HASH_PROBE_ROOT = '/__orca_shell_wrapper_root__' const ROOT_HASH_LENGTH = 16 // Why the hash sits ABOVE this leaf rather than below it: ZDOTDIR // self-reference guards -- in TS and as `*/shell-ready/zsh` globs baked into // the wrapper scripts -- match on that exact suffix. Without it a wrapper // sources itself and zsh dies with "job table full or recursion limit // exceeded". `//shell-ready/zsh` leaves every guard intact and // keeps older builds able to recognize a newer build's dir. const WRAPPER_ROOT_LEAF = 'shell-ready' export function resolveShellWrapperRoot(baseDir: string, build: ShellWrapperFileBuilder): string { const digest = createHash('sha256') for (const [path, content] of build(HASH_PROBE_ROOT)) { // Relative, so the digest does not vary with the base dir it is naming. digest.update(path.slice(HASH_PROBE_ROOT.length)) digest.update('\0') digest.update(content) digest.update('\0') } return join(baseDir, digest.digest('hex').slice(0, ROOT_HASH_LENGTH), WRAPPER_ROOT_LEAF) }