skillex 0.3.1 → 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.
Files changed (51) hide show
  1. package/CHANGELOG.md +262 -1
  2. package/README.md +57 -10
  3. package/dist/auto-sync.d.ts +66 -0
  4. package/dist/auto-sync.js +91 -0
  5. package/dist/catalog.js +5 -29
  6. package/dist/cli.d.ts +13 -0
  7. package/dist/cli.js +247 -141
  8. package/dist/confirm.js +3 -1
  9. package/dist/direct-github.d.ts +60 -0
  10. package/dist/direct-github.js +177 -0
  11. package/dist/doctor.d.ts +31 -0
  12. package/dist/doctor.js +172 -0
  13. package/dist/downloader.d.ts +42 -0
  14. package/dist/downloader.js +41 -0
  15. package/dist/fs.d.ts +21 -1
  16. package/dist/fs.js +30 -3
  17. package/dist/http.d.ts +28 -7
  18. package/dist/http.js +143 -42
  19. package/dist/install.d.ts +23 -9
  20. package/dist/install.js +75 -348
  21. package/dist/lockfile.d.ts +46 -0
  22. package/dist/lockfile.js +169 -0
  23. package/dist/output.d.ts +11 -0
  24. package/dist/output.js +49 -0
  25. package/dist/recommended.d.ts +13 -0
  26. package/dist/recommended.js +21 -0
  27. package/dist/runner.js +9 -9
  28. package/dist/skill.d.ts +2 -0
  29. package/dist/skill.js +3 -0
  30. package/dist/sync.js +12 -9
  31. package/dist/types.d.ts +39 -0
  32. package/dist/types.js +28 -0
  33. package/dist/ui.js +1 -1
  34. package/dist/user-config.d.ts +5 -0
  35. package/dist/user-config.js +22 -1
  36. package/dist/web-ui.js +5 -0
  37. package/dist-ui/assets/CatalogPage-CbtMTkxd.js +1 -0
  38. package/dist-ui/assets/CatalogPage-W5MqylAz.css +1 -0
  39. package/dist-ui/assets/DoctorPage-oUZyX91t.js +1 -0
  40. package/dist-ui/assets/Skeleton-B_xm5L3P.js +1 -0
  41. package/dist-ui/assets/Skeleton-_Ooiw1nN.css +1 -0
  42. package/dist-ui/assets/SkillDetailPage-5JHQLq3q.js +1 -0
  43. package/dist-ui/assets/SkillDetailPage-CBAaWpcc.css +1 -0
  44. package/dist-ui/assets/{index-UBECch6X.css → index-CWm7zQTg.css} +1 -1
  45. package/dist-ui/assets/index-I0b-syhc.js +26 -0
  46. package/dist-ui/assets/recommended-D_i10hwH.js +1 -0
  47. package/dist-ui/index.html +2 -2
  48. package/package.json +2 -2
  49. package/dist-ui/assets/CatalogPage-B_qic36n.js +0 -1
  50. package/dist-ui/assets/SkillDetailPage-BJ3onKk4.js +0 -1
  51. package/dist-ui/assets/index-DN-z--cR.js +0 -25
