Skip to main content

Packages

A package is the unit Rune installs with pkg install: a config overlay that wires something into Rune, plus whatever that thing needs to run. It is the shared delivery format underneath both extensions, which communicate with Rune through the SDK, and plugins, which wrap command-line tools.

Distributing from a git repository

You distribute your own extension by pushing it to a public git repository and telling users to install it by its repository ID, which is the repository's host and path:

pkg install github.com/<owner>/<repo>

The ID works for any git host Rune can clone over HTTPS, so GitHub, GitLab, Bitbucket, and self-hosted servers all install the same way. The host has to carry a dot, and the path needs at least two segments, which also covers nested groups:

pkg install gitlab.com/<group>/<subgroup>/<repo>
pkg install git.example.com/<owner>/<repo>

Rune clones the repository, reads a config.yaml at its root as the config overlay, installs anything the overlay requires, and runs your extension from source through the matching language toolchain. Because it runs from source, one repository serves every platform, and there is nothing to build or host. The repository must be public: Rune clones anonymously over HTTPS.

What the repository must contain

A git package is a normal repository with two things at its root:

  1. A config.yaml overlay that declares at least one extension under extensions.<id> (see Registering an extension). Every extension's path must point at a source entrypoint the runner can start: a main.go, a .py file, or a .rs file (see source-file extensions). Point path back into the checkout with $RUNE_DATADIR/lib/$RUNE_PKG_ID, which resolves to your repository's files on disk.
  2. The extension's sources and their project manifest, so the toolchain can build and run them: go.mod (and go.sum) for Go, pyproject.toml for Python, Cargo.toml for Rust.

A requirements: list naming the language package (python, go, or rust) belongs in the overlay so pkg install provisions the toolchain first. Rune verifies all of this while cloning and aborts the install with a descriptive error before writing anything if the config.yaml is missing, declares no extensions, or points an entrypoint at something the runner cannot start.

Example: a Go extension repository

A minimal Go extension repository:

<host>/<owner>/<repo>
.
├── go.mod # module and its dependencies
├── go.sum # pinned dependency checksums
├── main.go # entry point
└── config.yaml
config.yaml
requirements:
- go
extensions:
<extension_id>:
path: '$RUNE_DATADIR/lib/$RUNE_PKG_ID/main.go'
go.mod
module github.com/<owner>/<repo>

go 1.25

require github.com/unstablebuild/rune-go-sdk v0.0.107

On install, the go requirement provides the toolchain, and Rune runs main.go with go run against the checked-out module. Commit go.sum: Rune runs the module in read-only module mode, so every dependency must already be pinned there. See the Go SDK guide for writing the extension itself, and the Python guide for the equivalent layout in that language.

Versions: latest, tags, and commits

A git package's version is a point in the repository's history. Users select it as the optional third argument to pkg install:

pkg install <host>/<owner>/<repo> <version>Installs
(no version) or latestThe tip of the default branch. Recorded as the short commit SHA, so a later commit shows up as an update.
a tag (for example v1.2.0)That tag's commit, pinned.
a commit SHAThat exact commit, pinned.

Tag your releases and users can pin to them; pkg update-check and pkg update-all compare against the default branch's tip, so an install that tracks latest picks up new commits automatically. You do not publish or register anything: pushing a commit or tag is the release.

What pkg install does

Packages are installed from the Rune console with pkg install <name>. When a user installs one, Rune:

  1. Installs required packages first. If the package's config declares a top-level requirements: list, Rune resolves each named package to its latest version and installs any that are not already installed, config merge and environment included, before the package itself. If a requirement cannot be installed, the whole install is aborted.
  2. Puts executables on the PATH. Every executable the package ships is copied into Rune's shared binary directory, which is always first on the PATH of Rune's terminals and of anything Rune launches. The tools are available immediately, with no separate step.
  3. Merges the config overlay. The package's config is deep-merged into the user's config: keys the package introduces are added, including new leaves under a map the user already has. A package only needs to carry the settings that connect it to Rune, and it will not disturb the rest of the user's configuration. A value the user has already customized is left as is, so the package never clobbers a user setting silently. The one exception is a value the package derives from $RUNE_PKG_VERSION: when that changes across versions, Rune shows the user the diff and asks before applying it. The requirements key itself is package metadata, not config: it is stripped from the merge and never appears in the user's config.

This is the same on a git install and a first-party archive install: the difference is only where the files come from. A git-hosted extension usually has no executables of its own (step 2 is a no-op for it); its toolchain comes from a required language package. The layout the install produces on disk is described under the on-disk package format.

The shared binary directory is always on PATH

The overlay does not need a PATH entry for a bundled executable. Rune places its shared binary directory first on the PATH at startup, so a bundled binary is reachable as soon as it is installed. Only set gui.env in the overlay when a tool needs extra environment variables of its own, not to expose the binary.

The config overlay

Every package carries a config.yaml (or config.star) at its root: the config overlay. In a git package it is the repository's config.yaml; in a first-party archive it is the file at the top of the tarball. Either way, Rune deep-merges it into the user's config at install time (as described above) and it is where a package wires itself into Rune: registering an extension, requiring other packages, and pointing at its own files. If both config.yaml and config.star are present, config.yaml wins.

Referencing the install location

If a value in the overlay must point back into the installed package, use these variables. They expand to the install location when the configuration is applied:

VariableExpands to
$RUNE_DATADIRThe user's Rune data directory.
$RUNE_PKG_IDThe package's name.
$RUNE_PKG_VERSIONThe installed version.

A package's own files are reachable at $RUNE_DATADIR/lib/$RUNE_PKG_ID, which resolves to the version currently in use: the cloned repository for a git package, or the extracted archive for a first-party one. Point at those files by joining onto it. For example, an overlay can set an environment variable to a directory the package ships, or name a bundled asset by its path:

