@bretwardjames/ghp-cli 0.1.2 → 0.1.4

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.
@@ -1,35 +1,22 @@
1
- import { graphql } from '@octokit/graphql';
1
+ /**
2
+ * CLI-specific GitHub API wrapper.
3
+ *
4
+ * This module wraps the core GitHubAPI class with CLI-specific behavior:
5
+ * - Token from `gh auth token` or environment variables
6
+ * - Chalk-colored error messages
7
+ * - process.exit on auth errors
8
+ */
2
9
  import { exec } from 'child_process';
3
10
  import { promisify } from 'util';
4
11
  import chalk from 'chalk';
12
+ import { GitHubAPI as CoreGitHubAPI, } from '@bretwardjames/ghp-core';
5
13
  const execAsync = promisify(exec);
6
14
  /**
7
- * Check if an error is due to insufficient OAuth scopes
15
+ * CLI token provider that gets tokens from environment or gh CLI
8
16
  */
9
- function handleScopeError(error) {
10
- if (error && typeof error === 'object' && 'errors' in error) {
11
- const gqlError = error;
12
- const scopeError = gqlError.errors?.find(e => e.type === 'INSUFFICIENT_SCOPES');
13
- if (scopeError) {
14
- console.error(chalk.red('\nError:'), 'Your GitHub token is missing required scopes.');
15
- console.error(chalk.dim('GitHub Projects requires the'), chalk.cyan('read:project'), chalk.dim('scope.'));
16
- console.error();
17
- console.error('Run this command to add the required scope:');
18
- console.error(chalk.cyan(' gh auth refresh -s read:project -s project'));
19
- console.error();
20
- process.exit(1);
21
- }
22
- }
23
- throw error;
24
- }
25
- export class GitHubAPI {
26
- graphqlWithAuth = null;
27
- username = null;
28
- /**
29
- * Get token from gh CLI or environment variable
30
- */
17
+ const cliTokenProvider = {
31
18
  async getToken() {
32
- // First try environment variable
19
+ // First try environment variables
33
20
  if (process.env.GITHUB_TOKEN) {
34
21
  return process.env.GITHUB_TOKEN;
35
22
  }
@@ -44,846 +31,43 @@ export class GitHubAPI {
44
31
  catch {
45
32
  return null;
46
33
  }
47
- }
48
- /**
49
- * Authenticate with GitHub
50
- */
51
- async authenticate() {
52
- const token = await this.getToken();
53
- if (!token) {
54
- return false;
55
- }
56
- this.graphqlWithAuth = graphql.defaults({
57
- headers: {
58
- authorization: `token ${token}`,
59
- },
60
- });
61
- // Get current user
62
- try {
63
- const response = await this.graphqlWithAuth(`
64
- query {
65
- viewer {
66
- login
67
- }
68
- }
69
- `);
70
- this.username = response.viewer.login;
71
- return true;
72
- }
73
- catch {
74
- this.graphqlWithAuth = null;
75
- return false;
76
- }
77
- }
78
- get isAuthenticated() {
79
- return this.graphqlWithAuth !== null;
80
- }
81
- /**
82
- * Get projects linked to a repository
83
- */
84
- async getProjects(repo) {
85
- if (!this.graphqlWithAuth)
86
- throw new Error('Not authenticated');
87
- try {
88
- const response = await this.graphqlWithAuth(`
89
- query($owner: String!, $name: String!) {
90
- repository(owner: $owner, name: $name) {
91
- projectsV2(first: 20) {
92
- nodes {
93
- id
94
- title
95
- number
96
- url
97
- }
98
- }
99
- }
100
- }
101
- `, {
102
- owner: repo.owner,
103
- name: repo.name,
104
- });
105
- return response.repository.projectsV2.nodes;
106
- }
107
- catch (error) {
108
- handleScopeError(error);
109
- }
110
- }
111
- /**
112
- * Get items from a project
113
- */
114
- async getProjectItems(projectId, projectTitle) {
115
- if (!this.graphqlWithAuth)
116
- throw new Error('Not authenticated');
117
- // First, get the status field to build a status order map
118
- const statusField = await this.getStatusField(projectId);
119
- const statusOrderMap = new Map();
120
- if (statusField) {
121
- statusField.options.forEach((opt, idx) => {
122
- statusOrderMap.set(opt.name.toLowerCase(), idx);
123
- });
124
- }
125
- const response = await this.graphqlWithAuth(`
126
- query($projectId: ID!) {
127
- node(id: $projectId) {
128
- ... on ProjectV2 {
129
- items(first: 100) {
130
- nodes {
131
- id
132
- fieldValues(first: 20) {
133
- nodes {
134
- __typename
135
- ... on ProjectV2ItemFieldSingleSelectValue {
136
- name
137
- field { ... on ProjectV2SingleSelectField { name } }
138
- }
139
- ... on ProjectV2ItemFieldTextValue {
140
- text
141
- field { ... on ProjectV2Field { name } }
142
- }
143
- ... on ProjectV2ItemFieldNumberValue {
144
- number
145
- field { ... on ProjectV2Field { name } }
146
- }
147
- ... on ProjectV2ItemFieldDateValue {
148
- date
149
- field { ... on ProjectV2Field { name } }
150
- }
151
- ... on ProjectV2ItemFieldIterationValue {
152
- title
153
- field { ... on ProjectV2IterationField { name } }
154
- }
155
- }
156
- }
157
- content {
158
- __typename
159
- ... on Issue {
160
- title
161
- number
162
- url
163
- state
164
- issueType { name }
165
- assignees(first: 5) { nodes { login } }
166
- labels(first: 10) { nodes { name color } }
167
- repository { name }
168
- }
169
- ... on PullRequest {
170
- title
171
- number
172
- url
173
- state
174
- merged
175
- assignees(first: 5) { nodes { login } }
176
- labels(first: 10) { nodes { name color } }
177
- repository { name }
178
- }
179
- ... on DraftIssue {
180
- title
181
- }
182
- }
183
- }
184
- }
185
- }
186
- }
187
- }
188
- `, { projectId });
189
- return response.node.items.nodes
190
- .filter(item => item.content)
191
- .map(item => {
192
- const content = item.content;
193
- // Extract all field values into a map
194
- const fields = {};
195
- for (const fv of item.fieldValues.nodes) {
196
- const fieldName = fv.field?.name;
197
- if (!fieldName)
198
- continue;
199
- if (fv.__typename === 'ProjectV2ItemFieldSingleSelectValue' && fv.name) {
200
- fields[fieldName] = fv.name;
201
- }
202
- else if (fv.__typename === 'ProjectV2ItemFieldTextValue' && fv.text) {
203
- fields[fieldName] = fv.text;
204
- }
205
- else if (fv.__typename === 'ProjectV2ItemFieldNumberValue' && fv.number !== undefined) {
206
- fields[fieldName] = fv.number.toString();
207
- }
208
- else if (fv.__typename === 'ProjectV2ItemFieldDateValue' && fv.date) {
209
- fields[fieldName] = fv.date;
210
- }
211
- else if (fv.__typename === 'ProjectV2ItemFieldIterationValue' && fv.title) {
212
- fields[fieldName] = fv.title;
213
- }
214
- }
215
- let type = 'draft';
216
- if (content.__typename === 'Issue')
217
- type = 'issue';
218
- else if (content.__typename === 'PullRequest')
219
- type = 'pull_request';
220
- const status = fields['Status'] || null;
221
- const statusIndex = status
222
- ? (statusOrderMap.get(status.toLowerCase()) ?? 999)
223
- : 999;
224
- // Determine issue/PR state
225
- let state = null;
226
- if (content.state) {
227
- if (content.merged) {
228
- state = 'merged';
229
- }
230
- else if (content.state === 'OPEN') {
231
- state = 'open';
232
- }
233
- else {
234
- state = 'closed';
235
- }
236
- }
237
- return {
238
- id: item.id,
239
- title: content.title || 'Untitled',
240
- number: content.number || null,
241
- type,
242
- issueType: content.issueType?.name || null,
243
- status,
244
- statusIndex,
245
- state,
246
- assignees: content.assignees?.nodes.map(a => a.login) || [],
247
- labels: content.labels?.nodes || [],
248
- repository: content.repository?.name || null,
249
- url: content.url || null,
250
- projectId,
251
- projectTitle,
252
- fields,
253
- };
34
+ },
35
+ };
36
+ /**
37
+ * CLI error handler that prints colored messages and exits
38
+ */
39
+ function handleAuthError(error) {
40
+ if (error.type === 'INSUFFICIENT_SCOPES') {
41
+ console.error(chalk.red('\nError:'), 'Your GitHub token is missing required scopes.');
42
+ console.error(chalk.dim('GitHub Projects requires the'), chalk.cyan('read:project'), chalk.dim('scope.'));
43
+ console.error();
44
+ console.error('Run this command to add the required scope:');
45
+ console.error(chalk.cyan(' gh auth refresh -s read:project -s project'));
46
+ console.error();
47
+ }
48
+ else if (error.type === 'SSO_REQUIRED') {
49
+ console.error(chalk.red('\nError:'), 'SSO authentication required for this organization.');
50
+ console.error(chalk.dim('Please re-authenticate with SSO enabled.'));
51
+ console.error();
52
+ }
53
+ else {
54
+ console.error(chalk.red('\nError:'), error.message);
55
+ }
56
+ process.exit(1);
57
+ }
58
+ /**
59
+ * Extended GitHubAPI class for CLI with pre-configured token provider
60
+ */
61
+ class CLIGitHubAPI extends CoreGitHubAPI {
62
+ constructor() {
63
+ super({
64
+ tokenProvider: cliTokenProvider,
65
+ onAuthError: handleAuthError,
254
66
  });
255
67
  }
256
- /**
257
- * Get the Status field info for a project
258
- */
259
- async getStatusField(projectId) {
260
- if (!this.graphqlWithAuth)
261
- throw new Error('Not authenticated');
262
- const response = await this.graphqlWithAuth(`
263
- query($projectId: ID!) {
264
- node(id: $projectId) {
265
- ... on ProjectV2 {
266
- fields(first: 20) {
267
- nodes {
268
- __typename
269
- ... on ProjectV2SingleSelectField {
270
- id
271
- name
272
- options { id name }
273
- }
274
- }
275
- }
276
- }
277
- }
278
- }
279
- `, { projectId });
280
- const statusField = response.node.fields.nodes.find(f => f.__typename === 'ProjectV2SingleSelectField' && f.name === 'Status');
281
- if (!statusField || !statusField.options)
282
- return null;
283
- return {
284
- fieldId: statusField.id,
285
- options: statusField.options,
286
- };
287
- }
288
- /**
289
- * Get project views
290
- */
291
- async getProjectViews(projectId) {
292
- if (!this.graphqlWithAuth)
293
- throw new Error('Not authenticated');
294
- try {
295
- const response = await this.graphqlWithAuth(`
296
- query($projectId: ID!) {
297
- node(id: $projectId) {
298
- ... on ProjectV2 {
299
- views(first: 20) {
300
- nodes {
301
- name
302
- filter
303
- }
304
- }
305
- }
306
- }
307
- }
308
- `, { projectId });
309
- return response.node.views.nodes;
310
- }
311
- catch (error) {
312
- console.error('Error fetching project views:', error);
313
- return [];
314
- }
315
- }
316
- /**
317
- * Update an item's status
318
- */
319
- async updateItemStatus(projectId, itemId, fieldId, optionId) {
320
- if (!this.graphqlWithAuth)
321
- throw new Error('Not authenticated');
322
- try {
323
- await this.graphqlWithAuth(`
324
- mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
325
- updateProjectV2ItemFieldValue(input: {
326
- projectId: $projectId
327
- itemId: $itemId
328
- fieldId: $fieldId
329
- value: { singleSelectOptionId: $optionId }
330
- }) {
331
- projectV2Item { id }
332
- }
333
- }
334
- `, { projectId, itemId, fieldId, optionId });
335
- return true;
336
- }
337
- catch (error) {
338
- console.error('Failed to update status:', error);
339
- return false;
340
- }
341
- }
342
- /**
343
- * Find an item by issue number across all projects for this repo
344
- */
345
- async findItemByNumber(repo, issueNumber) {
346
- const projects = await this.getProjects(repo);
347
- for (const project of projects) {
348
- const items = await this.getProjectItems(project.id, project.title);
349
- const item = items.find(i => i.number === issueNumber);
350
- if (item)
351
- return item;
352
- }
353
- return null;
354
- }
355
- /**
356
- * Get all fields for a project
357
- */
358
- async getProjectFields(projectId) {
359
- if (!this.graphqlWithAuth)
360
- throw new Error('Not authenticated');
361
- const response = await this.graphqlWithAuth(`
362
- query($projectId: ID!) {
363
- node(id: $projectId) {
364
- ... on ProjectV2 {
365
- fields(first: 30) {
366
- nodes {
367
- __typename
368
- ... on ProjectV2Field {
369
- id
370
- name
371
- }
372
- ... on ProjectV2SingleSelectField {
373
- id
374
- name
375
- options { id name }
376
- }
377
- ... on ProjectV2IterationField {
378
- id
379
- name
380
- }
381
- }
382
- }
383
- }
384
- }
385
- }
386
- `, { projectId });
387
- return response.node.fields.nodes.map(f => ({
388
- id: f.id,
389
- name: f.name,
390
- type: f.__typename.replace('ProjectV2', '').replace('Field', ''),
391
- options: f.options,
392
- }));
393
- }
394
- /**
395
- * Set a field value on a project item
396
- */
397
- async setFieldValue(projectId, itemId, fieldId, value) {
398
- if (!this.graphqlWithAuth)
399
- throw new Error('Not authenticated');
400
- try {
401
- await this.graphqlWithAuth(`
402
- mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: ProjectV2FieldValue!) {
403
- updateProjectV2ItemFieldValue(input: {
404
- projectId: $projectId
405
- itemId: $itemId
406
- fieldId: $fieldId
407
- value: $value
408
- }) {
409
- projectV2Item { id }
410
- }
411
- }
412
- `, { projectId, itemId, fieldId, value });
413
- return true;
414
- }
415
- catch (error) {
416
- console.error('Failed to set field value:', error);
417
- return false;
418
- }
419
- }
420
- /**
421
- * Create a new issue
422
- */
423
- async createIssue(repo, title, body) {
424
- if (!this.graphqlWithAuth)
425
- throw new Error('Not authenticated');
426
- try {
427
- // First get the repository ID
428
- const repoResponse = await this.graphqlWithAuth(`
429
- query($owner: String!, $name: String!) {
430
- repository(owner: $owner, name: $name) {
431
- id
432
- }
433
- }
434
- `, { owner: repo.owner, name: repo.name });
435
- const response = await this.graphqlWithAuth(`
436
- mutation($repositoryId: ID!, $title: String!, $body: String) {
437
- createIssue(input: {
438
- repositoryId: $repositoryId
439
- title: $title
440
- body: $body
441
- }) {
442
- issue {
443
- id
444
- number
445
- }
446
- }
447
- }
448
- `, {
449
- repositoryId: repoResponse.repository.id,
450
- title,
451
- body: body || '',
452
- });
453
- return response.createIssue.issue;
454
- }
455
- catch (error) {
456
- console.error('Failed to create issue:', error);
457
- return null;
458
- }
459
- }
460
- /**
461
- * Add an issue to a project
462
- */
463
- async addToProject(projectId, contentId) {
464
- if (!this.graphqlWithAuth)
465
- throw new Error('Not authenticated');
466
- try {
467
- const response = await this.graphqlWithAuth(`
468
- mutation($projectId: ID!, $contentId: ID!) {
469
- addProjectV2ItemById(input: {
470
- projectId: $projectId
471
- contentId: $contentId
472
- }) {
473
- item { id }
474
- }
475
- }
476
- `, { projectId, contentId });
477
- return response.addProjectV2ItemById.item.id;
478
- }
479
- catch (error) {
480
- console.error('Failed to add to project:', error);
481
- return null;
482
- }
483
- }
484
- /**
485
- * Get full issue details including body and comments
486
- */
487
- async getIssueDetails(repo, issueNumber) {
488
- if (!this.graphqlWithAuth)
489
- throw new Error('Not authenticated');
490
- try {
491
- const response = await this.graphqlWithAuth(`
492
- query($owner: String!, $name: String!, $number: Int!) {
493
- repository(owner: $owner, name: $name) {
494
- issueOrPullRequest(number: $number) {
495
- __typename
496
- ... on Issue {
497
- title
498
- body
499
- state
500
- createdAt
501
- author { login }
502
- labels(first: 10) { nodes { name color } }
503
- comments(first: 50) {
504
- totalCount
505
- nodes {
506
- author { login }
507
- body
508
- createdAt
509
- }
510
- }
511
- }
512
- ... on PullRequest {
513
- title
514
- body
515
- state
516
- createdAt
517
- author { login }
518
- labels(first: 10) { nodes { name color } }
519
- comments(first: 50) {
520
- totalCount
521
- nodes {
522
- author { login }
523
- body
524
- createdAt
525
- }
526
- }
527
- }
528
- }
529
- }
530
- }
531
- `, {
532
- owner: repo.owner,
533
- name: repo.name,
534
- number: issueNumber,
535
- });
536
- const issue = response.repository.issueOrPullRequest;
537
- if (!issue)
538
- return null;
539
- return {
540
- title: issue.title,
541
- body: issue.body,
542
- state: issue.state,
543
- type: issue.__typename === 'PullRequest' ? 'pull_request' : 'issue',
544
- createdAt: issue.createdAt,
545
- author: issue.author?.login || 'unknown',
546
- labels: issue.labels.nodes,
547
- comments: issue.comments.nodes.map(c => ({
548
- author: c.author?.login || 'unknown',
549
- body: c.body,
550
- createdAt: c.createdAt,
551
- })),
552
- totalComments: issue.comments.totalCount,
553
- };
554
- }
555
- catch (error) {
556
- console.error('Failed to get issue details:', error);
557
- return null;
558
- }
559
- }
560
- /**
561
- * Add a comment to an issue or PR
562
- */
563
- async addComment(repo, issueNumber, body) {
564
- if (!this.graphqlWithAuth)
565
- throw new Error('Not authenticated');
566
- try {
567
- // First get the issue/PR node ID
568
- const issueResponse = await this.graphqlWithAuth(`
569
- query($owner: String!, $name: String!, $number: Int!) {
570
- repository(owner: $owner, name: $name) {
571
- issueOrPullRequest(number: $number) {
572
- ... on Issue { id }
573
- ... on PullRequest { id }
574
- }
575
- }
576
- }
577
- `, {
578
- owner: repo.owner,
579
- name: repo.name,
580
- number: issueNumber,
581
- });
582
- const subjectId = issueResponse.repository.issueOrPullRequest?.id;
583
- if (!subjectId) {
584
- console.error('Issue not found');
585
- return false;
586
- }
587
- await this.graphqlWithAuth(`
588
- mutation($subjectId: ID!, $body: String!) {
589
- addComment(input: { subjectId: $subjectId, body: $body }) {
590
- commentEdge {
591
- node { id }
592
- }
593
- }
594
- }
595
- `, { subjectId, body });
596
- return true;
597
- }
598
- catch (error) {
599
- console.error('Failed to add comment:', error);
600
- return false;
601
- }
602
- }
603
- /**
604
- * Get repository collaborators (for @ mention suggestions)
605
- */
606
- async getCollaborators(repo) {
607
- if (!this.graphqlWithAuth)
608
- throw new Error('Not authenticated');
609
- try {
610
- const response = await this.graphqlWithAuth(`
611
- query($owner: String!, $name: String!) {
612
- repository(owner: $owner, name: $name) {
613
- collaborators(first: 50) {
614
- nodes { login name }
615
- }
616
- assignableUsers(first: 50) {
617
- nodes { login name }
618
- }
619
- }
620
- }
621
- `, { owner: repo.owner, name: repo.name });
622
- // Use collaborators if available, fall back to assignable users
623
- const users = response.repository.collaborators?.nodes
624
- || response.repository.assignableUsers.nodes
625
- || [];
626
- return users.map(u => ({ login: u.login, name: u.name }));
627
- }
628
- catch {
629
- // Collaborators might not be accessible, return empty
630
- return [];
631
- }
632
- }
633
- /**
634
- * Get recent issues (for # reference suggestions)
635
- */
636
- async getRecentIssues(repo, limit = 20) {
637
- if (!this.graphqlWithAuth)
638
- throw new Error('Not authenticated');
639
- try {
640
- const response = await this.graphqlWithAuth(`
641
- query($owner: String!, $name: String!, $limit: Int!) {
642
- repository(owner: $owner, name: $name) {
643
- issues(first: $limit, orderBy: { field: UPDATED_AT, direction: DESC }) {
644
- nodes {
645
- number
646
- title
647
- state
648
- }
649
- }
650
- }
651
- }
652
- `, { owner: repo.owner, name: repo.name, limit });
653
- return response.repository.issues.nodes;
654
- }
655
- catch {
656
- return [];
657
- }
658
- }
659
- /**
660
- * Get the active label name for a user
661
- */
662
- getActiveLabelName() {
663
- return `@${this.username}:active`;
664
- }
665
- /**
666
- * Ensure a label exists in the repository, create if it doesn't
667
- */
668
- async ensureLabel(repo, labelName, color = '1f883d') {
669
- if (!this.graphqlWithAuth)
670
- throw new Error('Not authenticated');
671
- try {
672
- // First check if label exists
673
- const checkResponse = await this.graphqlWithAuth(`
674
- query($owner: String!, $name: String!, $labelName: String!) {
675
- repository(owner: $owner, name: $name) {
676
- label(name: $labelName) {
677
- id
678
- }
679
- }
680
- }
681
- `, { owner: repo.owner, name: repo.name, labelName });
682
- if (checkResponse.repository.label) {
683
- return true; // Label already exists
684
- }
685
- // Create the label using REST API (GraphQL createLabel requires different permissions)
686
- const token = await this.getToken();
687
- const response = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/labels`, {
688
- method: 'POST',
689
- headers: {
690
- 'Authorization': `token ${token}`,
691
- 'Content-Type': 'application/json',
692
- 'Accept': 'application/vnd.github.v3+json',
693
- },
694
- body: JSON.stringify({
695
- name: labelName,
696
- color: color,
697
- description: `Active working indicator for ${this.username}`,
698
- }),
699
- });
700
- if (response.status === 201) {
701
- return true;
702
- }
703
- else if (response.status === 422) {
704
- // Label already exists (race condition)
705
- return true;
706
- }
707
- else {
708
- console.error('Failed to create label:', await response.text());
709
- return false;
710
- }
711
- }
712
- catch (error) {
713
- console.error('Failed to ensure label:', error);
714
- return false;
715
- }
716
- }
717
- /**
718
- * Add a label to an issue
719
- */
720
- async addLabelToIssue(repo, issueNumber, labelName) {
721
- if (!this.graphqlWithAuth)
722
- throw new Error('Not authenticated');
723
- try {
724
- // Get the issue node ID and label ID
725
- const response = await this.graphqlWithAuth(`
726
- query($owner: String!, $name: String!, $number: Int!, $labelName: String!) {
727
- repository(owner: $owner, name: $name) {
728
- issue(number: $number) {
729
- id
730
- }
731
- label(name: $labelName) {
732
- id
733
- }
734
- }
735
- }
736
- `, { owner: repo.owner, name: repo.name, number: issueNumber, labelName });
737
- if (!response.repository.issue || !response.repository.label) {
738
- return false;
739
- }
740
- await this.graphqlWithAuth(`
741
- mutation($issueId: ID!, $labelIds: [ID!]!) {
742
- addLabelsToLabelable(input: { labelableId: $issueId, labelIds: $labelIds }) {
743
- clientMutationId
744
- }
745
- }
746
- `, {
747
- issueId: response.repository.issue.id,
748
- labelIds: [response.repository.label.id],
749
- });
750
- return true;
751
- }
752
- catch (error) {
753
- console.error('Failed to add label:', error);
754
- return false;
755
- }
756
- }
757
- /**
758
- * Remove a label from an issue
759
- */
760
- async removeLabelFromIssue(repo, issueNumber, labelName) {
761
- if (!this.graphqlWithAuth)
762
- throw new Error('Not authenticated');
763
- try {
764
- const response = await this.graphqlWithAuth(`
765
- query($owner: String!, $name: String!, $number: Int!, $labelName: String!) {
766
- repository(owner: $owner, name: $name) {
767
- issue(number: $number) {
768
- id
769
- }
770
- label(name: $labelName) {
771
- id
772
- }
773
- }
774
- }
775
- `, { owner: repo.owner, name: repo.name, number: issueNumber, labelName });
776
- if (!response.repository.issue || !response.repository.label) {
777
- return false;
778
- }
779
- await this.graphqlWithAuth(`
780
- mutation($issueId: ID!, $labelIds: [ID!]!) {
781
- removeLabelsFromLabelable(input: { labelableId: $issueId, labelIds: $labelIds }) {
782
- clientMutationId
783
- }
784
- }
785
- `, {
786
- issueId: response.repository.issue.id,
787
- labelIds: [response.repository.label.id],
788
- });
789
- return true;
790
- }
791
- catch (error) {
792
- console.error('Failed to remove label:', error);
793
- return false;
794
- }
795
- }
796
- /**
797
- * Find all issues with a specific label
798
- */
799
- async findIssuesWithLabel(repo, labelName) {
800
- if (!this.graphqlWithAuth)
801
- throw new Error('Not authenticated');
802
- try {
803
- const response = await this.graphqlWithAuth(`
804
- query($owner: String!, $name: String!, $labels: [String!]) {
805
- repository(owner: $owner, name: $name) {
806
- issues(first: 10, labels: $labels, states: [OPEN]) {
807
- nodes {
808
- number
809
- }
810
- }
811
- }
812
- }
813
- `, { owner: repo.owner, name: repo.name, labels: [labelName] });
814
- return response.repository.issues.nodes.map(i => i.number);
815
- }
816
- catch {
817
- return [];
818
- }
819
- }
820
- /**
821
- * Get available issue types for a repository
822
- */
823
- async getIssueTypes(repo) {
824
- if (!this.graphqlWithAuth)
825
- throw new Error('Not authenticated');
826
- try {
827
- const response = await this.graphqlWithAuth(`
828
- query($owner: String!, $name: String!) {
829
- repository(owner: $owner, name: $name) {
830
- issueTypes(first: 20) {
831
- nodes {
832
- id
833
- name
834
- }
835
- }
836
- }
837
- }
838
- `, { owner: repo.owner, name: repo.name });
839
- return response.repository.issueTypes?.nodes || [];
840
- }
841
- catch (error) {
842
- // Issue types may not be enabled for this repository
843
- return [];
844
- }
845
- }
846
- /**
847
- * Set the issue type on an issue
848
- */
849
- async setIssueType(repo, issueNumber, issueTypeId) {
850
- if (!this.graphqlWithAuth)
851
- throw new Error('Not authenticated');
852
- try {
853
- // First get the issue's node ID
854
- const issueResponse = await this.graphqlWithAuth(`
855
- query($owner: String!, $name: String!, $number: Int!) {
856
- repository(owner: $owner, name: $name) {
857
- issue(number: $number) {
858
- id
859
- }
860
- }
861
- }
862
- `, { owner: repo.owner, name: repo.name, number: issueNumber });
863
- if (!issueResponse.repository.issue) {
864
- return false;
865
- }
866
- // Update the issue type
867
- await this.graphqlWithAuth(`
868
- mutation($issueId: ID!, $issueTypeId: ID!) {
869
- updateIssue(input: { id: $issueId, issueTypeId: $issueTypeId }) {
870
- issue {
871
- id
872
- }
873
- }
874
- }
875
- `, {
876
- issueId: issueResponse.repository.issue.id,
877
- issueTypeId,
878
- });
879
- return true;
880
- }
881
- catch (error) {
882
- console.error('Failed to set issue type:', error);
883
- return false;
884
- }
885
- }
886
68
  }
887
- // Singleton instance
888
- export const api = new GitHubAPI();
69
+ // Singleton instance for CLI usage
70
+ export const api = new CLIGitHubAPI();
71
+ // Also export the class for testing
72
+ export { CLIGitHubAPI as GitHubAPI };
889
73
  //# sourceMappingURL=github-api.js.map