skillswitch 0.1.3 → 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 +519 -48
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import * as readline from "readline";
|
|
6
|
-
import * as
|
|
6
|
+
import * as fs13 from "fs";
|
|
7
7
|
|
|
8
8
|
// src/scanner.ts
|
|
9
9
|
import * as fs from "fs";
|
|
@@ -203,18 +203,351 @@ async function setBlockedPlugins(pluginIds, reason, claudeDir = defaultClaudeDir
|
|
|
203
203
|
await writeBlocklist(data, claudeDir);
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
// src/
|
|
206
|
+
// src/adapters/claude.ts
|
|
207
207
|
import * as fs4 from "fs";
|
|
208
208
|
import * as path4 from "path";
|
|
209
|
-
import
|
|
210
|
-
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");
|
|
211
544
|
function profileStorePath(claudeDir) {
|
|
212
|
-
return
|
|
545
|
+
return path12.join(claudeDir, "skillctl", "profiles.json");
|
|
213
546
|
}
|
|
214
547
|
function readProfileStore(claudeDir = defaultClaudeDir4) {
|
|
215
548
|
const filePath = profileStorePath(claudeDir);
|
|
216
549
|
try {
|
|
217
|
-
const raw =
|
|
550
|
+
const raw = fs11.readFileSync(filePath, "utf-8");
|
|
218
551
|
return JSON.parse(raw);
|
|
219
552
|
} catch (err) {
|
|
220
553
|
if (err.code === "ENOENT") {
|
|
@@ -229,10 +562,10 @@ function readProfileStore(claudeDir = defaultClaudeDir4) {
|
|
|
229
562
|
}
|
|
230
563
|
function writeProfileStore(store, claudeDir) {
|
|
231
564
|
const filePath = profileStorePath(claudeDir);
|
|
232
|
-
|
|
565
|
+
fs11.mkdirSync(path12.dirname(filePath), { recursive: true });
|
|
233
566
|
const tmp = filePath + ".tmp";
|
|
234
|
-
|
|
235
|
-
|
|
567
|
+
fs11.writeFileSync(tmp, JSON.stringify(store, null, 2));
|
|
568
|
+
fs11.renameSync(tmp, filePath);
|
|
236
569
|
}
|
|
237
570
|
function saveProfile(name, skills, plugins, claudeDir = defaultClaudeDir4) {
|
|
238
571
|
const store = readProfileStore(claudeDir);
|
|
@@ -278,19 +611,19 @@ function diffProfile(name, claudeDir = defaultClaudeDir4) {
|
|
|
278
611
|
const profile = store.profiles[name];
|
|
279
612
|
if (!profile) throw new Error(`Profile "${name}" does not exist`);
|
|
280
613
|
const profileSkills = new Set(profile.skills);
|
|
281
|
-
const skillsDir2 =
|
|
282
|
-
const disabledDir2 =
|
|
614
|
+
const skillsDir2 = path12.join(claudeDir, "skills");
|
|
615
|
+
const disabledDir2 = path12.join(claudeDir, "skills", ".disabled");
|
|
283
616
|
const toDisable = [];
|
|
284
617
|
const toEnable = [];
|
|
285
|
-
if (
|
|
286
|
-
for (const file of
|
|
287
|
-
if (!file.endsWith(".md") || !
|
|
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;
|
|
288
621
|
const n = file.slice(0, -3);
|
|
289
622
|
if (!profileSkills.has(n)) toDisable.push(n);
|
|
290
623
|
}
|
|
291
624
|
}
|
|
292
|
-
if (
|
|
293
|
-
for (const file of
|
|
625
|
+
if (fs11.existsSync(disabledDir2)) {
|
|
626
|
+
for (const file of fs11.readdirSync(disabledDir2)) {
|
|
294
627
|
if (!file.endsWith(".md")) continue;
|
|
295
628
|
const n = file.slice(0, -3);
|
|
296
629
|
if (profileSkills.has(n)) toEnable.push(n);
|
|
@@ -300,11 +633,11 @@ function diffProfile(name, claudeDir = defaultClaudeDir4) {
|
|
|
300
633
|
const toBlock = [];
|
|
301
634
|
const toUnblock = [];
|
|
302
635
|
try {
|
|
303
|
-
const raw = JSON.parse(
|
|
636
|
+
const raw = JSON.parse(fs11.readFileSync(path12.join(claudeDir, "plugins", "installed_plugins.json"), "utf-8"));
|
|
304
637
|
const installed = Object.keys(raw?.plugins ?? {});
|
|
305
638
|
let currentlyBlocked = /* @__PURE__ */ new Set();
|
|
306
639
|
try {
|
|
307
|
-
const blRaw = JSON.parse(
|
|
640
|
+
const blRaw = JSON.parse(fs11.readFileSync(path12.join(claudeDir, "plugins", "blocklist.json"), "utf-8"));
|
|
308
641
|
currentlyBlocked = new Set(blRaw.plugins.map((e) => e.plugin));
|
|
309
642
|
} catch {
|
|
310
643
|
}
|
|
@@ -339,12 +672,12 @@ function validateProfile(name, claudeDir = defaultClaudeDir4) {
|
|
|
339
672
|
const store = readProfileStore(claudeDir);
|
|
340
673
|
const profile = store.profiles[name];
|
|
341
674
|
if (!profile) throw new Error(`Profile "${name}" does not exist`);
|
|
342
|
-
const skillsDir2 =
|
|
343
|
-
const disabledDir2 =
|
|
675
|
+
const skillsDir2 = path12.join(claudeDir, "skills");
|
|
676
|
+
const disabledDir2 = path12.join(claudeDir, "skills", ".disabled");
|
|
344
677
|
const onDisk = /* @__PURE__ */ new Set();
|
|
345
678
|
for (const dir of [skillsDir2, disabledDir2]) {
|
|
346
|
-
if (!
|
|
347
|
-
for (const file of
|
|
679
|
+
if (!fs11.existsSync(dir)) continue;
|
|
680
|
+
for (const file of fs11.readdirSync(dir)) {
|
|
348
681
|
if (file.endsWith(".md")) onDisk.add(file.slice(0, -3));
|
|
349
682
|
}
|
|
350
683
|
}
|
|
@@ -360,14 +693,14 @@ async function activateProfile(name, claudeDir = defaultClaudeDir4) {
|
|
|
360
693
|
const profile = store.profiles[name];
|
|
361
694
|
if (!profile) throw new Error(`Profile "${name}" does not exist`);
|
|
362
695
|
const profileSkills = new Set(profile.skills);
|
|
363
|
-
const skillsDir2 =
|
|
364
|
-
const disabledDir2 =
|
|
696
|
+
const skillsDir2 = path12.join(claudeDir, "skills");
|
|
697
|
+
const disabledDir2 = path12.join(claudeDir, "skills", ".disabled");
|
|
365
698
|
const enabled = [];
|
|
366
699
|
const disabled = [];
|
|
367
|
-
if (
|
|
368
|
-
for (const file of
|
|
700
|
+
if (fs11.existsSync(skillsDir2)) {
|
|
701
|
+
for (const file of fs11.readdirSync(skillsDir2)) {
|
|
369
702
|
if (!file.endsWith(".md")) continue;
|
|
370
|
-
const stat =
|
|
703
|
+
const stat = fs11.statSync(path12.join(skillsDir2, file));
|
|
371
704
|
if (!stat.isFile()) continue;
|
|
372
705
|
const skillName = file.slice(0, -3);
|
|
373
706
|
if (!profileSkills.has(skillName)) {
|
|
@@ -376,8 +709,8 @@ async function activateProfile(name, claudeDir = defaultClaudeDir4) {
|
|
|
376
709
|
}
|
|
377
710
|
}
|
|
378
711
|
}
|
|
379
|
-
if (
|
|
380
|
-
for (const file of
|
|
712
|
+
if (fs11.existsSync(disabledDir2)) {
|
|
713
|
+
for (const file of fs11.readdirSync(disabledDir2)) {
|
|
381
714
|
if (!file.endsWith(".md")) continue;
|
|
382
715
|
const skillName = file.slice(0, -3);
|
|
383
716
|
if (profileSkills.has(skillName)) {
|
|
@@ -387,10 +720,10 @@ async function activateProfile(name, claudeDir = defaultClaudeDir4) {
|
|
|
387
720
|
}
|
|
388
721
|
}
|
|
389
722
|
const profilePlugins = new Set(profile.plugins);
|
|
390
|
-
const installedPath =
|
|
723
|
+
const installedPath = path12.join(claudeDir, "plugins", "installed_plugins.json");
|
|
391
724
|
let pluginsBlocked = [];
|
|
392
725
|
try {
|
|
393
|
-
const raw = JSON.parse(
|
|
726
|
+
const raw = JSON.parse(fs11.readFileSync(installedPath, "utf-8"));
|
|
394
727
|
const allInstalled = Object.keys(raw?.plugins ?? {});
|
|
395
728
|
pluginsBlocked = allInstalled.filter((id) => !profilePlugins.has(id));
|
|
396
729
|
} catch (err) {
|
|
@@ -404,10 +737,10 @@ async function activateProfile(name, claudeDir = defaultClaudeDir4) {
|
|
|
404
737
|
}
|
|
405
738
|
|
|
406
739
|
// src/catalog.ts
|
|
407
|
-
import * as
|
|
408
|
-
import * as
|
|
409
|
-
import * as
|
|
410
|
-
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");
|
|
411
744
|
function generateCatalog(claudeDir = defaultClaudeDir5) {
|
|
412
745
|
const standalone = scanStandaloneSkills(claudeDir);
|
|
413
746
|
const plugins = scanPlugins(claudeDir);
|
|
@@ -437,7 +770,7 @@ function generateCatalog(claudeDir = defaultClaudeDir5) {
|
|
|
437
770
|
sections.push(`## Plugin: ${p.name} \u2014 DISABLED (${p.skills.length})`, "| Skill | Description |", "|-------|-------------|", rows(p.skills), "");
|
|
438
771
|
}
|
|
439
772
|
const content = sections.join("\n");
|
|
440
|
-
|
|
773
|
+
fs12.writeFileSync(path13.join(claudeDir, "SKILLS.md"), content);
|
|
441
774
|
return content;
|
|
442
775
|
}
|
|
443
776
|
|
|
@@ -456,7 +789,7 @@ function validateProfileName(name) {
|
|
|
456
789
|
if (/[/\\]/.test(name)) throw new Error('Profile name cannot contain "/" or "\\"');
|
|
457
790
|
}
|
|
458
791
|
var program = new Command();
|
|
459
|
-
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)`);
|
|
460
793
|
function confirm(prompt) {
|
|
461
794
|
return new Promise((resolve) => {
|
|
462
795
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -467,7 +800,23 @@ function confirm(prompt) {
|
|
|
467
800
|
});
|
|
468
801
|
}
|
|
469
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) => {
|
|
470
|
-
const
|
|
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);
|
|
471
820
|
const standalone = scanStandaloneSkills(claudeDir);
|
|
472
821
|
const plugins = scanPlugins(claudeDir);
|
|
473
822
|
if (opts.json) {
|
|
@@ -490,8 +839,31 @@ ${p.id}${p.status === "disabled" ? " [DISABLED]" : ""} (${p.skills.length} skill
|
|
|
490
839
|
}
|
|
491
840
|
});
|
|
492
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) => {
|
|
493
|
-
const
|
|
842
|
+
const globalOpts = program.opts();
|
|
843
|
+
const cliTarget = globalOpts["for"] ?? "claude";
|
|
494
844
|
const q = query.toLowerCase();
|
|
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);
|
|
495
867
|
const standalone = scanStandaloneSkills(claudeDir);
|
|
496
868
|
const plugins = scanPlugins(claudeDir);
|
|
497
869
|
let matches = [
|
|
@@ -518,7 +890,24 @@ ${matches.length} match(es) for "${query}":
|
|
|
518
890
|
}
|
|
519
891
|
});
|
|
520
892
|
program.command("status").description("Show active profile and skill counts").option("--json", "Output as JSON").action((opts) => {
|
|
521
|
-
const
|
|
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);
|
|
522
911
|
const store = readProfileStore(claudeDir);
|
|
523
912
|
const standalone = scanStandaloneSkills(claudeDir);
|
|
524
913
|
const plugins = scanPlugins(claudeDir);
|
|
@@ -534,10 +923,43 @@ program.command("status").description("Show active profile and skill counts").op
|
|
|
534
923
|
console.log(`Total : ${active + disabled}`);
|
|
535
924
|
});
|
|
536
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) => {
|
|
537
|
-
const
|
|
926
|
+
const globalOpts = program.opts();
|
|
927
|
+
const cliTarget = globalOpts["for"] ?? "claude";
|
|
538
928
|
const log = (msg) => {
|
|
539
929
|
if (!opts.quiet) console.log(msg);
|
|
540
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);
|
|
959
|
+
}
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const claudeDir = getClaudeDir(globalOpts);
|
|
541
963
|
try {
|
|
542
964
|
if (opts.plugin) {
|
|
543
965
|
const plugins = scanPlugins(claudeDir);
|
|
@@ -592,10 +1014,43 @@ program.command("disable <name>").description("Disable a skill (substring match)
|
|
|
592
1014
|
}
|
|
593
1015
|
});
|
|
594
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) => {
|
|
595
|
-
const
|
|
1017
|
+
const globalOpts = program.opts();
|
|
1018
|
+
const cliTarget = globalOpts["for"] ?? "claude";
|
|
596
1019
|
const log = (msg) => {
|
|
597
1020
|
if (!opts.quiet) console.log(msg);
|
|
598
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);
|
|
1050
|
+
}
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
const claudeDir = getClaudeDir(globalOpts);
|
|
599
1054
|
try {
|
|
600
1055
|
if (opts.plugin) {
|
|
601
1056
|
const plugins = scanPlugins(claudeDir);
|
|
@@ -736,11 +1191,11 @@ profileCmd.command("delete <name>").description("Delete a saved profile").option
|
|
|
736
1191
|
}
|
|
737
1192
|
delete store.profiles[name];
|
|
738
1193
|
const storeFile = claudeDir + "/skillctl/profiles.json";
|
|
739
|
-
const { mkdirSync:
|
|
1194
|
+
const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync4, renameSync: renameSync4 } = fs13;
|
|
740
1195
|
const dir = storeFile.replace(/\/[^/]+$/, "");
|
|
741
|
-
|
|
1196
|
+
mkdirSync4(dir, { recursive: true });
|
|
742
1197
|
writeFileSync4(storeFile + ".tmp", JSON.stringify(store, null, 2));
|
|
743
|
-
|
|
1198
|
+
renameSync4(storeFile + ".tmp", storeFile);
|
|
744
1199
|
console.log(`Profile "${name}" deleted.`);
|
|
745
1200
|
return;
|
|
746
1201
|
}
|
|
@@ -797,7 +1252,7 @@ profileCmd.command("export <name>").description("Export a profile to stdout or a
|
|
|
797
1252
|
const claudeDir = getClaudeDir(program.opts());
|
|
798
1253
|
const json = exportProfile(name, claudeDir);
|
|
799
1254
|
if (opts.out) {
|
|
800
|
-
|
|
1255
|
+
fs13.writeFileSync(opts.out, json);
|
|
801
1256
|
console.log(`Profile "${name}" exported to ${opts.out}`);
|
|
802
1257
|
} else {
|
|
803
1258
|
process.stdout.write(json + "\n");
|
|
@@ -810,7 +1265,7 @@ profileCmd.command("export <name>").description("Export a profile to stdout or a
|
|
|
810
1265
|
profileCmd.command("import <file>").description("Import a profile from a JSON file").action((file) => {
|
|
811
1266
|
try {
|
|
812
1267
|
const claudeDir = getClaudeDir(program.opts());
|
|
813
|
-
const json =
|
|
1268
|
+
const json = fs13.readFileSync(file, "utf-8");
|
|
814
1269
|
const name = importProfile(json, claudeDir);
|
|
815
1270
|
console.log(`Profile "${name}" imported.`);
|
|
816
1271
|
} catch (err) {
|
|
@@ -839,7 +1294,7 @@ program.command("catalog").description("Generate ~/.claude/SKILLS.md catalog").o
|
|
|
839
1294
|
const claudeDir = getClaudeDir(program.opts());
|
|
840
1295
|
const content = generateCatalog(claudeDir);
|
|
841
1296
|
if (opts.out) {
|
|
842
|
-
|
|
1297
|
+
fs13.writeFileSync(opts.out, content);
|
|
843
1298
|
console.log(`Catalog written to ${opts.out}`);
|
|
844
1299
|
} else {
|
|
845
1300
|
console.log("Catalog written to ~/.claude/SKILLS.md");
|
|
@@ -887,4 +1342,20 @@ Blocked plugins (${disabledPlugins.length}):`);
|
|
|
887
1342
|
}
|
|
888
1343
|
if (!found) console.log("No issues found.");
|
|
889
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
|
+
});
|
|
890
1361
|
program.parse();
|