skill-tags 1.2.1 → 1.5.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/README.md +134 -30
- package/bin/categories.js +87 -114
- package/bin/postinstall.js +62 -22
- package/bin/setup.js +218 -0
- package/bin/skill-tags.js +79 -79
- package/bin/st-skills.js +36 -0
- package/package.json +3 -2
- package/sync.sh +46 -31
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ If you're interested in contributing to Cursor Kits, please let me know!
|
|
|
16
16
|
- [Agent Setup Prompt](#agent-setup-prompt)
|
|
17
17
|
- [Install Options](#install-options)
|
|
18
18
|
- [How It Works](#how-it-works)
|
|
19
|
+
- [Project-Level Install](#project-level-install)
|
|
19
20
|
- [CLI Reference](#cli-reference)
|
|
20
21
|
- [Manual Sync](#manual-sync)
|
|
21
22
|
- [Skill Sources Scanned](#skill-sources-scanned)
|
|
@@ -23,29 +24,36 @@ If you're interested in contributing to Cursor Kits, please let me know!
|
|
|
23
24
|
- [How Categorization Works](#how-categorization-works)
|
|
24
25
|
- [Uninstall](#uninstall)
|
|
25
26
|
- [Requirements](#requirements)
|
|
27
|
+
- [Changelog](#changelog)
|
|
26
28
|
- [License](#license)
|
|
27
29
|
|
|
28
30
|
---
|
|
29
31
|
|
|
30
32
|
## Quick Start
|
|
31
33
|
|
|
34
|
+
1. **Install:** `npm install skill-tags -g` (or `--save-dev` for project-level)
|
|
35
|
+
2. **Follow the prompts** — choose **Global** or **Project**, then **Auto** or **Manual** sync
|
|
36
|
+
3. **Use:** Reference `@skill-tags.md` in any Cursor chat
|
|
37
|
+
|
|
32
38
|
```bash
|
|
33
|
-
# Install
|
|
34
39
|
npm install skill-tags -g
|
|
35
|
-
|
|
36
|
-
# Add the shell auto-trigger wrapper
|
|
37
|
-
skill-tags --setup
|
|
40
|
+
# Setup wizard runs automatically — choose Global + Auto (recommended)
|
|
38
41
|
source ~/.zshrc
|
|
42
|
+
```
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
For project-level installs:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install skill-tags --save-dev
|
|
48
|
+
# Setup wizard runs automatically — choose Project + Auto
|
|
49
|
+
npm run skills add owner/repo/skill-name # adds skill + auto-syncs
|
|
42
50
|
```
|
|
43
51
|
|
|
44
52
|
## Usage
|
|
45
53
|
|
|
46
54
|
In any Cursor chat, attach the full index of your skills by referencing the generated command file:
|
|
47
55
|
|
|
48
|
-
- `@skill-tags.md`
|
|
56
|
+
- `@skill-tags.md` (global) or `@project-skill-tags.md` (local, from `skill-tags --local`)
|
|
49
57
|
|
|
50
58
|
Example:
|
|
51
59
|
|
|
@@ -63,20 +71,25 @@ For more focused context windows, you can group your skills into category-specif
|
|
|
63
71
|
skill-tags --categories
|
|
64
72
|
```
|
|
65
73
|
|
|
66
|
-
This opens an interactive wizard where you can
|
|
67
|
-
|
|
68
|
-
Suggested skills are pre-selected (`[*]`). Use arrow keys and space to toggle any skill before confirming.
|
|
74
|
+
This opens an interactive wizard where you can add and remove categories. On save, skill assignment is powered by keyword matching against the generated `skill-tags.md` index — all text is lowercased with strict full-word matching and automatic suffix expansion (plurals, gerunds, etc.) to avoid false positives.
|
|
69
75
|
|
|
70
76
|
Once configured, every `skill-tags` sync automatically regenerates the category files from the saved config at `~/.cursor/skill-tags-categories.conf`.
|
|
71
77
|
|
|
78
|
+
For project-level categories (using only skills in `.agents/skills/`):
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
skill-tags --categories --local
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Then reference focused category indexes instead of the full list:
|
|
85
|
+
|
|
72
86
|
```bash
|
|
73
|
-
# Inject a focused category index instead of the full list:
|
|
74
87
|
@skills-frontend.md
|
|
75
88
|
@skills-testing.md
|
|
76
89
|
@skills-ai-agents.md
|
|
77
90
|
```
|
|
78
91
|
|
|
79
|
-
Predefined categories: `frontend`, `backend`, `database`, `testing`, `design`, `accessibility`, `performance`, `ai-agents`, `devops`, `marketing`, `mobile`, `documentation`.
|
|
92
|
+
Predefined categories: `frontend`, `backend`, `database`, `testing`, `design`, `accessibility`, `performance`, `ai-agents`, `devops`, `marketing`, `mobile`, `documentation`.
|
|
80
93
|
|
|
81
94
|
---
|
|
82
95
|
|
|
@@ -93,10 +106,8 @@ Install and configure the `skill-tags` package (npm: https://www.npmjs.com/packa
|
|
|
93
106
|
Use a terminal that runs outside the sandbox with full permissions to avoid permission errors during install. In Cursor, this means using a non-sandboxed terminal session if available (required_permissions: ["all"] if running via agent shell tools).
|
|
94
107
|
|
|
95
108
|
Steps to perform:
|
|
96
|
-
1. Install the package: `npm install skill-tags -g`
|
|
97
|
-
2.
|
|
98
|
-
3. Run `skill-tags` to perform an initial sync of all installed skills
|
|
99
|
-
4. Source my shell rc file or instruct me to do so manually
|
|
109
|
+
1. Install the package: `npm install skill-tags -g` — the setup wizard runs automatically during install; when prompted, choose Global and Auto (recommended)
|
|
110
|
+
2. Source my shell rc file or instruct me to do so manually
|
|
100
111
|
|
|
101
112
|
When complete, output a summary that includes:
|
|
102
113
|
- Where the command file was generated (~/.cursor/commands/skill-tags.md)
|
|
@@ -113,14 +124,21 @@ When complete, output a summary that includes:
|
|
|
113
124
|
|
|
114
125
|
## Install Options
|
|
115
126
|
|
|
116
|
-
###
|
|
127
|
+
### Global install (npm)
|
|
117
128
|
|
|
118
129
|
```bash
|
|
119
|
-
npm install skill-tags -g
|
|
120
|
-
skill-tags --setup
|
|
130
|
+
npm install skill-tags -g # setup wizard runs automatically
|
|
121
131
|
source ~/.zshrc
|
|
122
132
|
```
|
|
123
133
|
|
|
134
|
+
### Project install (npm)
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npm install skill-tags --save-dev # setup wizard runs automatically
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Adds `"skills": "st-skills"` to `package.json`. Use `npm run skills add <pkg>` to add project skills — auto-syncs `.cursor/commands/project-skill-tags.md` on every change.
|
|
141
|
+
|
|
124
142
|
### One-off run (no install)
|
|
125
143
|
|
|
126
144
|
```bash
|
|
@@ -128,7 +146,7 @@ npx skill-tags
|
|
|
128
146
|
```
|
|
129
147
|
|
|
130
148
|
### Install via curl
|
|
131
|
-
|
|
149
|
+
|
|
132
150
|
```bash
|
|
133
151
|
curl -fsSL https://raw.githubusercontent.com/steve-piece/skill-tags/main/install.sh | bash
|
|
134
152
|
```
|
|
@@ -141,31 +159,76 @@ source ~/.zshrc # or ~/.bash_profile / ~/.bashrc
|
|
|
141
159
|
|
|
142
160
|
## How It Works
|
|
143
161
|
|
|
144
|
-
After setup, the `skills` command wraps `npx skills` and automatically runs a sync after every `skills add` or `skills remove`:
|
|
162
|
+
After global setup, the `skills` command wraps `npx skills` and automatically runs a sync after every `skills add` or `skills remove`:
|
|
145
163
|
|
|
146
164
|
```bash
|
|
147
165
|
# Install (or remove) a skill — sync runs automatically
|
|
148
166
|
skills add vercel-labs/agent-skills/vercel-react-best-practices
|
|
149
167
|
|
|
150
|
-
# The
|
|
168
|
+
# The command file is updated:
|
|
151
169
|
# ~/.cursor/commands/skill-tags.md
|
|
152
170
|
|
|
153
171
|
# Use it in Cursor chat:
|
|
154
172
|
# @skill-tags.md
|
|
155
173
|
```
|
|
156
174
|
|
|
175
|
+
## Project-Level Install
|
|
176
|
+
|
|
177
|
+
Install skill-tags as a dev dependency to manage project-specific skills without touching your global shell config. The setup wizard runs automatically during install and adds a `"skills"` npm script backed by the bundled `st-skills` binary.
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
npm install skill-tags --save-dev
|
|
181
|
+
# choose: Project + Auto when prompted
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
After setup, `package.json` will include:
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
"scripts": {
|
|
188
|
+
"skills": "st-skills"
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Use `npm run skills` to add, remove, or update project skills:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
npm run skills add owner/repo/skill-name # adds skill + auto-syncs
|
|
196
|
+
npm run skills remove owner/repo/skill-name # removes skill + auto-syncs
|
|
197
|
+
npm run skills update owner/repo/skill-name # updates skill + auto-syncs
|
|
198
|
+
|
|
199
|
+
# Manual re-sync:
|
|
200
|
+
skill-tags --local
|
|
201
|
+
|
|
202
|
+
# Use in Cursor chat:
|
|
203
|
+
# @project-skill-tags.md
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The `st-skills` binary (from this package) wraps `npx skills`, then automatically runs `skill-tags --local` on every successful `add`, `remove`, or `update`, writing the index to `.cursor/commands/project-skill-tags.md`.
|
|
207
|
+
|
|
157
208
|
## CLI Reference
|
|
158
209
|
|
|
159
210
|
```bash
|
|
160
|
-
skill-tags
|
|
161
|
-
skill-tags --categories
|
|
162
|
-
skill-tags --
|
|
163
|
-
skill-tags --
|
|
164
|
-
skill-tags --
|
|
165
|
-
skill-tags --
|
|
166
|
-
skill-tags --
|
|
211
|
+
skill-tags # sync all skills, generate/update the command file
|
|
212
|
+
skill-tags --categories # open interactive category wizard
|
|
213
|
+
skill-tags --categories --local # category wizard for project-level skills only
|
|
214
|
+
skill-tags --setup # re-run setup wizard (runs automatically on first install)
|
|
215
|
+
skill-tags --global # skip local skills (.agents/skills in CWD); scan global sources only
|
|
216
|
+
skill-tags --local # scan only .agents/skills in CWD; write to .cursor/commands/project-skill-tags.md
|
|
217
|
+
skill-tags --latest # update skill-tags to the latest version
|
|
218
|
+
skill-tags --version # print version
|
|
219
|
+
skill-tags --help # show usage
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### `st-skills` (project binary)
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
npm run skills add <pkg> # install a project skill + auto-sync
|
|
226
|
+
npm run skills remove <pkg> # remove a project skill + auto-sync
|
|
227
|
+
npm run skills update <pkg> # update a project skill + auto-sync
|
|
167
228
|
```
|
|
168
229
|
|
|
230
|
+
`st-skills` is registered in `package.json` by `skill-tags --setup` (local mode). It wraps `npx skills` and calls `skill-tags --local` after every mutating command.
|
|
231
|
+
|
|
169
232
|
## Manual Sync
|
|
170
233
|
|
|
171
234
|
Run at any time to regenerate the command file:
|
|
@@ -192,7 +255,7 @@ Skills are discovered from all of these locations automatically. When the same s
|
|
|
192
255
|
|
|
193
256
|
## Generated File Format
|
|
194
257
|
|
|
195
|
-
The `~/.cursor/commands/skill-tags.md`
|
|
258
|
+
The `~/.cursor/commands/skill-tags.md` (global) and `./.cursor/commands/project-skill-tags.md` (local) contain:
|
|
196
259
|
|
|
197
260
|
- An opening instruction for the agent to assess all skills and apply them autonomously.
|
|
198
261
|
- Instructions for the agent on how to plan with skills when in Plan Mode.
|
|
@@ -227,6 +290,47 @@ bash uninstall.sh
|
|
|
227
290
|
- bash or zsh
|
|
228
291
|
- [Cursor IDE](https://cursor.com)
|
|
229
292
|
|
|
293
|
+
## Changelog
|
|
294
|
+
|
|
295
|
+
### v1.5.0
|
|
296
|
+
|
|
297
|
+
- **`--latest` flag** — run `skill-tags --latest` to check npm for updates and self-update in place
|
|
298
|
+
- **Project-level categories** — `skill-tags --categories --local` runs the category wizard using only project skills (`.agents/skills/`), stores config in `.cursor/skill-tags-categories.conf`, and generates category files in `.cursor/commands/`
|
|
299
|
+
- **Simplified category wizard** — streamlined to three actions: Add categories (multi-select from predefined list), Edit categories (multi-select to remove), and Save changes (runs keyword matching on save)
|
|
300
|
+
- **Quieter sync from wizard** — `sync.sh` accepts `--quiet` to suppress verbose scan output; the category wizard uses it for cleaner post-save output with an aligned summary table
|
|
301
|
+
- **Removed custom category input** — categories are now limited to the predefined list for consistency
|
|
302
|
+
|
|
303
|
+
### v1.4.0
|
|
304
|
+
|
|
305
|
+
- Interactive `--categories` wizard with keyword-based auto-assignment
|
|
306
|
+
- Category config persistence at `~/.cursor/skill-tags-categories.conf`
|
|
307
|
+
- Auto-regeneration of category files on every sync
|
|
308
|
+
- Predefined categories: frontend, backend, database, testing, design, accessibility, performance, ai-agents, devops, marketing, mobile, documentation
|
|
309
|
+
|
|
310
|
+
### v1.3.0
|
|
311
|
+
|
|
312
|
+
- `--local` flag for project-level skill scanning
|
|
313
|
+
- `st-skills` binary for project-level add/remove/update with auto-sync
|
|
314
|
+
- Project setup mode in `--setup` wizard
|
|
315
|
+
|
|
316
|
+
### v1.2.0
|
|
317
|
+
|
|
318
|
+
- `--global` flag to skip local skills
|
|
319
|
+
- `--setup` wizard with Global and Project modes
|
|
320
|
+
- Shell wrapper auto-sync on `skills add/remove`
|
|
321
|
+
|
|
322
|
+
### v1.1.0
|
|
323
|
+
|
|
324
|
+
- Deduplication across skill sources (first-found wins by priority)
|
|
325
|
+
- Support for nested plugin cache directories
|
|
326
|
+
- Description extraction from YAML frontmatter and first content line
|
|
327
|
+
|
|
328
|
+
### v1.0.0
|
|
329
|
+
|
|
330
|
+
- Initial release
|
|
331
|
+
- Scans all known skill locations and generates `~/.cursor/commands/skill-tags.md`
|
|
332
|
+
- Priority-ordered skill sources with deduplication
|
|
333
|
+
|
|
230
334
|
## License
|
|
231
335
|
|
|
232
336
|
MIT
|
package/bin/categories.js
CHANGED
|
@@ -5,18 +5,26 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
-
const { checkbox, select
|
|
8
|
+
const { checkbox, select } = require('@inquirer/prompts');
|
|
9
9
|
const { spawnSync } = require('child_process');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const os = require('os');
|
|
13
13
|
|
|
14
14
|
const HOME = os.homedir();
|
|
15
|
-
const
|
|
16
|
-
const COMMANDS_DIR = path.join(HOME, '.cursor', 'commands');
|
|
17
|
-
const SKILL_TAGS_FILE = path.join(COMMANDS_DIR, 'skill-tags.md');
|
|
15
|
+
const IS_LOCAL = process.argv.includes('--local');
|
|
18
16
|
const SYNC_SCRIPT = path.join(__dirname, '..', 'sync.sh');
|
|
19
17
|
|
|
18
|
+
const CATEGORIES_CONFIG = IS_LOCAL
|
|
19
|
+
? path.join(process.cwd(), '.cursor', 'skill-tags-categories.conf')
|
|
20
|
+
: path.join(HOME, '.cursor', 'skill-tags-categories.conf');
|
|
21
|
+
const COMMANDS_DIR = IS_LOCAL
|
|
22
|
+
? path.join(process.cwd(), '.cursor', 'commands')
|
|
23
|
+
: path.join(HOME, '.cursor', 'commands');
|
|
24
|
+
const SKILL_TAGS_FILE = IS_LOCAL
|
|
25
|
+
? path.join(process.cwd(), '.cursor', 'commands', 'project-skill-tags.md')
|
|
26
|
+
: path.join(HOME, '.cursor', 'commands', 'skill-tags.md');
|
|
27
|
+
|
|
20
28
|
const PREDEFINED_CATEGORIES = [
|
|
21
29
|
'frontend',
|
|
22
30
|
'backend',
|
|
@@ -119,8 +127,10 @@ const CATEGORY_KEYWORDS = {
|
|
|
119
127
|
|
|
120
128
|
function ensureSkillTagsFile() {
|
|
121
129
|
if (fs.existsSync(SKILL_TAGS_FILE)) return;
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
const label = IS_LOCAL ? 'project-skill-tags.md' : 'skill-tags.md';
|
|
131
|
+
console.log(` ${label} not found — running initial sync...\n`);
|
|
132
|
+
const args = IS_LOCAL ? [SYNC_SCRIPT, '--local'] : [SYNC_SCRIPT];
|
|
133
|
+
spawnSync('bash', args, { stdio: 'inherit' });
|
|
124
134
|
}
|
|
125
135
|
|
|
126
136
|
function loadSkillsFromIndex() {
|
|
@@ -212,135 +222,66 @@ function toTitleCase(str) {
|
|
|
212
222
|
return str.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
213
223
|
}
|
|
214
224
|
|
|
215
|
-
function truncate(str, len) {
|
|
216
|
-
if (!str || str.length <= len) return str || '';
|
|
217
|
-
return str.slice(0, len - 1) + '…';
|
|
218
|
-
}
|
|
219
|
-
|
|
220
225
|
// ─── Wizard actions ──────────────────────────────────────────────────────────
|
|
221
226
|
|
|
222
|
-
async function addCategories(
|
|
227
|
+
async function addCategories(config) {
|
|
223
228
|
const existing = new Set(Object.keys(config));
|
|
224
229
|
const available = PREDEFINED_CATEGORIES.filter(c => !existing.has(c));
|
|
225
230
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
message: 'Select categories to add (space to toggle)',
|
|
230
|
-
choices: available.map(c => ({ name: toTitleCase(c), value: c })),
|
|
231
|
-
pageSize: 14,
|
|
232
|
-
});
|
|
233
|
-
} else {
|
|
234
|
-
console.log('\n All predefined categories already exist.');
|
|
231
|
+
if (available.length === 0) {
|
|
232
|
+
console.log('\n All categories already added.\n');
|
|
233
|
+
return;
|
|
235
234
|
}
|
|
236
235
|
|
|
237
|
-
const
|
|
238
|
-
message: '
|
|
236
|
+
const selected = await checkbox({
|
|
237
|
+
message: 'Select categories to add (space to toggle)',
|
|
238
|
+
choices: available.map(c => ({ name: toTitleCase(c), value: c })),
|
|
239
|
+
pageSize: 14,
|
|
239
240
|
});
|
|
240
241
|
|
|
241
|
-
if (custom.trim()) {
|
|
242
|
-
const normalized = custom.trim().toLowerCase()
|
|
243
|
-
.replace(/[^a-z0-9-]/g, '-')
|
|
244
|
-
.replace(/-+/g, '-')
|
|
245
|
-
.replace(/^-|-$/g, '');
|
|
246
|
-
if (normalized && !existing.has(normalized)) {
|
|
247
|
-
selected.push(normalized);
|
|
248
|
-
} else if (existing.has(normalized)) {
|
|
249
|
-
console.log(` "${normalized}" already exists — use Edit instead.`);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
242
|
if (selected.length === 0) {
|
|
254
243
|
console.log(' No categories selected.\n');
|
|
255
244
|
return;
|
|
256
245
|
}
|
|
257
246
|
|
|
258
247
|
for (const cat of selected) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const hint = match ? ` — ${match.reason}` : '';
|
|
262
|
-
return {
|
|
263
|
-
name: `${s.name}${hint}`,
|
|
264
|
-
value: s.name,
|
|
265
|
-
checked: !!match,
|
|
266
|
-
description: truncate(s.description, 60),
|
|
267
|
-
};
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
const selectedSkills = await checkbox({
|
|
271
|
-
message: `Select skills for "${cat}" (space to toggle)`,
|
|
272
|
-
choices: skillChoices,
|
|
273
|
-
pageSize: 15,
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
config[cat] = selectedSkills;
|
|
277
|
-
console.log(` ✓ ${toTitleCase(cat)}: ${selectedSkills.length} skill(s)\n`);
|
|
248
|
+
config[cat] = [];
|
|
249
|
+
console.log(` + ${toTitleCase(cat)}`);
|
|
278
250
|
}
|
|
251
|
+
console.log();
|
|
279
252
|
}
|
|
280
253
|
|
|
281
|
-
async function
|
|
282
|
-
const cats = Object.keys(config);
|
|
283
|
-
if (cats.length === 0) {
|
|
284
|
-
console.log(' No categories yet. Add one first.\n');
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const cat = await select({
|
|
289
|
-
message: 'Which category to edit?',
|
|
290
|
-
choices: cats.map(c => ({
|
|
291
|
-
name: `${toTitleCase(c)} (${config[c].length} skills)`,
|
|
292
|
-
value: c,
|
|
293
|
-
})),
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
const currentSet = new Set(config[cat] || []);
|
|
297
|
-
const skillChoices = skills.map(s => {
|
|
298
|
-
const match = matchKeywords(s, cat);
|
|
299
|
-
const hint = match ? ` — ${match.reason}` : '';
|
|
300
|
-
return {
|
|
301
|
-
name: `${s.name}${hint}`,
|
|
302
|
-
value: s.name,
|
|
303
|
-
checked: currentSet.has(s.name),
|
|
304
|
-
description: truncate(s.description, 60),
|
|
305
|
-
};
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const selectedSkills = await checkbox({
|
|
309
|
-
message: `Select skills for "${cat}" (space to toggle)`,
|
|
310
|
-
choices: skillChoices,
|
|
311
|
-
pageSize: 15,
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
config[cat] = selectedSkills;
|
|
315
|
-
console.log(` ✓ Updated ${toTitleCase(cat)}: ${selectedSkills.length} skill(s)\n`);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
async function deleteCategory(config) {
|
|
254
|
+
async function editCategories(config) {
|
|
319
255
|
const cats = Object.keys(config);
|
|
320
256
|
if (cats.length === 0) {
|
|
321
|
-
console.log(' No categories yet.\n');
|
|
257
|
+
console.log(' No categories yet. Add some first.\n');
|
|
322
258
|
return;
|
|
323
259
|
}
|
|
324
260
|
|
|
325
|
-
const
|
|
326
|
-
message: '
|
|
261
|
+
const kept = await checkbox({
|
|
262
|
+
message: 'Uncheck categories to remove (space to toggle)',
|
|
327
263
|
choices: cats.map(c => ({
|
|
328
264
|
name: `${toTitleCase(c)} (${config[c].length} skills)`,
|
|
329
265
|
value: c,
|
|
266
|
+
checked: true,
|
|
330
267
|
})),
|
|
268
|
+
pageSize: 14,
|
|
331
269
|
});
|
|
332
270
|
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
default: false,
|
|
336
|
-
});
|
|
271
|
+
const keptSet = new Set(kept);
|
|
272
|
+
const removed = cats.filter(c => !keptSet.has(c));
|
|
337
273
|
|
|
338
|
-
|
|
274
|
+
for (const cat of removed) {
|
|
339
275
|
delete config[cat];
|
|
340
276
|
const genFile = path.join(COMMANDS_DIR, `skills-${cat}.md`);
|
|
341
277
|
try { fs.unlinkSync(genFile); } catch {}
|
|
342
|
-
console.log(`
|
|
278
|
+
console.log(` - Removed: ${toTitleCase(cat)}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (removed.length === 0) {
|
|
282
|
+
console.log(' No changes.');
|
|
343
283
|
}
|
|
284
|
+
console.log();
|
|
344
285
|
}
|
|
345
286
|
|
|
346
287
|
function printCurrentCategories(config) {
|
|
@@ -359,11 +300,13 @@ function printCurrentCategories(config) {
|
|
|
359
300
|
// ─── Main loop ───────────────────────────────────────────────────────────────
|
|
360
301
|
|
|
361
302
|
async function main() {
|
|
362
|
-
|
|
303
|
+
const mode = IS_LOCAL ? 'project' : 'global';
|
|
304
|
+
const indexFile = IS_LOCAL ? 'project-skill-tags.md' : 'skill-tags.md';
|
|
305
|
+
console.log(`\n skill-tags: category wizard (${mode})\n`);
|
|
363
306
|
|
|
364
307
|
ensureSkillTagsFile();
|
|
365
308
|
|
|
366
|
-
console.log(
|
|
309
|
+
console.log(` Loading skills from ${indexFile}...`);
|
|
367
310
|
const skills = loadSkillsFromIndex();
|
|
368
311
|
console.log(` Found ${skills.length} skill(s)\n`);
|
|
369
312
|
|
|
@@ -376,30 +319,60 @@ async function main() {
|
|
|
376
319
|
message: 'What would you like to do?',
|
|
377
320
|
choices: [
|
|
378
321
|
{ name: 'Add categories', value: 'add' },
|
|
379
|
-
{ name: 'Edit
|
|
380
|
-
{ name: '
|
|
381
|
-
{ name: 'Save & generate files', value: 'save' },
|
|
322
|
+
{ name: 'Edit categories', value: 'edit' },
|
|
323
|
+
{ name: 'Save changes', value: 'save' },
|
|
382
324
|
{ name: 'Quit without saving', value: 'quit' },
|
|
383
325
|
],
|
|
384
326
|
});
|
|
385
327
|
|
|
386
328
|
switch (action) {
|
|
387
329
|
case 'add':
|
|
388
|
-
await addCategories(
|
|
330
|
+
await addCategories(config);
|
|
389
331
|
break;
|
|
390
332
|
case 'edit':
|
|
391
|
-
await
|
|
392
|
-
break;
|
|
393
|
-
case 'delete':
|
|
394
|
-
await deleteCategory(config);
|
|
333
|
+
await editCategories(config);
|
|
395
334
|
break;
|
|
396
335
|
case 'save': {
|
|
336
|
+
const cats = Object.keys(config);
|
|
337
|
+
if (cats.length === 0) {
|
|
338
|
+
console.log(' No categories to save.\n');
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const pad = Math.max(...cats.map(c => toTitleCase(c).length));
|
|
343
|
+
|
|
344
|
+
console.log();
|
|
345
|
+
console.log(' ┌─ Matching skills to categories ─────────────────');
|
|
346
|
+
for (const cat of cats) {
|
|
347
|
+
const matched = skills.filter(s => matchKeywords(s, cat));
|
|
348
|
+
config[cat] = matched.map(s => s.name);
|
|
349
|
+
const label = toTitleCase(cat).padEnd(pad);
|
|
350
|
+
const count = String(matched.length).padStart(2);
|
|
351
|
+
console.log(` │ ✓ ${label} ${count} skill${matched.length === 1 ? '' : 's'}`);
|
|
352
|
+
}
|
|
353
|
+
console.log(' └─────────────────────────────────────────────────');
|
|
354
|
+
|
|
397
355
|
writeConfig(config);
|
|
398
|
-
|
|
399
|
-
const
|
|
356
|
+
|
|
357
|
+
const syncArgs = IS_LOCAL
|
|
358
|
+
? [SYNC_SCRIPT, '--local', '--quiet']
|
|
359
|
+
: [SYNC_SCRIPT, '--quiet'];
|
|
360
|
+
console.log(`\n Syncing ${skills.length} skills...\n`);
|
|
361
|
+
const result = spawnSync('bash', syncArgs, { stdio: 'inherit' });
|
|
400
362
|
if (result.error) {
|
|
401
|
-
console.error(` Error
|
|
363
|
+
console.error(` Error: ${result.error.message}`);
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const configPath = IS_LOCAL
|
|
368
|
+
? '.cursor/skill-tags-categories.conf'
|
|
369
|
+
: '~/.cursor/skill-tags-categories.conf';
|
|
370
|
+
console.log(` ✓ Config saved → ${configPath}`);
|
|
371
|
+
console.log(` ✓ ${cats.length} category file${cats.length === 1 ? '' : 's'} generated`);
|
|
372
|
+
for (const cat of cats) {
|
|
373
|
+
console.log(` @skills-${cat}.md`);
|
|
402
374
|
}
|
|
375
|
+
console.log(`\n Tip: type @skills-<category>.md in Cursor chat to use a category.\n`);
|
|
403
376
|
process.exit(result.status ?? 0);
|
|
404
377
|
}
|
|
405
378
|
case 'quit':
|
package/bin/postinstall.js
CHANGED
|
@@ -1,42 +1,82 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// bin/postinstall.js
|
|
3
|
-
// Runs automatically after `npm install
|
|
4
|
-
//
|
|
3
|
+
// Runs automatically after `npm install skill-tags`.
|
|
4
|
+
// Launches interactive setup wizard on first install; re-syncs on reinstall.
|
|
5
|
+
// Falls back gracefully for non-interactive environments (CI, piped input).
|
|
5
6
|
// Never throws — a failed postinstall should never break npm install.
|
|
6
7
|
|
|
7
8
|
'use strict';
|
|
8
9
|
|
|
9
|
-
const { spawnSync } = require('child_process');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const fs = require('fs');
|
|
12
|
+
const os = require('os');
|
|
12
13
|
|
|
13
|
-
// Skip entirely on Windows
|
|
14
14
|
if (process.platform === 'win32') {
|
|
15
15
|
process.exit(0);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
18
|
+
const HOME = os.homedir();
|
|
19
|
+
const isGlobal = process.env.npm_config_global === 'true';
|
|
20
|
+
const SYNC_SCRIPT = path.join(__dirname, '..', 'sync.sh');
|
|
21
|
+
const WRAPPER_MARKER = '# ─── skill-tags / Cursor Skill Command Sync';
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
function isAlreadyConfigured() {
|
|
24
|
+
if (isGlobal) {
|
|
25
|
+
const shellName = path.basename(process.env.SHELL || 'bash');
|
|
26
|
+
let rcFile;
|
|
27
|
+
if (shellName === 'zsh') rcFile = path.join(HOME, '.zshrc');
|
|
28
|
+
else if (process.platform === 'darwin') rcFile = path.join(HOME, '.bash_profile');
|
|
29
|
+
else rcFile = path.join(HOME, '.bashrc');
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
31
|
+
try {
|
|
32
|
+
return fs.readFileSync(rcFile, 'utf-8').includes(WRAPPER_MARKER);
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
29
37
|
|
|
30
|
-
|
|
38
|
+
try {
|
|
39
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8'));
|
|
40
|
+
return pkg.scripts?.skills === 'st-skills';
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
31
45
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// Never fail the install
|
|
46
|
+
function runSync() {
|
|
47
|
+
if (!fs.existsSync(SYNC_SCRIPT)) return;
|
|
48
|
+
const { spawnSync } = require('child_process');
|
|
49
|
+
spawnSync('bash', [SYNC_SCRIPT], { stdio: 'inherit' });
|
|
37
50
|
}
|
|
38
51
|
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
async function main() {
|
|
53
|
+
if (isAlreadyConfigured()) {
|
|
54
|
+
if (isGlobal) {
|
|
55
|
+
console.log('\n skill-tags: re-syncing...\n');
|
|
56
|
+
runSync();
|
|
57
|
+
} else {
|
|
58
|
+
console.log('\n skill-tags: already configured.\n');
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const runSetup = require('./setup');
|
|
65
|
+
await runSetup();
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err.name === 'ExitPromptError') {
|
|
68
|
+
console.log();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (isGlobal) {
|
|
72
|
+
console.log('\n skill-tags: running initial sync...\n');
|
|
73
|
+
runSync();
|
|
74
|
+
console.log(' To configure auto-sync, run: skill-tags --setup\n');
|
|
75
|
+
} else {
|
|
76
|
+
console.log('\n skill-tags installed as a project dependency.');
|
|
77
|
+
console.log(' Run setup to configure: npx skill-tags --setup\n');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
41
81
|
|
|
42
|
-
|
|
82
|
+
main();
|
package/bin/setup.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bin/setup.js
|
|
3
|
+
// Interactive setup wizard: global/project install mode + auto/manual sync preference.
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
const HOME = os.homedir();
|
|
13
|
+
const SYNC_SCRIPT = path.join(__dirname, '..', 'sync.sh');
|
|
14
|
+
const WRAPPER_MARKER = '# ─── skill-tags / Cursor Skill Command Sync';
|
|
15
|
+
|
|
16
|
+
module.exports = async function runSetup() {
|
|
17
|
+
const { select, confirm } = require('@inquirer/prompts');
|
|
18
|
+
|
|
19
|
+
console.log('\n skill-tags: setup\n');
|
|
20
|
+
|
|
21
|
+
const isGlobal = process.env.npm_config_global === 'true';
|
|
22
|
+
|
|
23
|
+
const mode = await select({
|
|
24
|
+
message: 'How are you using skill-tags?',
|
|
25
|
+
choices: [
|
|
26
|
+
{
|
|
27
|
+
name: 'Global — add skills() wrapper to shell profile (recommended for most users)',
|
|
28
|
+
value: 'global',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'Project — add "skills" npm script to this project\'s package.json',
|
|
32
|
+
value: 'project',
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
default: isGlobal ? 'global' : 'project',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const syncMode = await select({
|
|
39
|
+
message: 'How should skill-tags sync?',
|
|
40
|
+
choices: [
|
|
41
|
+
{
|
|
42
|
+
name: 'Auto (recommended) — sync automatically whenever you add or remove a skill',
|
|
43
|
+
value: 'auto',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'Manual — run skill-tags yourself to sync when needed',
|
|
47
|
+
value: 'manual',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
default: 'auto',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (mode === 'global') {
|
|
54
|
+
await setupGlobal(confirm, syncMode);
|
|
55
|
+
} else {
|
|
56
|
+
await setupProject(confirm, syncMode);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ─── Global setup ─────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
async function setupGlobal(confirm, syncMode) {
|
|
63
|
+
const shellName = path.basename(process.env.SHELL || 'bash');
|
|
64
|
+
let rcFile;
|
|
65
|
+
if (shellName === 'zsh') rcFile = path.join(HOME, '.zshrc');
|
|
66
|
+
else if (process.platform === 'darwin') rcFile = path.join(HOME, '.bash_profile');
|
|
67
|
+
else rcFile = path.join(HOME, '.bashrc');
|
|
68
|
+
|
|
69
|
+
const displayRc = rcFile.replace(HOME, '~');
|
|
70
|
+
|
|
71
|
+
if (syncMode === 'manual') {
|
|
72
|
+
console.log('\n Manual sync selected.');
|
|
73
|
+
console.log(' Run skill-tags at any time to regenerate ~/.cursor/commands/skill-tags.md\n');
|
|
74
|
+
console.log(' Running initial sync...\n');
|
|
75
|
+
spawnSync('bash', [SYNC_SCRIPT], { stdio: 'inherit' });
|
|
76
|
+
console.log(`\n Done! Run skill-tags manually to re-sync.\n`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Auto sync: add shell wrapper
|
|
81
|
+
let alreadyInstalled = false;
|
|
82
|
+
try {
|
|
83
|
+
const content = fs.readFileSync(rcFile, 'utf-8');
|
|
84
|
+
if (content.includes(WRAPPER_MARKER)) alreadyInstalled = true;
|
|
85
|
+
} catch {}
|
|
86
|
+
|
|
87
|
+
if (alreadyInstalled) {
|
|
88
|
+
console.log(`\n ✓ Shell wrapper already installed in ${displayRc}\n`);
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(`\n This will add a skills() shell wrapper to ${displayRc}`);
|
|
93
|
+
console.log(` It auto-syncs skill-tags.md after every skills add/remove.\n`);
|
|
94
|
+
|
|
95
|
+
const yes = await confirm({
|
|
96
|
+
message: `Add the wrapper to ${displayRc}?`,
|
|
97
|
+
default: true,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!yes) {
|
|
101
|
+
console.log('\n Skipped.\n');
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const syncPath = fs.existsSync(path.join(HOME, '.cursor', 'sync-skill-commands.sh'))
|
|
106
|
+
? path.join(HOME, '.cursor', 'sync-skill-commands.sh')
|
|
107
|
+
: SYNC_SCRIPT;
|
|
108
|
+
|
|
109
|
+
const wrapper = `
|
|
110
|
+
${WRAPPER_MARKER} ────────────────────────────────────────────────
|
|
111
|
+
# Wraps \`npx skills\` to auto-generate skill-tags.md after install/removal.
|
|
112
|
+
# Run manually: skill-tags (or: bash ${syncPath})
|
|
113
|
+
function skills() {
|
|
114
|
+
npx skills "$@"
|
|
115
|
+
local exit_code=$?
|
|
116
|
+
if [[ "$1" == "add" || "$1" == "remove" ]] && [[ $exit_code -eq 0 ]]; then
|
|
117
|
+
bash "${syncPath}"
|
|
118
|
+
fi
|
|
119
|
+
return $exit_code
|
|
120
|
+
}
|
|
121
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
fs.appendFileSync(rcFile, wrapper);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error(`\n Failed to write to ${displayRc}: ${err.message}\n`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(`\n ✓ Added skills() wrapper to ${displayRc}`);
|
|
132
|
+
console.log(` Reload with: source ${displayRc}\n`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Project setup ────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
async function setupProject(confirm, syncMode) {
|
|
138
|
+
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
139
|
+
|
|
140
|
+
if (!fs.existsSync(pkgPath)) {
|
|
141
|
+
console.error('\n No package.json found in current directory.');
|
|
142
|
+
console.error(' Run skill-tags --setup from your project root.\n');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let pkg;
|
|
147
|
+
try {
|
|
148
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error(`\n Failed to read package.json: ${err.message}\n`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const scripts = pkg.scripts || {};
|
|
155
|
+
|
|
156
|
+
if (syncMode === 'manual') {
|
|
157
|
+
console.log('\n Manual sync selected.');
|
|
158
|
+
console.log(' Run skill-tags --local at any time to regenerate .cursor/commands/project-skill-tags.md\n');
|
|
159
|
+
console.log(' Running initial project sync...\n');
|
|
160
|
+
spawnSync(
|
|
161
|
+
'node',
|
|
162
|
+
[path.join(__dirname, 'skill-tags.js'), '--local'],
|
|
163
|
+
{ stdio: 'inherit', cwd: process.cwd() }
|
|
164
|
+
);
|
|
165
|
+
console.log(`\n Done! Run skill-tags --local manually to re-sync.\n`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Auto sync: add "skills" npm script
|
|
170
|
+
if (scripts.skills === 'st-skills') {
|
|
171
|
+
console.log('\n ✓ Already configured — "skills": "st-skills" is in package.json\n');
|
|
172
|
+
console.log(' Add a skill: npm run skills add <owner/repo/skill-name>');
|
|
173
|
+
console.log(' Remove: npm run skills remove <owner/repo/skill-name>\n');
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log('\n This will add a "skills" script to your package.json:');
|
|
178
|
+
console.log(' "scripts": { "skills": "st-skills" }');
|
|
179
|
+
console.log('\n Then use: npm run skills add <owner/repo/skill-name>');
|
|
180
|
+
console.log(' Auto-syncs .cursor/commands/project-skill-tags.md after every add/remove.\n');
|
|
181
|
+
|
|
182
|
+
const yes = await confirm({
|
|
183
|
+
message: 'Add the "skills" script to package.json?',
|
|
184
|
+
default: true,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (!yes) {
|
|
188
|
+
console.log('\n Skipped.\n');
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
pkg.scripts = { ...scripts, skills: 'st-skills' };
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.error(`\n Failed to write package.json: ${err.message}\n`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log('\n ✓ Added "skills": "st-skills" to package.json');
|
|
202
|
+
console.log('\n Running initial project sync...\n');
|
|
203
|
+
|
|
204
|
+
const result = spawnSync(
|
|
205
|
+
'node',
|
|
206
|
+
[path.join(__dirname, 'skill-tags.js'), '--local'],
|
|
207
|
+
{ stdio: 'inherit', cwd: process.cwd() }
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
if (result.error) {
|
|
211
|
+
console.error(`\n Warning: initial sync failed — ${result.error.message}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
console.log('\n Setup complete!\n');
|
|
215
|
+
console.log(' Add a skill: npm run skills add <owner/repo/skill-name>');
|
|
216
|
+
console.log(' Remove: npm run skills remove <owner/repo/skill-name>');
|
|
217
|
+
console.log(' Manual sync: skill-tags --local\n');
|
|
218
|
+
}
|
package/bin/skill-tags.js
CHANGED
|
@@ -7,12 +7,9 @@
|
|
|
7
7
|
const { spawnSync } = require('child_process');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const fs = require('fs');
|
|
10
|
-
const os = require('os');
|
|
11
10
|
|
|
12
11
|
const VERSION = require('../package.json').version;
|
|
13
|
-
const HOME = os.homedir();
|
|
14
12
|
const SYNC_SCRIPT = path.join(__dirname, '..', 'sync.sh');
|
|
15
|
-
const WRAPPER_MARKER = '# ─── skill-tags / Cursor Skill Command Sync';
|
|
16
13
|
|
|
17
14
|
// ─── --version ───────────────────────────────────────────────────────────────
|
|
18
15
|
|
|
@@ -40,9 +37,11 @@ if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
|
40
37
|
Options:
|
|
41
38
|
(none) Scan all skill sources and generate skill-tags.md
|
|
42
39
|
--categories Interactive category wizard (arrow keys + space to toggle)
|
|
43
|
-
--
|
|
44
|
-
--
|
|
45
|
-
--
|
|
40
|
+
--categories --local Category wizard for project-level skills only
|
|
41
|
+
--global Skip local skills (.agents/skills in CWD); scan global sources only
|
|
42
|
+
--local Scan only .agents/skills in CWD; write to .cursor/commands/project-skill-tags.md
|
|
43
|
+
--setup Interactive setup: choose Global (shell profile) or Project (package.json)
|
|
44
|
+
--latest Update skill-tags to the latest version
|
|
46
45
|
--version, -v Print version
|
|
47
46
|
--help, -h Show this help
|
|
48
47
|
|
|
@@ -55,12 +54,23 @@ if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
|
55
54
|
./.agents/skills/ project-level skills (current directory)
|
|
56
55
|
|
|
57
56
|
Output:
|
|
58
|
-
~/.cursor/commands/skill-tags.md
|
|
59
|
-
~/.cursor/commands/skills-<category>.md
|
|
60
|
-
./.cursor/commands/skill-tags.md
|
|
57
|
+
~/.cursor/commands/skill-tags.md full index of all skills (global)
|
|
58
|
+
~/.cursor/commands/skills-<category>.md generated by --categories
|
|
59
|
+
./.cursor/commands/project-skill-tags.md generated by --local
|
|
60
|
+
./.cursor/commands/skills-<category>.md generated by --categories --local
|
|
61
61
|
|
|
62
62
|
Config:
|
|
63
|
-
~/.cursor/skill-tags-categories.conf
|
|
63
|
+
~/.cursor/skill-tags-categories.conf global category config
|
|
64
|
+
./.cursor/skill-tags-categories.conf project category config (--categories --local)
|
|
65
|
+
|
|
66
|
+
Setup modes:
|
|
67
|
+
Global Adds skills() wrapper to ~/.zshrc — use: skills add <pkg>
|
|
68
|
+
Project Adds "skills": "st-skills" to package.json — use: npm run skills add <pkg>
|
|
69
|
+
|
|
70
|
+
Project binary (st-skills):
|
|
71
|
+
st-skills add <pkg> Install a project skill and auto-sync
|
|
72
|
+
st-skills remove <pkg> Remove a project skill and auto-sync
|
|
73
|
+
st-skills update <pkg> Update a project skill and auto-sync
|
|
64
74
|
`);
|
|
65
75
|
process.exit(0);
|
|
66
76
|
}
|
|
@@ -71,9 +81,68 @@ if (process.argv.includes('--categories')) {
|
|
|
71
81
|
require('./categories');
|
|
72
82
|
}
|
|
73
83
|
|
|
84
|
+
// ─── --latest ────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
else if (process.argv.includes('--latest')) {
|
|
87
|
+
const https = require('https');
|
|
88
|
+
|
|
89
|
+
function getLatestVersion() {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
https.get('https://registry.npmjs.org/skill-tags/latest', { headers: { Accept: 'application/json' } }, res => {
|
|
92
|
+
let data = '';
|
|
93
|
+
res.on('data', chunk => { data += chunk; });
|
|
94
|
+
res.on('end', () => {
|
|
95
|
+
try { resolve(JSON.parse(data).version); }
|
|
96
|
+
catch { reject(new Error('Failed to parse registry response')); }
|
|
97
|
+
});
|
|
98
|
+
}).on('error', reject);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
(async () => {
|
|
103
|
+
console.log(`\n skill-tags v${VERSION}\n`);
|
|
104
|
+
|
|
105
|
+
let latest;
|
|
106
|
+
try {
|
|
107
|
+
process.stdout.write(' Checking for updates...');
|
|
108
|
+
latest = await getLatestVersion();
|
|
109
|
+
process.stdout.write(` v${latest}\n`);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error(`\n Could not reach npm registry: ${err.message}\n`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (latest === VERSION) {
|
|
116
|
+
console.log(' Already on the latest version.\n');
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(` Updating: v${VERSION} → v${latest}\n`);
|
|
121
|
+
const result = spawnSync('npm', ['install', '-g', 'skill-tags@latest'], {
|
|
122
|
+
stdio: 'inherit',
|
|
123
|
+
shell: true,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (result.error) {
|
|
127
|
+
console.error(`\n Update failed: ${result.error.message}`);
|
|
128
|
+
console.error(' Try manually: npm install -g skill-tags@latest\n');
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (result.status !== 0) {
|
|
133
|
+
console.error('\n Update failed. You may need to run with sudo:');
|
|
134
|
+
console.error(' sudo npm install -g skill-tags@latest\n');
|
|
135
|
+
process.exit(result.status);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(`\n ✓ Updated to v${latest}\n`);
|
|
139
|
+
})();
|
|
140
|
+
}
|
|
141
|
+
|
|
74
142
|
// ─── --setup ─────────────────────────────────────────────────────────────────
|
|
75
143
|
|
|
76
144
|
else if (process.argv.includes('--setup')) {
|
|
145
|
+
const runSetup = require('./setup');
|
|
77
146
|
runSetup().catch(err => {
|
|
78
147
|
if (err.name === 'ExitPromptError') { console.log(); process.exit(0); }
|
|
79
148
|
console.error(' Error:', err.message);
|
|
@@ -100,72 +169,3 @@ else {
|
|
|
100
169
|
|
|
101
170
|
process.exit(result.status ?? 0);
|
|
102
171
|
}
|
|
103
|
-
|
|
104
|
-
// ─── Setup implementation ────────────────────────────────────────────────────
|
|
105
|
-
|
|
106
|
-
async function runSetup() {
|
|
107
|
-
const { confirm } = require('@inquirer/prompts');
|
|
108
|
-
|
|
109
|
-
const shellName = path.basename(process.env.SHELL || 'bash');
|
|
110
|
-
let rcFile;
|
|
111
|
-
if (shellName === 'zsh') rcFile = path.join(HOME, '.zshrc');
|
|
112
|
-
else if (process.platform === 'darwin') rcFile = path.join(HOME, '.bash_profile');
|
|
113
|
-
else rcFile = path.join(HOME, '.bashrc');
|
|
114
|
-
|
|
115
|
-
const displayRc = rcFile.replace(HOME, '~');
|
|
116
|
-
|
|
117
|
-
console.log(`\n skill-tags: shell setup\n`);
|
|
118
|
-
|
|
119
|
-
let alreadyInstalled = false;
|
|
120
|
-
try {
|
|
121
|
-
const content = fs.readFileSync(rcFile, 'utf-8');
|
|
122
|
-
if (content.includes(WRAPPER_MARKER)) alreadyInstalled = true;
|
|
123
|
-
} catch {}
|
|
124
|
-
|
|
125
|
-
if (alreadyInstalled) {
|
|
126
|
-
console.log(` ✓ Shell wrapper already installed in ${displayRc}\n`);
|
|
127
|
-
process.exit(0);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
console.log(` This will add a skills() shell wrapper to ${displayRc}`);
|
|
131
|
-
console.log(` It auto-syncs skill-tags.md after every skills add/remove.\n`);
|
|
132
|
-
|
|
133
|
-
const yes = await confirm({
|
|
134
|
-
message: `Add the wrapper to ${displayRc}?`,
|
|
135
|
-
default: true,
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
if (!yes) {
|
|
139
|
-
console.log('\n Skipped.\n');
|
|
140
|
-
process.exit(0);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const syncPath = fs.existsSync(path.join(HOME, '.cursor', 'sync-skill-commands.sh'))
|
|
144
|
-
? path.join(HOME, '.cursor', 'sync-skill-commands.sh')
|
|
145
|
-
: SYNC_SCRIPT;
|
|
146
|
-
|
|
147
|
-
const wrapper = `
|
|
148
|
-
${WRAPPER_MARKER} ────────────────────────────────────────────────
|
|
149
|
-
# Wraps \`npx skills\` to auto-generate skill-tags.md after install/removal.
|
|
150
|
-
# Run manually: skill-tags (or: bash ${syncPath})
|
|
151
|
-
function skills() {
|
|
152
|
-
npx skills "$@"
|
|
153
|
-
local exit_code=$?
|
|
154
|
-
if [[ "$1" == "add" || "$1" == "remove" ]] && [[ $exit_code -eq 0 ]]; then
|
|
155
|
-
bash "${syncPath}"
|
|
156
|
-
fi
|
|
157
|
-
return $exit_code
|
|
158
|
-
}
|
|
159
|
-
# ─────────────────────────────────────────────────────────────────────────────
|
|
160
|
-
`;
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
fs.appendFileSync(rcFile, wrapper);
|
|
164
|
-
} catch (err) {
|
|
165
|
-
console.error(`\n Failed to write to ${displayRc}: ${err.message}\n`);
|
|
166
|
-
process.exit(1);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
console.log(`\n ✓ Added skills() wrapper to ${displayRc}`);
|
|
170
|
-
console.log(` Reload with: source ${displayRc}\n`);
|
|
171
|
-
}
|
package/bin/st-skills.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bin/st-skills.js
|
|
3
|
+
// Project-level skills wrapper: runs npx skills and auto-syncs project-skill-tags.md on add/remove/update.
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const subcommand = args[0];
|
|
12
|
+
|
|
13
|
+
// Run npx skills with all forwarded arguments
|
|
14
|
+
const result = spawnSync('npx', ['skills', ...args], { stdio: 'inherit', shell: false });
|
|
15
|
+
|
|
16
|
+
if (result.error) {
|
|
17
|
+
console.error(`\n st-skills: failed to run npx skills — ${result.error.message}\n`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const exitCode = result.status ?? 1;
|
|
22
|
+
|
|
23
|
+
// Auto-sync project-skill-tags.md on successful mutating subcommands
|
|
24
|
+
if (['add', 'remove', 'update'].includes(subcommand) && exitCode === 0) {
|
|
25
|
+
const syncResult = spawnSync(
|
|
26
|
+
'node',
|
|
27
|
+
[path.join(__dirname, 'skill-tags.js'), '--local'],
|
|
28
|
+
{ stdio: 'inherit', cwd: process.cwd() }
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (syncResult.error) {
|
|
32
|
+
console.error(`\n st-skills: sync failed — ${syncResult.error.message}\n`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
process.exit(exitCode);
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skill-tags",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Generates consolidated command files (@skill-tags.md and custom @skills-<category>.md indexes) for all installed Cursor skills.",
|
|
5
5
|
"bin": {
|
|
6
|
-
"skill-tags": "bin/skill-tags.js"
|
|
6
|
+
"skill-tags": "bin/skill-tags.js",
|
|
7
|
+
"st-skills": "bin/st-skills.js"
|
|
7
8
|
},
|
|
8
9
|
"scripts": {
|
|
9
10
|
"postinstall": "node ./bin/postinstall.js"
|
package/sync.sh
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
set -euo pipefail
|
|
9
9
|
|
|
10
|
-
VERSION="1.
|
|
10
|
+
VERSION="1.5.0"
|
|
11
11
|
|
|
12
12
|
GLOBAL_COMMANDS_DIR="${HOME}/.cursor/commands"
|
|
13
13
|
OUTPUT_FILE="${GLOBAL_COMMANDS_DIR}/skill-tags.md"
|
|
14
14
|
CATEGORIES_CONFIG="${HOME}/.cursor/skill-tags-categories.conf"
|
|
15
15
|
PROJECT_COMMANDS_DIR="$(pwd)/.cursor/commands"
|
|
16
|
-
PROJECT_OUTPUT_FILE="${PROJECT_COMMANDS_DIR}/skill-tags.md"
|
|
16
|
+
PROJECT_OUTPUT_FILE="${PROJECT_COMMANDS_DIR}/project-skill-tags.md"
|
|
17
17
|
|
|
18
18
|
# ─── Priority-ordered skill source directories ─────────────────────────────────
|
|
19
19
|
# Earlier entries take priority when the same skill name exists in multiple locations.
|
|
@@ -36,11 +36,13 @@ trap 'rm -f "$SKILLS_TEMP"; rm -rf "$SKILLS_META_DIR"' EXIT
|
|
|
36
36
|
|
|
37
37
|
GLOBAL_ONLY=false
|
|
38
38
|
PROJECT_ONLY=false
|
|
39
|
+
QUIET=false
|
|
39
40
|
|
|
40
41
|
for arg in "$@"; do
|
|
41
42
|
case "$arg" in
|
|
42
|
-
--global
|
|
43
|
-
--
|
|
43
|
+
--global) GLOBAL_ONLY=true ;;
|
|
44
|
+
--local) PROJECT_ONLY=true ;;
|
|
45
|
+
--quiet) QUIET=true ;;
|
|
44
46
|
esac
|
|
45
47
|
done
|
|
46
48
|
|
|
@@ -49,8 +51,8 @@ done
|
|
|
49
51
|
count_found=0
|
|
50
52
|
count_dupes=0
|
|
51
53
|
|
|
52
|
-
log() { printf " %s\n" "$*"; }
|
|
53
|
-
success() { printf " ✓ %s\n" "$*"; }
|
|
54
|
+
log() { [[ "$QUIET" == "true" ]] || printf " %s\n" "$*"; }
|
|
55
|
+
success() { [[ "$QUIET" == "true" ]] || printf " ✓ %s\n" "$*"; }
|
|
54
56
|
|
|
55
57
|
# Tracks which skill names have already been processed (deduplication).
|
|
56
58
|
# Uses a delimited string for bash 3.2 compatibility (no associative arrays).
|
|
@@ -192,10 +194,14 @@ scan_tree() {
|
|
|
192
194
|
|
|
193
195
|
# ─── Generate category files ────────────────────────────────────────────────────
|
|
194
196
|
|
|
195
|
-
# Reads
|
|
196
|
-
#
|
|
197
|
+
# Reads a categories config and writes a skills-<category>.md command file for each.
|
|
198
|
+
# Usage: generate_category_files [config_file] [commands_dir]
|
|
199
|
+
# Defaults to global paths when called without arguments.
|
|
197
200
|
generate_category_files() {
|
|
198
|
-
|
|
201
|
+
local config_file="${1:-$CATEGORIES_CONFIG}"
|
|
202
|
+
local commands_dir="${2:-$GLOBAL_COMMANDS_DIR}"
|
|
203
|
+
|
|
204
|
+
[[ -f "$config_file" ]] || return 0
|
|
199
205
|
|
|
200
206
|
local gen_count=0
|
|
201
207
|
|
|
@@ -205,7 +211,7 @@ generate_category_files() {
|
|
|
205
211
|
|
|
206
212
|
local title
|
|
207
213
|
title="$(to_title_case "$cat_name")"
|
|
208
|
-
local out="${
|
|
214
|
+
local out="${commands_dir}/skills-${cat_name}.md"
|
|
209
215
|
|
|
210
216
|
local skills_section=""
|
|
211
217
|
while IFS= read -r sname; do
|
|
@@ -247,19 +253,21 @@ EOF
|
|
|
247
253
|
|
|
248
254
|
gen_count=$(( gen_count + 1 ))
|
|
249
255
|
success "Generated: ${out/#$HOME/~}"
|
|
250
|
-
done < "$
|
|
256
|
+
done < "$config_file"
|
|
251
257
|
|
|
252
|
-
if [[ $gen_count -gt 0 ]]; then
|
|
258
|
+
if [[ $gen_count -gt 0 && "$QUIET" != "true" ]]; then
|
|
253
259
|
printf " Category files: %d generated\n" "$gen_count"
|
|
254
260
|
fi
|
|
255
261
|
}
|
|
256
262
|
|
|
257
263
|
# ─── Main ──────────────────────────────────────────────────────────────────────
|
|
258
264
|
|
|
259
|
-
|
|
265
|
+
if [[ "$QUIET" != "true" ]]; then
|
|
266
|
+
printf "\n skill-tags v%s — syncing skills\n\n" "$VERSION"
|
|
267
|
+
fi
|
|
260
268
|
|
|
261
269
|
if [[ "$PROJECT_ONLY" == "true" ]]; then
|
|
262
|
-
# ─── --
|
|
270
|
+
# ─── --local: scan only .agents/skills in CWD ───────────────────────────────
|
|
263
271
|
PROJECT_SKILLS_DIR="$(pwd)/.agents/skills"
|
|
264
272
|
|
|
265
273
|
if [[ ! -d "$PROJECT_SKILLS_DIR" ]]; then
|
|
@@ -297,14 +305,19 @@ ${OPENING}
|
|
|
297
305
|
$(cat "$SKILLS_TEMP")
|
|
298
306
|
EOF
|
|
299
307
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
printf "
|
|
308
|
+
PROJECT_CATEGORIES_CONFIG="$(pwd)/.cursor/skill-tags-categories.conf"
|
|
309
|
+
generate_category_files "$PROJECT_CATEGORIES_CONFIG" "$PROJECT_COMMANDS_DIR"
|
|
310
|
+
|
|
311
|
+
if [[ "$QUIET" != "true" ]]; then
|
|
312
|
+
printf "\n"
|
|
313
|
+
if [[ "$is_update" == "true" ]]; then
|
|
314
|
+
printf " ✓ Updated: .cursor/commands/project-skill-tags.md\n"
|
|
315
|
+
else
|
|
316
|
+
printf " ✓ Generated: .cursor/commands/project-skill-tags.md\n"
|
|
317
|
+
fi
|
|
318
|
+
printf " Skills: %d indexed\n" "$count_found"
|
|
319
|
+
printf "\n Tip: type @project-skill-tags.md in Cursor chat to load the project skills reference.\n\n"
|
|
305
320
|
fi
|
|
306
|
-
printf " Skills: %d indexed\n" "$count_found"
|
|
307
|
-
printf "\n Tip: type @skill-tags.md in Cursor chat to load the project skills reference.\n\n"
|
|
308
321
|
|
|
309
322
|
else
|
|
310
323
|
# ─── Default: scan all sources ───────────────────────────────────────────────
|
|
@@ -352,16 +365,18 @@ EOF
|
|
|
352
365
|
|
|
353
366
|
generate_category_files
|
|
354
367
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
368
|
+
if [[ "$QUIET" != "true" ]]; then
|
|
369
|
+
printf "\n"
|
|
370
|
+
if [[ "$is_update" == "true" ]]; then
|
|
371
|
+
printf " ✓ Updated: %s\n" "${OUTPUT_FILE/#$HOME/~}"
|
|
372
|
+
else
|
|
373
|
+
printf " ✓ Generated: %s\n" "${OUTPUT_FILE/#$HOME/~}"
|
|
374
|
+
fi
|
|
375
|
+
printf " Skills: %d indexed\n" "$count_found"
|
|
376
|
+
if [[ $count_dupes -gt 0 ]]; then
|
|
377
|
+
printf " Duplicates: %d skipped (covered by higher-priority source)\n" "$count_dupes"
|
|
378
|
+
fi
|
|
379
|
+
printf "\n Tip: type @skill-tags.md in Cursor chat to load the full skills reference.\n\n"
|
|
364
380
|
fi
|
|
365
|
-
printf "\n Tip: type @skill-tags.md in Cursor chat to load the full skills reference.\n\n"
|
|
366
381
|
|
|
367
382
|
fi
|