skillswitch 0.1.2 → 0.1.3
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 +494 -106
- 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 fs6 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();
|
|
@@ -178,7 +218,11 @@ function readProfileStore(claudeDir = defaultClaudeDir4) {
|
|
|
178
218
|
return JSON.parse(raw);
|
|
179
219
|
} catch (err) {
|
|
180
220
|
if (err.code === "ENOENT") {
|
|
181
|
-
return { active: null, profiles: {} };
|
|
221
|
+
return { active: null, previous: null, profiles: {} };
|
|
222
|
+
}
|
|
223
|
+
if (err instanceof SyntaxError) {
|
|
224
|
+
process.stderr.write("Warning: profiles.json is corrupt \u2014 resetting to empty store\n");
|
|
225
|
+
return { active: null, previous: null, profiles: {} };
|
|
182
226
|
}
|
|
183
227
|
throw err;
|
|
184
228
|
}
|
|
@@ -194,6 +238,7 @@ function saveProfile(name, skills, plugins, claudeDir = defaultClaudeDir4) {
|
|
|
194
238
|
const store = readProfileStore(claudeDir);
|
|
195
239
|
const profile = {
|
|
196
240
|
created: store.profiles[name]?.created ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
241
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
197
242
|
skills,
|
|
198
243
|
plugins
|
|
199
244
|
};
|
|
@@ -207,12 +252,113 @@ function deleteProfile(name, claudeDir = defaultClaudeDir4) {
|
|
|
207
252
|
delete store.profiles[name];
|
|
208
253
|
writeProfileStore(store, claudeDir);
|
|
209
254
|
}
|
|
210
|
-
|
|
255
|
+
function renameProfile(oldName, newName, claudeDir = defaultClaudeDir4) {
|
|
256
|
+
const store = readProfileStore(claudeDir);
|
|
257
|
+
if (!store.profiles[oldName]) throw new Error(`Profile "${oldName}" does not exist`);
|
|
258
|
+
if (store.profiles[newName]) throw new Error(`Profile "${newName}" already exists`);
|
|
259
|
+
store.profiles[newName] = store.profiles[oldName];
|
|
260
|
+
delete store.profiles[oldName];
|
|
261
|
+
if (store.active === oldName) store.active = newName;
|
|
262
|
+
if (store.previous === oldName) store.previous = newName;
|
|
263
|
+
writeProfileStore(store, claudeDir);
|
|
264
|
+
}
|
|
265
|
+
function copyProfile(srcName, dstName, claudeDir = defaultClaudeDir4) {
|
|
266
|
+
const store = readProfileStore(claudeDir);
|
|
267
|
+
if (!store.profiles[srcName]) throw new Error(`Profile "${srcName}" does not exist`);
|
|
268
|
+
if (store.profiles[dstName]) throw new Error(`Profile "${dstName}" already exists`);
|
|
269
|
+
store.profiles[dstName] = {
|
|
270
|
+
...store.profiles[srcName],
|
|
271
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
272
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
273
|
+
};
|
|
274
|
+
writeProfileStore(store, claudeDir);
|
|
275
|
+
}
|
|
276
|
+
function diffProfile(name, claudeDir = defaultClaudeDir4) {
|
|
277
|
+
const store = readProfileStore(claudeDir);
|
|
278
|
+
const profile = store.profiles[name];
|
|
279
|
+
if (!profile) throw new Error(`Profile "${name}" does not exist`);
|
|
280
|
+
const profileSkills = new Set(profile.skills);
|
|
281
|
+
const skillsDir2 = path4.join(claudeDir, "skills");
|
|
282
|
+
const disabledDir2 = path4.join(claudeDir, "skills", ".disabled");
|
|
283
|
+
const toDisable = [];
|
|
284
|
+
const toEnable = [];
|
|
285
|
+
if (fs4.existsSync(skillsDir2)) {
|
|
286
|
+
for (const file of fs4.readdirSync(skillsDir2)) {
|
|
287
|
+
if (!file.endsWith(".md") || !fs4.statSync(path4.join(skillsDir2, file)).isFile()) continue;
|
|
288
|
+
const n = file.slice(0, -3);
|
|
289
|
+
if (!profileSkills.has(n)) toDisable.push(n);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (fs4.existsSync(disabledDir2)) {
|
|
293
|
+
for (const file of fs4.readdirSync(disabledDir2)) {
|
|
294
|
+
if (!file.endsWith(".md")) continue;
|
|
295
|
+
const n = file.slice(0, -3);
|
|
296
|
+
if (profileSkills.has(n)) toEnable.push(n);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const profilePlugins = new Set(profile.plugins);
|
|
300
|
+
const toBlock = [];
|
|
301
|
+
const toUnblock = [];
|
|
302
|
+
try {
|
|
303
|
+
const raw = JSON.parse(fs4.readFileSync(path4.join(claudeDir, "plugins", "installed_plugins.json"), "utf-8"));
|
|
304
|
+
const installed = Object.keys(raw?.plugins ?? {});
|
|
305
|
+
let currentlyBlocked = /* @__PURE__ */ new Set();
|
|
306
|
+
try {
|
|
307
|
+
const blRaw = JSON.parse(fs4.readFileSync(path4.join(claudeDir, "plugins", "blocklist.json"), "utf-8"));
|
|
308
|
+
currentlyBlocked = new Set(blRaw.plugins.map((e) => e.plugin));
|
|
309
|
+
} catch {
|
|
310
|
+
}
|
|
311
|
+
for (const id of installed) {
|
|
312
|
+
if (!profilePlugins.has(id) && !currentlyBlocked.has(id)) toBlock.push(id);
|
|
313
|
+
if (profilePlugins.has(id) && currentlyBlocked.has(id)) toUnblock.push(id);
|
|
314
|
+
}
|
|
315
|
+
} catch {
|
|
316
|
+
}
|
|
317
|
+
return { toEnable, toDisable, toBlock, toUnblock };
|
|
318
|
+
}
|
|
319
|
+
function exportProfile(name, claudeDir = defaultClaudeDir4) {
|
|
320
|
+
const store = readProfileStore(claudeDir);
|
|
321
|
+
const profile = store.profiles[name];
|
|
322
|
+
if (!profile) throw new Error(`Profile "${name}" does not exist`);
|
|
323
|
+
return JSON.stringify({ name, ...profile }, null, 2);
|
|
324
|
+
}
|
|
325
|
+
function importProfile(jsonStr, claudeDir = defaultClaudeDir4) {
|
|
326
|
+
let parsed;
|
|
327
|
+
try {
|
|
328
|
+
parsed = JSON.parse(jsonStr);
|
|
329
|
+
} catch {
|
|
330
|
+
throw new Error("Invalid JSON in import file");
|
|
331
|
+
}
|
|
332
|
+
if (!parsed.name || !Array.isArray(parsed.skills) || !Array.isArray(parsed.plugins)) {
|
|
333
|
+
throw new Error('Import file must have "name", "skills", and "plugins" fields');
|
|
334
|
+
}
|
|
335
|
+
saveProfile(parsed.name, parsed.skills, parsed.plugins, claudeDir);
|
|
336
|
+
return parsed.name;
|
|
337
|
+
}
|
|
338
|
+
function validateProfile(name, claudeDir = defaultClaudeDir4) {
|
|
211
339
|
const store = readProfileStore(claudeDir);
|
|
212
340
|
const profile = store.profiles[name];
|
|
213
|
-
if (!profile) {
|
|
214
|
-
|
|
341
|
+
if (!profile) throw new Error(`Profile "${name}" does not exist`);
|
|
342
|
+
const skillsDir2 = path4.join(claudeDir, "skills");
|
|
343
|
+
const disabledDir2 = path4.join(claudeDir, "skills", ".disabled");
|
|
344
|
+
const onDisk = /* @__PURE__ */ new Set();
|
|
345
|
+
for (const dir of [skillsDir2, disabledDir2]) {
|
|
346
|
+
if (!fs4.existsSync(dir)) continue;
|
|
347
|
+
for (const file of fs4.readdirSync(dir)) {
|
|
348
|
+
if (file.endsWith(".md")) onDisk.add(file.slice(0, -3));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const validSkills = [];
|
|
352
|
+
const ghostSkills = [];
|
|
353
|
+
for (const skill of profile.skills) {
|
|
354
|
+
(onDisk.has(skill) ? validSkills : ghostSkills).push(skill);
|
|
215
355
|
}
|
|
356
|
+
return { validSkills, ghostSkills };
|
|
357
|
+
}
|
|
358
|
+
async function activateProfile(name, claudeDir = defaultClaudeDir4) {
|
|
359
|
+
const store = readProfileStore(claudeDir);
|
|
360
|
+
const profile = store.profiles[name];
|
|
361
|
+
if (!profile) throw new Error(`Profile "${name}" does not exist`);
|
|
216
362
|
const profileSkills = new Set(profile.skills);
|
|
217
363
|
const skillsDir2 = path4.join(claudeDir, "skills");
|
|
218
364
|
const disabledDir2 = path4.join(claudeDir, "skills", ".disabled");
|
|
@@ -251,6 +397,7 @@ async function activateProfile(name, claudeDir = defaultClaudeDir4) {
|
|
|
251
397
|
if (err.code !== "ENOENT") throw err;
|
|
252
398
|
}
|
|
253
399
|
await setBlockedPlugins(pluginsBlocked, `blocked by profile: ${name}`, claudeDir);
|
|
400
|
+
store.previous = store.active;
|
|
254
401
|
store.active = name;
|
|
255
402
|
writeProfileStore(store, claudeDir);
|
|
256
403
|
return { enabled, disabled, pluginsBlocked };
|
|
@@ -295,8 +442,21 @@ function generateCatalog(claudeDir = defaultClaudeDir5) {
|
|
|
295
442
|
}
|
|
296
443
|
|
|
297
444
|
// src/cli.ts
|
|
445
|
+
process.on("unhandledRejection", (err) => {
|
|
446
|
+
process.stderr.write(`Error: ${err.message ?? err}
|
|
447
|
+
`);
|
|
448
|
+
process.exit(1);
|
|
449
|
+
});
|
|
450
|
+
function getClaudeDir(opts) {
|
|
451
|
+
return opts["claudeDir"] ?? process.env["SKILLSWITCH_CLAUDE_DIR"] ?? defaultClaudeDir;
|
|
452
|
+
}
|
|
453
|
+
function validateProfileName(name) {
|
|
454
|
+
if (!name || name.trim() === "") throw new Error("Profile name cannot be empty");
|
|
455
|
+
if (name.length > 64) throw new Error("Profile name must be 64 characters or less");
|
|
456
|
+
if (/[/\\]/.test(name)) throw new Error('Profile name cannot contain "/" or "\\"');
|
|
457
|
+
}
|
|
298
458
|
var program = new Command();
|
|
299
|
-
program.name("skillswitch").description("Manage Claude Code skills").version("0.1.
|
|
459
|
+
program.name("skillswitch").description("Manage Claude Code skills").version("0.1.2").option("--claude-dir <path>", "Override the Claude config directory (default: ~/.claude)");
|
|
300
460
|
function confirm(prompt) {
|
|
301
461
|
return new Promise((resolve) => {
|
|
302
462
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -306,25 +466,45 @@ function confirm(prompt) {
|
|
|
306
466
|
});
|
|
307
467
|
});
|
|
308
468
|
}
|
|
309
|
-
program.command("list").description("Show all skills grouped by source").option("--disabled", "Show only disabled skills").action((opts) => {
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
const
|
|
469
|
+
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) => {
|
|
470
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
471
|
+
const standalone = scanStandaloneSkills(claudeDir);
|
|
472
|
+
const plugins = scanPlugins(claudeDir);
|
|
473
|
+
if (opts.json) {
|
|
474
|
+
const filter = opts.disabled ? "disabled" : opts.enabled ? "active" : null;
|
|
475
|
+
const ss2 = filter ? standalone.filter((s) => s.status === filter) : standalone;
|
|
476
|
+
const pp2 = filter ? plugins.filter((p) => p.status === (filter === "active" ? "active" : "disabled")) : plugins;
|
|
477
|
+
process.stdout.write(JSON.stringify({ standalone: ss2, plugins: pp2 }, null, 2) + "\n");
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const statusFilter = opts.disabled ? "disabled" : opts.enabled ? "active" : null;
|
|
481
|
+
const ss = statusFilter ? standalone.filter((s) => s.status === statusFilter) : standalone;
|
|
313
482
|
console.log(`
|
|
314
483
|
Standalone (${standalone.length} total):`);
|
|
315
484
|
for (const s of ss) console.log(` ${s.name}${s.status === "disabled" ? " [disabled]" : ""}`);
|
|
316
|
-
const pp =
|
|
317
|
-
for (const p of pp)
|
|
485
|
+
const pp = statusFilter ? plugins.filter((p) => p.status === statusFilter) : plugins;
|
|
486
|
+
for (const p of pp) {
|
|
487
|
+
console.log(`
|
|
318
488
|
${p.id}${p.status === "disabled" ? " [DISABLED]" : ""} (${p.skills.length} skills)`);
|
|
489
|
+
for (const s of p.skills) console.log(` ${s.name}${s.status === "disabled" ? " [disabled]" : ""}`);
|
|
490
|
+
}
|
|
319
491
|
});
|
|
320
|
-
program.command("search <query>").description("Search skills by name or description (substring match)").action((query) => {
|
|
492
|
+
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) => {
|
|
493
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
321
494
|
const q = query.toLowerCase();
|
|
322
|
-
const standalone = scanStandaloneSkills();
|
|
323
|
-
const plugins = scanPlugins();
|
|
324
|
-
|
|
495
|
+
const standalone = scanStandaloneSkills(claudeDir);
|
|
496
|
+
const plugins = scanPlugins(claudeDir);
|
|
497
|
+
let matches = [
|
|
325
498
|
...standalone.filter((s) => s.name.includes(q) || s.description.toLowerCase().includes(q)),
|
|
326
499
|
...plugins.flatMap((p) => p.skills).filter((s) => s.name.includes(q) || s.description.toLowerCase().includes(q))
|
|
327
500
|
];
|
|
501
|
+
if (opts.status) {
|
|
502
|
+
matches = matches.filter((s) => s.status === opts.status);
|
|
503
|
+
}
|
|
504
|
+
if (opts.json) {
|
|
505
|
+
process.stdout.write(JSON.stringify(matches, null, 2) + "\n");
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
328
508
|
if (!matches.length) {
|
|
329
509
|
console.log(`No skills matching "${query}".`);
|
|
330
510
|
return;
|
|
@@ -337,101 +517,190 @@ ${matches.length} match(es) for "${query}":
|
|
|
337
517
|
if (s.description) console.log(` ${s.description}`);
|
|
338
518
|
}
|
|
339
519
|
});
|
|
340
|
-
program.command("status").description("Show active profile and skill counts").action(() => {
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
const
|
|
520
|
+
program.command("status").description("Show active profile and skill counts").option("--json", "Output as JSON").action((opts) => {
|
|
521
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
522
|
+
const store = readProfileStore(claudeDir);
|
|
523
|
+
const standalone = scanStandaloneSkills(claudeDir);
|
|
524
|
+
const plugins = scanPlugins(claudeDir);
|
|
344
525
|
const active = standalone.filter((s) => s.status === "active").length + plugins.filter((p) => p.status === "active").reduce((n, p) => n + p.skills.length, 0);
|
|
345
526
|
const disabled = standalone.filter((s) => s.status === "disabled").length + plugins.filter((p) => p.status === "disabled").reduce((n, p) => n + p.skills.length, 0);
|
|
527
|
+
if (opts.json) {
|
|
528
|
+
process.stdout.write(JSON.stringify({ activeProfile: store.active, active, disabled, total: active + disabled }, null, 2) + "\n");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
346
531
|
console.log(`Active profile : ${store.active ?? "none"}`);
|
|
347
532
|
console.log(`Enabled skills : ${active}`);
|
|
348
533
|
console.log(`Disabled skills: ${disabled}`);
|
|
349
534
|
console.log(`Total : ${active + disabled}`);
|
|
350
535
|
});
|
|
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
|
-
|
|
536
|
+
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) => {
|
|
537
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
538
|
+
const log = (msg) => {
|
|
539
|
+
if (!opts.quiet) console.log(msg);
|
|
540
|
+
};
|
|
541
|
+
try {
|
|
542
|
+
if (opts.plugin) {
|
|
543
|
+
const plugins = scanPlugins(claudeDir);
|
|
544
|
+
const matched = plugins.filter(
|
|
545
|
+
(p) => opts.exact ? p.id === name : p.id.includes(name)
|
|
546
|
+
);
|
|
547
|
+
if (!matched.length) {
|
|
548
|
+
console.log(`No plugins matching "${name}".`);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (matched.length > 1 && !opts.exact) {
|
|
552
|
+
console.log(`Matches: ${matched.map((p) => p.id).join(", ")}`);
|
|
553
|
+
if (!await confirm(`Disable all ${matched.length} plugins? (y/N) `)) {
|
|
554
|
+
console.log("Aborted.");
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
for (const p of matched) {
|
|
559
|
+
if (opts.dryRun) log(`[dry-run] Would block plugin: ${p.id}`);
|
|
560
|
+
else {
|
|
561
|
+
await blockPlugin(p.id, "skillswitch: manually disabled", claudeDir);
|
|
562
|
+
log(`Plugin blocked: ${p.id}`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
355
565
|
return;
|
|
356
566
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
console.log(`No active skills matching "${name}".`);
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
if (matches.length > 1) {
|
|
367
|
-
console.log(`Matches: ${matches.map((s) => s.name).join(", ")}`);
|
|
368
|
-
if (!await confirm(`Disable all ${matches.length}? (y/N) `)) {
|
|
369
|
-
console.log("Aborted.");
|
|
567
|
+
const matches = scanStandaloneSkills(claudeDir).filter((s) => {
|
|
568
|
+
const hit = opts.exact ? s.name === name : s.name.includes(name);
|
|
569
|
+
return hit && s.status === "active";
|
|
570
|
+
});
|
|
571
|
+
if (!matches.length) {
|
|
572
|
+
console.log(`No active skills matching "${name}".`);
|
|
370
573
|
return;
|
|
371
574
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
575
|
+
if (matches.length > 1) {
|
|
576
|
+
console.log(`Matches: ${matches.map((s) => s.name).join(", ")}`);
|
|
577
|
+
if (!await confirm(`Disable all ${matches.length}? (y/N) `)) {
|
|
578
|
+
console.log("Aborted.");
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
378
581
|
}
|
|
582
|
+
for (const s of matches) {
|
|
583
|
+
if (opts.dryRun) log(`[dry-run] Would disable: ${s.name}`);
|
|
584
|
+
else {
|
|
585
|
+
disableSkill(s.name, claudeDir);
|
|
586
|
+
log(`Disabled: ${s.name}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
} catch (err) {
|
|
590
|
+
console.error(`Error: ${err.message}`);
|
|
591
|
+
process.exit(1);
|
|
379
592
|
}
|
|
380
593
|
});
|
|
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
|
-
|
|
594
|
+
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) => {
|
|
595
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
596
|
+
const log = (msg) => {
|
|
597
|
+
if (!opts.quiet) console.log(msg);
|
|
598
|
+
};
|
|
599
|
+
try {
|
|
600
|
+
if (opts.plugin) {
|
|
601
|
+
const plugins = scanPlugins(claudeDir);
|
|
602
|
+
const matched = plugins.filter(
|
|
603
|
+
(p) => opts.exact ? p.id === name : p.id.includes(name)
|
|
604
|
+
);
|
|
605
|
+
if (!matched.length) {
|
|
606
|
+
console.log(`No plugins matching "${name}".`);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
for (const p of matched) {
|
|
610
|
+
if (opts.dryRun) {
|
|
611
|
+
log(`[dry-run] Would unblock plugin: ${p.id}`);
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
const wasBlocked = await unblockPlugin(p.id, claudeDir);
|
|
615
|
+
log(wasBlocked ? `Plugin unblocked: ${p.id}` : `Plugin was not blocked: ${p.id}`);
|
|
616
|
+
}
|
|
385
617
|
return;
|
|
386
618
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
console.log(`No disabled skills matching "${name}".`);
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
if (matches.length > 1) {
|
|
397
|
-
console.log(`Matches: ${matches.map((s) => s.name).join(", ")}`);
|
|
398
|
-
if (!await confirm(`Enable all ${matches.length}? (y/N) `)) {
|
|
399
|
-
console.log("Aborted.");
|
|
619
|
+
const matches = scanStandaloneSkills(claudeDir).filter((s) => {
|
|
620
|
+
const hit = opts.exact ? s.name === name : s.name.includes(name);
|
|
621
|
+
return hit && s.status === "disabled";
|
|
622
|
+
});
|
|
623
|
+
if (!matches.length) {
|
|
624
|
+
console.log(`No disabled skills matching "${name}".`);
|
|
400
625
|
return;
|
|
401
626
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
627
|
+
if (matches.length > 1) {
|
|
628
|
+
console.log(`Matches: ${matches.map((s) => s.name).join(", ")}`);
|
|
629
|
+
if (!await confirm(`Enable all ${matches.length}? (y/N) `)) {
|
|
630
|
+
console.log("Aborted.");
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
for (const s of matches) {
|
|
635
|
+
if (opts.dryRun) log(`[dry-run] Would enable: ${s.name}`);
|
|
636
|
+
else {
|
|
637
|
+
enableSkill(s.name, claudeDir);
|
|
638
|
+
log(`Enabled: ${s.name}`);
|
|
639
|
+
}
|
|
408
640
|
}
|
|
641
|
+
} catch (err) {
|
|
642
|
+
console.error(`Error: ${err.message}`);
|
|
643
|
+
process.exit(1);
|
|
409
644
|
}
|
|
410
645
|
});
|
|
411
646
|
var profileCmd = program.command("profile").description("Manage skill profiles");
|
|
412
647
|
profileCmd.command("create <name>").description("Snapshot current enabled skills as a named profile").action((name) => {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
648
|
+
try {
|
|
649
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
650
|
+
validateProfileName(name);
|
|
651
|
+
const skills = scanStandaloneSkills(claudeDir).filter((s) => s.status === "active").map((s) => s.name);
|
|
652
|
+
const plugins = scanPlugins(claudeDir).filter((p) => p.status === "active").map((p) => p.id);
|
|
653
|
+
const store = readProfileStore(claudeDir);
|
|
654
|
+
if (store.profiles[name]) console.log(`Overwriting existing profile "${name}".`);
|
|
655
|
+
if (!skills.length && !plugins.length) console.warn(`Warning: no active skills or plugins found \u2014 profile may be empty by mistake.`);
|
|
656
|
+
saveProfile(name, skills, plugins, claudeDir);
|
|
657
|
+
console.log(`Profile "${name}" saved: ${skills.length} skills, ${plugins.length} plugins.`);
|
|
658
|
+
} catch (err) {
|
|
659
|
+
console.error(`Error: ${err.message}`);
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
417
662
|
});
|
|
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
|
-
|
|
663
|
+
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) => {
|
|
664
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
665
|
+
try {
|
|
666
|
+
if (opts.dryRun) {
|
|
667
|
+
const store = readProfileStore(claudeDir);
|
|
668
|
+
const p = store.profiles[name];
|
|
669
|
+
if (!p) {
|
|
670
|
+
console.log(`Profile "${name}" not found.`);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const diff = diffProfile(name, claudeDir);
|
|
674
|
+
console.log(`[dry-run] Activating "${name}":`);
|
|
675
|
+
if (diff.toEnable.length) console.log(` Enable : ${diff.toEnable.join(", ")}`);
|
|
676
|
+
if (diff.toDisable.length) console.log(` Disable : ${diff.toDisable.join(", ")}`);
|
|
677
|
+
if (diff.toBlock.length) console.log(` Block : ${diff.toBlock.join(", ")}`);
|
|
678
|
+
if (diff.toUnblock.length) console.log(` Unblock : ${diff.toUnblock.join(", ")}`);
|
|
679
|
+
if (!diff.toEnable.length && !diff.toDisable.length && !diff.toBlock.length && !diff.toUnblock.length) {
|
|
680
|
+
console.log(" (no changes)");
|
|
681
|
+
}
|
|
424
682
|
return;
|
|
425
683
|
}
|
|
426
|
-
|
|
427
|
-
|
|
684
|
+
const result = await activateProfile(name, claudeDir);
|
|
685
|
+
if (!opts.quiet) {
|
|
686
|
+
console.log(`Activated "${name}".`);
|
|
687
|
+
if (result.enabled.length) console.log(` Enabled : ${result.enabled.join(", ")}`);
|
|
688
|
+
if (result.disabled.length) console.log(` Disabled: ${result.disabled.join(", ")}`);
|
|
689
|
+
if (result.pluginsBlocked.length) console.log(` Blocked : ${result.pluginsBlocked.join(", ")}`);
|
|
690
|
+
}
|
|
691
|
+
} catch (err) {
|
|
692
|
+
console.error(`Error: ${err.message}`);
|
|
693
|
+
process.exit(1);
|
|
428
694
|
}
|
|
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
695
|
});
|
|
432
|
-
profileCmd.command("list").description("List saved profiles").action(() => {
|
|
433
|
-
const
|
|
696
|
+
profileCmd.command("list").description("List saved profiles").option("--json", "Output as JSON").action((opts) => {
|
|
697
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
698
|
+
const store = readProfileStore(claudeDir);
|
|
434
699
|
const names = Object.keys(store.profiles);
|
|
700
|
+
if (opts.json) {
|
|
701
|
+
process.stdout.write(JSON.stringify({ active: store.active, previous: store.previous, profiles: store.profiles }, null, 2) + "\n");
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
435
704
|
if (!names.length) {
|
|
436
705
|
console.log("No profiles saved.");
|
|
437
706
|
return;
|
|
@@ -443,33 +712,148 @@ profileCmd.command("list").description("List saved profiles").action(() => {
|
|
|
443
712
|
}
|
|
444
713
|
});
|
|
445
714
|
profileCmd.command("show <name>").description("Show skills in a profile").action((name) => {
|
|
446
|
-
const
|
|
715
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
716
|
+
const store = readProfileStore(claudeDir);
|
|
447
717
|
const p = store.profiles[name];
|
|
448
718
|
if (!p) {
|
|
449
719
|
console.log(`Profile "${name}" not found.`);
|
|
450
720
|
return;
|
|
451
721
|
}
|
|
452
|
-
console.log(`Profile "${name}" (${p.created.slice(0, 10)}):`);
|
|
722
|
+
console.log(`Profile "${name}" (created ${p.created.slice(0, 10)}):`);
|
|
453
723
|
console.log(` Skills (${p.skills.length}): ${p.skills.join(", ") || "none"}`);
|
|
454
724
|
console.log(` Plugins (${p.plugins.length}): ${p.plugins.join(", ") || "none"}`);
|
|
455
725
|
});
|
|
456
|
-
profileCmd.command("delete <name>").description("Delete a saved profile").action((name) => {
|
|
726
|
+
profileCmd.command("delete <name>").description("Delete a saved profile").option("--force", "Delete even if this is the active profile").action((name, opts) => {
|
|
457
727
|
try {
|
|
458
|
-
|
|
728
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
729
|
+
if (opts.force) {
|
|
730
|
+
const store = readProfileStore(claudeDir);
|
|
731
|
+
if (store.active === name) {
|
|
732
|
+
store.active = null;
|
|
733
|
+
if (!store.profiles[name]) {
|
|
734
|
+
console.log(`Profile "${name}" not found.`);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
delete store.profiles[name];
|
|
738
|
+
const storeFile = claudeDir + "/skillctl/profiles.json";
|
|
739
|
+
const { mkdirSync: mkdirSync3, writeFileSync: writeFileSync4, renameSync: renameSync3 } = fs6;
|
|
740
|
+
const dir = storeFile.replace(/\/[^/]+$/, "");
|
|
741
|
+
mkdirSync3(dir, { recursive: true });
|
|
742
|
+
writeFileSync4(storeFile + ".tmp", JSON.stringify(store, null, 2));
|
|
743
|
+
renameSync3(storeFile + ".tmp", storeFile);
|
|
744
|
+
console.log(`Profile "${name}" deleted.`);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
deleteProfile(name, claudeDir);
|
|
459
749
|
console.log(`Profile "${name}" deleted.`);
|
|
460
|
-
} catch (
|
|
461
|
-
console.error(
|
|
750
|
+
} catch (err) {
|
|
751
|
+
console.error(err.message);
|
|
462
752
|
process.exit(1);
|
|
463
753
|
}
|
|
464
754
|
});
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
755
|
+
profileCmd.command("rename <old> <new>").description("Rename a profile").action((oldName, newName) => {
|
|
756
|
+
try {
|
|
757
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
758
|
+
validateProfileName(newName);
|
|
759
|
+
renameProfile(oldName, newName, claudeDir);
|
|
760
|
+
console.log(`Profile "${oldName}" renamed to "${newName}".`);
|
|
761
|
+
} catch (err) {
|
|
762
|
+
console.error(`Error: ${err.message}`);
|
|
763
|
+
process.exit(1);
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
profileCmd.command("copy <src> <dst>").description("Copy a profile to a new name").action((srcName, dstName) => {
|
|
767
|
+
try {
|
|
768
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
769
|
+
validateProfileName(dstName);
|
|
770
|
+
copyProfile(srcName, dstName, claudeDir);
|
|
771
|
+
console.log(`Profile "${srcName}" copied to "${dstName}".`);
|
|
772
|
+
} catch (err) {
|
|
773
|
+
console.error(`Error: ${err.message}`);
|
|
774
|
+
process.exit(1);
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
profileCmd.command("diff <name>").description("Show what would change when activating a profile").action((name) => {
|
|
778
|
+
try {
|
|
779
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
780
|
+
const diff = diffProfile(name, claudeDir);
|
|
781
|
+
if (!diff.toEnable.length && !diff.toDisable.length && !diff.toBlock.length && !diff.toUnblock.length) {
|
|
782
|
+
console.log(`Profile "${name}" matches current state \u2014 no changes needed.`);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
console.log(`Diff for profile "${name}":`);
|
|
786
|
+
if (diff.toEnable.length) console.log(` Enable (+): ${diff.toEnable.join(", ")}`);
|
|
787
|
+
if (diff.toDisable.length) console.log(` Disable (-): ${diff.toDisable.join(", ")}`);
|
|
788
|
+
if (diff.toBlock.length) console.log(` Block (-): ${diff.toBlock.join(", ")}`);
|
|
789
|
+
if (diff.toUnblock.length) console.log(` Unblock (+): ${diff.toUnblock.join(", ")}`);
|
|
790
|
+
} catch (err) {
|
|
791
|
+
console.error(`Error: ${err.message}`);
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
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) => {
|
|
796
|
+
try {
|
|
797
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
798
|
+
const json = exportProfile(name, claudeDir);
|
|
799
|
+
if (opts.out) {
|
|
800
|
+
fs6.writeFileSync(opts.out, json);
|
|
801
|
+
console.log(`Profile "${name}" exported to ${opts.out}`);
|
|
802
|
+
} else {
|
|
803
|
+
process.stdout.write(json + "\n");
|
|
804
|
+
}
|
|
805
|
+
} catch (err) {
|
|
806
|
+
console.error(`Error: ${err.message}`);
|
|
807
|
+
process.exit(1);
|
|
808
|
+
}
|
|
469
809
|
});
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
810
|
+
profileCmd.command("import <file>").description("Import a profile from a JSON file").action((file) => {
|
|
811
|
+
try {
|
|
812
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
813
|
+
const json = fs6.readFileSync(file, "utf-8");
|
|
814
|
+
const name = importProfile(json, claudeDir);
|
|
815
|
+
console.log(`Profile "${name}" imported.`);
|
|
816
|
+
} catch (err) {
|
|
817
|
+
console.error(`Error: ${err.message}`);
|
|
818
|
+
process.exit(1);
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
profileCmd.command("validate <name>").description("Check a profile for ghost skills (no longer on disk)").action((name) => {
|
|
822
|
+
try {
|
|
823
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
824
|
+
const result = validateProfile(name, claudeDir);
|
|
825
|
+
if (!result.ghostSkills.length) {
|
|
826
|
+
console.log(`Profile "${name}" is valid \u2014 all ${result.validSkills.length} skills found on disk.`);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
console.log(`Profile "${name}" has ${result.ghostSkills.length} ghost skill(s):`);
|
|
830
|
+
result.ghostSkills.forEach((s) => console.log(` ${s} (not found on disk)`));
|
|
831
|
+
if (result.validSkills.length) console.log(`${result.validSkills.length} skill(s) OK: ${result.validSkills.join(", ")}`);
|
|
832
|
+
} catch (err) {
|
|
833
|
+
console.error(`Error: ${err.message}`);
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
program.command("catalog").description("Generate ~/.claude/SKILLS.md catalog").option("--out <path>", "Write to custom path instead of ~/.claude/SKILLS.md").action((opts) => {
|
|
838
|
+
try {
|
|
839
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
840
|
+
const content = generateCatalog(claudeDir);
|
|
841
|
+
if (opts.out) {
|
|
842
|
+
fs6.writeFileSync(opts.out, content);
|
|
843
|
+
console.log(`Catalog written to ${opts.out}`);
|
|
844
|
+
} else {
|
|
845
|
+
console.log("Catalog written to ~/.claude/SKILLS.md");
|
|
846
|
+
console.log("Tip: use @~/.claude/SKILLS.md in any Claude session to reference it.");
|
|
847
|
+
}
|
|
848
|
+
} catch (err) {
|
|
849
|
+
console.error(`Error: ${err.message}`);
|
|
850
|
+
process.exit(1);
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
program.command("audit").description("Report duplicates, disabled skill counts, and plugin orphans").option("--json", "Output as JSON").action((opts) => {
|
|
854
|
+
const claudeDir = getClaudeDir(program.opts());
|
|
855
|
+
const standalone = scanStandaloneSkills(claudeDir);
|
|
856
|
+
const plugins = scanPlugins(claudeDir);
|
|
473
857
|
const standaloneNames = new Set(standalone.map((s) => s.name));
|
|
474
858
|
const pluginBaseNames = plugins.flatMap((p) => p.skills.map((s) => {
|
|
475
859
|
const parts = s.name.split(":");
|
|
@@ -478,6 +862,10 @@ program.command("audit").description("Report duplicates, disabled skill counts,
|
|
|
478
862
|
const duplicates = [...standaloneNames].filter((n) => pluginBaseNames.includes(n));
|
|
479
863
|
const disabledStandalone = standalone.filter((s) => s.status === "disabled");
|
|
480
864
|
const disabledPlugins = plugins.filter((p) => p.status === "disabled");
|
|
865
|
+
if (opts.json) {
|
|
866
|
+
process.stdout.write(JSON.stringify({ duplicates, disabledStandalone: disabledStandalone.map((s) => s.name), disabledPlugins: disabledPlugins.map((p) => p.id) }, null, 2) + "\n");
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
481
869
|
let found = false;
|
|
482
870
|
if (duplicates.length) {
|
|
483
871
|
found = true;
|