@haystackeditor/cli 0.25.1 → 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.
Files changed (97) hide show
  1. package/README.md +39 -455
  2. package/dist/capture/app-config.js +25 -1
  3. package/dist/commands/capture-brief.js +41 -32
  4. package/dist/commands/feedback.js +66 -0
  5. package/dist/commands/init-telemetry.js +53 -7
  6. package/dist/commands/init.js +7 -2
  7. package/dist/commands/lockfile-pin.js +307 -0
  8. package/dist/commands/verify.js +68 -13
  9. package/dist/index.js +27 -923
  10. package/dist/schema.js +4 -10
  11. package/dist/utils/haystack-api.js +0 -36
  12. package/package.json +1 -5
  13. package/schemas/feedback.v1.json +13 -0
  14. package/schemas/pre-verify.v2.json +240 -0
  15. package/schemas/verify-raw.v1.json +1132 -0
  16. package/schemas/verify.v2.json +655 -0
  17. package/dist/assets/hooks/agent-context/detect.ts +0 -316
  18. package/dist/assets/hooks/agent-context/format.ts +0 -100
  19. package/dist/assets/hooks/agent-context/index.ts +0 -41
  20. package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
  21. package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
  22. package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
  23. package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
  24. package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
  25. package/dist/assets/hooks/agent-context/types.ts +0 -58
  26. package/dist/assets/hooks/llm-rules-template.md +0 -59
  27. package/dist/assets/hooks/package-lock.json +0 -598
  28. package/dist/assets/hooks/package.json +0 -12
  29. package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
  30. package/dist/assets/hooks/scripts/post-commit.sh +0 -5
  31. package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
  32. package/dist/assets/hooks/scripts/pre-push.sh +0 -25
  33. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
  34. package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
  35. package/dist/assets/hooks/truncation-checker/index.ts +0 -595
  36. package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
  37. package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
  38. package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
  39. package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
  40. package/dist/assets/skills/map-your-system.md +0 -143
  41. package/dist/assets/skills/submit.md +0 -200
  42. package/dist/commands/ask.js +0 -20
  43. package/dist/commands/cloud-verifier-behaviors.js +0 -218
  44. package/dist/commands/cloud-verifier-data-store-census.js +0 -539
  45. package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
  46. package/dist/commands/cloud-verifier-identity-census.js +0 -4060
  47. package/dist/commands/cloud-verifier-materialization.js +0 -704
  48. package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
  49. package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
  50. package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
  51. package/dist/commands/cloud-verifier-universe.js +0 -10178
  52. package/dist/commands/config.js +0 -549
  53. package/dist/commands/design-verify.js +0 -311
  54. package/dist/commands/dismiss.js +0 -159
  55. package/dist/commands/hooks.js +0 -226
  56. package/dist/commands/inbox.js +0 -137
  57. package/dist/commands/mcp.js +0 -201
  58. package/dist/commands/policy.js +0 -371
  59. package/dist/commands/pr-status.js +0 -207
  60. package/dist/commands/pr.js +0 -105
  61. package/dist/commands/prepare-universe-review.js +0 -1092
  62. package/dist/commands/production-source-deny-policy.js +0 -100
  63. package/dist/commands/request-review.js +0 -74
  64. package/dist/commands/review.js +0 -191
  65. package/dist/commands/rules.js +0 -98
  66. package/dist/commands/scaffold-provisional-universe.js +0 -806
  67. package/dist/commands/setup.js +0 -1170
  68. package/dist/commands/skills.js +0 -447
  69. package/dist/commands/submit.js +0 -745
  70. package/dist/commands/system-map.js +0 -228
  71. package/dist/commands/triage.js +0 -598
  72. package/dist/commands/webhooks.js +0 -241
  73. package/dist/states.js +0 -46
  74. package/dist/tools/detect.js +0 -832
  75. package/dist/triage/astra.js +0 -202
  76. package/dist/triage/prompts.js +0 -188
  77. package/dist/triage/runner.js +0 -200
  78. package/dist/triage/types.js +0 -7
  79. package/dist/utils/action-output.js +0 -26
  80. package/dist/utils/analysis-api.js +0 -416
  81. package/dist/utils/design-verifier-api.js +0 -294
  82. package/dist/utils/design-verifier-history.js +0 -79
  83. package/dist/utils/design-verifier-result.js +0 -424
  84. package/dist/utils/github-api.js +0 -324
  85. package/dist/utils/pending-state.js +0 -86
  86. package/dist/utils/pr-ref.js +0 -56
  87. package/dist/utils/prompter.js +0 -328
  88. package/schemas/action.v1.json +0 -22
  89. package/schemas/ask.v1.json +0 -40
  90. package/schemas/inbox.v1.json +0 -27
  91. package/schemas/pr-status.v1.json +0 -61
  92. package/schemas/pr.v1.json +0 -97
  93. package/schemas/pr.v3.json +0 -45
  94. package/schemas/setup.v1.json +0 -75
  95. package/schemas/submit.v1.json +0 -90
  96. package/schemas/triage.v1.json +0 -103
  97. package/schemas/triage.v2.json +0 -64
@@ -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
- }
@@ -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
- }