@haystackeditor/cli 0.13.0 → 0.13.2

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.
@@ -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 { loadToken } from './login.js';
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
- const token = await loadToken();
68
- if (!token) {
69
- console.error(chalk.red('\nNot logged in. Run `haystack login` first.\n'));
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, {
@@ -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 { loadToken } from './login.js';
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
- const token = await loadToken();
129
- if (!token) {
130
- console.error(chalk.red('\nNot logged in. Run `haystack login` first.\n'));
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
- const token = await loadToken();
186
- if (!token) {
187
- console.error(chalk.red('\nNot logged in. Run `haystack login` first.\n'));
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`));
@@ -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>;
@@ -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 verifyToken(token);
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('Credentials saved to ~/.haystack/credentials.json\n'));
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 fs.unlink(TOKEN_FILE);
157
- console.log(chalk.green('\nLogged out successfully.\n'));
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.log(chalk.yellow('\nNot logged in.\n'));
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 { loadToken } from './login.js';
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
- const token = await loadToken();
124
- if (!token) {
125
- console.error(chalk.red('\nNot logged in. Run `haystack login` first.\n'));
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 { loadToken } from './login.js';
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
- // Require auth
63
- const token = await loadToken();
64
- if (!token) {
65
- console.error(chalk.red('\nNot logged in. Run `haystack login` first.\n'));
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) {
@@ -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 { loadToken } from './login.js';
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
- const token = await loadToken();
477
- if (!token) {
478
- console.log(chalk.yellow(' Not logged in. Run `haystack login` first.\n'));
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: {
@@ -10,6 +10,7 @@
10
10
  * - --force: skip triage checks
11
11
  */
12
12
  export interface SubmitOptions {
13
+ account?: string;
13
14
  title?: string;
14
15
  body?: string;
15
16
  bodyFile?: string;
@@ -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, requireGitHubAuth, requestReviewers, } from '../utils/github-api.js';
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
- // Check authentication
186
- let githubToken;
267
+ // Get remote info
268
+ let remoteInfo;
187
269
  try {
188
- githubToken = await requireGitHubAuth();
189
- console.log(chalk.green('✓ Authenticated'));
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
- // Get remote info
196
- let remoteInfo;
277
+ let githubToken;
278
+ let activeGitHubLogin;
279
+ let autoSelectedAccount = false;
197
280
  try {
198
- remoteInfo = parseRemoteUrl('origin');
199
- console.log(chalk.green(`✓ Repository: ${remoteInfo.owner}/${remoteInfo.repo}`));
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`));
@@ -267,8 +362,16 @@ export async function submitCommand(options) {
267
362
  // Step 4: Push to remote
268
363
  // -------------------------------------------------------------------------
269
364
  console.log(chalk.dim('\nPushing to origin...'));
365
+ if (!githubToken) {
366
+ console.error(chalk.red('\nInternal error: no Haystack account token resolved before push.\n'));
367
+ process.exit(1);
368
+ }
270
369
  try {
271
- pushBranch('origin', currentBranch);
370
+ pushBranch('origin', currentBranch, {
371
+ token: githubToken,
372
+ owner: remoteInfo.owner,
373
+ repo: remoteInfo.repo,
374
+ });
272
375
  console.log(chalk.green('✓ Branch pushed'));
273
376
  }
274
377
  catch (err) {
@@ -280,7 +383,7 @@ export async function submitCommand(options) {
280
383
  // -------------------------------------------------------------------------
281
384
  console.log(chalk.dim('\nCreating pull request...'));
282
385
  // Check if PR already exists
283
- const existingPR = await findExistingPR(remoteInfo.owner, remoteInfo.repo, currentBranch);
386
+ const existingPR = await findExistingPR(remoteInfo.owner, remoteInfo.repo, currentBranch, githubToken);
284
387
  let prNumber;
285
388
  let prUrl;
286
389
  if (existingPR) {
@@ -316,13 +419,14 @@ export async function submitCommand(options) {
316
419
  base: baseBranch,
317
420
  body: prBody,
318
421
  draft: options.draft,
422
+ token: githubToken,
319
423
  });
320
424
  prNumber = pr.number;
321
425
  prUrl = pr.html_url;
322
426
  console.log(chalk.green(`✓ Pull request created: #${prNumber}`));
323
427
  }
324
428
  catch (err) {
325
- console.error(chalk.red(`\n${err.message}\n`));
429
+ console.error(chalk.red(`\n${formatSubmitPermissionError(err, activeGitHubLogin, remoteInfo.owner, remoteInfo.repo)}\n`));
326
430
  process.exit(1);
327
431
  }
328
432
  }
@@ -331,8 +435,8 @@ export async function submitCommand(options) {
331
435
  // -------------------------------------------------------------------------
332
436
  if (options.autoMerge) {
333
437
  try {
334
- await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-merge']);
335
- await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-merge']);
438
+ await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-merge'], githubToken);
439
+ await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-merge'], githubToken);
336
440
  console.log(chalk.green('✓ Auto-merge enabled for this PR'));
337
441
  }
338
442
  catch (err) {
@@ -344,8 +448,8 @@ export async function submitCommand(options) {
344
448
  // -------------------------------------------------------------------------
345
449
  if (options.autoFix) {
346
450
  try {
347
- await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-fix']);
348
- await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-fix']);
451
+ await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-fix'], githubToken);
452
+ await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-fix'], githubToken);
349
453
  console.log(chalk.green('✓ Auto-fix (alpha) enabled for this PR'));
350
454
  }
351
455
  catch (err) {
@@ -357,11 +461,11 @@ export async function submitCommand(options) {
357
461
  // -------------------------------------------------------------------------
358
462
  if (options.review) {
359
463
  try {
360
- await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:needs-review']);
361
- await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:needs-review']);
464
+ await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:needs-review'], githubToken);
465
+ await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:needs-review'], githubToken);
362
466
  if (typeof options.review === 'string') {
363
467
  // Specific reviewer requested
364
- await requestReviewers(remoteInfo.owner, remoteInfo.repo, prNumber, [options.review]);
468
+ await requestReviewers(remoteInfo.owner, remoteInfo.repo, prNumber, [options.review], githubToken);
365
469
  console.log(chalk.green(`✓ Review requested from ${options.review}`));
366
470
  }
367
471
  else {