No description
  • Go 94.5%
  • Just 2.9%
  • Shell 2.6%
Find a file
Claudia 6919c7eb28
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
fix: require go 1.26, not a patch release
go get recorded the local toolchain's patch version; the suite pins
major.minor so the golang:1.26 CI image qualifies regardless of patch.
2026-07-23 20:51:56 +02:00
.just chore: add wrangle 2026-07-23 20:28:50 +02:00
.woodpecker ci: woodpecker build and release pipelines 2026-07-23 20:45:41 +02:00
internal feat: first-class claudia profile per domain 2026-07-23 20:14:13 +02:00
scripts chore: add wrangle 2026-07-23 20:28:50 +02:00
.gitignore feat: switch shells between development domains 2026-07-23 13:20:04 +02:00
CHANGELOG.md docs: changelog for v0.1.0 2026-07-23 20:29:00 +02:00
config.example.toml feat: first-class claudia profile per domain 2026-07-23 20:14:13 +02:00
go.mod fix: require go 1.26, not a patch release 2026-07-23 20:51:56 +02:00
go.sum feat: switch shells between development domains 2026-07-23 13:20:04 +02:00
justfile chore: add wrangle 2026-07-23 20:28:50 +02:00
main.go feat: first-class claudia profile per domain 2026-07-23 20:14:13 +02:00
README.md feat: first-class claudia profile per domain 2026-07-23 20:14:13 +02:00

moor

Switch the shell between development domains. A domain is a context under the workspace — client work, company work, private tools — and m amedia/foo jumps to a checkout while activating everything that context implies: the mise toolchain declared in the domain root, per-domain language caches, profile env vars, and the sage ssh identity. Switching is exclusive and eval-based, like sage load and skiff switch: moor prints a snippet, the m() wrapper applies it, and the next switch reverts it exactly. No per-cd hook, no nix, no direnv.

moor deliberately does not manage what can be dispatched statically: git identity belongs in ~/.gitconfig via includeIf "gitdir:…", ssh keys in ~/.ssh/config via per-host IdentityAgent pointing at sage's sockets. Tool provisioning is delegated to mise — each domain root carries a plain mise.toml, and moor uses mise purely as a provisioner and env printer (no mise shell activation is installed).

Install

Requires Go 1.26+ and just.

just install    # builds and copies to /usr/local/bin/

Then in ~/.zshrc, after compinit:

eval "$(moor hook zsh)"

Usage

m amedia/foo         # cd ~/dev/amedia/foo + activate the amedia domain
m vardevs            # cd to the domain root, activate vardevs
m .                  # re-sync context to the current directory
m                    # back to the workspace root, context ejected
m forks/thing        # plain cd — not a domain, context left alone

moor status          # active domain and the registry (also bare `moor`)
moor sync --all      # mise install + cache dirs for every domain
moor switch amedia   # the raw snippet m() evals (also: eject, cd, hook)

Config

The registry is explicit TOML at ~/.config/moor/config.toml — see config.example.toml:

workspace = "~/dev"

[domains.amedia]
sage    = "amedia"                          # sage identity to load on switch
claudia = "amedia"                          # claudia profile to load on switch
langs   = ["go", "node", "python", "maven"] # per-domain caches under <root>/.moor
hooks   = ["cat .envrc.local"]              # exports captured from each command
paths   = ["bin"]                           # extra PATH entries (root-relative)
[domains.amedia.env]
K8S_OBJECTS_DIR = "/home/varl/dev/amedia/tools/k8s-objects"

A hook is any command that prints export K=V statements (sage-style); moor captures them into the switch — PATH additions diffed against the base, other vars tracked and reverted like everything else.

Tools live in the domain root's mise.toml, mise-native:

[tools]
go = "1.26"
kubectl = "latest"
"ubi:gini/dexter" = "0.5.2"

Subprojects go one tier below the domain: a mise.toml inside a checkout (e.g. [env] _.file = ".env" to load that dir's dotenv) joins the context when you m into it — moor captures mise env at the directory you land in, and mise merges every config on the way up. New config files need a one-time mise trust; an untrusted config anywhere in the chain makes mise refuse the whole capture (moor degrades to a warning).

A mise.toml at the workspace root is the tier above: mise merges parent-dir configs, so its tools reach every domain under the workspace (moor applies it to config-less domains too) without touching the bare system. Machine-level packages stay with the system package manager; only the two-plus-tier split between "all domains" and "this domain" lives in mise.

How a switch works

moor switch <domain> prints a zsh snippet that:

  1. unsets every var the previous switch exported (tracked in MOOR_VARS),
  2. rebuilds PATH as the domain's prepends over the pre-moor base (MOOR_BASE_PATH, recorded on the first switch in a shell),
  3. exports the mise env for the domain root, the language-cache vars (GOPATH, NPM_CONFIG_PREFIX, …), MANPATH and zsh fpath entries probed from the tool paths, and the domain's own env table,
  4. passes sage's own export SSH_AUTH_SOCK… line through verbatim.

State lives only in the shell's environment (MOOR_DOMAIN, MOOR_VARS, MOOR_BASE_PATH, MOOR_FPATH), so a switch is a pure function of the registry plus the previous switch — nothing to get stale, nothing layered.

Migrating a domain off nix

  1. Write the root's mise.toml from the old shell.nix buildInputs.
  2. moor sync <domain>, then keep the caches: mv .nix-go .moor/go, mv .nix-npm .moor/npm, mv .nix-node .moor/node, and so on.
  3. Delete shell.nix and .envrc; drop the direnv wiring from ~/.zshrc once no domain uses it.