Skip to content

Commit 4cd2284

Browse files
committed
feat: add default ~/.agents sync with configurable toggles
2 parents e5a43b7 + 7492151 commit 4cd2284

8 files changed

Lines changed: 136 additions & 2 deletions

File tree

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ Create `~/.config/opencode/opencode-synced.jsonc`:
8484
},
8585
"includePromptStash": false,
8686
"includeModelFavorites": true,
87+
"includeOpencodeSkills": true,
88+
"includeAgentsDir": true,
8789
"extraSecretPaths": [],
8890
"extraConfigPaths": [],
8991
}
@@ -96,8 +98,13 @@ Create `~/.config/opencode/opencode-synced.jsonc`:
9698
- `~/.config/opencode/opencode.json` and `opencode.jsonc`
9799
- `~/.config/opencode/AGENTS.md`
98100
- `~/.config/opencode/agent/`, `command/`, `mode/`, `tool/`, `themes/`, `plugin/`, `skills/`
101+
- `~/.agents/`
99102
- `~/.local/state/opencode/model.json` (model favorites)
100-
- Any additional paths in `extraConfigPaths` (allowlist, files or folders). You do not need to include default paths like `~/.config/opencode/skills`.
103+
- Any additional paths in `extraConfigPaths` (allowlist, files or folders). You do not need to include default paths like `~/.config/opencode/skills` or `~/.agents`.
104+
105+
Disable default directory sync by setting:
106+
- `"includeOpencodeSkills": false` to skip `~/.config/opencode/skills/`
107+
- `"includeAgentsDir": false` to skip `~/.agents/`
101108

102109
### Secrets (private repos only)
103110

src/command/sync-init.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ Rules:
1313
- Keep repo private unless the user explicitly asked for public.
1414
- Include `includeSecrets` only if explicitly requested.
1515
- Include `includeMcpSecrets` only if explicitly requested and secrets are enabled.
16+
- Include `includeOpencodeSkills` only if explicitly requested.
17+
- Include `includeAgentsDir` only if explicitly requested.
1618
- Include `extraConfigPaths` only if explicitly provided.

