skillio 0.1.10 → 0.1.11
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
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,90 @@ 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) {
|
|
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: "skills-lock.json", 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
|
-
const
|
|
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 rows = bySource(records, roots);
|
|
665
|
+
const claudeSet = new Set(rows.claude.names.map((n) => n.name));
|
|
666
|
+
const agentsSet = new Set(rows.agents.names.map((n) => n.name));
|
|
667
|
+
const orphans = rows.lock.names.filter((n) => !claudeSet.has(n.name) && !agentsSet.has(n.name));
|
|
668
|
+
const sourceRows = [
|
|
669
|
+
{
|
|
670
|
+
row: rows.agents,
|
|
671
|
+
render: () => rows.agents.names.map(paintDisk).join(" ")
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
row: rows.claude,
|
|
675
|
+
render: () => rows.claude.names.map(paintDisk).join(" ")
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
row: rows.lock,
|
|
679
|
+
render: () => orphans.length === 0 ? green("All skills onboard!") : orphans.map((n) => red(n.name)).join(" ")
|
|
680
|
+
}
|
|
681
|
+
];
|
|
682
|
+
const labelWidth = Math.max(...sourceRows.map((r) => r.row.label.length));
|
|
683
|
+
const countCells = sourceRows.map((r) => `${r.row.totalCount} skill${r.row.totalCount === 1 ? "" : "s"}`);
|
|
766
684
|
const countWidth = Math.max(...countCells.map((c) => c.length));
|
|
767
685
|
for (let i = 0;i < sourceRows.length; i++) {
|
|
768
|
-
const
|
|
769
|
-
if (!
|
|
686
|
+
const entry = sourceRows[i];
|
|
687
|
+
if (!entry)
|
|
770
688
|
continue;
|
|
771
689
|
const countCell = countCells[i] ?? "";
|
|
772
|
-
const namesText =
|
|
773
|
-
const line = `${row.label.padEnd(labelWidth)} : ${countCell.padEnd(countWidth)}${namesText ? ` : ${namesText}` : ""}`;
|
|
690
|
+
const namesText = entry.render();
|
|
691
|
+
const line = `${entry.row.label.padEnd(labelWidth)} : ${countCell.padEnd(countWidth)}${namesText ? ` : ${namesText}` : ""}`;
|
|
774
692
|
console.log(line.trimEnd());
|
|
775
693
|
}
|
|
694
|
+
const claudeNames = rows.claude.names.map((n) => n.name);
|
|
695
|
+
const agentsNames = rows.agents.names.map((n) => n.name);
|
|
696
|
+
const lockNames = rows.lock.names.map((n) => n.name);
|
|
697
|
+
const lockOnly = lockNames.filter((n) => !claudeNames.includes(n) && !agentsNames.includes(n));
|
|
698
|
+
const claudeNotInLock = claudeNames.filter((n) => !lockNames.includes(n));
|
|
699
|
+
const agentsNotInLock = agentsNames.filter((n) => !lockNames.includes(n));
|
|
776
700
|
const diffs = [];
|
|
777
701
|
if (lockOnly.length) {
|
|
778
702
|
diffs.push(`skills-lock.json has ${lockOnly.length} skill${lockOnly.length === 1 ? "" : "s"} missing on disk: ${lockOnly.map(cyan).join(", ")}`);
|
|
@@ -792,46 +716,92 @@ var listCommand = defineCommand({
|
|
|
792
716
|
});
|
|
793
717
|
|
|
794
718
|
// src/commands/remove.ts
|
|
795
|
-
import { existsSync as
|
|
796
|
-
import { homedir as
|
|
797
|
-
import { dirname as
|
|
719
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
720
|
+
import { homedir as homedir2 } from "node:os";
|
|
721
|
+
import { dirname as dirname2, join as join3, resolve as resolve3 } from "node:path";
|
|
798
722
|
|
|
799
723
|
// src/utils/confirm.ts
|
|
800
|
-
import {
|
|
724
|
+
import { emitKeypressEvents } from "node:readline";
|
|
801
725
|
async function confirm(question, opts = {}) {
|
|
802
726
|
const input = opts.input ?? process.stdin;
|
|
803
727
|
const output = opts.output ?? process.stdout;
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
output
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
728
|
+
if (!input.isTTY || !output.isTTY) {
|
|
729
|
+
const { createInterface } = await import("node:readline/promises");
|
|
730
|
+
const rl = createInterface({ input, output });
|
|
731
|
+
try {
|
|
732
|
+
const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
|
|
733
|
+
return answer === "y" || answer === "yes";
|
|
734
|
+
} finally {
|
|
735
|
+
rl.close();
|
|
736
|
+
}
|
|
813
737
|
}
|
|
738
|
+
output.write(`${question} [y/N] `);
|
|
739
|
+
emitKeypressEvents(input);
|
|
740
|
+
if (input.setRawMode)
|
|
741
|
+
input.setRawMode(true);
|
|
742
|
+
input.resume();
|
|
743
|
+
return new Promise((resolve2) => {
|
|
744
|
+
const onKey = (str, key) => {
|
|
745
|
+
const lower = (str ?? "").toLowerCase();
|
|
746
|
+
if (key.ctrl && key.name === "c") {
|
|
747
|
+
cleanup();
|
|
748
|
+
output.write(`
|
|
749
|
+
`);
|
|
750
|
+
resolve2(false);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (lower === "y") {
|
|
754
|
+
cleanup();
|
|
755
|
+
output.write(`y
|
|
756
|
+
`);
|
|
757
|
+
resolve2(true);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (lower === "n" || key.name === "return" || key.name === "escape" || lower === "q") {
|
|
761
|
+
cleanup();
|
|
762
|
+
output.write(`${lower || "n"}
|
|
763
|
+
`);
|
|
764
|
+
resolve2(false);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
function onSigterm() {
|
|
769
|
+
cleanup();
|
|
770
|
+
output.write(`
|
|
771
|
+
`);
|
|
772
|
+
resolve2(false);
|
|
773
|
+
}
|
|
774
|
+
function cleanup() {
|
|
775
|
+
input.removeListener("keypress", onKey);
|
|
776
|
+
process.removeListener("SIGTERM", onSigterm);
|
|
777
|
+
if (input.setRawMode)
|
|
778
|
+
input.setRawMode(false);
|
|
779
|
+
input.pause();
|
|
780
|
+
}
|
|
781
|
+
process.once("SIGTERM", onSigterm);
|
|
782
|
+
input.on("keypress", onKey);
|
|
783
|
+
});
|
|
814
784
|
}
|
|
815
785
|
|
|
816
786
|
// src/utils/fs-rm.ts
|
|
817
|
-
import { existsSync as
|
|
818
|
-
import { join as
|
|
787
|
+
import { existsSync as existsSync2, lstatSync as lstatSync2, readdirSync, rmSync } from "node:fs";
|
|
788
|
+
import { join as join2, resolve as resolve2 } from "node:path";
|
|
819
789
|
function isInside(target, root) {
|
|
820
|
-
const t =
|
|
821
|
-
const r =
|
|
790
|
+
const t = resolve2(target);
|
|
791
|
+
const r = resolve2(root);
|
|
822
792
|
return t === r || t.startsWith(`${r}/`);
|
|
823
793
|
}
|
|
824
794
|
function countFiles(path) {
|
|
825
|
-
if (!
|
|
795
|
+
if (!existsSync2(path))
|
|
826
796
|
return 0;
|
|
827
|
-
const stat =
|
|
797
|
+
const stat = lstatSync2(path);
|
|
828
798
|
if (stat.isFile())
|
|
829
799
|
return 1;
|
|
830
800
|
if (!stat.isDirectory())
|
|
831
801
|
return 0;
|
|
832
802
|
let n = 0;
|
|
833
|
-
for (const entry of
|
|
834
|
-
n += countFiles(
|
|
803
|
+
for (const entry of readdirSync(path)) {
|
|
804
|
+
n += countFiles(join2(path, entry));
|
|
835
805
|
}
|
|
836
806
|
return n;
|
|
837
807
|
}
|
|
@@ -840,60 +810,51 @@ function rmSkillDir(path, opts) {
|
|
|
840
810
|
if (!safe) {
|
|
841
811
|
throw new Error(`Refusing to delete: "${path}" is outside allowed roots`);
|
|
842
812
|
}
|
|
843
|
-
if (!
|
|
813
|
+
if (!existsSync2(path))
|
|
844
814
|
return { removed: false, fileCount: 0 };
|
|
845
815
|
const fileCount = countFiles(path);
|
|
846
816
|
rmSync(path, { recursive: true, force: true });
|
|
847
817
|
return { removed: true, fileCount };
|
|
848
818
|
}
|
|
849
819
|
|
|
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
820
|
// src/commands/remove.ts
|
|
864
821
|
var q = (name) => `"${cyan(name)}"`;
|
|
865
822
|
function buildTarget(name, isGlobal, lockPath) {
|
|
866
823
|
const lock = readLock(lockPath);
|
|
867
824
|
const inLock = Object.hasOwn(lock.skills, name);
|
|
868
|
-
const baseClaude = isGlobal ?
|
|
869
|
-
const baseAgents = isGlobal ?
|
|
870
|
-
const claudeDir =
|
|
871
|
-
const agentsDir =
|
|
825
|
+
const baseClaude = isGlobal ? join3(homedir2(), ".claude", "skills") : join3(dirname2(resolve3(lockPath)), ".claude", "skills");
|
|
826
|
+
const baseAgents = isGlobal ? join3(homedir2(), ".agents", "skills") : join3(dirname2(resolve3(lockPath)), ".agents", "skills");
|
|
827
|
+
const claudeDir = existsSync3(join3(baseClaude, name)) ? join3(baseClaude, name) : undefined;
|
|
828
|
+
const agentsDir = existsSync3(join3(baseAgents, name)) ? join3(baseAgents, name) : undefined;
|
|
872
829
|
return { name, inLock, claudeDir, agentsDir };
|
|
873
830
|
}
|
|
831
|
+
function collectAllTargets(isGlobal, lockPath) {
|
|
832
|
+
const map = discoverSkills({ isGlobal, cwd: process.cwd(), lockPath });
|
|
833
|
+
return [...map.keys()].sort().map((name) => buildTarget(name, isGlobal, lockPath));
|
|
834
|
+
}
|
|
874
835
|
function fileCount(dir) {
|
|
875
|
-
const { readdirSync:
|
|
836
|
+
const { readdirSync: readdirSync2, statSync } = __require("node:fs");
|
|
876
837
|
let n = 0;
|
|
877
838
|
const stack = [dir];
|
|
878
839
|
while (stack.length) {
|
|
879
840
|
const cur = stack.pop();
|
|
880
|
-
const stat =
|
|
841
|
+
const stat = statSync(cur);
|
|
881
842
|
if (stat.isFile())
|
|
882
843
|
n++;
|
|
883
844
|
else if (stat.isDirectory())
|
|
884
|
-
for (const e of
|
|
885
|
-
stack.push(
|
|
845
|
+
for (const e of readdirSync2(cur))
|
|
846
|
+
stack.push(join3(cur, e));
|
|
886
847
|
}
|
|
887
848
|
return n;
|
|
888
849
|
}
|
|
889
|
-
function printPlan(plan,
|
|
850
|
+
function printPlan(plan, modifyLock) {
|
|
890
851
|
const { target } = plan;
|
|
891
852
|
console.log(`Will remove ${q(target.name)}:`);
|
|
892
853
|
if (target.inLock) {
|
|
893
|
-
if (
|
|
894
|
-
console.log(" - skills-lock.json (skipped: git-tracked; use --force-lock)");
|
|
895
|
-
else
|
|
854
|
+
if (modifyLock)
|
|
896
855
|
console.log(" - skills-lock.json");
|
|
856
|
+
else
|
|
857
|
+
console.log(" - skills-lock.json (kept; use --force-lock to remove lock entry)");
|
|
897
858
|
} else {
|
|
898
859
|
console.log(" - skills-lock.json (not in lock)");
|
|
899
860
|
}
|
|
@@ -907,27 +868,38 @@ function printPlan(plan, lockTracked) {
|
|
|
907
868
|
console.log(" - .agents/skills/ (not found)");
|
|
908
869
|
}
|
|
909
870
|
var removeCommand = defineCommand({
|
|
910
|
-
meta: {
|
|
871
|
+
meta: {
|
|
872
|
+
description: "Remove one or more skills from on-disk dirs (lock preserved unless --force-lock)"
|
|
873
|
+
},
|
|
911
874
|
args: {
|
|
912
875
|
global: { type: "boolean", alias: "g", default: false, description: "Use global scope" },
|
|
913
876
|
"dry-run": { type: "boolean", default: false, description: "Print plan, do not delete" },
|
|
914
877
|
yes: { type: "boolean", alias: "y", default: false, description: "Skip confirmation prompt" },
|
|
878
|
+
all: { type: "boolean", default: false, description: "Remove every skill in scope" },
|
|
915
879
|
"force-lock": {
|
|
916
880
|
type: "boolean",
|
|
917
881
|
default: false,
|
|
918
|
-
description: "
|
|
882
|
+
description: "Also remove entry from skills-lock.json (default is to keep lock untouched)"
|
|
919
883
|
}
|
|
920
884
|
},
|
|
921
885
|
async run({ args }) {
|
|
922
|
-
const { global: isGlobal, "dry-run": dryRun, yes, "force-lock":
|
|
886
|
+
const { global: isGlobal, "dry-run": dryRun, yes, all, "force-lock": modifyLock } = args;
|
|
923
887
|
const subcmdIdx = process.argv.findIndex((a) => a === "remove" || a === "rm");
|
|
924
888
|
const names = process.argv.slice(subcmdIdx + 1).filter((a) => !a.startsWith("-"));
|
|
925
|
-
if (names.length
|
|
889
|
+
if (all && names.length > 0) {
|
|
890
|
+
console.error("--all is mutually exclusive with positional skill names");
|
|
891
|
+
process.exit(1);
|
|
892
|
+
}
|
|
893
|
+
if (!all && names.length === 0) {
|
|
926
894
|
console.error("No skill names provided");
|
|
927
895
|
process.exit(1);
|
|
928
896
|
}
|
|
929
897
|
const lockPath = getLockPath(isGlobal);
|
|
930
|
-
const targets = names.map((n) => buildTarget(n, isGlobal, lockPath));
|
|
898
|
+
const targets = all ? collectAllTargets(isGlobal, lockPath) : names.map((n) => buildTarget(n, isGlobal, lockPath));
|
|
899
|
+
if (all && targets.length === 0) {
|
|
900
|
+
console.log("No skills to remove in scope.");
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
931
903
|
const orphan = targets.filter((t) => !t.inLock && !t.claudeDir && !t.agentsDir);
|
|
932
904
|
if (orphan.length) {
|
|
933
905
|
for (const o of orphan)
|
|
@@ -939,32 +911,29 @@ var removeCommand = defineCommand({
|
|
|
939
911
|
claudeFileCount: t.claudeDir ? fileCount(t.claudeDir) : undefined,
|
|
940
912
|
agentsFileCount: t.agentsDir ? fileCount(t.agentsDir) : undefined
|
|
941
913
|
}));
|
|
942
|
-
const lockTracked = !forceLock && isTrackedByGit(lockPath);
|
|
943
914
|
for (const p of plans) {
|
|
944
|
-
printPlan(p,
|
|
915
|
+
printPlan(p, modifyLock);
|
|
945
916
|
console.log("");
|
|
946
917
|
}
|
|
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
918
|
if (dryRun)
|
|
951
919
|
return;
|
|
952
920
|
if (!yes) {
|
|
953
|
-
const
|
|
921
|
+
const promptText = all ? `Remove ALL ${plans.length} skills?` : "Proceed?";
|
|
922
|
+
const ok = await confirm(promptText);
|
|
954
923
|
if (!ok) {
|
|
955
924
|
console.log("Aborted");
|
|
956
925
|
process.exit(1);
|
|
957
926
|
}
|
|
958
927
|
}
|
|
959
|
-
const allowedRoots = [isGlobal ?
|
|
928
|
+
const allowedRoots = [isGlobal ? homedir2() : dirname2(resolve3(lockPath)), homedir2()];
|
|
960
929
|
for (const { target } of plans) {
|
|
961
930
|
if (target.inLock) {
|
|
962
|
-
if (
|
|
963
|
-
console.log(`Skipped skills-lock.json (git-tracked) for ${q(target.name)}`);
|
|
964
|
-
} else {
|
|
931
|
+
if (modifyLock) {
|
|
965
932
|
const r = removeSkillFromLock(lockPath, target.name);
|
|
966
933
|
if (r.removed)
|
|
967
934
|
console.log(`Removed ${q(target.name)} from skills-lock.json`);
|
|
935
|
+
} else {
|
|
936
|
+
console.log(`Kept ${q(target.name)} in skills-lock.json (no --force-lock)`);
|
|
968
937
|
}
|
|
969
938
|
} else {
|
|
970
939
|
console.log(`Skipped skills-lock.json (not in lock)`);
|
|
@@ -986,11 +955,11 @@ var removeCommand = defineCommand({
|
|
|
986
955
|
});
|
|
987
956
|
|
|
988
957
|
// src/commands/usage.ts
|
|
989
|
-
import { existsSync as
|
|
990
|
-
import { join as
|
|
958
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
959
|
+
import { join as join7 } from "node:path";
|
|
991
960
|
|
|
992
961
|
// src/readers/claude.ts
|
|
993
|
-
import { readFileSync as
|
|
962
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
994
963
|
|
|
995
964
|
// src/utils/codex-cmd.ts
|
|
996
965
|
var READ_LIKE = new Set(["cat", "sed", "head", "tail", "bat", "batcat", "less", "more"]);
|
|
@@ -1214,26 +1183,26 @@ function isUserTurnEntry(entry) {
|
|
|
1214
1183
|
}
|
|
1215
1184
|
|
|
1216
1185
|
// src/utils/expand-home.ts
|
|
1217
|
-
import { homedir as
|
|
1218
|
-
import { join as
|
|
1186
|
+
import { homedir as homedir3 } from "node:os";
|
|
1187
|
+
import { join as join4, resolve as resolve4 } from "node:path";
|
|
1219
1188
|
function expandHome(p) {
|
|
1220
1189
|
if (p === "~")
|
|
1221
|
-
return
|
|
1190
|
+
return homedir3();
|
|
1222
1191
|
if (p.startsWith("~/"))
|
|
1223
|
-
return
|
|
1224
|
-
return
|
|
1192
|
+
return join4(homedir3(), p.slice(2));
|
|
1193
|
+
return resolve4(p);
|
|
1225
1194
|
}
|
|
1226
1195
|
|
|
1227
1196
|
// src/utils/jsonl.ts
|
|
1228
|
-
import { readdirSync as
|
|
1229
|
-
import { join as
|
|
1197
|
+
import { readdirSync as readdirSync2, readFileSync, statSync } from "node:fs";
|
|
1198
|
+
import { join as join5 } from "node:path";
|
|
1230
1199
|
function* findJsonlFiles(dir, since) {
|
|
1231
|
-
for (const item of
|
|
1232
|
-
const path =
|
|
1200
|
+
for (const item of readdirSync2(dir, { withFileTypes: true })) {
|
|
1201
|
+
const path = join5(dir, item.name);
|
|
1233
1202
|
if (item.isDirectory()) {
|
|
1234
1203
|
yield* findJsonlFiles(path, since);
|
|
1235
1204
|
} else if (item.isFile() && item.name.endsWith(".jsonl")) {
|
|
1236
|
-
if (!since ||
|
|
1205
|
+
if (!since || statSync(path).mtime >= since)
|
|
1237
1206
|
yield path;
|
|
1238
1207
|
}
|
|
1239
1208
|
}
|
|
@@ -1263,7 +1232,7 @@ function readClaudeUsage(options) {
|
|
|
1263
1232
|
let prevSkill = null;
|
|
1264
1233
|
const sessionAttr = new Map;
|
|
1265
1234
|
const sessionAct = new Map;
|
|
1266
|
-
for (const line of
|
|
1235
|
+
for (const line of readFileSync2(file, "utf8").split(`
|
|
1267
1236
|
`)) {
|
|
1268
1237
|
if (!line.trim())
|
|
1269
1238
|
continue;
|
|
@@ -1316,14 +1285,14 @@ function readClaudeUsage(options) {
|
|
|
1316
1285
|
}
|
|
1317
1286
|
|
|
1318
1287
|
// src/readers/codex.ts
|
|
1319
|
-
import { existsSync as
|
|
1288
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
|
|
1320
1289
|
|
|
1321
1290
|
// src/utils/scope.ts
|
|
1322
|
-
import { existsSync as
|
|
1323
|
-
import { homedir as
|
|
1324
|
-
import { dirname as
|
|
1291
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
1292
|
+
import { homedir as homedir4 } from "node:os";
|
|
1293
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
1325
1294
|
function detectScope(opts) {
|
|
1326
|
-
const home = opts.home ??
|
|
1295
|
+
const home = opts.home ?? homedir4();
|
|
1327
1296
|
if (opts.global || opts.rootOverride)
|
|
1328
1297
|
return { global: true };
|
|
1329
1298
|
if (norm(opts.cwd) === norm(home))
|
|
@@ -1341,9 +1310,9 @@ function encodeClaudeProjectDir(absPath) {
|
|
|
1341
1310
|
function findGitRoot(start) {
|
|
1342
1311
|
let dir = start;
|
|
1343
1312
|
while (true) {
|
|
1344
|
-
if (
|
|
1313
|
+
if (existsSync4(join6(dir, ".git")))
|
|
1345
1314
|
return dir;
|
|
1346
|
-
const parent =
|
|
1315
|
+
const parent = dirname3(dir);
|
|
1347
1316
|
if (parent === dir)
|
|
1348
1317
|
return;
|
|
1349
1318
|
dir = parent;
|
|
@@ -1357,7 +1326,7 @@ function norm(p) {
|
|
|
1357
1326
|
function readSessionCwd(file) {
|
|
1358
1327
|
let head;
|
|
1359
1328
|
try {
|
|
1360
|
-
head =
|
|
1329
|
+
head = readFileSync3(file, "utf8");
|
|
1361
1330
|
} catch {
|
|
1362
1331
|
return;
|
|
1363
1332
|
}
|
|
@@ -1390,7 +1359,7 @@ function readCodexActivations(options) {
|
|
|
1390
1359
|
continue;
|
|
1391
1360
|
}
|
|
1392
1361
|
filesRead++;
|
|
1393
|
-
for (const line of
|
|
1362
|
+
for (const line of readFileSync3(file, "utf8").split(`
|
|
1394
1363
|
`)) {
|
|
1395
1364
|
if (!line.trim())
|
|
1396
1365
|
continue;
|
|
@@ -1414,9 +1383,9 @@ function readCodexMentions(options) {
|
|
|
1414
1383
|
const historyPath = expandHome(options.history ?? "~/.codex/history.jsonl");
|
|
1415
1384
|
const counts = new Map;
|
|
1416
1385
|
let linesRead = 0;
|
|
1417
|
-
if (!
|
|
1386
|
+
if (!existsSync5(historyPath))
|
|
1418
1387
|
return { counts, filesRead: 0, linesRead: 0 };
|
|
1419
|
-
for (const line of
|
|
1388
|
+
for (const line of readFileSync3(historyPath, "utf8").split(`
|
|
1420
1389
|
`)) {
|
|
1421
1390
|
if (!line.trim())
|
|
1422
1391
|
continue;
|
|
@@ -1442,20 +1411,18 @@ var MINUTE_MS = 60 * SECOND_MS;
|
|
|
1442
1411
|
var HOUR_MS = 60 * MINUTE_MS;
|
|
1443
1412
|
var DAY_MS = 24 * HOUR_MS;
|
|
1444
1413
|
var UNITS_MS = {
|
|
1445
|
-
|
|
1446
|
-
|
|
1414
|
+
s: SECOND_MS,
|
|
1415
|
+
m: MINUTE_MS,
|
|
1447
1416
|
h: HOUR_MS,
|
|
1448
1417
|
d: DAY_MS,
|
|
1449
|
-
w: 7 * DAY_MS
|
|
1450
|
-
m: 30 * DAY_MS,
|
|
1451
|
-
y: 365 * DAY_MS
|
|
1418
|
+
w: 7 * DAY_MS
|
|
1452
1419
|
};
|
|
1453
1420
|
function parsePeriod(period) {
|
|
1454
1421
|
if (period === "all")
|
|
1455
1422
|
return Number.POSITIVE_INFINITY;
|
|
1456
|
-
const match = period.match(/^(\d+)(
|
|
1423
|
+
const match = period.match(/^(\d+)([smhdw])$/);
|
|
1457
1424
|
if (!match) {
|
|
1458
|
-
throw new Error(`Invalid period: "${period}". Use values like
|
|
1425
|
+
throw new Error(`Invalid period: "${period}". Use values like 60s, 30m, 24h, 30d, 2w, all.`);
|
|
1459
1426
|
}
|
|
1460
1427
|
const unit = UNITS_MS[match[2] ?? ""] ?? 0;
|
|
1461
1428
|
return Number(match[1]) * unit;
|
|
@@ -1490,7 +1457,7 @@ var usageArgs = {
|
|
|
1490
1457
|
type: "string",
|
|
1491
1458
|
alias: "p",
|
|
1492
1459
|
default: "all",
|
|
1493
|
-
description: "
|
|
1460
|
+
description: "60s, 30m, 24h, 30d, 2w, all"
|
|
1494
1461
|
},
|
|
1495
1462
|
since: { type: "string", description: "yyyy-mm-dd, overrides --period" },
|
|
1496
1463
|
mode: {
|
|
@@ -1522,8 +1489,8 @@ async function runUsage(args) {
|
|
|
1522
1489
|
cwd: process.cwd()
|
|
1523
1490
|
});
|
|
1524
1491
|
const claudeProjectsRoot = expandHome("~/.claude/projects");
|
|
1525
|
-
const claudeRoot = args.root ?? (scope.projectRoot ?
|
|
1526
|
-
const claudeRootMissing = !args.root && !!scope.projectRoot && !
|
|
1492
|
+
const claudeRoot = args.root ?? (scope.projectRoot ? join7(claudeProjectsRoot, encodeClaudeProjectDir(scope.projectRoot)) : claudeProjectsRoot);
|
|
1493
|
+
const claudeRootMissing = !args.root && !!scope.projectRoot && !existsSync6(claudeRoot);
|
|
1527
1494
|
const lockPath = getLockPath(args.global);
|
|
1528
1495
|
const skillUniverse = discoverSkills({
|
|
1529
1496
|
isGlobal: args.global,
|
|
@@ -1597,7 +1564,6 @@ async function runUsage(args) {
|
|
|
1597
1564
|
let grandActivations = 0;
|
|
1598
1565
|
for (const { agent, rows } of results) {
|
|
1599
1566
|
const activations = rows.reduce((acc, r) => acc + r.count, 0);
|
|
1600
|
-
console.log("");
|
|
1601
1567
|
console.log(`${agent} ${rows.length} skill${rows.length === 1 ? "" : "s"} ${activations} time${activations === 1 ? "" : "s"} by ${periodLabel}`);
|
|
1602
1568
|
if (rows.length === 0)
|
|
1603
1569
|
continue;
|
|
@@ -1625,15 +1591,15 @@ var usageCommand = defineCommand({
|
|
|
1625
1591
|
});
|
|
1626
1592
|
|
|
1627
1593
|
// src/utils/update-check.ts
|
|
1628
|
-
import { mkdirSync
|
|
1594
|
+
import { mkdirSync, readFileSync as readFileSync4, writeFileSync } from "node:fs";
|
|
1629
1595
|
import { get } from "node:https";
|
|
1630
|
-
import { homedir as
|
|
1631
|
-
import { dirname as
|
|
1596
|
+
import { homedir as homedir5 } from "node:os";
|
|
1597
|
+
import { dirname as dirname4, join as join8 } from "node:path";
|
|
1632
1598
|
var PKG = "skillio";
|
|
1633
1599
|
var TTL_MS = 24 * 60 * 60 * 1000;
|
|
1634
1600
|
var FETCH_TIMEOUT_MS = 1500;
|
|
1635
1601
|
function getCachePath() {
|
|
1636
|
-
return
|
|
1602
|
+
return join8(homedir5(), ".cache", "skillio", "version.json");
|
|
1637
1603
|
}
|
|
1638
1604
|
function compareVersions(a, b) {
|
|
1639
1605
|
const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
@@ -1648,23 +1614,23 @@ function compareVersions(a, b) {
|
|
|
1648
1614
|
}
|
|
1649
1615
|
function readCache(path = getCachePath()) {
|
|
1650
1616
|
try {
|
|
1651
|
-
return JSON.parse(
|
|
1617
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
1652
1618
|
} catch {
|
|
1653
1619
|
return;
|
|
1654
1620
|
}
|
|
1655
1621
|
}
|
|
1656
1622
|
function writeCache(cache, path = getCachePath()) {
|
|
1657
1623
|
try {
|
|
1658
|
-
|
|
1659
|
-
|
|
1624
|
+
mkdirSync(dirname4(path), { recursive: true });
|
|
1625
|
+
writeFileSync(path, JSON.stringify(cache));
|
|
1660
1626
|
} catch {}
|
|
1661
1627
|
}
|
|
1662
1628
|
function fetchLatest() {
|
|
1663
|
-
return new Promise((
|
|
1629
|
+
return new Promise((resolve5) => {
|
|
1664
1630
|
const req = get(`https://registry.npmjs.org/${PKG}/latest`, { timeout: FETCH_TIMEOUT_MS }, (res) => {
|
|
1665
1631
|
if (res.statusCode !== 200) {
|
|
1666
1632
|
res.resume();
|
|
1667
|
-
|
|
1633
|
+
resolve5(undefined);
|
|
1668
1634
|
return;
|
|
1669
1635
|
}
|
|
1670
1636
|
let body = "";
|
|
@@ -1675,16 +1641,16 @@ function fetchLatest() {
|
|
|
1675
1641
|
res.on("end", () => {
|
|
1676
1642
|
try {
|
|
1677
1643
|
const data = JSON.parse(body);
|
|
1678
|
-
|
|
1644
|
+
resolve5(typeof data.version === "string" ? data.version : undefined);
|
|
1679
1645
|
} catch {
|
|
1680
|
-
|
|
1646
|
+
resolve5(undefined);
|
|
1681
1647
|
}
|
|
1682
1648
|
});
|
|
1683
1649
|
});
|
|
1684
|
-
req.on("error", () =>
|
|
1650
|
+
req.on("error", () => resolve5(undefined));
|
|
1685
1651
|
req.on("timeout", () => {
|
|
1686
1652
|
req.destroy();
|
|
1687
|
-
|
|
1653
|
+
resolve5(undefined);
|
|
1688
1654
|
});
|
|
1689
1655
|
});
|
|
1690
1656
|
}
|
|
@@ -1780,7 +1746,7 @@ function printRootHelp() {
|
|
|
1780
1746
|
" -h, --help Show this help and exit",
|
|
1781
1747
|
" -v, --version Show version and exit",
|
|
1782
1748
|
" -g, --global Use global scope (default: false)",
|
|
1783
|
-
" -p, --period Period for `usage`:
|
|
1749
|
+
" -p, --period Period for `usage`: 60s, 30m, 24h, 30d, 2w, all (default: all)",
|
|
1784
1750
|
" -a, --agent Agent for `usage`: claude-code, codex (default: both)",
|
|
1785
1751
|
"",
|
|
1786
1752
|
"COMMANDS",
|
|
@@ -1845,7 +1811,7 @@ var main = defineCommand({
|
|
|
1845
1811
|
return;
|
|
1846
1812
|
const interactive = process.stdout.isTTY && process.stdin.isTTY;
|
|
1847
1813
|
if (interactive) {
|
|
1848
|
-
const { runPicker } = await import("./shared/chunk-
|
|
1814
|
+
const { runPicker } = await import("./shared/chunk-j9jc4e4c.js");
|
|
1849
1815
|
const status = await runPicker({
|
|
1850
1816
|
global: args.global ?? false
|
|
1851
1817
|
});
|
|
@@ -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 };
|
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import {
|
|
2
|
-
cyan
|
|
3
|
-
|
|
2
|
+
cyan,
|
|
3
|
+
discoverSkills,
|
|
4
|
+
getLockPath,
|
|
5
|
+
red
|
|
6
|
+
} from "./chunk-0qvp6v8g.js";
|
|
4
7
|
|
|
5
8
|
// src/commands/picker.ts
|
|
6
9
|
import { spawnSync } from "node:child_process";
|
|
7
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
|
+
|
|
8
27
|
// src/utils/prompt.ts
|
|
9
28
|
import { emitKeypressEvents } from "node:readline";
|
|
10
29
|
async function select(params) {
|
|
@@ -82,6 +101,28 @@ async function select(params) {
|
|
|
82
101
|
}
|
|
83
102
|
|
|
84
103
|
// src/commands/picker.ts
|
|
104
|
+
var CANCEL = "__cancel__";
|
|
105
|
+
async function pickRemoveTarget(args) {
|
|
106
|
+
const lockPath = getLockPath(args.global);
|
|
107
|
+
const { inLock, orphan } = listRemovableTargets({
|
|
108
|
+
isGlobal: args.global,
|
|
109
|
+
cwd: process.cwd(),
|
|
110
|
+
lockPath
|
|
111
|
+
});
|
|
112
|
+
if (inLock.length === 0 && orphan.length === 0) {
|
|
113
|
+
console.log("No skills found in scope.");
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const options = [
|
|
117
|
+
...inLock.map((name) => ({ value: name, label: name })),
|
|
118
|
+
...orphan.map((name) => ({ value: name, label: `${name} ${red("(orphan)")}` })),
|
|
119
|
+
{ value: CANCEL, label: "cancel" }
|
|
120
|
+
];
|
|
121
|
+
const choice = await select({ title: "skillio — pick a skill to remove", options });
|
|
122
|
+
if (choice === null || choice === CANCEL)
|
|
123
|
+
return null;
|
|
124
|
+
return choice;
|
|
125
|
+
}
|
|
85
126
|
async function runPicker(args) {
|
|
86
127
|
const choice = await select({
|
|
87
128
|
title: "skillio — pick a command",
|
|
@@ -89,6 +130,7 @@ async function runPicker(args) {
|
|
|
89
130
|
{ value: "usage", label: "usage — count of skill invocations" },
|
|
90
131
|
{ value: "cost", label: "cost — per-skill ambient tokens" },
|
|
91
132
|
{ value: "list", label: "list — installed skills per source" },
|
|
133
|
+
{ value: "remove", label: "remove — delete a skill (disk-only; lock with --force-lock)" },
|
|
92
134
|
{ value: "quit", label: "quit" }
|
|
93
135
|
]
|
|
94
136
|
});
|
|
@@ -99,7 +141,15 @@ async function runPicker(args) {
|
|
|
99
141
|
console.error("skillio: cannot resolve CLI path (process.argv[1] missing)");
|
|
100
142
|
return 1;
|
|
101
143
|
}
|
|
102
|
-
|
|
144
|
+
let argv;
|
|
145
|
+
if (choice === "remove") {
|
|
146
|
+
const target = await pickRemoveTarget(args);
|
|
147
|
+
if (target === null)
|
|
148
|
+
return 0;
|
|
149
|
+
argv = ["rm", target];
|
|
150
|
+
} else {
|
|
151
|
+
argv = [choice];
|
|
152
|
+
}
|
|
103
153
|
if (args.global)
|
|
104
154
|
argv.push("-g");
|
|
105
155
|
const r = spawnSync(process.execPath, [cliPath, ...argv], {
|
package/package.json
CHANGED
|
@@ -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 };
|