skillex 0.2.5 → 0.3.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 CHANGED
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.1] - 2026-04-08
11
+
12
+ ### Fixed
13
+ - Web UI: translate all remaining Portuguese strings to English (`No sources.`, `Sync history`, `Error`/`Done` toasts, `Visible`, `Catalog`, `Skill detail`, `Version`, `Installed`/`Yes`/`No`, `Compatibility`, `Instructions`, `Instructions unavailable`)
14
+ - Web UI dark theme: lift background and surface colors from near-black to a more comfortable dark grey; raise `--text-muted` and `--text-dim` contrast for better readability
15
+
16
+ ## [0.3.0] - 2026-04-08
17
+
18
+ ### Added
19
+ - `skillex ui` now launches a local HTTP server serving the interactive Vue/Vite SPA instead of a terminal TUI
20
+ - New Vue 3 + Vite single-page application under `ui/` with catalog browser, skill detail pages, and router-based navigation
21
+ - `src/web-ui.ts`: self-contained HTTP server with `/api/skills` and `/api/catalog` JSON endpoints and graceful shutdown
22
+ - `src/markdown.ts`: shared Markdown-to-HTML renderer used by the web API
23
+ - Terminal browser detection: `skillex ui` opens the local server in the platform default browser or a detected terminal viewer
24
+
25
+ ### Changed
26
+ - `src/cli.ts`: `ui` command now delegates to the local web-UI server
27
+ - `src/ui.ts`: updated to detect and delegate to terminal browser when available
28
+ - Build: `npm run build` now runs `build:ui` (Vite) followed by `tsc`
29
+
10
30
  ## [0.2.5] - 2026-04-08
11
31
 
12
32
  ### Added
package/README.md CHANGED
@@ -71,6 +71,23 @@ npm install -D skillex
71
71
  npx skillex <command>
72
72
  ```
73
73
 
74
+ ### Local Web UI development
75
+
76
+ When working on the repository itself, the browser UI is a standalone Vue 3 + Vite frontend under [`ui/`](/Users/lgili/Documents/01%20-%20Codes/01%20-%20Github/Skill/ui). The CLI serves the built assets from [`dist-ui/`](/Users/lgili/Documents/01%20-%20Codes/01%20-%20Github/Skill/dist-ui) and keeps all install/remove/update/sync logic in the local TypeScript backend.
77
+
78
+ ```bash
79
+ npm install
80
+ npm run build:ui
81
+ npm run build
82
+ node ./bin/skillex.js ui
83
+ ```
84
+
85
+ For frontend-only iteration:
86
+
87
+ ```bash
88
+ npm run dev:ui
89
+ ```
90
+
74
91
  ---
75
92
 
76
93
  ## Commands
@@ -259,12 +276,25 @@ skillex run git-master:cleanup --yes # skip confirmation
259
276
 
260
277
  ---
261
278
 
279
+ ### Default terminal browser
280
+
281
+ Running `skillex` with no subcommand now opens the interactive terminal browser by default.
282
+
283
+ ```bash
284
+ skillex
285
+ skillex browse
286
+ skillex tui
287
+ ```
288
+
289
+ ---
290
+
262
291
  ### `ui`
263
292
 
264
- Open an interactive terminal UI to browse and install skills.
293
+ Open the local Web UI in your browser.
265
294
 
266
295
  ```bash
267
296
  skillex ui
