opi 0.13.2

Operations Interface — a project control center for your terminal
# opi

**Operations Interface** — a project control center for your terminal.

Go into any repository, type `opi`, and get a usable interface for that
project — without configuring anything first.

> **Status: `0.13.2`.** Scripts are the main thing; health, security, updates,
> clean and the commit/push/release/deploy workflows are built on top of them. npm, pnpm,
> bun and Cargo projects are covered; yarn's scripts, health and security
> audit are too — its update listing is not read yet.

![opi listing a monorepo's scripts, switching groups, searching across all of them and running one](https://raw.githubusercontent.com/casoon/opi/main/assets/opi.gif)

## What it does

Its primary job is to be a better `npm run`. The start screen shows the
project's scripts — grouped by prefix, labelled from `scripts-info`, with the
package manager detected rather than typed:

```
astro-v7-workspace  pnpm
  12 entries · 4 groups

Development
› dev              Startet die Starter-App im Dev-Modus
  dev:blog         Startet die Blog-App im Dev-Modus
Build
  build            Baut Starter- und Blog-App
  build:blog       Baut die Blog-App
Preview
  preview          Vorschau des Starter-Builds
Quality
  check            Prüft das Repo mit Biome
  check:fix        Behebt Lint- und Formatfehler
```

`Enter` runs the highlighted script. That is the shortest path, and nothing is
placed in front of it. A list taller than the terminal scrolls, and `/` filters
it as you type — in a monorepo that is the difference between scrolling past
thirty entries and typing three letters.

Past **15 entries or 5 groups** the groups move into a row of tabs and only the
active one's entries are listed, because a heading per group over thirty-odd
scripts is taller than the terminal:

```
casoon-web  pnpm
  43 entries · 10 groups · 3 packages

  1 Development   2 Build   3 Preview   4 Quality   …   │   8 Packages
  ─────────────
› dev              Startet die Homepage
  dev:homepage     Startet die Homepage
  dev:insights     Startet Insights
  dev:webcheck     Startet Webcheck

←→ group   / search   H Health   C Clean   S Security   U Updates
```

A workspace's packages share the last tab rather than taking one each — 52 of
them against 6 action groups is a tab row nobody can use — and a divider marks
it, because the other tabs name what you are doing and that one names where.

The first group is preselected and the cursor is on its first entry, so `opi ↵`
still starts `dev` — the tabs hide what is not needed rather than asking for a
choice first. `←` `→`, `Tab` and the digits switch groups; `/` searches **all**
of them, which is the case the tabs are worst at. `opi | cat` lists every group
as before: nothing on the other end of a pipe can press a key.

Three speeds, all backed by the same task model:

```bash
opi                   # navigate: ↓ ↓ Enter
opi                   # filter:   /dep ↵
opi dev               # direct, no interface
```

Arguments after the name go to the script: `opi build --verbose`.

In a monorepo each workspace member's scripts appear under the member's name —
only those the root does not already define, since the root's script usually
wraps them and wins the name anyway. Where both exist, a bare name runs the
root's and `blog/dev` runs the member's:

```bash
opi dev          # the root's dev
opi blog/dev     # the blog package's dev
```

![opi dev starting the dev server directly, then opi api/db:migrate running a workspace member's script by path](https://raw.githubusercontent.com/casoon/opi/main/assets/direct.svg)

`opi` also works from anywhere inside a project, not only from the directory
holding `package.json`.

The script replaces the `opi` process, so `Ctrl-C` reaches your dev server
rather than killing a wrapper, and the script's exit code is what your shell
sees — `opi build && …` works.

## More than one kind of project

![opi listing a repository that carries both package.json and Cargo.toml](https://raw.githubusercontent.com/casoon/opi/main/assets/ecosystems.gif)

A repository can be several things at once, and `opi` does not make it choose.
`package.json` and `Cargo.toml` are both read, and their commands share one
list:

```
llmux-dashboard  npm · cargo

Development
  cargo run     Build and run the binary
  dev
Build
  build
  cargo build   Build in release mode
  cargo check   Type-check without building
```

Every area follows: `cargo fmt --check`, `cargo clippy`, `cargo rustdoc` and
`cargo test` run alongside the npm checks, `cargo audit` and `cargo outdated`
answer for the crates where they are installed, and `target/` is listed with
the build artefacts. A repository carrying both manifests gets a section each
rather than one merged list — `Dependencies` and `Dependencies (rust)` —
because "safe to take" orders within an ecosystem and not across two.

Both are external cargo subcommands rather than part of the toolchain, so
where one is missing `opi` says so along with the `cargo install` that adds
it, instead of leaving a section that reads like "nothing found".

`opi --security` also runs `osv-scanner` over the lockfiles (npm, pnpm, yarn,
bun, Cargo) as a section of its own, `Dependencies (osv)`. It is an additional
source, not a replacement: the native audits stay, and OSV lists only the
packages they did not already report. Where one of them is missing or failed,
all OSV findings are shown. Without `osv-scanner` the section says so.

In a Cargo workspace `opi` works on the workspace even when started inside one
of its crates — that is where `target/` lives and where the checks reach every
member. `cargo run` still follows you: it is offered in a binary crate, because
that is where cargo would start it.

## Saying more about a script

Optional, and only ever refinement — a project with neither `opi` nor
`scripts-info` still gets a usable list, which is the whole point.

```json
{
  "scripts": { "dev": "astro dev", "deploy": "wrangler deploy" },
  "scripts-info": { "dev": "Start development server" },
  "opi": {
    "scripts": {
      "dev":    { "favorite": true },
      "deploy": { "group": "Deployment", "confirm": true }
    }
  }
}
```

| | |
| --- | --- |
| `description` | Wins over `scripts-info`, which stays valid for `nr` |
| `group` | Replaces the group the script's name implies |
| `favorite` | Lifts it out of its group, to the top |
| `confirm` | Asks before running it; `opi --yes <script>` answers in advance |

A `confirm` script refuses to run without a terminal rather than assuming yes —
otherwise the protection would vanish in exactly the case it exists for.

## The other areas

![opi --health running five checks in a project with package.json and Cargo.toml, one failing with rustfmt's diff shown under it](https://raw.githubusercontent.com/casoon/opi/main/assets/health.svg)

Each is a hotkey in the list and a flag on the command line. Never a bare word:
`health`, `clean` and `release` are script names in real projects, and the bare
word stays theirs.

| | | |
| --- | --- | --- |
| `H` | `opi --health` | Runs every check the project's tools can answer, concurrently |
| `S` | `opi --security` | Secret scan, a parsed dependency audit per ecosystem, signatures, release age and dependencies with install scripts, licenses and dependencies from outside the registry, workflow actions not pinned to a commit SHA (pnpm, npm, bun and Cargo; production dependencies only) |
| `U` | `opi --updates` | What is outdated, split into safe and major — and the offer to take it |
| `C` | `opi --clean` | Removable artefacts, with what each one costs |
| | `opi --check commit` | Project checks plus staged secrets, whitespace, conflict markers and large additions |
| | `opi --check push` | Everything plus the build, on a clean tree that is not behind upstream |
| | `opi --check deploy` | Selected checks and build before deployment, with a clean working tree |
| | `opi --checks [workflow]` | Choose checks and build for commit, push, release or deploy |
| | `opi --check release` | Everything plus the build, a clean tree and an untagged version |
| | `opi --hooks` | Installs `opi --check push` as the pre-push hook |
| | `opi --sbom [dir]` | CycloneDX SBOMs, one file per ecosystem, written to `dir` or `.opi/sbom/` |

`--sbom` writes `sbom-npm.cdx.json` (from `npm sbom`, or `pnpm sbom` in a pnpm
project; Yarn and Bun projects use `npm sbom` too, which reads their
`node_modules`; npm projects are read from `package-lock.json` alone) and `sbom-cargo.cdx.json` (from `cargo cyclonedx`).
Without `dir` they go to `.opi/sbom/`, a folder with a `.gitignore` of its own. The documents are not merged, so each stays attributable to its producer. A
missing producer is named with the command that installs it, e.g.
`cargo install cargo-cyclonedx`.

```
astro-v7-workspace  health

✓ Lint & format                     0.2s  biome
✗ Secrets                           1.0s  nosecrets
✓ TypeScript (@astro-v7/starter)    5.2s  astro
✓ TypeScript (@astro-v7/blog)       5.4s  astro
```

`opi` bundles nosecrets as its default secret scanner, including in Rust-only projects.
A detected project scanner takes precedence. `.nosecrets.toml` and `.nosecretsignore`
are respected; no separate scanner installation is needed for the default. It scans
the files Git does not ignore, and a high-entropy string, a guess rather than a known
credential format, is a warning rather than a failure.

`opi` reimplements none of the detection rules. It detects which tool a project depends on —
Biome, ESLint, Prettier, `tsc`, `astro check`, Vitest, Jest, fallow, Knip, a
secret scanner, `cargo` — runs it, and relays what came back. In a workspace the
checks run per member, in the member, because that is where the tools and their
config live.

Output is parsed only where the format is documented (`audit --json`,
`osv-scanner --format json`, `outdated --json`, `audit signatures --json`, `licenses list --json`, `sbom --sbom-format cyclonedx`, `cargo metadata`). Everything else is relayed whole and capped at twenty lines,
with the command to see the rest: a parser that guesses at a tool's output
breaks on that tool's next release.

In addition to the bundled secret scanner, `--security` reads the lockfile: `package-lock.json`,
`pnpm-lock.yaml`, `yarn.lock`, `bun.lock` and `Cargo.lock` name every dependency
resolved from git, a tarball URL or a path outside the workspace, with its
source. `bun.lockb`, bun's binary lockfile, is not read.

![opi --security finding a vulnerable lodash through npm audit, while cargo audit finds nothing](https://raw.githubusercontent.com/casoon/opi/main/assets/security.svg)

![opi --updates listing lodash 4.17.20 → 4.18.1 as safe to take, with the Rust side current](https://raw.githubusercontent.com/casoon/opi/main/assets/updates.svg)

There is no health score. A composite number stops meaning anything within
weeks; what a failing tool actually said does not.

### Taking the updates

`--updates` can apply what it found, and then runs the commit checks on what
comes back — update, install, check, what is red now. That chain is the reason
the action exists; saving you from typing `pnpm update` would not have been.

The choice offered is **not** safe against major. That is how the list is
ordered, and it is the wrong question for an action: in a project whose ranges
are exact or `~`, `pnpm update` moves none of what the list calls safe. The
real choice is staying inside the declared ranges against raising them, and
each line carries its own count and disappears when that count is zero.

`opi` writes nothing itself. It calls the package manager with a list of names
— no `package.json`, no lockfile, no `pnpm-workspace.yaml`. Majors are never in
that list. Raising ranges is offered for pnpm only, because npm has no command
that keeps the operator: `npm install x@1.1.1` turns an exact `2.1.2` into
`^2.1.3`, and an exact pin is a statement. Where two lockfiles disagree and no
`packageManager` field settles it, the offer is withheld and the finding named
instead.

### Choosing workflow checks

Run `opi --checks push` or `opi --checks deploy` to select detected checks and
whether to build. Without a workflow name, `opi --checks` asks which one to edit.
The selection is saved in `.opi/checks.json` and can be committed with the project.
Without a saved selection, the existing defaults apply. Git gates remain in force.

`opi --check deploy` runs the selected checks and build; deploy scripts are still
run explicitly with `opi deploy`. See [Configuration](https://casoon.github.io/opi/docs/guides/configuration/).

### Before every push

For a repository without CI, `opi --hooks` makes the push workflow the
pre-push hook: the selected checks, then the build if enabled, and a push that fails any of them
does not leave the machine. `git push --no-verify` is the way past it.

![opi --hooks installing the pre-push hook, then git push running every check and the build and refusing the push because rustfmt found a diff](https://raw.githubusercontent.com/casoon/opi/main/assets/push.svg)

### In hooks and CI

Every area is also a plain command with an exit code, so the same checks run
outside the terminal: `opi --check commit` as a pre-commit hook (Husky,
Lefthook, pre-commit or plain Git), `opi --health` or `opi --check push` in a
CI job, `opi --check deploy && opi --yes deploy` before a deployment. Without a
terminal there is no menu, no colour and no implicit confirmation. A committed
`.opi/checks.json` keeps hooks and CI on the same selection. Setup examples are
in [Hooks and CI](https://casoon.github.io/opi/docs/guides/integrations/).

## Design rules

- **Zero configuration.** `opi` must be useful in an unmodified repository. A
  tool you have to configure first never gets started in someone else's project.
- **Project files define commands.** `package.json` and `Cargo.toml` remain
  the source of commands. Optional `.opi/checks.json` selects which detected
  checks and builds belong to a workflow; it defines no commands.
- **No task system of its own.** `opi` does not define tasks with their own
  commands — that would put it in competition with npm scripts, `just`, `make`
  and Taskfile for no gain.
- **Orchestration, not reimplementation.** `opi` is a UX layer over proven
  tooling.
- **No manifest rewriting.** Clean removes what you pick, updates are applied
  by the package manager, and `opi --hooks` adds one line to the pre-push hook.
  `opi --checks` saves the workflow selection and Git ignore rules under `.opi`. No manifest or lockfile is ever rewritten by
  `opi` itself, and where a tool cannot do something cleanly it is not offered rather than
  offered badly.

## Install

```bash
curl -fsSL https://raw.githubusercontent.com/casoon/opi/main/install.sh | sh
```

That downloads the release binary for your platform, checks it against the
published SHA-256 and puts it in `~/.local/bin` — `OPI_INSTALL_DIR` picks
another directory, `OPI_VERSION` another version. Binaries exist for macOS on
Apple silicon and Intel, and for Linux on x86-64 and arm64; the Linux ones are
static musl builds, so the distribution does not matter.

Official binaries come only from the
[GitHub releases of `casoon/opi`](https://github.com/casoon/opi/releases) —
built there by the release workflow, never on a developer machine. To verify a
download by hand, check the hash and the signed build provenance:

```bash
shasum -a 256 -c opi-<version>-<target>.tar.gz.sha256
gh attestation verify opi-<version>-<target>.tar.gz --repo casoon/opi
```

Each release also carries a CycloneDX SBOM, `opi-v<version>.cdx.json`, attested
against every archive. The binaries are built with `cargo auditable`, so the
dependency list sits inside them and a downloaded binary can be checked for
known vulnerabilities:

```bash
gh attestation verify opi-<version>-<target>.tar.gz --repo casoon/opi \
  --predicate-type https://cyclonedx.org/bom
cargo audit bin opi
```

With [cargo-binstall](https://github.com/cargo-bins/cargo-binstall), which
fetches the same release binary into `~/.cargo/bin` (without the hash check):

```bash
cargo binstall opi
```

From the registry instead, which builds it here:

```bash
cargo install opi
```

**Unix only.** `opi` runs a script by replacing its own process with `exec`, so
`Ctrl-C` reaches the dev server rather than killing a wrapper, and its
interactive list drives termios directly. Windows offers neither, and a second
execution model that nothing exercises would be worse than an honest boundary.
Building on Windows fails with that message rather than producing a degraded
binary.

## Documentation

**[casoon.github.io/opi](https://casoon.github.io/opi/)** — installation, a
guide per area, the command line reference and the changelog, with the terminal
output captured from the binary.

The files below are the same documentation as it sits in the repository:

- [docs/project-state.md]docs/project-state.md — what this is and where it stands
- [Architecture]https://casoon.github.io/opi/docs/architecture/ — the modules and how they fit
- [docs/constraints.md]docs/constraints.md — the boundaries the implementation respects
- [docs/conventions.md]docs/conventions.md — the rules future changes follow
- [docs/decisions.md]docs/decisions.md — the decisions in force, and why

[demo/](demo/) holds the fixtures and the scripted scenes the recordings above
are made from, and says what has to be true for a recording to show `opi` rather
than the repository it was recorded in.

Terminal presentation comes from [runemark](https://github.com/casoon/runemark),
the shared presentation layer for these CLI tools.

## Name

`opi` is unrelated to [OdradekAI/opi](https://github.com/OdradekAI/opi), which
owns the `opi-*` crate namespace on crates.io. Installing both puts two `opi`
binaries in `~/.cargo/bin`.

## License

MIT ([LICENSE](LICENSE)).