@victortomaili/skill-cli 0.1.1 → 0.2.0

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/CHANGELOG.md CHANGED
@@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
  - Cursor adapter (`.cursor/rules` format) for `init -g` bootstrap.
12
12
  - Per-agent hook adapters for automatic `/X` triggering (push model).
13
13
 
14
+ ## [0.2.0] - 2026-07-07
15
+
16
+ ### Added
17
+ - `skill remove <name>... [-y]` (aliases `rm`, `uninstall`) — removes skill(s) from the store and cleans up the global enabled list + current project allow-list. Interactive `y/N` confirmation on a TTY (default **N** = keep); non-interactive for agents, CI, and pipes (non-TTY), or when `-y`/`--yes`/`-f`/`--force` is passed. Destination names are canonicalized via the store (case-insensitive; an unknown/`../x` name can't reach the store path). Duplicate and case-variant names are de-duplicated (one removal, accurate count).
18
+
14
19
  ## [0.1.1] - 2026-07-07
15
20
 
16
21
  ### Fixed
@@ -48,6 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
48
53
  - Windows `npx` spawn runs with `shell:false` + an args array and rejects sources
49
54
  containing shell metacharacters (`& | < > ^`).
50
55
 
51
- [Unreleased]: https://github.com/victortomaili/skill-cli/compare/v0.1.1...HEAD
56
+ [Unreleased]: https://github.com/victortomaili/skill-cli/compare/v0.2.0...HEAD
57
+ [0.2.0]: https://github.com/victortomaili/skill-cli/compare/v0.1.1...v0.2.0
52
58
  [0.1.1]: https://github.com/victortomaili/skill-cli/compare/v0.1.0...v0.1.1
53
59
  [0.1.0]: https://github.com/victortomaili/skill-cli/releases/tag/v0.1.0
package/README.md CHANGED
@@ -78,10 +78,11 @@ short bootstrap block telling it to run `skill` on `/X`.
78
78
  | `skill list` | Show installed + active skills (cwd-aware) |
79
79
  | `skill show <name>` | Skill metadata (path, triggers, version) |
80
80
  | `skill cat <name>` | Dump skill content into context |
81
- | `skill trigger <keyword>` | `/X` trigger: single→content, multi→candidates, none→info |
81
+ | `skill trigger <keyword\|name>` | `/X` trigger or skill name content (single), candidates (multi) |
82
82
  | `skill update [name…\|--all]` | Re-fetch from source, update changed skills |
83
+ | `skill remove <name> [-y]` | Remove from store (prompts on TTY; agents / CI / `-y` skip) |
83
84
 
84
- Aliases: `ls` (list), `add` (install), `info` (show).
85
+ Aliases: `ls` (list), `add` (install), `info` (show), `rm`/`uninstall` (remove).
85
86
 
86
87
  ## Install sources
87
88
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@victortomaili/skill-cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Cross-agent skill manager — one global store, clean agent folders, activation via skill.config",
5
5
  "license": "MIT",
6
6
  "author": "Victor Tomaili <victor@tomaili.com>",
package/src/cli.js CHANGED
@@ -9,6 +9,7 @@ import { cmdEnable } from './commands/enable.js'
9
9
  import { cmdDisable } from './commands/disable.js'
10
10
  import { cmdInstall } from './commands/install.js'
11
11
  import { cmdUpdate } from './commands/update.js'
12
+ import { cmdRemove } from './commands/remove.js'
12
13
  import { VERSION } from './lib/version.js'
13
14
 
14
15
  const HELP = `${c.bold('skill')} ${c.gray('v' + VERSION)} — cross-agent skill manager
@@ -33,6 +34,7 @@ ${c.bold('Usage (agent)')}
33
34
 
34
35
  ${c.bold('Maintenance')}
35
36
  ${c.cyan('skill update')} ${c.gray('[name|--all]')} refresh store from source
37
+ ${c.cyan('skill remove')} ${c.gray('<name> [-y]')} remove from store (prompt unless -y / non-TTY)
36
38
 
37
39
  ${c.gray('Source formats (install): owner/repo | github/gitlab URL | git URL | local path | npm package')}
38
40
  ${c.gray('Test (no real ~ touched): SKILL_CLI_HOME=/tmp/sktest skill init -g')}
@@ -58,6 +60,7 @@ try {
58
60
  case 'cat': cmdCat(rest); break
59
61
  case 'trigger': cmdTrigger(rest); break
60
62
  case 'update': cmdUpdate(rest); break
63
+ case 'remove': case 'rm': case 'uninstall': cmdRemove(rest); break
61
64
  case '-v': case '--version':
62
65
  console.log('skill-cli ' + VERSION); break
63
66
  default:
@@ -0,0 +1,113 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import c from 'picocolors'
4
+ import { STORE_DIR } from '../lib/paths.js'
5
+ import { listStore } from '../lib/store.js'
6
+ import { readGlobalConfig, writeGlobalConfig, readProjectConfig, writeProjectConfig } from '../lib/config.js'
7
+ import { pad } from '../lib/format.js'
8
+
9
+ // Read one line from stdin (fd 0) synchronously. Only ever called when stdin is
10
+ // a TTY (or SKILL_CLI_FORCE_TTY is set), so it blocks for the user's keystrokes.
11
+ function readLine() {
12
+ let line = ''
13
+ const buf = Buffer.alloc(1)
14
+ while (true) {
15
+ let n
16
+ try { n = fs.readSync(0, buf, 0, 1, null) } catch { return line }
17
+ if (n === 0) break // EOF
18
+ const ch = buf[0]
19
+ if (ch === 0x0A || ch === 0x0D) break // \n or \r — end of line
20
+ if (ch === 0x03) process.exit(130) // Ctrl-C
21
+ line += String.fromCharCode(ch)
22
+ }
23
+ return line.trim()
24
+ }
25
+
26
+ function confirm(question) {
27
+ process.stdout.write(question)
28
+ const a = readLine().toLowerCase()
29
+ return a === 'y' || a === 'yes'
30
+ }
31
+
32
+ // Are we talking to a human at a terminal? Agents, CI, and pipes have a non-TTY
33
+ // stdin and must NOT be prompted (they would hang waiting for input that never
34
+ // arrives). SKILL_CLI_FORCE_TTY lets tests exercise the prompt branch by piping
35
+ // an answer on a non-TTY stdin.
36
+ function isInteractive() {
37
+ return process.stdin.isTTY === true || process.env.SKILL_CLI_FORCE_TTY === '1'
38
+ }
39
+
40
+ // `skill remove <name>... [-y|--yes|-f|--force]`
41
+ //
42
+ // TTY + no --yes → y/N confirmation prompt (default keeps the skill)
43
+ // non-TTY (agent/CI/pipe) OR --yes → remove without prompting
44
+ //
45
+ // The destination name is canonicalized via listStore (case-insensitive, and an
46
+ // unknown/`../x` name can never reach path.join(STORE_DIR, name)). Also drops the
47
+ // skill from the global enabled list and the current project's allow-list; other
48
+ // projects may keep a now-dangling allow entry, but computeEffective filters
49
+ // those out (harmless).
50
+ export function cmdRemove(args) {
51
+ const assumeYes = args.some(a => ['-y', '--yes', '-f', '--force'].includes(a))
52
+ const requested = args.filter(a => !a.startsWith('-'))
53
+
54
+ if (requested.length === 0) {
55
+ console.error(c.red('Usage: skill remove <name>... [-y]'))
56
+ console.error(c.gray(' Removes skill(s) from the store. Agents/CI auto-skip the prompt, or pass -y.'))
57
+ process.exit(1)
58
+ }
59
+
60
+ const installed = listStore()
61
+ const byLower = new Map(installed.map(s => [s.name.toLowerCase(), s.name]))
62
+ const targets = []
63
+ const seen = new Set()
64
+ for (const a of requested) {
65
+ const canon = byLower.get(a.toLowerCase())
66
+ if (!canon) { console.log(c.yellow(' · ' + pad(a)) + c.yellow('not installed — skipping')); continue }
67
+ // dedupe exact + case-variant duplicates (foo, FOO, foo → a single removal,
68
+ // not three, so the count and prompt list stay accurate)
69
+ const key = canon.toLowerCase()
70
+ if (seen.has(key)) continue
71
+ seen.add(key)
72
+ targets.push(canon)
73
+ }
74
+
75
+ if (targets.length === 0) {
76
+ console.log(c.gray('Nothing to remove.'))
77
+ return
78
+ }
79
+
80
+ if (isInteractive() && !assumeYes) {
81
+ const what = targets.length === 1 ? `skill ${c.bold(targets[0])}` : `${targets.length} skills`
82
+ const list = targets.map(t => ' • ' + t).join('\n')
83
+ if (!confirm(c.yellow(`About to remove ${what}:\n${list}\n`) + c.bold('Remove? [y/N] '))) {
84
+ console.log(c.gray('Aborted. Nothing removed.'))
85
+ return
86
+ }
87
+ }
88
+
89
+ let removed = 0
90
+ for (const name of targets) {
91
+ fs.rmSync(path.join(STORE_DIR, name), { recursive: true, force: true })
92
+ cleanConfig(name)
93
+ console.log(c.green(' ✓ ') + c.bold(pad(name)) + c.gray('removed'))
94
+ removed++
95
+ }
96
+ console.log()
97
+ console.log(c.green(`✓ ${removed} skill(s) removed`))
98
+ }
99
+
100
+ // Drop the skill from the global enabled list and the current project allow-list.
101
+ function cleanConfig(name) {
102
+ const g = readGlobalConfig()
103
+ if ((g.enabled_global || []).some(a => a.toLowerCase() === name.toLowerCase())) {
104
+ g.enabled_global = (g.enabled_global || []).filter(a => a.toLowerCase() !== name.toLowerCase())
105
+ writeGlobalConfig(g)
106
+ }
107
+ const cwd = process.cwd()
108
+ const p = readProjectConfig(cwd)
109
+ if (p && (p.allow || []).some(a => a.toLowerCase() === name.toLowerCase())) {
110
+ p.allow = (p.allow || []).filter(a => a.toLowerCase() !== name.toLowerCase())
111
+ writeProjectConfig(cwd, p)
112
+ }
113
+ }