@haystackeditor/cli 0.12.3 → 0.13.1
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/README.md +1 -1
- package/dist/commands/config.js +7 -5
- package/dist/commands/dismiss.js +16 -10
- package/dist/commands/login.d.ts +4 -5
- package/dist/commands/login.js +95 -71
- package/dist/commands/pr-status.js +8 -4
- package/dist/commands/request-review.js +10 -8
- package/dist/commands/setup.js +8 -4
- package/dist/commands/submit.d.ts +3 -0
- package/dist/commands/submit.js +182 -25
- package/dist/commands/triage.js +12 -3
- package/dist/index.js +42 -5
- package/dist/triage/prompts.d.ts +3 -3
- package/dist/triage/prompts.js +24 -6
- package/dist/triage/runner.d.ts +7 -1
- package/dist/triage/runner.js +86 -21
- package/dist/triage/types.d.ts +3 -1
- package/dist/types.d.ts +34 -0
- package/dist/types.js +12 -0
- package/dist/utils/auth.d.ts +50 -0
- package/dist/utils/auth.js +386 -0
- package/dist/utils/github-api.d.ts +36 -15
- package/dist/utils/github-api.js +52 -44
- package/package.json +1 -1
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
const CONFIG_DIR = path.join(os.homedir(), '.haystack');
|
|
5
|
+
const TOKEN_FILE = path.join(CONFIG_DIR, 'credentials.json');
|
|
6
|
+
const TOKEN_FILE_DISPLAY = '~/.haystack/credentials.json';
|
|
7
|
+
const GITHUB_LOGIN_PATTERN = /^[A-Za-z\d](?:[A-Za-z\d]|-(?=[A-Za-z\d])){0,38}$/;
|
|
8
|
+
function emptyStore() {
|
|
9
|
+
return {
|
|
10
|
+
version: 2,
|
|
11
|
+
activeLogin: null,
|
|
12
|
+
accounts: {},
|
|
13
|
+
repoDefaults: {},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function isLegacyCredentials(value) {
|
|
17
|
+
if (!value || typeof value !== 'object')
|
|
18
|
+
return false;
|
|
19
|
+
const candidate = value;
|
|
20
|
+
const keys = Object.keys(candidate);
|
|
21
|
+
return keys.length === 2
|
|
22
|
+
&& keys.includes('github_token')
|
|
23
|
+
&& keys.includes('created_at')
|
|
24
|
+
&& typeof candidate.github_token === 'string'
|
|
25
|
+
&& typeof candidate.created_at === 'string';
|
|
26
|
+
}
|
|
27
|
+
function hasAmbiguousLegacyCredentialsShape(value) {
|
|
28
|
+
if (!value || typeof value !== 'object')
|
|
29
|
+
return false;
|
|
30
|
+
const candidate = value;
|
|
31
|
+
const hasLegacyFields = 'github_token' in candidate || 'created_at' in candidate;
|
|
32
|
+
const hasV2Fields = 'version' in candidate
|
|
33
|
+
|| 'accounts' in candidate
|
|
34
|
+
|| 'activeLogin' in candidate
|
|
35
|
+
|| 'repoDefaults' in candidate;
|
|
36
|
+
return hasLegacyFields && hasV2Fields;
|
|
37
|
+
}
|
|
38
|
+
function isStoredCredentialsShape(value) {
|
|
39
|
+
if (!value || typeof value !== 'object')
|
|
40
|
+
return false;
|
|
41
|
+
const candidate = value;
|
|
42
|
+
return 'version' in candidate
|
|
43
|
+
|| 'accounts' in candidate
|
|
44
|
+
|| 'activeLogin' in candidate
|
|
45
|
+
|| 'repoDefaults' in candidate;
|
|
46
|
+
}
|
|
47
|
+
function normalizeGitHubLogin(value) {
|
|
48
|
+
if (typeof value !== 'string')
|
|
49
|
+
return null;
|
|
50
|
+
const normalized = value.trim();
|
|
51
|
+
if (!normalized || !GITHUB_LOGIN_PATTERN.test(normalized)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return normalized;
|
|
55
|
+
}
|
|
56
|
+
function findStoredLogin(accounts, login) {
|
|
57
|
+
const normalized = normalizeGitHubLogin(login);
|
|
58
|
+
if (!normalized)
|
|
59
|
+
return null;
|
|
60
|
+
const lookupKey = normalized.toLowerCase();
|
|
61
|
+
for (const storedLogin of Object.keys(accounts)) {
|
|
62
|
+
if (storedLogin.toLowerCase() === lookupKey) {
|
|
63
|
+
return storedLogin;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
async function readGitHubErrorMessage(response) {
|
|
69
|
+
const body = await response.text().catch(() => '');
|
|
70
|
+
if (!body)
|
|
71
|
+
return '';
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(body);
|
|
74
|
+
return typeof parsed.message === 'string' ? parsed.message : body;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return body;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function isInvalidTokenForbidden(response, message) {
|
|
81
|
+
const ssoHeader = response.headers.get('x-github-sso');
|
|
82
|
+
if (ssoHeader && !ssoHeader.startsWith('partial-results')) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
const normalizedMessage = message.toLowerCase();
|
|
86
|
+
return normalizedMessage.includes('resource protected by organization saml enforcement')
|
|
87
|
+
|| normalizedMessage.includes('single sign-on')
|
|
88
|
+
|| normalizedMessage.includes('must grant your oauth token access')
|
|
89
|
+
|| normalizedMessage.includes('resource not accessible by personal access token');
|
|
90
|
+
}
|
|
91
|
+
function normalizeStore(value) {
|
|
92
|
+
if (!value || typeof value !== 'object') {
|
|
93
|
+
return emptyStore();
|
|
94
|
+
}
|
|
95
|
+
const raw = value;
|
|
96
|
+
const accounts = {};
|
|
97
|
+
if (raw.accounts && typeof raw.accounts === 'object') {
|
|
98
|
+
for (const [login, account] of Object.entries(raw.accounts)) {
|
|
99
|
+
const normalizedLogin = normalizeGitHubLogin(login);
|
|
100
|
+
if (normalizedLogin &&
|
|
101
|
+
!findStoredLogin(accounts, normalizedLogin) &&
|
|
102
|
+
account &&
|
|
103
|
+
typeof account === 'object' &&
|
|
104
|
+
typeof account.github_token === 'string' &&
|
|
105
|
+
typeof account.created_at === 'string') {
|
|
106
|
+
accounts[normalizedLogin] = {
|
|
107
|
+
github_token: account.github_token,
|
|
108
|
+
created_at: account.created_at,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const accountLogins = Object.keys(accounts);
|
|
114
|
+
const activeLogin = findStoredLogin(accounts, raw.activeLogin) ?? accountLogins[0] ?? null;
|
|
115
|
+
const repoDefaults = {};
|
|
116
|
+
if (raw.repoDefaults && typeof raw.repoDefaults === 'object') {
|
|
117
|
+
for (const [repoFullName, login] of Object.entries(raw.repoDefaults)) {
|
|
118
|
+
const storedLogin = findStoredLogin(accounts, login);
|
|
119
|
+
if (storedLogin) {
|
|
120
|
+
repoDefaults[repoFullName] = storedLogin;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
version: 2,
|
|
126
|
+
activeLogin,
|
|
127
|
+
accounts,
|
|
128
|
+
repoDefaults,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
async function ensureConfigDir() {
|
|
132
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
async function saveStore(store) {
|
|
135
|
+
const normalized = normalizeStore(store);
|
|
136
|
+
if (Object.keys(normalized.accounts).length === 0) {
|
|
137
|
+
await fs.rm(TOKEN_FILE, { force: true });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
await ensureConfigDir();
|
|
141
|
+
await fs.writeFile(TOKEN_FILE, JSON.stringify(normalized, null, 2), {
|
|
142
|
+
mode: 0o600,
|
|
143
|
+
});
|
|
144
|
+
await fs.chmod(TOKEN_FILE, 0o600);
|
|
145
|
+
}
|
|
146
|
+
export async function verifyGitHubToken(token) {
|
|
147
|
+
try {
|
|
148
|
+
const response = await fetch('https://api.github.com/user', {
|
|
149
|
+
headers: {
|
|
150
|
+
Authorization: `Bearer ${token}`,
|
|
151
|
+
'User-Agent': 'Haystack-CLI',
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
if (response.status === 401) {
|
|
155
|
+
return { status: 'invalid' };
|
|
156
|
+
}
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
const rateLimitRemaining = response.headers.get('x-ratelimit-remaining');
|
|
159
|
+
if (response.status === 403 && rateLimitRemaining === '0') {
|
|
160
|
+
return {
|
|
161
|
+
status: 'error',
|
|
162
|
+
message: 'GitHub API rate limit exceeded while verifying credentials.',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const message = await readGitHubErrorMessage(response);
|
|
166
|
+
if (response.status === 403 && isInvalidTokenForbidden(response, message)) {
|
|
167
|
+
return { status: 'invalid' };
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
status: 'error',
|
|
171
|
+
message: message
|
|
172
|
+
? `GitHub API returned ${response.status} while verifying credentials: ${message}`
|
|
173
|
+
: `GitHub API returned ${response.status} while verifying credentials.`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
status: 'ok',
|
|
178
|
+
user: await response.json(),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
return {
|
|
183
|
+
status: 'error',
|
|
184
|
+
message: error instanceof Error ? error.message : String(error),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function migrateLegacyCredentials(legacy) {
|
|
189
|
+
const verification = await verifyGitHubToken(legacy.github_token);
|
|
190
|
+
if (verification.status === 'invalid') {
|
|
191
|
+
const store = emptyStore();
|
|
192
|
+
await saveStore(store);
|
|
193
|
+
return store;
|
|
194
|
+
}
|
|
195
|
+
if (verification.status === 'error') {
|
|
196
|
+
throw new Error(`Could not migrate legacy credentials from ${TOKEN_FILE_DISPLAY}: ${verification.message}`);
|
|
197
|
+
}
|
|
198
|
+
const user = verification.user;
|
|
199
|
+
const store = {
|
|
200
|
+
version: 2,
|
|
201
|
+
activeLogin: user.login,
|
|
202
|
+
accounts: {
|
|
203
|
+
[user.login]: {
|
|
204
|
+
github_token: legacy.github_token,
|
|
205
|
+
created_at: legacy.created_at,
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
repoDefaults: {},
|
|
209
|
+
};
|
|
210
|
+
await saveStore(store);
|
|
211
|
+
return store;
|
|
212
|
+
}
|
|
213
|
+
export async function loadCredentialsStore() {
|
|
214
|
+
let content;
|
|
215
|
+
try {
|
|
216
|
+
content = await fs.readFile(TOKEN_FILE, 'utf-8');
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (error?.code === 'ENOENT') {
|
|
220
|
+
return emptyStore();
|
|
221
|
+
}
|
|
222
|
+
throw new Error(`Failed to read ${TOKEN_FILE_DISPLAY}: ${error instanceof Error ? error.message : String(error)}`);
|
|
223
|
+
}
|
|
224
|
+
let parsed;
|
|
225
|
+
try {
|
|
226
|
+
parsed = JSON.parse(content);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
throw new Error(`Failed to parse ${TOKEN_FILE_DISPLAY}: ${error instanceof Error ? error.message : String(error)}`);
|
|
230
|
+
}
|
|
231
|
+
if (isLegacyCredentials(parsed)) {
|
|
232
|
+
return migrateLegacyCredentials(parsed);
|
|
233
|
+
}
|
|
234
|
+
if (hasAmbiguousLegacyCredentialsShape(parsed)) {
|
|
235
|
+
throw new Error(`Failed to parse ${TOKEN_FILE_DISPLAY}: unsupported mixed legacy/v2 credentials format.`);
|
|
236
|
+
}
|
|
237
|
+
if (!isStoredCredentialsShape(parsed)) {
|
|
238
|
+
throw new Error(`Failed to parse ${TOKEN_FILE_DISPLAY}: unsupported credentials format.`);
|
|
239
|
+
}
|
|
240
|
+
if (parsed.version !== undefined && parsed.version !== 2) {
|
|
241
|
+
throw new Error(`Failed to parse ${TOKEN_FILE_DISPLAY}: unsupported credentials version ${String(parsed.version)}.`);
|
|
242
|
+
}
|
|
243
|
+
return normalizeStore(parsed);
|
|
244
|
+
}
|
|
245
|
+
function repoKey(owner, repo) {
|
|
246
|
+
if (!owner || !repo)
|
|
247
|
+
return null;
|
|
248
|
+
return `${owner}/${repo}`;
|
|
249
|
+
}
|
|
250
|
+
export async function saveAuthenticatedToken(token) {
|
|
251
|
+
const verification = await verifyGitHubToken(token);
|
|
252
|
+
if (verification.status === 'invalid') {
|
|
253
|
+
throw new Error('GitHub token expired or invalid. Run `haystack login` again.');
|
|
254
|
+
}
|
|
255
|
+
if (verification.status === 'error') {
|
|
256
|
+
throw new Error(`Failed to verify token: ${verification.message}`);
|
|
257
|
+
}
|
|
258
|
+
const user = verification.user;
|
|
259
|
+
const store = await loadCredentialsStore();
|
|
260
|
+
store.accounts[user.login] = {
|
|
261
|
+
github_token: token,
|
|
262
|
+
created_at: new Date().toISOString(),
|
|
263
|
+
};
|
|
264
|
+
store.activeLogin = user.login;
|
|
265
|
+
await saveStore(store);
|
|
266
|
+
return user;
|
|
267
|
+
}
|
|
268
|
+
export async function listStoredAccounts() {
|
|
269
|
+
const store = await loadCredentialsStore();
|
|
270
|
+
const repoDefaultCounts = new Map();
|
|
271
|
+
for (const login of Object.values(store.repoDefaults)) {
|
|
272
|
+
repoDefaultCounts.set(login, (repoDefaultCounts.get(login) ?? 0) + 1);
|
|
273
|
+
}
|
|
274
|
+
return Object.entries(store.accounts)
|
|
275
|
+
.map(([login, account]) => ({
|
|
276
|
+
login,
|
|
277
|
+
createdAt: account.created_at,
|
|
278
|
+
isActive: store.activeLogin === login,
|
|
279
|
+
repoDefaultCount: repoDefaultCounts.get(login) ?? 0,
|
|
280
|
+
}))
|
|
281
|
+
.sort((a, b) => {
|
|
282
|
+
if (a.isActive !== b.isActive)
|
|
283
|
+
return a.isActive ? -1 : 1;
|
|
284
|
+
return a.login.localeCompare(b.login);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
export async function setActiveAccount(login) {
|
|
288
|
+
const store = await loadCredentialsStore();
|
|
289
|
+
const storedLogin = findStoredLogin(store.accounts, login);
|
|
290
|
+
if (!storedLogin) {
|
|
291
|
+
throw new Error(`No saved Haystack account for ${login}.`);
|
|
292
|
+
}
|
|
293
|
+
store.activeLogin = storedLogin;
|
|
294
|
+
await saveStore(store);
|
|
295
|
+
}
|
|
296
|
+
export async function removeAccount(login) {
|
|
297
|
+
const store = await loadCredentialsStore();
|
|
298
|
+
const targetLogin = login ? findStoredLogin(store.accounts, login) : store.activeLogin;
|
|
299
|
+
if (login && !targetLogin) {
|
|
300
|
+
throw new Error(`No saved Haystack account for ${login}.`);
|
|
301
|
+
}
|
|
302
|
+
if (!targetLogin || !store.accounts[targetLogin]) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
delete store.accounts[targetLogin];
|
|
306
|
+
for (const [repoFullName, mappedLogin] of Object.entries(store.repoDefaults)) {
|
|
307
|
+
if (mappedLogin === targetLogin) {
|
|
308
|
+
delete store.repoDefaults[repoFullName];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (store.activeLogin === targetLogin) {
|
|
312
|
+
store.activeLogin = Object.keys(store.accounts)[0] ?? null;
|
|
313
|
+
}
|
|
314
|
+
await saveStore(store);
|
|
315
|
+
return targetLogin;
|
|
316
|
+
}
|
|
317
|
+
export async function getActiveLogin() {
|
|
318
|
+
const store = await loadCredentialsStore();
|
|
319
|
+
return store.activeLogin;
|
|
320
|
+
}
|
|
321
|
+
export async function getRepoDefaultAccount(owner, repo) {
|
|
322
|
+
const store = await loadCredentialsStore();
|
|
323
|
+
const key = repoKey(owner, repo);
|
|
324
|
+
if (!key)
|
|
325
|
+
return null;
|
|
326
|
+
return store.repoDefaults[key] ?? null;
|
|
327
|
+
}
|
|
328
|
+
export async function setRepoDefaultAccount(owner, repo, login) {
|
|
329
|
+
const store = await loadCredentialsStore();
|
|
330
|
+
const storedLogin = findStoredLogin(store.accounts, login);
|
|
331
|
+
if (!storedLogin) {
|
|
332
|
+
throw new Error(`No saved Haystack account for ${login}.`);
|
|
333
|
+
}
|
|
334
|
+
const key = repoKey(owner, repo);
|
|
335
|
+
if (!key)
|
|
336
|
+
return;
|
|
337
|
+
store.repoDefaults[key] = storedLogin;
|
|
338
|
+
await saveStore(store);
|
|
339
|
+
}
|
|
340
|
+
export async function resolveAuthContext(options = {}) {
|
|
341
|
+
const store = await loadCredentialsStore();
|
|
342
|
+
const { preferredLogin, owner, repo } = options;
|
|
343
|
+
if (preferredLogin) {
|
|
344
|
+
const storedLogin = findStoredLogin(store.accounts, preferredLogin);
|
|
345
|
+
if (!storedLogin) {
|
|
346
|
+
throw new Error(`No saved Haystack account for ${preferredLogin}. Run \`haystack login\` to add it or \`haystack auth list\` to inspect saved accounts.`);
|
|
347
|
+
}
|
|
348
|
+
const account = store.accounts[storedLogin];
|
|
349
|
+
return {
|
|
350
|
+
login: storedLogin,
|
|
351
|
+
token: account.github_token,
|
|
352
|
+
source: 'preferred',
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const repoFullName = repoKey(owner, repo);
|
|
356
|
+
if (repoFullName) {
|
|
357
|
+
const repoLogin = store.repoDefaults[repoFullName];
|
|
358
|
+
if (repoLogin && store.accounts[repoLogin]) {
|
|
359
|
+
return {
|
|
360
|
+
login: repoLogin,
|
|
361
|
+
token: store.accounts[repoLogin].github_token,
|
|
362
|
+
source: 'repo-default',
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (store.activeLogin && store.accounts[store.activeLogin]) {
|
|
367
|
+
return {
|
|
368
|
+
login: store.activeLogin,
|
|
369
|
+
token: store.accounts[store.activeLogin].github_token,
|
|
370
|
+
source: 'active',
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
throw new Error('Not logged in. Run `haystack login` first.');
|
|
374
|
+
}
|
|
375
|
+
export async function loadToken(options = {}) {
|
|
376
|
+
try {
|
|
377
|
+
const context = await resolveAuthContext(options);
|
|
378
|
+
return context.token;
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
if (error instanceof Error && error.message.startsWith('Not logged in.')) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -11,6 +11,7 @@ export interface CreatePROptions {
|
|
|
11
11
|
base: string;
|
|
12
12
|
body?: string;
|
|
13
13
|
draft?: boolean;
|
|
14
|
+
token?: string;
|
|
14
15
|
}
|
|
15
16
|
export interface PRResponse {
|
|
16
17
|
number: number;
|
|
@@ -29,11 +30,33 @@ export interface LabelInfo {
|
|
|
29
30
|
color: string;
|
|
30
31
|
description?: string;
|
|
31
32
|
}
|
|
33
|
+
export interface GitHubUser {
|
|
34
|
+
login: string;
|
|
35
|
+
id: number;
|
|
36
|
+
}
|
|
37
|
+
export interface RepositoryPermissions {
|
|
38
|
+
admin?: boolean;
|
|
39
|
+
maintain?: boolean;
|
|
40
|
+
push?: boolean;
|
|
41
|
+
triage?: boolean;
|
|
42
|
+
pull?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface RepositoryAccess {
|
|
45
|
+
full_name: string;
|
|
46
|
+
owner: {
|
|
47
|
+
login: string;
|
|
48
|
+
};
|
|
49
|
+
permissions?: RepositoryPermissions;
|
|
50
|
+
}
|
|
32
51
|
export declare const HAYSTACK_LABELS: Record<string, LabelInfo>;
|
|
33
52
|
/**
|
|
34
|
-
* Get
|
|
53
|
+
* Get a GitHub token, throw if not authenticated
|
|
35
54
|
*/
|
|
36
|
-
export declare function requireGitHubAuth(
|
|
55
|
+
export declare function requireGitHubAuth(options?: {
|
|
56
|
+
preferredLogin?: string;
|
|
57
|
+
owner?: string;
|
|
58
|
+
repo?: string;
|
|
59
|
+
}): Promise<string>;
|
|
37
60
|
/**
|
|
38
61
|
* Create a pull request
|
|
39
62
|
*/
|
|
@@ -41,43 +64,41 @@ export declare function createPullRequest(options: CreatePROptions): Promise<PRR
|
|
|
41
64
|
/**
|
|
42
65
|
* Check if a PR already exists for a branch
|
|
43
66
|
*/
|
|
44
|
-
export declare function findExistingPR(owner: string, repo: string, head: string): Promise<PRResponse | null>;
|
|
67
|
+
export declare function findExistingPR(owner: string, repo: string, head: string, token?: string): Promise<PRResponse | null>;
|
|
45
68
|
/**
|
|
46
69
|
* Add labels to an issue/PR
|
|
47
70
|
*/
|
|
48
|
-
export declare function addLabelsToIssue(owner: string, repo: string, issueNumber: number, labels: string[]): Promise<void>;
|
|
71
|
+
export declare function addLabelsToIssue(owner: string, repo: string, issueNumber: number, labels: string[], token?: string): Promise<void>;
|
|
49
72
|
/**
|
|
50
73
|
* Get all labels from an issue/PR
|
|
51
74
|
*/
|
|
52
|
-
export declare function getLabelsFromIssue(owner: string, repo: string, issueNumber: number): Promise<string[]>;
|
|
75
|
+
export declare function getLabelsFromIssue(owner: string, repo: string, issueNumber: number, token?: string): Promise<string[]>;
|
|
53
76
|
/**
|
|
54
77
|
* Remove a label from an issue/PR
|
|
55
78
|
*/
|
|
56
|
-
export declare function removeLabelFromIssue(owner: string, repo: string, issueNumber: number, labelName: string): Promise<void>;
|
|
79
|
+
export declare function removeLabelFromIssue(owner: string, repo: string, issueNumber: number, labelName: string, token?: string): Promise<void>;
|
|
57
80
|
/**
|
|
58
81
|
* Check if a label exists in the repo
|
|
59
82
|
*/
|
|
60
|
-
export declare function labelExists(owner: string, repo: string, labelName: string): Promise<boolean>;
|
|
83
|
+
export declare function labelExists(owner: string, repo: string, labelName: string, token?: string): Promise<boolean>;
|
|
61
84
|
/**
|
|
62
85
|
* Create a label in the repo
|
|
63
86
|
*/
|
|
64
|
-
export declare function createLabel(owner: string, repo: string, label: LabelInfo): Promise<void>;
|
|
87
|
+
export declare function createLabel(owner: string, repo: string, label: LabelInfo, token?: string): Promise<void>;
|
|
65
88
|
/**
|
|
66
89
|
* Ensure all required Haystack labels exist in the repo
|
|
67
90
|
*/
|
|
68
|
-
export declare function ensureHaystackLabels(owner: string, repo: string, labelNames: string[]): Promise<void>;
|
|
91
|
+
export declare function ensureHaystackLabels(owner: string, repo: string, labelNames: string[], token?: string): Promise<void>;
|
|
69
92
|
/**
|
|
70
93
|
* Request reviewers on a pull request
|
|
71
94
|
*/
|
|
72
|
-
export declare function requestReviewers(owner: string, repo: string, prNumber: number, reviewers: string[]): Promise<void>;
|
|
95
|
+
export declare function requestReviewers(owner: string, repo: string, prNumber: number, reviewers: string[], token?: string): Promise<void>;
|
|
73
96
|
/**
|
|
74
97
|
* Get the authenticated user's info
|
|
75
98
|
*/
|
|
76
|
-
export declare function getCurrentUser(): Promise<
|
|
77
|
-
|
|
78
|
-
id: number;
|
|
79
|
-
}>;
|
|
99
|
+
export declare function getCurrentUser(token?: string): Promise<GitHubUser>;
|
|
100
|
+
export declare function getRepositoryAccess(owner: string, repo: string, token?: string): Promise<RepositoryAccess>;
|
|
80
101
|
/**
|
|
81
102
|
* Check if user has push access to a repo
|
|
82
103
|
*/
|
|
83
|
-
export declare function hasPushAccess(owner: string, repo: string): Promise<boolean>;
|
|
104
|
+
export declare function hasPushAccess(owner: string, repo: string, token?: string): Promise<boolean>;
|
package/dist/utils/github-api.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides functions for creating PRs, managing labels, etc.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { resolveAuthContext } from './auth.js';
|
|
7
7
|
const GITHUB_API_BASE = 'https://api.github.com';
|
|
8
8
|
// Label definitions for Haystack workflow
|
|
9
9
|
export const HAYSTACK_LABELS = {
|
|
@@ -27,14 +27,14 @@ export const HAYSTACK_LABELS = {
|
|
|
27
27
|
// Authentication
|
|
28
28
|
// ============================================================================
|
|
29
29
|
/**
|
|
30
|
-
* Get
|
|
30
|
+
* Get a GitHub token, throw if not authenticated
|
|
31
31
|
*/
|
|
32
|
-
export async function requireGitHubAuth() {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return token;
|
|
32
|
+
export async function requireGitHubAuth(options) {
|
|
33
|
+
const context = await resolveAuthContext(options);
|
|
34
|
+
return context.token;
|
|
35
|
+
}
|
|
36
|
+
async function resolveToken(token, options) {
|
|
37
|
+
return token ?? requireGitHubAuth(options);
|
|
38
38
|
}
|
|
39
39
|
// ============================================================================
|
|
40
40
|
// API helpers
|
|
@@ -88,7 +88,10 @@ async function handleApiError(response, context) {
|
|
|
88
88
|
* Create a pull request
|
|
89
89
|
*/
|
|
90
90
|
export async function createPullRequest(options) {
|
|
91
|
-
const token = await
|
|
91
|
+
const token = await resolveToken(options.token, {
|
|
92
|
+
owner: options.owner,
|
|
93
|
+
repo: options.repo,
|
|
94
|
+
});
|
|
92
95
|
const response = await githubApiRequest('POST', `/repos/${options.owner}/${options.repo}/pulls`, token, {
|
|
93
96
|
title: options.title,
|
|
94
97
|
head: options.head,
|
|
@@ -139,11 +142,11 @@ async function findMergedPRForBranch(owner, repo, head, token) {
|
|
|
139
142
|
/**
|
|
140
143
|
* Check if a PR already exists for a branch
|
|
141
144
|
*/
|
|
142
|
-
export async function findExistingPR(owner, repo, head) {
|
|
143
|
-
const
|
|
145
|
+
export async function findExistingPR(owner, repo, head, token) {
|
|
146
|
+
const authToken = await resolveToken(token, { owner, repo });
|
|
144
147
|
// head format should be "owner:branch" for cross-repo or just "branch" for same-repo
|
|
145
148
|
const headParam = head.includes(':') ? head : `${owner}:${head}`;
|
|
146
|
-
const response = await githubApiRequest('GET', `/repos/${owner}/${repo}/pulls?head=${encodeURIComponent(headParam)}&state=open`,
|
|
149
|
+
const response = await githubApiRequest('GET', `/repos/${owner}/${repo}/pulls?head=${encodeURIComponent(headParam)}&state=open`, authToken);
|
|
147
150
|
if (!response.ok) {
|
|
148
151
|
if (response.status === 404)
|
|
149
152
|
return null;
|
|
@@ -158,9 +161,9 @@ export async function findExistingPR(owner, repo, head) {
|
|
|
158
161
|
/**
|
|
159
162
|
* Add labels to an issue/PR
|
|
160
163
|
*/
|
|
161
|
-
export async function addLabelsToIssue(owner, repo, issueNumber, labels) {
|
|
162
|
-
const
|
|
163
|
-
const response = await githubApiRequest('POST', `/repos/${owner}/${repo}/issues/${issueNumber}/labels`,
|
|
164
|
+
export async function addLabelsToIssue(owner, repo, issueNumber, labels, token) {
|
|
165
|
+
const authToken = await resolveToken(token, { owner, repo });
|
|
166
|
+
const response = await githubApiRequest('POST', `/repos/${owner}/${repo}/issues/${issueNumber}/labels`, authToken, { labels });
|
|
164
167
|
if (!response.ok) {
|
|
165
168
|
await handleApiError(response, `adding labels to PR #${issueNumber}`);
|
|
166
169
|
}
|
|
@@ -168,9 +171,9 @@ export async function addLabelsToIssue(owner, repo, issueNumber, labels) {
|
|
|
168
171
|
/**
|
|
169
172
|
* Get all labels from an issue/PR
|
|
170
173
|
*/
|
|
171
|
-
export async function getLabelsFromIssue(owner, repo, issueNumber) {
|
|
172
|
-
const
|
|
173
|
-
const response = await githubApiRequest('GET', `/repos/${owner}/${repo}/issues/${issueNumber}/labels`,
|
|
174
|
+
export async function getLabelsFromIssue(owner, repo, issueNumber, token) {
|
|
175
|
+
const authToken = await resolveToken(token, { owner, repo });
|
|
176
|
+
const response = await githubApiRequest('GET', `/repos/${owner}/${repo}/issues/${issueNumber}/labels`, authToken);
|
|
174
177
|
if (!response.ok) {
|
|
175
178
|
await handleApiError(response, `getting labels from PR #${issueNumber}`);
|
|
176
179
|
}
|
|
@@ -180,9 +183,9 @@ export async function getLabelsFromIssue(owner, repo, issueNumber) {
|
|
|
180
183
|
/**
|
|
181
184
|
* Remove a label from an issue/PR
|
|
182
185
|
*/
|
|
183
|
-
export async function removeLabelFromIssue(owner, repo, issueNumber, labelName) {
|
|
184
|
-
const
|
|
185
|
-
const response = await githubApiRequest('DELETE', `/repos/${owner}/${repo}/issues/${issueNumber}/labels/${encodeURIComponent(labelName)}`,
|
|
186
|
+
export async function removeLabelFromIssue(owner, repo, issueNumber, labelName, token) {
|
|
187
|
+
const authToken = await resolveToken(token, { owner, repo });
|
|
188
|
+
const response = await githubApiRequest('DELETE', `/repos/${owner}/${repo}/issues/${issueNumber}/labels/${encodeURIComponent(labelName)}`, authToken);
|
|
186
189
|
// 404 is fine - label wasn't on the issue
|
|
187
190
|
if (!response.ok && response.status !== 404) {
|
|
188
191
|
await handleApiError(response, `removing label '${labelName}' from PR #${issueNumber}`);
|
|
@@ -191,17 +194,17 @@ export async function removeLabelFromIssue(owner, repo, issueNumber, labelName)
|
|
|
191
194
|
/**
|
|
192
195
|
* Check if a label exists in the repo
|
|
193
196
|
*/
|
|
194
|
-
export async function labelExists(owner, repo, labelName) {
|
|
195
|
-
const
|
|
196
|
-
const response = await githubApiRequest('GET', `/repos/${owner}/${repo}/labels/${encodeURIComponent(labelName)}`,
|
|
197
|
+
export async function labelExists(owner, repo, labelName, token) {
|
|
198
|
+
const authToken = await resolveToken(token, { owner, repo });
|
|
199
|
+
const response = await githubApiRequest('GET', `/repos/${owner}/${repo}/labels/${encodeURIComponent(labelName)}`, authToken);
|
|
197
200
|
return response.ok;
|
|
198
201
|
}
|
|
199
202
|
/**
|
|
200
203
|
* Create a label in the repo
|
|
201
204
|
*/
|
|
202
|
-
export async function createLabel(owner, repo, label) {
|
|
203
|
-
const
|
|
204
|
-
const response = await githubApiRequest('POST', `/repos/${owner}/${repo}/labels`,
|
|
205
|
+
export async function createLabel(owner, repo, label, token) {
|
|
206
|
+
const authToken = await resolveToken(token, { owner, repo });
|
|
207
|
+
const response = await githubApiRequest('POST', `/repos/${owner}/${repo}/labels`, authToken, {
|
|
205
208
|
name: label.name,
|
|
206
209
|
color: label.color,
|
|
207
210
|
description: label.description,
|
|
@@ -214,14 +217,14 @@ export async function createLabel(owner, repo, label) {
|
|
|
214
217
|
/**
|
|
215
218
|
* Ensure all required Haystack labels exist in the repo
|
|
216
219
|
*/
|
|
217
|
-
export async function ensureHaystackLabels(owner, repo, labelNames) {
|
|
220
|
+
export async function ensureHaystackLabels(owner, repo, labelNames, token) {
|
|
218
221
|
for (const name of labelNames) {
|
|
219
222
|
const labelInfo = HAYSTACK_LABELS[name];
|
|
220
223
|
if (!labelInfo)
|
|
221
224
|
continue;
|
|
222
|
-
const exists = await labelExists(owner, repo, name);
|
|
225
|
+
const exists = await labelExists(owner, repo, name, token);
|
|
223
226
|
if (!exists) {
|
|
224
|
-
await createLabel(owner, repo, labelInfo);
|
|
227
|
+
await createLabel(owner, repo, labelInfo, token);
|
|
225
228
|
}
|
|
226
229
|
}
|
|
227
230
|
}
|
|
@@ -231,9 +234,9 @@ export async function ensureHaystackLabels(owner, repo, labelNames) {
|
|
|
231
234
|
/**
|
|
232
235
|
* Request reviewers on a pull request
|
|
233
236
|
*/
|
|
234
|
-
export async function requestReviewers(owner, repo, prNumber, reviewers) {
|
|
235
|
-
const
|
|
236
|
-
const response = await githubApiRequest('POST', `/repos/${owner}/${repo}/pulls/${prNumber}/requested_reviewers`,
|
|
237
|
+
export async function requestReviewers(owner, repo, prNumber, reviewers, token) {
|
|
238
|
+
const authToken = await resolveToken(token, { owner, repo });
|
|
239
|
+
const response = await githubApiRequest('POST', `/repos/${owner}/${repo}/pulls/${prNumber}/requested_reviewers`, authToken, { reviewers });
|
|
237
240
|
if (!response.ok) {
|
|
238
241
|
await handleApiError(response, `requesting reviewers on PR #${prNumber}`);
|
|
239
242
|
}
|
|
@@ -244,23 +247,28 @@ export async function requestReviewers(owner, repo, prNumber, reviewers) {
|
|
|
244
247
|
/**
|
|
245
248
|
* Get the authenticated user's info
|
|
246
249
|
*/
|
|
247
|
-
export async function getCurrentUser() {
|
|
248
|
-
const
|
|
249
|
-
const response = await githubApiRequest('GET', '/user',
|
|
250
|
+
export async function getCurrentUser(token) {
|
|
251
|
+
const authToken = await resolveToken(token);
|
|
252
|
+
const response = await githubApiRequest('GET', '/user', authToken);
|
|
250
253
|
if (!response.ok) {
|
|
251
254
|
await handleApiError(response, 'getting current user');
|
|
252
255
|
}
|
|
253
256
|
return response.json();
|
|
254
257
|
}
|
|
258
|
+
export async function getRepositoryAccess(owner, repo, token) {
|
|
259
|
+
const authToken = await resolveToken(token, { owner, repo });
|
|
260
|
+
const response = await githubApiRequest('GET', `/repos/${owner}/${repo}`, authToken);
|
|
261
|
+
if (!response.ok) {
|
|
262
|
+
await handleApiError(response, `getting repository access for ${owner}/${repo}`);
|
|
263
|
+
}
|
|
264
|
+
return response.json();
|
|
265
|
+
}
|
|
255
266
|
/**
|
|
256
267
|
* Check if user has push access to a repo
|
|
257
268
|
*/
|
|
258
|
-
export async function hasPushAccess(owner, repo) {
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
const data = await response.json();
|
|
265
|
-
return data.permissions?.push === true;
|
|
269
|
+
export async function hasPushAccess(owner, repo, token) {
|
|
270
|
+
const data = await getRepositoryAccess(owner, repo, token);
|
|
271
|
+
return data.permissions?.admin === true
|
|
272
|
+
|| data.permissions?.maintain === true
|
|
273
|
+
|| data.permissions?.push === true;
|
|
266
274
|
}
|