package/CHANGELOG.md CHANGED
@@ -7,6 +7,259 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-05-02
11
+
12
+ A substantial release focused on **reliability, security, and UX**. The CLI is
13
+ now production-grade (HTTP timeouts, host-restricted token, parallel
14
+ downloads, hardened parser); the Web UI grew bulk actions, keyboard
15
+ shortcuts, multi-select, persistent state, an accessible doctor panel, and a
16
+ mobile drawer; and the codebase was refactored for maintainability without
17
+ breaking the public API.
18
+
19
+ Test count went from 63 → 88 (no regressions).
20
+
21
+ ### Highlights
22
+
23
+ - **Reliability & security pass** — HTTP timeouts everywhere, GitHub token
24
+ scoped to GitHub-owned hosts, lockfile path safety, symlink confinement,
25
+ ref-character validation, mode `0o600` on `~/.askillrc.json`, parallel
26
+ file downloads (5–10× speedup on multi-file skills).
27
+ - **CLI parser hardened** — unknown flags rejected with "did you mean"
28
+ suggestions, `--` sentinel honored, missing-value detection, unknown
29
+ commands suggest the closest match, `parseBooleanFlag` names the flag and
30
+ lists accepted values, `skillex doctor` differentiates DNS / TLS /
31
+ refused / timeout failures.
32
+ - **Three new top-level commands / flags** — `skillex show <id>` previews a
33
+ skill's SKILL.md without installing, `skillex init --install-recommended`
34
+ ships a curated starter pack, `skillex sync --dry-run --exit-code`
35
+ mirrors `git diff --exit-code` for CI drift detection.
36
+ - **Web UI marketplace overhaul** — Install all + Install recommended +
37
+ Remove all bulk actions; per-card optimistic UI; Shift+click multi-select
38
+ with range select; sticky selection bar; persistent selection across
39
+ refreshes; search highlight; group-by-source; related skills; mobile
40
+ drawer; Doctor panel + sidebar health dot; breadcrumbs; first-load
41
+ skeletons; `⌘K` and `⇧⌘A` shortcuts.
42
+ - **Internationalization & regression guard** — every user-facing string
43
+ is now English; `scripts/check-language.mjs` runs in `npm test` and fails
44
+ CI if banned Portuguese tokens reappear without an explicit
45
+ `i18n-allow:` annotation.
46
+ - **Refactor** — `src/install.ts` (1326 LOC) split into focused modules
47
+ (`lockfile.ts`, `direct-github.ts`, `auto-sync.ts`, `downloader.ts`,
48
+ `doctor.ts`) with re-exports preserving every existing import path.
49
+
50
+ ### Added — Core / CLI
51
+
52
+ - `skillex show <skill-id>` — preview a skill's manifest + rendered SKILL.md
53
+ from the configured sources without installing. `--raw` prints the
54
+ markdown verbatim; `--json` returns the manifest plus the entry content
55
+ as a single object.
56
+ - `skillex init --install-recommended` — after writing the lockfile, install
57
+ a curated 5-skill starter pack (`commit-craft`, `code-review`,
58
+ `secure-defaults`, `error-handling`, `test-discipline`) using the same
59
+ progress bar as `install --all`. The recommended list lives in
60
+ `src/recommended.ts` (single source of truth).
61
+ - `skillex sync --dry-run --exit-code` — exit `1` whenever the dry-run
62
+ would change at least one adapter (mirrors `git diff --exit-code`). CI
63
+ scripts can detect drift without parsing the diff output.
64
+ - `--tags <tag>` is accepted as a hidden alias of `--tag` on
65
+ `skillex search` so previously documented examples keep working.
66
+ - `src/doctor.ts` exports `runDoctorChecks(options): Promise<DoctorReport>`
67
+ — the canonical six health checks reused by both the CLI and the Web UI.
68
+ - `HttpError` typed error class with codes `HTTP_TIMEOUT`,
69
+ `HTTP_RATE_LIMIT`, `HTTP_AUTH_FAILED`, `HTTP_NOT_FOUND`,
70
+ `HTTP_SERVER_ERROR`, and `HTTP_ERROR`.
71
+ - `RemoveSkillsResult.autoSyncs: SyncCommandResult[]` — full per-adapter
72
+ sync aggregate (the existing `autoSync` field is preserved as the first
73
+ result for backward compat).
74
+ - `createSymlink(target, link, { allowedRoot })` overload that refuses
75
+ targets resolving outside the managed root.
76
+ - `SkillManifest.category?: string` (optional). Catalog publishers can
77
+ group skills explicitly instead of relying on consumer-side regex
78
+ inference. Read from both `skill.json` and SKILL.md frontmatter.
79
+ - `suggestClosest(actual, candidates, threshold = 2)` helper in
80
+ `src/output.ts` powers "did you mean" hints across the parser and
81
+ dispatcher.
82
+
83
+ ### Added — Web UI
84
+
85
+ - **Doctor panel** + `GET /api/doctor` endpoint mirroring the CLI's six
86
+ checks, with a sidebar status dot (green / yellow / red + pulse)
87
+ refreshed after every mutation.
88
+ - **Bulk install / remove / install-recommended** buttons in the catalog
89
+ hero, each guarded by an accessible `ConfirmDialog` (role=dialog,
90
+ aria-modal, focus on Cancel, Esc cancels, Enter only fires when no
91
+ button has focus).
92
+ - **Per-card optimistic UI** — single-skill install / remove / update show
93
+ a localized spinner inside the card and dim only that card. Backed by
94
+ `state.busyCards: Set<string>` and a `runCardAction(skillId, label, fn)`
95
+ store helper. Bulk actions still use the global overlay.
96
+ - **Bulk select via Shift+click** — sticky selection bar shows
97
+ `N selected`, `Select all visible (M)` link, and `Install N` /
98
+ `Remove N` buttons that count only the eligible subset. Plain click in
99
+ selection mode toggles. Esc clears (priority cascade — see Changed).
100
+ - **Range select** — Shift+click on a second card selects the range
101
+ between the anchor and the target in the visible-id ordering
102
+ (Finder/Excel behavior). The anchor moves with each interaction so
103
+ successive Shift+clicks extend from the latest endpoint.
104
+ - **Persistent selection** — selection (ids + anchor + scope) is mirrored
105
+ to `localStorage["skillex.selection"]`. Restored on startup, filtered to
106
+ ids that still exist in the loaded catalog under the same scope.
107
+ - **Search highlight** — query matches in skill name and description are
108
+ wrapped in `<mark class="search-mark">` with an accent background. Pure
109
+ template rendering (no `v-html`) — XSS-safe.
110
+ - **Group-by-source toggle** — when more than one source is configured, a
111
+ toggle in the category-pill row buckets the grid by source.repo with
112
+ per-bucket headers and counts.
113
+ - **Related skills** on the SkillDetailPage — up to 4 cards scored by tag
114
+ overlap (+ 0.5 bonus for matching `category`). Cards are keyboard
115
+ accessible, show a 2-line description clamp, up to 3 tags, and a ✓ when
116
+ the related skill is already installed.
117
+ - **Breadcrumbs** on the SkillDetailPage —
118
+ `Catalog (link, home icon) / category / skill-name (current)`.
119
+ Category resolves from the explicit manifest field first, falls back to
120
+ the regex inference, hidden when no signal.
121
+ - **Onboarding card** for fresh workspaces — replaces the generic empty
122
+ state with a bright Install-all CTA + count when the workspace has zero
123
+ installed skills and no filter is active.
124
+ - **Installed-only filter** — the "Installed" overview card is now a
125
+ toggle (`role=button`, Enter/Space, `aria-pressed`); active state filters
126
+ the grid to installed skills only. The stat now reads `N / total`.
127
+ - **Inferred category chip** on cards whose category came from regex
128
+ fallback rather than an explicit manifest field.
129
+ - **First-load skeletons** — new `Skeleton.vue` (variants `card` / `row`).
130
+ Catalog page renders 6 card-skeletons until `state.catalog` resolves;
131
+ Doctor page renders 4 row-skeletons; SkillDetailPage renders metadata
132
+ + body + related skeletons during its first fetch.
133
+ - **Mobile drawer** — sidebar slides in/out on viewports ≤680 px instead
134
+ of being hidden entirely. Triggered by a topbar hamburger; tap backdrop
135
+ or Esc to close; route changes auto-close.
136
+ - **Adapter dropdown** for compact viewports (≤1100 px) — replaces the 7
137
+ icon row with a single trigger + listbox menu (`role=listbox`,
138
+ `role=option`, `aria-selected`).
139
+ - **`⌘K` / `Ctrl+K` shortcut** — focuses the topbar search. Navigates back
140
+ to `/` first when invoked from another route. Adaptive hint badge.
141
+ - **`⇧⌘A` / `Ctrl+Shift+A` shortcut** — opens the Install-all confirm on
142
+ the catalog page. Skipped when typing in inputs. Shift required so the
143
+ browser's native "select all" (Cmd+A) is preserved. Discoverable via the
144
+ shortcut chip on the Install-all button.
145
+ - **Demo media placeholder** — new `docs/media/` with a README describing
146
+ how to regenerate `tui.gif`, `web-ui.png`, `web-ui-doctor.png` (vhs +
147
+ asciinema). Main README has a "Demo" section that references the media
148
+ via raw GitHub URLs (no binaries in the npm tarball).
149
+ - **"Why Skillex" section** in the README plus a Quick Start that leads
150
+ with `npx skillex@latest` (the TUI) and frames the
151
+ `init` → `install` chain as scriptable mode.
152
+
153
+ ### Changed
154
+
155
+ - **Reliability:** all HTTP helpers now abort after a default 30-second
156
+ timeout when the caller does not provide an `AbortSignal`, raising
157
+ `HttpError("HTTP_TIMEOUT")` instead of hanging.
158
+ - **HTTP errors:** 403 responses split into `HTTP_RATE_LIMIT` (when
159
+ `X-RateLimit-Remaining: 0`) and `HTTP_AUTH_FAILED`, each with an
160
+ actionable message. The rate-limit message includes the reset hint.
161
+ - **`maybeSyncAfterRemove`** now runs adapter syncs in parallel via
162
+ `Promise.all` and returns the full aggregate; the CLI prints one line
163
+ per adapter (was previously dropping all but the last adapter's
164
+ outcome).
165
+ - **`toInstallError`** preserves the underlying `error.code` (HTTP error
166
+ codes, `EACCES`, `ENOENT`, etc.) on the wrapped `InstallError`.
167
+ - **CLI parser** is now schema-driven via `STRING_FLAGS` / `BOOLEAN_FLAGS`
168
+ / `KNOWN_FLAGS` sets. Unknown flags raise `CliError("UNKNOWN_FLAG")`
169
+ with a Levenshtein-based "did you mean" suggestion (typos like
170
+ `--scop=global` are no longer silently accepted).
171
+ - **CLI parser** detects missing values for string flags and raises
172
+ `CliError("MISSING_FLAG_VALUE")` naming the offending flag.
173
+ - **CLI parser** honors the literal `--` end-of-options sentinel and
174
+ exposes everything after it via `ParsedArgs.positionalAfter`, so
175
+ `skillex run x:cmd -- --foo` forwards `--foo` to the underlying script
176
+ without flag interpretation.
177
+ - **CLI dispatcher** suggests the closest match for unknown commands
178
+ (e.g. `skillex insall git-master` → `Did you mean: install?`).
179
+ - **`parseBooleanFlag`** error now names the flag and lists every accepted
180
+ value: `Invalid value "maybe" for --auto-sync. Use true, false, yes,
181
+ no, on, off, 1, or 0.`
182
+ - **`INSTALL_NO_TARGETS`** (`skillex install` with no args) now prints a
183
+ 3-line inline usage block instead of a one-liner hint.
184
+ - **`skillex doctor`** differentiates DNS, connection-refused, TLS,
185
+ timeout, and 5xx failures and surfaces the underlying `error.message`
186
+ instead of collapsing every failure into "GitHub API is unreachable".
187
+ - **`skillex init`** ends with a three-line "Next steps" block (TUI /
188
+ starter pack / full catalog) instead of a single line. Suppressed when
189
+ `--install-recommended` was used.
190
+ - **SkillDetailPage** action order rewritten following "primary action
191
+ last": Sync (ghost) → Update (secondary, only when installed) →
192
+ Install (primary) or Remove (danger).
193
+ - **Web UI Esc cascade** — the Esc key now follows a priority chain:
194
+ ConfirmDialog → adapter dropdown → mobile drawer → CatalogPage
195
+ selection. Each handler checks `event.defaultPrevented` before acting,
196
+ so only one consumer fires per keypress.
197
+ - **Refactor:** `src/install.ts` (1326 LOC) split into `src/lockfile.ts`,
198
+ `src/direct-github.ts`, `src/auto-sync.ts`, `src/downloader.ts`. Public
199
+ `package.json#exports` and import paths preserved via re-exports.
200
+ - Consolidated SKILL.md frontmatter parsing on `parseSkillFrontmatter`
201
+ (`src/skill.ts`); the duplicated inline parser in `src/catalog.ts` was
202
+ removed.
203
+
204
+ ### Fixed
205
+
206
+ - All remaining Portuguese user-facing strings translated to English: TUI
207
+ prompt label, sync symlink-fallback warnings, runner errors,
208
+ direct-install confirmation, non-TTY confirm prompt, filesystem path
209
+ errors, and Web UI labels (sidebar, catalog, skill card buttons,
210
+ detail page).
211
+ - Sync warnings now route through `output.warn` instead of bare
212
+ `console.error` so they respect color and stream conventions.
213
+ - New `scripts/check-language.mjs` regression guard runs as part of
214
+ `npm test` and fails CI if banned Portuguese tokens reappear in
215
+ `src/**/*.ts` or `ui/src/**/*.{vue,ts}` without an explicit
216
+ `i18n-allow:` annotation.
217
+ - `toLockfileSource` boolean expression no longer contains a duplicated
218
+ `(label || repo === DEFAULT_REPO)` test (copy-paste artifact); behavior
219
+ unchanged.
220
+ - **README** example at the search section now uses the canonical
221
+ `--tag <tag>` flag instead of the silently-ignored `--tags`.
222
+ - **Web UI:** version badge in the sidebar reads the actual
223
+ `package.json#version` at build time via
224
+ `import.meta.env.VITE_SKILLEX_VERSION` instead of the previously
225
+ hardcoded `v0.2.4` (with a tautological ternary).
226
+ - **Web UI:** dead `⌘K` hint badge removed (no shortcut was wired) — and
227
+ brought back once `Cmd+K` was actually implemented.
228
+ - **Web UI:** misleading "Oficial" badge that every skill card displayed
229
+ (without any verification model) removed.
230
+ - **Web UI:** mobile (≤680 px) sidebar no longer disappears entirely.
231
+ - **Web UI:** `Remover` and `Carregando detalhes...` strings on the detail
232
+ page translated to English.
233
+
234
+ ### Security
235
+
236
+ - **Token confinement:** `Authorization: Bearer ${GITHUB_TOKEN}` is only
237
+ attached when the request host is `api.github.com`,
238
+ `raw.githubusercontent.com`, or any `*.githubusercontent.com` mirror.
239
+ Tokens are no longer leaked to third-party `--catalog-url` targets.
240
+ - **File mode:** `~/.askillrc.json` (which may contain `githubToken`) is
241
+ written with mode `0o600`. Existing world-readable files are tightened
242
+ on next save with a one-time warning.
243
+ - **Lockfile path safety:** `removeSkill` validates `metadata.path`
244
+ against the managed skills store before deleting; tampered lockfile
245
+ paths raise `INSTALL_PATH_UNSAFE` instead of removing arbitrary
246
+ directories.
247
+ - **Symlink confinement:** sync per-skill symlinks now refuse targets
248
+ that resolve outside the workspace state directory.
249
+ - **Direct-install ref:** `parseDirectGitHubRef` validates the ref segment
250
+ against `^[A-Za-z0-9_.\-/]+$` and rejects empty refs after `@`;
251
+ previously dangerous refs would land in the lockfile.
252
+ - **Web UI avatars:** skill author avatars are now a deterministic
253
+ CSS-only initials chip. The previous `<img>` to `dicebear.com` is gone
254
+ — the UI works offline and no longer leaks page-load telemetry to a
255
+ third-party host.
256
+
257
+ ### Performance
258
+
259
+ - **Parallel file downloads:** `downloadSkillFiles` fetches every file in
260
+ a skill via `Promise.all` instead of a sequential loop. Multi-file
261
+ skills now scale with bandwidth instead of file count.
262
+
10
263
  ## [0.3.1] - 2026-04-08
