skillswitch 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +985 -126
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import * as readline from "readline";
|
|
6
|
+
import * as fs13 from "fs";
|
|
6
7
|
|
|
7
8
|
// src/scanner.ts
|
|
8
9
|
import * as fs from "fs";
|
|
@@ -36,8 +37,13 @@ function scanStandaloneSkills(claudeDir = defaultClaudeDir) {
|
|
|
36
37
|
const filePath = path.join(dir, file);
|
|
37
38
|
if (!fs.statSync(filePath).isFile()) continue;
|
|
38
39
|
const name = file.slice(0, -3);
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
try {
|
|
41
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
42
|
+
skills.push({ source: "standalone", name, path: filePath, status, description: extractDescription(content) });
|
|
43
|
+
} catch {
|
|
44
|
+
process.stderr.write(`Warning: could not read ${filePath} \u2014 skipping
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
41
47
|
}
|
|
42
48
|
}
|
|
43
49
|
readDir(skillsDir2, "active");
|
|
@@ -48,13 +54,30 @@ function scanPlugins(claudeDir = defaultClaudeDir) {
|
|
|
48
54
|
const pluginsDir = path.join(claudeDir, "plugins");
|
|
49
55
|
const installedFile = path.join(pluginsDir, "installed_plugins.json");
|
|
50
56
|
if (!fs.existsSync(installedFile)) return [];
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
let pluginMap;
|
|
58
|
+
try {
|
|
59
|
+
const raw = JSON.parse(fs.readFileSync(installedFile, "utf-8"));
|
|
60
|
+
pluginMap = raw.plugins ?? {};
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (err instanceof SyntaxError) {
|
|
63
|
+
process.stderr.write("Warning: plugins/installed_plugins.json is malformed \u2014 skipping plugin scan\n");
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
53
68
|
const blockedSet = /* @__PURE__ */ new Set();
|
|
54
69
|
const blocklistFile = path.join(pluginsDir, "blocklist.json");
|
|
55
70
|
if (fs.existsSync(blocklistFile)) {
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
try {
|
|
72
|
+
const bl = JSON.parse(fs.readFileSync(blocklistFile, "utf-8"));
|
|
73
|
+
for (const entry of bl.plugins ?? []) blockedSet.add(entry.plugin);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (err instanceof SyntaxError) {
|
|
76
|
+
process.stderr.write("Warning: plugins/blocklist.json is malformed \u2014 treating as empty\n");
|
|
77
|
+
} else {
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
58
81
|
}
|
|
59
82
|
return Object.keys(pluginMap).map((pluginId) => {
|
|
60
83
|
const atIdx = pluginId.indexOf("@");
|
|
@@ -64,7 +87,7 @@ function scanPlugins(claudeDir = defaultClaudeDir) {
|
|
|
64
87
|
const cacheBase = path.join(pluginsDir, "cache", sourceMarket, name);
|
|
65
88
|
const pluginSkills = [];
|
|
66
89
|
if (fs.existsSync(cacheBase)) {
|
|
67
|
-
const versions = fs.readdirSync(cacheBase).filter((d) => fs.statSync(path.join(cacheBase, d)).isDirectory()).sort();
|
|
90
|
+
const versions = fs.readdirSync(cacheBase).filter((d) => fs.statSync(path.join(cacheBase, d)).isDirectory()).sort((a, b) => a.localeCompare(b, void 0, { numeric: true, sensitivity: "base" }));
|
|
68
91
|
if (versions.length > 0) {
|
|
69
92
|
const skillsDir2 = path.join(cacheBase, versions[versions.length - 1], "skills");
|
|
70
93
|
if (fs.existsSync(skillsDir2)) collectPluginSkills(skillsDir2, name, pluginId, status, pluginSkills);
|
|
@@ -79,12 +102,23 @@ function collectPluginSkills(dir, pluginName, pluginId, status, out) {
|
|
|
79
102
|
if (fs.statSync(entryPath).isDirectory()) {
|
|
80
103
|
for (const sub of fs.readdirSync(entryPath)) {
|
|
81
104
|
if (!sub.endsWith(".md")) continue;
|
|
82
|
-
const
|
|
83
|
-
|
|
105
|
+
const subPath = path.join(entryPath, sub);
|
|
106
|
+
try {
|
|
107
|
+
const content = fs.readFileSync(subPath, "utf-8");
|
|
108
|
+
out.push({ source: "plugin", name: `${entry}:${sub.slice(0, -3)}`, plugin: pluginId, status, description: extractDescription(content) });
|
|
109
|
+
} catch {
|
|
110
|
+
process.stderr.write(`Warning: could not read ${subPath} \u2014 skipping
|
|
111
|
+
`);
|
|
112
|
+
}
|
|
84
113
|
}
|
|
85
114
|
} else if (entry.endsWith(".md")) {
|
|
86
|
-
|
|
87
|
-
|
|
115
|
+
try {
|
|
116
|
+
const content = fs.readFileSync(entryPath, "utf-8");
|
|
117
|
+
out.push({ source: "plugin", name: `${pluginName}:${entry.slice(0, -3)}`, plugin: pluginId, status, description: extractDescription(content) });
|
|
118
|
+
} catch {
|
|
119
|
+
process.stderr.write(`Warning: could not read ${entryPath} \u2014 skipping
|
|
120
|
+
`);
|
|
121
|
+
}
|
|
88
122
|
}
|
|
89
123
|
}
|
|
90
124
|
}
|
|
@@ -107,6 +141,7 @@ function disableSkill(name, claudeDir = defaultClaudeDir2) {
|
|
|
107
141
|
function enableSkill(name, claudeDir = defaultClaudeDir2) {
|
|
108
142
|
const src = path2.join(disabledDir(claudeDir), `${name}.md`);
|
|
109
143
|
if (!fs2.existsSync(src)) throw new Error(`Skill "${name}" is not in disabled directory`);
|
|
144
|
+
fs2.mkdirSync(skillsDir(claudeDir), { recursive: true });
|
|
110
145
|
fs2.renameSync(src, path2.join(skillsDir(claudeDir), `${name}.md`));
|
|
111
146
|
}
|
|
112
147
|
|
|
@@ -129,6 +164,10 @@ async function readBlocklist(claudeDir = defaultClaudeDir3) {
|
|
|
129
164
|
if (err.code === "ENOENT") {
|
|
130
165
|
return { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), plugins: [] };
|
|
131
166
|
}
|
|
167
|
+
if (err instanceof SyntaxError) {
|
|
168
|
+
process.stderr.write("Warning: blocklist.json is malformed \u2014 treating as empty\n");
|
|
169
|
+
return { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), plugins: [] };
|
|
170
|
+
}
|
|
132
171
|
throw err;
|
|
133
172
|
}
|
|
134
173
|
}
|
|
@@ -151,10 +190,11 @@ async function blockPlugin(pluginId, reason, claudeDir = defaultClaudeDir3) {
|
|
|
151
190
|
async function unblockPlugin(pluginId, claudeDir = defaultClaudeDir3) {
|
|
152
191
|
const blocklist = await readBlocklist(claudeDir);
|
|
153
192
|
const filtered = blocklist.plugins.filter((e) => e.plugin !== pluginId);
|
|
154
|
-
if (filtered.length === blocklist.plugins.length) return;
|
|
193
|
+
if (filtered.length === blocklist.plugins.length) return false;
|
|
155
194
|
blocklist.plugins = filtered;
|
|
156
195
|
blocklist.fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
157
196
|
await writeBlocklist(blocklist, claudeDir);
|
|
197
|
+
return true;
|
|
158
198
|
}
|
|
159
199
|
async function setBlockedPlugins(pluginIds, reason, claudeDir = defaultClaudeDir3) {
|
|
160
200
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -163,37 +203,375 @@ async function setBlockedPlugins(pluginIds, reason, claudeDir = defaultClaudeDir
|
|
|
163
203
|
await writeBlocklist(data, claudeDir);
|
|
164
204
|
}
|
|
165
205
|
|
|
166
|
-
// src/
|
|
206
|
+
// src/adapters/claude.ts
|
|
167
207
|
import * as fs4 from "fs";
|
|
168
208
|
import * as path4 from "path";
|
|
169
|
-
import
|
|
170
|
-
var
|
|
209
|
+
import * as os3 from "os";
|
|
210
|
+
var ClaudeAdapter = class {
|
|
211
|
+
constructor(claudeDir = path4.join(os3.homedir(), ".claude")) {
|
|
212
|
+
this.claudeDir = claudeDir;
|
|
213
|
+
this.skillsDirs = [path4.join(claudeDir, "skills")];
|
|
214
|
+
}
|
|
215
|
+
claudeDir;
|
|
216
|
+
cliName = "claude";
|
|
217
|
+
displayName = "Claude Code";
|
|
218
|
+
skillsDirs;
|
|
219
|
+
isInstalled() {
|
|
220
|
+
return fs4.existsSync(this.claudeDir);
|
|
221
|
+
}
|
|
222
|
+
scanSkills() {
|
|
223
|
+
return scanStandaloneSkills(this.claudeDir).map((s) => ({
|
|
224
|
+
name: s.name,
|
|
225
|
+
status: s.status,
|
|
226
|
+
description: s.description
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
disableSkill(name) {
|
|
230
|
+
disableSkill(name, this.claudeDir);
|
|
231
|
+
}
|
|
232
|
+
enableSkill(name) {
|
|
233
|
+
enableSkill(name, this.claudeDir);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// src/adapters/gemini.ts
|
|
238
|
+
import * as fs6 from "fs";
|
|
239
|
+
import * as path6 from "path";
|
|
240
|
+
import * as os4 from "os";
|
|
241
|
+
|
|
242
|
+
// src/adapters/helpers.ts
|
|
243
|
+
import * as fs5 from "fs";
|
|
244
|
+
import * as path5 from "path";
|
|
245
|
+
function parseFrontmatterField(frontmatter, field) {
|
|
246
|
+
return frontmatter.match(new RegExp(`^${field}:\\s*(.+)$`, "m"))?.[1]?.trim() ?? "";
|
|
247
|
+
}
|
|
248
|
+
function parseSkillMd(content) {
|
|
249
|
+
const m = content.match(/^---\n([\s\S]*?)\n---/);
|
|
250
|
+
if (!m) return { name: "", description: "" };
|
|
251
|
+
return { name: parseFrontmatterField(m[1], "name"), description: parseFrontmatterField(m[1], "description") };
|
|
252
|
+
}
|
|
253
|
+
function extractFirstLine(content) {
|
|
254
|
+
let inFm = false, fmClosed = false;
|
|
255
|
+
for (const line of content.split("\n")) {
|
|
256
|
+
if (!fmClosed && line.trim() === "---") {
|
|
257
|
+
inFm = !inFm;
|
|
258
|
+
if (!inFm) fmClosed = true;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (inFm || !line.trim() || line.startsWith("#")) continue;
|
|
262
|
+
return line.trim();
|
|
263
|
+
}
|
|
264
|
+
return "";
|
|
265
|
+
}
|
|
266
|
+
function readDesc(filePath) {
|
|
267
|
+
try {
|
|
268
|
+
const content = fs5.readFileSync(filePath, "utf-8");
|
|
269
|
+
return parseSkillMd(content).description || extractFirstLine(content);
|
|
270
|
+
} catch {
|
|
271
|
+
return "";
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function scanFlatSkills(dir, group) {
|
|
275
|
+
const skills = [];
|
|
276
|
+
const disabledDir2 = path5.join(dir, ".disabled");
|
|
277
|
+
for (const [d, status] of [[dir, "active"], [disabledDir2, "disabled"]]) {
|
|
278
|
+
if (!fs5.existsSync(d)) continue;
|
|
279
|
+
for (const file of fs5.readdirSync(d)) {
|
|
280
|
+
if (!file.endsWith(".md")) continue;
|
|
281
|
+
const fp = path5.join(d, file);
|
|
282
|
+
if (!fs5.statSync(fp).isFile()) continue;
|
|
283
|
+
skills.push({ name: file.slice(0, -3), status, description: readDesc(fp), ...group ? { group } : {} });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return skills;
|
|
287
|
+
}
|
|
288
|
+
function disableFlatSkill(name, dir) {
|
|
289
|
+
const src = path5.join(dir, `${name}.md`);
|
|
290
|
+
if (!fs5.existsSync(src)) throw new Error(`Skill "${name}" not found in ${dir}`);
|
|
291
|
+
const disDir = path5.join(dir, ".disabled");
|
|
292
|
+
fs5.mkdirSync(disDir, { recursive: true });
|
|
293
|
+
fs5.renameSync(src, path5.join(disDir, `${name}.md`));
|
|
294
|
+
}
|
|
295
|
+
function enableFlatSkill(name, dir) {
|
|
296
|
+
const src = path5.join(dir, ".disabled", `${name}.md`);
|
|
297
|
+
if (!fs5.existsSync(src)) throw new Error(`Skill "${name}" is not disabled`);
|
|
298
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
299
|
+
fs5.renameSync(src, path5.join(dir, `${name}.md`));
|
|
300
|
+
}
|
|
301
|
+
function scanDirSkills(dir, group) {
|
|
302
|
+
const skills = [];
|
|
303
|
+
const disabledDir2 = path5.join(dir, ".disabled");
|
|
304
|
+
for (const [d, status] of [[dir, "active"], [disabledDir2, "disabled"]]) {
|
|
305
|
+
if (!fs5.existsSync(d)) continue;
|
|
306
|
+
for (const entry of fs5.readdirSync(d)) {
|
|
307
|
+
if (entry === ".disabled") continue;
|
|
308
|
+
const ep = path5.join(d, entry);
|
|
309
|
+
if (!fs5.statSync(ep).isDirectory()) continue;
|
|
310
|
+
const skillMd = path5.join(ep, "SKILL.md");
|
|
311
|
+
let description = "";
|
|
312
|
+
if (fs5.existsSync(skillMd)) {
|
|
313
|
+
const content = fs5.readFileSync(skillMd, "utf-8");
|
|
314
|
+
description = parseSkillMd(content).description || extractFirstLine(content);
|
|
315
|
+
}
|
|
316
|
+
skills.push({ name: entry, status, description, ...group ? { group } : {} });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return skills;
|
|
320
|
+
}
|
|
321
|
+
function disableDirSkill(name, dir) {
|
|
322
|
+
const src = path5.join(dir, name);
|
|
323
|
+
if (!fs5.existsSync(src) || !fs5.statSync(src).isDirectory()) throw new Error(`Skill "${name}" not found in ${dir}`);
|
|
324
|
+
const disDir = path5.join(dir, ".disabled");
|
|
325
|
+
fs5.mkdirSync(disDir, { recursive: true });
|
|
326
|
+
fs5.renameSync(src, path5.join(disDir, name));
|
|
327
|
+
}
|
|
328
|
+
function enableDirSkill(name, dir) {
|
|
329
|
+
const src = path5.join(dir, ".disabled", name);
|
|
330
|
+
if (!fs5.existsSync(src)) throw new Error(`Skill "${name}" is not disabled`);
|
|
331
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
332
|
+
fs5.renameSync(src, path5.join(dir, name));
|
|
333
|
+
}
|
|
334
|
+
function findSkillDir(name, dirs, dirBased) {
|
|
335
|
+
for (const dir of dirs) {
|
|
336
|
+
const target = dirBased ? path5.join(dir, name) : path5.join(dir, `${name}.md`);
|
|
337
|
+
const disTarget = dirBased ? path5.join(dir, ".disabled", name) : path5.join(dir, ".disabled", `${name}.md`);
|
|
338
|
+
if (fs5.existsSync(target) || fs5.existsSync(disTarget)) return dir;
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/adapters/gemini.ts
|
|
344
|
+
var HOME = os4.homedir();
|
|
345
|
+
var GeminiAdapter = class {
|
|
346
|
+
cliName = "gemini";
|
|
347
|
+
displayName = "Gemini CLI";
|
|
348
|
+
skillsDirs = [
|
|
349
|
+
path6.join(HOME, ".gemini", "skills"),
|
|
350
|
+
path6.join(HOME, ".agents", "skills")
|
|
351
|
+
];
|
|
352
|
+
isInstalled() {
|
|
353
|
+
return fs6.existsSync(path6.join(HOME, ".gemini"));
|
|
354
|
+
}
|
|
355
|
+
scanSkills() {
|
|
356
|
+
const seen = /* @__PURE__ */ new Set();
|
|
357
|
+
const skills = [];
|
|
358
|
+
for (const [i, dir] of this.skillsDirs.entries()) {
|
|
359
|
+
const group = i === 0 ? void 0 : "shared";
|
|
360
|
+
for (const s of scanDirSkills(dir, group)) {
|
|
361
|
+
if (!seen.has(s.name)) {
|
|
362
|
+
seen.add(s.name);
|
|
363
|
+
skills.push(s);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return skills;
|
|
368
|
+
}
|
|
369
|
+
disableSkill(name) {
|
|
370
|
+
const dir = findSkillDir(name, this.skillsDirs, true);
|
|
371
|
+
if (!dir) throw new Error(`Skill "${name}" not found in Gemini skills directories`);
|
|
372
|
+
disableDirSkill(name, dir);
|
|
373
|
+
}
|
|
374
|
+
enableSkill(name) {
|
|
375
|
+
const dir = findSkillDir(name, this.skillsDirs, true);
|
|
376
|
+
if (!dir) throw new Error(`Skill "${name}" not found in Gemini skills directories`);
|
|
377
|
+
enableDirSkill(name, dir);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// src/adapters/codex.ts
|
|
382
|
+
import * as fs7 from "fs";
|
|
383
|
+
import * as path7 from "path";
|
|
384
|
+
import * as os5 from "os";
|
|
385
|
+
var HOME2 = os5.homedir();
|
|
386
|
+
var CodexAdapter = class {
|
|
387
|
+
cliName = "codex";
|
|
388
|
+
displayName = "Codex CLI (OpenAI)";
|
|
389
|
+
skillsDirs = [
|
|
390
|
+
path7.join(HOME2, ".agents", "skills")
|
|
391
|
+
];
|
|
392
|
+
isInstalled() {
|
|
393
|
+
return fs7.existsSync(path7.join(HOME2, ".codex"));
|
|
394
|
+
}
|
|
395
|
+
scanSkills() {
|
|
396
|
+
return scanDirSkills(this.skillsDirs[0]);
|
|
397
|
+
}
|
|
398
|
+
disableSkill(name) {
|
|
399
|
+
const dir = findSkillDir(name, this.skillsDirs, true);
|
|
400
|
+
if (!dir) throw new Error(`Skill "${name}" not found in Codex skills directory (${this.skillsDirs[0]})`);
|
|
401
|
+
disableDirSkill(name, dir);
|
|
402
|
+
}
|
|
403
|
+
enableSkill(name) {
|
|
404
|
+
const dir = findSkillDir(name, this.skillsDirs, true);
|
|
405
|
+
if (!dir) throw new Error(`Skill "${name}" not found in Codex skills directory`);
|
|
406
|
+
enableDirSkill(name, dir);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// src/adapters/factory.ts
|
|
411
|
+
import * as fs8 from "fs";
|
|
412
|
+
import * as path8 from "path";
|
|
413
|
+
import * as os6 from "os";
|
|
414
|
+
var HOME3 = os6.homedir();
|
|
415
|
+
var FACTORY_DIR = path8.join(HOME3, ".factory");
|
|
416
|
+
var FactoryAdapter = class {
|
|
417
|
+
cliName = "droid";
|
|
418
|
+
displayName = "Factory Droid";
|
|
419
|
+
skillsDirs = [
|
|
420
|
+
path8.join(FACTORY_DIR, "droids"),
|
|
421
|
+
path8.join(FACTORY_DIR, "commands")
|
|
422
|
+
];
|
|
423
|
+
isInstalled() {
|
|
424
|
+
return fs8.existsSync(FACTORY_DIR);
|
|
425
|
+
}
|
|
426
|
+
scanSkills() {
|
|
427
|
+
return [
|
|
428
|
+
...scanFlatSkills(this.skillsDirs[0], "droid"),
|
|
429
|
+
...scanFlatSkills(this.skillsDirs[1], "command")
|
|
430
|
+
];
|
|
431
|
+
}
|
|
432
|
+
disableSkill(name) {
|
|
433
|
+
const dir = findSkillDir(name, this.skillsDirs, false);
|
|
434
|
+
if (!dir) throw new Error(`Skill "${name}" not found in Factory Droid directories`);
|
|
435
|
+
disableFlatSkill(name, dir);
|
|
436
|
+
}
|
|
437
|
+
enableSkill(name) {
|
|
438
|
+
const dir = findSkillDir(name, this.skillsDirs, false);
|
|
439
|
+
if (!dir) throw new Error(`Skill "${name}" not found in Factory Droid directories`);
|
|
440
|
+
enableFlatSkill(name, dir);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// src/adapters/amp.ts
|
|
445
|
+
import * as fs9 from "fs";
|
|
446
|
+
import * as path9 from "path";
|
|
447
|
+
import * as os7 from "os";
|
|
448
|
+
var HOME4 = os7.homedir();
|
|
449
|
+
var AmpAdapter = class {
|
|
450
|
+
cliName = "amp";
|
|
451
|
+
displayName = "Amp (Sourcegraph)";
|
|
452
|
+
skillsDirs = [
|
|
453
|
+
path9.join(HOME4, ".config", "amp", "skills"),
|
|
454
|
+
path9.join(HOME4, ".agents", "skills")
|
|
455
|
+
];
|
|
456
|
+
isInstalled() {
|
|
457
|
+
return fs9.existsSync(path9.join(HOME4, ".config", "amp"));
|
|
458
|
+
}
|
|
459
|
+
scanSkills() {
|
|
460
|
+
const seen = /* @__PURE__ */ new Set();
|
|
461
|
+
const skills = [];
|
|
462
|
+
for (const [i, dir] of this.skillsDirs.entries()) {
|
|
463
|
+
const group = i === 0 ? void 0 : "shared";
|
|
464
|
+
for (const s of scanDirSkills(dir, group)) {
|
|
465
|
+
if (!seen.has(s.name)) {
|
|
466
|
+
seen.add(s.name);
|
|
467
|
+
skills.push(s);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return skills;
|
|
472
|
+
}
|
|
473
|
+
disableSkill(name) {
|
|
474
|
+
const dir = findSkillDir(name, this.skillsDirs, true);
|
|
475
|
+
if (!dir) throw new Error(`Skill "${name}" not found in Amp skills directories`);
|
|
476
|
+
disableDirSkill(name, dir);
|
|
477
|
+
}
|
|
478
|
+
enableSkill(name) {
|
|
479
|
+
const dir = findSkillDir(name, this.skillsDirs, true);
|
|
480
|
+
if (!dir) throw new Error(`Skill "${name}" not found in Amp skills directories`);
|
|
481
|
+
enableDirSkill(name, dir);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// src/adapters/aider.ts
|
|
486
|
+
import * as fs10 from "fs";
|
|
487
|
+
import * as path10 from "path";
|
|
488
|
+
import * as os8 from "os";
|
|
489
|
+
var HOME5 = os8.homedir();
|
|
490
|
+
var AIDER_SKILLS_DIR = path10.join(HOME5, ".aider", "skills");
|
|
491
|
+
var AIDER_CONF = path10.join(HOME5, ".aider.conf.yml");
|
|
492
|
+
var AiderAdapter = class {
|
|
493
|
+
cliName = "aider";
|
|
494
|
+
displayName = "Aider";
|
|
495
|
+
skillsDirs = [AIDER_SKILLS_DIR];
|
|
496
|
+
isInstalled() {
|
|
497
|
+
return fs10.existsSync(AIDER_CONF) || fs10.existsSync(path10.join(HOME5, ".aider"));
|
|
498
|
+
}
|
|
499
|
+
scanSkills() {
|
|
500
|
+
return scanFlatSkills(AIDER_SKILLS_DIR);
|
|
501
|
+
}
|
|
502
|
+
disableSkill(name) {
|
|
503
|
+
disableFlatSkill(name, AIDER_SKILLS_DIR);
|
|
504
|
+
}
|
|
505
|
+
enableSkill(name) {
|
|
506
|
+
enableFlatSkill(name, AIDER_SKILLS_DIR);
|
|
507
|
+
}
|
|
508
|
+
/** Returns the `read:` block to paste into ~/.aider.conf.yml */
|
|
509
|
+
generateAiderConfig() {
|
|
510
|
+
const active = this.scanSkills().filter((s) => s.status === "active");
|
|
511
|
+
if (!active.length) return "# No active aider skills\n";
|
|
512
|
+
return `read:
|
|
513
|
+
${active.map((s) => ` - ${path10.join(AIDER_SKILLS_DIR, s.name + ".md")}`).join("\n")}
|
|
514
|
+
`;
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
// src/adapters/index.ts
|
|
519
|
+
import * as os9 from "os";
|
|
520
|
+
import * as path11 from "path";
|
|
521
|
+
var ADAPTERS = {
|
|
522
|
+
claude: (claudeDir) => new ClaudeAdapter(claudeDir ?? path11.join(os9.homedir(), ".claude")),
|
|
523
|
+
gemini: () => new GeminiAdapter(),
|
|
524
|
+
codex: () => new CodexAdapter(),
|
|
525
|
+
droid: () => new FactoryAdapter(),
|
|
526
|
+
amp: () => new AmpAdapter(),
|
|
527
|
+
aider: () => new AiderAdapter()
|
|
528
|
+
};
|
|
529
|
+
var CLI_NAMES = Object.keys(ADAPTERS);
|
|
530
|
+
function getAdapter(cli, claudeDir) {
|
|
531
|
+
const factory = ADAPTERS[cli.toLowerCase()];
|
|
532
|
+
if (!factory) throw new Error(`Unknown CLI "${cli}". Valid options: ${CLI_NAMES.join(", ")}`);
|
|
533
|
+
return factory(claudeDir);
|
|
534
|
+
}
|
|
535
|
+
function getAllAdapters() {
|
|
536
|
+
return CLI_NAMES.map((name) => ADAPTERS[name]());
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/profiles.ts
|
|
540
|
+
import * as fs11 from "fs";
|
|
541
|
+
import * as path12 from "path";
|
|
542
|
+
import { homedir as homedir11 } from "os";
|
|
543
|
+
var defaultClaudeDir4 = path12.join(homedir11(), ".claude");
|
|
171
544
|
function profileStorePath(claudeDir) {
|
|
172
|
-
return
|
|
545
|
+
return path12.join(claudeDir, "skillctl", "profiles.json");
|
|
173
546
|
}
|
|
174
547
|
function readProfileStore(claudeDir = defaultClaudeDir4) {
|
|
175
548
|
const filePath = profileStorePath(claudeDir);
|
|
176
549
|
try {
|
|
177
|
-
const raw =
|
|
550
|
+
const raw = fs11.readFileSync(filePath, "utf-8");
|
|
178
551
|
return JSON.parse(raw);
|
|
179
552
|
} catch (err) {
|
|
180
553
|
if (err.code === "ENOENT") {
|
|
181
|
-
return { active: null, profiles: {} };
|
|
554
|
+
return { active: null, previous: null, profiles: {} };
|
|
555
|
+
}
|
|
556
|
+
if (err instanceof SyntaxError) {
|
|
557
|
+
process.stderr.write("Warning: profiles.json is corrupt \u2014 resetting to empty store\n");
|
|
558
|
+
return { active: null, previous: null, profiles: {} };
|
|
182
559
|
}
|
|
183
560
|
throw err;
|
|
184
561
|
}
|
|
185
562
|
}
|
|
186
563
|
function writeProfileStore(store, claudeDir) {
|
|
187
564
|
const filePath = profileStorePath(claudeDir);
|
|
188
|
-
|
|
565
|
+
fs11.mkdirSync(path12.dirname(filePath), { recursive: true });
|
|
189
566
|
const tmp = filePath + ".tmp";
|
|
190
|
-
|
|
191
|
-
|
|
567
|
+
fs11.writeFileSync(tmp, JSON.stringify(store, null, 2));
|
|
568
|
+
fs11.renameSync(tmp, filePath);
|
|
192
569
|
}
|
|
193
570
|
function saveProfile(name, skills, plugins, claudeDir = defaultClaudeDir4) {
|
|
194
571
|
const store = readProfileStore(claudeDir);
|
|
195
572
|
const profile = {
|
|
196
573
|
created: store.profiles[name]?.created ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
574
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
197
575
|
skills,
|
|
198
576
|
plugins
|
|
199
577
|
};
|
|
@@ -207,21 +585,122 @@ function deleteProfile(name, claudeDir = defaultClaudeDir4) {
|
|
|
207
585
|
delete store.profiles[name];
|
|
208
586
|
writeProfileStore(store, claudeDir);
|
|
209
587
|
}
|
|
210
|
-
|
|
588
|
+
function renameProfile(oldName, newName, claudeDir = defaultClaudeDir4) {
|
|
589
|
+
const store = readProfileStore(claudeDir);
|
|
590
|
+
if (!store.profiles[oldName]) throw new Error(`Profile "${oldName}" does not exist`);
|
|
591
|
+
if (store.profiles[newName]) throw new Error(`Profile "${newName}" already exists`);
|
|
592
|
+
store.profiles[newName] = store.profiles[oldName];
|
|
593
|
+
delete store.profiles[oldName];
|
|
594
|
+
if (store.active === oldName) store.active = newName;
|
|
595
|
+
if (store.previous === oldName) store.previous = newName;
|
|
596
|
+
writeProfileStore(store, claudeDir);
|
|
597
|
+
}
|
|
598
|
+
function copyProfile(srcName, dstName, claudeDir = defaultClaudeDir4) {
|
|
599
|
+
const store = readProfileStore(claudeDir);
|
|
600
|
+
if (!store.profiles[srcName]) throw new Error(`Profile "${srcName}" does not exist`);
|
|
601
|
+
if (store.profiles[dstName]) throw new Error(`Profile "${dstName}" already exists`);
|
|
602
|
+
store.profiles[dstName] = {
|
|
603
|
+
...store.profiles[srcName],
|
|
604
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
605
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
606
|
+
};
|
|
607
|
+
writeProfileStore(store, claudeDir);
|
|
608
|
+
}
|
|
609
|
+
function diffProfile(name, claudeDir = defaultClaudeDir4) {
|
|
211
610
|
const store = readProfileStore(claudeDir);
|
|
212
611
|
const profile = store.profiles[name];
|
|
213
|
-
if (!profile) {
|
|
214
|
-
|
|
612
|
+
if (!profile) throw new Error(`Profile "${name}" does not exist`);
|
|
613
|
+
const profileSkills = new Set(profile.skills);
|
|
614
|
+
const skillsDir2 = path12.join(claudeDir, "skills");
|
|
615
|
+
const disabledDir2 = path12.join(claudeDir, "skills", ".disabled");
|
|
616
|
+
const toDisable = [];
|
|
617
|
+
const toEnable = [];
|
|
618
|
+
if (fs11.existsSync(skillsDir2)) {
|
|
619
|
+
for (const file of fs11.readdirSync(skillsDir2)) {
|
|
620
|
+
if (!file.endsWith(".md") || !fs11.statSync(path12.join(skillsDir2, file)).isFile()) continue;
|
|
621
|
+
const n = file.slice(0, -3);
|
|
622
|
+
if (!profileSkills.has(n)) toDisable.push(n);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (fs11.existsSync(disabledDir2)) {
|
|
626
|
+
for (const file of fs11.readdirSync(disabledDir2)) {
|
|
627
|
+
if (!file.endsWith(".md")) continue;
|
|
628
|
+
const n = file.slice(0, -3);
|
|
629
|
+
if (profileSkills.has(n)) toEnable.push(n);
|
|
630
|
+
}
|
|
215
631
|
}
|
|
632
|
+
const profilePlugins = new Set(profile.plugins);
|
|
633
|
+
const toBlock = [];
|
|
634
|
+
const toUnblock = [];
|
|
635
|
+
try {
|
|
636
|
+
const raw = JSON.parse(fs11.readFileSync(path12.join(claudeDir, "plugins", "installed_plugins.json"), "utf-8"));
|
|
637
|
+
const installed = Object.keys(raw?.plugins ?? {});
|
|
638
|
+
let currentlyBlocked = /* @__PURE__ */ new Set();
|
|
639
|
+
try {
|
|
640
|
+
const blRaw = JSON.parse(fs11.readFileSync(path12.join(claudeDir, "plugins", "blocklist.json"), "utf-8"));
|
|
641
|
+
currentlyBlocked = new Set(blRaw.plugins.map((e) => e.plugin));
|
|
642
|
+
} catch {
|
|
643
|
+
}
|
|
644
|
+
for (const id of installed) {
|
|
645
|
+
if (!profilePlugins.has(id) && !currentlyBlocked.has(id)) toBlock.push(id);
|
|
646
|
+
if (profilePlugins.has(id) && currentlyBlocked.has(id)) toUnblock.push(id);
|
|
647
|
+
}
|
|
648
|
+
} catch {
|
|
649
|
+
}
|
|
650
|
+
return { toEnable, toDisable, toBlock, toUnblock };
|
|
651
|
+
}
|
|
652
|
+
function exportProfile(name, claudeDir = defaultClaudeDir4) {
|
|
653
|
+
const store = readProfileStore(claudeDir);
|
|
654
|
+
const profile = store.profiles[name];
|
|
655
|
+
if (!profile) throw new Error(`Profile "${name}" does not exist`);
|
|
656
|
+
return JSON.stringify({ name, ...profile }, null, 2);
|
|
657
|
+
}
|
|
658
|
+
function importProfile(jsonStr, claudeDir = defaultClaudeDir4) {
|
|
659
|
+
let parsed;
|
|
660
|
+
try {
|
|
661
|
+
parsed = JSON.parse(jsonStr);
|
|
662
|
+
} catch {
|
|
663
|
+
throw new Error("Invalid JSON in import file");
|
|
664
|
+
}
|
|
665
|
+
if (!parsed.name || !Array.isArray(parsed.skills) || !Array.isArray(parsed.plugins)) {
|
|
666
|
+
throw new Error('Import file must have "name", "skills", and "plugins" fields');
|
|
667
|
+
}
|
|
668
|
+
saveProfile(parsed.name, parsed.skills, parsed.plugins, claudeDir);
|
|
669
|
+
return parsed.name;
|
|
670
|
+
}
|
|
671
|
+
function validateProfile(name, claudeDir = defaultClaudeDir4) {
|
|
672
|
+
const store = readProfileStore(claudeDir);
|
|
673
|
+
const profile = store.profiles[name];
|
|
674
|
+
if (!profile) throw new Error(`Profile "${name}" does not exist`);
|
|
675
|
+
const skillsDir2 = path12.join(claudeDir, "skills");
|
|
676
|
+
const disabledDir2 = path12.join(claudeDir, "skills", ".disabled");
|
|
677
|
+
const onDisk = /* @__PURE__ */ new Set();
|
|
678
|
+
for (const dir of [skillsDir2, disabledDir2]) {
|
|
679
|
+
if (!fs11.existsSync(dir)) continue;
|
|
680
|
+
for (const file of fs11.readdirSync(dir)) {
|
|
681
|
+
if (file.endsWith(".md")) onDisk.add(file.slice(0, -3));
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const validSkills = [];
|
|
685
|
+
const ghostSkills = [];
|
|
686
|
+
for (const skill of profile.skills) {
|
|
687
|
+
(onDisk.has(skill) ? validSkills : ghostSkills).push(skill);
|
|
688
|
+
}
|
|
689
|
+
return { validSkills, ghostSkills };
|
|
690
|
+
}
|
|
691
|
+
async function activateProfile(name, claudeDir = defaultClaudeDir4) {
|
|
692
|
+
const store = readProfileStore(claudeDir);
|
|
693
|
+
const profile = store.profiles[name];
|
|
694
|
+
if (!profile) throw new Error(`Profile "${name}" does not exist`);
|
|
216
695
|
const profileSkills = new Set(profile.skills);
|
|
217
|
-
const skillsDir2 =
|
|
218
|
-
const disabledDir2 =
|
|
696
|
+
const skillsDir2 = path12.join(claudeDir, "skills");
|
|
697
|
+
const disabledDir2 = path12.join(claudeDir, "skills", ".disabled");
|
|
219
698
|
const enabled = [];
|
|
220
699
|
const disabled = [];
|
|
221
|
-
if (
|
|
222
|
-
for (const file of
|
|
700
|
+
if (fs11.existsSync(skillsDir2)) {
|
|
701
|
+
for (const file of fs11.readdirSync(skillsDir2)) {
|
|
223
702
|
if (!file.endsWith(".md")) continue;
|
|
224
|
-
const stat =
|
|
703
|
+
const stat = fs11.statSync(path12.join(skillsDir2, file));
|
|
225
704
|
if (!stat.isFile()) continue;
|
|
226
705
|
const skillName = file.slice(0, -3);
|
|
227
706
|
if (!profileSkills.has(skillName)) {
|
|
@@ -230,8 +709,8 @@ async function activateProfile(name, claudeDir = defaultClaudeDir4) {
|
|
|
230
709
|
}
|
|
231
710
|
}
|
|
232
711
|
}
|
|
233
|
-
if (
|
|
234
|
-
for (const file of
|
|
712
|
+
if (fs11.existsSync(disabledDir2)) {
|
|
713
|
+
for (const file of fs11.readdirSync(disabledDir2)) {
|
|
235
714
|
if (!file.endsWith(".md")) continue;
|
|
236
715
|
const skillName = file.slice(0, -3);
|
|
237
716
|
if (profileSkills.has(skillName)) {
|
|
@@ -241,26 +720,27 @@ async function activateProfile(name, claudeDir = defaultClaudeDir4) {
|
|
|
241
720
|
}
|
|
242
721
|
}
|
|
243
722
|
const profilePlugins = new Set(profile.plugins);
|
|
244
|
-
const installedPath =
|
|
723
|
+
const installedPath = path12.join(claudeDir, "plugins", "installed_plugins.json");
|
|
245
724
|
let pluginsBlocked = [];
|
|
246
725
|
try {
|
|
247
|
-
const raw = JSON.parse(
|
|
726
|
+
const raw = JSON.parse(fs11.readFileSync(installedPath, "utf-8"));
|
|
248
727
|
const allInstalled = Object.keys(raw?.plugins ?? {});
|
|
249
728
|
pluginsBlocked = allInstalled.filter((id) => !profilePlugins.has(id));
|
|
250
729
|
} catch (err) {
|
|
251
730
|
if (err.code !== "ENOENT") throw err;
|
|
252
731
|
}
|
|
253
732
|
await setBlockedPlugins(pluginsBlocked, `blocked by profile: ${name}`, claudeDir);
|
|
733
|
+
store.previous = store.active;
|
|
254
734
|
store.active = name;
|
|
255
735
|
writeProfileStore(store, claudeDir);
|
|
256
736
|
return { enabled, disabled, pluginsBlocked };
|
|
257
737
|
}
|
|
258
738
|
|
|
259
739
|
// src/catalog.ts
|
|
260
|
-
import * as
|
|
261
|
-
import * as
|
|
262
|
-
import * as
|
|
263
|
-
var defaultClaudeDir5 =
|
|
740
|
+
import * as fs12 from "fs";
|
|
741
|
+
import * as path13 from "path";
|
|
742
|
+
import * as os10 from "os";
|
|
743
|
+
var defaultClaudeDir5 = path13.join(os10.homedir(), ".claude");
|
|
264
744
|
function generateCatalog(claudeDir = defaultClaudeDir5) {
|
|
265
745
|
const standalone = scanStandaloneSkills(claudeDir);
|
|
266
746
|
const plugins = scanPlugins(claudeDir);
|
|
@@ -290,13 +770,26 @@ function generateCatalog(claudeDir = defaultClaudeDir5) {
|
|
|
290
770
|
sections.push(`## Plugin: ${p.name} \u2014 DISABLED (${p.skills.length})`, "| Skill | Description |", "|-------|-------------|", rows(p.skills), "");
|
|
291
771
|
}
|
|
292
772
|
const content = sections.join("\n");
|
|
293
|
-
|
|
773
|
+
fs12.writeFileSync(path13.join(claudeDir, "SKILLS.md"), content);
|
|
294
774
|
return content;
|
|
295
775
|
}
|
|
296
776
|
|
|
297
777
|
// src/cli.ts
|
|
778
|
+
process.on("unhandledRejection", (err) => {
|
|
779
|
+
process.stderr.write(`Error: ${err.message ?? err}
|
|
780
|
+
`);
|
|
781
|
+
process.exit(1);
|
|
782
|
+
});
|
|
783
|
+
function getClaudeDir(opts) {
|
|
784
|
+
return opts["claudeDir"] ?? process.env["SKILLSWITCH_CLAUDE_DIR"] ?? defaultClaudeDir;
|
|
785
|
+
}
|
|
786
|
+
function validateProfileName(name) {
|
|
787
|
+
if (!name || name.trim() === "") throw new Error("Profile name cannot be empty");
|
|
788
|
+
if (name.length > 64) throw new Error("Profile name must be 64 characters or less");
|
|
789
|
+
if (/[/\\]/.test(name)) throw new Error('Profile name cannot contain "/" or "\\"');
|
|
790
|
+
}
|
|
298
791
|
var program = new Command();
|
|
299
|
-
program.name("skillswitch").description(
|
|
792
|
+
program.name("skillswitch").description(`Manage AI CLI skills \u2014 Claude Code, Gemini CLI, Codex CLI, Factory Droid, Amp, Aider`).version("0.1.3").option("--claude-dir <path>", "Override the Claude config directory (default: ~/.claude)").option("--for <cli>", `Target CLI: ${CLI_NAMES.join(" | ")} (default: claude)`);
|
|
300
793
|
function confirm(prompt) {
|
|
301
794
|
return new Promise((resolve) => {
|
|
302
795
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -306,25 +799,84 @@ function confirm(prompt) {
|
|
|
306
799
|
});
|
|
307
800
|
});
|
|
308
801
|
}
|
|
309
|
-
program.command("list").description("Show all skills grouped by source").option("--disabled", "Show only disabled skills").action((opts) => {
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
|
|
802
|
+
program.command("list").description("Show all skills grouped by source").option("--disabled", "Show only disabled skills").option("--enabled", "Show only enabled (active) skills").option("--json", "Output as JSON").action((opts) => {
|
|
803
|
+
const globalOpts = program.opts();
|
|
804
|
+
const cliTarget = globalOpts["for"] ?? "claude";
|
|
805
|
+
if (cliTarget !== "claude") {
|
|
806
|
+
const adapter = getAdapter(cliTarget);
|
|
807
|
+
const statusFilter2 = opts.disabled ? "disabled" : opts.enabled ? "active" : null;
|
|
808
|
+
let skills = adapter.scanSkills();
|
|
809
|
+
if (statusFilter2) skills = skills.filter((s) => s.status === statusFilter2);
|
|
810
|
+
if (opts.json) {
|
|
811
|
+
process.stdout.write(JSON.stringify(skills, null, 2) + "\n");
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
console.log(`
|
|
815
|
+
${adapter.displayName} skills (${skills.length}):`);
|
|
816
|
+
for (const s of skills) console.log(` ${s.name}${s.group ? ` [${s.group}]` : ""}${s.status === "disabled" ? " [disabled]" : ""}`);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const claudeDir = getClaudeDir(globalOpts);
|
|
820
|
+
const standalone = scanStandaloneSkills(claudeDir);
|
|
821
|
+
const plugins = scanPlugins(claudeDir);
|
|
822
|
+
if (opts.json) {
|
|
823
|
+
const filter = opts.disabled ? "disabled" : opts.enabled ? "active" : null;
|
|
824
|
+
const ss2 = filter ? standalone.filter((s) => s.status === filter) : standalone;
|
|
825
|
+
const pp2 = filter ? plugins.filter((p) => p.status === (filter === "active" ? "active" : "disabled")) : plugins;
|
|
826
|
+
process.stdout.write(JSON.stringify({ standalone: ss2, plugins: pp2 }, null, 2) + "\n");
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const statusFilter = opts.disabled ? "disabled" : opts.enabled ? "active" : null;
|
|
830
|
+
const ss = statusFilter ? standalone.filter((s) => s.status === statusFilter) : standalone;
|
|
313
831
|
console.log(`
|
|
314
832
|
Standalone (${standalone.length} total):`);
|
|
315
833
|
for (const s of ss) console.log(` ${s.name}${s.status === "disabled" ? " [disabled]" : ""}`);
|
|
316
|
-
const pp =
|
|
317
|
-
for (const p of pp)
|
|
834
|
+
const pp = statusFilter ? plugins.filter((p) => p.status === statusFilter) : plugins;
|
|
835
|
+
for (const p of pp) {
|
|
836
|
+
console.log(`
|
|
318
837
|
${p.id}${p.status === "disabled" ? " [DISABLED]" : ""} (${p.skills.length} skills)`);
|
|
838
|
+
for (const s of p.skills) console.log(` ${s.name}${s.status === "disabled" ? " [disabled]" : ""}`);
|
|
839
|
+
}
|
|
319
840
|
});
|
|
320
|
-
program.command("search <query>").description("Search skills by name or description (substring match)").action((query) => {
|
|
841
|
+
program.command("search <query>").description("Search skills by name or description (substring match)").option("--status <status>", "Filter by status: active or disabled").option("--json", "Output as JSON").action((query, opts) => {
|
|
842
|
+
const globalOpts = program.opts();
|
|
843
|
+
const cliTarget = globalOpts["for"] ?? "claude";
|
|
321
844
|
const q = query.toLowerCase();
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
845
|
+
if (cliTarget !== "claude") {
|
|
846
|
+
const adapter = getAdapter(cliTarget);
|
|
847
|
+
let matches2 = adapter.scanSkills().filter((s) => s.name.includes(q) || s.description.toLowerCase().includes(q));
|
|
848
|
+
if (opts.status) matches2 = matches2.filter((s) => s.status === opts.status);
|
|
849
|
+
if (opts.json) {
|
|
850
|
+
process.stdout.write(JSON.stringify(matches2, null, 2) + "\n");
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (!matches2.length) {
|
|
854
|
+
console.log(`No ${adapter.displayName} skills matching "${query}".`);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
console.log(`
|
|
858
|
+
${matches2.length} match(es) for "${query}" in ${adapter.displayName}:
|
|
859
|
+
`);
|
|
860
|
+
for (const s of matches2) {
|
|
861
|
+
console.log(` ${s.name} [${s.status}]${s.group ? ` (${s.group})` : ""}`);
|
|
862
|
+
if (s.description) console.log(` ${s.description}`);
|
|
863
|
+
}
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const claudeDir = getClaudeDir(globalOpts);
|
|
867
|
+
const standalone = scanStandaloneSkills(claudeDir);
|
|
868
|
+
const plugins = scanPlugins(claudeDir);
|
|
869
|
+
let matches = [
|
|
325
870
|
...standalone.filter((s) => s.name.includes(q) || s.description.toLowerCase().includes(q)),
|
|
326
871
|
...plugins.flatMap((p) => p.skills).filter((s) => s.name.includes(q) || s.description.toLowerCase().includes(q))
|
|
327
872
|
];
|
|
873
|
+
if (opts.status) {
|
|
874
|
+
matches = matches.filter((s) => s.status === opts.status);
|
|
875
|
+
}
|
|
876
|
+
if (opts.json) {
|
|
877
|
+
process.stdout.write(JSON.stringify(matches, null, 2) + "\n");
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
328
880
|
if (!matches.length) {
|
|
329
881
|
console.log(`No skills matching "${query}".`);
|
|
330
882
|
return;
|
|
@@ -337,101 +889,273 @@ ${matches.length} match(es) for "${query}":
|
|
|
337
889
|
if (s.description) console.log(` ${s.description}`);
|
|
338
890
|
}
|
|
339
891
|
});
|
|
340
|
-
program.command("status").description("Show active profile and skill counts").action(() => {
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
|
|
892
|
+
program.command("status").description("Show active profile and skill counts").option("--json", "Output as JSON").action((opts) => {
|
|
893
|
+
const globalOpts = program.opts();
|
|
894
|
+
const cliTarget = globalOpts["for"] ?? "claude";
|
|
895
|
+
if (cliTarget !== "claude") {
|
|
896
|
+
const adapter = getAdapter(cliTarget);
|
|
897
|
+
const skills = adapter.scanSkills();
|
|
898
|
+
const active2 = skills.filter((s) => s.status === "active").length;
|
|
899
|
+
const disabled2 = skills.filter((s) => s.status === "disabled").length;
|
|
900
|
+
if (opts.json) {
|
|
901
|
+
process.stdout.write(JSON.stringify({ cli: adapter.displayName, active: active2, disabled: disabled2, total: active2 + disabled2 }, null, 2) + "\n");
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
console.log(`CLI : ${adapter.displayName}`);
|
|
905
|
+
console.log(`Enabled skills : ${active2}`);
|
|
906
|
+
console.log(`Disabled skills: ${disabled2}`);
|
|
907
|
+
console.log(`Total : ${active2 + disabled2}`);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
const claudeDir = getClaudeDir(globalOpts);
|
|
911
|
+
const store = readProfileStore(claudeDir);
|
|
912
|
+
const standalone = scanStandaloneSkills(claudeDir);
|
|
913
|
+
const plugins = scanPlugins(claudeDir);
|
|
344
914
|
const active = standalone.filter((s) => s.status === "active").length + plugins.filter((p) => p.status === "active").reduce((n, p) => n + p.skills.length, 0);
|
|
345
915
|
const disabled = standalone.filter((s) => s.status === "disabled").length + plugins.filter((p) => p.status === "disabled").reduce((n, p) => n + p.skills.length, 0);
|
|
916
|
+
if (opts.json) {
|
|
917
|
+
process.stdout.write(JSON.stringify({ activeProfile: store.active, active, disabled, total: active + disabled }, null, 2) + "\n");
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
346
920
|
console.log(`Active profile : ${store.active ?? "none"}`);
|
|
347
921
|
console.log(`Enabled skills : ${active}`);
|
|
348
922
|
console.log(`Disabled skills: ${disabled}`);
|
|
349
923
|
console.log(`Total : ${active + disabled}`);
|
|
350
924
|
});
|
|
351
|
-
program.command("disable <name>").description("Disable a skill (substring match) or a plugin (--plugin)").option("--plugin", "Treat <name> as a full plugin ID (name@source)").option("--dry-run", "Preview without making changes").action(async (name, opts) => {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
925
|
+
program.command("disable <name>").description("Disable a skill (substring match) or a plugin (--plugin)").option("--plugin", "Treat <name> as a full plugin ID (name@source)").option("--exact", "Require exact name match instead of substring match").option("--dry-run", "Preview without making changes").option("--quiet", "Suppress output except errors").action(async (name, opts) => {
|
|
926
|
+
const globalOpts = program.opts();
|
|
927
|
+
const cliTarget = globalOpts["for"] ?? "claude";
|
|
928
|
+
const log = (msg) => {
|
|
929
|
+
if (!opts.quiet) console.log(msg);
|
|
930
|
+
};
|
|
931
|
+
if (cliTarget !== "claude") {
|
|
932
|
+
try {
|
|
933
|
+
const adapter = getAdapter(cliTarget);
|
|
934
|
+
const matches = adapter.scanSkills().filter((s) => {
|
|
935
|
+
const hit = opts.exact ? s.name === name : s.name.includes(name);
|
|
936
|
+
return hit && s.status === "active";
|
|
937
|
+
});
|
|
938
|
+
if (!matches.length) {
|
|
939
|
+
console.log(`No active ${adapter.displayName} skills matching "${name}".`);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (matches.length > 1) {
|
|
943
|
+
console.log(`Matches: ${matches.map((s) => s.name).join(", ")}`);
|
|
944
|
+
if (!await confirm(`Disable all ${matches.length}? (y/N) `)) {
|
|
945
|
+
console.log("Aborted.");
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
for (const s of matches) {
|
|
950
|
+
if (opts.dryRun) log(`[dry-run] Would disable: ${s.name}`);
|
|
951
|
+
else {
|
|
952
|
+
adapter.disableSkill(s.name);
|
|
953
|
+
log(`Disabled: ${s.name}`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
} catch (err) {
|
|
957
|
+
console.error(`Error: ${err.message}`);
|
|
958
|
+
process.exit(1);
|
|
356
959
|
}
|
|
357
|
-
await blockPlugin(name, "skillswitch: manually disabled");
|
|
358
|
-
console.log(`Plugin blocked: ${name}`);
|
|
359
960
|
return;
|
|
360
961
|
}
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
962
|
+
const claudeDir = getClaudeDir(globalOpts);
|
|
963
|
+
try {
|
|
964
|
+
if (opts.plugin) {
|
|
965
|
+
const plugins = scanPlugins(claudeDir);
|
|
966
|
+
const matched = plugins.filter(
|
|
967
|
+
(p) => opts.exact ? p.id === name : p.id.includes(name)
|
|
968
|
+
);
|
|
969
|
+
if (!matched.length) {
|
|
970
|
+
console.log(`No plugins matching "${name}".`);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
if (matched.length > 1 && !opts.exact) {
|
|
974
|
+
console.log(`Matches: ${matched.map((p) => p.id).join(", ")}`);
|
|
975
|
+
if (!await confirm(`Disable all ${matched.length} plugins? (y/N) `)) {
|
|
976
|
+
console.log("Aborted.");
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
for (const p of matched) {
|
|
981
|
+
if (opts.dryRun) log(`[dry-run] Would block plugin: ${p.id}`);
|
|
982
|
+
else {
|
|
983
|
+
await blockPlugin(p.id, "skillswitch: manually disabled", claudeDir);
|
|
984
|
+
log(`Plugin blocked: ${p.id}`);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
370
987
|
return;
|
|
371
988
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
console.log(`
|
|
989
|
+
const matches = scanStandaloneSkills(claudeDir).filter((s) => {
|
|
990
|
+
const hit = opts.exact ? s.name === name : s.name.includes(name);
|
|
991
|
+
return hit && s.status === "active";
|
|
992
|
+
});
|
|
993
|
+
if (!matches.length) {
|
|
994
|
+
console.log(`No active skills matching "${name}".`);
|
|
995
|
+
return;
|
|
378
996
|
}
|
|
997
|
+
if (matches.length > 1) {
|
|
998
|
+
console.log(`Matches: ${matches.map((s) => s.name).join(", ")}`);
|
|
999
|
+
if (!await confirm(`Disable all ${matches.length}? (y/N) `)) {
|
|
1000
|
+
console.log("Aborted.");
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
for (const s of matches) {
|
|
1005
|
+
if (opts.dryRun) log(`[dry-run] Would disable: ${s.name}`);
|
|
1006
|
+
else {
|
|
1007
|
+
disableSkill(s.name, claudeDir);
|
|
1008
|
+
log(`Disabled: ${s.name}`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
} catch (err) {
|
|
1012
|
+
console.error(`Error: ${err.message}`);
|
|
1013
|
+
process.exit(1);
|
|
379
1014
|
}
|
|
380
1015
|
});
|
|
381
|
-
program.command("enable <name>").description("Enable a skill (substring match) or a plugin (--plugin)").option("--plugin", "Treat <name> as a full plugin ID (name@source)").option("--dry-run", "Preview without making changes").action(async (name, opts) => {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
1016
|
+
program.command("enable <name>").description("Enable a skill (substring match) or a plugin (--plugin)").option("--plugin", "Treat <name> as a full plugin ID (name@source)").option("--exact", "Require exact name match instead of substring match").option("--dry-run", "Preview without making changes").option("--quiet", "Suppress output except errors").action(async (name, opts) => {
|
|
1017
|
+
const globalOpts = program.opts();
|
|
1018
|
+
const cliTarget = globalOpts["for"] ?? "claude";
|
|
1019
|
+
const log = (msg) => {
|
|
1020
|
+
if (!opts.quiet) console.log(msg);
|
|
1021
|
+
};
|
|
1022
|
+
if (cliTarget !== "claude") {
|
|
1023
|
+
try {
|
|
1024
|
+
const adapter = getAdapter(cliTarget);
|
|
1025
|
+
const matches = adapter.scanSkills().filter((s) => {
|
|
1026
|
+
const hit = opts.exact ? s.name === name : s.name.includes(name);
|
|
1027
|
+
return hit && s.status === "disabled";
|
|
1028
|
+
});
|
|
1029
|
+
if (!matches.length) {
|
|
1030
|
+
console.log(`No disabled ${adapter.displayName} skills matching "${name}".`);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (matches.length > 1) {
|
|
1034
|
+
console.log(`Matches: ${matches.map((s) => s.name).join(", ")}`);
|
|
1035
|
+
if (!await confirm(`Enable all ${matches.length}? (y/N) `)) {
|
|
1036
|
+
console.log("Aborted.");
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
for (const s of matches) {
|
|
1041
|
+
if (opts.dryRun) log(`[dry-run] Would enable: ${s.name}`);
|
|
1042
|
+
else {
|
|
1043
|
+
adapter.enableSkill(s.name);
|
|
1044
|
+
log(`Enabled: ${s.name}`);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
} catch (err) {
|
|
1048
|
+
console.error(`Error: ${err.message}`);
|
|
1049
|
+
process.exit(1);
|
|
386
1050
|
}
|
|
387
|
-
await unblockPlugin(name);
|
|
388
|
-
console.log(`Plugin unblocked: ${name}`);
|
|
389
1051
|
return;
|
|
390
1052
|
}
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
1053
|
+
const claudeDir = getClaudeDir(globalOpts);
|
|
1054
|
+
try {
|
|
1055
|
+
if (opts.plugin) {
|
|
1056
|
+
const plugins = scanPlugins(claudeDir);
|
|
1057
|
+
const matched = plugins.filter(
|
|
1058
|
+
(p) => opts.exact ? p.id === name : p.id.includes(name)
|
|
1059
|
+
);
|
|
1060
|
+
if (!matched.length) {
|
|
1061
|
+
console.log(`No plugins matching "${name}".`);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
for (const p of matched) {
|
|
1065
|
+
if (opts.dryRun) {
|
|
1066
|
+
log(`[dry-run] Would unblock plugin: ${p.id}`);
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
const wasBlocked = await unblockPlugin(p.id, claudeDir);
|
|
1070
|
+
log(wasBlocked ? `Plugin unblocked: ${p.id}` : `Plugin was not blocked: ${p.id}`);
|
|
1071
|
+
}
|
|
400
1072
|
return;
|
|
401
1073
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
console.log(`
|
|
1074
|
+
const matches = scanStandaloneSkills(claudeDir).filter((s) => {
|
|
1075
|
+
const hit = opts.exact ? s.name === name : s.name.includes(name);
|
|
1076
|
+
return hit && s.status === "disabled";
|
|
1077
|
+
});
|
|
1078
|
+
if (!matches.length) {
|
|
1079
|
+
console.log(`No disabled skills matching "${name}".`);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (matches.length > 1) {
|
|
1083
|
+
console.log(`Matches: ${matches.map((s) => s.name).join(", ")}`);
|
|
1084
|
+
if (!await confirm(`Enable all ${matches.length}? (y/N) `)) {
|
|
1085
|
+
console.log("Aborted.");
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
for (const s of matches) {
|
|
1090
|
+
if (opts.dryRun) log(`[dry-run] Would enable: ${s.name}`);
|
|
1091
|
+
else {
|
|
1092
|
+
enableSkill(s.name, claudeDir);
|
|
1093
|
+
log(`Enabled: ${s.name}`);
|
|
1094
|
+
}
|
|
408
1095
|
}
|
|
1096
|
+
} catch (err) {
|
|
1097
|
+
console.error(`Error: ${err.message}`);
|
|
1098
|
+
process.exit(1);
|
|
409
1099
|
}
|
|
410
1100
|
});
|
|
411
1101
|
var profileCmd = program.command("profile").description("Manage skill profiles");
|
|
412
1102
|
profileCmd.command("create <name>").description("Snapshot current enabled skills as a named profile").action((name) => {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
1103
|
+
try {
|
|
1104
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
1105
|
+
validateProfileName(name);
|
|
1106
|
+
const skills = scanStandaloneSkills(claudeDir).filter((s) => s.status === "active").map((s) => s.name);
|
|
1107
|
+
const plugins = scanPlugins(claudeDir).filter((p) => p.status === "active").map((p) => p.id);
|
|
1108
|
+
const store = readProfileStore(claudeDir);
|
|
1109
|
+
if (store.profiles[name]) console.log(`Overwriting existing profile "${name}".`);
|
|
1110
|
+
if (!skills.length && !plugins.length) console.warn(`Warning: no active skills or plugins found \u2014 profile may be empty by mistake.`);
|
|
1111
|
+
saveProfile(name, skills, plugins, claudeDir);
|
|
1112
|
+
console.log(`Profile "${name}" saved: ${skills.length} skills, ${plugins.length} plugins.`);
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
console.error(`Error: ${err.message}`);
|
|
1115
|
+
process.exit(1);
|
|
1116
|
+
}
|
|
417
1117
|
});
|
|
418
|
-
profileCmd.command("use <name>").description("Activate a profile").option("--dry-run", "Preview without making changes").action(async (name, opts) => {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
1118
|
+
profileCmd.command("use <name>").description("Activate a profile").option("--dry-run", "Preview without making changes").option("--quiet", "Suppress output except errors").action(async (name, opts) => {
|
|
1119
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
1120
|
+
try {
|
|
1121
|
+
if (opts.dryRun) {
|
|
1122
|
+
const store = readProfileStore(claudeDir);
|
|
1123
|
+
const p = store.profiles[name];
|
|
1124
|
+
if (!p) {
|
|
1125
|
+
console.log(`Profile "${name}" not found.`);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
const diff = diffProfile(name, claudeDir);
|
|
1129
|
+
console.log(`[dry-run] Activating "${name}":`);
|
|
1130
|
+
if (diff.toEnable.length) console.log(` Enable : ${diff.toEnable.join(", ")}`);
|
|
1131
|
+
if (diff.toDisable.length) console.log(` Disable : ${diff.toDisable.join(", ")}`);
|
|
1132
|
+
if (diff.toBlock.length) console.log(` Block : ${diff.toBlock.join(", ")}`);
|
|
1133
|
+
if (diff.toUnblock.length) console.log(` Unblock : ${diff.toUnblock.join(", ")}`);
|
|
1134
|
+
if (!diff.toEnable.length && !diff.toDisable.length && !diff.toBlock.length && !diff.toUnblock.length) {
|
|
1135
|
+
console.log(" (no changes)");
|
|
1136
|
+
}
|
|
424
1137
|
return;
|
|
425
1138
|
}
|
|
426
|
-
|
|
427
|
-
|
|
1139
|
+
const result = await activateProfile(name, claudeDir);
|
|
1140
|
+
if (!opts.quiet) {
|
|
1141
|
+
console.log(`Activated "${name}".`);
|
|
1142
|
+
if (result.enabled.length) console.log(` Enabled : ${result.enabled.join(", ")}`);
|
|
1143
|
+
if (result.disabled.length) console.log(` Disabled: ${result.disabled.join(", ")}`);
|
|
1144
|
+
if (result.pluginsBlocked.length) console.log(` Blocked : ${result.pluginsBlocked.join(", ")}`);
|
|
1145
|
+
}
|
|
1146
|
+
} catch (err) {
|
|
1147
|
+
console.error(`Error: ${err.message}`);
|
|
1148
|
+
process.exit(1);
|
|
428
1149
|
}
|
|
429
|
-
const result = await activateProfile(name);
|
|
430
|
-
console.log(`Activated "${name}": ${result.disabled.length} disabled, ${result.enabled.length} enabled, ${result.pluginsBlocked.length} plugins blocked.`);
|
|
431
1150
|
});
|
|
432
|
-
profileCmd.command("list").description("List saved profiles").action(() => {
|
|
433
|
-
const
|
|
1151
|
+
profileCmd.command("list").description("List saved profiles").option("--json", "Output as JSON").action((opts) => {
|
|
1152
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
1153
|
+
const store = readProfileStore(claudeDir);
|
|
434
1154
|
const names = Object.keys(store.profiles);
|
|
1155
|
+
if (opts.json) {
|
|
1156
|
+
process.stdout.write(JSON.stringify({ active: store.active, previous: store.previous, profiles: store.profiles }, null, 2) + "\n");
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
435
1159
|
if (!names.length) {
|
|
436
1160
|
console.log("No profiles saved.");
|
|
437
1161
|
return;
|
|
@@ -443,33 +1167,148 @@ profileCmd.command("list").description("List saved profiles").action(() => {
|
|
|
443
1167
|
}
|
|
444
1168
|
});
|
|
445
1169
|
profileCmd.command("show <name>").description("Show skills in a profile").action((name) => {
|
|
446
|
-
const
|
|
1170
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
1171
|
+
const store = readProfileStore(claudeDir);
|
|
447
1172
|
const p = store.profiles[name];
|
|
448
1173
|
if (!p) {
|
|
449
1174
|
console.log(`Profile "${name}" not found.`);
|
|
450
1175
|
return;
|
|
451
1176
|
}
|
|
452
|
-
console.log(`Profile "${name}" (${p.created.slice(0, 10)}):`);
|
|
1177
|
+
console.log(`Profile "${name}" (created ${p.created.slice(0, 10)}):`);
|
|
453
1178
|
console.log(` Skills (${p.skills.length}): ${p.skills.join(", ") || "none"}`);
|
|
454
1179
|
console.log(` Plugins (${p.plugins.length}): ${p.plugins.join(", ") || "none"}`);
|
|
455
1180
|
});
|
|
456
|
-
profileCmd.command("delete <name>").description("Delete a saved profile").action((name) => {
|
|
1181
|
+
profileCmd.command("delete <name>").description("Delete a saved profile").option("--force", "Delete even if this is the active profile").action((name, opts) => {
|
|
457
1182
|
try {
|
|
458
|
-
|
|
1183
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
1184
|
+
if (opts.force) {
|
|
1185
|
+
const store = readProfileStore(claudeDir);
|
|
1186
|
+
if (store.active === name) {
|
|
1187
|
+
store.active = null;
|
|
1188
|
+
if (!store.profiles[name]) {
|
|
1189
|
+
console.log(`Profile "${name}" not found.`);
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
delete store.profiles[name];
|
|
1193
|
+
const storeFile = claudeDir + "/skillctl/profiles.json";
|
|
1194
|
+
const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync4, renameSync: renameSync4 } = fs13;
|
|
1195
|
+
const dir = storeFile.replace(/\/[^/]+$/, "");
|
|
1196
|
+
mkdirSync4(dir, { recursive: true });
|
|
1197
|
+
writeFileSync4(storeFile + ".tmp", JSON.stringify(store, null, 2));
|
|
1198
|
+
renameSync4(storeFile + ".tmp", storeFile);
|
|
1199
|
+
console.log(`Profile "${name}" deleted.`);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
deleteProfile(name, claudeDir);
|
|
459
1204
|
console.log(`Profile "${name}" deleted.`);
|
|
460
|
-
} catch (
|
|
461
|
-
console.error(
|
|
1205
|
+
} catch (err) {
|
|
1206
|
+
console.error(err.message);
|
|
1207
|
+
process.exit(1);
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
profileCmd.command("rename <old> <new>").description("Rename a profile").action((oldName, newName) => {
|
|
1211
|
+
try {
|
|
1212
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
1213
|
+
validateProfileName(newName);
|
|
1214
|
+
renameProfile(oldName, newName, claudeDir);
|
|
1215
|
+
console.log(`Profile "${oldName}" renamed to "${newName}".`);
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
console.error(`Error: ${err.message}`);
|
|
462
1218
|
process.exit(1);
|
|
463
1219
|
}
|
|
464
1220
|
});
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
1221
|
+
profileCmd.command("copy <src> <dst>").description("Copy a profile to a new name").action((srcName, dstName) => {
|
|
1222
|
+
try {
|
|
1223
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
1224
|
+
validateProfileName(dstName);
|
|
1225
|
+
copyProfile(srcName, dstName, claudeDir);
|
|
1226
|
+
console.log(`Profile "${srcName}" copied to "${dstName}".`);
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
console.error(`Error: ${err.message}`);
|
|
1229
|
+
process.exit(1);
|
|
1230
|
+
}
|
|
469
1231
|
});
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
1232
|
+
profileCmd.command("diff <name>").description("Show what would change when activating a profile").action((name) => {
|
|
1233
|
+
try {
|
|
1234
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
1235
|
+
const diff = diffProfile(name, claudeDir);
|
|
1236
|
+
if (!diff.toEnable.length && !diff.toDisable.length && !diff.toBlock.length && !diff.toUnblock.length) {
|
|
1237
|
+
console.log(`Profile "${name}" matches current state \u2014 no changes needed.`);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
console.log(`Diff for profile "${name}":`);
|
|
1241
|
+
if (diff.toEnable.length) console.log(` Enable (+): ${diff.toEnable.join(", ")}`);
|
|
1242
|
+
if (diff.toDisable.length) console.log(` Disable (-): ${diff.toDisable.join(", ")}`);
|
|
1243
|
+
if (diff.toBlock.length) console.log(` Block (-): ${diff.toBlock.join(", ")}`);
|
|
1244
|
+
if (diff.toUnblock.length) console.log(` Unblock (+): ${diff.toUnblock.join(", ")}`);
|
|
1245
|
+
} catch (err) {
|
|
1246
|
+
console.error(`Error: ${err.message}`);
|
|
1247
|
+
process.exit(1);
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
profileCmd.command("export <name>").description("Export a profile to stdout or a file").option("--out <file>", "Write to file instead of stdout").action((name, opts) => {
|
|
1251
|
+
try {
|
|
1252
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
1253
|
+
const json = exportProfile(name, claudeDir);
|
|
1254
|
+
if (opts.out) {
|
|
1255
|
+
fs13.writeFileSync(opts.out, json);
|
|
1256
|
+
console.log(`Profile "${name}" exported to ${opts.out}`);
|
|
1257
|
+
} else {
|
|
1258
|
+
process.stdout.write(json + "\n");
|
|
1259
|
+
}
|
|
1260
|
+
} catch (err) {
|
|
1261
|
+
console.error(`Error: ${err.message}`);
|
|
1262
|
+
process.exit(1);
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
profileCmd.command("import <file>").description("Import a profile from a JSON file").action((file) => {
|
|
1266
|
+
try {
|
|
1267
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
1268
|
+
const json = fs13.readFileSync(file, "utf-8");
|
|
1269
|
+
const name = importProfile(json, claudeDir);
|
|
1270
|
+
console.log(`Profile "${name}" imported.`);
|
|
1271
|
+
} catch (err) {
|
|
1272
|
+
console.error(`Error: ${err.message}`);
|
|
1273
|
+
process.exit(1);
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
profileCmd.command("validate <name>").description("Check a profile for ghost skills (no longer on disk)").action((name) => {
|
|
1277
|
+
try {
|
|
1278
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
1279
|
+
const result = validateProfile(name, claudeDir);
|
|
1280
|
+
if (!result.ghostSkills.length) {
|
|
1281
|
+
console.log(`Profile "${name}" is valid \u2014 all ${result.validSkills.length} skills found on disk.`);
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
console.log(`Profile "${name}" has ${result.ghostSkills.length} ghost skill(s):`);
|
|
1285
|
+
result.ghostSkills.forEach((s) => console.log(` ${s} (not found on disk)`));
|
|
1286
|
+
if (result.validSkills.length) console.log(`${result.validSkills.length} skill(s) OK: ${result.validSkills.join(", ")}`);
|
|
1287
|
+
} catch (err) {
|
|
1288
|
+
console.error(`Error: ${err.message}`);
|
|
1289
|
+
process.exit(1);
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
program.command("catalog").description("Generate ~/.claude/SKILLS.md catalog").option("--out <path>", "Write to custom path instead of ~/.claude/SKILLS.md").action((opts) => {
|
|
1293
|
+
try {
|
|
1294
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
1295
|
+
const content = generateCatalog(claudeDir);
|
|
1296
|
+
if (opts.out) {
|
|
1297
|
+
fs13.writeFileSync(opts.out, content);
|
|
1298
|
+
console.log(`Catalog written to ${opts.out}`);
|
|
1299
|
+
} else {
|
|
1300
|
+
console.log("Catalog written to ~/.claude/SKILLS.md");
|
|
1301
|
+
console.log("Tip: use @~/.claude/SKILLS.md in any Claude session to reference it.");
|
|
1302
|
+
}
|
|
1303
|
+
} catch (err) {
|
|
1304
|
+
console.error(`Error: ${err.message}`);
|
|
1305
|
+
process.exit(1);
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
program.command("audit").description("Report duplicates, disabled skill counts, and plugin orphans").option("--json", "Output as JSON").action((opts) => {
|
|
1309
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
1310
|
+
const standalone = scanStandaloneSkills(claudeDir);
|
|
1311
|
+
const plugins = scanPlugins(claudeDir);
|
|
473
1312
|
const standaloneNames = new Set(standalone.map((s) => s.name));
|
|
474
1313
|
const pluginBaseNames = plugins.flatMap((p) => p.skills.map((s) => {
|
|
475
1314
|
const parts = s.name.split(":");
|
|
@@ -478,6 +1317,10 @@ program.command("audit").description("Report duplicates, disabled skill counts,
|
|
|
478
1317
|
const duplicates = [...standaloneNames].filter((n) => pluginBaseNames.includes(n));
|
|
479
1318
|
const disabledStandalone = standalone.filter((s) => s.status === "disabled");
|
|
480
1319
|
const disabledPlugins = plugins.filter((p) => p.status === "disabled");
|
|
1320
|
+
if (opts.json) {
|
|
1321
|
+
process.stdout.write(JSON.stringify({ duplicates, disabledStandalone: disabledStandalone.map((s) => s.name), disabledPlugins: disabledPlugins.map((p) => p.id) }, null, 2) + "\n");
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
481
1324
|
let found = false;
|
|
482
1325
|
if (duplicates.length) {
|
|
483
1326
|
found = true;
|
|
@@ -499,4 +1342,20 @@ Blocked plugins (${disabledPlugins.length}):`);
|
|
|
499
1342
|
}
|
|
500
1343
|
if (!found) console.log("No issues found.");
|
|
501
1344
|
});
|
|
1345
|
+
program.command("detect").description("Show which supported AI CLIs are installed").option("--json", "Output as JSON").action((opts) => {
|
|
1346
|
+
const adapters = getAllAdapters();
|
|
1347
|
+
const results = adapters.map((a) => ({ cli: a.cliName, name: a.displayName, installed: a.isInstalled() }));
|
|
1348
|
+
if (opts.json) {
|
|
1349
|
+
process.stdout.write(JSON.stringify(results, null, 2) + "\n");
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
console.log("\nDetected CLIs:");
|
|
1353
|
+
for (const r of results) {
|
|
1354
|
+
console.log(` ${r.name.padEnd(20)} ${r.installed ? "installed" : "not found"}`);
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
program.command("aider-config").description("Print the read: block to paste into ~/.aider.conf.yml for active Aider skills").action(() => {
|
|
1358
|
+
const adapter = new AiderAdapter();
|
|
1359
|
+
process.stdout.write(adapter.generateAiderConfig());
|
|
1360
|
+
});
|
|
502
1361
|
program.parse();
|