diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 678bec5..e9cf10d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,149 @@ jobs: - run: npm run build + installer: + # The only place a real scheduled job is ever registered: disposable + # runners. Installs the packed checkout through the real installer, links + # a local bare repository with every root redirected into the runner's + # temp directory, then installs, runs and removes the scheduler entry. + name: installer · ${{ matrix.os }} + runs-on: ${{ matrix.os }} + needs: check + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + env: + OPENCODE_SYNC_NO_GH: "1" + OPENCODE_SYNC_NO_STAGGER: "1" + OPENCODE_SYNC_LANG: en + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Configure git identity + shell: bash + run: | + git config --global user.name "CI" + git config --global user.email "ci@example.com" + git config --global init.defaultBranch main + + - name: Build and pack the checkout + shell: bash + run: | + npm ci + npm run build + npm pack + pkg="$(pwd)/$(ls opencode-github-sync-*.tgz)" + if [ "$RUNNER_OS" = "Windows" ]; then pkg=$(cygpath -w "$pkg"); fi + echo "PKG=$pkg" >> "$GITHUB_ENV" + + - name: Isolated roots and a local bare repository + shell: bash + run: | + base="$RUNNER_TEMP/ogs" + mkdir -p "$base/home" "$base/config" + git init -q --bare -b main "$base/remote.git" + printf '{ "model": "ci" }\n' > "$base/config/opencode.jsonc" + { + echo "SYNC_CONFIG_ROOT=$base/config" + echo "SYNC_DATA_ROOT=$base/data" + echo "SYNC_STATE_ROOT=$base/state" + echo "SYNC_AGENTS_ROOT=$base/agents/skills" + echo "SYNC_CACHE_ROOT=$base/cache" + echo "SYNC_HOME=$base/home" + echo "OGS_REMOTE=$base/remote.git" + echo "OGS_LOG=$base/config/logs/opencode-sync-autosync.log" + } >> "$GITHUB_ENV" + + - name: Run install.sh non-interactively + if: runner.os != 'Windows' + shell: bash + run: sh install.sh --mode link --repo "$OGS_REMOTE" --interval 5 --lang en --package "$PKG" --yes /dev/null \ + || crontab -l | grep -q "opencode-github-sync autosync" ;; + esac + + - name: The scheduler actually starts the job + shell: bash + run: | + if [ "$RUNNER_OS" = "Windows" ]; then schtasks //Run //TN opencode-github-sync; fi + if [ "$RUNNER_OS" = "Linux" ]; then + if systemctl --user is-enabled opencode-github-sync.timer >/dev/null 2>&1; then + systemctl --user start opencode-github-sync.service || true + else + echo "cron fallback: not waiting for the next minute boundary"; exit 0 + fi + fi + for _ in $(seq 1 45); do + if grep -q "scheduled:" "$OGS_LOG" 2>/dev/null; then cat "$OGS_LOG"; exit 0; fi + sleep 2 + done + echo "the scheduled job did not write to $OGS_LOG" + cat "$OGS_LOG" 2>/dev/null || true + cat "$SYNC_CONFIG_ROOT/logs/autosync-launchd.log" 2>/dev/null || true + if [ "$RUNNER_OS" = "Windows" ]; then + # An InteractiveToken task needs a signed-in desktop session, which + # a hosted runner may not provide. Report what Task Scheduler saw. + powershell -NoProfile -Command "Get-ScheduledTaskInfo -TaskName opencode-github-sync | Format-List" + echo "::warning::Task Scheduler did not start the job in this runner session" + exit 0 + fi + exit 1 + + - name: autosync run + shell: bash + run: | + opencode-sync autosync run --trigger ci + grep "ci:" "$OGS_LOG" + + - name: autosync uninstall removes the scheduler entry + shell: bash + run: | + opencode-sync autosync uninstall + case "$RUNNER_OS" in + Windows) ! schtasks //Query //TN opencode-github-sync >/dev/null 2>&1 ;; + macOS) test ! -f "$HOME/Library/LaunchAgents/com.opencode-github-sync.autosync.plist" + ! launchctl list | grep -q com.opencode-github-sync.autosync ;; + Linux) ! systemctl --user is-enabled opencode-github-sync.timer >/dev/null 2>&1 + ! crontab -l 2>/dev/null | grep -q "opencode-github-sync autosync" ;; + esac + + shellcheck: + name: shellcheck install.sh + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: shellcheck -s sh install.sh + package: name: package contents runs-on: ubuntu-latest @@ -68,3 +211,6 @@ jobs: test -f dist/plugin/index.js test -f dist/cli/index.js test -f dist/core/index.js + # Installers and README images are served from GitHub, not npm. + files=$(npm pack --dry-run --json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s)[0].files.map(f=>f.path).join("\n")))') + ! echo "$files" | grep -E '^(install\.(sh|ps1)|docs/)' diff --git a/CHANGELOG.md b/CHANGELOG.md index 07506e1..09fad75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,84 @@ [简体中文](./CHANGELOG.zh-CN.md) +## 3.2.0 + +### Added +- **One-line installers.** `install.ps1` (Windows PowerShell 5.1 and + PowerShell 7) and `install.sh` (macOS, Linux) check for Node.js 18+ and git, + offer to install a missing one through winget, Homebrew or the system + package manager (they ask first and never install silently), run + `npm install -g opencode-github-sync@latest`, and open the menu. `--mode`, + `--repo`, `--interval`, `--lang`, `--proxy` and `--yes` run the whole setup + non-interactively. Under `curl | sh` questions are read from the terminal, + and on Windows the menu runs as its own process so it gets the keyboard. +- **Interactive menu.** `opencode-sync` with no arguments (or `opencode-sync + menu`) opens a menu with arrow keys, hotkeys and checkboxes, and falls back + to numbered prompts whenever input is not a terminal. Every existing + subcommand still works and stays scriptable. First run offers: install the + command only, link a repository, or link and make this the primary device + with autosync. When the machine and the repository differ, it shows the + differences and lets you pick a side; the losing side's files go to a + recovery folder. +- **English and Simplified Chinese** for every menu and command string, + detected from `OPENCODE_SYNC_LANG`, `LC_ALL`/`LC_MESSAGES`/`LANG` or the + system locale, and switchable with `--lang` or from the menu. +- **Autosync driven by the operating system.** `opencode-sync autosync + install|uninstall|enable|disable|status|run|reset|pause|resume`. Task + Scheduler on Windows (logon, unlock, remote connect and a repeating timer; + hidden through `conhost --headless` or a UTF-16 VBScript launcher), launchd + on macOS, a systemd user timer on Linux with a crontab fallback. Each run + pushes only when the synced trees have been quiet for a settle period, and + otherwise receives only when this machine has nothing of its own in flight. + Machines are staggered so they do not collide, failures back off from 30 to + 60 minutes without ever giving up, a lost push race is retried rather than + counted, and a desktop notification appears only when backing off starts + and when it recovers. +- **What to sync** as checkboxes: main config, instruction files, agents, + commands, plugins, skills, credentials and sessions. A switched-off category + stays local in both directions. +- **Large-change guard.** A push that adds more than 300 files, more than + 10 MB, or any file over 2 MB waits for confirmation (thresholds are + adjustable). Autosync never confirms: it records a failure and notifies. + The first push into an empty repository carries the whole existing setup, + so only the single-file limit applies there. + Generated directories (`node_modules`, `.venv`, `venv`, `__pycache__`, + `dist`, `.cache`) are never staged. +- **Restore a file from history**, with the replaced version kept in the + recovery folder. +- **Device list** built from commit history, and renaming this machine. +- **Network proxy** for git, gh and npm, applied to child processes only. +- **Preview** of what a sync would change before running it. +- **Support bundle** export with tokens, passwords and URL credentials removed. +- **Update** menu: check npm, upgrade, and an optional daily auto-update run + by the autosync job. + +### Changed +- When autosync is on, the plugin no longer pushes on idle (it still pulls at + startup), so it never races the scheduled job. +- Sync commits carry an `Opencode-Sync-Device` trailer naming the machine. +- `link` accepts any git URL or local path in addition to `owner/name`. +- A forced pull, and the first pull onto a machine that already has files, copy + the files they replace into `opencode-sync-recovery` under the data + directory first. This covers new folders, non-ASCII file names, and local + skills and extra-path files that the forced pull removes or overwrites. + +### Fixed +- A push that loses a race with another machine rebases and retries up to three + times instead of failing. A protected branch or a declined hook is reported + as a failure rather than retried. +- The first push from a machine whose config directory is not yet a + repository now writes the remote's skills out before staging, so it can no + longer publish every skill it lacks as a deletion. +- Git runs with `core.quotepath=off` and English messages, so non-ASCII file + names count toward the large-change guard and a lost push race is + recognised on a localized git. + +### Contributors +- @vitaly74 — OpenCode 2 plugin entrypoint (#6, 3.1.0) +- @aquelejota — deterministic shard export and working session tests (#5), analysis of #2, #3 and #4 (3.1.0) +- @logser13 — reported #2, #3 and #4 with reproductions and tested fixes (3.1.x) + ## 3.1.1 ### Fixed diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 24f0d8b..49cab2d 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -2,6 +2,38 @@ [English](./CHANGELOG.md) +## 3.2.0 + +### 新增 +- **一行命令安装。** `install.ps1`(Windows PowerShell 5.1 和 PowerShell 7 都能用)和 `install.sh`(macOS、Linux)会检查 Node.js 18+ 和 git,缺了就提示用 winget、Homebrew 或系统包管理器安装(一定先问,绝不偷偷装),然后执行 `npm install -g opencode-github-sync@latest` 并打开菜单。带上 `--mode`、`--repo`、`--interval`、`--lang`、`--proxy`、`--yes` 可以全程无人值守完成设置。用 `curl | sh` 时问题从终端读取;在 Windows 上菜单作为独立进程启动,键盘输入不会被管道吃掉。 +- **交互菜单。** 直接运行 `opencode-sync`(或 `opencode-sync menu`)会打开菜单,支持方向键、快捷键和勾选框;输入不是终端时自动改用编号提问。原有子命令全部保留,照样可以写进脚本。第一次运行时可以选:只装命令、关联仓库、关联并把本机设为开启自动同步的主力设备。本机和仓库内容不一样时会先列出差异让你选以哪边为准,被替换的一方会先存进恢复目录。 +- **英文和简体中文界面**,覆盖所有菜单和命令输出。语言按 `OPENCODE_SYNC_LANG`、`LC_ALL`/`LC_MESSAGES`/`LANG`、系统区域设置依次判断,也可以用 `--lang` 或在菜单里切换。 +- **由操作系统驱动的自动同步。** 新增 `opencode-sync autosync install|uninstall|enable|disable|status|run|reset|pause|resume`。Windows 用任务计划程序(登录、解锁、远程连接时触发,外加定时重复;通过 `conhost --headless` 或 UTF-16 编码的 VBScript 隐藏窗口),macOS 用 launchd,Linux 用 systemd 用户定时器,不可用时退回 crontab。每次运行只在同步目录安静一段时间后才推送;还有文件在变化时,只有本机没有待处理的改动才会接收。多台机器会错开时间避免撞车;连续失败后退避间隔从 30 分钟加到 60 分钟,但永不放弃;推送被别的机器抢先不算失败,会自动重试;桌面通知只在开始退避和恢复正常时各弹一次。 +- **同步内容勾选**:主配置、指令文件、智能体、自定义命令、插件、技能、登录凭据、会话。关掉的类别双向都只留在本机。 +- **大改动保护。** 一次推送新增超过 300 个文件、总量超过 10 MB,或有单个文件超过 2 MB 时,要先确认(阈值可调)。自动同步从不自动确认,只记一次失败并发通知。往空仓库的第一次推送本来就要带上全部现有配置,所以只检查单个文件大小。`node_modules`、`.venv`、`venv`、`__pycache__`、`dist`、`.cache` 这类生成目录永远不会提交。 +- **从历史版本恢复单个文件**,被替换的版本保存在恢复目录里。 +- **设备列表**,根据提交历史显示每台机器上次同步的时间,并可以给本机改名。 +- **网络代理**,只作用于 opencode-sync 启动的 git、gh 和 npm。 +- **同步前预览**会改动哪些文件。 +- **导出排障包**,自动去掉令牌、密码和 URL 里的账号密码。 +- **更新**菜单:检查 npm 新版本、立即升级,以及可选的每日自动更新(由自动同步任务执行)。 + +### 变更 +- 开启自动同步后,插件不再在会话空闲时推送(启动时仍然拉取),避免和定时任务抢着同步。 +- 同步提交带上 `Opencode-Sync-Device` 尾注,记录是哪台机器提交的。 +- `link` 除了 `用户名/仓库名`,也接受任意 git 地址或本地路径。 +- 强制拉取,以及本机已有文件时的第一次拉取,会先把将被替换的文件复制到数据目录下的 `opencode-sync-recovery`。新建的整个文件夹、中文等非 ASCII 文件名,以及强制拉取会删除或覆盖的本机技能和额外路径文件,也都包括在内。 + +### 修复 +- 推送时被别的机器抢先,会自动变基并最多重试三次,而不是直接失败。分支受保护或被钩子拒绝时会如实报错,不会反复重试。 +- 配置目录还不是仓库的机器第一次推送时,会先把仓库里的技能写回本机再打包,不会再把本机缺少的技能当成删除推送出去。 +- git 改为以 `core.quotepath=off` 和英文提示运行:中文文件名会正确计入大改动保护,非英文系统上也能识别推送被抢先的情况。 + +### Contributors +- @vitaly74 — OpenCode 2 plugin entrypoint (#6, 3.1.0) +- @aquelejota — deterministic shard export and working session tests (#5), analysis of #2, #3 and #4 (3.1.0) +- @logser13 — reported #2, #3 and #4 with reproductions and tested fixes (3.1.x) + ## 3.1.1 ### 修复 diff --git a/README.md b/README.md index 49c1b5a..db18ad1 100644 --- a/README.md +++ b/README.md @@ -1,175 +1,227 @@

