skill-linker 1.1.0 → 3.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
@@ -1,22 +1,23 @@
1
- # AI Agent Skill Installer
1
+ # AI Agent Skill Installer (skill-linker)
2
2
 
3
- 一個互動式 CLI 工具,用於將 AI Agent Skills 快速連結(Symlink)到各種 AI Agent 的專案或全域目錄中。
3
+ 一個現代化的互動式 CLI 工具,用於將 AI Agent Skills 快速連結(Symlink)到各種 AI Agent 的專案或全域目錄中。
4
4
 
5
5
  ## ✨ 功能特色
6
6
 
7
- - **多 Agent 支援**:支援 Claude Code, GitHub Copilot, Antigravity, Cursor, Windsurf, OpenCode 等。
7
+ - **現代化 TUI 介面**:使用 `prompts` 提供流暢的互動體驗。
8
+ - **模糊搜尋 (Fuzzy Search)**:在選擇 Skill 時可輸入關鍵字快速過濾。
9
+ - **智慧偵測**:自動偵測系統中已安裝的 Agent,並在選單中預設勾選。
10
+ - **多 Agent 支援**:支援 Claude Code, GitHub Copilot, Antigravity, Cursor, Windsurf, OpenCode, Gemini CLI 等。
8
11
  - **雙重範圍 (Scope)**:可選擇安裝到當前 `專案目錄 (Project)` 或 `全域目錄 (Global)`。
9
- - **自動 Clone**:使用 `--from` 參數可直接從 GitHub Clone Skill
10
- - **Skill Library 支援**:自動偵測統一的 Skill 存放區。
12
+ - **自動 Clone**:支援從 GitHub Clone 並自動處理 Multi-skill Repos
13
+ - **完全相容 npx**:無需安裝,隨插即用。
11
14
 
12
15
  ## 🚀 快速開始
13
16
 
14
17
  ### 方式 1:使用 npx (推薦)
15
18
 
16
- 無需安裝,直接執行:
17
-
18
19
  ```bash
19
- # 互動式選擇本地 Skill
20
+ # 啟動互動式安裝介面
20
21
  npx skill-linker
21
22
 
22
23
  # 從 GitHub Clone 並安裝
@@ -26,63 +27,57 @@ npx skill-linker --from https://github.com/user/my-skill
26
27
  npx skill-linker /path/to/my-skill
27
28
  ```
28
29
 
29
- ### 方式 2:Clone 此專案
30
+ ### 方式 2:本地開發/安裝
30
31
 
31
32
  ```bash
32
- git clone https://github.com/user/skill-installer.git
33
- cd skill-installer
34
- ./link-skill.sh
33
+ git clone https://github.com/raybird/skill-linker.git
34
+ cd skill-linker
35
+ npm install
36
+ npm link # 之後可直接使用 skill-linker 指令
35
37
  ```
36
38
 
37
39
  ## 🛠️ 命令說明
38
40
 
39
41
  ```
40
- Usage: link-skill.sh [OPTIONS] [SKILL_PATH]
42
+ Usage: skill-linker [options] [command] [skill-path]
41
43
 
42
- Options:
43
- --from <github_url> 從 GitHub Clone Skill 後再連結
44
- --help 顯示說明
45
-
46
- Examples:
47
- ./link-skill.sh # 互動式選擇
48
- ./link-skill.sh /path/to/skill # 指定本地 Skill
49
- ./link-skill.sh --from https://github.com/user/my-skill
50
- ./link-skill.sh --from https://github.com/anthropics/skills/tree/main/skills/pdf
51
- ```
44
+ Interactive CLI to link AI Agent Skills to various agents
52
45
 
53
- ### Multi-Skill Repo 支援
46
+ Arguments:
47
+ skill-path 指定本地 Skill 目錄路徑
54
48
 
55
- 對於包含多個 Skills 的 Repo(如 `anthropics/skills`),腳本會:
56
- 1. 自動偵測 `skills/` 子目錄
57
- 2. 列出所有可用的 Skills 讓您選擇
58
- 3. 或者您可以直接在 URL 中指定子路徑(如 `/tree/main/skills/pdf`)
49
+ Options:
50
+ -V, --version 顯示版本號
51
+ --from <github-url> 從 GitHub Clone Skill 後再連結
52
+ -l, --list 列出庫中的 Skills (可互動選擇)
53
+ -h, --help 顯示說明
59
54
 
