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/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
- * 公共 Registry URL
434
- * 用于无 scope skill 安装
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
- * 支持私有 Registry(带 @scope)和公共 Registry(无 scope)两种格式。
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
- * // 私有 Registry
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
- * // 公共 Registry
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
- // scope 的格式: @scope/name[@version]
608
+ // Scoped format: @scope/name[@version]
634
609
  if (trimmed.startsWith('@')) {
635
- // 正则匹配: @scope/name[@version]
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
- // scope 的格式: name[@version](公共 Registry)
650
- // name 不能包含 /(否则可能是 git shorthand
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
- return {
1660
+ result = {
1318
1661
  success: true,
1319
1662
  path: agentDir,
1320
1663
  mode: 'copy'
1321
1664
  };
1322
- }
1323
- // Symlink mode: copy to canonical location, then create symlink
1324
- installer_ensureDir(canonicalDir);
1325
- installer_remove(canonicalDir);
1326
- copyDirectory(sourcePath, canonicalDir);
1327
- const symlinkCreated = await installer_createSymlink(canonicalDir, agentDir);
1328
- if (!symlinkCreated) {
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
- return {
1346
- success: true,
1347
- path: agentDir,
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)) return this.parseGitUrlRef(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
- const subPath = parts.length > 2 ? parts.slice(2).join('/') : void 0;
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 pointing to archive files (.tar.gz, .tgz, .zip, .tar)
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
- // 排除 Git 仓库 URL(以 .git 结尾)
2385
- if (urlPart.endsWith('.git')) return false;
2386
- // 排除 GitHub/GitLab web URL(包含 /tree/, /blob/, /raw/)
2387
- if (/\/(tree|blob|raw)\//.test(urlPart)) return false;
2388
- return urlPart.startsWith('http://') || urlPart.startsWith('https://') || urlPart.startsWith('oss://') || urlPart.startsWith('s3://');
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 + apiPrefix)
3162
+ * Get API base URL (registry + /api)
2715
3163
  *
2716
- * @returns Base URL for API calls, e.g., 'https://example.com/api' or 'https://rush.com/api/reskill'
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}${prefix}`;
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
- // 如果提供了 shortName,则在路径前添加顶层目录
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
- * 获取 skill 基本信息(包含 source_type
2803
- * 用于 install 命令判断安装逻辑分支
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 - 完整名称,如 @kanyun/my-skill
2806
- * @returns Skill 基本信息
2807
- * @throws RegistryError 如果 skill 不存在或请求失败
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
- // 404 时给出明确的 skill 不存在错误
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 返回格式: { success: true, data: { ... } }
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
- // 如果是 semver 版本号,直接返回
3324
+ // If it's already a semver version number, return as-is
2841
3325
  if (/^\d+\.\d+\.\d+/.test(version)) return version;
2842
- // 否则视为 tag,需要查询 dist-tags
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 返回格式: { success: true, data: { dist_tags: [{ tag, version }] } }
3336
+ // API response format: { success: true, data: { dist_tags: [{ tag, version }] } }
2853
3337
  const responseData = await response.json();
2854
- // 优先使用 npm 风格的 dist-tags(如果存在)
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
- // 使用 reskill-app dist_tags 数组格式
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
- // 解析 integrity 格式: algorithm-hash
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
- // 只支持 sha256 sha512
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
- // 提取短名称作为 tarball 顶层目录(不含 scope 前缀)
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
- // 排除 Git SSH 格式 (git@...)
3679
+ // Exclude Git SSH format (git@...)
3196
3680
  if (ref.startsWith('git@') || ref.startsWith('git://')) return false;
3197
- // 排除 .git 结尾的 URL
3681
+ // Exclude URLs ending with .git
3198
3682
  if (ref.includes('.git')) return false;
3199
- // 排除 HTTP/HTTPS/OSS URL
3683
+ // Exclude HTTP/HTTPS/OSS URLs
3200
3684
  if (ref.startsWith('http://') || ref.startsWith('https://') || ref.startsWith('oss://') || ref.startsWith('s3://')) return false;
3201
- // 排除 registry shorthand 格式 (github:, gitlab:, custom.com:)
3202
- // 这类格式是 "registry:owner/repo" 而不是 "@scope/name"
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
- // 检查是否是 @scope/name 格式(私有 registry
3688
+ // Check for @scope/name format (private registry)
3205
3689
  if (ref.startsWith('@') && ref.includes('/')) {
3206
- // @scope/name @scope/name@version
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
- // 检查是否是简单的 name name@version 格式(公共 registry
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. 解析 skill 标识
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. 获取 registry URL
3230
- const registryUrl = getRegistryUrl(parsed.scope);
3231
- // 3. 创建 client 并解析版本
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. 下载 tarball
3721
+ // 4. Download tarball
3238
3722
  const { tarball, integrity } = await client.downloadSkill(parsed.fullName, version);
3239
- // 5. 验证 integrity
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
- // 获取顶层目录名(即 skill 名称)
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
- // Get the real skill name from SKILL.md in cache
3891
+ // Resolve source path (cache root or skill subdirectory for multi-skill repos)
3599
3892
  const cachePath = this.cache.getCachePath(parsed, gitRef);
3600
- const metadata = this.getSkillMetadataFromDir(cachePath);
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
- await this.cache.copyTo(parsed, gitRef, skillPath);
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
- // Get cache path as source
3971
- const sourcePath = this.cache.getCachePath(parsed, gitRef);
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
- // 解析 skill 标识(获取 fullName version)
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
- // 新增:先查询 skill 信息获取 source_type
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
- // 新增:根据 source_type 分支
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, options);
4116
- // 1. Resolve registry skill(现有流程)
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
- // 4. Extract tarball
4161
- const extractedPath = await this.registryResolver.extract(tarball, tempDir);
4162
- logger_logger.debug(`Extracted to ${extractedPath}`);
4163
- // 5. Create Installer with custom installDir from config
4164
- const defaults = this.config.getDefaults();
4165
- const installer = new Installer({
4166
- cwd: this.projectRoot,
4167
- global: this.isGlobal,
4168
- installDir: defaults.installDir
4169
- });
4170
- // 6. Install to all target agents
4171
- const results = await installer.installToAgents(extractedPath, shortName, targetAgents, {
4172
- mode: mode
4173
- });
4174
- // 7. Update lock file (project mode only)
4175
- if (!this.isGlobal) this.lockManager.lockSkill(shortName, {
4176
- source: `registry:${resolvedParsed.fullName}`,
4177
- version,
4178
- ref: version,
4179
- resolved: resolvedRegistryUrl,
4180
- commit: resolved.integrity
4181
- });
4182
- // 8. Update skills.json (project mode only)
4183
- if (!this.isGlobal && save) {
4184
- this.config.ensureExists();
4185
- // Save with full name for registry skills
4186
- this.config.addSkill(shortName, ref);
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
- * 安装页面发布的 skill
4614
+ * Install a web-published skill.
4210
4615
  *
4211
- * 页面发布的 skill 不支持版本管理,根据 source_type 分支到不同的安装逻辑:
4212
- * - github/gitlab: 复用 installToAgentsFromGit
4213
- * - oss_url/custom_url: 复用 installToAgentsFromHttp
4214
- * - local: 通过 Registry API 下载 tarball
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
- // 页面发布的 skill 不支持版本指定
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 是完整的 Git URL(包含 ref path
4225
- // 复用已有的 Git 安装逻辑
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
- // 直接下载 URL
4635
+ // Direct download URL
4230
4636
  return this.installToAgentsFromHttp(source_url, targetAgents, options);
4231
4637
  case 'local':
4232
- // 通过 Registry API 下载 tarball
4233
- return this.installFromRegistryLocal(skillInfo, parsed, targetAgents, options);
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
- * 安装 Local Folder 模式发布的 skill
4645
+ * Install a skill published via "local folder" mode.
4240
4646
  *
4241
- * 通过 Registry {apiPrefix}/skills/:name/download API 下载 tarball
4242
- * (apiPrefix 根据 registry 不同而不同,如 /api /api/reskill)
4243
- */ async installFromRegistryLocal(_skillInfo, parsed, targetAgents, options = {}) {
4244
- const registryUrl = getRegistryUrl(parsed.scope);
4245
- // 构造下载 URL(通过 Registry API)
4246
- // Ensure trailing slash for proper URL concatenation (defensive coding)
4247
- const baseUrl = registryUrl.endsWith('/') ? registryUrl : `${registryUrl}/`;
4248
- const downloadUrl = `${baseUrl}api/skills/${encodeURIComponent(parsed.fullName)}/download`;
4249
- logger_logger.debug(`Downloading from: ${downloadUrl}`);
4250
- // 复用 HTTP 下载逻辑
4251
- return this.installToAgentsFromHttp(downloadUrl, targetAgents, options);
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
- console.log();
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
- // Step 1: Resolve target agents
5815
- const targetAgents = await resolveTargetAgents(ctx, spinner);
5816
- // Step 2: Resolve installation scope
5817
- const installGlobally = await resolveInstallScope(ctx);
5818
- // Validate: Cannot install all skills globally
5819
- if (ctx.isReinstallAll && installGlobally) {
5820
- __WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.error('Cannot install all skills globally. Please specify a skill to install.');
5821
- process.exit(1);
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);