skills-package-manager 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,46 +1,682 @@
1
+ import { cac } from "cac";
1
2
  import picocolors from "picocolors";
2
- import { access, cp as promises_cp, lstat, mkdir, mkdtemp, readFile, readdir, rm as promises_rm, stat as promises_stat, symlink, writeFile } from "node:fs/promises";
3
- import node_path, { join } from "node:path";
3
+ import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, rm, stat as promises_stat, symlink, writeFile } from "node:fs/promises";
4
+ import node_path, { basename, join } from "node:path";
4
5
  import yaml from "yaml";
5
- import { createHash } from "node:crypto";
6
- import { tmpdir } from "node:os";
7
6
  import { execFile } from "node:child_process";
7
+ import { homedir, tmpdir } from "node:os";
8
8
  import { promisify } from "node:util";
9
- import { cac } from "cac";
9
+ import { createHash } from "node:crypto";
10
+ import semver from "semver";
11
+ import { createReadStream } from "node:fs";
12
+ import { x } from "tar";
10
13
  import * as __rspack_external__clack_prompts_3cae1695 from "@clack/prompts";
14
+ var package_namespaceObject = {
15
+ rE: "0.4.0"
16
+ };
17
+ const UNIVERSAL_AGENT_NAMES = [
18
+ 'Amp',
19
+ 'Antigravity',
20
+ 'Cline',
21
+ 'Codex',
22
+ 'Cursor',
23
+ 'Deep Agents',
24
+ 'Firebender',
25
+ 'Gemini CLI',
26
+ 'GitHub Copilot',
27
+ 'Kimi Code CLI',
28
+ 'OpenCode',
29
+ 'Warp'
30
+ ];
31
+ const ADDITIONAL_AGENT_TARGETS = [
32
+ {
33
+ label: 'Augment',
34
+ path: '.augment/skills'
35
+ },
36
+ {
37
+ label: 'IBM Bob',
38
+ path: '.bob/skills'
39
+ },
40
+ {
41
+ label: 'Claude Code',
42
+ path: '.claude/skills'
43
+ },
44
+ {
45
+ label: 'OpenClaw',
46
+ path: 'skills'
47
+ },
48
+ {
49
+ label: 'CodeBuddy',
50
+ path: '.codebuddy/skills'
51
+ },
52
+ {
53
+ label: 'Command Code',
54
+ path: '.commandcode/skills'
55
+ },
56
+ {
57
+ label: 'Continue',
58
+ path: '.continue/skills'
59
+ },
60
+ {
61
+ label: 'Cortex Code',
62
+ path: '.cortex/skills'
63
+ },
64
+ {
65
+ label: 'Trae',
66
+ path: '.trae/skills'
67
+ }
68
+ ];
69
+ async function promptSkillSelection(skills) {
70
+ if (0 === skills.length) throw new Error('No skills found in repository');
71
+ if (1 === skills.length) return skills;
72
+ const options = skills.map((skill)=>({
73
+ value: skill,
74
+ label: skill.name,
75
+ hint: skill.description ? skill.description.length > 60 ? `${skill.description.slice(0, 57)}...` : skill.description : void 0
76
+ }));
77
+ const selected = await __rspack_external__clack_prompts_3cae1695.multiselect({
78
+ message: 'Select skills to install',
79
+ options,
80
+ required: true
81
+ });
82
+ if (__rspack_external__clack_prompts_3cae1695.isCancel(selected)) {
83
+ __rspack_external__clack_prompts_3cae1695.cancel('Installation cancelled');
84
+ process.exit(0);
85
+ }
86
+ return selected;
87
+ }
88
+ async function promptInitManifestOptions(promptApi = __rspack_external__clack_prompts_3cae1695, exit = process.exit) {
89
+ const installDirInput = await promptApi.text({
90
+ message: 'Where should skills be installed?',
91
+ initialValue: '.agents/skills'
92
+ });
93
+ if (promptApi.isCancel(installDirInput)) {
94
+ promptApi.cancel('Initialization cancelled');
95
+ exit(0);
96
+ }
97
+ promptApi.note(UNIVERSAL_AGENT_NAMES.join('\n'), 'Universal (.agents/skills) — always included');
98
+ const selected = await promptApi.groupMultiselect({
99
+ message: 'Which agents do you want to install to?',
100
+ options: {
101
+ 'Additional agents': ADDITIONAL_AGENT_TARGETS.map((agent)=>({
102
+ value: agent.path,
103
+ label: `${agent.label} (${agent.path})`
104
+ }))
105
+ },
106
+ required: false
107
+ });
108
+ if (promptApi.isCancel(selected)) {
109
+ promptApi.cancel('Initialization cancelled');
110
+ exit(0);
111
+ }
112
+ const installDir = String(installDirInput).trim() || '.agents/skills';
113
+ return {
114
+ installDir,
115
+ linkTargets: selected
116
+ };
117
+ }
118
+ var codes_ErrorCode = /*#__PURE__*/ function(ErrorCode) {
119
+ ErrorCode["FILE_NOT_FOUND"] = "ENOENT";
120
+ ErrorCode["PERMISSION_DENIED"] = "EACCES";
121
+ ErrorCode["FILE_EXISTS"] = "EEXIST";
122
+ ErrorCode["FS_ERROR"] = "EFS";
123
+ ErrorCode["GIT_CLONE_FAILED"] = "EGITCLONE";
124
+ ErrorCode["GIT_FETCH_FAILED"] = "EGITFETCH";
125
+ ErrorCode["GIT_CHECKOUT_FAILED"] = "EGITCHECKOUT";
126
+ ErrorCode["GIT_REF_NOT_FOUND"] = "EGITREF";
127
+ ErrorCode["GIT_NOT_INSTALLED"] = "EGITNOTFOUND";
128
+ ErrorCode["PARSE_ERROR"] = "EPARSE";
129
+ ErrorCode["JSON_PARSE_ERROR"] = "EJSONPARSE";
130
+ ErrorCode["YAML_PARSE_ERROR"] = "EYAMLPARSE";
131
+ ErrorCode["INVALID_SPECIFIER"] = "EINVALIDSPEC";
132
+ ErrorCode["MANIFEST_NOT_FOUND"] = "EMANIFEST";
133
+ ErrorCode["LOCKFILE_NOT_FOUND"] = "ELOCKFILE";
134
+ ErrorCode["LOCKFILE_OUTDATED"] = "ELOCKOUTDATED";
135
+ ErrorCode["MANIFEST_EXISTS"] = "EMANIFESTEXISTS";
136
+ ErrorCode["NETWORK_ERROR"] = "ENETWORK";
137
+ ErrorCode["REPO_NOT_FOUND"] = "EREPONOTFOUND";
138
+ ErrorCode["UNKNOWN_ERROR"] = "EUNKNOWN";
139
+ ErrorCode["NOT_IMPLEMENTED"] = "ENOTIMPL";
140
+ ErrorCode["VALIDATION_ERROR"] = "EVALIDATION";
141
+ ErrorCode["SKILL_NOT_FOUND"] = "ESKILLNOTFOUND";
142
+ ErrorCode["SKILL_EXISTS"] = "ESKILLEXISTS";
143
+ return ErrorCode;
144
+ }({});
145
+ class SpmError extends Error {
146
+ code;
147
+ cause;
148
+ context;
149
+ constructor(options){
150
+ super(options.message);
151
+ this.code = options.code;
152
+ this.cause = options.cause;
153
+ this.context = options.context ?? {};
154
+ this.name = 'SpmError';
155
+ if (Error.captureStackTrace) Error.captureStackTrace(this, SpmError);
156
+ }
157
+ toString() {
158
+ let result = `${this.code}: ${this.message}`;
159
+ if (this.cause) result += `\n Caused by: ${this.cause.message}`;
160
+ return result;
161
+ }
162
+ toJSON() {
163
+ return {
164
+ name: this.name,
165
+ code: this.code,
166
+ message: this.message,
167
+ context: this.context,
168
+ cause: this.cause ? {
169
+ name: this.cause.name,
170
+ message: this.cause.message
171
+ } : void 0,
172
+ stack: this.stack
173
+ };
174
+ }
175
+ }
176
+ class FileSystemError extends SpmError {
177
+ operation;
178
+ path;
179
+ constructor(options){
180
+ const message = options.message ?? `${options.operation} failed for ${options.path}`;
181
+ super({
182
+ code: options.code,
183
+ message,
184
+ cause: options.cause,
185
+ context: {
186
+ operation: options.operation,
187
+ path: options.path
188
+ }
189
+ });
190
+ this.operation = options.operation;
191
+ this.path = options.path;
192
+ this.name = 'FileSystemError';
193
+ }
194
+ }
195
+ class GitError extends SpmError {
196
+ operation;
197
+ repoUrl;
198
+ ref;
199
+ constructor(options){
200
+ const message = options.message ?? `git ${options.operation} failed${options.repoUrl ? ` for ${options.repoUrl}` : ''}`;
201
+ super({
202
+ code: options.code,
203
+ message,
204
+ cause: options.cause,
205
+ context: {
206
+ operation: options.operation,
207
+ repoUrl: options.repoUrl,
208
+ ref: options.ref
209
+ }
210
+ });
211
+ this.operation = options.operation;
212
+ this.repoUrl = options.repoUrl;
213
+ this.ref = options.ref;
214
+ this.name = 'GitError';
215
+ }
216
+ }
217
+ class ParseError extends SpmError {
218
+ filePath;
219
+ content;
220
+ constructor(options){
221
+ super({
222
+ code: options.code,
223
+ message: options.message,
224
+ cause: options.cause,
225
+ context: {
226
+ filePath: options.filePath,
227
+ contentSnippet: options.content?.slice(0, 200)
228
+ }
229
+ });
230
+ this.filePath = options.filePath;
231
+ this.content = options.content;
232
+ this.name = 'ParseError';
233
+ }
234
+ }
235
+ class ManifestError extends SpmError {
236
+ filePath;
237
+ constructor(options){
238
+ const defaultMessages = {
239
+ [codes_ErrorCode.MANIFEST_NOT_FOUND]: `Manifest not found: ${options.filePath}`,
240
+ [codes_ErrorCode.LOCKFILE_NOT_FOUND]: `Lockfile not found: ${options.filePath}`,
241
+ [codes_ErrorCode.LOCKFILE_OUTDATED]: `Lockfile is out of date: ${options.filePath}`,
242
+ [codes_ErrorCode.MANIFEST_EXISTS]: `Manifest already exists: ${options.filePath}`
243
+ };
244
+ const message = options.message ?? defaultMessages[options.code] ?? `Manifest error: ${options.filePath}`;
245
+ super({
246
+ code: options.code,
247
+ message,
248
+ cause: options.cause,
249
+ context: {
250
+ filePath: options.filePath
251
+ }
252
+ });
253
+ this.filePath = options.filePath;
254
+ this.name = 'ManifestError';
255
+ }
256
+ }
257
+ class SkillError extends SpmError {
258
+ skillName;
259
+ constructor(options){
260
+ const defaultMessages = {
261
+ [codes_ErrorCode.SKILL_NOT_FOUND]: `Skill not found: ${options.skillName}`,
262
+ [codes_ErrorCode.SKILL_EXISTS]: `Skill already exists: ${options.skillName}`,
263
+ [codes_ErrorCode.VALIDATION_ERROR]: `Skill validation failed: ${options.skillName}`
264
+ };
265
+ const message = options.message ?? defaultMessages[options.code] ?? `Skill error: ${options.skillName}`;
266
+ super({
267
+ code: options.code,
268
+ message,
269
+ cause: options.cause,
270
+ context: {
271
+ skillName: options.skillName
272
+ }
273
+ });
274
+ this.skillName = options.skillName;
275
+ this.name = 'SkillError';
276
+ }
277
+ }
278
+ class NetworkError extends SpmError {
279
+ url;
280
+ constructor(options){
281
+ super({
282
+ code: options.code,
283
+ message: options.message,
284
+ cause: options.cause,
285
+ context: {
286
+ url: options.url
287
+ }
288
+ });
289
+ this.url = options.url;
290
+ this.name = 'NetworkError';
291
+ }
292
+ }
293
+ function convertNodeError(error, context) {
294
+ switch(error.code){
295
+ case 'ENOENT':
296
+ return new FileSystemError({
297
+ code: codes_ErrorCode.FILE_NOT_FOUND,
298
+ operation: context.operation,
299
+ path: context.path,
300
+ cause: error
301
+ });
302
+ case 'EACCES':
303
+ case 'EPERM':
304
+ return new FileSystemError({
305
+ code: codes_ErrorCode.PERMISSION_DENIED,
306
+ operation: context.operation,
307
+ path: context.path,
308
+ cause: error
309
+ });
310
+ case 'EEXIST':
311
+ return new FileSystemError({
312
+ code: codes_ErrorCode.FILE_EXISTS,
313
+ operation: context.operation,
314
+ path: context.path,
315
+ cause: error
316
+ });
317
+ case 'ENOTDIR':
318
+ return new FileSystemError({
319
+ code: codes_ErrorCode.FS_ERROR,
320
+ operation: context.operation,
321
+ path: context.path,
322
+ message: `Not a directory: ${context.path}`,
323
+ cause: error
324
+ });
325
+ case 'EISDIR':
326
+ return new FileSystemError({
327
+ code: codes_ErrorCode.FS_ERROR,
328
+ operation: context.operation,
329
+ path: context.path,
330
+ message: `Is a directory: ${context.path}`,
331
+ cause: error
332
+ });
333
+ default:
334
+ return new FileSystemError({
335
+ code: codes_ErrorCode.FS_ERROR,
336
+ operation: context.operation,
337
+ path: context.path,
338
+ message: error.message,
339
+ cause: error
340
+ });
341
+ }
342
+ }
343
+ function formatErrorForDisplay(error) {
344
+ if (error instanceof SpmError) {
345
+ let output = `Error [${error.code}]: ${error.message}`;
346
+ if (error instanceof FileSystemError) {
347
+ if (error.code === codes_ErrorCode.FILE_NOT_FOUND) {
348
+ output += `\n\nThe file "${error.path}" was not found.`;
349
+ if (error.path.endsWith('skills.json')) output += `\nRun "spm init" to create a new skills.json file.`;
350
+ } else if (error.code === codes_ErrorCode.PERMISSION_DENIED) {
351
+ output += `\n\nPermission denied for "${error.path}".`;
352
+ output += `\nCheck that you have read/write access to this location.`;
353
+ }
354
+ } else if (error instanceof GitError) {
355
+ if (error.code === codes_ErrorCode.GIT_REF_NOT_FOUND) {
356
+ output += `\n\nThe reference "${error.ref}" could not be found in the repository.`;
357
+ output += `\nPlease check that the branch, tag, or commit hash is correct.`;
358
+ } else if (error.code === codes_ErrorCode.GIT_CLONE_FAILED) {
359
+ output += `\n\nFailed to clone the repository.`;
360
+ output += `\nPossible causes:`;
361
+ output += `\n - The repository URL is incorrect`;
362
+ output += `\n - The repository is private and requires authentication`;
363
+ output += `\n - There is no internet connection`;
364
+ }
365
+ } else if (error instanceof ParseError) {
366
+ if (error.code === codes_ErrorCode.JSON_PARSE_ERROR || error.code === codes_ErrorCode.YAML_PARSE_ERROR) {
367
+ output += `\n\nFile: ${error.filePath}`;
368
+ output += `\nPlease check the file syntax and fix any formatting issues.`;
369
+ } else if (error.code === codes_ErrorCode.INVALID_SPECIFIER) {
370
+ output += `\n\nInvalid skill specifier format.`;
371
+ output += `\nExpected formats:`;
372
+ output += `\n - owner/repo (GitHub shorthand)`;
373
+ output += `\n - https://github.com/owner/repo.git`;
374
+ output += `\n - link:./path/to/skill-dir`;
375
+ output += `\n - file:./path/to/skill-package.tgz#path:/skills/my-skill`;
376
+ output += `\n - npm:@scope/skill-package#path:/skills/my-skill`;
377
+ }
378
+ } else if (error instanceof ManifestError) {
379
+ if (error.code === codes_ErrorCode.LOCKFILE_OUTDATED) {
380
+ output += `\n\nThe lockfile is out of sync with skills.json.`;
381
+ output += `\nRun "spm install" to update the lockfile.`;
382
+ }
383
+ }
384
+ if (error.cause && !(error instanceof GitError || error instanceof FileSystemError)) output += `\n\nCaused by: ${error.cause.message}`;
385
+ return output;
386
+ }
387
+ if (error instanceof Error) return `Error: ${error.message}`;
388
+ return `Error: ${String(error)}`;
389
+ }
390
+ function isSpmError(error) {
391
+ return error instanceof SpmError;
392
+ }
393
+ function getExitCode(error) {
394
+ if (error instanceof SpmError) {
395
+ if (error.code.startsWith('E1')) return 101;
396
+ if (error.code.startsWith('E2')) return 102;
397
+ if (error.code.startsWith('E3')) return 103;
398
+ if (error.code.startsWith('E4')) return 104;
399
+ if (error.code.startsWith('E5')) return 105;
400
+ if (error.code.startsWith('E9')) return 109;
401
+ }
402
+ return 1;
403
+ }
11
404
  async function readSkillsLock(rootDir) {
12
405
  const filePath = node_path.join(rootDir, 'skills-lock.yaml');
13
406
  try {
14
407
  const raw = await readFile(filePath, 'utf8');
15
- return yaml.parse(raw);
408
+ try {
409
+ return yaml.parse(raw);
410
+ } catch (parseError) {
411
+ throw new ParseError({
412
+ code: codes_ErrorCode.YAML_PARSE_ERROR,
413
+ filePath,
414
+ content: raw,
415
+ message: `Failed to parse skills-lock.yaml: ${parseError.message}`,
416
+ cause: parseError
417
+ });
418
+ }
16
419
  } catch (error) {
17
420
  if ('ENOENT' === error.code) return null;
18
- throw error;
421
+ if (error instanceof ParseError) throw error;
422
+ throw convertNodeError(error, {
423
+ operation: 'read',
424
+ path: filePath
425
+ });
19
426
  }
20
427
  }