11
264
 
12
265
  ### Fixed
@@ -127,7 +380,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
127
380
  - Adapter detection and managed-block sync for Copilot, Cline, Cursor, Claude, Gemini, Windsurf, Codex
128
381
  - Lockfile-based workspace state at `.agent-skills/skills.json`
129
382
 
130
- [Unreleased]: https://github.com/lgili/skillex/compare/v0.2.0...HEAD
383
+ [Unreleased]: https://github.com/lgili/skillex/compare/v0.4.0...HEAD
384
+ [0.4.0]: https://github.com/lgili/skillex/compare/v0.3.1...v0.4.0
385
+ [0.3.1]: https://github.com/lgili/skillex/compare/v0.3.0...v0.3.1
386
+ [0.3.0]: https://github.com/lgili/skillex/compare/v0.2.5...v0.3.0
387
+ [0.2.5]: https://github.com/lgili/skillex/compare/v0.2.4...v0.2.5
388
+ [0.2.4]: https://github.com/lgili/skillex/compare/v0.2.3...v0.2.4
389
+ [0.2.3]: https://github.com/lgili/skillex/compare/v0.2.2...v0.2.3
390
+ [0.2.2]: https://github.com/lgili/skillex/compare/v0.2.1...v0.2.2
391
+ [0.2.1]: https://github.com/lgili/skillex/compare/v0.2.0...v0.2.1
131
392
  [0.2.0]: https://github.com/lgili/skillex/compare/v0.1.1...v0.2.0
