Inheritance and narrowing

Two mechanisms share one rule: a name means one command across the repository, and inheriting can only add to it.

extends

An extends script resolves another script's command and appends its own arguments.

rune.config.ts
export default defineConfig({
  scripts: {
    test: { command: 'vitest run --reporter=dot' },
    'test:coverage': { extends: 'test', appendArgs: ['--coverage'] },
    'test:ci': { extends: 'test:coverage', appendArgs: ['--reporter=junit'] },
  },
});

test:ci resolves to:

vitest run --reporter=dot --coverage --reporter=junit

Chains resolve transitively, in declaration order from the base outward. A cycle is rejected with the names that form it.

appendArgs entries are quoted per element for the shell that will run the command, the same way pass-through arguments are. An entry containing a space arrives as one argument.

appendArgs without extends is a type error, and Rune rejects it when the config loads:

script `test` sets `appendArgs` but does not extend anything

`appendArgs` adds arguments to a command a script inherits, so it only means something
next to `extends`. A script with a `command` of its own writes its arguments into it.

A group cannot be extended

extends may only name a script that has a command. A group has none of its own, so there would be nothing for the inherited arguments to join:

script `x` extends `g`, which is a group

`extends` builds on the command a script runs, and a group has none of its own — it names
the scripts it runs. To reuse a group, name it as a member of another group.

Narrowing in a package

A package may hold its own rune.config.ts. Rune uses the nearest one it finds, so a script name defined in both resolves to the package's version.

packages/legacy/rune.config.ts
export default defineConfig({
  scripts: {
    test: { extends: 'test', appendArgs: ['--maxWorkers=1'] },
  },
});

Inside packages/legacy, rune run test runs the root command plus --maxWorkers=1. Everywhere else it runs the root command. Neither package.json changed.

Extend, never replace

A package config that gives a colliding name its own command is a validation error:

packages/legacy/rune.config.ts redefines the root script `test` with `command`

a package may extend a script the root defines, never replace it:

  test: { extends: "test", appendArgs: ["--your-flag"] }

replacing it would leave this package behind the next time the root definition changes,
and nothing would say so. A genuinely different command deserves a name of its own.

The reason is what the tool is for. A test that means one command in nine packages and something unrelated in the tenth reproduces the situation Rune removes, and it does it invisibly.

A name that does not exist at the root narrows nothing. It is a new script, scoped to that package.

Ignoring what the package narrowed

--root resolves against the root config only.

rune run test --root

Seeing the chain

rune inspect prints the resolution, and it never spawns anything.

$ rune inspect test
test

command      vitest run --reporter=dot --maxWorkers=1
directory    packages/legacy

resolved through
  rune.config.ts                  test  runs `vitest run --reporter=dot`
  packages/legacy/rune.config.ts  test  appends `--maxWorkers=1`

rune list annotates the same information more briefly: (defined here) for a script this package owns, (narrowed here) for one it narrows.

The cache and two configs

The resolved config is cached by content. When a package config is in play, both configs and everything they import enter the cache key, so editing either one invalidates the entry. See Config evaluation.