skillio 0.1.10 → 0.1.12
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 +237 -263
- package/dist/shared/chunk-0qvp6v8g.js +138 -0
- package/dist/shared/chunk-j1p4zpqy.js +252 -0
- package/package.json +1 -1
- package/dist/shared/chunk-eq7h491z.js +0 -113
- package/dist/shared/chunk-s3421yr2.js +0 -27
package/dist/cli.js
CHANGED
|
@@ -3,11 +3,15 @@ import {
|
|
|
3
3
|
__require,
|
|
4
4
|
cyan,
|
|
5
5
|
detectColorSupport,
|
|
6
|
+
discoverSkills,
|
|
7
|
+
getLockPath,
|
|
6
8
|
green,
|
|
9
|
+
readLock,
|
|
7
10
|
red,
|
|
11
|
+
removeSkillFromLock,
|
|
8
12
|
setColorEnabled,
|
|
9
13
|
yellow
|
|
10
|
-
} from "./shared/chunk-
|
|
14
|
+
} from "./shared/chunk-0qvp6v8g.js";
|
|
11
15
|
|
|
12
16
|
// src/cli.ts
|
|
13
17
|
import { createRequire } from "node:module";
|
|
@@ -554,115 +558,6 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
|
|
|
554
558
|
return [`--${long}`, `-${short}`];
|
|
555
559
|
}
|
|
556
560
|
|
|
557
|
-
// src/lock/file.ts
|
|
558
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
559
|
-
import { homedir } from "node:os";
|
|
560
|
-
import { dirname, join } from "node:path";
|
|
561
|
-
function getLockPath(global) {
|
|
562
|
-
return global ? join(homedir(), ".agents", ".skill-lock.json") : "skills-lock.json";
|
|
563
|
-
}
|
|
564
|
-
function readLock(path) {
|
|
565
|
-
if (!existsSync(path))
|
|
566
|
-
return { skills: {} };
|
|
567
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
568
|
-
}
|
|
569
|
-
function writeLock(path, lock) {
|
|
570
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
571
|
-
const tmp = join(dirname(path), `.${Date.now()}.skill-lock.json`);
|
|
572
|
-
writeFileSync(tmp, `${JSON.stringify(lock, null, 2)}
|
|
573
|
-
`);
|
|
574
|
-
renameSync(tmp, path);
|
|
575
|
-
}
|
|
576
|
-
function removeSkillFromLock(path, skill) {
|
|
577
|
-
if (!existsSync(path))
|
|
578
|
-
return { removed: false };
|
|
579
|
-
const lock = readLock(path);
|
|
580
|
-
if (!Object.hasOwn(lock.skills, skill))
|
|
581
|
-
return { removed: false };
|
|
582
|
-
delete lock.skills[skill];
|
|
583
|
-
writeLock(path, lock);
|
|
584
|
-
return { removed: true };
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// src/utils/discover-skills.ts
|
|
588
|
-
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
589
|
-
import { homedir as homedir3 } from "node:os";
|
|
590
|
-
import { dirname as dirname3, join as join3, resolve as resolve2 } from "node:path";
|
|
591
|
-
|
|
592
|
-
// src/utils/skill-files.ts
|
|
593
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
594
|
-
import { homedir as homedir2 } from "node:os";
|
|
595
|
-
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
596
|
-
var CHARS_PER_TOKEN = 4;
|
|
597
|
-
function extractFrontmatter(content) {
|
|
598
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
599
|
-
return match?.[1];
|
|
600
|
-
}
|
|
601
|
-
function estimateTokens(text) {
|
|
602
|
-
return Math.round(text.length / CHARS_PER_TOKEN);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// src/utils/discover-skills.ts
|
|
606
|
-
function resolveRoots(input) {
|
|
607
|
-
if (input.isGlobal) {
|
|
608
|
-
return {
|
|
609
|
-
claude: join3(homedir3(), ".claude", "skills"),
|
|
610
|
-
agents: join3(homedir3(), ".agents", "skills")
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
const repo = dirname3(resolve2(input.lockPath));
|
|
614
|
-
return {
|
|
615
|
-
claude: join3(repo, ".claude", "skills"),
|
|
616
|
-
agents: join3(repo, ".agents", "skills")
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
function listSkillNames(root) {
|
|
620
|
-
if (!root || !existsSync3(root))
|
|
621
|
-
return [];
|
|
622
|
-
return readdirSync(root).filter((name) => {
|
|
623
|
-
const skill = join3(root, name, "SKILL.md");
|
|
624
|
-
return existsSync3(skill) && statSync(skill).isFile();
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
function tokensFromFile(path) {
|
|
628
|
-
const content = readFileSync3(path, "utf8");
|
|
629
|
-
const fm = extractFrontmatter(content);
|
|
630
|
-
if (fm === undefined)
|
|
631
|
-
return { status: "no-frontmatter" };
|
|
632
|
-
return { tokens: estimateTokens(fm), status: "ok" };
|
|
633
|
-
}
|
|
634
|
-
function discoverSkills(input) {
|
|
635
|
-
const roots = resolveRoots(input);
|
|
636
|
-
const lock = readLock(input.lockPath);
|
|
637
|
-
const lockNames = Object.keys(lock.skills);
|
|
638
|
-
const claudeNames = listSkillNames(roots.claude);
|
|
639
|
-
const agentsNames = listSkillNames(roots.agents);
|
|
640
|
-
const all = new Set([...lockNames, ...claudeNames, ...agentsNames]);
|
|
641
|
-
const out = new Map;
|
|
642
|
-
for (const name of all) {
|
|
643
|
-
const sources = [];
|
|
644
|
-
if (lockNames.includes(name))
|
|
645
|
-
sources.push("lock");
|
|
646
|
-
if (claudeNames.includes(name))
|
|
647
|
-
sources.push(".claude");
|
|
648
|
-
if (agentsNames.includes(name))
|
|
649
|
-
sources.push(".agents");
|
|
650
|
-
let skillFile;
|
|
651
|
-
if (claudeNames.includes(name) && roots.claude) {
|
|
652
|
-
skillFile = join3(roots.claude, name, "SKILL.md");
|
|
653
|
-
} else if (agentsNames.includes(name) && roots.agents) {
|
|
654
|
-
skillFile = join3(roots.agents, name, "SKILL.md");
|
|
655
|
-
}
|
|
656
|
-
if (!skillFile) {
|
|
657
|
-
out.set(name, { name, sources, status: "missing" });
|
|
658
|
-
continue;
|
|
659
|
-
}
|
|
660
|
-
const { tokens, status } = tokensFromFile(skillFile);
|
|
661
|
-
out.set(name, { name, sources, skillFile, frontmatterTokens: tokens, status });
|
|
662
|
-
}
|
|
663
|
-
return out;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
561
|
// src/commands/cost.ts
|
|
667
562
|
function classify(total) {
|
|
668
563
|
if (total < 1000)
|
|
@@ -718,61 +613,98 @@ var costCommand = defineCommand({
|
|
|
718
613
|
});
|
|
719
614
|
|
|
720
615
|
// src/commands/list.ts
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
616
|
+
import { existsSync, lstatSync } from "node:fs";
|
|
617
|
+
import { homedir } from "node:os";
|
|
618
|
+
import { dirname, join, resolve } from "node:path";
|
|
619
|
+
function rootFor(isGlobal, lockPath, kind) {
|
|
620
|
+
if (isGlobal)
|
|
621
|
+
return join(homedir(), kind, "skills");
|
|
622
|
+
return join(dirname(resolve(lockPath)), kind, "skills");
|
|
623
|
+
}
|
|
624
|
+
function getInstall(root, name) {
|
|
625
|
+
const dir = join(root, name);
|
|
626
|
+
if (!existsSync(dir))
|
|
627
|
+
return;
|
|
628
|
+
return lstatSync(dir).isSymbolicLink() ? "symlink" : "real";
|
|
629
|
+
}
|
|
630
|
+
function paintDisk(n) {
|
|
631
|
+
if (n.install === "symlink")
|
|
632
|
+
return yellow(n.name);
|
|
633
|
+
if (n.install === "real")
|
|
634
|
+
return green(n.name);
|
|
635
|
+
return cyan(n.name);
|
|
636
|
+
}
|
|
637
|
+
function bySource(records, roots, lockLabel) {
|
|
638
|
+
const claudeRecords = records.filter((r) => r.sources.includes(".claude"));
|
|
639
|
+
const agentsRecords = records.filter((r) => r.sources.includes(".agents"));
|
|
640
|
+
const lockRecords = records.filter((r) => r.sources.includes("lock"));
|
|
641
|
+
const claudeNames = claudeRecords.map((r) => ({ name: r.name, install: getInstall(roots.claude, r.name) })).sort((a, b) => a.name.localeCompare(b.name));
|
|
642
|
+
const agentsNames = agentsRecords.map((r) => ({ name: r.name, install: getInstall(roots.agents, r.name) })).sort((a, b) => a.name.localeCompare(b.name));
|
|
643
|
+
const lockNames = lockRecords.map((r) => ({ name: r.name })).sort((a, b) => a.name.localeCompare(b.name));
|
|
726
644
|
return {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
tokens: sumTokens(claudeNames),
|
|
731
|
-
exists: true
|
|
732
|
-
},
|
|
733
|
-
agents: {
|
|
734
|
-
label: ".agents/skills",
|
|
735
|
-
names: agentsNames,
|
|
736
|
-
tokens: sumTokens(agentsNames),
|
|
737
|
-
exists: true
|
|
738
|
-
},
|
|
739
|
-
lock: {
|
|
740
|
-
label: "skills-lock.json",
|
|
741
|
-
names: lockNames,
|
|
742
|
-
tokens: sumTokens(lockNames),
|
|
743
|
-
exists: true
|
|
744
|
-
}
|
|
645
|
+
agents: { label: ".agents/skills", names: agentsNames, totalCount: agentsNames.length },
|
|
646
|
+
claude: { label: ".claude/skills", names: claudeNames, totalCount: claudeNames.length },
|
|
647
|
+
lock: { label: lockLabel, names: lockNames, totalCount: lockNames.length }
|
|
745
648
|
};
|
|
746
649
|
}
|
|
747
650
|
var listCommand = defineCommand({
|
|
748
|
-
meta: { description: "List skills per source with
|
|
651
|
+
meta: { description: "List skills per source with install-type coloring and lock orphan filter" },
|
|
749
652
|
args: {
|
|
750
653
|
global: { type: "boolean", alias: "g", default: false, description: "Use global scope" }
|
|
751
654
|
},
|
|
752
655
|
run({ args }) {
|
|
753
656
|
const lockPath = getLockPath(args.global);
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
const
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
const
|
|
765
|
-
const
|
|
657
|
+
const records = [
|
|
658
|
+
...discoverSkills({ isGlobal: args.global, cwd: process.cwd(), lockPath }).values()
|
|
659
|
+
];
|
|
660
|
+
const roots = {
|
|
661
|
+
claude: rootFor(args.global, lockPath, ".claude"),
|
|
662
|
+
agents: rootFor(args.global, lockPath, ".agents")
|
|
663
|
+
};
|
|
664
|
+
const lockLabel = args.global ? ".agents/.skill-lock.json" : "skills-lock.json";
|
|
665
|
+
const rows = bySource(records, roots, lockLabel);
|
|
666
|
+
console.log(args.global ? "Global" : "Local");
|
|
667
|
+
const claudeSet = new Set(rows.claude.names.map((n) => n.name));
|
|
668
|
+
const agentsSet = new Set(rows.agents.names.map((n) => n.name));
|
|
669
|
+
const orphans = rows.lock.names.filter((n) => !claudeSet.has(n.name) && !agentsSet.has(n.name));
|
|
670
|
+
const sourceRows = [
|
|
671
|
+
{
|
|
672
|
+
row: rows.agents,
|
|
673
|
+
render: () => rows.agents.names.map(paintDisk).join(" ")
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
row: rows.claude,
|
|
677
|
+
render: () => rows.claude.names.map(paintDisk).join(" ")
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
row: rows.lock,
|
|
681
|
+
render: () => {
|
|
682
|
+
if (rows.lock.totalCount === 0)
|
|
683
|
+
return "";
|
|
684
|
+
if (orphans.length === 0)
|
|
685
|
+
return green("All skills onboard!");
|
|
686
|
+
return orphans.map((n) => red(n.name)).join(" ");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
];
|
|
690
|
+
const labelWidth = Math.max(...sourceRows.map((r) => r.row.label.length));
|
|
691
|
+
const countCells = sourceRows.map((r) => `${r.row.totalCount} skill${r.row.totalCount === 1 ? "" : "s"}`);
|
|
766
692
|
const countWidth = Math.max(...countCells.map((c) => c.length));
|
|
767
693
|
for (let i = 0;i < sourceRows.length; i++) {
|
|
768
|
-
const
|
|
769
|
-
if (!
|
|
694
|
+
const entry = sourceRows[i];
|
|
695
|
+
if (!entry)
|
|
770
696
|
continue;
|
|
771
697
|
const countCell = countCells[i] ?? "";
|
|
772
|
-
const namesText =
|
|
773
|
-
const line = `${row.label.padEnd(labelWidth)} : ${countCell.padEnd(countWidth)}${namesText ? ` : ${namesText}` : ""}`;
|
|
698
|
+
const namesText = entry.render();
|
|
699
|
+
const line = `${entry.row.label.padEnd(labelWidth)} : ${countCell.padEnd(countWidth)}${namesText ? ` : ${namesText}` : ""}`;
|
|
774
700
|
console.log(line.trimEnd());
|
|
775
701
|
}
|
|
702
|
+
const claudeNames = rows.claude.names.map((n) => n.name);
|
|
703
|
+
const agentsNames = rows.agents.names.map((n) => n.name);
|
|
704
|
+
const lockNames = rows.lock.names.map((n) => n.name);
|
|
705
|
+
const lockOnly = lockNames.filter((n) => !claudeNames.includes(n) && !agentsNames.includes(n));
|
|
706
|
+
const claudeNotInLock = claudeNames.filter((n) => !lockNames.includes(n));
|
|
707
|
+
const agentsNotInLock = agentsNames.filter((n) => !lockNames.includes(n));
|
|
776
708
|
const diffs = [];
|
|
777
709
|
if (lockOnly.length) {
|
|
778
710
|
diffs.push(`skills-lock.json has ${lockOnly.length} skill${lockOnly.length === 1 ? "" : "s"} missing on disk: ${lockOnly.map(cyan).join(", ")}`);
|
|
@@ -792,46 +724,92 @@ var listCommand = defineCommand({
|
|
|
792
724
|
});
|
|
793
725
|
|
|
794
726
|
// src/commands/remove.ts
|
|
795
|
-
import { existsSync as
|
|
796
|
-
import { homedir as
|
|
797
|
-
import { dirname as
|
|
727
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
728
|
+
import { homedir as homedir2 } from "node:os";
|
|
729
|
+
import { dirname as dirname2, join as join3, resolve as resolve3 } from "node:path";
|
|
798
730
|
|
|
799
731
|
// src/utils/confirm.ts
|
|
800
|
-
import {
|
|
732
|
+
import { emitKeypressEvents } from "node:readline";
|
|
801
733
|
async function confirm(question, opts = {}) {
|
|
802
734
|
const input = opts.input ?? process.stdin;
|
|
803
735
|
const output = opts.output ?? process.stdout;
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
output
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
736
|
+
if (!input.isTTY || !output.isTTY) {
|
|
737
|
+
const { createInterface } = await import("node:readline/promises");
|
|
738
|
+
const rl = createInterface({ input, output });
|
|
739
|
+
try {
|
|
740
|
+
const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
|
|
741
|
+
return answer === "y" || answer === "yes";
|
|
742
|
+
} finally {
|
|
743
|
+
rl.close();
|
|
744
|
+
}
|
|
813
745
|
}
|
|
746
|
+
output.write(`${question} [y/N] `);
|
|
747
|
+
emitKeypressEvents(input);
|
|
748
|
+
if (input.setRawMode)
|
|
749
|
+
input.setRawMode(true);
|
|
750
|
+
input.resume();
|
|
751
|
+
return new Promise((resolve2) => {
|
|
752
|
+
const onKey = (str, key) => {
|
|
753
|
+
const lower = (str ?? "").toLowerCase();
|
|
754
|
+
if (key.ctrl && key.name === "c") {
|
|
755
|
+
cleanup();
|
|
756
|
+
output.write(`
|
|
757
|
+
`);
|
|
758
|
+
resolve2(false);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (lower === "y") {
|
|
762
|
+
cleanup();
|
|
763
|
+
output.write(`y
|
|
764
|
+
`);
|
|
765
|
+
resolve2(true);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (lower === "n" || key.name === "return" || key.name === "escape" || lower === "q") {
|
|
769
|
+
cleanup();
|
|
770
|
+
output.write(`${lower || "n"}
|
|
771
|
+
`);
|
|
772
|
+
resolve2(false);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
function onSigterm() {
|
|
777
|
+
cleanup();
|
|
778
|
+
output.write(`
|
|
779
|
+
`);
|
|
780
|
+
resolve2(false);
|
|
781
|
+
}
|
|
782
|
+
function cleanup() {
|
|
783
|
+
input.removeListener("keypress", onKey);
|
|
784
|
+
process.removeListener("SIGTERM", onSigterm);
|
|
785
|
+
if (input.setRawMode)
|
|
786
|
+
input.setRawMode(false);
|
|
787
|
+
input.pause();
|
|
788
|
+
}
|
|
789
|
+
process.once("SIGTERM", onSigterm);
|
|
790
|
+
input.on("keypress", onKey);
|
|
791
|
+
});
|
|
814
792
|
}
|
|
815
793
|
|
|
816
794
|
// src/utils/fs-rm.ts
|
|
817
|
-
import { existsSync as
|
|
818
|
-
import { join as
|
|
795
|
+
import { existsSync as existsSync2, lstatSync as lstatSync2, readdirSync, rmSync } from "node:fs";
|
|
796
|
+
import { join as join2, resolve as resolve2 } from "node:path";
|
|
819
797
|
function isInside(target, root) {
|
|
820
|
-
const t =
|
|
821
|
-
const r =
|
|
798
|
+
const t = resolve2(target);
|
|
799
|
+
const r = resolve2(root);
|
|
822
800
|
return t === r || t.startsWith(`${r}/`);
|
|
823
801
|
}
|
|
824
802
|
function countFiles(path) {
|
|
825
|
-
if (!
|
|
803
|
+
if (!existsSync2(path))
|
|
826
804
|
return 0;
|
|
827
|
-
const stat =
|
|
805
|
+
const stat = lstatSync2(path);
|
|
828
806
|
if (stat.isFile())
|
|
829
807
|
return 1;
|
|
830
808
|
if (!stat.isDirectory())
|
|
831
809
|
return 0;
|
|
832
810
|
let n = 0;
|
|
833
|
-
for (const entry of
|
|
834
|
-
n += countFiles(
|
|
811
|
+
for (const entry of readdirSync(path)) {
|
|
812
|
+
n += countFiles(join2(path, entry));
|
|
835
813
|
}
|
|
836
814
|
return n;
|
|
837
815
|
}
|
|
@@ -840,60 +818,51 @@ function rmSkillDir(path, opts) {
|
|
|
840
818
|
if (!safe) {
|
|
841
819
|
throw new Error(`Refusing to delete: "${path}" is outside allowed roots`);
|
|
842
820
|
}
|
|
843
|
-
if (!
|
|
821
|
+
if (!existsSync2(path))
|
|
844
822
|
return { removed: false, fileCount: 0 };
|
|
845
823
|
const fileCount = countFiles(path);
|
|
846
824
|
rmSync(path, { recursive: true, force: true });
|
|
847
825
|
return { removed: true, fileCount };
|
|
848
826
|
}
|
|
849
827
|
|
|
850
|
-
// src/utils/git.ts
|
|
851
|
-
import { spawnSync } from "node:child_process";
|
|
852
|
-
import { dirname as dirname4, resolve as resolve4 } from "node:path";
|
|
853
|
-
function isTrackedByGit(path) {
|
|
854
|
-
const abs = resolve4(path);
|
|
855
|
-
const cwd = dirname4(abs);
|
|
856
|
-
const r = spawnSync("git", ["ls-files", "--error-unmatch", abs], {
|
|
857
|
-
cwd,
|
|
858
|
-
stdio: ["ignore", "ignore", "ignore"]
|
|
859
|
-
});
|
|
860
|
-
return r.status === 0;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
828
|
// src/commands/remove.ts
|
|
864
829
|
var q = (name) => `"${cyan(name)}"`;
|
|
865
830
|
function buildTarget(name, isGlobal, lockPath) {
|
|
866
831
|
const lock = readLock(lockPath);
|
|
867
832
|
const inLock = Object.hasOwn(lock.skills, name);
|
|
868
|
-
const baseClaude = isGlobal ?
|
|
869
|
-
const baseAgents = isGlobal ?
|
|
870
|
-
const claudeDir =
|
|
871
|
-
const agentsDir =
|
|
833
|
+
const baseClaude = isGlobal ? join3(homedir2(), ".claude", "skills") : join3(dirname2(resolve3(lockPath)), ".claude", "skills");
|
|
834
|
+
const baseAgents = isGlobal ? join3(homedir2(), ".agents", "skills") : join3(dirname2(resolve3(lockPath)), ".agents", "skills");
|
|
835
|
+
const claudeDir = existsSync3(join3(baseClaude, name)) ? join3(baseClaude, name) : undefined;
|
|
836
|
+
const agentsDir = existsSync3(join3(baseAgents, name)) ? join3(baseAgents, name) : undefined;
|
|
872
837
|
return { name, inLock, claudeDir, agentsDir };
|
|
873
838
|
}
|
|
839
|
+
function collectAllTargets(isGlobal, lockPath) {
|
|
840
|
+
const map = discoverSkills({ isGlobal, cwd: process.cwd(), lockPath });
|
|
841
|
+
return [...map.keys()].sort().map((name) => buildTarget(name, isGlobal, lockPath));
|
|
842
|
+
}
|
|
874
843
|
function fileCount(dir) {
|
|
875
|
-
const { readdirSync:
|
|
844
|
+
const { readdirSync: readdirSync2, statSync } = __require("node:fs");
|
|
876
845
|
let n = 0;
|
|
877
846
|
const stack = [dir];
|
|
878
847
|
while (stack.length) {
|
|
879
848
|
const cur = stack.pop();
|
|
880
|
-
const stat =
|
|
849
|
+
const stat = statSync(cur);
|
|
881
850
|
if (stat.isFile())
|
|
882
851
|
n++;
|
|
883
852
|
else if (stat.isDirectory())
|
|
884
|
-
for (const e of
|
|
885
|
-
stack.push(
|
|
853
|
+
for (const e of readdirSync2(cur))
|
|
854
|
+
stack.push(join3(cur, e));
|
|
886
855
|
}
|
|
887
856
|
return n;
|
|
888
857
|
}
|
|
889
|
-
function printPlan(plan,
|
|
858
|
+
function printPlan(plan, modifyLock) {
|
|
890
859
|
const { target } = plan;
|
|
891
860
|
console.log(`Will remove ${q(target.name)}:`);
|
|
892
861
|
if (target.inLock) {
|
|
893
|
-
if (
|
|
894
|
-
console.log(" - skills-lock.json (skipped: git-tracked; use --force-lock)");
|
|
895
|
-
else
|
|
862
|
+
if (modifyLock)
|
|
896
863
|
console.log(" - skills-lock.json");
|
|
864
|
+
else
|
|
865
|
+
console.log(" - skills-lock.json (kept; use --force-lock to remove lock entry)");
|
|
897
866
|
} else {
|
|
898
867
|
console.log(" - skills-lock.json (not in lock)");
|
|
899
868
|
}
|
|
@@ -907,27 +876,38 @@ function printPlan(plan, lockTracked) {
|
|
|
907
876
|
console.log(" - .agents/skills/ (not found)");
|
|
908
877
|
}
|
|
909
878
|
var removeCommand = defineCommand({
|
|
910
|
-
meta: {
|
|
879
|
+
meta: {
|
|
880
|
+
description: "Remove one or more skills from on-disk dirs (lock preserved unless --force-lock)"
|
|
881
|
+
},
|
|
911
882
|
args: {
|
|
912
883
|
global: { type: "boolean", alias: "g", default: false, description: "Use global scope" },
|
|
913
884
|
"dry-run": { type: "boolean", default: false, description: "Print plan, do not delete" },
|
|
914
885
|
yes: { type: "boolean", alias: "y", default: false, description: "Skip confirmation prompt" },
|
|
886
|
+
all: { type: "boolean", default: false, description: "Remove every skill in scope" },
|
|
915
887
|
"force-lock": {
|
|
916
888
|
type: "boolean",
|
|
917
889
|
default: false,
|
|
918
|
-
description: "
|
|
890
|
+
description: "Also remove entry from skills-lock.json (default is to keep lock untouched)"
|
|
919
891
|
}
|
|
920
892
|
},
|
|
921
893
|
async run({ args }) {
|
|
922
|
-
const { global: isGlobal, "dry-run": dryRun, yes, "force-lock":
|
|
894
|
+
const { global: isGlobal, "dry-run": dryRun, yes, all, "force-lock": modifyLock } = args;
|
|
923
895
|
const subcmdIdx = process.argv.findIndex((a) => a === "remove" || a === "rm");
|
|
924
896
|
const names = process.argv.slice(subcmdIdx + 1).filter((a) => !a.startsWith("-"));
|
|
925
|
-
if (names.length
|
|
897
|
+
if (all && names.length > 0) {
|
|
898
|
+
console.error("--all is mutually exclusive with positional skill names");
|
|
899
|
+
process.exit(1);
|
|
900
|
+
}
|
|
901
|
+
if (!all && names.length === 0) {
|
|
926
902
|
console.error("No skill names provided");
|
|
927
903
|
process.exit(1);
|
|
928
904
|
}
|
|
929
905
|
const lockPath = getLockPath(isGlobal);
|
|
930
|
-
const targets = names.map((n) => buildTarget(n, isGlobal, lockPath));
|
|
906
|
+
const targets = all ? collectAllTargets(isGlobal, lockPath) : names.map((n) => buildTarget(n, isGlobal, lockPath));
|
|
907
|
+
if (all && targets.length === 0) {
|
|
908
|
+
console.log("No skills to remove in scope.");
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
931
911
|
const orphan = targets.filter((t) => !t.inLock && !t.claudeDir && !t.agentsDir);
|
|
932
912
|
if (orphan.length) {
|
|
933
913
|
for (const o of orphan)
|
|
@@ -939,32 +919,29 @@ var removeCommand = defineCommand({
|
|
|
939
919
|
claudeFileCount: t.claudeDir ? fileCount(t.claudeDir) : undefined,
|
|
940
920
|
agentsFileCount: t.agentsDir ? fileCount(t.agentsDir) : undefined
|
|
941
921
|
}));
|
|
942
|
-
const lockTracked = !forceLock && isTrackedByGit(lockPath);
|
|
943
922
|
for (const p of plans) {
|
|
944
|
-
printPlan(p,
|
|
923
|
+
printPlan(p, modifyLock);
|
|
945
924
|
console.log("");
|
|
946
925
|
}
|
|
947
|
-
if (lockTracked && plans.some((p) => p.target.inLock)) {
|
|
948
|
-
console.error(red("Skipping skills-lock.json (tracked by git; pass --force-lock to override)"));
|
|
949
|
-
}
|
|
950
926
|
if (dryRun)
|
|
951
927
|
return;
|
|
952
928
|
if (!yes) {
|
|
953
|
-
const
|
|
929
|
+
const promptText = all ? `Remove ALL ${plans.length} skills?` : "Proceed?";
|
|
930
|
+
const ok = await confirm(promptText);
|
|
954
931
|
if (!ok) {
|
|
955
932
|
console.log("Aborted");
|
|
956
933
|
process.exit(1);
|
|
957
934
|
}
|
|
958
935
|
}
|
|
959
|
-
const allowedRoots = [isGlobal ?
|
|
936
|
+
const allowedRoots = [isGlobal ? homedir2() : dirname2(resolve3(lockPath)), homedir2()];
|
|
960
937
|
for (const { target } of plans) {
|
|
961
938
|
if (target.inLock) {
|
|
962
|
-
if (
|
|
963
|
-
console.log(`Skipped skills-lock.json (git-tracked) for ${q(target.name)}`);
|
|
964
|
-
} else {
|
|
939
|
+
if (modifyLock) {
|
|
965
940
|
const r = removeSkillFromLock(lockPath, target.name);
|
|
966
941
|
if (r.removed)
|
|
967
942
|
console.log(`Removed ${q(target.name)} from skills-lock.json`);
|
|
943
|
+
} else {
|
|
944
|
+
console.log(`Kept ${q(target.name)} in skills-lock.json (no --force-lock)`);
|
|
968
945
|
}
|
|
969
946
|
} else {
|
|
970
947
|
console.log(`Skipped skills-lock.json (not in lock)`);
|
|
@@ -986,11 +963,11 @@ var removeCommand = defineCommand({
|
|
|
986
963
|
});
|
|
987
964
|
|
|
988
965
|
// src/commands/usage.ts
|
|
989
|
-
import { existsSync as
|
|
990
|
-
import { join as
|
|
966
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
967
|
+
import { join as join7 } from "node:path";
|
|
991
968
|
|
|
992
969
|
// src/readers/claude.ts
|
|
993
|
-
import { readFileSync as
|
|
970
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
994
971
|
|
|
995
972
|
// src/utils/codex-cmd.ts
|
|
996
973
|
var READ_LIKE = new Set(["cat", "sed", "head", "tail", "bat", "batcat", "less", "more"]);
|
|
@@ -1214,26 +1191,26 @@ function isUserTurnEntry(entry) {
|
|
|
1214
1191
|
}
|
|
1215
1192
|
|
|
1216
1193
|
// src/utils/expand-home.ts
|
|
1217
|
-
import { homedir as
|
|
1218
|
-
import { join as
|
|
1194
|
+
import { homedir as homedir3 } from "node:os";
|
|
1195
|
+
import { join as join4, resolve as resolve4 } from "node:path";
|
|
1219
1196
|
function expandHome(p) {
|
|
1220
1197
|
if (p === "~")
|
|
1221
|
-
return
|
|
1198
|
+
return homedir3();
|
|
1222
1199
|
if (p.startsWith("~/"))
|
|
1223
|
-
return
|
|
1224
|
-
return
|
|
1200
|
+
return join4(homedir3(), p.slice(2));
|
|
1201
|
+
return resolve4(p);
|
|
1225
1202
|
}
|
|
1226
1203
|
|
|
1227
1204
|
// src/utils/jsonl.ts
|
|
1228
|
-
import { readdirSync as
|
|
1229
|
-
import { join as
|
|
1205
|
+
import { readdirSync as readdirSync2, readFileSync, statSync } from "node:fs";
|
|
1206
|
+
import { join as join5 } from "node:path";
|
|
1230
1207
|
function* findJsonlFiles(dir, since) {
|
|
1231
|
-
for (const item of
|
|
1232
|
-
const path =
|
|
1208
|
+
for (const item of readdirSync2(dir, { withFileTypes: true })) {
|
|
1209
|
+
const path = join5(dir, item.name);
|
|
1233
1210
|
if (item.isDirectory()) {
|
|
1234
1211
|
yield* findJsonlFiles(path, since);
|
|
1235
1212
|
} else if (item.isFile() && item.name.endsWith(".jsonl")) {
|
|
1236
|
-
if (!since ||
|
|
1213
|
+
if (!since || statSync(path).mtime >= since)
|
|
1237
1214
|
yield path;
|
|
1238
1215
|
}
|
|
1239
1216
|
}
|
|
@@ -1263,7 +1240,7 @@ function readClaudeUsage(options) {
|
|
|
1263
1240
|
let prevSkill = null;
|
|
1264
1241
|
const sessionAttr = new Map;
|
|
1265
1242
|
const sessionAct = new Map;
|
|
1266
|
-
for (const line of
|
|
1243
|
+
for (const line of readFileSync2(file, "utf8").split(`
|
|
1267
1244
|
`)) {
|
|
1268
1245
|
if (!line.trim())
|
|
1269
1246
|
continue;
|
|
@@ -1316,14 +1293,14 @@ function readClaudeUsage(options) {
|
|
|
1316
1293
|
}
|
|
1317
1294
|
|
|
1318
1295
|
// src/readers/codex.ts
|
|
1319
|
-
import { existsSync as
|
|
1296
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
|
|
1320
1297
|
|
|
1321
1298
|
// src/utils/scope.ts
|
|
1322
|
-
import { existsSync as
|
|
1323
|
-
import { homedir as
|
|
1324
|
-
import { dirname as
|
|
1299
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
1300
|
+
import { homedir as homedir4 } from "node:os";
|
|
1301
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
1325
1302
|
function detectScope(opts) {
|
|
1326
|
-
const home = opts.home ??
|
|
1303
|
+
const home = opts.home ?? homedir4();
|
|
1327
1304
|
if (opts.global || opts.rootOverride)
|
|
1328
1305
|
return { global: true };
|
|
1329
1306
|
if (norm(opts.cwd) === norm(home))
|
|
@@ -1341,9 +1318,9 @@ function encodeClaudeProjectDir(absPath) {
|
|
|
1341
1318
|
function findGitRoot(start) {
|
|
1342
1319
|
let dir = start;
|
|
1343
1320
|
while (true) {
|
|
1344
|
-
if (
|
|
1321
|
+
if (existsSync4(join6(dir, ".git")))
|
|
1345
1322
|
return dir;
|
|
1346
|
-
const parent =
|
|
1323
|
+
const parent = dirname3(dir);
|
|
1347
1324
|
if (parent === dir)
|
|
1348
1325
|
return;
|
|
1349
1326
|
dir = parent;
|
|
@@ -1357,7 +1334,7 @@ function norm(p) {
|
|
|
1357
1334
|
function readSessionCwd(file) {
|
|
1358
1335
|
let head;
|
|
1359
1336
|
try {
|
|
1360
|
-
head =
|
|
1337
|
+
head = readFileSync3(file, "utf8");
|
|
1361
1338
|
} catch {
|
|
1362
1339
|
return;
|
|
1363
1340
|
}
|
|
@@ -1390,7 +1367,7 @@ function readCodexActivations(options) {
|
|
|
1390
1367
|
continue;
|
|
1391
1368
|
}
|
|
1392
1369
|
filesRead++;
|
|
1393
|
-
for (const line of
|
|
1370
|
+
for (const line of readFileSync3(file, "utf8").split(`
|
|
1394
1371
|
`)) {
|
|
1395
1372
|
if (!line.trim())
|
|
1396
1373
|
continue;
|
|
@@ -1414,9 +1391,9 @@ function readCodexMentions(options) {
|
|
|
1414
1391
|
const historyPath = expandHome(options.history ?? "~/.codex/history.jsonl");
|
|
1415
1392
|
const counts = new Map;
|
|
1416
1393
|
let linesRead = 0;
|
|
1417
|
-
if (!
|
|
1394
|
+
if (!existsSync5(historyPath))
|
|
1418
1395
|
return { counts, filesRead: 0, linesRead: 0 };
|
|
1419
|
-
for (const line of
|
|
1396
|
+
for (const line of readFileSync3(historyPath, "utf8").split(`
|
|
1420
1397
|
`)) {
|
|
1421
1398
|
if (!line.trim())
|
|
1422
1399
|
continue;
|
|
@@ -1442,20 +1419,18 @@ var MINUTE_MS = 60 * SECOND_MS;
|
|
|
1442
1419
|
var HOUR_MS = 60 * MINUTE_MS;
|
|
1443
1420
|
var DAY_MS = 24 * HOUR_MS;
|
|
1444
1421
|
var UNITS_MS = {
|
|
1445
|
-
|
|
1446
|
-
|
|
1422
|
+
s: SECOND_MS,
|
|
1423
|
+
m: MINUTE_MS,
|
|
1447
1424
|
h: HOUR_MS,
|
|
1448
1425
|
d: DAY_MS,
|
|
1449
|
-
w: 7 * DAY_MS
|
|
1450
|
-
m: 30 * DAY_MS,
|
|
1451
|
-
y: 365 * DAY_MS
|
|
1426
|
+
w: 7 * DAY_MS
|
|
1452
1427
|
};
|
|
1453
1428
|
function parsePeriod(period) {
|
|
1454
1429
|
if (period === "all")
|
|
1455
1430
|
return Number.POSITIVE_INFINITY;
|
|
1456
|
-
const match = period.match(/^(\d+)(
|
|
1431
|
+
const match = period.match(/^(\d+)([smhdw])$/);
|
|
1457
1432
|
if (!match) {
|
|
1458
|
-
throw new Error(`Invalid period: "${period}". Use values like
|
|
1433
|
+
throw new Error(`Invalid period: "${period}". Use values like 60s, 30m, 24h, 30d, 2w, all.`);
|
|
1459
1434
|
}
|
|
1460
1435
|
const unit = UNITS_MS[match[2] ?? ""] ?? 0;
|
|
1461
1436
|
return Number(match[1]) * unit;
|
|
@@ -1490,7 +1465,7 @@ var usageArgs = {
|
|
|
1490
1465
|
type: "string",
|
|
1491
1466
|
alias: "p",
|
|
1492
1467
|
default: "all",
|
|
1493
|
-
description: "
|
|
1468
|
+
description: "60s, 30m, 24h, 30d, 2w, all"
|
|
1494
1469
|
},
|
|
1495
1470
|
since: { type: "string", description: "yyyy-mm-dd, overrides --period" },
|
|
1496
1471
|
mode: {
|
|
@@ -1522,8 +1497,8 @@ async function runUsage(args) {
|
|
|
1522
1497
|
cwd: process.cwd()
|
|
1523
1498
|
});
|
|
1524
1499
|
const claudeProjectsRoot = expandHome("~/.claude/projects");
|
|
1525
|
-
const claudeRoot = args.root ?? (scope.projectRoot ?
|
|
1526
|
-
const claudeRootMissing = !args.root && !!scope.projectRoot && !
|
|
1500
|
+
const claudeRoot = args.root ?? (scope.projectRoot ? join7(claudeProjectsRoot, encodeClaudeProjectDir(scope.projectRoot)) : claudeProjectsRoot);
|
|
1501
|
+
const claudeRootMissing = !args.root && !!scope.projectRoot && !existsSync6(claudeRoot);
|
|
1527
1502
|
const lockPath = getLockPath(args.global);
|
|
1528
1503
|
const skillUniverse = discoverSkills({
|
|
1529
1504
|
isGlobal: args.global,
|
|
@@ -1597,7 +1572,6 @@ async function runUsage(args) {
|
|
|
1597
1572
|
let grandActivations = 0;
|
|
1598
1573
|
for (const { agent, rows } of results) {
|
|
1599
1574
|
const activations = rows.reduce((acc, r) => acc + r.count, 0);
|
|
1600
|
-
console.log("");
|
|
1601
1575
|
console.log(`${agent} ${rows.length} skill${rows.length === 1 ? "" : "s"} ${activations} time${activations === 1 ? "" : "s"} by ${periodLabel}`);
|
|
1602
1576
|
if (rows.length === 0)
|
|
1603
1577
|
continue;
|
|
@@ -1625,15 +1599,15 @@ var usageCommand = defineCommand({
|
|
|
1625
1599
|
});
|
|
1626
1600
|
|
|
1627
1601
|
// src/utils/update-check.ts
|
|
1628
|
-
import { mkdirSync
|
|
1602
|
+
import { mkdirSync, readFileSync as readFileSync4, writeFileSync } from "node:fs";
|
|
1629
1603
|
import { get } from "node:https";
|
|
1630
|
-
import { homedir as
|
|
1631
|
-
import { dirname as
|
|
1604
|
+
import { homedir as homedir5 } from "node:os";
|
|
1605
|
+
import { dirname as dirname4, join as join8 } from "node:path";
|
|
1632
1606
|
var PKG = "skillio";
|
|
1633
1607
|
var TTL_MS = 24 * 60 * 60 * 1000;
|
|
1634
1608
|
var FETCH_TIMEOUT_MS = 1500;
|
|
1635
1609
|
function getCachePath() {
|
|
1636
|
-
return
|
|
1610
|
+
return join8(homedir5(), ".cache", "skillio", "version.json");
|
|
1637
1611
|
}
|
|
1638
1612
|
function compareVersions(a, b) {
|
|
1639
1613
|
const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
@@ -1648,23 +1622,23 @@ function compareVersions(a, b) {
|
|
|
1648
1622
|
}
|
|
1649
1623
|
function readCache(path = getCachePath()) {
|
|
1650
1624
|
try {
|
|
1651
|
-
return JSON.parse(
|
|
1625
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
1652
1626
|
} catch {
|
|
1653
1627
|
return;
|
|
1654
1628
|
}
|
|
1655
1629
|
}
|
|
1656
1630
|
function writeCache(cache, path = getCachePath()) {
|
|
1657
1631
|
try {
|
|
1658
|
-
|
|
1659
|
-
|
|
1632
|
+
mkdirSync(dirname4(path), { recursive: true });
|
|
1633
|
+
writeFileSync(path, JSON.stringify(cache));
|
|
1660
1634
|
} catch {}
|
|
1661
1635
|
}
|
|
1662
1636
|
function fetchLatest() {
|
|
1663
|
-
return new Promise((
|
|
1637
|
+
return new Promise((resolve5) => {
|
|
1664
1638
|
const req = get(`https://registry.npmjs.org/${PKG}/latest`, { timeout: FETCH_TIMEOUT_MS }, (res) => {
|
|
1665
1639
|
if (res.statusCode !== 200) {
|
|
1666
1640
|
res.resume();
|
|
1667
|
-
|
|
1641
|
+
resolve5(undefined);
|
|
1668
1642
|
return;
|
|
1669
1643
|
}
|
|
1670
1644
|
let body = "";
|
|
@@ -1675,16 +1649,16 @@ function fetchLatest() {
|
|
|
1675
1649
|
res.on("end", () => {
|
|
1676
1650
|
try {
|
|
1677
1651
|
const data = JSON.parse(body);
|
|
1678
|
-
|
|
1652
|
+
resolve5(typeof data.version === "string" ? data.version : undefined);
|
|
1679
1653
|
} catch {
|
|
1680
|
-
|
|
1654
|
+
resolve5(undefined);
|
|
1681
1655
|
}
|
|
1682
1656
|
});
|
|
1683
1657
|
});
|
|
1684
|
-
req.on("error", () =>
|
|
1658
|
+
req.on("error", () => resolve5(undefined));
|
|
1685
1659
|
req.on("timeout", () => {
|
|
1686
1660
|
req.destroy();
|
|
1687
|
-
|
|
1661
|
+
resolve5(undefined);
|
|
1688
1662
|
});
|
|
1689
1663
|
});
|
|
1690
1664
|
}
|
|
@@ -1780,7 +1754,7 @@ function printRootHelp() {
|
|
|
1780
1754
|
" -h, --help Show this help and exit",
|
|
1781
1755
|
" -v, --version Show version and exit",
|
|
1782
1756
|
" -g, --global Use global scope (default: false)",
|
|
1783
|
-
" -p, --period Period for `usage`:
|
|
1757
|
+
" -p, --period Period for `usage`: 60s, 30m, 24h, 30d, 2w, all (default: all)",
|
|
1784
1758
|
" -a, --agent Agent for `usage`: claude-code, codex (default: both)",
|
|
1785
1759
|
"",
|
|
1786
1760
|
"COMMANDS",
|
|
@@ -1845,7 +1819,7 @@ var main = defineCommand({
|
|
|
1845
1819
|
return;
|
|
1846
1820
|
const interactive = process.stdout.isTTY && process.stdin.isTTY;
|
|
1847
1821
|
if (interactive) {
|
|
1848
|
-
const { runPicker } = await import("./shared/chunk-
|
|
1822
|
+
const { runPicker } = await import("./shared/chunk-j1p4zpqy.js");
|
|
1849
1823
|
const status = await runPicker({
|
|
1850
1824
|
global: args.global ?? false
|
|
1851
1825
|
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/lock/file.ts
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
function getLockPath(global) {
|
|
9
|
+
return global ? join(homedir(), ".agents", ".skill-lock.json") : "skills-lock.json";
|
|
10
|
+
}
|
|
11
|
+
function readLock(path) {
|
|
12
|
+
if (!existsSync(path))
|
|
13
|
+
return { skills: {} };
|
|
14
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
function writeLock(path, lock) {
|
|
17
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
18
|
+
const tmp = join(dirname(path), `.${Date.now()}.skill-lock.json`);
|
|
19
|
+
writeFileSync(tmp, `${JSON.stringify(lock, null, 2)}
|
|
20
|
+
`);
|
|
21
|
+
renameSync(tmp, path);
|
|
22
|
+
}
|
|
23
|
+
function removeSkillFromLock(path, skill) {
|
|
24
|
+
if (!existsSync(path))
|
|
25
|
+
return { removed: false };
|
|
26
|
+
const lock = readLock(path);
|
|
27
|
+
if (!Object.hasOwn(lock.skills, skill))
|
|
28
|
+
return { removed: false };
|
|
29
|
+
delete lock.skills[skill];
|
|
30
|
+
writeLock(path, lock);
|
|
31
|
+
return { removed: true };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/utils/ansi.ts
|
|
35
|
+
var enabled = false;
|
|
36
|
+
function setColorEnabled(value) {
|
|
37
|
+
enabled = value;
|
|
38
|
+
}
|
|
39
|
+
function detectColorSupport() {
|
|
40
|
+
if (process.env.NO_COLOR)
|
|
41
|
+
return false;
|
|
42
|
+
if (process.env.FORCE_COLOR)
|
|
43
|
+
return true;
|
|
44
|
+
return Boolean(process.stdout.isTTY);
|
|
45
|
+
}
|
|
46
|
+
function green(s) {
|
|
47
|
+
return enabled ? `\x1B[32m${s}\x1B[0m` : s;
|
|
48
|
+
}
|
|
49
|
+
function yellow(s) {
|
|
50
|
+
return enabled ? `\x1B[33m${s}\x1B[0m` : s;
|
|
51
|
+
}
|
|
52
|
+
function red(s) {
|
|
53
|
+
return enabled ? `\x1B[31m${s}\x1B[0m` : s;
|
|
54
|
+
}
|
|
55
|
+
function cyan(s) {
|
|
56
|
+
return enabled ? `\x1B[36m${s}\x1B[0m` : s;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/utils/discover-skills.ts
|
|
60
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
61
|
+
import { homedir as homedir3 } from "node:os";
|
|
62
|
+
import { dirname as dirname3, join as join3, resolve as resolve2 } from "node:path";
|
|
63
|
+
|
|
64
|
+
// src/utils/skill-files.ts
|
|
65
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
66
|
+
import { homedir as homedir2 } from "node:os";
|
|
67
|
+
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
68
|
+
var CHARS_PER_TOKEN = 4;
|
|
69
|
+
function extractFrontmatter(content) {
|
|
70
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
71
|
+
return match?.[1];
|
|
72
|
+
}
|
|
73
|
+
function estimateTokens(text) {
|
|
74
|
+
return Math.round(text.length / CHARS_PER_TOKEN);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/utils/discover-skills.ts
|
|
78
|
+
function resolveRoots(input) {
|
|
79
|
+
if (input.isGlobal) {
|
|
80
|
+
return {
|
|
81
|
+
claude: join3(homedir3(), ".claude", "skills"),
|
|
82
|
+
agents: join3(homedir3(), ".agents", "skills")
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const repo = dirname3(resolve2(input.lockPath));
|
|
86
|
+
return {
|
|
87
|
+
claude: join3(repo, ".claude", "skills"),
|
|
88
|
+
agents: join3(repo, ".agents", "skills")
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function listSkillNames(root) {
|
|
92
|
+
if (!root || !existsSync3(root))
|
|
93
|
+
return [];
|
|
94
|
+
return readdirSync(root).filter((name) => {
|
|
95
|
+
const skill = join3(root, name, "SKILL.md");
|
|
96
|
+
return existsSync3(skill) && statSync(skill).isFile();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function tokensFromFile(path) {
|
|
100
|
+
const content = readFileSync3(path, "utf8");
|
|
101
|
+
const fm = extractFrontmatter(content);
|
|
102
|
+
if (fm === undefined)
|
|
103
|
+
return { status: "no-frontmatter" };
|
|
104
|
+
return { tokens: estimateTokens(fm), status: "ok" };
|
|
105
|
+
}
|
|
106
|
+
function discoverSkills(input) {
|
|
107
|
+
const roots = resolveRoots(input);
|
|
108
|
+
const lock = readLock(input.lockPath);
|
|
109
|
+
const lockNames = Object.keys(lock.skills);
|
|
110
|
+
const claudeNames = listSkillNames(roots.claude);
|
|
111
|
+
const agentsNames = listSkillNames(roots.agents);
|
|
112
|
+
const all = new Set([...lockNames, ...claudeNames, ...agentsNames]);
|
|
113
|
+
const out = new Map;
|
|
114
|
+
for (const name of all) {
|
|
115
|
+
const sources = [];
|
|
116
|
+
if (lockNames.includes(name))
|
|
117
|
+
sources.push("lock");
|
|
118
|
+
if (claudeNames.includes(name))
|
|
119
|
+
sources.push(".claude");
|
|
120
|
+
if (agentsNames.includes(name))
|
|
121
|
+
sources.push(".agents");
|
|
122
|
+
let skillFile;
|
|
123
|
+
if (claudeNames.includes(name) && roots.claude) {
|
|
124
|
+
skillFile = join3(roots.claude, name, "SKILL.md");
|
|
125
|
+
} else if (agentsNames.includes(name) && roots.agents) {
|
|
126
|
+
skillFile = join3(roots.agents, name, "SKILL.md");
|
|
127
|
+
}
|
|
128
|
+
if (!skillFile) {
|
|
129
|
+
out.set(name, { name, sources, status: "missing" });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const { tokens, status } = tokensFromFile(skillFile);
|
|
133
|
+
out.set(name, { name, sources, skillFile, frontmatterTokens: tokens, status });
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { __require, getLockPath, readLock, removeSkillFromLock, setColorEnabled, detectColorSupport, green, yellow, red, cyan, discoverSkills };
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cyan,
|
|
3
|
+
discoverSkills,
|
|
4
|
+
getLockPath,
|
|
5
|
+
red
|
|
6
|
+
} from "./chunk-0qvp6v8g.js";
|
|
7
|
+
|
|
8
|
+
// src/commands/picker.ts
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
// src/utils/list-removable.ts
|
|
12
|
+
function listRemovableTargets(input) {
|
|
13
|
+
const records = [...discoverSkills(input).values()];
|
|
14
|
+
const inLock = [];
|
|
15
|
+
const orphan = [];
|
|
16
|
+
for (const r of records) {
|
|
17
|
+
if (r.sources.includes("lock"))
|
|
18
|
+
inLock.push(r.name);
|
|
19
|
+
else
|
|
20
|
+
orphan.push(r.name);
|
|
21
|
+
}
|
|
22
|
+
inLock.sort();
|
|
23
|
+
orphan.sort();
|
|
24
|
+
return { inLock, orphan };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/utils/prompt.ts
|
|
28
|
+
import { emitKeypressEvents } from "node:readline";
|
|
29
|
+
async function select(params) {
|
|
30
|
+
const input = params.input ?? process.stdin;
|
|
31
|
+
const output = params.output ?? process.stdout;
|
|
32
|
+
if (!input.isTTY || !output.isTTY)
|
|
33
|
+
return null;
|
|
34
|
+
let cursor = 0;
|
|
35
|
+
const total = params.options.length;
|
|
36
|
+
function render() {
|
|
37
|
+
output.write(`${params.title}
|
|
38
|
+
`);
|
|
39
|
+
for (let i = 0;i < total; i++) {
|
|
40
|
+
const opt = params.options[i];
|
|
41
|
+
if (!opt)
|
|
42
|
+
continue;
|
|
43
|
+
const marker = i === cursor ? cyan(">") : " ";
|
|
44
|
+
output.write(`${marker} ${opt.label}
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function clear() {
|
|
49
|
+
output.write(`\x1B[${total + 1}A\x1B[J`);
|
|
50
|
+
}
|
|
51
|
+
emitKeypressEvents(input);
|
|
52
|
+
if (input.setRawMode)
|
|
53
|
+
input.setRawMode(true);
|
|
54
|
+
input.resume();
|
|
55
|
+
render();
|
|
56
|
+
return await new Promise((resolve) => {
|
|
57
|
+
const onKey = (_str, key) => {
|
|
58
|
+
if (key.ctrl && key.name === "c") {
|
|
59
|
+
cleanup();
|
|
60
|
+
resolve(null);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (key.name === "escape" || key.name === "q") {
|
|
64
|
+
cleanup();
|
|
65
|
+
resolve(null);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (key.name === "up" && cursor > 0) {
|
|
69
|
+
cursor--;
|
|
70
|
+
clear();
|
|
71
|
+
render();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (key.name === "down" && cursor < total - 1) {
|
|
75
|
+
cursor++;
|
|
76
|
+
clear();
|
|
77
|
+
render();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (key.name === "return") {
|
|
81
|
+
cleanup();
|
|
82
|
+
const chosen = params.options[cursor]?.value ?? null;
|
|
83
|
+
resolve(chosen);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
function onSigterm() {
|
|
88
|
+
cleanup();
|
|
89
|
+
resolve(null);
|
|
90
|
+
}
|
|
91
|
+
function cleanup() {
|
|
92
|
+
input.removeListener("keypress", onKey);
|
|
93
|
+
process.removeListener("SIGTERM", onSigterm);
|
|
94
|
+
if (input.setRawMode)
|
|
95
|
+
input.setRawMode(false);
|
|
96
|
+
input.pause();
|
|
97
|
+
}
|
|
98
|
+
process.once("SIGTERM", onSigterm);
|
|
99
|
+
input.on("keypress", onKey);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async function multiSelect(params) {
|
|
103
|
+
const input = params.input ?? process.stdin;
|
|
104
|
+
const output = params.output ?? process.stdout;
|
|
105
|
+
if (!input.isTTY || !output.isTTY)
|
|
106
|
+
return null;
|
|
107
|
+
let cursor = 0;
|
|
108
|
+
const total = params.options.length;
|
|
109
|
+
const selected = new Set;
|
|
110
|
+
function render() {
|
|
111
|
+
output.write(`${params.title}
|
|
112
|
+
`);
|
|
113
|
+
for (let i = 0;i < total; i++) {
|
|
114
|
+
const opt = params.options[i];
|
|
115
|
+
if (!opt)
|
|
116
|
+
continue;
|
|
117
|
+
const cursorMark = i === cursor ? cyan(">") : " ";
|
|
118
|
+
const checkbox = selected.has(i) ? "[x]" : "[ ]";
|
|
119
|
+
output.write(`${cursorMark} ${checkbox} ${opt.label}
|
|
120
|
+
`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function clear() {
|
|
124
|
+
output.write(`\x1B[${total + 1}A\x1B[J`);
|
|
125
|
+
}
|
|
126
|
+
emitKeypressEvents(input);
|
|
127
|
+
if (input.setRawMode)
|
|
128
|
+
input.setRawMode(true);
|
|
129
|
+
input.resume();
|
|
130
|
+
render();
|
|
131
|
+
return await new Promise((resolve) => {
|
|
132
|
+
const onKey = (_str, key) => {
|
|
133
|
+
if (key.ctrl && key.name === "c") {
|
|
134
|
+
cleanup();
|
|
135
|
+
resolve(null);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (key.name === "escape" || key.name === "q") {
|
|
139
|
+
cleanup();
|
|
140
|
+
resolve(null);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (key.name === "up" && cursor > 0) {
|
|
144
|
+
cursor--;
|
|
145
|
+
clear();
|
|
146
|
+
render();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (key.name === "down" && cursor < total - 1) {
|
|
150
|
+
cursor++;
|
|
151
|
+
clear();
|
|
152
|
+
render();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (key.name === "space") {
|
|
156
|
+
if (selected.has(cursor))
|
|
157
|
+
selected.delete(cursor);
|
|
158
|
+
else
|
|
159
|
+
selected.add(cursor);
|
|
160
|
+
clear();
|
|
161
|
+
render();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (key.name === "return") {
|
|
165
|
+
cleanup();
|
|
166
|
+
const values = [];
|
|
167
|
+
for (let i = 0;i < total; i++) {
|
|
168
|
+
if (selected.has(i)) {
|
|
169
|
+
const v = params.options[i]?.value;
|
|
170
|
+
if (v !== undefined)
|
|
171
|
+
values.push(v);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
resolve(values);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
function onSigterm() {
|
|
179
|
+
cleanup();
|
|
180
|
+
resolve(null);
|
|
181
|
+
}
|
|
182
|
+
function cleanup() {
|
|
183
|
+
input.removeListener("keypress", onKey);
|
|
184
|
+
process.removeListener("SIGTERM", onSigterm);
|
|
185
|
+
if (input.setRawMode)
|
|
186
|
+
input.setRawMode(false);
|
|
187
|
+
input.pause();
|
|
188
|
+
}
|
|
189
|
+
process.once("SIGTERM", onSigterm);
|
|
190
|
+
input.on("keypress", onKey);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/commands/picker.ts
|
|
195
|
+
async function pickRemoveTargets(args) {
|
|
196
|
+
const lockPath = getLockPath(args.global);
|
|
197
|
+
const { inLock, orphan } = listRemovableTargets({
|
|
198
|
+
isGlobal: args.global,
|
|
199
|
+
cwd: process.cwd(),
|
|
200
|
+
lockPath
|
|
201
|
+
});
|
|
202
|
+
if (inLock.length === 0 && orphan.length === 0) {
|
|
203
|
+
console.log("No skills found in scope.");
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
const options = [
|
|
207
|
+
...inLock.map((name) => ({ value: name, label: name })),
|
|
208
|
+
...orphan.map((name) => ({ value: name, label: `${name} ${red("(orphan)")}` }))
|
|
209
|
+
];
|
|
210
|
+
return await multiSelect({
|
|
211
|
+
title: "skillio — pick skills to remove (Space toggle, Enter confirm)",
|
|
212
|
+
options
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
async function runPicker(args) {
|
|
216
|
+
const choice = await select({
|
|
217
|
+
title: "skillio — pick a command",
|
|
218
|
+
options: [
|
|
219
|
+
{ value: "usage", label: "usage — count of skill invocations" },
|
|
220
|
+
{ value: "cost", label: "cost — per-skill ambient tokens" },
|
|
221
|
+
{ value: "list", label: "list — installed skills per source" },
|
|
222
|
+
{ value: "remove", label: "remove — delete a skill (disk-only; lock with --force-lock)" },
|
|
223
|
+
{ value: "quit", label: "quit" }
|
|
224
|
+
]
|
|
225
|
+
});
|
|
226
|
+
if (choice === null || choice === "quit")
|
|
227
|
+
return 0;
|
|
228
|
+
const cliPath = process.argv[1];
|
|
229
|
+
if (!cliPath) {
|
|
230
|
+
console.error("skillio: cannot resolve CLI path (process.argv[1] missing)");
|
|
231
|
+
return 1;
|
|
232
|
+
}
|
|
233
|
+
let argv;
|
|
234
|
+
if (choice === "remove") {
|
|
235
|
+
const targets = await pickRemoveTargets(args);
|
|
236
|
+
if (targets === null || targets.length === 0)
|
|
237
|
+
return 0;
|
|
238
|
+
argv = ["rm", ...targets];
|
|
239
|
+
} else {
|
|
240
|
+
argv = [choice];
|
|
241
|
+
}
|
|
242
|
+
if (args.global)
|
|
243
|
+
argv.push("-g");
|
|
244
|
+
const r = spawnSync(process.execPath, [cliPath, ...argv], {
|
|
245
|
+
stdio: "inherit",
|
|
246
|
+
env: process.env
|
|
247
|
+
});
|
|
248
|
+
return r.status ?? 0;
|
|
249
|
+
}
|
|
250
|
+
export {
|
|
251
|
+
runPicker
|
|
252
|
+
};
|
package/package.json
CHANGED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
cyan
|
|
3
|
-
} from "./chunk-s3421yr2.js";
|
|
4
|
-
|
|
5
|
-
// src/commands/picker.ts
|
|
6
|
-
import { spawnSync } from "node:child_process";
|
|
7
|
-
|
|
8
|
-
// src/utils/prompt.ts
|
|
9
|
-
import { emitKeypressEvents } from "node:readline";
|
|
10
|
-
async function select(params) {
|
|
11
|
-
const input = params.input ?? process.stdin;
|
|
12
|
-
const output = params.output ?? process.stdout;
|
|
13
|
-
if (!input.isTTY || !output.isTTY)
|
|
14
|
-
return null;
|
|
15
|
-
let cursor = 0;
|
|
16
|
-
const total = params.options.length;
|
|
17
|
-
function render() {
|
|
18
|
-
output.write(`${params.title}
|
|
19
|
-
`);
|
|
20
|
-
for (let i = 0;i < total; i++) {
|
|
21
|
-
const opt = params.options[i];
|
|
22
|
-
if (!opt)
|
|
23
|
-
continue;
|
|
24
|
-
const marker = i === cursor ? cyan(">") : " ";
|
|
25
|
-
output.write(`${marker} ${opt.label}
|
|
26
|
-
`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function clear() {
|
|
30
|
-
output.write(`\x1B[${total + 1}A\x1B[J`);
|
|
31
|
-
}
|
|
32
|
-
emitKeypressEvents(input);
|
|
33
|
-
if (input.setRawMode)
|
|
34
|
-
input.setRawMode(true);
|
|
35
|
-
input.resume();
|
|
36
|
-
render();
|
|
37
|
-
return await new Promise((resolve) => {
|
|
38
|
-
const onKey = (_str, key) => {
|
|
39
|
-
if (key.ctrl && key.name === "c") {
|
|
40
|
-
cleanup();
|
|
41
|
-
resolve(null);
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
if (key.name === "escape" || key.name === "q") {
|
|
45
|
-
cleanup();
|
|
46
|
-
resolve(null);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
if (key.name === "up" && cursor > 0) {
|
|
50
|
-
cursor--;
|
|
51
|
-
clear();
|
|
52
|
-
render();
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
if (key.name === "down" && cursor < total - 1) {
|
|
56
|
-
cursor++;
|
|
57
|
-
clear();
|
|
58
|
-
render();
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (key.name === "return") {
|
|
62
|
-
cleanup();
|
|
63
|
-
const chosen = params.options[cursor]?.value ?? null;
|
|
64
|
-
resolve(chosen);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
function onSigterm() {
|
|
69
|
-
cleanup();
|
|
70
|
-
resolve(null);
|
|
71
|
-
}
|
|
72
|
-
function cleanup() {
|
|
73
|
-
input.removeListener("keypress", onKey);
|
|
74
|
-
process.removeListener("SIGTERM", onSigterm);
|
|
75
|
-
if (input.setRawMode)
|
|
76
|
-
input.setRawMode(false);
|
|
77
|
-
input.pause();
|
|
78
|
-
}
|
|
79
|
-
process.once("SIGTERM", onSigterm);
|
|
80
|
-
input.on("keypress", onKey);
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// src/commands/picker.ts
|
|
85
|
-
async function runPicker(args) {
|
|
86
|
-
const choice = await select({
|
|
87
|
-
title: "skillio — pick a command",
|
|
88
|
-
options: [
|
|
89
|
-
{ value: "usage", label: "usage — count of skill invocations" },
|
|
90
|
-
{ value: "cost", label: "cost — per-skill ambient tokens" },
|
|
91
|
-
{ value: "list", label: "list — installed skills per source" },
|
|
92
|
-
{ value: "quit", label: "quit" }
|
|
93
|
-
]
|
|
94
|
-
});
|
|
95
|
-
if (choice === null || choice === "quit")
|
|
96
|
-
return 0;
|
|
97
|
-
const cliPath = process.argv[1];
|
|
98
|
-
if (!cliPath) {
|
|
99
|
-
console.error("skillio: cannot resolve CLI path (process.argv[1] missing)");
|
|
100
|
-
return 1;
|
|
101
|
-
}
|
|
102
|
-
const argv = [choice];
|
|
103
|
-
if (args.global)
|
|
104
|
-
argv.push("-g");
|
|
105
|
-
const r = spawnSync(process.execPath, [cliPath, ...argv], {
|
|
106
|
-
stdio: "inherit",
|
|
107
|
-
env: process.env
|
|
108
|
-
});
|
|
109
|
-
return r.status ?? 0;
|
|
110
|
-
}
|
|
111
|
-
export {
|
|
112
|
-
runPicker
|
|
113
|
-
};
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
-
|
|
4
|
-
// src/utils/ansi.ts
|
|
5
|
-
var enabled = false;
|
|
6
|
-
function setColorEnabled(value) {
|
|
7
|
-
enabled = value;
|
|
8
|
-
}
|
|
9
|
-
function detectColorSupport() {
|
|
10
|
-
if (process.env.NO_COLOR)
|
|
11
|
-
return false;
|
|
12
|
-
return Boolean(process.stdout.isTTY);
|
|
13
|
-
}
|
|
14
|
-
function green(s) {
|
|
15
|
-
return enabled ? `\x1B[32m${s}\x1B[0m` : s;
|
|
16
|
-
}
|
|
17
|
-
function yellow(s) {
|
|
18
|
-
return enabled ? `\x1B[33m${s}\x1B[0m` : s;
|
|
19
|
-
}
|
|
20
|
-
function red(s) {
|
|
21
|
-
return enabled ? `\x1B[31m${s}\x1B[0m` : s;
|
|
22
|
-
}
|
|
23
|
-
function cyan(s) {
|
|
24
|
-
return enabled ? `\x1B[36m${s}\x1B[0m` : s;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export { __require, setColorEnabled, detectColorSupport, green, yellow, red, cyan };
|