@haystackeditor/cli 0.25.0 → 0.26.0
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/README.md +39 -455
- package/dist/capture/adapters/django.js +7 -0
- package/dist/capture/adapters/index.js +10 -0
- package/dist/capture/adapters/next-build.js +113 -0
- package/dist/capture/adapters/next.js +272 -105
- package/dist/capture/adapters/nuxt.js +7 -0
- package/dist/capture/adapters/rails.js +7 -0
- package/dist/capture/adapters/react-router.js +7 -0
- package/dist/capture/adapters/sveltekit.js +7 -0
- package/dist/capture/adapters/vite.js +14 -0
- package/dist/capture/app-config.js +27 -2
- package/dist/capture/csp.js +7 -6
- package/dist/capture/url-rewrites.js +232 -0
- package/dist/capture-step.js +3 -0
- package/dist/commands/capture-brief.js +41 -32
- package/dist/commands/capture-manifest.js +25 -17
- package/dist/commands/feedback.js +66 -0
- package/dist/commands/init-capture.js +44 -27
- package/dist/commands/init-telemetry.js +77 -14
- package/dist/commands/init.js +19 -4
- package/dist/commands/lockfile-pin.js +307 -0
- package/dist/commands/verify-onboarding.js +16 -0
- package/dist/commands/verify.js +70 -15
- package/dist/index.js +29 -923
- package/dist/schema.js +4 -10
- package/dist/utils/haystack-api.js +0 -36
- package/package.json +1 -5
- package/schemas/feedback.v1.json +13 -0
- package/schemas/init.v1.json +1 -1
- package/schemas/pre-verify.v2.json +240 -0
- package/schemas/verify-raw.v1.json +1132 -0
- package/schemas/verify.v2.json +655 -0
- package/dist/assets/hooks/agent-context/detect.ts +0 -316
- package/dist/assets/hooks/agent-context/format.ts +0 -100
- package/dist/assets/hooks/agent-context/index.ts +0 -41
- package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
- package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
- package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
- package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
- package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
- package/dist/assets/hooks/agent-context/types.ts +0 -58
- package/dist/assets/hooks/llm-rules-template.md +0 -59
- package/dist/assets/hooks/package-lock.json +0 -598
- package/dist/assets/hooks/package.json +0 -12
- package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
- package/dist/assets/hooks/scripts/post-commit.sh +0 -5
- package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
- package/dist/assets/hooks/scripts/pre-push.sh +0 -25
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
- package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
- package/dist/assets/hooks/truncation-checker/index.ts +0 -595
- package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
- package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
- package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
- package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
- package/dist/assets/skills/map-your-system.md +0 -143
- package/dist/assets/skills/submit.md +0 -200
- package/dist/commands/ask.js +0 -20
- package/dist/commands/cloud-verifier-behaviors.js +0 -218
- package/dist/commands/cloud-verifier-data-store-census.js +0 -539
- package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
- package/dist/commands/cloud-verifier-identity-census.js +0 -4060
- package/dist/commands/cloud-verifier-materialization.js +0 -704
- package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
- package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
- package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
- package/dist/commands/cloud-verifier-universe.js +0 -10178
- package/dist/commands/config.js +0 -549
- package/dist/commands/design-verify.js +0 -311
- package/dist/commands/dismiss.js +0 -159
- package/dist/commands/hooks.js +0 -226
- package/dist/commands/inbox.js +0 -137
- package/dist/commands/mcp.js +0 -201
- package/dist/commands/policy.js +0 -371
- package/dist/commands/pr-status.js +0 -207
- package/dist/commands/pr.js +0 -105
- package/dist/commands/prepare-universe-review.js +0 -1092
- package/dist/commands/production-source-deny-policy.js +0 -100
- package/dist/commands/request-review.js +0 -74
- package/dist/commands/review.js +0 -191
- package/dist/commands/rules.js +0 -98
- package/dist/commands/scaffold-provisional-universe.js +0 -806
- package/dist/commands/setup.js +0 -1170
- package/dist/commands/skills.js +0 -447
- package/dist/commands/submit.js +0 -745
- package/dist/commands/system-map.js +0 -228
- package/dist/commands/triage.js +0 -598
- package/dist/commands/webhooks.js +0 -241
- package/dist/states.js +0 -46
- package/dist/tools/detect.js +0 -832
- package/dist/triage/astra.js +0 -202
- package/dist/triage/prompts.js +0 -188
- package/dist/triage/runner.js +0 -200
- package/dist/triage/types.js +0 -7
- package/dist/utils/action-output.js +0 -26
- package/dist/utils/analysis-api.js +0 -416
- package/dist/utils/design-verifier-api.js +0 -294
- package/dist/utils/design-verifier-history.js +0 -79
- package/dist/utils/design-verifier-result.js +0 -424
- package/dist/utils/github-api.js +0 -324
- package/dist/utils/pending-state.js +0 -86
- package/dist/utils/pr-ref.js +0 -56
- package/dist/utils/prompter.js +0 -328
- package/schemas/action.v1.json +0 -22
- package/schemas/ask.v1.json +0 -40
- package/schemas/inbox.v1.json +0 -27
- package/schemas/pr-status.v1.json +0 -61
- package/schemas/pr.v1.json +0 -97
- package/schemas/pr.v3.json +0 -45
- package/schemas/setup.v1.json +0 -75
- package/schemas/submit.v1.json +0 -90
- package/schemas/triage.v1.json +0 -103
- package/schemas/triage.v2.json +0 -64
package/dist/utils/github-api.js
DELETED
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GitHub API utilities for haystack submit command.
|
|
3
|
-
*
|
|
4
|
-
* Provides functions for creating PRs, managing labels, etc.
|
|
5
|
-
*/
|
|
6
|
-
import { isHeadlessToken, resolveAuthContext } from './auth.js';
|
|
7
|
-
import { trackError } from './telemetry.js';
|
|
8
|
-
const GITHUB_API_BASE = 'https://api.github.com';
|
|
9
|
-
// OAuth App Policy fallback. Orgs can restrict our OAuth app, which makes the
|
|
10
|
-
// user's token 403 on every API call to that org — so `haystack submit` can't
|
|
11
|
-
// open PRs, add labels, or request reviewers there. The auth-worker GitHub
|
|
12
|
-
// proxy swaps in the GitHub App installation token server-side (the App isn't
|
|
13
|
-
// subject to OAuth App Policy / SAML SSO enforcement the way the OAuth token
|
|
14
|
-
// is), so on such a 403 we retry the same request through it. Direct calls are
|
|
15
|
-
// tried first, so on unrestricted orgs nothing changes and PRs stay
|
|
16
|
-
// user-authored; only the fallback path is performed by the App (haystack[bot]).
|
|
17
|
-
const GITHUB_PROXY_BASE = 'https://haystackeditor.com/api/github';
|
|
18
|
-
/**
|
|
19
|
-
* A 403 that is NOT a rate-limit is almost always OAuth App Policy or SAML SSO
|
|
20
|
-
* enforcement blocking our OAuth app on this org — exactly what the
|
|
21
|
-
* installation-token proxy can bypass. Rate-limit 403s carry
|
|
22
|
-
* `x-ratelimit-remaining: 0` and the proxy can't help with those, so we don't
|
|
23
|
-
* retry them.
|
|
24
|
-
*/
|
|
25
|
-
/**
|
|
26
|
-
* REST base URL for a token. Haystack headless tokens (hsk_live_*) only work
|
|
27
|
-
* through the Haystack proxy and must never reach api.github.com.
|
|
28
|
-
*/
|
|
29
|
-
export function githubRestBaseFor(token) {
|
|
30
|
-
return isHeadlessToken(token) ? GITHUB_PROXY_BASE : GITHUB_API_BASE;
|
|
31
|
-
}
|
|
32
|
-
function isOAuthPolicyForbidden(response) {
|
|
33
|
-
return response.status === 403 && response.headers.get('x-ratelimit-remaining') !== '0';
|
|
34
|
-
}
|
|
35
|
-
// Label definitions for Haystack workflow
|
|
36
|
-
export const HAYSTACK_LABELS = {
|
|
37
|
-
'haystack:auto-merge': {
|
|
38
|
-
name: 'haystack:auto-merge',
|
|
39
|
-
color: '0E8A16', // Green
|
|
40
|
-
description: 'Auto-merge after Haystack approval',
|
|
41
|
-
},
|
|
42
|
-
'haystack:needs-review': {
|
|
43
|
-
name: 'haystack:needs-review',
|
|
44
|
-
color: 'FBCA04', // Yellow
|
|
45
|
-
description: 'Requires Haystack review',
|
|
46
|
-
},
|
|
47
|
-
'haystack:ready-to-merge': {
|
|
48
|
-
name: 'haystack:ready-to-merge',
|
|
49
|
-
color: '2EA44F', // GitHub green
|
|
50
|
-
description: 'Passed Haystack analysis, ready to merge',
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
// ============================================================================
|
|
54
|
-
// Authentication
|
|
55
|
-
// ============================================================================
|
|
56
|
-
/**
|
|
57
|
-
* Get a GitHub token, throw if not authenticated
|
|
58
|
-
*/
|
|
59
|
-
export async function requireGitHubAuth(options) {
|
|
60
|
-
const context = await resolveAuthContext(options);
|
|
61
|
-
return context.token;
|
|
62
|
-
}
|
|
63
|
-
async function resolveToken(token, options) {
|
|
64
|
-
return token ?? requireGitHubAuth(options);
|
|
65
|
-
}
|
|
66
|
-
// ============================================================================
|
|
67
|
-
// API helpers
|
|
68
|
-
// ============================================================================
|
|
69
|
-
/**
|
|
70
|
-
* Make an authenticated GitHub API request
|
|
71
|
-
*/
|
|
72
|
-
async function githubApiRequest(method, path, token, body) {
|
|
73
|
-
// body is serialized to a string (not a stream), so the same init can be
|
|
74
|
-
// reused for the proxy retry below.
|
|
75
|
-
const init = {
|
|
76
|
-
method,
|
|
77
|
-
headers: {
|
|
78
|
-
Authorization: `Bearer ${token}`,
|
|
79
|
-
Accept: 'application/vnd.github.v3+json',
|
|
80
|
-
'Content-Type': 'application/json',
|
|
81
|
-
'User-Agent': 'Haystack-CLI',
|
|
82
|
-
},
|
|
83
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
84
|
-
};
|
|
85
|
-
// A Haystack headless token (hsk_live_*) is not a GitHub credential: sending
|
|
86
|
-
// it to api.github.com would leak it to GitHub and fail anyway. Only the
|
|
87
|
-
// Haystack proxy understands it (it resolves an App installation token).
|
|
88
|
-
if (isHeadlessToken(token)) {
|
|
89
|
-
return fetch(`${GITHUB_PROXY_BASE}${path}`, init);
|
|
90
|
-
}
|
|
91
|
-
const direct = await fetch(`${GITHUB_API_BASE}${path}`, init);
|
|
92
|
-
if (!isOAuthPolicyForbidden(direct))
|
|
93
|
-
return direct;
|
|
94
|
-
// Org restricts our OAuth app — retry through the proxy on the App's
|
|
95
|
-
// installation token. Same `Authorization: Bearer <github-token>`; the proxy
|
|
96
|
-
// validates it, then uses its own token upstream. If the proxy can't help
|
|
97
|
-
// (e.g. the App isn't installed on this owner), fall back to the original
|
|
98
|
-
// response so handleApiError surfaces GitHub's actual "org restricted" 403.
|
|
99
|
-
const proxied = await fetch(`${GITHUB_PROXY_BASE}${path}`, init);
|
|
100
|
-
return proxied.status === 403 ? direct : proxied;
|
|
101
|
-
}
|
|
102
|
-
async function handleApiError(response, context, operation) {
|
|
103
|
-
if (response.status === 401) {
|
|
104
|
-
throw new Error('GitHub token expired or invalid. Run `haystack login` again.');
|
|
105
|
-
}
|
|
106
|
-
if (response.status === 403) {
|
|
107
|
-
const rateLimitRemaining = response.headers.get('x-ratelimit-remaining');
|
|
108
|
-
if (rateLimitRemaining === '0') {
|
|
109
|
-
// Log every rate-limit hit so we can watch real-world frequency and decide
|
|
110
|
-
// whether the CLI needs its own token bucket. Routes to PostHog (and on to
|
|
111
|
-
// Slack via the ops-alert pipeline). Fire-and-forget; never blocks.
|
|
112
|
-
trackError('cli_github_rate_limited', {
|
|
113
|
-
operation,
|
|
114
|
-
status: 403,
|
|
115
|
-
rate_limit_limit: response.headers.get('x-ratelimit-limit'),
|
|
116
|
-
rate_limit_reset: response.headers.get('x-ratelimit-reset'),
|
|
117
|
-
rate_limit_resource: response.headers.get('x-ratelimit-resource'),
|
|
118
|
-
});
|
|
119
|
-
throw new Error('GitHub API rate limit exceeded. Please wait and try again.');
|
|
120
|
-
}
|
|
121
|
-
throw new Error(`Permission denied: ${context}`);
|
|
122
|
-
}
|
|
123
|
-
if (response.status === 404) {
|
|
124
|
-
throw new Error(`Not found: ${context}`);
|
|
125
|
-
}
|
|
126
|
-
if (response.status === 422) {
|
|
127
|
-
const data = await response.json().catch(() => ({}));
|
|
128
|
-
const errorMsg = data.errors?.[0]?.message || data.message || 'Validation failed';
|
|
129
|
-
throw new Error(`GitHub API error: ${errorMsg}`);
|
|
130
|
-
}
|
|
131
|
-
const data = await response.json().catch(() => ({}));
|
|
132
|
-
throw new Error(data.message || `GitHub API error (${response.status}): ${context}`);
|
|
133
|
-
}
|
|
134
|
-
// ============================================================================
|
|
135
|
-
// Pull Request operations
|
|
136
|
-
// ============================================================================
|
|
137
|
-
/**
|
|
138
|
-
* Create a pull request
|
|
139
|
-
*/
|
|
140
|
-
export async function createPullRequest(options) {
|
|
141
|
-
const token = await resolveToken(options.token, {
|
|
142
|
-
owner: options.owner,
|
|
143
|
-
repo: options.repo,
|
|
144
|
-
});
|
|
145
|
-
const response = await githubApiRequest('POST', `/repos/${options.owner}/${options.repo}/pulls`, token, {
|
|
146
|
-
title: options.title,
|
|
147
|
-
head: options.head,
|
|
148
|
-
base: options.base,
|
|
149
|
-
body: options.body || '',
|
|
150
|
-
draft: options.draft || false,
|
|
151
|
-
});
|
|
152
|
-
if (!response.ok) {
|
|
153
|
-
// GitHub returns 500 when creating a PR from a branch that was the head
|
|
154
|
-
// of a recently merged PR. Check for this case and give an actionable error.
|
|
155
|
-
if (response.status >= 500) {
|
|
156
|
-
const mergedPR = await findMergedPRForBranch(options.owner, options.repo, options.head, token);
|
|
157
|
-
if (mergedPR) {
|
|
158
|
-
throw new Error(`Branch "${options.head}" was already used for merged PR #${mergedPR.number}. ` +
|
|
159
|
-
`Push to a new branch name and try again.`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
await handleApiError(response, `creating PR in ${options.owner}/${options.repo}`, 'create_pull_request');
|
|
163
|
-
}
|
|
164
|
-
return response.json();
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Check if a merged PR exists for a given branch (used for diagnosing 500s on PR creation).
|
|
168
|
-
*
|
|
169
|
-
* Only searches the exact qualified head (owner:branch). We intentionally do NOT
|
|
170
|
-
* fall back to a bare branch name because that can match a different fork's branch
|
|
171
|
-
* and produce a misleading "branch already used" diagnosis.
|
|
172
|
-
*
|
|
173
|
-
* Returns null if the lookup fails (5xx, network error) so the caller falls through
|
|
174
|
-
* to the generic error handler rather than surfacing a wrong diagnosis.
|
|
175
|
-
*/
|
|
176
|
-
async function findMergedPRForBranch(owner, repo, head, token) {
|
|
177
|
-
const headParam = head.includes(':') ? head : `${owner}:${head}`;
|
|
178
|
-
try {
|
|
179
|
-
const response = await githubApiRequest('GET', `/repos/${owner}/${repo}/pulls?head=${encodeURIComponent(headParam)}&state=closed`, token);
|
|
180
|
-
if (!response.ok) {
|
|
181
|
-
console.error(`[github-api] Merged-PR lookup failed (head=${headParam}): HTTP ${response.status}`);
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
const prs = await response.json();
|
|
185
|
-
return prs.find((pr) => pr.merged_at) ?? null;
|
|
186
|
-
}
|
|
187
|
-
catch (err) {
|
|
188
|
-
console.error(`[github-api] Merged-PR lookup error (head=${headParam}): ${err}`);
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Check if a PR already exists for a branch
|
|
194
|
-
*/
|
|
195
|
-
export async function findExistingPR(owner, repo, head, token) {
|
|
196
|
-
const authToken = await resolveToken(token, { owner, repo });
|
|
197
|
-
// head format should be "owner:branch" for cross-repo or just "branch" for same-repo
|
|
198
|
-
const headParam = head.includes(':') ? head : `${owner}:${head}`;
|
|
199
|
-
const response = await githubApiRequest('GET', `/repos/${owner}/${repo}/pulls?head=${encodeURIComponent(headParam)}&state=open`, authToken);
|
|
200
|
-
if (!response.ok) {
|
|
201
|
-
if (response.status === 404)
|
|
202
|
-
return null;
|
|
203
|
-
await handleApiError(response, 'checking for existing PR', 'find_existing_pull_request');
|
|
204
|
-
}
|
|
205
|
-
const prs = await response.json();
|
|
206
|
-
return prs.length > 0 ? prs[0] : null;
|
|
207
|
-
}
|
|
208
|
-
// ============================================================================
|
|
209
|
-
// Label operations
|
|
210
|
-
// ============================================================================
|
|
211
|
-
/**
|
|
212
|
-
* Add labels to an issue/PR
|
|
213
|
-
*/
|
|
214
|
-
export async function addLabelsToIssue(owner, repo, issueNumber, labels, token) {
|
|
215
|
-
const authToken = await resolveToken(token, { owner, repo });
|
|
216
|
-
const response = await githubApiRequest('POST', `/repos/${owner}/${repo}/issues/${issueNumber}/labels`, authToken, { labels });
|
|
217
|
-
if (!response.ok) {
|
|
218
|
-
await handleApiError(response, `adding labels to PR #${issueNumber}`, 'add_labels');
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Get all labels from an issue/PR
|
|
223
|
-
*/
|
|
224
|
-
export async function getLabelsFromIssue(owner, repo, issueNumber, token) {
|
|
225
|
-
const authToken = await resolveToken(token, { owner, repo });
|
|
226
|
-
const response = await githubApiRequest('GET', `/repos/${owner}/${repo}/issues/${issueNumber}/labels`, authToken);
|
|
227
|
-
if (!response.ok) {
|
|
228
|
-
await handleApiError(response, `getting labels from PR #${issueNumber}`, 'get_labels');
|
|
229
|
-
}
|
|
230
|
-
const labels = await response.json();
|
|
231
|
-
return labels.map((l) => l.name);
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Remove a label from an issue/PR
|
|
235
|
-
*/
|
|
236
|
-
export async function removeLabelFromIssue(owner, repo, issueNumber, labelName, token) {
|
|
237
|
-
const authToken = await resolveToken(token, { owner, repo });
|
|
238
|
-
const response = await githubApiRequest('DELETE', `/repos/${owner}/${repo}/issues/${issueNumber}/labels/${encodeURIComponent(labelName)}`, authToken);
|
|
239
|
-
// 404 is fine - label wasn't on the issue
|
|
240
|
-
if (!response.ok && response.status !== 404) {
|
|
241
|
-
await handleApiError(response, `removing label '${labelName}' from PR #${issueNumber}`, 'remove_label');
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Check if a label exists in the repo
|
|
246
|
-
*/
|
|
247
|
-
export async function labelExists(owner, repo, labelName, token) {
|
|
248
|
-
const authToken = await resolveToken(token, { owner, repo });
|
|
249
|
-
const response = await githubApiRequest('GET', `/repos/${owner}/${repo}/labels/${encodeURIComponent(labelName)}`, authToken);
|
|
250
|
-
return response.ok;
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Create a label in the repo
|
|
254
|
-
*/
|
|
255
|
-
export async function createLabel(owner, repo, label, token) {
|
|
256
|
-
const authToken = await resolveToken(token, { owner, repo });
|
|
257
|
-
const response = await githubApiRequest('POST', `/repos/${owner}/${repo}/labels`, authToken, {
|
|
258
|
-
name: label.name,
|
|
259
|
-
color: label.color,
|
|
260
|
-
description: label.description,
|
|
261
|
-
});
|
|
262
|
-
if (!response.ok && response.status !== 422) {
|
|
263
|
-
// 422 means label already exists, which is fine
|
|
264
|
-
await handleApiError(response, `creating label '${label.name}'`, 'create_label');
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Ensure all required Haystack labels exist in the repo
|
|
269
|
-
*/
|
|
270
|
-
export async function ensureHaystackLabels(owner, repo, labelNames, token) {
|
|
271
|
-
for (const name of labelNames) {
|
|
272
|
-
const labelInfo = HAYSTACK_LABELS[name];
|
|
273
|
-
if (!labelInfo)
|
|
274
|
-
continue;
|
|
275
|
-
const exists = await labelExists(owner, repo, name, token);
|
|
276
|
-
if (!exists) {
|
|
277
|
-
await createLabel(owner, repo, labelInfo, token);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
// ============================================================================
|
|
282
|
-
// Review request operations
|
|
283
|
-
// ============================================================================
|
|
284
|
-
/**
|
|
285
|
-
* Request reviewers on a pull request
|
|
286
|
-
*/
|
|
287
|
-
export async function requestReviewers(owner, repo, prNumber, reviewers, token) {
|
|
288
|
-
const authToken = await resolveToken(token, { owner, repo });
|
|
289
|
-
const response = await githubApiRequest('POST', `/repos/${owner}/${repo}/pulls/${prNumber}/requested_reviewers`, authToken, { reviewers });
|
|
290
|
-
if (!response.ok) {
|
|
291
|
-
await handleApiError(response, `requesting reviewers on PR #${prNumber}`, 'request_reviewers');
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
// ============================================================================
|
|
295
|
-
// User operations
|
|
296
|
-
// ============================================================================
|
|
297
|
-
/**
|
|
298
|
-
* Get the authenticated user's info
|
|
299
|
-
*/
|
|
300
|
-
export async function getCurrentUser(token) {
|
|
301
|
-
const authToken = await resolveToken(token);
|
|
302
|
-
const response = await githubApiRequest('GET', '/user', authToken);
|
|
303
|
-
if (!response.ok) {
|
|
304
|
-
await handleApiError(response, 'getting current user', 'get_current_user');
|
|
305
|
-
}
|
|
306
|
-
return response.json();
|
|
307
|
-
}
|
|
308
|
-
export async function getRepositoryAccess(owner, repo, token) {
|
|
309
|
-
const authToken = await resolveToken(token, { owner, repo });
|
|
310
|
-
const response = await githubApiRequest('GET', `/repos/${owner}/${repo}`, authToken);
|
|
311
|
-
if (!response.ok) {
|
|
312
|
-
await handleApiError(response, `getting repository access for ${owner}/${repo}`, 'get_repository_access');
|
|
313
|
-
}
|
|
314
|
-
return response.json();
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* Check if user has push access to a repo
|
|
318
|
-
*/
|
|
319
|
-
export async function hasPushAccess(owner, repo, token) {
|
|
320
|
-
const data = await getRepositoryAccess(owner, repo, token);
|
|
321
|
-
return data.permissions?.admin === true
|
|
322
|
-
|| data.permissions?.maintain === true
|
|
323
|
-
|| data.permissions?.push === true;
|
|
324
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pending submit state persistence.
|
|
3
|
-
*
|
|
4
|
-
* Manages `.haystack/pending-submit.json` in the git root directory.
|
|
5
|
-
* This file persists across CLI sessions so that analysis results
|
|
6
|
-
* can be retrieved after disconnection.
|
|
7
|
-
*/
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
9
|
-
import { join } from 'path';
|
|
10
|
-
import { findGitRoot } from './git.js';
|
|
11
|
-
// ============================================================================
|
|
12
|
-
// Constants
|
|
13
|
-
// ============================================================================
|
|
14
|
-
const PENDING_FILE = 'pending-submit.json';
|
|
15
|
-
const HAYSTACK_DIR = '.haystack';
|
|
16
|
-
const STALE_THRESHOLD_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
17
|
-
// ============================================================================
|
|
18
|
-
// Functions
|
|
19
|
-
// ============================================================================
|
|
20
|
-
function getPendingPath(gitRoot) {
|
|
21
|
-
const root = gitRoot || findGitRoot();
|
|
22
|
-
if (!root)
|
|
23
|
-
return null;
|
|
24
|
-
return join(root, HAYSTACK_DIR, PENDING_FILE);
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Save pending submit state to disk.
|
|
28
|
-
*/
|
|
29
|
-
export function savePendingSubmit(state, gitRoot) {
|
|
30
|
-
const root = gitRoot || findGitRoot();
|
|
31
|
-
if (!root)
|
|
32
|
-
throw new Error('Not a git repository');
|
|
33
|
-
const dir = join(root, HAYSTACK_DIR);
|
|
34
|
-
if (!existsSync(dir)) {
|
|
35
|
-
mkdirSync(dir, { recursive: true });
|
|
36
|
-
}
|
|
37
|
-
const filePath = join(dir, PENDING_FILE);
|
|
38
|
-
writeFileSync(filePath, JSON.stringify(state, null, 2));
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Load pending submit state from disk.
|
|
42
|
-
* Returns null if no pending state exists or if it's stale.
|
|
43
|
-
*/
|
|
44
|
-
export function loadPendingSubmit(gitRoot) {
|
|
45
|
-
const filePath = getPendingPath(gitRoot);
|
|
46
|
-
if (!filePath || !existsSync(filePath))
|
|
47
|
-
return null;
|
|
48
|
-
try {
|
|
49
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
50
|
-
const state = JSON.parse(content);
|
|
51
|
-
// Check version
|
|
52
|
-
if (state.version !== 1)
|
|
53
|
-
return null;
|
|
54
|
-
// Auto-mark stale entries
|
|
55
|
-
if (state.status === 'polling') {
|
|
56
|
-
const age = Date.now() - new Date(state.submittedAt).getTime();
|
|
57
|
-
if (age > STALE_THRESHOLD_MS) {
|
|
58
|
-
state.status = 'stale';
|
|
59
|
-
savePendingSubmit(state, gitRoot);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return state;
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Clear pending submit state (delete the file).
|
|
70
|
-
*/
|
|
71
|
-
export function clearPendingSubmit(gitRoot) {
|
|
72
|
-
const filePath = getPendingPath(gitRoot);
|
|
73
|
-
if (filePath && existsSync(filePath)) {
|
|
74
|
-
unlinkSync(filePath);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Update the status and optionally the analysis result of a pending submit.
|
|
79
|
-
*/
|
|
80
|
-
export function updatePendingSubmit(updates, gitRoot) {
|
|
81
|
-
const state = loadPendingSubmit(gitRoot);
|
|
82
|
-
if (!state)
|
|
83
|
-
return;
|
|
84
|
-
Object.assign(state, updates);
|
|
85
|
-
savePendingSubmit(state, gitRoot);
|
|
86
|
-
}
|
package/dist/utils/pr-ref.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared PR-identifier parsing for every command that takes a PR ref.
|
|
3
|
-
*
|
|
4
|
-
* One grammar, one error message, one repo-inference path. This used to be
|
|
5
|
-
* duplicated per command, and the copies drifted: different "accepted formats"
|
|
6
|
-
* lists (none mentioned the `#123` form the regex accepted), and one copy let
|
|
7
|
-
* a raw git error escape where the others gave a friendly hint. Commands must
|
|
8
|
-
* import from here rather than reimplementing the grammar.
|
|
9
|
-
*
|
|
10
|
-
* Accepted formats, in the order they are tried:
|
|
11
|
-
* https://github.com/owner/repo/pull/123 GitHub URL
|
|
12
|
-
* owner/repo#123 Fully qualified
|
|
13
|
-
* 123 or #123 PR number (repo inferred from the `origin` remote)
|
|
14
|
-
*/
|
|
15
|
-
import { parseRemoteUrl } from './git.js';
|
|
16
|
-
/** The accepted-formats block used in parse errors and help text. */
|
|
17
|
-
export function acceptedPrRefFormats(commandName) {
|
|
18
|
-
return (` haystack ${commandName} 123\n` +
|
|
19
|
-
` haystack ${commandName} #123\n` +
|
|
20
|
-
` haystack ${commandName} owner/repo#123\n` +
|
|
21
|
-
` haystack ${commandName} https://github.com/owner/repo/pull/123`);
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Parse a PR identifier. `commandName` is interpolated into error messages so
|
|
25
|
-
* the agent can copy a corrected invocation verbatim (e.g. "triage",
|
|
26
|
-
* "pr get", "traces list").
|
|
27
|
-
*/
|
|
28
|
-
export function parsePrRef(identifier, commandName) {
|
|
29
|
-
const urlMatch = identifier.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/);
|
|
30
|
-
if (urlMatch) {
|
|
31
|
-
return { owner: urlMatch[1], repo: urlMatch[2], prNumber: parseInt(urlMatch[3], 10) };
|
|
32
|
-
}
|
|
33
|
-
// owner/repo#123 — repo segment may not contain `/` (a stray extra segment
|
|
34
|
-
// used to silently become part of the repo name and 404 downstream).
|
|
35
|
-
const qualifiedMatch = identifier.match(/^([^/#\s]+)\/([^/#\s]+)#(\d+)$/);
|
|
36
|
-
if (qualifiedMatch) {
|
|
37
|
-
return { owner: qualifiedMatch[1], repo: qualifiedMatch[2], prNumber: parseInt(qualifiedMatch[3], 10) };
|
|
38
|
-
}
|
|
39
|
-
const numberMatch = identifier.match(/^#?(\d+)$/);
|
|
40
|
-
if (numberMatch) {
|
|
41
|
-
const prNumber = parseInt(numberMatch[1], 10);
|
|
42
|
-
try {
|
|
43
|
-
const remote = parseRemoteUrl('origin');
|
|
44
|
-
return { owner: remote.owner, repo: remote.repo, prNumber };
|
|
45
|
-
}
|
|
46
|
-
catch (err) {
|
|
47
|
-
throw new Error(`Cannot determine the repository for PR #${prNumber} ` +
|
|
48
|
-
`(${err instanceof Error ? err.message : String(err)}).\n` +
|
|
49
|
-
`Run from inside a checkout with an "origin" remote, or use the full format:\n` +
|
|
50
|
-
` haystack ${commandName} owner/repo#${prNumber}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
throw new Error(`Invalid PR identifier: "${identifier}"\n\n` +
|
|
54
|
-
`Accepted formats:\n${acceptedPrRefFormats(commandName)}\n\n` +
|
|
55
|
-
`See \`haystack ${commandName.split(' ')[0]} --help\` for details.`);
|
|
56
|
-
}
|