opencode-synced 0.1.0 → 0.2.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
@@ -37,13 +37,25 @@ opencode
37
37
 
38
38
  ## Configure
39
39
 
40
- Run `/sync-init` to get started. This will:
40
+ ### First machine (create new sync repo)
41
41
 
42
- 1. Detect your GitHub username from the CLI
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
- That's it! Your config will now sync automatically on startup.
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
- - `/sync-init` to set up sync (creates repo if needed)
140
- - `/sync-status` for repo status and last sync
141
- - `/sync-pull` to fetch and apply remote config
142
- - `/sync-push` to commit and push local changes
143
- - `/sync-enable-secrets` to opt in to secrets sync
144
- - `/sync-resolve` to automatically resolve uncommitted changes using AI
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.prototype.hasOwnProperty.call(right, key))
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
- await showToast(ctx, formatError3(error45), "error");
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 "Unable to automatically resolve. Please manually resolve in: " + repoRoot;
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
- await showToast(ctx, `Uncommitted changes detected. Run /sync-resolve to auto-fix, or manually resolve in: ${repoRoot}`, "warning");
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
- service.startupSync();
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.0",
3
+ "version": "0.2.0",
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
- "@eslint/js": "^9.39.1",
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
- "eslint": "^9.39.1",
35
- "eslint-config-prettier": "10.1.8",
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": "eslint .",
47
- "lint:fix": "eslint . --fix",
48
- "format": "prettier --write ."
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,5 +0,0 @@
1
- ---
2
- description: Enable secrets sync (private repo required)
3
- ---
4
- Use the opencode_sync tool with command "enable-secrets".
5
- If the user supplies extra secret paths, pass them via extraSecretPaths.
@@ -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.
@@ -1,5 +0,0 @@
1
- ---
2
- description: Pull and apply synced OpenCode config
3
- ---
4
- Use the opencode_sync tool with command "pull".
5
- If updates are applied, remind the user to restart OpenCode.
@@ -1,4 +0,0 @@
1
- ---
2
- description: Push local OpenCode config to the sync repo
3
- ---
4
- Use the opencode_sync tool with command "push".
@@ -1,5 +0,0 @@
1
- ---
2
- description: Resolve uncommitted changes in sync repo
3
- ---
4
-
5
- Use the opencode_sync tool with command "resolve" to automatically resolve uncommitted changes in the local sync repository using AI.
@@ -1,5 +0,0 @@
1
- ---
2
- description: Show opencode-synced status
3
- ---
4
-
5
- Use the opencode_sync tool with command "status" and report the results.
package/dist/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- import type { Plugin } from '@opencode-ai/plugin';
2
- export declare const OpencodeConfigSync: Plugin;
@@ -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>;
@@ -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 {};
@@ -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 {};
@@ -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
- }
@@ -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 {};
@@ -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 {};
@@ -1 +0,0 @@
1
- export {};
@@ -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 {};
@@ -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 {};