297
+ skillex ui --global
268
298
  ```
269
299
 
270
300
  ---
package/dist/cli.d.ts CHANGED
@@ -5,3 +5,4 @@
5
5
  * @throws {CliError} When the command or flag values are invalid.
6
6
  */
7
7
  export declare function main(argv: string[]): Promise<void>;
8
+ export declare function resolveCommandRoute(command: string | undefined): string;
package/dist/cli.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import * as path from "node:path";
2
2
  import { listAdapters } from "./adapters.js";
3
- import { computeCatalogCacheKey, loadCatalog, readCatalogCache, searchCatalogSkills, } from "./catalog.js";
3
+ import { computeCatalogCacheKey, readCatalogCache, searchCatalogSkills, } from "./catalog.js";
4
4
  import { DEFAULT_INSTALL_SCOPE, getScopedStatePaths } from "./config.js";
5
5
  import { addProjectSource, getInstalledSkills, initProject, installSkills, listProjectSources, loadProjectCatalogs, removeProjectSource, removeSkills, resolveProjectSource, syncInstalledSkills, updateInstalledSkills, } from "./install.js";
6
6
  import * as output from "./output.js";
7
7
  import { setVerbose } from "./output.js";
8
8
  import { parseSkillCommandReference, runSkillScript } from "./runner.js";
9
9
  import { runInteractiveUi } from "./ui.js";
10
+ import { startWebUiServer } from "./web-ui.js";
10
11
  import { CliError } from "./types.js";
11
12
  import { VALID_CONFIG_KEYS, readUserConfig, writeUserConfig } from "./user-config.js";
12
13
  // ---------------------------------------------------------------------------
@@ -124,7 +125,9 @@ Options:
124
125
 
125
126
  Example:
126
127
  skillex run git-master:cleanup --yes`,
127
- ui: `Usage: skillex ui [options]
128
+ browse: `Usage: skillex browse [options]
129
+ skillex tui [options]
130
+ skillex [options]
128
131
 
129
132
  Open the interactive terminal browser to browse and install skills.
130
133
 
@@ -133,7 +136,21 @@ Options:
133
136
  --no-cache Bypass local catalog cache
134
137
 
135
138
  Example:
136
- skillex ui`,
139
+ skillex
140
+ skillex browse`,
141
+ ui: `Usage: skillex ui [options]
142
+
143
+ Open the local Web UI in your browser.
144
+
145
+ Options:
146
+ --repo <owner/repo> GitHub repository
147
+ --scope <scope> local or global (default: local)
148
+ --global Shortcut for --scope global
149
+ --no-cache Bypass local catalog cache
150
+
151
+ Example:
152
+ skillex ui
153
+ skillex ui --global`,
137
154
  status: `Usage: skillex status [options]
138
155
 
139
156
  Show the installation status of the selected scope.
@@ -206,9 +223,14 @@ export async function main(argv) {
206
223
  if (userConfig.githubToken && !process.env.GITHUB_TOKEN) {
207
224
  process.env.GITHUB_TOKEN = userConfig.githubToken;
208
225
  }
226
+ const resolvedCommand = resolveCommandRoute(command);
227
+ if (flags.help === true && !command) {
228
+ printHelp();
229
+ return;
230
+ }
209
231
  // Per-command --help
210
- if (flags.help === true && command && command !== "help") {
211
- const helpText = COMMAND_HELP[command];
232
+ if (flags.help === true && resolvedCommand && resolvedCommand !== "help") {
233
+ const helpText = COMMAND_HELP[resolvedCommand];
212
234
  if (helpText) {
213
235
  output.info(helpText);
214
236
  }
@@ -217,13 +239,13 @@ export async function main(argv) {
217
239
  }
218
240
  return;
219
241
  }
220
- // Resolve command aliases
221
- const resolvedCommand = resolveAlias(command);
222
242
  switch (resolvedCommand) {
223
243
  case "help":
224
- case undefined:
225
244
  printHelp();
226
245
  return;
246
+ case "browse":
247
+ await handleBrowse(flags, userConfig);
248
+ return;
227
249
  case "init":
228
250
  await handleInit(flags, userConfig);
229
251
  return;
@@ -249,7 +271,7 @@ export async function main(argv) {
249
271
  await handleRun(positionals, flags, userConfig);
250
272
  return;
251
273
  case "ui":
252
- await handleUi(flags, userConfig);
274
+ await handleWebUi(flags, userConfig);
253
275
  return;
254
276
  case "status":
255
277
  await handleStatus(flags, userConfig);
@@ -439,12 +461,11 @@ async function handleRun(positionals, flags, userConfig) {
439
461
  process.exitCode = exitCode;
440
462
  }
441
463
  }
442
- async function handleUi(flags, userConfig) {
464
+ async function handleBrowse(flags, userConfig) {
443
465
  const options = commonOptions(flags, userConfig);
444
466
  const state = await getInstalledSkills(options);
445
- const source = await resolveProjectSource(options);
446
467
  output.statusLine("Fetching catalog...");
447
- const catalog = await loadCatalog({ ...source, ...cacheOptions(options) });
468
+ const catalog = await loadProjectCatalogs({ ...options, ...cacheOptions(options) });
448
469
  output.clearStatus();
449
470
  if (catalog.skills.length === 0) {
450
471
  output.info("No skills available in the catalog.");
@@ -477,6 +498,15 @@ async function handleUi(flags, userConfig) {
477
498
  }
478
499
  printAutoSyncResult(installResult?.autoSync ?? removeResult?.autoSync ?? null);
479
500
  }
501
+ async function handleWebUi(flags, userConfig) {
502
+ const options = commonOptions(flags, userConfig);
503
+ const session = await startWebUiServer(options);
504
+ output.success(`Skillex Web UI running at ${session.url}`);
505
+ if (!session.opened) {
506
+ output.warn("Could not open the browser automatically. Open the URL above manually.");
507
+ }
508
+ output.info("Press Ctrl+C to stop the local server.");
509
+ }
480
510
  async function handleStatus(flags, userConfig) {
481
511
  const options = commonOptions(flags, userConfig);
482
512
  const state = await getInstalledSkills(options);
@@ -737,13 +767,17 @@ async function handleConfig(positionals, flags) {
737
767
  // ---------------------------------------------------------------------------
738
768
  // Helpers
739
769
  // ---------------------------------------------------------------------------
740
- function resolveAlias(command) {
770
+ export function resolveCommandRoute(command) {
741
771
  const ALIASES = {
742
772
  ls: "list",
743
773
  rm: "remove",
744
774
  uninstall: "remove",
775
+ tui: "browse",
745
776
  };
746
- return command !== undefined ? (ALIASES[command] ?? command) : undefined;
777
+ if (command === undefined) {
778
+ return "browse";
779
+ }
780
+ return ALIASES[command] ?? command;
747
781
  }
748
782
  function commonOptions(flags, userConfig = {}) {
749
783
  const options = {
@@ -861,6 +895,7 @@ function printHelp() {
861
895
  output.info(`skillex — AI agent skill manager