21
428
  async function readSkillsManifest(rootDir) {
22
429
  const filePath = node_path.join(rootDir, 'skills.json');
23
430
  try {
24
431
  const raw = await readFile(filePath, 'utf8');
25
- const json = JSON.parse(raw);
26
- return {
27
- installDir: json.installDir ?? '.agents/skills',
28
- linkTargets: json.linkTargets ?? [],
29
- skills: json.skills ?? {}
30
- };
432
+ try {
433
+ const json = JSON.parse(raw);
434
+ return {
435
+ installDir: json.installDir ?? '.agents/skills',
436
+ linkTargets: json.linkTargets ?? [],
437
+ skills: json.skills ?? {}
438
+ };
439
+ } catch (parseError) {
440
+ throw new ParseError({
441
+ code: codes_ErrorCode.JSON_PARSE_ERROR,
442
+ filePath,
443
+ content: raw,
444
+ message: `Failed to parse skills.json: ${parseError.message}`,
445
+ cause: parseError
446
+ });
447
+ }
31
448
  } catch (error) {
32
449
  if ('ENOENT' === error.code) return null;
450
+ if (error instanceof ParseError) throw error;
451
+ throw convertNodeError(error, {
452
+ operation: 'read',
453
+ path: filePath
454
+ });
455
+ }
456
+ }
457
+ const resolvedNpmPackageCache = new Map();
458
+ function normalizeRegistryUrl(url) {
459
+ return url.endsWith('/') ? url : `${url}/`;
460
+ }
461
+ function interpolateEnv(value) {
462
+ return value.replace(/\$\{([^}]+)\}/g, (_match, key)=>process.env[key] ?? '');
463
+ }
464
+ async function readNpmRc(filePath) {
465
+ try {
466
+ const content = await readFile(filePath, 'utf8');
467
+ const entries = new Map();
468
+ for (const rawLine of content.split(/\r?\n/)){
469
+ const line = rawLine.trim();
470
+ if (!line || line.startsWith('#') || line.startsWith(';')) continue;
471
+ const separator = line.indexOf('=');
472
+ if (!(separator < 0)) entries.set(line.slice(0, separator).trim(), interpolateEnv(line.slice(separator + 1).trim()));
473
+ }
474
+ return entries;
475
+ } catch {
476
+ return new Map();
477
+ }
478
+ }
479
+ function getCandidateDirs(cwd) {
480
+ const resolvedCwd = node_path.resolve(cwd);
481
+ if ('win32' === process.platform) {
482
+ const parsed = node_path.parse(resolvedCwd);
483
+ const relative = resolvedCwd.slice(parsed.root.length);
484
+ const parts = relative.split(node_path.sep).filter(Boolean);
485
+ return [
486
+ parsed.root,
487
+ ...parts.map((_part, index)=>node_path.join(parsed.root, ...parts.slice(0, index + 1)))
488
+ ];
489
+ }
490
+ const parts = resolvedCwd.split(node_path.sep).filter(Boolean);
491
+ return [
492
+ '/',
493
+ ...parts.map((_part, index)=>node_path.join('/', ...parts.slice(0, index + 1)))
494
+ ];
495
+ }
496
+ function buildRegistryAuthEntries(settings) {
497
+ const registryAuthConfigs = new Map();
498
+ for (const [key, value] of settings){
499
+ const match = key.match(/^(\/\/.+\/):(_authToken|_auth|username|_password)$/);
500
+ if (!match) continue;
501
+ const [, prefix, field] = match;
502
+ const config = registryAuthConfigs.get(prefix) ?? {};
503
+ registryAuthConfigs.set(prefix, {
504
+ ...config,
505
+ ['_authToken' === field ? 'authToken' : '_auth' === field ? 'auth' : 'username' === field ? 'username' : 'password']: value
506
+ });
507
+ }
508
+ return [
509
+ ...registryAuthConfigs.entries()
510
+ ].map(([prefix, config])=>{
511
+ if (config.authToken) return {
512
+ prefix,
513
+ authorization: `Bearer ${config.authToken}`
514
+ };
515
+ if (config.auth) return {
516
+ prefix,
517
+ authorization: `Basic ${config.auth}`
518
+ };
519
+ if (config.username && config.password) {
520
+ const decodedPassword = Buffer.from(config.password, 'base64').toString('utf8');
521
+ return {
522
+ prefix,
523
+ authorization: `Basic ${Buffer.from(`${config.username}:${decodedPassword}`).toString('base64')}`
524
+ };
525
+ }
526
+ return null;
527
+ }).filter((entry)=>null !== entry).sort((a, b)=>b.prefix.length - a.prefix.length);
528
+ }
529
+ async function loadNpmConfig(cwd) {
530
+ const configs = new Map();
531
+ for (const [key, value] of (await readNpmRc(node_path.join(homedir(), '.npmrc'))))configs.set(key, value);
532
+ for (const candidateDir of getCandidateDirs(cwd))for (const [key, value] of (await readNpmRc(node_path.join(candidateDir, '.npmrc'))))configs.set(key, value);
533
+ return {
534
+ settings: configs,
535
+ authEntries: buildRegistryAuthEntries(configs)
536
+ };
537
+ }
538
+ function resolveRegistryConfig(config, packageName) {
539
+ const scopeMatch = packageName.match(/^(@[^/]+)\//);
540
+ if (scopeMatch) {
541
+ const scopeRegistry = config.settings.get(`${scopeMatch[1]}:registry`);
542
+ if (scopeRegistry) return normalizeRegistryUrl(scopeRegistry);
543
+ }
544
+ return normalizeRegistryUrl(config.settings.get('registry') ?? 'https://registry.npmjs.org/');
545
+ }
546
+ function resolveAuthorizationHeader(config, requestUrl) {
547
+ const url = new URL(requestUrl);
548
+ const requestKey = `//${url.host}${url.pathname}`;
549
+ const matched = config.authEntries.find((entry)=>requestKey.startsWith(entry.prefix));
550
+ return matched?.authorization;
551
+ }
552
+ function createRequestHeaders(config, requestUrl) {
553
+ const authorization = resolveAuthorizationHeader(config, requestUrl);
554
+ if (!authorization) return;
555
+ return {
556
+ authorization
557
+ };
558
+ }
559
+ function parseRegistryPackageSpecifier(specifier) {
560
+ const scopedMatch = specifier.match(/^(@[^/]+\/[^@]+)(?:@(.+))?$/);
561
+ if (scopedMatch) return {
562
+ packageName: scopedMatch[1],
563
+ requestedVersion: scopedMatch[2] ?? null
564
+ };
565
+ const unscopedMatch = specifier.match(/^([^@/:][^@]*?)(?:@(.+))?$/);
566
+ if (unscopedMatch) return {
567
+ packageName: unscopedMatch[1],
568
+ requestedVersion: unscopedMatch[2] ?? null
569
+ };
570
+ throw new Error(`Unsupported npm specifier: ${specifier}`);
571
+ }
572
+ function resolveVersionFromMetadata(metadata, requestedVersion) {
573
+ const versions = metadata.versions ?? {};
574
+ const versionKeys = Object.keys(versions);
575
+ const requested = requestedVersion ?? 'latest';
576
+ const taggedVersion = metadata['dist-tags']?.[requested];
577
+ if (taggedVersion && versions[taggedVersion]) return taggedVersion;
578
+ if (semver.valid(requested) && versions[requested]) return requested;
579
+ const matched = semver.maxSatisfying(versionKeys, requested);
580
+ if (matched) return matched;
581
+ throw new Error(`Unable to resolve npm version "${requested}"`);
582
+ }
583
+ async function resolveNpmPackageUncached(cwd, specifier) {
584
+ const config = await loadNpmConfig(cwd);
585
+ const { packageName, requestedVersion } = parseRegistryPackageSpecifier(specifier);
586
+ const registry = resolveRegistryConfig(config, packageName);
587
+ const metadataUrl = new URL(encodeURIComponent(packageName), registry);
588
+ const response = await fetch(metadataUrl, {
589
+ headers: createRequestHeaders(config, metadataUrl.toString())
590
+ });
591
+ if (!response.ok) throw new Error(`Failed to fetch npm metadata for ${packageName}: ${response.status}`);
592
+ const metadata = await response.json();
593
+ const version = resolveVersionFromMetadata(metadata, requestedVersion);
594
+ const manifest = metadata.versions?.[version];
595
+ const tarballUrl = manifest?.dist?.tarball;
596
+ if (!manifest?.name || !manifest.version || !tarballUrl) throw new Error(`Invalid npm metadata for ${packageName}@${version}`);
597
+ return {
598
+ name: manifest.name,
599
+ version: manifest.version,
600
+ tarballUrl,
601
+ integrity: manifest.dist?.integrity,
602
+ registry
603
+ };
604
+ }
605
+ async function resolveNpmPackage(cwd, specifier) {
606
+ const cacheKey = `${node_path.resolve(cwd)}\0${specifier}`;
607
+ const cached = resolvedNpmPackageCache.get(cacheKey);
608
+ if (cached) return cached;
609
+ const pending = resolveNpmPackageUncached(cwd, specifier);
610
+ resolvedNpmPackageCache.set(cacheKey, pending);
611
+ try {
612
+ return await pending;
613
+ } catch (error) {
614
+ resolvedNpmPackageCache.delete(cacheKey);
33
615
  throw error;
34
616
  }
35
617
  }
618
+ function verifyIntegrity(buffer, integrity) {
619
+ for (const entry of integrity.split(/\s+/).filter(Boolean)){
620
+ const separatorIndex = entry.indexOf('-');
621
+ if (separatorIndex <= 0) continue;
622
+ const algorithm = entry.slice(0, separatorIndex);
623
+ const expectedDigest = entry.slice(separatorIndex + 1);
624
+ try {
625
+ const actualDigest = createHash(algorithm).update(buffer).digest('base64');
626
+ if (actualDigest === expectedDigest) return true;
627
+ } catch {}
628
+ }
629
+ return false;
630
+ }
631
+ async function downloadNpmPackageTarball(cwd, tarballUrl, expectedIntegrity) {
632
+ const downloadRoot = await mkdtemp(node_path.join(tmpdir(), 'skills-pm-npm-download-'));
633
+ try {
634
+ const config = await loadNpmConfig(cwd);
635
+ const response = await fetch(tarballUrl, {
636
+ headers: createRequestHeaders(config, tarballUrl)
637
+ });
638
+ if (!response.ok) throw new Error(`Failed to download npm tarball: ${response.status}`);
639
+ const tarballBuffer = Buffer.from(await response.arrayBuffer());
640
+ if (expectedIntegrity && !verifyIntegrity(tarballBuffer, expectedIntegrity)) throw new Error(`Integrity check failed for npm tarball ${tarballUrl}`);
641
+ const tarballPath = node_path.join(downloadRoot, node_path.basename(new URL(tarballUrl).pathname) || 'package.tgz');
642
+ await writeFile(tarballPath, tarballBuffer);
643
+ return tarballPath;
644
+ } catch (error) {
645
+ await rm(downloadRoot, {
646
+ recursive: true,
647
+ force: true
648
+ }).catch(()=>{});
649
+ throw new Error(`Failed to download npm tarball ${tarballUrl}: ${error.message}`, {
650
+ cause: error
651
+ });
652
+ }
653
+ }
654
+ async function cleanupPackedNpmPackage(tarballPath) {
655
+ await rm(node_path.dirname(tarballPath), {
656
+ recursive: true,
657
+ force: true
658
+ }).catch(()=>{});
659
+ }
660
+ function normalizeLinkSource(sourcePart) {
661
+ const linkPath = sourcePart.slice(5).replace(/\\/g, '/').replace(/\/+$/, '');
662
+ return `link:${linkPath}`;
663
+ }
36
664
  function parseSpecifier(specifier) {
37
665
  const firstHashIndex = specifier.indexOf('#');
38
666
  const secondHashIndex = firstHashIndex >= 0 ? specifier.indexOf('#', firstHashIndex + 1) : -1;
39
- if (secondHashIndex >= 0) throw new Error('Invalid specifier: multiple # fragments are not supported');
667
+ if (secondHashIndex >= 0) throw new ParseError({
668
+ code: codes_ErrorCode.INVALID_SPECIFIER,
669
+ message: 'Invalid specifier: multiple # fragments are not supported',
670
+ content: specifier
671
+ });
40
672
  const hashIndex = firstHashIndex;
41
673
  const sourcePart = hashIndex >= 0 ? specifier.slice(0, hashIndex) : specifier;
42
674
  const fragment = hashIndex >= 0 ? specifier.slice(hashIndex + 1) : '';
43
- if (!sourcePart) throw new Error('Specifier source is required');
675
+ if (!sourcePart) throw new ParseError({
676
+ code: codes_ErrorCode.INVALID_SPECIFIER,
677
+ message: 'Specifier source is required',
678
+ content: specifier
679
+ });
44
680
  if (!fragment) return {
45
681
  sourcePart,
46
682
  ref: null,
@@ -63,8 +699,37 @@ function parseSpecifier(specifier) {
63
699
  };
64
700
  }
65
701
  function normalizeSpecifier(specifier) {
66
- const parsed = parseSpecifier(specifier);
67
- const type = parsed.sourcePart.startsWith('file:') ? 'file' : parsed.sourcePart.startsWith('npm:') ? 'npm' : 'git';
702
+ if (specifier.startsWith('link:') && specifier.includes('#')) throw new ParseError({
703
+ code: codes_ErrorCode.INVALID_SPECIFIER,
704
+ message: 'Invalid link specifier: link: must point directly to a skill directory',
705
+ content: specifier
706
+ });
707
+ let parsed;
708
+ try {
709
+ parsed = parseSpecifier(specifier);
710
+ } catch (error) {
711
+ if (error instanceof ParseError) throw error;
712
+ throw new ParseError({
713
+ code: codes_ErrorCode.INVALID_SPECIFIER,
714
+ message: `Invalid specifier: ${error.message}`,
715
+ content: specifier,
716
+ cause: error
717
+ });
718
+ }
719
+ const type = parsed.sourcePart.startsWith('link:') ? 'link' : parsed.sourcePart.startsWith('file:') ? 'file' : parsed.sourcePart.startsWith('npm:') ? 'npm' : 'git';
720
+ if ('link' === type) {
721
+ const linkSource = normalizeLinkSource(parsed.sourcePart);
722
+ const linkPath = linkSource.slice(5);
723
+ const skillName = node_path.posix.basename(linkPath);
724
+ return {
725
+ type,
726
+ source: linkSource,
727
+ ref: null,
728
+ path: '/',
729
+ normalized: linkSource,
730
+ skillName
731
+ };
732
+ }
68
733
  const skillPath = parsed.path || '/';
69
734
  const skillName = node_path.posix.basename(skillPath);
70
735
  const normalized = parsed.ref ? `${parsed.sourcePart}#${parsed.ref}&path:${skillPath}` : parsed.path ? `${parsed.sourcePart}#path:${skillPath}` : parsed.sourcePart;
@@ -80,7 +745,50 @@ function normalizeSpecifier(specifier) {
80
745
  function sha256(content) {
81
746
  return `sha256-${createHash('sha256').update(content).digest('hex')}`;
82
747
  }
748
+ function toPortablePath(filePath) {
749
+ return '/' === node_path.sep ? filePath : filePath.split(node_path.sep).join('/');
750
+ }
751
+ async function hashDirectoryEntry(hash, rootDir, currentDir) {
752
+ const entries = await readdir(currentDir, {
753
+ withFileTypes: true
754
+ });
755
+ entries.sort((a, b)=>a.name.localeCompare(b.name));
756
+ for (const entry of entries){
757
+ const absolutePath = node_path.join(currentDir, entry.name);
758
+ const relativePath = toPortablePath(node_path.relative(rootDir, absolutePath));
759
+ const stats = await lstat(absolutePath);
760
+ if (stats.isSymbolicLink()) {
761
+ hash.update(`symlink:${relativePath}\n`);
762
+ hash.update(await readlink(absolutePath));
763
+ hash.update('\n');
764
+ continue;
765
+ }
766
+ if (stats.isDirectory()) {
767
+ hash.update(`dir:${relativePath}\n`);
768
+ await hashDirectoryEntry(hash, rootDir, absolutePath);
769
+ continue;
770
+ }
771
+ hash.update(`file:${relativePath}\n`);
772
+ hash.update(await readFile(absolutePath));
773
+ hash.update('\n');
774
+ }
775
+ }
776
+ async function sha256Directory(rootDir) {
777
+ const hash = createHash('sha256');
778
+ await hashDirectoryEntry(hash, rootDir, rootDir);
779
+ return `sha256-${hash.digest('hex')}`;
780
+ }
781
+ async function sha256File(filePath, suffix = '') {
782
+ const hash = createHash('sha256');
783
+ for await (const chunk of createReadStream(filePath))hash.update(chunk);
784
+ if (suffix) hash.update(suffix);
785
+ return `sha256-${hash.digest('hex')}`;
786
+ }
83
787
  const execFileAsync = promisify(execFile);
788
+ function toPortableRelativePath(from, to) {
789
+ const relativePath = node_path.relative(from, to) || '.';
790
+ return '/' === node_path.sep ? relativePath : relativePath.split(node_path.sep).join('/');
791
+ }
84
792
  async function resolveGitCommitByLsRemote(url, target) {
85
793
  try {
86
794
  const { stdout } = await execFileAsync('git', [
@@ -118,7 +826,7 @@ async function resolveGitCommitByClone(url, target) {
118
826
  } catch {
119
827
  return null;
120
828
  } finally{
121
- await promises_rm(checkoutRoot, {
829
+ await rm(checkoutRoot, {
122
830
  recursive: true,
123
831
  force: true
124
832
  }).catch(()=>{});
@@ -130,28 +838,61 @@ async function resolveGitCommit(url, ref) {
130
838
  if (commit) return commit;
131
839
  const clonedCommit = await resolveGitCommitByClone(url, target);
132
840
  if (clonedCommit) return clonedCommit;
133
- throw new Error(`Unable to resolve git ref ${target} for ${url}`);
841
+ throw new GitError({
842
+ code: codes_ErrorCode.GIT_REF_NOT_FOUND,
843
+ operation: 'resolve-ref',
844
+ repoUrl: url,
845
+ ref: target,
846
+ message: `Unable to resolve git ref "${target}" for ${url}`
847
+ });
134
848
  }
135
- async function resolveLockEntry(cwd, specifier) {
136
- const normalized = normalizeSpecifier(specifier);
137
- if ('file' === normalized.type) {
849
+ async function resolveLockEntry(cwd, specifier, skillName) {
850
+ let normalized;
851
+ try {
852
+ normalized = normalizeSpecifier(specifier);
853
+ } catch (error) {
854
+ if (error instanceof ParseError) throw error;
855
+ throw new ParseError({
856
+ code: codes_ErrorCode.INVALID_SPECIFIER,
857
+ message: `Failed to parse specifier "${specifier}": ${error.message}`,
858
+ content: specifier,
859
+ cause: error
860
+ });
861
+ }
862
+ const finalSkillName = skillName || normalized.skillName;
863
+ if ('link' === normalized.type) {
138
864
  const sourceRoot = node_path.resolve(cwd, normalized.source.slice(5));
139
865
  return {
140
- skillName: normalized.skillName,
866
+ skillName: finalSkillName,
867
+ entry: {
868
+ specifier: normalized.normalized,
869
+ resolution: {
870
+ type: 'link',
871
+ path: toPortableRelativePath(cwd, sourceRoot)
872
+ },
873
+ digest: await sha256Directory(sourceRoot)
874
+ }
875
+ };
876
+ }
877
+ if ('file' === normalized.type) {
878
+ const tarballPath = node_path.resolve(cwd, normalized.source.slice(5));
879
+ return {
880
+ skillName: finalSkillName,
141
881
  entry: {
142
882
  specifier: normalized.normalized,
143
883
  resolution: {
144
884
  type: 'file',
145
- path: node_path.relative(cwd, sourceRoot) || '.'
885
+ tarball: toPortableRelativePath(cwd, tarballPath),
886
+ path: normalized.path
146
887
  },
147
- digest: sha256(`${sourceRoot}:${normalized.path}`)
888
+ digest: await sha256File(tarballPath, `:${normalized.path}`)
148
889
  }
149
890
  };
150
891
  }
151
892
  if ('git' === normalized.type) {
152
893
  const commit = await resolveGitCommit(normalized.source, normalized.ref);
153
894
  return {
154
- skillName: normalized.skillName,
895
+ skillName: finalSkillName,
155
896
  entry: {
156
897
  specifier: normalized.normalized,
157
898
  resolution: {
@@ -164,14 +905,52 @@ async function resolveLockEntry(cwd, specifier) {
164
905
  }
165
906
  };
166
907
  }
167
- throw new Error(`Unsupported specifier type in 0.1.0 core flow: ${normalized.type}`);
168
- }
169
- async function syncSkillsLock(cwd, manifest, existingLock) {
170
- const nextSkills = {};
171
- for (const specifier of Object.values(manifest.skills)){
172
- const { skillName, entry } = await resolveLockEntry(cwd, specifier);
173
- nextSkills[skillName] = entry;
908
+ if ('npm' === normalized.type) {
909
+ const packageSpecifier = normalized.source.slice(4);
910
+ const resolved = await resolveNpmPackage(cwd, packageSpecifier);
911
+ return {
912
+ skillName: finalSkillName,
913
+ entry: {
914
+ specifier: normalized.normalized,
915
+ resolution: {
916
+ type: 'npm',
917
+ packageName: resolved.name,
918
+ version: resolved.version,
919
+ path: normalized.path,
920
+ tarball: resolved.tarballUrl,
921
+ integrity: resolved.integrity,
922
+ registry: resolved.registry
923
+ },
924
+ digest: sha256([
925
+ resolved.name,
926
+ resolved.version,
927
+ resolved.tarballUrl,
928
+ resolved.integrity ?? '',
929
+ resolved.registry ?? '',
930
+ normalized.path
931
+ ].join(':'))
932
+ }
933
+ };
174
934
  }
935
+ throw new ParseError({
936
+ code: codes_ErrorCode.INVALID_SPECIFIER,
937
+ message: `Unsupported specifier type in 0.1.0 core flow: ${normalized.type}`,
938
+ content: specifier
939
+ });
940
+ }
941
+ async function syncSkillsLock(cwd, manifest, _existingLock, options) {
942
+ const entries = await Promise.all(Object.entries(manifest.skills).map(async ([skillName, specifier])=>{
943
+ const { skillName: resolvedName, entry } = await resolveLockEntry(cwd, specifier, skillName);
944
+ options?.onProgress?.({
945
+ type: 'resolved',
946
+ skillName: resolvedName
947
+ });
948
+ return [
949
+ resolvedName,
950
+ entry
951
+ ];
952
+ }));
953
+ const nextSkills = Object.fromEntries(entries);
175
954
  return {
176
955
  lockfileVersion: '0.1',
177
956
  installDir: manifest.installDir ?? '.agents/skills',
@@ -181,7 +960,14 @@ async function syncSkillsLock(cwd, manifest, existingLock) {
181
960
  }
182
961
  async function writeSkillsLock(rootDir, lockfile) {
183
962
  const filePath = node_path.join(rootDir, 'skills-lock.yaml');
184
- await writeFile(filePath, yaml.stringify(lockfile), 'utf8');
963
+ try {
964
+ await writeFile(filePath, yaml.stringify(lockfile), 'utf8');
965
+ } catch (error) {
966
+ throw convertNodeError(error, {
967
+ operation: 'write',
968
+ path: filePath
969
+ });
970
+ }
185
971
  }
186
972
  async function writeSkillsManifest(rootDir, manifest) {
187
973
  const filePath = node_path.join(rootDir, 'skills.json');
@@ -190,7 +976,14 @@ async function writeSkillsManifest(rootDir, manifest) {
190
976
  linkTargets: manifest.linkTargets ?? [],
191
977
  skills: manifest.skills
192
978
  };
193
- await writeFile(filePath, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8');
979
+ try {
980
+ await writeFile(filePath, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8');
981
+ } catch (error) {
982
+ throw convertNodeError(error, {
983
+ operation: 'write',
984
+ path: filePath
985
+ });
986
+ }
194
987
  }
195
988
  const listSkills_execFileAsync = promisify(execFile);
196
989
  const SKIP_DIRS = new Set([
@@ -226,7 +1019,7 @@ async function parseSkillDir(dir, relativePath) {
226
1019
  try {
227
1020
  const content = await readFile(join(dir, 'SKILL.md'), 'utf8');
228
1021
  const meta = parseSkillFrontmatter(content);
229
- const dirName = dir.split('/').pop() ?? '';
1022
+ const dirName = basename(dir);
230
1023
  return {
231
1024
  name: meta.name || dirName,
232
1025
  description: meta.description,
@@ -268,189 +1061,138 @@ async function cloneAndDiscover(gitUrl, ref) {
268
1061
  ref,
269
1062
  gitUrl,
270
1063
  tempDir
271
- ] : [
272
- 'clone',
273
- '--depth',
274
- '1',
275
- gitUrl,
276
- tempDir
277
- ];
278
- await listSkills_execFileAsync('git', cloneArgs, {
279
- env: {
280
- ...process.env,
281
- GIT_TERMINAL_PROMPT: '0'
282
- },
283
- timeout: 60000
284
- });
285
- const skills = await discoverSkillsInDir(tempDir);
286
- return {
287
- skills,
288
- cleanup: async ()=>{
289
- await promises_rm(tempDir, {
290
- recursive: true,
291
- force: true
292
- }).catch(()=>{});
293
- }
294
- };
295
- } catch (error) {
296
- await promises_rm(tempDir, {
297
- recursive: true,
298
- force: true
299
- }).catch(()=>{});
300
- throw error;
301
- }
302
- }
303
- async function discoverSkillsInDir(baseDir) {
304
- const rootSkills = await scanForSkills(baseDir, '');
305
- if (rootSkills.length > 0) {
306
- rootSkills.sort((a, b)=>a.name.localeCompare(b.name));
307
- return rootSkills;
308
- }
309
- const commonDirs = [
310
- 'skills',
311
- '.agents/skills',
312
- '.claude/skills',
313
- '.github/skills'
314
- ];
315
- for (const dir of commonDirs){
316
- const skills = await scanForSkills(baseDir, dir);
317
- if (skills.length > 0) {
318
- skills.sort((a, b)=>a.name.localeCompare(b.name));
319
- return skills;
320
- }
321
- }
322
- return [];
323
- }
324
- async function listRepoSkills(owner, repo, ref) {
325
- const gitUrl = `https://github.com/${owner}/${repo}.git`;
326
- const { skills, cleanup } = await cloneAndDiscover(gitUrl, ref);
327
- await cleanup();
328
- return skills;
329
- }
330
- function parseOwnerRepo(input) {
331
- const match = input.match(/^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)$/);
332
- if (!match) return null;
333
- return {
334
- owner: match[1],
335
- repo: match[2]
336
- };
337
- }
338
- function parseGitHubUrl(input) {
339
- const match = input.match(/^https?:\/\/github\.com\/([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+?)(?:\.git)?\/?$/);
340
- if (!match) return null;
341
- return {
342
- owner: match[1],
343
- repo: match[2]
344
- };
345
- }
346
- const UNIVERSAL_AGENT_NAMES = [
347
- 'Amp',
348
- 'Antigravity',
349
- 'Cline',
350
- 'Codex',
351
- 'Cursor',
352
- 'Deep Agents',
353
- 'Firebender',
354
- 'Gemini CLI',
355
- 'GitHub Copilot',
356
- 'Kimi Code CLI',
357
- 'OpenCode',
358
- 'Warp'
359
- ];
360
- const ADDITIONAL_AGENT_TARGETS = [
361
- {
362
- label: 'Augment',
363
- path: '.augment/skills'
364
- },
365
- {
366
- label: 'IBM Bob',
367
- path: '.bob/skills'
368
- },
369
- {
370
- label: 'Claude Code',
371
- path: '.claude/skills'
372
- },
373
- {
374
- label: 'OpenClaw',
375
- path: 'skills'
376
- },
377
- {
378
- label: 'CodeBuddy',
379
- path: '.codebuddy/skills'
380
- },
381
- {
382
- label: 'Command Code',
383
- path: '.commandcode/skills'
384
- },
385
- {
386
- label: 'Continue',
387
- path: '.continue/skills'
388
- },
389
- {
390
- label: 'Cortex Code',
391
- path: '.cortex/skills'
392
- },
393
- {
394
- label: 'Trae',
395
- path: '.trae/skills'
396
- }
397
- ];
398
- async function promptSkillSelection(skills) {
399
- if (0 === skills.length) throw new Error('No skills found in repository');
400
- if (1 === skills.length) return skills;
401
- const options = skills.map((skill)=>({
402
- value: skill,
403
- label: skill.name,
404
- hint: skill.description ? skill.description.length > 60 ? `${skill.description.slice(0, 57)}...` : skill.description : void 0
405
- }));
406
- const selected = await __rspack_external__clack_prompts_3cae1695.multiselect({
407
- message: 'Select skills to install',
408
- options,
409
- required: true
410
- });
411
- if (__rspack_external__clack_prompts_3cae1695.isCancel(selected)) {
412
- __rspack_external__clack_prompts_3cae1695.cancel('Installation cancelled');
413
- process.exit(0);
1064
+ ] : [
1065
+ 'clone',
1066
+ '--depth',
1067
+ '1',
1068
+ gitUrl,
1069
+ tempDir
1070
+ ];
1071
+ await listSkills_execFileAsync('git', cloneArgs, {
1072
+ env: {
1073
+ ...process.env,
1074
+ GIT_TERMINAL_PROMPT: '0'
1075
+ },
1076
+ timeout: 60000
1077
+ });
1078
+ const skills = await discoverSkillsInDir(tempDir);
1079
+ return {
1080
+ skills,
1081
+ cleanup: async ()=>{
1082
+ await rm(tempDir, {
1083
+ recursive: true,
1084
+ force: true
1085
+ }).catch(()=>{});
1086
+ }
1087
+ };
1088
+ } catch (error) {
1089
+ await rm(tempDir, {
1090
+ recursive: true,
1091
+ force: true
1092
+ }).catch(()=>{});
1093
+ throw error;
414
1094
  }
415
- return selected;
416
1095
  }
417
- async function promptInitManifestOptions(promptApi = __rspack_external__clack_prompts_3cae1695, exit = process.exit) {
418
- const installDirInput = await promptApi.text({
419
- message: 'Where should skills be installed?',
420
- initialValue: '.agents/skills'
421
- });
422
- if (promptApi.isCancel(installDirInput)) {
423
- promptApi.cancel('Initialization cancelled');
424
- exit(0);
1096
+ async function discoverSkillsInDir(baseDir) {
1097
+ const rootSkills = await scanForSkills(baseDir, '');
1098
+ if (rootSkills.length > 0) {
1099
+ rootSkills.sort((a, b)=>a.name.localeCompare(b.name));
1100
+ return rootSkills;
425
1101
  }
426
- promptApi.note(UNIVERSAL_AGENT_NAMES.join('\n'), 'Universal (.agents/skills) — always included');
427
- const selected = await promptApi.groupMultiselect({
428
- message: 'Which agents do you want to install to?',
429
- options: {
430
- 'Additional agents': ADDITIONAL_AGENT_TARGETS.map((agent)=>({
431
- value: agent.path,
432
- label: `${agent.label} (${agent.path})`
433
- }))
434
- },
435
- required: false
436
- });
437
- if (promptApi.isCancel(selected)) {
438
- promptApi.cancel('Initialization cancelled');
439
- exit(0);
1102
+ const commonDirs = [
1103
+ 'skills',
1104
+ '.agents/skills',
1105
+ '.claude/skills',
1106
+ '.github/skills'
1107
+ ];
1108
+ for (const dir of commonDirs){
1109
+ const skills = await scanForSkills(baseDir, dir);
1110
+ if (skills.length > 0) {
1111
+ skills.sort((a, b)=>a.name.localeCompare(b.name));
1112
+ return skills;
1113
+ }
440
1114
  }
441
- const installDir = String(installDirInput).trim() || '.agents/skills';
1115
+ return [];
1116
+ }
1117
+ async function listRepoSkills(owner, repo, ref) {
1118
+ const gitUrl = `https://github.com/${owner}/${repo}.git`;
1119
+ const { skills, cleanup } = await cloneAndDiscover(gitUrl, ref);
1120
+ await cleanup();
1121
+ return skills;
1122
+ }
1123
+ function parseOwnerRepo(input) {
1124
+ const match = input.match(/^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)$/);
1125
+ if (!match) return null;
442
1126
  return {
443
- installDir,
444
- linkTargets: selected
1127
+ owner: match[1],
1128
+ repo: match[2]
1129
+ };
1130
+ }
1131
+ function parseGitHubUrl(input) {
1132
+ const match = input.match(/^https?:\/\/github\.com\/([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+?)(?:\.git)?\/?$/);
1133
+ if (!match) return null;
1134
+ return {
1135
+ owner: match[1],
1136
+ repo: match[2]
1137
+ };
1138
+ }
1139
+ function parseForComparison(specifier) {
1140
+ const parsed = parseSpecifier(specifier);
1141
+ const isLink = parsed.sourcePart.startsWith('link:');
1142
+ return {
1143
+ sourcePart: isLink ? normalizeLinkSource(parsed.sourcePart) : parsed.sourcePart,
1144
+ ref: isLink ? null : parsed.ref,
1145
+ path: isLink ? '/' : parsed.path || '/'
445
1146
  };
446
1147
  }
1148
+ function isSpecifierCompatible(manifestSpecifier, lockSpecifier) {
1149
+ const manifest = parseForComparison(manifestSpecifier);
1150
+ const lock = parseForComparison(lockSpecifier);
1151
+ if (manifest.sourcePart !== lock.sourcePart) return false;
1152
+ if (manifest.path !== lock.path) return false;
1153
+ if (null === manifest.ref) return true;
1154
+ return manifest.ref === lock.ref;
1155
+ }
1156
+ function normalizeInstallDir(dir) {
1157
+ return dir ?? '.agents/skills';
1158
+ }
1159
+ function normalizeLinkTargets(targets) {
1160
+ return targets ?? [];
1161
+ }
1162
+ function arraysEqual(a, b) {
1163
+ if (a.length !== b.length) return false;
1164
+ return a.every((val, i)=>val === b[i]);
1165
+ }
1166
+ function isLockInSync(manifest, lock) {
1167
+ if (!lock) return false;
1168
+ if (normalizeInstallDir(manifest.installDir) !== normalizeInstallDir(lock.installDir)) return false;
1169
+ if (!arraysEqual(normalizeLinkTargets(manifest.linkTargets), normalizeLinkTargets(lock.linkTargets))) return false;
1170
+ const manifestSkills = Object.entries(manifest.skills);
1171
+ const lockSkillNames = Object.keys(lock.skills);
1172
+ if (manifestSkills.length !== lockSkillNames.length) return false;
1173
+ for (const [name, specifier] of manifestSkills){
1174
+ const lockEntry = lock.skills[name];
1175
+ if (!lockEntry) return false;
1176
+ if (!isSpecifierCompatible(specifier, lockEntry.specifier)) return false;
1177
+ }
1178
+ return true;
1179
+ }
447
1180
  async function ensureDir(dirPath) {
448
1181
  await mkdir(dirPath, {
449
1182
  recursive: true
450
1183
  });
451
1184
  }
1185
+ async function replaceDir(from, to) {
1186
+ await rm(to, {
1187
+ recursive: true,
1188
+ force: true
1189
+ });
1190
+ await cp(from, to, {
1191
+ recursive: true
1192
+ });
1193
+ }
452
1194
  async function replaceSymlink(target, linkPath) {
453
- await promises_rm(linkPath, {
1195
+ await rm(linkPath, {
454
1196
  recursive: true,
455
1197
  force: true
456
1198
  });
@@ -459,26 +1201,27 @@ async function replaceSymlink(target, linkPath) {
459
1201
  async function writeJson(filePath, value) {
460
1202
  await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
461
1203
  }
462
- async function linkSkill(rootDir, installDir, linkTarget, skillName) {
463
- const absoluteTarget = node_path.join(rootDir, installDir, skillName);
464
- const absoluteLink = node_path.join(rootDir, linkTarget, skillName);
465
- await ensureDir(node_path.dirname(absoluteLink));
466
- await replaceSymlink(absoluteTarget, absoluteLink);
467
- }
468
- async function readInstallState(rootDir) {
469
- const filePath = node_path.join(rootDir, '.agents/skills/.skills-pm-install-state.json');
1204
+ const INSTALL_STATE_FILE = '.skills-pm-install-state.json';
1205
+ async function readInstallState(rootDir, installDir) {
1206
+ const filePath = node_path.join(rootDir, installDir, INSTALL_STATE_FILE);
470
1207
  try {
471
1208
  return JSON.parse(await readFile(filePath, 'utf8'));
472
1209
  } catch {
473
1210
  return null;
474
1211
  }
475
1212
  }
476
- async function writeInstallState(rootDir, value) {
477
- const dirPath = node_path.join(rootDir, '.agents/skills');
1213
+ async function writeInstallState(rootDir, installDir, value) {
1214
+ const dirPath = node_path.join(rootDir, installDir);
478
1215
  await ensureDir(dirPath);
479
- const filePath = node_path.join(dirPath, '.skills-pm-install-state.json');
1216
+ const filePath = node_path.join(dirPath, INSTALL_STATE_FILE);
480
1217
  await writeJson(filePath, value);
481
1218
  }
1219
+ async function linkSkill(rootDir, installDir, linkTarget, skillName) {
1220
+ const absoluteTarget = node_path.join(rootDir, installDir, skillName);
1221
+ const absoluteLink = node_path.join(rootDir, linkTarget, skillName);
1222
+ await ensureDir(node_path.dirname(absoluteLink));
1223
+ await replaceSymlink(absoluteTarget, absoluteLink);
1224
+ }
482
1225
  async function materializeLocalSkill(rootDir, skillName, sourceRoot, sourcePath, installDir) {
483
1226
  const relativeSkillPath = sourcePath.replace(/^\//, '');
484
1227
  const absoluteSkillPath = node_path.join(sourceRoot, relativeSkillPath);
@@ -492,10 +1235,7 @@ async function materializeLocalSkill(rootDir, skillName, sourceRoot, sourcePath,
492
1235
  if (!skillDoc) throw new Error(`Invalid skill at ${absoluteSkillPath}: missing SKILL.md`);
493
1236
  const targetDir = node_path.join(rootDir, installDir, skillName);
494
1237
  await ensureDir(node_path.dirname(targetDir));
495
- await promises_cp(absoluteSkillPath, targetDir, {
496
- recursive: true,
497
- force: true
498
- });
1238
+ await replaceDir(absoluteSkillPath, targetDir);
499
1239
  await writeJson(node_path.join(targetDir, '.skills-pm.json'), {
500
1240
  name: skillName,
501
1241
  installedBy: 'skills-package-manager',
@@ -504,14 +1244,24 @@ async function materializeLocalSkill(rootDir, skillName, sourceRoot, sourcePath,
504
1244
  }
505
1245
  const materializeGitSkill_execFileAsync = promisify(execFile);
506
1246
  async function checkoutCommit(checkoutRoot, commit) {
507
- await materializeGitSkill_execFileAsync('git', [
508
- 'checkout',
509
- commit
510
- ], {
511
- cwd: checkoutRoot
512
- });
1247
+ try {
1248
+ await materializeGitSkill_execFileAsync('git', [
1249
+ 'checkout',
1250
+ commit
1251
+ ], {
1252
+ cwd: checkoutRoot
1253
+ });
1254
+ } catch (error) {
1255
+ throw new GitError({
1256
+ code: codes_ErrorCode.GIT_CHECKOUT_FAILED,
1257
+ operation: 'checkout',
1258
+ ref: commit,
1259
+ message: `Failed to checkout commit ${commit}`,
1260
+ cause: error
1261
+ });
1262
+ }
513
1263
  }
514
- async function fetchCommitFallback(checkoutRoot, commit) {
1264
+ async function fetchCommitFallback(checkoutRoot, commit, _repoUrl) {
515
1265
  try {
516
1266
  await materializeGitSkill_execFileAsync('git', [
517
1267
  'fetch',
@@ -554,29 +1304,65 @@ async function fetchCommitFallback(checkoutRoot, commit) {
554
1304
  async function materializeGitSkill(rootDir, skillName, repoUrl, commit, sourcePath, installDir) {
555
1305
  const checkoutRoot = await mkdtemp(node_path.join(tmpdir(), 'skills-pm-git-checkout-'));
556
1306
  try {
557
- await materializeGitSkill_execFileAsync('git', [
558
- 'clone',
559
- '--depth',
560
- '1',
561
- repoUrl,
562
- checkoutRoot
563
- ]);
1307
+ try {
1308
+ await materializeGitSkill_execFileAsync('git', [
1309
+ 'clone',
1310
+ '--depth',
1311
+ '1',
1312
+ repoUrl,
1313
+ checkoutRoot
1314
+ ]);
1315
+ } catch (error) {
1316
+ throw new GitError({
1317
+ code: codes_ErrorCode.GIT_CLONE_FAILED,
1318
+ operation: 'clone',
1319
+ repoUrl,
1320
+ message: `Failed to clone repository ${repoUrl}`,
1321
+ cause: error
1322
+ });
1323
+ }
564
1324
  if (commit && 'HEAD' !== commit) try {
565
1325
  await checkoutCommit(checkoutRoot, commit);
566
- } catch {
567
- await fetchCommitFallback(checkoutRoot, commit);
568
- await checkoutCommit(checkoutRoot, commit);
1326
+ } catch (checkoutError) {
1327
+ if (checkoutError instanceof GitError) try {
1328
+ await fetchCommitFallback(checkoutRoot, commit, repoUrl);
1329
+ await checkoutCommit(checkoutRoot, commit);
1330
+ } catch {
1331
+ throw checkoutError;
1332
+ }
1333
+ else throw checkoutError;
569
1334
  }
570
1335
  const skillDocPath = node_path.join(checkoutRoot, sourcePath.replace(/^\//, ''), 'SKILL.md');
571
1336
  await readFile(skillDocPath, 'utf8');
572
1337
  await materializeLocalSkill(rootDir, skillName, checkoutRoot, sourcePath, installDir);
573
1338
  } finally{
574
- await promises_rm(checkoutRoot, {
1339
+ await rm(checkoutRoot, {
575
1340
  recursive: true,
576
1341
  force: true
577
1342
  });
578
1343
  }
579
1344
  }
1345
+ async function materializePackedSkill(rootDir, skillName, tarballPath, sourcePath, installDir) {
1346
+ const extractRoot = await mkdtemp(node_path.join(tmpdir(), 'skills-pm-packed-skill-'));
1347
+ try {
1348
+ await mkdir(node_path.join(extractRoot, 'package'), {
1349
+ recursive: true
1350
+ });
1351
+ await x({
1352
+ file: tarballPath,
1353
+ cwd: node_path.join(extractRoot, 'package'),
1354
+ strip: 1,
1355
+ preservePaths: false,
1356
+ strict: true
1357
+ });
1358
+ await materializeLocalSkill(rootDir, skillName, node_path.join(extractRoot, 'package'), sourcePath, installDir);
1359
+ } finally{
1360
+ await rm(extractRoot, {
1361
+ recursive: true,
1362
+ force: true
1363
+ }).catch(()=>{});
1364
+ }
1365
+ }
580
1366
  async function isManagedSkillDir(dirPath) {
581
1367
  try {
582
1368
  const marker = JSON.parse(await readFile(node_path.join(dirPath, '.skills-pm.json'), 'utf8'));
@@ -595,7 +1381,7 @@ async function pruneManagedSkills(rootDir, installDir, linkTargets, wantedSkillN
595
1381
  const skillDir = node_path.join(absoluteInstallDir, entry);
596
1382
  if (await isManagedSkillDir(skillDir)) {
597
1383
  if (!wanted.has(entry)) {
598
- await promises_rm(skillDir, {
1384
+ await rm(skillDir, {
599
1385
  recursive: true,
600
1386
  force: true
601
1387
  });
@@ -603,7 +1389,7 @@ async function pruneManagedSkills(rootDir, installDir, linkTargets, wantedSkillN
603
1389
  const linkPath = node_path.join(rootDir, linkTarget, entry);
604
1390
  try {
605
1391
  const stat = await lstat(linkPath);
606
- if (stat.isSymbolicLink() || stat.isDirectory() || stat.isFile()) await promises_rm(linkPath, {
1392
+ if (stat.isSymbolicLink() || stat.isDirectory() || stat.isFile()) await rm(linkPath, {
607
1393
  recursive: true,
608
1394
  force: true
609
1395
  });
@@ -617,66 +1403,129 @@ async function pruneManagedSkills(rootDir, installDir, linkTargets, wantedSkillN
617
1403
  const installStageHooks = {
618
1404
  beforeFetch: async (_rootDir, _manifest, _lockfile)=>{}
619
1405
  };
620
- function extractSkillPath(specifier, skillName) {
621
- const marker = '#path:';
622
- const index = specifier.indexOf(marker);
623
- if (index >= 0) return specifier.slice(index + marker.length);
624
- return `/${skillName}`;
1406
+ async function areManagedSkillsInstalled(rootDir, installDir, skillNames) {
1407
+ for (const skillName of skillNames)try {
1408
+ await access(node_path.join(rootDir, installDir, skillName, 'SKILL.md'));
1409
+ } catch {
1410
+ return false;
1411
+ }
1412
+ return true;
625
1413
  }
626
- async function fetchSkillsFromLock(rootDir, manifest, lockfile) {
1414
+ async function fetchSkillsFromLock(rootDir, manifest, lockfile, options) {
627
1415
  await installStageHooks.beforeFetch(rootDir, manifest, lockfile);
1416
+ const installDir = manifest.installDir ?? '.agents/skills';
1417
+ const linkTargets = manifest.linkTargets ?? [];
1418
+ await pruneManagedSkills(rootDir, installDir, linkTargets, Object.keys(lockfile.skills));
628
1419
  const lockDigest = sha256(JSON.stringify(lockfile));
629
- const state = await readInstallState(rootDir);
630
- if (state?.lockDigest === lockDigest) return {
1420
+ const state = await readInstallState(rootDir, installDir);
1421
+ if (state?.lockDigest === lockDigest && await areManagedSkillsInstalled(rootDir, installDir, Object.keys(lockfile.skills))) return {
631
1422
  status: 'skipped',
632
1423
  reason: 'up-to-date'
633
1424
  };
634
- const installDir = manifest.installDir ?? '.agents/skills';
635
- const linkTargets = manifest.linkTargets ?? [];
636
- await pruneManagedSkills(rootDir, installDir, linkTargets, Object.keys(lockfile.skills));
637
- for (const [skillName, entry] of Object.entries(lockfile.skills)){
638
- if ('file' === entry.resolution.type) {
639
- await materializeLocalSkill(rootDir, skillName, node_path.resolve(rootDir, entry.resolution.path), extractSkillPath(entry.specifier, skillName), installDir);
640
- continue;
641
- }
642
- if ('git' === entry.resolution.type) {
643
- await materializeGitSkill(rootDir, skillName, entry.resolution.url, entry.resolution.commit, entry.resolution.path, installDir);
644
- continue;
1425
+ const downloadedTarballs = new Map();
1426
+ try {
1427
+ for (const [skillName, entry] of Object.entries(lockfile.skills)){
1428
+ if ('link' === entry.resolution.type) {
1429
+ await materializeLocalSkill(rootDir, skillName, node_path.resolve(rootDir, entry.resolution.path), '/', installDir);
1430
+ options?.onProgress?.({
1431
+ type: 'added',
1432
+ skillName
1433
+ });
1434
+ continue;
1435
+ }
1436
+ if ('file' === entry.resolution.type) {
1437
+ await materializePackedSkill(rootDir, skillName, node_path.resolve(rootDir, entry.resolution.tarball), entry.resolution.path, installDir);
1438
+ options?.onProgress?.({
1439
+ type: 'added',
1440
+ skillName
1441
+ });
1442
+ continue;
1443
+ }
1444
+ if ('git' === entry.resolution.type) {
1445
+ await materializeGitSkill(rootDir, skillName, entry.resolution.url, entry.resolution.commit, entry.resolution.path, installDir);
1446
+ options?.onProgress?.({
1447
+ type: 'added',
1448
+ skillName
1449
+ });
1450
+ continue;
1451
+ }
1452
+ if ('npm' === entry.resolution.type) {
1453
+ const cacheKey = `${entry.resolution.tarball}\0${entry.resolution.integrity ?? ''}`;
1454
+ let tarballPathPromise = downloadedTarballs.get(cacheKey);
1455
+ if (!tarballPathPromise) {
1456
+ tarballPathPromise = downloadNpmPackageTarball(rootDir, entry.resolution.tarball, entry.resolution.integrity);
1457
+ downloadedTarballs.set(cacheKey, tarballPathPromise);
1458
+ }
1459
+ const tarballPath = await tarballPathPromise;
1460
+ await materializePackedSkill(rootDir, skillName, tarballPath, entry.resolution.path, installDir);
1461
+ options?.onProgress?.({
1462
+ type: 'added',
1463
+ skillName
1464
+ });
1465
+ continue;
1466
+ }
1467
+ throw new Error(`Unsupported resolution type in 0.1.0 core flow: ${entry.resolution.type}`);
645
1468
  }
646
- throw new Error(`Unsupported resolution type in 0.1.0 core flow: ${entry.resolution.type}`);
1469
+ await writeInstallState(rootDir, installDir, {
1470
+ lockDigest,
1471
+ installDir,
1472
+ linkTargets,
1473
+ installerVersion: '0.1.0',
1474
+ installedAt: new Date().toISOString()
1475
+ });
1476
+ } finally{
1477
+ const settledTarballs = await Promise.allSettled(downloadedTarballs.values());
1478
+ const downloadedPaths = new Set(settledTarballs.filter((result)=>'fulfilled' === result.status).map((result)=>result.value));
1479
+ await Promise.all([
1480
+ ...downloadedPaths
1481
+ ].map((tarballPath)=>cleanupPackedNpmPackage(tarballPath)));
647
1482
  }
648
- await writeInstallState(rootDir, {
649
- lockDigest,
650
- installDir,
651
- linkTargets,
652
- installerVersion: '0.1.0',
653
- installedAt: new Date().toISOString()
654
- });
655
1483
  return {
656
1484
  status: 'fetched',
657
1485
  fetched: Object.keys(lockfile.skills)
658
1486
  };
659
1487
  }
660
- async function linkSkillsFromLock(rootDir, manifest, lockfile) {
1488
+ async function linkSkillsFromLock(rootDir, manifest, lockfile, options) {
661
1489
  const installDir = manifest.installDir ?? '.agents/skills';
662
1490
  const linkTargets = manifest.linkTargets ?? [];
663
- for (const skillName of Object.keys(lockfile.skills))for (const linkTarget of linkTargets)await linkSkill(rootDir, installDir, linkTarget, skillName);
1491
+ for (const skillName of Object.keys(lockfile.skills)){
1492
+ for (const linkTarget of linkTargets)await linkSkill(rootDir, installDir, linkTarget, skillName);
1493
+ options?.onProgress?.({
1494
+ type: 'installed',
1495
+ skillName
1496
+ });
1497
+ }
664
1498
  return {
665
1499
  status: 'linked',
666
1500
  linked: Object.keys(lockfile.skills)
667
1501
  };
668
1502
  }
669
- async function installSkills(rootDir) {
1503
+ async function installSkills(rootDir, options) {
670
1504
  const manifest = await readSkillsManifest(rootDir);
671
1505
  if (!manifest) return {
672
1506
  status: 'skipped',
673
1507
  reason: 'manifest-missing'
674
1508
  };
675
1509
  const currentLock = await readSkillsLock(rootDir);
676
- const lockfile = await syncSkillsLock(rootDir, manifest, currentLock);
677
- await fetchSkillsFromLock(rootDir, manifest, lockfile);
678
- await linkSkillsFromLock(rootDir, manifest, lockfile);
679
- await writeSkillsLock(rootDir, lockfile);
1510
+ let lockfile;
1511
+ if (options?.frozenLockfile) {
1512
+ if (!currentLock) throw new Error('Lockfile is required in frozen mode but none was found');
1513
+ if (!isLockInSync(manifest, currentLock)) throw new Error('Lockfile is out of sync with manifest. Run install without --frozen-lockfile to update.');
1514
+ lockfile = currentLock;
1515
+ for (const skillName of Object.keys(lockfile.skills))options?.onProgress?.({
1516
+ type: 'resolved',
1517
+ skillName
1518
+ });
1519
+ } else lockfile = await syncSkillsLock(rootDir, manifest, currentLock, {
1520
+ onProgress: options?.onProgress
1521
+ });
1522
+ await fetchSkillsFromLock(rootDir, manifest, lockfile, {
1523
+ onProgress: options?.onProgress
1524
+ });
1525
+ await linkSkillsFromLock(rootDir, manifest, lockfile, {
1526
+ onProgress: options?.onProgress
1527
+ });
1528
+ if (!options?.frozenLockfile) await writeSkillsLock(rootDir, lockfile);
680
1529
  return {
681
1530
  status: 'installed',
682
1531
  installed: Object.keys(lockfile.skills)
@@ -686,14 +1535,29 @@ function buildGitHubSpecifier(owner, repo, skillPath) {
686
1535
  return `https://github.com/${owner}/${repo}.git#path:${skillPath}`;
687
1536
  }
688
1537
  async function addSingleSkill(cwd, specifier) {
689
- const normalized = normalizeSpecifier(specifier);
1538
+ let normalized;
1539
+ try {
1540
+ normalized = normalizeSpecifier(specifier);
1541
+ } catch (error) {
1542
+ if (error instanceof ParseError) throw error;
1543
+ throw new ParseError({
1544
+ code: codes_ErrorCode.INVALID_SPECIFIER,
1545
+ message: `Invalid specifier: ${error.message}`,
1546
+ content: specifier,
1547
+ cause: error
1548
+ });
1549
+ }
690
1550
  const existingManifest = await readSkillsManifest(cwd) ?? {
691
1551
  installDir: '.agents/skills',
692
1552
  linkTargets: [],
693
1553
  skills: {}
694
1554
  };
695
1555
  const existing = existingManifest.skills[normalized.skillName];
696
- if (existing && existing !== normalized.normalized) throw new Error(`Skill ${normalized.skillName} already exists with a different specifier`);
1556
+ if (existing && existing !== normalized.normalized) throw new SkillError({
1557
+ code: codes_ErrorCode.SKILL_EXISTS,
1558
+ skillName: normalized.skillName,
1559
+ message: `Skill ${normalized.skillName} already exists with a different specifier`
1560
+ });
697
1561
  existingManifest.skills[normalized.skillName] = normalized.normalized;
698
1562
  await writeSkillsManifest(cwd, existingManifest);
699
1563
  const existingLock = await readSkillsLock(cwd);
@@ -722,7 +1586,9 @@ async function addCommand(options) {
722
1586
  const skillPath = found?.path ?? `/${skill}`;
723
1587
  const gitSpecifier = buildGitHubSpecifier(owner, repo, skillPath);
724
1588
  const result = await addSingleSkill(cwd, gitSpecifier);
1589
+ spinner.start('Installing skills...');
725
1590
  await installSkills(cwd);
1591
+ spinner.stop('Installed skills');
726
1592
  __rspack_external__clack_prompts_3cae1695.outro(`Added ${picocolors.cyan(result.skillName)}`);
727
1593
  return result;
728
1594
  }
@@ -731,7 +1597,11 @@ async function addCommand(options) {
731
1597
  if (0 === skills.length) {
732
1598
  spinner.stop(picocolors.red('No skills found'));
733
1599
  __rspack_external__clack_prompts_3cae1695.outro(picocolors.red(`No valid skills found in ${source}`));
734
- throw new Error(`No skills found in ${source}`);
1600
+ throw new SkillError({
1601
+ code: codes_ErrorCode.SKILL_NOT_FOUND,
1602
+ skillName: source,
1603
+ message: `No skills found in ${source}`
1604
+ });
735
1605
  }
736
1606
  spinner.stop(`Found ${picocolors.green(String(skills.length))} skill${1 !== skills.length ? 's' : ''}`);
737
1607
  const selected = await promptSkillSelection(skills);
@@ -742,22 +1612,38 @@ async function addCommand(options) {
742
1612
  results.push(result);
743
1613
  __rspack_external__clack_prompts_3cae1695.log.success(`Added ${picocolors.cyan(result.skillName)}`);
744
1614
  }
1615
+ spinner.start('Installing skills...');
745
1616
  await installSkills(cwd);
1617
+ spinner.stop('Installed skills');
746
1618
  __rspack_external__clack_prompts_3cae1695.outro('Done');
747
1619
  return 1 === results.length ? results[0] : results;
748
1620
  }
749
1621
  const result = await addSingleSkill(cwd, specifier);
1622
+ const spinner = __rspack_external__clack_prompts_3cae1695.spinner();
1623
+ spinner.start('Installing skills...');
750
1624
  await installSkills(cwd);
1625
+ spinner.stop('Installed skills');
751
1626
  return result;
752
1627
  }
753
1628
  async function assertManifestMissing(cwd) {
754
1629
  const filePath = node_path.join(cwd, 'skills.json');
755
1630
  try {
756
1631
  await access(filePath);
757
- throw new Error('skills.json already exists');
1632
+ throw new ManifestError({
1633
+ code: codes_ErrorCode.MANIFEST_EXISTS,
1634
+ filePath,
1635
+ message: 'skills.json already exists'
1636
+ });
758
1637
  } catch (error) {
1638
+ if (error instanceof ManifestError) throw error;
759
1639
  if ('ENOENT' === error.code) return;
760
- throw error;
1640
+ throw new FileSystemError({
1641
+ code: codes_ErrorCode.FS_ERROR,
1642
+ operation: 'access',
1643
+ path: filePath,
1644
+ message: `Failed to check if manifest exists: ${error.message}`,
1645
+ cause: error
1646
+ });
761
1647
  }
762
1648
  }
763
1649
  function createDefaultManifest() {
@@ -776,8 +1662,186 @@ async function initCommand(options, promptInit = promptInitManifestOptions) {
776
1662
  await writeSkillsManifest(options.cwd, manifest);
777
1663
  return manifest;
778
1664
  }
1665
+ const phaseLabelMap = {
1666
+ resolving: 'Resolving',
1667
+ fetching: 'Fetching',
1668
+ linking: 'Linking',
1669
+ finalizing: 'Finalizing',
1670
+ done: 'Done'
1671
+ };
1672
+ function clampCount(value, total) {
1673
+ if (value < 0) return 0;
1674
+ if (value > total) return total;
1675
+ return value;
1676
+ }
1677
+ function calculatePercent(snapshot) {
1678
+ if ('done' === snapshot.phase) return 100;
1679
+ if (0 === snapshot.total) return 0;
1680
+ const maxSteps = 3 * snapshot.total;
1681
+ const completed = snapshot.resolved + snapshot.added + snapshot.installed;
1682
+ return Math.floor(completed / maxSteps * 100);
1683
+ }
1684
+ function formatSummary(snapshot) {
1685
+ const total = snapshot.total;
1686
+ return `resolved ${snapshot.resolved}/${total}, added ${snapshot.added}/${total}, installed ${snapshot.installed}/${total}`;
1687
+ }
1688
+ function formatTTYLine(snapshot) {
1689
+ const percent = calculatePercent(snapshot);
1690
+ const progress = Math.round(percent / 100 * 20);
1691
+ const filled = '='.repeat(progress);
1692
+ const empty = '-'.repeat(Math.max(0, 20 - progress));
1693
+ const phase = phaseLabelMap[snapshot.phase];
1694
+ const summary = formatSummary(snapshot);
1695
+ const skill = snapshot.currentSkill ? `, skill: ${snapshot.currentSkill}` : '';
1696
+ return `[${filled}${empty}] ${percent}% ${phase} ${summary}${skill}`;
1697
+ }
1698
+ function createInstallProgressReporter(options = {}) {
1699
+ const write = options.write ?? ((text)=>process.stderr.write(text));
1700
+ const info = options.info ?? ((text)=>console.info(text));
1701
+ const useTTY = options.isTTY ?? true === process.stderr.isTTY;
1702
+ const snapshot = {
1703
+ total: 0,
1704
+ resolved: 0,
1705
+ added: 0,
1706
+ installed: 0,
1707
+ phase: 'resolving'
1708
+ };
1709
+ let renderedTTY = false;
1710
+ let lastLineLength = 0;
1711
+ function renderTTY() {
1712
+ const line = formatTTYLine(snapshot);
1713
+ const clearPadding = lastLineLength > line.length ? ' '.repeat(lastLineLength - line.length) : '';
1714
+ write(`\r${line}${clearPadding}`);
1715
+ lastLineLength = line.length;
1716
+ renderedTTY = true;
1717
+ }
1718
+ function logStage(phase) {
1719
+ info(`spm install: ${phaseLabelMap[phase].toLowerCase()}...`);
1720
+ }
1721
+ function render() {
1722
+ if (useTTY) return void renderTTY();
1723
+ }
1724
+ return {
1725
+ start (total) {
1726
+ snapshot.total = Math.max(0, total);
1727
+ snapshot.resolved = 0;
1728
+ snapshot.added = 0;
1729
+ snapshot.installed = 0;
1730
+ snapshot.phase = 'resolving';
1731
+ snapshot.currentSkill = void 0;
1732
+ if (useTTY) renderTTY();
1733
+ else {
1734
+ const noun = 1 === snapshot.total ? 'skill' : 'skills';
1735
+ info(`spm install: starting (${snapshot.total} ${noun})`);
1736
+ logStage('resolving');
1737
+ }
1738
+ },
1739
+ setPhase (phase) {
1740
+ snapshot.phase = phase;
1741
+ snapshot.currentSkill = void 0;
1742
+ render();
1743
+ if (!useTTY && 'finalizing' !== phase) logStage(phase);
1744
+ },
1745
+ onProgress (event) {
1746
+ snapshot.currentSkill = event.skillName;
1747
+ switch(event.type){
1748
+ case 'resolved':
1749
+ snapshot.resolved = clampCount(snapshot.resolved + 1, snapshot.total);
1750
+ break;
1751
+ case 'added':
1752
+ snapshot.added = clampCount(snapshot.added + 1, snapshot.total);
1753
+ break;
1754
+ case 'installed':
1755
+ snapshot.installed = clampCount(snapshot.installed + 1, snapshot.total);
1756
+ break;
1757
+ default:
1758
+ }
1759
+ render();
1760
+ },
1761
+ complete () {
1762
+ snapshot.phase = 'done';
1763
+ snapshot.currentSkill = void 0;
1764
+ const summary = formatSummary(snapshot);
1765
+ if (useTTY) {
1766
+ renderTTY();
1767
+ write('\n');
1768
+ }
1769
+ info(`spm install: ${summary}`);
1770
+ },
1771
+ fail () {
1772
+ if (useTTY && renderedTTY) write('\n');
1773
+ }
1774
+ };
1775
+ }
779
1776
  async function installCommand(options) {
780
- return installSkills(options.cwd);
1777
+ const manifest = await readSkillsManifest(options.cwd);
1778
+ if (!manifest) throw new ManifestError({
1779
+ code: codes_ErrorCode.MANIFEST_NOT_FOUND,
1780
+ filePath: `${options.cwd}/skills.json`,
1781
+ message: 'No skills.json found in the current directory. Run "spm init" to create one.'
1782
+ });
1783
+ const currentLock = await readSkillsLock(options.cwd);
1784
+ const totalSkills = Object.keys(manifest.skills).length;
1785
+ const reporter = createInstallProgressReporter();
1786
+ const onProgress = (event)=>reporter.onProgress(event);
1787
+ let started = false;
1788
+ try {
1789
+ if (options.frozenLockfile) {
1790
+ if (!currentLock) throw new ManifestError({
1791
+ code: codes_ErrorCode.LOCKFILE_NOT_FOUND,
1792
+ filePath: `${options.cwd}/skills-lock.yaml`,
1793
+ message: 'Lockfile is required in frozen mode but none was found. Run "spm install" first.'
1794
+ });
1795
+ if (!isLockInSync(manifest, currentLock)) throw new ManifestError({
1796
+ code: codes_ErrorCode.LOCKFILE_OUTDATED,
1797
+ filePath: `${options.cwd}/skills-lock.yaml`,
1798
+ message: 'Lockfile is out of sync with manifest. Run install without --frozen-lockfile to update.'
1799
+ });
1800
+ reporter.start(totalSkills);
1801
+ started = true;
1802
+ for (const skillName of Object.keys(currentLock.skills))onProgress({
1803
+ type: 'resolved',
1804
+ skillName
1805
+ });
1806
+ reporter.setPhase('fetching');
1807
+ await fetchSkillsFromLock(options.cwd, manifest, currentLock, {
1808
+ onProgress
1809
+ });
1810
+ reporter.setPhase('linking');
1811
+ await linkSkillsFromLock(options.cwd, manifest, currentLock, {
1812
+ onProgress
1813
+ });
1814
+ reporter.setPhase('finalizing');
1815
+ reporter.complete();
1816
+ return {
1817
+ status: 'installed',
1818
+ installed: Object.keys(currentLock.skills)
1819
+ };
1820
+ }
1821
+ reporter.start(totalSkills);
1822
+ started = true;
1823
+ const lockfile = await syncSkillsLock(options.cwd, manifest, currentLock, {
1824
+ onProgress
1825
+ });
1826
+ reporter.setPhase('fetching');
1827
+ await fetchSkillsFromLock(options.cwd, manifest, lockfile, {
1828
+ onProgress
1829
+ });
1830
+ reporter.setPhase('linking');
1831
+ await linkSkillsFromLock(options.cwd, manifest, lockfile, {
1832
+ onProgress
1833
+ });
1834
+ reporter.setPhase('finalizing');
1835
+ await writeSkillsLock(options.cwd, lockfile);
1836
+ reporter.complete();
1837
+ return {
1838
+ status: 'installed',
1839
+ installed: Object.keys(lockfile.skills)
1840
+ };
1841
+ } catch (error) {
1842
+ if (started) reporter.fail();
1843
+ throw error;
1844
+ }
781
1845
  }
782
1846
  function createEmptyResult() {
783
1847
  return {
@@ -788,7 +1852,7 @@ function createEmptyResult() {
788
1852
  failed: []
789
1853
  };
790
1854
  }
791
- function createBaseLock(cwd, currentLock) {
1855
+ function createBaseLock(_cwd, currentLock) {
792
1856
  if (currentLock) return {
793
1857
  ...currentLock,
794
1858
  skills: {
@@ -804,27 +1868,44 @@ function createBaseLock(cwd, currentLock) {
804
1868
  }
805
1869
  async function updateCommand(options) {
806
1870
  const manifest = await readSkillsManifest(options.cwd);
807
- if (!manifest) return createEmptyResult();
1871
+ if (!manifest) throw new ManifestError({
1872
+ code: codes_ErrorCode.MANIFEST_NOT_FOUND,
1873
+ filePath: `${options.cwd}/skills.json`,
1874
+ message: 'No skills.json found in the current directory. Run "spm init" to create one.'
1875
+ });
808
1876
  const currentLock = await readSkillsLock(options.cwd);
809
1877
  const targetSkills = options.skills ?? Object.keys(manifest.skills);
810
- for (const skillName of targetSkills)if (!(skillName in manifest.skills)) throw new Error(`Unknown skill: ${skillName}`);
1878
+ for (const skillName of targetSkills)if (!(skillName in manifest.skills)) throw new SkillError({
1879
+ code: codes_ErrorCode.SKILL_NOT_FOUND,
1880
+ skillName,
1881
+ message: `Unknown skill: ${skillName}`
1882
+ });
811
1883
  const result = createEmptyResult();
812
1884
  const candidateLock = createBaseLock(options.cwd, currentLock);
813
1885
  candidateLock.installDir = manifest.installDir ?? '.agents/skills';
814
1886
  candidateLock.linkTargets = manifest.linkTargets ?? [];
815
1887
  for (const skillName of targetSkills){
816
1888
  const specifier = manifest.skills[skillName];
817
- if (specifier.startsWith('file:')) {
818
- result.skipped.push({
819
- name: skillName,
820
- reason: 'file-specifier'
821
- });
822
- continue;
823
- }
824
1889
  try {
1890
+ const normalized = normalizeSpecifier(specifier);
1891
+ if ('link' === normalized.type) {
1892
+ result.skipped.push({
1893
+ name: skillName,
1894
+ reason: 'link-specifier'
1895
+ });
1896
+ continue;
1897
+ }
825
1898
  const { entry } = await resolveLockEntry(options.cwd, specifier);
826
1899
  const previous = currentLock?.skills[skillName];
827
- if (previous?.resolution.type === 'git' && 'git' === entry.resolution.type && previous.resolution.commit === entry.resolution.commit) {
1900
+ if (previous?.resolution.type === 'git' && 'git' === entry.resolution.type && previous.specifier === entry.specifier && previous.resolution.url === entry.resolution.url && previous.resolution.commit === entry.resolution.commit && previous.resolution.path === entry.resolution.path) {
1901
+ result.unchanged.push(skillName);
1902
+ continue;
1903
+ }
1904
+ if (previous?.resolution.type === 'npm' && 'npm' === entry.resolution.type && previous.specifier === entry.specifier && previous.resolution.packageName === entry.resolution.packageName && previous.resolution.version === entry.resolution.version && previous.resolution.path === entry.resolution.path && previous.resolution.tarball === entry.resolution.tarball && previous.resolution.integrity === entry.resolution.integrity && previous.resolution.registry === entry.resolution.registry) {
1905
+ result.unchanged.push(skillName);
1906
+ continue;
1907
+ }
1908
+ if (previous?.resolution.type === 'file' && 'file' === entry.resolution.type && previous.specifier === entry.specifier && previous.digest === entry.digest) {
828
1909
  result.unchanged.push(skillName);
829
1910
  continue;
830
1911
  }
@@ -847,9 +1928,6 @@ async function updateCommand(options) {
847
1928
  result.status = result.updated.length > 0 ? 'updated' : 'skipped';
848
1929
  return result;
849
1930
  }
850
- var package_namespaceObject = {
851
- rE: "0.2.0"
852
- };
853
1931
  function createHandlers(overrides) {
854
1932
  return {
855
1933
  addCommand: addCommand,
@@ -870,7 +1948,7 @@ async function runCli(argv, context = {}) {
870
1948
  cli.help();
871
1949
  cli.version(packageVersion);
872
1950
  cli.showVersionOnExit = false;
873
- cli.command('add [...positionals]').option('--skill <name>', 'Select a skill').action((positionals = [], options)=>{
1951
+ cli.command('add [...positionals]').option('--skill <name>', 'Select a skill').action(async (positionals = [], options)=>{
874
1952
  const specifier = positionals[0];
875
1953
  if (!specifier) throw new Error('Missing required specifier');
876
1954
  return handlers.addCommand({
@@ -879,16 +1957,17 @@ async function runCli(argv, context = {}) {
879
1957
  skill: options.skill
880
1958
  });
881
1959
  });
882
- cli.command('install [...args]').action(()=>handlers.installCommand({
883
- cwd
1960
+ cli.command('install [...args]').option('--frozen-lockfile', 'Fail if lockfile is out of sync').action(async (_args, options)=>handlers.installCommand({
1961
+ cwd,
1962
+ frozenLockfile: options.frozenLockfile
884
1963
  }));
885
- cli.command('update [...skills]').action((skills = [])=>handlers.updateCommand({
1964
+ cli.command('update [...skills]').action(async (skills = [])=>handlers.updateCommand({
886
1965
  cwd,
887
1966
  skills: skills.length > 0 ? skills : void 0
888
1967
  }));
889
1968
  cli.command('init [...args]', '', {
890
1969
  allowUnknownOptions: true
891
- }).option('--yes [value]', 'Skip prompts and write defaults').action((args = [], options)=>{
1970
+ }).option('--yes [value]', 'Skip prompts and write defaults').action(async (args = [], options)=>{
892
1971
  if (args.length > 0) throw new Error('init does not accept positional arguments');
893
1972
  for (const key of Object.keys(options))if ('--' !== key) {
894
1973
  if ('yes' !== key) throw new Error(`Unknown flag for init: --${formatFlagName(key)}`);
@@ -907,6 +1986,14 @@ async function runCli(argv, context = {}) {
907
1986
  if (argv.length <= 2) return void cli.outputHelp();
908
1987
  if (globalOptions.help || globalOptions.h) return;
909
1988
  if (!cli.matchedCommand) throw new Error(`Unknown command: ${argv[2]}`);
910
- return cli.runMatchedCommand();
1989
+ try {
1990
+ return await cli.runMatchedCommand();
1991
+ } catch (error) {
1992
+ if (error instanceof SpmError) {
1993
+ const enhancedError = new Error(formatErrorForDisplay(error));
1994
+ throw enhancedError;
1995
+ }
1996
+ throw error;
1997
+ }
911
1998
  }
912
- export { addCommand, cloneAndDiscover, discoverSkillsInDir, fetchSkillsFromLock, initCommand, installCommand, installSkills, installStageHooks, linkSkillsFromLock, listRepoSkills, parseGitHubUrl, parseOwnerRepo, resolveLockEntry, runCli, updateCommand };
1999
+ export { FileSystemError, GitError, ManifestError, NetworkError, ParseError, SkillError, SpmError, addCommand, cloneAndDiscover, codes_ErrorCode as ErrorCode, convertNodeError, createInstallProgressReporter, discoverSkillsInDir, fetchSkillsFromLock, formatErrorForDisplay, getExitCode, initCommand, installCommand, installSkills, installStageHooks, isLockInSync, isSpmError, linkSkillsFromLock, listRepoSkills, normalizeSpecifier, parseGitHubUrl, parseOwnerRepo, parseSpecifier, readSkillsLock, readSkillsManifest, resolveLockEntry, runCli, updateCommand, writeSkillsLock, writeSkillsManifest };