/** * ReplSession — transport-agnostic protocol session. * * Usage: * const session = connectRepl(transport) * session.messages$.subscribe(event => ...) * session.eval('1+1') * await session.deploy(files, envVars) * session.close() */ import { type Observable } from 'rxjs'; import { type FirmwareCompatDirection } from './firmwareCompat.js'; import { type CompletionResult } from './protocol.js'; import type { Transport } from './transport.js'; export interface FirmwareAdvisory { /** 'incompatible' is only set in best-effort mode, where an out-of-range * firmware is downgraded from a hard error to a warning. */ kind: FirmwareCompatDirection | 'incompatible'; message: string; } export interface ReadyEvent { type: 'ready'; chip: string | null; id: string | null; version: string | null; advisory?: FirmwareAdvisory | null; } export interface TextEvent { type: 'log' | 'warn' | 'error' | 'info' | 'debug' | 'result' | 'eval_error'; text: string; /** Attached by the REPL state machine to `result`/`eval_error` entries so the * eval duration (from the preceding `prompt`) renders on the result line. */ timing?: string; } export interface CompletionsEvent { type: 'completions'; result: CompletionResult; } export interface PromptEvent { type: 'prompt'; timing: string; } export interface RawEvent { type: 'raw'; text: string; } export interface OkEvent { type: 'ok'; payload: Buffer; } export interface ErrEvent { type: 'err'; message: string; } export interface ChecksumResultEvent { type: 'checksum_result'; match: boolean; } export interface ConfigEntriesEvent { type: 'config_entries'; entries: EnvEntry[]; } export interface FsChunkEvent { type: 'fs_chunk'; data: Buffer; } export interface TestEvent { type: 'test'; data: Record; } export interface ManifestDoneEvent { type: 'manifest_done'; } export interface DisconnectEvent { type: 'disconnect'; error?: string; } /** Emitted by `createSupervisedSession` when the underlying serial transport * drops but a reconnect attempt is in progress. The state machine renders * a subtle "reconnecting…" line. */ export interface ReconnectingEvent { type: 'reconnecting'; } /** Emitted by `createSupervisedSession` after the reconnect handshake * succeeds (a fresh MSG_READY arrives via the new transport). The state * machine's `ready` reducer already handles the transition back to * `ready`; this event is informational. */ export interface ReconnectedEvent { type: 'reconnected'; } export type ReplEvent = ReadyEvent | TextEvent | CompletionsEvent | PromptEvent | RawEvent | OkEvent | ErrEvent | ChecksumResultEvent | ConfigEntriesEvent | FsChunkEvent | TestEvent | ManifestDoneEvent | DisconnectEvent | ReconnectingEvent | ReconnectedEvent; export interface DeployFile { path: string; data: Buffer; } export interface EnvVar { key: string; value: string; secret: boolean; } export interface DeployOptions { files: DeployFile[]; envVars?: EnvVar[]; force?: boolean; erase?: boolean; restart?: boolean; /** When true, restart the device even if the deploy short-circuits as a * no-op (no file changes, no env changes). `mikro test` needs this so a * re-run against an unchanged app still boots fresh and the supervisor * picks up the test manifest from a clean runtime. Has no effect when * `restart` is false. */ alwaysRestart?: boolean; /** Timeout in ms for waiting for device to be ready (default: 10000) */ timeout?: number; } export type DeployEvent = { type: 'connecting'; } | { type: 'checking'; file: string; index: number; total: number; } | { type: 'uploading'; file: string; index: number; total: number; } | { type: 'env_changed'; changed: string[]; removed: string[]; } | { type: 'restarting'; } | { type: 'complete'; deployed: boolean; stats: { put: number; kept: number; }; }; export interface EnvEntry { key: string; value: string; secret: boolean; } export interface ReplSession { /** Observable stream of protocol events */ messages$: Observable; /** Send an eval command */ eval(code: string): void; /** Send a directive command */ directive(name: string): void; /** Send a tab-completion request */ complete(partial: string): void; /** Send exit command */ exit(): void; /** Emits each time the device sends MSG_READY (in response to CMD_HELLO). * shareReplay(1) means late subscribers get the most recent ready event. * The device only replies to HELLO, so something must drive the * handshake (deploy/config/eraseApp do this internally; otherwise call * awaitReady$). */ ready$: Observable; /** Send CMD_HELLO repeatedly until the device responds with a fresh * MSG_READY (newer than any pre-restart cached ready). Throws on timeout * or version incompatibility. Most callers can rely on deploy/config * awaiting ready internally; this is for explicit handshake control. */ awaitReady$(timeoutMs?: number): Observable; /** High-level incremental deploy. Emits progress events, completes after final event. */ deploy(options: DeployOptions): Observable; /** Erase the deployed app on the device without re-deploying anything. * Sends ERASE + DONE + optional RESTART. Unlike deploy({files: [], erase: true}), * this leaves nothing behind: no staged dir, no .checksums stamp, no /app at all. */ eraseApp(options?: { restart?: boolean; }): Promise; /** Config operations */ config: { list(): Promise; set(key: string, value: string, secret: boolean): Promise; delete(key: string): Promise; }; /** Pull a file off the device. Streams chunks over the wire and resolves * with the concatenated body. Rejects with the device error message * (e.g. "open failed: ENOENT") when the path can't be read. */ fsGet(path: string): Promise; /** Clear the on-device log files. The device deletes log.txt + log.txt.1 * and reopens a fresh log without restarting. Resolves once acknowledged. */ logsReset(): Promise; /** Send restart command */ restart(): void; /** Tear down the session */ close(): void; } /** Thrown when a protocol command (or the initial handshake) doesn't get * a response in time. Carries the timed-out operation label so logs and * `--agent` consumers can tell *which* step stalled, while the UI footer * can render just the human summary. Callers downstream (devSession, * RenderAndExit handlers) classify connection-class failures via * `instanceof DeviceTimeoutError` rather than string-matching the * message. */ export declare class DeviceTimeoutError extends Error { readonly name = "DeviceTimeoutError"; readonly timeoutMs: number; readonly context: string | undefined; constructor(timeoutMs: number, context?: string); } export interface ConnectReplOptions { /** * Firmware-version policy for the handshake: * - 'enforce' (default): an incompatible firmware version throws * FirmwareIncompatibleError, blocking the command. * - 'best-effort': an incompatible version is downgraded to a one-time * stderr warning and the command proceeds anyway. For read-only * diagnostic commands (logs, env list) where reading a mismatched * device's state is worth attempting even if the wire protocol may * have drifted. * - 'report': never throws, never prints — just attaches the advisory * (including 'incompatible') to the ReadyEvent. For callers that probe * compatibility and render their own UI (FirmwareGate). */ compat?: 'enforce' | 'best-effort' | 'report'; } export declare function connectRepl(transport: Transport, connectOptions?: ConnectReplOptions): ReplSession; //# sourceMappingURL=session.d.ts.map