skillex 0.2.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/CHANGELOG.md +64 -0
- package/LICENSE +21 -0
- package/README.md +631 -0
- package/bin/skillex.js +11 -0
- package/dist/adapters.d.ts +60 -0
- package/dist/adapters.js +213 -0
- package/dist/catalog.d.ts +73 -0
- package/dist/catalog.js +356 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +885 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +25 -0
- package/dist/confirm.d.ts +8 -0
- package/dist/confirm.js +24 -0
- package/dist/fs.d.ts +79 -0
- package/dist/fs.js +184 -0
- package/dist/http.d.ts +43 -0
- package/dist/http.js +123 -0
- package/dist/install.d.ts +115 -0
- package/dist/install.js +895 -0
- package/dist/output.d.ts +46 -0
- package/dist/output.js +78 -0
- package/dist/runner.d.ts +31 -0
- package/dist/runner.js +94 -0
- package/dist/skill.d.ts +23 -0
- package/dist/skill.js +61 -0
- package/dist/sync.d.ts +41 -0
- package/dist/sync.js +384 -0
- package/dist/types.d.ts +442 -0
- package/dist/types.js +83 -0
- package/dist/ui.d.ts +43 -0
- package/dist/ui.js +78 -0
- package/dist/user-config.d.ts +39 -0
- package/dist/user-config.js +54 -0
- package/package.json +93 -0
package/dist/install.js
ADDED
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { DEFAULT_AGENT_SKILLS_DIR, DEFAULT_REF, DEFAULT_REPO, getStatePaths } from "./config.js";
|
|
3
|
+
import { confirmAction } from "./confirm.js";
|
|
4
|
+
import { ensureDir, pathExists, readJson, removePath, writeJson, writeText } from "./fs.js";
|
|
5
|
+
import { fetchOptionalJson, fetchOptionalText, fetchText } from "./http.js";
|
|
6
|
+
import { buildRawGitHubUrl, loadCatalog, resolveSource } from "./catalog.js";
|
|
7
|
+
import { resolveAdapterState } from "./adapters.js";
|
|
8
|
+
import { loadInstalledSkillDocuments, syncAdapterFiles } from "./sync.js";
|
|
9
|
+
import { parseSkillFrontmatter } from "./skill.js";
|
|
10
|
+
import { CliError, InstallError } from "./types.js";
|
|
11
|
+
/**
|
|
12
|
+
* Initializes the local Skillex workspace state.
|
|
13
|
+
*
|
|
14
|
+
* @param options - Project initialization options.
|
|
15
|
+
* @returns Lockfile initialization result.
|
|
16
|
+
* @throws {InstallError} When initialization fails.
|
|
17
|
+
*/
|
|
18
|
+
export async function initProject(options = {}) {
|
|
19
|
+
try {
|
|
20
|
+
const cwd = options.cwd || process.cwd();
|
|
21
|
+
const statePaths = getStatePaths(cwd, options.agentSkillsDir || DEFAULT_AGENT_SKILLS_DIR);
|
|
22
|
+
const now = getNow(options);
|
|
23
|
+
await ensureDir(statePaths.stateDir);
|
|
24
|
+
await ensureDir(statePaths.skillsDirPath);
|
|
25
|
+
await ensureDir(statePaths.generatedDirPath);
|
|
26
|
+
const existing = await readJson(statePaths.lockfilePath, null);
|
|
27
|
+
const source = resolveSource(toCatalogSourceInput(options, resolvePrimarySourceOverride(options, existing)));
|
|
28
|
+
const lockfile = normalizeLockfile(existing, source, now);
|
|
29
|
+
lockfile.adapters = await resolveAdapterState({
|
|
30
|
+
cwd,
|
|
31
|
+
...(options.adapter || lockfile.adapters.active
|
|
32
|
+
? { adapter: options.adapter || lockfile.adapters.active || undefined }
|
|
33
|
+
: {}),
|
|
34
|
+
});
|
|
35
|
+
if (options.repo) {
|
|
36
|
+
lockfile.sources = [toLockfileSource(source)];
|
|
37
|
+
}
|
|
38
|
+
lockfile.settings.autoSync = options.autoSync ?? lockfile.settings.autoSync;
|
|
39
|
+
if (lockfile.settings.autoSync && !lockfile.adapters.active) {
|
|
40
|
+
throw new InstallError("Auto-sync requires an active adapter. Use --adapter <id> or run in a detectable workspace.", "AUTO_SYNC_REQUIRES_ADAPTER");
|
|
41
|
+
}
|
|
42
|
+
lockfile.updatedAt = now();
|
|
43
|
+
await writeJson(statePaths.lockfilePath, lockfile);
|
|
44
|
+
// Create .gitignore for the state directory on first init
|
|
45
|
+
const gitignorePath = path.join(statePaths.stateDir, ".gitignore");
|
|
46
|
+
if (!(await pathExists(gitignorePath))) {
|
|
47
|
+
await writeText(gitignorePath, ".cache/\n*.log\n");
|
|
48
|
+
}
|
|
49
|
+
return { created: !existing, statePaths, lockfile };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw toInstallError(error, "Failed to initialize project");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Installs one or more catalog skills or direct GitHub skills into the local workspace state.
|
|
57
|
+
*
|
|
58
|
+
* @param requestedSkillIds - Requested skill ids or `owner/repo[@ref]` direct references.
|
|
59
|
+
* @param options - Installation options.
|
|
60
|
+
* @returns Install summary.
|
|
61
|
+
* @throws {InstallError} When installation fails.
|
|
62
|
+
*/
|
|
63
|
+
export async function installSkills(requestedSkillIds, options = {}) {
|
|
64
|
+
try {
|
|
65
|
+
const cwd = options.cwd || process.cwd();
|
|
66
|
+
const statePaths = getStatePaths(cwd, options.agentSkillsDir || DEFAULT_AGENT_SKILLS_DIR);
|
|
67
|
+
const now = getNow(options);
|
|
68
|
+
const catalogLoader = options.catalogLoader || loadCatalog;
|
|
69
|
+
const downloader = options.downloader || downloadSkill;
|
|
70
|
+
await ensureDir(statePaths.stateDir);
|
|
71
|
+
await ensureDir(statePaths.skillsDirPath);
|
|
72
|
+
await ensureDir(statePaths.generatedDirPath);
|
|
73
|
+
const existing = await readJson(statePaths.lockfilePath, null);
|
|
74
|
+
const overrideSource = options.repo ? resolveSource(toCatalogSourceInput(options, { repo: options.repo, ref: options.ref })) : null;
|
|
75
|
+
const defaultSource = overrideSource || resolveSource(toCatalogSourceInput(options, resolvePrimarySourceOverride(options, existing)));
|
|
76
|
+
const lockfile = normalizeLockfile(existing, defaultSource, now);
|
|
77
|
+
if (!lockfile.adapters.active) {
|
|
78
|
+
lockfile.adapters = await resolveAdapterState({
|
|
79
|
+
cwd,
|
|
80
|
+
...(options.adapter ? { adapter: options.adapter } : {}),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const directRefs = requestedSkillIds
|
|
84
|
+
.map((skillId) => ({ input: skillId, reference: parseDirectGitHubRef(skillId) }))
|
|
85
|
+
.filter((entry) => entry.reference !== null);
|
|
86
|
+
const catalogIds = requestedSkillIds.filter((skillId) => parseDirectGitHubRef(skillId) === null);
|
|
87
|
+
const installedSkills = [];
|
|
88
|
+
if (options.installAll && directRefs.length > 0) {
|
|
89
|
+
throw new InstallError("Do not mix --all with direct GitHub references.", "INSTALL_ALL_WITH_DIRECT_REF");
|
|
90
|
+
}
|
|
91
|
+
if (options.installAll || catalogIds.length > 0) {
|
|
92
|
+
const selectedSkills = await selectSkillsFromSources(lockfile, catalogIds, options, catalogLoader);
|
|
93
|
+
const totalCount = selectedSkills.length + directRefs.length;
|
|
94
|
+
for (let i = 0; i < selectedSkills.length; i++) {
|
|
95
|
+
const selection = selectedSkills[i];
|
|
96
|
+
const skill = selection.skill;
|
|
97
|
+
options.onProgress?.(i + 1, totalCount, skill.id);
|
|
98
|
+
await downloader(skill, selection.catalog, statePaths.skillsDirPath);
|
|
99
|
+
lockfile.installed[skill.id] = buildInstalledMetadata(skill, {
|
|
100
|
+
cwd,
|
|
101
|
+
statePaths,
|
|
102
|
+
installedAt: now(),
|
|
103
|
+
source: `catalog:${selection.catalog.repo}@${selection.catalog.ref}`,
|
|
104
|
+
});
|
|
105
|
+
installedSkills.push(skill);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (!directRefs.length) {
|
|
109
|
+
throw new InstallError("Provide at least one skill-id, use --all, or pass owner/repo[@ref].", "INSTALL_REQUIRES_SKILL");
|
|
110
|
+
}
|
|
111
|
+
for (const directRef of directRefs) {
|
|
112
|
+
if (!options.trust) {
|
|
113
|
+
await confirmDirectInstall(directRef.input, options);
|
|
114
|
+
}
|
|
115
|
+
const directSkill = await fetchDirectGitHubSkill(directRef.reference);
|
|
116
|
+
await downloadDirectGitHubSkill(directSkill, statePaths.skillsDirPath);
|
|
117
|
+
lockfile.installed[directSkill.manifest.id] = buildInstalledMetadata(directSkill.manifest, {
|
|
118
|
+
cwd,
|
|
119
|
+
statePaths,
|
|
120
|
+
installedAt: now(),
|
|
121
|
+
source: directSkill.source,
|
|
122
|
+
});
|
|
123
|
+
installedSkills.push(directSkill.manifest);
|
|
124
|
+
}
|
|
125
|
+
lockfile.updatedAt = now();
|
|
126
|
+
await writeJson(statePaths.lockfilePath, lockfile);
|
|
127
|
+
const autoSync = await maybeAutoSync(withAgentSkillsDir({
|
|
128
|
+
cwd,
|
|
129
|
+
adapter: lockfile.adapters.active,
|
|
130
|
+
enabled: lockfile.settings.autoSync,
|
|
131
|
+
now,
|
|
132
|
+
changed: installedSkills.length > 0,
|
|
133
|
+
mode: options.mode,
|
|
134
|
+
}, options.agentSkillsDir));
|
|
135
|
+
return {
|
|
136
|
+
installedCount: installedSkills.length,
|
|
137
|
+
installedSkills,
|
|
138
|
+
statePaths,
|
|
139
|
+
autoSync,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
throw toInstallError(error, "Failed to install skills");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Updates installed skills from the configured remote catalog or their direct GitHub sources.
|
|
148
|
+
*
|
|
149
|
+
* @param requestedSkillIds - Optional subset of installed skills to update.
|
|
150
|
+
* @param options - Update options.
|
|
151
|
+
* @returns Update summary.
|
|
152
|
+
* @throws {InstallError} When update fails.
|
|
153
|
+
*/
|
|
154
|
+
export async function updateInstalledSkills(requestedSkillIds, options = {}) {
|
|
155
|
+
try {
|
|
156
|
+
const cwd = options.cwd || process.cwd();
|
|
157
|
+
const statePaths = getStatePaths(cwd, options.agentSkillsDir || DEFAULT_AGENT_SKILLS_DIR);
|
|
158
|
+
const now = getNow(options);
|
|
159
|
+
const catalogLoader = options.catalogLoader || loadCatalog;
|
|
160
|
+
const downloader = options.downloader || downloadSkill;
|
|
161
|
+
const existing = await readJson(statePaths.lockfilePath, null);
|
|
162
|
+
if (!existing) {
|
|
163
|
+
throw new InstallError("No local installation found. Run: skillex init", "LOCKFILE_MISSING");
|
|
164
|
+
}
|
|
165
|
+
const defaultSource = resolveSource(toCatalogSourceInput(options, resolvePrimarySourceOverride(options, existing)));
|
|
166
|
+
const lockfile = normalizeLockfile(existing, defaultSource, now);
|
|
167
|
+
const skillIds = resolveInstalledSkillIds(lockfile, requestedSkillIds);
|
|
168
|
+
const directIds = skillIds.filter((skillId) => {
|
|
169
|
+
const metadata = lockfile.installed[skillId];
|
|
170
|
+
return Boolean(metadata?.source?.startsWith("github:"));
|
|
171
|
+
});
|
|
172
|
+
const catalogIds = skillIds.filter((skillId) => !directIds.includes(skillId));
|
|
173
|
+
const updatedSkills = [];
|
|
174
|
+
const missingFromCatalog = [];
|
|
175
|
+
if (catalogIds.length > 0) {
|
|
176
|
+
for (const skillId of catalogIds) {
|
|
177
|
+
const metadata = lockfile.installed[skillId];
|
|
178
|
+
const catalogSelection = await resolveInstalledCatalogSelection(skillId, metadata?.source, options, lockfile, catalogLoader);
|
|
179
|
+
if (!catalogSelection) {
|
|
180
|
+
missingFromCatalog.push(skillId);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const skill = catalogSelection.skill;
|
|
184
|
+
await downloader(skill, catalogSelection.catalog, statePaths.skillsDirPath);
|
|
185
|
+
lockfile.installed[skill.id] = buildInstalledMetadata(skill, {
|
|
186
|
+
cwd,
|
|
187
|
+
statePaths,
|
|
188
|
+
installedAt: now(),
|
|
189
|
+
source: `catalog:${catalogSelection.catalog.repo}@${catalogSelection.catalog.ref}`,
|
|
190
|
+
});
|
|
191
|
+
updatedSkills.push(skill);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const skillId of directIds) {
|
|
195
|
+
const metadata = lockfile.installed[skillId];
|
|
196
|
+
const directRef = metadata?.source ? parseGitHubSource(metadata.source) : null;
|
|
197
|
+
if (!directRef) {
|
|
198
|
+
throw new InstallError(`Invalid direct source for "${skillId}".`, "DIRECT_SOURCE_INVALID");
|
|
199
|
+
}
|
|
200
|
+
const directSkill = await fetchDirectGitHubSkill(directRef);
|
|
201
|
+
await downloadDirectGitHubSkill(directSkill, statePaths.skillsDirPath);
|
|
202
|
+
lockfile.installed[directSkill.manifest.id] = buildInstalledMetadata(directSkill.manifest, {
|
|
203
|
+
cwd,
|
|
204
|
+
statePaths,
|
|
205
|
+
installedAt: now(),
|
|
206
|
+
source: directSkill.source,
|
|
207
|
+
});
|
|
208
|
+
updatedSkills.push(directSkill.manifest);
|
|
209
|
+
}
|
|
210
|
+
lockfile.updatedAt = now();
|
|
211
|
+
await writeJson(statePaths.lockfilePath, lockfile);
|
|
212
|
+
const autoSync = await maybeAutoSync(withAgentSkillsDir({
|
|
213
|
+
cwd,
|
|
214
|
+
adapter: lockfile.adapters.active,
|
|
215
|
+
enabled: lockfile.settings.autoSync,
|
|
216
|
+
now,
|
|
217
|
+
changed: updatedSkills.length > 0,
|
|
218
|
+
mode: options.mode,
|
|
219
|
+
}, options.agentSkillsDir));
|
|
220
|
+
return {
|
|
221
|
+
statePaths,
|
|
222
|
+
updatedSkills,
|
|
223
|
+
missingFromCatalog,
|
|
224
|
+
autoSync,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
throw toInstallError(error, "Failed to update skills");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Removes installed skills from the local workspace state.
|
|
233
|
+
*
|
|
234
|
+
* @param requestedSkillIds - Skill ids to remove.
|
|
235
|
+
* @param options - Remove options.
|
|
236
|
+
* @returns Remove summary.
|
|
237
|
+
* @throws {InstallError} When removal fails.
|
|
238
|
+
*/
|
|
239
|
+
export async function removeSkills(requestedSkillIds, options = {}) {
|
|
240
|
+
try {
|
|
241
|
+
const cwd = options.cwd || process.cwd();
|
|
242
|
+
const statePaths = getStatePaths(cwd, options.agentSkillsDir || DEFAULT_AGENT_SKILLS_DIR);
|
|
243
|
+
const now = getNow(options);
|
|
244
|
+
const existing = await readJson(statePaths.lockfilePath, null);
|
|
245
|
+
if (!existing) {
|
|
246
|
+
throw new InstallError("No local installation found. Run: skillex init", "LOCKFILE_MISSING");
|
|
247
|
+
}
|
|
248
|
+
if (!requestedSkillIds.length) {
|
|
249
|
+
throw new InstallError("Provide at least one skill-id to remove.", "REMOVE_REQUIRES_SKILL");
|
|
250
|
+
}
|
|
251
|
+
const defaultSource = resolveSource(toCatalogSourceInput(options, resolvePrimarySourceOverride(options, existing)));
|
|
252
|
+
const lockfile = normalizeLockfile(existing, defaultSource, now);
|
|
253
|
+
const removedSkills = [];
|
|
254
|
+
const missingSkills = [];
|
|
255
|
+
for (const skillId of requestedSkillIds) {
|
|
256
|
+
const metadata = lockfile.installed[skillId];
|
|
257
|
+
if (!metadata) {
|
|
258
|
+
missingSkills.push(skillId);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
await removePath(path.resolve(cwd, metadata.path));
|
|
262
|
+
delete lockfile.installed[skillId];
|
|
263
|
+
removedSkills.push(skillId);
|
|
264
|
+
}
|
|
265
|
+
lockfile.updatedAt = now();
|
|
266
|
+
await writeJson(statePaths.lockfilePath, lockfile);
|
|
267
|
+
const autoSync = await maybeAutoSync(withAgentSkillsDir({
|
|
268
|
+
cwd,
|
|
269
|
+
adapter: lockfile.adapters.active,
|
|
270
|
+
enabled: lockfile.settings.autoSync,
|
|
271
|
+
now,
|
|
272
|
+
changed: removedSkills.length > 0,
|
|
273
|
+
mode: options.mode,
|
|
274
|
+
}, options.agentSkillsDir));
|
|
275
|
+
return {
|
|
276
|
+
statePaths,
|
|
277
|
+
removedSkills,
|
|
278
|
+
missingSkills,
|
|
279
|
+
autoSync,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
throw toInstallError(error, "Failed to remove skills");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Synchronizes installed skills to the active or requested adapter target.
|
|
288
|
+
*
|
|
289
|
+
* @param options - Sync options.
|
|
290
|
+
* @returns Sync command result.
|
|
291
|
+
* @throws {InstallError} When workspace state is missing or invalid.
|
|
292
|
+
*/
|
|
293
|
+
export async function syncInstalledSkills(options = {}) {
|
|
294
|
+
try {
|
|
295
|
+
const cwd = options.cwd || process.cwd();
|
|
296
|
+
const statePaths = getStatePaths(cwd, options.agentSkillsDir || DEFAULT_AGENT_SKILLS_DIR);
|
|
297
|
+
const now = getNow(options);
|
|
298
|
+
const existing = await readJson(statePaths.lockfilePath, null);
|
|
299
|
+
if (!existing) {
|
|
300
|
+
throw new InstallError("No local installation found. Run: skillex init", "LOCKFILE_MISSING");
|
|
301
|
+
}
|
|
302
|
+
const defaultSource = resolveSource(toCatalogSourceInput(options, resolvePrimarySourceOverride(options, existing)));
|
|
303
|
+
const lockfile = normalizeLockfile(existing, defaultSource, now);
|
|
304
|
+
const adapterId = options.adapter || lockfile.adapters.active;
|
|
305
|
+
if (!adapterId) {
|
|
306
|
+
throw new InstallError("No active adapter configured. Run: skillex init --adapter <id> or use --adapter.", "ACTIVE_ADAPTER_MISSING");
|
|
307
|
+
}
|
|
308
|
+
const skills = await loadInstalledSkillDocuments({
|
|
309
|
+
cwd,
|
|
310
|
+
lockfile,
|
|
311
|
+
});
|
|
312
|
+
const syncResult = await syncAdapterFiles({
|
|
313
|
+
cwd,
|
|
314
|
+
adapterId,
|
|
315
|
+
statePaths,
|
|
316
|
+
skills,
|
|
317
|
+
...(options.mode ? { mode: options.mode } : {}),
|
|
318
|
+
...(options.dryRun !== undefined ? { dryRun: options.dryRun } : {}),
|
|
319
|
+
});
|
|
320
|
+
if (options.dryRun) {
|
|
321
|
+
return {
|
|
322
|
+
statePaths,
|
|
323
|
+
sync: {
|
|
324
|
+
adapter: syncResult.adapter,
|
|
325
|
+
targetPath: syncResult.targetPath,
|
|
326
|
+
},
|
|
327
|
+
skillCount: skills.length,
|
|
328
|
+
changed: syncResult.changed,
|
|
329
|
+
diff: syncResult.diff,
|
|
330
|
+
dryRun: true,
|
|
331
|
+
syncMode: syncResult.syncMode,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
lockfile.sync = {
|
|
335
|
+
adapter: syncResult.adapter,
|
|
336
|
+
targetPath: syncResult.targetPath,
|
|
337
|
+
syncedAt: now(),
|
|
338
|
+
};
|
|
339
|
+
lockfile.syncMode = syncResult.syncMode;
|
|
340
|
+
lockfile.updatedAt = now();
|
|
341
|
+
await writeJson(statePaths.lockfilePath, lockfile);
|
|
342
|
+
return {
|
|
343
|
+
statePaths,
|
|
344
|
+
sync: lockfile.sync,
|
|
345
|
+
skillCount: skills.length,
|
|
346
|
+
changed: syncResult.changed,
|
|
347
|
+
diff: syncResult.diff,
|
|
348
|
+
dryRun: false,
|
|
349
|
+
syncMode: syncResult.syncMode,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
throw toInstallError(error, "Failed to synchronize skills");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Reads the local workspace lockfile when it exists.
|
|
358
|
+
*
|
|
359
|
+
* @param options - Project lookup options.
|
|
360
|
+
* @returns Normalized lockfile state or `null` when no install exists.
|
|
361
|
+
*/
|
|
362
|
+
export async function getInstalledSkills(options = {}) {
|
|
363
|
+
const cwd = options.cwd || process.cwd();
|
|
364
|
+
const statePaths = getStatePaths(cwd, options.agentSkillsDir || DEFAULT_AGENT_SKILLS_DIR);
|
|
365
|
+
if (!(await pathExists(statePaths.lockfilePath))) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const source = resolveSource(toCatalogSourceInput(options, { repo: options.repo, ref: options.ref }));
|
|
369
|
+
const existing = await readJson(statePaths.lockfilePath, null);
|
|
370
|
+
if (!existing) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
return normalizeLockfile(existing, source, getNow(options));
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Resolves the effective project catalog source using CLI overrides or local state.
|
|
377
|
+
*
|
|
378
|
+
* @param options - Project lookup options.
|
|
379
|
+
* @returns Resolved catalog source.
|
|
380
|
+
*/
|
|
381
|
+
export async function resolveProjectSource(options = {}) {
|
|
382
|
+
const cwd = options.cwd || process.cwd();
|
|
383
|
+
const statePaths = getStatePaths(cwd, options.agentSkillsDir || DEFAULT_AGENT_SKILLS_DIR);
|
|
384
|
+
const existing = await readJson(statePaths.lockfilePath, null);
|
|
385
|
+
return resolveSource(toCatalogSourceInput(options, resolvePrimarySourceOverride(options, existing)));
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Resolves all effective project sources using CLI overrides or local state.
|
|
389
|
+
*
|
|
390
|
+
* @param options - Project lookup options.
|
|
391
|
+
* @returns Resolved source list.
|
|
392
|
+
*/
|
|
393
|
+
export async function resolveProjectSources(options = {}) {
|
|
394
|
+
const cwd = options.cwd || process.cwd();
|
|
395
|
+
const statePaths = getStatePaths(cwd, options.agentSkillsDir || DEFAULT_AGENT_SKILLS_DIR);
|
|
396
|
+
const existing = await readJson(statePaths.lockfilePath, null);
|
|
397
|
+
if (options.repo) {
|
|
398
|
+
return [toLockfileSource(resolveSource(toCatalogSourceInput(options, { repo: options.repo, ref: options.ref })))];
|
|
399
|
+
}
|
|
400
|
+
return getLockfileSources(existing, resolveSource(toCatalogSourceInput(options)));
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Aggregates skills from all effective project sources.
|
|
404
|
+
*
|
|
405
|
+
* @param options - Project lookup options.
|
|
406
|
+
* @param catalogLoader - Optional catalog loader override.
|
|
407
|
+
* @returns Aggregated skills grouped by source metadata.
|
|
408
|
+
*/
|
|
409
|
+
export async function loadProjectCatalogs(options = {}, catalogLoader = loadCatalog) {
|
|
410
|
+
const sources = await resolveProjectSources(options);
|
|
411
|
+
const loaded = await Promise.all(sources.map(async (source) => {
|
|
412
|
+
const catalog = await catalogLoader(resolveSource(toCatalogSourceInput(options, source)));
|
|
413
|
+
const skills = catalog.skills.map((skill) => ({
|
|
414
|
+
...skill,
|
|
415
|
+
source: {
|
|
416
|
+
repo: source.repo,
|
|
417
|
+
ref: source.ref,
|
|
418
|
+
...(source.label ? { label: source.label } : {}),
|
|
419
|
+
},
|
|
420
|
+
}));
|
|
421
|
+
return { source, catalog, skills };
|
|
422
|
+
}));
|
|
423
|
+
return {
|
|
424
|
+
formatVersion: 1,
|
|
425
|
+
skills: loaded.flatMap((entry) => entry.skills),
|
|
426
|
+
sources: loaded.map((entry) => ({ ...entry.source, skillCount: entry.catalog.skills.length })),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Adds a source to the workspace lockfile.
|
|
431
|
+
*
|
|
432
|
+
* @param sourceInput - Repository to add.
|
|
433
|
+
* @param options - Project options.
|
|
434
|
+
* @returns Updated normalized lockfile.
|
|
435
|
+
*/
|
|
436
|
+
export async function addProjectSource(sourceInput, options = {}) {
|
|
437
|
+
const cwd = options.cwd || process.cwd();
|
|
438
|
+
const statePaths = getStatePaths(cwd, options.agentSkillsDir || DEFAULT_AGENT_SKILLS_DIR);
|
|
439
|
+
const now = getNow(options);
|
|
440
|
+
await ensureDir(statePaths.stateDir);
|
|
441
|
+
await ensureDir(statePaths.skillsDirPath);
|
|
442
|
+
await ensureDir(statePaths.generatedDirPath);
|
|
443
|
+
const existing = await readJson(statePaths.lockfilePath, null);
|
|
444
|
+
const fallbackSource = resolveSource(toCatalogSourceInput(options, resolvePrimarySourceOverride(options, existing)));
|
|
445
|
+
const lockfile = normalizeLockfile(existing, fallbackSource, now);
|
|
446
|
+
const source = toLockfileSource(resolveSource(toCatalogSourceInput(options, sourceInput)), sourceInput.label);
|
|
447
|
+
if (lockfile.sources.some((entry) => entry.repo === source.repo && entry.ref === source.ref)) {
|
|
448
|
+
throw new InstallError(`Source \"${source.repo}@${source.ref}\" is already configured.`, "SOURCE_ALREADY_EXISTS");
|
|
449
|
+
}
|
|
450
|
+
lockfile.sources.push(source);
|
|
451
|
+
lockfile.updatedAt = now();
|
|
452
|
+
await writeJson(statePaths.lockfilePath, lockfile);
|
|
453
|
+
return lockfile;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Removes a source from the workspace lockfile.
|
|
457
|
+
*
|
|
458
|
+
* @param repo - Repository to remove.
|
|
459
|
+
* @param options - Project options.
|
|
460
|
+
* @returns Updated normalized lockfile.
|
|
461
|
+
*/
|
|
462
|
+
export async function removeProjectSource(repo, options = {}) {
|
|
463
|
+
const cwd = options.cwd || process.cwd();
|
|
464
|
+
const statePaths = getStatePaths(cwd, options.agentSkillsDir || DEFAULT_AGENT_SKILLS_DIR);
|
|
465
|
+
const now = getNow(options);
|
|
466
|
+
const existing = await readJson(statePaths.lockfilePath, null);
|
|
467
|
+
if (!existing) {
|
|
468
|
+
throw new InstallError("No local installation found. Run: skillex init", "LOCKFILE_MISSING");
|
|
469
|
+
}
|
|
470
|
+
const fallbackSource = resolveSource(toCatalogSourceInput(options, resolvePrimarySourceOverride(options, existing)));
|
|
471
|
+
const lockfile = normalizeLockfile(existing, fallbackSource, now);
|
|
472
|
+
const remaining = lockfile.sources.filter((entry) => entry.repo !== repo);
|
|
473
|
+
if (remaining.length === lockfile.sources.length) {
|
|
474
|
+
throw new InstallError(`Source \"${repo}\" is not configured.`, "SOURCE_NOT_FOUND");
|
|
475
|
+
}
|
|
476
|
+
if (remaining.length === 0) {
|
|
477
|
+
throw new InstallError("At least one source must remain configured.", "SOURCE_REMOVE_LAST");
|
|
478
|
+
}
|
|
479
|
+
lockfile.sources = remaining;
|
|
480
|
+
lockfile.updatedAt = now();
|
|
481
|
+
await writeJson(statePaths.lockfilePath, lockfile);
|
|
482
|
+
return lockfile;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Lists configured workspace sources.
|
|
486
|
+
*
|
|
487
|
+
* @param options - Project options.
|
|
488
|
+
* @returns Normalized source list.
|
|
489
|
+
*/
|
|
490
|
+
export async function listProjectSources(options = {}) {
|
|
491
|
+
const cwd = options.cwd || process.cwd();
|
|
492
|
+
const statePaths = getStatePaths(cwd, options.agentSkillsDir || DEFAULT_AGENT_SKILLS_DIR);
|
|
493
|
+
const existing = await readJson(statePaths.lockfilePath, null);
|
|
494
|
+
const fallbackSource = resolveSource(toCatalogSourceInput(options, resolvePrimarySourceOverride(options, existing)));
|
|
495
|
+
return getLockfileSources(existing, fallbackSource);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Parses a direct GitHub install reference in `owner/repo[@ref]` format.
|
|
499
|
+
*
|
|
500
|
+
* @param input - User-supplied install argument.
|
|
501
|
+
* @returns Parsed direct GitHub reference or `null` when the value is not a direct ref.
|
|
502
|
+
*/
|
|
503
|
+
export function parseDirectGitHubRef(input) {
|
|
504
|
+
if (!input || input.startsWith("http://") || input.startsWith("https://")) {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
const match = input.trim().match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:@(.+))?$/);
|
|
508
|
+
if (!match) {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
owner: match[1],
|
|
513
|
+
repo: match[2],
|
|
514
|
+
ref: match[3] || "main",
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function createBaseLockfile(source, now) {
|
|
518
|
+
return {
|
|
519
|
+
formatVersion: 1,
|
|
520
|
+
createdAt: now(),
|
|
521
|
+
updatedAt: now(),
|
|
522
|
+
sources: [toLockfileSource(source)],
|
|
523
|
+
adapters: {
|
|
524
|
+
active: null,
|
|
525
|
+
detected: [],
|
|
526
|
+
},
|
|
527
|
+
settings: {
|
|
528
|
+
autoSync: false,
|
|
529
|
+
},
|
|
530
|
+
sync: null,
|
|
531
|
+
syncMode: null,
|
|
532
|
+
installed: {},
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
async function downloadSkill(skill, catalog, skillsDirPath) {
|
|
536
|
+
const skillTargetDir = path.join(skillsDirPath, skill.id);
|
|
537
|
+
await removePath(skillTargetDir);
|
|
538
|
+
await ensureDir(skillTargetDir);
|
|
539
|
+
for (const relativePath of skill.files) {
|
|
540
|
+
const remotePath = skill.path ? path.posix.join(skill.path, relativePath) : relativePath;
|
|
541
|
+
const rawUrl = buildRawGitHubUrl(catalog.repo, catalog.ref, remotePath);
|
|
542
|
+
const content = await fetchText(rawUrl, { headers: { Accept: "text/plain" } });
|
|
543
|
+
const localPath = path.join(skillTargetDir, relativePath);
|
|
544
|
+
await writeText(localPath, content);
|
|
545
|
+
}
|
|
546
|
+
await writeDownloadedManifest(skillTargetDir, {
|
|
547
|
+
...skill,
|
|
548
|
+
source: {
|
|
549
|
+
repo: catalog.repo,
|
|
550
|
+
ref: catalog.ref,
|
|
551
|
+
path: skill.path,
|
|
552
|
+
},
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
async function downloadDirectGitHubSkill(skill, skillsDirPath) {
|
|
556
|
+
const skillTargetDir = path.join(skillsDirPath, skill.manifest.id);
|
|
557
|
+
await removePath(skillTargetDir);
|
|
558
|
+
await ensureDir(skillTargetDir);
|
|
559
|
+
for (const relativePath of skill.manifest.files) {
|
|
560
|
+
const remotePath = skill.manifest.path ? path.posix.join(skill.manifest.path, relativePath) : relativePath;
|
|
561
|
+
const rawUrl = buildRawGitHubUrl(skill.repo, skill.ref, remotePath);
|
|
562
|
+
const content = await fetchText(rawUrl, { headers: { Accept: "text/plain" } });
|
|
563
|
+
await writeText(path.join(skillTargetDir, relativePath), content);
|
|
564
|
+
}
|
|
565
|
+
await writeDownloadedManifest(skillTargetDir, {
|
|
566
|
+
...skill.manifest,
|
|
567
|
+
source: {
|
|
568
|
+
repo: skill.repo,
|
|
569
|
+
ref: skill.ref,
|
|
570
|
+
path: skill.manifest.path,
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
async function writeDownloadedManifest(skillTargetDir, manifest) {
|
|
575
|
+
await writeJson(path.join(skillTargetDir, "skill.json"), manifest);
|
|
576
|
+
}
|
|
577
|
+
async function fetchDirectGitHubSkill(reference) {
|
|
578
|
+
const repoId = `${reference.owner}/${reference.repo}`;
|
|
579
|
+
const manifestUrl = buildRawGitHubUrl(repoId, reference.ref, "skill.json");
|
|
580
|
+
const manifest = await fetchOptionalJson(manifestUrl, {
|
|
581
|
+
headers: { Accept: "application/json" },
|
|
582
|
+
});
|
|
583
|
+
if (manifest) {
|
|
584
|
+
return {
|
|
585
|
+
repo: repoId,
|
|
586
|
+
ref: reference.ref,
|
|
587
|
+
source: `github:${repoId}@${reference.ref}`,
|
|
588
|
+
manifest: normalizeDirectManifest(manifest, reference),
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
const skillMarkdown = await fetchOptionalText(buildRawGitHubUrl(repoId, reference.ref, "SKILL.md"), {
|
|
592
|
+
headers: { Accept: "text/plain" },
|
|
593
|
+
});
|
|
594
|
+
if (!skillMarkdown) {
|
|
595
|
+
throw new InstallError(`No skill.json or SKILL.md found at ${repoId}@${reference.ref}.`, "DIRECT_SKILL_NOT_FOUND");
|
|
596
|
+
}
|
|
597
|
+
const frontmatter = parseSkillFrontmatter(skillMarkdown);
|
|
598
|
+
return {
|
|
599
|
+
repo: repoId,
|
|
600
|
+
ref: reference.ref,
|
|
601
|
+
source: `github:${repoId}@${reference.ref}`,
|
|
602
|
+
manifest: {
|
|
603
|
+
id: normalizeRepoSkillId(reference.repo),
|
|
604
|
+
name: frontmatter.name || toTitleCase(reference.repo),
|
|
605
|
+
version: "0.1.0",
|
|
606
|
+
description: frontmatter.description || `Skill instalada diretamente de ${repoId}.`,
|
|
607
|
+
author: reference.owner,
|
|
608
|
+
tags: [],
|
|
609
|
+
compatibility: [],
|
|
610
|
+
entry: "SKILL.md",
|
|
611
|
+
path: "",
|
|
612
|
+
files: ["SKILL.md"],
|
|
613
|
+
},
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function normalizeDirectManifest(manifest, reference) {
|
|
617
|
+
return {
|
|
618
|
+
id: manifest.id || normalizeRepoSkillId(reference.repo),
|
|
619
|
+
name: manifest.name || toTitleCase(reference.repo),
|
|
620
|
+
version: manifest.version || "0.1.0",
|
|
621
|
+
description: manifest.description || `Skill instalada diretamente de ${reference.owner}/${reference.repo}.`,
|
|
622
|
+
author: manifest.author || reference.owner,
|
|
623
|
+
tags: Array.isArray(manifest.tags) ? manifest.tags : [],
|
|
624
|
+
compatibility: Array.isArray(manifest.compatibility) ? manifest.compatibility : [],
|
|
625
|
+
entry: manifest.entry || "SKILL.md",
|
|
626
|
+
path: manifest.path || "",
|
|
627
|
+
files: Array.isArray(manifest.files) && manifest.files.length > 0 ? manifest.files : [manifest.entry || "SKILL.md"],
|
|
628
|
+
...(manifest.scripts ? { scripts: manifest.scripts } : {}),
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function normalizeLockfile(existing, source, now) {
|
|
632
|
+
if (!existing) {
|
|
633
|
+
return createBaseLockfile(source, now);
|
|
634
|
+
}
|
|
635
|
+
const detectedAdapters = Array.isArray(existing.adapters)
|
|
636
|
+
? existing.adapters
|
|
637
|
+
: Array.isArray(existing.adapters?.detected)
|
|
638
|
+
? existing.adapters.detected
|
|
639
|
+
: [];
|
|
640
|
+
const activeAdapter = Array.isArray(existing.adapters)
|
|
641
|
+
? existing.adapters[0] || null
|
|
642
|
+
: existing.adapters?.active || detectedAdapters[0] || null;
|
|
643
|
+
return {
|
|
644
|
+
formatVersion: Number(existing.formatVersion || 1),
|
|
645
|
+
createdAt: existing.createdAt || now(),
|
|
646
|
+
updatedAt: existing.updatedAt || now(),
|
|
647
|
+
sources: getLockfileSources(existing, source),
|
|
648
|
+
adapters: {
|
|
649
|
+
active: activeAdapter,
|
|
650
|
+
detected: [...new Set(detectedAdapters.filter(Boolean))],
|
|
651
|
+
},
|
|
652
|
+
settings: {
|
|
653
|
+
autoSync: Boolean(existing.settings?.autoSync),
|
|
654
|
+
},
|
|
655
|
+
sync: existing.sync || null,
|
|
656
|
+
syncMode: existing.syncMode || null,
|
|
657
|
+
installed: existing.installed || {},
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
function getLockfileSources(existing, fallbackSource) {
|
|
661
|
+
const legacyCatalog = getLegacyCatalog(existing);
|
|
662
|
+
const configuredSources = Array.isArray(existing?.sources)
|
|
663
|
+
? existing.sources
|
|
664
|
+
.filter((entry) => Boolean(entry?.repo))
|
|
665
|
+
.map((entry) => ({
|
|
666
|
+
repo: entry.repo,
|
|
667
|
+
ref: entry.ref || DEFAULT_REF,
|
|
668
|
+
...(entry.label ? { label: entry.label } : {}),
|
|
669
|
+
}))
|
|
670
|
+
: [];
|
|
671
|
+
if (configuredSources.length > 0) {
|
|
672
|
+
return dedupeSources(configuredSources);
|
|
673
|
+
}
|
|
674
|
+
if (legacyCatalog?.repo) {
|
|
675
|
+
return dedupeSources([
|
|
676
|
+
{
|
|
677
|
+
repo: legacyCatalog.repo,
|
|
678
|
+
ref: legacyCatalog.ref || DEFAULT_REF,
|
|
679
|
+
},
|
|
680
|
+
]);
|
|
681
|
+
}
|
|
682
|
+
return [toLockfileSource(fallbackSource)];
|
|
683
|
+
}
|
|
684
|
+
function getLegacyCatalog(existing) {
|
|
685
|
+
if (!existing || !("catalog" in existing)) {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
const legacyState = existing;
|
|
689
|
+
return legacyState.catalog || null;
|
|
690
|
+
}
|
|
691
|
+
function dedupeSources(sources) {
|
|
692
|
+
const unique = new Map();
|
|
693
|
+
for (const source of sources) {
|
|
694
|
+
const key = `${source.repo}@${source.ref}`;
|
|
695
|
+
if (!unique.has(key)) {
|
|
696
|
+
unique.set(key, source);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return [...unique.values()];
|
|
700
|
+
}
|
|
701
|
+
function toLockfileSource(source, label) {
|
|
702
|
+
return {
|
|
703
|
+
repo: source.repo,
|
|
704
|
+
ref: source.ref,
|
|
705
|
+
...((label || source.repo === DEFAULT_REPO) && (label || source.repo === DEFAULT_REPO)
|
|
706
|
+
? { label: label || "official" }
|
|
707
|
+
: {}),
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
function resolvePrimarySourceOverride(options, existing) {
|
|
711
|
+
const sources = getLockfileSources(existing, resolveSource(toCatalogSourceInput(options)));
|
|
712
|
+
const repo = options.repo || sources[0]?.repo;
|
|
713
|
+
const ref = options.ref || sources[0]?.ref;
|
|
714
|
+
return {
|
|
715
|
+
...(repo ? { repo } : {}),
|
|
716
|
+
...(ref ? { ref } : {}),
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
async function selectSkillsFromSources(lockfile, requestedSkillIds, options, catalogLoader) {
|
|
720
|
+
const sources = options.repo
|
|
721
|
+
? [toLockfileSource(resolveSource(toCatalogSourceInput(options, { repo: options.repo, ref: options.ref })))]
|
|
722
|
+
: lockfile.sources;
|
|
723
|
+
const catalogs = await Promise.all(sources.map(async (source) => ({
|
|
724
|
+
source,
|
|
725
|
+
catalog: await catalogLoader(resolveSource(toCatalogSourceInput(options, source))),
|
|
726
|
+
})));
|
|
727
|
+
if (options.installAll) {
|
|
728
|
+
const selections = catalogs.flatMap(({ source, catalog }) => catalog.skills.map((skill) => ({ skill, catalog, source })));
|
|
729
|
+
const seen = new Map();
|
|
730
|
+
for (const entry of selections) {
|
|
731
|
+
const currentSource = `${entry.source.repo}@${entry.source.ref}`;
|
|
732
|
+
const previousSource = seen.get(entry.skill.id);
|
|
733
|
+
if (previousSource) {
|
|
734
|
+
throw new InstallError(`Skill "${entry.skill.id}" exists in multiple sources: ${previousSource}, ${currentSource}. Use --repo to choose one source at a time.`, "SKILL_AMBIGUOUS_SOURCE");
|
|
735
|
+
}
|
|
736
|
+
seen.set(entry.skill.id, currentSource);
|
|
737
|
+
}
|
|
738
|
+
return selections;
|
|
739
|
+
}
|
|
740
|
+
if (!requestedSkillIds.length) {
|
|
741
|
+
return [];
|
|
742
|
+
}
|
|
743
|
+
return requestedSkillIds.map((skillId) => {
|
|
744
|
+
const matches = catalogs.flatMap(({ source, catalog }) => {
|
|
745
|
+
const skill = catalog.skills.find((entry) => entry.id === skillId);
|
|
746
|
+
return skill ? [{ skill, catalog, source }] : [];
|
|
747
|
+
});
|
|
748
|
+
if (matches.length === 0) {
|
|
749
|
+
throw new InstallError(`Skill "${skillId}" not found in the configured sources.`, "SKILL_NOT_FOUND");
|
|
750
|
+
}
|
|
751
|
+
if (matches.length > 1) {
|
|
752
|
+
const sourceList = matches.map((entry) => `${entry.source.repo}@${entry.source.ref}`).join(", ");
|
|
753
|
+
throw new InstallError(`Skill "${skillId}" exists in multiple sources: ${sourceList}. Use --repo to choose one.`, "SKILL_AMBIGUOUS_SOURCE");
|
|
754
|
+
}
|
|
755
|
+
return matches[0];
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
async function resolveInstalledCatalogSelection(skillId, sourceRef, options, lockfile, catalogLoader) {
|
|
759
|
+
const explicitSource = sourceRef?.startsWith("catalog:") ? parseCatalogSource(sourceRef) : null;
|
|
760
|
+
const sources = explicitSource ? [explicitSource] : lockfile.sources;
|
|
761
|
+
for (const source of sources) {
|
|
762
|
+
const catalog = await catalogLoader(resolveSource(toCatalogSourceInput(options, source)));
|
|
763
|
+
const skill = catalog.skills.find((entry) => entry.id === skillId);
|
|
764
|
+
if (skill) {
|
|
765
|
+
return { skill, catalog, source };
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
function parseCatalogSource(source) {
|
|
771
|
+
const match = source.match(/^catalog:([^@]+\/[^@]+)@(.+)$/);
|
|
772
|
+
if (!match) {
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
repo: match[1],
|
|
777
|
+
ref: match[2],
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
function buildInstalledMetadata(skill, context) {
|
|
781
|
+
return {
|
|
782
|
+
name: skill.name,
|
|
783
|
+
version: skill.version,
|
|
784
|
+
path: toPosix(path.relative(context.cwd, path.join(context.statePaths.skillsDirPath, skill.id))),
|
|
785
|
+
installedAt: context.installedAt,
|
|
786
|
+
compatibility: skill.compatibility,
|
|
787
|
+
tags: skill.tags,
|
|
788
|
+
source: context.source,
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
function resolveInstalledSkillIds(lockfile, requestedSkillIds) {
|
|
792
|
+
const installedIds = Object.keys(lockfile.installed || {});
|
|
793
|
+
if (installedIds.length === 0) {
|
|
794
|
+
throw new InstallError("No skills installed to update.", "NO_SKILLS_INSTALLED");
|
|
795
|
+
}
|
|
796
|
+
if (!requestedSkillIds.length) {
|
|
797
|
+
return installedIds;
|
|
798
|
+
}
|
|
799
|
+
const installedSet = new Set(installedIds);
|
|
800
|
+
for (const skillId of requestedSkillIds) {
|
|
801
|
+
if (!installedSet.has(skillId)) {
|
|
802
|
+
throw new InstallError(`Skill "${skillId}" is not installed locally.`, "SKILL_NOT_INSTALLED");
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return requestedSkillIds;
|
|
806
|
+
}
|
|
807
|
+
function getNow(options) {
|
|
808
|
+
return options.now || (() => new Date().toISOString());
|
|
809
|
+
}
|
|
810
|
+
function toPosix(value) {
|
|
811
|
+
return value.split(path.sep).join("/");
|
|
812
|
+
}
|
|
813
|
+
async function maybeAutoSync(options) {
|
|
814
|
+
if (!options.enabled || !options.changed) {
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
return syncInstalledSkills({
|
|
818
|
+
cwd: options.cwd,
|
|
819
|
+
...(options.agentSkillsDir ? { agentSkillsDir: options.agentSkillsDir } : {}),
|
|
820
|
+
...(options.adapter ? { adapter: options.adapter } : {}),
|
|
821
|
+
...(options.mode ? { mode: options.mode } : {}),
|
|
822
|
+
now: options.now,
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
function toCatalogSourceInput(options, overrides = {}) {
|
|
826
|
+
const input = {};
|
|
827
|
+
if (options.owner) {
|
|
828
|
+
input.owner = options.owner;
|
|
829
|
+
}
|
|
830
|
+
if (options.repoName) {
|
|
831
|
+
input.repoName = options.repoName;
|
|
832
|
+
}
|
|
833
|
+
if (overrides.repo ?? options.repo) {
|
|
834
|
+
input.repo = overrides.repo ?? options.repo;
|
|
835
|
+
}
|
|
836
|
+
if (overrides.ref ?? options.ref) {
|
|
837
|
+
input.ref = overrides.ref ?? options.ref;
|
|
838
|
+
}
|
|
839
|
+
if (options.catalogPath) {
|
|
840
|
+
input.catalogPath = options.catalogPath;
|
|
841
|
+
}
|
|
842
|
+
if (options.skillsDir) {
|
|
843
|
+
input.skillsDir = options.skillsDir;
|
|
844
|
+
}
|
|
845
|
+
if (options.catalogUrl !== undefined) {
|
|
846
|
+
input.catalogUrl = options.catalogUrl;
|
|
847
|
+
}
|
|
848
|
+
return input;
|
|
849
|
+
}
|
|
850
|
+
function withAgentSkillsDir(options, agentSkillsDir) {
|
|
851
|
+
return {
|
|
852
|
+
...options,
|
|
853
|
+
...(agentSkillsDir ? { agentSkillsDir } : {}),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
async function confirmDirectInstall(skillRef, options) {
|
|
857
|
+
const warning = `Warning: ${skillRef} will be installed directly from GitHub and has not been verified by the active catalog.`;
|
|
858
|
+
(options.warn || console.error)(warning);
|
|
859
|
+
const confirm = options.confirm || (() => confirmAction("Continuar com a instalacao direta?"));
|
|
860
|
+
const accepted = await confirm();
|
|
861
|
+
if (!accepted) {
|
|
862
|
+
throw new InstallError("Instalacao direta cancelada pelo usuario.", "INSTALL_CANCELLED");
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
function parseGitHubSource(source) {
|
|
866
|
+
if (!source.startsWith("github:")) {
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
const withoutPrefix = source.slice("github:".length);
|
|
870
|
+
const separatorIndex = withoutPrefix.lastIndexOf("@");
|
|
871
|
+
if (separatorIndex <= 0) {
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
return parseDirectGitHubRef(`${withoutPrefix.slice(0, separatorIndex)}@${withoutPrefix.slice(separatorIndex + 1)}`);
|
|
875
|
+
}
|
|
876
|
+
function normalizeRepoSkillId(repo) {
|
|
877
|
+
return repo.trim().toLowerCase();
|
|
878
|
+
}
|
|
879
|
+
function toTitleCase(skillId) {
|
|
880
|
+
return skillId
|
|
881
|
+
.split("-")
|
|
882
|
+
.filter(Boolean)
|
|
883
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
884
|
+
.join(" ");
|
|
885
|
+
}
|
|
886
|
+
function toInstallError(error, fallbackMessage) {
|
|
887
|
+
if (error instanceof InstallError) {
|
|
888
|
+
return error;
|
|
889
|
+
}
|
|
890
|
+
if (error instanceof CliError) {
|
|
891
|
+
return new InstallError(error.message, error.code);
|
|
892
|
+
}
|
|
893
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
894
|
+
return new InstallError(`${fallbackMessage}: ${message}`);
|
|
895
|
+
}
|