Run as Command Line Interface

August 3, 2026 · View on GitHub

You have two options to install the CLI application:

  1. Global Installation: This will make the brs-cli command available system-wide.

    $ npm install -g brs-node
    
  2. Build NodeJS Package: Clone the repo, build the NodeJS package and link it to your system:

    $ git clone https://github.com/lvcabral/brs-engine.git
    $ cd brs-engine
    $ npm install
    $ npm run build:cli
    $ cd packages/node
    $ npm link
    

Keeping the CLI Up to Date

When running on an interactive terminal, the CLI checks the npm registry (at most once a day, in background) for a newer release of brs-node, and shows a notice with the command to upgrade the installation it detected (global, local or npx):

Update available: 2.3.0 -> 2.4.0
Run npm install -g brs-node@latest to update.

The check never delays the startup nor the exit: the notice is displayed from the previously cached result, while the registry request runs in background and is dropped if the app finishes first. To disable it, set either the BRS_NO_UPDATE_CHECK or the NO_UPDATE_NOTIFIER environment variable; it is also skipped when the output is redirected (not a terminal) or when CI is set.

Usage

Once installed, you can execute the brs-cli command, which operates as a REPL, runs source files or creates encrypted app packages. For a list of options run:

$ brs-cli --help
Usage: brs-cli [options] [brsFiles...]

BrightScript Simulation Engine CLI

