rune init

rune init [--from-package-json]

Writes rune.config.ts in the current directory and reports the path it wrote on standard error. It refuses to overwrite an existing file and exits 1 when one is there.

A fresh config

rune init
rune.config.ts
// Scripts for this repository. Every package runs them with `rune run <name>`, so a
// shared command changes here once instead of in every package.json.
//
// A script runs a `command` of its own, `extends` another one and adds to it, or runs
// several others in `serial`:
//
//   build:    { command: "tsc -b" }
//   build:ci: { extends: "build", appendArgs: ["--force"] }
//   ci:       { serial: ["lint", "build:ci", "test"] }
//
// A group stops at the first member that fails and exits with that member's code; add
// `continueOnError: true` to run the rest anyway. `dependsOn` puts other scripts before
// a command of its own:
//
//   build: { command: "tsc -b", dependsOn: ["clean"] }
//
// A `command` may instead name one per operating system, with a `default` for the rest:
//
//   clean: { command: { default: "rm -rf dist", win32: "rmdir /s /q dist" } }
//
// Any script may add `description`, `cwd`, `env` and `envFile`.
//
// This file is TypeScript: variables, template strings and relative imports all work.
// Node globals are not available. To branch on the machine or the environment, import
// what rune supplies:
//
//   import { rune } from "@gio-labs/rune";
//
//   test: { command: rune.isCI ? "vitest --run" : "vitest" }
//
// That gives `rune.env`, `rune.platform` and `rune.isCI`.

export default {
  scripts: {
    hello: {
      command: "echo rune is set up",
      description: "Check that rune can run a script",
    },
  },
};

The comments are half the file: they name every field the version that wrote it accepts.

The generated file loads through the normal pipeline. rune list and rune run hello both work immediately after init.

Seeding from package.json

rune init --from-package-json

Reads the nearest package.json and writes one command script per entry in its scripts field, keeping the command strings exactly as they were.

Given:

packages/api/package.json
{
  "scripts": {
    "build": "tsc --build",
    "test:ci": "vitest run --coverage",
    "dev": "tsx watch src/main.ts"
  }
}

The result, under a header that opens differently — it says the scripts came from package.json one for one, and points at extends for the ones that repeat — and then carries the same field guide:

rune.config.ts
export default {
  scripts: {
    build: {
      command: "tsc --build",
    },
    dev: {
      command: "tsx watch src/main.ts",
    },
    "test:ci": {
      command: "vitest run --coverage",
    },
  },
};

Scripts are written sorted by name, and a name that is not an identifier is quoted. Descriptions are left out, because package.json has nowhere to put one.

The translation is mechanical on purpose. Rune does not try to spot commands that repeat across packages and fold them into extends chains: guessing which duplicates were intentional would leave you auditing the result line by line. Rune puts everything in one file; the factoring is yours.

Move the file to the repository root and delete the entries that belong to one package only. What remains is the shared set. Adopting Rune in a monorepo covers the rest.

Exit codes

SituationCode
File written0
rune.config.ts already exists1, and nothing is written
--from-package-json with no package.json above the current directory1