skillio 0.1.14 → 0.1.16

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
@@ -81,13 +81,13 @@ skl cost # ambient ballast cost (frontmatter token
81
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
- skl rm brainstorming # delete on-disk dir; lock kept (Y/n prompt)
85
- skl rm brainstorming writing-plans # remove multiple
86
- skl rm . # remove all skills in scope (lock kept)
87
- skl rm . -fl # remove all, including lock entries
88
- skl rm --yes brainstorming # skip confirmation
89
- skl rm --dry-run brainstorming # preview only
90
- skl rm --force-lock brainstorming # also remove the lock entry (-fl)
84
+ skl rm brainstorming # colored plan, Proceed? [y/n], then Clean lock? [y/n]
85
+ skl rm brainstorming writing-plans # remove multiple, one pair of prompts
86
+ skl rm . # remove all skills in scope
87
+ skl rm --yes brainstorming # skip both prompts
88
+ skl rm brainstorming --lock-only # only the lock entry (alias --lo)
89
+ skl rm brainstorming --agents-only # only .agents/skills (alias --ao)
90
+ skl rm brainstorming --claude-only # only .claude/skills (alias --co)
91
91
 
92
92
  # scope flags
93
93
  skl -g # force global scope on any subcommand
@@ -169,15 +169,14 @@ skillio cost --global # same, against ~/.agents/.skill-lock.json
169
169
  ### `skillio remove` / `rm`
170
170
 
171
171
  ```sh
172
- skillio remove <skill-name> # delete on-disk dir; lock kept
172
+ skillio remove <skill-name> # colored plan/summary, two prompts (disk, then lock)
173
173
  skillio remove <skill-one> <skill-two>
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)
177
- skillio remove --lock-only <skill-name> # only the lock entry; keep on disk
174
+ skillio remove . # remove all skills in scope
175
+ skillio remove --lock-only <skill-name> # only the lock entry; keep on disk (alias --lo)
176
+ skillio remove --agents-only <skill-name> # only .agents/skills; keep .claude/skills and lock (alias --ao)
177
+ skillio remove --claude-only <skill-name> # only .claude/skills; keep .agents/skills and lock (alias --co)
178
178
  skillio remove --global <skill-name>
179
- skillio remove --dry-run <skill-name> # preview only
180
- skillio remove --yes <skill-name> # skip confirmation prompt
179
+ skillio remove --yes <skill-name> # skip both confirmation prompts
181
180
  ```
182
181
 
183
182
  ### Shell completion
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  __require,
4
+ countLockLinesToRemove,
4
5
  cyan,
5
6
  detectColorSupport,
6
7
  discoverSkills,
@@ -11,7 +12,7 @@ import {
11
12
  removeSkillFromLock,
12
13
  setColorEnabled,
13
14
  yellow
14
- } from "./shared/chunk-0qvp6v8g.js";
15
+ } from "./shared/chunk-nkpp3h85.js";
15
16
 
16
17
  // src/cli.ts
17
18
  import { createRequire } from "node:module";
@@ -577,7 +578,7 @@ _skillio_completions() {
577
578
  case "\${sub}" in
578
579
  rm|remove)
579
580
  if [[ "\${cur}" == -* ]]; then
580
- COMPREPLY=( $(compgen -W "-g --global --dry-run -y --yes --force-lock -fl --lock-only -h --help" -- "\${cur}") )
581
+ COMPREPLY=( $(compgen -W "-g --global -y --yes --lock-only --lo --agents-only --ao --claude-only --co -h --help" -- "\${cur}") )
581
582
  else
582
583
  local names
583
584
  local scope=""