132
393
  [0.1.1]: https://github.com/lgili/skillex/compare/v0.1.0...v0.1.1
133
394
  [0.1.0]: https://github.com/lgili/skillex/releases/tag/v0.1.0
package/README.md CHANGED
@@ -28,21 +28,40 @@
28
28
 
29
29
  ---
30
30
 
31
+ ## Why Skillex?
32
+
33
+ - **One install, every agent.** Skills get exposed to Claude / Codex / Cursor / Copilot / Cline / Gemini / Windsurf at once, instead of copy-pasting the same prompt into seven config files.
34
+ - **Catalog-driven, not folder-driven.** Skills live in versioned GitHub repos with manifests, descriptions, and tags. Update them with one command instead of `git submodule` gymnastics.
35
+ - **Lockfile + auto-sync.** Skillex tracks what's installed and re-syncs after every change so the agent surface in your repo always matches the lockfile.
36
+
31
37
  ## Quick Start
32
38
 
33
39
  Get up and running in under two minutes using the built-in first-party skills catalog:
34
40
 
35
41
  ```bash
36
- # 1. Initialize your workspace (auto-detects your AI agent)
37
- npx skillex@latest init
42
+ # Easiest: open the interactive terminal browser
43
+ npx skillex@latest
38
44
 
39
- # 2. Browse available skills
40
- npx skillex@latest list
41
-
42
- # 3. Install a skill
43
- npx skillex@latest install create-skills
45
+ # Or, scriptable mode:
46
+ npx skillex@latest init # set up the workspace
47
+ npx skillex@latest init --install-recommended # ...with a curated starter pack
48
+ npx skillex@latest install create-skills # install one skill by id
49
+ npx skillex@latest show code-review # preview a skill before installing
50
+ npx skillex@latest list # list every available skill
44
51
  ```
