/** This process is running on Windows. */ export declare const IS_WINDOWS: boolean; /** Windows delivers resizes on the stream; everyone else raises SIGWINCH. */ export declare function usesSigwinch(platform?: NodeJS.Platform): boolean; /** The launcher's environment file: a shell fragment or a `cmd` script. */ export declare function envFileName(platform?: NodeJS.Platform): string; /** The shell a Windows user has: PowerShell, where POSIX code would say bash. */ export declare function shellName(platform?: NodeJS.Platform): string; /** * Whether lock liveness is decided by asking the OS about the process (Windows: * `Get-Process` + creation time) rather than by reading `/proc` (Linux) or the * command line (darwin). * * `lockOwnerIsAlive` returns early into `windowsProcessMatchesLock` when this is * true; the POSIX body below it has no meaning there (`/proc` does not exist). */ export declare function usesProcessIdentity(platform?: NodeJS.Platform): boolean; /** * How to start the background Host so it outlives this process *and* does not * make its own children flash console windows on Windows. * * POSIX wants `detached: true` (setsid) so the Host survives the launcher and a * hung-up terminal. * * Windows is the opposite, and the trade-off is forced: `detached: true` maps to * DETACHED_PROCESS, and Windows ignores CREATE_NO_WINDOW (what `windowsHide` * sets) when DETACHED_PROCESS is present. Every console child the Host then * starts — each tool call, every shell, node, git — allocates its own console, * which is a visible window flashing over the TUI. So the Host is spawned * non-detached and inherits the launcher's console; descendants inherit it too * instead of creating one. * * The price *was* on the lifecycle side, and it was measured (on the Windows CI * leg by `scripts/tui-mock-probe.mjs --busy`, not reasoned about): libuv assigns * a non-detached child to its global job object, which is created with * KILL_ON_JOB_CLOSE. The launcher died, the job closed, and the Host was * terminated with it — a closed terminal window ended the session's compute. * * This function still returns `detached: false` there, because it describes a * *direct* spawn, and a direct spawn cannot have both properties: Node exposes * `detached` (DETACHED_PROCESS, which makes Windows ignore CREATE_NO_WINDOW) and * `windowsHide`, but not `CREATE_NEW_CONSOLE`. The way out is to not spawn the * Host directly on Windows: {@link hostBootstrapCommand} starts it through the * OS PowerShell, which *can* ask for a new console and hide it. This direct path * stays as the fallback for a Windows box without PowerShell. */ export declare function hostSpawnOptions(platform?: NodeJS.Platform): { detached: boolean; windowsHide: boolean; }; /** * The Windows PowerShell that ships with the operating system. * * `powershell.exe` and not `pwsh.exe`: the latter is PowerShell 7, an optional * install, while 5.1 is part of Windows 10/11. `%SystemRoot%` is where it lives * (`%windir%` is the legacy spelling of the same thing); when neither is set * there is nothing to find, and the caller falls back to a direct spawn. */ export declare function windowsPowerShellPath(env?: NodeJS.ProcessEnv): string | undefined; /** A PowerShell single-quoted literal; `''` is the only escape inside one. */ export declare function psQuote(value: string): string; /** * Quote one argv array into a Windows command line, the way `CreateProcess` * parses it back (the rule from "Everyone quotes command line arguments the * wrong way"): wrap in double quotes when the argument is empty or contains * whitespace or a quote, double the backslashes that precede a quote, and double * trailing backslashes before the closing quote. `Start-Process -ArgumentList` * joins its array with spaces and adds no quoting of its own, so the line has to * be right before it is handed over — a `DSH_HOME` with a space in it is the * normal case, not the exotic one. */ export declare function windowsCommandLine(argv: string[]): string; /** `-EncodedCommand` wants base64 of UTF-16LE, which is also what dodges quoting. */ export declare function encodePowerShellCommand(script: string): string; /** * The bootstrap script: start the Host with **its own console, hidden**, and * print its pid so the launcher can watch it. * * `Start-Process -WindowStyle Hidden` is the only way to ask for this from a * Node process. It is ShellExecuteEx/CreateProcess with `CREATE_NEW_CONSOLE` and * `SW_HIDE`: the Host gets a console of its own, so closing the user's terminal * no longer takes it down, and that console is invisible, so the tool calls that * inherit it do not flash. `-PassThru` gives the object whose `Id` is printed; * `-RedirectStandardError` keeps the Host's stderr log, which the direct spawn * used to feed through an inherited fd. * * The whole script travels as an encoded command, so nothing in it is ever * re-parsed by a shell. */ export declare function hiddenConsoleHostScript(options: { execPath: string; argv: string[]; /** Where the Host's stderr goes; omitted only in tests. */ stderrFile?: string; /** Where the Host's pid is written for the launcher to read. */ pidFile: string; }): string; /** * Set on the Host's environment by the bootstrap below, through * {@link bootstrapEnv}. * * The Host cannot ask whether it has a console of its own — Node exposes no such * question — so the launcher tells it. That is what lets the one build where the * lifecycle promise does not hold (Windows, no PowerShell, direct child) say so * at boot instead of letting the user discover it by closing the window. */ export declare const TUI_HOST_START_ENV = "DSH_TUI_HOST_START"; /** Marker value for a Host started with a hidden console of its own. */ export declare const TUI_HOST_START_BOOTSTRAP = "hidden-console"; /** The environment the bootstrap hands to the Host: the marker is added here. */ export declare function bootstrapEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv; /** * Whether this Host survives its terminal being closed. * * POSIX always does (`setsid`); Windows does exactly when the bootstrap started * it. A Windows Host without the marker is the fallback direct child, which * libuv's `KILL_ON_JOB_CLOSE` job takes down with the launcher — the one case * worth a boot notice. */ export declare function hostHasOwnConsole(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean; /** How the Host is started when it must not be a direct child. */ export interface HostBootstrapCommand { command: string; args: string[]; } /** * The hidden-console bootstrap for this platform, or `undefined` when the Host * should be spawned directly. * * Windows only, and only when the OS PowerShell is really there: the caller * falls back to {@link hostSpawnOptions}, which keeps the boot working (without * the survival property) on a machine where PowerShell is missing or blocked. * `exists` is injectable so the decision is assertable on Linux. */ export declare function hostBootstrapCommand(options: { platform?: NodeJS.Platform; env?: NodeJS.ProcessEnv; exists?: (path: string) => boolean; execPath: string; argv: string[]; stderrFile?: string; pidFile: string; }): HostBootstrapCommand | undefined; /** * A path a human can read, with the platform's own shorthand. * * `~/.dsh/env.sh` is the POSIX form; Windows users know `%USERPROFILE%`, and a * `C:\Users\...` prefix spelled out is noise in a one-line hint. */ export declare function displayHomePath(home: string, file: string, options?: { platform?: NodeJS.Platform; env?: NodeJS.ProcessEnv; userHome?: string; }): string; /** * Who owns the files this plugin writes, and how to keep it that way. * * On POSIX the `mode` passed to `writeFile`/`mkdir` (`0o600`, `0o700`) is the * whole story. **On Windows it is silently ignored**, and the files that matter * here are not cosmetic: `env.cmd` carries API keys, the SuperGrok token file * carries an OAuth grant, and the lock/socket directories carry session * metadata. What they get instead is the ACL inherited from their parent — fine * under `%USERPROFILE%\.dsh`, and *not* fine when `DSH_HOME` points somewhere * shared (`C:\dsh`, a network share, a machine where `Users` can read the * directory), which is exactly when nobody notices. * * So the intent is applied explicitly: `icacls` with inheritance removed and a * single grant to the current user. The argv is built by a pure function so it * can be asserted on Linux; applying it is best-effort by design — a machine * without `icacls`, or a path held open by another process, must not fail the * write that just succeeded. Failing closed here would mean a TUI that cannot * save its own settings. */ /** * The account `icacls` should grant, or undefined when the environment has none. * * `%USERDOMAIN%\%USERNAME%` where both are present: on a domain-joined machine a * bare name can resolve to the machine-local account of the same name, and a * grant to the wrong account — with inheritance already removed — would leave * the user's own API keys inaccessible to them. */ export declare function aclUserName(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string | undefined; /** * The exact `icacls` arguments for one path. * * `/inheritance:r` drops what the parent offered (this is the part that matters: * adding a grant without removing inheritance leaves `Users` in place), and * `(OI)(CI)` makes a directory's grant apply to what is created inside it — * without it every new lock file would need its own call. */ export declare function restrictPathArgs(path: string, user: string, options?: { directory?: boolean; }): string[]; /** Directories and files whose contents must not be readable by other users. */ export interface RestrictDeps { /** Runner for `icacls`; injected by tests. Returns true on success. */ run?: (command: string, args: string[]) => boolean; platform?: NodeJS.Platform; env?: NodeJS.ProcessEnv; } /** * Apply `mode` on POSIX, a single-user ACL on Windows. * * Returns whether the restriction was applied. Never throws: the caller has * already written the file, and a permission call is not worth losing it over. */ export declare function restrictPathToUser(path: string, options?: { mode: number; directory?: boolean; } & RestrictDeps): Promise; /** Synchronous twin, for the sites that create their file with `openSync`. */ export declare function restrictPathToUserSync(path: string, options?: { mode: number; directory?: boolean; } & RestrictDeps): boolean; /** * Whether the terminal in front of us can paint UTF-8 at all. * * The chrome this TUI draws — rules, status dots, the warning glyph — is * Unicode. A console whose output code page is not UTF-8 (a legacy Windows * console left on CP936 or CP437) and a POSIX locale that is not UTF-8 both * render those bytes as mojibake, which is worse than plain ASCII: the frame * still has to line up. This answers only "can it decode UTF-8", never "which * terminal", so the capability table stays where it is. * * The decision is read from the environment, which is all a portable test can * assert: `DSH_TUI_ASCII=1` forces the fallback and `=0` forbids it; otherwise * a locale naming a non-UTF-8 charset (or the bare `C` / `POSIX`) opts in, * and so does a Windows console whose output code page is recorded as anything * but 65001. An unset locale is left alone — a UTF-8 SSH session often exports * nothing, and guessing "ASCII" there would strip the chrome from the audience * this TUI is built for. * @param env - the environment to read (tests pass their own). * @param platform - the platform the decision is for. * @returns true when chrome must be drawn in ASCII. */ export declare function asciiFallbackEnabled(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean; /** * What this process can serve. * * The plugin is a *terminal* UI, and the host it is mounted into decides whether * there is a terminal at all: * * - `host-relay`: this is the detached TUI Host itself (see * `isTuiHostProcess`); its terminal lives in whichever window attaches to the * session, so a missing TTY here is expected and it must serve. * - `terminal`: a real TTY on stdin and stdout. * - `desktop`: the desktop Harness launcher (Electron-as-Node, GUI subsystem). * It never has a console, so no terminal profile can ever run under it. * - `no-tty`: anything else without a TTY — a pipe (`… | tee`), cron, a service. */ export type TerminalAvailability = 'host-relay' | 'terminal' | 'desktop' | 'no-tty'; /** * Decide what to serve, from facts the caller can state. * * Pure on purpose: this is the one branch that decides whether the plugin mounts * anything at all, and every caller-visible behaviour difference (a window, a * log line, a hard failure) hangs off it. * @param facts - the host process marker, the two TTY answers, and whether the * launcher is the desktop's. * @returns the availability bucket. */ export declare function terminalAvailability(facts: { hostProcess: boolean; stdinIsTty: boolean; stdoutIsTty: boolean; desktop: boolean; }): TerminalAvailability; /** * Whether this process is the desktop app's launcher rather than a terminal Node. * * The desktop Harness ships a PATH shim that runs its own GUI-subsystem binary * with `ELECTRON_RUN_AS_NODE=1` (`DeepSeek Harness.exe --expose-internals * app.asar/dsh/.../cli.js`). A GUI-subsystem process started from a console does * not attach to it on Windows, so `process.stdin.isTTY` / `stdout.isTTY` stay * undefined and no terminal profile can start under it. That is a property of * the launcher, not of the user's terminal, and the two cases deserve different * advice — which is all this decides. * @param facts - the runtime's own answers (`process.versions`, `execPath`, `argv`). * @returns true when the launcher is the desktop app's Electron-as-Node shim. */ export declare function desktopLauncher(facts: { electron?: string | undefined; execPath?: string | undefined; argv?: readonly string[] | undefined; }): boolean; /** * The message a terminal profile fails with when the launcher has no TTY. * * The desktop case is worth its own wording: the generic "must be TTYs" line * sends a desktop user hunting for a broken terminal, while the real answer is * "this launcher never has one — use the npm CLI in a terminal/SSH session, and * note the desktop app does not need this plugin at all". * @param desktop - whether {@link desktopLauncher} recognised the launcher. * @returns the error text for the plugin to throw. */ export declare function nonTtyErrorMessage(desktop: boolean): string; /** * What to log when the plugin has nowhere to draw. * * Deliberately not an error: on the desktop the situation is normal (a GUI app * has no console), and a terminal profile is not a thing the user broke. The * line has to (a) say nothing is running *from this plugin*, (b) explain why, * (c) give the one command that fixes it, and (d) not read as a failure — the * plugin used to throw here, which surfaced as "1 entry did not activate" and, * on the desktop, took the app down with it. * @param availability - `desktop` or `no-tty`. * @returns the log line. */ export declare function inactiveNotice(availability: 'desktop' | 'no-tty'): string; /** * The message for the opt-in hard failure (`requireTerminal: true`). * * Same two situations as {@link inactiveNotice}, but for callers who would * rather a profile fail loudly than quietly do nothing — a script asserting that * a terminal profile really starts, for one. * @param desktop - whether the launcher is the desktop's. * @returns the error text. */ export declare function requiredTerminalError(desktop: boolean): string;