reskill 1.20.3 → 1.22.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 +2 -0
- package/README.zh-CN.md +2 -0
- package/dist/cli/commands/install.d.ts.map +1 -1
- package/dist/cli/commands/list.d.ts.map +1 -1
- package/dist/cli/index.js +1474 -1122
- 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 +17 -2
- package/dist/core/skill-manager.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1488 -1149
- 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',
|
|
@@ -306,1151 +926,788 @@ var external_node_fs_ = __webpack_require__("node:fs");
|
|
|
306
926
|
* File system utilities
|
|
307
927
|
*/ /**
|
|
308
928
|
* Check if a file or directory exists
|
|
309
|
-
*/ function
|
|
929
|
+
*/ function fs_exists(filePath) {
|
|
310
930
|
return external_node_fs_.existsSync(filePath);
|
|
311
931
|
}
|
|
312
932
|
/**
|
|
313
933
|
* Read JSON file
|
|
314
934
|
*/ 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 (!
|
|
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()
|
|
935
|
+
const content = external_node_fs_.readFileSync(filePath, 'utf-8');
|
|
936
|
+
return JSON.parse(content);
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
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
|
|
501
944
|
});
|
|
502
|
-
|
|
945
|
+
external_node_fs_.writeFileSync(filePath, `${JSON.stringify(data, null, indent)}\n`, 'utf-8');
|
|
503
946
|
}
|
|
504
947
|
/**
|
|
505
|
-
*
|
|
506
|
-
*/
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
}
|
|
948
|
+
* Create directory recursively
|
|
949
|
+
*/ function fs_ensureDir(dirPath) {
|
|
950
|
+
if (!fs_exists(dirPath)) external_node_fs_.mkdirSync(dirPath, {
|
|
951
|
+
recursive: true
|
|
952
|
+
});
|
|
532
953
|
}
|
|
533
954
|
/**
|
|
534
|
-
*
|
|
535
|
-
*/
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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);
|
|
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
|
|
543
960
|
});
|
|
544
|
-
return sortedTags[0];
|
|
545
961
|
}
|
|
546
962
|
/**
|
|
547
|
-
*
|
|
963
|
+
* Copy directory recursively
|
|
548
964
|
*
|
|
549
|
-
* @
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
|
|
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);
|
|
561
983
|
}
|
|
562
984
|
}
|
|
563
985
|
/**
|
|
564
|
-
*
|
|
565
|
-
*/
|
|
566
|
-
return
|
|
567
|
-
|
|
568
|
-
'HEAD'
|
|
569
|
-
], cwd);
|
|
986
|
+
* List directory contents
|
|
987
|
+
*/ function listDir(dirPath) {
|
|
988
|
+
if (!fs_exists(dirPath)) return [];
|
|
989
|
+
return external_node_fs_.readdirSync(dirPath);
|
|
570
990
|
}
|
|
571
991
|
/**
|
|
572
|
-
*
|
|
573
|
-
*/
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
'ls-remote',
|
|
577
|
-
'--symref',
|
|
578
|
-
repoUrl,
|
|
579
|
-
'HEAD'
|
|
580
|
-
]);
|
|
581
|
-
const match = output.match(/ref: refs\/heads\/(\S+)/);
|
|
582
|
-
return match ? match[1] : 'main';
|
|
583
|
-
} catch {
|
|
584
|
-
return 'main';
|
|
585
|
-
}
|
|
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();
|
|
586
996
|
}
|
|
587
997
|
/**
|
|
588
|
-
*
|
|
589
|
-
*/ function
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const maxLength = Math.max(aParts.length, bParts.length);
|
|
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;
|
|
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();
|
|
600
1002
|
}
|
|
601
1003
|
/**
|
|
602
|
-
*
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
* - SSH: git@github.com:user/repo.git
|
|
606
|
-
* - HTTPS: https://github.com/user/repo.git
|
|
607
|
-
* - Git protocol: git://github.com/user/repo.git
|
|
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');
|
|
1004
|
+
* Get real path of symbolic link
|
|
1005
|
+
*/ function getRealPath(linkPath) {
|
|
1006
|
+
return external_node_fs_.realpathSync(linkPath);
|
|
612
1007
|
}
|
|
613
1008
|
/**
|
|
614
|
-
*
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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.
|
|
623
|
-
*
|
|
624
|
-
* @param url The Git URL to parse
|
|
625
|
-
* @returns Parsed URL information or null if parsing fails
|
|
626
|
-
*/ function parseGitUrl(url) {
|
|
627
|
-
// Remove trailing .git if present
|
|
628
|
-
const cleanUrl = url.replace(/\.git$/, '');
|
|
629
|
-
// SSH format: git@github.com:user/repo
|
|
630
|
-
const sshMatch = cleanUrl.match(/^git@([^:]+):(.+)$/);
|
|
631
|
-
if (sshMatch) {
|
|
632
|
-
const [, host, path] = sshMatch;
|
|
633
|
-
const parts = path.split('/');
|
|
634
|
-
if (parts.length >= 2) {
|
|
635
|
-
// Handle nested paths like org/sub/repo
|
|
636
|
-
const owner = parts.slice(0, -1).join('/');
|
|
637
|
-
const repo = parts[parts.length - 1];
|
|
638
|
-
return {
|
|
639
|
-
host,
|
|
640
|
-
owner,
|
|
641
|
-
repo,
|
|
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
|
-
}
|
|
682
|
-
}
|
|
683
|
-
return null;
|
|
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');
|
|
684
1013
|
}
|
|
685
1014
|
/**
|
|
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
|
-
};
|
|
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');
|
|
1019
|
+
}
|
|
706
1020
|
/**
|
|
707
|
-
* Get
|
|
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;
|
|
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');
|
|
722
1025
|
}
|
|
723
1026
|
/**
|
|
724
|
-
* Get
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
* @param customRegistries - Optional custom scope-to-registry mapping (from skills.json)
|
|
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;
|
|
1027
|
+
* Get home directory
|
|
1028
|
+
*/ function getHomeDir() {
|
|
1029
|
+
return process.env.HOME || process.env.USERPROFILE || '';
|
|
749
1030
|
}
|
|
750
1031
|
/**
|
|
751
|
-
*
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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;
|
|
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;
|
|
779
1039
|
}
|
|
1040
|
+
const git_execAsync = (0, __WEBPACK_EXTERNAL_MODULE_node_util__.promisify)(__WEBPACK_EXTERNAL_MODULE_node_child_process__.exec);
|
|
780
1041
|
/**
|
|
781
|
-
*
|
|
782
|
-
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
|
|
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
|
-
};
|
|
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() {
|
|
800
1053
|
return {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1054
|
+
...process.env,
|
|
1055
|
+
GIT_SSH_COMMAND,
|
|
1056
|
+
// Disable interactive prompts for HTTPS as well
|
|
1057
|
+
GIT_TERMINAL_PROMPT: '0'
|
|
804
1058
|
};
|
|
805
1059
|
}
|
|
806
1060
|
/**
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
+
}
|
|
822
1113
|
}
|
|
823
1114
|
/**
|
|
824
|
-
*
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
*/ function getShortName(skillName) {
|
|
833
|
-
return parseSkillName(skillName).name;
|
|
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();
|
|
834
1123
|
}
|
|
835
1124
|
/**
|
|
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
|
-
};
|
|
1125
|
+
* Get remote tags for a repository
|
|
1126
|
+
*/ async function getRemoteTags(repoUrl) {
|
|
1127
|
+
try {
|
|
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
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
return tags;
|
|
1149
|
+
} catch {
|
|
1150
|
+
return [];
|
|
881
1151
|
}
|
|
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;
|
|
887
|
-
return {
|
|
888
|
-
scope: null,
|
|
889
|
-
name,
|
|
890
|
-
version: version || void 0,
|
|
891
|
-
fullName: name
|
|
892
|
-
};
|
|
893
1152
|
}
|
|
894
1153
|
/**
|
|
895
|
-
*
|
|
896
|
-
*/
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
this.statusCode = statusCode;
|
|
907
|
-
this.originalError = originalError;
|
|
908
|
-
}
|
|
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];
|
|
909
1165
|
}
|
|
910
1166
|
/**
|
|
911
|
-
*
|
|
1167
|
+
* Clone a repository with shallow clone
|
|
912
1168
|
*
|
|
913
|
-
* @
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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);
|
|
920
1177
|
try {
|
|
921
|
-
|
|
922
|
-
const controller = new AbortController();
|
|
923
|
-
const timeoutId = setTimeout(()=>controller.abort(), timeout);
|
|
924
|
-
const response = await fetch(url, {
|
|
925
|
-
signal: controller.signal,
|
|
926
|
-
headers: {
|
|
927
|
-
'User-Agent': 'reskill/1.0',
|
|
928
|
-
...headers
|
|
929
|
-
}
|
|
930
|
-
});
|
|
931
|
-
clearTimeout(timeoutId);
|
|
932
|
-
if (!response.ok) throw new HttpDownloadError(url, `HTTP ${response.status}: ${response.statusText}`, response.status);
|
|
933
|
-
// Stream response to file
|
|
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);
|
|
1178
|
+
await git(args);
|
|
941
1179
|
} catch (error) {
|
|
942
|
-
|
|
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);
|
|
1180
|
+
throw new GitCloneError(repoUrl, error);
|
|
948
1181
|
}
|
|
949
1182
|
}
|
|
950
1183
|
/**
|
|
951
|
-
*
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
// Auto-detect format from extension
|
|
958
|
-
const detectedFormat = format || detectArchiveFormat(archivePath);
|
|
959
|
-
if (!detectedFormat) throw new Error(`Unable to detect archive format for: ${archivePath}`);
|
|
960
|
-
// Ensure destination directory exists
|
|
961
|
-
ensureDir(destDir);
|
|
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
|
-
}
|
|
1184
|
+
* Get current commit hash
|
|
1185
|
+
*/ async function getCurrentCommit(cwd) {
|
|
1186
|
+
return git([
|
|
1187
|
+
'rev-parse',
|
|
1188
|
+
'HEAD'
|
|
1189
|
+
], cwd);
|
|
974
1190
|
}
|
|
975
1191
|
/**
|
|
976
|
-
*
|
|
977
|
-
*/ async function
|
|
978
|
-
const { exec } = await import("node:child_process");
|
|
979
|
-
const { promisify } = await import("node:util");
|
|
980
|
-
const execAsync = promisify(exec);
|
|
981
|
-
const flags = gzipped ? '-xzf' : '-xf';
|
|
1192
|
+
* Get default branch name
|
|
1193
|
+
*/ async function getDefaultBranch(repoUrl) {
|
|
982
1194
|
try {
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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);
|
|
1012
|
-
} catch (error) {
|
|
1013
|
-
throw new Error(`Failed to extract tar archive: ${error.message}`);
|
|
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';
|
|
1014
1205
|
}
|
|
1015
1206
|
}
|
|
1016
1207
|
/**
|
|
1017
|
-
*
|
|
1018
|
-
*/
|
|
1019
|
-
const
|
|
1020
|
-
const
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
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/';
|
|
1314
|
+
/**
|
|
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
|
|
1328
|
+
*
|
|
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;
|
|
1065
1342
|
}
|
|
1066
1343
|
/**
|
|
1067
|
-
*
|
|
1344
|
+
* Get the registry URL for a given scope (reverse lookup)
|
|
1068
1345
|
*
|
|
1069
|
-
* @param
|
|
1070
|
-
* @param
|
|
1071
|
-
* @
|
|
1072
|
-
*
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
if (!
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
return
|
|
1087
|
-
} finally{
|
|
1088
|
-
// Clean up temp archive
|
|
1089
|
-
if (external_node_fs_.existsSync(tempArchive)) external_node_fs_.unlinkSync(tempArchive);
|
|
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
|
|
1349
|
+
*
|
|
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}/`;
|
|
1090
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;
|
|
1091
1369
|
}
|
|
1092
1370
|
/**
|
|
1093
|
-
*
|
|
1371
|
+
* Get the registry URL for a given scope
|
|
1094
1372
|
*
|
|
1095
|
-
*
|
|
1373
|
+
* - With scope → lookup private Registry (throws if not found)
|
|
1374
|
+
* - Without scope (null/undefined/'') → returns public Registry
|
|
1096
1375
|
*
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1100
|
-
*
|
|
1101
|
-
*
|
|
1102
|
-
|
|
1103
|
-
*
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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.`);
|
|
1109
1397
|
}
|
|
1398
|
+
return registry;
|
|
1110
1399
|
}
|
|
1111
1400
|
/**
|
|
1112
|
-
*
|
|
1113
|
-
* Parses --- delimited YAML header
|
|
1401
|
+
* Parse a skill name into its components
|
|
1114
1402
|
*
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
1118
|
-
*
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
let currentKey = '';
|
|
1133
|
-
let currentValue = '';
|
|
1134
|
-
let inMultiline = false;
|
|
1135
|
-
let inNestedObject = false;
|
|
1136
|
-
let inPlainScalar = false;
|
|
1137
|
-
let nestedObject = {};
|
|
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();
|
|
1210
|
-
}
|
|
1211
|
-
// Save last accumulated value
|
|
1212
|
-
flushCurrent();
|
|
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
|
+
};
|
|
1213
1420
|
return {
|
|
1214
|
-
|
|
1215
|
-
|
|
1421
|
+
scope: null,
|
|
1422
|
+
name: skillName,
|
|
1423
|
+
fullName: skillName
|
|
1216
1424
|
};
|
|
1217
1425
|
}
|
|
1218
1426
|
/**
|
|
1219
|
-
*
|
|
1220
|
-
*/ function parseYamlValue(value) {
|
|
1221
|
-
if (!value) return '';
|
|
1222
|
-
// Boolean value
|
|
1223
|
-
if ('true' === value) return true;
|
|
1224
|
-
if ('false' === value) return false;
|
|
1225
|
-
// Number
|
|
1226
|
-
if (/^-?\d+$/.test(value)) return parseInt(value, 10);
|
|
1227
|
-
if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
|
|
1228
|
-
// Remove quotes
|
|
1229
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
1230
|
-
return value;
|
|
1231
|
-
}
|
|
1232
|
-
/**
|
|
1233
|
-
* Validate skill name format
|
|
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
|
|
@@ -4033,6 +4308,16 @@ class RegistryResolver {
|
|
|
4033
4308
|
return this.isGlobal;
|
|
4034
4309
|
}
|
|
4035
4310
|
/**
|
|
4311
|
+
* Determine if the installation is effectively global.
|
|
4312
|
+
*
|
|
4313
|
+
* Claude Cowork 3P always installs to a global app-managed directory regardless
|
|
4314
|
+
* of the isGlobal flag. When all target agents are claude-cowork-3p, the
|
|
4315
|
+
* installation should be treated as global (skip skills.json/skills.lock writes).
|
|
4316
|
+
*/ isEffectivelyGlobal(targetAgents) {
|
|
4317
|
+
if (this.isGlobal) return true;
|
|
4318
|
+
return targetAgents.length > 0 && targetAgents.every((a)=>a === claude_3p_installer_CLAUDE_COWORK_3P_AGENT);
|
|
4319
|
+
}
|
|
4320
|
+
/**
|
|
4036
4321
|
* Get project root directory
|
|
4037
4322
|
*/ getProjectRoot() {
|
|
4038
4323
|
return this.projectRoot;
|
|
@@ -4064,11 +4349,11 @@ class RegistryResolver {
|
|
|
4064
4349
|
*/ getSkillPath(name) {
|
|
4065
4350
|
// Check canonical location first (.agents/skills/)
|
|
4066
4351
|
const canonicalPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.getCanonicalSkillsDir(), name);
|
|
4067
|
-
if (
|
|
4352
|
+
if (fs_exists(canonicalPath)) return canonicalPath;
|
|
4068
4353
|
// Check configured installation directory (.skills/ or custom)
|
|
4069
4354
|
const installDir = this.getInstallDir();
|
|
4070
4355
|
const installPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(installDir, name);
|
|
4071
|
-
if (
|
|
4356
|
+
if (fs_exists(installPath)) return installPath;
|
|
4072
4357
|
// Default to configured installation directory for new installations
|
|
4073
4358
|
// if it's not the default .skills, otherwise use canonical location.
|
|
4074
4359
|
// This respects "installDir" in skills.json.
|
|
@@ -4148,7 +4433,7 @@ class RegistryResolver {
|
|
|
4148
4433
|
const semanticVersion = metadata?.version ?? gitRef;
|
|
4149
4434
|
const skillPath = this.getSkillPath(skillName);
|
|
4150
4435
|
// Check if already installed (using the real name from SKILL.md)
|
|
4151
|
-
if (
|
|
4436
|
+
if (fs_exists(skillPath) && !force) {
|
|
4152
4437
|
const locked = this.lockManager.get(skillName);
|
|
4153
4438
|
// Compare ref if available, fallback to version for backward compatibility
|
|
4154
4439
|
const lockedRef = locked?.ref || locked?.version;
|
|
@@ -4165,10 +4450,10 @@ class RegistryResolver {
|
|
|
4165
4450
|
}
|
|
4166
4451
|
logger_logger["package"](`Installing ${skillName}@${gitRef}...`);
|
|
4167
4452
|
// Copy to installation directory
|
|
4168
|
-
|
|
4169
|
-
if (
|
|
4453
|
+
fs_ensureDir(this.getInstallDir());
|
|
4454
|
+
if (fs_exists(skillPath)) fs_remove(skillPath);
|
|
4170
4455
|
copyDir(sourcePath, skillPath, {
|
|
4171
|
-
exclude:
|
|
4456
|
+
exclude: installer_DEFAULT_EXCLUDE_FILES
|
|
4172
4457
|
});
|
|
4173
4458
|
// Update lock file (project mode only)
|
|
4174
4459
|
if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
|
|
@@ -4215,7 +4500,7 @@ class RegistryResolver {
|
|
|
4215
4500
|
const semanticVersion = metadata?.version ?? version;
|
|
4216
4501
|
const skillPath = this.getSkillPath(skillName);
|
|
4217
4502
|
// Check if already installed (using the real name from SKILL.md)
|
|
4218
|
-
if (
|
|
4503
|
+
if (fs_exists(skillPath) && !force) {
|
|
4219
4504
|
const locked = this.lockManager.get(skillName);
|
|
4220
4505
|
const lockedRef = locked?.ref || locked?.version;
|
|
4221
4506
|
if (locked && lockedRef === version) {
|
|
@@ -4231,8 +4516,8 @@ class RegistryResolver {
|
|
|
4231
4516
|
}
|
|
4232
4517
|
logger_logger["package"](`Installing ${skillName}@${version} from ${httpInfo.host}...`);
|
|
4233
4518
|
// Copy to installation directory
|
|
4234
|
-
|
|
4235
|
-
if (
|
|
4519
|
+
fs_ensureDir(this.getInstallDir());
|
|
4520
|
+
if (fs_exists(skillPath)) fs_remove(skillPath);
|
|
4236
4521
|
await this.cache.copyTo(parsed, version, skillPath);
|
|
4237
4522
|
// Update lock file (project mode only)
|
|
4238
4523
|
if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
|
|
@@ -4277,13 +4562,13 @@ class RegistryResolver {
|
|
|
4277
4562
|
* Uninstall skill
|
|
4278
4563
|
*/ uninstall(name) {
|
|
4279
4564
|
const skillPath = this.getSkillPath(name);
|
|
4280
|
-
if (!
|
|
4565
|
+
if (!fs_exists(skillPath)) {
|
|
4281
4566
|
const location = this.isGlobal ? '(global)' : '';
|
|
4282
4567
|
logger_logger.warn(`Skill ${name} is not installed ${location}`.trim());
|
|
4283
4568
|
return false;
|
|
4284
4569
|
}
|
|
4285
4570
|
// Remove installation directory
|
|
4286
|
-
|
|
4571
|
+
fs_remove(skillPath);
|
|
4287
4572
|
// Remove from lock file (project mode only)
|
|
4288
4573
|
if (!this.isGlobal) this.lockManager.remove(name);
|
|
4289
4574
|
// Remove from skills.json (project mode only)
|
|
@@ -4442,13 +4727,15 @@ class RegistryResolver {
|
|
|
4442
4727
|
/**
|
|
4443
4728
|
* List installed skills
|
|
4444
4729
|
*
|
|
4445
|
-
*
|
|
4446
|
-
|
|
4730
|
+
* When `agent` is specified, lists skills installed to that specific agent.
|
|
4731
|
+
* Otherwise checks both canonical (.agents/skills/) and legacy (.skills/) locations.
|
|
4732
|
+
*/ list(options) {
|
|
4733
|
+
if (options?.agent) return this.listByAgent(options.agent);
|
|
4447
4734
|
const skills = [];
|
|
4448
4735
|
const seenNames = new Set();
|
|
4449
4736
|
// Check canonical location first (.agents/skills/)
|
|
4450
4737
|
const canonicalDir = this.getCanonicalSkillsDir();
|
|
4451
|
-
if (
|
|
4738
|
+
if (fs_exists(canonicalDir)) for (const name of listDir(canonicalDir)){
|
|
4452
4739
|
const skillPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(canonicalDir, name);
|
|
4453
4740
|
if (!isDirectory(skillPath)) continue;
|
|
4454
4741
|
const skill = this.getInstalledSkillFromPath(name, skillPath);
|
|
@@ -4459,7 +4746,7 @@ class RegistryResolver {
|
|
|
4459
4746
|
}
|
|
4460
4747
|
// Check legacy location (.skills/)
|
|
4461
4748
|
const legacyDir = this.getInstallDir();
|
|
4462
|
-
if (
|
|
4749
|
+
if (fs_exists(legacyDir) && legacyDir !== canonicalDir) for (const name of listDir(legacyDir)){
|
|
4463
4750
|
// Skip if already found in canonical location
|
|
4464
4751
|
if (seenNames.has(name)) continue;
|
|
4465
4752
|
const skillPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(legacyDir, name);
|
|
@@ -4477,6 +4764,38 @@ class RegistryResolver {
|
|
|
4477
4764
|
seenNames.add(name);
|
|
4478
4765
|
}
|
|
4479
4766
|
}
|
|
4767
|
+
// In global mode, also include claude-cowork-3p skills (always global)
|
|
4768
|
+
if (this.isGlobal) try {
|
|
4769
|
+
for (const name of listClaude3pSkills()){
|
|
4770
|
+
if (seenNames.has(name)) continue;
|
|
4771
|
+
const skillPath = getClaude3pSkillPath(name);
|
|
4772
|
+
const skill = this.getInstalledSkillFromPath(name, skillPath);
|
|
4773
|
+
if (skill) {
|
|
4774
|
+
skills.push(skill);
|
|
4775
|
+
seenNames.add(name);
|
|
4776
|
+
}
|
|
4777
|
+
}
|
|
4778
|
+
} catch {
|
|
4779
|
+
// Claude Cowork 3P not configured or accessible — skip silently
|
|
4780
|
+
}
|
|
4781
|
+
return skills;
|
|
4782
|
+
}
|
|
4783
|
+
/**
|
|
4784
|
+
* List skills installed to a specific agent
|
|
4785
|
+
*/ listByAgent(agent) {
|
|
4786
|
+
const installer = new Installer({
|
|
4787
|
+
cwd: this.projectRoot,
|
|
4788
|
+
global: this.isGlobal
|
|
4789
|
+
});
|
|
4790
|
+
const skillNames = installer.listInstalledSkills(agent);
|
|
4791
|
+
const skills = [];
|
|
4792
|
+
for (const name of skillNames)try {
|
|
4793
|
+
const skillPath = installer.getAgentSkillPath(name, agent);
|
|
4794
|
+
const skill = this.getInstalledSkillFromPath(name, skillPath);
|
|
4795
|
+
if (skill) skills.push(skill);
|
|
4796
|
+
} catch {
|
|
4797
|
+
// Skip skills whose paths cannot be resolved (e.g. claude-3p not configured)
|
|
4798
|
+
}
|
|
4480
4799
|
return skills;
|
|
4481
4800
|
}
|
|
4482
4801
|
/**
|
|
@@ -4489,18 +4808,26 @@ class RegistryResolver {
|
|
|
4489
4808
|
const canonicalDir = this.getCanonicalSkillsDir();
|
|
4490
4809
|
const installed = [];
|
|
4491
4810
|
for (const [type, config] of Object.entries(agents)){
|
|
4811
|
+
if (type === claude_3p_installer_CLAUDE_COWORK_3P_AGENT) {
|
|
4812
|
+
try {
|
|
4813
|
+
if (fs_exists(getClaude3pSkillPath(skillName))) installed.push(type);
|
|
4814
|
+
} catch {
|
|
4815
|
+
// Claude Cowork 3P keeps skills under an app-managed account directory.
|
|
4816
|
+
}
|
|
4817
|
+
continue;
|
|
4818
|
+
}
|
|
4492
4819
|
const agentBase = this.isGlobal ? config.globalSkillsDir : __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.projectRoot, config.skillsDir);
|
|
4493
4820
|
// Skip agents whose skillsDir is the canonical directory itself
|
|
4494
4821
|
if (__WEBPACK_EXTERNAL_MODULE_node_path__.resolve(agentBase) === __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(canonicalDir)) continue;
|
|
4495
4822
|
const agentSkillDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(agentBase, skillName);
|
|
4496
|
-
if (
|
|
4823
|
+
if (fs_exists(agentSkillDir)) installed.push(type);
|
|
4497
4824
|
}
|
|
4498
4825
|
return installed;
|
|
4499
4826
|
}
|
|
4500
4827
|
/**
|
|
4501
4828
|
* Get installed skill information from a specific path
|
|
4502
4829
|
*/ getInstalledSkillFromPath(name, skillPath) {
|
|
4503
|
-
if (!
|
|
4830
|
+
if (!fs_exists(skillPath)) return null;
|
|
4504
4831
|
const isLinked = isSymlink(skillPath);
|
|
4505
4832
|
const locked = this.lockManager.get(name);
|
|
4506
4833
|
// Read metadata from SKILL.md (sole source per agentskills.io spec)
|
|
@@ -4523,10 +4850,10 @@ class RegistryResolver {
|
|
|
4523
4850
|
*/ getInstalledSkill(name) {
|
|
4524
4851
|
// Check canonical location first (.agents/skills/)
|
|
4525
4852
|
const canonicalPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.getCanonicalSkillsDir(), name);
|
|
4526
|
-
if (
|
|
4853
|
+
if (fs_exists(canonicalPath)) return this.getInstalledSkillFromPath(name, canonicalPath);
|
|
4527
4854
|
// Check legacy location (.skills/)
|
|
4528
4855
|
const legacyPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.getInstallDir(), name);
|
|
4529
|
-
if (
|
|
4856
|
+
if (fs_exists(legacyPath)) return this.getInstalledSkillFromPath(name, legacyPath);
|
|
4530
4857
|
return null;
|
|
4531
4858
|
}
|
|
4532
4859
|
/**
|
|
@@ -4722,14 +5049,15 @@ class RegistryResolver {
|
|
|
4722
5049
|
const results = await installer.installToAgents(skillInfo.dirPath, skillInfo.name, targetAgents, {
|
|
4723
5050
|
mode: mode
|
|
4724
5051
|
});
|
|
4725
|
-
|
|
5052
|
+
const effectivelyGlobal = this.isEffectivelyGlobal(targetAgents);
|
|
5053
|
+
if (!effectivelyGlobal) this.lockManager.lockSkill(skillInfo.name, {
|
|
4726
5054
|
source: skillSource,
|
|
4727
5055
|
version: semanticVersion,
|
|
4728
5056
|
ref: gitRef,
|
|
4729
5057
|
resolved: repoUrl,
|
|
4730
5058
|
commit: cacheResult.commit
|
|
4731
5059
|
});
|
|
4732
|
-
if (!
|
|
5060
|
+
if (!effectivelyGlobal && save) {
|
|
4733
5061
|
this.config.ensureExists();
|
|
4734
5062
|
this.config.addSkill(skillInfo.name, `${baseRefForSave}#${skillInfo.name}`);
|
|
4735
5063
|
}
|
|
@@ -4787,8 +5115,9 @@ class RegistryResolver {
|
|
|
4787
5115
|
const results = await installer.installToAgents(sourcePath, skillName, targetAgents, {
|
|
4788
5116
|
mode: mode
|
|
4789
5117
|
});
|
|
4790
|
-
// Update lock file (project mode only)
|
|
4791
|
-
|
|
5118
|
+
// Update lock file (project mode only, skip for effectively-global installs)
|
|
5119
|
+
const effectivelyGlobal = this.isEffectivelyGlobal(targetAgents);
|
|
5120
|
+
if (!effectivelyGlobal) {
|
|
4792
5121
|
const lockSource = registryContext?.lockSource ?? `${parsed.registry}:${parsed.owner}/${parsed.repo}${parsed.subPath ? `/${parsed.subPath}` : ''}`;
|
|
4793
5122
|
this.lockManager.lockSkill(skillName, {
|
|
4794
5123
|
source: lockSource,
|
|
@@ -4799,8 +5128,8 @@ class RegistryResolver {
|
|
|
4799
5128
|
registry: registryContext?.registryUrl
|
|
4800
5129
|
});
|
|
4801
5130
|
}
|
|
4802
|
-
// Update skills.json (project mode only)
|
|
4803
|
-
if (!
|
|
5131
|
+
// Update skills.json (project mode only, skip for effectively-global installs)
|
|
5132
|
+
if (!effectivelyGlobal && save) {
|
|
4804
5133
|
this.config.ensureExists();
|
|
4805
5134
|
const configRef = registryContext?.configRef ?? this.config.normalizeSkillRef(ref);
|
|
4806
5135
|
this.config.addSkill(skillName, configRef);
|
|
@@ -4858,8 +5187,9 @@ class RegistryResolver {
|
|
|
4858
5187
|
const results = await installer.installToAgents(sourcePath, skillName, targetAgents, {
|
|
4859
5188
|
mode: mode
|
|
4860
5189
|
});
|
|
4861
|
-
// Update lock file (project mode only)
|
|
4862
|
-
|
|
5190
|
+
// Update lock file (project mode only, skip for effectively-global installs)
|
|
5191
|
+
const effectivelyGlobal = this.isEffectivelyGlobal(targetAgents);
|
|
5192
|
+
if (!effectivelyGlobal) {
|
|
4863
5193
|
const lockSource = registryContext?.lockSource ?? `http:${httpInfo.host}/${skillName}`;
|
|
4864
5194
|
this.lockManager.lockSkill(skillName, {
|
|
4865
5195
|
source: lockSource,
|
|
@@ -4870,8 +5200,8 @@ class RegistryResolver {
|
|
|
4870
5200
|
registry: registryContext?.registryUrl
|
|
4871
5201
|
});
|
|
4872
5202
|
}
|
|
4873
|
-
// Update skills.json (project mode only)
|
|
4874
|
-
if (!
|
|
5203
|
+
// Update skills.json (project mode only, skip for effectively-global installs)
|
|
5204
|
+
if (!effectivelyGlobal && save) {
|
|
4875
5205
|
this.config.ensureExists();
|
|
4876
5206
|
const configRef = registryContext?.configRef ?? ref;
|
|
4877
5207
|
this.config.addSkill(skillName, configRef);
|
|
@@ -4937,7 +5267,7 @@ class RegistryResolver {
|
|
|
4937
5267
|
const { shortName, version, registryUrl: resolvedRegistryUrl, tarball, parsed: resolvedParsed } = resolved;
|
|
4938
5268
|
// 2. Check if already installed (skip if --force)
|
|
4939
5269
|
const skillPath = this.getSkillPath(shortName);
|
|
4940
|
-
if (
|
|
5270
|
+
if (fs_exists(skillPath) && !force) {
|
|
4941
5271
|
const locked = this.lockManager.get(shortName);
|
|
4942
5272
|
const lockedVersion = locked?.version;
|
|
4943
5273
|
// Same version already installed
|
|
@@ -4974,8 +5304,8 @@ class RegistryResolver {
|
|
|
4974
5304
|
logger_logger["package"](`Installing ${shortName}@${version} from ${resolvedRegistryUrl} to ${targetAgents.length} agent(s)...`);
|
|
4975
5305
|
// 3. Create temp directory for extraction (clean stale files first)
|
|
4976
5306
|
const tempDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cache.getCacheDir(), 'registry-temp', `${shortName}-${version}`);
|
|
4977
|
-
await
|
|
4978
|
-
await
|
|
5307
|
+
await fs_remove(tempDir);
|
|
5308
|
+
await fs_ensureDir(tempDir);
|
|
4979
5309
|
try {
|
|
4980
5310
|
// 4. Extract tarball
|
|
4981
5311
|
const extractedPath = await this.registryResolver.extract(tarball, tempDir);
|
|
@@ -4991,8 +5321,9 @@ class RegistryResolver {
|
|
|
4991
5321
|
const results = await installer.installToAgents(extractedPath, shortName, targetAgents, {
|
|
4992
5322
|
mode: mode
|
|
4993
5323
|
});
|
|
4994
|
-
// 7. Update lock file (project mode only)
|
|
4995
|
-
|
|
5324
|
+
// 7. Update lock file (project mode only, skip for effectively-global installs)
|
|
5325
|
+
const effectivelyGlobal = this.isEffectivelyGlobal(targetAgents);
|
|
5326
|
+
if (!effectivelyGlobal) this.lockManager.lockSkill(shortName, {
|
|
4996
5327
|
source: `registry:${resolvedParsed.fullName}`,
|
|
4997
5328
|
version,
|
|
4998
5329
|
ref: version,
|
|
@@ -5000,8 +5331,8 @@ class RegistryResolver {
|
|
|
5000
5331
|
commit: resolved.integrity,
|
|
5001
5332
|
registry: resolvedRegistryUrl
|
|
5002
5333
|
});
|
|
5003
|
-
// 8. Update skills.json (project mode only)
|
|
5004
|
-
if (!
|
|
5334
|
+
// 8. Update skills.json (project mode only, skip for effectively-global installs)
|
|
5335
|
+
if (!effectivelyGlobal && save) {
|
|
5005
5336
|
this.config.ensureExists();
|
|
5006
5337
|
// Save with full name for registry skills
|
|
5007
5338
|
this.config.addSkill(shortName, ref);
|
|
@@ -5032,7 +5363,7 @@ class RegistryResolver {
|
|
|
5032
5363
|
};
|
|
5033
5364
|
} finally{
|
|
5034
5365
|
// Clean up temp directory after installation
|
|
5035
|
-
await
|
|
5366
|
+
await fs_remove(tempDir);
|
|
5036
5367
|
}
|
|
5037
5368
|
}
|
|
5038
5369
|
// ============================================================================
|
|
@@ -5067,7 +5398,8 @@ class RegistryResolver {
|
|
|
5067
5398
|
registryContext
|
|
5068
5399
|
};
|
|
5069
5400
|
// Save custom registry to skills.json.registries (for reinstall without lock file)
|
|
5070
|
-
|
|
5401
|
+
const effectivelyGlobal = this.isEffectivelyGlobal(targetAgents);
|
|
5402
|
+
if (!effectivelyGlobal && options.registry) {
|
|
5071
5403
|
const registryName = this.deriveRegistryName(options.registry);
|
|
5072
5404
|
if (registryName) {
|
|
5073
5405
|
this.config.ensureExists();
|
|
@@ -5151,8 +5483,8 @@ class RegistryResolver {
|
|
|
5151
5483
|
logger_logger["package"](`Installing ${shortName} from ${registryUrl} to ${targetAgents.length} agent(s)...`);
|
|
5152
5484
|
// Extract tarball to temp directory (clean stale files first)
|
|
5153
5485
|
const tempDir = __WEBPACK_EXTERNAL_MODULE_node_path__.join(this.cache.getCacheDir(), 'registry-temp', `${shortName}-${version}`);
|
|
5154
|
-
await
|
|
5155
|
-
await
|
|
5486
|
+
await fs_remove(tempDir);
|
|
5487
|
+
await fs_ensureDir(tempDir);
|
|
5156
5488
|
try {
|
|
5157
5489
|
const extractedPath = await this.registryResolver.extract(tarball, tempDir);
|
|
5158
5490
|
logger_logger.debug(`Extracted to ${extractedPath}`);
|
|
@@ -5170,8 +5502,9 @@ class RegistryResolver {
|
|
|
5170
5502
|
const metadata = this.getSkillMetadataFromDir(extractedPath);
|
|
5171
5503
|
const skillName = metadata?.name ?? shortName;
|
|
5172
5504
|
const semanticVersion = metadata?.version ?? version;
|
|
5173
|
-
// Update lock file (project mode only)
|
|
5174
|
-
|
|
5505
|
+
// Update lock file (project mode only, skip for effectively-global installs)
|
|
5506
|
+
const effectivelyGlobal = this.isEffectivelyGlobal(targetAgents);
|
|
5507
|
+
if (!effectivelyGlobal) this.lockManager.lockSkill(skillName, {
|
|
5175
5508
|
source: `registry:${parsed.fullName}`,
|
|
5176
5509
|
version: semanticVersion,
|
|
5177
5510
|
ref: version,
|
|
@@ -5179,8 +5512,8 @@ class RegistryResolver {
|
|
|
5179
5512
|
commit: '',
|
|
5180
5513
|
registry: registryUrl
|
|
5181
5514
|
});
|
|
5182
|
-
// Update skills.json (project mode only)
|
|
5183
|
-
if (!
|
|
5515
|
+
// Update skills.json (project mode only, skip for effectively-global installs)
|
|
5516
|
+
if (!effectivelyGlobal && save) {
|
|
5184
5517
|
this.config.ensureExists();
|
|
5185
5518
|
this.config.addSkill(skillName, parsed.fullName);
|
|
5186
5519
|
// Save custom registry to skills.json.registries (for reinstall without lock file)
|
|
@@ -5203,7 +5536,7 @@ class RegistryResolver {
|
|
|
5203
5536
|
};
|
|
5204
5537
|
} finally{
|
|
5205
5538
|
// Clean up temp directory after installation
|
|
5206
|
-
await
|
|
5539
|
+
await fs_remove(tempDir);
|
|
5207
5540
|
}
|
|
5208
5541
|
}
|
|
5209
5542
|
/**
|
|
@@ -5254,10 +5587,11 @@ class RegistryResolver {
|
|
|
5254
5587
|
installDir: defaults.installDir
|
|
5255
5588
|
});
|
|
5256
5589
|
const results = installer.uninstallFromAgents(name, targetAgents);
|
|
5257
|
-
// Remove from lock file (project mode only)
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5590
|
+
// Remove from lock file (project mode only, skip for effectively-global installs)
|
|
5591
|
+
const effectivelyGlobal = this.isEffectivelyGlobal(targetAgents);
|
|
5592
|
+
if (!effectivelyGlobal) this.lockManager.remove(name);
|
|
5593
|
+
// Remove from skills.json (project mode only, skip for effectively-global installs)
|
|
5594
|
+
if (!effectivelyGlobal && this.config.exists()) this.config.removeSkill(name);
|
|
5261
5595
|
const successCount = Array.from(results.values()).filter((r)=>r).length;
|
|
5262
5596
|
logger_logger.success(`Uninstalled ${name} from ${successCount} agent(s)`);
|
|
5263
5597
|
return results;
|
|
@@ -7326,12 +7660,19 @@ const DEFAULT_INSTALL_DIR = '.skills';
|
|
|
7326
7660
|
// ============================================================================
|
|
7327
7661
|
/**
|
|
7328
7662
|
* Resolve installation scope (global vs project)
|
|
7329
|
-
*/ async function resolveInstallScope(ctx) {
|
|
7663
|
+
*/ async function resolveInstallScope(ctx, targetAgents) {
|
|
7330
7664
|
const { options, isReinstallAll, skipConfirm } = ctx;
|
|
7331
7665
|
// Explicit --global flag
|
|
7332
7666
|
if (void 0 !== options.global) return options.global;
|
|
7333
|
-
// Skip prompt for reinstall-all (always project scope)
|
|
7667
|
+
// Skip prompt for reinstall-all (always project scope).
|
|
7668
|
+
// Must be checked before the claude-cowork-3p override to avoid
|
|
7669
|
+
// "Cannot install all skills globally" when 3p is the only detected agent.
|
|
7334
7670
|
if (isReinstallAll) return false;
|
|
7671
|
+
// claude-cowork-3p always installs globally — skip prompt
|
|
7672
|
+
if (targetAgents.length > 0 && targetAgents.every((a)=>a === claude_3p_installer_CLAUDE_COWORK_3P_AGENT)) {
|
|
7673
|
+
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.info('Using global scope (claude-cowork-3p is always global)');
|
|
7674
|
+
return true;
|
|
7675
|
+
}
|
|
7335
7676
|
// Skip prompt if --yes
|
|
7336
7677
|
if (skipConfirm) return false;
|
|
7337
7678
|
// Prompt user
|
|
@@ -7361,10 +7702,12 @@ const DEFAULT_INSTALL_DIR = '.skills';
|
|
|
7361
7702
|
// ============================================================================
|
|
7362
7703
|
/**
|
|
7363
7704
|
* Resolve installation mode (symlink vs copy)
|
|
7364
|
-
*/ async function resolveInstallMode(ctx) {
|
|
7705
|
+
*/ async function resolveInstallMode(ctx, targetAgents) {
|
|
7365
7706
|
const { options, storedMode, isReinstallAll, skipConfirm } = ctx;
|
|
7366
7707
|
// Priority 1: CLI --mode option
|
|
7367
7708
|
if (options.mode) return options.mode;
|
|
7709
|
+
// claude-cowork-3p always uses copy mode — skip prompt
|
|
7710
|
+
if (targetAgents.length > 0 && targetAgents.every((a)=>a === claude_3p_installer_CLAUDE_COWORK_3P_AGENT)) return 'copy';
|
|
7368
7711
|
// Priority 2: Reinstall all with stored mode
|
|
7369
7712
|
if (isReinstallAll && storedMode) {
|
|
7370
7713
|
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.info(`Using saved install mode: ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].cyan(storedMode)}`);
|
|
@@ -7846,14 +8189,14 @@ const DEFAULT_INSTALL_DIR = '.skills';
|
|
|
7846
8189
|
// Step 1: Resolve target agents
|
|
7847
8190
|
targetAgents = await resolveTargetAgents(ctx, spinner);
|
|
7848
8191
|
// Step 2: Resolve installation scope
|
|
7849
|
-
installGlobally = await resolveInstallScope(ctx);
|
|
8192
|
+
installGlobally = await resolveInstallScope(ctx, targetAgents);
|
|
7850
8193
|
// Validate: Cannot install all skills globally
|
|
7851
8194
|
if (ctx.isReinstallAll && installGlobally) {
|
|
7852
8195
|
__WEBPACK_EXTERNAL_MODULE__clack_prompts__.log.error('Cannot install all skills globally. Please specify a skill to install.');
|
|
7853
8196
|
process.exit(1);
|
|
7854
8197
|
}
|
|
7855
8198
|
// Step 3: Resolve installation mode
|
|
7856
|
-
installMode = await resolveInstallMode(ctx);
|
|
8199
|
+
installMode = await resolveInstallMode(ctx, targetAgents);
|
|
7857
8200
|
}
|
|
7858
8201
|
// Step 4: Execute installation
|
|
7859
8202
|
if (ctx.isReinstallAll) await installAllSkills(ctx, targetAgents, installMode, spinner);
|
|
@@ -7871,14 +8214,23 @@ const DEFAULT_INSTALL_DIR = '.skills';
|
|
|
7871
8214
|
});
|
|
7872
8215
|
/**
|
|
7873
8216
|
* list command - List installed skills
|
|
7874
|
-
*/ const listCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('list').alias('ls').description('List installed skills').option('-j, --json', 'Output as JSON').option('-g, --global', 'List globally installed skills').action((options)=>{
|
|
7875
|
-
const
|
|
8217
|
+
*/ const listCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('list').alias('ls').description('List installed skills').option('-j, --json', 'Output as JSON').option('-g, --global', 'List globally installed skills').option('-a, --agent <agent>', 'List skills installed to a specific agent').action((options)=>{
|
|
8218
|
+
const agentInput = options.agent;
|
|
8219
|
+
if (void 0 !== agentInput && !isValidAgentType(agentInput)) {
|
|
8220
|
+
logger_logger.error(`Invalid agent: ${agentInput}`);
|
|
8221
|
+
process.exit(1);
|
|
8222
|
+
}
|
|
8223
|
+
const agent = agentInput;
|
|
8224
|
+
// claude-cowork-3p is always global
|
|
8225
|
+
const isGlobal = options.global || agent === claude_3p_installer_CLAUDE_COWORK_3P_AGENT;
|
|
7876
8226
|
const skillManager = new SkillManager(void 0, {
|
|
7877
8227
|
global: isGlobal
|
|
7878
8228
|
});
|
|
7879
|
-
const skills = skillManager.list(
|
|
8229
|
+
const skills = skillManager.list(agent ? {
|
|
8230
|
+
agent
|
|
8231
|
+
} : void 0);
|
|
7880
8232
|
if (0 === skills.length) {
|
|
7881
|
-
const location = isGlobal ? 'globally' : 'in this project';
|
|
8233
|
+
const location = agent ? `for ${getAgentConfig(agent).displayName}` : isGlobal ? 'globally' : 'in this project';
|
|
7882
8234
|
logger_logger.info(`No skills installed ${location}`);
|
|
7883
8235
|
return;
|
|
7884
8236
|
}
|
|
@@ -7886,7 +8238,7 @@ const DEFAULT_INSTALL_DIR = '.skills';
|
|
|
7886
8238
|
console.log(JSON.stringify(skills, null, 2));
|
|
7887
8239
|
return;
|
|
7888
8240
|
}
|
|
7889
|
-
const scopeLabel = isGlobal ? 'global' : 'project';
|
|
8241
|
+
const scopeLabel = agent ? getAgentConfig(agent).displayName : isGlobal ? 'global' : 'project';
|
|
7890
8242
|
logger_logger.log(`Installed Skills (${scopeLabel}):`);
|
|
7891
8243
|
logger_logger.newline();
|
|
7892
8244
|
const headers = [
|
|
@@ -9504,7 +9856,7 @@ async function publishAction(skillPath, options) {
|
|
|
9504
9856
|
const validation = validator.validate(absolutePath);
|
|
9505
9857
|
// 3.5. Content security scan
|
|
9506
9858
|
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(absolutePath, 'SKILL.md');
|
|
9507
|
-
if (
|
|
9859
|
+
if (fs_exists(skillMdPath)) {
|
|
9508
9860
|
const scanner = new ContentScanner();
|
|
9509
9861
|
const scanResult = scanner.scanFile(skillMdPath);
|
|
9510
9862
|
displayScanFindings(scanResult);
|