opencode-synced 0.4.2 → 0.5.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 CHANGED
@@ -78,6 +78,7 @@ Create `~/.config/opencode/opencode-synced.jsonc`:
78
78
  "branch": "main",
79
79
  },
80
80
  "includeSecrets": false,
81
+ "includeMcpSecrets": false,
81
82
  "includeSessions": false,
82
83
  "includePromptStash": false,
83
84
  "extraSecretPaths": [],
@@ -100,6 +101,9 @@ Enable secrets with `/sync-enable-secrets` or set `"includeSecrets": true`:
100
101
  - `~/.local/share/opencode/mcp-auth.json`
101
102
  - Any extra paths in `extraSecretPaths` (allowlist)
102
103
 
104
+ MCP API keys stored inside `opencode.json(c)` are **not** committed by default. To allow them
105
+ in a private repo, set `"includeMcpSecrets": true` (requires `includeSecrets`).
106
+
103
107
  ### Sessions (private repos only)
104
108
 
105
109
  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.
@@ -146,6 +150,22 @@ Create a local-only overrides file at:
146
150
 
147
151
  Overrides are merged into the runtime config and re-applied to `opencode.json(c)` after pull.
148
152
 
153
+ ### MCP secret scrubbing
154
+
155
+ If your `opencode.json(c)` contains MCP secrets (for example `mcp.*.headers` or `mcp.*.oauth.clientSecret`), opencode-synced will automatically:
156
+
157
+ 1. Move the secret values into `opencode-synced.overrides.jsonc` (local-only).
158
+ 2. Replace the values in the synced config with `{env:...}` placeholders.
159
+
160
+ This keeps secrets out of the repo while preserving local behavior. On other machines, set the matching environment variables (or add local overrides).
161
+ If you want MCP secrets committed (private repos only), set `"includeMcpSecrets": true` alongside `"includeSecrets": true`.
162
+
163
+ Env var naming rules:
164
+
165
+ - If the header name already looks like an env var (e.g. `CONTEXT7_API_KEY`), it is used directly.
166
+ - Otherwise: `OPENCODE_MCP_<SERVER>_<HEADER>` (uppercase, non-alphanumerics become `_`).
167
+ - OAuth client secrets use `OPENCODE_MCP_<SERVER>_OAUTH_CLIENT_SECRET`.
168
+
149
169
  ## Usage
150
170
 
151
171
  | Command | Description |
@@ -4,3 +4,4 @@ description: Enable secrets sync (private repo required)
4
4
 
5
5
  Use the opencode_sync tool with command "enable-secrets".
6
6
  If the user supplies extra secret paths, pass them via extraSecretPaths.
7
+ If they want MCP secrets committed in a private repo, pass includeMcpSecrets: true.
@@ -9,3 +9,4 @@ If the user wants a custom repo name, pass name="custom-name".
9
9
  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
