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