@haystackeditor/cli 0.13.1 → 0.13.3
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/submit.js +5 -1
- package/dist/index.js +1 -1
- package/dist/utils/git.d.ts +14 -2
- package/dist/utils/git.js +82 -11
- package/package.json +1 -1
package/dist/commands/submit.js
CHANGED
|
@@ -362,8 +362,12 @@ 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, { token: githubToken });
|
|
367
371
|
console.log(chalk.green('✓ Branch pushed'));
|
|
368
372
|
}
|
|
369
373
|
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.
|
|
41
|
+
.version('0.13.3');
|
|
42
42
|
program
|
|
43
43
|
.command('init')
|
|
44
44
|
.description('Create .haystack.json configuration')
|
package/dist/utils/git.d.ts
CHANGED
|
@@ -52,10 +52,22 @@ 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
|
+
}
|
|
55
58
|
/**
|
|
56
|
-
* Push current branch to remote with upstream tracking
|
|
59
|
+
* Push current branch to remote with upstream tracking.
|
|
60
|
+
*
|
|
61
|
+
* When `auth` is provided, the push is authenticated with the given GitHub
|
|
62
|
+
* token via GIT_ASKPASS against an explicit HTTPS URL, independent of the
|
|
63
|
+
* user's ambient git credential helper and independent of whatever
|
|
64
|
+
* `remote.<name>.url` / `pushurl` they have configured. Remote-tracking
|
|
65
|
+
* refs are then seeded locally so upstream tracking (`-u`-style) works on
|
|
66
|
+
* first push. `.git/config` is never touched.
|
|
67
|
+
*
|
|
68
|
+
* Without `auth`, falls back to `git push -u <remote>` using ambient creds.
|
|
57
69
|
*/
|
|
58
|
-
export declare function pushBranch(remoteName: string, branchName: string): void;
|
|
70
|
+
export declare function pushBranch(remoteName: string, branchName: string, auth?: PushAuth): void;
|
|
59
71
|
/**
|
|
60
72
|
* Parse remote URL to extract owner and repo
|
|
61
73
|
* 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,23 @@ 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 an explicit HTTPS URL, independent of the
|
|
170
|
+
* user's ambient git credential helper and independent of whatever
|
|
171
|
+
* `remote.<name>.url` / `pushurl` they have configured. Remote-tracking
|
|
172
|
+
* refs are then seeded locally so upstream tracking (`-u`-style) works on
|
|
173
|
+
* first push. `.git/config` is never touched.
|
|
174
|
+
*
|
|
175
|
+
* Without `auth`, falls back to `git push -u <remote>` using ambient creds.
|
|
167
176
|
*/
|
|
168
|
-
export function pushBranch(remoteName, branchName) {
|
|
177
|
+
export function pushBranch(remoteName, branchName, auth) {
|
|
178
|
+
if (auth) {
|
|
179
|
+
pushBranchWithToken(remoteName, branchName, auth.token);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
169
182
|
try {
|
|
170
183
|
execSync(`git push -u ${remoteName} "${branchName}"`, {
|
|
171
184
|
encoding: 'utf-8',
|
|
@@ -173,15 +186,73 @@ export function pushBranch(remoteName, branchName) {
|
|
|
173
186
|
});
|
|
174
187
|
}
|
|
175
188
|
catch (err) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
189
|
+
throw translatePushError(err);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function pushBranchWithToken(remoteName, branchName, token) {
|
|
193
|
+
// Derive owner/repo from the remote itself so the push URL always matches
|
|
194
|
+
// the tracking ref we'll seed below. No way for caller and remote identity
|
|
195
|
+
// to silently disagree.
|
|
196
|
+
const { owner, repo } = parseRemoteUrl(remoteName);
|
|
197
|
+
const pushUrl = `https://github.com/${owner}/${repo}.git`;
|
|
198
|
+
const repoSlug = `${owner}/${repo}`;
|
|
199
|
+
const askpassPath = path.join(os.tmpdir(), `haystack-askpass-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.sh`);
|
|
200
|
+
// Script reads token from env at call time — keeps it out of the script contents and argv.
|
|
201
|
+
const script = `#!/bin/sh\ncase "$1" in\n Username*) printf '%s' "x-access-token" ;;\n *) printf '%s' "$HAYSTACK_GIT_TOKEN" ;;\nesac\n`;
|
|
202
|
+
fs.writeFileSync(askpassPath, script, { mode: 0o700 });
|
|
203
|
+
const authedEnv = {
|
|
204
|
+
...process.env,
|
|
205
|
+
GIT_ASKPASS: askpassPath,
|
|
206
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
207
|
+
HAYSTACK_GIT_TOKEN: token,
|
|
208
|
+
};
|
|
209
|
+
try {
|
|
210
|
+
// Push to an explicit HTTPS URL with GIT_ASKPASS, bypassing ambient
|
|
211
|
+
// credential helpers entirely. Using a raw URL (not the named remote)
|
|
212
|
+
// means the user's configured url/pushurl/credential-helper settings
|
|
213
|
+
// are untouched — sidesteps precedence and multi-value edge cases
|
|
214
|
+
// around `remote.<name>.url` vs `remote.<name>.pushurl`.
|
|
215
|
+
execSync(`git -c credential.helper= push "${pushUrl}" "HEAD:refs/heads/${branchName}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], env: authedEnv });
|
|
216
|
+
// Pushing to a raw URL doesn't update refs/remotes/<name>/<branch>. Seed
|
|
217
|
+
// it from the server's authoritative state with a targeted fetch — this
|
|
218
|
+
// reflects any server-side rewrite or hook side effects, not a fabricated
|
|
219
|
+
// pre-push SHA. Then set upstream tracking to the named remote.
|
|
220
|
+
try {
|
|
221
|
+
execSync(`git -c credential.helper= fetch "${pushUrl}" "refs/heads/${branchName}:refs/remotes/${remoteName}/${branchName}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], env: authedEnv });
|
|
222
|
+
execSync(`git branch --set-upstream-to=${remoteName}/${branchName} "${branchName}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
179
223
|
}
|
|
180
|
-
|
|
181
|
-
|
|
224
|
+
catch (err) {
|
|
225
|
+
// Non-fatal: push succeeded; upstream tracking is a convenience.
|
|
226
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
227
|
+
console.warn(`Warning: failed to set upstream tracking for ${branchName}: ${message}`);
|
|
182
228
|
}
|
|
183
|
-
throw new Error(`Failed to push: ${message}`);
|
|
184
229
|
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
throw translatePushError(err, repoSlug);
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
try {
|
|
235
|
+
fs.unlinkSync(askpassPath);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
239
|
+
// Best-effort cleanup; log so we can spot leaking askpass scripts in /tmp.
|
|
240
|
+
console.warn(`Warning: failed to remove temp askpass script ${askpassPath}: ${message}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function translatePushError(err, repoSlug) {
|
|
245
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
246
|
+
if (message.includes('permission denied') || message.includes('403')) {
|
|
247
|
+
if (repoSlug) {
|
|
248
|
+
return new Error(`Permission denied pushing to ${repoSlug}. The Haystack account token lacks write access to this repo — run \`haystack auth list\` and pick an account that's a collaborator.`);
|
|
249
|
+
}
|
|
250
|
+
return new Error('Permission denied. Check your GitHub credentials.');
|
|
251
|
+
}
|
|
252
|
+
if (message.includes('remote rejected')) {
|
|
253
|
+
return new Error('Remote rejected the push. The branch may be protected.');
|
|
254
|
+
}
|
|
255
|
+
return new Error(`Failed to push: ${message}`);
|
|
185
256
|
}
|
|
186
257
|
// ============================================================================
|
|
187
258
|
// Remote operations
|