@repodeckhz/core 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1096 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ Cache: () => Cache,
34
+ CardBuilder: () => CardBuilder,
35
+ GitHubClient: () => GitHubClient,
36
+ GraphQLClient: () => GraphQLClient,
37
+ RepoDeckError: () => RepoDeckError,
38
+ assignBentoSpans: () => assignBentoSpans,
39
+ buildCacheKey: () => buildCacheKey,
40
+ buildCardData: () => buildCardData,
41
+ buildRawUrl: () => buildRawUrl,
42
+ checkRateLimit: () => checkRateLimit,
43
+ clearCache: () => clearCache,
44
+ defaultPresets: () => defaultPresets,
45
+ fetchDirectoryListing: () => fetchDirectoryListing,
46
+ fetchFileContent: () => fetchFileContent,
47
+ fetchMultipleReposViaGraphQL: () => fetchMultipleReposViaGraphQL,
48
+ fetchRepoStats: () => fetchRepoStats,
49
+ getCached: () => getCached,
50
+ getPreset: () => getPreset,
51
+ listPresets: () => listPresets,
52
+ mergePresetWithUserConfig: () => mergePresetWithUserConfig,
53
+ parseReadme: () => parseReadme,
54
+ parseReadmeFrontmatter: () => parseReadmeFrontmatter,
55
+ parseResume: () => parseResume,
56
+ prefetchCardData: () => prefetchCardData,
57
+ registerPreset: () => registerPreset,
58
+ repodeckError: () => repodeckError,
59
+ resolveRadiusTokens: () => resolveRadiusTokens,
60
+ resolveScreenshotLayout: () => resolveScreenshotLayout,
61
+ resolveScreenshots: () => resolveScreenshots,
62
+ setCached: () => setCached,
63
+ validateConfig: () => validateConfig
64
+ });
65
+ module.exports = __toCommonJS(index_exports);
66
+
67
+ // src/errors.ts
68
+ var RepoDeckError = class extends Error {
69
+ constructor(code, message, opts) {
70
+ super(message);
71
+ this.name = "RepoDeckError";
72
+ this.code = code;
73
+ if (opts?.status !== void 0) this.status = opts.status;
74
+ if (opts?.field !== void 0) this.field = opts.field;
75
+ if (opts?.cause !== void 0) this.cause = opts.cause;
76
+ }
77
+ toJSON() {
78
+ return {
79
+ name: this.name,
80
+ code: this.code,
81
+ message: this.message,
82
+ status: this.status,
83
+ field: this.field
84
+ };
85
+ }
86
+ };
87
+ var repodeckError = RepoDeckError;
88
+
89
+ // src/cache.ts
90
+ var DEFAULT_TTL_MS = 5 * 60 * 1e3;
91
+ var Cache = class {
92
+ constructor(defaultTtlMs = DEFAULT_TTL_MS) {
93
+ this.defaultTtlMs = defaultTtlMs;
94
+ this.store = /* @__PURE__ */ new Map();
95
+ }
96
+ get(key) {
97
+ const entry = this.store.get(key);
98
+ if (!entry) return void 0;
99
+ if (Date.now() > entry.expiresAt) {
100
+ this.store.delete(key);
101
+ return void 0;
102
+ }
103
+ return entry.value;
104
+ }
105
+ set(key, value, ttlMs = this.defaultTtlMs) {
106
+ this.store.set(key, { value, expiresAt: Date.now() + ttlMs });
107
+ }
108
+ /** Returns the cached entry including its expiry (for introspection). */
109
+ getEntry(key) {
110
+ const entry = this.store.get(key);
111
+ if (!entry) return void 0;
112
+ if (Date.now() > entry.expiresAt) {
113
+ this.store.delete(key);
114
+ return void 0;
115
+ }
116
+ return entry;
117
+ }
118
+ delete(key) {
119
+ this.store.delete(key);
120
+ }
121
+ clear() {
122
+ this.store.clear();
123
+ }
124
+ get size() {
125
+ return this.store.size;
126
+ }
127
+ /** Builds a deterministic cache key from card request parameters. */
128
+ static buildKey(parts) {
129
+ return `rc:${parts.owner}/${parts.repo}@${parts.branch}:${parts.configPath}:${parts.include}`;
130
+ }
131
+ };
132
+ var sharedCache = new Cache();
133
+ function getCached(key) {
134
+ return sharedCache.getEntry(key);
135
+ }
136
+ function setCached(key, value, ttlMs = DEFAULT_TTL_MS) {
137
+ sharedCache.set(key, value, ttlMs);
138
+ }
139
+ function clearCache(key) {
140
+ if (key) sharedCache.delete(key);
141
+ else sharedCache.clear();
142
+ }
143
+ function buildCacheKey(parts) {
144
+ return Cache.buildKey(parts);
145
+ }
146
+
147
+ // src/presets.ts
148
+ var defaultPresets = {
149
+ /** title + 1 screenshot cover */
150
+ minimal: {
151
+ include: { readme: false, resume: false, screenshots: true },
152
+ readmePreview: false,
153
+ showTitle: true
154
+ },
155
+ /** title + resume + screenshot cover */
156
+ standard: {
157
+ include: { readme: true, resume: true, screenshots: true },
158
+ readmePreview: false,
159
+ showTitle: true
160
+ },
161
+ /** title + resume + screenshots + README preview line */
162
+ detailed: {
163
+ include: { readme: true, resume: true, screenshots: true },
164
+ readmePreview: true,
165
+ readmePreviewChars: 140,
166
+ showTitle: true
167
+ }
168
+ };
169
+ var registry = new Map(Object.entries(defaultPresets));
170
+ function registerPreset(name, definition) {
171
+ registry.set(name, definition);
172
+ }
173
+ function getPreset(name) {
174
+ const def = registry.get(name);
175
+ if (!def) {
176
+ throw new repodeckError(
177
+ "INVALID_CONFIG",
178
+ `Unknown preset "${name}". Available: ${[...registry.keys()].join(", ")}`
179
+ );
180
+ }
181
+ return def;
182
+ }
183
+ function listPresets() {
184
+ return [...registry.keys()];
185
+ }
186
+ function mergePresetWithUserConfig(preset, overrides, name = "custom") {
187
+ return {
188
+ name,
189
+ ...preset,
190
+ ...overrides,
191
+ include: { ...preset.include, ...overrides.include ?? {} }
192
+ };
193
+ }
194
+
195
+ // src/radius.ts
196
+ var PRESET_VALUES = {
197
+ sharp: { card: "2px", button: "2px", modal: "2px" },
198
+ soft: { card: "12px", button: "8px", modal: "16px" },
199
+ round: { card: "22px", button: "999px", modal: "24px" }
200
+ };
201
+ function isPresetName(v) {
202
+ return v === "sharp" || v === "soft" || v === "round";
203
+ }
204
+ function looksLikeCssLength(v) {
205
+ return /^-?\d*\.?\d+(px|rem|em|%|vh|vw|vmin|vmax|ch|ex|pt|pc|in|cm|mm)?$/.test(v.trim());
206
+ }
207
+ function resolveRadiusTokens(radiusAttr) {
208
+ const trimmed = (radiusAttr ?? "").trim();
209
+ if (trimmed === "") return PRESET_VALUES.soft;
210
+ if (isPresetName(trimmed)) return PRESET_VALUES[trimmed];
211
+ if (looksLikeCssLength(trimmed)) {
212
+ return { card: trimmed, button: trimmed, modal: trimmed };
213
+ }
214
+ return PRESET_VALUES.soft;
215
+ }
216
+
217
+ // src/layout.ts
218
+ function assignBentoSpans(count) {
219
+ if (count <= 0) return [];
220
+ if (count === 1) return [{ colSpan: 2, rowSpan: 2, hero: true }];
221
+ if (count === 2) {
222
+ return [
223
+ { colSpan: 2, rowSpan: 2, hero: true },
224
+ { colSpan: 2, rowSpan: 2, hero: false }
225
+ ];
226
+ }
227
+ if (count === 3) {
228
+ return [
229
+ { colSpan: 2, rowSpan: 2, hero: true },
230
+ { colSpan: 2, rowSpan: 1, hero: false },
231
+ { colSpan: 2, rowSpan: 1, hero: false }
232
+ ];
233
+ }
234
+ const spans = [
235
+ { colSpan: 2, rowSpan: 2, hero: true }
236
+ ];
237
+ for (let i = 1; i < count; i++) {
238
+ spans.push({ colSpan: 1, rowSpan: 1, hero: false });
239
+ }
240
+ return spans;
241
+ }
242
+ function resolveScreenshotLayout(screenshots, context) {
243
+ if (screenshots.length === 0) {
244
+ return { mode: "single", items: [], total: 0, hidden: 0 };
245
+ }
246
+ const total = screenshots.length;
247
+ if (context === "modal") {
248
+ if (total === 1) {
249
+ return {
250
+ mode: "single",
251
+ items: [{ screenshot: screenshots[0], colSpan: 1, rowSpan: 1, hero: true }],
252
+ total,
253
+ hidden: 0
254
+ };
255
+ }
256
+ const spans = assignBentoSpans(total);
257
+ return {
258
+ mode: "bento",
259
+ items: screenshots.map((s, i) => ({
260
+ screenshot: s,
261
+ colSpan: spans[i].colSpan,
262
+ rowSpan: spans[i].rowSpan,
263
+ hero: spans[i].hero
264
+ })),
265
+ total,
266
+ hidden: 0
267
+ };
268
+ }
269
+ const hidden = total - 1;
270
+ const items = [
271
+ { screenshot: screenshots[0], colSpan: 2, rowSpan: 2, hero: true }
272
+ ];
273
+ if (hidden > 0) items[0].overflow = hidden;
274
+ return { mode: "single", items, total, hidden };
275
+ }
276
+
277
+ // src/debug.ts
278
+ function isDebugEnabled(flag) {
279
+ if (flag) return true;
280
+ const env = process.env.REPODECK_DEBUG;
281
+ return env === "1" || env === "true" || env === "TRUE" || env === "yes";
282
+ }
283
+ function dbgLog(enabled, scope, ...args) {
284
+ if (!enabled) return;
285
+ console.log(`[repodeck:${scope}]`, ...args);
286
+ }
287
+
288
+ // src/github-client.ts
289
+ var API_BASE = "https://api.github.com";
290
+ var GitHubClient = class _GitHubClient {
291
+ constructor(options = {}) {
292
+ this.token = options.token;
293
+ this.timeoutMs = options.timeoutMs ?? 12e3;
294
+ this.retries = options.retries ?? 2;
295
+ this.debug = isDebugEnabled(options.debug);
296
+ }
297
+ /** Creates a client from `FetchOptions` (backward-compatible with the old API). */
298
+ static from(options = {}) {
299
+ return new _GitHubClient(options);
300
+ }
301
+ headers() {
302
+ const h = {
303
+ Accept: "application/vnd.github+json",
304
+ "X-GitHub-Api-Version": "2022-11-28",
305
+ "User-Agent": "repodeck-core"
306
+ };
307
+ if (this.token) h.Authorization = `Bearer ${this.token}`;
308
+ return h;
309
+ }
310
+ /** Retry wrapper — only retries on network errors / 5xx, never on 404/401/403. */
311
+ async fetchWithRetry(url) {
312
+ const path = urlPath(url);
313
+ const started = Date.now();
314
+ let lastErr;
315
+ for (let attempt = 0; attempt <= this.retries; attempt++) {
316
+ const controller = new AbortController();
317
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
318
+ try {
319
+ const res = await fetch(url, { headers: this.headers(), signal: controller.signal });
320
+ clearTimeout(timer);
321
+ const ms = Date.now() - started;
322
+ const rate = res.headers.get("x-ratelimit-remaining") ? `${res.headers.get("x-ratelimit-remaining")}/${res.headers.get("x-ratelimit-limit")}` : "n/a";
323
+ if (res.status >= 500 && attempt < this.retries) {
324
+ dbgLog(this.debug, "http", `${path} \u2192 ${res.status} (retry ${attempt + 1}/${this.retries}) ${ms}ms rl=${rate}`);
325
+ await sleep(2 ** attempt * 250);
326
+ continue;
327
+ }
328
+ dbgLog(this.debug, "http", `${path} \u2192 ${res.status} ${ms}ms rl=${rate} attempt=${attempt + 1}`);
329
+ return res;
330
+ } catch (err) {
331
+ clearTimeout(timer);
332
+ lastErr = err;
333
+ const ms = Date.now() - started;
334
+ dbgLog(this.debug, "http", `${path} \u2192 network error after ${ms}ms (retry ${attempt + 1}/${this.retries})`);
335
+ if (attempt < this.retries) {
336
+ await sleep(2 ** attempt * 250);
337
+ continue;
338
+ }
339
+ }
340
+ }
341
+ throw new repodeckError("NETWORK_ERROR", `Network error fetching ${url}`, { cause: lastErr });
342
+ }
343
+ static decodeBase64(b64) {
344
+ const clean = b64.replace(/\n/g, "");
345
+ if (typeof Buffer !== "undefined") {
346
+ return Buffer.from(clean, "base64").toString("utf-8");
347
+ }
348
+ const binary = atob(clean);
349
+ const bytes = new Uint8Array(binary.length);
350
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
351
+ return new TextDecoder().decode(bytes);
352
+ }
353
+ /** Fetches a single file's content. 404 → null (not an error). */
354
+ async fetchFileContent(owner, repo, path, ref) {
355
+ const url = `${API_BASE}/repos/${enc(owner)}/${enc(repo)}/contents/${enc(path)}?ref=${enc(ref ?? "main")}`;
356
+ const res = await this.fetchWithRetry(url);
357
+ if (res.status === 404) return null;
358
+ this.assertOk(res, path);
359
+ const data = await res.json();
360
+ if (data.encoding === "base64" && typeof data.content === "string") {
361
+ return _GitHubClient.decodeBase64(data.content);
362
+ }
363
+ return typeof data.content === "string" ? data.content : null;
364
+ }
365
+ /** Lists files in a directory. 404 → empty array. */
366
+ async fetchDirectoryListing(owner, repo, path, ref) {
367
+ const url = `${API_BASE}/repos/${enc(owner)}/${enc(repo)}/contents/${enc(path)}?ref=${enc(ref ?? "main")}`;
368
+ const res = await this.fetchWithRetry(url);
369
+ if (res.status === 404) return [];
370
+ this.assertOk(res, path);
371
+ const data = await res.json();
372
+ return data.filter((e) => e.type === "file").map((e) => ({ name: e.name, path: e.path, download_url: e.download_url, size: e.size }));
373
+ }
374
+ /** Fetches repo metadata and statistics in a single API call. */
375
+ async fetchRepoStats(owner, repo) {
376
+ const url = `${API_BASE}/repos/${enc(owner)}/${enc(repo)}`;
377
+ const res = await this.fetchWithRetry(url);
378
+ if (res.status === 404) {
379
+ throw new repodeckError("NOT_FOUND", `Repository ${owner}/${repo} not found.`, { status: 404 });
380
+ }
381
+ this.assertOk(res, "repo stats");
382
+ const data = await res.json();
383
+ return {
384
+ stars: data.stargazers_count ?? 0,
385
+ forks: data.forks_count ?? 0,
386
+ watchers: data.watchers_count ?? 0,
387
+ openIssues: data.open_issues_count ?? 0,
388
+ defaultBranch: data.default_branch ?? "main",
389
+ pushedAt: data.pushed_at ? Date.parse(data.pushed_at) : 0,
390
+ // Metadata — replaces manual frontmatter fields
391
+ repoName: data.name,
392
+ repoDescription: data.description ?? null,
393
+ topics: Array.isArray(data.topics) ? data.topics : [],
394
+ language: data.language ?? null,
395
+ homepage: data.homepage || null,
396
+ ownerLogin: data.owner?.login ?? owner,
397
+ ownerAvatarUrl: data.owner?.avatar_url ?? ""
398
+ };
399
+ }
400
+ /** Checks the current rate-limit budget. */
401
+ async checkRateLimit() {
402
+ const res = await this.fetchWithRetry(`${API_BASE}/rate_limit`);
403
+ if (!res.ok) {
404
+ throw new repodeckError("NETWORK_ERROR", `rate_limit endpoint returned ${res.status}`, { status: res.status });
405
+ }
406
+ const data = await res.json();
407
+ return { remaining: data.resources.core.remaining, limit: data.resources.core.limit, resetAt: data.resources.core.reset * 1e3 };
408
+ }
409
+ assertOk(res, context) {
410
+ if (res.status === 401) {
411
+ throw new repodeckError("UNAUTHORIZED", "GitHub token is invalid or expired.", { status: 401 });
412
+ }
413
+ if (res.status === 403) {
414
+ const remaining = res.headers.get("x-ratelimit-remaining");
415
+ if (remaining === "0") {
416
+ throw new repodeckError("RATE_LIMITED", "GitHub API rate limit exceeded.", { status: 403 });
417
+ }
418
+ throw new repodeckError("UNAUTHORIZED", "GitHub API returned 403 Forbidden.", { status: 403 });
419
+ }
420
+ if (!res.ok) {
421
+ throw new repodeckError("NETWORK_ERROR", `GitHub API ${res.status} for ${context}`, { status: res.status });
422
+ }
423
+ }
424
+ };
425
+ function buildRawUrl(owner, repo, branch, path) {
426
+ return `https://raw.githubusercontent.com/${enc(owner)}/${enc(repo)}/${enc(branch)}/${path.split("/").map(enc).join("/")}`;
427
+ }
428
+ async function fetchFileContent(owner, repo, path, options = {}) {
429
+ return GitHubClient.from(options).fetchFileContent(owner, repo, path, options.branch);
430
+ }
431
+ async function fetchDirectoryListing(owner, repo, path, options = {}) {
432
+ return GitHubClient.from(options).fetchDirectoryListing(owner, repo, path, options.branch);
433
+ }
434
+ async function fetchRepoStats(owner, repo, options = {}) {
435
+ return GitHubClient.from(options).fetchRepoStats(owner, repo);
436
+ }
437
+ async function checkRateLimit(options = {}) {
438
+ return GitHubClient.from(options).checkRateLimit();
439
+ }
440
+ function enc(s) {
441
+ return encodeURIComponent(s);
442
+ }
443
+ function sleep(ms) {
444
+ return new Promise((r) => setTimeout(r, ms));
445
+ }
446
+ function urlPath(url) {
447
+ try {
448
+ return new URL(url).pathname;
449
+ } catch {
450
+ return url;
451
+ }
452
+ }
453
+
454
+ // src/parser.ts
455
+ var IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg"];
456
+ var _marked = null;
457
+ var _DOMPurify = null;
458
+ async function loadMarked() {
459
+ if (!_marked) {
460
+ const mod = await import("marked");
461
+ _marked = mod.marked;
462
+ }
463
+ return _marked;
464
+ }
465
+ async function loadDOMPurify() {
466
+ if (!_DOMPurify) {
467
+ const mod = await import("isomorphic-dompurify");
468
+ _DOMPurify = mod.default;
469
+ }
470
+ installLinkHardeningHook();
471
+ return _DOMPurify;
472
+ }
473
+ var _linkHookInstalled = false;
474
+ function installLinkHardeningHook() {
475
+ if (_linkHookInstalled || !_DOMPurify) return;
476
+ _linkHookInstalled = true;
477
+ _DOMPurify.addHook("afterSanitizeAttributes", (node) => {
478
+ if (node.tagName === "A") {
479
+ node.setAttribute("target", "_blank");
480
+ node.setAttribute("rel", "noopener noreferrer");
481
+ }
482
+ });
483
+ }
484
+ var DEFAULT_RESUME_MAX = 280;
485
+ function parseReadmeFrontmatter(rawMarkdown) {
486
+ const m = String(rawMarkdown ?? "").match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
487
+ if (!m) return { metadata: {}, content: rawMarkdown ?? "" };
488
+ const yamlBlock = m[1];
489
+ const content = (rawMarkdown ?? "").slice(m[0].length);
490
+ const metadata = {};
491
+ yamlBlock.split("\n").forEach((line) => {
492
+ const kv = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.+)$/);
493
+ if (!kv) return;
494
+ const key = kv[1].trim();
495
+ const val = kv[2].trim().replace(/^["']|["']$/g, "");
496
+ if (key === "accent" && !/^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/.test(val)) return;
497
+ if (key === "order") {
498
+ const n = Number(val);
499
+ if (!isNaN(n)) metadata[key] = n;
500
+ return;
501
+ }
502
+ if (["title", "accent", "order", "description", "tags", "topics", "author"].includes(key)) {
503
+ metadata[key] = val;
504
+ }
505
+ });
506
+ return { metadata, content };
507
+ }
508
+ function parseResume(rawText, options) {
509
+ const maxChars = options?.maxChars ?? DEFAULT_RESUME_MAX;
510
+ const normalised = rawText.replace(/\r\n/g, "\n").replace(/[ \t]+/g, " ").replace(/ *\n */g, "\n").replace(/\n{3,}/g, "\n\n").trim();
511
+ if (normalised.length <= maxChars) {
512
+ return { text: normalised, truncated: false };
513
+ }
514
+ let cut = normalised.slice(0, maxChars);
515
+ const lastSpace = cut.lastIndexOf(" ");
516
+ if (lastSpace > maxChars * 0.6) cut = cut.slice(0, lastSpace);
517
+ return { text: cut, truncated: true };
518
+ }
519
+ async function parseReadme(rawMarkdown) {
520
+ const { metadata, content } = parseReadmeFrontmatter(rawMarkdown ?? "");
521
+ const marked = await loadMarked();
522
+ const DOMPurify = await loadDOMPurify();
523
+ marked.setOptions({ gfm: true, breaks: false });
524
+ const dirtyHtml = marked.parse(content, { async: false });
525
+ const html = DOMPurify.sanitize(dirtyHtml, {
526
+ ALLOWED_TAGS: [
527
+ "h1",
528
+ "h2",
529
+ "h3",
530
+ "h4",
531
+ "h5",
532
+ "h6",
533
+ "p",
534
+ "br",
535
+ "hr",
536
+ "blockquote",
537
+ "pre",
538
+ "code",
539
+ "ul",
540
+ "ol",
541
+ "li",
542
+ "a",
543
+ "strong",
544
+ "em",
545
+ "del",
546
+ "s",
547
+ "sup",
548
+ "sub",
549
+ "table",
550
+ "thead",
551
+ "tbody",
552
+ "tr",
553
+ "th",
554
+ "td",
555
+ "img",
556
+ "span",
557
+ "div",
558
+ "details",
559
+ "summary"
560
+ ],
561
+ ALLOWED_ATTR: [
562
+ "href",
563
+ "title",
564
+ "alt",
565
+ "src",
566
+ "width",
567
+ "height",
568
+ "colspan",
569
+ "rowspan",
570
+ "target",
571
+ "rel",
572
+ "align"
573
+ ],
574
+ ALLOW_DATA_ATTR: false
575
+ });
576
+ return {
577
+ html,
578
+ raw: rawMarkdown ?? "",
579
+ frontmatter: Object.keys(metadata).length > 0 ? metadata : void 0
580
+ };
581
+ }
582
+ function resolveScreenshots(entries, owner, repo, branch) {
583
+ return entries.filter((e) => IMAGE_EXTENSIONS.some((ext) => e.name.toLowerCase().endsWith(ext))).sort((a, b) => a.name.localeCompare(b.name, "en", { numeric: true, sensitivity: "base" })).map((e) => {
584
+ const path = e.path;
585
+ return {
586
+ url: buildRawUrl(owner, repo, branch, path),
587
+ filename: e.name,
588
+ alt: e.name.replace(/\.[^.]+$/, "").replace(/[-_]+/g, " ")
589
+ };
590
+ });
591
+ }
592
+
593
+ // src/builder.ts
594
+ function validateConfig(owner, repo, options) {
595
+ if (typeof owner !== "string" || !owner.trim()) {
596
+ throw new repodeckError("INVALID_CONFIG", "`owner` is required.");
597
+ }
598
+ if (typeof repo !== "string" || !repo.trim()) {
599
+ throw new repodeckError("INVALID_CONFIG", "`repo` is required.");
600
+ }
601
+ const cp = options?.configPath;
602
+ if (cp !== void 0 && (typeof cp !== "string" || cp.includes(".."))) {
603
+ throw new repodeckError("INVALID_CONFIG", "`configPath` must be a safe string.");
604
+ }
605
+ }
606
+ function joinPath(configPath, file) {
607
+ return `${configPath.replace(/^\/+|\/+$/g, "")}/${file}`;
608
+ }
609
+ var CardBuilder = class {
610
+ constructor(client = new GitHubClient(), cache) {
611
+ this.client = client;
612
+ this.cache = cache;
613
+ }
614
+ async build(owner, repo, options = {}) {
615
+ validateConfig(owner, repo, options);
616
+ const debug = isDebugEnabled(options.debug);
617
+ const started = Date.now();
618
+ const branch = options.branch ?? "main";
619
+ const configPath = options.configPath ?? "config-repodeck";
620
+ const include = options.include ?? { readme: true, resume: true, screenshots: true };
621
+ const cacheKey = Cache.buildKey({ owner, repo, branch, configPath, include: includeKey(include) });
622
+ if (this.cache) {
623
+ const cached = this.cache.get(cacheKey);
624
+ if (cached) {
625
+ dbgLog(debug, "builder", `${owner}/${repo} cache HIT`);
626
+ return cached;
627
+ }
628
+ }
629
+ dbgLog(debug, "builder", `${owner}/${repo} build start branch=${branch} configPath=${configPath} include=${includeKey(include)}`);
630
+ const tasks = [];
631
+ if (include.resume) tasks.push(this.fetchResume(owner, repo, branch, configPath, options));
632
+ if (include.readme) tasks.push(this.fetchReadme(owner, repo, branch, configPath));
633
+ if (include.screenshots) tasks.push(this.fetchScreenshots(owner, repo, branch, configPath));
634
+ if (include.stats) tasks.push(this.fetchStats(owner, repo));
635
+ const results = await Promise.allSettled(tasks);
636
+ const card = {
637
+ meta: { owner, repo, url: `https://github.com/${owner}/${repo}`, branch, configPath, fetchedAt: Date.now() },
638
+ resume: null,
639
+ readme: null,
640
+ screenshots: null,
641
+ stats: null
642
+ };
643
+ for (const r of results) {
644
+ if (r.status !== "fulfilled") continue;
645
+ const { key, data } = r.value;
646
+ if (data === null) continue;
647
+ card[key] = data;
648
+ }
649
+ dbgLog(debug, "builder", `${owner}/${repo} done in ${Date.now() - started}ms resume=${ok(card.resume)} readme=${ok(card.readme)} screenshots=${ok(card.screenshots)} stats=${ok(card.stats)}`);
650
+ if (this.cache) this.cache.set(cacheKey, card);
651
+ return card;
652
+ }
653
+ // --- per-field fetchers (each isolates errors) --------------------------
654
+ async fetchResume(owner, repo, branch, configPath, options) {
655
+ const debug = isDebugEnabled(options.debug);
656
+ return this.safeFetch("resume", async () => {
657
+ let raw = null;
658
+ let usedLocale;
659
+ if (options.locale) {
660
+ raw = await this.client.fetchFileContent(owner, repo, joinPath(configPath, `RESUME.${options.locale}.txt`), branch);
661
+ if (raw !== null) usedLocale = options.locale;
662
+ else dbgLog(debug, "builder", `${owner}/${repo} RESUME.${options.locale}.txt missing \u2192 falling back to RESUME.txt`);
663
+ }
664
+ if (raw === null) {
665
+ raw = await this.client.fetchFileContent(owner, repo, joinPath(configPath, "RESUME.txt"), branch);
666
+ }
667
+ if (raw === null) throw new repodeckError("NOT_FOUND", "RESUME.txt not found in config folder.", { field: "resume" });
668
+ return { ...parseResume(raw, { maxChars: options.resumeMaxChars }), locale: usedLocale };
669
+ });
670
+ }
671
+ async fetchReadme(owner, repo, branch, configPath) {
672
+ return this.safeFetch("readme", async () => {
673
+ const raw = await this.client.fetchFileContent(owner, repo, joinPath(configPath, "README.md"), branch);
674
+ if (raw === null) throw new repodeckError("NOT_FOUND", "README.md not found in config folder.", { field: "readme" });
675
+ return await parseReadme(raw);
676
+ });
677
+ }
678
+ async fetchScreenshots(owner, repo, branch, configPath) {
679
+ return this.safeFetch("screenshots", async () => {
680
+ const entries = await this.client.fetchDirectoryListing(owner, repo, joinPath(configPath, "screenshots"), branch);
681
+ return resolveScreenshots(entries, owner, repo, branch);
682
+ });
683
+ }
684
+ async fetchStats(owner, repo) {
685
+ return this.safeFetch("stats", async () => {
686
+ return await this.client.fetchRepoStats(owner, repo);
687
+ });
688
+ }
689
+ /** Wraps a fetcher in try/catch, converting errors to per-field repodeckError. */
690
+ async safeFetch(key, fn) {
691
+ try {
692
+ return { key, data: await fn() };
693
+ } catch (err) {
694
+ const e = err instanceof repodeckError ? err : new repodeckError("NETWORK_ERROR", String(err), { field: key, cause: err });
695
+ return { key, data: { error: e } };
696
+ }
697
+ }
698
+ };
699
+ function ok(v) {
700
+ if (v === null || v === void 0) return "missing";
701
+ if (typeof v === "object" && v.code) return `ERR:${v.code}`;
702
+ return "ok";
703
+ }
704
+ function includeKey(include) {
705
+ return `${Number(!!include.readme)}${Number(!!include.resume)}${Number(!!include.screenshots)}${Number(!!include.stats)}`;
706
+ }
707
+ async function buildCardData(owner, repo, options = {}) {
708
+ const client = GitHubClient.from({ token: options.token, timeoutMs: options.timeoutMs, retries: options.retries });
709
+ const builder = new CardBuilder(client);
710
+ return builder.build(owner, repo, options);
711
+ }
712
+
713
+ // src/prefetch.ts
714
+ var DEFAULT_INCLUDE = {
715
+ readme: true,
716
+ resume: true,
717
+ screenshots: true
718
+ };
719
+ async function prefetchCardData(targets, options = {}) {
720
+ if (!Array.isArray(targets) || targets.length === 0) {
721
+ return /* @__PURE__ */ new Map();
722
+ }
723
+ const concurrency = clamp(options.concurrency ?? 4, 1, 16);
724
+ const debug = isDebugEnabled(options.debug);
725
+ const client = new GitHubClient({
726
+ token: options.token,
727
+ timeoutMs: options.timeoutMs,
728
+ retries: options.retries,
729
+ debug
730
+ });
731
+ const cache = options.cache;
732
+ const defaultInclude = options.include ?? DEFAULT_INCLUDE;
733
+ const results = /* @__PURE__ */ new Map();
734
+ const errors = /* @__PURE__ */ new Map();
735
+ dbgLog(debug, "prefetch", `${targets.length} target(s), concurrency=${concurrency}`);
736
+ const queue = targets.slice();
737
+ async function startNextWorker() {
738
+ const target = queue.shift();
739
+ if (!target) return;
740
+ const key = `${target.owner}/${target.repo}`;
741
+ try {
742
+ const card = await buildOne(target, client, cache, defaultInclude);
743
+ results.set(key, card);
744
+ } catch (err) {
745
+ errors.set(key, err);
746
+ dbgLog(debug, "prefetch", `${key} fatal error: ${err instanceof Error ? err.message : String(err)}`);
747
+ }
748
+ }
749
+ const workers = [];
750
+ for (let i = 0; i < concurrency; i++) {
751
+ const chain = (async () => {
752
+ while (queue.length > 0) {
753
+ await startNextWorker();
754
+ }
755
+ })();
756
+ workers.push(chain);
757
+ }
758
+ await Promise.allSettled(workers);
759
+ if (errors.size > 0) {
760
+ attachErrors(results, errors);
761
+ }
762
+ return results;
763
+ }
764
+ function attachErrors(map, errors) {
765
+ Object.defineProperty(map, "errors", {
766
+ value: errors,
767
+ enumerable: false,
768
+ writable: false,
769
+ configurable: false
770
+ });
771
+ }
772
+ async function buildOne(target, client, cache, defaultInclude) {
773
+ const include = { ...defaultInclude, ...target.include ?? {} };
774
+ const builder = new CardBuilder(client, cache);
775
+ return builder.build(target.owner, target.repo, {
776
+ include,
777
+ branch: target.branch,
778
+ configPath: target.configPath
779
+ });
780
+ }
781
+ function clamp(value, min, max) {
782
+ if (Number.isNaN(value)) return min;
783
+ return Math.max(min, Math.min(max, value));
784
+ }
785
+
786
+ // src/graphql.ts
787
+ var GRAPHQL_ENDPOINT = "https://api.github.com/graphql";
788
+ var DEFAULT_INCLUDE2 = {
789
+ readme: true,
790
+ resume: true,
791
+ screenshots: true
792
+ };
793
+ var DEFAULT_BATCH_SIZE = 10;
794
+ var DEFAULT_TIMEOUT_MS = 3e4;
795
+ var GraphQLClient = class {
796
+ constructor(opts) {
797
+ if (!opts.token) {
798
+ throw new RepoDeckError("UNAUTHORIZED", "GraphQL requires a GitHub token.", {});
799
+ }
800
+ this.token = opts.token;
801
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
802
+ this.retries = opts.retries ?? 2;
803
+ this.debug = isDebugEnabled(opts.debug);
804
+ }
805
+ /**
806
+ * POSTs a GraphQL query. Returns the `data` field of the response (already
807
+ * stripped of `errors`). Throws `RepoDeckError('NETWORK_ERROR', …)` on a
808
+ * non-2xx response, or `RepoDeckError` carrying the GraphQL errors when
809
+ * the response is 200 but carries `errors: [...]`.
810
+ */
811
+ async request(query, variables) {
812
+ let body = JSON.stringify({ query });
813
+ if (variables !== void 0) body = JSON.stringify({ query, variables });
814
+ const started = Date.now();
815
+ let lastErr;
816
+ for (let attempt = 0; attempt <= this.retries; attempt++) {
817
+ const controller = new AbortController();
818
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
819
+ try {
820
+ const res = await fetch(GRAPHQL_ENDPOINT, {
821
+ method: "POST",
822
+ headers: {
823
+ Authorization: `Bearer ${this.token}`,
824
+ "Content-Type": "application/json",
825
+ Accept: "application/vnd.github+json",
826
+ "User-Agent": "repodeck-core"
827
+ },
828
+ body,
829
+ signal: controller.signal
830
+ });
831
+ clearTimeout(timer);
832
+ const ms = Date.now() - started;
833
+ const rate = res.headers.get("x-ratelimit-remaining") ? `${res.headers.get("x-ratelimit-remaining")}/${res.headers.get("x-ratelimit-limit")}` : "n/a";
834
+ if (res.status >= 500 && attempt < this.retries) {
835
+ dbgLog(this.debug, "graphql", `POST /graphql \u2192 ${res.status} (retry ${attempt + 1}/${this.retries}) ${ms}ms rl=${rate}`);
836
+ await sleep2(2 ** attempt * 250);
837
+ continue;
838
+ }
839
+ dbgLog(this.debug, "graphql", `POST /graphql \u2192 ${res.status} ${ms}ms rl=${rate} attempt=${attempt + 1}`);
840
+ if (res.status === 401) {
841
+ throw new RepoDeckError("UNAUTHORIZED", "GitHub token is invalid or expired.", { status: 401 });
842
+ }
843
+ if (res.status === 403) {
844
+ throw new RepoDeckError("RATE_LIMITED", "GitHub GraphQL rate limit exceeded.", { status: 403 });
845
+ }
846
+ if (!res.ok) {
847
+ throw new RepoDeckError("NETWORK_ERROR", `GraphQL HTTP ${res.status}`, { status: res.status });
848
+ }
849
+ const json = await res.json();
850
+ if (json.errors && json.errors.length > 0 && json.data === void 0) {
851
+ const msg = json.errors.map((e) => e.message).join("; ");
852
+ dbgLog(this.debug, "graphql", `GraphQL errors: ${msg}`);
853
+ throw new RepoDeckError("NETWORK_ERROR", `GraphQL errors: ${msg}`);
854
+ }
855
+ if (json.data === void 0) {
856
+ throw new RepoDeckError("NETWORK_ERROR", "GraphQL returned no data and no errors.");
857
+ }
858
+ return json.data;
859
+ } catch (err) {
860
+ clearTimeout(timer);
861
+ if (err instanceof RepoDeckError) throw err;
862
+ lastErr = err;
863
+ if (attempt < this.retries) {
864
+ await sleep2(2 ** attempt * 250);
865
+ continue;
866
+ }
867
+ }
868
+ }
869
+ throw new RepoDeckError("NETWORK_ERROR", `Network error posting to GraphQL endpoint`, { cause: lastErr });
870
+ }
871
+ };
872
+ function planAliases(batch) {
873
+ return batch.map((target, i) => ({ alias: `r${i}`, target }));
874
+ }
875
+ function buildQuery(plan) {
876
+ const fragments = plan.map(({ alias, target }) => {
877
+ const owner = JSON.stringify(target.owner);
878
+ const name = JSON.stringify(target.repo);
879
+ const branchVal = target.branch ?? "main";
880
+ const configPath = (target.configPath ?? "config-repodeck").replace(/\/+$/, "");
881
+ const readmePath = `${configPath}/README.md`;
882
+ const resumeDefault = `${configPath}/RESUME.txt`;
883
+ const resumeLocalized = target.locale ? `${configPath}/RESUME.${target.locale}.txt` : null;
884
+ const expr = (p) => JSON.stringify(`${branchVal}:${p}`);
885
+ const fragments2 = [];
886
+ fragments2.push(`${alias}_meta: repository(owner: ${owner}, name: ${name}) {
887
+ name
888
+ description
889
+ url
890
+ homepageUrl
891
+ primaryLanguage { name }
892
+ repositoryTopics(first: 16) { nodes { topic { name } } }
893
+ stargazerCount
894
+ forkCount
895
+ watchers { totalCount }
896
+ issues(states: OPEN) { totalCount }
897
+ defaultBranchRef { name }
898
+ pushedAt
899
+ owner { login avatarUrl }
900
+ }`);
901
+ fragments2.push(`${alias}_readme: repository(owner: ${owner}, name: ${name}) {
902
+ object(expression: ${expr(readmePath)}) {
903
+ ... on Blob { text }
904
+ }
905
+ }`);
906
+ if (resumeLocalized) {
907
+ fragments2.push(`${alias}_resume: repository(owner: ${owner}, name: ${name}) {
908
+ object(expression: ${expr(resumeLocalized)}) {
909
+ ... on Blob { text }
910
+ }
911
+ }`);
912
+ } else {
913
+ fragments2.push(`${alias}_resume: repository(owner: ${owner}, name: ${name}) {
914
+ object(expression: ${expr(resumeDefault)}) {
915
+ ... on Blob { text }
916
+ }
917
+ }`);
918
+ }
919
+ return fragments2.join("\n");
920
+ });
921
+ return `query repodeckBatch { ${fragments.join("\n")} }`;
922
+ }
923
+ function metaToStats(meta, fallbackOwner) {
924
+ return {
925
+ stars: meta.stargazerCount ?? 0,
926
+ forks: meta.forkCount ?? 0,
927
+ watchers: meta.watchers?.totalCount ?? 0,
928
+ openIssues: meta.issues?.totalCount ?? 0,
929
+ defaultBranch: meta.defaultBranchRef?.name ?? "main",
930
+ pushedAt: meta.pushedAt ? Date.parse(meta.pushedAt) : 0,
931
+ repoName: meta.name,
932
+ repoDescription: meta.description,
933
+ topics: Array.isArray(meta.repositoryTopics?.nodes) ? meta.repositoryTopics.nodes.map((n) => n?.topic?.name).filter((t) => Boolean(t)) : [],
934
+ language: meta.primaryLanguage?.name ?? null,
935
+ homepage: meta.homepageUrl || null,
936
+ ownerLogin: meta.owner?.login ?? fallbackOwner,
937
+ ownerAvatarUrl: meta.owner?.avatarUrl ?? ""
938
+ };
939
+ }
940
+ async function fetchMultipleReposViaGraphQL(targets, options) {
941
+ if (!Array.isArray(targets) || targets.length === 0) {
942
+ return /* @__PURE__ */ new Map();
943
+ }
944
+ if (!options.token) {
945
+ throw new RepoDeckError("UNAUTHORIZED", "GraphQL batch requires a GitHub token.", {});
946
+ }
947
+ const batchSize = Math.max(1, Math.min(options.batchSize ?? DEFAULT_BATCH_SIZE, 50));
948
+ const defaultInclude = options.include ?? DEFAULT_INCLUDE2;
949
+ const debug = isDebugEnabled(options.debug);
950
+ const restClient = options.restClient ?? new GitHubClient({
951
+ token: options.token,
952
+ timeoutMs: 12e3,
953
+ retries: options.retries ?? 0,
954
+ debug
955
+ });
956
+ const results = /* @__PURE__ */ new Map();
957
+ const batchCount = Math.ceil(targets.length / batchSize);
958
+ dbgLog(debug, "graphql", `${targets.length} target(s), batchSize=${batchSize} \u2192 ${batchCount} request(s)`);
959
+ for (let i = 0; i < targets.length; i += batchSize) {
960
+ const slice = targets.slice(i, i + batchSize);
961
+ await runBatch(slice, options, defaultInclude, restClient, results, debug);
962
+ }
963
+ return results;
964
+ }
965
+ async function runBatch(batch, options, defaultInclude, restClient, results, debug) {
966
+ const plan = planAliases(batch);
967
+ const query = buildQuery(plan);
968
+ dbgLog(debug, "graphql", `request for ${plan.map((p) => `${p.alias}=${p.target.owner}/${p.target.repo}`).join(", ")} (query ${query.length} chars)`);
969
+ const client = new GraphQLClient({
970
+ token: options.token,
971
+ timeoutMs: options.timeoutMs,
972
+ retries: options.retries,
973
+ debug
974
+ });
975
+ let responseData;
976
+ try {
977
+ responseData = await client.request(query);
978
+ } catch (err) {
979
+ const isUnauthorized = err instanceof RepoDeckError && err.code === "UNAUTHORIZED";
980
+ const code = isUnauthorized ? "UNAUTHORIZED" : "NETWORK_ERROR";
981
+ const message = err instanceof Error ? err.message : "GraphQL request failed.";
982
+ dbgLog(debug, "graphql", `batch failed (${code}): ${message}`);
983
+ for (const { target } of plan) {
984
+ const key = `${target.owner}/${target.repo}`;
985
+ const branch = target.branch ?? "main";
986
+ const configPath = target.configPath ?? "config-repodeck";
987
+ const errInstance = new RepoDeckError(code, message);
988
+ results.set(key, {
989
+ meta: { owner: target.owner, repo: target.repo, url: `https://github.com/${target.owner}/${target.repo}`, branch, configPath, fetchedAt: Date.now() },
990
+ resume: { error: errInstance },
991
+ readme: { error: errInstance },
992
+ screenshots: { error: errInstance },
993
+ stats: { error: errInstance }
994
+ });
995
+ }
996
+ return;
997
+ }
998
+ const cardPromises = plan.map(async ({ alias, target }) => {
999
+ const key = `${target.owner}/${target.repo}`;
1000
+ const branch = target.branch ?? "main";
1001
+ const configPath = target.configPath ?? "config-repodeck";
1002
+ const include = { ...defaultInclude, ...target.include ?? {} };
1003
+ const meta = responseData[`${alias}_meta`];
1004
+ const readmeBlob = responseData[`${alias}_readme`];
1005
+ const resumeBlob = responseData[`${alias}_resume`];
1006
+ const card = {
1007
+ meta: { owner: target.owner, repo: target.repo, url: `https://github.com/${target.owner}/${target.repo}`, branch, configPath, fetchedAt: Date.now() },
1008
+ resume: null,
1009
+ readme: null,
1010
+ screenshots: null,
1011
+ stats: null
1012
+ };
1013
+ if (include.stats) {
1014
+ if (meta) {
1015
+ card.stats = metaToStats(meta, target.owner);
1016
+ } else {
1017
+ card.stats = { error: new RepoDeckError("NOT_FOUND", `Repository ${target.owner}/${target.repo} not accessible.`, { field: "stats" }) };
1018
+ }
1019
+ } else if (meta) {
1020
+ card.stats = metaToStats(meta, target.owner);
1021
+ }
1022
+ if (include.readme) {
1023
+ const raw = readmeBlob?.object?.text;
1024
+ if (raw !== null && raw !== void 0) {
1025
+ try {
1026
+ card.readme = await parseReadme(raw);
1027
+ } catch (err) {
1028
+ card.readme = { error: new RepoDeckError("NETWORK_ERROR", `Could not parse README: ${String(err)}`, { field: "readme" }) };
1029
+ }
1030
+ } else {
1031
+ card.readme = { error: new RepoDeckError("NOT_FOUND", "README.md not found in config folder.", { field: "readme" }) };
1032
+ }
1033
+ }
1034
+ if (include.resume) {
1035
+ const raw = resumeBlob?.object?.text;
1036
+ if (raw !== null && raw !== void 0) {
1037
+ const parsed = parseResume(raw, { maxChars: options.resumeMaxChars });
1038
+ card.resume = { ...parsed, locale: target.locale };
1039
+ } else {
1040
+ card.resume = { error: new RepoDeckError("NOT_FOUND", "RESUME.txt not found in config folder.", { field: "resume" }) };
1041
+ }
1042
+ }
1043
+ if (include.screenshots) {
1044
+ try {
1045
+ const screenshotsPath = `${configPath.replace(/^\/+|\/+$/g, "")}/screenshots`;
1046
+ const entries = await restClient.fetchDirectoryListing(target.owner, target.repo, screenshotsPath, branch);
1047
+ card.screenshots = resolveScreenshots(entries, target.owner, target.repo, branch);
1048
+ } catch (err) {
1049
+ const e = err instanceof RepoDeckError ? err : new RepoDeckError("NETWORK_ERROR", String(err), { field: "screenshots", cause: err });
1050
+ card.screenshots = { error: e };
1051
+ }
1052
+ }
1053
+ return { key, card };
1054
+ });
1055
+ const settled = await Promise.allSettled(cardPromises);
1056
+ for (const r of settled) {
1057
+ if (r.status === "fulfilled") results.set(r.value.key, r.value.card);
1058
+ }
1059
+ }
1060
+ function sleep2(ms) {
1061
+ return new Promise((r) => setTimeout(r, ms));
1062
+ }
1063
+ // Annotate the CommonJS export names for ESM import in node:
1064
+ 0 && (module.exports = {
1065
+ Cache,
1066
+ CardBuilder,
1067
+ GitHubClient,
1068
+ GraphQLClient,
1069
+ RepoDeckError,
1070
+ assignBentoSpans,
1071
+ buildCacheKey,
1072
+ buildCardData,
1073
+ buildRawUrl,
1074
+ checkRateLimit,
1075
+ clearCache,
1076
+ defaultPresets,
1077
+ fetchDirectoryListing,
1078
+ fetchFileContent,
1079
+ fetchMultipleReposViaGraphQL,
1080
+ fetchRepoStats,
1081
+ getCached,
1082
+ getPreset,
1083
+ listPresets,
1084
+ mergePresetWithUserConfig,
1085
+ parseReadme,
1086
+ parseReadmeFrontmatter,
1087
+ parseResume,
1088
+ prefetchCardData,
1089
+ registerPreset,
1090
+ repodeckError,
1091
+ resolveRadiusTokens,
1092
+ resolveScreenshotLayout,
1093
+ resolveScreenshots,
1094
+ setCached,
1095
+ validateConfig
1096
+ });