reskill 1.20.3 → 1.21.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/cli/index.js CHANGED
@@ -156,10 +156,616 @@ var external_node_fs_ = __webpack_require__("node:fs");
156
156
  └────────────────────────────────────────────────────┘
157
157
  `;
158
158
  }
159
+ /**
160
+ * Skill Parser - SKILL.md parser
161
+ *
162
+ * Following agentskills.io specification: https://agentskills.io/specification
163
+ *
164
+ * SKILL.md format requirements:
165
+ * - YAML frontmatter containing name and description (required)
166
+ * - name: max 64 characters, lowercase letters, numbers, hyphens
167
+ * - description: max 1024 characters
168
+ * - Optional fields: license, compatibility, metadata, allowed-tools
169
+ */ /**
170
+ * Skill validation error
171
+ */ class SkillValidationError extends Error {
172
+ field;
173
+ constructor(message, field){
174
+ super(message), this.field = field;
175
+ this.name = 'SkillValidationError';
176
+ }
177
+ }
178
+ /**
179
+ * Simple YAML frontmatter parser
180
+ * Parses --- delimited YAML header
181
+ *
182
+ * Supports:
183
+ * - Basic key: value pairs
184
+ * - Multiline strings (| and >)
185
+ * - Nested objects (one level deep, for metadata field)
186
+ */ function parseFrontmatter(content) {
187
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
188
+ const match = content.match(frontmatterRegex);
189
+ if (!match) return {
190
+ data: {},
191
+ content
192
+ };
193
+ const yamlContent = match[1];
194
+ const markdownContent = match[2];
195
+ // Simple YAML parsing (supports basic key: value, one level of nesting,
196
+ // block scalars (| and >), and plain scalars spanning multiple indented lines)
197
+ const data = {};
198
+ const lines = yamlContent.split('\n');
199
+ let currentKey = '';
200
+ let currentValue = '';
201
+ let inMultiline = false;
202
+ let inNestedObject = false;
203
+ let inPlainScalar = false;
204
+ let nestedObject = {};
205
+ /**
206
+ * Save the current key/value accumulated so far, then reset state.
207
+ */ function flushCurrent() {
208
+ if (!currentKey) return;
209
+ if (inNestedObject) {
210
+ data[currentKey] = nestedObject;
211
+ nestedObject = {};
212
+ inNestedObject = false;
213
+ } else if (inPlainScalar || inMultiline) {
214
+ data[currentKey] = currentValue.trim();
215
+ inPlainScalar = false;
216
+ inMultiline = false;
217
+ } else data[currentKey] = parseYamlValue(currentValue.trim());
218
+ currentKey = '';
219
+ currentValue = '';
220
+ }
221
+ for (const line of lines){
222
+ const trimmedLine = line.trim();
223
+ if (!trimmedLine || trimmedLine.startsWith('#')) continue;
224
+ const isIndented = line.startsWith(' ');
225
+ // ---- Inside a block scalar (| or >) ----
226
+ if (inMultiline) {
227
+ if (isIndented) {
228
+ currentValue += (currentValue ? '\n' : '') + line.slice(2);
229
+ continue;
230
+ }
231
+ // Unindented line ends the block scalar — fall through to top-level parsing
232
+ flushCurrent();
233
+ }
234
+ // ---- Inside a plain scalar (multiline value without | or >) ----
235
+ if (inPlainScalar) {
236
+ if (isIndented) {
237
+ // Continuation line: join with a space (YAML plain scalar folding)
238
+ currentValue += ` ${trimmedLine}`;
239
+ continue;
240
+ }
241
+ // Unindented line ends the plain scalar — fall through to top-level parsing
242
+ flushCurrent();
243
+ }
244
+ // ---- Inside a nested object ----
245
+ if (inNestedObject && isIndented) {
246
+ const nestedMatch = line.match(/^ {2}([a-zA-Z_-]+):\s*(.*)$/);
247
+ if (nestedMatch) {
248
+ const [, nestedKey, nestedValue] = nestedMatch;
249
+ nestedObject[nestedKey] = parseYamlValue(nestedValue.trim());
250
+ continue;
251
+ }
252
+ // Indented line that isn't a nested key:value — this key was actually
253
+ // a plain scalar, not a nested object. Switch modes.
254
+ inNestedObject = false;
255
+ inPlainScalar = true;
256
+ currentValue = trimmedLine;
257
+ continue;
258
+ }
259
+ // ---- Top-level key: value ----
260
+ const keyValueMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
261
+ if (keyValueMatch) {
262
+ flushCurrent();
263
+ currentKey = keyValueMatch[1];
264
+ currentValue = keyValueMatch[2];
265
+ if ('|' === currentValue || '>' === currentValue) {
266
+ inMultiline = true;
267
+ currentValue = '';
268
+ } else if ('' === currentValue) {
269
+ // Empty value — could be nested object or plain scalar; peek at next lines
270
+ inNestedObject = true;
271
+ nestedObject = {};
272
+ }
273
+ continue;
274
+ }
275
+ // ---- Unindented line that isn't key:value while in nested object ----
276
+ if (inNestedObject) flushCurrent();
277
+ }
278
+ // Save last accumulated value
279
+ flushCurrent();
280
+ return {
281
+ data,
282
+ content: markdownContent
283
+ };
284
+ }
285
+ /**
286
+ * Parse YAML value
287
+ */ function parseYamlValue(value) {
288
+ if (!value) return '';
289
+ // Boolean value
290
+ if ('true' === value) return true;
291
+ if ('false' === value) return false;
292
+ // Number
293
+ if (/^-?\d+$/.test(value)) return parseInt(value, 10);
294
+ if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
295
+ // Remove quotes
296
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
297
+ return value;
298
+ }
299
+ /**
300
+ * Validate skill name format
301
+ *
302
+ * Specification requirements:
303
+ * - Max 64 characters
304
+ * - Only lowercase letters, numbers, hyphens allowed
305
+ * - Cannot start or end with hyphen
306
+ * - Cannot contain consecutive hyphens
307
+ */ function validateSkillName(name) {
308
+ if (!name) throw new SkillValidationError('Skill name is required', 'name');
309
+ if (name.length > 64) throw new SkillValidationError('Skill name must be at most 64 characters', 'name');
310
+ if (!/^[a-z0-9]/.test(name)) throw new SkillValidationError('Skill name must start with a lowercase letter or number', 'name');
311
+ if (!/[a-z0-9]$/.test(name)) throw new SkillValidationError('Skill name must end with a lowercase letter or number', 'name');
312
+ if (/--/.test(name)) throw new SkillValidationError('Skill name cannot contain consecutive hyphens', 'name');
313
+ 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');
314
+ // Single character name
315
+ if (1 === name.length && !/^[a-z0-9]$/.test(name)) throw new SkillValidationError('Single character skill name must be a lowercase letter or number', 'name');
316
+ }
317
+ /**
318
+ * Validate skill description
319
+ *
320
+ * Specification requirements:
321
+ * - Max 1024 characters
322
+ * - Angle brackets are allowed per agentskills.io spec
323
+ */ function validateSkillDescription(description) {
324
+ if (!description) throw new SkillValidationError('Skill description is required', 'description');
325
+ if (description.length > 1024) throw new SkillValidationError('Skill description must be at most 1024 characters', 'description');
326
+ // Note: angle brackets are allowed per agentskills.io spec
327
+ }
328
+ /**
329
+ * Parse SKILL.md content
330
+ *
331
+ * @param content - SKILL.md file content
332
+ * @param options - Parse options
333
+ * @returns Parsed skill info, or null if format is invalid
334
+ * @throws SkillValidationError if validation fails in strict mode
335
+ */ function parseSkillMd(content, options = {}) {
336
+ const { strict = false } = options;
337
+ try {
338
+ const { data, content: body } = parseFrontmatter(content);
339
+ // Check required fields
340
+ if (!data.name || !data.description) {
341
+ if (strict) throw new SkillValidationError('SKILL.md must have name and description in frontmatter');
342
+ return null;
343
+ }
344
+ const name = String(data.name);
345
+ const description = String(data.description);
346
+ // Validate field format
347
+ if (strict) {
348
+ validateSkillName(name);
349
+ validateSkillDescription(description);
350
+ }
351
+ // Parse allowed-tools
352
+ let allowedTools;
353
+ if (data['allowed-tools']) {
354
+ const toolsStr = String(data['allowed-tools']);
355
+ allowedTools = toolsStr.split(/\s+/).filter(Boolean);
356
+ }
357
+ return {
358
+ name,
359
+ description,
360
+ version: data.version ? String(data.version) : void 0,
361
+ license: data.license ? String(data.license) : void 0,
362
+ compatibility: data.compatibility ? String(data.compatibility) : void 0,
363
+ metadata: data.metadata,
364
+ allowedTools,
365
+ content: body,
366
+ rawContent: content
367
+ };
368
+ } catch (error) {
369
+ if (error instanceof SkillValidationError) throw error;
370
+ if (strict) throw new SkillValidationError(`Failed to parse SKILL.md: ${error}`);
371
+ return null;
372
+ }
373
+ }
374
+ /**
375
+ * Parse SKILL.md from file path
376
+ */ function parseSkillMdFile(filePath, options = {}) {
377
+ if (!external_node_fs_.existsSync(filePath)) {
378
+ if (options.strict) throw new SkillValidationError(`SKILL.md not found: ${filePath}`);
379
+ return null;
380
+ }
381
+ const content = external_node_fs_.readFileSync(filePath, 'utf-8');
382
+ return parseSkillMd(content, options);
383
+ }
384
+ /**
385
+ * Parse SKILL.md from skill directory
386
+ */ function parseSkillFromDir(dirPath, options = {}) {
387
+ const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
388
+ return parseSkillMdFile(skillMdPath, options);
389
+ }
390
+ /**
391
+ * Check if directory contains valid SKILL.md
392
+ */ function hasValidSkillMd(dirPath) {
393
+ const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
394
+ if (!external_node_fs_.existsSync(skillMdPath)) return false;
395
+ try {
396
+ const skill = parseSkillMdFile(skillMdPath);
397
+ return null !== skill;
398
+ } catch {
399
+ return false;
400
+ }
401
+ }
402
+ const SKIP_DIRS = [
403
+ 'node_modules',
404
+ '.git',
405
+ 'dist',
406
+ 'build',
407
+ '__pycache__'
408
+ ];
409
+ const MAX_DISCOVER_DEPTH = 5;
410
+ const PRIORITY_SKILL_DIRS = [
411
+ 'skills',
412
+ '.agents/skills',
413
+ '.cursor/skills',
414
+ '.claude/skills',
415
+ '.windsurf/skills',
416
+ '.github/skills'
417
+ ];
418
+ function findSkillDirsRecursive(dir, depth, maxDepth, visitedDirs) {
419
+ if (depth > maxDepth) return [];
420
+ const resolvedDir = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir);
421
+ if (visitedDirs.has(resolvedDir)) return [];
422
+ if (!external_node_fs_.existsSync(dir) || !external_node_fs_.statSync(dir).isDirectory()) return [];
423
+ visitedDirs.add(resolvedDir);
424
+ const results = [];
425
+ let entries;
426
+ try {
427
+ entries = external_node_fs_.readdirSync(dir);
428
+ } catch {
429
+ return [];
430
+ }
431
+ for (const entry of entries){
432
+ if (SKIP_DIRS.includes(entry)) continue;
433
+ const fullPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
434
+ const resolvedFull = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(fullPath);
435
+ if (visitedDirs.has(resolvedFull)) continue;
436
+ let stat;
437
+ try {
438
+ stat = external_node_fs_.statSync(fullPath);
439
+ } catch {
440
+ continue;
441
+ }
442
+ if (!!stat.isDirectory()) {
443
+ if (hasValidSkillMd(fullPath)) results.push(fullPath);
444
+ results.push(...findSkillDirsRecursive(fullPath, depth + 1, maxDepth, visitedDirs));
445
+ }
446
+ }
447
+ return results;
448
+ }
449
+ /**
450
+ * Discover all skills in a directory by scanning for SKILL.md files.
451
+ *
452
+ * Strategy:
453
+ * 1. Check root for SKILL.md
454
+ * 2. Search priority directories (skills/, .agents/skills/, .cursor/skills/, etc.)
455
+ * 3. Fall back to recursive search (max depth 5, skip node_modules, .git, dist, etc.)
456
+ *
457
+ * @param basePath - Root directory to search
458
+ * @returns List of parsed skills with their directory paths (absolute)
459
+ */ function discoverSkillsInDir(basePath) {
460
+ const resolvedBase = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(basePath);
461
+ const results = [];
462
+ const seenNames = new Set();
463
+ function addSkill(dirPath) {
464
+ const skill = parseSkillFromDir(dirPath);
465
+ if (skill && !seenNames.has(skill.name)) {
466
+ seenNames.add(skill.name);
467
+ results.push({
468
+ ...skill,
469
+ dirPath: __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dirPath)
470
+ });
471
+ }
472
+ }
473
+ if (hasValidSkillMd(resolvedBase)) addSkill(resolvedBase);
474
+ // Track visited directories to avoid redundant I/O during recursive scan
475
+ const visitedDirs = new Set();
476
+ for (const sub of PRIORITY_SKILL_DIRS){
477
+ const dir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(resolvedBase, sub);
478
+ if (!!external_node_fs_.existsSync(dir) && !!external_node_fs_.statSync(dir).isDirectory()) {
479
+ visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir));
480
+ try {
481
+ const entries = external_node_fs_.readdirSync(dir);
482
+ for (const entry of entries){
483
+ const skillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
484
+ try {
485
+ if (external_node_fs_.statSync(skillDir).isDirectory() && hasValidSkillMd(skillDir)) {
486
+ addSkill(skillDir);
487
+ visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(skillDir));
488
+ }
489
+ } catch {
490
+ // Skip entries that can't be stat'd (race condition, permission, etc.)
491
+ }
492
+ }
493
+ } catch {
494
+ // Skip if unreadable
495
+ }
496
+ }
497
+ }
498
+ const recursiveDirs = findSkillDirsRecursive(resolvedBase, 0, MAX_DISCOVER_DEPTH, visitedDirs);
499
+ for (const skillDir of recursiveDirs)addSkill(skillDir);
500
+ return results;
501
+ }
502
+ /**
503
+ * Filter skills by name (case-insensitive exact match).
504
+ *
505
+ * Note: an empty `names` array returns an empty result (not all skills).
506
+ * Callers should check `names.length` before calling if "no filter = all" is desired.
507
+ *
508
+ * @param skills - List of discovered skills
509
+ * @param names - Skill names to match (e.g. from --skill pdf commit)
510
+ * @returns Skills whose name matches any of the given names
511
+ */ function filterSkillsByName(skills, names) {
512
+ const normalized = names.map((n)=>n.toLowerCase());
513
+ return skills.filter((skill)=>{
514
+ // Match against SKILL.md name field
515
+ if (normalized.includes(skill.name.toLowerCase())) return true;
516
+ // Also match against the directory name (basename of dirPath)
517
+ // Users naturally refer to skills by their directory name
518
+ const dirName = __WEBPACK_EXTERNAL_MODULE_node_path__.basename(skill.dirPath).toLowerCase();
519
+ return normalized.includes(dirName);
520
+ });
521
+ }
522
+ const claude_3p_installer_CLAUDE_COWORK_3P_AGENT = 'claude-cowork-3p';
523
+ const CLAUDE_3P_SKILLS_ROOT_ENV = 'CLAUDE_3P_SKILLS_ROOT';
524
+ const CLAUDE_3P_SKILLS_PLUGIN_BASE_ENV = 'CLAUDE_3P_SKILLS_PLUGIN_BASE';
525
+ const DEFAULT_EXCLUDE_FILES = [
526
+ 'README.md',
527
+ 'metadata.json',
528
+ '.reskill-commit'
529
+ ];
530
+ const EXCLUDE_PREFIX = '_';
531
+ const MAX_MANIFEST_BACKUPS = 10;
532
+ function exists(targetPath) {
533
+ return external_node_fs_.existsSync(targetPath);
534
+ }
535
+ function ensureDir(dirPath) {
536
+ if (!exists(dirPath)) external_node_fs_.mkdirSync(dirPath, {
537
+ recursive: true
538
+ });
539
+ }
540
+ function remove(targetPath) {
541
+ if (exists(targetPath)) external_node_fs_.rmSync(targetPath, {
542
+ recursive: true,
543
+ force: true
544
+ });
545
+ }
546
+ function isSafeSkillId(name) {
547
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) && '.' !== name && '..' !== name;
548
+ }
549
+ function isPathSafe(basePath, targetPath) {
550
+ const normalizedBase = __WEBPACK_EXTERNAL_MODULE_node_path__.normalize(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(basePath));
551
+ const normalizedTarget = __WEBPACK_EXTERNAL_MODULE_node_path__.normalize(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(targetPath));
552
+ return normalizedTarget.startsWith(normalizedBase + __WEBPACK_EXTERNAL_MODULE_node_path__.sep);
553
+ }
554
+ function getSafeSkillPath(root, skillName) {
555
+ if (!isSafeSkillId(skillName)) throw new Error(`Skill name is not safe for Claude Cowork 3P: ${skillName}`);
556
+ const skillsDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills');
557
+ const skillPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillsDir, skillName);
558
+ if (!isPathSafe(skillsDir, skillPath)) throw new Error(`Skill path escapes Claude Cowork 3P skills directory: ${skillName}`);
559
+ return skillPath;
560
+ }
561
+ function isSkillsRoot(root) {
562
+ return exists(__WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'manifest.json')) && exists(__WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills'));
563
+ }
564
+ function copyDirectory(src, dest) {
565
+ ensureDir(dest);
566
+ for (const entry of external_node_fs_.readdirSync(src, {
567
+ withFileTypes: true
568
+ })){
569
+ if (DEFAULT_EXCLUDE_FILES.includes(entry.name) || entry.name.startsWith(EXCLUDE_PREFIX)) continue;
570
+ const srcPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(src, entry.name);
571
+ const destPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dest, entry.name);
572
+ if (entry.isDirectory()) copyDirectory(srcPath, destPath);
573
+ else if (entry.isFile()) external_node_fs_.copyFileSync(srcPath, destPath);
574
+ }
575
+ }
576
+ function getClaude3pSkillsPluginBase(options = {}) {
577
+ const env = options.env ?? process.env;
578
+ const explicitBase = env[CLAUDE_3P_SKILLS_PLUGIN_BASE_ENV];
579
+ if (explicitBase) return explicitBase;
580
+ const homeDir = options.homeDir ?? (0, __WEBPACK_EXTERNAL_MODULE_node_os__.homedir)();
581
+ const currentPlatform = options.platform ?? (0, __WEBPACK_EXTERNAL_MODULE_node_os__.platform)();
582
+ if ('darwin' === currentPlatform) return __WEBPACK_EXTERNAL_MODULE_node_path__.join(homeDir, 'Library', 'Application Support', 'Claude-3p', 'local-agent-mode-sessions', 'skills-plugin');
583
+ if ('win32' === currentPlatform) return __WEBPACK_EXTERNAL_MODULE_node_path__.join(env.APPDATA ?? __WEBPACK_EXTERNAL_MODULE_node_path__.join(homeDir, 'AppData', 'Roaming'), 'Claude-3p', 'local-agent-mode-sessions', 'skills-plugin');
584
+ return __WEBPACK_EXTERNAL_MODULE_node_path__.join(env.XDG_CONFIG_HOME ?? __WEBPACK_EXTERNAL_MODULE_node_path__.join(homeDir, '.config'), 'Claude-3p', 'local-agent-mode-sessions', 'skills-plugin');
585
+ }
586
+ function findClaude3pSkillsRoots(options = {}) {
587
+ const env = options.env ?? process.env;
588
+ const explicitRoot = env[CLAUDE_3P_SKILLS_ROOT_ENV];
589
+ if (explicitRoot) return isSkillsRoot(explicitRoot) ? [
590
+ explicitRoot
591
+ ] : [];
592
+ const base = getClaude3pSkillsPluginBase(options);
593
+ if (!exists(base)) return [];
594
+ const roots = [];
595
+ for (const organization of external_node_fs_.readdirSync(base, {
596
+ withFileTypes: true
597
+ })){
598
+ if (!organization.isDirectory()) continue;
599
+ const organizationPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(base, organization.name);
600
+ for (const account of external_node_fs_.readdirSync(organizationPath, {
601
+ withFileTypes: true
602
+ })){
603
+ if (!account.isDirectory()) continue;
604
+ const candidate = __WEBPACK_EXTERNAL_MODULE_node_path__.join(organizationPath, account.name);
605
+ if (isSkillsRoot(candidate)) roots.push(candidate);
606
+ }
607
+ }
608
+ return roots;
609
+ }
610
+ function claude_3p_installer_resolveClaude3pSkillsRoot(options = {}) {
611
+ const env = options.env ?? process.env;
612
+ const explicitRoot = env[CLAUDE_3P_SKILLS_ROOT_ENV];
613
+ if (explicitRoot) {
614
+ if (!isSkillsRoot(explicitRoot)) throw new Error(`${CLAUDE_3P_SKILLS_ROOT_ENV} must contain manifest.json and skills/: ${explicitRoot}`);
615
+ return explicitRoot;
616
+ }
617
+ const roots = findClaude3pSkillsRoots(options);
618
+ if (1 === roots.length) return roots[0];
619
+ const base = getClaude3pSkillsPluginBase(options);
620
+ if (0 === roots.length) throw new Error(`Claude Cowork 3P skills root not found under ${base}. Set ${CLAUDE_3P_SKILLS_ROOT_ENV} to the account directory containing manifest.json and skills/.`);
621
+ throw new Error(`Multiple Claude Cowork 3P skills roots found. Set ${CLAUDE_3P_SKILLS_ROOT_ENV} to one of:\n${roots.map((root)=>` ${root}`).join('\n')}`);
622
+ }
623
+ function getClaude3pSkillPath(skillName) {
624
+ const root = claude_3p_installer_resolveClaude3pSkillsRoot();
625
+ return getSafeSkillPath(root, skillName);
626
+ }
627
+ function readManifest(manifestPath) {
628
+ const content = external_node_fs_.readFileSync(manifestPath, 'utf-8');
629
+ return JSON.parse(content);
630
+ }
631
+ function writeManifest(manifestPath, manifest) {
632
+ external_node_fs_.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
633
+ }
634
+ function createManifestBackup(manifestPath) {
635
+ const backupPath = `${manifestPath}.bak.${Date.now()}`;
636
+ external_node_fs_.copyFileSync(manifestPath, backupPath);
637
+ return backupPath;
638
+ }
639
+ function pruneManifestBackups(manifestPath) {
640
+ const manifestDir = __WEBPACK_EXTERNAL_MODULE_node_path__.dirname(manifestPath);
641
+ const backupPrefix = `${__WEBPACK_EXTERNAL_MODULE_node_path__.basename(manifestPath)}.bak.`;
642
+ const backups = external_node_fs_.readdirSync(manifestDir, {
643
+ withFileTypes: true
644
+ }).filter((entry)=>entry.isFile() && entry.name.startsWith(backupPrefix)).map((entry)=>({
645
+ name: entry.name,
646
+ path: __WEBPACK_EXTERNAL_MODULE_node_path__.join(manifestDir, entry.name),
647
+ mtimeMs: external_node_fs_.statSync(__WEBPACK_EXTERNAL_MODULE_node_path__.join(manifestDir, entry.name)).mtimeMs
648
+ })).sort((a, b)=>b.mtimeMs - a.mtimeMs);
649
+ for (const backup of backups.slice(MAX_MANIFEST_BACKUPS))remove(backup.path);
650
+ }
651
+ function updateManifest(manifestPath, skillId, name, description) {
652
+ const manifest = readManifest(manifestPath);
653
+ if (!Array.isArray(manifest.skills)) manifest.skills = [];
654
+ const entry = {
655
+ skillId,
656
+ name,
657
+ description,
658
+ creatorType: 'user',
659
+ syncManaged: false,
660
+ updatedAt: new Date().toISOString(),
661
+ enabled: true
662
+ };
663
+ const index = manifest.skills.findIndex((skill)=>skill.skillId === skillId || skill.name === name);
664
+ if (index >= 0) manifest.skills[index] = {
665
+ ...manifest.skills[index],
666
+ ...entry
667
+ };
668
+ else manifest.skills.push(entry);
669
+ manifest.lastUpdated = Date.now();
670
+ writeManifest(manifestPath, manifest);
671
+ }
672
+ function removeFromManifest(manifestPath, skillId) {
673
+ const manifest = readManifest(manifestPath);
674
+ if (!Array.isArray(manifest.skills)) return;
675
+ const before = manifest.skills.length;
676
+ manifest.skills = manifest.skills.filter((skill)=>skill.skillId !== skillId && skill.name !== skillId);
677
+ if (manifest.skills.length !== before) {
678
+ manifest.lastUpdated = Date.now();
679
+ writeManifest(manifestPath, manifest);
680
+ }
681
+ }
682
+ function installClaude3pSkill(sourcePath, fallbackName, _options = {}) {
683
+ const installMode = 'copy';
684
+ let manifestPath;
685
+ let manifestBackupPath;
686
+ let targetPath;
687
+ let tmpTarget;
688
+ let rollbackTarget;
689
+ try {
690
+ const metadata = parseSkillFromDir(sourcePath);
691
+ const skillId = metadata?.name ?? fallbackName;
692
+ const description = metadata?.description ?? '';
693
+ if (!isSafeSkillId(skillId)) return {
694
+ success: false,
695
+ path: '',
696
+ mode: installMode,
697
+ error: `SKILL.md name is not safe for Claude Cowork 3P: ${skillId}`
698
+ };
699
+ const root = claude_3p_installer_resolveClaude3pSkillsRoot();
700
+ manifestPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'manifest.json');
701
+ const skillsDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills');
702
+ targetPath = getSafeSkillPath(root, skillId);
703
+ tmpTarget = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillsDir, `.${skillId}.installing.${process.pid}`);
704
+ rollbackTarget = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillsDir, `.${skillId}.rollback.${process.pid}`);
705
+ if (!isPathSafe(skillsDir, tmpTarget)) throw new Error(`Temporary skill path escapes Claude Cowork 3P skills directory: ${skillId}`);
706
+ if (!isPathSafe(skillsDir, rollbackTarget)) throw new Error(`Rollback skill path escapes Claude Cowork 3P skills directory: ${skillId}`);
707
+ manifestBackupPath = createManifestBackup(manifestPath);
708
+ remove(tmpTarget);
709
+ remove(rollbackTarget);
710
+ copyDirectory(sourcePath, tmpTarget);
711
+ if (exists(targetPath)) external_node_fs_.renameSync(targetPath, rollbackTarget);
712
+ external_node_fs_.renameSync(tmpTarget, targetPath);
713
+ try {
714
+ updateManifest(manifestPath, skillId, skillId, description);
715
+ } catch (error) {
716
+ remove(targetPath);
717
+ if (rollbackTarget && exists(rollbackTarget)) external_node_fs_.renameSync(rollbackTarget, targetPath);
718
+ if (manifestBackupPath && manifestPath && exists(manifestBackupPath)) external_node_fs_.copyFileSync(manifestBackupPath, manifestPath);
719
+ throw error;
720
+ }
721
+ if (rollbackTarget) remove(rollbackTarget);
722
+ pruneManifestBackups(manifestPath);
723
+ return {
724
+ success: true,
725
+ path: targetPath,
726
+ mode: installMode
727
+ };
728
+ } catch (error) {
729
+ if (tmpTarget) remove(tmpTarget);
730
+ if (targetPath && rollbackTarget && exists(rollbackTarget) && !exists(targetPath)) try {
731
+ external_node_fs_.renameSync(rollbackTarget, targetPath);
732
+ } catch {
733
+ // Ignore rollback errors while reporting the original installation failure.
734
+ }
735
+ if (manifestPath) try {
736
+ pruneManifestBackups(manifestPath);
737
+ } catch {
738
+ // Ignore cleanup errors while reporting the original installation failure.
739
+ }
740
+ return {
741
+ success: false,
742
+ path: '',
743
+ mode: installMode,
744
+ error: error instanceof Error ? error.message : 'Unknown error'
745
+ };
746
+ }
747
+ }
748
+ function uninstallClaude3pSkill(skillName) {
749
+ const root = claude_3p_installer_resolveClaude3pSkillsRoot();
750
+ const targetPath = getSafeSkillPath(root, skillName);
751
+ const manifestPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'manifest.json');
752
+ const existed = exists(targetPath);
753
+ if (existed) remove(targetPath);
754
+ removeFromManifest(manifestPath, skillName);
755
+ return existed;
756
+ }
757
+ function listClaude3pSkills() {
758
+ const root = claude_3p_installer_resolveClaude3pSkillsRoot();
759
+ const skillsDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills');
760
+ if (!exists(skillsDir)) return [];
761
+ return external_node_fs_.readdirSync(skillsDir, {
762
+ withFileTypes: true
763
+ }).filter((entry)=>entry.isDirectory() && isSafeSkillId(entry.name)).map((entry)=>entry.name);
764
+ }
159
765
  /**
160
766
  * Agent Registry - Multi-Agent configuration definitions
161
767
  *
162
- * Supports global and project-level installation for 17 coding agents
768
+ * Supports global and project-level installation for 18 coding agents
163
769
  * Reference: https://github.com/vercel-labs/add-skill
164
770
  */ const agent_registry_home = (0, __WEBPACK_EXTERNAL_MODULE_node_os__.homedir)();
165
771
  /**
@@ -186,6 +792,20 @@ var external_node_fs_ = __webpack_require__("node:fs");
186
792
  globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.claude/skills'),
187
793
  detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.claude'))
188
794
  },
795
+ [claude_3p_installer_CLAUDE_COWORK_3P_AGENT]: {
796
+ name: claude_3p_installer_CLAUDE_COWORK_3P_AGENT,
797
+ displayName: 'Claude Cowork 3P',
798
+ skillsDir: '.claude-3p/skills',
799
+ globalSkillsDir: getClaude3pSkillsPluginBase(),
800
+ detectInstalled: async ()=>{
801
+ try {
802
+ claude_3p_installer_resolveClaude3pSkillsRoot();
803
+ return true;
804
+ } catch {
805
+ return false;
806
+ }
807
+ }
808
+ },
189
809
  clawdbot: {
190
810
  name: 'clawdbot',
191
811
  displayName: 'Clawdbot',
@@ -283,1174 +903,811 @@ var external_node_fs_ = __webpack_require__("node:fs");
283
903
  skillsDir: '.neovate/skills',
284
904
  globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.neovate/skills'),
285
905
  detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.neovate'))
286
- }
287
- };
288
- /**
289
- * Detect installed Agents
290
- */ async function detectInstalledAgents() {
291
- const installed = [];
292
- for (const [type, config] of Object.entries(agents))if (await config.detectInstalled()) installed.push(type);
293
- return installed;
294
- }
295
- /**
296
- * Get Agent configuration
297
- */ function getAgentConfig(type) {
298
- return agents[type];
299
- }
300
- /**
301
- * Validate if Agent type is valid
302
- */ function isValidAgentType(type) {
303
- return type in agents;
304
- }
305
- /**
306
- * File system utilities
307
- */ /**
308
- * Check if a file or directory exists
309
- */ function exists(filePath) {
310
- return external_node_fs_.existsSync(filePath);
311
- }
312
- /**
313
- * Read JSON file
314
- */ function readJson(filePath) {
315
- const content = external_node_fs_.readFileSync(filePath, 'utf-8');
316
- return JSON.parse(content);
317
- }
318
- /**
319
- * Write JSON file
320
- */ function writeJson(filePath, data, indent = 2) {
321
- const dir = __WEBPACK_EXTERNAL_MODULE_node_path__.dirname(filePath);
322
- if (!exists(dir)) external_node_fs_.mkdirSync(dir, {
323
- recursive: true
324
- });
325
- external_node_fs_.writeFileSync(filePath, `${JSON.stringify(data, null, indent)}\n`, 'utf-8');
326
- }
327
- /**
328
- * Create directory recursively
329
- */ function ensureDir(dirPath) {
330
- if (!exists(dirPath)) external_node_fs_.mkdirSync(dirPath, {
331
- recursive: true
332
- });
333
- }
334
- /**
335
- * Remove file or directory
336
- */ function remove(targetPath) {
337
- if (exists(targetPath)) external_node_fs_.rmSync(targetPath, {
338
- recursive: true,
339
- force: true
340
- });
341
- }
342
- /**
343
- * Copy directory recursively
344
- *
345
- * @param src - Source directory
346
- * @param dest - Destination directory
347
- * @param options.exclude - Array of filenames to exclude
348
- * @param options.excludePrefix - Prefix for files to exclude (e.g., '_' to exclude _private.md)
349
- */ function copyDir(src, dest, options) {
350
- const exclude = options?.exclude || [];
351
- const excludePrefix = options?.excludePrefix || '_';
352
- ensureDir(dest);
353
- const entries = external_node_fs_.readdirSync(src, {
354
- withFileTypes: true
355
- });
356
- for (const entry of entries){
357
- // Skip files in exclude list or starting with excludePrefix
358
- if (exclude.includes(entry.name) || entry.name.startsWith(excludePrefix)) continue;
359
- const srcPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(src, entry.name);
360
- const destPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dest, entry.name);
361
- if (entry.isDirectory()) copyDir(srcPath, destPath, options);
362
- else external_node_fs_.copyFileSync(srcPath, destPath);
363
- }
364
- }
365
- /**
366
- * List directory contents
367
- */ function listDir(dirPath) {
368
- if (!exists(dirPath)) return [];
369
- return external_node_fs_.readdirSync(dirPath);
370
- }
371
- /**
372
- * Check if path is a directory
373
- */ function isDirectory(targetPath) {
374
- if (!exists(targetPath)) return false;
375
- return external_node_fs_.statSync(targetPath).isDirectory();
376
- }
377
- /**
378
- * Check if path is a symbolic link
379
- */ function isSymlink(targetPath) {
380
- if (!exists(targetPath)) return false;
381
- return external_node_fs_.lstatSync(targetPath).isSymbolicLink();
382
- }
383
- /**
384
- * Get real path of symbolic link
385
- */ function getRealPath(linkPath) {
386
- return external_node_fs_.realpathSync(linkPath);
387
- }
388
- /**
389
- * Get skills.json path for current project
390
- */ function getSkillsJsonPath(projectRoot) {
391
- const root = projectRoot || process.cwd();
392
- return __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills.json');
393
- }
394
- /**
395
- * Get skills.lock path for current project
396
- */ function getSkillsLockPath(projectRoot) {
397
- const root = projectRoot || process.cwd();
398
- return __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills.lock');
399
- }
400
- /**
401
- * Get global cache directory
402
- */ function getCacheDir() {
403
- const home = process.env.HOME || process.env.USERPROFILE || '';
404
- return process.env.RESKILL_CACHE_DIR || __WEBPACK_EXTERNAL_MODULE_node_path__.join(home, '.reskill-cache');
405
- }
406
- /**
407
- * Get home directory
408
- */ function getHomeDir() {
409
- return process.env.HOME || process.env.USERPROFILE || '';
410
- }
411
- /**
412
- * Shorten path display (replace home directory with ~)
413
- */ function shortenPath(fullPath, cwd) {
414
- const home = getHomeDir();
415
- const currentDir = cwd || process.cwd();
416
- if (fullPath.startsWith(home)) return fullPath.replace(home, '~');
417
- if (fullPath.startsWith(currentDir)) return `.${fullPath.slice(currentDir.length)}`;
418
- return fullPath;
419
- }
420
- const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEBPACK_EXTERNAL_MODULE_node_child_process__.exec);
421
- /**
422
- * Git utilities
423
- */ /**
424
- * SSH command with auto-accept for new host keys
425
- * Uses StrictHostKeyChecking=accept-new which:
426
- * - Automatically accepts keys for hosts not in known_hosts
427
- * - Still rejects connections if a known host's key has changed (security)
428
- */ const GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes';
429
- /**
430
- * Get environment variables for git commands that access remote repositories
431
- * Configures SSH to auto-accept new host keys and disables interactive prompts
432
- */ function getGitEnv() {
433
- return {
434
- ...process.env,
435
- GIT_SSH_COMMAND,
436
- // Disable interactive prompts for HTTPS as well
437
- GIT_TERMINAL_PROMPT: '0'
438
- };
439
- }
440
- /**
441
- * Custom error class for Git clone failures
442
- * Provides helpful tips for private repository authentication
443
- */ class GitCloneError extends Error {
444
- repoUrl;
445
- originalError;
446
- isAuthError;
447
- urlType;
448
- constructor(repoUrl, originalError){
449
- const isAuthError = GitCloneError.isAuthenticationError(originalError.message);
450
- const urlType = GitCloneError.detectUrlType(repoUrl);
451
- let message = `Failed to clone repository: ${repoUrl}`;
452
- if (isAuthError) {
453
- message += '\n\nTip: For private repos, ensure git credentials are configured:';
454
- if ('ssh' === urlType) {
455
- message += '\n - Check ~/.ssh/id_rsa or ~/.ssh/id_ed25519';
456
- message += '\n - Ensure SSH key is added to your Git hosting service';
457
- } else {
458
- // HTTPS or unknown
459
- message += "\n - Run 'git config --global credential.helper store'";
460
- message += '\n - Or use a personal access token in the URL';
461
- }
462
- }
463
- super(message);
464
- this.name = 'GitCloneError';
465
- this.repoUrl = repoUrl;
466
- this.originalError = originalError;
467
- this.isAuthError = isAuthError;
468
- this.urlType = urlType;
469
- }
470
- /**
471
- * Detect URL type from repository URL
472
- */ static detectUrlType(url) {
473
- if (url.startsWith('git@') || url.startsWith('ssh://')) return 'ssh';
474
- if (url.startsWith('http://') || url.startsWith('https://')) return 'https';
475
- return 'unknown';
476
- }
477
- /**
478
- * Check if an error message indicates an authentication problem
479
- */ static isAuthenticationError(message) {
480
- const authPatterns = [
481
- /permission denied/i,
482
- /could not read from remote/i,
483
- /authentication failed/i,
484
- /fatal: repository.*not found/i,
485
- /host key verification failed/i,
486
- /access denied/i,
487
- /unauthorized/i,
488
- /403/,
489
- /401/
490
- ];
491
- return authPatterns.some((pattern)=>pattern.test(message));
492
- }
493
- }
494
- /**
495
- * Execute git command asynchronously
496
- */ async function git(args, cwd) {
497
- const { stdout } = await git_execAsync(`git ${args.join(' ')}`, {
498
- cwd,
499
- encoding: 'utf-8',
500
- env: getGitEnv()
501
- });
502
- return stdout.trim();
906
+ }
907
+ };
908
+ /**
909
+ * Detect installed Agents
910
+ */ async function detectInstalledAgents() {
911
+ const installed = [];
912
+ for (const [type, config] of Object.entries(agents))if (await config.detectInstalled()) installed.push(type);
913
+ return installed;
503
914
  }
504
915
  /**
505
- * Get remote tags for a repository
506
- */ async function getRemoteTags(repoUrl) {
507
- try {
508
- const output = await git([
509
- 'ls-remote',
510
- '--tags',
511
- '--refs',
512
- repoUrl
513
- ]);
514
- if (!output) return [];
515
- const tags = [];
516
- const lines = output.split('\n');
517
- for (const line of lines){
518
- const [commit, ref] = line.split('\t');
519
- if (commit && ref) {
520
- // Extract tag name from refs/tags/v1.0.0
521
- const tagName = ref.replace('refs/tags/', '');
522
- tags.push({
523
- name: tagName,
524
- commit
525
- });
526
- }
527
- }
528
- return tags;
529
- } catch {
530
- return [];
531
- }
916
+ * Get Agent configuration
917
+ */ function getAgentConfig(type) {
918
+ return agents[type];
532
919
  }
533
920
  /**
534
- * Get latest tag from repository
535
- */ async function getLatestTag(repoUrl) {
536
- const tags = await getRemoteTags(repoUrl);
537
- if (0 === tags.length) return null;
538
- // Sort by semver (simple version sort)
539
- const sortedTags = tags.sort((a, b)=>{
540
- const aVer = a.name.replace(/^v/, '');
541
- const bVer = b.name.replace(/^v/, '');
542
- return compareVersions(bVer, aVer);
543
- });
544
- return sortedTags[0];
921
+ * Validate if Agent type is valid
922
+ */ function isValidAgentType(type) {
923
+ return type in agents;
545
924
  }
546
925
  /**
547
- * Clone a repository with shallow clone
548
- *
549
- * @throws {GitCloneError} When clone fails, with helpful tips for authentication issues
550
- */ async function clone(repoUrl, destPath, options) {
551
- const args = [
552
- 'clone'
553
- ];
554
- if (options?.depth) args.push('--depth', options.depth.toString());
555
- if (options?.branch) args.push('--branch', options.branch);
556
- args.push(repoUrl, destPath);
557
- try {
558
- await git(args);
559
- } catch (error) {
560
- throw new GitCloneError(repoUrl, error);
561
- }
926
+ * File system utilities
927
+ */ /**
928
+ * Check if a file or directory exists
929
+ */ function fs_exists(filePath) {
930
+ return external_node_fs_.existsSync(filePath);
562
931
  }
563
932
  /**
564
- * Get current commit hash
565
- */ async function getCurrentCommit(cwd) {
566
- return git([
567
- 'rev-parse',
568
- 'HEAD'
569
- ], cwd);
933
+ * Read JSON file
934
+ */ function readJson(filePath) {
935
+ const content = external_node_fs_.readFileSync(filePath, 'utf-8');
936
+ return JSON.parse(content);
570
937
  }
571
938
  /**
572
- * Get default branch name
573
- */ async function getDefaultBranch(repoUrl) {
574
- try {
575
- const output = await git([
576
- 'ls-remote',
577
- '--symref',
578
- repoUrl,
579
- 'HEAD'
580
- ]);
581
- const match = output.match(/ref: refs\/heads\/(\S+)/);
582
- return match ? match[1] : 'main';
583
- } catch {
584
- return 'main';
585
- }
939
+ * Write JSON file
940
+ */ function writeJson(filePath, data, indent = 2) {
941
+ const dir = __WEBPACK_EXTERNAL_MODULE_node_path__.dirname(filePath);
942
+ if (!fs_exists(dir)) external_node_fs_.mkdirSync(dir, {
943
+ recursive: true
944
+ });
945
+ external_node_fs_.writeFileSync(filePath, `${JSON.stringify(data, null, indent)}\n`, 'utf-8');
586
946
  }
587
947
  /**
588
- * Simple version comparison (for sorting)
589
- */ function compareVersions(a, b) {
590
- const aParts = a.split('.').map((p)=>parseInt(p, 10) || 0);
591
- const bParts = b.split('.').map((p)=>parseInt(p, 10) || 0);
592
- const maxLength = Math.max(aParts.length, bParts.length);
593
- for(let i = 0; i < maxLength; i++){
594
- const aPart = aParts[i] || 0;
595
- const bPart = bParts[i] || 0;
596
- if (aPart > bPart) return 1;
597
- if (aPart < bPart) return -1;
598
- }
599
- return 0;
948
+ * Create directory recursively
949
+ */ function fs_ensureDir(dirPath) {
950
+ if (!fs_exists(dirPath)) external_node_fs_.mkdirSync(dirPath, {
951
+ recursive: true
952
+ });
600
953
  }
601
954
  /**
602
- * Check if a source string is a complete Git URL (SSH, HTTPS, git://, or file://)
603
- *
604
- * Supported formats:
605
- * - SSH: git@github.com:user/repo.git
606
- * - HTTPS: https://github.com/user/repo.git
607
- * - Git protocol: git://github.com/user/repo.git
608
- * - File protocol: file:///path/to/repo (for local testing)
609
- * - URLs ending with .git
610
- */ function isGitUrl(source) {
611
- return source.startsWith('git@') || source.startsWith('git://') || source.startsWith('http://') || source.startsWith('https://') || source.startsWith('file://') || source.endsWith('.git');
955
+ * Remove file or directory
956
+ */ function fs_remove(targetPath) {
957
+ if (fs_exists(targetPath)) external_node_fs_.rmSync(targetPath, {
958
+ recursive: true,
959
+ force: true
960
+ });
612
961
  }
613
962
  /**
614
- * Parse a Git URL and extract host, owner, and repo information
615
- *
616
- * Supports:
617
- * - SSH: git@github.com:user/repo.git
618
- * - HTTPS: https://github.com/user/repo.git
619
- * - Git protocol: git://github.com/user/repo.git
620
- *
621
- * Note: GitHub/GitLab web URLs (with /tree/, /blob/, etc.) are handled
622
- * at a higher level in GitResolver.parseGitUrlRef() before calling this function.
963
+ * Copy directory recursively
623
964
  *
624
- * @param url The Git URL to parse
625
- * @returns Parsed URL information or null if parsing fails
626
- */ function parseGitUrl(url) {
627
- // Remove trailing .git if present
628
- const cleanUrl = url.replace(/\.git$/, '');
629
- // SSH format: git@github.com:user/repo
630
- const sshMatch = cleanUrl.match(/^git@([^:]+):(.+)$/);
631
- if (sshMatch) {
632
- const [, host, path] = sshMatch;
633
- const parts = path.split('/');
634
- if (parts.length >= 2) {
635
- // Handle nested paths like org/sub/repo
636
- const owner = parts.slice(0, -1).join('/');
637
- const repo = parts[parts.length - 1];
638
- return {
639
- host,
640
- owner,
641
- repo,
642
- url,
643
- type: 'ssh'
644
- };
645
- }
646
- }
647
- // HTTPS/Git protocol format: https://github.com/user/repo or git://github.com/user/repo
648
- const httpMatch = cleanUrl.match(/^(https?|git):\/\/([^/]+)\/(.+)$/);
649
- if (httpMatch) {
650
- const [, protocol, host, path] = httpMatch;
651
- const parts = path.split('/');
652
- if (parts.length >= 2) {
653
- const owner = parts.slice(0, -1).join('/');
654
- const repo = parts[parts.length - 1];
655
- return {
656
- host,
657
- owner,
658
- repo,
659
- url,
660
- type: 'git' === protocol ? 'git' : 'https'
661
- };
662
- }
663
- }
664
- // File protocol format: file:///path/to/repo
665
- // Used for local testing and development
666
- const fileMatch = cleanUrl.match(/^file:\/\/(.+)$/);
667
- if (fileMatch) {
668
- const [, filePath] = fileMatch;
669
- const parts = filePath.split('/').filter(Boolean);
670
- if (parts.length >= 1) {
671
- // Use 'local' as host, path components as owner/repo
672
- const repo = parts[parts.length - 1];
673
- const owner = parts.length > 1 ? parts[parts.length - 2] : 'local';
674
- return {
675
- host: 'local',
676
- owner,
677
- repo,
678
- url,
679
- type: 'file'
680
- };
681
- }
965
+ * @param src - Source directory
966
+ * @param dest - Destination directory
967
+ * @param options.exclude - Array of filenames to exclude
968
+ * @param options.excludePrefix - Prefix for files to exclude (e.g., '_' to exclude _private.md)
969
+ */ function copyDir(src, dest, options) {
970
+ const exclude = options?.exclude || [];
971
+ const excludePrefix = options?.excludePrefix || '_';
972
+ fs_ensureDir(dest);
973
+ const entries = external_node_fs_.readdirSync(src, {
974
+ withFileTypes: true
975
+ });
976
+ for (const entry of entries){
977
+ // Skip files in exclude list or starting with excludePrefix
978
+ if (exclude.includes(entry.name) || entry.name.startsWith(excludePrefix)) continue;
979
+ const srcPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(src, entry.name);
980
+ const destPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dest, entry.name);
981
+ if (entry.isDirectory()) copyDir(srcPath, destPath, options);
982
+ else external_node_fs_.copyFileSync(srcPath, destPath);
682
983
  }
683
- return null;
684
984
  }
685
985
  /**
686
- * Registry-Scope Mapping Utilities
687
- *
688
- * Maps registry URLs to their corresponding scopes.
689
- * Currently hardcoded; TODO: fetch from /api/registry/info in the future.
690
- */ /**
691
- * Public Registry URL
692
- * Used for installing skills without a scope
693
- */ const PUBLIC_REGISTRY = 'https://reskill.info/';
694
- /**
695
- * Hardcoded registry to scope mapping
696
- * TODO: Replace with dynamic fetching from /api/registry/info
697
- */ const REGISTRY_SCOPE_MAP = {
698
- // rush-app (private registry, new)
699
- 'https://rush-test.zhenguanyu.com': '@kanyun-test',
700
- 'https://rush.zhenguanyu.com': '@kanyun',
701
- // reskill-app (private registry, legacy)
702
- 'https://reskill-test.zhenguanyu.com': '@kanyun-test',
703
- // Local development
704
- 'http://localhost:3000': '@kanyun-test'
705
- };
986
+ * List directory contents
987
+ */ function listDir(dirPath) {
988
+ if (!fs_exists(dirPath)) return [];
989
+ return external_node_fs_.readdirSync(dirPath);
990
+ }
706
991
  /**
707
- * Get the scope for a given registry URL
708
- *
709
- * @param registry - Registry URL
710
- * @returns Scope string (e.g., "@kanyun") or null if not found
711
- *
712
- * @example
713
- * getScopeForRegistry('https://rush-test.zhenguanyu.com') // '@kanyun'
714
- * getScopeForRegistry('https://unknown.com') // null
715
- */ function getScopeForRegistry(registry) {
716
- if (!registry) return null;
717
- // Try exact match first
718
- if (REGISTRY_SCOPE_MAP[registry]) return REGISTRY_SCOPE_MAP[registry];
719
- // Try with/without trailing slash
720
- const normalized = registry.endsWith('/') ? registry.slice(0, -1) : `${registry}/`;
721
- return REGISTRY_SCOPE_MAP[normalized] || null;
992
+ * Check if path is a directory
993
+ */ function isDirectory(targetPath) {
994
+ if (!fs_exists(targetPath)) return false;
995
+ return external_node_fs_.statSync(targetPath).isDirectory();
722
996
  }
723
997
  /**
724
- * Get the registry URL for a given scope (reverse lookup)
725
- *
726
- * @param scope - Scope string (with or without @ prefix), e.g., "@kanyun" or "kanyun"
727
- * @param customRegistries - Optional custom scope-to-registry mapping (from skills.json)
728
- * @returns Registry URL (with trailing slash) or null if not found
729
- *
730
- * @example
731
- * getRegistryForScope('@kanyun') // 'https://rush-test.zhenguanyu.com/'
732
- * getRegistryForScope('kanyun') // 'https://rush-test.zhenguanyu.com/'
733
- * getRegistryForScope('@unknown') // null
734
- * getRegistryForScope('@mycompany', { '@mycompany': 'https://my.registry.com/' }) // 'https://my.registry.com/'
735
- */ function getRegistryForScope(scope, customRegistries) {
736
- if (!scope) return null;
737
- // Normalize scope: ensure @ prefix
738
- const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
739
- // 1. First check custom scopeRegistries (from skills.json)
740
- if (customRegistries?.[normalizedScope]) {
741
- const url = customRegistries[normalizedScope];
742
- // Normalize trailing slash
743
- return url.endsWith('/') ? url : `${url}/`;
744
- }
745
- // 2. Fall back to hardcoded defaults
746
- for (const [registry, registryScope] of Object.entries(REGISTRY_SCOPE_MAP))if (registryScope === normalizedScope) // Return URL with trailing slash (normalized format)
747
- return registry.endsWith('/') ? registry : `${registry}/`;
748
- return null;
998
+ * Check if path is a symbolic link
999
+ */ function isSymlink(targetPath) {
1000
+ if (!fs_exists(targetPath)) return false;
1001
+ return external_node_fs_.lstatSync(targetPath).isSymbolicLink();
749
1002
  }
750
1003
  /**
751
- * Get the registry URL for a given scope
752
- *
753
- * - With scope → lookup private Registry (throws if not found)
754
- * - Without scope (null/undefined/'') → returns public Registry
755
- *
756
- * @param scope - Scope string (with or without @ prefix), null, undefined, or empty string
757
- * @param customRegistries - Optional custom scope-to-registry mapping (from skills.json)
758
- * @returns Registry URL (with trailing slash)
759
- * @throws Error if scope is provided but not found in the registry map
760
- *
761
- * @example
762
- * getRegistryUrl('@kanyun') // 'https://rush-test.zhenguanyu.com/'
763
- * getRegistryUrl('kanyun') // 'https://rush-test.zhenguanyu.com/'
764
- * getRegistryUrl(null) // 'https://reskill.info/'
765
- * getRegistryUrl('') // 'https://reskill.info/'
766
- * getRegistryUrl('@unknown') // throws Error
767
- * getRegistryUrl('@mycompany', { '@mycompany': 'https://my.registry.com/' }) // 'https://my.registry.com/'
768
- */ function getRegistryUrl(scope, customRegistries) {
769
- // No scope → return public Registry
770
- if (!scope) return PUBLIC_REGISTRY;
771
- // With scope → lookup private Registry
772
- const registry = getRegistryForScope(scope, customRegistries);
773
- if (!registry) {
774
- // Normalize scope for error message
775
- const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
776
- throw new Error(`Unknown scope ${normalizedScope}. No registry configured for this scope.`);
777
- }
778
- return registry;
1004
+ * Get real path of symbolic link
1005
+ */ function getRealPath(linkPath) {
1006
+ return external_node_fs_.realpathSync(linkPath);
779
1007
  }
780
1008
  /**
781
- * Parse a skill name into its components
782
- *
783
- * @param skillName - Full or short skill name
784
- * @returns Parsed skill name with scope and name
785
- *
786
- * @example
787
- * parseSkillName('@kanyun/planning-with-files')
788
- * // { scope: '@kanyun', name: 'planning-with-files', fullName: '@kanyun/planning-with-files' }
789
- *
790
- * parseSkillName('planning-with-files')
791
- * // { scope: null, name: 'planning-with-files', fullName: 'planning-with-files' }
792
- */ function parseSkillName(skillName) {
793
- // Match @scope/name pattern
794
- const match = skillName.match(/^(@[^/]+)\/(.+)$/);
795
- if (match) return {
796
- scope: match[1],
797
- name: match[2],
798
- fullName: skillName
799
- };
800
- return {
801
- scope: null,
802
- name: skillName,
803
- fullName: skillName
804
- };
1009
+ * Get skills.json path for current project
1010
+ */ function getSkillsJsonPath(projectRoot) {
1011
+ const root = projectRoot || process.cwd();
1012
+ return __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills.json');
805
1013
  }
806
1014
  /**
807
- * Build full skill name from scope and name
808
- *
809
- * @param scope - Scope (with or without @ prefix), or null
810
- * @param name - Short skill name
811
- * @returns Full skill name (e.g., "@kanyun/planning-with-files")
812
- *
813
- * @example
814
- * buildFullSkillName('@kanyun', 'planning-with-files') // '@kanyun/planning-with-files'
815
- * buildFullSkillName('kanyun', 'my-skill') // '@kanyun/my-skill'
816
- * buildFullSkillName(null, 'my-skill') // 'my-skill'
817
- */ function buildFullSkillName(scope, name) {
818
- if (!scope) return name;
819
- // Ensure scope starts with @
820
- const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
821
- return `${normalizedScope}/${name}`;
1015
+ * Get skills.lock path for current project
1016
+ */ function getSkillsLockPath(projectRoot) {
1017
+ const root = projectRoot || process.cwd();
1018
+ return __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills.lock');
822
1019
  }
823
1020
  /**
824
- * Get short name from a skill name (removes scope if present)
825
- *
826
- * @param skillName - Full or short skill name
827
- * @returns Short name without scope
828
- *
829
- * @example
830
- * getShortName('@kanyun/planning-with-files') // 'planning-with-files'
831
- * getShortName('planning-with-files') // 'planning-with-files'
832
- */ function getShortName(skillName) {
833
- return parseSkillName(skillName).name;
1021
+ * Get global cache directory
1022
+ */ function getCacheDir() {
1023
+ const home = process.env.HOME || process.env.USERPROFILE || '';
1024
+ return process.env.RESKILL_CACHE_DIR || __WEBPACK_EXTERNAL_MODULE_node_path__.join(home, '.reskill-cache');
834
1025
  }
835
1026
  /**
836
- * Parse a skill identifier into its components (with version support)
837
- *
838
- * Supports both private registry (with @scope) and public registry (without scope) formats.
839
- *
840
- * @param identifier - Skill identifier string
841
- * @returns Parsed skill identifier with scope, name, version, and fullName
842
- * @throws Error if identifier is invalid
843
- *
844
- * @example
845
- * // Private registry
846
- * parseSkillIdentifier('@kanyun/planning-with-files')
847
- * // { scope: '@kanyun', name: 'planning-with-files', version: undefined, fullName: '@kanyun/planning-with-files' }
848
- *
849
- * parseSkillIdentifier('@kanyun/skill@2.4.5')
850
- * // { scope: '@kanyun', name: 'skill', version: '2.4.5', fullName: '@kanyun/skill' }
851
- *
852
- * // Public registry
853
- * parseSkillIdentifier('planning-with-files')
854
- * // { scope: null, name: 'planning-with-files', version: undefined, fullName: 'planning-with-files' }
855
- *
856
- * parseSkillIdentifier('skill@latest')
857
- * // { scope: null, name: 'skill', version: 'latest', fullName: 'skill' }
858
- */ function parseSkillIdentifier(identifier) {
859
- const trimmed = identifier.trim();
860
- // Empty string or whitespace only
861
- if (!trimmed) throw new Error('Invalid skill identifier: empty string');
862
- // Starting with @@ is invalid
863
- if (trimmed.startsWith('@@')) throw new Error('Invalid skill identifier: invalid scope format');
864
- // Bare @ is invalid
865
- if ('@' === trimmed) throw new Error('Invalid skill identifier: missing scope and name');
866
- // Scoped format: @scope/name[@version]
867
- if (trimmed.startsWith('@')) {
868
- // Regex: @scope/name[@version]
869
- // scope: starts with @, followed by alphanumeric, hyphens, underscores
870
- // name: alphanumeric, hyphens, underscores
871
- // version: optional, @ followed by any non-empty string
872
- const scopedMatch = trimmed.match(/^(@[\w-]+)\/([\w-]+)(?:@(.+))?$/);
873
- if (!scopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
874
- const [, scope, name, version] = scopedMatch;
875
- return {
876
- scope,
877
- name,
878
- version: version || void 0,
879
- fullName: `${scope}/${name}`
880
- };
881
- }
882
- // Unscoped format: name[@version] (public registry)
883
- // name must not contain / (otherwise it might be a git shorthand)
884
- const unscopedMatch = trimmed.match(/^([\w-]+)(?:@(.+))?$/);
885
- if (!unscopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
886
- const [, name, version] = unscopedMatch;
1027
+ * Get home directory
1028
+ */ function getHomeDir() {
1029
+ return process.env.HOME || process.env.USERPROFILE || '';
1030
+ }
1031
+ /**
1032
+ * Shorten path display (replace home directory with ~)
1033
+ */ function shortenPath(fullPath, cwd) {
1034
+ const home = getHomeDir();
1035
+ const currentDir = cwd || process.cwd();
1036
+ if (fullPath.startsWith(home)) return fullPath.replace(home, '~');
1037
+ if (fullPath.startsWith(currentDir)) return `.${fullPath.slice(currentDir.length)}`;
1038
+ return fullPath;
1039
+ }
1040
+ const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEBPACK_EXTERNAL_MODULE_node_child_process__.exec);
1041
+ /**
1042
+ * Git utilities
1043
+ */ /**
1044
+ * SSH command with auto-accept for new host keys
1045
+ * Uses StrictHostKeyChecking=accept-new which:
1046
+ * - Automatically accepts keys for hosts not in known_hosts
1047
+ * - Still rejects connections if a known host's key has changed (security)
1048
+ */ const GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes';
1049
+ /**
1050
+ * Get environment variables for git commands that access remote repositories
1051
+ * Configures SSH to auto-accept new host keys and disables interactive prompts
1052
+ */ function getGitEnv() {
887
1053
  return {
888
- scope: null,
889
- name,
890
- version: version || void 0,
891
- fullName: name
1054
+ ...process.env,
1055
+ GIT_SSH_COMMAND,
1056
+ // Disable interactive prompts for HTTPS as well
1057
+ GIT_TERMINAL_PROMPT: '0'
892
1058
  };
893
1059
  }
894
1060
  /**
895
- * HTTP utilities for downloading and extracting skill archives
896
- */ /**
897
- * Custom error class for HTTP download failures
898
- */ class HttpDownloadError extends Error {
899
- url;
900
- statusCode;
901
- originalError;
902
- constructor(url, message, statusCode, originalError){
903
- super(message);
904
- this.name = 'HttpDownloadError';
905
- this.url = url;
906
- this.statusCode = statusCode;
907
- this.originalError = originalError;
908
- }
1061
+ * Custom error class for Git clone failures
1062
+ * Provides helpful tips for private repository authentication
1063
+ */ class GitCloneError extends Error {
1064
+ repoUrl;
1065
+ originalError;
1066
+ isAuthError;
1067
+ urlType;
1068
+ constructor(repoUrl, originalError){
1069
+ const isAuthError = GitCloneError.isAuthenticationError(originalError.message);
1070
+ const urlType = GitCloneError.detectUrlType(repoUrl);
1071
+ let message = `Failed to clone repository: ${repoUrl}`;
1072
+ if (isAuthError) {
1073
+ message += '\n\nTip: For private repos, ensure git credentials are configured:';
1074
+ if ('ssh' === urlType) {
1075
+ message += '\n - Check ~/.ssh/id_rsa or ~/.ssh/id_ed25519';
1076
+ message += '\n - Ensure SSH key is added to your Git hosting service';
1077
+ } else {
1078
+ // HTTPS or unknown
1079
+ message += "\n - Run 'git config --global credential.helper store'";
1080
+ message += '\n - Or use a personal access token in the URL';
1081
+ }
1082
+ }
1083
+ super(message);
1084
+ this.name = 'GitCloneError';
1085
+ this.repoUrl = repoUrl;
1086
+ this.originalError = originalError;
1087
+ this.isAuthError = isAuthError;
1088
+ this.urlType = urlType;
1089
+ }
1090
+ /**
1091
+ * Detect URL type from repository URL
1092
+ */ static detectUrlType(url) {
1093
+ if (url.startsWith('git@') || url.startsWith('ssh://')) return 'ssh';
1094
+ if (url.startsWith('http://') || url.startsWith('https://')) return 'https';
1095
+ return 'unknown';
1096
+ }
1097
+ /**
1098
+ * Check if an error message indicates an authentication problem
1099
+ */ static isAuthenticationError(message) {
1100
+ const authPatterns = [
1101
+ /permission denied/i,
1102
+ /could not read from remote/i,
1103
+ /authentication failed/i,
1104
+ /fatal: repository.*not found/i,
1105
+ /host key verification failed/i,
1106
+ /access denied/i,
1107
+ /unauthorized/i,
1108
+ /403/,
1109
+ /401/
1110
+ ];
1111
+ return authPatterns.some((pattern)=>pattern.test(message));
1112
+ }
1113
+ }
1114
+ /**
1115
+ * Execute git command asynchronously
1116
+ */ async function git(args, cwd) {
1117
+ const { stdout } = await git_execAsync(`git ${args.join(' ')}`, {
1118
+ cwd,
1119
+ encoding: 'utf-8',
1120
+ env: getGitEnv()
1121
+ });
1122
+ return stdout.trim();
909
1123
  }
910
1124
  /**
911
- * Download a file from HTTP/HTTPS URL
912
- *
913
- * @param url - URL to download from
914
- * @param destPath - Destination file path
915
- * @param options - Download options
916
- */ async function downloadFile(url, destPath, options = {}) {
917
- const { timeout = 60000, headers = {} } = options;
918
- // Ensure destination directory exists
919
- ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(destPath));
1125
+ * Get remote tags for a repository
1126
+ */ async function getRemoteTags(repoUrl) {
920
1127
  try {
921
- // Use native fetch for HTTP/HTTPS
922
- const controller = new AbortController();
923
- const timeoutId = setTimeout(()=>controller.abort(), timeout);
924
- const response = await fetch(url, {
925
- signal: controller.signal,
926
- headers: {
927
- 'User-Agent': 'reskill/1.0',
928
- ...headers
1128
+ const output = await git([
1129
+ 'ls-remote',
1130
+ '--tags',
1131
+ '--refs',
1132
+ repoUrl
1133
+ ]);
1134
+ if (!output) return [];
1135
+ const tags = [];
1136
+ const lines = output.split('\n');
1137
+ for (const line of lines){
1138
+ const [commit, ref] = line.split('\t');
1139
+ if (commit && ref) {
1140
+ // Extract tag name from refs/tags/v1.0.0
1141
+ const tagName = ref.replace('refs/tags/', '');
1142
+ tags.push({
1143
+ name: tagName,
1144
+ commit
1145
+ });
929
1146
  }
930
- });
931
- clearTimeout(timeoutId);
932
- if (!response.ok) throw new HttpDownloadError(url, `HTTP ${response.status}: ${response.statusText}`, response.status);
933
- // Stream response to file
934
- const fileStream = external_node_fs_.createWriteStream(destPath);
935
- const body = response.body;
936
- if (!body) throw new HttpDownloadError(url, 'Response body is empty');
937
- // Convert Web ReadableStream to Node.js Readable
938
- const { Readable } = await import("node:stream");
939
- const nodeStream = Readable.fromWeb(body);
940
- await (0, __WEBPACK_EXTERNAL_MODULE_node_stream_promises__.pipeline)(nodeStream, fileStream);
941
- } catch (error) {
942
- // Clean up partial download
943
- if (external_node_fs_.existsSync(destPath)) external_node_fs_.unlinkSync(destPath);
944
- if (error instanceof HttpDownloadError) throw error;
945
- const err = error;
946
- if ('AbortError' === err.name) throw new HttpDownloadError(url, `Download timeout after ${timeout}ms`);
947
- throw new HttpDownloadError(url, `Download failed: ${err.message}`, void 0, err);
1147
+ }
1148
+ return tags;
1149
+ } catch {
1150
+ return [];
948
1151
  }
949
1152
  }
950
1153
  /**
951
- * Extract an archive to a directory
952
- *
953
- * @param archivePath - Path to the archive file
954
- * @param destDir - Destination directory
955
- * @param format - Archive format (auto-detected from extension if not provided)
956
- */ async function extractArchive(archivePath, destDir, format) {
957
- // Auto-detect format from extension
958
- const detectedFormat = format || detectArchiveFormat(archivePath);
959
- if (!detectedFormat) throw new Error(`Unable to detect archive format for: ${archivePath}`);
960
- // Ensure destination directory exists
961
- ensureDir(destDir);
962
- switch(detectedFormat){
963
- case 'tar.gz':
964
- case 'tgz':
965
- case 'tar':
966
- await extractTar(archivePath, destDir, 'tar.gz' === detectedFormat || 'tgz' === detectedFormat);
967
- break;
968
- case 'zip':
969
- await extractZip(archivePath, destDir);
970
- break;
971
- default:
972
- throw new Error(`Unsupported archive format: ${detectedFormat}`);
973
- }
1154
+ * Get latest tag from repository
1155
+ */ async function getLatestTag(repoUrl) {
1156
+ const tags = await getRemoteTags(repoUrl);
1157
+ if (0 === tags.length) return null;
1158
+ // Sort by semver (simple version sort)
1159
+ const sortedTags = tags.sort((a, b)=>{
1160
+ const aVer = a.name.replace(/^v/, '');
1161
+ const bVer = b.name.replace(/^v/, '');
1162
+ return compareVersions(bVer, aVer);
1163
+ });
1164
+ return sortedTags[0];
974
1165
  }
975
1166
  /**
976
- * Extract tar archive using native tar command
977
- */ async function extractTar(archivePath, destDir, gzipped) {
978
- const { exec } = await import("node:child_process");
979
- const { promisify } = await import("node:util");
980
- const execAsync = promisify(exec);
981
- const flags = gzipped ? '-xzf' : '-xf';
1167
+ * Clone a repository with shallow clone
1168
+ *
1169
+ * @throws {GitCloneError} When clone fails, with helpful tips for authentication issues
1170
+ */ async function clone(repoUrl, destPath, options) {
1171
+ const args = [
1172
+ 'clone'
1173
+ ];
1174
+ if (options?.depth) args.push('--depth', options.depth.toString());
1175
+ if (options?.branch) args.push('--branch', options.branch);
1176
+ args.push(repoUrl, destPath);
982
1177
  try {
983
- // Extract to a temp directory first to handle single-folder archives
984
- const tempExtractDir = `${destDir}.extract-temp`;
985
- ensureDir(tempExtractDir);
986
- await execAsync(`tar ${flags} "${archivePath}" -C "${tempExtractDir}"`, {
987
- encoding: 'utf-8'
988
- });
989
- // Check if archive contains a single root directory
990
- const extractedItems = external_node_fs_.readdirSync(tempExtractDir);
991
- if (1 === extractedItems.length) {
992
- const singleItem = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, extractedItems[0]);
993
- if (external_node_fs_.statSync(singleItem).isDirectory()) {
994
- // Move contents of single directory to destination
995
- const contents = external_node_fs_.readdirSync(singleItem);
996
- for (const item of contents){
997
- const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(singleItem, item);
998
- const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
999
- external_node_fs_.renameSync(src, dest);
1000
- }
1001
- remove(tempExtractDir);
1002
- return;
1003
- }
1004
- }
1005
- // Move all items to destination
1006
- for (const item of extractedItems){
1007
- const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, item);
1008
- const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
1009
- external_node_fs_.renameSync(src, dest);
1010
- }
1011
- remove(tempExtractDir);
1178
+ await git(args);
1012
1179
  } catch (error) {
1013
- throw new Error(`Failed to extract tar archive: ${error.message}`);
1180
+ throw new GitCloneError(repoUrl, error);
1014
1181
  }
1015
1182
  }
1016
1183
  /**
1017
- * Extract zip archive using native unzip command or Node.js
1018
- */ async function extractZip(archivePath, destDir) {
1019
- const { exec } = await import("node:child_process");
1020
- const { promisify } = await import("node:util");
1021
- const execAsync = promisify(exec);
1184
+ * Get current commit hash
1185
+ */ async function getCurrentCommit(cwd) {
1186
+ return git([
1187
+ 'rev-parse',
1188
+ 'HEAD'
1189
+ ], cwd);
1190
+ }
1191
+ /**
1192
+ * Get default branch name
1193
+ */ async function getDefaultBranch(repoUrl) {
1022
1194
  try {
1023
- // Extract to a temp directory first
1024
- const tempExtractDir = `${destDir}.extract-temp`;
1025
- ensureDir(tempExtractDir);
1026
- // Try using unzip command (available on most systems)
1027
- await execAsync(`unzip -q "${archivePath}" -d "${tempExtractDir}"`, {
1028
- encoding: 'utf-8'
1029
- });
1030
- // Check if archive contains a single root directory
1031
- const extractedItems = external_node_fs_.readdirSync(tempExtractDir);
1032
- if (1 === extractedItems.length) {
1033
- const singleItem = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, extractedItems[0]);
1034
- if (external_node_fs_.statSync(singleItem).isDirectory()) {
1035
- // Move contents of single directory to destination
1036
- const contents = external_node_fs_.readdirSync(singleItem);
1037
- for (const item of contents){
1038
- const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(singleItem, item);
1039
- const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
1040
- external_node_fs_.renameSync(src, dest);
1041
- }
1042
- remove(tempExtractDir);
1043
- return;
1044
- }
1195
+ const output = await git([
1196
+ 'ls-remote',
1197
+ '--symref',
1198
+ repoUrl,
1199
+ 'HEAD'
1200
+ ]);
1201
+ const match = output.match(/ref: refs\/heads\/(\S+)/);
1202
+ return match ? match[1] : 'main';
1203
+ } catch {
1204
+ return 'main';
1205
+ }
1206
+ }
1207
+ /**
1208
+ * Simple version comparison (for sorting)
1209
+ */ function compareVersions(a, b) {
1210
+ const aParts = a.split('.').map((p)=>parseInt(p, 10) || 0);
1211
+ const bParts = b.split('.').map((p)=>parseInt(p, 10) || 0);
1212
+ const maxLength = Math.max(aParts.length, bParts.length);
1213
+ for(let i = 0; i < maxLength; i++){
1214
+ const aPart = aParts[i] || 0;
1215
+ const bPart = bParts[i] || 0;
1216
+ if (aPart > bPart) return 1;
1217
+ if (aPart < bPart) return -1;
1218
+ }
1219
+ return 0;
1220
+ }
1221
+ /**
1222
+ * Check if a source string is a complete Git URL (SSH, HTTPS, git://, or file://)
1223
+ *
1224
+ * Supported formats:
1225
+ * - SSH: git@github.com:user/repo.git
1226
+ * - HTTPS: https://github.com/user/repo.git
1227
+ * - Git protocol: git://github.com/user/repo.git
1228
+ * - File protocol: file:///path/to/repo (for local testing)
1229
+ * - URLs ending with .git
1230
+ */ function isGitUrl(source) {
1231
+ return source.startsWith('git@') || source.startsWith('git://') || source.startsWith('http://') || source.startsWith('https://') || source.startsWith('file://') || source.endsWith('.git');
1232
+ }
1233
+ /**
1234
+ * Parse a Git URL and extract host, owner, and repo information
1235
+ *
1236
+ * Supports:
1237
+ * - SSH: git@github.com:user/repo.git
1238
+ * - HTTPS: https://github.com/user/repo.git
1239
+ * - Git protocol: git://github.com/user/repo.git
1240
+ *
1241
+ * Note: GitHub/GitLab web URLs (with /tree/, /blob/, etc.) are handled
1242
+ * at a higher level in GitResolver.parseGitUrlRef() before calling this function.
1243
+ *
1244
+ * @param url The Git URL to parse
1245
+ * @returns Parsed URL information or null if parsing fails
1246
+ */ function parseGitUrl(url) {
1247
+ // Remove trailing .git if present
1248
+ const cleanUrl = url.replace(/\.git$/, '');
1249
+ // SSH format: git@github.com:user/repo
1250
+ const sshMatch = cleanUrl.match(/^git@([^:]+):(.+)$/);
1251
+ if (sshMatch) {
1252
+ const [, host, path] = sshMatch;
1253
+ const parts = path.split('/');
1254
+ if (parts.length >= 2) {
1255
+ // Handle nested paths like org/sub/repo
1256
+ const owner = parts.slice(0, -1).join('/');
1257
+ const repo = parts[parts.length - 1];
1258
+ return {
1259
+ host,
1260
+ owner,
1261
+ repo,
1262
+ url,
1263
+ type: 'ssh'
1264
+ };
1045
1265
  }
1046
- // Move all items to destination
1047
- for (const item of extractedItems){
1048
- const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, item);
1049
- const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
1050
- external_node_fs_.renameSync(src, dest);
1266
+ }
1267
+ // HTTPS/Git protocol format: https://github.com/user/repo or git://github.com/user/repo
1268
+ const httpMatch = cleanUrl.match(/^(https?|git):\/\/([^/]+)\/(.+)$/);
1269
+ if (httpMatch) {
1270
+ const [, protocol, host, path] = httpMatch;
1271
+ const parts = path.split('/');
1272
+ if (parts.length >= 2) {
1273
+ const owner = parts.slice(0, -1).join('/');
1274
+ const repo = parts[parts.length - 1];
1275
+ return {
1276
+ host,
1277
+ owner,
1278
+ repo,
1279
+ url,
1280
+ type: 'git' === protocol ? 'git' : 'https'
1281
+ };
1282
+ }
1283
+ }
1284
+ // File protocol format: file:///path/to/repo
1285
+ // Used for local testing and development
1286
+ const fileMatch = cleanUrl.match(/^file:\/\/(.+)$/);
1287
+ if (fileMatch) {
1288
+ const [, filePath] = fileMatch;
1289
+ const parts = filePath.split('/').filter(Boolean);
1290
+ if (parts.length >= 1) {
1291
+ // Use 'local' as host, path components as owner/repo
1292
+ const repo = parts[parts.length - 1];
1293
+ const owner = parts.length > 1 ? parts[parts.length - 2] : 'local';
1294
+ return {
1295
+ host: 'local',
1296
+ owner,
1297
+ repo,
1298
+ url,
1299
+ type: 'file'
1300
+ };
1051
1301
  }
1052
- remove(tempExtractDir);
1053
- } catch (error) {
1054
- throw new Error(`Failed to extract zip archive: ${error.message}`);
1055
1302
  }
1303
+ return null;
1056
1304
  }
1057
1305
  /**
1058
- * Detect archive format from file path
1059
- */ function detectArchiveFormat(filePath) {
1060
- const lower = filePath.toLowerCase();
1061
- if (lower.endsWith('.tar.gz')) return 'tar.gz';
1062
- if (lower.endsWith('.tgz')) return 'tgz';
1063
- if (lower.endsWith('.zip')) return 'zip';
1064
- if (lower.endsWith('.tar')) return 'tar';
1065
- }
1306
+ * Registry-Scope Mapping Utilities
1307
+ *
1308
+ * Maps registry URLs to their corresponding scopes.
1309
+ * Currently hardcoded; TODO: fetch from /api/registry/info in the future.
1310
+ */ /**
1311
+ * Public Registry URL
1312
+ * Used for installing skills without a scope
1313
+ */ const PUBLIC_REGISTRY = 'https://reskill.info/';
1066
1314
  /**
1067
- * Download and extract an archive in one operation
1315
+ * Hardcoded registry to scope mapping
1316
+ * TODO: Replace with dynamic fetching from /api/registry/info
1317
+ */ const REGISTRY_SCOPE_MAP = {
1318
+ // rush-app (private registry, new)
1319
+ 'https://rush-test.zhenguanyu.com': '@kanyun-test',
1320
+ 'https://rush.zhenguanyu.com': '@kanyun',
1321
+ // reskill-app (private registry, legacy)
1322
+ 'https://reskill-test.zhenguanyu.com': '@kanyun-test',
1323
+ // Local development
1324
+ 'http://localhost:3000': '@kanyun-test'
1325
+ };
1326
+ /**
1327
+ * Get the scope for a given registry URL
1068
1328
  *
1069
- * @param url - URL to download from
1070
- * @param destDir - Destination directory for extracted contents
1071
- * @param options - Download options
1072
- * @returns Path to extracted contents
1073
- */ async function downloadAndExtract(url, destDir, options = {}) {
1074
- // Determine archive filename from URL
1075
- const urlObj = new URL(url);
1076
- const filename = __WEBPACK_EXTERNAL_MODULE_node_path__.basename(urlObj.pathname);
1077
- // Detect format from original filename before adding .download suffix
1078
- const format = detectArchiveFormat(filename);
1079
- if (!format) throw new Error(`Unable to detect archive format from URL: ${url}`);
1080
- const tempArchive = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, `../${filename}.download`);
1081
- try {
1082
- // Download archive
1083
- await downloadFile(url, tempArchive, options);
1084
- // Extract archive with explicit format
1085
- await extractArchive(tempArchive, destDir, format);
1086
- return destDir;
1087
- } finally{
1088
- // Clean up temp archive
1089
- if (external_node_fs_.existsSync(tempArchive)) external_node_fs_.unlinkSync(tempArchive);
1090
- }
1329
+ * @param registry - Registry URL
1330
+ * @returns Scope string (e.g., "@kanyun") or null if not found
1331
+ *
1332
+ * @example
1333
+ * getScopeForRegistry('https://rush-test.zhenguanyu.com') // '@kanyun'
1334
+ * getScopeForRegistry('https://unknown.com') // null
1335
+ */ function getScopeForRegistry(registry) {
1336
+ if (!registry) return null;
1337
+ // Try exact match first
1338
+ if (REGISTRY_SCOPE_MAP[registry]) return REGISTRY_SCOPE_MAP[registry];
1339
+ // Try with/without trailing slash
1340
+ const normalized = registry.endsWith('/') ? registry.slice(0, -1) : `${registry}/`;
1341
+ return REGISTRY_SCOPE_MAP[normalized] || null;
1091
1342
  }
1092
1343
  /**
1093
- * Skill Parser - SKILL.md parser
1344
+ * Get the registry URL for a given scope (reverse lookup)
1094
1345
  *
1095
- * Following agentskills.io specification: https://agentskills.io/specification
1346
+ * @param scope - Scope string (with or without @ prefix), e.g., "@kanyun" or "kanyun"
1347
+ * @param customRegistries - Optional custom scope-to-registry mapping (from skills.json)
1348
+ * @returns Registry URL (with trailing slash) or null if not found
1096
1349
  *
1097
- * SKILL.md format requirements:
1098
- * - YAML frontmatter containing name and description (required)
1099
- * - name: max 64 characters, lowercase letters, numbers, hyphens
1100
- * - description: max 1024 characters
1101
- * - Optional fields: license, compatibility, metadata, allowed-tools
1102
- */ /**
1103
- * Skill validation error
1104
- */ class SkillValidationError extends Error {
1105
- field;
1106
- constructor(message, field){
1107
- super(message), this.field = field;
1108
- this.name = 'SkillValidationError';
1350
+ * @example
1351
+ * getRegistryForScope('@kanyun') // 'https://rush-test.zhenguanyu.com/'
1352
+ * getRegistryForScope('kanyun') // 'https://rush-test.zhenguanyu.com/'
1353
+ * getRegistryForScope('@unknown') // null
1354
+ * getRegistryForScope('@mycompany', { '@mycompany': 'https://my.registry.com/' }) // 'https://my.registry.com/'
1355
+ */ function getRegistryForScope(scope, customRegistries) {
1356
+ if (!scope) return null;
1357
+ // Normalize scope: ensure @ prefix
1358
+ const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
1359
+ // 1. First check custom scopeRegistries (from skills.json)
1360
+ if (customRegistries?.[normalizedScope]) {
1361
+ const url = customRegistries[normalizedScope];
1362
+ // Normalize trailing slash
1363
+ return url.endsWith('/') ? url : `${url}/`;
1109
1364
  }
1365
+ // 2. Fall back to hardcoded defaults
1366
+ for (const [registry, registryScope] of Object.entries(REGISTRY_SCOPE_MAP))if (registryScope === normalizedScope) // Return URL with trailing slash (normalized format)
1367
+ return registry.endsWith('/') ? registry : `${registry}/`;
1368
+ return null;
1110
1369
  }
1111
1370
  /**
1112
- * Simple YAML frontmatter parser
1113
- * Parses --- delimited YAML header
1371
+ * Get the registry URL for a given scope
1114
1372
  *
1115
- * Supports:
1116
- * - Basic key: value pairs
1117
- * - Multiline strings (| and >)
1118
- * - Nested objects (one level deep, for metadata field)
1119
- */ function parseFrontmatter(content) {
1120
- const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
1121
- const match = content.match(frontmatterRegex);
1122
- if (!match) return {
1123
- data: {},
1124
- content
1125
- };
1126
- const yamlContent = match[1];
1127
- const markdownContent = match[2];
1128
- // Simple YAML parsing (supports basic key: value, one level of nesting,
1129
- // block scalars (| and >), and plain scalars spanning multiple indented lines)
1130
- const data = {};
1131
- const lines = yamlContent.split('\n');
1132
- let currentKey = '';
1133
- let currentValue = '';
1134
- let inMultiline = false;
1135
- let inNestedObject = false;
1136
- let inPlainScalar = false;
1137
- let nestedObject = {};
1138
- /**
1139
- * Save the current key/value accumulated so far, then reset state.
1140
- */ function flushCurrent() {
1141
- if (!currentKey) return;
1142
- if (inNestedObject) {
1143
- data[currentKey] = nestedObject;
1144
- nestedObject = {};
1145
- inNestedObject = false;
1146
- } else if (inPlainScalar || inMultiline) {
1147
- data[currentKey] = currentValue.trim();
1148
- inPlainScalar = false;
1149
- inMultiline = false;
1150
- } else data[currentKey] = parseYamlValue(currentValue.trim());
1151
- currentKey = '';
1152
- currentValue = '';
1153
- }
1154
- for (const line of lines){
1155
- const trimmedLine = line.trim();
1156
- if (!trimmedLine || trimmedLine.startsWith('#')) continue;
1157
- const isIndented = line.startsWith(' ');
1158
- // ---- Inside a block scalar (| or >) ----
1159
- if (inMultiline) {
1160
- if (isIndented) {
1161
- currentValue += (currentValue ? '\n' : '') + line.slice(2);
1162
- continue;
1163
- }
1164
- // Unindented line ends the block scalar — fall through to top-level parsing
1165
- flushCurrent();
1166
- }
1167
- // ---- Inside a plain scalar (multiline value without | or >) ----
1168
- if (inPlainScalar) {
1169
- if (isIndented) {
1170
- // Continuation line: join with a space (YAML plain scalar folding)
1171
- currentValue += ` ${trimmedLine}`;
1172
- continue;
1173
- }
1174
- // Unindented line ends the plain scalar — fall through to top-level parsing
1175
- flushCurrent();
1176
- }
1177
- // ---- Inside a nested object ----
1178
- if (inNestedObject && isIndented) {
1179
- const nestedMatch = line.match(/^ {2}([a-zA-Z_-]+):\s*(.*)$/);
1180
- if (nestedMatch) {
1181
- const [, nestedKey, nestedValue] = nestedMatch;
1182
- nestedObject[nestedKey] = parseYamlValue(nestedValue.trim());
1183
- continue;
1184
- }
1185
- // Indented line that isn't a nested key:value — this key was actually
1186
- // a plain scalar, not a nested object. Switch modes.
1187
- inNestedObject = false;
1188
- inPlainScalar = true;
1189
- currentValue = trimmedLine;
1190
- continue;
1191
- }
1192
- // ---- Top-level key: value ----
1193
- const keyValueMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
1194
- if (keyValueMatch) {
1195
- flushCurrent();
1196
- currentKey = keyValueMatch[1];
1197
- currentValue = keyValueMatch[2];
1198
- if ('|' === currentValue || '>' === currentValue) {
1199
- inMultiline = true;
1200
- currentValue = '';
1201
- } else if ('' === currentValue) {
1202
- // Empty value — could be nested object or plain scalar; peek at next lines
1203
- inNestedObject = true;
1204
- nestedObject = {};
1205
- }
1206
- continue;
1207
- }
1208
- // ---- Unindented line that isn't key:value while in nested object ----
1209
- if (inNestedObject) flushCurrent();
1373
+ * - With scope → lookup private Registry (throws if not found)
1374
+ * - Without scope (null/undefined/'') → returns public Registry
1375
+ *
1376
+ * @param scope - Scope string (with or without @ prefix), null, undefined, or empty string
1377
+ * @param customRegistries - Optional custom scope-to-registry mapping (from skills.json)
1378
+ * @returns Registry URL (with trailing slash)
1379
+ * @throws Error if scope is provided but not found in the registry map
1380
+ *
1381
+ * @example
1382
+ * getRegistryUrl('@kanyun') // 'https://rush-test.zhenguanyu.com/'
1383
+ * getRegistryUrl('kanyun') // 'https://rush-test.zhenguanyu.com/'
1384
+ * getRegistryUrl(null) // 'https://reskill.info/'
1385
+ * getRegistryUrl('') // 'https://reskill.info/'
1386
+ * getRegistryUrl('@unknown') // throws Error
1387
+ * getRegistryUrl('@mycompany', { '@mycompany': 'https://my.registry.com/' }) // 'https://my.registry.com/'
1388
+ */ function getRegistryUrl(scope, customRegistries) {
1389
+ // No scope → return public Registry
1390
+ if (!scope) return PUBLIC_REGISTRY;
1391
+ // With scope → lookup private Registry
1392
+ const registry = getRegistryForScope(scope, customRegistries);
1393
+ if (!registry) {
1394
+ // Normalize scope for error message
1395
+ const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
1396
+ throw new Error(`Unknown scope ${normalizedScope}. No registry configured for this scope.`);
1210
1397
  }
1211
- // Save last accumulated value
1212
- flushCurrent();
1213
- return {
1214
- data,
1215
- content: markdownContent
1216
- };
1398
+ return registry;
1217
1399
  }
1218
1400
  /**
1219
- * Parse YAML value
1220
- */ function parseYamlValue(value) {
1221
- if (!value) return '';
1222
- // Boolean value
1223
- if ('true' === value) return true;
1224
- if ('false' === value) return false;
1225
- // Number
1226
- if (/^-?\d+$/.test(value)) return parseInt(value, 10);
1227
- if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
1228
- // Remove quotes
1229
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
1230
- return value;
1401
+ * Parse a skill name into its components
1402
+ *
1403
+ * @param skillName - Full or short skill name
1404
+ * @returns Parsed skill name with scope and name
1405
+ *
1406
+ * @example
1407
+ * parseSkillName('@kanyun/planning-with-files')
1408
+ * // { scope: '@kanyun', name: 'planning-with-files', fullName: '@kanyun/planning-with-files' }
1409
+ *
1410
+ * parseSkillName('planning-with-files')
1411
+ * // { scope: null, name: 'planning-with-files', fullName: 'planning-with-files' }
1412
+ */ function parseSkillName(skillName) {
1413
+ // Match @scope/name pattern
1414
+ const match = skillName.match(/^(@[^/]+)\/(.+)$/);
1415
+ if (match) return {
1416
+ scope: match[1],
1417
+ name: match[2],
1418
+ fullName: skillName
1419
+ };
1420
+ return {
1421
+ scope: null,
1422
+ name: skillName,
1423
+ fullName: skillName
1424
+ };
1231
1425
  }
1232
1426
  /**
1233
- * Validate skill name format
1427
+ * Build full skill name from scope and name
1234
1428
  *
1235
- * Specification requirements:
1236
- * - Max 64 characters
1237
- * - Only lowercase letters, numbers, hyphens allowed
1238
- * - Cannot start or end with hyphen
1239
- * - Cannot contain consecutive hyphens
1240
- */ function validateSkillName(name) {
1241
- if (!name) throw new SkillValidationError('Skill name is required', 'name');
1242
- if (name.length > 64) throw new SkillValidationError('Skill name must be at most 64 characters', 'name');
1243
- if (!/^[a-z0-9]/.test(name)) throw new SkillValidationError('Skill name must start with a lowercase letter or number', 'name');
1244
- if (!/[a-z0-9]$/.test(name)) throw new SkillValidationError('Skill name must end with a lowercase letter or number', 'name');
1245
- if (/--/.test(name)) throw new SkillValidationError('Skill name cannot contain consecutive hyphens', 'name');
1246
- 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');
1247
- // Single character name
1248
- if (1 === name.length && !/^[a-z0-9]$/.test(name)) throw new SkillValidationError('Single character skill name must be a lowercase letter or number', 'name');
1429
+ * @param scope - Scope (with or without @ prefix), or null
1430
+ * @param name - Short skill name
1431
+ * @returns Full skill name (e.g., "@kanyun/planning-with-files")
1432
+ *
1433
+ * @example
1434
+ * buildFullSkillName('@kanyun', 'planning-with-files') // '@kanyun/planning-with-files'
1435
+ * buildFullSkillName('kanyun', 'my-skill') // '@kanyun/my-skill'
1436
+ * buildFullSkillName(null, 'my-skill') // 'my-skill'
1437
+ */ function buildFullSkillName(scope, name) {
1438
+ if (!scope) return name;
1439
+ // Ensure scope starts with @
1440
+ const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
1441
+ return `${normalizedScope}/${name}`;
1249
1442
  }
1250
1443
  /**
1251
- * Validate skill description
1444
+ * Get short name from a skill name (removes scope if present)
1252
1445
  *
1253
- * Specification requirements:
1254
- * - Max 1024 characters
1255
- * - Angle brackets are allowed per agentskills.io spec
1256
- */ function validateSkillDescription(description) {
1257
- if (!description) throw new SkillValidationError('Skill description is required', 'description');
1258
- if (description.length > 1024) throw new SkillValidationError('Skill description must be at most 1024 characters', 'description');
1259
- // Note: angle brackets are allowed per agentskills.io spec
1446
+ * @param skillName - Full or short skill name
1447
+ * @returns Short name without scope
1448
+ *
1449
+ * @example
1450
+ * getShortName('@kanyun/planning-with-files') // 'planning-with-files'
1451
+ * getShortName('planning-with-files') // 'planning-with-files'
1452
+ */ function getShortName(skillName) {
1453
+ return parseSkillName(skillName).name;
1260
1454
  }
1261
1455
  /**
1262
- * Parse SKILL.md content
1456
+ * Parse a skill identifier into its components (with version support)
1263
1457
  *
1264
- * @param content - SKILL.md file content
1265
- * @param options - Parse options
1266
- * @returns Parsed skill info, or null if format is invalid
1267
- * @throws SkillValidationError if validation fails in strict mode
1268
- */ function parseSkillMd(content, options = {}) {
1269
- const { strict = false } = options;
1270
- try {
1271
- const { data, content: body } = parseFrontmatter(content);
1272
- // Check required fields
1273
- if (!data.name || !data.description) {
1274
- if (strict) throw new SkillValidationError('SKILL.md must have name and description in frontmatter');
1275
- return null;
1276
- }
1277
- const name = String(data.name);
1278
- const description = String(data.description);
1279
- // Validate field format
1280
- if (strict) {
1281
- validateSkillName(name);
1282
- validateSkillDescription(description);
1283
- }
1284
- // Parse allowed-tools
1285
- let allowedTools;
1286
- if (data['allowed-tools']) {
1287
- const toolsStr = String(data['allowed-tools']);
1288
- allowedTools = toolsStr.split(/\s+/).filter(Boolean);
1289
- }
1458
+ * Supports both private registry (with @scope) and public registry (without scope) formats.
1459
+ *
1460
+ * @param identifier - Skill identifier string
1461
+ * @returns Parsed skill identifier with scope, name, version, and fullName
1462
+ * @throws Error if identifier is invalid
1463
+ *
1464
+ * @example
1465
+ * // Private registry
1466
+ * parseSkillIdentifier('@kanyun/planning-with-files')
1467
+ * // { scope: '@kanyun', name: 'planning-with-files', version: undefined, fullName: '@kanyun/planning-with-files' }
1468
+ *
1469
+ * parseSkillIdentifier('@kanyun/skill@2.4.5')
1470
+ * // { scope: '@kanyun', name: 'skill', version: '2.4.5', fullName: '@kanyun/skill' }
1471
+ *
1472
+ * // Public registry
1473
+ * parseSkillIdentifier('planning-with-files')
1474
+ * // { scope: null, name: 'planning-with-files', version: undefined, fullName: 'planning-with-files' }
1475
+ *
1476
+ * parseSkillIdentifier('skill@latest')
1477
+ * // { scope: null, name: 'skill', version: 'latest', fullName: 'skill' }
1478
+ */ function parseSkillIdentifier(identifier) {
1479
+ const trimmed = identifier.trim();
1480
+ // Empty string or whitespace only
1481
+ if (!trimmed) throw new Error('Invalid skill identifier: empty string');
1482
+ // Starting with @@ is invalid
1483
+ if (trimmed.startsWith('@@')) throw new Error('Invalid skill identifier: invalid scope format');
1484
+ // Bare @ is invalid
1485
+ if ('@' === trimmed) throw new Error('Invalid skill identifier: missing scope and name');
1486
+ // Scoped format: @scope/name[@version]
1487
+ if (trimmed.startsWith('@')) {
1488
+ // Regex: @scope/name[@version]
1489
+ // scope: starts with @, followed by alphanumeric, hyphens, underscores
1490
+ // name: alphanumeric, hyphens, underscores
1491
+ // version: optional, @ followed by any non-empty string
1492
+ const scopedMatch = trimmed.match(/^(@[\w-]+)\/([\w-]+)(?:@(.+))?$/);
1493
+ if (!scopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
1494
+ const [, scope, name, version] = scopedMatch;
1290
1495
  return {
1496
+ scope,
1291
1497
  name,
1292
- description,
1293
- version: data.version ? String(data.version) : void 0,
1294
- license: data.license ? String(data.license) : void 0,
1295
- compatibility: data.compatibility ? String(data.compatibility) : void 0,
1296
- metadata: data.metadata,
1297
- allowedTools,
1298
- content: body,
1299
- rawContent: content
1498
+ version: version || void 0,
1499
+ fullName: `${scope}/${name}`
1300
1500
  };
1301
- } catch (error) {
1302
- if (error instanceof SkillValidationError) throw error;
1303
- if (strict) throw new SkillValidationError(`Failed to parse SKILL.md: ${error}`);
1304
- return null;
1305
1501
  }
1502
+ // Unscoped format: name[@version] (public registry)
1503
+ // name must not contain / (otherwise it might be a git shorthand)
1504
+ const unscopedMatch = trimmed.match(/^([\w-]+)(?:@(.+))?$/);
1505
+ if (!unscopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
1506
+ const [, name, version] = unscopedMatch;
1507
+ return {
1508
+ scope: null,
1509
+ name,
1510
+ version: version || void 0,
1511
+ fullName: name
1512
+ };
1306
1513
  }
1307
1514
  /**
1308
- * Parse SKILL.md from file path
1309
- */ function parseSkillMdFile(filePath, options = {}) {
1310
- if (!external_node_fs_.existsSync(filePath)) {
1311
- if (options.strict) throw new SkillValidationError(`SKILL.md not found: ${filePath}`);
1312
- return null;
1515
+ * HTTP utilities for downloading and extracting skill archives
1516
+ */ /**
1517
+ * Custom error class for HTTP download failures
1518
+ */ class HttpDownloadError extends Error {
1519
+ url;
1520
+ statusCode;
1521
+ originalError;
1522
+ constructor(url, message, statusCode, originalError){
1523
+ super(message);
1524
+ this.name = 'HttpDownloadError';
1525
+ this.url = url;
1526
+ this.statusCode = statusCode;
1527
+ this.originalError = originalError;
1313
1528
  }
1314
- const content = external_node_fs_.readFileSync(filePath, 'utf-8');
1315
- return parseSkillMd(content, options);
1316
1529
  }
1317
1530
  /**
1318
- * Parse SKILL.md from skill directory
1319
- */ function parseSkillFromDir(dirPath, options = {}) {
1320
- const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
1321
- return parseSkillMdFile(skillMdPath, options);
1531
+ * Download a file from HTTP/HTTPS URL
1532
+ *
1533
+ * @param url - URL to download from
1534
+ * @param destPath - Destination file path
1535
+ * @param options - Download options
1536
+ */ async function downloadFile(url, destPath, options = {}) {
1537
+ const { timeout = 60000, headers = {} } = options;
1538
+ // Ensure destination directory exists
1539
+ fs_ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(destPath));
1540
+ try {
1541
+ // Use native fetch for HTTP/HTTPS
1542
+ const controller = new AbortController();
1543
+ const timeoutId = setTimeout(()=>controller.abort(), timeout);
1544
+ const response = await fetch(url, {
1545
+ signal: controller.signal,
1546
+ headers: {
1547
+ 'User-Agent': 'reskill/1.0',
1548
+ ...headers
1549
+ }
1550
+ });
1551
+ clearTimeout(timeoutId);
1552
+ if (!response.ok) throw new HttpDownloadError(url, `HTTP ${response.status}: ${response.statusText}`, response.status);
1553
+ // Stream response to file
1554
+ const fileStream = external_node_fs_.createWriteStream(destPath);
1555
+ const body = response.body;
1556
+ if (!body) throw new HttpDownloadError(url, 'Response body is empty');
1557
+ // Convert Web ReadableStream to Node.js Readable
1558
+ const { Readable } = await import("node:stream");
1559
+ const nodeStream = Readable.fromWeb(body);
1560
+ await (0, __WEBPACK_EXTERNAL_MODULE_node_stream_promises__.pipeline)(nodeStream, fileStream);
1561
+ } catch (error) {
1562
+ // Clean up partial download
1563
+ if (external_node_fs_.existsSync(destPath)) external_node_fs_.unlinkSync(destPath);
1564
+ if (error instanceof HttpDownloadError) throw error;
1565
+ const err = error;
1566
+ if ('AbortError' === err.name) throw new HttpDownloadError(url, `Download timeout after ${timeout}ms`);
1567
+ throw new HttpDownloadError(url, `Download failed: ${err.message}`, void 0, err);
1568
+ }
1322
1569
  }
1323
1570
  /**
1324
- * Check if directory contains valid SKILL.md
1325
- */ function hasValidSkillMd(dirPath) {
1326
- const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
1327
- if (!external_node_fs_.existsSync(skillMdPath)) return false;
1328
- try {
1329
- const skill = parseSkillMdFile(skillMdPath);
1330
- return null !== skill;
1331
- } catch {
1332
- return false;
1571
+ * Extract an archive to a directory
1572
+ *
1573
+ * @param archivePath - Path to the archive file
1574
+ * @param destDir - Destination directory
1575
+ * @param format - Archive format (auto-detected from extension if not provided)
1576
+ */ async function extractArchive(archivePath, destDir, format) {
1577
+ // Auto-detect format from extension
1578
+ const detectedFormat = format || detectArchiveFormat(archivePath);
1579
+ if (!detectedFormat) throw new Error(`Unable to detect archive format for: ${archivePath}`);
1580
+ // Ensure destination directory exists
1581
+ fs_ensureDir(destDir);
1582
+ switch(detectedFormat){
1583
+ case 'tar.gz':
1584
+ case 'tgz':
1585
+ case 'tar':
1586
+ await extractTar(archivePath, destDir, 'tar.gz' === detectedFormat || 'tgz' === detectedFormat);
1587
+ break;
1588
+ case 'zip':
1589
+ await extractZip(archivePath, destDir);
1590
+ break;
1591
+ default:
1592
+ throw new Error(`Unsupported archive format: ${detectedFormat}`);
1333
1593
  }
1334
1594
  }
1335
- const SKIP_DIRS = [
1336
- 'node_modules',
1337
- '.git',
1338
- 'dist',
1339
- 'build',
1340
- '__pycache__'
1341
- ];
1342
- const MAX_DISCOVER_DEPTH = 5;
1343
- const PRIORITY_SKILL_DIRS = [
1344
- 'skills',
1345
- '.agents/skills',
1346
- '.cursor/skills',
1347
- '.claude/skills',
1348
- '.windsurf/skills',
1349
- '.github/skills'
1350
- ];
1351
- function findSkillDirsRecursive(dir, depth, maxDepth, visitedDirs) {
1352
- if (depth > maxDepth) return [];
1353
- const resolvedDir = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir);
1354
- if (visitedDirs.has(resolvedDir)) return [];
1355
- if (!external_node_fs_.existsSync(dir) || !external_node_fs_.statSync(dir).isDirectory()) return [];
1356
- visitedDirs.add(resolvedDir);
1357
- const results = [];
1358
- let entries;
1595
+ /**
1596
+ * Extract tar archive using native tar command
1597
+ */ async function extractTar(archivePath, destDir, gzipped) {
1598
+ const { exec } = await import("node:child_process");
1599
+ const { promisify } = await import("node:util");
1600
+ const execAsync = promisify(exec);
1601
+ const flags = gzipped ? '-xzf' : '-xf';
1359
1602
  try {
1360
- entries = external_node_fs_.readdirSync(dir);
1361
- } catch {
1362
- return [];
1363
- }
1364
- for (const entry of entries){
1365
- if (SKIP_DIRS.includes(entry)) continue;
1366
- const fullPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
1367
- const resolvedFull = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(fullPath);
1368
- if (visitedDirs.has(resolvedFull)) continue;
1369
- let stat;
1370
- try {
1371
- stat = external_node_fs_.statSync(fullPath);
1372
- } catch {
1373
- continue;
1603
+ // Extract to a temp directory first to handle single-folder archives
1604
+ const tempExtractDir = `${destDir}.extract-temp`;
1605
+ fs_ensureDir(tempExtractDir);
1606
+ await execAsync(`tar ${flags} "${archivePath}" -C "${tempExtractDir}"`, {
1607
+ encoding: 'utf-8'
1608
+ });
1609
+ // Check if archive contains a single root directory
1610
+ const extractedItems = external_node_fs_.readdirSync(tempExtractDir);
1611
+ if (1 === extractedItems.length) {
1612
+ const singleItem = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, extractedItems[0]);
1613
+ if (external_node_fs_.statSync(singleItem).isDirectory()) {
1614
+ // Move contents of single directory to destination
1615
+ const contents = external_node_fs_.readdirSync(singleItem);
1616
+ for (const item of contents){
1617
+ const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(singleItem, item);
1618
+ const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
1619
+ external_node_fs_.renameSync(src, dest);
1620
+ }
1621
+ fs_remove(tempExtractDir);
1622
+ return;
1623
+ }
1374
1624
  }
1375
- if (!!stat.isDirectory()) {
1376
- if (hasValidSkillMd(fullPath)) results.push(fullPath);
1377
- results.push(...findSkillDirsRecursive(fullPath, depth + 1, maxDepth, visitedDirs));
1625
+ // Move all items to destination
1626
+ for (const item of extractedItems){
1627
+ const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, item);
1628
+ const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
1629
+ external_node_fs_.renameSync(src, dest);
1378
1630
  }
1631
+ fs_remove(tempExtractDir);
1632
+ } catch (error) {
1633
+ throw new Error(`Failed to extract tar archive: ${error.message}`);
1379
1634
  }
1380
- return results;
1381
1635
  }
1382
1636
  /**
1383
- * Discover all skills in a directory by scanning for SKILL.md files.
1384
- *
1385
- * Strategy:
1386
- * 1. Check root for SKILL.md
1387
- * 2. Search priority directories (skills/, .agents/skills/, .cursor/skills/, etc.)
1388
- * 3. Fall back to recursive search (max depth 5, skip node_modules, .git, dist, etc.)
1389
- *
1390
- * @param basePath - Root directory to search
1391
- * @returns List of parsed skills with their directory paths (absolute)
1392
- */ function discoverSkillsInDir(basePath) {
1393
- const resolvedBase = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(basePath);
1394
- const results = [];
1395
- const seenNames = new Set();
1396
- function addSkill(dirPath) {
1397
- const skill = parseSkillFromDir(dirPath);
1398
- if (skill && !seenNames.has(skill.name)) {
1399
- seenNames.add(skill.name);
1400
- results.push({
1401
- ...skill,
1402
- dirPath: __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dirPath)
1403
- });
1404
- }
1405
- }
1406
- if (hasValidSkillMd(resolvedBase)) addSkill(resolvedBase);
1407
- // Track visited directories to avoid redundant I/O during recursive scan
1408
- const visitedDirs = new Set();
1409
- for (const sub of PRIORITY_SKILL_DIRS){
1410
- const dir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(resolvedBase, sub);
1411
- if (!!external_node_fs_.existsSync(dir) && !!external_node_fs_.statSync(dir).isDirectory()) {
1412
- visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir));
1413
- try {
1414
- const entries = external_node_fs_.readdirSync(dir);
1415
- for (const entry of entries){
1416
- const skillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
1417
- try {
1418
- if (external_node_fs_.statSync(skillDir).isDirectory() && hasValidSkillMd(skillDir)) {
1419
- addSkill(skillDir);
1420
- visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(skillDir));
1421
- }
1422
- } catch {
1423
- // Skip entries that can't be stat'd (race condition, permission, etc.)
1424
- }
1637
+ * Extract zip archive using native unzip command or Node.js
1638
+ */ async function extractZip(archivePath, destDir) {
1639
+ const { exec } = await import("node:child_process");
1640
+ const { promisify } = await import("node:util");
1641
+ const execAsync = promisify(exec);
1642
+ try {
1643
+ // Extract to a temp directory first
1644
+ const tempExtractDir = `${destDir}.extract-temp`;
1645
+ fs_ensureDir(tempExtractDir);
1646
+ // Try using unzip command (available on most systems)
1647
+ await execAsync(`unzip -q "${archivePath}" -d "${tempExtractDir}"`, {
1648
+ encoding: 'utf-8'
1649
+ });
1650
+ // Check if archive contains a single root directory
1651
+ const extractedItems = external_node_fs_.readdirSync(tempExtractDir);
1652
+ if (1 === extractedItems.length) {
1653
+ const singleItem = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, extractedItems[0]);
1654
+ if (external_node_fs_.statSync(singleItem).isDirectory()) {
1655
+ // Move contents of single directory to destination
1656
+ const contents = external_node_fs_.readdirSync(singleItem);
1657
+ for (const item of contents){
1658
+ const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(singleItem, item);
1659
+ const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
1660
+ external_node_fs_.renameSync(src, dest);
1425
1661
  }
1426
- } catch {
1427
- // Skip if unreadable
1662
+ fs_remove(tempExtractDir);
1663
+ return;
1428
1664
  }
1429
1665
  }
1666
+ // Move all items to destination
1667
+ for (const item of extractedItems){
1668
+ const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, item);
1669
+ const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
1670
+ external_node_fs_.renameSync(src, dest);
1671
+ }
1672
+ fs_remove(tempExtractDir);
1673
+ } catch (error) {
1674
+ throw new Error(`Failed to extract zip archive: ${error.message}`);
1430
1675
  }
1431
- const recursiveDirs = findSkillDirsRecursive(resolvedBase, 0, MAX_DISCOVER_DEPTH, visitedDirs);
1432
- for (const skillDir of recursiveDirs)addSkill(skillDir);
1433
- return results;
1434
1676
  }
1435
1677
  /**
1436
- * Filter skills by name (case-insensitive exact match).
1437
- *
1438
- * Note: an empty `names` array returns an empty result (not all skills).
1439
- * Callers should check `names.length` before calling if "no filter = all" is desired.
1678
+ * Detect archive format from file path
1679
+ */ function detectArchiveFormat(filePath) {
1680
+ const lower = filePath.toLowerCase();
1681
+ if (lower.endsWith('.tar.gz')) return 'tar.gz';
1682
+ if (lower.endsWith('.tgz')) return 'tgz';
1683
+ if (lower.endsWith('.zip')) return 'zip';
1684
+ if (lower.endsWith('.tar')) return 'tar';
1685
+ }
1686
+ /**
1687
+ * Download and extract an archive in one operation
1440
1688
  *
1441
- * @param skills - List of discovered skills
1442
- * @param names - Skill names to match (e.g. from --skill pdf commit)
1443
- * @returns Skills whose name matches any of the given names
1444
- */ function filterSkillsByName(skills, names) {
1445
- const normalized = names.map((n)=>n.toLowerCase());
1446
- return skills.filter((skill)=>{
1447
- // Match against SKILL.md name field
1448
- if (normalized.includes(skill.name.toLowerCase())) return true;
1449
- // Also match against the directory name (basename of dirPath)
1450
- // Users naturally refer to skills by their directory name
1451
- const dirName = __WEBPACK_EXTERNAL_MODULE_node_path__.basename(skill.dirPath).toLowerCase();
1452
- return normalized.includes(dirName);
1453
- });
1689
+ * @param url - URL to download from
1690
+ * @param destDir - Destination directory for extracted contents
1691
+ * @param options - Download options
1692
+ * @returns Path to extracted contents
1693
+ */ async function downloadAndExtract(url, destDir, options = {}) {
1694
+ // Determine archive filename from URL
1695
+ const urlObj = new URL(url);
1696
+ const filename = __WEBPACK_EXTERNAL_MODULE_node_path__.basename(urlObj.pathname);
1697
+ // Detect format from original filename before adding .download suffix
1698
+ const format = detectArchiveFormat(filename);
1699
+ if (!format) throw new Error(`Unable to detect archive format from URL: ${url}`);
1700
+ const tempArchive = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, `../${filename}.download`);
1701
+ try {
1702
+ // Download archive
1703
+ await downloadFile(url, tempArchive, options);
1704
+ // Extract archive with explicit format
1705
+ await extractArchive(tempArchive, destDir, format);
1706
+ return destDir;
1707
+ } finally{
1708
+ // Clean up temp archive
1709
+ if (external_node_fs_.existsSync(tempArchive)) external_node_fs_.unlinkSync(tempArchive);
1710
+ }
1454
1711
  }
1455
1712
  /**
1456
1713
  * Installer - Multi-Agent installer
@@ -1469,14 +1726,14 @@ const installer_SKILLS_SUBDIR = 'skills';
1469
1726
  /**
1470
1727
  * Default files to exclude when copying skills
1471
1728
  * These files are typically used for repository metadata and should not be copied to agent directories
1472
- */ const DEFAULT_EXCLUDE_FILES = [
1729
+ */ const installer_DEFAULT_EXCLUDE_FILES = [
1473
1730
  'README.md',
1474
1731
  'metadata.json',
1475
1732
  '.reskill-commit'
1476
1733
  ];
1477
1734
  /**
1478
1735
  * Prefix for files that should be excluded (internal/private files)
1479
- */ const EXCLUDE_PREFIX = '_';
1736
+ */ const installer_EXCLUDE_PREFIX = '_';
1480
1737
  /**
1481
1738
  * Sanitize filename to prevent path traversal attacks
1482
1739
  */ function installer_sanitizeName(name) {
@@ -1530,18 +1787,18 @@ const installer_SKILLS_SUBDIR = 'skills';
1530
1787
  * By default excludes:
1531
1788
  * - Files in DEFAULT_EXCLUDE_FILES (README.md, metadata.json, .reskill-commit)
1532
1789
  * - Files starting with EXCLUDE_PREFIX ('_')
1533
- */ function copyDirectory(src, dest, options) {
1534
- const exclude = new Set(options?.exclude || DEFAULT_EXCLUDE_FILES);
1790
+ */ function installer_copyDirectory(src, dest, options) {
1791
+ const exclude = new Set(options?.exclude || installer_DEFAULT_EXCLUDE_FILES);
1535
1792
  installer_ensureDir(dest);
1536
1793
  const entries = external_node_fs_.readdirSync(src, {
1537
1794
  withFileTypes: true
1538
1795
  });
1539
1796
  for (const entry of entries){
1540
1797
  // Skip files starting with EXCLUDE_PREFIX and files in exclude list
1541
- if (exclude.has(entry.name) || entry.name.startsWith(EXCLUDE_PREFIX)) continue;
1798
+ if (exclude.has(entry.name) || entry.name.startsWith(installer_EXCLUDE_PREFIX)) continue;
1542
1799
  const srcPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(src, entry.name);
1543
1800
  const destPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dest, entry.name);
1544
- if (entry.isDirectory()) copyDirectory(srcPath, destPath, options);
1801
+ if (entry.isDirectory()) installer_copyDirectory(srcPath, destPath, options);
1545
1802
  else external_node_fs_.copyFileSync(srcPath, destPath);
1546
1803
  }
1547
1804
  }
@@ -1608,6 +1865,7 @@ const installer_SKILLS_SUBDIR = 'skills';
1608
1865
  /**
1609
1866
  * Get agent's skill installation path
1610
1867
  */ getAgentSkillPath(skillName, agentType) {
1868
+ if (agentType === claude_3p_installer_CLAUDE_COWORK_3P_AGENT) return getClaude3pSkillPath(skillName);
1611
1869
  const agent = getAgentConfig(agentType);
1612
1870
  const sanitized = installer_sanitizeName(skillName);
1613
1871
  const agentBase = this.isGlobal ? agent.globalSkillsDir : __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cwd, agent.skillsDir);
@@ -1621,6 +1879,9 @@ const installer_SKILLS_SUBDIR = 'skills';
1621
1879
  * @param agentType - Target agent type
1622
1880
  * @param options - Installation options
1623
1881
  */ async installForAgent(sourcePath, skillName, agentType, options = {}) {
1882
+ if (agentType === claude_3p_installer_CLAUDE_COWORK_3P_AGENT) return installClaude3pSkill(sourcePath, skillName, {
1883
+ mode: options.mode
1884
+ });
1624
1885
  const agent = getAgentConfig(agentType);
1625
1886
  const installMode = options.mode || 'symlink';
1626
1887
  const sanitized = installer_sanitizeName(skillName);
@@ -1649,7 +1910,7 @@ const installer_SKILLS_SUBDIR = 'skills';
1649
1910
  if ('copy' === installMode) {
1650
1911
  installer_ensureDir(agentDir);
1651
1912
  installer_remove(agentDir);
1652
- copyDirectory(sourcePath, agentDir);
1913
+ installer_copyDirectory(sourcePath, agentDir);
1653
1914
  result = {
1654
1915
  success: true,
1655
1916
  path: agentDir,
@@ -1659,7 +1920,7 @@ const installer_SKILLS_SUBDIR = 'skills';
1659
1920
  // Symlink mode: copy to canonical location, then create symlink
1660
1921
  installer_ensureDir(canonicalDir);
1661
1922
  installer_remove(canonicalDir);
1662
- copyDirectory(sourcePath, canonicalDir);
1923
+ installer_copyDirectory(sourcePath, canonicalDir);
1663
1924
  const symlinkCreated = await installer_createSymlink(canonicalDir, agentDir);
1664
1925
  if (symlinkCreated) result = {
1665
1926
  success: true,
@@ -1675,7 +1936,7 @@ const installer_SKILLS_SUBDIR = 'skills';
1675
1936
  // Ignore cleanup errors
1676
1937
  }
1677
1938
  installer_ensureDir(agentDir);
1678
- copyDirectory(sourcePath, agentDir);
1939
+ installer_copyDirectory(sourcePath, agentDir);
1679
1940
  result = {
1680
1941
  success: true,
1681
1942
  path: agentDir,
@@ -1710,8 +1971,12 @@ const installer_SKILLS_SUBDIR = 'skills';
1710
1971
  /**
1711
1972
  * Check if skill is installed to specified agent
1712
1973
  */ isInstalled(skillName, agentType) {
1713
- const skillPath = this.getAgentSkillPath(skillName, agentType);
1714
- return external_node_fs_.existsSync(skillPath);
1974
+ try {
1975
+ const skillPath = this.getAgentSkillPath(skillName, agentType);
1976
+ return external_node_fs_.existsSync(skillPath);
1977
+ } catch {
1978
+ return false;
1979
+ }
1715
1980
  }
1716
1981
  /**
1717
1982
  * Check if skill is installed in canonical location
@@ -1722,6 +1987,11 @@ const installer_SKILLS_SUBDIR = 'skills';
1722
1987
  /**
1723
1988
  * Uninstall skill from specified agent
1724
1989
  */ uninstallFromAgent(skillName, agentType) {
1990
+ if (agentType === claude_3p_installer_CLAUDE_COWORK_3P_AGENT) try {
1991
+ return uninstallClaude3pSkill(skillName);
1992
+ } catch {
1993
+ return false;
1994
+ }
1725
1995
  const skillPath = this.getAgentSkillPath(skillName, agentType);
1726
1996
  if (!external_node_fs_.existsSync(skillPath)) return false;
1727
1997
  installer_remove(skillPath);
@@ -1742,6 +2012,11 @@ const installer_SKILLS_SUBDIR = 'skills';
1742
2012
  /**
1743
2013
  * Get all skills installed to specified agent
1744
2014
  */ listInstalledSkills(agentType) {
2015
+ if (agentType === claude_3p_installer_CLAUDE_COWORK_3P_AGENT) try {
2016
+ return listClaude3pSkills();
2017
+ } catch {
2018
+ return [];
2019
+ }
1745
2020
  const agent = getAgentConfig(agentType);
1746
2021
  const skillsDir = this.isGlobal ? agent.globalSkillsDir : __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cwd, agent.skillsDir);
1747
2022
  if (!external_node_fs_.existsSync(skillsDir)) return [];
@@ -1869,7 +2144,7 @@ ${CURSOR_BRIDGE_MARKER}
1869
2144
  * Check if skill is cached
1870
2145
  */ isCached(parsed, version) {
1871
2146
  const cachePath = this.getSkillCachePath(parsed, version);
1872
- return exists(cachePath) && isDirectory(cachePath);
2147
+ return fs_exists(cachePath) && isDirectory(cachePath);
1873
2148
  }
1874
2149
  /**
1875
2150
  * Get cached skill
@@ -1881,7 +2156,7 @@ ${CURSOR_BRIDGE_MARKER}
1881
2156
  let commit = '';
1882
2157
  try {
1883
2158
  const fs = await import("node:fs");
1884
- if (exists(commitFile)) commit = fs.readFileSync(commitFile, 'utf-8').trim();
2159
+ if (fs_exists(commitFile)) commit = fs.readFileSync(commitFile, 'utf-8').trim();
1885
2160
  } catch {
1886
2161
  // Ignore read errors
1887
2162
  }
@@ -1895,11 +2170,11 @@ ${CURSOR_BRIDGE_MARKER}
1895
2170
  */ async cache(repoUrl, parsed, ref, version) {
1896
2171
  const cachePath = this.getSkillCachePath(parsed, version);
1897
2172
  // If exists, delete first
1898
- if (exists(cachePath)) remove(cachePath);
1899
- ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(cachePath));
2173
+ if (fs_exists(cachePath)) fs_remove(cachePath);
2174
+ fs_ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(cachePath));
1900
2175
  // Clone repository
1901
2176
  const tempPath = `${cachePath}.tmp`;
1902
- remove(tempPath);
2177
+ fs_remove(tempPath);
1903
2178
  await clone(repoUrl, tempPath, {
1904
2179
  depth: 1,
1905
2180
  branch: ref
@@ -1909,8 +2184,8 @@ ${CURSOR_BRIDGE_MARKER}
1909
2184
  // If has subPath, only keep subdirectory
1910
2185
  if (parsed.subPath) {
1911
2186
  const subDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempPath, parsed.subPath);
1912
- if (!exists(subDir)) {
1913
- remove(tempPath);
2187
+ if (!fs_exists(subDir)) {
2188
+ fs_remove(tempPath);
1914
2189
  throw new Error(`Subpath ${parsed.subPath} not found in repository`);
1915
2190
  }
1916
2191
  copyDir(subDir, cachePath, {
@@ -1926,7 +2201,7 @@ ${CURSOR_BRIDGE_MARKER}
1926
2201
  // Save commit info
1927
2202
  external_node_fs_.writeFileSync(__WEBPACK_EXTERNAL_MODULE_node_path__.join(cachePath, '.reskill-commit'), commit);
1928
2203
  // Clean up temp directory
1929
- remove(tempPath);
2204
+ fs_remove(tempPath);
1930
2205
  return {
1931
2206
  path: cachePath,
1932
2207
  commit
@@ -1945,8 +2220,8 @@ ${CURSOR_BRIDGE_MARKER}
1945
2220
  */ async cacheFromHttp(url, parsed, version) {
1946
2221
  const cachePath = this.getSkillCachePath(parsed, version);
1947
2222
  // If exists, delete first
1948
- if (exists(cachePath)) remove(cachePath);
1949
- ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(cachePath));
2223
+ if (fs_exists(cachePath)) fs_remove(cachePath);
2224
+ fs_ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(cachePath));
1950
2225
  // Download and extract to cache path
1951
2226
  await downloadAndExtract(url, cachePath);
1952
2227
  // Generate a commit-like identifier from URL and version
@@ -1971,10 +2246,10 @@ ${CURSOR_BRIDGE_MARKER}
1971
2246
  const cached = await this.get(parsed, version);
1972
2247
  if (!cached) throw new Error(`Skill ${parsed.raw} version ${version} not found in cache`);
1973
2248
  // If target exists, delete first
1974
- if (exists(destPath)) remove(destPath);
2249
+ if (fs_exists(destPath)) fs_remove(destPath);
1975
2250
  // Use same exclude rules as Installer for consistency
1976
2251
  copyDir(cached.path, destPath, {
1977
- exclude: DEFAULT_EXCLUDE_FILES
2252
+ exclude: installer_DEFAULT_EXCLUDE_FILES
1978
2253
  });
1979
2254
  }
1980
2255
  /**
@@ -1982,22 +2257,22 @@ ${CURSOR_BRIDGE_MARKER}
1982
2257
  */ clearSkill(parsed, version) {
1983
2258
  if (version) {
1984
2259
  const cachePath = this.getSkillCachePath(parsed, version);
1985
- remove(cachePath);
2260
+ fs_remove(cachePath);
1986
2261
  } else {
1987
2262
  // Clear all versions
1988
2263
  const skillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cacheDir, parsed.registry, parsed.owner, parsed.repo);
1989
- remove(skillDir);
2264
+ fs_remove(skillDir);
1990
2265
  }
1991
2266
  }
1992
2267
  /**
1993
2268
  * Clear all cache
1994
2269
  */ clearAll() {
1995
- remove(this.cacheDir);
2270
+ fs_remove(this.cacheDir);
1996
2271
  }
1997
2272
  /**
1998
2273
  * Get cache statistics
1999
2274
  */ getStats() {
2000
- if (!exists(this.cacheDir)) return {
2275
+ if (!fs_exists(this.cacheDir)) return {
2001
2276
  totalSkills: 0,
2002
2277
  registries: []
2003
2278
  };
@@ -2151,7 +2426,7 @@ ${CURSOR_BRIDGE_MARKER}
2151
2426
  /**
2152
2427
  * Check if configuration file exists
2153
2428
  */ exists() {
2154
- return exists(this.configPath);
2429
+ return fs_exists(this.configPath);
2155
2430
  }
2156
2431
  /**
2157
2432
  * Load configuration from file
@@ -3073,7 +3348,7 @@ ${CURSOR_BRIDGE_MARKER}
3073
3348
  /**
3074
3349
  * Check if lock file exists
3075
3350
  */ exists() {
3076
- return exists(this.lockPath);
3351
+ return fs_exists(this.lockPath);
3077
3352
  }
3078
3353
  /**
3079
3354
  * Load lock file
@@ -4064,11 +4339,11 @@ class RegistryResolver {
4064
4339
  */ getSkillPath(name) {
4065
4340
  // Check canonical location first (.agents/skills/)
4066
4341
  const canonicalPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.getCanonicalSkillsDir(), name);
4067
- if (exists(canonicalPath)) return canonicalPath;
4342
+ if (fs_exists(canonicalPath)) return canonicalPath;
4068
4343
  // Check configured installation directory (.skills/ or custom)
4069
4344
  const installDir = this.getInstallDir();
4070
4345
  const installPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(installDir, name);
4071
- if (exists(installPath)) return installPath;
4346
+ if (fs_exists(installPath)) return installPath;
4072
4347
  // Default to configured installation directory for new installations
4073
4348
  // if it's not the default .skills, otherwise use canonical location.
4074
4349
  // This respects "installDir" in skills.json.
@@ -4148,7 +4423,7 @@ class RegistryResolver {
4148
4423
  const semanticVersion = metadata?.version ?? gitRef;
4149
4424
  const skillPath = this.getSkillPath(skillName);
4150
4425
  // Check if already installed (using the real name from SKILL.md)
4151
- if (exists(skillPath) && !force) {
4426
+ if (fs_exists(skillPath) && !force) {
4152
4427
  const locked = this.lockManager.get(skillName);
4153
4428
  // Compare ref if available, fallback to version for backward compatibility
4154
4429
  const lockedRef = locked?.ref || locked?.version;
@@ -4165,10 +4440,10 @@ class RegistryResolver {
4165
4440
  }
4166
4441
  logger_logger["package"](`Installing ${skillName}@${gitRef}...`);
4167
4442
  // Copy to installation directory
4168
- ensureDir(this.getInstallDir());
4169
- if (exists(skillPath)) remove(skillPath);
4443
+ fs_ensureDir(this.getInstallDir());
4444
+ if (fs_exists(skillPath)) fs_remove(skillPath);
4170
4445
  copyDir(sourcePath, skillPath, {
4171
- exclude: DEFAULT_EXCLUDE_FILES
4446
+ exclude: installer_DEFAULT_EXCLUDE_FILES
4172
4447
  });
4173
4448
  // Update lock file (project mode only)
4174
4449
  if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
@@ -4215,7 +4490,7 @@ class RegistryResolver {
4215
4490
  const semanticVersion = metadata?.version ?? version;
4216
4491
  const skillPath = this.getSkillPath(skillName);
4217
4492
  // Check if already installed (using the real name from SKILL.md)
4218
- if (exists(skillPath) && !force) {
4493
+ if (fs_exists(skillPath) && !force) {
4219
4494
  const locked = this.lockManager.get(skillName);
4220
4495
  const lockedRef = locked?.ref || locked?.version;
4221
4496
  if (locked && lockedRef === version) {
@@ -4231,8 +4506,8 @@ class RegistryResolver {
4231
4506
  }
4232
4507
  logger_logger["package"](`Installing ${skillName}@${version} from ${httpInfo.host}...`);
4233
4508
  // Copy to installation directory
4234
- ensureDir(this.getInstallDir());
4235
- if (exists(skillPath)) remove(skillPath);
4509
+ fs_ensureDir(this.getInstallDir());
4510
+ if (fs_exists(skillPath)) fs_remove(skillPath);
4236
4511
  await this.cache.copyTo(parsed, version, skillPath);
4237
4512
  // Update lock file (project mode only)
4238
4513
  if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
@@ -4277,13 +4552,13 @@ class RegistryResolver {
4277
4552
  * Uninstall skill
4278
4553
  */ uninstall(name) {
4279
4554
  const skillPath = this.getSkillPath(name);
4280
- if (!exists(skillPath)) {
4555
+ if (!fs_exists(skillPath)) {
4281
4556
  const location = this.isGlobal ? '(global)' : '';
4282
4557
  logger_logger.warn(`Skill ${name} is not installed ${location}`.trim());
4283
4558
  return false;
4284
4559
  }
4285
4560
  // Remove installation directory
4286
- remove(skillPath);
4561
+ fs_remove(skillPath);
4287
4562
  // Remove from lock file (project mode only)
4288
4563
  if (!this.isGlobal) this.lockManager.remove(name);
4289
4564
  // Remove from skills.json (project mode only)
@@ -4448,7 +4723,7 @@ class RegistryResolver {
4448
4723
  const seenNames = new Set();
4449
4724
  // Check canonical location first (.agents/skills/)
4450
4725
  const canonicalDir = this.getCanonicalSkillsDir();
4451
- if (exists(canonicalDir)) for (const name of listDir(canonicalDir)){
4726
+ if (fs_exists(canonicalDir)) for (const name of listDir(canonicalDir)){
4452
4727
  const skillPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(canonicalDir, name);
4453
4728
  if (!isDirectory(skillPath)) continue;
4454
4729
  const skill = this.getInstalledSkillFromPath(name, skillPath);
@@ -4459,7 +4734,7 @@ class RegistryResolver {
4459
4734
  }
4460
4735
  // Check legacy location (.skills/)
4461
4736
  const legacyDir = this.getInstallDir();
4462
- if (exists(legacyDir) && legacyDir !== canonicalDir) for (const name of listDir(legacyDir)){
4737
+ if (fs_exists(legacyDir) && legacyDir !== canonicalDir) for (const name of listDir(legacyDir)){
4463
4738
  // Skip if already found in canonical location
4464
4739
  if (seenNames.has(name)) continue;
4465
4740
  const skillPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(legacyDir, name);
@@ -4489,18 +4764,26 @@ class RegistryResolver {
4489
4764
  const canonicalDir = this.getCanonicalSkillsDir();
4490
4765
  const installed = [];
4491
4766
  for (const [type, config] of Object.entries(agents)){
4767
+ if (type === claude_3p_installer_CLAUDE_COWORK_3P_AGENT) {
4768
+ try {
4769
+ if (fs_exists(getClaude3pSkillPath(skillName))) installed.push(type);
4770
+ } catch {
4771
+ // Claude Cowork 3P keeps skills under an app-managed account directory.
4772
+ }
4773
+ continue;
4774
+ }
4492
4775
  const agentBase = this.isGlobal ? config.globalSkillsDir : __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.projectRoot, config.skillsDir);
4493
4776
  // Skip agents whose skillsDir is the canonical directory itself
4494
4777
  if (__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(agentBase) === __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(canonicalDir)) continue;
4495
4778
  const agentSkillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(agentBase, skillName);
4496
- if (exists(agentSkillDir)) installed.push(type);
4779
+ if (fs_exists(agentSkillDir)) installed.push(type);
4497
4780
  }
4498
4781
  return installed;
4499
4782
  }
4500
4783
  /**
4501
4784
  * Get installed skill information from a specific path
4502
4785
  */ getInstalledSkillFromPath(name, skillPath) {
4503
- if (!exists(skillPath)) return null;
4786
+ if (!fs_exists(skillPath)) return null;
4504
4787
  const isLinked = isSymlink(skillPath);
4505
4788
  const locked = this.lockManager.get(name);
4506
4789
  // Read metadata from SKILL.md (sole source per agentskills.io spec)
@@ -4523,10 +4806,10 @@ class RegistryResolver {
4523
4806
  */ getInstalledSkill(name) {
4524
4807
  // Check canonical location first (.agents/skills/)
4525
4808
  const canonicalPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.getCanonicalSkillsDir(), name);
4526
- if (exists(canonicalPath)) return this.getInstalledSkillFromPath(name, canonicalPath);
4809
+ if (fs_exists(canonicalPath)) return this.getInstalledSkillFromPath(name, canonicalPath);
4527
4810
  // Check legacy location (.skills/)
4528
4811
  const legacyPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.getInstallDir(), name);
4529
- if (exists(legacyPath)) return this.getInstalledSkillFromPath(name, legacyPath);
4812
+ if (fs_exists(legacyPath)) return this.getInstalledSkillFromPath(name, legacyPath);
4530
4813
  return null;
4531
4814
  }
4532
4815
  /**
@@ -4937,7 +5220,7 @@ class RegistryResolver {
4937
5220
  const { shortName, version, registryUrl: resolvedRegistryUrl, tarball, parsed: resolvedParsed } = resolved;
4938
5221
  // 2. Check if already installed (skip if --force)
4939
5222
  const skillPath = this.getSkillPath(shortName);
4940
- if (exists(skillPath) && !force) {
5223
+ if (fs_exists(skillPath) && !force) {
4941
5224
  const locked = this.lockManager.get(shortName);
4942
5225
  const lockedVersion = locked?.version;
4943
5226
  // Same version already installed
@@ -4974,8 +5257,8 @@ class RegistryResolver {
4974
5257
  logger_logger["package"](`Installing ${shortName}@${version} from ${resolvedRegistryUrl} to ${targetAgents.length} agent(s)...`);
4975
5258
  // 3. Create temp directory for extraction (clean stale files first)
4976
5259
  const tempDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cache.getCacheDir(), 'registry-temp', `${shortName}-${version}`);
4977
- await remove(tempDir);
4978
- await ensureDir(tempDir);
5260
+ await fs_remove(tempDir);
5261
+ await fs_ensureDir(tempDir);
4979
5262
  try {
4980
5263
  // 4. Extract tarball
4981
5264
  const extractedPath = await this.registryResolver.extract(tarball, tempDir);
@@ -5032,7 +5315,7 @@ class RegistryResolver {
5032
5315
  };
5033
5316
  } finally{
5034
5317
  // Clean up temp directory after installation
5035
- await remove(tempDir);
5318
+ await fs_remove(tempDir);
5036
5319
  }
5037
5320
  }
5038
5321
  // ============================================================================
@@ -5151,8 +5434,8 @@ class RegistryResolver {
5151
5434
  logger_logger["package"](`Installing ${shortName} from ${registryUrl} to ${targetAgents.length} agent(s)...`);
5152
5435
  // Extract tarball to temp directory (clean stale files first)
5153
5436
  const tempDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cache.getCacheDir(), 'registry-temp', `${shortName}-${version}`);
5154
- await remove(tempDir);
5155
- await ensureDir(tempDir);
5437
+ await fs_remove(tempDir);
5438
+ await fs_ensureDir(tempDir);
5156
5439
  try {
5157
5440
  const extractedPath = await this.registryResolver.extract(tarball, tempDir);
5158
5441
  logger_logger.debug(`Extracted to ${extractedPath}`);
@@ -5203,7 +5486,7 @@ class RegistryResolver {
5203
5486
  };
5204
5487
  } finally{
5205
5488
  // Clean up temp directory after installation
5206
- await remove(tempDir);
5489
+ await fs_remove(tempDir);
5207
5490
  }
5208
5491
  }
5209
5492
  /**
@@ -9504,7 +9787,7 @@ async function publishAction(skillPath, options) {
9504
9787
  const validation = validator.validate(absolutePath);
9505
9788
  // 3.5. Content security scan
9506
9789
  const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(absolutePath, 'SKILL.md');
9507
- if (exists(skillMdPath)) {
9790
+ if (fs_exists(skillMdPath)) {
9508
9791
  const scanner = new ContentScanner();
9509
9792
  const scanResult = scanner.scanFile(skillMdPath);
9510
9793
  displayScanFindings(scanResult);