- opencode-github-sync + opencode-github-sync: one config, every machine

npm CI - license + license platform

- 简体中文 + 简体中文  ·  - Quick start + Install  ·  - Overrides + Setup  ·  - Sessions + Autosync  ·  Commands +  ·  + Troubleshooting

--- -You use OpenCode on more than one machine. Your config, agents, commands, skills -and MCP servers live on whichever one you touched last. - -**opencode-github-sync** keeps them all in step through a private GitHub -repository — as an OpenCode plugin, as a CLI, or both. - -```bash -npm install -g opencode-github-sync -opencode-sync init # creates the private repo and configures this machine -opencode-sync push -``` - -

- opencode-sync push -

- ---- - -## Why another sync tool - -Most config-sync tools stop being safe the moment real use hits them. These are -the three places that happens, and what this one does instead. - -| | Typical approach | Here | -| ------------------ | ----------------------------------------------- | -------------------------------------------------------------------------- | -| **Machine differences** | Every machine forced identical | [Per-machine overrides](#per-machine-overrides) that sync can never overwrite | -| **Sessions** | Commit `opencode.db` — gigabytes, unmergeable | [Selective per-session shards](#session-sync), a few MB, conflict-free | -| **When it breaks** | Plugin-only: bad config means no way back | The CLI runs outside OpenCode and can always recover you | +You use OpenCode on more than one computer. Your config, agents, commands, +skills and MCP servers end up wherever you last touched them. -Plus the boring things that turn out to matter: +**opencode-github-sync** keeps every machine on the same setup through a +private GitHub repository. Run it by hand, or let one machine sync itself in +the background. -- **Symlinks are refused, not followed.** No copying files out of the machine by accident, no junction turning a delete into the wrong delete. -- **Every replacement is atomic.** A crash mid-sync cannot leave a half-written config that stops OpenCode from starting. -- **A push is verified against the remote.** `git push` can exit `0` without the remote moving; that is checked, not assumed. -- **Nothing runs twice at once.** A cross-process lock covers the plugin, the CLI and every OpenCode window. -- **Credentials are opt-in** and refused outright on a public repository. -- **Your hostname never leaves the machine.** Commits are signed with a stable pseudonym, so a corporate asset tag never lands in a repo that might not stay private. +## Install ---- +**macOS / Linux** -## Install +```bash +curl -fsSL https://raw.githubusercontent.com/doomsday616/opencode-github-sync/main/install.sh | sh +``` -### As an OpenCode plugin +**Windows** (PowerShell 5.1 or 7) -```jsonc -// ~/.config/opencode/opencode.json -{ - "$schema": "https://opencode.ai/config.json", - "plugin": ["opencode-github-sync"] -} +```powershell +irm https://raw.githubusercontent.com/doomsday616/opencode-github-sync/main/install.ps1 | iex ``` -This `"plugin"` form works on both OpenCode 1.x (1.3.4 and later) and -OpenCode 2.x: 1.x reads only `"plugin"`, and 2.x still accepts it alongside its -native `"plugins"` key. If you run only OpenCode 2, you can list it under -`"plugins"` instead. Do not list the plugin under both keys — OpenCode 2 loads -entries from both lists. +The installer checks for Node.js 18+ and git. If one is missing it shows the +command that would install it and asks before running anything. Then it runs +`npm install -g opencode-github-sync@latest` and opens the menu. -OpenCode 1.0–1.3.3 cannot load 3.1.0 and later; stay on -`opencode-github-sync@3.0.1` there. +Already have Node.js? `npm install -g opencode-github-sync` and then +`opencode-sync` does the same thing. -OpenCode installs it on the next start. The plugin adds a pull on startup, an -optional push when a session goes idle, and an `opencode_sync` tool so you can -just ask for a sync in plain language. +### Set up a machine in one line -### As a CLI +Pass options to skip the questions, for example when provisioning a new +machine: ```bash -npm install -g opencode-github-sync +curl -fsSL https://raw.githubusercontent.com/doomsday616/opencode-github-sync/main/install.sh | sh -s -- --mode auto --repo you/my-opencode-config ``` -Keep the CLI even if you use the plugin. A plugin cannot fix the configuration -that stops OpenCode from loading the plugin. +```powershell +& ([scriptblock]::Create((irm https://raw.githubusercontent.com/doomsday616/opencode-github-sync/main/install.ps1))) -Mode auto -Repo you/my-opencode-config +``` -**Requirements** — git, and [`gh`](https://cli.github.com) for automatic setup. -Node 18+ for config sync; Node 22.5+ or Bun for [session sync](#session-sync). +| Option | Meaning | +| --- | --- | +| `--mode cli` / `-Mode cli` | Install the command only | +| `--mode link` | Link `--repo` and sync once | +| `--mode auto` | Link, sync once and turn on autosync | +| `--repo owner/name` | The repository (a git URL works too); created as private if it does not exist | +| `--interval N` | Autosync every N minutes (minimum 2, default 5) | +| `--lang en` / `zh-CN` | Interface language | +| `--proxy URL` | Proxy for npm and git, during install and for later syncs | +| `--yes` | Install missing Node.js or git without asking | +| `--package SPEC` | npm package spec to install (default `opencode-github-sync@latest`) | +| `--dry-run` | Print what would run, change nothing | + +On Windows each option is the same word with a leading dash: `-Repo`, +`-Interval`, `-Lang`, `-Proxy`, `-Yes`, `-Package`, `-DryRun`. + +When the repository already has content and this machine also has its own +files, setup takes the repository's version and keeps the replaced files in a +[recovery folder](#safety-features). --- -## Quick start +## Pick your setup -### First machine +

