CLI Mode vs Module Mode in ego-browser: How the ego-lite Runtime Works
CLI mode executes JavaScript from stdin as a standalone command-line program, while module mode installs SDK helpers onto a target object for programmatic library integration.
The ego-browser package within the citrolabs/ego-lite repository offers a dual-mode architecture that determines how browser automation code is executed. Depending on whether you invoke the binary directly or import the package as a dependency, the runtime activates distinct entry points, stdin handling mechanisms, and output strategies.
Entry Points and Execution Detection
The src/index.ts file serves as the central router that detects how the package is being consumed. It uses the isDirectCli() check to branch between two execution paths.
CLI Mode: Direct Execution
When the file is executed directly as a command-line program (indicated by #!/usr/bin/env node), the runtime enters CLI mode. The isDirectCli() function returns true, triggering the runMain() function sourced from src/run.ts.
According to the source code in [index.ts lines 56-63](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/index.ts#L56-L63), this path reads JavaScript from STDIN, wraps it in an async execution context, and sets the process exit code upon completion. This design supports fire-and-forget automation scripts run from shell environments or CI pipelines.
Module Mode: Library Import
When the package is imported via import { installEgoSdk } from "ego-browser", the runtime falls back to the module branch at [index.ts lines 64-66](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/index.ts#L64-L66). Instead of launching the CLI runner, it calls installEgoSdk(), which mounts the SDK helpers onto a target object—by default globalThis—without processing stdin or managing process lifecycles.
How Code is Supplied and Executed
The fundamental difference between CLI mode and module mode lies in how you supply browser automation instructions.
CLI mode expects code via stdin. The runMain() function in src/run.ts consumes the entire input stream, injects the helper context built by src/helpers.ts, and executes the script within a temporary async wrapper. For example:
echo "await page.goto('https://example.com'); console.log('Done')" | npx ego-browser
Module mode treats your project as the host. After calling installEgoSdk(), the helpers become available as ordinary functions on the target object, allowing you to write standard Node.js code that drives the browser directly:
import { installEgoSdk } from "ego-browser";
// Install SDK onto globalThis (default)
installEgoSdk();
// Helpers are now available globally
await page.goto("https://example.com");
console.log("Done");
Output Handling and Lifecycle Differences
CLI mode and module mode diverge significantly in how they handle console output and process lifecycles.
In CLI mode, the runtime replaces console.log with a buffered sink to ensure logs appear after the script's output rather than interleaving with browser protocol messages. As implemented in [index.ts lines 75-82](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/index.ts#L75-L82), this buffer flushes when the process terminates. The lifecycle is short-lived: runMain() executes the stdin payload, sets process.exitCode, and terminates the Node process.
In module mode, the SDK accepts an optional cliLog function via the installEgoSdk options parameter. If not provided, it defaults to the same buffered sink, but the host application maintains full control over the logging surface. The SDK remains resident in memory, enabling repeated calls to browser helpers within the same Node runtime—ideal for test harnesses or long-running services.
Practical Code Examples
Understanding the distinction requires seeing both patterns in practice.
Running One-Off Scripts with CLI Mode
CLI mode excels at shell automation and ephemeral tasks. The src/run.ts implementation handles stdin reading and context injection automatically:
# Execute navigation via pipe
cat <<'EOF' | npx ego-browser
await page.goto('https://github.com/citrolabs/ego-lite');
const title = await page.title();
console.log(`Page title: ${title}`);
EOF
Integrating as a Node.js Library
Module mode suits library integration. The installEgoSdk() function exposes the same helpers without stdin processing:
import { installEgoSdk } from "ego-browser";
import { createServer } from "http";
// Install with custom logging target
installEgoSdk(globalThis, {
cliLog: (msg) => process.stdout.write(`[EGO] ${msg}\n`)
});
// Reusable browser automation function
export async function scrapeUrl(url) {
await page.goto(url);
return await page.content();
}
Summary
- CLI mode activates when executing the package directly, reads JavaScript from stdin via
runMain()insrc/run.ts, buffers console output, and exits the process after execution. - Module mode activates when importing the package, calls
installEgoSdk()to mount helpers ontoglobalThisor a custom target, and allows programmatic control without stdin processing. - The detection logic resides in
src/index.ts, which branches based onisDirectCli()between lines 56-66. - CLI mode suits one-off shell scripts and CI pipelines; module mode suits library authors and test frameworks requiring persistent browser sessions.
Frequently Asked Questions
Can I use ego-browser in both modes simultaneously within the same process?
No. The entry point in src/index.ts executes an exclusive branch: if isDirectCli() returns true, it immediately invokes runMain() and never exports the module interface. Conversely, importing the package as a module skips the CLI runner entirely. You must choose one activation pattern per Node process.
How does ego-browser detect whether to run in CLI or module mode?
The runtime checks if the file is being executed directly using isDirectCli(). When the shebang (#!/usr/bin/env node) triggers direct execution, the condition evaluates true and the code path calls runMain(). When imported via require() or import, the condition fails, falling through to installEgoSdk() as shown in [index.ts lines 64-66](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/index.ts#L64-L66).
What happens to console.log output in CLI mode vs module mode?
In CLI mode, console.log is replaced with a buffered sink (implemented in [index.ts lines 75-82](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/index.ts#L75-L82)) that flushes upon process exit to prevent log interleaving. In module mode, logging defaults to the same buffered behavior unless you override it by passing a custom cliLog function to installEgoSdk(), giving you explicit control over the output destination.
Is there a performance difference between CLI mode and module mode?
Yes. CLI mode incurs the overhead of process startup and stdin stream processing for each execution, making it suitable for discrete tasks. Module mode maintains the SDK in-process, eliminating startup overhead for repeated automation calls and allowing connection reuse, which significantly improves performance in iterative scenarios such as test suites or web scraping applications.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →