skillio 0.1.13 → 0.1.14
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/README.md +8 -6
- package/dist/cli.js +66 -74
- package/dist/shared/chunk-0qvp6v8g.js +138 -0
- package/dist/shared/chunk-vwrhawsv.js +252 -0
- package/package.json +1 -1
- package/dist/shared/chunk-2gt0ysd1.js +0 -316
- package/dist/shared/chunk-ajnqh9j9.js +0 -86
package/README.md
CHANGED
|
@@ -78,15 +78,16 @@ skillio # equivalent
|
|
|
78
78
|
# subcommands
|
|
79
79
|
skl ls # list skills per source with diffs
|
|
80
80
|
skl cost # ambient ballast cost (frontmatter tokens) per skill
|
|
81
|
-
skl
|
|
81
|
+
skl cs # alias for cost (also: cst)
|
|
82
82
|
skl usage # consumption: usage count × frontmatter tokens
|
|
83
83
|
skl usg # alias for usage
|
|
84
84
|
skl rm brainstorming # delete on-disk dir; lock kept (Y/n prompt)
|
|
85
85
|
skl rm brainstorming writing-plans # remove multiple
|
|
86
|
-
skl rm
|
|
86
|
+
skl rm . # remove all skills in scope (lock kept)
|
|
87
|
+
skl rm . -fl # remove all, including lock entries
|
|
87
88
|
skl rm --yes brainstorming # skip confirmation
|
|
88
89
|
skl rm --dry-run brainstorming # preview only
|
|
89
|
-
skl rm --force-lock brainstorming # also remove the lock entry
|
|
90
|
+
skl rm --force-lock brainstorming # also remove the lock entry (-fl)
|
|
90
91
|
|
|
91
92
|
# scope flags
|
|
92
93
|
skl -g # force global scope on any subcommand
|
|
@@ -158,7 +159,7 @@ skillio list # local skills-lock.json
|
|
|
158
159
|
skillio list --global # ~/.agents/.skill-lock.json
|
|
159
160
|
```
|
|
160
161
|
|
|
161
|
-
### `skillio cost` / `
|
|
162
|
+
### `skillio cost` / `cs`
|
|
162
163
|
|
|
163
164
|
```sh
|
|
164
165
|
skillio cost # local: per-skill frontmatter tokens with verdict
|
|
@@ -170,8 +171,9 @@ skillio cost --global # same, against ~/.agents/.skill-lock.json
|
|
|
170
171
|
```sh
|
|
171
172
|
skillio remove <skill-name> # delete on-disk dir; lock kept
|
|
172
173
|
skillio remove <skill-one> <skill-two>
|
|
173
|
-
skillio remove
|
|
174
|
-
skillio remove
|
|
174
|
+
skillio remove . # remove all skills in scope (lock kept)
|
|
175
|
+
skillio remove . -fl # remove all, including lock entries
|
|
176
|
+
skillio remove --force-lock <skill-name> # also remove the lock entry (alias -fl)
|
|
175
177
|
skillio remove --lock-only <skill-name> # only the lock entry; keep on disk
|
|
176
178
|
skillio remove --global <skill-name>
|
|
177
179
|
skillio remove --dry-run <skill-name> # preview only
|
package/dist/cli.js
CHANGED
|
@@ -6,13 +6,12 @@ import {
|
|
|
6
6
|
discoverSkills,
|
|
7
7
|
getLockPath,
|
|
8
8
|
green,
|
|
9
|
-
promptText,
|
|
10
9
|
readLock,
|
|
11
10
|
red,
|
|
12
11
|
removeSkillFromLock,
|
|
13
12
|
setColorEnabled,
|
|
14
13
|
yellow
|
|
15
|
-
} from "./shared/chunk-
|
|
14
|
+
} from "./shared/chunk-0qvp6v8g.js";
|
|
16
15
|
|
|
17
16
|
// src/cli.ts
|
|
18
17
|
import { createRequire } from "node:module";
|
|
@@ -568,7 +567,7 @@ _skillio_completions() {
|
|
|
568
567
|
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
569
568
|
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
570
569
|
|
|
571
|
-
local cmds="list ls remove rm cost
|
|
570
|
+
local cmds="list ls remove rm cost cs cst usage us usg completion"
|
|
572
571
|
if [ "\${COMP_CWORD}" -eq 1 ]; then
|
|
573
572
|
COMPREPLY=( $(compgen -W "\${cmds} -h --help -v --version" -- "\${cur}") )
|
|
574
573
|
return 0
|
|
@@ -578,7 +577,7 @@ _skillio_completions() {
|
|
|
578
577
|
case "\${sub}" in
|
|
579
578
|
rm|remove)
|
|
580
579
|
if [[ "\${cur}" == -* ]]; then
|
|
581
|
-
COMPREPLY=( $(compgen -W "-g --global --
|
|
580
|
+
COMPREPLY=( $(compgen -W "-g --global --dry-run -y --yes --force-lock -fl --lock-only -h --help" -- "\${cur}") )
|
|
582
581
|
else
|
|
583
582
|
local names
|
|
584
583
|
local scope=""
|
|
@@ -609,7 +608,7 @@ _skillio() {
|
|
|
609
608
|
'remove:Delete on-disk skill dirs'
|
|
610
609
|
'rm:Alias for remove'
|
|
611
610
|
'cost:Show ambient ballast cost'
|
|
612
|
-
'
|
|
611
|
+
'cs:Alias for cost'
|
|
613
612
|
'cst:Alias for cost'
|
|
614
613
|
'usage:Show skill usage'
|
|
615
614
|
'us:Alias for usage'
|
|
@@ -626,10 +625,9 @@ _skillio() {
|
|
|
626
625
|
if [[ \${words[CURRENT]} == -* ]]; then
|
|
627
626
|
_values 'flag' \\
|
|
628
627
|
'-g[global scope]' '--global[global scope]' \\
|
|
629
|
-
'--all[remove every skill in scope]' \\
|
|
630
628
|
'--dry-run[print plan without deleting]' \\
|
|
631
629
|
'-y[skip confirmation]' '--yes[skip confirmation]' \\
|
|
632
|
-
'--force-lock[also remove lock entry]' \\
|
|
630
|
+
'--force-lock[also remove lock entry]' '-fl[alias for --force-lock]' \\
|
|
633
631
|
'--lock-only[remove only lock entry, keep disk]'
|
|
634
632
|
else
|
|
635
633
|
local scope=""
|
|
@@ -671,17 +669,17 @@ function __skillio_using_subcommand
|
|
|
671
669
|
test "$cmd[2]" = "$argv[1]"
|
|
672
670
|
end
|
|
673
671
|
|
|
674
|
-
complete -c skl -n __skillio_needs_command -a 'list ls remove rm cost
|
|
675
|
-
complete -c skillio -n __skillio_needs_command -a 'list ls remove rm cost
|
|
672
|
+
complete -c skl -n __skillio_needs_command -a 'list ls remove rm cost cs cst usage us usg completion'
|
|
673
|
+
complete -c skillio -n __skillio_needs_command -a 'list ls remove rm cost cs cst usage us usg completion'
|
|
676
674
|
|
|
677
675
|
for sub in rm remove
|
|
678
676
|
complete -c skl -n "__skillio_using_subcommand $sub" -f -a '(__skillio_skill_names)'
|
|
679
677
|
complete -c skillio -n "__skillio_using_subcommand $sub" -f -a '(__skillio_skill_names)'
|
|
680
678
|
complete -c skl -n "__skillio_using_subcommand $sub" -s g -l global -d 'Use global scope'
|
|
681
|
-
complete -c skl -n "__skillio_using_subcommand $sub" -l all -d 'Remove every skill in scope'
|
|
682
679
|
complete -c skl -n "__skillio_using_subcommand $sub" -l dry-run -d 'Print plan without deleting'
|
|
683
680
|
complete -c skl -n "__skillio_using_subcommand $sub" -s y -l yes -d 'Skip confirmation prompt'
|
|
684
681
|
complete -c skl -n "__skillio_using_subcommand $sub" -l force-lock -d 'Also remove lock entry'
|
|
682
|
+
complete -c skl -n "__skillio_using_subcommand $sub" -o fl -d 'Alias for --force-lock'
|
|
685
683
|
complete -c skl -n "__skillio_using_subcommand $sub" -l lock-only -d 'Remove only lock entry, keep disk'
|
|
686
684
|
end
|
|
687
685
|
|
|
@@ -881,13 +879,9 @@ var listCommand = defineCommand({
|
|
|
881
879
|
const claudeNames = rows.claude.names.map((n) => n.name);
|
|
882
880
|
const agentsNames = rows.agents.names.map((n) => n.name);
|
|
883
881
|
const lockNames = rows.lock.names.map((n) => n.name);
|
|
884
|
-
const lockOnly = lockNames.filter((n) => !claudeNames.includes(n) && !agentsNames.includes(n));
|
|
885
882
|
const claudeNotInLock = claudeNames.filter((n) => !lockNames.includes(n));
|
|
886
883
|
const agentsNotInLock = agentsNames.filter((n) => !lockNames.includes(n));
|
|
887
884
|
const diffs = [];
|
|
888
|
-
if (lockOnly.length) {
|
|
889
|
-
diffs.push(`skills-lock.json has ${lockOnly.length} skill${lockOnly.length === 1 ? "" : "s"} missing on disk: ${lockOnly.map(cyan).join(", ")}`);
|
|
890
|
-
}
|
|
891
885
|
if (claudeNotInLock.length) {
|
|
892
886
|
diffs.push(`.claude/skills has ${claudeNotInLock.length} skill${claudeNotInLock.length === 1 ? "" : "s"} not in lock: ${claudeNotInLock.map(cyan).join(", ")}`);
|
|
893
887
|
}
|
|
@@ -1073,7 +1067,6 @@ var removeCommand = defineCommand({
|
|
|
1073
1067
|
global: { type: "boolean", alias: "g", default: false, description: "Use global scope" },
|
|
1074
1068
|
"dry-run": { type: "boolean", default: false, description: "Print plan, do not delete" },
|
|
1075
1069
|
yes: { type: "boolean", alias: "y", default: false, description: "Skip confirmation prompt" },
|
|
1076
|
-
all: { type: "boolean", default: false, description: "Remove every skill in scope" },
|
|
1077
1070
|
"force-lock": {
|
|
1078
1071
|
type: "boolean",
|
|
1079
1072
|
default: false,
|
|
@@ -1090,7 +1083,6 @@ var removeCommand = defineCommand({
|
|
|
1090
1083
|
global: isGlobal,
|
|
1091
1084
|
"dry-run": dryRun,
|
|
1092
1085
|
yes,
|
|
1093
|
-
all,
|
|
1094
1086
|
"force-lock": modifyLock,
|
|
1095
1087
|
"lock-only": lockOnly
|
|
1096
1088
|
} = args;
|
|
@@ -1099,9 +1091,11 @@ var removeCommand = defineCommand({
|
|
|
1099
1091
|
process.exit(1);
|
|
1100
1092
|
}
|
|
1101
1093
|
const subcmdIdx = process.argv.findIndex((a) => a === "remove" || a === "rm");
|
|
1102
|
-
const
|
|
1094
|
+
const rawNames = process.argv.slice(subcmdIdx + 1).filter((a) => !a.startsWith("-"));
|
|
1095
|
+
const all = rawNames.includes(".");
|
|
1096
|
+
const names = rawNames.filter((n) => n !== ".");
|
|
1103
1097
|
if (all && names.length > 0) {
|
|
1104
|
-
console.error("
|
|
1098
|
+
console.error('"." (all skills) is mutually exclusive with positional skill names');
|
|
1105
1099
|
process.exit(1);
|
|
1106
1100
|
}
|
|
1107
1101
|
if (!all && names.length === 0) {
|
|
@@ -1131,64 +1125,58 @@ var removeCommand = defineCommand({
|
|
|
1131
1125
|
}
|
|
1132
1126
|
if (dryRun)
|
|
1133
1127
|
return;
|
|
1134
|
-
if (
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
if (phrase !== "all") {
|
|
1140
|
-
console.log("Aborted");
|
|
1141
|
-
process.exit(1);
|
|
1142
|
-
}
|
|
1143
|
-
} else if (!yes) {
|
|
1144
|
-
const ok = await confirm(`Remove ${subject}?`);
|
|
1145
|
-
if (!ok) {
|
|
1146
|
-
console.log("Aborted");
|
|
1147
|
-
process.exit(1);
|
|
1148
|
-
}
|
|
1128
|
+
if (!yes) {
|
|
1129
|
+
let question = "Proceed?";
|
|
1130
|
+
if (all) {
|
|
1131
|
+
const subject = lockOnly ? `ALL ${plans.length} lock entries (disk preserved)` : `ALL ${plans.length} skills`;
|
|
1132
|
+
question = `Remove ${subject}?`;
|
|
1149
1133
|
}
|
|
1150
|
-
|
|
1151
|
-
const ok = await confirm("Proceed?");
|
|
1134
|
+
const ok = await confirm(question);
|
|
1152
1135
|
if (!ok) {
|
|
1153
1136
|
console.log("Aborted");
|
|
1154
1137
|
process.exit(1);
|
|
1155
1138
|
}
|
|
1156
1139
|
}
|
|
1157
1140
|
const allowedRoots = [isGlobal ? homedir2() : dirname2(resolve3(lockPath)), homedir2()];
|
|
1141
|
+
const removed = (s) => red("removed") + s;
|
|
1142
|
+
const kept = (s) => green("kept") + s;
|
|
1143
|
+
const skipped = (s) => yellow("skipped") + s;
|
|
1158
1144
|
for (const { target } of plans) {
|
|
1145
|
+
console.log("");
|
|
1146
|
+
console.log(q(target.name));
|
|
1147
|
+
if (lockOnly) {
|
|
1148
|
+
if (target.agentsDir)
|
|
1149
|
+
console.log(kept(" .agents/skills (--lock-only)"));
|
|
1150
|
+
else
|
|
1151
|
+
console.log(skipped(" .agents/skills (not found)"));
|
|
1152
|
+
if (target.claudeDir)
|
|
1153
|
+
console.log(kept(" .claude/skills (--lock-only)"));
|
|
1154
|
+
else
|
|
1155
|
+
console.log(skipped(" .claude/skills (not found)"));
|
|
1156
|
+
} else {
|
|
1157
|
+
if (target.agentsDir) {
|
|
1158
|
+
const r = rmSkillDir(target.agentsDir, { allowedRoots });
|
|
1159
|
+
console.log(removed(` from .agents/skills (${r.fileCount} files)`));
|
|
1160
|
+
} else {
|
|
1161
|
+
console.log(skipped(" .agents/skills (not found)"));
|
|
1162
|
+
}
|
|
1163
|
+
if (target.claudeDir) {
|
|
1164
|
+
const r = rmSkillDir(target.claudeDir, { allowedRoots });
|
|
1165
|
+
console.log(removed(` from .claude/skills (${r.fileCount} files)`));
|
|
1166
|
+
} else {
|
|
1167
|
+
console.log(skipped(" .claude/skills (not found)"));
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1159
1170
|
if (target.inLock) {
|
|
1160
1171
|
if (lockOnly || modifyLock) {
|
|
1161
1172
|
const r = removeSkillFromLock(lockPath, target.name);
|
|
1162
1173
|
if (r.removed)
|
|
1163
|
-
console.log(
|
|
1174
|
+
console.log(removed(" from skills-lock.json"));
|
|
1164
1175
|
} else {
|
|
1165
|
-
console.log(
|
|
1176
|
+
console.log(kept(" in skills-lock.json"));
|
|
1166
1177
|
}
|
|
1167
1178
|
} else {
|
|
1168
|
-
console.log(
|
|
1169
|
-
}
|
|
1170
|
-
if (lockOnly) {
|
|
1171
|
-
if (target.claudeDir)
|
|
1172
|
-
console.log(`Kept .claude/skills/${target.name}/ (--lock-only)`);
|
|
1173
|
-
else
|
|
1174
|
-
console.log("Skipped .claude/skills (not found)");
|
|
1175
|
-
if (target.agentsDir)
|
|
1176
|
-
console.log(`Kept .agents/skills/${target.name}/ (--lock-only)`);
|
|
1177
|
-
else
|
|
1178
|
-
console.log("Skipped .agents/skills (not found)");
|
|
1179
|
-
continue;
|
|
1180
|
-
}
|
|
1181
|
-
if (target.claudeDir) {
|
|
1182
|
-
const r = rmSkillDir(target.claudeDir, { allowedRoots });
|
|
1183
|
-
console.log(`Removed ${q(target.name)} from .claude/skills (${r.fileCount} files)`);
|
|
1184
|
-
} else {
|
|
1185
|
-
console.log("Skipped .claude/skills (not found)");
|
|
1186
|
-
}
|
|
1187
|
-
if (target.agentsDir) {
|
|
1188
|
-
const r = rmSkillDir(target.agentsDir, { allowedRoots });
|
|
1189
|
-
console.log(`Removed ${q(target.name)} from .agents/skills (${r.fileCount} files)`);
|
|
1190
|
-
} else {
|
|
1191
|
-
console.log("Skipped .agents/skills (not found)");
|
|
1179
|
+
console.log(skipped(" skills-lock.json (not in lock)"));
|
|
1192
1180
|
}
|
|
1193
1181
|
}
|
|
1194
1182
|
}
|
|
@@ -1962,7 +1950,7 @@ var SUBCOMMAND_NAMES = new Set([
|
|
|
1962
1950
|
"remove",
|
|
1963
1951
|
"rm",
|
|
1964
1952
|
"cost",
|
|
1965
|
-
"
|
|
1953
|
+
"cs",
|
|
1966
1954
|
"cst",
|
|
1967
1955
|
"usage",
|
|
1968
1956
|
"us",
|
|
@@ -1981,7 +1969,10 @@ function reorderRootFlagsToSubcommand(argv) {
|
|
|
1981
1969
|
return argv;
|
|
1982
1970
|
return [argv[0] ?? "", argv[1] ?? "", sub, ...before, ...after];
|
|
1983
1971
|
}
|
|
1984
|
-
|
|
1972
|
+
function normalizeShortFlags(argv) {
|
|
1973
|
+
return argv.map((tok) => tok === "-fl" ? "--force-lock" : tok);
|
|
1974
|
+
}
|
|
1975
|
+
process.argv = reorderRootFlagsToSubcommand(normalizeShortFlags(mergeAgentArgs(process.argv)));
|
|
1985
1976
|
function printRootHelp() {
|
|
1986
1977
|
const lines = [
|
|
1987
1978
|
`Audit and manage AI agent skills (skillio v${version})`,
|
|
@@ -2000,7 +1991,7 @@ function printRootHelp() {
|
|
|
2000
1991
|
"",
|
|
2001
1992
|
" list, ls List skills per source: install type, lock orphans, disk/lock diff",
|
|
2002
1993
|
" remove, rm Delete on-disk skill dirs; lock kept unless --force-lock",
|
|
2003
|
-
" cost,
|
|
1994
|
+
" cost, cs, cst Show ambient ballast cost (per-skill frontmatter tokens) sorted desc",
|
|
2004
1995
|
" usage, us, usg Show skill usage × cost (consumption) with missed rows",
|
|
2005
1996
|
" completion Print shell completion script (bash, zsh, fish)"
|
|
2006
1997
|
];
|
|
@@ -2030,22 +2021,23 @@ function printRemoveHelp() {
|
|
|
2030
2021
|
"",
|
|
2031
2022
|
"ARGUMENTS",
|
|
2032
2023
|
"",
|
|
2033
|
-
|
|
2024
|
+
' SKILL... One or more skill names. Use "." to target every skill in scope.',
|
|
2034
2025
|
"",
|
|
2035
2026
|
"OPTIONS",
|
|
2036
2027
|
"",
|
|
2037
|
-
" -g, --global
|
|
2038
|
-
" --
|
|
2039
|
-
|
|
2040
|
-
"
|
|
2041
|
-
"
|
|
2042
|
-
" --lock-only
|
|
2028
|
+
" -g, --global Use global scope (default: false)",
|
|
2029
|
+
" --dry-run Print plan without deleting",
|
|
2030
|
+
' -y, --yes Skip confirmation prompt (non-TTY only for ".")',
|
|
2031
|
+
" --force-lock Also remove entry from skills-lock.json (default: lock preserved)",
|
|
2032
|
+
" -fl Alias for --force-lock",
|
|
2033
|
+
" --lock-only Remove only the lock entry; keep on-disk directories",
|
|
2043
2034
|
"",
|
|
2044
2035
|
"EXAMPLES",
|
|
2045
2036
|
"",
|
|
2046
2037
|
" skillio rm brainstorming",
|
|
2047
2038
|
" skillio rm brainstorming writing-plans --yes",
|
|
2048
|
-
" skillio rm
|
|
2039
|
+
" skillio rm . --dry-run",
|
|
2040
|
+
" skillio rm . -fl",
|
|
2049
2041
|
" skillio rm --force-lock obsolete-skill",
|
|
2050
2042
|
" skillio rm --lock-only stale-entry"
|
|
2051
2043
|
];
|
|
@@ -2097,7 +2089,7 @@ var main = defineCommand({
|
|
|
2097
2089
|
return;
|
|
2098
2090
|
const interactive = process.stdout.isTTY && process.stdin.isTTY;
|
|
2099
2091
|
if (interactive) {
|
|
2100
|
-
const { runPicker } = await import("./shared/chunk-
|
|
2092
|
+
const { runPicker } = await import("./shared/chunk-vwrhawsv.js");
|
|
2101
2093
|
const status = await runPicker({
|
|
2102
2094
|
global: args.global ?? false
|
|
2103
2095
|
});
|
|
@@ -2115,7 +2107,7 @@ var main = defineCommand({
|
|
|
2115
2107
|
remove: removeCommand,
|
|
2116
2108
|
rm: removeCommand,
|
|
2117
2109
|
cost: costCommand,
|
|
2118
|
-
|
|
2110
|
+
cs: costCommand,
|
|
2119
2111
|
cst: costCommand,
|
|
2120
2112
|
usage: usageCommand,
|
|
2121
2113
|
us: usageCommand,
|
|
@@ -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,316 +0,0 @@
|
|
|
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/prompt.ts
|
|
60
|
-
import { emitKeypressEvents } from "node:readline";
|
|
61
|
-
async function select(params) {
|
|
62
|
-
const input = params.input ?? process.stdin;
|
|
63
|
-
const output = params.output ?? process.stdout;
|
|
64
|
-
if (!input.isTTY || !output.isTTY)
|
|
65
|
-
return null;
|
|
66
|
-
let cursor = 0;
|
|
67
|
-
const total = params.options.length;
|
|
68
|
-
function render() {
|
|
69
|
-
output.write(`${params.title}
|
|
70
|
-
`);
|
|
71
|
-
for (let i = 0;i < total; i++) {
|
|
72
|
-
const opt = params.options[i];
|
|
73
|
-
if (!opt)
|
|
74
|
-
continue;
|
|
75
|
-
const marker = i === cursor ? cyan(">") : " ";
|
|
76
|
-
output.write(`${marker} ${opt.label}
|
|
77
|
-
`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
function clear() {
|
|
81
|
-
output.write(`\x1B[${total + 1}A\x1B[J`);
|
|
82
|
-
}
|
|
83
|
-
emitKeypressEvents(input);
|
|
84
|
-
if (input.setRawMode)
|
|
85
|
-
input.setRawMode(true);
|
|
86
|
-
input.resume();
|
|
87
|
-
render();
|
|
88
|
-
return await new Promise((resolve) => {
|
|
89
|
-
const onKey = (_str, key) => {
|
|
90
|
-
if (key.ctrl && key.name === "c") {
|
|
91
|
-
cleanup();
|
|
92
|
-
resolve(null);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (key.name === "escape" || key.name === "q") {
|
|
96
|
-
cleanup();
|
|
97
|
-
resolve(null);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (key.name === "up" && cursor > 0) {
|
|
101
|
-
cursor--;
|
|
102
|
-
clear();
|
|
103
|
-
render();
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (key.name === "down" && cursor < total - 1) {
|
|
107
|
-
cursor++;
|
|
108
|
-
clear();
|
|
109
|
-
render();
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
if (key.name === "return") {
|
|
113
|
-
cleanup();
|
|
114
|
-
const chosen = params.options[cursor]?.value ?? null;
|
|
115
|
-
resolve(chosen);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
function onSigterm() {
|
|
120
|
-
cleanup();
|
|
121
|
-
resolve(null);
|
|
122
|
-
}
|
|
123
|
-
function cleanup() {
|
|
124
|
-
input.removeListener("keypress", onKey);
|
|
125
|
-
process.removeListener("SIGTERM", onSigterm);
|
|
126
|
-
if (input.setRawMode)
|
|
127
|
-
input.setRawMode(false);
|
|
128
|
-
input.pause();
|
|
129
|
-
}
|
|
130
|
-
process.once("SIGTERM", onSigterm);
|
|
131
|
-
input.on("keypress", onKey);
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
async function promptText(question, params) {
|
|
135
|
-
const input = params?.input ?? process.stdin;
|
|
136
|
-
const output = params?.output ?? process.stdout;
|
|
137
|
-
const { createInterface } = await import("node:readline/promises");
|
|
138
|
-
const rl = createInterface({ input, output });
|
|
139
|
-
try {
|
|
140
|
-
return (await rl.question(`${question} `)).trim();
|
|
141
|
-
} finally {
|
|
142
|
-
rl.close();
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
async function multiSelect(params) {
|
|
146
|
-
const input = params.input ?? process.stdin;
|
|
147
|
-
const output = params.output ?? process.stdout;
|
|
148
|
-
if (!input.isTTY || !output.isTTY)
|
|
149
|
-
return null;
|
|
150
|
-
let cursor = 0;
|
|
151
|
-
const total = params.options.length;
|
|
152
|
-
const selected = new Set;
|
|
153
|
-
function render() {
|
|
154
|
-
output.write(`${params.title}
|
|
155
|
-
`);
|
|
156
|
-
for (let i = 0;i < total; i++) {
|
|
157
|
-
const opt = params.options[i];
|
|
158
|
-
if (!opt)
|
|
159
|
-
continue;
|
|
160
|
-
const cursorMark = i === cursor ? cyan(">") : " ";
|
|
161
|
-
const checkbox = selected.has(i) ? "[x]" : "[ ]";
|
|
162
|
-
output.write(`${cursorMark} ${checkbox} ${opt.label}
|
|
163
|
-
`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
function clear() {
|
|
167
|
-
output.write(`\x1B[${total + 1}A\x1B[J`);
|
|
168
|
-
}
|
|
169
|
-
emitKeypressEvents(input);
|
|
170
|
-
if (input.setRawMode)
|
|
171
|
-
input.setRawMode(true);
|
|
172
|
-
input.resume();
|
|
173
|
-
render();
|
|
174
|
-
return await new Promise((resolve) => {
|
|
175
|
-
const onKey = (_str, key) => {
|
|
176
|
-
if (key.ctrl && key.name === "c") {
|
|
177
|
-
cleanup();
|
|
178
|
-
resolve(null);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
if (key.name === "escape" || key.name === "q") {
|
|
182
|
-
cleanup();
|
|
183
|
-
resolve(null);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
if (key.name === "up" && cursor > 0) {
|
|
187
|
-
cursor--;
|
|
188
|
-
clear();
|
|
189
|
-
render();
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
if (key.name === "down" && cursor < total - 1) {
|
|
193
|
-
cursor++;
|
|
194
|
-
clear();
|
|
195
|
-
render();
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
if (key.name === "space") {
|
|
199
|
-
if (selected.has(cursor))
|
|
200
|
-
selected.delete(cursor);
|
|
201
|
-
else
|
|
202
|
-
selected.add(cursor);
|
|
203
|
-
clear();
|
|
204
|
-
render();
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
if (key.name === "return") {
|
|
208
|
-
cleanup();
|
|
209
|
-
const values = [];
|
|
210
|
-
for (let i = 0;i < total; i++) {
|
|
211
|
-
if (selected.has(i)) {
|
|
212
|
-
const v = params.options[i]?.value;
|
|
213
|
-
if (v !== undefined)
|
|
214
|
-
values.push(v);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
resolve(values);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
function onSigterm() {
|
|
222
|
-
cleanup();
|
|
223
|
-
resolve(null);
|
|
224
|
-
}
|
|
225
|
-
function cleanup() {
|
|
226
|
-
input.removeListener("keypress", onKey);
|
|
227
|
-
process.removeListener("SIGTERM", onSigterm);
|
|
228
|
-
if (input.setRawMode)
|
|
229
|
-
input.setRawMode(false);
|
|
230
|
-
input.pause();
|
|
231
|
-
}
|
|
232
|
-
process.once("SIGTERM", onSigterm);
|
|
233
|
-
input.on("keypress", onKey);
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// src/utils/discover-skills.ts
|
|
238
|
-
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
239
|
-
import { homedir as homedir3 } from "node:os";
|
|
240
|
-
import { dirname as dirname3, join as join3, resolve as resolve2 } from "node:path";
|
|
241
|
-
|
|
242
|
-
// src/utils/skill-files.ts
|
|
243
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
244
|
-
import { homedir as homedir2 } from "node:os";
|
|
245
|
-
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
246
|
-
var CHARS_PER_TOKEN = 4;
|
|
247
|
-
function extractFrontmatter(content) {
|
|
248
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
249
|
-
return match?.[1];
|
|
250
|
-
}
|
|
251
|
-
function estimateTokens(text) {
|
|
252
|
-
return Math.round(text.length / CHARS_PER_TOKEN);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// src/utils/discover-skills.ts
|
|
256
|
-
function resolveRoots(input) {
|
|
257
|
-
if (input.isGlobal) {
|
|
258
|
-
return {
|
|
259
|
-
claude: join3(homedir3(), ".claude", "skills"),
|
|
260
|
-
agents: join3(homedir3(), ".agents", "skills")
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
const repo = dirname3(resolve2(input.lockPath));
|
|
264
|
-
return {
|
|
265
|
-
claude: join3(repo, ".claude", "skills"),
|
|
266
|
-
agents: join3(repo, ".agents", "skills")
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
function listSkillNames(root) {
|
|
270
|
-
if (!root || !existsSync3(root))
|
|
271
|
-
return [];
|
|
272
|
-
return readdirSync(root).filter((name) => {
|
|
273
|
-
const skill = join3(root, name, "SKILL.md");
|
|
274
|
-
return existsSync3(skill) && statSync(skill).isFile();
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
function tokensFromFile(path) {
|
|
278
|
-
const content = readFileSync3(path, "utf8");
|
|
279
|
-
const fm = extractFrontmatter(content);
|
|
280
|
-
if (fm === undefined)
|
|
281
|
-
return { status: "no-frontmatter" };
|
|
282
|
-
return { tokens: estimateTokens(fm), status: "ok" };
|
|
283
|
-
}
|
|
284
|
-
function discoverSkills(input) {
|
|
285
|
-
const roots = resolveRoots(input);
|
|
286
|
-
const lock = readLock(input.lockPath);
|
|
287
|
-
const lockNames = Object.keys(lock.skills);
|
|
288
|
-
const claudeNames = listSkillNames(roots.claude);
|
|
289
|
-
const agentsNames = listSkillNames(roots.agents);
|
|
290
|
-
const all = new Set([...lockNames, ...claudeNames, ...agentsNames]);
|
|
291
|
-
const out = new Map;
|
|
292
|
-
for (const name of all) {
|
|
293
|
-
const sources = [];
|
|
294
|
-
if (lockNames.includes(name))
|
|
295
|
-
sources.push("lock");
|
|
296
|
-
if (claudeNames.includes(name))
|
|
297
|
-
sources.push(".claude");
|
|
298
|
-
if (agentsNames.includes(name))
|
|
299
|
-
sources.push(".agents");
|
|
300
|
-
let skillFile;
|
|
301
|
-
if (claudeNames.includes(name) && roots.claude) {
|
|
302
|
-
skillFile = join3(roots.claude, name, "SKILL.md");
|
|
303
|
-
} else if (agentsNames.includes(name) && roots.agents) {
|
|
304
|
-
skillFile = join3(roots.agents, name, "SKILL.md");
|
|
305
|
-
}
|
|
306
|
-
if (!skillFile) {
|
|
307
|
-
out.set(name, { name, sources, status: "missing" });
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
const { tokens, status } = tokensFromFile(skillFile);
|
|
311
|
-
out.set(name, { name, sources, skillFile, frontmatterTokens: tokens, status });
|
|
312
|
-
}
|
|
313
|
-
return out;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export { __require, getLockPath, readLock, removeSkillFromLock, setColorEnabled, detectColorSupport, green, yellow, red, cyan, discoverSkills, select, promptText, multiSelect };
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
discoverSkills,
|
|
3
|
-
getLockPath,
|
|
4
|
-
multiSelect,
|
|
5
|
-
red,
|
|
6
|
-
select
|
|
7
|
-
} from "./chunk-2gt0ysd1.js";
|
|
8
|
-
|
|
9
|
-
// src/commands/picker.ts
|
|
10
|
-
import { spawnSync } from "node:child_process";
|
|
11
|
-
|
|
12
|
-
// src/utils/list-removable.ts
|
|
13
|
-
function listRemovableTargets(input) {
|
|
14
|
-
const records = [...discoverSkills(input).values()];
|
|
15
|
-
const inLock = [];
|
|
16
|
-
const orphan = [];
|
|
17
|
-
for (const r of records) {
|
|
18
|
-
if (r.sources.includes("lock"))
|
|
19
|
-
inLock.push(r.name);
|
|
20
|
-
else
|
|
21
|
-
orphan.push(r.name);
|
|
22
|
-
}
|
|
23
|
-
inLock.sort();
|
|
24
|
-
orphan.sort();
|
|
25
|
-
return { inLock, orphan };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// src/commands/picker.ts
|
|
29
|
-
async function pickRemoveTargets(args) {
|
|
30
|
-
const lockPath = getLockPath(args.global);
|
|
31
|
-
const { inLock, orphan } = listRemovableTargets({
|
|
32
|
-
isGlobal: args.global,
|
|
33
|
-
cwd: process.cwd(),
|
|
34
|
-
lockPath
|
|
35
|
-
});
|
|
36
|
-
if (inLock.length === 0 && orphan.length === 0) {
|
|
37
|
-
console.log("No skills found in scope.");
|
|
38
|
-
return [];
|
|
39
|
-
}
|
|
40
|
-
const options = [
|
|
41
|
-
...inLock.map((name) => ({ value: name, label: name })),
|
|
42
|
-
...orphan.map((name) => ({ value: name, label: `${name} ${red("(orphan)")}` }))
|
|
43
|
-
];
|
|
44
|
-
return await multiSelect({
|
|
45
|
-
title: "skillio — pick skills to remove (Space toggle, Enter confirm)",
|
|
46
|
-
options
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
async function runPicker(args) {
|
|
50
|
-
const choice = await select({
|
|
51
|
-
title: "skillio — pick a command",
|
|
52
|
-
options: [
|
|
53
|
-
{ value: "usage", label: "usage — count of skill invocations" },
|
|
54
|
-
{ value: "cost", label: "cost — per-skill ambient tokens" },
|
|
55
|
-
{ value: "list", label: "list — installed skills per source" },
|
|
56
|
-
{ value: "remove", label: "remove — delete a skill (disk-only; lock with --force-lock)" },
|
|
57
|
-
{ value: "quit", label: "quit" }
|
|
58
|
-
]
|
|
59
|
-
});
|
|
60
|
-
if (choice === null || choice === "quit")
|
|
61
|
-
return 0;
|
|
62
|
-
const cliPath = process.argv[1];
|
|
63
|
-
if (!cliPath) {
|
|
64
|
-
console.error("skillio: cannot resolve CLI path (process.argv[1] missing)");
|
|
65
|
-
return 1;
|
|
66
|
-
}
|
|
67
|
-
let argv;
|
|
68
|
-
if (choice === "remove") {
|
|
69
|
-
const targets = await pickRemoveTargets(args);
|
|
70
|
-
if (targets === null || targets.length === 0)
|
|
71
|
-
return 0;
|
|
72
|
-
argv = ["rm", ...targets];
|
|
73
|
-
} else {
|
|
74
|
-
argv = [choice];
|
|
75
|
-
}
|
|
76
|
-
if (args.global)
|
|
77
|
-
argv.push("-g");
|
|
78
|
-
const r = spawnSync(process.execPath, [cliPath, ...argv], {
|
|
79
|
-
stdio: "inherit",
|
|
80
|
-
env: process.env
|
|
81
|
-
});
|
|
82
|
-
return r.status ?? 0;
|
|
83
|
-
}
|
|
84
|
-
export {
|
|
85
|
-
runPicker
|
|
86
|
-
};
|