/** * macOS only, and deliberately so. * * The desktop app — and with it this in-process agent — is started by launchd * (SMAppService, dev.aicommander.tray.plist), which declares no * EnvironmentVariables, so the process inherits launchd's minimal * /usr/bin:/bin:/usr/sbin:/sbin. Commands then run through `/bin/sh -c`, a * NON-login, NON-interactive shell that reads no profile, so homebrew, nvm/fnm, * docker and everything else the user installed is simply not on PATH — while * the very same command typed into the user's own terminal works. Asking the * user's login shell once what its PATH is, and merging that in, is what makes * the two agree. * * The probe is best-effort BY CONSTRUCTION: a login+interactive shell runs the * user's rc files, which may be slow, broken, or block waiting for input. Every * failure mode — timeout, non-zero exit, empty or implausible output, spawn * error — collapses to "keep the inherited PATH", because a broken .zshrc must * never be able to wedge command execution. It runs at most once per process. * * Elevated exec deliberately does NOT use this: priv-helper keeps a locked PATH * of system directories only, because /usr/local/bin and /opt/homebrew/bin are * admin-writable on macOS and would be a root-planting vector. * * The probe carries a SECOND value for the same reason and at no extra cost: * $LANG. launchd hands the app no locale at all, so a remote command on macOS * ran with LANG empty while the identical command on Linux saw en_US.UTF-8 — * and a Python or Node tool that assumes a UTF-8 locale then mangles or crashes * on non-ASCII output, which this fleet produces daily (Polish, Japanese). One * shell invocation answers both questions; see applyLoginShellLocale. */ export declare const LOGIN_SHELL_PATH_TIMEOUT_MS = 2000; /** What one probe learned. Either field may be null: they fail open separately. */ interface ProbeResult { path: string | null; lang: string | null; } /** * Resolve the user's login-shell PATH (and $LANG) once per process. Memoized: * every caller shares the single probe, and the result is cached for * synchronous reuse. */ export declare function startLoginShellPathProbe(): Promise; /** * The promise a caller must await before building a child environment, or null * when there is nothing to wait for (not macOS, or already resolved). Starting * the probe is a side effect of asking — the first command pays for it, every * later one reads the cache. */ export declare function pendingLoginShellPath(): Promise | null; /** * Merge the resolved login-shell PATH into an environment block, in place. * A no-op until the probe has resolved to a usable value, so it is always safe * to call synchronously. Inherited entries the login shell does not know about * are KEPT (appended): this adds directories, it never takes any away. */ export declare function applyLoginShellPath(env: NodeJS.ProcessEnv): void; /** * Give a macOS child a usable locale, in place. macOS only — Linux agents * already inherit a LANG from systemd/the login session, and Windows does not * use the variable at all. * * WHAT WAS WRONG. launchd declares no environment, so a remote command on macOS * ran with `LANG=[]` where the same command on Linux saw `LANG=en_US.UTF-8`. * An empty LANG means the C locale: Python (pre-3.7 defaults and any * locale-aware library), Perl, sort, and a long tail of CLI tools then treat * output as ASCII and mangle or refuse non-ASCII text. This fleet handles Polish * and Japanese daily, so macOS being the odd one out was a live source of * corrupted output. * * PRECEDENCE, unchanged from PATH: an inherited non-empty LANG is left alone, * and because this runs BEFORE the caller's overlay is merged, an explicit LANG * in `env` still wins. We only fill a hole. * * TERM is filled in by applyNonInteractiveTerm, one call later and on every * POSIX platform — same "fill a hole, never overwrite" rule, different variable. */ export declare function applyLoginShellLocale(env: NodeJS.ProcessEnv): void; /** * Give a POSIX child a TERM, in place. Fills a HOLE only — an inherited TERM is * left exactly as it is, and because this runs before the caller's overlay is * merged, an explicit TERM in `env` still wins. Same precedence as PATH and * LANG, deliberately. * * WHAT WAS WRONG. This used to be a comment claiming "a remote command inherits * TERM=dumb, so ANSI sequences are suppressed" — a guarantee nobody provided. * Nothing in the agent set TERM: under launchd/systemd (the same situation the * LANG fix above exists for) the agent has NO TERM at all, and started from a * terminal it has a full xterm-256color. So the value the reasoning rested on * was whatever happened to be there. A comment asserting a security- or * correctness-relevant property the code does not have is worse than no comment, * because the next reader stops checking; so the code now does the thing. * * WHY `dumb` AND NOT NOTHING. A remote command is non-interactive by * construction — its output is read by a model, never rendered by a terminal — * and with TERM unset a good deal of software guesses. `dumb` is what tells ls, * git, pytest, npm and friends to emit no colour and no cursor control, which * would otherwise arrive as escape noise interleaved with the text the caller * actually asked for. Filling the hole is also strictly closer to a real * terminal session than leaving it empty: a login shell always has one. * * THE HONEST LIMIT. Because an inherited value wins, an agent started FROM a * terminal still passes that terminal's TERM down, and its children may still * colour their output. That is the same precedence trade PATH and LANG make (the * machine's own configuration outranks our default) and it costs nothing where * it matters: the shipped agent runs under launchd, systemd or the Windows * service, none of which hand it a TERM. So the guarantee here is "the child is * never left with TERM unset", not "output is never coloured". * * Windows is skipped: cmd.exe and PowerShell take no notice of TERM, and the * few ported tools that do would be reading a POSIX terminal name off a machine * that has no terminfo to look it up in. */ export declare function applyNonInteractiveTerm(env: NodeJS.ProcessEnv): void; /** Test helper: forget the memoized probe so a new platform/spawn can be set up. */ export declare function resetLoginShellPathForTest(): void; export {};