reskill 1.1.0 → 1.2.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 +30 -1
- package/README.zh-CN.md +35 -6
- package/dist/cli/commands/__integration__/helpers.d.ts +2 -1
- package/dist/cli/commands/__integration__/helpers.d.ts.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/index.js +2035 -261
- package/dist/core/cache-manager.d.ts +16 -1
- package/dist/core/cache-manager.d.ts.map +1 -1
- package/dist/core/config-loader.d.ts +52 -0
- package/dist/core/config-loader.d.ts.map +1 -1
- package/dist/core/git-resolver.d.ts +26 -1
- package/dist/core/git-resolver.d.ts.map +1 -1
- package/dist/core/http-resolver.d.ts +104 -0
- package/dist/core/http-resolver.d.ts.map +1 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/skill-manager.d.ts +22 -1
- package/dist/core/skill-manager.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1717 -198
- package/dist/utils/git.d.ts +12 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/http.d.ts +51 -0
- package/dist/utils/http.d.ts.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as __WEBPACK_EXTERNAL_MODULE_node_os__ from "node:os";
|
|
|
3
3
|
import * as __WEBPACK_EXTERNAL_MODULE_node_path__ from "node:path";
|
|
4
4
|
import * as __WEBPACK_EXTERNAL_MODULE_node_child_process__ from "node:child_process";
|
|
5
5
|
import * as __WEBPACK_EXTERNAL_MODULE_node_util__ from "node:util";
|
|
6
|
+
import * as __WEBPACK_EXTERNAL_MODULE_node_stream_promises__ from "node:stream/promises";
|
|
6
7
|
import * as __WEBPACK_EXTERNAL_MODULE_semver__ from "semver";
|
|
7
8
|
import * as __WEBPACK_EXTERNAL_MODULE_chalk__ from "chalk";
|
|
8
9
|
var __webpack_modules__ = {
|
|
@@ -10,19 +11,33 @@ var __webpack_modules__ = {
|
|
|
10
11
|
module.exports = __WEBPACK_EXTERNAL_MODULE_node_fs__;
|
|
11
12
|
}
|
|
12
13
|
};
|
|
14
|
+
/************************************************************************/ // The module cache
|
|
13
15
|
var __webpack_module_cache__ = {};
|
|
16
|
+
// The require function
|
|
14
17
|
function __webpack_require__(moduleId) {
|
|
18
|
+
// Check if module is in cache
|
|
15
19
|
var cachedModule = __webpack_module_cache__[moduleId];
|
|
16
20
|
if (void 0 !== cachedModule) return cachedModule.exports;
|
|
21
|
+
// Create a new module (and put it into the cache)
|
|
17
22
|
var module = __webpack_module_cache__[moduleId] = {
|
|
18
23
|
exports: {}
|
|
19
24
|
};
|
|
25
|
+
// Execute the module function
|
|
20
26
|
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
|
27
|
+
// Return the exports of the module
|
|
21
28
|
return module.exports;
|
|
22
29
|
}
|
|
30
|
+
/************************************************************************/ // EXTERNAL MODULE: external "node:fs"
|
|
23
31
|
var external_node_fs_ = __webpack_require__("node:fs");
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Agent Registry - Multi-Agent configuration definitions
|
|
34
|
+
*
|
|
35
|
+
* Supports global and project-level installation for 17 coding agents
|
|
36
|
+
* Reference: https://github.com/vercel-labs/add-skill
|
|
37
|
+
*/ const agent_registry_home = (0, __WEBPACK_EXTERNAL_MODULE_node_os__.homedir)();
|
|
38
|
+
/**
|
|
39
|
+
* All supported Agents configuration
|
|
40
|
+
*/ const agents = {
|
|
26
41
|
amp: {
|
|
27
42
|
name: 'amp',
|
|
28
43
|
displayName: 'Amp',
|
|
@@ -143,52 +158,81 @@ const agents = {
|
|
|
143
158
|
detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.neovate'))
|
|
144
159
|
}
|
|
145
160
|
};
|
|
146
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Get all Agent type list
|
|
163
|
+
*/ function getAllAgentTypes() {
|
|
147
164
|
return Object.keys(agents);
|
|
148
165
|
}
|
|
149
|
-
|
|
166
|
+
/**
|
|
167
|
+
* Detect installed Agents
|
|
168
|
+
*/ async function detectInstalledAgents() {
|
|
150
169
|
const installed = [];
|
|
151
170
|
for (const [type, config] of Object.entries(agents))if (await config.detectInstalled()) installed.push(type);
|
|
152
171
|
return installed;
|
|
153
172
|
}
|
|
154
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Get Agent configuration
|
|
175
|
+
*/ function getAgentConfig(type) {
|
|
155
176
|
return agents[type];
|
|
156
177
|
}
|
|
157
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Validate if Agent type is valid
|
|
180
|
+
*/ function isValidAgentType(type) {
|
|
158
181
|
return type in agents;
|
|
159
182
|
}
|
|
160
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Get Agent's project-level skills directory
|
|
185
|
+
*/ function getAgentSkillsDir(type, options = {}) {
|
|
161
186
|
const config = agents[type];
|
|
162
187
|
if (options.global) return config.globalSkillsDir;
|
|
163
188
|
const cwd = options.cwd || process.cwd();
|
|
164
189
|
return (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(cwd, config.skillsDir);
|
|
165
190
|
}
|
|
166
|
-
|
|
191
|
+
/**
|
|
192
|
+
* File system utilities
|
|
193
|
+
*/ /**
|
|
194
|
+
* Check if a file or directory exists
|
|
195
|
+
*/ function exists(filePath) {
|
|
167
196
|
return external_node_fs_.existsSync(filePath);
|
|
168
197
|
}
|
|
169
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Read JSON file
|
|
200
|
+
*/ function readJson(filePath) {
|
|
170
201
|
const content = external_node_fs_.readFileSync(filePath, 'utf-8');
|
|
171
202
|
return JSON.parse(content);
|
|
172
203
|
}
|
|
173
|
-
|
|
204
|
+
/**
|
|
205
|
+
* Write JSON file
|
|
206
|
+
*/ function writeJson(filePath, data, indent = 2) {
|
|
174
207
|
const dir = __WEBPACK_EXTERNAL_MODULE_node_path__.dirname(filePath);
|
|
175
208
|
if (!exists(dir)) external_node_fs_.mkdirSync(dir, {
|
|
176
209
|
recursive: true
|
|
177
210
|
});
|
|
178
211
|
external_node_fs_.writeFileSync(filePath, `${JSON.stringify(data, null, indent)}\n`, 'utf-8');
|
|
179
212
|
}
|
|
180
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Create directory recursively
|
|
215
|
+
*/ function ensureDir(dirPath) {
|
|
181
216
|
if (!exists(dirPath)) external_node_fs_.mkdirSync(dirPath, {
|
|
182
217
|
recursive: true
|
|
183
218
|
});
|
|
184
219
|
}
|
|
185
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Remove file or directory
|
|
222
|
+
*/ function remove(targetPath) {
|
|
186
223
|
if (exists(targetPath)) external_node_fs_.rmSync(targetPath, {
|
|
187
224
|
recursive: true,
|
|
188
225
|
force: true
|
|
189
226
|
});
|
|
190
227
|
}
|
|
191
|
-
|
|
228
|
+
/**
|
|
229
|
+
* Copy directory recursively
|
|
230
|
+
*
|
|
231
|
+
* @param src - Source directory
|
|
232
|
+
* @param dest - Destination directory
|
|
233
|
+
* @param options.exclude - Array of filenames to exclude
|
|
234
|
+
* @param options.excludePrefix - Prefix for files to exclude (e.g., '_' to exclude _private.md)
|
|
235
|
+
*/ function copyDir(src, dest, options) {
|
|
192
236
|
const exclude = options?.exclude || [];
|
|
193
237
|
const excludePrefix = options?.excludePrefix || '_';
|
|
194
238
|
ensureDir(dest);
|
|
@@ -196,6 +240,7 @@ function copyDir(src, dest, options) {
|
|
|
196
240
|
withFileTypes: true
|
|
197
241
|
});
|
|
198
242
|
for (const entry of entries){
|
|
243
|
+
// Skip files in exclude list or starting with excludePrefix
|
|
199
244
|
if (exclude.includes(entry.name) || entry.name.startsWith(excludePrefix)) continue;
|
|
200
245
|
const srcPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(src, entry.name);
|
|
201
246
|
const destPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dest, entry.name);
|
|
@@ -203,72 +248,134 @@ function copyDir(src, dest, options) {
|
|
|
203
248
|
else external_node_fs_.copyFileSync(srcPath, destPath);
|
|
204
249
|
}
|
|
205
250
|
}
|
|
206
|
-
|
|
251
|
+
/**
|
|
252
|
+
* List directory contents
|
|
253
|
+
*/ function listDir(dirPath) {
|
|
207
254
|
if (!exists(dirPath)) return [];
|
|
208
255
|
return external_node_fs_.readdirSync(dirPath);
|
|
209
256
|
}
|
|
210
|
-
|
|
257
|
+
/**
|
|
258
|
+
* Check if path is a directory
|
|
259
|
+
*/ function isDirectory(targetPath) {
|
|
211
260
|
if (!exists(targetPath)) return false;
|
|
212
261
|
return external_node_fs_.statSync(targetPath).isDirectory();
|
|
213
262
|
}
|
|
214
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Check if path is a symbolic link
|
|
265
|
+
*/ function isSymlink(targetPath) {
|
|
215
266
|
if (!exists(targetPath)) return false;
|
|
216
267
|
return external_node_fs_.lstatSync(targetPath).isSymbolicLink();
|
|
217
268
|
}
|
|
218
|
-
|
|
269
|
+
/**
|
|
270
|
+
* Get real path of symbolic link
|
|
271
|
+
*/ function getRealPath(linkPath) {
|
|
219
272
|
return external_node_fs_.realpathSync(linkPath);
|
|
220
273
|
}
|
|
221
|
-
|
|
274
|
+
/**
|
|
275
|
+
* Get skills.json path for current project
|
|
276
|
+
*/ function getSkillsJsonPath(projectRoot) {
|
|
222
277
|
const root = projectRoot || process.cwd();
|
|
223
278
|
return __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills.json');
|
|
224
279
|
}
|
|
225
|
-
|
|
280
|
+
/**
|
|
281
|
+
* Get skills.lock path for current project
|
|
282
|
+
*/ function getSkillsLockPath(projectRoot) {
|
|
226
283
|
const root = projectRoot || process.cwd();
|
|
227
284
|
return __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills.lock');
|
|
228
285
|
}
|
|
229
|
-
|
|
286
|
+
/**
|
|
287
|
+
* Get global cache directory
|
|
288
|
+
*/ function getCacheDir() {
|
|
230
289
|
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
231
290
|
return process.env.RESKILL_CACHE_DIR || __WEBPACK_EXTERNAL_MODULE_node_path__.join(home, '.reskill-cache');
|
|
232
291
|
}
|
|
233
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Get home directory
|
|
294
|
+
*/ function getHomeDir() {
|
|
234
295
|
return process.env.HOME || process.env.USERPROFILE || '';
|
|
235
296
|
}
|
|
236
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Get global skills installation directory (~/.claude/skills)
|
|
299
|
+
* @deprecated Use getAgentGlobalSkillsDir from agent-registry instead
|
|
300
|
+
*/ function getGlobalSkillsDir() {
|
|
237
301
|
const home = getHomeDir();
|
|
238
302
|
return __WEBPACK_EXTERNAL_MODULE_node_path__.join(home, '.claude', 'skills');
|
|
239
303
|
}
|
|
240
|
-
|
|
304
|
+
// ============================================================================
|
|
305
|
+
// Multi-Agent path utilities
|
|
306
|
+
// ============================================================================
|
|
307
|
+
/**
|
|
308
|
+
* Canonical skills directory name
|
|
309
|
+
*/ const AGENTS_DIR = '.agents';
|
|
241
310
|
const SKILLS_SUBDIR = 'skills';
|
|
242
|
-
|
|
311
|
+
/**
|
|
312
|
+
* Get canonical skills directory path
|
|
313
|
+
*
|
|
314
|
+
* Canonical location: .agents/skills/ (project level) or ~/.agents/skills/ (global)
|
|
315
|
+
* This is the storage location for skill source files, each agent directory points here via symlinks
|
|
316
|
+
*/ function getCanonicalSkillsDir(options = {}) {
|
|
243
317
|
const { global: isGlobal = false, cwd } = options;
|
|
244
318
|
const baseDir = isGlobal ? getHomeDir() : cwd || process.cwd();
|
|
245
319
|
return __WEBPACK_EXTERNAL_MODULE_node_path__.join(baseDir, AGENTS_DIR, SKILLS_SUBDIR);
|
|
246
320
|
}
|
|
247
|
-
|
|
321
|
+
/**
|
|
322
|
+
* Get canonical skill path
|
|
323
|
+
*/ function getCanonicalSkillPath(skillName, options = {}) {
|
|
248
324
|
return __WEBPACK_EXTERNAL_MODULE_node_path__.join(getCanonicalSkillsDir(options), skillName);
|
|
249
325
|
}
|
|
250
|
-
|
|
326
|
+
/**
|
|
327
|
+
* Shorten path display (replace home directory with ~)
|
|
328
|
+
*/ function shortenPath(fullPath, cwd) {
|
|
251
329
|
const home = getHomeDir();
|
|
252
330
|
const currentDir = cwd || process.cwd();
|
|
253
331
|
if (fullPath.startsWith(home)) return fullPath.replace(home, '~');
|
|
254
332
|
if (fullPath.startsWith(currentDir)) return `.${fullPath.slice(currentDir.length)}`;
|
|
255
333
|
return fullPath;
|
|
256
334
|
}
|
|
257
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Validate path safety (prevent path traversal attacks)
|
|
337
|
+
*/ function isPathSafe(basePath, targetPath) {
|
|
258
338
|
const normalizedBase = __WEBPACK_EXTERNAL_MODULE_node_path__.normalize(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(basePath));
|
|
259
339
|
const normalizedTarget = __WEBPACK_EXTERNAL_MODULE_node_path__.normalize(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(targetPath));
|
|
260
340
|
return normalizedTarget.startsWith(normalizedBase + __WEBPACK_EXTERNAL_MODULE_node_path__.sep) || normalizedTarget === normalizedBase;
|
|
261
341
|
}
|
|
262
|
-
|
|
342
|
+
/**
|
|
343
|
+
* Sanitize filename (prevent path traversal attacks)
|
|
344
|
+
*/ function sanitizeName(name) {
|
|
345
|
+
// Remove path separators and special characters
|
|
263
346
|
let sanitized = name.replace(/[/\\:\0]/g, '');
|
|
347
|
+
// Remove leading and trailing dots and spaces
|
|
264
348
|
sanitized = sanitized.replace(/^[.\s]+|[.\s]+$/g, '');
|
|
349
|
+
// Remove leading dots
|
|
265
350
|
sanitized = sanitized.replace(/^\.+/, '');
|
|
266
351
|
if (!sanitized || 0 === sanitized.length) sanitized = 'unnamed-skill';
|
|
267
352
|
if (sanitized.length > 255) sanitized = sanitized.substring(0, 255);
|
|
268
353
|
return sanitized;
|
|
269
354
|
}
|
|
270
355
|
const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEBPACK_EXTERNAL_MODULE_node_child_process__.exec);
|
|
271
|
-
|
|
356
|
+
/**
|
|
357
|
+
* Git utilities
|
|
358
|
+
*/ /**
|
|
359
|
+
* SSH command with auto-accept for new host keys
|
|
360
|
+
* Uses StrictHostKeyChecking=accept-new which:
|
|
361
|
+
* - Automatically accepts keys for hosts not in known_hosts
|
|
362
|
+
* - Still rejects connections if a known host's key has changed (security)
|
|
363
|
+
*/ const GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes';
|
|
364
|
+
/**
|
|
365
|
+
* Get environment variables for git commands that access remote repositories
|
|
366
|
+
* Configures SSH to auto-accept new host keys and disables interactive prompts
|
|
367
|
+
*/ function getGitEnv() {
|
|
368
|
+
return {
|
|
369
|
+
...process.env,
|
|
370
|
+
GIT_SSH_COMMAND,
|
|
371
|
+
// Disable interactive prompts for HTTPS as well
|
|
372
|
+
GIT_TERMINAL_PROMPT: '0'
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Custom error class for Git clone failures
|
|
377
|
+
* Provides helpful tips for private repository authentication
|
|
378
|
+
*/ class GitCloneError extends Error {
|
|
272
379
|
repoUrl;
|
|
273
380
|
originalError;
|
|
274
381
|
isAuthError;
|
|
@@ -283,6 +390,7 @@ class GitCloneError extends Error {
|
|
|
283
390
|
message += '\n - Check ~/.ssh/id_rsa or ~/.ssh/id_ed25519';
|
|
284
391
|
message += '\n - Ensure SSH key is added to your Git hosting service';
|
|
285
392
|
} else {
|
|
393
|
+
// HTTPS or unknown
|
|
286
394
|
message += "\n - Run 'git config --global credential.helper store'";
|
|
287
395
|
message += '\n - Or use a personal access token in the URL';
|
|
288
396
|
}
|
|
@@ -294,12 +402,16 @@ class GitCloneError extends Error {
|
|
|
294
402
|
this.isAuthError = isAuthError;
|
|
295
403
|
this.urlType = urlType;
|
|
296
404
|
}
|
|
297
|
-
|
|
405
|
+
/**
|
|
406
|
+
* Detect URL type from repository URL
|
|
407
|
+
*/ static detectUrlType(url) {
|
|
298
408
|
if (url.startsWith('git@') || url.startsWith('ssh://')) return 'ssh';
|
|
299
409
|
if (url.startsWith('http://') || url.startsWith('https://')) return 'https';
|
|
300
410
|
return 'unknown';
|
|
301
411
|
}
|
|
302
|
-
|
|
412
|
+
/**
|
|
413
|
+
* Check if an error message indicates an authentication problem
|
|
414
|
+
*/ static isAuthenticationError(message) {
|
|
303
415
|
const authPatterns = [
|
|
304
416
|
/permission denied/i,
|
|
305
417
|
/could not read from remote/i,
|
|
@@ -314,14 +426,19 @@ class GitCloneError extends Error {
|
|
|
314
426
|
return authPatterns.some((pattern)=>pattern.test(message));
|
|
315
427
|
}
|
|
316
428
|
}
|
|
317
|
-
|
|
429
|
+
/**
|
|
430
|
+
* Execute git command asynchronously
|
|
431
|
+
*/ async function git(args, cwd) {
|
|
318
432
|
const { stdout } = await git_execAsync(`git ${args.join(' ')}`, {
|
|
319
433
|
cwd,
|
|
320
|
-
encoding: 'utf-8'
|
|
434
|
+
encoding: 'utf-8',
|
|
435
|
+
env: getGitEnv()
|
|
321
436
|
});
|
|
322
437
|
return stdout.trim();
|
|
323
438
|
}
|
|
324
|
-
|
|
439
|
+
/**
|
|
440
|
+
* Get remote tags for a repository
|
|
441
|
+
*/ async function getRemoteTags(repoUrl) {
|
|
325
442
|
try {
|
|
326
443
|
const output = await git([
|
|
327
444
|
'ls-remote',
|
|
@@ -335,6 +452,7 @@ async function getRemoteTags(repoUrl) {
|
|
|
335
452
|
for (const line of lines){
|
|
336
453
|
const [commit, ref] = line.split('\t');
|
|
337
454
|
if (commit && ref) {
|
|
455
|
+
// Extract tag name from refs/tags/v1.0.0
|
|
338
456
|
const tagName = ref.replace('refs/tags/', '');
|
|
339
457
|
tags.push({
|
|
340
458
|
name: tagName,
|
|
@@ -347,9 +465,12 @@ async function getRemoteTags(repoUrl) {
|
|
|
347
465
|
return [];
|
|
348
466
|
}
|
|
349
467
|
}
|
|
350
|
-
|
|
468
|
+
/**
|
|
469
|
+
* Get latest tag from repository
|
|
470
|
+
*/ async function getLatestTag(repoUrl) {
|
|
351
471
|
const tags = await getRemoteTags(repoUrl);
|
|
352
472
|
if (0 === tags.length) return null;
|
|
473
|
+
// Sort by semver (simple version sort)
|
|
353
474
|
const sortedTags = tags.sort((a, b)=>{
|
|
354
475
|
const aVer = a.name.replace(/^v/, '');
|
|
355
476
|
const bVer = b.name.replace(/^v/, '');
|
|
@@ -357,7 +478,11 @@ async function getLatestTag(repoUrl) {
|
|
|
357
478
|
});
|
|
358
479
|
return sortedTags[0];
|
|
359
480
|
}
|
|
360
|
-
|
|
481
|
+
/**
|
|
482
|
+
* Clone a repository with shallow clone
|
|
483
|
+
*
|
|
484
|
+
* @throws {GitCloneError} When clone fails, with helpful tips for authentication issues
|
|
485
|
+
*/ async function clone(repoUrl, destPath, options) {
|
|
361
486
|
const args = [
|
|
362
487
|
'clone'
|
|
363
488
|
];
|
|
@@ -370,13 +495,17 @@ async function clone(repoUrl, destPath, options) {
|
|
|
370
495
|
throw new GitCloneError(repoUrl, error);
|
|
371
496
|
}
|
|
372
497
|
}
|
|
373
|
-
|
|
498
|
+
/**
|
|
499
|
+
* Get current commit hash
|
|
500
|
+
*/ async function getCurrentCommit(cwd) {
|
|
374
501
|
return git([
|
|
375
502
|
'rev-parse',
|
|
376
503
|
'HEAD'
|
|
377
504
|
], cwd);
|
|
378
505
|
}
|
|
379
|
-
|
|
506
|
+
/**
|
|
507
|
+
* Get default branch name
|
|
508
|
+
*/ async function getDefaultBranch(repoUrl) {
|
|
380
509
|
try {
|
|
381
510
|
const output = await git([
|
|
382
511
|
'ls-remote',
|
|
@@ -390,7 +519,9 @@ async function getDefaultBranch(repoUrl) {
|
|
|
390
519
|
return 'main';
|
|
391
520
|
}
|
|
392
521
|
}
|
|
393
|
-
|
|
522
|
+
/**
|
|
523
|
+
* Simple version comparison (for sorting)
|
|
524
|
+
*/ function compareVersions(a, b) {
|
|
394
525
|
const aParts = a.split('.').map((p)=>parseInt(p, 10) || 0);
|
|
395
526
|
const bParts = b.split('.').map((p)=>parseInt(p, 10) || 0);
|
|
396
527
|
const maxLength = Math.max(aParts.length, bParts.length);
|
|
@@ -402,24 +533,40 @@ function compareVersions(a, b) {
|
|
|
402
533
|
}
|
|
403
534
|
return 0;
|
|
404
535
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
function isGitUrl(source) {
|
|
536
|
+
/**
|
|
537
|
+
* Check if a source string is a complete Git URL (SSH, HTTPS, or git://)
|
|
538
|
+
*
|
|
539
|
+
* Supported formats:
|
|
540
|
+
* - SSH: git@github.com:user/repo.git
|
|
541
|
+
* - HTTPS: https://github.com/user/repo.git
|
|
542
|
+
* - Git protocol: git://github.com/user/repo.git
|
|
543
|
+
* - URLs ending with .git
|
|
544
|
+
*/ function isGitUrl(source) {
|
|
414
545
|
return source.startsWith('git@') || source.startsWith('git://') || source.startsWith('http://') || source.startsWith('https://') || source.endsWith('.git');
|
|
415
546
|
}
|
|
416
|
-
|
|
547
|
+
/**
|
|
548
|
+
* Parse a Git URL and extract host, owner, and repo information
|
|
549
|
+
*
|
|
550
|
+
* Supports:
|
|
551
|
+
* - SSH: git@github.com:user/repo.git
|
|
552
|
+
* - HTTPS: https://github.com/user/repo.git
|
|
553
|
+
* - Git protocol: git://github.com/user/repo.git
|
|
554
|
+
*
|
|
555
|
+
* Note: GitHub/GitLab web URLs (with /tree/, /blob/, etc.) are handled
|
|
556
|
+
* at a higher level in GitResolver.parseGitUrlRef() before calling this function.
|
|
557
|
+
*
|
|
558
|
+
* @param url The Git URL to parse
|
|
559
|
+
* @returns Parsed URL information or null if parsing fails
|
|
560
|
+
*/ function parseGitUrl(url) {
|
|
561
|
+
// Remove trailing .git if present
|
|
417
562
|
const cleanUrl = url.replace(/\.git$/, '');
|
|
563
|
+
// SSH format: git@github.com:user/repo
|
|
418
564
|
const sshMatch = cleanUrl.match(/^git@([^:]+):(.+)$/);
|
|
419
565
|
if (sshMatch) {
|
|
420
566
|
const [, host, path] = sshMatch;
|
|
421
567
|
const parts = path.split('/');
|
|
422
568
|
if (parts.length >= 2) {
|
|
569
|
+
// Handle nested paths like org/sub/repo
|
|
423
570
|
const owner = parts.slice(0, -1).join('/');
|
|
424
571
|
const repo = parts[parts.length - 1];
|
|
425
572
|
return {
|
|
@@ -431,6 +578,7 @@ function parseGitUrl(url) {
|
|
|
431
578
|
};
|
|
432
579
|
}
|
|
433
580
|
}
|
|
581
|
+
// HTTPS/Git protocol format: https://github.com/user/repo or git://github.com/user/repo
|
|
434
582
|
const httpMatch = cleanUrl.match(/^(https?|git):\/\/([^/]+)\/(.+)$/);
|
|
435
583
|
if (httpMatch) {
|
|
436
584
|
const [, protocol, host, path] = httpMatch;
|
|
@@ -449,50 +597,286 @@ function parseGitUrl(url) {
|
|
|
449
597
|
}
|
|
450
598
|
return null;
|
|
451
599
|
}
|
|
452
|
-
|
|
600
|
+
/**
|
|
601
|
+
* HTTP utilities for downloading and extracting skill archives
|
|
602
|
+
*/ /**
|
|
603
|
+
* Custom error class for HTTP download failures
|
|
604
|
+
*/ class HttpDownloadError extends Error {
|
|
605
|
+
url;
|
|
606
|
+
statusCode;
|
|
607
|
+
originalError;
|
|
608
|
+
constructor(url, message, statusCode, originalError){
|
|
609
|
+
super(message);
|
|
610
|
+
this.name = 'HttpDownloadError';
|
|
611
|
+
this.url = url;
|
|
612
|
+
this.statusCode = statusCode;
|
|
613
|
+
this.originalError = originalError;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Download a file from HTTP/HTTPS URL
|
|
618
|
+
*
|
|
619
|
+
* @param url - URL to download from
|
|
620
|
+
* @param destPath - Destination file path
|
|
621
|
+
* @param options - Download options
|
|
622
|
+
*/ async function downloadFile(url, destPath, options = {}) {
|
|
623
|
+
const { timeout = 60000, headers = {} } = options;
|
|
624
|
+
// Ensure destination directory exists
|
|
625
|
+
ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(destPath));
|
|
626
|
+
try {
|
|
627
|
+
// Use native fetch for HTTP/HTTPS
|
|
628
|
+
const controller = new AbortController();
|
|
629
|
+
const timeoutId = setTimeout(()=>controller.abort(), timeout);
|
|
630
|
+
const response = await fetch(url, {
|
|
631
|
+
signal: controller.signal,
|
|
632
|
+
headers: {
|
|
633
|
+
'User-Agent': 'reskill/1.0',
|
|
634
|
+
...headers
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
clearTimeout(timeoutId);
|
|
638
|
+
if (!response.ok) throw new HttpDownloadError(url, `HTTP ${response.status}: ${response.statusText}`, response.status);
|
|
639
|
+
// Stream response to file
|
|
640
|
+
const fileStream = external_node_fs_.createWriteStream(destPath);
|
|
641
|
+
const body = response.body;
|
|
642
|
+
if (!body) throw new HttpDownloadError(url, 'Response body is empty');
|
|
643
|
+
// Convert Web ReadableStream to Node.js Readable
|
|
644
|
+
const { Readable } = await import("node:stream");
|
|
645
|
+
const nodeStream = Readable.fromWeb(body);
|
|
646
|
+
await (0, __WEBPACK_EXTERNAL_MODULE_node_stream_promises__.pipeline)(nodeStream, fileStream);
|
|
647
|
+
} catch (error) {
|
|
648
|
+
// Clean up partial download
|
|
649
|
+
if (external_node_fs_.existsSync(destPath)) external_node_fs_.unlinkSync(destPath);
|
|
650
|
+
if (error instanceof HttpDownloadError) throw error;
|
|
651
|
+
const err = error;
|
|
652
|
+
if ('AbortError' === err.name) throw new HttpDownloadError(url, `Download timeout after ${timeout}ms`);
|
|
653
|
+
throw new HttpDownloadError(url, `Download failed: ${err.message}`, void 0, err);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Extract an archive to a directory
|
|
658
|
+
*
|
|
659
|
+
* @param archivePath - Path to the archive file
|
|
660
|
+
* @param destDir - Destination directory
|
|
661
|
+
* @param format - Archive format (auto-detected from extension if not provided)
|
|
662
|
+
*/ async function extractArchive(archivePath, destDir, format) {
|
|
663
|
+
// Auto-detect format from extension
|
|
664
|
+
const detectedFormat = format || detectArchiveFormat(archivePath);
|
|
665
|
+
if (!detectedFormat) throw new Error(`Unable to detect archive format for: ${archivePath}`);
|
|
666
|
+
// Ensure destination directory exists
|
|
667
|
+
ensureDir(destDir);
|
|
668
|
+
switch(detectedFormat){
|
|
669
|
+
case 'tar.gz':
|
|
670
|
+
case 'tgz':
|
|
671
|
+
case 'tar':
|
|
672
|
+
await extractTar(archivePath, destDir, 'tar.gz' === detectedFormat || 'tgz' === detectedFormat);
|
|
673
|
+
break;
|
|
674
|
+
case 'zip':
|
|
675
|
+
await extractZip(archivePath, destDir);
|
|
676
|
+
break;
|
|
677
|
+
default:
|
|
678
|
+
throw new Error(`Unsupported archive format: ${detectedFormat}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Extract tar archive using native tar command
|
|
683
|
+
*/ async function extractTar(archivePath, destDir, gzipped) {
|
|
684
|
+
const { exec } = await import("node:child_process");
|
|
685
|
+
const { promisify } = await import("node:util");
|
|
686
|
+
const execAsync = promisify(exec);
|
|
687
|
+
const flags = gzipped ? '-xzf' : '-xf';
|
|
688
|
+
try {
|
|
689
|
+
// Extract to a temp directory first to handle single-folder archives
|
|
690
|
+
const tempExtractDir = `${destDir}.extract-temp`;
|
|
691
|
+
ensureDir(tempExtractDir);
|
|
692
|
+
await execAsync(`tar ${flags} "${archivePath}" -C "${tempExtractDir}"`, {
|
|
693
|
+
encoding: 'utf-8'
|
|
694
|
+
});
|
|
695
|
+
// Check if archive contains a single root directory
|
|
696
|
+
const extractedItems = external_node_fs_.readdirSync(tempExtractDir);
|
|
697
|
+
if (1 === extractedItems.length) {
|
|
698
|
+
const singleItem = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, extractedItems[0]);
|
|
699
|
+
if (external_node_fs_.statSync(singleItem).isDirectory()) {
|
|
700
|
+
// Move contents of single directory to destination
|
|
701
|
+
const contents = external_node_fs_.readdirSync(singleItem);
|
|
702
|
+
for (const item of contents){
|
|
703
|
+
const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(singleItem, item);
|
|
704
|
+
const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
|
|
705
|
+
external_node_fs_.renameSync(src, dest);
|
|
706
|
+
}
|
|
707
|
+
remove(tempExtractDir);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// Move all items to destination
|
|
712
|
+
for (const item of extractedItems){
|
|
713
|
+
const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, item);
|
|
714
|
+
const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
|
|
715
|
+
external_node_fs_.renameSync(src, dest);
|
|
716
|
+
}
|
|
717
|
+
remove(tempExtractDir);
|
|
718
|
+
} catch (error) {
|
|
719
|
+
throw new Error(`Failed to extract tar archive: ${error.message}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Extract zip archive using native unzip command or Node.js
|
|
724
|
+
*/ async function extractZip(archivePath, destDir) {
|
|
725
|
+
const { exec } = await import("node:child_process");
|
|
726
|
+
const { promisify } = await import("node:util");
|
|
727
|
+
const execAsync = promisify(exec);
|
|
728
|
+
try {
|
|
729
|
+
// Extract to a temp directory first
|
|
730
|
+
const tempExtractDir = `${destDir}.extract-temp`;
|
|
731
|
+
ensureDir(tempExtractDir);
|
|
732
|
+
// Try using unzip command (available on most systems)
|
|
733
|
+
await execAsync(`unzip -q "${archivePath}" -d "${tempExtractDir}"`, {
|
|
734
|
+
encoding: 'utf-8'
|
|
735
|
+
});
|
|
736
|
+
// Check if archive contains a single root directory
|
|
737
|
+
const extractedItems = external_node_fs_.readdirSync(tempExtractDir);
|
|
738
|
+
if (1 === extractedItems.length) {
|
|
739
|
+
const singleItem = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, extractedItems[0]);
|
|
740
|
+
if (external_node_fs_.statSync(singleItem).isDirectory()) {
|
|
741
|
+
// Move contents of single directory to destination
|
|
742
|
+
const contents = external_node_fs_.readdirSync(singleItem);
|
|
743
|
+
for (const item of contents){
|
|
744
|
+
const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(singleItem, item);
|
|
745
|
+
const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
|
|
746
|
+
external_node_fs_.renameSync(src, dest);
|
|
747
|
+
}
|
|
748
|
+
remove(tempExtractDir);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// Move all items to destination
|
|
753
|
+
for (const item of extractedItems){
|
|
754
|
+
const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, item);
|
|
755
|
+
const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
|
|
756
|
+
external_node_fs_.renameSync(src, dest);
|
|
757
|
+
}
|
|
758
|
+
remove(tempExtractDir);
|
|
759
|
+
} catch (error) {
|
|
760
|
+
throw new Error(`Failed to extract zip archive: ${error.message}`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Detect archive format from file path
|
|
765
|
+
*/ function detectArchiveFormat(filePath) {
|
|
766
|
+
const lower = filePath.toLowerCase();
|
|
767
|
+
if (lower.endsWith('.tar.gz')) return 'tar.gz';
|
|
768
|
+
if (lower.endsWith('.tgz')) return 'tgz';
|
|
769
|
+
if (lower.endsWith('.zip')) return 'zip';
|
|
770
|
+
if (lower.endsWith('.tar')) return 'tar';
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Download and extract an archive in one operation
|
|
774
|
+
*
|
|
775
|
+
* @param url - URL to download from
|
|
776
|
+
* @param destDir - Destination directory for extracted contents
|
|
777
|
+
* @param options - Download options
|
|
778
|
+
* @returns Path to extracted contents
|
|
779
|
+
*/ async function downloadAndExtract(url, destDir, options = {}) {
|
|
780
|
+
// Determine archive filename from URL
|
|
781
|
+
const urlObj = new URL(url);
|
|
782
|
+
const filename = __WEBPACK_EXTERNAL_MODULE_node_path__.basename(urlObj.pathname);
|
|
783
|
+
// Detect format from original filename before adding .download suffix
|
|
784
|
+
const format = detectArchiveFormat(filename);
|
|
785
|
+
if (!format) throw new Error(`Unable to detect archive format from URL: ${url}`);
|
|
786
|
+
const tempArchive = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, `../${filename}.download`);
|
|
787
|
+
try {
|
|
788
|
+
// Download archive
|
|
789
|
+
await downloadFile(url, tempArchive, options);
|
|
790
|
+
// Extract archive with explicit format
|
|
791
|
+
await extractArchive(tempArchive, destDir, format);
|
|
792
|
+
return destDir;
|
|
793
|
+
} finally{
|
|
794
|
+
// Clean up temp archive
|
|
795
|
+
if (external_node_fs_.existsSync(tempArchive)) external_node_fs_.unlinkSync(tempArchive);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Installer - Multi-Agent installer
|
|
800
|
+
*
|
|
801
|
+
* Supports two installation modes:
|
|
802
|
+
* - symlink: Canonical location (.agents/skills/) + symlinks to each agent directory
|
|
803
|
+
* - copy: Direct copy to each agent directory
|
|
804
|
+
*
|
|
805
|
+
* Reference: https://github.com/vercel-labs/add-skill/blob/main/src/installer.ts
|
|
806
|
+
*/ const installer_AGENTS_DIR = '.agents';
|
|
453
807
|
const installer_SKILLS_SUBDIR = 'skills';
|
|
454
|
-
|
|
808
|
+
/**
|
|
809
|
+
* Default files to exclude when copying skills
|
|
810
|
+
* These files are typically used for repository metadata and should not be copied to agent directories
|
|
811
|
+
*/ const DEFAULT_EXCLUDE_FILES = [
|
|
455
812
|
'README.md',
|
|
456
813
|
'metadata.json',
|
|
457
814
|
'.reskill-commit'
|
|
458
815
|
];
|
|
459
|
-
|
|
460
|
-
|
|
816
|
+
/**
|
|
817
|
+
* Prefix for files that should be excluded (internal/private files)
|
|
818
|
+
*/ const EXCLUDE_PREFIX = '_';
|
|
819
|
+
/**
|
|
820
|
+
* Sanitize filename to prevent path traversal attacks
|
|
821
|
+
*/ function installer_sanitizeName(name) {
|
|
822
|
+
// Remove path separators and special characters
|
|
461
823
|
let sanitized = name.replace(/[/\\:\0]/g, '');
|
|
824
|
+
// Remove leading and trailing dots and spaces
|
|
462
825
|
sanitized = sanitized.replace(/^[.\s]+|[.\s]+$/g, '');
|
|
826
|
+
// Remove leading dots
|
|
463
827
|
sanitized = sanitized.replace(/^\.+/, '');
|
|
464
828
|
if (!sanitized || 0 === sanitized.length) sanitized = 'unnamed-skill';
|
|
465
829
|
if (sanitized.length > 255) sanitized = sanitized.substring(0, 255);
|
|
466
830
|
return sanitized;
|
|
467
831
|
}
|
|
468
|
-
|
|
832
|
+
/**
|
|
833
|
+
* Validate path safety
|
|
834
|
+
*/ function installer_isPathSafe(basePath, targetPath) {
|
|
469
835
|
const normalizedBase = __WEBPACK_EXTERNAL_MODULE_node_path__.normalize(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(basePath));
|
|
470
836
|
const normalizedTarget = __WEBPACK_EXTERNAL_MODULE_node_path__.normalize(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(targetPath));
|
|
471
837
|
return normalizedTarget.startsWith(normalizedBase + __WEBPACK_EXTERNAL_MODULE_node_path__.sep) || normalizedTarget === normalizedBase;
|
|
472
838
|
}
|
|
473
|
-
|
|
839
|
+
/**
|
|
840
|
+
* Get canonical skills directory path
|
|
841
|
+
*
|
|
842
|
+
* @param isGlobal - Whether installing globally
|
|
843
|
+
* @param cwd - Current working directory
|
|
844
|
+
* @param installDir - Custom installation directory (relative to cwd), overrides default
|
|
845
|
+
*/ function installer_getCanonicalSkillsDir(isGlobal, cwd, installDir) {
|
|
474
846
|
const baseDir = isGlobal ? (0, __WEBPACK_EXTERNAL_MODULE_node_os__.homedir)() : cwd || process.cwd();
|
|
847
|
+
// Use custom installDir if provided, otherwise use default
|
|
475
848
|
if (installDir && !isGlobal) return __WEBPACK_EXTERNAL_MODULE_node_path__.join(baseDir, installDir);
|
|
476
849
|
return __WEBPACK_EXTERNAL_MODULE_node_path__.join(baseDir, installer_AGENTS_DIR, installer_SKILLS_SUBDIR);
|
|
477
850
|
}
|
|
478
|
-
|
|
851
|
+
/**
|
|
852
|
+
* Ensure directory exists
|
|
853
|
+
*/ function installer_ensureDir(dirPath) {
|
|
479
854
|
if (!external_node_fs_.existsSync(dirPath)) external_node_fs_.mkdirSync(dirPath, {
|
|
480
855
|
recursive: true
|
|
481
856
|
});
|
|
482
857
|
}
|
|
483
|
-
|
|
858
|
+
/**
|
|
859
|
+
* Remove file or directory
|
|
860
|
+
*/ function installer_remove(targetPath) {
|
|
484
861
|
if (external_node_fs_.existsSync(targetPath)) external_node_fs_.rmSync(targetPath, {
|
|
485
862
|
recursive: true,
|
|
486
863
|
force: true
|
|
487
864
|
});
|
|
488
865
|
}
|
|
489
|
-
|
|
866
|
+
/**
|
|
867
|
+
* Copy directory with file exclusion
|
|
868
|
+
*
|
|
869
|
+
* By default excludes:
|
|
870
|
+
* - Files in DEFAULT_EXCLUDE_FILES (README.md, metadata.json, .reskill-commit)
|
|
871
|
+
* - Files starting with EXCLUDE_PREFIX ('_')
|
|
872
|
+
*/ function copyDirectory(src, dest, options) {
|
|
490
873
|
const exclude = new Set(options?.exclude || DEFAULT_EXCLUDE_FILES);
|
|
491
874
|
installer_ensureDir(dest);
|
|
492
875
|
const entries = external_node_fs_.readdirSync(src, {
|
|
493
876
|
withFileTypes: true
|
|
494
877
|
});
|
|
495
878
|
for (const entry of entries){
|
|
879
|
+
// Skip files starting with EXCLUDE_PREFIX and files in exclude list
|
|
496
880
|
if (exclude.has(entry.name) || entry.name.startsWith(EXCLUDE_PREFIX)) continue;
|
|
497
881
|
const srcPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(src, entry.name);
|
|
498
882
|
const destPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dest, entry.name);
|
|
@@ -500,8 +884,13 @@ function copyDirectory(src, dest, options) {
|
|
|
500
884
|
else external_node_fs_.copyFileSync(srcPath, destPath);
|
|
501
885
|
}
|
|
502
886
|
}
|
|
503
|
-
|
|
887
|
+
/**
|
|
888
|
+
* Create symbolic link
|
|
889
|
+
*
|
|
890
|
+
* @returns true if successful, false if needs to fallback to copy
|
|
891
|
+
*/ async function installer_createSymlink(target, linkPath) {
|
|
504
892
|
try {
|
|
893
|
+
// Check existing link
|
|
505
894
|
try {
|
|
506
895
|
const stats = external_node_fs_.lstatSync(linkPath);
|
|
507
896
|
if (stats.isSymbolicLink()) {
|
|
@@ -512,17 +901,24 @@ async function installer_createSymlink(target, linkPath) {
|
|
|
512
901
|
recursive: true
|
|
513
902
|
});
|
|
514
903
|
} catch (err) {
|
|
904
|
+
// ELOOP = circular symlink, ENOENT = does not exist
|
|
515
905
|
if (err && 'object' == typeof err && 'code' in err) {
|
|
516
906
|
if ('ELOOP' === err.code) try {
|
|
517
907
|
external_node_fs_.rmSync(linkPath, {
|
|
518
908
|
force: true
|
|
519
909
|
});
|
|
520
|
-
} catch {
|
|
910
|
+
} catch {
|
|
911
|
+
// If unable to delete, symlink creation will fail and trigger copy fallback
|
|
912
|
+
}
|
|
521
913
|
}
|
|
914
|
+
// For ENOENT or other errors, continue trying to create symlink
|
|
522
915
|
}
|
|
916
|
+
// Ensure parent directory exists
|
|
523
917
|
const linkDir = __WEBPACK_EXTERNAL_MODULE_node_path__.dirname(linkPath);
|
|
524
918
|
installer_ensureDir(linkDir);
|
|
919
|
+
// Calculate relative path
|
|
525
920
|
const relativePath = __WEBPACK_EXTERNAL_MODULE_node_path__.relative(linkDir, target);
|
|
921
|
+
// Windows uses junction, other systems use default
|
|
526
922
|
const symlinkType = 'win32' === (0, __WEBPACK_EXTERNAL_MODULE_node_os__.platform)() ? 'junction' : void 0;
|
|
527
923
|
external_node_fs_.symlinkSync(relativePath, linkPath, symlinkType);
|
|
528
924
|
return true;
|
|
@@ -530,7 +926,9 @@ async function installer_createSymlink(target, linkPath) {
|
|
|
530
926
|
return false;
|
|
531
927
|
}
|
|
532
928
|
}
|
|
533
|
-
|
|
929
|
+
/**
|
|
930
|
+
* Installer class - Multi-Agent installer
|
|
931
|
+
*/ class Installer {
|
|
534
932
|
cwd;
|
|
535
933
|
isGlobal;
|
|
536
934
|
installDir;
|
|
@@ -539,25 +937,39 @@ class Installer {
|
|
|
539
937
|
this.isGlobal = options.global || false;
|
|
540
938
|
this.installDir = options.installDir;
|
|
541
939
|
}
|
|
542
|
-
|
|
940
|
+
/**
|
|
941
|
+
* Get canonical installation path
|
|
942
|
+
*/ getCanonicalPath(skillName) {
|
|
543
943
|
const sanitized = installer_sanitizeName(skillName);
|
|
544
944
|
const canonicalBase = installer_getCanonicalSkillsDir(this.isGlobal, this.cwd, this.installDir);
|
|
545
945
|
return __WEBPACK_EXTERNAL_MODULE_node_path__.join(canonicalBase, sanitized);
|
|
546
946
|
}
|
|
547
|
-
|
|
947
|
+
/**
|
|
948
|
+
* Get agent's skill installation path
|
|
949
|
+
*/ getAgentSkillPath(skillName, agentType) {
|
|
548
950
|
const agent = getAgentConfig(agentType);
|
|
549
951
|
const sanitized = installer_sanitizeName(skillName);
|
|
550
952
|
const agentBase = this.isGlobal ? agent.globalSkillsDir : __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cwd, agent.skillsDir);
|
|
551
953
|
return __WEBPACK_EXTERNAL_MODULE_node_path__.join(agentBase, sanitized);
|
|
552
954
|
}
|
|
553
|
-
|
|
955
|
+
/**
|
|
956
|
+
* Install skill to specified agent
|
|
957
|
+
*
|
|
958
|
+
* @param sourcePath - Skill source directory path
|
|
959
|
+
* @param skillName - Skill name
|
|
960
|
+
* @param agentType - Target agent type
|
|
961
|
+
* @param options - Installation options
|
|
962
|
+
*/ async installForAgent(sourcePath, skillName, agentType, options = {}) {
|
|
554
963
|
const agent = getAgentConfig(agentType);
|
|
555
964
|
const installMode = options.mode || 'symlink';
|
|
556
965
|
const sanitized = installer_sanitizeName(skillName);
|
|
966
|
+
// Canonical location
|
|
557
967
|
const canonicalBase = installer_getCanonicalSkillsDir(this.isGlobal, this.cwd, this.installDir);
|
|
558
968
|
const canonicalDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(canonicalBase, sanitized);
|
|
969
|
+
// Agent specific location
|
|
559
970
|
const agentBase = this.isGlobal ? agent.globalSkillsDir : __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cwd, agent.skillsDir);
|
|
560
971
|
const agentDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(agentBase, sanitized);
|
|
972
|
+
// Validate path safety
|
|
561
973
|
if (!installer_isPathSafe(canonicalBase, canonicalDir)) return {
|
|
562
974
|
success: false,
|
|
563
975
|
path: agentDir,
|
|
@@ -571,6 +983,7 @@ class Installer {
|
|
|
571
983
|
error: 'Invalid skill name: potential path traversal detected'
|
|
572
984
|
};
|
|
573
985
|
try {
|
|
986
|
+
// Copy mode: directly copy to agent location
|
|
574
987
|
if ('copy' === installMode) {
|
|
575
988
|
installer_ensureDir(agentDir);
|
|
576
989
|
installer_remove(agentDir);
|
|
@@ -581,14 +994,18 @@ class Installer {
|
|
|
581
994
|
mode: 'copy'
|
|
582
995
|
};
|
|
583
996
|
}
|
|
997
|
+
// Symlink mode: copy to canonical location, then create symlink
|
|
584
998
|
installer_ensureDir(canonicalDir);
|
|
585
999
|
installer_remove(canonicalDir);
|
|
586
1000
|
copyDirectory(sourcePath, canonicalDir);
|
|
587
1001
|
const symlinkCreated = await installer_createSymlink(canonicalDir, agentDir);
|
|
588
1002
|
if (!symlinkCreated) {
|
|
1003
|
+
// Symlink failed, fallback to copy
|
|
589
1004
|
try {
|
|
590
1005
|
installer_remove(agentDir);
|
|
591
|
-
} catch {
|
|
1006
|
+
} catch {
|
|
1007
|
+
// Ignore cleanup errors
|
|
1008
|
+
}
|
|
592
1009
|
installer_ensureDir(agentDir);
|
|
593
1010
|
copyDirectory(sourcePath, agentDir);
|
|
594
1011
|
return {
|
|
@@ -614,7 +1031,9 @@ class Installer {
|
|
|
614
1031
|
};
|
|
615
1032
|
}
|
|
616
1033
|
}
|
|
617
|
-
|
|
1034
|
+
/**
|
|
1035
|
+
* Install skill to multiple agents
|
|
1036
|
+
*/ async installToAgents(sourcePath, skillName, targetAgents, options = {}) {
|
|
618
1037
|
const results = new Map();
|
|
619
1038
|
for (const agent of targetAgents){
|
|
620
1039
|
const result = await this.installForAgent(sourcePath, skillName, agent, options);
|
|
@@ -622,24 +1041,33 @@ class Installer {
|
|
|
622
1041
|
}
|
|
623
1042
|
return results;
|
|
624
1043
|
}
|
|
625
|
-
|
|
1044
|
+
/**
|
|
1045
|
+
* Check if skill is installed to specified agent
|
|
1046
|
+
*/ isInstalled(skillName, agentType) {
|
|
626
1047
|
const skillPath = this.getAgentSkillPath(skillName, agentType);
|
|
627
1048
|
return external_node_fs_.existsSync(skillPath);
|
|
628
1049
|
}
|
|
629
|
-
|
|
1050
|
+
/**
|
|
1051
|
+
* Uninstall skill from specified agent
|
|
1052
|
+
*/ uninstallFromAgent(skillName, agentType) {
|
|
630
1053
|
const skillPath = this.getAgentSkillPath(skillName, agentType);
|
|
631
1054
|
if (!external_node_fs_.existsSync(skillPath)) return false;
|
|
632
1055
|
installer_remove(skillPath);
|
|
633
1056
|
return true;
|
|
634
1057
|
}
|
|
635
|
-
|
|
1058
|
+
/**
|
|
1059
|
+
* Uninstall skill from multiple agents
|
|
1060
|
+
*/ uninstallFromAgents(skillName, targetAgents) {
|
|
636
1061
|
const results = new Map();
|
|
637
1062
|
for (const agent of targetAgents)results.set(agent, this.uninstallFromAgent(skillName, agent));
|
|
1063
|
+
// Also delete canonical location
|
|
638
1064
|
const canonicalPath = this.getCanonicalPath(skillName);
|
|
639
1065
|
if (external_node_fs_.existsSync(canonicalPath)) installer_remove(canonicalPath);
|
|
640
1066
|
return results;
|
|
641
1067
|
}
|
|
642
|
-
|
|
1068
|
+
/**
|
|
1069
|
+
* Get all skills installed to specified agent
|
|
1070
|
+
*/ listInstalledSkills(agentType) {
|
|
643
1071
|
const agent = getAgentConfig(agentType);
|
|
644
1072
|
const skillsDir = this.isGlobal ? agent.globalSkillsDir : __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cwd, agent.skillsDir);
|
|
645
1073
|
if (!external_node_fs_.existsSync(skillsDir)) return [];
|
|
@@ -648,49 +1076,95 @@ class Installer {
|
|
|
648
1076
|
}).filter((entry)=>entry.isDirectory() || entry.isSymbolicLink()).map((entry)=>entry.name);
|
|
649
1077
|
}
|
|
650
1078
|
}
|
|
651
|
-
|
|
1079
|
+
/**
|
|
1080
|
+
* CacheManager - Manage global skill cache
|
|
1081
|
+
*
|
|
1082
|
+
* Cache directory structure:
|
|
1083
|
+
* ~/.reskill-cache/
|
|
1084
|
+
* ├── github/ # Shorthand format registry
|
|
1085
|
+
* │ └── user/
|
|
1086
|
+
* │ └── skill/
|
|
1087
|
+
* │ ├── v1.0.0/
|
|
1088
|
+
* │ └── v1.1.0/
|
|
1089
|
+
* ├── github.com/ # Git URL format, using host as directory
|
|
1090
|
+
* │ └── user/
|
|
1091
|
+
* │ └── private-skill/
|
|
1092
|
+
* │ └── v1.0.0/
|
|
1093
|
+
* └── gitlab.company.com/ # Private GitLab instance
|
|
1094
|
+
* └── team/
|
|
1095
|
+
* └── skill/
|
|
1096
|
+
* └── v2.0.0/
|
|
1097
|
+
*
|
|
1098
|
+
* For Git URL format (SSH/HTTPS):
|
|
1099
|
+
* - git@github.com:user/repo.git -> github.com/user/repo/version
|
|
1100
|
+
* - https://gitlab.company.com/team/skill.git -> gitlab.company.com/team/skill/version
|
|
1101
|
+
*/ class CacheManager {
|
|
652
1102
|
cacheDir;
|
|
653
1103
|
constructor(cacheDir){
|
|
654
1104
|
this.cacheDir = cacheDir || getCacheDir();
|
|
655
1105
|
}
|
|
656
|
-
|
|
1106
|
+
/**
|
|
1107
|
+
* Get cache directory
|
|
1108
|
+
*/ getCacheDir() {
|
|
657
1109
|
return this.cacheDir;
|
|
658
1110
|
}
|
|
659
|
-
|
|
1111
|
+
/**
|
|
1112
|
+
* Get skill path in cache
|
|
1113
|
+
*
|
|
1114
|
+
* For different reference formats, cache paths are:
|
|
1115
|
+
* - github:user/repo@v1.0.0 -> ~/.reskill-cache/github/user/repo/v1.0.0
|
|
1116
|
+
* - git@github.com:user/repo.git@v1.0.0 -> ~/.reskill-cache/github.com/user/repo/v1.0.0
|
|
1117
|
+
* - https://gitlab.company.com/team/skill.git@v2.0.0 -> ~/.reskill-cache/gitlab.company.com/team/skill/v2.0.0
|
|
1118
|
+
*/ getSkillCachePath(parsed, version) {
|
|
660
1119
|
return __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cacheDir, parsed.registry, parsed.owner, parsed.repo, version);
|
|
661
1120
|
}
|
|
662
|
-
|
|
1121
|
+
/**
|
|
1122
|
+
* Get cache path (alias for getSkillCachePath)
|
|
1123
|
+
*/ getCachePath(parsed, version) {
|
|
663
1124
|
return this.getSkillCachePath(parsed, version);
|
|
664
1125
|
}
|
|
665
|
-
|
|
1126
|
+
/**
|
|
1127
|
+
* Check if skill is cached
|
|
1128
|
+
*/ isCached(parsed, version) {
|
|
666
1129
|
const cachePath = this.getSkillCachePath(parsed, version);
|
|
667
1130
|
return exists(cachePath) && isDirectory(cachePath);
|
|
668
1131
|
}
|
|
669
|
-
|
|
1132
|
+
/**
|
|
1133
|
+
* Get cached skill
|
|
1134
|
+
*/ async get(parsed, version) {
|
|
670
1135
|
const cachePath = this.getSkillCachePath(parsed, version);
|
|
671
1136
|
if (!this.isCached(parsed, version)) return null;
|
|
1137
|
+
// Read cached commit info
|
|
672
1138
|
const commitFile = __WEBPACK_EXTERNAL_MODULE_node_path__.join(cachePath, '.reskill-commit');
|
|
673
1139
|
let commit = '';
|
|
674
1140
|
try {
|
|
675
1141
|
const fs = await import("node:fs");
|
|
676
1142
|
if (exists(commitFile)) commit = fs.readFileSync(commitFile, 'utf-8').trim();
|
|
677
|
-
} catch {
|
|
1143
|
+
} catch {
|
|
1144
|
+
// Ignore read errors
|
|
1145
|
+
}
|
|
678
1146
|
return {
|
|
679
1147
|
path: cachePath,
|
|
680
1148
|
commit
|
|
681
1149
|
};
|
|
682
1150
|
}
|
|
683
|
-
|
|
1151
|
+
/**
|
|
1152
|
+
* Cache skill from Git repository
|
|
1153
|
+
*/ async cache(repoUrl, parsed, ref, version) {
|
|
684
1154
|
const cachePath = this.getSkillCachePath(parsed, version);
|
|
1155
|
+
// If exists, delete first
|
|
685
1156
|
if (exists(cachePath)) remove(cachePath);
|
|
686
1157
|
ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(cachePath));
|
|
1158
|
+
// Clone repository
|
|
687
1159
|
const tempPath = `${cachePath}.tmp`;
|
|
688
1160
|
remove(tempPath);
|
|
689
1161
|
await clone(repoUrl, tempPath, {
|
|
690
1162
|
depth: 1,
|
|
691
1163
|
branch: ref
|
|
692
1164
|
});
|
|
1165
|
+
// Get commit hash
|
|
693
1166
|
const commit = await getCurrentCommit(tempPath);
|
|
1167
|
+
// If has subPath, only keep subdirectory
|
|
694
1168
|
if (parsed.subPath) {
|
|
695
1169
|
const subDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempPath, parsed.subPath);
|
|
696
1170
|
if (!exists(subDir)) {
|
|
@@ -707,35 +1181,80 @@ class CacheManager {
|
|
|
707
1181
|
'.git'
|
|
708
1182
|
]
|
|
709
1183
|
});
|
|
710
|
-
|
|
711
|
-
|
|
1184
|
+
// Save commit info
|
|
1185
|
+
external_node_fs_.writeFileSync(__WEBPACK_EXTERNAL_MODULE_node_path__.join(cachePath, '.reskill-commit'), commit);
|
|
1186
|
+
// Clean up temp directory
|
|
712
1187
|
remove(tempPath);
|
|
713
1188
|
return {
|
|
714
1189
|
path: cachePath,
|
|
715
1190
|
commit
|
|
716
1191
|
};
|
|
717
1192
|
}
|
|
718
|
-
|
|
1193
|
+
/**
|
|
1194
|
+
* Cache skill from HTTP/OSS URL
|
|
1195
|
+
*
|
|
1196
|
+
* Downloads and extracts an archive from the given URL.
|
|
1197
|
+
* Supports tar.gz, tgz, zip, and tar formats.
|
|
1198
|
+
*
|
|
1199
|
+
* @param url - HTTP/HTTPS URL to download from
|
|
1200
|
+
* @param parsed - Parsed skill reference
|
|
1201
|
+
* @param version - Version string for cache path
|
|
1202
|
+
* @returns Cache path and a hash of the download URL as commit identifier
|
|
1203
|
+
*/ async cacheFromHttp(url, parsed, version) {
|
|
1204
|
+
const cachePath = this.getSkillCachePath(parsed, version);
|
|
1205
|
+
// If exists, delete first
|
|
1206
|
+
if (exists(cachePath)) remove(cachePath);
|
|
1207
|
+
ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(cachePath));
|
|
1208
|
+
// Download and extract to cache path
|
|
1209
|
+
await downloadAndExtract(url, cachePath);
|
|
1210
|
+
// Generate a commit-like identifier from URL and version
|
|
1211
|
+
// This serves as a pseudo-commit for HTTP sources
|
|
1212
|
+
const crypto = await import("node:crypto");
|
|
1213
|
+
const commit = crypto.createHash('sha256').update(`${url}@${version}`).digest('hex').slice(0, 40);
|
|
1214
|
+
// Save commit info
|
|
1215
|
+
external_node_fs_.writeFileSync(__WEBPACK_EXTERNAL_MODULE_node_path__.join(cachePath, '.reskill-commit'), commit);
|
|
1216
|
+
// Also save the source URL for reference
|
|
1217
|
+
external_node_fs_.writeFileSync(__WEBPACK_EXTERNAL_MODULE_node_path__.join(cachePath, '.reskill-source'), url);
|
|
1218
|
+
return {
|
|
1219
|
+
path: cachePath,
|
|
1220
|
+
commit
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Copy from cache to target directory
|
|
1225
|
+
*
|
|
1226
|
+
* Uses the same exclude rules as Installer to ensure consistency:
|
|
1227
|
+
* - DEFAULT_EXCLUDE_FILES (README.md, metadata.json, .reskill-commit)
|
|
1228
|
+
*/ async copyTo(parsed, version, destPath) {
|
|
719
1229
|
const cached = await this.get(parsed, version);
|
|
720
1230
|
if (!cached) throw new Error(`Skill ${parsed.raw} version ${version} not found in cache`);
|
|
1231
|
+
// If target exists, delete first
|
|
721
1232
|
if (exists(destPath)) remove(destPath);
|
|
1233
|
+
// Use same exclude rules as Installer for consistency
|
|
722
1234
|
copyDir(cached.path, destPath, {
|
|
723
1235
|
exclude: DEFAULT_EXCLUDE_FILES
|
|
724
1236
|
});
|
|
725
1237
|
}
|
|
726
|
-
|
|
1238
|
+
/**
|
|
1239
|
+
* Clear cache for specific skill
|
|
1240
|
+
*/ clearSkill(parsed, version) {
|
|
727
1241
|
if (version) {
|
|
728
1242
|
const cachePath = this.getSkillCachePath(parsed, version);
|
|
729
1243
|
remove(cachePath);
|
|
730
1244
|
} else {
|
|
1245
|
+
// Clear all versions
|
|
731
1246
|
const skillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cacheDir, parsed.registry, parsed.owner, parsed.repo);
|
|
732
1247
|
remove(skillDir);
|
|
733
1248
|
}
|
|
734
1249
|
}
|
|
735
|
-
|
|
1250
|
+
/**
|
|
1251
|
+
* Clear all cache
|
|
1252
|
+
*/ clearAll() {
|
|
736
1253
|
remove(this.cacheDir);
|
|
737
1254
|
}
|
|
738
|
-
|
|
1255
|
+
/**
|
|
1256
|
+
* Get cache statistics
|
|
1257
|
+
*/ getStats() {
|
|
739
1258
|
if (!exists(this.cacheDir)) return {
|
|
740
1259
|
totalSkills: 0,
|
|
741
1260
|
registries: []
|
|
@@ -756,11 +1275,20 @@ class CacheManager {
|
|
|
756
1275
|
registries
|
|
757
1276
|
};
|
|
758
1277
|
}
|
|
759
|
-
|
|
1278
|
+
/**
|
|
1279
|
+
* Get the remote commit hash for a specific ref without cloning
|
|
1280
|
+
*
|
|
1281
|
+
* Uses `git ls-remote` to fetch the commit hash efficiently.
|
|
1282
|
+
*
|
|
1283
|
+
* @param repoUrl - Repository URL
|
|
1284
|
+
* @param ref - Git reference (branch, tag, or commit)
|
|
1285
|
+
* @returns Commit hash string
|
|
1286
|
+
*/ async getRemoteCommit(repoUrl, ref) {
|
|
760
1287
|
const { exec } = await import("node:child_process");
|
|
761
1288
|
const { promisify } = await import("node:util");
|
|
762
1289
|
const execAsync = promisify(exec);
|
|
763
1290
|
try {
|
|
1291
|
+
// Try to get commit for the ref
|
|
764
1292
|
const { stdout } = await execAsync(`git ls-remote ${repoUrl} ${ref}`, {
|
|
765
1293
|
encoding: 'utf-8'
|
|
766
1294
|
});
|
|
@@ -768,6 +1296,7 @@ class CacheManager {
|
|
|
768
1296
|
const [commit] = stdout.trim().split('\t');
|
|
769
1297
|
return commit;
|
|
770
1298
|
}
|
|
1299
|
+
// If ref is not found directly, try refs/heads/ and refs/tags/
|
|
771
1300
|
const { stdout: allRefs } = await execAsync(`git ls-remote ${repoUrl}`, {
|
|
772
1301
|
encoding: 'utf-8'
|
|
773
1302
|
});
|
|
@@ -776,28 +1305,62 @@ class CacheManager {
|
|
|
776
1305
|
const [commit, refPath] = line.split('\t');
|
|
777
1306
|
if (refPath === `refs/heads/${ref}` || refPath === `refs/tags/${ref}` || refPath === ref) return commit;
|
|
778
1307
|
}
|
|
1308
|
+
// If still not found, return empty string (will trigger update)
|
|
779
1309
|
return '';
|
|
780
1310
|
} catch {
|
|
1311
|
+
// On error, return empty string to trigger update
|
|
781
1312
|
return '';
|
|
782
1313
|
}
|
|
783
1314
|
}
|
|
784
1315
|
}
|
|
785
|
-
|
|
1316
|
+
// ============================================================================
|
|
1317
|
+
// Constants
|
|
1318
|
+
// ============================================================================
|
|
1319
|
+
/**
|
|
1320
|
+
* Default skills.json configuration template
|
|
1321
|
+
*/ const DEFAULT_SKILLS_JSON = {
|
|
786
1322
|
skills: {},
|
|
1323
|
+
registries: {
|
|
1324
|
+
github: 'https://github.com'
|
|
1325
|
+
},
|
|
787
1326
|
defaults: {
|
|
788
1327
|
installDir: '.skills'
|
|
789
1328
|
}
|
|
790
1329
|
};
|
|
791
|
-
|
|
1330
|
+
/**
|
|
1331
|
+
* Default values for SkillsDefaults fields
|
|
1332
|
+
*/ const DEFAULT_VALUES = {
|
|
792
1333
|
installDir: '.skills',
|
|
793
1334
|
targetAgents: [],
|
|
794
1335
|
installMode: 'symlink'
|
|
795
1336
|
};
|
|
796
|
-
|
|
1337
|
+
/**
|
|
1338
|
+
* Well-known registry URLs
|
|
1339
|
+
*/ const DEFAULT_REGISTRIES = {
|
|
797
1340
|
github: 'https://github.com',
|
|
798
1341
|
gitlab: 'https://gitlab.com'
|
|
799
1342
|
};
|
|
800
|
-
|
|
1343
|
+
// ============================================================================
|
|
1344
|
+
// ConfigLoader Class
|
|
1345
|
+
// ============================================================================
|
|
1346
|
+
/**
|
|
1347
|
+
* ConfigLoader - Load and manage skills.json configuration
|
|
1348
|
+
*
|
|
1349
|
+
* Handles reading, writing, and managing the project's skills.json file.
|
|
1350
|
+
* Provides methods for:
|
|
1351
|
+
* - Loading/saving configuration
|
|
1352
|
+
* - Managing skill dependencies
|
|
1353
|
+
* - Managing default settings (registry, installDir, targetAgents, installMode)
|
|
1354
|
+
*
|
|
1355
|
+
* @example
|
|
1356
|
+
* ```ts
|
|
1357
|
+
* const config = new ConfigLoader();
|
|
1358
|
+
* if (config.exists()) {
|
|
1359
|
+
* const defaults = config.getDefaults();
|
|
1360
|
+
* console.log(defaults.targetAgents);
|
|
1361
|
+
* }
|
|
1362
|
+
* ```
|
|
1363
|
+
*/ class ConfigLoader {
|
|
801
1364
|
projectRoot;
|
|
802
1365
|
configPath;
|
|
803
1366
|
config = null;
|
|
@@ -805,20 +1368,38 @@ class ConfigLoader {
|
|
|
805
1368
|
this.projectRoot = projectRoot ?? process.cwd();
|
|
806
1369
|
this.configPath = getSkillsJsonPath(this.projectRoot);
|
|
807
1370
|
}
|
|
808
|
-
|
|
1371
|
+
// ==========================================================================
|
|
1372
|
+
// Path Accessors
|
|
1373
|
+
// ==========================================================================
|
|
1374
|
+
/**
|
|
1375
|
+
* Get project root directory
|
|
1376
|
+
*/ getProjectRoot() {
|
|
809
1377
|
return this.projectRoot;
|
|
810
1378
|
}
|
|
811
|
-
|
|
1379
|
+
/**
|
|
1380
|
+
* Get configuration file path
|
|
1381
|
+
*/ getConfigPath() {
|
|
812
1382
|
return this.configPath;
|
|
813
1383
|
}
|
|
814
|
-
|
|
1384
|
+
/**
|
|
1385
|
+
* Get installation directory (resolved absolute path)
|
|
1386
|
+
*/ getInstallDir() {
|
|
815
1387
|
const { installDir } = this.getDefaults();
|
|
816
1388
|
return __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.projectRoot, installDir);
|
|
817
1389
|
}
|
|
818
|
-
|
|
1390
|
+
// ==========================================================================
|
|
1391
|
+
// File Operations
|
|
1392
|
+
// ==========================================================================
|
|
1393
|
+
/**
|
|
1394
|
+
* Check if configuration file exists
|
|
1395
|
+
*/ exists() {
|
|
819
1396
|
return exists(this.configPath);
|
|
820
1397
|
}
|
|
821
|
-
|
|
1398
|
+
/**
|
|
1399
|
+
* Load configuration from file
|
|
1400
|
+
*
|
|
1401
|
+
* @throws Error if file doesn't exist or is invalid JSON
|
|
1402
|
+
*/ load() {
|
|
822
1403
|
if (this.config) return this.config;
|
|
823
1404
|
if (!this.exists()) throw new Error(`skills.json not found in ${this.projectRoot}. Run 'reskill init' first.`);
|
|
824
1405
|
try {
|
|
@@ -828,26 +1409,46 @@ class ConfigLoader {
|
|
|
828
1409
|
throw new Error(`Failed to parse skills.json: ${error.message}`);
|
|
829
1410
|
}
|
|
830
1411
|
}
|
|
831
|
-
|
|
1412
|
+
/**
|
|
1413
|
+
* Reload configuration from file (ignores cache)
|
|
1414
|
+
*/ reload() {
|
|
832
1415
|
this.config = null;
|
|
833
1416
|
return this.load();
|
|
834
1417
|
}
|
|
835
|
-
|
|
1418
|
+
/**
|
|
1419
|
+
* Save configuration to file
|
|
1420
|
+
*
|
|
1421
|
+
* @param config - Configuration to save (uses cached config if not provided)
|
|
1422
|
+
* @throws Error if no configuration to save
|
|
1423
|
+
*/ save(config) {
|
|
836
1424
|
const toSave = config ?? this.config;
|
|
837
1425
|
if (!toSave) throw new Error('No config to save');
|
|
838
1426
|
writeJson(this.configPath, toSave);
|
|
839
1427
|
this.config = toSave;
|
|
840
1428
|
}
|
|
841
|
-
|
|
1429
|
+
/**
|
|
1430
|
+
* Ensure skills.json exists, create with defaults if not
|
|
1431
|
+
*
|
|
1432
|
+
* @returns true if file was created, false if it already existed
|
|
1433
|
+
*/ ensureExists() {
|
|
842
1434
|
if (this.exists()) return false;
|
|
843
1435
|
this.create();
|
|
844
1436
|
return true;
|
|
845
1437
|
}
|
|
846
|
-
|
|
1438
|
+
/**
|
|
1439
|
+
* Create new configuration file with defaults
|
|
1440
|
+
*
|
|
1441
|
+
* @param options - Optional overrides for default configuration
|
|
1442
|
+
*/ create(options) {
|
|
847
1443
|
const config = {
|
|
848
1444
|
...DEFAULT_SKILLS_JSON,
|
|
849
1445
|
...options,
|
|
850
1446
|
skills: options?.skills ?? {},
|
|
1447
|
+
// Deep copy registries to avoid mutating the default object
|
|
1448
|
+
registries: {
|
|
1449
|
+
...DEFAULT_SKILLS_JSON.registries,
|
|
1450
|
+
...options?.registries
|
|
1451
|
+
},
|
|
851
1452
|
defaults: {
|
|
852
1453
|
...DEFAULT_SKILLS_JSON.defaults,
|
|
853
1454
|
...options?.defaults
|
|
@@ -856,7 +1457,15 @@ class ConfigLoader {
|
|
|
856
1457
|
this.save(config);
|
|
857
1458
|
return config;
|
|
858
1459
|
}
|
|
859
|
-
|
|
1460
|
+
// ==========================================================================
|
|
1461
|
+
// Defaults Management
|
|
1462
|
+
// ==========================================================================
|
|
1463
|
+
/**
|
|
1464
|
+
* Get default configuration values
|
|
1465
|
+
*
|
|
1466
|
+
* Returns a complete defaults object with all fields populated.
|
|
1467
|
+
* Uses stored values if available, falls back to defaults.
|
|
1468
|
+
*/ getDefaults() {
|
|
860
1469
|
const config = this.getConfigOrDefault();
|
|
861
1470
|
const storedDefaults = config.defaults ?? {};
|
|
862
1471
|
return {
|
|
@@ -865,7 +1474,13 @@ class ConfigLoader {
|
|
|
865
1474
|
installMode: storedDefaults.installMode ?? DEFAULT_VALUES.installMode
|
|
866
1475
|
};
|
|
867
1476
|
}
|
|
868
|
-
|
|
1477
|
+
/**
|
|
1478
|
+
* Update default configuration values
|
|
1479
|
+
*
|
|
1480
|
+
* Merges the provided updates with existing defaults and saves to file.
|
|
1481
|
+
*
|
|
1482
|
+
* @param updates - Partial defaults to merge
|
|
1483
|
+
*/ updateDefaults(updates) {
|
|
869
1484
|
this.ensureConfigLoaded();
|
|
870
1485
|
if (this.config) {
|
|
871
1486
|
this.config.defaults = {
|
|
@@ -875,20 +1490,176 @@ class ConfigLoader {
|
|
|
875
1490
|
this.save();
|
|
876
1491
|
}
|
|
877
1492
|
}
|
|
878
|
-
|
|
1493
|
+
// ==========================================================================
|
|
1494
|
+
// Registry Management
|
|
1495
|
+
// ==========================================================================
|
|
1496
|
+
/**
|
|
1497
|
+
* Get registry URL by name
|
|
1498
|
+
*
|
|
1499
|
+
* Resolution order:
|
|
1500
|
+
* 1. Custom registries defined in skills.json
|
|
1501
|
+
* 2. Well-known registries (github, gitlab)
|
|
1502
|
+
* 3. Assumes it's a custom domain (https://{registryName})
|
|
1503
|
+
*/ getRegistryUrl(registryName) {
|
|
879
1504
|
const config = this.getConfigOrDefault();
|
|
1505
|
+
// Check custom registries
|
|
880
1506
|
if (config.registries?.[registryName]) return config.registries[registryName];
|
|
1507
|
+
// Check well-known registries
|
|
881
1508
|
if (DEFAULT_REGISTRIES[registryName]) return DEFAULT_REGISTRIES[registryName];
|
|
1509
|
+
// Assume it's a custom domain
|
|
882
1510
|
return `https://${registryName}`;
|
|
883
1511
|
}
|
|
884
|
-
|
|
1512
|
+
/**
|
|
1513
|
+
* Find registry name for a given URL
|
|
1514
|
+
*
|
|
1515
|
+
* Reverse lookup: finds which registry (if any) matches the URL.
|
|
1516
|
+
* Custom registries are checked first, then well-known registries.
|
|
1517
|
+
*
|
|
1518
|
+
* @param url - The URL to match (e.g., "https://gitlab.company.com/team/tool")
|
|
1519
|
+
* @returns The registry name if found, or undefined
|
|
1520
|
+
*/ findRegistryForUrl(url) {
|
|
1521
|
+
const config = this.getConfigOrDefault();
|
|
1522
|
+
// Normalize URL - remove trailing slash
|
|
1523
|
+
const normalizedUrl = url.replace(/\/$/, '');
|
|
1524
|
+
// Check custom registries first (higher priority)
|
|
1525
|
+
if (config.registries) for (const [name, registryUrl] of Object.entries(config.registries)){
|
|
1526
|
+
const normalizedRegistryUrl = registryUrl.replace(/\/$/, '');
|
|
1527
|
+
if (normalizedUrl.startsWith(normalizedRegistryUrl)) return name;
|
|
1528
|
+
}
|
|
1529
|
+
// Check well-known registries
|
|
1530
|
+
for (const [name, registryUrl] of Object.entries(DEFAULT_REGISTRIES)){
|
|
1531
|
+
const normalizedRegistryUrl = registryUrl.replace(/\/$/, '');
|
|
1532
|
+
if (normalizedUrl.startsWith(normalizedRegistryUrl)) return name;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Normalize a skill reference to use registry shorthand if possible
|
|
1537
|
+
*
|
|
1538
|
+
* Converts full URLs to registry format when they match a configured registry.
|
|
1539
|
+
* E.g., "https://gitlab.company.com/team/tool@v1.0.0" → "internal:team/tool@v1.0.0"
|
|
1540
|
+
* (if "internal": "https://gitlab.company.com" is configured)
|
|
1541
|
+
*
|
|
1542
|
+
* @param ref - The skill reference to normalize
|
|
1543
|
+
* @returns Normalized reference using registry shorthand, or original if no match
|
|
1544
|
+
*/ normalizeSkillRef(ref) {
|
|
1545
|
+
// Check if it's an SSH URL (git@...) - must check first as it contains ':'
|
|
1546
|
+
if (ref.startsWith('git@')) return this.normalizeGitSshUrl(ref);
|
|
1547
|
+
// Check if it's an HTTPS URL
|
|
1548
|
+
if (ref.startsWith('https://') || ref.startsWith('http://')) return this.normalizeHttpsUrl(ref);
|
|
1549
|
+
// Check if it's already in registry format (contains : but not a URL)
|
|
1550
|
+
// At this point we've excluded git@ and http(s):// URLs
|
|
1551
|
+
ref.includes(':');
|
|
1552
|
+
return ref;
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Normalize an HTTPS URL to registry format
|
|
1556
|
+
*/ normalizeHttpsUrl(ref) {
|
|
1557
|
+
// Extract version part if present
|
|
1558
|
+
let url = ref;
|
|
1559
|
+
let version = '';
|
|
1560
|
+
// Handle .git suffix with version
|
|
1561
|
+
const gitVersionMatch = ref.match(/^(.+\.git)(@.+)$/);
|
|
1562
|
+
if (gitVersionMatch) {
|
|
1563
|
+
url = gitVersionMatch[1];
|
|
1564
|
+
version = gitVersionMatch[2];
|
|
1565
|
+
} else {
|
|
1566
|
+
// Handle URL without .git suffix
|
|
1567
|
+
const versionMatch = ref.match(/^(.+?)(@[^@]+)$/);
|
|
1568
|
+
if (versionMatch && !versionMatch[1].includes('@')) {
|
|
1569
|
+
url = versionMatch[1];
|
|
1570
|
+
version = versionMatch[2];
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
// Find matching registry
|
|
1574
|
+
const registryName = this.findRegistryForUrl(url);
|
|
1575
|
+
if (!registryName) return ref;
|
|
1576
|
+
const registryUrl = this.getRegistryUrl(registryName).replace(/\/$/, '');
|
|
1577
|
+
// Extract the path after registry URL
|
|
1578
|
+
let path = url.replace(registryUrl, '').replace(/^\//, '');
|
|
1579
|
+
// Remove .git suffix if present
|
|
1580
|
+
path = path.replace(/\.git$/, '');
|
|
1581
|
+
if (!path) return ref;
|
|
1582
|
+
return `${registryName}:${path}${version}`;
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Normalize a Git SSH URL to registry format
|
|
1586
|
+
*/ normalizeGitSshUrl(ref) {
|
|
1587
|
+
// Parse: git@host:owner/repo.git[@version] or git@host:owner/repo[@version]
|
|
1588
|
+
// The .git suffix and @version are both optional
|
|
1589
|
+
// Use greedy match for repoPath (.+) to ensure .git is captured as part of the path,
|
|
1590
|
+
// then explicitly remove it. This avoids issues with non-greedy matching and optional groups.
|
|
1591
|
+
const match = ref.match(/^git@([^:]+):(.+?)(@[^@]+)?$/);
|
|
1592
|
+
if (!match) return ref;
|
|
1593
|
+
const [, host, rawRepoPath, version = ''] = match;
|
|
1594
|
+
// Remove .git suffix if present
|
|
1595
|
+
const repoPath = rawRepoPath.replace(/\.git$/, '');
|
|
1596
|
+
const testUrl = `https://${host}`;
|
|
1597
|
+
// Find matching registry
|
|
1598
|
+
const registryName = this.findRegistryForUrl(testUrl);
|
|
1599
|
+
if (!registryName) return ref;
|
|
1600
|
+
return `${registryName}:${repoPath}${version}`;
|
|
1601
|
+
}
|
|
1602
|
+
// ==========================================================================
|
|
1603
|
+
// Skills Management
|
|
1604
|
+
// ==========================================================================
|
|
1605
|
+
/**
|
|
1606
|
+
* Add skill to configuration
|
|
1607
|
+
*
|
|
1608
|
+
* Also auto-adds the registry to the registries field if it's a well-known registry.
|
|
1609
|
+
*/ addSkill(name, ref) {
|
|
885
1610
|
this.ensureConfigLoaded();
|
|
886
1611
|
if (this.config) {
|
|
887
1612
|
this.config.skills[name] = ref;
|
|
1613
|
+
// Auto-add registry if it's a well-known registry
|
|
1614
|
+
const registryName = this.extractRegistryFromRef(ref);
|
|
1615
|
+
if (registryName && DEFAULT_REGISTRIES[registryName]) this.addRegistry(registryName, DEFAULT_REGISTRIES[registryName]);
|
|
888
1616
|
this.save();
|
|
889
1617
|
}
|
|
890
1618
|
}
|
|
891
|
-
|
|
1619
|
+
/**
|
|
1620
|
+
* Add registry to configuration
|
|
1621
|
+
*
|
|
1622
|
+
* Only adds if the registry doesn't already exist.
|
|
1623
|
+
* Note: This method requires config to be loaded first via load() or create().
|
|
1624
|
+
* If config is not loaded, this method is a no-op (silent return) since it's
|
|
1625
|
+
* typically called as a side effect of addSkill() which handles config loading.
|
|
1626
|
+
*
|
|
1627
|
+
* @param name - Registry name (e.g., 'github', 'gitlab', 'internal')
|
|
1628
|
+
* @param url - Registry URL (e.g., 'https://github.com')
|
|
1629
|
+
*/ addRegistry(name, url) {
|
|
1630
|
+
if (!this.config) // Config not loaded - this is expected when called before load()/create()
|
|
1631
|
+
// Callers like addSkill() ensure config is loaded before calling this
|
|
1632
|
+
return;
|
|
1633
|
+
if (!this.config.registries) this.config.registries = {};
|
|
1634
|
+
// Don't overwrite existing registries
|
|
1635
|
+
if (!this.config.registries[name]) this.config.registries[name] = url;
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Extract registry name from a skill reference
|
|
1639
|
+
*
|
|
1640
|
+
* @example
|
|
1641
|
+
* extractRegistryFromRef('github:user/repo@v1.0.0') // 'github'
|
|
1642
|
+
* extractRegistryFromRef('gitlab:user/repo') // 'gitlab'
|
|
1643
|
+
* extractRegistryFromRef('https://github.com/user/repo') // undefined
|
|
1644
|
+
*/ extractRegistryFromRef(ref) {
|
|
1645
|
+
// Check for registry format: registry:path[@version]
|
|
1646
|
+
const match = ref.match(/^([a-zA-Z][a-zA-Z0-9-]*):(.+)$/);
|
|
1647
|
+
if (match) {
|
|
1648
|
+
const registryName = match[1];
|
|
1649
|
+
// Exclude URL protocols (http, https, git, ssh)
|
|
1650
|
+
if (![
|
|
1651
|
+
'http',
|
|
1652
|
+
'https',
|
|
1653
|
+
'git',
|
|
1654
|
+
'ssh'
|
|
1655
|
+
].includes(registryName.toLowerCase())) return registryName;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Remove skill from configuration
|
|
1660
|
+
*
|
|
1661
|
+
* @returns true if skill was removed, false if it didn't exist
|
|
1662
|
+
*/ removeSkill(name) {
|
|
892
1663
|
this.ensureConfigLoaded();
|
|
893
1664
|
if (this.config?.skills[name]) {
|
|
894
1665
|
delete this.config.skills[name];
|
|
@@ -897,7 +1668,9 @@ class ConfigLoader {
|
|
|
897
1668
|
}
|
|
898
1669
|
return false;
|
|
899
1670
|
}
|
|
900
|
-
|
|
1671
|
+
/**
|
|
1672
|
+
* Get all skills as a shallow copy
|
|
1673
|
+
*/ getSkills() {
|
|
901
1674
|
if (!this.config) {
|
|
902
1675
|
if (!this.exists()) return {};
|
|
903
1676
|
this.load();
|
|
@@ -906,41 +1679,116 @@ class ConfigLoader {
|
|
|
906
1679
|
...this.config?.skills
|
|
907
1680
|
};
|
|
908
1681
|
}
|
|
909
|
-
|
|
1682
|
+
/**
|
|
1683
|
+
* Check if skill exists in configuration
|
|
1684
|
+
*/ hasSkill(name) {
|
|
910
1685
|
const skills = this.getSkills();
|
|
911
1686
|
return name in skills;
|
|
912
1687
|
}
|
|
913
|
-
|
|
1688
|
+
/**
|
|
1689
|
+
* Get skill reference by name
|
|
1690
|
+
*/ getSkillRef(name) {
|
|
914
1691
|
const skills = this.getSkills();
|
|
915
1692
|
return skills[name];
|
|
916
1693
|
}
|
|
917
|
-
|
|
1694
|
+
// ==========================================================================
|
|
1695
|
+
// Private Helpers
|
|
1696
|
+
// ==========================================================================
|
|
1697
|
+
/**
|
|
1698
|
+
* Get loaded config or default (does not throw)
|
|
1699
|
+
*/ getConfigOrDefault() {
|
|
918
1700
|
if (this.config) return this.config;
|
|
919
1701
|
if (this.exists()) return this.load();
|
|
920
1702
|
return DEFAULT_SKILLS_JSON;
|
|
921
1703
|
}
|
|
922
|
-
|
|
1704
|
+
/**
|
|
1705
|
+
* Ensure config is loaded into memory
|
|
1706
|
+
*/ ensureConfigLoaded() {
|
|
923
1707
|
if (!this.config) this.load();
|
|
924
1708
|
}
|
|
925
1709
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1710
|
+
/**
|
|
1711
|
+
* GitResolver - Parse skill references and versions
|
|
1712
|
+
*
|
|
1713
|
+
* Reference formats:
|
|
1714
|
+
* Full: <registry>:<owner>/<repo>@<version>
|
|
1715
|
+
* Short: <owner>/<repo>@<version>
|
|
1716
|
+
* Git URL: git@github.com:user/repo.git[@version]
|
|
1717
|
+
* HTTPS: https://github.com/user/repo.git[@version]
|
|
1718
|
+
*
|
|
1719
|
+
* Version formats:
|
|
1720
|
+
* - @v1.0.0 Exact version
|
|
1721
|
+
* - @latest Latest tag
|
|
1722
|
+
* - @^2.0.0 Semver range
|
|
1723
|
+
* - @branch:dev Branch
|
|
1724
|
+
* - @commit:abc Commit hash
|
|
1725
|
+
* - (none) Default branch
|
|
1726
|
+
*/ class GitResolver {
|
|
1727
|
+
defaultRegistry;
|
|
1728
|
+
customRegistries;
|
|
1729
|
+
registryResolver;
|
|
1730
|
+
/**
|
|
1731
|
+
* Create a GitResolver instance
|
|
1732
|
+
*
|
|
1733
|
+
* @param defaultRegistry - Default registry name (defaults to 'github')
|
|
1734
|
+
* @param registries - Custom registry configuration (name -> URL mapping)
|
|
1735
|
+
* @param registryResolver - Optional custom registry resolver function
|
|
1736
|
+
*/ constructor(defaultRegistry = 'github', registries, registryResolver){
|
|
1737
|
+
this.defaultRegistry = defaultRegistry;
|
|
1738
|
+
this.customRegistries = registries ?? {};
|
|
1739
|
+
this.registryResolver = registryResolver;
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Get registry URL by name
|
|
1743
|
+
*
|
|
1744
|
+
* Resolution order:
|
|
1745
|
+
* 1. Custom registry resolver function (if provided)
|
|
1746
|
+
* 2. Custom registries from configuration
|
|
1747
|
+
* 3. Well-known registries (github, gitlab)
|
|
1748
|
+
* 4. Assumes it's a custom domain (https://{registryName})
|
|
1749
|
+
*/ getRegistryUrl(registryName) {
|
|
1750
|
+
// Use custom resolver if provided
|
|
1751
|
+
if (this.registryResolver) return this.registryResolver(registryName);
|
|
1752
|
+
// Check custom registries from configuration
|
|
1753
|
+
if (this.customRegistries[registryName]) return this.customRegistries[registryName];
|
|
1754
|
+
// Check well-known registries
|
|
1755
|
+
if (DEFAULT_REGISTRIES[registryName]) return DEFAULT_REGISTRIES[registryName];
|
|
1756
|
+
// Assume it's a custom domain
|
|
1757
|
+
return `https://${registryName}`;
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Parse skill reference string
|
|
1761
|
+
*
|
|
1762
|
+
* Supported formats:
|
|
1763
|
+
* - Short: owner/repo[@version]
|
|
1764
|
+
* - Full: registry:owner/repo[@version]
|
|
1765
|
+
* - SSH URL: git@github.com:user/repo.git[@version]
|
|
1766
|
+
* - HTTPS URL: https://github.com/user/repo.git[@version]
|
|
1767
|
+
* - Monorepo: git@github.com:org/repo.git/subpath[@version]
|
|
1768
|
+
*/ parseRef(ref) {
|
|
929
1769
|
const raw = ref;
|
|
1770
|
+
// First check if it's a Git URL (SSH, HTTPS, git://)
|
|
1771
|
+
// For Git URLs, need special handling for version separator
|
|
1772
|
+
// Format: git@host:user/repo.git[@version] or git@host:user/repo.git/subpath[@version]
|
|
930
1773
|
if (isGitUrl(ref)) return this.parseGitUrlRef(ref);
|
|
1774
|
+
// Standard format parsing for non-Git URLs
|
|
931
1775
|
let remaining = ref;
|
|
932
1776
|
let registry = this.defaultRegistry;
|
|
933
1777
|
let version;
|
|
1778
|
+
// Check for registry prefix (github:, gitlab:, custom.com:)
|
|
934
1779
|
const registryMatch = remaining.match(/^([a-zA-Z0-9.-]+):(.+)$/);
|
|
935
1780
|
if (registryMatch) {
|
|
936
1781
|
registry = registryMatch[1];
|
|
937
1782
|
remaining = registryMatch[2];
|
|
938
1783
|
}
|
|
1784
|
+
// Separate version part
|
|
939
1785
|
const atIndex = remaining.lastIndexOf('@');
|
|
940
1786
|
if (atIndex > 0) {
|
|
941
1787
|
version = remaining.slice(atIndex + 1);
|
|
942
1788
|
remaining = remaining.slice(0, atIndex);
|
|
943
1789
|
}
|
|
1790
|
+
// Parse owner/repo and possible subPath
|
|
1791
|
+
// E.g.: user/repo or org/monorepo/skills/pdf
|
|
944
1792
|
const parts = remaining.split('/');
|
|
945
1793
|
if (parts.length < 2) throw new Error(`Invalid skill reference: ${ref}. Expected format: owner/repo[@version]`);
|
|
946
1794
|
const owner = parts[0];
|
|
@@ -955,16 +1803,30 @@ class GitResolver {
|
|
|
955
1803
|
raw
|
|
956
1804
|
};
|
|
957
1805
|
}
|
|
958
|
-
|
|
1806
|
+
/**
|
|
1807
|
+
* Parse Git URL format reference
|
|
1808
|
+
*
|
|
1809
|
+
* Supported formats:
|
|
1810
|
+
* - git@github.com:user/repo.git
|
|
1811
|
+
* - git@github.com:user/repo.git@v1.0.0
|
|
1812
|
+
* - git@github.com:user/repo.git/subpath@v1.0.0
|
|
1813
|
+
* - https://github.com/user/repo.git
|
|
1814
|
+
* - https://github.com/user/repo.git@v1.0.0
|
|
1815
|
+
* - https://github.com/user/repo/tree/branch/path (GitHub web URL)
|
|
1816
|
+
*/ parseGitUrlRef(ref) {
|
|
959
1817
|
const raw = ref;
|
|
960
1818
|
let gitUrl = ref;
|
|
961
1819
|
let version;
|
|
962
1820
|
let subPath;
|
|
1821
|
+
// Check for GitHub/GitLab web URL format: https://github.com/user/repo/tree/branch/path
|
|
963
1822
|
const webUrlMatch = ref.match(/^(https?:\/\/[^/]+)\/([^/]+)\/([^/]+)\/(tree|blob|raw)\/([^/]+)(?:\/(.+))?$/);
|
|
964
1823
|
if (webUrlMatch) {
|
|
965
1824
|
const [, baseUrl, owner, repo, , branch, path] = webUrlMatch;
|
|
1825
|
+
// Build standard Git URL
|
|
966
1826
|
gitUrl = `${baseUrl}/${owner}/${repo}.git`;
|
|
1827
|
+
// Extract branch as version
|
|
967
1828
|
version = `branch:${branch}`;
|
|
1829
|
+
// Extract subpath
|
|
968
1830
|
subPath = path;
|
|
969
1831
|
return {
|
|
970
1832
|
registry: new URL(baseUrl).hostname,
|
|
@@ -976,25 +1838,33 @@ class GitResolver {
|
|
|
976
1838
|
gitUrl
|
|
977
1839
|
};
|
|
978
1840
|
}
|
|
1841
|
+
// For URLs ending with .git, first check for /subpath@version or @version
|
|
1842
|
+
// Format: url.git/subpath@version or url.git@version
|
|
979
1843
|
const gitSuffixIndex = ref.indexOf('.git');
|
|
980
1844
|
if (-1 !== gitSuffixIndex) {
|
|
981
1845
|
const afterGit = ref.slice(gitSuffixIndex + 4);
|
|
982
1846
|
if (afterGit) {
|
|
1847
|
+
// Check version (@version)
|
|
983
1848
|
const atIndex = afterGit.lastIndexOf('@');
|
|
984
1849
|
if (-1 !== atIndex) {
|
|
985
1850
|
version = afterGit.slice(atIndex + 1);
|
|
986
1851
|
const pathPart = afterGit.slice(0, atIndex);
|
|
987
1852
|
if (pathPart.startsWith('/')) subPath = pathPart.slice(1);
|
|
988
1853
|
} else if (afterGit.startsWith('/')) subPath = afterGit.slice(1);
|
|
1854
|
+
// Extract clean Git URL (without subpath and version)
|
|
989
1855
|
gitUrl = ref.slice(0, gitSuffixIndex + 4);
|
|
990
1856
|
}
|
|
991
1857
|
} else {
|
|
1858
|
+
// URL without .git suffix, try to separate version
|
|
992
1859
|
const atIndex = ref.lastIndexOf('@');
|
|
1860
|
+
// For SSH URL, @ at the beginning is normal (git@...), need to skip
|
|
993
1861
|
if (atIndex > 4) {
|
|
1862
|
+
// Make sure it's not the @ in git@host
|
|
994
1863
|
version = ref.slice(atIndex + 1);
|
|
995
1864
|
gitUrl = ref.slice(0, atIndex);
|
|
996
1865
|
}
|
|
997
1866
|
}
|
|
1867
|
+
// Parse Git URL to get host, owner, repo
|
|
998
1868
|
const parsed = parseGitUrl(gitUrl);
|
|
999
1869
|
if (!parsed) throw new Error(`Invalid Git URL: ${ref}. Expected format: git@host:owner/repo.git or https://host/owner/repo.git`);
|
|
1000
1870
|
return {
|
|
@@ -1007,53 +1877,73 @@ class GitResolver {
|
|
|
1007
1877
|
gitUrl
|
|
1008
1878
|
};
|
|
1009
1879
|
}
|
|
1010
|
-
|
|
1880
|
+
/**
|
|
1881
|
+
* Parse version specification
|
|
1882
|
+
*/ parseVersion(versionSpec) {
|
|
1011
1883
|
if (!versionSpec) return {
|
|
1012
1884
|
type: 'branch',
|
|
1013
1885
|
value: 'main',
|
|
1014
1886
|
raw: ''
|
|
1015
1887
|
};
|
|
1016
1888
|
const raw = versionSpec;
|
|
1889
|
+
// latest
|
|
1017
1890
|
if ('latest' === versionSpec) return {
|
|
1018
1891
|
type: 'latest',
|
|
1019
1892
|
value: 'latest',
|
|
1020
1893
|
raw
|
|
1021
1894
|
};
|
|
1895
|
+
// branch:xxx
|
|
1022
1896
|
if (versionSpec.startsWith('branch:')) return {
|
|
1023
1897
|
type: 'branch',
|
|
1024
1898
|
value: versionSpec.slice(7),
|
|
1025
1899
|
raw
|
|
1026
1900
|
};
|
|
1901
|
+
// commit:xxx
|
|
1027
1902
|
if (versionSpec.startsWith('commit:')) return {
|
|
1028
1903
|
type: 'commit',
|
|
1029
1904
|
value: versionSpec.slice(7),
|
|
1030
1905
|
raw
|
|
1031
1906
|
};
|
|
1907
|
+
// semver range (^, ~, >, <, etc.)
|
|
1032
1908
|
if (/^[\^~><]/.test(versionSpec)) return {
|
|
1033
1909
|
type: 'range',
|
|
1034
1910
|
value: versionSpec,
|
|
1035
1911
|
raw
|
|
1036
1912
|
};
|
|
1913
|
+
// exact version (v1.0.0 or 1.0.0)
|
|
1037
1914
|
return {
|
|
1038
1915
|
type: 'exact',
|
|
1039
1916
|
value: versionSpec,
|
|
1040
1917
|
raw
|
|
1041
1918
|
};
|
|
1042
1919
|
}
|
|
1043
|
-
|
|
1920
|
+
/**
|
|
1921
|
+
* Build repository URL
|
|
1922
|
+
*
|
|
1923
|
+
* If parsed contains gitUrl, return it directly;
|
|
1924
|
+
* Otherwise build HTTPS URL from registry and owner/repo
|
|
1925
|
+
*/ buildRepoUrl(parsed) {
|
|
1926
|
+
// If has complete Git URL, return directly
|
|
1044
1927
|
if (parsed.gitUrl) return parsed.gitUrl;
|
|
1045
|
-
|
|
1928
|
+
// Use our registry resolver to get the base URL
|
|
1929
|
+
const baseUrl = this.getRegistryUrl(parsed.registry);
|
|
1930
|
+
return `${baseUrl}/${parsed.owner}/${parsed.repo}`;
|
|
1046
1931
|
}
|
|
1047
|
-
|
|
1932
|
+
/**
|
|
1933
|
+
* Resolve version and get specific ref (tag name or commit)
|
|
1934
|
+
*/ async resolveVersion(repoUrl, versionSpec) {
|
|
1048
1935
|
switch(versionSpec.type){
|
|
1049
1936
|
case 'exact':
|
|
1937
|
+
// Use specified tag directly
|
|
1050
1938
|
return {
|
|
1051
1939
|
ref: versionSpec.value
|
|
1052
1940
|
};
|
|
1053
1941
|
case 'latest':
|
|
1054
1942
|
{
|
|
1943
|
+
// Get latest tag
|
|
1055
1944
|
const latestTag = await getLatestTag(repoUrl);
|
|
1056
1945
|
if (!latestTag) {
|
|
1946
|
+
// No tag, use default branch
|
|
1057
1947
|
const defaultBranch = await getDefaultBranch(repoUrl);
|
|
1058
1948
|
return {
|
|
1059
1949
|
ref: defaultBranch
|
|
@@ -1066,12 +1956,14 @@ class GitResolver {
|
|
|
1066
1956
|
}
|
|
1067
1957
|
case 'range':
|
|
1068
1958
|
{
|
|
1959
|
+
// Get all tags, find latest version satisfying semver range
|
|
1069
1960
|
const tags = await getRemoteTags(repoUrl);
|
|
1070
1961
|
const matchingTags = tags.filter((tag)=>{
|
|
1071
1962
|
const version = tag.name.replace(/^v/, '');
|
|
1072
1963
|
return __WEBPACK_EXTERNAL_MODULE_semver__.satisfies(version, versionSpec.value);
|
|
1073
1964
|
});
|
|
1074
1965
|
if (0 === matchingTags.length) throw new Error(`No version found matching ${versionSpec.raw} for ${repoUrl}`);
|
|
1966
|
+
// Sort by version, get latest
|
|
1075
1967
|
matchingTags.sort((a, b)=>{
|
|
1076
1968
|
const aVer = a.name.replace(/^v/, '');
|
|
1077
1969
|
const bVer = b.name.replace(/^v/, '');
|
|
@@ -1095,7 +1987,9 @@ class GitResolver {
|
|
|
1095
1987
|
throw new Error(`Unknown version type: ${versionSpec.type}`);
|
|
1096
1988
|
}
|
|
1097
1989
|
}
|
|
1098
|
-
|
|
1990
|
+
/**
|
|
1991
|
+
* Full resolution: from reference string to clone-ready information
|
|
1992
|
+
*/ async resolve(ref) {
|
|
1099
1993
|
const parsed = this.parseRef(ref);
|
|
1100
1994
|
const repoUrl = this.buildRepoUrl(parsed);
|
|
1101
1995
|
const versionSpec = this.parseVersion(parsed.version);
|
|
@@ -1108,8 +2002,204 @@ class GitResolver {
|
|
|
1108
2002
|
};
|
|
1109
2003
|
}
|
|
1110
2004
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
2005
|
+
/**
|
|
2006
|
+
* HttpResolver - Parse HTTP/OSS URLs and resolve skill references
|
|
2007
|
+
*
|
|
2008
|
+
* Supported URL formats:
|
|
2009
|
+
* - https://bucket.oss-cn-hangzhou.aliyuncs.com/path/to/skill.tar.gz
|
|
2010
|
+
* - https://bucket.s3.amazonaws.com/skills/skill-v1.0.0.tar.gz
|
|
2011
|
+
* - https://example.com/skills/my-skill.zip
|
|
2012
|
+
* - http://localhost:8080/skills/test-skill.tar.gz
|
|
2013
|
+
*
|
|
2014
|
+
* The resolver extracts:
|
|
2015
|
+
* - Skill name from filename (removes version suffix and archive extension)
|
|
2016
|
+
* - Version from filename pattern (e.g., skill-v1.0.0.tar.gz -> v1.0.0)
|
|
2017
|
+
* - Archive format for extraction
|
|
2018
|
+
*/ class HttpResolver {
|
|
2019
|
+
/**
|
|
2020
|
+
* Check if a reference is an HTTP/OSS URL
|
|
2021
|
+
*
|
|
2022
|
+
* Returns true for:
|
|
2023
|
+
* - http:// or https:// URLs
|
|
2024
|
+
* - Explicit oss:// or s3:// protocol URLs
|
|
2025
|
+
*/ static isHttpUrl(ref) {
|
|
2026
|
+
// Remove version suffix for checking (e.g., url@v1.0.0)
|
|
2027
|
+
const urlPart = ref.split('@')[0];
|
|
2028
|
+
return urlPart.startsWith('http://') || urlPart.startsWith('https://') || urlPart.startsWith('oss://') || urlPart.startsWith('s3://');
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Parse an HTTP/OSS URL reference
|
|
2032
|
+
*
|
|
2033
|
+
* Supported formats:
|
|
2034
|
+
* - https://host/path/to/skill-v1.0.0.tar.gz
|
|
2035
|
+
* - https://host/path/to/skill.tar.gz@v1.0.0
|
|
2036
|
+
* - oss://bucket/path/to/skill.tar.gz
|
|
2037
|
+
*/ parseUrl(ref) {
|
|
2038
|
+
let url = ref;
|
|
2039
|
+
let explicitVersion;
|
|
2040
|
+
// Extract explicit version suffix (@v1.0.0)
|
|
2041
|
+
const atIndex = ref.lastIndexOf('@');
|
|
2042
|
+
if (atIndex > 0 && !ref.slice(atIndex).includes('/')) {
|
|
2043
|
+
explicitVersion = ref.slice(atIndex + 1);
|
|
2044
|
+
url = ref.slice(0, atIndex);
|
|
2045
|
+
}
|
|
2046
|
+
// Normalize protocol
|
|
2047
|
+
url = this.normalizeUrl(url);
|
|
2048
|
+
// Parse URL components
|
|
2049
|
+
const urlObj = new URL(url);
|
|
2050
|
+
const host = urlObj.host;
|
|
2051
|
+
const urlPath = urlObj.pathname;
|
|
2052
|
+
const filename = __WEBPACK_EXTERNAL_MODULE_node_path__.basename(urlPath);
|
|
2053
|
+
// Detect archive format
|
|
2054
|
+
const format = this.detectArchiveFormat(filename);
|
|
2055
|
+
// Extract skill name and version from filename
|
|
2056
|
+
const { name, version: filenameVersion } = this.parseFilename(filename);
|
|
2057
|
+
const skillName = name;
|
|
2058
|
+
const version = explicitVersion || filenameVersion;
|
|
2059
|
+
return {
|
|
2060
|
+
url,
|
|
2061
|
+
host,
|
|
2062
|
+
path: urlPath,
|
|
2063
|
+
filename,
|
|
2064
|
+
format,
|
|
2065
|
+
skillName,
|
|
2066
|
+
version
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
/**
|
|
2070
|
+
* Parse HTTP URL to ParsedSkillRef format (for compatibility with existing system)
|
|
2071
|
+
*/ parseRef(ref) {
|
|
2072
|
+
const parsed = this.parseUrl(ref);
|
|
2073
|
+
// Use host as registry, 'http' as owner (convention for HTTP sources)
|
|
2074
|
+
return {
|
|
2075
|
+
registry: 'http',
|
|
2076
|
+
owner: parsed.host,
|
|
2077
|
+
repo: parsed.skillName,
|
|
2078
|
+
version: parsed.version,
|
|
2079
|
+
raw: ref
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
/**
|
|
2083
|
+
* Parse version specification (same as GitResolver for consistency)
|
|
2084
|
+
*/ parseVersion(versionSpec) {
|
|
2085
|
+
if (!versionSpec) return {
|
|
2086
|
+
type: 'exact',
|
|
2087
|
+
value: 'latest',
|
|
2088
|
+
raw: ''
|
|
2089
|
+
};
|
|
2090
|
+
const raw = versionSpec;
|
|
2091
|
+
// latest
|
|
2092
|
+
if ('latest' === versionSpec) return {
|
|
2093
|
+
type: 'latest',
|
|
2094
|
+
value: 'latest',
|
|
2095
|
+
raw
|
|
2096
|
+
};
|
|
2097
|
+
// exact version (v1.0.0 or 1.0.0)
|
|
2098
|
+
return {
|
|
2099
|
+
type: 'exact',
|
|
2100
|
+
value: versionSpec,
|
|
2101
|
+
raw
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Resolve version for HTTP sources
|
|
2106
|
+
* Since HTTP sources are static, we just return the parsed version
|
|
2107
|
+
*/ async resolveVersion(_url, versionSpec) {
|
|
2108
|
+
// For HTTP sources, version is embedded in URL or specified explicitly
|
|
2109
|
+
// No remote version resolution like Git
|
|
2110
|
+
return {
|
|
2111
|
+
ref: versionSpec.value
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Build the download URL
|
|
2116
|
+
* For HTTP sources, the URL is already complete
|
|
2117
|
+
*/ buildRepoUrl(parsed) {
|
|
2118
|
+
// For HTTP sources, raw contains the full URL
|
|
2119
|
+
const urlPart = parsed.raw.split('@')[0];
|
|
2120
|
+
return this.normalizeUrl(urlPart);
|
|
2121
|
+
}
|
|
2122
|
+
/**
|
|
2123
|
+
* Full resolution: from reference string to download-ready information
|
|
2124
|
+
*/ async resolve(ref) {
|
|
2125
|
+
const httpInfo = this.parseUrl(ref);
|
|
2126
|
+
const parsed = this.parseRef(ref);
|
|
2127
|
+
const repoUrl = httpInfo.url;
|
|
2128
|
+
const versionSpec = this.parseVersion(parsed.version);
|
|
2129
|
+
const resolved = await this.resolveVersion(repoUrl, versionSpec);
|
|
2130
|
+
return {
|
|
2131
|
+
parsed,
|
|
2132
|
+
repoUrl,
|
|
2133
|
+
ref: resolved.ref,
|
|
2134
|
+
commit: resolved.commit,
|
|
2135
|
+
httpInfo
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Normalize URL protocol
|
|
2140
|
+
* Converts oss:// and s3:// to https://
|
|
2141
|
+
*/ normalizeUrl(url) {
|
|
2142
|
+
// Convert oss:// to https:// (Aliyun OSS)
|
|
2143
|
+
if (url.startsWith('oss://')) {
|
|
2144
|
+
// oss://bucket-name/path -> https://bucket-name.oss.aliyuncs.com/path
|
|
2145
|
+
const parts = url.slice(6).split('/');
|
|
2146
|
+
const bucket = parts[0];
|
|
2147
|
+
const rest = parts.slice(1).join('/');
|
|
2148
|
+
return `https://${bucket}.oss.aliyuncs.com/${rest}`;
|
|
2149
|
+
}
|
|
2150
|
+
// Convert s3:// to https:// (AWS S3)
|
|
2151
|
+
if (url.startsWith('s3://')) {
|
|
2152
|
+
// s3://bucket-name/path -> https://bucket-name.s3.amazonaws.com/path
|
|
2153
|
+
const parts = url.slice(5).split('/');
|
|
2154
|
+
const bucket = parts[0];
|
|
2155
|
+
const rest = parts.slice(1).join('/');
|
|
2156
|
+
return `https://${bucket}.s3.amazonaws.com/${rest}`;
|
|
2157
|
+
}
|
|
2158
|
+
return url;
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Detect archive format from filename
|
|
2162
|
+
*/ detectArchiveFormat(filename) {
|
|
2163
|
+
const lower = filename.toLowerCase();
|
|
2164
|
+
if (lower.endsWith('.tar.gz')) return 'tar.gz';
|
|
2165
|
+
if (lower.endsWith('.tgz')) return 'tgz';
|
|
2166
|
+
if (lower.endsWith('.zip')) return 'zip';
|
|
2167
|
+
if (lower.endsWith('.tar')) return 'tar';
|
|
2168
|
+
}
|
|
2169
|
+
/**
|
|
2170
|
+
* Parse filename to extract skill name and version
|
|
2171
|
+
*
|
|
2172
|
+
* Examples:
|
|
2173
|
+
* - my-skill-v1.0.0.tar.gz -> { name: 'my-skill', version: 'v1.0.0' }
|
|
2174
|
+
* - skill.tar.gz -> { name: 'skill', version: undefined }
|
|
2175
|
+
* - test-skill-1.2.3.zip -> { name: 'test-skill', version: '1.2.3' }
|
|
2176
|
+
*/ parseFilename(filename) {
|
|
2177
|
+
// Remove archive extension
|
|
2178
|
+
const baseName = filename.replace(/\.tar\.gz$/i, '').replace(/\.tgz$/i, '').replace(/\.zip$/i, '').replace(/\.tar$/i, '');
|
|
2179
|
+
// Try to extract version pattern
|
|
2180
|
+
// Patterns: -v1.0.0, -1.0.0, _v1.0.0, _1.0.0
|
|
2181
|
+
const versionMatch = baseName.match(/[-_](v?\d+\.\d+\.\d+(?:-[\w.]+)?)$/i);
|
|
2182
|
+
if (versionMatch) {
|
|
2183
|
+
const version = versionMatch[1];
|
|
2184
|
+
const name = baseName.slice(0, -versionMatch[0].length);
|
|
2185
|
+
return {
|
|
2186
|
+
name,
|
|
2187
|
+
version
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
return {
|
|
2191
|
+
name: baseName
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* Current lockfile version
|
|
2197
|
+
*/ const LOCKFILE_VERSION = 1;
|
|
2198
|
+
/**
|
|
2199
|
+
* LockManager - Manage skills.lock file
|
|
2200
|
+
*
|
|
2201
|
+
* Used for locking exact versions to ensure team consistency
|
|
2202
|
+
*/ class LockManager {
|
|
1113
2203
|
projectRoot;
|
|
1114
2204
|
lockPath;
|
|
1115
2205
|
lockData = null;
|
|
@@ -1117,15 +2207,22 @@ class LockManager {
|
|
|
1117
2207
|
this.projectRoot = projectRoot || process.cwd();
|
|
1118
2208
|
this.lockPath = getSkillsLockPath(this.projectRoot);
|
|
1119
2209
|
}
|
|
1120
|
-
|
|
2210
|
+
/**
|
|
2211
|
+
* Get lock file path
|
|
2212
|
+
*/ getLockPath() {
|
|
1121
2213
|
return this.lockPath;
|
|
1122
2214
|
}
|
|
1123
|
-
|
|
2215
|
+
/**
|
|
2216
|
+
* Check if lock file exists
|
|
2217
|
+
*/ exists() {
|
|
1124
2218
|
return exists(this.lockPath);
|
|
1125
2219
|
}
|
|
1126
|
-
|
|
2220
|
+
/**
|
|
2221
|
+
* Load lock file
|
|
2222
|
+
*/ load() {
|
|
1127
2223
|
if (this.lockData) return this.lockData;
|
|
1128
2224
|
if (!this.exists()) {
|
|
2225
|
+
// If not exists, create empty lock
|
|
1129
2226
|
this.lockData = {
|
|
1130
2227
|
lockfileVersion: LOCKFILE_VERSION,
|
|
1131
2228
|
skills: {}
|
|
@@ -1139,26 +2236,36 @@ class LockManager {
|
|
|
1139
2236
|
throw new Error(`Failed to parse skills.lock: ${error.message}`);
|
|
1140
2237
|
}
|
|
1141
2238
|
}
|
|
1142
|
-
|
|
2239
|
+
/**
|
|
2240
|
+
* Reload lock file
|
|
2241
|
+
*/ reload() {
|
|
1143
2242
|
this.lockData = null;
|
|
1144
2243
|
return this.load();
|
|
1145
2244
|
}
|
|
1146
|
-
|
|
2245
|
+
/**
|
|
2246
|
+
* Save lock file
|
|
2247
|
+
*/ save(lockToSave) {
|
|
1147
2248
|
const toSave = lockToSave || this.lockData;
|
|
1148
2249
|
if (!toSave) throw new Error('No lock to save');
|
|
1149
2250
|
writeJson(this.lockPath, toSave);
|
|
1150
2251
|
this.lockData = toSave;
|
|
1151
2252
|
}
|
|
1152
|
-
|
|
2253
|
+
/**
|
|
2254
|
+
* Get locked skill
|
|
2255
|
+
*/ get(name) {
|
|
1153
2256
|
const lock = this.load();
|
|
1154
2257
|
return lock.skills[name];
|
|
1155
2258
|
}
|
|
1156
|
-
|
|
2259
|
+
/**
|
|
2260
|
+
* Set locked skill
|
|
2261
|
+
*/ set(name, skill) {
|
|
1157
2262
|
const lock = this.load();
|
|
1158
2263
|
lock.skills[name] = skill;
|
|
1159
2264
|
this.save();
|
|
1160
2265
|
}
|
|
1161
|
-
|
|
2266
|
+
/**
|
|
2267
|
+
* Remove locked skill
|
|
2268
|
+
*/ remove(name) {
|
|
1162
2269
|
const lock = this.load();
|
|
1163
2270
|
if (lock.skills[name]) {
|
|
1164
2271
|
delete lock.skills[name];
|
|
@@ -1167,7 +2274,9 @@ class LockManager {
|
|
|
1167
2274
|
}
|
|
1168
2275
|
return false;
|
|
1169
2276
|
}
|
|
1170
|
-
|
|
2277
|
+
/**
|
|
2278
|
+
* Lock a skill
|
|
2279
|
+
*/ lockSkill(name, options) {
|
|
1171
2280
|
const lockedSkill = {
|
|
1172
2281
|
source: options.source,
|
|
1173
2282
|
version: options.version,
|
|
@@ -1179,29 +2288,39 @@ class LockManager {
|
|
|
1179
2288
|
this.set(name, lockedSkill);
|
|
1180
2289
|
return lockedSkill;
|
|
1181
2290
|
}
|
|
1182
|
-
|
|
2291
|
+
/**
|
|
2292
|
+
* Get all locked skills
|
|
2293
|
+
*/ getAll() {
|
|
1183
2294
|
const lock = this.load();
|
|
1184
2295
|
return {
|
|
1185
2296
|
...lock.skills
|
|
1186
2297
|
};
|
|
1187
2298
|
}
|
|
1188
|
-
|
|
2299
|
+
/**
|
|
2300
|
+
* Check if skill is locked
|
|
2301
|
+
*/ has(name) {
|
|
1189
2302
|
const lock = this.load();
|
|
1190
2303
|
return name in lock.skills;
|
|
1191
2304
|
}
|
|
1192
|
-
|
|
2305
|
+
/**
|
|
2306
|
+
* Check if locked version matches current version
|
|
2307
|
+
*/ isVersionMatch(name, version) {
|
|
1193
2308
|
const locked = this.get(name);
|
|
1194
2309
|
if (!locked) return false;
|
|
1195
2310
|
return locked.version === version;
|
|
1196
2311
|
}
|
|
1197
|
-
|
|
2312
|
+
/**
|
|
2313
|
+
* Clear all locks
|
|
2314
|
+
*/ clear() {
|
|
1198
2315
|
this.lockData = {
|
|
1199
2316
|
lockfileVersion: LOCKFILE_VERSION,
|
|
1200
2317
|
skills: {}
|
|
1201
2318
|
};
|
|
1202
2319
|
this.save();
|
|
1203
2320
|
}
|
|
1204
|
-
|
|
2321
|
+
/**
|
|
2322
|
+
* Delete lock file
|
|
2323
|
+
*/ delete() {
|
|
1205
2324
|
if (this.exists()) {
|
|
1206
2325
|
const fs = __webpack_require__("node:fs");
|
|
1207
2326
|
fs.unlinkSync(this.lockPath);
|
|
@@ -1209,32 +2328,53 @@ class LockManager {
|
|
|
1209
2328
|
this.lockData = null;
|
|
1210
2329
|
}
|
|
1211
2330
|
}
|
|
1212
|
-
|
|
1213
|
-
|
|
2331
|
+
/**
|
|
2332
|
+
* Logger utility for CLI output
|
|
2333
|
+
*/ const logger_logger = {
|
|
2334
|
+
/**
|
|
2335
|
+
* Info message (blue)
|
|
2336
|
+
*/ info (message) {
|
|
1214
2337
|
console.log(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].blue('ℹ'), message);
|
|
1215
2338
|
},
|
|
1216
|
-
|
|
2339
|
+
/**
|
|
2340
|
+
* Success message (green)
|
|
2341
|
+
*/ success (message) {
|
|
1217
2342
|
console.log(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].green('✅'), message);
|
|
1218
2343
|
},
|
|
1219
|
-
|
|
2344
|
+
/**
|
|
2345
|
+
* Warning message (yellow)
|
|
2346
|
+
*/ warn (message) {
|
|
1220
2347
|
console.log(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].yellow('⚠️'), message);
|
|
1221
2348
|
},
|
|
1222
|
-
|
|
2349
|
+
/**
|
|
2350
|
+
* Error message (red)
|
|
2351
|
+
*/ error (message) {
|
|
1223
2352
|
console.error(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].red('❌'), message);
|
|
1224
2353
|
},
|
|
1225
|
-
|
|
2354
|
+
/**
|
|
2355
|
+
* Debug message (gray, only in verbose mode)
|
|
2356
|
+
*/ debug (message) {
|
|
1226
2357
|
if (process.env.DEBUG || process.env.VERBOSE) console.log(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray('🔍'), __WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(message));
|
|
1227
2358
|
},
|
|
1228
|
-
|
|
2359
|
+
/**
|
|
2360
|
+
* Package/skill message (package emoji)
|
|
2361
|
+
*/ package (message) {
|
|
1229
2362
|
console.log(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].cyan('📦'), message);
|
|
1230
2363
|
},
|
|
1231
|
-
|
|
2364
|
+
/**
|
|
2365
|
+
* Plain message without icon
|
|
2366
|
+
*/ log (message) {
|
|
1232
2367
|
console.log(message);
|
|
1233
2368
|
},
|
|
1234
|
-
|
|
2369
|
+
/**
|
|
2370
|
+
* Newline
|
|
2371
|
+
*/ newline () {
|
|
1235
2372
|
console.log();
|
|
1236
2373
|
},
|
|
1237
|
-
|
|
2374
|
+
/**
|
|
2375
|
+
* Table-like output
|
|
2376
|
+
*/ table (headers, rows) {
|
|
2377
|
+
// Calculate column widths
|
|
1238
2378
|
const widths = headers.map((h, i)=>{
|
|
1239
2379
|
const colValues = [
|
|
1240
2380
|
h,
|
|
@@ -1242,17 +2382,31 @@ const logger_logger = {
|
|
|
1242
2382
|
];
|
|
1243
2383
|
return Math.max(...colValues.map((v)=>v.length));
|
|
1244
2384
|
});
|
|
2385
|
+
// Print header
|
|
1245
2386
|
const headerRow = headers.map((h, i)=>h.padEnd(widths[i])).join(' ');
|
|
1246
2387
|
console.log(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(headerRow));
|
|
2388
|
+
// Print separator (removed)
|
|
2389
|
+
// console.log(widths.map(w => '-'.repeat(w)).join(' '));
|
|
2390
|
+
// Print rows
|
|
1247
2391
|
for (const row of rows){
|
|
1248
2392
|
const rowStr = row.map((cell, i)=>(cell || '').padEnd(widths[i])).join(' ');
|
|
1249
2393
|
console.log(rowStr);
|
|
1250
2394
|
}
|
|
1251
2395
|
}
|
|
1252
2396
|
};
|
|
1253
|
-
|
|
2397
|
+
/**
|
|
2398
|
+
* SkillManager - Core Skill management class
|
|
2399
|
+
*
|
|
2400
|
+
* Integrates GitResolver, CacheManager, ConfigLoader, LockManager
|
|
2401
|
+
* Provides complete skill installation, update, and uninstall functionality
|
|
2402
|
+
*
|
|
2403
|
+
* Installation directories:
|
|
2404
|
+
* - Project mode (default): .skills/ or directory configured in skills.json
|
|
2405
|
+
* - Global mode (-g): ~/.claude/skills/
|
|
2406
|
+
*/ class SkillManager {
|
|
1254
2407
|
projectRoot;
|
|
1255
2408
|
resolver;
|
|
2409
|
+
httpResolver;
|
|
1256
2410
|
cache;
|
|
1257
2411
|
config;
|
|
1258
2412
|
lockManager;
|
|
@@ -1263,42 +2417,86 @@ class SkillManager {
|
|
|
1263
2417
|
this.config = new ConfigLoader(this.projectRoot);
|
|
1264
2418
|
this.lockManager = new LockManager(this.projectRoot);
|
|
1265
2419
|
this.cache = new CacheManager();
|
|
1266
|
-
|
|
2420
|
+
// Pass registry resolver from ConfigLoader to GitResolver
|
|
2421
|
+
this.resolver = new GitResolver('github', void 0, (registryName)=>this.config.getRegistryUrl(registryName));
|
|
2422
|
+
this.httpResolver = new HttpResolver();
|
|
1267
2423
|
}
|
|
1268
|
-
|
|
2424
|
+
/**
|
|
2425
|
+
* Check if in global mode
|
|
2426
|
+
*/ isGlobalMode() {
|
|
1269
2427
|
return this.isGlobal;
|
|
1270
2428
|
}
|
|
1271
|
-
|
|
2429
|
+
/**
|
|
2430
|
+
* Get project root directory
|
|
2431
|
+
*/ getProjectRoot() {
|
|
1272
2432
|
return this.projectRoot;
|
|
1273
2433
|
}
|
|
1274
|
-
|
|
2434
|
+
/**
|
|
2435
|
+
* Get legacy installation directory (for backward compatibility)
|
|
2436
|
+
*
|
|
2437
|
+
* - Global mode: ~/.claude/skills/
|
|
2438
|
+
* - Project mode: .skills/ or directory configured in skills.json
|
|
2439
|
+
*/ getInstallDir() {
|
|
1275
2440
|
if (this.isGlobal) return getGlobalSkillsDir();
|
|
1276
2441
|
return this.config.getInstallDir();
|
|
1277
2442
|
}
|
|
1278
|
-
|
|
2443
|
+
/**
|
|
2444
|
+
* Get canonical skills directory
|
|
2445
|
+
*
|
|
2446
|
+
* This is the primary storage location used by installToAgents().
|
|
2447
|
+
* - Project mode: .agents/skills/
|
|
2448
|
+
* - Global mode: ~/.agents/skills/
|
|
2449
|
+
*/ getCanonicalSkillsDir() {
|
|
1279
2450
|
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
1280
2451
|
const baseDir = this.isGlobal ? home : this.projectRoot;
|
|
1281
2452
|
return __WEBPACK_EXTERNAL_MODULE_node_path__.join(baseDir, '.agents', 'skills');
|
|
1282
2453
|
}
|
|
1283
|
-
|
|
2454
|
+
/**
|
|
2455
|
+
* Get skill installation path
|
|
2456
|
+
*
|
|
2457
|
+
* Checks canonical location first, then falls back to configured installDir.
|
|
2458
|
+
*/ getSkillPath(name) {
|
|
2459
|
+
// Check canonical location first (.agents/skills/)
|
|
1284
2460
|
const canonicalPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.getCanonicalSkillsDir(), name);
|
|
1285
2461
|
if (exists(canonicalPath)) return canonicalPath;
|
|
2462
|
+
// Check configured installation directory (.skills/ or custom)
|
|
1286
2463
|
const installDir = this.getInstallDir();
|
|
1287
2464
|
const installPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(installDir, name);
|
|
1288
2465
|
if (exists(installPath)) return installPath;
|
|
2466
|
+
// Default to configured installation directory for new installations
|
|
2467
|
+
// if it's not the default .skills, otherwise use canonical location.
|
|
2468
|
+
// This respects "installDir" in skills.json.
|
|
1289
2469
|
const defaults = this.config.getDefaults();
|
|
1290
2470
|
if ('.skills' !== defaults.installDir && !this.isGlobal) return installPath;
|
|
2471
|
+
// Default to canonical location for new installations
|
|
1291
2472
|
return canonicalPath;
|
|
1292
2473
|
}
|
|
1293
|
-
|
|
2474
|
+
/**
|
|
2475
|
+
* Detect if a reference is an HTTP/OSS URL
|
|
2476
|
+
*/ isHttpSource(ref) {
|
|
2477
|
+
return HttpResolver.isHttpUrl(ref);
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Install skill
|
|
2481
|
+
*/ async install(ref, options = {}) {
|
|
2482
|
+
// Detect source type and delegate to appropriate installer
|
|
2483
|
+
if (this.isHttpSource(ref)) return this.installFromHttp(ref, options);
|
|
2484
|
+
return this.installFromGit(ref, options);
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2487
|
+
* Install skill from Git repository
|
|
2488
|
+
*/ async installFromGit(ref, options = {}) {
|
|
1294
2489
|
const { force = false, save = true } = options;
|
|
2490
|
+
// Parse reference
|
|
1295
2491
|
const resolved = await this.resolver.resolve(ref);
|
|
1296
2492
|
const { parsed, repoUrl } = resolved;
|
|
1297
|
-
const gitRef = resolved.ref;
|
|
2493
|
+
const gitRef = resolved.ref; // Git reference (tag, branch, commit)
|
|
1298
2494
|
const skillName = parsed.subPath ? __WEBPACK_EXTERNAL_MODULE_node_path__.basename(parsed.subPath) : parsed.repo;
|
|
1299
2495
|
const skillPath = this.getSkillPath(skillName);
|
|
2496
|
+
// Check if already installed
|
|
1300
2497
|
if (exists(skillPath) && !force) {
|
|
1301
2498
|
const locked = this.lockManager.get(skillName);
|
|
2499
|
+
// Compare ref if available, fallback to version for backward compatibility
|
|
1302
2500
|
const lockedRef = locked?.ref || locked?.version;
|
|
1303
2501
|
if (locked && lockedRef === gitRef) {
|
|
1304
2502
|
logger_logger.info(`${skillName}@${gitRef} is already installed`);
|
|
@@ -1312,21 +2510,27 @@ class SkillManager {
|
|
|
1312
2510
|
}
|
|
1313
2511
|
}
|
|
1314
2512
|
logger_logger["package"](`Installing ${skillName}@${gitRef}...`);
|
|
2513
|
+
// Check cache
|
|
1315
2514
|
let cacheResult = await this.cache.get(parsed, gitRef);
|
|
1316
2515
|
if (cacheResult) logger_logger.debug(`Using cached ${skillName}@${gitRef}`);
|
|
1317
2516
|
else {
|
|
1318
2517
|
logger_logger.debug(`Caching ${skillName}@${gitRef} from ${repoUrl}`);
|
|
1319
2518
|
cacheResult = await this.cache.cache(repoUrl, parsed, gitRef, gitRef);
|
|
1320
2519
|
}
|
|
2520
|
+
// Copy to installation directory
|
|
1321
2521
|
ensureDir(this.getInstallDir());
|
|
1322
2522
|
if (exists(skillPath)) remove(skillPath);
|
|
1323
2523
|
await this.cache.copyTo(parsed, gitRef, skillPath);
|
|
1324
|
-
|
|
2524
|
+
// Read semantic version from skill.json
|
|
2525
|
+
let semanticVersion = gitRef; // fallback to gitRef if no skill.json
|
|
1325
2526
|
const skillJsonPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, 'skill.json');
|
|
1326
2527
|
if (exists(skillJsonPath)) try {
|
|
1327
2528
|
const skillJson = readJson(skillJsonPath);
|
|
1328
2529
|
if (skillJson.version) semanticVersion = skillJson.version;
|
|
1329
|
-
} catch {
|
|
2530
|
+
} catch {
|
|
2531
|
+
// Ignore parse errors, use gitRef as fallback
|
|
2532
|
+
}
|
|
2533
|
+
// Update lock file (project mode only)
|
|
1330
2534
|
if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
|
|
1331
2535
|
source: `${parsed.registry}:${parsed.owner}/${parsed.repo}${parsed.subPath ? `/${parsed.subPath}` : ''}`,
|
|
1332
2536
|
version: semanticVersion,
|
|
@@ -1334,9 +2538,12 @@ class SkillManager {
|
|
|
1334
2538
|
resolved: repoUrl,
|
|
1335
2539
|
commit: cacheResult.commit
|
|
1336
2540
|
});
|
|
2541
|
+
// Update skills.json (project mode only)
|
|
1337
2542
|
if (!this.isGlobal && save) {
|
|
1338
2543
|
this.config.ensureExists();
|
|
1339
|
-
|
|
2544
|
+
// Normalize the reference to use registry shorthand if possible
|
|
2545
|
+
const normalizedRef = this.config.normalizeSkillRef(ref);
|
|
2546
|
+
this.config.addSkill(skillName, normalizedRef);
|
|
1340
2547
|
}
|
|
1341
2548
|
const displayVersion = semanticVersion !== gitRef ? `${semanticVersion} (${gitRef})` : gitRef;
|
|
1342
2549
|
const locationHint = this.isGlobal ? '(global)' : '';
|
|
@@ -1345,7 +2552,75 @@ class SkillManager {
|
|
|
1345
2552
|
if (!installed) throw new Error(`Failed to get installed skill info for ${skillName}`);
|
|
1346
2553
|
return installed;
|
|
1347
2554
|
}
|
|
1348
|
-
|
|
2555
|
+
/**
|
|
2556
|
+
* Install skill from HTTP/OSS URL
|
|
2557
|
+
*/ async installFromHttp(ref, options = {}) {
|
|
2558
|
+
const { force = false, save = true } = options;
|
|
2559
|
+
// Parse HTTP reference
|
|
2560
|
+
const resolved = await this.httpResolver.resolve(ref);
|
|
2561
|
+
const { parsed, repoUrl, httpInfo } = resolved;
|
|
2562
|
+
const version = resolved.ref || 'latest';
|
|
2563
|
+
const skillName = httpInfo.skillName;
|
|
2564
|
+
const skillPath = this.getSkillPath(skillName);
|
|
2565
|
+
// Check if already installed
|
|
2566
|
+
if (exists(skillPath) && !force) {
|
|
2567
|
+
const locked = this.lockManager.get(skillName);
|
|
2568
|
+
const lockedRef = locked?.ref || locked?.version;
|
|
2569
|
+
if (locked && lockedRef === version) {
|
|
2570
|
+
logger_logger.info(`${skillName}@${version} is already installed`);
|
|
2571
|
+
const installed = this.getInstalledSkill(skillName);
|
|
2572
|
+
if (installed) return installed;
|
|
2573
|
+
}
|
|
2574
|
+
if (!force) {
|
|
2575
|
+
logger_logger.warn(`${skillName} is already installed. Use --force to reinstall.`);
|
|
2576
|
+
const installed = this.getInstalledSkill(skillName);
|
|
2577
|
+
if (installed) return installed;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
logger_logger["package"](`Installing ${skillName}@${version} from ${httpInfo.host}...`);
|
|
2581
|
+
// Check cache
|
|
2582
|
+
let cacheResult = await this.cache.get(parsed, version);
|
|
2583
|
+
if (cacheResult) logger_logger.debug(`Using cached ${skillName}@${version}`);
|
|
2584
|
+
else {
|
|
2585
|
+
logger_logger.debug(`Downloading ${skillName}@${version} from ${repoUrl}`);
|
|
2586
|
+
cacheResult = await this.cache.cacheFromHttp(repoUrl, parsed, version);
|
|
2587
|
+
}
|
|
2588
|
+
// Copy to installation directory
|
|
2589
|
+
ensureDir(this.getInstallDir());
|
|
2590
|
+
if (exists(skillPath)) remove(skillPath);
|
|
2591
|
+
await this.cache.copyTo(parsed, version, skillPath);
|
|
2592
|
+
// Read semantic version from skill.json
|
|
2593
|
+
let semanticVersion = version;
|
|
2594
|
+
const skillJsonPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, 'skill.json');
|
|
2595
|
+
if (exists(skillJsonPath)) try {
|
|
2596
|
+
const skillJson = readJson(skillJsonPath);
|
|
2597
|
+
if (skillJson.version) semanticVersion = skillJson.version;
|
|
2598
|
+
} catch {
|
|
2599
|
+
// Ignore parse errors
|
|
2600
|
+
}
|
|
2601
|
+
// Update lock file (project mode only)
|
|
2602
|
+
if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
|
|
2603
|
+
source: `http:${httpInfo.host}/${skillName}`,
|
|
2604
|
+
version: semanticVersion,
|
|
2605
|
+
ref: version,
|
|
2606
|
+
resolved: repoUrl,
|
|
2607
|
+
commit: cacheResult.commit
|
|
2608
|
+
});
|
|
2609
|
+
// Update skills.json (project mode only)
|
|
2610
|
+
if (!this.isGlobal && save) {
|
|
2611
|
+
this.config.ensureExists();
|
|
2612
|
+
this.config.addSkill(skillName, ref);
|
|
2613
|
+
}
|
|
2614
|
+
const displayVersion = semanticVersion !== version ? `${semanticVersion} (${version})` : version;
|
|
2615
|
+
const locationHint = this.isGlobal ? '(global)' : '';
|
|
2616
|
+
logger_logger.success(`Installed ${skillName}@${displayVersion} to ${skillPath} ${locationHint}`.trim());
|
|
2617
|
+
const installed = this.getInstalledSkill(skillName);
|
|
2618
|
+
if (!installed) throw new Error(`Failed to get installed skill info for ${skillName}`);
|
|
2619
|
+
return installed;
|
|
2620
|
+
}
|
|
2621
|
+
/**
|
|
2622
|
+
* Install all skills from skills.json
|
|
2623
|
+
*/ async installAll(options = {}) {
|
|
1349
2624
|
const skills = this.config.getSkills();
|
|
1350
2625
|
const installed = [];
|
|
1351
2626
|
for (const [name, ref] of Object.entries(skills))try {
|
|
@@ -1359,38 +2634,59 @@ class SkillManager {
|
|
|
1359
2634
|
}
|
|
1360
2635
|
return installed;
|
|
1361
2636
|
}
|
|
1362
|
-
|
|
2637
|
+
/**
|
|
2638
|
+
* Uninstall skill
|
|
2639
|
+
*/ uninstall(name) {
|
|
1363
2640
|
const skillPath = this.getSkillPath(name);
|
|
1364
2641
|
if (!exists(skillPath)) {
|
|
1365
2642
|
const location = this.isGlobal ? '(global)' : '';
|
|
1366
2643
|
logger_logger.warn(`Skill ${name} is not installed ${location}`.trim());
|
|
1367
2644
|
return false;
|
|
1368
2645
|
}
|
|
2646
|
+
// Remove installation directory
|
|
1369
2647
|
remove(skillPath);
|
|
2648
|
+
// Remove from lock file (project mode only)
|
|
1370
2649
|
if (!this.isGlobal) this.lockManager.remove(name);
|
|
2650
|
+
// Remove from skills.json (project mode only)
|
|
1371
2651
|
if (!this.isGlobal && this.config.exists()) this.config.removeSkill(name);
|
|
1372
2652
|
const locationHint = this.isGlobal ? '(global)' : '';
|
|
1373
2653
|
logger_logger.success(`Uninstalled ${name} ${locationHint}`.trim());
|
|
1374
2654
|
return true;
|
|
1375
2655
|
}
|
|
1376
|
-
|
|
2656
|
+
/**
|
|
2657
|
+
* Check if a skill needs to be updated by comparing local and remote commits
|
|
2658
|
+
*
|
|
2659
|
+
* @param name - Skill name
|
|
2660
|
+
* @param remoteCommit - Remote commit hash to compare against
|
|
2661
|
+
* @returns true if update is needed, false if already up to date
|
|
2662
|
+
*/ checkNeedsUpdate(name, remoteCommit) {
|
|
1377
2663
|
const locked = this.lockManager.get(name);
|
|
2664
|
+
// No lock info or no commit hash means we need to update
|
|
1378
2665
|
if (!locked?.commit) return true;
|
|
2666
|
+
// Compare commits
|
|
1379
2667
|
return locked.commit !== remoteCommit;
|
|
1380
2668
|
}
|
|
1381
|
-
|
|
2669
|
+
/**
|
|
2670
|
+
* Update skill
|
|
2671
|
+
*/ async update(name) {
|
|
1382
2672
|
const updated = [];
|
|
1383
2673
|
if (name) {
|
|
2674
|
+
// Update single skill
|
|
1384
2675
|
const ref = this.config.getSkillRef(name);
|
|
1385
2676
|
if (!ref) {
|
|
1386
2677
|
logger_logger.error(`Skill ${name} not found in skills.json`);
|
|
1387
2678
|
return [];
|
|
1388
2679
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
2680
|
+
// Check if update is needed (skip check for HTTP sources - always re-download)
|
|
2681
|
+
if (this.isHttpSource(ref)) // For HTTP sources, log that we're re-downloading
|
|
2682
|
+
logger_logger.info(`${name} is from HTTP source, re-downloading...`);
|
|
2683
|
+
else {
|
|
2684
|
+
const resolved = await this.resolver.resolve(ref);
|
|
2685
|
+
const remoteCommit = await this.cache.getRemoteCommit(resolved.repoUrl, resolved.ref);
|
|
2686
|
+
if (!this.checkNeedsUpdate(name, remoteCommit)) {
|
|
2687
|
+
logger_logger.info(`${name} is already up to date`);
|
|
2688
|
+
return [];
|
|
2689
|
+
}
|
|
1394
2690
|
}
|
|
1395
2691
|
const skill = await this.install(ref, {
|
|
1396
2692
|
force: true,
|
|
@@ -1398,13 +2694,18 @@ class SkillManager {
|
|
|
1398
2694
|
});
|
|
1399
2695
|
updated.push(skill);
|
|
1400
2696
|
} else {
|
|
2697
|
+
// Update all
|
|
1401
2698
|
const skills = this.config.getSkills();
|
|
1402
2699
|
for (const [skillName, ref] of Object.entries(skills))try {
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
2700
|
+
// Check if update is needed (skip check for HTTP sources)
|
|
2701
|
+
if (this.isHttpSource(ref)) logger_logger.info(`${skillName} is from HTTP source, re-downloading...`);
|
|
2702
|
+
else {
|
|
2703
|
+
const resolved = await this.resolver.resolve(ref);
|
|
2704
|
+
const remoteCommit = await this.cache.getRemoteCommit(resolved.repoUrl, resolved.ref);
|
|
2705
|
+
if (!this.checkNeedsUpdate(skillName, remoteCommit)) {
|
|
2706
|
+
logger_logger.info(`${skillName} is already up to date`);
|
|
2707
|
+
continue;
|
|
2708
|
+
}
|
|
1408
2709
|
}
|
|
1409
2710
|
const skill = await this.install(ref, {
|
|
1410
2711
|
force: true,
|
|
@@ -1417,9 +2718,14 @@ class SkillManager {
|
|
|
1417
2718
|
}
|
|
1418
2719
|
return updated;
|
|
1419
2720
|
}
|
|
1420
|
-
|
|
2721
|
+
/**
|
|
2722
|
+
* List installed skills
|
|
2723
|
+
*
|
|
2724
|
+
* Checks both canonical (.agents/skills/) and legacy (.skills/) locations.
|
|
2725
|
+
*/ list() {
|
|
1421
2726
|
const skills = [];
|
|
1422
2727
|
const seenNames = new Set();
|
|
2728
|
+
// Check canonical location first (.agents/skills/)
|
|
1423
2729
|
const canonicalDir = this.getCanonicalSkillsDir();
|
|
1424
2730
|
if (exists(canonicalDir)) for (const name of listDir(canonicalDir)){
|
|
1425
2731
|
const skillPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(canonicalDir, name);
|
|
@@ -1430,15 +2736,20 @@ class SkillManager {
|
|
|
1430
2736
|
seenNames.add(name);
|
|
1431
2737
|
}
|
|
1432
2738
|
}
|
|
2739
|
+
// Check legacy location (.skills/)
|
|
1433
2740
|
const legacyDir = this.getInstallDir();
|
|
1434
2741
|
if (exists(legacyDir) && legacyDir !== canonicalDir) for (const name of listDir(legacyDir)){
|
|
2742
|
+
// Skip if already found in canonical location
|
|
1435
2743
|
if (seenNames.has(name)) continue;
|
|
1436
2744
|
const skillPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(legacyDir, name);
|
|
1437
2745
|
if (!isDirectory(skillPath)) continue;
|
|
2746
|
+
// Skip symlinks pointing to canonical location (avoid duplicates)
|
|
1438
2747
|
if (isSymlink(skillPath)) try {
|
|
1439
2748
|
const realPath = getRealPath(skillPath);
|
|
1440
2749
|
if (realPath.includes(__WEBPACK_EXTERNAL_MODULE_node_path__.join('.agents', 'skills'))) continue;
|
|
1441
|
-
} catch {
|
|
2750
|
+
} catch {
|
|
2751
|
+
// If we can't resolve the symlink, include it anyway
|
|
2752
|
+
}
|
|
1442
2753
|
const skill = this.getInstalledSkillFromPath(name, skillPath);
|
|
1443
2754
|
if (skill) {
|
|
1444
2755
|
skills.push(skill);
|
|
@@ -1447,7 +2758,9 @@ class SkillManager {
|
|
|
1447
2758
|
}
|
|
1448
2759
|
return skills;
|
|
1449
2760
|
}
|
|
1450
|
-
|
|
2761
|
+
/**
|
|
2762
|
+
* Get installed skill information from a specific path
|
|
2763
|
+
*/ getInstalledSkillFromPath(name, skillPath) {
|
|
1451
2764
|
if (!exists(skillPath)) return null;
|
|
1452
2765
|
const isLinked = isSymlink(skillPath);
|
|
1453
2766
|
const locked = this.lockManager.get(name);
|
|
@@ -1455,7 +2768,9 @@ class SkillManager {
|
|
|
1455
2768
|
const skillJsonPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, 'skill.json');
|
|
1456
2769
|
if (exists(skillJsonPath)) try {
|
|
1457
2770
|
metadata = readJson(skillJsonPath);
|
|
1458
|
-
} catch {
|
|
2771
|
+
} catch {
|
|
2772
|
+
// Ignore parse errors
|
|
2773
|
+
}
|
|
1459
2774
|
return {
|
|
1460
2775
|
name,
|
|
1461
2776
|
path: skillPath,
|
|
@@ -1465,35 +2780,59 @@ class SkillManager {
|
|
|
1465
2780
|
isLinked
|
|
1466
2781
|
};
|
|
1467
2782
|
}
|
|
1468
|
-
|
|
2783
|
+
/**
|
|
2784
|
+
* Get installed skill information
|
|
2785
|
+
*
|
|
2786
|
+
* Checks canonical location first, then legacy location.
|
|
2787
|
+
*/ getInstalledSkill(name) {
|
|
2788
|
+
// Check canonical location first (.agents/skills/)
|
|
1469
2789
|
const canonicalPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.getCanonicalSkillsDir(), name);
|
|
1470
2790
|
if (exists(canonicalPath)) return this.getInstalledSkillFromPath(name, canonicalPath);
|
|
2791
|
+
// Check legacy location (.skills/)
|
|
1471
2792
|
const legacyPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.getInstallDir(), name);
|
|
1472
2793
|
if (exists(legacyPath)) return this.getInstalledSkillFromPath(name, legacyPath);
|
|
1473
2794
|
return null;
|
|
1474
2795
|
}
|
|
1475
|
-
|
|
2796
|
+
/**
|
|
2797
|
+
* Get skill details
|
|
2798
|
+
*/ getInfo(name) {
|
|
1476
2799
|
return {
|
|
1477
2800
|
installed: this.getInstalledSkill(name),
|
|
1478
2801
|
locked: this.lockManager.get(name),
|
|
1479
2802
|
config: this.config.getSkillRef(name)
|
|
1480
2803
|
};
|
|
1481
2804
|
}
|
|
1482
|
-
|
|
2805
|
+
/**
|
|
2806
|
+
* Check for outdated skills
|
|
2807
|
+
*/ async checkOutdated() {
|
|
1483
2808
|
const results = [];
|
|
1484
2809
|
const skills = this.config.getSkills();
|
|
1485
2810
|
for (const [name, ref] of Object.entries(skills))try {
|
|
1486
2811
|
const locked = this.lockManager.get(name);
|
|
2812
|
+
// Use ref for comparison (git tag/branch/commit), fallback to version for backward compatibility
|
|
1487
2813
|
const currentRef = locked?.ref || locked?.version || 'unknown';
|
|
1488
2814
|
const currentVersion = locked?.version || 'unknown';
|
|
2815
|
+
// HTTP sources don't support version checking
|
|
2816
|
+
if (this.isHttpSource(ref)) {
|
|
2817
|
+
results.push({
|
|
2818
|
+
name,
|
|
2819
|
+
current: currentVersion,
|
|
2820
|
+
latest: 'n/a (HTTP source)',
|
|
2821
|
+
updateAvailable: false
|
|
2822
|
+
});
|
|
2823
|
+
continue;
|
|
2824
|
+
}
|
|
2825
|
+
// Parse latest version
|
|
1489
2826
|
const parsed = this.resolver.parseRef(ref);
|
|
1490
2827
|
const repoUrl = this.resolver.buildRepoUrl(parsed);
|
|
2828
|
+
// Force get latest
|
|
1491
2829
|
const latestResolved = await this.resolver.resolveVersion(repoUrl, {
|
|
1492
2830
|
type: 'latest',
|
|
1493
2831
|
value: 'latest',
|
|
1494
2832
|
raw: 'latest'
|
|
1495
2833
|
});
|
|
1496
2834
|
const latest = latestResolved.ref;
|
|
2835
|
+
// Compare using git refs, not semantic versions
|
|
1497
2836
|
const updateAvailable = currentRef !== latest && 'unknown' !== currentRef;
|
|
1498
2837
|
results.push({
|
|
1499
2838
|
name,
|
|
@@ -1512,35 +2851,60 @@ class SkillManager {
|
|
|
1512
2851
|
}
|
|
1513
2852
|
return results;
|
|
1514
2853
|
}
|
|
1515
|
-
|
|
2854
|
+
// ============================================================================
|
|
2855
|
+
// Multi-Agent installation methods
|
|
2856
|
+
// ============================================================================
|
|
2857
|
+
/**
|
|
2858
|
+
* Install skill to multiple agents
|
|
2859
|
+
*
|
|
2860
|
+
* @param ref - Skill reference (e.g., github:user/repo@v1.0.0 or HTTP URL)
|
|
2861
|
+
* @param targetAgents - Target agents list
|
|
2862
|
+
* @param options - Installation options
|
|
2863
|
+
*/ async installToAgents(ref, targetAgents, options = {}) {
|
|
2864
|
+
// Detect source type and delegate to appropriate installer
|
|
2865
|
+
if (this.isHttpSource(ref)) return this.installToAgentsFromHttp(ref, targetAgents, options);
|
|
2866
|
+
return this.installToAgentsFromGit(ref, targetAgents, options);
|
|
2867
|
+
}
|
|
2868
|
+
/**
|
|
2869
|
+
* Install skill from Git to multiple agents
|
|
2870
|
+
*/ async installToAgentsFromGit(ref, targetAgents, options = {}) {
|
|
1516
2871
|
const { save = true, mode = 'symlink' } = options;
|
|
2872
|
+
// Parse reference
|
|
1517
2873
|
const resolved = await this.resolver.resolve(ref);
|
|
1518
2874
|
const { parsed, repoUrl } = resolved;
|
|
1519
|
-
const gitRef = resolved.ref;
|
|
2875
|
+
const gitRef = resolved.ref; // Git reference (tag, branch, commit)
|
|
1520
2876
|
const skillName = parsed.subPath ? __WEBPACK_EXTERNAL_MODULE_node_path__.basename(parsed.subPath) : parsed.repo;
|
|
1521
2877
|
logger_logger["package"](`Installing ${skillName}@${gitRef} to ${targetAgents.length} agent(s)...`);
|
|
2878
|
+
// Check cache
|
|
1522
2879
|
let cacheResult = await this.cache.get(parsed, gitRef);
|
|
1523
2880
|
if (cacheResult) logger_logger.debug(`Using cached ${skillName}@${gitRef}`);
|
|
1524
2881
|
else {
|
|
1525
2882
|
logger_logger.debug(`Caching ${skillName}@${gitRef} from ${repoUrl}`);
|
|
1526
2883
|
cacheResult = await this.cache.cache(repoUrl, parsed, gitRef, gitRef);
|
|
1527
2884
|
}
|
|
2885
|
+
// Get cache path as source
|
|
1528
2886
|
const sourcePath = this.cache.getCachePath(parsed, gitRef);
|
|
1529
|
-
|
|
2887
|
+
// Read semantic version from skill.json
|
|
2888
|
+
let semanticVersion = gitRef; // fallback to gitRef if no skill.json
|
|
1530
2889
|
const skillJsonPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(sourcePath, 'skill.json');
|
|
1531
2890
|
if (exists(skillJsonPath)) try {
|
|
1532
2891
|
const skillJson = readJson(skillJsonPath);
|
|
1533
2892
|
if (skillJson.version) semanticVersion = skillJson.version;
|
|
1534
|
-
} catch {
|
|
2893
|
+
} catch {
|
|
2894
|
+
// Ignore parse errors, use gitRef as fallback
|
|
2895
|
+
}
|
|
2896
|
+
// Create Installer with custom installDir from config
|
|
1535
2897
|
const defaults = this.config.getDefaults();
|
|
1536
2898
|
const installer = new Installer({
|
|
1537
2899
|
cwd: this.projectRoot,
|
|
1538
2900
|
global: this.isGlobal,
|
|
1539
2901
|
installDir: defaults.installDir
|
|
1540
2902
|
});
|
|
2903
|
+
// Install to all target agents
|
|
1541
2904
|
const results = await installer.installToAgents(sourcePath, skillName, targetAgents, {
|
|
1542
2905
|
mode: mode
|
|
1543
2906
|
});
|
|
2907
|
+
// Update lock file (project mode only)
|
|
1544
2908
|
if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
|
|
1545
2909
|
source: `${parsed.registry}:${parsed.owner}/${parsed.repo}${parsed.subPath ? `/${parsed.subPath}` : ''}`,
|
|
1546
2910
|
version: semanticVersion,
|
|
@@ -1548,15 +2912,20 @@ class SkillManager {
|
|
|
1548
2912
|
resolved: repoUrl,
|
|
1549
2913
|
commit: cacheResult.commit
|
|
1550
2914
|
});
|
|
2915
|
+
// Update skills.json (project mode only)
|
|
1551
2916
|
if (!this.isGlobal && save) {
|
|
1552
2917
|
this.config.ensureExists();
|
|
1553
|
-
|
|
2918
|
+
// Normalize the reference to use registry shorthand if possible
|
|
2919
|
+
const normalizedRef = this.config.normalizeSkillRef(ref);
|
|
2920
|
+
this.config.addSkill(skillName, normalizedRef);
|
|
1554
2921
|
}
|
|
2922
|
+
// Count results
|
|
1555
2923
|
const successCount = Array.from(results.values()).filter((r)=>r.success).length;
|
|
1556
2924
|
const failCount = results.size - successCount;
|
|
1557
2925
|
const displayVersion = semanticVersion !== gitRef ? `${semanticVersion} (${gitRef})` : gitRef;
|
|
1558
2926
|
if (0 === failCount) logger_logger.success(`Installed ${skillName}@${displayVersion} to ${successCount} agent(s)`);
|
|
1559
2927
|
else logger_logger.warn(`Installed ${skillName}@${displayVersion} to ${successCount} agent(s), ${failCount} failed`);
|
|
2928
|
+
// Build the InstalledSkill to return
|
|
1560
2929
|
const skill = {
|
|
1561
2930
|
name: skillName,
|
|
1562
2931
|
path: sourcePath,
|
|
@@ -1568,17 +2937,100 @@ class SkillManager {
|
|
|
1568
2937
|
results
|
|
1569
2938
|
};
|
|
1570
2939
|
}
|
|
1571
|
-
|
|
2940
|
+
/**
|
|
2941
|
+
* Install skill from HTTP/OSS to multiple agents
|
|
2942
|
+
*/ async installToAgentsFromHttp(ref, targetAgents, options = {}) {
|
|
2943
|
+
const { save = true, mode = 'symlink' } = options;
|
|
2944
|
+
// Parse HTTP reference
|
|
2945
|
+
const resolved = await this.httpResolver.resolve(ref);
|
|
2946
|
+
const { parsed, repoUrl, httpInfo } = resolved;
|
|
2947
|
+
const version = resolved.ref || 'latest';
|
|
2948
|
+
const skillName = httpInfo.skillName;
|
|
2949
|
+
logger_logger["package"](`Installing ${skillName}@${version} from ${httpInfo.host} to ${targetAgents.length} agent(s)...`);
|
|
2950
|
+
// Check cache
|
|
2951
|
+
let cacheResult = await this.cache.get(parsed, version);
|
|
2952
|
+
if (cacheResult) logger_logger.debug(`Using cached ${skillName}@${version}`);
|
|
2953
|
+
else {
|
|
2954
|
+
logger_logger.debug(`Downloading ${skillName}@${version} from ${repoUrl}`);
|
|
2955
|
+
cacheResult = await this.cache.cacheFromHttp(repoUrl, parsed, version);
|
|
2956
|
+
}
|
|
2957
|
+
// Get cache path as source
|
|
2958
|
+
const sourcePath = this.cache.getCachePath(parsed, version);
|
|
2959
|
+
// Read semantic version from skill.json
|
|
2960
|
+
let semanticVersion = version;
|
|
2961
|
+
const skillJsonPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(sourcePath, 'skill.json');
|
|
2962
|
+
if (exists(skillJsonPath)) try {
|
|
2963
|
+
const skillJson = readJson(skillJsonPath);
|
|
2964
|
+
if (skillJson.version) semanticVersion = skillJson.version;
|
|
2965
|
+
} catch {
|
|
2966
|
+
// Ignore parse errors
|
|
2967
|
+
}
|
|
2968
|
+
// Create Installer with custom installDir from config
|
|
2969
|
+
const defaults = this.config.getDefaults();
|
|
2970
|
+
const installer = new Installer({
|
|
2971
|
+
cwd: this.projectRoot,
|
|
2972
|
+
global: this.isGlobal,
|
|
2973
|
+
installDir: defaults.installDir
|
|
2974
|
+
});
|
|
2975
|
+
// Install to all target agents
|
|
2976
|
+
const results = await installer.installToAgents(sourcePath, skillName, targetAgents, {
|
|
2977
|
+
mode: mode
|
|
2978
|
+
});
|
|
2979
|
+
// Update lock file (project mode only)
|
|
2980
|
+
if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
|
|
2981
|
+
source: `http:${httpInfo.host}/${skillName}`,
|
|
2982
|
+
version: semanticVersion,
|
|
2983
|
+
ref: version,
|
|
2984
|
+
resolved: repoUrl,
|
|
2985
|
+
commit: cacheResult.commit
|
|
2986
|
+
});
|
|
2987
|
+
// Update skills.json (project mode only)
|
|
2988
|
+
if (!this.isGlobal && save) {
|
|
2989
|
+
this.config.ensureExists();
|
|
2990
|
+
this.config.addSkill(skillName, ref);
|
|
2991
|
+
}
|
|
2992
|
+
// Count results
|
|
2993
|
+
const successCount = Array.from(results.values()).filter((r)=>r.success).length;
|
|
2994
|
+
const failCount = results.size - successCount;
|
|
2995
|
+
const displayVersion = semanticVersion !== version ? `${semanticVersion} (${version})` : version;
|
|
2996
|
+
if (0 === failCount) logger_logger.success(`Installed ${skillName}@${displayVersion} to ${successCount} agent(s)`);
|
|
2997
|
+
else logger_logger.warn(`Installed ${skillName}@${displayVersion} to ${successCount} agent(s), ${failCount} failed`);
|
|
2998
|
+
// Build the InstalledSkill to return
|
|
2999
|
+
const skill = {
|
|
3000
|
+
name: skillName,
|
|
3001
|
+
path: sourcePath,
|
|
3002
|
+
version: semanticVersion,
|
|
3003
|
+
source: `http:${httpInfo.host}/${skillName}`
|
|
3004
|
+
};
|
|
3005
|
+
return {
|
|
3006
|
+
skill,
|
|
3007
|
+
results
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
/**
|
|
3011
|
+
* Get default target agents
|
|
3012
|
+
*
|
|
3013
|
+
* Priority:
|
|
3014
|
+
* 1. defaults.targetAgents in skills.json
|
|
3015
|
+
* 2. Auto-detect installed agents
|
|
3016
|
+
* 3. Return empty array
|
|
3017
|
+
*/ async getDefaultTargetAgents() {
|
|
3018
|
+
// Read from configuration
|
|
1572
3019
|
const defaults = this.config.getDefaults();
|
|
1573
3020
|
if (defaults.targetAgents && defaults.targetAgents.length > 0) return defaults.targetAgents.filter(isValidAgentType);
|
|
3021
|
+
// Auto-detect
|
|
1574
3022
|
return detectInstalledAgents();
|
|
1575
3023
|
}
|
|
1576
|
-
|
|
3024
|
+
/**
|
|
3025
|
+
* Get default installation mode
|
|
3026
|
+
*/ getDefaultInstallMode() {
|
|
1577
3027
|
const defaults = this.config.getDefaults();
|
|
1578
3028
|
if ('copy' === defaults.installMode || 'symlink' === defaults.installMode) return defaults.installMode;
|
|
1579
3029
|
return 'symlink';
|
|
1580
3030
|
}
|
|
1581
|
-
|
|
3031
|
+
/**
|
|
3032
|
+
* Validate agent type list
|
|
3033
|
+
*/ validateAgentTypes(agentNames) {
|
|
1582
3034
|
const valid = [];
|
|
1583
3035
|
const invalid = [];
|
|
1584
3036
|
for (const name of agentNames)if (isValidAgentType(name)) valid.push(name);
|
|
@@ -1588,10 +3040,14 @@ class SkillManager {
|
|
|
1588
3040
|
invalid
|
|
1589
3041
|
};
|
|
1590
3042
|
}
|
|
1591
|
-
|
|
3043
|
+
/**
|
|
3044
|
+
* Get all available agent types
|
|
3045
|
+
*/ getAllAgentTypes() {
|
|
1592
3046
|
return Object.keys(agents);
|
|
1593
3047
|
}
|
|
1594
|
-
|
|
3048
|
+
/**
|
|
3049
|
+
* Uninstall skill from specified agents
|
|
3050
|
+
*/ uninstallFromAgents(name, targetAgents) {
|
|
1595
3051
|
const defaults = this.config.getDefaults();
|
|
1596
3052
|
const installer = new Installer({
|
|
1597
3053
|
cwd: this.projectRoot,
|
|
@@ -1599,21 +3055,38 @@ class SkillManager {
|
|
|
1599
3055
|
installDir: defaults.installDir
|
|
1600
3056
|
});
|
|
1601
3057
|
const results = installer.uninstallFromAgents(name, targetAgents);
|
|
3058
|
+
// Remove from lock file (project mode only)
|
|
1602
3059
|
if (!this.isGlobal) this.lockManager.remove(name);
|
|
3060
|
+
// Remove from skills.json (project mode only)
|
|
1603
3061
|
if (!this.isGlobal && this.config.exists()) this.config.removeSkill(name);
|
|
1604
3062
|
const successCount = Array.from(results.values()).filter((r)=>r).length;
|
|
1605
3063
|
logger_logger.success(`Uninstalled ${name} from ${successCount} agent(s)`);
|
|
1606
3064
|
return results;
|
|
1607
3065
|
}
|
|
1608
3066
|
}
|
|
1609
|
-
|
|
3067
|
+
/**
|
|
3068
|
+
* Skill Parser - SKILL.md parser
|
|
3069
|
+
*
|
|
3070
|
+
* Following agentskills.io specification: https://agentskills.io/specification
|
|
3071
|
+
*
|
|
3072
|
+
* SKILL.md format requirements:
|
|
3073
|
+
* - YAML frontmatter containing name and description (required)
|
|
3074
|
+
* - name: max 64 characters, lowercase letters, numbers, hyphens
|
|
3075
|
+
* - description: max 1024 characters
|
|
3076
|
+
* - Optional fields: license, compatibility, metadata, allowed-tools
|
|
3077
|
+
*/ /**
|
|
3078
|
+
* Skill validation error
|
|
3079
|
+
*/ class SkillValidationError extends Error {
|
|
1610
3080
|
field;
|
|
1611
3081
|
constructor(message, field){
|
|
1612
3082
|
super(message), this.field = field;
|
|
1613
3083
|
this.name = 'SkillValidationError';
|
|
1614
3084
|
}
|
|
1615
3085
|
}
|
|
1616
|
-
|
|
3086
|
+
/**
|
|
3087
|
+
* Simple YAML frontmatter parser
|
|
3088
|
+
* Parses --- delimited YAML header
|
|
3089
|
+
*/ function parseFrontmatter(content) {
|
|
1617
3090
|
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
1618
3091
|
const match = content.match(frontmatterRegex);
|
|
1619
3092
|
if (!match) return {
|
|
@@ -1622,6 +3095,7 @@ function parseFrontmatter(content) {
|
|
|
1622
3095
|
};
|
|
1623
3096
|
const yamlContent = match[1];
|
|
1624
3097
|
const markdownContent = match[2];
|
|
3098
|
+
// Simple YAML parsing (supports basic key: value format)
|
|
1625
3099
|
const data = {};
|
|
1626
3100
|
const lines = yamlContent.split('\n');
|
|
1627
3101
|
let currentKey = '';
|
|
@@ -1630,19 +3104,25 @@ function parseFrontmatter(content) {
|
|
|
1630
3104
|
for (const line of lines){
|
|
1631
3105
|
const trimmedLine = line.trim();
|
|
1632
3106
|
if (!trimmedLine || trimmedLine.startsWith('#')) continue;
|
|
3107
|
+
// Check if it's a new key: value pair
|
|
1633
3108
|
const keyValueMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
|
|
1634
3109
|
if (keyValueMatch && !inMultiline) {
|
|
3110
|
+
// Save previous value
|
|
1635
3111
|
if (currentKey) data[currentKey] = parseYamlValue(currentValue.trim());
|
|
1636
3112
|
currentKey = keyValueMatch[1];
|
|
1637
3113
|
currentValue = keyValueMatch[2];
|
|
3114
|
+
// Check if it's start of multiline string
|
|
1638
3115
|
if ('|' === currentValue || '>' === currentValue) {
|
|
1639
3116
|
inMultiline = true;
|
|
1640
3117
|
currentValue = '';
|
|
1641
3118
|
}
|
|
1642
|
-
} else if (inMultiline && line.startsWith(' '))
|
|
3119
|
+
} else if (inMultiline && line.startsWith(' ')) // Multiline string continuation
|
|
3120
|
+
currentValue += (currentValue ? '\n' : '') + line.slice(2);
|
|
1643
3121
|
else if (inMultiline && !line.startsWith(' ')) {
|
|
3122
|
+
// Multiline string end
|
|
1644
3123
|
inMultiline = false;
|
|
1645
3124
|
data[currentKey] = currentValue.trim();
|
|
3125
|
+
// Try to parse new line
|
|
1646
3126
|
const newKeyMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
|
|
1647
3127
|
if (newKeyMatch) {
|
|
1648
3128
|
currentKey = newKeyMatch[1];
|
|
@@ -1650,49 +3130,80 @@ function parseFrontmatter(content) {
|
|
|
1650
3130
|
}
|
|
1651
3131
|
}
|
|
1652
3132
|
}
|
|
3133
|
+
// Save last value
|
|
1653
3134
|
if (currentKey) data[currentKey] = parseYamlValue(currentValue.trim());
|
|
1654
3135
|
return {
|
|
1655
3136
|
data,
|
|
1656
3137
|
content: markdownContent
|
|
1657
3138
|
};
|
|
1658
3139
|
}
|
|
1659
|
-
|
|
3140
|
+
/**
|
|
3141
|
+
* Parse YAML value
|
|
3142
|
+
*/ function parseYamlValue(value) {
|
|
1660
3143
|
if (!value) return '';
|
|
3144
|
+
// Boolean value
|
|
1661
3145
|
if ('true' === value) return true;
|
|
1662
3146
|
if ('false' === value) return false;
|
|
3147
|
+
// Number
|
|
1663
3148
|
if (/^-?\d+$/.test(value)) return parseInt(value, 10);
|
|
1664
3149
|
if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
|
|
3150
|
+
// Remove quotes
|
|
1665
3151
|
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
1666
3152
|
return value;
|
|
1667
3153
|
}
|
|
1668
|
-
|
|
3154
|
+
/**
|
|
3155
|
+
* Validate skill name format
|
|
3156
|
+
*
|
|
3157
|
+
* Specification requirements:
|
|
3158
|
+
* - Max 64 characters
|
|
3159
|
+
* - Only lowercase letters, numbers, hyphens allowed
|
|
3160
|
+
* - Cannot start or end with hyphen
|
|
3161
|
+
* - Cannot contain consecutive hyphens
|
|
3162
|
+
*/ function validateSkillName(name) {
|
|
1669
3163
|
if (!name) throw new SkillValidationError('Skill name is required', 'name');
|
|
1670
3164
|
if (name.length > 64) throw new SkillValidationError('Skill name must be at most 64 characters', 'name');
|
|
1671
3165
|
if (!/^[a-z0-9]/.test(name)) throw new SkillValidationError('Skill name must start with a lowercase letter or number', 'name');
|
|
1672
3166
|
if (!/[a-z0-9]$/.test(name)) throw new SkillValidationError('Skill name must end with a lowercase letter or number', 'name');
|
|
1673
3167
|
if (/--/.test(name)) throw new SkillValidationError('Skill name cannot contain consecutive hyphens', 'name');
|
|
1674
3168
|
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(name) && name.length > 1) throw new SkillValidationError('Skill name can only contain lowercase letters, numbers, and hyphens', 'name');
|
|
3169
|
+
// Single character name
|
|
1675
3170
|
if (1 === name.length && !/^[a-z0-9]$/.test(name)) throw new SkillValidationError('Single character skill name must be a lowercase letter or number', 'name');
|
|
1676
3171
|
}
|
|
1677
|
-
|
|
3172
|
+
/**
|
|
3173
|
+
* Validate skill description
|
|
3174
|
+
*
|
|
3175
|
+
* Specification requirements:
|
|
3176
|
+
* - Max 1024 characters
|
|
3177
|
+
* - Cannot contain angle brackets
|
|
3178
|
+
*/ function validateSkillDescription(description) {
|
|
1678
3179
|
if (!description) throw new SkillValidationError('Skill description is required', 'description');
|
|
1679
3180
|
if (description.length > 1024) throw new SkillValidationError('Skill description must be at most 1024 characters', 'description');
|
|
1680
3181
|
if (/<|>/.test(description)) throw new SkillValidationError('Skill description cannot contain angle brackets', 'description');
|
|
1681
3182
|
}
|
|
1682
|
-
|
|
3183
|
+
/**
|
|
3184
|
+
* Parse SKILL.md content
|
|
3185
|
+
*
|
|
3186
|
+
* @param content - SKILL.md file content
|
|
3187
|
+
* @param options - Parse options
|
|
3188
|
+
* @returns Parsed skill info, or null if format is invalid
|
|
3189
|
+
* @throws SkillValidationError if validation fails in strict mode
|
|
3190
|
+
*/ function parseSkillMd(content, options = {}) {
|
|
1683
3191
|
const { strict = false } = options;
|
|
1684
3192
|
try {
|
|
1685
3193
|
const { data, content: body } = parseFrontmatter(content);
|
|
3194
|
+
// Check required fields
|
|
1686
3195
|
if (!data.name || !data.description) {
|
|
1687
3196
|
if (strict) throw new SkillValidationError('SKILL.md must have name and description in frontmatter');
|
|
1688
3197
|
return null;
|
|
1689
3198
|
}
|
|
1690
3199
|
const name = String(data.name);
|
|
1691
3200
|
const description = String(data.description);
|
|
3201
|
+
// Validate field format
|
|
1692
3202
|
if (strict) {
|
|
1693
3203
|
validateSkillName(name);
|
|
1694
3204
|
validateSkillDescription(description);
|
|
1695
3205
|
}
|
|
3206
|
+
// Parse allowed-tools
|
|
1696
3207
|
let allowedTools;
|
|
1697
3208
|
if (data['allowed-tools']) {
|
|
1698
3209
|
const toolsStr = String(data['allowed-tools']);
|
|
@@ -1714,7 +3225,9 @@ function parseSkillMd(content, options = {}) {
|
|
|
1714
3225
|
return null;
|
|
1715
3226
|
}
|
|
1716
3227
|
}
|
|
1717
|
-
|
|
3228
|
+
/**
|
|
3229
|
+
* Parse SKILL.md from file path
|
|
3230
|
+
*/ function parseSkillMdFile(filePath, options = {}) {
|
|
1718
3231
|
if (!external_node_fs_.existsSync(filePath)) {
|
|
1719
3232
|
if (options.strict) throw new SkillValidationError(`SKILL.md not found: ${filePath}`);
|
|
1720
3233
|
return null;
|
|
@@ -1722,11 +3235,15 @@ function parseSkillMdFile(filePath, options = {}) {
|
|
|
1722
3235
|
const content = external_node_fs_.readFileSync(filePath, 'utf-8');
|
|
1723
3236
|
return parseSkillMd(content, options);
|
|
1724
3237
|
}
|
|
1725
|
-
|
|
3238
|
+
/**
|
|
3239
|
+
* Parse SKILL.md from skill directory
|
|
3240
|
+
*/ function parseSkillFromDir(dirPath, options = {}) {
|
|
1726
3241
|
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
|
|
1727
3242
|
return parseSkillMdFile(skillMdPath, options);
|
|
1728
3243
|
}
|
|
1729
|
-
|
|
3244
|
+
/**
|
|
3245
|
+
* Check if directory contains valid SKILL.md
|
|
3246
|
+
*/ function hasValidSkillMd(dirPath) {
|
|
1730
3247
|
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
|
|
1731
3248
|
if (!external_node_fs_.existsSync(skillMdPath)) return false;
|
|
1732
3249
|
try {
|
|
@@ -1736,7 +3253,9 @@ function hasValidSkillMd(dirPath) {
|
|
|
1736
3253
|
return false;
|
|
1737
3254
|
}
|
|
1738
3255
|
}
|
|
1739
|
-
|
|
3256
|
+
/**
|
|
3257
|
+
* Generate SKILL.md content
|
|
3258
|
+
*/ function generateSkillMd(skill) {
|
|
1740
3259
|
const frontmatter = [
|
|
1741
3260
|
'---'
|
|
1742
3261
|
];
|
|
@@ -1749,4 +3268,4 @@ function generateSkillMd(skill) {
|
|
|
1749
3268
|
frontmatter.push('');
|
|
1750
3269
|
return frontmatter.join('\n') + skill.content;
|
|
1751
3270
|
}
|
|
1752
|
-
export { CacheManager, ConfigLoader, DEFAULT_REGISTRIES, GitResolver, Installer, LockManager, SkillManager, SkillValidationError, agents, detectInstalledAgents, generateSkillMd, getAgentConfig, getAgentSkillsDir, getAllAgentTypes, getCanonicalSkillPath, getCanonicalSkillsDir, hasValidSkillMd, isPathSafe, isValidAgentType, logger_logger as logger, parseSkillFromDir, parseSkillMd, parseSkillMdFile, sanitizeName, shortenPath, validateSkillDescription, validateSkillName };
|
|
3271
|
+
export { CacheManager, ConfigLoader, DEFAULT_REGISTRIES, GitResolver, HttpResolver, Installer, LockManager, SkillManager, SkillValidationError, agents, detectInstalledAgents, generateSkillMd, getAgentConfig, getAgentSkillsDir, getAllAgentTypes, getCanonicalSkillPath, getCanonicalSkillsDir, hasValidSkillMd, isPathSafe, isValidAgentType, logger_logger as logger, parseSkillFromDir, parseSkillMd, parseSkillMdFile, sanitizeName, shortenPath, validateSkillDescription, validateSkillName };
|