+ Include includeMcpSecrets only if they want MCP secrets committed to a private repo.
package/dist/index.js CHANGED
@@ -100,6 +100,10 @@ export const OpencodeConfigSync = async (ctx) => {
100
100
  url: tool.schema.string().optional().describe('Repo URL'),
101
101
  branch: tool.schema.string().optional().describe('Repo branch'),
102
102
  includeSecrets: tool.schema.boolean().optional().describe('Enable secrets sync'),
103
+ includeMcpSecrets: tool.schema
104
+ .boolean()
105
+ .optional()
106
+ .describe('Allow MCP secrets to be committed (requires includeSecrets)'),
103
107
  includeSessions: tool.schema
104
108
  .boolean()
105
109
  .optional()
@@ -126,6 +130,7 @@ export const OpencodeConfigSync = async (ctx) => {
126
130
  url: args.url,
127
131
  branch: args.branch,
128
132
  includeSecrets: args.includeSecrets,
133
+ includeMcpSecrets: args.includeMcpSecrets,
129
134
  includeSessions: args.includeSessions,
130
135
  includePromptStash: args.includePromptStash,
131
136
  create: args.create,
@@ -146,7 +151,10 @@ export const OpencodeConfigSync = async (ctx) => {
146
151
  return await service.push();
147
152
  }
148
153
  if (args.command === 'enable-secrets') {
149
- return await service.enableSecrets(args.extraSecretPaths);
154
+ return await service.enableSecrets({
155
+ extraSecretPaths: args.extraSecretPaths,
156
+ includeMcpSecrets: args.includeMcpSecrets,
157
+ });
150
158
  }
151
159
  if (args.command === 'resolve') {
152
160
  return await service.resolve();
@@ -1,3 +1,6 @@
1
1
  import type { SyncPlan } from './paths.js';
2
2
  export declare function syncRepoToLocal(plan: SyncPlan, overrides: Record<string, unknown> | null): Promise<void>;
3
- export declare function syncLocalToRepo(plan: SyncPlan, overrides: Record<string, unknown> | null): Promise<void>;
3
+ export declare function syncLocalToRepo(plan: SyncPlan, overrides: Record<string, unknown> | null, options?: {
4
+ overridesPath?: string;
5
+ allowMcpSecrets?: boolean;
6
+ }): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { deepMerge, parseJsonc, pathExists, stripOverrides, writeJsonFile } from './config.js';
3
+ import { deepMerge, hasOwn, parseJsonc, pathExists, stripOverrides, writeJsonFile, } from './config.js';
4
+ import { extractMcpSecrets, hasOverrides, mergeOverrides, stripOverrideKeys, } from './mcp-secrets.js';
4
5
  import { normalizePath } from './paths.js';
5
6
  export async function syncRepoToLocal(plan, overrides) {
6
7
  for (const item of plan.items) {
@@ -11,10 +12,39 @@ export async function syncRepoToLocal(plan, overrides) {
11
12
  await applyOverridesToLocalConfig(plan, overrides);
12
13
  }
13
14
  }
14
- export async function syncLocalToRepo(plan, overrides) {
15
+ export async function syncLocalToRepo(plan, overrides, options = {}) {
16
+ const configItems = plan.items.filter((item) => item.isConfigFile);
17
+ const sanitizedConfigs = new Map();
18
+ let secretOverrides = {};
19
+ const allowMcpSecrets = Boolean(options.allowMcpSecrets);
20
+ for (const item of configItems) {
21
+ if (!(await pathExists(item.localPath)))
22
+ continue;
23
+ const content = await fs.readFile(item.localPath, 'utf8');
24
+ const parsed = parseJsonc(content);
25
+ const { sanitizedConfig, secretOverrides: extracted } = extractMcpSecrets(parsed);
26
+ if (!allowMcpSecrets) {
27
+ sanitizedConfigs.set(item.localPath, sanitizedConfig);
28
+ }
29
+ if (hasOverrides(extracted)) {
30
+ secretOverrides = mergeOverrides(secretOverrides, extracted);
31
+ }
32
+ }
33
+ let overridesForStrip = overrides;
34
+ if (hasOverrides(secretOverrides)) {
35
+ if (!allowMcpSecrets) {
36
+ const baseOverrides = overrides ?? {};
37
+ const mergedOverrides = mergeOverrides(baseOverrides, secretOverrides);
38
+ if (options.overridesPath && !isDeepEqual(baseOverrides, mergedOverrides)) {
39
+ await writeJsonFile(options.overridesPath, mergedOverrides, { jsonc: true });
40
+ }
41
+ }
42
+ overridesForStrip = overrides ? stripOverrideKeys(overrides, secretOverrides) : overrides;
43
+ }
15
44
  for (const item of plan.items) {
16
- if (item.isConfigFile && overrides && Object.keys(overrides).length > 0) {
17
- await copyConfigForRepo(item, overrides, plan.repoRoot);
45
+ if (item.isConfigFile) {
46
+ const sanitized = sanitizedConfigs.get(item.localPath);
47
+ await copyConfigForRepo(item, overridesForStrip, plan.repoRoot, sanitized);
18
48
  continue;
19
49
  }
20
50
  await copyItem(item.localPath, item.repoPath, item.type, true);
@@ -35,21 +65,22 @@ async function copyItem(sourcePath, destinationPath, type, removeWhenMissing = f
35
65
  await removePath(destinationPath);
36
66
  await copyDirRecursive(sourcePath, destinationPath);
37
67
  }
38
- async function copyConfigForRepo(item, overrides, repoRoot) {
68
+ async function copyConfigForRepo(item, overrides, repoRoot, configOverride) {
39
69
  if (!(await pathExists(item.localPath))) {
40
70
  await removePath(item.repoPath);
41
71
  return;
42
72
  }
43
- const localContent = await fs.readFile(item.localPath, 'utf8');
44
- const localConfig = parseJsonc(localContent);
73
+ const localConfig = configOverride ??
74
+ parseJsonc(await fs.readFile(item.localPath, 'utf8'));
45
75
  const baseConfig = await readRepoConfig(item, repoRoot);
76
+ const effectiveOverrides = overrides ?? {};
46
77
  if (baseConfig) {
47
- const expectedLocal = deepMerge(baseConfig, overrides);
78
+ const expectedLocal = deepMerge(baseConfig, effectiveOverrides);
48
79
  if (isDeepEqual(localConfig, expectedLocal)) {
49
80
  return;
50
81
  }
51
82
  }
52
- const stripped = stripOverrides(localConfig, overrides, baseConfig);
83
+ const stripped = stripOverrides(localConfig, effectiveOverrides, baseConfig);
53
84
  const stat = await fs.stat(item.localPath);
54
85
  await fs.mkdir(path.dirname(item.repoPath), { recursive: true });
55
86
  await writeJsonFile(item.repoPath, stripped, {
@@ -183,7 +214,7 @@ function isDeepEqual(left, right) {
183
214
  if (leftKeys.length !== rightKeys.length)
184
215
  return false;
185
216
  for (const key of leftKeys) {
186
- if (!Object.hasOwn(right, key))
217
+ if (!hasOwn(right, key))
187
218
  return false;
188
219
  if (!isDeepEqual(left[key], right[key])) {
189
220
  return false;
@@ -9,6 +9,7 @@ export interface SyncConfig {
9
9
  repo?: SyncRepoConfig;
10
10
  localRepoPath?: string;
11
11
  includeSecrets?: boolean;
12
+ includeMcpSecrets?: boolean;
12
13
  includeSessions?: boolean;
13
14
  includePromptStash?: boolean;
14
15
  extraSecretPaths?: string[];
@@ -20,6 +21,7 @@ export interface SyncState {
20
21
  }
21
22
  export declare function pathExists(filePath: string): Promise<boolean>;
22
23
  export declare function normalizeSyncConfig(config: SyncConfig): SyncConfig;
24
+ export declare function canCommitMcpSecrets(config: SyncConfig): boolean;
23
25
  export declare function loadSyncConfig(locations: SyncLocations): Promise<SyncConfig | null>;
24
26
  export declare function writeSyncConfig(locations: SyncLocations, config: SyncConfig): Promise<void>;
25
27
  export declare function loadOverrides(locations: SyncLocations): Promise<Record<string, unknown> | null>;
@@ -33,3 +35,5 @@ export declare function writeJsonFile(filePath: string, data: unknown, options?:
33
35
  jsonc: boolean;
34
36
  mode?: number;
35
37
  }): Promise<void>;
38
+ export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
39
+ export declare function hasOwn(target: Record<string, unknown>, key: string): boolean;
@@ -10,8 +10,10 @@ export async function pathExists(filePath) {
10
10
  }
11
11
  }
12
12
  export function normalizeSyncConfig(config) {
13
+ const includeSecrets = Boolean(config.includeSecrets);
13
14
  return {
14
- includeSecrets: Boolean(config.includeSecrets),
15
+ includeSecrets,
16
+ includeMcpSecrets: includeSecrets ? Boolean(config.includeMcpSecrets) : false,
15
17
  includeSessions: Boolean(config.includeSessions),
16
18
  includePromptStash: Boolean(config.includePromptStash),
17
19
  extraSecretPaths: Array.isArray(config.extraSecretPaths) ? config.extraSecretPaths : [],
@@ -19,6 +21,9 @@ export function normalizeSyncConfig(config) {
19
21
  repo: config.repo,
20
22
  };
21
23
  }
24
+ export function canCommitMcpSecrets(config) {
25
+ return Boolean(config.includeSecrets) && Boolean(config.includeMcpSecrets);
26
+ }
22
27
  export async function loadSyncConfig(locations) {
23
28
  if (!(await pathExists(locations.syncConfigPath))) {
24
29
  return null;
@@ -112,11 +117,14 @@ export async function writeJsonFile(filePath, data, options = { jsonc: false })
112
117
  await fs.chmod(filePath, options.mode);
113
118
  }
114
119
  }
115
- function isPlainObject(value) {
120
+ export function isPlainObject(value) {
116
121
  if (!value || typeof value !== 'object')
117
122
  return false;
118
123
  return Object.getPrototypeOf(value) === Object.prototype;
119
124
  }
125
+ export function hasOwn(target, key) {
126
+ return Object.hasOwn(target, key);
127
+ }
120
128
  function stripJsonComments(input) {
121
129
  let output = '';
122
130
  let inString = false;
@@ -0,0 +1,8 @@
1
+ export interface McpSecretExtraction {
2
+ sanitizedConfig: Record<string, unknown>;
3
+ secretOverrides: Record<string, unknown>;
4
+ }
5
+ export declare function extractMcpSecrets(config: Record<string, unknown>): McpSecretExtraction;
6
+ export declare function mergeOverrides(base: Record<string, unknown>, extra: Record<string, unknown>): Record<string, unknown>;
7
+ export declare function stripOverrideKeys(base: Record<string, unknown>, toRemove: Record<string, unknown>): Record<string, unknown>;
8
+ export declare function hasOverrides(value: Record<string, unknown>): boolean;
@@ -0,0 +1,122 @@
1
+ import { deepMerge, hasOwn, isPlainObject } from './config.js';
2
+ const ENV_PLACEHOLDER_PATTERN = /\{env:[^}]+\}/i;
3
+ export function extractMcpSecrets(config) {
4
+ const sanitizedConfig = cloneConfig(config);
5
+ const secretOverrides = {};
6
+ const mcp = getPlainObject(sanitizedConfig.mcp);
7
+ if (!mcp) {
8
+ return { sanitizedConfig, secretOverrides };
9
+ }
10
+ for (const [serverName, serverConfigValue] of Object.entries(mcp)) {
11
+ const serverConfig = getPlainObject(serverConfigValue);
12
+ if (!serverConfig)
13
+ continue;
14
+ const headers = getPlainObject(serverConfig.headers);
15
+ if (headers) {
16
+ for (const [headerName, headerValue] of Object.entries(headers)) {
17
+ if (!isSecretString(headerValue))
18
+ continue;
19
+ const envVar = buildHeaderEnvVar(serverName, headerName);
20
+ const placeholder = buildHeaderPlaceholder(String(headerValue), envVar, headerName);
21
+ headers[headerName] = placeholder;
22
+ setNestedValue(secretOverrides, ['mcp', serverName, 'headers', headerName], headerValue);
23
+ }
24
+ }
25
+ const oauth = getPlainObject(serverConfig.oauth);
26
+ if (oauth) {
27
+ const clientSecret = oauth.clientSecret;
28
+ if (isSecretString(clientSecret)) {
29
+ const envVar = buildEnvVar(serverName, 'OAUTH_CLIENT_SECRET');
30
+ oauth.clientSecret = `{env:${envVar}}`;
31
+ setNestedValue(secretOverrides, ['mcp', serverName, 'oauth', 'clientSecret'], clientSecret);
32
+ }
33
+ }
34
+ }
35
+ return { sanitizedConfig, secretOverrides };
36
+ }
37
+ function isSecretString(value) {
38
+ return typeof value === 'string' && value.length > 0 && !ENV_PLACEHOLDER_PATTERN.test(value);
39
+ }
40
+ function buildHeaderEnvVar(serverName, headerName) {
41
+ if (/^[A-Z0-9_]+$/.test(headerName)) {
42
+ return headerName;
43
+ }
44
+ return buildEnvVar(serverName, headerName);
45
+ }
46
+ function buildEnvVar(serverName, key) {
47
+ const serverToken = toEnvToken(serverName, 'SERVER');
48
+ const keyToken = toEnvToken(key, 'VALUE');
49
+ return `OPENCODE_MCP_${serverToken}_${keyToken}`;
50
+ }
51
+ function toEnvToken(input, fallback) {
52
+ const cleaned = String(input)
53
+ .trim()
54
+ .replace(/[^a-zA-Z0-9]+/g, '_')
55
+ .replace(/^_+|_+$/g, '');
56
+ if (!cleaned)
57
+ return fallback;
58
+ return cleaned.toUpperCase();
59
+ }
60
+ function buildHeaderPlaceholder(value, envVar, headerName) {
61
+ if (!isAuthorizationHeader(headerName)) {
62
+ return `{env:${envVar}}`;
63
+ }
64
+ const schemeMatch = value.match(/^([A-Za-z][A-Za-z0-9+.-]*)\s+/);
65
+ if (schemeMatch) {
66
+ return `${schemeMatch[0]}{env:${envVar}}`;
67
+ }
68
+ return `{env:${envVar}}`;
69
+ }
70
+ function isAuthorizationHeader(headerName) {
71
+ if (!headerName)
72
+ return false;
73
+ const normalized = headerName.toLowerCase();
74
+ return normalized === 'authorization' || normalized === 'proxy-authorization';
75
+ }
76
+ function setNestedValue(target, path, value) {
77
+ let current = target;
78
+ for (let i = 0; i < path.length - 1; i += 1) {
79
+ const key = path[i];
80
+ const next = current[key];
81
+ if (!isPlainObject(next)) {
82
+ current[key] = {};
83
+ }
84
+ current = current[key];
85
+ }
86
+ current[path[path.length - 1]] = value;
87
+ }
88
+ function getPlainObject(value) {
89
+ return isPlainObject(value) ? value : null;
90
+ }
91
+ function cloneConfig(config) {
92
+ return JSON.parse(JSON.stringify(config));
93
+ }
94
+ export function mergeOverrides(base, extra) {
95
+ return deepMerge(base, extra);
96
+ }
97
+ export function stripOverrideKeys(base, toRemove) {
98
+ if (!isPlainObject(base) || !isPlainObject(toRemove)) {
99
+ return base;
100
+ }
101
+ const result = { ...base };
102
+ for (const [key, removeValue] of Object.entries(toRemove)) {
103
+ if (!hasOwn(result, key))
104
+ continue;
105
+ const currentValue = result[key];
106
+ if (isPlainObject(removeValue) && isPlainObject(currentValue)) {
107
+ const stripped = stripOverrideKeys(currentValue, removeValue);
108
+ if (Object.keys(stripped).length === 0) {
109
+ delete result[key];
110
+ }
111
+ else {
112
+ result[key] = stripped;
113
+ }
114
+ continue;
115
+ }
116
+ delete result[key];
117
+ }
118
+ return result;
119
+ }
120
+ export function hasOverrides(value) {
121
+ return Object.keys(value).length > 0;
122
+ }
@@ -7,6 +7,7 @@ interface InitOptions {
7
7
  url?: string;
8
8
  branch?: string;
9
9
  includeSecrets?: boolean;
10
+ includeMcpSecrets?: boolean;
10
11
  includeSessions?: boolean;
11
12
  includePromptStash?: boolean;
12
13
  create?: boolean;
@@ -24,7 +25,10 @@ export interface SyncService {
24
25
  link: (_options: LinkOptions) => Promise<string>;
25
26
  pull: () => Promise<string>;
26
27
  push: () => Promise<string>;
27
- enableSecrets: (_extraSecretPaths?: string[]) => Promise<string>;
28
+ enableSecrets: (_options?: {
29
+ extraSecretPaths?: string[];
30
+ includeMcpSecrets?: boolean;
31
+ }) => Promise<string>;
28
32
  resolve: () => Promise<string>;
29
33
  }
30
34
  export declare function createSyncService(ctx: SyncServiceContext): SyncService;
@@ -1,6 +1,6 @@
1
1
  import { syncLocalToRepo, syncRepoToLocal } from './apply.js';
2
2
  import { generateCommitMessage } from './commit.js';
3
- import { loadOverrides, loadState, loadSyncConfig, normalizeSyncConfig, writeState, writeSyncConfig, } from './config.js';
3
+ import { canCommitMcpSecrets, loadOverrides, loadState, loadSyncConfig, normalizeSyncConfig, writeState, writeSyncConfig, } from './config.js';
4
4
  import { SyncCommandError, SyncConfigMissingError } from './errors.js';
5
5
  import { buildSyncPlan, resolveRepoRoot, resolveSyncLocations } from './paths.js';
6
6
  import { commitAll, ensureRepoCloned, ensureRepoPrivate, fetchAndFastForward, findSyncRepo, getAuthenticatedUser, getRepoStatus, hasLocalChanges, isRepoCloned, pushBranch, repoExists, resolveRepoBranch, resolveRepoIdentifier, } from './repo.js';
@@ -12,7 +12,7 @@ export function createSyncService(ctx) {
12
12
  startupSync: async () => {
13
13
  const config = await loadSyncConfig(locations);
14
14
  if (!config) {
15
- await showToast(ctx.client, 'Configure opencode-synced with /sync-init.', 'info');
15
+ await showToast(ctx.client, 'Configure opencode-synced with /sync-init or link to an existing repo with /sync-link', 'info');
16
16
  return;
17
17
  }
18
18
  try {
@@ -48,6 +48,7 @@ export function createSyncService(ctx) {
48
48
  }
49
49
  const repoIdentifier = resolveRepoIdentifier(config);
50
50
  const includeSecrets = config.includeSecrets ? 'enabled' : 'disabled';
51
+ const includeMcpSecrets = config.includeMcpSecrets ? 'enabled' : 'disabled';
51
52
  const includeSessions = config.includeSessions ? 'enabled' : 'disabled';
52
53
  const includePromptStash = config.includePromptStash ? 'enabled' : 'disabled';
53
54
  const lastPull = state.lastPull ?? 'never';
@@ -68,6 +69,7 @@ export function createSyncService(ctx) {
68
69
  `Repo: ${repoIdentifier}`,
69
70
  `Branch: ${branch}`,
70
71
  `Secrets: ${includeSecrets}`,
72
+ `MCP secrets: ${includeMcpSecrets}`,
71
73
  `Sessions: ${includeSessions}`,
72
74
  `Prompt stash: ${includePromptStash}`,
73
75
  `Last pull: ${lastPull}`,
@@ -93,7 +95,10 @@ export function createSyncService(ctx) {
93
95
  if (created) {
94
96
  const overrides = await loadOverrides(locations);
95
97
  const plan = buildSyncPlan(config, locations, repoRoot);
96
- await syncLocalToRepo(plan, overrides);
98
+ await syncLocalToRepo(plan, overrides, {
99
+ overridesPath: locations.overridesPath,
100
+ allowMcpSecrets: canCommitMcpSecrets(config),
101
+ });
97
102
  const dirty = await hasLocalChanges(ctx.$, repoRoot);
98
103
  if (dirty) {
99
104
  const branch = resolveRepoBranch(config);
@@ -130,6 +135,7 @@ export function createSyncService(ctx) {
130
135
  const config = normalizeSyncConfig({
131
136
  repo: { owner: found.owner, name: found.name },
132
137
  includeSecrets: false,
138
+ includeMcpSecrets: false,
133
139
  includeSessions: false,
134
140
  includePromptStash: false,
135
141
  extraSecretPaths: [],
@@ -197,7 +203,10 @@ export function createSyncService(ctx) {
197
203
  }
198
204
  const overrides = await loadOverrides(locations);
199
205
  const plan = buildSyncPlan(config, locations, repoRoot);
200
- await syncLocalToRepo(plan, overrides);
206
+ await syncLocalToRepo(plan, overrides, {
207
+ overridesPath: locations.overridesPath,
208
+ allowMcpSecrets: canCommitMcpSecrets(config),
209
+ });
201
210
  const dirty = await hasLocalChanges(ctx.$, repoRoot);
202
211
  if (!dirty) {
203
212
  return 'No local changes to push.';
@@ -210,11 +219,14 @@ export function createSyncService(ctx) {
210
219
  });
211
220
  return `Pushed changes: ${message}`;
212
221
  },
213
- enableSecrets: async (extraSecretPaths) => {
222
+ enableSecrets: async (options) => {
214
223
  const config = await getConfigOrThrow(locations);
215
224
  config.includeSecrets = true;
216
- if (extraSecretPaths) {
217
- config.extraSecretPaths = extraSecretPaths;
225
+ if (options?.extraSecretPaths) {
226
+ config.extraSecretPaths = options.extraSecretPaths;
227
+ }
228
+ if (options?.includeMcpSecrets !== undefined) {
229
+ config.includeMcpSecrets = options.includeMcpSecrets;
218
230
  }
219
231
  await ensureRepoPrivate(ctx.$, config);
220
232
  await writeSyncConfig(locations, config);
@@ -277,7 +289,10 @@ async function runStartup(ctx, locations, config, log) {
277
289
  }
278
290
  const overrides = await loadOverrides(locations);
279
291
  const plan = buildSyncPlan(config, locations, repoRoot);
280
- await syncLocalToRepo(plan, overrides);
292
+ await syncLocalToRepo(plan, overrides, {
293
+ overridesPath: locations.overridesPath,
294
+ allowMcpSecrets: canCommitMcpSecrets(config),
295
+ });
281
296
  const changes = await hasLocalChanges(ctx.$, repoRoot);
282
297
  if (!changes) {
283
298
  log.debug('No local changes to push');
@@ -318,6 +333,7 @@ async function buildConfigFromInit($, options) {
318
333
  return normalizeSyncConfig({
319
334
  repo,
320
335
  includeSecrets: options.includeSecrets ?? false,
336
+ includeMcpSecrets: options.includeMcpSecrets ?? false,
321
337
  includeSessions: options.includeSessions ?? false,
322
338
  includePromptStash: options.includePromptStash ?? false,
323
339
  extraSecretPaths: options.extraSecretPaths ?? [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-synced",
3
- "version": "0.4.2",
3
+ "version": "0.5.1",
4
4
  "description": "Sync global OpenCode config across machines via GitHub.",
5
5
  "author": {
6
6
  "name": "Ian Hildebrand"
@@ -22,7 +22,9 @@
22
22
  "publishConfig": {
23
23
  "access": "public"
24
24
  },
25
- "files": ["dist"],
25
+ "files": [
26
+ "dist"
27
+ ],
26
28
  "dependencies": {
27
29
  "@opencode-ai/plugin": "1.0.85"
28
30
  },
@@ -49,6 +51,8 @@
49
51
  "prepare": "husky"
50
52
  },
51
53
  "lint-staged": {
52
- "*.{js,ts,json}": ["biome check --write --no-errors-on-unmatched"]
54
+ "*.{js,ts,json}": [
55
+ "biome check --write --no-errors-on-unmatched"
56
+ ]
53
57
  }
54
58
  }