@onezhao/skill-sync 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # skill-sync
2
+
3
+ Sync skills from `.agents/skills` to agent-specific directories.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ # Clone the repository
9
+ git clone https://github.com/your-repo/skill-sync.git
10
+ cd skill-sync
11
+
12
+ # Install dependencies
13
+ pnpm install
14
+
15
+ # Run directly
16
+ node bin/cli.mjs
17
+
18
+ # Or link globally
19
+ npm link
20
+ skill-sync
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```bash
26
+ # Interactive mode
27
+ skill-sync
28
+
29
+ # Sync global skills (non-interactive)
30
+ skill-sync -g -y
31
+
32
+ # Sync to specific agents
33
+ skill-sync -a claude-code
34
+
35
+ # Sync global skills to multiple agents
36
+ skill-sync -g -a cursor windsurf -y
37
+ ```
38
+
39
+ ### Options
40
+
41
+ | Option | Description |
42
+ |--------|-------------|
43
+ | `-g, --global` | Use global skills from `~/.agents/skills` |
44
+ | `-a, --agent` | Target specific agents (e.g., `claude-code`, `cursor`, `windsurf`) |
45
+ | `-y, --yes` | Skip all confirmation prompts |
46
+ | `-h, --help` | Show help message |
47
+
48
+ ### Supported Agents
49
+
50
+ The tool supports syncing to agents with non-universal skill directories:
51
+
52
+ | Agent | `--agent` | Skills Directory |
53
+ |-------|-----------|------------------|
54
+ | Windsurf | `windsurf` | `.windsurf/skills/` |
55
+ | Trae | `trae` | `.trae/skills/` |
56
+ | Claude Code | `claude-code` | `.claude/skills/` |
57
+ | Cursor | `cursor` | `.agents/skills/` (universal) |
58
+ | Cline | `cline` | `.agents/skills/` (universal) |
59
+ | OpenCode | `opencode` | `.agents/skills/` (universal) |
60
+ | Codex | `codex` | `.agents/skills/` (universal) |
61
+
62
+ > Note: Universal agents (like Cursor, Cline, OpenCode, Codex) use `.agents/skills/` directly and don't need syncing.
63
+
64
+ ## How It Works
65
+
66
+ 1. **Scan Skills**: Reads all skills from `.agents/skills/` (project) or `~/.agents/skills/` (global)
67
+ 2. **Select Skills**: Choose which skills to sync (multi-select)
68
+ 3. **Select Agents**: Choose target agents that need skill directories
69
+ 4. **Create Symlinks**: Creates symlinks from agent-specific directories to the canonical `.agents/skills/` location
70
+
71
+ ## Examples
72
+
73
+ ### Sync to Windsurf
74
+
75
+ ```bash
76
+ # Install skills first using the skills CLI
77
+ npx skills add vercel-labs/agent-skills
78
+
79
+ # Then sync to Windsurf
80
+ skill-sync -a windsurf -y
81
+ ```
82
+
83
+ ### Sync to Multiple Agents
84
+
85
+ ```bash
86
+ skill-sync -g -a windsurf trae claude-code -y
87
+ ```
88
+
89
+ ### Interactive Mode
90
+
91
+ ```bash
92
+ skill-sync
93
+ # 1. Select scope (Project/Global)
94
+ # 2. Select skills to sync
95
+ # 3. Select target agents
96
+ # 4. Confirm
97
+ ```
98
+
99
+ ## Why Use Symlinks?
100
+
101
+ Using symlinks provides these benefits:
102
+
103
+ - **Single Source of Truth**: Skills are stored in one location (`.agents/skills/`)
104
+ - **Easy Updates**: Update skills in one place, changes reflect everywhere
105
+ - **Disk Space**: No duplicate copies of skills
106
+
107
+ ## Troubleshooting
108
+
109
+ ### "No skills found"
110
+
111
+ Make sure you've installed skills first using the `skills` CLI:
112
+
113
+ ```bash
114
+ npx skills add vercel-labs/agent-skills
115
+ ```
116
+
117
+ ### "Symlink failed" on Windows
118
+
119
+ On Windows, you may need to enable Developer Mode for symlink support, or the tool will fall back to copying files.
120
+
121
+ ### Agent not detected
122
+
123
+ The tool automatically detects installed agents. If your agent isn't detected, you can still manually specify it with the `-a` flag.
124
+
125
+ ## Related
126
+
127
+ - [skills](https://github.com/vercel-labs/skills) - The main CLI for managing agent skills
128
+ - [Agent Skills Specification](https://agentskills.io) - Specification for agent skills
129
+
130
+ ## License
131
+
132
+ MIT
@@ -0,0 +1,132 @@
1
+ # skill-sync
2
+
3
+ 将技能从 `.agents/skills` 同步到各代理的专用目录。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ # 克隆仓库
9
+ git clone https://github.com/your-repo/skill-sync.git
10
+ cd skill-sync
11
+
12
+ # 安装依赖
13
+ pnpm install
14
+
15
+ # 直接运行
16
+ node bin/cli.mjs
17
+
18
+ # 或者全局链接
19
+ npm link
20
+ skill-sync
21
+ ```
22
+
23
+ ## 使用方法
24
+
25
+ ```bash
26
+ # 交互模式
27
+ skill-sync
28
+
29
+ # 同步全局技能(非交互)
30
+ skill-sync -g -y
31
+
32
+ # 同步到指定代理
33
+ skill-sync -a claude-code
34
+
35
+ # 同步全局技能到多个代理
36
+ skill-sync -g -a cursor windsurf -y
37
+ ```
38
+
39
+ ### 选项
40
+
41
+ | 选项 | 说明 |
42
+ |--------|-------------|
43
+ | `-g, --global` | 使用 `~/.agents/skills` 中的全局技能 |
44
+ | `-a, --agent` | 目标代理(如 `claude-code`、`cursor`、`windsurf`)|
45
+ | `-y, --yes` | 跳过所有确认提示 |
46
+ | `-h, --help` | 显示帮助信息 |
47
+
48
+ ### 支持的代理
49
+
50
+ 该工具支持同步到具有非通用技能目录的代理:
51
+
52
+ | 代理 | `--agent` | 技能目录 |
53
+ |-------|-----------|------------------|
54
+ | Windsurf | `windsurf` | `.windsurf/skills/` |
55
+ | Trae | `trae` | `.trae/skills/` |
56
+ | Claude Code | `claude-code` | `.claude/skills/` |
57
+ | Cursor | `cursor` | `.agents/skills/`(通用)|
58
+ | Cline | `cline` | `.agents/skills/`(通用)|
59
+ | OpenCode | `opencode` | `.agents/skills/`(通用)|
60
+ | Codex | `codex` | `.agents/skills/`(通用)|
61
+
62
+ > 注意:通用代理(如 Cursor、Cline、OpenCode、Codex)直接使用 `.agents/skills/`,无需同步。
63
+
64
+ ## 工作原理
65
+
66
+ 1. **扫描技能**:从 `.agents/skills/`(项目)或 `~/.agents/skills/`(全局)读取所有技能
67
+ 2. **选择技能**:选择要同步的技能(多选)
68
+ 3. **选择代理**:选择需要技能目录的目标代理
69
+ 4. **创建符号链接**:从代理专用目录创建指向 `.agents/skills/` 位置的符号链接
70
+
71
+ ## 示例
72
+
73
+ ### 同步到 Windsurf
74
+
75
+ ```bash
76
+ # 首先使用 skills CLI 安装技能
77
+ npx skills add vercel-labs/agent-skills
78
+
79
+ # 然后同步到 Windsurf
80
+ skill-sync -a windsurf -y
81
+ ```
82
+
83
+ ### 同步到多个代理
84
+
85
+ ```bash
86
+ skill-sync -g -a windsurf trae claude-code -y
87
+ ```
88
+
89
+ ### 交互模式
90
+
91
+ ```bash
92
+ skill-sync
93
+ # 1. 选择范围(项目/全局)
94
+ # 2. 选择要同步的技能
95
+ # 3. 选择目标代理
96
+ # 4. 确认
97
+ ```
98
+
99
+ ## 为什么要使用符号链接?
100
+
101
+ 使用符号链接有以下优势:
102
+
103
+ - **单一数据源**:技能存储在一个位置(`.agents/skills/`)
104
+ - **轻松更新**:在一处更新技能,所有地方都会生效
105
+ - **节省磁盘空间**:无需复制技能文件
106
+
107
+ ## 故障排除
108
+
109
+ ### "未找到技能"
110
+
111
+ 请确保已使用 `skills` CLI 安装技能:
112
+
113
+ ```bash
114
+ npx skills add vercel-labs/agent-skills
115
+ ```
116
+
117
+ ### Windows 上的"符号链接失败"
118
+
119
+ 在 Windows 上,您可能需要启用开发者模式以支持符号链接,否则该工具将回退到复制文件。
120
+
121
+ ### 代理未被检测到
122
+
123
+ 该工具会自动检测已安装的代理。如果未检测到您的代理,您仍然可以使用 `-a` 标志手动指定。
124
+
125
+ ## 相关项目
126
+
127
+ - [skills](https://github.com/vercel-labs/skills) - 管理代理技能的主 CLI
128
+ - [Agent Skills Specification](https://agentskills.io) - 代理技能规范
129
+
130
+ ## 许可证
131
+
132
+ MIT
package/bin/cli.mjs ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runSkillSync } from '../dist/cli.mjs';
4
+
5
+ runSkillSync(process.argv.slice(2));
package/dist/cli.d.mts ADDED
@@ -0,0 +1,4 @@
1
+ //#region src/cli.d.ts
2
+ declare function runSkillSync(args?: string[]): Promise<void>;
3
+ //#endregion
4
+ export { runSkillSync };
package/dist/cli.mjs ADDED
@@ -0,0 +1,671 @@
1
+ import * as p from "@clack/prompts";
2
+ import pc from "picocolors";
3
+ import { existsSync, readdirSync } from "fs";
4
+ import { dirname, join, relative, resolve, sep } from "path";
5
+ import { access, lstat, mkdir, readFile, readlink, rm, symlink } from "fs/promises";
6
+ import { homedir, platform } from "os";
7
+ import matter from "gray-matter";
8
+ import { xdgConfig } from "xdg-basedir";
9
+ //#region src/skills.ts
10
+ async function parseSkillMd(skillMdPath, options) {
11
+ try {
12
+ const content = await readFile(skillMdPath, "utf-8");
13
+ const { data } = matter(content);
14
+ if (!data.name || !data.description) return null;
15
+ if (typeof data.name !== "string" || typeof data.description !== "string") return null;
16
+ return {
17
+ name: data.name,
18
+ description: data.description,
19
+ path: dirname(skillMdPath),
20
+ rawContent: content,
21
+ metadata: data.metadata
22
+ };
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+ //#endregion
28
+ //#region src/agents.ts
29
+ const home = homedir();
30
+ const configHome = xdgConfig ?? join(home, ".config");
31
+ const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
32
+ const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
33
+ function getOpenClawGlobalSkillsDir(homeDir = home, pathExists = existsSync) {
34
+ if (pathExists(join(homeDir, ".openclaw"))) return join(homeDir, ".openclaw/skills");
35
+ if (pathExists(join(homeDir, ".clawdbot"))) return join(homeDir, ".clawdbot/skills");
36
+ if (pathExists(join(homeDir, ".moltbot"))) return join(homeDir, ".moltbot/skills");
37
+ return join(homeDir, ".openclaw/skills");
38
+ }
39
+ const agents = {
40
+ amp: {
41
+ name: "amp",
42
+ displayName: "Amp",
43
+ skillsDir: ".agents/skills",
44
+ globalSkillsDir: join(configHome, "agents/skills"),
45
+ detectInstalled: async () => existsSync(join(configHome, "amp"))
46
+ },
47
+ antigravity: {
48
+ name: "antigravity",
49
+ displayName: "Antigravity",
50
+ skillsDir: ".agent/skills",
51
+ globalSkillsDir: join(home, ".gemini/antigravity/skills"),
52
+ detectInstalled: async () => existsSync(join(home, ".gemini/antigravity"))
53
+ },
54
+ augment: {
55
+ name: "augment",
56
+ displayName: "Augment",
57
+ skillsDir: ".augment/skills",
58
+ globalSkillsDir: join(home, ".augment/skills"),
59
+ detectInstalled: async () => existsSync(join(home, ".augment"))
60
+ },
61
+ "claude-code": {
62
+ name: "claude-code",
63
+ displayName: "Claude Code",
64
+ skillsDir: ".claude/skills",
65
+ globalSkillsDir: join(claudeHome, "skills"),
66
+ detectInstalled: async () => existsSync(claudeHome)
67
+ },
68
+ openclaw: {
69
+ name: "openclaw",
70
+ displayName: "OpenClaw",
71
+ skillsDir: "skills",
72
+ globalSkillsDir: getOpenClawGlobalSkillsDir(),
73
+ detectInstalled: async () => existsSync(join(home, ".openclaw")) || existsSync(join(home, ".clawdbot")) || existsSync(join(home, ".moltbot"))
74
+ },
75
+ cline: {
76
+ name: "cline",
77
+ displayName: "Cline",
78
+ skillsDir: ".agents/skills",
79
+ globalSkillsDir: join(home, ".agents", "skills"),
80
+ detectInstalled: async () => existsSync(join(home, ".cline"))
81
+ },
82
+ codebuddy: {
83
+ name: "codebuddy",
84
+ displayName: "CodeBuddy",
85
+ skillsDir: ".codebuddy/skills",
86
+ globalSkillsDir: join(home, ".codebuddy/skills"),
87
+ detectInstalled: async () => existsSync(join(process.cwd(), ".codebuddy")) || existsSync(join(home, ".codebuddy"))
88
+ },
89
+ codex: {
90
+ name: "codex",
91
+ displayName: "Codex",
92
+ skillsDir: ".agents/skills",
93
+ globalSkillsDir: join(codexHome, "skills"),
94
+ detectInstalled: async () => existsSync(codexHome) || existsSync("/etc/codex")
95
+ },
96
+ "command-code": {
97
+ name: "command-code",
98
+ displayName: "Command Code",
99
+ skillsDir: ".commandcode/skills",
100
+ globalSkillsDir: join(home, ".commandcode/skills"),
101
+ detectInstalled: async () => existsSync(join(home, ".commandcode"))
102
+ },
103
+ continue: {
104
+ name: "continue",
105
+ displayName: "Continue",
106
+ skillsDir: ".continue/skills",
107
+ globalSkillsDir: join(home, ".continue/skills"),
108
+ detectInstalled: async () => existsSync(join(process.cwd(), ".continue")) || existsSync(join(home, ".continue"))
109
+ },
110
+ cortex: {
111
+ name: "cortex",
112
+ displayName: "Cortex Code",
113
+ skillsDir: ".cortex/skills",
114
+ globalSkillsDir: join(home, ".snowflake/cortex/skills"),
115
+ detectInstalled: async () => existsSync(join(home, ".snowflake/cortex"))
116
+ },
117
+ crush: {
118
+ name: "crush",
119
+ displayName: "Crush",
120
+ skillsDir: ".crush/skills",
121
+ globalSkillsDir: join(home, ".config/crush/skills"),
122
+ detectInstalled: async () => existsSync(join(home, ".config/crush"))
123
+ },
124
+ cursor: {
125
+ name: "cursor",
126
+ displayName: "Cursor",
127
+ skillsDir: ".agents/skills",
128
+ globalSkillsDir: join(home, ".cursor/skills"),
129
+ detectInstalled: async () => existsSync(join(home, ".cursor"))
130
+ },
131
+ droid: {
132
+ name: "droid",
133
+ displayName: "Droid",
134
+ skillsDir: ".factory/skills",
135
+ globalSkillsDir: join(home, ".factory/skills"),
136
+ detectInstalled: async () => existsSync(join(home, ".factory"))
137
+ },
138
+ "gemini-cli": {
139
+ name: "gemini-cli",
140
+ displayName: "Gemini CLI",
141
+ skillsDir: ".agents/skills",
142
+ globalSkillsDir: join(home, ".gemini/skills"),
143
+ detectInstalled: async () => existsSync(join(home, ".gemini"))
144
+ },
145
+ "github-copilot": {
146
+ name: "github-copilot",
147
+ displayName: "GitHub Copilot",
148
+ skillsDir: ".agents/skills",
149
+ globalSkillsDir: join(home, ".copilot/skills"),
150
+ detectInstalled: async () => existsSync(join(home, ".copilot"))
151
+ },
152
+ goose: {
153
+ name: "goose",
154
+ displayName: "Goose",
155
+ skillsDir: ".goose/skills",
156
+ globalSkillsDir: join(configHome, "goose/skills"),
157
+ detectInstalled: async () => existsSync(join(configHome, "goose"))
158
+ },
159
+ junie: {
160
+ name: "junie",
161
+ displayName: "Junie",
162
+ skillsDir: ".junie/skills",
163
+ globalSkillsDir: join(home, ".junie/skills"),
164
+ detectInstalled: async () => existsSync(join(home, ".junie"))
165
+ },
166
+ "iflow-cli": {
167
+ name: "iflow-cli",
168
+ displayName: "iFlow CLI",
169
+ skillsDir: ".iflow/skills",
170
+ globalSkillsDir: join(home, ".iflow/skills"),
171
+ detectInstalled: async () => existsSync(join(home, ".iflow"))
172
+ },
173
+ kilo: {
174
+ name: "kilo",
175
+ displayName: "Kilo Code",
176
+ skillsDir: ".kilocode/skills",
177
+ globalSkillsDir: join(home, ".kilocode/skills"),
178
+ detectInstalled: async () => existsSync(join(home, ".kilocode"))
179
+ },
180
+ "kimi-cli": {
181
+ name: "kimi-cli",
182
+ displayName: "Kimi Code CLI",
183
+ skillsDir: ".agents/skills",
184
+ globalSkillsDir: join(home, ".config/agents/skills"),
185
+ detectInstalled: async () => existsSync(join(home, ".kimi"))
186
+ },
187
+ "kiro-cli": {
188
+ name: "kiro-cli",
189
+ displayName: "Kiro CLI",
190
+ skillsDir: ".kiro/skills",
191
+ globalSkillsDir: join(home, ".kiro/skills"),
192
+ detectInstalled: async () => existsSync(join(home, ".kiro"))
193
+ },
194
+ kode: {
195
+ name: "kode",
196
+ displayName: "Kode",
197
+ skillsDir: ".kode/skills",
198
+ globalSkillsDir: join(home, ".kode/skills"),
199
+ detectInstalled: async () => existsSync(join(home, ".kode"))
200
+ },
201
+ mcpjam: {
202
+ name: "mcpjam",
203
+ displayName: "MCPJam",
204
+ skillsDir: ".mcpjam/skills",
205
+ globalSkillsDir: join(home, ".mcpjam/skills"),
206
+ detectInstalled: async () => existsSync(join(home, ".mcpjam"))
207
+ },
208
+ "mistral-vibe": {
209
+ name: "mistral-vibe",
210
+ displayName: "Mistral Vibe",
211
+ skillsDir: ".vibe/skills",
212
+ globalSkillsDir: join(home, ".vibe/skills"),
213
+ detectInstalled: async () => existsSync(join(home, ".vibe"))
214
+ },
215
+ mux: {
216
+ name: "mux",
217
+ displayName: "Mux",
218
+ skillsDir: ".mux/skills",
219
+ globalSkillsDir: join(home, ".mux/skills"),
220
+ detectInstalled: async () => existsSync(join(home, ".mux"))
221
+ },
222
+ opencode: {
223
+ name: "opencode",
224
+ displayName: "OpenCode",
225
+ skillsDir: ".agents/skills",
226
+ globalSkillsDir: join(configHome, "opencode/skills"),
227
+ detectInstalled: async () => existsSync(join(configHome, "opencode"))
228
+ },
229
+ openhands: {
230
+ name: "openhands",
231
+ displayName: "OpenHands",
232
+ skillsDir: ".openhands/skills",
233
+ globalSkillsDir: join(home, ".openhands/skills"),
234
+ detectInstalled: async () => existsSync(join(home, ".openhands"))
235
+ },
236
+ pi: {
237
+ name: "pi",
238
+ displayName: "Pi",
239
+ skillsDir: ".pi/skills",
240
+ globalSkillsDir: join(home, ".pi/agent/skills"),
241
+ detectInstalled: async () => existsSync(join(home, ".pi/agent"))
242
+ },
243
+ qoder: {
244
+ name: "qoder",
245
+ displayName: "Qoder",
246
+ skillsDir: ".qoder/skills",
247
+ globalSkillsDir: join(home, ".qoder/skills"),
248
+ detectInstalled: async () => existsSync(join(home, ".qoder"))
249
+ },
250
+ "qwen-code": {
251
+ name: "qwen-code",
252
+ displayName: "Qwen Code",
253
+ skillsDir: ".qwen/skills",
254
+ globalSkillsDir: join(home, ".qwen/skills"),
255
+ detectInstalled: async () => existsSync(join(home, ".qwen"))
256
+ },
257
+ replit: {
258
+ name: "replit",
259
+ displayName: "Replit",
260
+ skillsDir: ".agents/skills",
261
+ globalSkillsDir: join(configHome, "agents/skills"),
262
+ showInUniversalList: false,
263
+ detectInstalled: async () => existsSync(join(process.cwd(), ".replit"))
264
+ },
265
+ roo: {
266
+ name: "roo",
267
+ displayName: "Roo Code",
268
+ skillsDir: ".roo/skills",
269
+ globalSkillsDir: join(home, ".roo/skills"),
270
+ detectInstalled: async () => existsSync(join(home, ".roo"))
271
+ },
272
+ trae: {
273
+ name: "trae",
274
+ displayName: "Trae",
275
+ skillsDir: ".trae/skills",
276
+ globalSkillsDir: join(home, ".trae/skills"),
277
+ detectInstalled: async () => existsSync(join(home, ".trae"))
278
+ },
279
+ "trae-cn": {
280
+ name: "trae-cn",
281
+ displayName: "Trae CN",
282
+ skillsDir: ".trae/skills",
283
+ globalSkillsDir: join(home, ".trae-cn/skills"),
284
+ detectInstalled: async () => existsSync(join(home, ".trae-cn"))
285
+ },
286
+ windsurf: {
287
+ name: "windsurf",
288
+ displayName: "Windsurf",
289
+ skillsDir: ".windsurf/skills",
290
+ globalSkillsDir: join(home, ".codeium/windsurf/skills"),
291
+ detectInstalled: async () => existsSync(join(home, ".codeium/windsurf"))
292
+ },
293
+ zencoder: {
294
+ name: "zencoder",
295
+ displayName: "Zencoder",
296
+ skillsDir: ".zencoder/skills",
297
+ globalSkillsDir: join(home, ".zencoder/skills"),
298
+ detectInstalled: async () => existsSync(join(home, ".zencoder"))
299
+ },
300
+ neovate: {
301
+ name: "neovate",
302
+ displayName: "Neovate",
303
+ skillsDir: ".neovate/skills",
304
+ globalSkillsDir: join(home, ".neovate/skills"),
305
+ detectInstalled: async () => existsSync(join(home, ".neovate"))
306
+ },
307
+ pochi: {
308
+ name: "pochi",
309
+ displayName: "Pochi",
310
+ skillsDir: ".pochi/skills",
311
+ globalSkillsDir: join(home, ".pochi/skills"),
312
+ detectInstalled: async () => existsSync(join(home, ".pochi"))
313
+ },
314
+ adal: {
315
+ name: "adal",
316
+ displayName: "AdaL",
317
+ skillsDir: ".adal/skills",
318
+ globalSkillsDir: join(home, ".adal/skills"),
319
+ detectInstalled: async () => existsSync(join(home, ".adal"))
320
+ },
321
+ universal: {
322
+ name: "universal",
323
+ displayName: "Universal",
324
+ skillsDir: ".agents/skills",
325
+ globalSkillsDir: join(configHome, "agents/skills"),
326
+ showInUniversalList: false,
327
+ detectInstalled: async () => false
328
+ }
329
+ };
330
+ async function detectInstalledAgents() {
331
+ return (await Promise.all(Object.entries(agents).map(async ([type, config]) => ({
332
+ type,
333
+ installed: await config.detectInstalled()
334
+ })))).filter((r) => r.installed).map((r) => r.type);
335
+ }
336
+ //#endregion
337
+ //#region src/cli.ts
338
+ const AGENTS_DIR = ".agents";
339
+ const SKILLS_SUBDIR = "skills";
340
+ function getCanonicalSkillsDir(global, cwd) {
341
+ return join(global ? homedir() : cwd || process.cwd(), AGENTS_DIR, SKILLS_SUBDIR);
342
+ }
343
+ async function getInstalledSkills(global, cwd) {
344
+ const skills = [];
345
+ const canonicalDir = getCanonicalSkillsDir(global, cwd);
346
+ if (!existsSync(canonicalDir)) return skills;
347
+ try {
348
+ const entries = readdirSync(canonicalDir, { withFileTypes: true });
349
+ for (const entry of entries) {
350
+ if (!entry.isDirectory()) continue;
351
+ const skillDir = join(canonicalDir, entry.name);
352
+ const skillMdPath = join(skillDir, "SKILL.md");
353
+ if (!existsSync(skillMdPath)) continue;
354
+ try {
355
+ const skill = await parseSkillMd(skillMdPath);
356
+ if (skill) skills.push({
357
+ name: skill.name,
358
+ description: skill.description,
359
+ path: skillDir
360
+ });
361
+ } catch {}
362
+ }
363
+ } catch {}
364
+ return skills;
365
+ }
366
+ async function getTargetAgents(global) {
367
+ const installedAgents = await detectInstalledAgents();
368
+ const cwd = process.cwd();
369
+ const targetAgents = [];
370
+ for (const [agentType, config] of Object.entries(agents)) {
371
+ if (config.skillsDir === ".agents/skills" && config.showInUniversalList !== false) continue;
372
+ if (global && !config.globalSkillsDir) continue;
373
+ const isInstalled = installedAgents.includes(agentType);
374
+ global && config.globalSkillsDir ? config.globalSkillsDir : join(cwd, config.skillsDir);
375
+ targetAgents.push({
376
+ type: agentType,
377
+ displayName: config.displayName,
378
+ skillsDir: config.skillsDir,
379
+ globalSkillsDir: config.globalSkillsDir || "",
380
+ isInstalled
381
+ });
382
+ }
383
+ return targetAgents;
384
+ }
385
+ function sanitizeName(name) {
386
+ return name.toLowerCase().replace(/[^a-z0-9._]+/g, "-").replace(/^[.\-]+|[.\-]+$/g, "").substring(0, 255) || "unnamed-skill";
387
+ }
388
+ function isPathSafe(basePath, targetPath) {
389
+ const normalizedBase = resolve(basePath);
390
+ const normalizedTarget = resolve(targetPath);
391
+ const baseWithSep = normalizedBase.endsWith(sep) ? normalizedBase : normalizedBase + sep;
392
+ return normalizedTarget.startsWith(baseWithSep) || normalizedTarget === normalizedBase;
393
+ }
394
+ async function createSymlink(target, linkPath) {
395
+ try {
396
+ const resolvedTarget = resolve(target);
397
+ resolve(linkPath);
398
+ try {
399
+ if ((await lstat(linkPath)).isSymbolicLink()) {
400
+ const existingTarget = await readlink(linkPath);
401
+ if (resolve(dirname(linkPath), existingTarget) === resolvedTarget) return true;
402
+ await rm(linkPath);
403
+ } else await rm(linkPath, { recursive: true });
404
+ } catch {}
405
+ const linkDir = dirname(linkPath);
406
+ await mkdir(linkDir, { recursive: true });
407
+ await symlink(relative(linkDir, target), linkPath, platform() === "win32" ? "junction" : void 0);
408
+ return true;
409
+ } catch {
410
+ return false;
411
+ }
412
+ }
413
+ async function linkSkillToAgent(skill, agent, global) {
414
+ const cwd = process.cwd();
415
+ const baseDir = global ? homedir() : cwd;
416
+ const agentSkillsDir = global && agent.globalSkillsDir ? agent.globalSkillsDir : join(baseDir, agent.skillsDir);
417
+ const linkPath = join(agentSkillsDir, sanitizeName(skill.name));
418
+ if (!isPathSafe(agentSkillsDir, linkPath)) return {
419
+ success: false,
420
+ skipped: false,
421
+ error: "Invalid path - potential path traversal detected"
422
+ };
423
+ try {
424
+ await access(linkPath);
425
+ const stats = await lstat(linkPath);
426
+ if (stats.isSymbolicLink() || stats.isDirectory()) {
427
+ const existingTarget = await readlink(linkPath).catch(() => null);
428
+ if (existingTarget) {
429
+ if (resolve(dirname(linkPath), existingTarget) === resolve(skill.path)) return {
430
+ success: true,
431
+ skipped: true
432
+ };
433
+ } else if (stats.isDirectory()) {
434
+ const resolvedSkill = resolve(skill.path);
435
+ const canonicalBase = resolve(join(process.cwd(), ".agents", "skills"));
436
+ if (resolvedSkill.startsWith(canonicalBase) || resolvedSkill === canonicalBase) return {
437
+ success: true,
438
+ skipped: true
439
+ };
440
+ }
441
+ return {
442
+ success: false,
443
+ skipped: false,
444
+ error: "Path already exists with different content"
445
+ };
446
+ }
447
+ return {
448
+ success: false,
449
+ skipped: false,
450
+ error: "Path already exists"
451
+ };
452
+ } catch {}
453
+ try {
454
+ await mkdir(agentSkillsDir, { recursive: true });
455
+ if (await createSymlink(skill.path, linkPath)) return {
456
+ success: true,
457
+ skipped: false
458
+ };
459
+ else return {
460
+ success: false,
461
+ skipped: false,
462
+ error: "Failed to create symlink"
463
+ };
464
+ } catch (error) {
465
+ return {
466
+ success: false,
467
+ skipped: false,
468
+ error: error instanceof Error ? error.message : "Unknown error"
469
+ };
470
+ }
471
+ }
472
+ function parseSyncOptions(args) {
473
+ const options = {};
474
+ for (let i = 0; i < args.length; i++) {
475
+ const arg = args[i];
476
+ if (arg === "-g" || arg === "--global") options.global = true;
477
+ else if (arg === "-y" || arg === "--yes") options.yes = true;
478
+ else if (arg === "-a" || arg === "--agent") {
479
+ options.agent = options.agent || [];
480
+ i++;
481
+ while (i < args.length && !args[i].startsWith("-")) {
482
+ options.agent.push(args[i]);
483
+ i++;
484
+ }
485
+ i--;
486
+ } else if (arg === "--help" || arg === "-h") {
487
+ showSyncHelp();
488
+ process.exit(0);
489
+ }
490
+ }
491
+ return options;
492
+ }
493
+ function showSyncHelp() {
494
+ console.log(`
495
+ ${pc.bold("Usage:")} skill-sync [options]
496
+
497
+ ${pc.bold("Description:")}
498
+ Link skills from .agents/skills to agent-specific directories.
499
+
500
+ ${pc.bold("Options:")}
501
+ -g, --global Use global skills (~/.agents/skills)
502
+ -a, --agent <agents> Target specific agents (e.g., claude-code, cursor)
503
+ -y, --yes Skip all confirmation prompts
504
+ -h, --help Show this help message
505
+
506
+ ${pc.bold("Examples:")}
507
+ ${pc.dim("$")} skill-sync # Interactive mode
508
+ ${pc.dim("$")} skill-sync -g -y # Sync global skills
509
+ ${pc.dim("$")} skill-sync -a claude-code # Sync to specific agent
510
+ ${pc.dim("$")} skill-sync -g -a cursor windsurf # Sync global to multiple agents
511
+ `);
512
+ }
513
+ async function runSkillSync(args = []) {
514
+ const options = parseSyncOptions(args);
515
+ console.log();
516
+ p.intro(pc.bgCyan(pc.black(" skill-sync ")));
517
+ let installGlobally = options.global ?? false;
518
+ if (!options.yes && !options.global) {
519
+ const scopeChoice = await p.select({
520
+ message: "Installation scope",
521
+ options: [{
522
+ value: false,
523
+ label: "Project",
524
+ hint: "Skills in current directory"
525
+ }, {
526
+ value: true,
527
+ label: "Global",
528
+ hint: "Skills in home directory (~)"
529
+ }]
530
+ });
531
+ if (p.isCancel(scopeChoice)) {
532
+ p.cancel("Cancelled");
533
+ process.exit(0);
534
+ }
535
+ installGlobally = scopeChoice;
536
+ }
537
+ const cwd = process.cwd();
538
+ const spinner = p.spinner();
539
+ spinner.start("Scanning skills...");
540
+ const skills = await getInstalledSkills(installGlobally, cwd);
541
+ if (skills.length === 0) {
542
+ spinner.stop(pc.red("No skills found"));
543
+ const canonicalDir = getCanonicalSkillsDir(installGlobally, cwd);
544
+ p.outro(pc.red(`No skills found in ${canonicalDir}\nInstall skills first with: npx skills add <package>`));
545
+ process.exit(1);
546
+ }
547
+ spinner.stop(`Found ${pc.green(skills.length)} skill${skills.length !== 1 ? "s" : ""}`);
548
+ const skillChoices = skills.map((s) => ({
549
+ value: s,
550
+ label: s.name,
551
+ hint: s.description.length > 50 ? s.description.slice(0, 47) + "..." : s.description
552
+ }));
553
+ let selectedSkills;
554
+ if (options.yes) {
555
+ selectedSkills = skills;
556
+ p.log.info(`Selected all ${skills.length} skills`);
557
+ } else {
558
+ selectedSkills = await p.multiselect({
559
+ message: "Select skills to sync",
560
+ options: skillChoices,
561
+ required: true
562
+ });
563
+ if (p.isCancel(selectedSkills)) {
564
+ p.cancel("Cancelled");
565
+ process.exit(0);
566
+ }
567
+ }
568
+ if (selectedSkills.length === 0) {
569
+ p.cancel("No skills selected");
570
+ process.exit(0);
571
+ }
572
+ const targetAgents = await getTargetAgents(installGlobally);
573
+ if (targetAgents.length === 0) {
574
+ p.log.warn("No target agents available");
575
+ process.exit(0);
576
+ }
577
+ let filteredAgents = targetAgents;
578
+ if (options.agent && options.agent.length > 0) {
579
+ filteredAgents = targetAgents.filter((a) => {
580
+ return options.agent.some((name) => a.type === name || a.displayName.toLowerCase() === name.toLowerCase());
581
+ });
582
+ if (filteredAgents.length === 0) {
583
+ p.log.error(`No matching agents found for: ${options.agent.join(", ")}`);
584
+ p.log.info(`Available agents: ${targetAgents.map((a) => a.type).join(", ")}`);
585
+ process.exit(1);
586
+ }
587
+ p.log.info(`Selected agents: ${filteredAgents.map((a) => a.displayName).join(", ")}`);
588
+ }
589
+ let agentsToLink;
590
+ if (options.agent && options.agent.length > 0) agentsToLink = filteredAgents;
591
+ else if (options.yes) {
592
+ agentsToLink = filteredAgents;
593
+ p.log.info(`Selected all ${filteredAgents.length} available agents`);
594
+ } else {
595
+ const agentChoices = filteredAgents.map((a) => ({
596
+ value: a,
597
+ label: a.displayName,
598
+ hint: a.isInstalled ? `${a.skillsDir} ${pc.green("(installed)")}` : `${a.skillsDir} ${pc.yellow("(not detected)")}`
599
+ }));
600
+ const selectedAgents = await p.multiselect({
601
+ message: "Select target agents",
602
+ options: agentChoices,
603
+ required: true
604
+ });
605
+ if (p.isCancel(selectedAgents)) {
606
+ p.cancel("Cancelled");
607
+ process.exit(0);
608
+ }
609
+ agentsToLink = selectedAgents;
610
+ if (agentsToLink.length === 0) {
611
+ p.cancel("No agents selected");
612
+ process.exit(0);
613
+ }
614
+ }
615
+ console.log();
616
+ const summaryLines = [];
617
+ summaryLines.push(`${pc.cyan("Skills to sync:")}`);
618
+ for (const skill of selectedSkills) summaryLines.push(` ${pc.green("•")} ${skill.name}`);
619
+ summaryLines.push("");
620
+ summaryLines.push(`${pc.cyan("Target agents:")}`);
621
+ for (const agent of agentsToLink) summaryLines.push(` ${pc.green("→")} ${agent.displayName} (${agent.skillsDir})`);
622
+ p.note(summaryLines.join("\n"), "Summary");
623
+ if (!options.yes) {
624
+ const confirmed = await p.confirm({ message: "Proceed with syncing?" });
625
+ if (p.isCancel(confirmed) || !confirmed) {
626
+ p.cancel("Cancelled");
627
+ process.exit(0);
628
+ }
629
+ }
630
+ spinner.start("Linking skills...");
631
+ const results = [];
632
+ for (const skill of selectedSkills) for (const agent of agentsToLink) {
633
+ const result = await linkSkillToAgent(skill, agent, installGlobally);
634
+ results.push({
635
+ skill: skill.name,
636
+ agent: agent.displayName,
637
+ ...result
638
+ });
639
+ }
640
+ spinner.stop("Linking complete");
641
+ console.log();
642
+ const successful = results.filter((r) => r.success);
643
+ const skipped = results.filter((r) => r.skipped);
644
+ const failed = results.filter((r) => !r.success && !r.skipped);
645
+ if (successful.length > 0 || skipped.length > 0) {
646
+ const resultLines = [];
647
+ const bySkill = /* @__PURE__ */ new Map();
648
+ for (const r of [...successful, ...skipped]) {
649
+ const existing = bySkill.get(r.skill) || [];
650
+ existing.push(r);
651
+ bySkill.set(r.skill, existing);
652
+ }
653
+ for (const [skillName, skillResults] of bySkill) {
654
+ resultLines.push(`${pc.green("✓")} ${skillName}`);
655
+ for (const r of skillResults) if (r.skipped) resultLines.push(` ${pc.dim("→")} ${r.agent} ${pc.yellow("(skipped - already linked)")}`);
656
+ else resultLines.push(` ${pc.dim("→")} ${r.agent}`);
657
+ }
658
+ const title = pc.green(`Synced ${successful.length} skill${successful.length !== 1 ? "s" : ""}`);
659
+ if (skipped.length > 0) p.note(resultLines.join("\n"), `${title} ${pc.dim(`(${skipped.length} skipped)`)}`);
660
+ else p.note(resultLines.join("\n"), title);
661
+ }
662
+ if (failed.length > 0) {
663
+ console.log();
664
+ p.log.error(pc.red(`Failed to link ${failed.length} item(s):`));
665
+ for (const r of failed) p.log.message(` ${pc.red("✗")} ${r.skill} → ${r.agent}: ${pc.dim(r.error || "Unknown error")}`);
666
+ }
667
+ console.log();
668
+ p.outro(pc.green("Done!"));
669
+ }
670
+ //#endregion
671
+ export { runSkillSync };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@onezhao/skill-sync",
3
+ "version": "1.0.0",
4
+ "description": "Sync skills from .agents/skills to agent-specific directories",
5
+ "type": "module",
6
+ "bin": {
7
+ "skill-sync": "./bin/cli.mjs"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "bin"
12
+ ],
13
+ "scripts": {
14
+ "build": "obuild",
15
+ "dev": "node --import tsx src/cli.ts",
16
+ "format": "prettier --write 'src/**/*.ts'",
17
+ "format:check": "prettier --check 'src/**/*.ts'",
18
+ "type-check": "tsc --noEmit",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "keywords": [
22
+ "cli",
23
+ "skill-sync",
24
+ "agent-skills",
25
+ "skills",
26
+ "ai-agents",
27
+ "claude-code",
28
+ "cursor",
29
+ "windsurf",
30
+ "trae",
31
+ "sync"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/your-repo/skill-sync.git"
36
+ },
37
+ "homepage": "https://github.com/your-repo/skill-sync#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/your-repo/skill-sync/issues"
40
+ },
41
+ "author": "Your Name <your@email.com>",
42
+ "license": "MIT",
43
+ "dependencies": {
44
+ "@clack/prompts": "^0.11.0",
45
+ "gray-matter": "^4.0.3",
46
+ "picocolors": "^1.1.1",
47
+ "xdg-basedir": "^5.1.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^22.10.0",
51
+ "jiti": "^2.6.1",
52
+ "obuild": "^0.4.22",
53
+ "prettier": "^3.8.1",
54
+ "tsx": "^4.19.0",
55
+ "typescript": "^5.9.3"
56
+ },
57
+ "engines": {
58
+ "node": ">=18"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ }
63
+ }