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,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
-
@@ -1,69 +1,93 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const os = require('os');
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
4
 
5
5
  /**
6
6
  * Supported AI Agent configurations
7
- * Format: { name, projectDir, globalDir }
7
+ * Format: { name, projectDir, globalDir, aliases }
8
8
  */
9
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
- }
10
+ {
11
+ name: "Claude Code",
12
+ projectDir: ".claude/skills",
13
+ globalDir: path.join(os.homedir(), ".claude/skills"),
14
+ aliases: ["claude", "claude-code", "anthropic"],
15
+ },
16
+ {
17
+ name: "GitHub Copilot",
18
+ projectDir: ".github/skills",
19
+ globalDir: path.join(os.homedir(), ".copilot/skills"),
20
+ aliases: ["copilot", "github", "gh-copilot"],
21
+ },
22
+ {
23
+ name: "Google Antigravity",
24
+ projectDir: ".agent/skills",
25
+ globalDir: path.join(os.homedir(), ".gemini/antigravity/skills"),
26
+ aliases: ["antigravity", "gemini-antigravity"],
27
+ },
28
+ {
29
+ name: "Cursor",
30
+ projectDir: ".cursor/skills",
31
+ globalDir: path.join(os.homedir(), ".cursor/skills"),
32
+ aliases: ["cursor"],
33
+ },
34
+ {
35
+ name: "OpenCode",
36
+ projectDir: ".opencode/skills",
37
+ globalDir: path.join(os.homedir(), ".config/opencode/skills"),
38
+ aliases: ["opencode", "open-code"],
39
+ },
40
+ {
41
+ name: "OpenAI Codex",
42
+ projectDir: ".codex/skills",
43
+ globalDir: path.join(os.homedir(), ".codex/skills"),
44
+ aliases: ["codex", "openai", "openai-codex"],
45
+ },
46
+ {
47
+ name: "Gemini CLI",
48
+ projectDir: ".gemini/skills",
49
+ globalDir: path.join(os.homedir(), ".gemini/skills"),
50
+ aliases: ["gemini", "gemini-cli", "google-gemini"],
51
+ },
52
+ {
53
+ name: "Windsurf",
54
+ projectDir: ".windsurf/skills",
55
+ globalDir: path.join(os.homedir(), ".codeium/windsurf/skills"),
56
+ aliases: ["windsurf", "codeium"],
57
+ },
50
58
  ];
51
59
 
60
+ /**
61
+ * Find agent index by name or alias (case-insensitive)
62
+ * @param {string} name - Agent name or alias
63
+ * @returns {number|null} Agent index or null if not found
64
+ */
65
+ function findAgentIndex(nameOrAlias) {
66
+ const lower = nameOrAlias.toLowerCase();
67
+ const idx = AGENTS.findIndex(
68
+ (agent) =>
69
+ agent.name.toLowerCase() === lower ||
70
+ (agent.aliases &&
71
+ agent.aliases.some((alias) => alias.toLowerCase() === lower)),
72
+ );
73
+ return idx !== -1 ? idx : null;
74
+ }
75
+
52
76
  /**
53
77
  * Detect which agents are installed on the system
54
78
  * @returns {Array} List of detected agent indices
55
79
  */
56
80
  function detectInstalledAgents() {
57
- const installed = [];
81
+ const installed = [];
58
82
 
59
- AGENTS.forEach((agent, index) => {
60
- // Check if global directory exists
61
- if (fs.existsSync(agent.globalDir)) {
62
- installed.push(index);
63
- }
64
- });
83
+ AGENTS.forEach((agent, index) => {
84
+ // Check if global directory exists
85
+ if (fs.existsSync(agent.globalDir)) {
86
+ installed.push(index);
87
+ }
88
+ });
65
89
 
66
- return installed;
90
+ return installed;
67
91
  }
68
92
 
69
93
  /**
@@ -72,7 +96,7 @@ function detectInstalledAgents() {
72
96
  * @returns {Object} Agent configuration
73
97
  */
74
98
  function getAgent(index) {
75
- return AGENTS[index];
99
+ return AGENTS[index];
76
100
  }
77
101
 
78
102
  /**
@@ -80,12 +104,13 @@ function getAgent(index) {
80
104
  * @returns {Array} All agent configurations
81
105
  */
82
106
  function getAllAgents() {
83
- return AGENTS;
107
+ return AGENTS;
84
108
  }
85
109
 
86
110
  module.exports = {
87
- AGENTS,
88
- detectInstalledAgents,
89
- getAgent,
90
- getAllAgents
111
+ AGENTS,
112
+ detectInstalledAgents,
113
+ getAgent,
114
+ getAllAgents,
115
+ findAgentIndex,
91
116
  };
package/src/utils/git.js CHANGED
@@ -1,9 +1,9 @@
1
- const execa = require('execa');
2
- const path = require('path');
3
- const os = require('os');
4
- const { dirExists } = require('./file-system');
1
+ const execa = require("execa");
2
+ const path = require("path");
3
+ const os = require("os");
4
+ const { dirExists } = require("./file-system");
5
5
 