45
52
 
53
+ > **Tip:** after the first `npx skillex@latest` call, the binary is cached on your machine, so subsequent invocations can drop the `npx skillex@latest` prefix and just call `skillex`.
54
+
55
+ ## Demo
56
+
57
+ | Surface | Preview |
58
+ |---------|---------|
59
+ | Terminal browser (`skillex` with no args) | ![TUI demo](https://raw.githubusercontent.com/lgili/skillex/main/docs/media/tui.gif) |
60
+ | Web UI catalog page (`skillex ui`) | ![Web UI catalog](https://raw.githubusercontent.com/lgili/skillex/main/docs/media/web-ui.png) |
61
+ | Web UI Doctor panel | ![Web UI Doctor](https://raw.githubusercontent.com/lgili/skillex/main/docs/media/web-ui-doctor.png) |
62
+
63
+ > The media files live in [`docs/media/`](https://github.com/lgili/skillex/tree/main/docs/media) and are referenced via raw GitHub URLs so the npm tarball stays small. See [`docs/media/README.md`](https://github.com/lgili/skillex/blob/main/docs/media/README.md) for re-recording instructions.
64
+
46
65
  > **Important:** auto-sync is enabled by default. After `init`, `install`, `update`, and `remove`, Skillex automatically synchronizes skills into every detected adapter target. For directory-native adapters such as Codex, Claude, and Gemini, this materializes one folder per skill under the agent's `skills/` directory. For file-based adapters such as Copilot, Cursor, Cline, and Windsurf, it updates the adapter config file. Use `skillex sync` when you want to preview, re-run manually, or target a specific adapter.
47
66
 
48
67
  After `init`, Skillex saves the configured source list in the local lockfile. New workspaces start with `lgili/skillex@main` by default, and you can add more sources later with `skillex source add`.
@@ -110,6 +129,7 @@ skillex init --global --adapter codex
110
129
  | `--repo <owner/repo>` | Optional. Overrides the default first-party source for this workspace. |
111
130
  | `--adapter <id>` | Force a specific adapter instead of auto-detecting. |
112
131
  | `--auto-sync` | Enable or disable automatic sync after install, update, and remove. Default: `true`. |
132
+ | `--install-recommended` | After init, install a curated starter pack (`commit-craft`, `code-review`, `secure-defaults`, `error-handling`, `test-discipline`). |
113
133
  | `--ref <branch>` | Use a specific branch or tag (default: `main`). |
114
134
  | `--scope <local\|global>` | Choose whether Skillex manages workspace or user-global state. |
115
135
  | `--global` | Shortcut for `--scope global`. |
@@ -150,7 +170,7 @@ skillex search code-review --repo myorg/my-skills
150
170
  |------|-------------|
151
171
  | `--repo <owner/repo>` | Limit the command to a single source instead of aggregating all configured sources. |
152
172
  | `--compatibility <adapter>` | Filter by adapter (e.g. `cursor`, `claude`, `codex`). |
153
- | `--tags <tag>` | Filter by tag. |
173
+ | `--tag <tag>` | Filter by tag. (`--tags` is also accepted for backward compatibility with earlier docs.) |
154
174
  | `--json` | Print raw JSON. |
155
175
 
156
176
  ---
@@ -271,11 +291,34 @@ Execute a script declared in a skill's `skill.json`.
271
291
 
272
292
  ```bash
273
293
  skillex run git-master:cleanup
274
- skillex run git-master:cleanup --yes # skip confirmation
294
+ skillex run git-master:cleanup --yes # skip confirmation
295
+ skillex run git-master:cleanup -- --extra=arg # forward "--extra=arg" to the script
275
296
  ```
276
297
 
277
298
  ---
278
299
 
300
+ ### `show`
301
+
302
+ Print a skill's manifest summary plus its rendered `SKILL.md` content from the
303
+ configured catalog sources, **without installing anything**. Use this to
304
+ preview a skill before deciding to install.
305
+
306
+ ```bash
307
+ skillex show git-master # human-friendly summary + content
308
+ skillex show code-review --raw # SKILL.md verbatim, no header
309
+ skillex show secure-defaults --json # manifest + entry content as JSON
310
+ skillex show foo --repo myorg/my-skills # disambiguate when multiple sources match
311
+ ```
312
+
313
+ | Flag | Description |
314
+ |------|-------------|
315
+ | `--repo <owner/repo>` | Limit resolution to one source. |
316
+ | `--raw` | Print SKILL.md verbatim with no manifest header. |
317
+ | `--json` | Print the resolved manifest plus the raw SKILL.md as a single JSON object. |
318
+ | `--no-cache` | Bypass local catalog cache. |
319
+
320
+ ---
321
+
279
322
  ### Default terminal browser
280
323
 
281
324
  Running `skillex` with no subcommand now opens the interactive terminal browser by default.
@@ -481,6 +524,7 @@ catalog.json ← optional but recommended
481
524
  "description": "Teaches the agent to write semantic commits and manage branches.",
482
525
  "author": "your-name",
483
526
  "tags": ["git", "workflow"],
527
+ "category": "workflow",
484
528
  "compatibility": ["codex", "copilot", "cline", "cursor", "claude", "gemini", "windsurf"],
485
529
  "entry": "SKILL.md",
486
530
  "files": ["SKILL.md", "tools/git-cleanup.js"],
@@ -490,12 +534,15 @@ catalog.json ← optional but recommended
490
534
  }
491
535
  ```
492
536
 
537
+ The `category` field is **optional**. When present, the Web UI groups the skill under that category explicitly. When absent, the catalog falls back to a regex-based inference and tags the resulting badge with a small `(inferred)` chip so contributors can see when their metadata is incomplete.
538
+
493
539
  ### `SKILL.md` frontmatter
494
540
 
495
541
  ```markdown
496
542
  ---
497
543
  name: "git-master"
498
544
  description: "Git workflow instructions"
545
+ category: "workflow"
499
546
  autoInject: true
500
547
  activationPrompt: "Always apply Git Master rules when the user asks for Git help."
501
548
  ---
@@ -505,7 +552,7 @@ activationPrompt: "Always apply Git Master rules when the user asks for Git help
505
552
  Your skill content goes here...
506
553
  ```
507
554
 
508
- When `autoInject: true` and `activationPrompt` are set, `skillex sync` injects the activation prompt in a separate managed block for adapters that use a shared or dedicated config file.
555
+ When `autoInject: true` and `activationPrompt` are set, `skillex sync` injects the activation prompt in a separate managed block for adapters that use a shared or dedicated config file. `category` is optional and behaves the same whether declared in `skill.json` or in the SKILL.md frontmatter.
509
556
 
510
557
  ---
511
558
 
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Auto-sync orchestration after install / update / remove operations.
3
+ *
4
+ * Decoupled from `install.ts` so the install/update/remove handlers focus on
5
+ * lockfile mutations only. The `syncFn` parameter (typically
6
+ * `syncInstalledSkills` from `install.ts`) is injected to avoid an import
7
+ * cycle.
8
+ */
9
+ import type { InstallScope, LockfileState, NowFn, SyncCommandResult, SyncHistory, SyncWriteMode } from "./types.js";
10
+ /**
11
+ * Resolves the list of adapter ids that should receive a sync. When an
12
+ * explicit override is provided, only that adapter is targeted. Otherwise,
13
+ * the active adapter (if any) is listed first followed by the remaining
14
+ * detected adapters in workspace order.
15
+ */
16
+ export declare function resolveSyncAdapterIds(adapters: LockfileState["adapters"], adapterOverride?: string): string[];
17
+ /** Options passed to `maybeAutoSync` after install / update operations. */
18
+ export interface MaybeAutoSyncOptions {
19
+ cwd: string;
20
+ scope?: InstallScope | undefined;
21
+ agentSkillsDir?: string | undefined;
22
+ adapters: LockfileState["adapters"];
23
+ adapterOverride?: string | undefined;
24
+ enabled: boolean;
25
+ now: NowFn;
26
+ changed: boolean;
27
+ mode?: SyncWriteMode | undefined;
28
+ }
29
+ /** Sync function shape injected by callers (typically `syncInstalledSkills`). */
30
+ export type SyncFn = (options: {
31
+ cwd: string;
32
+ scope: InstallScope;
33
+ agentSkillsDir?: string;
34
+ adapter?: string;
35
+ mode?: SyncWriteMode;
36
+ now: NowFn;
37
+ }) => Promise<SyncCommandResult>;
38
+ /**
39
+ * Triggers a sync after install/update when auto-sync is enabled and at
40
+ * least one skill changed. Returns `null` when no sync was performed.
41
+ */
42
+ export declare function maybeAutoSync(options: MaybeAutoSyncOptions, syncFn: SyncFn): Promise<SyncCommandResult | null>;
43
+ /** Options passed to `maybeSyncAfterRemove`. */
44
+ export interface MaybeSyncAfterRemoveOptions {
45
+ cwd: string;
46
+ scope?: InstallScope | undefined;
47
+ agentSkillsDir?: string | undefined;
48
+ adapters: LockfileState["adapters"];
49
+ adapterOverride?: string | undefined;
50
+ syncHistory: SyncHistory;
51
+ legacySync: LockfileState["sync"];
52
+ enabled: boolean;
53
+ now: NowFn;
54
+ changed: boolean;
55
+ mode?: SyncWriteMode | undefined;
56
+ }
57
+ /**
58
+ * Triggers a sync after remove operations across every previously-synced
59
+ * adapter (so that removed skills are dropped from each adapter's view).
60
+ *
61
+ * Each adapter is synced concurrently via `Promise.all` and the results are
62
+ * aggregated into an array so callers can report each adapter's outcome
63
+ * individually. Returns `null` when no sync was performed (no changes or
64
+ * no adapters to sync).
65
+ */
66
+ export declare function maybeSyncAfterRemove(options: MaybeSyncAfterRemoveOptions, syncFn: SyncFn): Promise<SyncCommandResult[] | null>;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Auto-sync orchestration after install / update / remove operations.
3
+ *
4
+ * Decoupled from `install.ts` so the install/update/remove handlers focus on
5
+ * lockfile mutations only. The `syncFn` parameter (typically
6
+ * `syncInstalledSkills` from `install.ts`) is injected to avoid an import
7
+ * cycle.
8
+ */
9
+ import { DEFAULT_INSTALL_SCOPE } from "./config.js";
10
+ /**
11
+ * Resolves the list of adapter ids that should receive a sync. When an
12
+ * explicit override is provided, only that adapter is targeted. Otherwise,
13
+ * the active adapter (if any) is listed first followed by the remaining
14
+ * detected adapters in workspace order.
15
+ */
16
+ export function resolveSyncAdapterIds(adapters, adapterOverride) {
17
+ if (adapterOverride) {
18
+ return [adapterOverride];
19
+ }
20
+ const adapterIds = [];
21
+ if (adapters.active) {
22
+ adapterIds.push(adapters.active);
23
+ }
24
+ for (const adapterId of adapters.detected || []) {
25
+ if (!adapterIds.includes(adapterId)) {
26
+ adapterIds.push(adapterId);
27
+ }
28
+ }
29
+ return adapterIds;
30
+ }
31
+ /**
32
+ * Triggers a sync after install/update when auto-sync is enabled and at
33
+ * least one skill changed. Returns `null` when no sync was performed.
34
+ */
35
+ export async function maybeAutoSync(options, syncFn) {
36
+ if (!options.enabled || !options.changed) {
37
+ return null;
38
+ }
39
+ if (resolveSyncAdapterIds(options.adapters, options.adapterOverride).length === 0) {
40
+ return null;
41
+ }
42
+ return syncFn({
43
+ cwd: options.cwd,
44
+ scope: options.scope || DEFAULT_INSTALL_SCOPE,
45
+ ...(options.agentSkillsDir ? { agentSkillsDir: options.agentSkillsDir } : {}),
46
+ ...(options.adapterOverride ? { adapter: options.adapterOverride } : {}),
47
+ ...(options.mode ? { mode: options.mode } : {}),
48
+ now: options.now,
49
+ });
50
+ }
51
+ /**
52
+ * Triggers a sync after remove operations across every previously-synced
53
+ * adapter (so that removed skills are dropped from each adapter's view).
54
+ *
55
+ * Each adapter is synced concurrently via `Promise.all` and the results are
56
+ * aggregated into an array so callers can report each adapter's outcome
57
+ * individually. Returns `null` when no sync was performed (no changes or
58
+ * no adapters to sync).
59
+ */
60
+ export async function maybeSyncAfterRemove(options, syncFn) {
61
+ if (!options.changed) {
62
+ return null;
63
+ }
64
+ const adapters = new Set();
65
+ for (const adapterId of Object.keys(options.syncHistory || {})) {
66
+ adapters.add(adapterId);
67
+ }
68
+ if (options.legacySync?.adapter) {
69
+ adapters.add(options.legacySync.adapter);
70
+ }
71
+ if (options.adapterOverride) {
72
+ adapters.add(options.adapterOverride);
73
+ }
74
+ else if (options.enabled) {
75
+ for (const adapterId of resolveSyncAdapterIds(options.adapters)) {
76
+ adapters.add(adapterId);
77
+ }
78
+ }
79
+ if (adapters.size === 0) {
80
+ return null;
81
+ }
82
+ const results = await Promise.all([...adapters].map((adapterId) => syncFn({
83
+ cwd: options.cwd,
84
+ scope: options.scope || DEFAULT_INSTALL_SCOPE,
85
+ ...(options.agentSkillsDir ? { agentSkillsDir: options.agentSkillsDir } : {}),
86
+ adapter: adapterId,
87
+ ...(options.mode ? { mode: options.mode } : {}),
88
+ now: options.now,
89
+ })));
90
+ return results;
91
+ }
package/dist/catalog.js CHANGED
@@ -6,6 +6,7 @@ import { normalizeAdapterList } from "./adapters.js";
6
6
  import { assertSafeRelativePath } from "./fs.js";
7
7
  import { fetchJson, fetchOptionalJson, fetchText } from "./http.js";
8
8
  import { debug } from "./output.js";
9
+ import { parseSkillFrontmatter } from "./skill.js";
9
10
  import { CatalogError } from "./types.js";
10
11
  const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
11
12
  /**
@@ -210,13 +211,14 @@ async function loadCatalogFromTree(source) {
210
211
  const skillPath = skillFile.path.slice(0, -"/SKILL.md".length);
211
212
  const skillId = skillPath.split("/").pop();
212
213
  const skillBody = await fetchJsonLikeText(buildRawGitHubUrl(source.repo, source.ref, skillFile.path));
213
- const metadata = extractSkillMetadata(skillBody);
214
+ const metadata = parseSkillFrontmatter(skillBody);
214
215
  return normalizeSkill({
215
216
  id: skillId,
216
217
  path: skillPath,
217
218
  name: metadata.name || skillId,
218
219
  description: metadata.description || "",
219
220
  files: collectFilesUnderPath(files, skillPath),
221
+ ...(metadata.category ? { category: metadata.category } : {}),
220
222
  }, source);
221
223
  }));
222
224
  return {
@@ -229,34 +231,6 @@ async function loadCatalogFromTree(source) {
229
231
  async function fetchJsonLikeText(url) {
230
232
  return fetchText(url, { headers: { Accept: "text/plain" } });
231
233
  }
232
- function extractSkillMetadata(content) {
233
- const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
234
- if (!match) {
235
- return {};
236
- }
237
- const frontmatter = match[1];
238
- if (frontmatter === undefined) {
239
- return {};
240
- }
241
- const name = extractFrontmatterValue(frontmatter, "name");
242
- const description = extractFrontmatterValue(frontmatter, "description");
243
- return {
244
- ...(name !== null ? { name } : {}),
245
- ...(description !== null ? { description } : {}),
246
- };
247
- }
248
- function extractFrontmatterValue(frontmatter, key) {
249
- const expression = new RegExp(`^${key}:\\s*(.+)$`, "m");
250
- const match = frontmatter.match(expression);
251
- if (!match) {
252
- return null;
253
- }
254
- const value = match[1];
255
- if (value === undefined) {
256
- return null;
257
- }
258
- return value.trim().replace(/^["']|["']$/g, "");
259
- }
260
234
  function collectFilesUnderPath(treeFiles, skillPath) {
261
235
  return treeFiles
262
236
  .filter((item) => item.type === "blob" && item.path.startsWith(`${skillPath}/`))
@@ -303,6 +277,7 @@ function normalizeSkill(skill, source) {
303
277
  const skillPath = skill.path || `${source.skillsDir}/${id}`;
304
278
  const files = Array.isArray(skill.files) ? skill.files.map(assertSafeRelativePath) : ["SKILL.md"];
305
279
  const uniqueFiles = [...new Set(files)];
280
+ const category = typeof skill.category === "string" && skill.category.trim() ? skill.category.trim() : undefined;
306
281
  return {
307
282
  id,
308
283
  name: skill.name || id,
@@ -314,6 +289,7 @@ function normalizeSkill(skill, source) {
314
289
  entry: skill.entry || "SKILL.md",
315
290
  path: stripLeadingSlash(skillPath),
316
291
  files: uniqueFiles,
292
+ ...(category !== undefined ? { category } : {}),
317
293
  };
318
294
  }
319
295
  /**
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ParsedArgs } from "./types.js";
1
2
  /**
2
3
  * Runs the Skillex CLI entrypoint.
3
4
  *
@@ -6,3 +7,15 @@
6
7
  */
7
8
  export declare function main(argv: string[]): Promise<void>;
8
9
  export declare function resolveCommandRoute(command: string | undefined): string;
10
+ /**
11
+ * Parses argv into a typed `ParsedArgs` shape with strict validation:
12
+ *
13
+ * - Unknown flags raise `UNKNOWN_FLAG` with a "did you mean" suggestion.
14
+ * - Boolean flags (`BOOLEAN_FLAGS`) accept presence-only or `--flag=value` forms.
15
+ * - String flags (`STRING_FLAGS`) require a value via `--flag=value` or
16
+ * `--flag value`. Missing values raise `MISSING_FLAG_VALUE`.
17
+ * - The literal `--` token marks end-of-options; remaining tokens become
18
+ * `positionalAfter` and are forwarded to handlers (used by `run` to pass
19
+ * arguments to the underlying script without flag interpretation).
20
+ */
21
+ export declare function parseArgs(argv: string[]): ParsedArgs;