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.
Files changed (2) hide show
  1. package/dist/cli.js +519 -48
  2. 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 fs6 from "fs";
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/profiles.ts
206
+ // src/adapters/claude.ts
207
207
  import * as fs4 from "fs";
208
208
  import * as path4 from "path";
209
- import { homedir as homedir4 } from "os";
210
- var defaultClaudeDir4 = path4.join(homedir4(), ".claude");
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 path4.join(claudeDir, "skillctl", "profiles.json");
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 = fs4.readFileSync(filePath, "utf-8");
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
- fs4.mkdirSync(path4.dirname(filePath), { recursive: true });
565
+ fs11.mkdirSync(path12.dirname(filePath), { recursive: true });
233
566
  const tmp = filePath + ".tmp";
234
- fs4.writeFileSync(tmp, JSON.stringify(store, null, 2));
235
- fs4.renameSync(tmp, filePath);
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 = path4.join(claudeDir, "skills");
282
- const disabledDir2 = path4.join(claudeDir, "skills", ".disabled");
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 (fs4.existsSync(skillsDir2)) {
286
- for (const file of fs4.readdirSync(skillsDir2)) {
287
- if (!file.endsWith(".md") || !fs4.statSync(path4.join(skillsDir2, file)).isFile()) continue;
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 (fs4.existsSync(disabledDir2)) {
293
- for (const file of fs4.readdirSync(disabledDir2)) {
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(fs4.readFileSync(path4.join(claudeDir, "plugins", "installed_plugins.json"), "utf-8"));
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(fs4.readFileSync(path4.join(claudeDir, "plugins", "blocklist.json"), "utf-8"));
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 = path4.join(claudeDir, "skills");
343
- const disabledDir2 = path4.join(claudeDir, "skills", ".disabled");
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 (!fs4.existsSync(dir)) continue;
347
- for (const file of fs4.readdirSync(dir)) {
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 = path4.join(claudeDir, "skills");
364
- const disabledDir2 = path4.join(claudeDir, "skills", ".disabled");
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 (fs4.existsSync(skillsDir2)) {
368
- for (const file of fs4.readdirSync(skillsDir2)) {
700
+ if (fs11.existsSync(skillsDir2)) {
701
+ for (const file of fs11.readdirSync(skillsDir2)) {
369
702
  if (!file.endsWith(".md")) continue;
370
- const stat = fs4.statSync(path4.join(skillsDir2, file));
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 (fs4.existsSync(disabledDir2)) {
380
- for (const file of fs4.readdirSync(disabledDir2)) {
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 = path4.join(claudeDir, "plugins", "installed_plugins.json");
723
+ const installedPath = path12.join(claudeDir, "plugins", "installed_plugins.json");
391
724
  let pluginsBlocked = [];
392
725
  try {
393
- const raw = JSON.parse(fs4.readFileSync(installedPath, "utf-8"));
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 fs5 from "fs";
408
- import * as path5 from "path";
409
- import * as os3 from "os";
410
- var defaultClaudeDir5 = path5.join(os3.homedir(), ".claude");
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
- fs5.writeFileSync(path5.join(claudeDir, "SKILLS.md"), content);
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("Manage Claude Code skills").version("0.1.2").option("--claude-dir <path>", "Override the Claude config directory (default: ~/.claude)");
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 claudeDir = getClaudeDir(program.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);
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 claudeDir = getClaudeDir(program.opts());
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 claudeDir = getClaudeDir(program.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);
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 claudeDir = getClaudeDir(program.opts());
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 claudeDir = getClaudeDir(program.opts());
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: mkdirSync3, writeFileSync: writeFileSync4, renameSync: renameSync3 } = fs6;
1194
+ const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync4, renameSync: renameSync4 } = fs13;
740
1195
  const dir = storeFile.replace(/\/[^/]+$/, "");
741
- mkdirSync3(dir, { recursive: true });
1196
+ mkdirSync4(dir, { recursive: true });
742
1197
  writeFileSync4(storeFile + ".tmp", JSON.stringify(store, null, 2));
743
- renameSync3(storeFile + ".tmp", storeFile);
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
- fs6.writeFileSync(opts.out, json);
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 = fs6.readFileSync(file, "utf-8");
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
- fs6.writeFileSync(opts.out, content);
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillswitch",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Manage Claude Code skills — profiles, disable/enable, catalog generation",
5
5
  "type": "module",
6
6
  "bin": {