src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@ export const opencodeConfigSync: Plugin = async (ctx) => {
160160
.boolean()
161161
.optional()
162162
.describe('Sync model favorites (state/model.json)'),
163+
includeOpencodeSkills: tool.schema
164+
.boolean()
165+
.optional()
166+
.describe('Sync ~/.config/opencode/skills directory'),
167+
includeAgentsDir: tool.schema.boolean().optional().describe('Sync ~/.agents directory'),
163168
create: tool.schema.boolean().optional().describe('Create repo if missing'),
164169
private: tool.schema.boolean().optional().describe('Create repo as private'),
165170
extraSecretPaths: tool.schema.array(tool.schema.string()).optional(),
@@ -194,6 +199,8 @@ export const opencodeConfigSync: Plugin = async (ctx) => {
194199
includeModelFavorites: args.includeModelFavorites,
195200
setupTurso: args.setupTurso,
196201
migrateSessions: args.migrateSessions,
202+
includeOpencodeSkills: args.includeOpencodeSkills,
203+
includeAgentsDir: args.includeAgentsDir,
197204
create: args.create,
198205
private: args.private,
199206
extraSecretPaths: args.extraSecretPaths,

src/sync/config.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,21 @@ describe('normalizeSyncConfig', () => {
8181
expect(normalized.includeModelFavorites).toBe(true);
8282
});
8383

84+
it('enables skills and home .agents by default', () => {
85+
const normalized = normalizeSyncConfig({});
86+
expect(normalized.includeOpencodeSkills).toBe(true);
87+
expect(normalized.includeAgentsDir).toBe(true);
88+
});
89+
90+
it('allows disabling skills and home .agents', () => {
91+
const normalized = normalizeSyncConfig({
92+
includeOpencodeSkills: false,
93+
includeAgentsDir: false,
94+
});
95+
expect(normalized.includeOpencodeSkills).toBe(false);
96+
expect(normalized.includeAgentsDir).toBe(false);
97+
});
98+
8499
it('defaults extra path lists when omitted', () => {
85100
const normalized = normalizeSyncConfig({ includeSecrets: true });
86101
expect(normalized.extraSecretPaths).toEqual([]);

src/sync/config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ export interface SyncConfig {
5959
sessionBackend?: SessionBackendConfig;
6060
includePromptStash?: boolean;
6161
includeModelFavorites?: boolean;
62+
includeOpencodeSkills?: boolean;
63+
includeAgentsDir?: boolean;
6264
secretsBackend?: SecretsBackendConfig;
6365
extraSecretPaths?: string[];
6466
extraConfigPaths?: string[];
@@ -71,6 +73,8 @@ export interface NormalizedSyncConfig extends SyncConfig {
7173
sessionBackend: NormalizedSessionBackendConfig;
7274
includePromptStash: boolean;
7375
includeModelFavorites: boolean;
76+
includeOpencodeSkills: boolean;
77+
includeAgentsDir: boolean;
7478
secretsBackend?: SecretsBackendConfig;
7579
extraSecretPaths: string[];
7680
extraConfigPaths: string[];
@@ -169,13 +173,17 @@ export function isTursoSessionBackend(config: SyncConfig | NormalizedSyncConfig)
169173
export function normalizeSyncConfig(config: SyncConfig): NormalizedSyncConfig {
170174
const includeSecrets = Boolean(config.includeSecrets);
171175
const includeModelFavorites = config.includeModelFavorites !== false;
176+
const includeOpencodeSkills = config.includeOpencodeSkills !== false;
177+
const includeAgentsDir = config.includeAgentsDir !== false;
172178
return {
173179
includeSecrets,
174180
includeMcpSecrets: includeSecrets ? Boolean(config.includeMcpSecrets) : false,
175181
includeSessions: Boolean(config.includeSessions),
176182
sessionBackend: normalizeSessionBackend(config.sessionBackend),
177183
includePromptStash: Boolean(config.includePromptStash),
178184
includeModelFavorites,
185+
includeOpencodeSkills,
186+
includeAgentsDir,
179187
secretsBackend: normalizeSecretsBackend(config.secretsBackend),
180188
extraSecretPaths: Array.isArray(config.extraSecretPaths) ? config.extraSecretPaths : [],
181189
extraConfigPaths: Array.isArray(config.extraConfigPaths) ? config.extraConfigPaths : [],

src/sync/paths.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,17 @@ describe('buildSyncPlan', () => {
121121

122122
expect(skillsItem).toBeTruthy();
123123
expect(skillsItem?.type).toBe('dir');
124+
125+
const disabledPlan = buildSyncPlan(
126+
normalizeSyncConfig({ ...config, includeOpencodeSkills: false }),
127+
locations,
128+
'/repo',
129+
'linux'
130+
);
131+
const disabledSkillsItem = disabledPlan.items.find((item) =>
132+
item.localPath.endsWith('/.config/opencode/skills')
133+
);
134+
expect(disabledSkillsItem).toBeUndefined();
124135
});
125136

126137
it('filters skills path from extra config paths', () => {
@@ -152,6 +163,60 @@ describe('buildSyncPlan', () => {
152163
expect(plan.extraConfigs.allowlist).toEqual([customConfigPath]);
153164
});
154165

166+
it('includes home .agents directory by default and allows disabling', () => {
167+
const env = { HOME: '/home/test' } as NodeJS.ProcessEnv;
168+
const locations = resolveSyncLocations(env, 'linux');
169+
const config: SyncConfig = {
170+
repo: { owner: 'acme', name: 'config' },
171+
includeSecrets: false,
172+
};
173+
174+
const plan = buildSyncPlan(normalizeSyncConfig(config), locations, '/repo', 'linux');
175+
const agentsItem = plan.items.find((item) => item.localPath.endsWith('/.agents'));
176+
177+
expect(agentsItem).toBeTruthy();
178+
expect(agentsItem?.repoPath.endsWith('/config/.agents')).toBe(true);
179+
expect(agentsItem?.type).toBe('dir');
180+
181+
const disabledPlan = buildSyncPlan(
182+
normalizeSyncConfig({ ...config, includeAgentsDir: false }),
183+
locations,
184+
'/repo',
185+
'linux'
186+
);
187+
const disabledAgentsItem = disabledPlan.items.find((item) =>
188+
item.localPath.endsWith('/.agents')
189+
);
190+
expect(disabledAgentsItem).toBeUndefined();
191+
});
192+
193+
it('filters home .agents path from extra config paths', () => {
194+
const env = { HOME: '/home/test' } as NodeJS.ProcessEnv;
195+
const locations = resolveSyncLocations(env, 'linux');
196+
const config: SyncConfig = {
197+
repo: { owner: 'acme', name: 'config' },
198+
includeSecrets: false,
199+
extraConfigPaths: ['~/.agents'],
200+
};
201+
202+
const plan = buildSyncPlan(normalizeSyncConfig(config), locations, '/repo', 'linux');
203+
expect(plan.extraConfigs.allowlist.length).toBe(0);
204+
});
205+
206+
it('keeps non-default extra config paths when home .agents is also listed', () => {
207+
const env = { HOME: '/home/test' } as NodeJS.ProcessEnv;
208+
const locations = resolveSyncLocations(env, 'linux');
209+
const customConfigPath = `${locations.configRoot}/custom.json`;
210+
const config: SyncConfig = {
211+
repo: { owner: 'acme', name: 'config' },
212+
includeSecrets: false,
213+
extraConfigPaths: ['~/.agents', customConfigPath],
214+
};
215+
216+
const plan = buildSyncPlan(normalizeSyncConfig(config), locations, '/repo', 'linux');
217+
expect(plan.extraConfigs.allowlist).toEqual([customConfigPath]);
218+
});
219+
155220
it('includes secrets when includeSecrets is true', () => {
156221
const env = { HOME: '/home/test' } as NodeJS.ProcessEnv;
157222
const locations = resolveSyncLocations(env, 'linux');

src/sync/paths.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,13 @@ const DEFAULT_SYNC_CONFIG_NAME = 'opencode-synced.jsonc';
5252
const DEFAULT_OVERRIDES_NAME = 'opencode-synced.overrides.jsonc';
5353
const DEFAULT_STATE_NAME = 'sync-state.json';
5454

55-
const CONFIG_DIRS = ['agent', 'command', 'mode', 'tool', 'themes', 'plugin', 'skills'];
55+
const CONFIG_DIRS = ['agent', 'command', 'mode', 'tool', 'themes', 'plugin'];
5656
const SESSION_DIRS = ['storage/session', 'storage/message', 'storage/part', 'storage/session_diff'];
5757
const SESSION_DB_FILE = 'opencode.db';
5858
const PROMPT_STASH_FILES = ['prompt-stash.jsonl', 'prompt-history.jsonl'];
5959
const MODEL_FAVORITES_FILE = 'model.json';
60+
const SKILLS_DIR = 'skills';
61+
const HOME_AGENTS_DIR = '.agents';
6062

6163
export function resolveHomeDir(
6264
env: NodeJS.ProcessEnv = process.env,
@@ -216,6 +218,26 @@ export function buildSyncPlan(
216218
});
217219
}
218220

221+
if (config.includeOpencodeSkills !== false) {
222+
items.push({
223+
localPath: path.join(configRoot, SKILLS_DIR),
224+
repoPath: path.join(repoConfigRoot, SKILLS_DIR),
225+
type: 'dir',
226+
isSecret: false,
227+
isConfigFile: false,
228+
});
229+
}
230+
231+
if (config.includeAgentsDir !== false) {
232+
items.push({
233+
localPath: path.join(locations.xdg.homeDir, HOME_AGENTS_DIR),
234+
repoPath: path.join(repoConfigRoot, HOME_AGENTS_DIR),
235+
type: 'dir',
236+
isSecret: false,
237+
isConfigFile: false,
238+
});
239+
}
240+
219241
if (config.includeModelFavorites !== false) {
220242
items.push({
221243
localPath: path.join(stateRoot, MODEL_FAVORITES_FILE),

src/sync/service.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ interface InitOptions {
7474
includeModelFavorites?: boolean;
7575
setupTurso?: boolean;
7676
migrateSessions?: boolean;
77+
includeOpencodeSkills?: boolean;
78+
includeAgentsDir?: boolean;
7779
create?: boolean;
7880
private?: boolean;
7981
extraSecretPaths?: string[];
@@ -677,6 +679,8 @@ export function createSyncService(ctx: SyncServiceContext): SyncService {
677679
: 'git (best effort, may conflict with concurrent writers)';
678680
const includePromptStash = config.includePromptStash ? 'enabled' : 'disabled';
679681
const includeModelFavorites = config.includeModelFavorites ? 'enabled' : 'disabled';
682+
const includeOpencodeSkills = config.includeOpencodeSkills ? 'enabled' : 'disabled';
683+
const includeAgentsDir = config.includeAgentsDir ? 'enabled' : 'disabled';
680684
const secretsBackend = config.secretsBackend?.type ?? 'none';
681685
const lastPull = state.lastPull ?? 'never';
682686
const lastPush = state.lastPush ?? 'never';
@@ -714,6 +718,8 @@ export function createSyncService(ctx: SyncServiceContext): SyncService {
714718
`Last session push: ${lastSessionPush}`,
715719
`Prompt stash: ${includePromptStash}`,
716720
`Model favorites: ${includeModelFavorites}`,
721+
`Skills: ${includeOpencodeSkills}`,
722+
`Home .agents: ${includeAgentsDir}`,
717723
`Last pull: ${lastPull}`,
718724
`Last push: ${lastPush}`,
719725
`Working tree: ${changesLabel}`,
@@ -1446,6 +1452,8 @@ async function buildConfigFromInit($: Shell, options: InitOptions) {
14461452
: undefined,
14471453
includePromptStash: options.includePromptStash ?? false,
14481454
includeModelFavorites: options.includeModelFavorites ?? true,
1455+
includeOpencodeSkills: options.includeOpencodeSkills ?? true,
1456+
includeAgentsDir: options.includeAgentsDir ?? true,
14491457
extraSecretPaths: options.extraSecretPaths ?? [],
14501458
extraConfigPaths: options.extraConfigPaths ?? [],
14511459
localRepoPath: options.localRepoPath,

0 commit comments

Comments
 (0)