opencode-synced 0.7.0 → 0.8.0
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 +14 -12
- package/dist/command/sync-init.md +1 -0
- 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 +6 -4
- package/dist/sync/apply.js +102 -25
- package/dist/sync/commit.js +2 -2
- package/dist/sync/config.d.ts +1 -0
- package/dist/sync/config.js +1 -0
- package/dist/sync/mcp-secrets.js +1 -1
- package/dist/sync/paths.d.ts +5 -3
- package/dist/sync/paths.js +22 -15
- package/dist/sync/service.d.ts +1 -0
- package/dist/sync/service.js +8 -6
- 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,7 +28,7 @@ Enable the plugin in your global OpenCode config (OpenCode will install it on ne
|
|
|
28
28
|
}
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
opencode does not auto-update plugins. To update, modify the version number in your config file.
|
|
32
32
|
|
|
33
33
|
## Configure
|
|
34
34
|
|
|
@@ -50,7 +50,7 @@ Run `/sync-link` to connect to your existing sync repo:
|
|
|
50
50
|
|
|
51
51
|
If auto-detection fails, specify the repo name: `/sync-link my-opencode-config`
|
|
52
52
|
|
|
53
|
-
After linking, restart
|
|
53
|
+
After linking, restart opencode to apply the synced settings.
|
|
54
54
|
|
|
55
55
|
### Custom repo name or org
|
|
56
56
|
|
|
@@ -77,6 +77,7 @@ Create `~/.config/opencode/opencode-synced.jsonc`:
|
|
|
77
77
|
"includeSessions": false,
|
|
78
78
|
"includePromptStash": false,
|
|
79
79
|
"extraSecretPaths": [],
|
|
80
|
+
"extraConfigPaths": [],
|
|
80
81
|
}
|
|
81
82
|
```
|
|
82
83
|
|
|
@@ -87,6 +88,7 @@ Create `~/.config/opencode/opencode-synced.jsonc`:
|
|
|
87
88
|
- `~/.config/opencode/opencode.json` and `opencode.jsonc`
|
|
88
89
|
- `~/.config/opencode/AGENTS.md`
|
|
89
90
|
- `~/.config/opencode/agent/`, `command/`, `mode/`, `tool/`, `themes/`, `plugin/`
|
|
91
|
+
- Any extra paths in `extraConfigPaths` (allowlist, files or folders)
|
|
90
92
|
|
|
91
93
|
### Secrets (private repos only)
|
|
92
94
|
|
|
@@ -94,14 +96,14 @@ Enable secrets with `/sync-enable-secrets` or set `"includeSecrets": true`:
|
|
|
94
96
|
|
|
95
97
|
- `~/.local/share/opencode/auth.json`
|
|
96
98
|
- `~/.local/share/opencode/mcp-auth.json`
|
|
97
|
-
- Any extra paths in `extraSecretPaths` (allowlist)
|
|
99
|
+
- Any extra paths in `extraSecretPaths` (allowlist, files or folders)
|
|
98
100
|
|
|
99
101
|
MCP API keys stored inside `opencode.json(c)` are **not** committed by default. To allow them
|
|
100
102
|
in a private repo, set `"includeMcpSecrets": true` (requires `includeSecrets`).
|
|
101
103
|
|
|
102
104
|
### Sessions (private repos only)
|
|
103
105
|
|
|
104
|
-
Sync your
|
|
106
|
+
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.
|
|
105
107
|
|
|
106
108
|
```jsonc
|
|
107
109
|
{
|
|
@@ -158,8 +160,8 @@ If you want MCP secrets committed (private repos only), set `"includeMcpSecrets"
|
|
|
158
160
|
Env var naming rules:
|
|
159
161
|
|
|
160
162
|
- If the header name already looks like an env var (e.g. `CONTEXT7_API_KEY`), it is used directly.
|
|
161
|
-
- Otherwise: `
|
|
162
|
-
- OAuth client secrets use `
|
|
163
|
+
- Otherwise: `opencode_mcp_<SERVER>_<HEADER>` (non-alphanumerics become `_`).
|
|
164
|
+
- OAuth client secrets use `opencode_mcp_<SERVER>_OAUTH_CLIENT_SECRET`.
|
|
163
165
|
|
|
164
166
|
## Usage
|
|
165
167
|
|
|
@@ -178,7 +180,7 @@ Env var naming rules:
|
|
|
178
180
|
|
|
179
181
|
### Trigger a sync
|
|
180
182
|
|
|
181
|
-
Restart
|
|
183
|
+
Restart opencode to run the startup sync flow (pull remote, apply if changed, push local changes if needed).
|
|
182
184
|
|
|
183
185
|
### Check status
|
|
184
186
|
|
|
@@ -265,7 +267,7 @@ bun -e '
|
|
|
265
267
|
### Local testing (production-like)
|
|
266
268
|
|
|
267
269
|
To test the same artifact that would be published, install from a packed tarball
|
|
268
|
-
into
|
|
270
|
+
into opencode's cache:
|
|
269
271
|
|
|
270
272
|
```bash
|
|
271
273
|
mise run local-pack-test
|
|
@@ -279,7 +281,7 @@ Then set `~/.config/opencode/opencode.json` to use:
|
|
|
279
281
|
}
|
|
280
282
|
```
|
|
281
283
|
|
|
282
|
-
Restart
|
|
284
|
+
Restart opencode to pick up the cached install.
|
|
283
285
|
|
|
284
286
|
|
|
285
287
|
## Prefer a CLI version?
|
|
@@ -10,3 +10,4 @@ If the user wants an org-owned repo, pass owner="org-name".
|
|
|
10
10
|
If the user wants a public repo, pass private=false.
|
|
11
11
|
Include includeSecrets if the user explicitly opts in.
|
|
12
12
|
Include includeMcpSecrets only if they want MCP secrets committed to a private repo.
|
|
13
|
+
If the user supplies extra config paths, pass extraConfigPaths.
|
|
@@ -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'])
|
|
@@ -115,6 +115,7 @@ export const OpencodeConfigSync = async (ctx) => {
|
|
|
115
115
|
create: tool.schema.boolean().optional().describe('Create repo if missing'),
|
|
116
116
|
private: tool.schema.boolean().optional().describe('Create repo as private'),
|
|
117
117
|
extraSecretPaths: tool.schema.array(tool.schema.string()).optional(),
|
|
118
|
+
extraConfigPaths: tool.schema.array(tool.schema.string()).optional(),
|
|
118
119
|
localRepoPath: tool.schema.string().optional().describe('Override local repo path'),
|
|
119
120
|
},
|
|
120
121
|
async execute(args) {
|
|
@@ -136,6 +137,7 @@ export const OpencodeConfigSync = async (ctx) => {
|
|
|
136
137
|
create: args.create,
|
|
137
138
|
private: args.private,
|
|
138
139
|
extraSecretPaths: args.extraSecretPaths,
|
|
140
|
+
extraConfigPaths: args.extraConfigPaths,
|
|
139
141
|
localRepoPath: args.localRepoPath,
|
|
140
142
|
});
|
|
141
143
|
}
|
|
@@ -200,8 +202,8 @@ export const OpencodeConfigSync = async (ctx) => {
|
|
|
200
202
|
},
|
|
201
203
|
};
|
|
202
204
|
};
|
|
203
|
-
export const
|
|
204
|
-
export default
|
|
205
|
+
export const opencodeSynced = opencodeConfigSync;
|
|
206
|
+
export default opencodeConfigSync;
|
|
205
207
|
function formatError(error) {
|
|
206
208
|
if (error instanceof Error)
|
|
207
209
|
return error.message;
|
package/dist/sync/apply.js
CHANGED
|
@@ -7,7 +7,8 @@ export async function syncRepoToLocal(plan, overrides) {
|
|
|
7
7
|
for (const item of plan.items) {
|
|
8
8
|
await copyItem(item.repoPath, item.localPath, item.type);
|
|
9
9
|
}
|
|
10
|
-
await
|
|
10
|
+
await applyExtraPaths(plan, plan.extraConfigs);
|
|
11
|
+
await applyExtraPaths(plan, plan.extraSecrets);
|
|
11
12
|
if (overrides && Object.keys(overrides).length > 0) {
|
|
12
13
|
await applyOverridesToLocalConfig(plan, overrides);
|
|
13
14
|
}
|
|
@@ -49,7 +50,8 @@ export async function syncLocalToRepo(plan, overrides, options = {}) {
|
|
|
49
50
|
}
|
|
50
51
|
await copyItem(item.localPath, item.repoPath, item.type, true);
|
|
51
52
|
}
|
|
52
|
-
await
|
|
53
|
+
await writeExtraPathManifest(plan, plan.extraConfigs);
|
|
54
|
+
await writeExtraPathManifest(plan, plan.extraSecrets);
|
|
53
55
|
}
|
|
54
56
|
async function copyItem(sourcePath, destinationPath, type, removeWhenMissing = false) {
|
|
55
57
|
if (!(await pathExists(sourcePath))) {
|
|
@@ -139,13 +141,13 @@ async function copyDirRecursive(sourcePath, destinationPath) {
|
|
|
139
141
|
async function removePath(targetPath) {
|
|
140
142
|
await fs.rm(targetPath, { recursive: true, force: true });
|
|
141
143
|
}
|
|
142
|
-
async function
|
|
143
|
-
const allowlist =
|
|
144
|
+
async function applyExtraPaths(plan, extra) {
|
|
145
|
+
const allowlist = extra.allowlist;
|
|
144
146
|
if (allowlist.length === 0)
|
|
145
147
|
return;
|
|
146
|
-
if (!(await pathExists(
|
|
148
|
+
if (!(await pathExists(extra.manifestPath)))
|
|
147
149
|
return;
|
|
148
|
-
const manifestContent = await fs.readFile(
|
|
150
|
+
const manifestContent = await fs.readFile(extra.manifestPath, 'utf8');
|
|
149
151
|
const manifest = parseJsonc(manifestContent);
|
|
150
152
|
for (const entry of manifest.entries) {
|
|
151
153
|
const normalized = normalizePath(entry.sourcePath, plan.homeDir, plan.platform);
|
|
@@ -156,41 +158,116 @@ async function applyExtraSecrets(plan, fromRepo) {
|
|
|
156
158
|
? entry.repoPath
|
|
157
159
|
: path.join(plan.repoRoot, entry.repoPath);
|
|
158
160
|
const localPath = entry.sourcePath;
|
|
161
|
+
const entryType = entry.type ?? 'file';
|
|
159
162
|
if (!(await pathExists(repoPath)))
|
|
160
163
|
continue;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (entry.mode !== undefined) {
|
|
164
|
-
await chmodIfExists(localPath, entry.mode);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
164
|
+
await copyItem(repoPath, localPath, entryType);
|
|
165
|
+
await applyExtraPathModes(localPath, entry);
|
|
167
166
|
}
|
|
168
167
|
}
|
|
169
|
-
async function
|
|
170
|
-
const allowlist =
|
|
171
|
-
const extraDir = path.join(path.dirname(
|
|
168
|
+
async function writeExtraPathManifest(plan, extra) {
|
|
169
|
+
const allowlist = extra.allowlist;
|
|
170
|
+
const extraDir = path.join(path.dirname(extra.manifestPath), 'extra');
|
|
172
171
|
if (allowlist.length === 0) {
|
|
173
|
-
await removePath(
|
|
172
|
+
await removePath(extra.manifestPath);
|
|
174
173
|
await removePath(extraDir);
|
|
175
174
|
return;
|
|
176
175
|
}
|
|
177
176
|
await removePath(extraDir);
|
|
178
177
|
const entries = [];
|
|
179
|
-
for (const entry of
|
|
178
|
+
for (const entry of extra.entries) {
|
|
180
179
|
const sourcePath = entry.sourcePath;
|
|
181
180
|
if (!(await pathExists(sourcePath))) {
|
|
182
181
|
continue;
|
|
183
182
|
}
|
|
184
183
|
const stat = await fs.stat(sourcePath);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
sourcePath,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
184
|
+
if (stat.isDirectory()) {
|
|
185
|
+
await copyDirRecursive(sourcePath, entry.repoPath);
|
|
186
|
+
const items = await collectExtraPathItems(sourcePath, sourcePath);
|
|
187
|
+
entries.push({
|
|
188
|
+
sourcePath,
|
|
189
|
+
repoPath: path.relative(plan.repoRoot, entry.repoPath),
|
|
190
|
+
type: 'dir',
|
|
191
|
+
mode: stat.mode & 0o777,
|
|
192
|
+
items,
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (stat.isFile()) {
|
|
197
|
+
await copyFileWithMode(sourcePath, entry.repoPath);
|
|
198
|
+
entries.push({
|
|
199
|
+
sourcePath,
|
|
200
|
+
repoPath: path.relative(plan.repoRoot, entry.repoPath),
|
|
201
|
+
type: 'file',
|
|
202
|
+
mode: stat.mode & 0o777,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
await fs.mkdir(path.dirname(extra.manifestPath), { recursive: true });
|
|
207
|
+
await writeJsonFile(extra.manifestPath, { entries }, { jsonc: false });
|
|
208
|
+
}
|
|
209
|
+
async function collectExtraPathItems(sourcePath, basePath) {
|
|
210
|
+
const items = [];
|
|
211
|
+
const entries = await fs.readdir(sourcePath, { withFileTypes: true });
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
const entrySource = path.join(sourcePath, entry.name);
|
|
214
|
+
const relativePath = path.relative(basePath, entrySource);
|
|
215
|
+
if (entry.isDirectory()) {
|
|
216
|
+
const stat = await fs.stat(entrySource);
|
|
217
|
+
items.push({
|
|
218
|
+
relativePath,
|
|
219
|
+
type: 'dir',
|
|
220
|
+
mode: stat.mode & 0o777,
|
|
221
|
+
});
|
|
222
|
+
const nested = await collectExtraPathItems(entrySource, basePath);
|
|
223
|
+
items.push(...nested);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (entry.isFile()) {
|
|
227
|
+
const stat = await fs.stat(entrySource);
|
|
228
|
+
items.push({
|
|
229
|
+
relativePath,
|
|
230
|
+
type: 'file',
|
|
231
|
+
mode: stat.mode & 0o777,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return items;
|
|
236
|
+
}
|
|
237
|
+
async function applyExtraPathModes(targetPath, entry) {
|
|
238
|
+
if (entry.mode !== undefined) {
|
|
239
|
+
await chmodIfExists(targetPath, entry.mode);
|
|
240
|
+
}
|
|
241
|
+
if (entry.type !== 'dir') {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (!entry.items || entry.items.length === 0) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
for (const item of entry.items) {
|
|
248
|
+
if (item.mode === undefined)
|
|
249
|
+
continue;
|
|
250
|
+
const itemPath = resolveExtraPathItem(targetPath, item.relativePath);
|
|
251
|
+
if (!itemPath)
|
|
252
|
+
continue;
|
|
253
|
+
await chmodIfExists(itemPath, item.mode);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function resolveExtraPathItem(basePath, relativePath) {
|
|
257
|
+
if (!relativePath)
|
|
258
|
+
return null;
|
|
259
|
+
if (path.isAbsolute(relativePath))
|
|
260
|
+
return null;
|
|
261
|
+
const resolvedBase = path.resolve(basePath);
|
|
262
|
+
const resolvedPath = path.resolve(basePath, relativePath);
|
|
263
|
+
const relative = path.relative(resolvedBase, resolvedPath);
|
|
264
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`)) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
if (path.isAbsolute(relative)) {
|
|
268
|
+
return null;
|
|
191
269
|
}
|
|
192
|
-
|
|
193
|
-
await writeJsonFile(plan.extraSecrets.manifestPath, { entries }, { jsonc: false });
|
|
270
|
+
return resolvedPath;
|
|
194
271
|
}
|
|
195
272
|
function isDeepEqual(left, right) {
|
|
196
273
|
if (left === right)
|
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
package/dist/sync/config.js
CHANGED
|
@@ -28,6 +28,7 @@ export function normalizeSyncConfig(config) {
|
|
|
28
28
|
includeSessions: Boolean(config.includeSessions),
|
|
29
29
|
includePromptStash: Boolean(config.includePromptStash),
|
|
30
30
|
extraSecretPaths: Array.isArray(config.extraSecretPaths) ? config.extraSecretPaths : [],
|
|
31
|
+
extraConfigPaths: Array.isArray(config.extraConfigPaths) ? config.extraConfigPaths : [],
|
|
31
32
|
localRepoPath: config.localRepoPath,
|
|
32
33
|
repo: config.repo,
|
|
33
34
|
};
|
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.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface SyncItem {
|
|
|
21
21
|
isSecret: boolean;
|
|
22
22
|
isConfigFile: boolean;
|
|
23
23
|
}
|
|
24
|
-
export interface
|
|
24
|
+
export interface ExtraPathPlan {
|
|
25
25
|
allowlist: string[];
|
|
26
26
|
manifestPath: string;
|
|
27
27
|
entries: Array<{
|
|
@@ -31,7 +31,8 @@ export interface ExtraSecretPlan {
|
|
|
31
31
|
}
|
|
32
32
|
export interface SyncPlan {
|
|
33
33
|
items: SyncItem[];
|
|
34
|
-
extraSecrets:
|
|
34
|
+
extraSecrets: ExtraPathPlan;
|
|
35
|
+
extraConfigs: ExtraPathPlan;
|
|
35
36
|
repoRoot: string;
|
|
36
37
|
homeDir: string;
|
|
37
38
|
platform: NodeJS.Platform;
|
|
@@ -42,6 +43,7 @@ export declare function resolveSyncLocations(env?: NodeJS.ProcessEnv, platform?:
|
|
|
42
43
|
export declare function expandHome(inputPath: string, homeDir: string): string;
|
|
43
44
|
export declare function normalizePath(inputPath: string, homeDir: string, platform?: NodeJS.Platform): string;
|
|
44
45
|
export declare function isSamePath(left: string, right: string, homeDir: string, platform?: NodeJS.Platform): boolean;
|
|
45
|
-
export declare function
|
|
46
|
+
export declare function encodeExtraPath(inputPath: string): string;
|
|
47
|
+
export declare const encodeSecretPath: typeof encodeExtraPath;
|
|
46
48
|
export declare function resolveRepoRoot(config: SyncConfig | null, locations: SyncLocations): string;
|
|
47
49
|
export declare function buildSyncPlan(config: SyncConfig, locations: SyncLocations, repoRoot: string, platform?: NodeJS.Platform): SyncPlan;
|
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');
|
|
@@ -75,13 +75,14 @@ export function normalizePath(inputPath, homeDir, platform = process.platform) {
|
|
|
75
75
|
export function isSamePath(left, right, homeDir, platform = process.platform) {
|
|
76
76
|
return normalizePath(left, homeDir, platform) === normalizePath(right, homeDir, platform);
|
|
77
77
|
}
|
|
78
|
-
export function
|
|
78
|
+
export function encodeExtraPath(inputPath) {
|
|
79
79
|
const normalized = inputPath.replace(/\\/g, '/');
|
|
80
80
|
const safeBase = normalized.replace(/[^a-zA-Z0-9._-]+/g, '_').replace(/^_+/, '');
|
|
81
81
|
const hash = crypto.createHash('sha1').update(normalized).digest('hex').slice(0, 8);
|
|
82
|
-
const base = safeBase ? safeBase.slice(-80) : '
|
|
82
|
+
const base = safeBase ? safeBase.slice(-80) : 'path';
|
|
83
83
|
return `${base}-${hash}`;
|
|
84
84
|
}
|
|
85
|
+
export const encodeSecretPath = encodeExtraPath;
|
|
85
86
|
export function resolveRepoRoot(config, locations) {
|
|
86
87
|
if (config?.localRepoPath) {
|
|
87
88
|
return expandHome(config.localRepoPath, locations.xdg.homeDir);
|
|
@@ -96,6 +97,8 @@ export function buildSyncPlan(config, locations, repoRoot, platform = process.pl
|
|
|
96
97
|
const repoSecretsRoot = path.join(repoRoot, 'secrets');
|
|
97
98
|
const repoExtraDir = path.join(repoSecretsRoot, 'extra');
|
|
98
99
|
const manifestPath = path.join(repoSecretsRoot, 'extra-manifest.json');
|
|
100
|
+
const repoConfigExtraDir = path.join(repoConfigRoot, 'extra');
|
|
101
|
+
const configManifestPath = path.join(repoConfigRoot, 'extra-manifest.json');
|
|
99
102
|
const items = [];
|
|
100
103
|
const addFile = (name, isSecret, isConfigFile) => {
|
|
101
104
|
items.push({
|
|
@@ -157,22 +160,26 @@ export function buildSyncPlan(config, locations, repoRoot, platform = process.pl
|
|
|
157
160
|
}
|
|
158
161
|
}
|
|
159
162
|
}
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
: [];
|
|
163
|
-
const entries = allowlist.map((sourcePath) => ({
|
|
164
|
-
sourcePath,
|
|
165
|
-
repoPath: path.join(repoExtraDir, encodeSecretPath(sourcePath)),
|
|
166
|
-
}));
|
|
163
|
+
const extraSecrets = buildExtraPathPlan(config.includeSecrets ? config.extraSecretPaths : [], locations, repoExtraDir, manifestPath, platform);
|
|
164
|
+
const extraConfigs = buildExtraPathPlan(config.extraConfigPaths, locations, repoConfigExtraDir, configManifestPath, platform);
|
|
167
165
|
return {
|
|
168
166
|
items,
|
|
169
|
-
extraSecrets
|
|
170
|
-
|
|
171
|
-
manifestPath,
|
|
172
|
-
entries,
|
|
173
|
-
},
|
|
167
|
+
extraSecrets,
|
|
168
|
+
extraConfigs,
|
|
174
169
|
repoRoot,
|
|
175
170
|
homeDir: locations.xdg.homeDir,
|
|
176
171
|
platform,
|
|
177
172
|
};
|
|
178
173
|
}
|
|
174
|
+
function buildExtraPathPlan(inputPaths, locations, repoExtraDir, manifestPath, platform) {
|
|
175
|
+
const allowlist = (inputPaths ?? []).map((entry) => normalizePath(entry, locations.xdg.homeDir, platform));
|
|
176
|
+
const entries = allowlist.map((sourcePath) => ({
|
|
177
|
+
sourcePath,
|
|
178
|
+
repoPath: path.join(repoExtraDir, encodeExtraPath(sourcePath)),
|
|
179
|
+
}));
|
|
180
|
+
return {
|
|
181
|
+
allowlist,
|
|
182
|
+
manifestPath,
|
|
183
|
+
entries,
|
|
184
|
+
};
|
|
185
|
+
}
|
package/dist/sync/service.d.ts
CHANGED
package/dist/sync/service.js
CHANGED
|
@@ -171,6 +171,7 @@ export function createSyncService(ctx) {
|
|
|
171
171
|
includeSessions: false,
|
|
172
172
|
includePromptStash: false,
|
|
173
173
|
extraSecretPaths: [],
|
|
174
|
+
extraConfigPaths: [],
|
|
174
175
|
});
|
|
175
176
|
await writeSyncConfig(locations, config);
|
|
176
177
|
const repoRoot = resolveRepoRoot(config, locations);
|
|
@@ -187,16 +188,16 @@ export function createSyncService(ctx) {
|
|
|
187
188
|
const lines = [
|
|
188
189
|
`Linked to existing sync repo: ${found.owner}/${found.name}`,
|
|
189
190
|
'',
|
|
190
|
-
'Your local
|
|
191
|
+
'Your local opencode config has been OVERWRITTEN with the synced config.',
|
|
191
192
|
'Your local overrides file was preserved and applied on top.',
|
|
192
193
|
'',
|
|
193
|
-
'Restart
|
|
194
|
+
'Restart opencode to apply the new settings.',
|
|
194
195
|
'',
|
|
195
196
|
found.isPrivate
|
|
196
197
|
? 'To enable secrets sync, run: /sync-enable-secrets'
|
|
197
198
|
: 'Note: Repo is public. Secrets sync is disabled.',
|
|
198
199
|
];
|
|
199
|
-
await showToast(ctx.client, 'Config synced. Restart
|
|
200
|
+
await showToast(ctx.client, 'Config synced. Restart opencode to apply.', 'info');
|
|
200
201
|
return lines.join('\n');
|
|
201
202
|
}),
|
|
202
203
|
pull: () => runExclusive(async () => {
|
|
@@ -220,8 +221,8 @@ export function createSyncService(ctx) {
|
|
|
220
221
|
lastPull: new Date().toISOString(),
|
|
221
222
|
lastRemoteUpdate: new Date().toISOString(),
|
|
222
223
|
});
|
|
223
|
-
await showToast(ctx.client, 'Config updated. Restart
|
|
224
|
-
return 'Remote config applied. Restart
|
|
224
|
+
await showToast(ctx.client, 'Config updated. Restart opencode to apply.', 'info');
|
|
225
|
+
return 'Remote config applied. Restart opencode to use new settings.';
|
|
225
226
|
}),
|
|
226
227
|
push: () => runExclusive(async () => {
|
|
227
228
|
const config = await getConfigOrThrow(locations);
|
|
@@ -316,7 +317,7 @@ async function runStartup(ctx, locations, config, log) {
|
|
|
316
317
|
lastPull: new Date().toISOString(),
|
|
317
318
|
lastRemoteUpdate: new Date().toISOString(),
|
|
318
319
|
});
|
|
319
|
-
await showToast(ctx.client, 'Config updated. Restart
|
|
320
|
+
await showToast(ctx.client, 'Config updated. Restart opencode to apply.', 'info');
|
|
320
321
|
return;
|
|
321
322
|
}
|
|
322
323
|
const overrides = await loadOverrides(locations);
|
|
@@ -369,6 +370,7 @@ async function buildConfigFromInit($, options) {
|
|
|
369
370
|
includeSessions: options.includeSessions ?? false,
|
|
370
371
|
includePromptStash: options.includePromptStash ?? false,
|
|
371
372
|
extraSecretPaths: options.extraSecretPaths ?? [],
|
|
373
|
+
extraConfigPaths: options.extraConfigPaths ?? [],
|
|
372
374
|
localRepoPath: options.localRepoPath,
|
|
373
375
|
});
|
|
374
376
|
}
|
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.8.0",
|
|
4
|
+
"description": "Sync global opencode config across machines via GitHub.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Ian Hildebrand"
|
|
7
7
|
},
|