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 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 cst # alias for cost
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 --all # remove all skills in scope
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` / `co`
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 --all # remove all skills in scope
174
- skillio remove --force-lock <skill-name> # also remove the lock entry
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-2gt0ysd1.js";
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 co cst usage us usg completion"
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 --all --dry-run -y --yes --force-lock --lock-only -h --help" -- "\${cur}") )
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
- 'co:Alias for cost'
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 co cst usage us usg completion'
675
- complete -c skillio -n __skillio_needs_command -a 'list ls remove rm cost co cst usage us usg completion'
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 names = process.argv.slice(subcmdIdx + 1).filter((a) => !a.startsWith("-"));
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("--all is mutually exclusive with positional skill names");
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 (all) {
1135
- const subject = lockOnly ? `ALL ${plans.length} lock entries (disk preserved)` : `ALL ${plans.length} skills`;
1136
- const interactive = process.stdin.isTTY && process.stdout.isTTY;
1137
- if (interactive) {
1138
- const phrase = await promptText(`This will remove ${subject}. Type "all" to confirm:`);
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
- } else if (!yes) {
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(`Removed ${q(target.name)} from skills-lock.json`);
1174
+ console.log(removed(" from skills-lock.json"));
1164
1175
  } else {
1165
- console.log(`Kept ${q(target.name)} in skills-lock.json (no --force-lock)`);
1176
+ console.log(kept(" in skills-lock.json"));
1166
1177
  }
1167
1178
  } else {
1168
- console.log(`Skipped skills-lock.json (not in lock)`);
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
- "co",
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
- process.argv = reorderRootFlagsToSubcommand(mergeAgentArgs(process.argv));
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, co, cst Show ambient ballast cost (per-skill frontmatter tokens) sorted desc",
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
- " SKILL... One or more skill names. Use --all to target every skill in scope.",
2024
+ ' SKILL... One or more skill names. Use "." to target every skill in scope.',
2034
2025
  "",
2035
2026
  "OPTIONS",
2036
2027
  "",
2037
- " -g, --global Use global scope (default: false)",
2038
- " --all Remove every skill in scope (mutually exclusive with SKILL)",
2039
- " --dry-run Print plan without deleting",
2040
- " -y, --yes Skip confirmation prompt (non-TTY only for --all)",
2041
- " --force-lock Also remove entry from skills-lock.json (default: lock preserved)",
2042
- " --lock-only Remove only the lock entry; keep on-disk directories",
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 --all --dry-run",
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-ajnqh9j9.js");
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
- co: costCommand,
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,6 +1,6 @@
1
1
  {
2
2
  "name": "skillio",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Audit and manage AI agent skills for Claude Code and Codex",
5
5
  "license": "MIT",
6
6
  "author": "ihororlovskyi",
@@ -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
- };