@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 +7 -1
- package/README.md +3 -2
- package/package.json +1 -1
- package/src/cli.js +3 -0
- package/src/commands/remove.js +113 -0
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.
|
|
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
|
|
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.
|
|
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
|
+
}
|