@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/commands/config.js
CHANGED
|
@@ -8,7 +8,7 @@ import chalk from 'chalk';
|
|
|
8
8
|
import { readFileSync, writeFileSync } from 'fs';
|
|
9
9
|
import { resolve } from 'path';
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
|
-
import {
|
|
11
|
+
import { resolveAuthContext } from '../utils/auth.js';
|
|
12
12
|
import { AI_REVIEWER_SOURCES, AI_REVIEWER_DISPLAY_NAMES, AI_REVIEWER_BOT_USERNAMES } from '../types.js';
|
|
13
13
|
const API_BASE = 'https://haystackeditor.com/api/preferences';
|
|
14
14
|
const AGENTIC_TOOL_LABELS = {
|
|
@@ -64,12 +64,14 @@ function setPreference(key, value) {
|
|
|
64
64
|
// API helpers (user-level preferences)
|
|
65
65
|
// ============================================================================
|
|
66
66
|
async function requireAuth() {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
try {
|
|
68
|
+
const authContext = await resolveAuthContext();
|
|
69
|
+
return authContext.token;
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
console.error(chalk.red(`\n${err.message}\n`));
|
|
70
73
|
process.exit(1);
|
|
71
74
|
}
|
|
72
|
-
return token;
|
|
73
75
|
}
|
|
74
76
|
async function apiRequest(method, token, body) {
|
|
75
77
|
return fetch(API_BASE, {
|
package/dist/commands/dismiss.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* https://github.com/owner/repo/pull/123 # GitHub URL
|
|
14
14
|
*/
|
|
15
15
|
import chalk from 'chalk';
|
|
16
|
-
import {
|
|
16
|
+
import { resolveAuthContext } from '../utils/auth.js';
|
|
17
17
|
import { parseRemoteUrl } from '../utils/git.js';
|
|
18
18
|
// ============================================================================
|
|
19
19
|
// PR identifier parsing (shared with triage — duplicated to avoid circular deps)
|
|
@@ -125,9 +125,12 @@ async function runOverride(identifier, type, commandName) {
|
|
|
125
125
|
process.exit(1);
|
|
126
126
|
}
|
|
127
127
|
// Require auth
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
let authContext;
|
|
129
|
+
try {
|
|
130
|
+
authContext = await resolveAuthContext({ owner: pr.owner, repo: pr.repo });
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
console.error(chalk.red(`\n${err.message}\n`));
|
|
131
134
|
process.exit(1);
|
|
132
135
|
}
|
|
133
136
|
const prLabel = `${pr.owner}/${pr.repo}#${pr.prNumber}`;
|
|
@@ -139,7 +142,7 @@ async function runOverride(identifier, type, commandName) {
|
|
|
139
142
|
// Get PR HEAD SHA
|
|
140
143
|
let headSha;
|
|
141
144
|
try {
|
|
142
|
-
headSha = await getPRHeadSha(pr.owner, pr.repo, pr.prNumber, token);
|
|
145
|
+
headSha = await getPRHeadSha(pr.owner, pr.repo, pr.prNumber, authContext.token);
|
|
143
146
|
}
|
|
144
147
|
catch (err) {
|
|
145
148
|
console.error(chalk.red(`\n${err.message}\n`));
|
|
@@ -147,7 +150,7 @@ async function runOverride(identifier, type, commandName) {
|
|
|
147
150
|
}
|
|
148
151
|
// Post the override
|
|
149
152
|
try {
|
|
150
|
-
await postUserOverride(repoFullName, pr.prNumber, headSha, type, token);
|
|
153
|
+
await postUserOverride(repoFullName, pr.prNumber, headSha, type, authContext.token);
|
|
151
154
|
}
|
|
152
155
|
catch (err) {
|
|
153
156
|
console.error(chalk.red(`\n${err.message}\n`));
|
|
@@ -182,16 +185,19 @@ export async function undismissCommand(identifier) {
|
|
|
182
185
|
console.error(chalk.red(`\n${err.message}\n`));
|
|
183
186
|
process.exit(1);
|
|
184
187
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
+
let authContext;
|
|
189
|
+
try {
|
|
190
|
+
authContext = await resolveAuthContext({ owner: pr.owner, repo: pr.repo });
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
console.error(chalk.red(`\n${err.message}\n`));
|
|
188
194
|
process.exit(1);
|
|
189
195
|
}
|
|
190
196
|
const prLabel = `${pr.owner}/${pr.repo}#${pr.prNumber}`;
|
|
191
197
|
const repoFullName = `${pr.owner}/${pr.repo}`;
|
|
192
198
|
console.log(chalk.dim(`\nClearing overrides for ${prLabel}...`));
|
|
193
199
|
try {
|
|
194
|
-
await deleteUserOverrides(repoFullName, pr.prNumber, token);
|
|
200
|
+
await deleteUserOverrides(repoFullName, pr.prNumber, authContext.token);
|
|
195
201
|
}
|
|
196
202
|
catch (err) {
|
|
197
203
|
console.error(chalk.red(`\n${err.message}\n`));
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Login command - GitHub OAuth device flow
|
|
3
3
|
*/
|
|
4
|
-
/**
|
|
5
|
-
* Load token from disk
|
|
6
|
-
*/
|
|
7
|
-
export declare function loadToken(): Promise<string | null>;
|
|
8
4
|
export declare function loginCommand(): Promise<void>;
|
|
9
|
-
export declare function logoutCommand(): Promise<void>;
|
|
5
|
+
export declare function logoutCommand(login?: string): Promise<void>;
|
|
6
|
+
export declare function whoamiCommand(): Promise<void>;
|
|
7
|
+
export declare function authListCommand(): Promise<void>;
|
|
8
|
+
export declare function authUseCommand(login: string): Promise<void>;
|
package/dist/commands/login.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Login command - GitHub OAuth device flow
|
|
3
3
|
*/
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
import * as fs from 'fs/promises';
|
|
6
5
|
import * as path from 'path';
|
|
7
6
|
import * as os from 'os';
|
|
7
|
+
import { getActiveLogin, getRepoDefaultAccount, listStoredAccounts, removeAccount, saveAuthenticatedToken, setActiveAccount, } from '../utils/auth.js';
|
|
8
|
+
import { parseRemoteUrl } from '../utils/git.js';
|
|
8
9
|
const GITHUB_CLIENT_ID = 'Ov23liW3JE38D3gZWa85'; // Haystack GitHub OAuth App
|
|
9
10
|
const CONFIG_DIR = path.join(os.homedir(), '.haystack');
|
|
10
|
-
const TOKEN_FILE = path.join(CONFIG_DIR, 'credentials.json');
|
|
11
11
|
/**
|
|
12
12
|
* Start device flow and get user code
|
|
13
13
|
*/
|
|
@@ -68,66 +68,15 @@ async function pollForToken(deviceCode, interval) {
|
|
|
68
68
|
throw new Error(`OAuth error: ${data.error}`);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
/**
|
|
72
|
-
* Save token to disk
|
|
73
|
-
*/
|
|
74
|
-
async function saveToken(token) {
|
|
75
|
-
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
76
|
-
const credentials = {
|
|
77
|
-
github_token: token,
|
|
78
|
-
created_at: new Date().toISOString(),
|
|
79
|
-
};
|
|
80
|
-
await fs.writeFile(TOKEN_FILE, JSON.stringify(credentials, null, 2), {
|
|
81
|
-
mode: 0o600, // Owner read/write only
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Load token from disk
|
|
86
|
-
*/
|
|
87
|
-
export async function loadToken() {
|
|
88
|
-
try {
|
|
89
|
-
const content = await fs.readFile(TOKEN_FILE, 'utf-8');
|
|
90
|
-
const credentials = JSON.parse(content);
|
|
91
|
-
return credentials.github_token;
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Verify token is valid by calling GitHub API
|
|
99
|
-
*/
|
|
100
|
-
async function verifyToken(token) {
|
|
101
|
-
try {
|
|
102
|
-
const response = await fetch('https://api.github.com/user', {
|
|
103
|
-
headers: {
|
|
104
|
-
'Authorization': `Bearer ${token}`,
|
|
105
|
-
'User-Agent': 'Haystack-CLI',
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
if (!response.ok) {
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
return response.json();
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
71
|
export async function loginCommand() {
|
|
118
|
-
console.log(chalk.bold('\nHaystack Login\n'));
|
|
119
|
-
// Check if already logged in
|
|
120
|
-
const existingToken = await loadToken();
|
|
121
|
-
if (existingToken) {
|
|
122
|
-
const user = await verifyToken(existingToken);
|
|
123
|
-
if (user) {
|
|
124
|
-
console.log(chalk.green(`Already logged in as ${chalk.bold(user.login)}`));
|
|
125
|
-
console.log(chalk.dim('Run `haystack logout` to sign out.\n'));
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
console.log('Authenticating with GitHub...\n');
|
|
130
72
|
try {
|
|
73
|
+
console.log(chalk.bold('\nHaystack Login\n'));
|
|
74
|
+
const activeLogin = await getActiveLogin();
|
|
75
|
+
if (activeLogin) {
|
|
76
|
+
console.log(chalk.dim(`Current active account: ${chalk.bold(activeLogin)}`));
|
|
77
|
+
console.log(chalk.dim('This login will add or refresh a saved account and make it active.\n'));
|
|
78
|
+
}
|
|
79
|
+
console.log('Authenticating with GitHub...\n');
|
|
131
80
|
// Start device flow
|
|
132
81
|
const deviceFlow = await startDeviceFlow();
|
|
133
82
|
console.log(chalk.yellow('Open this URL in your browser:\n'));
|
|
@@ -138,25 +87,100 @@ export async function loginCommand() {
|
|
|
138
87
|
// Poll for token
|
|
139
88
|
const token = await pollForToken(deviceFlow.device_code, deviceFlow.interval);
|
|
140
89
|
// Verify and save
|
|
141
|
-
const user = await
|
|
142
|
-
if (!user) {
|
|
143
|
-
throw new Error('Failed to verify token');
|
|
144
|
-
}
|
|
145
|
-
await saveToken(token);
|
|
90
|
+
const user = await saveAuthenticatedToken(token);
|
|
146
91
|
console.log(chalk.green(`\nLogged in as ${chalk.bold(user.login)}`));
|
|
147
|
-
console.log(chalk.dim(
|
|
92
|
+
console.log(chalk.dim(`Credentials saved to ${path.join(CONFIG_DIR, 'credentials.json')}\n`));
|
|
148
93
|
}
|
|
149
94
|
catch (error) {
|
|
150
95
|
console.error(chalk.red(`\nLogin failed: ${error.message}\n`));
|
|
151
96
|
process.exit(1);
|
|
152
97
|
}
|
|
153
98
|
}
|
|
154
|
-
export async function logoutCommand() {
|
|
99
|
+
export async function logoutCommand(login) {
|
|
155
100
|
try {
|
|
156
|
-
await
|
|
157
|
-
|
|
101
|
+
const removedLogin = await removeAccount(login);
|
|
102
|
+
if (!removedLogin) {
|
|
103
|
+
console.log(chalk.yellow('\nNot logged in.\n'));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
console.log(chalk.green(`\nRemoved saved account ${chalk.bold(removedLogin)}.`));
|
|
107
|
+
const nextActive = await getActiveLogin();
|
|
108
|
+
if (nextActive) {
|
|
109
|
+
console.log(chalk.dim(`Active account is now ${chalk.bold(nextActive)}.\n`));
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.log(chalk.dim('No saved Haystack accounts remain.\n'));
|
|
113
|
+
}
|
|
158
114
|
}
|
|
159
|
-
catch {
|
|
160
|
-
console.
|
|
115
|
+
catch (error) {
|
|
116
|
+
console.error(chalk.red(`\n${error.message}\n`));
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export async function whoamiCommand() {
|
|
121
|
+
try {
|
|
122
|
+
const accounts = await listStoredAccounts();
|
|
123
|
+
if (accounts.length === 0) {
|
|
124
|
+
console.log(chalk.yellow('\nNot logged in.\n'));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const active = accounts.find((account) => account.isActive);
|
|
128
|
+
if (!active) {
|
|
129
|
+
console.log(chalk.yellow('\nNo active Haystack account is configured.\n'));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
console.log(chalk.bold('\nHaystack Account\n'));
|
|
133
|
+
console.log(` ${chalk.dim('Active:')} ${chalk.bold(active.login)}`);
|
|
134
|
+
console.log(` ${chalk.dim('Saved:')} ${accounts.length} account(s)`);
|
|
135
|
+
let remote = null;
|
|
136
|
+
try {
|
|
137
|
+
remote = parseRemoteUrl('origin');
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
remote = null;
|
|
141
|
+
}
|
|
142
|
+
if (remote) {
|
|
143
|
+
const repoDefault = await getRepoDefaultAccount(remote.owner, remote.repo);
|
|
144
|
+
if (repoDefault) {
|
|
145
|
+
console.log(` ${chalk.dim('Repo:')} ${remote.owner}/${remote.repo} -> ${repoDefault}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
console.log('');
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
console.error(chalk.red(`\n${error.message}\n`));
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export async function authListCommand() {
|
|
156
|
+
try {
|
|
157
|
+
const accounts = await listStoredAccounts();
|
|
158
|
+
if (accounts.length === 0) {
|
|
159
|
+
console.log(chalk.yellow('\nNo saved Haystack accounts.\n'));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
console.log(chalk.bold('\nSaved Haystack Accounts\n'));
|
|
163
|
+
for (const account of accounts) {
|
|
164
|
+
const marker = account.isActive ? chalk.green('✓') : ' ';
|
|
165
|
+
const repoDefaultNote = account.repoDefaultCount > 0
|
|
166
|
+
? chalk.dim(` (${account.repoDefaultCount} repo default${account.repoDefaultCount === 1 ? '' : 's'})`)
|
|
167
|
+
: '';
|
|
168
|
+
console.log(` ${marker} ${chalk.bold(account.login)}${repoDefaultNote}`);
|
|
169
|
+
}
|
|
170
|
+
console.log('');
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
console.error(chalk.red(`\n${error.message}\n`));
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export async function authUseCommand(login) {
|
|
178
|
+
try {
|
|
179
|
+
await setActiveAccount(login);
|
|
180
|
+
console.log(chalk.green(`\nActive Haystack account set to ${chalk.bold(login)}.\n`));
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
console.error(chalk.red(`\n${error.message}\n`));
|
|
184
|
+
process.exit(1);
|
|
161
185
|
}
|
|
162
186
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* https://github.com/owner/repo/pull/123 # GitHub URL
|
|
12
12
|
*/
|
|
13
13
|
import chalk from 'chalk';
|
|
14
|
-
import {
|
|
14
|
+
import { resolveAuthContext } from '../utils/auth.js';
|
|
15
15
|
import { parseRemoteUrl } from '../utils/git.js';
|
|
16
16
|
// ============================================================================
|
|
17
17
|
// PR identifier parsing
|
|
@@ -120,11 +120,15 @@ export async function prStatusCommand(identifier, options) {
|
|
|
120
120
|
console.error(chalk.red(`\n${err.message}\n`));
|
|
121
121
|
process.exit(1);
|
|
122
122
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
let authContext;
|
|
124
|
+
try {
|
|
125
|
+
authContext = await resolveAuthContext({ owner: pr.owner, repo: pr.repo });
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
console.error(chalk.red(`\n${err.message}\n`));
|
|
126
129
|
process.exit(1);
|
|
127
130
|
}
|
|
131
|
+
const token = authContext.token;
|
|
128
132
|
const prLabel = `${pr.owner}/${pr.repo}#${pr.prNumber}`;
|
|
129
133
|
if (!options.json) {
|
|
130
134
|
console.log(chalk.dim(`\nFetching status for ${prLabel}...\n`));
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* https://github.com/owner/repo/pull/123 # GitHub URL
|
|
14
14
|
*/
|
|
15
15
|
import chalk from 'chalk';
|
|
16
|
-
import {
|
|
16
|
+
import { resolveAuthContext } from '../utils/auth.js';
|
|
17
17
|
import { parseRemoteUrl } from '../utils/git.js';
|
|
18
18
|
import { ensureHaystackLabels, addLabelsToIssue, requestReviewers, } from '../utils/github-api.js';
|
|
19
19
|
// ============================================================================
|
|
@@ -59,18 +59,20 @@ export async function requestReviewCommand(identifier, reviewer) {
|
|
|
59
59
|
console.error(chalk.red(`\n${err.message}\n`));
|
|
60
60
|
process.exit(1);
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
let authContext;
|
|
63
|
+
try {
|
|
64
|
+
authContext = await resolveAuthContext({ owner: pr.owner, repo: pr.repo });
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
console.error(chalk.red(`\n${err.message}\n`));
|
|
66
68
|
process.exit(1);
|
|
67
69
|
}
|
|
68
70
|
const prLabel = `${pr.owner}/${pr.repo}#${pr.prNumber}`;
|
|
69
71
|
console.log(chalk.dim(`\nRequesting review for ${prLabel}...`));
|
|
70
72
|
// Ensure the label exists in the repo, then add it to the PR
|
|
71
73
|
try {
|
|
72
|
-
await ensureHaystackLabels(pr.owner, pr.repo, ['haystack:needs-review']);
|
|
73
|
-
await addLabelsToIssue(pr.owner, pr.repo, pr.prNumber, ['haystack:needs-review']);
|
|
74
|
+
await ensureHaystackLabels(pr.owner, pr.repo, ['haystack:needs-review'], authContext.token);
|
|
75
|
+
await addLabelsToIssue(pr.owner, pr.repo, pr.prNumber, ['haystack:needs-review'], authContext.token);
|
|
74
76
|
}
|
|
75
77
|
catch (err) {
|
|
76
78
|
console.error(chalk.red(`\nFailed to add label: ${err.message}\n`));
|
|
@@ -79,7 +81,7 @@ export async function requestReviewCommand(identifier, reviewer) {
|
|
|
79
81
|
// Request specific reviewer if provided
|
|
80
82
|
if (reviewer) {
|
|
81
83
|
try {
|
|
82
|
-
await requestReviewers(pr.owner, pr.repo, pr.prNumber, [reviewer]);
|
|
84
|
+
await requestReviewers(pr.owner, pr.repo, pr.prNumber, [reviewer], authContext.token);
|
|
83
85
|
console.log(chalk.green(`\n Review requested from ${reviewer} on ${prLabel}`));
|
|
84
86
|
}
|
|
85
87
|
catch (err) {
|
package/dist/commands/setup.js
CHANGED
|
@@ -16,7 +16,7 @@ import inquirer from 'inquirer';
|
|
|
16
16
|
import { execFileSync, execSync } from 'child_process';
|
|
17
17
|
import { mkdirSync } from 'fs';
|
|
18
18
|
import { basename, join } from 'path';
|
|
19
|
-
import {
|
|
19
|
+
import { resolveAuthContext } from '../utils/auth.js';
|
|
20
20
|
import { hooksInstall } from './hooks.js';
|
|
21
21
|
import { findGitRoot } from '../utils/hooks.js';
|
|
22
22
|
import { trackError, trackSetupEvent } from '../utils/telemetry.js';
|
|
@@ -473,11 +473,15 @@ async function stepConfirm(selectedRepos, rules, signals, policies, token) {
|
|
|
473
473
|
export async function setupCommand() {
|
|
474
474
|
console.log(chalk.cyan('\n Haystack Setup Wizard\n'));
|
|
475
475
|
// Step 0: Check login
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
476
|
+
let authContext;
|
|
477
|
+
try {
|
|
478
|
+
authContext = await resolveAuthContext();
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
481
|
+
console.log(chalk.yellow(` ${err.message}\n`));
|
|
479
482
|
process.exit(1);
|
|
480
483
|
}
|
|
484
|
+
const token = authContext.token;
|
|
481
485
|
// Verify token
|
|
482
486
|
const userResponse = await fetch(`${GITHUB_API}/user`, {
|
|
483
487
|
headers: {
|
package/dist/commands/submit.js
CHANGED
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
import chalk from 'chalk';
|
|
13
13
|
import fs from 'fs';
|
|
14
14
|
import { findGitRoot, getCurrentBranch, getDefaultBranch, hasUncommittedChanges, parseRemoteUrl, pushBranch, getLastCommitMessage, getCommitMessagesSinceBase, } from '../utils/git.js';
|
|
15
|
-
import { createPullRequest, findExistingPR,
|
|
15
|
+
import { createPullRequest, findExistingPR, getRepositoryAccess, requestReviewers, } from '../utils/github-api.js';
|
|
16
16
|
import { runTriage } from '../triage/runner.js';
|
|
17
17
|
import { loadConfig } from '../utils/config.js';
|
|
18
18
|
import { checkAnalysisReady, fetchAnalysisResults } from '../utils/analysis-api.js';
|
|
19
19
|
import { savePendingSubmit, updatePendingSubmit } from '../utils/pending-state.js';
|
|
20
20
|
import { ensureHaystackLabels, addLabelsToIssue, } from '../utils/github-api.js';
|
|
21
|
+
import { listStoredAccounts, resolveAuthContext, setRepoDefaultAccount, } from '../utils/auth.js';
|
|
21
22
|
// ============================================================================
|
|
22
23
|
// Helpers
|
|
23
24
|
// ============================================================================
|
|
@@ -28,6 +29,87 @@ function buildPRBody(commitMessages) {
|
|
|
28
29
|
---
|
|
29
30
|
*Created via [Haystack CLI](https://haystackeditor.com)*`;
|
|
30
31
|
}
|
|
32
|
+
function canCreateSameRepoPullRequest(permissions) {
|
|
33
|
+
return permissions?.admin === true
|
|
34
|
+
|| permissions?.maintain === true
|
|
35
|
+
|| permissions?.push === true;
|
|
36
|
+
}
|
|
37
|
+
function buildSubmitPermissionMessage(activeLogin, owner, repo, detail) {
|
|
38
|
+
const repoName = `${owner}/${repo}`;
|
|
39
|
+
const lines = [
|
|
40
|
+
`Haystack is authenticated as ${activeLogin}, but that account cannot create a same-repo pull request in ${repoName}.`,
|
|
41
|
+
'Use `haystack submit --account <login>` or `haystack auth use <login>` to select a different saved account.',
|
|
42
|
+
];
|
|
43
|
+
if (detail) {
|
|
44
|
+
lines.push(detail);
|
|
45
|
+
}
|
|
46
|
+
lines.push(`If the correct account is not saved yet, run \`haystack login\` with a GitHub account that has collaborator access to ${repoName}, then retry \`haystack submit\`.`);
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
function formatSubmitPermissionError(error, activeLogin, owner, repo) {
|
|
50
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
51
|
+
const lowerMessage = message.toLowerCase();
|
|
52
|
+
const shouldWrap = lowerMessage.includes('must be a collaborator')
|
|
53
|
+
|| lowerMessage.includes('permission denied')
|
|
54
|
+
|| message.startsWith('Not found: getting repository access');
|
|
55
|
+
if (!shouldWrap) {
|
|
56
|
+
return message;
|
|
57
|
+
}
|
|
58
|
+
return buildSubmitPermissionMessage(activeLogin, owner, repo, `GitHub said: ${message}`);
|
|
59
|
+
}
|
|
60
|
+
async function checkSubmitAccess(owner, repo, authContext) {
|
|
61
|
+
try {
|
|
62
|
+
const repoAccess = await getRepositoryAccess(owner, repo, authContext.token);
|
|
63
|
+
return { repoAccess };
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return { error };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function ambiguousAccountMessage(owner, repo, logins) {
|
|
70
|
+
return [
|
|
71
|
+
`Multiple saved Haystack accounts can create a same-repo pull request in ${owner}/${repo}: ${logins.join(', ')}.`,
|
|
72
|
+
'Re-run `haystack submit --account <login>` or `haystack auth use <login>` to choose one.',
|
|
73
|
+
].join('\n');
|
|
74
|
+
}
|
|
75
|
+
async function resolveSubmitAuthContext(owner, repo, preferredLogin) {
|
|
76
|
+
const primaryContext = await resolveAuthContext({ preferredLogin, owner, repo });
|
|
77
|
+
const primaryResult = await checkSubmitAccess(owner, repo, primaryContext);
|
|
78
|
+
if (primaryResult.repoAccess && canCreateSameRepoPullRequest(primaryResult.repoAccess.permissions)) {
|
|
79
|
+
return {
|
|
80
|
+
authContext: primaryContext,
|
|
81
|
+
repoAccess: primaryResult.repoAccess,
|
|
82
|
+
autoSelected: false,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (!preferredLogin) {
|
|
86
|
+
const alternatives = [];
|
|
87
|
+
const accounts = await listStoredAccounts();
|
|
88
|
+
for (const account of accounts) {
|
|
89
|
+
if (account.login === primaryContext.login)
|
|
90
|
+
continue;
|
|
91
|
+
const candidateContext = await resolveAuthContext({ preferredLogin: account.login });
|
|
92
|
+
const candidateResult = await checkSubmitAccess(owner, repo, candidateContext);
|
|
93
|
+
if (candidateResult.repoAccess && canCreateSameRepoPullRequest(candidateResult.repoAccess.permissions)) {
|
|
94
|
+
alternatives.push({
|
|
95
|
+
authContext: candidateContext,
|
|
96
|
+
repoAccess: candidateResult.repoAccess,
|
|
97
|
+
autoSelected: true,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (alternatives.length === 1) {
|
|
102
|
+
return alternatives[0];
|
|
103
|
+
}
|
|
104
|
+
if (alternatives.length > 1) {
|
|
105
|
+
throw new Error(ambiguousAccountMessage(owner, repo, alternatives.map((candidate) => candidate.authContext.login)));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (primaryResult.error) {
|
|
109
|
+
throw new Error(formatSubmitPermissionError(primaryResult.error, primaryContext.login, owner, repo));
|
|
110
|
+
}
|
|
111
|
+
throw new Error(buildSubmitPermissionMessage(primaryContext.login, owner, repo, 'The selected Haystack account does not have the collaborator access required for same-repo PR creation.'));
|
|
112
|
+
}
|
|
31
113
|
// ============================================================================
|
|
32
114
|
// Triage output
|
|
33
115
|
// ============================================================================
|
|
@@ -182,21 +264,34 @@ export async function submitCommand(options) {
|
|
|
182
264
|
console.error(chalk.red('\nNot a git repository.\n'));
|
|
183
265
|
process.exit(1);
|
|
184
266
|
}
|
|
185
|
-
//
|
|
186
|
-
let
|
|
267
|
+
// Get remote info
|
|
268
|
+
let remoteInfo;
|
|
187
269
|
try {
|
|
188
|
-
|
|
189
|
-
console.log(chalk.green(
|
|
270
|
+
remoteInfo = parseRemoteUrl('origin');
|
|
271
|
+
console.log(chalk.green(`✓ Repository: ${remoteInfo.owner}/${remoteInfo.repo}`));
|
|
190
272
|
}
|
|
191
273
|
catch (err) {
|
|
192
274
|
console.error(chalk.red(`\n${err.message}\n`));
|
|
193
275
|
process.exit(1);
|
|
194
276
|
}
|
|
195
|
-
|
|
196
|
-
let
|
|
277
|
+
let githubToken;
|
|
278
|
+
let activeGitHubLogin;
|
|
279
|
+
let autoSelectedAccount = false;
|
|
197
280
|
try {
|
|
198
|
-
|
|
199
|
-
|
|
281
|
+
const authResolution = await resolveSubmitAuthContext(remoteInfo.owner, remoteInfo.repo, options.account);
|
|
282
|
+
githubToken = authResolution.authContext.token;
|
|
283
|
+
activeGitHubLogin = authResolution.authContext.login;
|
|
284
|
+
autoSelectedAccount = authResolution.autoSelected;
|
|
285
|
+
try {
|
|
286
|
+
await setRepoDefaultAccount(remoteInfo.owner, remoteInfo.repo, activeGitHubLogin);
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
console.log(chalk.yellow(`⚠ Could not save ${activeGitHubLogin} as the default Haystack account for ${remoteInfo.owner}/${remoteInfo.repo}: ${err.message}`));
|
|
290
|
+
}
|
|
291
|
+
console.log(chalk.green(`✓ Authenticated as ${activeGitHubLogin}`));
|
|
292
|
+
if (autoSelectedAccount) {
|
|
293
|
+
console.log(chalk.dim(` Auto-selected saved account ${activeGitHubLogin} for ${remoteInfo.owner}/${remoteInfo.repo}`));
|
|
294
|
+
}
|
|
200
295
|
}
|
|
201
296
|
catch (err) {
|
|
202
297
|
console.error(chalk.red(`\n${err.message}\n`));
|
|
@@ -280,7 +375,7 @@ export async function submitCommand(options) {
|
|
|
280
375
|
// -------------------------------------------------------------------------
|
|
281
376
|
console.log(chalk.dim('\nCreating pull request...'));
|
|
282
377
|
// Check if PR already exists
|
|
283
|
-
const existingPR = await findExistingPR(remoteInfo.owner, remoteInfo.repo, currentBranch);
|
|
378
|
+
const existingPR = await findExistingPR(remoteInfo.owner, remoteInfo.repo, currentBranch, githubToken);
|
|
284
379
|
let prNumber;
|
|
285
380
|
let prUrl;
|
|
286
381
|
if (existingPR) {
|
|
@@ -316,13 +411,14 @@ export async function submitCommand(options) {
|
|
|
316
411
|
base: baseBranch,
|
|
317
412
|
body: prBody,
|
|
318
413
|
draft: options.draft,
|
|
414
|
+
token: githubToken,
|
|
319
415
|
});
|
|
320
416
|
prNumber = pr.number;
|
|
321
417
|
prUrl = pr.html_url;
|
|
322
418
|
console.log(chalk.green(`✓ Pull request created: #${prNumber}`));
|
|
323
419
|
}
|
|
324
420
|
catch (err) {
|
|
325
|
-
console.error(chalk.red(`\n${err.
|
|
421
|
+
console.error(chalk.red(`\n${formatSubmitPermissionError(err, activeGitHubLogin, remoteInfo.owner, remoteInfo.repo)}\n`));
|
|
326
422
|
process.exit(1);
|
|
327
423
|
}
|
|
328
424
|
}
|
|
@@ -331,8 +427,8 @@ export async function submitCommand(options) {
|
|
|
331
427
|
// -------------------------------------------------------------------------
|
|
332
428
|
if (options.autoMerge) {
|
|
333
429
|
try {
|
|
334
|
-
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-merge']);
|
|
335
|
-
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-merge']);
|
|
430
|
+
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-merge'], githubToken);
|
|
431
|
+
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-merge'], githubToken);
|
|
336
432
|
console.log(chalk.green('✓ Auto-merge enabled for this PR'));
|
|
337
433
|
}
|
|
338
434
|
catch (err) {
|
|
@@ -344,8 +440,8 @@ export async function submitCommand(options) {
|
|
|
344
440
|
// -------------------------------------------------------------------------
|
|
345
441
|
if (options.autoFix) {
|
|
346
442
|
try {
|
|
347
|
-
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-fix']);
|
|
348
|
-
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-fix']);
|
|
443
|
+
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-fix'], githubToken);
|
|
444
|
+
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-fix'], githubToken);
|
|
349
445
|
console.log(chalk.green('✓ Auto-fix (alpha) enabled for this PR'));
|
|
350
446
|
}
|
|
351
447
|
catch (err) {
|
|
@@ -357,11 +453,11 @@ export async function submitCommand(options) {
|
|
|
357
453
|
// -------------------------------------------------------------------------
|
|
358
454
|
if (options.review) {
|
|
359
455
|
try {
|
|
360
|
-
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:needs-review']);
|
|
361
|
-
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:needs-review']);
|
|
456
|
+
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:needs-review'], githubToken);
|
|
457
|
+
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:needs-review'], githubToken);
|
|
362
458
|
if (typeof options.review === 'string') {
|
|
363
459
|
// Specific reviewer requested
|
|
364
|
-
await requestReviewers(remoteInfo.owner, remoteInfo.repo, prNumber, [options.review]);
|
|
460
|
+
await requestReviewers(remoteInfo.owner, remoteInfo.repo, prNumber, [options.review], githubToken);
|
|
365
461
|
console.log(chalk.green(`✓ Review requested from ${options.review}`));
|
|
366
462
|
}
|
|
367
463
|
else {
|
package/dist/commands/triage.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import chalk from 'chalk';
|
|
23
23
|
import { checkAnalysisReady, fetchRatingSynthesis, fetchAutoFixManifest } from '../utils/analysis-api.js';
|
|
24
24
|
import { parseRemoteUrl } from '../utils/git.js';
|
|
25
|
-
import { loadToken } from '
|
|
25
|
+
import { loadToken } from '../utils/auth.js';
|
|
26
26
|
import { loadPendingSubmit, updatePendingSubmit, clearPendingSubmit } from '../utils/pending-state.js';
|
|
27
27
|
// ============================================================================
|
|
28
28
|
// PR identifier parsing
|
|
@@ -248,9 +248,18 @@ export async function triageCommand(identifier, options) {
|
|
|
248
248
|
const pr = resolvePR(identifier, options);
|
|
249
249
|
if (!pr)
|
|
250
250
|
return;
|
|
251
|
-
// Load GitHub token for private repo auth
|
|
252
|
-
const token = await loadToken() || undefined;
|
|
253
251
|
const isQuiet = options.hook || options.json;
|
|
252
|
+
// Load GitHub token for private repo auth
|
|
253
|
+
let token;
|
|
254
|
+
try {
|
|
255
|
+
token = await loadToken({ owner: pr.owner, repo: pr.repo }) || undefined;
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
if (!isQuiet) {
|
|
259
|
+
console.log(chalk.yellow(`\n⚠ Could not load Haystack credentials: ${err.message}`));
|
|
260
|
+
console.log(chalk.dim(' Continuing without GitHub auth. Private repos may require `haystack login`.\n'));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
254
263
|
if (!isQuiet) {
|
|
255
264
|
console.log(chalk.dim(`\nFetching analysis for ${pr.owner}/${pr.repo}#${pr.prNumber}...`));
|
|
256
265
|
}
|