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/cli/index.js
CHANGED
|
@@ -430,46 +430,21 @@ var external_node_fs_ = __webpack_require__("node:fs");
|
|
|
430
430
|
* Maps registry URLs to their corresponding scopes.
|
|
431
431
|
* Currently hardcoded; TODO: fetch from /api/registry/info in the future.
|
|
432
432
|
*/ /**
|
|
433
|
-
*
|
|
434
|
-
*
|
|
433
|
+
* Public Registry URL
|
|
434
|
+
* Used for installing skills without a scope
|
|
435
435
|
*/ const PUBLIC_REGISTRY = 'https://reskill.info/';
|
|
436
436
|
/**
|
|
437
437
|
* Hardcoded registry to scope mapping
|
|
438
438
|
* TODO: Replace with dynamic fetching from /api/registry/info
|
|
439
439
|
*/ const REGISTRY_SCOPE_MAP = {
|
|
440
440
|
// rush-app (private registry, new)
|
|
441
|
-
'https://rush-test.zhenguanyu.com': '@kanyun',
|
|
441
|
+
'https://rush-test.zhenguanyu.com': '@kanyun-test',
|
|
442
442
|
'https://rush.zhenguanyu.com': '@kanyun',
|
|
443
443
|
// reskill-app (private registry, legacy)
|
|
444
|
-
'https://reskill-test.zhenguanyu.com': '@kanyun',
|
|
444
|
+
'https://reskill-test.zhenguanyu.com': '@kanyun-test',
|
|
445
445
|
// Local development
|
|
446
|
-
'http://localhost:3000': '@kanyun'
|
|
446
|
+
'http://localhost:3000': '@kanyun-test'
|
|
447
447
|
};
|
|
448
|
-
/**
|
|
449
|
-
* Registry API prefix mapping
|
|
450
|
-
*
|
|
451
|
-
* rush-app hosts reskill APIs under /api/reskill/ prefix.
|
|
452
|
-
* Default for unlisted registries: '/api'
|
|
453
|
-
*/ const REGISTRY_API_PREFIX = {
|
|
454
|
-
'https://rush-test.zhenguanyu.com': '/api/reskill',
|
|
455
|
-
'https://rush.zhenguanyu.com': '/api/reskill',
|
|
456
|
-
'http://localhost:3000': '/api/reskill'
|
|
457
|
-
};
|
|
458
|
-
/**
|
|
459
|
-
* Get the API path prefix for a given registry URL
|
|
460
|
-
*
|
|
461
|
-
* @param registryUrl - Registry URL
|
|
462
|
-
* @returns API prefix string (e.g., '/api' or '/api/reskill')
|
|
463
|
-
*
|
|
464
|
-
* @example
|
|
465
|
-
* getApiPrefix('https://rush-test.zhenguanyu.com') // '/api/reskill'
|
|
466
|
-
* getApiPrefix('https://reskill.info') // '/api'
|
|
467
|
-
* getApiPrefix('https://unknown.com') // '/api'
|
|
468
|
-
*/ function getApiPrefix(registryUrl) {
|
|
469
|
-
if (!registryUrl) return '/api';
|
|
470
|
-
const normalized = registryUrl.endsWith('/') ? registryUrl.slice(0, -1) : registryUrl;
|
|
471
|
-
return REGISTRY_API_PREFIX[normalized] || '/api';
|
|
472
|
-
}
|
|
473
448
|
/**
|
|
474
449
|
* Get the scope for a given registry URL
|
|
475
450
|
*
|
|
@@ -602,21 +577,21 @@ var external_node_fs_ = __webpack_require__("node:fs");
|
|
|
602
577
|
/**
|
|
603
578
|
* Parse a skill identifier into its components (with version support)
|
|
604
579
|
*
|
|
605
|
-
*
|
|
580
|
+
* Supports both private registry (with @scope) and public registry (without scope) formats.
|
|
606
581
|
*
|
|
607
582
|
* @param identifier - Skill identifier string
|
|
608
583
|
* @returns Parsed skill identifier with scope, name, version, and fullName
|
|
609
584
|
* @throws Error if identifier is invalid
|
|
610
585
|
*
|
|
611
586
|
* @example
|
|
612
|
-
* //
|
|
587
|
+
* // Private registry
|
|
613
588
|
* parseSkillIdentifier('@kanyun/planning-with-files')
|
|
614
589
|
* // { scope: '@kanyun', name: 'planning-with-files', version: undefined, fullName: '@kanyun/planning-with-files' }
|
|
615
590
|
*
|
|
616
591
|
* parseSkillIdentifier('@kanyun/skill@2.4.5')
|
|
617
592
|
* // { scope: '@kanyun', name: 'skill', version: '2.4.5', fullName: '@kanyun/skill' }
|
|
618
593
|
*
|
|
619
|
-
* //
|
|
594
|
+
* // Public registry
|
|
620
595
|
* parseSkillIdentifier('planning-with-files')
|
|
621
596
|
* // { scope: null, name: 'planning-with-files', version: undefined, fullName: 'planning-with-files' }
|
|
622
597
|
*
|
|
@@ -624,18 +599,18 @@ var external_node_fs_ = __webpack_require__("node:fs");
|
|
|
624
599
|
* // { scope: null, name: 'skill', version: 'latest', fullName: 'skill' }
|
|
625
600
|
*/ function parseSkillIdentifier(identifier) {
|
|
626
601
|
const trimmed = identifier.trim();
|
|
627
|
-
//
|
|
602
|
+
// Empty string or whitespace only
|
|
628
603
|
if (!trimmed) throw new Error('Invalid skill identifier: empty string');
|
|
629
|
-
//
|
|
604
|
+
// Starting with @@ is invalid
|
|
630
605
|
if (trimmed.startsWith('@@')) throw new Error('Invalid skill identifier: invalid scope format');
|
|
631
|
-
//
|
|
606
|
+
// Bare @ is invalid
|
|
632
607
|
if ('@' === trimmed) throw new Error('Invalid skill identifier: missing scope and name');
|
|
633
|
-
//
|
|
608
|
+
// Scoped format: @scope/name[@version]
|
|
634
609
|
if (trimmed.startsWith('@')) {
|
|
635
|
-
//
|
|
636
|
-
// scope:
|
|
637
|
-
// name:
|
|
638
|
-
// version:
|
|
610
|
+
// Regex: @scope/name[@version]
|
|
611
|
+
// scope: starts with @, followed by alphanumeric, hyphens, underscores
|
|
612
|
+
// name: alphanumeric, hyphens, underscores
|
|
613
|
+
// version: optional, @ followed by any non-empty string
|
|
639
614
|
const scopedMatch = trimmed.match(/^(@[\w-]+)\/([\w-]+)(?:@(.+))?$/);
|
|
640
615
|
if (!scopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
|
|
641
616
|
const [, scope, name, version] = scopedMatch;
|
|
@@ -646,8 +621,8 @@ var external_node_fs_ = __webpack_require__("node:fs");
|
|
|
646
621
|
fullName: `${scope}/${name}`
|
|
647
622
|
};
|
|
648
623
|
}
|
|
649
|
-
//
|
|
650
|
-
// name
|
|
624
|
+
// Unscoped format: name[@version] (public registry)
|
|
625
|
+
// name must not contain / (otherwise it might be a git shorthand)
|
|
651
626
|
const unscopedMatch = trimmed.match(/^([\w-]+)(?:@(.+))?$/);
|
|
652
627
|
if (!unscopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
|
|
653
628
|
const [, name, version] = unscopedMatch;
|
|
@@ -1121,6 +1096,369 @@ const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEB
|
|
|
1121
1096
|
if (external_node_fs_.existsSync(tempArchive)) external_node_fs_.unlinkSync(tempArchive);
|
|
1122
1097
|
}
|
|
1123
1098
|
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Skill Parser - SKILL.md parser
|
|
1101
|
+
*
|
|
1102
|
+
* Following agentskills.io specification: https://agentskills.io/specification
|
|
1103
|
+
*
|
|
1104
|
+
* SKILL.md format requirements:
|
|
1105
|
+
* - YAML frontmatter containing name and description (required)
|
|
1106
|
+
* - name: max 64 characters, lowercase letters, numbers, hyphens
|
|
1107
|
+
* - description: max 1024 characters
|
|
1108
|
+
* - Optional fields: license, compatibility, metadata, allowed-tools
|
|
1109
|
+
*/ /**
|
|
1110
|
+
* Skill validation error
|
|
1111
|
+
*/ class SkillValidationError extends Error {
|
|
1112
|
+
field;
|
|
1113
|
+
constructor(message, field){
|
|
1114
|
+
super(message), this.field = field;
|
|
1115
|
+
this.name = 'SkillValidationError';
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Simple YAML frontmatter parser
|
|
1120
|
+
* Parses --- delimited YAML header
|
|
1121
|
+
*
|
|
1122
|
+
* Supports:
|
|
1123
|
+
* - Basic key: value pairs
|
|
1124
|
+
* - Multiline strings (| and >)
|
|
1125
|
+
* - Nested objects (one level deep, for metadata field)
|
|
1126
|
+
*/ function parseFrontmatter(content) {
|
|
1127
|
+
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
1128
|
+
const match = content.match(frontmatterRegex);
|
|
1129
|
+
if (!match) return {
|
|
1130
|
+
data: {},
|
|
1131
|
+
content
|
|
1132
|
+
};
|
|
1133
|
+
const yamlContent = match[1];
|
|
1134
|
+
const markdownContent = match[2];
|
|
1135
|
+
// Simple YAML parsing (supports basic key: value, one level of nesting,
|
|
1136
|
+
// block scalars (| and >), and plain scalars spanning multiple indented lines)
|
|
1137
|
+
const data = {};
|
|
1138
|
+
const lines = yamlContent.split('\n');
|
|
1139
|
+
let currentKey = '';
|
|
1140
|
+
let currentValue = '';
|
|
1141
|
+
let inMultiline = false;
|
|
1142
|
+
let inNestedObject = false;
|
|
1143
|
+
let inPlainScalar = false;
|
|
1144
|
+
let nestedObject = {};
|
|
1145
|
+
/**
|
|
1146
|
+
* Save the current key/value accumulated so far, then reset state.
|
|
1147
|
+
*/ function flushCurrent() {
|
|
1148
|
+
if (!currentKey) return;
|
|
1149
|
+
if (inNestedObject) {
|
|
1150
|
+
data[currentKey] = nestedObject;
|
|
1151
|
+
nestedObject = {};
|
|
1152
|
+
inNestedObject = false;
|
|
1153
|
+
} else if (inPlainScalar || inMultiline) {
|
|
1154
|
+
data[currentKey] = currentValue.trim();
|
|
1155
|
+
inPlainScalar = false;
|
|
1156
|
+
inMultiline = false;
|
|
1157
|
+
} else data[currentKey] = parseYamlValue(currentValue.trim());
|
|
1158
|
+
currentKey = '';
|
|
1159
|
+
currentValue = '';
|
|
1160
|
+
}
|
|
1161
|
+
for (const line of lines){
|
|
1162
|
+
const trimmedLine = line.trim();
|
|
1163
|
+
if (!trimmedLine || trimmedLine.startsWith('#')) continue;
|
|
1164
|
+
const isIndented = line.startsWith(' ');
|
|
1165
|
+
// ---- Inside a block scalar (| or >) ----
|
|
1166
|
+
if (inMultiline) {
|
|
1167
|
+
if (isIndented) {
|
|
1168
|
+
currentValue += (currentValue ? '\n' : '') + line.slice(2);
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
// Unindented line ends the block scalar — fall through to top-level parsing
|
|
1172
|
+
flushCurrent();
|
|
1173
|
+
}
|
|
1174
|
+
// ---- Inside a plain scalar (multiline value without | or >) ----
|
|
1175
|
+
if (inPlainScalar) {
|
|
1176
|
+
if (isIndented) {
|
|
1177
|
+
// Continuation line: join with a space (YAML plain scalar folding)
|
|
1178
|
+
currentValue += ` ${trimmedLine}`;
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
// Unindented line ends the plain scalar — fall through to top-level parsing
|
|
1182
|
+
flushCurrent();
|
|
1183
|
+
}
|
|
1184
|
+
// ---- Inside a nested object ----
|
|
1185
|
+
if (inNestedObject && isIndented) {
|
|
1186
|
+
const nestedMatch = line.match(/^ {2}([a-zA-Z_-]+):\s*(.*)$/);
|
|
1187
|
+
if (nestedMatch) {
|
|
1188
|
+
const [, nestedKey, nestedValue] = nestedMatch;
|
|
1189
|
+
nestedObject[nestedKey] = parseYamlValue(nestedValue.trim());
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
// Indented line that isn't a nested key:value — this key was actually
|
|
1193
|
+
// a plain scalar, not a nested object. Switch modes.
|
|
1194
|
+
inNestedObject = false;
|
|
1195
|
+
inPlainScalar = true;
|
|
1196
|
+
currentValue = trimmedLine;
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
// ---- Top-level key: value ----
|
|
1200
|
+
const keyValueMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
|
|
1201
|
+
if (keyValueMatch) {
|
|
1202
|
+
flushCurrent();
|
|
1203
|
+
currentKey = keyValueMatch[1];
|
|
1204
|
+
currentValue = keyValueMatch[2];
|
|
1205
|
+
if ('|' === currentValue || '>' === currentValue) {
|
|
1206
|
+
inMultiline = true;
|
|
1207
|
+
currentValue = '';
|
|
1208
|
+
} else if ('' === currentValue) {
|
|
1209
|
+
// Empty value — could be nested object or plain scalar; peek at next lines
|
|
1210
|
+
inNestedObject = true;
|
|
1211
|
+
nestedObject = {};
|
|
1212
|
+
}
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
// ---- Unindented line that isn't key:value while in nested object ----
|
|
1216
|
+
if (inNestedObject) flushCurrent();
|
|
1217
|
+
}
|
|
1218
|
+
// Save last accumulated value
|
|
1219
|
+
flushCurrent();
|
|
1220
|
+
return {
|
|
1221
|
+
data,
|
|
1222
|
+
content: markdownContent
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Parse YAML value
|
|
1227
|
+
*/ function parseYamlValue(value) {
|
|
1228
|
+
if (!value) return '';
|
|
1229
|
+
// Boolean value
|
|
1230
|
+
if ('true' === value) return true;
|
|
1231
|
+
if ('false' === value) return false;
|
|
1232
|
+
// Number
|
|
1233
|
+
if (/^-?\d+$/.test(value)) return parseInt(value, 10);
|
|
1234
|
+
if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
|
|
1235
|
+
// Remove quotes
|
|
1236
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
1237
|
+
return value;
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Validate skill name format
|
|
1241
|
+
*
|
|
1242
|
+
* Specification requirements:
|
|
1243
|
+
* - Max 64 characters
|
|
1244
|
+
* - Only lowercase letters, numbers, hyphens allowed
|
|
1245
|
+
* - Cannot start or end with hyphen
|
|
1246
|
+
* - Cannot contain consecutive hyphens
|
|
1247
|
+
*/ function validateSkillName(name) {
|
|
1248
|
+
if (!name) throw new SkillValidationError('Skill name is required', 'name');
|
|
1249
|
+
if (name.length > 64) throw new SkillValidationError('Skill name must be at most 64 characters', 'name');
|
|
1250
|
+
if (!/^[a-z0-9]/.test(name)) throw new SkillValidationError('Skill name must start with a lowercase letter or number', 'name');
|
|
1251
|
+
if (!/[a-z0-9]$/.test(name)) throw new SkillValidationError('Skill name must end with a lowercase letter or number', 'name');
|
|
1252
|
+
if (/--/.test(name)) throw new SkillValidationError('Skill name cannot contain consecutive hyphens', 'name');
|
|
1253
|
+
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');
|
|
1254
|
+
// Single character name
|
|
1255
|
+
if (1 === name.length && !/^[a-z0-9]$/.test(name)) throw new SkillValidationError('Single character skill name must be a lowercase letter or number', 'name');
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Validate skill description
|
|
1259
|
+
*
|
|
1260
|
+
* Specification requirements:
|
|
1261
|
+
* - Max 1024 characters
|
|
1262
|
+
* - Angle brackets are allowed per agentskills.io spec
|
|
1263
|
+
*/ function validateSkillDescription(description) {
|
|
1264
|
+
if (!description) throw new SkillValidationError('Skill description is required', 'description');
|
|
1265
|
+
if (description.length > 1024) throw new SkillValidationError('Skill description must be at most 1024 characters', 'description');
|
|
1266
|
+
// Note: angle brackets are allowed per agentskills.io spec
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Parse SKILL.md content
|
|
1270
|
+
*
|
|
1271
|
+
* @param content - SKILL.md file content
|
|
1272
|
+
* @param options - Parse options
|
|
1273
|
+
* @returns Parsed skill info, or null if format is invalid
|
|
1274
|
+
* @throws SkillValidationError if validation fails in strict mode
|
|
1275
|
+
*/ function parseSkillMd(content, options = {}) {
|
|
1276
|
+
const { strict = false } = options;
|
|
1277
|
+
try {
|
|
1278
|
+
const { data, content: body } = parseFrontmatter(content);
|
|
1279
|
+
// Check required fields
|
|
1280
|
+
if (!data.name || !data.description) {
|
|
1281
|
+
if (strict) throw new SkillValidationError('SKILL.md must have name and description in frontmatter');
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
const name = String(data.name);
|
|
1285
|
+
const description = String(data.description);
|
|
1286
|
+
// Validate field format
|
|
1287
|
+
if (strict) {
|
|
1288
|
+
validateSkillName(name);
|
|
1289
|
+
validateSkillDescription(description);
|
|
1290
|
+
}
|
|
1291
|
+
// Parse allowed-tools
|
|
1292
|
+
let allowedTools;
|
|
1293
|
+
if (data['allowed-tools']) {
|
|
1294
|
+
const toolsStr = String(data['allowed-tools']);
|
|
1295
|
+
allowedTools = toolsStr.split(/\s+/).filter(Boolean);
|
|
1296
|
+
}
|
|
1297
|
+
return {
|
|
1298
|
+
name,
|
|
1299
|
+
description,
|
|
1300
|
+
version: data.version ? String(data.version) : void 0,
|
|
1301
|
+
license: data.license ? String(data.license) : void 0,
|
|
1302
|
+
compatibility: data.compatibility ? String(data.compatibility) : void 0,
|
|
1303
|
+
metadata: data.metadata,
|
|
1304
|
+
allowedTools,
|
|
1305
|
+
content: body,
|
|
1306
|
+
rawContent: content
|
|
1307
|
+
};
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
if (error instanceof SkillValidationError) throw error;
|
|
1310
|
+
if (strict) throw new SkillValidationError(`Failed to parse SKILL.md: ${error}`);
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Parse SKILL.md from file path
|
|
1316
|
+
*/ function parseSkillMdFile(filePath, options = {}) {
|
|
1317
|
+
if (!external_node_fs_.existsSync(filePath)) {
|
|
1318
|
+
if (options.strict) throw new SkillValidationError(`SKILL.md not found: ${filePath}`);
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
1321
|
+
const content = external_node_fs_.readFileSync(filePath, 'utf-8');
|
|
1322
|
+
return parseSkillMd(content, options);
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Parse SKILL.md from skill directory
|
|
1326
|
+
*/ function parseSkillFromDir(dirPath, options = {}) {
|
|
1327
|
+
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
|
|
1328
|
+
return parseSkillMdFile(skillMdPath, options);
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Check if directory contains valid SKILL.md
|
|
1332
|
+
*/ function hasValidSkillMd(dirPath) {
|
|
1333
|
+
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
|
|
1334
|
+
if (!external_node_fs_.existsSync(skillMdPath)) return false;
|
|
1335
|
+
try {
|
|
1336
|
+
const skill = parseSkillMdFile(skillMdPath);
|
|
1337
|
+
return null !== skill;
|
|
1338
|
+
} catch {
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
const SKIP_DIRS = [
|
|
1343
|
+
'node_modules',
|
|
1344
|
+
'.git',
|
|
1345
|
+
'dist',
|
|
1346
|
+
'build',
|
|
1347
|
+
'__pycache__'
|
|
1348
|
+
];
|
|
1349
|
+
const MAX_DISCOVER_DEPTH = 5;
|
|
1350
|
+
const PRIORITY_SKILL_DIRS = [
|
|
1351
|
+
'skills',
|
|
1352
|
+
'.agents/skills',
|
|
1353
|
+
'.cursor/skills',
|
|
1354
|
+
'.claude/skills',
|
|
1355
|
+
'.windsurf/skills',
|
|
1356
|
+
'.github/skills'
|
|
1357
|
+
];
|
|
1358
|
+
function findSkillDirsRecursive(dir, depth, maxDepth, visitedDirs) {
|
|
1359
|
+
if (depth > maxDepth) return [];
|
|
1360
|
+
const resolvedDir = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir);
|
|
1361
|
+
if (visitedDirs.has(resolvedDir)) return [];
|
|
1362
|
+
if (!external_node_fs_.existsSync(dir) || !external_node_fs_.statSync(dir).isDirectory()) return [];
|
|
1363
|
+
visitedDirs.add(resolvedDir);
|
|
1364
|
+
const results = [];
|
|
1365
|
+
let entries;
|
|
1366
|
+
try {
|
|
1367
|
+
entries = external_node_fs_.readdirSync(dir);
|
|
1368
|
+
} catch {
|
|
1369
|
+
return [];
|
|
1370
|
+
}
|
|
1371
|
+
for (const entry of entries){
|
|
1372
|
+
if (SKIP_DIRS.includes(entry)) continue;
|
|
1373
|
+
const fullPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
|
|
1374
|
+
const resolvedFull = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(fullPath);
|
|
1375
|
+
if (visitedDirs.has(resolvedFull)) continue;
|
|
1376
|
+
let stat;
|
|
1377
|
+
try {
|
|
1378
|
+
stat = external_node_fs_.statSync(fullPath);
|
|
1379
|
+
} catch {
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
if (!!stat.isDirectory()) {
|
|
1383
|
+
if (hasValidSkillMd(fullPath)) results.push(fullPath);
|
|
1384
|
+
results.push(...findSkillDirsRecursive(fullPath, depth + 1, maxDepth, visitedDirs));
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return results;
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Discover all skills in a directory by scanning for SKILL.md files.
|
|
1391
|
+
*
|
|
1392
|
+
* Strategy:
|
|
1393
|
+
* 1. Check root for SKILL.md
|
|
1394
|
+
* 2. Search priority directories (skills/, .agents/skills/, .cursor/skills/, etc.)
|
|
1395
|
+
* 3. Fall back to recursive search (max depth 5, skip node_modules, .git, dist, etc.)
|
|
1396
|
+
*
|
|
1397
|
+
* @param basePath - Root directory to search
|
|
1398
|
+
* @returns List of parsed skills with their directory paths (absolute)
|
|
1399
|
+
*/ function discoverSkillsInDir(basePath) {
|
|
1400
|
+
const resolvedBase = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(basePath);
|
|
1401
|
+
const results = [];
|
|
1402
|
+
const seenNames = new Set();
|
|
1403
|
+
function addSkill(dirPath) {
|
|
1404
|
+
const skill = parseSkillFromDir(dirPath);
|
|
1405
|
+
if (skill && !seenNames.has(skill.name)) {
|
|
1406
|
+
seenNames.add(skill.name);
|
|
1407
|
+
results.push({
|
|
1408
|
+
...skill,
|
|
1409
|
+
dirPath: __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dirPath)
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
if (hasValidSkillMd(resolvedBase)) addSkill(resolvedBase);
|
|
1414
|
+
// Track visited directories to avoid redundant I/O during recursive scan
|
|
1415
|
+
const visitedDirs = new Set();
|
|
1416
|
+
for (const sub of PRIORITY_SKILL_DIRS){
|
|
1417
|
+
const dir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(resolvedBase, sub);
|
|
1418
|
+
if (!!external_node_fs_.existsSync(dir) && !!external_node_fs_.statSync(dir).isDirectory()) {
|
|
1419
|
+
visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir));
|
|
1420
|
+
try {
|
|
1421
|
+
const entries = external_node_fs_.readdirSync(dir);
|
|
1422
|
+
for (const entry of entries){
|
|
1423
|
+
const skillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
|
|
1424
|
+
try {
|
|
1425
|
+
if (external_node_fs_.statSync(skillDir).isDirectory() && hasValidSkillMd(skillDir)) {
|
|
1426
|
+
addSkill(skillDir);
|
|
1427
|
+
visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(skillDir));
|
|
1428
|
+
}
|
|
1429
|
+
} catch {
|
|
1430
|
+
// Skip entries that can't be stat'd (race condition, permission, etc.)
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
} catch {
|
|
1434
|
+
// Skip if unreadable
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
const recursiveDirs = findSkillDirsRecursive(resolvedBase, 0, MAX_DISCOVER_DEPTH, visitedDirs);
|
|
1439
|
+
for (const skillDir of recursiveDirs)addSkill(skillDir);
|
|
1440
|
+
return results;
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Filter skills by name (case-insensitive exact match).
|
|
1444
|
+
*
|
|
1445
|
+
* Note: an empty `names` array returns an empty result (not all skills).
|
|
1446
|
+
* Callers should check `names.length` before calling if "no filter = all" is desired.
|
|
1447
|
+
*
|
|
1448
|
+
* @param skills - List of discovered skills
|
|
1449
|
+
* @param names - Skill names to match (e.g. from --skill pdf commit)
|
|
1450
|
+
* @returns Skills whose name matches any of the given names
|
|
1451
|
+
*/ function filterSkillsByName(skills, names) {
|
|
1452
|
+
const normalized = names.map((n)=>n.toLowerCase());
|
|
1453
|
+
return skills.filter((skill)=>{
|
|
1454
|
+
// Match against SKILL.md name field
|
|
1455
|
+
if (normalized.includes(skill.name.toLowerCase())) return true;
|
|
1456
|
+
// Also match against the directory name (basename of dirPath)
|
|
1457
|
+
// Users naturally refer to skills by their directory name
|
|
1458
|
+
const dirName = __WEBPACK_EXTERNAL_MODULE_node_path__.basename(skill.dirPath).toLowerCase();
|
|
1459
|
+
return normalized.includes(dirName);
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1124
1462
|
/**
|
|
1125
1463
|
* Installer - Multi-Agent installer
|
|
1126
1464
|
*
|
|
@@ -1131,6 +1469,10 @@ const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEB
|
|
|
1131
1469
|
* Reference: https://github.com/vercel-labs/add-skill/blob/main/src/installer.ts
|
|
1132
1470
|
*/ const installer_AGENTS_DIR = '.agents';
|
|
1133
1471
|
const installer_SKILLS_SUBDIR = 'skills';
|
|
1472
|
+
/**
|
|
1473
|
+
* Marker comment in auto-generated Cursor bridge rule files.
|
|
1474
|
+
* Used to distinguish auto-generated files from manually created ones.
|
|
1475
|
+
*/ const CURSOR_BRIDGE_MARKER = '<!-- reskill:auto-generated -->';
|
|
1134
1476
|
/**
|
|
1135
1477
|
* Default files to exclude when copying skills
|
|
1136
1478
|
* These files are typically used for repository metadata and should not be copied to agent directories
|
|
@@ -1309,45 +1651,50 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1309
1651
|
error: 'Invalid skill name: potential path traversal detected'
|
|
1310
1652
|
};
|
|
1311
1653
|
try {
|
|
1654
|
+
let result;
|
|
1312
1655
|
// Copy mode: directly copy to agent location
|
|
1313
1656
|
if ('copy' === installMode) {
|
|
1314
1657
|
installer_ensureDir(agentDir);
|
|
1315
1658
|
installer_remove(agentDir);
|
|
1316
1659
|
copyDirectory(sourcePath, agentDir);
|
|
1317
|
-
|
|
1660
|
+
result = {
|
|
1318
1661
|
success: true,
|
|
1319
1662
|
path: agentDir,
|
|
1320
1663
|
mode: 'copy'
|
|
1321
1664
|
};
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
// Symlink failed, fallback to copy
|
|
1330
|
-
try {
|
|
1331
|
-
installer_remove(agentDir);
|
|
1332
|
-
} catch {
|
|
1333
|
-
// Ignore cleanup errors
|
|
1334
|
-
}
|
|
1335
|
-
installer_ensureDir(agentDir);
|
|
1336
|
-
copyDirectory(sourcePath, agentDir);
|
|
1337
|
-
return {
|
|
1665
|
+
} else {
|
|
1666
|
+
// Symlink mode: copy to canonical location, then create symlink
|
|
1667
|
+
installer_ensureDir(canonicalDir);
|
|
1668
|
+
installer_remove(canonicalDir);
|
|
1669
|
+
copyDirectory(sourcePath, canonicalDir);
|
|
1670
|
+
const symlinkCreated = await installer_createSymlink(canonicalDir, agentDir);
|
|
1671
|
+
if (symlinkCreated) result = {
|
|
1338
1672
|
success: true,
|
|
1339
1673
|
path: agentDir,
|
|
1340
1674
|
canonicalPath: canonicalDir,
|
|
1341
|
-
mode: 'symlink'
|
|
1342
|
-
symlinkFailed: true
|
|
1675
|
+
mode: 'symlink'
|
|
1343
1676
|
};
|
|
1677
|
+
else {
|
|
1678
|
+
// Symlink failed, fallback to copy
|
|
1679
|
+
try {
|
|
1680
|
+
installer_remove(agentDir);
|
|
1681
|
+
} catch {
|
|
1682
|
+
// Ignore cleanup errors
|
|
1683
|
+
}
|
|
1684
|
+
installer_ensureDir(agentDir);
|
|
1685
|
+
copyDirectory(sourcePath, agentDir);
|
|
1686
|
+
result = {
|
|
1687
|
+
success: true,
|
|
1688
|
+
path: agentDir,
|
|
1689
|
+
canonicalPath: canonicalDir,
|
|
1690
|
+
mode: 'symlink',
|
|
1691
|
+
symlinkFailed: true
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1344
1694
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
canonicalPath: canonicalDir,
|
|
1349
|
-
mode: 'symlink'
|
|
1350
|
-
};
|
|
1695
|
+
// Create Cursor bridge rule file (project-level only)
|
|
1696
|
+
if ('cursor' === agentType && !this.isGlobal) this.createCursorBridgeRule(sanitized, sourcePath);
|
|
1697
|
+
return result;
|
|
1351
1698
|
} catch (error) {
|
|
1352
1699
|
return {
|
|
1353
1700
|
success: false,
|
|
@@ -1385,6 +1732,8 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1385
1732
|
const skillPath = this.getAgentSkillPath(skillName, agentType);
|
|
1386
1733
|
if (!external_node_fs_.existsSync(skillPath)) return false;
|
|
1387
1734
|
installer_remove(skillPath);
|
|
1735
|
+
// Remove Cursor bridge rule file (project-level only)
|
|
1736
|
+
if ('cursor' === agentType && !this.isGlobal) this.removeCursorBridgeRule(installer_sanitizeName(skillName));
|
|
1388
1737
|
return true;
|
|
1389
1738
|
}
|
|
1390
1739
|
/**
|
|
@@ -1407,6 +1756,65 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1407
1756
|
withFileTypes: true
|
|
1408
1757
|
}).filter((entry)=>entry.isDirectory() || entry.isSymbolicLink()).map((entry)=>entry.name);
|
|
1409
1758
|
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Create a Cursor bridge rule file (.mdc) for the installed skill.
|
|
1761
|
+
*
|
|
1762
|
+
* Cursor does not natively read SKILL.md from .cursor/skills/.
|
|
1763
|
+
* This bridge file in .cursor/rules/ references the SKILL.md via @file directive,
|
|
1764
|
+
* allowing Cursor to discover and activate the skill based on the description.
|
|
1765
|
+
*
|
|
1766
|
+
* @param skillName - Sanitized skill name
|
|
1767
|
+
* @param sourcePath - Source directory containing SKILL.md
|
|
1768
|
+
*/ createCursorBridgeRule(skillName, sourcePath) {
|
|
1769
|
+
try {
|
|
1770
|
+
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(sourcePath, 'SKILL.md');
|
|
1771
|
+
if (!external_node_fs_.existsSync(skillMdPath)) return;
|
|
1772
|
+
const content = external_node_fs_.readFileSync(skillMdPath, 'utf-8');
|
|
1773
|
+
const parsed = parseSkillMd(content);
|
|
1774
|
+
if (!parsed || !parsed.description) return;
|
|
1775
|
+
const rulesDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cwd, '.cursor', 'rules');
|
|
1776
|
+
installer_ensureDir(rulesDir);
|
|
1777
|
+
// Do not overwrite manually created rule files (without auto-generated marker)
|
|
1778
|
+
const bridgePath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(rulesDir, `${skillName}.mdc`);
|
|
1779
|
+
if (external_node_fs_.existsSync(bridgePath)) {
|
|
1780
|
+
const existingContent = external_node_fs_.readFileSync(bridgePath, 'utf-8');
|
|
1781
|
+
if (!existingContent.includes(CURSOR_BRIDGE_MARKER)) return;
|
|
1782
|
+
}
|
|
1783
|
+
// Quote description to prevent YAML injection from special characters
|
|
1784
|
+
const safeDescription = parsed.description.replace(/"/g, '\\"');
|
|
1785
|
+
const agent = getAgentConfig('cursor');
|
|
1786
|
+
const bridgeContent = `---
|
|
1787
|
+
description: "${safeDescription}"
|
|
1788
|
+
globs:
|
|
1789
|
+
alwaysApply: false
|
|
1790
|
+
---
|
|
1791
|
+
|
|
1792
|
+
${CURSOR_BRIDGE_MARKER}
|
|
1793
|
+
@file ${agent.skillsDir}/${skillName}/SKILL.md
|
|
1794
|
+
`;
|
|
1795
|
+
external_node_fs_.writeFileSync(bridgePath, bridgeContent, 'utf-8');
|
|
1796
|
+
} catch {
|
|
1797
|
+
// Silently skip bridge file creation on errors
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Remove a Cursor bridge rule file (.mdc) for the uninstalled skill.
|
|
1802
|
+
*
|
|
1803
|
+
* Only removes files that contain the auto-generated marker to avoid
|
|
1804
|
+
* deleting manually created rule files.
|
|
1805
|
+
*
|
|
1806
|
+
* @param skillName - Sanitized skill name
|
|
1807
|
+
*/ removeCursorBridgeRule(skillName) {
|
|
1808
|
+
try {
|
|
1809
|
+
const bridgePath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cwd, '.cursor', 'rules', `${skillName}.mdc`);
|
|
1810
|
+
if (!external_node_fs_.existsSync(bridgePath)) return;
|
|
1811
|
+
const content = external_node_fs_.readFileSync(bridgePath, 'utf-8');
|
|
1812
|
+
if (!content.includes(CURSOR_BRIDGE_MARKER)) return;
|
|
1813
|
+
external_node_fs_.rmSync(bridgePath);
|
|
1814
|
+
} catch {
|
|
1815
|
+
// Silently skip bridge file removal on errors
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1410
1818
|
}
|
|
1411
1819
|
/**
|
|
1412
1820
|
* CacheManager - Manage global skill cache
|
|
@@ -2117,10 +2525,24 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
2117
2525
|
* - Monorepo: git@github.com:org/repo.git/subpath[@version]
|
|
2118
2526
|
*/ parseRef(ref) {
|
|
2119
2527
|
const raw = ref;
|
|
2528
|
+
// Extract #skillName fragment before any parsing (for both Git URLs and shorthand)
|
|
2529
|
+
let skillName;
|
|
2530
|
+
const hashIndex = ref.indexOf('#');
|
|
2531
|
+
if (hashIndex >= 0) {
|
|
2532
|
+
skillName = ref.slice(hashIndex + 1);
|
|
2533
|
+
ref = ref.slice(0, hashIndex);
|
|
2534
|
+
}
|
|
2120
2535
|
// First check if it's a Git URL (SSH, HTTPS, git://)
|
|
2121
2536
|
// For Git URLs, need special handling for version separator
|
|
2122
2537
|
// Format: git@host:user/repo.git[@version] or git@host:user/repo.git/subpath[@version]
|
|
2123
|
-
if (isGitUrl(ref))
|
|
2538
|
+
if (isGitUrl(ref)) {
|
|
2539
|
+
const parsed = this.parseGitUrlRef(ref);
|
|
2540
|
+
return {
|
|
2541
|
+
...parsed,
|
|
2542
|
+
raw,
|
|
2543
|
+
skillName
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2124
2546
|
// Standard format parsing for non-Git URLs
|
|
2125
2547
|
let remaining = ref;
|
|
2126
2548
|
let registry = this.defaultRegistry;
|
|
@@ -2139,18 +2561,33 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
2139
2561
|
}
|
|
2140
2562
|
// Parse owner/repo and possible subPath
|
|
2141
2563
|
// E.g.: user/repo or org/monorepo/skills/pdf
|
|
2564
|
+
// Also handle GitHub web URL style: owner/repo/tree/branch/path
|
|
2142
2565
|
const parts = remaining.split('/');
|
|
2143
2566
|
if (parts.length < 2) throw new Error(`Invalid skill reference: ${ref}. Expected format: owner/repo[@version]`);
|
|
2144
2567
|
const owner = parts[0];
|
|
2145
2568
|
const repo = parts[1];
|
|
2146
|
-
|
|
2569
|
+
let subPath;
|
|
2570
|
+
// Check for GitHub/GitLab web URL pattern: owner/repo/(tree|blob|raw)/branch/path
|
|
2571
|
+
// e.g. vercel-labs/skills/tree/main/skills/find-skills
|
|
2572
|
+
// Only apply this heuristic when no explicit @version is provided.
|
|
2573
|
+
// With @version, treat tree/blob/raw as literal directory names (standard monorepo subPath).
|
|
2574
|
+
if (parts.length >= 4 && [
|
|
2575
|
+
'tree',
|
|
2576
|
+
'blob',
|
|
2577
|
+
'raw'
|
|
2578
|
+
].includes(parts[2]) && !version) {
|
|
2579
|
+
const branch = parts[3];
|
|
2580
|
+
version = `branch:${branch}`;
|
|
2581
|
+
subPath = parts.length > 4 ? parts.slice(4).join('/') : void 0;
|
|
2582
|
+
} else subPath = parts.length > 2 ? parts.slice(2).join('/') : void 0;
|
|
2147
2583
|
return {
|
|
2148
2584
|
registry,
|
|
2149
2585
|
owner,
|
|
2150
2586
|
repo,
|
|
2151
2587
|
subPath,
|
|
2152
2588
|
version,
|
|
2153
|
-
raw
|
|
2589
|
+
raw,
|
|
2590
|
+
skillName
|
|
2154
2591
|
};
|
|
2155
2592
|
}
|
|
2156
2593
|
/**
|
|
@@ -2372,20 +2809,31 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
2372
2809
|
* Check if a reference is an HTTP/OSS URL (for archive downloads)
|
|
2373
2810
|
*
|
|
2374
2811
|
* Returns true for:
|
|
2375
|
-
* - http:// or https:// URLs
|
|
2376
|
-
* - Explicit oss:// or s3:// protocol URLs
|
|
2812
|
+
* - http:// or https:// URLs with archive file extensions (.tar.gz, .tgz, .zip, .tar)
|
|
2813
|
+
* - Explicit oss:// or s3:// protocol URLs (always treated as archive sources)
|
|
2377
2814
|
*
|
|
2378
2815
|
* Returns false for:
|
|
2379
2816
|
* - Git repository URLs (*.git)
|
|
2380
2817
|
* - GitHub/GitLab web URLs (/tree/, /blob/, /raw/)
|
|
2818
|
+
* - Bare HTTPS repo URLs without archive extensions (e.g., https://github.com/user/repo)
|
|
2819
|
+
* These are treated as Git references and handled by GitResolver.
|
|
2381
2820
|
*/ static isHttpUrl(ref) {
|
|
2382
2821
|
// Remove version suffix for checking (e.g., url@v1.0.0)
|
|
2383
2822
|
const urlPart = ref.split('@')[0];
|
|
2384
|
-
//
|
|
2385
|
-
if (urlPart.
|
|
2386
|
-
//
|
|
2387
|
-
if (
|
|
2388
|
-
|
|
2823
|
+
// oss:// and s3:// are always archive download sources
|
|
2824
|
+
if (urlPart.startsWith('oss://') || urlPart.startsWith('s3://')) return true;
|
|
2825
|
+
// For http:// and https:// URLs, distinguish between Git repos and archive downloads
|
|
2826
|
+
if (urlPart.startsWith('http://') || urlPart.startsWith('https://')) {
|
|
2827
|
+
// Exclude Git repository URLs (ending with .git)
|
|
2828
|
+
if (urlPart.endsWith('.git')) return false;
|
|
2829
|
+
// Exclude GitHub/GitLab web URLs (containing /tree/, /blob/, /raw/)
|
|
2830
|
+
if (/\/(tree|blob|raw)\//.test(urlPart)) return false;
|
|
2831
|
+
// Only classify as HTTP archive if URL has a recognized archive extension.
|
|
2832
|
+
// Bare HTTPS URLs like https://github.com/user/repo are Git references,
|
|
2833
|
+
// not archive downloads, and should fall through to GitResolver.
|
|
2834
|
+
return /\.(tar\.gz|tgz|zip|tar)$/i.test(urlPart);
|
|
2835
|
+
}
|
|
2836
|
+
return false;
|
|
2389
2837
|
}
|
|
2390
2838
|
/**
|
|
2391
2839
|
* Parse an HTTP/OSS URL reference
|
|
@@ -2711,13 +3159,14 @@ class RegistryClient {
|
|
|
2711
3159
|
this.config = config;
|
|
2712
3160
|
}
|
|
2713
3161
|
/**
|
|
2714
|
-
* Get API base URL (registry +
|
|
3162
|
+
* Get API base URL (registry + /api)
|
|
2715
3163
|
*
|
|
2716
|
-
*
|
|
3164
|
+
* All registries use the unified '/api' prefix.
|
|
3165
|
+
*
|
|
3166
|
+
* @returns Base URL for API calls, e.g., 'https://example.com/api'
|
|
2717
3167
|
*/ getApiBase() {
|
|
2718
|
-
const prefix = this.config.apiPrefix || '/api';
|
|
2719
3168
|
const registry = this.config.registry.endsWith('/') ? this.config.registry.slice(0, -1) : this.config.registry;
|
|
2720
|
-
return `${registry}
|
|
3169
|
+
return `${registry}/api`;
|
|
2721
3170
|
}
|
|
2722
3171
|
/**
|
|
2723
3172
|
* Get authorization headers
|
|
@@ -2732,7 +3181,7 @@ class RegistryClient {
|
|
|
2732
3181
|
/**
|
|
2733
3182
|
* Get current user info (whoami)
|
|
2734
3183
|
*/ async whoami() {
|
|
2735
|
-
const url = `${this.getApiBase()}/auth/me`;
|
|
3184
|
+
const url = `${this.getApiBase()}/skill-auth/me`;
|
|
2736
3185
|
const response = await fetch(url, {
|
|
2737
3186
|
method: 'GET',
|
|
2738
3187
|
headers: this.getAuthHeaders()
|
|
@@ -2744,13 +3193,13 @@ class RegistryClient {
|
|
|
2744
3193
|
/**
|
|
2745
3194
|
* CLI login - verify token and get user info
|
|
2746
3195
|
*
|
|
2747
|
-
* Calls POST /api/auth/login-cli to validate the token and retrieve user information.
|
|
3196
|
+
* Calls POST /api/skill-auth/login-cli to validate the token and retrieve user information.
|
|
2748
3197
|
* This is the preferred method for CLI authentication.
|
|
2749
3198
|
*
|
|
2750
3199
|
* @returns User information if authentication succeeds
|
|
2751
3200
|
* @throws RegistryError if authentication fails
|
|
2752
3201
|
*/ async loginCli() {
|
|
2753
|
-
const url = `${this.getApiBase()}/auth/login-cli`;
|
|
3202
|
+
const url = `${this.getApiBase()}/skill-auth/login-cli`;
|
|
2754
3203
|
const response = await fetch(url, {
|
|
2755
3204
|
method: 'POST',
|
|
2756
3205
|
headers: this.getAuthHeaders()
|
|
@@ -2782,7 +3231,7 @@ class RegistryClient {
|
|
|
2782
3231
|
if (external_node_fs_.existsSync(filePath)) {
|
|
2783
3232
|
const content = external_node_fs_.readFileSync(filePath);
|
|
2784
3233
|
const stat = external_node_fs_.statSync(filePath);
|
|
2785
|
-
//
|
|
3234
|
+
// Prepend shortName as top-level directory if provided
|
|
2786
3235
|
const entryName = shortName ? `${shortName}/${file}` : file;
|
|
2787
3236
|
tarPack.entry({
|
|
2788
3237
|
name: entryName,
|
|
@@ -2796,15 +3245,15 @@ class RegistryClient {
|
|
|
2796
3245
|
});
|
|
2797
3246
|
}
|
|
2798
3247
|
// ============================================================================
|
|
2799
|
-
// Skill Info Methods (
|
|
3248
|
+
// Skill Info Methods (web-published skill support)
|
|
2800
3249
|
// ============================================================================
|
|
2801
3250
|
/**
|
|
2802
|
-
*
|
|
2803
|
-
*
|
|
3251
|
+
* Get basic skill info (including source_type).
|
|
3252
|
+
* Used by the install command to determine the installation logic branch.
|
|
2804
3253
|
*
|
|
2805
|
-
* @param skillName -
|
|
2806
|
-
* @returns
|
|
2807
|
-
* @throws RegistryError
|
|
3254
|
+
* @param skillName - Full skill name, e.g., @kanyun/my-skill
|
|
3255
|
+
* @returns Basic skill information
|
|
3256
|
+
* @throws RegistryError if skill not found or request failed
|
|
2808
3257
|
*/ async getSkillInfo(skillName) {
|
|
2809
3258
|
const url = `${this.getApiBase()}/skills/${encodeURIComponent(skillName)}`;
|
|
2810
3259
|
const response = await fetch(url, {
|
|
@@ -2813,15 +3262,50 @@ class RegistryClient {
|
|
|
2813
3262
|
});
|
|
2814
3263
|
if (!response.ok) {
|
|
2815
3264
|
const data = await response.json();
|
|
2816
|
-
//
|
|
3265
|
+
// Return a clear "not found" error for 404 responses
|
|
2817
3266
|
if (404 === response.status) throw new RegistryError(`Skill not found: ${skillName}`, response.status, data);
|
|
2818
3267
|
throw new RegistryError(data.error || `Failed to get skill info: ${response.statusText}`, response.status, data);
|
|
2819
3268
|
}
|
|
2820
|
-
// API
|
|
3269
|
+
// API response format: { success: true, data: { ... } }
|
|
2821
3270
|
const responseData = await response.json();
|
|
2822
3271
|
return responseData.data || responseData;
|
|
2823
3272
|
}
|
|
2824
3273
|
// ============================================================================
|
|
3274
|
+
// Search Methods
|
|
3275
|
+
// ============================================================================
|
|
3276
|
+
/**
|
|
3277
|
+
* Search for skills in the registry
|
|
3278
|
+
*
|
|
3279
|
+
* @param query - Search query string
|
|
3280
|
+
* @param options - Search options (limit, offset)
|
|
3281
|
+
* @returns Array of matching skills
|
|
3282
|
+
* @throws RegistryError if the request fails
|
|
3283
|
+
*
|
|
3284
|
+
* @example
|
|
3285
|
+
* const results = await client.search('typescript');
|
|
3286
|
+
* const results = await client.search('planning', { limit: 5 });
|
|
3287
|
+
*/ async search(query, options = {}) {
|
|
3288
|
+
const params = new URLSearchParams({
|
|
3289
|
+
q: query
|
|
3290
|
+
});
|
|
3291
|
+
if (void 0 !== options.limit) params.set('limit', String(options.limit));
|
|
3292
|
+
if (void 0 !== options.offset) params.set('offset', String(options.offset));
|
|
3293
|
+
const url = `${this.getApiBase()}/skills?${params.toString()}`;
|
|
3294
|
+
const response = await fetch(url, {
|
|
3295
|
+
method: 'GET',
|
|
3296
|
+
headers: this.getAuthHeaders()
|
|
3297
|
+
});
|
|
3298
|
+
if (!response.ok) {
|
|
3299
|
+
const data = await response.json();
|
|
3300
|
+
throw new RegistryError(data.error || `Search failed: ${response.status}`, response.status, data);
|
|
3301
|
+
}
|
|
3302
|
+
const data = await response.json();
|
|
3303
|
+
return {
|
|
3304
|
+
items: data.data || [],
|
|
3305
|
+
total: data.meta?.pagination?.totalItems ?? data.data?.length ?? 0
|
|
3306
|
+
};
|
|
3307
|
+
}
|
|
3308
|
+
// ============================================================================
|
|
2825
3309
|
// Download Methods (Step 3.3)
|
|
2826
3310
|
// ============================================================================
|
|
2827
3311
|
/**
|
|
@@ -2834,12 +3318,12 @@ class RegistryClient {
|
|
|
2834
3318
|
*
|
|
2835
3319
|
* @example
|
|
2836
3320
|
* await client.resolveVersion('@kanyun/test-skill', 'latest') // '2.4.5'
|
|
2837
|
-
* await client.resolveVersion('@kanyun/test-skill', '2.4.5') // '2.4.5' (
|
|
3321
|
+
* await client.resolveVersion('@kanyun/test-skill', '2.4.5') // '2.4.5' (returned as-is)
|
|
2838
3322
|
*/ async resolveVersion(skillName, tagOrVersion) {
|
|
2839
3323
|
const version = tagOrVersion || 'latest';
|
|
2840
|
-
//
|
|
3324
|
+
// If it's already a semver version number, return as-is
|
|
2841
3325
|
if (/^\d+\.\d+\.\d+/.test(version)) return version;
|
|
2842
|
-
//
|
|
3326
|
+
// Otherwise treat it as a tag and query dist-tags
|
|
2843
3327
|
const url = `${this.getApiBase()}/skills/${encodeURIComponent(skillName)}`;
|
|
2844
3328
|
const response = await fetch(url, {
|
|
2845
3329
|
method: 'GET',
|
|
@@ -2849,14 +3333,14 @@ class RegistryClient {
|
|
|
2849
3333
|
const data = await response.json();
|
|
2850
3334
|
throw new RegistryError(data.error || `Failed to fetch skill metadata: ${response.status}`, response.status, data);
|
|
2851
3335
|
}
|
|
2852
|
-
// API
|
|
3336
|
+
// API response format: { success: true, data: { dist_tags: [{ tag, version }] } }
|
|
2853
3337
|
const responseData = await response.json();
|
|
2854
|
-
//
|
|
3338
|
+
// Prefer npm-style dist-tags if present
|
|
2855
3339
|
if (responseData['dist-tags']) {
|
|
2856
3340
|
const resolvedVersion = responseData['dist-tags'][version];
|
|
2857
3341
|
if (resolvedVersion) return resolvedVersion;
|
|
2858
3342
|
}
|
|
2859
|
-
//
|
|
3343
|
+
// Fall back to reskill-app's dist_tags array format
|
|
2860
3344
|
const distTags = responseData.data?.dist_tags;
|
|
2861
3345
|
if (distTags && Array.isArray(distTags)) {
|
|
2862
3346
|
const tagEntry = distTags.find((t)=>t.tag === version);
|
|
@@ -2937,11 +3421,11 @@ class RegistryClient {
|
|
|
2937
3421
|
* @example
|
|
2938
3422
|
* RegistryClient.verifyIntegrity(buffer, 'sha256-abc123...') // true or false
|
|
2939
3423
|
*/ static verifyIntegrity(content, expectedIntegrity) {
|
|
2940
|
-
//
|
|
3424
|
+
// Parse integrity format: algorithm-hash
|
|
2941
3425
|
const match = expectedIntegrity.match(/^(\w+)-(.+)$/);
|
|
2942
3426
|
if (!match) throw new Error(`Invalid integrity format: ${expectedIntegrity}`);
|
|
2943
3427
|
const [, algorithm, expectedHash] = match;
|
|
2944
|
-
//
|
|
3428
|
+
// Only sha256 and sha512 are supported
|
|
2945
3429
|
if ('sha256' !== algorithm && 'sha512' !== algorithm) throw new Error(`Unsupported integrity algorithm: ${algorithm}`);
|
|
2946
3430
|
const actualHash = __WEBPACK_EXTERNAL_MODULE_node_crypto__.createHash(algorithm).update(content).digest('base64');
|
|
2947
3431
|
return actualHash === expectedHash;
|
|
@@ -2953,7 +3437,7 @@ class RegistryClient {
|
|
|
2953
3437
|
* Publish a skill to the registry
|
|
2954
3438
|
*/ async publish(skillName, payload, skillPath, options = {}) {
|
|
2955
3439
|
const url = `${this.getApiBase()}/skills/publish`;
|
|
2956
|
-
//
|
|
3440
|
+
// Extract short name as tarball top-level directory (without scope prefix)
|
|
2957
3441
|
const shortName = getShortName(skillName);
|
|
2958
3442
|
// Create tarball with short name as top-level directory
|
|
2959
3443
|
const tarball = await this.createTarball(skillPath, payload.files, shortName);
|
|
@@ -3192,23 +3676,23 @@ class RegistryResolver {
|
|
|
3192
3676
|
* - HTTP/OSS: https://example.com/skill.tar.gz
|
|
3193
3677
|
* - Registry shorthand: github:user/repo, gitlab:org/repo
|
|
3194
3678
|
*/ static isRegistryRef(ref) {
|
|
3195
|
-
//
|
|
3679
|
+
// Exclude Git SSH format (git@...)
|
|
3196
3680
|
if (ref.startsWith('git@') || ref.startsWith('git://')) return false;
|
|
3197
|
-
//
|
|
3681
|
+
// Exclude URLs ending with .git
|
|
3198
3682
|
if (ref.includes('.git')) return false;
|
|
3199
|
-
//
|
|
3683
|
+
// Exclude HTTP/HTTPS/OSS URLs
|
|
3200
3684
|
if (ref.startsWith('http://') || ref.startsWith('https://') || ref.startsWith('oss://') || ref.startsWith('s3://')) return false;
|
|
3201
|
-
//
|
|
3202
|
-
//
|
|
3685
|
+
// Exclude registry shorthand format (github:, gitlab:, custom.com:)
|
|
3686
|
+
// These follow "registry:owner/repo" pattern, not "@scope/name"
|
|
3203
3687
|
if (/^[a-zA-Z0-9.-]+:[^@]/.test(ref)) return false;
|
|
3204
|
-
//
|
|
3688
|
+
// Check for @scope/name format (private registry)
|
|
3205
3689
|
if (ref.startsWith('@') && ref.includes('/')) {
|
|
3206
|
-
// @scope/name
|
|
3690
|
+
// @scope/name or @scope/name@version
|
|
3207
3691
|
const scopeNamePattern = /^@[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
|
|
3208
3692
|
return scopeNamePattern.test(ref);
|
|
3209
3693
|
}
|
|
3210
|
-
//
|
|
3211
|
-
//
|
|
3694
|
+
// Check for simple name or name@version format (public registry)
|
|
3695
|
+
// Simple names contain only letters, digits, hyphens, underscores, and dots
|
|
3212
3696
|
const namePattern = /^[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
|
|
3213
3697
|
return namePattern.test(ref);
|
|
3214
3698
|
}
|
|
@@ -3216,27 +3700,27 @@ class RegistryResolver {
|
|
|
3216
3700
|
* Resolve a registry skill reference
|
|
3217
3701
|
*
|
|
3218
3702
|
* @param ref - Skill reference (e.g., "@kanyun/planning-with-files@2.4.5" or "my-skill@latest")
|
|
3703
|
+
* @param overrideRegistryUrl - Optional registry URL override (bypasses scope-based lookup)
|
|
3219
3704
|
* @returns Resolved skill information including downloaded tarball
|
|
3220
3705
|
*
|
|
3221
3706
|
* @example
|
|
3222
3707
|
* const result = await resolver.resolve('@kanyun/planning-with-files@2.4.5');
|
|
3223
3708
|
* console.log(result.shortName); // 'planning-with-files'
|
|
3224
3709
|
* console.log(result.version); // '2.4.5'
|
|
3225
|
-
*/ async resolve(ref) {
|
|
3226
|
-
// 1.
|
|
3710
|
+
*/ async resolve(ref, overrideRegistryUrl) {
|
|
3711
|
+
// 1. Parse skill identifier
|
|
3227
3712
|
const parsed = parseSkillIdentifier(ref);
|
|
3228
3713
|
const shortName = getShortName(parsed.fullName);
|
|
3229
|
-
// 2.
|
|
3230
|
-
const registryUrl = getRegistryUrl(parsed.scope);
|
|
3231
|
-
// 3.
|
|
3714
|
+
// 2. Get registry URL (CLI override takes precedence)
|
|
3715
|
+
const registryUrl = overrideRegistryUrl || getRegistryUrl(parsed.scope);
|
|
3716
|
+
// 3. Create client and resolve version
|
|
3232
3717
|
const client = new RegistryClient({
|
|
3233
|
-
registry: registryUrl
|
|
3234
|
-
apiPrefix: getApiPrefix(registryUrl)
|
|
3718
|
+
registry: registryUrl
|
|
3235
3719
|
});
|
|
3236
3720
|
const version = await client.resolveVersion(parsed.fullName, parsed.version);
|
|
3237
|
-
// 4.
|
|
3721
|
+
// 4. Download tarball
|
|
3238
3722
|
const { tarball, integrity } = await client.downloadSkill(parsed.fullName, version);
|
|
3239
|
-
// 5.
|
|
3723
|
+
// 5. Verify integrity
|
|
3240
3724
|
const isValid = RegistryClient.verifyIntegrity(tarball, integrity);
|
|
3241
3725
|
if (!isValid) throw new Error(`Integrity verification failed for ${ref}`);
|
|
3242
3726
|
return {
|
|
@@ -3256,215 +3740,12 @@ class RegistryResolver {
|
|
|
3256
3740
|
* @returns Path to the extracted skill directory
|
|
3257
3741
|
*/ async extract(tarball, destDir) {
|
|
3258
3742
|
await extractTarballBuffer(tarball, destDir);
|
|
3259
|
-
//
|
|
3743
|
+
// Get top-level directory name (i.e. skill name)
|
|
3260
3744
|
const topDir = await getTarballTopDir(tarball);
|
|
3261
3745
|
if (topDir) return `${destDir}/${topDir}`;
|
|
3262
3746
|
return destDir;
|
|
3263
3747
|
}
|
|
3264
3748
|
}
|
|
3265
|
-
/**
|
|
3266
|
-
* Skill Parser - SKILL.md parser
|
|
3267
|
-
*
|
|
3268
|
-
* Following agentskills.io specification: https://agentskills.io/specification
|
|
3269
|
-
*
|
|
3270
|
-
* SKILL.md format requirements:
|
|
3271
|
-
* - YAML frontmatter containing name and description (required)
|
|
3272
|
-
* - name: max 64 characters, lowercase letters, numbers, hyphens
|
|
3273
|
-
* - description: max 1024 characters
|
|
3274
|
-
* - Optional fields: license, compatibility, metadata, allowed-tools
|
|
3275
|
-
*/ /**
|
|
3276
|
-
* Skill validation error
|
|
3277
|
-
*/ class SkillValidationError extends Error {
|
|
3278
|
-
field;
|
|
3279
|
-
constructor(message, field){
|
|
3280
|
-
super(message), this.field = field;
|
|
3281
|
-
this.name = 'SkillValidationError';
|
|
3282
|
-
}
|
|
3283
|
-
}
|
|
3284
|
-
/**
|
|
3285
|
-
* Simple YAML frontmatter parser
|
|
3286
|
-
* Parses --- delimited YAML header
|
|
3287
|
-
*
|
|
3288
|
-
* Supports:
|
|
3289
|
-
* - Basic key: value pairs
|
|
3290
|
-
* - Multiline strings (| and >)
|
|
3291
|
-
* - Nested objects (one level deep, for metadata field)
|
|
3292
|
-
*/ function parseFrontmatter(content) {
|
|
3293
|
-
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
3294
|
-
const match = content.match(frontmatterRegex);
|
|
3295
|
-
if (!match) return {
|
|
3296
|
-
data: {},
|
|
3297
|
-
content
|
|
3298
|
-
};
|
|
3299
|
-
const yamlContent = match[1];
|
|
3300
|
-
const markdownContent = match[2];
|
|
3301
|
-
// Simple YAML parsing (supports basic key: value format and one level of nesting)
|
|
3302
|
-
const data = {};
|
|
3303
|
-
const lines = yamlContent.split('\n');
|
|
3304
|
-
let currentKey = '';
|
|
3305
|
-
let currentValue = '';
|
|
3306
|
-
let inMultiline = false;
|
|
3307
|
-
let inNestedObject = false;
|
|
3308
|
-
let nestedObject = {};
|
|
3309
|
-
for (const line of lines){
|
|
3310
|
-
const trimmedLine = line.trim();
|
|
3311
|
-
if (!trimmedLine || trimmedLine.startsWith('#')) continue;
|
|
3312
|
-
// Check if it's a nested key: value pair (indented with 2 spaces)
|
|
3313
|
-
const nestedMatch = line.match(/^ {2}([a-zA-Z_-]+):\s*(.*)$/);
|
|
3314
|
-
if (nestedMatch && inNestedObject) {
|
|
3315
|
-
const [, nestedKey, nestedValue] = nestedMatch;
|
|
3316
|
-
nestedObject[nestedKey] = parseYamlValue(nestedValue.trim());
|
|
3317
|
-
continue;
|
|
3318
|
-
}
|
|
3319
|
-
// Check if it's a new key: value pair (no indent)
|
|
3320
|
-
const keyValueMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
|
|
3321
|
-
if (keyValueMatch && !inMultiline) {
|
|
3322
|
-
// Save previous nested object if any
|
|
3323
|
-
if (inNestedObject && currentKey) {
|
|
3324
|
-
data[currentKey] = nestedObject;
|
|
3325
|
-
nestedObject = {};
|
|
3326
|
-
inNestedObject = false;
|
|
3327
|
-
}
|
|
3328
|
-
// Save previous value
|
|
3329
|
-
if (currentKey && !inNestedObject) data[currentKey] = parseYamlValue(currentValue.trim());
|
|
3330
|
-
currentKey = keyValueMatch[1];
|
|
3331
|
-
currentValue = keyValueMatch[2];
|
|
3332
|
-
// Check if it's start of multiline string
|
|
3333
|
-
if ('|' === currentValue || '>' === currentValue) {
|
|
3334
|
-
inMultiline = true;
|
|
3335
|
-
currentValue = '';
|
|
3336
|
-
} else if ('' === currentValue) {
|
|
3337
|
-
// Empty value - might be start of nested object
|
|
3338
|
-
inNestedObject = true;
|
|
3339
|
-
nestedObject = {};
|
|
3340
|
-
}
|
|
3341
|
-
} else if (inMultiline && line.startsWith(' ')) // Multiline string continuation
|
|
3342
|
-
currentValue += (currentValue ? '\n' : '') + line.slice(2);
|
|
3343
|
-
else if (inMultiline && !line.startsWith(' ')) {
|
|
3344
|
-
// Multiline string end
|
|
3345
|
-
inMultiline = false;
|
|
3346
|
-
data[currentKey] = currentValue.trim();
|
|
3347
|
-
// Try to parse new line
|
|
3348
|
-
const newKeyMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
|
|
3349
|
-
if (newKeyMatch) {
|
|
3350
|
-
currentKey = newKeyMatch[1];
|
|
3351
|
-
currentValue = newKeyMatch[2];
|
|
3352
|
-
}
|
|
3353
|
-
}
|
|
3354
|
-
}
|
|
3355
|
-
// Save last value
|
|
3356
|
-
if (inNestedObject && currentKey) data[currentKey] = nestedObject;
|
|
3357
|
-
else if (currentKey) data[currentKey] = parseYamlValue(currentValue.trim());
|
|
3358
|
-
return {
|
|
3359
|
-
data,
|
|
3360
|
-
content: markdownContent
|
|
3361
|
-
};
|
|
3362
|
-
}
|
|
3363
|
-
/**
|
|
3364
|
-
* Parse YAML value
|
|
3365
|
-
*/ function parseYamlValue(value) {
|
|
3366
|
-
if (!value) return '';
|
|
3367
|
-
// Boolean value
|
|
3368
|
-
if ('true' === value) return true;
|
|
3369
|
-
if ('false' === value) return false;
|
|
3370
|
-
// Number
|
|
3371
|
-
if (/^-?\d+$/.test(value)) return parseInt(value, 10);
|
|
3372
|
-
if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
|
|
3373
|
-
// Remove quotes
|
|
3374
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
3375
|
-
return value;
|
|
3376
|
-
}
|
|
3377
|
-
/**
|
|
3378
|
-
* Validate skill name format
|
|
3379
|
-
*
|
|
3380
|
-
* Specification requirements:
|
|
3381
|
-
* - Max 64 characters
|
|
3382
|
-
* - Only lowercase letters, numbers, hyphens allowed
|
|
3383
|
-
* - Cannot start or end with hyphen
|
|
3384
|
-
* - Cannot contain consecutive hyphens
|
|
3385
|
-
*/ function validateSkillName(name) {
|
|
3386
|
-
if (!name) throw new SkillValidationError('Skill name is required', 'name');
|
|
3387
|
-
if (name.length > 64) throw new SkillValidationError('Skill name must be at most 64 characters', 'name');
|
|
3388
|
-
if (!/^[a-z0-9]/.test(name)) throw new SkillValidationError('Skill name must start with a lowercase letter or number', 'name');
|
|
3389
|
-
if (!/[a-z0-9]$/.test(name)) throw new SkillValidationError('Skill name must end with a lowercase letter or number', 'name');
|
|
3390
|
-
if (/--/.test(name)) throw new SkillValidationError('Skill name cannot contain consecutive hyphens', 'name');
|
|
3391
|
-
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');
|
|
3392
|
-
// Single character name
|
|
3393
|
-
if (1 === name.length && !/^[a-z0-9]$/.test(name)) throw new SkillValidationError('Single character skill name must be a lowercase letter or number', 'name');
|
|
3394
|
-
}
|
|
3395
|
-
/**
|
|
3396
|
-
* Validate skill description
|
|
3397
|
-
*
|
|
3398
|
-
* Specification requirements:
|
|
3399
|
-
* - Max 1024 characters
|
|
3400
|
-
* - Angle brackets are allowed per agentskills.io spec
|
|
3401
|
-
*/ function validateSkillDescription(description) {
|
|
3402
|
-
if (!description) throw new SkillValidationError('Skill description is required', 'description');
|
|
3403
|
-
if (description.length > 1024) throw new SkillValidationError('Skill description must be at most 1024 characters', 'description');
|
|
3404
|
-
// Note: angle brackets are allowed per agentskills.io spec
|
|
3405
|
-
}
|
|
3406
|
-
/**
|
|
3407
|
-
* Parse SKILL.md content
|
|
3408
|
-
*
|
|
3409
|
-
* @param content - SKILL.md file content
|
|
3410
|
-
* @param options - Parse options
|
|
3411
|
-
* @returns Parsed skill info, or null if format is invalid
|
|
3412
|
-
* @throws SkillValidationError if validation fails in strict mode
|
|
3413
|
-
*/ function parseSkillMd(content, options = {}) {
|
|
3414
|
-
const { strict = false } = options;
|
|
3415
|
-
try {
|
|
3416
|
-
const { data, content: body } = parseFrontmatter(content);
|
|
3417
|
-
// Check required fields
|
|
3418
|
-
if (!data.name || !data.description) {
|
|
3419
|
-
if (strict) throw new SkillValidationError('SKILL.md must have name and description in frontmatter');
|
|
3420
|
-
return null;
|
|
3421
|
-
}
|
|
3422
|
-
const name = String(data.name);
|
|
3423
|
-
const description = String(data.description);
|
|
3424
|
-
// Validate field format
|
|
3425
|
-
if (strict) {
|
|
3426
|
-
validateSkillName(name);
|
|
3427
|
-
validateSkillDescription(description);
|
|
3428
|
-
}
|
|
3429
|
-
// Parse allowed-tools
|
|
3430
|
-
let allowedTools;
|
|
3431
|
-
if (data['allowed-tools']) {
|
|
3432
|
-
const toolsStr = String(data['allowed-tools']);
|
|
3433
|
-
allowedTools = toolsStr.split(/\s+/).filter(Boolean);
|
|
3434
|
-
}
|
|
3435
|
-
return {
|
|
3436
|
-
name,
|
|
3437
|
-
description,
|
|
3438
|
-
version: data.version ? String(data.version) : void 0,
|
|
3439
|
-
license: data.license ? String(data.license) : void 0,
|
|
3440
|
-
compatibility: data.compatibility ? String(data.compatibility) : void 0,
|
|
3441
|
-
metadata: data.metadata,
|
|
3442
|
-
allowedTools,
|
|
3443
|
-
content: body,
|
|
3444
|
-
rawContent: content
|
|
3445
|
-
};
|
|
3446
|
-
} catch (error) {
|
|
3447
|
-
if (error instanceof SkillValidationError) throw error;
|
|
3448
|
-
if (strict) throw new SkillValidationError(`Failed to parse SKILL.md: ${error}`);
|
|
3449
|
-
return null;
|
|
3450
|
-
}
|
|
3451
|
-
}
|
|
3452
|
-
/**
|
|
3453
|
-
* Parse SKILL.md from file path
|
|
3454
|
-
*/ function parseSkillMdFile(filePath, options = {}) {
|
|
3455
|
-
if (!external_node_fs_.existsSync(filePath)) {
|
|
3456
|
-
if (options.strict) throw new SkillValidationError(`SKILL.md not found: ${filePath}`);
|
|
3457
|
-
return null;
|
|
3458
|
-
}
|
|
3459
|
-
const content = external_node_fs_.readFileSync(filePath, 'utf-8');
|
|
3460
|
-
return parseSkillMd(content, options);
|
|
3461
|
-
}
|
|
3462
|
-
/**
|
|
3463
|
-
* Parse SKILL.md from skill directory
|
|
3464
|
-
*/ function parseSkillFromDir(dirPath, options = {}) {
|
|
3465
|
-
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
|
|
3466
|
-
return parseSkillMdFile(skillMdPath, options);
|
|
3467
|
-
}
|
|
3468
3749
|
/**
|
|
3469
3750
|
* SkillManager - Core Skill management class
|
|
3470
3751
|
*
|
|
@@ -3573,6 +3854,18 @@ class RegistryResolver {
|
|
|
3573
3854
|
return null;
|
|
3574
3855
|
}
|
|
3575
3856
|
/**
|
|
3857
|
+
* Resolve the actual source path for installation.
|
|
3858
|
+
* For multi-skill repos (parsed.skillName is set), discovers skills in the
|
|
3859
|
+
* cached directory and returns the matching skill's subdirectory.
|
|
3860
|
+
* For single-skill repos, returns basePath as-is.
|
|
3861
|
+
*/ resolveSourcePath(basePath, parsed) {
|
|
3862
|
+
if (!parsed.skillName) return basePath;
|
|
3863
|
+
const discovered = discoverSkillsInDir(basePath);
|
|
3864
|
+
const match = discovered.find((s)=>s.name === parsed.skillName);
|
|
3865
|
+
if (!match) throw new Error(`Skill "${parsed.skillName}" not found in repository. Available: ${discovered.map((s)=>s.name).join(', ')}`);
|
|
3866
|
+
return match.dirPath;
|
|
3867
|
+
}
|
|
3868
|
+
/**
|
|
3576
3869
|
* Install skill
|
|
3577
3870
|
*/ async install(ref, options = {}) {
|
|
3578
3871
|
// Detect source type and delegate to appropriate installer
|
|
@@ -3595,9 +3888,10 @@ class RegistryResolver {
|
|
|
3595
3888
|
logger_logger.debug(`Caching from ${repoUrl}@${gitRef}`);
|
|
3596
3889
|
cacheResult = await this.cache.cache(repoUrl, parsed, gitRef, gitRef);
|
|
3597
3890
|
}
|
|
3598
|
-
//
|
|
3891
|
+
// Resolve source path (cache root or skill subdirectory for multi-skill repos)
|
|
3599
3892
|
const cachePath = this.cache.getCachePath(parsed, gitRef);
|
|
3600
|
-
const
|
|
3893
|
+
const sourcePath = this.resolveSourcePath(cachePath, parsed);
|
|
3894
|
+
const metadata = this.getSkillMetadataFromDir(sourcePath);
|
|
3601
3895
|
const skillName = metadata?.name ?? fallbackName;
|
|
3602
3896
|
const semanticVersion = metadata?.version ?? gitRef;
|
|
3603
3897
|
const skillPath = this.getSkillPath(skillName);
|
|
@@ -3621,7 +3915,9 @@ class RegistryResolver {
|
|
|
3621
3915
|
// Copy to installation directory
|
|
3622
3916
|
ensureDir(this.getInstallDir());
|
|
3623
3917
|
if (exists(skillPath)) remove(skillPath);
|
|
3624
|
-
|
|
3918
|
+
copyDir(sourcePath, skillPath, {
|
|
3919
|
+
exclude: DEFAULT_EXCLUDE_FILES
|
|
3920
|
+
});
|
|
3625
3921
|
// Update lock file (project mode only)
|
|
3626
3922
|
if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
|
|
3627
3923
|
source: `${parsed.registry}:${parsed.owner}/${parsed.repo}${parsed.subPath ? `/${parsed.subPath}` : ''}`,
|
|
@@ -3946,12 +4242,112 @@ class RegistryResolver {
|
|
|
3946
4242
|
* @param options - Installation options
|
|
3947
4243
|
*/ async installToAgents(ref, targetAgents, options = {}) {
|
|
3948
4244
|
// Detect source type and delegate to appropriate installer
|
|
3949
|
-
// Priority: Registry > HTTP > Git (registry
|
|
4245
|
+
// Priority: Registry > HTTP > Git (registry first, as its format is most constrained)
|
|
3950
4246
|
if (this.isRegistrySource(ref)) return this.installToAgentsFromRegistry(ref, targetAgents, options);
|
|
3951
4247
|
if (this.isHttpSource(ref)) return this.installToAgentsFromHttp(ref, targetAgents, options);
|
|
3952
4248
|
return this.installToAgentsFromGit(ref, targetAgents, options);
|
|
3953
4249
|
}
|
|
3954
4250
|
/**
|
|
4251
|
+
* Multi-skill install: discover skills in a Git repo and install selected ones (or list only).
|
|
4252
|
+
* Only Git references are supported (including https://github.com/...); registry refs are not.
|
|
4253
|
+
*
|
|
4254
|
+
* @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
|
|
4255
|
+
* @param skillNames - If non-empty, install only these skills (by SKILL.md name). If empty and !listOnly, install all.
|
|
4256
|
+
* @param targetAgents - Target agents
|
|
4257
|
+
* @param options - Install options; listOnly: true means discover and return skills without installing
|
|
4258
|
+
*/ async installSkillsFromRepo(ref, skillNames, targetAgents, options = {}) {
|
|
4259
|
+
const { listOnly = false, force = false, save = true, mode = 'symlink' } = options;
|
|
4260
|
+
const refForResolve = ref.replace(/#.*$/, '').trim();
|
|
4261
|
+
const resolved = await this.resolver.resolve(refForResolve);
|
|
4262
|
+
const { parsed, repoUrl } = resolved;
|
|
4263
|
+
const gitRef = resolved.ref;
|
|
4264
|
+
let cacheResult = await this.cache.get(parsed, gitRef);
|
|
4265
|
+
if (!cacheResult) {
|
|
4266
|
+
logger_logger.debug(`Caching from ${repoUrl}@${gitRef}`);
|
|
4267
|
+
cacheResult = await this.cache.cache(repoUrl, parsed, gitRef, gitRef);
|
|
4268
|
+
}
|
|
4269
|
+
const cachePath = this.cache.getCachePath(parsed, gitRef);
|
|
4270
|
+
const discovered = discoverSkillsInDir(cachePath);
|
|
4271
|
+
if (0 === discovered.length) throw new Error('No valid skills found. Skills require a SKILL.md with name and description.');
|
|
4272
|
+
if (listOnly) return {
|
|
4273
|
+
listOnly: true,
|
|
4274
|
+
skills: discovered
|
|
4275
|
+
};
|
|
4276
|
+
const selected = skillNames.length > 0 ? filterSkillsByName(discovered, skillNames) : discovered;
|
|
4277
|
+
if (skillNames.length > 0 && 0 === selected.length) {
|
|
4278
|
+
const available = discovered.map((s)=>s.name).join(', ');
|
|
4279
|
+
throw new Error(`No matching skills found for: ${skillNames.join(', ')}. Available skills: ${available}`);
|
|
4280
|
+
}
|
|
4281
|
+
const baseRefForSave = this.config.normalizeSkillRef(refForResolve);
|
|
4282
|
+
const defaults = this.config.getDefaults();
|
|
4283
|
+
// Only pass custom installDir to Installer; default '.skills' should use
|
|
4284
|
+
// the Installer's built-in canonical path (.agents/skills/)
|
|
4285
|
+
const customInstallDir = '.skills' !== defaults.installDir ? defaults.installDir : void 0;
|
|
4286
|
+
const installer = new Installer({
|
|
4287
|
+
cwd: this.projectRoot,
|
|
4288
|
+
global: this.isGlobal,
|
|
4289
|
+
installDir: customInstallDir
|
|
4290
|
+
});
|
|
4291
|
+
const installed = [];
|
|
4292
|
+
const skipped = [];
|
|
4293
|
+
const skillSource = `${parsed.registry}:${parsed.owner}/${parsed.repo}${parsed.subPath ? `/${parsed.subPath}` : ''}`;
|
|
4294
|
+
for (const skillInfo of selected){
|
|
4295
|
+
const semanticVersion = skillInfo.version ?? gitRef;
|
|
4296
|
+
// Skip already-installed skills unless --force is set
|
|
4297
|
+
if (!force) {
|
|
4298
|
+
const existingSkill = this.getInstalledSkill(skillInfo.name);
|
|
4299
|
+
if (existingSkill) {
|
|
4300
|
+
const locked = this.lockManager.get(skillInfo.name);
|
|
4301
|
+
const lockedRef = locked?.ref || locked?.version;
|
|
4302
|
+
if (lockedRef === gitRef) {
|
|
4303
|
+
const reason = `already installed at ${gitRef}`;
|
|
4304
|
+
logger_logger.info(`${skillInfo.name}@${gitRef} is already installed, skipping`);
|
|
4305
|
+
skipped.push({
|
|
4306
|
+
name: skillInfo.name,
|
|
4307
|
+
reason
|
|
4308
|
+
});
|
|
4309
|
+
continue;
|
|
4310
|
+
}
|
|
4311
|
+
// Different version installed — allow upgrade without --force
|
|
4312
|
+
// Only skip when the exact same ref is already locked
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
logger_logger["package"](`Installing ${skillInfo.name}@${gitRef} to ${targetAgents.length} agent(s)...`);
|
|
4316
|
+
// Note: force is handled at the SkillManager level (skip-if-installed check above).
|
|
4317
|
+
// The Installer always overwrites (remove + copy), so no force flag is needed there.
|
|
4318
|
+
const results = await installer.installToAgents(skillInfo.dirPath, skillInfo.name, targetAgents, {
|
|
4319
|
+
mode: mode
|
|
4320
|
+
});
|
|
4321
|
+
if (!this.isGlobal) this.lockManager.lockSkill(skillInfo.name, {
|
|
4322
|
+
source: skillSource,
|
|
4323
|
+
version: semanticVersion,
|
|
4324
|
+
ref: gitRef,
|
|
4325
|
+
resolved: repoUrl,
|
|
4326
|
+
commit: cacheResult.commit
|
|
4327
|
+
});
|
|
4328
|
+
if (!this.isGlobal && save) {
|
|
4329
|
+
this.config.ensureExists();
|
|
4330
|
+
this.config.addSkill(skillInfo.name, `${baseRefForSave}#${skillInfo.name}`);
|
|
4331
|
+
}
|
|
4332
|
+
const successCount = Array.from(results.values()).filter((r)=>r.success).length;
|
|
4333
|
+
logger_logger.success(`Installed ${skillInfo.name}@${semanticVersion} to ${successCount} agent(s)`);
|
|
4334
|
+
installed.push({
|
|
4335
|
+
skill: {
|
|
4336
|
+
name: skillInfo.name,
|
|
4337
|
+
path: skillInfo.dirPath,
|
|
4338
|
+
version: semanticVersion,
|
|
4339
|
+
source: skillSource
|
|
4340
|
+
},
|
|
4341
|
+
results
|
|
4342
|
+
});
|
|
4343
|
+
}
|
|
4344
|
+
return {
|
|
4345
|
+
listOnly: false,
|
|
4346
|
+
installed,
|
|
4347
|
+
skipped
|
|
4348
|
+
};
|
|
4349
|
+
}
|
|
4350
|
+
/**
|
|
3955
4351
|
* Install skill from Git to multiple agents
|
|
3956
4352
|
*/ async installToAgentsFromGit(ref, targetAgents, options = {}) {
|
|
3957
4353
|
const { save = true, mode = 'symlink' } = options;
|
|
@@ -3967,8 +4363,9 @@ class RegistryResolver {
|
|
|
3967
4363
|
logger_logger.debug(`Caching from ${repoUrl}@${gitRef}`);
|
|
3968
4364
|
cacheResult = await this.cache.cache(repoUrl, parsed, gitRef, gitRef);
|
|
3969
4365
|
}
|
|
3970
|
-
//
|
|
3971
|
-
const
|
|
4366
|
+
// Resolve source path (cache root or skill subdirectory for multi-skill repos)
|
|
4367
|
+
const cachePath = this.cache.getCachePath(parsed, gitRef);
|
|
4368
|
+
const sourcePath = this.resolveSourcePath(cachePath, parsed);
|
|
3972
4369
|
// Get the real skill name from SKILL.md in cache
|
|
3973
4370
|
const metadata = this.getSkillMetadataFromDir(sourcePath);
|
|
3974
4371
|
const skillName = metadata?.name ?? fallbackName;
|
|
@@ -4092,14 +4489,13 @@ class RegistryResolver {
|
|
|
4092
4489
|
* - Web-published skills (github/gitlab/oss_url/custom_url/local)
|
|
4093
4490
|
*/ async installToAgentsFromRegistry(ref, targetAgents, options = {}) {
|
|
4094
4491
|
const { force = false, save = true, mode = 'symlink' } = options;
|
|
4095
|
-
//
|
|
4492
|
+
// Parse skill identifier and resolve registry URL once (single source of truth)
|
|
4096
4493
|
const parsed = parseSkillIdentifier(ref);
|
|
4097
|
-
const registryUrl = getRegistryUrl(parsed.scope);
|
|
4494
|
+
const registryUrl = options.registry || getRegistryUrl(parsed.scope);
|
|
4098
4495
|
const client = new RegistryClient({
|
|
4099
|
-
registry: registryUrl
|
|
4100
|
-
apiPrefix: getApiPrefix(registryUrl)
|
|
4496
|
+
registry: registryUrl
|
|
4101
4497
|
});
|
|
4102
|
-
//
|
|
4498
|
+
// Query skill info to determine source_type
|
|
4103
4499
|
let skillInfo;
|
|
4104
4500
|
try {
|
|
4105
4501
|
skillInfo = await client.getSkillInfo(parsed.fullName);
|
|
@@ -4110,12 +4506,15 @@ class RegistryResolver {
|
|
|
4110
4506
|
};
|
|
4111
4507
|
else throw error;
|
|
4112
4508
|
}
|
|
4113
|
-
//
|
|
4509
|
+
// Branch based on source_type (pass resolved registryUrl via options to avoid re-computation)
|
|
4114
4510
|
const sourceType = skillInfo.source_type;
|
|
4115
|
-
if (sourceType && 'registry' !== sourceType) return this.installFromWebPublished(skillInfo, parsed, targetAgents,
|
|
4116
|
-
|
|
4511
|
+
if (sourceType && 'registry' !== sourceType) return this.installFromWebPublished(skillInfo, parsed, targetAgents, {
|
|
4512
|
+
...options,
|
|
4513
|
+
registry: registryUrl
|
|
4514
|
+
});
|
|
4515
|
+
// 1. Resolve registry skill (pass pre-resolved registryUrl)
|
|
4117
4516
|
logger_logger["package"](`Resolving ${ref} from registry...`);
|
|
4118
|
-
const resolved = await this.registryResolver.resolve(ref);
|
|
4517
|
+
const resolved = await this.registryResolver.resolve(ref, registryUrl);
|
|
4119
4518
|
const { shortName, version, registryUrl: resolvedRegistryUrl, tarball, parsed: resolvedParsed } = resolved;
|
|
4120
4519
|
// 2. Check if already installed (skip if --force)
|
|
4121
4520
|
const skillPath = this.getSkillPath(shortName);
|
|
@@ -4154,101 +4553,157 @@ class RegistryResolver {
|
|
|
4154
4553
|
};
|
|
4155
4554
|
}
|
|
4156
4555
|
logger_logger["package"](`Installing ${shortName}@${version} from ${resolvedRegistryUrl} to ${targetAgents.length} agent(s)...`);
|
|
4157
|
-
// 3. Create temp directory for extraction
|
|
4556
|
+
// 3. Create temp directory for extraction (clean stale files first)
|
|
4158
4557
|
const tempDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cache.getCacheDir(), 'registry-temp', `${shortName}-${version}`);
|
|
4558
|
+
await remove(tempDir);
|
|
4159
4559
|
await ensureDir(tempDir);
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
this.
|
|
4185
|
-
|
|
4186
|
-
|
|
4560
|
+
try {
|
|
4561
|
+
// 4. Extract tarball
|
|
4562
|
+
const extractedPath = await this.registryResolver.extract(tarball, tempDir);
|
|
4563
|
+
logger_logger.debug(`Extracted to ${extractedPath}`);
|
|
4564
|
+
// 5. Create Installer with custom installDir from config
|
|
4565
|
+
const defaults = this.config.getDefaults();
|
|
4566
|
+
const installer = new Installer({
|
|
4567
|
+
cwd: this.projectRoot,
|
|
4568
|
+
global: this.isGlobal,
|
|
4569
|
+
installDir: defaults.installDir
|
|
4570
|
+
});
|
|
4571
|
+
// 6. Install to all target agents
|
|
4572
|
+
const results = await installer.installToAgents(extractedPath, shortName, targetAgents, {
|
|
4573
|
+
mode: mode
|
|
4574
|
+
});
|
|
4575
|
+
// 7. Update lock file (project mode only)
|
|
4576
|
+
if (!this.isGlobal) this.lockManager.lockSkill(shortName, {
|
|
4577
|
+
source: `registry:${resolvedParsed.fullName}`,
|
|
4578
|
+
version,
|
|
4579
|
+
ref: version,
|
|
4580
|
+
resolved: resolvedRegistryUrl,
|
|
4581
|
+
commit: resolved.integrity
|
|
4582
|
+
});
|
|
4583
|
+
// 8. Update skills.json (project mode only)
|
|
4584
|
+
if (!this.isGlobal && save) {
|
|
4585
|
+
this.config.ensureExists();
|
|
4586
|
+
// Save with full name for registry skills
|
|
4587
|
+
this.config.addSkill(shortName, ref);
|
|
4588
|
+
}
|
|
4589
|
+
// 9. Count results and log
|
|
4590
|
+
const successCount = Array.from(results.values()).filter((r)=>r.success).length;
|
|
4591
|
+
const failCount = results.size - successCount;
|
|
4592
|
+
if (0 === failCount) logger_logger.success(`Installed ${shortName}@${version} to ${successCount} agent(s)`);
|
|
4593
|
+
else logger_logger.warn(`Installed ${shortName}@${version} to ${successCount} agent(s), ${failCount} failed`);
|
|
4594
|
+
// 10. Build the InstalledSkill to return
|
|
4595
|
+
const skill = {
|
|
4596
|
+
name: shortName,
|
|
4597
|
+
path: extractedPath,
|
|
4598
|
+
version,
|
|
4599
|
+
source: `registry:${resolvedParsed.fullName}`
|
|
4600
|
+
};
|
|
4601
|
+
return {
|
|
4602
|
+
skill,
|
|
4603
|
+
results
|
|
4604
|
+
};
|
|
4605
|
+
} finally{
|
|
4606
|
+
// Clean up temp directory after installation
|
|
4607
|
+
await remove(tempDir);
|
|
4187
4608
|
}
|
|
4188
|
-
// 9. Count results and log
|
|
4189
|
-
const successCount = Array.from(results.values()).filter((r)=>r.success).length;
|
|
4190
|
-
const failCount = results.size - successCount;
|
|
4191
|
-
if (0 === failCount) logger_logger.success(`Installed ${shortName}@${version} to ${successCount} agent(s)`);
|
|
4192
|
-
else logger_logger.warn(`Installed ${shortName}@${version} to ${successCount} agent(s), ${failCount} failed`);
|
|
4193
|
-
// 9. Build the InstalledSkill to return
|
|
4194
|
-
const skill = {
|
|
4195
|
-
name: shortName,
|
|
4196
|
-
path: extractedPath,
|
|
4197
|
-
version,
|
|
4198
|
-
source: `registry:${resolvedParsed.fullName}`
|
|
4199
|
-
};
|
|
4200
|
-
return {
|
|
4201
|
-
skill,
|
|
4202
|
-
results
|
|
4203
|
-
};
|
|
4204
4609
|
}
|
|
4205
4610
|
// ============================================================================
|
|
4206
|
-
// Web-published skill installation
|
|
4611
|
+
// Web-published skill installation
|
|
4207
4612
|
// ============================================================================
|
|
4208
4613
|
/**
|
|
4209
|
-
*
|
|
4614
|
+
* Install a web-published skill.
|
|
4210
4615
|
*
|
|
4211
|
-
*
|
|
4212
|
-
*
|
|
4213
|
-
* -
|
|
4214
|
-
* -
|
|
4616
|
+
* Web-published skills do not support versioning. Branches to different
|
|
4617
|
+
* installation logic based on source_type:
|
|
4618
|
+
* - github/gitlab: reuses installToAgentsFromGit
|
|
4619
|
+
* - oss_url/custom_url: reuses installToAgentsFromHttp
|
|
4620
|
+
* - local: downloads tarball via Registry API
|
|
4215
4621
|
*/ async installFromWebPublished(skillInfo, parsed, targetAgents, options = {}) {
|
|
4216
4622
|
const { source_type, source_url } = skillInfo;
|
|
4217
|
-
//
|
|
4623
|
+
// Web-published skills do not support version specifiers
|
|
4218
4624
|
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}`);
|
|
4219
4625
|
if (!source_url) throw new Error(`Missing source_url for web-published skill: ${parsed.fullName}`);
|
|
4220
4626
|
logger_logger["package"](`Installing ${parsed.fullName} from ${source_type} source...`);
|
|
4221
4627
|
switch(source_type){
|
|
4222
4628
|
case 'github':
|
|
4223
4629
|
case 'gitlab':
|
|
4224
|
-
// source_url
|
|
4225
|
-
//
|
|
4630
|
+
// source_url is a full Git URL (includes ref and path)
|
|
4631
|
+
// Reuse existing Git installation logic
|
|
4226
4632
|
return this.installToAgentsFromGit(source_url, targetAgents, options);
|
|
4227
4633
|
case 'oss_url':
|
|
4228
4634
|
case 'custom_url':
|
|
4229
|
-
//
|
|
4635
|
+
// Direct download URL
|
|
4230
4636
|
return this.installToAgentsFromHttp(source_url, targetAgents, options);
|
|
4231
4637
|
case 'local':
|
|
4232
|
-
//
|
|
4233
|
-
return this.installFromRegistryLocal(
|
|
4638
|
+
// Download tarball via Registry API
|
|
4639
|
+
return this.installFromRegistryLocal(parsed, targetAgents, options);
|
|
4234
4640
|
default:
|
|
4235
4641
|
throw new Error(`Unknown source_type: ${source_type}`);
|
|
4236
4642
|
}
|
|
4237
4643
|
}
|
|
4238
4644
|
/**
|
|
4239
|
-
*
|
|
4645
|
+
* Install a skill published via "local folder" mode.
|
|
4240
4646
|
*
|
|
4241
|
-
*
|
|
4242
|
-
*
|
|
4243
|
-
*/ async installFromRegistryLocal(
|
|
4244
|
-
const
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
const
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4647
|
+
* Downloads tarball via RegistryClient (handles 302 redirects to signed OSS URLs),
|
|
4648
|
+
* then extracts and installs using the same flow as registry source_type.
|
|
4649
|
+
*/ async installFromRegistryLocal(parsed, targetAgents, options = {}) {
|
|
4650
|
+
const { save = true, mode = 'symlink' } = options;
|
|
4651
|
+
const registryUrl = options.registry || getRegistryUrl(parsed.scope);
|
|
4652
|
+
const shortName = getShortName(parsed.fullName);
|
|
4653
|
+
const version = 'latest';
|
|
4654
|
+
// Download tarball via RegistryClient (handles auth + 302 redirect to signed URL)
|
|
4655
|
+
const client = new RegistryClient({
|
|
4656
|
+
registry: registryUrl
|
|
4657
|
+
});
|
|
4658
|
+
const { tarball } = await client.downloadSkill(parsed.fullName, version);
|
|
4659
|
+
logger_logger["package"](`Installing ${shortName} from ${registryUrl} to ${targetAgents.length} agent(s)...`);
|
|
4660
|
+
// Extract tarball to temp directory (clean stale files first)
|
|
4661
|
+
const tempDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cache.getCacheDir(), 'registry-temp', `${shortName}-${version}`);
|
|
4662
|
+
await remove(tempDir);
|
|
4663
|
+
await ensureDir(tempDir);
|
|
4664
|
+
try {
|
|
4665
|
+
const extractedPath = await this.registryResolver.extract(tarball, tempDir);
|
|
4666
|
+
logger_logger.debug(`Extracted to ${extractedPath}`);
|
|
4667
|
+
// Install to all target agents
|
|
4668
|
+
const defaults = this.config.getDefaults();
|
|
4669
|
+
const installer = new Installer({
|
|
4670
|
+
cwd: this.projectRoot,
|
|
4671
|
+
global: this.isGlobal,
|
|
4672
|
+
installDir: defaults.installDir
|
|
4673
|
+
});
|
|
4674
|
+
const results = await installer.installToAgents(extractedPath, shortName, targetAgents, {
|
|
4675
|
+
mode: mode
|
|
4676
|
+
});
|
|
4677
|
+
// Get metadata from extracted path
|
|
4678
|
+
const metadata = this.getSkillMetadataFromDir(extractedPath);
|
|
4679
|
+
const skillName = metadata?.name ?? shortName;
|
|
4680
|
+
const semanticVersion = metadata?.version ?? version;
|
|
4681
|
+
// Update lock file (project mode only)
|
|
4682
|
+
if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
|
|
4683
|
+
source: `registry:${parsed.fullName}`,
|
|
4684
|
+
version: semanticVersion,
|
|
4685
|
+
ref: version,
|
|
4686
|
+
resolved: registryUrl,
|
|
4687
|
+
commit: ''
|
|
4688
|
+
});
|
|
4689
|
+
// Update skills.json (project mode only)
|
|
4690
|
+
if (!this.isGlobal && save) {
|
|
4691
|
+
this.config.ensureExists();
|
|
4692
|
+
this.config.addSkill(skillName, parsed.fullName);
|
|
4693
|
+
}
|
|
4694
|
+
return {
|
|
4695
|
+
skill: {
|
|
4696
|
+
name: skillName,
|
|
4697
|
+
path: extractedPath,
|
|
4698
|
+
version: semanticVersion,
|
|
4699
|
+
source: `registry:${parsed.fullName}`
|
|
4700
|
+
},
|
|
4701
|
+
results
|
|
4702
|
+
};
|
|
4703
|
+
} finally{
|
|
4704
|
+
// Clean up temp directory after installation
|
|
4705
|
+
await remove(tempDir);
|
|
4706
|
+
}
|
|
4252
4707
|
}
|
|
4253
4708
|
/**
|
|
4254
4709
|
* Get default target agents
|
|
@@ -5231,6 +5686,172 @@ class RegistryResolver {
|
|
|
5231
5686
|
if (warnings > 0) logger_logger.warn(`Found ${warnings} warning${1 !== warnings ? 's' : ''}, but reskill should work`);
|
|
5232
5687
|
else logger_logger.success('All checks passed! reskill is ready to use.');
|
|
5233
5688
|
});
|
|
5689
|
+
/**
|
|
5690
|
+
* Registry URL resolution utilities
|
|
5691
|
+
*
|
|
5692
|
+
* Shared utility for resolving registry URLs across CLI commands.
|
|
5693
|
+
*/ /**
|
|
5694
|
+
* Attempt to resolve registry URL from multiple sources.
|
|
5695
|
+
*
|
|
5696
|
+
* Priority (highest to lowest):
|
|
5697
|
+
* 1. --registry CLI option
|
|
5698
|
+
* 2. RESKILL_REGISTRY environment variable
|
|
5699
|
+
* 3. defaults.publishRegistry in skills.json
|
|
5700
|
+
*
|
|
5701
|
+
* Returns the resolved URL, or null if none found.
|
|
5702
|
+
*
|
|
5703
|
+
* @param cliRegistry - Registry URL from CLI option
|
|
5704
|
+
* @param projectRoot - Project root directory (defaults to cwd)
|
|
5705
|
+
* @returns Resolved registry URL, or null if not configured
|
|
5706
|
+
*/ function tryResolveRegistry(cliRegistry, projectRoot = process.cwd()) {
|
|
5707
|
+
// 1. CLI option (highest priority)
|
|
5708
|
+
if (cliRegistry) return cliRegistry;
|
|
5709
|
+
// 2. Environment variable
|
|
5710
|
+
const envRegistry = process.env.RESKILL_REGISTRY;
|
|
5711
|
+
if (envRegistry) return envRegistry;
|
|
5712
|
+
// 3. From skills.json
|
|
5713
|
+
try {
|
|
5714
|
+
const configLoader = new ConfigLoader(projectRoot);
|
|
5715
|
+
if (configLoader.exists()) {
|
|
5716
|
+
const publishRegistry = configLoader.getPublishRegistry();
|
|
5717
|
+
if (publishRegistry) return publishRegistry;
|
|
5718
|
+
}
|
|
5719
|
+
} catch {
|
|
5720
|
+
// Config loading failed, return null
|
|
5721
|
+
}
|
|
5722
|
+
return null;
|
|
5723
|
+
}
|
|
5724
|
+
/**
|
|
5725
|
+
* Resolve registry URL from multiple sources (strict — required for publish)
|
|
5726
|
+
*
|
|
5727
|
+
* Priority (highest to lowest):
|
|
5728
|
+
* 1. --registry CLI option
|
|
5729
|
+
* 2. RESKILL_REGISTRY environment variable
|
|
5730
|
+
* 3. defaults.publishRegistry in skills.json
|
|
5731
|
+
*
|
|
5732
|
+
* Intentionally has NO default - users must explicitly configure their registry.
|
|
5733
|
+
*
|
|
5734
|
+
* @param cliRegistry - Registry URL from CLI option
|
|
5735
|
+
* @param projectRoot - Project root directory (defaults to cwd)
|
|
5736
|
+
* @returns Resolved registry URL
|
|
5737
|
+
* @throws Exits process with code 1 if no registry is configured
|
|
5738
|
+
*/ function resolveRegistry(cliRegistry, projectRoot = process.cwd()) {
|
|
5739
|
+
const resolved = tryResolveRegistry(cliRegistry, projectRoot);
|
|
5740
|
+
if (resolved) return resolved;
|
|
5741
|
+
// No registry configured - error
|
|
5742
|
+
logger_logger.error('No registry specified');
|
|
5743
|
+
logger_logger.newline();
|
|
5744
|
+
logger_logger.log('Please specify a registry using one of these methods:');
|
|
5745
|
+
logger_logger.log(' • --registry <url> option');
|
|
5746
|
+
logger_logger.log(' • RESKILL_REGISTRY environment variable');
|
|
5747
|
+
logger_logger.log(' • "defaults.publishRegistry" in skills.json');
|
|
5748
|
+
process.exit(1);
|
|
5749
|
+
}
|
|
5750
|
+
/**
|
|
5751
|
+
* Resolve registry URL for search, with graceful fallback to public registry.
|
|
5752
|
+
*
|
|
5753
|
+
* Same priority as `resolveRegistry()`, but falls back to the public registry
|
|
5754
|
+
* instead of exiting when no registry is configured.
|
|
5755
|
+
*
|
|
5756
|
+
* @param cliRegistry - Registry URL from CLI option
|
|
5757
|
+
* @param projectRoot - Project root directory (defaults to cwd)
|
|
5758
|
+
* @returns Resolved registry URL (never null)
|
|
5759
|
+
*/ function resolveRegistryForSearch(cliRegistry, projectRoot = process.cwd()) {
|
|
5760
|
+
return tryResolveRegistry(cliRegistry, projectRoot) ?? PUBLIC_REGISTRY;
|
|
5761
|
+
}
|
|
5762
|
+
/**
|
|
5763
|
+
* find command - Search for skills in the registry
|
|
5764
|
+
*
|
|
5765
|
+
* Supports both public and private registries via --registry option.
|
|
5766
|
+
* Resolves registry from CLI option > RESKILL_REGISTRY env > skills.json config.
|
|
5767
|
+
*
|
|
5768
|
+
* Usage:
|
|
5769
|
+
* reskill find <query> # Search public registry
|
|
5770
|
+
* reskill find <query> --registry <url> # Search private registry
|
|
5771
|
+
* reskill find <query> --json # Output as JSON
|
|
5772
|
+
* reskill find <query> --limit 5 # Limit results
|
|
5773
|
+
*/ // ============================================================================
|
|
5774
|
+
// Display Helpers
|
|
5775
|
+
// ============================================================================
|
|
5776
|
+
/**
|
|
5777
|
+
* Format a single search result for terminal display
|
|
5778
|
+
*/ function formatResultItem(item, index) {
|
|
5779
|
+
const lines = [];
|
|
5780
|
+
const name = __WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold.cyan(item.name);
|
|
5781
|
+
const version = item.latest_version ? __WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(`@${item.latest_version}`) : '';
|
|
5782
|
+
lines.push(` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(`${index + 1}.`)} ${name}${version}`);
|
|
5783
|
+
if (item.description) {
|
|
5784
|
+
const desc = item.description.length > 80 ? `${item.description.slice(0, 80)}...` : item.description;
|
|
5785
|
+
lines.push(` ${desc}`);
|
|
5786
|
+
}
|
|
5787
|
+
const meta = [];
|
|
5788
|
+
if (item.keywords && item.keywords.length > 0) meta.push(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(`keywords: ${item.keywords.join(', ')}`));
|
|
5789
|
+
if (item.publisher?.handle) meta.push(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(`by ${item.publisher.handle}`));
|
|
5790
|
+
if (meta.length > 0) lines.push(` ${meta.join(' · ')}`);
|
|
5791
|
+
return lines.join('\n');
|
|
5792
|
+
}
|
|
5793
|
+
/**
|
|
5794
|
+
* Display search results in human-readable format
|
|
5795
|
+
*/ function displayResults(items, total, query) {
|
|
5796
|
+
if (0 === items.length) {
|
|
5797
|
+
logger_logger.warn(`No skills found for "${query}"`);
|
|
5798
|
+
return;
|
|
5799
|
+
}
|
|
5800
|
+
logger_logger.newline();
|
|
5801
|
+
logger_logger.log(`Found ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(String(total))} skill${1 === total ? '' : 's'} matching "${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(query)}":`);
|
|
5802
|
+
logger_logger.newline();
|
|
5803
|
+
for(let i = 0; i < items.length; i++){
|
|
5804
|
+
logger_logger.log(formatResultItem(items[i], i));
|
|
5805
|
+
if (i < items.length - 1) logger_logger.newline();
|
|
5806
|
+
}
|
|
5807
|
+
logger_logger.newline();
|
|
5808
|
+
logger_logger.log(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray('Install with: reskill install <name>'));
|
|
5809
|
+
}
|
|
5810
|
+
/**
|
|
5811
|
+
* Display search results as JSON
|
|
5812
|
+
*/ function displayJsonResults(items, total) {
|
|
5813
|
+
console.log(JSON.stringify({
|
|
5814
|
+
total,
|
|
5815
|
+
items
|
|
5816
|
+
}, null, 2));
|
|
5817
|
+
}
|
|
5818
|
+
// ============================================================================
|
|
5819
|
+
// Main Action
|
|
5820
|
+
// ============================================================================
|
|
5821
|
+
/**
|
|
5822
|
+
* Execute the find command
|
|
5823
|
+
*
|
|
5824
|
+
* @internal Exported for testing
|
|
5825
|
+
*/ async function findAction(query, options) {
|
|
5826
|
+
const limit = Number.parseInt(options.limit || '10', 10);
|
|
5827
|
+
if (Number.isNaN(limit) || limit < 1) {
|
|
5828
|
+
logger_logger.error('Invalid --limit value. Must be a positive integer.');
|
|
5829
|
+
process.exit(1);
|
|
5830
|
+
return;
|
|
5831
|
+
}
|
|
5832
|
+
const registry = resolveRegistryForSearch(options.registry);
|
|
5833
|
+
const client = new RegistryClient({
|
|
5834
|
+
registry
|
|
5835
|
+
});
|
|
5836
|
+
try {
|
|
5837
|
+
const { items, total } = await client.search(query, {
|
|
5838
|
+
limit
|
|
5839
|
+
});
|
|
5840
|
+
if (options.json) displayJsonResults(items, total);
|
|
5841
|
+
else displayResults(items, total, query);
|
|
5842
|
+
} catch (error) {
|
|
5843
|
+
if (error instanceof RegistryError) {
|
|
5844
|
+
logger_logger.error(`Search failed: ${error.message}`);
|
|
5845
|
+
if (401 === error.statusCode || 403 === error.statusCode) logger_logger.log('This registry may require authentication. Try: reskill login');
|
|
5846
|
+
} else logger_logger.error(`Search failed: ${error.message}`);
|
|
5847
|
+
process.exit(1);
|
|
5848
|
+
return;
|
|
5849
|
+
}
|
|
5850
|
+
}
|
|
5851
|
+
// ============================================================================
|
|
5852
|
+
// Command Definition
|
|
5853
|
+
// ============================================================================
|
|
5854
|
+
const findCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('find').alias('search').description('Search for skills in the registry').argument('<query>', 'Search query').option('-r, --registry <url>', 'Registry URL (or set RESKILL_REGISTRY env var, or defaults.publishRegistry in skills.json)').option('-l, --limit <n>', 'Maximum number of results', '10').option('-j, --json', 'Output as JSON').action(findAction);
|
|
5234
5855
|
/**
|
|
5235
5856
|
* info command - Show skill details
|
|
5236
5857
|
*/ const infoCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('info').description('Show skill details').argument('<skill>', 'Skill name').option('-j, --json', 'Output as JSON').action((skillName, options)=>{
|
|
@@ -5571,7 +6192,8 @@ const DEFAULT_INSTALL_DIR = '.skills';
|
|
|
5571
6192
|
const { results } = await skillManager.installToAgents(ref, targetAgents, {
|
|
5572
6193
|
force: options.force,
|
|
5573
6194
|
save: false,
|
|
5574
|
-
mode: installMode
|
|
6195
|
+
mode: installMode,
|
|
6196
|
+
registry: options.registry
|
|
5575
6197
|
});
|
|
5576
6198
|
const successCount = Array.from(results.values()).filter((r)=>r.success).length;
|
|
5577
6199
|
totalInstalled += successCount;
|
|
@@ -5620,7 +6242,8 @@ const DEFAULT_INSTALL_DIR = '.skills';
|
|
|
5620
6242
|
const { skill: installed, results } = await skillManager.installToAgents(skill, targetAgents, {
|
|
5621
6243
|
force: options.force,
|
|
5622
6244
|
save: false !== options.save && !installGlobally,
|
|
5623
|
-
mode: installMode
|
|
6245
|
+
mode: installMode,
|
|
6246
|
+
registry: options.registry
|
|
5624
6247
|
});
|
|
5625
6248
|
spinner.stop('Installation complete');
|
|
5626
6249
|
// Process and display results
|
|
@@ -5636,6 +6259,77 @@ const DEFAULT_INSTALL_DIR = '.skills';
|
|
|
5636
6259
|
});
|
|
5637
6260
|
}
|
|
5638
6261
|
}
|
|
6262
|
+
/**
|
|
6263
|
+
* Multi-skill path: list or install selected skills from a single repo (--skill / --list)
|
|
6264
|
+
*/ async function installMultiSkillFromRepo(ref, skillNames, listOnly, ctx, targetAgents, installGlobally, installMode, spinner) {
|
|
6265
|
+
const skillManager = new SkillManager(void 0, {
|
|
6266
|
+
global: installGlobally
|
|
6267
|
+
});
|
|
6268
|
+
if (listOnly) {
|
|
6269
|
+
spinner.start('Discovering skills...');
|
|
6270
|
+
const result = await skillManager.installSkillsFromRepo(ref, [], [], {
|
|
6271
|
+
listOnly: true
|
|
6272
|
+
});
|
|
6273
|
+
if (!result.listOnly || 0 === result.skills.length) {
|
|
6274
|
+
spinner.stop('No skills found');
|
|
6275
|
+
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.outro(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim('No skills found.'));
|
|
6276
|
+
return;
|
|
6277
|
+
}
|
|
6278
|
+
spinner.stop(`Found ${result.skills.length} skill(s)`);
|
|
6279
|
+
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.message('');
|
|
6280
|
+
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.step(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold('Available skills'));
|
|
6281
|
+
for (const s of result.skills){
|
|
6282
|
+
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.message(` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].cyan(s.name)}`);
|
|
6283
|
+
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.message(` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim(s.description)}`);
|
|
6284
|
+
}
|
|
6285
|
+
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.message('');
|
|
6286
|
+
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.outro(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim('Use --skill <name> to install specific skills.'));
|
|
6287
|
+
return;
|
|
6288
|
+
}
|
|
6289
|
+
const summaryLines = [
|
|
6290
|
+
__WEBPACK_EXTERNAL_MODULE_chalk__["default"].cyan(ref),
|
|
6291
|
+
` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim('→')} ${formatAgentNames(targetAgents)}`,
|
|
6292
|
+
` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim('Skills:')} ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].cyan(skillNames.join(', '))}`,
|
|
6293
|
+
` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim('Scope:')} ${installGlobally ? 'Global' : 'Project'}${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim(', Mode:')} ${installMode}`
|
|
6294
|
+
];
|
|
6295
|
+
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.note(summaryLines.join('\n'), 'Installation Summary');
|
|
6296
|
+
if (!ctx.skipConfirm) {
|
|
6297
|
+
const confirmed = await __WEBPACK_EXTERNAL_MODULE__clack_prompts__.confirm({
|
|
6298
|
+
message: 'Proceed with installation?'
|
|
6299
|
+
});
|
|
6300
|
+
if (__WEBPACK_EXTERNAL_MODULE__clack_prompts__.isCancel(confirmed) || !confirmed) {
|
|
6301
|
+
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.cancel('Installation cancelled');
|
|
6302
|
+
process.exit(0);
|
|
6303
|
+
}
|
|
6304
|
+
}
|
|
6305
|
+
spinner.start('Installing skills...');
|
|
6306
|
+
const result = await skillManager.installSkillsFromRepo(ref, skillNames, targetAgents, {
|
|
6307
|
+
force: ctx.options.force,
|
|
6308
|
+
save: false !== ctx.options.save && !installGlobally,
|
|
6309
|
+
mode: installMode,
|
|
6310
|
+
registry: ctx.options.registry
|
|
6311
|
+
});
|
|
6312
|
+
spinner.stop('Installation complete');
|
|
6313
|
+
// listOnly is always false here (the listOnly path returns early above)
|
|
6314
|
+
if (result.listOnly) return;
|
|
6315
|
+
const { installed, skipped } = result;
|
|
6316
|
+
if (0 === installed.length && skipped.length > 0) {
|
|
6317
|
+
const skipLines = skipped.map((s)=>` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim('–')} ${s.name}: ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim(s.reason)}`);
|
|
6318
|
+
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.note(skipLines.join('\n'), __WEBPACK_EXTERNAL_MODULE_chalk__["default"].yellow('All skills were already installed'));
|
|
6319
|
+
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.info('Use --force to reinstall.');
|
|
6320
|
+
return;
|
|
6321
|
+
}
|
|
6322
|
+
const resultLines = installed.map((r)=>` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].green('✓')} ${r.skill.name}@${r.skill.version}`);
|
|
6323
|
+
if (skipped.length > 0) for (const s of skipped)resultLines.push(` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim('–')} ${s.name}: ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim(s.reason)}`);
|
|
6324
|
+
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.note(resultLines.join('\n'), __WEBPACK_EXTERNAL_MODULE_chalk__["default"].green(`Installed ${installed.length} skill(s)`));
|
|
6325
|
+
if (!installGlobally && installed.length > 0 && ctx.configLoader.exists()) {
|
|
6326
|
+
ctx.configLoader.reload();
|
|
6327
|
+
ctx.configLoader.updateDefaults({
|
|
6328
|
+
targetAgents,
|
|
6329
|
+
installMode
|
|
6330
|
+
});
|
|
6331
|
+
}
|
|
6332
|
+
}
|
|
5639
6333
|
/**
|
|
5640
6334
|
* Install multiple skills in batch
|
|
5641
6335
|
*/ async function installMultipleSkills(ctx, targetAgents, installGlobally, installMode, spinner) {
|
|
@@ -5672,6 +6366,7 @@ const DEFAULT_INSTALL_DIR = '.skills';
|
|
|
5672
6366
|
const { skill: installed, results } = await skillManager.installToAgents(skillRef, targetAgents, {
|
|
5673
6367
|
force: options.force,
|
|
5674
6368
|
save: false !== options.save && !installGlobally,
|
|
6369
|
+
registry: options.registry,
|
|
5675
6370
|
mode: installMode
|
|
5676
6371
|
});
|
|
5677
6372
|
const successful = Array.from(results.values()).filter((r)=>r.success);
|
|
@@ -5709,7 +6404,7 @@ const DEFAULT_INSTALL_DIR = '.skills';
|
|
|
5709
6404
|
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.error(`${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].red('✗')} ${result.skillRef}`);
|
|
5710
6405
|
}
|
|
5711
6406
|
// Display batch results
|
|
5712
|
-
|
|
6407
|
+
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.message('');
|
|
5713
6408
|
displayBatchInstallResults(successfulSkills, failedSkills, targetAgents.length);
|
|
5714
6409
|
// Save installation defaults (only for project installs with success)
|
|
5715
6410
|
if (!installGlobally && successfulSkills.length > 0 && configLoader.exists()) {
|
|
@@ -5798,7 +6493,7 @@ const DEFAULT_INSTALL_DIR = '.skills';
|
|
|
5798
6493
|
* Behavior:
|
|
5799
6494
|
* - Single skill install: Prompts for agents/mode (stored config as defaults)
|
|
5800
6495
|
* - Reinstall all (no args): Uses stored config directly, no confirmation
|
|
5801
|
-
*/ const installCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('install').alias('i').description('Install one or more skills, or all skills from skills.json').argument('[skills...]', 'Skill references (e.g., github:user/skill@v1.0.0 or git@github.com:user/repo.git)').option('-f, --force', 'Force reinstall even if already installed').option('-g, --global', 'Install globally to user home directory').option('--no-save', 'Do not save to skills.json').option('-a, --agent <agents...>', 'Specify target agents (e.g., cursor, claude-code)').option('--mode <mode>', 'Installation mode: symlink or copy').option('-y, --yes', 'Skip confirmation prompts').option('--all', 'Install to all agents (implies -y -g)').action(async (skills, options)=>{
|
|
6496
|
+
*/ const installCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('install').alias('i').description('Install one or more skills, or all skills from skills.json').argument('[skills...]', 'Skill references (e.g., github:user/skill@v1.0.0 or git@github.com:user/repo.git)').option('-f, --force', 'Force reinstall even if already installed').option('-g, --global', 'Install globally to user home directory').option('--no-save', 'Do not save to skills.json').option('-a, --agent <agents...>', 'Specify target agents (e.g., cursor, claude-code)').option('--mode <mode>', 'Installation mode: symlink or copy').option('-y, --yes', 'Skip confirmation prompts').option('--all', 'Install to all agents (implies -y -g)').option('-s, --skill <names...>', 'Select specific skill(s) by name from a multi-skill repository').option('--list', 'List available skills in the repository without installing').option('-r, --registry <url>', 'Registry URL override for registry-based installs').action(async (skills, options)=>{
|
|
5802
6497
|
// Handle --all flag implications
|
|
5803
6498
|
if (options.all) {
|
|
5804
6499
|
options.yes = true;
|
|
@@ -5811,19 +6506,34 @@ const DEFAULT_INSTALL_DIR = '.skills';
|
|
|
5811
6506
|
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.intro(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bgCyan.black(' reskill '));
|
|
5812
6507
|
try {
|
|
5813
6508
|
const spinner = __WEBPACK_EXTERNAL_MODULE__clack_prompts__.spinner();
|
|
5814
|
-
//
|
|
5815
|
-
const
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
6509
|
+
// Multi-skill path (single ref + --skill or --list): list only skips scope/mode/agents
|
|
6510
|
+
const hasMultiSkillFlags = true === ctx.options.list || ctx.options.skill && ctx.options.skill.length > 0;
|
|
6511
|
+
const isMultiSkillPath = !ctx.isReinstallAll && 1 === ctx.skills.length && hasMultiSkillFlags;
|
|
6512
|
+
// Warn if --skill/--list used with multiple refs (flags will be ignored)
|
|
6513
|
+
if (ctx.skills.length > 1 && hasMultiSkillFlags) __WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.warn('--skill and --list are only supported with a single repository reference');
|
|
6514
|
+
let targetAgents;
|
|
6515
|
+
let installGlobally;
|
|
6516
|
+
let installMode;
|
|
6517
|
+
if (isMultiSkillPath && true === ctx.options.list) {
|
|
6518
|
+
targetAgents = [];
|
|
6519
|
+
installGlobally = false;
|
|
6520
|
+
installMode = 'symlink';
|
|
6521
|
+
} else {
|
|
6522
|
+
// Step 1: Resolve target agents
|
|
6523
|
+
targetAgents = await resolveTargetAgents(ctx, spinner);
|
|
6524
|
+
// Step 2: Resolve installation scope
|
|
6525
|
+
installGlobally = await resolveInstallScope(ctx);
|
|
6526
|
+
// Validate: Cannot install all skills globally
|
|
6527
|
+
if (ctx.isReinstallAll && installGlobally) {
|
|
6528
|
+
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.error('Cannot install all skills globally. Please specify a skill to install.');
|
|
6529
|
+
process.exit(1);
|
|
6530
|
+
}
|
|
6531
|
+
// Step 3: Resolve installation mode
|
|
6532
|
+
installMode = await resolveInstallMode(ctx);
|
|
5822
6533
|
}
|
|
5823
|
-
// Step 3: Resolve installation mode
|
|
5824
|
-
const installMode = await resolveInstallMode(ctx);
|
|
5825
6534
|
// Step 4: Execute installation
|
|
5826
6535
|
if (ctx.isReinstallAll) await installAllSkills(ctx, targetAgents, installMode, spinner);
|
|
6536
|
+
else if (isMultiSkillPath) await installMultiSkillFromRepo(ctx.skills[0], ctx.options.skill ?? [], true === ctx.options.list, ctx, targetAgents, installGlobally, installMode, spinner);
|
|
5827
6537
|
else if (ctx.isBatchInstall) await installMultipleSkills(ctx, targetAgents, installGlobally, installMode, spinner);
|
|
5828
6538
|
else await installSingleSkill(ctx, targetAgents, installGlobally, installMode, spinner);
|
|
5829
6539
|
// Done
|
|
@@ -5996,45 +6706,6 @@ class AuthManager {
|
|
|
5996
6706
|
});
|
|
5997
6707
|
}
|
|
5998
6708
|
}
|
|
5999
|
-
/**
|
|
6000
|
-
* Registry URL resolution utilities
|
|
6001
|
-
*
|
|
6002
|
-
* Shared utility for resolving registry URLs across CLI commands.
|
|
6003
|
-
*/ /**
|
|
6004
|
-
* Resolve registry URL from multiple sources
|
|
6005
|
-
*
|
|
6006
|
-
* Priority (highest to lowest):
|
|
6007
|
-
* 1. --registry CLI option
|
|
6008
|
-
* 2. RESKILL_REGISTRY environment variable
|
|
6009
|
-
* 3. defaults.publishRegistry in skills.json
|
|
6010
|
-
*
|
|
6011
|
-
* Intentionally has NO default - users must explicitly configure their registry.
|
|
6012
|
-
*
|
|
6013
|
-
* @param cliRegistry - Registry URL from CLI option
|
|
6014
|
-
* @param projectRoot - Project root directory (defaults to cwd)
|
|
6015
|
-
* @returns Resolved registry URL
|
|
6016
|
-
* @throws Exits process with code 1 if no registry is configured
|
|
6017
|
-
*/ function resolveRegistry(cliRegistry, projectRoot = process.cwd()) {
|
|
6018
|
-
// 1. CLI option (highest priority)
|
|
6019
|
-
if (cliRegistry) return cliRegistry;
|
|
6020
|
-
// 2. Environment variable
|
|
6021
|
-
const envRegistry = process.env.RESKILL_REGISTRY;
|
|
6022
|
-
if (envRegistry) return envRegistry;
|
|
6023
|
-
// 3. From skills.json
|
|
6024
|
-
const configLoader = new ConfigLoader(projectRoot);
|
|
6025
|
-
if (configLoader.exists()) {
|
|
6026
|
-
const publishRegistry = configLoader.getPublishRegistry();
|
|
6027
|
-
if (publishRegistry) return publishRegistry;
|
|
6028
|
-
}
|
|
6029
|
-
// No registry configured - error
|
|
6030
|
-
logger_logger.error('No registry specified');
|
|
6031
|
-
logger_logger.newline();
|
|
6032
|
-
logger_logger.log('Please specify a registry using one of these methods:');
|
|
6033
|
-
logger_logger.log(' • --registry <url> option');
|
|
6034
|
-
logger_logger.log(' • RESKILL_REGISTRY environment variable');
|
|
6035
|
-
logger_logger.log(' • "defaults.publishRegistry" in skills.json');
|
|
6036
|
-
process.exit(1);
|
|
6037
|
-
}
|
|
6038
6709
|
/**
|
|
6039
6710
|
* login command - Authenticate with a reskill registry
|
|
6040
6711
|
*
|
|
@@ -6066,8 +6737,7 @@ async function loginAction(options) {
|
|
|
6066
6737
|
// Verify token by calling login-cli endpoint
|
|
6067
6738
|
const client = new RegistryClient({
|
|
6068
6739
|
registry,
|
|
6069
|
-
token
|
|
6070
|
-
apiPrefix: getApiPrefix(registry)
|
|
6740
|
+
token
|
|
6071
6741
|
});
|
|
6072
6742
|
try {
|
|
6073
6743
|
const response = await client.loginCli();
|
|
@@ -7184,8 +7854,7 @@ async function publishAction(skillPath, options) {
|
|
|
7184
7854
|
logger_logger.log(`Publishing to ${registry}...`);
|
|
7185
7855
|
const client = new RegistryClient({
|
|
7186
7856
|
registry,
|
|
7187
|
-
token
|
|
7188
|
-
apiPrefix: getApiPrefix(registry)
|
|
7857
|
+
token
|
|
7189
7858
|
});
|
|
7190
7859
|
try {
|
|
7191
7860
|
// Get skill name from SKILL.md (primary source per agentskills.io spec)
|
|
@@ -7363,8 +8032,7 @@ async function whoamiAction(options) {
|
|
|
7363
8032
|
// Verify with server
|
|
7364
8033
|
const client = new RegistryClient({
|
|
7365
8034
|
registry,
|
|
7366
|
-
token
|
|
7367
|
-
apiPrefix: getApiPrefix(registry)
|
|
8035
|
+
token
|
|
7368
8036
|
});
|
|
7369
8037
|
try {
|
|
7370
8038
|
const response = await client.whoami();
|
|
@@ -7400,6 +8068,7 @@ const program = new __WEBPACK_EXTERNAL_MODULE_commander__.Command();
|
|
|
7400
8068
|
program.name('reskill').description('AI Skills Package Manager - Git-based skills management for AI agents').version(cli_rslib_entry_packageJson.version);
|
|
7401
8069
|
// Register all commands
|
|
7402
8070
|
program.addCommand(initCommand);
|
|
8071
|
+
program.addCommand(findCommand);
|
|
7403
8072
|
program.addCommand(installCommand);
|
|
7404
8073
|
program.addCommand(listCommand);
|
|
7405
8074
|
program.addCommand(infoCommand);
|