gh-api-client 1.0.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.mjs ADDED
@@ -0,0 +1,948 @@
1
+ // src/security/Security.ts
2
+ var Security = class {
3
+ /**
4
+ * Creates a new Security instance with a GitHub personal access token.
5
+ *
6
+ * @param token - A GitHub personal access token (e.g., `ghp_...`) or OAuth token
7
+ * @param apiUrl - The base URL of the GitHub API. Defaults to `'https://api.github.com'`.
8
+ * Must be a valid URL; throws if it cannot be parsed.
9
+ *
10
+ * @throws {TypeError} If `apiUrl` is not a valid URL
11
+ */
12
+ constructor(token, apiUrl = "https://api.github.com") {
13
+ if (!URL.canParse(apiUrl)) {
14
+ throw new TypeError(`Invalid apiUrl: "${apiUrl}" is not a valid URL`);
15
+ }
16
+ this.apiUrl = apiUrl.replace(/\/$/, "");
17
+ this.token = token;
18
+ }
19
+ /**
20
+ * Returns the base URL of the GitHub API, without a trailing slash.
21
+ *
22
+ * @returns The API base URL
23
+ */
24
+ getApiUrl() {
25
+ return this.apiUrl;
26
+ }
27
+ /**
28
+ * Returns the value of the `Authorization` header for Bearer authentication.
29
+ *
30
+ * @returns The Authorization header value in the format `Bearer <token>`
31
+ */
32
+ getAuthorizationHeader() {
33
+ return `Bearer ${this.token}`;
34
+ }
35
+ /**
36
+ * Returns the full set of HTTP headers required for authenticated GitHub API requests.
37
+ *
38
+ * @returns An object containing `Authorization`, `Accept`, `Content-Type`, and `X-GitHub-Api-Version` headers
39
+ */
40
+ getHeaders() {
41
+ return {
42
+ Authorization: `Bearer ${this.token}`,
43
+ Accept: "application/vnd.github+json",
44
+ "Content-Type": "application/json",
45
+ "X-GitHub-Api-Version": "2022-11-28"
46
+ };
47
+ }
48
+ /**
49
+ * Returns headers for raw file content requests.
50
+ *
51
+ * @returns Headers with `Accept: application/vnd.github.raw+json`
52
+ */
53
+ getRawHeaders() {
54
+ return {
55
+ ...this.getHeaders(),
56
+ Accept: "application/vnd.github.raw+json"
57
+ };
58
+ }
59
+ };
60
+
61
+ // src/errors/GitHubApiError.ts
62
+ var GitHubApiError = class extends Error {
63
+ constructor(status, statusText) {
64
+ super(`GitHub API error: ${status} ${statusText}`);
65
+ this.name = "GitHubApiError";
66
+ this.status = status;
67
+ this.statusText = statusText;
68
+ }
69
+ };
70
+
71
+ // src/resources/PullRequestResource.ts
72
+ var PullRequestResource = class {
73
+ /** @internal */
74
+ constructor(request, requestList, owner, repo, pullNumber) {
75
+ this.request = request;
76
+ this.requestList = requestList;
77
+ this.basePath = `/repos/${owner}/${repo}/pulls/${pullNumber}`;
78
+ }
79
+ /**
80
+ * Allows the resource to be awaited directly, resolving with the pull request info.
81
+ * Delegates to {@link PullRequestResource.get}.
82
+ */
83
+ then(onfulfilled, onrejected) {
84
+ return this.get().then(onfulfilled, onrejected);
85
+ }
86
+ /**
87
+ * Fetches the pull request details.
88
+ *
89
+ * `GET /repos/{owner}/{repo}/pulls/{pull_number}`
90
+ *
91
+ * @returns The pull request object
92
+ */
93
+ async get() {
94
+ return this.request(this.basePath);
95
+ }
96
+ /**
97
+ * Fetches the commits included in this pull request.
98
+ *
99
+ * `GET /repos/{owner}/{repo}/pulls/{pull_number}/commits`
100
+ *
101
+ * @param params - Optional pagination: `per_page`, `page`
102
+ * @returns A paged response of commits
103
+ */
104
+ async commits(params) {
105
+ return this.requestList(
106
+ `${this.basePath}/commits`,
107
+ params
108
+ );
109
+ }
110
+ /**
111
+ * Fetches the files changed by this pull request.
112
+ *
113
+ * `GET /repos/{owner}/{repo}/pulls/{pull_number}/files`
114
+ *
115
+ * @param params - Optional pagination: `per_page`, `page`
116
+ * @returns A paged response of changed files
117
+ */
118
+ async files(params) {
119
+ return this.requestList(
120
+ `${this.basePath}/files`,
121
+ params
122
+ );
123
+ }
124
+ /**
125
+ * Fetches the reviews submitted on this pull request.
126
+ *
127
+ * `GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews`
128
+ *
129
+ * @param params - Optional pagination: `per_page`, `page`
130
+ * @returns A paged response of reviews
131
+ */
132
+ async reviews(params) {
133
+ return this.requestList(
134
+ `${this.basePath}/reviews`,
135
+ params
136
+ );
137
+ }
138
+ /**
139
+ * Fetches the inline review comments on this pull request's diff.
140
+ *
141
+ * `GET /repos/{owner}/{repo}/pulls/{pull_number}/comments`
142
+ *
143
+ * @param params - Optional filters: `sort`, `direction`, `since`, `per_page`, `page`
144
+ * @returns A paged response of review comments
145
+ */
146
+ async reviewComments(params) {
147
+ return this.requestList(
148
+ `${this.basePath}/comments`,
149
+ params
150
+ );
151
+ }
152
+ /**
153
+ * Checks whether the pull request has been merged.
154
+ *
155
+ * `GET /repos/{owner}/{repo}/pulls/{pull_number}/merge`
156
+ *
157
+ * @returns `true` if merged (HTTP 204), `false` if not merged (HTTP 404)
158
+ */
159
+ async isMerged() {
160
+ try {
161
+ await this.request(`${this.basePath}/merge`);
162
+ return true;
163
+ } catch {
164
+ return false;
165
+ }
166
+ }
167
+ };
168
+
169
+ // src/resources/CommitResource.ts
170
+ var CommitResource = class {
171
+ /** @internal */
172
+ constructor(request, requestList, owner, repo, ref) {
173
+ this.request = request;
174
+ this.requestList = requestList;
175
+ this.basePath = `/repos/${owner}/${repo}/commits/${ref}`;
176
+ this.ref = ref;
177
+ }
178
+ /**
179
+ * Allows the resource to be awaited directly, resolving with the commit info.
180
+ * Delegates to {@link CommitResource.get}.
181
+ */
182
+ then(onfulfilled, onrejected) {
183
+ return this.get().then(onfulfilled, onrejected);
184
+ }
185
+ /**
186
+ * Fetches the commit details, including stats and changed files.
187
+ *
188
+ * `GET /repos/{owner}/{repo}/commits/{ref}`
189
+ *
190
+ * @returns The commit object with stats and files
191
+ */
192
+ async get() {
193
+ return this.request(this.basePath);
194
+ }
195
+ /**
196
+ * Fetches the individual commit statuses (from CI/CD systems via the Statuses API).
197
+ *
198
+ * `GET /repos/{owner}/{repo}/statuses/{sha}`
199
+ *
200
+ * @param params - Optional pagination: `per_page`, `page`
201
+ * @returns A paged response of commit statuses
202
+ */
203
+ async statuses(params) {
204
+ const repoPath = this.basePath.replace(`/commits/${this.ref}`, "");
205
+ return this.requestList(
206
+ `${repoPath}/statuses/${this.ref}`,
207
+ params
208
+ );
209
+ }
210
+ /**
211
+ * Fetches the combined commit status — an aggregation of all statuses for this ref.
212
+ *
213
+ * `GET /repos/{owner}/{repo}/commits/{ref}/status`
214
+ *
215
+ * @returns The combined status object
216
+ */
217
+ async combinedStatus() {
218
+ return this.request(`${this.basePath}/status`);
219
+ }
220
+ /**
221
+ * Fetches GitHub Actions check runs for this commit.
222
+ *
223
+ * `GET /repos/{owner}/{repo}/commits/{ref}/check-runs`
224
+ *
225
+ * @param params - Optional filters: `check_name`, `status`, `app_id`, `per_page`, `page`
226
+ * @returns A paged response of check runs
227
+ */
228
+ async checkRuns(params) {
229
+ const raw = await this.request(
230
+ `${this.basePath}/check-runs`,
231
+ params
232
+ );
233
+ return {
234
+ values: raw.check_runs,
235
+ hasNextPage: false
236
+ };
237
+ }
238
+ };
239
+
240
+ // src/resources/RepositoryResource.ts
241
+ var RepositoryResource = class {
242
+ /** @internal */
243
+ constructor(request, requestList, requestText, owner, repo) {
244
+ this.request = request;
245
+ this.requestList = requestList;
246
+ this.requestText = requestText;
247
+ this.owner = owner;
248
+ this.repo = repo;
249
+ this.basePath = `/repos/${owner}/${repo}`;
250
+ }
251
+ /**
252
+ * Allows the resource to be awaited directly, resolving with the repository info.
253
+ * Delegates to {@link RepositoryResource.get}.
254
+ */
255
+ then(onfulfilled, onrejected) {
256
+ return this.get().then(onfulfilled, onrejected);
257
+ }
258
+ /**
259
+ * Fetches the repository details.
260
+ *
261
+ * `GET /repos/{owner}/{repo}`
262
+ *
263
+ * @returns The repository object
264
+ */
265
+ async get() {
266
+ return this.request(this.basePath);
267
+ }
268
+ /**
269
+ * Fetches pull requests for this repository.
270
+ *
271
+ * `GET /repos/{owner}/{repo}/pulls`
272
+ *
273
+ * @param params - Optional filters: `state`, `head`, `base`, `sort`, `direction`, `per_page`, `page`
274
+ * @returns A paged response of pull requests
275
+ */
276
+ async pullRequests(params) {
277
+ return this.requestList(
278
+ `${this.basePath}/pulls`,
279
+ params
280
+ );
281
+ }
282
+ /**
283
+ * Returns a {@link PullRequestResource} for a given pull request number.
284
+ *
285
+ * The returned resource can be awaited directly to fetch pull request info,
286
+ * or chained to access nested resources.
287
+ *
288
+ * @param pullNumber - The pull request number (not the ID)
289
+ * @returns A chainable pull request resource
290
+ *
291
+ * @example
292
+ * ```typescript
293
+ * const pr = await gh.org('github').repo('linguist').pullRequest(42);
294
+ * const files = await gh.org('github').repo('linguist').pullRequest(42).files();
295
+ * const reviews = await gh.org('github').repo('linguist').pullRequest(42).reviews();
296
+ * ```
297
+ */
298
+ pullRequest(pullNumber) {
299
+ return new PullRequestResource(
300
+ this.request,
301
+ this.requestList,
302
+ this.owner,
303
+ this.repo,
304
+ pullNumber
305
+ );
306
+ }
307
+ /**
308
+ * Fetches commits for this repository.
309
+ *
310
+ * `GET /repos/{owner}/{repo}/commits`
311
+ *
312
+ * @param params - Optional filters: `sha`, `path`, `author`, `since`, `until`, `per_page`, `page`
313
+ * @returns A paged response of commits
314
+ */
315
+ async commits(params) {
316
+ return this.requestList(
317
+ `${this.basePath}/commits`,
318
+ params
319
+ );
320
+ }
321
+ /**
322
+ * Returns a {@link CommitResource} for a given commit ref (SHA, branch, or tag).
323
+ *
324
+ * The returned resource can be awaited directly to fetch commit info,
325
+ * or chained to access nested resources.
326
+ *
327
+ * @param ref - Commit SHA, branch name, or tag name
328
+ * @returns A chainable commit resource
329
+ *
330
+ * @example
331
+ * ```typescript
332
+ * const commit = await gh.org('github').repo('linguist').commit('abc123');
333
+ * const statuses = await gh.org('github').repo('linguist').commit('abc123').statuses();
334
+ * ```
335
+ */
336
+ commit(ref) {
337
+ return new CommitResource(
338
+ this.request,
339
+ this.requestList,
340
+ this.owner,
341
+ this.repo,
342
+ ref
343
+ );
344
+ }
345
+ /**
346
+ * Fetches branches for this repository.
347
+ *
348
+ * `GET /repos/{owner}/{repo}/branches`
349
+ *
350
+ * @param params - Optional filters: `protected`, `per_page`, `page`
351
+ * @returns A paged response of branches
352
+ */
353
+ async branches(params) {
354
+ return this.requestList(
355
+ `${this.basePath}/branches`,
356
+ params
357
+ );
358
+ }
359
+ /**
360
+ * Fetches a specific branch by name.
361
+ *
362
+ * `GET /repos/{owner}/{repo}/branches/{branch}`
363
+ *
364
+ * @param name - The branch name (e.g., `'main'`)
365
+ * @returns The branch object
366
+ */
367
+ async branch(name) {
368
+ return this.request(`${this.basePath}/branches/${encodeURIComponent(name)}`);
369
+ }
370
+ /**
371
+ * Fetches tags for this repository.
372
+ *
373
+ * `GET /repos/{owner}/{repo}/tags`
374
+ *
375
+ * @param params - Optional pagination: `per_page`, `page`
376
+ * @returns A paged response of tags
377
+ */
378
+ async tags(params) {
379
+ return this.requestList(
380
+ `${this.basePath}/tags`,
381
+ params
382
+ );
383
+ }
384
+ /**
385
+ * Fetches releases for this repository.
386
+ *
387
+ * `GET /repos/{owner}/{repo}/releases`
388
+ *
389
+ * @param params - Optional pagination: `per_page`, `page`
390
+ * @returns A paged response of releases
391
+ */
392
+ async releases(params) {
393
+ return this.requestList(
394
+ `${this.basePath}/releases`,
395
+ params
396
+ );
397
+ }
398
+ /**
399
+ * Fetches the latest published release for this repository.
400
+ *
401
+ * `GET /repos/{owner}/{repo}/releases/latest`
402
+ *
403
+ * @returns The latest release object
404
+ */
405
+ async latestRelease() {
406
+ return this.request(`${this.basePath}/releases/latest`);
407
+ }
408
+ /**
409
+ * Fetches the forks of this repository.
410
+ *
411
+ * `GET /repos/{owner}/{repo}/forks`
412
+ *
413
+ * @param params - Optional filters: `sort`, `per_page`, `page`
414
+ * @returns A paged response of forked repositories
415
+ */
416
+ async forks(params) {
417
+ return this.requestList(
418
+ `${this.basePath}/forks`,
419
+ params
420
+ );
421
+ }
422
+ /**
423
+ * Fetches webhooks configured on this repository.
424
+ *
425
+ * `GET /repos/{owner}/{repo}/hooks`
426
+ *
427
+ * @param params - Optional pagination: `per_page`, `page`
428
+ * @returns A paged response of webhooks
429
+ */
430
+ async webhooks(params) {
431
+ return this.requestList(
432
+ `${this.basePath}/hooks`,
433
+ params
434
+ );
435
+ }
436
+ /**
437
+ * Fetches the contents of a file or directory in this repository.
438
+ *
439
+ * `GET /repos/{owner}/{repo}/contents/{path}`
440
+ *
441
+ * Returns a single {@link GitHubContent} for files, or an array for directories.
442
+ *
443
+ * @param path - Path to the file or directory (e.g., `'src/index.ts'` or `'src'`). Omit for root.
444
+ * @param params - Optional: `ref` (branch, tag, or commit SHA)
445
+ * @returns File content object or array of directory entries
446
+ */
447
+ async contents(path, params) {
448
+ const contentPath = path ? `${this.basePath}/contents/${path}` : `${this.basePath}/contents`;
449
+ return this.request(
450
+ contentPath,
451
+ params
452
+ );
453
+ }
454
+ /**
455
+ * Fetches the raw text content of a file in this repository.
456
+ *
457
+ * Uses `Accept: application/vnd.github.raw+json` to retrieve the file directly
458
+ * without base64 encoding.
459
+ *
460
+ * `GET /repos/{owner}/{repo}/contents/{filePath}`
461
+ *
462
+ * @param filePath - Path to the file (e.g., `'src/index.ts'`)
463
+ * @param params - Optional: `ref` (branch, tag, or commit SHA)
464
+ * @returns The raw file content as a string
465
+ */
466
+ async raw(filePath, params) {
467
+ return this.requestText(
468
+ `${this.basePath}/contents/${filePath}`,
469
+ params
470
+ );
471
+ }
472
+ /**
473
+ * Fetches the repository topics.
474
+ *
475
+ * `GET /repos/{owner}/{repo}/topics`
476
+ *
477
+ * @returns An array of topic strings
478
+ */
479
+ async topics() {
480
+ const data = await this.request(`${this.basePath}/topics`);
481
+ return data.names;
482
+ }
483
+ /**
484
+ * Fetches contributors to this repository.
485
+ *
486
+ * `GET /repos/{owner}/{repo}/contributors`
487
+ *
488
+ * @param params - Optional filters: `anon` (include anonymous contributors), `per_page`, `page`
489
+ * @returns A paged response of contributors
490
+ */
491
+ async contributors(params) {
492
+ return this.requestList(
493
+ `${this.basePath}/contributors`,
494
+ params
495
+ );
496
+ }
497
+ };
498
+
499
+ // src/resources/OrganizationResource.ts
500
+ var OrganizationResource = class {
501
+ /** @internal */
502
+ constructor(request, requestList, requestText, org) {
503
+ this.request = request;
504
+ this.requestList = requestList;
505
+ this.requestText = requestText;
506
+ this.org = org;
507
+ }
508
+ /**
509
+ * Allows the resource to be awaited directly, resolving with the organization info.
510
+ * Delegates to {@link OrganizationResource.get}.
511
+ */
512
+ then(onfulfilled, onrejected) {
513
+ return this.get().then(onfulfilled, onrejected);
514
+ }
515
+ /**
516
+ * Fetches the organization details.
517
+ *
518
+ * `GET /orgs/{org}`
519
+ *
520
+ * @returns The organization object
521
+ */
522
+ async get() {
523
+ return this.request(`/orgs/${this.org}`);
524
+ }
525
+ /**
526
+ * Fetches repositories belonging to this organization.
527
+ *
528
+ * `GET /orgs/{org}/repos`
529
+ *
530
+ * @param params - Optional filters: `type`, `sort`, `direction`, `per_page`, `page`
531
+ * @returns A paged response of repositories
532
+ */
533
+ async repos(params) {
534
+ return this.requestList(
535
+ `/orgs/${this.org}/repos`,
536
+ params
537
+ );
538
+ }
539
+ /**
540
+ * Returns a {@link RepositoryResource} for a given repository name within this organization.
541
+ *
542
+ * The returned resource can be awaited directly to fetch repository info,
543
+ * or chained to access nested resources.
544
+ *
545
+ * @param name - The repository name (e.g., `'linguist'`)
546
+ * @returns A chainable repository resource
547
+ *
548
+ * @example
549
+ * ```typescript
550
+ * const repo = await gh.org('github').repo('linguist');
551
+ * const prs = await gh.org('github').repo('linguist').pullRequests({ state: 'open' });
552
+ * const commits = await gh.org('github').repo('linguist').commits({ per_page: 10 });
553
+ * ```
554
+ */
555
+ repo(name) {
556
+ return new RepositoryResource(
557
+ this.request,
558
+ this.requestList,
559
+ this.requestText,
560
+ this.org,
561
+ name
562
+ );
563
+ }
564
+ /**
565
+ * Fetches members of this organization.
566
+ *
567
+ * `GET /orgs/{org}/members`
568
+ *
569
+ * @param params - Optional filters: `role`, `filter`, `per_page`, `page`
570
+ * @returns A paged response of users
571
+ */
572
+ async members(params) {
573
+ return this.requestList(
574
+ `/orgs/${this.org}/members`,
575
+ params
576
+ );
577
+ }
578
+ };
579
+
580
+ // src/resources/UserResource.ts
581
+ var UserResource = class {
582
+ /** @internal */
583
+ constructor(request, requestList, requestText, login) {
584
+ this.request = request;
585
+ this.requestList = requestList;
586
+ this.requestText = requestText;
587
+ this.login = login;
588
+ this.basePath = `/users/${login}`;
589
+ }
590
+ /**
591
+ * Allows the resource to be awaited directly, resolving with the user info.
592
+ * Delegates to {@link UserResource.get}.
593
+ */
594
+ then(onfulfilled, onrejected) {
595
+ return this.get().then(onfulfilled, onrejected);
596
+ }
597
+ /**
598
+ * Fetches the user details.
599
+ *
600
+ * `GET /users/{username}`
601
+ *
602
+ * @returns The user object
603
+ */
604
+ async get() {
605
+ return this.request(this.basePath);
606
+ }
607
+ /**
608
+ * Fetches public repositories for this user.
609
+ *
610
+ * `GET /users/{username}/repos`
611
+ *
612
+ * @param params - Optional filters: `type`, `sort`, `direction`, `per_page`, `page`
613
+ * @returns A paged response of repositories
614
+ */
615
+ async repos(params) {
616
+ return this.requestList(
617
+ `${this.basePath}/repos`,
618
+ params
619
+ );
620
+ }
621
+ /**
622
+ * Returns a {@link RepositoryResource} for a given repository under this user,
623
+ * providing access to all repository sub-resources.
624
+ *
625
+ * @param name - The repository name
626
+ * @returns A chainable repository resource
627
+ *
628
+ * @example
629
+ * ```typescript
630
+ * const repo = await gh.user('octocat').repo('Hello-World');
631
+ * const content = await gh.user('octocat').repo('Hello-World').raw('README.md');
632
+ * ```
633
+ */
634
+ repo(name) {
635
+ return new RepositoryResource(
636
+ this.request,
637
+ this.requestList,
638
+ this.requestText,
639
+ this.login,
640
+ name
641
+ );
642
+ }
643
+ /**
644
+ * Fetches the users this user is following.
645
+ *
646
+ * `GET /users/{username}/following`
647
+ *
648
+ * @returns A paged response of users
649
+ */
650
+ async following(params) {
651
+ return this.requestList(
652
+ `${this.basePath}/following`,
653
+ params
654
+ );
655
+ }
656
+ /**
657
+ * Fetches the users following this user.
658
+ *
659
+ * `GET /users/{username}/followers`
660
+ *
661
+ * @returns A paged response of users
662
+ */
663
+ async followers(params) {
664
+ return this.requestList(
665
+ `${this.basePath}/followers`,
666
+ params
667
+ );
668
+ }
669
+ };
670
+
671
+ // src/GitHubClient.ts
672
+ var GitHubClient = class {
673
+ /**
674
+ * @param options - Authentication and connection options
675
+ * @throws {TypeError} If `apiUrl` is not a valid URL
676
+ */
677
+ constructor({ token, apiUrl }) {
678
+ this.listeners = /* @__PURE__ */ new Map();
679
+ this.security = new Security(token, apiUrl);
680
+ }
681
+ /**
682
+ * Subscribes to a client event.
683
+ *
684
+ * @example
685
+ * ```typescript
686
+ * gh.on('request', (event) => {
687
+ * console.log(`${event.method} ${event.url} — ${event.durationMs}ms`);
688
+ * if (event.error) console.error('Request failed:', event.error);
689
+ * });
690
+ * ```
691
+ */
692
+ on(event, callback) {
693
+ const callbacks = this.listeners.get(event) ?? [];
694
+ callbacks.push(callback);
695
+ this.listeners.set(event, callbacks);
696
+ return this;
697
+ }
698
+ emit(event, payload) {
699
+ const callbacks = this.listeners.get(event) ?? [];
700
+ for (const cb of callbacks) {
701
+ cb(payload);
702
+ }
703
+ }
704
+ /**
705
+ * Performs an authenticated GET request returning a single JSON object.
706
+ * @internal
707
+ */
708
+ async request(path, params, options) {
709
+ const base = `${this.security.getApiUrl()}${path}`;
710
+ const url = buildUrl(base, params);
711
+ const startedAt = /* @__PURE__ */ new Date();
712
+ let statusCode;
713
+ try {
714
+ const headers = options?.headers ?? this.security.getHeaders();
715
+ const response = await fetch(url, { headers });
716
+ statusCode = response.status;
717
+ if (!response.ok) {
718
+ throw new GitHubApiError(response.status, response.statusText);
719
+ }
720
+ const data = await response.json();
721
+ this.emit("request", { url, method: "GET", startedAt, finishedAt: /* @__PURE__ */ new Date(), durationMs: Date.now() - startedAt.getTime(), statusCode });
722
+ return data;
723
+ } catch (err) {
724
+ const finishedAt = /* @__PURE__ */ new Date();
725
+ this.emit("request", { url, method: "GET", startedAt, finishedAt, durationMs: finishedAt.getTime() - startedAt.getTime(), statusCode, error: err instanceof Error ? err : new Error(String(err)) });
726
+ throw err;
727
+ }
728
+ }
729
+ /**
730
+ * Performs an authenticated GET request returning a paginated list.
731
+ * Parses the `Link` response header to determine if more pages exist.
732
+ * @internal
733
+ */
734
+ async requestList(path, params) {
735
+ const base = `${this.security.getApiUrl()}${path}`;
736
+ const url = buildUrl(base, params);
737
+ const startedAt = /* @__PURE__ */ new Date();
738
+ let statusCode;
739
+ try {
740
+ const response = await fetch(url, { headers: this.security.getHeaders() });
741
+ statusCode = response.status;
742
+ if (!response.ok) {
743
+ throw new GitHubApiError(response.status, response.statusText);
744
+ }
745
+ const data = await response.json();
746
+ const linkHeader = response.headers.get("Link");
747
+ const nextPage = parseNextPage(linkHeader);
748
+ this.emit("request", { url, method: "GET", startedAt, finishedAt: /* @__PURE__ */ new Date(), durationMs: Date.now() - startedAt.getTime(), statusCode });
749
+ return {
750
+ values: data,
751
+ hasNextPage: nextPage !== void 0,
752
+ nextPage
753
+ };
754
+ } catch (err) {
755
+ const finishedAt = /* @__PURE__ */ new Date();
756
+ this.emit("request", { url, method: "GET", startedAt, finishedAt, durationMs: finishedAt.getTime() - startedAt.getTime(), statusCode, error: err instanceof Error ? err : new Error(String(err)) });
757
+ throw err;
758
+ }
759
+ }
760
+ /**
761
+ * Performs a GET request returning raw text content.
762
+ * Uses `Accept: application/vnd.github.raw+json` to retrieve file content directly.
763
+ * @internal
764
+ */
765
+ async requestText(path, params) {
766
+ const base = `${this.security.getApiUrl()}${path}`;
767
+ const url = buildUrl(base, params);
768
+ const startedAt = /* @__PURE__ */ new Date();
769
+ let statusCode;
770
+ try {
771
+ const response = await fetch(url, { headers: this.security.getRawHeaders() });
772
+ statusCode = response.status;
773
+ if (!response.ok) {
774
+ throw new GitHubApiError(response.status, response.statusText);
775
+ }
776
+ const text = await response.text();
777
+ this.emit("request", { url, method: "GET", startedAt, finishedAt: /* @__PURE__ */ new Date(), durationMs: Date.now() - startedAt.getTime(), statusCode });
778
+ return text;
779
+ } catch (err) {
780
+ const finishedAt = /* @__PURE__ */ new Date();
781
+ this.emit("request", { url, method: "GET", startedAt, finishedAt, durationMs: finishedAt.getTime() - startedAt.getTime(), statusCode, error: err instanceof Error ? err : new Error(String(err)) });
782
+ throw err;
783
+ }
784
+ }
785
+ makeRequestFn() {
786
+ return (path, params) => this.request(path, params);
787
+ }
788
+ makeRequestListFn() {
789
+ return (path, params) => this.requestList(path, params);
790
+ }
791
+ makeRequestTextFn() {
792
+ return (path, params) => this.requestText(path, params);
793
+ }
794
+ /**
795
+ * Fetches the authenticated user's profile.
796
+ *
797
+ * `GET /user`
798
+ *
799
+ * @returns The authenticated user object
800
+ *
801
+ * @example
802
+ * ```typescript
803
+ * const me = await gh.currentUser();
804
+ * console.log(me.login); // 'octocat'
805
+ * ```
806
+ */
807
+ async currentUser() {
808
+ return this.request("/user");
809
+ }
810
+ /**
811
+ * Returns a {@link UserResource} for a given GitHub login, providing access
812
+ * to user data and their repositories.
813
+ *
814
+ * The returned resource can be awaited directly to fetch user info,
815
+ * or chained to access nested resources.
816
+ *
817
+ * @param login - The user's login name (e.g., `'octocat'`)
818
+ * @returns A chainable user resource
819
+ *
820
+ * @example
821
+ * ```typescript
822
+ * const user = await gh.user('octocat');
823
+ * const repos = await gh.user('octocat').repos({ sort: 'updated' });
824
+ * const pr = await gh.user('octocat').repo('Hello-World').pullRequest(1).files();
825
+ * ```
826
+ */
827
+ user(login) {
828
+ return new UserResource(
829
+ this.makeRequestFn(),
830
+ this.makeRequestListFn(),
831
+ this.makeRequestTextFn(),
832
+ login
833
+ );
834
+ }
835
+ /**
836
+ * Returns an {@link OrganizationResource} for a given GitHub organization, providing
837
+ * access to organization data and its repositories.
838
+ *
839
+ * The returned resource can be awaited directly to fetch organization info,
840
+ * or chained to access nested resources.
841
+ *
842
+ * @param name - The organization's login name (e.g., `'github'`)
843
+ * @returns A chainable organization resource
844
+ *
845
+ * @example
846
+ * ```typescript
847
+ * const org = await gh.org('github');
848
+ * const repos = await gh.org('github').repos({ type: 'public' });
849
+ * const prs = await gh.org('github').repo('linguist').pullRequests({ state: 'open' });
850
+ * ```
851
+ */
852
+ org(name) {
853
+ return new OrganizationResource(
854
+ this.makeRequestFn(),
855
+ this.makeRequestListFn(),
856
+ this.makeRequestTextFn(),
857
+ name
858
+ );
859
+ }
860
+ /**
861
+ * Returns a {@link RepositoryResource} for a given owner and repository name.
862
+ *
863
+ * Shortcut that works for both user repositories and organization repositories.
864
+ *
865
+ * @param owner - The owner login (user or organization)
866
+ * @param name - The repository name
867
+ * @returns A chainable repository resource
868
+ *
869
+ * @example
870
+ * ```typescript
871
+ * const repo = await gh.repo('octocat', 'Hello-World');
872
+ * const prs = await gh.repo('octocat', 'Hello-World').pullRequests();
873
+ * ```
874
+ */
875
+ repo(owner, name) {
876
+ return new RepositoryResource(
877
+ this.makeRequestFn(),
878
+ this.makeRequestListFn(),
879
+ this.makeRequestTextFn(),
880
+ owner,
881
+ name
882
+ );
883
+ }
884
+ /**
885
+ * Searches for repositories using GitHub's search syntax.
886
+ *
887
+ * `GET /search/repositories`
888
+ *
889
+ * @param params - Search query and optional filters. `q` is required.
890
+ * @returns A paged response of repositories with `totalCount`
891
+ *
892
+ * @example
893
+ * ```typescript
894
+ * const results = await gh.searchRepos({ q: 'language:typescript stars:>1000', sort: 'stars' });
895
+ * console.log(`Found ${results.totalCount} repositories`);
896
+ * ```
897
+ */
898
+ async searchRepos(params) {
899
+ const base = `${this.security.getApiUrl()}/search/repositories`;
900
+ const url = buildUrl(base, params);
901
+ const startedAt = /* @__PURE__ */ new Date();
902
+ let statusCode;
903
+ try {
904
+ const response = await fetch(url, { headers: this.security.getHeaders() });
905
+ statusCode = response.status;
906
+ if (!response.ok) {
907
+ throw new GitHubApiError(response.status, response.statusText);
908
+ }
909
+ const data = await response.json();
910
+ const linkHeader = response.headers.get("Link");
911
+ const nextPage = parseNextPage(linkHeader);
912
+ this.emit("request", { url, method: "GET", startedAt, finishedAt: /* @__PURE__ */ new Date(), durationMs: Date.now() - startedAt.getTime(), statusCode });
913
+ return {
914
+ values: data.items,
915
+ hasNextPage: nextPage !== void 0,
916
+ nextPage,
917
+ totalCount: data.total_count
918
+ };
919
+ } catch (err) {
920
+ const finishedAt = /* @__PURE__ */ new Date();
921
+ this.emit("request", { url, method: "GET", startedAt, finishedAt, durationMs: finishedAt.getTime() - startedAt.getTime(), statusCode, error: err instanceof Error ? err : new Error(String(err)) });
922
+ throw err;
923
+ }
924
+ }
925
+ };
926
+ function buildUrl(base, params) {
927
+ if (!params) return base;
928
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0);
929
+ if (entries.length === 0) return base;
930
+ const search = new URLSearchParams(entries.map(([k, v]) => [k, String(v)]));
931
+ return `${base}?${search.toString()}`;
932
+ }
933
+ function parseNextPage(linkHeader) {
934
+ if (!linkHeader) return void 0;
935
+ const match = linkHeader.match(/<[^>]*[?&]page=(\d+)[^>]*>;\s*rel="next"/);
936
+ return match ? parseInt(match[1], 10) : void 0;
937
+ }
938
+ export {
939
+ CommitResource,
940
+ GitHubApiError,
941
+ GitHubClient,
942
+ OrganizationResource,
943
+ PullRequestResource,
944
+ RepositoryResource,
945
+ Security,
946
+ UserResource
947
+ };
948
+ //# sourceMappingURL=index.mjs.map