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/dist/http.js CHANGED
@@ -1,25 +1,72 @@
1
1
  /**
2
2
  * HTTP fetch utilities with GitHub API support.
3
3
  *
4
- * Automatically attaches GitHub headers and, when `GITHUB_TOKEN` is set in
5
- * the environment, an `Authorization: Bearer` header to raise the API rate
6
- * limit from 60 to 5,000 requests per hour.
4
+ * Automatically attaches a default 30-second timeout, a `User-Agent`, and a
5
+ * GitHub `Accept` header. When `GITHUB_TOKEN` is set in the environment, an
6
+ * `Authorization: Bearer` header is attached **only for GitHub hosts** so the
7
+ * token does not leak to third-party `--catalog-url` targets.
8
+ *
9
+ * HTTP errors raise typed `HttpError` instances with codes that distinguish
10
+ * timeouts, rate limits, auth failures, and server errors.
7
11
  */
8
12
  import { debug } from "./output.js";
13
+ import { HttpError } from "./types.js";
14
+ let defaultHttpTimeoutMs = 30_000;
15
+ /**
16
+ * Overrides the default HTTP timeout (in milliseconds) used when callers do
17
+ * not pass their own `init.signal`. Primarily for tests; production callers
18
+ * should rely on the default.
19
+ *
20
+ * @param ms - Positive integer in milliseconds.
21
+ */
22
+ export function setDefaultHttpTimeoutMs(ms) {
23
+ if (!Number.isFinite(ms) || ms <= 0) {
24
+ throw new RangeError(`Invalid HTTP timeout: ${ms}`);
25
+ }
26
+ defaultHttpTimeoutMs = Math.floor(ms);
27
+ }
28
+ /**
29
+ * Returns the current default HTTP timeout in milliseconds.
30
+ */
31
+ export function getDefaultHttpTimeoutMs() {
32
+ return defaultHttpTimeoutMs;
33
+ }
34
+ /**
35
+ * Hostnames that are considered GitHub-owned for the purpose of attaching the
36
+ * `Authorization: Bearer ${GITHUB_TOKEN}` header.
37
+ */
38
+ const GITHUB_HOSTS = new Set(["api.github.com", "raw.githubusercontent.com"]);
39
+ /**
40
+ * Returns `true` when the URL targets a GitHub-owned host (api or raw mirrors,
41
+ * including any `*.githubusercontent.com` subdomain).
42
+ */
43
+ export function isGitHubHost(url) {
44
+ try {
45
+ const parsed = new URL(url);
46
+ if (GITHUB_HOSTS.has(parsed.hostname)) {
47
+ return true;
48
+ }
49
+ if (parsed.hostname.endsWith(".githubusercontent.com")) {
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
9
58
  /**
10
59
  * Fetches a JSON document and parses it with default Skillex headers.
11
60
  *
12
61
  * @param url - Target URL.
13
62
  * @param init - Fetch init overrides.
14
63
  * @returns Parsed JSON payload.
15
- * @throws {Error} With an actionable message on HTTP errors.
64
+ * @throws {HttpError} On non-2xx responses or timeouts.
16
65
  */
17
66
  export async function fetchJson(url, init = {}) {
18
- debug(`GET ${url}`);
19
- const response = await fetch(url, withDefaultHeaders(init));
20
- debug(`${response.status} ${url}`);
67
+ const response = await fetchWithDefaults(url, init);
21
68
  if (!response.ok) {
22
- throw new Error(buildHttpErrorMessage(url, response.status));
69
+ throw await buildHttpError(url, response);
23
70
  }
24
71
  return (await response.json());
25
72
  }
@@ -29,14 +76,12 @@ export async function fetchJson(url, init = {}) {
29
76
  * @param url - Target URL.
30
77
  * @param init - Fetch init overrides.
31
78
  * @returns Response text body.
32
- * @throws {Error} With an actionable message on HTTP errors.
79
+ * @throws {HttpError} On non-2xx responses or timeouts.
33
80
  */
34
81
  export async function fetchText(url, init = {}) {
35
- debug(`GET ${url}`);
36
- const response = await fetch(url, withDefaultHeaders(init));
37
- debug(`${response.status} ${url}`);
82
+ const response = await fetchWithDefaults(url, init);
38
83
  if (!response.ok) {
39
- throw new Error(buildHttpErrorMessage(url, response.status));
84
+ throw await buildHttpError(url, response);
40
85
  }
41
86
  return response.text();
42
87
  }
@@ -46,17 +91,15 @@ export async function fetchText(url, init = {}) {
46
91
  * @param url - Target URL.
47
92
  * @param init - Fetch init overrides.
48
93
  * @returns Response text body or `null` for HTTP 404.
49
- * @throws {Error} With an actionable message on non-404 HTTP errors.
94
+ * @throws {HttpError} On non-404 non-2xx responses or timeouts.
50
95
  */
51
96
  export async function fetchOptionalText(url, init = {}) {
52
- debug(`GET ${url}`);
53
- const response = await fetch(url, withDefaultHeaders(init));
54
- debug(`${response.status} ${url}`);
97
+ const response = await fetchWithDefaults(url, init);
55
98
  if (response.status === 404) {
56
99
  return null;
57
100
  }
58
101
  if (!response.ok) {
59
- throw new Error(buildHttpErrorMessage(url, response.status));
102
+ throw await buildHttpError(url, response);
60
103
  }
61
104
  return response.text();
62
105
  }
@@ -66,28 +109,44 @@ export async function fetchOptionalText(url, init = {}) {
66
109
  * @param url - Target URL.
67
110
  * @param init - Fetch init overrides.
68
111
  * @returns Parsed JSON payload or `null` for HTTP 404.
69
- * @throws {Error} With an actionable message on non-404 HTTP errors.
112
+ * @throws {HttpError} On non-404 non-2xx responses or timeouts.
70
113
  */
71
114
  export async function fetchOptionalJson(url, init = {}) {
72
- debug(`GET ${url}`);
73
- const response = await fetch(url, withDefaultHeaders(init));
74
- debug(`${response.status} ${url}`);
115
+ const response = await fetchWithDefaults(url, init);
75
116
  if (response.status === 404) {
76
117
  return null;
77
118
  }
78
119
  if (!response.ok) {
79
- throw new Error(buildHttpErrorMessage(url, response.status));
120
+ throw await buildHttpError(url, response);
80
121
  }
81
122
  return (await response.json());
82
123
  }
124
+ /**
125
+ * Performs a `fetch` with default headers and a default abort timeout. Wraps
126
+ * abort errors into a typed `HttpError` with code `HTTP_TIMEOUT`.
127
+ */
128
+ async function fetchWithDefaults(url, init) {
129
+ debug(`GET ${url}`);
130
+ const { merged, attachedTimeout } = withDefaultHeaders(url, init);
131
+ try {
132
+ const response = await fetch(url, merged);
133
+ debug(`${response.status} ${url}`);
134
+ return response;
135
+ }
136
+ catch (error) {
137
+ if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) {
138
+ const timeout = attachedTimeout ?? defaultHttpTimeoutMs;
139
+ throw new HttpError(`Request timed out after ${timeout}ms: ${url}`, "HTTP_TIMEOUT", { url });
140
+ }
141
+ throw error;
142
+ }
143
+ }
83
144
  /**
84
145
  * Applies default HTTP headers expected by GitHub-hosted catalog requests.
85
- * Attaches `Authorization: Bearer` when `GITHUB_TOKEN` is set in the environment.
86
- *
87
- * @param init - User-supplied fetch options.
88
- * @returns Fetch options with default headers merged in.
146
+ * Attaches `Authorization: Bearer` only when `GITHUB_TOKEN` is set AND the
147
+ * target host is GitHub-owned.
89
148
  */
90
- function withDefaultHeaders(init) {
149
+ function withDefaultHeaders(url, init) {
91
150
  const headers = new Headers(init.headers || {});
92
151
  if (!headers.has("User-Agent")) {
93
152
  headers.set("User-Agent", "skillex");
@@ -97,27 +156,69 @@ function withDefaultHeaders(init) {
97
156
  }
98
157
  const token = process.env.GITHUB_TOKEN;
99
158
  if (token && !headers.has("Authorization")) {
100
- headers.set("Authorization", `Bearer ${token}`);
101
- debug("Using GITHUB_TOKEN for authentication");
159
+ if (isGitHubHost(url)) {
160
+ headers.set("Authorization", `Bearer ${token}`);
161
+ debug("Using GITHUB_TOKEN for authentication");
162
+ }
163
+ else {
164
+ debug(`GITHUB_TOKEN suppressed: non-GitHub host (${safeHost(url)})`);
165
+ }
166
+ }
167
+ let attachedTimeout = null;
168
+ let signal = init.signal;
169
+ if (signal === undefined || signal === null) {
170
+ attachedTimeout = defaultHttpTimeoutMs;
171
+ signal = AbortSignal.timeout(defaultHttpTimeoutMs);
172
+ }
173
+ const merged = {
174
+ ...init,
175
+ headers,
176
+ signal,
177
+ };
178
+ return { merged, attachedTimeout };
179
+ }
180
+ function safeHost(url) {
181
+ try {
182
+ return new URL(url).hostname;
183
+ }
184
+ catch {
185
+ return "<invalid-url>";
102
186
  }
103
- return { ...init, headers };
104
187
  }
105
188
  /**
106
- * Builds a human-readable, actionable error message for HTTP failures.
107
- *
108
- * @param url - The URL that failed.
109
- * @param status - HTTP response status code.
110
- * @returns Descriptive error message.
189
+ * Builds a typed `HttpError` for a non-2xx response, splitting 403 into
190
+ * rate-limit vs auth based on `X-RateLimit-Remaining`.
111
191
  */
112
- function buildHttpErrorMessage(url, status) {
113
- if (status === 403) {
114
- return "GitHub API rate limit exceeded or access denied. Set the GITHUB_TOKEN environment variable to authenticate.";
192
+ async function buildHttpError(url, response) {
193
+ const status = response.status;
194
+ if (status === 403 || status === 401) {
195
+ const remainingHeader = response.headers.get("x-ratelimit-remaining");
196
+ const isRateLimited = status === 403 && remainingHeader !== null && remainingHeader.trim() === "0";
197
+ if (isRateLimited) {
198
+ const reset = response.headers.get("x-ratelimit-reset");
199
+ const resetHint = reset ? buildResetHint(reset) : "Set GITHUB_TOKEN to raise the limit.";
200
+ return new HttpError(`GitHub API rate limit exceeded. ${resetHint}`, "HTTP_RATE_LIMIT", { status, url });
201
+ }
202
+ return new HttpError("GitHub API authentication failed. Check that GITHUB_TOKEN is valid and has access.", "HTTP_AUTH_FAILED", { status, url });
115
203
  }
116
204
  if (status === 404) {
117
- return `Repository or file not found. Check that --repo is correct and the repository is public. (${url})`;
205
+ return new HttpError(`Repository or file not found. Check that --repo is correct and the repository is public. (${url})`, "HTTP_NOT_FOUND", { status, url });
118
206
  }
119
207
  if (status >= 500) {
120
- return `GitHub API returned a server error (${status}). Try again in a moment.`;
208
+ return new HttpError(`GitHub API returned a server error (${status}). Try again in a moment.`, "HTTP_SERVER_ERROR", { status, url });
209
+ }
210
+ return new HttpError(`Failed to fetch ${url} (HTTP ${status})`, "HTTP_ERROR", { status, url });
211
+ }
212
+ function buildResetHint(resetEpoch) {
213
+ const epoch = Number(resetEpoch);
214
+ if (!Number.isFinite(epoch)) {
215
+ return "Set GITHUB_TOKEN to raise the limit.";
216
+ }
217
+ const resetDate = new Date(epoch * 1000);
218
+ const seconds = Math.max(0, Math.round((resetDate.getTime() - Date.now()) / 1000));
219
+ if (seconds < 60) {
220
+ return `Resets in ${seconds}s. Set GITHUB_TOKEN to raise the limit.`;
121
221
  }
122
- return `Failed to fetch ${url} (HTTP ${status})`;
222
+ const minutes = Math.round(seconds / 60);
223
+ return `Resets in ~${minutes}m. Set GITHUB_TOKEN to raise the limit.`;
123
224
  }
package/dist/install.d.ts CHANGED
@@ -1,4 +1,26 @@
1
- import type { AggregatedCatalogData, CatalogLoader, CatalogSource, DirectGitHubRef, InitProjectResult, InstallSkillsResult, LockfileSource, LockfileState, ProjectOptions, RemoveSkillsResult, SkillDownloader, SyncCommandResult, UpdateInstalledSkillsResult } from "./types.js";
1
+ /**
2
+ * Install / update / remove orchestration for catalog and direct-GitHub skills.
3
+ *
4
+ * Historically this module owned every install-related concern. Lockfile
5
+ * shape, direct-GitHub install, auto-sync, and the shared file downloader
6
+ * have been extracted into focused modules. This file now contains only
7
+ * orchestration and re-exports the moved symbols so existing imports keep
8
+ * working until callers migrate to the canonical paths.
9
+ *
10
+ * Re-export shim → canonical module mapping:
11
+ * - lockfile shape and source-list helpers → ./lockfile.js
12
+ * - direct GitHub parsing / fetch / download → ./direct-github.js
13
+ * - auto-sync orchestration → ./auto-sync.js
14
+ * - shared per-file download helper → ./downloader.js
15
+ */
16
+ import type { AggregatedCatalogData, CatalogLoader, CatalogSource, InitProjectResult, InstallSkillsResult, LockfileSource, LockfileState, ProjectOptions, RemoveSkillsResult, SkillDownloader, SyncCommandResult, UpdateInstalledSkillsResult } from "./types.js";
17
+ export { createBaseLockfile, dedupeSources, getLockfileSources, normalizeLockfile, normalizeSyncHistory, parseCatalogSource, PLACEHOLDER_REPOS, toLockfileSource, } from "./lockfile.js";
18
+ export { confirmDirectInstall, downloadDirectGitHubSkill, fetchDirectGitHubSkill, normalizeDirectManifest, parseDirectGitHubRef, parseGitHubSource, } from "./direct-github.js";
19
+ export type { DirectInstallPayload } from "./direct-github.js";
20
+ export { maybeAutoSync, maybeSyncAfterRemove, resolveSyncAdapterIds, } from "./auto-sync.js";
21
+ export type { SyncFn } from "./auto-sync.js";
22
+ export { downloadSkillFiles, writeDownloadedManifest, } from "./downloader.js";
23
+ export type { DownloadedSkillManifest } from "./downloader.js";
2
24
  interface InstallOptions extends ProjectOptions {
3
25
  catalogLoader?: CatalogLoader;
4
26
  downloader?: SkillDownloader;
@@ -105,11 +127,3 @@ export declare function removeProjectSource(repo: string, options?: ProjectOptio
105
127
  * @returns Normalized source list.
106
128
  */
107
129
  export declare function listProjectSources(options?: ProjectOptions): Promise<LockfileSource[]>;
108
- /**
109
- * Parses a direct GitHub install reference in `owner/repo[@ref]` format.
110
- *
111
- * @param input - User-supplied install argument.
112
- * @returns Parsed direct GitHub reference or `null` when the value is not a direct ref.
113
- */
114
- export declare function parseDirectGitHubRef(input: string): DirectGitHubRef | null;
115
- export {};