opencode-synced 0.4.1 → 0.5.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
@@ -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 |
@@ -247,6 +267,25 @@ bun -e '
247
267
  - `bun run test`
248
268
  - `bun run lint`
249
269
 
270
+ ### Local testing (production-like)
271
+
272
+ To test the same artifact that would be published, install from a packed tarball
273
+ into OpenCode's cache:
274
+
275
+ ```bash
276
+ mise run local-pack-test
277
+ ```
278
+
279
+ Then set `~/.config/opencode/opencode.json` to use:
280
+
281
+ ```jsonc
282
+ {
283
+ "plugin": ["opencode-synced"]
284
+ }
285
+ ```
286
+
287
+ Restart OpenCode to pick up the cached install.
288
+
250
289
 
251
290
  ## Prefer a CLI version?
252
291
 
@@ -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.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  import type { Plugin } from '@opencode-ai/plugin';
2
2
  export declare const OpencodeConfigSync: Plugin;
3
+ export declare const OpencodeSynced: Plugin;
4
+ export default OpencodeConfigSync;
package/dist/index.js CHANGED
@@ -59,17 +59,29 @@ async function scanMdFiles(dir) {
59
59
  async function loadCommands() {
60
60
  const commands = [];
61
61
  const commandDir = path.join(getModuleDir(), 'command');
62
+ try {
63
+ const stats = await fs.stat(commandDir);
64
+ if (!stats.isDirectory()) {
65
+ return commands;
66
+ }
67
+ }
68
+ catch {
69
+ return commands;
70
+ }
62
71
  const files = await scanMdFiles(commandDir);
63
72
  for (const file of files) {
64
- const content = await fs.readFile(file, 'utf-8');
65
- const { frontmatter, body } = parseFrontmatter(content);
66
- const relativePath = path.relative(commandDir, file);
67
- const name = relativePath.replace(/\.md$/, '').replace(/\//g, '-');
68
- commands.push({
69
- name,
70
- frontmatter,
71
- template: body,
72
- });
73
+ try {
74
+ const content = await fs.readFile(file, 'utf-8');
75
+ const { frontmatter, body } = parseFrontmatter(content);
76
+ const relativePath = path.relative(commandDir, file);
77
+ const name = relativePath.replace(/\.md$/, '').replace(/\//g, '-');
78
+ commands.push({
79
+ name,
80
+ frontmatter,
81
+ template: body,
82
+ });
83
+ }
84
+ catch { }
73
85
  }
74
86
  return commands;
75
87
  }
@@ -88,6 +100,10 @@ export const OpencodeConfigSync = async (ctx) => {
88
100
  url: tool.schema.string().optional().describe('Repo URL'),
89
101
  branch: tool.schema.string().optional().describe('Repo branch'),
90
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)'),
91
107
  includeSessions: tool.schema
92
108
  .boolean()
93
109
  .optional()
@@ -114,6 +130,7 @@ export const OpencodeConfigSync = async (ctx) => {
114
130
  url: args.url,
115
131
  branch: args.branch,
116
132
  includeSecrets: args.includeSecrets,
133
+ includeMcpSecrets: args.includeMcpSecrets,
117
134
  includeSessions: args.includeSessions,
118
135
  includePromptStash: args.includePromptStash,
119
136
  create: args.create,
@@ -134,7 +151,10 @@ export const OpencodeConfigSync = async (ctx) => {
134
151
  return await service.push();
135
152
  }
136
153
  if (args.command === 'enable-secrets') {
137
- return await service.enableSecrets(args.extraSecretPaths);
154
+ return await service.enableSecrets({
155
+ extraSecretPaths: args.extraSecretPaths,
156
+ includeMcpSecrets: args.includeMcpSecrets,
157
+ });
138
158
  }
139
159
  if (args.command === 'resolve') {
140
160
  return await service.resolve();
@@ -180,6 +200,8 @@ export const OpencodeConfigSync = async (ctx) => {
180
200
  },
181
201
  };
182
202
  };
203
+ export const OpencodeSynced = OpencodeConfigSync;
204
+ export default OpencodeConfigSync;
183
205
  function formatError(error) {
184
206
  if (error instanceof Error)
185
207
  return error.message;
@@ -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
3
  import { deepMerge, 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, {
@@ -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>;
@@ -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;
@@ -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,130 @@
1
+ import { deepMerge } 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 isPlainObject(value) {
92
+ if (!value || typeof value !== 'object')
93
+ return false;
94
+ return Object.getPrototypeOf(value) === Object.prototype;
95
+ }
96
+ function cloneConfig(config) {
97
+ if (typeof structuredClone === 'function') {
98
+ return structuredClone(config);
99
+ }
100
+ return JSON.parse(JSON.stringify(config));
101
+ }
102
+ export function mergeOverrides(base, extra) {
103
+ return deepMerge(base, extra);
104
+ }
105
+ export function stripOverrideKeys(base, toRemove) {
106
+ if (!isPlainObject(base) || !isPlainObject(toRemove)) {
107
+ return base;
108
+ }
109
+ const result = { ...base };
110
+ for (const [key, removeValue] of Object.entries(toRemove)) {
111
+ if (!Object.hasOwn(result, key))
112
+ continue;
113
+ const currentValue = result[key];
114
+ if (isPlainObject(removeValue) && isPlainObject(currentValue)) {
115
+ const stripped = stripOverrideKeys(currentValue, removeValue);
116
+ if (Object.keys(stripped).length === 0) {
117
+ delete result[key];
118
+ }
119
+ else {
120
+ result[key] = stripped;
121
+ }
122
+ continue;
123
+ }
124
+ delete result[key];
125
+ }
126
+ return result;
127
+ }
128
+ export function hasOverrides(value) {
129
+ return Object.keys(value).length > 0;
130
+ }
@@ -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,13 +1,16 @@
1
1
  {
2
2
  "name": "opencode-synced",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Sync global OpenCode config across machines via GitHub.",
5
5
  "author": {
6
6
  "name": "Ian Hildebrand"
7
7
  },
8
8
  "type": "module",
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
9
11
  "exports": {
10
12
  ".": {
13
+ "import": "./dist/index.js",
11
14
  "types": "./dist/index.d.ts",
12
15
  "default": "./dist/index.js"
13
16
  }
@@ -38,6 +41,7 @@
38
41
  },
39
42
  "scripts": {
40
43
  "build": "rm -rf dist && tsc -p tsconfig.build.json && cp -r src/command dist/command",
44
+ "prepack": "bun run build",
41
45
  "test": "vitest run",
42
46
  "test:watch": "vitest",
43
47
  "lint": "biome lint .",