diff --git a/README.md b/README.md index 143687c..6942ad6 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ Create `~/.config/opencode/opencode-synced.jsonc`: "includeMcpSecrets": false, "includeSessions": false, "includePromptStash": false, + "includeModelFavorites": true, "extraSecretPaths": [], "extraConfigPaths": [], } @@ -88,6 +89,7 @@ Create `~/.config/opencode/opencode-synced.jsonc`: - `~/.config/opencode/opencode.json` and `opencode.jsonc` - `~/.config/opencode/AGENTS.md` - `~/.config/opencode/agent/`, `command/`, `mode/`, `tool/`, `themes/`, `plugin/` +- `~/.local/state/opencode/model.json` (model favorites) - Any extra paths in `extraConfigPaths` (allowlist, files or folders) ### Secrets (private repos only) diff --git a/src/command/sync-init.md b/src/command/sync-init.md index 02e714b..18eb40e 100644 --- a/src/command/sync-init.md +++ b/src/command/sync-init.md @@ -11,3 +11,4 @@ If the user wants a public repo, pass private=false. Include includeSecrets if the user explicitly opts in. Include includeMcpSecrets only if they want MCP secrets committed to a private repo. If the user supplies extra config paths, pass extraConfigPaths. +Model favorites sync is enabled by default; set includeModelFavorites=false to disable. diff --git a/src/index.ts b/src/index.ts index f167961..48fbb75 100644 --- a/src/index.ts +++ b/src/index.ts @@ -137,6 +137,10 @@ export const OpencodeConfigSync: Plugin = async (ctx) => { .boolean() .optional() .describe('Enable prompt stash/history sync (requires includeSecrets)'), + includeModelFavorites: tool.schema + .boolean() + .optional() + .describe('Sync model favorites (state/model.json)'), create: tool.schema.boolean().optional().describe('Create repo if missing'), private: tool.schema.boolean().optional().describe('Create repo as private'), extraSecretPaths: tool.schema.array(tool.schema.string()).optional(), @@ -159,6 +163,7 @@ export const OpencodeConfigSync: Plugin = async (ctx) => { includeMcpSecrets: args.includeMcpSecrets, includeSessions: args.includeSessions, includePromptStash: args.includePromptStash, + includeModelFavorites: args.includeModelFavorites, create: args.create, private: args.private, extraSecretPaths: args.extraSecretPaths, diff --git a/src/sync/config.test.ts b/src/sync/config.test.ts index 28b369f..e8b925c 100644 --- a/src/sync/config.test.ts +++ b/src/sync/config.test.ts @@ -72,6 +72,11 @@ describe('normalizeSyncConfig', () => { }); expect(normalized.includeMcpSecrets).toBe(true); }); + + it('enables model favorites by default', () => { + const normalized = normalizeSyncConfig({}); + expect(normalized.includeModelFavorites).toBe(true); + }); }); describe('canCommitMcpSecrets', () => { diff --git a/src/sync/config.ts b/src/sync/config.ts index ed0a1bc..cdc6b2d 100644 --- a/src/sync/config.ts +++ b/src/sync/config.ts @@ -17,6 +17,7 @@ export interface SyncConfig { includeMcpSecrets?: boolean; includeSessions?: boolean; includePromptStash?: boolean; + includeModelFavorites?: boolean; extraSecretPaths?: string[]; extraConfigPaths?: string[]; } @@ -48,11 +49,13 @@ export async function chmodIfExists(filePath: string, mode: number): Promise { expect(plan.extraSecrets.allowlist.length).toBe(1); expect(plan.extraConfigs.allowlist.length).toBe(1); }); + + it('includes model favorites by default and allows disabling', () => { + const env = { HOME: '/home/test' } as NodeJS.ProcessEnv; + const locations = resolveSyncLocations(env, 'linux'); + const config: SyncConfig = { + repo: { owner: 'acme', name: 'config' }, + includeSecrets: false, + }; + + const plan = buildSyncPlan(config, locations, '/repo', 'linux'); + const favoritesItem = plan.items.find((item) => + item.localPath.endsWith('/.local/state/opencode/model.json') + ); + + expect(favoritesItem).toBeTruthy(); + + const disabledPlan = buildSyncPlan( + { ...config, includeModelFavorites: false }, + locations, + '/repo', + 'linux' + ); + const disabledItem = disabledPlan.items.find((item) => + item.localPath.endsWith('/.local/state/opencode/model.json') + ); + + expect(disabledItem).toBeUndefined(); + }); }); diff --git a/src/sync/paths.ts b/src/sync/paths.ts index fd474f5..4aeb094 100644 --- a/src/sync/paths.ts +++ b/src/sync/paths.ts @@ -54,6 +54,7 @@ const DEFAULT_STATE_NAME = 'sync-state.json'; const CONFIG_DIRS = ['agent', 'command', 'mode', 'tool', 'themes', 'plugin']; const SESSION_DIRS = ['storage/session', 'storage/message', 'storage/part', 'storage/session_diff']; const PROMPT_STASH_FILES = ['prompt-stash.jsonl', 'prompt-history.jsonl']; +const MODEL_FAVORITES_FILE = 'model.json'; export function resolveHomeDir( env: NodeJS.ProcessEnv = process.env, @@ -173,9 +174,11 @@ export function buildSyncPlan( ): SyncPlan { const configRoot = locations.configRoot; const dataRoot = path.join(locations.xdg.dataDir, 'opencode'); + const stateRoot = path.join(locations.xdg.stateDir, 'opencode'); const repoConfigRoot = path.join(repoRoot, 'config'); const repoDataRoot = path.join(repoRoot, 'data'); const repoSecretsRoot = path.join(repoRoot, 'secrets'); + const repoStateRoot = path.join(repoRoot, 'state'); const repoExtraDir = path.join(repoSecretsRoot, 'extra'); const manifestPath = path.join(repoSecretsRoot, 'extra-manifest.json'); const repoConfigExtraDir = path.join(repoConfigRoot, 'extra'); @@ -207,6 +210,16 @@ export function buildSyncPlan( }); } + if (config.includeModelFavorites !== false) { + items.push({ + localPath: path.join(stateRoot, MODEL_FAVORITES_FILE), + repoPath: path.join(repoStateRoot, MODEL_FAVORITES_FILE), + type: 'file', + isSecret: false, + isConfigFile: false, + }); + } + if (config.includeSecrets) { items.push( { @@ -238,8 +251,6 @@ export function buildSyncPlan( } if (config.includePromptStash) { - const stateRoot = path.join(locations.xdg.stateDir, 'opencode'); - const repoStateRoot = path.join(repoRoot, 'state'); for (const fileName of PROMPT_STASH_FILES) { items.push({ localPath: path.join(stateRoot, fileName), diff --git a/src/sync/service.ts b/src/sync/service.ts index ee300f5..64c47f0 100644 --- a/src/sync/service.ts +++ b/src/sync/service.ts @@ -53,6 +53,7 @@ interface InitOptions { includeMcpSecrets?: boolean; includeSessions?: boolean; includePromptStash?: boolean; + includeModelFavorites?: boolean; create?: boolean; private?: boolean; extraSecretPaths?: string[]; @@ -175,6 +176,7 @@ export function createSyncService(ctx: SyncServiceContext): SyncService { const includeMcpSecrets = config.includeMcpSecrets ? 'enabled' : 'disabled'; const includeSessions = config.includeSessions ? 'enabled' : 'disabled'; const includePromptStash = config.includePromptStash ? 'enabled' : 'disabled'; + const includeModelFavorites = config.includeModelFavorites ? 'enabled' : 'disabled'; const lastPull = state.lastPull ?? 'never'; const lastPush = state.lastPush ?? 'never'; @@ -195,6 +197,7 @@ export function createSyncService(ctx: SyncServiceContext): SyncService { `MCP secrets: ${includeMcpSecrets}`, `Sessions: ${includeSessions}`, `Prompt stash: ${includePromptStash}`, + `Model favorites: ${includeModelFavorites}`, `Last pull: ${lastPull}`, `Last push: ${lastPush}`, `Working tree: ${changesLabel}`, @@ -539,6 +542,7 @@ async function buildConfigFromInit($: Shell, options: InitOptions) { includeMcpSecrets: options.includeMcpSecrets ?? false, includeSessions: options.includeSessions ?? false, includePromptStash: options.includePromptStash ?? false, + includeModelFavorites: options.includeModelFavorites ?? true, extraSecretPaths: options.extraSecretPaths ?? [], extraConfigPaths: options.extraConfigPaths ?? [], localRepoPath: options.localRepoPath,