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
15 changes: 15 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ jobs:
mise run test
mise run build

- name: Assert dual v1/v2 entrypoint
run: |
bun -e '
const plugin = await import("./dist/index.js");
const entry = plugin.default ?? {};
for (const key of ["id", "setup", "server"]) {
if (typeof entry[key] !== "string" && typeof entry[key] !== "function") {
throw new Error(`dist default export is missing v1/v2 entrypoint member: ${key}`);
}
}
if (entry.id !== "opencode-synced") throw new Error(`unexpected plugin id: ${entry.id}`);
if (typeof plugin.opencodeConfigSync !== "function") throw new Error("missing v1 opencodeConfigSync export");
console.log("Dual v1/v2 entrypoint OK:", entry.id);
'

WindowsPaths:
runs-on: windows-latest
steps:
Expand Down
31 changes: 26 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ an explicit-URL path for pre-created remotes.
- Git installed and available on PATH
- GitHub CLI (`gh`) installed and authenticated (`gh auth login`) when using automatic GitHub
creation, discovery, or privacy verification
- opencode v1 `>= 1.18.29` **or** opencode v2 `^2.0.0` (one package supports both runtimes)

## Setup

Enable the plugin in your global opencode config (opencode will install it on next run):
Enable the plugin in your global opencode config (opencode will install it on next run).

For opencode v1:

```jsonc
{
Expand All @@ -30,27 +33,45 @@ Enable the plugin in your global opencode config (opencode will install it on ne
}
```

For opencode v2:

```jsonc
{
"$schema": "https://opencode.ai/config.json",
"plugins": ["opencode-synced"],
}
```

opencode does not auto-update plugins. To update, modify the version number in your config file.

On v2, use the `opencode_sync` tool for sync operations. The `/sync-*` slash
commands are available on v1 only. V2 cannot display a slash command's direct
result without placing it in the model's prompt queue, which could trigger
another operation. For example, ask OpenCode to call `opencode_sync` with
`{"command":"status"}` to inspect the current state.

## Configure

### First machine (create new sync repo)

Run `/sync-init` to create a new sync repo:
On v1, run `/sync-init` to create a new sync repo. On v2, ask OpenCode to call
`opencode_sync` with `{"command":"init"}`:

1. Detects your GitHub username
2. Creates a private repo (`my-opencode-config` by default)
3. Clones the repo and pushes your current config

### Additional machines (link to existing repo)

Run `/sync-link` to connect to your existing sync repo:
On v1, run `/sync-link` to connect to your existing sync repo. On v2, ask
OpenCode to call `opencode_sync` with `{"command":"link"}`:

1. Searches your GitHub for common sync repo names (prioritizes `my-opencode-config`)
2. Clones and applies the synced config
3. **Overwrites local config** with synced content (preserves your local overrides file)

If auto-detection fails, specify the repo name: `/sync-link my-opencode-config`
If auto-detection fails, specify the repo name with `/sync-link my-opencode-config`
on v1 or `{"command":"link","repo":"my-opencode-config"}` on v2.

After linking, restart opencode to apply the synced settings.

Expand Down Expand Up @@ -353,7 +374,7 @@ bun -e '
```

### Manual steps
1. Remove `"opencode-synced"` from the `plugin` array in `~/.config/opencode/opencode.json` (or `.jsonc`).
1. Remove `"opencode-synced"` from the `plugin` array in `~/.config/opencode/opencode.json` (or `.jsonc`; v2 uses the `plugins` key).
2. Delete the local configuration and state:
```bash
rm ~/.config/opencode/opencode-synced.jsonc
Expand Down
579 changes: 575 additions & 4 deletions bun.lock

Large diffs are not rendered by default.

86 changes: 86 additions & 0 deletions docs/v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
V1 + V2 plugin support

Goal
One package supporting both opencode v1 (`@opencode-ai/plugin`, `plugin` config
key) and v2 (`@opencode/plugin`, `plugins` config key), per
https://opencode.ai/v2/docs/build/plugins/migrate-v1.
Minimums: v1 >= 1.18.29 (object entrypoints), v2 ^2.0.0. Both packages are
runtime dependencies because the dual entrypoint (`src/index.ts`) imports both.

Why v2 does config differently
V1 hands plugins one mutable config object (`config(config)` in `src/index.ts`).
V2 replaces it with replayable, synchronous, per-domain transforms (core replays
all transforms in order onto a fresh value). So there is no global-merge shim:
each override key maps to the domain that owns it. Overrides are loaded +
`{env:…}`-resolved once in `setupV2` (`src/v2.ts`) before registering;
transform callbacks are pure snapshots (no I/O, no logging) so replays are
side-effect-free. Disk is never re-read inside a transform; an overrides change
needs a host restart (or host-triggered `reload()` — we do not call `reload()`
and run no file watcher by design).

