@haystackeditor/cli 0.13.1 → 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.
@@ -362,8 +362,16 @@ export async function submitCommand(options) {
362
362
  // Step 4: Push to remote
363
363
  // -------------------------------------------------------------------------
364
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
+ }
365
369
  try {
366
- pushBranch('origin', currentBranch);
370
+ pushBranch('origin', currentBranch, {
371
+ token: githubToken,
372
+ owner: remoteInfo.owner,
373
+ repo: remoteInfo.repo,
374
+ });
367
375
  console.log(chalk.green('✓ Branch pushed'));
368
376
  }
369
377
  catch (err) {
package/dist/index.js CHANGED
@@ -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.1');
41
+ .version('0.13.2');
42
42
  program
43
43
  .command('init')
44
44
  .description('Create .haystack.json configuration')
@@ -52,10 +52,23 @@ export declare function remoteBranchExists(remoteName: string, branchName: strin
52
52
  * Checkout an existing branch
53
53
  */
54
54
  export declare function checkoutBranch(branchName: string): void;
55
+ export interface PushAuth {
56
+ token: string;
57
+ owner: string;
58
+ repo: string;
59
+ }
55
60
  /**
56
- * Push current branch to remote with upstream tracking
61
+ * Push current branch to remote with upstream tracking.
62
+ *
63
+ * When `auth` is provided, the push is authenticated with the given GitHub
64
+ * token via GIT_ASKPASS against `https://github.com/{owner}/{repo}.git` —
65
+ * independent of the user's ambient git credential helper. Upstream tracking
66
+ * is then set against the original `remoteName` so the token URL never lands
67
+ * in `.git/config`.
68
+ *
69
+ * Without `auth`, falls back to `git push -u <remote>` using ambient creds.
57
70
  */
58
- export declare function pushBranch(remoteName: string, branchName: string): void;
71
+ export declare function pushBranch(remoteName: string, branchName: string, auth?: PushAuth): void;
59
72
  /**
60
73
  * Parse remote URL to extract owner and repo
61
74
  * Supports both SSH and HTTPS URLs
package/dist/utils/git.js CHANGED
@@ -4,6 +4,9 @@
4
4
  * Provides functions for git operations: branch management, push, remote parsing.
5
5
  */
6
6
  import { execSync } from 'child_process';
7
+ import * as fs from 'fs';
8
+ import * as os from 'os';
9
+ import * as path from 'path';
7
10
  // Re-export findGitRoot from hooks for consistency
8
11
  export { findGitRoot } from './hooks.js';
9
12
  // ============================================================================
@@ -159,13 +162,22 @@ export function checkoutBranch(branchName) {
159
162
  throw new Error(`Failed to checkout branch: ${message}`);
160
163
  }
161
164
  }
162
- // ============================================================================
163
- // Push operations
164
- // ============================================================================
165
165
  /**
166
- * Push current branch to remote with upstream tracking
166
+ * Push current branch to remote with upstream tracking.
167
+ *
168
+ * When `auth` is provided, the push is authenticated with the given GitHub
169
+ * token via GIT_ASKPASS against `https://github.com/{owner}/{repo}.git` —
170
+ * independent of the user's ambient git credential helper. Upstream tracking
171
+ * is then set against the original `remoteName` so the token URL never lands
172
+ * in `.git/config`.
173
+ *
174
+ * Without `auth`, falls back to `git push -u <remote>` using ambient creds.
167
175
  */
168
- export function pushBranch(remoteName, branchName) {
176
+ export function pushBranch(remoteName, branchName, auth) {
177
+ if (auth) {
178
+ pushBranchWithToken(remoteName, branchName, auth);
179
+ return;
180
+ }
169
181
  try {
170
182
  execSync(`git push -u ${remoteName} "${branchName}"`, {
171
183
  encoding: 'utf-8',
@@ -173,15 +185,64 @@ export function pushBranch(remoteName, branchName) {
173
185
  });
174
186
  }
175
187
  catch (err) {
176
- const message = err instanceof Error ? err.message : String(err);
177
- if (message.includes('permission denied') || message.includes('403')) {
178
- throw new Error('Permission denied. Check your GitHub credentials.');
188
+ throw translatePushError(err);
189
+ }
190
+ }
191
+ function pushBranchWithToken(remoteName, branchName, auth) {
192
+ const askpassPath = path.join(os.tmpdir(), `haystack-askpass-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.sh`);
193
+ // Script reads token from env at call time — keeps it out of the script contents and argv.
194
+ const script = `#!/bin/sh\ncase "$1" in\n Username*) printf '%s' "x-access-token" ;;\n *) printf '%s' "$HAYSTACK_GIT_TOKEN" ;;\nesac\n`;
195
+ fs.writeFileSync(askpassPath, script, { mode: 0o700 });
196
+ try {
197
+ const pushUrl = `https://github.com/${auth.owner}/${auth.repo}.git`;
198
+ execSync(`git push "${pushUrl}" "HEAD:refs/heads/${branchName}"`, {
199
+ encoding: 'utf-8',
200
+ stdio: ['pipe', 'pipe', 'pipe'],
201
+ env: {
202
+ ...process.env,
203
+ GIT_ASKPASS: askpassPath,
204
+ GIT_TERMINAL_PROMPT: '0',
205
+ HAYSTACK_GIT_TOKEN: auth.token,
206
+ },
207
+ });
208
+ try {
209
+ execSync(`git branch --set-upstream-to=${remoteName}/${branchName} "${branchName}"`, {
210
+ encoding: 'utf-8',
211
+ stdio: ['pipe', 'pipe', 'pipe'],
212
+ });
213
+ }
214
+ catch (err) {
215
+ // Non-fatal: push succeeded; upstream tracking is a convenience.
216
+ const message = err instanceof Error ? err.message : String(err);
217
+ console.warn(`Warning: failed to set upstream tracking for ${branchName}: ${message}`);
218
+ }
219
+ }
220
+ catch (err) {
221
+ throw translatePushError(err, auth);
222
+ }
223
+ finally {
224
+ try {
225
+ fs.unlinkSync(askpassPath);
226
+ }
227
+ catch (err) {
228
+ const message = err instanceof Error ? err.message : String(err);
229
+ // Best-effort cleanup; log so we can spot leaking askpass scripts in /tmp.
230
+ console.warn(`Warning: failed to remove temp askpass script ${askpassPath}: ${message}`);
179
231
  }
180
- if (message.includes('remote rejected')) {
181
- throw new Error('Remote rejected the push. The branch may be protected.');
232
+ }
233
+ }
234
+ function translatePushError(err, auth) {
235
+ const message = err instanceof Error ? err.message : String(err);
236
+ if (message.includes('permission denied') || message.includes('403')) {
237
+ if (auth) {
238
+ return new Error(`Permission denied pushing to ${auth.owner}/${auth.repo}. The Haystack account token lacks write access to this repo — run \`haystack auth list\` and pick an account that's a collaborator.`);
182
239
  }
183
- throw new Error(`Failed to push: ${message}`);
240
+ return new Error('Permission denied. Check your GitHub credentials.');
241
+ }
242
+ if (message.includes('remote rejected')) {
243
+ return new Error('Remote rejected the push. The branch may be protected.');
184
244
  }
245
+ return new Error(`Failed to push: ${message}`);
185
246
  }
186
247
  // ============================================================================
187
248
  // Remote operations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haystackeditor/cli",
3
- "version": "0.13.1",
3
+ "version": "0.13.2",
4
4
  "description": "Set up Haystack for your project — automated PR review, triage, and merge queue",
5
5
  "type": "module",
6
6
  "bin": {