@haystackeditor/cli 0.13.0 → 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/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 +1 -0
- package/dist/commands/submit.js +114 -18
- package/dist/commands/triage.js +12 -3
- package/dist/index.js +22 -5
- 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
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { Command } from 'commander';
|
|
23
23
|
import { statusCommand } from './commands/status.js';
|
|
24
24
|
import { initCommand } from './commands/init.js';
|
|
25
|
-
import { loginCommand, logoutCommand } from './commands/login.js';
|
|
25
|
+
import { authListCommand, authUseCommand, loginCommand, logoutCommand, whoamiCommand, } from './commands/login.js';
|
|
26
26
|
import { handleAgenticTool, handleAutoMerge, handleWaitForReviewers, isAutoMergeEnabled } from './commands/config.js';
|
|
27
27
|
import { installSkills, listSkills } from './commands/skills.js';
|
|
28
28
|
import { hooksInstall, hooksStatus, hooksUpdate } from './commands/hooks.js';
|
|
@@ -38,7 +38,7 @@ const program = new Command();
|
|
|
38
38
|
program
|
|
39
39
|
.name('haystack')
|
|
40
40
|
.description('Haystack CLI — automated PR review, triage, and merge queue')
|
|
41
|
-
.version('0.13.
|
|
41
|
+
.version('0.13.1');
|
|
42
42
|
program
|
|
43
43
|
.command('init')
|
|
44
44
|
.description('Create .haystack.json configuration')
|
|
@@ -78,15 +78,31 @@ program
|
|
|
78
78
|
.action(statusCommand);
|
|
79
79
|
program
|
|
80
80
|
.command('login')
|
|
81
|
-
.description('
|
|
81
|
+
.description('Add or refresh a saved GitHub account')
|
|
82
82
|
.action(loginCommand);
|
|
83
83
|
program
|
|
84
|
-
.command('logout')
|
|
85
|
-
.description('Remove
|
|
84
|
+
.command('logout [login]')
|
|
85
|
+
.description('Remove a saved GitHub account (defaults to the active account)')
|
|
86
86
|
.action(logoutCommand);
|
|
87
|
+
program
|
|
88
|
+
.command('whoami')
|
|
89
|
+
.description('Show the active Haystack GitHub account')
|
|
90
|
+
.action(whoamiCommand);
|
|
91
|
+
const authProgram = program
|
|
92
|
+
.command('auth')
|
|
93
|
+
.description('Manage saved Haystack GitHub accounts');
|
|
94
|
+
authProgram
|
|
95
|
+
.command('list')
|
|
96
|
+
.description('List saved Haystack GitHub accounts')
|
|
97
|
+
.action(authListCommand);
|
|
98
|
+
authProgram
|
|
99
|
+
.command('use <login>')
|
|
100
|
+
.description('Set the active Haystack GitHub account')
|
|
101
|
+
.action(authUseCommand);
|
|
87
102
|
program
|
|
88
103
|
.command('submit')
|
|
89
104
|
.description('Create a PR from current changes')
|
|
105
|
+
.option('--account <login>', 'Saved Haystack account login to use for GitHub API calls')
|
|
90
106
|
.option('--title <title>', 'PR title (default: last commit message)')
|
|
91
107
|
.option('--body <body>', 'PR body (default: commit messages since base)')
|
|
92
108
|
.option('--body-file <path>', 'Read PR body from file (use "-" for stdin)')
|
|
@@ -164,6 +180,7 @@ Examples:
|
|
|
164
180
|
haystack submit --draft # Create as draft PR
|
|
165
181
|
haystack submit --review # ⚠ Blocks auto-merge, needs human approval
|
|
166
182
|
haystack submit --review octocat # ⚠ Blocks auto-merge, requests review from octocat
|
|
183
|
+
haystack submit --account octocat # Use a specific saved Haystack account for this submit
|
|
167
184
|
haystack submit --max-turns 12 # Raise triage turn cap to 12 per checker
|
|
168
185
|
haystack submit --triage-timeout 300 # Raise triage wall-clock to 5 minutes
|
|
169
186
|
`)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface StoredAccount {
|
|
2
|
+
github_token: string;
|
|
3
|
+
created_at: string;
|
|
4
|
+
}
|
|
5
|
+
export interface CredentialsStore {
|
|
6
|
+
version: 2;
|
|
7
|
+
activeLogin: string | null;
|
|
8
|
+
accounts: Record<string, StoredAccount>;
|
|
9
|
+
repoDefaults: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
export interface GitHubUser {
|
|
12
|
+
login: string;
|
|
13
|
+
id: number;
|
|
14
|
+
}
|
|
15
|
+
export type VerifyGitHubTokenResult = {
|
|
16
|
+
status: 'ok';
|
|
17
|
+
user: GitHubUser;
|
|
18
|
+
} | {
|
|
19
|
+
status: 'invalid';
|
|
20
|
+
} | {
|
|
21
|
+
status: 'error';
|
|
22
|
+
message: string;
|
|
23
|
+
};
|
|
24
|
+
export interface ResolveAuthOptions {
|
|
25
|
+
preferredLogin?: string;
|
|
26
|
+
owner?: string;
|
|
27
|
+
repo?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ResolvedAuthContext {
|
|
30
|
+
login: string;
|
|
31
|
+
token: string;
|
|
32
|
+
source: 'preferred' | 'repo-default' | 'active';
|
|
33
|
+
}
|
|
34
|
+
export interface AccountSummary {
|
|
35
|
+
login: string;
|
|
36
|
+
createdAt: string;
|
|
37
|
+
isActive: boolean;
|
|
38
|
+
repoDefaultCount: number;
|
|
39
|
+
}
|
|
40
|
+
export declare function verifyGitHubToken(token: string): Promise<VerifyGitHubTokenResult>;
|
|
41
|
+
export declare function loadCredentialsStore(): Promise<CredentialsStore>;
|
|
42
|
+
export declare function saveAuthenticatedToken(token: string): Promise<GitHubUser>;
|
|
43
|
+
export declare function listStoredAccounts(): Promise<AccountSummary[]>;
|
|
44
|
+
export declare function setActiveAccount(login: string): Promise<void>;
|
|
45
|
+
export declare function removeAccount(login?: string): Promise<string | null>;
|
|
46
|
+
export declare function getActiveLogin(): Promise<string | null>;
|
|
47
|
+
export declare function getRepoDefaultAccount(owner: string, repo: string): Promise<string | null>;
|
|
48
|
+
export declare function setRepoDefaultAccount(owner: string, repo: string, login: string): Promise<void>;
|
|
49
|
+
export declare function resolveAuthContext(options?: ResolveAuthOptions): Promise<ResolvedAuthContext>;
|
|
50
|
+
export declare function loadToken(options?: ResolveAuthOptions): Promise<string | null>;
|
|
@@ -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>;
|