rune run

rune run [--root] <name> [args...]

Resolves name against the nearest config, spawns it through the platform shell, waits, and exits with the child's code.

ArgumentMeaning
--rootResolve against the root config, ignoring anything a package narrowed
<name>The script name. Required
args...Everything after the name is appended to the resolved command

Arguments for the command

rune run test --watch --reporter=verbatim

The script name is the boundary. Rune's own options come before it — the rule npm run already uses — and everything after it belongs to the command.

Each element is quoted for the shell that will receive it, so an argument containing a space arrives as one argument. This matches npm run test -- --watch, and the parity is covered by a test suite that compares the two byte for byte.

From a package.json script

This is the layout to write, and it needs no separator:

{ "scripts": { "test": "rune run test" } }

npm test -- --watch and pnpm test -- --watch both reach the child. The package manager implements its -- by appending --watch to the command string, so the separator is spent on the append and never reaches Rune. Nothing needs it to.

Typing the separator yourself

-- still works and still means what it always meant:

rune run test -- --watch       # the same as rune run test --watch
rune run build --              # appends nothing
rune run build -- --root       # passes --root to the command

The last line is the one it is still needed for: a value that would otherwise read as one of Rune's own options.

An option Rune knows, after the name

$ rune run build --root
warning: `--root` went to the command, not to rune

rune's own options come before the script name:
  rune run --root build

The command still receives --root and resolution is unaffected. This is the one case the positional rule can surprise someone, so it is never silent.

How to call Rune on Windows

pnpm exec rune, a package.json script, and Git Bash all hand your arguments to Rune unchanged.

Running node_modules\.bin\rune.CMD by hand does not. That file is a batch shim the package manager generated, and cmd.exe re-parses everything through it, so &, ^, |, <, >, (, ), % and ! inside an argument change meaning before Rune starts. This is true of every tool installed from npm, not of Rune alone.

PowerShell removes a bare -- from the arguments it forwards. Nothing here depends on the separator surviving, so the PowerShell path works — but it is why typing -- is not a way to make an invocation safer there.

Arguments Rune passes to a tool

The same trap sits on the other side, and Rune closes it. On Windows nearly every tool in node_modules/.bin is a batch file, and a batch file re-reads its own arguments through %* after cmd.exe has already read the line. An argument escaped once for cmd.exe arrives at the tool with its & acting as a command separator, which turns a filename into two commands:

rune run lint "zz-a&cd.ts"

When the command resolves to a .cmd or .bat file, Rune escapes each argument for both readers, so that filename reaches the tool whole. The decision is made from the file PATH resolution chose, not from the text of the command, because nobody writes the .cmd extension. A command that starts with an operator or a variable expansion is left exactly as it is.

The limit, stated rather than left to be found: a batch file that calls another batch file adds a third reader and is not covered. No runner in this ecosystem covers it.

The terminal

A single script inherits Rune's standard input, output and error. The child holds the real terminal, so colour detection, progress bars, watch-mode redraws and interactive prompts behave as they would without Rune in front of them. Rune copies no bytes.

Parallel groups are the exception. Their members are piped so their output can be prefixed. See Prefixed output.

Ctrl+C

The interrupt reaches the child directly, because the child shares Rune's process group on POSIX and the console on Windows. Rune records the signal and keeps waiting. It never exits before the child.

The child's own outcome is what Rune reports. A watcher that handles SIGINT and exits 0 makes Rune exit 0. A process killed by the signal makes Rune exit 130.

Exit codes

rune run test; echo $?

The child's code, exactly. Tools speak through exit codes and Rune does not translate them. The full table is on Exit codes.

When the script does not exist

$ rune run biuld
no script named `biuld`

did you mean `build`?

scripts defined here:
  build
  lint
  test

The suggestion appears when a defined name is within a Levenshtein distance of 2. The full list is printed either way, because a suggestion alone hides the config from someone seeing it for the first time.

When there is no config

$ rune run test
no rune.config.ts found

searched upward from /tmp/scratch and reached the top of the filesystem without finding a repository.

change into your project and run this again, or start a new one with:

  rune init

Running a group

name may be a serial or parallel group. A group has no command of its own, so it takes no arguments:

$ rune run ci --watch
`ci` runs other scripts, so it has no command for these arguments to join

arguments go to one command. Name the member that needs them:
  rune run <member> ...

See Groups.

The shell

PlatformShellInvocation
Windowscmd.execmd /d /s /c "<command>", arguments passed verbatim
macOS, Linux/bin/shsh -c "<command>"

npm_config_script_shell overrides both, and it arrives in the environment automatically when Rune is called from a package-manager script. A PowerShell value is invoked with -NoProfile -Command.

Going through a shell is what makes &&, pipes and redirection work without Rune parsing any of it, and it is what npm does, so a command string that worked in package.json keeps working.