Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@
- The `key` prompt of a multi-instance module may carry an `optionsCommand` (the only non-select prompt allowed to). It runs on the host to list accounts importable as new instances (e.g. `aws`/`outscale` list profiles, `ssh` lists `~/.ssh/config` host aliases). In the editor's add flow this drives an import picker: selecting one or more host accounts creates one instance each storing only the key value, with the actual credentials pulled by the module's `resolve` hook at install time (so secrets never reach the manifest); the picker also offers an "Add manually…" option that opens a single-page form (all fields at once) for an account not present on the host, whose typed values are stored as usual. The key prompt stays a free-text `string` so manual entry still works; `resolve` must emit nothing for an account it does not find on the host, leaving the manually-typed values in place.
- A module.yml may declare `restartServer: false` to mark itself environment-neutral: it writes only its own config files (e.g. `~/.kube/config`, `~/.aws/credentials`) that tools read live, never `~/.env`. Such a module is never bounced (the manager skips the server restart even if `~/.env` somehow changed — and logs a warning, since that means the manifest lies) and, crucially, its add/remove is allowed from the editor **while a task is running**, because nothing interrupts the in-flight session. Omitting the key (or setting `restartServer: true`) keeps the conservative default: the module is assumed to touch `~/.env`, so the manager bounces the server when `~/.env` changes and the editor blocks the edit until the workspace is idle. All current built-ins (`aws`, `git`, `kubernetes`, `outscale`, `ssh`) are `restartServer: false`.
- Workspace containers are long-lived and tied to the workspace lifecycle.
- New workspaces seed OpenCode files from `~/.config/opencode-manager/opencode/` into `home/.config/opencode/` for `opencode.json`, `agent/`, `commands/`, `plugins/`, and `skills/`.
- Shared OpenCode config lives in `~/.config/opencode-manager/opencode/` and is copied one-way into each workspace's `home/.config/opencode/` on provision, startup reconcile, and source changes while the manager runs. A per-workspace journal permits safe removal of previously managed source entries. The shared source must not contain generated top-level state (`.gitignore`, package manifests/lockfiles, or `node_modules`); those are preserved per workspace. Existing legacy root templates migrate only when `opencode/` is absent or empty.
- `config.yaml` defines `baseImage.name`, `baseImage.packages`, and `baseImage.commands`; commands run during image build immediately after package installation.
- `config.yaml` supports `useLocalOpenCodeAuth` (default `false`); when `true`, `~/.local/share/opencode/auth.json` is mounted read-write into the same path in workspace containers.
- `config.yaml` supports `extraCACertificate` (default empty list); absolute host certificate paths are mounted read-only and installed in the workspace system trust store at startup. Legacy single-path config remains supported.
- `config.yaml` supports `workspaceEnv`, a map applied to every workspace container. Literal values and `{env:HOST_VARIABLE}` references are supported; host values are resolved on every provision and are never persisted to manifests.
- Generated workspace images must include `npx`, `uvx`, `git`, `ripgrep`, and `jq` by default.
- Managed base images are tagged from a stable hash of `baseImage` definition and reused until that definition changes.
- The TUI ensures the managed base image exists at startup and shows `Creating the base image...` while it is built.
Expand Down
28 changes: 16 additions & 12 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ Each workspace has:
- A name.
- A dedicated directory under the configured workspace root.
- A dedicated home directory.
- A global `opencode.json` mounted read-only at `home/.config/opencode/opencode.json`.
- Globally shared OpenCode commands, skills, agents, and plugins, mounted read-only.
- A one-way copy of `~/.config/opencode-manager/opencode/` at
`home/.config/opencode/`, reconciled at manager startup and on source changes.
- Per-workspace generated OpenCode state remains local and never syncs back.
- Selected module configuration.
- A generated image.
- A long-lived container.
Expand Down Expand Up @@ -169,6 +170,7 @@ workspaceRoot: /home/user/.local/share/opencode-manager
runtime: docker
useLocalOpenCodeAuth: false
extraCACertificate: []
workspaceEnv: {}
hostNetwork: false
runtimeArgs:
- --dns
Expand Down Expand Up @@ -201,6 +203,10 @@ container's Debian system trust store. The option is an empty list by default. A
certificate list or content change takes effect when the workspace next starts,
which recreates its container. Existing single-path configurations remain valid.

`workspaceEnv` passes named environment variables to every workspace. Values may
be literal or `{env:HOST_VARIABLE}`, which is resolved from the manager host at
workspace provisioning time. Resolved values are not persisted to manifests.