+ Command only, or primary device with autosync +

-```bash -opencode-sync init # → /my-opencode-config -opencode-sync init team-config # custom name -opencode-sync init my-org/shared # inside an organisation -opencode-sync push -``` +The first time you run `opencode-sync` it asks how this machine should work. -`init` creates a **private** repository, writes -`~/.config/opencode/opencode-sync.jsonc`, and leaves an empty overrides file -ready for you. +| | Command only | Primary device + autosync | +| --- | --- | --- | +| How it syncs | You run `opencode-sync push` / `pull`, or the plugin pulls at startup | The operating system runs a sync every few minutes, even when OpenCode is closed | +| Good for | A machine you use now and then, a shared or locked-down machine | The machines you work on every day | +| Background job | None | Task Scheduler, launchd or a systemd timer | -### Every other machine +You can change your mind any time from the **Autosync** menu. Autosync is +opt-in per machine: turning it on here never turns it on anywhere else. -```bash -opencode-sync link doomsday616/my-opencode-config -opencode-sync pull -``` +--- -Then restart OpenCode. +## The menu -### Day to day +Run `opencode-sync` with no arguments. -```bash -opencode-sync push # share what changed here -opencode-sync pull # take what changed elsewhere -opencode-sync status # what is in sync, what is not -``` +

