The levior CLI (Go) and cli-api (Bun: repo/team PRs on infra, CI runs and logs, previews; synced by Argo CD from .argo/)
  • Go 76.5%
  • TypeScript 22.1%
  • Shell 1.3%
  • Dockerfile 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Simon Borgbjerg Laursen b5bc4035c3
All checks were successful
ci / image-build (push) Has been skipped
ci / test (push) Successful in 1m8s
ci / release (push) Successful in 34s
ci / image (push) Successful in 1m22s
Merge pull request 'Remove the Scotland Yard caller workflow' (#35) from simlau/remove-scotland-yard-caller into main
Reviewed-on: #35
2026-10-29 21:34:32 +00:00
.argo fix: cli-api takes the project audience only from listed machine clients 2026-10-28 21:42:00 +01:00
.forgejo/workflows Remove the Scotland Yard caller workflow 2026-10-29 22:28:51 +01:00
cmd/levior feat: levior repo list, and repo create requires a description 2026-10-28 21:06:42 +01:00
docs/superpowers levior pr create: open as the user 2026-10-26 21:50:16 +01:00
internal feat: levior repo list, and repo create requires a description 2026-10-28 21:06:42 +01:00
packages/api Update dependency jose to v6 2026-10-28 21:13:12 +00:00
scripts chore: drop the Bun formula script, generate the Homebrew formula with a shell script 2026-10-22 13:11:07 +02:00
.gitignore levior CLI: Zitadel login and kubectl credentials 2026-10-21 15:54:46 +02:00
bun.lock Update dependency typescript to v7 2026-10-28 21:14:19 +00:00
CLAUDE.md docs: README for people and agents; CLAUDE.md imports it [skip release] 2026-10-27 09:20:31 +01:00
Dockerfile feat: API package, team and repo create commands, image build and Argo deployment 2026-10-21 19:00:52 +02:00
go.mod Update module codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 to v3 2026-10-28 21:15:50 +00:00
go.sum Update module codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 to v3 2026-10-28 21:15:50 +00:00
install.sh Add install script 2026-10-21 16:55:55 +02:00
package.json Update dependency typescript to v7 2026-10-28 21:14:19 +00:00
README.md fix: cli-api takes the project audience only from listed machine clients 2026-10-28 21:42:00 +01:00
tsconfig.json chore: drop the Bun formula script, generate the Homebrew formula with a shell script 2026-10-22 13:11:07 +02:00

levior

The command line tool for the Levior platform. One login through Zitadel (https://idp.levior.io) gives you kubectl on https://k8s.levior.io, CI runs and logs of private repos, pull requests on https://code.levior.io as yourself, preview environments, and new teams and repos, without personal API tokens.

This repo holds two programs:

  • levior, the CLI (Go, cobra): cmd/levior, internal/*.
  • cli-api (Bun, TypeScript): packages/api, deployed by Argo CD from .argo/api.yaml at https://cli-api.app.levior.io. It does what needs the platform's bot token: team and repo PRs on levior/infra, CI runs and logs, preview labels.

Install

macOS or Linux with Homebrew:

brew tap levior/tap https://code.levior.io/levior/homebrew-tap.git
brew trust levior/tap
brew install levior

Without Homebrew, install.sh puts the latest release into ~/.local/bin (LEVIOR_BIN_DIR changes that):

curl -fsSL https://code.levior.io/levior/cli/raw/branch/main/install.sh | sh

Then, once per machine:

levior login     # browser: Zitadel, then code.levior.io
levior k8s       # kubectl context `levior`
levior whoami    # email, groups, code.levior.io login

After brew upgrade levior, run levior k8s again: the kubeconfig points at the binary's Cellar path, which changes with every version.

Commands

Every command has --help; this is the overview.

Command What it does
levior login [--device] Signs in to Zitadel in the browser, then to code.levior.io. --device prints a code to enter on another device instead, but skips code.levior.io, so levior pr won't work.
levior logout Forgets both sessions.
levior whoami Your email, Zitadel groups and code.levior.io login (or why there is none).
levior k8s Writes cluster levior-remote, user levior-oidc and context levior into your kubeconfig. Pass --context levior to kubectl.
levior docker login Opens https://reg.levior.io/home, where you create an API key, and runs docker login reg.levior.io with your email and that key.
levior ci runs [-r <team>/<repo>] The last 15 CI runs. Inside a checkout, -r defaults to its origin.
levior ci logs [<run>] [--job <name>] [-r ...] Logs of a run (default: the latest), only its failed jobs if any failed.
levior repo log [-R <team>/<repo>] [--ref <ref>] [--path <path>] [-n 10] [--json] Commit history, newest first, with each author's Forgejo login when their email belongs to an account. Reads as you on code.levior.io.
levior ci run [-w <workflow>] [--ref <branch>] [-r ...] Starts a new run of a workflow, by default ci.yml on main, e.g. to rebuild main after a failed build. Forgejo can't re-run a run through its API, so the workflow needs workflow_dispatch: under on:. Team admin or edit.
levior pr create Opens a PR from the current branch, which must be pushed to origin under the same name. Title and body come from the commits. Flags as in gh pr create (-t, -b, -F, -B, -H, -d, -r, -a, -l, -e, -w).
levior pr list|view|diff|checkout|checks Read PRs, like gh pr. list and view take --json <fields>.
levior pr edit|ready|comment|review|close|reopen|merge Change PRs, like gh pr. close -d and merge -d delete the branch, merge --auto waits for the required checks.
levior deploy feature [--delete] Deploys the current branch as a preview environment (see below).
levior repo list [-t <team>] [--json] Repositories you can see, with their description (what each is, and whether and how it is deployed) and last change.
levior repo create -n <name> -t <team> -d <description> Opens a levior/infra PR that adds the repo (--public). The description is required: what the repo is, and whether and how it is deployed. Any member who may push to the team's repos may do this.
levior team create <name> Opens a levior/infra PR that adds a team. Cluster admins only.

levior pr commands take the PR as a number, a branch or a URL, default to the current branch's PR, and act on -R <team>/<repo> or the checkout's origin. levior pr checks exits 8 while checks are pending and 1 when one failed or there are none, so scripts can wait on it.

Using it from an agent

Agents (Claude Code and others) can run every command except the logins:

  • levior login needs a person at a browser. When a command says not logged in to code.levior.io or session expired, stop and ask the user to run levior login; don't look for a token instead. The Zitadel session refreshes on its own; the code.levior.io session needs a browser login every 30 days, and after a --device login.
  • Use levior ci runs/logs to read CI of private repos and levior pr for PRs; there are no personal Forgejo tokens on the platform.
  • Open a PR whose CI needs secrets (infra, anything that pushes images) with git push origin HEAD:refs/heads/<branch> and levior pr create. AGit PRs (git push origin HEAD:refs/for/main) run their CI without secrets.
  • Prefer --json and the exit codes of levior pr checks over parsing tables.
  • levior repo create and levior team create print the infra PR's URL right away. levior-bot comments digger apply once the plan is green; Digger applies and merges. The repo or team exists a few minutes later.

Headless (a service in the cluster, e.g. Holmes), without levior login:

  • LEVIOR_CLIENT_ID, LEVIOR_CLIENT_SECRET and LEVIOR_PROJECT_ID: a Zitadel machine user's client credentials. Every cli-api command (levior ci, deploy, repo create, ...) gets a JWT access token by client credentials, with the project (LEVIOR_PROJECT_ID, the kubernetes project) as audience and the team roles the machine user has been granted as groups. It is not stored. Machine users come from infra (holmes-identities.tf); their tokens don't work against the kube-apiserver, which requires a verified email.
  • LEVIOR_FORGEJO_TOKEN: a Forgejo token for the commands that talk to Forgejo directly (levior pr, levior repo log), used as is.

The Levior skills (levior/skills, Claude Code plugin levior) say when to use which command on the platform; this README is the reference for the CLI itself.

How it works

Login. levior login runs an OIDC authorization code flow with PKCE against Zitadel, with the browser redirecting to localhost:8000 (or 18000 when that is busy); --device uses the device flow. The ID token, with your email and groups, is what every other part trusts. A second browser login against code.levior.io (Forgejo OAuth) gets a token for levior pr; it refreshes for 30 days from that login. Both are stored in ~/.config/levior/ (LEVIOR_CONFIG_DIR), mode 0600, and every command refreshes them when they are about to expire. Only levior login signs in; the others fail with a message saying so when a login is due.

kubectl. levior k8s registers levior k8s token as an exec credential plugin. kubectl calls it for every request, and it prints the current ID token, refreshing it first if needed. The API server trusts Zitadel and maps your groups to roles in your team's namespace.

Pull requests. levior pr talks to the Forgejo API directly with your own token, so PRs, comments, reviews and merges are yours. A PR opened from a branch of the repo gets the repo's Actions secrets, which an AGit PR doesn't.

cli-api. levior ci, levior deploy feature, levior repo create and levior team create call https://cli-api.app.levior.io (LEVIOR_API overrides it) with the ID token as Bearer (headless: the machine user's access token, whose audience is the project, PROJECT_ID; only clients in MACHINE_CLIENT_IDS may use that audience, and their client_id names the caller). cli-api checks it against Zitadel's keys and your groups (<team>-admin|edit|view, cluster-admin) and then acts with its own Forgejo token:

Route Who What
GET /repos/<team>/<repo>/runs[/<n>], .../jobs/<id>/logs team members, any role CI runs, jobs, logs
POST /repos/<team>/<repo>/runs admin, edit dispatches a workflow (workflow, ref), so the run shows as the bot's
POST /repos/<team>/<repo>/deploy admin, edit opens the branch's PR if needed and adds or removes the label preview
POST /repos admin, edit of the team adds the repo to infra/repos.tf in a new PR
POST /teams cluster-admin adds the team to infra/teams.tf in a new PR

For repo and team PRs cli-api waits for digger/plan, comments digger apply once it is green, and leaves a failed plan to people.

Preview environments. levior deploy feature labels the branch's PR preview. Argo CD's ApplicationSet <team>-previews deploys .argo/ of the PR's head into the team namespace as <team>-<repo>-pr-<number>, with names and the first label of each Ingress host suffixed -pr-<number>. If .argo/feature/kustomization.yaml exists, previews use it instead of .argo/. Closing the PR or --delete takes the preview down. What a preview shares with main (Secrets, databases) is in infra/docs/runbooks/onboarding.md, "Preview environments".

Configuration. Issuer, client IDs, API server and its CA are compiled in from internal/config/config.json, which scripts/sync-config.sh <infra checkout> writes from the infra output levior_cli_config.

Development

go test ./...                 # CLI
go run ./cmd/levior --help
bun install && bun test       # cli-api
bun run typecheck
scripts/build.sh              # dist/levior-{darwin,linux}-{arm64,x64}

Point a local CLI at a local cli-api with LEVIOR_API=http://localhost:8080, and keep test sessions apart with LEVIOR_CONFIG_DIR.

CI (.forgejo/workflows/ci.yml) runs both test suites on every push and PR. On main it also builds the cli-api image, which Argo CD rolls out, and releases the CLI:

  • The version comes from the commit messages since the last tag: feat: bumps the minor version, <type>!: or BREAKING CHANGE the major, anything else the patch.
  • The binaries go to a Forgejo release, and levior/homebrew-tap's Formula/levior.rb is rewritten by scripts/formula.sh. Don't edit the formula by hand.
  • [skip release] in the head commit's message skips the release, for changes that don't touch the CLI (docs, cli-api only).

main is protected: open a PR (levior pr create, or AGit, since this CI needs no secrets for PRs) and wait for ci / test.