Set `hostNetwork: true` to run each container in the host's network namespace
(`--network host`) instead of an isolated one, so the agent and its tools can
reach services on the host loopback. The default `false` keeps each container
Expand Down Expand Up @@ -262,11 +268,10 @@ When the TUI starts, it ensures the base image is available and shows

## Global OpenCode Templates

OpenCode configuration is shared across all workspaces from the global config
directory:
OpenCode configuration is shared across all workspaces from this source tree:

```text
~/.config/opencode-manager/
~/.config/opencode-manager/opencode/
├── AGENTS.md
├── opencode.json
├── agents/
Expand All @@ -275,14 +280,13 @@ directory:
└── skills/
```

These entries are **mounted read-only** into every workspace container at
`/home/debian/.config/opencode/`. Editing a file on the host propagates live to
all running workspaces — no copy is made and no recreation is needed. Adding or
removing a template takes effect the next time a workspace container is
(re)created.
The manager copies this tree one way into each workspace at
`/home/debian/.config/opencode/` during provisioning, at startup, and after a
host-side source change while it is active. A per-workspace journal makes source
removals safe by removing only entries the manager previously copied. Generated
top-level state remains local to each workspace and never syncs back.

On startup, `opencode-manager` creates any missing entries (so the mounts always
have a source):
On startup, `opencode-manager` creates any missing entries in the shared source:

- `AGENTS.md` and the `agents/`, `commands/`, `plugins/`, `skills/` directories
are created empty.
Expand Down
5 changes: 5 additions & 0 deletions PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ workspaceRoot: /home/user/.local/share/opencode-manager
runtime: docker
useLocalOpenCodeAuth: false
extraCACertificate: []
workspaceEnv: {}
baseImage:
name: debian:stable-slim
packages:
Expand All @@ -148,6 +149,10 @@ certificate files. Each is mounted read-only and installed in every workspace
container's system trust store before OpenCode starts. A single legacy path is
also accepted.

`workspaceEnv` defines environment variables for every workspace. A value may be
literal or `{env:HOST_VARIABLE}` to resolve the current manager-host value when
the workspace is provisioned.

Generated workspace images always include `npx`, `uvx`, `git`, `ripgrep`, and `jq`. Additional Debian packages are declared through `baseImage.packages`, and additional build steps are declared through `baseImage.commands`.

The managed base image is tagged from a stable hash of the base image definition and is reused while that definition stays unchanged. Workspace-specific images are built from the managed base image and add only workspace-specific setup such as the matching UID/GID user.
Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ directory on your `PATH`.

1. **Install** (see above).

2. **Add your OpenCode config.** Every workspace shares the OpenCode templates in
your global config directory. Copy your existing `opencode.json`, skills,
2. **Add your OpenCode config.** Every workspace receives a one-way copy of the
shared config in `~/.config/opencode-manager/opencode/`. Copy your existing `opencode.json`, skills,
commands, agents, and plugins into it:

```sh
cd ~/.config/opencode-manager
cd ~/.config/opencode-manager/opencode

cp /path/to/your/opencode.json .
cp -r /path/to/your/skills/* skills/
Expand All @@ -78,9 +78,11 @@ directory on your `PATH`.
cp /path/to/your/AGENTS.md .
```

These are mounted read-only into every workspace, so editing them on the host
updates all your workspaces live. (`ocm` creates this directory and an empty
`opencode.json` for you on first run, so the layout already exists.)
`ocm` copies this tree to each workspace on creation, reconciles it at startup,
and watches it while active. Host changes win; workspace changes never flow
back. Per-workspace generated state such as lockfiles and `node_modules` is
preserved. (`ocm` creates this directory and an empty `opencode.json` for you
on first run.)

3. **Launch the dashboard.**

Expand Down
9 changes: 9 additions & 0 deletions cmd/opencode-manager/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"fmt"
"log/slog"
"os"
Expand Down Expand Up @@ -44,6 +45,14 @@ func main() {
os.Exit(1)
}

syncContext, cancelSync := context.WithCancel(context.Background())
defer cancelSync()
if err := workspace.StartOpenCodeConfigSync(syncContext, cfg); err != nil {
slog.Error("failed to synchronize shared OpenCode config", "error", err)
fmt.Fprintf(os.Stderr, "failed to synchronize shared OpenCode config: %v\n", err)
os.Exit(1)
}

