reskill 1.20.3 → 1.21.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 +1 -0
- package/README.zh-CN.md +1 -0
- package/dist/cli/index.js +1401 -1118
- package/dist/core/agent-registry.d.ts +6 -2
- package/dist/core/agent-registry.d.ts.map +1 -1
- package/dist/core/claude-3p-installer.d.ts +28 -0
- package/dist/core/claude-3p-installer.d.ts.map +1 -0
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/installer.d.ts.map +1 -1
- package/dist/core/skill-manager.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1412 -1124
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -156,10 +156,616 @@ var external_node_fs_ = __webpack_require__("node:fs");
|
|
|
156
156
|
└────────────────────────────────────────────────────┘
|
|
157
157
|
`;
|
|
158
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Skill Parser - SKILL.md parser
|
|
161
|
+
*
|
|
162
|
+
* Following agentskills.io specification: https://agentskills.io/specification
|
|
163
|
+
*
|
|
164
|
+
* SKILL.md format requirements:
|
|
165
|
+
* - YAML frontmatter containing name and description (required)
|
|
166
|
+
* - name: max 64 characters, lowercase letters, numbers, hyphens
|
|
167
|
+
* - description: max 1024 characters
|
|
168
|
+
* - Optional fields: license, compatibility, metadata, allowed-tools
|
|
169
|
+
*/ /**
|
|
170
|
+
* Skill validation error
|
|
171
|
+
*/ class SkillValidationError extends Error {
|
|
172
|
+
field;
|
|
173
|
+
constructor(message, field){
|
|
174
|
+
super(message), this.field = field;
|
|
175
|
+
this.name = 'SkillValidationError';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Simple YAML frontmatter parser
|
|
180
|
+
* Parses --- delimited YAML header
|
|
181
|
+
*
|
|
182
|
+
* Supports:
|
|
183
|
+
* - Basic key: value pairs
|
|
184
|
+
* - Multiline strings (| and >)
|
|
185
|
+
* - Nested objects (one level deep, for metadata field)
|
|
186
|
+
*/ function parseFrontmatter(content) {
|
|
187
|
+
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
188
|
+
const match = content.match(frontmatterRegex);
|
|
189
|
+
if (!match) return {
|
|
190
|
+
data: {},
|
|
191
|
+
content
|
|
192
|
+
};
|
|
193
|
+
const yamlContent = match[1];
|
|
194
|
+
const markdownContent = match[2];
|
|
195
|
+
// Simple YAML parsing (supports basic key: value, one level of nesting,
|
|
196
|
+
// block scalars (| and >), and plain scalars spanning multiple indented lines)
|
|
197
|
+
const data = {};
|
|
198
|
+
const lines = yamlContent.split('\n');
|
|
199
|
+
let currentKey = '';
|
|
200
|
+
let currentValue = '';
|
|
201
|
+
let inMultiline = false;
|
|
202
|
+
let inNestedObject = false;
|
|
203
|
+
let inPlainScalar = false;
|
|
204
|
+
let nestedObject = {};
|
|
205
|
+
/**
|
|
206
|
+
* Save the current key/value accumulated so far, then reset state.
|
|
207
|
+
*/ function flushCurrent() {
|
|
208
|
+
if (!currentKey) return;
|
|
209
|
+
if (inNestedObject) {
|
|
210
|
+
data[currentKey] = nestedObject;
|
|
211
|
+
nestedObject = {};
|
|
212
|
+
inNestedObject = false;
|
|
213
|
+
} else if (inPlainScalar || inMultiline) {
|
|
214
|
+
data[currentKey] = currentValue.trim();
|
|
215
|
+
inPlainScalar = false;
|
|
216
|
+
inMultiline = false;
|
|
217
|
+
} else data[currentKey] = parseYamlValue(currentValue.trim());
|
|
218
|
+
currentKey = '';
|
|
219
|
+
currentValue = '';
|
|
220
|
+
}
|
|
221
|
+
for (const line of lines){
|
|
222
|
+
const trimmedLine = line.trim();
|
|
223
|
+
if (!trimmedLine || trimmedLine.startsWith('#')) continue;
|
|
224
|
+
const isIndented = line.startsWith(' ');
|
|
225
|
+
// ---- Inside a block scalar (| or >) ----
|
|
226
|
+
if (inMultiline) {
|
|
227
|
+
if (isIndented) {
|
|
228
|
+
currentValue += (currentValue ? '\n' : '') + line.slice(2);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
// Unindented line ends the block scalar — fall through to top-level parsing
|
|
232
|
+
flushCurrent();
|
|
233
|
+
}
|
|
234
|
+
// ---- Inside a plain scalar (multiline value without | or >) ----
|
|
235
|
+
if (inPlainScalar) {
|
|
236
|
+
if (isIndented) {
|
|
237
|
+
// Continuation line: join with a space (YAML plain scalar folding)
|
|
238
|
+
currentValue += ` ${trimmedLine}`;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
// Unindented line ends the plain scalar — fall through to top-level parsing
|
|
242
|
+
flushCurrent();
|
|
243
|
+
}
|
|
244
|
+
// ---- Inside a nested object ----
|
|
245
|
+
if (inNestedObject && isIndented) {
|
|
246
|
+
const nestedMatch = line.match(/^ {2}([a-zA-Z_-]+):\s*(.*)$/);
|
|
247
|
+
if (nestedMatch) {
|
|
248
|
+
const [, nestedKey, nestedValue] = nestedMatch;
|
|
249
|
+
nestedObject[nestedKey] = parseYamlValue(nestedValue.trim());
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
// Indented line that isn't a nested key:value — this key was actually
|
|
253
|
+
// a plain scalar, not a nested object. Switch modes.
|
|
254
|
+
inNestedObject = false;
|
|
255
|
+
inPlainScalar = true;
|
|
256
|
+
currentValue = trimmedLine;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
// ---- Top-level key: value ----
|
|
260
|
+
const keyValueMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
|
|
261
|
+
if (keyValueMatch) {
|
|
262
|
+
flushCurrent();
|
|
263
|
+
currentKey = keyValueMatch[1];
|
|
264
|
+
currentValue = keyValueMatch[2];
|
|
265
|
+
if ('|' === currentValue || '>' === currentValue) {
|
|
266
|
+
inMultiline = true;
|
|
267
|
+
currentValue = '';
|
|
268
|
+
} else if ('' === currentValue) {
|
|
269
|
+
// Empty value — could be nested object or plain scalar; peek at next lines
|
|
270
|
+
inNestedObject = true;
|
|
271
|
+
nestedObject = {};
|
|
272
|
+
}
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
// ---- Unindented line that isn't key:value while in nested object ----
|
|
276
|
+
if (inNestedObject) flushCurrent();
|
|
277
|
+
}
|
|
278
|
+
// Save last accumulated value
|
|
279
|
+
flushCurrent();
|
|
280
|
+
return {
|
|
281
|
+
data,
|
|
282
|
+
content: markdownContent
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Parse YAML value
|
|
287
|
+
*/ function parseYamlValue(value) {
|
|
288
|
+
if (!value) return '';
|
|
289
|
+
// Boolean value
|
|
290
|
+
if ('true' === value) return true;
|
|
291
|
+
if ('false' === value) return false;
|
|
292
|
+
// Number
|
|
293
|
+
if (/^-?\d+$/.test(value)) return parseInt(value, 10);
|
|
294
|
+
if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
|
|
295
|
+
// Remove quotes
|
|
296
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
297
|
+
return value;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Validate skill name format
|
|
301
|
+
*
|
|
302
|
+
* Specification requirements:
|
|
303
|
+
* - Max 64 characters
|
|
304
|
+
* - Only lowercase letters, numbers, hyphens allowed
|
|
305
|
+
* - Cannot start or end with hyphen
|
|
306
|
+
* - Cannot contain consecutive hyphens
|
|
307
|
+
*/ function validateSkillName(name) {
|
|
308
|
+
if (!name) throw new SkillValidationError('Skill name is required', 'name');
|
|
309
|
+
if (name.length > 64) throw new SkillValidationError('Skill name must be at most 64 characters', 'name');
|
|
310
|
+
if (!/^[a-z0-9]/.test(name)) throw new SkillValidationError('Skill name must start with a lowercase letter or number', 'name');
|
|
311
|
+
if (!/[a-z0-9]$/.test(name)) throw new SkillValidationError('Skill name must end with a lowercase letter or number', 'name');
|
|
312
|
+
if (/--/.test(name)) throw new SkillValidationError('Skill name cannot contain consecutive hyphens', 'name');
|
|
313
|
+
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');
|
|
314
|
+
// Single character name
|
|
315
|
+
if (1 === name.length && !/^[a-z0-9]$/.test(name)) throw new SkillValidationError('Single character skill name must be a lowercase letter or number', 'name');
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Validate skill description
|
|
319
|
+
*
|
|
320
|
+
* Specification requirements:
|
|
321
|
+
* - Max 1024 characters
|
|
322
|
+
* - Angle brackets are allowed per agentskills.io spec
|
|
323
|
+
*/ function validateSkillDescription(description) {
|
|
324
|
+
if (!description) throw new SkillValidationError('Skill description is required', 'description');
|
|
325
|
+
if (description.length > 1024) throw new SkillValidationError('Skill description must be at most 1024 characters', 'description');
|
|
326
|
+
// Note: angle brackets are allowed per agentskills.io spec
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Parse SKILL.md content
|
|
330
|
+
*
|
|
331
|
+
* @param content - SKILL.md file content
|
|
332
|
+
* @param options - Parse options
|
|
333
|
+
* @returns Parsed skill info, or null if format is invalid
|
|
334
|
+
* @throws SkillValidationError if validation fails in strict mode
|
|
335
|
+
*/ function parseSkillMd(content, options = {}) {
|
|
336
|
+
const { strict = false } = options;
|
|
337
|
+
try {
|
|
338
|
+
const { data, content: body } = parseFrontmatter(content);
|
|
339
|
+
// Check required fields
|
|
340
|
+
if (!data.name || !data.description) {
|
|
341
|
+
if (strict) throw new SkillValidationError('SKILL.md must have name and description in frontmatter');
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
const name = String(data.name);
|
|
345
|
+
const description = String(data.description);
|
|
346
|
+
// Validate field format
|
|
347
|
+
if (strict) {
|
|
348
|
+
validateSkillName(name);
|
|
349
|
+
validateSkillDescription(description);
|
|
350
|
+
}
|
|
351
|
+
// Parse allowed-tools
|
|
352
|
+
let allowedTools;
|
|
353
|
+
if (data['allowed-tools']) {
|
|
354
|
+
const toolsStr = String(data['allowed-tools']);
|
|
355
|
+
allowedTools = toolsStr.split(/\s+/).filter(Boolean);
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
name,
|
|
359
|
+
description,
|
|
360
|
+
version: data.version ? String(data.version) : void 0,
|
|
361
|
+
license: data.license ? String(data.license) : void 0,
|
|
362
|
+
compatibility: data.compatibility ? String(data.compatibility) : void 0,
|
|
363
|
+
metadata: data.metadata,
|
|
364
|
+
allowedTools,
|
|
365
|
+
content: body,
|
|
366
|
+
rawContent: content
|
|
367
|
+
};
|
|
368
|
+
} catch (error) {
|
|
369
|
+
if (error instanceof SkillValidationError) throw error;
|
|
370
|
+
if (strict) throw new SkillValidationError(`Failed to parse SKILL.md: ${error}`);
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Parse SKILL.md from file path
|
|
376
|
+
*/ function parseSkillMdFile(filePath, options = {}) {
|
|
377
|
+
if (!external_node_fs_.existsSync(filePath)) {
|
|
378
|
+
if (options.strict) throw new SkillValidationError(`SKILL.md not found: ${filePath}`);
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
const content = external_node_fs_.readFileSync(filePath, 'utf-8');
|
|
382
|
+
return parseSkillMd(content, options);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Parse SKILL.md from skill directory
|
|
386
|
+
*/ function parseSkillFromDir(dirPath, options = {}) {
|
|
387
|
+
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
|
|
388
|
+
return parseSkillMdFile(skillMdPath, options);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Check if directory contains valid SKILL.md
|
|
392
|
+
*/ function hasValidSkillMd(dirPath) {
|
|
393
|
+
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
|
|
394
|
+
if (!external_node_fs_.existsSync(skillMdPath)) return false;
|
|
395
|
+
try {
|
|
396
|
+
const skill = parseSkillMdFile(skillMdPath);
|
|
397
|
+
return null !== skill;
|
|
398
|
+
} catch {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const SKIP_DIRS = [
|
|
403
|
+
'node_modules',
|
|
404
|
+
'.git',
|
|
405
|
+
'dist',
|
|
406
|
+
'build',
|
|
407
|
+
'__pycache__'
|
|
408
|
+
];
|
|
409
|
+
const MAX_DISCOVER_DEPTH = 5;
|
|
410
|
+
const PRIORITY_SKILL_DIRS = [
|
|
411
|
+
'skills',
|
|
412
|
+
'.agents/skills',
|
|
413
|
+
'.cursor/skills',
|
|
414
|
+
'.claude/skills',
|
|
415
|
+
'.windsurf/skills',
|
|
416
|
+
'.github/skills'
|
|
417
|
+
];
|
|
418
|
+
function findSkillDirsRecursive(dir, depth, maxDepth, visitedDirs) {
|
|
419
|
+
if (depth > maxDepth) return [];
|
|
420
|
+
const resolvedDir = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir);
|
|
421
|
+
if (visitedDirs.has(resolvedDir)) return [];
|
|
422
|
+
if (!external_node_fs_.existsSync(dir) || !external_node_fs_.statSync(dir).isDirectory()) return [];
|
|
423
|
+
visitedDirs.add(resolvedDir);
|
|
424
|
+
const results = [];
|
|
425
|
+
let entries;
|
|
426
|
+
try {
|
|
427
|
+
entries = external_node_fs_.readdirSync(dir);
|
|
428
|
+
} catch {
|
|
429
|
+
return [];
|
|
430
|
+
}
|
|
431
|
+
for (const entry of entries){
|
|
432
|
+
if (SKIP_DIRS.includes(entry)) continue;
|
|
433
|
+
const fullPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
|
|
434
|
+
const resolvedFull = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(fullPath);
|
|
435
|
+
if (visitedDirs.has(resolvedFull)) continue;
|
|
436
|
+
let stat;
|
|
437
|
+
try {
|
|
438
|
+
stat = external_node_fs_.statSync(fullPath);
|
|
439
|
+
} catch {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (!!stat.isDirectory()) {
|
|
443
|
+
if (hasValidSkillMd(fullPath)) results.push(fullPath);
|
|
444
|
+
results.push(...findSkillDirsRecursive(fullPath, depth + 1, maxDepth, visitedDirs));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return results;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Discover all skills in a directory by scanning for SKILL.md files.
|
|
451
|
+
*
|
|
452
|
+
* Strategy:
|
|
453
|
+
* 1. Check root for SKILL.md
|
|
454
|
+
* 2. Search priority directories (skills/, .agents/skills/, .cursor/skills/, etc.)
|
|
455
|
+
* 3. Fall back to recursive search (max depth 5, skip node_modules, .git, dist, etc.)
|
|
456
|
+
*
|
|
457
|
+
* @param basePath - Root directory to search
|
|
458
|
+
* @returns List of parsed skills with their directory paths (absolute)
|
|
459
|
+
*/ function discoverSkillsInDir(basePath) {
|
|
460
|
+
const resolvedBase = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(basePath);
|
|
461
|
+
const results = [];
|
|
462
|
+
const seenNames = new Set();
|
|
463
|
+
function addSkill(dirPath) {
|
|
464
|
+
const skill = parseSkillFromDir(dirPath);
|
|
465
|
+
if (skill && !seenNames.has(skill.name)) {
|
|
466
|
+
seenNames.add(skill.name);
|
|
467
|
+
results.push({
|
|
468
|
+
...skill,
|
|
469
|
+
dirPath: __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dirPath)
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (hasValidSkillMd(resolvedBase)) addSkill(resolvedBase);
|
|
474
|
+
// Track visited directories to avoid redundant I/O during recursive scan
|
|
475
|
+
const visitedDirs = new Set();
|
|
476
|
+
for (const sub of PRIORITY_SKILL_DIRS){
|
|
477
|
+
const dir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(resolvedBase, sub);
|
|
478
|
+
if (!!external_node_fs_.existsSync(dir) && !!external_node_fs_.statSync(dir).isDirectory()) {
|
|
479
|
+
visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir));
|
|
480
|
+
try {
|
|
481
|
+
const entries = external_node_fs_.readdirSync(dir);
|
|
482
|
+
for (const entry of entries){
|
|
483
|
+
const skillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
|
|
484
|
+
try {
|
|
485
|
+
if (external_node_fs_.statSync(skillDir).isDirectory() && hasValidSkillMd(skillDir)) {
|
|
486
|
+
addSkill(skillDir);
|
|
487
|
+
visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(skillDir));
|
|
488
|
+
}
|
|
489
|
+
} catch {
|
|
490
|
+
// Skip entries that can't be stat'd (race condition, permission, etc.)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
} catch {
|
|
494
|
+
// Skip if unreadable
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const recursiveDirs = findSkillDirsRecursive(resolvedBase, 0, MAX_DISCOVER_DEPTH, visitedDirs);
|
|
499
|
+
for (const skillDir of recursiveDirs)addSkill(skillDir);
|
|
500
|
+
return results;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Filter skills by name (case-insensitive exact match).
|
|
504
|
+
*
|
|
505
|
+
* Note: an empty `names` array returns an empty result (not all skills).
|
|
506
|
+
* Callers should check `names.length` before calling if "no filter = all" is desired.
|
|
507
|
+
*
|
|
508
|
+
* @param skills - List of discovered skills
|
|
509
|
+
* @param names - Skill names to match (e.g. from --skill pdf commit)
|
|
510
|
+
* @returns Skills whose name matches any of the given names
|
|
511
|
+
*/ function filterSkillsByName(skills, names) {
|
|
512
|
+
const normalized = names.map((n)=>n.toLowerCase());
|
|
513
|
+
return skills.filter((skill)=>{
|
|
514
|
+
// Match against SKILL.md name field
|
|
515
|
+
if (normalized.includes(skill.name.toLowerCase())) return true;
|
|
516
|
+
// Also match against the directory name (basename of dirPath)
|
|
517
|
+
// Users naturally refer to skills by their directory name
|
|
518
|
+
const dirName = __WEBPACK_EXTERNAL_MODULE_node_path__.basename(skill.dirPath).toLowerCase();
|
|
519
|
+
return normalized.includes(dirName);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
const claude_3p_installer_CLAUDE_COWORK_3P_AGENT = 'claude-cowork-3p';
|
|
523
|
+
const CLAUDE_3P_SKILLS_ROOT_ENV = 'CLAUDE_3P_SKILLS_ROOT';
|
|
524
|
+
const CLAUDE_3P_SKILLS_PLUGIN_BASE_ENV = 'CLAUDE_3P_SKILLS_PLUGIN_BASE';
|
|
525
|
+
const DEFAULT_EXCLUDE_FILES = [
|
|
526
|
+
'README.md',
|
|
527
|
+
'metadata.json',
|
|
528
|
+
'.reskill-commit'
|
|
529
|
+
];
|
|
530
|
+
const EXCLUDE_PREFIX = '_';
|
|
531
|
+
const MAX_MANIFEST_BACKUPS = 10;
|
|
532
|
+
function exists(targetPath) {
|
|
533
|
+
return external_node_fs_.existsSync(targetPath);
|
|
534
|
+
}
|
|
535
|
+
function ensureDir(dirPath) {
|
|
536
|
+
if (!exists(dirPath)) external_node_fs_.mkdirSync(dirPath, {
|
|
537
|
+
recursive: true
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
function remove(targetPath) {
|
|
541
|
+
if (exists(targetPath)) external_node_fs_.rmSync(targetPath, {
|
|
542
|
+
recursive: true,
|
|
543
|
+
force: true
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
function isSafeSkillId(name) {
|
|
547
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) && '.' !== name && '..' !== name;
|
|
548
|
+
}
|
|
549
|
+
function isPathSafe(basePath, targetPath) {
|
|
550
|
+
const normalizedBase = __WEBPACK_EXTERNAL_MODULE_node_path__.normalize(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(basePath));
|
|
551
|
+
const normalizedTarget = __WEBPACK_EXTERNAL_MODULE_node_path__.normalize(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(targetPath));
|
|
552
|
+
return normalizedTarget.startsWith(normalizedBase + __WEBPACK_EXTERNAL_MODULE_node_path__.sep);
|
|
553
|
+
}
|
|
554
|
+
function getSafeSkillPath(root, skillName) {
|
|
555
|
+
if (!isSafeSkillId(skillName)) throw new Error(`Skill name is not safe for Claude Cowork 3P: ${skillName}`);
|
|
556
|
+
const skillsDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills');
|
|
557
|
+
const skillPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillsDir, skillName);
|
|
558
|
+
if (!isPathSafe(skillsDir, skillPath)) throw new Error(`Skill path escapes Claude Cowork 3P skills directory: ${skillName}`);
|
|
559
|
+
return skillPath;
|
|
560
|
+
}
|
|
561
|
+
function isSkillsRoot(root) {
|
|
562
|
+
return exists(__WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'manifest.json')) && exists(__WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills'));
|
|
563
|
+
}
|
|
564
|
+
function copyDirectory(src, dest) {
|
|
565
|
+
ensureDir(dest);
|
|
566
|
+
for (const entry of external_node_fs_.readdirSync(src, {
|
|
567
|
+
withFileTypes: true
|
|
568
|
+
})){
|
|
569
|
+
if (DEFAULT_EXCLUDE_FILES.includes(entry.name) || entry.name.startsWith(EXCLUDE_PREFIX)) continue;
|
|
570
|
+
const srcPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(src, entry.name);
|
|
571
|
+
const destPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dest, entry.name);
|
|
572
|
+
if (entry.isDirectory()) copyDirectory(srcPath, destPath);
|
|
573
|
+
else if (entry.isFile()) external_node_fs_.copyFileSync(srcPath, destPath);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function getClaude3pSkillsPluginBase(options = {}) {
|
|
577
|
+
const env = options.env ?? process.env;
|
|
578
|
+
const explicitBase = env[CLAUDE_3P_SKILLS_PLUGIN_BASE_ENV];
|
|
579
|
+
if (explicitBase) return explicitBase;
|
|
580
|
+
const homeDir = options.homeDir ?? (0, __WEBPACK_EXTERNAL_MODULE_node_os__.homedir)();
|
|
581
|
+
const currentPlatform = options.platform ?? (0, __WEBPACK_EXTERNAL_MODULE_node_os__.platform)();
|
|
582
|
+
if ('darwin' === currentPlatform) return __WEBPACK_EXTERNAL_MODULE_node_path__.join(homeDir, 'Library', 'Application Support', 'Claude-3p', 'local-agent-mode-sessions', 'skills-plugin');
|
|
583
|
+
if ('win32' === currentPlatform) return __WEBPACK_EXTERNAL_MODULE_node_path__.join(env.APPDATA ?? __WEBPACK_EXTERNAL_MODULE_node_path__.join(homeDir, 'AppData', 'Roaming'), 'Claude-3p', 'local-agent-mode-sessions', 'skills-plugin');
|
|
584
|
+
return __WEBPACK_EXTERNAL_MODULE_node_path__.join(env.XDG_CONFIG_HOME ?? __WEBPACK_EXTERNAL_MODULE_node_path__.join(homeDir, '.config'), 'Claude-3p', 'local-agent-mode-sessions', 'skills-plugin');
|
|
585
|
+
}
|
|
586
|
+
function findClaude3pSkillsRoots(options = {}) {
|
|
587
|
+
const env = options.env ?? process.env;
|
|
588
|
+
const explicitRoot = env[CLAUDE_3P_SKILLS_ROOT_ENV];
|
|
589
|
+
if (explicitRoot) return isSkillsRoot(explicitRoot) ? [
|
|
590
|
+
explicitRoot
|
|
591
|
+
] : [];
|
|
592
|
+
const base = getClaude3pSkillsPluginBase(options);
|
|
593
|
+
if (!exists(base)) return [];
|
|
594
|
+
const roots = [];
|
|
595
|
+
for (const organization of external_node_fs_.readdirSync(base, {
|
|
596
|
+
withFileTypes: true
|
|
597
|
+
})){
|
|
598
|
+
if (!organization.isDirectory()) continue;
|
|
599
|
+
const organizationPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(base, organization.name);
|
|
600
|
+
for (const account of external_node_fs_.readdirSync(organizationPath, {
|
|
601
|
+
withFileTypes: true
|
|
602
|
+
})){
|
|
603
|
+
if (!account.isDirectory()) continue;
|
|
604
|
+
const candidate = __WEBPACK_EXTERNAL_MODULE_node_path__.join(organizationPath, account.name);
|
|
605
|
+
if (isSkillsRoot(candidate)) roots.push(candidate);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return roots;
|
|
609
|
+
}
|
|
610
|
+
function claude_3p_installer_resolveClaude3pSkillsRoot(options = {}) {
|
|
611
|
+
const env = options.env ?? process.env;
|
|
612
|
+
const explicitRoot = env[CLAUDE_3P_SKILLS_ROOT_ENV];
|
|
613
|
+
if (explicitRoot) {
|
|
614
|
+
if (!isSkillsRoot(explicitRoot)) throw new Error(`${CLAUDE_3P_SKILLS_ROOT_ENV} must contain manifest.json and skills/: ${explicitRoot}`);
|
|
615
|
+
return explicitRoot;
|
|
616
|
+
}
|
|
617
|
+
const roots = findClaude3pSkillsRoots(options);
|
|
618
|
+
if (1 === roots.length) return roots[0];
|
|
619
|
+
const base = getClaude3pSkillsPluginBase(options);
|
|
620
|
+
if (0 === roots.length) throw new Error(`Claude Cowork 3P skills root not found under ${base}. Set ${CLAUDE_3P_SKILLS_ROOT_ENV} to the account directory containing manifest.json and skills/.`);
|
|
621
|
+
throw new Error(`Multiple Claude Cowork 3P skills roots found. Set ${CLAUDE_3P_SKILLS_ROOT_ENV} to one of:\n${roots.map((root)=>` ${root}`).join('\n')}`);
|
|
622
|
+
}
|
|
623
|
+
function getClaude3pSkillPath(skillName) {
|
|
624
|
+
const root = claude_3p_installer_resolveClaude3pSkillsRoot();
|
|
625
|
+
return getSafeSkillPath(root, skillName);
|
|
626
|
+
}
|
|
627
|
+
function readManifest(manifestPath) {
|
|
628
|
+
const content = external_node_fs_.readFileSync(manifestPath, 'utf-8');
|
|
629
|
+
return JSON.parse(content);
|
|
630
|
+
}
|
|
631
|
+
function writeManifest(manifestPath, manifest) {
|
|
632
|
+
external_node_fs_.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
|
|
633
|
+
}
|
|
634
|
+
function createManifestBackup(manifestPath) {
|
|
635
|
+
const backupPath = `${manifestPath}.bak.${Date.now()}`;
|
|
636
|
+
external_node_fs_.copyFileSync(manifestPath, backupPath);
|
|
637
|
+
return backupPath;
|
|
638
|
+
}
|
|
639
|
+
function pruneManifestBackups(manifestPath) {
|
|
640
|
+
const manifestDir = __WEBPACK_EXTERNAL_MODULE_node_path__.dirname(manifestPath);
|
|
641
|
+
const backupPrefix = `${__WEBPACK_EXTERNAL_MODULE_node_path__.basename(manifestPath)}.bak.`;
|
|
642
|
+
const backups = external_node_fs_.readdirSync(manifestDir, {
|
|
643
|
+
withFileTypes: true
|
|
644
|
+
}).filter((entry)=>entry.isFile() && entry.name.startsWith(backupPrefix)).map((entry)=>({
|
|
645
|
+
name: entry.name,
|
|
646
|
+
path: __WEBPACK_EXTERNAL_MODULE_node_path__.join(manifestDir, entry.name),
|
|
647
|
+
mtimeMs: external_node_fs_.statSync(__WEBPACK_EXTERNAL_MODULE_node_path__.join(manifestDir, entry.name)).mtimeMs
|
|
648
|
+
})).sort((a, b)=>b.mtimeMs - a.mtimeMs);
|
|
649
|
+
for (const backup of backups.slice(MAX_MANIFEST_BACKUPS))remove(backup.path);
|
|
650
|
+
}
|
|
651
|
+
function updateManifest(manifestPath, skillId, name, description) {
|
|
652
|
+
const manifest = readManifest(manifestPath);
|
|
653
|
+
if (!Array.isArray(manifest.skills)) manifest.skills = [];
|
|
654
|
+
const entry = {
|
|
655
|
+
skillId,
|
|
656
|
+
name,
|
|
657
|
+
description,
|
|
658
|
+
creatorType: 'user',
|
|
659
|
+
syncManaged: false,
|
|
660
|
+
updatedAt: new Date().toISOString(),
|
|
661
|
+
enabled: true
|
|
662
|
+
};
|
|
663
|
+
const index = manifest.skills.findIndex((skill)=>skill.skillId === skillId || skill.name === name);
|
|
664
|
+
if (index >= 0) manifest.skills[index] = {
|
|
665
|
+
...manifest.skills[index],
|
|
666
|
+
...entry
|
|
667
|
+
};
|
|
668
|
+
else manifest.skills.push(entry);
|
|
669
|
+
manifest.lastUpdated = Date.now();
|
|
670
|
+
writeManifest(manifestPath, manifest);
|
|
671
|
+
}
|
|
672
|
+
function removeFromManifest(manifestPath, skillId) {
|
|
673
|
+
const manifest = readManifest(manifestPath);
|
|
674
|
+
if (!Array.isArray(manifest.skills)) return;
|
|
675
|
+
const before = manifest.skills.length;
|
|
676
|
+
manifest.skills = manifest.skills.filter((skill)=>skill.skillId !== skillId && skill.name !== skillId);
|
|
677
|
+
if (manifest.skills.length !== before) {
|
|
678
|
+
manifest.lastUpdated = Date.now();
|
|
679
|
+
writeManifest(manifestPath, manifest);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function installClaude3pSkill(sourcePath, fallbackName, _options = {}) {
|
|
683
|
+
const installMode = 'copy';
|
|
684
|
+
let manifestPath;
|
|
685
|
+
let manifestBackupPath;
|
|
686
|
+
let targetPath;
|
|
687
|
+
let tmpTarget;
|
|
688
|
+
let rollbackTarget;
|
|
689
|
+
try {
|
|
690
|
+
const metadata = parseSkillFromDir(sourcePath);
|
|
691
|
+
const skillId = metadata?.name ?? fallbackName;
|
|
692
|
+
const description = metadata?.description ?? '';
|
|
693
|
+
if (!isSafeSkillId(skillId)) return {
|
|
694
|
+
success: false,
|
|
695
|
+
path: '',
|
|
696
|
+
mode: installMode,
|
|
697
|
+
error: `SKILL.md name is not safe for Claude Cowork 3P: ${skillId}`
|
|
698
|
+
};
|
|
699
|
+
const root = claude_3p_installer_resolveClaude3pSkillsRoot();
|
|
700
|
+
manifestPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'manifest.json');
|
|
701
|
+
const skillsDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills');
|
|
702
|
+
targetPath = getSafeSkillPath(root, skillId);
|
|
703
|
+
tmpTarget = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillsDir, `.${skillId}.installing.${process.pid}`);
|
|
704
|
+
rollbackTarget = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillsDir, `.${skillId}.rollback.${process.pid}`);
|
|
705
|
+
if (!isPathSafe(skillsDir, tmpTarget)) throw new Error(`Temporary skill path escapes Claude Cowork 3P skills directory: ${skillId}`);
|
|
706
|
+
if (!isPathSafe(skillsDir, rollbackTarget)) throw new Error(`Rollback skill path escapes Claude Cowork 3P skills directory: ${skillId}`);
|
|
707
|
+
manifestBackupPath = createManifestBackup(manifestPath);
|
|
708
|
+
remove(tmpTarget);
|
|
709
|
+
remove(rollbackTarget);
|
|
710
|
+
copyDirectory(sourcePath, tmpTarget);
|
|
711
|
+
if (exists(targetPath)) external_node_fs_.renameSync(targetPath, rollbackTarget);
|
|
712
|
+
external_node_fs_.renameSync(tmpTarget, targetPath);
|
|
713
|
+
try {
|
|
714
|
+
updateManifest(manifestPath, skillId, skillId, description);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
remove(targetPath);
|
|
717
|
+
if (rollbackTarget && exists(rollbackTarget)) external_node_fs_.renameSync(rollbackTarget, targetPath);
|
|
718
|
+
if (manifestBackupPath && manifestPath && exists(manifestBackupPath)) external_node_fs_.copyFileSync(manifestBackupPath, manifestPath);
|
|
719
|
+
throw error;
|
|
720
|
+
}
|
|
721
|
+
if (rollbackTarget) remove(rollbackTarget);
|
|
722
|
+
pruneManifestBackups(manifestPath);
|
|
723
|
+
return {
|
|
724
|
+
success: true,
|
|
725
|
+
path: targetPath,
|
|
726
|
+
mode: installMode
|
|
727
|
+
};
|
|
728
|
+
} catch (error) {
|
|
729
|
+
if (tmpTarget) remove(tmpTarget);
|
|
730
|
+
if (targetPath && rollbackTarget && exists(rollbackTarget) && !exists(targetPath)) try {
|
|
731
|
+
external_node_fs_.renameSync(rollbackTarget, targetPath);
|
|
732
|
+
} catch {
|
|
733
|
+
// Ignore rollback errors while reporting the original installation failure.
|
|
734
|
+
}
|
|
735
|
+
if (manifestPath) try {
|
|
736
|
+
pruneManifestBackups(manifestPath);
|
|
737
|
+
} catch {
|
|
738
|
+
// Ignore cleanup errors while reporting the original installation failure.
|
|
739
|
+
}
|
|
740
|
+
return {
|
|
741
|
+
success: false,
|
|
742
|
+
path: '',
|
|
743
|
+
mode: installMode,
|
|
744
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function uninstallClaude3pSkill(skillName) {
|
|
749
|
+
const root = claude_3p_installer_resolveClaude3pSkillsRoot();
|
|
750
|
+
const targetPath = getSafeSkillPath(root, skillName);
|
|
751
|
+
const manifestPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'manifest.json');
|
|
752
|
+
const existed = exists(targetPath);
|
|
753
|
+
if (existed) remove(targetPath);
|
|
754
|
+
removeFromManifest(manifestPath, skillName);
|
|
755
|
+
return existed;
|
|
756
|
+
}
|
|
757
|
+
function listClaude3pSkills() {
|
|
758
|
+
const root = claude_3p_installer_resolveClaude3pSkillsRoot();
|
|
759
|
+
const skillsDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills');
|
|
760
|
+
if (!exists(skillsDir)) return [];
|
|
761
|
+
return external_node_fs_.readdirSync(skillsDir, {
|
|
762
|
+
withFileTypes: true
|
|
763
|
+
}).filter((entry)=>entry.isDirectory() && isSafeSkillId(entry.name)).map((entry)=>entry.name);
|
|
764
|
+
}
|
|
159
765
|
/**
|
|
160
766
|
* Agent Registry - Multi-Agent configuration definitions
|
|
161
767
|
*
|
|
162
|
-
* Supports global and project-level installation for
|
|
768
|
+
* Supports global and project-level installation for 18 coding agents
|
|
163
769
|
* Reference: https://github.com/vercel-labs/add-skill
|
|
164
770
|
*/ const agent_registry_home = (0, __WEBPACK_EXTERNAL_MODULE_node_os__.homedir)();
|
|
165
771
|
/**
|
|
@@ -186,6 +792,20 @@ var external_node_fs_ = __webpack_require__("node:fs");
|
|
|
186
792
|
globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.claude/skills'),
|
|
187
793
|
detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.claude'))
|
|
188
794
|
},
|
|
795
|
+
[claude_3p_installer_CLAUDE_COWORK_3P_AGENT]: {
|
|
796
|
+
name: claude_3p_installer_CLAUDE_COWORK_3P_AGENT,
|
|
797
|
+
displayName: 'Claude Cowork 3P',
|
|
798
|
+
skillsDir: '.claude-3p/skills',
|
|
799
|
+
globalSkillsDir: getClaude3pSkillsPluginBase(),
|
|
800
|
+
detectInstalled: async ()=>{
|
|
801
|
+
try {
|
|
802
|
+
claude_3p_installer_resolveClaude3pSkillsRoot();
|
|
803
|
+
return true;
|
|
804
|
+
} catch {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
},
|
|
189
809
|
clawdbot: {
|
|
190
810
|
name: 'clawdbot',
|
|
191
811
|
displayName: 'Clawdbot',
|
|
@@ -283,1174 +903,811 @@ var external_node_fs_ = __webpack_require__("node:fs");
|
|
|
283
903
|
skillsDir: '.neovate/skills',
|
|
284
904
|
globalSkillsDir: (0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.neovate/skills'),
|
|
285
905
|
detectInstalled: async ()=>(0, external_node_fs_.existsSync)((0, __WEBPACK_EXTERNAL_MODULE_node_path__.join)(agent_registry_home, '.neovate'))
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
/**
|
|
289
|
-
* Detect installed Agents
|
|
290
|
-
*/ async function detectInstalledAgents() {
|
|
291
|
-
const installed = [];
|
|
292
|
-
for (const [type, config] of Object.entries(agents))if (await config.detectInstalled()) installed.push(type);
|
|
293
|
-
return installed;
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* Get Agent configuration
|
|
297
|
-
*/ function getAgentConfig(type) {
|
|
298
|
-
return agents[type];
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Validate if Agent type is valid
|
|
302
|
-
*/ function isValidAgentType(type) {
|
|
303
|
-
return type in agents;
|
|
304
|
-
}
|
|
305
|
-
/**
|
|
306
|
-
* File system utilities
|
|
307
|
-
*/ /**
|
|
308
|
-
* Check if a file or directory exists
|
|
309
|
-
*/ function exists(filePath) {
|
|
310
|
-
return external_node_fs_.existsSync(filePath);
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* Read JSON file
|
|
314
|
-
*/ function readJson(filePath) {
|
|
315
|
-
const content = external_node_fs_.readFileSync(filePath, 'utf-8');
|
|
316
|
-
return JSON.parse(content);
|
|
317
|
-
}
|
|
318
|
-
/**
|
|
319
|
-
* Write JSON file
|
|
320
|
-
*/ function writeJson(filePath, data, indent = 2) {
|
|
321
|
-
const dir = __WEBPACK_EXTERNAL_MODULE_node_path__.dirname(filePath);
|
|
322
|
-
if (!exists(dir)) external_node_fs_.mkdirSync(dir, {
|
|
323
|
-
recursive: true
|
|
324
|
-
});
|
|
325
|
-
external_node_fs_.writeFileSync(filePath, `${JSON.stringify(data, null, indent)}\n`, 'utf-8');
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* Create directory recursively
|
|
329
|
-
*/ function ensureDir(dirPath) {
|
|
330
|
-
if (!exists(dirPath)) external_node_fs_.mkdirSync(dirPath, {
|
|
331
|
-
recursive: true
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Remove file or directory
|
|
336
|
-
*/ function remove(targetPath) {
|
|
337
|
-
if (exists(targetPath)) external_node_fs_.rmSync(targetPath, {
|
|
338
|
-
recursive: true,
|
|
339
|
-
force: true
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
/**
|
|
343
|
-
* Copy directory recursively
|
|
344
|
-
*
|
|
345
|
-
* @param src - Source directory
|
|
346
|
-
* @param dest - Destination directory
|
|
347
|
-
* @param options.exclude - Array of filenames to exclude
|
|
348
|
-
* @param options.excludePrefix - Prefix for files to exclude (e.g., '_' to exclude _private.md)
|
|
349
|
-
*/ function copyDir(src, dest, options) {
|
|
350
|
-
const exclude = options?.exclude || [];
|
|
351
|
-
const excludePrefix = options?.excludePrefix || '_';
|
|
352
|
-
ensureDir(dest);
|
|
353
|
-
const entries = external_node_fs_.readdirSync(src, {
|
|
354
|
-
withFileTypes: true
|
|
355
|
-
});
|
|
356
|
-
for (const entry of entries){
|
|
357
|
-
// Skip files in exclude list or starting with excludePrefix
|
|
358
|
-
if (exclude.includes(entry.name) || entry.name.startsWith(excludePrefix)) continue;
|
|
359
|
-
const srcPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(src, entry.name);
|
|
360
|
-
const destPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dest, entry.name);
|
|
361
|
-
if (entry.isDirectory()) copyDir(srcPath, destPath, options);
|
|
362
|
-
else external_node_fs_.copyFileSync(srcPath, destPath);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* List directory contents
|
|
367
|
-
*/ function listDir(dirPath) {
|
|
368
|
-
if (!exists(dirPath)) return [];
|
|
369
|
-
return external_node_fs_.readdirSync(dirPath);
|
|
370
|
-
}
|
|
371
|
-
/**
|
|
372
|
-
* Check if path is a directory
|
|
373
|
-
*/ function isDirectory(targetPath) {
|
|
374
|
-
if (!exists(targetPath)) return false;
|
|
375
|
-
return external_node_fs_.statSync(targetPath).isDirectory();
|
|
376
|
-
}
|
|
377
|
-
/**
|
|
378
|
-
* Check if path is a symbolic link
|
|
379
|
-
*/ function isSymlink(targetPath) {
|
|
380
|
-
if (!exists(targetPath)) return false;
|
|
381
|
-
return external_node_fs_.lstatSync(targetPath).isSymbolicLink();
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* Get real path of symbolic link
|
|
385
|
-
*/ function getRealPath(linkPath) {
|
|
386
|
-
return external_node_fs_.realpathSync(linkPath);
|
|
387
|
-
}
|
|
388
|
-
/**
|
|
389
|
-
* Get skills.json path for current project
|
|
390
|
-
*/ function getSkillsJsonPath(projectRoot) {
|
|
391
|
-
const root = projectRoot || process.cwd();
|
|
392
|
-
return __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills.json');
|
|
393
|
-
}
|
|
394
|
-
/**
|
|
395
|
-
* Get skills.lock path for current project
|
|
396
|
-
*/ function getSkillsLockPath(projectRoot) {
|
|
397
|
-
const root = projectRoot || process.cwd();
|
|
398
|
-
return __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills.lock');
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* Get global cache directory
|
|
402
|
-
*/ function getCacheDir() {
|
|
403
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
404
|
-
return process.env.RESKILL_CACHE_DIR || __WEBPACK_EXTERNAL_MODULE_node_path__.join(home, '.reskill-cache');
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* Get home directory
|
|
408
|
-
*/ function getHomeDir() {
|
|
409
|
-
return process.env.HOME || process.env.USERPROFILE || '';
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* Shorten path display (replace home directory with ~)
|
|
413
|
-
*/ function shortenPath(fullPath, cwd) {
|
|
414
|
-
const home = getHomeDir();
|
|
415
|
-
const currentDir = cwd || process.cwd();
|
|
416
|
-
if (fullPath.startsWith(home)) return fullPath.replace(home, '~');
|
|
417
|
-
if (fullPath.startsWith(currentDir)) return `.${fullPath.slice(currentDir.length)}`;
|
|
418
|
-
return fullPath;
|
|
419
|
-
}
|
|
420
|
-
const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEBPACK_EXTERNAL_MODULE_node_child_process__.exec);
|
|
421
|
-
/**
|
|
422
|
-
* Git utilities
|
|
423
|
-
*/ /**
|
|
424
|
-
* SSH command with auto-accept for new host keys
|
|
425
|
-
* Uses StrictHostKeyChecking=accept-new which:
|
|
426
|
-
* - Automatically accepts keys for hosts not in known_hosts
|
|
427
|
-
* - Still rejects connections if a known host's key has changed (security)
|
|
428
|
-
*/ const GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes';
|
|
429
|
-
/**
|
|
430
|
-
* Get environment variables for git commands that access remote repositories
|
|
431
|
-
* Configures SSH to auto-accept new host keys and disables interactive prompts
|
|
432
|
-
*/ function getGitEnv() {
|
|
433
|
-
return {
|
|
434
|
-
...process.env,
|
|
435
|
-
GIT_SSH_COMMAND,
|
|
436
|
-
// Disable interactive prompts for HTTPS as well
|
|
437
|
-
GIT_TERMINAL_PROMPT: '0'
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
/**
|
|
441
|
-
* Custom error class for Git clone failures
|
|
442
|
-
* Provides helpful tips for private repository authentication
|
|
443
|
-
*/ class GitCloneError extends Error {
|
|
444
|
-
repoUrl;
|
|
445
|
-
originalError;
|
|
446
|
-
isAuthError;
|
|
447
|
-
urlType;
|
|
448
|
-
constructor(repoUrl, originalError){
|
|
449
|
-
const isAuthError = GitCloneError.isAuthenticationError(originalError.message);
|
|
450
|
-
const urlType = GitCloneError.detectUrlType(repoUrl);
|
|
451
|
-
let message = `Failed to clone repository: ${repoUrl}`;
|
|
452
|
-
if (isAuthError) {
|
|
453
|
-
message += '\n\nTip: For private repos, ensure git credentials are configured:';
|
|
454
|
-
if ('ssh' === urlType) {
|
|
455
|
-
message += '\n - Check ~/.ssh/id_rsa or ~/.ssh/id_ed25519';
|
|
456
|
-
message += '\n - Ensure SSH key is added to your Git hosting service';
|
|
457
|
-
} else {
|
|
458
|
-
// HTTPS or unknown
|
|
459
|
-
message += "\n - Run 'git config --global credential.helper store'";
|
|
460
|
-
message += '\n - Or use a personal access token in the URL';
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
super(message);
|
|
464
|
-
this.name = 'GitCloneError';
|
|
465
|
-
this.repoUrl = repoUrl;
|
|
466
|
-
this.originalError = originalError;
|
|
467
|
-
this.isAuthError = isAuthError;
|
|
468
|
-
this.urlType = urlType;
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* Detect URL type from repository URL
|
|
472
|
-
*/ static detectUrlType(url) {
|
|
473
|
-
if (url.startsWith('git@') || url.startsWith('ssh://')) return 'ssh';
|
|
474
|
-
if (url.startsWith('http://') || url.startsWith('https://')) return 'https';
|
|
475
|
-
return 'unknown';
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* Check if an error message indicates an authentication problem
|
|
479
|
-
*/ static isAuthenticationError(message) {
|
|
480
|
-
const authPatterns = [
|
|
481
|
-
/permission denied/i,
|
|
482
|
-
/could not read from remote/i,
|
|
483
|
-
/authentication failed/i,
|
|
484
|
-
/fatal: repository.*not found/i,
|
|
485
|
-
/host key verification failed/i,
|
|
486
|
-
/access denied/i,
|
|
487
|
-
/unauthorized/i,
|
|
488
|
-
/403/,
|
|
489
|
-
/401/
|
|
490
|
-
];
|
|
491
|
-
return authPatterns.some((pattern)=>pattern.test(message));
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
/**
|
|
495
|
-
* Execute git command asynchronously
|
|
496
|
-
*/ async function git(args, cwd) {
|
|
497
|
-
const { stdout } = await git_execAsync(`git ${args.join(' ')}`, {
|
|
498
|
-
cwd,
|
|
499
|
-
encoding: 'utf-8',
|
|
500
|
-
env: getGitEnv()
|
|
501
|
-
});
|
|
502
|
-
return stdout.trim();
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
/**
|
|
909
|
+
* Detect installed Agents
|
|
910
|
+
*/ async function detectInstalledAgents() {
|
|
911
|
+
const installed = [];
|
|
912
|
+
for (const [type, config] of Object.entries(agents))if (await config.detectInstalled()) installed.push(type);
|
|
913
|
+
return installed;
|
|
503
914
|
}
|
|
504
915
|
/**
|
|
505
|
-
* Get
|
|
506
|
-
*/
|
|
507
|
-
|
|
508
|
-
const output = await git([
|
|
509
|
-
'ls-remote',
|
|
510
|
-
'--tags',
|
|
511
|
-
'--refs',
|
|
512
|
-
repoUrl
|
|
513
|
-
]);
|
|
514
|
-
if (!output) return [];
|
|
515
|
-
const tags = [];
|
|
516
|
-
const lines = output.split('\n');
|
|
517
|
-
for (const line of lines){
|
|
518
|
-
const [commit, ref] = line.split('\t');
|
|
519
|
-
if (commit && ref) {
|
|
520
|
-
// Extract tag name from refs/tags/v1.0.0
|
|
521
|
-
const tagName = ref.replace('refs/tags/', '');
|
|
522
|
-
tags.push({
|
|
523
|
-
name: tagName,
|
|
524
|
-
commit
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
return tags;
|
|
529
|
-
} catch {
|
|
530
|
-
return [];
|
|
531
|
-
}
|
|
916
|
+
* Get Agent configuration
|
|
917
|
+
*/ function getAgentConfig(type) {
|
|
918
|
+
return agents[type];
|
|
532
919
|
}
|
|
533
920
|
/**
|
|
534
|
-
*
|
|
535
|
-
*/
|
|
536
|
-
|
|
537
|
-
if (0 === tags.length) return null;
|
|
538
|
-
// Sort by semver (simple version sort)
|
|
539
|
-
const sortedTags = tags.sort((a, b)=>{
|
|
540
|
-
const aVer = a.name.replace(/^v/, '');
|
|
541
|
-
const bVer = b.name.replace(/^v/, '');
|
|
542
|
-
return compareVersions(bVer, aVer);
|
|
543
|
-
});
|
|
544
|
-
return sortedTags[0];
|
|
921
|
+
* Validate if Agent type is valid
|
|
922
|
+
*/ function isValidAgentType(type) {
|
|
923
|
+
return type in agents;
|
|
545
924
|
}
|
|
546
925
|
/**
|
|
547
|
-
*
|
|
548
|
-
|
|
549
|
-
*
|
|
550
|
-
*/
|
|
551
|
-
|
|
552
|
-
'clone'
|
|
553
|
-
];
|
|
554
|
-
if (options?.depth) args.push('--depth', options.depth.toString());
|
|
555
|
-
if (options?.branch) args.push('--branch', options.branch);
|
|
556
|
-
args.push(repoUrl, destPath);
|
|
557
|
-
try {
|
|
558
|
-
await git(args);
|
|
559
|
-
} catch (error) {
|
|
560
|
-
throw new GitCloneError(repoUrl, error);
|
|
561
|
-
}
|
|
926
|
+
* File system utilities
|
|
927
|
+
*/ /**
|
|
928
|
+
* Check if a file or directory exists
|
|
929
|
+
*/ function fs_exists(filePath) {
|
|
930
|
+
return external_node_fs_.existsSync(filePath);
|
|
562
931
|
}
|
|
563
932
|
/**
|
|
564
|
-
*
|
|
565
|
-
*/
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
'HEAD'
|
|
569
|
-
], cwd);
|
|
933
|
+
* Read JSON file
|
|
934
|
+
*/ function readJson(filePath) {
|
|
935
|
+
const content = external_node_fs_.readFileSync(filePath, 'utf-8');
|
|
936
|
+
return JSON.parse(content);
|
|
570
937
|
}
|
|
571
938
|
/**
|
|
572
|
-
*
|
|
573
|
-
*/
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
'HEAD'
|
|
580
|
-
]);
|
|
581
|
-
const match = output.match(/ref: refs\/heads\/(\S+)/);
|
|
582
|
-
return match ? match[1] : 'main';
|
|
583
|
-
} catch {
|
|
584
|
-
return 'main';
|
|
585
|
-
}
|
|
939
|
+
* Write JSON file
|
|
940
|
+
*/ function writeJson(filePath, data, indent = 2) {
|
|
941
|
+
const dir = __WEBPACK_EXTERNAL_MODULE_node_path__.dirname(filePath);
|
|
942
|
+
if (!fs_exists(dir)) external_node_fs_.mkdirSync(dir, {
|
|
943
|
+
recursive: true
|
|
944
|
+
});
|
|
945
|
+
external_node_fs_.writeFileSync(filePath, `${JSON.stringify(data, null, indent)}\n`, 'utf-8');
|
|
586
946
|
}
|
|
587
947
|
/**
|
|
588
|
-
*
|
|
589
|
-
*/ function
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
for(let i = 0; i < maxLength; i++){
|
|
594
|
-
const aPart = aParts[i] || 0;
|
|
595
|
-
const bPart = bParts[i] || 0;
|
|
596
|
-
if (aPart > bPart) return 1;
|
|
597
|
-
if (aPart < bPart) return -1;
|
|
598
|
-
}
|
|
599
|
-
return 0;
|
|
948
|
+
* Create directory recursively
|
|
949
|
+
*/ function fs_ensureDir(dirPath) {
|
|
950
|
+
if (!fs_exists(dirPath)) external_node_fs_.mkdirSync(dirPath, {
|
|
951
|
+
recursive: true
|
|
952
|
+
});
|
|
600
953
|
}
|
|
601
954
|
/**
|
|
602
|
-
*
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
* - File protocol: file:///path/to/repo (for local testing)
|
|
609
|
-
* - URLs ending with .git
|
|
610
|
-
*/ function isGitUrl(source) {
|
|
611
|
-
return source.startsWith('git@') || source.startsWith('git://') || source.startsWith('http://') || source.startsWith('https://') || source.startsWith('file://') || source.endsWith('.git');
|
|
955
|
+
* Remove file or directory
|
|
956
|
+
*/ function fs_remove(targetPath) {
|
|
957
|
+
if (fs_exists(targetPath)) external_node_fs_.rmSync(targetPath, {
|
|
958
|
+
recursive: true,
|
|
959
|
+
force: true
|
|
960
|
+
});
|
|
612
961
|
}
|
|
613
962
|
/**
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
* Supports:
|
|
617
|
-
* - SSH: git@github.com:user/repo.git
|
|
618
|
-
* - HTTPS: https://github.com/user/repo.git
|
|
619
|
-
* - Git protocol: git://github.com/user/repo.git
|
|
620
|
-
*
|
|
621
|
-
* Note: GitHub/GitLab web URLs (with /tree/, /blob/, etc.) are handled
|
|
622
|
-
* at a higher level in GitResolver.parseGitUrlRef() before calling this function.
|
|
963
|
+
* Copy directory recursively
|
|
623
964
|
*
|
|
624
|
-
* @param
|
|
625
|
-
* @
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
url,
|
|
643
|
-
type: 'ssh'
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
// HTTPS/Git protocol format: https://github.com/user/repo or git://github.com/user/repo
|
|
648
|
-
const httpMatch = cleanUrl.match(/^(https?|git):\/\/([^/]+)\/(.+)$/);
|
|
649
|
-
if (httpMatch) {
|
|
650
|
-
const [, protocol, host, path] = httpMatch;
|
|
651
|
-
const parts = path.split('/');
|
|
652
|
-
if (parts.length >= 2) {
|
|
653
|
-
const owner = parts.slice(0, -1).join('/');
|
|
654
|
-
const repo = parts[parts.length - 1];
|
|
655
|
-
return {
|
|
656
|
-
host,
|
|
657
|
-
owner,
|
|
658
|
-
repo,
|
|
659
|
-
url,
|
|
660
|
-
type: 'git' === protocol ? 'git' : 'https'
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
// File protocol format: file:///path/to/repo
|
|
665
|
-
// Used for local testing and development
|
|
666
|
-
const fileMatch = cleanUrl.match(/^file:\/\/(.+)$/);
|
|
667
|
-
if (fileMatch) {
|
|
668
|
-
const [, filePath] = fileMatch;
|
|
669
|
-
const parts = filePath.split('/').filter(Boolean);
|
|
670
|
-
if (parts.length >= 1) {
|
|
671
|
-
// Use 'local' as host, path components as owner/repo
|
|
672
|
-
const repo = parts[parts.length - 1];
|
|
673
|
-
const owner = parts.length > 1 ? parts[parts.length - 2] : 'local';
|
|
674
|
-
return {
|
|
675
|
-
host: 'local',
|
|
676
|
-
owner,
|
|
677
|
-
repo,
|
|
678
|
-
url,
|
|
679
|
-
type: 'file'
|
|
680
|
-
};
|
|
681
|
-
}
|
|
965
|
+
* @param src - Source directory
|
|
966
|
+
* @param dest - Destination directory
|
|
967
|
+
* @param options.exclude - Array of filenames to exclude
|
|
968
|
+
* @param options.excludePrefix - Prefix for files to exclude (e.g., '_' to exclude _private.md)
|
|
969
|
+
*/ function copyDir(src, dest, options) {
|
|
970
|
+
const exclude = options?.exclude || [];
|
|
971
|
+
const excludePrefix = options?.excludePrefix || '_';
|
|
972
|
+
fs_ensureDir(dest);
|
|
973
|
+
const entries = external_node_fs_.readdirSync(src, {
|
|
974
|
+
withFileTypes: true
|
|
975
|
+
});
|
|
976
|
+
for (const entry of entries){
|
|
977
|
+
// Skip files in exclude list or starting with excludePrefix
|
|
978
|
+
if (exclude.includes(entry.name) || entry.name.startsWith(excludePrefix)) continue;
|
|
979
|
+
const srcPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(src, entry.name);
|
|
980
|
+
const destPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dest, entry.name);
|
|
981
|
+
if (entry.isDirectory()) copyDir(srcPath, destPath, options);
|
|
982
|
+
else external_node_fs_.copyFileSync(srcPath, destPath);
|
|
682
983
|
}
|
|
683
|
-
return null;
|
|
684
984
|
}
|
|
685
985
|
/**
|
|
686
|
-
*
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
* Public Registry URL
|
|
692
|
-
* Used for installing skills without a scope
|
|
693
|
-
*/ const PUBLIC_REGISTRY = 'https://reskill.info/';
|
|
694
|
-
/**
|
|
695
|
-
* Hardcoded registry to scope mapping
|
|
696
|
-
* TODO: Replace with dynamic fetching from /api/registry/info
|
|
697
|
-
*/ const REGISTRY_SCOPE_MAP = {
|
|
698
|
-
// rush-app (private registry, new)
|
|
699
|
-
'https://rush-test.zhenguanyu.com': '@kanyun-test',
|
|
700
|
-
'https://rush.zhenguanyu.com': '@kanyun',
|
|
701
|
-
// reskill-app (private registry, legacy)
|
|
702
|
-
'https://reskill-test.zhenguanyu.com': '@kanyun-test',
|
|
703
|
-
// Local development
|
|
704
|
-
'http://localhost:3000': '@kanyun-test'
|
|
705
|
-
};
|
|
986
|
+
* List directory contents
|
|
987
|
+
*/ function listDir(dirPath) {
|
|
988
|
+
if (!fs_exists(dirPath)) return [];
|
|
989
|
+
return external_node_fs_.readdirSync(dirPath);
|
|
990
|
+
}
|
|
706
991
|
/**
|
|
707
|
-
*
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
*
|
|
712
|
-
* @example
|
|
713
|
-
* getScopeForRegistry('https://rush-test.zhenguanyu.com') // '@kanyun'
|
|
714
|
-
* getScopeForRegistry('https://unknown.com') // null
|
|
715
|
-
*/ function getScopeForRegistry(registry) {
|
|
716
|
-
if (!registry) return null;
|
|
717
|
-
// Try exact match first
|
|
718
|
-
if (REGISTRY_SCOPE_MAP[registry]) return REGISTRY_SCOPE_MAP[registry];
|
|
719
|
-
// Try with/without trailing slash
|
|
720
|
-
const normalized = registry.endsWith('/') ? registry.slice(0, -1) : `${registry}/`;
|
|
721
|
-
return REGISTRY_SCOPE_MAP[normalized] || null;
|
|
992
|
+
* Check if path is a directory
|
|
993
|
+
*/ function isDirectory(targetPath) {
|
|
994
|
+
if (!fs_exists(targetPath)) return false;
|
|
995
|
+
return external_node_fs_.statSync(targetPath).isDirectory();
|
|
722
996
|
}
|
|
723
997
|
/**
|
|
724
|
-
*
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
* @returns Registry URL (with trailing slash) or null if not found
|
|
729
|
-
*
|
|
730
|
-
* @example
|
|
731
|
-
* getRegistryForScope('@kanyun') // 'https://rush-test.zhenguanyu.com/'
|
|
732
|
-
* getRegistryForScope('kanyun') // 'https://rush-test.zhenguanyu.com/'
|
|
733
|
-
* getRegistryForScope('@unknown') // null
|
|
734
|
-
* getRegistryForScope('@mycompany', { '@mycompany': 'https://my.registry.com/' }) // 'https://my.registry.com/'
|
|
735
|
-
*/ function getRegistryForScope(scope, customRegistries) {
|
|
736
|
-
if (!scope) return null;
|
|
737
|
-
// Normalize scope: ensure @ prefix
|
|
738
|
-
const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
|
|
739
|
-
// 1. First check custom scopeRegistries (from skills.json)
|
|
740
|
-
if (customRegistries?.[normalizedScope]) {
|
|
741
|
-
const url = customRegistries[normalizedScope];
|
|
742
|
-
// Normalize trailing slash
|
|
743
|
-
return url.endsWith('/') ? url : `${url}/`;
|
|
744
|
-
}
|
|
745
|
-
// 2. Fall back to hardcoded defaults
|
|
746
|
-
for (const [registry, registryScope] of Object.entries(REGISTRY_SCOPE_MAP))if (registryScope === normalizedScope) // Return URL with trailing slash (normalized format)
|
|
747
|
-
return registry.endsWith('/') ? registry : `${registry}/`;
|
|
748
|
-
return null;
|
|
998
|
+
* Check if path is a symbolic link
|
|
999
|
+
*/ function isSymlink(targetPath) {
|
|
1000
|
+
if (!fs_exists(targetPath)) return false;
|
|
1001
|
+
return external_node_fs_.lstatSync(targetPath).isSymbolicLink();
|
|
749
1002
|
}
|
|
750
1003
|
/**
|
|
751
|
-
* Get
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
* - Without scope (null/undefined/'') → returns public Registry
|
|
755
|
-
*
|
|
756
|
-
* @param scope - Scope string (with or without @ prefix), null, undefined, or empty string
|
|
757
|
-
* @param customRegistries - Optional custom scope-to-registry mapping (from skills.json)
|
|
758
|
-
* @returns Registry URL (with trailing slash)
|
|
759
|
-
* @throws Error if scope is provided but not found in the registry map
|
|
760
|
-
*
|
|
761
|
-
* @example
|
|
762
|
-
* getRegistryUrl('@kanyun') // 'https://rush-test.zhenguanyu.com/'
|
|
763
|
-
* getRegistryUrl('kanyun') // 'https://rush-test.zhenguanyu.com/'
|
|
764
|
-
* getRegistryUrl(null) // 'https://reskill.info/'
|
|
765
|
-
* getRegistryUrl('') // 'https://reskill.info/'
|
|
766
|
-
* getRegistryUrl('@unknown') // throws Error
|
|
767
|
-
* getRegistryUrl('@mycompany', { '@mycompany': 'https://my.registry.com/' }) // 'https://my.registry.com/'
|
|
768
|
-
*/ function getRegistryUrl(scope, customRegistries) {
|
|
769
|
-
// No scope → return public Registry
|
|
770
|
-
if (!scope) return PUBLIC_REGISTRY;
|
|
771
|
-
// With scope → lookup private Registry
|
|
772
|
-
const registry = getRegistryForScope(scope, customRegistries);
|
|
773
|
-
if (!registry) {
|
|
774
|
-
// Normalize scope for error message
|
|
775
|
-
const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
|
|
776
|
-
throw new Error(`Unknown scope ${normalizedScope}. No registry configured for this scope.`);
|
|
777
|
-
}
|
|
778
|
-
return registry;
|
|
1004
|
+
* Get real path of symbolic link
|
|
1005
|
+
*/ function getRealPath(linkPath) {
|
|
1006
|
+
return external_node_fs_.realpathSync(linkPath);
|
|
779
1007
|
}
|
|
780
1008
|
/**
|
|
781
|
-
*
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
*
|
|
786
|
-
* @example
|
|
787
|
-
* parseSkillName('@kanyun/planning-with-files')
|
|
788
|
-
* // { scope: '@kanyun', name: 'planning-with-files', fullName: '@kanyun/planning-with-files' }
|
|
789
|
-
*
|
|
790
|
-
* parseSkillName('planning-with-files')
|
|
791
|
-
* // { scope: null, name: 'planning-with-files', fullName: 'planning-with-files' }
|
|
792
|
-
*/ function parseSkillName(skillName) {
|
|
793
|
-
// Match @scope/name pattern
|
|
794
|
-
const match = skillName.match(/^(@[^/]+)\/(.+)$/);
|
|
795
|
-
if (match) return {
|
|
796
|
-
scope: match[1],
|
|
797
|
-
name: match[2],
|
|
798
|
-
fullName: skillName
|
|
799
|
-
};
|
|
800
|
-
return {
|
|
801
|
-
scope: null,
|
|
802
|
-
name: skillName,
|
|
803
|
-
fullName: skillName
|
|
804
|
-
};
|
|
1009
|
+
* Get skills.json path for current project
|
|
1010
|
+
*/ function getSkillsJsonPath(projectRoot) {
|
|
1011
|
+
const root = projectRoot || process.cwd();
|
|
1012
|
+
return __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills.json');
|
|
805
1013
|
}
|
|
806
1014
|
/**
|
|
807
|
-
*
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
* @returns Full skill name (e.g., "@kanyun/planning-with-files")
|
|
812
|
-
*
|
|
813
|
-
* @example
|
|
814
|
-
* buildFullSkillName('@kanyun', 'planning-with-files') // '@kanyun/planning-with-files'
|
|
815
|
-
* buildFullSkillName('kanyun', 'my-skill') // '@kanyun/my-skill'
|
|
816
|
-
* buildFullSkillName(null, 'my-skill') // 'my-skill'
|
|
817
|
-
*/ function buildFullSkillName(scope, name) {
|
|
818
|
-
if (!scope) return name;
|
|
819
|
-
// Ensure scope starts with @
|
|
820
|
-
const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
|
|
821
|
-
return `${normalizedScope}/${name}`;
|
|
1015
|
+
* Get skills.lock path for current project
|
|
1016
|
+
*/ function getSkillsLockPath(projectRoot) {
|
|
1017
|
+
const root = projectRoot || process.cwd();
|
|
1018
|
+
return __WEBPACK_EXTERNAL_MODULE_node_path__.join(root, 'skills.lock');
|
|
822
1019
|
}
|
|
823
1020
|
/**
|
|
824
|
-
* Get
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
*
|
|
829
|
-
* @example
|
|
830
|
-
* getShortName('@kanyun/planning-with-files') // 'planning-with-files'
|
|
831
|
-
* getShortName('planning-with-files') // 'planning-with-files'
|
|
832
|
-
*/ function getShortName(skillName) {
|
|
833
|
-
return parseSkillName(skillName).name;
|
|
1021
|
+
* Get global cache directory
|
|
1022
|
+
*/ function getCacheDir() {
|
|
1023
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
1024
|
+
return process.env.RESKILL_CACHE_DIR || __WEBPACK_EXTERNAL_MODULE_node_path__.join(home, '.reskill-cache');
|
|
834
1025
|
}
|
|
835
1026
|
/**
|
|
836
|
-
*
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
*
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
*
|
|
852
|
-
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
// Starting with @@ is invalid
|
|
863
|
-
if (trimmed.startsWith('@@')) throw new Error('Invalid skill identifier: invalid scope format');
|
|
864
|
-
// Bare @ is invalid
|
|
865
|
-
if ('@' === trimmed) throw new Error('Invalid skill identifier: missing scope and name');
|
|
866
|
-
// Scoped format: @scope/name[@version]
|
|
867
|
-
if (trimmed.startsWith('@')) {
|
|
868
|
-
// Regex: @scope/name[@version]
|
|
869
|
-
// scope: starts with @, followed by alphanumeric, hyphens, underscores
|
|
870
|
-
// name: alphanumeric, hyphens, underscores
|
|
871
|
-
// version: optional, @ followed by any non-empty string
|
|
872
|
-
const scopedMatch = trimmed.match(/^(@[\w-]+)\/([\w-]+)(?:@(.+))?$/);
|
|
873
|
-
if (!scopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
|
|
874
|
-
const [, scope, name, version] = scopedMatch;
|
|
875
|
-
return {
|
|
876
|
-
scope,
|
|
877
|
-
name,
|
|
878
|
-
version: version || void 0,
|
|
879
|
-
fullName: `${scope}/${name}`
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
// Unscoped format: name[@version] (public registry)
|
|
883
|
-
// name must not contain / (otherwise it might be a git shorthand)
|
|
884
|
-
const unscopedMatch = trimmed.match(/^([\w-]+)(?:@(.+))?$/);
|
|
885
|
-
if (!unscopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
|
|
886
|
-
const [, name, version] = unscopedMatch;
|
|
1027
|
+
* Get home directory
|
|
1028
|
+
*/ function getHomeDir() {
|
|
1029
|
+
return process.env.HOME || process.env.USERPROFILE || '';
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Shorten path display (replace home directory with ~)
|
|
1033
|
+
*/ function shortenPath(fullPath, cwd) {
|
|
1034
|
+
const home = getHomeDir();
|
|
1035
|
+
const currentDir = cwd || process.cwd();
|
|
1036
|
+
if (fullPath.startsWith(home)) return fullPath.replace(home, '~');
|
|
1037
|
+
if (fullPath.startsWith(currentDir)) return `.${fullPath.slice(currentDir.length)}`;
|
|
1038
|
+
return fullPath;
|
|
1039
|
+
}
|
|
1040
|
+
const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEBPACK_EXTERNAL_MODULE_node_child_process__.exec);
|
|
1041
|
+
/**
|
|
1042
|
+
* Git utilities
|
|
1043
|
+
*/ /**
|
|
1044
|
+
* SSH command with auto-accept for new host keys
|
|
1045
|
+
* Uses StrictHostKeyChecking=accept-new which:
|
|
1046
|
+
* - Automatically accepts keys for hosts not in known_hosts
|
|
1047
|
+
* - Still rejects connections if a known host's key has changed (security)
|
|
1048
|
+
*/ const GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes';
|
|
1049
|
+
/**
|
|
1050
|
+
* Get environment variables for git commands that access remote repositories
|
|
1051
|
+
* Configures SSH to auto-accept new host keys and disables interactive prompts
|
|
1052
|
+
*/ function getGitEnv() {
|
|
887
1053
|
return {
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1054
|
+
...process.env,
|
|
1055
|
+
GIT_SSH_COMMAND,
|
|
1056
|
+
// Disable interactive prompts for HTTPS as well
|
|
1057
|
+
GIT_TERMINAL_PROMPT: '0'
|
|
892
1058
|
};
|
|
893
1059
|
}
|
|
894
1060
|
/**
|
|
895
|
-
*
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
constructor(
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1061
|
+
* Custom error class for Git clone failures
|
|
1062
|
+
* Provides helpful tips for private repository authentication
|
|
1063
|
+
*/ class GitCloneError extends Error {
|
|
1064
|
+
repoUrl;
|
|
1065
|
+
originalError;
|
|
1066
|
+
isAuthError;
|
|
1067
|
+
urlType;
|
|
1068
|
+
constructor(repoUrl, originalError){
|
|
1069
|
+
const isAuthError = GitCloneError.isAuthenticationError(originalError.message);
|
|
1070
|
+
const urlType = GitCloneError.detectUrlType(repoUrl);
|
|
1071
|
+
let message = `Failed to clone repository: ${repoUrl}`;
|
|
1072
|
+
if (isAuthError) {
|
|
1073
|
+
message += '\n\nTip: For private repos, ensure git credentials are configured:';
|
|
1074
|
+
if ('ssh' === urlType) {
|
|
1075
|
+
message += '\n - Check ~/.ssh/id_rsa or ~/.ssh/id_ed25519';
|
|
1076
|
+
message += '\n - Ensure SSH key is added to your Git hosting service';
|
|
1077
|
+
} else {
|
|
1078
|
+
// HTTPS or unknown
|
|
1079
|
+
message += "\n - Run 'git config --global credential.helper store'";
|
|
1080
|
+
message += '\n - Or use a personal access token in the URL';
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
super(message);
|
|
1084
|
+
this.name = 'GitCloneError';
|
|
1085
|
+
this.repoUrl = repoUrl;
|
|
1086
|
+
this.originalError = originalError;
|
|
1087
|
+
this.isAuthError = isAuthError;
|
|
1088
|
+
this.urlType = urlType;
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Detect URL type from repository URL
|
|
1092
|
+
*/ static detectUrlType(url) {
|
|
1093
|
+
if (url.startsWith('git@') || url.startsWith('ssh://')) return 'ssh';
|
|
1094
|
+
if (url.startsWith('http://') || url.startsWith('https://')) return 'https';
|
|
1095
|
+
return 'unknown';
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Check if an error message indicates an authentication problem
|
|
1099
|
+
*/ static isAuthenticationError(message) {
|
|
1100
|
+
const authPatterns = [
|
|
1101
|
+
/permission denied/i,
|
|
1102
|
+
/could not read from remote/i,
|
|
1103
|
+
/authentication failed/i,
|
|
1104
|
+
/fatal: repository.*not found/i,
|
|
1105
|
+
/host key verification failed/i,
|
|
1106
|
+
/access denied/i,
|
|
1107
|
+
/unauthorized/i,
|
|
1108
|
+
/403/,
|
|
1109
|
+
/401/
|
|
1110
|
+
];
|
|
1111
|
+
return authPatterns.some((pattern)=>pattern.test(message));
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Execute git command asynchronously
|
|
1116
|
+
*/ async function git(args, cwd) {
|
|
1117
|
+
const { stdout } = await git_execAsync(`git ${args.join(' ')}`, {
|
|
1118
|
+
cwd,
|
|
1119
|
+
encoding: 'utf-8',
|
|
1120
|
+
env: getGitEnv()
|
|
1121
|
+
});
|
|
1122
|
+
return stdout.trim();
|
|
909
1123
|
}
|
|
910
1124
|
/**
|
|
911
|
-
*
|
|
912
|
-
|
|
913
|
-
* @param url - URL to download from
|
|
914
|
-
* @param destPath - Destination file path
|
|
915
|
-
* @param options - Download options
|
|
916
|
-
*/ async function downloadFile(url, destPath, options = {}) {
|
|
917
|
-
const { timeout = 60000, headers = {} } = options;
|
|
918
|
-
// Ensure destination directory exists
|
|
919
|
-
ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(destPath));
|
|
1125
|
+
* Get remote tags for a repository
|
|
1126
|
+
*/ async function getRemoteTags(repoUrl) {
|
|
920
1127
|
try {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1128
|
+
const output = await git([
|
|
1129
|
+
'ls-remote',
|
|
1130
|
+
'--tags',
|
|
1131
|
+
'--refs',
|
|
1132
|
+
repoUrl
|
|
1133
|
+
]);
|
|
1134
|
+
if (!output) return [];
|
|
1135
|
+
const tags = [];
|
|
1136
|
+
const lines = output.split('\n');
|
|
1137
|
+
for (const line of lines){
|
|
1138
|
+
const [commit, ref] = line.split('\t');
|
|
1139
|
+
if (commit && ref) {
|
|
1140
|
+
// Extract tag name from refs/tags/v1.0.0
|
|
1141
|
+
const tagName = ref.replace('refs/tags/', '');
|
|
1142
|
+
tags.push({
|
|
1143
|
+
name: tagName,
|
|
1144
|
+
commit
|
|
1145
|
+
});
|
|
929
1146
|
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
const fileStream = external_node_fs_.createWriteStream(destPath);
|
|
935
|
-
const body = response.body;
|
|
936
|
-
if (!body) throw new HttpDownloadError(url, 'Response body is empty');
|
|
937
|
-
// Convert Web ReadableStream to Node.js Readable
|
|
938
|
-
const { Readable } = await import("node:stream");
|
|
939
|
-
const nodeStream = Readable.fromWeb(body);
|
|
940
|
-
await (0, __WEBPACK_EXTERNAL_MODULE_node_stream_promises__.pipeline)(nodeStream, fileStream);
|
|
941
|
-
} catch (error) {
|
|
942
|
-
// Clean up partial download
|
|
943
|
-
if (external_node_fs_.existsSync(destPath)) external_node_fs_.unlinkSync(destPath);
|
|
944
|
-
if (error instanceof HttpDownloadError) throw error;
|
|
945
|
-
const err = error;
|
|
946
|
-
if ('AbortError' === err.name) throw new HttpDownloadError(url, `Download timeout after ${timeout}ms`);
|
|
947
|
-
throw new HttpDownloadError(url, `Download failed: ${err.message}`, void 0, err);
|
|
1147
|
+
}
|
|
1148
|
+
return tags;
|
|
1149
|
+
} catch {
|
|
1150
|
+
return [];
|
|
948
1151
|
}
|
|
949
1152
|
}
|
|
950
1153
|
/**
|
|
951
|
-
*
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
switch(detectedFormat){
|
|
963
|
-
case 'tar.gz':
|
|
964
|
-
case 'tgz':
|
|
965
|
-
case 'tar':
|
|
966
|
-
await extractTar(archivePath, destDir, 'tar.gz' === detectedFormat || 'tgz' === detectedFormat);
|
|
967
|
-
break;
|
|
968
|
-
case 'zip':
|
|
969
|
-
await extractZip(archivePath, destDir);
|
|
970
|
-
break;
|
|
971
|
-
default:
|
|
972
|
-
throw new Error(`Unsupported archive format: ${detectedFormat}`);
|
|
973
|
-
}
|
|
1154
|
+
* Get latest tag from repository
|
|
1155
|
+
*/ async function getLatestTag(repoUrl) {
|
|
1156
|
+
const tags = await getRemoteTags(repoUrl);
|
|
1157
|
+
if (0 === tags.length) return null;
|
|
1158
|
+
// Sort by semver (simple version sort)
|
|
1159
|
+
const sortedTags = tags.sort((a, b)=>{
|
|
1160
|
+
const aVer = a.name.replace(/^v/, '');
|
|
1161
|
+
const bVer = b.name.replace(/^v/, '');
|
|
1162
|
+
return compareVersions(bVer, aVer);
|
|
1163
|
+
});
|
|
1164
|
+
return sortedTags[0];
|
|
974
1165
|
}
|
|
975
1166
|
/**
|
|
976
|
-
*
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
const
|
|
981
|
-
|
|
1167
|
+
* Clone a repository with shallow clone
|
|
1168
|
+
*
|
|
1169
|
+
* @throws {GitCloneError} When clone fails, with helpful tips for authentication issues
|
|
1170
|
+
*/ async function clone(repoUrl, destPath, options) {
|
|
1171
|
+
const args = [
|
|
1172
|
+
'clone'
|
|
1173
|
+
];
|
|
1174
|
+
if (options?.depth) args.push('--depth', options.depth.toString());
|
|
1175
|
+
if (options?.branch) args.push('--branch', options.branch);
|
|
1176
|
+
args.push(repoUrl, destPath);
|
|
982
1177
|
try {
|
|
983
|
-
|
|
984
|
-
const tempExtractDir = `${destDir}.extract-temp`;
|
|
985
|
-
ensureDir(tempExtractDir);
|
|
986
|
-
await execAsync(`tar ${flags} "${archivePath}" -C "${tempExtractDir}"`, {
|
|
987
|
-
encoding: 'utf-8'
|
|
988
|
-
});
|
|
989
|
-
// Check if archive contains a single root directory
|
|
990
|
-
const extractedItems = external_node_fs_.readdirSync(tempExtractDir);
|
|
991
|
-
if (1 === extractedItems.length) {
|
|
992
|
-
const singleItem = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, extractedItems[0]);
|
|
993
|
-
if (external_node_fs_.statSync(singleItem).isDirectory()) {
|
|
994
|
-
// Move contents of single directory to destination
|
|
995
|
-
const contents = external_node_fs_.readdirSync(singleItem);
|
|
996
|
-
for (const item of contents){
|
|
997
|
-
const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(singleItem, item);
|
|
998
|
-
const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
|
|
999
|
-
external_node_fs_.renameSync(src, dest);
|
|
1000
|
-
}
|
|
1001
|
-
remove(tempExtractDir);
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
// Move all items to destination
|
|
1006
|
-
for (const item of extractedItems){
|
|
1007
|
-
const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, item);
|
|
1008
|
-
const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
|
|
1009
|
-
external_node_fs_.renameSync(src, dest);
|
|
1010
|
-
}
|
|
1011
|
-
remove(tempExtractDir);
|
|
1178
|
+
await git(args);
|
|
1012
1179
|
} catch (error) {
|
|
1013
|
-
throw new
|
|
1180
|
+
throw new GitCloneError(repoUrl, error);
|
|
1014
1181
|
}
|
|
1015
1182
|
}
|
|
1016
1183
|
/**
|
|
1017
|
-
*
|
|
1018
|
-
*/ async function
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1184
|
+
* Get current commit hash
|
|
1185
|
+
*/ async function getCurrentCommit(cwd) {
|
|
1186
|
+
return git([
|
|
1187
|
+
'rev-parse',
|
|
1188
|
+
'HEAD'
|
|
1189
|
+
], cwd);
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Get default branch name
|
|
1193
|
+
*/ async function getDefaultBranch(repoUrl) {
|
|
1022
1194
|
try {
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1195
|
+
const output = await git([
|
|
1196
|
+
'ls-remote',
|
|
1197
|
+
'--symref',
|
|
1198
|
+
repoUrl,
|
|
1199
|
+
'HEAD'
|
|
1200
|
+
]);
|
|
1201
|
+
const match = output.match(/ref: refs\/heads\/(\S+)/);
|
|
1202
|
+
return match ? match[1] : 'main';
|
|
1203
|
+
} catch {
|
|
1204
|
+
return 'main';
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Simple version comparison (for sorting)
|
|
1209
|
+
*/ function compareVersions(a, b) {
|
|
1210
|
+
const aParts = a.split('.').map((p)=>parseInt(p, 10) || 0);
|
|
1211
|
+
const bParts = b.split('.').map((p)=>parseInt(p, 10) || 0);
|
|
1212
|
+
const maxLength = Math.max(aParts.length, bParts.length);
|
|
1213
|
+
for(let i = 0; i < maxLength; i++){
|
|
1214
|
+
const aPart = aParts[i] || 0;
|
|
1215
|
+
const bPart = bParts[i] || 0;
|
|
1216
|
+
if (aPart > bPart) return 1;
|
|
1217
|
+
if (aPart < bPart) return -1;
|
|
1218
|
+
}
|
|
1219
|
+
return 0;
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Check if a source string is a complete Git URL (SSH, HTTPS, git://, or file://)
|
|
1223
|
+
*
|
|
1224
|
+
* Supported formats:
|
|
1225
|
+
* - SSH: git@github.com:user/repo.git
|
|
1226
|
+
* - HTTPS: https://github.com/user/repo.git
|
|
1227
|
+
* - Git protocol: git://github.com/user/repo.git
|
|
1228
|
+
* - File protocol: file:///path/to/repo (for local testing)
|
|
1229
|
+
* - URLs ending with .git
|
|
1230
|
+
*/ function isGitUrl(source) {
|
|
1231
|
+
return source.startsWith('git@') || source.startsWith('git://') || source.startsWith('http://') || source.startsWith('https://') || source.startsWith('file://') || source.endsWith('.git');
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Parse a Git URL and extract host, owner, and repo information
|
|
1235
|
+
*
|
|
1236
|
+
* Supports:
|
|
1237
|
+
* - SSH: git@github.com:user/repo.git
|
|
1238
|
+
* - HTTPS: https://github.com/user/repo.git
|
|
1239
|
+
* - Git protocol: git://github.com/user/repo.git
|
|
1240
|
+
*
|
|
1241
|
+
* Note: GitHub/GitLab web URLs (with /tree/, /blob/, etc.) are handled
|
|
1242
|
+
* at a higher level in GitResolver.parseGitUrlRef() before calling this function.
|
|
1243
|
+
*
|
|
1244
|
+
* @param url The Git URL to parse
|
|
1245
|
+
* @returns Parsed URL information or null if parsing fails
|
|
1246
|
+
*/ function parseGitUrl(url) {
|
|
1247
|
+
// Remove trailing .git if present
|
|
1248
|
+
const cleanUrl = url.replace(/\.git$/, '');
|
|
1249
|
+
// SSH format: git@github.com:user/repo
|
|
1250
|
+
const sshMatch = cleanUrl.match(/^git@([^:]+):(.+)$/);
|
|
1251
|
+
if (sshMatch) {
|
|
1252
|
+
const [, host, path] = sshMatch;
|
|
1253
|
+
const parts = path.split('/');
|
|
1254
|
+
if (parts.length >= 2) {
|
|
1255
|
+
// Handle nested paths like org/sub/repo
|
|
1256
|
+
const owner = parts.slice(0, -1).join('/');
|
|
1257
|
+
const repo = parts[parts.length - 1];
|
|
1258
|
+
return {
|
|
1259
|
+
host,
|
|
1260
|
+
owner,
|
|
1261
|
+
repo,
|
|
1262
|
+
url,
|
|
1263
|
+
type: 'ssh'
|
|
1264
|
+
};
|
|
1045
1265
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1266
|
+
}
|
|
1267
|
+
// HTTPS/Git protocol format: https://github.com/user/repo or git://github.com/user/repo
|
|
1268
|
+
const httpMatch = cleanUrl.match(/^(https?|git):\/\/([^/]+)\/(.+)$/);
|
|
1269
|
+
if (httpMatch) {
|
|
1270
|
+
const [, protocol, host, path] = httpMatch;
|
|
1271
|
+
const parts = path.split('/');
|
|
1272
|
+
if (parts.length >= 2) {
|
|
1273
|
+
const owner = parts.slice(0, -1).join('/');
|
|
1274
|
+
const repo = parts[parts.length - 1];
|
|
1275
|
+
return {
|
|
1276
|
+
host,
|
|
1277
|
+
owner,
|
|
1278
|
+
repo,
|
|
1279
|
+
url,
|
|
1280
|
+
type: 'git' === protocol ? 'git' : 'https'
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
// File protocol format: file:///path/to/repo
|
|
1285
|
+
// Used for local testing and development
|
|
1286
|
+
const fileMatch = cleanUrl.match(/^file:\/\/(.+)$/);
|
|
1287
|
+
if (fileMatch) {
|
|
1288
|
+
const [, filePath] = fileMatch;
|
|
1289
|
+
const parts = filePath.split('/').filter(Boolean);
|
|
1290
|
+
if (parts.length >= 1) {
|
|
1291
|
+
// Use 'local' as host, path components as owner/repo
|
|
1292
|
+
const repo = parts[parts.length - 1];
|
|
1293
|
+
const owner = parts.length > 1 ? parts[parts.length - 2] : 'local';
|
|
1294
|
+
return {
|
|
1295
|
+
host: 'local',
|
|
1296
|
+
owner,
|
|
1297
|
+
repo,
|
|
1298
|
+
url,
|
|
1299
|
+
type: 'file'
|
|
1300
|
+
};
|
|
1051
1301
|
}
|
|
1052
|
-
remove(tempExtractDir);
|
|
1053
|
-
} catch (error) {
|
|
1054
|
-
throw new Error(`Failed to extract zip archive: ${error.message}`);
|
|
1055
1302
|
}
|
|
1303
|
+
return null;
|
|
1056
1304
|
}
|
|
1057
1305
|
/**
|
|
1058
|
-
*
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1306
|
+
* Registry-Scope Mapping Utilities
|
|
1307
|
+
*
|
|
1308
|
+
* Maps registry URLs to their corresponding scopes.
|
|
1309
|
+
* Currently hardcoded; TODO: fetch from /api/registry/info in the future.
|
|
1310
|
+
*/ /**
|
|
1311
|
+
* Public Registry URL
|
|
1312
|
+
* Used for installing skills without a scope
|
|
1313
|
+
*/ const PUBLIC_REGISTRY = 'https://reskill.info/';
|
|
1066
1314
|
/**
|
|
1067
|
-
*
|
|
1315
|
+
* Hardcoded registry to scope mapping
|
|
1316
|
+
* TODO: Replace with dynamic fetching from /api/registry/info
|
|
1317
|
+
*/ const REGISTRY_SCOPE_MAP = {
|
|
1318
|
+
// rush-app (private registry, new)
|
|
1319
|
+
'https://rush-test.zhenguanyu.com': '@kanyun-test',
|
|
1320
|
+
'https://rush.zhenguanyu.com': '@kanyun',
|
|
1321
|
+
// reskill-app (private registry, legacy)
|
|
1322
|
+
'https://reskill-test.zhenguanyu.com': '@kanyun-test',
|
|
1323
|
+
// Local development
|
|
1324
|
+
'http://localhost:3000': '@kanyun-test'
|
|
1325
|
+
};
|
|
1326
|
+
/**
|
|
1327
|
+
* Get the scope for a given registry URL
|
|
1068
1328
|
*
|
|
1069
|
-
* @param
|
|
1070
|
-
* @
|
|
1071
|
-
*
|
|
1072
|
-
* @
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
//
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1082
|
-
// Download archive
|
|
1083
|
-
await downloadFile(url, tempArchive, options);
|
|
1084
|
-
// Extract archive with explicit format
|
|
1085
|
-
await extractArchive(tempArchive, destDir, format);
|
|
1086
|
-
return destDir;
|
|
1087
|
-
} finally{
|
|
1088
|
-
// Clean up temp archive
|
|
1089
|
-
if (external_node_fs_.existsSync(tempArchive)) external_node_fs_.unlinkSync(tempArchive);
|
|
1090
|
-
}
|
|
1329
|
+
* @param registry - Registry URL
|
|
1330
|
+
* @returns Scope string (e.g., "@kanyun") or null if not found
|
|
1331
|
+
*
|
|
1332
|
+
* @example
|
|
1333
|
+
* getScopeForRegistry('https://rush-test.zhenguanyu.com') // '@kanyun'
|
|
1334
|
+
* getScopeForRegistry('https://unknown.com') // null
|
|
1335
|
+
*/ function getScopeForRegistry(registry) {
|
|
1336
|
+
if (!registry) return null;
|
|
1337
|
+
// Try exact match first
|
|
1338
|
+
if (REGISTRY_SCOPE_MAP[registry]) return REGISTRY_SCOPE_MAP[registry];
|
|
1339
|
+
// Try with/without trailing slash
|
|
1340
|
+
const normalized = registry.endsWith('/') ? registry.slice(0, -1) : `${registry}/`;
|
|
1341
|
+
return REGISTRY_SCOPE_MAP[normalized] || null;
|
|
1091
1342
|
}
|
|
1092
1343
|
/**
|
|
1093
|
-
*
|
|
1344
|
+
* Get the registry URL for a given scope (reverse lookup)
|
|
1094
1345
|
*
|
|
1095
|
-
*
|
|
1346
|
+
* @param scope - Scope string (with or without @ prefix), e.g., "@kanyun" or "kanyun"
|
|
1347
|
+
* @param customRegistries - Optional custom scope-to-registry mapping (from skills.json)
|
|
1348
|
+
* @returns Registry URL (with trailing slash) or null if not found
|
|
1096
1349
|
*
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1100
|
-
*
|
|
1101
|
-
*
|
|
1102
|
-
*/
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1350
|
+
* @example
|
|
1351
|
+
* getRegistryForScope('@kanyun') // 'https://rush-test.zhenguanyu.com/'
|
|
1352
|
+
* getRegistryForScope('kanyun') // 'https://rush-test.zhenguanyu.com/'
|
|
1353
|
+
* getRegistryForScope('@unknown') // null
|
|
1354
|
+
* getRegistryForScope('@mycompany', { '@mycompany': 'https://my.registry.com/' }) // 'https://my.registry.com/'
|
|
1355
|
+
*/ function getRegistryForScope(scope, customRegistries) {
|
|
1356
|
+
if (!scope) return null;
|
|
1357
|
+
// Normalize scope: ensure @ prefix
|
|
1358
|
+
const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
|
|
1359
|
+
// 1. First check custom scopeRegistries (from skills.json)
|
|
1360
|
+
if (customRegistries?.[normalizedScope]) {
|
|
1361
|
+
const url = customRegistries[normalizedScope];
|
|
1362
|
+
// Normalize trailing slash
|
|
1363
|
+
return url.endsWith('/') ? url : `${url}/`;
|
|
1109
1364
|
}
|
|
1365
|
+
// 2. Fall back to hardcoded defaults
|
|
1366
|
+
for (const [registry, registryScope] of Object.entries(REGISTRY_SCOPE_MAP))if (registryScope === normalizedScope) // Return URL with trailing slash (normalized format)
|
|
1367
|
+
return registry.endsWith('/') ? registry : `${registry}/`;
|
|
1368
|
+
return null;
|
|
1110
1369
|
}
|
|
1111
1370
|
/**
|
|
1112
|
-
*
|
|
1113
|
-
* Parses --- delimited YAML header
|
|
1371
|
+
* Get the registry URL for a given scope
|
|
1114
1372
|
*
|
|
1115
|
-
*
|
|
1116
|
-
* -
|
|
1117
|
-
*
|
|
1118
|
-
* -
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
* Save the current key/value accumulated so far, then reset state.
|
|
1140
|
-
*/ function flushCurrent() {
|
|
1141
|
-
if (!currentKey) return;
|
|
1142
|
-
if (inNestedObject) {
|
|
1143
|
-
data[currentKey] = nestedObject;
|
|
1144
|
-
nestedObject = {};
|
|
1145
|
-
inNestedObject = false;
|
|
1146
|
-
} else if (inPlainScalar || inMultiline) {
|
|
1147
|
-
data[currentKey] = currentValue.trim();
|
|
1148
|
-
inPlainScalar = false;
|
|
1149
|
-
inMultiline = false;
|
|
1150
|
-
} else data[currentKey] = parseYamlValue(currentValue.trim());
|
|
1151
|
-
currentKey = '';
|
|
1152
|
-
currentValue = '';
|
|
1153
|
-
}
|
|
1154
|
-
for (const line of lines){
|
|
1155
|
-
const trimmedLine = line.trim();
|
|
1156
|
-
if (!trimmedLine || trimmedLine.startsWith('#')) continue;
|
|
1157
|
-
const isIndented = line.startsWith(' ');
|
|
1158
|
-
// ---- Inside a block scalar (| or >) ----
|
|
1159
|
-
if (inMultiline) {
|
|
1160
|
-
if (isIndented) {
|
|
1161
|
-
currentValue += (currentValue ? '\n' : '') + line.slice(2);
|
|
1162
|
-
continue;
|
|
1163
|
-
}
|
|
1164
|
-
// Unindented line ends the block scalar — fall through to top-level parsing
|
|
1165
|
-
flushCurrent();
|
|
1166
|
-
}
|
|
1167
|
-
// ---- Inside a plain scalar (multiline value without | or >) ----
|
|
1168
|
-
if (inPlainScalar) {
|
|
1169
|
-
if (isIndented) {
|
|
1170
|
-
// Continuation line: join with a space (YAML plain scalar folding)
|
|
1171
|
-
currentValue += ` ${trimmedLine}`;
|
|
1172
|
-
continue;
|
|
1173
|
-
}
|
|
1174
|
-
// Unindented line ends the plain scalar — fall through to top-level parsing
|
|
1175
|
-
flushCurrent();
|
|
1176
|
-
}
|
|
1177
|
-
// ---- Inside a nested object ----
|
|
1178
|
-
if (inNestedObject && isIndented) {
|
|
1179
|
-
const nestedMatch = line.match(/^ {2}([a-zA-Z_-]+):\s*(.*)$/);
|
|
1180
|
-
if (nestedMatch) {
|
|
1181
|
-
const [, nestedKey, nestedValue] = nestedMatch;
|
|
1182
|
-
nestedObject[nestedKey] = parseYamlValue(nestedValue.trim());
|
|
1183
|
-
continue;
|
|
1184
|
-
}
|
|
1185
|
-
// Indented line that isn't a nested key:value — this key was actually
|
|
1186
|
-
// a plain scalar, not a nested object. Switch modes.
|
|
1187
|
-
inNestedObject = false;
|
|
1188
|
-
inPlainScalar = true;
|
|
1189
|
-
currentValue = trimmedLine;
|
|
1190
|
-
continue;
|
|
1191
|
-
}
|
|
1192
|
-
// ---- Top-level key: value ----
|
|
1193
|
-
const keyValueMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
|
|
1194
|
-
if (keyValueMatch) {
|
|
1195
|
-
flushCurrent();
|
|
1196
|
-
currentKey = keyValueMatch[1];
|
|
1197
|
-
currentValue = keyValueMatch[2];
|
|
1198
|
-
if ('|' === currentValue || '>' === currentValue) {
|
|
1199
|
-
inMultiline = true;
|
|
1200
|
-
currentValue = '';
|
|
1201
|
-
} else if ('' === currentValue) {
|
|
1202
|
-
// Empty value — could be nested object or plain scalar; peek at next lines
|
|
1203
|
-
inNestedObject = true;
|
|
1204
|
-
nestedObject = {};
|
|
1205
|
-
}
|
|
1206
|
-
continue;
|
|
1207
|
-
}
|
|
1208
|
-
// ---- Unindented line that isn't key:value while in nested object ----
|
|
1209
|
-
if (inNestedObject) flushCurrent();
|
|
1373
|
+
* - With scope → lookup private Registry (throws if not found)
|
|
1374
|
+
* - Without scope (null/undefined/'') → returns public Registry
|
|
1375
|
+
*
|
|
1376
|
+
* @param scope - Scope string (with or without @ prefix), null, undefined, or empty string
|
|
1377
|
+
* @param customRegistries - Optional custom scope-to-registry mapping (from skills.json)
|
|
1378
|
+
* @returns Registry URL (with trailing slash)
|
|
1379
|
+
* @throws Error if scope is provided but not found in the registry map
|
|
1380
|
+
*
|
|
1381
|
+
* @example
|
|
1382
|
+
* getRegistryUrl('@kanyun') // 'https://rush-test.zhenguanyu.com/'
|
|
1383
|
+
* getRegistryUrl('kanyun') // 'https://rush-test.zhenguanyu.com/'
|
|
1384
|
+
* getRegistryUrl(null) // 'https://reskill.info/'
|
|
1385
|
+
* getRegistryUrl('') // 'https://reskill.info/'
|
|
1386
|
+
* getRegistryUrl('@unknown') // throws Error
|
|
1387
|
+
* getRegistryUrl('@mycompany', { '@mycompany': 'https://my.registry.com/' }) // 'https://my.registry.com/'
|
|
1388
|
+
*/ function getRegistryUrl(scope, customRegistries) {
|
|
1389
|
+
// No scope → return public Registry
|
|
1390
|
+
if (!scope) return PUBLIC_REGISTRY;
|
|
1391
|
+
// With scope → lookup private Registry
|
|
1392
|
+
const registry = getRegistryForScope(scope, customRegistries);
|
|
1393
|
+
if (!registry) {
|
|
1394
|
+
// Normalize scope for error message
|
|
1395
|
+
const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
|
|
1396
|
+
throw new Error(`Unknown scope ${normalizedScope}. No registry configured for this scope.`);
|
|
1210
1397
|
}
|
|
1211
|
-
|
|
1212
|
-
flushCurrent();
|
|
1213
|
-
return {
|
|
1214
|
-
data,
|
|
1215
|
-
content: markdownContent
|
|
1216
|
-
};
|
|
1398
|
+
return registry;
|
|
1217
1399
|
}
|
|
1218
1400
|
/**
|
|
1219
|
-
* Parse
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1401
|
+
* Parse a skill name into its components
|
|
1402
|
+
*
|
|
1403
|
+
* @param skillName - Full or short skill name
|
|
1404
|
+
* @returns Parsed skill name with scope and name
|
|
1405
|
+
*
|
|
1406
|
+
* @example
|
|
1407
|
+
* parseSkillName('@kanyun/planning-with-files')
|
|
1408
|
+
* // { scope: '@kanyun', name: 'planning-with-files', fullName: '@kanyun/planning-with-files' }
|
|
1409
|
+
*
|
|
1410
|
+
* parseSkillName('planning-with-files')
|
|
1411
|
+
* // { scope: null, name: 'planning-with-files', fullName: 'planning-with-files' }
|
|
1412
|
+
*/ function parseSkillName(skillName) {
|
|
1413
|
+
// Match @scope/name pattern
|
|
1414
|
+
const match = skillName.match(/^(@[^/]+)\/(.+)$/);
|
|
1415
|
+
if (match) return {
|
|
1416
|
+
scope: match[1],
|
|
1417
|
+
name: match[2],
|
|
1418
|
+
fullName: skillName
|
|
1419
|
+
};
|
|
1420
|
+
return {
|
|
1421
|
+
scope: null,
|
|
1422
|
+
name: skillName,
|
|
1423
|
+
fullName: skillName
|
|
1424
|
+
};
|
|
1231
1425
|
}
|
|
1232
1426
|
/**
|
|
1233
|
-
*
|
|
1427
|
+
* Build full skill name from scope and name
|
|
1234
1428
|
*
|
|
1235
|
-
*
|
|
1236
|
-
* -
|
|
1237
|
-
*
|
|
1238
|
-
*
|
|
1239
|
-
*
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
if (
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
if (1 === name.length && !/^[a-z0-9]$/.test(name)) throw new SkillValidationError('Single character skill name must be a lowercase letter or number', 'name');
|
|
1429
|
+
* @param scope - Scope (with or without @ prefix), or null
|
|
1430
|
+
* @param name - Short skill name
|
|
1431
|
+
* @returns Full skill name (e.g., "@kanyun/planning-with-files")
|
|
1432
|
+
*
|
|
1433
|
+
* @example
|
|
1434
|
+
* buildFullSkillName('@kanyun', 'planning-with-files') // '@kanyun/planning-with-files'
|
|
1435
|
+
* buildFullSkillName('kanyun', 'my-skill') // '@kanyun/my-skill'
|
|
1436
|
+
* buildFullSkillName(null, 'my-skill') // 'my-skill'
|
|
1437
|
+
*/ function buildFullSkillName(scope, name) {
|
|
1438
|
+
if (!scope) return name;
|
|
1439
|
+
// Ensure scope starts with @
|
|
1440
|
+
const normalizedScope = scope.startsWith('@') ? scope : `@${scope}`;
|
|
1441
|
+
return `${normalizedScope}/${name}`;
|
|
1249
1442
|
}
|
|
1250
1443
|
/**
|
|
1251
|
-
*
|
|
1444
|
+
* Get short name from a skill name (removes scope if present)
|
|
1252
1445
|
*
|
|
1253
|
-
*
|
|
1254
|
-
*
|
|
1255
|
-
*
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1446
|
+
* @param skillName - Full or short skill name
|
|
1447
|
+
* @returns Short name without scope
|
|
1448
|
+
*
|
|
1449
|
+
* @example
|
|
1450
|
+
* getShortName('@kanyun/planning-with-files') // 'planning-with-files'
|
|
1451
|
+
* getShortName('planning-with-files') // 'planning-with-files'
|
|
1452
|
+
*/ function getShortName(skillName) {
|
|
1453
|
+
return parseSkillName(skillName).name;
|
|
1260
1454
|
}
|
|
1261
1455
|
/**
|
|
1262
|
-
* Parse
|
|
1456
|
+
* Parse a skill identifier into its components (with version support)
|
|
1263
1457
|
*
|
|
1264
|
-
* @
|
|
1265
|
-
*
|
|
1266
|
-
* @
|
|
1267
|
-
* @
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1458
|
+
* Supports both private registry (with @scope) and public registry (without scope) formats.
|
|
1459
|
+
*
|
|
1460
|
+
* @param identifier - Skill identifier string
|
|
1461
|
+
* @returns Parsed skill identifier with scope, name, version, and fullName
|
|
1462
|
+
* @throws Error if identifier is invalid
|
|
1463
|
+
*
|
|
1464
|
+
* @example
|
|
1465
|
+
* // Private registry
|
|
1466
|
+
* parseSkillIdentifier('@kanyun/planning-with-files')
|
|
1467
|
+
* // { scope: '@kanyun', name: 'planning-with-files', version: undefined, fullName: '@kanyun/planning-with-files' }
|
|
1468
|
+
*
|
|
1469
|
+
* parseSkillIdentifier('@kanyun/skill@2.4.5')
|
|
1470
|
+
* // { scope: '@kanyun', name: 'skill', version: '2.4.5', fullName: '@kanyun/skill' }
|
|
1471
|
+
*
|
|
1472
|
+
* // Public registry
|
|
1473
|
+
* parseSkillIdentifier('planning-with-files')
|
|
1474
|
+
* // { scope: null, name: 'planning-with-files', version: undefined, fullName: 'planning-with-files' }
|
|
1475
|
+
*
|
|
1476
|
+
* parseSkillIdentifier('skill@latest')
|
|
1477
|
+
* // { scope: null, name: 'skill', version: 'latest', fullName: 'skill' }
|
|
1478
|
+
*/ function parseSkillIdentifier(identifier) {
|
|
1479
|
+
const trimmed = identifier.trim();
|
|
1480
|
+
// Empty string or whitespace only
|
|
1481
|
+
if (!trimmed) throw new Error('Invalid skill identifier: empty string');
|
|
1482
|
+
// Starting with @@ is invalid
|
|
1483
|
+
if (trimmed.startsWith('@@')) throw new Error('Invalid skill identifier: invalid scope format');
|
|
1484
|
+
// Bare @ is invalid
|
|
1485
|
+
if ('@' === trimmed) throw new Error('Invalid skill identifier: missing scope and name');
|
|
1486
|
+
// Scoped format: @scope/name[@version]
|
|
1487
|
+
if (trimmed.startsWith('@')) {
|
|
1488
|
+
// Regex: @scope/name[@version]
|
|
1489
|
+
// scope: starts with @, followed by alphanumeric, hyphens, underscores
|
|
1490
|
+
// name: alphanumeric, hyphens, underscores
|
|
1491
|
+
// version: optional, @ followed by any non-empty string
|
|
1492
|
+
const scopedMatch = trimmed.match(/^(@[\w-]+)\/([\w-]+)(?:@(.+))?$/);
|
|
1493
|
+
if (!scopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
|
|
1494
|
+
const [, scope, name, version] = scopedMatch;
|
|
1290
1495
|
return {
|
|
1496
|
+
scope,
|
|
1291
1497
|
name,
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
license: data.license ? String(data.license) : void 0,
|
|
1295
|
-
compatibility: data.compatibility ? String(data.compatibility) : void 0,
|
|
1296
|
-
metadata: data.metadata,
|
|
1297
|
-
allowedTools,
|
|
1298
|
-
content: body,
|
|
1299
|
-
rawContent: content
|
|
1498
|
+
version: version || void 0,
|
|
1499
|
+
fullName: `${scope}/${name}`
|
|
1300
1500
|
};
|
|
1301
|
-
} catch (error) {
|
|
1302
|
-
if (error instanceof SkillValidationError) throw error;
|
|
1303
|
-
if (strict) throw new SkillValidationError(`Failed to parse SKILL.md: ${error}`);
|
|
1304
|
-
return null;
|
|
1305
1501
|
}
|
|
1502
|
+
// Unscoped format: name[@version] (public registry)
|
|
1503
|
+
// name must not contain / (otherwise it might be a git shorthand)
|
|
1504
|
+
const unscopedMatch = trimmed.match(/^([\w-]+)(?:@(.+))?$/);
|
|
1505
|
+
if (!unscopedMatch) throw new Error(`Invalid skill identifier: ${identifier}`);
|
|
1506
|
+
const [, name, version] = unscopedMatch;
|
|
1507
|
+
return {
|
|
1508
|
+
scope: null,
|
|
1509
|
+
name,
|
|
1510
|
+
version: version || void 0,
|
|
1511
|
+
fullName: name
|
|
1512
|
+
};
|
|
1306
1513
|
}
|
|
1307
1514
|
/**
|
|
1308
|
-
*
|
|
1309
|
-
*/
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1515
|
+
* HTTP utilities for downloading and extracting skill archives
|
|
1516
|
+
*/ /**
|
|
1517
|
+
* Custom error class for HTTP download failures
|
|
1518
|
+
*/ class HttpDownloadError extends Error {
|
|
1519
|
+
url;
|
|
1520
|
+
statusCode;
|
|
1521
|
+
originalError;
|
|
1522
|
+
constructor(url, message, statusCode, originalError){
|
|
1523
|
+
super(message);
|
|
1524
|
+
this.name = 'HttpDownloadError';
|
|
1525
|
+
this.url = url;
|
|
1526
|
+
this.statusCode = statusCode;
|
|
1527
|
+
this.originalError = originalError;
|
|
1313
1528
|
}
|
|
1314
|
-
const content = external_node_fs_.readFileSync(filePath, 'utf-8');
|
|
1315
|
-
return parseSkillMd(content, options);
|
|
1316
1529
|
}
|
|
1317
1530
|
/**
|
|
1318
|
-
*
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1531
|
+
* Download a file from HTTP/HTTPS URL
|
|
1532
|
+
*
|
|
1533
|
+
* @param url - URL to download from
|
|
1534
|
+
* @param destPath - Destination file path
|
|
1535
|
+
* @param options - Download options
|
|
1536
|
+
*/ async function downloadFile(url, destPath, options = {}) {
|
|
1537
|
+
const { timeout = 60000, headers = {} } = options;
|
|
1538
|
+
// Ensure destination directory exists
|
|
1539
|
+
fs_ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(destPath));
|
|
1540
|
+
try {
|
|
1541
|
+
// Use native fetch for HTTP/HTTPS
|
|
1542
|
+
const controller = new AbortController();
|
|
1543
|
+
const timeoutId = setTimeout(()=>controller.abort(), timeout);
|
|
1544
|
+
const response = await fetch(url, {
|
|
1545
|
+
signal: controller.signal,
|
|
1546
|
+
headers: {
|
|
1547
|
+
'User-Agent': 'reskill/1.0',
|
|
1548
|
+
...headers
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1551
|
+
clearTimeout(timeoutId);
|
|
1552
|
+
if (!response.ok) throw new HttpDownloadError(url, `HTTP ${response.status}: ${response.statusText}`, response.status);
|
|
1553
|
+
// Stream response to file
|
|
1554
|
+
const fileStream = external_node_fs_.createWriteStream(destPath);
|
|
1555
|
+
const body = response.body;
|
|
1556
|
+
if (!body) throw new HttpDownloadError(url, 'Response body is empty');
|
|
1557
|
+
// Convert Web ReadableStream to Node.js Readable
|
|
1558
|
+
const { Readable } = await import("node:stream");
|
|
1559
|
+
const nodeStream = Readable.fromWeb(body);
|
|
1560
|
+
await (0, __WEBPACK_EXTERNAL_MODULE_node_stream_promises__.pipeline)(nodeStream, fileStream);
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
// Clean up partial download
|
|
1563
|
+
if (external_node_fs_.existsSync(destPath)) external_node_fs_.unlinkSync(destPath);
|
|
1564
|
+
if (error instanceof HttpDownloadError) throw error;
|
|
1565
|
+
const err = error;
|
|
1566
|
+
if ('AbortError' === err.name) throw new HttpDownloadError(url, `Download timeout after ${timeout}ms`);
|
|
1567
|
+
throw new HttpDownloadError(url, `Download failed: ${err.message}`, void 0, err);
|
|
1568
|
+
}
|
|
1322
1569
|
}
|
|
1323
1570
|
/**
|
|
1324
|
-
*
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1571
|
+
* Extract an archive to a directory
|
|
1572
|
+
*
|
|
1573
|
+
* @param archivePath - Path to the archive file
|
|
1574
|
+
* @param destDir - Destination directory
|
|
1575
|
+
* @param format - Archive format (auto-detected from extension if not provided)
|
|
1576
|
+
*/ async function extractArchive(archivePath, destDir, format) {
|
|
1577
|
+
// Auto-detect format from extension
|
|
1578
|
+
const detectedFormat = format || detectArchiveFormat(archivePath);
|
|
1579
|
+
if (!detectedFormat) throw new Error(`Unable to detect archive format for: ${archivePath}`);
|
|
1580
|
+
// Ensure destination directory exists
|
|
1581
|
+
fs_ensureDir(destDir);
|
|
1582
|
+
switch(detectedFormat){
|
|
1583
|
+
case 'tar.gz':
|
|
1584
|
+
case 'tgz':
|
|
1585
|
+
case 'tar':
|
|
1586
|
+
await extractTar(archivePath, destDir, 'tar.gz' === detectedFormat || 'tgz' === detectedFormat);
|
|
1587
|
+
break;
|
|
1588
|
+
case 'zip':
|
|
1589
|
+
await extractZip(archivePath, destDir);
|
|
1590
|
+
break;
|
|
1591
|
+
default:
|
|
1592
|
+
throw new Error(`Unsupported archive format: ${detectedFormat}`);
|
|
1333
1593
|
}
|
|
1334
1594
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
const MAX_DISCOVER_DEPTH = 5;
|
|
1343
|
-
const PRIORITY_SKILL_DIRS = [
|
|
1344
|
-
'skills',
|
|
1345
|
-
'.agents/skills',
|
|
1346
|
-
'.cursor/skills',
|
|
1347
|
-
'.claude/skills',
|
|
1348
|
-
'.windsurf/skills',
|
|
1349
|
-
'.github/skills'
|
|
1350
|
-
];
|
|
1351
|
-
function findSkillDirsRecursive(dir, depth, maxDepth, visitedDirs) {
|
|
1352
|
-
if (depth > maxDepth) return [];
|
|
1353
|
-
const resolvedDir = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir);
|
|
1354
|
-
if (visitedDirs.has(resolvedDir)) return [];
|
|
1355
|
-
if (!external_node_fs_.existsSync(dir) || !external_node_fs_.statSync(dir).isDirectory()) return [];
|
|
1356
|
-
visitedDirs.add(resolvedDir);
|
|
1357
|
-
const results = [];
|
|
1358
|
-
let entries;
|
|
1595
|
+
/**
|
|
1596
|
+
* Extract tar archive using native tar command
|
|
1597
|
+
*/ async function extractTar(archivePath, destDir, gzipped) {
|
|
1598
|
+
const { exec } = await import("node:child_process");
|
|
1599
|
+
const { promisify } = await import("node:util");
|
|
1600
|
+
const execAsync = promisify(exec);
|
|
1601
|
+
const flags = gzipped ? '-xzf' : '-xf';
|
|
1359
1602
|
try {
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
const
|
|
1368
|
-
if (
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1603
|
+
// Extract to a temp directory first to handle single-folder archives
|
|
1604
|
+
const tempExtractDir = `${destDir}.extract-temp`;
|
|
1605
|
+
fs_ensureDir(tempExtractDir);
|
|
1606
|
+
await execAsync(`tar ${flags} "${archivePath}" -C "${tempExtractDir}"`, {
|
|
1607
|
+
encoding: 'utf-8'
|
|
1608
|
+
});
|
|
1609
|
+
// Check if archive contains a single root directory
|
|
1610
|
+
const extractedItems = external_node_fs_.readdirSync(tempExtractDir);
|
|
1611
|
+
if (1 === extractedItems.length) {
|
|
1612
|
+
const singleItem = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, extractedItems[0]);
|
|
1613
|
+
if (external_node_fs_.statSync(singleItem).isDirectory()) {
|
|
1614
|
+
// Move contents of single directory to destination
|
|
1615
|
+
const contents = external_node_fs_.readdirSync(singleItem);
|
|
1616
|
+
for (const item of contents){
|
|
1617
|
+
const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(singleItem, item);
|
|
1618
|
+
const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
|
|
1619
|
+
external_node_fs_.renameSync(src, dest);
|
|
1620
|
+
}
|
|
1621
|
+
fs_remove(tempExtractDir);
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1374
1624
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1625
|
+
// Move all items to destination
|
|
1626
|
+
for (const item of extractedItems){
|
|
1627
|
+
const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, item);
|
|
1628
|
+
const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
|
|
1629
|
+
external_node_fs_.renameSync(src, dest);
|
|
1378
1630
|
}
|
|
1631
|
+
fs_remove(tempExtractDir);
|
|
1632
|
+
} catch (error) {
|
|
1633
|
+
throw new Error(`Failed to extract tar archive: ${error.message}`);
|
|
1379
1634
|
}
|
|
1380
|
-
return results;
|
|
1381
1635
|
}
|
|
1382
1636
|
/**
|
|
1383
|
-
*
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
const
|
|
1398
|
-
if (
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
// Track visited directories to avoid redundant I/O during recursive scan
|
|
1408
|
-
const visitedDirs = new Set();
|
|
1409
|
-
for (const sub of PRIORITY_SKILL_DIRS){
|
|
1410
|
-
const dir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(resolvedBase, sub);
|
|
1411
|
-
if (!!external_node_fs_.existsSync(dir) && !!external_node_fs_.statSync(dir).isDirectory()) {
|
|
1412
|
-
visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(dir));
|
|
1413
|
-
try {
|
|
1414
|
-
const entries = external_node_fs_.readdirSync(dir);
|
|
1415
|
-
for (const entry of entries){
|
|
1416
|
-
const skillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dir, entry);
|
|
1417
|
-
try {
|
|
1418
|
-
if (external_node_fs_.statSync(skillDir).isDirectory() && hasValidSkillMd(skillDir)) {
|
|
1419
|
-
addSkill(skillDir);
|
|
1420
|
-
visitedDirs.add(__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(skillDir));
|
|
1421
|
-
}
|
|
1422
|
-
} catch {
|
|
1423
|
-
// Skip entries that can't be stat'd (race condition, permission, etc.)
|
|
1424
|
-
}
|
|
1637
|
+
* Extract zip archive using native unzip command or Node.js
|
|
1638
|
+
*/ async function extractZip(archivePath, destDir) {
|
|
1639
|
+
const { exec } = await import("node:child_process");
|
|
1640
|
+
const { promisify } = await import("node:util");
|
|
1641
|
+
const execAsync = promisify(exec);
|
|
1642
|
+
try {
|
|
1643
|
+
// Extract to a temp directory first
|
|
1644
|
+
const tempExtractDir = `${destDir}.extract-temp`;
|
|
1645
|
+
fs_ensureDir(tempExtractDir);
|
|
1646
|
+
// Try using unzip command (available on most systems)
|
|
1647
|
+
await execAsync(`unzip -q "${archivePath}" -d "${tempExtractDir}"`, {
|
|
1648
|
+
encoding: 'utf-8'
|
|
1649
|
+
});
|
|
1650
|
+
// Check if archive contains a single root directory
|
|
1651
|
+
const extractedItems = external_node_fs_.readdirSync(tempExtractDir);
|
|
1652
|
+
if (1 === extractedItems.length) {
|
|
1653
|
+
const singleItem = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, extractedItems[0]);
|
|
1654
|
+
if (external_node_fs_.statSync(singleItem).isDirectory()) {
|
|
1655
|
+
// Move contents of single directory to destination
|
|
1656
|
+
const contents = external_node_fs_.readdirSync(singleItem);
|
|
1657
|
+
for (const item of contents){
|
|
1658
|
+
const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(singleItem, item);
|
|
1659
|
+
const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
|
|
1660
|
+
external_node_fs_.renameSync(src, dest);
|
|
1425
1661
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1662
|
+
fs_remove(tempExtractDir);
|
|
1663
|
+
return;
|
|
1428
1664
|
}
|
|
1429
1665
|
}
|
|
1666
|
+
// Move all items to destination
|
|
1667
|
+
for (const item of extractedItems){
|
|
1668
|
+
const src = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempExtractDir, item);
|
|
1669
|
+
const dest = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, item);
|
|
1670
|
+
external_node_fs_.renameSync(src, dest);
|
|
1671
|
+
}
|
|
1672
|
+
fs_remove(tempExtractDir);
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
throw new Error(`Failed to extract zip archive: ${error.message}`);
|
|
1430
1675
|
}
|
|
1431
|
-
const recursiveDirs = findSkillDirsRecursive(resolvedBase, 0, MAX_DISCOVER_DEPTH, visitedDirs);
|
|
1432
|
-
for (const skillDir of recursiveDirs)addSkill(skillDir);
|
|
1433
|
-
return results;
|
|
1434
1676
|
}
|
|
1435
1677
|
/**
|
|
1436
|
-
*
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1678
|
+
* Detect archive format from file path
|
|
1679
|
+
*/ function detectArchiveFormat(filePath) {
|
|
1680
|
+
const lower = filePath.toLowerCase();
|
|
1681
|
+
if (lower.endsWith('.tar.gz')) return 'tar.gz';
|
|
1682
|
+
if (lower.endsWith('.tgz')) return 'tgz';
|
|
1683
|
+
if (lower.endsWith('.zip')) return 'zip';
|
|
1684
|
+
if (lower.endsWith('.tar')) return 'tar';
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Download and extract an archive in one operation
|
|
1440
1688
|
*
|
|
1441
|
-
* @param
|
|
1442
|
-
* @param
|
|
1443
|
-
* @
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1689
|
+
* @param url - URL to download from
|
|
1690
|
+
* @param destDir - Destination directory for extracted contents
|
|
1691
|
+
* @param options - Download options
|
|
1692
|
+
* @returns Path to extracted contents
|
|
1693
|
+
*/ async function downloadAndExtract(url, destDir, options = {}) {
|
|
1694
|
+
// Determine archive filename from URL
|
|
1695
|
+
const urlObj = new URL(url);
|
|
1696
|
+
const filename = __WEBPACK_EXTERNAL_MODULE_node_path__.basename(urlObj.pathname);
|
|
1697
|
+
// Detect format from original filename before adding .download suffix
|
|
1698
|
+
const format = detectArchiveFormat(filename);
|
|
1699
|
+
if (!format) throw new Error(`Unable to detect archive format from URL: ${url}`);
|
|
1700
|
+
const tempArchive = __WEBPACK_EXTERNAL_MODULE_node_path__.join(destDir, `../${filename}.download`);
|
|
1701
|
+
try {
|
|
1702
|
+
// Download archive
|
|
1703
|
+
await downloadFile(url, tempArchive, options);
|
|
1704
|
+
// Extract archive with explicit format
|
|
1705
|
+
await extractArchive(tempArchive, destDir, format);
|
|
1706
|
+
return destDir;
|
|
1707
|
+
} finally{
|
|
1708
|
+
// Clean up temp archive
|
|
1709
|
+
if (external_node_fs_.existsSync(tempArchive)) external_node_fs_.unlinkSync(tempArchive);
|
|
1710
|
+
}
|
|
1454
1711
|
}
|
|
1455
1712
|
/**
|
|
1456
1713
|
* Installer - Multi-Agent installer
|
|
@@ -1469,14 +1726,14 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1469
1726
|
/**
|
|
1470
1727
|
* Default files to exclude when copying skills
|
|
1471
1728
|
* These files are typically used for repository metadata and should not be copied to agent directories
|
|
1472
|
-
*/ const
|
|
1729
|
+
*/ const installer_DEFAULT_EXCLUDE_FILES = [
|
|
1473
1730
|
'README.md',
|
|
1474
1731
|
'metadata.json',
|
|
1475
1732
|
'.reskill-commit'
|
|
1476
1733
|
];
|
|
1477
1734
|
/**
|
|
1478
1735
|
* Prefix for files that should be excluded (internal/private files)
|
|
1479
|
-
*/ const
|
|
1736
|
+
*/ const installer_EXCLUDE_PREFIX = '_';
|
|
1480
1737
|
/**
|
|
1481
1738
|
* Sanitize filename to prevent path traversal attacks
|
|
1482
1739
|
*/ function installer_sanitizeName(name) {
|
|
@@ -1530,18 +1787,18 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1530
1787
|
* By default excludes:
|
|
1531
1788
|
* - Files in DEFAULT_EXCLUDE_FILES (README.md, metadata.json, .reskill-commit)
|
|
1532
1789
|
* - Files starting with EXCLUDE_PREFIX ('_')
|
|
1533
|
-
*/ function
|
|
1534
|
-
const exclude = new Set(options?.exclude ||
|
|
1790
|
+
*/ function installer_copyDirectory(src, dest, options) {
|
|
1791
|
+
const exclude = new Set(options?.exclude || installer_DEFAULT_EXCLUDE_FILES);
|
|
1535
1792
|
installer_ensureDir(dest);
|
|
1536
1793
|
const entries = external_node_fs_.readdirSync(src, {
|
|
1537
1794
|
withFileTypes: true
|
|
1538
1795
|
});
|
|
1539
1796
|
for (const entry of entries){
|
|
1540
1797
|
// Skip files starting with EXCLUDE_PREFIX and files in exclude list
|
|
1541
|
-
if (exclude.has(entry.name) || entry.name.startsWith(
|
|
1798
|
+
if (exclude.has(entry.name) || entry.name.startsWith(installer_EXCLUDE_PREFIX)) continue;
|
|
1542
1799
|
const srcPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(src, entry.name);
|
|
1543
1800
|
const destPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dest, entry.name);
|
|
1544
|
-
if (entry.isDirectory())
|
|
1801
|
+
if (entry.isDirectory()) installer_copyDirectory(srcPath, destPath, options);
|
|
1545
1802
|
else external_node_fs_.copyFileSync(srcPath, destPath);
|
|
1546
1803
|
}
|
|
1547
1804
|
}
|
|
@@ -1608,6 +1865,7 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1608
1865
|
/**
|
|
1609
1866
|
* Get agent's skill installation path
|
|
1610
1867
|
*/ getAgentSkillPath(skillName, agentType) {
|
|
1868
|
+
if (agentType === claude_3p_installer_CLAUDE_COWORK_3P_AGENT) return getClaude3pSkillPath(skillName);
|
|
1611
1869
|
const agent = getAgentConfig(agentType);
|
|
1612
1870
|
const sanitized = installer_sanitizeName(skillName);
|
|
1613
1871
|
const agentBase = this.isGlobal ? agent.globalSkillsDir : __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cwd, agent.skillsDir);
|
|
@@ -1621,6 +1879,9 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1621
1879
|
* @param agentType - Target agent type
|
|
1622
1880
|
* @param options - Installation options
|
|
1623
1881
|
*/ async installForAgent(sourcePath, skillName, agentType, options = {}) {
|
|
1882
|
+
if (agentType === claude_3p_installer_CLAUDE_COWORK_3P_AGENT) return installClaude3pSkill(sourcePath, skillName, {
|
|
1883
|
+
mode: options.mode
|
|
1884
|
+
});
|
|
1624
1885
|
const agent = getAgentConfig(agentType);
|
|
1625
1886
|
const installMode = options.mode || 'symlink';
|
|
1626
1887
|
const sanitized = installer_sanitizeName(skillName);
|
|
@@ -1649,7 +1910,7 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1649
1910
|
if ('copy' === installMode) {
|
|
1650
1911
|
installer_ensureDir(agentDir);
|
|
1651
1912
|
installer_remove(agentDir);
|
|
1652
|
-
|
|
1913
|
+
installer_copyDirectory(sourcePath, agentDir);
|
|
1653
1914
|
result = {
|
|
1654
1915
|
success: true,
|
|
1655
1916
|
path: agentDir,
|
|
@@ -1659,7 +1920,7 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1659
1920
|
// Symlink mode: copy to canonical location, then create symlink
|
|
1660
1921
|
installer_ensureDir(canonicalDir);
|
|
1661
1922
|
installer_remove(canonicalDir);
|
|
1662
|
-
|
|
1923
|
+
installer_copyDirectory(sourcePath, canonicalDir);
|
|
1663
1924
|
const symlinkCreated = await installer_createSymlink(canonicalDir, agentDir);
|
|
1664
1925
|
if (symlinkCreated) result = {
|
|
1665
1926
|
success: true,
|
|
@@ -1675,7 +1936,7 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1675
1936
|
// Ignore cleanup errors
|
|
1676
1937
|
}
|
|
1677
1938
|
installer_ensureDir(agentDir);
|
|
1678
|
-
|
|
1939
|
+
installer_copyDirectory(sourcePath, agentDir);
|
|
1679
1940
|
result = {
|
|
1680
1941
|
success: true,
|
|
1681
1942
|
path: agentDir,
|
|
@@ -1710,8 +1971,12 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1710
1971
|
/**
|
|
1711
1972
|
* Check if skill is installed to specified agent
|
|
1712
1973
|
*/ isInstalled(skillName, agentType) {
|
|
1713
|
-
|
|
1714
|
-
|
|
1974
|
+
try {
|
|
1975
|
+
const skillPath = this.getAgentSkillPath(skillName, agentType);
|
|
1976
|
+
return external_node_fs_.existsSync(skillPath);
|
|
1977
|
+
} catch {
|
|
1978
|
+
return false;
|
|
1979
|
+
}
|
|
1715
1980
|
}
|
|
1716
1981
|
/**
|
|
1717
1982
|
* Check if skill is installed in canonical location
|
|
@@ -1722,6 +1987,11 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1722
1987
|
/**
|
|
1723
1988
|
* Uninstall skill from specified agent
|
|
1724
1989
|
*/ uninstallFromAgent(skillName, agentType) {
|
|
1990
|
+
if (agentType === claude_3p_installer_CLAUDE_COWORK_3P_AGENT) try {
|
|
1991
|
+
return uninstallClaude3pSkill(skillName);
|
|
1992
|
+
} catch {
|
|
1993
|
+
return false;
|
|
1994
|
+
}
|
|
1725
1995
|
const skillPath = this.getAgentSkillPath(skillName, agentType);
|
|
1726
1996
|
if (!external_node_fs_.existsSync(skillPath)) return false;
|
|
1727
1997
|
installer_remove(skillPath);
|
|
@@ -1742,6 +2012,11 @@ const installer_SKILLS_SUBDIR = 'skills';
|
|
|
1742
2012
|
/**
|
|
1743
2013
|
* Get all skills installed to specified agent
|
|
1744
2014
|
*/ listInstalledSkills(agentType) {
|
|
2015
|
+
if (agentType === claude_3p_installer_CLAUDE_COWORK_3P_AGENT) try {
|
|
2016
|
+
return listClaude3pSkills();
|
|
2017
|
+
} catch {
|
|
2018
|
+
return [];
|
|
2019
|
+
}
|
|
1745
2020
|
const agent = getAgentConfig(agentType);
|
|
1746
2021
|
const skillsDir = this.isGlobal ? agent.globalSkillsDir : __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cwd, agent.skillsDir);
|
|
1747
2022
|
if (!external_node_fs_.existsSync(skillsDir)) return [];
|
|
@@ -1869,7 +2144,7 @@ ${CURSOR_BRIDGE_MARKER}
|
|
|
1869
2144
|
* Check if skill is cached
|
|
1870
2145
|
*/ isCached(parsed, version) {
|
|
1871
2146
|
const cachePath = this.getSkillCachePath(parsed, version);
|
|
1872
|
-
return
|
|
2147
|
+
return fs_exists(cachePath) && isDirectory(cachePath);
|
|
1873
2148
|
}
|
|
1874
2149
|
/**
|
|
1875
2150
|
* Get cached skill
|
|
@@ -1881,7 +2156,7 @@ ${CURSOR_BRIDGE_MARKER}
|
|
|
1881
2156
|
let commit = '';
|
|
1882
2157
|
try {
|
|
1883
2158
|
const fs = await import("node:fs");
|
|
1884
|
-
if (
|
|
2159
|
+
if (fs_exists(commitFile)) commit = fs.readFileSync(commitFile, 'utf-8').trim();
|
|
1885
2160
|
} catch {
|
|
1886
2161
|
// Ignore read errors
|
|
1887
2162
|
}
|
|
@@ -1895,11 +2170,11 @@ ${CURSOR_BRIDGE_MARKER}
|
|
|
1895
2170
|
*/ async cache(repoUrl, parsed, ref, version) {
|
|
1896
2171
|
const cachePath = this.getSkillCachePath(parsed, version);
|
|
1897
2172
|
// If exists, delete first
|
|
1898
|
-
if (
|
|
1899
|
-
|
|
2173
|
+
if (fs_exists(cachePath)) fs_remove(cachePath);
|
|
2174
|
+
fs_ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(cachePath));
|
|
1900
2175
|
// Clone repository
|
|
1901
2176
|
const tempPath = `${cachePath}.tmp`;
|
|
1902
|
-
|
|
2177
|
+
fs_remove(tempPath);
|
|
1903
2178
|
await clone(repoUrl, tempPath, {
|
|
1904
2179
|
depth: 1,
|
|
1905
2180
|
branch: ref
|
|
@@ -1909,8 +2184,8 @@ ${CURSOR_BRIDGE_MARKER}
|
|
|
1909
2184
|
// If has subPath, only keep subdirectory
|
|
1910
2185
|
if (parsed.subPath) {
|
|
1911
2186
|
const subDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(tempPath, parsed.subPath);
|
|
1912
|
-
if (!
|
|
1913
|
-
|
|
2187
|
+
if (!fs_exists(subDir)) {
|
|
2188
|
+
fs_remove(tempPath);
|
|
1914
2189
|
throw new Error(`Subpath ${parsed.subPath} not found in repository`);
|
|
1915
2190
|
}
|
|
1916
2191
|
copyDir(subDir, cachePath, {
|
|
@@ -1926,7 +2201,7 @@ ${CURSOR_BRIDGE_MARKER}
|
|
|
1926
2201
|
// Save commit info
|
|
1927
2202
|
external_node_fs_.writeFileSync(__WEBPACK_EXTERNAL_MODULE_node_path__.join(cachePath, '.reskill-commit'), commit);
|
|
1928
2203
|
// Clean up temp directory
|
|
1929
|
-
|
|
2204
|
+
fs_remove(tempPath);
|
|
1930
2205
|
return {
|
|
1931
2206
|
path: cachePath,
|
|
1932
2207
|
commit
|
|
@@ -1945,8 +2220,8 @@ ${CURSOR_BRIDGE_MARKER}
|
|
|
1945
2220
|
*/ async cacheFromHttp(url, parsed, version) {
|
|
1946
2221
|
const cachePath = this.getSkillCachePath(parsed, version);
|
|
1947
2222
|
// If exists, delete first
|
|
1948
|
-
if (
|
|
1949
|
-
|
|
2223
|
+
if (fs_exists(cachePath)) fs_remove(cachePath);
|
|
2224
|
+
fs_ensureDir(__WEBPACK_EXTERNAL_MODULE_node_path__.dirname(cachePath));
|
|
1950
2225
|
// Download and extract to cache path
|
|
1951
2226
|
await downloadAndExtract(url, cachePath);
|
|
1952
2227
|
// Generate a commit-like identifier from URL and version
|
|
@@ -1971,10 +2246,10 @@ ${CURSOR_BRIDGE_MARKER}
|
|
|
1971
2246
|
const cached = await this.get(parsed, version);
|
|
1972
2247
|
if (!cached) throw new Error(`Skill ${parsed.raw} version ${version} not found in cache`);
|
|
1973
2248
|
// If target exists, delete first
|
|
1974
|
-
if (
|
|
2249
|
+
if (fs_exists(destPath)) fs_remove(destPath);
|
|
1975
2250
|
// Use same exclude rules as Installer for consistency
|
|
1976
2251
|
copyDir(cached.path, destPath, {
|
|
1977
|
-
exclude:
|
|
2252
|
+
exclude: installer_DEFAULT_EXCLUDE_FILES
|
|
1978
2253
|
});
|
|
1979
2254
|
}
|
|
1980
2255
|
/**
|
|
@@ -1982,22 +2257,22 @@ ${CURSOR_BRIDGE_MARKER}
|
|
|
1982
2257
|
*/ clearSkill(parsed, version) {
|
|
1983
2258
|
if (version) {
|
|
1984
2259
|
const cachePath = this.getSkillCachePath(parsed, version);
|
|
1985
|
-
|
|
2260
|
+
fs_remove(cachePath);
|
|
1986
2261
|
} else {
|
|
1987
2262
|
// Clear all versions
|
|
1988
2263
|
const skillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cacheDir, parsed.registry, parsed.owner, parsed.repo);
|
|
1989
|
-
|
|
2264
|
+
fs_remove(skillDir);
|
|
1990
2265
|
}
|
|
1991
2266
|
}
|
|
1992
2267
|
/**
|
|
1993
2268
|
* Clear all cache
|
|
1994
2269
|
*/ clearAll() {
|
|
1995
|
-
|
|
2270
|
+
fs_remove(this.cacheDir);
|
|
1996
2271
|
}
|
|
1997
2272
|
/**
|
|
1998
2273
|
* Get cache statistics
|
|
1999
2274
|
*/ getStats() {
|
|
2000
|
-
if (!
|
|
2275
|
+
if (!fs_exists(this.cacheDir)) return {
|
|
2001
2276
|
totalSkills: 0,
|
|
2002
2277
|
registries: []
|
|
2003
2278
|
};
|
|
@@ -2151,7 +2426,7 @@ ${CURSOR_BRIDGE_MARKER}
|
|
|
2151
2426
|
/**
|
|
2152
2427
|
* Check if configuration file exists
|
|
2153
2428
|
*/ exists() {
|
|
2154
|
-
return
|
|
2429
|
+
return fs_exists(this.configPath);
|
|
2155
2430
|
}
|
|
2156
2431
|
/**
|
|
2157
2432
|
* Load configuration from file
|
|
@@ -3073,7 +3348,7 @@ ${CURSOR_BRIDGE_MARKER}
|
|
|
3073
3348
|
/**
|
|
3074
3349
|
* Check if lock file exists
|
|
3075
3350
|
*/ exists() {
|
|
3076
|
-
return
|
|
3351
|
+
return fs_exists(this.lockPath);
|
|
3077
3352
|
}
|
|
3078
3353
|
/**
|
|
3079
3354
|
* Load lock file
|
|
@@ -4064,11 +4339,11 @@ class RegistryResolver {
|
|
|
4064
4339
|
*/ getSkillPath(name) {
|
|
4065
4340
|
// Check canonical location first (.agents/skills/)
|
|
4066
4341
|
const canonicalPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.getCanonicalSkillsDir(), name);
|
|
4067
|
-
if (
|
|
4342
|
+
if (fs_exists(canonicalPath)) return canonicalPath;
|
|
4068
4343
|
// Check configured installation directory (.skills/ or custom)
|
|
4069
4344
|
const installDir = this.getInstallDir();
|
|
4070
4345
|
const installPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(installDir, name);
|
|
4071
|
-
if (
|
|
4346
|
+
if (fs_exists(installPath)) return installPath;
|
|
4072
4347
|
// Default to configured installation directory for new installations
|
|
4073
4348
|
// if it's not the default .skills, otherwise use canonical location.
|
|
4074
4349
|
// This respects "installDir" in skills.json.
|
|
@@ -4148,7 +4423,7 @@ class RegistryResolver {
|
|
|
4148
4423
|
const semanticVersion = metadata?.version ?? gitRef;
|
|
4149
4424
|
const skillPath = this.getSkillPath(skillName);
|
|
4150
4425
|
// Check if already installed (using the real name from SKILL.md)
|
|
4151
|
-
if (
|
|
4426
|
+
if (fs_exists(skillPath) && !force) {
|
|
4152
4427
|
const locked = this.lockManager.get(skillName);
|
|
4153
4428
|
// Compare ref if available, fallback to version for backward compatibility
|
|
4154
4429
|
const lockedRef = locked?.ref || locked?.version;
|
|
@@ -4165,10 +4440,10 @@ class RegistryResolver {
|
|
|
4165
4440
|
}
|
|
4166
4441
|
logger_logger["package"](`Installing ${skillName}@${gitRef}...`);
|
|
4167
4442
|
// Copy to installation directory
|
|
4168
|
-
|
|
4169
|
-
if (
|
|
4443
|
+
fs_ensureDir(this.getInstallDir());
|
|
4444
|
+
if (fs_exists(skillPath)) fs_remove(skillPath);
|
|
4170
4445
|
copyDir(sourcePath, skillPath, {
|
|
4171
|
-
exclude:
|
|
4446
|
+
exclude: installer_DEFAULT_EXCLUDE_FILES
|
|
4172
4447
|
});
|
|
4173
4448
|
// Update lock file (project mode only)
|
|
4174
4449
|
if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
|
|
@@ -4215,7 +4490,7 @@ class RegistryResolver {
|
|
|
4215
4490
|
const semanticVersion = metadata?.version ?? version;
|
|
4216
4491
|
const skillPath = this.getSkillPath(skillName);
|
|
4217
4492
|
// Check if already installed (using the real name from SKILL.md)
|
|
4218
|
-
if (
|
|
4493
|
+
if (fs_exists(skillPath) && !force) {
|
|
4219
4494
|
const locked = this.lockManager.get(skillName);
|
|
4220
4495
|
const lockedRef = locked?.ref || locked?.version;
|
|
4221
4496
|
if (locked && lockedRef === version) {
|
|
@@ -4231,8 +4506,8 @@ class RegistryResolver {
|
|
|
4231
4506
|
}
|
|
4232
4507
|
logger_logger["package"](`Installing ${skillName}@${version} from ${httpInfo.host}...`);
|
|
4233
4508
|
// Copy to installation directory
|
|
4234
|
-
|
|
4235
|
-
if (
|
|
4509
|
+
fs_ensureDir(this.getInstallDir());
|
|
4510
|
+
if (fs_exists(skillPath)) fs_remove(skillPath);
|
|
4236
4511
|
await this.cache.copyTo(parsed, version, skillPath);
|
|
4237
4512
|
// Update lock file (project mode only)
|
|
4238
4513
|
if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
|
|
@@ -4277,13 +4552,13 @@ class RegistryResolver {
|
|
|
4277
4552
|
* Uninstall skill
|
|
4278
4553
|
*/ uninstall(name) {
|
|
4279
4554
|
const skillPath = this.getSkillPath(name);
|
|
4280
|
-
if (!
|
|
4555
|
+
if (!fs_exists(skillPath)) {
|
|
4281
4556
|
const location = this.isGlobal ? '(global)' : '';
|
|
4282
4557
|
logger_logger.warn(`Skill ${name} is not installed ${location}`.trim());
|
|
4283
4558
|
return false;
|
|
4284
4559
|
}
|
|
4285
4560
|
// Remove installation directory
|
|
4286
|
-
|
|
4561
|
+
fs_remove(skillPath);
|
|
4287
4562
|
// Remove from lock file (project mode only)
|
|
4288
4563
|
if (!this.isGlobal) this.lockManager.remove(name);
|
|
4289
4564
|
// Remove from skills.json (project mode only)
|
|
@@ -4448,7 +4723,7 @@ class RegistryResolver {
|
|
|
4448
4723
|
const seenNames = new Set();
|
|
4449
4724
|
// Check canonical location first (.agents/skills/)
|
|
4450
4725
|
const canonicalDir = this.getCanonicalSkillsDir();
|
|
4451
|
-
if (
|
|
4726
|
+
if (fs_exists(canonicalDir)) for (const name of listDir(canonicalDir)){
|
|
4452
4727
|
const skillPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(canonicalDir, name);
|
|
4453
4728
|
if (!isDirectory(skillPath)) continue;
|
|
4454
4729
|
const skill = this.getInstalledSkillFromPath(name, skillPath);
|
|
@@ -4459,7 +4734,7 @@ class RegistryResolver {
|
|
|
4459
4734
|
}
|
|
4460
4735
|
// Check legacy location (.skills/)
|
|
4461
4736
|
const legacyDir = this.getInstallDir();
|
|
4462
|
-
if (
|
|
4737
|
+
if (fs_exists(legacyDir) && legacyDir !== canonicalDir) for (const name of listDir(legacyDir)){
|
|
4463
4738
|
// Skip if already found in canonical location
|
|
4464
4739
|
if (seenNames.has(name)) continue;
|
|
4465
4740
|
const skillPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(legacyDir, name);
|
|
@@ -4489,18 +4764,26 @@ class RegistryResolver {
|
|
|
4489
4764
|
const canonicalDir = this.getCanonicalSkillsDir();
|
|
4490
4765
|
const installed = [];
|
|
4491
4766
|
for (const [type, config] of Object.entries(agents)){
|
|
4767
|
+
if (type === claude_3p_installer_CLAUDE_COWORK_3P_AGENT) {
|
|
4768
|
+
try {
|
|
4769
|
+
if (fs_exists(getClaude3pSkillPath(skillName))) installed.push(type);
|
|
4770
|
+
} catch {
|
|
4771
|
+
// Claude Cowork 3P keeps skills under an app-managed account directory.
|
|
4772
|
+
}
|
|
4773
|
+
continue;
|
|
4774
|
+
}
|
|
4492
4775
|
const agentBase = this.isGlobal ? config.globalSkillsDir : __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.projectRoot, config.skillsDir);
|
|
4493
4776
|
// Skip agents whose skillsDir is the canonical directory itself
|
|
4494
4777
|
if (__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(agentBase) === __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(canonicalDir)) continue;
|
|
4495
4778
|
const agentSkillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(agentBase, skillName);
|
|
4496
|
-
if (
|
|
4779
|
+
if (fs_exists(agentSkillDir)) installed.push(type);
|
|
4497
4780
|
}
|
|
4498
4781
|
return installed;
|
|
4499
4782
|
}
|
|
4500
4783
|
/**
|
|
4501
4784
|
* Get installed skill information from a specific path
|
|
4502
4785
|
*/ getInstalledSkillFromPath(name, skillPath) {
|
|
4503
|
-
if (!
|
|
4786
|
+
if (!fs_exists(skillPath)) return null;
|
|
4504
4787
|
const isLinked = isSymlink(skillPath);
|
|
4505
4788
|
const locked = this.lockManager.get(name);
|
|
4506
4789
|
// Read metadata from SKILL.md (sole source per agentskills.io spec)
|
|
@@ -4523,10 +4806,10 @@ class RegistryResolver {
|
|
|
4523
4806
|
*/ getInstalledSkill(name) {
|
|
4524
4807
|
// Check canonical location first (.agents/skills/)
|
|
4525
4808
|
const canonicalPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.getCanonicalSkillsDir(), name);
|
|
4526
|
-
if (
|
|
4809
|
+
if (fs_exists(canonicalPath)) return this.getInstalledSkillFromPath(name, canonicalPath);
|
|
4527
4810
|
// Check legacy location (.skills/)
|
|
4528
4811
|
const legacyPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.getInstallDir(), name);
|
|
4529
|
-
if (
|
|
4812
|
+
if (fs_exists(legacyPath)) return this.getInstalledSkillFromPath(name, legacyPath);
|
|
4530
4813
|
return null;
|
|
4531
4814
|
}
|
|
4532
4815
|
/**
|
|
@@ -4937,7 +5220,7 @@ class RegistryResolver {
|
|
|
4937
5220
|
const { shortName, version, registryUrl: resolvedRegistryUrl, tarball, parsed: resolvedParsed } = resolved;
|
|
4938
5221
|
// 2. Check if already installed (skip if --force)
|
|
4939
5222
|
const skillPath = this.getSkillPath(shortName);
|
|
4940
|
-
if (
|
|
5223
|
+
if (fs_exists(skillPath) && !force) {
|
|
4941
5224
|
const locked = this.lockManager.get(shortName);
|
|
4942
5225
|
const lockedVersion = locked?.version;
|
|
4943
5226
|
// Same version already installed
|
|
@@ -4974,8 +5257,8 @@ class RegistryResolver {
|
|
|
4974
5257
|
logger_logger["package"](`Installing ${shortName}@${version} from ${resolvedRegistryUrl} to ${targetAgents.length} agent(s)...`);
|
|
4975
5258
|
// 3. Create temp directory for extraction (clean stale files first)
|
|
4976
5259
|
const tempDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cache.getCacheDir(), 'registry-temp', `${shortName}-${version}`);
|
|
4977
|
-
await
|
|
4978
|
-
await
|
|
5260
|
+
await fs_remove(tempDir);
|
|
5261
|
+
await fs_ensureDir(tempDir);
|
|
4979
5262
|
try {
|
|
4980
5263
|
// 4. Extract tarball
|
|
4981
5264
|
const extractedPath = await this.registryResolver.extract(tarball, tempDir);
|
|
@@ -5032,7 +5315,7 @@ class RegistryResolver {
|
|
|
5032
5315
|
};
|
|
5033
5316
|
} finally{
|
|
5034
5317
|
// Clean up temp directory after installation
|
|
5035
|
-
await
|
|
5318
|
+
await fs_remove(tempDir);
|
|
5036
5319
|
}
|
|
5037
5320
|
}
|
|
5038
5321
|
// ============================================================================
|
|
@@ -5151,8 +5434,8 @@ class RegistryResolver {
|
|
|
5151
5434
|
logger_logger["package"](`Installing ${shortName} from ${registryUrl} to ${targetAgents.length} agent(s)...`);
|
|
5152
5435
|
// Extract tarball to temp directory (clean stale files first)
|
|
5153
5436
|
const tempDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cache.getCacheDir(), 'registry-temp', `${shortName}-${version}`);
|
|
5154
|
-
await
|
|
5155
|
-
await
|
|
5437
|
+
await fs_remove(tempDir);
|
|
5438
|
+
await fs_ensureDir(tempDir);
|
|
5156
5439
|
try {
|
|
5157
5440
|
const extractedPath = await this.registryResolver.extract(tarball, tempDir);
|
|
5158
5441
|
logger_logger.debug(`Extracted to ${extractedPath}`);
|
|
@@ -5203,7 +5486,7 @@ class RegistryResolver {
|
|
|
5203
5486
|
};
|
|
5204
5487
|
} finally{
|
|
5205
5488
|
// Clean up temp directory after installation
|
|
5206
|
-
await
|
|
5489
|
+
await fs_remove(tempDir);
|
|
5207
5490
|
}
|
|
5208
5491
|
}
|
|
5209
5492
|
/**
|
|
@@ -9504,7 +9787,7 @@ async function publishAction(skillPath, options) {
|
|
|
9504
9787
|
const validation = validator.validate(absolutePath);
|
|
9505
9788
|
// 3.5. Content security scan
|
|
9506
9789
|
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(absolutePath, 'SKILL.md');
|
|
9507
|
-
if (
|
|
9790
|
+
if (fs_exists(skillMdPath)) {
|
|
9508
9791
|
const scanner = new ContentScanner();
|
|
9509
9792
|
const scanResult = scanner.scanFile(skillMdPath);
|
|
9510
9793
|
displayScanFindings(scanResult);
|