Metadata-Version: 2.4
Name: ocenv
Version: 0.1.0
Summary: Declarative identities and profiles for running layered opencode configs side by side
Author-email: Felix Scherz <felixwscherz@gmail.com>
Requires-Python: >=3.13
Requires-Dist: pydantic>=2.7
Requires-Dist: typer>=0.12
Description-Content-Type: text/markdown

# ocenv

Run multiple "flavors" of [opencode](https://opencode.ai) side by side - personal vs. work
auth, company-specific providers, per-project config - from a single declarative file
instead of a pile of hand-maintained shell aliases.

opencode's `OPENCODE_CONFIG` is a single file pointer, not a stack, so you cannot express
"always include my company's providers *and* this project's config". ocenv replaces the
aliases with a model of **identities**, **layers**, and **profiles**, composes layered
config, and launches opencode in a fully isolated environment.

## Concepts

- **Identity** - an isolated opencode footprint. Redirects `XDG_DATA_HOME` so auth
  (`auth.json`) and the session db are per-identity, and injects the composed config via
  `OPENCODE_CONFIG`. State, cache, and your shared `~/.config/opencode` stay shared, so the
  redirect does not leak into other XDG-aware tools (e.g. `nvim`) launched from inside
  opencode. `opencode auth login` is done once per identity. Set `isolate_config = true`
  for a hermetic identity that also suppresses the shared global config.
- **Layer** - a named, reusable fragment contributing a config payload and/or environment
  variables. The config payload is either inline in `ocenv.toml` or a reference to an
  external JSON/JSONC file (e.g. your company's providers); `env` is independent, so an
  `env`-only layer is a valid, reusable secrets/env bundle.
- **Profile** - the launchable unit: an identity + an ordered list of layers + extra env +
  a working directory. Launching folds the layers' config and env into one, and starts
  opencode.

Layer/profile `env` composes left-to-right (later wins), with `profile.env` applied last.
ocenv's own managed vars (`XDG_DATA_HOME`, `OPENCODE_CONFIG`, and `XDG_CONFIG_HOME` for a
hermetic identity) are pinned last of all, so env cannot accidentally break isolation - if a
layer/profile tries to set one, it is ignored and ocenv warns.

Layers are merged with opencode's own semantics (objects deep-merge, arrays replace, and
`instructions` is concatenated + de-duplicated), and injected via `OPENCODE_CONFIG` - which
opencode loads *below* project config, so a project's own `.opencode` config still wins,
exactly as normal.

## Getting started

Requires Python 3.13+, [uv](https://docs.astral.sh/uv/), and `opencode` on your `PATH`.

```sh
# install
uv sync

# 1. create a config
mkdir -p ~/.config/ocenv
cp examples/ocenv.toml ~/.config/ocenv/ocenv.toml   # then edit to taste

# 2. see what a profile resolves to (no opencode launched)
uv run ocenv --profile work show

# 3. inspect the isolated environment
uv run ocenv --profile work env

# 4. authenticate the identity once
uv run ocenv --profile work opencode -- auth login

# 5. launch opencode with the composed, isolated environment
uv run ocenv --profile work opencode
```

Set a profile for the whole shell with the `OCENV_PROFILE` environment variable so you can
drop the flag:

```sh
export OCENV_PROFILE=work
uv run ocenv opencode
```

## CLI

Profile selection is a **global** option (aws-cli style) and must come *before* the command:

```
ocenv --profile <p> opencode [-- <opencode args...>]   # compose + launch opencode
ocenv --profile <p> show                               # print the composed config as JSON
ocenv --profile <p> env                                # print the resolved env (export lines)
ocenv list                                             # list profiles + identities
```

Everything after `--` is passed straight through to opencode:

```sh
ocenv --profile work opencode -- run "summarize this repo"
ocenv --profile work opencode -- debug config          # opencode's own resolved config
```

`--profile` / `-p` also reads from the `OCENV_PROFILE` env var. Point ocenv at a specific
config file with `--config <path>`; otherwise it is discovered at
`$XDG_CONFIG_HOME/ocenv/ocenv.toml` (falling back to `~/.config/ocenv/ocenv.toml`).

## Configuration

`ocenv.toml` has three sections - `layers`, `identities`, and `profiles`:

```toml
[layers.personal-base.config]                          # inline opencode fragment
model = "anthropic/claude-opus-4-8"

[layers.company-providers]
file = "~/workspaces/acme/.opencode/providers.json"   # external JSON/JSONC file

[layers.company-secrets.env]                           # env-only layer, reusable bundle
JIRA_URL = "https://jira.acme.example"

[identities.work]
root = "~/.local/share/ocenv/identities/work"          # optional; defaults to a managed path
isolate_config = true                                  # optional; suppress shared global config

[profiles.work]
identity = "work"
layers = ["personal-base", "company-providers", "company-secrets"]   # order: later wins
cwd = "~/workspaces/acme"
[profiles.work.env]
SOME_TOKEN = "..."                                      # applied after all layer env
```

See the [`examples/`](examples/) directory for complete, commented configurations.

## How isolation works

opencode resolves its storage locations from the raw `XDG_*` env vars, and those are
inherited by every process opencode spawns (the bash tool, `$EDITOR`, an embedded terminal,
`nvim`). Overriding all four would therefore redirect those tools too - so ocenv keeps the
redirect deliberately narrow:

- **`XDG_DATA_HOME`** → `<identity>/data`. This is the only store for `auth.json` (and the
  session db), and opencode offers no other override for it, so this redirect is
  unavoidable. It is the one leak that remains: `nvim`'s *data* (plugins) is redirected if
  you launch it from inside opencode, though its config/state/cache are untouched.
- **`OPENCODE_CONFIG`** → the composed config file. This is opencode-specific (not an XDG
  var, so no leak) and loads *below* project config, preserving "project wins".
- **state** and **cache** are left shared (`XDG_STATE_HOME` / `XDG_CACHE_HOME` untouched).
- your shared `~/.config/opencode` still applies as opencode's global base. Set
  `isolate_config = true` on an identity to suppress it: ocenv then points
  `XDG_CONFIG_HOME` at a managed empty dir (this redirect *is* inherited by child tools, so
  it is opt-in).

ocenv also **scrubs** opencode's other config-pointer env vars (`OPENCODE_CONFIG_CONTENT`,
`OPENCODE_CONFIG_DIR`, `OPENCODE_DISABLE_PROJECT_CONFIG`) from the launched environment, so
a stray value in your shell cannot leak config into an otherwise isolated identity.
`OPENCODE_CONFIG` is not scrubbed because ocenv sets it itself.

## Development

```sh
uv sync
uv run pytest              # unit + integration tests
```

Integration tests exec the real `opencode` binary and assert against `opencode debug config`
/ `opencode debug paths`; they skip automatically when `opencode` is not installed.

See [`DESIGN.md`](DESIGN.md) for the full design rationale and opencode config-resolution
details.