config.yaml
gui:
env:
# a directory bundled in the package, e.g. a toolchain root
<TOOL_HOME>: '$RUNE_DATADIR/lib/$RUNE_PKG_ID'
extensions:
<pkg>:
config:
# a single bundled file
asset_path: '$RUNE_DATADIR/lib/$RUNE_PKG_ID/<data-file>'

A plugin that only puts a CLI on the PATH usually does not need these variables at all, since the overlay can refer to the tool by name.

Requiring other packages

A package that depends on another Rune package declares it in a top-level requirements: list in its config overlay. Each entry is a package name, the same name users type after pkg install:

config.yaml
requirements:
- go
extensions:
<extension_id>:
path: '$RUNE_DATADIR/lib/$RUNE_PKG_ID/main.go'

At install time, each requirement is resolved to its latest version and installed in full before the declaring package, so anything the requirement contributes (executables on the PATH, environment variables from its overlay) is in place by the time the declaring package starts anything. A requirement that is already installed is left at its installed version. If a requirement fails to install, the declaring package's install is aborted.

Registering an extension

An extension package registers itself through the config overlay. Add an entry under extensions.<extension_id> with a path to its entrypoint and, optionally, a config map with defaults:

config.yaml
extensions:
<extension_id>:
path: '$RUNE_DATADIR/lib/$RUNE_PKG_ID/main.go'
config:
<setting>: <default>
<group>:
<setting>: <default>

Rune starts what path names and passes the config block to the extension as its config.Config. For a git package, path is a source entrypoint in the checkout (a main.go, a .py file, or a .rs file), which Rune runs through the required language toolchain; the example above points at the repository's main.go. A first-party archive that ships a compiled binary instead points path at that executable (for example /bin/<extension_binary>). Including useful defaults makes the extension's configurable surface visible in the user's config immediately after install, and the user can edit those values like any other config. See the Extensions guide for the SDK side of writing the extension process, and source-file extensions for how each source entrypoint is run.

Registering a tutorial

A package can also ship an in-IDE tutorial and register it from the overlay. Include the tutorial's .star file in the package and add a tutorials.<name> entry that points at it with the install-location variables:

config.yaml
tutorials:
<name>: '$RUNE_DATADIR/lib/$RUNE_PKG_ID/<tutorial>.star'

After install, the tutorial is available like any other. See the Tutorials guide for how to author one and register it additively.

The on-disk package format

Whichever way a package is delivered, Rune installs it into the same layout under the user's data directory ($RUNE_DATADIR, by default ~/.rune). You do not assemble this by hand for a git package (Rune builds it from your clone), but understanding it explains where $RUNE_DATADIR/lib/$RUNE_PKG_ID points and how bundled executables reach the PATH. In the trees below, <pkg> is the package's name and <version> is the installed version.

A first-party package is a gzipped tar archive holding up to three kinds of content:

<pkg>.tar.gz
.
├── bin/ # executables, copied onto PATH
│ ├── <tool>
│ └── <tool2>
├── lib/ # payload, kept in place
│ ├── <shared-library>
│ └── <data-file>
└── config.yaml # config overlay, merged into the user config

Installing it, or cloning a git repository, produces this layout:

~/.rune/
.
├── bin/ # package executables, copied here by base name
│ ├── <tool> # (this directory is always first on PATH)
│ └── <tool2>
├── pkg/
│ └── <pkg>/
│ └── <version>/ # the archive extracted, or the repo cloned, verbatim
│ ├── bin/
│ ├── lib/
│ └── config.yaml
├── lib/
│ └── <pkg> -> pkg/<pkg>/<version>/ # symlink to the in-use version
│ # ($RUNE_DATADIR/lib/$RUNE_PKG_ID)
└── config.yaml # user config, with the overlay merged in

Three kinds of content, three destinations:

  • Executables. Any non-hidden file with an execute bit set is copied into Rune's shared binary directory by its base name, and that directory is always first on the PATH. A bin/ directory is the convention, not a requirement, and the file can be a compiled binary or a script with a shebang. Because names are taken by base name, keep each executable's file name unique. A git-hosted extension usually ships no executables at all: it runs from source through a required language toolchain.
  • Everything else (the payload). All other files stay in place under pkg/<pkg>/<version>/, reachable through $RUNE_DATADIR/lib/$RUNE_PKG_ID. For a git package this is the whole checked-out repository, including the extension's sources; for a first-party archive it is data files, shared libraries, or a bundled toolchain.
  • The config overlay. The config.yaml at the root, merged into the user's config at install time.

Example: a first-party plugin (ripgrep)

A plugin that wraps a command-line tool is the simplest first-party package: one binary plus an overlay that points a Rune feature at it. Rune's own ripgrep (rg) plugin backs fuzzy search: the archive carries the rg binary under bin/, and the overlay wires the two commands fuzzy search reads:

config.yaml
extensions:
fuzzy_search:
config:
file:
command: rg -l ""
line:
command: rg --color never -n --no-heading --max-columns 500 ""

It refers to rg by name because the binary is already on the PATH. Everything else in the user's extensions and fuzzy_search configuration is left untouched by the merge. A user installs it by name from the console:

pkg install rg

The ripgrep package is a complete, working example of this format.

This format is internal

Third-party developers do not build or host these archives. They are how Unstable Build ships first-party packages (language runtimes, the Rune Agent, runectl, plugins like the one above), installed by a bare name. To distribute your own extension, use a git repository.

See also

  • Plugins: package a CLI so a Rune feature can use it.
  • Extensions: package a program that communicates with Rune through the SDK.
  • Console: install and manage packages with pkg.
  • Config: how Rune configuration is structured and merged.
Ask Rune Agent