reskill 1.7.0 → 1.8.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -47
- package/README.zh-CN.md +47 -47
- package/dist/cli/commands/__integration__/helpers.d.ts +16 -0
- package/dist/cli/commands/__integration__/helpers.d.ts.map +1 -1
- package/dist/cli/commands/find.d.ts +27 -0
- package/dist/cli/commands/find.d.ts.map +1 -0
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/install.d.ts.map +1 -1
- package/dist/cli/commands/login.d.ts.map +1 -1
- package/dist/cli/commands/whoami.d.ts.map +1 -1
- package/dist/cli/index.js +1104 -466
- package/dist/core/git-resolver.d.ts.map +1 -1
- package/dist/core/http-resolver.d.ts +4 -2
- package/dist/core/http-resolver.d.ts.map +1 -1
- package/dist/core/installer.d.ts +25 -0
- package/dist/core/installer.d.ts.map +1 -1
- package/dist/core/registry-client.d.ts +66 -11
- package/dist/core/registry-client.d.ts.map +1 -1
- package/dist/core/registry-resolver.d.ts +2 -1
- package/dist/core/registry-resolver.d.ts.map +1 -1
- package/dist/core/skill-manager.d.ts +35 -8
- package/dist/core/skill-manager.d.ts.map +1 -1
- package/dist/core/skill-parser.d.ts +32 -0
- package/dist/core/skill-parser.d.ts.map +1 -1
- package/dist/index.js +929 -517
- package/dist/types/index.d.ts +16 -14
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/registry-scope.d.ts +8 -20
- package/dist/utils/registry-scope.d.ts.map +1 -1
- package/dist/utils/registry.d.ts +27 -1
- package/dist/utils/registry.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -818,6 +818,384 @@ const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEB
|
|
|
818
818
|
if (external_node_fs_.existsSync(tempArchive)) external_node_fs_.unlinkSync(tempArchive);
|
|
819
819
|
}
|
|
820
820
|
}
|
|
821
|
+
/**
|
|
822
|
+
* Skill Parser - SKILL.md parser
|
|
823
|
+
*
|
|
824
|
+
* Following agentskills.io specification: https://agentskills.io/specification
|
|
825
|
+
*
|
|
826
|
+
* SKILL.md format requirements:
|
|
827
|
+
* - YAML frontmatter containing name and description (required)
|
|
828
|
+
* - name: max 64 characters, lowercase letters, numbers, hyphens
|
|
829
|
+
* - description: max 1024 characters
|
|
830
|
+
* - Optional fields: license, compatibility, metadata, allowed-tools
|
|
831
|
+
*/ /**
|
|
832
|
+
* Skill validation error
|
|
833
|
+
*/ class SkillValidationError extends Error {
|
|
834
|
+
field;
|
|
835
|
+
constructor(message, field){
|
|
836
|
+
super(message), this.field = field;
|
|
837
|
+
this.name = 'SkillValidationError';
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Simple YAML frontmatter parser
|
|
842
|
+
* Parses --- delimited YAML header
|
|
843
|
+
*
|
|
844
|
+
* Supports:
|
|
845
|
+
* - Basic key: value pairs
|
|
846
|
+
* - Multiline strings (| and >)
|
|
847
|
+
* - Nested objects (one level deep, for metadata field)
|
|
848
|
+
*/ function parseFrontmatter(content) {
|
|
849
|
+
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
850
|
+
const match = content.match(frontmatterRegex);
|
|
851
|
+
if (!match) return {
|
|
852
|
+
data: {},
|
|
853
|
+
content
|
|
854
|
+
};
|
|
855
|
+
const yamlContent = match[1];
|
|
856
|
+
const markdownContent = match[2];
|
|
857
|
+
// Simple YAML parsing (supports basic key: value, one level of nesting,
|
|
858
|
+
// block scalars (| and >), and plain scalars spanning multiple indented lines)
|
|
859
|
+
const data = {};
|
|
860
|
+
const lines = yamlContent.split('\n');
|
|
861
|
+
let currentKey = '';
|
|
862
|
+
let currentValue = '';
|
|
863
|
+
let inMultiline = false;
|
|
864
|
+
let inNestedObject = false;
|
|
865
|
+
let inPlainScalar = false;
|
|
866
|
+
let nestedObject = {};
|
|
867
|
+
/**
|
|
868
|
+
* Save the current key/value accumulated so far, then reset state.
|
|
869
|
+
*/ function flushCurrent() {
|
|
870
|
+
if (!currentKey) return;
|
|
871
|
+
if (inNestedObject) {
|
|
872
|
+
data[currentKey] = nestedObject;
|
|
873
|
+
nestedObject = {};
|
|
874
|
+
inNestedObject = false;
|
|
875
|
+
} else if (inPlainScalar || inMultiline) {
|
|
876
|
+
data[currentKey] = currentValue.trim();
|
|
877
|
+
inPlainScalar = false;
|
|
878
|
+
inMultiline = false;
|
|
879
|
+
} else data[currentKey] = parseYamlValue(currentValue.trim());
|
|
880
|
+
currentKey = '';
|
|
881
|
+
currentValue = '';
|
|
882
|
+
}
|
|
883
|
+
for (const line of lines){
|
|
884
|
+
const trimmedLine = line.trim();
|
|
885
|
+
if (!trimmedLine || trimmedLine.startsWith('#')) continue;
|
|
886
|
+
const isIndented = line.startsWith(' ');
|
|
887
|
+
// ---- Inside a block scalar (| or >) ----
|
|
888
|
+
if (inMultiline) {
|
|
889
|
+
if (isIndented) {
|
|
890
|
+
currentValue += (currentValue ? '\n' : '') + line.slice(2);
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
// Unindented line ends the block scalar — fall through to top-level parsing
|
|
894
|
+
flushCurrent();
|
|
895
|
+
}
|
|
896
|
+
// ---- Inside a plain scalar (multiline value without | or >) ----
|
|
897
|
+
if (inPlainScalar) {
|
|
898
|
+
if (isIndented) {
|
|
899
|
+
// Continuation line: join with a space (YAML plain scalar folding)
|
|
900
|
+
currentValue += ` ${trimmedLine}`;
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
// Unindented line ends the plain scalar — fall through to top-level parsing
|
|
904
|
+
flushCurrent();
|
|
905
|
+
}
|
|
906
|
+
// ---- Inside a nested object ----
|
|
907
|
+
if (inNestedObject && isIndented) {
|
|
908
|
+
const nestedMatch = line.match(/^ {2}([a-zA-Z_-]+):\s*(.*)$/);
|
|
909
|
+
if (nestedMatch) {
|
|
910
|
+
const [, nestedKey, nestedValue] = nestedMatch;
|
|
911
|
+
nestedObject[nestedKey] = parseYamlValue(nestedValue.trim());
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
// Indented line that isn't a nested key:value — this key was actually
|
|
915
|
+
// a plain scalar, not a nested object. Switch modes.
|
|
916
|
+
inNestedObject = false;
|
|
917
|
+
inPlainScalar = true;
|
|
918
|
+
currentValue = trimmedLine;
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
// ---- Top-level key: value ----
|
|
922
|
+
const keyValueMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
|
|
923
|
+
if (keyValueMatch) {
|
|
924
|
+
flushCurrent();
|
|
925
|
+
currentKey = keyValueMatch[1];
|
|
926
|
+
currentValue = keyValueMatch[2];
|
|
927
|
+
if ('|' === currentValue || '>' === currentValue) {
|
|
928
|
+
inMultiline = true;
|
|
929
|
+
currentValue = '';
|
|
930
|
+
} else if ('' === currentValue) {
|
|
931
|
+
// Empty value — could be nested object or plain scalar; peek at next lines
|
|
932
|
+
inNestedObject = true;
|
|
933
|
+
nestedObject = {};
|
|
934
|
+
}
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
// ---- Unindented line that isn't key:value while in nested object ----
|
|
938
|
+
if (inNestedObject) flushCurrent();
|
|
939
|
+
}
|
|
940
|
+
// Save last accumulated value
|
|
941
|
+
flushCurrent();
|
|
942
|
+
return {
|
|
943
|
+
data,
|
|
944
|
+
content: markdownContent
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Parse YAML value
|
|
949
|
+
*/ function parseYamlValue(value) {
|
|
950
|
+
if (!value) return '';
|
|
951
|
+
// Boolean value
|
|
952
|
+
if ('true' === value) return true;
|
|
953
|
+
if ('false' === value) return false;
|
|
954
|
+
// Number
|
|
955
|
+
if (/^-?\d+$/.test(value)) return parseInt(value, 10);
|
|
956
|
+
if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
|
|
957
|
+
// Remove quotes
|
|
958
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
959
|
+
return value;
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Validate skill name format
|
|
963
|
+
*
|
|
964
|
+
* Specification requirements:
|
|
965
|
+
* - Max 64 characters
|
|
966
|
+
* - Only lowercase letters, numbers, hyphens allowed
|
|
967
|
+
* - Cannot start or end with hyphen
|
|
968
|
+
* - Cannot contain consecutive hyphens
|
|
969
|
+
*/ function validateSkillName(name) {
|
|
970
|
+
if (!name) throw new SkillValidationError('Skill name is required', 'name');
|
|
971
|
+
if (name.length > 64) throw new SkillValidationError('Skill name must be at most 64 characters', 'name');
|
|
972
|
+
if (!/^[a-z0-9]/.test(name)) throw new SkillValidationError('Skill name must start with a lowercase letter or number', 'name');
|
|
973
|
+
if (!/[a-z0-9]$/.test(name)) throw new SkillValidationError('Skill name must end with a lowercase letter or number', 'name');
|
|
974
|
+
if (/--/.test(name)) throw new SkillValidationError('Skill name cannot contain consecutive hyphens', 'name');
|
|
975
|
+
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');
|
|
976
|
+
// Single character name
|
|
977
|
+
if (1 === name.length && !/^[a-z0-9]$/.test(name)) throw new SkillValidationError('Single character skill name must be a lowercase letter or number', 'name');
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Validate skill description
|
|
981
|
+
*
|
|
982
|
+
* Specification requirements:
|
|
983
|
+
* - Max 1024 characters
|
|
984
|
+
* - Angle brackets are allowed per agentskills.io spec
|
|
985
|
+
*/ function validateSkillDescription(description) {
|
|
986
|
+
if (!description) throw new SkillValidationError('Skill description is required', 'description');
|
|
987
|
+
if (description.length > 1024) throw new SkillValidationError('Skill description must be at most 1024 characters', 'description');
|
|
988
|
+
// Note: angle brackets are allowed per agentskills.io spec
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Parse SKILL.md content
|
|
992
|
+
*
|
|
993
|
+
* @param content - SKILL.md file content
|
|
994
|
+
* @param options - Parse options
|
|
995
|
+
* @returns Parsed skill info, or null if format is invalid
|
|
996
|
+
* @throws SkillValidationError if validation fails in strict mode
|
|
997
|
+
*/ function parseSkillMd(content, options = {}) {
|
|
998
|
+
const { strict = false } = options;
|
|
999
|
+
try {
|
|
1000
|
+
const { data, content: body } = parseFrontmatter(content);
|
|
1001
|
+
// Check required fields
|
|
1002
|
+
if (!data.name || !data.description) {
|
|
1003
|
+
if (strict) throw new SkillValidationError('SKILL.md must have name and description in frontmatter');
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
const name = String(data.name);
|
|
1007
|
+
const description = String(data.description);
|
|
1008
|
+
// Validate field format
|
|
1009
|
+
if (strict) {
|
|
1010
|
+
validateSkillName(name);
|
|
1011
|
+
validateSkillDescription(description);
|
|
1012
|
+
}
|
|
1013
|
+
// Parse allowed-tools
|
|
1014
|
+
let allowedTools;
|
|
1015
|
+
if (data['allowed-tools']) {
|
|
1016
|
+
const toolsStr = String(data['allowed-tools']);
|
|
1017
|
+
allowedTools = toolsStr.split(/\s+/).filter(Boolean);
|
|
1018
|
+
}
|
|
1019
|
+
return {
|
|
1020
|
+
name,
|
|
1021
|
+
description,
|
|
1022
|
+
version: data.version ? String(data.version) : void 0,
|
|
1023
|
+
license: data.license ? String(data.license) : void 0,
|
|
1024
|
+
compatibility: data.compatibility ? String(data.compatibility) : void 0,
|
|
1025
|
+
metadata: data.metadata,
|
|
1026
|
+
allowedTools,
|
|
1027
|
+
content: body,
|
|
1028
|
+
rawContent: content
|
|
1029
|
+
};
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
if (error instanceof SkillValidationError) throw error;
|
|
1032
|
+
if (strict) throw new SkillValidationError(`Failed to parse SKILL.md: ${error}`);
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Parse SKILL.md from file path
|
|
1038
|
+
*/ function skill_parser_parseSkillMdFile(filePath, options = {}) {
|
|
1039
|
+
if (!external_node_fs_.existsSync(filePath)) {
|
|
1040
|
+
if (options.strict) throw new SkillValidationError(`SKILL.md not found: ${filePath}`);
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
const content = external_node_fs_.readFileSync(filePath, 'utf-8');
|
|
1044
|
+
return parseSkillMd(content, options);
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Parse SKILL.md from skill directory
|
|
1048
|
+
*/ function parseSkillFromDir(dirPath, options = {}) {
|
|
1049
|
+
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
|
|
1050
|
+
return skill_parser_parseSkillMdFile(skillMdPath, options);
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Check if directory contains valid SKILL.md
|
|
1054
|
+
*/ function hasValidSkillMd(dirPath) {
|
|
1055
|
+
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
|
|
1056
|
+
if (!external_node_fs_.existsSync(skillMdPath)) return false;
|
|
1057
|
+
try {
|
|
1058
|
+
const skill = skill_parser_parseSkillMdFile(skillMdPath);
|
|
1059
|
+
return null !== skill;
|
|
1060
|
+
} catch {
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
const SKIP_DIRS = [
|
|
1065
|
+
'node_modules',
|
|
1066
|
+
'.git',
|
|
1067
|
+
'dist',
|
|
1068
|
+
'build',
|
|
1069
|
+
'__pycache__'
|
|
1070
|
+
];
|
|
1071
|
+
const MAX_DISCOVER_DEPTH = 5;
|
|
1072
|
+
const PRIORITY_SKILL_DIRS = [
|
|
1073
|
+
'skills',
|
|
1074
|
+
'.agents/skills',
|
|
1075
|
+
'.cursor/skills',
|
|
1076
|
+
'.claude/skills',
|
|
1077
|
+
'.windsurf/skills',
|
|
1078
|
+
'.github/skills'
|
|
1079
|
+
];
|
|
1080
|
+
function findSkillDirsRecursive(dir, depth, maxDepth, visitedDirs) {
|
|
1081
|
+
if (depth > maxDepth) return [];
|
|
1082
|
+
const resolvedDir = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir);
|
|
1083
|
+
if (visitedDirs.has(resolvedDir)) return [];
|
|
1084
|
+
if (!external_node_fs_.existsSync(dir) || !external_node_fs_.statSync(dir).isDirectory()) return [];
|
|
1085
|
+
visitedDirs.add(resolvedDir);
|
|
1086
|
+
const results = [];
|
|
1087
|
+
let entries;
|
|
1088
|
+
try {
|
|
1089
|
+
entries = external_node_fs_.readdirSync(dir);
|
|
1090
|
+
} catch {
|
|
1091
|
+
return [];
|
|
1092
|
+
}
|
|
1093
|
+
for (const entry of entries){
|
|
1094
|
+
if (SKIP_DIRS.includes(entry)) continue;
|
|
1095
|
+
const fullPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
|
|
1096
|
+
const resolvedFull = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(fullPath);
|
|
1097
|
+
if (visitedDirs.has(resolvedFull)) continue;
|
|
1098
|
+
let stat;
|
|
1099
|
+
try {
|
|
1100
|
+
stat = external_node_fs_.statSync(fullPath);
|
|
1101
|
+
} catch {
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
if (!!stat.isDirectory()) {
|
|
1105
|
+
if (hasValidSkillMd(fullPath)) results.push(fullPath);
|
|
1106
|
+
results.push(...findSkillDirsRecursive(fullPath, depth + 1, maxDepth, visitedDirs));
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return results;
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Discover all skills in a directory by scanning for SKILL.md files.
|
|
1113
|
+
*
|
|
1114
|
+
* Strategy:
|
|
1115
|
+
* 1. Check root for SKILL.md
|
|
1116
|
+
* 2. Search priority directories (skills/, .agents/skills/, .cursor/skills/, etc.)
|
|
1117
|
+
* 3. Fall back to recursive search (max depth 5, skip node_modules, .git, dist, etc.)
|
|
1118
|
+
*
|
|
1119
|
+
* @param basePath - Root directory to search
|
|
1120
|
+
* @returns List of parsed skills with their directory paths (absolute)
|
|
1121
|
+
*/ function discoverSkillsInDir(basePath) {
|
|
1122
|
+
const resolvedBase = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(basePath);
|
|
1123
|
+
const results = [];
|
|
1124
|
+
const seenNames = new Set();
|
|
1125
|
+
function addSkill(dirPath) {
|
|
1126
|
+
const skill = parseSkillFromDir(dirPath);
|
|
1127
|
+
if (skill && !seenNames.has(skill.name)) {
|
|
1128
|
+
seenNames.add(skill.name);
|
|
1129
|
+
results.push({
|
|
1130
|
+
...skill,
|
|
1131
|
+
dirPath: __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dirPath)
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (hasValidSkillMd(resolvedBase)) addSkill(resolvedBase);
|
|
1136
|
+
// Track visited directories to avoid redundant I/O during recursive scan
|
|
1137
|
+
const visitedDirs = new Set();
|
|
1138
|
+
for (const sub of PRIORITY_SKILL_DIRS){
|
|
1139
|
+
const dir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(resolvedBase, sub);
|
|
1140
|
+
if (!!external_node_fs_.existsSync(dir) && !!external_node_fs_.statSync(dir).isDirectory()) {
|
|
1141
|
+
visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir));
|
|
1142
|
+
try {
|
|
1143
|
+
const entries = external_node_fs_.readdirSync(dir);
|
|
1144
|
+
for (const entry of entries){
|
|
1145
|
+
const skillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
|
|
1146
|
+
try {
|
|
1147
|
+
if (external_node_fs_.statSync(skillDir).isDirectory() && hasValidSkillMd(skillDir)) {
|
|
1148
|
+
addSkill(skillDir);
|
|
1149
|
+
visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(skillDir));
|
|
1150
|
+
}
|
|
1151
|
+
} catch {
|
|
1152
|
+
// Skip entries that can't be stat'd (race condition, permission, etc.)
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
} catch {
|
|
1156
|
+
// Skip if unreadable
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
const recursiveDirs = findSkillDirsRecursive(resolvedBase, 0, MAX_DISCOVER_DEPTH, visitedDirs);
|
|
1161
|
+
for (const skillDir of recursiveDirs)addSkill(skillDir);
|
|
1162
|
+
return results;
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Filter skills by name (case-insensitive exact match).
|
|
1166
|
+
*
|
|
1167
|
+
* Note: an empty `names` array returns an empty result (not all skills).
|
|
1168
|
+
* Callers should check `names.length` before calling if "no filter = all" is desired.
|
|
1169
|
+
*
|
|
1170
|
+
* @param skills - List of discovered skills
|
|
1171
|
+
* @param names - Skill names to match (e.g. from --skill pdf commit)
|
|
1172
|
+
* @returns Skills whose name matches any of the given names
|
|
1173
|
+
*/ function filterSkillsByName(skills, names) {
|
|
1174
|
+
const normalized = names.map((n)=>n.toLowerCase());
|
|
1175
|
+
return skills.filter((skill)=>{
|
|
1176
|
+
// Match against SKILL.md name field
|
|
1177
|
+
if (normalized.includes(skill.name.toLowerCase())) return true;
|
|
1178
|
+
// Also match against the directory name (basename of dirPath)
|
|
1179
|
+
// Users naturally refer to skills by their directory name
|
|
1180
|
+
const dirName = __WEBPACK_EXTERNAL_MODULE_node_path__.basename(skill.dirPath).toLowerCase();
|
|
1181
|
+
return normalized.includes(dirName);
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Generate SKILL.md content
|
|
1186
|
+
*/ function generateSkillMd(skill) {
|
|
1187
|
+
const frontmatter = [
|
|
1188
|
+
'---'
|
|
1189
|
+
];
|
|
1190
|
+
frontmatter.push(`name: ${skill.name}`);
|
|
1191
|
+
frontmatter.push(`description: ${skill.description}`);
|
|
1192
|
+
if (skill.license) frontmatter.push(`license: ${skill.license}`);
|
|
1193
|
+
if (skill.compatibility) frontmatter.push(`compatibility: ${skill.compatibility}`);
|
|
1194
|
+
if (skill.allowedTools && skill.allowedTools.length > 0) frontmatter.push(`allowed-tools: ${skill.allowedTools.join(' ')}`);
|
|
1195
|
+
frontmatter.push('---');
|
|
1196
|
+
frontmatter.push('');
|
|
1197
|
+
return frontmatter.join('\n') + skill.content;
|
|
1198
|
+
}
|
|
821
1199
|
/**
|
|
822
1200
|
* Installer - Multi-Agent installer
|
|
823
1201
|
*
|
|
@@ -828,6 +1206,10 @@ const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEB
|
|
|
828
1206
|
* Reference: https://github.com/vercel-labs/add-skill/blob/main/src/installer.ts
|
|
829
1207
|
*/ const installer_AGENTS_DIR = '.agents';
|
|
830
1208
|
const installer_SKILLS_SUBDIR = 'skills';
|
|
1209
|
+
/**
|
|
1210
|
+
* Marker comment in auto-generated Cursor bridge rule files.
|
|
1211
|
+
* Used to distinguish auto-generated files from manually created ones.
|
|
1212
|
+
*/ const CURSOR_BRIDGE_MARKER = '<!-- reskill:auto-generated -->';
|
|
831
1213
|
/**
|
|
832
1214
|
* Default files to exclude when copying skills
|
|
833
1215
|
* These files are typically used for repository metadata and should not be copied to agent directories
|
|
@@ -1006,45 +1388,50 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1006
1388
|
error: 'Invalid skill name: potential path traversal detected'
|
|
1007
1389
|
};
|
|
1008
1390
|
try {
|
|
1391
|
+
let result;
|
|
1009
1392
|
// Copy mode: directly copy to agent location
|
|
1010
1393
|
if ('copy' === installMode) {
|
|
1011
1394
|
installer_ensureDir(agentDir);
|
|
1012
1395
|
installer_remove(agentDir);
|
|
1013
1396
|
copyDirectory(sourcePath, agentDir);
|
|
1014
|
-
|
|
1397
|
+
result = {
|
|
1015
1398
|
success: true,
|
|
1016
1399
|
path: agentDir,
|
|
1017
1400
|
mode: 'copy'
|
|
1018
1401
|
};
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
// Symlink failed, fallback to copy
|
|
1027
|
-
try {
|
|
1028
|
-
installer_remove(agentDir);
|
|
1029
|
-
} catch {
|
|
1030
|
-
// Ignore cleanup errors
|
|
1031
|
-
}
|
|
1032
|
-
installer_ensureDir(agentDir);
|
|
1033
|
-
copyDirectory(sourcePath, agentDir);
|
|
1034
|
-
return {
|
|
1402
|
+
} else {
|
|
1403
|
+
// Symlink mode: copy to canonical location, then create symlink
|
|
1404
|
+
installer_ensureDir(canonicalDir);
|
|
1405
|
+
installer_remove(canonicalDir);
|
|
1406
|
+
copyDirectory(sourcePath, canonicalDir);
|
|
1407
|
+
const symlinkCreated = await installer_createSymlink(canonicalDir, agentDir);
|
|
1408
|
+
if (symlinkCreated) result = {
|
|
1035
1409
|
success: true,
|
|
1036
1410
|
path: agentDir,
|
|
1037
1411
|
canonicalPath: canonicalDir,
|
|
1038
|
-
mode: 'symlink'
|
|
1039
|
-
symlinkFailed: true
|
|
1412
|
+
mode: 'symlink'
|
|
1040
1413
|
};
|
|
1414
|
+
else {
|
|
1415
|
+
// Symlink failed, fallback to copy
|
|
1416
|
+
try {
|
|
1417
|
+
installer_remove(agentDir);
|
|
1418
|
+
} catch {
|
|
1419
|
+
// Ignore cleanup errors
|
|
1420
|
+
}
|
|
1421
|
+
installer_ensureDir(agentDir);
|
|
1422
|
+
copyDirectory(sourcePath, agentDir);
|
|
1423
|
+
result = {
|
|
1424
|
+
success: true,
|
|
1425
|
+
path: agentDir,
|
|
1426
|
+
canonicalPath: canonicalDir,
|
|
1427
|
+
mode: 'symlink',
|
|
1428
|
+
symlinkFailed: true
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1041
1431
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
canonicalPath: canonicalDir,
|
|
1046
|
-
mode: 'symlink'
|
|
1047
|
-
};
|
|
1432
|
+
// Create Cursor bridge rule file (project-level only)
|
|
1433
|
+
if ('cursor' === agentType && !this.isGlobal) this.createCursorBridgeRule(sanitized, sourcePath);
|
|
1434
|
+
return result;
|
|
1048
1435
|
} catch (error) {
|
|
1049
1436
|
return {
|
|
1050
1437
|
success: false,
|
|
@@ -1082,6 +1469,8 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1082
1469
|
const skillPath = this.getAgentSkillPath(skillName, agentType);
|
|
1083
1470
|
if (!external_node_fs_.existsSync(skillPath)) return false;
|
|
1084
1471
|
installer_remove(skillPath);
|
|
1472
|
+
// Remove Cursor bridge rule file (project-level only)
|
|
1473
|
+
if ('cursor' === agentType && !this.isGlobal) this.removeCursorBridgeRule(installer_sanitizeName(skillName));
|
|
1085
1474
|
return true;
|
|
1086
1475
|
}
|
|
1087
1476
|
/**
|
|
@@ -1104,6 +1493,65 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1104
1493
|
withFileTypes: true
|
|
1105
1494
|
}).filter((entry)=>entry.isDirectory() || entry.isSymbolicLink()).map((entry)=>entry.name);
|
|
1106
1495
|
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Create a Cursor bridge rule file (.mdc) for the installed skill.
|
|
1498
|
+
*
|
|
1499
|
+
* Cursor does not natively read SKILL.md from .cursor/skills/.
|
|
1500
|
+
* This bridge file in .cursor/rules/ references the SKILL.md via @file directive,
|
|
1501
|
+
* allowing Cursor to discover and activate the skill based on the description.
|
|
1502
|
+
*
|
|
1503
|
+
* @param skillName - Sanitized skill name
|
|
1504
|
+
* @param sourcePath - Source directory containing SKILL.md
|
|
1505
|
+
*/ createCursorBridgeRule(skillName, sourcePath) {
|
|
1506
|
+
try {
|
|
1507
|
+
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(sourcePath, 'SKILL.md');
|
|
1508
|
+
if (!external_node_fs_.existsSync(skillMdPath)) return;
|
|
1509
|
+
const content = external_node_fs_.readFileSync(skillMdPath, 'utf-8');
|
|
1510
|
+
const parsed = parseSkillMd(content);
|
|
1511
|
+
if (!parsed || !parsed.description) return;
|
|
1512
|
+
const rulesDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cwd, '.cursor', 'rules');
|
|
1513
|
+
installer_ensureDir(rulesDir);
|
|
1514
|
+
// Do not overwrite manually created rule files (without auto-generated marker)
|
|
1515
|
+
const bridgePath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(rulesDir, `${skillName}.mdc`);
|
|
1516
|
+
if (external_node_fs_.existsSync(bridgePath)) {
|
|
1517
|
+
const existingContent = external_node_fs_.readFileSync(bridgePath, 'utf-8');
|
|
1518
|
+
if (!existingContent.includes(CURSOR_BRIDGE_MARKER)) return;
|
|
1519
|
+
}
|
|
1520
|
+
// Quote description to prevent YAML injection from special characters
|
|
1521
|
+
const safeDescription = parsed.description.replace(/"/g, '\\"');
|
|
1522
|
+
const agent = getAgentConfig('cursor');
|
|
1523
|
+
const bridgeContent = `---
|
|
1524
|
+
description: "${safeDescription}"
|
|
1525
|
+
globs:
|
|
1526
|
+
alwaysApply: false
|
|
1527
|
+
---
|
|
1528
|
+
|
|
1529
|
+
${CURSOR_BRIDGE_MARKER}
|
|
1530
|
+
@file ${agent.skillsDir}/${skillName}/SKILL.md
|
|
1531
|
+
`;
|
|
1532
|
+
external_node_fs_.writeFileSync(bridgePath, bridgeContent, 'utf-8');
|
|
1533
|
+
} catch {
|
|
1534
|
+
// Silently skip bridge file creation on errors
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Remove a Cursor bridge rule file (.mdc) for the uninstalled skill.
|
|
1539
|
+
*
|
|
1540
|
+
* Only removes files that contain the auto-generated marker to avoid
|
|
1541
|
+
* deleting manually created rule files.
|
|
1542
|
+
*
|
|
1543
|
+
* @param skillName - Sanitized skill name
|
|
1544
|
+
*/ removeCursorBridgeRule(skillName) {
|
|
1545
|
+
try {
|
|
1546
|
+
const bridgePath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cwd, '.cursor', 'rules', `${skillName}.mdc`);
|
|
1547
|
+
if (!external_node_fs_.existsSync(bridgePath)) return;
|
|
1548
|
+
const content = external_node_fs_.readFileSync(bridgePath, 'utf-8');
|
|
1549
|
+
if (!content.includes(CURSOR_BRIDGE_MARKER)) return;
|
|
1550
|
+
external_node_fs_.rmSync(bridgePath);
|
|
1551
|
+
} catch {
|
|
1552
|
+
// Silently skip bridge file removal on errors
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1107
1555
|
}
|
|
1108
1556
|
/**
|
|
1109
1557
|
* CacheManager - Manage global skill cache
|
|
@@ -1836,11 +2284,25 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1836
2284
|
}
|
|
1837
2285
|
// Parse owner/repo and possible subPath
|
|
1838
2286
|
// E.g.: user/repo or org/monorepo/skills/pdf
|
|
2287
|
+
// Also handle GitHub web URL style: owner/repo/tree/branch/path
|
|
1839
2288
|
const parts = remaining.split('/');
|
|
1840
2289
|
if (parts.length < 2) throw new Error(`Invalid skill reference: ${ref}. Expected format: owner/repo[@version]`);
|
|
1841
2290
|
const owner = parts[0];
|
|
1842
2291
|
const repo = parts[1];
|
|
1843
|
-
|
|
2292
|
+
let subPath;
|
|
2293
|
+
// Check for GitHub/GitLab web URL pattern: owner/repo/(tree|blob|raw)/branch/path
|
|
2294
|
+
// e.g. vercel-labs/skills/tree/main/skills/find-skills
|
|
2295
|
+
// Only apply this heuristic when no explicit @version is provided.
|
|
2296
|
+
// With @version, treat tree/blob/raw as literal directory names (standard monorepo subPath).
|
|
2297
|
+
if (parts.length >= 4 && [
|
|
2298
|
+
'tree',
|
|
2299
|
+
'blob',
|
|
2300
|
+
'raw'
|
|
2301
|
+
].includes(parts[2]) && !version) {
|
|
2302
|
+
const branch = parts[3];
|
|
2303
|
+
version = `branch:${branch}`;
|
|
2304
|
+
subPath = parts.length > 4 ? parts.slice(4).join('/') : void 0;
|
|
2305
|
+
} else subPath = parts.length > 2 ? parts.slice(2).join('/') : void 0;
|
|
1844
2306
|
return {
|
|
1845
2307
|
registry,
|
|
1846
2308
|
owner,
|
|
@@ -2069,20 +2531,31 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
2069
2531
|
* Check if a reference is an HTTP/OSS URL (for archive downloads)
|
|
2070
2532
|
*
|
|
2071
2533
|
* Returns true for:
|
|
2072
|
-
* - http:// or https:// URLs
|
|
2073
|
-
* - Explicit oss:// or s3:// protocol URLs
|
|
2534
|
+
* - http:// or https:// URLs with archive file extensions (.tar.gz, .tgz, .zip, .tar)
|
|
2535
|
+
* - Explicit oss:// or s3:// protocol URLs (always treated as archive sources)
|
|
2074
2536
|
*
|
|
2075
2537
|
* Returns false for:
|
|
2076
2538
|
* - Git repository URLs (*.git)
|
|
2077
2539
|
* - GitHub/GitLab web URLs (/tree/, /blob/, /raw/)
|
|
2540
|
+
* - Bare HTTPS repo URLs without archive extensions (e.g., https://github.com/user/repo)
|
|
2541
|
+
* These are treated as Git references and handled by GitResolver.
|
|
2078
2542
|
*/ static isHttpUrl(ref) {
|
|
2079
2543
|
// Remove version suffix for checking (e.g., url@v1.0.0)
|
|
2080
2544
|
const urlPart = ref.split('@')[0];
|
|
2081
|
-
//
|
|
2082
|
-
if (urlPart.
|
|
2083
|
-
//
|
|
2084
|
-
if (
|
|
2085
|
-
|
|
2545
|
+
// oss:// and s3:// are always archive download sources
|
|
2546
|
+
if (urlPart.startsWith('oss://') || urlPart.startsWith('s3://')) return true;
|
|
2547
|
+
// For http:// and https:// URLs, distinguish between Git repos and archive downloads
|
|
2548
|
+
if (urlPart.startsWith('http://') || urlPart.startsWith('https://')) {
|
|
2549
|
+
// Exclude Git repository URLs (ending with .git)
|
|
2550
|
+
if (urlPart.endsWith('.git')) return false;
|
|
2551
|
+
// Exclude GitHub/GitLab web URLs (containing /tree/, /blob/, /raw/)
|
|
2552
|
+
if (/\/(tree|blob|raw)\//.test(urlPart)) return false;
|
|
2553
|
+
// Only classify as HTTP archive if URL has a recognized archive extension.
|
|
2554
|
+
// Bare HTTPS URLs like https://github.com/user/repo are Git references,
|
|
2555
|
+
// not archive downloads, and should fall through to GitResolver.
|
|
2556
|
+
return /\.(tar\.gz|tgz|zip|tar)$/i.test(urlPart);
|
|
2557
|
+
}
|
|
2558
|
+
return false;
|
|
2086
2559
|
}
|
|
2087
2560
|
/**
|
|
2088
2561
|
* Parse an HTTP/OSS URL reference
|
|
@@ -2391,46 +2864,21 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
2391
2864
|
* Maps registry URLs to their corresponding scopes.
|
|
2392
2865
|
* Currently hardcoded; TODO: fetch from /api/registry/info in the future.
|
|
2393
2866
|
*/ /**
|
|
2394
|
-
*
|
|
2395
|
-
*
|
|
2396
|
-
*/ const
|
|
2867
|
+
* Public Registry URL
|
|
2868
|
+
* Used for installing skills without a scope
|
|
2869
|
+
*/ const registry_scope_PUBLIC_REGISTRY = 'https://reskill.info/';
|
|
2397
2870
|
/**
|
|
2398
2871
|
* Hardcoded registry to scope mapping
|
|
2399
2872
|
* TODO: Replace with dynamic fetching from /api/registry/info
|
|
2400
2873
|
*/ const REGISTRY_SCOPE_MAP = {
|
|
2401
2874
|
// rush-app (private registry, new)
|
|
2402
|
-
'https://rush-test.zhenguanyu.com': '@kanyun',
|
|
2875
|
+
'https://rush-test.zhenguanyu.com': '@kanyun-test',
|
|
2403
2876
|
'https://rush.zhenguanyu.com': '@kanyun',
|
|
2404
2877
|
// reskill-app (private registry, legacy)
|
|
2405
|
-
'https://reskill-test.zhenguanyu.com': '@kanyun',
|
|
2878
|
+
'https://reskill-test.zhenguanyu.com': '@kanyun-test',
|
|
2406
2879
|
// Local development
|
|
2407
|
-
'http://localhost:3000': '@kanyun'
|
|
2408
|
-
};
|
|
2409
|
-
/**
|
|
2410
|
-
* Registry API prefix mapping
|
|
2411
|
-
*
|
|
2412
|
-
* rush-app hosts reskill APIs under /api/reskill/ prefix.
|
|
2413
|
-
* Default for unlisted registries: '/api'
|
|
2414
|
-
*/ const REGISTRY_API_PREFIX = {
|
|
2415
|
-
'https://rush-test.zhenguanyu.com': '/api/reskill',
|
|
2416
|
-
'https://rush.zhenguanyu.com': '/api/reskill',
|
|
2417
|
-
'http://localhost:3000': '/api/reskill'
|
|
2880
|
+
'http://localhost:3000': '@kanyun-test'
|
|
2418
2881
|
};
|
|
2419
|
-
/**
|
|
2420
|
-
* Get the API path prefix for a given registry URL
|
|
2421
|
-
*
|
|
2422
|
-
* @param registryUrl - Registry URL
|
|
2423
|
-
* @returns API prefix string (e.g., '/api' or '/api/reskill')
|
|
2424
|
-
*
|
|
2425
|
-
* @example
|
|
2426
|
-
* getApiPrefix('https://rush-test.zhenguanyu.com') // '/api/reskill'
|
|
2427
|
-
* getApiPrefix('https://reskill.info') // '/api'
|
|
2428
|
-
* getApiPrefix('https://unknown.com') // '/api'
|
|
2429
|
-
*/ function getApiPrefix(registryUrl) {
|
|
2430
|
-
if (!registryUrl) return '/api';
|
|
2431
|
-
const normalized = registryUrl.endsWith('/') ? registryUrl.slice(0, -1) : registryUrl;
|
|
2432
|
-
return REGISTRY_API_PREFIX[normalized] || '/api';
|
|
2433
|
-
}
|
|
2434
2882
|
/**
|
|
2435
2883
|
* Get the registry URL for a given scope (reverse lookup)
|
|
2436
2884
|
*
|
|
@@ -2478,7 +2926,7 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
2478
2926
|
* getRegistryUrl('@mycompany', { '@mycompany': 'https://my.registry.com/' }) // 'https://my.registry.com/'
|
|
2479
2927
|
*/ function getRegistryUrl(scope, customRegistries) {
|
|
2480
2928
|
// No scope → return public Registry
|
|
2481
|
-
if (!scope) return
|
|
2929
|
+
if (!scope) return registry_scope_PUBLIC_REGISTRY;
|
|
2482
2930
|
// With scope → lookup private Registry
|
|
2483
2931
|
const registry = getRegistryForScope(scope, customRegistries);
|
|
2484
2932
|
if (!registry) {
|
|
@@ -2529,21 +2977,21 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
2529
2977
|
/**
|
|
2530
2978
|
* Parse a skill identifier into its components (with version support)
|
|
2531
2979
|
*
|
|
2532
|
-
*
|
|
2980
|
+
* Supports both private registry (with @scope) and public registry (without scope) formats.
|
|
2533
2981
|
*
|
|
2534
2982
|
* @param identifier - Skill identifier string
|
|
2535
2983
|
* @returns Parsed skill identifier with scope, name, version, and fullName
|
|
2536
2984
|
* @throws Error if identifier is invalid
|
|
2537
2985
|
*
|
|
2538
2986
|
* @example
|
|
2539
|
-
* //
|
|
2987
|
+
* // Private registry
|
|
2540
2988
|
* parseSkillIdentifier('@kanyun/planning-with-files')
|
|
2541
2989
|
* // { scope: '@kanyun', name: 'planning-with-files', version: undefined, fullName: '@kanyun/planning-with-files' }
|
|
2542
2990
|
*
|
|
2543
2991
|
* parseSkillIdentifier('@kanyun/skill@2.4.5')
|
|
2544
2992
|
* // { scope: '@kanyun', name: 'skill', version: '2.4.5', fullName: '@kanyun/skill' }
|
|
2545
2993
|
*
|
|
2546
|
-
* //
|
|
2994
|
+
* // Public registry
|
|
2547
2995
|
* parseSkillIdentifier('planning-with-files')
|
|
2548
2996
|
* // { scope: null, name: 'planning-with-files', version: undefined, fullName: 'planning-with-files' }
|
|
2549
2997
|
*
|
|
@@ -2551,18 +2999,18 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
2551
2999
|
* // { scope: null, name: 'skill', version: 'latest', fullName: 'skill' }
|
|
2552
3000
|
*/ function parseSkillIdentifier(identifier) {
|
|
2553
3001
|
const trimmed = identifier.trim();
|
|
2554
|
-
//
|
|
3002
|
+
// Empty string or whitespace only
|
|
2555
3003
|
if (!trimmed) throw new Error('Invalid skill identifier: empty string');
|
|
2556
|
-
//
|
|
3004
|
+
// Starting with @@ is invalid
|
|
2557
3005
|
if (trimmed.startsWith('@@')) throw new Error('Invalid skill identifier: invalid scope format');
|
|
2558
|
-
//
|
|
3006
|
+
// Bare @ is invalid
|
|
2559
3007
|
if ('@' === trimmed) throw new Error('Invalid skill identifier: missing scope and name');
|
|
2560
|
-
//
|
|
3008
|
+
// Scoped format: @scope/name[@version]
|
|
2561
3009
|
if (trimmed.startsWith('@')) {
|
|
2562
|
-
//
|
|
2563
|
-
// scope:
|
|
2564
|
-
// name:
|
|
2565
|
-
// version:
|
|
3010
|
+
// Regex: @scope/name[@version]
|
|
3011
|
+
// scope: starts with @, followed by alphanumeric, hyphens, underscores
|
|
3012
|
+
// name: alphanumeric, hyphens, underscores
|
|
3013
|
+
// version: optional, @ followed by any non-empty string
|
|
2566
3014
|
const scopedMatch = trimmed.match(/^(@[\w-]+)\/([\w-]+)(?:@(.+))?$/);
|
|
2567
3015
|
if (!scopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
|
|
2568
3016
|
const [, scope, name, version] = scopedMatch;
|
|
@@ -2573,8 +3021,8 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
2573
3021
|
fullName: `${scope}/${name}`
|
|
2574
3022
|
};
|
|
2575
3023
|
}
|
|
2576
|
-
//
|
|
2577
|
-
// name
|
|
3024
|
+
// Unscoped format: name[@version] (public registry)
|
|
3025
|
+
// name must not contain / (otherwise it might be a git shorthand)
|
|
2578
3026
|
const unscopedMatch = trimmed.match(/^([\w-]+)(?:@(.+))?$/);
|
|
2579
3027
|
if (!unscopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
|
|
2580
3028
|
const [, name, version] = unscopedMatch;
|
|
@@ -2608,13 +3056,14 @@ class RegistryClient {
|
|
|
2608
3056
|
this.config = config;
|
|
2609
3057
|
}
|
|
2610
3058
|
/**
|
|
2611
|
-
* Get API base URL (registry +
|
|
3059
|
+
* Get API base URL (registry + /api)
|
|
2612
3060
|
*
|
|
2613
|
-
*
|
|
3061
|
+
* All registries use the unified '/api' prefix.
|
|
3062
|
+
*
|
|
3063
|
+
* @returns Base URL for API calls, e.g., 'https://example.com/api'
|
|
2614
3064
|
*/ getApiBase() {
|
|
2615
|
-
const prefix = this.config.apiPrefix || '/api';
|
|
2616
3065
|
const registry = this.config.registry.endsWith('/') ? this.config.registry.slice(0, -1) : this.config.registry;
|
|
2617
|
-
return `${registry}
|
|
3066
|
+
return `${registry}/api`;
|
|
2618
3067
|
}
|
|
2619
3068
|
/**
|
|
2620
3069
|
* Get authorization headers
|
|
@@ -2629,7 +3078,7 @@ class RegistryClient {
|
|
|
2629
3078
|
/**
|
|
2630
3079
|
* Get current user info (whoami)
|
|
2631
3080
|
*/ async whoami() {
|
|
2632
|
-
const url = `${this.getApiBase()}/auth/me`;
|
|
3081
|
+
const url = `${this.getApiBase()}/skill-auth/me`;
|
|
2633
3082
|
const response = await fetch(url, {
|
|
2634
3083
|
method: 'GET',
|
|
2635
3084
|
headers: this.getAuthHeaders()
|
|
@@ -2641,13 +3090,13 @@ class RegistryClient {
|
|
|
2641
3090
|
/**
|
|
2642
3091
|
* CLI login - verify token and get user info
|
|
2643
3092
|
*
|
|
2644
|
-
* Calls POST /api/auth/login-cli to validate the token and retrieve user information.
|
|
3093
|
+
* Calls POST /api/skill-auth/login-cli to validate the token and retrieve user information.
|
|
2645
3094
|
* This is the preferred method for CLI authentication.
|
|
2646
3095
|
*
|
|
2647
3096
|
* @returns User information if authentication succeeds
|
|
2648
3097
|
* @throws RegistryError if authentication fails
|
|
2649
3098
|
*/ async loginCli() {
|
|
2650
|
-
const url = `${this.getApiBase()}/auth/login-cli`;
|
|
3099
|
+
const url = `${this.getApiBase()}/skill-auth/login-cli`;
|
|
2651
3100
|
const response = await fetch(url, {
|
|
2652
3101
|
method: 'POST',
|
|
2653
3102
|
headers: this.getAuthHeaders()
|
|
@@ -2679,7 +3128,7 @@ class RegistryClient {
|
|
|
2679
3128
|
if (external_node_fs_.existsSync(filePath)) {
|
|
2680
3129
|
const content = external_node_fs_.readFileSync(filePath);
|
|
2681
3130
|
const stat = external_node_fs_.statSync(filePath);
|
|
2682
|
-
//
|
|
3131
|
+
// Prepend shortName as top-level directory if provided
|
|
2683
3132
|
const entryName = shortName ? `${shortName}/${file}` : file;
|
|
2684
3133
|
tarPack.entry({
|
|
2685
3134
|
name: entryName,
|
|
@@ -2693,15 +3142,15 @@ class RegistryClient {
|
|
|
2693
3142
|
});
|
|
2694
3143
|
}
|
|
2695
3144
|
// ============================================================================
|
|
2696
|
-
// Skill Info Methods (
|
|
3145
|
+
// Skill Info Methods (web-published skill support)
|
|
2697
3146
|
// ============================================================================
|
|
2698
3147
|
/**
|
|
2699
|
-
*
|
|
2700
|
-
*
|
|
3148
|
+
* Get basic skill info (including source_type).
|
|
3149
|
+
* Used by the install command to determine the installation logic branch.
|
|
2701
3150
|
*
|
|
2702
|
-
* @param skillName -
|
|
2703
|
-
* @returns
|
|
2704
|
-
* @throws RegistryError
|
|
3151
|
+
* @param skillName - Full skill name, e.g., @kanyun/my-skill
|
|
3152
|
+
* @returns Basic skill information
|
|
3153
|
+
* @throws RegistryError if skill not found or request failed
|
|
2705
3154
|
*/ async getSkillInfo(skillName) {
|
|
2706
3155
|
const url = `${this.getApiBase()}/skills/${encodeURIComponent(skillName)}`;
|
|
2707
3156
|
const response = await fetch(url, {
|
|
@@ -2710,15 +3159,50 @@ class RegistryClient {
|
|
|
2710
3159
|
});
|
|
2711
3160
|
if (!response.ok) {
|
|
2712
3161
|
const data = await response.json();
|
|
2713
|
-
//
|
|
3162
|
+
// Return a clear "not found" error for 404 responses
|
|
2714
3163
|
if (404 === response.status) throw new RegistryError(`Skill not found: ${skillName}`, response.status, data);
|
|
2715
3164
|
throw new RegistryError(data.error || `Failed to get skill info: ${response.statusText}`, response.status, data);
|
|
2716
3165
|
}
|
|
2717
|
-
// API
|
|
3166
|
+
// API response format: { success: true, data: { ... } }
|
|
2718
3167
|
const responseData = await response.json();
|
|
2719
3168
|
return responseData.data || responseData;
|
|
2720
3169
|
}
|
|
2721
3170
|
// ============================================================================
|
|
3171
|
+
// Search Methods
|
|
3172
|
+
// ============================================================================
|
|
3173
|
+
/**
|
|
3174
|
+
* Search for skills in the registry
|
|
3175
|
+
*
|
|
3176
|
+
* @param query - Search query string
|
|
3177
|
+
* @param options - Search options (limit, offset)
|
|
3178
|
+
* @returns Array of matching skills
|
|
3179
|
+
* @throws RegistryError if the request fails
|
|
3180
|
+
*
|
|
3181
|
+
* @example
|
|
3182
|
+
* const results = await client.search('typescript');
|
|
3183
|
+
* const results = await client.search('planning', { limit: 5 });
|
|
3184
|
+
*/ async search(query, options = {}) {
|
|
3185
|
+
const params = new URLSearchParams({
|
|
3186
|
+
q: query
|
|
3187
|
+
});
|
|
3188
|
+
if (void 0 !== options.limit) params.set('limit', String(options.limit));
|
|
3189
|
+
if (void 0 !== options.offset) params.set('offset', String(options.offset));
|
|
3190
|
+
const url = `${this.getApiBase()}/skills?${params.toString()}`;
|
|
3191
|
+
const response = await fetch(url, {
|
|
3192
|
+
method: 'GET',
|
|
3193
|
+
headers: this.getAuthHeaders()
|
|
3194
|
+
});
|
|
3195
|
+
if (!response.ok) {
|
|
3196
|
+
const data = await response.json();
|
|
3197
|
+
throw new RegistryError(data.error || `Search failed: ${response.status}`, response.status, data);
|
|
3198
|
+
}
|
|
3199
|
+
const data = await response.json();
|
|
3200
|
+
return {
|
|
3201
|
+
items: data.data || [],
|
|
3202
|
+
total: data.meta?.pagination?.totalItems ?? data.data?.length ?? 0
|
|
3203
|
+
};
|
|
3204
|
+
}
|
|
3205
|
+
// ============================================================================
|
|
2722
3206
|
// Download Methods (Step 3.3)
|
|
2723
3207
|
// ============================================================================
|
|
2724
3208
|
/**
|
|
@@ -2731,12 +3215,12 @@ class RegistryClient {
|
|
|
2731
3215
|
*
|
|
2732
3216
|
* @example
|
|
2733
3217
|
* await client.resolveVersion('@kanyun/test-skill', 'latest') // '2.4.5'
|
|
2734
|
-
* await client.resolveVersion('@kanyun/test-skill', '2.4.5') // '2.4.5' (
|
|
3218
|
+
* await client.resolveVersion('@kanyun/test-skill', '2.4.5') // '2.4.5' (returned as-is)
|
|
2735
3219
|
*/ async resolveVersion(skillName, tagOrVersion) {
|
|
2736
3220
|
const version = tagOrVersion || 'latest';
|
|
2737
|
-
//
|
|
3221
|
+
// If it's already a semver version number, return as-is
|
|
2738
3222
|
if (/^\d+\.\d+\.\d+/.test(version)) return version;
|
|
2739
|
-
//
|
|
3223
|
+
// Otherwise treat it as a tag and query dist-tags
|
|
2740
3224
|
const url = `${this.getApiBase()}/skills/${encodeURIComponent(skillName)}`;
|
|
2741
3225
|
const response = await fetch(url, {
|
|
2742
3226
|
method: 'GET',
|
|
@@ -2746,14 +3230,14 @@ class RegistryClient {
|
|
|
2746
3230
|
const data = await response.json();
|
|
2747
3231
|
throw new RegistryError(data.error || `Failed to fetch skill metadata: ${response.status}`, response.status, data);
|
|
2748
3232
|
}
|
|
2749
|
-
// API
|
|
3233
|
+
// API response format: { success: true, data: { dist_tags: [{ tag, version }] } }
|
|
2750
3234
|
const responseData = await response.json();
|
|
2751
|
-
//
|
|
3235
|
+
// Prefer npm-style dist-tags if present
|
|
2752
3236
|
if (responseData['dist-tags']) {
|
|
2753
3237
|
const resolvedVersion = responseData['dist-tags'][version];
|
|
2754
3238
|
if (resolvedVersion) return resolvedVersion;
|
|
2755
3239
|
}
|
|
2756
|
-
//
|
|
3240
|
+
// Fall back to reskill-app's dist_tags array format
|
|
2757
3241
|
const distTags = responseData.data?.dist_tags;
|
|
2758
3242
|
if (distTags && Array.isArray(distTags)) {
|
|
2759
3243
|
const tagEntry = distTags.find((t)=>t.tag === version);
|
|
@@ -2834,11 +3318,11 @@ class RegistryClient {
|
|
|
2834
3318
|
* @example
|
|
2835
3319
|
* RegistryClient.verifyIntegrity(buffer, 'sha256-abc123...') // true or false
|
|
2836
3320
|
*/ static verifyIntegrity(content, expectedIntegrity) {
|
|
2837
|
-
//
|
|
3321
|
+
// Parse integrity format: algorithm-hash
|
|
2838
3322
|
const match = expectedIntegrity.match(/^(\w+)-(.+)$/);
|
|
2839
3323
|
if (!match) throw new Error(`Invalid integrity format: ${expectedIntegrity}`);
|
|
2840
3324
|
const [, algorithm, expectedHash] = match;
|
|
2841
|
-
//
|
|
3325
|
+
// Only sha256 and sha512 are supported
|
|
2842
3326
|
if ('sha256' !== algorithm && 'sha512' !== algorithm) throw new Error(`Unsupported integrity algorithm: ${algorithm}`);
|
|
2843
3327
|
const actualHash = __WEBPACK_EXTERNAL_MODULE_node_crypto__.createHash(algorithm).update(content).digest('base64');
|
|
2844
3328
|
return actualHash === expectedHash;
|
|
@@ -2850,7 +3334,7 @@ class RegistryClient {
|
|
|
2850
3334
|
* Publish a skill to the registry
|
|
2851
3335
|
*/ async publish(skillName, payload, skillPath, options = {}) {
|
|
2852
3336
|
const url = `${this.getApiBase()}/skills/publish`;
|
|
2853
|
-
//
|
|
3337
|
+
// Extract short name as tarball top-level directory (without scope prefix)
|
|
2854
3338
|
const shortName = getShortName(skillName);
|
|
2855
3339
|
// Create tarball with short name as top-level directory
|
|
2856
3340
|
const tarball = await this.createTarball(skillPath, payload.files, shortName);
|
|
@@ -3109,352 +3593,122 @@ class RegistryClient {
|
|
|
3109
3593
|
let topDir = null;
|
|
3110
3594
|
extractor.on('entry', (header, stream, next)=>{
|
|
3111
3595
|
if (!topDir && header.name) {
|
|
3112
|
-
// Get top-level directory from first entry
|
|
3113
|
-
const parts = header.name.split('/');
|
|
3114
|
-
if (parts.length > 0 && parts[0]) topDir = parts[0];
|
|
3115
|
-
}
|
|
3116
|
-
stream.resume();
|
|
3117
|
-
next();
|
|
3118
|
-
});
|
|
3119
|
-
extractor.on('finish', ()=>{
|
|
3120
|
-
resolve(topDir);
|
|
3121
|
-
});
|
|
3122
|
-
extractor.on('error', (err)=>{
|
|
3123
|
-
reject(new Error(`Failed to read tarball: ${err.message}`));
|
|
3124
|
-
});
|
|
3125
|
-
gunzip.on('error', (err)=>{
|
|
3126
|
-
reject(new Error(`Failed to decompress tarball: ${err.message}`));
|
|
3127
|
-
});
|
|
3128
|
-
gunzip.pipe(extractor);
|
|
3129
|
-
gunzip.end(tarball);
|
|
3130
|
-
});
|
|
3131
|
-
}
|
|
3132
|
-
/**
|
|
3133
|
-
* Registry Resolver (Step 5.1)
|
|
3134
|
-
*
|
|
3135
|
-
* Resolves skill references from npm-style registries:
|
|
3136
|
-
* - Private registry: @scope/name[@version] (e.g., @kanyun/planning-with-files@2.4.5)
|
|
3137
|
-
* - Public registry: name[@version] (e.g., my-skill@1.0.0)
|
|
3138
|
-
*
|
|
3139
|
-
* Uses RegistryClient to download and verify skills.
|
|
3140
|
-
*/ // ============================================================================
|
|
3141
|
-
// RegistryResolver Class
|
|
3142
|
-
// ============================================================================
|
|
3143
|
-
class RegistryResolver {
|
|
3144
|
-
/**
|
|
3145
|
-
* Check if a reference is a registry source (not Git or HTTP)
|
|
3146
|
-
*
|
|
3147
|
-
* Registry formats:
|
|
3148
|
-
* - @scope/name[@version] - private registry
|
|
3149
|
-
* - name[@version] - public registry (if not matching other formats)
|
|
3150
|
-
*
|
|
3151
|
-
* Explicitly excluded:
|
|
3152
|
-
* - Git SSH: git@github.com:user/repo.git
|
|
3153
|
-
* - Git HTTPS: https://github.com/user/repo.git
|
|
3154
|
-
* - GitHub web: https://github.com/user/repo/tree/...
|
|
3155
|
-
* - HTTP/OSS: https://example.com/skill.tar.gz
|
|
3156
|
-
* - Registry shorthand: github:user/repo, gitlab:org/repo
|
|
3157
|
-
*/ static isRegistryRef(ref) {
|
|
3158
|
-
// 排除 Git SSH 格式 (git@...)
|
|
3159
|
-
if (ref.startsWith('git@') || ref.startsWith('git://')) return false;
|
|
3160
|
-
// 排除 .git 结尾的 URL
|
|
3161
|
-
if (ref.includes('.git')) return false;
|
|
3162
|
-
// 排除 HTTP/HTTPS/OSS URL
|
|
3163
|
-
if (ref.startsWith('http://') || ref.startsWith('https://') || ref.startsWith('oss://') || ref.startsWith('s3://')) return false;
|
|
3164
|
-
// 排除 registry shorthand 格式 (github:, gitlab:, custom.com:)
|
|
3165
|
-
// 这类格式是 "registry:owner/repo" 而不是 "@scope/name"
|
|
3166
|
-
if (/^[a-zA-Z0-9.-]+:[^@]/.test(ref)) return false;
|
|
3167
|
-
// 检查是否是 @scope/name 格式(私有 registry)
|
|
3168
|
-
if (ref.startsWith('@') && ref.includes('/')) {
|
|
3169
|
-
// @scope/name 或 @scope/name@version
|
|
3170
|
-
const scopeNamePattern = /^@[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
|
|
3171
|
-
return scopeNamePattern.test(ref);
|
|
3172
|
-
}
|
|
3173
|
-
// 检查是否是简单的 name 或 name@version 格式(公共 registry)
|
|
3174
|
-
// 简单名称只包含字母、数字、连字符、下划线和点
|
|
3175
|
-
const namePattern = /^[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
|
|
3176
|
-
return namePattern.test(ref);
|
|
3177
|
-
}
|
|
3178
|
-
/**
|
|
3179
|
-
* Resolve a registry skill reference
|
|
3180
|
-
*
|
|
3181
|
-
* @param ref - Skill reference (e.g., "@kanyun/planning-with-files@2.4.5" or "my-skill@latest")
|
|
3182
|
-
* @returns Resolved skill information including downloaded tarball
|
|
3183
|
-
*
|
|
3184
|
-
* @example
|
|
3185
|
-
* const result = await resolver.resolve('@kanyun/planning-with-files@2.4.5');
|
|
3186
|
-
* console.log(result.shortName); // 'planning-with-files'
|
|
3187
|
-
* console.log(result.version); // '2.4.5'
|
|
3188
|
-
*/ async resolve(ref) {
|
|
3189
|
-
// 1. 解析 skill 标识
|
|
3190
|
-
const parsed = parseSkillIdentifier(ref);
|
|
3191
|
-
const shortName = getShortName(parsed.fullName);
|
|
3192
|
-
// 2. 获取 registry URL
|
|
3193
|
-
const registryUrl = getRegistryUrl(parsed.scope);
|
|
3194
|
-
// 3. 创建 client 并解析版本
|
|
3195
|
-
const client = new RegistryClient({
|
|
3196
|
-
registry: registryUrl,
|
|
3197
|
-
apiPrefix: getApiPrefix(registryUrl)
|
|
3198
|
-
});
|
|
3199
|
-
const version = await client.resolveVersion(parsed.fullName, parsed.version);
|
|
3200
|
-
// 4. 下载 tarball
|
|
3201
|
-
const { tarball, integrity } = await client.downloadSkill(parsed.fullName, version);
|
|
3202
|
-
// 5. 验证 integrity
|
|
3203
|
-
const isValid = RegistryClient.verifyIntegrity(tarball, integrity);
|
|
3204
|
-
if (!isValid) throw new Error(`Integrity verification failed for ${ref}`);
|
|
3205
|
-
return {
|
|
3206
|
-
parsed,
|
|
3207
|
-
shortName,
|
|
3208
|
-
version,
|
|
3209
|
-
registryUrl,
|
|
3210
|
-
tarball,
|
|
3211
|
-
integrity
|
|
3212
|
-
};
|
|
3213
|
-
}
|
|
3214
|
-
/**
|
|
3215
|
-
* Extract tarball to a target directory
|
|
3216
|
-
*
|
|
3217
|
-
* @param tarball - Tarball buffer
|
|
3218
|
-
* @param destDir - Destination directory
|
|
3219
|
-
* @returns Path to the extracted skill directory
|
|
3220
|
-
*/ async extract(tarball, destDir) {
|
|
3221
|
-
await extractTarballBuffer(tarball, destDir);
|
|
3222
|
-
// 获取顶层目录名(即 skill 名称)
|
|
3223
|
-
const topDir = await getTarballTopDir(tarball);
|
|
3224
|
-
if (topDir) return `${destDir}/${topDir}`;
|
|
3225
|
-
return destDir;
|
|
3226
|
-
}
|
|
3227
|
-
}
|
|
3228
|
-
/**
|
|
3229
|
-
* Skill Parser - SKILL.md parser
|
|
3230
|
-
*
|
|
3231
|
-
* Following agentskills.io specification: https://agentskills.io/specification
|
|
3232
|
-
*
|
|
3233
|
-
* SKILL.md format requirements:
|
|
3234
|
-
* - YAML frontmatter containing name and description (required)
|
|
3235
|
-
* - name: max 64 characters, lowercase letters, numbers, hyphens
|
|
3236
|
-
* - description: max 1024 characters
|
|
3237
|
-
* - Optional fields: license, compatibility, metadata, allowed-tools
|
|
3238
|
-
*/ /**
|
|
3239
|
-
* Skill validation error
|
|
3240
|
-
*/ class SkillValidationError extends Error {
|
|
3241
|
-
field;
|
|
3242
|
-
constructor(message, field){
|
|
3243
|
-
super(message), this.field = field;
|
|
3244
|
-
this.name = 'SkillValidationError';
|
|
3245
|
-
}
|
|
3246
|
-
}
|
|
3247
|
-
/**
|
|
3248
|
-
* Simple YAML frontmatter parser
|
|
3249
|
-
* Parses --- delimited YAML header
|
|
3250
|
-
*
|
|
3251
|
-
* Supports:
|
|
3252
|
-
* - Basic key: value pairs
|
|
3253
|
-
* - Multiline strings (| and >)
|
|
3254
|
-
* - Nested objects (one level deep, for metadata field)
|
|
3255
|
-
*/ function parseFrontmatter(content) {
|
|
3256
|
-
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
3257
|
-
const match = content.match(frontmatterRegex);
|
|
3258
|
-
if (!match) return {
|
|
3259
|
-
data: {},
|
|
3260
|
-
content
|
|
3261
|
-
};
|
|
3262
|
-
const yamlContent = match[1];
|
|
3263
|
-
const markdownContent = match[2];
|
|
3264
|
-
// Simple YAML parsing (supports basic key: value format and one level of nesting)
|
|
3265
|
-
const data = {};
|
|
3266
|
-
const lines = yamlContent.split('\n');
|
|
3267
|
-
let currentKey = '';
|
|
3268
|
-
let currentValue = '';
|
|
3269
|
-
let inMultiline = false;
|
|
3270
|
-
let inNestedObject = false;
|
|
3271
|
-
let nestedObject = {};
|
|
3272
|
-
for (const line of lines){
|
|
3273
|
-
const trimmedLine = line.trim();
|
|
3274
|
-
if (!trimmedLine || trimmedLine.startsWith('#')) continue;
|
|
3275
|
-
// Check if it's a nested key: value pair (indented with 2 spaces)
|
|
3276
|
-
const nestedMatch = line.match(/^ {2}([a-zA-Z_-]+):\s*(.*)$/);
|
|
3277
|
-
if (nestedMatch && inNestedObject) {
|
|
3278
|
-
const [, nestedKey, nestedValue] = nestedMatch;
|
|
3279
|
-
nestedObject[nestedKey] = parseYamlValue(nestedValue.trim());
|
|
3280
|
-
continue;
|
|
3281
|
-
}
|
|
3282
|
-
// Check if it's a new key: value pair (no indent)
|
|
3283
|
-
const keyValueMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
|
|
3284
|
-
if (keyValueMatch && !inMultiline) {
|
|
3285
|
-
// Save previous nested object if any
|
|
3286
|
-
if (inNestedObject && currentKey) {
|
|
3287
|
-
data[currentKey] = nestedObject;
|
|
3288
|
-
nestedObject = {};
|
|
3289
|
-
inNestedObject = false;
|
|
3290
|
-
}
|
|
3291
|
-
// Save previous value
|
|
3292
|
-
if (currentKey && !inNestedObject) data[currentKey] = parseYamlValue(currentValue.trim());
|
|
3293
|
-
currentKey = keyValueMatch[1];
|
|
3294
|
-
currentValue = keyValueMatch[2];
|
|
3295
|
-
// Check if it's start of multiline string
|
|
3296
|
-
if ('|' === currentValue || '>' === currentValue) {
|
|
3297
|
-
inMultiline = true;
|
|
3298
|
-
currentValue = '';
|
|
3299
|
-
} else if ('' === currentValue) {
|
|
3300
|
-
// Empty value - might be start of nested object
|
|
3301
|
-
inNestedObject = true;
|
|
3302
|
-
nestedObject = {};
|
|
3303
|
-
}
|
|
3304
|
-
} else if (inMultiline && line.startsWith(' ')) // Multiline string continuation
|
|
3305
|
-
currentValue += (currentValue ? '\n' : '') + line.slice(2);
|
|
3306
|
-
else if (inMultiline && !line.startsWith(' ')) {
|
|
3307
|
-
// Multiline string end
|
|
3308
|
-
inMultiline = false;
|
|
3309
|
-
data[currentKey] = currentValue.trim();
|
|
3310
|
-
// Try to parse new line
|
|
3311
|
-
const newKeyMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
|
|
3312
|
-
if (newKeyMatch) {
|
|
3313
|
-
currentKey = newKeyMatch[1];
|
|
3314
|
-
currentValue = newKeyMatch[2];
|
|
3315
|
-
}
|
|
3316
|
-
}
|
|
3317
|
-
}
|
|
3318
|
-
// Save last value
|
|
3319
|
-
if (inNestedObject && currentKey) data[currentKey] = nestedObject;
|
|
3320
|
-
else if (currentKey) data[currentKey] = parseYamlValue(currentValue.trim());
|
|
3321
|
-
return {
|
|
3322
|
-
data,
|
|
3323
|
-
content: markdownContent
|
|
3324
|
-
};
|
|
3325
|
-
}
|
|
3326
|
-
/**
|
|
3327
|
-
* Parse YAML value
|
|
3328
|
-
*/ function parseYamlValue(value) {
|
|
3329
|
-
if (!value) return '';
|
|
3330
|
-
// Boolean value
|
|
3331
|
-
if ('true' === value) return true;
|
|
3332
|
-
if ('false' === value) return false;
|
|
3333
|
-
// Number
|
|
3334
|
-
if (/^-?\d+$/.test(value)) return parseInt(value, 10);
|
|
3335
|
-
if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
|
|
3336
|
-
// Remove quotes
|
|
3337
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
3338
|
-
return value;
|
|
3339
|
-
}
|
|
3340
|
-
/**
|
|
3341
|
-
* Validate skill name format
|
|
3342
|
-
*
|
|
3343
|
-
* Specification requirements:
|
|
3344
|
-
* - Max 64 characters
|
|
3345
|
-
* - Only lowercase letters, numbers, hyphens allowed
|
|
3346
|
-
* - Cannot start or end with hyphen
|
|
3347
|
-
* - Cannot contain consecutive hyphens
|
|
3348
|
-
*/ function validateSkillName(name) {
|
|
3349
|
-
if (!name) throw new SkillValidationError('Skill name is required', 'name');
|
|
3350
|
-
if (name.length > 64) throw new SkillValidationError('Skill name must be at most 64 characters', 'name');
|
|
3351
|
-
if (!/^[a-z0-9]/.test(name)) throw new SkillValidationError('Skill name must start with a lowercase letter or number', 'name');
|
|
3352
|
-
if (!/[a-z0-9]$/.test(name)) throw new SkillValidationError('Skill name must end with a lowercase letter or number', 'name');
|
|
3353
|
-
if (/--/.test(name)) throw new SkillValidationError('Skill name cannot contain consecutive hyphens', 'name');
|
|
3354
|
-
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');
|
|
3355
|
-
// Single character name
|
|
3356
|
-
if (1 === name.length && !/^[a-z0-9]$/.test(name)) throw new SkillValidationError('Single character skill name must be a lowercase letter or number', 'name');
|
|
3596
|
+
// Get top-level directory from first entry
|
|
3597
|
+
const parts = header.name.split('/');
|
|
3598
|
+
if (parts.length > 0 && parts[0]) topDir = parts[0];
|
|
3599
|
+
}
|
|
3600
|
+
stream.resume();
|
|
3601
|
+
next();
|
|
3602
|
+
});
|
|
3603
|
+
extractor.on('finish', ()=>{
|
|
3604
|
+
resolve(topDir);
|
|
3605
|
+
});
|
|
3606
|
+
extractor.on('error', (err)=>{
|
|
3607
|
+
reject(new Error(`Failed to read tarball: ${err.message}`));
|
|
3608
|
+
});
|
|
3609
|
+
gunzip.on('error', (err)=>{
|
|
3610
|
+
reject(new Error(`Failed to decompress tarball: ${err.message}`));
|
|
3611
|
+
});
|
|
3612
|
+
gunzip.pipe(extractor);
|
|
3613
|
+
gunzip.end(tarball);
|
|
3614
|
+
});
|
|
3357
3615
|
}
|
|
3358
3616
|
/**
|
|
3359
|
-
*
|
|
3617
|
+
* Registry Resolver (Step 5.1)
|
|
3360
3618
|
*
|
|
3361
|
-
*
|
|
3362
|
-
* -
|
|
3363
|
-
* -
|
|
3364
|
-
*/ function validateSkillDescription(description) {
|
|
3365
|
-
if (!description) throw new SkillValidationError('Skill description is required', 'description');
|
|
3366
|
-
if (description.length > 1024) throw new SkillValidationError('Skill description must be at most 1024 characters', 'description');
|
|
3367
|
-
// Note: angle brackets are allowed per agentskills.io spec
|
|
3368
|
-
}
|
|
3369
|
-
/**
|
|
3370
|
-
* Parse SKILL.md content
|
|
3619
|
+
* Resolves skill references from npm-style registries:
|
|
3620
|
+
* - Private registry: @scope/name[@version] (e.g., @kanyun/planning-with-files@2.4.5)
|
|
3621
|
+
* - Public registry: name[@version] (e.g., my-skill@1.0.0)
|
|
3371
3622
|
*
|
|
3372
|
-
*
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
if (
|
|
3395
|
-
|
|
3396
|
-
|
|
3623
|
+
* Uses RegistryClient to download and verify skills.
|
|
3624
|
+
*/ // ============================================================================
|
|
3625
|
+
// RegistryResolver Class
|
|
3626
|
+
// ============================================================================
|
|
3627
|
+
class RegistryResolver {
|
|
3628
|
+
/**
|
|
3629
|
+
* Check if a reference is a registry source (not Git or HTTP)
|
|
3630
|
+
*
|
|
3631
|
+
* Registry formats:
|
|
3632
|
+
* - @scope/name[@version] - private registry
|
|
3633
|
+
* - name[@version] - public registry (if not matching other formats)
|
|
3634
|
+
*
|
|
3635
|
+
* Explicitly excluded:
|
|
3636
|
+
* - Git SSH: git@github.com:user/repo.git
|
|
3637
|
+
* - Git HTTPS: https://github.com/user/repo.git
|
|
3638
|
+
* - GitHub web: https://github.com/user/repo/tree/...
|
|
3639
|
+
* - HTTP/OSS: https://example.com/skill.tar.gz
|
|
3640
|
+
* - Registry shorthand: github:user/repo, gitlab:org/repo
|
|
3641
|
+
*/ static isRegistryRef(ref) {
|
|
3642
|
+
// Exclude Git SSH format (git@...)
|
|
3643
|
+
if (ref.startsWith('git@') || ref.startsWith('git://')) return false;
|
|
3644
|
+
// Exclude URLs ending with .git
|
|
3645
|
+
if (ref.includes('.git')) return false;
|
|
3646
|
+
// Exclude HTTP/HTTPS/OSS URLs
|
|
3647
|
+
if (ref.startsWith('http://') || ref.startsWith('https://') || ref.startsWith('oss://') || ref.startsWith('s3://')) return false;
|
|
3648
|
+
// Exclude registry shorthand format (github:, gitlab:, custom.com:)
|
|
3649
|
+
// These follow "registry:owner/repo" pattern, not "@scope/name"
|
|
3650
|
+
if (/^[a-zA-Z0-9.-]+:[^@]/.test(ref)) return false;
|
|
3651
|
+
// Check for @scope/name format (private registry)
|
|
3652
|
+
if (ref.startsWith('@') && ref.includes('/')) {
|
|
3653
|
+
// @scope/name or @scope/name@version
|
|
3654
|
+
const scopeNamePattern = /^@[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
|
|
3655
|
+
return scopeNamePattern.test(ref);
|
|
3397
3656
|
}
|
|
3657
|
+
// Check for simple name or name@version format (public registry)
|
|
3658
|
+
// Simple names contain only letters, digits, hyphens, underscores, and dots
|
|
3659
|
+
const namePattern = /^[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
|
|
3660
|
+
return namePattern.test(ref);
|
|
3661
|
+
}
|
|
3662
|
+
/**
|
|
3663
|
+
* Resolve a registry skill reference
|
|
3664
|
+
*
|
|
3665
|
+
* @param ref - Skill reference (e.g., "@kanyun/planning-with-files@2.4.5" or "my-skill@latest")
|
|
3666
|
+
* @param overrideRegistryUrl - Optional registry URL override (bypasses scope-based lookup)
|
|
3667
|
+
* @returns Resolved skill information including downloaded tarball
|
|
3668
|
+
*
|
|
3669
|
+
* @example
|
|
3670
|
+
* const result = await resolver.resolve('@kanyun/planning-with-files@2.4.5');
|
|
3671
|
+
* console.log(result.shortName); // 'planning-with-files'
|
|
3672
|
+
* console.log(result.version); // '2.4.5'
|
|
3673
|
+
*/ async resolve(ref, overrideRegistryUrl) {
|
|
3674
|
+
// 1. Parse skill identifier
|
|
3675
|
+
const parsed = parseSkillIdentifier(ref);
|
|
3676
|
+
const shortName = getShortName(parsed.fullName);
|
|
3677
|
+
// 2. Get registry URL (CLI override takes precedence)
|
|
3678
|
+
const registryUrl = overrideRegistryUrl || getRegistryUrl(parsed.scope);
|
|
3679
|
+
// 3. Create client and resolve version
|
|
3680
|
+
const client = new RegistryClient({
|
|
3681
|
+
registry: registryUrl
|
|
3682
|
+
});
|
|
3683
|
+
const version = await client.resolveVersion(parsed.fullName, parsed.version);
|
|
3684
|
+
// 4. Download tarball
|
|
3685
|
+
const { tarball, integrity } = await client.downloadSkill(parsed.fullName, version);
|
|
3686
|
+
// 5. Verify integrity
|
|
3687
|
+
const isValid = RegistryClient.verifyIntegrity(tarball, integrity);
|
|
3688
|
+
if (!isValid) throw new Error(`Integrity verification failed for ${ref}`);
|
|
3398
3689
|
return {
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
version
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
allowedTools,
|
|
3406
|
-
content: body,
|
|
3407
|
-
rawContent: content
|
|
3690
|
+
parsed,
|
|
3691
|
+
shortName,
|
|
3692
|
+
version,
|
|
3693
|
+
registryUrl,
|
|
3694
|
+
tarball,
|
|
3695
|
+
integrity
|
|
3408
3696
|
};
|
|
3409
|
-
} catch (error) {
|
|
3410
|
-
if (error instanceof SkillValidationError) throw error;
|
|
3411
|
-
if (strict) throw new SkillValidationError(`Failed to parse SKILL.md: ${error}`);
|
|
3412
|
-
return null;
|
|
3413
|
-
}
|
|
3414
|
-
}
|
|
3415
|
-
/**
|
|
3416
|
-
* Parse SKILL.md from file path
|
|
3417
|
-
*/ function skill_parser_parseSkillMdFile(filePath, options = {}) {
|
|
3418
|
-
if (!external_node_fs_.existsSync(filePath)) {
|
|
3419
|
-
if (options.strict) throw new SkillValidationError(`SKILL.md not found: ${filePath}`);
|
|
3420
|
-
return null;
|
|
3421
3697
|
}
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
|
|
3435
|
-
if (!external_node_fs_.existsSync(skillMdPath)) return false;
|
|
3436
|
-
try {
|
|
3437
|
-
const skill = skill_parser_parseSkillMdFile(skillMdPath);
|
|
3438
|
-
return null !== skill;
|
|
3439
|
-
} catch {
|
|
3440
|
-
return false;
|
|
3698
|
+
/**
|
|
3699
|
+
* Extract tarball to a target directory
|
|
3700
|
+
*
|
|
3701
|
+
* @param tarball - Tarball buffer
|
|
3702
|
+
* @param destDir - Destination directory
|
|
3703
|
+
* @returns Path to the extracted skill directory
|
|
3704
|
+
*/ async extract(tarball, destDir) {
|
|
3705
|
+
await extractTarballBuffer(tarball, destDir);
|
|
3706
|
+
// Get top-level directory name (i.e. skill name)
|
|
3707
|
+
const topDir = await getTarballTopDir(tarball);
|
|
3708
|
+
if (topDir) return `${destDir}/${topDir}`;
|
|
3709
|
+
return destDir;
|
|
3441
3710
|
}
|
|
3442
3711
|
}
|
|
3443
|
-
/**
|
|
3444
|
-
* Generate SKILL.md content
|
|
3445
|
-
*/ function generateSkillMd(skill) {
|
|
3446
|
-
const frontmatter = [
|
|
3447
|
-
'---'
|
|
3448
|
-
];
|
|
3449
|
-
frontmatter.push(`name: ${skill.name}`);
|
|
3450
|
-
frontmatter.push(`description: ${skill.description}`);
|
|
3451
|
-
if (skill.license) frontmatter.push(`license: ${skill.license}`);
|
|
3452
|
-
if (skill.compatibility) frontmatter.push(`compatibility: ${skill.compatibility}`);
|
|
3453
|
-
if (skill.allowedTools && skill.allowedTools.length > 0) frontmatter.push(`allowed-tools: ${skill.allowedTools.join(' ')}`);
|
|
3454
|
-
frontmatter.push('---');
|
|
3455
|
-
frontmatter.push('');
|
|
3456
|
-
return frontmatter.join('\n') + skill.content;
|
|
3457
|
-
}
|
|
3458
3712
|
/**
|
|
3459
3713
|
* SkillManager - Core Skill management class
|
|
3460
3714
|
*
|
|
@@ -3936,12 +4190,112 @@ class RegistryResolver {
|
|
|
3936
4190
|
* @param options - Installation options
|
|
3937
4191
|
*/ async installToAgents(ref, targetAgents, options = {}) {
|
|
3938
4192
|
// Detect source type and delegate to appropriate installer
|
|
3939
|
-
// Priority: Registry > HTTP > Git (registry
|
|
4193
|
+
// Priority: Registry > HTTP > Git (registry first, as its format is most constrained)
|
|
3940
4194
|
if (this.isRegistrySource(ref)) return this.installToAgentsFromRegistry(ref, targetAgents, options);
|
|
3941
4195
|
if (this.isHttpSource(ref)) return this.installToAgentsFromHttp(ref, targetAgents, options);
|
|
3942
4196
|
return this.installToAgentsFromGit(ref, targetAgents, options);
|
|
3943
4197
|
}
|
|
3944
4198
|
/**
|
|
4199
|
+
* Multi-skill install: discover skills in a Git repo and install selected ones (or list only).
|
|
4200
|
+
* Only Git references are supported (including https://github.com/...); registry refs are not.
|
|
4201
|
+
*
|
|
4202
|
+
* @param ref - Git skill reference (e.g. github:user/repo@v1.0.0 or https://github.com/user/repo); any #fragment is stripped for resolution
|
|
4203
|
+
* @param skillNames - If non-empty, install only these skills (by SKILL.md name). If empty and !listOnly, install all.
|
|
4204
|
+
* @param targetAgents - Target agents
|
|
4205
|
+
* @param options - Install options; listOnly: true means discover and return skills without installing
|
|
4206
|
+
*/ async installSkillsFromRepo(ref, skillNames, targetAgents, options = {}) {
|
|
4207
|
+
const { listOnly = false, force = false, save = true, mode = 'symlink' } = options;
|
|
4208
|
+
const refForResolve = ref.replace(/#.*$/, '').trim();
|
|
4209
|
+
const resolved = await this.resolver.resolve(refForResolve);
|
|
4210
|
+
const { parsed, repoUrl } = resolved;
|
|
4211
|
+
const gitRef = resolved.ref;
|
|
4212
|
+
let cacheResult = await this.cache.get(parsed, gitRef);
|
|
4213
|
+
if (!cacheResult) {
|
|
4214
|
+
logger_logger.debug(`Caching from ${repoUrl}@${gitRef}`);
|
|
4215
|
+
cacheResult = await this.cache.cache(repoUrl, parsed, gitRef, gitRef);
|
|
4216
|
+
}
|
|
4217
|
+
const cachePath = this.cache.getCachePath(parsed, gitRef);
|
|
4218
|
+
const discovered = discoverSkillsInDir(cachePath);
|
|
4219
|
+
if (0 === discovered.length) throw new Error('No valid skills found. Skills require a SKILL.md with name and description.');
|
|
4220
|
+
if (listOnly) return {
|
|
4221
|
+
listOnly: true,
|
|
4222
|
+
skills: discovered
|
|
4223
|
+
};
|
|
4224
|
+
const selected = skillNames.length > 0 ? filterSkillsByName(discovered, skillNames) : discovered;
|
|
4225
|
+
if (skillNames.length > 0 && 0 === selected.length) {
|
|
4226
|
+
const available = discovered.map((s)=>s.name).join(', ');
|
|
4227
|
+
throw new Error(`No matching skills found for: ${skillNames.join(', ')}. Available skills: ${available}`);
|
|
4228
|
+
}
|
|
4229
|
+
const baseRefForSave = this.config.normalizeSkillRef(refForResolve);
|
|
4230
|
+
const defaults = this.config.getDefaults();
|
|
4231
|
+
// Only pass custom installDir to Installer; default '.skills' should use
|
|
4232
|
+
// the Installer's built-in canonical path (.agents/skills/)
|
|
4233
|
+
const customInstallDir = '.skills' !== defaults.installDir ? defaults.installDir : void 0;
|
|
4234
|
+
const installer = new Installer({
|
|
4235
|
+
cwd: this.projectRoot,
|
|
4236
|
+
global: this.isGlobal,
|
|
4237
|
+
installDir: customInstallDir
|
|
4238
|
+
});
|
|
4239
|
+
const installed = [];
|
|
4240
|
+
const skipped = [];
|
|
4241
|
+
const skillSource = `${parsed.registry}:${parsed.owner}/${parsed.repo}${parsed.subPath ? `/${parsed.subPath}` : ''}`;
|
|
4242
|
+
for (const skillInfo of selected){
|
|
4243
|
+
const semanticVersion = skillInfo.version ?? gitRef;
|
|
4244
|
+
// Skip already-installed skills unless --force is set
|
|
4245
|
+
if (!force) {
|
|
4246
|
+
const existingSkill = this.getInstalledSkill(skillInfo.name);
|
|
4247
|
+
if (existingSkill) {
|
|
4248
|
+
const locked = this.lockManager.get(skillInfo.name);
|
|
4249
|
+
const lockedRef = locked?.ref || locked?.version;
|
|
4250
|
+
if (lockedRef === gitRef) {
|
|
4251
|
+
const reason = `already installed at ${gitRef}`;
|
|
4252
|
+
logger_logger.info(`${skillInfo.name}@${gitRef} is already installed, skipping`);
|
|
4253
|
+
skipped.push({
|
|
4254
|
+
name: skillInfo.name,
|
|
4255
|
+
reason
|
|
4256
|
+
});
|
|
4257
|
+
continue;
|
|
4258
|
+
}
|
|
4259
|
+
// Different version installed — allow upgrade without --force
|
|
4260
|
+
// Only skip when the exact same ref is already locked
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
logger_logger["package"](`Installing ${skillInfo.name}@${gitRef} to ${targetAgents.length} agent(s)...`);
|
|
4264
|
+
// Note: force is handled at the SkillManager level (skip-if-installed check above).
|
|
4265
|
+
// The Installer always overwrites (remove + copy), so no force flag is needed there.
|
|
4266
|
+
const results = await installer.installToAgents(skillInfo.dirPath, skillInfo.name, targetAgents, {
|
|
4267
|
+
mode: mode
|
|
4268
|
+
});
|
|
4269
|
+
if (!this.isGlobal) this.lockManager.lockSkill(skillInfo.name, {
|
|
4270
|
+
source: skillSource,
|
|
4271
|
+
version: semanticVersion,
|
|
4272
|
+
ref: gitRef,
|
|
4273
|
+
resolved: repoUrl,
|
|
4274
|
+
commit: cacheResult.commit
|
|
4275
|
+
});
|
|
4276
|
+
if (!this.isGlobal && save) {
|
|
4277
|
+
this.config.ensureExists();
|
|
4278
|
+
this.config.addSkill(skillInfo.name, `${baseRefForSave}#${skillInfo.name}`);
|
|
4279
|
+
}
|
|
4280
|
+
const successCount = Array.from(results.values()).filter((r)=>r.success).length;
|
|
4281
|
+
logger_logger.success(`Installed ${skillInfo.name}@${semanticVersion} to ${successCount} agent(s)`);
|
|
4282
|
+
installed.push({
|
|
4283
|
+
skill: {
|
|
4284
|
+
name: skillInfo.name,
|
|
4285
|
+
path: skillInfo.dirPath,
|
|
4286
|
+
version: semanticVersion,
|
|
4287
|
+
source: skillSource
|
|
4288
|
+
},
|
|
4289
|
+
results
|
|
4290
|
+
});
|
|
4291
|
+
}
|
|
4292
|
+
return {
|
|
4293
|
+
listOnly: false,
|
|
4294
|
+
installed,
|
|
4295
|
+
skipped
|
|
4296
|
+
};
|
|
4297
|
+
}
|
|
4298
|
+
/**
|
|
3945
4299
|
* Install skill from Git to multiple agents
|
|
3946
4300
|
*/ async installToAgentsFromGit(ref, targetAgents, options = {}) {
|
|
3947
4301
|
const { save = true, mode = 'symlink' } = options;
|
|
@@ -4082,14 +4436,13 @@ class RegistryResolver {
|
|
|
4082
4436
|
* - Web-published skills (github/gitlab/oss_url/custom_url/local)
|
|
4083
4437
|
*/ async installToAgentsFromRegistry(ref, targetAgents, options = {}) {
|
|
4084
4438
|
const { force = false, save = true, mode = 'symlink' } = options;
|
|
4085
|
-
//
|
|
4439
|
+
// Parse skill identifier and resolve registry URL once (single source of truth)
|
|
4086
4440
|
const parsed = parseSkillIdentifier(ref);
|
|
4087
|
-
const registryUrl = getRegistryUrl(parsed.scope);
|
|
4441
|
+
const registryUrl = options.registry || getRegistryUrl(parsed.scope);
|
|
4088
4442
|
const client = new RegistryClient({
|
|
4089
|
-
registry: registryUrl
|
|
4090
|
-
apiPrefix: getApiPrefix(registryUrl)
|
|
4443
|
+
registry: registryUrl
|
|
4091
4444
|
});
|
|
4092
|
-
//
|
|
4445
|
+
// Query skill info to determine source_type
|
|
4093
4446
|
let skillInfo;
|
|
4094
4447
|
try {
|
|
4095
4448
|
skillInfo = await client.getSkillInfo(parsed.fullName);
|
|
@@ -4100,12 +4453,15 @@ class RegistryResolver {
|
|
|
4100
4453
|
};
|
|
4101
4454
|
else throw error;
|
|
4102
4455
|
}
|
|
4103
|
-
//
|
|
4456
|
+
// Branch based on source_type (pass resolved registryUrl via options to avoid re-computation)
|
|
4104
4457
|
const sourceType = skillInfo.source_type;
|
|
4105
|
-
if (sourceType && 'registry' !== sourceType) return this.installFromWebPublished(skillInfo, parsed, targetAgents,
|
|
4106
|
-
|
|
4458
|
+
if (sourceType && 'registry' !== sourceType) return this.installFromWebPublished(skillInfo, parsed, targetAgents, {
|
|
4459
|
+
...options,
|
|
4460
|
+
registry: registryUrl
|
|
4461
|
+
});
|
|
4462
|
+
// 1. Resolve registry skill (pass pre-resolved registryUrl)
|
|
4107
4463
|
logger_logger["package"](`Resolving ${ref} from registry...`);
|
|
4108
|
-
const resolved = await this.registryResolver.resolve(ref);
|
|
4464
|
+
const resolved = await this.registryResolver.resolve(ref, registryUrl);
|
|
4109
4465
|
const { shortName, version, registryUrl: resolvedRegistryUrl, tarball, parsed: resolvedParsed } = resolved;
|
|
4110
4466
|
// 2. Check if already installed (skip if --force)
|
|
4111
4467
|
const skillPath = this.getSkillPath(shortName);
|
|
@@ -4144,101 +4500,157 @@ class RegistryResolver {
|
|
|
4144
4500
|
};
|
|
4145
4501
|
}
|
|
4146
4502
|
logger_logger["package"](`Installing ${shortName}@${version} from ${resolvedRegistryUrl} to ${targetAgents.length} agent(s)...`);
|
|
4147
|
-
// 3. Create temp directory for extraction
|
|
4503
|
+
// 3. Create temp directory for extraction (clean stale files first)
|
|
4148
4504
|
const tempDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cache.getCacheDir(), 'registry-temp', `${shortName}-${version}`);
|
|
4505
|
+
await remove(tempDir);
|
|
4149
4506
|
await ensureDir(tempDir);
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
this.
|
|
4175
|
-
|
|
4176
|
-
|
|
4507
|
+
try {
|
|
4508
|
+
// 4. Extract tarball
|
|
4509
|
+
const extractedPath = await this.registryResolver.extract(tarball, tempDir);
|
|
4510
|
+
logger_logger.debug(`Extracted to ${extractedPath}`);
|
|
4511
|
+
// 5. Create Installer with custom installDir from config
|
|
4512
|
+
const defaults = this.config.getDefaults();
|
|
4513
|
+
const installer = new Installer({
|
|
4514
|
+
cwd: this.projectRoot,
|
|
4515
|
+
global: this.isGlobal,
|
|
4516
|
+
installDir: defaults.installDir
|
|
4517
|
+
});
|
|
4518
|
+
// 6. Install to all target agents
|
|
4519
|
+
const results = await installer.installToAgents(extractedPath, shortName, targetAgents, {
|
|
4520
|
+
mode: mode
|
|
4521
|
+
});
|
|
4522
|
+
// 7. Update lock file (project mode only)
|
|
4523
|
+
if (!this.isGlobal) this.lockManager.lockSkill(shortName, {
|
|
4524
|
+
source: `registry:${resolvedParsed.fullName}`,
|
|
4525
|
+
version,
|
|
4526
|
+
ref: version,
|
|
4527
|
+
resolved: resolvedRegistryUrl,
|
|
4528
|
+
commit: resolved.integrity
|
|
4529
|
+
});
|
|
4530
|
+
// 8. Update skills.json (project mode only)
|
|
4531
|
+
if (!this.isGlobal && save) {
|
|
4532
|
+
this.config.ensureExists();
|
|
4533
|
+
// Save with full name for registry skills
|
|
4534
|
+
this.config.addSkill(shortName, ref);
|
|
4535
|
+
}
|
|
4536
|
+
// 9. Count results and log
|
|
4537
|
+
const successCount = Array.from(results.values()).filter((r)=>r.success).length;
|
|
4538
|
+
const failCount = results.size - successCount;
|
|
4539
|
+
if (0 === failCount) logger_logger.success(`Installed ${shortName}@${version} to ${successCount} agent(s)`);
|
|
4540
|
+
else logger_logger.warn(`Installed ${shortName}@${version} to ${successCount} agent(s), ${failCount} failed`);
|
|
4541
|
+
// 10. Build the InstalledSkill to return
|
|
4542
|
+
const skill = {
|
|
4543
|
+
name: shortName,
|
|
4544
|
+
path: extractedPath,
|
|
4545
|
+
version,
|
|
4546
|
+
source: `registry:${resolvedParsed.fullName}`
|
|
4547
|
+
};
|
|
4548
|
+
return {
|
|
4549
|
+
skill,
|
|
4550
|
+
results
|
|
4551
|
+
};
|
|
4552
|
+
} finally{
|
|
4553
|
+
// Clean up temp directory after installation
|
|
4554
|
+
await remove(tempDir);
|
|
4177
4555
|
}
|
|
4178
|
-
// 9. Count results and log
|
|
4179
|
-
const successCount = Array.from(results.values()).filter((r)=>r.success).length;
|
|
4180
|
-
const failCount = results.size - successCount;
|
|
4181
|
-
if (0 === failCount) logger_logger.success(`Installed ${shortName}@${version} to ${successCount} agent(s)`);
|
|
4182
|
-
else logger_logger.warn(`Installed ${shortName}@${version} to ${successCount} agent(s), ${failCount} failed`);
|
|
4183
|
-
// 9. Build the InstalledSkill to return
|
|
4184
|
-
const skill = {
|
|
4185
|
-
name: shortName,
|
|
4186
|
-
path: extractedPath,
|
|
4187
|
-
version,
|
|
4188
|
-
source: `registry:${resolvedParsed.fullName}`
|
|
4189
|
-
};
|
|
4190
|
-
return {
|
|
4191
|
-
skill,
|
|
4192
|
-
results
|
|
4193
|
-
};
|
|
4194
4556
|
}
|
|
4195
4557
|
// ============================================================================
|
|
4196
|
-
// Web-published skill installation
|
|
4558
|
+
// Web-published skill installation
|
|
4197
4559
|
// ============================================================================
|
|
4198
4560
|
/**
|
|
4199
|
-
*
|
|
4561
|
+
* Install a web-published skill.
|
|
4200
4562
|
*
|
|
4201
|
-
*
|
|
4202
|
-
*
|
|
4203
|
-
* -
|
|
4204
|
-
* -
|
|
4563
|
+
* Web-published skills do not support versioning. Branches to different
|
|
4564
|
+
* installation logic based on source_type:
|
|
4565
|
+
* - github/gitlab: reuses installToAgentsFromGit
|
|
4566
|
+
* - oss_url/custom_url: reuses installToAgentsFromHttp
|
|
4567
|
+
* - local: downloads tarball via Registry API
|
|
4205
4568
|
*/ async installFromWebPublished(skillInfo, parsed, targetAgents, options = {}) {
|
|
4206
4569
|
const { source_type, source_url } = skillInfo;
|
|
4207
|
-
//
|
|
4570
|
+
// Web-published skills do not support version specifiers
|
|
4208
4571
|
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}`);
|
|
4209
4572
|
if (!source_url) throw new Error(`Missing source_url for web-published skill: ${parsed.fullName}`);
|
|
4210
4573
|
logger_logger["package"](`Installing ${parsed.fullName} from ${source_type} source...`);
|
|
4211
4574
|
switch(source_type){
|
|
4212
4575
|
case 'github':
|
|
4213
4576
|
case 'gitlab':
|
|
4214
|
-
// source_url
|
|
4215
|
-
//
|
|
4577
|
+
// source_url is a full Git URL (includes ref and path)
|
|
4578
|
+
// Reuse existing Git installation logic
|
|
4216
4579
|
return this.installToAgentsFromGit(source_url, targetAgents, options);
|
|
4217
4580
|
case 'oss_url':
|
|
4218
4581
|
case 'custom_url':
|
|
4219
|
-
//
|
|
4582
|
+
// Direct download URL
|
|
4220
4583
|
return this.installToAgentsFromHttp(source_url, targetAgents, options);
|
|
4221
4584
|
case 'local':
|
|
4222
|
-
//
|
|
4223
|
-
return this.installFromRegistryLocal(
|
|
4585
|
+
// Download tarball via Registry API
|
|
4586
|
+
return this.installFromRegistryLocal(parsed, targetAgents, options);
|
|
4224
4587
|
default:
|
|
4225
4588
|
throw new Error(`Unknown source_type: ${source_type}`);
|
|
4226
4589
|
}
|
|
4227
4590
|
}
|
|
4228
4591
|
/**
|
|
4229
|
-
*
|
|
4592
|
+
* Install a skill published via "local folder" mode.
|
|
4230
4593
|
*
|
|
4231
|
-
*
|
|
4232
|
-
*
|
|
4233
|
-
*/ async installFromRegistryLocal(
|
|
4234
|
-
const
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
const
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4594
|
+
* Downloads tarball via RegistryClient (handles 302 redirects to signed OSS URLs),
|
|
4595
|
+
* then extracts and installs using the same flow as registry source_type.
|
|
4596
|
+
*/ async installFromRegistryLocal(parsed, targetAgents, options = {}) {
|
|
4597
|
+
const { save = true, mode = 'symlink' } = options;
|
|
4598
|
+
const registryUrl = options.registry || getRegistryUrl(parsed.scope);
|
|
4599
|
+
const shortName = getShortName(parsed.fullName);
|
|
4600
|
+
const version = 'latest';
|
|
4601
|
+
// Download tarball via RegistryClient (handles auth + 302 redirect to signed URL)
|
|
4602
|
+
const client = new RegistryClient({
|
|
4603
|
+
registry: registryUrl
|
|
4604
|
+
});
|
|
4605
|
+
const { tarball } = await client.downloadSkill(parsed.fullName, version);
|
|
4606
|
+
logger_logger["package"](`Installing ${shortName} from ${registryUrl} to ${targetAgents.length} agent(s)...`);
|
|
4607
|
+
// Extract tarball to temp directory (clean stale files first)
|
|
4608
|
+
const tempDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cache.getCacheDir(), 'registry-temp', `${shortName}-${version}`);
|
|
4609
|
+
await remove(tempDir);
|
|
4610
|
+
await ensureDir(tempDir);
|
|
4611
|
+
try {
|
|
4612
|
+
const extractedPath = await this.registryResolver.extract(tarball, tempDir);
|
|
4613
|
+
logger_logger.debug(`Extracted to ${extractedPath}`);
|
|
4614
|
+
// Install to all target agents
|
|
4615
|
+
const defaults = this.config.getDefaults();
|
|
4616
|
+
const installer = new Installer({
|
|
4617
|
+
cwd: this.projectRoot,
|
|
4618
|
+
global: this.isGlobal,
|
|
4619
|
+
installDir: defaults.installDir
|
|
4620
|
+
});
|
|
4621
|
+
const results = await installer.installToAgents(extractedPath, shortName, targetAgents, {
|
|
4622
|
+
mode: mode
|
|
4623
|
+
});
|
|
4624
|
+
// Get metadata from extracted path
|
|
4625
|
+
const metadata = this.getSkillMetadataFromDir(extractedPath);
|
|
4626
|
+
const skillName = metadata?.name ?? shortName;
|
|
4627
|
+
const semanticVersion = metadata?.version ?? version;
|
|
4628
|
+
// Update lock file (project mode only)
|
|
4629
|
+
if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
|
|
4630
|
+
source: `registry:${parsed.fullName}`,
|
|
4631
|
+
version: semanticVersion,
|
|
4632
|
+
ref: version,
|
|
4633
|
+
resolved: registryUrl,
|
|
4634
|
+
commit: ''
|
|
4635
|
+
});
|
|
4636
|
+
// Update skills.json (project mode only)
|
|
4637
|
+
if (!this.isGlobal && save) {
|
|
4638
|
+
this.config.ensureExists();
|
|
4639
|
+
this.config.addSkill(skillName, parsed.fullName);
|
|
4640
|
+
}
|
|
4641
|
+
return {
|
|
4642
|
+
skill: {
|
|
4643
|
+
name: skillName,
|
|
4644
|
+
path: extractedPath,
|
|
4645
|
+
version: semanticVersion,
|
|
4646
|
+
source: `registry:${parsed.fullName}`
|
|
4647
|
+
},
|
|
4648
|
+
results
|
|
4649
|
+
};
|
|
4650
|
+
} finally{
|
|
4651
|
+
// Clean up temp directory after installation
|
|
4652
|
+
await remove(tempDir);
|
|
4653
|
+
}
|
|
4242
4654
|
}
|
|
4243
4655
|
/**
|
|
4244
4656
|
* Get default target agents
|