skillex 0.3.1 → 0.4.1
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 +286 -1
- package/README.md +82 -16
- package/dist/auto-sync.d.ts +66 -0
- package/dist/auto-sync.js +91 -0
- package/dist/catalog.js +5 -29
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +266 -144
- package/dist/confirm.js +3 -1
- package/dist/direct-github.d.ts +60 -0
- package/dist/direct-github.js +177 -0
- package/dist/doctor.d.ts +31 -0
- package/dist/doctor.js +172 -0
- package/dist/downloader.d.ts +42 -0
- package/dist/downloader.js +41 -0
- package/dist/fs.d.ts +21 -1
- package/dist/fs.js +30 -3
- package/dist/http.d.ts +28 -7
- package/dist/http.js +143 -42
- package/dist/install.d.ts +23 -9
- package/dist/install.js +75 -348
- package/dist/lockfile.d.ts +46 -0
- package/dist/lockfile.js +169 -0
- package/dist/output.d.ts +11 -0
- package/dist/output.js +49 -0
- package/dist/recommended.d.ts +13 -0
- package/dist/recommended.js +21 -0
- package/dist/runner.js +9 -9
- package/dist/skill.d.ts +2 -0
- package/dist/skill.js +3 -0
- package/dist/sync.js +12 -9
- package/dist/types.d.ts +39 -0
- package/dist/types.js +28 -0
- package/dist/ui.js +1 -1
- package/dist/user-config.d.ts +5 -0
- package/dist/user-config.js +22 -1
- package/dist/web-ui.js +5 -0
- package/dist-ui/assets/CatalogPage-CKEfRSvG.js +1 -0
- package/dist-ui/assets/CatalogPage-W5MqylAz.css +1 -0
- package/dist-ui/assets/DoctorPage-C92pEVl_.js +1 -0
- package/dist-ui/assets/Skeleton-BISmLuhY.js +1 -0
- package/dist-ui/assets/Skeleton-_Ooiw1nN.css +1 -0
- package/dist-ui/assets/SkillDetailPage-CBAaWpcc.css +1 -0
- package/dist-ui/assets/SkillDetailPage-CWGjTH2M.js +1 -0
- package/dist-ui/assets/{index-UBECch6X.css → index-CWm7zQTg.css} +1 -1
- package/dist-ui/assets/index-DAVP4Xp_.js +26 -0
- package/dist-ui/assets/recommended-D_i10hwH.js +1 -0
- package/dist-ui/index.html +2 -2
- package/package.json +6 -2
- package/dist-ui/assets/CatalogPage-B_qic36n.js +0 -1
- package/dist-ui/assets/SkillDetailPage-BJ3onKk4.js +0 -1
- package/dist-ui/assets/index-DN-z--cR.js +0 -25
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,282 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.1] - 2026-05-02
|
|
11
|
+
|
|
12
|
+
A developer-experience patch on top of 0.4.0. Same runtime behaviour for end
|
|
13
|
+
users; one-command Web UI dev workflow for contributors.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- **`npm run dev`** — one-command dev orchestrator (`scripts/dev.mjs`). Spawns the Skillex backend with `--no-open --port 4174`, parses the token from the first `Skillex Web UI running at <url>` line, spawns the Vite dev server on port 4175 with `VITE_SKILLEX_BACKEND` pre-wired, and opens the browser at `http://127.0.0.1:4175?token=<token>` automatically. Ctrl+C cleanly tears both processes down.
|
|
17
|
+
- New convenience npm scripts:
|
|
18
|
+
- `npm run dev` — full dev loop (above)
|
|
19
|
+
- `npm run web` — alias for `npm run build && node ./bin/skillex.js ui`
|
|
20
|
+
- `npm run tui` — open the interactive terminal browser
|
|
21
|
+
- `npm run skillex -- <args>` — run any CLI command without typing `node ./bin/skillex.js`
|
|
22
|
+
- `skillex ui` gains `--host`, `--port`, and `--no-open` flags so the dev orchestrator (and any other automation) can run the backend on a known port without auto-opening the browser.
|
|
23
|
+
- Env vars to override the dev orchestrator: `SKILLEX_DEV_BACKEND_PORT`, `SKILLEX_DEV_VITE_PORT`, `SKILLEX_DEV_NO_OPEN=1`.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **Web UI dev mode (`npm run dev:ui`)** no longer throws `Missing Skillex Web UI bootstrap payload` and a blank page. When no bootstrap is injected (Vite serves `index.html` directly without the backend's substitution), an in-page overlay now explains exactly how to wire the dev server to a running `skillex ui` backend.
|
|
27
|
+
- The dev page can also recover the bootstrap from a query string (e.g. `http://127.0.0.1:4174?token=<token>&scope=local`); the token is cached in `localStorage` so subsequent reloads work.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- `vite.config.ts` reads a new `VITE_SKILLEX_BACKEND` env var. When set, the Vite dev server proxies `/api/*` to that backend so the UI talks to a real `skillex ui` instance with HMR enabled. The new `npm run dev` script sets this automatically.
|
|
31
|
+
- README "Local Web UI development" rewritten: `npm run dev` is now the primary workflow; production-style (`npm run web`) is documented as a single one-liner.
|
|
32
|
+
|
|
33
|
+
## [0.4.0] - 2026-05-02
|
|
34
|
+
|
|
35
|
+
A substantial release focused on **reliability, security, and UX**. The CLI is
|
|
36
|
+
now production-grade (HTTP timeouts, host-restricted token, parallel
|
|
37
|
+
downloads, hardened parser); the Web UI grew bulk actions, keyboard
|
|
38
|
+
shortcuts, multi-select, persistent state, an accessible doctor panel, and a
|
|
39
|
+
mobile drawer; and the codebase was refactored for maintainability without
|
|
40
|
+
breaking the public API.
|
|
41
|
+
|
|
42
|
+
Test count went from 63 → 88 (no regressions).
|
|
43
|
+
|
|
44
|
+
### Highlights
|
|
45
|
+
|
|
46
|
+
- **Reliability & security pass** — HTTP timeouts everywhere, GitHub token
|
|
47
|
+
scoped to GitHub-owned hosts, lockfile path safety, symlink confinement,
|
|
48
|
+
ref-character validation, mode `0o600` on `~/.askillrc.json`, parallel
|
|
49
|
+
file downloads (5–10× speedup on multi-file skills).
|
|
50
|
+
- **CLI parser hardened** — unknown flags rejected with "did you mean"
|
|
51
|
+
suggestions, `--` sentinel honored, missing-value detection, unknown
|
|
52
|
+
commands suggest the closest match, `parseBooleanFlag` names the flag and
|
|
53
|
+
lists accepted values, `skillex doctor` differentiates DNS / TLS /
|
|
54
|
+
refused / timeout failures.
|
|
55
|
+
- **Three new top-level commands / flags** — `skillex show <id>` previews a
|
|
56
|
+
skill's SKILL.md without installing, `skillex init --install-recommended`
|
|
57
|
+
ships a curated starter pack, `skillex sync --dry-run --exit-code`
|
|
58
|
+
mirrors `git diff --exit-code` for CI drift detection.
|
|
59
|
+
- **Web UI marketplace overhaul** — Install all + Install recommended +
|
|
60
|
+
Remove all bulk actions; per-card optimistic UI; Shift+click multi-select
|
|
61
|
+
with range select; sticky selection bar; persistent selection across
|
|
62
|
+
refreshes; search highlight; group-by-source; related skills; mobile
|
|
63
|
+
drawer; Doctor panel + sidebar health dot; breadcrumbs; first-load
|
|
64
|
+
skeletons; `⌘K` and `⇧⌘A` shortcuts.
|
|
65
|
+
- **Internationalization & regression guard** — every user-facing string
|
|
66
|
+
is now English; `scripts/check-language.mjs` runs in `npm test` and fails
|
|
67
|
+
CI if banned Portuguese tokens reappear without an explicit
|
|
68
|
+
`i18n-allow:` annotation.
|
|
69
|
+
- **Refactor** — `src/install.ts` (1326 LOC) split into focused modules
|
|
70
|
+
(`lockfile.ts`, `direct-github.ts`, `auto-sync.ts`, `downloader.ts`,
|
|
71
|
+
`doctor.ts`) with re-exports preserving every existing import path.
|
|
72
|
+
|
|
73
|
+
### Added — Core / CLI
|
|
74
|
+
|
|
75
|
+
- `skillex show <skill-id>` — preview a skill's manifest + rendered SKILL.md
|
|
76
|
+
from the configured sources without installing. `--raw` prints the
|
|
77
|
+
markdown verbatim; `--json` returns the manifest plus the entry content
|
|
78
|
+
as a single object.
|
|
79
|
+
- `skillex init --install-recommended` — after writing the lockfile, install
|
|
80
|
+
a curated 5-skill starter pack (`commit-craft`, `code-review`,
|
|
81
|
+
`secure-defaults`, `error-handling`, `test-discipline`) using the same
|
|
82
|
+
progress bar as `install --all`. The recommended list lives in
|
|
83
|
+
`src/recommended.ts` (single source of truth).
|
|
84
|
+
- `skillex sync --dry-run --exit-code` — exit `1` whenever the dry-run
|
|
85
|
+
would change at least one adapter (mirrors `git diff --exit-code`). CI
|
|
86
|
+
scripts can detect drift without parsing the diff output.
|
|
87
|
+
- `--tags <tag>` is accepted as a hidden alias of `--tag` on
|
|
88
|
+
`skillex search` so previously documented examples keep working.
|
|
89
|
+
- `src/doctor.ts` exports `runDoctorChecks(options): Promise<DoctorReport>`
|
|
90
|
+
— the canonical six health checks reused by both the CLI and the Web UI.
|
|
91
|
+
- `HttpError` typed error class with codes `HTTP_TIMEOUT`,
|
|
92
|
+
`HTTP_RATE_LIMIT`, `HTTP_AUTH_FAILED`, `HTTP_NOT_FOUND`,
|
|
93
|
+
`HTTP_SERVER_ERROR`, and `HTTP_ERROR`.
|
|
94
|
+
- `RemoveSkillsResult.autoSyncs: SyncCommandResult[]` — full per-adapter
|
|
95
|
+
sync aggregate (the existing `autoSync` field is preserved as the first
|
|
96
|
+
result for backward compat).
|
|
97
|
+
- `createSymlink(target, link, { allowedRoot })` overload that refuses
|
|
98
|
+
targets resolving outside the managed root.
|
|
99
|
+
- `SkillManifest.category?: string` (optional). Catalog publishers can
|
|
100
|
+
group skills explicitly instead of relying on consumer-side regex
|
|
101
|
+
inference. Read from both `skill.json` and SKILL.md frontmatter.
|
|
102
|
+
- `suggestClosest(actual, candidates, threshold = 2)` helper in
|
|
103
|
+
`src/output.ts` powers "did you mean" hints across the parser and
|
|
104
|
+
dispatcher.
|
|
105
|
+
|
|
106
|
+
### Added — Web UI
|
|
107
|
+
|
|
108
|
+
- **Doctor panel** + `GET /api/doctor` endpoint mirroring the CLI's six
|
|
109
|
+
checks, with a sidebar status dot (green / yellow / red + pulse)
|
|
110
|
+
refreshed after every mutation.
|
|
111
|
+
- **Bulk install / remove / install-recommended** buttons in the catalog
|
|
112
|
+
hero, each guarded by an accessible `ConfirmDialog` (role=dialog,
|
|
113
|
+
aria-modal, focus on Cancel, Esc cancels, Enter only fires when no
|
|
114
|
+
button has focus).
|
|
115
|
+
- **Per-card optimistic UI** — single-skill install / remove / update show
|
|
116
|
+
a localized spinner inside the card and dim only that card. Backed by
|
|
117
|
+
`state.busyCards: Set<string>` and a `runCardAction(skillId, label, fn)`
|
|
118
|
+
store helper. Bulk actions still use the global overlay.
|
|
119
|
+
- **Bulk select via Shift+click** — sticky selection bar shows
|
|
120
|
+
`N selected`, `Select all visible (M)` link, and `Install N` /
|
|
121
|
+
`Remove N` buttons that count only the eligible subset. Plain click in
|
|
122
|
+
selection mode toggles. Esc clears (priority cascade — see Changed).
|
|
123
|
+
- **Range select** — Shift+click on a second card selects the range
|
|
124
|
+
between the anchor and the target in the visible-id ordering
|
|
125
|
+
(Finder/Excel behavior). The anchor moves with each interaction so
|
|
126
|
+
successive Shift+clicks extend from the latest endpoint.
|
|
127
|
+
- **Persistent selection** — selection (ids + anchor + scope) is mirrored
|
|
128
|
+
to `localStorage["skillex.selection"]`. Restored on startup, filtered to
|
|
129
|
+
ids that still exist in the loaded catalog under the same scope.
|
|
130
|
+
- **Search highlight** — query matches in skill name and description are
|
|
131
|
+
wrapped in `<mark class="search-mark">` with an accent background. Pure
|
|
132
|
+
template rendering (no `v-html`) — XSS-safe.
|
|
133
|
+
- **Group-by-source toggle** — when more than one source is configured, a
|
|
134
|
+
toggle in the category-pill row buckets the grid by source.repo with
|
|
135
|
+
per-bucket headers and counts.
|
|
136
|
+
- **Related skills** on the SkillDetailPage — up to 4 cards scored by tag
|
|
137
|
+
overlap (+ 0.5 bonus for matching `category`). Cards are keyboard
|
|
138
|
+
accessible, show a 2-line description clamp, up to 3 tags, and a ✓ when
|
|
139
|
+
the related skill is already installed.
|
|
140
|
+
- **Breadcrumbs** on the SkillDetailPage —
|
|
141
|
+
`Catalog (link, home icon) / category / skill-name (current)`.
|
|
142
|
+
Category resolves from the explicit manifest field first, falls back to
|
|
143
|
+
the regex inference, hidden when no signal.
|
|
144
|
+
- **Onboarding card** for fresh workspaces — replaces the generic empty
|
|
145
|
+
state with a bright Install-all CTA + count when the workspace has zero
|
|
146
|
+
installed skills and no filter is active.
|
|
147
|
+
- **Installed-only filter** — the "Installed" overview card is now a
|
|
148
|
+
toggle (`role=button`, Enter/Space, `aria-pressed`); active state filters
|
|
149
|
+
the grid to installed skills only. The stat now reads `N / total`.
|
|
150
|
+
- **Inferred category chip** on cards whose category came from regex
|
|
151
|
+
fallback rather than an explicit manifest field.
|
|
152
|
+
- **First-load skeletons** — new `Skeleton.vue` (variants `card` / `row`).
|
|
153
|
+
Catalog page renders 6 card-skeletons until `state.catalog` resolves;
|
|
154
|
+
Doctor page renders 4 row-skeletons; SkillDetailPage renders metadata
|
|
155
|
+
+ body + related skeletons during its first fetch.
|
|
156
|
+
- **Mobile drawer** — sidebar slides in/out on viewports ≤680 px instead
|
|
157
|
+
of being hidden entirely. Triggered by a topbar hamburger; tap backdrop
|
|
158
|
+
or Esc to close; route changes auto-close.
|
|
159
|
+
- **Adapter dropdown** for compact viewports (≤1100 px) — replaces the 7
|
|
160
|
+
icon row with a single trigger + listbox menu (`role=listbox`,
|
|
161
|
+
`role=option`, `aria-selected`).
|
|
162
|
+
- **`⌘K` / `Ctrl+K` shortcut** — focuses the topbar search. Navigates back
|
|
163
|
+
to `/` first when invoked from another route. Adaptive hint badge.
|
|
164
|
+
- **`⇧⌘A` / `Ctrl+Shift+A` shortcut** — opens the Install-all confirm on
|
|
165
|
+
the catalog page. Skipped when typing in inputs. Shift required so the
|
|
166
|
+
browser's native "select all" (Cmd+A) is preserved. Discoverable via the
|
|
167
|
+
shortcut chip on the Install-all button.
|
|
168
|
+
- **Demo media placeholder** — new `docs/media/` with a README describing
|
|
169
|
+
how to regenerate `tui.gif`, `web-ui.png`, `web-ui-doctor.png` (vhs +
|
|
170
|
+
asciinema). Main README has a "Demo" section that references the media
|
|
171
|
+
via raw GitHub URLs (no binaries in the npm tarball).
|
|
172
|
+
- **"Why Skillex" section** in the README plus a Quick Start that leads
|
|
173
|
+
with `npx skillex@latest` (the TUI) and frames the
|
|
174
|
+
`init` → `install` chain as scriptable mode.
|
|
175
|
+
|
|
176
|
+
### Changed
|
|
177
|
+
|
|
178
|
+
- **Reliability:** all HTTP helpers now abort after a default 30-second
|
|
179
|
+
timeout when the caller does not provide an `AbortSignal`, raising
|
|
180
|
+
`HttpError("HTTP_TIMEOUT")` instead of hanging.
|
|
181
|
+
- **HTTP errors:** 403 responses split into `HTTP_RATE_LIMIT` (when
|
|
182
|
+
`X-RateLimit-Remaining: 0`) and `HTTP_AUTH_FAILED`, each with an
|
|
183
|
+
actionable message. The rate-limit message includes the reset hint.
|
|
184
|
+
- **`maybeSyncAfterRemove`** now runs adapter syncs in parallel via
|
|
185
|
+
`Promise.all` and returns the full aggregate; the CLI prints one line
|
|
186
|
+
per adapter (was previously dropping all but the last adapter's
|
|
187
|
+
outcome).
|
|
188
|
+
- **`toInstallError`** preserves the underlying `error.code` (HTTP error
|
|
189
|
+
codes, `EACCES`, `ENOENT`, etc.) on the wrapped `InstallError`.
|
|
190
|
+
- **CLI parser** is now schema-driven via `STRING_FLAGS` / `BOOLEAN_FLAGS`
|
|
191
|
+
/ `KNOWN_FLAGS` sets. Unknown flags raise `CliError("UNKNOWN_FLAG")`
|
|
192
|
+
with a Levenshtein-based "did you mean" suggestion (typos like
|
|
193
|
+
`--scop=global` are no longer silently accepted).
|
|
194
|
+
- **CLI parser** detects missing values for string flags and raises
|
|
195
|
+
`CliError("MISSING_FLAG_VALUE")` naming the offending flag.
|
|
196
|
+
- **CLI parser** honors the literal `--` end-of-options sentinel and
|
|
197
|
+
exposes everything after it via `ParsedArgs.positionalAfter`, so
|
|
198
|
+
`skillex run x:cmd -- --foo` forwards `--foo` to the underlying script
|
|
199
|
+
without flag interpretation.
|
|
200
|
+
- **CLI dispatcher** suggests the closest match for unknown commands
|
|
201
|
+
(e.g. `skillex insall git-master` → `Did you mean: install?`).
|
|
202
|
+
- **`parseBooleanFlag`** error now names the flag and lists every accepted
|
|
203
|
+
value: `Invalid value "maybe" for --auto-sync. Use true, false, yes,
|
|
204
|
+
no, on, off, 1, or 0.`
|
|
205
|
+
- **`INSTALL_NO_TARGETS`** (`skillex install` with no args) now prints a
|
|
206
|
+
3-line inline usage block instead of a one-liner hint.
|
|
207
|
+
- **`skillex doctor`** differentiates DNS, connection-refused, TLS,
|
|
208
|
+
timeout, and 5xx failures and surfaces the underlying `error.message`
|
|
209
|
+
instead of collapsing every failure into "GitHub API is unreachable".
|
|
210
|
+
- **`skillex init`** ends with a three-line "Next steps" block (TUI /
|
|
211
|
+
starter pack / full catalog) instead of a single line. Suppressed when
|
|
212
|
+
`--install-recommended` was used.
|
|
213
|
+
- **SkillDetailPage** action order rewritten following "primary action
|
|
214
|
+
last": Sync (ghost) → Update (secondary, only when installed) →
|
|
215
|
+
Install (primary) or Remove (danger).
|
|
216
|
+
- **Web UI Esc cascade** — the Esc key now follows a priority chain:
|
|
217
|
+
ConfirmDialog → adapter dropdown → mobile drawer → CatalogPage
|
|
218
|
+
selection. Each handler checks `event.defaultPrevented` before acting,
|
|
219
|
+
so only one consumer fires per keypress.
|
|
220
|
+
- **Refactor:** `src/install.ts` (1326 LOC) split into `src/lockfile.ts`,
|
|
221
|
+
`src/direct-github.ts`, `src/auto-sync.ts`, `src/downloader.ts`. Public
|
|
222
|
+
`package.json#exports` and import paths preserved via re-exports.
|
|
223
|
+
- Consolidated SKILL.md frontmatter parsing on `parseSkillFrontmatter`
|
|
224
|
+
(`src/skill.ts`); the duplicated inline parser in `src/catalog.ts` was
|
|
225
|
+
removed.
|
|
226
|
+
|
|
227
|
+
### Fixed
|
|
228
|
+
|
|
229
|
+
- All remaining Portuguese user-facing strings translated to English: TUI
|
|
230
|
+
prompt label, sync symlink-fallback warnings, runner errors,
|
|
231
|
+
direct-install confirmation, non-TTY confirm prompt, filesystem path
|
|
232
|
+
errors, and Web UI labels (sidebar, catalog, skill card buttons,
|
|
233
|
+
detail page).
|
|
234
|
+
- Sync warnings now route through `output.warn` instead of bare
|
|
235
|
+
`console.error` so they respect color and stream conventions.
|
|
236
|
+
- New `scripts/check-language.mjs` regression guard runs as part of
|
|
237
|
+
`npm test` and fails CI if banned Portuguese tokens reappear in
|
|
238
|
+
`src/**/*.ts` or `ui/src/**/*.{vue,ts}` without an explicit
|
|
239
|
+
`i18n-allow:` annotation.
|
|
240
|
+
- `toLockfileSource` boolean expression no longer contains a duplicated
|
|
241
|
+
`(label || repo === DEFAULT_REPO)` test (copy-paste artifact); behavior
|
|
242
|
+
unchanged.
|
|
243
|
+
- **README** example at the search section now uses the canonical
|
|
244
|
+
`--tag <tag>` flag instead of the silently-ignored `--tags`.
|
|
245
|
+
- **Web UI:** version badge in the sidebar reads the actual
|
|
246
|
+
`package.json#version` at build time via
|
|
247
|
+
`import.meta.env.VITE_SKILLEX_VERSION` instead of the previously
|
|
248
|
+
hardcoded `v0.2.4` (with a tautological ternary).
|
|
249
|
+
- **Web UI:** dead `⌘K` hint badge removed (no shortcut was wired) — and
|
|
250
|
+
brought back once `Cmd+K` was actually implemented.
|
|
251
|
+
- **Web UI:** misleading "Oficial" badge that every skill card displayed
|
|
252
|
+
(without any verification model) removed.
|
|
253
|
+
- **Web UI:** mobile (≤680 px) sidebar no longer disappears entirely.
|
|
254
|
+
- **Web UI:** `Remover` and `Carregando detalhes...` strings on the detail
|
|
255
|
+
page translated to English.
|
|
256
|
+
|
|
257
|
+
### Security
|
|
258
|
+
|
|
259
|
+
- **Token confinement:** `Authorization: Bearer ${GITHUB_TOKEN}` is only
|
|
260
|
+
attached when the request host is `api.github.com`,
|
|
261
|
+
`raw.githubusercontent.com`, or any `*.githubusercontent.com` mirror.
|
|
262
|
+
Tokens are no longer leaked to third-party `--catalog-url` targets.
|
|
263
|
+
- **File mode:** `~/.askillrc.json` (which may contain `githubToken`) is
|
|
264
|
+
written with mode `0o600`. Existing world-readable files are tightened
|
|
265
|
+
on next save with a one-time warning.
|
|
266
|
+
- **Lockfile path safety:** `removeSkill` validates `metadata.path`
|
|
267
|
+
against the managed skills store before deleting; tampered lockfile
|
|
268
|
+
paths raise `INSTALL_PATH_UNSAFE` instead of removing arbitrary
|
|
269
|
+
directories.
|
|
270
|
+
- **Symlink confinement:** sync per-skill symlinks now refuse targets
|
|
271
|
+
that resolve outside the workspace state directory.
|
|
272
|
+
- **Direct-install ref:** `parseDirectGitHubRef` validates the ref segment
|
|
273
|
+
against `^[A-Za-z0-9_.\-/]+$` and rejects empty refs after `@`;
|
|
274
|
+
previously dangerous refs would land in the lockfile.
|
|
275
|
+
- **Web UI avatars:** skill author avatars are now a deterministic
|
|
276
|
+
CSS-only initials chip. The previous `<img>` to `dicebear.com` is gone
|
|
277
|
+
— the UI works offline and no longer leaks page-load telemetry to a
|
|
278
|
+
third-party host.
|
|
279
|
+
|
|
280
|
+
### Performance
|
|
281
|
+
|
|
282
|
+
- **Parallel file downloads:** `downloadSkillFiles` fetches every file in
|
|
283
|
+
a skill via `Promise.all` instead of a sequential loop. Multi-file
|
|
284
|
+
skills now scale with bandwidth instead of file count.
|
|
285
|
+
|
|
10
286
|
## [0.3.1] - 2026-04-08
|
|
11
287
|
|
|
12
288
|
### Fixed
|
|
@@ -127,7 +403,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
127
403
|
- Adapter detection and managed-block sync for Copilot, Cline, Cursor, Claude, Gemini, Windsurf, Codex
|
|
128
404
|
- Lockfile-based workspace state at `.agent-skills/skills.json`
|
|
129
405
|
|
|
130
|
-
[Unreleased]: https://github.com/lgili/skillex/compare/v0.
|
|
406
|
+
[Unreleased]: https://github.com/lgili/skillex/compare/v0.4.1...HEAD
|
|
407
|
+
[0.4.1]: https://github.com/lgili/skillex/compare/v0.4.0...v0.4.1
|
|
408
|
+
[0.4.0]: https://github.com/lgili/skillex/compare/v0.3.1...v0.4.0
|
|
409
|
+
[0.3.1]: https://github.com/lgili/skillex/compare/v0.3.0...v0.3.1
|
|
410
|
+
[0.3.0]: https://github.com/lgili/skillex/compare/v0.2.5...v0.3.0
|
|
411
|
+
[0.2.5]: https://github.com/lgili/skillex/compare/v0.2.4...v0.2.5
|
|
412
|
+
[0.2.4]: https://github.com/lgili/skillex/compare/v0.2.3...v0.2.4
|
|
413
|
+
[0.2.3]: https://github.com/lgili/skillex/compare/v0.2.2...v0.2.3
|
|
414
|
+
[0.2.2]: https://github.com/lgili/skillex/compare/v0.2.1...v0.2.2
|
|
415
|
+
[0.2.1]: https://github.com/lgili/skillex/compare/v0.2.0...v0.2.1
|
|
131
416
|
[0.2.0]: https://github.com/lgili/skillex/compare/v0.1.1...v0.2.0
|
|
132
417
|
[0.1.1]: https://github.com/lgili/skillex/compare/v0.1.0...v0.1.1
|
|
133
418
|
[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
|
-
#
|
|
37
|
-
npx skillex@latest
|
|
38
|
-
|
|
39
|
-
# 2. Browse available skills
|
|
40
|
-
npx skillex@latest list
|
|
42
|
+
# Easiest: open the interactive terminal browser
|
|
43
|
+
npx skillex@latest
|
|
41
44
|
|
|
42
|
-
#
|
|
43
|
-
npx skillex@latest
|
|
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) |  |
|
|
60
|
+
| Web UI catalog page (`skillex ui`) |  |
|
|
61
|
+
| Web UI Doctor panel |  |
|
|
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`.
|
|
@@ -73,21 +92,40 @@ npx skillex <command>
|
|
|
73
92
|
|
|
74
93
|
### Local Web UI development
|
|
75
94
|
|
|
76
|
-
When working on the repository itself, the browser UI is a standalone Vue 3 + Vite frontend under
|
|
95
|
+
When working on the repository itself, the browser UI is a standalone Vue 3 + Vite frontend under `ui/`. The CLI serves the built assets from `dist-ui/` and keeps all install/remove/update/sync logic in the local TypeScript backend.
|
|
96
|
+
|
|
97
|
+
**One-command dev mode** — `npm run dev` spawns the backend, the Vite dev server with `/api/*` proxied, and opens the browser with the right token. Edit a `.vue` file and HMR reloads instantly.
|
|
77
98
|
|
|
78
99
|
```bash
|
|
79
100
|
npm install
|
|
80
|
-
npm run
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
npm run dev
|
|
102
|
+
# ➜ Backend: http://127.0.0.1:4174 (token captured automatically)
|
|
103
|
+
# ➜ UI: http://127.0.0.1:4175 (opens in your browser)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Override the ports if you need to:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
SKILLEX_DEV_BACKEND_PORT=5000 SKILLEX_DEV_VITE_PORT=5001 npm run dev
|
|
110
|
+
SKILLEX_DEV_NO_OPEN=1 npm run dev # keep the URL on stdout, don't open
|
|
83
111
|
```
|
|
84
112
|
|
|
85
|
-
|
|
113
|
+
**Production-style local run** — builds everything and serves it exactly like an end user gets via `npx skillex ui`:
|
|
86
114
|
|
|
87
115
|
```bash
|
|
88
|
-
npm run
|
|
116
|
+
npm run web # alias for `npm run build && node ./bin/skillex.js ui`
|
|
89
117
|
```
|
|
90
118
|
|
|
119
|
+
**Other handy aliases:**
|
|
120
|
+
|
|
121
|
+
| Command | What it does |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `npm run dev` | Backend + Vite + auto-open (full HMR loop) |
|
|
124
|
+
| `npm run web` | Production-style: build + run the local Web UI server |
|
|
125
|
+
| `npm run tui` | Open the interactive terminal browser |
|
|
126
|
+
| `npm run skillex -- <args>` | Run any CLI command without `node ./bin/skillex.js` (e.g. `npm run skillex -- list`) |
|
|
127
|
+
| `npm run dev:ui` | Vite dev server only (advanced; needs `VITE_SKILLEX_BACKEND=…` env var pointing at a running backend) |
|
|
128
|
+
|
|
91
129
|
---
|
|
92
130
|
|
|
93
131
|
## Commands
|
|
@@ -110,6 +148,7 @@ skillex init --global --adapter codex
|
|
|
110
148
|
| `--repo <owner/repo>` | Optional. Overrides the default first-party source for this workspace. |
|
|
111
149
|
| `--adapter <id>` | Force a specific adapter instead of auto-detecting. |
|
|
112
150
|
| `--auto-sync` | Enable or disable automatic sync after install, update, and remove. Default: `true`. |
|
|
151
|
+
| `--install-recommended` | After init, install a curated starter pack (`commit-craft`, `code-review`, `secure-defaults`, `error-handling`, `test-discipline`). |
|
|
113
152
|
| `--ref <branch>` | Use a specific branch or tag (default: `main`). |
|
|
114
153
|
| `--scope <local\|global>` | Choose whether Skillex manages workspace or user-global state. |
|
|
115
154
|
| `--global` | Shortcut for `--scope global`. |
|
|
@@ -150,7 +189,7 @@ skillex search code-review --repo myorg/my-skills
|
|
|
150
189
|
|------|-------------|
|
|
151
190
|
| `--repo <owner/repo>` | Limit the command to a single source instead of aggregating all configured sources. |
|
|
152
191
|
| `--compatibility <adapter>` | Filter by adapter (e.g. `cursor`, `claude`, `codex`). |
|
|
153
|
-
| `--
|
|
192
|
+
| `--tag <tag>` | Filter by tag. (`--tags` is also accepted for backward compatibility with earlier docs.) |
|
|
154
193
|
| `--json` | Print raw JSON. |
|
|
155
194
|
|
|
156
195
|
---
|
|
@@ -271,11 +310,34 @@ Execute a script declared in a skill's `skill.json`.
|
|
|
271
310
|
|
|
272
311
|
```bash
|
|
273
312
|
skillex run git-master:cleanup
|
|
274
|
-
skillex run git-master:cleanup --yes
|
|
313
|
+
skillex run git-master:cleanup --yes # skip confirmation
|
|
314
|
+
skillex run git-master:cleanup -- --extra=arg # forward "--extra=arg" to the script
|
|
275
315
|
```
|
|
276
316
|
|
|
277
317
|
---
|
|
278
318
|
|
|
319
|
+
### `show`
|
|
320
|
+
|
|
321
|
+
Print a skill's manifest summary plus its rendered `SKILL.md` content from the
|
|
322
|
+
configured catalog sources, **without installing anything**. Use this to
|
|
323
|
+
preview a skill before deciding to install.
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
skillex show git-master # human-friendly summary + content
|
|
327
|
+
skillex show code-review --raw # SKILL.md verbatim, no header
|
|
328
|
+
skillex show secure-defaults --json # manifest + entry content as JSON
|
|
329
|
+
skillex show foo --repo myorg/my-skills # disambiguate when multiple sources match
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
| Flag | Description |
|
|
333
|
+
|------|-------------|
|
|
334
|
+
| `--repo <owner/repo>` | Limit resolution to one source. |
|
|
335
|
+
| `--raw` | Print SKILL.md verbatim with no manifest header. |
|
|
336
|
+
| `--json` | Print the resolved manifest plus the raw SKILL.md as a single JSON object. |
|
|
337
|
+
| `--no-cache` | Bypass local catalog cache. |
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
279
341
|
### Default terminal browser
|
|
280
342
|
|
|
281
343
|
Running `skillex` with no subcommand now opens the interactive terminal browser by default.
|
|
@@ -481,6 +543,7 @@ catalog.json ← optional but recommended
|
|
|
481
543
|
"description": "Teaches the agent to write semantic commits and manage branches.",
|
|
482
544
|
"author": "your-name",
|
|
483
545
|
"tags": ["git", "workflow"],
|
|
546
|
+
"category": "workflow",
|
|
484
547
|
"compatibility": ["codex", "copilot", "cline", "cursor", "claude", "gemini", "windsurf"],
|
|
485
548
|
"entry": "SKILL.md",
|
|
486
549
|
"files": ["SKILL.md", "tools/git-cleanup.js"],
|
|
@@ -490,12 +553,15 @@ catalog.json ← optional but recommended
|
|
|
490
553
|
}
|
|
491
554
|
```
|
|
492
555
|
|
|
556
|
+
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.
|
|
557
|
+
|
|
493
558
|
### `SKILL.md` frontmatter
|
|
494
559
|
|
|
495
560
|
```markdown
|
|
496
561
|
---
|
|
497
562
|
name: "git-master"
|
|
498
563
|
description: "Git workflow instructions"
|
|
564
|
+
category: "workflow"
|
|
499
565
|
autoInject: true
|
|
500
566
|
activationPrompt: "Always apply Git Master rules when the user asks for Git help."
|
|
501
567
|
---
|
|
@@ -505,7 +571,7 @@ activationPrompt: "Always apply Git Master rules when the user asks for Git help
|
|
|
505
571
|
Your skill content goes here...
|
|
506
572
|
```
|
|
507
573
|
|
|
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.
|
|
574
|
+
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
575
|
|
|
510
576
|
---
|
|
511
577
|
|
|
@@ -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
|
+
}
|