aweskill 0.1.5
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/LICENSE +373 -0
- package/README.md +283 -0
- package/README.zh-CN.md +279 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +2174 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
- package/template/bundles/K-Dense-AI-scientific-skills.yaml +144 -0
- package/template/bundles/Superpowers.yaml +16 -0
- package/template/bundles/global.yaml +10 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { homedir as homedir2 } from "os";
|
|
6
|
+
|
|
7
|
+
// src/lib/backup.ts
|
|
8
|
+
import { access, mkdtemp, mkdir } from "fs/promises";
|
|
9
|
+
import { tmpdir } from "os";
|
|
10
|
+
import path2 from "path";
|
|
11
|
+
import { spawn } from "child_process";
|
|
12
|
+
|
|
13
|
+
// src/lib/path.ts
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import path from "path";
|
|
16
|
+
function sanitizeName(input) {
|
|
17
|
+
return input.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, 80);
|
|
18
|
+
}
|
|
19
|
+
function getAweskillPaths(homeDir) {
|
|
20
|
+
const rootDir = path.join(homeDir, ".aweskill");
|
|
21
|
+
return {
|
|
22
|
+
homeDir,
|
|
23
|
+
rootDir,
|
|
24
|
+
skillsDir: path.join(rootDir, "skills"),
|
|
25
|
+
dupSkillsDir: path.join(rootDir, "dup_skills"),
|
|
26
|
+
backupDir: path.join(rootDir, "backup"),
|
|
27
|
+
bundlesDir: path.join(rootDir, "bundles")
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function uniqueSorted(items) {
|
|
31
|
+
return [...new Set(items)].sort();
|
|
32
|
+
}
|
|
33
|
+
function splitCommaValues(input) {
|
|
34
|
+
return input.split(",").map((value) => value.trim()).filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/lib/backup.ts
|
|
38
|
+
function formatTimestamp(date) {
|
|
39
|
+
return date.toISOString().replace(/:/g, "-").replace(/\.\d{3}Z$/, "Z");
|
|
40
|
+
}
|
|
41
|
+
async function runTar(args) {
|
|
42
|
+
await new Promise((resolve, reject) => {
|
|
43
|
+
const child = spawn("tar", args, { stdio: "ignore" });
|
|
44
|
+
child.on("error", reject);
|
|
45
|
+
child.on("exit", (code) => {
|
|
46
|
+
if (code === 0) {
|
|
47
|
+
resolve();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
reject(new Error(`tar exited with code ${code ?? "unknown"}`));
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async function createSkillsBackupArchive(homeDir) {
|
|
55
|
+
const { rootDir, skillsDir, backupDir } = getAweskillPaths(homeDir);
|
|
56
|
+
await mkdir(backupDir, { recursive: true });
|
|
57
|
+
const archivePath = await nextBackupArchivePath(backupDir);
|
|
58
|
+
await runTar(["-czf", archivePath, "-C", rootDir, path2.relative(rootDir, skillsDir)]);
|
|
59
|
+
return archivePath;
|
|
60
|
+
}
|
|
61
|
+
async function extractSkillsArchive(archivePath) {
|
|
62
|
+
const tempDir = await mkdtemp(path2.join(tmpdir(), "aweskill-restore-"));
|
|
63
|
+
await runTar(["-xzf", archivePath, "-C", tempDir]);
|
|
64
|
+
return {
|
|
65
|
+
tempDir,
|
|
66
|
+
extractedSkillsDir: path2.join(tempDir, "skills")
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function nextBackupArchivePath(backupDir) {
|
|
70
|
+
const base = `skills-${formatTimestamp(/* @__PURE__ */ new Date())}`;
|
|
71
|
+
const primary = path2.join(backupDir, `${base}.tar.gz`);
|
|
72
|
+
if (!await pathExists(primary)) {
|
|
73
|
+
return primary;
|
|
74
|
+
}
|
|
75
|
+
let index = 1;
|
|
76
|
+
while (true) {
|
|
77
|
+
const candidate = path2.join(backupDir, `${base}-${index}.tar.gz`);
|
|
78
|
+
if (!await pathExists(candidate)) {
|
|
79
|
+
return candidate;
|
|
80
|
+
}
|
|
81
|
+
index += 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function pathExists(targetPath) {
|
|
85
|
+
try {
|
|
86
|
+
await access(targetPath);
|
|
87
|
+
return true;
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/commands/backup.ts
|
|
94
|
+
async function runBackup(context) {
|
|
95
|
+
const archivePath = await createSkillsBackupArchive(context.homeDir);
|
|
96
|
+
context.write(`Backed up skills to ${archivePath}`);
|
|
97
|
+
return { archivePath };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/lib/bundles.ts
|
|
101
|
+
import { access as access3, mkdir as mkdir3, readFile, readdir as readdir2, rm, writeFile } from "fs/promises";
|
|
102
|
+
import path4 from "path";
|
|
103
|
+
import { parse, stringify } from "yaml";
|
|
104
|
+
|
|
105
|
+
// src/lib/skills.ts
|
|
106
|
+
import { access as access2, mkdir as mkdir2, readdir } from "fs/promises";
|
|
107
|
+
import path3 from "path";
|
|
108
|
+
async function pathExists2(targetPath) {
|
|
109
|
+
try {
|
|
110
|
+
await access2(targetPath);
|
|
111
|
+
return true;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function ensureHomeLayout(homeDir) {
|
|
117
|
+
const paths = getAweskillPaths(homeDir);
|
|
118
|
+
await mkdir2(paths.rootDir, { recursive: true });
|
|
119
|
+
await mkdir2(paths.skillsDir, { recursive: true });
|
|
120
|
+
await mkdir2(paths.dupSkillsDir, { recursive: true });
|
|
121
|
+
await mkdir2(paths.backupDir, { recursive: true });
|
|
122
|
+
await mkdir2(paths.bundlesDir, { recursive: true });
|
|
123
|
+
}
|
|
124
|
+
function getSkillPath(homeDir, skillName) {
|
|
125
|
+
return path3.join(getAweskillPaths(homeDir).skillsDir, sanitizeName(skillName));
|
|
126
|
+
}
|
|
127
|
+
async function listSkills(homeDir) {
|
|
128
|
+
const skillsDir = getAweskillPaths(homeDir).skillsDir;
|
|
129
|
+
return listSkillEntriesInDirectory(skillsDir);
|
|
130
|
+
}
|
|
131
|
+
async function listSkillEntriesInDirectory(skillsDir) {
|
|
132
|
+
if (!await pathExists2(skillsDir)) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
136
|
+
const skills = await Promise.all(
|
|
137
|
+
entries.filter((entry) => entry.isDirectory() || entry.isSymbolicLink()).map(async (entry) => {
|
|
138
|
+
const skillPath = path3.join(skillsDir, entry.name);
|
|
139
|
+
return {
|
|
140
|
+
name: entry.name,
|
|
141
|
+
path: skillPath,
|
|
142
|
+
hasSKILLMd: await pathExists2(path3.join(skillPath, "SKILL.md"))
|
|
143
|
+
};
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
return skills.sort((left, right) => left.name.localeCompare(right.name));
|
|
147
|
+
}
|
|
148
|
+
async function assertSkillSource(sourcePath) {
|
|
149
|
+
const skillReadme = path3.join(sourcePath, "SKILL.md");
|
|
150
|
+
if (!await pathExists2(skillReadme)) {
|
|
151
|
+
throw new Error(`Skill source must contain SKILL.md: ${sourcePath}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function skillExists(homeDir, skillName) {
|
|
155
|
+
return pathExists2(getSkillPath(homeDir, skillName));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/lib/bundles.ts
|
|
159
|
+
async function pathExists3(targetPath) {
|
|
160
|
+
try {
|
|
161
|
+
await access3(targetPath);
|
|
162
|
+
return true;
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function bundleFilePath(homeDir, bundleName) {
|
|
168
|
+
return path4.join(getAweskillPaths(homeDir).bundlesDir, `${sanitizeName(bundleName)}.yaml`);
|
|
169
|
+
}
|
|
170
|
+
function bundleFilePathInDirectory(bundlesDir, bundleName) {
|
|
171
|
+
return path4.join(bundlesDir, `${sanitizeName(bundleName)}.yaml`);
|
|
172
|
+
}
|
|
173
|
+
function normalizeBundle(raw, fallbackName) {
|
|
174
|
+
const data = raw ?? {};
|
|
175
|
+
return {
|
|
176
|
+
name: sanitizeName(data.name ?? fallbackName),
|
|
177
|
+
skills: uniqueSorted((data.skills ?? []).map((skill) => sanitizeName(String(skill))).filter(Boolean))
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
async function listBundles(homeDir) {
|
|
181
|
+
const bundlesDir = getAweskillPaths(homeDir).bundlesDir;
|
|
182
|
+
return listBundlesInDirectory(bundlesDir);
|
|
183
|
+
}
|
|
184
|
+
async function listBundlesInDirectory(bundlesDir) {
|
|
185
|
+
await mkdir3(bundlesDir, { recursive: true });
|
|
186
|
+
const entries = await readdir2(bundlesDir, { withFileTypes: true });
|
|
187
|
+
const bundles = await Promise.all(
|
|
188
|
+
entries.filter((entry) => entry.isFile() && entry.name.endsWith(".yaml")).map(async (entry) => readBundleFromDirectory(bundlesDir, entry.name.replace(/\.yaml$/, "")))
|
|
189
|
+
);
|
|
190
|
+
return bundles.sort((left, right) => left.name.localeCompare(right.name));
|
|
191
|
+
}
|
|
192
|
+
async function readBundle(homeDir, bundleName) {
|
|
193
|
+
const filePath = bundleFilePath(homeDir, bundleName);
|
|
194
|
+
const content = await readFile(filePath, "utf8");
|
|
195
|
+
return normalizeBundle(parse(content), bundleName);
|
|
196
|
+
}
|
|
197
|
+
async function readBundleFromDirectory(bundlesDir, bundleName) {
|
|
198
|
+
const filePath = bundleFilePathInDirectory(bundlesDir, bundleName);
|
|
199
|
+
const content = await readFile(filePath, "utf8");
|
|
200
|
+
return normalizeBundle(parse(content), bundleName);
|
|
201
|
+
}
|
|
202
|
+
async function writeBundle(homeDir, bundle) {
|
|
203
|
+
const normalized = normalizeBundle(bundle, bundle.name);
|
|
204
|
+
const filePath = bundleFilePath(homeDir, normalized.name);
|
|
205
|
+
await mkdir3(path4.dirname(filePath), { recursive: true });
|
|
206
|
+
await writeFile(filePath, stringify(normalized), "utf8");
|
|
207
|
+
return normalized;
|
|
208
|
+
}
|
|
209
|
+
async function createBundle(homeDir, bundleName) {
|
|
210
|
+
const normalizedName = sanitizeName(bundleName);
|
|
211
|
+
return writeBundle(homeDir, { name: normalizedName, skills: [] });
|
|
212
|
+
}
|
|
213
|
+
async function addSkillToBundle(homeDir, bundleName, skillName) {
|
|
214
|
+
const normalizedSkill = sanitizeName(skillName);
|
|
215
|
+
if (!await skillExists(homeDir, normalizedSkill)) {
|
|
216
|
+
throw new Error(`Unknown skill: ${normalizedSkill}`);
|
|
217
|
+
}
|
|
218
|
+
const bundle = await readBundle(homeDir, bundleName);
|
|
219
|
+
bundle.skills = uniqueSorted([...bundle.skills, normalizedSkill].filter(Boolean));
|
|
220
|
+
return writeBundle(homeDir, bundle);
|
|
221
|
+
}
|
|
222
|
+
async function removeSkillFromBundle(homeDir, bundleName, skillName) {
|
|
223
|
+
const bundle = await readBundle(homeDir, bundleName);
|
|
224
|
+
const normalizedSkill = sanitizeName(skillName);
|
|
225
|
+
bundle.skills = bundle.skills.filter((skill) => skill !== normalizedSkill);
|
|
226
|
+
return writeBundle(homeDir, bundle);
|
|
227
|
+
}
|
|
228
|
+
async function deleteBundle(homeDir, bundleName) {
|
|
229
|
+
const filePath = bundleFilePath(homeDir, bundleName);
|
|
230
|
+
if (!await pathExists3(filePath)) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
await rm(filePath, { force: true });
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/lib/templates.ts
|
|
238
|
+
import { access as access4 } from "fs/promises";
|
|
239
|
+
import path5 from "path";
|
|
240
|
+
import { fileURLToPath } from "url";
|
|
241
|
+
async function pathExists4(targetPath) {
|
|
242
|
+
try {
|
|
243
|
+
await access4(targetPath);
|
|
244
|
+
return true;
|
|
245
|
+
} catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async function getTemplateBundlesDir() {
|
|
250
|
+
const moduleDir = path5.dirname(fileURLToPath(import.meta.url));
|
|
251
|
+
const candidates = [
|
|
252
|
+
path5.resolve(moduleDir, "..", "..", "template", "bundles"),
|
|
253
|
+
path5.resolve(moduleDir, "..", "template", "bundles")
|
|
254
|
+
];
|
|
255
|
+
for (const candidate of candidates) {
|
|
256
|
+
if (await pathExists4(candidate)) {
|
|
257
|
+
return candidate;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
throw new Error(`Template bundles directory not found. Checked: ${candidates.join(", ")}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/commands/bundle.ts
|
|
264
|
+
function parseNames(value) {
|
|
265
|
+
return uniqueSorted(splitCommaValues(value).map((entry) => sanitizeName(entry)));
|
|
266
|
+
}
|
|
267
|
+
async function runBundleCreate(context, bundleName) {
|
|
268
|
+
const bundles = await Promise.all(parseNames(bundleName).map((name) => createBundle(context.homeDir, name)));
|
|
269
|
+
context.write(bundles.map((bundle) => `Created bundle ${bundle.name}`).join("\n"));
|
|
270
|
+
return bundles;
|
|
271
|
+
}
|
|
272
|
+
async function runBundleShow(context, bundleName) {
|
|
273
|
+
const bundles = await Promise.all(parseNames(bundleName).map((name) => readBundle(context.homeDir, name)));
|
|
274
|
+
context.write(bundles.map((bundle) => `${bundle.name}: ${bundle.skills.join(", ") || "(empty)"}`).join("\n"));
|
|
275
|
+
return bundles;
|
|
276
|
+
}
|
|
277
|
+
async function runBundleAddSkill(context, bundleName, skillName) {
|
|
278
|
+
const bundleNames = parseNames(bundleName);
|
|
279
|
+
const skillNames = parseNames(skillName);
|
|
280
|
+
const bundles = [];
|
|
281
|
+
for (const currentBundleName of bundleNames) {
|
|
282
|
+
let bundle = await readBundle(context.homeDir, currentBundleName);
|
|
283
|
+
for (const currentSkillName of skillNames) {
|
|
284
|
+
bundle = await addSkillToBundle(context.homeDir, bundle.name, currentSkillName);
|
|
285
|
+
}
|
|
286
|
+
bundles.push(bundle);
|
|
287
|
+
}
|
|
288
|
+
context.write(bundles.map((bundle) => `Bundle ${bundle.name}: ${bundle.skills.join(", ") || "(empty)"}`).join("\n"));
|
|
289
|
+
return bundles;
|
|
290
|
+
}
|
|
291
|
+
async function runBundleRemoveSkill(context, bundleName, skillName) {
|
|
292
|
+
const bundleNames = parseNames(bundleName);
|
|
293
|
+
const skillNames = parseNames(skillName);
|
|
294
|
+
const bundles = [];
|
|
295
|
+
for (const currentBundleName of bundleNames) {
|
|
296
|
+
let bundle = await readBundle(context.homeDir, currentBundleName);
|
|
297
|
+
for (const currentSkillName of skillNames) {
|
|
298
|
+
bundle = await removeSkillFromBundle(context.homeDir, bundle.name, currentSkillName);
|
|
299
|
+
}
|
|
300
|
+
bundles.push(bundle);
|
|
301
|
+
}
|
|
302
|
+
context.write(bundles.map((bundle) => `Bundle ${bundle.name}: ${bundle.skills.join(", ") || "(empty)"}`).join("\n"));
|
|
303
|
+
return bundles;
|
|
304
|
+
}
|
|
305
|
+
async function runBundleDelete(context, bundleName) {
|
|
306
|
+
const deletedNames = [];
|
|
307
|
+
for (const currentBundleName of parseNames(bundleName)) {
|
|
308
|
+
const deleted = await deleteBundle(context.homeDir, currentBundleName);
|
|
309
|
+
if (!deleted) {
|
|
310
|
+
throw new Error(`Bundle not found: ${currentBundleName}`);
|
|
311
|
+
}
|
|
312
|
+
deletedNames.push(currentBundleName);
|
|
313
|
+
}
|
|
314
|
+
context.write(deletedNames.map((name) => `Deleted bundle ${name}`).join("\n"));
|
|
315
|
+
return deletedNames;
|
|
316
|
+
}
|
|
317
|
+
async function runBundleAddTemplate(context, bundleName) {
|
|
318
|
+
const templateBundlesDir = await getTemplateBundlesDir();
|
|
319
|
+
const bundles = [];
|
|
320
|
+
for (const currentBundleName of parseNames(bundleName)) {
|
|
321
|
+
const templateBundle = await readBundleFromDirectory(templateBundlesDir, currentBundleName);
|
|
322
|
+
try {
|
|
323
|
+
await readBundle(context.homeDir, templateBundle.name);
|
|
324
|
+
throw new Error(`Bundle already exists: ${templateBundle.name}`);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (!(error instanceof Error) || !error.message.includes("ENOENT")) {
|
|
327
|
+
if (error instanceof Error && error.message.startsWith("Bundle already exists:")) {
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
bundles.push(await writeBundle(context.homeDir, templateBundle));
|
|
334
|
+
}
|
|
335
|
+
context.write(bundles.map((bundle) => `Added bundle ${bundle.name} from template`).join("\n"));
|
|
336
|
+
return bundles;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/lib/import.ts
|
|
340
|
+
import { access as access5, cp, lstat, mkdir as mkdir4, readlink, readdir as readdir3, rename, rm as rm2, stat } from "fs/promises";
|
|
341
|
+
import path6 from "path";
|
|
342
|
+
var MissingSymlinkSourceError = class extends Error {
|
|
343
|
+
sourcePath;
|
|
344
|
+
resolvedSourcePath;
|
|
345
|
+
constructor(sourcePath, resolvedSourcePath) {
|
|
346
|
+
super(`Missing symlink source: ${resolvedSourcePath}`);
|
|
347
|
+
this.name = "MissingSymlinkSourceError";
|
|
348
|
+
this.sourcePath = sourcePath;
|
|
349
|
+
this.resolvedSourcePath = resolvedSourcePath;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
async function pathExists5(targetPath) {
|
|
353
|
+
try {
|
|
354
|
+
await access5(targetPath);
|
|
355
|
+
return true;
|
|
356
|
+
} catch {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async function resolveImportSource(sourcePath) {
|
|
361
|
+
const statResult = await lstat(sourcePath);
|
|
362
|
+
if (!statResult.isSymbolicLink()) {
|
|
363
|
+
return {
|
|
364
|
+
effectiveSourcePath: sourcePath,
|
|
365
|
+
isSymlinkSource: false
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const linkTarget = await readlink(sourcePath);
|
|
369
|
+
const resolvedSourcePath = path6.resolve(path6.dirname(sourcePath), linkTarget);
|
|
370
|
+
if (!await pathExists5(resolvedSourcePath)) {
|
|
371
|
+
throw new MissingSymlinkSourceError(sourcePath, resolvedSourcePath);
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
effectiveSourcePath: resolvedSourcePath,
|
|
375
|
+
isSymlinkSource: true
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
async function mergeMissingEntries(sourcePath, destinationPath) {
|
|
379
|
+
const sourceStat = await stat(sourcePath);
|
|
380
|
+
if (sourceStat.isDirectory()) {
|
|
381
|
+
await mkdir4(destinationPath, { recursive: true });
|
|
382
|
+
const entries = await readdir3(sourcePath, { withFileTypes: true });
|
|
383
|
+
for (const entry of entries) {
|
|
384
|
+
await mergeMissingEntries(path6.join(sourcePath, entry.name), path6.join(destinationPath, entry.name));
|
|
385
|
+
}
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (await pathExists5(destinationPath)) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
await mkdir4(path6.dirname(destinationPath), { recursive: true });
|
|
392
|
+
await cp(sourcePath, destinationPath, { recursive: false, errorOnExist: true, force: false });
|
|
393
|
+
}
|
|
394
|
+
async function copyIntoDestination(sourcePath, destination, override) {
|
|
395
|
+
if (!override && await pathExists5(destination)) {
|
|
396
|
+
await mergeMissingEntries(sourcePath, destination);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
await cp(sourcePath, destination, { recursive: true, errorOnExist: false, force: override });
|
|
400
|
+
}
|
|
401
|
+
async function moveIntoDestination(sourcePath, destination, override) {
|
|
402
|
+
if (!await pathExists5(destination)) {
|
|
403
|
+
await rename(sourcePath, destination);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (override) {
|
|
407
|
+
await cp(sourcePath, destination, { recursive: true, errorOnExist: false, force: true });
|
|
408
|
+
await rm2(sourcePath, { recursive: true, force: true });
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
await mergeMissingEntries(sourcePath, destination);
|
|
412
|
+
}
|
|
413
|
+
async function importBatchSources(options) {
|
|
414
|
+
const seen = /* @__PURE__ */ new Set();
|
|
415
|
+
const imported = [];
|
|
416
|
+
const skipped = [];
|
|
417
|
+
const overwritten = [];
|
|
418
|
+
const warnings = [];
|
|
419
|
+
const errors = [];
|
|
420
|
+
let missingSources = 0;
|
|
421
|
+
for (const source of options.sources) {
|
|
422
|
+
if (seen.has(source.name)) {
|
|
423
|
+
skipped.push(source.name);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
seen.add(source.name);
|
|
427
|
+
const alreadyExists = await skillExists(options.homeDir, source.name);
|
|
428
|
+
if (alreadyExists && !options.override) {
|
|
429
|
+
skipped.push(source.name);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
const result = await importSkill({
|
|
434
|
+
homeDir: options.homeDir,
|
|
435
|
+
sourcePath: source.path,
|
|
436
|
+
mode: options.mode,
|
|
437
|
+
override: options.override
|
|
438
|
+
});
|
|
439
|
+
warnings.push(...result.warnings);
|
|
440
|
+
if (alreadyExists) {
|
|
441
|
+
overwritten.push(source.name);
|
|
442
|
+
} else {
|
|
443
|
+
imported.push(source.name);
|
|
444
|
+
}
|
|
445
|
+
} catch (error) {
|
|
446
|
+
if (error instanceof MissingSymlinkSourceError) {
|
|
447
|
+
missingSources += 1;
|
|
448
|
+
errors.push(`Broken symlink for ${source.name}: ${source.path}; source not found: ${error.resolvedSourcePath}`);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (error instanceof Error && error.message.startsWith("Skill source must contain SKILL.md:")) {
|
|
452
|
+
skipped.push(source.name);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return { imported, skipped, overwritten, warnings, errors, missingSources };
|
|
459
|
+
}
|
|
460
|
+
async function listImportableChildren(sourceRoot) {
|
|
461
|
+
const entries = await readdir3(sourceRoot, { withFileTypes: true });
|
|
462
|
+
const sources = [];
|
|
463
|
+
for (const entry of entries) {
|
|
464
|
+
if (!(entry.isDirectory() || entry.isSymbolicLink())) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const childPath = path6.join(sourceRoot, entry.name);
|
|
468
|
+
if (await pathExists5(path6.join(childPath, "SKILL.md"))) {
|
|
469
|
+
sources.push({
|
|
470
|
+
name: sanitizeName(entry.name),
|
|
471
|
+
path: childPath
|
|
472
|
+
});
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (entry.isSymbolicLink()) {
|
|
476
|
+
sources.push({
|
|
477
|
+
name: sanitizeName(entry.name),
|
|
478
|
+
path: childPath
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return sources.sort((left, right) => left.name.localeCompare(right.name));
|
|
483
|
+
}
|
|
484
|
+
async function importSkill(options) {
|
|
485
|
+
const { effectiveSourcePath, isSymlinkSource } = await resolveImportSource(options.sourcePath);
|
|
486
|
+
await assertSkillSource(effectiveSourcePath);
|
|
487
|
+
const skillName = sanitizeName(path6.basename(options.sourcePath));
|
|
488
|
+
if (!skillName) {
|
|
489
|
+
throw new Error(`Unable to infer skill name from path: ${options.sourcePath}`);
|
|
490
|
+
}
|
|
491
|
+
const destination = getSkillPath(options.homeDir, skillName);
|
|
492
|
+
await mkdir4(path6.dirname(destination), { recursive: true });
|
|
493
|
+
const warnings = [];
|
|
494
|
+
if (isSymlinkSource) {
|
|
495
|
+
warnings.push(`Source ${options.sourcePath} is a symlink; copied from ${effectiveSourcePath} to ${destination}`);
|
|
496
|
+
}
|
|
497
|
+
if (options.mode === "mv" && !isSymlinkSource) {
|
|
498
|
+
await moveIntoDestination(effectiveSourcePath, destination, options.override ?? false);
|
|
499
|
+
} else {
|
|
500
|
+
await copyIntoDestination(effectiveSourcePath, destination, options.override ?? false);
|
|
501
|
+
}
|
|
502
|
+
return { name: skillName, destination, warnings };
|
|
503
|
+
}
|
|
504
|
+
async function importScannedSkills(options) {
|
|
505
|
+
return importBatchSources({
|
|
506
|
+
homeDir: options.homeDir,
|
|
507
|
+
sources: options.candidates.map((candidate) => ({
|
|
508
|
+
name: candidate.name,
|
|
509
|
+
path: candidate.path
|
|
510
|
+
})),
|
|
511
|
+
mode: options.mode,
|
|
512
|
+
override: options.override
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
async function importPath(options) {
|
|
516
|
+
if (await pathExists5(path6.join(options.sourcePath, "SKILL.md"))) {
|
|
517
|
+
const skillName = sanitizeName(path6.basename(options.sourcePath));
|
|
518
|
+
const alreadyExisted = skillName ? await skillExists(options.homeDir, skillName) : false;
|
|
519
|
+
if (alreadyExisted && !options.override) {
|
|
520
|
+
return {
|
|
521
|
+
kind: "single",
|
|
522
|
+
alreadyExisted: true,
|
|
523
|
+
name: skillName,
|
|
524
|
+
destination: getSkillPath(options.homeDir, skillName),
|
|
525
|
+
warnings: []
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
const result = await importSkill(options);
|
|
529
|
+
return { kind: "single", alreadyExisted, ...result };
|
|
530
|
+
}
|
|
531
|
+
const sources = await listImportableChildren(options.sourcePath);
|
|
532
|
+
if (sources.length === 0) {
|
|
533
|
+
throw new Error(`Path is neither a skill directory nor a skills directory: ${options.sourcePath}`);
|
|
534
|
+
}
|
|
535
|
+
const batchResult = await importBatchSources({
|
|
536
|
+
homeDir: options.homeDir,
|
|
537
|
+
sources,
|
|
538
|
+
mode: options.mode,
|
|
539
|
+
override: options.override
|
|
540
|
+
});
|
|
541
|
+
return { kind: "batch", ...batchResult };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/lib/scanner.ts
|
|
545
|
+
import { access as access7, lstat as lstat2, readdir as readdir4, readlink as readlink2 } from "fs/promises";
|
|
546
|
+
import path8 from "path";
|
|
547
|
+
|
|
548
|
+
// src/lib/agents.ts
|
|
549
|
+
import { access as access6 } from "fs/promises";
|
|
550
|
+
import path7 from "path";
|
|
551
|
+
var AGENTS = {
|
|
552
|
+
amp: {
|
|
553
|
+
id: "amp",
|
|
554
|
+
displayName: "Amp",
|
|
555
|
+
defaultProjectionMode: "symlink",
|
|
556
|
+
rootDir: (homeDir) => path7.join(homeDir, ".amp"),
|
|
557
|
+
globalSkillsDir: (homeDir) => path7.join(homeDir, ".amp", "skills"),
|
|
558
|
+
projectSkillsDir: (projectDir) => path7.join(projectDir, ".amp", "skills")
|
|
559
|
+
},
|
|
560
|
+
"claude-code": {
|
|
561
|
+
id: "claude-code",
|
|
562
|
+
displayName: "Claude Code",
|
|
563
|
+
defaultProjectionMode: "symlink",
|
|
564
|
+
rootDir: (homeDir) => path7.join(homeDir, ".claude"),
|
|
565
|
+
globalSkillsDir: (homeDir) => path7.join(homeDir, ".claude", "skills"),
|
|
566
|
+
projectSkillsDir: (projectDir) => path7.join(projectDir, ".claude", "skills")
|
|
567
|
+
},
|
|
568
|
+
cline: {
|
|
569
|
+
id: "cline",
|
|
570
|
+
displayName: "Cline",
|
|
571
|
+
defaultProjectionMode: "symlink",
|
|
572
|
+
rootDir: (homeDir) => path7.join(homeDir, ".cline"),
|
|
573
|
+
globalSkillsDir: (homeDir) => path7.join(homeDir, ".cline", "skills"),
|
|
574
|
+
projectSkillsDir: (projectDir) => path7.join(projectDir, ".cline", "skills")
|
|
575
|
+
},
|
|
576
|
+
codex: {
|
|
577
|
+
id: "codex",
|
|
578
|
+
displayName: "Codex",
|
|
579
|
+
defaultProjectionMode: "symlink",
|
|
580
|
+
rootDir: (homeDir) => path7.join(homeDir, ".codex"),
|
|
581
|
+
globalSkillsDir: (homeDir) => path7.join(homeDir, ".codex", "skills"),
|
|
582
|
+
projectSkillsDir: (projectDir) => path7.join(projectDir, ".codex", "skills")
|
|
583
|
+
},
|
|
584
|
+
cursor: {
|
|
585
|
+
id: "cursor",
|
|
586
|
+
displayName: "Cursor",
|
|
587
|
+
defaultProjectionMode: "copy",
|
|
588
|
+
rootDir: (homeDir) => path7.join(homeDir, ".cursor"),
|
|
589
|
+
globalSkillsDir: (homeDir) => path7.join(homeDir, ".cursor", "skills"),
|
|
590
|
+
projectSkillsDir: (projectDir) => path7.join(projectDir, ".cursor", "skills")
|
|
591
|
+
},
|
|
592
|
+
"gemini-cli": {
|
|
593
|
+
id: "gemini-cli",
|
|
594
|
+
displayName: "Gemini CLI",
|
|
595
|
+
defaultProjectionMode: "symlink",
|
|
596
|
+
rootDir: (homeDir) => path7.join(homeDir, ".gemini"),
|
|
597
|
+
globalSkillsDir: (homeDir) => path7.join(homeDir, ".gemini", "skills"),
|
|
598
|
+
projectSkillsDir: (projectDir) => path7.join(projectDir, ".gemini", "skills")
|
|
599
|
+
},
|
|
600
|
+
goose: {
|
|
601
|
+
id: "goose",
|
|
602
|
+
displayName: "Goose",
|
|
603
|
+
defaultProjectionMode: "symlink",
|
|
604
|
+
rootDir: (homeDir) => path7.join(homeDir, ".goose"),
|
|
605
|
+
globalSkillsDir: (homeDir) => path7.join(homeDir, ".goose", "skills"),
|
|
606
|
+
projectSkillsDir: (projectDir) => path7.join(projectDir, ".goose", "skills")
|
|
607
|
+
},
|
|
608
|
+
opencode: {
|
|
609
|
+
id: "opencode",
|
|
610
|
+
displayName: "OpenCode",
|
|
611
|
+
defaultProjectionMode: "symlink",
|
|
612
|
+
rootDir: (homeDir) => path7.join(homeDir, ".opencode"),
|
|
613
|
+
globalSkillsDir: (homeDir) => path7.join(homeDir, ".opencode", "skills"),
|
|
614
|
+
projectSkillsDir: (projectDir) => path7.join(projectDir, ".opencode", "skills")
|
|
615
|
+
},
|
|
616
|
+
roo: {
|
|
617
|
+
id: "roo",
|
|
618
|
+
displayName: "Roo Code",
|
|
619
|
+
defaultProjectionMode: "symlink",
|
|
620
|
+
rootDir: (homeDir) => path7.join(homeDir, ".roo"),
|
|
621
|
+
globalSkillsDir: (homeDir) => path7.join(homeDir, ".roo", "skills"),
|
|
622
|
+
projectSkillsDir: (projectDir) => path7.join(projectDir, ".roo", "skills")
|
|
623
|
+
},
|
|
624
|
+
windsurf: {
|
|
625
|
+
id: "windsurf",
|
|
626
|
+
displayName: "Windsurf",
|
|
627
|
+
defaultProjectionMode: "symlink",
|
|
628
|
+
rootDir: (homeDir) => path7.join(homeDir, ".windsurf"),
|
|
629
|
+
globalSkillsDir: (homeDir) => path7.join(homeDir, ".windsurf", "skills"),
|
|
630
|
+
projectSkillsDir: (projectDir) => path7.join(projectDir, ".windsurf", "skills")
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
async function pathExists6(targetPath) {
|
|
634
|
+
try {
|
|
635
|
+
await access6(targetPath);
|
|
636
|
+
return true;
|
|
637
|
+
} catch {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function listSupportedAgents() {
|
|
642
|
+
return Object.values(AGENTS);
|
|
643
|
+
}
|
|
644
|
+
function listSupportedAgentIds() {
|
|
645
|
+
return listSupportedAgents().map((agent) => agent.id).sort();
|
|
646
|
+
}
|
|
647
|
+
function isAgentId(value) {
|
|
648
|
+
return value in AGENTS;
|
|
649
|
+
}
|
|
650
|
+
function getAgentDefinition(agentId) {
|
|
651
|
+
return AGENTS[agentId];
|
|
652
|
+
}
|
|
653
|
+
function resolveAgentSkillsDir(agentId, scope, baseDir) {
|
|
654
|
+
const definition = getAgentDefinition(agentId);
|
|
655
|
+
return scope === "global" ? definition.globalSkillsDir(baseDir) : definition.projectSkillsDir(baseDir);
|
|
656
|
+
}
|
|
657
|
+
function getProjectionMode(agentId) {
|
|
658
|
+
return getAgentDefinition(agentId).defaultProjectionMode;
|
|
659
|
+
}
|
|
660
|
+
async function detectInstalledAgents(options) {
|
|
661
|
+
const installed = [];
|
|
662
|
+
for (const agent of listSupportedAgents()) {
|
|
663
|
+
const globalRootPath = agent.rootDir(options.homeDir);
|
|
664
|
+
const projectPath = options.projectDir ? agent.projectSkillsDir(options.projectDir) : null;
|
|
665
|
+
if (await pathExists6(globalRootPath)) {
|
|
666
|
+
installed.push(agent.id);
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (projectPath && await pathExists6(projectPath)) {
|
|
670
|
+
installed.push(agent.id);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return installed.sort();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/lib/scanner.ts
|
|
677
|
+
async function hasSkillReadme(skillDir) {
|
|
678
|
+
try {
|
|
679
|
+
await access7(path8.join(skillDir, "SKILL.md"));
|
|
680
|
+
return true;
|
|
681
|
+
} catch {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
async function isSymlinkPath(targetPath) {
|
|
686
|
+
try {
|
|
687
|
+
return (await lstat2(targetPath)).isSymbolicLink();
|
|
688
|
+
} catch {
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
async function pathExists7(targetPath) {
|
|
693
|
+
try {
|
|
694
|
+
await access7(targetPath);
|
|
695
|
+
return true;
|
|
696
|
+
} catch {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
async function resolveSymlinkSource(targetPath) {
|
|
701
|
+
try {
|
|
702
|
+
const linkTarget = await readlink2(targetPath);
|
|
703
|
+
const sourcePath = path8.resolve(path8.dirname(targetPath), linkTarget);
|
|
704
|
+
return {
|
|
705
|
+
sourcePath,
|
|
706
|
+
isBroken: !await pathExists7(sourcePath)
|
|
707
|
+
};
|
|
708
|
+
} catch {
|
|
709
|
+
return {
|
|
710
|
+
isBroken: true
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
async function scanDirectory(baseDir, agentId, scope, projectDir) {
|
|
715
|
+
try {
|
|
716
|
+
const entries = await readdir4(baseDir, { withFileTypes: true });
|
|
717
|
+
const candidates = [];
|
|
718
|
+
for (const entry of entries) {
|
|
719
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
const fullPath = path8.join(baseDir, entry.name);
|
|
723
|
+
const isSymlink = await isSymlinkPath(fullPath);
|
|
724
|
+
const symlinkInfo = isSymlink ? await resolveSymlinkSource(fullPath) : { isBroken: false };
|
|
725
|
+
if (!await hasSkillReadme(fullPath) && !symlinkInfo.isBroken) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
candidates.push({
|
|
729
|
+
agentId,
|
|
730
|
+
name: sanitizeName(entry.name),
|
|
731
|
+
path: fullPath,
|
|
732
|
+
scope,
|
|
733
|
+
projectDir,
|
|
734
|
+
isSymlink,
|
|
735
|
+
symlinkSourcePath: symlinkInfo.sourcePath,
|
|
736
|
+
isBrokenSymlink: symlinkInfo.isBroken
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
return candidates;
|
|
740
|
+
} catch {
|
|
741
|
+
return [];
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
async function scanSkills(options) {
|
|
745
|
+
const results = [];
|
|
746
|
+
for (const agent of listSupportedAgents()) {
|
|
747
|
+
results.push(...await scanDirectory(agent.globalSkillsDir(options.homeDir), agent.id, "global"));
|
|
748
|
+
for (const projectDir of options.projectDirs ?? []) {
|
|
749
|
+
results.push(...await scanDirectory(agent.projectSkillsDir(projectDir), agent.id, "project", projectDir));
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return results.sort((left, right) => left.path.localeCompare(right.path));
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/commands/import.ts
|
|
756
|
+
async function runImport(context, options) {
|
|
757
|
+
if (options.scan) {
|
|
758
|
+
const candidates = await scanSkills({
|
|
759
|
+
homeDir: context.homeDir,
|
|
760
|
+
projectDirs: [context.cwd]
|
|
761
|
+
});
|
|
762
|
+
const result2 = await importScannedSkills({
|
|
763
|
+
homeDir: context.homeDir,
|
|
764
|
+
candidates,
|
|
765
|
+
mode: options.mode,
|
|
766
|
+
override: options.override
|
|
767
|
+
});
|
|
768
|
+
for (const warning of result2.warnings) {
|
|
769
|
+
context.write(`Warning: ${warning}`);
|
|
770
|
+
}
|
|
771
|
+
for (const error of result2.errors) {
|
|
772
|
+
context.error(`Error: ${error}`);
|
|
773
|
+
}
|
|
774
|
+
context.write(`Imported ${result2.imported.length} skills`);
|
|
775
|
+
if (result2.overwritten.length > 0) {
|
|
776
|
+
context.write(`Overwritten ${result2.overwritten.length} existing skills: ${result2.overwritten.join(", ")}`);
|
|
777
|
+
}
|
|
778
|
+
if (result2.skipped.length > 0) {
|
|
779
|
+
context.write(`Skipped ${result2.skipped.length} existing skills (use --override to overwrite): ${result2.skipped.join(", ")}`);
|
|
780
|
+
}
|
|
781
|
+
if (result2.missingSources > 0) {
|
|
782
|
+
context.write(`Missing source files: ${result2.missingSources}`);
|
|
783
|
+
}
|
|
784
|
+
return result2;
|
|
785
|
+
}
|
|
786
|
+
if (!options.sourcePath) {
|
|
787
|
+
throw new Error("import requires a source path or --scan");
|
|
788
|
+
}
|
|
789
|
+
const result = await importPath({
|
|
790
|
+
homeDir: context.homeDir,
|
|
791
|
+
sourcePath: options.sourcePath,
|
|
792
|
+
mode: options.mode,
|
|
793
|
+
override: options.override
|
|
794
|
+
});
|
|
795
|
+
if (result.kind === "single") {
|
|
796
|
+
for (const warning of result.warnings) {
|
|
797
|
+
context.write(`Warning: ${warning}`);
|
|
798
|
+
}
|
|
799
|
+
if (result.alreadyExisted && !options.override) {
|
|
800
|
+
context.write(`Skipped ${result.name} (already exists; use --override to overwrite)`);
|
|
801
|
+
} else if (result.alreadyExisted) {
|
|
802
|
+
context.write(`Overwritten ${result.name}`);
|
|
803
|
+
} else {
|
|
804
|
+
context.write(`Imported ${result.name}`);
|
|
805
|
+
}
|
|
806
|
+
return result;
|
|
807
|
+
}
|
|
808
|
+
for (const warning of result.warnings) {
|
|
809
|
+
context.write(`Warning: ${warning}`);
|
|
810
|
+
}
|
|
811
|
+
for (const error of result.errors) {
|
|
812
|
+
context.error(`Error: ${error}`);
|
|
813
|
+
}
|
|
814
|
+
context.write(`Imported ${result.imported.length} skills`);
|
|
815
|
+
if (result.overwritten.length > 0) {
|
|
816
|
+
context.write(`Overwritten ${result.overwritten.length} existing skills: ${result.overwritten.join(", ")}`);
|
|
817
|
+
}
|
|
818
|
+
if (result.skipped.length > 0) {
|
|
819
|
+
context.write(`Skipped ${result.skipped.length} existing skills (use --override to overwrite): ${result.skipped.join(", ")}`);
|
|
820
|
+
}
|
|
821
|
+
if (result.missingSources > 0) {
|
|
822
|
+
context.write(`Missing source files: ${result.missingSources}`);
|
|
823
|
+
}
|
|
824
|
+
return result;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/lib/symlink.ts
|
|
828
|
+
import { cp as cp2, lstat as lstat3, mkdir as mkdir5, readFile as readFile2, readdir as readdir5, readlink as readlink3, rm as rm3, symlink, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
829
|
+
import path9 from "path";
|
|
830
|
+
var COPY_MARKER = ".aweskill-projection.json";
|
|
831
|
+
async function tryLstat(targetPath) {
|
|
832
|
+
try {
|
|
833
|
+
return await lstat3(targetPath);
|
|
834
|
+
} catch {
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
async function readCopyMarker(targetPath) {
|
|
839
|
+
try {
|
|
840
|
+
const content = await readFile2(path9.join(targetPath, COPY_MARKER), "utf8");
|
|
841
|
+
const parsed = JSON.parse(content);
|
|
842
|
+
return parsed.managedBy === "aweskill" ? parsed : null;
|
|
843
|
+
} catch {
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
async function assertProjectionTargetSafe(mode, sourcePath, targetPath, options = {}) {
|
|
848
|
+
const existing = await tryLstat(targetPath);
|
|
849
|
+
if (!existing) {
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (mode === "symlink") {
|
|
853
|
+
if (!existing.isSymbolicLink()) {
|
|
854
|
+
if (options.allowReplaceExisting) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
throw new Error(`Refusing to overwrite non-symlink target: ${targetPath}`);
|
|
858
|
+
}
|
|
859
|
+
const currentTarget = await readlink3(targetPath);
|
|
860
|
+
const resolvedCurrent = path9.resolve(path9.dirname(targetPath), currentTarget);
|
|
861
|
+
if (resolvedCurrent === path9.resolve(sourcePath)) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (existing.isSymbolicLink()) {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const marker = await readCopyMarker(targetPath);
|
|
870
|
+
if (!marker) {
|
|
871
|
+
if (options.allowReplaceExisting) {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
throw new Error(`Refusing to overwrite unmanaged directory: ${targetPath}`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
async function createSkillSymlink(sourcePath, targetPath, options = {}) {
|
|
878
|
+
await mkdir5(path9.dirname(targetPath), { recursive: true });
|
|
879
|
+
const existing = await tryLstat(targetPath);
|
|
880
|
+
if (existing?.isSymbolicLink()) {
|
|
881
|
+
const currentTarget = await readlink3(targetPath);
|
|
882
|
+
const resolvedCurrent = path9.resolve(path9.dirname(targetPath), currentTarget);
|
|
883
|
+
if (resolvedCurrent === path9.resolve(sourcePath)) {
|
|
884
|
+
return "skipped";
|
|
885
|
+
}
|
|
886
|
+
await unlink(targetPath);
|
|
887
|
+
} else if (existing) {
|
|
888
|
+
if (options.allowReplaceExisting) {
|
|
889
|
+
await rm3(targetPath, { force: true, recursive: true });
|
|
890
|
+
} else {
|
|
891
|
+
throw new Error(`Refusing to overwrite non-symlink target: ${targetPath}`);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
const linkTarget = path9.relative(path9.dirname(targetPath), sourcePath) || ".";
|
|
895
|
+
await symlink(linkTarget, targetPath, "dir");
|
|
896
|
+
return "created";
|
|
897
|
+
}
|
|
898
|
+
async function createSkillCopy(sourcePath, targetPath, options = {}) {
|
|
899
|
+
await mkdir5(path9.dirname(targetPath), { recursive: true });
|
|
900
|
+
const existing = await tryLstat(targetPath);
|
|
901
|
+
if (existing?.isSymbolicLink()) {
|
|
902
|
+
await unlink(targetPath);
|
|
903
|
+
} else if (existing) {
|
|
904
|
+
const marker2 = await readCopyMarker(targetPath);
|
|
905
|
+
if (!marker2) {
|
|
906
|
+
if (!options.allowReplaceExisting) {
|
|
907
|
+
throw new Error(`Refusing to overwrite unmanaged directory: ${targetPath}`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
await rm3(targetPath, { force: true, recursive: true });
|
|
911
|
+
}
|
|
912
|
+
await cp2(sourcePath, targetPath, { recursive: true });
|
|
913
|
+
const marker = { managedBy: "aweskill", sourcePath: path9.resolve(sourcePath) };
|
|
914
|
+
await writeFile2(path9.join(targetPath, COPY_MARKER), JSON.stringify(marker, null, 2), "utf8");
|
|
915
|
+
return "created";
|
|
916
|
+
}
|
|
917
|
+
async function removeManagedProjection(targetPath) {
|
|
918
|
+
const existing = await tryLstat(targetPath);
|
|
919
|
+
if (!existing) {
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
if (existing.isSymbolicLink()) {
|
|
923
|
+
await unlink(targetPath);
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
926
|
+
if (existing.isDirectory()) {
|
|
927
|
+
const marker = await readCopyMarker(targetPath);
|
|
928
|
+
if (marker) {
|
|
929
|
+
await rm3(targetPath, { force: true, recursive: true });
|
|
930
|
+
return true;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
async function listManagedSkillNames(skillsDir, centralSkillsDir) {
|
|
936
|
+
const result = /* @__PURE__ */ new Map();
|
|
937
|
+
try {
|
|
938
|
+
const entries = await readdir5(skillsDir, { withFileTypes: true });
|
|
939
|
+
for (const entry of entries) {
|
|
940
|
+
const targetPath = path9.join(skillsDir, entry.name);
|
|
941
|
+
if (entry.isSymbolicLink()) {
|
|
942
|
+
try {
|
|
943
|
+
const currentTarget = await readlink3(targetPath);
|
|
944
|
+
const resolvedCurrent = path9.resolve(path9.dirname(targetPath), currentTarget);
|
|
945
|
+
if (resolvedCurrent.startsWith(path9.resolve(centralSkillsDir))) {
|
|
946
|
+
result.set(entry.name, "symlink");
|
|
947
|
+
}
|
|
948
|
+
} catch {
|
|
949
|
+
result.set(entry.name, "symlink");
|
|
950
|
+
}
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (entry.isDirectory()) {
|
|
954
|
+
const marker = await readCopyMarker(targetPath);
|
|
955
|
+
if (marker) {
|
|
956
|
+
result.set(entry.name, "copy");
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
} catch {
|
|
961
|
+
return result;
|
|
962
|
+
}
|
|
963
|
+
return result;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/commands/check.ts
|
|
967
|
+
var DEFAULT_PREVIEW_COUNT = 5;
|
|
968
|
+
function getProjectDir(context, explicitProjectDir) {
|
|
969
|
+
return explicitProjectDir ?? context.cwd;
|
|
970
|
+
}
|
|
971
|
+
async function resolveAgentsForScope(context, requestedAgents, scope, projectDir) {
|
|
972
|
+
if (requestedAgents.length === 0 || requestedAgents.includes("all")) {
|
|
973
|
+
const detected = await detectInstalledAgents({
|
|
974
|
+
homeDir: context.homeDir,
|
|
975
|
+
projectDir: scope === "project" ? projectDir : void 0
|
|
976
|
+
});
|
|
977
|
+
return detected.length > 0 ? detected : listSupportedAgentIds();
|
|
978
|
+
}
|
|
979
|
+
return uniqueSorted(
|
|
980
|
+
requestedAgents.map((agent) => {
|
|
981
|
+
if (!isAgentId(agent)) {
|
|
982
|
+
throw new Error(`Unsupported agent: ${agent}`);
|
|
983
|
+
}
|
|
984
|
+
return agent;
|
|
985
|
+
})
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
function formatSkillBlockWithSummary(title, skills, verbose = false) {
|
|
989
|
+
if (skills.length === 0) {
|
|
990
|
+
return [`No skills found for ${title.replace(/:$/, "").toLowerCase()}.`];
|
|
991
|
+
}
|
|
992
|
+
const lines = [title];
|
|
993
|
+
const categories = [
|
|
994
|
+
{ title: " linked", marker: "\u2713", key: "linked" },
|
|
995
|
+
{ title: " duplicate", marker: "!", key: "duplicate" },
|
|
996
|
+
{ title: " new", marker: "+", key: "new" }
|
|
997
|
+
];
|
|
998
|
+
for (const category of categories) {
|
|
999
|
+
const entries = skills.filter((skill) => skill.category === category.key);
|
|
1000
|
+
lines.push(`${category.title}: ${entries.length}`);
|
|
1001
|
+
const preview = verbose ? entries : entries.slice(0, DEFAULT_PREVIEW_COUNT);
|
|
1002
|
+
for (const skill of preview) {
|
|
1003
|
+
lines.push(` ${category.marker} ${skill.name} ${skill.path}`);
|
|
1004
|
+
}
|
|
1005
|
+
if (!verbose && entries.length > preview.length) {
|
|
1006
|
+
lines.push(` ... and ${entries.length - preview.length} more (use --verbose to show all)`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return lines;
|
|
1010
|
+
}
|
|
1011
|
+
async function runCheck(context, options) {
|
|
1012
|
+
const lines = [];
|
|
1013
|
+
const centralSkillEntries = await listSkills(context.homeDir);
|
|
1014
|
+
const centralSkills = new Set(centralSkillEntries.map((skill) => skill.name));
|
|
1015
|
+
const projectDir = options.scope === "project" ? getProjectDir(context, options.projectDir) : void 0;
|
|
1016
|
+
const agents = await resolveAgentsForScope(context, options.agents, options.scope, projectDir);
|
|
1017
|
+
const updated = [];
|
|
1018
|
+
const skipped = [];
|
|
1019
|
+
let importedCount = 0;
|
|
1020
|
+
for (const agentId of agents) {
|
|
1021
|
+
const baseDir = options.scope === "global" ? context.homeDir : projectDir;
|
|
1022
|
+
const skillsDir = resolveAgentSkillsDir(agentId, options.scope, baseDir);
|
|
1023
|
+
const managed = await listManagedSkillNames(skillsDir, getAweskillPaths(context.homeDir).skillsDir);
|
|
1024
|
+
const skills = await listSkillEntriesInDirectory(skillsDir);
|
|
1025
|
+
const checked = skills.map((skill) => {
|
|
1026
|
+
let category = "new";
|
|
1027
|
+
if (managed.has(skill.name)) {
|
|
1028
|
+
category = "linked";
|
|
1029
|
+
} else if (centralSkills.has(skill.name)) {
|
|
1030
|
+
category = "duplicate";
|
|
1031
|
+
}
|
|
1032
|
+
return {
|
|
1033
|
+
name: skill.name,
|
|
1034
|
+
path: skill.path,
|
|
1035
|
+
category,
|
|
1036
|
+
hasSKILLMd: skill.hasSKILLMd
|
|
1037
|
+
};
|
|
1038
|
+
});
|
|
1039
|
+
if (options.update) {
|
|
1040
|
+
for (const skill of checked) {
|
|
1041
|
+
if (skill.category === "linked") {
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
if (!skill.hasSKILLMd) {
|
|
1045
|
+
context.write(`Warning: Skipping ${agentId}:${skill.name}; missing SKILL.md in ${skill.path}`);
|
|
1046
|
+
skipped.push(`${agentId}:${skill.name}`);
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
if (skill.category === "new") {
|
|
1050
|
+
await importSkill({
|
|
1051
|
+
homeDir: context.homeDir,
|
|
1052
|
+
sourcePath: skill.path,
|
|
1053
|
+
mode: "mv"
|
|
1054
|
+
});
|
|
1055
|
+
centralSkills.add(skill.name);
|
|
1056
|
+
importedCount += 1;
|
|
1057
|
+
}
|
|
1058
|
+
await createSkillSymlink(getSkillPath(context.homeDir, skill.name), skill.path, {
|
|
1059
|
+
allowReplaceExisting: true
|
|
1060
|
+
});
|
|
1061
|
+
updated.push(`${agentId}:${skill.name}`);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
const title = options.scope === "global" ? `Global skills for ${agentId}:` : `Project skills for ${agentId} (${projectDir}):`;
|
|
1065
|
+
lines.push("");
|
|
1066
|
+
lines.push(...formatSkillBlockWithSummary(title, checked, options.verbose));
|
|
1067
|
+
}
|
|
1068
|
+
if (options.update) {
|
|
1069
|
+
lines.push("");
|
|
1070
|
+
lines.push(`Updated ${updated.length} skills`);
|
|
1071
|
+
if (importedCount > 0) {
|
|
1072
|
+
lines.push(`Imported ${importedCount} new skills into the central repo`);
|
|
1073
|
+
}
|
|
1074
|
+
if (skipped.length > 0) {
|
|
1075
|
+
lines.push(`Skipped ${skipped.length} entries: ${skipped.join(", ")}`);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
context.write(lines.join("\n").trim());
|
|
1079
|
+
return { agents, updated, importedCount, skipped };
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// src/commands/disable.ts
|
|
1083
|
+
function getProjectDir2(context, explicitProjectDir) {
|
|
1084
|
+
return explicitProjectDir ?? context.cwd;
|
|
1085
|
+
}
|
|
1086
|
+
async function resolveAgentsForScope2(context, requestedAgents, scope, projectDir) {
|
|
1087
|
+
if (requestedAgents.length === 0 || requestedAgents.includes("all")) {
|
|
1088
|
+
const detected = await detectInstalledAgents({
|
|
1089
|
+
homeDir: context.homeDir,
|
|
1090
|
+
projectDir: scope === "project" ? projectDir : void 0
|
|
1091
|
+
});
|
|
1092
|
+
return detected.length > 0 ? detected : listSupportedAgentIds();
|
|
1093
|
+
}
|
|
1094
|
+
return uniqueSorted(
|
|
1095
|
+
requestedAgents.map((agent) => {
|
|
1096
|
+
if (!isAgentId(agent)) {
|
|
1097
|
+
throw new Error(`Unsupported agent: ${agent}`);
|
|
1098
|
+
}
|
|
1099
|
+
return agent;
|
|
1100
|
+
})
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
async function resolveSkillNames(context, type, names) {
|
|
1104
|
+
const normalizedNames = uniqueSorted(splitCommaValues(names).map((name) => sanitizeName(name)));
|
|
1105
|
+
if (normalizedNames.includes("all")) {
|
|
1106
|
+
if (type === "bundle") {
|
|
1107
|
+
const bundles = await listBundles(context.homeDir);
|
|
1108
|
+
return uniqueSorted(bundles.flatMap((bundle) => bundle.skills));
|
|
1109
|
+
}
|
|
1110
|
+
const { skillsDir: centralSkillsDir } = getAweskillPaths(context.homeDir);
|
|
1111
|
+
const detected = await detectInstalledAgents({
|
|
1112
|
+
homeDir: context.homeDir
|
|
1113
|
+
});
|
|
1114
|
+
const globalManaged = await Promise.all(
|
|
1115
|
+
detected.map(async (agentId) => {
|
|
1116
|
+
const agentSkillsDir = resolveAgentSkillsDir(agentId, "global", context.homeDir);
|
|
1117
|
+
return listManagedSkillNames(agentSkillsDir, centralSkillsDir);
|
|
1118
|
+
})
|
|
1119
|
+
);
|
|
1120
|
+
const managedSkillNames = uniqueSorted(
|
|
1121
|
+
globalManaged.flatMap((managed) => [...managed.keys()])
|
|
1122
|
+
);
|
|
1123
|
+
return managedSkillNames;
|
|
1124
|
+
}
|
|
1125
|
+
if (type === "bundle") {
|
|
1126
|
+
const bundles = await Promise.all(normalizedNames.map((bundleName) => readBundle(context.homeDir, bundleName)));
|
|
1127
|
+
return uniqueSorted(bundles.flatMap((bundle) => bundle.skills));
|
|
1128
|
+
}
|
|
1129
|
+
const resolvedNames = [];
|
|
1130
|
+
for (const normalizedName of normalizedNames) {
|
|
1131
|
+
if (!await skillExists(context.homeDir, normalizedName)) {
|
|
1132
|
+
resolvedNames.push(normalizedName);
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
resolvedNames.push(normalizedName);
|
|
1136
|
+
}
|
|
1137
|
+
return uniqueSorted(resolvedNames);
|
|
1138
|
+
}
|
|
1139
|
+
async function bundlesWithCoEnabledSiblings(options) {
|
|
1140
|
+
const { skillsDir: centralSkillsDir } = getAweskillPaths(options.homeDir);
|
|
1141
|
+
const bundles = await listBundles(options.homeDir);
|
|
1142
|
+
const normalized = sanitizeName(options.skillName);
|
|
1143
|
+
const hit = /* @__PURE__ */ new Set();
|
|
1144
|
+
for (const bundle of bundles) {
|
|
1145
|
+
if (!bundle.skills.includes(normalized)) {
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
const siblings = bundle.skills.filter((s) => s !== normalized);
|
|
1149
|
+
if (siblings.length === 0) {
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
for (const agentId of options.agents) {
|
|
1153
|
+
const agentSkillsDir = resolveAgentSkillsDir(agentId, options.scope, options.baseDir);
|
|
1154
|
+
const managed = await listManagedSkillNames(agentSkillsDir, centralSkillsDir);
|
|
1155
|
+
if (siblings.some((s) => managed.has(s))) {
|
|
1156
|
+
hit.add(bundle.name);
|
|
1157
|
+
break;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return [...hit].sort();
|
|
1162
|
+
}
|
|
1163
|
+
async function runDisable(context, options) {
|
|
1164
|
+
const projectDir = options.scope === "project" ? getProjectDir2(context, options.projectDir) : void 0;
|
|
1165
|
+
const agents = await resolveAgentsForScope2(context, options.agents, options.scope, projectDir);
|
|
1166
|
+
const baseDir = options.scope === "global" ? context.homeDir : projectDir ?? context.cwd;
|
|
1167
|
+
const skillNames = await resolveSkillNames(context, options.type, options.name);
|
|
1168
|
+
if (splitCommaValues(options.name).map((name) => sanitizeName(name)).includes("all") && options.type === "skill") {
|
|
1169
|
+
const { skillsDir: centralSkillsDir } = getAweskillPaths(context.homeDir);
|
|
1170
|
+
const scopedManaged = await Promise.all(
|
|
1171
|
+
agents.map(async (agentId) => {
|
|
1172
|
+
const agentSkillsDir = resolveAgentSkillsDir(agentId, options.scope, baseDir);
|
|
1173
|
+
return listManagedSkillNames(agentSkillsDir, centralSkillsDir);
|
|
1174
|
+
})
|
|
1175
|
+
);
|
|
1176
|
+
const managedSkillNames = uniqueSorted(
|
|
1177
|
+
scopedManaged.flatMap((managed) => [...managed.keys()])
|
|
1178
|
+
);
|
|
1179
|
+
skillNames.splice(0, skillNames.length, ...managedSkillNames);
|
|
1180
|
+
}
|
|
1181
|
+
if (options.type === "skill" && skillNames.length === 1 && !options.force) {
|
|
1182
|
+
const bundleNames = await bundlesWithCoEnabledSiblings({
|
|
1183
|
+
homeDir: context.homeDir,
|
|
1184
|
+
skillName: skillNames[0],
|
|
1185
|
+
agents,
|
|
1186
|
+
scope: options.scope,
|
|
1187
|
+
baseDir
|
|
1188
|
+
});
|
|
1189
|
+
if (bundleNames.length > 0) {
|
|
1190
|
+
throw new Error(
|
|
1191
|
+
`Skill "${skillNames[0]}" is listed in bundle(s): ${bundleNames.join(", ")}. Other skills from those bundle(s) are still enabled in this scope. Use --force to remove only this skill's projection, or run "aweskill agent remove bundle <name>" to drop the whole bundle.`
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
const removed = [];
|
|
1196
|
+
for (const agentId of agents) {
|
|
1197
|
+
const skillsDir = resolveAgentSkillsDir(agentId, options.scope, baseDir);
|
|
1198
|
+
for (const skillName of skillNames) {
|
|
1199
|
+
const targetPath = `${skillsDir}/${skillName}`;
|
|
1200
|
+
const wasRemoved = await removeManagedProjection(targetPath);
|
|
1201
|
+
if (wasRemoved) {
|
|
1202
|
+
removed.push(`${agentId}:${skillName}`);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
const scopeLabel = options.scope === "global" ? "global scope" : projectDir ?? context.cwd;
|
|
1207
|
+
const targetLabel = uniqueSorted(splitCommaValues(options.name).map((name) => sanitizeName(name))).join(", ");
|
|
1208
|
+
context.write(`Disabled ${options.type} ${targetLabel} for ${agents.join(", ")} in ${scopeLabel}${removed.length > 0 ? ` (${removed.length} removed)` : ""}`);
|
|
1209
|
+
return { agents, skillNames, removed };
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// src/commands/enable.ts
|
|
1213
|
+
import { mkdir as mkdir6 } from "fs/promises";
|
|
1214
|
+
function getProjectDir3(context, explicitProjectDir) {
|
|
1215
|
+
return explicitProjectDir ?? context.cwd;
|
|
1216
|
+
}
|
|
1217
|
+
async function resolveAgentsForScope3(context, requestedAgents, scope, projectDir) {
|
|
1218
|
+
if (requestedAgents.length === 0 || requestedAgents.includes("all")) {
|
|
1219
|
+
const detected = await detectInstalledAgents({
|
|
1220
|
+
homeDir: context.homeDir,
|
|
1221
|
+
projectDir: scope === "project" ? projectDir : void 0
|
|
1222
|
+
});
|
|
1223
|
+
return detected.length > 0 ? detected : listSupportedAgentIds();
|
|
1224
|
+
}
|
|
1225
|
+
return uniqueSorted(
|
|
1226
|
+
requestedAgents.map((agent) => {
|
|
1227
|
+
if (!isAgentId(agent)) {
|
|
1228
|
+
throw new Error(`Unsupported agent: ${agent}`);
|
|
1229
|
+
}
|
|
1230
|
+
return agent;
|
|
1231
|
+
})
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
async function resolveSkillNames2(context, type, names) {
|
|
1235
|
+
const normalizedNames = uniqueSorted(splitCommaValues(names).map((name) => sanitizeName(name)));
|
|
1236
|
+
if (normalizedNames.includes("all")) {
|
|
1237
|
+
if (type === "bundle") {
|
|
1238
|
+
const bundles = await listBundles(context.homeDir);
|
|
1239
|
+
const skillNames = uniqueSorted(bundles.flatMap((bundle) => bundle.skills));
|
|
1240
|
+
for (const skillName of skillNames) {
|
|
1241
|
+
if (!await skillExists(context.homeDir, skillName)) {
|
|
1242
|
+
throw new Error(`A bundle in "all" references unknown skill: ${skillName}`);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return skillNames;
|
|
1246
|
+
}
|
|
1247
|
+
const skills = await listSkills(context.homeDir);
|
|
1248
|
+
return skills.map((skill) => skill.name);
|
|
1249
|
+
}
|
|
1250
|
+
if (type === "bundle") {
|
|
1251
|
+
const bundles = await Promise.all(normalizedNames.map((bundleName) => readBundle(context.homeDir, bundleName)));
|
|
1252
|
+
const skillNames = uniqueSorted(bundles.flatMap((bundle) => bundle.skills));
|
|
1253
|
+
for (const skillName of skillNames) {
|
|
1254
|
+
if (!await skillExists(context.homeDir, skillName)) {
|
|
1255
|
+
const bundle = bundles.find((candidate) => candidate.skills.includes(skillName));
|
|
1256
|
+
throw new Error(`Bundle ${bundle?.name ?? "(unknown)"} references unknown skill: ${skillName}`);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return skillNames;
|
|
1260
|
+
}
|
|
1261
|
+
for (const normalizedName of normalizedNames) {
|
|
1262
|
+
if (!await skillExists(context.homeDir, normalizedName)) {
|
|
1263
|
+
throw new Error(`Unknown skill: ${normalizedName}`);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
return normalizedNames;
|
|
1267
|
+
}
|
|
1268
|
+
async function runEnable(context, options) {
|
|
1269
|
+
const projectDir = options.scope === "project" ? getProjectDir3(context, options.projectDir) : void 0;
|
|
1270
|
+
const agents = await resolveAgentsForScope3(context, options.agents, options.scope, projectDir);
|
|
1271
|
+
const skillNames = await resolveSkillNames2(context, options.type, options.name);
|
|
1272
|
+
const baseDir = options.scope === "global" ? context.homeDir : projectDir ?? context.cwd;
|
|
1273
|
+
for (const agentId of agents) {
|
|
1274
|
+
const skillsDir = resolveAgentSkillsDir(agentId, options.scope, baseDir);
|
|
1275
|
+
await mkdir6(skillsDir, { recursive: true });
|
|
1276
|
+
for (const skillName of skillNames) {
|
|
1277
|
+
const sourcePath = getSkillPath(context.homeDir, skillName);
|
|
1278
|
+
const targetPath = `${skillsDir}/${skillName}`;
|
|
1279
|
+
await assertProjectionTargetSafe(getProjectionMode(agentId), sourcePath, targetPath);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
const created = [];
|
|
1283
|
+
for (const agentId of agents) {
|
|
1284
|
+
const skillsDir = resolveAgentSkillsDir(agentId, options.scope, baseDir);
|
|
1285
|
+
const mode = getProjectionMode(agentId);
|
|
1286
|
+
for (const skillName of skillNames) {
|
|
1287
|
+
const sourcePath = getSkillPath(context.homeDir, skillName);
|
|
1288
|
+
const targetPath = `${skillsDir}/${skillName}`;
|
|
1289
|
+
const result = mode === "symlink" ? await createSkillSymlink(sourcePath, targetPath) : await createSkillCopy(sourcePath, targetPath);
|
|
1290
|
+
if (result === "created") {
|
|
1291
|
+
created.push(`${agentId}:${skillName}`);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
const scopeLabel = options.scope === "global" ? "global scope" : projectDir ?? context.cwd;
|
|
1296
|
+
const targetLabel = uniqueSorted(splitCommaValues(options.name).map((name) => sanitizeName(name))).join(", ");
|
|
1297
|
+
context.write(`Enabled ${options.type} ${targetLabel} for ${agents.join(", ")} in ${scopeLabel}${created.length > 0 ? ` (${created.length} created)` : ""}`);
|
|
1298
|
+
return { agents, skillNames, created };
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// src/commands/scan.ts
|
|
1302
|
+
function groupLabel(candidate) {
|
|
1303
|
+
return candidate.scope === "global" ? `Global scanned skills for ${candidate.agentId}:` : `Project scanned skills for ${candidate.agentId} (${candidate.projectDir}):`;
|
|
1304
|
+
}
|
|
1305
|
+
function formatScanSummary(candidates, verbose = false) {
|
|
1306
|
+
if (candidates.length === 0) {
|
|
1307
|
+
return "(no scanned skills)";
|
|
1308
|
+
}
|
|
1309
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1310
|
+
for (const candidate of candidates) {
|
|
1311
|
+
const key = `${candidate.scope}:${candidate.agentId}:${candidate.projectDir ?? ""}`;
|
|
1312
|
+
const bucket = groups.get(key) ?? [];
|
|
1313
|
+
bucket.push(candidate);
|
|
1314
|
+
groups.set(key, bucket);
|
|
1315
|
+
}
|
|
1316
|
+
const lines = ["Scanned skills:"];
|
|
1317
|
+
for (const [, groupedCandidates] of [...groups.entries()].sort((left, right) => left[0].localeCompare(right[0]))) {
|
|
1318
|
+
const sorted = [...groupedCandidates].sort((left, right) => left.name.localeCompare(right.name));
|
|
1319
|
+
lines.push(` ${groupLabel(sorted[0])} ${sorted.length}`);
|
|
1320
|
+
if (verbose) {
|
|
1321
|
+
for (const candidate of sorted) {
|
|
1322
|
+
lines.push(` \u2713 ${candidate.name} ${candidate.path}`);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
return lines.join("\n");
|
|
1327
|
+
}
|
|
1328
|
+
async function runScan(context, options) {
|
|
1329
|
+
const candidates = await scanSkills({
|
|
1330
|
+
homeDir: context.homeDir,
|
|
1331
|
+
projectDirs: [context.cwd]
|
|
1332
|
+
});
|
|
1333
|
+
context.write(formatScanSummary(candidates, options.verbose));
|
|
1334
|
+
if (options.add) {
|
|
1335
|
+
const result = await importScannedSkills({
|
|
1336
|
+
homeDir: context.homeDir,
|
|
1337
|
+
candidates,
|
|
1338
|
+
mode: options.mode ?? "cp",
|
|
1339
|
+
override: options.override
|
|
1340
|
+
});
|
|
1341
|
+
for (const warning of result.warnings) {
|
|
1342
|
+
context.write(`Warning: ${warning}`);
|
|
1343
|
+
}
|
|
1344
|
+
for (const error of result.errors) {
|
|
1345
|
+
context.error(`Error: ${error}`);
|
|
1346
|
+
}
|
|
1347
|
+
context.write(`Imported ${result.imported.length} skills`);
|
|
1348
|
+
if (result.overwritten.length > 0) {
|
|
1349
|
+
context.write(`Overwritten ${result.overwritten.length} existing skills: ${result.overwritten.join(", ")}`);
|
|
1350
|
+
}
|
|
1351
|
+
if (result.skipped.length > 0) {
|
|
1352
|
+
context.write(`Skipped ${result.skipped.length} existing skills (use --override to overwrite): ${result.skipped.join(", ")}`);
|
|
1353
|
+
}
|
|
1354
|
+
if (result.missingSources > 0) {
|
|
1355
|
+
context.write(`Missing source files: ${result.missingSources}`);
|
|
1356
|
+
}
|
|
1357
|
+
return { candidates, ...result };
|
|
1358
|
+
}
|
|
1359
|
+
return candidates;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// src/commands/init.ts
|
|
1363
|
+
async function runInit(context, options) {
|
|
1364
|
+
await ensureHomeLayout(context.homeDir);
|
|
1365
|
+
context.write(`Initialized ${context.homeDir}/.aweskill`);
|
|
1366
|
+
if (options.scan) {
|
|
1367
|
+
const candidates = await scanSkills({ homeDir: context.homeDir, projectDirs: [context.cwd] });
|
|
1368
|
+
context.write(formatScanSummary(candidates, options.verbose));
|
|
1369
|
+
return candidates;
|
|
1370
|
+
}
|
|
1371
|
+
return [];
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// src/commands/list.ts
|
|
1375
|
+
var DEFAULT_PREVIEW_COUNT2 = 5;
|
|
1376
|
+
async function runListSkills(context, options = {}) {
|
|
1377
|
+
const skills = await listSkills(context.homeDir);
|
|
1378
|
+
if (skills.length === 0) {
|
|
1379
|
+
context.write("No skills found in central repo.");
|
|
1380
|
+
return skills;
|
|
1381
|
+
}
|
|
1382
|
+
const preview = options.verbose ? skills : skills.slice(0, DEFAULT_PREVIEW_COUNT2);
|
|
1383
|
+
const lines = [`Skills in central repo: ${skills.length} total`];
|
|
1384
|
+
if (!options.verbose && skills.length > preview.length) {
|
|
1385
|
+
lines.push(`Showing first ${preview.length} skills (use --verbose to show all)`);
|
|
1386
|
+
}
|
|
1387
|
+
for (const skill of preview) {
|
|
1388
|
+
const marker = skill.hasSKILLMd ? "\u2713" : "!";
|
|
1389
|
+
lines.push(` ${marker} ${skill.name} ${skill.path}`);
|
|
1390
|
+
}
|
|
1391
|
+
context.write(lines.join("\n"));
|
|
1392
|
+
return skills;
|
|
1393
|
+
}
|
|
1394
|
+
function formatBundleLines(title, bundles, verbose) {
|
|
1395
|
+
if (bundles.length === 0) {
|
|
1396
|
+
return [title, "(none)"];
|
|
1397
|
+
}
|
|
1398
|
+
const preview = verbose ? bundles : bundles.slice(0, DEFAULT_PREVIEW_COUNT2);
|
|
1399
|
+
const lines = [`${title}: ${bundles.length} total`];
|
|
1400
|
+
if (!verbose && bundles.length > preview.length) {
|
|
1401
|
+
lines.push(`Showing first ${preview.length} bundles (use --verbose to show all)`);
|
|
1402
|
+
}
|
|
1403
|
+
for (const bundle of preview) {
|
|
1404
|
+
const skillsPreview = verbose ? bundle.skills : bundle.skills.slice(0, DEFAULT_PREVIEW_COUNT2);
|
|
1405
|
+
const suffix = !verbose && bundle.skills.length > skillsPreview.length ? `, ... (+${bundle.skills.length - skillsPreview.length} more)` : "";
|
|
1406
|
+
lines.push(` - ${bundle.name}: ${bundle.skills.length} skills${skillsPreview.length > 0 ? ` -> ${skillsPreview.join(", ")}${suffix}` : " -> (empty)"}`);
|
|
1407
|
+
}
|
|
1408
|
+
return lines;
|
|
1409
|
+
}
|
|
1410
|
+
async function runListBundles(context, options = {}) {
|
|
1411
|
+
const bundles = await listBundles(context.homeDir);
|
|
1412
|
+
context.write(formatBundleLines("Bundles in central repo", bundles, options.verbose).join("\n"));
|
|
1413
|
+
return bundles;
|
|
1414
|
+
}
|
|
1415
|
+
async function runListTemplateBundles(context, options = {}) {
|
|
1416
|
+
const templateBundlesDir = await getTemplateBundlesDir();
|
|
1417
|
+
const bundles = await listBundlesInDirectory(templateBundlesDir);
|
|
1418
|
+
context.write(formatBundleLines("Bundle templates", bundles, options.verbose).join("\n"));
|
|
1419
|
+
return bundles;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// src/commands/recover.ts
|
|
1423
|
+
import { cp as cp3, mkdir as mkdir7, readlink as readlink4, unlink as unlink2 } from "fs/promises";
|
|
1424
|
+
import path10 from "path";
|
|
1425
|
+
function getProjectDir4(context, explicitProjectDir) {
|
|
1426
|
+
return explicitProjectDir ?? context.cwd;
|
|
1427
|
+
}
|
|
1428
|
+
async function resolveAgentsForScope4(context, requestedAgents, scope, projectDir) {
|
|
1429
|
+
if (requestedAgents.length === 0 || requestedAgents.includes("all")) {
|
|
1430
|
+
const detected = await detectInstalledAgents({
|
|
1431
|
+
homeDir: context.homeDir,
|
|
1432
|
+
projectDir: scope === "project" ? projectDir : void 0
|
|
1433
|
+
});
|
|
1434
|
+
return detected.length > 0 ? detected : listSupportedAgentIds();
|
|
1435
|
+
}
|
|
1436
|
+
return uniqueSorted(
|
|
1437
|
+
requestedAgents.map((agent) => {
|
|
1438
|
+
if (!isAgentId(agent)) {
|
|
1439
|
+
throw new Error(`Unsupported agent: ${agent}`);
|
|
1440
|
+
}
|
|
1441
|
+
return agent;
|
|
1442
|
+
})
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
async function runRecover(context, options) {
|
|
1446
|
+
const projectDir = options.scope === "project" ? getProjectDir4(context, options.projectDir) : void 0;
|
|
1447
|
+
const agents = await resolveAgentsForScope4(context, options.agents, options.scope, projectDir);
|
|
1448
|
+
const { skillsDir: centralSkillsDir } = getAweskillPaths(context.homeDir);
|
|
1449
|
+
const baseDir = options.scope === "global" ? context.homeDir : projectDir ?? context.cwd;
|
|
1450
|
+
const recovered = [];
|
|
1451
|
+
for (const agentId of agents) {
|
|
1452
|
+
const agentSkillsDir = resolveAgentSkillsDir(agentId, options.scope, baseDir);
|
|
1453
|
+
const managed = await listManagedSkillNames(agentSkillsDir, centralSkillsDir);
|
|
1454
|
+
for (const [skillName, mode] of managed) {
|
|
1455
|
+
if (mode !== "symlink") {
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
const targetPath = path10.join(agentSkillsDir, skillName);
|
|
1459
|
+
const sourcePath = path10.join(centralSkillsDir, skillName);
|
|
1460
|
+
const currentTarget = await readlink4(targetPath);
|
|
1461
|
+
const resolvedCurrent = path10.resolve(path10.dirname(targetPath), currentTarget);
|
|
1462
|
+
if (resolvedCurrent !== path10.resolve(sourcePath)) {
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
await unlink2(targetPath);
|
|
1466
|
+
await mkdir7(path10.dirname(targetPath), { recursive: true });
|
|
1467
|
+
await cp3(sourcePath, targetPath, { recursive: true });
|
|
1468
|
+
recovered.push(`${agentId}:${skillName}`);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
const scopeLabel = options.scope === "global" ? "global scope" : projectDir ?? context.cwd;
|
|
1472
|
+
context.write(`Recovered ${recovered.length} skill projection(s) in ${scopeLabel}${recovered.length > 0 ? `: ${recovered.join(", ")}` : ""}`);
|
|
1473
|
+
return { agents, recovered };
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// src/lib/references.ts
|
|
1477
|
+
import { rm as rm4 } from "fs/promises";
|
|
1478
|
+
import path11 from "path";
|
|
1479
|
+
async function findSkillReferences(options) {
|
|
1480
|
+
const normalizedSkill = sanitizeName(options.skillName);
|
|
1481
|
+
const { skillsDir } = getAweskillPaths(options.homeDir);
|
|
1482
|
+
const bundles = (await listBundles(options.homeDir)).filter((bundle) => bundle.skills.includes(normalizedSkill)).map((bundle) => bundle.name);
|
|
1483
|
+
const agentProjections = [];
|
|
1484
|
+
const baseDirs = [
|
|
1485
|
+
{ scope: "global", dir: options.homeDir }
|
|
1486
|
+
];
|
|
1487
|
+
if (options.projectDir) {
|
|
1488
|
+
baseDirs.push({ scope: "project", dir: options.projectDir });
|
|
1489
|
+
}
|
|
1490
|
+
for (const { scope, dir } of baseDirs) {
|
|
1491
|
+
for (const agent of listSupportedAgents()) {
|
|
1492
|
+
const agentSkillsDir = resolveAgentSkillsDir(agent.id, scope, dir);
|
|
1493
|
+
const managed = await listManagedSkillNames(agentSkillsDir, skillsDir);
|
|
1494
|
+
if (managed.has(normalizedSkill)) {
|
|
1495
|
+
agentProjections.push(`${agent.id}(${scope}):${normalizedSkill}`);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
return { bundles, agentProjections };
|
|
1500
|
+
}
|
|
1501
|
+
async function removeSkillWithReferences(options) {
|
|
1502
|
+
const normalizedSkill = sanitizeName(options.skillName);
|
|
1503
|
+
const { skillsDir } = getAweskillPaths(options.homeDir);
|
|
1504
|
+
const bundles = await listBundles(options.homeDir);
|
|
1505
|
+
for (const bundle of bundles) {
|
|
1506
|
+
if (!bundle.skills.includes(normalizedSkill)) {
|
|
1507
|
+
continue;
|
|
1508
|
+
}
|
|
1509
|
+
bundle.skills = bundle.skills.filter((skill) => skill !== normalizedSkill);
|
|
1510
|
+
await writeBundle(options.homeDir, bundle);
|
|
1511
|
+
}
|
|
1512
|
+
const baseDirs = [
|
|
1513
|
+
{ scope: "global", dir: options.homeDir }
|
|
1514
|
+
];
|
|
1515
|
+
if (options.projectDir) {
|
|
1516
|
+
baseDirs.push({ scope: "project", dir: options.projectDir });
|
|
1517
|
+
}
|
|
1518
|
+
for (const { scope, dir } of baseDirs) {
|
|
1519
|
+
for (const agent of listSupportedAgents()) {
|
|
1520
|
+
const agentSkillsDir = resolveAgentSkillsDir(agent.id, scope, dir);
|
|
1521
|
+
const managed = await listManagedSkillNames(agentSkillsDir, skillsDir);
|
|
1522
|
+
if (managed.has(normalizedSkill)) {
|
|
1523
|
+
await removeManagedProjection(path11.join(agentSkillsDir, normalizedSkill));
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
await rm4(getSkillPath(options.homeDir, normalizedSkill), { force: true, recursive: true });
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// src/commands/remove.ts
|
|
1531
|
+
async function runRemove(context, options) {
|
|
1532
|
+
const projectDir = options.projectDir ?? context.cwd;
|
|
1533
|
+
const references = await findSkillReferences({
|
|
1534
|
+
homeDir: context.homeDir,
|
|
1535
|
+
skillName: options.skillName,
|
|
1536
|
+
projectDir
|
|
1537
|
+
});
|
|
1538
|
+
const referenceCount = references.bundles.length + references.agentProjections.length;
|
|
1539
|
+
if (referenceCount > 0 && !options.force) {
|
|
1540
|
+
throw new Error(
|
|
1541
|
+
`Skill ${options.skillName} is still referenced: ${[
|
|
1542
|
+
...references.bundles,
|
|
1543
|
+
...references.agentProjections
|
|
1544
|
+
].join(", ")}`
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
await removeSkillWithReferences({
|
|
1548
|
+
homeDir: context.homeDir,
|
|
1549
|
+
skillName: options.skillName,
|
|
1550
|
+
projectDir
|
|
1551
|
+
});
|
|
1552
|
+
context.write(`Removed ${options.skillName}`);
|
|
1553
|
+
return references;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// src/commands/restore.ts
|
|
1557
|
+
import { cp as cp4, mkdir as mkdir8, rm as rm5 } from "fs/promises";
|
|
1558
|
+
import path12 from "path";
|
|
1559
|
+
async function runRestore(context, options) {
|
|
1560
|
+
const { tempDir, extractedSkillsDir } = await extractSkillsArchive(options.archivePath);
|
|
1561
|
+
try {
|
|
1562
|
+
const extracted = await listSkillEntriesInDirectory(extractedSkillsDir);
|
|
1563
|
+
if (extracted.length === 0) {
|
|
1564
|
+
throw new Error(`Archive does not contain a skills/ directory: ${options.archivePath}`);
|
|
1565
|
+
}
|
|
1566
|
+
const { skillsDir } = getAweskillPaths(context.homeDir);
|
|
1567
|
+
const current = await listSkillEntriesInDirectory(skillsDir);
|
|
1568
|
+
const currentNames = new Set(current.map((entry) => entry.name));
|
|
1569
|
+
const conflicts = extracted.map((entry) => entry.name).filter((name) => currentNames.has(name));
|
|
1570
|
+
if (conflicts.length > 0 && !options.override) {
|
|
1571
|
+
throw new Error(`Restore would overwrite existing skills: ${conflicts.join(", ")}. Use --override to replace them.`);
|
|
1572
|
+
}
|
|
1573
|
+
const backupArchivePath = await createSkillsBackupArchive(context.homeDir);
|
|
1574
|
+
if (options.override) {
|
|
1575
|
+
await rm5(skillsDir, { recursive: true, force: true });
|
|
1576
|
+
await mkdir8(path12.dirname(skillsDir), { recursive: true });
|
|
1577
|
+
await cp4(extractedSkillsDir, skillsDir, { recursive: true });
|
|
1578
|
+
} else {
|
|
1579
|
+
await mkdir8(skillsDir, { recursive: true });
|
|
1580
|
+
for (const entry of extracted) {
|
|
1581
|
+
await cp4(entry.path, path12.join(skillsDir, entry.name), { recursive: true });
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
context.write(`Restored ${extracted.length} skills from ${options.archivePath}`);
|
|
1585
|
+
context.write(`Backed up current skills to ${backupArchivePath}`);
|
|
1586
|
+
return { restored: extracted.map((entry) => entry.name), backupArchivePath };
|
|
1587
|
+
} finally {
|
|
1588
|
+
await rm5(tempDir, { recursive: true, force: true });
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// src/lib/rmdup.ts
|
|
1593
|
+
import { mkdir as mkdir9, readdir as readdir7, rename as rename2, rm as rm6 } from "fs/promises";
|
|
1594
|
+
import path13 from "path";
|
|
1595
|
+
var NUMERIC_SUFFIX_PATTERN = /^(.*?)-(\d+(?:\.\d+)*)$/;
|
|
1596
|
+
function parseSkillName(name) {
|
|
1597
|
+
const match = name.match(NUMERIC_SUFFIX_PATTERN);
|
|
1598
|
+
if (!match) {
|
|
1599
|
+
return {
|
|
1600
|
+
baseName: name,
|
|
1601
|
+
hasNumericSuffix: false
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
return {
|
|
1605
|
+
baseName: match[1],
|
|
1606
|
+
hasNumericSuffix: true,
|
|
1607
|
+
numericKey: match[2].split(".").map((part) => Number.parseInt(part, 10))
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
function compareNumericKeys(left, right) {
|
|
1611
|
+
const length = Math.max(left.length, right.length);
|
|
1612
|
+
for (let index = 0; index < length; index += 1) {
|
|
1613
|
+
const leftValue = left[index] ?? 0;
|
|
1614
|
+
const rightValue = right[index] ?? 0;
|
|
1615
|
+
if (leftValue !== rightValue) {
|
|
1616
|
+
return leftValue - rightValue;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
return 0;
|
|
1620
|
+
}
|
|
1621
|
+
function choosePreferredSkill(entries) {
|
|
1622
|
+
return [...entries].sort((left, right) => {
|
|
1623
|
+
const leftParsed = parseSkillName(left.name);
|
|
1624
|
+
const rightParsed = parseSkillName(right.name);
|
|
1625
|
+
if (leftParsed.hasNumericSuffix && !rightParsed.hasNumericSuffix) {
|
|
1626
|
+
return -1;
|
|
1627
|
+
}
|
|
1628
|
+
if (!leftParsed.hasNumericSuffix && rightParsed.hasNumericSuffix) {
|
|
1629
|
+
return 1;
|
|
1630
|
+
}
|
|
1631
|
+
if (leftParsed.numericKey && rightParsed.numericKey) {
|
|
1632
|
+
const comparison = compareNumericKeys(rightParsed.numericKey, leftParsed.numericKey);
|
|
1633
|
+
if (comparison !== 0) {
|
|
1634
|
+
return comparison;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
return left.name.localeCompare(right.name);
|
|
1638
|
+
})[0];
|
|
1639
|
+
}
|
|
1640
|
+
async function findDuplicateSkills(homeDir) {
|
|
1641
|
+
const skills = await listSkills(homeDir);
|
|
1642
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1643
|
+
for (const skill of skills) {
|
|
1644
|
+
const parsed = parseSkillName(skill.name);
|
|
1645
|
+
const bucket = grouped.get(parsed.baseName) ?? [];
|
|
1646
|
+
bucket.push(skill);
|
|
1647
|
+
grouped.set(parsed.baseName, bucket);
|
|
1648
|
+
}
|
|
1649
|
+
const duplicates = [];
|
|
1650
|
+
for (const [baseName, entries] of grouped) {
|
|
1651
|
+
if (entries.length < 2) {
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
const kept = choosePreferredSkill(entries);
|
|
1655
|
+
const removed = entries.filter((entry) => entry.path !== kept.path).sort((left, right) => left.name.localeCompare(right.name));
|
|
1656
|
+
if (removed.length > 0) {
|
|
1657
|
+
duplicates.push({ baseName, kept, removed });
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
return duplicates.sort((left, right) => left.baseName.localeCompare(right.baseName));
|
|
1661
|
+
}
|
|
1662
|
+
async function removeDuplicateSkills(homeDir, duplicates, options = {}) {
|
|
1663
|
+
const paths = getAweskillPaths(homeDir);
|
|
1664
|
+
await mkdir9(paths.dupSkillsDir, { recursive: true });
|
|
1665
|
+
const moved = [];
|
|
1666
|
+
const deleted = [];
|
|
1667
|
+
for (const group of duplicates) {
|
|
1668
|
+
for (const skill of group.removed) {
|
|
1669
|
+
if (options.delete) {
|
|
1670
|
+
await rm6(skill.path, { recursive: true, force: true });
|
|
1671
|
+
deleted.push(skill.name);
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
const targetPath = await nextAvailableDupPath(paths.dupSkillsDir, skill.name);
|
|
1675
|
+
await rename2(skill.path, targetPath);
|
|
1676
|
+
moved.push(`${skill.name} -> ${targetPath}`);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
return { moved, deleted };
|
|
1680
|
+
}
|
|
1681
|
+
async function nextAvailableDupPath(dupSkillsDir, skillName) {
|
|
1682
|
+
const entries = new Set(await readdir7(dupSkillsDir).catch(() => []));
|
|
1683
|
+
if (!entries.has(skillName)) {
|
|
1684
|
+
return path13.join(dupSkillsDir, skillName);
|
|
1685
|
+
}
|
|
1686
|
+
let index = 1;
|
|
1687
|
+
while (entries.has(`${skillName}-${index}`)) {
|
|
1688
|
+
index += 1;
|
|
1689
|
+
}
|
|
1690
|
+
return path13.join(dupSkillsDir, `${skillName}-${index}`);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// src/commands/rmdup.ts
|
|
1694
|
+
async function runRmdup(context, options) {
|
|
1695
|
+
if (options.delete && !options.remove) {
|
|
1696
|
+
throw new Error("--delete requires --remove");
|
|
1697
|
+
}
|
|
1698
|
+
const duplicates = await findDuplicateSkills(context.homeDir);
|
|
1699
|
+
if (duplicates.length === 0) {
|
|
1700
|
+
context.write("No duplicate skills found in the central repo.");
|
|
1701
|
+
return { duplicates, moved: [], deleted: [] };
|
|
1702
|
+
}
|
|
1703
|
+
const lines = ["Duplicate skill groups in central repo:"];
|
|
1704
|
+
for (const group of duplicates) {
|
|
1705
|
+
lines.push(` ${group.baseName}: ${group.removed.length + 1} entries`);
|
|
1706
|
+
lines.push(` keep: ${group.kept.name} ${group.kept.path}`);
|
|
1707
|
+
for (const skill of group.removed) {
|
|
1708
|
+
lines.push(` drop: ${skill.name} ${skill.path}`);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
let moved = [];
|
|
1712
|
+
let deleted = [];
|
|
1713
|
+
if (options.remove) {
|
|
1714
|
+
const result = await removeDuplicateSkills(context.homeDir, duplicates, { delete: options.delete });
|
|
1715
|
+
moved = result.moved;
|
|
1716
|
+
deleted = result.deleted;
|
|
1717
|
+
lines.push("");
|
|
1718
|
+
if (options.delete) {
|
|
1719
|
+
lines.push(`Deleted ${deleted.length} duplicate skills`);
|
|
1720
|
+
} else {
|
|
1721
|
+
lines.push(`Moved ${moved.length} duplicate skills to ${context.homeDir}/.aweskill/dup_skills`);
|
|
1722
|
+
}
|
|
1723
|
+
} else {
|
|
1724
|
+
lines.push("");
|
|
1725
|
+
lines.push("Dry run only. Use --remove to move duplicates into dup_skills, or --remove --delete to delete them.");
|
|
1726
|
+
}
|
|
1727
|
+
context.write(lines.join("\n"));
|
|
1728
|
+
return { duplicates, moved, deleted };
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// src/commands/sync.ts
|
|
1732
|
+
import { access as access8 } from "fs/promises";
|
|
1733
|
+
import path14 from "path";
|
|
1734
|
+
async function pathExists8(targetPath) {
|
|
1735
|
+
try {
|
|
1736
|
+
await access8(targetPath);
|
|
1737
|
+
return true;
|
|
1738
|
+
} catch {
|
|
1739
|
+
return false;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
async function runSync(context, options) {
|
|
1743
|
+
const { skillsDir } = getAweskillPaths(context.homeDir);
|
|
1744
|
+
const baseDirs = /* @__PURE__ */ new Set();
|
|
1745
|
+
baseDirs.add({ scope: "global", dir: context.homeDir });
|
|
1746
|
+
if (options.projectDir) {
|
|
1747
|
+
baseDirs.add({ scope: "project", dir: options.projectDir });
|
|
1748
|
+
}
|
|
1749
|
+
if (await pathExists8(path14.join(context.cwd, ".aweskill.yaml"))) {
|
|
1750
|
+
baseDirs.add({ scope: "project", dir: context.cwd });
|
|
1751
|
+
}
|
|
1752
|
+
let removed = 0;
|
|
1753
|
+
const warnings = [];
|
|
1754
|
+
for (const { scope, dir } of baseDirs) {
|
|
1755
|
+
for (const agent of listSupportedAgents()) {
|
|
1756
|
+
const skillsDir2 = resolveAgentSkillsDir(agent.id, scope, dir);
|
|
1757
|
+
const managed = await listManagedSkillNames(skillsDir2, skillsDir);
|
|
1758
|
+
for (const [skillName] of managed) {
|
|
1759
|
+
const sourcePath = path14.join(skillsDir, skillName);
|
|
1760
|
+
if (!await pathExists8(sourcePath)) {
|
|
1761
|
+
const wasRemoved = await removeManagedProjection(path14.join(skillsDir2, skillName));
|
|
1762
|
+
if (wasRemoved) {
|
|
1763
|
+
removed += 1;
|
|
1764
|
+
warnings.push(`Removed stale projection: ${agent.id}:${skillName} (source missing)`);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
context.write(`Sync complete. Removed ${removed} stale projection(s).`);
|
|
1771
|
+
if (warnings.length > 0) {
|
|
1772
|
+
context.write(warnings.join("\n"));
|
|
1773
|
+
}
|
|
1774
|
+
return { removed, warnings };
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/lib/runtime.ts
|
|
1778
|
+
import { realpathSync } from "fs";
|
|
1779
|
+
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
|
|
1780
|
+
function isDirectCliEntry(importMetaUrl, argv1) {
|
|
1781
|
+
if (!argv1) {
|
|
1782
|
+
return false;
|
|
1783
|
+
}
|
|
1784
|
+
try {
|
|
1785
|
+
const executedPath = realpathSync(argv1);
|
|
1786
|
+
const modulePath = realpathSync(fileURLToPath2(importMetaUrl));
|
|
1787
|
+
return pathToFileURL(modulePath).href === pathToFileURL(executedPath).href;
|
|
1788
|
+
} catch {
|
|
1789
|
+
return importMetaUrl === pathToFileURL(argv1).href;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// src/lib/ui.ts
|
|
1794
|
+
import * as p from "@clack/prompts";
|
|
1795
|
+
import pc from "picocolors";
|
|
1796
|
+
function emitMessage(line) {
|
|
1797
|
+
const trimmed = line.trim();
|
|
1798
|
+
if (!trimmed) {
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
if (trimmed.startsWith("Warning: ")) {
|
|
1802
|
+
p.log.warn(trimmed.slice("Warning: ".length));
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
if (trimmed.startsWith("Error: ")) {
|
|
1806
|
+
p.log.error(trimmed.slice("Error: ".length));
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
if (trimmed.startsWith("Created ") || trimmed.startsWith("Backed up ") || trimmed.startsWith("Imported ") || trimmed.startsWith("Enabled ") || trimmed.startsWith("Disabled ") || trimmed.startsWith("Initialized ") || trimmed.startsWith("Deleted ") || trimmed.startsWith("Removed ") || trimmed.startsWith("Recovered ") || trimmed.startsWith("Sync applied ")) {
|
|
1810
|
+
p.log.success(trimmed);
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
if (trimmed.startsWith("Skills in central repo:") || trimmed === "Scanned skills:" || trimmed === "Duplicate skill groups in central repo:" || trimmed === "Bundles:" || trimmed.startsWith("Global skills for ") || trimmed.startsWith("Project skills for ")) {
|
|
1814
|
+
console.log(pc.bold(trimmed));
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
if (trimmed.startsWith("linked:") || trimmed.startsWith("duplicate:") || trimmed.startsWith("new:")) {
|
|
1818
|
+
console.log(pc.dim(trimmed));
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
if (trimmed.startsWith("No duplicate skills found")) {
|
|
1822
|
+
console.log(pc.dim(trimmed));
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
if (line.startsWith(" Global scanned skills for ") || line.startsWith(" Project scanned skills for ")) {
|
|
1826
|
+
console.log(pc.dim(line.trim()));
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
if (line.startsWith(" keep: ") || line.startsWith(" drop: ")) {
|
|
1830
|
+
const [label, ...rest] = line.trim().split(" ");
|
|
1831
|
+
const marker = label === "keep:" ? pc.green("keep:") : pc.yellow("drop:");
|
|
1832
|
+
console.log(` ${marker} ${rest.join(" ")}`);
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
if (line.startsWith(" \u2713 ")) {
|
|
1836
|
+
const rest = line.slice(4);
|
|
1837
|
+
const firstSpace = rest.indexOf(" ");
|
|
1838
|
+
if (firstSpace === -1) {
|
|
1839
|
+
console.log(` ${pc.green("\u2713")} ${pc.cyan(rest)}`);
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
const name = rest.slice(0, firstSpace);
|
|
1843
|
+
const location = rest.slice(firstSpace + 1);
|
|
1844
|
+
console.log(` ${pc.green("\u2713")} ${pc.cyan(name)} ${pc.dim(location)}`);
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
if (line.startsWith(" ! ")) {
|
|
1848
|
+
const rest = line.slice(4);
|
|
1849
|
+
const firstSpace = rest.indexOf(" ");
|
|
1850
|
+
if (firstSpace === -1) {
|
|
1851
|
+
console.log(` ${pc.yellow("!")} ${pc.cyan(rest)}`);
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
const name = rest.slice(0, firstSpace);
|
|
1855
|
+
const location = rest.slice(firstSpace + 1);
|
|
1856
|
+
console.log(` ${pc.yellow("!")} ${pc.cyan(name)} ${pc.dim(location)}`);
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
if (line.startsWith(" \u2713 ") || line.startsWith(" ! ") || line.startsWith(" + ")) {
|
|
1860
|
+
const marker = line.slice(4, 5);
|
|
1861
|
+
const rest = line.slice(6);
|
|
1862
|
+
const firstSpace = rest.indexOf(" ");
|
|
1863
|
+
const name = firstSpace === -1 ? rest : rest.slice(0, firstSpace);
|
|
1864
|
+
const location = firstSpace === -1 ? "" : rest.slice(firstSpace + 1);
|
|
1865
|
+
const coloredMarker = marker === "\u2713" ? pc.green("\u2713") : marker === "!" ? pc.yellow("!") : pc.cyan("+");
|
|
1866
|
+
console.log(` ${coloredMarker} ${pc.cyan(name)}${location ? ` ${pc.dim(location)}` : ""}`);
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
if (trimmed.startsWith("No skills found ") || trimmed === "No bundles found." || trimmed.startsWith("Showing first ") || trimmed.startsWith("... and ")) {
|
|
1870
|
+
console.log(pc.dim(trimmed));
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
p.log.message(trimmed, { symbol: pc.cyan("\u2022") });
|
|
1874
|
+
}
|
|
1875
|
+
function commandTitle(title) {
|
|
1876
|
+
return pc.bgCyan(pc.black(` ${title} `));
|
|
1877
|
+
}
|
|
1878
|
+
function writeCliMessage(message) {
|
|
1879
|
+
for (const line of message.split("\n")) {
|
|
1880
|
+
emitMessage(line);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
function writeCliError(message) {
|
|
1884
|
+
for (const line of message.split("\n")) {
|
|
1885
|
+
if (!line.trim()) {
|
|
1886
|
+
continue;
|
|
1887
|
+
}
|
|
1888
|
+
p.log.error(line.replace(/^Error:\s*/, ""));
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
function introCommand(title) {
|
|
1892
|
+
p.intro(commandTitle(title));
|
|
1893
|
+
}
|
|
1894
|
+
function outroCommand(message = pc.green("Done.")) {
|
|
1895
|
+
p.outro(message);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// src/index.ts
|
|
1899
|
+
function createRuntimeContext(overrides = {}) {
|
|
1900
|
+
return {
|
|
1901
|
+
cwd: overrides.cwd ?? process.cwd(),
|
|
1902
|
+
homeDir: overrides.homeDir ?? process.env.AWESKILL_HOME ?? homedir2(),
|
|
1903
|
+
write: overrides.write ?? writeCliMessage,
|
|
1904
|
+
error: overrides.error ?? writeCliError
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
async function runFramedCommand(title, action) {
|
|
1908
|
+
introCommand(title);
|
|
1909
|
+
const result = await action();
|
|
1910
|
+
outroCommand();
|
|
1911
|
+
return result;
|
|
1912
|
+
}
|
|
1913
|
+
function collectAgents(value, previous) {
|
|
1914
|
+
return [...previous ?? [], ...value.split(",").map((entry) => entry.trim()).filter(Boolean)];
|
|
1915
|
+
}
|
|
1916
|
+
function getMode(value) {
|
|
1917
|
+
if (value === "mv" || value === "cp") {
|
|
1918
|
+
return value;
|
|
1919
|
+
}
|
|
1920
|
+
throw new Error(`Unsupported import mode: ${value}. Use "cp" or "mv".`);
|
|
1921
|
+
}
|
|
1922
|
+
function getActivationType(value) {
|
|
1923
|
+
if (value === "bundle" || value === "skill") {
|
|
1924
|
+
return value;
|
|
1925
|
+
}
|
|
1926
|
+
throw new Error(`Unsupported activation type: ${value}`);
|
|
1927
|
+
}
|
|
1928
|
+
function formatCliErrorMessage(message) {
|
|
1929
|
+
const match = message.match(/missing required argument '([^']+)'/i);
|
|
1930
|
+
if (!match) {
|
|
1931
|
+
const optionMatch = message.match(/option '([^']+)' argument missing/i);
|
|
1932
|
+
if (optionMatch?.[1] === "--agent <agent>") {
|
|
1933
|
+
return 'Option --agent <agent> argument missing. Use one or more supported agent ids, for example "codex" or "codex,cursor". Run "aweskill agent supported" to see the supported agent list.';
|
|
1934
|
+
}
|
|
1935
|
+
const normalizedMessage = message.replace(/^error:\s*/i, "");
|
|
1936
|
+
const bundleFileMatch = normalizedMessage.match(/ENOENT: no such file or directory, open '([^']+\/bundles\/([^/'"]+)\.ya?ml)'/i);
|
|
1937
|
+
if (bundleFileMatch) {
|
|
1938
|
+
const bundleName = bundleFileMatch[2];
|
|
1939
|
+
if (bundleFileMatch[1]?.includes("/template/bundles/")) {
|
|
1940
|
+
return `Bundle template not found: ${bundleName}. Run "aweskill bundle template list" to see available bundle templates.`;
|
|
1941
|
+
}
|
|
1942
|
+
return `Bundle not found: ${bundleName}. Run "aweskill bundle list" to see available bundles.`;
|
|
1943
|
+
}
|
|
1944
|
+
const unknownSkillMatch = normalizedMessage.match(/^Unknown skill: (.+)$/);
|
|
1945
|
+
if (unknownSkillMatch) {
|
|
1946
|
+
return `Unknown skill: ${unknownSkillMatch[1]}. Run "aweskill skill list" to see available skills.`;
|
|
1947
|
+
}
|
|
1948
|
+
const bundleNotFoundMatch = normalizedMessage.match(/^Bundle not found: (.+)$/);
|
|
1949
|
+
if (bundleNotFoundMatch) {
|
|
1950
|
+
return `Bundle not found: ${bundleNotFoundMatch[1]}. Run "aweskill bundle list" to see available bundles.`;
|
|
1951
|
+
}
|
|
1952
|
+
return normalizedMessage;
|
|
1953
|
+
}
|
|
1954
|
+
const argName = match[1];
|
|
1955
|
+
const hints = {
|
|
1956
|
+
archive: 'Use a backup archive path, for example "skills-2026-04-12T19-20-00Z.tar.gz".',
|
|
1957
|
+
bundle: "Use a bundle name.",
|
|
1958
|
+
name: 'Use a bundle or skill name, for example "my-bundle", "biopython", or "all".',
|
|
1959
|
+
skill: "Use a skill name.",
|
|
1960
|
+
type: 'Use "bundle" or "skill".'
|
|
1961
|
+
};
|
|
1962
|
+
const hint = hints[argName];
|
|
1963
|
+
return `Missing required argument <${argName}>.${hint ? ` ${hint}` : ""}`;
|
|
1964
|
+
}
|
|
1965
|
+
function writeSupportedAgents(context) {
|
|
1966
|
+
const lines = [
|
|
1967
|
+
"Supported agents:",
|
|
1968
|
+
"amp (Amp)",
|
|
1969
|
+
"claude-code (Claude Code)",
|
|
1970
|
+
"cline (Cline)",
|
|
1971
|
+
"codex (Codex)",
|
|
1972
|
+
"cursor (Cursor)",
|
|
1973
|
+
"gemini-cli (Gemini CLI)",
|
|
1974
|
+
"goose (Goose)",
|
|
1975
|
+
"opencode (OpenCode)",
|
|
1976
|
+
"roo (Roo Code)",
|
|
1977
|
+
"windsurf (Windsurf)"
|
|
1978
|
+
];
|
|
1979
|
+
for (const line of lines) {
|
|
1980
|
+
context.write(line);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
function configureCommandTree(command) {
|
|
1984
|
+
command.showHelpAfterError();
|
|
1985
|
+
command.exitOverride((error) => {
|
|
1986
|
+
throw new Error(formatCliErrorMessage(error.message));
|
|
1987
|
+
});
|
|
1988
|
+
for (const child of command.commands) {
|
|
1989
|
+
configureCommandTree(child);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
function createProgram(overrides = {}) {
|
|
1993
|
+
const context = createRuntimeContext(overrides);
|
|
1994
|
+
const program = new Command();
|
|
1995
|
+
program.name("aweskill").description("Local skill orchestration CLI for AI agents").version("0.1.5").helpOption("-h, --help", "Display help");
|
|
1996
|
+
const skill = program.command("skill").description("Manage skills in the central store");
|
|
1997
|
+
skill.command("list").description("List skills in the central store").option("--verbose", "show all skills instead of a short preview", false).action(async (options) => {
|
|
1998
|
+
await runListSkills(context, { verbose: options.verbose });
|
|
1999
|
+
});
|
|
2000
|
+
skill.command("scan").description("Scan supported agent skill directories").option("--verbose", "show scanned skill details instead of per-agent totals", false).action(async (options) => {
|
|
2001
|
+
await runFramedCommand(
|
|
2002
|
+
" aweskill skill scan ",
|
|
2003
|
+
async () => runScan(context, {
|
|
2004
|
+
verbose: options.verbose
|
|
2005
|
+
})
|
|
2006
|
+
);
|
|
2007
|
+
});
|
|
2008
|
+
skill.command("import").argument("[path]").description("Import one skill or a skills root directory").option("--scan", "import scanned skills", false).option("--mode <mode>", "import mode: cp (default) or mv", getMode, "cp").option("--override", "overwrite existing files when importing", false).action(async (sourcePath, options) => {
|
|
2009
|
+
await runFramedCommand(
|
|
2010
|
+
" aweskill skill import ",
|
|
2011
|
+
async () => runImport(context, {
|
|
2012
|
+
sourcePath,
|
|
2013
|
+
scan: options.scan,
|
|
2014
|
+
mode: options.mode,
|
|
2015
|
+
override: options.override
|
|
2016
|
+
})
|
|
2017
|
+
);
|
|
2018
|
+
});
|
|
2019
|
+
skill.command("remove").argument("<skill>").description("Remove a skill from the central store").option("--force", "remove and clean references", false).option("--project <dir>", "project config to inspect").action(async (skillName, options) => {
|
|
2020
|
+
await runFramedCommand(
|
|
2021
|
+
" aweskill skill remove ",
|
|
2022
|
+
async () => runRemove(context, {
|
|
2023
|
+
skillName,
|
|
2024
|
+
force: options.force,
|
|
2025
|
+
projectDir: options.project
|
|
2026
|
+
})
|
|
2027
|
+
);
|
|
2028
|
+
});
|
|
2029
|
+
const bundle = program.command("bundle").description("Manage skill bundles");
|
|
2030
|
+
bundle.command("list").description("List bundles in the central store").option("--verbose", "show all bundles instead of a short preview", false).action(async (options) => {
|
|
2031
|
+
await runListBundles(context, { verbose: options.verbose });
|
|
2032
|
+
});
|
|
2033
|
+
bundle.command("create").argument("<name>").description("Create a bundle").action(async (name) => {
|
|
2034
|
+
await runFramedCommand(" aweskill bundle create ", async () => runBundleCreate(context, name));
|
|
2035
|
+
});
|
|
2036
|
+
bundle.command("show").argument("<name>").description("Show bundle contents").action(async (name) => {
|
|
2037
|
+
await runBundleShow(context, name);
|
|
2038
|
+
});
|
|
2039
|
+
bundle.command("add").argument("<bundle>").argument("<skill>").description("Add skill entries to one or more bundles").action(async (bundleName, skillName) => {
|
|
2040
|
+
await runFramedCommand(" aweskill bundle add ", async () => runBundleAddSkill(context, bundleName, skillName));
|
|
2041
|
+
});
|
|
2042
|
+
bundle.command("remove").argument("<bundle>").argument("<skill>").description("Remove skill entries from one or more bundles").action(async (bundleName, skillName) => {
|
|
2043
|
+
await runFramedCommand(" aweskill bundle remove ", async () => runBundleRemoveSkill(context, bundleName, skillName));
|
|
2044
|
+
});
|
|
2045
|
+
bundle.command("delete").argument("<name>").description("Delete a bundle").action(async (name) => {
|
|
2046
|
+
await runFramedCommand(" aweskill bundle delete ", async () => runBundleDelete(context, name));
|
|
2047
|
+
});
|
|
2048
|
+
const bundleTemplate = bundle.command("template").description("Manage built-in bundle templates");
|
|
2049
|
+
bundleTemplate.command("list").description("List available built-in bundle templates").option("--verbose", "show all bundle templates instead of a short preview", false).action(async (options) => {
|
|
2050
|
+
await runListTemplateBundles(context, { verbose: options.verbose });
|
|
2051
|
+
});
|
|
2052
|
+
bundleTemplate.command("import").argument("<name>").description("Copy built-in templates into the central store").action(async (name) => {
|
|
2053
|
+
await runFramedCommand(" aweskill bundle template import ", async () => runBundleAddTemplate(context, name));
|
|
2054
|
+
});
|
|
2055
|
+
const agent = program.command("agent").description("Manage skills used by agents");
|
|
2056
|
+
agent.command("supported").description("List supported agent ids and display names").action(async () => {
|
|
2057
|
+
writeSupportedAgents(context);
|
|
2058
|
+
});
|
|
2059
|
+
agent.command("list").description("Inspect agent skill directories and optionally normalize them").option("--global", "check global scope (default when no scope flag given)").option("--project [dir]", "check project scope; uses cwd when dir is omitted").option("--agent <agent>", 'repeat or use comma list; defaults to all; run "aweskill agent supported" to see supported ids', collectAgents).option("--update", "import missing skills into the central store and relink duplicates/new skills", false).option("--verbose", "show all skills in each category instead of a short preview", false).action(async (options) => {
|
|
2060
|
+
const isProject = options.project !== void 0;
|
|
2061
|
+
const scope = isProject ? "project" : "global";
|
|
2062
|
+
const projectDir = isProject && typeof options.project === "string" ? options.project : void 0;
|
|
2063
|
+
await runFramedCommand(
|
|
2064
|
+
" aweskill agent list ",
|
|
2065
|
+
async () => runCheck(context, {
|
|
2066
|
+
scope,
|
|
2067
|
+
agents: options.agent ?? [],
|
|
2068
|
+
projectDir,
|
|
2069
|
+
update: options.update,
|
|
2070
|
+
verbose: options.verbose
|
|
2071
|
+
})
|
|
2072
|
+
);
|
|
2073
|
+
});
|
|
2074
|
+
agent.command("add").description("Create skill projections in agent directories").argument("<type>", "bundle or skill", getActivationType).argument("<name>", 'bundle or skill name(s), comma-separated, or "all"').option("--global", "apply to global scope (default when no scope flag given)").option("--project [dir]", "apply to project scope; uses cwd when dir is omitted").option("--agent <agent>", 'repeat or use comma list; defaults to all; run "aweskill agent supported" to see supported ids', collectAgents).action(async (type, targetName, options) => {
|
|
2075
|
+
const isProject = options.project !== void 0;
|
|
2076
|
+
const scope = isProject ? "project" : "global";
|
|
2077
|
+
const projectDir = isProject && typeof options.project === "string" ? options.project : void 0;
|
|
2078
|
+
await runFramedCommand(
|
|
2079
|
+
" aweskill agent add ",
|
|
2080
|
+
async () => runEnable(context, {
|
|
2081
|
+
type,
|
|
2082
|
+
name: targetName,
|
|
2083
|
+
scope,
|
|
2084
|
+
agents: options.agent ?? [],
|
|
2085
|
+
projectDir
|
|
2086
|
+
})
|
|
2087
|
+
);
|
|
2088
|
+
});
|
|
2089
|
+
agent.command("remove").description("Remove skill projections in agent directories").argument("<type>", "bundle or skill", getActivationType).argument("<name>", 'bundle or skill name(s), comma-separated, or "all"').option("--global", "apply to global scope (default when no scope flag given)").option("--project [dir]", "apply to project scope; uses cwd when dir is omitted").option("--agent <agent>", 'repeat or use comma list; defaults to all; run "aweskill agent supported" to see supported ids', collectAgents).option(
|
|
2090
|
+
"--force",
|
|
2091
|
+
"with skill: remove projection even when this skill is in a bundle that still has other members enabled here",
|
|
2092
|
+
false
|
|
2093
|
+
).action(async (type, targetName, options) => {
|
|
2094
|
+
const isProject = options.project !== void 0;
|
|
2095
|
+
const scope = isProject ? "project" : "global";
|
|
2096
|
+
const projectDir = isProject && typeof options.project === "string" ? options.project : void 0;
|
|
2097
|
+
await runFramedCommand(
|
|
2098
|
+
" aweskill agent remove ",
|
|
2099
|
+
async () => runDisable(context, {
|
|
2100
|
+
type,
|
|
2101
|
+
name: targetName,
|
|
2102
|
+
scope,
|
|
2103
|
+
agents: options.agent ?? [],
|
|
2104
|
+
projectDir,
|
|
2105
|
+
force: options.force
|
|
2106
|
+
})
|
|
2107
|
+
);
|
|
2108
|
+
});
|
|
2109
|
+
agent.command("sync").description("Remove stale managed projections whose source skill no longer exists").option("--project <dir>", "also check this project directory").action(async (options) => {
|
|
2110
|
+
await runFramedCommand(" aweskill agent sync ", async () => runSync(context, { projectDir: options.project }));
|
|
2111
|
+
});
|
|
2112
|
+
agent.command("recover").description("Convert aweskill-managed symlink projections into full skill directories").option("--global", "recover global scope (default when no scope flag given)").option("--project [dir]", "recover project scope; uses cwd when dir is omitted").option("--agent <agent>", 'repeat or use comma list; defaults to all; run "aweskill agent supported" to see supported ids', collectAgents).action(async (options) => {
|
|
2113
|
+
const isProject = options.project !== void 0;
|
|
2114
|
+
const scope = isProject ? "project" : "global";
|
|
2115
|
+
const projectDir = isProject && typeof options.project === "string" ? options.project : void 0;
|
|
2116
|
+
await runFramedCommand(
|
|
2117
|
+
" aweskill agent recover ",
|
|
2118
|
+
async () => runRecover(context, {
|
|
2119
|
+
scope,
|
|
2120
|
+
agents: options.agent ?? [],
|
|
2121
|
+
projectDir
|
|
2122
|
+
})
|
|
2123
|
+
);
|
|
2124
|
+
});
|
|
2125
|
+
const store = program.command("store").description("Manage the aweskill local store");
|
|
2126
|
+
store.command("init").description("Create ~/.aweskill layout").option("--scan", "scan existing agent directories after init", false).option("--verbose", "show scanned skill details instead of per-agent totals", false).action(async (options) => {
|
|
2127
|
+
await runFramedCommand(" aweskill store init ", async () => runInit(context, options));
|
|
2128
|
+
});
|
|
2129
|
+
store.command("backup").description("Create a timestamped archive of the central skills repository").action(async () => {
|
|
2130
|
+
await runFramedCommand(" aweskill store backup ", async () => runBackup(context));
|
|
2131
|
+
});
|
|
2132
|
+
store.command("restore").argument("<archive>").description("Restore skills from a backup archive and auto-back up the current skills first").option("--override", "replace existing skills with the archive contents", false).action(async (archivePath, options) => {
|
|
2133
|
+
await runFramedCommand(
|
|
2134
|
+
" aweskill store restore ",
|
|
2135
|
+
async () => runRestore(context, {
|
|
2136
|
+
archivePath,
|
|
2137
|
+
override: options.override
|
|
2138
|
+
})
|
|
2139
|
+
);
|
|
2140
|
+
});
|
|
2141
|
+
const doctor = program.command("doctor").description("Diagnose and repair repository issues");
|
|
2142
|
+
doctor.command("dedupe").description("Find or remove duplicate central-store skills with numeric/version suffixes").option("--fix", "move duplicate skills into dup_skills (or delete them with --delete)", false).option("--delete", "when used with --fix, permanently delete duplicates instead of moving them", false).action(async (options) => {
|
|
2143
|
+
await runFramedCommand(
|
|
2144
|
+
" aweskill doctor dedupe ",
|
|
2145
|
+
async () => runRmdup(context, {
|
|
2146
|
+
remove: options.fix,
|
|
2147
|
+
delete: options.delete
|
|
2148
|
+
})
|
|
2149
|
+
);
|
|
2150
|
+
});
|
|
2151
|
+
configureCommandTree(program);
|
|
2152
|
+
return program;
|
|
2153
|
+
}
|
|
2154
|
+
async function main(argv = process.argv) {
|
|
2155
|
+
const program = createProgram();
|
|
2156
|
+
try {
|
|
2157
|
+
await program.parseAsync(argv);
|
|
2158
|
+
} catch (error) {
|
|
2159
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2160
|
+
if (message === "(outputHelp)" || message === "outputHelp") {
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
console.error(`Error: ${formatCliErrorMessage(message)}`);
|
|
2164
|
+
process.exitCode = 1;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
if (isDirectCliEntry(import.meta.url, process.argv[1])) {
|
|
2168
|
+
void main();
|
|
2169
|
+
}
|
|
2170
|
+
export {
|
|
2171
|
+
createProgram,
|
|
2172
|
+
main
|
|
2173
|
+
};
|
|
2174
|
+
//# sourceMappingURL=index.js.map
|