@victortomaili/skill-cli 0.2.0 → 0.4.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 +24 -0
- package/README.md +74 -21
- package/package.json +3 -1
- package/src/cli.js +45 -6
- package/src/commands/defaults.js +73 -0
- package/src/commands/disable.js +4 -4
- package/src/commands/enable.js +3 -3
- package/src/commands/install.js +25 -14
- package/src/commands/list.js +6 -4
- package/src/commands/manager.js +191 -0
- package/src/commands/remove.js +5 -11
- package/src/commands/search.js +92 -0
- package/src/lib/agents-md.js +4 -0
- package/src/lib/config.js +32 -5
- package/src/lib/interactive.js +8 -0
- package/src/lib/npx.js +19 -1
- package/src/lib/paths.js +2 -0
- package/src/lib/skills-find.js +58 -0
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,30 @@ 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.4.0] - 2026-07-07
|
|
15
|
+
|
|
16
|
+
### Changed (breaking — config format)
|
|
17
|
+
- **Unified the "default" and "enabled" concepts into ONE global list.** Previously there were two independent global lists — `enabled_global` (skills active by default in every project) and `defaults_global` (skills auto-loaded on session start, independent of active state). Now there is a single `defaults` list in `config.yaml`: a default skill is BOTH active by default in every project AND auto-loaded on session start. This dissolves the confusion where the manager used a hidden `deny` to turn off a global skill — a default is simply the active-by-default set, and a project refines it with `allow` / `deny`.
|
|
18
|
+
- `skill default <name>` is now always GLOBAL (the `-g` flag is accepted but a no-op). `skill enable -g <name>` and `skill default <name>` now do the same thing; `skill disable -g` / `skill undefault` remove it.
|
|
19
|
+
- The per-project `defaults:` key in `skill.config` is removed — defaults are a global concept only. A project refines the active set with `allow` / `deny` / `inherit` (see Activation).
|
|
20
|
+
- **Automatic migration:** a legacy `config.yaml` with `enabled_global:` and/or `defaults_global:` is folded into `defaults:` (the union of both, deduped) on the next write. No manual steps.
|
|
21
|
+
- AGENTS.md bootstrap block: the "Defaults" line now describes the unified model (active by default + auto-load, global).
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `skill init -g` now bootstraps **pi** too — the AGENTS.md block is injected into `~/.pi/agent/AGENTS.md` (pi's global context file). Claude Code, Codex, Gemini, and pi are now all bootstrapped on a single re-run.
|
|
25
|
+
|
|
26
|
+
## [0.3.0] - 2026-07-07
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- **`skill search` / `skill browse`** — interactive search & multi-install TUI (TTY only). Type a query → browse live `npx skills find` results → mark one or more with `space` → `enter` installs all marked → loops back so you can search again ("research" → pick → "deploy" → pick). `skill install` with no source also opens it on a terminal. Agents/CI (non-TTY) and explicit `skill install <source>` stay fully non-interactive.
|
|
30
|
+
- **`@skill`-pinned sources** (`owner/repo@skill`) now install just that one skill instead of the whole repo (`--skill '*'` is dropped when the source is pinned).
|
|
31
|
+
- **`skill` (no args) / `skill manager` / `skill ui`** — interactive skill-manager TUI (TTY only). Manage every installed skill from the keyboard: ↑↓ navigate · `space` toggle active in the current project · `d` delete (with confirm) · `enter` view the `SKILL.md` · `q` quit. Agents/CI (non-TTY) never enter it; `skill` with no args on a non-TTY still prints help. Toggle follows the allow/deny model (allow wins; a project deny overrides a global enable).
|
|
32
|
+
- **Default skills — auto-load on session start.** New commands: `skill default <name> [-g]` / `skill undefault <name> [-g]` to mark/unmark, and `skill defaults` to list them (this is the command the agent runs on startup). The AGENTS.md bootstrap block now instructs the agent to run `skill defaults` then `skill cat <name>` for each. Defaults are a flag **independent** of enable/disable (a skill can be a default without being active), stored in `skill.config` `defaults:` (project) and `config.yaml` `defaults_global:` (global). Markable from the CLI, or in the manager via the `a` key (★ marker); `skill list` shows ★ too.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- `skill search` and the manager now exit on `ESC` (previously only Enter / Ctrl-C worked — the prebuilt `@inquirer` prompts ignore ESC). ESC aborts the active prompt via its `AbortSignal`; the terminal/raw-mode is restored on abort. The manager also quits on `ESC` from the main list.
|
|
36
|
+
- **225 tests** (unit + CLI), network-free by default.
|
|
37
|
+
|
|
14
38
|
## [0.2.0] - 2026-07-07
|
|
15
39
|
|
|
16
40
|
### Added
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/@victortomaili/skill-cli)
|
|
7
7
|
[](./LICENSE)
|
|
8
8
|
[](https://nodejs.org)
|
|
9
|
-
[](#development)
|
|
10
10
|
|
|
11
11
|
**skill-cli** is a cross-agent skill manager for AI coding agents (Claude Code,
|
|
12
12
|
Codex, Cursor, Gemini, …). Skills live in a single global store — your agent
|
|
@@ -18,8 +18,10 @@ config file.
|
|
|
18
18
|
- 🎛️ **Three-layer activation** — installed → enabled globally → enabled per project. `allow` always wins over `deny`.
|
|
19
19
|
- 🔌 **Universal sources** — `owner/repo`, GitHub/GitLab URL, git URL, local path, npm package (via `npx skills`).
|
|
20
20
|
- ⚡ **Pull-based** — agents pull skill content into context on demand via `skill trigger /X`. No hooks required.
|
|
21
|
+
- 🔍 **Interactive TUIs** — `skill search` to discover & multi-install from the skills registry; `skill` (no args) to manage every installed skill with the keyboard. TTY-only; agents & CI stay non-interactive.
|
|
22
|
+
- ⭐ **Default skills** — one global list: active by default in every project AND auto-loaded on session start.
|
|
21
23
|
- 🖥️ **Cross-platform** — Windows, macOS, Linux (handles the Windows `npx` spawn quirk for you).
|
|
22
|
-
- 🧪 **Well tested** —
|
|
24
|
+
- 🧪 **Well tested** — 225 unit + CLI tests, network-free by default.
|
|
23
25
|
|
|
24
26
|
## ⭐ Found this useful?
|
|
25
27
|
|
|
@@ -40,10 +42,11 @@ Requires Node.js 22+.
|
|
|
40
42
|
# 1) one-time global setup (creates the store + bootstraps detected agents)
|
|
41
43
|
skill init -g
|
|
42
44
|
|
|
43
|
-
# 2) install a skill from any source
|
|
45
|
+
# 2) install a skill from any source (or `skill search` to browse interactively)
|
|
44
46
|
skill install owner/repo
|
|
45
47
|
|
|
46
|
-
# 3)
|
|
48
|
+
# 3) manage everything with the keyboard (terminal only)
|
|
49
|
+
skill # ↑↓ move · space toggle · a default · d delete · enter view
|
|
47
50
|
skill list
|
|
48
51
|
skill cat <name>
|
|
49
52
|
```
|
|
@@ -55,12 +58,13 @@ That's it. Your agent now knows: *when the user types `/X`, run `skill trigger X
|
|
|
55
58
|
```
|
|
56
59
|
~/.skill-cli/
|
|
57
60
|
store/<skill>/SKILL.md ← one canonical store (skills live here)
|
|
58
|
-
config.yaml ← global
|
|
61
|
+
config.yaml ← global defaults (active by default + auto-load)
|
|
59
62
|
|
|
60
|
-
<project>/skill.config ← per-project
|
|
63
|
+
<project>/skill.config ← per-project overrides (inherit / deny / allow)
|
|
61
64
|
~/.claude/CLAUDE.md ← bootstrap block injected by `skill init -g`
|
|
62
65
|
~/.codex/AGENTS.md (idempotent — preserves your existing content)
|
|
63
66
|
~/.gemini/GEMINI.md
|
|
67
|
+
~/.pi/agent/AGENTS.md
|
|
64
68
|
```
|
|
65
69
|
|
|
66
70
|
Agents don't read `skill.config` directly — the CLI does. The agent only needs the
|
|
@@ -73,16 +77,21 @@ short bootstrap block telling it to run `skill` on `/X`.
|
|
|
73
77
|
| `skill init -g` | Global setup: create store + inject bootstrap into agent files |
|
|
74
78
|
| `skill init` | Create a `skill.config` for the current project |
|
|
75
79
|
| `skill install <source>` | Fetch skill(s) to the store (agent dirs untouched) |
|
|
76
|
-
| `skill
|
|
77
|
-
| `skill
|
|
78
|
-
| `skill
|
|
80
|
+
| `skill search` | Interactive search & multi-install from the skills registry (TTY) |
|
|
81
|
+
| `skill` / `skill manager` | Interactive manager: toggle, default, delete, view (TTY) |
|
|
82
|
+
| `skill enable <name> [-g]` | Allow in project, or global default with `-g` |
|
|
83
|
+
| `skill disable <name> [-g]` | Deny in project, or remove global default with `-g` |
|
|
84
|
+
| `skill default <name>` | Mark a default skill (global: active + auto-load) |
|
|
85
|
+
| `skill undefault <name>` | Remove the default flag |
|
|
86
|
+
| `skill defaults` | List default skills (the command your agent runs on start) |
|
|
87
|
+
| `skill list` | Show installed + active skills (cwd-aware, ★ = default) |
|
|
79
88
|
| `skill show <name>` | Skill metadata (path, triggers, version) |
|
|
80
89
|
| `skill cat <name>` | Dump skill content into context |
|
|
81
90
|
| `skill trigger <keyword\|name>` | `/X` trigger or skill name → content (single), candidates (multi) |
|
|
82
91
|
| `skill update [name…\|--all]` | Re-fetch from source, update changed skills |
|
|
83
92
|
| `skill remove <name> [-y]` | Remove from store (prompts on TTY; agents / CI / `-y` skip) |
|
|
84
93
|
|
|
85
|
-
Aliases: `ls` (list), `add` (install), `info` (show), `rm`/`uninstall` (remove).
|
|
94
|
+
Aliases: `ls` (list), `add` (install), `info` (show), `rm`/`uninstall` (remove), `browse` (search), `ui` (manager), `def` (default), `undef` (undefault).
|
|
86
95
|
|
|
87
96
|
## Install sources
|
|
88
97
|
|
|
@@ -121,29 +130,73 @@ for a minimal, copy-paste template.
|
|
|
121
130
|
|
|
122
131
|
## Activation
|
|
123
132
|
|
|
124
|
-
|
|
133
|
+
One global concept, refined per project:
|
|
125
134
|
|
|
126
135
|
1. **installed** — present in the store (passive)
|
|
127
|
-
2. **
|
|
128
|
-
3. **
|
|
136
|
+
2. **default (global)** — listed in `config.yaml` `defaults:`; **active by default in every project** and **auto-loaded on agent session start**. Mark one with `skill default <name>` (or `skill enable -g`).
|
|
137
|
+
3. **per-project override** — a project's `skill.config` refines the set:
|
|
138
|
+
- `allow` — activate an otherwise-passive skill **in this project only**
|
|
139
|
+
- `deny` — turn off a default skill **in this project only**
|
|
140
|
+
- `inherit: false` — ignore global defaults entirely (only `allow` applies)
|
|
129
141
|
|
|
130
|
-
A project `skill.config
|
|
142
|
+
A project with **no `skill.config`** inherits the global defaults (all active). Pure
|
|
143
|
+
allowlist mode in a single project:
|
|
131
144
|
|
|
132
145
|
```yaml
|
|
133
146
|
# <project>/skill.config
|
|
134
|
-
inherit:
|
|
135
|
-
|
|
136
|
-
allow: [react-best-practices] # then open them one by one
|
|
147
|
+
inherit: false # ignore global defaults in this project
|
|
148
|
+
allow: [react-best-practices] # only this skill is active here
|
|
137
149
|
```
|
|
138
150
|
|
|
139
|
-
> `allow` always wins over `deny`, so `
|
|
140
|
-
> Matching is case-insensitive throughout.
|
|
151
|
+
> `allow` always wins over `deny`, so with `inherit: true` you can also write
|
|
152
|
+
> `deny: ["*"]` + `allow: [X]` = "only X here". Matching is case-insensitive throughout.
|
|
153
|
+
|
|
154
|
+
## Interactive mode (terminal only)
|
|
155
|
+
|
|
156
|
+
When run in a real terminal, skill-cli offers two keyboard UIs. **Agents and CI
|
|
157
|
+
never enter them** (a non-TTY stdin is detected) — they use the plain commands.
|
|
158
|
+
|
|
159
|
+
### `skill` — manage installed skills
|
|
160
|
+
|
|
161
|
+
Opens a full-screen manager over your store:
|
|
162
|
+
|
|
163
|
+
| Key | Action |
|
|
164
|
+
|---|---|
|
|
165
|
+
| `↑` / `↓` | move |
|
|
166
|
+
| `space` | toggle active in the current project |
|
|
167
|
+
| `a` | toggle the global default (active + auto-load) |
|
|
168
|
+
| `d` | delete (asks `y/N`) |
|
|
169
|
+
| `enter` | view the `SKILL.md` |
|
|
170
|
+
| `q` / `esc` | quit |
|
|
171
|
+
|
|
172
|
+
### `skill search` — discover & install
|
|
173
|
+
|
|
174
|
+
Type a query → browse live results from the [skills registry](https://skills.sh)
|
|
175
|
+
(`npx skills find`) → mark one or more with `space` → `enter` installs all marked
|
|
176
|
+
→ loops back so you can search again. `esc` or an empty query quits. `skill install`
|
|
177
|
+
with no source also opens it.
|
|
178
|
+
|
|
179
|
+
## Default skills
|
|
180
|
+
|
|
181
|
+
A **default** is a skill that is **active by default in every project** AND
|
|
182
|
+
**auto-loaded on agent session start** — one unified global list in `config.yaml`
|
|
183
|
+
(`defaults:`). Mark one with `skill default <name>`, remove with
|
|
184
|
+
`skill undefault <name>`, list them with `skill defaults`. The AGENTS.md bootstrap
|
|
185
|
+
block tells your agent to run `skill defaults` on start, then `skill cat <name>`
|
|
186
|
+
for each.
|
|
187
|
+
|
|
188
|
+
Defaults are a **global** concept (never per-project). A project can still turn a
|
|
189
|
+
default off locally with `skill disable <name>` (a project `deny`), or activate a
|
|
190
|
+
non-default locally with `skill enable <name>` (a project `allow`) — see
|
|
191
|
+
[Activation](#activation). In the interactive manager, `a` toggles the global
|
|
192
|
+
default; `space` toggles the per-project override.
|
|
141
193
|
|
|
142
194
|
## Bootstrap
|
|
143
195
|
|
|
144
196
|
`skill init -g` injects a short, idempotent block into each detected agent's global
|
|
145
197
|
instruction file (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`):
|
|
146
198
|
|
|
199
|
+
> On session start, run `skill defaults` and load each with `skill cat <name>`.
|
|
147
200
|
> When the user types `/X`, run `skill trigger X`. Single match → apply the output;
|
|
148
201
|
> multiple → show candidates; load each skill only once per session.
|
|
149
202
|
|
|
@@ -151,7 +204,7 @@ It's wrapped in `<!-- BEGIN skill-cli --> … <!-- END skill-cli -->` markers, n
|
|
|
151
204
|
duplicates, and preserves your existing file content. Re-run `init -g` any time —
|
|
152
205
|
it reports `already set up` and rewrites nothing.
|
|
153
206
|
|
|
154
|
-
**Supported agents:** Claude Code, Codex, Gemini. (Cursor uses `.cursor/rules` with
|
|
207
|
+
**Supported agents:** Claude Code, Codex, Gemini, and pi. (Cursor uses `.cursor/rules` with
|
|
155
208
|
a different format — adapter planned. See [issue tracker](https://github.com/victortomaili/skill-cli/issues).)
|
|
156
209
|
|
|
157
210
|
## Cross-platform
|
|
@@ -167,7 +220,7 @@ git clone https://github.com/victortomaili/skill-cli
|
|
|
167
220
|
cd skill-cli
|
|
168
221
|
npm install
|
|
169
222
|
npm link # makes `skill` point at your checkout
|
|
170
|
-
npm test #
|
|
223
|
+
npm test # 225 tests, network-free (~3s)
|
|
171
224
|
npm run test:e2e # opt-in: real npx fetch over the network
|
|
172
225
|
```
|
|
173
226
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@victortomaili/skill-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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>",
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
"test:e2e": "node scripts/e2e.mjs"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
+
"@inquirer/core": "^11.2.1",
|
|
34
|
+
"@inquirer/prompts": "^8.5.2",
|
|
33
35
|
"picocolors": "^1.1.1",
|
|
34
36
|
"yaml": "^2.6.0"
|
|
35
37
|
},
|
package/src/cli.js
CHANGED
|
@@ -10,23 +10,35 @@ import { cmdDisable } from './commands/disable.js'
|
|
|
10
10
|
import { cmdInstall } from './commands/install.js'
|
|
11
11
|
import { cmdUpdate } from './commands/update.js'
|
|
12
12
|
import { cmdRemove } from './commands/remove.js'
|
|
13
|
+
import { cmdSearch } from './commands/search.js'
|
|
14
|
+
import { cmdManager } from './commands/manager.js'
|
|
15
|
+
import { cmdDefaults, cmdDefault, cmdUndefault } from './commands/defaults.js'
|
|
16
|
+
import { isInteractive } from './lib/interactive.js'
|
|
13
17
|
import { VERSION } from './lib/version.js'
|
|
14
18
|
|
|
15
19
|
const HELP = `${c.bold('skill')} ${c.gray('v' + VERSION)} — cross-agent skill manager
|
|
16
20
|
${c.gray('Single global store + activation (skill.config) + bootstrap (AGENTS.md).')}
|
|
17
21
|
|
|
22
|
+
${c.bold('skill')} ${c.gray('interactive manager (↑↓ space d enter — TTY only)')}
|
|
23
|
+
|
|
18
24
|
${c.bold('Setup')}
|
|
19
25
|
${c.cyan('skill init -g')} global setup (store + AGENTS.md injection)
|
|
20
26
|
${c.cyan('skill init')} create skill.config for this project
|
|
21
27
|
|
|
22
28
|
${c.bold('Acquire skills')}
|
|
23
29
|
${c.cyan('skill install')} ${c.gray('<source>')} fetch to store via npx skills (agent dirs untouched)
|
|
30
|
+
${c.cyan('skill search')} interactive search & multi-install (TTY only)
|
|
24
31
|
|
|
25
32
|
${c.bold('Activation')}
|
|
26
|
-
${c.cyan('skill enable')} ${c.gray('<name> [-g]')}
|
|
27
|
-
${c.cyan('skill disable')} ${c.gray('<name> [-g]')}
|
|
33
|
+
${c.cyan('skill enable')} ${c.gray('<name> [-g]')} allow in project, or global default (-g)
|
|
34
|
+
${c.cyan('skill disable')} ${c.gray('<name> [-g]')} deny in project, or remove global default (-g)
|
|
28
35
|
${c.cyan('skill list')} installed + active skills (cwd-aware)
|
|
29
36
|
|
|
37
|
+
${c.bold('Defaults (active + auto-load)')}
|
|
38
|
+
${c.cyan('skill default')} ${c.gray('<name>')} mark a default skill (global: active in every project)
|
|
39
|
+
${c.cyan('skill undefault')} ${c.gray('<name>')} remove the default flag
|
|
40
|
+
${c.cyan('skill defaults')} list default skills (agent runs this on start)
|
|
41
|
+
|
|
30
42
|
${c.bold('Usage (agent)')}
|
|
31
43
|
${c.cyan('skill show')} ${c.gray('<name>')} metadata + path + triggers
|
|
32
44
|
${c.cyan('skill cat')} ${c.gray('<name>')} dump content to context
|
|
@@ -46,16 +58,41 @@ ${c.bold('★ Enjoying skill-cli?')} ${c.gray('Star the repo — it helps others
|
|
|
46
58
|
|
|
47
59
|
const [, , cmd, ...rest] = process.argv
|
|
48
60
|
|
|
49
|
-
|
|
61
|
+
async function main() {
|
|
50
62
|
switch (cmd) {
|
|
63
|
+
case 'manager': case 'ui':
|
|
64
|
+
if (!isInteractive()) {
|
|
65
|
+
console.error(c.red("'skill manager' is interactive — run it in a terminal, or use 'skill list'."))
|
|
66
|
+
process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
await cmdManager()
|
|
69
|
+
break
|
|
51
70
|
case undefined:
|
|
71
|
+
if (isInteractive()) { await cmdManager() }
|
|
72
|
+
else { console.log(HELP) }
|
|
73
|
+
break
|
|
52
74
|
case '-h': case '--help': case 'help':
|
|
53
75
|
console.log(HELP); break
|
|
54
76
|
case 'init': cmdInit(rest); break
|
|
55
|
-
case 'install': case 'add':
|
|
77
|
+
case 'install': case 'add':
|
|
78
|
+
// No source + a real terminal → interactive search TUI. Agents/CI (non-TTY)
|
|
79
|
+
// and explicit sources stay non-interactive.
|
|
80
|
+
if (rest.length === 0 && isInteractive()) { await cmdSearch(rest) }
|
|
81
|
+
else { cmdInstall(rest) }
|
|
82
|
+
break
|
|
83
|
+
case 'search': case 'browse':
|
|
84
|
+
if (!isInteractive()) {
|
|
85
|
+
console.error(c.red("'skill search' is interactive — run it in a terminal, or use 'skill install <source>'."))
|
|
86
|
+
process.exit(1)
|
|
87
|
+
}
|
|
88
|
+
await cmdSearch(rest)
|
|
89
|
+
break
|
|
56
90
|
case 'enable': cmdEnable(rest); break
|
|
57
91
|
case 'disable': cmdDisable(rest); break
|
|
58
92
|
case 'list': case 'ls': cmdList(rest); break
|
|
93
|
+
case 'defaults': cmdDefaults(rest); break
|
|
94
|
+
case 'default': case 'def': cmdDefault(rest); break
|
|
95
|
+
case 'undefault': case 'undef': cmdUndefault(rest); break
|
|
59
96
|
case 'show': case 'info': cmdShow(rest); break
|
|
60
97
|
case 'cat': cmdCat(rest); break
|
|
61
98
|
case 'trigger': cmdTrigger(rest); break
|
|
@@ -68,7 +105,9 @@ try {
|
|
|
68
105
|
console.error(c.gray(' skill --help'))
|
|
69
106
|
process.exit(1)
|
|
70
107
|
}
|
|
71
|
-
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main().catch(e => {
|
|
72
111
|
console.error(c.red('Error: ') + e.message)
|
|
73
112
|
process.exit(1)
|
|
74
|
-
}
|
|
113
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import c from 'picocolors'
|
|
2
|
+
import { listStore } from '../lib/store.js'
|
|
3
|
+
import { readGlobalConfig, writeGlobalConfig, computeDefaults } from '../lib/config.js'
|
|
4
|
+
import { trunc } from '../lib/format.js'
|
|
5
|
+
|
|
6
|
+
// `skill defaults` — list the skills marked as defaults. In the unified model a
|
|
7
|
+
// default skill is BOTH active-by-default in every project AND auto-loaded on
|
|
8
|
+
// agent session start (one global `defaults` list). This is the agent-facing
|
|
9
|
+
// command the AGENTS.md block tells the agent to run, then `skill cat <name>`
|
|
10
|
+
// for each. Defaults are a GLOBAL concept (never per-folder).
|
|
11
|
+
export function cmdDefaults() {
|
|
12
|
+
const installed = listStore()
|
|
13
|
+
const globalCfg = readGlobalConfig()
|
|
14
|
+
const eff = computeDefaults(installed, globalCfg)
|
|
15
|
+
|
|
16
|
+
console.log(c.bold('skill defaults') + c.gray(' — active by default + auto-load on session start (global)'))
|
|
17
|
+
console.log()
|
|
18
|
+
|
|
19
|
+
if (eff.length === 0) {
|
|
20
|
+
console.log(c.gray(' No default skills yet. Mark with: ') + c.cyan('skill default <name>'))
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const name of eff) {
|
|
25
|
+
const s = installed.find(x => x.name === name)
|
|
26
|
+
const trg = s && s.triggers.length ? ' ' + c.gray('/' + s.triggers.join(', /')) : ''
|
|
27
|
+
console.log(` ${c.yellow('★')} ${c.bold(name.padEnd(22))}${trg}`)
|
|
28
|
+
if (s && s.description) console.log(c.gray(' ' + trunc(s.description, 66)))
|
|
29
|
+
}
|
|
30
|
+
console.log()
|
|
31
|
+
console.log(c.gray('Load each into context: ') + c.cyan('skill cat <name>'))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// `skill default <name>` — mark a skill as a default (active by default in every
|
|
35
|
+
// project + auto-loaded on session start). Always GLOBAL. Requires the skill to
|
|
36
|
+
// be installed (a typo would silently "succeed" but never resolve). `-g`/`--global`
|
|
37
|
+
// is accepted for compatibility but is a no-op — defaults are inherently global.
|
|
38
|
+
export function cmdDefault(args) {
|
|
39
|
+
const name = args.find(a => !a.startsWith('-'))
|
|
40
|
+
if (!name) {
|
|
41
|
+
console.error(c.red('Usage: skill default <name>'))
|
|
42
|
+
console.error(c.gray(' Marks a skill as a default: active by default in every project + auto-load on session start.'))
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
if (!listStore().some(s => s.name.toLowerCase() === name.toLowerCase())) {
|
|
46
|
+
console.error(c.red('Not installed: ' + name))
|
|
47
|
+
console.error(c.gray(' Install first: skill install <source>'))
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
50
|
+
const cfg = readGlobalConfig()
|
|
51
|
+
if (!(cfg.defaults || []).some(d => d.toLowerCase() === name.toLowerCase())) {
|
|
52
|
+
cfg.defaults.push(name.toLowerCase())
|
|
53
|
+
cfg.defaults.sort()
|
|
54
|
+
}
|
|
55
|
+
writeGlobalConfig(cfg)
|
|
56
|
+
console.log(c.green('✓') + ' default (global): ' + c.bold(name) + c.gray(' (active + auto-load in every project)'))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// `skill undefault <name>` — remove the default flag (always global).
|
|
60
|
+
export function cmdUndefault(args) {
|
|
61
|
+
const name = args.find(a => !a.startsWith('-'))
|
|
62
|
+
if (!name) {
|
|
63
|
+
console.error(c.red('Usage: skill undefault <name>'))
|
|
64
|
+
process.exit(1)
|
|
65
|
+
}
|
|
66
|
+
const cfg = readGlobalConfig()
|
|
67
|
+
const had = (cfg.defaults || []).some(d => d.toLowerCase() === name.toLowerCase())
|
|
68
|
+
cfg.defaults = (cfg.defaults || []).filter(d => d.toLowerCase() !== name.toLowerCase())
|
|
69
|
+
writeGlobalConfig(cfg)
|
|
70
|
+
console.log(had
|
|
71
|
+
? (c.green('✓') + ' removed default: ' + c.bold(name))
|
|
72
|
+
: (c.gray('·') + ' not a default: ' + c.bold(name) + c.gray(' (nothing to do)')))
|
|
73
|
+
}
|
package/src/commands/disable.js
CHANGED
|
@@ -8,12 +8,12 @@ export function cmdDisable(args) {
|
|
|
8
8
|
|
|
9
9
|
if (global) {
|
|
10
10
|
const cfg = readGlobalConfig()
|
|
11
|
-
const had = (cfg.
|
|
12
|
-
cfg.
|
|
11
|
+
const had = (cfg.defaults || []).some(a => a.toLowerCase() === name.toLowerCase())
|
|
12
|
+
cfg.defaults = (cfg.defaults || []).filter(a => a.toLowerCase() !== name.toLowerCase())
|
|
13
13
|
writeGlobalConfig(cfg)
|
|
14
14
|
console.log(had
|
|
15
|
-
? (c.green('✓') + '
|
|
16
|
-
: (c.gray('·') + ' not
|
|
15
|
+
? (c.green('✓') + ' removed global default: ' + c.bold(name))
|
|
16
|
+
: (c.gray('·') + ' not a global default: ' + c.bold(name) + c.gray(' (nothing to do)')))
|
|
17
17
|
} else {
|
|
18
18
|
const cwd = process.cwd()
|
|
19
19
|
const cfg = readProjectConfig(cwd) || { inherit: true, deny: [], allow: [] }
|
package/src/commands/enable.js
CHANGED
|
@@ -16,11 +16,11 @@ export function cmdEnable(args) {
|
|
|
16
16
|
|
|
17
17
|
if (global) {
|
|
18
18
|
const cfg = readGlobalConfig()
|
|
19
|
-
if (!cfg.
|
|
20
|
-
cfg.
|
|
19
|
+
if (!(cfg.defaults || []).some(a => a.toLowerCase() === name.toLowerCase())) {
|
|
20
|
+
cfg.defaults.push(name.toLowerCase()); cfg.defaults.sort()
|
|
21
21
|
}
|
|
22
22
|
writeGlobalConfig(cfg)
|
|
23
|
-
console.log(c.green('✓') + ' enabled globally: ' + c.bold(name) + c.gray(' (active in all projects)'))
|
|
23
|
+
console.log(c.green('✓') + ' enabled globally (default): ' + c.bold(name) + c.gray(' (active + auto-load in all projects)'))
|
|
24
24
|
} else {
|
|
25
25
|
const cwd = process.cwd()
|
|
26
26
|
const cfg = readProjectConfig(cwd) || { inherit: true, deny: [], allow: [] }
|
package/src/commands/install.js
CHANGED
|
@@ -14,17 +14,10 @@ function resolveSource(source) {
|
|
|
14
14
|
return source
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
// Fetch skill(s) from
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
export function
|
|
21
|
-
const source = args[0]
|
|
22
|
-
if (!source) {
|
|
23
|
-
console.error(c.red('Usage: skill install <source>'))
|
|
24
|
-
console.error(c.gray(' source: owner/repo | github/gitlab URL | git URL | local path | npm package'))
|
|
25
|
-
process.exit(1)
|
|
26
|
-
}
|
|
27
|
-
|
|
17
|
+
// Fetch skill(s) from `source` into the store. Throws on failure (does NOT
|
|
18
|
+
// process.exit) so callers — the `install` command AND the `search` TUI loop —
|
|
19
|
+
// can decide how to handle errors. Returns the list of moved skill names.
|
|
20
|
+
export function installSource(source) {
|
|
28
21
|
const resolved = resolveSource(source)
|
|
29
22
|
console.log(c.cyan('Fetching: ') + resolved)
|
|
30
23
|
console.log(c.gray(' via npx skills add (temp cwd; agent folders untouched)'))
|
|
@@ -33,9 +26,7 @@ export function cmdInstall(args) {
|
|
|
33
26
|
try {
|
|
34
27
|
({ tmp, fetchedDir } = fetchSkillsToTemp(resolved))
|
|
35
28
|
} catch (e) {
|
|
36
|
-
|
|
37
|
-
console.error(c.gray('Check the source (owner/repo, URL, path, npm package).'))
|
|
38
|
-
process.exit(1)
|
|
29
|
+
throw new Error(e.message)
|
|
39
30
|
}
|
|
40
31
|
|
|
41
32
|
try {
|
|
@@ -84,7 +75,27 @@ export function cmdInstall(args) {
|
|
|
84
75
|
}
|
|
85
76
|
console.log()
|
|
86
77
|
console.log(c.gray('Skills are passive until enabled. Activate with: ') + c.cyan('skill enable <name>') + c.gray(' or ') + c.cyan('-g'))
|
|
78
|
+
return moved
|
|
87
79
|
} finally {
|
|
88
80
|
if (tmp) fs.rmSync(tmp, { recursive: true, force: true })
|
|
89
81
|
}
|
|
90
82
|
}
|
|
83
|
+
|
|
84
|
+
// `skill install <source>` — non-interactive. The TTY/no-args route to the search
|
|
85
|
+
// TUI is handled in cli.js (avoids a circular import with search.js).
|
|
86
|
+
export function cmdInstall(args) {
|
|
87
|
+
const source = args[0]
|
|
88
|
+
if (!source) {
|
|
89
|
+
console.error(c.red('Usage: skill install <source>'))
|
|
90
|
+
console.error(c.gray(' source: owner/repo | owner/repo@skill | github/gitlab URL | git URL | local path | npm package'))
|
|
91
|
+
console.error(c.gray(' (in a terminal with no source → interactive search: skill search)'))
|
|
92
|
+
process.exit(1)
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
installSource(source)
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.error(c.red(e.message))
|
|
98
|
+
console.error(c.gray('Check the source (owner/repo, URL, path, npm package).'))
|
|
99
|
+
process.exit(1)
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/commands/list.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import c from 'picocolors'
|
|
2
2
|
import { listStore } from '../lib/store.js'
|
|
3
|
-
import { readGlobalConfig, readProjectConfig, computeEffective } from '../lib/config.js'
|
|
3
|
+
import { readGlobalConfig, readProjectConfig, computeEffective, computeDefaults } from '../lib/config.js'
|
|
4
4
|
import { trunc } from '../lib/format.js'
|
|
5
5
|
|
|
6
6
|
export function cmdList(_args = []) {
|
|
@@ -8,6 +8,7 @@ export function cmdList(_args = []) {
|
|
|
8
8
|
const globalCfg = readGlobalConfig()
|
|
9
9
|
const projCfg = readProjectConfig()
|
|
10
10
|
const effective = computeEffective(installed, globalCfg, projCfg)
|
|
11
|
+
const defaults = computeDefaults(installed, globalCfg)
|
|
11
12
|
|
|
12
13
|
const scope = projCfg ? c.magenta(process.cwd()) : c.gray('(global)')
|
|
13
14
|
console.log(c.bold('skill list') + c.gray(' — project: ') + scope)
|
|
@@ -21,18 +22,19 @@ export function cmdList(_args = []) {
|
|
|
21
22
|
for (const s of installed) {
|
|
22
23
|
const active = effective.includes(s.name)
|
|
23
24
|
const mark = active ? c.green('●') : c.gray('○')
|
|
25
|
+
const star = defaults.includes(s.name) ? c.yellow('★') : c.gray('·')
|
|
24
26
|
const sc = labelScope(s.name, globalCfg, projCfg)
|
|
25
27
|
const trg = s.triggers.length ? ' ' + c.gray('/' + s.triggers.join(', /')) : ''
|
|
26
|
-
console.log(` ${mark} ${c.bold(s.name.padEnd(22))} ${c.gray(String(s.version).padEnd(8))} ${sc}${trg}`)
|
|
28
|
+
console.log(` ${mark} ${star} ${c.bold(s.name.padEnd(22))} ${c.gray(String(s.version).padEnd(8))} ${sc}${trg}`)
|
|
27
29
|
if (s.description) console.log(c.gray(' ' + trunc(s.description, 68)))
|
|
28
30
|
}
|
|
29
31
|
console.log()
|
|
30
|
-
console.log(c.green('● active ') + c.gray('○ passive (
|
|
32
|
+
console.log(c.green('● active ') + c.gray('○ passive ') + c.yellow('★ default (auto-load on session start)'))
|
|
31
33
|
console.log(c.cyan('Details: ') + 'skill show <name> ' + c.cyan('Load: ') + 'skill cat <name>')
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
function labelScope(name, globalCfg, projCfg) {
|
|
35
37
|
if (projCfg && (projCfg.allow || []).some(a => a.toLowerCase() === name.toLowerCase())) return c.magenta('project')
|
|
36
|
-
if ((globalCfg.
|
|
38
|
+
if ((globalCfg.defaults || []).some(a => a.toLowerCase() === name.toLowerCase())) return c.blue('global ')
|
|
37
39
|
return c.gray('- ')
|
|
38
40
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { createPrompt, useState, useKeypress, usePrefix, isEnterKey, isUpKey, isDownKey } from '@inquirer/core'
|
|
4
|
+
import c from 'picocolors'
|
|
5
|
+
import { STORE_DIR } from '../lib/paths.js'
|
|
6
|
+
import { listStore } from '../lib/store.js'
|
|
7
|
+
import { readGlobalConfig, writeGlobalConfig, readProjectConfig, writeProjectConfig, computeEffective, computeDefaults } from '../lib/config.js'
|
|
8
|
+
import { cleanConfig } from './remove.js'
|
|
9
|
+
import { trunc } from '../lib/format.js'
|
|
10
|
+
|
|
11
|
+
const KEYS = c.gray('↑↓ move · space toggle · a default · d delete · enter view · q/esc quit')
|
|
12
|
+
|
|
13
|
+
// where a skill's activation currently comes from (mirrors `skill list`)
|
|
14
|
+
function labelScope(name, g, p) {
|
|
15
|
+
if (p && (p.allow || []).some(a => a.toLowerCase() === name.toLowerCase())) return c.magenta('project')
|
|
16
|
+
if ((g.defaults || []).some(a => a.toLowerCase() === name.toLowerCase())) return c.blue('global ')
|
|
17
|
+
return c.gray('- ')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Pure decision: given the current configs, compute the new {allow, deny} arrays
|
|
21
|
+
// that flip `name`'s active state in THIS project. No I/O → unit-testable without
|
|
22
|
+
// SKILL_CLI_HOME. The global `defaults` list is untouched (the manager toggle is project-scoped).
|
|
23
|
+
// passive → add to project allow (active here)
|
|
24
|
+
// active via project allow → remove from allow
|
|
25
|
+
// active via global only → add a local deny so it goes passive HERE
|
|
26
|
+
export function computeToggle(installed, globalCfg, projCfg, name) {
|
|
27
|
+
const lc = String(name).toLowerCase()
|
|
28
|
+
const p = {
|
|
29
|
+
inherit: projCfg?.inherit !== false,
|
|
30
|
+
deny: [...(projCfg?.deny || [])],
|
|
31
|
+
allow: [...(projCfg?.allow || [])],
|
|
32
|
+
}
|
|
33
|
+
const active = computeEffective(installed, globalCfg, p).includes(name)
|
|
34
|
+
const isGlobal = (globalCfg.defaults || []).some(a => String(a).toLowerCase() === lc)
|
|
35
|
+
const isProjectAllow = p.allow.some(a => String(a).toLowerCase() === lc)
|
|
36
|
+
if (active) {
|
|
37
|
+
p.allow = p.allow.filter(a => String(a).toLowerCase() !== lc)
|
|
38
|
+
if (isGlobal && !isProjectAllow) {
|
|
39
|
+
if (!p.deny.some(d => String(d).toLowerCase() === lc)) p.deny.push(lc)
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
p.deny = p.deny.filter(d => String(d).toLowerCase() !== lc)
|
|
43
|
+
if (!p.allow.some(a => String(a).toLowerCase() === lc)) p.allow.push(lc)
|
|
44
|
+
}
|
|
45
|
+
return { allow: p.allow, deny: p.deny }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Apply computeToggle's decision to disk and return a status line for the UI.
|
|
49
|
+
export function toggleActive(name, cwd = process.cwd()) {
|
|
50
|
+
const installed = listStore()
|
|
51
|
+
const g = readGlobalConfig()
|
|
52
|
+
const p = readProjectConfig(cwd) || { inherit: true, deny: [], allow: [] }
|
|
53
|
+
const wasActive = computeEffective(installed, g, p).includes(name)
|
|
54
|
+
const next = computeToggle(installed, g, p, name)
|
|
55
|
+
writeProjectConfig(cwd, { ...p, ...next })
|
|
56
|
+
return wasActive ? (c.gray('· disabled in project: ') + name) : (c.green('✓ enabled in project: ') + name)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function removeOne(name) {
|
|
60
|
+
fs.rmSync(path.join(STORE_DIR, name), { recursive: true, force: true })
|
|
61
|
+
cleanConfig(name)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Bounded view of SKILL.md — the full file can be large and inquirer does not scroll.
|
|
65
|
+
function viewBody(s) {
|
|
66
|
+
const file = path.join(STORE_DIR, s.name, 'SKILL.md')
|
|
67
|
+
let content
|
|
68
|
+
try { content = fs.readFileSync(file, 'utf8') } catch { return c.gray(' (no SKILL.md)') }
|
|
69
|
+
const lines = content.split(/\r?\n/)
|
|
70
|
+
const max = 22
|
|
71
|
+
const shown = lines.slice(0, max).map(l => (l ? ' ' + l : ''))
|
|
72
|
+
if (lines.length > max) shown.push(c.gray(' … ' + (lines.length - max) + ' more lines — `skill cat ' + s.name + '`'))
|
|
73
|
+
return shown.join('\n')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Pure decision: the new global `defaults` array after toggling `name`'s default
|
|
77
|
+
// flag. (Defaults are a GLOBAL concept — active by default + auto-load. The
|
|
78
|
+
// manager `a` key edits this list; `space` does the per-folder allow/deny.)
|
|
79
|
+
export function computeToggleDefault(list, name) {
|
|
80
|
+
const lc = String(name).toLowerCase()
|
|
81
|
+
const defs = [...(list || [])]
|
|
82
|
+
return defs.some(d => String(d).toLowerCase() === lc)
|
|
83
|
+
? defs.filter(d => String(d).toLowerCase() !== lc)
|
|
84
|
+
: [...defs, lc]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Apply the default toggle to the GLOBAL config and return a status line for the UI.
|
|
88
|
+
function toggleDefault(name) {
|
|
89
|
+
const installed = listStore()
|
|
90
|
+
const g = readGlobalConfig()
|
|
91
|
+
const wasDefault = computeDefaults(installed, g).includes(name)
|
|
92
|
+
g.defaults = computeToggleDefault(g.defaults, name)
|
|
93
|
+
writeGlobalConfig(g)
|
|
94
|
+
return wasDefault ? (c.gray('· un-defaulted: ') + name) : (c.yellow('★ default: ') + name + c.gray(' (active + auto-load)'))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Clamp a cursor move so it can't leave the list bounds (pure → unit-tested).
|
|
98
|
+
export function moveCursor(cur, length, delta) {
|
|
99
|
+
if (length <= 0) return 0
|
|
100
|
+
return Math.max(0, Math.min(length - 1, cur + delta))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// One custom prompt owns the whole keyboard loop (select/checkbox can't do `d`/view).
|
|
104
|
+
// ↑↓ move · space toggle active in this project · d delete (confirm) ·
|
|
105
|
+
// enter view the SKILL.md · q quit. Re-reads the store/config every render so
|
|
106
|
+
// toggles & deletes are reflected immediately.
|
|
107
|
+
const managerPrompt = createPrompt((_config, done) => {
|
|
108
|
+
const [cursor, setCursor] = useState(0)
|
|
109
|
+
const [mode, setMode] = useState('list') // 'list' | 'confirm' | 'view'
|
|
110
|
+
const [tick, setTick] = useState(0) // bump (a VALUE, not a fn) to force a re-render
|
|
111
|
+
const [status, setStatus] = useState('')
|
|
112
|
+
const prefix = usePrefix({ theme: { prefix: { idle: c.cyan('?'), done: c.cyan('✓') } } })
|
|
113
|
+
|
|
114
|
+
const installed = listStore()
|
|
115
|
+
const g = readGlobalConfig()
|
|
116
|
+
const p = readProjectConfig()
|
|
117
|
+
const eff = computeEffective(installed, g, p)
|
|
118
|
+
const defs = computeDefaults(installed, g)
|
|
119
|
+
const cur = installed.length ? Math.max(0, Math.min(cursor, installed.length - 1)) : 0
|
|
120
|
+
const s = installed[cur]
|
|
121
|
+
|
|
122
|
+
useKeypress((key) => {
|
|
123
|
+
if (mode === 'view') { setMode('list'); return } // any key → back to list
|
|
124
|
+
if (mode === 'confirm') {
|
|
125
|
+
if (key.name === 'y') {
|
|
126
|
+
if (s) { removeOne(s.name); setStatus(c.red('✓ removed ') + s.name) }
|
|
127
|
+
setMode('list'); setTick(tick + 1)
|
|
128
|
+
} else if (key.name === 'escape' || key.name === 'n' || (key.ctrl && key.name === 'c')) {
|
|
129
|
+
setMode('list'); setStatus(c.gray('cancelled'))
|
|
130
|
+
}
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
// list mode
|
|
134
|
+
// NB: @inquirer/core's useState setter takes a VALUE (there is NO functional
|
|
135
|
+
// updater like React's). We read the clamped `cur` from THIS render's closure —
|
|
136
|
+
// useKeypress keeps the handler fresh on every render, so it's always current.
|
|
137
|
+
if (isUpKey(key)) { setCursor(moveCursor(cur, installed.length, -1)); setStatus('') }
|
|
138
|
+
else if (isDownKey(key)) { setCursor(moveCursor(cur, installed.length, 1)); setStatus('') }
|
|
139
|
+
else if (key.name === 'space') { if (s) { setStatus(toggleActive(s.name)); setTick(tick + 1) } }
|
|
140
|
+
else if (key.name === 'a') { if (s) { setStatus(toggleDefault(s.name)); setTick(tick + 1) } }
|
|
141
|
+
else if (key.name === 'd') { if (s) { setMode('confirm'); setStatus('') } }
|
|
142
|
+
else if (isEnterKey(key)) { if (s) { setMode('view'); setStatus('') } }
|
|
143
|
+
else if (key.name === 'q' || key.name === 'escape' || (key.ctrl && key.name === 'c')) done()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// ── render ──
|
|
147
|
+
if (mode === 'view' && s) {
|
|
148
|
+
return prefix + ' ' + c.bold(s.name) + c.gray(' (any key to go back)') + '\n' + viewBody(s)
|
|
149
|
+
}
|
|
150
|
+
const lines = [prefix + ' ' + c.bold('skill manager') + c.gray(' — ' + installed.length + ' installed · ' + eff.length + ' active')]
|
|
151
|
+
for (let i = 0; i < installed.length; i++) {
|
|
152
|
+
const sk = installed[i]
|
|
153
|
+
const active = eff.includes(sk.name)
|
|
154
|
+
const arrow = i === cur ? c.cyan('❯') : ' '
|
|
155
|
+
const mark = active ? c.green('●') : c.gray('○')
|
|
156
|
+
const star = defs.includes(sk.name) ? c.yellow('★') : c.gray('·')
|
|
157
|
+
const sc = labelScope(sk.name, g, p)
|
|
158
|
+
const trg = sk.triggers.length ? ' ' + c.gray('/' + sk.triggers.join(', /')) : ''
|
|
159
|
+
const nameStr = sk.name.padEnd(24)
|
|
160
|
+
const nameCol = i === cur ? c.bold(nameStr) : nameStr
|
|
161
|
+
lines.push(`${arrow} ${mark} ${star} ${nameCol} ${c.gray(String(sk.version || '').padEnd(8))} ${sc}${trg}`)
|
|
162
|
+
if (i === cur && sk.description) lines.push(c.gray(' ' + trunc(sk.description, 62)))
|
|
163
|
+
}
|
|
164
|
+
lines.push(c.gray(' ' + '─'.repeat(58)))
|
|
165
|
+
lines.push(' ' + KEYS)
|
|
166
|
+
if (mode === 'confirm') {
|
|
167
|
+
lines.push('')
|
|
168
|
+
lines.push(' ' + c.yellow('Delete ' + c.bold(s ? s.name : '') + '? [y/N] ') + c.gray('(N keeps it)'))
|
|
169
|
+
} else if (status) {
|
|
170
|
+
lines.push(' ' + status)
|
|
171
|
+
}
|
|
172
|
+
return lines.join('\n')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// `skill` (no args) / `skill manager` / `skill ui` on a terminal. Agents/CI never
|
|
176
|
+
// reach here (cli.js gates on isInteractive).
|
|
177
|
+
export async function cmdManager() {
|
|
178
|
+
const installed = listStore()
|
|
179
|
+
if (installed.length === 0) {
|
|
180
|
+
console.log(c.yellow('Store empty.') + c.gray(' Install a skill first: ') + c.cyan('skill install <source>'))
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
await managerPrompt({})
|
|
185
|
+
} catch { /* Ctrl-C / abort — fall through to the footer */ }
|
|
186
|
+
const g = readGlobalConfig()
|
|
187
|
+
const p = readProjectConfig()
|
|
188
|
+
const eff = computeEffective(listStore(), g, p)
|
|
189
|
+
console.log()
|
|
190
|
+
console.log(c.gray('Closed. ' + listStore().length + ' installed · ' + eff.length + ' active in ' + process.cwd()))
|
|
191
|
+
}
|
package/src/commands/remove.js
CHANGED
|
@@ -5,6 +5,7 @@ import { STORE_DIR } from '../lib/paths.js'
|
|
|
5
5
|
import { listStore } from '../lib/store.js'
|
|
6
6
|
import { readGlobalConfig, writeGlobalConfig, readProjectConfig, writeProjectConfig } from '../lib/config.js'
|
|
7
7
|
import { pad } from '../lib/format.js'
|
|
8
|
+
import { isInteractive } from '../lib/interactive.js'
|
|
8
9
|
|
|
9
10
|
// Read one line from stdin (fd 0) synchronously. Only ever called when stdin is
|
|
10
11
|
// a TTY (or SKILL_CLI_FORCE_TTY is set), so it blocks for the user's keystrokes.
|
|
@@ -29,14 +30,6 @@ function confirm(question) {
|
|
|
29
30
|
return a === 'y' || a === 'yes'
|
|
30
31
|
}
|
|
31
32
|
|
|
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
33
|
// `skill remove <name>... [-y|--yes|-f|--force]`
|
|
41
34
|
//
|
|
42
35
|
// TTY + no --yes → y/N confirmation prompt (default keeps the skill)
|
|
@@ -98,10 +91,11 @@ export function cmdRemove(args) {
|
|
|
98
91
|
}
|
|
99
92
|
|
|
100
93
|
// Drop the skill from the global enabled list and the current project allow-list.
|
|
101
|
-
|
|
94
|
+
// Exported so the interactive manager can reuse it on its `d` (delete) action.
|
|
95
|
+
export function cleanConfig(name) {
|
|
102
96
|
const g = readGlobalConfig()
|
|
103
|
-
if ((g.
|
|
104
|
-
g.
|
|
97
|
+
if ((g.defaults || []).some(a => a.toLowerCase() === name.toLowerCase())) {
|
|
98
|
+
g.defaults = (g.defaults || []).filter(a => a.toLowerCase() !== name.toLowerCase())
|
|
105
99
|
writeGlobalConfig(g)
|
|
106
100
|
}
|
|
107
101
|
const cwd = process.cwd()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { input, checkbox } from '@inquirer/prompts'
|
|
2
|
+
import c from 'picocolors'
|
|
3
|
+
import { searchSkills } from '../lib/skills-find.js'
|
|
4
|
+
import { installSource } from './install.js'
|
|
5
|
+
|
|
6
|
+
// Run an @inquirer prompt that ALSO exits on ESC. The prebuilt input/checkbox
|
|
7
|
+
// prompts only respond to Enter / Ctrl-C; we wire ESC to abort via the `signal`
|
|
8
|
+
// (passed as the prompt's 2nd context arg → createPrompt's abort path, which
|
|
9
|
+
// rejects with AbortPromptError). Ctrl-C still works (ExitPromptError). Tested
|
|
10
|
+
// without a TTY by pre-aborting the signal (see test/cli/search.test.mjs).
|
|
11
|
+
//
|
|
12
|
+
// During a prompt createPrompt's readline (terminal:true) emits `keypress` on
|
|
13
|
+
// process.stdin, so our listener receives the ESC and aborts. Exported so the
|
|
14
|
+
// error-handling path is unit-testable.
|
|
15
|
+
export async function withEscExit(run) {
|
|
16
|
+
const controller = new AbortController()
|
|
17
|
+
const onKey = (_ch, key) => { if (key && key.name === 'escape') controller.abort() }
|
|
18
|
+
process.stdin.on('keypress', onKey)
|
|
19
|
+
try {
|
|
20
|
+
return { aborted: false, value: await run(controller.signal) }
|
|
21
|
+
} catch (e) {
|
|
22
|
+
if (e && /^(Abort|Exit|Cancel)PromptError$/.test(e.name)) return { aborted: true }
|
|
23
|
+
throw e
|
|
24
|
+
} finally {
|
|
25
|
+
process.stdin.removeListener('keypress', onKey)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Interactive search & multi-install (TTY only — cli.js gates on isInteractive).
|
|
30
|
+
//
|
|
31
|
+
// type a query → `npx skills find <query>` → multi-select (space to mark,
|
|
32
|
+
// enter to install all marked) → loops back so you can search again.
|
|
33
|
+
// ESC (or an empty query) at any prompt quits.
|
|
34
|
+
//
|
|
35
|
+
// Non-interactive (agents/CI) never reaches here; they use `skill install <source>`.
|
|
36
|
+
export async function cmdSearch() {
|
|
37
|
+
console.log(c.bold('skill search') + c.gray(' — discover & install from the skills ecosystem (skills.sh)'))
|
|
38
|
+
console.log(c.gray(' type a query · space to mark · enter to install marked · esc or empty query to quit'))
|
|
39
|
+
console.log()
|
|
40
|
+
|
|
41
|
+
let totalInstalled = 0
|
|
42
|
+
while (true) {
|
|
43
|
+
const q = await withEscExit(signal => input({ message: 'Search skills:', default: '' }, { signal }))
|
|
44
|
+
if (q.aborted) break // ESC / Ctrl-C → exit
|
|
45
|
+
const query = (q.value || '').trim()
|
|
46
|
+
if (!query) break // empty Enter → exit
|
|
47
|
+
|
|
48
|
+
let results
|
|
49
|
+
try {
|
|
50
|
+
results = searchSkills(query)
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error(c.red(' ' + e.message))
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
if (results.length === 0) {
|
|
56
|
+
console.log(c.gray(' no skills matched "' + query + '".'))
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const choices = results.map(r => ({
|
|
61
|
+
name: `${r.skill} ${c.gray(r.owner + '/' + r.repo)} ${c.gray('(' + (r.installs || '?') + ' installs)')}`,
|
|
62
|
+
value: r.source,
|
|
63
|
+
short: r.skill,
|
|
64
|
+
}))
|
|
65
|
+
|
|
66
|
+
const sel = await withEscExit(signal => checkbox({
|
|
67
|
+
message: `Results for "${query}" — space to mark, enter to install (esc to quit):`,
|
|
68
|
+
choices,
|
|
69
|
+
pageSize: 12,
|
|
70
|
+
}, { signal }))
|
|
71
|
+
if (sel.aborted) break // ESC / Ctrl-C → exit
|
|
72
|
+
const selected = sel.value || []
|
|
73
|
+
if (selected.length === 0) {
|
|
74
|
+
console.log(c.gray(' nothing marked — search again, or esc / empty query to quit.'))
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const src of selected) {
|
|
79
|
+
console.log()
|
|
80
|
+
try {
|
|
81
|
+
installSource(src)
|
|
82
|
+
totalInstalled++
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.error(c.red(' install failed for ' + src + ': ' + (e.message || e)))
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
console.log()
|
|
88
|
+
console.log(c.green(`✓ ${selected.length} selected this round.`) + c.gray(' Search again, or esc / empty query to finish.'))
|
|
89
|
+
console.log()
|
|
90
|
+
}
|
|
91
|
+
console.log(c.gray(`\nDone. ${totalInstalled} skill(s) installed this session.`))
|
|
92
|
+
}
|
package/src/lib/agents-md.js
CHANGED
|
@@ -19,6 +19,10 @@ Usage:
|
|
|
19
19
|
- \`skill show <name>\` — skill summary (path, triggers, version)
|
|
20
20
|
- \`skill cat <name>\` — load skill content into context
|
|
21
21
|
|
|
22
|
+
Defaults: on session start, run \`skill defaults\` to list skills marked as
|
|
23
|
+
defaults (active by default + auto-load), then \`skill cat <name>\` for each. Mark
|
|
24
|
+
your own with \`skill default <name>\` (global — active in every project).
|
|
25
|
+
|
|
22
26
|
Triggers: when the user types \`/X\`, run \`skill trigger X\`.
|
|
23
27
|
- Single match → apply the output directly.
|
|
24
28
|
- Multiple matches → show the candidate list; load the right one with \`skill cat <name>\`.
|
package/src/lib/config.js
CHANGED
|
@@ -6,7 +6,7 @@ import { CLI_ROOT, GLOBAL_CONFIG, PROJECT_CONFIG, STORE_DIR } from './paths.js'
|
|
|
6
6
|
const DEFAULT_GLOBAL = {
|
|
7
7
|
version: 1,
|
|
8
8
|
store: STORE_DIR,
|
|
9
|
-
|
|
9
|
+
defaults: [],
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function readGlobalConfig() {
|
|
@@ -17,7 +17,20 @@ export function readGlobalConfig() {
|
|
|
17
17
|
process.stderr.write('skill-cli config: parse error (' + (e.message || e) + ') — using defaults\n')
|
|
18
18
|
return { ...DEFAULT_GLOBAL }
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
const merged = { ...DEFAULT_GLOBAL, ...(parsed || {}) }
|
|
21
|
+
// backward-compat: legacy configs split the active-by-default set
|
|
22
|
+
// (`enabled_global`) from the auto-load set (`defaults_global`). The unified
|
|
23
|
+
// model collapses them into one `defaults` list (a default skill is now BOTH
|
|
24
|
+
// active-by-default AND auto-loaded). On read, adopt the union of both legacy
|
|
25
|
+
// lists when no new-format `defaults` is present; `defaults` always wins.
|
|
26
|
+
if (!Array.isArray(merged.defaults) || merged.defaults.length === 0) {
|
|
27
|
+
const legacy = [
|
|
28
|
+
...(Array.isArray(parsed?.enabled_global) ? parsed.enabled_global : []),
|
|
29
|
+
...(Array.isArray(parsed?.defaults_global) ? parsed.defaults_global : []),
|
|
30
|
+
]
|
|
31
|
+
if (legacy.length) merged.defaults = [...new Set(legacy.map(String))]
|
|
32
|
+
}
|
|
33
|
+
return merged
|
|
21
34
|
}
|
|
22
35
|
|
|
23
36
|
export function writeGlobalConfig(cfg) {
|
|
@@ -28,7 +41,7 @@ export function writeGlobalConfig(cfg) {
|
|
|
28
41
|
const out = {
|
|
29
42
|
version: cfg.version ?? 1,
|
|
30
43
|
store: cfg.store ?? STORE_DIR,
|
|
31
|
-
|
|
44
|
+
defaults: cfg.defaults || [],
|
|
32
45
|
}
|
|
33
46
|
fs.writeFileSync(GLOBAL_CONFIG, yaml.stringify(out), 'utf8')
|
|
34
47
|
}
|
|
@@ -47,7 +60,7 @@ export function readProjectConfig(cwd = process.cwd()) {
|
|
|
47
60
|
process.stderr.write('skill.config: parse error (' + (e.message || e) + ') — using global behavior\n')
|
|
48
61
|
return null
|
|
49
62
|
}
|
|
50
|
-
return { inherit: true, deny: [], allow: [], ...(parsed ||
|
|
63
|
+
return { inherit: true, deny: [], allow: [], ...(parsed || []) }
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
export function writeProjectConfig(cwd, cfg) {
|
|
@@ -78,7 +91,7 @@ function globMatch(pattern, name) {
|
|
|
78
91
|
export function computeEffective(installed, globalCfg, projCfg) {
|
|
79
92
|
const canonByLower = new Map(installed.map(s => [String(s.name).toLowerCase(), s.name]))
|
|
80
93
|
const enabled = new Set(
|
|
81
|
-
(projCfg && projCfg.inherit === false ? [] : (globalCfg.
|
|
94
|
+
(projCfg && projCfg.inherit === false ? [] : (globalCfg.defaults || []))
|
|
82
95
|
.map(s => String(s).toLowerCase())
|
|
83
96
|
)
|
|
84
97
|
if (projCfg) {
|
|
@@ -98,3 +111,17 @@ export function computeEffective(installed, globalCfg, projCfg) {
|
|
|
98
111
|
.map(n => canonByLower.get(n))
|
|
99
112
|
.sort()
|
|
100
113
|
}
|
|
114
|
+
|
|
115
|
+
// Effective DEFAULT skills (auto-loaded on agent session start), as CANONICAL
|
|
116
|
+
// names. In the unified model the default list IS the globally-active-by-default
|
|
117
|
+
// set (one `defaults` key in config.yaml): a default skill is active in every
|
|
118
|
+
// project AND auto-loaded on start. So this just returns that list filtered to
|
|
119
|
+
// installed skills. Defaults are GLOBAL (never per-folder) and ignore a
|
|
120
|
+
// project's deny rules (deny only governs active state, not auto-load).
|
|
121
|
+
export function computeDefaults(installed, globalCfg) {
|
|
122
|
+
const canonByLower = new Map(installed.map(s => [String(s.name).toLowerCase(), s.name]))
|
|
123
|
+
return [...new Set((globalCfg.defaults || []).map(s => String(s).toLowerCase()))]
|
|
124
|
+
.filter(n => canonByLower.has(n))
|
|
125
|
+
.map(n => canonByLower.get(n))
|
|
126
|
+
.sort()
|
|
127
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Are we talking to a human at a terminal? Agents, CI, and pipes have a non-TTY
|
|
2
|
+
// stdin and must NOT be shown an interactive prompt (they would hang waiting for
|
|
3
|
+
// input that never arrives). Shared by `remove` (y/N confirm) and the TUI commands
|
|
4
|
+
// (`search`, and the forthcoming manager). SKILL_CLI_FORCE_TTY lets tests exercise
|
|
5
|
+
// the prompt branch on a piped stdin.
|
|
6
|
+
export function isInteractive() {
|
|
7
|
+
return process.stdin.isTTY === true || process.env.SKILL_CLI_FORCE_TTY === '1'
|
|
8
|
+
}
|
package/src/lib/npx.js
CHANGED
|
@@ -22,8 +22,26 @@ import os from 'node:os'
|
|
|
22
22
|
// as a single argv element — no shell injection — and `--skill *` stays literal
|
|
23
23
|
// (cmd.exe does no globbing and no POSIX shell is ever involved). npx must be on
|
|
24
24
|
// PATH; it ships with node/npm on every platform.
|
|
25
|
+
// Detect a `@skill` pin on the source (skills ecosystem: owner/repo@skill or a
|
|
26
|
+
// URL@skill). SSH git URLs (`git@host:owner/repo`) start with git@ and are NOT a
|
|
27
|
+
// pin. When the source is pinned we must NOT pass `--skill '*'` — that flag
|
|
28
|
+
// overrides the pin and fetches every skill in the repo (verified), so a search
|
|
29
|
+
// result like owner/repo@research would otherwise dump the whole repo.
|
|
30
|
+
export function skillPin(source) {
|
|
31
|
+
if (typeof source !== 'string') return null
|
|
32
|
+
if (source.startsWith('git@')) return null
|
|
33
|
+
const at = source.lastIndexOf('@')
|
|
34
|
+
if (at <= 0) return null
|
|
35
|
+
const skill = source.slice(at + 1)
|
|
36
|
+
if (!skill || skill.includes('/') || skill.includes(':')) return null
|
|
37
|
+
return skill
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
export function buildNpxSpawn(source, platform = process.platform) {
|
|
26
|
-
|
|
41
|
+
// pinned source (owner/repo@skill) → let the @pin select the skill (passing
|
|
42
|
+
// --skill '*' here would override it and grab the whole repo). Otherwise fetch all.
|
|
43
|
+
const skillArgs = skillPin(source) ? [] : ['--skill', '*']
|
|
44
|
+
const base = ['-y', 'skills', 'add', source, '--copy', '--agent', 'claude-code', ...skillArgs, '-y']
|
|
27
45
|
return platform === 'win32'
|
|
28
46
|
? { cmd: 'cmd.exe', args: ['/c', 'npx', ...base] }
|
|
29
47
|
: { cmd: 'npx', args: base }
|
package/src/lib/paths.js
CHANGED
|
@@ -17,4 +17,6 @@ export const AGENT_GLOBALS = [
|
|
|
17
17
|
{ id: 'claude', dir: '.claude', file: 'CLAUDE.md' },
|
|
18
18
|
{ id: 'codex', dir: '.codex', file: 'AGENTS.md' },
|
|
19
19
|
{ id: 'gemini', dir: '.gemini', file: 'GEMINI.md' },
|
|
20
|
+
// pi loads global context from ~/.pi/agent/AGENTS.md (or CLAUDE.md).
|
|
21
|
+
{ id: 'pi', dir: '.pi/agent', file: 'AGENTS.md' },
|
|
20
22
|
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
// Parse the output of `npx skills find <query>` into structured results.
|
|
4
|
+
// Output shape (after ANSI strip):
|
|
5
|
+
// owner/repo@skill 25.7K installs
|
|
6
|
+
// └ https://skills.sh/owner/repo/skill
|
|
7
|
+
// Pure + exported so it is unit-testable without spawning npx.
|
|
8
|
+
export function parseFind(raw) {
|
|
9
|
+
const clean = String(raw == null ? '' : raw).replace(/\x1b\[[0-9;]*m/g, '')
|
|
10
|
+
const lines = clean.split(/\r?\n/)
|
|
11
|
+
const out = []
|
|
12
|
+
const srcRe = /^([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)@([A-Za-z0-9._-]+)\s+([0-9.]+[KM]?)?\s*installs\b/
|
|
13
|
+
for (let i = 0; i < lines.length; i++) {
|
|
14
|
+
const m = lines[i].match(srcRe)
|
|
15
|
+
if (!m) continue
|
|
16
|
+
const [, owner, repo, skill, installs] = m
|
|
17
|
+
const urlM = (lines[i + 1] || '').match(/https:\/\/\S+/)
|
|
18
|
+
out.push({
|
|
19
|
+
source: `${owner}/${repo}@${skill}`,
|
|
20
|
+
owner, repo, skill,
|
|
21
|
+
installs: installs || '',
|
|
22
|
+
url: urlM ? urlM[0] : '',
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
return out
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Run `npx skills find <query>` non-interactively (empty stdin, CI=1) and parse.
|
|
29
|
+
// Cross-platform: Windows spawns via cmd.exe (Node CVE-2024-27980 refuses npx.cmd
|
|
30
|
+
// with shell:false). The query is validated to a safe charset so it never reaches
|
|
31
|
+
// cmd.exe's metachar surface (& | < > etc.).
|
|
32
|
+
export function searchSkills(query) {
|
|
33
|
+
const q = String(query || '').trim()
|
|
34
|
+
if (!q) return []
|
|
35
|
+
if (/[^A-Za-z0-9 .\/_-]/.test(q)) {
|
|
36
|
+
throw new Error('search query may only contain letters, numbers, spaces, and . / _ -')
|
|
37
|
+
}
|
|
38
|
+
const isWin = process.platform === 'win32'
|
|
39
|
+
const cmd = isWin ? 'cmd.exe' : 'npx'
|
|
40
|
+
const args = isWin ? ['/c', 'npx', '-y', 'skills', 'find', q] : ['-y', 'skills', 'find', q]
|
|
41
|
+
let raw
|
|
42
|
+
try {
|
|
43
|
+
raw = execFileSync(cmd, args, {
|
|
44
|
+
input: '',
|
|
45
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
shell: false,
|
|
48
|
+
timeout: 60000,
|
|
49
|
+
env: { ...process.env, CI: '1' },
|
|
50
|
+
})
|
|
51
|
+
} catch (e) {
|
|
52
|
+
if (e.code === 'ENOENT') throw new Error('npx not found on PATH — is Node.js installed?')
|
|
53
|
+
const timedOut = e.signal === 'SIGTERM' || e.code === 'ETIMEDOUT'
|
|
54
|
+
if (timedOut) throw new Error('search timed out after 60s (network too slow?)')
|
|
55
|
+
throw new Error('skills find failed: ' + (((e.stderr || e.stdout || '').toString().trim().split('\n').pop()) || e.message))
|
|
56
|
+
}
|
|
57
|
+
return parseFind(raw)
|
|
58
|
+
}
|