skill-linker 2.0.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 +43 -64
- package/bin/cli.js +3 -43
- package/package.json +11 -5
- package/src/cli.js +45 -0
- package/src/commands/install.js +260 -0
- package/src/commands/list.js +78 -0
- package/src/utils/agents.js +91 -0
- package/src/utils/file-system.js +166 -0
- package/src/utils/git.js +108 -0
- package/link-skill.sh +0 -413
|
@@ -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
|
+
};
|
package/src/utils/git.js
ADDED
|
@@ -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
|
+
};
|