Runtime Support
dreamcli runs on Node.js, Bun, and Deno without code changes. A thin RuntimeAdapter interface abstracts the platform-specific edges.
Supported Runtimes
| Runtime | Status | Package |
|---|---|---|
| Node.js >= 22.22.2 | Supported | @kjanat/dreamcli (npm) |
| Bun >= 1.3 | Supported | @kjanat/dreamcli (npm) |
| Deno >= 2.6.0 | Supported | @kjanat/dreamcli (JSR) |
These minimums are the tested support floor, declared in the package engines field. As a dependency, your package manager enforces them at install time; DreamCLI does not hard-fail the host CLI at runtime when the version is lower.
How It Works
The core framework never imports platform-specific APIs directly. Instead, a RuntimeAdapter provides:
argv— command-line argumentsenv— environment variablescwd— current working directorystdin— line reader for interactive promptsreadStdin— full piped stdin reader for.stdin()flags and argumentsexit— process exitisTTY— terminal detectionstdinIsTTY— interactive stdin detectiongetTerminalSize— current stdout columns and rows when availableonTerminalResize— terminal resize subscription when supportedreadFile/stat/homedir/configDir— filesystem access
Runtime detection is automatic — dreamcli picks the right adapter at startup.
Terminal Width
When cli().run() renders help, DreamCLI uses the runtime adapter's terminal width if stdout is a TTY and no explicit help width is configured. Direct formatHelp() calls and cli().execute() keep the deterministic 80-column fallback.
Node and Bun use process.stdout.getWindowSize() when available, falling back to process.stdout.columns / rows. Deno uses Deno.consoleSize(). Resize subscriptions use stdout resize on Node/Bun and SIGWINCH on Deno Unix platforms.
Set a fixed width with .help({ width }) or runtime help: { width } when tests or generated output need stable wrapping.
Explicit Adapter
import {
createAdapter,
createNodeAdapter,
} from '@kjanat/dreamcli/runtime';
const adapter = createAdapter(); // auto-detect
const nodeAdapter = createNodeAdapter(); // explicitDeno Permissions
On Deno, the adapter asks for nothing it was not granted. A full --allow-env grant snapshots the whole environment. A scoped grant reads only the variables it names, so a command bound to .env('WASM_PATH') needs no more than that variable:
deno run --allow-read --allow-env=WASM_PATH mycli.ts deployWithout any env grant the adapter still starts, and every variable reads as unset, so .env() inputs fall through to config, prompts, and defaults. The adapter checks permission state before each read, so it never triggers a Deno permission prompt. When a required input is missing and its variable was not granted, the error names the grant to add, never the value:
Missing required flag --wasm-path
Suggestion: Provide --wasm-path <value> or set WASM_PATH (Deno has not granted access to WASM_PATH; run with --allow-env=WASM_PATH)Grant the variables your CLI reads for its own purposes too, such as HOME and XDG_CONFIG_HOME for config discovery, and NO_COLOR, FORCE_COLOR, or NO_HYPERLINKS if users should be able to steer output styling. --allow-read covers config files and path checks; without it, config discovery finds nothing.
Testing Runtime Seams
For command behavior tests, runCommand() is process-free and injects runtime state directly:
import { runCommand } from '@kjanat/dreamcli/testkit';
const result = await runCommand(regionCmd, [], {
env: { MY_REGION: 'test' },
});When you need adapter-level control (argv, filesystem reads, exit behavior), use createTestAdapter() with cli().run({ adapter }).
What's Next?
- Testing — in-process test harness
- Getting Started — installation per runtime