@@ -625,10 +626,10 @@ _skillio() {
625
626
  if [[ \${words[CURRENT]} == -* ]]; then
626
627
  _values 'flag' \\
627
628
  '-g[global scope]' '--global[global scope]' \\
628
- '--dry-run[print plan without deleting]' \\
629
629
  '-y[skip confirmation]' '--yes[skip confirmation]' \\
630
- '--force-lock[also remove lock entry]' '-fl[alias for --force-lock]' \\
631
- '--lock-only[remove only lock entry, keep disk]'
630
+ '--lock-only[only remove lock entry]' '--lo[alias for --lock-only]' \\
631
+ '--agents-only[only remove from .agents/skills]' '--ao[alias for --agents-only]' \\
632
+ '--claude-only[only remove from .claude/skills]' '--co[alias for --claude-only]'
632
633
  else
633
634
  local scope=""
634
635
  for w in \${words[@]}; do
@@ -676,11 +677,13 @@ for sub in rm remove
676
677
  complete -c skl -n "__skillio_using_subcommand $sub" -f -a '(__skillio_skill_names)'
677
678
  complete -c skillio -n "__skillio_using_subcommand $sub" -f -a '(__skillio_skill_names)'
678
679
  complete -c skl -n "__skillio_using_subcommand $sub" -s g -l global -d 'Use global scope'
679
- complete -c skl -n "__skillio_using_subcommand $sub" -l dry-run -d 'Print plan without deleting'
680
680
  complete -c skl -n "__skillio_using_subcommand $sub" -s y -l yes -d 'Skip confirmation prompt'
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'
683
- complete -c skl -n "__skillio_using_subcommand $sub" -l lock-only -d 'Remove only lock entry, keep disk'
681
+ complete -c skl -n "__skillio_using_subcommand $sub" -l lock-only -d 'Only remove lock entry'
682
+ complete -c skl -n "__skillio_using_subcommand $sub" -l lo -d 'Alias for --lock-only'
683
+ complete -c skl -n "__skillio_using_subcommand $sub" -l agents-only -d 'Only remove from .agents/skills'
684
+ complete -c skl -n "__skillio_using_subcommand $sub" -l ao -d 'Alias for --agents-only'
685
+ complete -c skl -n "__skillio_using_subcommand $sub" -l claude-only -d 'Only remove from .claude/skills'
686
+ complete -c skl -n "__skillio_using_subcommand $sub" -l co -d 'Alias for --claude-only'
684
687
  end
685
688
 
686
689
  for sub in completion
@@ -858,9 +861,8 @@ var listCommand = defineCommand({
858
861
  render: () => {
859
862
  if (rows.lock.totalCount === 0)
860
863
  return "";
861
- if (orphans.length === 0)
862
- return green("All skills onboard!");
863
- return orphans.map((n) => red(n.name)).join(" ");
864
+ const orphanSet = new Set(orphans.map((n) => n.name));
865
+ return rows.lock.names.map((n) => orphanSet.has(n.name) ? red(n.name) : n.name).join(" ");
864
866
  }
865
867
  }
866
868
  ];
@@ -897,7 +899,6 @@ var listCommand = defineCommand({
897
899
  });
898
900
 
899
901
  // src/commands/remove.ts
900
- import { existsSync as existsSync3 } from "node:fs";
901
902
  import { homedir as homedir2 } from "node:os";
902
903
  import { dirname as dirname2, join as join3, resolve as resolve3 } from "node:path";
903
904
 
@@ -910,13 +911,13 @@ async function confirm(question, opts = {}) {
910
911
  const { createInterface } = await import("node:readline/promises");
911
912
  const rl = createInterface({ input, output });
912
913
  try {
913
- const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
914
+ const answer = (await rl.question(`${question} [y/n] `)).trim().toLowerCase();
914
915
  return answer === "y" || answer === "yes";
915
916
  } finally {
916
917
  rl.close();
917
918
  }
918
919
  }
919
- output.write(`${question} [y/N] `);
920
+ output.write(`${question} [y/n] `);
920
921
  emitKeypressEvents(input);
921
922
  if (input.setRawMode)
922
923
  input.setRawMode(true);
@@ -963,19 +964,46 @@ async function confirm(question, opts = {}) {
963
964
  input.on("keypress", onKey);
964
965
  });
965
966
  }
967
+ function createConfirmer(opts = {}) {
968
+ const input = opts.input ?? process.stdin;
969
+ const output = opts.output ?? process.stdout;
970
+ if (input.isTTY && output.isTTY) {
971
+ return (question) => confirm(question, { input, output });
972
+ }
973
+ let queue;
974
+ return async (question) => {
975
+ if (queue === undefined) {
976
+ let raw = "";
977
+ for await (const chunk of input)
978
+ raw += chunk;
979
+ queue = raw.split(`
980
+ `);
981
+ }
982
+ output.write(`${question} [y/n] `);
983
+ const line = (queue.shift() ?? "").trim().toLowerCase();
984
+ return line === "y" || line === "yes";
985
+ };
986
+ }
966
987
 
