skill-linker 3.0.6 → 3.0.8
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 +4 -2
- package/package.json +1 -1
- package/src/commands/install.js +141 -54
- package/src/utils/file-system.js +6 -4
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
## ✨ 功能特色
|
|
10
10
|
|
|
11
11
|
- **現代化 TUI 介面**:使用 `prompts` 提供流暢的互動體驗。
|
|
12
|
+
- **智慧來源選擇**:執行 `npx skill-linker` 時自動偵測,可選擇從本地庫或 GitHub Clone。
|
|
12
13
|
- **模糊搜尋 (Fuzzy Search)**:在選擇 Repository 時,直接輸入文字即可即時過濾清單。
|
|
13
14
|
- **智慧偵測**:自動偵測系統中已安裝的 Agent,並在選單中預設勾選。
|
|
14
15
|
- **多 Agent 支援**:支援 Claude Code, GitHub Copilot, Antigravity, Cursor, Windsurf, OpenCode, Gemini CLI 等。
|
|
@@ -21,7 +22,8 @@
|
|
|
21
22
|
### 方式 1:使用 npx (推薦)
|
|
22
23
|
|
|
23
24
|
```bash
|
|
24
|
-
# 啟動互動式安裝介面
|
|
25
|
+
# 啟動互動式安裝介面
|
|
26
|
+
# 第一步會詢問:從本地庫選擇 或 從 GitHub Clone
|
|
25
27
|
npx skill-linker
|
|
26
28
|
|
|
27
29
|
# 瀏覽並從庫中 (AgentSkills/) 挑選已下載的 Skill
|
|
@@ -29,7 +31,7 @@ npx skill-linker list
|
|
|
29
31
|
# 或使用縮寫
|
|
30
32
|
npx skill-linker -l
|
|
31
33
|
|
|
32
|
-
#
|
|
34
|
+
# 直接從 GitHub Clone 並安裝 (跳過來源選擇)
|
|
33
35
|
npx skill-linker --from https://github.com/user/my-skill
|
|
34
36
|
|
|
35
37
|
# 指定本地路徑 (如果是自己 clone 下來的指定目錄)
|
package/package.json
CHANGED
package/src/commands/install.js
CHANGED
|
@@ -80,71 +80,158 @@ async function install(options) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
if (skillPaths.length === 0) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
// First, ask user to choose source: local library or GitHub
|
|
84
|
+
const hasLocalLibrary = dirExists(DEFAULT_LIB_PATH) && findRepos(DEFAULT_LIB_PATH).length > 0;
|
|
85
|
+
|
|
86
|
+
const sourceChoices = [
|
|
87
|
+
{ title: 'Clone from GitHub', value: 'github' }
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
if (hasLocalLibrary) {
|
|
91
|
+
sourceChoices.unshift({ title: 'Select from local library', value: 'local' });
|
|
87
92
|
}
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
|
|
94
|
+
const { source } = await prompts({
|
|
95
|
+
type: 'select',
|
|
96
|
+
name: 'source',
|
|
97
|
+
message: 'Where do you want to get skills from?',
|
|
98
|
+
choices: sourceChoices
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (!source) {
|
|
102
|
+
console.log(chalk.yellow('[WARNING]'), 'No source selected. Exiting.');
|
|
103
|
+
process.exit(0);
|
|
95
104
|
}
|
|
105
|
+
|
|
106
|
+
// Handle GitHub source
|
|
107
|
+
if (source === 'github') {
|
|
108
|
+
const { githubUrl } = await prompts({
|
|
109
|
+
type: 'text',
|
|
110
|
+
name: 'githubUrl',
|
|
111
|
+
message: 'Enter GitHub URL:',
|
|
112
|
+
validate: value => value.trim() !== '' || 'Please enter a valid GitHub URL'
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (!githubUrl) {
|
|
116
|
+
console.log(chalk.yellow('[WARNING]'), 'No URL provided. Exiting.');
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Use the same logic as --from flag
|
|
121
|
+
console.log(chalk.blue('[INFO]'), `Cloning from ${githubUrl}...`);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const { skillPath: clonedPath, targetPath, needsUpdate, hasSubpath } = await cloneOrUpdateRepo(githubUrl);
|
|
125
|
+
|
|
126
|
+
if (needsUpdate) {
|
|
127
|
+
const { shouldUpdate } = await prompts({
|
|
128
|
+
type: 'confirm',
|
|
129
|
+
name: 'shouldUpdate',
|
|
130
|
+
message: `Repository already exists. Update with git pull?`,
|
|
131
|
+
initial: false
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (shouldUpdate) {
|
|
135
|
+
await pullRepo(targetPath);
|
|
136
|
+
console.log(chalk.green('[SUCCESS]'), 'Repository updated!');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// If no subpath, check for skills/ subdirectory
|
|
141
|
+
if (!hasSubpath && dirExists(path.join(targetPath, 'skills'))) {
|
|
142
|
+
const subSkills = listDirectories(path.join(targetPath, 'skills'));
|
|
143
|
+
|
|
144
|
+
if (subSkills.length > 0) {
|
|
145
|
+
const { selectedSkills } = await prompts({
|
|
146
|
+
type: 'multiselect',
|
|
147
|
+
name: 'selectedSkills',
|
|
148
|
+
message: 'This repo contains multiple skills. Select skills to install:',
|
|
149
|
+
choices: [
|
|
150
|
+
...subSkills.map(s => ({ title: s, value: path.join(targetPath, 'skills', s) })),
|
|
151
|
+
{ title: 'Link entire repo', value: targetPath }
|
|
152
|
+
],
|
|
153
|
+
hint: '- Space to select. Return to submit'
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (selectedSkills && selectedSkills.length > 0) {
|
|
157
|
+
skillPaths = selectedSkills;
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
skillPaths = [targetPath];
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
skillPaths = [clonedPath];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log(chalk.green('[SUCCESS]'), 'Clone completed!');
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error(chalk.red('[ERROR]'), error.message);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Handle local library source
|
|
174
|
+
if (source === 'local') {
|
|
175
|
+
const repos = findRepos(DEFAULT_LIB_PATH);
|
|
176
|
+
|
|
177
|
+
if (repos.length === 0) {
|
|
178
|
+
console.error(chalk.red('[ERROR]'), `No repos found in ${DEFAULT_LIB_PATH}`);
|
|
179
|
+
console.log(chalk.blue('[INFO]'), 'Use --from <github_url> to clone skills first.');
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log('');
|
|
184
|
+
|
|
185
|
+
// 1. Select Repository
|
|
186
|
+
const { selectedRepo } = await prompts({
|
|
187
|
+
type: 'autocomplete',
|
|
188
|
+
name: 'selectedRepo',
|
|
189
|
+
message: 'Select a repository:',
|
|
190
|
+
choices: repos.map(repo => ({
|
|
191
|
+
title: `${repo.name}${repo.hasSkillsDir ? chalk.dim(' (has skills/)') : ''}`,
|
|
192
|
+
value: repo
|
|
193
|
+
})),
|
|
194
|
+
suggest: (input, choices) => {
|
|
195
|
+
const inputLower = input.toLowerCase();
|
|
196
|
+
return Promise.resolve(
|
|
197
|
+
choices.filter(choice => choice.title.toLowerCase().includes(inputLower))
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
96
201
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
);
|
|
202
|
+
if (!selectedRepo) {
|
|
203
|
+
console.log(chalk.yellow('[WARNING]'), 'No repository selected. Exiting.');
|
|
204
|
+
process.exit(0);
|
|
113
205
|
}
|
|
114
|
-
});
|
|
115
206
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
207
|
+
// 2. Select Sub-skills (if applicable)
|
|
208
|
+
if (selectedRepo.hasSkillsDir) {
|
|
209
|
+
const skillsDir = path.join(selectedRepo.path, 'skills');
|
|
210
|
+
const subSkills = listDirectories(skillsDir);
|
|
120
211
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
});
|
|
212
|
+
if (subSkills.length > 0) {
|
|
213
|
+
const { selectedSubSkills } = await prompts({
|
|
214
|
+
type: 'multiselect',
|
|
215
|
+
name: 'selectedSubSkills',
|
|
216
|
+
message: `Select skills from ${chalk.cyan(selectedRepo.name)} (Space to select):`,
|
|
217
|
+
choices: [
|
|
218
|
+
...subSkills.map(s => ({ title: s, value: path.join(skillsDir, s) })),
|
|
219
|
+
{ title: 'Link entire repo', value: selectedRepo.path }
|
|
220
|
+
],
|
|
221
|
+
hint: '- Space to select. Return to submit'
|
|
222
|
+
});
|
|
137
223
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
224
|
+
if (!selectedSubSkills || selectedSubSkills.length === 0) {
|
|
225
|
+
console.log(chalk.yellow('[WARNING]'), 'No skills selected. Exiting.');
|
|
226
|
+
process.exit(0);
|
|
227
|
+
}
|
|
228
|
+
skillPaths = selectedSubSkills;
|
|
229
|
+
} else {
|
|
230
|
+
skillPaths = [selectedRepo.path];
|
|
141
231
|
}
|
|
142
|
-
skillPaths = selectedSubSkills;
|
|
143
232
|
} else {
|
|
144
233
|
skillPaths = [selectedRepo.path];
|
|
145
234
|
}
|
|
146
|
-
} else {
|
|
147
|
-
skillPaths = [selectedRepo.path];
|
|
148
235
|
}
|
|
149
236
|
}
|
|
150
237
|
|
package/src/utils/file-system.js
CHANGED
|
@@ -32,10 +32,12 @@ function ensureDir(dirPath) {
|
|
|
32
32
|
*/
|
|
33
33
|
function createSymlink(source, target) {
|
|
34
34
|
try {
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
// Ensure parent directory exists
|
|
36
|
+
ensureDir(path.dirname(target));
|
|
37
|
+
|
|
38
|
+
// Remove existing link/file/directory if present
|
|
39
|
+
// force: true makes it ignore the error if file doesn't exist
|
|
40
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
39
41
|
|
|
40
42
|
fs.symlinkSync(source, target, 'dir');
|
|
41
43
|
return true;
|