reskill 1.11.0 → 1.11.1
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 +404 -247
- package/dist/core/config-loader.d.ts +4 -0
- package/dist/core/config-loader.d.ts.map +1 -1
- package/dist/core/lock-manager.d.ts +1 -0
- package/dist/core/lock-manager.d.ts.map +1 -1
- package/dist/core/skill-manager.d.ts +43 -0
- package/dist/core/skill-manager.d.ts.map +1 -1
- package/dist/index.js +212 -38
- package/dist/types/index.d.ts +29 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -417,215 +417,6 @@ var external_node_fs_ = __webpack_require__("node:fs");
|
|
|
417
417
|
if (fullPath.startsWith(currentDir)) return `.${fullPath.slice(currentDir.length)}`;
|
|
418
418
|
return fullPath;
|
|
419
419
|
}
|
|
420
|
-
/**
|
|
421
|
-
* Registry-Scope Mapping Utilities
|
|
422
|
-
*
|
|
423
|
-
* Maps registry URLs to their corresponding scopes.
|
|
424
|
-
* Currently hardcoded; TODO: fetch from /api/registry/info in the future.
|
|
425
|
-
*/ /**
|
|
426
|
-
* Public Registry URL
|
|
427
|
-
* Used for installing skills without a scope
|
|
428
|
-
*/ const PUBLIC_REGISTRY = 'https://reskill.info/';
|
|
429
|
-
/**
|
|
430
|
-
* Hardcoded registry to scope mapping
|
|
431
|
-
* TODO: Replace with dynamic fetching from /api/registry/info
|
|
432
|
-
*/ const REGISTRY_SCOPE_MAP = {
|
|
433
|
-
// rush-app (private registry, new)
|
|
434
|
-
'https://rush-test.zhenguanyu.com': '@kanyun-test',
|
|
435
|
-
'https://rush.zhenguanyu.com': '@kanyun',
|
|
436
|
-
// reskill-app (private registry, legacy)
|
|
437
|
-
'https://reskill-test.zhenguanyu.com': '@kanyun-test',
|
|
438
|
-
// Local development
|
|
439
|
-
'http://localhost:3000': '@kanyun-test'
|
|
440
|
-
};
|
|
441
|
-
/**
|
|
442
|
-
* Get the scope for a given registry URL
|
|
443
|
-
*
|
|
444
|
-
* @param registry - Registry URL
|
|
445
|
-
* @returns Scope string (e.g., "@kanyun") or null if not found
|
|
446
|
-
*
|
|
447
|
-
* @example
|
|
448
|
-
* getScopeForRegistry('https://rush-test.zhenguanyu.com') // '@kanyun'
|
|
449
|
-
* getScopeForRegistry('https://unknown.com') // null
|
|
450
|
-
*/ function getScopeForRegistry(registry) {
|
|
451
|
-
if (!registry) return null;
|
|
452
|
-
// Try exact match first
|
|
453
|
-
if (REGISTRY_SCOPE_MAP[registry]) return REGISTRY_SCOPE_MAP[registry];
|
|
454
|
-
// Try with/without trailing slash
|
|
455
|
-
const normalized = registry.endsWith('/') ? registry.slice(0, -1) : `${registry}/`;
|
|
456
|
-
return REGISTRY_SCOPE_MAP[normalized] || null;
|
|
457
|
-
}
|
|
458
|
-
/**
|
|
459
|
-
* Get the registry URL for a given scope (reverse lookup)
|
|
460
|
-
*
|
|
461
|
-
* @param scope - Scope string (with or without @ prefix), e.g., "@kanyun" or "kanyun"
|
|
462
|
-
* @param customRegistries - Optional custom scope-to-registry mapping (from skills.json)
|
|
463
|
-
* @returns Registry URL (with trailing slash) or null if not found
|
|
464
|
-
*
|
|
465
|
-
* @example
|
|
466
|
-
* getRegistryForScope('@kanyun') // 'https://rush-test.zhenguanyu.com/'
|
|
467
|
-
* getRegistryForScope('kanyun') // 'https://rush-test.zhenguanyu.com/'
|
|
468
|
-
* getRegistryForScope('@unknown') // null
|
|
469
|
-
* getRegistryForScope('@mycompany', { '@mycompany': 'https://my.registry.com/' }) // 'https://my.registry.com/'
|
|
470
|
-
*/ function getRegistryForScope(scope, customRegistries) {
|
|
471
|
-
if (!scope) return null;
|
|
472
|
-
// Normalize scope: ensure @ prefix
|
|
473
|
-
const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
|
|
474
|
-
// 1. First check custom scopeRegistries (from skills.json)
|
|
475
|
-
if (customRegistries?.[normalizedScope]) {
|
|
476
|
-
const url = customRegistries[normalizedScope];
|
|
477
|
-
// Normalize trailing slash
|
|
478
|
-
return url.endsWith('/') ? url : `${url}/`;
|
|
479
|
-
}
|
|
480
|
-
// 2. Fall back to hardcoded defaults
|
|
481
|
-
for (const [registry, registryScope] of Object.entries(REGISTRY_SCOPE_MAP))if (registryScope === normalizedScope) // Return URL with trailing slash (normalized format)
|
|
482
|
-
return registry.endsWith('/') ? registry : `${registry}/`;
|
|
483
|
-
return null;
|
|
484
|
-
}
|
|
485
|
-
/**
|
|
486
|
-
* Get the registry URL for a given scope
|
|
487
|
-
*
|
|
488
|
-
* - With scope → lookup private Registry (throws if not found)
|
|
489
|
-
* - Without scope (null/undefined/'') → returns public Registry
|
|
490
|
-
*
|
|
491
|
-
* @param scope - Scope string (with or without @ prefix), null, undefined, or empty string
|
|
492
|
-
* @param customRegistries - Optional custom scope-to-registry mapping (from skills.json)
|
|
493
|
-
* @returns Registry URL (with trailing slash)
|
|
494
|
-
* @throws Error if scope is provided but not found in the registry map
|
|
495
|
-
*
|
|
496
|
-
* @example
|
|
497
|
-
* getRegistryUrl('@kanyun') // 'https://rush-test.zhenguanyu.com/'
|
|
498
|
-
* getRegistryUrl('kanyun') // 'https://rush-test.zhenguanyu.com/'
|
|
499
|
-
* getRegistryUrl(null) // 'https://reskill.info/'
|
|
500
|
-
* getRegistryUrl('') // 'https://reskill.info/'
|
|
501
|
-
* getRegistryUrl('@unknown') // throws Error
|
|
502
|
-
* getRegistryUrl('@mycompany', { '@mycompany': 'https://my.registry.com/' }) // 'https://my.registry.com/'
|
|
503
|
-
*/ function getRegistryUrl(scope, customRegistries) {
|
|
504
|
-
// No scope → return public Registry
|
|
505
|
-
if (!scope) return PUBLIC_REGISTRY;
|
|
506
|
-
// With scope → lookup private Registry
|
|
507
|
-
const registry = getRegistryForScope(scope, customRegistries);
|
|
508
|
-
if (!registry) {
|
|
509
|
-
// Normalize scope for error message
|
|
510
|
-
const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
|
|
511
|
-
throw new Error(`Unknown scope ${normalizedScope}. No registry configured for this scope.`);
|
|
512
|
-
}
|
|
513
|
-
return registry;
|
|
514
|
-
}
|
|
515
|
-
/**
|
|
516
|
-
* Parse a skill name into its components
|
|
517
|
-
*
|
|
518
|
-
* @param skillName - Full or short skill name
|
|
519
|
-
* @returns Parsed skill name with scope and name
|
|
520
|
-
*
|
|
521
|
-
* @example
|
|
522
|
-
* parseSkillName('@kanyun/planning-with-files')
|
|
523
|
-
* // { scope: '@kanyun', name: 'planning-with-files', fullName: '@kanyun/planning-with-files' }
|
|
524
|
-
*
|
|
525
|
-
* parseSkillName('planning-with-files')
|
|
526
|
-
* // { scope: null, name: 'planning-with-files', fullName: 'planning-with-files' }
|
|
527
|
-
*/ function parseSkillName(skillName) {
|
|
528
|
-
// Match @scope/name pattern
|
|
529
|
-
const match = skillName.match(/^(@[^/]+)\/(.+)$/);
|
|
530
|
-
if (match) return {
|
|
531
|
-
scope: match[1],
|
|
532
|
-
name: match[2],
|
|
533
|
-
fullName: skillName
|
|
534
|
-
};
|
|
535
|
-
return {
|
|
536
|
-
scope: null,
|
|
537
|
-
name: skillName,
|
|
538
|
-
fullName: skillName
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
|
-
/**
|
|
542
|
-
* Build full skill name from scope and name
|
|
543
|
-
*
|
|
544
|
-
* @param scope - Scope (with or without @ prefix), or null
|
|
545
|
-
* @param name - Short skill name
|
|
546
|
-
* @returns Full skill name (e.g., "@kanyun/planning-with-files")
|
|
547
|
-
*
|
|
548
|
-
* @example
|
|
549
|
-
* buildFullSkillName('@kanyun', 'planning-with-files') // '@kanyun/planning-with-files'
|
|
550
|
-
* buildFullSkillName('kanyun', 'my-skill') // '@kanyun/my-skill'
|
|
551
|
-
* buildFullSkillName(null, 'my-skill') // 'my-skill'
|
|
552
|
-
*/ function buildFullSkillName(scope, name) {
|
|
553
|
-
if (!scope) return name;
|
|
554
|
-
// Ensure scope starts with @
|
|
555
|
-
const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
|
|
556
|
-
return `${normalizedScope}/${name}`;
|
|
557
|
-
}
|
|
558
|
-
/**
|
|
559
|
-
* Get short name from a skill name (removes scope if present)
|
|
560
|
-
*
|
|
561
|
-
* @param skillName - Full or short skill name
|
|
562
|
-
* @returns Short name without scope
|
|
563
|
-
*
|
|
564
|
-
* @example
|
|
565
|
-
* getShortName('@kanyun/planning-with-files') // 'planning-with-files'
|
|
566
|
-
* getShortName('planning-with-files') // 'planning-with-files'
|
|
567
|
-
*/ function getShortName(skillName) {
|
|
568
|
-
return parseSkillName(skillName).name;
|
|
569
|
-
}
|
|
570
|
-
/**
|
|
571
|
-
* Parse a skill identifier into its components (with version support)
|
|
572
|
-
*
|
|
573
|
-
* Supports both private registry (with @scope) and public registry (without scope) formats.
|
|
574
|
-
*
|
|
575
|
-
* @param identifier - Skill identifier string
|
|
576
|
-
* @returns Parsed skill identifier with scope, name, version, and fullName
|
|
577
|
-
* @throws Error if identifier is invalid
|
|
578
|
-
*
|
|
579
|
-
* @example
|
|
580
|
-
* // Private registry
|
|
581
|
-
* parseSkillIdentifier('@kanyun/planning-with-files')
|
|
582
|
-
* // { scope: '@kanyun', name: 'planning-with-files', version: undefined, fullName: '@kanyun/planning-with-files' }
|
|
583
|
-
*
|
|
584
|
-
* parseSkillIdentifier('@kanyun/skill@2.4.5')
|
|
585
|
-
* // { scope: '@kanyun', name: 'skill', version: '2.4.5', fullName: '@kanyun/skill' }
|
|
586
|
-
*
|
|
587
|
-
* // Public registry
|
|
588
|
-
* parseSkillIdentifier('planning-with-files')
|
|
589
|
-
* // { scope: null, name: 'planning-with-files', version: undefined, fullName: 'planning-with-files' }
|
|
590
|
-
*
|
|
591
|
-
* parseSkillIdentifier('skill@latest')
|
|
592
|
-
* // { scope: null, name: 'skill', version: 'latest', fullName: 'skill' }
|
|
593
|
-
*/ function parseSkillIdentifier(identifier) {
|
|
594
|
-
const trimmed = identifier.trim();
|
|
595
|
-
// Empty string or whitespace only
|
|
596
|
-
if (!trimmed) throw new Error('Invalid skill identifier: empty string');
|
|
597
|
-
// Starting with @@ is invalid
|
|
598
|
-
if (trimmed.startsWith('@@')) throw new Error('Invalid skill identifier: invalid scope format');
|
|
599
|
-
// Bare @ is invalid
|
|
600
|
-
if ('@' === trimmed) throw new Error('Invalid skill identifier: missing scope and name');
|
|
601
|
-
// Scoped format: @scope/name[@version]
|
|
602
|
-
if (trimmed.startsWith('@')) {
|
|
603
|
-
// Regex: @scope/name[@version]
|
|
604
|
-
// scope: starts with @, followed by alphanumeric, hyphens, underscores
|
|
605
|
-
// name: alphanumeric, hyphens, underscores
|
|
606
|
-
// version: optional, @ followed by any non-empty string
|
|
607
|
-
const scopedMatch = trimmed.match(/^(@[\w-]+)\/([\w-]+)(?:@(.+))?$/);
|
|
608
|
-
if (!scopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
|
|
609
|
-
const [, scope, name, version] = scopedMatch;
|
|
610
|
-
return {
|
|
611
|
-
scope,
|
|
612
|
-
name,
|
|
613
|
-
version: version || void 0,
|
|
614
|
-
fullName: `${scope}/${name}`
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
// Unscoped format: name[@version] (public registry)
|
|
618
|
-
// name must not contain / (otherwise it might be a git shorthand)
|
|
619
|
-
const unscopedMatch = trimmed.match(/^([\w-]+)(?:@(.+))?$/);
|
|
620
|
-
if (!unscopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
|
|
621
|
-
const [, name, version] = unscopedMatch;
|
|
622
|
-
return {
|
|
623
|
-
scope: null,
|
|
624
|
-
name,
|
|
625
|
-
version: version || void 0,
|
|
626
|
-
fullName: name
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
420
|
const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEBPACK_EXTERNAL_MODULE_node_child_process__.exec);
|
|
630
421
|
/**
|
|
631
422
|
* Git utilities
|
|
@@ -891,6 +682,215 @@ const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEB
|
|
|
891
682
|
}
|
|
892
683
|
return null;
|
|
893
684
|
}
|
|
685
|
+
/**
|
|
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
|
+
};
|
|
706
|
+
/**
|
|
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;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
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;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
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;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
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
|
+
};
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
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}`;
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
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;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
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;
|
|
887
|
+
return {
|
|
888
|
+
scope: null,
|
|
889
|
+
name,
|
|
890
|
+
version: version || void 0,
|
|
891
|
+
fullName: name
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
894
|
/**
|
|
895
895
|
* HTTP utilities for downloading and extracting skill archives
|
|
896
896
|
*/ /**
|
|
@@ -2250,6 +2250,15 @@ ${CURSOR_BRIDGE_MARKER}
|
|
|
2250
2250
|
// Registry Management
|
|
2251
2251
|
// ==========================================================================
|
|
2252
2252
|
/**
|
|
2253
|
+
* Get all configured registries (custom + default).
|
|
2254
|
+
*/ getRegistries() {
|
|
2255
|
+
const config = this.getConfigOrDefault();
|
|
2256
|
+
return {
|
|
2257
|
+
...DEFAULT_REGISTRIES,
|
|
2258
|
+
...config.registries
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
/**
|
|
2253
2262
|
* Get registry URL by name
|
|
2254
2263
|
*
|
|
2255
2264
|
* Resolution order:
|
|
@@ -3091,6 +3100,8 @@ ${CURSOR_BRIDGE_MARKER}
|
|
|
3091
3100
|
commit: options.commit,
|
|
3092
3101
|
installedAt: new Date().toISOString()
|
|
3093
3102
|
};
|
|
3103
|
+
// Only persist registry URL for registry-sourced skills
|
|
3104
|
+
if (options.registry) lockedSkill.registry = options.registry;
|
|
3094
3105
|
this.set(name, lockedSkill);
|
|
3095
3106
|
return lockedSkill;
|
|
3096
3107
|
}
|
|
@@ -4005,8 +4016,11 @@ class RegistryResolver {
|
|
|
4005
4016
|
*/ async installAll(options = {}) {
|
|
4006
4017
|
const skills = this.config.getSkills();
|
|
4007
4018
|
const installed = [];
|
|
4019
|
+
const targetAgents = await detectInstalledAgents();
|
|
4008
4020
|
for (const [name, ref] of Object.entries(skills))try {
|
|
4009
|
-
|
|
4021
|
+
// Use installToAgents (not install) to correctly route registry refs
|
|
4022
|
+
// resolveRegistryUrl inside handles lock file / registries probe fallback
|
|
4023
|
+
const { skill } = await this.installToAgents(ref, targetAgents, {
|
|
4010
4024
|
...options,
|
|
4011
4025
|
save: false
|
|
4012
4026
|
});
|
|
@@ -4052,6 +4066,7 @@ class RegistryResolver {
|
|
|
4052
4066
|
* Update skill
|
|
4053
4067
|
*/ async update(name) {
|
|
4054
4068
|
const updated = [];
|
|
4069
|
+
const targetAgents = await detectInstalledAgents();
|
|
4055
4070
|
if (name) {
|
|
4056
4071
|
// Update single skill
|
|
4057
4072
|
const ref = this.config.getSkillRef(name);
|
|
@@ -4059,9 +4074,10 @@ class RegistryResolver {
|
|
|
4059
4074
|
logger_logger.error(`Skill ${name} not found in skills.json`);
|
|
4060
4075
|
return [];
|
|
4061
4076
|
}
|
|
4062
|
-
// Check if update is needed (skip
|
|
4063
|
-
|
|
4064
|
-
logger_logger.info(`${name} is from
|
|
4077
|
+
// Check if update is needed (skip for HTTP and Registry - always re-download/reinstall)
|
|
4078
|
+
const isRegistry = this.isRegistrySource(ref);
|
|
4079
|
+
if (isRegistry) logger_logger.info(`${name} is from registry, re-installing...`);
|
|
4080
|
+
else if (this.isHttpSource(ref)) logger_logger.info(`${name} is from HTTP source, re-downloading...`);
|
|
4065
4081
|
else {
|
|
4066
4082
|
const resolved = await this.resolver.resolve(ref);
|
|
4067
4083
|
const remoteCommit = await this.cache.getRemoteCommit(resolved.repoUrl, resolved.ref);
|
|
@@ -4070,7 +4086,7 @@ class RegistryResolver {
|
|
|
4070
4086
|
return [];
|
|
4071
4087
|
}
|
|
4072
4088
|
}
|
|
4073
|
-
const skill = await this.
|
|
4089
|
+
const skill = await this.installForUpdate(ref, targetAgents, {
|
|
4074
4090
|
force: true,
|
|
4075
4091
|
save: false
|
|
4076
4092
|
});
|
|
@@ -4079,8 +4095,9 @@ class RegistryResolver {
|
|
|
4079
4095
|
// Update all
|
|
4080
4096
|
const skills = this.config.getSkills();
|
|
4081
4097
|
for (const [skillName, ref] of Object.entries(skills))try {
|
|
4082
|
-
// Check if update is needed (skip
|
|
4083
|
-
if (this.
|
|
4098
|
+
// Check if update is needed (skip for HTTP and Registry)
|
|
4099
|
+
if (this.isRegistrySource(ref)) logger_logger.info(`${skillName} is from registry, re-installing...`);
|
|
4100
|
+
else if (this.isHttpSource(ref)) logger_logger.info(`${skillName} is from HTTP source, re-downloading...`);
|
|
4084
4101
|
else {
|
|
4085
4102
|
const resolved = await this.resolver.resolve(ref);
|
|
4086
4103
|
const remoteCommit = await this.cache.getRemoteCommit(resolved.repoUrl, resolved.ref);
|
|
@@ -4089,7 +4106,7 @@ class RegistryResolver {
|
|
|
4089
4106
|
continue;
|
|
4090
4107
|
}
|
|
4091
4108
|
}
|
|
4092
|
-
const skill = await this.
|
|
4109
|
+
const skill = await this.installForUpdate(ref, targetAgents, {
|
|
4093
4110
|
force: true,
|
|
4094
4111
|
save: false
|
|
4095
4112
|
});
|
|
@@ -4101,6 +4118,70 @@ class RegistryResolver {
|
|
|
4101
4118
|
return updated;
|
|
4102
4119
|
}
|
|
4103
4120
|
/**
|
|
4121
|
+
* Resolve registry URL for a skill reference.
|
|
4122
|
+
*
|
|
4123
|
+
* Resolution order:
|
|
4124
|
+
* 1. Explicit CLI override (options.registry)
|
|
4125
|
+
* 2. Scoped skills → getRegistryUrl(scope)
|
|
4126
|
+
* 3. Unscoped skills → lock file registry (O(1), no network)
|
|
4127
|
+
* 4. Unscoped skills → probe skills.json registries (non-git-host, network)
|
|
4128
|
+
* 5. Default → PUBLIC_REGISTRY
|
|
4129
|
+
*/ async resolveRegistryUrl(ref, explicitRegistry) {
|
|
4130
|
+
if (explicitRegistry) return explicitRegistry;
|
|
4131
|
+
const parsed = parseSkillIdentifier(ref);
|
|
4132
|
+
if (parsed.scope) return getRegistryUrl(parsed.scope);
|
|
4133
|
+
// Fast path: lock file has registry URL
|
|
4134
|
+
const locked = this.lockManager.get(parsed.name);
|
|
4135
|
+
if (locked?.registry) return locked.registry;
|
|
4136
|
+
// Slow path: probe configured registries (skip git hosts)
|
|
4137
|
+
const registries = this.config.getRegistries();
|
|
4138
|
+
for (const [name, url] of Object.entries(registries))if (!this.isGitHostRegistry(name, url)) try {
|
|
4139
|
+
const client = new RegistryClient({
|
|
4140
|
+
registry: url
|
|
4141
|
+
});
|
|
4142
|
+
await client.getSkillInfo(parsed.fullName);
|
|
4143
|
+
return url; // Found it
|
|
4144
|
+
} catch {}
|
|
4145
|
+
return getRegistryUrl(null); // PUBLIC_REGISTRY
|
|
4146
|
+
}
|
|
4147
|
+
/**
|
|
4148
|
+
* Check if a registry entry is a git host (github, gitlab, etc.)
|
|
4149
|
+
* Git hosts are not skill registries and should be skipped during probe.
|
|
4150
|
+
*/ isGitHostRegistry(name, url) {
|
|
4151
|
+
const gitHostNames = new Set(Object.keys(DEFAULT_REGISTRIES));
|
|
4152
|
+
if (gitHostNames.has(name)) return true;
|
|
4153
|
+
const gitHostPatterns = [
|
|
4154
|
+
'github.com',
|
|
4155
|
+
'gitlab.com'
|
|
4156
|
+
];
|
|
4157
|
+
const normalizedUrl = url.toLowerCase();
|
|
4158
|
+
return gitHostPatterns.some((pattern)=>normalizedUrl.includes(pattern));
|
|
4159
|
+
}
|
|
4160
|
+
/**
|
|
4161
|
+
* Derive a registry name from a URL for storing in skills.json.registries.
|
|
4162
|
+
* Returns null for git hosts (already in DEFAULT_REGISTRIES).
|
|
4163
|
+
*/ deriveRegistryName(registryUrl) {
|
|
4164
|
+
// Skip git hosts
|
|
4165
|
+
if (this.isGitHostRegistry('', registryUrl)) return null;
|
|
4166
|
+
// Try known scope mapping first (e.g., kanyun registries)
|
|
4167
|
+
const scope = getScopeForRegistry(registryUrl);
|
|
4168
|
+
if (scope) return scope;
|
|
4169
|
+
// Use hostname as registry name
|
|
4170
|
+
try {
|
|
4171
|
+
const url = new URL(registryUrl);
|
|
4172
|
+
return url.hostname;
|
|
4173
|
+
} catch {
|
|
4174
|
+
return null;
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
/**
|
|
4178
|
+
* Install skill for update flow. Uses installToAgents so registry refs are
|
|
4179
|
+
* routed correctly (Registry > HTTP > Git).
|
|
4180
|
+
*/ async installForUpdate(ref, targetAgents, options) {
|
|
4181
|
+
const { skill } = await this.installToAgents(ref, targetAgents, options);
|
|
4182
|
+
return skill;
|
|
4183
|
+
}
|
|
4184
|
+
/**
|
|
4104
4185
|
* List installed skills
|
|
4105
4186
|
*
|
|
4106
4187
|
* Checks both canonical (.agents/skills/) and legacy (.skills/) locations.
|
|
@@ -4367,7 +4448,7 @@ class RegistryResolver {
|
|
|
4367
4448
|
/**
|
|
4368
4449
|
* Install skill from Git to multiple agents
|
|
4369
4450
|
*/ async installToAgentsFromGit(ref, targetAgents, options = {}) {
|
|
4370
|
-
const { save = true, mode = 'symlink' } = options;
|
|
4451
|
+
const { save = true, mode = 'symlink', registryContext } = options;
|
|
4371
4452
|
// Parse reference
|
|
4372
4453
|
const resolved = await this.resolver.resolve(ref);
|
|
4373
4454
|
const { parsed, repoUrl } = resolved;
|
|
@@ -4385,7 +4466,8 @@ class RegistryResolver {
|
|
|
4385
4466
|
const sourcePath = this.resolveSourcePath(cachePath, parsed);
|
|
4386
4467
|
// Get the real skill name from SKILL.md in cache
|
|
4387
4468
|
const metadata = this.getSkillMetadataFromDir(sourcePath);
|
|
4388
|
-
|
|
4469
|
+
// Priority: registryContext name > SKILL.md name > fallback from Git URL
|
|
4470
|
+
const skillName = registryContext?.skillName ?? metadata?.name ?? fallbackName;
|
|
4389
4471
|
const semanticVersion = metadata?.version ?? gitRef;
|
|
4390
4472
|
logger_logger["package"](`Installing ${skillName}@${gitRef} to ${targetAgents.length} agent(s)...`);
|
|
4391
4473
|
// Create Installer with custom installDir from config
|
|
@@ -4400,19 +4482,22 @@ class RegistryResolver {
|
|
|
4400
4482
|
mode: mode
|
|
4401
4483
|
});
|
|
4402
4484
|
// Update lock file (project mode only)
|
|
4403
|
-
if (!this.isGlobal)
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4485
|
+
if (!this.isGlobal) {
|
|
4486
|
+
const lockSource = registryContext?.lockSource ?? `${parsed.registry}:${parsed.owner}/${parsed.repo}${parsed.subPath ? `/${parsed.subPath}` : ''}`;
|
|
4487
|
+
this.lockManager.lockSkill(skillName, {
|
|
4488
|
+
source: lockSource,
|
|
4489
|
+
version: semanticVersion,
|
|
4490
|
+
ref: gitRef,
|
|
4491
|
+
resolved: repoUrl,
|
|
4492
|
+
commit: cacheResult.commit,
|
|
4493
|
+
registry: registryContext?.registryUrl
|
|
4494
|
+
});
|
|
4495
|
+
}
|
|
4410
4496
|
// Update skills.json (project mode only)
|
|
4411
4497
|
if (!this.isGlobal && save) {
|
|
4412
4498
|
this.config.ensureExists();
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
this.config.addSkill(skillName, normalizedRef);
|
|
4499
|
+
const configRef = registryContext?.configRef ?? this.config.normalizeSkillRef(ref);
|
|
4500
|
+
this.config.addSkill(skillName, configRef);
|
|
4416
4501
|
}
|
|
4417
4502
|
// Count results
|
|
4418
4503
|
const successCount = Array.from(results.values()).filter((r)=>r.success).length;
|
|
@@ -4435,7 +4520,7 @@ class RegistryResolver {
|
|
|
4435
4520
|
/**
|
|
4436
4521
|
* Install skill from HTTP/OSS to multiple agents
|
|
4437
4522
|
*/ async installToAgentsFromHttp(ref, targetAgents, options = {}) {
|
|
4438
|
-
const { save = true, mode = 'symlink' } = options;
|
|
4523
|
+
const { save = true, mode = 'symlink', registryContext } = options;
|
|
4439
4524
|
// Parse HTTP reference
|
|
4440
4525
|
const resolved = await this.httpResolver.resolve(ref);
|
|
4441
4526
|
const { parsed, repoUrl, httpInfo } = resolved;
|
|
@@ -4452,7 +4537,8 @@ class RegistryResolver {
|
|
|
4452
4537
|
const sourcePath = this.cache.getCachePath(parsed, version);
|
|
4453
4538
|
// Get the real skill name from SKILL.md in cache
|
|
4454
4539
|
const metadata = this.getSkillMetadataFromDir(sourcePath);
|
|
4455
|
-
|
|
4540
|
+
// Priority: registryContext name > SKILL.md name > fallback from URL
|
|
4541
|
+
const skillName = registryContext?.skillName ?? metadata?.name ?? fallbackName;
|
|
4456
4542
|
const semanticVersion = metadata?.version ?? version;
|
|
4457
4543
|
logger_logger["package"](`Installing ${skillName}@${version} from ${httpInfo.host} to ${targetAgents.length} agent(s)...`);
|
|
4458
4544
|
// Create Installer with custom installDir from config
|
|
@@ -4467,17 +4553,22 @@ class RegistryResolver {
|
|
|
4467
4553
|
mode: mode
|
|
4468
4554
|
});
|
|
4469
4555
|
// Update lock file (project mode only)
|
|
4470
|
-
if (!this.isGlobal)
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4556
|
+
if (!this.isGlobal) {
|
|
4557
|
+
const lockSource = registryContext?.lockSource ?? `http:${httpInfo.host}/${skillName}`;
|
|
4558
|
+
this.lockManager.lockSkill(skillName, {
|
|
4559
|
+
source: lockSource,
|
|
4560
|
+
version: semanticVersion,
|
|
4561
|
+
ref: version,
|
|
4562
|
+
resolved: repoUrl,
|
|
4563
|
+
commit: cacheResult.commit,
|
|
4564
|
+
registry: registryContext?.registryUrl
|
|
4565
|
+
});
|
|
4566
|
+
}
|
|
4477
4567
|
// Update skills.json (project mode only)
|
|
4478
4568
|
if (!this.isGlobal && save) {
|
|
4479
4569
|
this.config.ensureExists();
|
|
4480
|
-
|
|
4570
|
+
const configRef = registryContext?.configRef ?? ref;
|
|
4571
|
+
this.config.addSkill(skillName, configRef);
|
|
4481
4572
|
}
|
|
4482
4573
|
// Count results
|
|
4483
4574
|
const successCount = Array.from(results.values()).filter((r)=>r.success).length;
|
|
@@ -4508,7 +4599,7 @@ class RegistryResolver {
|
|
|
4508
4599
|
const { force = false, save = true, mode = 'symlink' } = options;
|
|
4509
4600
|
// Parse skill identifier and resolve registry URL once (single source of truth)
|
|
4510
4601
|
const parsed = parseSkillIdentifier(ref);
|
|
4511
|
-
const registryUrl = options.registry
|
|
4602
|
+
const registryUrl = await this.resolveRegistryUrl(ref, options.registry);
|
|
4512
4603
|
const client = new RegistryClient({
|
|
4513
4604
|
registry: registryUrl
|
|
4514
4605
|
});
|
|
@@ -4595,13 +4686,22 @@ class RegistryResolver {
|
|
|
4595
4686
|
version,
|
|
4596
4687
|
ref: version,
|
|
4597
4688
|
resolved: resolvedRegistryUrl,
|
|
4598
|
-
commit: resolved.integrity
|
|
4689
|
+
commit: resolved.integrity,
|
|
4690
|
+
registry: resolvedRegistryUrl
|
|
4599
4691
|
});
|
|
4600
4692
|
// 8. Update skills.json (project mode only)
|
|
4601
4693
|
if (!this.isGlobal && save) {
|
|
4602
4694
|
this.config.ensureExists();
|
|
4603
4695
|
// Save with full name for registry skills
|
|
4604
4696
|
this.config.addSkill(shortName, ref);
|
|
4697
|
+
// Save custom registry to skills.json.registries (for reinstall without lock file)
|
|
4698
|
+
if (options.registry) {
|
|
4699
|
+
const registryName = this.deriveRegistryName(options.registry);
|
|
4700
|
+
if (registryName) {
|
|
4701
|
+
this.config.addRegistry(registryName, options.registry);
|
|
4702
|
+
this.config.save();
|
|
4703
|
+
}
|
|
4704
|
+
}
|
|
4605
4705
|
}
|
|
4606
4706
|
// 9. Count results and log
|
|
4607
4707
|
const successCount = Array.from(results.values()).filter((r)=>r.success).length;
|
|
@@ -4641,16 +4741,41 @@ class RegistryResolver {
|
|
|
4641
4741
|
if (parsed.version && 'latest' !== parsed.version) throw new Error(`Version specifier not supported for web-published skills.\n'${parsed.fullName}' was published via web and does not support versioning.\nUse: reskill install ${parsed.fullName}`);
|
|
4642
4742
|
if (!source_url) throw new Error(`Missing source_url for web-published skill: ${parsed.fullName}`);
|
|
4643
4743
|
logger_logger["package"](`Installing ${parsed.fullName} from ${source_type} source...`);
|
|
4744
|
+
// Build registry context so downstream methods use the registry name
|
|
4745
|
+
// instead of deriving from the source URL (e.g., Git repo name)
|
|
4746
|
+
const shortName = getShortName(parsed.fullName);
|
|
4747
|
+
const registryUrl = await this.resolveRegistryUrl(parsed.fullName, options.registry);
|
|
4748
|
+
const registryContext = {
|
|
4749
|
+
skillName: shortName,
|
|
4750
|
+
configRef: parsed.fullName,
|
|
4751
|
+
lockSource: `registry:${parsed.fullName}`,
|
|
4752
|
+
registryUrl
|
|
4753
|
+
};
|
|
4754
|
+
const optionsWithContext = {
|
|
4755
|
+
...options,
|
|
4756
|
+
registryContext
|
|
4757
|
+
};
|
|
4758
|
+
// Save custom registry to skills.json.registries (for reinstall without lock file)
|
|
4759
|
+
if (!this.isGlobal && options.registry) {
|
|
4760
|
+
const registryName = this.deriveRegistryName(options.registry);
|
|
4761
|
+
if (registryName) {
|
|
4762
|
+
this.config.ensureExists();
|
|
4763
|
+
this.config.load();
|
|
4764
|
+
this.config.addRegistry(registryName, options.registry);
|
|
4765
|
+
this.config.save();
|
|
4766
|
+
}
|
|
4767
|
+
}
|
|
4644
4768
|
switch(source_type){
|
|
4645
4769
|
case 'github':
|
|
4646
4770
|
case 'gitlab':
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4771
|
+
{
|
|
4772
|
+
const gitRef = this.buildGitRefForWebPublished(source_type, source_url, skillInfo.skill_path, parsed);
|
|
4773
|
+
return this.installToAgentsFromGit(gitRef, targetAgents, optionsWithContext);
|
|
4774
|
+
}
|
|
4650
4775
|
case 'oss_url':
|
|
4651
4776
|
case 'custom_url':
|
|
4652
4777
|
// Direct download URL
|
|
4653
|
-
return this.installToAgentsFromHttp(source_url, targetAgents,
|
|
4778
|
+
return this.installToAgentsFromHttp(source_url, targetAgents, optionsWithContext);
|
|
4654
4779
|
case 'local':
|
|
4655
4780
|
// Download tarball via Registry API
|
|
4656
4781
|
return this.installFromRegistryLocal(parsed, targetAgents, options);
|
|
@@ -4659,13 +4784,36 @@ class RegistryResolver {
|
|
|
4659
4784
|
}
|
|
4660
4785
|
}
|
|
4661
4786
|
/**
|
|
4787
|
+
* Build a Git ref for web-published github/gitlab skills.
|
|
4788
|
+
*
|
|
4789
|
+
* When `skillPath` is provided (multi-skill repo), constructs a shorthand ref
|
|
4790
|
+
* like `github:owner/repo/skills/my-skill` so that only the sub-directory is
|
|
4791
|
+
* cached and installed.
|
|
4792
|
+
*
|
|
4793
|
+
* Fallback: if `parseGitUrl` fails (non-standard URL), appends `#shortName`
|
|
4794
|
+
* as a skill-name selector. The `#` fragment is extracted by
|
|
4795
|
+
* `GitResolver.parseRef()` as `parsed.skillName`, then matched against
|
|
4796
|
+
* SKILL.md `name` fields via `resolveSourcePath` / `discoverSkillsInDir`.
|
|
4797
|
+
* This differs from the subPath approach (directory-based) but works because
|
|
4798
|
+
* skill names typically match their directory basenames.
|
|
4799
|
+
*
|
|
4800
|
+
* Returns the raw `sourceUrl` when no `skillPath` is available.
|
|
4801
|
+
*/ buildGitRefForWebPublished(sourceType, sourceUrl, skillPath, parsed) {
|
|
4802
|
+
if (!skillPath) return sourceUrl;
|
|
4803
|
+
const urlParsed = parseGitUrl(sourceUrl);
|
|
4804
|
+
if (urlParsed) return `${sourceType}:${urlParsed.owner}/${urlParsed.repo}/${skillPath}`;
|
|
4805
|
+
const shortName = getShortName(parsed.fullName);
|
|
4806
|
+
if (shortName) return `${sourceUrl}#${shortName}`;
|
|
4807
|
+
return sourceUrl;
|
|
4808
|
+
}
|
|
4809
|
+
/**
|
|
4662
4810
|
* Install a skill published via "local folder" mode.
|
|
4663
4811
|
*
|
|
4664
4812
|
* Downloads tarball via RegistryClient (handles 302 redirects to signed OSS URLs),
|
|
4665
4813
|
* then extracts and installs using the same flow as registry source_type.
|
|
4666
4814
|
*/ async installFromRegistryLocal(parsed, targetAgents, options = {}) {
|
|
4667
4815
|
const { save = true, mode = 'symlink' } = options;
|
|
4668
|
-
const registryUrl =
|
|
4816
|
+
const registryUrl = await this.resolveRegistryUrl(parsed.fullName, options.registry);
|
|
4669
4817
|
const shortName = getShortName(parsed.fullName);
|
|
4670
4818
|
const version = 'latest';
|
|
4671
4819
|
// Download tarball via RegistryClient (handles auth + 302 redirect to signed URL)
|
|
@@ -4701,12 +4849,21 @@ class RegistryResolver {
|
|
|
4701
4849
|
version: semanticVersion,
|
|
4702
4850
|
ref: version,
|
|
4703
4851
|
resolved: registryUrl,
|
|
4704
|
-
commit: ''
|
|
4852
|
+
commit: '',
|
|
4853
|
+
registry: registryUrl
|
|
4705
4854
|
});
|
|
4706
4855
|
// Update skills.json (project mode only)
|
|
4707
4856
|
if (!this.isGlobal && save) {
|
|
4708
4857
|
this.config.ensureExists();
|
|
4709
4858
|
this.config.addSkill(skillName, parsed.fullName);
|
|
4859
|
+
// Save custom registry to skills.json.registries (for reinstall without lock file)
|
|
4860
|
+
if (options.registry) {
|
|
4861
|
+
const registryName = this.deriveRegistryName(options.registry);
|
|
4862
|
+
if (registryName) {
|
|
4863
|
+
this.config.addRegistry(registryName, options.registry);
|
|
4864
|
+
this.config.save();
|
|
4865
|
+
}
|
|
4866
|
+
}
|
|
4710
4867
|
}
|
|
4711
4868
|
return {
|
|
4712
4869
|
skill: {
|