opencode-synced 0.1.0 → 0.1.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 +80 -11
- package/dist/index.js +288 -153
- package/package.json +16 -12
- package/dist/command/sync-enable-secrets.md +0 -5
- package/dist/command/sync-init.md +0 -11
- package/dist/command/sync-pull.md +0 -5
- package/dist/command/sync-push.md +0 -4
- package/dist/command/sync-resolve.md +0 -5
- package/dist/command/sync-status.md +0 -5
- package/dist/index.d.ts +0 -2
- package/dist/sync/apply.d.ts +0 -3
- package/dist/sync/commit.d.ts +0 -9
- package/dist/sync/config.d.ts +0 -35
- package/dist/sync/config.test.d.ts +0 -1
- package/dist/sync/errors.d.ts +0 -19
- package/dist/sync/paths.d.ts +0 -47
- package/dist/sync/paths.test.d.ts +0 -1
- package/dist/sync/repo.d.ts +0 -25
- package/dist/sync/repo.test.d.ts +0 -1
- package/dist/sync/service.d.ts +0 -27
- package/dist/sync/utils.d.ts +0 -9
package/README.md
CHANGED
|
@@ -37,13 +37,25 @@ opencode
|
|
|
37
37
|
|
|
38
38
|
## Configure
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
### First machine (create new sync repo)
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
2. Create a private repo (`my-opencode-config` by default) if it doesn't exist
|
|
44
|
-
3. Clone the repo and set up sync
|
|
42
|
+
Run `/sync-init` to create a new sync repo:
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
1. Detects your GitHub username
|
|
45
|
+
2. Creates a private repo (`my-opencode-config` by default)
|
|
46
|
+
3. Clones the repo and pushes your current config
|
|
47
|
+
|
|
48
|
+
### Additional machines (link to existing repo)
|
|
49
|
+
|
|
50
|
+
Run `/sync-link` to connect to your existing sync repo:
|
|
51
|
+
|
|
52
|
+
1. Searches your GitHub for common sync repo names (prioritizes `my-opencode-config`)
|
|
53
|
+
2. Clones and applies the synced config
|
|
54
|
+
3. **Overwrites local config** with synced content (preserves your local overrides file)
|
|
55
|
+
|
|
56
|
+
If auto-detection fails, specify the repo name: `/sync-link my-opencode-config`
|
|
57
|
+
|
|
58
|
+
After linking, restart OpenCode to apply the synced settings.
|
|
47
59
|
|
|
48
60
|
### Custom repo name or org
|
|
49
61
|
|
|
@@ -136,12 +148,15 @@ Overrides are merged into the runtime config and re-applied to `opencode.json(c)
|
|
|
136
148
|
|
|
137
149
|
## Usage
|
|
138
150
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
151
|
+
| Command | Description |
|
|
152
|
+
|---------|-------------|
|
|
153
|
+
| `/sync-init` | Create a new sync repo (first machine) |
|
|
154
|
+
| `/sync-link` | Link to existing sync repo (additional machines) |
|
|
155
|
+
| `/sync-status` | Show repo status and last sync times |
|
|
156
|
+
| `/sync-pull` | Fetch and apply remote config |
|
|
157
|
+
| `/sync-push` | Commit and push local changes |
|
|
158
|
+
| `/sync-enable-secrets` | Enable secrets sync (private repos only) |
|
|
159
|
+
| `/sync-resolve` | Auto-resolve uncommitted changes using AI |
|
|
145
160
|
|
|
146
161
|
<details>
|
|
147
162
|
<summary>Manual sync (without slash commands)</summary>
|
|
@@ -177,8 +192,62 @@ git pull --rebase
|
|
|
177
192
|
|
|
178
193
|
Then re-run `/sync-pull` or `/sync-push`.
|
|
179
194
|
|
|
195
|
+
## Removal
|
|
196
|
+
|
|
197
|
+
<details>
|
|
198
|
+
<summary>How to completely remove and delete opencode-synced</summary>
|
|
199
|
+
|
|
200
|
+
Run this one-liner to remove the plugin from your config, delete local sync files, and delete the GitHub repository:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
bun -e '
|
|
204
|
+
const fs = require("node:fs"), path = require("node:path"), os = require("node:os"), { spawnSync } = require("node:child_process");
|
|
205
|
+
const isWin = os.platform() === "win32", home = os.homedir();
|
|
206
|
+
const configDir = isWin ? path.join(process.env.APPDATA, "opencode") : path.join(home, ".config", "opencode");
|
|
207
|
+
const dataDir = isWin ? path.join(process.env.LOCALAPPDATA, "opencode") : path.join(home, ".local", "share", "opencode");
|
|
208
|
+
["opencode.json", "opencode.jsonc"].forEach(f => {
|
|
209
|
+
const p = path.join(configDir, f);
|
|
210
|
+
if (fs.existsSync(p)) {
|
|
211
|
+
const c = fs.readFileSync(p, "utf8"), u = c.replace(/"opencode-synced"\s*,?\s*/g, "").replace(/,\s*\]/g, "]");
|
|
212
|
+
if (c !== u) fs.writeFileSync(p, u);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
const scp = path.join(configDir, "opencode-synced.jsonc");
|
|
216
|
+
if (fs.existsSync(scp)) {
|
|
217
|
+
try {
|
|
218
|
+
const c = JSON.parse(fs.readFileSync(scp, "utf8").replace(/\/\/.*/g, ""));
|
|
219
|
+
if (c.repo?.owner && c.repo?.name) {
|
|
220
|
+
const res = spawnSync("gh", ["repo", "delete", `${c.repo.owner}/${c.repo.name}`, "--yes"], { stdio: "inherit" });
|
|
221
|
+
if (res.status !== 0) console.log("\nNote: Repository delete failed. If it is a permission error, run: gh auth refresh -s delete_repo\n");
|
|
222
|
+
}
|
|
223
|
+
} catch (e) {}
|
|
224
|
+
}
|
|
225
|
+
[scp, path.join(configDir, "opencode-synced.overrides.jsonc"), path.join(dataDir, "sync-state.json"), path.join(dataDir, "opencode-synced")].forEach(p => {
|
|
226
|
+
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
|
227
|
+
});
|
|
228
|
+
console.log("opencode-synced removed.");
|
|
229
|
+
'
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Manual steps
|
|
233
|
+
1. Remove `"opencode-synced"` from the `plugin` array in `~/.config/opencode/opencode.json` (or `.jsonc`).
|
|
234
|
+
2. Delete the local configuration and state:
|
|
235
|
+
```bash
|
|
236
|
+
rm ~/.config/opencode/opencode-synced.jsonc
|
|
237
|
+
rm ~/.local/share/opencode/sync-state.json
|
|
238
|
+
rm -rf ~/.local/share/opencode/opencode-synced
|
|
239
|
+
```
|
|
240
|
+
3. (Optional) Delete the backup repository on GitHub via the web UI or `gh repo delete`.
|
|
241
|
+
|
|
242
|
+
</details>
|
|
243
|
+
|
|
180
244
|
## Development
|
|
181
245
|
|
|
182
246
|
- `bun run build`
|
|
183
247
|
- `bun run test`
|
|
184
248
|
- `bun run lint`
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
## Prefer a CLI version?
|
|
252
|
+
|
|
253
|
+
I stumbled upon [opencodesync](https://www.npmjs.com/package/opencodesync) while publishing this plugin.
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,9 @@ var __export = (target, all) => {
|
|
|
10
10
|
});
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
// src/index.ts
|
|
14
|
+
import path5 from "path";
|
|
15
|
+
|
|
13
16
|
// node_modules/zod/v4/classic/external.js
|
|
14
17
|
var exports_external = {};
|
|
15
18
|
__export(exports_external, {
|
|
@@ -12330,9 +12333,6 @@ function tool(input) {
|
|
|
12330
12333
|
return input;
|
|
12331
12334
|
}
|
|
12332
12335
|
tool.schema = exports_external;
|
|
12333
|
-
// src/index.ts
|
|
12334
|
-
import path5 from "path";
|
|
12335
|
-
|
|
12336
12336
|
// src/sync/config.ts
|
|
12337
12337
|
import { promises as fs } from "fs";
|
|
12338
12338
|
import path from "path";
|
|
@@ -12722,120 +12722,6 @@ function buildSyncPlan(config2, locations, repoRoot, platform = process.platform
|
|
|
12722
12722
|
};
|
|
12723
12723
|
}
|
|
12724
12724
|
|
|
12725
|
-
// src/sync/utils.ts
|
|
12726
|
-
function unwrapData(response) {
|
|
12727
|
-
if (!response || typeof response !== "object")
|
|
12728
|
-
return null;
|
|
12729
|
-
const maybeError = response.error;
|
|
12730
|
-
if (maybeError)
|
|
12731
|
-
return null;
|
|
12732
|
-
if ("data" in response) {
|
|
12733
|
-
const data = response.data;
|
|
12734
|
-
if (data !== undefined)
|
|
12735
|
-
return data;
|
|
12736
|
-
return null;
|
|
12737
|
-
}
|
|
12738
|
-
return response;
|
|
12739
|
-
}
|
|
12740
|
-
function extractTextFromResponse(response) {
|
|
12741
|
-
if (!response || typeof response !== "object")
|
|
12742
|
-
return null;
|
|
12743
|
-
const parts = response.parts ?? response.info?.parts ?? [];
|
|
12744
|
-
const textPart = parts.find((part) => part.type === "text" && part.text);
|
|
12745
|
-
return textPart?.text?.trim() ?? null;
|
|
12746
|
-
}
|
|
12747
|
-
async function resolveSmallModel(client) {
|
|
12748
|
-
try {
|
|
12749
|
-
const response = await client.config.get();
|
|
12750
|
-
const config2 = unwrapData(response);
|
|
12751
|
-
if (!config2)
|
|
12752
|
-
return null;
|
|
12753
|
-
const modelValue = config2.small_model ?? config2.model;
|
|
12754
|
-
if (!modelValue)
|
|
12755
|
-
return null;
|
|
12756
|
-
const [providerID, modelID] = modelValue.split("/", 2);
|
|
12757
|
-
if (!providerID || !modelID)
|
|
12758
|
-
return null;
|
|
12759
|
-
return { providerID, modelID };
|
|
12760
|
-
} catch {
|
|
12761
|
-
return null;
|
|
12762
|
-
}
|
|
12763
|
-
}
|
|
12764
|
-
|
|
12765
|
-
// src/sync/commit.ts
|
|
12766
|
-
async function generateCommitMessage(ctx, repoDir, fallbackDate = new Date) {
|
|
12767
|
-
const fallback = `Sync OpenCode config (${formatDate(fallbackDate)})`;
|
|
12768
|
-
const diffSummary = await getDiffSummary(ctx.$, repoDir);
|
|
12769
|
-
if (!diffSummary)
|
|
12770
|
-
return fallback;
|
|
12771
|
-
const model = await resolveSmallModel(ctx.client);
|
|
12772
|
-
if (!model)
|
|
12773
|
-
return fallback;
|
|
12774
|
-
const prompt = [
|
|
12775
|
-
"Generate a concise single-line git commit message (max 72 chars).",
|
|
12776
|
-
"Focus on OpenCode config sync changes.",
|
|
12777
|
-
"Return only the message, no quotes.",
|
|
12778
|
-
"",
|
|
12779
|
-
"Diff summary:",
|
|
12780
|
-
diffSummary
|
|
12781
|
-
].join(`
|
|
12782
|
-
`);
|
|
12783
|
-
let sessionId = null;
|
|
12784
|
-
try {
|
|
12785
|
-
const sessionResult = await ctx.client.session.create({ body: { title: "opencode-synced" } });
|
|
12786
|
-
const session = unwrapData(sessionResult);
|
|
12787
|
-
sessionId = session?.id ?? null;
|
|
12788
|
-
if (!sessionId)
|
|
12789
|
-
return fallback;
|
|
12790
|
-
const response = await ctx.client.session.prompt({
|
|
12791
|
-
path: { id: sessionId },
|
|
12792
|
-
body: {
|
|
12793
|
-
model,
|
|
12794
|
-
parts: [{ type: "text", text: prompt }]
|
|
12795
|
-
}
|
|
12796
|
-
});
|
|
12797
|
-
const message = extractTextFromResponse(unwrapData(response) ?? response);
|
|
12798
|
-
if (!message)
|
|
12799
|
-
return fallback;
|
|
12800
|
-
const sanitized = sanitizeMessage(message);
|
|
12801
|
-
return sanitized || fallback;
|
|
12802
|
-
} catch {
|
|
12803
|
-
return fallback;
|
|
12804
|
-
} finally {
|
|
12805
|
-
if (sessionId) {
|
|
12806
|
-
try {
|
|
12807
|
-
await ctx.client.session.delete({ path: { id: sessionId } });
|
|
12808
|
-
} catch {}
|
|
12809
|
-
}
|
|
12810
|
-
}
|
|
12811
|
-
}
|
|
12812
|
-
function sanitizeMessage(message) {
|
|
12813
|
-
const firstLine = message.split(`
|
|
12814
|
-
`)[0].trim();
|
|
12815
|
-
const trimmed = firstLine.replace(/^["'`]+|["'`]+$/g, "").trim();
|
|
12816
|
-
if (!trimmed)
|
|
12817
|
-
return "";
|
|
12818
|
-
if (trimmed.length <= 72)
|
|
12819
|
-
return trimmed;
|
|
12820
|
-
return trimmed.slice(0, 72).trim();
|
|
12821
|
-
}
|
|
12822
|
-
async function getDiffSummary($, repoDir) {
|
|
12823
|
-
try {
|
|
12824
|
-
const nameStatus = await $`git -C ${repoDir} diff --name-status`.text();
|
|
12825
|
-
const stats = await $`git -C ${repoDir} diff --stat`.text();
|
|
12826
|
-
return [nameStatus.trim(), stats.trim()].filter(Boolean).join(`
|
|
12827
|
-
`);
|
|
12828
|
-
} catch {
|
|
12829
|
-
return "";
|
|
12830
|
-
}
|
|
12831
|
-
}
|
|
12832
|
-
function formatDate(date5) {
|
|
12833
|
-
const year = String(date5.getFullYear());
|
|
12834
|
-
const month = String(date5.getMonth() + 1).padStart(2, "0");
|
|
12835
|
-
const day = String(date5.getDate()).padStart(2, "0");
|
|
12836
|
-
return `${year}-${month}-${day}`;
|
|
12837
|
-
}
|
|
12838
|
-
|
|
12839
12725
|
// src/sync/apply.ts
|
|
12840
12726
|
import { promises as fs2 } from "fs";
|
|
12841
12727
|
import path3 from "path";
|
|
@@ -13018,7 +12904,7 @@ function isDeepEqual(left, right) {
|
|
|
13018
12904
|
if (leftKeys.length !== rightKeys.length)
|
|
13019
12905
|
return false;
|
|
13020
12906
|
for (const key of leftKeys) {
|
|
13021
|
-
if (!Object.
|
|
12907
|
+
if (!Object.hasOwn(right, key))
|
|
13022
12908
|
return false;
|
|
13023
12909
|
if (!isDeepEqual(left[key], right[key])) {
|
|
13024
12910
|
return false;
|
|
@@ -13029,6 +12915,147 @@ function isDeepEqual(left, right) {
|
|
|
13029
12915
|
return false;
|
|
13030
12916
|
}
|
|
13031
12917
|
|
|
12918
|
+
// src/sync/utils.ts
|
|
12919
|
+
var SERVICE_NAME = "opencode-synced";
|
|
12920
|
+
function createLogger(client) {
|
|
12921
|
+
return {
|
|
12922
|
+
debug: (message, extra) => log(client, "debug", message, extra),
|
|
12923
|
+
info: (message, extra) => log(client, "info", message, extra),
|
|
12924
|
+
warn: (message, extra) => log(client, "warn", message, extra),
|
|
12925
|
+
error: (message, extra) => log(client, "error", message, extra)
|
|
12926
|
+
};
|
|
12927
|
+
}
|
|
12928
|
+
function log(client, level, message, extra) {
|
|
12929
|
+
client.app.log({
|
|
12930
|
+
body: {
|
|
12931
|
+
service: SERVICE_NAME,
|
|
12932
|
+
level,
|
|
12933
|
+
message,
|
|
12934
|
+
extra
|
|
12935
|
+
}
|
|
12936
|
+
}).catch((err) => {
|
|
12937
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
12938
|
+
showToast(client, `Logging failed: ${errorMsg}`, "error");
|
|
12939
|
+
});
|
|
12940
|
+
}
|
|
12941
|
+
async function showToast(client, message, variant) {
|
|
12942
|
+
await client.tui.showToast({
|
|
12943
|
+
body: { title: "opencode-synced plugin", message, variant }
|
|
12944
|
+
});
|
|
12945
|
+
}
|
|
12946
|
+
function unwrapData(response) {
|
|
12947
|
+
if (!response || typeof response !== "object")
|
|
12948
|
+
return null;
|
|
12949
|
+
const maybeError = response.error;
|
|
12950
|
+
if (maybeError)
|
|
12951
|
+
return null;
|
|
12952
|
+
if ("data" in response) {
|
|
12953
|
+
const data = response.data;
|
|
12954
|
+
if (data !== undefined)
|
|
12955
|
+
return data;
|
|
12956
|
+
return null;
|
|
12957
|
+
}
|
|
12958
|
+
return response;
|
|
12959
|
+
}
|
|
12960
|
+
function extractTextFromResponse(response) {
|
|
12961
|
+
if (!response || typeof response !== "object")
|
|
12962
|
+
return null;
|
|
12963
|
+
const parts = response.parts ?? response.info?.parts ?? [];
|
|
12964
|
+
const textPart = parts.find((part) => part.type === "text" && part.text);
|
|
12965
|
+
return textPart?.text?.trim() ?? null;
|
|
12966
|
+
}
|
|
12967
|
+
async function resolveSmallModel(client) {
|
|
12968
|
+
try {
|
|
12969
|
+
const response = await client.config.get();
|
|
12970
|
+
const config2 = unwrapData(response);
|
|
12971
|
+
if (!config2)
|
|
12972
|
+
return null;
|
|
12973
|
+
const modelValue = config2.small_model ?? config2.model;
|
|
12974
|
+
if (!modelValue)
|
|
12975
|
+
return null;
|
|
12976
|
+
const [providerID, modelID] = modelValue.split("/", 2);
|
|
12977
|
+
if (!providerID || !modelID)
|
|
12978
|
+
return null;
|
|
12979
|
+
return { providerID, modelID };
|
|
12980
|
+
} catch {
|
|
12981
|
+
return null;
|
|
12982
|
+
}
|
|
12983
|
+
}
|
|
12984
|
+
|
|
12985
|
+
// src/sync/commit.ts
|
|
12986
|
+
async function generateCommitMessage(ctx, repoDir, fallbackDate = new Date) {
|
|
12987
|
+
const fallback = `Sync OpenCode config (${formatDate(fallbackDate)})`;
|
|
12988
|
+
const diffSummary = await getDiffSummary(ctx.$, repoDir);
|
|
12989
|
+
if (!diffSummary)
|
|
12990
|
+
return fallback;
|
|
12991
|
+
const model = await resolveSmallModel(ctx.client);
|
|
12992
|
+
if (!model)
|
|
12993
|
+
return fallback;
|
|
12994
|
+
const prompt = [
|
|
12995
|
+
"Generate a concise single-line git commit message (max 72 chars).",
|
|
12996
|
+
"Focus on OpenCode config sync changes.",
|
|
12997
|
+
"Return only the message, no quotes.",
|
|
12998
|
+
"",
|
|
12999
|
+
"Diff summary:",
|
|
13000
|
+
diffSummary
|
|
13001
|
+
].join(`
|
|
13002
|
+
`);
|
|
13003
|
+
let sessionId = null;
|
|
13004
|
+
try {
|
|
13005
|
+
const sessionResult = await ctx.client.session.create({ body: { title: "opencode-synced" } });
|
|
13006
|
+
const session = unwrapData(sessionResult);
|
|
13007
|
+
sessionId = session?.id ?? null;
|
|
13008
|
+
if (!sessionId)
|
|
13009
|
+
return fallback;
|
|
13010
|
+
const response = await ctx.client.session.prompt({
|
|
13011
|
+
path: { id: sessionId },
|
|
13012
|
+
body: {
|
|
13013
|
+
model,
|
|
13014
|
+
parts: [{ type: "text", text: prompt }]
|
|
13015
|
+
}
|
|
13016
|
+
});
|
|
13017
|
+
const message = extractTextFromResponse(unwrapData(response) ?? response);
|
|
13018
|
+
if (!message)
|
|
13019
|
+
return fallback;
|
|
13020
|
+
const sanitized = sanitizeMessage(message);
|
|
13021
|
+
return sanitized || fallback;
|
|
13022
|
+
} catch {
|
|
13023
|
+
return fallback;
|
|
13024
|
+
} finally {
|
|
13025
|
+
if (sessionId) {
|
|
13026
|
+
try {
|
|
13027
|
+
await ctx.client.session.delete({ path: { id: sessionId } });
|
|
13028
|
+
} catch {}
|
|
13029
|
+
}
|
|
13030
|
+
}
|
|
13031
|
+
}
|
|
13032
|
+
function sanitizeMessage(message) {
|
|
13033
|
+
const firstLine = message.split(`
|
|
13034
|
+
`)[0].trim();
|
|
13035
|
+
const trimmed = firstLine.replace(/^["'`]+|["'`]+$/g, "").trim();
|
|
13036
|
+
if (!trimmed)
|
|
13037
|
+
return "";
|
|
13038
|
+
if (trimmed.length <= 72)
|
|
13039
|
+
return trimmed;
|
|
13040
|
+
return trimmed.slice(0, 72).trim();
|
|
13041
|
+
}
|
|
13042
|
+
async function getDiffSummary($, repoDir) {
|
|
13043
|
+
try {
|
|
13044
|
+
const nameStatus = await $`git -C ${repoDir} diff --name-status`.quiet().text();
|
|
13045
|
+
const stats = await $`git -C ${repoDir} diff --stat`.quiet().text();
|
|
13046
|
+
return [nameStatus.trim(), stats.trim()].filter(Boolean).join(`
|
|
13047
|
+
`);
|
|
13048
|
+
} catch {
|
|
13049
|
+
return "";
|
|
13050
|
+
}
|
|
13051
|
+
}
|
|
13052
|
+
function formatDate(date5) {
|
|
13053
|
+
const year = String(date5.getFullYear());
|
|
13054
|
+
const month = String(date5.getMonth() + 1).padStart(2, "0");
|
|
13055
|
+
const day = String(date5.getDate()).padStart(2, "0");
|
|
13056
|
+
return `${year}-${month}-${day}`;
|
|
13057
|
+
}
|
|
13058
|
+
|
|
13032
13059
|
// src/sync/repo.ts
|
|
13033
13060
|
import { promises as fs3 } from "fs";
|
|
13034
13061
|
import path4 from "path";
|
|
@@ -13060,7 +13087,7 @@ async function ensureRepoCloned($, config2, repoDir) {
|
|
|
13060
13087
|
await fs3.mkdir(path4.dirname(repoDir), { recursive: true });
|
|
13061
13088
|
const repoIdentifier = resolveRepoIdentifier(config2);
|
|
13062
13089
|
try {
|
|
13063
|
-
await $`gh repo clone ${repoIdentifier} ${repoDir}
|
|
13090
|
+
await $`gh repo clone ${repoIdentifier} ${repoDir}`.quiet();
|
|
13064
13091
|
} catch (error45) {
|
|
13065
13092
|
throw new SyncCommandError(`Failed to clone repo: ${formatError2(error45)}`);
|
|
13066
13093
|
}
|
|
@@ -13069,7 +13096,7 @@ async function ensureRepoPrivate($, config2) {
|
|
|
13069
13096
|
const repoIdentifier = resolveRepoIdentifier(config2);
|
|
13070
13097
|
let output;
|
|
13071
13098
|
try {
|
|
13072
|
-
output = await $`gh repo view ${repoIdentifier} --json isPrivate`.text();
|
|
13099
|
+
output = await $`gh repo view ${repoIdentifier} --json isPrivate`.quiet().text();
|
|
13073
13100
|
} catch (error45) {
|
|
13074
13101
|
throw new RepoVisibilityError(`Unable to verify repo visibility: ${formatError2(error45)}`);
|
|
13075
13102
|
}
|
|
@@ -13092,7 +13119,7 @@ function parseRepoVisibility(output) {
|
|
|
13092
13119
|
}
|
|
13093
13120
|
async function fetchAndFastForward($, repoDir, branch) {
|
|
13094
13121
|
try {
|
|
13095
|
-
await $`git -C ${repoDir} fetch --prune
|
|
13122
|
+
await $`git -C ${repoDir} fetch --prune`.quiet();
|
|
13096
13123
|
} catch (error45) {
|
|
13097
13124
|
throw new SyncCommandError(`Failed to fetch repo: ${formatError2(error45)}`);
|
|
13098
13125
|
}
|
|
@@ -13108,7 +13135,7 @@ async function fetchAndFastForward($, repoDir, branch) {
|
|
|
13108
13135
|
}
|
|
13109
13136
|
if (behind > 0) {
|
|
13110
13137
|
try {
|
|
13111
|
-
await $`git -C ${repoDir} merge --ff-only ${remoteRef}
|
|
13138
|
+
await $`git -C ${repoDir} merge --ff-only ${remoteRef}`.quiet();
|
|
13112
13139
|
return { updated: true, branch };
|
|
13113
13140
|
} catch (error45) {
|
|
13114
13141
|
throw new SyncCommandError(`Failed to fast-forward: ${formatError2(error45)}`);
|
|
@@ -13127,22 +13154,22 @@ async function hasLocalChanges($, repoDir) {
|
|
|
13127
13154
|
}
|
|
13128
13155
|
async function commitAll($, repoDir, message) {
|
|
13129
13156
|
try {
|
|
13130
|
-
await $`git -C ${repoDir} add -A
|
|
13131
|
-
await $`git -C ${repoDir} commit -m ${message}
|
|
13157
|
+
await $`git -C ${repoDir} add -A`.quiet();
|
|
13158
|
+
await $`git -C ${repoDir} commit -m ${message}`.quiet();
|
|
13132
13159
|
} catch (error45) {
|
|
13133
13160
|
throw new SyncCommandError(`Failed to commit changes: ${formatError2(error45)}`);
|
|
13134
13161
|
}
|
|
13135
13162
|
}
|
|
13136
13163
|
async function pushBranch($, repoDir, branch) {
|
|
13137
13164
|
try {
|
|
13138
|
-
await $`git -C ${repoDir} push -u origin ${branch}
|
|
13165
|
+
await $`git -C ${repoDir} push -u origin ${branch}`.quiet();
|
|
13139
13166
|
} catch (error45) {
|
|
13140
13167
|
throw new SyncCommandError(`Failed to push changes: ${formatError2(error45)}`);
|
|
13141
13168
|
}
|
|
13142
13169
|
}
|
|
13143
13170
|
async function getCurrentBranch($, repoDir) {
|
|
13144
13171
|
try {
|
|
13145
|
-
const output = await $`git -C ${repoDir} rev-parse --abbrev-ref HEAD`.text();
|
|
13172
|
+
const output = await $`git -C ${repoDir} rev-parse --abbrev-ref HEAD`.quiet().text();
|
|
13146
13173
|
const branch = output.trim();
|
|
13147
13174
|
if (!branch || branch === "HEAD")
|
|
13148
13175
|
return "main";
|
|
@@ -13155,17 +13182,17 @@ async function checkoutBranch($, repoDir, branch) {
|
|
|
13155
13182
|
const exists = await hasLocalBranch($, repoDir, branch);
|
|
13156
13183
|
try {
|
|
13157
13184
|
if (exists) {
|
|
13158
|
-
await $`git -C ${repoDir} checkout ${branch}
|
|
13185
|
+
await $`git -C ${repoDir} checkout ${branch}`.quiet();
|
|
13159
13186
|
return;
|
|
13160
13187
|
}
|
|
13161
|
-
await $`git -C ${repoDir} checkout -b ${branch}
|
|
13188
|
+
await $`git -C ${repoDir} checkout -b ${branch}`.quiet();
|
|
13162
13189
|
} catch (error45) {
|
|
13163
13190
|
throw new SyncCommandError(`Failed to checkout branch: ${formatError2(error45)}`);
|
|
13164
13191
|
}
|
|
13165
13192
|
}
|
|
13166
13193
|
async function hasLocalBranch($, repoDir, branch) {
|
|
13167
13194
|
try {
|
|
13168
|
-
await $`git -C ${repoDir} show-ref --verify refs/heads/${branch}
|
|
13195
|
+
await $`git -C ${repoDir} show-ref --verify refs/heads/${branch}`.quiet();
|
|
13169
13196
|
return true;
|
|
13170
13197
|
} catch {
|
|
13171
13198
|
return false;
|
|
@@ -13173,7 +13200,7 @@ async function hasLocalBranch($, repoDir, branch) {
|
|
|
13173
13200
|
}
|
|
13174
13201
|
async function hasRemoteRef($, repoDir, branch) {
|
|
13175
13202
|
try {
|
|
13176
|
-
await $`git -C ${repoDir} show-ref --verify refs/remotes/origin/${branch}
|
|
13203
|
+
await $`git -C ${repoDir} show-ref --verify refs/remotes/origin/${branch}`.quiet();
|
|
13177
13204
|
return true;
|
|
13178
13205
|
} catch {
|
|
13179
13206
|
return false;
|
|
@@ -13181,7 +13208,7 @@ async function hasRemoteRef($, repoDir, branch) {
|
|
|
13181
13208
|
}
|
|
13182
13209
|
async function getAheadBehind($, repoDir, remoteRef) {
|
|
13183
13210
|
try {
|
|
13184
|
-
const output = await $`git -C ${repoDir} rev-list --left-right --count HEAD...${remoteRef}`.text();
|
|
13211
|
+
const output = await $`git -C ${repoDir} rev-list --left-right --count HEAD...${remoteRef}`.quiet().text();
|
|
13185
13212
|
const [aheadRaw, behindRaw] = output.trim().split(/\s+/);
|
|
13186
13213
|
const ahead = Number(aheadRaw ?? 0);
|
|
13187
13214
|
const behind = Number(behindRaw ?? 0);
|
|
@@ -13192,7 +13219,7 @@ async function getAheadBehind($, repoDir, remoteRef) {
|
|
|
13192
13219
|
}
|
|
13193
13220
|
async function getStatusLines($, repoDir) {
|
|
13194
13221
|
try {
|
|
13195
|
-
const output = await $`git -C ${repoDir} status --porcelain`.text();
|
|
13222
|
+
const output = await $`git -C ${repoDir} status --porcelain`.quiet().text();
|
|
13196
13223
|
return output.split(`
|
|
13197
13224
|
`).map((line) => line.trim()).filter(Boolean);
|
|
13198
13225
|
} catch {
|
|
@@ -13206,7 +13233,7 @@ function formatError2(error45) {
|
|
|
13206
13233
|
}
|
|
13207
13234
|
async function repoExists($, repoIdentifier) {
|
|
13208
13235
|
try {
|
|
13209
|
-
await $`gh repo view ${repoIdentifier} --json name
|
|
13236
|
+
await $`gh repo view ${repoIdentifier} --json name`.quiet();
|
|
13210
13237
|
return true;
|
|
13211
13238
|
} catch {
|
|
13212
13239
|
return false;
|
|
@@ -13214,27 +13241,63 @@ async function repoExists($, repoIdentifier) {
|
|
|
13214
13241
|
}
|
|
13215
13242
|
async function getAuthenticatedUser($) {
|
|
13216
13243
|
try {
|
|
13217
|
-
const output = await $`gh api user --jq .login`.text();
|
|
13244
|
+
const output = await $`gh api user --jq .login`.quiet().text();
|
|
13218
13245
|
return output.trim();
|
|
13219
13246
|
} catch (error45) {
|
|
13220
13247
|
throw new SyncCommandError(`Failed to detect GitHub user. Ensure gh is authenticated: ${formatError2(error45)}`);
|
|
13221
13248
|
}
|
|
13222
13249
|
}
|
|
13250
|
+
var LIKELY_SYNC_REPO_NAMES = [
|
|
13251
|
+
"my-opencode-config",
|
|
13252
|
+
"opencode-config",
|
|
13253
|
+
"opencode-sync",
|
|
13254
|
+
"opencode-synced",
|
|
13255
|
+
"dotfiles-opencode"
|
|
13256
|
+
];
|
|
13257
|
+
async function findSyncRepo($, repoName) {
|
|
13258
|
+
const owner = await getAuthenticatedUser($);
|
|
13259
|
+
if (repoName) {
|
|
13260
|
+
const exists = await repoExists($, `${owner}/${repoName}`);
|
|
13261
|
+
if (exists) {
|
|
13262
|
+
const isPrivate = await checkRepoPrivate($, `${owner}/${repoName}`);
|
|
13263
|
+
return { owner, name: repoName, isPrivate };
|
|
13264
|
+
}
|
|
13265
|
+
return null;
|
|
13266
|
+
}
|
|
13267
|
+
for (const name of LIKELY_SYNC_REPO_NAMES) {
|
|
13268
|
+
const exists = await repoExists($, `${owner}/${name}`);
|
|
13269
|
+
if (exists) {
|
|
13270
|
+
const isPrivate = await checkRepoPrivate($, `${owner}/${name}`);
|
|
13271
|
+
return { owner, name, isPrivate };
|
|
13272
|
+
}
|
|
13273
|
+
}
|
|
13274
|
+
return null;
|
|
13275
|
+
}
|
|
13276
|
+
async function checkRepoPrivate($, repoIdentifier) {
|
|
13277
|
+
try {
|
|
13278
|
+
const output = await $`gh repo view ${repoIdentifier} --json isPrivate`.quiet().text();
|
|
13279
|
+
return parseRepoVisibility(output);
|
|
13280
|
+
} catch {
|
|
13281
|
+
return false;
|
|
13282
|
+
}
|
|
13283
|
+
}
|
|
13223
13284
|
|
|
13224
13285
|
// src/sync/service.ts
|
|
13225
13286
|
function createSyncService(ctx) {
|
|
13226
13287
|
const locations = resolveSyncLocations();
|
|
13288
|
+
const log2 = createLogger(ctx.client);
|
|
13227
13289
|
return {
|
|
13228
13290
|
startupSync: async () => {
|
|
13229
13291
|
const config2 = await loadSyncConfig(locations);
|
|
13230
13292
|
if (!config2) {
|
|
13231
|
-
await showToast(ctx, "Configure opencode-synced with /sync-init.", "info");
|
|
13293
|
+
await showToast(ctx.client, "Configure opencode-synced with /sync-init.", "info");
|
|
13232
13294
|
return;
|
|
13233
13295
|
}
|
|
13234
13296
|
try {
|
|
13235
|
-
await runStartup(ctx, locations, config2);
|
|
13297
|
+
await runStartup(ctx, locations, config2, log2);
|
|
13236
13298
|
} catch (error45) {
|
|
13237
|
-
|
|
13299
|
+
log2.error("Startup sync failed", { error: formatError3(error45) });
|
|
13300
|
+
await showToast(ctx.client, formatError3(error45), "error");
|
|
13238
13301
|
}
|
|
13239
13302
|
},
|
|
13240
13303
|
status: async () => {
|
|
@@ -13301,6 +13364,18 @@ function createSyncService(ctx) {
|
|
|
13301
13364
|
const repoRoot = resolveRepoRoot(config2, locations);
|
|
13302
13365
|
await ensureRepoCloned(ctx.$, config2, repoRoot);
|
|
13303
13366
|
await ensureSecretsPolicy(ctx, config2);
|
|
13367
|
+
if (created) {
|
|
13368
|
+
const overrides = await loadOverrides(locations);
|
|
13369
|
+
const plan = buildSyncPlan(config2, locations, repoRoot);
|
|
13370
|
+
await syncLocalToRepo(plan, overrides);
|
|
13371
|
+
const dirty = await hasLocalChanges(ctx.$, repoRoot);
|
|
13372
|
+
if (dirty) {
|
|
13373
|
+
const branch = resolveRepoBranch(config2);
|
|
13374
|
+
await commitAll(ctx.$, repoRoot, "Initial sync from opencode-synced");
|
|
13375
|
+
await pushBranch(ctx.$, repoRoot, branch);
|
|
13376
|
+
await writeState(locations, { lastPush: new Date().toISOString() });
|
|
13377
|
+
}
|
|
13378
|
+
}
|
|
13304
13379
|
const lines = [
|
|
13305
13380
|
"opencode-synced configured.",
|
|
13306
13381
|
`Repo: ${repoIdentifier}${created ? " (created)" : ""}`,
|
|
@@ -13308,6 +13383,55 @@ function createSyncService(ctx) {
|
|
|
13308
13383
|
`Local repo: ${repoRoot}`
|
|
13309
13384
|
];
|
|
13310
13385
|
return lines.join(`
|
|
13386
|
+
`);
|
|
13387
|
+
},
|
|
13388
|
+
link: async (options) => {
|
|
13389
|
+
const found = await findSyncRepo(ctx.$, options.repo);
|
|
13390
|
+
if (!found) {
|
|
13391
|
+
const searchedFor = options.repo ? `"${options.repo}"` : "common sync repo names (my-opencode-config, opencode-config, etc.)";
|
|
13392
|
+
const lines2 = [
|
|
13393
|
+
`Could not find an existing sync repo. Searched for: ${searchedFor}`,
|
|
13394
|
+
"",
|
|
13395
|
+
"To link to an existing repo, run:",
|
|
13396
|
+
" /sync-link <repo-name>",
|
|
13397
|
+
"",
|
|
13398
|
+
"To create a new sync repo, run:",
|
|
13399
|
+
" /sync-init"
|
|
13400
|
+
];
|
|
13401
|
+
return lines2.join(`
|
|
13402
|
+
`);
|
|
13403
|
+
}
|
|
13404
|
+
const config2 = normalizeSyncConfig({
|
|
13405
|
+
repo: { owner: found.owner, name: found.name },
|
|
13406
|
+
includeSecrets: false,
|
|
13407
|
+
includeSessions: false,
|
|
13408
|
+
includePromptStash: false,
|
|
13409
|
+
extraSecretPaths: []
|
|
13410
|
+
});
|
|
13411
|
+
await writeSyncConfig(locations, config2);
|
|
13412
|
+
const repoRoot = resolveRepoRoot(config2, locations);
|
|
13413
|
+
await ensureRepoCloned(ctx.$, config2, repoRoot);
|
|
13414
|
+
const branch = await resolveBranch(ctx, config2, repoRoot);
|
|
13415
|
+
await fetchAndFastForward(ctx.$, repoRoot, branch);
|
|
13416
|
+
const overrides = await loadOverrides(locations);
|
|
13417
|
+
const plan = buildSyncPlan(config2, locations, repoRoot);
|
|
13418
|
+
await syncRepoToLocal(plan, overrides);
|
|
13419
|
+
await writeState(locations, {
|
|
13420
|
+
lastPull: new Date().toISOString(),
|
|
13421
|
+
lastRemoteUpdate: new Date().toISOString()
|
|
13422
|
+
});
|
|
13423
|
+
const lines = [
|
|
13424
|
+
`Linked to existing sync repo: ${found.owner}/${found.name}`,
|
|
13425
|
+
"",
|
|
13426
|
+
"Your local OpenCode config has been OVERWRITTEN with the synced config.",
|
|
13427
|
+
"Your local overrides file was preserved and applied on top.",
|
|
13428
|
+
"",
|
|
13429
|
+
"Restart OpenCode to apply the new settings.",
|
|
13430
|
+
"",
|
|
13431
|
+
found.isPrivate ? "To enable secrets sync, run: /sync-enable-secrets" : "Note: Repo is public. Secrets sync is disabled."
|
|
13432
|
+
];
|
|
13433
|
+
await showToast(ctx.client, "Config synced. Restart OpenCode to apply.", "info");
|
|
13434
|
+
return lines.join(`
|
|
13311
13435
|
`);
|
|
13312
13436
|
},
|
|
13313
13437
|
pull: async () => {
|
|
@@ -13331,7 +13455,7 @@ function createSyncService(ctx) {
|
|
|
13331
13455
|
lastPull: new Date().toISOString(),
|
|
13332
13456
|
lastRemoteUpdate: new Date().toISOString()
|
|
13333
13457
|
});
|
|
13334
|
-
await showToast(ctx, "Config updated. Restart OpenCode to apply.", "info");
|
|
13458
|
+
await showToast(ctx.client, "Config updated. Restart OpenCode to apply.", "info");
|
|
13335
13459
|
return "Remote config applied. Restart OpenCode to use new settings.";
|
|
13336
13460
|
},
|
|
13337
13461
|
push: async () => {
|
|
@@ -13380,35 +13504,39 @@ function createSyncService(ctx) {
|
|
|
13380
13504
|
const status = await getRepoStatus(ctx.$, repoRoot);
|
|
13381
13505
|
const decision = await analyzeAndDecideResolution({ client: ctx.client, $: ctx.$ }, repoRoot, status.changes);
|
|
13382
13506
|
if (decision.action === "commit") {
|
|
13383
|
-
const message = decision.message;
|
|
13507
|
+
const message = decision.message ?? "Sync: Auto-resolved uncommitted changes";
|
|
13384
13508
|
await commitAll(ctx.$, repoRoot, message);
|
|
13385
13509
|
return `Resolved by committing changes: ${message}`;
|
|
13386
13510
|
}
|
|
13387
13511
|
if (decision.action === "reset") {
|
|
13388
13512
|
try {
|
|
13389
|
-
await ctx.$`git -C ${repoRoot} reset --hard HEAD
|
|
13390
|
-
await ctx.$`git -C ${repoRoot} clean -fd
|
|
13513
|
+
await ctx.$`git -C ${repoRoot} reset --hard HEAD`.quiet();
|
|
13514
|
+
await ctx.$`git -C ${repoRoot} clean -fd`.quiet();
|
|
13391
13515
|
return "Resolved by discarding all uncommitted changes.";
|
|
13392
13516
|
} catch (error45) {
|
|
13393
13517
|
throw new SyncCommandError(`Failed to reset changes: ${formatError3(error45)}`);
|
|
13394
13518
|
}
|
|
13395
13519
|
}
|
|
13396
|
-
return
|
|
13520
|
+
return `Unable to automatically resolve. Please manually resolve in: ${repoRoot}`;
|
|
13397
13521
|
}
|
|
13398
13522
|
};
|
|
13399
13523
|
}
|
|
13400
|
-
async function runStartup(ctx, locations, config2) {
|
|
13524
|
+
async function runStartup(ctx, locations, config2, log2) {
|
|
13401
13525
|
const repoRoot = resolveRepoRoot(config2, locations);
|
|
13526
|
+
log2.debug("Starting sync", { repoRoot });
|
|
13402
13527
|
await ensureRepoCloned(ctx.$, config2, repoRoot);
|
|
13403
13528
|
await ensureSecretsPolicy(ctx, config2);
|
|
13404
13529
|
const branch = await resolveBranch(ctx, config2, repoRoot);
|
|
13530
|
+
log2.debug("Resolved branch", { branch });
|
|
13405
13531
|
const dirty = await hasLocalChanges(ctx.$, repoRoot);
|
|
13406
13532
|
if (dirty) {
|
|
13407
|
-
|
|
13533
|
+
log2.warn("Uncommitted changes detected", { repoRoot });
|
|
13534
|
+
await showToast(ctx.client, `Uncommitted changes detected. Run /sync-resolve to auto-fix, or manually resolve in: ${repoRoot}`, "warning");
|
|
13408
13535
|
return;
|
|
13409
13536
|
}
|
|
13410
13537
|
const update = await fetchAndFastForward(ctx.$, repoRoot, branch);
|
|
13411
13538
|
if (update.updated) {
|
|
13539
|
+
log2.info("Pulled remote changes", { branch });
|
|
13412
13540
|
const overrides2 = await loadOverrides(locations);
|
|
13413
13541
|
const plan2 = buildSyncPlan(config2, locations, repoRoot);
|
|
13414
13542
|
await syncRepoToLocal(plan2, overrides2);
|
|
@@ -13416,16 +13544,19 @@ async function runStartup(ctx, locations, config2) {
|
|
|
13416
13544
|
lastPull: new Date().toISOString(),
|
|
13417
13545
|
lastRemoteUpdate: new Date().toISOString()
|
|
13418
13546
|
});
|
|
13419
|
-
await showToast(ctx, "Config updated. Restart OpenCode to apply.", "info");
|
|
13547
|
+
await showToast(ctx.client, "Config updated. Restart OpenCode to apply.", "info");
|
|
13420
13548
|
return;
|
|
13421
13549
|
}
|
|
13422
13550
|
const overrides = await loadOverrides(locations);
|
|
13423
13551
|
const plan = buildSyncPlan(config2, locations, repoRoot);
|
|
13424
13552
|
await syncLocalToRepo(plan, overrides);
|
|
13425
13553
|
const changes = await hasLocalChanges(ctx.$, repoRoot);
|
|
13426
|
-
if (!changes)
|
|
13554
|
+
if (!changes) {
|
|
13555
|
+
log2.debug("No local changes to push");
|
|
13427
13556
|
return;
|
|
13557
|
+
}
|
|
13428
13558
|
const message = await generateCommitMessage({ client: ctx.client, $: ctx.$ }, repoRoot);
|
|
13559
|
+
log2.info("Pushing local changes", { message });
|
|
13429
13560
|
await commitAll(ctx.$, repoRoot, message);
|
|
13430
13561
|
await pushBranch(ctx.$, repoRoot, branch);
|
|
13431
13562
|
await writeState(locations, {
|
|
@@ -13496,14 +13627,11 @@ async function createRepo($, config2, isPrivate) {
|
|
|
13496
13627
|
}
|
|
13497
13628
|
const visibility = isPrivate ? "--private" : "--public";
|
|
13498
13629
|
try {
|
|
13499
|
-
await $`gh repo create ${owner}/${name} ${visibility} --confirm
|
|
13630
|
+
await $`gh repo create ${owner}/${name} ${visibility} --confirm`.quiet();
|
|
13500
13631
|
} catch (error45) {
|
|
13501
13632
|
throw new SyncCommandError(`Failed to create repo: ${formatError3(error45)}`);
|
|
13502
13633
|
}
|
|
13503
13634
|
}
|
|
13504
|
-
async function showToast(ctx, message, variant) {
|
|
13505
|
-
await ctx.client.tui.showToast({ body: { message: `opencode-synced: ${message}`, variant } });
|
|
13506
|
-
}
|
|
13507
13635
|
function formatError3(error45) {
|
|
13508
13636
|
if (error45 instanceof Error)
|
|
13509
13637
|
return error45.message;
|
|
@@ -13511,7 +13639,7 @@ function formatError3(error45) {
|
|
|
13511
13639
|
}
|
|
13512
13640
|
async function analyzeAndDecideResolution(ctx, repoRoot, changes) {
|
|
13513
13641
|
try {
|
|
13514
|
-
const diff = await ctx.$`git -C ${repoRoot} diff HEAD`.text();
|
|
13642
|
+
const diff = await ctx.$`git -C ${repoRoot} diff HEAD`.quiet().text();
|
|
13515
13643
|
const statusOutput = changes.join(`
|
|
13516
13644
|
`);
|
|
13517
13645
|
const prompt = [
|
|
@@ -13641,7 +13769,7 @@ var OpencodeConfigSync = async (ctx) => {
|
|
|
13641
13769
|
const syncTool = tool({
|
|
13642
13770
|
description: "Manage OpenCode config sync with a GitHub repo",
|
|
13643
13771
|
args: {
|
|
13644
|
-
command: tool.schema.enum(["status", "init", "pull", "push", "enable-secrets", "resolve"]).describe("Sync command to execute"),
|
|
13772
|
+
command: tool.schema.enum(["status", "init", "link", "pull", "push", "enable-secrets", "resolve"]).describe("Sync command to execute"),
|
|
13645
13773
|
repo: tool.schema.string().optional().describe("Repo owner/name or URL"),
|
|
13646
13774
|
owner: tool.schema.string().optional().describe("Repo owner"),
|
|
13647
13775
|
name: tool.schema.string().optional().describe("Repo name"),
|
|
@@ -13676,6 +13804,11 @@ var OpencodeConfigSync = async (ctx) => {
|
|
|
13676
13804
|
localRepoPath: args.localRepoPath
|
|
13677
13805
|
});
|
|
13678
13806
|
}
|
|
13807
|
+
if (args.command === "link") {
|
|
13808
|
+
return await service.link({
|
|
13809
|
+
repo: args.repo ?? args.name
|
|
13810
|
+
});
|
|
13811
|
+
}
|
|
13679
13812
|
if (args.command === "pull") {
|
|
13680
13813
|
return await service.pull();
|
|
13681
13814
|
}
|
|
@@ -13697,7 +13830,9 @@ var OpencodeConfigSync = async (ctx) => {
|
|
|
13697
13830
|
}
|
|
13698
13831
|
}
|
|
13699
13832
|
});
|
|
13700
|
-
|
|
13833
|
+
setTimeout(() => {
|
|
13834
|
+
service.startupSync();
|
|
13835
|
+
}, 1000);
|
|
13701
13836
|
return {
|
|
13702
13837
|
tool: {
|
|
13703
13838
|
opencode_sync: syncTool
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-synced",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Sync global OpenCode config across machines via GitHub.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Ian Hildebrand"
|
|
@@ -26,25 +26,29 @@
|
|
|
26
26
|
"@opencode-ai/plugin": "1.0.85"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@
|
|
29
|
+
"@biomejs/biome": "2.3.10",
|
|
30
|
+
"@commitlint/cli": "^20.2.0",
|
|
31
|
+
"@commitlint/config-conventional": "^20.2.0",
|
|
30
32
|
"@types/node": "^20.11.5",
|
|
31
|
-
"@typescript-eslint/eslint-plugin": "8.47.0",
|
|
32
|
-
"@typescript-eslint/parser": "8.47.0",
|
|
33
33
|
"bun-types": "latest",
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"eslint-plugin-prettier": "^5.1.3",
|
|
37
|
-
"prettier": "^3.2.4",
|
|
34
|
+
"husky": "^9.1.7",
|
|
35
|
+
"lint-staged": "^16.2.7",
|
|
38
36
|
"typescript": "^5.9.2",
|
|
39
|
-
"typescript-eslint": "^8.47.0",
|
|
40
37
|
"vitest": "^3.2.4"
|
|
41
38
|
},
|
|
42
39
|
"scripts": {
|
|
43
40
|
"build": "rm -rf dist && bun build ./src/index.ts --outdir dist --target bun && tsc -p tsconfig.build.json && cp -r src/command dist/command",
|
|
44
41
|
"test": "vitest run",
|
|
45
42
|
"test:watch": "vitest",
|
|
46
|
-
"lint": "
|
|
47
|
-
"lint:fix": "
|
|
48
|
-
"format": "
|
|
43
|
+
"lint": "biome lint .",
|
|
44
|
+
"lint:fix": "biome lint --write .",
|
|
45
|
+
"format": "biome format --write .",
|
|
46
|
+
"check": "biome check --write .",
|
|
47
|
+
"prepare": "husky"
|
|
48
|
+
},
|
|
49
|
+
"lint-staged": {
|
|
50
|
+
"*.{js,ts,json}": [
|
|
51
|
+
"biome check --write --no-errors-on-unmatched"
|
|
52
|
+
]
|
|
49
53
|
}
|
|
50
54
|
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Initialize opencode-synced configuration
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Use the opencode_sync tool with command "init".
|
|
6
|
-
The repo will be created automatically if it doesn't exist (private by default).
|
|
7
|
-
Default repo name is "my-opencode-config" with owner auto-detected from GitHub CLI.
|
|
8
|
-
If the user wants a custom repo name, pass name="custom-name".
|
|
9
|
-
If the user wants an org-owned repo, pass owner="org-name".
|
|
10
|
-
If the user wants a public repo, pass private=false.
|
|
11
|
-
Include includeSecrets if the user explicitly opts in.
|
package/dist/index.d.ts
DELETED
package/dist/sync/apply.d.ts
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import type { SyncPlan } from './paths.ts';
|
|
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>;
|
package/dist/sync/commit.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { PluginInput } from '@opencode-ai/plugin';
|
|
2
|
-
type CommitClient = PluginInput['client'];
|
|
3
|
-
type Shell = PluginInput['$'];
|
|
4
|
-
interface CommitContext {
|
|
5
|
-
client: CommitClient;
|
|
6
|
-
$: Shell;
|
|
7
|
-
}
|
|
8
|
-
export declare function generateCommitMessage(ctx: CommitContext, repoDir: string, fallbackDate?: Date): Promise<string>;
|
|
9
|
-
export {};
|
package/dist/sync/config.d.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { SyncLocations } from './paths.ts';
|
|
2
|
-
export interface SyncRepoConfig {
|
|
3
|
-
url?: string;
|
|
4
|
-
owner?: string;
|
|
5
|
-
name?: string;
|
|
6
|
-
branch?: string;
|
|
7
|
-
}
|
|
8
|
-
export interface SyncConfig {
|
|
9
|
-
repo?: SyncRepoConfig;
|
|
10
|
-
localRepoPath?: string;
|
|
11
|
-
includeSecrets?: boolean;
|
|
12
|
-
includeSessions?: boolean;
|
|
13
|
-
includePromptStash?: boolean;
|
|
14
|
-
extraSecretPaths?: string[];
|
|
15
|
-
}
|
|
16
|
-
export interface SyncState {
|
|
17
|
-
lastPull?: string;
|
|
18
|
-
lastPush?: string;
|
|
19
|
-
lastRemoteUpdate?: string;
|
|
20
|
-
}
|
|
21
|
-
export declare function pathExists(filePath: string): Promise<boolean>;
|
|
22
|
-
export declare function normalizeSyncConfig(config: SyncConfig): SyncConfig;
|
|
23
|
-
export declare function loadSyncConfig(locations: SyncLocations): Promise<SyncConfig | null>;
|
|
24
|
-
export declare function writeSyncConfig(locations: SyncLocations, config: SyncConfig): Promise<void>;
|
|
25
|
-
export declare function loadOverrides(locations: SyncLocations): Promise<Record<string, unknown> | null>;
|
|
26
|
-
export declare function loadState(locations: SyncLocations): Promise<SyncState>;
|
|
27
|
-
export declare function writeState(locations: SyncLocations, state: SyncState): Promise<void>;
|
|
28
|
-
export declare function applyOverridesToRuntimeConfig(config: Record<string, unknown>, overrides: Record<string, unknown>): void;
|
|
29
|
-
export declare function deepMerge<T>(base: T, override: unknown): T;
|
|
30
|
-
export declare function stripOverrides(localConfig: Record<string, unknown>, overrides: Record<string, unknown>, baseConfig: Record<string, unknown> | null): Record<string, unknown>;
|
|
31
|
-
export declare function parseJsonc<T>(content: string): T;
|
|
32
|
-
export declare function writeJsonFile(filePath: string, data: unknown, options?: {
|
|
33
|
-
jsonc: boolean;
|
|
34
|
-
mode?: number;
|
|
35
|
-
}): Promise<void>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/sync/errors.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export declare class SyncError extends Error {
|
|
2
|
-
readonly code: string;
|
|
3
|
-
constructor(code: string, message: string);
|
|
4
|
-
}
|
|
5
|
-
export declare class SyncConfigMissingError extends SyncError {
|
|
6
|
-
constructor(message: string);
|
|
7
|
-
}
|
|
8
|
-
export declare class RepoDivergedError extends SyncError {
|
|
9
|
-
constructor(message: string);
|
|
10
|
-
}
|
|
11
|
-
export declare class RepoPrivateRequiredError extends SyncError {
|
|
12
|
-
constructor(message: string);
|
|
13
|
-
}
|
|
14
|
-
export declare class RepoVisibilityError extends SyncError {
|
|
15
|
-
constructor(message: string);
|
|
16
|
-
}
|
|
17
|
-
export declare class SyncCommandError extends SyncError {
|
|
18
|
-
constructor(message: string);
|
|
19
|
-
}
|
package/dist/sync/paths.d.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { SyncConfig } from './config.ts';
|
|
2
|
-
export interface XdgPaths {
|
|
3
|
-
homeDir: string;
|
|
4
|
-
configDir: string;
|
|
5
|
-
dataDir: string;
|
|
6
|
-
stateDir: string;
|
|
7
|
-
}
|
|
8
|
-
export interface SyncLocations {
|
|
9
|
-
xdg: XdgPaths;
|
|
10
|
-
configRoot: string;
|
|
11
|
-
syncConfigPath: string;
|
|
12
|
-
overridesPath: string;
|
|
13
|
-
statePath: string;
|
|
14
|
-
defaultRepoDir: string;
|
|
15
|
-
}
|
|
16
|
-
export type SyncItemType = 'file' | 'dir';
|
|
17
|
-
export interface SyncItem {
|
|
18
|
-
localPath: string;
|
|
19
|
-
repoPath: string;
|
|
20
|
-
type: SyncItemType;
|
|
21
|
-
isSecret: boolean;
|
|
22
|
-
isConfigFile: boolean;
|
|
23
|
-
}
|
|
24
|
-
export interface ExtraSecretPlan {
|
|
25
|
-
allowlist: string[];
|
|
26
|
-
manifestPath: string;
|
|
27
|
-
entries: Array<{
|
|
28
|
-
sourcePath: string;
|
|
29
|
-
repoPath: string;
|
|
30
|
-
}>;
|
|
31
|
-
}
|
|
32
|
-
export interface SyncPlan {
|
|
33
|
-
items: SyncItem[];
|
|
34
|
-
extraSecrets: ExtraSecretPlan;
|
|
35
|
-
repoRoot: string;
|
|
36
|
-
homeDir: string;
|
|
37
|
-
platform: NodeJS.Platform;
|
|
38
|
-
}
|
|
39
|
-
export declare function resolveHomeDir(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string;
|
|
40
|
-
export declare function resolveXdgPaths(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): XdgPaths;
|
|
41
|
-
export declare function resolveSyncLocations(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): SyncLocations;
|
|
42
|
-
export declare function expandHome(inputPath: string, homeDir: string): string;
|
|
43
|
-
export declare function normalizePath(inputPath: string, homeDir: string, platform?: NodeJS.Platform): string;
|
|
44
|
-
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 resolveRepoRoot(config: SyncConfig | null, locations: SyncLocations): string;
|
|
47
|
-
export declare function buildSyncPlan(config: SyncConfig, locations: SyncLocations, repoRoot: string, platform?: NodeJS.Platform): SyncPlan;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/sync/repo.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { PluginInput } from '@opencode-ai/plugin';
|
|
2
|
-
import type { SyncConfig } from './config.ts';
|
|
3
|
-
export interface RepoStatus {
|
|
4
|
-
branch: string;
|
|
5
|
-
changes: string[];
|
|
6
|
-
}
|
|
7
|
-
export interface RepoUpdateResult {
|
|
8
|
-
updated: boolean;
|
|
9
|
-
branch: string;
|
|
10
|
-
}
|
|
11
|
-
type Shell = PluginInput['$'];
|
|
12
|
-
export declare function isRepoCloned(repoDir: string): Promise<boolean>;
|
|
13
|
-
export declare function resolveRepoIdentifier(config: SyncConfig): string;
|
|
14
|
-
export declare function resolveRepoBranch(config: SyncConfig, fallback?: string): string;
|
|
15
|
-
export declare function ensureRepoCloned($: Shell, config: SyncConfig, repoDir: string): Promise<void>;
|
|
16
|
-
export declare function ensureRepoPrivate($: Shell, config: SyncConfig): Promise<void>;
|
|
17
|
-
export declare function parseRepoVisibility(output: string): boolean;
|
|
18
|
-
export declare function fetchAndFastForward($: Shell, repoDir: string, branch: string): Promise<RepoUpdateResult>;
|
|
19
|
-
export declare function getRepoStatus($: Shell, repoDir: string): Promise<RepoStatus>;
|
|
20
|
-
export declare function hasLocalChanges($: Shell, repoDir: string): Promise<boolean>;
|
|
21
|
-
export declare function commitAll($: Shell, repoDir: string, message: string): Promise<void>;
|
|
22
|
-
export declare function pushBranch($: Shell, repoDir: string, branch: string): Promise<void>;
|
|
23
|
-
export declare function repoExists($: Shell, repoIdentifier: string): Promise<boolean>;
|
|
24
|
-
export declare function getAuthenticatedUser($: Shell): Promise<string>;
|
|
25
|
-
export {};
|
package/dist/sync/repo.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/sync/service.d.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { PluginInput } from '@opencode-ai/plugin';
|
|
2
|
-
type SyncServiceContext = Pick<PluginInput, 'client' | '$'>;
|
|
3
|
-
interface InitOptions {
|
|
4
|
-
repo?: string;
|
|
5
|
-
owner?: string;
|
|
6
|
-
name?: string;
|
|
7
|
-
url?: string;
|
|
8
|
-
branch?: string;
|
|
9
|
-
includeSecrets?: boolean;
|
|
10
|
-
includeSessions?: boolean;
|
|
11
|
-
includePromptStash?: boolean;
|
|
12
|
-
create?: boolean;
|
|
13
|
-
private?: boolean;
|
|
14
|
-
extraSecretPaths?: string[];
|
|
15
|
-
localRepoPath?: string;
|
|
16
|
-
}
|
|
17
|
-
export interface SyncService {
|
|
18
|
-
startupSync: () => Promise<void>;
|
|
19
|
-
status: () => Promise<string>;
|
|
20
|
-
init: (_options: InitOptions) => Promise<string>;
|
|
21
|
-
pull: () => Promise<string>;
|
|
22
|
-
push: () => Promise<string>;
|
|
23
|
-
enableSecrets: (_extraSecretPaths?: string[]) => Promise<string>;
|
|
24
|
-
resolve: () => Promise<string>;
|
|
25
|
-
}
|
|
26
|
-
export declare function createSyncService(ctx: SyncServiceContext): SyncService;
|
|
27
|
-
export {};
|
package/dist/sync/utils.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { PluginInput } from '@opencode-ai/plugin';
|
|
2
|
-
type Client = PluginInput['client'];
|
|
3
|
-
export declare function unwrapData<T>(response: unknown): T | null;
|
|
4
|
-
export declare function extractTextFromResponse(response: unknown): string | null;
|
|
5
|
-
export declare function resolveSmallModel(client: Client): Promise<{
|
|
6
|
-
providerID: string;
|
|
7
|
-
modelID: string;
|
|
8
|
-
} | null>;
|
|
9
|
-
export {};
|