862
896
 
863
897
  Commands:
898
+ skillex open the terminal browser
864
899
  skillex init [--repo owner/repo] [--ref main]
865
900
  skillex list [--json]
866
901
  skillex search [query] [--compatibility claude] [--tag git]
@@ -868,10 +903,11 @@ Commands:
868
903
  skillex install --all
869
904
  skillex update [skill-id...]
870
905
  skillex remove <skill-id...> aliases: rm, uninstall
906
+ skillex browse aliases: tui
871
907
  skillex source <add|remove|list> [...]
872
908
  skillex sync [--adapter id] [--dry-run] [--mode copy]
873
909
  skillex run <skill-id:command> [--yes] [--timeout 30]
874
- skillex ui
910
+ skillex ui open local Web UI
875
911
  skillex status [--json]
876
912
  skillex doctor [--json]
877
913
  skillex config set <key> <value>
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Renders a safe HTML representation for common Markdown constructs used by skills.
3
+ *
4
+ * @param markdown - Raw markdown source.
5
+ * @returns Sanitized HTML string.
6
+ */
7
+ export declare function renderMarkdownToHtml(markdown: string): string;
@@ -0,0 +1,193 @@
1
+ function escapeHtml(value) {
2
+ return value
3
+ .replaceAll("&", "&amp;")
4
+ .replaceAll("<", "&lt;")
5
+ .replaceAll(">", "&gt;")
6
+ .replaceAll('"', "&quot;")
7
+ .replaceAll("'", "&#39;");
8
+ }
9
+ function renderInlineMarkdown(value) {
10
+ let html = escapeHtml(value);
11
+ html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, (_match, label, url) => {
12
+ const safeLabel = escapeHtml(label);
13
+ const safeUrl = escapeHtml(url);
14
+ return `<a href="${safeUrl}" target="_blank" rel="noreferrer noopener">${safeLabel}</a>`;
15
+ });
16
+ html = html.replace(/`([^`]+)`/g, (_match, code) => `<code>${escapeHtml(code)}</code>`);
17
+ html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
18
+ html = html.replace(/\*([^*]+)\*/g, "<em>$1</em>");
19
+ return html;
20
+ }
21
+ function flushParagraph(paragraph, html) {
22
+ if (paragraph.length === 0) {
23
+ return;
24
+ }
25
+ html.push(`<p>${paragraph.map((line) => renderInlineMarkdown(line)).join("<br>")}</p>`);
26
+ paragraph.length = 0;
27
+ }
28
+ function closeList(currentList, html) {
29
+ if (currentList.type) {
30
+ html.push(`</${currentList.type}>`);
31
+ currentList.type = null;
32
+ }
33
+ }
34
+ function isTableSeparator(line) {
35
+ const trimmed = line.trim();
36
+ if (!trimmed.startsWith("|") || !trimmed.endsWith("|")) {
37
+ return false;
38
+ }
39
+ return trimmed
40
+ .slice(1, -1)
41
+ .split("|")
42
+ .every((cell) => /^:?-{3,}:?$/.test(cell.trim()));
43
+ }
44
+ function splitTableRow(line) {
45
+ return line
46
+ .trim()
47
+ .slice(1, -1)
48
+ .split("|")
49
+ .map((cell) => renderInlineMarkdown(cell.trim()));
50
+ }
51
+ function consumeTable(lines, startIndex) {
52
+ const headerLine = lines[startIndex]?.trim();
53
+ const separatorLine = lines[startIndex + 1]?.trim();
54
+ if (!headerLine || !separatorLine || !headerLine.startsWith("|") || !headerLine.endsWith("|") || !isTableSeparator(separatorLine)) {
55
+ return null;
56
+ }
57
+ const headers = splitTableRow(headerLine);
58
+ const rows = [];
59
+ let index = startIndex + 2;
60
+ while (index < lines.length) {
61
+ const candidate = lines[index]?.trim();
62
+ if (!candidate || !candidate.startsWith("|") || !candidate.endsWith("|")) {
63
+ break;
64
+ }
65
+ rows.push(splitTableRow(candidate));
66
+ index += 1;
67
+ }
68
+ const html = [
69
+ "<div class=\"markdown-table-wrap\">",
70
+ "<table>",
71
+ "<thead>",
72
+ "<tr>",
73
+ ...headers.map((header) => `<th>${header}</th>`),
74
+ "</tr>",
75
+ "</thead>",
76
+ "<tbody>",
77
+ ...rows.map((row) => `<tr>${row.map((cell) => `<td>${cell}</td>`).join("")}</tr>`),
78
+ "</tbody>",
79
+ "</table>",
80
+ "</div>",
81
+ ].join("");
82
+ return {
83
+ html,
84
+ nextIndex: index - 1,
85
+ };
86
+ }
87
+ /**
88
+ * Renders a safe HTML representation for common Markdown constructs used by skills.
89
+ *
90
+ * @param markdown - Raw markdown source.
91
+ * @returns Sanitized HTML string.
92
+ */
93
+ export function renderMarkdownToHtml(markdown) {
94
+ const lines = markdown.replace(/\r\n/g, "\n").split("\n");
95
+ const html = [];
96
+ const paragraph = [];
97
+ const currentList = { type: null };
98
+ let inCodeFence = false;
99
+ let codeFenceLanguage = "";
100
+ const codeFenceLines = [];
101
+ for (let index = 0; index < lines.length; index += 1) {
102
+ const line = lines[index] ?? "";
103
+ const trimmed = line.trim();
104
+ if (trimmed.startsWith("```")) {
105
+ flushParagraph(paragraph, html);
106
+ closeList(currentList, html);
107
+ if (inCodeFence) {
108
+ const languageClass = codeFenceLanguage ? ` class="language-${escapeHtml(codeFenceLanguage)}"` : "";
109
+ html.push(`<pre><code${languageClass}>${escapeHtml(codeFenceLines.join("\n"))}</code></pre>`);
110
+ codeFenceLines.length = 0;
111
+ codeFenceLanguage = "";
112
+ inCodeFence = false;
113
+ }
114
+ else {
115
+ inCodeFence = true;
116
+ codeFenceLanguage = trimmed.slice(3).trim();
117
+ }
118
+ continue;
119
+ }
120
+ if (inCodeFence) {
121
+ codeFenceLines.push(line);
122
+ continue;
123
+ }
124
+ const table = consumeTable(lines, index);
125
+ if (table) {
126
+ flushParagraph(paragraph, html);
127
+ closeList(currentList, html);
128
+ html.push(table.html);
129
+ index = table.nextIndex;
130
+ continue;
131
+ }
132
+ if (!trimmed) {
133
+ flushParagraph(paragraph, html);
134
+ closeList(currentList, html);
135
+ continue;
136
+ }
137
+ const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
138
+ if (headingMatch) {
139
+ flushParagraph(paragraph, html);
140
+ closeList(currentList, html);
141
+ const level = headingMatch[1]?.length ?? 1;
142
+ html.push(`<h${level}>${renderInlineMarkdown(headingMatch[2] ?? "")}</h${level}>`);
143
+ continue;
144
+ }
145
+ const unorderedMatch = trimmed.match(/^[-*+]\s+(.+)$/);
146
+ if (unorderedMatch) {
147
+ flushParagraph(paragraph, html);
148
+ if (currentList.type !== "ul") {
149
+ closeList(currentList, html);
150
+ currentList.type = "ul";
151
+ html.push("<ul>");
152
+ }
153
+ html.push(`<li>${renderInlineMarkdown(unorderedMatch[1] ?? "")}</li>`);
154
+ continue;
155
+ }
156
+ const orderedMatch = trimmed.match(/^\d+\.\s+(.+)$/);
157
+ if (orderedMatch) {
158
+ flushParagraph(paragraph, html);
159
+ if (currentList.type !== "ol") {
160
+ closeList(currentList, html);
161
+ currentList.type = "ol";
162
+ html.push("<ol>");
163
+ }
164
+ html.push(`<li>${renderInlineMarkdown(orderedMatch[1] ?? "")}</li>`);
165
+ continue;
166
+ }
167
+ const quoteMatch = trimmed.match(/^>\s?(.*)$/);
168
+ if (quoteMatch) {
169
+ flushParagraph(paragraph, html);
170
+ closeList(currentList, html);
171
+ const quoteLines = [renderInlineMarkdown(quoteMatch[1] ?? "")];
172
+ while (index + 1 < lines.length) {
173
+ const next = lines[index + 1]?.trim() ?? "";
174
+ const nextQuote = next.match(/^>\s?(.*)$/);
175
+ if (!nextQuote) {
176
+ break;
177
+ }
178
+ quoteLines.push(renderInlineMarkdown(nextQuote[1] ?? ""));
179
+ index += 1;
180
+ }
181
+ html.push(`<blockquote><p>${quoteLines.join("<br>")}</p></blockquote>`);
182
+ continue;
183
+ }
184
+ paragraph.push(trimmed);
185
+ }
186
+ if (inCodeFence) {
187
+ const languageClass = codeFenceLanguage ? ` class="language-${escapeHtml(codeFenceLanguage)}"` : "";
188
+ html.push(`<pre><code${languageClass}>${escapeHtml(codeFenceLines.join("\n"))}</code></pre>`);
189
+ }
190
+ flushParagraph(paragraph, html);
191
+ closeList(currentList, html);
192
+ return html.join("\n");
193
+ }
package/dist/ui.d.ts CHANGED
@@ -17,21 +17,26 @@ interface UiPrompts {
17
17
  }) => Promise<string[]>) | undefined;
