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/README.md +15 -8
- package/bin/skills-package-manager.js +10 -5
- package/bin/spm.js +10 -5
- package/dist/index.js +1389 -302
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -1,46 +1,682 @@
|
|
|
1
|
+
import { cac } from "cac";
|
|
1
2
|
import picocolors from "picocolors";
|
|
2
|
-
import { access, cp
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
137
|
-
|
|
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:
|
|
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
|
-
|
|
885
|
+
tarball: toPortableRelativePath(cwd, tarballPath),
|
|
886
|
+
path: normalized.path
|
|
146
887
|
},
|
|
147
|
-
digest:
|
|
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:
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
290
|
-
recursive: true,
|
|
291
|
-
force: true
|
|
292
|
-
}).catch(()=>{});
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
} catch (error) {
|
|
296
|
-
await
|
|
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
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
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
|
-
|
|
444
|
-
|
|
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
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
const
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
508
|
-
'
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
558
|
-
'
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
568
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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)
|
|
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
|
|
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
|
-
|
|
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 };
|