@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/LICENSE.md +26 -0
- package/README.md +342 -0
- package/dist/index.d.mts +431 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1125 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +60 -0
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
|