@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,416 +0,0 @@
1
- /**
2
- * Haystack Analysis API client.
3
- *
4
- * Polls the result endpoint by PR identifier to check if analysis is ready,
5
- * then fetches detailed results (bug detection + rule violations).
6
- *
7
- * Analysis is triggered automatically by the GitHub App webhook when a PR
8
- * is created or updated — the CLI does not need to trigger it directly.
9
- */
10
- import { classifyHttpError } from './haystack-api.js';
11
- // ============================================================================
12
- // Constants
13
- // ============================================================================
14
- /**
15
- * Analysis API base.
16
- *
17
- * IMPORTANT: this used to point directly at AWS API Gateway. That worked
18
- * when the CLI carried a GitHub token (X-GitHub-Token header to the
19
- * Lambda), but BROKE for `haystack login --headless` users — an
20
- * hsk_live_* token would land on AWS as if it were a GH token, the
21
- * Lambda would 401, and triage / pr / chat / MCP would silently return
22
- * nothing. The right move is to route reads through the auth-worker,
23
- * which:
24
- *
25
- * 1. Knows how to introspect hsk_live tokens (and GH tokens, agent JWTs,
26
- * session cookies) — single normalized auth path.
27
- * 2. Applies the hsk_live scope gate (denyForCliScope) before forwarding
28
- * to AWS, so a repo-scoped CI token can't read other repos' synthesis.
29
- * 3. Adds the right backend token (install token for org-installed reads,
30
- * fallback otherwise) — so private-repo reads work without the CLI
31
- * ever holding a GH token.
32
- *
33
- * Override with HAYSTACK_API_BASE for local dev (e.g., http://localhost:8788
34
- * pointing at a local auth-worker).
35
- */
36
- const ANALYSIS_API_BASE = process.env.HAYSTACK_API_BASE ?? 'https://haystackeditor.com';
37
- /** Build the auth-worker URL for a v3 result file (rating_synthesis.json, etc). */
38
- function v3ResultUrl(owner, repo, prNumber, file) {
39
- // The auth-worker exposes `/api/analysis/{prIdentifier}/{file}` (see
40
- // V3_API_PATTERNS in infra/auth-worker/index.js). prIdentifier format:
41
- // owner/repo#prnum, URL-encoded as %2F + %23.
42
- const prId = encodeURIComponent(`${owner}/${repo}#${prNumber}`);
43
- return `${ANALYSIS_API_BASE}/api/analysis/${prId}${file ? `/${file}` : ''}`;
44
- }
45
- // ============================================================================
46
- // API helpers
47
- // ============================================================================
48
- /**
49
- * Build headers for an auth-worker-proxied analysis request.
50
- *
51
- * Auth-worker accepts hsk_live_*, GH tokens, agent JWTs, and session
52
- * cookies on `Authorization: Bearer`. The legacy `X-GitHub-Token` header
53
- * targeted AWS directly and is no longer used.
54
- */
55
- function apiHeaders(token) {
56
- const headers = {
57
- 'Cache-Control': 'no-cache',
58
- 'User-Agent': 'Haystack-CLI',
59
- };
60
- if (token) {
61
- headers['Authorization'] = `Bearer ${token}`;
62
- }
63
- return headers;
64
- }
65
- /**
66
- * Trigger a fresh full analysis for a PR via the auth-worker
67
- * (`/api/analysis-trigger/{owner}/{repo}/{pr}`). Requires auth — triggers
68
- * start a real analysis run. Returns the request id to wait on.
69
- */
70
- export async function triggerAnalysis(owner, repo, prNumber, token, headSha) {
71
- const url = `${ANALYSIS_API_BASE}/api/analysis-trigger/${owner}/${repo}/${prNumber}`;
72
- let response;
73
- try {
74
- // Force semantics ride on a server-set header at the trigger route.
75
- // commitSha matters for dedup: without it, ingestion matches this
76
- // request onto any in-flight run for an OLDER head and the CLI would
77
- // wait on (and render) stale results.
78
- response = await fetch(url, {
79
- method: 'POST',
80
- headers: { ...apiHeaders(token), 'Content-Type': 'application/json' },
81
- body: JSON.stringify(headSha ? { commitSha: headSha } : {}),
82
- });
83
- }
84
- catch (err) {
85
- return { status: 'error', message: err.message };
86
- }
87
- let data = {};
88
- let parseFailed = false;
89
- try {
90
- data = await response.json();
91
- }
92
- catch {
93
- // Recorded, not swallowed: for non-OK responses the status-code handling
94
- // below surfaces the failure; a 2xx with an unparseable body is its own
95
- // explicit error (never "empty data") so callers can't mistake a
96
- // malformed response for a normal one.
97
- parseFailed = true;
98
- }
99
- if (parseFailed && response.ok) {
100
- return { status: 'error', message: `Trigger endpoint returned invalid JSON (HTTP ${response.status})` };
101
- }
102
- if (response.status === 429) {
103
- return {
104
- status: 'rate_limited',
105
- message: data.message || 'Too many analysis requests. Please wait before trying again.',
106
- retryAfterSeconds: data.retryAfter,
107
- };
108
- }
109
- if (response.status === 401 || response.status === 403) {
110
- return {
111
- status: 'denied',
112
- message: data.message || data.error || `HTTP ${response.status}`,
113
- };
114
- }
115
- if (!response.ok) {
116
- return { status: 'error', message: data.message || data.error || `HTTP ${response.status}` };
117
- }
118
- if (!data.request_id) {
119
- return { status: 'error', message: 'Trigger response did not include a request id' };
120
- }
121
- if (data.status === 'already_processing') {
122
- return { status: 'already_processing', requestId: data.request_id };
123
- }
124
- return { status: 'started', requestId: data.request_id };
125
- }
126
- // Terminal states of the v3 status vocabulary (map_status_for_frontend in
127
- // haystack-v3-request-status). Everything else — QUEUED, PROCESSING,
128
- // CLONING, PREPARING, ANALYZING, not_found (row lag), and any state a newer
129
- // backend introduces — reads as pending: misclassifying a healthy
130
- // in-progress state as an error would abort the wait.
131
- const TERMINAL_STATUSES = {
132
- COMPLETED: 'COMPLETED',
133
- FAILED: 'FAILED',
134
- TIMED_OUT: 'FAILED',
135
- SUPERSEDED: 'SUPERSEDED',
136
- };
137
- /**
138
- * Poll the status of a triggered run by request id. The PR-level result
139
- * endpoint can't distinguish a fresh re-run on the same head from the
140
- * previous completed analysis — the request id can.
141
- */
142
- export async function fetchTriggerStatus(requestId, token) {
143
- const url = `${ANALYSIS_API_BASE}/api/analysis-trigger/status/${requestId}`;
144
- try {
145
- const response = await fetch(url, { headers: apiHeaders(token) });
146
- // The status Lambda returns not_found as a 404 WITH a JSON body — parse
147
- // before rejecting on status code so "not yet written" is distinguishable
148
- // from a real transport failure.
149
- let data = {};
150
- try {
151
- data = await response.json();
152
- }
153
- catch {
154
- return { status: 'error', message: `HTTP ${response.status}` };
155
- }
156
- const status = typeof data.status === 'string' ? data.status : '';
157
- const terminal = TERMINAL_STATUSES[status];
158
- if (terminal) {
159
- return { status: terminal, message: data.message };
160
- }
161
- // Only successful responses (plus the documented 404 not_found row-lag
162
- // case) read as pending — a 5xx carrying {"status":"error"} must count
163
- // toward the error budget, not spin the wait for the full timeout.
164
- if (response.ok || (response.status === 404 && status === 'not_found')) {
165
- return { status: 'pending', message: data.message };
166
- }
167
- return { status: 'error', message: data.message || `HTTP ${response.status}` };
168
- }
169
- catch (err) {
170
- return { status: 'error', message: err.message };
171
- }
172
- }
173
- /**
174
- * Check if analysis results are available for a PR by polling the result endpoint.
175
- * Returns 'ready' if complete, 'pending' if not yet available, or 'error' for server failures.
176
- */
177
- export async function checkAnalysisReady(owner, repo, prNumber, token) {
178
- const url = v3ResultUrl(owner, repo, prNumber);
179
- let response;
180
- try {
181
- response = await fetch(url, {
182
- headers: apiHeaders(token),
183
- });
184
- }
185
- catch (err) {
186
- return { status: 'error', message: err.message };
187
- }
188
- if (response.status === 404)
189
- return { status: 'pending' };
190
- if (!response.ok) {
191
- // Classified, actionable message — a bare "HTTP 403" used to send agents
192
- // into a retry loop when the fix was `haystack login`.
193
- return { status: 'error', message: (await classifyHttpError(response, 'Analysis status check')).message };
194
- }
195
- let data;
196
- try {
197
- data = await response.json();
198
- }
199
- catch {
200
- return { status: 'error', message: 'Invalid JSON response' };
201
- }
202
- return data.status === 'COMPLETED' ? { status: 'ready' } : { status: 'pending' };
203
- }
204
- /**
205
- * Fetch analysis results once complete. Used by `haystack submit`'s
206
- * wait-for-triage loop to render a verdict block.
207
- *
208
- * Verdict semantics — three distinct signals, never conflated:
209
- *
210
- * state = is the analysis itself clean?
211
- * → 'good-to-merge' iff analysisVerdict === 'clean'
212
- * AND !needsHumanReview. canAutoMerge is NOT
213
- * the source of truth (it only means "analysis
214
- * would let this through" — review policy,
215
- * grace period, or a missing auto-merge label
216
- * can still block).
217
- * Falls back to haystackRating ≥ 5 only when
218
- * analysisVerdict is absent (legacy payloads).
219
- * needsHumanReview = preserved on the return shape so callers can
220
- * distinguish "clean + needs human" from clean.
221
- * autoMerged = was the PR ACTUALLY merged? Read from the
222
- * GitHub REST endpoint, not from anything in
223
- * the synthesis. canAutoMerge is eligibility,
224
- * not outcome — never claim "auto-merged" from
225
- * the analysis side.
226
- *
227
- * Sources from the v3 rating-synthesis path (auth-worker-proxied — so
228
- * hsk_live tokens, GH tokens, agent JWTs, and cookies all work, and the
229
- * scope gate runs). The legacy v2 bug-detection / rule-violation /
230
- * merge-decision JSONs are no longer queried (no auth-worker proxy).
231
- */
232
- async function isPrMerged(owner, repo, prNumber, token) {
233
- // Authoritative merge check. We hit the same auth-worker proxy as the
234
- // rest of the CLI so hsk_live / OAuth / install tokens all work, AND
235
- // scope enforcement happens here too (a token not scoped to this repo
236
- // would 403; we treat that as "unknown → not merged" rather than
237
- // crashing the submit flow).
238
- if (!token)
239
- return false;
240
- try {
241
- const base = process.env.HAYSTACK_API_BASE ?? 'https://haystackeditor.com';
242
- const res = await fetch(`${base}/api/github/repos/${owner}/${repo}/pulls/${prNumber}`, {
243
- headers: {
244
- Accept: 'application/vnd.github+json',
245
- Authorization: `Bearer ${token}`,
246
- 'User-Agent': 'Haystack-CLI',
247
- },
248
- });
249
- if (!res.ok)
250
- return false;
251
- const data = (await res.json());
252
- return data.merged === true;
253
- }
254
- catch {
255
- return false;
256
- }
257
- }
258
- export async function fetchAnalysisResults(owner, repo, prNumber, token) {
259
- const reviewUrl = `https://haystackeditor.com/review/${owner}/${repo}/${prNumber}`;
260
- const [synthesis, autoMerged] = await Promise.all([
261
- fetchRatingSynthesis(owner, repo, prNumber, token),
262
- isPrMerged(owner, repo, prNumber, token),
263
- ]);
264
- if (!synthesis) {
265
- return {
266
- state: 'needs-input',
267
- bugCount: 0,
268
- warningCount: 0,
269
- ruleViolationCount: 0,
270
- summary: 'Analysis is ready but synthesis could not be loaded.',
271
- issues: [],
272
- reviewUrl,
273
- autoMerged,
274
- };
275
- }
276
- const display = synthesis.synthesisDisplay ?? [];
277
- const issues = display.map((d) => ({
278
- file: 'unknown', // synthesis findings don't always carry a path; the rich data lives on the feed
279
- message: d.summary || d.detail || `Finding (${d.category})`,
280
- severity: 'warning',
281
- // Map the synthesis source onto the verdict's two-bucket model.
282
- // Anything other than an explicit rule violation lands on bug-detection.
283
- source: d.source === 'rule-violation' ? 'rule-violation' : 'bug-detection',
284
- }));
285
- const bugCount = issues.filter((i) => i.source === 'bug-detection').length;
286
- const ruleViolationCount = issues.filter((i) => i.source === 'rule-violation').length;
287
- const warningCount = issues.length;
288
- // Three discrete signals, NEVER conflated:
289
- // verdictKind : 'clean' | 'has-issues' | 'needs-review'
290
- // (legacy payloads without analysisVerdict fall back
291
- // to haystackRating ≥ 5 → 'clean')
292
- // needsHumanReview: true if review policy demands a human, OR if the
293
- // orchestrator escalated verdict to 'needs-review'
294
- // (which carries the same semantic).
295
- // analysisIsClean : true iff verdict is 'clean' OR 'needs-review'
296
- // (both mean: no bugs/violations were found; the
297
- // review-policy gate is the separate axis).
298
- //
299
- // good-to-merge = analysis clean AND no human required.
300
- // 'has-issues' → state 'needs-input'.
301
- // 'needs-review' → state 'needs-input' + needsHumanReview true.
302
- // 'clean' + needsHumanReview → same as 'needs-review' (defensive — the
303
- // orchestrator should already have escalated, but we handle the case
304
- // where it didn't).
305
- const verdictKind = synthesis.analysisVerdict
306
- ?? (synthesis.haystackRating >= 5 ? 'clean' : 'has-issues');
307
- const analysisIsClean = verdictKind === 'clean' || verdictKind === 'needs-review';
308
- const needsHumanReview = synthesis.needsHumanReview === true || verdictKind === 'needs-review';
309
- const isGoodToMerge = analysisIsClean && !needsHumanReview;
310
- const state = isGoodToMerge ? 'good-to-merge' : 'needs-input';
311
- let summary;
312
- if (state === 'good-to-merge') {
313
- summary = autoMerged
314
- ? 'No blocking issues. PR was merged.'
315
- : 'No blocking issues. Safe to merge.';
316
- }
317
- else if (analysisIsClean && needsHumanReview) {
318
- summary = 'Analysis is clean, but review policy requires a human reviewer.';
319
- }
320
- else {
321
- const parts = [];
322
- if (bugCount > 0)
323
- parts.push(`${bugCount} finding(s)`);
324
- if (ruleViolationCount > 0)
325
- parts.push(`${ruleViolationCount} rule violation(s)`);
326
- summary = parts.length > 0
327
- ? parts.join(', ') + ' found.'
328
- : `Rating ${synthesis.haystackRating}/5 — see the feed for details.`;
329
- }
330
- return {
331
- state,
332
- bugCount,
333
- warningCount,
334
- ruleViolationCount,
335
- summary,
336
- issues,
337
- reviewUrl,
338
- autoMerged,
339
- needsHumanReview,
340
- };
341
- }
342
- /**
343
- * Null means "artifact does not exist (yet)" — 404s and transient
344
- * network/parse failures. Auth failures (401/403) THROW a classified
345
- * HaystackApiError instead: collapsing them into null used to make an
346
- * expired token read as "not available yet — try again in a moment",
347
- * sending agents into a retry loop when the fix was `haystack login`.
348
- */
349
- export async function fetchAutoFixManifest(owner, repo, prNumber, token) {
350
- const url = v3ResultUrl(owner, repo, prNumber, 'auto-fix-manifest.json');
351
- try {
352
- const response = await fetch(url, {
353
- headers: apiHeaders(token),
354
- });
355
- if (response.status === 401 || response.status === 403) {
356
- throw await classifyHttpError(response, 'Auto-fix manifest fetch');
357
- }
358
- if (!response.ok)
359
- return null;
360
- const data = await response.json();
361
- if (data.download_url) {
362
- const s3Resp = await fetch(data.download_url, {
363
- headers: { 'User-Agent': 'Haystack-CLI' },
364
- });
365
- if (!s3Resp.ok)
366
- return null;
367
- return await s3Resp.json();
368
- }
369
- return data;
370
- }
371
- catch (err) {
372
- if (err instanceof Error && err.name === 'HaystackApiError')
373
- throw err;
374
- // Transient network/parse failure — same "not available" semantics as 404.
375
- return null;
376
- }
377
- }
378
- /**
379
- * Null means "synthesis does not exist (yet)". Auth failures (401/403) THROW
380
- * a classified HaystackApiError — see fetchAutoFixManifest for why.
381
- */
382
- export async function fetchRatingSynthesis(owner, repo, prNumber, token) {
383
- // Route through the auth-worker (/api/analysis/{prId}/rating_synthesis.json)
384
- // so hsk_live tokens are introspected and the scope gate applies — see
385
- // ANALYSIS_API_BASE comment above.
386
- const url = v3ResultUrl(owner, repo, prNumber, 'rating_synthesis.json');
387
- try {
388
- const response = await fetch(url, {
389
- headers: apiHeaders(token),
390
- });
391
- if (response.status === 401 || response.status === 403) {
392
- throw await classifyHttpError(response, 'Rating synthesis fetch');
393
- }
394
- if (!response.ok)
395
- return null;
396
- const data = await response.json();
397
- // Handle presigned URL redirect (Lambda returns { download_url } for large files)
398
- // Presigned S3 URLs include auth in query params — do NOT forward the GitHub token
399
- // (S3 rejects requests with both presigned auth and an Authorization/custom header)
400
- if (data.download_url) {
401
- const s3Resp = await fetch(data.download_url, {
402
- headers: { 'User-Agent': 'Haystack-CLI' },
403
- });
404
- if (!s3Resp.ok)
405
- return null;
406
- return await s3Resp.json();
407
- }
408
- return data;
409
- }
410
- catch (err) {
411
- if (err instanceof Error && err.name === 'HaystackApiError')
412
- throw err;
413
- // Transient network/parse failure — same "not available" semantics as 404.
414
- return null;
415
- }
416
- }
@@ -1,54 +0,0 @@
1
- /**
2
- * Configuration file utilities
3
- */
4
- import * as fs from 'fs/promises';
5
- import * as path from 'path';
6
- const CONFIG_FILENAME = '.haystack.json';
7
- /**
8
- * Find the config file by walking up the directory tree
9
- */
10
- export async function findConfigPath(startDir = process.cwd()) {
11
- let dir = startDir;
12
- for (;;) {
13
- const configPath = path.join(dir, CONFIG_FILENAME);
14
- try {
15
- await fs.access(configPath);
16
- return configPath;
17
- }
18
- catch {
19
- // Not found, try parent
20
- }
21
- const parent = path.dirname(dir);
22
- if (parent === dir) {
23
- // Reached root
24
- return null;
25
- }
26
- dir = parent;
27
- }
28
- }
29
- /**
30
- * Load and parse the config file
31
- */
32
- export async function loadConfig(configPath) {
33
- const resolvedPath = configPath || (await findConfigPath());
34
- if (!resolvedPath) {
35
- return null;
36
- }
37
- try {
38
- const content = await fs.readFile(resolvedPath, 'utf-8');
39
- return JSON.parse(content);
40
- }
41
- catch (err) {
42
- throw new Error(`Failed to parse ${resolvedPath}: ${err.message}`);
43
- }
44
- }
45
- /**
46
- * Get the project root (directory containing .haystack.json or current dir)
47
- */
48
- export async function getProjectRoot() {
49
- const configPath = await findConfigPath();
50
- if (configPath) {
51
- return path.dirname(configPath);
52
- }
53
- return process.cwd();
54
- }