opencode-synced 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -16
- package/dist/command/sync-link.md +2 -2
- package/dist/command/sync-pull.md +2 -2
- package/dist/command/sync-push.md +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -4
- package/dist/sync/apply.js +4 -4
- package/dist/sync/commit.js +2 -2
- package/dist/sync/config.d.ts +1 -0
- package/dist/sync/config.js +12 -1
- package/dist/sync/lock.d.ts +17 -0
- package/dist/sync/lock.js +91 -0
- package/dist/sync/mcp-secrets.js +1 -1
- package/dist/sync/paths.js +1 -1
- package/dist/sync/service.js +43 -20
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# opencode-synced
|
|
2
2
|
|
|
3
|
-
Sync global
|
|
3
|
+
Sync global opencode configuration across machines via a GitHub repo, with optional secrets support for private repos.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- Syncs global
|
|
7
|
+
- Syncs global opencode config (`~/.config/opencode`) and related directories
|
|
8
8
|
- Optional secrets sync when the repo is private
|
|
9
9
|
- Optional session sync to share conversation history across machines
|
|
10
10
|
- Optional prompt stash sync to share stashed prompts and history across machines
|
|
@@ -19,7 +19,7 @@ Sync global OpenCode configuration across machines via a GitHub repo, with optio
|
|
|
19
19
|
|
|
20
20
|
## Setup
|
|
21
21
|
|
|
22
|
-
Enable the plugin in your global
|
|
22
|
+
Enable the plugin in your global opencode config (opencode will install it on next run):
|
|
23
23
|
|
|
24
24
|
```jsonc
|
|
25
25
|
{
|
|
@@ -28,12 +28,7 @@ Enable the plugin in your global OpenCode config (OpenCode will install it on ne
|
|
|
28
28
|
}
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
rm -rf ~/.cache/opencode/node_modules/opencode-synced
|
|
35
|
-
opencode
|
|
36
|
-
```
|
|
31
|
+
opencode does not auto-update plugins. To update, modify the version number in your config file.
|
|
37
32
|
|
|
38
33
|
## Configure
|
|
39
34
|
|
|
@@ -55,7 +50,7 @@ Run `/sync-link` to connect to your existing sync repo:
|
|
|
55
50
|
|
|
56
51
|
If auto-detection fails, specify the repo name: `/sync-link my-opencode-config`
|
|
57
52
|
|
|
58
|
-
After linking, restart
|
|
53
|
+
After linking, restart opencode to apply the synced settings.
|
|
59
54
|
|
|
60
55
|
### Custom repo name or org
|
|
61
56
|
|
|
@@ -106,7 +101,7 @@ in a private repo, set `"includeMcpSecrets": true` (requires `includeSecrets`).
|
|
|
106
101
|
|
|
107
102
|
### Sessions (private repos only)
|
|
108
103
|
|
|
109
|
-
Sync your
|
|
104
|
+
Sync your opencode sessions (conversation history from `/sessions`) across machines by setting `"includeSessions": true`. This requires `includeSecrets` to also be enabled since sessions may contain sensitive data.
|
|
110
105
|
|
|
111
106
|
```jsonc
|
|
112
107
|
{
|
|
@@ -163,8 +158,8 @@ If you want MCP secrets committed (private repos only), set `"includeMcpSecrets"
|
|
|
163
158
|
Env var naming rules:
|
|
164
159
|
|
|
165
160
|
- If the header name already looks like an env var (e.g. `CONTEXT7_API_KEY`), it is used directly.
|
|
166
|
-
- Otherwise: `
|
|
167
|
-
- OAuth client secrets use `
|
|
161
|
+
- Otherwise: `opencode_mcp_<SERVER>_<HEADER>` (non-alphanumerics become `_`).
|
|
162
|
+
- OAuth client secrets use `opencode_mcp_<SERVER>_OAUTH_CLIENT_SECRET`.
|
|
168
163
|
|
|
169
164
|
## Usage
|
|
170
165
|
|
|
@@ -183,7 +178,7 @@ Env var naming rules:
|
|
|
183
178
|
|
|
184
179
|
### Trigger a sync
|
|
185
180
|
|
|
186
|
-
Restart
|
|
181
|
+
Restart opencode to run the startup sync flow (pull remote, apply if changed, push local changes if needed).
|
|
187
182
|
|
|
188
183
|
### Check status
|
|
189
184
|
|
|
@@ -270,7 +265,7 @@ bun -e '
|
|
|
270
265
|
### Local testing (production-like)
|
|
271
266
|
|
|
272
267
|
To test the same artifact that would be published, install from a packed tarball
|
|
273
|
-
into
|
|
268
|
+
into opencode's cache:
|
|
274
269
|
|
|
275
270
|
```bash
|
|
276
271
|
mise run local-pack-test
|
|
@@ -284,7 +279,7 @@ Then set `~/.config/opencode/opencode.json` to use:
|
|
|
284
279
|
}
|
|
285
280
|
```
|
|
286
281
|
|
|
287
|
-
Restart
|
|
282
|
+
Restart opencode to pick up the cached install.
|
|
288
283
|
|
|
289
284
|
|
|
290
285
|
## Prefer a CLI version?
|
|
@@ -5,11 +5,11 @@ description: Link this computer to an existing sync repo
|
|
|
5
5
|
Use the opencode_sync tool with command "link".
|
|
6
6
|
This command is for linking a second (or additional) computer to an existing sync repo that was created on another machine.
|
|
7
7
|
|
|
8
|
-
IMPORTANT: This will OVERWRITE the local
|
|
8
|
+
IMPORTANT: This will OVERWRITE the local opencode configuration with the contents from the synced repo. The only thing preserved is the local overrides file (opencode-synced.overrides.jsonc).
|
|
9
9
|
|
|
10
10
|
If the user provides a repo name argument, pass it as name="repo-name".
|
|
11
11
|
If no repo name is provided, the tool will automatically search for common sync repo names.
|
|
12
12
|
|
|
13
13
|
After linking:
|
|
14
|
-
- Remind the user to restart
|
|
14
|
+
- Remind the user to restart opencode to apply the synced config
|
|
15
15
|
- If they want to enable secrets sync, they should run /sync-enable-secrets
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Pull and apply synced
|
|
2
|
+
description: Pull and apply synced opencode config
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
Use the opencode_sync tool with command "pull".
|
|
6
|
-
If updates are applied, remind the user to restart
|
|
6
|
+
If updates are applied, remind the user to restart opencode.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { Plugin } from '@opencode-ai/plugin';
|
|
2
|
-
export declare const
|
|
3
|
-
export declare const
|
|
4
|
-
export default
|
|
2
|
+
export declare const opencodeConfigSync: Plugin;
|
|
3
|
+
export declare const opencodeSynced: Plugin;
|
|
4
|
+
export default opencodeConfigSync;
|
package/dist/index.js
CHANGED
|
@@ -85,11 +85,11 @@ async function loadCommands() {
|
|
|
85
85
|
}
|
|
86
86
|
return commands;
|
|
87
87
|
}
|
|
88
|
-
export const
|
|
88
|
+
export const opencodeConfigSync = async (ctx) => {
|
|
89
89
|
const commands = await loadCommands();
|
|
90
90
|
const service = createSyncService(ctx);
|
|
91
91
|
const syncTool = tool({
|
|
92
|
-
description: 'Manage
|
|
92
|
+
description: 'Manage opencode config sync with a GitHub repo',
|
|
93
93
|
args: {
|
|
94
94
|
command: tool.schema
|
|
95
95
|
.enum(['status', 'init', 'link', 'pull', 'push', 'enable-secrets', 'resolve'])
|
|
@@ -200,8 +200,8 @@ export const OpencodeConfigSync = async (ctx) => {
|
|
|
200
200
|
},
|
|
201
201
|
};
|
|
202
202
|
};
|
|
203
|
-
export const
|
|
204
|
-
export default
|
|
203
|
+
export const opencodeSynced = opencodeConfigSync;
|
|
204
|
+
export default opencodeConfigSync;
|
|
205
205
|
function formatError(error) {
|
|
206
206
|
if (error instanceof Error)
|
|
207
207
|
return error.message;
|
package/dist/sync/apply.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { deepMerge, hasOwn, parseJsonc, pathExists, stripOverrides, writeJsonFile, } from './config.js';
|
|
3
|
+
import { chmodIfExists, deepMerge, hasOwn, parseJsonc, pathExists, stripOverrides, writeJsonFile, } from './config.js';
|
|
4
4
|
import { extractMcpSecrets, hasOverrides, mergeOverrides, stripOverrideKeys, } from './mcp-secrets.js';
|
|
5
5
|
import { normalizePath } from './paths.js';
|
|
6
6
|
export async function syncRepoToLocal(plan, overrides) {
|
|
@@ -117,7 +117,7 @@ async function copyFileWithMode(sourcePath, destinationPath) {
|
|
|
117
117
|
const stat = await fs.stat(sourcePath);
|
|
118
118
|
await fs.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
119
119
|
await fs.copyFile(sourcePath, destinationPath);
|
|
120
|
-
await
|
|
120
|
+
await chmodIfExists(destinationPath, stat.mode & 0o777);
|
|
121
121
|
}
|
|
122
122
|
async function copyDirRecursive(sourcePath, destinationPath) {
|
|
123
123
|
const stat = await fs.stat(sourcePath);
|
|
@@ -134,7 +134,7 @@ async function copyDirRecursive(sourcePath, destinationPath) {
|
|
|
134
134
|
await copyFileWithMode(entrySource, entryDest);
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
-
await
|
|
137
|
+
await chmodIfExists(destinationPath, stat.mode & 0o777);
|
|
138
138
|
}
|
|
139
139
|
async function removePath(targetPath) {
|
|
140
140
|
await fs.rm(targetPath, { recursive: true, force: true });
|
|
@@ -161,7 +161,7 @@ async function applyExtraSecrets(plan, fromRepo) {
|
|
|
161
161
|
if (fromRepo) {
|
|
162
162
|
await copyFileWithMode(repoPath, localPath);
|
|
163
163
|
if (entry.mode !== undefined) {
|
|
164
|
-
await
|
|
164
|
+
await chmodIfExists(localPath, entry.mode);
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
}
|
package/dist/sync/commit.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { extractTextFromResponse, resolveSmallModel, unwrapData } from './utils.js';
|
|
2
2
|
export async function generateCommitMessage(ctx, repoDir, fallbackDate = new Date()) {
|
|
3
|
-
const fallback = `Sync
|
|
3
|
+
const fallback = `Sync opencode config (${formatDate(fallbackDate)})`;
|
|
4
4
|
const diffSummary = await getDiffSummary(ctx.$, repoDir);
|
|
5
5
|
if (!diffSummary)
|
|
6
6
|
return fallback;
|
|
@@ -9,7 +9,7 @@ export async function generateCommitMessage(ctx, repoDir, fallbackDate = new Dat
|
|
|
9
9
|
return fallback;
|
|
10
10
|
const prompt = [
|
|
11
11
|
'Generate a concise single-line git commit message (max 72 chars).',
|
|
12
|
-
'Focus on
|
|
12
|
+
'Focus on opencode config sync changes.',
|
|
13
13
|
'Return only the message, no quotes.',
|
|
14
14
|
'',
|
|
15
15
|
'Diff summary:',
|
package/dist/sync/config.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface SyncState {
|
|
|
20
20
|
lastRemoteUpdate?: string;
|
|
21
21
|
}
|
|
22
22
|
export declare function pathExists(filePath: string): Promise<boolean>;
|
|
23
|
+
export declare function chmodIfExists(filePath: string, mode: number): Promise<void>;
|
|
23
24
|
export declare function normalizeSyncConfig(config: SyncConfig): SyncConfig;
|
|
24
25
|
export declare function canCommitMcpSecrets(config: SyncConfig): boolean;
|
|
25
26
|
export declare function loadSyncConfig(locations: SyncLocations): Promise<SyncConfig | null>;
|
package/dist/sync/config.js
CHANGED
|
@@ -9,6 +9,17 @@ export async function pathExists(filePath) {
|
|
|
9
9
|
return false;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
+
export async function chmodIfExists(filePath, mode) {
|
|
13
|
+
try {
|
|
14
|
+
await fs.chmod(filePath, mode);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
const maybeErrno = error;
|
|
18
|
+
if (maybeErrno.code === 'ENOENT')
|
|
19
|
+
return;
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
12
23
|
export function normalizeSyncConfig(config) {
|
|
13
24
|
const includeSecrets = Boolean(config.includeSecrets);
|
|
14
25
|
return {
|
|
@@ -177,7 +188,7 @@ export async function writeJsonFile(filePath, data, options = { jsonc: false })
|
|
|
177
188
|
const content = options.jsonc ? `// Generated by opencode-synced\n${json}\n` : `${json}\n`;
|
|
178
189
|
await fs.writeFile(filePath, content, 'utf8');
|
|
179
190
|
if (options.mode !== undefined) {
|
|
180
|
-
await
|
|
191
|
+
await chmodIfExists(filePath, options.mode);
|
|
181
192
|
}
|
|
182
193
|
}
|
|
183
194
|
export function isPlainObject(value) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface SyncLockInfo {
|
|
2
|
+
pid: number;
|
|
3
|
+
startedAt: string;
|
|
4
|
+
hostname: string;
|
|
5
|
+
}
|
|
6
|
+
export type SyncLockResult = {
|
|
7
|
+
acquired: true;
|
|
8
|
+
info: SyncLockInfo;
|
|
9
|
+
release: () => Promise<void>;
|
|
10
|
+
} | {
|
|
11
|
+
acquired: false;
|
|
12
|
+
info: SyncLockInfo | null;
|
|
13
|
+
};
|
|
14
|
+
export declare function tryAcquireSyncLock(lockPath: string): Promise<SyncLockResult>;
|
|
15
|
+
export declare function withSyncLock<T>(lockPath: string, options: {
|
|
16
|
+
onBusy: (info: SyncLockInfo | null) => T | Promise<T>;
|
|
17
|
+
}, fn: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { mkdir, open, readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
function isProcessAlive(pid) {
|
|
5
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
6
|
+
return false;
|
|
7
|
+
try {
|
|
8
|
+
process.kill(pid, 0);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
const maybeErrno = error;
|
|
13
|
+
if (maybeErrno.code === 'ESRCH')
|
|
14
|
+
return false;
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function readLockInfo(lockPath) {
|
|
19
|
+
try {
|
|
20
|
+
const content = await readFile(lockPath, 'utf8');
|
|
21
|
+
const parsed = JSON.parse(content);
|
|
22
|
+
if (typeof parsed.pid !== 'number')
|
|
23
|
+
return null;
|
|
24
|
+
if (typeof parsed.startedAt !== 'string')
|
|
25
|
+
return null;
|
|
26
|
+
if (typeof parsed.hostname !== 'string')
|
|
27
|
+
return null;
|
|
28
|
+
return {
|
|
29
|
+
pid: parsed.pid,
|
|
30
|
+
startedAt: parsed.startedAt,
|
|
31
|
+
hostname: parsed.hostname,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const maybeErrno = error;
|
|
36
|
+
if (maybeErrno.code === 'ENOENT')
|
|
37
|
+
return null;
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function tryAcquireSyncLock(lockPath) {
|
|
42
|
+
const parentDir = path.dirname(lockPath);
|
|
43
|
+
await mkdir(parentDir, { recursive: true });
|
|
44
|
+
const ourInfo = {
|
|
45
|
+
pid: process.pid,
|
|
46
|
+
startedAt: new Date().toISOString(),
|
|
47
|
+
hostname: os.hostname(),
|
|
48
|
+
};
|
|
49
|
+
let attempt = 0;
|
|
50
|
+
while (true) {
|
|
51
|
+
try {
|
|
52
|
+
const handle = await open(lockPath, 'wx');
|
|
53
|
+
await handle.writeFile(`${JSON.stringify(ourInfo, null, 2)}\n`, 'utf8');
|
|
54
|
+
return {
|
|
55
|
+
acquired: true,
|
|
56
|
+
info: ourInfo,
|
|
57
|
+
release: async () => {
|
|
58
|
+
await handle.close().catch(() => {
|
|
59
|
+
// ignore close failures
|
|
60
|
+
});
|
|
61
|
+
await rm(lockPath, { force: true });
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
const maybeErrno = error;
|
|
67
|
+
if (maybeErrno.code !== 'EEXIST')
|
|
68
|
+
throw error;
|
|
69
|
+
const existing = await readLockInfo(lockPath);
|
|
70
|
+
const shouldBreakLock = existing === null || !isProcessAlive(existing.pid);
|
|
71
|
+
if (attempt === 0 && shouldBreakLock) {
|
|
72
|
+
await rm(lockPath, { force: true });
|
|
73
|
+
attempt += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
return { acquired: false, info: existing };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export async function withSyncLock(lockPath, options, fn) {
|
|
81
|
+
const result = await tryAcquireSyncLock(lockPath);
|
|
82
|
+
if (!result.acquired) {
|
|
83
|
+
return await options.onBusy(result.info);
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
return await fn();
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
await result.release();
|
|
90
|
+
}
|
|
91
|
+
}
|
package/dist/sync/mcp-secrets.js
CHANGED
|
@@ -46,7 +46,7 @@ function buildHeaderEnvVar(serverName, headerName) {
|
|
|
46
46
|
function buildEnvVar(serverName, key) {
|
|
47
47
|
const serverToken = toEnvToken(serverName, 'SERVER');
|
|
48
48
|
const keyToken = toEnvToken(key, 'VALUE');
|
|
49
|
-
return `
|
|
49
|
+
return `opencode_mcp_${serverToken}_${keyToken}`;
|
|
50
50
|
}
|
|
51
51
|
function toEnvToken(input, fallback) {
|
|
52
52
|
const cleaned = String(input)
|
package/dist/sync/paths.js
CHANGED
|
@@ -39,7 +39,7 @@ export function resolveXdgPaths(env = process.env, platform = process.platform)
|
|
|
39
39
|
}
|
|
40
40
|
export function resolveSyncLocations(env = process.env, platform = process.platform) {
|
|
41
41
|
const xdg = resolveXdgPaths(env, platform);
|
|
42
|
-
const customConfigDir = env.
|
|
42
|
+
const customConfigDir = env.opencode_config_dir;
|
|
43
43
|
const configRoot = customConfigDir
|
|
44
44
|
? path.resolve(expandHome(customConfigDir, xdg.homeDir))
|
|
45
45
|
: path.join(xdg.configDir, 'opencode');
|
package/dist/sync/service.js
CHANGED
|
@@ -1,15 +1,38 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import { syncLocalToRepo, syncRepoToLocal } from './apply.js';
|
|
2
3
|
import { generateCommitMessage } from './commit.js';
|
|
3
4
|
import { canCommitMcpSecrets, loadOverrides, loadState, loadSyncConfig, normalizeSyncConfig, writeState, writeSyncConfig, } from './config.js';
|
|
4
5
|
import { SyncCommandError, SyncConfigMissingError } from './errors.js';
|
|
6
|
+
import { withSyncLock } from './lock.js';
|
|
5
7
|
import { buildSyncPlan, resolveRepoRoot, resolveSyncLocations } from './paths.js';
|
|
6
8
|
import { commitAll, ensureRepoCloned, ensureRepoPrivate, fetchAndFastForward, findSyncRepo, getAuthenticatedUser, getRepoStatus, hasLocalChanges, isRepoCloned, pushBranch, repoExists, resolveRepoBranch, resolveRepoIdentifier, } from './repo.js';
|
|
7
9
|
import { createLogger, extractTextFromResponse, resolveSmallModel, showToast, unwrapData, } from './utils.js';
|
|
8
10
|
export function createSyncService(ctx) {
|
|
9
11
|
const locations = resolveSyncLocations();
|
|
10
12
|
const log = createLogger(ctx.client);
|
|
13
|
+
const lockPath = path.join(path.dirname(locations.statePath), 'sync.lock');
|
|
14
|
+
const formatLockInfo = (info) => {
|
|
15
|
+
if (!info)
|
|
16
|
+
return 'Another sync is already in progress.';
|
|
17
|
+
return `Another sync is already in progress (pid ${info.pid} on ${info.hostname}, started ${info.startedAt}).`;
|
|
18
|
+
};
|
|
19
|
+
const runExclusive = (fn) => withSyncLock(lockPath, {
|
|
20
|
+
onBusy: (info) => {
|
|
21
|
+
throw new SyncCommandError(formatLockInfo(info));
|
|
22
|
+
},
|
|
23
|
+
}, fn);
|
|
24
|
+
const skipIfBusy = (fn) => withSyncLock(lockPath, {
|
|
25
|
+
onBusy: (info) => {
|
|
26
|
+
log.debug('Sync already running, skipping', {
|
|
27
|
+
pid: info?.pid,
|
|
28
|
+
hostname: info?.hostname,
|
|
29
|
+
startedAt: info?.startedAt,
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
},
|
|
33
|
+
}, fn);
|
|
11
34
|
return {
|
|
12
|
-
startupSync: async () => {
|
|
35
|
+
startupSync: () => skipIfBusy(async () => {
|
|
13
36
|
let config = null;
|
|
14
37
|
try {
|
|
15
38
|
config = await loadSyncConfig(locations);
|
|
@@ -31,7 +54,7 @@ export function createSyncService(ctx) {
|
|
|
31
54
|
log.error('Startup sync failed', { error: formatError(error) });
|
|
32
55
|
await showToast(ctx.client, formatError(error), 'error');
|
|
33
56
|
}
|
|
34
|
-
},
|
|
57
|
+
}),
|
|
35
58
|
status: async () => {
|
|
36
59
|
const config = await loadSyncConfig(locations);
|
|
37
60
|
if (!config) {
|
|
@@ -87,7 +110,7 @@ export function createSyncService(ctx) {
|
|
|
87
110
|
];
|
|
88
111
|
return statusLines.join('\n');
|
|
89
112
|
},
|
|
90
|
-
init: async (
|
|
113
|
+
init: (options) => runExclusive(async () => {
|
|
91
114
|
const config = await buildConfigFromInit(ctx.$, options);
|
|
92
115
|
const repoIdentifier = resolveRepoIdentifier(config);
|
|
93
116
|
const isPrivate = options.private ?? true;
|
|
@@ -123,8 +146,8 @@ export function createSyncService(ctx) {
|
|
|
123
146
|
`Local repo: ${repoRoot}`,
|
|
124
147
|
];
|
|
125
148
|
return lines.join('\n');
|
|
126
|
-
},
|
|
127
|
-
link: async (
|
|
149
|
+
}),
|
|
150
|
+
link: (options) => runExclusive(async () => {
|
|
128
151
|
const found = await findSyncRepo(ctx.$, options.repo);
|
|
129
152
|
if (!found) {
|
|
130
153
|
const searchedFor = options.repo
|
|
@@ -164,19 +187,19 @@ export function createSyncService(ctx) {
|
|
|
164
187
|
const lines = [
|
|
165
188
|
`Linked to existing sync repo: ${found.owner}/${found.name}`,
|
|
166
189
|
'',
|
|
167
|
-
'Your local
|
|
190
|
+
'Your local opencode config has been OVERWRITTEN with the synced config.',
|
|
168
191
|
'Your local overrides file was preserved and applied on top.',
|
|
169
192
|
'',
|
|
170
|
-
'Restart
|
|
193
|
+
'Restart opencode to apply the new settings.',
|
|
171
194
|
'',
|
|
172
195
|
found.isPrivate
|
|
173
196
|
? 'To enable secrets sync, run: /sync-enable-secrets'
|
|
174
197
|
: 'Note: Repo is public. Secrets sync is disabled.',
|
|
175
198
|
];
|
|
176
|
-
await showToast(ctx.client, 'Config synced. Restart
|
|
199
|
+
await showToast(ctx.client, 'Config synced. Restart opencode to apply.', 'info');
|
|
177
200
|
return lines.join('\n');
|
|
178
|
-
},
|
|
179
|
-
pull: async () => {
|
|
201
|
+
}),
|
|
202
|
+
pull: () => runExclusive(async () => {
|
|
180
203
|
const config = await getConfigOrThrow(locations);
|
|
181
204
|
const repoRoot = resolveRepoRoot(config, locations);
|
|
182
205
|
await ensureRepoCloned(ctx.$, config, repoRoot);
|
|
@@ -197,10 +220,10 @@ export function createSyncService(ctx) {
|
|
|
197
220
|
lastPull: new Date().toISOString(),
|
|
198
221
|
lastRemoteUpdate: new Date().toISOString(),
|
|
199
222
|
});
|
|
200
|
-
await showToast(ctx.client, 'Config updated. Restart
|
|
201
|
-
return 'Remote config applied. Restart
|
|
202
|
-
},
|
|
203
|
-
push: async () => {
|
|
223
|
+
await showToast(ctx.client, 'Config updated. Restart opencode to apply.', 'info');
|
|
224
|
+
return 'Remote config applied. Restart opencode to use new settings.';
|
|
225
|
+
}),
|
|
226
|
+
push: () => runExclusive(async () => {
|
|
204
227
|
const config = await getConfigOrThrow(locations);
|
|
205
228
|
const repoRoot = resolveRepoRoot(config, locations);
|
|
206
229
|
await ensureRepoCloned(ctx.$, config, repoRoot);
|
|
@@ -227,8 +250,8 @@ export function createSyncService(ctx) {
|
|
|
227
250
|
lastPush: new Date().toISOString(),
|
|
228
251
|
});
|
|
229
252
|
return `Pushed changes: ${message}`;
|
|
230
|
-
},
|
|
231
|
-
enableSecrets: async (
|
|
253
|
+
}),
|
|
254
|
+
enableSecrets: (options) => runExclusive(async () => {
|
|
232
255
|
const config = await getConfigOrThrow(locations);
|
|
233
256
|
config.includeSecrets = true;
|
|
234
257
|
if (options?.extraSecretPaths) {
|
|
@@ -240,8 +263,8 @@ export function createSyncService(ctx) {
|
|
|
240
263
|
await ensureRepoPrivate(ctx.$, config);
|
|
241
264
|
await writeSyncConfig(locations, config);
|
|
242
265
|
return 'Secrets sync enabled for this repo.';
|
|
243
|
-
},
|
|
244
|
-
resolve: async () => {
|
|
266
|
+
}),
|
|
267
|
+
resolve: () => runExclusive(async () => {
|
|
245
268
|
const config = await getConfigOrThrow(locations);
|
|
246
269
|
const repoRoot = resolveRepoRoot(config, locations);
|
|
247
270
|
await ensureRepoCloned(ctx.$, config, repoRoot);
|
|
@@ -267,7 +290,7 @@ export function createSyncService(ctx) {
|
|
|
267
290
|
}
|
|
268
291
|
}
|
|
269
292
|
return `Unable to automatically resolve. Please manually resolve in: ${repoRoot}`;
|
|
270
|
-
},
|
|
293
|
+
}),
|
|
271
294
|
};
|
|
272
295
|
}
|
|
273
296
|
async function runStartup(ctx, locations, config, log) {
|
|
@@ -293,7 +316,7 @@ async function runStartup(ctx, locations, config, log) {
|
|
|
293
316
|
lastPull: new Date().toISOString(),
|
|
294
317
|
lastRemoteUpdate: new Date().toISOString(),
|
|
295
318
|
});
|
|
296
|
-
await showToast(ctx.client, 'Config updated. Restart
|
|
319
|
+
await showToast(ctx.client, 'Config updated. Restart opencode to apply.', 'info');
|
|
297
320
|
return;
|
|
298
321
|
}
|
|
299
322
|
const overrides = await loadOverrides(locations);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-synced",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Sync global
|
|
3
|
+
"version": "0.7.1",
|
|
4
|
+
"description": "Sync global opencode config across machines via GitHub.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Ian Hildebrand"
|
|
7
7
|
},
|