@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 +132 -0
- package/README.zh-CN.md +132 -0
- package/bin/cli.mjs +5 -0
- package/dist/cli.d.mts +4 -0
- package/dist/cli.mjs +671 -0
- package/package.json +63 -0
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
|
package/README.zh-CN.md
ADDED
|
@@ -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
package/dist/cli.d.mts
ADDED
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
|
+
}
|