Options:
  -a, --ascii <columns>     Enable ASCII screen mode with # of columns.
  -u, --unicode             Render ASCII screen mode using Unicode block characters.
  -i, --image [percent]     Render the screen as images on the terminal with optional width % (default: 100).
  -s, --snapshot [filename] Enable Ctrl+S to save the current screen as a PNG image.
  -c, --colors <level>      Define the console color level (0 to disable). (default: 3)
  -d, --debug               Developer mode: micro debugger on crash + resource tracking.
  -l, --log [filename]      Redirect the text output to a log file (default: brs-cli.log).
  -g, --log-level <level>   Set console log verbosity: debug, warning or error. (default: "warning")
  -z, --log-rendezvous      Trace SceneGraph cross-thread rendezvous (like Roku's logrendezvous).
  -e, --ecp                 Enable the ECP server for control simulation.
  -n, --no-sg               Disable the SceneGraph extension.
  -p, --pack <password>     The password to generate the encrypted package. (default: "")
  -o, --out <directory>     The directory to save the encrypted package file. (default: "./")
  -r, --root <directory>    The root directory from which `pkg:` paths will be resolved.
  -x, --ext-vol <path>      Path to directory or zip file from which `ext1:` will be mounted.
  -k, --deep-link <params>  Parameters to be passed to the application. (format: key=value,...)
  -y, --registry            Persist the simulated device registry on disk.
  -v, --version             output the version number
  -h, --help                output usage information

REPL

An interactive BrightScript REPL (Read-Execute-Print Loop) is available by running brs-cli with no arguments, e.g.:

$ brs-cli

BrightScript Simulation Engine CLI [v2.3.0]

type `help` to see the list of valid REPL commands.

brs> ? "Dennis Ritchie said ""Hello, World!"""
Dennis Ritchie said "Hello, World!"

Quit by pressing ^D (Control-D) or executing exit.

Any valid BrightScript expression is compiled and run live. In addition, the REPL accepts the following commands:

CommandDescription
print or ?Print a variable value or expression
var or vars [scope]Display variables and their types/values (scope is global, module or function)
loglevel [level]Show the current log level, or set it (level is debug, warning or error)
vol or volsDisplay the file system mounted volumes
mnt or mount <path>Mount the ext1: volume from a directory or zip file
umt or umountUnmount the ext1: volume
xt or extDisplay the loaded extensions
help or hintShow the REPL command list
clear or clsClear the terminal screen
exit, quit or qTerminate the REPL session

Setting the Color Level

By default the CLI will display text in ANSI Truecolor mode (level 3 below), but you can change it for the session by running:

$ brs-cli --colors 0
LevelDescription
0All colors disabled
1Basic color support (16 colors)
2256 color support
3Truecolor support (16 million colors)

Setting the Log Level

By default the CLI keeps console output at warning level, matching a Roku device out of the box. Pass --log-level debug to add the threadID in every print, and show debug-level lines (e.g. the extra action/target detail on rendezvous tracing), or --log-level error to silence warning-level lines too:

$ brs-cli --log-level debug app.zip

In the REPL, use loglevel (no argument) to show the current level, or loglevel <level> to change it for the session:

brs> loglevel
Current log level: warning
brs> loglevel debug
Log level set to: debug
LevelDescription
debugEverything: debug, warning and error lines
warningwarning and error lines only (default)
errorerror lines only

Executing files

The CLI can execute an arbitrary list of BrightScript files (.brs) as well! Simply pass the file(s) to the brs-cli executable, e.g.:

$ cat hello-world.brs
? "Dennis Ritchie said ""Hello, World!"""

$ brs-cli hello-world.brs
Dennis Ritchie said "Hello, World!"

A folder can be passed with the flag --root to mount the pkg:/ volume, and in this case, the BrightScript files path should be relative to the mounted root folder. Please be aware that this is using the host file system, so if you are running the CLI on a Linux machine the paths are case sensitive, unlike Roku (or using zip files with the engine).

If --root is passed without any file arguments, the CLI runs the folder as a full application: it loads every .brs under source/ and serves the components/ tree (including SceneGraph components) from the mounted pkg:/ volume, e.g:

$ brs-cli --root ./my-app

It is also possible to run a full BrightScript application .zip or .bpk file, e.g:

$ brs-cli ../tests/test-sandbox.zip

Notes

  • If the app has ifDraw2D screens, the app will run but nothing is displayed, unless you use one of the screen rendering options --ascii, --unicode or --image (see below).
  • The app runs on a dedicated worker thread, so you can control it interactively with the keyboard (see below) or via the --ecp option.
  • SceneGraph Task nodes run on their own worker threads, mirroring the browser engine and a real device.
  • Use the flag --registry to have the device registry data saved to the disk, and restored in following app executions.
  • Use the flag --ext-vol to mount a directory or zip archive as the ext1: volume.
  • To send parameters (deep linking) to the app, use the flag --deep-link followed by the parameters in the format: key=value,...

Showing Screen as ASCII Rendering on the Terminal

If you pass the --ascii <columns> option, the CLI will run the application and show a representation of the screen bitmap as ASCII rendering on the terminal screen. Use the optional --unicode flag to render the output using Unicode block characters for smoother gradients. If --unicode is provided without --ascii, it will calculate the columns based on the terminal width and height.

$ brs-cli ../apps/collisions.zip --ascii 170

The <columns> defines the width in number of character columns, the height will follow the screen proportion, if not provided, it will try to fit the terminal size.

Screen Rendering as ASCII Art

Showing the Screen as Images on the Terminal

The --image option renders the screen as actual images on the terminal (an alternative to --ascii/--unicode). On terminals with native graphics support (iTerm2, Kitty and compatible), frames display in full quality via the terminal's image protocol; elsewhere it falls back to ANSI half-block rendering.

$ brs-cli ../apps/collisions.zip --image

The optional [percent] argument scales the image to a percentage of the terminal width (valid range: 10-100, default: 100), keeping the screen's aspect ratio:

$ brs-cli ../apps/collisions.zip --image 60

Redirecting the Text Output to a Log File

While the screen is being rendered on the terminal (--ascii, --unicode or --image), any text the app or the engine prints is held back and only shown after the app finishes — writing text over the frames would corrupt (and on graphics terminals, flicker) the rendering. If you want to follow the output live instead, use the --log option to redirect all text output (prints, warnings, errors and diagnostics) to a file, appended as it arrives with ANSI colors stripped — so you can watch it from another terminal with tail -f:

$ brs-cli ../apps/my-app.zip --image --log my-app.log

The filename is optional (default: brs-cli.log). When the Micro Debugger is active it takes over the terminal, so its interaction stays on screen regardless of this option.

Saving the Screen as a PNG Image

The --snapshot option enables the Ctrl+S keyboard shortcut: while the app is running, pressing it saves the current screen as a PNG file, so you can capture any moment of the rendering. The filename is optional: if omitted, the image is saved in the current directory using the app file name (e.g. my-app.png); each press overwrites the previous capture.

$ brs-cli ../apps/collisions.zip --snapshot screenshot.png

This option is independent of the screen modes: you can combine it with --ascii, --unicode or --image, or use it alone.

Controlling the App

The app runs on a dedicated worker thread, leaving the terminal free for interactive control: when the CLI is attached to a terminal (TTY), the keyboard acts as the remote control while the app is running.

KeyRoku Remote
Arrow keysUp / Down / Left / Right
EnterSelect (OK)
Esc or DeleteBack
Home or F2Home (exits the app)
BackspaceInstant Replay
End or F8Play/Pause
PageUp / PageDownRewind / Fast Forward
F7 / F9Rewind / Fast Forward
Ctrl+Left / Ctrl+RightRewind / Fast Forward
Insert or F10Info
Ctrl+BackspaceBackspace (deletes a character in keyboard dialogs)
Ctrl+EnterPlay/Pause
Ctrl+A / Ctrl+ZA / B (game remote)
Letters / digitsText input (keyboard dialogs)
Ctrl+SSave a screenshot (requires --snapshot)
Ctrl+BBreak into the Micro Debugger (requires --debug)
Ctrl+CProduction mode: terminate the CLI; with --debug: break into the Micro Debugger (like a STOP)
Ctrl+DTerminate the CLI

If you need remote control simulation from other devices, enable the option --ecp that will launch the ECP Server in port 8060 (same as a Roku device). With this option enabled, you can connect to your computer using any remote control app that uses ECP, including the Roku Remote Tool, the Roku GamePad Gateway or the Roku mobile apps. This option also enables an SSDP service to allow it to be discovered in your local network.

If port 8060 is already taken — by another instance of the engine, a desktop build, or a remote control tool — set the BRS_ECP_PORT environment variable to bind elsewhere:

$ BRS_ECP_PORT=8160 brs-cli --ecp app.zip

SSDP discovery is skipped on a non-default port: remote control apps only look for ECP on 8060, so advertising a relocated server would publish an endpoint nothing can use.

Production vs Developer mode

By default the engine runs in production mode, which keeps it lean by skipping all debug instrumentation. Passing --debug (or setting debugOnCrash in the device info) switches to developer mode, which enables the Micro Debugger and the resource tracking used by its inspection commands. In developer mode you can break into the debugger at any time with Ctrl+B; while the debugger is active the keyboard switches to line mode for entering debug commands and BrightScript expressions (cont resumes the app and restores remote-control keys).

CapabilityProduction (default)Developer (--debug)
Micro Debugger (on crash, STOP, or break)disabledenabled
STOP statementexits the appopens the debugger
bscs / sgnodes / stats debug commandsemptypopulated
Crash BackTrace: outputsuppressedshown
ECP query/r2d2-bitmapsemptypopulated
ECP sgrendezvous trackingdisabledenabled
try/catch e.backtraceworksworks
Reference counting, dispose(), error messagesunchangedunchanged

This avoids the per-object/per-node bookkeeping overhead when you are just running an app. Note that try/catch exception backtraces (e.backtrace) keep working in both modes.

Encrypted packages (.bpk) always run in production mode — debugOnCrash is forced off even if you pass --debug — so a protected app cannot be inspected through the debug instrumentation.

Tracing cross-thread rendezvous

SceneGraph Task nodes run on their own worker threads, and every field read, field write, or method call that crosses a thread boundary goes through a rendezvous (see SceneGraph Rendezvous). Passing -z/--log-rendezvous — the equivalent of Roku's logrendezvous — makes each crossing print a matched pair of lines, the same shape a real device prints to its debug console:

$ brs-cli -z --log trace.log app.zip
08-02 17:34:24.021 [sg.node.BLOCK] Rendezvous[63] at pkg:/components/ProbeTask.brs(11)
08-02 17:34:24.146 [sg.node.UNBLOCK] Rendezvous[63] completed in 0.124 s

The [sg.node.BLOCK] line prints when the wait begins (with the BrightScript call site that triggered it); the matching [sg.node.UNBLOCK] line prints when it ends (with the elapsed time, omitted entirely when it rounds to zero). Rendezvous[N] is a monotonic id shared by every thread, so interleaved crossings from different Tasks can still be told apart. When the device's log verbosity (deviceInfo.logLevel) is set to LogLevel.Debug, both lines additionally show the action and target (get/set/call and the type.key accessed) — detail a real device's own console does not print.

The flag is independent of --debug and applies to the render thread and every Task worker, so both ends of a crossing appear. It is held in the shared control array rather than per thread, so a host embedding the engine can also flip it mid-run (see setRendezvousLog in the engine API).

There are also lower-level structural trace lines (unrelated to the BLOCK/UNBLOCK pair above), tracing the fan-out queue that delivers observed-field updates to a Task:

LineMeaning
thread N queued fan-out task.request -> task thread Mrender thread has work for task M
thread N flushed fan-out task.request -> task thread Mit was written into the shared buffer
thread 0 applied set node.content from thread Na task's write landed on the render thread
thread N broker fan-out …the update went through the main-thread broker

Combine it with --log to capture a full trace, since it is verbose on task-heavy apps.

Inspecting Texture Memory

With the ECP server and developer mode enabled (--ecp --debug), the CLI exposes the query/r2d2-bitmaps endpoint, mirroring a real Roku device (which likewise requires developer mode for this query). It returns, as XML, the list of bitmaps currently loaded into texture memory (width, height, bytes-per-pixel, size and name) together with the registered fonts and the system/texture memory totals. This is useful for diagnosing texture-memory pressure in 2D API apps and SceneGraph apps. In production mode the endpoint responds with an empty list.

$ curl http://localhost:8060/query/r2d2-bitmaps
<?xml version="1.0"?>
<r2d2-bitmaps>
  <timestamp>1782607141042</timestamp>
  <channel-id>dev</channel-id>
  <graphics-instances>
    <rographics>
      <sytem-memory>
        <used>0</used>
      </sytem-memory>
      <texture-memory>
        <used>1298800</used>
        <available>98701200</available>
        <max>100000000</max>
      </texture-memory>
      <bitmap>
        <width>100</width>
        <height>100</height>
        <bpp>4</bpp>
        <size>40000</size>
        <name>pkg:/images/alpha.png</name>
      </bitmap>
      <!-- ...one <bitmap> per loaded image and registered font... -->
    </rographics>
  </graphics-instances>
  <status>OK</status>
</r2d2-bitmaps>

Sizes are an approximation (width × height × bpp); on a real device the texture allocator pads them to alignment boundaries.

Tracking SceneGraph rendezvous via ECP

With the ECP server and developer mode enabled (--ecp --debug), the CLI also exposes Roku's sgrendezvous ECP commands: a queryable, queue-based tracker for cross-thread rendezvous events, independent of the -z/--log-rendezvous console trace described above. Start tracking, trigger some app activity, then read back the queued events:

$ curl -d '' http://localhost:8060/sgrendezvous/track
<?xml version="1.0"?>
<sgrendezvous>
  <tracking-enabled>true</tracking-enabled>
  <status>OK</status>
</sgrendezvous>
$ curl http://localhost:8060/query/sgrendezvous
<?xml version="1.0"?>
<sgrendezvous>
  <data>
    <tracking-enabled>true</tracking-enabled>
    <plugin-id>dev</plugin-id>
    <plugin-title>My App</plugin-title>
    <drop-count>0</drop-count>
    <count>1</count>
    <item>
      <id>63</id>
      <start-tm>195918805</start-tm>
      <end-tm>195918878</end-tm>
      <line-number>11</line-number>
      <file>pkg:/components/ProbeTask.brs</file>
    </item>
  </data>
  <timestamp>1782607142000</timestamp>
  <status>OK</status>
</sgrendezvous>

start-tm/end-tm are a monotonic clock (matching a real device's small-magnitude, non-epoch start-tm/end-tm), not Date.now(); only timestamp (the query time) is epoch milliseconds.

Each GET drains the queue: only events queued since tracking started, or since the previous GET, are returned. Up to 1,000 events are queued between calls; events beyond that cap are not kept, but counted in drop-count. Stop tracking with:

$ curl -d '' http://localhost:8060/sgrendezvous/untrack

In production mode (--ecp without --debug) the endpoint always reports tracking-enabled: false with an empty item list, mirroring a real device's developer-mode requirement for this query.

Creating an encrypted App package file

If you want to protect your BrightScript application source code, you can create an encrypted package using the CLI, using the parameters:

  1. -p, --pack <password>: The password to generate the encrypted package (32 characters long). (default: "")
  2. -o, --out <directory>: The directory to save the encrypted package file. (default: "./")

If no password is provided the app will be executed and no encryption happens, below an example of how to encrypt a package:

$ brs-cli ../tests/test-sandbox.zip --pack b4bf93d0d5e547ca8edcc0f39c6bcc16 --out ./release

BrightScript Simulation Engine CLI [v2.3.0]

Packaging ../tests/test-sandbox.zip...

Package file created as ./release/test-sandbox.bpk with 528 KB.

SceneGraph applications are fully supported: in addition to the pkg:/source/ code, every component file under pkg:/components/ (both .brs scripts and .xml definitions, including their inline <script> blocks) is encrypted into the package and removed from the distributed .bpk. The component files are restored in memory only at runtime, after the package is decrypted with the password, and are dropped again once the engine has parsed them — so a packaged app cannot read its own component source back at runtime (e.g. via ReadAsciiFile), just like the encrypted pkg:/source/ code. The empty components/ directory tree is also pruned from the package so it does not reveal the app's structure.

In addition, the entire .bpk container is encrypted with the same password (AES-256), so even the plaintext assets — images, fonts, data files, and the manifest — cannot be read without it. The performance cost is negligible (a few hundred microseconds to a few milliseconds at load, depending on package size). Plain .zip files and packages created by older versions are still opened normally and do not require a password.

To run an encrypted .bpk, provide the same password used to create it via --pack:

$ brs-cli ./release/test-sandbox.bpk --pack b4bf93d0d5e547ca8edcc0f39c6bcc16

Note: The package is only protected at rest. Anyone with the password can decrypt and run it, so keep the password private and never embed it in a public web app.