if err := cli.NewRootCommand(cfg).Execute(); err != nil {
slog.Error("command failed", "args", os.Args[1:], "error", err)
fmt.Fprintf(os.Stderr, "%v\n", err)
Expand Down
22 changes: 11 additions & 11 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,8 @@ container. Each workspace has:
- a **name**;
- a dedicated directory under the configured workspace root;
- a dedicated **home directory** (`home/`);
- the global `opencode.json` mounted read-only;
- the globally shared OpenCode commands, skills, agents, and plugins, mounted
read-only;
- a one-way copy of shared OpenCode configuration, including `opencode.json`,
commands, skills, agents, and plugins;
- its selected **module** configuration;
- a generated **image** and a long-lived, attachable **container** that runs
OpenCode interactively.
Expand Down Expand Up @@ -80,11 +79,10 @@ with exactly those modules already installed. Templates are stored as

## Shared OpenCode config

OpenCode configuration is shared across all workspaces from the global config
directory:
OpenCode configuration is shared across all workspaces from this source tree:

```text
~/.config/opencode-manager/
~/.config/opencode-manager/opencode/
├── AGENTS.md
├── opencode.json
├── agents/
Expand All @@ -93,12 +91,14 @@ directory:
└── skills/
```

These are **mounted read-only** into every workspace at
`/home/debian/.config/opencode/`. Editing a file on the host propagates live to
all running workspaces — no copy, no recreation. Adding or removing an entry
takes effect the next time a workspace container is (re)created.
The manager copies this tree one way into each workspace at
`/home/debian/.config/opencode/` during provisioning, at startup, and after a
host-side source change while it is active. Host changes win and workspace
changes never flow back. A per-workspace journal lets source removals remove only
entries that the manager previously copied. Generated top-level state such as
package manifests, lockfiles, and `node_modules` is intentionally local.

On startup `ocm` creates any missing entries so the mounts always have a source:
On startup `ocm` creates any missing entries in the shared source:
empty `AGENTS.md` and `agents/`, `commands/`, `plugins/`, `skills/` directories,
and a minimal valid `opencode.json`:

Expand Down
20 changes: 20 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ workspaceRoot: /home/user/.local/share/opencode-manager
runtime: docker
useLocalOpenCodeAuth: false
extraCACertificate: []
workspaceEnv: {}
hostNetwork: false
runtimeArgs:
- --dns
Expand Down Expand Up @@ -106,6 +107,25 @@ workspace (the home is bind-mounted and modules reinstall idempotently).
> also has limited support on Docker Desktop (macOS/Windows); on Linux with
> docker/podman it works as expected.

### `workspaceEnv`

Environment variables passed to every workspace container. Values can be literal
or a reference to a variable in the manager host's environment:

```yaml
workspaceEnv:
LOG_LEVEL: debug
MY_VAR: "{env:LOCAL_VAR}"
```

`MY_VAR` is available in each workspace with the current host value of
`LOCAL_VAR`. The reference is resolved whenever a workspace is provisioned; a
missing host variable prevents the workspace from starting. Changes to literal
values, referenced host values, or configured keys recreate the workspace
container. Referenced values are not written to `workspace.yaml`, but they are
visible to users who can inspect the container runtime. Module exports in the
workspace's `~/.env` take precedence for the OpenCode process.

### `runtimeArgs`

Optional list of extra flags passed **verbatim** to the `docker`/`podman create`
Expand Down
14 changes: 8 additions & 6 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ npm install -g @mickaelroger78/opencode-manager

## 2. Add your OpenCode config

Every workspace shares the OpenCode configuration in your global config
directory. Copy your existing setup into it so all workspaces inherit it:
Every workspace receives a one-way copy of the shared OpenCode configuration.
Copy your existing setup into it so all workspaces inherit it:

```sh
cd ~/.config/opencode-manager
cd ~/.config/opencode-manager/opencode

cp /path/to/your/opencode.json .
cp -r /path/to/your/skills/* skills/
Expand All @@ -28,9 +28,11 @@ cp -r /path/to/your/plugins/* plugins/
cp /path/to/your/AGENTS.md .
```

These are **mounted read-only** into every workspace, so editing them on the
host updates all workspaces live. `ocm` creates this directory and an empty
`opencode.json` on first run, so the layout already exists.
`ocm` copies this tree to each workspace when it is provisioned, reconciles it
on startup, and watches it while active. Host changes win and workspace changes
never flow back. Generated workspace state such as lockfiles and `node_modules`
is preserved. `ocm` creates this directory and an empty `opencode.json` on first
run, so the layout already exists.

See [Concepts → Shared OpenCode config](concepts.md#shared-opencode-config) for
exactly what is shared and how.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.25.8
require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/fsnotify/fsnotify v1.9.0
github.com/spf13/cobra v1.10.2
go.yaml.in/yaml/v4 v4.0.0-rc.6
)
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNE
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
Expand Down
Loading
Loading