@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.
- package/dist/branch-linker.d.ts +31 -9
- package/dist/branch-linker.d.ts.map +1 -1
- package/dist/branch-linker.js +61 -35
- package/dist/branch-linker.js.map +1 -1
- package/dist/commands/edit.d.ts +2 -0
- package/dist/commands/edit.d.ts.map +1 -0
- package/dist/commands/edit.js +126 -0
- package/dist/commands/edit.js.map +1 -0
- package/dist/git-utils.d.ts +6 -52
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +7 -137
- package/dist/git-utils.js.map +1 -1
- package/dist/github-api.d.ts +17 -146
- package/dist/github-api.d.ts.map +1 -1
- package/dist/github-api.js +48 -864
- package/dist/github-api.js.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +6 -38
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
package/dist/github-api.js
CHANGED
|
@@ -1,35 +1,22 @@
|
|
|
1
|
-
|
|
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
|
-
*
|
|
15
|
+
* CLI token provider that gets tokens from environment or gh CLI
|
|
8
16
|
*/
|
|
9
|
-
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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
|