@aigne/afs-github 1.11.0-beta.6

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.mjs ADDED
@@ -0,0 +1,1125 @@
1
+ import { retry } from "@octokit/plugin-retry";
2
+ import { throttling } from "@octokit/plugin-throttling";
3
+ import { Octokit } from "@octokit/rest";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { MappingCompiler } from "@aigne/afs-mapping";
7
+ import { WorldMappingCore } from "@aigne/afs-world-mapping";
8
+ import { z } from "zod";
9
+
10
+ //#region src/client.ts
11
+ /**
12
+ * GitHub API Client
13
+ *
14
+ * Wraps Octokit with throttling, retry, and caching.
15
+ */
16
+ /**
17
+ * GitHub API client with throttling, retry, and caching
18
+ */
19
+ var GitHubClient = class {
20
+ octokit;
21
+ cache = /* @__PURE__ */ new Map();
22
+ cacheConfig;
23
+ constructor(options) {
24
+ this.cacheConfig = {
25
+ enabled: options.cache?.enabled ?? true,
26
+ ttl: options.cache?.ttl ?? 6e4
27
+ };
28
+ const maxRetries = options.rateLimit?.maxRetries ?? 3;
29
+ this.octokit = new (Octokit.plugin(throttling, retry))({
30
+ auth: options.auth?.token,
31
+ baseUrl: options.baseUrl ?? "https://api.github.com",
32
+ log: {
33
+ debug: () => {},
34
+ info: () => {},
35
+ warn: () => {},
36
+ error: () => {}
37
+ },
38
+ throttle: {
39
+ onRateLimit: (retryAfter, _options, _octokit, retryCount) => {
40
+ if (retryCount < maxRetries) {
41
+ console.warn(`Rate limited. Retrying after ${retryAfter} seconds.`);
42
+ return true;
43
+ }
44
+ return false;
45
+ },
46
+ onSecondaryRateLimit: (retryAfter, _options, _octokit, retryCount) => {
47
+ if (retryCount < maxRetries) {
48
+ console.warn(`Secondary rate limit. Retrying after ${retryAfter} seconds.`);
49
+ return true;
50
+ }
51
+ return false;
52
+ }
53
+ },
54
+ retry: {
55
+ enabled: options.rateLimit?.autoRetry ?? true,
56
+ retries: maxRetries
57
+ }
58
+ });
59
+ }
60
+ /**
61
+ * Make a request to the GitHub API
62
+ */
63
+ async request(route, params) {
64
+ const isGet = route.startsWith("GET ");
65
+ if (isGet && this.cacheConfig.enabled) {
66
+ const cacheKey = this.getCacheKey(route, params);
67
+ const cached = this.getFromCache(cacheKey);
68
+ if (cached !== void 0) return cached;
69
+ }
70
+ const response = await this.octokit.request(route, params);
71
+ const result = {
72
+ data: response.data,
73
+ status: response.status,
74
+ headers: response.headers
75
+ };
76
+ if (isGet && this.cacheConfig.enabled) {
77
+ const cacheKey = this.getCacheKey(route, params);
78
+ this.setCache(cacheKey, result);
79
+ }
80
+ return result;
81
+ }
82
+ /**
83
+ * Generate cache key from route and params
84
+ */
85
+ getCacheKey(route, params) {
86
+ return `${route}:${JSON.stringify(params ?? {})}`;
87
+ }
88
+ /**
89
+ * Get from cache if not expired
90
+ */
91
+ getFromCache(key) {
92
+ const entry = this.cache.get(key);
93
+ if (!entry) return void 0;
94
+ if (Date.now() > entry.expiresAt) {
95
+ this.cache.delete(key);
96
+ return;
97
+ }
98
+ return entry.data;
99
+ }
100
+ /**
101
+ * Set cache entry
102
+ */
103
+ setCache(key, data) {
104
+ this.cache.set(key, {
105
+ data,
106
+ expiresAt: Date.now() + this.cacheConfig.ttl
107
+ });
108
+ }
109
+ /**
110
+ * Clear the cache
111
+ */
112
+ clearCache() {
113
+ this.cache.clear();
114
+ }
115
+ /**
116
+ * Get the underlying Octokit instance (for advanced usage)
117
+ */
118
+ getOctokit() {
119
+ return this.octokit;
120
+ }
121
+ /**
122
+ * Get repository contents at a path
123
+ * @param owner Repository owner
124
+ * @param repo Repository name
125
+ * @param path Path within the repository (empty string for root)
126
+ * @param ref Optional branch, tag, or commit SHA
127
+ * @returns Contents item(s) - array for directories, single item for files
128
+ */
129
+ async getContents(owner, repo, path, ref) {
130
+ const params = {
131
+ owner,
132
+ repo,
133
+ path: path || ""
134
+ };
135
+ if (ref) params.ref = ref;
136
+ return (await this.request("GET /repos/{owner}/{repo}/contents/{path}", params)).data;
137
+ }
138
+ /**
139
+ * Get blob content for large files (>1MB)
140
+ * @param owner Repository owner
141
+ * @param repo Repository name
142
+ * @param sha Blob SHA
143
+ * @returns Decoded content as string
144
+ */
145
+ async getBlob(owner, repo, sha) {
146
+ const response = await this.request("GET /repos/{owner}/{repo}/git/blobs/{sha}", {
147
+ owner,
148
+ repo,
149
+ sha
150
+ });
151
+ return Buffer.from(response.data.content, "base64").toString("utf-8");
152
+ }
153
+ /**
154
+ * Get repository branches
155
+ * @param owner Repository owner
156
+ * @param repo Repository name
157
+ * @returns Array of branch items
158
+ */
159
+ async getBranches(owner, repo) {
160
+ const allBranches = [];
161
+ let page = 1;
162
+ const perPage = 100;
163
+ while (true) {
164
+ const response = await this.request("GET /repos/{owner}/{repo}/branches", {
165
+ owner,
166
+ repo,
167
+ per_page: perPage,
168
+ page
169
+ });
170
+ allBranches.push(...response.data);
171
+ if (response.data.length < perPage) break;
172
+ page++;
173
+ }
174
+ return allBranches;
175
+ }
176
+ /**
177
+ * Get default branch for a repository
178
+ * @param owner Repository owner
179
+ * @param repo Repository name
180
+ * @returns Default branch name
181
+ */
182
+ async getDefaultBranch(owner, repo) {
183
+ return (await this.request("GET /repos/{owner}/{repo}", {
184
+ owner,
185
+ repo
186
+ })).data.default_branch;
187
+ }
188
+ };
189
+
190
+ //#endregion
191
+ //#region src/types.ts
192
+ /**
193
+ * Type definitions for AFSGitHub provider
194
+ */
195
+ /**
196
+ * Authentication options
197
+ */
198
+ const authOptionsSchema = z.object({ token: z.string().optional() });
199
+ /**
200
+ * Cache configuration
201
+ */
202
+ const cacheOptionsSchema = z.object({
203
+ enabled: z.boolean().default(true),
204
+ ttl: z.number().default(6e4)
205
+ });
206
+ /**
207
+ * Rate limiting configuration
208
+ */
209
+ const rateLimitOptionsSchema = z.object({
210
+ autoRetry: z.boolean().default(true),
211
+ maxRetries: z.number().default(3)
212
+ });
213
+ /**
214
+ * Access mode for the provider
215
+ */
216
+ const accessModeSchema = z.enum(["readonly", "readwrite"]).default("readonly");
217
+ /**
218
+ * Repository mode
219
+ * - single-repo: Access a single repository (requires owner and repo)
220
+ * - multi-repo: Access multiple repositories with full paths
221
+ * - org: Access all repositories in an organization (requires owner only)
222
+ */
223
+ const repoModeSchema = z.enum([
224
+ "single-repo",
225
+ "multi-repo",
226
+ "org"
227
+ ]).default("single-repo");
228
+ /**
229
+ * Owner type for org mode
230
+ * - org: GitHub organization (uses /orgs/{org}/repos endpoint)
231
+ * - user: GitHub user account (uses /users/{username}/repos endpoint)
232
+ * - undefined: Auto-detect (tries org first, falls back to user)
233
+ */
234
+ const ownerTypeSchema = z.enum(["org", "user"]).optional();
235
+ /**
236
+ * Full options schema for AFSGitHub
237
+ */
238
+ const afsGitHubOptionsSchema = z.object({
239
+ name: z.string().default("github"),
240
+ description: z.string().optional(),
241
+ auth: authOptionsSchema.optional(),
242
+ owner: z.string().optional(),
243
+ repo: z.string().optional(),
244
+ accessMode: accessModeSchema.optional(),
245
+ mode: repoModeSchema.optional(),
246
+ ownerType: ownerTypeSchema.optional(),
247
+ baseUrl: z.string().default("https://api.github.com"),
248
+ mappingPath: z.string().optional(),
249
+ cache: cacheOptionsSchema.optional(),
250
+ rateLimit: rateLimitOptionsSchema.optional(),
251
+ ref: z.string().optional()
252
+ });
253
+
254
+ //#endregion
255
+ //#region src/github.ts
256
+ /**
257
+ * AFSGitHub Provider
258
+ *
259
+ * AFS module for accessing GitHub Issues, PRs, and Actions.
260
+ * Implements AFSModule and AFSWorldMappingCapable interfaces.
261
+ */
262
+ const DEFAULT_MAPPING_PATH = join(fileURLToPath(new URL(".", import.meta.url)), "default-mapping");
263
+ /**
264
+ * AFSGitHub Provider
265
+ *
266
+ * Provides access to GitHub Issues, Pull Requests, and Actions through AFS.
267
+ *
268
+ * @example
269
+ * ```typescript
270
+ * const github = new AFSGitHub({
271
+ * auth: { token: process.env.GITHUB_TOKEN },
272
+ * owner: "aigne",
273
+ * repo: "afs",
274
+ * });
275
+ *
276
+ * // Mount to AFS
277
+ * afs.mount("/github", github);
278
+ *
279
+ * // List issues
280
+ * const issues = await afs.list("/github/issues");
281
+ * ```
282
+ */
283
+ var AFSGitHub = class {
284
+ name;
285
+ description;
286
+ accessMode;
287
+ options;
288
+ client;
289
+ compiled = null;
290
+ mappingPath = null;
291
+ mappingLoadedAt;
292
+ mappingError;
293
+ /** World mapping core for schema-level path resolution and field projection (Level 1) */
294
+ worldCore;
295
+ constructor(options) {
296
+ const parsed = afsGitHubOptionsSchema.parse(options);
297
+ this.options = {
298
+ name: parsed.name,
299
+ description: parsed.description,
300
+ auth: parsed.auth,
301
+ owner: parsed.owner,
302
+ repo: parsed.repo,
303
+ accessMode: parsed.accessMode ?? "readonly",
304
+ mode: parsed.mode ?? "single-repo",
305
+ ownerType: parsed.ownerType,
306
+ baseUrl: parsed.baseUrl,
307
+ mappingPath: parsed.mappingPath,
308
+ cache: {
309
+ enabled: parsed.cache?.enabled ?? true,
310
+ ttl: parsed.cache?.ttl ?? 6e4
311
+ },
312
+ rateLimit: {
313
+ autoRetry: parsed.rateLimit?.autoRetry ?? true,
314
+ maxRetries: parsed.rateLimit?.maxRetries ?? 3
315
+ },
316
+ ref: parsed.ref
317
+ };
318
+ this.name = this.options.name;
319
+ this.description = this.options.description;
320
+ this.accessMode = this.options.accessMode;
321
+ if (this.options.mode === "single-repo") {
322
+ if (!this.options.owner || !this.options.repo) throw new Error("owner and repo are required in single-repo mode");
323
+ } else if (this.options.mode === "org") {
324
+ if (!this.options.owner) throw new Error("owner is required in org mode");
325
+ this.options.owner = this.options.owner.trim();
326
+ }
327
+ this.client = new GitHubClient({
328
+ auth: this.options.auth,
329
+ baseUrl: this.options.baseUrl,
330
+ cache: this.options.cache,
331
+ rateLimit: this.options.rateLimit
332
+ });
333
+ this.worldCore = new WorldMappingCore(this.getDefaultWorldSchema(), this.getDefaultWorldBinding());
334
+ this.loadDefaultMapping();
335
+ }
336
+ /**
337
+ * Load the default mapping configuration
338
+ */
339
+ loadDefaultMapping() {
340
+ try {
341
+ const compiler = new MappingCompiler();
342
+ this.mappingPath = this.options.mappingPath ?? DEFAULT_MAPPING_PATH;
343
+ this.compiled = compiler.compileConfig({
344
+ name: "github",
345
+ version: "1.0",
346
+ defaults: {
347
+ baseUrl: this.options.baseUrl,
348
+ headers: {
349
+ Accept: "application/vnd.github+json",
350
+ "X-GitHub-Api-Version": "2022-11-28"
351
+ }
352
+ },
353
+ routes: this.getDefaultRoutes()
354
+ });
355
+ this.mappingLoadedAt = /* @__PURE__ */ new Date();
356
+ this.mappingError = void 0;
357
+ } catch (error) {
358
+ this.mappingError = error instanceof Error ? error.message : String(error);
359
+ }
360
+ }
361
+ /**
362
+ * Get default route definitions
363
+ * This is used when loading mapping synchronously
364
+ */
365
+ getDefaultRoutes() {
366
+ return {
367
+ "/{owner}/{repo}/issues": { list: {
368
+ method: "GET",
369
+ path: "/repos/{owner}/{repo}/issues",
370
+ params: {
371
+ owner: "path.owner",
372
+ repo: "path.repo",
373
+ state: "query.state | default(\"open\")",
374
+ per_page: "query.limit | default(30)"
375
+ },
376
+ transform: {
377
+ items: "$",
378
+ entry: {
379
+ id: "$.number | string",
380
+ path: "/{owner}/{repo}/issues/{$.number}",
381
+ summary: "$.title",
382
+ content: "$.body",
383
+ metadata: {
384
+ type: "issue",
385
+ state: "$.state",
386
+ author: "$.user.login"
387
+ }
388
+ }
389
+ }
390
+ } },
391
+ "/{owner}/{repo}/issues/{number}": { read: {
392
+ method: "GET",
393
+ path: "/repos/{owner}/{repo}/issues/{number}",
394
+ params: {
395
+ owner: "path.owner",
396
+ repo: "path.repo",
397
+ number: "path.number"
398
+ },
399
+ transform: { entry: {
400
+ id: "$.number | string",
401
+ path: "/{owner}/{repo}/issues/{$.number}",
402
+ summary: "$.title",
403
+ content: "$.body",
404
+ metadata: {
405
+ type: "issue",
406
+ state: "$.state",
407
+ author: "$.user.login"
408
+ }
409
+ } }
410
+ } },
411
+ "/{owner}/{repo}/pulls": { list: {
412
+ method: "GET",
413
+ path: "/repos/{owner}/{repo}/pulls",
414
+ params: {
415
+ owner: "path.owner",
416
+ repo: "path.repo",
417
+ state: "query.state | default(\"open\")",
418
+ per_page: "query.limit | default(30)"
419
+ },
420
+ transform: {
421
+ items: "$",
422
+ entry: {
423
+ id: "$.number | string",
424
+ path: "/{owner}/{repo}/pulls/{$.number}",
425
+ summary: "$.title",
426
+ content: "$.body",
427
+ metadata: {
428
+ type: "pull_request",
429
+ state: "$.state",
430
+ author: "$.user.login"
431
+ }
432
+ }
433
+ }
434
+ } },
435
+ "/{owner}/{repo}/pulls/{number}": { read: {
436
+ method: "GET",
437
+ path: "/repos/{owner}/{repo}/pulls/{number}",
438
+ params: {
439
+ owner: "path.owner",
440
+ repo: "path.repo",
441
+ number: "path.number"
442
+ },
443
+ transform: { entry: {
444
+ id: "$.number | string",
445
+ path: "/{owner}/{repo}/pulls/{$.number}",
446
+ summary: "$.title",
447
+ content: "$.body",
448
+ metadata: {
449
+ type: "pull_request",
450
+ state: "$.state",
451
+ author: "$.user.login"
452
+ }
453
+ } }
454
+ } }
455
+ };
456
+ }
457
+ /**
458
+ * Get default world schema for GitHub
459
+ * Defines Issue, PullRequest, Comment as world kinds
460
+ */
461
+ getDefaultWorldSchema() {
462
+ return {
463
+ world: "github",
464
+ version: "1.0",
465
+ kinds: {
466
+ Issue: {
467
+ key: "number",
468
+ path: "/{owner}/{repo}/issues/{number}",
469
+ fields: {
470
+ number: "int",
471
+ title: "string",
472
+ body: "text",
473
+ state: "string",
474
+ author: "string",
475
+ labels: "string",
476
+ assignees: "string",
477
+ created_at: "datetime",
478
+ updated_at: "datetime",
479
+ comments_count: "int"
480
+ }
481
+ },
482
+ PullRequest: {
483
+ key: "number",
484
+ path: "/{owner}/{repo}/pulls/{number}",
485
+ fields: {
486
+ number: "int",
487
+ title: "string",
488
+ body: "text",
489
+ state: "string",
490
+ author: "string",
491
+ created_at: "datetime",
492
+ updated_at: "datetime"
493
+ }
494
+ },
495
+ Comment: {
496
+ key: "id",
497
+ path: "/{owner}/{repo}/issues/{number}/comments/{id}",
498
+ fields: {
499
+ id: "int",
500
+ body: "text",
501
+ author: "string",
502
+ created_at: "datetime",
503
+ updated_at: "datetime"
504
+ }
505
+ }
506
+ }
507
+ };
508
+ }
509
+ /**
510
+ * Get default world binding for GitHub
511
+ * Maps world fields to GitHub API response fields
512
+ */
513
+ getDefaultWorldBinding() {
514
+ return {
515
+ source: "/github",
516
+ mappings: {
517
+ Issue: {
518
+ path: "/{owner}/{repo}/issues/{number}",
519
+ fieldMap: {
520
+ author: "user_login",
521
+ comments_count: "comments"
522
+ }
523
+ },
524
+ PullRequest: {
525
+ path: "/{owner}/{repo}/pulls/{number}",
526
+ fieldMap: { author: "user_login" }
527
+ },
528
+ Comment: {
529
+ path: "/{owner}/{repo}/issues/{number}/comments/{id}",
530
+ fieldMap: { author: "user_login" }
531
+ }
532
+ }
533
+ };
534
+ }
535
+ onMount(_root) {}
536
+ /**
537
+ * Resolve a path based on mode
538
+ * - single-repo: prepend owner/repo
539
+ * - org: prepend owner (org name)
540
+ * - multi-repo: pass through
541
+ */
542
+ resolvePath(path) {
543
+ let decodedPath;
544
+ try {
545
+ decodedPath = decodeURIComponent(path);
546
+ } catch {
547
+ decodedPath = path;
548
+ }
549
+ const cleanPath = decodedPath.split("/").filter((segment) => segment !== "" && segment !== "." && segment !== "..").join("/");
550
+ if (this.options.mode === "single-repo" && this.options.owner && this.options.repo) {
551
+ if (!cleanPath) return `/${this.options.owner}/${this.options.repo}`;
552
+ return `/${this.options.owner}/${this.options.repo}/${cleanPath}`;
553
+ }
554
+ if (this.options.mode === "org" && this.options.owner) {
555
+ if (!cleanPath) return "/";
556
+ return `/${this.options.owner}/${cleanPath}`;
557
+ }
558
+ return cleanPath ? `/${cleanPath}` : "/";
559
+ }
560
+ /**
561
+ * Get virtual directory structure for root path
562
+ * Returns the available resource types (issues, pulls, repo)
563
+ */
564
+ getVirtualDirectories() {
565
+ return [
566
+ {
567
+ id: "issues",
568
+ path: "/issues",
569
+ summary: "Repository Issues",
570
+ metadata: {
571
+ childrenCount: -1,
572
+ description: "List and read repository issues"
573
+ }
574
+ },
575
+ {
576
+ id: "pulls",
577
+ path: "/pulls",
578
+ summary: "Pull Requests",
579
+ metadata: {
580
+ childrenCount: -1,
581
+ description: "List and read pull requests"
582
+ }
583
+ },
584
+ {
585
+ id: "repo",
586
+ path: "/repo",
587
+ summary: "Repository Code",
588
+ metadata: {
589
+ childrenCount: -1,
590
+ description: "Repository source code (via Contents API)"
591
+ }
592
+ }
593
+ ];
594
+ }
595
+ /**
596
+ * Check if path is root or empty
597
+ */
598
+ isRootPath(path) {
599
+ return path.replace(/^\/+|\/+$/g, "") === "";
600
+ }
601
+ /**
602
+ * Get root entry (the mount point itself)
603
+ */
604
+ getRootEntry() {
605
+ let summary;
606
+ let childrenCount;
607
+ if (this.options.mode === "org") {
608
+ summary = this.description ?? `GitHub Organization: ${this.options.owner}`;
609
+ childrenCount = -1;
610
+ } else if (this.options.mode === "single-repo") {
611
+ summary = this.description ?? `GitHub: ${this.options.owner}/${this.options.repo}`;
612
+ childrenCount = 3;
613
+ } else {
614
+ summary = this.description ?? "GitHub";
615
+ childrenCount = -1;
616
+ }
617
+ return {
618
+ id: this.name,
619
+ path: "/",
620
+ summary,
621
+ metadata: {
622
+ childrenCount,
623
+ description: summary
624
+ }
625
+ };
626
+ }
627
+ /**
628
+ * Get virtual directories for a repo (issues, pulls, repo)
629
+ */
630
+ getRepoVirtualDirectories(repoPath) {
631
+ return [
632
+ {
633
+ id: `${repoPath}/issues`,
634
+ path: `${repoPath}/issues`,
635
+ summary: "Repository Issues",
636
+ metadata: {
637
+ childrenCount: -1,
638
+ description: "List and read repository issues"
639
+ }
640
+ },
641
+ {
642
+ id: `${repoPath}/pulls`,
643
+ path: `${repoPath}/pulls`,
644
+ summary: "Pull Requests",
645
+ metadata: {
646
+ childrenCount: -1,
647
+ description: "List and read pull requests"
648
+ }
649
+ },
650
+ {
651
+ id: `${repoPath}/repo`,
652
+ path: `${repoPath}/repo`,
653
+ summary: "Repository Code",
654
+ metadata: {
655
+ childrenCount: -1,
656
+ description: "Repository source code (via Contents API)"
657
+ }
658
+ }
659
+ ];
660
+ }
661
+ /**
662
+ * Check if a path represents a repo root in org mode
663
+ * e.g., "/reponame" without /issues or /pulls suffix
664
+ */
665
+ isRepoRootPath(path) {
666
+ return path.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean).length === 1;
667
+ }
668
+ /**
669
+ * Format a user-friendly error message based on the error type
670
+ */
671
+ formatErrorMessage(error, context) {
672
+ const status = error?.status;
673
+ const message = error instanceof Error ? error.message : String(error);
674
+ const sanitized = message.replace(/ghp_[a-zA-Z0-9]+/g, "[REDACTED]");
675
+ if (status === 401 || message.includes("Bad credentials")) {
676
+ if (!this.options.auth?.token) return `GitHub authentication required for ${context}. Set GITHUB_TOKEN environment variable or provide auth.token in config.`;
677
+ return `GitHub token is invalid or expired. Please check your GITHUB_TOKEN and ensure it has the required permissions.`;
678
+ }
679
+ if (status === 403) {
680
+ if (message.includes("rate limit")) return `GitHub API rate limit exceeded. ${this.options.auth?.token ? "Your token may have hit its limit." : "Set GITHUB_TOKEN for higher rate limits (5000/hour vs 60/hour)."} Try again later.`;
681
+ return `GitHub access forbidden for ${context}. Check that your token has the required permissions (repo scope for private repos).`;
682
+ }
683
+ if (status === 404) return `GitHub ${context} not found. Check that the organization/user name is correct and accessible.`;
684
+ if (message.includes("ENOTFOUND") || message.toLowerCase().includes("network")) return `Network error connecting to GitHub. Check your internet connection.`;
685
+ return `GitHub API error: ${sanitized}`;
686
+ }
687
+ /**
688
+ * Check if a path is a repo path (should be routed to Contents API)
689
+ * - Single-repo mode: /repo or /repo/*
690
+ * - Org mode: /{repoName}/repo or /{repoName}/repo/*
691
+ */
692
+ isRepoPath(path) {
693
+ const segments = path.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
694
+ if (this.options.mode === "single-repo") return segments[0] === "repo";
695
+ if (this.options.mode === "org") return segments.length >= 2 && segments[1] === "repo";
696
+ return false;
697
+ }
698
+ /**
699
+ * Parse a repo path into owner, repo name, branch, and file path
700
+ * Structure: /repo/{branch}/{filePath}
701
+ * - Single-repo mode: /repo/main/src/index.ts -> { owner, repo, branch: "main", filePath: "src/index.ts" }
702
+ * - Org mode: /afs/repo/main/src/index.ts -> { owner, repo: "afs", branch: "main", filePath: "src/index.ts" }
703
+ */
704
+ parseRepoPath(path) {
705
+ const segments = path.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
706
+ if (this.options.mode === "single-repo") {
707
+ const branch = segments[1];
708
+ const filePath = segments.slice(2).join("/");
709
+ return {
710
+ owner: this.options.owner,
711
+ repo: this.options.repo,
712
+ branch,
713
+ filePath
714
+ };
715
+ }
716
+ if (this.options.mode === "org") {
717
+ const repoName = segments[0];
718
+ const branch = segments[2];
719
+ const filePath = segments.slice(3).join("/");
720
+ return {
721
+ owner: this.options.owner,
722
+ repo: repoName,
723
+ branch,
724
+ filePath
725
+ };
726
+ }
727
+ return {
728
+ owner: this.options.owner,
729
+ repo: this.options.repo,
730
+ branch: void 0,
731
+ filePath: ""
732
+ };
733
+ }
734
+ /**
735
+ * Build the path prefix for a repo entry
736
+ */
737
+ getRepoPathPrefix(repo) {
738
+ return this.options.mode === "single-repo" ? "/repo" : `/${repo}/repo`;
739
+ }
740
+ /**
741
+ * List repository contents via GitHub Contents API
742
+ * - /repo -> list all branches
743
+ * - /repo/{branch} -> list branch root
744
+ * - /repo/{branch}/path -> list contents at path
745
+ */
746
+ async listViaContentsAPI(path, _options) {
747
+ const { owner, repo, branch, filePath } = this.parseRepoPath(path);
748
+ const repoPrefix = this.getRepoPathPrefix(repo);
749
+ if (!branch) try {
750
+ const branches = await this.client.getBranches(owner, repo);
751
+ const defaultBranch = await this.client.getDefaultBranch(owner, repo);
752
+ return { data: branches.map((b) => ({
753
+ id: `${repoPrefix}/${b.name}`,
754
+ path: `${repoPrefix}/${b.name}`,
755
+ summary: b.name + (b.name === defaultBranch ? " (default)" : ""),
756
+ metadata: {
757
+ type: "branch",
758
+ sha: b.commit.sha,
759
+ protected: b.protected,
760
+ isDefault: b.name === defaultBranch,
761
+ childrenCount: -1
762
+ }
763
+ })) };
764
+ } catch (error) {
765
+ return {
766
+ data: [],
767
+ message: this.formatErrorMessage(error, `branches for ${owner}/${repo}`)
768
+ };
769
+ }
770
+ try {
771
+ const contents = await this.client.getContents(owner, repo, filePath, branch);
772
+ if (!Array.isArray(contents)) {
773
+ const entryPath = filePath ? `${repoPrefix}/${branch}/${contents.path}` : `${repoPrefix}/${branch}/${contents.name}`;
774
+ return { data: [{
775
+ id: entryPath,
776
+ path: entryPath,
777
+ summary: contents.name,
778
+ metadata: {
779
+ type: contents.type,
780
+ size: contents.size,
781
+ sha: contents.sha,
782
+ childrenCount: contents.type === "dir" ? -1 : void 0
783
+ }
784
+ }] };
785
+ }
786
+ return { data: contents.map((item) => {
787
+ const entryPath = filePath ? `${repoPrefix}/${branch}/${filePath}/${item.name}` : `${repoPrefix}/${branch}/${item.name}`;
788
+ return {
789
+ id: entryPath,
790
+ path: entryPath,
791
+ summary: item.name,
792
+ metadata: {
793
+ type: item.type,
794
+ size: item.size,
795
+ sha: item.sha,
796
+ childrenCount: item.type === "dir" ? -1 : void 0
797
+ }
798
+ };
799
+ }) };
800
+ } catch (error) {
801
+ if (error?.status === 404) return {
802
+ data: [],
803
+ message: `Path not found: ${branch}/${filePath || ""}`
804
+ };
805
+ return {
806
+ data: [],
807
+ message: this.formatErrorMessage(error, `repository contents for ${owner}/${repo}`)
808
+ };
809
+ }
810
+ }
811
+ /**
812
+ * Read file content via GitHub Contents API
813
+ * Path format: /repo/{branch}/{filePath}
814
+ */
815
+ async readViaContentsAPI(path, _options) {
816
+ const { owner, repo, branch, filePath } = this.parseRepoPath(path);
817
+ const repoPrefix = this.getRepoPathPrefix(repo);
818
+ if (!branch) return {
819
+ data: void 0,
820
+ message: "Branch is required to read files"
821
+ };
822
+ if (!filePath) return {
823
+ data: void 0,
824
+ message: "Cannot read branch root as file"
825
+ };
826
+ try {
827
+ const contents = await this.client.getContents(owner, repo, filePath, branch);
828
+ if (Array.isArray(contents)) return {
829
+ data: void 0,
830
+ message: "Path is a directory, not a file"
831
+ };
832
+ if (contents.type !== "file") return {
833
+ data: void 0,
834
+ message: `Path is a ${contents.type}, not a file`
835
+ };
836
+ let content;
837
+ if (contents.content) content = Buffer.from(contents.content, "base64").toString("utf-8");
838
+ else content = await this.client.getBlob(owner, repo, contents.sha);
839
+ const entryPath = `${repoPrefix}/${branch}/${filePath}`;
840
+ return { data: {
841
+ id: entryPath,
842
+ path: entryPath,
843
+ summary: contents.name,
844
+ content,
845
+ metadata: {
846
+ type: "file",
847
+ size: contents.size,
848
+ sha: contents.sha,
849
+ branch
850
+ }
851
+ } };
852
+ } catch (error) {
853
+ if (error?.status === 404) return {
854
+ data: void 0,
855
+ message: `File not found: ${branch}/${filePath}`
856
+ };
857
+ return {
858
+ data: void 0,
859
+ message: this.formatErrorMessage(error, `file ${filePath} in ${owner}/${repo}@${branch}`)
860
+ };
861
+ }
862
+ }
863
+ /**
864
+ * List organization or user repositories
865
+ * Uses ownerType if specified, otherwise tries org endpoint first and falls back to user
866
+ */
867
+ async listOrgRepos(includePrivate) {
868
+ const owner = this.options.owner;
869
+ const ownerType = this.options.ownerType;
870
+ let allRepos = [];
871
+ if (ownerType) try {
872
+ allRepos = await this.fetchAllRepos(ownerType, owner, includePrivate);
873
+ } catch (error) {
874
+ const context = ownerType === "org" ? `organization "${owner}"` : `user "${owner}"`;
875
+ return {
876
+ data: [],
877
+ message: this.formatErrorMessage(error, context)
878
+ };
879
+ }
880
+ else try {
881
+ allRepos = await this.fetchAllRepos("org", owner, includePrivate);
882
+ } catch (error) {
883
+ if (error instanceof Error && error.message.includes("404") || error?.status === 404) try {
884
+ allRepos = await this.fetchAllRepos("user", owner, includePrivate);
885
+ } catch (userError) {
886
+ if (userError?.status === 404) return {
887
+ data: [],
888
+ message: `GitHub organization or user "${owner}" not found. Check that the name is correct.`
889
+ };
890
+ return {
891
+ data: [],
892
+ message: this.formatErrorMessage(userError, `user "${owner}"`)
893
+ };
894
+ }
895
+ else return {
896
+ data: [],
897
+ message: this.formatErrorMessage(error, `organization "${owner}"`)
898
+ };
899
+ }
900
+ return { data: (this.options.auth?.token ? allRepos : allRepos.filter((r) => !r.private)).map((repo) => ({
901
+ id: repo.name,
902
+ path: `/${repo.name}`,
903
+ summary: repo.description || repo.name,
904
+ metadata: {
905
+ childrenCount: 3,
906
+ description: repo.description,
907
+ private: repo.private
908
+ }
909
+ })) };
910
+ }
911
+ /**
912
+ * Fetch all repos with pagination support
913
+ */
914
+ async fetchAllRepos(type, owner, includePrivate) {
915
+ const allRepos = [];
916
+ let page = 1;
917
+ const perPage = 100;
918
+ while (true) {
919
+ let response;
920
+ if (type === "org") response = await this.client.request("GET /orgs/{org}/repos", {
921
+ org: owner,
922
+ per_page: perPage,
923
+ page,
924
+ type: includePrivate ? "all" : "public"
925
+ });
926
+ else response = await this.client.request("GET /users/{username}/repos", {
927
+ username: owner,
928
+ per_page: perPage,
929
+ page,
930
+ type: includePrivate ? "all" : "public"
931
+ });
932
+ const repos = response.data;
933
+ allRepos.push(...repos);
934
+ if (repos.length < perPage) break;
935
+ page++;
936
+ }
937
+ return allRepos;
938
+ }
939
+ /**
940
+ * List entries at a path
941
+ */
942
+ async list(path, options) {
943
+ const maxDepth = options?.maxDepth ?? 1;
944
+ if (this.isRepoPath(path)) return this.listViaContentsAPI(path, options);
945
+ if (this.isRootPath(path)) {
946
+ if (maxDepth === 0) return { data: [this.getRootEntry()] };
947
+ if (this.options.mode === "org") return this.listOrgRepos(!!this.options.auth?.token);
948
+ return { data: this.getVirtualDirectories() };
949
+ }
950
+ if (this.options.mode === "org" && this.isRepoRootPath(path)) {
951
+ const repoName = path.replace(/^\/+|\/+$/g, "");
952
+ if (maxDepth === 0) return { data: [{
953
+ id: repoName,
954
+ path: `/${repoName}`,
955
+ summary: `Repository: ${this.options.owner}/${repoName}`,
956
+ metadata: { childrenCount: 2 }
957
+ }] };
958
+ return { data: this.getRepoVirtualDirectories(`/${repoName}`) };
959
+ }
960
+ const fullPath = this.resolvePath(path);
961
+ if (!this.compiled) return {
962
+ data: [],
963
+ message: "Mapping not loaded"
964
+ };
965
+ const resolved = this.compiled.resolve(fullPath);
966
+ if (!resolved || !resolved.operations.list) return {
967
+ data: [],
968
+ message: `No list operation for path: ${fullPath}`
969
+ };
970
+ const request = this.compiled.buildRequest(fullPath, "list", { query: options?.filter });
971
+ if (!request) return {
972
+ data: [],
973
+ message: "Failed to build request"
974
+ };
975
+ try {
976
+ let responseData = (await this.client.request(`${request.method} ${request.path}`, request.params)).data;
977
+ if (Array.isArray(responseData) && fullPath.endsWith("/issues")) responseData = responseData.filter((item) => !item.pull_request);
978
+ const entries = this.compiled.projectResponse(fullPath, "list", responseData);
979
+ if (this.options.mode === "single-repo") {
980
+ const prefix = `/${this.options.owner}/${this.options.repo}`;
981
+ for (const entry of entries) if (entry.path.startsWith(prefix)) entry.path = entry.path.slice(prefix.length) || "/";
982
+ } else if (this.options.mode === "org") {
983
+ const prefix = `/${this.options.owner}`;
984
+ for (const entry of entries) if (entry.path.startsWith(prefix)) entry.path = entry.path.slice(prefix.length) || "/";
985
+ }
986
+ return { data: entries };
987
+ } catch (error) {
988
+ return {
989
+ data: [],
990
+ message: this.formatErrorMessage(error, `path "${path}"`)
991
+ };
992
+ }
993
+ }
994
+ /**
995
+ * Read a single entry
996
+ */
997
+ async read(path, options) {
998
+ if (this.isRepoPath(path)) return this.readViaContentsAPI(path, options);
999
+ if (this.options.mode === "org" || this.options.mode === "single-repo") {
1000
+ const segments = path.replace(/^\/+|\/+$/g, "").split("/");
1001
+ const issuesIdx = segments.indexOf("issues");
1002
+ const pullsIdx = segments.indexOf("pulls");
1003
+ if (issuesIdx !== -1 && segments[issuesIdx + 1]) {
1004
+ const issueNum = segments[issuesIdx + 1];
1005
+ if (!/^\d+$/.test(issueNum)) return {
1006
+ data: void 0,
1007
+ message: "Invalid issue number"
1008
+ };
1009
+ }
1010
+ if (pullsIdx !== -1 && segments[pullsIdx + 1]) {
1011
+ const prNum = segments[pullsIdx + 1];
1012
+ if (!/^\d+$/.test(prNum)) return {
1013
+ data: void 0,
1014
+ message: "Invalid pull request number"
1015
+ };
1016
+ }
1017
+ }
1018
+ const fullPath = this.resolvePath(path);
1019
+ if (!this.compiled) return {
1020
+ data: void 0,
1021
+ message: "Mapping not loaded"
1022
+ };
1023
+ const resolved = this.compiled.resolve(fullPath);
1024
+ if (!resolved || !resolved.operations.read) return {
1025
+ data: void 0,
1026
+ message: `No read operation for path: ${fullPath}`
1027
+ };
1028
+ const request = this.compiled.buildRequest(fullPath, "read", {});
1029
+ if (!request) return {
1030
+ data: void 0,
1031
+ message: "Failed to build request"
1032
+ };
1033
+ try {
1034
+ const response = await this.client.request(`${request.method} ${request.path}`, request.params);
1035
+ const entries = this.compiled.projectResponse(fullPath, "read", response.data);
1036
+ if (entries.length === 0) return {
1037
+ data: void 0,
1038
+ message: "No data returned"
1039
+ };
1040
+ const entry = entries[0];
1041
+ if (this.options.mode === "single-repo") {
1042
+ const prefix = `/${this.options.owner}/${this.options.repo}`;
1043
+ if (entry.path.startsWith(prefix)) entry.path = entry.path.slice(prefix.length) || "/";
1044
+ } else if (this.options.mode === "org") {
1045
+ const prefix = `/${this.options.owner}`;
1046
+ if (entry.path.startsWith(prefix)) entry.path = entry.path.slice(prefix.length) || "/";
1047
+ }
1048
+ return { data: entry };
1049
+ } catch (error) {
1050
+ return {
1051
+ data: void 0,
1052
+ message: this.formatErrorMessage(error, `path "${path}"`)
1053
+ };
1054
+ }
1055
+ }
1056
+ async loadMapping(mappingPath) {
1057
+ const compiler = new MappingCompiler();
1058
+ try {
1059
+ this.compiled = await compiler.compileDirectory(mappingPath);
1060
+ this.mappingPath = mappingPath;
1061
+ this.mappingLoadedAt = /* @__PURE__ */ new Date();
1062
+ this.mappingError = void 0;
1063
+ } catch (error) {
1064
+ this.mappingError = error instanceof Error ? error.message : String(error);
1065
+ throw error;
1066
+ }
1067
+ }
1068
+ async reloadMapping() {
1069
+ if (!this.mappingPath) throw new Error("No mapping path configured");
1070
+ const previousCompiled = this.compiled;
1071
+ try {
1072
+ await this.loadMapping(this.mappingPath);
1073
+ } catch (error) {
1074
+ this.compiled = previousCompiled;
1075
+ throw error;
1076
+ }
1077
+ }
1078
+ getMappingStatus() {
1079
+ return {
1080
+ loaded: this.compiled !== null,
1081
+ loadedAt: this.mappingLoadedAt,
1082
+ mappingPath: this.mappingPath ?? void 0,
1083
+ compiled: this.compiled !== null,
1084
+ error: this.mappingError,
1085
+ stats: this.compiled ? {
1086
+ routes: this.compiled.routeCount,
1087
+ operations: this.compiled.operationCount
1088
+ } : void 0
1089
+ };
1090
+ }
1091
+ resolve(path) {
1092
+ const fullPath = this.resolvePath(path);
1093
+ if (!this.compiled) return null;
1094
+ const resolved = this.compiled.resolve(fullPath);
1095
+ if (!resolved) return null;
1096
+ if (!(resolved.operations.read ?? resolved.operations.list)) return null;
1097
+ const request = this.compiled.buildRequest(fullPath, resolved.operations.read ? "read" : "list", {});
1098
+ if (!request) return null;
1099
+ return {
1100
+ type: "http",
1101
+ target: `${this.options.baseUrl}${request.path}`,
1102
+ method: request.method,
1103
+ params: {
1104
+ ...resolved.params,
1105
+ ...request.params
1106
+ },
1107
+ headers: request.headers
1108
+ };
1109
+ }
1110
+ project(externalData, context) {
1111
+ if (!this.compiled) return [];
1112
+ const operationType = context.rule === "list" ? "list" : "read";
1113
+ return this.compiled.projectResponse(context.path, operationType, externalData);
1114
+ }
1115
+ async mutate(_path, _action, _payload) {
1116
+ return {
1117
+ success: false,
1118
+ error: "Write operations not yet implemented"
1119
+ };
1120
+ }
1121
+ };
1122
+
1123
+ //#endregion
1124
+ export { AFSGitHub, GitHubClient, accessModeSchema, afsGitHubOptionsSchema, authOptionsSchema, cacheOptionsSchema, ownerTypeSchema, rateLimitOptionsSchema, repoModeSchema };
1125
+ //# sourceMappingURL=index.mjs.map