skillio 0.1.12 → 0.1.13

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.
@@ -0,0 +1,316 @@
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 };
@@ -0,0 +1,86 @@
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillio",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Audit and manage AI agent skills for Claude Code and Codex",
5
5
  "license": "MIT",
6
6
  "author": "ihororlovskyi",
@@ -45,6 +45,7 @@
45
45
  "lint": "biome check src/",
46
46
  "format": "biome format --write src/",
47
47
  "test": "vitest run",
48
+ "test:coverage": "vitest run --coverage",
48
49
  "test:e2e": "vitest run --config vitest.e2e.config.ts",
49
50
  "release": "changeset publish",
50
51
  "prepublishOnly": "biome check src/ && vitest run && ~/.bun/bin/bun run node_modules/.bin/bunup && vitest run --config vitest.e2e.config.ts"
@@ -53,6 +54,7 @@
53
54
  "@biomejs/biome": "^2.4.14",
54
55
  "@changesets/cli": "^2.31.0",
55
56
  "@types/node": "^25.6.2",
57
+ "@vitest/coverage-v8": "^4.1.6",
56
58
  "bunup": "^0.16.31",
57
59
  "citty": "^0.2.2",
58
60
  "typescript": "^6.0.3",
@@ -1,138 +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/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 };