6
- const DEFAULT_LIB_PATH = path.join(os.homedir(), 'Documents/AgentSkills');
6
+ const DEFAULT_LIB_PATH = path.join(os.homedir(), "Documents/AgentSkills");
7
7
 
8
8
  /**
9
9
  * Parse GitHub URL to extract owner, repo, branch, and subpath
@@ -11,46 +11,50 @@ const DEFAULT_LIB_PATH = path.join(os.homedir(), 'Documents/AgentSkills');
11
11
  * @returns {Object} Parsed components
12
12
  */
13
13
  function parseGitHubUrl(url) {
14
- let cleanUrl = url;
15
- let subpath = '';
16
- let branch = 'main';
14
+ let cleanUrl = url;
15
+ let subpath = "";
16
+ let branch = "main";
17
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
- }
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
25
 
26
- // Extract owner/repo
27
- const repoMatch = cleanUrl.match(/github\.com[/:]([^/]+)\/([^/]+?)(\.git)?$/);
26
+ // Extract owner/repo
27
+ const repoMatch = cleanUrl.match(/github\.com[/:]([^/]+)\/([^/]+?)(\.git)?$/);
28
28
 
29
- if (!repoMatch) {
30
- throw new Error('Invalid GitHub URL format');
31
- }
29
+ if (!repoMatch) {
30
+ throw new Error("Invalid GitHub URL format");
31
+ }
32
32
 
33
- return {
34
- owner: repoMatch[1],
35
- repo: repoMatch[2].replace('.git', ''),
36
- branch,
37
- subpath,
38
- cleanUrl
39
- };
33
+ return {
34
+ owner: repoMatch[1],
35
+ repo: repoMatch[2].replace(".git", ""),
36
+ branch,
37
+ subpath,
38
+ cleanUrl,
39
+ };
40
40
  }
41
41
 
42
42
  /**
43
43
  * Clone a GitHub repository
44
44
  * @param {string} url - GitHub URL
45
45
  * @param {string} targetPath - Target directory
46
+ * @param {boolean} shallow - Use shallow clone (default true)
46
47
  * @returns {Promise<void>}
47
48
  */
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
- }
49
+ async function cloneRepo(url, targetPath, shallow = true) {
50
+ try {
51
+ const args = shallow
52
+ ? ["clone", "--depth", "1", url, targetPath]
53
+ : ["clone", url, targetPath];
54
+ await execa("git", args);
55
+ } catch (error) {
56
+ throw new Error(`Failed to clone repository: ${error.message}`);
57
+ }
54
58
  }
55
59
 
56
60
  /**
@@ -59,11 +63,11 @@ async function cloneRepo(url, targetPath) {
59
63
  * @returns {Promise<void>}
60
64
  */
61
65
  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
- }
66
+ try {
67
+ await execa("git", ["-C", repoPath, "pull", "--rebase"]);
68
+ } catch (error) {
69
+ throw new Error(`Failed to pull repository: ${error.message}`);
70
+ }
67
71
  }
68
72
 
69
73
  /**
@@ -72,37 +76,37 @@ async function pullRepo(repoPath) {
72
76
  * @returns {Promise<{skillPath: string, needsUpdate: boolean}>}
73
77
  */
74
78
  async function cloneOrUpdateRepo(url) {
75
- const parsed = parseGitHubUrl(url);
76
- const targetPath = path.join(DEFAULT_LIB_PATH, parsed.owner, parsed.repo);
79
+ const parsed = parseGitHubUrl(url);
80
+ const targetPath = path.join(DEFAULT_LIB_PATH, parsed.owner, parsed.repo);
77
81
 
78
- let needsUpdate = false;
82
+ let needsUpdate = false;
79
83
 
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
- }
84
+ if (dirExists(targetPath)) {
85
+ // Repo exists, ask if user wants to update
86
+ needsUpdate = true;
87
+ } else {
88
+ // Clone new repo
89
+ await cloneRepo(parsed.cleanUrl, targetPath);
90
+ }
87
91
 
88
- // Determine final skill path
89
- let skillPath = targetPath;
90
- if (parsed.subpath) {
91
- skillPath = path.join(targetPath, parsed.subpath);
92
- }
92
+ // Determine final skill path
93
+ let skillPath = targetPath;
94
+ if (parsed.subpath) {
95
+ skillPath = path.join(targetPath, parsed.subpath);
96
+ }
93
97
 
94
- return {
95
- skillPath,
96
- targetPath,
97
- needsUpdate,
98
- hasSubpath: !!parsed.subpath
99
- };
98
+ return {
99
+ skillPath,
100
+ targetPath,
101
+ needsUpdate,
102
+ hasSubpath: !!parsed.subpath,
103
+ };
100
104
  }
101
105
 
102
106
  module.exports = {
103
- DEFAULT_LIB_PATH,
104
- parseGitHubUrl,
105
- cloneRepo,
106
- pullRepo,
107
- cloneOrUpdateRepo
107
+ DEFAULT_LIB_PATH,
108
+ parseGitHubUrl,
109
+ cloneRepo,
110
+ pullRepo,
111
+ cloneOrUpdateRepo,
108
112
  };