- Go 76.5%
- TypeScript 22.1%
- Shell 1.3%
- Dockerfile 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .argo | ||
| .forgejo/workflows | ||
| cmd/levior | ||
| docs/superpowers | ||
| internal | ||
| packages/api | ||
| scripts | ||
| .gitignore | ||
| bun.lock | ||
| CLAUDE.md | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| install.sh | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
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.yamlat https://cli-api.app.levior.io. It does what needs the platform's bot token: team and repo PRs onlevior/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 loginneeds a person at a browser. When a command saysnot logged in to code.levior.ioorsession expired, stop and ask the user to runlevior 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--devicelogin.- Use
levior ci runs/logsto read CI of private repos andlevior prfor 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>andlevior pr create. AGit PRs (git push origin HEAD:refs/for/main) run their CI without secrets. - Prefer
--jsonand the exit codes oflevior pr checksover parsing tables. levior repo createandlevior team createprint the infra PR's URL right away. levior-bot commentsdigger applyonce 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_SECRETandLEVIOR_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>!:orBREAKING CHANGEthe major, anything else the patch. - The binaries go to a Forgejo release, and
levior/homebrew-tap'sFormula/levior.rbis rewritten byscripts/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.