Skip to content

Commit a5c484a

Browse files
authored
Add shared skills support across all adapters (#5)
1 parent 83de91e commit a5c484a

24 files changed

+777
-23
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ Global configuration is stored at `~/.syncode/config.json`:
188188
├── packages-arch.txt
189189
├── packages-debian.txt
190190
├── README.md
191+
├── .agents/ # Shared skills (symlinked)
192+
│ └── skills/
191193
└── configs/
192194
├── amp/ # Symlinked
193195
├── antigravity/ # Copy sync
@@ -215,7 +217,7 @@ Global configuration is stored at `~/.syncode/config.json`:
215217
```bash
216218
# Edit your AI agent configs normally
217219
# Example: ~/.config/opencode/opencode.json
218-
# Example: ~/.claude/skills/my-helper.md
220+
# Example: ~/.agents/skills/my-helper.md
219221
# Changes are synced via symlinks automatically
220222

221223
# Check what changed

docs/agents/project-structure.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
- `src/config/`: Configuration management (manager.ts, types.ts).
88
- `src/utils/`: Shared helpers (fs, git, paths, shell, platform).
99
- `configs/`: In the user's repo, stores tracked agent configs.
10+
- `.agents/`: In the user's repo, stores shared skills (symlinked).

src/adapters/agents.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/**
2+
* Shared Agents adapter
3+
*/
4+
5+
import { unlinkSync } from "node:fs";
6+
import { join } from "node:path";
7+
import {
8+
copyDir,
9+
createSymlink,
10+
ensureDir,
11+
exists,
12+
getSymlinkTarget,
13+
isSymlink,
14+
removeDir,
15+
} from "../utils/fs";
16+
import { contractHome } from "../utils/paths";
17+
import type {
18+
AgentAdapter,
19+
ExportResult,
20+
ImportResult,
21+
Platform,
22+
} from "./types";
23+
24+
export class AgentsAdapter implements AgentAdapter {
25+
readonly id = "agents";
26+
readonly name = "Shared Agents";
27+
readonly version = "1.0.0";
28+
readonly syncStrategy = {
29+
import: "copy" as const,
30+
export: "symlink" as const,
31+
};
32+
33+
getConfigPath(_platform: Platform): string {
34+
return join(process.env.HOME || "", ".agents");
35+
}
36+
37+
getRepoPath(repoRoot: string): string {
38+
return join(repoRoot, ".agents");
39+
}
40+
41+
getSkillsPath(_platform: Platform): string {
42+
return join(this.getConfigPath(_platform), "skills");
43+
}
44+
45+
isInstalled(platform: Platform): boolean {
46+
return exists(this.getConfigPath(platform));
47+
}
48+
49+
detect(): boolean {
50+
const platform =
51+
process.platform === "darwin"
52+
? "macos"
53+
: process.platform === "win32"
54+
? "windows"
55+
: "linux";
56+
return this.isInstalled(platform);
57+
}
58+
59+
isLinked(systemPath: string, repoPath: string): boolean {
60+
if (!exists(systemPath) || !exists(repoPath) || !isSymlink(systemPath)) {
61+
return false;
62+
}
63+
return getSymlinkTarget(systemPath) === repoPath;
64+
}
65+
66+
async import(systemPath: string, repoPath: string): Promise<ImportResult> {
67+
if (!exists(systemPath)) {
68+
return {
69+
success: false,
70+
message: "Shared agents config not found on system",
71+
};
72+
}
73+
74+
if (isSymlink(systemPath)) {
75+
return {
76+
success: true,
77+
message: "Already linked to repo - no import needed",
78+
};
79+
}
80+
81+
if (exists(repoPath)) {
82+
return {
83+
success: true,
84+
message: "Configs already in repo - no import needed",
85+
};
86+
}
87+
88+
ensureDir(repoPath);
89+
copyDir(systemPath, repoPath);
90+
91+
return {
92+
success: true,
93+
message: "Imported shared agents configs to repo",
94+
};
95+
}
96+
97+
async export(repoPath: string, systemPath: string): Promise<ExportResult> {
98+
if (!exists(repoPath)) {
99+
return {
100+
success: false,
101+
message: "Shared agents configs not found in repo",
102+
};
103+
}
104+
105+
ensureDir(join(repoPath, "skills"));
106+
107+
if (isSymlink(systemPath)) {
108+
const target = getSymlinkTarget(systemPath);
109+
if (target === repoPath) {
110+
return {
111+
success: true,
112+
message: "Already linked to repo - no export needed",
113+
linkedTo: repoPath,
114+
};
115+
}
116+
}
117+
118+
if (exists(systemPath)) {
119+
if (isSymlink(systemPath)) {
120+
unlinkSync(systemPath);
121+
} else {
122+
const backupPath = `${systemPath}.backup`;
123+
if (exists(backupPath)) {
124+
if (isSymlink(backupPath)) {
125+
unlinkSync(backupPath);
126+
} else {
127+
removeDir(backupPath);
128+
}
129+
}
130+
require("node:fs").renameSync(systemPath, backupPath);
131+
}
132+
}
133+
134+
createSymlink(repoPath, systemPath);
135+
136+
return {
137+
success: true,
138+
message: `Linked shared agents configs to ${contractHome(systemPath)}`,
139+
linkedTo: repoPath,
140+
};
141+
}
142+
}
143+
144+
export const agentsAdapter = new AgentsAdapter();

src/adapters/amp.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import {
1414
removeDir,
1515
} from "../utils/fs";
1616
import { contractHome } from "../utils/paths";
17+
import {
18+
getSharedSkillsPath,
19+
getSharedSkillsRepoPath,
20+
linkSharedSkillsInRepo,
21+
} from "./shared-skills";
1722
import type {
1823
AgentAdapter,
1924
ExportResult,
@@ -34,6 +39,10 @@ export class AmpAdapter implements AgentAdapter {
3439
return join(process.env.HOME || "", ".config/amp");
3540
}
3641

42+
getSkillsPath(_platform: Platform): string {
43+
return getSharedSkillsPath();
44+
}
45+
3746
getRepoPath(repoRoot: string): string {
3847
return join(repoRoot, "configs", "amp");
3948
}
@@ -130,6 +139,8 @@ export class AmpAdapter implements AgentAdapter {
130139
// Create symlink
131140
createSymlink(repoPath, systemPath);
132141

142+
linkSharedSkillsInRepo(repoPath, getSharedSkillsRepoPath(repoPath));
143+
133144
return {
134145
success: true,
135146
message: `Linked Amp configs to ${contractHome(systemPath)}`,

src/adapters/claude.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
isSymlink,
1313
} from "../utils/fs";
1414
import { contractHome } from "../utils/paths";
15+
import { getSharedSkillsPath, linkSharedSkillsOnSystem } from "./shared-skills";
1516
import type {
1617
AgentAdapter,
1718
CanonicalSkill,
@@ -30,7 +31,7 @@ export class ClaudeAdapter implements AgentAdapter {
3031
};
3132

3233
// Files/folders to sync (exclude cache, history, etc.)
33-
private syncPatterns = ["settings.json", "CLAUDE.md", "commands", "skills"];
34+
private syncPatterns = ["settings.json", "CLAUDE.md", "commands"];
3435

3536
getConfigPath(platform: Platform): string {
3637
if (platform === "windows") {
@@ -39,8 +40,8 @@ export class ClaudeAdapter implements AgentAdapter {
3940
return join(process.env.HOME || "", ".claude");
4041
}
4142

42-
getSkillsPath(platform: Platform): string {
43-
return join(this.getConfigPath(platform), "skills");
43+
getSkillsPath(_platform: Platform): string {
44+
return getSharedSkillsPath();
4445
}
4546

4647
getRepoPath(repoRoot: string): string {
@@ -107,7 +108,7 @@ export class ClaudeAdapter implements AgentAdapter {
107108
return {
108109
success: true,
109110
message:
110-
"No Claude configs found to import (settings.json, CLAUDE.md, commands/, skills/)",
111+
"No Claude configs found to import (settings.json, CLAUDE.md, commands/)",
111112
};
112113
}
113114

@@ -150,6 +151,8 @@ export class ClaudeAdapter implements AgentAdapter {
150151
}
151152
}
152153

154+
linkSharedSkillsOnSystem(join(systemPath, "skills"));
155+
153156
return {
154157
success: true,
155158
message: `Copied Claude configs to ${contractHome(systemPath)}`,

src/adapters/codex.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import {
1414
removeDir,
1515
} from "../utils/fs";
1616
import { contractHome } from "../utils/paths";
17+
import {
18+
getSharedSkillsPath,
19+
getSharedSkillsRepoPath,
20+
linkSharedSkillsInRepo,
21+
} from "./shared-skills";
1722
import type {
1823
AgentAdapter,
1924
ExportResult,
@@ -34,6 +39,10 @@ export class CodexAdapter implements AgentAdapter {
3439
return join(process.env.HOME || "", ".codex");
3540
}
3641

42+
getSkillsPath(_platform: Platform): string {
43+
return getSharedSkillsPath();
44+
}
45+
3746
getRepoPath(repoRoot: string): string {
3847
return join(repoRoot, "configs", "codex");
3948
}
@@ -130,6 +139,8 @@ export class CodexAdapter implements AgentAdapter {
130139
// Create symlink
131140
createSymlink(repoPath, systemPath);
132141

142+
linkSharedSkillsInRepo(repoPath, getSharedSkillsRepoPath(repoPath));
143+
133144
return {
134145
success: true,
135146
message: `Linked Codex configs to ${contractHome(systemPath)}`,

src/adapters/cursor.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
removeDir,
2222
} from "../utils/fs";
2323
import { contractHome } from "../utils/paths";
24+
import { getSharedSkillsPath, linkSharedSkillsOnSystem } from "./shared-skills";
2425
import type {
2526
AgentAdapter,
2627
CanonicalSkill,
@@ -63,6 +64,10 @@ export class CursorAdapter implements AgentAdapter {
6364
}
6465
}
6566

67+
getSkillsPath(_platform: Platform): string {
68+
return getSharedSkillsPath();
69+
}
70+
6671
getRepoPath(repoRoot: string): string {
6772
return join(repoRoot, "configs", "cursor");
6873
}
@@ -137,6 +142,8 @@ export class CursorAdapter implements AgentAdapter {
137142
};
138143
}
139144

145+
linkSharedSkillsOnSystem(join(systemPath, "skills"));
146+
140147
return {
141148
success: true,
142149
message: "Imported Cursor configs to repo",

0 commit comments

Comments
 (0)