60
- ## 📦 推薦的 Public Skill Repos
55
+ Commands:
56
+ list 列出所有庫中的 Repos 與其 Skills
57
+ ```
61
58
 
62
- | Repo | 說明 |
63
- |------|------|
64
- | [anthropics/skills](https://github.com/anthropics/skills) | Claude 官方 Skills (pdf, docx, pptx, xlsx...) |
65
- | [obra/superpowers](https://github.com/obra/superpowers) | 開發流程 Skills (TDD, debugging, code-review...) |
59
+ ### 📋 瀏覽模式 (List Mode)
66
60
 
67
- ```bash
68
- # 安裝 Anthropic 的 PDF Skill
69
- npx skill-linker --from https://github.com/anthropics/skills/tree/main/skills/pdf
61
+ 使用 `list` 子指令或 `-l` 參數可以啟動兩層式的互動瀏覽:
70
62
 
71
- # 安裝 obra 的所有開發 Skills (可互動選擇)
72
- npx skill-linker --from https://github.com/obra/superpowers
63
+ ```bash
64
+ npx skill-linker list
73
65
  ```
74
66
 
75
- ## 📂 Skill Library
67
+ 1. **第一層**:選擇已 Clone 的 Repository (會標註是否有 `skills/` 子目錄)。
68
+ 2. **第二層**:如果該 Repo 包含多個 Skills,會進階列出供您查看。
76
69
 
77
- 建議將您的 Public Skills 統一存放在 `~/Documents/AgentSkills`:
70
+ ## 📂 Skill Library 管理
78
71
 
79
- ```bash
80
- mkdir -p ~/Documents/AgentSkills
81
- cd ~/Documents/AgentSkills
82
- git clone https://github.com/anthropics/skills.git
83
- ```
72
+ 當您使用 `--from` 參數時,Skills 會自動存放到 `~/Documents/AgentSkills`,並以 **owner/repo** 結構分層:
84
73
 
85
- 腳本會自動偵測此目錄並列出可用的 Skills。
74
+ ```
75
+ ~/Documents/AgentSkills/
76
+ ├── anthropics/
77
+ │ └── skills/ # https://github.com/anthropics/skills
78
+ └── your-org/
79
+ └── your-skill/ # https://github.com/your-org/your-skill
80
+ ```
86
81
 
87
82
  ## 🛠️ 支援的 Agent 與路徑
88
83
 
@@ -97,11 +92,19 @@ git clone https://github.com/anthropics/skills.git
97
92
  | **Gemini CLI** | `.gemini/skills/` | `~/.gemini/skills/` |
98
93
  | **Windsurf** | `.windsurf/skills/` | `~/.codeium/windsurf/skills/` |
99
94
 
