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.
@@ -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
+ };