/** * What a question does while nobody is attached to answer it. * * A dropped SSH link leaves the question queued in memory, and the only trace of * it used to be a transcript line written *after* the user reconnected — which is * how a turn spent waiting for an answer went unnoticed until then. Two signals * exist before that: * * - a marker file under `$DSH_HOME/tui-socks/`, so logging back into the jump * host shows a session waiting on an answer before the TUI is even open; * - one command the user configured, run once, carrying the question in its * environment. Mail and WeCom are presets of that command rather than clients * of their own: a jump host has whichever of them it already has, and neither * belongs in this plugin's dependency tree. * @module dsh-ssh-tui/question-wait */ import { spawn } from 'node:child_process'; /** One question waiting on a person who is not attached. */ export interface WaitingQuestion { /** The session the question belongs to. */ sessionId: string; /** How many questions are waiting, this one included. */ count: number; /** The question text, already clipped. */ question: string; /** When the wait started, so a later reader can say how long it has been. */ since: number; } /** * The marker for one session. * * Beside the session's stderr log, under the same digested label, so a long id * stays inside a Windows path and two ids that sanitize to the same stem do not * share a file. `.waiting` rather than a dotfile: the point is that `ls` shows it. * @param sessionId - the session that is waiting. * @param dshHome - the harness home; defaults to the process one. * @returns the marker path. */ export declare function waitingMarkerPath(sessionId: string, dshHome?: string): string; /** * Write the marker, replacing one already there. * * The question text is the only sensitive part, so the file is owner-only. A * failure to write is reported as `false` and never thrown: the question is * still queued, and a marker is not worth failing it over. * * Deliberately a plain `writeFile`, not a staged sibling renamed into place. * The rename is atomic on POSIX, but Windows refuses to replace a file another * process has open — and the whole point of this marker is that something else * polls it (a jump-host `cat` in a loop), so the "atomic" version stops updating * exactly while it is being read. The cost of the plain write is a window * between the create and the first byte where a reader sees an empty file; the * marker is rewritten on every change, so that window is what a consumer has to * tolerate rather than something the writer can hide. * @param question - the question that just started waiting. * @param dshHome - the harness home; defaults to the process one. * @returns whether the marker is on disk. */ export declare function writeWaitingMarker(question: WaitingQuestion, dshHome?: string): Promise; /** * Remove the marker. Missing is fine: the question may have been answered, or * the marker may never have been written. * @param sessionId - the session whose wait ended. * @param dshHome - the harness home; defaults to the process one. */ export declare function clearWaitingMarker(sessionId: string, dshHome?: string): Promise; /** * The command to run when a question starts waiting, if the user configured one. * * The environment wins over settings, the same order `/disconnect` uses, so a * one-off invocation can point somewhere else without editing the profile. * Empty and whitespace are "not configured". * @param env - the process environment. * @param saved - the `ssh-tui.notify` value, when settings has one. * @returns the command, or undefined when notifications are off. */ export declare function notifyCommand(env?: NodeJS.ProcessEnv, saved?: string): string | undefined; /** What one notification knows about the question it is announcing. */ export interface NotifyContext { sessionId: string; /** How many questions are waiting. */ count: number; /** The question text. */ question: string; /** Milliseconds the question has already been waiting. */ waitedMs: number; /** A command that reattaches to the session. */ resumeCommand: string; /** SMTP username, present only for an authenticated server. */ smtpUser?: string; /** SMTP password, present only for an authenticated server. */ smtpPassword?: string; } /** * One notify target, as `/notify` parsed it. * * `off` clears the command. `mail` is the local mailer. `smtp` is a submission * to a server, authenticated when a password is given. `local` is the machine's * own SMTP listener, which needs no account. */ export type NotifyTarget = { kind: 'off'; } | { kind: 'mail'; address: string; } | { kind: 'smtp'; host: string; port: number; from: string; to: string; user?: string; password?: string; } | { kind: 'local'; port: number; from: string; to: string; }; /** * Parse one `/notify` argument into a target. * * Four shapes, deliberately small: * * - empty, `off`, `none` — turn it off; * - `mail you@example.com` — the local mailer; * - `smtp host[:port] from to [user [password]]` — an SMTP server, port 587 * when omitted, authenticated only when a user is given; * - `local [port] from to` — the machine's own mailer on 25, or another port. * * @param raw - everything after `/notify`. * @returns the target, or undefined when the shape is not one of those. */ export declare function parseNotifyTarget(raw: string): NotifyTarget | undefined; /** * The shell command one target runs. * * SMTP and the local mailer both go through Python's stdlib `smtplib`, which * is present on every jump host this plugin runs on (Node itself is). The * password, when there is one, is read from `DSH_TUI_NOTIFY_SMTP_PASSWORD` at * send time rather than written into the command, so it never lands in the * settings file or the process list. The message text arrives on stdin. * @param target - a parsed target other than `off`. * @returns the command `/notify` stores. */ /** * What `/notify` confirms back: the target in the user's own words, without the * password. * @param target - a parsed target other than `off`. * @returns a short label. */ export declare function notifyTargetLabel(target: Exclude): string; export declare function notifyTargetCommand(target: Exclude): string; /** * A mail command for an address, using whichever sender the host has. * * `mail` (or `mailx`) is on most jump hosts and speaks SMTP for them; no * provider, token, or library joins this plugin. The body arrives on stdin, so * the question text is never a shell argument. * @param address - where the mail goes. * @returns the command. */ export declare function mailNotifyCommand(address: string): string; /** * A WeCom group-robot command for one webhook key. * * The payload is assembled by the caller and sent with `curl`, which a jump * host already has. The key is the only secret, and it stays in the command the * user configured rather than in this repo. * @param key - the robot's webhook key. * @returns the command. */ export declare function wecomNotifyCommand(key: string): string; /** * The text a notification shows. Short on purpose: a mail subject and a WeCom * message both get truncated by their carrier, so the resume command has to fit. * @param context - the question being announced. * @returns the message, one fact per line. */ export declare function notifyMessage(context: NotifyContext): string; /** * The body a WeCom robot expects: a text message whose content is the notice. * @param context - the question being announced. * @returns JSON. */ export declare function wecomPayload(context: NotifyContext): string; /** * Run the user's notify command once. * * The message goes to stdin and the facts go to the environment, so a command * can be either `mail` (reads stdin) or anything that reads the variables. The * command runs through a shell because it is a shell command by nature — the * user wrote it as one — and the platform's own shell, so a Windows host uses * `cmd`. Nothing it prints or fails with comes back: a notification that errors * must not surface in the transcript or delay the question. * @param command - the configured command. * @param context - the question being announced. * @param deps - injectable process spawn and clock, for tests. * @returns once the command has exited, timed out, or failed to start. */ export declare function runNotify(command: string, context: NotifyContext, deps?: { spawnFn?: typeof spawn; platform?: NodeJS.Platform; timeoutMs?: number; }): Promise; /** * The shell that runs a notify command, and the flag that precedes it. * * A jump host means `sh -c`. Windows has no `sh` on PATH as a rule, so there * the command runs under `cmd /c` and the user writes it for `cmd`. * @param platform - injectable so the Windows shape is testable from POSIX. * @returns the executable and its command flag. */ export declare function notifyShell(platform?: NodeJS.Platform): { command: string; flag: string; };