+ The opencode-sync menu +

+ +Arrow keys or the shown hotkey pick an item, space ticks a checkbox, Enter +confirms and Esc goes back. When the input is not a terminal (a pipe, a script, +CI) every prompt becomes a numbered question, so the menu can be driven from a +file. + +| Item | What you can do there | +| --- | --- | +| **Sync now** | Push and receive, receive only, push only, or preview first | +| **What to sync** | Tick main config, instruction files, agents, commands, plugins, skills, credentials, sessions; set session limits; add extra paths | +| **Autosync** | Turn it on or off, choose 5/10/15/30/60 minutes or your own interval, pause it, run while signed out (Windows), notifications | +| **Update** | Check npm for a newer version, upgrade, update automatically once a day | +| **This machine's overrides** | Open the overrides file in your editor, see what it changes | +| **Diagnostics** | git, node, gh, repository access, the scheduled job, the last log lines; export a support bundle | +| **Danger zone** | Make this machine match the repository, or the other way round (you type `overwrite` to confirm) | +| **Uninstall** | Remove the scheduled job, optionally unlink and remove the command. Your configuration is never deleted | +| **Network proxy** | A proxy for git, gh and npm, used only by opencode-sync | +| **Restore a file from history** | Pick a file, pick a version, put it back | +| **Devices** | Every machine that has synced and when, and renaming this one | +| **Large-change guard** | The limits that make a big push stop and ask | +| **Language** | English, 简体中文 or automatic | + +Every command from earlier versions still works and is still scriptable. --- ## What gets synced

