From dbe10b5d28c18e94f69b045dd55631f7b74fc335 Mon Sep 17 00:00:00 2001 From: iHildy Date: Mon, 13 Apr 2026 00:57:57 -0700 Subject: [PATCH 1/3] feat: sync skills directory by default --- README.md | 4 ++-- src/sync/paths.test.ts | 46 ++++++++++++++++++++++++++++++++++++++++++ src/sync/paths.ts | 6 ++++-- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2819bf4..2358a29 100644 --- a/README.md +++ b/README.md @@ -88,9 +88,9 @@ 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/` +- `~/.config/opencode/agent/`, `command/`, `mode/`, `tool/`, `themes/`, `plugin/`, `skills/` - `~/.local/state/opencode/model.json` (model favorites) -- Any extra paths in `extraConfigPaths` (allowlist, files or folders) +- Any additional paths in `extraConfigPaths` (allowlist, files or folders). You do not need to include default paths like `~/.config/opencode/skills`. ### Secrets (private repos only) diff --git a/src/sync/paths.test.ts b/src/sync/paths.test.ts index 9ede236..d410ac1 100644 --- a/src/sync/paths.test.ts +++ b/src/sync/paths.test.ts @@ -87,6 +87,52 @@ describe('buildSyncPlan', () => { expect(plan.extraConfigs.allowlist.length).toBe(0); }); + it('includes skills directory in default sync items', () => { + 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(normalizeSyncConfig(config), locations, '/repo', 'linux'); + const skillsItem = plan.items.find((item) => + item.localPath.endsWith('/.config/opencode/skills') + ); + + expect(skillsItem).toBeTruthy(); + expect(skillsItem?.type).toBe('dir'); + }); + + it('filters skills path from extra config paths', () => { + const env = { HOME: '/home/test' } as NodeJS.ProcessEnv; + const locations = resolveSyncLocations(env, 'linux'); + const config: SyncConfig = { + repo: { owner: 'acme', name: 'config' }, + includeSecrets: false, + extraConfigPaths: [`${locations.configRoot}/skills`], + }; + + const plan = buildSyncPlan(normalizeSyncConfig(config), locations, '/repo', 'linux'); + + expect(plan.extraConfigs.allowlist.length).toBe(0); + }); + + it('keeps non-default extra config paths when skills is also listed', () => { + const env = { HOME: '/home/test' } as NodeJS.ProcessEnv; + const locations = resolveSyncLocations(env, 'linux'); + const customConfigPath = `${locations.configRoot}/custom.json`; + const config: SyncConfig = { + repo: { owner: 'acme', name: 'config' }, + includeSecrets: false, + extraConfigPaths: [`${locations.configRoot}/skills`, customConfigPath], + }; + + const plan = buildSyncPlan(normalizeSyncConfig(config), locations, '/repo', 'linux'); + + expect(plan.extraConfigs.allowlist).toEqual([customConfigPath]); + }); + it('includes secrets when includeSecrets is true', () => { const env = { HOME: '/home/test' } as NodeJS.ProcessEnv; const locations = resolveSyncLocations(env, 'linux'); diff --git a/src/sync/paths.ts b/src/sync/paths.ts index 995118d..5b292d4 100644 --- a/src/sync/paths.ts +++ b/src/sync/paths.ts @@ -51,7 +51,7 @@ const DEFAULT_SYNC_CONFIG_NAME = 'opencode-synced.jsonc'; const DEFAULT_OVERRIDES_NAME = 'opencode-synced.overrides.jsonc'; const DEFAULT_STATE_NAME = 'sync-state.json'; -const CONFIG_DIRS = ['agent', 'command', 'mode', 'tool', 'themes', 'plugin']; +const CONFIG_DIRS = ['agent', 'command', 'mode', 'tool', 'themes', 'plugin', 'skills']; 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'; @@ -287,7 +287,9 @@ export function buildSyncPlan( ); const extraConfigPaths = (config.extraConfigPaths ?? []).filter( - (entry) => !isSamePath(entry, locations.syncConfigPath, locations.xdg.homeDir, platform) + (entry) => + !isSamePath(entry, locations.syncConfigPath, locations.xdg.homeDir, platform) && + !isSamePath(entry, path.join(configRoot, 'skills'), locations.xdg.homeDir, platform) ); const extraConfigs = buildExtraPathPlan( From 7492151883c3c2c149e78a8743da723e38165a43 Mon Sep 17 00:00:00 2001 From: Ian Hildebrand <25069719+iHildy@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:23:38 -0700 Subject: [PATCH 2/3] feat: add default ~/.agents sync with configurable toggles (#57) * feat: sync ~/.agents by default with opt-out flags * refactor: rename includeSkills to includeOpencodeSkills --- README.md | 9 +++++- src/command/sync-init.md | 2 ++ src/index.ts | 7 +++++ src/sync/config.test.ts | 15 ++++++++++ src/sync/config.ts | 8 +++++ src/sync/paths.test.ts | 65 ++++++++++++++++++++++++++++++++++++++++ src/sync/paths.ts | 32 ++++++++++++++++++-- src/sync/service.ts | 8 +++++ 8 files changed, 143 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2358a29..7317479 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,8 @@ Create `~/.config/opencode/opencode-synced.jsonc`: "includeSessions": false, "includePromptStash": false, "includeModelFavorites": true, + "includeOpencodeSkills": true, + "includeAgentsDir": true, "extraSecretPaths": [], "extraConfigPaths": [], } @@ -89,8 +91,13 @@ 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/`, `skills/` +- `~/.agents/` - `~/.local/state/opencode/model.json` (model favorites) -- Any additional paths in `extraConfigPaths` (allowlist, files or folders). You do not need to include default paths like `~/.config/opencode/skills`. +- Any additional paths in `extraConfigPaths` (allowlist, files or folders). You do not need to include default paths like `~/.config/opencode/skills` or `~/.agents`. + +Disable default directory sync by setting: +- `"includeOpencodeSkills": false` to skip `~/.config/opencode/skills/` +- `"includeAgentsDir": false` to skip `~/.agents/` ### Secrets (private repos only) diff --git a/src/command/sync-init.md b/src/command/sync-init.md index 083dde5..50ebe35 100644 --- a/src/command/sync-init.md +++ b/src/command/sync-init.md @@ -13,4 +13,6 @@ Rules: - Keep repo private unless the user explicitly asked for public. - Include `includeSecrets` only if explicitly requested. - Include `includeMcpSecrets` only if explicitly requested and secrets are enabled. +- Include `includeOpencodeSkills` only if explicitly requested. +- Include `includeAgentsDir` only if explicitly requested. - Include `extraConfigPaths` only if explicitly provided. diff --git a/src/index.ts b/src/index.ts index 1f314ce..5353756 100644 --- a/src/index.ts +++ b/src/index.ts @@ -152,6 +152,11 @@ export const opencodeConfigSync: Plugin = async (ctx) => { .boolean() .optional() .describe('Sync model favorites (state/model.json)'), + includeOpencodeSkills: tool.schema + .boolean() + .optional() + .describe('Sync ~/.config/opencode/skills directory'), + includeAgentsDir: tool.schema.boolean().optional().describe('Sync ~/.agents directory'), 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(), @@ -175,6 +180,8 @@ export const opencodeConfigSync: Plugin = async (ctx) => { includeSessions: args.includeSessions, includePromptStash: args.includePromptStash, includeModelFavorites: args.includeModelFavorites, + includeOpencodeSkills: args.includeOpencodeSkills, + includeAgentsDir: args.includeAgentsDir, create: args.create, private: args.private, extraSecretPaths: args.extraSecretPaths, diff --git a/src/sync/config.test.ts b/src/sync/config.test.ts index 66b8894..2f6c757 100644 --- a/src/sync/config.test.ts +++ b/src/sync/config.test.ts @@ -79,6 +79,21 @@ describe('normalizeSyncConfig', () => { expect(normalized.includeModelFavorites).toBe(true); }); + it('enables skills and home .agents by default', () => { + const normalized = normalizeSyncConfig({}); + expect(normalized.includeOpencodeSkills).toBe(true); + expect(normalized.includeAgentsDir).toBe(true); + }); + + it('allows disabling skills and home .agents', () => { + const normalized = normalizeSyncConfig({ + includeOpencodeSkills: false, + includeAgentsDir: false, + }); + expect(normalized.includeOpencodeSkills).toBe(false); + expect(normalized.includeAgentsDir).toBe(false); + }); + it('defaults extra path lists when omitted', () => { const normalized = normalizeSyncConfig({ includeSecrets: true }); expect(normalized.extraSecretPaths).toEqual([]); diff --git a/src/sync/config.ts b/src/sync/config.ts index b0d4af5..ec8ad23 100644 --- a/src/sync/config.ts +++ b/src/sync/config.ts @@ -32,6 +32,8 @@ export interface SyncConfig { includeSessions?: boolean; includePromptStash?: boolean; includeModelFavorites?: boolean; + includeOpencodeSkills?: boolean; + includeAgentsDir?: boolean; secretsBackend?: SecretsBackendConfig; extraSecretPaths?: string[]; extraConfigPaths?: string[]; @@ -43,6 +45,8 @@ export interface NormalizedSyncConfig extends SyncConfig { includeSessions: boolean; includePromptStash: boolean; includeModelFavorites: boolean; + includeOpencodeSkills: boolean; + includeAgentsDir: boolean; secretsBackend?: SecretsBackendConfig; extraSecretPaths: string[]; extraConfigPaths: string[]; @@ -106,12 +110,16 @@ export function normalizeSecretsBackend( export function normalizeSyncConfig(config: SyncConfig): NormalizedSyncConfig { const includeSecrets = Boolean(config.includeSecrets); const includeModelFavorites = config.includeModelFavorites !== false; + const includeOpencodeSkills = config.includeOpencodeSkills !== false; + const includeAgentsDir = config.includeAgentsDir !== false; return { includeSecrets, includeMcpSecrets: includeSecrets ? Boolean(config.includeMcpSecrets) : false, includeSessions: Boolean(config.includeSessions), includePromptStash: Boolean(config.includePromptStash), includeModelFavorites, + includeOpencodeSkills, + includeAgentsDir, secretsBackend: normalizeSecretsBackend(config.secretsBackend), extraSecretPaths: Array.isArray(config.extraSecretPaths) ? config.extraSecretPaths : [], extraConfigPaths: Array.isArray(config.extraConfigPaths) ? config.extraConfigPaths : [], diff --git a/src/sync/paths.test.ts b/src/sync/paths.test.ts index d410ac1..31a8b60 100644 --- a/src/sync/paths.test.ts +++ b/src/sync/paths.test.ts @@ -102,6 +102,17 @@ describe('buildSyncPlan', () => { expect(skillsItem).toBeTruthy(); expect(skillsItem?.type).toBe('dir'); + + const disabledPlan = buildSyncPlan( + normalizeSyncConfig({ ...config, includeOpencodeSkills: false }), + locations, + '/repo', + 'linux' + ); + const disabledSkillsItem = disabledPlan.items.find((item) => + item.localPath.endsWith('/.config/opencode/skills') + ); + expect(disabledSkillsItem).toBeUndefined(); }); it('filters skills path from extra config paths', () => { @@ -133,6 +144,60 @@ describe('buildSyncPlan', () => { expect(plan.extraConfigs.allowlist).toEqual([customConfigPath]); }); + it('includes home .agents directory 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(normalizeSyncConfig(config), locations, '/repo', 'linux'); + const agentsItem = plan.items.find((item) => item.localPath.endsWith('/.agents')); + + expect(agentsItem).toBeTruthy(); + expect(agentsItem?.repoPath.endsWith('/config/.agents')).toBe(true); + expect(agentsItem?.type).toBe('dir'); + + const disabledPlan = buildSyncPlan( + normalizeSyncConfig({ ...config, includeAgentsDir: false }), + locations, + '/repo', + 'linux' + ); + const disabledAgentsItem = disabledPlan.items.find((item) => + item.localPath.endsWith('/.agents') + ); + expect(disabledAgentsItem).toBeUndefined(); + }); + + it('filters home .agents path from extra config paths', () => { + const env = { HOME: '/home/test' } as NodeJS.ProcessEnv; + const locations = resolveSyncLocations(env, 'linux'); + const config: SyncConfig = { + repo: { owner: 'acme', name: 'config' }, + includeSecrets: false, + extraConfigPaths: ['~/.agents'], + }; + + const plan = buildSyncPlan(normalizeSyncConfig(config), locations, '/repo', 'linux'); + expect(plan.extraConfigs.allowlist.length).toBe(0); + }); + + it('keeps non-default extra config paths when home .agents is also listed', () => { + const env = { HOME: '/home/test' } as NodeJS.ProcessEnv; + const locations = resolveSyncLocations(env, 'linux'); + const customConfigPath = `${locations.configRoot}/custom.json`; + const config: SyncConfig = { + repo: { owner: 'acme', name: 'config' }, + includeSecrets: false, + extraConfigPaths: ['~/.agents', customConfigPath], + }; + + const plan = buildSyncPlan(normalizeSyncConfig(config), locations, '/repo', 'linux'); + expect(plan.extraConfigs.allowlist).toEqual([customConfigPath]); + }); + it('includes secrets when includeSecrets is true', () => { const env = { HOME: '/home/test' } as NodeJS.ProcessEnv; const locations = resolveSyncLocations(env, 'linux'); diff --git a/src/sync/paths.ts b/src/sync/paths.ts index 5b292d4..d7aff76 100644 --- a/src/sync/paths.ts +++ b/src/sync/paths.ts @@ -51,10 +51,12 @@ const DEFAULT_SYNC_CONFIG_NAME = 'opencode-synced.jsonc'; const DEFAULT_OVERRIDES_NAME = 'opencode-synced.overrides.jsonc'; const DEFAULT_STATE_NAME = 'sync-state.json'; -const CONFIG_DIRS = ['agent', 'command', 'mode', 'tool', 'themes', 'plugin', 'skills']; +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'; +const SKILLS_DIR = 'skills'; +const HOME_AGENTS_DIR = '.agents'; export function resolveHomeDir( env: NodeJS.ProcessEnv = process.env, @@ -214,6 +216,26 @@ export function buildSyncPlan( }); } + if (config.includeOpencodeSkills !== false) { + items.push({ + localPath: path.join(configRoot, SKILLS_DIR), + repoPath: path.join(repoConfigRoot, SKILLS_DIR), + type: 'dir', + isSecret: false, + isConfigFile: false, + }); + } + + if (config.includeAgentsDir !== false) { + items.push({ + localPath: path.join(locations.xdg.homeDir, HOME_AGENTS_DIR), + repoPath: path.join(repoConfigRoot, HOME_AGENTS_DIR), + type: 'dir', + isSecret: false, + isConfigFile: false, + }); + } + if (config.includeModelFavorites !== false) { items.push({ localPath: path.join(stateRoot, MODEL_FAVORITES_FILE), @@ -289,7 +311,13 @@ export function buildSyncPlan( const extraConfigPaths = (config.extraConfigPaths ?? []).filter( (entry) => !isSamePath(entry, locations.syncConfigPath, locations.xdg.homeDir, platform) && - !isSamePath(entry, path.join(configRoot, 'skills'), locations.xdg.homeDir, platform) + !isSamePath(entry, path.join(configRoot, SKILLS_DIR), locations.xdg.homeDir, platform) && + !isSamePath( + entry, + path.join(locations.xdg.homeDir, HOME_AGENTS_DIR), + locations.xdg.homeDir, + platform + ) ); const extraConfigs = buildExtraPathPlan( diff --git a/src/sync/service.ts b/src/sync/service.ts index b9d4b65..f220e74 100644 --- a/src/sync/service.ts +++ b/src/sync/service.ts @@ -63,6 +63,8 @@ interface InitOptions { includeSessions?: boolean; includePromptStash?: boolean; includeModelFavorites?: boolean; + includeOpencodeSkills?: boolean; + includeAgentsDir?: boolean; create?: boolean; private?: boolean; extraSecretPaths?: string[]; @@ -317,6 +319,8 @@ export function createSyncService(ctx: SyncServiceContext): SyncService { const includeSessions = config.includeSessions ? 'enabled' : 'disabled'; const includePromptStash = config.includePromptStash ? 'enabled' : 'disabled'; const includeModelFavorites = config.includeModelFavorites ? 'enabled' : 'disabled'; + const includeOpencodeSkills = config.includeOpencodeSkills ? 'enabled' : 'disabled'; + const includeAgentsDir = config.includeAgentsDir ? 'enabled' : 'disabled'; const secretsBackend = config.secretsBackend?.type ?? 'none'; const lastPull = state.lastPull ?? 'never'; const lastPush = state.lastPush ?? 'never'; @@ -340,6 +344,8 @@ export function createSyncService(ctx: SyncServiceContext): SyncService { `Sessions: ${includeSessions}`, `Prompt stash: ${includePromptStash}`, `Model favorites: ${includeModelFavorites}`, + `Skills: ${includeOpencodeSkills}`, + `Home .agents: ${includeAgentsDir}`, `Last pull: ${lastPull}`, `Last push: ${lastPush}`, `Working tree: ${changesLabel}`, @@ -767,6 +773,8 @@ async function buildConfigFromInit($: Shell, options: InitOptions) { includeSessions: options.includeSessions ?? false, includePromptStash: options.includePromptStash ?? false, includeModelFavorites: options.includeModelFavorites ?? true, + includeOpencodeSkills: options.includeOpencodeSkills ?? true, + includeAgentsDir: options.includeAgentsDir ?? true, extraSecretPaths: options.extraSecretPaths ?? [], extraConfigPaths: options.extraConfigPaths ?? [], localRepoPath: options.localRepoPath, From 5e59cc20713e6970af7d2428e8be110eeb9d42dc Mon Sep 17 00:00:00 2001 From: iHildy Date: Mon, 13 Apr 2026 01:27:06 -0700 Subject: [PATCH 3/3] fix: dedupe extra config paths against default sync items --- src/sync/paths.test.ts | 19 +++++++++++++++++++ src/sync/paths.ts | 3 +-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/sync/paths.test.ts b/src/sync/paths.test.ts index d410ac1..0b1507e 100644 --- a/src/sync/paths.test.ts +++ b/src/sync/paths.test.ts @@ -87,6 +87,25 @@ describe('buildSyncPlan', () => { expect(plan.extraConfigs.allowlist.length).toBe(0); }); + it('filters default sync items from extra config paths', () => { + const env = { HOME: '/home/test' } as NodeJS.ProcessEnv; + const locations = resolveSyncLocations(env, 'linux'); + const customConfigPath = `${locations.configRoot}/custom.json`; + const config: SyncConfig = { + repo: { owner: 'acme', name: 'config' }, + includeSecrets: false, + extraConfigPaths: [ + `${locations.configRoot}/agent`, + `${locations.configRoot}/opencode.json`, + customConfigPath, + ], + }; + + const plan = buildSyncPlan(normalizeSyncConfig(config), locations, '/repo', 'linux'); + + expect(plan.extraConfigs.allowlist).toEqual([customConfigPath]); + }); + it('includes skills directory in default sync items', () => { const env = { HOME: '/home/test' } as NodeJS.ProcessEnv; const locations = resolveSyncLocations(env, 'linux'); diff --git a/src/sync/paths.ts b/src/sync/paths.ts index 5b292d4..c1964d9 100644 --- a/src/sync/paths.ts +++ b/src/sync/paths.ts @@ -288,8 +288,7 @@ export function buildSyncPlan( const extraConfigPaths = (config.extraConfigPaths ?? []).filter( (entry) => - !isSamePath(entry, locations.syncConfigPath, locations.xdg.homeDir, platform) && - !isSamePath(entry, path.join(configRoot, 'skills'), locations.xdg.homeDir, platform) + !items.some((item) => isSamePath(entry, item.localPath, locations.xdg.homeDir, platform)) ); const extraConfigs = buildExtraPathPlan(