@super-repo/rune
v0.2.0
Published
Rune — runtime script orchestrator. Move package.json scripts into a shareable, sectioned config file.
Downloads
1,821
Maintainers
Readme
@super-repo/rune
Rune — runtime script orchestrator. Replace package.json#scripts with a shareable, comment-friendly config file that supports sequential commands, per-script env, and .env.<name> overlays.
Why
package.json#scripts is a flat string-only map: no comments, no grouping, no way to share a chunk of build/test commands across packages. Rune keeps that flat shape (so pnpm exec rune <name> still feels like pnpm <name>) but lets each value be a string, an array, or a rich object — and pulls descriptions straight from your source-file comments.
Install
pnpm add -D @super-repo/runeQuick start
Scaffold a config (imports your existing package.json scripts if any):
pnpm exec rune initDrop in a rune.config.ts:
export default {
scripts: {
build: 'tsc -p config/tsconfig.build.json', // Compile this package
test: 'vitest run', // Run vitest once
clean: ['rm -rf dist', 'rm -rf .nx'], // sequential, stop on first failure
publish: { // rich object form
run: 'npm publish',
description: 'Publish with provenance metadata',
env: { NPM_CONFIG_PROVENANCE: 'true' },
},
},
}Run any script:
pnpm exec rune build # → tsc -p ...
pnpm exec rune build -- --watch # forward extra args to the underlying command
pnpm exec rune list # print every script with its description
pnpm exec rune init # scaffold rune.config.ts (imports package.json#scripts)
pnpm exec rune sync # mirror rune scripts → package.json as 'rune <name>' proxiesScript value forms
Each entry in scripts may be:
| Form | Example | When to use |
| --- | --- | --- |
| string | test: 'vitest run' | Single command — drop-in for what you'd put in package.json#scripts. |
| string[] | release: ['nx version', 'nx changelog', 'nx publish'] | Sequential commands, stops on first non-zero exit. |
| object | { run, description?, cwd?, env? } | When a script needs its own cwd, env overlay, or an explicit description. run accepts string or string[]. |
Trailing // comment (or a single // comment line directly above the entry) becomes the description shown by rune list. Multi-line prose belongs in a /* … */ block — section dividers stacked above a key are intentionally ignored so they don't get pulled into the description.
{
scripts: {
// ─── build ──────────── (section divider — ignored)
// Compile every package ← this becomes build's description
build: 'nx run-many -t build',
test: 'vitest run', // Run unit tests once
},
}Env-file overlay
Pass -e <name> (or --env <name>) to load an env file before the script runs. Files resolve against the package root — the closest package.json walking up from cwd:
pnpm exec rune dev:cli -e dev # loads <packageRoot>/.env.dev
pnpm exec rune dev:cli -e prod # loads <packageRoot>/.env.prod
pnpm exec rune dev:cli -e ./custom.env # explicit pathPin custom locations via envFiles in the config:
export default {
envFiles: {
dev: '.env.dev',
prod: 'env/prod.env',
test: '/abs/path/.env.test',
},
scripts: {
'dev:cli': 'tsx src/main.ts',
},
}Layering — last write wins:
- host
process.env(real shell exports always win) - config-level
env - per-script
env(rich form) - file picked by
-e(only fills keys not yet set)
Top-level options
| Option | Type | Purpose |
| -------------- | --------------------------------- | -------------------------------------------------------------------- |
| scripts | Record<string, ScriptSpec> | Required. Flat map of script name → command(s). |
| extends | string \| string[] | Inherit + override scripts from another rune config. |
| shell | string | Shell to use (default: /bin/sh on POSIX). Set to 'bash' for arrays. |
| defaultCwd | string | Default cwd for every script. Relative paths resolve against the config file. |
| env | Record<string, string> | Env vars injected into every script run. |
| envFiles | Record<string, string> | Map --env <name> → path (relative to the package root). |
| descriptions | Record<string, string> | Override / supply descriptions when comments aren't possible (JSON configs). |
CLI
rune List all scripts (default when no subcommand)
rune list Same as above, with comment descriptions
rune <name> [args...] Run a script
rune <name> -- <raw args> Forward raw args to the underlying command
rune init Scaffold rune.config.ts (imports package.json#scripts)
rune sync Mirror rune scripts → package.json as 'rune <name>' proxies
rune --ai <request> Interpret a free-form request: pick the best
existing script, or generate one ad-hoc and run it
rune --ai-suggest <request> Same as --ai but only print the suggestion (no exec)
Options:
-c, --config <path> Path to rune.config.{ts,js,mjs,json}
-e, --env <name|path> Load .env file before running
--no-banner Skip the banner on list/init/sync output
-h, --help Show this messagerune sync keeps pnpm <name> working for everyone who hasn't installed rune yet — it writes "<name>": "rune <name>" proxies into package.json#scripts while preserving lifecycle keys (prepare, postinstall, preinstall).
Config discovery
Rune resolves its config in this order:
-c <path>if passed on the command line.rune.config.{ts,mjs,js,cjs,json}in the current working directory.- The closest
package.jsonwalking up from cwd — read itsrunefield.
The package.json fallback accepts three shapes:
// (a) Single config file
{
"rune": {
"config": "./rune.config.ts"
}
}
// (b) Array of config files — loaded in order, merged left-to-right
// (later overlays earlier; each entry may have its own `extends`)
{
"rune": {
"config": ["./shared/base.config.ts", "./local/overrides.ts"]
}
}
// (c) Inline config (with optional `extends`, resolved relative to package.json)
{
"rune": {
"extends": "./shared/base.config.ts",
"scripts": {
"lint:custom": "eslint . --fix"
}
}
}This makes rune's config sit alongside super and czar blocks in a single package.json — useful when a package wants one canonical place for tooling config.
Extending configs
Any rune config (file or package.json inline) can declare an extends field — same idea as tsconfig.json. Relative paths resolve against the config file's directory (or the package.json's directory for inline blocks).
// rune.config.ts
import { defineConfig } from '@super-repo/rune'
export default defineConfig({
extends: './shared/base.config.ts',
// or: extends: ['./shared/base.config.ts', './shared/lint-presets.ts'],
scripts: {
'lint:custom': 'eslint . --fix',
},
})Merge semantics: later configs overlay earlier ones; scripts, env, descriptions shallow-merged per key; shell / defaultCwd / etc. last-defined wins. Circular extends chains throw RuneConfigError.
AI mode
When a free-form request doesn't map to an obvious script, rune can ask Claude to either pick the best existing script or generate a new shell command on the fly:
export ANTHROPIC_API_KEY=...
rune --ai build a docker image and tag it as latest
rune --ai-suggest "lint everything and fix what you can"
rune --ai="ship the demo branch to staging"Behavior:
- Claude returns strict JSON:
matched(existing script name),generated(new shell command + suggested name), orunsure. --airuns the matched/generated command. Successful generated runs print a tip showing how to save the new command as a permanent script.--ai-suggestonly prints the proposal — no execution. Useful for discovery and CI.
The model defaults to claude-sonnet-4-6; the request is the verbatim natural-language input, and the system prompt anchors Claude to either pick an existing script or produce a portable POSIX shell command (no sudo, no destructive ops outside the project).
Use with NX
For monorepos that drive builds through NX, point each project's project.json at the shared rune config via nx:run-commands:
{
"name": "@super-repo/cli",
"targets": {
"build": {
"executor": "nx:run-commands",
"options": {
"command": "rune -c ../../config/rune.config.ts build",
"cwd": "{projectRoot}"
},
"dependsOn": ["^build", "@super-repo/rune:build"],
"outputs": ["{projectRoot}/dist"],
"cache": true
}
}
}The shared config can then expose generic build / test keys that work cwd-relative for every package, plus workspace-wide variants (build:all, test:all).
Examples
Live, runnable configs covering every feature live in examples/:
| File | Demonstrates |
| --- | --- |
| basic.config.ts | Shorthand strings + arrays. |
| expanded.config.ts | Rich object form (cwd, env, description). |
| env-files.config.ts | envFiles map plus -e <name> invocation. |
| .env.dev, .env.prod, .env.test | Sample env files referenced by the env-files example. |
pnpm exec rune -c packages/rune/examples/env-files.config.ts env:show -e devProgrammatic API
import {
loadConfig,
runScript,
findScript,
renderList,
loadEnvByName,
} from '@super-repo/rune'
const { config, path } = (await loadConfig({ cwd: process.cwd() }))!
const { spec } = findScript(config, 'build')!
const result = await runScript({ config, configPath: path, script: spec })
process.exit(result.exitCode)Full surface — including parseConfig, extractDescriptionsFromSource, findPackageRoot, and syncToPackageJson — is exported from src/index.ts.
