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/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
- const agent_registry_home = (0, __WEBPACK_EXTERNAL_MODULE_node_os__.homedir)();
25
- const agents = {
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
- function getAllAgentTypes() {
161
+ /**
162
+ * Get all Agent type list
163
+ */ function getAllAgentTypes() {
147
164
  return Object.keys(agents);
148
165
  }
149
- async function detectInstalledAgents() {
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
- function getAgentConfig(type) {
173
+ /**
174
+ * Get Agent configuration
175
+ */ function getAgentConfig(type) {
155
176
  return agents[type];
156
177
  }
157
- function isValidAgentType(type) {
178
+ /**
179
+ * Validate if Agent type is valid
180
+ */ function isValidAgentType(type) {
158
181
  return type in agents;
159
182
  }
160
- function getAgentSkillsDir(type, options = {}) {
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
- function exists(filePath) {
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
- function readJson(filePath) {
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
- function writeJson(filePath, data, indent = 2) {
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
- function ensureDir(dirPath) {
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
- function remove(targetPath) {
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
- function copyDir(src, dest, options) {
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
- function listDir(dirPath) {
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
- function isDirectory(targetPath) {
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
- function isSymlink(targetPath) {
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
- function getRealPath(linkPath) {
269
+ /**
270
+ * Get real path of symbolic link
271
+ */ function getRealPath(linkPath) {
219
272
  return external_node_fs_.realpathSync(linkPath);
220
273
  }
221
- function getSkillsJsonPath(projectRoot) {
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
- function getSkillsLockPath(projectRoot) {
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
- function getCacheDir() {
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
- function getHomeDir() {
292
+ /**
293
+ * Get home directory
294
+ */ function getHomeDir() {
234
295
  return process.env.HOME || process.env.USERPROFILE || '';
235
296
  }
236
- function getGlobalSkillsDir() {
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
- const AGENTS_DIR = '.agents';
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
- function getCanonicalSkillsDir(options = {}) {
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
- function getCanonicalSkillPath(skillName, options = {}) {
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
- function shortenPath(fullPath, cwd) {
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
- function isPathSafe(basePath, targetPath) {
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
- function sanitizeName(name) {
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
- class GitCloneError extends Error {
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
- static detectUrlType(url) {
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
- static isAuthenticationError(message) {
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
- async function git(args, cwd) {
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
- async function getRemoteTags(repoUrl) {
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
- async function getLatestTag(repoUrl) {
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
- async function clone(repoUrl, destPath, options) {
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
- async function getCurrentCommit(cwd) {
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
- async function getDefaultBranch(repoUrl) {
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
- function compareVersions(a, b) {
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
- function buildRepoUrl(registry, ownerRepo) {
406
- const registryUrls = {
407
- github: 'https://github.com',
408
- gitlab: 'https://gitlab.com'
409
- };
410
- const baseUrl = registryUrls[registry] || `https://${registry}`;
411
- return `${baseUrl}/${ownerRepo}`;
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
- function parseGitUrl(url) {
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
- const installer_AGENTS_DIR = '.agents';
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
- const DEFAULT_EXCLUDE_FILES = [
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
- const EXCLUDE_PREFIX = '_';
460
- function installer_sanitizeName(name) {
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
- function installer_isPathSafe(basePath, targetPath) {
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
- function installer_getCanonicalSkillsDir(isGlobal, cwd, installDir) {
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
- function installer_ensureDir(dirPath) {
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
- function installer_remove(targetPath) {
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
- function copyDirectory(src, dest, options) {
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
- async function installer_createSymlink(target, linkPath) {
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
- class Installer {
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
- getCanonicalPath(skillName) {
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
- getAgentSkillPath(skillName, agentType) {
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
- async installForAgent(sourcePath, skillName, agentType, options = {}) {
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
- async installToAgents(sourcePath, skillName, targetAgents, options = {}) {
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
- isInstalled(skillName, agentType) {
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
- uninstallFromAgent(skillName, agentType) {
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
- uninstallFromAgents(skillName, targetAgents) {
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
- listInstalledSkills(agentType) {
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
- class CacheManager {
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
- getCacheDir() {
1106
+ /**
1107
+ * Get cache directory
1108
+ */ getCacheDir() {
657
1109
  return this.cacheDir;
658
1110
  }
659
- getSkillCachePath(parsed, version) {
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
- getCachePath(parsed, version) {
1121
+ /**
1122
+ * Get cache path (alias for getSkillCachePath)
1123
+ */ getCachePath(parsed, version) {
663
1124
  return this.getSkillCachePath(parsed, version);
664
1125
  }
665
- isCached(parsed, version) {
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
- async get(parsed, version) {
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
- async cache(repoUrl, parsed, ref, version) {
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
- const fs = await import("node:fs");
711
- fs.writeFileSync(__WEBPACK_EXTERNAL_MODULE_node_path__.join(cachePath, '.reskill-commit'), commit);
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
- async copyTo(parsed, version, destPath) {
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
- clearSkill(parsed, version) {
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
- clearAll() {
1250
+ /**
1251
+ * Clear all cache
1252
+ */ clearAll() {
736
1253
  remove(this.cacheDir);
737
1254
  }
738
- getStats() {
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
- async getRemoteCommit(repoUrl, ref) {
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
- const DEFAULT_SKILLS_JSON = {
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
- const DEFAULT_VALUES = {
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
- const DEFAULT_REGISTRIES = {
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
- class ConfigLoader {
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
- getProjectRoot() {
1371
+ // ==========================================================================
1372
+ // Path Accessors
1373
+ // ==========================================================================
1374
+ /**
1375
+ * Get project root directory
1376
+ */ getProjectRoot() {
809
1377
  return this.projectRoot;
810
1378
  }
811
- getConfigPath() {
1379
+ /**
1380
+ * Get configuration file path
1381
+ */ getConfigPath() {
812
1382
  return this.configPath;
813
1383
  }
814
- getInstallDir() {
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
- exists() {
1390
+ // ==========================================================================
1391
+ // File Operations
1392
+ // ==========================================================================
1393
+ /**
1394
+ * Check if configuration file exists
1395
+ */ exists() {
819
1396
  return exists(this.configPath);
820
1397
  }
821
- load() {
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
- reload() {
1412
+ /**
1413
+ * Reload configuration from file (ignores cache)
1414
+ */ reload() {
832
1415
  this.config = null;
833
1416
  return this.load();
834
1417
  }
835
- save(config) {
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
- ensureExists() {
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
- create(options) {
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
- getDefaults() {
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
- updateDefaults(updates) {
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
- getRegistryUrl(registryName) {
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
- addSkill(name, ref) {
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
- removeSkill(name) {
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
- getSkills() {
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
- hasSkill(name) {
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
- getSkillRef(name) {
1688
+ /**
1689
+ * Get skill reference by name
1690
+ */ getSkillRef(name) {
914
1691
  const skills = this.getSkills();
915
1692
  return skills[name];
916
1693
  }
917
- getConfigOrDefault() {
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
- ensureConfigLoaded() {
1704
+ /**
1705
+ * Ensure config is loaded into memory
1706
+ */ ensureConfigLoaded() {
923
1707
  if (!this.config) this.load();
924
1708
  }
925
1709
  }
926
- class GitResolver {
927
- defaultRegistry = 'github';
928
- parseRef(ref) {
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
- parseGitUrlRef(ref) {
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
- parseVersion(versionSpec) {
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
- buildRepoUrl(parsed) {
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
- return buildRepoUrl(parsed.registry, `${parsed.owner}/${parsed.repo}`);
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
- async resolveVersion(repoUrl, versionSpec) {
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
- async resolve(ref) {
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
- const LOCKFILE_VERSION = 1;
1112
- class LockManager {
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
- getLockPath() {
2210
+ /**
2211
+ * Get lock file path
2212
+ */ getLockPath() {
1121
2213
  return this.lockPath;
1122
2214
  }
1123
- exists() {
2215
+ /**
2216
+ * Check if lock file exists
2217
+ */ exists() {
1124
2218
  return exists(this.lockPath);
1125
2219
  }
1126
- load() {
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
- reload() {
2239
+ /**
2240
+ * Reload lock file
2241
+ */ reload() {
1143
2242
  this.lockData = null;
1144
2243
  return this.load();
1145
2244
  }
1146
- save(lockToSave) {
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
- get(name) {
2253
+ /**
2254
+ * Get locked skill
2255
+ */ get(name) {
1153
2256
  const lock = this.load();
1154
2257
  return lock.skills[name];
1155
2258
  }
1156
- set(name, skill) {
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
- remove(name) {
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
- lockSkill(name, options) {
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
- getAll() {
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
- has(name) {
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
- isVersionMatch(name, version) {
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
- clear() {
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
- delete() {
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
- const logger_logger = {
1213
- info (message) {
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
- success (message) {
2339
+ /**
2340
+ * Success message (green)
2341
+ */ success (message) {
1217
2342
  console.log(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].green('✅'), message);
1218
2343
  },
1219
- warn (message) {
2344
+ /**
2345
+ * Warning message (yellow)
2346
+ */ warn (message) {
1220
2347
  console.log(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].yellow('⚠️'), message);
1221
2348
  },
1222
- error (message) {
2349
+ /**
2350
+ * Error message (red)
2351
+ */ error (message) {
1223
2352
  console.error(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].red('❌'), message);
1224
2353
  },
1225
- debug (message) {
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
- package (message) {
2359
+ /**
2360
+ * Package/skill message (package emoji)
2361
+ */ package (message) {
1229
2362
  console.log(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].cyan('📦'), message);
1230
2363
  },
1231
- log (message) {
2364
+ /**
2365
+ * Plain message without icon
2366
+ */ log (message) {
1232
2367
  console.log(message);
1233
2368
  },
1234
- newline () {
2369
+ /**
2370
+ * Newline
2371
+ */ newline () {
1235
2372
  console.log();
1236
2373
  },
1237
- table (headers, rows) {
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
- class SkillManager {
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
- this.resolver = new GitResolver();
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
- isGlobalMode() {
2424
+ /**
2425
+ * Check if in global mode
2426
+ */ isGlobalMode() {
1269
2427
  return this.isGlobal;
1270
2428
  }
1271
- getProjectRoot() {
2429
+ /**
2430
+ * Get project root directory
2431
+ */ getProjectRoot() {
1272
2432
  return this.projectRoot;
1273
2433
  }
1274
- getInstallDir() {
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
- getCanonicalSkillsDir() {
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
- getSkillPath(name) {
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
- async install(ref, options = {}) {
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
- let semanticVersion = gitRef;
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
- this.config.addSkill(skillName, ref);
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
- async installAll(options = {}) {
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
- uninstall(name) {
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
- checkNeedsUpdate(name, remoteCommit) {
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
- async update(name) {
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
- const resolved = await this.resolver.resolve(ref);
1390
- const remoteCommit = await this.cache.getRemoteCommit(resolved.repoUrl, resolved.ref);
1391
- if (!this.checkNeedsUpdate(name, remoteCommit)) {
1392
- logger_logger.info(`${name} is already up to date`);
1393
- return [];
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
- const resolved = await this.resolver.resolve(ref);
1404
- const remoteCommit = await this.cache.getRemoteCommit(resolved.repoUrl, resolved.ref);
1405
- if (!this.checkNeedsUpdate(skillName, remoteCommit)) {
1406
- logger_logger.info(`${skillName} is already up to date`);
1407
- continue;
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
- list() {
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
- getInstalledSkillFromPath(name, skillPath) {
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
- getInstalledSkill(name) {
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
- getInfo(name) {
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
- async checkOutdated() {
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
- async installToAgents(ref, targetAgents, options = {}) {
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
- let semanticVersion = gitRef;
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
- this.config.addSkill(skillName, ref);
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
- async getDefaultTargetAgents() {
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
- getDefaultInstallMode() {
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
- validateAgentTypes(agentNames) {
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
- getAllAgentTypes() {
3043
+ /**
3044
+ * Get all available agent types
3045
+ */ getAllAgentTypes() {
1592
3046
  return Object.keys(agents);
1593
3047
  }
1594
- uninstallFromAgents(name, targetAgents) {
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
- class SkillValidationError extends Error {
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
- function parseFrontmatter(content) {
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(' ')) currentValue += (currentValue ? '\n' : '') + line.slice(2);
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
- function parseYamlValue(value) {
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
- function validateSkillName(name) {
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
- function validateSkillDescription(description) {
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
- function parseSkillMd(content, options = {}) {
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
- function parseSkillMdFile(filePath, options = {}) {
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
- function parseSkillFromDir(dirPath, options = {}) {
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
- function hasValidSkillMd(dirPath) {
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
- function generateSkillMd(skill) {
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 };