Skip to content

Runtime Support ​

dreamcli runs on Node.js, Bun, and Deno without code changes. A thin RuntimeAdapter interface abstracts the platform-specific edges.

Supported Runtimes ​

RuntimeStatusPackage
Node.js >= 22.22.2Supported@kjanat/dreamcli (npm)
Bun >= 1.3Supported@kjanat/dreamcli (npm)
Deno >= 2.6.0Supported@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 arguments
  • env — environment variables
  • cwd — current working directory
  • stdin — line reader for interactive prompts
  • readStdin — full piped stdin reader for .stdin() flags and arguments
  • exit — process exit
  • isTTY — terminal detection
  • stdinIsTTY — interactive stdin detection
  • getTerminalSize — current stdout columns and rows when available
  • onTerminalResize — terminal resize subscription when supported
  • readFile / 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 ​

ts
import {
  createAdapter,
  createNodeAdapter,
} from '@kjanat/dreamcli/runtime';

const adapter = createAdapter(); // auto-detect
const nodeAdapter = createNodeAdapter(); // explicit

Deno 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:

bash
deno run --allow-read --allow-env=WASM_PATH mycli.ts deploy

Without 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:

text
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:

ts
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? ​

Released under the MIT License.