opencode-synced 0.7.1 → 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 +3 -1
- package/dist/command/sync-init.md +1 -0
- package/dist/index.js +2 -0
- package/dist/sync/apply.js +102 -25
- package/dist/sync/config.d.ts +1 -0
- package/dist/sync/config.js +1 -0
- package/dist/sync/paths.d.ts +5 -3
- package/dist/sync/paths.js +21 -14
- package/dist/sync/service.d.ts +1 -0
- package/dist/sync/service.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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,7 +96,7 @@ 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`).
|
|
@@ -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.
|
package/dist/index.js
CHANGED
|
@@ -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
|
}
|
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/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/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
|
@@ -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);
|
|
@@ -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
|
}
|