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 CHANGED
@@ -4,13 +4,12 @@
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
  [![Node.js Version](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org/)
6
6
 
7
- 一個現代化的互動式 CLI 工具,用於將 AI Agent Skills 快速連結(Symlink)到各種 AI Agent 的專案或全域目錄中。
7
+ 一個現代化的 CLI 工具,用於將 AI Agent Skills 快速連結(Symlink)到各種 AI Agent 的專案或全域目錄中。
8
8
 
9
9
  ## ✨ 功能特色
10
10
 
11
- - **現代化 TUI 介面**:使用 `prompts` 提供流暢的互動體驗。
12
- - **模糊搜尋 (Fuzzy Search)**:在選擇 Repository 時,直接輸入文字即可即時過濾清單。
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
- # 啟動互動式安裝介面 (選擇本地或新 Clone)
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
- # 瀏覽並從庫中 (AgentSkills/) 挑選已下載的 Skill
27
+ # 列出已安裝的 Repos
28
28
  npx skill-linker list
29
- # 或使用縮寫
30
- npx skill-linker -l
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 [options] [command] [skill-path]
45
+ Usage: skill-linker [command]
52
46
 
53
- Interactive CLI to link AI Agent Skills to various agents
47
+ CLI to link AI Agent Skills to various agents
54
48
 
55
- Arguments:
56
- skill-path 指定本地 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
- --from <github-url> 先從 GitHub Clone Skill 後再進行連結
61
- -l, --list 列出庫中可用的 Skills (可互動選擇)
62
- -h, --help 顯示說明
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
- ### 📋 瀏覽模式 (List Mode)
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
- 如果您想從之前透過 `--from` 下載過的庫 (`~/Documents/AgentSkills`) 中挑選 Skill 來安裝,請使用 `list` 子指令:
71
+ 範例:
71
72
 
72
73
  ```bash
73
- npx skill-linker list
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
- ```bash
78
- npx skill-linker -l
84
+ ### list 命令
85
+
79
86
  ```
87
+ Usage: skill-linker list [options]
80
88
 
81
- 1. **第一層**:選擇已 Clone 的 Repository (會標註是否有 `skills/` 子目錄)。
82
- 2. **第二層**:如果該 Repo 包含多個 Skills,會進階列出供您查看。
89
+ Options:
90
+ -r, --repo <name> 指定 Repository 名稱
91
+ --json JSON 輸出格式
92
+ ```
93
+
94
+ 範例:
83
95
 
84
- > 💡 **提示**:如果您已經手動 `git clone` 了某個 Skill Repo,也可以直接指定路徑安裝:
85
- > ```bash
86
- > npx skill-linker /path/to/your-cloned-repo
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** | `.claude/skills/` | `~/.claude/skills/` |
106
- | **GitHub Copilot** | `.github/skills/` | `~/.copilot/skills/` |
107
- | **Google Antigravity** | `.agent/skills/` | `~/.gemini/antigravity/skills/` |
108
- | **Cursor** | `.cursor/skills/` | `~/.cursor/skills/` |
109
- | **OpenCode** | `.opencode/skill/` | `~/.config/opencode/skill/` |
110
- | **OpenAI Codex** | `.codex/skills/` | `~/.codex/skills/` |
111
- | **Gemini CLI** | `.gemini/skills/` | `~/.gemini/skills/` |
112
- | **Windsurf** | `.windsurf/skills/` | `~/.codeium/windsurf/skills/` |
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": "3.0.7",
4
- "description": "Interactive CLI to link AI Agent Skills to various agents (Claude, Copilot, Antigravity, Cursor, etc.)",
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('commander');
4
- const chalk = require('chalk');
5
- const install = require('./commands/install');
6
- const list = require('./commands/list');
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('../package.json');
9
+ const packageJson = require("../package.json");
10
10
 
11
11
  program
12
- .name('skill-linker')
13
- .description('Interactive CLI to link AI Agent Skills to various agents')
14
- .version(packageJson.version);
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
- // Default command (install)
18
+ // Install command
17
19
  program
18
- .argument('[skill-path]', 'Path to skill directory')
19
- .option('--from <github-url>', 'Clone skill from GitHub URL first, then link')
20
- .option('-l, --list', 'List available skills in library')
21
- .action(async (skillPath, options) => {
22
- // Handle --list flag
23
- if (options.list) {
24
- await list();
25
- return;
26
- }
27
-
28
- // Run install command
29
- await install({
30
- skill: skillPath,
31
- from: options.from
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
- // Standalone list command
43
+ // List command
36
44
  program
37
- .command('list')
38
- .description('List all available skills in the library')
39
- .action(async () => {
40
- await list();
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
-
@@ -1,260 +1,201 @@
1
- const prompts = require('prompts');
2
- const chalk = require('chalk');
3
- const path = require('path');
4
- const { findSkills, findRepos, dirExists, ensureDir, createSymlink, listDirectories } = require('../utils/file-system');
5
- const { getAllAgents, detectInstalledAgents } = require('../utils/agents');
6
- const { DEFAULT_LIB_PATH, cloneOrUpdateRepo, pullRepo } = require('../utils/git');
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
- let skillPaths = [];
14
-
15
- // Handle --from flag: Clone from GitHub
16
- if (options.from) {
17
- console.log(chalk.blue('[INFO]'), `Cloning from ${options.from}...`);
18
-
19
- try {
20
- const { skillPath: clonedPath, targetPath, needsUpdate, hasSubpath } = await cloneOrUpdateRepo(options.from);
21
-
22
- if (needsUpdate) {
23
- const { shouldUpdate } = await prompts({
24
- type: 'confirm',
25
- name: 'shouldUpdate',
26
- message: `Repository already exists. Update with git pull?`,
27
- initial: false
28
- });
29
-
30
- if (shouldUpdate) {
31
- await pullRepo(targetPath);
32
- console.log(chalk.green('[SUCCESS]'), 'Repository updated!');
33
- }
34
- }
35
-
36
- // If no subpath, check for skills/ subdirectory
37
- if (!hasSubpath && dirExists(path.join(targetPath, 'skills'))) {
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
- if (selectedSkills && selectedSkills.length > 0) {
53
- skillPaths = selectedSkills;
54
- } else {
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
- console.log(chalk.green('[SUCCESS]'), 'Clone completed!');
71
- } catch (error) {
72
- console.error(chalk.red('[ERROR]'), error.message);
73
- process.exit(1);
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
- // If no skill path provided, show library selection
78
- if (skillPaths.length === 0 && options.skill) {
79
- skillPaths = [options.skill];
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
- if (skillPaths.length === 0) {
83
- if (!dirExists(DEFAULT_LIB_PATH)) {
84
- console.error(chalk.red('[ERROR]'), `Skill library not found: ${DEFAULT_LIB_PATH}`);
85
- console.log(chalk.blue('[INFO]'), 'Use --from <github_url> to clone skills first.');
86
- process.exit(1);
87
- }
88
-
89
- const repos = findRepos(DEFAULT_LIB_PATH);
90
-
91
- if (repos.length === 0) {
92
- console.error(chalk.red('[ERROR]'), `No repos found in ${DEFAULT_LIB_PATH}`);
93
- console.log(chalk.blue('[INFO]'), 'Use --from <github_url> to clone skills first.');
94
- process.exit(1);
95
- }
96
-
97
- console.log('');
98
-
99
- // 1. Select Repository
100
- const { selectedRepo } = await prompts({
101
- type: 'autocomplete',
102
- name: 'selectedRepo',
103
- message: 'Select a repository:',
104
- choices: repos.map(repo => ({
105
- title: `${repo.name}${repo.hasSkillsDir ? chalk.dim(' (has skills/)') : ''}`,
106
- value: repo
107
- })),
108
- suggest: (input, choices) => {
109
- const inputLower = input.toLowerCase();
110
- return Promise.resolve(
111
- choices.filter(choice => choice.title.toLowerCase().includes(inputLower))
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
- // 2. Select Sub-skills (if applicable)
122
- if (selectedRepo.hasSkillsDir) {
123
- const skillsDir = path.join(selectedRepo.path, 'skills');
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
- // Validate skill paths
152
- for (const p of skillPaths) {
153
- if (!dirExists(p)) {
154
- console.error(chalk.red('[ERROR]'), `Skill directory not found: ${p}`);
155
- process.exit(1);
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
- if (skillPaths.length > 1) {
160
- console.log(chalk.blue('[INFO]'), `Selected ${skillPaths.length} skills`);
161
- } else {
162
- const skillName = path.basename(skillPaths[0]);
163
- console.log(chalk.blue('[INFO]'), `Selected Skill: ${chalk.cyan(skillName)} (${skillPaths[0]})`);
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
- // Agent selection
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
- // Process each selected agent
188
- for (const agentIndex of selectedAgents) {
189
- const agent = agents[agentIndex];
190
-
191
- console.log('');
192
- console.log(chalk.blue('[INFO]'), `Configuring for ${chalk.cyan(agent.name)}...`);
193
-
194
- const { scope } = await prompts({
195
- type: 'select',
196
- name: 'scope',
197
- message: 'Select scope:',
198
- choices: [
199
- { title: `Project (${agent.projectDir})`, value: 'project' },
200
- { title: `Global (${agent.globalDir})`, value: 'global' },
201
- { title: 'Both', value: 'both' },
202
- { title: 'Skip', value: 'skip' }
203
- ]
204
- });
205
-
206
- if (scope === 'skip') continue;
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
- for (const targetBase of targets) {
217
- ensureDir(targetBase);
218
-
219
- // Loop through all selected skills and link them
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
- console.log('');
257
- console.log(chalk.green('[SUCCESS]'), 'All operations completed.');
197
+ console.log("");
198
+ console.log(chalk.green("[SUCCESS]"), "All operations completed.");
258
199
  }
259
200
 
260
201
  module.exports = install;
@@ -1,78 +1,155 @@
1
- const chalk = require('chalk');
2
- const prompts = require('prompts');
3
- const path = require('path');
4
- const { findRepos, listDirectories, dirExists } = require('../utils/file-system');
5
- const { DEFAULT_LIB_PATH } = require('../utils/git');
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, then skills within selected repo
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
- if (!dirExists(DEFAULT_LIB_PATH)) {
12
- console.error(chalk.red('[ERROR]'), `Skill library not found: ${DEFAULT_LIB_PATH}`);
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
- const repos = findRepos(DEFAULT_LIB_PATH);
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
- if (repos.length === 0) {
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
- console.log('');
26
- console.log(chalk.blue('[INFO]'), `Repositories in library (${DEFAULT_LIB_PATH}):`);
27
- console.log('');
28
-
29
- // Show repos with indication if they have skills/ directory
30
- const { selectedRepo } = await prompts({
31
- type: 'select',
32
- name: 'selectedRepo',
33
- message: 'Select a repository to view:',
34
- choices: repos.map(repo => ({
35
- title: `${repo.name}${repo.hasSkillsDir ? chalk.dim(' (has skills/)') : ''}`,
36
- value: repo,
37
- description: repo.path
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
- console.log(chalk.yellow('[INFO]'), 'No repository selected.');
43
- return;
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
- console.log('');
47
- console.log(chalk.blue('[INFO]'), `Repository: ${chalk.cyan(selectedRepo.name)}`);
48
- console.log(chalk.dim(`Path: ${selectedRepo.path}`));
49
- console.log('');
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
- // If repo has skills/ subdirectory, list the skills
52
- if (selectedRepo.hasSkillsDir) {
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
- console.log(chalk.yellow('[WARNING]'), 'No skills found in skills/ directory');
58
- return;
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('[INFO]'), 'Skills in this repository:');
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
- console.log(` ${index + 1}. ${chalk.cyan(skill)}`);
66
- console.log(` ${chalk.dim(path.join(skillsDir, skill))}`);
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
- } else {
71
- console.log(chalk.blue('[INFO]'), 'This is a single-skill repository (no skills/ subdirectory)');
72
- console.log(chalk.dim('The entire repository acts as one skill'));
73
- console.log('');
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
-