From 8998f8cbbf62cd90a50dc8dc064eb31de7303235 Mon Sep 17 00:00:00 2001 From: iHildy Date: Sun, 28 Dec 2025 11:12:00 -0600 Subject: [PATCH 1/2] feat: add GitHub user auto-detection and auto-create sync repo Add getAuthenticatedUser and repoExists helpers and a default repo name my-opencode-config. Update init flow to auto-create a private repo when missing and refresh README and command docs. --- README.md | 46 +++++++++++++++++++--------------------- src/command/sync-init.md | 8 +++++-- src/sync/repo.ts | 20 +++++++++++++++++ src/sync/service.ts | 43 +++++++++++++++++++++++++------------ 4 files changed, 78 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 28a0b26..e626246 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,25 @@ opencode ## Configure +Run `/sync-init` to get started. This will: + +1. Detect your GitHub username from the CLI +2. Create a private repo (`my-opencode-config` by default) if it doesn't exist +3. Clone the repo and set up sync + +That's it! Your config will now sync automatically on startup. + +### Custom repo name or org + +You can specify a custom repo name or use an organization: + +- `/sync-init` - Uses `{your-username}/my-opencode-config` +- `/sync-init my-config` - Uses `{your-username}/my-config` +- `/sync-init my-org/team-config` - Uses `my-org/team-config` + +
+Manual configuration + Create `~/.config/opencode/opencode-sync.jsonc`: ```jsonc @@ -53,7 +72,7 @@ Create `~/.config/opencode/opencode-sync.jsonc`: } ``` -You can also run `/sync-init` to scaffold this file. +
### Synced paths (default) @@ -117,6 +136,7 @@ Overrides are merged into the runtime config and re-applied to `opencode.json(c) ## Usage +- `/sync-init` to set up sync (creates repo if needed) - `/sync-status` for repo status and last sync - `/sync-pull` to fetch and apply remote config - `/sync-push` to commit and push local changes @@ -124,29 +144,7 @@ Overrides are merged into the runtime config and re-applied to `opencode.json(c) - `/sync-resolve` to automatically resolve uncommitted changes using AI
-Manual (slash command alternative) - -### Configure - -Create `~/.config/opencode/opencode-sync.jsonc`: - -```jsonc -{ - "repo": { - "owner": "your-org", - "name": "opencode-config", - "branch": "main", - }, - "includeSecrets": false, - "includeSessions": false, - "includePromptStash": false, - "extraSecretPaths": [], -} -``` - -### Enable secrets (private repo required) - -Set `"includeSecrets": true` and optionally add `"extraSecretPaths"`. The plugin will refuse to sync secrets if the repo is not private. +Manual sync (without slash commands) ### Trigger a sync diff --git a/src/command/sync-init.md b/src/command/sync-init.md index c5e305a..d3d9dc6 100644 --- a/src/command/sync-init.md +++ b/src/command/sync-init.md @@ -1,7 +1,11 @@ --- description: Initialize opencode-sync configuration --- + Use the opencode_sync tool with command "init". -If the repo is unknown, ask for a GitHub repo (owner/name or URL). -If the user wants to create it, pass create=true and private=true unless they request public. +The repo will be created automatically if it doesn't exist (private by default). +Default repo name is "my-opencode-config" with owner auto-detected from GitHub CLI. +If the user wants a custom repo name, pass name="custom-name". +If the user wants an org-owned repo, pass owner="org-name". +If the user wants a public repo, pass private=false. Include includeSecrets if the user explicitly opts in. diff --git a/src/sync/repo.ts b/src/sync/repo.ts index 1520b08..f5c428b 100644 --- a/src/sync/repo.ts +++ b/src/sync/repo.ts @@ -236,3 +236,23 @@ function formatError(error: unknown): string { if (error instanceof Error) return error.message; return String(error); } + +export async function repoExists($: Shell, repoIdentifier: string): Promise { + try { + await $`gh repo view ${repoIdentifier} --json name`; + return true; + } catch { + return false; + } +} + +export async function getAuthenticatedUser($: Shell): Promise { + try { + const output = await $`gh api user --jq .login`.text(); + return output.trim(); + } catch (error) { + throw new SyncCommandError( + `Failed to detect GitHub user. Ensure gh is authenticated: ${formatError(error)}` + ); + } +} diff --git a/src/sync/service.ts b/src/sync/service.ts index 357b3ab..a26c226 100644 --- a/src/sync/service.ts +++ b/src/sync/service.ts @@ -18,10 +18,12 @@ import { ensureRepoCloned, ensureRepoPrivate, fetchAndFastForward, + getAuthenticatedUser, getRepoStatus, hasLocalChanges, isRepoCloned, pushBranch, + repoExists, resolveRepoBranch, resolveRepoIdentifier, } from './repo.ts'; @@ -125,14 +127,16 @@ export function createSyncService(ctx: SyncServiceContext): SyncService { return statusLines.join('\n'); }, init: async (options: InitOptions) => { - const config = buildConfigFromInit(options); - if (!config.repo) { - throw new SyncCommandError('Provide repo info (owner/name or URL) to initialize sync.'); - } + const config = await buildConfigFromInit(ctx.$, options); const repoIdentifier = resolveRepoIdentifier(config); - if (options.create) { - await createRepo(ctx.$, config, options.private ?? true); + const isPrivate = options.private ?? true; + + const exists = await repoExists(ctx.$, repoIdentifier); + let created = false; + if (!exists) { + await createRepo(ctx.$, config, isPrivate); + created = true; } await writeSyncConfig(locations, config); @@ -140,12 +144,14 @@ export function createSyncService(ctx: SyncServiceContext): SyncService { await ensureRepoCloned(ctx.$, config, repoRoot); await ensureSecretsPolicy(ctx, config); - return [ + const lines = [ 'opencode-sync configured.', - `Repo: ${repoIdentifier}`, + `Repo: ${repoIdentifier}${created ? ' (created)' : ''}`, `Branch: ${resolveRepoBranch(config)}`, `Local repo: ${repoRoot}`, - ].join('\n'); + ]; + + return lines.join('\n'); }, pull: async () => { const config = await getConfigOrThrow(locations); @@ -340,8 +346,10 @@ async function resolveBranch( } } -function buildConfigFromInit(options: InitOptions) { - const repo = resolveRepoFromInit(options); +const DEFAULT_REPO_NAME = 'my-opencode-config'; + +async function buildConfigFromInit($: Shell, options: InitOptions) { + const repo = await resolveRepoFromInit($, options); return normalizeSyncConfig({ repo, includeSecrets: options.includeSecrets ?? false, @@ -352,7 +360,7 @@ function buildConfigFromInit(options: InitOptions) { }); } -function resolveRepoFromInit(options: InitOptions) { +async function resolveRepoFromInit($: Shell, options: InitOptions) { if (options.url) { return { url: options.url, branch: options.branch }; } @@ -367,8 +375,17 @@ function resolveRepoFromInit(options: InitOptions) { if (owner && name) { return { owner, name, branch: options.branch }; } + // If only a name is provided (no slash), use it as the repo name with auto-detected owner + if (options.repo && !options.repo.includes('/')) { + const owner = await getAuthenticatedUser($); + return { owner, name: options.repo, branch: options.branch }; + } } - return undefined; + + // Default: auto-detect owner, use default repo name + const owner = await getAuthenticatedUser($); + const name = DEFAULT_REPO_NAME; + return { owner, name, branch: options.branch }; } async function createRepo( From b1c0fef508c3f9ea6e00a42abb033fcbdad18c19 Mon Sep 17 00:00:00 2001 From: iHildy Date: Sun, 28 Dec 2025 16:03:27 -0600 Subject: [PATCH 2/2] fix: adjust repo org/name detection --- src/sync/service.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/sync/service.ts b/src/sync/service.ts index a26c226..a07fdb9 100644 --- a/src/sync/service.ts +++ b/src/sync/service.ts @@ -371,15 +371,15 @@ async function resolveRepoFromInit($: Shell, options: InitOptions) { if (options.repo.includes('://') || options.repo.endsWith('.git')) { return { url: options.repo, branch: options.branch }; } - const [owner, name] = options.repo.split('/'); - if (owner && name) { - return { owner, name, branch: options.branch }; - } - // If only a name is provided (no slash), use it as the repo name with auto-detected owner - if (options.repo && !options.repo.includes('/')) { - const owner = await getAuthenticatedUser($); - return { owner, name: options.repo, branch: options.branch }; + if (options.repo.includes('/')) { + const [owner, name] = options.repo.split('/'); + if (owner && name) { + return { owner, name, branch: options.branch }; + } } + + const owner = await getAuthenticatedUser($); + return { owner, name: options.repo, branch: options.branch }; } // Default: auto-detect owner, use default repo name