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 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
- const skill = await this.install(ref, {
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 check for HTTP sources - always re-download)
4063
- if (this.isHttpSource(ref)) // For HTTP sources, log that we're re-downloading
4064
- logger_logger.info(`${name} is from HTTP source, re-downloading...`);
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.install(ref, {
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 check for HTTP sources)
4083
- if (this.isHttpSource(ref)) logger_logger.info(`${skillName} is from HTTP source, re-downloading...`);
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.install(ref, {
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
- const skillName = metadata?.name ?? fallbackName;
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) this.lockManager.lockSkill(skillName, {
4404
- source: `${parsed.registry}:${parsed.owner}/${parsed.repo}${parsed.subPath ? `/${parsed.subPath}` : ''}`,
4405
- version: semanticVersion,
4406
- ref: gitRef,
4407
- resolved: repoUrl,
4408
- commit: cacheResult.commit
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
- // Normalize the reference to use registry shorthand if possible
4414
- const normalizedRef = this.config.normalizeSkillRef(ref);
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
- const skillName = metadata?.name ?? fallbackName;
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) this.lockManager.lockSkill(skillName, {
4471
- source: `http:${httpInfo.host}/${skillName}`,
4472
- version: semanticVersion,
4473
- ref: version,
4474
- resolved: repoUrl,
4475
- commit: cacheResult.commit
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
- this.config.addSkill(skillName, ref);
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 || getRegistryUrl(parsed.scope);
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
- // source_url is a full Git URL (includes ref and path)
4648
- // Reuse existing Git installation logic
4649
- return this.installToAgentsFromGit(source_url, targetAgents, options);
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, options);
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 = options.registry || getRegistryUrl(parsed.scope);
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: {