- Architecture + Each machine pushes and pulls through one private repository

-| Source | In the repo | Contents | -| -------------------------- | ----------- | -------------------------------------------- | -| `~/.config/opencode/` | repo root | config, agents, commands, skills, themes, MCP | -| `~/.local/share/opencode/` | `_data/` | project metadata, optional credentials | -| `~/.local/state/opencode/` | `_state/` | frecency, model cache, prompt history | -| `~/.agents/skills/` | `_agents/` | skills installed by the `skills` CLI | -| selected sessions | `_sessions/`| one gzipped shard per session | +Your `~/.config/opencode` folder becomes a working copy of the repository. +Everything else is mirrored into a folder inside it. -**Never synced:** `opencode.db` and its journals, tool output, snapshots, logs, -and your local settings and overrides files. +| On your machine | In the repository | What it holds | +| --- | --- | --- | +| `~/.config/opencode/` | the root | config, agents, commands, plugins, themes, instruction files | +| `~/.local/share/opencode/` | `_data/` | project metadata, and credentials if you opt in | +| `~/.local/state/opencode/` | `_state/` | model cache, recent files, prompt history | +| `~/.agents/skills/` | `_agents/` | skills installed with the `skills` CLI | +| selected sessions | `_sessions/` | one small compressed file per session | +| your extra paths | `_extra/` | anything else you add | -Add anything else with `extraPaths`: +Each part can be switched off under **What to sync**. A part you switch off +stays local in both directions: your edits are not sent, and other machines' +versions do not replace yours. -```jsonc -{ - "extraPaths": [".tavily", ".config/gh/config.yml"] -} -``` +**Never synced:** the `opencode.db` database, tool output, snapshots, logs, +`node_modules` and other generated folders, and opencode-sync's own settings +and overrides files. --- -## Per-machine overrides +## How autosync works -A synced repository makes every machine identical. That is exactly wrong for the -settings that are genuinely machine-specific: a corporate proxy, a local -toolchain path, an MCP server that only exists on one box. +

