import { WriteFn } from "../core/output/writer.mjs"; import "../core/output/index.mjs"; import { ReadFn } from "../core/prompt/index.mjs"; //#region src/runtime/adapter.d.ts /** * Runtime adapter interface. * * Defines the minimal contract between the platform-agnostic core and * the host runtime (Node.js, Bun, Deno). Every platform-dependent * operation flows through this interface — the core never calls * `process.*`, `Deno.*`, or `Bun.*` directly. * * Adapters are designed to be: * - **Immutable in shape:** all properties are readonly * - **Minimal:** only the operations the framework actually needs * - **Testable:** easily stubbed in tests via {@linkcode createTestAdapter | createTestAdapter()} * * @example * ```ts * // Production: auto-detected * cli('mycli').run(); // uses Node/Bun/Deno adapter * * // Test: explicit adapter * cli('mycli').run({ adapter: createTestAdapter({ argv: ['deploy'] }) }); * ``` */ interface RuntimeAdapter { /** Raw argv array (including binary + script path, e.g. `['node', 'cli.js', 'deploy']`). */ readonly argv: readonly string[]; /** * Environment variables. * Values are `string | undefined` — mirrors Node's `process.env` semantics. */ readonly env: Readonly>; /** Current working directory (absolute path). */ readonly cwd: string; /** Writer for stdout. Framework routes `out.log`/`out.info` through this. */ readonly stdout: WriteFn; /** Writer for stderr. Framework routes `out.warn`/`out.error` through this. */ readonly stderr: WriteFn; /** * Line reader for stdin. Used by the prompt engine for interactive input. * * Returns `null` on EOF (Ctrl+D on Unix, Ctrl+Z on Windows), * indicating the user closed the input stream (treated as cancel). */ readonly stdin: ReadFn; /** * Read all of stdin as a single string (for piped data). * * Returns the full stdin contents when data is piped (`stdinIsTTY` is * false), or `null` when stdin is a TTY (no piped data available). * * Used by the resolve chain for args with `.stdin()` configured. * Unlike {@link stdin} (which reads one line for prompts), this * consumes the entire stream to EOF. * * @returns Full stdin contents as a string, or `null` if stdin is a TTY. */ readonly readStdin: () => Promise; /** Whether stdout is connected to a TTY. */ readonly isTTY: boolean; /** Whether stdin is connected to a TTY (used for prompt gating). */ readonly stdinIsTTY: boolean; /** Read current terminal dimensions for stdout, if available. */ readonly getTerminalSize: () => TerminalSize | undefined; /** Subscribe to terminal resize events when the runtime supports them. */ readonly onTerminalResize: (listener: () => void) => (() => void) | undefined; /** * Exit the process with the given code. * Must not return (divergent function). */ readonly exit: (code: number) => never; /** * Read a file as UTF-8 text. * * Returns file contents on success, `null` if the file does not exist * (ENOENT/NotFound). Throws on other I/O errors (permission denied, * is-directory, etc.) — those indicate unexpected failures, not * "try the next path". * * Used by config file discovery to probe multiple candidate paths. */ readonly readFile: (path: string) => Promise; /** * Probe what exists at a filesystem path. * * Returns `'file'` or `'directory'` for existing paths (other kinds such * as sockets report `'file'`), or `null` when nothing exists there. * * Used by `flag.path()` existence/type checks after resolution. */ readonly stat: (path: string) => Promise<'file' | 'directory' | null>; /** * Create a directory, including missing parents. * * Succeeds when the directory already exists. Throws on other I/O errors * (permission denied, existing file at the path, etc.). * * Used by `flag.path()` `create` checks after resolution. */ readonly mkdir: (path: string) => Promise; /** * User home directory (absolute path). * * - Node/Bun: derived from `HOME` / `USERPROFILE` env * - Deno: `Deno.env.get('HOME')` / `Deno.env.get('USERPROFILE')` */ readonly homedir: string; /** * Platform-specific user configuration directory (absolute path). * * - Unix: `$XDG_CONFIG_HOME` or `~/.config` * - Windows: `%APPDATA%` or `~\AppData\Roaming` * * Config discovery appends the app-specific subdirectory. */ readonly configDir: string; /** * User-scope config roots for discovery, highest priority first. * * - Unix: `[configDir]` * - macOS: `[configDir, ~/Library/Application Support]` * - Windows: `[configDir]` * * When absent, discovery falls back to `[configDir]`. */ readonly userConfigDirs?: readonly string[]; /** * System-scope config roots for discovery (`['/etc']` on Linux and * macOS, `[]` on Windows). When absent, discovery probes none. */ readonly systemConfigDirs?: readonly string[]; } /** Current terminal dimensions in columns and rows. */ interface TerminalSize { /** Terminal width in columns. */ readonly columns: number; /** Terminal height in rows. */ readonly rows: number; } /** * Options for creating a test adapter. * * All fields are optional — sensible defaults are applied for testing * scenarios (empty argv, empty env, noop stdout/stderr, non-TTY, exit * throws instead of killing the process). */ interface TestAdapterOptions { /** Raw argv (defaults to `['node', 'test']`). */ readonly argv?: readonly string[]; /** Environment variables (defaults to `{}`). */ readonly env?: Readonly>; /** Working directory (defaults to `'/test'`). */ readonly cwd?: string; /** Stdout writer (defaults to noop). */ readonly stdout?: WriteFn; /** Stderr writer (defaults to noop). */ readonly stderr?: WriteFn; /** * Stdin line reader (defaults to returning `null` — immediate EOF). * * Use a custom {@linkcode ReadFn} to simulate user input in tests. */ readonly stdin?: ReadFn; /** * Piped stdin data for testing args with `.stdin()` configured. * * When provided and `stdinIsTTY` is `false`, `readStdin()` returns this * string once, then `null` on subsequent reads. When absent, or when * `stdinIsTTY` is `true`, `readStdin()` returns `null`. * * @example * ```ts * createTestAdapter({ * stdinData: '{"key": "value"}', * }) * ``` */ readonly stdinData?: string; /** TTY flag for stdout (defaults to `false`). */ readonly isTTY?: boolean; /** TTY flag for stdin (defaults to `false`). */ readonly stdinIsTTY?: boolean; /** Terminal dimensions returned by `getTerminalSize()`. */ readonly terminalSize?: TerminalSize; /** Resize subscription hook returned by `onTerminalResize()`. */ readonly onTerminalResize?: (listener: () => void) => (() => void) | undefined; /** * Exit function (defaults to throwing {@linkcode ExitError}). * The default throw-based exit allows tests to catch the exit code. */ readonly exit?: (code: number) => never; /** * File reader stub (defaults to returning `null` — all files not found). * * Supply a custom function to simulate a virtual filesystem in tests: * ```ts * createTestAdapter({ * readFile: (path) => Promise.resolve( * path === '/home/test/.config/myapp/config.json' * ? '{"region":"eu"}' * : null * ), * }) * ``` */ readonly readFile?: (path: string) => Promise; /** * Filesystem probe stub for `flag.path()` checks (defaults to returning * `null` — nothing exists). * * Supply a custom function to simulate a virtual filesystem in tests: * ```ts * createTestAdapter({ * stat: (path) => Promise.resolve(path === '/data' ? 'directory' : null), * }) * ``` */ readonly stat?: (path: string) => Promise<'file' | 'directory' | null>; /** * Directory creation stub for `flag.path()` `create` checks (defaults to * a noop that resolves without creating anything). */ readonly mkdir?: (path: string) => Promise; /** Home directory (defaults to `'/home/test'`). */ readonly homedir?: string; /** Config directory (defaults to `'/home/test/.config'`). */ readonly configDir?: string; /** User-scope config roots for discovery (defaults to `[configDir]`). */ readonly userConfigDirs?: readonly string[]; /** System-scope config roots for discovery (defaults to `[]`). */ readonly systemConfigDirs?: readonly string[]; } /** * Error thrown by adapter implementations that model process exit as an * exception instead of terminating immediately. * * Runtime and CLI dispatch layers can catch this to perform real process * termination, while tests can assert on exit codes without killing the * runner: * * ```ts * try { * await cli.run({ adapter: createTestAdapter() }); * } catch (e) { * if (e instanceof ExitError) expect(e.code).toBe(0); * } * ``` */ declare class ExitError extends Error { /** Error name — always `'ExitError'` for `instanceof` checks. */ readonly name = "ExitError"; /** The exit code passed to `exit()`. */ readonly code: number; /** Create an ExitError for the given process exit code. */ constructor(code: number); } /** * Create a test runtime adapter with injectable process state. * * Use this in `@kjanat/dreamcli/testkit` tests when you need to simulate argv, * environment variables, TTY state, stdin, or config-file reads without * touching the host process. * * @param options - Optional overrides for any adapter field. * @returns A {@linkcode RuntimeAdapter} suitable for test scenarios. * * @example * ```ts * const adapter = createTestAdapter({ * argv: ['node', 'cli.js', 'deploy', '--force'], * env: { DEPLOY_REGION: 'us' }, * }); * * const result = await cli('mycli').run({ adapter }); * ``` */ declare function createTestAdapter(options?: TestAdapterOptions): RuntimeAdapter; //#endregion export { ExitError, type RuntimeAdapter, type TerminalSize, type TestAdapterOptions, createTestAdapter };