skill-linker 3.0.8 → 4.0.4

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.
@@ -1,347 +1,203 @@
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 {
10
+ getAllAgents,
11
+ detectInstalledAgents,
12
+ findAgentIndex,
13
+ } = require("../utils/agents");
14
+ const { cloneOrUpdateRepo, pullRepo } = require("../utils/git");
7
15
 
8
16
  /**
9
- * Main install command
17
+ * Main install command (CLI mode only)
10
18
  * @param {Object} options - Command options
19
+ * @param {string} options.skill - Skill path or --from clone URL
20
+ * @param {string} [options.from] - GitHub URL to clone from
21
+ * @param {string[]} [options.agents] - Agent names to install to
22
+ * @param {string} [options.scope] - Scope: project, global, or both
23
+ * @param {boolean} [options.yes] - Auto-overwrite existing links
11
24
  */
12
25
  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
- });
26
+ let skillPaths = [];
27
+
28
+ // Handle --from flag: Clone from GitHub
29
+ if (options.from) {
30
+ console.log(chalk.blue("[INFO]"), `Cloning from ${options.from}...`);
31
+
32
+ try {
33
+ const {
34
+ skillPath: clonedPath,
35
+ targetPath,
36
+ needsUpdate,
37
+ hasSubpath,
38
+ } = await cloneOrUpdateRepo(options.from);
39
+
40
+ if (needsUpdate) {
41
+ if (options.yes) {
42
+ await pullRepo(targetPath);
43
+ console.log(chalk.green("[SUCCESS]"), "Repository updated!");
44
+ } else {
45
+ console.log(
46
+ chalk.yellow("[WARNING]"),
47
+ "Repository already exists. Use --yes to update.",
48
+ );
49
+ }
50
+ }
51
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
- }
52
+ // If no subpath, check for skills/ subdirectory
53
+ if (!hasSubpath && dirExists(path.join(targetPath, "skills"))) {
54
+ const subSkills = listDirectories(path.join(targetPath, "skills"));
69
55
 
70
- console.log(chalk.green('[SUCCESS]'), 'Clone completed!');
71
- } catch (error) {
72
- console.error(chalk.red('[ERROR]'), error.message);
73
- process.exit(1);
56
+ if (subSkills.length > 0) {
57
+ // Install all skills from skills/ directory
58
+ skillPaths = subSkills.map((s) => path.join(targetPath, "skills", s));
59
+ } else {
60
+ skillPaths = [targetPath];
74
61
  }
62
+ } else {
63
+ skillPaths = [clonedPath];
64
+ }
65
+
66
+ console.log(chalk.green("[SUCCESS]"), "Clone completed!");
67
+ } catch (error) {
68
+ console.error(chalk.red("[ERROR]"), error.message);
69
+ process.exit(1);
75
70
  }
76
-
77
- // If no skill path provided, show library selection
78
- if (skillPaths.length === 0 && options.skill) {
79
- skillPaths = [options.skill];
71
+ }
72
+
73
+ // If no skill path provided via --from, use --skill
74
+ if (skillPaths.length === 0 && options.skill) {
75
+ skillPaths = [options.skill];
76
+ }
77
+
78
+ // Validate skill paths
79
+ for (const p of skillPaths) {
80
+ if (!dirExists(p)) {
81
+ console.error(chalk.red("[ERROR]"), `Skill directory not found: ${p}`);
82
+ process.exit(1);
80
83
  }
81
-
82
- if (skillPaths.length === 0) {
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' });
92
- }
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);
84
+ }
85
+
86
+ if (skillPaths.length > 1) {
87
+ console.log(chalk.blue("[INFO]"), `Selected ${skillPaths.length} skills`);
88
+ } else {
89
+ const skillName = path.basename(skillPaths[0]);
90
+ console.log(
91
+ chalk.blue("[INFO]"),
92
+ `Selected Skill: ${chalk.cyan(skillName)} (${skillPaths[0]})`,
93
+ );
94
+ }
95
+
96
+ // Agent selection
97
+ const agents = getAllAgents();
98
+ const installedIndices = detectInstalledAgents();
99
+
100
+ let selectedAgents = [];
101
+
102
+ // Use provided agents list
103
+ if (options.agents && options.agents.length > 0) {
104
+ selectedAgents = options.agents
105
+ .map((agentName) => {
106
+ const idx = findAgentIndex(agentName);
107
+ if (idx === null) {
108
+ console.log(
109
+ chalk.yellow("[WARNING]"),
110
+ `Unknown agent: ${agentName}, skipping...`,
111
+ );
112
+ return null;
104
113
  }
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
- });
201
-
202
- if (!selectedRepo) {
203
- console.log(chalk.yellow('[WARNING]'), 'No repository selected. Exiting.');
204
- process.exit(0);
205
- }
114
+ return idx;
115
+ })
116
+ .filter((idx) => idx !== null);
206
117
 
