rune
A script runner for JavaScript and TypeScript monorepos, written in Rust. Commands live in one typed config at the root, and every package calls them by name.
- rust
- typescript
- monorepo
- cli
- tooling
rune moves every command in a monorepo out of the scripts blocks and into a single typed rune.config.ts at the root. Packages stop carrying command strings and call scripts by name instead.
In a workspace with twenty packages, the same vitest run --coverage --reporter=dot ends up copied into twenty package.json files, drifting a flag at a time. Define it once; reference it everywhere.
The config
// rune.config.ts
import { defineConfig } from '@gio-labs/rune';
export default defineConfig({
scripts: {
build: { command: 'tsc --build', description: 'Compile every package' },
test: {
command: 'vitest run',
env: { NODE_ENV: 'test' },
dependsOn: ['build'],
},
dev: { parallel: ['dev:api', 'dev:web'] },
'dev:api': { command: 'tsx watch src/server.ts', cwd: 'packages/api' },
'dev:web': { command: 'vite', cwd: 'packages/web' },
},
});{ "scripts": { "test": "rune run test" } }Why it works this way
- The config is real TypeScript, but it never loads Node. Types are stripped and the file runs in an embedded JavaScript engine, so a warm run lands under 5 ms. A config that had to resolve
node_moduleswould cost as much as the scripts it describes.rune.platform,rune.envandrune.isCIreplaceprocess, and relative imports still work. - Per-platform commands are declared, not branched.
command: { default: 'xdg-open …', win32: 'start …', darwin: 'open …' }. - The child gets the real terminal, rune exits with the child’s code, and rune’s own diagnostics go to stderr — so stdout belongs to the script.
- Arguments survive Windows. Nearly every tool in
node_modules/.binis a batch file that re-reads its own arguments, so rune escapes for both readers when a command resolves to.cmdor.bat. - One native binary per platform, resolved through optional dependencies — nothing compiles on install and no postinstall script runs. The Linux builds are statically linked and checked in both a musl and a glibc container before release.
Rune is a script registry and a runner, not a task graph: no caching, no topological ordering. Turbo and Nx sit on top of it and keep doing that part — add rune.config.ts to their declared inputs, since the command strings they hash no longer change.
Guides and the full CLI reference live at rune.gio-labs.com.