From 8ab55b547577a0dfbba911161a7d13f2b054c0fb Mon Sep 17 00:00:00 2001 From: iHildy Date: Mon, 13 Apr 2026 01:10:23 -0700 Subject: [PATCH 1/2] feat: sync ~/.agents by default with opt-out flags --- README.md | 9 +++++- src/command/sync-init.md | 2 ++ src/index.ts | 7 +++++ src/sync/config.test.ts | 12 ++++++++ src/sync/config.ts | 8 +++++ src/sync/paths.test.ts | 65 ++++++++++++++++++++++++++++++++++++++++ src/sync/paths.ts | 32 ++++++++++++++++++-- src/sync/service.ts | 8 +++++ 8 files changed, 140 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2358a29..417db77 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,8 @@ Create `~/.config/opencode/opencode-synced.jsonc`: "includeSessions": false, "includePromptStash": false, "includeModelFavorites": true, + "includeSkills": 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: +- `"includeSkills": 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..796730d 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 `includeSkills` 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..87c63df 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)'), + includeSkills: 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, + includeSkills: args.includeSkills, + 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..ce5cde6 100644 --- a/src/sync/config.test.ts +++ b/src/sync/config.test.ts @@ -79,6 +79,18 @@ describe('normalizeSyncConfig', () => { expect(normalized.includeModelFavorites).toBe(true); }); + it('enables skills and home .agents by default', () => { + const normalized = normalizeSyncConfig({}); + expect(normalized.includeSkills).toBe(true); + expect(normalized.includeAgentsDir).toBe(true); + }); + + it('allows disabling skills and home .agents', () => { + const normalized = normalizeSyncConfig({ includeSkills: false, includeAgentsDir: false }); + expect(normalized.includeSkills).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..c3dfefa 100644 --- a/src/sync/config.ts +++ b/src/sync/config.ts @@ -32,6 +32,8 @@ export interface SyncConfig { includeSessions?: boolean; includePromptStash?: boolean; includeModelFavorites?: boolean; + includeSkills?: boolean; + includeAgentsDir?: boolean; secretsBackend?: SecretsBackendConfig; extraSecretPaths?: string[]; extraConfigPaths?: string[]; @@ -43,6 +45,8 @@ export interface NormalizedSyncConfig extends SyncConfig { includeSessions: boolean; includePromptStash: boolean; includeModelFavorites: boolean; + includeSkills: 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 includeSkills = config.includeSkills !== false; + const includeAgentsDir = config.includeAgentsDir !== false; return { includeSecrets, includeMcpSecrets: includeSecrets ? Boolean(config.includeMcpSecrets) : false, includeSessions: Boolean(config.includeSessions), includePromptStash: Boolean(config.includePromptStash), includeModelFavorites, + includeSkills, + 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..302edaa 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, includeSkills: 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..31c7bc4 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.includeSkills !== 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..e777216 100644 --- a/src/sync/service.ts +++ b/src/sync/service.ts @@ -63,6 +63,8 @@ interface InitOptions { includeSessions?: boolean; includePromptStash?: boolean; includeModelFavorites?: boolean; + includeSkills?: 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 includeSkills = config.includeSkills ? '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: ${includeSkills}`, + `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, + includeSkills: options.includeSkills ?? true, + includeAgentsDir: options.includeAgentsDir ?? true, extraSecretPaths: options.extraSecretPaths ?? [], extraConfigPaths: options.extraConfigPaths ?? [], localRepoPath: options.localRepoPath, From 7713e2bbc61f7fe515f25334e2fd29155983919b Mon Sep 17 00:00:00 2001 From: iHildy Date: Mon, 13 Apr 2026 01:14:41 -0700 Subject: [PATCH 2/2] refactor: rename includeSkills to includeOpencodeSkills --- README.md | 4 ++-- src/command/sync-init.md | 2 +- src/index.ts | 4 ++-- src/sync/config.test.ts | 9 ++++++--- src/sync/config.ts | 8 ++++---- src/sync/paths.test.ts | 2 +- src/sync/paths.ts | 2 +- src/sync/service.ts | 8 ++++---- 8 files changed, 21 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 417db77..7317479 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Create `~/.config/opencode/opencode-synced.jsonc`: "includeSessions": false, "includePromptStash": false, "includeModelFavorites": true, - "includeSkills": true, + "includeOpencodeSkills": true, "includeAgentsDir": true, "extraSecretPaths": [], "extraConfigPaths": [], @@ -96,7 +96,7 @@ Create `~/.config/opencode/opencode-synced.jsonc`: - 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: -- `"includeSkills": false` to skip `~/.config/opencode/skills/` +- `"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 796730d..50ebe35 100644 --- a/src/command/sync-init.md +++ b/src/command/sync-init.md @@ -13,6 +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 `includeSkills` only if explicitly requested. +- 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 87c63df..5353756 100644 --- a/src/index.ts +++ b/src/index.ts @@ -152,7 +152,7 @@ export const opencodeConfigSync: Plugin = async (ctx) => { .boolean() .optional() .describe('Sync model favorites (state/model.json)'), - includeSkills: tool.schema + includeOpencodeSkills: tool.schema .boolean() .optional() .describe('Sync ~/.config/opencode/skills directory'), @@ -180,7 +180,7 @@ export const opencodeConfigSync: Plugin = async (ctx) => { includeSessions: args.includeSessions, includePromptStash: args.includePromptStash, includeModelFavorites: args.includeModelFavorites, - includeSkills: args.includeSkills, + includeOpencodeSkills: args.includeOpencodeSkills, includeAgentsDir: args.includeAgentsDir, create: args.create, private: args.private, diff --git a/src/sync/config.test.ts b/src/sync/config.test.ts index ce5cde6..2f6c757 100644 --- a/src/sync/config.test.ts +++ b/src/sync/config.test.ts @@ -81,13 +81,16 @@ describe('normalizeSyncConfig', () => { it('enables skills and home .agents by default', () => { const normalized = normalizeSyncConfig({}); - expect(normalized.includeSkills).toBe(true); + expect(normalized.includeOpencodeSkills).toBe(true); expect(normalized.includeAgentsDir).toBe(true); }); it('allows disabling skills and home .agents', () => { - const normalized = normalizeSyncConfig({ includeSkills: false, includeAgentsDir: false }); - expect(normalized.includeSkills).toBe(false); + const normalized = normalizeSyncConfig({ + includeOpencodeSkills: false, + includeAgentsDir: false, + }); + expect(normalized.includeOpencodeSkills).toBe(false); expect(normalized.includeAgentsDir).toBe(false); }); diff --git a/src/sync/config.ts b/src/sync/config.ts index c3dfefa..ec8ad23 100644 --- a/src/sync/config.ts +++ b/src/sync/config.ts @@ -32,7 +32,7 @@ export interface SyncConfig { includeSessions?: boolean; includePromptStash?: boolean; includeModelFavorites?: boolean; - includeSkills?: boolean; + includeOpencodeSkills?: boolean; includeAgentsDir?: boolean; secretsBackend?: SecretsBackendConfig; extraSecretPaths?: string[]; @@ -45,7 +45,7 @@ export interface NormalizedSyncConfig extends SyncConfig { includeSessions: boolean; includePromptStash: boolean; includeModelFavorites: boolean; - includeSkills: boolean; + includeOpencodeSkills: boolean; includeAgentsDir: boolean; secretsBackend?: SecretsBackendConfig; extraSecretPaths: string[]; @@ -110,7 +110,7 @@ export function normalizeSecretsBackend( export function normalizeSyncConfig(config: SyncConfig): NormalizedSyncConfig { const includeSecrets = Boolean(config.includeSecrets); const includeModelFavorites = config.includeModelFavorites !== false; - const includeSkills = config.includeSkills !== false; + const includeOpencodeSkills = config.includeOpencodeSkills !== false; const includeAgentsDir = config.includeAgentsDir !== false; return { includeSecrets, @@ -118,7 +118,7 @@ export function normalizeSyncConfig(config: SyncConfig): NormalizedSyncConfig { includeSessions: Boolean(config.includeSessions), includePromptStash: Boolean(config.includePromptStash), includeModelFavorites, - includeSkills, + includeOpencodeSkills, includeAgentsDir, secretsBackend: normalizeSecretsBackend(config.secretsBackend), extraSecretPaths: Array.isArray(config.extraSecretPaths) ? config.extraSecretPaths : [], diff --git a/src/sync/paths.test.ts b/src/sync/paths.test.ts index 302edaa..31a8b60 100644 --- a/src/sync/paths.test.ts +++ b/src/sync/paths.test.ts @@ -104,7 +104,7 @@ describe('buildSyncPlan', () => { expect(skillsItem?.type).toBe('dir'); const disabledPlan = buildSyncPlan( - normalizeSyncConfig({ ...config, includeSkills: false }), + normalizeSyncConfig({ ...config, includeOpencodeSkills: false }), locations, '/repo', 'linux' diff --git a/src/sync/paths.ts b/src/sync/paths.ts index 31c7bc4..d7aff76 100644 --- a/src/sync/paths.ts +++ b/src/sync/paths.ts @@ -216,7 +216,7 @@ export function buildSyncPlan( }); } - if (config.includeSkills !== false) { + if (config.includeOpencodeSkills !== false) { items.push({ localPath: path.join(configRoot, SKILLS_DIR), repoPath: path.join(repoConfigRoot, SKILLS_DIR), diff --git a/src/sync/service.ts b/src/sync/service.ts index e777216..f220e74 100644 --- a/src/sync/service.ts +++ b/src/sync/service.ts @@ -63,7 +63,7 @@ interface InitOptions { includeSessions?: boolean; includePromptStash?: boolean; includeModelFavorites?: boolean; - includeSkills?: boolean; + includeOpencodeSkills?: boolean; includeAgentsDir?: boolean; create?: boolean; private?: boolean; @@ -319,7 +319,7 @@ 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 includeSkills = config.includeSkills ? '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'; @@ -344,7 +344,7 @@ export function createSyncService(ctx: SyncServiceContext): SyncService { `Sessions: ${includeSessions}`, `Prompt stash: ${includePromptStash}`, `Model favorites: ${includeModelFavorites}`, - `Skills: ${includeSkills}`, + `Skills: ${includeOpencodeSkills}`, `Home .agents: ${includeAgentsDir}`, `Last pull: ${lastPull}`, `Last push: ${lastPush}`, @@ -773,7 +773,7 @@ async function buildConfigFromInit($: Shell, options: InitOptions) { includeSessions: options.includeSessions ?? false, includePromptStash: options.includePromptStash ?? false, includeModelFavorites: options.includeModelFavorites ?? true, - includeSkills: options.includeSkills ?? true, + includeOpencodeSkills: options.includeOpencodeSkills ?? true, includeAgentsDir: options.includeAgentsDir ?? true, extraSecretPaths: options.extraSecretPaths ?? [], extraConfigPaths: options.extraConfigPaths ?? [],