skill-linker 3.0.7 → 4.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 +74 -50
- package/package.json +3 -4
- package/src/cli.js +42 -30
- package/src/commands/install.js +176 -235
- package/src/commands/list.js +131 -54
package/README.md
CHANGED
|
@@ -4,13 +4,12 @@
|
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](https://nodejs.org/)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
一個現代化的 CLI 工具,用於將 AI Agent Skills 快速連結(Symlink)到各種 AI Agent 的專案或全域目錄中。
|
|
8
8
|
|
|
9
9
|
## ✨ 功能特色
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
- **智慧偵測**:自動偵測系統中已安裝的 Agent,並在選單中預設勾選。
|
|
11
|
+
- **CLI 優先設計**:專為 AI Agent 打造的命令列介面,無需互動問答。
|
|
12
|
+
- **自動化流程**:支援自動 Clone、安裝、覆寫。
|
|
14
13
|
- **多 Agent 支援**:支援 Claude Code, GitHub Copilot, Antigravity, Cursor, Windsurf, OpenCode, Gemini CLI 等。
|
|
15
14
|
- **雙重範圍 (Scope)**:可選擇安裝到當前 `專案目錄 (Project)` 或 `全域目錄 (Global)`。
|
|
16
15
|
- **自動 Clone**:支援從 GitHub Clone 並自動處理 Multi-skill Repos。
|
|
@@ -21,19 +20,14 @@
|
|
|
21
20
|
### 方式 1:使用 npx (推薦)
|
|
22
21
|
|
|
23
22
|
```bash
|
|
24
|
-
#
|
|
25
|
-
npx skill-linker
|
|
23
|
+
# 安裝技能(需要 --skill 或 --from)
|
|
24
|
+
npx /app/workspace/projects/skill-linker install --skill <路徑> --agent opencode --scope both --yes
|
|
25
|
+
npx skill-linker install --from https://github.com/anthropics/skills --agent claude --scope both
|
|
26
26
|
|
|
27
|
-
#
|
|
27
|
+
# 列出已安裝的 Repos
|
|
28
28
|
npx skill-linker list
|
|
29
|
-
|
|
30
|
-
npx skill-linker -
|
|
31
|
-
|
|
32
|
-
# 從 GitHub Clone 並安裝
|
|
33
|
-
npx skill-linker --from https://github.com/user/my-skill
|
|
34
|
-
|
|
35
|
-
# 指定本地路徑 (如果是自己 clone 下來的指定目錄)
|
|
36
|
-
npx skill-linker /path/to/my-skill
|
|
29
|
+
npx skill-linker list --repo skill-name
|
|
30
|
+
npx skill-linker list --repo skill-name --json
|
|
37
31
|
```
|
|
38
32
|
|
|
39
33
|
### 方式 2:本地開發/安裝
|
|
@@ -48,43 +42,67 @@ npm link # 之後可直接使用 skill-linker 指令
|
|
|
48
42
|
## 🛠️ 命令說明
|
|
49
43
|
|
|
50
44
|
```
|
|
51
|
-
Usage: skill-linker [
|
|
45
|
+
Usage: skill-linker [command]
|
|
52
46
|
|
|
53
|
-
|
|
47
|
+
CLI to link AI Agent Skills to various agents
|
|
54
48
|
|
|
55
|
-
|
|
56
|
-
skill
|
|
49
|
+
Commands:
|
|
50
|
+
install Install a skill to specified agents
|
|
51
|
+
list List available skills in library
|
|
57
52
|
|
|
58
53
|
Options:
|
|
59
|
-
-V, --version
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
-V, --version 顯示版本號
|
|
55
|
+
-h, --help 顯示說明
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### install 命令
|
|
63
59
|
|
|
64
|
-
Commands:
|
|
65
|
-
list 列出庫中所有可用的 Repos 與其 Skills
|
|
66
60
|
```
|
|
61
|
+
Usage: skill-linker install --skill <path>
|
|
67
62
|
|
|
68
|
-
|
|
63
|
+
Options:
|
|
64
|
+
--skill <path> 指定本地 Skill 目錄路徑(必需)
|
|
65
|
+
--from <github-url> 從 GitHub Clone 後再進行連結
|
|
66
|
+
-a, --agent <names> 指定 Agent 名稱(opencode, claude, cursor 等)
|
|
67
|
+
-s, --scope <scope> 範圍:project, global, both(預設 both)
|
|
68
|
+
-y, --yes 自動覆寫已存在的連結
|
|
69
|
+
```
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
範例:
|
|
71
72
|
|
|
72
73
|
```bash
|
|
73
|
-
|
|
74
|
+
# 指定路徑安裝到 opencode
|
|
75
|
+
npx skill-linker install --skill /path/to/skill --agent opencode
|
|
76
|
+
|
|
77
|
+
# 從 GitHub Clone 並安裝到多個 Agents
|
|
78
|
+
npx skill-linker install --from https://github.com/anthropics/skills --agent claude cursor --scope both
|
|
79
|
+
|
|
80
|
+
# 安裝到所有已偵測到的 Agents
|
|
81
|
+
npx skill-linker install --skill /path/to/skill --scope both --yes
|
|
74
82
|
```
|
|
75
83
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
npx skill-linker -l
|
|
84
|
+
### list 命令
|
|
85
|
+
|
|
79
86
|
```
|
|
87
|
+
Usage: skill-linker list [options]
|
|
80
88
|
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
Options:
|
|
90
|
+
-r, --repo <name> 指定 Repository 名稱
|
|
91
|
+
--json JSON 輸出格式
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
範例:
|
|
83
95
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
```bash
|
|
97
|
+
# 列出所有 Repos
|
|
98
|
+
npx skill-linker list
|
|
99
|
+
|
|
100
|
+
# 列出特定 Repo 的 Skills
|
|
101
|
+
npx skill-linker list --repo skill-name
|
|
102
|
+
|
|
103
|
+
# JSON 輸出
|
|
104
|
+
npx skill-linker list --repo skill-name --json
|
|
105
|
+
```
|
|
88
106
|
|
|
89
107
|
## 📂 Skill Library 管理
|
|
90
108
|
|
|
@@ -100,35 +118,41 @@ npx skill-linker -l
|
|
|
100
118
|
|
|
101
119
|
## 🛠️ 支援的 Agent 與路徑
|
|
102
120
|
|
|
103
|
-
| 平台 / 工具
|
|
104
|
-
|
|
105
|
-
| **Claude Code**
|
|
106
|
-
| **GitHub Copilot**
|
|
107
|
-
| **Google Antigravity** | `.agent/skills/`
|
|
108
|
-
| **Cursor**
|
|
109
|
-
| **OpenCode**
|
|
110
|
-
| **OpenAI Codex**
|
|
111
|
-
| **Gemini CLI**
|
|
112
|
-
| **Windsurf**
|
|
121
|
+
| 平台 / 工具 | 專案目錄 | 全域目錄 |
|
|
122
|
+
| ---------------------- | ------------------- | ------------------------------- |
|
|
123
|
+
| **Claude Code** | `.claude/skills/` | `~/.claude/skills/` |
|
|
124
|
+
| **GitHub Copilot** | `.github/skills/` | `~/.copilot/skills/` |
|
|
125
|
+
| **Google Antigravity** | `.agent/skills/` | `~/.gemini/antigravity/skills/` |
|
|
126
|
+
| **Cursor** | `.cursor/skills/` | `~/.cursor/skills/` |
|
|
127
|
+
| **OpenCode** | `.opencode/skill/` | `~/.config/opencode/skill/` |
|
|
128
|
+
| **OpenAI Codex** | `.codex/skills/` | `~/.codex/skills/` |
|
|
129
|
+
| **Gemini CLI** | `.gemini/skills/` | `~/.gemini/skills/` |
|
|
130
|
+
| **Windsurf** | `.windsurf/skills/` | `~/.codeium/windsurf/skills/` |
|
|
113
131
|
|
|
114
132
|
## 📦 推薦的 Public Skill Repos
|
|
115
133
|
|
|
116
134
|
### Claude 官方 Skills (pdf, docx, pptx, xlsx...)
|
|
135
|
+
|
|
117
136
|
[anthropics/skills](https://github.com/anthropics/skills)
|
|
137
|
+
|
|
118
138
|
```bash
|
|
119
|
-
npx skill-linker --from https://github.com/anthropics/skills
|
|
139
|
+
npx skill-linker install --from https://github.com/anthropics/skills --agent claude
|
|
120
140
|
```
|
|
121
141
|
|
|
122
142
|
### moltbot 的 AI Agent Skills (來自 clawdhub.com)
|
|
143
|
+
|
|
123
144
|
[moltbot/skills](https://github.com/moltbot/skills)
|
|
145
|
+
|
|
124
146
|
```bash
|
|
125
|
-
npx skill-linker --from https://github.com/moltbot/skills
|
|
147
|
+
npx skill-linker install --from https://github.com/moltbot/skills --agent opencode
|
|
126
148
|
```
|
|
127
149
|
|
|
128
150
|
### 精選的 AI Skills 工具箱
|
|
151
|
+
|
|
129
152
|
[obra/superpowers](https://github.com/obra/superpowers)
|
|
153
|
+
|
|
130
154
|
```bash
|
|
131
|
-
npx skill-linker --from https://github.com/obra/superpowers
|
|
155
|
+
npx skill-linker install --from https://github.com/obra/superpowers --agent claude cursor
|
|
132
156
|
```
|
|
133
157
|
|
|
134
158
|
## ⚠️ 注意事項
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skill-linker",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "CLI to link AI Agent Skills to various agents (Claude, Copilot, Antigravity, Cursor, etc.)",
|
|
5
5
|
"main": "bin/cli.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"skill-linker": "bin/cli.js"
|
|
@@ -28,8 +28,7 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"chalk": "^4.1.2",
|
|
30
30
|
"commander": "^12.0.0",
|
|
31
|
-
"execa": "^5.1.1"
|
|
32
|
-
"prompts": "^2.4.2"
|
|
31
|
+
"execa": "^5.1.1"
|
|
33
32
|
},
|
|
34
33
|
"repository": {
|
|
35
34
|
"type": "git",
|
package/src/cli.js
CHANGED
|
@@ -1,45 +1,57 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { program } = require(
|
|
4
|
-
const chalk = require(
|
|
5
|
-
const install = require(
|
|
6
|
-
const list = require(
|
|
3
|
+
const { program } = require("commander");
|
|
4
|
+
const chalk = require("chalk");
|
|
5
|
+
const install = require("./commands/install");
|
|
6
|
+
const list = require("./commands/list");
|
|
7
7
|
|
|
8
8
|
// Package info
|
|
9
|
-
const packageJson = require(
|
|
9
|
+
const packageJson = require("../package.json");
|
|
10
10
|
|
|
11
11
|
program
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
.name("skill-linker")
|
|
13
|
+
.description(
|
|
14
|
+
"CLI to link AI Agent Skills to various agents (Claude, Copilot, Antigravity, Cursor, etc.)",
|
|
15
|
+
)
|
|
16
|
+
.version(packageJson.version);
|
|
15
17
|
|
|
16
|
-
//
|
|
18
|
+
// Install command
|
|
17
19
|
program
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
20
|
+
.command("install")
|
|
21
|
+
.description("Install a skill to specified agents")
|
|
22
|
+
.requiredOption(
|
|
23
|
+
"--skill <path>",
|
|
24
|
+
"Path to skill directory or --from clone URL",
|
|
25
|
+
)
|
|
26
|
+
.option("--from <github-url>", "Clone skill from GitHub URL first, then link")
|
|
27
|
+
.option(
|
|
28
|
+
"-a, --agent <names...>",
|
|
29
|
+
"Agent names to install to (opencode, claude, cursor, etc.)",
|
|
30
|
+
)
|
|
31
|
+
.option("-s, --scope <scope>", "Scope: project, global, or both")
|
|
32
|
+
.option("-y, --yes", "Skip confirmation prompts")
|
|
33
|
+
.action(async (options) => {
|
|
34
|
+
await install({
|
|
35
|
+
skill: options.skill,
|
|
36
|
+
from: options.from,
|
|
37
|
+
agents: options.agent,
|
|
38
|
+
scope: options.scope,
|
|
39
|
+
yes: options.yes || false,
|
|
33
40
|
});
|
|
41
|
+
});
|
|
34
42
|
|
|
35
|
-
//
|
|
43
|
+
// List command
|
|
36
44
|
program
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
.command("list")
|
|
46
|
+
.description("List available skills in library")
|
|
47
|
+
.option("-r, --repo <name>", "Repository name to list skills from")
|
|
48
|
+
.option("--json", "Output as JSON")
|
|
49
|
+
.action(async (options) => {
|
|
50
|
+
await list({
|
|
51
|
+
repo: options.repo,
|
|
52
|
+
json: options.json || false,
|
|
41
53
|
});
|
|
54
|
+
});
|
|
42
55
|
|
|
43
56
|
// Parse command line arguments
|
|
44
57
|
program.parse(process.argv);
|
|
45
|
-
|
package/src/commands/install.js
CHANGED
|
@@ -1,260 +1,201 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
const chalk = require("chalk");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const {
|
|
4
|
+
dirExists,
|
|
5
|
+
ensureDir,
|
|
6
|
+
createSymlink,
|
|
7
|
+
listDirectories,
|
|
8
|
+
} = require("../utils/file-system");
|
|
9
|
+
const { getAllAgents, detectInstalledAgents } = require("../utils/agents");
|
|
10
|
+
const { cloneOrUpdateRepo, pullRepo } = require("../utils/git");
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
|
-
* Main install command
|
|
13
|
+
* Main install command (CLI mode only)
|
|
10
14
|
* @param {Object} options - Command options
|
|
15
|
+
* @param {string} options.skill - Skill path or --from clone URL
|
|
16
|
+
* @param {string} [options.from] - GitHub URL to clone from
|
|
17
|
+
* @param {string[]} [options.agents] - Agent names to install to
|
|
18
|
+
* @param {string} [options.scope] - Scope: project, global, or both
|
|
19
|
+
* @param {boolean} [options.yes] - Auto-overwrite existing links
|
|
11
20
|
*/
|
|
12
21
|
async function install(options) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const subSkills = listDirectories(path.join(targetPath, 'skills'));
|
|
39
|
-
|
|
40
|
-
if (subSkills.length > 0) {
|
|
41
|
-
const { selectedSkills } = await prompts({
|
|
42
|
-
type: 'multiselect',
|
|
43
|
-
name: 'selectedSkills',
|
|
44
|
-
message: 'This repo contains multiple skills. Select skills to install:',
|
|
45
|
-
choices: [
|
|
46
|
-
...subSkills.map(s => ({ title: s, value: path.join(targetPath, 'skills', s) })),
|
|
47
|
-
{ title: 'Link entire repo', value: targetPath }
|
|
48
|
-
],
|
|
49
|
-
hint: '- Space to select. Return to submit'
|
|
50
|
-
});
|
|
22
|
+
let skillPaths = [];
|
|
23
|
+
|
|
24
|
+
// Handle --from flag: Clone from GitHub
|
|
25
|
+
if (options.from) {
|
|
26
|
+
console.log(chalk.blue("[INFO]"), `Cloning from ${options.from}...`);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const {
|
|
30
|
+
skillPath: clonedPath,
|
|
31
|
+
targetPath,
|
|
32
|
+
needsUpdate,
|
|
33
|
+
hasSubpath,
|
|
34
|
+
} = await cloneOrUpdateRepo(options.from);
|
|
35
|
+
|
|
36
|
+
if (needsUpdate) {
|
|
37
|
+
if (options.yes) {
|
|
38
|
+
await pullRepo(targetPath);
|
|
39
|
+
console.log(chalk.green("[SUCCESS]"), "Repository updated!");
|
|
40
|
+
} else {
|
|
41
|
+
console.log(
|
|
42
|
+
chalk.yellow("[WARNING]"),
|
|
43
|
+
"Repository already exists. Use --yes to update.",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
51
47
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// If nothing selected, maybe they just hit enter without selection? Default to repo path?
|
|
56
|
-
// Or better, error out if multiselect returns empty.
|
|
57
|
-
// Let's assume empty selection means exit or user made mistake.
|
|
58
|
-
// But to be safe, if they chose "Link entire repo" in multiselect (which is weird), handle it.
|
|
59
|
-
// Actually multiselect is better for picking subsets.
|
|
60
|
-
// If empty, let's fall back to entire repo (or maybe error).
|
|
61
|
-
// Let's error to be consistent with agent selection.
|
|
62
|
-
}
|
|
63
|
-
} else {
|
|
64
|
-
skillPaths = [targetPath];
|
|
65
|
-
}
|
|
66
|
-
} else {
|
|
67
|
-
skillPaths = [clonedPath];
|
|
68
|
-
}
|
|
48
|
+
// If no subpath, check for skills/ subdirectory
|
|
49
|
+
if (!hasSubpath && dirExists(path.join(targetPath, "skills"))) {
|
|
50
|
+
const subSkills = listDirectories(path.join(targetPath, "skills"));
|
|
69
51
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
52
|
+
if (subSkills.length > 0) {
|
|
53
|
+
// Install all skills from skills/ directory
|
|
54
|
+
skillPaths = subSkills.map((s) => path.join(targetPath, "skills", s));
|
|
55
|
+
} else {
|
|
56
|
+
skillPaths = [targetPath];
|
|
74
57
|
}
|
|
58
|
+
} else {
|
|
59
|
+
skillPaths = [clonedPath];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log(chalk.green("[SUCCESS]"), "Clone completed!");
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(chalk.red("[ERROR]"), error.message);
|
|
65
|
+
process.exit(1);
|
|
75
66
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// If no skill path provided via --from, use --skill
|
|
70
|
+
if (skillPaths.length === 0 && options.skill) {
|
|
71
|
+
skillPaths = [options.skill];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Validate skill paths
|
|
75
|
+
for (const p of skillPaths) {
|
|
76
|
+
if (!dirExists(p)) {
|
|
77
|
+
console.error(chalk.red("[ERROR]"), `Skill directory not found: ${p}`);
|
|
78
|
+
process.exit(1);
|
|
80
79
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
if (!selectedRepo) {
|
|
117
|
-
console.log(chalk.yellow('[WARNING]'), 'No repository selected. Exiting.');
|
|
118
|
-
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (skillPaths.length > 1) {
|
|
83
|
+
console.log(chalk.blue("[INFO]"), `Selected ${skillPaths.length} skills`);
|
|
84
|
+
} else {
|
|
85
|
+
const skillName = path.basename(skillPaths[0]);
|
|
86
|
+
console.log(
|
|
87
|
+
chalk.blue("[INFO]"),
|
|
88
|
+
`Selected Skill: ${chalk.cyan(skillName)} (${skillPaths[0]})`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Agent selection
|
|
93
|
+
const agents = getAllAgents();
|
|
94
|
+
const installedIndices = detectInstalledAgents();
|
|
95
|
+
|
|
96
|
+
let selectedAgents = [];
|
|
97
|
+
|
|
98
|
+
// Use provided agents list
|
|
99
|
+
if (options.agents && options.agents.length > 0) {
|
|
100
|
+
selectedAgents = options.agents
|
|
101
|
+
.map((agentName) => {
|
|
102
|
+
const idx = agents.findIndex(
|
|
103
|
+
(a) => a.name.toLowerCase() === agentName.toLowerCase(),
|
|
104
|
+
);
|
|
105
|
+
if (idx === -1) {
|
|
106
|
+
console.log(
|
|
107
|
+
chalk.yellow("[WARNING]"),
|
|
108
|
+
`Unknown agent: ${agentName}, skipping...`,
|
|
109
|
+
);
|
|
110
|
+
return null;
|
|
119
111
|
}
|
|
112
|
+
return idx;
|
|
113
|
+
})
|
|
114
|
+
.filter((idx) => idx !== null);
|
|
120
115
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const subSkills = listDirectories(skillsDir);
|
|
125
|
-
|
|
126
|
-
if (subSkills.length > 0) {
|
|
127
|
-
const { selectedSubSkills } = await prompts({
|
|
128
|
-
type: 'multiselect',
|
|
129
|
-
name: 'selectedSubSkills',
|
|
130
|
-
message: `Select skills from ${chalk.cyan(selectedRepo.name)} (Space to select):`,
|
|
131
|
-
choices: [
|
|
132
|
-
...subSkills.map(s => ({ title: s, value: path.join(skillsDir, s) })),
|
|
133
|
-
{ title: 'Link entire repo', value: selectedRepo.path }
|
|
134
|
-
],
|
|
135
|
-
hint: '- Space to select. Return to submit'
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
if (!selectedSubSkills || selectedSubSkills.length === 0) {
|
|
139
|
-
console.log(chalk.yellow('[WARNING]'), 'No skills selected. Exiting.');
|
|
140
|
-
process.exit(0);
|
|
141
|
-
}
|
|
142
|
-
skillPaths = selectedSubSkills;
|
|
143
|
-
} else {
|
|
144
|
-
skillPaths = [selectedRepo.path];
|
|
145
|
-
}
|
|
146
|
-
} else {
|
|
147
|
-
skillPaths = [selectedRepo.path];
|
|
148
|
-
}
|
|
116
|
+
if (selectedAgents.length === 0) {
|
|
117
|
+
console.error(chalk.red("[ERROR]"), "No valid agents specified");
|
|
118
|
+
process.exit(1);
|
|
149
119
|
}
|
|
150
|
-
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
120
|
+
} else {
|
|
121
|
+
// Use all installed agents
|
|
122
|
+
selectedAgents = installedIndices;
|
|
123
|
+
if (selectedAgents.length === 0) {
|
|
124
|
+
console.error(
|
|
125
|
+
chalk.red("[ERROR]"),
|
|
126
|
+
"No installed agents detected. Please specify --agent.",
|
|
127
|
+
);
|
|
128
|
+
process.exit(1);
|
|
157
129
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(
|
|
133
|
+
chalk.blue("[INFO]"),
|
|
134
|
+
`Installing to ${selectedAgents.length} agent(s): ${selectedAgents.map((i) => agents[i].name).join(", ")}`,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Determine scope
|
|
138
|
+
let scope = options.scope ? options.scope.toLowerCase() : "both";
|
|
139
|
+
if (!["project", "global", "both"].includes(scope)) {
|
|
140
|
+
console.error(
|
|
141
|
+
chalk.red("[ERROR]"),
|
|
142
|
+
`Invalid scope: ${scope}. Use: project, global, or both`,
|
|
143
|
+
);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
console.log(chalk.blue("[INFO]"), `Scope: ${scope}`);
|
|
147
|
+
|
|
148
|
+
// Process each selected agent
|
|
149
|
+
for (const agentIndex of selectedAgents) {
|
|
150
|
+
const agent = agents[agentIndex];
|
|
151
|
+
|
|
152
|
+
console.log("");
|
|
153
|
+
console.log(
|
|
154
|
+
chalk.blue("[INFO]"),
|
|
155
|
+
`Configuring for ${chalk.cyan(agent.name)}...`,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const targets = [];
|
|
159
|
+
if (scope === "project" || scope === "both") {
|
|
160
|
+
targets.push(path.join(process.cwd(), agent.projectDir));
|
|
164
161
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const agents = getAllAgents();
|
|
168
|
-
const installedIndices = detectInstalledAgents();
|
|
169
|
-
|
|
170
|
-
const { selectedAgents } = await prompts({
|
|
171
|
-
type: 'multiselect',
|
|
172
|
-
name: 'selectedAgents',
|
|
173
|
-
message: 'Select agents to install to (Space to select, Enter to confirm):',
|
|
174
|
-
choices: agents.map((agent, index) => ({
|
|
175
|
-
title: agent.name + (installedIndices.includes(index) ? chalk.green(' (Installed)') : ''),
|
|
176
|
-
value: index,
|
|
177
|
-
selected: installedIndices.includes(index)
|
|
178
|
-
})),
|
|
179
|
-
hint: '- Space to select. Return to submit'
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
if (!selectedAgents || selectedAgents.length === 0) {
|
|
183
|
-
console.log(chalk.yellow('[WARNING]'), 'No agents selected. Exiting.');
|
|
184
|
-
process.exit(0);
|
|
162
|
+
if (scope === "global" || scope === "both") {
|
|
163
|
+
targets.push(agent.globalDir);
|
|
185
164
|
}
|
|
186
165
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const targets = [];
|
|
209
|
-
if (scope === 'project' || scope === 'both') {
|
|
210
|
-
targets.push(path.join(process.cwd(), agent.projectDir));
|
|
211
|
-
}
|
|
212
|
-
if (scope === 'global' || scope === 'both') {
|
|
213
|
-
targets.push(agent.globalDir);
|
|
166
|
+
for (const targetBase of targets) {
|
|
167
|
+
ensureDir(targetBase);
|
|
168
|
+
|
|
169
|
+
// Loop through all selected skills and link them
|
|
170
|
+
for (const sPath of skillPaths) {
|
|
171
|
+
const sName = path.basename(sPath);
|
|
172
|
+
const targetLink = path.join(targetBase, sName);
|
|
173
|
+
|
|
174
|
+
if (dirExists(targetLink)) {
|
|
175
|
+
if (!options.yes) {
|
|
176
|
+
console.log(
|
|
177
|
+
chalk.yellow("[WARNING]"),
|
|
178
|
+
`Already exists: ${targetLink}. Use --yes to overwrite.`,
|
|
179
|
+
);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
console.log(
|
|
183
|
+
chalk.blue("[INFO]"),
|
|
184
|
+
`Overwriting existing: ${targetLink}`,
|
|
185
|
+
);
|
|
214
186
|
}
|
|
215
187
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
for (const sPath of skillPaths) {
|
|
221
|
-
const sName = path.basename(sPath);
|
|
222
|
-
const targetLink = path.join(targetBase, sName);
|
|
223
|
-
|
|
224
|
-
if (dirExists(targetLink)) {
|
|
225
|
-
// Check if already correct link to avoid prompt
|
|
226
|
-
// But for simplicity, we prompt or skip.
|
|
227
|
-
// To avoid spamming prompts for multiple skills, maybe auto-overwrite or ask once?
|
|
228
|
-
// Let's ask individually for safety for now, or maybe just log and skip if overwrite not confirmed.
|
|
229
|
-
// Actually, prompting for every file in a loop is annoying.
|
|
230
|
-
// Let's check overlap first? Or just try createSymlink which handles unlink.
|
|
231
|
-
|
|
232
|
-
// Let's prompt once per agent/target if any conflicts? Too complex.
|
|
233
|
-
// Simple approach: Prompt for each conflict.
|
|
234
|
-
const { overwrite } = await prompts({
|
|
235
|
-
type: 'confirm',
|
|
236
|
-
name: 'overwrite',
|
|
237
|
-
message: `${targetLink} already exists. Overwrite?`,
|
|
238
|
-
initial: false
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
if (!overwrite) {
|
|
242
|
-
console.log(chalk.blue('[INFO]'), `Skipping ${sName}...`);
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (createSymlink(sPath, targetLink)) {
|
|
248
|
-
console.log(chalk.green('[SUCCESS]'), `Linked ${sName}`);
|
|
249
|
-
} else {
|
|
250
|
-
console.error(chalk.red('[ERROR]'), `Failed to link ${sName}`);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
188
|
+
if (createSymlink(sPath, targetLink)) {
|
|
189
|
+
console.log(chalk.green("[SUCCESS]"), `Linked ${sName}`);
|
|
190
|
+
} else {
|
|
191
|
+
console.error(chalk.red("[ERROR]"), `Failed to link ${sName}`);
|
|
253
192
|
}
|
|
193
|
+
}
|
|
254
194
|
}
|
|
195
|
+
}
|
|
255
196
|
|
|
256
|
-
|
|
257
|
-
|
|
197
|
+
console.log("");
|
|
198
|
+
console.log(chalk.green("[SUCCESS]"), "All operations completed.");
|
|
258
199
|
}
|
|
259
200
|
|
|
260
201
|
module.exports = install;
|
package/src/commands/list.js
CHANGED
|
@@ -1,78 +1,155 @@
|
|
|
1
|
-
const chalk = require(
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
const chalk = require("chalk");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const {
|
|
4
|
+
findRepos,
|
|
5
|
+
listDirectories,
|
|
6
|
+
dirExists,
|
|
7
|
+
} = require("../utils/file-system");
|
|
8
|
+
const { DEFAULT_LIB_PATH } = require("../utils/git");
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
|
-
* List command - shows repos
|
|
11
|
+
* List command - shows repos and skills (CLI mode only)
|
|
12
|
+
* @param {Object} options - Command options
|
|
13
|
+
* @param {string} [options.repo] - Repository name to list
|
|
14
|
+
* @param {boolean} [options.json] - Output as JSON
|
|
9
15
|
*/
|
|
10
|
-
async function list() {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
console.log(chalk.blue('[INFO]'), 'Use --from <github_url> to clone skills first.');
|
|
14
|
-
process.exit(1);
|
|
15
|
-
}
|
|
16
|
+
async function list(options = {}) {
|
|
17
|
+
const repoName = options.repo;
|
|
18
|
+
const outputJson = options.json || false;
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
if (!dirExists(DEFAULT_LIB_PATH)) {
|
|
21
|
+
console.error(
|
|
22
|
+
chalk.red("[ERROR]"),
|
|
23
|
+
`Skill library not found: ${DEFAULT_LIB_PATH}`,
|
|
24
|
+
);
|
|
25
|
+
console.log(
|
|
26
|
+
chalk.blue("[INFO]"),
|
|
27
|
+
"Use --from <github_url> to clone skills first.",
|
|
28
|
+
);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
18
31
|
|
|
19
|
-
|
|
20
|
-
console.log(chalk.yellow('[WARNING]'), `No repos found in ${DEFAULT_LIB_PATH}`);
|
|
21
|
-
console.log(chalk.blue('[INFO]'), 'Use --from <github_url> to clone skills first.');
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
32
|
+
const repos = findRepos(DEFAULT_LIB_PATH);
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
console.log(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
if (repos.length === 0) {
|
|
35
|
+
console.log(
|
|
36
|
+
chalk.yellow("[WARNING]"),
|
|
37
|
+
`No repos found in ${DEFAULT_LIB_PATH}`,
|
|
38
|
+
);
|
|
39
|
+
console.log(
|
|
40
|
+
chalk.blue("[INFO]"),
|
|
41
|
+
"Use --from <github_url> to clone skills first.",
|
|
42
|
+
);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// CLI mode: use provided repo name
|
|
47
|
+
if (repoName) {
|
|
48
|
+
const selectedRepo = repos.find(
|
|
49
|
+
(r) => r.name.toLowerCase() === repoName.toLowerCase(),
|
|
50
|
+
);
|
|
40
51
|
|
|
41
52
|
if (!selectedRepo) {
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
console.error(chalk.red("[ERROR]"), `Repository not found: ${repoName}`);
|
|
54
|
+
console.log(
|
|
55
|
+
chalk.blue("[INFO]"),
|
|
56
|
+
"Available repos:",
|
|
57
|
+
repos.map((r) => r.name).join(", "),
|
|
58
|
+
);
|
|
59
|
+
process.exit(1);
|
|
44
60
|
}
|
|
45
61
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
if (outputJson) {
|
|
63
|
+
const skills = selectedRepo.hasSkillsDir
|
|
64
|
+
? listDirectories(path.join(selectedRepo.path, "skills"))
|
|
65
|
+
: [];
|
|
66
|
+
console.log(
|
|
67
|
+
JSON.stringify(
|
|
68
|
+
{
|
|
69
|
+
name: selectedRepo.name,
|
|
70
|
+
path: selectedRepo.path,
|
|
71
|
+
hasSkillsDir: selectedRepo.hasSkillsDir,
|
|
72
|
+
skills: skills,
|
|
73
|
+
},
|
|
74
|
+
null,
|
|
75
|
+
2,
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
} else {
|
|
79
|
+
console.log("");
|
|
80
|
+
console.log(
|
|
81
|
+
chalk.blue("[INFO]"),
|
|
82
|
+
`Repository: ${chalk.cyan(selectedRepo.name)}`,
|
|
83
|
+
);
|
|
84
|
+
console.log(chalk.dim(`Path: ${selectedRepo.path}`));
|
|
85
|
+
console.log("");
|
|
50
86
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const skillsDir = path.join(selectedRepo.path, 'skills');
|
|
87
|
+
if (selectedRepo.hasSkillsDir) {
|
|
88
|
+
const skillsDir = path.join(selectedRepo.path, "skills");
|
|
54
89
|
const skills = listDirectories(skillsDir);
|
|
55
90
|
|
|
56
91
|
if (skills.length === 0) {
|
|
57
|
-
|
|
58
|
-
|
|
92
|
+
console.log(
|
|
93
|
+
chalk.yellow("[WARNING]"),
|
|
94
|
+
"No skills found in skills/ directory",
|
|
95
|
+
);
|
|
96
|
+
return;
|
|
59
97
|
}
|
|
60
98
|
|
|
61
|
-
console.log(chalk.blue(
|
|
62
|
-
console.log(
|
|
99
|
+
console.log(chalk.blue("[INFO]"), "Skills in this repository:");
|
|
100
|
+
console.log("");
|
|
63
101
|
|
|
64
102
|
skills.forEach((skill, index) => {
|
|
65
|
-
|
|
66
|
-
|
|
103
|
+
console.log(` ${index + 1}. ${chalk.cyan(skill)}`);
|
|
104
|
+
console.log(` ${chalk.dim(path.join(skillsDir, skill))}`);
|
|
67
105
|
});
|
|
68
106
|
|
|
69
|
-
console.log(
|
|
70
|
-
|
|
71
|
-
console.log(
|
|
72
|
-
|
|
73
|
-
|
|
107
|
+
console.log("");
|
|
108
|
+
} else {
|
|
109
|
+
console.log(
|
|
110
|
+
chalk.blue("[INFO]"),
|
|
111
|
+
"This is a single-skill repository (no skills/ subdirectory)",
|
|
112
|
+
);
|
|
113
|
+
console.log(chalk.dim("The entire repository acts as one skill"));
|
|
114
|
+
console.log("");
|
|
115
|
+
}
|
|
74
116
|
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// No repo specified - list all repos
|
|
121
|
+
if (outputJson) {
|
|
122
|
+
console.log(
|
|
123
|
+
JSON.stringify(
|
|
124
|
+
repos.map((repo) => ({
|
|
125
|
+
name: repo.name,
|
|
126
|
+
path: repo.path,
|
|
127
|
+
hasSkillsDir: repo.hasSkillsDir,
|
|
128
|
+
})),
|
|
129
|
+
null,
|
|
130
|
+
2,
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
} else {
|
|
134
|
+
console.log("");
|
|
135
|
+
console.log(
|
|
136
|
+
chalk.blue("[INFO]"),
|
|
137
|
+
`Repositories in library (${DEFAULT_LIB_PATH}):`,
|
|
138
|
+
);
|
|
139
|
+
console.log("");
|
|
140
|
+
|
|
141
|
+
repos.forEach((repo, index) => {
|
|
142
|
+
const hasSkills = repo.hasSkillsDir ? chalk.dim(" (has skills/)") : "";
|
|
143
|
+
console.log(` ${index + 1}. ${chalk.cyan(repo.name)}${hasSkills}`);
|
|
144
|
+
console.log(` ${chalk.dim(repo.path)}`);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
console.log("");
|
|
148
|
+
console.log(
|
|
149
|
+
chalk.blue("[INFO]"),
|
|
150
|
+
"Use --repo <name> to list skills in a specific repo.",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
75
153
|
}
|
|
76
154
|
|
|
77
155
|
module.exports = list;
|
|
78
|
-
|