Download packages/kaos/src/login-shell-path.ts from SaylorTwift/kimi-code: direct link, hf CLI and curl.
- Browser
- Download file 6.77 kB
-
https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/kaos/src/login-shell-path.ts
- Command line
-
hf download hf://SaylorTwift/kimi-code/packages/kaos/src/login-shell-path.ts
-
curl -L -o login-shell-path.ts https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/kaos/src/login-shell-path.ts
6.77 kB
| /** | |
| * Login-shell PATH probe — enrich `process.env.PATH` with entries from the | |
| * user's login shell. | |
| * | |
| * When kimi-code is launched from a context that skipped the user's shell | |
| * profile (GUI launchers, non-login parent shells), `process.env.PATH` | |
| * misses entries like `/opt/homebrew/bin`, so commands spawned by the Bash | |
| * tool can't find tools the user has in their interactive shell (e.g. | |
| * `gh`). We run the user's login shell once (`$SHELL -l -c /usr/bin/env`), | |
| * extract its PATH, and append the entries the current PATH lacks. Existing | |
| * entries keep their order and priority; failures (no resolvable shell, | |
| * hung or broken profile) silently leave PATH untouched. | |
| * | |
| * launchd/daemon launches can leave `$SHELL` unset or blank, so | |
| * the probe falls back to the OS account's login shell from the user | |
| * database before giving up. | |
| * | |
| * Like `detectEnvironment`, the probe is a pure function of injected deps | |
| * so the suite runs identically on any host. Windows is skipped: the | |
| * problem is specific to POSIX login-shell profiles. | |
| */ | |
| import { userInfo } from 'node:os'; | |
| import { execFileText } from './environment'; | |
| export interface LoginShellPathDeps { | |
| readonly platform: string; | |
| readonly env: Record<string, string | undefined>; | |
| /** Login shell from the OS user database; fallback when $SHELL is unset. */ | |
| readonly userShell: () => string | undefined; | |
| readonly execFileText: ( | |
| file: string, | |
| args: readonly string[], | |
| timeoutMs: number, | |
| ) => Promise<string | undefined>; | |
| } | |
| const LOGIN_SHELL_ENV_TIMEOUT_MS = 5_000; | |
| /** | |
| * Run the user's login shell and return its PATH, or `undefined` when the | |
| * probe does not apply (Windows, no resolvable shell) or fails (spawn | |
| * error, timeout, no PATH in the output). | |
| */ | |
| export async function probeLoginShellPath(deps: LoginShellPathDeps): Promise<string | undefined> { | |
| if (deps.platform === 'win32') return undefined; | |
| // A set-but-blank $SHELL (some daemon/launchd envs) must also fall back. | |
| const envShell = deps.env['SHELL']?.trim(); | |
| const shell = envShell === undefined || envShell.length === 0 ? deps.userShell() : envShell; | |
| if (shell === undefined || shell.length === 0) return undefined; | |
| // `env` prints the resolved environment in every shell dialect, unlike | |
| // `echo $PATH`, which fish would join with spaces. Invoke it by absolute | |
| // path: a bare `env` resolves through the inherited PATH — which may | |
| // carry cwd-dependent components — from the workspace cwd, so a | |
| // repo-planted `env` binary could run at session startup and feed us an | |
| // arbitrary PATH. The absolute path also bypasses profile function | |
| // shadowing, and /usr/bin/env is guaranteed on every mainstream POSIX | |
| // system (it is the canonical shebang interpreter path). | |
| const stdout = await deps.execFileText( | |
| shell, | |
| ['-l', '-c', '/usr/bin/env'], | |
| LOGIN_SHELL_ENV_TIMEOUT_MS, | |
| ); | |
| if (stdout === undefined) return undefined; | |
| // Profile output lands on stdout before `env` runs, so keep the last | |
| // PATH= line. | |
| let path: string | undefined; | |
| for (const line of stdout.split('\n')) { | |
| if (line.startsWith('PATH=')) { | |
| path = line.slice('PATH='.length).trim(); | |
| } | |
| } | |
| if (path === undefined || path.length === 0) return undefined; | |
| return path; | |
| } | |
| /** | |
| * Union of the current PATH and the login-shell PATH: the current PATH | |
| * string is kept verbatim — including empty components, which POSIX | |
| * command lookup treats as the current directory — and login-shell | |
| * entries the current PATH lacks are appended in their own order. When | |
| * nothing is missing the current string is returned unchanged. Only | |
| * absolute login-shell entries are imported: empty, `.`, and relative | |
| * components are all cwd-dependent lookup, and appending one the user | |
| * did not already have would widen their search path — LocalKaos runs | |
| * commands from arbitrary workspace directories. | |
| */ | |
| export function mergeLoginShellPath( | |
| currentPath: string | undefined, | |
| loginShellPath: string, | |
| ): string { | |
| const current = currentPath ?? ''; | |
| const seen = new Set(current.split(':').filter((entry) => entry.length > 0)); | |
| const additions: string[] = []; | |
| for (const entry of loginShellPath.split(':')) { | |
| // The probe only runs on POSIX (win32 bails before merging), so a | |
| // leading slash is a sufficient absoluteness test. Empty components | |
| // fail it too. | |
| if (!entry.startsWith('/') || seen.has(entry)) continue; | |
| seen.add(entry); | |
| additions.push(entry); | |
| } | |
| if (additions.length === 0) return current; | |
| // `undefined` means "no PATH at all", so the additions stand alone; '' | |
| // is a real (cwd-only) PATH whose empty component must survive as a | |
| // leading colon. | |
| if (currentPath === undefined) return additions.join(':'); | |
| return `${current}:${additions.join(':')}`; | |
| } | |
| /** Probe the login shell and merge its PATH into `deps.env['PATH']`. */ | |
| export async function applyLoginShellPath(deps: LoginShellPathDeps): Promise<void> { | |
| const loginShellPath = await probeLoginShellPath(deps); | |
| if (loginShellPath === undefined) return; | |
| const currentPath = deps.env['PATH']; | |
| const merged = mergeLoginShellPath(currentPath, loginShellPath); | |
| // Only write when something was appended — an unset PATH must stay | |
| // unset (assigning '' would turn "implementation default search path" | |
| // into "cwd-only lookup"), and a set PATH must not be rewritten. | |
| if (merged === (currentPath ?? '')) return; | |
| deps.env['PATH'] = merged; | |
| } | |
| /** | |
| * Production convenience — apply the probe to `process.env` once per | |
| * process. Memoised like `detectEnvironmentFromNode`: the login-shell PATH | |
| * does not change for the lifetime of the process, and repeated | |
| * `LocalKaos.create()` calls must not re-spawn the shell. | |
| */ | |
| /** | |
| * Login shell from the OS user database (`/etc/passwd` via getpwuid on | |
| * Linux, Directory Services on macOS). `userInfo()` throws when the uid | |
| * has no database entry (e.g. containers running an arbitrary uid), and | |
| * service accounts may carry `/usr/sbin/nologin` — the latter needs no | |
| * special casing here because probing it simply fails and degrades | |
| * silently. | |
| */ | |
| function userShellFromNode(): string | undefined { | |
| try { | |
| const shell = userInfo().shell; | |
| return shell === null || shell.length === 0 ? undefined : shell; | |
| } catch { | |
| return undefined; | |
| } | |
| } | |
| let appliedLoginShellPath: Promise<void> | undefined; | |
| export function applyLoginShellPathFromNode(): Promise<void> { | |
| if (appliedLoginShellPath !== undefined) return appliedLoginShellPath; | |
| appliedLoginShellPath = applyLoginShellPath({ | |
| platform: process.platform, | |
| env: process.env as Record<string, string | undefined>, | |
| userShell: userShellFromNode, | |
| execFileText, | |
| }); | |
| return appliedLoginShellPath; | |
| } | |