967
988
  // src/utils/fs-rm.ts
968
- import { existsSync as existsSync2, lstatSync as lstatSync2, readdirSync, rmSync } from "node:fs";
989
+ import { lstatSync as lstatSync2, readdirSync, rmSync } from "node:fs";
969
990
  import { join as join2, resolve as resolve2 } from "node:path";
970
991
  function isInside(target, root) {
971
992
  const t = resolve2(target);
972
993
  const r = resolve2(root);
973
994
  return t === r || t.startsWith(`${r}/`);
974
995
  }
996
+ function lstatOrNull(path) {
997
+ try {
998
+ return lstatSync2(path);
999
+ } catch {
1000
+ return null;
1001
+ }
1002
+ }
975
1003
  function countFiles(path) {
976
- if (!existsSync2(path))
1004
+ const stat = lstatOrNull(path);
1005
+ if (!stat)
977
1006
  return 0;
978
- const stat = lstatSync2(path);
979
1007
  if (stat.isFile())
980
1008
  return 1;
981
1009
  if (!stat.isDirectory())
@@ -986,12 +1014,27 @@ function countFiles(path) {
986
1014
  }
987
1015
  return n;
988
1016
  }
1017
+ function countFoldersAndFiles(dir) {
1018
+ let folders = 0;
1019
+ let files = 0;
1020
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1021
+ if (entry.isDirectory()) {
1022
+ folders++;
1023
+ const nested = countFoldersAndFiles(join2(dir, entry.name));
1024
+ folders += nested.folders;
1025
+ files += nested.files;
1026
+ } else {
1027
+ files++;
1028
+ }
1029
+ }
1030
+ return { folders, files };
1031
+ }
989
1032
  function rmSkillDir(path, opts) {
990
1033
  const safe = opts.allowedRoots.some((root) => isInside(path, root));
991
1034
  if (!safe) {
992
1035
  throw new Error(`Refusing to delete: "${path}" is outside allowed roots`);
993
1036
  }
994
- if (!existsSync2(path))
1037
+ if (!lstatOrNull(path))
995
1038
  return { removed: false, fileCount: 0 };
996
1039
  const fileCount = countFiles(path);
997
1040
  rmSync(path, { recursive: true, force: true });
@@ -999,97 +1042,141 @@ function rmSkillDir(path, opts) {
999
1042
  }
1000
1043
 
1001
1044
  // src/commands/remove.ts
1002
- var q = (name) => `"${cyan(name)}"`;
1045
+ function buildLocation(dir) {
1046
+ const stat = lstatOrNull(dir);
1047
+ if (!stat)
1048
+ return { kind: "missing", subfolders: 0, files: 0 };
1049
+ if (stat.isSymbolicLink())
1050
+ return { kind: "symlink", subfolders: 0, files: 0 };
1051
+ const { folders, files } = countFoldersAndFiles(dir);
1052
+ return { kind: "real", subfolders: folders, files };
1053
+ }
1054
+ function rootFor2(base, isGlobal, lockPath) {
1055
+ return isGlobal ? join3(homedir2(), base, "skills") : join3(dirname2(resolve3(lockPath)), base, "skills");
1056
+ }
1003
1057
  function buildTarget(name, isGlobal, lockPath) {
1004
- const lock = readLock(lockPath);
1005
- const inLock = Object.hasOwn(lock.skills, name);
1006
- const baseClaude = isGlobal ? join3(homedir2(), ".claude", "skills") : join3(dirname2(resolve3(lockPath)), ".claude", "skills");
1007
- const baseAgents = isGlobal ? join3(homedir2(), ".agents", "skills") : join3(dirname2(resolve3(lockPath)), ".agents", "skills");
1008
- const claudeDir = existsSync3(join3(baseClaude, name)) ? join3(baseClaude, name) : undefined;
1009
- const agentsDir = existsSync3(join3(baseAgents, name)) ? join3(baseAgents, name) : undefined;
1010
- return { name, inLock, claudeDir, agentsDir };
1058
+ const agentsDir = join3(rootFor2(".agents", isGlobal, lockPath), name);
1059
+ const claudeDir = join3(rootFor2(".claude", isGlobal, lockPath), name);
1060
+ return {
1061
+ name,
1062
+ agentsDir,
1063
+ claudeDir,
1064
+ agents: buildLocation(agentsDir),
1065
+ claude: buildLocation(claudeDir)
1066
+ };
1011
1067
  }
1012
1068
  function collectAllTargets(isGlobal, lockPath) {
1013
1069
  const map = discoverSkills({ isGlobal, cwd: process.cwd(), lockPath });
1014
1070
  return [...map.keys()].sort().map((name) => buildTarget(name, isGlobal, lockPath));
1015
1071
  }
1016
- function fileCount(dir) {
1017
- const { readdirSync: readdirSync2, statSync } = __require("node:fs");
1018
- let n = 0;
1019
- const stack = [dir];
1020
- while (stack.length) {
1021
- const cur = stack.pop();
1022
- const stat = statSync(cur);
1023
- if (stat.isFile())
1024
- n++;
1025
- else if (stat.isDirectory())
1026
- for (const e of readdirSync2(cur))
1027
- stack.push(join3(cur, e));
1072
+ function plural(n, word) {
1073
+ return `${n} ${word}${n === 1 ? "" : "s"}`;
1074
+ }
1075
+ function header(targets, verb) {
1076
+ const names = targets.map((t) => red(t.name)).join(" ");
1077
+ return `${plural(targets.length, "skill")} ${names} ${verb}`;
1078
+ }
1079
+ function aggregateLocations(infos) {
1080
+ let folders = 0;
1081
+ let subfolders = 0;
1082
+ let files = 0;
1083
+ let symlinks = 0;
1084
+ for (const info of infos) {
1085
+ if (info.kind === "real") {
1086
+ folders += 1;
1087
+ subfolders += info.subfolders;
1088
+ files += info.files;
1089
+ } else if (info.kind === "symlink") {
1090
+ symlinks += 1;
1091
+ }
1028
1092
  }
1029
- return n;
1093
+ return { folders, subfolders, files, symlinks, found: infos.some((i) => i.kind !== "missing") };
1030
1094
  }
1031
- function printPlan(plan, modifyLock, lockOnly) {
1032
- const { target } = plan;
1033
- console.log(`Will remove ${q(target.name)}:`);
1034
- if (target.inLock) {
1035
- if (lockOnly || modifyLock)
1036
- console.log(" - skills-lock.json");
1037
- else
1038
- console.log(" - skills-lock.json (kept; use --force-lock to remove lock entry)");
1039
- } else {
1040
- console.log(" - skills-lock.json (not in lock)");
1041
- }
1042
- if (lockOnly) {
1043
- if (target.claudeDir)
1044
- console.log(` - .claude/skills/${target.name}/ (kept; --lock-only)`);
1045
- else
1046
- console.log(" - .claude/skills/ (not found)");
1047
- if (target.agentsDir)
1048
- console.log(` - .agents/skills/${target.name}/ (kept; --lock-only)`);
1049
- else
1050
- console.log(" - .agents/skills/ (not found)");
1051
- return;
1095
+ function diskCell(infos, variant) {
1096
+ const agg = aggregateLocations(infos);
1097
+ if (!agg.found)
1098
+ return "not found";
1099
+ const parts = [];
1100
+ if (agg.folders > 0 || agg.files > 0) {
1101
+ const text = `${plural(agg.folders, "folder")}, ${plural(agg.subfolders, "subfolder")}, ${plural(agg.files, "file")}`;
1102
+ parts.push(variant === "summary" ? red(text) : green(text));
1103
+ }
1104
+ if (agg.symlinks > 0) {
1105
+ const text = plural(agg.symlinks, "symlink");
1106
+ parts.push(variant === "summary" ? red(text) : yellow(text));
1107
+ }
1108
+ return parts.join(", ");
1109
+ }
1110
+ function lockCell(n, variant) {
1111
+ if (n === 0)
1112
+ return "not in lock";
1113
+ if (variant === "kept")
1114
+ return green(`${plural(n, "line")} kept`);
1115
+ return red(plural(n, "line"));
1116
+ }
1117
+ function printBlock(targets, scope, verb, lockLinesToRemove, diskVariant, lockVariant) {
1118
+ console.log(header(targets, verb));
1119
+ const rows = [];
1120
+ if (scope === "all" || scope === "agents-only") {
1121
+ rows.push([
1122
+ ".agents/skills/",
1123
+ diskCell(targets.map((t) => t.agents), diskVariant)
1124
+ ]);
1125
+ }
1126
+ if (scope === "all" || scope === "claude-only") {
1127
+ rows.push([
1128
+ ".claude/skills/",
1129
+ diskCell(targets.map((t) => t.claude), diskVariant)
1130
+ ]);
1131
+ }
1132
+ if (scope === "all" || scope === "lock-only") {
1133
+ rows.push(["skills-lock.json", lockCell(lockLinesToRemove, lockVariant)]);
1134
+ }
1135
+ const labelWidth = Math.max(...rows.map(([label]) => label.length));
1136
+ for (const [label, cell] of rows) {
1137
+ console.log(`${label.padEnd(labelWidth)} ${cell}`);
1052
1138
  }
1053
- if (target.claudeDir)
1054
- console.log(` - .claude/skills/${target.name}/ (${plan.claudeFileCount} files)`);
1055
- else
1056
- console.log(" - .claude/skills/ (not found)");
1057
- if (target.agentsDir)
1058
- console.log(` - .agents/skills/${target.name}/ (${plan.agentsFileCount} files)`);
1059
- else
1060
- console.log(" - .agents/skills/ (not found)");
1061
1139
  }
1062
1140
  var removeCommand = defineCommand({
1063
1141
  meta: {
1064
- description: "Remove one or more skills from on-disk dirs (lock preserved unless --force-lock)"
1142
+ description: "Remove one or more skills from on-disk dirs and/or skills-lock.json"
1065
1143
  },
1066
1144
  args: {
1067
1145
  global: { type: "boolean", alias: "g", default: false, description: "Use global scope" },
1068
- "dry-run": { type: "boolean", default: false, description: "Print plan, do not delete" },
1069
- yes: { type: "boolean", alias: "y", default: false, description: "Skip confirmation prompt" },
1070
- "force-lock": {
1146
+ yes: { type: "boolean", alias: "y", default: false, description: "Skip confirmation prompts" },
1147
+ "lock-only": {
1071
1148
  type: "boolean",
1149
+ alias: "lo",
1072
1150
  default: false,
1073
- description: "Also remove entry from skills-lock.json (default is to keep lock untouched)"
1151
+ description: "Only remove the skills-lock.json entry; keep on-disk directories"
1074
1152
  },
1075
- "lock-only": {
1153
+ "agents-only": {
1154
+ type: "boolean",
1155
+ alias: "ao",
1156
+ default: false,
1157
+ description: "Only remove from .agents/skills; keep .claude/skills and the lock entry"
1158
+ },
1159
+ "claude-only": {
1076
1160
  type: "boolean",
1161
+ alias: "co",
1077
1162
  default: false,
1078
- description: "Remove only the skills-lock.json entry; keep on-disk directories"
1163
+ description: "Only remove from .claude/skills; keep .agents/skills and the lock entry"
1079
1164
  }
1080
1165
  },
1081
1166
  async run({ args }) {
1082
1167
  const {
1083
1168
  global: isGlobal,
1084
- "dry-run": dryRun,
1085
1169
  yes,
1086
- "force-lock": modifyLock,
1087
- "lock-only": lockOnly
1170
+ "lock-only": lockOnly,
1171
+ "agents-only": agentsOnly,
1172
+ "claude-only": claudeOnly
1088
1173
  } = args;
1089
- if (lockOnly && modifyLock) {
1090
- console.error("--lock-only is mutually exclusive with --force-lock");
1174
+ const onlyFlagCount = [lockOnly, agentsOnly, claudeOnly].filter(Boolean).length;
1175
+ if (onlyFlagCount > 1) {
1176
+ console.error("--lock-only, --agents-only, and --claude-only are mutually exclusive");
1091
1177
  process.exit(1);
1092
1178
  }
1179
+ const scope = lockOnly ? "lock-only" : agentsOnly ? "agents-only" : claudeOnly ? "claude-only" : "all";
1093
1180
  const subcmdIdx = process.argv.findIndex((a) => a === "remove" || a === "rm");
1094
1181
  const rawNames = process.argv.slice(subcmdIdx + 1).filter((a) => !a.startsWith("-"));
1095
1182
  const all = rawNames.includes(".");
@@ -1108,82 +1195,57 @@ var removeCommand = defineCommand({
1108
1195
  console.log("No skills to remove in scope.");
1109
1196
  return;
1110
1197
  }
1111
- const orphan = targets.filter((t) => !t.inLock && !t.claudeDir && !t.agentsDir);
1198
+ const lockNames = new Set(Object.keys(readLock(lockPath).skills));
1199
+ const orphan = targets.filter((t) => t.agents.kind === "missing" && t.claude.kind === "missing" && !lockNames.has(t.name));
1112
1200
  if (orphan.length) {
1113
1201
  for (const o of orphan)
1114
- console.log(`${q(o.name)} is not in lock or on disk`);
1202
+ console.log(`"${o.name}" is not in lock or on disk`);
1115
1203
  process.exit(1);
1116
1204
  }
1117
- const plans = targets.map((t) => ({
1118
- target: t,
1119
- claudeFileCount: t.claudeDir ? fileCount(t.claudeDir) : undefined,
1120
- agentsFileCount: t.agentsDir ? fileCount(t.agentsDir) : undefined
1121
- }));
1122
- for (const p of plans) {
1123
- printPlan(p, modifyLock, lockOnly);
1124
- console.log("");
1125
- }
1126
- if (dryRun)
1127
- return;
1205
+ const lockLinesToRemove = countLockLinesToRemove(lockPath, targets.map((t) => t.name));
1206
+ printBlock(targets, scope, "will be removed from:", lockLinesToRemove, "plan", "removed");
1207
+ const ask = createConfirmer();
1128
1208
  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}?`;
1133
- }
1134
- const ok = await confirm(question);
1209
+ console.log("");
1210
+ const ok = await ask("Proceed?");
1135
1211
  if (!ok) {
1136
1212
  console.log("Aborted");
1137
1213
  process.exit(1);
1138
1214
  }
1139
1215
  }
1140
1216
  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;
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
- }
1217
+ if (scope === "all" || scope === "agents-only") {
1218
+ for (const t of targets)
1219
+ rmSkillDir(t.agentsDir, { allowedRoots });
1220
+ }
1221
+ if (scope === "all" || scope === "claude-only") {
1222
+ for (const t of targets)
1223
+ rmSkillDir(t.claudeDir, { allowedRoots });
1224
+ }
1225
+ let lockCleaned = false;
1226
+ if (scope === "lock-only") {
1227
+ for (const t of targets)
1228
+ removeSkillFromLock(lockPath, t.name);
1229
+ lockCleaned = true;
1230
+ } else if (scope === "all" && lockLinesToRemove > 0) {
1231
+ let cleanLock = yes;
1232
+ if (!yes) {
1233
+ console.log("");
1234
+ cleanLock = await ask(`Clean skills-lock.json (${plural(lockLinesToRemove, "line")})?`);
1169
1235
  }
1170
- if (target.inLock) {
1171
- if (lockOnly || modifyLock) {
1172
- const r = removeSkillFromLock(lockPath, target.name);
1173
- if (r.removed)
1174
- console.log(removed(" from skills-lock.json"));
1175
- } else {
1176
- console.log(kept(" in skills-lock.json"));
1177
- }
1178
- } else {
1179
- console.log(skipped(" skills-lock.json (not in lock)"));
1236
+ if (cleanLock) {
1237
+ for (const t of targets)
1238
+ removeSkillFromLock(lockPath, t.name);
1239
+ lockCleaned = true;
1180
1240
  }
1181
1241
  }
1242
+ console.log("");
1243
+ printBlock(targets, scope, "removed from:", lockLinesToRemove, "summary", lockCleaned ? "removed" : "kept");
1182
1244
  }
1183
1245
  });
1184
1246
 
1185
1247
  // src/commands/usage.ts
1186
- import { existsSync as existsSync6 } from "node:fs";
1248
+ import { existsSync as existsSync4 } from "node:fs";
1187
1249
  import { join as join7 } from "node:path";
1188
1250
 
1189
1251
  // src/readers/claude.ts
@@ -1517,10 +1579,10 @@ function readClaudeUsage(options) {
1517
1579
  }
1518
1580
 
1519
1581
  // src/readers/codex.ts
1520
- import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
1582
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
1521
1583
 
1522
1584
  // src/utils/scope.ts
1523
- import { existsSync as existsSync4 } from "node:fs";
1585
+ import { existsSync as existsSync2 } from "node:fs";
1524
1586
  import { homedir as homedir4 } from "node:os";
1525
1587
  import { dirname as dirname3, join as join6 } from "node:path";
1526
1588
  function detectScope(opts) {
@@ -1542,7 +1604,7 @@ function encodeClaudeProjectDir(absPath) {
1542
1604
  function findGitRoot(start) {
1543
1605
  let dir = start;
1544
1606
  while (true) {
1545
- if (existsSync4(join6(dir, ".git")))
1607
+ if (existsSync2(join6(dir, ".git")))
1546
1608
  return dir;
1547
1609
  const parent = dirname3(dir);
1548
1610
  if (parent === dir)
@@ -1615,7 +1677,7 @@ function readCodexMentions(options) {
1615
1677
  const historyPath = expandHome(options.history ?? "~/.codex/history.jsonl");
1616
1678
  const counts = new Map;
1617
1679
  let linesRead = 0;
1618
- if (!existsSync5(historyPath))
1680
+ if (!existsSync3(historyPath))
1619
1681
  return { counts, filesRead: 0, linesRead: 0 };
1620
1682
  for (const line of readFileSync3(historyPath, "utf8").split(`
1621
1683
  `)) {
@@ -1725,7 +1787,7 @@ async function runUsage(args) {
1725
1787
  });
1726
1788
  const claudeProjectsRoot = expandHome("~/.claude/projects");
1727
1789
  const claudeRoot = args.root ?? (scope.projectRoot ? join7(claudeProjectsRoot, encodeClaudeProjectDir(scope.projectRoot)) : claudeProjectsRoot);
1728
- const claudeRootMissing = !args.root && !!scope.projectRoot && !existsSync6(claudeRoot);
1790
+ const claudeRootMissing = !args.root && !!scope.projectRoot && !existsSync4(claudeRoot);
1729
1791
  const lockPath = getLockPath(args.global);
1730
1792
  const skillUniverse = discoverSkills({
1731
1793
  isGlobal: args.global,
@@ -1969,10 +2031,7 @@ function reorderRootFlagsToSubcommand(argv) {
1969
2031
  return argv;
1970
2032
  return [argv[0] ?? "", argv[1] ?? "", sub, ...before, ...after];
1971
2033
  }
1972
- function normalizeShortFlags(argv) {
1973
- return argv.map((tok) => tok === "-fl" ? "--force-lock" : tok);
1974
- }
1975
- process.argv = reorderRootFlagsToSubcommand(normalizeShortFlags(mergeAgentArgs(process.argv)));
2034
+ process.argv = reorderRootFlagsToSubcommand(mergeAgentArgs(process.argv));
1976
2035
  function printRootHelp() {
1977
2036
  const lines = [
1978
2037
  `Audit and manage AI agent skills (skillio v${version})`,
@@ -1990,7 +2049,7 @@ function printRootHelp() {
1990
2049
  "COMMANDS",
1991
2050
  "",
1992
2051
  " list, ls List skills per source: install type, lock orphans, disk/lock diff",
1993
- " remove, rm Delete on-disk skill dirs; lock kept unless --force-lock",
2052
+ " remove, rm Delete on-disk skill dirs and/or skills-lock.json (interactive)",
1994
2053
  " cost, cs, cst Show ambient ballast cost (per-skill frontmatter tokens) sorted desc",
1995
2054
  " usage, us, usg Show skill usage × cost (consumption) with missed rows",
1996
2055
  " completion Print shell completion script (bash, zsh, fish)"
@@ -2014,32 +2073,30 @@ function isRemoveHelp(argv) {
2014
2073
  }
2015
2074
  function printRemoveHelp() {
2016
2075
  const lines = [
2017
- "Remove skills from on-disk dirs (lock preserved unless --force-lock).",
2076
+ "Remove skills from on-disk dirs and/or skills-lock.json.",
2018
2077
  "",
2019
2078
  "USAGE skillio remove [SKILL...] [OPTIONS]",
2020
2079
  " skillio rm [SKILL...] [OPTIONS]",
2021
2080
  "",
2022
2081
  "ARGUMENTS",
2023
2082
  "",
2024
- ' SKILL... One or more skill names. Use "." to target every skill in scope.',
2083
+ ' SKILL... One or more skill names. Use "." to target every skill in scope.',
2025
2084
  "",
2026
2085
  "OPTIONS",
2027
2086
  "",
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",
2087
+ " -g, --global Use global scope (default: false)",
2088
+ " -y, --yes Skip confirmation prompts",
2089
+ " --lock-only Only remove the skills-lock.json entry (alias --lo)",
2090
+ " --agents-only Only remove from .agents/skills (alias --ao)",
2091
+ " --claude-only Only remove from .claude/skills (alias --co)",
2034
2092
  "",
2035
2093
  "EXAMPLES",
2036
2094
  "",
2037
2095
  " skillio rm brainstorming",
2038
2096
  " skillio rm brainstorming writing-plans --yes",
2039
- " skillio rm . --dry-run",
2040
- " skillio rm . -fl",
2041
- " skillio rm --force-lock obsolete-skill",
2042
- " skillio rm --lock-only stale-entry"
2097
+ " skillio rm .",
2098
+ " skillio rm brainstorming --agents-only",
2099
+ " skillio rm brainstorming --lock-only"
2043
2100
  ];
2044
2101
  console.log(lines.join(`
2045
2102
  `));
@@ -2089,7 +2146,7 @@ var main = defineCommand({
2089
2146
  return;
2090
2147
  const interactive = process.stdout.isTTY && process.stdin.isTTY;
2091
2148
  if (interactive) {
2092
- const { runPicker } = await import("./shared/chunk-vwrhawsv.js");
2149
+ const { runPicker } = await import("./shared/chunk-dwrjph29.js");
2093
2150
  const status = await runPicker({
2094
2151
  global: args.global ?? false
2095
2152
  });
@@ -3,7 +3,7 @@ import {
3
3
  discoverSkills,
4
4
  getLockPath,
5
5
  red
6
- } from "./chunk-0qvp6v8g.js";
6
+ } from "./chunk-nkpp3h85.js";
7
7
 
8
8
  // src/commands/picker.ts
9
9
  import { spawnSync } from "node:child_process";
@@ -219,7 +219,7 @@ async function runPicker(args) {
219
219
  { value: "usage", label: "usage — count of skill invocations" },
220
220
  { value: "cost", label: "cost — per-skill ambient tokens" },
221
221
  { value: "list", label: "list — installed skills per source" },
222
- { value: "remove", label: "remove — delete a skill (disk-only; lock with --force-lock)" },
222
+ { value: "remove", label: "remove — delete a skill (asks about lock cleanup)" },
223
223
  { value: "quit", label: "quit" }
224
224
  ]
225
225
  });
@@ -13,13 +13,28 @@ function readLock(path) {
13
13
  return { skills: {} };
14
14
  return JSON.parse(readFileSync(path, "utf8"));
15
15
  }
16
+ function serialize(lock) {
17
+ return `${JSON.stringify(lock, null, 2)}
18
+ `;
19
+ }
16
20
  function writeLock(path, lock) {
17
21
  mkdirSync(dirname(path), { recursive: true });
18
22
  const tmp = join(dirname(path), `.${Date.now()}.skill-lock.json`);
19
- writeFileSync(tmp, `${JSON.stringify(lock, null, 2)}
20
- `);
23
+ writeFileSync(tmp, serialize(lock));
21
24
  renameSync(tmp, path);
22
25
  }
26
+ function countLockLinesToRemove(path, names) {
27
+ if (!existsSync(path))
28
+ return 0;
29
+ const lock = readLock(path);
30
+ const before = serialize(lock);
31
+ const after = { skills: { ...lock.skills } };
32
+ for (const name of names)
33
+ delete after.skills[name];
34
+ return before.split(`
35
+ `).length - serialize(after).split(`
36
+ `).length;
37
+ }
23
38
  function removeSkillFromLock(path, skill) {
24
39
  if (!existsSync(path))
25
40
  return { removed: false };
@@ -135,4 +150,4 @@ function discoverSkills(input) {
135
150
  return out;
136
151
  }
137
152
 
138
- export { __require, getLockPath, readLock, removeSkillFromLock, setColorEnabled, detectColorSupport, green, yellow, red, cyan, discoverSkills };
153
+ export { __require, getLockPath, readLock, countLockLinesToRemove, removeSkillFromLock, setColorEnabled, detectColorSupport, green, yellow, red, cyan, discoverSkills };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillio",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Audit and manage AI agent skills for Claude Code and Codex",
5
5
  "license": "MIT",
6
6
  "author": "ihororlovskyi",