95
+ ## 📦 推薦的 Public Skill Repos
96
+
97
+ | Repo | 說明 |
98
+ |------|------|
99
+ | [anthropics/skills](https://github.com/anthropics/skills) | Claude 官方 Skills (pdf, docx, pptx, xlsx...) |
100
+ | [moltbot/skills](https://github.com/moltbot/skills) | 各種封存的 AI Agent Skills (來自 clawdhub.com) |
101
+
100
102
  ## ⚠️ 注意事項
101
103
 
102
- 1. **Windows 使用者**:請使用 WSL 或 Git Bash 執行此工具。
103
- 2. **Git Clone First**:`--from` 參數會自動處理 clone,但如果不使用該參數,請確保 Skill 已在本地。
104
+ 1. **權限問題**:在建立 Symlink 時,請確保您有對應目錄的寫入權限。
105
+ 2. **環境需求**:需安裝 Node.js 14.0.0 以上版本。
104
106
 
105
107
  ## 授權
106
108
 
107
109
  MIT License
110
+
package/bin/cli.js CHANGED
@@ -1,48 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * skill-linker CLI
5
- * Node.js wrapper for link-skill.sh
4
+ * skill-linker CLI entry point
5
+ * Modern Node.js implementation with interactive TUI
6
6
  */
7
7
 
8
- const { spawn } = require('child_process');
9
- const path = require('path');
10
- const fs = require('fs');
11
-
12
- // Get the path to the shell script
13
- const scriptPath = path.join(__dirname, '..', 'link-skill.sh');
14
-
15
- // Check if bash is available
16
- const isWindows = process.platform === 'win32';
17
-
18
- if (isWindows) {
19
- console.error('\x1b[31m[ERROR]\x1b[0m This tool requires Bash.');
20
- console.error('On Windows, please use WSL (Windows Subsystem for Linux) or Git Bash.');
21
- console.error('');
22
- console.error('Example with WSL:');
23
- console.error(' wsl npx skill-linker');
24
- process.exit(1);
25
- }
26
-
27
- // Check if script exists
28
- if (!fs.existsSync(scriptPath)) {
29
- console.error('\x1b[31m[ERROR]\x1b[0m Shell script not found:', scriptPath);
30
- process.exit(1);
31
- }
32
-
33
- // Forward all arguments to the shell script
34
- const args = process.argv.slice(2);
35
-
36
- const child = spawn('bash', [scriptPath, ...args], {
37
- stdio: 'inherit',
38
- cwd: process.cwd()
39
- });
40
-
41
- child.on('error', (err) => {
42
- console.error('\x1b[31m[ERROR]\x1b[0m Failed to run script:', err.message);
43
- process.exit(1);
44
- });
45
-
46
- child.on('close', (code) => {
47
- process.exit(code || 0);
48
- });
8
+ require('../src/cli.js');
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "skill-linker",
3
- "version": "1.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "Interactive CLI to link AI Agent Skills to various agents (Claude, Copilot, Antigravity, Cursor, etc.)",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {
7
- "skill-linker": "./bin/cli.js"
7
+ "skill-linker": "bin/cli.js"
8
8
  },
9
9
  "scripts": {
10
10
  "test": "echo \"No tests yet\""
@@ -25,16 +25,22 @@
25
25
  ],
26
26
  "author": "",
27
27
  "license": "MIT",
28
+ "dependencies": {
29
+ "chalk": "^4.1.2",
30
+ "commander": "^12.0.0",
31
+ "execa": "^5.1.1",
32
+ "prompts": "^2.4.2"
33
+ },
28
34
  "repository": {
29
35
  "type": "git",
30
- "url": "git+https://github.com/raybird/skill-installer.git"
36
+ "url": "git+https://github.com/raybird/skill-linker.git"
31
37
  },
32
38
  "engines": {
33
39
  "node": ">=14.0.0"
34
40
  },
35
41
  "files": [
36
42
  "bin/",
37
- "link-skill.sh",
43
+ "src/",
38
44
  "README.md"
39
45
  ]
40
- }
46
+ }
package/src/cli.js ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const chalk = require('chalk');
5
+ const install = require('./commands/install');
6
+ const list = require('./commands/list');
7
+
8
+ // Package info
9
+ const packageJson = require('../package.json');
10
+
11
+ program
12
+ .name('skill-linker')
13
+ .description('Interactive CLI to link AI Agent Skills to various agents')
14
+ .version(packageJson.version);
15
+
16
+ // Default command (install)
17
+ 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
+ });
33
+ });
34
+
35
+ // Standalone list command
36
+ program
37
+ .command('list')
38
+ .description('List all available skills in the library')
39
+ .action(async () => {
40
+ await list();
41
+ });
42
+
43
+ // Parse command line arguments
44
+ program.parse(process.argv);
45
+
@@ -0,0 +1,260 @@
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');
7
+
8
+ /**
9
+ * Main install command
10
+ * @param {Object} options - Command options
11
+ */
12
+ 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
+ });
51
+
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
+ }
69
+
70
+ console.log(chalk.green('[SUCCESS]'), 'Clone completed!');
71
+ } catch (error) {
72
+ console.error(chalk.red('[ERROR]'), error.message);
73
+ process.exit(1);
74
+ }
75
+ }
76
+
77
+ // If no skill path provided, show library selection
78
+ if (skillPaths.length === 0 && options.skill) {
79
+ skillPaths = [options.skill];
80
+ }
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);
119
+ }
120
+
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
+ }
149
+ }
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
+ }
157
+ }
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]})`);
164
+ }
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);
185
+ }
186
+
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);
214
+ }
215
+
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
+ }
253
+ }
254
+ }
255
+
256
+ console.log('');
257
+ console.log(chalk.green('[SUCCESS]'), 'All operations completed.');
258
+ }
259
+
260
+ module.exports = install;
@@ -0,0 +1,78 @@
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');
6
+
7
+ /**
8
+ * List command - shows repos, then skills within selected repo
9
+ */
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
+
17
+ const repos = findRepos(DEFAULT_LIB_PATH);
18
+
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
+ }
24
+
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
+ });
40
+
41
+ if (!selectedRepo) {
42
+ console.log(chalk.yellow('[INFO]'), 'No repository selected.');
43
+ return;
44
+ }
45
+
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('');
50
+
51
+ // If repo has skills/ subdirectory, list the skills
52
+ if (selectedRepo.hasSkillsDir) {
53
+ const skillsDir = path.join(selectedRepo.path, 'skills');
54
+ const skills = listDirectories(skillsDir);
55
+
56
+ if (skills.length === 0) {
57
+ console.log(chalk.yellow('[WARNING]'), 'No skills found in skills/ directory');
58
+ return;
59
+ }
60
+
61
+ console.log(chalk.blue('[INFO]'), 'Skills in this repository:');
62
+ console.log('');
63
+
64
+ skills.forEach((skill, index) => {
65
+ console.log(` ${index + 1}. ${chalk.cyan(skill)}`);
66
+ console.log(` ${chalk.dim(path.join(skillsDir, skill))}`);
67
+ });
68
+
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('');
74
+ }
75
+ }
76
+
77
+ module.exports = list;
78
+
@@ -0,0 +1,91 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ /**
6
+ * Supported AI Agent configurations
7
+ * Format: { name, projectDir, globalDir }
8
+ */
9
+ const AGENTS = [
10
+ {
11
+ name: 'Claude Code',
12
+ projectDir: '.claude/skills',
13
+ globalDir: path.join(os.homedir(), '.claude/skills')
14
+ },
15
+ {
16
+ name: 'GitHub Copilot',
17
+ projectDir: '.github/skills',
18
+ globalDir: path.join(os.homedir(), '.copilot/skills')
19
+ },
20
+ {
21
+ name: 'Google Antigravity',
22
+ projectDir: '.agent/skills',
23
+ globalDir: path.join(os.homedir(), '.gemini/antigravity/skills')
24
+ },
25
+ {
26
+ name: 'Cursor',
27
+ projectDir: '.cursor/skills',
28
+ globalDir: path.join(os.homedir(), '.cursor/skills')
29
+ },
30
+ {
31
+ name: 'OpenCode',
32
+ projectDir: '.opencode/skill',
33
+ globalDir: path.join(os.homedir(), '.config/opencode/skill')
34
+ },
35
+ {
36
+ name: 'OpenAI Codex',
37
+ projectDir: '.codex/skills',
38
+ globalDir: path.join(os.homedir(), '.codex/skills')
39
+ },
40
+ {
41
+ name: 'Gemini CLI',
42
+ projectDir: '.gemini/skills',
43
+ globalDir: path.join(os.homedir(), '.gemini/skills')
44
+ },
45
+ {
46
+ name: 'Windsurf',
47
+ projectDir: '.windsurf/skills',
48
+ globalDir: path.join(os.homedir(), '.codeium/windsurf/skills')
49
+ }
50
+ ];
51
+
52
+ /**
53
+ * Detect which agents are installed on the system
54
+ * @returns {Array} List of detected agent indices
55
+ */
56
+ function detectInstalledAgents() {
57
+ const installed = [];
58
+
59
+ AGENTS.forEach((agent, index) => {
60
+ // Check if global directory exists
61
+ if (fs.existsSync(agent.globalDir)) {
62
+ installed.push(index);
63
+ }
64
+ });
65
+
66
+ return installed;
67
+ }
68
+
69
+ /**
70
+ * Get agent configuration by index
71
+ * @param {number} index
72
+ * @returns {Object} Agent configuration
73
+ */
74
+ function getAgent(index) {
75
+ return AGENTS[index];
76
+ }
77
+
78
+ /**
79
+ * Get all agents
80
+ * @returns {Array} All agent configurations
81
+ */
82
+ function getAllAgents() {
83
+ return AGENTS;
84
+ }
85
+
86
+ module.exports = {
87
+ AGENTS,
88
+ detectInstalledAgents,
89
+ getAgent,
90
+ getAllAgents
91
+ };
@@ -0,0 +1,166 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Check if a directory exists
6
+ * @param {string} dirPath
7
+ * @returns {boolean}
8
+ */
9
+ function dirExists(dirPath) {
10
+ try {
11
+ return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory();
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Ensure directory exists (create if not)
19
+ * @param {string} dirPath
20
+ */
21
+ function ensureDir(dirPath) {
22
+ if (!dirExists(dirPath)) {
23
+ fs.mkdirSync(dirPath, { recursive: true });
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Create a symbolic link
29
+ * @param {string} source - Source path
30
+ * @param {string} target - Target symlink path
31
+ * @returns {boolean} Success status
32
+ */
33
+ function createSymlink(source, target) {
34
+ try {
35
+ // Remove existing link/file if present
36
+ if (fs.existsSync(target) || fs.lstatSync(target).isSymbolicLink()) {
37
+ fs.unlinkSync(target);
38
+ }
39
+
40
+ fs.symlinkSync(source, target, 'dir');
41
+ return true;
42
+ } catch (error) {
43
+ console.error(`Failed to create symlink: ${error.message}`);
44
+ return false;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * List directories in a path
50
+ * @param {string} dirPath
51
+ * @returns {Array<string>} List of directory names
52
+ */
53
+ function listDirectories(dirPath) {
54
+ if (!dirExists(dirPath)) {
55
+ return [];
56
+ }
57
+
58
+ try {
59
+ return fs.readdirSync(dirPath)
60
+ .filter(item => {
61
+ const fullPath = path.join(dirPath, item);
62
+ return fs.statSync(fullPath).isDirectory();
63
+ });
64
+ } catch {
65
+ return [];
66
+ }
67
+ }
68
+
69
+
70
+ /**
71
+ * Find all repositories in library
72
+ * @param {string} libPath - Library root path
73
+ * @returns {Array<{name: string, path: string, owner: string, repo: string, hasSkillsDir: boolean}>}
74
+ */
75
+ function findRepos(libPath) {
76
+ if (!dirExists(libPath)) {
77
+ return [];
78
+ }
79
+
80
+ const repos = [];
81
+
82
+ // Scan owner directories
83
+ const owners = listDirectories(libPath);
84
+
85
+ for (const owner of owners) {
86
+ const ownerPath = path.join(libPath, owner);
87
+ const repoList = listDirectories(ownerPath);
88
+
89
+ for (const repo of repoList) {
90
+ const repoPath = path.join(ownerPath, repo);
91
+ const skillsDir = path.join(repoPath, 'skills');
92
+
93
+ repos.push({
94
+ name: `${owner}/${repo}`,
95
+ path: repoPath,
96
+ owner,
97
+ repo,
98
+ hasSkillsDir: dirExists(skillsDir)
99
+ });
100
+ }
101
+ }
102
+
103
+ return repos;
104
+ }
105
+
106
+ /**
107
+ * Find all skill directories in library
108
+ * @param {string} libPath - Library root path
109
+ * @returns {Array<{name: string, path: string, owner: string, repo: string}>}
110
+ */
111
+ function findSkills(libPath) {
112
+ if (!dirExists(libPath)) {
113
+ return [];
114
+ }
115
+
116
+ const skills = [];
117
+
118
+ // Scan owner directories
119
+ const owners = listDirectories(libPath);
120
+
121
+ for (const owner of owners) {
122
+ const ownerPath = path.join(libPath, owner);
123
+ const repos = listDirectories(ownerPath);
124
+
125
+ for (const repo of repos) {
126
+ const repoPath = path.join(ownerPath, repo);
127
+
128
+ // Check if this repo has a skills/ subdirectory
129
+ const skillsDir = path.join(repoPath, 'skills');
130
+
131
+ if (dirExists(skillsDir)) {
132
+ // Multi-skill repo
133
+ const subSkills = listDirectories(skillsDir);
134
+
135
+ for (const skill of subSkills) {
136
+ skills.push({
137
+ name: `${owner}/${repo}/${skill}`,
138
+ path: path.join(skillsDir, skill),
139
+ owner,
140
+ repo,
141
+ skill
142
+ });
143
+ }
144
+ } else {
145
+ // Single skill repo
146
+ skills.push({
147
+ name: `${owner}/${repo}`,
148
+ path: repoPath,
149
+ owner,
150
+ repo
151
+ });
152
+ }
153
+ }
154
+ }
155
+
156
+ return skills;
157
+ }
158
+
159
+ module.exports = {
160
+ dirExists,
161
+ ensureDir,
162
+ createSymlink,
163
+ listDirectories,
164
+ findRepos,
165
+ findSkills
166
+ };
@@ -0,0 +1,108 @@
1
+ const execa = require('execa');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { dirExists } = require('./file-system');
5
+
6
+ const DEFAULT_LIB_PATH = path.join(os.homedir(), 'Documents/AgentSkills');
7
+
8
+ /**
9
+ * Parse GitHub URL to extract owner, repo, branch, and subpath
10
+ * @param {string} url - GitHub URL
11
+ * @returns {Object} Parsed components
12
+ */
13
+ function parseGitHubUrl(url) {
14
+ let cleanUrl = url;
15
+ let subpath = '';
16
+ let branch = 'main';
17
+
18
+ // Check for /tree/branch/path format
19
+ const treeMatch = url.match(/(.+)\/tree\/([^/]+)\/(.+)$/);
20
+ if (treeMatch) {
21
+ cleanUrl = treeMatch[1];
22
+ branch = treeMatch[2];
23
+ subpath = treeMatch[3];
24
+ }
25
+
26
+ // Extract owner/repo
27
+ const repoMatch = cleanUrl.match(/github\.com[/:]([^/]+)\/([^/]+?)(\.git)?$/);
28
+
29
+ if (!repoMatch) {
30
+ throw new Error('Invalid GitHub URL format');
31
+ }
32
+
33
+ return {
34
+ owner: repoMatch[1],
35
+ repo: repoMatch[2].replace('.git', ''),
36
+ branch,
37
+ subpath,
38
+ cleanUrl
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Clone a GitHub repository
44
+ * @param {string} url - GitHub URL
45
+ * @param {string} targetPath - Target directory
46
+ * @returns {Promise<void>}
47
+ */
48
+ async function cloneRepo(url, targetPath) {
49
+ try {
50
+ await execa('git', ['clone', url, targetPath]);
51
+ } catch (error) {
52
+ throw new Error(`Failed to clone repository: ${error.message}`);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Pull latest changes in a repository
58
+ * @param {string} repoPath - Path to repository
59
+ * @returns {Promise<void>}
60
+ */
61
+ async function pullRepo(repoPath) {
62
+ try {
63
+ await execa('git', ['-C', repoPath, 'pull']);
64
+ } catch (error) {
65
+ throw new Error(`Failed to pull repository: ${error.message}`);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Clone or update a GitHub repository
71
+ * @param {string} url - GitHub URL
72
+ * @returns {Promise<{skillPath: string, needsUpdate: boolean}>}
73
+ */
74
+ async function cloneOrUpdateRepo(url) {
75
+ const parsed = parseGitHubUrl(url);
76
+ const targetPath = path.join(DEFAULT_LIB_PATH, parsed.owner, parsed.repo);
77
+
78
+ let needsUpdate = false;
79
+
80
+ if (dirExists(targetPath)) {
81
+ // Repo exists, ask if user wants to update
82
+ needsUpdate = true;
83
+ } else {
84
+ // Clone new repo
85
+ await cloneRepo(parsed.cleanUrl, targetPath);
86
+ }
87
+
88
+ // Determine final skill path
89
+ let skillPath = targetPath;
90
+ if (parsed.subpath) {
91
+ skillPath = path.join(targetPath, parsed.subpath);
92
+ }
93
+
94
+ return {
95
+ skillPath,
96
+ targetPath,
97
+ needsUpdate,
98
+ hasSubpath: !!parsed.subpath
99
+ };
100
+ }
101
+
102
+ module.exports = {
103
+ DEFAULT_LIB_PATH,
104
+ parseGitHubUrl,
105
+ cloneRepo,
106
+ pullRepo,
107
+ cloneOrUpdateRepo
108
+ };
package/link-skill.sh DELETED
@@ -1,316 +0,0 @@
1
- #!/bin/bash
2
-
3
- # link-skill.sh
4
- # Interactive script to link AI Agent Skills to various AI agents
5
- DEFAULT_LIB_PATH="$HOME/Documents/AgentSkills"
6
- SKILL_PATH=""
7
- FROM_URL=""
8
-
9
- # Helper function for colored output
10
- print_info() { echo -e "\033[1;34m[INFO]\033[0m $1"; }
11
- print_success() { echo -e "\033[1;32m[SUCCESS]\033[0m $1"; }
12
- print_warning() { echo -e "\033[1;33m[WARNING]\033[0m $1"; }
13
- print_error() { echo -e "\033[1;31m[ERROR]\033[0m $1"; }
14
-
15
- # Show help
16
- show_help() {
17
- echo "Usage: link-skill.sh [OPTIONS] [SKILL_PATH]"
18
- echo ""
19
- echo "Options:"
20
- echo " --from <github_url> Clone skill from GitHub URL first, then link"
21
- echo " --help Show this help message"
22
- echo ""
23
- echo "Examples:"
24
- echo " ./link-skill.sh # Interactive selection from library"
25
- echo " ./link-skill.sh /path/to/skill # Link specific local skill"
26
- echo " ./link-skill.sh --from https://github.com/user/my-skill"
27
- echo " ./link-skill.sh --from https://github.com/anthropics/skills/tree/main/skills/pdf"
28
- echo ""
29
- echo "Notes:"
30
- echo " - If the cloned repo has a 'skills/' subdirectory, you can pick a specific skill"
31
- echo " - GitHub URLs with /tree/branch/path are supported for direct subpath access"
32
- echo ""
33
- exit 0
34
- }
35
-
36
- # Parse arguments
37
- while [[ $# -gt 0 ]]; do
38
- case $1 in
39
- --from)
40
- FROM_URL="$2"
41
- shift 2
42
- ;;
43
- --help|-h)
44
- show_help
45
- ;;
46
- *)
47
- SKILL_PATH="$1"
48
- shift
49
- ;;
50
- esac
51
- done
52
-
53
- # 1. Handle --from flag: Clone from GitHub
54
- if [ -n "$FROM_URL" ]; then
55
- # Parse GitHub URL - check for /tree/branch/path format
56
- SUBPATH=""
57
- CLEAN_URL="$FROM_URL"
58
-
59
- if [[ "$FROM_URL" =~ (.+)/tree/([^/]+)/(.+)$ ]]; then
60
- # URL contains /tree/branch/path - extract subpath
61
- BASE_REPO="${BASH_REMATCH[1]}"
62
- BRANCH="${BASH_REMATCH[2]}"
63
- SUBPATH="${BASH_REMATCH[3]}"
64
- CLEAN_URL="$BASE_REPO"
65
- print_info "Detected subpath: $SUBPATH (branch: $BRANCH)"
66
- fi
67
-
68
- # Extract repo name from URL
69
- REPO_NAME=$(basename "$CLEAN_URL" .git)
70
- TARGET_CLONE_PATH="$DEFAULT_LIB_PATH/$REPO_NAME"
71
-
72
- # Ensure library directory exists
73
- mkdir -p "$DEFAULT_LIB_PATH"
74
-
75
- if [ -d "$TARGET_CLONE_PATH" ]; then
76
- print_warning "Directory already exists: $TARGET_CLONE_PATH"
77
- read -p "Update with git pull? (y/N): " do_pull
78
- if [[ "$do_pull" =~ ^[yY]$ ]]; then
79
- print_info "Pulling latest changes..."
80
- git -C "$TARGET_CLONE_PATH" pull
81
- fi
82
- else
83
- print_info "Cloning $CLEAN_URL to $TARGET_CLONE_PATH..."
84
- git clone "$CLEAN_URL" "$TARGET_CLONE_PATH"
85
- if [ $? -ne 0 ]; then
86
- print_error "Failed to clone repository"
87
- exit 1
88
- fi
89
- print_success "Clone completed!"
90
- fi
91
-
92
- # If subpath was specified in URL, use it directly
93
- if [ -n "$SUBPATH" ]; then
94
- SKILL_PATH="$TARGET_CLONE_PATH/$SUBPATH"
95
- if [ ! -d "$SKILL_PATH" ]; then
96
- print_error "Subpath not found: $SKILL_PATH"
97
- exit 1
98
- fi
99
- else
100
- # Check if this is a multi-skill repo (has skills/ subdirectory)
101
- SKILLS_DIR="$TARGET_CLONE_PATH/skills"
102
- if [ -d "$SKILLS_DIR" ]; then
103
- print_info "Detected multi-skill repository. Listing available skills..."
104
-
105
- sub_skills=("$SKILLS_DIR"/*/)
106
- if [ ${#sub_skills[@]} -eq 0 ]; then
107
- print_warning "No skills found in $SKILLS_DIR, using repo root"
108
- SKILL_PATH="$TARGET_CLONE_PATH"
109
- else
110
- # Extract skill names for display
111
- sub_skill_names=()
112
- for s in "${sub_skills[@]}"; do
113
- sub_skill_names+=("$(basename "$s")")
114
- done
115
-
116
- echo ""
117
- echo "Available Skills in this repo:"
118
- select sub_skill_name in "${sub_skill_names[@]}" "Link entire repo"; do
119
- if [ -n "$sub_skill_name" ]; then
120
- if [ "$sub_skill_name" == "Link entire repo" ]; then
121
- SKILL_PATH="$TARGET_CLONE_PATH"
122
- else
123
- SKILL_PATH="$SKILLS_DIR/$sub_skill_name"
124
- fi
125
- break
126
- else
127
- echo "Invalid selection. Please try again."
128
- fi
129
- done
130
- fi
131
- else
132
- SKILL_PATH="$TARGET_CLONE_PATH"
133
- fi
134
- fi
135
- fi
136
-
137
- # 2. Determine Source Skill Path (if not already set by --from)
138
- if [ -z "$SKILL_PATH" ]; then
139
- # Check if default library exists
140
- if [ -d "$DEFAULT_LIB_PATH" ]; then
141
- print_info "No skill path provided. Checking default library: $DEFAULT_LIB_PATH"
142
- skills=("$DEFAULT_LIB_PATH"/*/)
143
-
144
- if [ ${#skills[@]} -eq 0 ]; then
145
- print_error "No skills found in $DEFAULT_LIB_PATH"
146
- print_info "Please provide a skill path: ./link-skill.sh <path_to_skill>"
147
- exit 1
148
- fi
149
-
150
- # Extract skill names for display
151
- skill_names=()
152
- for s in "${skills[@]}"; do
153
- skill_names+=("$(basename "$s")")
154
- done
155
-
156
- echo "Available Skills:"
157
- select skill_name in "${skill_names[@]}"; do
158
- if [ -n "$skill_name" ]; then
159
- SKILL_PATH="${DEFAULT_LIB_PATH}/${skill_name}"
160
- break
161
- else
162
- echo "Invalid selection. Please try again."
163
- fi
164
- done
165
- else
166
- # Fallback to current directory prompt
167
- read -p "Enter path to skill directory (default: current dir): " input_path
168
- input_path=${input_path:-.}
169
- SKILL_PATH=$(realpath "$input_path")
170
- fi
171
- elif [ -n "$SKILL_PATH" ] && [ -z "$FROM_URL" ]; then
172
- # Path provided directly as argument
173
- SKILL_PATH=$(realpath "$SKILL_PATH")
174
- fi
175
-
176
- if [ ! -d "$SKILL_PATH" ]; then
177
- print_error "Skill directory not found: $SKILL_PATH"
178
- exit 1
179
- fi
180
-
181
- SKILL_NAME=$(basename "$SKILL_PATH")
182
- print_info "Selected Skill: \033[1;36m$SKILL_NAME\033[0m ($SKILL_PATH)"
183
-
184
- # 2. Define Supported Agents
185
- # Format: "Name:ProjectDir:GlobalDir"
186
- AGENTS=(
187
- "Claude Code:.claude/skills:$HOME/.claude/skills"
188
- "GitHub Copilot:.github/skills:$HOME/.copilot/skills"
189
- "Google Antigravity:.agent/skills:$HOME/.gemini/antigravity/skills"
190
- "Cursor:.cursor/skills:$HOME/.cursor/skills"
191
- "OpenCode:.opencode/skill:$HOME/.config/opencode/skill"
192
- "OpenAI Codex:.codex/skills:$HOME/.codex/skills"
193
- "Gemini CLI:.gemini/skills:$HOME/.gemini/skills"
194
- "Windsurf:.windsurf/skills:$HOME/.codeium/windsurf/skills"
195
- )
196
-
197
- # 3. Agent Selection
198
- echo ""
199
- echo "Select Agents to install to (Space to select, Enter to confirm):"
200
- # Simple multi-select implementation using arrays
201
- selected_indices=()
202
- while true; do
203
- for i in "${!AGENTS[@]}"; do
204
- agent_info="${AGENTS[$i]}"
205
- agent_name="${agent_info%%:*}"
206
-
207
- # Check if selected
208
- if [[ " ${selected_indices[*]} " =~ " $i " ]]; then
209
- mark="[*]"
210
- else
211
- mark="[ ]"
212
- fi
213
- echo "$i) $mark $agent_name"
214
- done
215
-
216
- echo "a) Select All"
217
- echo "d) Done"
218
- read -p "Select option: " choice
219
-
220
- if [[ "$choice" == "d" ]]; then
221
- break
222
- elif [[ "$choice" == "a" ]]; then
223
- selected_indices=()
224
- for i in "${!AGENTS[@]}"; do
225
- selected_indices+=("$i")
226
- done
227
- elif [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 0 ] && [ "$choice" -lt "${#AGENTS[@]}" ]; then
228
- if [[ " ${selected_indices[*]} " =~ " $choice " ]]; then
229
- # Deselect
230
- new_indices=()
231
- for idx in "${selected_indices[@]}"; do
232
- [[ "$idx" != "$choice" ]] && new_indices+=("$idx")
233
- done
234
- selected_indices=("${new_indices[@]}")
235
- else
236
- # Select
237
- selected_indices+=("$choice")
238
- fi
239
- else
240
- echo "Invalid choice."
241
- fi
242
- echo "------------------------"
243
- done
244
-
245
- if [ ${#selected_indices[@]} -eq 0 ]; then
246
- print_warning "No agents selected. Exiting."
247
- exit 0
248
- fi
249
-
250
- # 4. Process Each Selected Agent
251
- for idx in "${selected_indices[@]}"; do
252
- agent_raw="${AGENTS[$idx]}"
253
- IFS=':' read -r agent_name project_dir global_dir <<< "$agent_raw"
254
-
255
- echo ""
256
- print_info "Configuring for \033[1;36m$agent_name\033[0m..."
257
-
258
- # Scope Selection
259
- echo "Select Scope:"
260
- echo "1) Project ($project_dir)"
261
- echo "2) Global ($global_dir)"
262
- echo "3) Both"
263
- echo "s) Skip"
264
- read -p "Choice [1-3]: " scope_choice
265
-
266
- targets=()
267
- case $scope_choice in
268
- 1) targets+=("$project_dir") ;;
269
- 2) targets+=("$global_dir") ;;
270
- 3) targets+=("$project_dir" "$global_dir") ;;
271
- s|S) continue ;;
272
- *) print_warning "Invalid choice, skipping $agent_name"; continue ;;
273
- esac
274
-
275
- for target_base in "${targets[@]}"; do
276
- # Resolve path expansion if needed (already expanded in definition for Global, Project is relative)
277
- if [[ "$target_base" == /* ]]; then
278
- # Absolute path (Global)
279
- target_dir="$target_base"
280
- else
281
- # Relative path (Project) - assume current dir is project root
282
- target_dir="$(pwd)/$target_base"
283
- fi
284
-
285
- # Ensure target parent directory exists
286
- if [ ! -d "$target_dir" ]; then
287
- print_info "Creating directory: $target_dir"
288
- mkdir -p "$target_dir"
289
- fi
290
-
291
- target_link="$target_dir/$SKILL_NAME"
292
-
293
- # Check for existing link or directory
294
- if [ -e "$target_link" ] || [ -L "$target_link" ]; then
295
- print_warning "$target_link already exists."
296
- read -p "Overwrite? (y/N): " overwrite
297
- if [[ "$overwrite" =~ ^[yY]$ ]]; then
298
- rm -rf "$target_link"
299
- else
300
- print_info "Skipping..."
301
- continue
302
- fi
303
- fi
304
-
305
- # Create Symlink
306
- ln -s "$SKILL_PATH" "$target_link"
307
- if [ $? -eq 0 ]; then
308
- print_success "Linked to $target_link"
309
- else
310
- print_error "Failed to link to $target_link"
311
- fi
312
- done
313
- done
314
-
315
- echo ""
316
- print_success "All operations completed."