reskill 1.7.0 → 1.8.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 +1136 -467
- 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 +42 -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 +958 -515
- package/dist/types/index.d.ts +18 -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
|
|
@@ -1814,10 +2262,24 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1814
2262
|
* - Monorepo: git@github.com:org/repo.git/subpath[@version]
|
|
1815
2263
|
*/ parseRef(ref) {
|
|
1816
2264
|
const raw = ref;
|
|
2265
|
+
// Extract #skillName fragment before any parsing (for both Git URLs and shorthand)
|
|
2266
|
+
let skillName;
|
|
2267
|
+
const hashIndex = ref.indexOf('#');
|
|
2268
|
+
if (hashIndex >= 0) {
|
|
2269
|
+
skillName = ref.slice(hashIndex + 1);
|
|
2270
|
+
ref = ref.slice(0, hashIndex);
|
|
2271
|
+
}
|
|
1817
2272
|
// First check if it's a Git URL (SSH, HTTPS, git://)
|
|
1818
2273
|
// For Git URLs, need special handling for version separator
|
|
1819
2274
|
// Format: git@host:user/repo.git[@version] or git@host:user/repo.git/subpath[@version]
|
|
1820
|
-
if (isGitUrl(ref))
|
|
2275
|
+
if (isGitUrl(ref)) {
|
|
2276
|
+
const parsed = this.parseGitUrlRef(ref);
|
|
2277
|
+
return {
|
|
2278
|
+
...parsed,
|
|
2279
|
+
raw,
|
|
2280
|
+
skillName
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
1821
2283
|
// Standard format parsing for non-Git URLs
|
|
1822
2284
|
let remaining = ref;
|
|
1823
2285
|
let registry = this.defaultRegistry;
|
|
@@ -1836,18 +2298,33 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1836
2298
|
}
|
|
1837
2299
|
// Parse owner/repo and possible subPath
|
|
1838
2300
|
// E.g.: user/repo or org/monorepo/skills/pdf
|
|
2301
|
+
// Also handle GitHub web URL style: owner/repo/tree/branch/path
|
|
1839
2302
|
const parts = remaining.split('/');
|
|
1840
2303
|
if (parts.length < 2) throw new Error(`Invalid skill reference: ${ref}. Expected format: owner/repo[@version]`);
|
|
1841
2304
|
const owner = parts[0];
|
|
1842
2305
|
const repo = parts[1];
|
|
1843
|
-
|
|
2306
|
+
let subPath;
|
|
2307
|
+
// Check for GitHub/GitLab web URL pattern: owner/repo/(tree|blob|raw)/branch/path
|
|
2308
|
+
// e.g. vercel-labs/skills/tree/main/skills/find-skills
|
|
2309
|
+
// Only apply this heuristic when no explicit @version is provided.
|
|
2310
|
+
// With @version, treat tree/blob/raw as literal directory names (standard monorepo subPath).
|
|
2311
|
+
if (parts.length >= 4 && [
|
|
2312
|
+
'tree',
|
|
2313
|
+
'blob',
|
|
2314
|
+
'raw'
|
|
2315
|
+
].includes(parts[2]) && !version) {
|
|
2316
|
+
const branch = parts[3];
|
|
2317
|
+
version = `branch:${branch}`;
|
|
2318
|
+
subPath = parts.length > 4 ? parts.slice(4).join('/') : void 0;
|
|
2319
|
+
} else subPath = parts.length > 2 ? parts.slice(2).join('/') : void 0;
|
|
1844
2320
|
return {
|
|
1845
2321
|
registry,
|
|
1846
2322
|
owner,
|
|
1847
2323
|
repo,
|
|
1848
2324
|
subPath,
|
|
1849
2325
|
version,
|
|
1850
|
-
raw
|
|
2326
|
+
raw,
|
|
2327
|
+
skillName
|
|
1851
2328
|
};
|
|
1852
2329
|
}
|
|
1853
2330
|
/**
|
|
@@ -2069,20 +2546,31 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
2069
2546
|
* Check if a reference is an HTTP/OSS URL (for archive downloads)
|
|
2070
2547
|
*
|
|
2071
2548
|
* Returns true for:
|
|
2072
|
-
* - http:// or https:// URLs
|
|
2073
|
-
* - Explicit oss:// or s3:// protocol URLs
|
|
2549
|
+
* - http:// or https:// URLs with archive file extensions (.tar.gz, .tgz, .zip, .tar)
|
|
2550
|
+
* - Explicit oss:// or s3:// protocol URLs (always treated as archive sources)
|
|
2074
2551
|
*
|
|
2075
2552
|
* Returns false for:
|
|
2076
2553
|
* - Git repository URLs (*.git)
|
|
2077
2554
|
* - GitHub/GitLab web URLs (/tree/, /blob/, /raw/)
|
|
2555
|
+
* - Bare HTTPS repo URLs without archive extensions (e.g., https://github.com/user/repo)
|
|
2556
|
+
* These are treated as Git references and handled by GitResolver.
|
|
2078
2557
|
*/ static isHttpUrl(ref) {
|
|
2079
2558
|
// Remove version suffix for checking (e.g., url@v1.0.0)
|
|
2080
2559
|
const urlPart = ref.split('@')[0];
|
|
2081
|
-
//
|
|
2082
|
-
if (urlPart.
|
|
2083
|
-
//
|
|
2084
|
-
if (
|
|
2085
|
-
|
|
2560
|
+
// oss:// and s3:// are always archive download sources
|
|
2561
|
+
if (urlPart.startsWith('oss://') || urlPart.startsWith('s3://')) return true;
|
|
2562
|
+
// For http:// and https:// URLs, distinguish between Git repos and archive downloads
|
|
2563
|
+
if (urlPart.startsWith('http://') || urlPart.startsWith('https://')) {
|
|
2564
|
+
// Exclude Git repository URLs (ending with .git)
|
|
2565
|
+
if (urlPart.endsWith('.git')) return false;
|
|
2566
|
+
// Exclude GitHub/GitLab web URLs (containing /tree/, /blob/, /raw/)
|
|
2567
|
+
if (/\/(tree|blob|raw)\//.test(urlPart)) return false;
|
|
2568
|
+
// Only classify as HTTP archive if URL has a recognized archive extension.
|
|
2569
|
+
// Bare HTTPS URLs like https://github.com/user/repo are Git references,
|
|
2570
|
+
// not archive downloads, and should fall through to GitResolver.
|
|
2571
|
+
return /\.(tar\.gz|tgz|zip|tar)$/i.test(urlPart);
|
|
2572
|
+
}
|
|
2573
|
+
return false;
|
|
2086
2574
|
}
|
|
2087
2575
|
/**
|
|
2088
2576
|
* Parse an HTTP/OSS URL reference
|
|
@@ -2391,46 +2879,21 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
2391
2879
|
* Maps registry URLs to their corresponding scopes.
|
|
2392
2880
|
* Currently hardcoded; TODO: fetch from /api/registry/info in the future.
|
|
2393
2881
|
*/ /**
|
|
2394
|
-
*
|
|
2395
|
-
*
|
|
2396
|
-
*/ const
|
|
2882
|
+
* Public Registry URL
|
|
2883
|
+
* Used for installing skills without a scope
|
|
2884
|
+
*/ const registry_scope_PUBLIC_REGISTRY = 'https://reskill.info/';
|
|
2397
2885
|
/**
|
|
2398
2886
|
* Hardcoded registry to scope mapping
|
|
2399
2887
|
* TODO: Replace with dynamic fetching from /api/registry/info
|
|
2400
2888
|
*/ const REGISTRY_SCOPE_MAP = {
|
|
2401
2889
|
// rush-app (private registry, new)
|
|
2402
|
-
'https://rush-test.zhenguanyu.com': '@kanyun',
|
|
2890
|
+
'https://rush-test.zhenguanyu.com': '@kanyun-test',
|
|
2403
2891
|
'https://rush.zhenguanyu.com': '@kanyun',
|
|
2404
2892
|
// reskill-app (private registry, legacy)
|
|
2405
|
-
'https://reskill-test.zhenguanyu.com': '@kanyun',
|
|
2893
|
+
'https://reskill-test.zhenguanyu.com': '@kanyun-test',
|
|
2406
2894
|
// 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'
|
|
2895
|
+
'http://localhost:3000': '@kanyun-test'
|
|
2418
2896
|
};
|
|
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
2897
|
/**
|
|
2435
2898
|
* Get the registry URL for a given scope (reverse lookup)
|
|
2436
2899
|
*
|
|
@@ -2478,7 +2941,7 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
2478
2941
|
* getRegistryUrl('@mycompany', { '@mycompany': 'https://my.registry.com/' }) // 'https://my.registry.com/'
|
|
2479
2942
|
*/ function getRegistryUrl(scope, customRegistries) {
|
|
2480
2943
|
// No scope → return public Registry
|
|
2481
|
-
if (!scope) return
|
|
2944
|
+
if (!scope) return registry_scope_PUBLIC_REGISTRY;
|
|
2482
2945
|
// With scope → lookup private Registry
|
|
2483
2946
|
const registry = getRegistryForScope(scope, customRegistries);
|
|
2484
2947
|
if (!registry) {
|
|
@@ -2529,21 +2992,21 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
2529
2992
|
/**
|
|
2530
2993
|
* Parse a skill identifier into its components (with version support)
|
|
2531
2994
|
*
|
|
2532
|
-
*
|
|
2995
|
+
* Supports both private registry (with @scope) and public registry (without scope) formats.
|
|
2533
2996
|
*
|
|
2534
2997
|
* @param identifier - Skill identifier string
|
|
2535
2998
|
* @returns Parsed skill identifier with scope, name, version, and fullName
|
|
2536
2999
|
* @throws Error if identifier is invalid
|
|
2537
3000
|
*
|
|
2538
3001
|
* @example
|
|
2539
|
-
* //
|
|
3002
|
+
* // Private registry
|
|
2540
3003
|
* parseSkillIdentifier('@kanyun/planning-with-files')
|
|
2541
3004
|
* // { scope: '@kanyun', name: 'planning-with-files', version: undefined, fullName: '@kanyun/planning-with-files' }
|
|
2542
3005
|
*
|
|
2543
3006
|
* parseSkillIdentifier('@kanyun/skill@2.4.5')
|
|
2544
3007
|
* // { scope: '@kanyun', name: 'skill', version: '2.4.5', fullName: '@kanyun/skill' }
|
|
2545
3008
|
*
|
|
2546
|
-
* //
|
|
3009
|
+
* // Public registry
|
|
2547
3010
|
* parseSkillIdentifier('planning-with-files')
|
|
2548
3011
|
* // { scope: null, name: 'planning-with-files', version: undefined, fullName: 'planning-with-files' }
|
|
2549
3012
|
*
|
|
@@ -2551,18 +3014,18 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
2551
3014
|
* // { scope: null, name: 'skill', version: 'latest', fullName: 'skill' }
|
|
2552
3015
|
*/ function parseSkillIdentifier(identifier) {
|
|
2553
3016
|
const trimmed = identifier.trim();
|
|
2554
|
-
//
|
|
3017
|
+
// Empty string or whitespace only
|
|
2555
3018
|
if (!trimmed) throw new Error('Invalid skill identifier: empty string');
|
|
2556
|
-
//
|
|
3019
|
+
// Starting with @@ is invalid
|
|
2557
3020
|
if (trimmed.startsWith('@@')) throw new Error('Invalid skill identifier: invalid scope format');
|
|
2558
|
-
//
|
|
3021
|
+
// Bare @ is invalid
|
|
2559
3022
|
if ('@' === trimmed) throw new Error('Invalid skill identifier: missing scope and name');
|
|
2560
|
-
//
|
|
3023
|
+
// Scoped format: @scope/name[@version]
|
|
2561
3024
|
if (trimmed.startsWith('@')) {
|
|
2562
|
-
//
|
|
2563
|
-
// scope:
|
|
2564
|
-
// name:
|
|
2565
|
-
// version:
|
|
3025
|
+
// Regex: @scope/name[@version]
|
|
3026
|
+
// scope: starts with @, followed by alphanumeric, hyphens, underscores
|
|
3027
|
+
// name: alphanumeric, hyphens, underscores
|
|
3028
|
+
// version: optional, @ followed by any non-empty string
|
|
2566
3029
|
const scopedMatch = trimmed.match(/^(@[\w-]+)\/([\w-]+)(?:@(.+))?$/);
|
|
2567
3030
|
if (!scopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
|
|
2568
3031
|
const [, scope, name, version] = scopedMatch;
|
|
@@ -2573,8 +3036,8 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
2573
3036
|
fullName: `${scope}/${name}`
|
|
2574
3037
|
};
|
|
2575
3038
|
}
|
|
2576
|
-
//
|
|
2577
|
-
// name
|
|
3039
|
+
// Unscoped format: name[@version] (public registry)
|
|
3040
|
+
// name must not contain / (otherwise it might be a git shorthand)
|
|
2578
3041
|
const unscopedMatch = trimmed.match(/^([\w-]+)(?:@(.+))?$/);
|
|
2579
3042
|
if (!unscopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
|
|
2580
3043
|
const [, name, version] = unscopedMatch;
|
|
@@ -2608,13 +3071,14 @@ class RegistryClient {
|
|
|
2608
3071
|
this.config = config;
|
|
2609
3072
|
}
|
|
2610
3073
|
/**
|
|
2611
|
-
* Get API base URL (registry +
|
|
3074
|
+
* Get API base URL (registry + /api)
|
|
2612
3075
|
*
|
|
2613
|
-
*
|
|
3076
|
+
* All registries use the unified '/api' prefix.
|
|
3077
|
+
*
|
|
3078
|
+
* @returns Base URL for API calls, e.g., 'https://example.com/api'
|
|
2614
3079
|
*/ getApiBase() {
|
|
2615
|
-
const prefix = this.config.apiPrefix || '/api';
|
|
2616
3080
|
const registry = this.config.registry.endsWith('/') ? this.config.registry.slice(0, -1) : this.config.registry;
|
|
2617
|
-
return `${registry}
|
|
3081
|
+
return `${registry}/api`;
|
|
2618
3082
|
}
|
|
2619
3083
|
/**
|
|
2620
3084
|
* Get authorization headers
|
|
@@ -2629,7 +3093,7 @@ class RegistryClient {
|
|
|
2629
3093
|
/**
|
|
2630
3094
|
* Get current user info (whoami)
|
|
2631
3095
|
*/ async whoami() {
|
|
2632
|
-
const url = `${this.getApiBase()}/auth/me`;
|
|
3096
|
+
const url = `${this.getApiBase()}/skill-auth/me`;
|
|
2633
3097
|
const response = await fetch(url, {
|
|
2634
3098
|
method: 'GET',
|
|
2635
3099
|
headers: this.getAuthHeaders()
|
|
@@ -2641,13 +3105,13 @@ class RegistryClient {
|
|
|
2641
3105
|
/**
|
|
2642
3106
|
* CLI login - verify token and get user info
|
|
2643
3107
|
*
|
|
2644
|
-
* Calls POST /api/auth/login-cli to validate the token and retrieve user information.
|
|
3108
|
+
* Calls POST /api/skill-auth/login-cli to validate the token and retrieve user information.
|
|
2645
3109
|
* This is the preferred method for CLI authentication.
|
|
2646
3110
|
*
|
|
2647
3111
|
* @returns User information if authentication succeeds
|
|
2648
3112
|
* @throws RegistryError if authentication fails
|
|
2649
3113
|
*/ async loginCli() {
|
|
2650
|
-
const url = `${this.getApiBase()}/auth/login-cli`;
|
|
3114
|
+
const url = `${this.getApiBase()}/skill-auth/login-cli`;
|
|
2651
3115
|
const response = await fetch(url, {
|
|
2652
3116
|
method: 'POST',
|
|
2653
3117
|
headers: this.getAuthHeaders()
|
|
@@ -2679,7 +3143,7 @@ class RegistryClient {
|
|
|
2679
3143
|
if (external_node_fs_.existsSync(filePath)) {
|
|
2680
3144
|
const content = external_node_fs_.readFileSync(filePath);
|
|
2681
3145
|
const stat = external_node_fs_.statSync(filePath);
|
|
2682
|
-
//
|
|
3146
|
+
// Prepend shortName as top-level directory if provided
|
|
2683
3147
|
const entryName = shortName ? `${shortName}/${file}` : file;
|
|
2684
3148
|
tarPack.entry({
|
|
2685
3149
|
name: entryName,
|
|
@@ -2693,15 +3157,15 @@ class RegistryClient {
|
|
|
2693
3157
|
});
|
|
2694
3158
|
}
|
|
2695
3159
|
// ============================================================================
|
|
2696
|
-
// Skill Info Methods (
|
|
3160
|
+
// Skill Info Methods (web-published skill support)
|
|
2697
3161
|
// ============================================================================
|
|
2698
3162
|
/**
|
|
2699
|
-
*
|
|
2700
|
-
*
|
|
3163
|
+
* Get basic skill info (including source_type).
|
|
3164
|
+
* Used by the install command to determine the installation logic branch.
|
|
2701
3165
|
*
|
|
2702
|
-
* @param skillName -
|
|
2703
|
-
* @returns
|
|
2704
|
-
* @throws RegistryError
|
|
3166
|
+
* @param skillName - Full skill name, e.g., @kanyun/my-skill
|
|
3167
|
+
* @returns Basic skill information
|
|
3168
|
+
* @throws RegistryError if skill not found or request failed
|
|
2705
3169
|
*/ async getSkillInfo(skillName) {
|
|
2706
3170
|
const url = `${this.getApiBase()}/skills/${encodeURIComponent(skillName)}`;
|
|
2707
3171
|
const response = await fetch(url, {
|
|
@@ -2710,15 +3174,50 @@ class RegistryClient {
|
|
|
2710
3174
|
});
|
|
2711
3175
|
if (!response.ok) {
|
|
2712
3176
|
const data = await response.json();
|
|
2713
|
-
//
|
|
3177
|
+
// Return a clear "not found" error for 404 responses
|
|
2714
3178
|
if (404 === response.status) throw new RegistryError(`Skill not found: ${skillName}`, response.status, data);
|
|
2715
3179
|
throw new RegistryError(data.error || `Failed to get skill info: ${response.statusText}`, response.status, data);
|
|
2716
3180
|
}
|
|
2717
|
-
// API
|
|
3181
|
+
// API response format: { success: true, data: { ... } }
|
|
2718
3182
|
const responseData = await response.json();
|
|
2719
3183
|
return responseData.data || responseData;
|
|
2720
3184
|
}
|
|
2721
3185
|
// ============================================================================
|
|
3186
|
+
// Search Methods
|
|
3187
|
+
// ============================================================================
|
|
3188
|
+
/**
|
|
3189
|
+
* Search for skills in the registry
|
|
3190
|
+
*
|
|
3191
|
+
* @param query - Search query string
|
|
3192
|
+
* @param options - Search options (limit, offset)
|
|
3193
|
+
* @returns Array of matching skills
|
|
3194
|
+
* @throws RegistryError if the request fails
|
|
3195
|
+
*
|
|
3196
|
+
* @example
|
|
3197
|
+
* const results = await client.search('typescript');
|
|
3198
|
+
* const results = await client.search('planning', { limit: 5 });
|
|
3199
|
+
*/ async search(query, options = {}) {
|
|
3200
|
+
const params = new URLSearchParams({
|
|
3201
|
+
q: query
|
|
3202
|
+
});
|
|
3203
|
+
if (void 0 !== options.limit) params.set('limit', String(options.limit));
|
|
3204
|
+
if (void 0 !== options.offset) params.set('offset', String(options.offset));
|
|
3205
|
+
const url = `${this.getApiBase()}/skills?${params.toString()}`;
|
|
3206
|
+
const response = await fetch(url, {
|
|
3207
|
+
method: 'GET',
|
|
3208
|
+
headers: this.getAuthHeaders()
|
|
3209
|
+
});
|
|
3210
|
+
if (!response.ok) {
|
|
3211
|
+
const data = await response.json();
|
|
3212
|
+
throw new RegistryError(data.error || `Search failed: ${response.status}`, response.status, data);
|
|
3213
|
+
}
|
|
3214
|
+
const data = await response.json();
|
|
3215
|
+
return {
|
|
3216
|
+
items: data.data || [],
|
|
3217
|
+
total: data.meta?.pagination?.totalItems ?? data.data?.length ?? 0
|
|
3218
|
+
};
|
|
3219
|
+
}
|
|
3220
|
+
// ============================================================================
|
|
2722
3221
|
// Download Methods (Step 3.3)
|
|
2723
3222
|
// ============================================================================
|
|
2724
3223
|
/**
|
|
@@ -2731,12 +3230,12 @@ class RegistryClient {
|
|
|
2731
3230
|
*
|
|
2732
3231
|
* @example
|
|
2733
3232
|
* await client.resolveVersion('@kanyun/test-skill', 'latest') // '2.4.5'
|
|
2734
|
-
* await client.resolveVersion('@kanyun/test-skill', '2.4.5') // '2.4.5' (
|
|
3233
|
+
* await client.resolveVersion('@kanyun/test-skill', '2.4.5') // '2.4.5' (returned as-is)
|
|
2735
3234
|
*/ async resolveVersion(skillName, tagOrVersion) {
|
|
2736
3235
|
const version = tagOrVersion || 'latest';
|
|
2737
|
-
//
|
|
3236
|
+
// If it's already a semver version number, return as-is
|
|
2738
3237
|
if (/^\d+\.\d+\.\d+/.test(version)) return version;
|
|
2739
|
-
//
|
|
3238
|
+
// Otherwise treat it as a tag and query dist-tags
|
|
2740
3239
|
const url = `${this.getApiBase()}/skills/${encodeURIComponent(skillName)}`;
|
|
2741
3240
|
const response = await fetch(url, {
|
|
2742
3241
|
method: 'GET',
|
|
@@ -2746,14 +3245,14 @@ class RegistryClient {
|
|
|
2746
3245
|
const data = await response.json();
|
|
2747
3246
|
throw new RegistryError(data.error || `Failed to fetch skill metadata: ${response.status}`, response.status, data);
|
|
2748
3247
|
}
|
|
2749
|
-
// API
|
|
3248
|
+
// API response format: { success: true, data: { dist_tags: [{ tag, version }] } }
|
|
2750
3249
|
const responseData = await response.json();
|
|
2751
|
-
//
|
|
3250
|
+
// Prefer npm-style dist-tags if present
|
|
2752
3251
|
if (responseData['dist-tags']) {
|
|
2753
3252
|
const resolvedVersion = responseData['dist-tags'][version];
|
|
2754
3253
|
if (resolvedVersion) return resolvedVersion;
|
|
2755
3254
|
}
|
|
2756
|
-
//
|
|
3255
|
+
// Fall back to reskill-app's dist_tags array format
|
|
2757
3256
|
const distTags = responseData.data?.dist_tags;
|
|
2758
3257
|
if (distTags && Array.isArray(distTags)) {
|
|
2759
3258
|
const tagEntry = distTags.find((t)=>t.tag === version);
|
|
@@ -2834,11 +3333,11 @@ class RegistryClient {
|
|
|
2834
3333
|
* @example
|
|
2835
3334
|
* RegistryClient.verifyIntegrity(buffer, 'sha256-abc123...') // true or false
|
|
2836
3335
|
*/ static verifyIntegrity(content, expectedIntegrity) {
|
|
2837
|
-
//
|
|
3336
|
+
// Parse integrity format: algorithm-hash
|
|
2838
3337
|
const match = expectedIntegrity.match(/^(\w+)-(.+)$/);
|
|
2839
3338
|
if (!match) throw new Error(`Invalid integrity format: ${expectedIntegrity}`);
|
|
2840
3339
|
const [, algorithm, expectedHash] = match;
|
|
2841
|
-
//
|
|
3340
|
+
// Only sha256 and sha512 are supported
|
|
2842
3341
|
if ('sha256' !== algorithm && 'sha512' !== algorithm) throw new Error(`Unsupported integrity algorithm: ${algorithm}`);
|
|
2843
3342
|
const actualHash = __WEBPACK_EXTERNAL_MODULE_node_crypto__.createHash(algorithm).update(content).digest('base64');
|
|
2844
3343
|
return actualHash === expectedHash;
|
|
@@ -2850,7 +3349,7 @@ class RegistryClient {
|
|
|
2850
3349
|
* Publish a skill to the registry
|
|
2851
3350
|
*/ async publish(skillName, payload, skillPath, options = {}) {
|
|
2852
3351
|
const url = `${this.getApiBase()}/skills/publish`;
|
|
2853
|
-
//
|
|
3352
|
+
// Extract short name as tarball top-level directory (without scope prefix)
|
|
2854
3353
|
const shortName = getShortName(skillName);
|
|
2855
3354
|
// Create tarball with short name as top-level directory
|
|
2856
3355
|
const tarball = await this.createTarball(skillPath, payload.files, shortName);
|
|
@@ -3118,343 +3617,113 @@ class RegistryClient {
|
|
|
3118
3617
|
});
|
|
3119
3618
|
extractor.on('finish', ()=>{
|
|
3120
3619
|
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');
|
|
3620
|
+
});
|
|
3621
|
+
extractor.on('error', (err)=>{
|
|
3622
|
+
reject(new Error(`Failed to read tarball: ${err.message}`));
|
|
3623
|
+
});
|
|
3624
|
+
gunzip.on('error', (err)=>{
|
|
3625
|
+
reject(new Error(`Failed to decompress tarball: ${err.message}`));
|
|
3626
|
+
});
|
|
3627
|
+
gunzip.pipe(extractor);
|
|
3628
|
+
gunzip.end(tarball);
|
|
3629
|
+
});
|
|
3357
3630
|
}
|
|
3358
3631
|
/**
|
|
3359
|
-
*
|
|
3632
|
+
* Registry Resolver (Step 5.1)
|
|
3360
3633
|
*
|
|
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
|
|
3634
|
+
* Resolves skill references from npm-style registries:
|
|
3635
|
+
* - Private registry: @scope/name[@version] (e.g., @kanyun/planning-with-files@2.4.5)
|
|
3636
|
+
* - Public registry: name[@version] (e.g., my-skill@1.0.0)
|
|
3371
3637
|
*
|
|
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
|
-
|
|
3638
|
+
* Uses RegistryClient to download and verify skills.
|
|
3639
|
+
*/ // ============================================================================
|
|
3640
|
+
// RegistryResolver Class
|
|
3641
|
+
// ============================================================================
|
|
3642
|
+
class RegistryResolver {
|
|
3643
|
+
/**
|
|
3644
|
+
* Check if a reference is a registry source (not Git or HTTP)
|
|
3645
|
+
*
|
|
3646
|
+
* Registry formats:
|
|
3647
|
+
* - @scope/name[@version] - private registry
|
|
3648
|
+
* - name[@version] - public registry (if not matching other formats)
|
|
3649
|
+
*
|
|
3650
|
+
* Explicitly excluded:
|
|
3651
|
+
* - Git SSH: git@github.com:user/repo.git
|
|
3652
|
+
* - Git HTTPS: https://github.com/user/repo.git
|
|
3653
|
+
* - GitHub web: https://github.com/user/repo/tree/...
|
|
3654
|
+
* - HTTP/OSS: https://example.com/skill.tar.gz
|
|
3655
|
+
* - Registry shorthand: github:user/repo, gitlab:org/repo
|
|
3656
|
+
*/ static isRegistryRef(ref) {
|
|
3657
|
+
// Exclude Git SSH format (git@...)
|
|
3658
|
+
if (ref.startsWith('git@') || ref.startsWith('git://')) return false;
|
|
3659
|
+
// Exclude URLs ending with .git
|
|
3660
|
+
if (ref.includes('.git')) return false;
|
|
3661
|
+
// Exclude HTTP/HTTPS/OSS URLs
|
|
3662
|
+
if (ref.startsWith('http://') || ref.startsWith('https://') || ref.startsWith('oss://') || ref.startsWith('s3://')) return false;
|
|
3663
|
+
// Exclude registry shorthand format (github:, gitlab:, custom.com:)
|
|
3664
|
+
// These follow "registry:owner/repo" pattern, not "@scope/name"
|
|
3665
|
+
if (/^[a-zA-Z0-9.-]+:[^@]/.test(ref)) return false;
|
|
3666
|
+
// Check for @scope/name format (private registry)
|
|
3667
|
+
if (ref.startsWith('@') && ref.includes('/')) {
|
|
3668
|
+
// @scope/name or @scope/name@version
|
|
3669
|
+
const scopeNamePattern = /^@[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
|
|
3670
|
+
return scopeNamePattern.test(ref);
|
|
3397
3671
|
}
|
|
3672
|
+
// Check for simple name or name@version format (public registry)
|
|
3673
|
+
// Simple names contain only letters, digits, hyphens, underscores, and dots
|
|
3674
|
+
const namePattern = /^[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
|
|
3675
|
+
return namePattern.test(ref);
|
|
3676
|
+
}
|
|
3677
|
+
/**
|
|
3678
|
+
* Resolve a registry skill reference
|
|
3679
|
+
*
|
|
3680
|
+
* @param ref - Skill reference (e.g., "@kanyun/planning-with-files@2.4.5" or "my-skill@latest")
|
|
3681
|
+
* @param overrideRegistryUrl - Optional registry URL override (bypasses scope-based lookup)
|
|
3682
|
+
* @returns Resolved skill information including downloaded tarball
|
|
3683
|
+
*
|
|
3684
|
+
* @example
|
|
3685
|
+
* const result = await resolver.resolve('@kanyun/planning-with-files@2.4.5');
|
|
3686
|
+
* console.log(result.shortName); // 'planning-with-files'
|
|
3687
|
+
* console.log(result.version); // '2.4.5'
|
|
3688
|
+
*/ async resolve(ref, overrideRegistryUrl) {
|
|
3689
|
+
// 1. Parse skill identifier
|
|
3690
|
+
const parsed = parseSkillIdentifier(ref);
|
|
3691
|
+
const shortName = getShortName(parsed.fullName);
|
|
3692
|
+
// 2. Get registry URL (CLI override takes precedence)
|
|
3693
|
+
const registryUrl = overrideRegistryUrl || getRegistryUrl(parsed.scope);
|
|
3694
|
+
// 3. Create client and resolve version
|
|
3695
|
+
const client = new RegistryClient({
|
|
3696
|
+
registry: registryUrl
|
|
3697
|
+
});
|
|
3698
|
+
const version = await client.resolveVersion(parsed.fullName, parsed.version);
|
|
3699
|
+
// 4. Download tarball
|
|
3700
|
+
const { tarball, integrity } = await client.downloadSkill(parsed.fullName, version);
|
|
3701
|
+
// 5. Verify integrity
|
|
3702
|
+
const isValid = RegistryClient.verifyIntegrity(tarball, integrity);
|
|
3703
|
+
if (!isValid) throw new Error(`Integrity verification failed for ${ref}`);
|
|
3398
3704
|
return {
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
version
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
allowedTools,
|
|
3406
|
-
content: body,
|
|
3407
|
-
rawContent: content
|
|
3705
|
+
parsed,
|
|
3706
|
+
shortName,
|
|
3707
|
+
version,
|
|
3708
|
+
registryUrl,
|
|
3709
|
+
tarball,
|
|
3710
|
+
integrity
|
|
3408
3711
|
};
|
|
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
3712
|
}
|
|
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;
|
|
3713
|
+
/**
|
|
3714
|
+
* Extract tarball to a target directory
|
|
3715
|
+
*
|
|
3716
|
+
* @param tarball - Tarball buffer
|
|
3717
|
+
* @param destDir - Destination directory
|
|
3718
|
+
* @returns Path to the extracted skill directory
|
|
3719
|
+
*/ async extract(tarball, destDir) {
|
|
3720
|
+
await extractTarballBuffer(tarball, destDir);
|
|
3721
|
+
// Get top-level directory name (i.e. skill name)
|
|
3722
|
+
const topDir = await getTarballTopDir(tarball);
|
|
3723
|
+
if (topDir) return `${destDir}/${topDir}`;
|
|
3724
|
+
return destDir;
|
|
3441
3725
|
}
|
|
3442
3726
|
}
|
|
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
3727
|
/**
|
|
3459
3728
|
* SkillManager - Core Skill management class
|
|
3460
3729
|
*
|
|
@@ -3563,6 +3832,18 @@ class RegistryResolver {
|
|
|
3563
3832
|
return null;
|
|
3564
3833
|
}
|
|
3565
3834
|
/**
|
|
3835
|
+
* Resolve the actual source path for installation.
|
|
3836
|
+
* For multi-skill repos (parsed.skillName is set), discovers skills in the
|
|
3837
|
+
* cached directory and returns the matching skill's subdirectory.
|
|
3838
|
+
* For single-skill repos, returns basePath as-is.
|
|
3839
|
+
*/ resolveSourcePath(basePath, parsed) {
|
|
3840
|
+
if (!parsed.skillName) return basePath;
|
|
3841
|
+
const discovered = discoverSkillsInDir(basePath);
|
|
3842
|
+
const match = discovered.find((s)=>s.name === parsed.skillName);
|
|
3843
|
+
if (!match) throw new Error(`Skill "${parsed.skillName}" not found in repository. Available: ${discovered.map((s)=>s.name).join(', ')}`);
|
|
3844
|
+
return match.dirPath;
|
|
3845
|
+
}
|
|
3846
|
+
/**
|
|
3566
3847
|
* Install skill
|
|
3567
3848
|
*/ async install(ref, options = {}) {
|
|
3568
3849
|
// Detect source type and delegate to appropriate installer
|
|
@@ -3585,9 +3866,10 @@ class RegistryResolver {
|
|
|
3585
3866
|
logger_logger.debug(`Caching from ${repoUrl}@${gitRef}`);
|
|
3586
3867
|
cacheResult = await this.cache.cache(repoUrl, parsed, gitRef, gitRef);
|
|
3587
3868
|
}
|
|
3588
|
-
//
|
|
3869
|
+
// Resolve source path (cache root or skill subdirectory for multi-skill repos)
|
|
3589
3870
|
const cachePath = this.cache.getCachePath(parsed, gitRef);
|
|
3590
|
-
const
|
|
3871
|
+
const sourcePath = this.resolveSourcePath(cachePath, parsed);
|
|
3872
|
+
const metadata = this.getSkillMetadataFromDir(sourcePath);
|
|
3591
3873
|
const skillName = metadata?.name ?? fallbackName;
|
|
3592
3874
|
const semanticVersion = metadata?.version ?? gitRef;
|
|
3593
3875
|
const skillPath = this.getSkillPath(skillName);
|
|
@@ -3611,7 +3893,9 @@ class RegistryResolver {
|
|
|
3611
3893
|
// Copy to installation directory
|
|
3612
3894
|
ensureDir(this.getInstallDir());
|
|
3613
3895
|
if (exists(skillPath)) remove(skillPath);
|
|
3614
|
-
|
|
3896
|
+
copyDir(sourcePath, skillPath, {
|
|
3897
|
+
exclude: DEFAULT_EXCLUDE_FILES
|
|
3898
|
+
});
|
|
3615
3899
|
// Update lock file (project mode only)
|
|
3616
3900
|
if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
|
|
3617
3901
|
source: `${parsed.registry}:${parsed.owner}/${parsed.repo}${parsed.subPath ? `/${parsed.subPath}` : ''}`,
|
|
@@ -3936,12 +4220,112 @@ class RegistryResolver {
|
|
|
3936
4220
|
* @param options - Installation options
|
|
3937
4221
|
*/ async installToAgents(ref, targetAgents, options = {}) {
|
|
3938
4222
|
// Detect source type and delegate to appropriate installer
|
|
3939
|
-
// Priority: Registry > HTTP > Git (registry
|
|
4223
|
+
// Priority: Registry > HTTP > Git (registry first, as its format is most constrained)
|
|
3940
4224
|
if (this.isRegistrySource(ref)) return this.installToAgentsFromRegistry(ref, targetAgents, options);
|
|
3941
4225
|
if (this.isHttpSource(ref)) return this.installToAgentsFromHttp(ref, targetAgents, options);
|
|
3942
4226
|
return this.installToAgentsFromGit(ref, targetAgents, options);
|
|
3943
4227
|
}
|
|
3944
4228
|
/**
|
|
4229
|
+
* Multi-skill install: discover skills in a Git repo and install selected ones (or list only).
|
|
4230
|
+
* Only Git references are supported (including https://github.com/...); registry refs are not.
|
|
4231
|
+
*
|
|
4232
|
+
* @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
|
|
4233
|
+
* @param skillNames - If non-empty, install only these skills (by SKILL.md name). If empty and !listOnly, install all.
|
|
4234
|
+
* @param targetAgents - Target agents
|
|
4235
|
+
* @param options - Install options; listOnly: true means discover and return skills without installing
|
|
4236
|
+
*/ async installSkillsFromRepo(ref, skillNames, targetAgents, options = {}) {
|
|
4237
|
+
const { listOnly = false, force = false, save = true, mode = 'symlink' } = options;
|
|
4238
|
+
const refForResolve = ref.replace(/#.*$/, '').trim();
|
|
4239
|
+
const resolved = await this.resolver.resolve(refForResolve);
|
|
4240
|
+
const { parsed, repoUrl } = resolved;
|
|
4241
|
+
const gitRef = resolved.ref;
|
|
4242
|
+
let cacheResult = await this.cache.get(parsed, gitRef);
|
|
4243
|
+
if (!cacheResult) {
|
|
4244
|
+
logger_logger.debug(`Caching from ${repoUrl}@${gitRef}`);
|
|
4245
|
+
cacheResult = await this.cache.cache(repoUrl, parsed, gitRef, gitRef);
|
|
4246
|
+
}
|
|
4247
|
+
const cachePath = this.cache.getCachePath(parsed, gitRef);
|
|
4248
|
+
const discovered = discoverSkillsInDir(cachePath);
|
|
4249
|
+
if (0 === discovered.length) throw new Error('No valid skills found. Skills require a SKILL.md with name and description.');
|
|
4250
|
+
if (listOnly) return {
|
|
4251
|
+
listOnly: true,
|
|
4252
|
+
skills: discovered
|
|
4253
|
+
};
|
|
4254
|
+
const selected = skillNames.length > 0 ? filterSkillsByName(discovered, skillNames) : discovered;
|
|
4255
|
+
if (skillNames.length > 0 && 0 === selected.length) {
|
|
4256
|
+
const available = discovered.map((s)=>s.name).join(', ');
|
|
4257
|
+
throw new Error(`No matching skills found for: ${skillNames.join(', ')}. Available skills: ${available}`);
|
|
4258
|
+
}
|
|
4259
|
+
const baseRefForSave = this.config.normalizeSkillRef(refForResolve);
|
|
4260
|
+
const defaults = this.config.getDefaults();
|
|
4261
|
+
// Only pass custom installDir to Installer; default '.skills' should use
|
|
4262
|
+
// the Installer's built-in canonical path (.agents/skills/)
|
|
4263
|
+
const customInstallDir = '.skills' !== defaults.installDir ? defaults.installDir : void 0;
|
|
4264
|
+
const installer = new Installer({
|
|
4265
|
+
cwd: this.projectRoot,
|
|
4266
|
+
global: this.isGlobal,
|
|
4267
|
+
installDir: customInstallDir
|
|
4268
|
+
});
|
|
4269
|
+
const installed = [];
|
|
4270
|
+
const skipped = [];
|
|
4271
|
+
const skillSource = `${parsed.registry}:${parsed.owner}/${parsed.repo}${parsed.subPath ? `/${parsed.subPath}` : ''}`;
|
|
4272
|
+
for (const skillInfo of selected){
|
|
4273
|
+
const semanticVersion = skillInfo.version ?? gitRef;
|
|
4274
|
+
// Skip already-installed skills unless --force is set
|
|
4275
|
+
if (!force) {
|
|
4276
|
+
const existingSkill = this.getInstalledSkill(skillInfo.name);
|
|
4277
|
+
if (existingSkill) {
|
|
4278
|
+
const locked = this.lockManager.get(skillInfo.name);
|
|
4279
|
+
const lockedRef = locked?.ref || locked?.version;
|
|
4280
|
+
if (lockedRef === gitRef) {
|
|
4281
|
+
const reason = `already installed at ${gitRef}`;
|
|
4282
|
+
logger_logger.info(`${skillInfo.name}@${gitRef} is already installed, skipping`);
|
|
4283
|
+
skipped.push({
|
|
4284
|
+
name: skillInfo.name,
|
|
4285
|
+
reason
|
|
4286
|
+
});
|
|
4287
|
+
continue;
|
|
4288
|
+
}
|
|
4289
|
+
// Different version installed — allow upgrade without --force
|
|
4290
|
+
// Only skip when the exact same ref is already locked
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
logger_logger["package"](`Installing ${skillInfo.name}@${gitRef} to ${targetAgents.length} agent(s)...`);
|
|
4294
|
+
// Note: force is handled at the SkillManager level (skip-if-installed check above).
|
|
4295
|
+
// The Installer always overwrites (remove + copy), so no force flag is needed there.
|
|
4296
|
+
const results = await installer.installToAgents(skillInfo.dirPath, skillInfo.name, targetAgents, {
|
|
4297
|
+
mode: mode
|
|
4298
|
+
});
|
|
4299
|
+
if (!this.isGlobal) this.lockManager.lockSkill(skillInfo.name, {
|
|
4300
|
+
source: skillSource,
|
|
4301
|
+
version: semanticVersion,
|
|
4302
|
+
ref: gitRef,
|
|
4303
|
+
resolved: repoUrl,
|
|
4304
|
+
commit: cacheResult.commit
|
|
4305
|
+
});
|
|
4306
|
+
if (!this.isGlobal && save) {
|
|
4307
|
+
this.config.ensureExists();
|
|
4308
|
+
this.config.addSkill(skillInfo.name, `${baseRefForSave}#${skillInfo.name}`);
|
|
4309
|
+
}
|
|
4310
|
+
const successCount = Array.from(results.values()).filter((r)=>r.success).length;
|
|
4311
|
+
logger_logger.success(`Installed ${skillInfo.name}@${semanticVersion} to ${successCount} agent(s)`);
|
|
4312
|
+
installed.push({
|
|
4313
|
+
skill: {
|
|
4314
|
+
name: skillInfo.name,
|
|
4315
|
+
path: skillInfo.dirPath,
|
|
4316
|
+
version: semanticVersion,
|
|
4317
|
+
source: skillSource
|
|
4318
|
+
},
|
|
4319
|
+
results
|
|
4320
|
+
});
|
|
4321
|
+
}
|
|
4322
|
+
return {
|
|
4323
|
+
listOnly: false,
|
|
4324
|
+
installed,
|
|
4325
|
+
skipped
|
|
4326
|
+
};
|
|
4327
|
+
}
|
|
4328
|
+
/**
|
|
3945
4329
|
* Install skill from Git to multiple agents
|
|
3946
4330
|
*/ async installToAgentsFromGit(ref, targetAgents, options = {}) {
|
|
3947
4331
|
const { save = true, mode = 'symlink' } = options;
|
|
@@ -3957,8 +4341,9 @@ class RegistryResolver {
|
|
|
3957
4341
|
logger_logger.debug(`Caching from ${repoUrl}@${gitRef}`);
|
|
3958
4342
|
cacheResult = await this.cache.cache(repoUrl, parsed, gitRef, gitRef);
|
|
3959
4343
|
}
|
|
3960
|
-
//
|
|
3961
|
-
const
|
|
4344
|
+
// Resolve source path (cache root or skill subdirectory for multi-skill repos)
|
|
4345
|
+
const cachePath = this.cache.getCachePath(parsed, gitRef);
|
|
4346
|
+
const sourcePath = this.resolveSourcePath(cachePath, parsed);
|
|
3962
4347
|
// Get the real skill name from SKILL.md in cache
|
|
3963
4348
|
const metadata = this.getSkillMetadataFromDir(sourcePath);
|
|
3964
4349
|
const skillName = metadata?.name ?? fallbackName;
|
|
@@ -4082,14 +4467,13 @@ class RegistryResolver {
|
|
|
4082
4467
|
* - Web-published skills (github/gitlab/oss_url/custom_url/local)
|
|
4083
4468
|
*/ async installToAgentsFromRegistry(ref, targetAgents, options = {}) {
|
|
4084
4469
|
const { force = false, save = true, mode = 'symlink' } = options;
|
|
4085
|
-
//
|
|
4470
|
+
// Parse skill identifier and resolve registry URL once (single source of truth)
|
|
4086
4471
|
const parsed = parseSkillIdentifier(ref);
|
|
4087
|
-
const registryUrl = getRegistryUrl(parsed.scope);
|
|
4472
|
+
const registryUrl = options.registry || getRegistryUrl(parsed.scope);
|
|
4088
4473
|
const client = new RegistryClient({
|
|
4089
|
-
registry: registryUrl
|
|
4090
|
-
apiPrefix: getApiPrefix(registryUrl)
|
|
4474
|
+
registry: registryUrl
|
|
4091
4475
|
});
|
|
4092
|
-
//
|
|
4476
|
+
// Query skill info to determine source_type
|
|
4093
4477
|
let skillInfo;
|
|
4094
4478
|
try {
|
|
4095
4479
|
skillInfo = await client.getSkillInfo(parsed.fullName);
|
|
@@ -4100,12 +4484,15 @@ class RegistryResolver {
|
|
|
4100
4484
|
};
|
|
4101
4485
|
else throw error;
|
|
4102
4486
|
}
|
|
4103
|
-
//
|
|
4487
|
+
// Branch based on source_type (pass resolved registryUrl via options to avoid re-computation)
|
|
4104
4488
|
const sourceType = skillInfo.source_type;
|
|
4105
|
-
if (sourceType && 'registry' !== sourceType) return this.installFromWebPublished(skillInfo, parsed, targetAgents,
|
|
4106
|
-
|
|
4489
|
+
if (sourceType && 'registry' !== sourceType) return this.installFromWebPublished(skillInfo, parsed, targetAgents, {
|
|
4490
|
+
...options,
|
|
4491
|
+
registry: registryUrl
|
|
4492
|
+
});
|
|
4493
|
+
// 1. Resolve registry skill (pass pre-resolved registryUrl)
|
|
4107
4494
|
logger_logger["package"](`Resolving ${ref} from registry...`);
|
|
4108
|
-
const resolved = await this.registryResolver.resolve(ref);
|
|
4495
|
+
const resolved = await this.registryResolver.resolve(ref, registryUrl);
|
|
4109
4496
|
const { shortName, version, registryUrl: resolvedRegistryUrl, tarball, parsed: resolvedParsed } = resolved;
|
|
4110
4497
|
// 2. Check if already installed (skip if --force)
|
|
4111
4498
|
const skillPath = this.getSkillPath(shortName);
|
|
@@ -4144,101 +4531,157 @@ class RegistryResolver {
|
|
|
4144
4531
|
};
|
|
4145
4532
|
}
|
|
4146
4533
|
logger_logger["package"](`Installing ${shortName}@${version} from ${resolvedRegistryUrl} to ${targetAgents.length} agent(s)...`);
|
|
4147
|
-
// 3. Create temp directory for extraction
|
|
4534
|
+
// 3. Create temp directory for extraction (clean stale files first)
|
|
4148
4535
|
const tempDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cache.getCacheDir(), 'registry-temp', `${shortName}-${version}`);
|
|
4536
|
+
await remove(tempDir);
|
|
4149
4537
|
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
|
-
|
|
4538
|
+
try {
|
|
4539
|
+
// 4. Extract tarball
|
|
4540
|
+
const extractedPath = await this.registryResolver.extract(tarball, tempDir);
|
|
4541
|
+
logger_logger.debug(`Extracted to ${extractedPath}`);
|
|
4542
|
+
// 5. Create Installer with custom installDir from config
|
|
4543
|
+
const defaults = this.config.getDefaults();
|
|
4544
|
+
const installer = new Installer({
|
|
4545
|
+
cwd: this.projectRoot,
|
|
4546
|
+
global: this.isGlobal,
|
|
4547
|
+
installDir: defaults.installDir
|
|
4548
|
+
});
|
|
4549
|
+
// 6. Install to all target agents
|
|
4550
|
+
const results = await installer.installToAgents(extractedPath, shortName, targetAgents, {
|
|
4551
|
+
mode: mode
|
|
4552
|
+
});
|
|
4553
|
+
// 7. Update lock file (project mode only)
|
|
4554
|
+
if (!this.isGlobal) this.lockManager.lockSkill(shortName, {
|
|
4555
|
+
source: `registry:${resolvedParsed.fullName}`,
|
|
4556
|
+
version,
|
|
4557
|
+
ref: version,
|
|
4558
|
+
resolved: resolvedRegistryUrl,
|
|
4559
|
+
commit: resolved.integrity
|
|
4560
|
+
});
|
|
4561
|
+
// 8. Update skills.json (project mode only)
|
|
4562
|
+
if (!this.isGlobal && save) {
|
|
4563
|
+
this.config.ensureExists();
|
|
4564
|
+
// Save with full name for registry skills
|
|
4565
|
+
this.config.addSkill(shortName, ref);
|
|
4566
|
+
}
|
|
4567
|
+
// 9. Count results and log
|
|
4568
|
+
const successCount = Array.from(results.values()).filter((r)=>r.success).length;
|
|
4569
|
+
const failCount = results.size - successCount;
|
|
4570
|
+
if (0 === failCount) logger_logger.success(`Installed ${shortName}@${version} to ${successCount} agent(s)`);
|
|
4571
|
+
else logger_logger.warn(`Installed ${shortName}@${version} to ${successCount} agent(s), ${failCount} failed`);
|
|
4572
|
+
// 10. Build the InstalledSkill to return
|
|
4573
|
+
const skill = {
|
|
4574
|
+
name: shortName,
|
|
4575
|
+
path: extractedPath,
|
|
4576
|
+
version,
|
|
4577
|
+
source: `registry:${resolvedParsed.fullName}`
|
|
4578
|
+
};
|
|
4579
|
+
return {
|
|
4580
|
+
skill,
|
|
4581
|
+
results
|
|
4582
|
+
};
|
|
4583
|
+
} finally{
|
|
4584
|
+
// Clean up temp directory after installation
|
|
4585
|
+
await remove(tempDir);
|
|
4177
4586
|
}
|
|
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
4587
|
}
|
|
4195
4588
|
// ============================================================================
|
|
4196
|
-
// Web-published skill installation
|
|
4589
|
+
// Web-published skill installation
|
|
4197
4590
|
// ============================================================================
|
|
4198
4591
|
/**
|
|
4199
|
-
*
|
|
4592
|
+
* Install a web-published skill.
|
|
4200
4593
|
*
|
|
4201
|
-
*
|
|
4202
|
-
*
|
|
4203
|
-
* -
|
|
4204
|
-
* -
|
|
4594
|
+
* Web-published skills do not support versioning. Branches to different
|
|
4595
|
+
* installation logic based on source_type:
|
|
4596
|
+
* - github/gitlab: reuses installToAgentsFromGit
|
|
4597
|
+
* - oss_url/custom_url: reuses installToAgentsFromHttp
|
|
4598
|
+
* - local: downloads tarball via Registry API
|
|
4205
4599
|
*/ async installFromWebPublished(skillInfo, parsed, targetAgents, options = {}) {
|
|
4206
4600
|
const { source_type, source_url } = skillInfo;
|
|
4207
|
-
//
|
|
4601
|
+
// Web-published skills do not support version specifiers
|
|
4208
4602
|
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
4603
|
if (!source_url) throw new Error(`Missing source_url for web-published skill: ${parsed.fullName}`);
|
|
4210
4604
|
logger_logger["package"](`Installing ${parsed.fullName} from ${source_type} source...`);
|
|
4211
4605
|
switch(source_type){
|
|
4212
4606
|
case 'github':
|
|
4213
4607
|
case 'gitlab':
|
|
4214
|
-
// source_url
|
|
4215
|
-
//
|
|
4608
|
+
// source_url is a full Git URL (includes ref and path)
|
|
4609
|
+
// Reuse existing Git installation logic
|
|
4216
4610
|
return this.installToAgentsFromGit(source_url, targetAgents, options);
|
|
4217
4611
|
case 'oss_url':
|
|
4218
4612
|
case 'custom_url':
|
|
4219
|
-
//
|
|
4613
|
+
// Direct download URL
|
|
4220
4614
|
return this.installToAgentsFromHttp(source_url, targetAgents, options);
|
|
4221
4615
|
case 'local':
|
|
4222
|
-
//
|
|
4223
|
-
return this.installFromRegistryLocal(
|
|
4616
|
+
// Download tarball via Registry API
|
|
4617
|
+
return this.installFromRegistryLocal(parsed, targetAgents, options);
|
|
4224
4618
|
default:
|
|
4225
4619
|
throw new Error(`Unknown source_type: ${source_type}`);
|
|
4226
4620
|
}
|
|
4227
4621
|
}
|
|
4228
4622
|
/**
|
|
4229
|
-
*
|
|
4623
|
+
* Install a skill published via "local folder" mode.
|
|
4230
4624
|
*
|
|
4231
|
-
*
|
|
4232
|
-
*
|
|
4233
|
-
*/ async installFromRegistryLocal(
|
|
4234
|
-
const
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
const
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4625
|
+
* Downloads tarball via RegistryClient (handles 302 redirects to signed OSS URLs),
|
|
4626
|
+
* then extracts and installs using the same flow as registry source_type.
|
|
4627
|
+
*/ async installFromRegistryLocal(parsed, targetAgents, options = {}) {
|
|
4628
|
+
const { save = true, mode = 'symlink' } = options;
|
|
4629
|
+
const registryUrl = options.registry || getRegistryUrl(parsed.scope);
|
|
4630
|
+
const shortName = getShortName(parsed.fullName);
|
|
4631
|
+
const version = 'latest';
|
|
4632
|
+
// Download tarball via RegistryClient (handles auth + 302 redirect to signed URL)
|
|
4633
|
+
const client = new RegistryClient({
|
|
4634
|
+
registry: registryUrl
|
|
4635
|
+
});
|
|
4636
|
+
const { tarball } = await client.downloadSkill(parsed.fullName, version);
|
|
4637
|
+
logger_logger["package"](`Installing ${shortName} from ${registryUrl} to ${targetAgents.length} agent(s)...`);
|
|
4638
|
+
// Extract tarball to temp directory (clean stale files first)
|
|
4639
|
+
const tempDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cache.getCacheDir(), 'registry-temp', `${shortName}-${version}`);
|
|
4640
|
+
await remove(tempDir);
|
|
4641
|
+
await ensureDir(tempDir);
|
|
4642
|
+
try {
|
|
4643
|
+
const extractedPath = await this.registryResolver.extract(tarball, tempDir);
|
|
4644
|
+
logger_logger.debug(`Extracted to ${extractedPath}`);
|
|
4645
|
+
// Install to all target agents
|
|
4646
|
+
const defaults = this.config.getDefaults();
|
|
4647
|
+
const installer = new Installer({
|
|
4648
|
+
cwd: this.projectRoot,
|
|
4649
|
+
global: this.isGlobal,
|
|
4650
|
+
installDir: defaults.installDir
|
|
4651
|
+
});
|
|
4652
|
+
const results = await installer.installToAgents(extractedPath, shortName, targetAgents, {
|
|
4653
|
+
mode: mode
|
|
4654
|
+
});
|
|
4655
|
+
// Get metadata from extracted path
|
|
4656
|
+
const metadata = this.getSkillMetadataFromDir(extractedPath);
|
|
4657
|
+
const skillName = metadata?.name ?? shortName;
|
|
4658
|
+
const semanticVersion = metadata?.version ?? version;
|
|
4659
|
+
// Update lock file (project mode only)
|
|
4660
|
+
if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
|
|
4661
|
+
source: `registry:${parsed.fullName}`,
|
|
4662
|
+
version: semanticVersion,
|
|
4663
|
+
ref: version,
|
|
4664
|
+
resolved: registryUrl,
|
|
4665
|
+
commit: ''
|
|
4666
|
+
});
|
|
4667
|
+
// Update skills.json (project mode only)
|
|
4668
|
+
if (!this.isGlobal && save) {
|
|
4669
|
+
this.config.ensureExists();
|
|
4670
|
+
this.config.addSkill(skillName, parsed.fullName);
|
|
4671
|
+
}
|
|
4672
|
+
return {
|
|
4673
|
+
skill: {
|
|
4674
|
+
name: skillName,
|
|
4675
|
+
path: extractedPath,
|
|
4676
|
+
version: semanticVersion,
|
|
4677
|
+
source: `registry:${parsed.fullName}`
|
|
4678
|
+
},
|
|
4679
|
+
results
|
|
4680
|
+
};
|
|
4681
|
+
} finally{
|
|
4682
|
+
// Clean up temp directory after installation
|
|
4683
|
+
await remove(tempDir);
|
|
4684
|
+
}
|
|
4242
4685
|
}
|
|
4243
4686
|
/**
|
|
4244
4687
|
* Get default target agents
|