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/README.md +47 -47
- package/README.zh-CN.md +47 -47
- package/dist/cli/commands/__integration__/helpers.d.ts +16 -0
- package/dist/cli/commands/__integration__/helpers.d.ts.map +1 -1
- package/dist/cli/commands/find.d.ts +27 -0
- package/dist/cli/commands/find.d.ts.map +1 -0
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/install.d.ts.map +1 -1
- package/dist/cli/index.js +1137 -438
- package/dist/core/git-resolver.d.ts.map +1 -1
- package/dist/core/http-resolver.d.ts +4 -2
- package/dist/core/http-resolver.d.ts.map +1 -1
- package/dist/core/installer.d.ts +25 -0
- package/dist/core/installer.d.ts.map +1 -1
- package/dist/core/registry-client.d.ts +70 -7
- package/dist/core/registry-client.d.ts.map +1 -1
- package/dist/core/registry-resolver.d.ts +2 -1
- package/dist/core/registry-resolver.d.ts.map +1 -1
- package/dist/core/skill-manager.d.ts +35 -7
- package/dist/core/skill-manager.d.ts.map +1 -1
- package/dist/core/skill-parser.d.ts +32 -0
- package/dist/core/skill-parser.d.ts.map +1 -1
- package/dist/index.js +961 -491
- package/dist/types/index.d.ts +16 -14
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/registry-scope.d.ts +13 -13
- package/dist/utils/registry-scope.d.ts.map +1 -1
- package/dist/utils/registry.d.ts +27 -1
- package/dist/utils/registry.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -430,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
|
-
*
|
|
434
|
-
*
|
|
433
|
+
* Public Registry URL
|
|
434
|
+
* Used for installing skills without a scope
|
|
435
435
|
*/ const PUBLIC_REGISTRY = 'https://reskill.info/';
|
|
436
436
|
/**
|
|
437
437
|
* Hardcoded registry to scope mapping
|
|
438
438
|
* TODO: Replace with dynamic fetching from /api/registry/info
|
|
439
439
|
*/ const REGISTRY_SCOPE_MAP = {
|
|
440
|
-
|
|
441
|
-
'https://
|
|
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://
|
|
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://
|
|
472
|
-
* getRegistryForScope('kanyun') // 'https://
|
|
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://
|
|
503
|
-
* getRegistryUrl('kanyun') // 'https://
|
|
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
|
-
*
|
|
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
|
-
* //
|
|
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
|
-
* //
|
|
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
|
-
//
|
|
608
|
+
// Scoped format: @scope/name[@version]
|
|
607
609
|
if (trimmed.startsWith('@')) {
|
|
608
|
-
//
|
|
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
|
-
//
|
|
623
|
-
// name
|
|
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
|
-
|
|
1660
|
+
result = {
|
|
1291
1661
|
success: true,
|
|
1292
1662
|
path: agentDir,
|
|
1293
1663
|
mode: 'copy'
|
|
1294
1664
|
};
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
2358
|
-
if (urlPart.
|
|
2359
|
-
//
|
|
2360
|
-
if (
|
|
2361
|
-
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
*
|
|
2767
|
-
*
|
|
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 -
|
|
2770
|
-
* @returns
|
|
2771
|
-
* @throws RegistryError
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
3309
|
+
// If it's already a semver version number, return as-is
|
|
2805
3310
|
if (/^\d+\.\d+\.\d+/.test(version)) return version;
|
|
2806
|
-
//
|
|
2807
|
-
const url = `${this.
|
|
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
|
|
3321
|
+
// API response format: { success: true, data: { dist_tags: [{ tag, version }] } }
|
|
2817
3322
|
const responseData = await response.json();
|
|
2818
|
-
//
|
|
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
|
-
//
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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.
|
|
2901
|
-
//
|
|
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
|
-
//
|
|
3664
|
+
// Exclude Git SSH format (git@...)
|
|
3141
3665
|
if (ref.startsWith('git@') || ref.startsWith('git://')) return false;
|
|
3142
|
-
//
|
|
3666
|
+
// Exclude URLs ending with .git
|
|
3143
3667
|
if (ref.includes('.git')) return false;
|
|
3144
|
-
//
|
|
3668
|
+
// Exclude HTTP/HTTPS/OSS URLs
|
|
3145
3669
|
if (ref.startsWith('http://') || ref.startsWith('https://') || ref.startsWith('oss://') || ref.startsWith('s3://')) return false;
|
|
3146
|
-
//
|
|
3147
|
-
//
|
|
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
|
-
//
|
|
3673
|
+
// Check for @scope/name format (private registry)
|
|
3150
3674
|
if (ref.startsWith('@') && ref.includes('/')) {
|
|
3151
|
-
// @scope/name
|
|
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
|
-
//
|
|
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.
|
|
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.
|
|
3175
|
-
const registryUrl = getRegistryUrl(parsed.scope);
|
|
3176
|
-
// 3.
|
|
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.
|
|
3706
|
+
// 4. Download tarball
|
|
3182
3707
|
const { tarball, integrity } = await client.downloadSkill(parsed.fullName, version);
|
|
3183
|
-
// 5.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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,
|
|
4059
|
-
|
|
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
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
this.
|
|
4128
|
-
|
|
4129
|
-
|
|
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
|
-
*
|
|
4583
|
+
* Install a web-published skill.
|
|
4153
4584
|
*
|
|
4154
|
-
*
|
|
4155
|
-
*
|
|
4156
|
-
* -
|
|
4157
|
-
* -
|
|
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
|
-
//
|
|
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
|
|
4168
|
-
//
|
|
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
|
-
//
|
|
4604
|
+
// Direct download URL
|
|
4173
4605
|
return this.installToAgentsFromHttp(source_url, targetAgents, options);
|
|
4174
4606
|
case 'local':
|
|
4175
|
-
//
|
|
4176
|
-
return this.installFromRegistryLocal(
|
|
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
|
-
*
|
|
4614
|
+
* Install a skill published via "local folder" mode.
|
|
4183
4615
|
*
|
|
4184
|
-
*
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
const
|
|
4190
|
-
const
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
5757
|
-
const
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
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);
|