+ Each machine syncs on its own schedule, waits while you edit, and backs off on errors +

+ +When autosync is on, the operating system starts `opencode-sync autosync run` +every few minutes and at sign-in (on Windows also when you unlock or reconnect +over Remote Desktop). Each run makes one decision: + +1. **Is it quiet?** If any file in your config or skills folders changed in the + last minute, something is still being written. Sending now could publish a + half-saved file, so the push waits. +2. **Can it at least receive?** While busy, it still takes other machines' + changes, but only when this machine has nothing of its own waiting. + Otherwise it leaves everything for the next run. +3. **Push and receive.** Your changes go up, other machines' changes come down, + in one step. + +The rules that keep this safe: + +- **Machines take turns.** Each machine waits a fixed few seconds based on its + name, so two machines on the same schedule do not collide every time. If + another machine still gets there first, this one picks up the new commit and + tries again; that does not count as an error. +- **It backs off, but never gives up.** After three failures in a row + (typically no network) it waits 30 minutes, then an hour between attempts. + The first success puts it back on schedule. +- **It only interrupts you twice.** One notification when it starts backing + off, one when it recovers. Nothing for ordinary syncs. +- **It never runs twice at once.** A lock is shared by the scheduled job, the + CLI, the menu and every OpenCode window; if one is busy, the others skip. +- **It never confirms a large change.** Anything that trips the + [large-change guard](#safety-features) is held back and you get a + notification. + +| Platform | Scheduler | Notes | +| --- | --- | --- | +| Windows | Task Scheduler | Runs hidden. To run while signed out, turn that on in the menu and install from an administrator terminal | +| macOS | launchd agent | `~/Library/LaunchAgents/com.opencode-github-sync.autosync.plist` | +| Linux | systemd user timer | Falls back to a crontab line. On a server, run `loginctl enable-linger` so it runs while you are logged out | + +`opencode-sync autosync status` shows the last run, the last error and when it +will try next. With autosync on, the OpenCode plugin stops pushing on idle so +the two never compete; it still pulls when OpenCode starts. -Put those in `~/.config/opencode/opencode-sync.overrides.jsonc`: +--- + +## Per-machine overrides + +Some settings belong to one machine only: a work proxy, a local tool path, an +MCP server that only exists on one box. Put them in +`~/.config/opencode/opencode-sync.overrides.jsonc`, or use **This machine's +overrides** in the menu: ```jsonc { @@ -181,175 +233,244 @@ Put those in `~/.config/opencode/opencode-sync.overrides.jsonc`: ``` ``` - repo opencode.jsonc shared baseline, committed + repo opencode.jsonc shared, committed + overrides.jsonc this machine only, never committed - ───────────────────────── deep merge - = effective opencode.jsonc what OpenCode actually reads + ───────────────────────── merged + = effective opencode.jsonc what OpenCode reads ``` -Merge rules: objects merge key by key, arrays and scalars replace, `null` -deletes a key. - -The half that other tools get wrong is **push**. Because the effective file is -what sits on disk, a naive push uploads your overrides to everyone. Here, any -key the overrides file claims is restored to the value the repository already -had before committing — so an overridden key is invisible to sync in both -directions, and your machine keeps its own settings the whole time. - -> With no overrides file the config is copied byte for byte and your comments -> and formatting are untouched. The structural rewrite only happens once you -> actually opt in. +Objects merge key by key, lists and plain values replace, and `null` removes a +key. Before every push, any key the overrides file claims is put back to the +repository's value, so your machine-only values never reach other machines. +Without an overrides file, the config is copied exactly as written, comments +and all. --- -## Session sync +## Sessions -Off by default. Turn it on only for a private repository — conversations are -private data. +Off by default. Only turn it on for a private repository: conversations are +private. ```bash opencode-sync sessions enable opencode-sync sessions list -opencode-sync sessions include ses_065cad1caffeN3lf1RgLZno30 # pin one forever +opencode-sync sessions include ses_065cad1caffeN3lf1RgLZno30 # always sync this one opencode-sync sessions exclude ses_0ba19b4e7ffeMbSmWYi3Nj6xfZ # never sync this one ``` -### Why not just commit the database +Sessions are not synced by committing the database. A real OpenCode database +grows to several gigabytes, mostly tool output, and git cannot merge it. Each +session is exported on its own as a small compressed file instead: + +- **Small.** Only recent sessions (7 days and 50 sessions by default), and any + single session over 5 MB is skipped. +- **No conflicts in practice.** Two machines working on different sessions + touch different files. The same session edited in two places keeps the newer + version. +- **Safe to import.** Importing runs in one transaction, so a failure leaves + your database as it was. + +Window, count and size limits are under **What to sync → Session settings**. +Session sync needs Node.js 22.5 or newer. -A real OpenCode database reaches several gigabytes, almost all of it tool output -inside `part.data`. Git cannot delta-compress it, GitHub's LFS free tier is one -gigabyte, and copying a live database alongside its write-ahead log can capture -a torn state that only shows up much later. +--- -So sessions are exported **one at a time**. Each becomes an independent gzipped -JSON shard holding its own rows plus the project and workspace rows it needs: +## Safety features -- **No large files.** Shards are small and compress well. -- **Conflicts isolate themselves.** Two machines working on different sessions - touch different files, so git merges them with no special handling. The only - real conflict is one session edited in two places, and there the newer - `time_updated` wins — an in-progress conversation is never clobbered by a - stale copy. -- **You choose what travels.** A time window, an explicit include list, a - project filter and a per-session size cap. Ancient sessions stay where they - are. +

+ One sync at a time, all-or-nothing writes, restore earlier versions, large-change guard +

-Import is an upsert inside a transaction, so a failure leaves your database -exactly as it was. +- **Atomic replace.** Files are written beside their target and then swapped + in, so a crash mid-sync cannot leave a half-written config that stops + OpenCode from starting. +- **Push verification.** After pushing, the remote is read back. `git push` + can report success without the remote changing; that is checked, not + assumed. +- **One sync at a time.** A lock shared by the plugin, the CLI, the menu and + the scheduled job. +- **Large-change guard.** A push that adds more than 300 files, more than + 10 MB in total, or any single file over 2 MB stops and asks. The limits are + adjustable. Generated folders such as `node_modules`, `.venv`, `venv`, + `__pycache__`, `dist` and `.cache` are never sent. +- **Recovery folder.** Before a forced pull, a "repository wins" first link, + or a restore from history, the files about to be replaced are copied to + `~/.local/share/opencode/opencode-sync-recovery/