18
18
  }
19
19
  /**
20
- * Filters catalog skills for the interactive UI using a case-insensitive text query.
20
+ * Filters catalog skills for the interactive terminal browser using a case-insensitive text query.
21
21
  *
22
22
  * @param skills - Catalog skills.
23
23
  * @param query - Search text.
24
24
  * @returns Filtered skills in their original order.
25
25
  */
26
- export declare function filterCatalogForUi(skills: SkillManifest[], query: string): SkillManifest[];
26
+ export declare function filterCatalogForUi<T extends SkillManifest>(skills: T[], query: string): T[];
27
27
  /**
28
- * Runs the interactive terminal flow used by `skillex ui`.
28
+ * Runs the interactive terminal browser flow used by `skillex`, `skillex browse`, and `skillex tui`.
29
29
  *
30
30
  * @param options - UI state and optional prompt overrides.
31
31
  * @returns Selected, installable, and removable skill ids.
32
32
  */
33
- export declare function runInteractiveUi(options: {
34
- skills: SkillManifest[];
33
+ export declare function runInteractiveUi<T extends SkillManifest & {
34
+ source?: {
35
+ repo: string;
36
+ label?: string | undefined;
37
+ };
38
+ }>(options: {
39
+ skills: T[];
35
40
  installedIds: string[];
36
41
  prompts?: UiPrompts | undefined;
37
42
  }): Promise<{
package/dist/ui.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Filters catalog skills for the interactive UI using a case-insensitive text query.
2
+ * Filters catalog skills for the interactive terminal browser using a case-insensitive text query.
3
3
  *
4
4
  * @param skills - Catalog skills.
5
5
  * @param query - Search text.
@@ -16,7 +16,7 @@ export function filterCatalogForUi(skills, query) {
16
16
  .includes(normalized));
17
17
  }
18
18
  /**
19
- * Runs the interactive terminal flow used by `skillex ui`.
19
+ * Runs the interactive terminal browser flow used by `skillex`, `skillex browse`, and `skillex tui`.
20
20
  *
21
21
  * @param options - UI state and optional prompt overrides.
22
22
  * @returns Selected, installable, and removable skill ids.
@@ -39,9 +39,12 @@ export async function runInteractiveUi(options) {
39
39
  choices: filteredSkills.map((skill) => {
40
40
  const tags = (skill.tags ?? []).slice(0, 4).join(", ");
41
41
  const detail = tags || (skill.description ?? "").slice(0, 55);
42
+ const source = skill.source && (skill.source.label || skill.source.repo)
43
+ ? ` · ${skill.source.label || skill.source.repo}`
44
+ : "";
42
45
  const label = detail
43
- ? `${skill.name} (${skill.id}) · ${detail}`
44
- : `${skill.name} (${skill.id})`;
46
+ ? `${skill.name} (${skill.id}) · ${detail}${source}`
47
+ : `${skill.name} (${skill.id})${source}`;
45
48
  return {
46
49
  name: label,
47
50
  value: skill.id,
@@ -0,0 +1,31 @@
1
+ import { type IncomingMessage, type ServerResponse } from "node:http";
2
+ import type { CatalogLoader, ProjectOptions, SkillDownloader, SourcedSkillManifest } from "./types.js";
3
+ type BrowserOpener = (url: string) => Promise<void>;
4
+ type SkillBodyLoader = (skill: SourcedSkillManifest, context: {
5
+ options: ProjectOptions;
6
+ }) => Promise<string>;
7
+ export interface WebUiServerOptions extends ProjectOptions {
8
+ host?: string | undefined;
9
+ port?: number | undefined;
10
+ autoOpen?: boolean | undefined;
11
+ openBrowser?: BrowserOpener | undefined;
12
+ catalogLoader?: CatalogLoader | undefined;
13
+ downloader?: SkillDownloader | undefined;
14
+ skillBodyLoader?: SkillBodyLoader | undefined;
15
+ }
16
+ export interface WebUiSession {
17
+ url: string;
18
+ opened: boolean;
19
+ close: () => Promise<void>;
20
+ }
21
+ /**
22
+ * Starts a loopback-only local Web UI server for Skillex and optionally opens the browser.
23
+ */
24
+ export declare function startWebUiServer(options?: WebUiServerOptions): Promise<WebUiSession>;
25
+ /**
26
+ * Creates the HTTP request handler used by the local Web UI server.
27
+ */
28
+ export declare function createWebUiHandler(options: WebUiServerOptions & {
29
+ token: string;
30
+ }): (request: IncomingMessage, response: ServerResponse) => Promise<void>;
31
+ export {};