207
- // 2. Select Sub-skills (if applicable)
208
- if (selectedRepo.hasSkillsDir) {
209
- const skillsDir = path.join(selectedRepo.path, 'skills');
210
- const subSkills = listDirectories(skillsDir);
211
-
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
- });
223
-
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];
231
- }
232
- } else {
233
- skillPaths = [selectedRepo.path];
234
- }
235
- }
118
+ if (selectedAgents.length === 0) {
119
+ console.error(chalk.red("[ERROR]"), "No valid agents specified");
120
+ process.exit(1);
236
121
  }
237
-
238
- // Validate skill paths
239
- for (const p of skillPaths) {
240
- if (!dirExists(p)) {
241
- console.error(chalk.red('[ERROR]'), `Skill directory not found: ${p}`);
242
- process.exit(1);
243
- }
122
+ } else {
123
+ // Use all installed agents
124
+ selectedAgents = installedIndices;
125
+ if (selectedAgents.length === 0) {
126
+ console.error(
127
+ chalk.red("[ERROR]"),
128
+ "No installed agents detected. Please specify --agent.",
129
+ );
130
+ process.exit(1);
244
131
  }
245
-
246
- if (skillPaths.length > 1) {
247
- console.log(chalk.blue('[INFO]'), `Selected ${skillPaths.length} skills`);
248
- } else {
249
- const skillName = path.basename(skillPaths[0]);
250
- console.log(chalk.blue('[INFO]'), `Selected Skill: ${chalk.cyan(skillName)} (${skillPaths[0]})`);
132
+ }
133
+
134
+ console.log(
135
+ chalk.blue("[INFO]"),
136
+ `Installing to ${selectedAgents.length} agent(s): ${selectedAgents.map((i) => agents[i].name).join(", ")}`,
137
+ );
138
+
139
+ // Determine scope
140
+ let scope = options.scope ? options.scope.toLowerCase() : "both";
141
+ if (!["project", "global", "both"].includes(scope)) {
142
+ console.error(
143
+ chalk.red("[ERROR]"),
144
+ `Invalid scope: ${scope}. Use: project, global, or both`,
145
+ );
146
+ process.exit(1);
147
+ }
148
+ console.log(chalk.blue("[INFO]"), `Scope: ${scope}`);
149
+
150
+ // Process each selected agent
151
+ for (const agentIndex of selectedAgents) {
152
+ const agent = agents[agentIndex];
153
+
154
+ console.log("");
155
+ console.log(
156
+ chalk.blue("[INFO]"),
157
+ `Configuring for ${chalk.cyan(agent.name)}...`,
158
+ );
159
+
160
+ const targets = [];
161
+ if (scope === "project" || scope === "both") {
162
+ targets.push(path.join(process.cwd(), agent.projectDir));
251
163
  }
252
-
253
- // Agent selection
254
- const agents = getAllAgents();
255
- const installedIndices = detectInstalledAgents();
256
-
257
- const { selectedAgents } = await prompts({
258
- type: 'multiselect',
259
- name: 'selectedAgents',
260
- message: 'Select agents to install to (Space to select, Enter to confirm):',
261
- choices: agents.map((agent, index) => ({
262
- title: agent.name + (installedIndices.includes(index) ? chalk.green(' (Installed)') : ''),
263
- value: index,
264
- selected: installedIndices.includes(index)
265
- })),
266
- hint: '- Space to select. Return to submit'
267
- });
268
-
269
- if (!selectedAgents || selectedAgents.length === 0) {
270
- console.log(chalk.yellow('[WARNING]'), 'No agents selected. Exiting.');
271
- process.exit(0);
164
+ if (scope === "global" || scope === "both") {
165
+ targets.push(agent.globalDir);
272
166
  }
273
167
 
274
- // Process each selected agent
275
- for (const agentIndex of selectedAgents) {
276
- const agent = agents[agentIndex];
277
-
278
- console.log('');
279
- console.log(chalk.blue('[INFO]'), `Configuring for ${chalk.cyan(agent.name)}...`);
280
-
281
- const { scope } = await prompts({
282
- type: 'select',
283
- name: 'scope',
284
- message: 'Select scope:',
285
- choices: [
286
- { title: `Project (${agent.projectDir})`, value: 'project' },
287
- { title: `Global (${agent.globalDir})`, value: 'global' },
288
- { title: 'Both', value: 'both' },
289
- { title: 'Skip', value: 'skip' }
290
- ]
291
- });
292
-
293
- if (scope === 'skip') continue;
294
-
295
- const targets = [];
296
- if (scope === 'project' || scope === 'both') {
297
- targets.push(path.join(process.cwd(), agent.projectDir));
298
- }
299
- if (scope === 'global' || scope === 'both') {
300
- targets.push(agent.globalDir);
168
+ for (const targetBase of targets) {
169
+ ensureDir(targetBase);
170
+
171
+ // Loop through all selected skills and link them
172
+ for (const sPath of skillPaths) {
173
+ const sName = path.basename(sPath);
174
+ const targetLink = path.join(targetBase, sName);
175
+
176
+ if (dirExists(targetLink)) {
177
+ if (!options.yes) {
178
+ console.log(
179
+ chalk.yellow("[WARNING]"),
180
+ `Already exists: ${targetLink}. Use --yes to overwrite.`,
181
+ );
182
+ continue;
183
+ }
184
+ console.log(
185
+ chalk.blue("[INFO]"),
186
+ `Overwriting existing: ${targetLink}`,
187
+ );
301
188
  }
302
189
 
303
- for (const targetBase of targets) {
304
- ensureDir(targetBase);
305
-
306
- // Loop through all selected skills and link them
307
- for (const sPath of skillPaths) {
308
- const sName = path.basename(sPath);
309
- const targetLink = path.join(targetBase, sName);
310
-
311
- if (dirExists(targetLink)) {
312
- // Check if already correct link to avoid prompt
313
- // But for simplicity, we prompt or skip.
314
- // To avoid spamming prompts for multiple skills, maybe auto-overwrite or ask once?
315
- // Let's ask individually for safety for now, or maybe just log and skip if overwrite not confirmed.
316
- // Actually, prompting for every file in a loop is annoying.
317
- // Let's check overlap first? Or just try createSymlink which handles unlink.
318
-
319
- // Let's prompt once per agent/target if any conflicts? Too complex.
320
- // Simple approach: Prompt for each conflict.
321
- const { overwrite } = await prompts({
322
- type: 'confirm',
323
- name: 'overwrite',
324
- message: `${targetLink} already exists. Overwrite?`,
325
- initial: false
326
- });
327
-
328
- if (!overwrite) {
329
- console.log(chalk.blue('[INFO]'), `Skipping ${sName}...`);
330
- continue;
331
- }
332
- }
333
-
334
- if (createSymlink(sPath, targetLink)) {
335
- console.log(chalk.green('[SUCCESS]'), `Linked ${sName}`);
336
- } else {
337
- console.error(chalk.red('[ERROR]'), `Failed to link ${sName}`);
338
- }
339
- }
190
+ if (createSymlink(sPath, targetLink)) {
191
+ console.log(chalk.green("[SUCCESS]"), `Linked ${sName}`);
192
+ } else {
193
+ console.error(chalk.red("[ERROR]"), `Failed to link ${sName}`);
340
194
  }
195
+ }
341
196
  }
197
+ }
342
198
 
343
- console.log('');
344
- console.log(chalk.green('[SUCCESS]'), 'All operations completed.');
199
+ console.log("");
200
+ console.log(chalk.green("[SUCCESS]"), "All operations completed.");
345
201
  }
346
202
 
347
203
  module.exports = install;