reskill 1.6.0 → 1.8.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -430,18 +430,20 @@ 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
- 'https://reskill-test.zhenguanyu.com': '@kanyun',
441
- 'https://reskill-test.zhenguanyu.com/': '@kanyun',
440
+ // rush-app (private registry, new)
441
+ 'https://rush-test.zhenguanyu.com': '@kanyun-test',
442
+ 'https://rush.zhenguanyu.com': '@kanyun',
443
+ // reskill-app (private registry, legacy)
444
+ 'https://reskill-test.zhenguanyu.com': '@kanyun-test',
442
445
  // Local development
443
- 'http://localhost:3000': '@kanyun',
444
- 'http://localhost:3000/': '@kanyun'
446
+ 'http://localhost:3000': '@kanyun-test'
445
447
  };
446
448
  /**
447
449
  * Get the scope for a given registry URL
@@ -450,7 +452,7 @@ var external_node_fs_ = __webpack_require__("node:fs");
450
452
  * @returns Scope string (e.g., "@kanyun") or null if not found
451
453
  *
452
454
  * @example
453
- * getScopeForRegistry('https://reskill-test.zhenguanyu.com') // '@kanyun'
455
+ * getScopeForRegistry('https://rush-test.zhenguanyu.com') // '@kanyun'
454
456
  * getScopeForRegistry('https://unknown.com') // null
455
457
  */ function getScopeForRegistry(registry) {
456
458
  if (!registry) return null;
@@ -468,8 +470,8 @@ var external_node_fs_ = __webpack_require__("node:fs");
468
470
  * @returns Registry URL (with trailing slash) or null if not found
469
471
  *
470
472
  * @example
471
- * getRegistryForScope('@kanyun') // 'https://reskill-test.zhenguanyu.com/'
472
- * getRegistryForScope('kanyun') // 'https://reskill-test.zhenguanyu.com/'
473
+ * getRegistryForScope('@kanyun') // 'https://rush-test.zhenguanyu.com/'
474
+ * getRegistryForScope('kanyun') // 'https://rush-test.zhenguanyu.com/'
473
475
  * getRegistryForScope('@unknown') // null
474
476
  * getRegistryForScope('@mycompany', { '@mycompany': 'https://my.registry.com/' }) // 'https://my.registry.com/'
475
477
  */ function getRegistryForScope(scope, customRegistries) {
@@ -499,8 +501,8 @@ var external_node_fs_ = __webpack_require__("node:fs");
499
501
  * @throws Error if scope is provided but not found in the registry map
500
502
  *
501
503
  * @example
502
- * getRegistryUrl('@kanyun') // 'https://reskill-test.zhenguanyu.com/'
503
- * getRegistryUrl('kanyun') // 'https://reskill-test.zhenguanyu.com/'
504
+ * getRegistryUrl('@kanyun') // 'https://rush-test.zhenguanyu.com/'
505
+ * getRegistryUrl('kanyun') // 'https://rush-test.zhenguanyu.com/'
504
506
  * getRegistryUrl(null) // 'https://reskill.info/'
505
507
  * getRegistryUrl('') // 'https://reskill.info/'
506
508
  * getRegistryUrl('@unknown') // throws Error
@@ -575,21 +577,21 @@ var external_node_fs_ = __webpack_require__("node:fs");
575
577
  /**
576
578
  * Parse a skill identifier into its components (with version support)
577
579
  *
578
- * 支持私有 Registry(带 @scope)和公共 Registry(无 scope)两种格式。
580
+ * Supports both private registry (with @scope) and public registry (without scope) formats.
579
581
  *
580
582
  * @param identifier - Skill identifier string
581
583
  * @returns Parsed skill identifier with scope, name, version, and fullName
582
584
  * @throws Error if identifier is invalid
583
585
  *
584
586
  * @example
585
- * // 私有 Registry
587
+ * // Private registry
586
588
  * parseSkillIdentifier('@kanyun/planning-with-files')
587
589
  * // { scope: '@kanyun', name: 'planning-with-files', version: undefined, fullName: '@kanyun/planning-with-files' }
588
590
  *
589
591
  * parseSkillIdentifier('@kanyun/skill@2.4.5')
590
592
  * // { scope: '@kanyun', name: 'skill', version: '2.4.5', fullName: '@kanyun/skill' }
591
593
  *
592
- * // 公共 Registry
594
+ * // Public registry
593
595
  * parseSkillIdentifier('planning-with-files')
594
596
  * // { scope: null, name: 'planning-with-files', version: undefined, fullName: 'planning-with-files' }
595
597
  *
@@ -597,18 +599,18 @@ var external_node_fs_ = __webpack_require__("node:fs");
597
599
  * // { scope: null, name: 'skill', version: 'latest', fullName: 'skill' }
598
600
  */ function parseSkillIdentifier(identifier) {
599
601
  const trimmed = identifier.trim();
600
- // 空字符串或仅空白
602
+ // Empty string or whitespace only
601
603
  if (!trimmed) throw new Error('Invalid skill identifier: empty string');
602
- // @@ 开头无效
604
+ // Starting with @@ is invalid
603
605
  if (trimmed.startsWith('@@')) throw new Error('Invalid skill identifier: invalid scope format');
604
- // 只有 @ 无效
606
+ // Bare @ is invalid
605
607
  if ('@' === trimmed) throw new Error('Invalid skill identifier: missing scope and name');
606
- // scope 的格式: @scope/name[@version]
608
+ // Scoped format: @scope/name[@version]
607
609
  if (trimmed.startsWith('@')) {
608
- // 正则匹配: @scope/name[@version]
609
- // scope: @ 开头,后面跟字母数字、连字符、下划线
610
- // name: 字母数字、连字符、下划线
611
- // 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
612
614
  const scopedMatch = trimmed.match(/^(@[\w-]+)\/([\w-]+)(?:@(.+))?$/);
613
615
  if (!scopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
614
616
  const [, scope, name, version] = scopedMatch;
@@ -619,8 +621,8 @@ var external_node_fs_ = __webpack_require__("node:fs");
619
621
  fullName: `${scope}/${name}`
620
622
  };
621
623
  }
622
- // scope 的格式: name[@version](公共 Registry)
623
- // name 不能包含 /(否则可能是 git shorthand
624
+ // Unscoped format: name[@version] (public registry)
625
+ // name must not contain / (otherwise it might be a git shorthand)
624
626
  const unscopedMatch = trimmed.match(/^([\w-]+)(?:@(.+))?$/);
625
627
  if (!unscopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
626
628
  const [, name, version] = unscopedMatch;
@@ -1094,6 +1096,369 @@ const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEB
1094
1096
  if (external_node_fs_.existsSync(tempArchive)) external_node_fs_.unlinkSync(tempArchive);
1095
1097
  }
1096
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
+ }
1097
1462
  /**
1098
1463
  * Installer - Multi-Agent installer
1099
1464
  *
@@ -1104,6 +1469,10 @@ const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEB
1104
1469
  * Reference: https://github.com/vercel-labs/add-skill/blob/main/src/installer.ts
1105
1470
  */ const installer_AGENTS_DIR = '.agents';
1106
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 -->';
1107
1476
  /**
1108
1477
  * Default files to exclude when copying skills
1109
1478
  * These files are typically used for repository metadata and should not be copied to agent directories
@@ -1282,45 +1651,50 @@ const installer_SKILLS_SUBDIR = 'skills';
1282
1651
  error: 'Invalid skill name: potential path traversal detected'
1283
1652
  };
1284
1653
  try {
1654
+ let result;
1285
1655
  // Copy mode: directly copy to agent location
1286
1656
  if ('copy' === installMode) {
1287
1657
  installer_ensureDir(agentDir);
1288
1658
  installer_remove(agentDir);
1289
1659
  copyDirectory(sourcePath, agentDir);
1290
- return {
1660
+ result = {
1291
1661
  success: true,
1292
1662
  path: agentDir,
1293
1663
  mode: 'copy'
1294
1664
  };
1295
- }
1296
- // Symlink mode: copy to canonical location, then create symlink
1297
- installer_ensureDir(canonicalDir);
1298
- installer_remove(canonicalDir);
1299
- copyDirectory(sourcePath, canonicalDir);
1300
- const symlinkCreated = await installer_createSymlink(canonicalDir, agentDir);
1301
- if (!symlinkCreated) {
1302
- // Symlink failed, fallback to copy
1303
- try {
1304
- installer_remove(agentDir);
1305
- } catch {
1306
- // Ignore cleanup errors
1307
- }
1308
- installer_ensureDir(agentDir);
1309
- copyDirectory(sourcePath, agentDir);
1310
- 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 = {
1311
1672
  success: true,
1312
1673
  path: agentDir,
1313
1674
  canonicalPath: canonicalDir,
1314
- mode: 'symlink',
1315
- symlinkFailed: true
1675
+ mode: 'symlink'
1316
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
+ }
1317
1694
  }
1318
- return {
1319
- success: true,
1320
- path: agentDir,
1321
- canonicalPath: canonicalDir,
1322
- mode: 'symlink'
1323
- };
1695
+ // Create Cursor bridge rule file (project-level only)
1696
+ if ('cursor' === agentType && !this.isGlobal) this.createCursorBridgeRule(sanitized, sourcePath);
1697
+ return result;
1324
1698
  } catch (error) {
1325
1699
  return {
1326
1700
  success: false,
@@ -1358,6 +1732,8 @@ const installer_SKILLS_SUBDIR = 'skills';
1358
1732
  const skillPath = this.getAgentSkillPath(skillName, agentType);
1359
1733
  if (!external_node_fs_.existsSync(skillPath)) return false;
1360
1734
  installer_remove(skillPath);
1735
+ // Remove Cursor bridge rule file (project-level only)
1736
+ if ('cursor' === agentType && !this.isGlobal) this.removeCursorBridgeRule(installer_sanitizeName(skillName));
1361
1737
  return true;
1362
1738
  }
1363
1739
  /**
@@ -1380,6 +1756,65 @@ const installer_SKILLS_SUBDIR = 'skills';
1380
1756
  withFileTypes: true
1381
1757
  }).filter((entry)=>entry.isDirectory() || entry.isSymbolicLink()).map((entry)=>entry.name);
1382
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
+ }
1383
1818
  }
1384
1819
  /**
1385
1820
  * CacheManager - Manage global skill cache
@@ -2112,11 +2547,25 @@ const installer_SKILLS_SUBDIR = 'skills';
2112
2547
  }
2113
2548
  // Parse owner/repo and possible subPath
2114
2549
  // E.g.: user/repo or org/monorepo/skills/pdf
2550
+ // Also handle GitHub web URL style: owner/repo/tree/branch/path
2115
2551
  const parts = remaining.split('/');
2116
2552
  if (parts.length < 2) throw new Error(`Invalid skill reference: ${ref}. Expected format: owner/repo[@version]`);
2117
2553
  const owner = parts[0];
2118
2554
  const repo = parts[1];
2119
- const subPath = parts.length > 2 ? parts.slice(2).join('/') : void 0;
2555
+ let subPath;
2556
+ // Check for GitHub/GitLab web URL pattern: owner/repo/(tree|blob|raw)/branch/path
2557
+ // e.g. vercel-labs/skills/tree/main/skills/find-skills
2558
+ // Only apply this heuristic when no explicit @version is provided.
2559
+ // With @version, treat tree/blob/raw as literal directory names (standard monorepo subPath).
2560
+ if (parts.length >= 4 && [
2561
+ 'tree',
2562
+ 'blob',
2563
+ 'raw'
2564
+ ].includes(parts[2]) && !version) {
2565
+ const branch = parts[3];
2566
+ version = `branch:${branch}`;
2567
+ subPath = parts.length > 4 ? parts.slice(4).join('/') : void 0;
2568
+ } else subPath = parts.length > 2 ? parts.slice(2).join('/') : void 0;
2120
2569
  return {
2121
2570
  registry,
2122
2571
  owner,
@@ -2345,20 +2794,31 @@ const installer_SKILLS_SUBDIR = 'skills';
2345
2794
  * Check if a reference is an HTTP/OSS URL (for archive downloads)
2346
2795
  *
2347
2796
  * Returns true for:
2348
- * - http:// or https:// URLs pointing to archive files (.tar.gz, .tgz, .zip, .tar)
2349
- * - Explicit oss:// or s3:// protocol URLs
2797
+ * - http:// or https:// URLs with archive file extensions (.tar.gz, .tgz, .zip, .tar)
2798
+ * - Explicit oss:// or s3:// protocol URLs (always treated as archive sources)
2350
2799
  *
2351
2800
  * Returns false for:
2352
2801
  * - Git repository URLs (*.git)
2353
2802
  * - GitHub/GitLab web URLs (/tree/, /blob/, /raw/)
2803
+ * - Bare HTTPS repo URLs without archive extensions (e.g., https://github.com/user/repo)
2804
+ * These are treated as Git references and handled by GitResolver.
2354
2805
  */ static isHttpUrl(ref) {
2355
2806
  // Remove version suffix for checking (e.g., url@v1.0.0)
2356
2807
  const urlPart = ref.split('@')[0];
2357
- // 排除 Git 仓库 URL(以 .git 结尾)
2358
- if (urlPart.endsWith('.git')) return false;
2359
- // 排除 GitHub/GitLab web URL(包含 /tree/, /blob/, /raw/)
2360
- if (/\/(tree|blob|raw)\//.test(urlPart)) return false;
2361
- return urlPart.startsWith('http://') || urlPart.startsWith('https://') || urlPart.startsWith('oss://') || urlPart.startsWith('s3://');
2808
+ // oss:// and s3:// are always archive download sources
2809
+ if (urlPart.startsWith('oss://') || urlPart.startsWith('s3://')) return true;
2810
+ // For http:// and https:// URLs, distinguish between Git repos and archive downloads
2811
+ if (urlPart.startsWith('http://') || urlPart.startsWith('https://')) {
2812
+ // Exclude Git repository URLs (ending with .git)
2813
+ if (urlPart.endsWith('.git')) return false;
2814
+ // Exclude GitHub/GitLab web URLs (containing /tree/, /blob/, /raw/)
2815
+ if (/\/(tree|blob|raw)\//.test(urlPart)) return false;
2816
+ // Only classify as HTTP archive if URL has a recognized archive extension.
2817
+ // Bare HTTPS URLs like https://github.com/user/repo are Git references,
2818
+ // not archive downloads, and should fall through to GitResolver.
2819
+ return /\.(tar\.gz|tgz|zip|tar)$/i.test(urlPart);
2820
+ }
2821
+ return false;
2362
2822
  }
2363
2823
  /**
2364
2824
  * Parse an HTTP/OSS URL reference
@@ -2684,6 +3144,16 @@ class RegistryClient {
2684
3144
  this.config = config;
2685
3145
  }
2686
3146
  /**
3147
+ * Get API base URL (registry + /api)
3148
+ *
3149
+ * All registries use the unified '/api' prefix.
3150
+ *
3151
+ * @returns Base URL for API calls, e.g., 'https://example.com/api'
3152
+ */ getApiBase() {
3153
+ const registry = this.config.registry.endsWith('/') ? this.config.registry.slice(0, -1) : this.config.registry;
3154
+ return `${registry}/api`;
3155
+ }
3156
+ /**
2687
3157
  * Get authorization headers
2688
3158
  */ getAuthHeaders() {
2689
3159
  const headers = {
@@ -2696,7 +3166,7 @@ class RegistryClient {
2696
3166
  /**
2697
3167
  * Get current user info (whoami)
2698
3168
  */ async whoami() {
2699
- const url = `${this.config.registry}/api/auth/me`;
3169
+ const url = `${this.getApiBase()}/skill-auth/me`;
2700
3170
  const response = await fetch(url, {
2701
3171
  method: 'GET',
2702
3172
  headers: this.getAuthHeaders()
@@ -2708,13 +3178,13 @@ class RegistryClient {
2708
3178
  /**
2709
3179
  * CLI login - verify token and get user info
2710
3180
  *
2711
- * Calls POST /api/auth/login-cli to validate the token and retrieve user information.
3181
+ * Calls POST /api/skill-auth/login-cli to validate the token and retrieve user information.
2712
3182
  * This is the preferred method for CLI authentication.
2713
3183
  *
2714
3184
  * @returns User information if authentication succeeds
2715
3185
  * @throws RegistryError if authentication fails
2716
3186
  */ async loginCli() {
2717
- const url = `${this.config.registry}/api/auth/login-cli`;
3187
+ const url = `${this.getApiBase()}/skill-auth/login-cli`;
2718
3188
  const response = await fetch(url, {
2719
3189
  method: 'POST',
2720
3190
  headers: this.getAuthHeaders()
@@ -2746,7 +3216,7 @@ class RegistryClient {
2746
3216
  if (external_node_fs_.existsSync(filePath)) {
2747
3217
  const content = external_node_fs_.readFileSync(filePath);
2748
3218
  const stat = external_node_fs_.statSync(filePath);
2749
- // 如果提供了 shortName,则在路径前添加顶层目录
3219
+ // Prepend shortName as top-level directory if provided
2750
3220
  const entryName = shortName ? `${shortName}/${file}` : file;
2751
3221
  tarPack.entry({
2752
3222
  name: entryName,
@@ -2760,32 +3230,67 @@ class RegistryClient {
2760
3230
  });
2761
3231
  }
2762
3232
  // ============================================================================
2763
- // Skill Info Methods (页面发布适配)
3233
+ // Skill Info Methods (web-published skill support)
2764
3234
  // ============================================================================
2765
3235
  /**
2766
- * 获取 skill 基本信息(包含 source_type
2767
- * 用于 install 命令判断安装逻辑分支
3236
+ * Get basic skill info (including source_type).
3237
+ * Used by the install command to determine the installation logic branch.
2768
3238
  *
2769
- * @param skillName - 完整名称,如 @kanyun/my-skill
2770
- * @returns Skill 基本信息
2771
- * @throws RegistryError 如果 skill 不存在或请求失败
3239
+ * @param skillName - Full skill name, e.g., @kanyun/my-skill
3240
+ * @returns Basic skill information
3241
+ * @throws RegistryError if skill not found or request failed
2772
3242
  */ async getSkillInfo(skillName) {
2773
- const url = `${this.config.registry}/api/skills/${encodeURIComponent(skillName)}`;
3243
+ const url = `${this.getApiBase()}/skills/${encodeURIComponent(skillName)}`;
2774
3244
  const response = await fetch(url, {
2775
3245
  method: 'GET',
2776
3246
  headers: this.getAuthHeaders()
2777
3247
  });
2778
3248
  if (!response.ok) {
2779
3249
  const data = await response.json();
2780
- // 404 时给出明确的 skill 不存在错误
3250
+ // Return a clear "not found" error for 404 responses
2781
3251
  if (404 === response.status) throw new RegistryError(`Skill not found: ${skillName}`, response.status, data);
2782
3252
  throw new RegistryError(data.error || `Failed to get skill info: ${response.statusText}`, response.status, data);
2783
3253
  }
2784
- // API 返回格式: { success: true, data: { ... } }
3254
+ // API response format: { success: true, data: { ... } }
2785
3255
  const responseData = await response.json();
2786
3256
  return responseData.data || responseData;
2787
3257
  }
2788
3258
  // ============================================================================
3259
+ // Search Methods
3260
+ // ============================================================================
3261
+ /**
3262
+ * Search for skills in the registry
3263
+ *
3264
+ * @param query - Search query string
3265
+ * @param options - Search options (limit, offset)
3266
+ * @returns Array of matching skills
3267
+ * @throws RegistryError if the request fails
3268
+ *
3269
+ * @example
3270
+ * const results = await client.search('typescript');
3271
+ * const results = await client.search('planning', { limit: 5 });
3272
+ */ async search(query, options = {}) {
3273
+ const params = new URLSearchParams({
3274
+ q: query
3275
+ });
3276
+ if (void 0 !== options.limit) params.set('limit', String(options.limit));
3277
+ if (void 0 !== options.offset) params.set('offset', String(options.offset));
3278
+ const url = `${this.getApiBase()}/skills?${params.toString()}`;
3279
+ const response = await fetch(url, {
3280
+ method: 'GET',
3281
+ headers: this.getAuthHeaders()
3282
+ });
3283
+ if (!response.ok) {
3284
+ const data = await response.json();
3285
+ throw new RegistryError(data.error || `Search failed: ${response.status}`, response.status, data);
3286
+ }
3287
+ const data = await response.json();
3288
+ return {
3289
+ items: data.data || [],
3290
+ total: data.meta?.pagination?.totalItems ?? data.data?.length ?? 0
3291
+ };
3292
+ }
3293
+ // ============================================================================
2789
3294
  // Download Methods (Step 3.3)
2790
3295
  // ============================================================================
2791
3296
  /**
@@ -2798,13 +3303,13 @@ class RegistryClient {
2798
3303
  *
2799
3304
  * @example
2800
3305
  * await client.resolveVersion('@kanyun/test-skill', 'latest') // '2.4.5'
2801
- * await client.resolveVersion('@kanyun/test-skill', '2.4.5') // '2.4.5' (直接返回)
3306
+ * await client.resolveVersion('@kanyun/test-skill', '2.4.5') // '2.4.5' (returned as-is)
2802
3307
  */ async resolveVersion(skillName, tagOrVersion) {
2803
3308
  const version = tagOrVersion || 'latest';
2804
- // 如果是 semver 版本号,直接返回
3309
+ // If it's already a semver version number, return as-is
2805
3310
  if (/^\d+\.\d+\.\d+/.test(version)) return version;
2806
- // 否则视为 tag,需要查询 dist-tags
2807
- const url = `${this.config.registry}/api/skills/${encodeURIComponent(skillName)}`;
3311
+ // Otherwise treat it as a tag and query dist-tags
3312
+ const url = `${this.getApiBase()}/skills/${encodeURIComponent(skillName)}`;
2808
3313
  const response = await fetch(url, {
2809
3314
  method: 'GET',
2810
3315
  headers: this.getAuthHeaders()
@@ -2813,14 +3318,14 @@ class RegistryClient {
2813
3318
  const data = await response.json();
2814
3319
  throw new RegistryError(data.error || `Failed to fetch skill metadata: ${response.status}`, response.status, data);
2815
3320
  }
2816
- // API 返回格式: { success: true, data: { dist_tags: [{ tag, version }] } }
3321
+ // API response format: { success: true, data: { dist_tags: [{ tag, version }] } }
2817
3322
  const responseData = await response.json();
2818
- // 优先使用 npm 风格的 dist-tags(如果存在)
3323
+ // Prefer npm-style dist-tags if present
2819
3324
  if (responseData['dist-tags']) {
2820
3325
  const resolvedVersion = responseData['dist-tags'][version];
2821
3326
  if (resolvedVersion) return resolvedVersion;
2822
3327
  }
2823
- // 使用 reskill-app dist_tags 数组格式
3328
+ // Fall back to reskill-app's dist_tags array format
2824
3329
  const distTags = responseData.data?.dist_tags;
2825
3330
  if (distTags && Array.isArray(distTags)) {
2826
3331
  const tagEntry = distTags.find((t)=>t.tag === version);
@@ -2839,15 +3344,34 @@ class RegistryClient {
2839
3344
  * @example
2840
3345
  * const { tarball, integrity } = await client.downloadSkill('@kanyun/test-skill', '1.0.0');
2841
3346
  */ async downloadSkill(skillName, version) {
2842
- const url = `${this.config.registry}/api/skills/${encodeURIComponent(skillName)}/versions/${version}/download`;
3347
+ const url = `${this.getApiBase()}/skills/${encodeURIComponent(skillName)}/versions/${version}/download`;
3348
+ // Use redirect: 'manual' to capture x-integrity header from 302 responses.
3349
+ // The registry returns a 302 redirect to OSS with the integrity header,
3350
+ // which would be lost if fetch auto-follows the redirect.
2843
3351
  const response = await fetch(url, {
2844
3352
  method: 'GET',
2845
- headers: this.getAuthHeaders()
3353
+ headers: this.getAuthHeaders(),
3354
+ redirect: 'manual'
2846
3355
  });
3356
+ // Handle 302 redirect (registry → OSS signed URL)
3357
+ if (301 === response.status || 302 === response.status) {
3358
+ const integrity = response.headers.get('x-integrity') || '';
3359
+ const location = response.headers.get('location');
3360
+ if (!location) throw new RegistryError('Missing redirect location in download response', response.status);
3361
+ const downloadResponse = await fetch(location);
3362
+ if (!downloadResponse.ok) throw new RegistryError(`Download from storage failed: ${downloadResponse.status}`, downloadResponse.status);
3363
+ const arrayBuffer = await downloadResponse.arrayBuffer();
3364
+ const tarball = Buffer.from(arrayBuffer);
3365
+ return {
3366
+ tarball,
3367
+ integrity
3368
+ };
3369
+ }
2847
3370
  if (!response.ok) {
2848
3371
  const data = await response.json();
2849
3372
  throw new RegistryError(data.error || `Download failed: ${response.status}`, response.status, data);
2850
3373
  }
3374
+ // Direct response (no redirect) - read tarball and integrity directly
2851
3375
  const arrayBuffer = await response.arrayBuffer();
2852
3376
  const tarball = Buffer.from(arrayBuffer);
2853
3377
  const integrity = response.headers.get('x-integrity') || '';
@@ -2882,11 +3406,11 @@ class RegistryClient {
2882
3406
  * @example
2883
3407
  * RegistryClient.verifyIntegrity(buffer, 'sha256-abc123...') // true or false
2884
3408
  */ static verifyIntegrity(content, expectedIntegrity) {
2885
- // 解析 integrity 格式: algorithm-hash
3409
+ // Parse integrity format: algorithm-hash
2886
3410
  const match = expectedIntegrity.match(/^(\w+)-(.+)$/);
2887
3411
  if (!match) throw new Error(`Invalid integrity format: ${expectedIntegrity}`);
2888
3412
  const [, algorithm, expectedHash] = match;
2889
- // 只支持 sha256 sha512
3413
+ // Only sha256 and sha512 are supported
2890
3414
  if ('sha256' !== algorithm && 'sha512' !== algorithm) throw new Error(`Unsupported integrity algorithm: ${algorithm}`);
2891
3415
  const actualHash = __WEBPACK_EXTERNAL_MODULE_node_crypto__.createHash(algorithm).update(content).digest('base64');
2892
3416
  return actualHash === expectedHash;
@@ -2897,8 +3421,8 @@ class RegistryClient {
2897
3421
  /**
2898
3422
  * Publish a skill to the registry
2899
3423
  */ async publish(skillName, payload, skillPath, options = {}) {
2900
- const url = `${this.config.registry}/api/skills/publish`;
2901
- // 提取短名称作为 tarball 顶层目录(不含 scope 前缀)
3424
+ const url = `${this.getApiBase()}/skills/publish`;
3425
+ // Extract short name as tarball top-level directory (without scope prefix)
2902
3426
  const shortName = getShortName(skillName);
2903
3427
  // Create tarball with short name as top-level directory
2904
3428
  const tarball = await this.createTarball(skillPath, payload.files, shortName);
@@ -3137,23 +3661,23 @@ class RegistryResolver {
3137
3661
  * - HTTP/OSS: https://example.com/skill.tar.gz
3138
3662
  * - Registry shorthand: github:user/repo, gitlab:org/repo
3139
3663
  */ static isRegistryRef(ref) {
3140
- // 排除 Git SSH 格式 (git@...)
3664
+ // Exclude Git SSH format (git@...)
3141
3665
  if (ref.startsWith('git@') || ref.startsWith('git://')) return false;
3142
- // 排除 .git 结尾的 URL
3666
+ // Exclude URLs ending with .git
3143
3667
  if (ref.includes('.git')) return false;
3144
- // 排除 HTTP/HTTPS/OSS URL
3668
+ // Exclude HTTP/HTTPS/OSS URLs
3145
3669
  if (ref.startsWith('http://') || ref.startsWith('https://') || ref.startsWith('oss://') || ref.startsWith('s3://')) return false;
3146
- // 排除 registry shorthand 格式 (github:, gitlab:, custom.com:)
3147
- // 这类格式是 "registry:owner/repo" 而不是 "@scope/name"
3670
+ // Exclude registry shorthand format (github:, gitlab:, custom.com:)
3671
+ // These follow "registry:owner/repo" pattern, not "@scope/name"
3148
3672
  if (/^[a-zA-Z0-9.-]+:[^@]/.test(ref)) return false;
3149
- // 检查是否是 @scope/name 格式(私有 registry
3673
+ // Check for @scope/name format (private registry)
3150
3674
  if (ref.startsWith('@') && ref.includes('/')) {
3151
- // @scope/name @scope/name@version
3675
+ // @scope/name or @scope/name@version
3152
3676
  const scopeNamePattern = /^@[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
3153
3677
  return scopeNamePattern.test(ref);
3154
3678
  }
3155
- // 检查是否是简单的 name name@version 格式(公共 registry
3156
- // 简单名称只包含字母、数字、连字符、下划线和点
3679
+ // Check for simple name or name@version format (public registry)
3680
+ // Simple names contain only letters, digits, hyphens, underscores, and dots
3157
3681
  const namePattern = /^[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
3158
3682
  return namePattern.test(ref);
3159
3683
  }
@@ -3161,26 +3685,27 @@ class RegistryResolver {
3161
3685
  * Resolve a registry skill reference
3162
3686
  *
3163
3687
  * @param ref - Skill reference (e.g., "@kanyun/planning-with-files@2.4.5" or "my-skill@latest")
3688
+ * @param overrideRegistryUrl - Optional registry URL override (bypasses scope-based lookup)
3164
3689
  * @returns Resolved skill information including downloaded tarball
3165
3690
  *
3166
3691
  * @example
3167
3692
  * const result = await resolver.resolve('@kanyun/planning-with-files@2.4.5');
3168
3693
  * console.log(result.shortName); // 'planning-with-files'
3169
3694
  * console.log(result.version); // '2.4.5'
3170
- */ async resolve(ref) {
3171
- // 1. 解析 skill 标识
3695
+ */ async resolve(ref, overrideRegistryUrl) {
3696
+ // 1. Parse skill identifier
3172
3697
  const parsed = parseSkillIdentifier(ref);
3173
3698
  const shortName = getShortName(parsed.fullName);
3174
- // 2. 获取 registry URL
3175
- const registryUrl = getRegistryUrl(parsed.scope);
3176
- // 3. 创建 client 并解析版本
3699
+ // 2. Get registry URL (CLI override takes precedence)
3700
+ const registryUrl = overrideRegistryUrl || getRegistryUrl(parsed.scope);
3701
+ // 3. Create client and resolve version
3177
3702
  const client = new RegistryClient({
3178
3703
  registry: registryUrl
3179
3704
  });
3180
3705
  const version = await client.resolveVersion(parsed.fullName, parsed.version);
3181
- // 4. 下载 tarball
3706
+ // 4. Download tarball
3182
3707
  const { tarball, integrity } = await client.downloadSkill(parsed.fullName, version);
3183
- // 5. 验证 integrity
3708
+ // 5. Verify integrity
3184
3709
  const isValid = RegistryClient.verifyIntegrity(tarball, integrity);
3185
3710
  if (!isValid) throw new Error(`Integrity verification failed for ${ref}`);
3186
3711
  return {
@@ -3197,217 +3722,14 @@ class RegistryResolver {
3197
3722
  *
3198
3723
  * @param tarball - Tarball buffer
3199
3724
  * @param destDir - Destination directory
3200
- * @returns Path to the extracted skill directory
3201
- */ async extract(tarball, destDir) {
3202
- await extractTarballBuffer(tarball, destDir);
3203
- // 获取顶层目录名(即 skill 名称)
3204
- const topDir = await getTarballTopDir(tarball);
3205
- if (topDir) return `${destDir}/${topDir}`;
3206
- return destDir;
3207
- }
3208
- }
3209
- /**
3210
- * Skill Parser - SKILL.md parser
3211
- *
3212
- * Following agentskills.io specification: https://agentskills.io/specification
3213
- *
3214
- * SKILL.md format requirements:
3215
- * - YAML frontmatter containing name and description (required)
3216
- * - name: max 64 characters, lowercase letters, numbers, hyphens
3217
- * - description: max 1024 characters
3218
- * - Optional fields: license, compatibility, metadata, allowed-tools
3219
- */ /**
3220
- * Skill validation error
3221
- */ class SkillValidationError extends Error {
3222
- field;
3223
- constructor(message, field){
3224
- super(message), this.field = field;
3225
- this.name = 'SkillValidationError';
3226
- }
3227
- }
3228
- /**
3229
- * Simple YAML frontmatter parser
3230
- * Parses --- delimited YAML header
3231
- *
3232
- * Supports:
3233
- * - Basic key: value pairs
3234
- * - Multiline strings (| and >)
3235
- * - Nested objects (one level deep, for metadata field)
3236
- */ function parseFrontmatter(content) {
3237
- const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
3238
- const match = content.match(frontmatterRegex);
3239
- if (!match) return {
3240
- data: {},
3241
- content
3242
- };
3243
- const yamlContent = match[1];
3244
- const markdownContent = match[2];
3245
- // Simple YAML parsing (supports basic key: value format and one level of nesting)
3246
- const data = {};
3247
- const lines = yamlContent.split('\n');
3248
- let currentKey = '';
3249
- let currentValue = '';
3250
- let inMultiline = false;
3251
- let inNestedObject = false;
3252
- let nestedObject = {};
3253
- for (const line of lines){
3254
- const trimmedLine = line.trim();
3255
- if (!trimmedLine || trimmedLine.startsWith('#')) continue;
3256
- // Check if it's a nested key: value pair (indented with 2 spaces)
3257
- const nestedMatch = line.match(/^ {2}([a-zA-Z_-]+):\s*(.*)$/);
3258
- if (nestedMatch && inNestedObject) {
3259
- const [, nestedKey, nestedValue] = nestedMatch;
3260
- nestedObject[nestedKey] = parseYamlValue(nestedValue.trim());
3261
- continue;
3262
- }
3263
- // Check if it's a new key: value pair (no indent)
3264
- const keyValueMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
3265
- if (keyValueMatch && !inMultiline) {
3266
- // Save previous nested object if any
3267
- if (inNestedObject && currentKey) {
3268
- data[currentKey] = nestedObject;
3269
- nestedObject = {};
3270
- inNestedObject = false;
3271
- }
3272
- // Save previous value
3273
- if (currentKey && !inNestedObject) data[currentKey] = parseYamlValue(currentValue.trim());
3274
- currentKey = keyValueMatch[1];
3275
- currentValue = keyValueMatch[2];
3276
- // Check if it's start of multiline string
3277
- if ('|' === currentValue || '>' === currentValue) {
3278
- inMultiline = true;
3279
- currentValue = '';
3280
- } else if ('' === currentValue) {
3281
- // Empty value - might be start of nested object
3282
- inNestedObject = true;
3283
- nestedObject = {};
3284
- }
3285
- } else if (inMultiline && line.startsWith(' ')) // Multiline string continuation
3286
- currentValue += (currentValue ? '\n' : '') + line.slice(2);
3287
- else if (inMultiline && !line.startsWith(' ')) {
3288
- // Multiline string end
3289
- inMultiline = false;
3290
- data[currentKey] = currentValue.trim();
3291
- // Try to parse new line
3292
- const newKeyMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
3293
- if (newKeyMatch) {
3294
- currentKey = newKeyMatch[1];
3295
- currentValue = newKeyMatch[2];
3296
- }
3297
- }
3298
- }
3299
- // Save last value
3300
- if (inNestedObject && currentKey) data[currentKey] = nestedObject;
3301
- else if (currentKey) data[currentKey] = parseYamlValue(currentValue.trim());
3302
- return {
3303
- data,
3304
- content: markdownContent
3305
- };
3306
- }
3307
- /**
3308
- * Parse YAML value
3309
- */ function parseYamlValue(value) {
3310
- if (!value) return '';
3311
- // Boolean value
3312
- if ('true' === value) return true;
3313
- if ('false' === value) return false;
3314
- // Number
3315
- if (/^-?\d+$/.test(value)) return parseInt(value, 10);
3316
- if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
3317
- // Remove quotes
3318
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
3319
- return value;
3320
- }
3321
- /**
3322
- * Validate skill name format
3323
- *
3324
- * Specification requirements:
3325
- * - Max 64 characters
3326
- * - Only lowercase letters, numbers, hyphens allowed
3327
- * - Cannot start or end with hyphen
3328
- * - Cannot contain consecutive hyphens
3329
- */ function validateSkillName(name) {
3330
- if (!name) throw new SkillValidationError('Skill name is required', 'name');
3331
- if (name.length > 64) throw new SkillValidationError('Skill name must be at most 64 characters', 'name');
3332
- if (!/^[a-z0-9]/.test(name)) throw new SkillValidationError('Skill name must start with a lowercase letter or number', 'name');
3333
- if (!/[a-z0-9]$/.test(name)) throw new SkillValidationError('Skill name must end with a lowercase letter or number', 'name');
3334
- if (/--/.test(name)) throw new SkillValidationError('Skill name cannot contain consecutive hyphens', 'name');
3335
- 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');
3336
- // Single character name
3337
- if (1 === name.length && !/^[a-z0-9]$/.test(name)) throw new SkillValidationError('Single character skill name must be a lowercase letter or number', 'name');
3338
- }
3339
- /**
3340
- * Validate skill description
3341
- *
3342
- * Specification requirements:
3343
- * - Max 1024 characters
3344
- * - Angle brackets are allowed per agentskills.io spec
3345
- */ function validateSkillDescription(description) {
3346
- if (!description) throw new SkillValidationError('Skill description is required', 'description');
3347
- if (description.length > 1024) throw new SkillValidationError('Skill description must be at most 1024 characters', 'description');
3348
- // Note: angle brackets are allowed per agentskills.io spec
3349
- }
3350
- /**
3351
- * Parse SKILL.md content
3352
- *
3353
- * @param content - SKILL.md file content
3354
- * @param options - Parse options
3355
- * @returns Parsed skill info, or null if format is invalid
3356
- * @throws SkillValidationError if validation fails in strict mode
3357
- */ function parseSkillMd(content, options = {}) {
3358
- const { strict = false } = options;
3359
- try {
3360
- const { data, content: body } = parseFrontmatter(content);
3361
- // Check required fields
3362
- if (!data.name || !data.description) {
3363
- if (strict) throw new SkillValidationError('SKILL.md must have name and description in frontmatter');
3364
- return null;
3365
- }
3366
- const name = String(data.name);
3367
- const description = String(data.description);
3368
- // Validate field format
3369
- if (strict) {
3370
- validateSkillName(name);
3371
- validateSkillDescription(description);
3372
- }
3373
- // Parse allowed-tools
3374
- let allowedTools;
3375
- if (data['allowed-tools']) {
3376
- const toolsStr = String(data['allowed-tools']);
3377
- allowedTools = toolsStr.split(/\s+/).filter(Boolean);
3378
- }
3379
- return {
3380
- name,
3381
- description,
3382
- version: data.version ? String(data.version) : void 0,
3383
- license: data.license ? String(data.license) : void 0,
3384
- compatibility: data.compatibility ? String(data.compatibility) : void 0,
3385
- metadata: data.metadata,
3386
- allowedTools,
3387
- content: body,
3388
- rawContent: content
3389
- };
3390
- } catch (error) {
3391
- if (error instanceof SkillValidationError) throw error;
3392
- if (strict) throw new SkillValidationError(`Failed to parse SKILL.md: ${error}`);
3393
- return null;
3394
- }
3395
- }
3396
- /**
3397
- * Parse SKILL.md from file path
3398
- */ function parseSkillMdFile(filePath, options = {}) {
3399
- if (!external_node_fs_.existsSync(filePath)) {
3400
- if (options.strict) throw new SkillValidationError(`SKILL.md not found: ${filePath}`);
3401
- return null;
3402
- }
3403
- const content = external_node_fs_.readFileSync(filePath, 'utf-8');
3404
- return parseSkillMd(content, options);
3405
- }
3406
- /**
3407
- * Parse SKILL.md from skill directory
3408
- */ function parseSkillFromDir(dirPath, options = {}) {
3409
- const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
3410
- return parseSkillMdFile(skillMdPath, options);
3725
+ * @returns Path to the extracted skill directory
3726
+ */ async extract(tarball, destDir) {
3727
+ await extractTarballBuffer(tarball, destDir);
3728
+ // Get top-level directory name (i.e. skill name)
3729
+ const topDir = await getTarballTopDir(tarball);
3730
+ if (topDir) return `${destDir}/${topDir}`;
3731
+ return destDir;
3732
+ }
3411
3733
  }
3412
3734
  /**
3413
3735
  * SkillManager - Core Skill management class
@@ -3890,12 +4212,112 @@ class RegistryResolver {
3890
4212
  * @param options - Installation options
3891
4213
  */ async installToAgents(ref, targetAgents, options = {}) {
3892
4214
  // Detect source type and delegate to appropriate installer
3893
- // Priority: Registry > HTTP > Git (registry 优先,因为它的格式最受限)
4215
+ // Priority: Registry > HTTP > Git (registry first, as its format is most constrained)
3894
4216
  if (this.isRegistrySource(ref)) return this.installToAgentsFromRegistry(ref, targetAgents, options);
3895
4217
  if (this.isHttpSource(ref)) return this.installToAgentsFromHttp(ref, targetAgents, options);
3896
4218
  return this.installToAgentsFromGit(ref, targetAgents, options);
3897
4219
  }
3898
4220
  /**
4221
+ * Multi-skill install: discover skills in a Git repo and install selected ones (or list only).
4222
+ * Only Git references are supported (including https://github.com/...); registry refs are not.
4223
+ *
4224
+ * @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
4225
+ * @param skillNames - If non-empty, install only these skills (by SKILL.md name). If empty and !listOnly, install all.
4226
+ * @param targetAgents - Target agents
4227
+ * @param options - Install options; listOnly: true means discover and return skills without installing
4228
+ */ async installSkillsFromRepo(ref, skillNames, targetAgents, options = {}) {
4229
+ const { listOnly = false, force = false, save = true, mode = 'symlink' } = options;
4230
+ const refForResolve = ref.replace(/#.*$/, '').trim();
4231
+ const resolved = await this.resolver.resolve(refForResolve);
4232
+ const { parsed, repoUrl } = resolved;
4233
+ const gitRef = resolved.ref;
4234
+ let cacheResult = await this.cache.get(parsed, gitRef);
4235
+ if (!cacheResult) {
4236
+ logger_logger.debug(`Caching from ${repoUrl}@${gitRef}`);
4237
+ cacheResult = await this.cache.cache(repoUrl, parsed, gitRef, gitRef);
4238
+ }
4239
+ const cachePath = this.cache.getCachePath(parsed, gitRef);
4240
+ const discovered = discoverSkillsInDir(cachePath);
4241
+ if (0 === discovered.length) throw new Error('No valid skills found. Skills require a SKILL.md with name and description.');
4242
+ if (listOnly) return {
4243
+ listOnly: true,
4244
+ skills: discovered
4245
+ };
4246
+ const selected = skillNames.length > 0 ? filterSkillsByName(discovered, skillNames) : discovered;
4247
+ if (skillNames.length > 0 && 0 === selected.length) {
4248
+ const available = discovered.map((s)=>s.name).join(', ');
4249
+ throw new Error(`No matching skills found for: ${skillNames.join(', ')}. Available skills: ${available}`);
4250
+ }
4251
+ const baseRefForSave = this.config.normalizeSkillRef(refForResolve);
4252
+ const defaults = this.config.getDefaults();
4253
+ // Only pass custom installDir to Installer; default '.skills' should use
4254
+ // the Installer's built-in canonical path (.agents/skills/)
4255
+ const customInstallDir = '.skills' !== defaults.installDir ? defaults.installDir : void 0;
4256
+ const installer = new Installer({
4257
+ cwd: this.projectRoot,
4258
+ global: this.isGlobal,
4259
+ installDir: customInstallDir
4260
+ });
4261
+ const installed = [];
4262
+ const skipped = [];
4263
+ const skillSource = `${parsed.registry}:${parsed.owner}/${parsed.repo}${parsed.subPath ? `/${parsed.subPath}` : ''}`;
4264
+ for (const skillInfo of selected){
4265
+ const semanticVersion = skillInfo.version ?? gitRef;
4266
+ // Skip already-installed skills unless --force is set
4267
+ if (!force) {
4268
+ const existingSkill = this.getInstalledSkill(skillInfo.name);
4269
+ if (existingSkill) {
4270
+ const locked = this.lockManager.get(skillInfo.name);
4271
+ const lockedRef = locked?.ref || locked?.version;
4272
+ if (lockedRef === gitRef) {
4273
+ const reason = `already installed at ${gitRef}`;
4274
+ logger_logger.info(`${skillInfo.name}@${gitRef} is already installed, skipping`);
4275
+ skipped.push({
4276
+ name: skillInfo.name,
4277
+ reason
4278
+ });
4279
+ continue;
4280
+ }
4281
+ // Different version installed — allow upgrade without --force
4282
+ // Only skip when the exact same ref is already locked
4283
+ }
4284
+ }
4285
+ logger_logger["package"](`Installing ${skillInfo.name}@${gitRef} to ${targetAgents.length} agent(s)...`);
4286
+ // Note: force is handled at the SkillManager level (skip-if-installed check above).
4287
+ // The Installer always overwrites (remove + copy), so no force flag is needed there.
4288
+ const results = await installer.installToAgents(skillInfo.dirPath, skillInfo.name, targetAgents, {
4289
+ mode: mode
4290
+ });
4291
+ if (!this.isGlobal) this.lockManager.lockSkill(skillInfo.name, {
4292
+ source: skillSource,
4293
+ version: semanticVersion,
4294
+ ref: gitRef,
4295
+ resolved: repoUrl,
4296
+ commit: cacheResult.commit
4297
+ });
4298
+ if (!this.isGlobal && save) {
4299
+ this.config.ensureExists();
4300
+ this.config.addSkill(skillInfo.name, `${baseRefForSave}#${skillInfo.name}`);
4301
+ }
4302
+ const successCount = Array.from(results.values()).filter((r)=>r.success).length;
4303
+ logger_logger.success(`Installed ${skillInfo.name}@${semanticVersion} to ${successCount} agent(s)`);
4304
+ installed.push({
4305
+ skill: {
4306
+ name: skillInfo.name,
4307
+ path: skillInfo.dirPath,
4308
+ version: semanticVersion,
4309
+ source: skillSource
4310
+ },
4311
+ results
4312
+ });
4313
+ }
4314
+ return {
4315
+ listOnly: false,
4316
+ installed,
4317
+ skipped
4318
+ };
4319
+ }
4320
+ /**
3899
4321
  * Install skill from Git to multiple agents
3900
4322
  */ async installToAgentsFromGit(ref, targetAgents, options = {}) {
3901
4323
  const { save = true, mode = 'symlink' } = options;
@@ -4036,13 +4458,13 @@ class RegistryResolver {
4036
4458
  * - Web-published skills (github/gitlab/oss_url/custom_url/local)
4037
4459
  */ async installToAgentsFromRegistry(ref, targetAgents, options = {}) {
4038
4460
  const { force = false, save = true, mode = 'symlink' } = options;
4039
- // 解析 skill 标识(获取 fullName version)
4461
+ // Parse skill identifier and resolve registry URL once (single source of truth)
4040
4462
  const parsed = parseSkillIdentifier(ref);
4041
- const registryUrl = getRegistryUrl(parsed.scope);
4463
+ const registryUrl = options.registry || getRegistryUrl(parsed.scope);
4042
4464
  const client = new RegistryClient({
4043
4465
  registry: registryUrl
4044
4466
  });
4045
- // 新增:先查询 skill 信息获取 source_type
4467
+ // Query skill info to determine source_type
4046
4468
  let skillInfo;
4047
4469
  try {
4048
4470
  skillInfo = await client.getSkillInfo(parsed.fullName);
@@ -4053,12 +4475,15 @@ class RegistryResolver {
4053
4475
  };
4054
4476
  else throw error;
4055
4477
  }
4056
- // 新增:根据 source_type 分支
4478
+ // Branch based on source_type (pass resolved registryUrl via options to avoid re-computation)
4057
4479
  const sourceType = skillInfo.source_type;
4058
- if (sourceType && 'registry' !== sourceType) return this.installFromWebPublished(skillInfo, parsed, targetAgents, options);
4059
- // 1. Resolve registry skill(现有流程)
4480
+ if (sourceType && 'registry' !== sourceType) return this.installFromWebPublished(skillInfo, parsed, targetAgents, {
4481
+ ...options,
4482
+ registry: registryUrl
4483
+ });
4484
+ // 1. Resolve registry skill (pass pre-resolved registryUrl)
4060
4485
  logger_logger["package"](`Resolving ${ref} from registry...`);
4061
- const resolved = await this.registryResolver.resolve(ref);
4486
+ const resolved = await this.registryResolver.resolve(ref, registryUrl);
4062
4487
  const { shortName, version, registryUrl: resolvedRegistryUrl, tarball, parsed: resolvedParsed } = resolved;
4063
4488
  // 2. Check if already installed (skip if --force)
4064
4489
  const skillPath = this.getSkillPath(shortName);
@@ -4097,100 +4522,157 @@ class RegistryResolver {
4097
4522
  };
4098
4523
  }
4099
4524
  logger_logger["package"](`Installing ${shortName}@${version} from ${resolvedRegistryUrl} to ${targetAgents.length} agent(s)...`);
4100
- // 3. Create temp directory for extraction
4525
+ // 3. Create temp directory for extraction (clean stale files first)
4101
4526
  const tempDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cache.getCacheDir(), 'registry-temp', `${shortName}-${version}`);
4527
+ await remove(tempDir);
4102
4528
  await ensureDir(tempDir);
4103
- // 4. Extract tarball
4104
- const extractedPath = await this.registryResolver.extract(tarball, tempDir);
4105
- logger_logger.debug(`Extracted to ${extractedPath}`);
4106
- // 5. Create Installer with custom installDir from config
4107
- const defaults = this.config.getDefaults();
4108
- const installer = new Installer({
4109
- cwd: this.projectRoot,
4110
- global: this.isGlobal,
4111
- installDir: defaults.installDir
4112
- });
4113
- // 6. Install to all target agents
4114
- const results = await installer.installToAgents(extractedPath, shortName, targetAgents, {
4115
- mode: mode
4116
- });
4117
- // 7. Update lock file (project mode only)
4118
- if (!this.isGlobal) this.lockManager.lockSkill(shortName, {
4119
- source: `registry:${resolvedParsed.fullName}`,
4120
- version,
4121
- ref: version,
4122
- resolved: resolvedRegistryUrl,
4123
- commit: resolved.integrity
4124
- });
4125
- // 8. Update skills.json (project mode only)
4126
- if (!this.isGlobal && save) {
4127
- this.config.ensureExists();
4128
- // Save with full name for registry skills
4129
- this.config.addSkill(shortName, ref);
4529
+ try {
4530
+ // 4. Extract tarball
4531
+ const extractedPath = await this.registryResolver.extract(tarball, tempDir);
4532
+ logger_logger.debug(`Extracted to ${extractedPath}`);
4533
+ // 5. Create Installer with custom installDir from config
4534
+ const defaults = this.config.getDefaults();
4535
+ const installer = new Installer({
4536
+ cwd: this.projectRoot,
4537
+ global: this.isGlobal,
4538
+ installDir: defaults.installDir
4539
+ });
4540
+ // 6. Install to all target agents
4541
+ const results = await installer.installToAgents(extractedPath, shortName, targetAgents, {
4542
+ mode: mode
4543
+ });
4544
+ // 7. Update lock file (project mode only)
4545
+ if (!this.isGlobal) this.lockManager.lockSkill(shortName, {
4546
+ source: `registry:${resolvedParsed.fullName}`,
4547
+ version,
4548
+ ref: version,
4549
+ resolved: resolvedRegistryUrl,
4550
+ commit: resolved.integrity
4551
+ });
4552
+ // 8. Update skills.json (project mode only)
4553
+ if (!this.isGlobal && save) {
4554
+ this.config.ensureExists();
4555
+ // Save with full name for registry skills
4556
+ this.config.addSkill(shortName, ref);
4557
+ }
4558
+ // 9. Count results and log
4559
+ const successCount = Array.from(results.values()).filter((r)=>r.success).length;
4560
+ const failCount = results.size - successCount;
4561
+ if (0 === failCount) logger_logger.success(`Installed ${shortName}@${version} to ${successCount} agent(s)`);
4562
+ else logger_logger.warn(`Installed ${shortName}@${version} to ${successCount} agent(s), ${failCount} failed`);
4563
+ // 10. Build the InstalledSkill to return
4564
+ const skill = {
4565
+ name: shortName,
4566
+ path: extractedPath,
4567
+ version,
4568
+ source: `registry:${resolvedParsed.fullName}`
4569
+ };
4570
+ return {
4571
+ skill,
4572
+ results
4573
+ };
4574
+ } finally{
4575
+ // Clean up temp directory after installation
4576
+ await remove(tempDir);
4130
4577
  }
4131
- // 9. Count results and log
4132
- const successCount = Array.from(results.values()).filter((r)=>r.success).length;
4133
- const failCount = results.size - successCount;
4134
- if (0 === failCount) logger_logger.success(`Installed ${shortName}@${version} to ${successCount} agent(s)`);
4135
- else logger_logger.warn(`Installed ${shortName}@${version} to ${successCount} agent(s), ${failCount} failed`);
4136
- // 9. Build the InstalledSkill to return
4137
- const skill = {
4138
- name: shortName,
4139
- path: extractedPath,
4140
- version,
4141
- source: `registry:${resolvedParsed.fullName}`
4142
- };
4143
- return {
4144
- skill,
4145
- results
4146
- };
4147
4578
  }
4148
4579
  // ============================================================================
4149
- // Web-published skill installation (页面发布适配)
4580
+ // Web-published skill installation
4150
4581
  // ============================================================================
4151
4582
  /**
4152
- * 安装页面发布的 skill
4583
+ * Install a web-published skill.
4153
4584
  *
4154
- * 页面发布的 skill 不支持版本管理,根据 source_type 分支到不同的安装逻辑:
4155
- * - github/gitlab: 复用 installToAgentsFromGit
4156
- * - oss_url/custom_url: 复用 installToAgentsFromHttp
4157
- * - local: 通过 Registry API 下载 tarball
4585
+ * Web-published skills do not support versioning. Branches to different
4586
+ * installation logic based on source_type:
4587
+ * - github/gitlab: reuses installToAgentsFromGit
4588
+ * - oss_url/custom_url: reuses installToAgentsFromHttp
4589
+ * - local: downloads tarball via Registry API
4158
4590
  */ async installFromWebPublished(skillInfo, parsed, targetAgents, options = {}) {
4159
4591
  const { source_type, source_url } = skillInfo;
4160
- // 页面发布的 skill 不支持版本指定
4592
+ // Web-published skills do not support version specifiers
4161
4593
  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}`);
4162
4594
  if (!source_url) throw new Error(`Missing source_url for web-published skill: ${parsed.fullName}`);
4163
4595
  logger_logger["package"](`Installing ${parsed.fullName} from ${source_type} source...`);
4164
4596
  switch(source_type){
4165
4597
  case 'github':
4166
4598
  case 'gitlab':
4167
- // source_url 是完整的 Git URL(包含 ref path
4168
- // 复用已有的 Git 安装逻辑
4599
+ // source_url is a full Git URL (includes ref and path)
4600
+ // Reuse existing Git installation logic
4169
4601
  return this.installToAgentsFromGit(source_url, targetAgents, options);
4170
4602
  case 'oss_url':
4171
4603
  case 'custom_url':
4172
- // 直接下载 URL
4604
+ // Direct download URL
4173
4605
  return this.installToAgentsFromHttp(source_url, targetAgents, options);
4174
4606
  case 'local':
4175
- // 通过 Registry API 下载 tarball
4176
- return this.installFromRegistryLocal(skillInfo, parsed, targetAgents, options);
4607
+ // Download tarball via Registry API
4608
+ return this.installFromRegistryLocal(parsed, targetAgents, options);
4177
4609
  default:
4178
4610
  throw new Error(`Unknown source_type: ${source_type}`);
4179
4611
  }
4180
4612
  }
4181
4613
  /**
4182
- * 安装 Local Folder 模式发布的 skill
4614
+ * Install a skill published via "local folder" mode.
4183
4615
  *
4184
- * 通过 Registry /api/skills/:name/download API 下载 tarball
4185
- */ async installFromRegistryLocal(_skillInfo, parsed, targetAgents, options = {}) {
4186
- const registryUrl = getRegistryUrl(parsed.scope);
4187
- // 构造下载 URL(通过 Registry API)
4188
- // Ensure trailing slash for proper URL concatenation (defensive coding)
4189
- const baseUrl = registryUrl.endsWith('/') ? registryUrl : `${registryUrl}/`;
4190
- const downloadUrl = `${baseUrl}api/skills/${encodeURIComponent(parsed.fullName)}/download`;
4191
- logger_logger.debug(`Downloading from: ${downloadUrl}`);
4192
- // 复用 HTTP 下载逻辑
4193
- return this.installToAgentsFromHttp(downloadUrl, targetAgents, options);
4616
+ * Downloads tarball via RegistryClient (handles 302 redirects to signed OSS URLs),
4617
+ * then extracts and installs using the same flow as registry source_type.
4618
+ */ async installFromRegistryLocal(parsed, targetAgents, options = {}) {
4619
+ const { save = true, mode = 'symlink' } = options;
4620
+ const registryUrl = options.registry || getRegistryUrl(parsed.scope);
4621
+ const shortName = getShortName(parsed.fullName);
4622
+ const version = 'latest';
4623
+ // Download tarball via RegistryClient (handles auth + 302 redirect to signed URL)
4624
+ const client = new RegistryClient({
4625
+ registry: registryUrl
4626
+ });
4627
+ const { tarball } = await client.downloadSkill(parsed.fullName, version);
4628
+ logger_logger["package"](`Installing ${shortName} from ${registryUrl} to ${targetAgents.length} agent(s)...`);
4629
+ // Extract tarball to temp directory (clean stale files first)
4630
+ const tempDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cache.getCacheDir(), 'registry-temp', `${shortName}-${version}`);
4631
+ await remove(tempDir);
4632
+ await ensureDir(tempDir);
4633
+ try {
4634
+ const extractedPath = await this.registryResolver.extract(tarball, tempDir);
4635
+ logger_logger.debug(`Extracted to ${extractedPath}`);
4636
+ // Install to all target agents
4637
+ const defaults = this.config.getDefaults();
4638
+ const installer = new Installer({
4639
+ cwd: this.projectRoot,
4640
+ global: this.isGlobal,
4641
+ installDir: defaults.installDir
4642
+ });
4643
+ const results = await installer.installToAgents(extractedPath, shortName, targetAgents, {
4644
+ mode: mode
4645
+ });
4646
+ // Get metadata from extracted path
4647
+ const metadata = this.getSkillMetadataFromDir(extractedPath);
4648
+ const skillName = metadata?.name ?? shortName;
4649
+ const semanticVersion = metadata?.version ?? version;
4650
+ // Update lock file (project mode only)
4651
+ if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
4652
+ source: `registry:${parsed.fullName}`,
4653
+ version: semanticVersion,
4654
+ ref: version,
4655
+ resolved: registryUrl,
4656
+ commit: ''
4657
+ });
4658
+ // Update skills.json (project mode only)
4659
+ if (!this.isGlobal && save) {
4660
+ this.config.ensureExists();
4661
+ this.config.addSkill(skillName, parsed.fullName);
4662
+ }
4663
+ return {
4664
+ skill: {
4665
+ name: skillName,
4666
+ path: extractedPath,
4667
+ version: semanticVersion,
4668
+ source: `registry:${parsed.fullName}`
4669
+ },
4670
+ results
4671
+ };
4672
+ } finally{
4673
+ // Clean up temp directory after installation
4674
+ await remove(tempDir);
4675
+ }
4194
4676
  }
4195
4677
  /**
4196
4678
  * Get default target agents
@@ -5173,6 +5655,172 @@ class RegistryResolver {
5173
5655
  if (warnings > 0) logger_logger.warn(`Found ${warnings} warning${1 !== warnings ? 's' : ''}, but reskill should work`);
5174
5656
  else logger_logger.success('All checks passed! reskill is ready to use.');
5175
5657
  });
5658
+ /**
5659
+ * Registry URL resolution utilities
5660
+ *
5661
+ * Shared utility for resolving registry URLs across CLI commands.
5662
+ */ /**
5663
+ * Attempt to resolve registry URL from multiple sources.
5664
+ *
5665
+ * Priority (highest to lowest):
5666
+ * 1. --registry CLI option
5667
+ * 2. RESKILL_REGISTRY environment variable
5668
+ * 3. defaults.publishRegistry in skills.json
5669
+ *
5670
+ * Returns the resolved URL, or null if none found.
5671
+ *
5672
+ * @param cliRegistry - Registry URL from CLI option
5673
+ * @param projectRoot - Project root directory (defaults to cwd)
5674
+ * @returns Resolved registry URL, or null if not configured
5675
+ */ function tryResolveRegistry(cliRegistry, projectRoot = process.cwd()) {
5676
+ // 1. CLI option (highest priority)
5677
+ if (cliRegistry) return cliRegistry;
5678
+ // 2. Environment variable
5679
+ const envRegistry = process.env.RESKILL_REGISTRY;
5680
+ if (envRegistry) return envRegistry;
5681
+ // 3. From skills.json
5682
+ try {
5683
+ const configLoader = new ConfigLoader(projectRoot);
5684
+ if (configLoader.exists()) {
5685
+ const publishRegistry = configLoader.getPublishRegistry();
5686
+ if (publishRegistry) return publishRegistry;
5687
+ }
5688
+ } catch {
5689
+ // Config loading failed, return null
5690
+ }
5691
+ return null;
5692
+ }
5693
+ /**
5694
+ * Resolve registry URL from multiple sources (strict — required for publish)
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
+ * Intentionally has NO default - users must explicitly configure their registry.
5702
+ *
5703
+ * @param cliRegistry - Registry URL from CLI option
5704
+ * @param projectRoot - Project root directory (defaults to cwd)
5705
+ * @returns Resolved registry URL
5706
+ * @throws Exits process with code 1 if no registry is configured
5707
+ */ function resolveRegistry(cliRegistry, projectRoot = process.cwd()) {
5708
+ const resolved = tryResolveRegistry(cliRegistry, projectRoot);
5709
+ if (resolved) return resolved;
5710
+ // No registry configured - error
5711
+ logger_logger.error('No registry specified');
5712
+ logger_logger.newline();
5713
+ logger_logger.log('Please specify a registry using one of these methods:');
5714
+ logger_logger.log(' • --registry <url> option');
5715
+ logger_logger.log(' • RESKILL_REGISTRY environment variable');
5716
+ logger_logger.log(' • "defaults.publishRegistry" in skills.json');
5717
+ process.exit(1);
5718
+ }
5719
+ /**
5720
+ * Resolve registry URL for search, with graceful fallback to public registry.
5721
+ *
5722
+ * Same priority as `resolveRegistry()`, but falls back to the public registry
5723
+ * instead of exiting when no registry is configured.
5724
+ *
5725
+ * @param cliRegistry - Registry URL from CLI option
5726
+ * @param projectRoot - Project root directory (defaults to cwd)
5727
+ * @returns Resolved registry URL (never null)
5728
+ */ function resolveRegistryForSearch(cliRegistry, projectRoot = process.cwd()) {
5729
+ return tryResolveRegistry(cliRegistry, projectRoot) ?? PUBLIC_REGISTRY;
5730
+ }
5731
+ /**
5732
+ * find command - Search for skills in the registry
5733
+ *
5734
+ * Supports both public and private registries via --registry option.
5735
+ * Resolves registry from CLI option > RESKILL_REGISTRY env > skills.json config.
5736
+ *
5737
+ * Usage:
5738
+ * reskill find <query> # Search public registry
5739
+ * reskill find <query> --registry <url> # Search private registry
5740
+ * reskill find <query> --json # Output as JSON
5741
+ * reskill find <query> --limit 5 # Limit results
5742
+ */ // ============================================================================
5743
+ // Display Helpers
5744
+ // ============================================================================
5745
+ /**
5746
+ * Format a single search result for terminal display
5747
+ */ function formatResultItem(item, index) {
5748
+ const lines = [];
5749
+ const name = __WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold.cyan(item.name);
5750
+ const version = item.latest_version ? __WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(`@${item.latest_version}`) : '';
5751
+ lines.push(` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(`${index + 1}.`)} ${name}${version}`);
5752
+ if (item.description) {
5753
+ const desc = item.description.length > 80 ? `${item.description.slice(0, 80)}...` : item.description;
5754
+ lines.push(` ${desc}`);
5755
+ }
5756
+ const meta = [];
5757
+ if (item.keywords && item.keywords.length > 0) meta.push(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(`keywords: ${item.keywords.join(', ')}`));
5758
+ if (item.publisher?.handle) meta.push(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(`by ${item.publisher.handle}`));
5759
+ if (meta.length > 0) lines.push(` ${meta.join(' · ')}`);
5760
+ return lines.join('\n');
5761
+ }
5762
+ /**
5763
+ * Display search results in human-readable format
5764
+ */ function displayResults(items, total, query) {
5765
+ if (0 === items.length) {
5766
+ logger_logger.warn(`No skills found for "${query}"`);
5767
+ return;
5768
+ }
5769
+ logger_logger.newline();
5770
+ logger_logger.log(`Found ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(String(total))} skill${1 === total ? '' : 's'} matching "${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(query)}":`);
5771
+ logger_logger.newline();
5772
+ for(let i = 0; i < items.length; i++){
5773
+ logger_logger.log(formatResultItem(items[i], i));
5774
+ if (i < items.length - 1) logger_logger.newline();
5775
+ }
5776
+ logger_logger.newline();
5777
+ logger_logger.log(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray('Install with: reskill install <name>'));
5778
+ }
5779
+ /**
5780
+ * Display search results as JSON
5781
+ */ function displayJsonResults(items, total) {
5782
+ console.log(JSON.stringify({
5783
+ total,
5784
+ items
5785
+ }, null, 2));
5786
+ }
5787
+ // ============================================================================
5788
+ // Main Action
5789
+ // ============================================================================
5790
+ /**
5791
+ * Execute the find command
5792
+ *
5793
+ * @internal Exported for testing
5794
+ */ async function findAction(query, options) {
5795
+ const limit = Number.parseInt(options.limit || '10', 10);
5796
+ if (Number.isNaN(limit) || limit < 1) {
5797
+ logger_logger.error('Invalid --limit value. Must be a positive integer.');
5798
+ process.exit(1);
5799
+ return;
5800
+ }
5801
+ const registry = resolveRegistryForSearch(options.registry);
5802
+ const client = new RegistryClient({
5803
+ registry
5804
+ });
5805
+ try {
5806
+ const { items, total } = await client.search(query, {
5807
+ limit
5808
+ });
5809
+ if (options.json) displayJsonResults(items, total);
5810
+ else displayResults(items, total, query);
5811
+ } catch (error) {
5812
+ if (error instanceof RegistryError) {
5813
+ logger_logger.error(`Search failed: ${error.message}`);
5814
+ if (401 === error.statusCode || 403 === error.statusCode) logger_logger.log('This registry may require authentication. Try: reskill login');
5815
+ } else logger_logger.error(`Search failed: ${error.message}`);
5816
+ process.exit(1);
5817
+ return;
5818
+ }
5819
+ }
5820
+ // ============================================================================
5821
+ // Command Definition
5822
+ // ============================================================================
5823
+ 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);
5176
5824
  /**
5177
5825
  * info command - Show skill details
5178
5826
  */ 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)=>{
@@ -5513,7 +6161,8 @@ const DEFAULT_INSTALL_DIR = '.skills';
5513
6161
  const { results } = await skillManager.installToAgents(ref, targetAgents, {
5514
6162
  force: options.force,
5515
6163
  save: false,
5516
- mode: installMode
6164
+ mode: installMode,
6165
+ registry: options.registry
5517
6166
  });
5518
6167
  const successCount = Array.from(results.values()).filter((r)=>r.success).length;
5519
6168
  totalInstalled += successCount;
@@ -5562,7 +6211,8 @@ const DEFAULT_INSTALL_DIR = '.skills';
5562
6211
  const { skill: installed, results } = await skillManager.installToAgents(skill, targetAgents, {
5563
6212
  force: options.force,
5564
6213
  save: false !== options.save && !installGlobally,
5565
- mode: installMode
6214
+ mode: installMode,
6215
+ registry: options.registry
5566
6216
  });
5567
6217
  spinner.stop('Installation complete');
5568
6218
  // Process and display results
@@ -5578,6 +6228,77 @@ const DEFAULT_INSTALL_DIR = '.skills';
5578
6228
  });
5579
6229
  }
5580
6230
  }
6231
+ /**
6232
+ * Multi-skill path: list or install selected skills from a single repo (--skill / --list)
6233
+ */ async function installMultiSkillFromRepo(ref, skillNames, listOnly, ctx, targetAgents, installGlobally, installMode, spinner) {
6234
+ const skillManager = new SkillManager(void 0, {
6235
+ global: installGlobally
6236
+ });
6237
+ if (listOnly) {
6238
+ spinner.start('Discovering skills...');
6239
+ const result = await skillManager.installSkillsFromRepo(ref, [], [], {
6240
+ listOnly: true
6241
+ });
6242
+ if (!result.listOnly || 0 === result.skills.length) {
6243
+ spinner.stop('No skills found');
6244
+ __WEBPACK_EXTERNAL_MODULE__clack_prompts__.outro(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim('No skills found.'));
6245
+ return;
6246
+ }
6247
+ spinner.stop(`Found ${result.skills.length} skill(s)`);
6248
+ __WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.message('');
6249
+ __WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.step(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold('Available skills'));
6250
+ for (const s of result.skills){
6251
+ __WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.message(` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].cyan(s.name)}`);
6252
+ __WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.message(` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim(s.description)}`);
6253
+ }
6254
+ __WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.message('');
6255
+ __WEBPACK_EXTERNAL_MODULE__clack_prompts__.outro(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim('Use --skill <name> to install specific skills.'));
6256
+ return;
6257
+ }
6258
+ const summaryLines = [
6259
+ __WEBPACK_EXTERNAL_MODULE_chalk__["default"].cyan(ref),
6260
+ ` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim('→')} ${formatAgentNames(targetAgents)}`,
6261
+ ` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim('Skills:')} ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].cyan(skillNames.join(', '))}`,
6262
+ ` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim('Scope:')} ${installGlobally ? 'Global' : 'Project'}${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim(', Mode:')} ${installMode}`
6263
+ ];
6264
+ __WEBPACK_EXTERNAL_MODULE__clack_prompts__.note(summaryLines.join('\n'), 'Installation Summary');
6265
+ if (!ctx.skipConfirm) {
6266
+ const confirmed = await __WEBPACK_EXTERNAL_MODULE__clack_prompts__.confirm({
6267
+ message: 'Proceed with installation?'
6268
+ });
6269
+ if (__WEBPACK_EXTERNAL_MODULE__clack_prompts__.isCancel(confirmed) || !confirmed) {
6270
+ __WEBPACK_EXTERNAL_MODULE__clack_prompts__.cancel('Installation cancelled');
6271
+ process.exit(0);
6272
+ }
6273
+ }
6274
+ spinner.start('Installing skills...');
6275
+ const result = await skillManager.installSkillsFromRepo(ref, skillNames, targetAgents, {
6276
+ force: ctx.options.force,
6277
+ save: false !== ctx.options.save && !installGlobally,
6278
+ mode: installMode,
6279
+ registry: ctx.options.registry
6280
+ });
6281
+ spinner.stop('Installation complete');
6282
+ // listOnly is always false here (the listOnly path returns early above)
6283
+ if (result.listOnly) return;
6284
+ const { installed, skipped } = result;
6285
+ if (0 === installed.length && skipped.length > 0) {
6286
+ const skipLines = skipped.map((s)=>` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim('–')} ${s.name}: ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].dim(s.reason)}`);
6287
+ __WEBPACK_EXTERNAL_MODULE__clack_prompts__.note(skipLines.join('\n'), __WEBPACK_EXTERNAL_MODULE_chalk__["default"].yellow('All skills were already installed'));
6288
+ __WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.info('Use --force to reinstall.');
6289
+ return;
6290
+ }
6291
+ const resultLines = installed.map((r)=>` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].green('✓')} ${r.skill.name}@${r.skill.version}`);
6292
+ 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)}`);
6293
+ __WEBPACK_EXTERNAL_MODULE__clack_prompts__.note(resultLines.join('\n'), __WEBPACK_EXTERNAL_MODULE_chalk__["default"].green(`Installed ${installed.length} skill(s)`));
6294
+ if (!installGlobally && installed.length > 0 && ctx.configLoader.exists()) {
6295
+ ctx.configLoader.reload();
6296
+ ctx.configLoader.updateDefaults({
6297
+ targetAgents,
6298
+ installMode
6299
+ });
6300
+ }
6301
+ }
5581
6302
  /**
5582
6303
  * Install multiple skills in batch
5583
6304
  */ async function installMultipleSkills(ctx, targetAgents, installGlobally, installMode, spinner) {
@@ -5614,6 +6335,7 @@ const DEFAULT_INSTALL_DIR = '.skills';
5614
6335
  const { skill: installed, results } = await skillManager.installToAgents(skillRef, targetAgents, {
5615
6336
  force: options.force,
5616
6337
  save: false !== options.save && !installGlobally,
6338
+ registry: options.registry,
5617
6339
  mode: installMode
5618
6340
  });
5619
6341
  const successful = Array.from(results.values()).filter((r)=>r.success);
@@ -5651,7 +6373,7 @@ const DEFAULT_INSTALL_DIR = '.skills';
5651
6373
  __WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.error(`${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].red('✗')} ${result.skillRef}`);
5652
6374
  }
5653
6375
  // Display batch results
5654
- console.log();
6376
+ __WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.message('');
5655
6377
  displayBatchInstallResults(successfulSkills, failedSkills, targetAgents.length);
5656
6378
  // Save installation defaults (only for project installs with success)
5657
6379
  if (!installGlobally && successfulSkills.length > 0 && configLoader.exists()) {
@@ -5740,7 +6462,7 @@ const DEFAULT_INSTALL_DIR = '.skills';
5740
6462
  * Behavior:
5741
6463
  * - Single skill install: Prompts for agents/mode (stored config as defaults)
5742
6464
  * - Reinstall all (no args): Uses stored config directly, no confirmation
5743
- */ 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)=>{
6465
+ */ 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)=>{
5744
6466
  // Handle --all flag implications
5745
6467
  if (options.all) {
5746
6468
  options.yes = true;
@@ -5753,19 +6475,34 @@ const DEFAULT_INSTALL_DIR = '.skills';
5753
6475
  __WEBPACK_EXTERNAL_MODULE__clack_prompts__.intro(__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bgCyan.black(' reskill '));
5754
6476
  try {
5755
6477
  const spinner = __WEBPACK_EXTERNAL_MODULE__clack_prompts__.spinner();
5756
- // Step 1: Resolve target agents
5757
- const targetAgents = await resolveTargetAgents(ctx, spinner);
5758
- // Step 2: Resolve installation scope
5759
- const installGlobally = await resolveInstallScope(ctx);
5760
- // Validate: Cannot install all skills globally
5761
- if (ctx.isReinstallAll && installGlobally) {
5762
- __WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.error('Cannot install all skills globally. Please specify a skill to install.');
5763
- process.exit(1);
6478
+ // Multi-skill path (single ref + --skill or --list): list only skips scope/mode/agents
6479
+ const hasMultiSkillFlags = true === ctx.options.list || ctx.options.skill && ctx.options.skill.length > 0;
6480
+ const isMultiSkillPath = !ctx.isReinstallAll && 1 === ctx.skills.length && hasMultiSkillFlags;
6481
+ // Warn if --skill/--list used with multiple refs (flags will be ignored)
6482
+ if (ctx.skills.length > 1 && hasMultiSkillFlags) __WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.warn('--skill and --list are only supported with a single repository reference');
6483
+ let targetAgents;
6484
+ let installGlobally;
6485
+ let installMode;
6486
+ if (isMultiSkillPath && true === ctx.options.list) {
6487
+ targetAgents = [];
6488
+ installGlobally = false;
6489
+ installMode = 'symlink';
6490
+ } else {
6491
+ // Step 1: Resolve target agents
6492
+ targetAgents = await resolveTargetAgents(ctx, spinner);
6493
+ // Step 2: Resolve installation scope
6494
+ installGlobally = await resolveInstallScope(ctx);
6495
+ // Validate: Cannot install all skills globally
6496
+ if (ctx.isReinstallAll && installGlobally) {
6497
+ __WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.error('Cannot install all skills globally. Please specify a skill to install.');
6498
+ process.exit(1);
6499
+ }
6500
+ // Step 3: Resolve installation mode
6501
+ installMode = await resolveInstallMode(ctx);
5764
6502
  }
5765
- // Step 3: Resolve installation mode
5766
- const installMode = await resolveInstallMode(ctx);
5767
6503
  // Step 4: Execute installation
5768
6504
  if (ctx.isReinstallAll) await installAllSkills(ctx, targetAgents, installMode, spinner);
6505
+ else if (isMultiSkillPath) await installMultiSkillFromRepo(ctx.skills[0], ctx.options.skill ?? [], true === ctx.options.list, ctx, targetAgents, installGlobally, installMode, spinner);
5769
6506
  else if (ctx.isBatchInstall) await installMultipleSkills(ctx, targetAgents, installGlobally, installMode, spinner);
5770
6507
  else await installSingleSkill(ctx, targetAgents, installGlobally, installMode, spinner);
5771
6508
  // Done
@@ -5938,45 +6675,6 @@ class AuthManager {
5938
6675
  });
5939
6676
  }
5940
6677
  }
5941
- /**
5942
- * Registry URL resolution utilities
5943
- *
5944
- * Shared utility for resolving registry URLs across CLI commands.
5945
- */ /**
5946
- * Resolve registry URL from multiple sources
5947
- *
5948
- * Priority (highest to lowest):
5949
- * 1. --registry CLI option
5950
- * 2. RESKILL_REGISTRY environment variable
5951
- * 3. defaults.publishRegistry in skills.json
5952
- *
5953
- * Intentionally has NO default - users must explicitly configure their registry.
5954
- *
5955
- * @param cliRegistry - Registry URL from CLI option
5956
- * @param projectRoot - Project root directory (defaults to cwd)
5957
- * @returns Resolved registry URL
5958
- * @throws Exits process with code 1 if no registry is configured
5959
- */ function resolveRegistry(cliRegistry, projectRoot = process.cwd()) {
5960
- // 1. CLI option (highest priority)
5961
- if (cliRegistry) return cliRegistry;
5962
- // 2. Environment variable
5963
- const envRegistry = process.env.RESKILL_REGISTRY;
5964
- if (envRegistry) return envRegistry;
5965
- // 3. From skills.json
5966
- const configLoader = new ConfigLoader(projectRoot);
5967
- if (configLoader.exists()) {
5968
- const publishRegistry = configLoader.getPublishRegistry();
5969
- if (publishRegistry) return publishRegistry;
5970
- }
5971
- // No registry configured - error
5972
- logger_logger.error('No registry specified');
5973
- logger_logger.newline();
5974
- logger_logger.log('Please specify a registry using one of these methods:');
5975
- logger_logger.log(' • --registry <url> option');
5976
- logger_logger.log(' • RESKILL_REGISTRY environment variable');
5977
- logger_logger.log(' • "defaults.publishRegistry" in skills.json');
5978
- process.exit(1);
5979
- }
5980
6678
  /**
5981
6679
  * login command - Authenticate with a reskill registry
5982
6680
  *
@@ -7339,6 +8037,7 @@ const program = new __WEBPACK_EXTERNAL_MODULE_commander__.Command();
7339
8037
  program.name('reskill').description('AI Skills Package Manager - Git-based skills management for AI agents').version(cli_rslib_entry_packageJson.version);
7340
8038
  // Register all commands
7341
8039
  program.addCommand(initCommand);
8040
+ program.addCommand(findCommand);
7342
8041
  program.addCommand(installCommand);
7343
8042
  program.addCommand(listCommand);
7344
8043
  program.addCommand(infoCommand);