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
16 changes: 14 additions & 2 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,6 +33,15 @@ 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.

## Configure
Expand Down Expand Up @@ -350,7 +362,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.

89 changes: 89 additions & 0 deletions docs/v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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.model.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 Sync commands are owned by this plugin and registered via `ctx.command.transform` from `src/command/*.md`. External `command` overrides are not applied.
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 (documented, not bugs)
- `CommandDefinition` only carries `name/description/execute` — unlike v1
`config.command` there is no `template/agent/model/subtask`. The md template
is executed directly: run service → post via `ctx.session.synthetic`.
- Slash commands only carry free text (`prompt.text`). Only a single bare
`owner/repo` (or URL) is parsed (`parseCommandRepoArg` in `src/v2.ts` takes
the first token, strips quotes/`$ARGUMENTS`); `init`/`link` extra flags and
`enable-secrets`/Turso options are not parseable from slash text — use the
`opencode_sync` tool for full args.
- `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"]`), commands (parse bare repo arg, post via
`session.synthetic`), 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/command/mcp/event
registration, status round-trip, missing-env disables server, transform
replay purity (no warn on second replay), Turso immediate-sync (empty status
= idle), `parseCommandRepoArg` edge cases. 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 — build assertion that `dist` exposes both `setup` and `server`; unit
tests; matrix smoke (v1 + v2 installs vs packed plugin, fail on early exit).

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.
- Slash commands support bare repo/backend token only; full options require the
`opencode_sync` tool.
- 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