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 CHANGED
@@ -1,10 +1,10 @@
1
1
  # opencode-synced
2
2
 
3
- Sync global OpenCode configuration across machines via a GitHub repo, with optional secrets support for private repos.
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 OpenCode config (`~/.config/opencode`) and related directories
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 OpenCode config (OpenCode will install it on next run):
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
- OpenCode does not auto-update plugins. To update, modify the version number in your config file.
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 OpenCode to apply the synced settings.
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 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.
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: `OPENCODE_MCP_<SERVER>_<HEADER>` (uppercase, non-alphanumerics become `_`).
162
- - OAuth client secrets use `OPENCODE_MCP_<SERVER>_OAUTH_CLIENT_SECRET`.
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 OpenCode to run the startup sync flow (pull remote, apply if changed, push local changes if needed).
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 OpenCode's cache:
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 OpenCode to pick up the cached install.
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 OpenCode configuration with the contents from the synced repo. The only thing preserved is the local overrides file (opencode-synced.overrides.jsonc).
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 OpenCode to apply the synced config
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 OpenCode config
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 OpenCode.
6
+ If updates are applied, remind the user to restart opencode.
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Push local OpenCode config to the sync repo
2
+ description: Push local opencode config to the sync repo
3
3
  ---
4
4
 
5
5
  Use the opencode_sync tool with command "push".
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import type { Plugin } from '@opencode-ai/plugin';
2
- export declare const OpencodeConfigSync: Plugin;
3
- export declare const OpencodeSynced: Plugin;
4
- export default OpencodeConfigSync;
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 OpencodeConfigSync = async (ctx) => {
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 OpenCode config sync with a GitHub repo',
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 OpencodeSynced = OpencodeConfigSync;
204
- export default OpencodeConfigSync;
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;
@@ -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 applyExtraSecrets(plan, true);
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 writeExtraSecretsManifest(plan);
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 applyExtraSecrets(plan, fromRepo) {
143
- const allowlist = plan.extraSecrets.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(plan.extraSecrets.manifestPath)))
148
+ if (!(await pathExists(extra.manifestPath)))
147
149
  return;
148
- const manifestContent = await fs.readFile(plan.extraSecrets.manifestPath, 'utf8');
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
- if (fromRepo) {
162
- await copyFileWithMode(repoPath, localPath);
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 writeExtraSecretsManifest(plan) {
170
- const allowlist = plan.extraSecrets.allowlist;
171
- const extraDir = path.join(path.dirname(plan.extraSecrets.manifestPath), 'extra');
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(plan.extraSecrets.manifestPath);
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 plan.extraSecrets.entries) {
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
- await copyFileWithMode(sourcePath, entry.repoPath);
186
- entries.push({
187
- sourcePath,
188
- repoPath: path.relative(plan.repoRoot, entry.repoPath),
189
- mode: stat.mode & 0o777,
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
- await fs.mkdir(path.dirname(plan.extraSecrets.manifestPath), { recursive: true });
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)
@@ -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 OpenCode config (${formatDate(fallbackDate)})`;
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 OpenCode config sync changes.',
12
+ 'Focus on opencode config sync changes.',
13
13
  'Return only the message, no quotes.',
14
14
  '',
15
15
  'Diff summary:',
@@ -13,6 +13,7 @@ export interface SyncConfig {
13
13
  includeSessions?: boolean;
14
14
  includePromptStash?: boolean;
15
15
  extraSecretPaths?: string[];
16
+ extraConfigPaths?: string[];
16
17
  }
17
18
  export interface SyncState {
18
19
  lastPull?: string;
@@ -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
  };
@@ -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 `OPENCODE_MCP_${serverToken}_${keyToken}`;
49
+ return `opencode_mcp_${serverToken}_${keyToken}`;
50
50
  }
51
51
  function toEnvToken(input, fallback) {
52
52
  const cleaned = String(input)
@@ -21,7 +21,7 @@ export interface SyncItem {
21
21
  isSecret: boolean;
22
22
  isConfigFile: boolean;
23
23
  }
24
- export interface ExtraSecretPlan {
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: ExtraSecretPlan;
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 encodeSecretPath(inputPath: string): string;
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;
@@ -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.OPENCODE_CONFIG_DIR;
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 encodeSecretPath(inputPath) {
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) : 'secret';
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 allowlist = config.includeSecrets
161
- ? (config.extraSecretPaths ?? []).map((entry) => normalizePath(entry, locations.xdg.homeDir, platform))
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
- allowlist,
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
+ }
@@ -13,6 +13,7 @@ interface InitOptions {
13
13
  create?: boolean;
14
14
  private?: boolean;
15
15
  extraSecretPaths?: string[];
16
+ extraConfigPaths?: string[];
16
17
  localRepoPath?: string;
17
18
  }
18
19
  interface LinkOptions {
@@ -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 OpenCode config has been OVERWRITTEN with the synced config.',
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 OpenCode to apply the new settings.',
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 OpenCode to apply.', 'info');
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 OpenCode to apply.', 'info');
224
- return 'Remote config applied. Restart OpenCode to use new settings.';
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 OpenCode to apply.', 'info');
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.7.0",
4
- "description": "Sync global OpenCode config across machines via GitHub.",
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
  },