Overrides mapping
Override key V2 destination Notes
mcp `ctx.mcp.transform` (set/update per server) Unresolvable `{env:…}`: store secret-free copy (`blankEnvPlaceholders` in `src/sync/config.ts`) with `disabled:true` + `console.error`. Mirrors v1 `disableMcpServerForResolutionFailure` intent without mutating caller state.
agent `ctx.agent.transform` (update-only) Editor cannot create agents; unknown IDs warn once outside the transform, skip silently inside.
model `ctx.model.transform` (update-only) Non-object shapes warn; unknown provider IDs warn via `ctx.provider.list()` best-effort; unknown model IDs skip silently (no bulk "has" API).
provider `ctx.provider.transform` (update-only) Unknown IDs warn once outside via `ctx.provider.list()`, skip silently inside.
command Ignored with warn V2 uses the `opencode_sync` tool. Slash commands and external `command` overrides are not registered.
everything else `console.warn` with key name + docs pointer No fake global merge.

File-level behavior (`syncRepoToLocal`/`syncLocalToRepo`, `stripOverrides` in
`src/sync/apply.ts`, `src/sync/config.ts`) is runtime-independent and unchanged.

V2 command + tool limits
- V2 does not register `/sync-*` slash commands. Its `CommandDefinition`
callback has no direct result channel. Posting command output with
`session.synthetic` resumes the model as if the output were a new user prompt;
even `/sync-status` can then prompt a follow-up sync operation. Using
`resume: false` prevents that operation but leaves the output in the inbox
rather than the visible session transcript. Use the `opencode_sync` tool,
which returns the result as tool content.
- `Tool.Result.content` accepts `string | Content[]`; we return a plain string
to keep status output readable.

AI commit messages (kosher v2)
V1 throwaway-session flow (`session.create → prompt → delete` in
`src/sync/ai.ts`) is replaced with `ctx.model.default()` +
`ctx.generate.text({model, prompt})` (`createV2AiProvider` in `src/v2.ts`).
Same prompts/sanitization (72-char single line in `src/sync/ai.ts`,
`src/sync/commit.ts`); fallback `Sync opencode config (YYYY-MM-DD)`. Applies to
`generateCommitMessage` and `/sync-resolve` analysis. Returns `null` (→ fallback)
when no model is available.

Implementation map
1. Shared core — `src/shared.ts`: md loading (`loadCommands`), tool args
(`SYNC_TOOL_COMMANDS`, `buildSyncToolInputSchema`), `executeSyncCommand`.
2. `src/shell-node.ts` — Node `child_process.exec` (`/bin/sh`) `$` shim
(`quiet()`/`text()`/throw-on-nonzero; v2 has no `$`). POSIX-only, inherits
host env, 32 MiB `maxBuffer`, `exec`-shaped errors (not Bun `ShellError`).
3. `src/v2.ts` — `setupV2`: console-prefixed logging (`v2Log/v2Warn/v2Error`;
no-op toast — v2 has neither toast nor log sink), session-status facade
returns `{data:{}}` (empty = idle → Turso idle-gating intentionally skipped,
syncs immediately), AI via `generate.text`. Registers tool (JSON Schema,
`required:["command"]`), mcp/agent/model/provider transforms,
`event.subscribe` → `service.handleEvent`, timed startup sync with dispose
cleanup (`clearTimeout` + `abort` + `service.dispose()` which stops the
Turso sync loop/idle-flush timers).
4. Dual entrypoint (`src/index.ts`) — `opencodeConfigSync` untouched; explicit
default export `{ id, setup: setupV2, server }` (no spread so runtimes do not
leak fields). Deps: `@opencode-ai/plugin ^1.18.29`, `@opencode/plugin ^2.0.0`.
README documents minimum versions.
5. Tests — `src/v2.test.ts`: dual export shape, mock-ctx tool/mcp/event
registration, status round-trip without slash commands or synthetic messages,
missing-env disables server, transform replay purity (no warn on second
replay), Turso immediate-sync (empty status = idle). Existing v1 tests unchanged.
6. Local verify — `bun install`, `bun run check`, `bun test`, `bun run build`;
manual packed-tarball smoke on opencode v1 (`plugin`) and v2 (`plugins`).
7. CI — lint, unit tests, build, and Windows path tests. Packed v1/v2 host
installation is a manual verification step, not a CI matrix job.

Risks / known parity gaps
- Non-MCP runtime keys warn-only in v2 (no global merge possible).
- No toasts in v2 (console + `app.log` facade); no session-idle gating (empty
status map = idle → immediate Turso sync).
- AI messages fall back to static when no model is available.
- V2 sync operations require the `opencode_sync` tool; v1 keeps `/sync-*` commands.
- Overrides require restart; no watcher/`reload()` calls.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
},
"files": ["dist"],
"dependencies": {
"@opencode-ai/plugin": "1.0.85"
"@opencode-ai/plugin": "^1.18.29",
"@opencode/plugin": "^2.0.0"
},
"devDependencies": {
"@biomejs/biome": "2.3.10",
Expand Down
Loading
Loading