opencode-synced 0.4.2 → 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 +20 -0
- package/dist/command/sync-enable-secrets.md +1 -0
- package/dist/command/sync-init.md +1 -0
- package/dist/index.js +9 -1
- package/dist/sync/apply.d.ts +4 -1
- package/dist/sync/apply.js +39 -8
- package/dist/sync/config.d.ts +2 -0
- package/dist/sync/config.js +6 -1
- package/dist/sync/mcp-secrets.d.ts +8 -0
- package/dist/sync/mcp-secrets.js +130 -0
- package/dist/sync/service.d.ts +5 -1
- package/dist/sync/service.js +24 -8
- package/package.json +7 -3
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(
|
|
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();
|
package/dist/sync/apply.d.ts
CHANGED
|
@@ -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
|
|
3
|
+
export declare function syncLocalToRepo(plan: SyncPlan, overrides: Record<string, unknown> | null, options?: {
|
|
4
|
+
overridesPath?: string;
|
|
5
|
+
allowMcpSecrets?: boolean;
|
|
6
|
+
}): Promise<void>;
|
package/dist/sync/apply.js
CHANGED
|
@@ -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
|
|
17
|
-
|
|
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
|
|
44
|
-
|
|
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,
|
|
78
|
+
const expectedLocal = deepMerge(baseConfig, effectiveOverrides);
|
|
48
79
|
if (isDeepEqual(localConfig, expectedLocal)) {
|
|
49
80
|
return;
|
|
50
81
|
}
|
|
51
82
|
}
|
|
52
|
-
const stripped = stripOverrides(localConfig,
|
|
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, {
|
package/dist/sync/config.d.ts
CHANGED
|
@@ -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>;
|
package/dist/sync/config.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/dist/sync/service.d.ts
CHANGED
|
@@ -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: (
|
|
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;
|
package/dist/sync/service.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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.
|
|
3
|
+
"version": "0.5.0",
|
|
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": [
|
|
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}": [
|
|
54
|
+
"*.{js,ts,json}": [
|
|
55
|
+
"biome check --write --no-errors-on-unmatched"
|
|
56
|
+
]
|
|
53
57
|
}
|
|
54
58
|
}
|