@haystackeditor/cli 0.26.0 → 0.27.1
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 +0 -8
- package/dist/capture/app-config.js +7 -6
- package/dist/commands/capture-brief.js +4 -3
- package/dist/commands/capture-contract.js +4 -4
- package/dist/commands/crawl-report.js +196 -0
- package/dist/commands/db-profile-upload.js +3 -3
- package/dist/commands/feedback.js +1 -1
- package/dist/commands/init-telemetry.js +64 -14
- package/dist/commands/init.js +96 -39
- package/dist/commands/lockfile-pin.js +4 -8
- package/dist/commands/telemetry-token.js +3 -3
- package/dist/commands/tokens.js +4 -4
- package/dist/commands/verify-explore.js +3 -3
- package/dist/commands/verify-history.js +2 -2
- package/dist/commands/verify-onboarding.js +13 -16
- package/dist/commands/verify-precompute.js +3 -4
- package/dist/commands/verify.js +9 -151
- package/dist/index.js +28 -45
- package/dist/utils/haystack-api.js +8 -0
- package/package.json +1 -1
- package/dist/commands/status.js +0 -35
- package/dist/types.js +0 -326
- package/dist/utils/config.js +0 -54
package/dist/commands/init.js
CHANGED
|
@@ -56,7 +56,7 @@ export const AGENT_NOTE = [
|
|
|
56
56
|
' `haystack verify answer <question-id> <choice>`.',
|
|
57
57
|
'- Read its short report. `--json` prints the same report as data; `--raw` prints the crawl\'s whole record (large), for scripts;',
|
|
58
58
|
' `haystack verify --help` explains the output and exit codes.',
|
|
59
|
-
'- If `haystack` is not installed, run each command as `npx -y @haystackeditor/cli@latest <command>`.',
|
|
59
|
+
'- If `haystack` is not installed, run each command as `npx -y -p @haystackeditor/cli@latest haystack <command>`.',
|
|
60
60
|
'- If Haystack fails, confuses you or gets in your way, say so in a sentence with',
|
|
61
61
|
' `haystack feedback "<what happened>"` and keep going: the Haystack team sees it at once.',
|
|
62
62
|
NOTE_END,
|
|
@@ -299,6 +299,33 @@ export function planInit(gitRoot) {
|
|
|
299
299
|
const notices = [];
|
|
300
300
|
return { changes: [...planStopHook(gitRoot, notices), ...planAgentNotes(gitRoot, notices)], notices };
|
|
301
301
|
}
|
|
302
|
+
/** Makes `changes` all or nothing: one that fails (an install that does not end as planned) restores every file they
|
|
303
|
+
* touched (bytes, not text: a lockfile can be binary), so no edit is left referencing what was never installed. */
|
|
304
|
+
function applyAll(gitRoot, changes, say) {
|
|
305
|
+
const before = new Map();
|
|
306
|
+
for (const planned of changes) {
|
|
307
|
+
for (const path of [planned.target, ...(planned.run?.touches ?? [])]) {
|
|
308
|
+
if (!before.has(path))
|
|
309
|
+
before.set(path, existsSync(path) ? readFileSync(path) : null);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
for (const planned of changes) {
|
|
314
|
+
applyChange(gitRoot, planned);
|
|
315
|
+
say(chalk.green(`✓ ${planned.action === 'create' ? 'Created' : planned.action === 'delete' ? 'Deleted' : 'Updated'} ${planned.path}`));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
for (const [path, content] of before) {
|
|
320
|
+
if (content !== null)
|
|
321
|
+
writeFileSync(path, content);
|
|
322
|
+
else if (existsSync(path))
|
|
323
|
+
unlinkSync(path);
|
|
324
|
+
}
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const errorText = (error) => (error instanceof Error ? error.message : String(error));
|
|
302
329
|
function applyChange(gitRoot, planned) {
|
|
303
330
|
const inside = relative(gitRoot, planned.target);
|
|
304
331
|
if (!inside.startsWith('..') && !isAbsolute(inside))
|
|
@@ -362,19 +389,35 @@ export async function initCommand(options) {
|
|
|
362
389
|
// Read before anything else, so notes that cannot be used stop init with nothing done.
|
|
363
390
|
const notes = options.notes === undefined ? null : readNotes(options.notes);
|
|
364
391
|
const plan = planInit(gitRoot);
|
|
365
|
-
// Telemetry is part of onboarding (CAPTURE-V1 rule 8): planned on every run; a missing answer stops only it.
|
|
392
|
+
// Telemetry is part of onboarding (CAPTURE-V1 rule 8): planned on every run; a missing answer stops only it. It never fails
|
|
393
|
+
// init: whatever goes wrong with it (a file it cannot read, a package manager that fails, Haystack not answering) puts its
|
|
394
|
+
// own files back and leaves it manual with what happened, and the rest of init goes on.
|
|
366
395
|
const telemetryModule = await import('./init-telemetry.js');
|
|
367
396
|
const telemetryOptions = { app: options.app, origins: options.origin ?? [], consent: options.consent, urlRewrites: options.urlRewrites };
|
|
368
|
-
let
|
|
397
|
+
let telemetryFailure = null;
|
|
398
|
+
let telemetryPlan = telemetryModule.emptyTelemetryPlan();
|
|
399
|
+
try {
|
|
400
|
+
telemetryPlan = await telemetryModule.planTelemetry(gitRoot, repository, telemetryOptions);
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
telemetryFailure = errorText(error);
|
|
404
|
+
}
|
|
369
405
|
const telemetryFrom = plan.changes.length;
|
|
370
406
|
plan.changes.push(...telemetryPlan.changes);
|
|
371
407
|
plan.notices.push(...telemetryPlan.notices);
|
|
372
408
|
let notesWritten = null;
|
|
373
409
|
let telemetry = null;
|
|
410
|
+
/** Telemetry did not happen: its changes leave the plan (none of them was made), and the report says why. */
|
|
411
|
+
const telemetryFailed = (error) => {
|
|
412
|
+
telemetryFailure = errorText(error);
|
|
413
|
+
plan.changes.splice(telemetryFrom);
|
|
414
|
+
say(chalk.yellow(`Telemetry was not set up: ${telemetryFailure} The rest of init goes on; the telemetry report says what to do.`));
|
|
415
|
+
};
|
|
374
416
|
// Before the apply (a preview, or no login), the report is the preview: what telemetry will do, `received` unchecked (null).
|
|
375
|
-
const report = () =>
|
|
417
|
+
const report = () => (telemetryFailure !== null ? telemetryModule.failedTelemetry(telemetryPlan, telemetryFailure)
|
|
418
|
+
: telemetry ?? telemetryModule.previewTelemetry(telemetryPlan, repository, notes));
|
|
376
419
|
const showPreview = () => {
|
|
377
|
-
const preview =
|
|
420
|
+
const preview = report();
|
|
378
421
|
for (const line of telemetryModule.formatTelemetry(preview.telemetry, preview.steps))
|
|
379
422
|
say(line);
|
|
380
423
|
say('');
|
|
@@ -424,44 +467,48 @@ export async function initCommand(options) {
|
|
|
424
467
|
return;
|
|
425
468
|
}
|
|
426
469
|
}
|
|
427
|
-
//
|
|
428
|
-
|
|
470
|
+
// Init's own changes (the agent instructions, the Stop hook) first, all or nothing: a failure there is init's.
|
|
471
|
+
try {
|
|
472
|
+
applyAll(gitRoot, plan.changes.slice(0, telemetryFrom), say);
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
throw new Error(`${errorText(error)} Every file init changed was restored; nothing was changed.`);
|
|
476
|
+
}
|
|
477
|
+
// Then telemetry, all or nothing on its own. Browser capture's registration (rule 8b), which the preview announced, is
|
|
478
|
+
// checked with Haystack first, and when the files need a new key the telemetry changes are planned again with it (or
|
|
479
|
+
// without capture's, when it is refused).
|
|
429
480
|
const approved = options.yes === true || plan.changes.length > 0;
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
for (const planned of plan.changes) {
|
|
441
|
-
for (const path of [planned.target, ...(planned.run?.touches ?? [])]) {
|
|
442
|
-
if (!before.has(path))
|
|
443
|
-
before.set(path, existsSync(path) ? readFileSync(path) : null);
|
|
481
|
+
let capture = null;
|
|
482
|
+
if (telemetryFailure === null) {
|
|
483
|
+
try {
|
|
484
|
+
capture = telemetryPlan.captureRegistration
|
|
485
|
+
? await telemetryModule.prepareCapture(telemetryPlan.captureRegistration, token, say, approved) : null;
|
|
486
|
+
if (capture?.replan) {
|
|
487
|
+
telemetryPlan = await telemetryModule.planTelemetry(gitRoot, repository, { ...telemetryOptions, registration: capture.replan });
|
|
488
|
+
plan.changes.splice(telemetryFrom, plan.changes.length - telemetryFrom, ...telemetryPlan.changes);
|
|
489
|
+
}
|
|
490
|
+
applyAll(gitRoot, plan.changes.slice(telemetryFrom), say);
|
|
444
491
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
492
|
+
catch (error) {
|
|
493
|
+
try {
|
|
494
|
+
await capture?.rollback(say);
|
|
495
|
+
}
|
|
496
|
+
catch (rollbackError) {
|
|
497
|
+
say(chalk.yellow(`Capture's registration could not be put back: ${errorText(rollbackError)}`));
|
|
498
|
+
}
|
|
499
|
+
telemetryFailed(error);
|
|
450
500
|
}
|
|
451
501
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
502
|
+
if (telemetryFailure === null && capture) {
|
|
503
|
+
try {
|
|
504
|
+
const captureFollowUp = await capture.finish(say);
|
|
505
|
+
if (captureFollowUp)
|
|
506
|
+
telemetryPlan.capture = telemetryModule.withCaptureStep(telemetryPlan.capture, captureFollowUp);
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
telemetryPlan.capture = telemetryModule.withCaptureStep(telemetryPlan.capture, `Run \`haystack init\` again: Haystack did not take the app's origins (${errorText(error)}).`);
|
|
458
510
|
}
|
|
459
|
-
await capture?.rollback(say);
|
|
460
|
-
throw new Error(`${error instanceof Error ? error.message : String(error)} Every file init changed was restored; nothing was changed.`);
|
|
461
511
|
}
|
|
462
|
-
const captureFollowUp = await capture?.finish(say);
|
|
463
|
-
if (captureFollowUp)
|
|
464
|
-
telemetryPlan.capture = telemetryModule.withCaptureStep(telemetryPlan.capture, captureFollowUp);
|
|
465
512
|
let onboarding;
|
|
466
513
|
try {
|
|
467
514
|
// The notes first: onboarding plans from the facts it starts with, and new notes are new facts (a new onboarding).
|
|
@@ -485,8 +532,18 @@ export async function initCommand(options) {
|
|
|
485
532
|
reportOnboardingState(onboarding);
|
|
486
533
|
say(formatOnboarding(onboarding));
|
|
487
534
|
say('');
|
|
488
|
-
|
|
489
|
-
|
|
535
|
+
if (telemetryFailure === null) {
|
|
536
|
+
try {
|
|
537
|
+
telemetry = await telemetryModule.telemetryReport(telemetryPlan, { repository, token, onboarding, notes });
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
// What Haystack holds could not be read: the report as previewed (`received` unchecked), and why.
|
|
541
|
+
const preview = telemetryModule.previewTelemetry(telemetryPlan, repository, notes);
|
|
542
|
+
telemetry = { telemetry: preview.telemetry, steps: [...preview.steps, `Run \`haystack init --json\` again to read what telemetry Haystack has received (it could not: ${errorText(error)}).`] };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const shown = report();
|
|
546
|
+
for (const line of telemetryModule.formatTelemetry(shown.telemetry, shown.steps))
|
|
490
547
|
say(line);
|
|
491
548
|
say('');
|
|
492
549
|
if (onboarding.state === 'ready')
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
* locks older versions of packages the CLI also uses):
|
|
5
5
|
* - yarn 1, yarn 4 and bun add only the new descriptors and entries; nothing else moves.
|
|
6
6
|
* - npm hoists the new tree and replaces hoisted packages the app already locks (ws, yaml, zod moved to the CLI's
|
|
7
|
-
* versions)
|
|
7
|
+
* versions), and re-resolves other entries even with `--install-strategy=nested` (39 in fosrl/pangolin): init never
|
|
8
|
+
* runs it (init-telemetry.ts leaves the npm install to the person, who sees the lockfile's diff).
|
|
8
9
|
* - pnpm re-resolves the dependencies of every package the new tree shares with the old one to the newest version in the
|
|
9
10
|
* graph (47 entries in Rallly, from @smithy/* to express), and no flag stops it. So init lets pnpm resolve the new
|
|
10
11
|
* dependency (`--lockfile-only`), keeps every entry the lockfile had, adds the entries only the new dependency
|
|
11
12
|
* reaches (mergePnpmLockfile), and installs exactly that (`pnpm install --frozen-lockfile`, which also proves it).
|
|
12
|
-
* lockfileDrift checks the result for every format init
|
|
13
|
-
* change (and init restores
|
|
13
|
+
* lockfileDrift checks the result for every format init runs, so a package manager that moves anything else fails the
|
|
14
|
+
* change (and init restores telemetry's files, leaving telemetry manual).
|
|
14
15
|
*/
|
|
15
16
|
function unquote(text) {
|
|
16
17
|
if (text.startsWith('\'') && text.endsWith('\''))
|
|
@@ -271,11 +272,6 @@ function entriesOf(format, text, importer) {
|
|
|
271
272
|
}
|
|
272
273
|
}
|
|
273
274
|
}
|
|
274
|
-
else if (format === 'npm') {
|
|
275
|
-
const packages = JSON.parse(text).packages ?? {};
|
|
276
|
-
for (const [key, value] of Object.entries(packages))
|
|
277
|
-
out.set(key === (importer === '.' ? '' : importer) ? IMPORTER : key, JSON.stringify(value));
|
|
278
|
-
}
|
|
279
275
|
else if (format === 'yarn') {
|
|
280
276
|
for (const block of text.split(/\n\s*\n/)) {
|
|
281
277
|
const lines = block.split('\n').filter(line => line.trim() !== '' && !line.startsWith('#'));
|
|
@@ -22,7 +22,7 @@ import { basename, dirname, join, resolve } from 'node:path';
|
|
|
22
22
|
import { createInterface } from 'node:readline/promises';
|
|
23
23
|
import chalk from 'chalk';
|
|
24
24
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
25
|
-
import { classifyHttpError, HaystackApiError } from '../utils/haystack-api.js';
|
|
25
|
+
import { classifyHttpError, HaystackApiError, repoApiPath } from '../utils/haystack-api.js';
|
|
26
26
|
import { gatewayFetch } from './case-batch.js';
|
|
27
27
|
import { TELEMETRY_ENVIRONMENT, TELEMETRY_SERVER_STATUS_PATH, TELEMETRY_TOKEN_PATH, TELEMETRY_TOKEN_SHA256, TELEMETRY_TOKEN_VERSION, } from './capture-contract.js';
|
|
28
28
|
/** GitHub's owner and repository name alphabets. */
|
|
@@ -51,7 +51,7 @@ async function confirmed(question) {
|
|
|
51
51
|
}
|
|
52
52
|
/** GET the repository's server-telemetry status: its active token's hash, the endpoint, the newest flush. */
|
|
53
53
|
export async function readTelemetryServerStatus(repository, token) {
|
|
54
|
-
const path =
|
|
54
|
+
const path = repoApiPath(repository, TELEMETRY_SERVER_STATUS_PATH);
|
|
55
55
|
const response = await gatewayFetch(path, token);
|
|
56
56
|
if (!response.ok)
|
|
57
57
|
throw await classifyHttpError(response, `Haystack API ${TELEMETRY_SERVER_STATUS_PATH}`);
|
|
@@ -199,7 +199,7 @@ async function mintUnderLock(repository, file, authToken, options) {
|
|
|
199
199
|
let answer;
|
|
200
200
|
try {
|
|
201
201
|
const request = { repository, tokenSha256, rotate: options.rotate === true };
|
|
202
|
-
const response = await gatewayFetch(
|
|
202
|
+
const response = await gatewayFetch(repoApiPath(repository, TELEMETRY_TOKEN_PATH), authToken, {
|
|
203
203
|
method: 'POST', body: JSON.stringify(request),
|
|
204
204
|
});
|
|
205
205
|
if (!response.ok)
|
package/dist/commands/tokens.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* - `haystack login --headless [--label NAME] [--scope repo:owner/name,...]`
|
|
6
6
|
* Opens the browser to mint a token via the auth-worker, then writes it
|
|
7
7
|
* locally so subsequent CLI calls auth via Bearer.
|
|
8
|
-
* - `haystack tokens
|
|
8
|
+
* - `haystack auth tokens`
|
|
9
9
|
* Lists tokens on the server side (the local file only stores ONE
|
|
10
10
|
* active token at a time; the server may have many tied to your user).
|
|
11
|
-
* - `haystack
|
|
11
|
+
* - `haystack auth revoke <id>`
|
|
12
12
|
* Revokes a server-side token. If the revoked token is the one
|
|
13
13
|
* currently on disk, the local file is wiped so the next CLI call
|
|
14
14
|
* doesn't keep using a dead token.
|
|
@@ -123,12 +123,12 @@ export async function headlessLoginCommand(opts) {
|
|
|
123
123
|
console.log(` ${chalk.dim('path:')} ${path}`);
|
|
124
124
|
console.log(` ${chalk.dim('id:')} ${result.id}`);
|
|
125
125
|
console.log(chalk.dim(' Subsequent `haystack` calls will use this token.'));
|
|
126
|
-
console.log(chalk.dim(' Revoke server-side with: haystack
|
|
126
|
+
console.log(chalk.dim(' Revoke server-side with: haystack auth revoke ' + result.id));
|
|
127
127
|
}
|
|
128
128
|
export async function headlessLogoutCommand() {
|
|
129
129
|
await clearHeadlessToken();
|
|
130
130
|
console.log(chalk.green('✓ Headless token removed from disk.'));
|
|
131
|
-
console.log(chalk.dim(' Server-side token is still active. Revoke it with `haystack
|
|
131
|
+
console.log(chalk.dim(' Server-side token is still active. Revoke it with `haystack auth revoke <id>`.'));
|
|
132
132
|
}
|
|
133
133
|
export async function listTokensCommand(opts) {
|
|
134
134
|
const token = await loadToken();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
3
|
-
import { haystackJson } from '../utils/haystack-api.js';
|
|
3
|
+
import { haystackJson, repoApiPath } from '../utils/haystack-api.js';
|
|
4
4
|
const RUN_ID = /^cv_[0-9a-f]{48}$/;
|
|
5
5
|
const CASE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
6
6
|
const ACTION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
@@ -27,8 +27,8 @@ async function context(runId, repository) {
|
|
|
27
27
|
const { token } = await resolveAuthContext({ owner, repo });
|
|
28
28
|
return {
|
|
29
29
|
token,
|
|
30
|
-
path: `/
|
|
31
|
-
query:
|
|
30
|
+
path: repoApiPath(repository, `/runs/${runId}/cases`),
|
|
31
|
+
query: '',
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
export async function verifyCasesCommand(runId, options) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
2
|
-
import { haystackJson, HaystackApiError } from '../utils/haystack-api.js';
|
|
2
|
+
import { haystackJson, HaystackApiError, repoApiPath } from '../utils/haystack-api.js';
|
|
3
3
|
const HEALTH = ['healthy', 'degraded', 'infrastructure-error', 'not-executed', 'unknown'];
|
|
4
4
|
const LIFECYCLES = ['complete', 'failed', 'cancelled', 'budget-stopped', 'account-action'];
|
|
5
5
|
const RUN_ID = /^cv_[0-9a-f]{48}$/;
|
|
@@ -139,7 +139,7 @@ export async function verifyHistoryCommand(repository, options) {
|
|
|
139
139
|
const auth = await resolveAuthContext({ preferredLogin: options.account, owner, repo: name });
|
|
140
140
|
let response;
|
|
141
141
|
try {
|
|
142
|
-
response = await haystackJson(
|
|
142
|
+
response = await haystackJson(repoApiPath(`${owner}/${name}`, `/runs?limit=${limit}`), auth.token, { method: 'GET' });
|
|
143
143
|
}
|
|
144
144
|
catch (error) {
|
|
145
145
|
if (error instanceof HaystackApiError && error.status === 404) {
|
|
@@ -10,17 +10,14 @@
|
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
import { withSchema } from '../schema.js';
|
|
12
12
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
13
|
-
import { classifyHttpError, readWithRetries, SERVICE_SILENCE_LIMIT_MS, ServiceSilentError, transientReadFailure, } from '../utils/haystack-api.js';
|
|
13
|
+
import { classifyHttpError, readWithRetries, SERVICE_SILENCE_LIMIT_MS, ServiceSilentError, transientReadFailure, repoApiPath, } from '../utils/haystack-api.js';
|
|
14
14
|
import { trackOnboardingEvent } from '../utils/telemetry.js';
|
|
15
15
|
import { gatewayFetch } from './case-batch.js';
|
|
16
16
|
import { ONBOARDING_STAGES, ONBOARDING_VERSION, } from './onboarding-contract.js';
|
|
17
17
|
import { sameNotes } from './onboarding-notes.js';
|
|
18
18
|
import { deriveBaseSha, EXPLICIT_WALL_MS, resolveOriginRepository } from './verify-precompute.js';
|
|
19
19
|
import { findGitRoot } from '../utils/hooks.js';
|
|
20
|
-
const
|
|
21
|
-
const FACTS_PATH = `${ONBOARDING_PATH}/facts`;
|
|
22
|
-
const ANSWERS_PATH = `${ONBOARDING_PATH}/answers`;
|
|
23
|
-
const NOTES_PATH = `${ONBOARDING_PATH}/notes`;
|
|
20
|
+
const onboardingPath = (repository, rest = '') => repoApiPath(repository, `/onboarding${rest}`);
|
|
24
21
|
const ONBOARD_RUN_ID = /^ob_[0-9a-f]{48}$/;
|
|
25
22
|
const SHA256 = /^[0-9a-f]{64}$/;
|
|
26
23
|
const COMMIT = /^[0-9a-f]{40}$/;
|
|
@@ -155,11 +152,11 @@ async function body(response, what) {
|
|
|
155
152
|
export async function readOnboardingStatus(repository, baseCommit, token, onboardRunId = null) {
|
|
156
153
|
if (!COMMIT.test(baseCommit))
|
|
157
154
|
throw new Error(`Invalid base commit ${baseCommit}.`);
|
|
158
|
-
const path =
|
|
159
|
-
+ (onboardRunId === null ? '' : `&
|
|
155
|
+
const path = onboardingPath(repository, `?base=${baseCommit}`)
|
|
156
|
+
+ (onboardRunId === null ? '' : `&run=${encodeURIComponent(onboardRunId)}`);
|
|
160
157
|
const response = await gatewayFetch(path, token);
|
|
161
158
|
if (!response.ok)
|
|
162
|
-
throw await classifyHttpError(response, `Haystack API ${
|
|
159
|
+
throw await classifyHttpError(response, `Haystack API ${path}`);
|
|
163
160
|
return parseOnboardingStatus(await body(response, 'a status'), { repository, baseCommit });
|
|
164
161
|
}
|
|
165
162
|
/** Rule 12's start POST: onboard this base now, before any change exists. Idempotent: a
|
|
@@ -167,11 +164,11 @@ export async function readOnboardingStatus(repository, baseCommit, token, onboar
|
|
|
167
164
|
export async function startOnboarding(repository, baseCommit, token) {
|
|
168
165
|
if (!COMMIT.test(baseCommit))
|
|
169
166
|
throw new Error(`Invalid base commit ${baseCommit}.`);
|
|
170
|
-
const response = await gatewayFetch(
|
|
167
|
+
const response = await gatewayFetch(onboardingPath(repository), token, {
|
|
171
168
|
method: 'POST', body: JSON.stringify({ repository, baseCommit }),
|
|
172
169
|
});
|
|
173
170
|
if (!response.ok)
|
|
174
|
-
throw await classifyHttpError(response, `Haystack API ${
|
|
171
|
+
throw await classifyHttpError(response, `Haystack API ${onboardingPath(repository)}`);
|
|
175
172
|
return parseOnboardingStatus(await body(response, 'a status'), { repository, baseCommit });
|
|
176
173
|
}
|
|
177
174
|
/** How often a command reads onboarding status while it runs. The status route holds no read (a crawl's reads wait for a
|
|
@@ -357,9 +354,9 @@ function repositoryOf(value) {
|
|
|
357
354
|
return `${origin.owner}/${origin.repository}`;
|
|
358
355
|
}
|
|
359
356
|
export async function readFacts(repository, token) {
|
|
360
|
-
const response = await gatewayFetch(
|
|
357
|
+
const response = await gatewayFetch(onboardingPath(repository, '/facts'), token);
|
|
361
358
|
if (!response.ok)
|
|
362
|
-
throw await classifyHttpError(response, `Haystack API ${
|
|
359
|
+
throw await classifyHttpError(response, `Haystack API ${onboardingPath(repository, '/facts')}`);
|
|
363
360
|
return parseFactsView(await body(response, 'facts'), repository);
|
|
364
361
|
}
|
|
365
362
|
/** `haystack verify answer <question-id> <choice>`: files the answer to an open
|
|
@@ -379,7 +376,7 @@ export async function verifyAnswerCommand(questionId, choice, options) {
|
|
|
379
376
|
if (!question.choices.includes(choice)) {
|
|
380
377
|
throw new Error(`"${choice}" is not one of the question's choices: ${question.choices.map(item => JSON.stringify(item)).join(', ')}.`);
|
|
381
378
|
}
|
|
382
|
-
const response = await gatewayFetch(
|
|
379
|
+
const response = await gatewayFetch(onboardingPath(repository, '/answers'), auth.token, {
|
|
383
380
|
method: 'POST', body: JSON.stringify({ repository, expectedFactsDigest: facts.factsDigest, questionId, answer: choice }),
|
|
384
381
|
});
|
|
385
382
|
if (response.status === 409 && attempt < 3) {
|
|
@@ -387,7 +384,7 @@ export async function verifyAnswerCommand(questionId, choice, options) {
|
|
|
387
384
|
continue;
|
|
388
385
|
}
|
|
389
386
|
if (!response.ok)
|
|
390
|
-
throw await classifyHttpError(response, `Haystack API ${
|
|
387
|
+
throw await classifyHttpError(response, `Haystack API ${onboardingPath(repository, '/answers')}`);
|
|
391
388
|
const view = parseFactsView(await body(response, 'facts'), repository);
|
|
392
389
|
if (options.json) {
|
|
393
390
|
process.stdout.write(`${JSON.stringify(withSchema('verify-answer', { facts: view }), null, 2)}\n`);
|
|
@@ -407,7 +404,7 @@ export async function writeOnboardingNotes(repository, notes, token) {
|
|
|
407
404
|
if (sameNotes(facts.facts.notes, notes))
|
|
408
405
|
return 'unchanged';
|
|
409
406
|
const request = { repository, expectedFactsDigest: facts.factsDigest, notes };
|
|
410
|
-
const response = await gatewayFetch(
|
|
407
|
+
const response = await gatewayFetch(onboardingPath(repository, '/notes'), token, {
|
|
411
408
|
method: 'POST', body: JSON.stringify(request),
|
|
412
409
|
});
|
|
413
410
|
if (response.status === 409 && attempt < 3) {
|
|
@@ -415,7 +412,7 @@ export async function writeOnboardingNotes(repository, notes, token) {
|
|
|
415
412
|
continue;
|
|
416
413
|
}
|
|
417
414
|
if (!response.ok)
|
|
418
|
-
throw await classifyHttpError(response, `Haystack API ${
|
|
415
|
+
throw await classifyHttpError(response, `Haystack API ${onboardingPath(repository, '/notes')}`);
|
|
419
416
|
if (!sameNotes(parseFactsView(await body(response, 'facts'), repository).facts.notes, notes))
|
|
420
417
|
invalid('the notes it answered with');
|
|
421
418
|
return 'written';
|
|
@@ -8,7 +8,7 @@ import chalk from 'chalk';
|
|
|
8
8
|
import { NotLoggedInError, resolveAuthContext } from '../utils/auth.js';
|
|
9
9
|
import { findGitRoot, parseRemoteUrl, } from '../utils/git.js';
|
|
10
10
|
import { resolveVerifyBaseSha } from '../utils/verify-base.js';
|
|
11
|
-
import { HaystackApiError, haystackFetch, haystackJson, REPO_NOT_ENTITLED_CODE, SUBSCRIPTION_REQUIRED_CODE, } from '../utils/haystack-api.js';
|
|
11
|
+
import { HaystackApiError, haystackFetch, haystackJson, REPO_NOT_ENTITLED_CODE, SUBSCRIPTION_REQUIRED_CODE, repoApiPath, } from '../utils/haystack-api.js';
|
|
12
12
|
import { trackPrecomputeEvent } from '../utils/telemetry.js';
|
|
13
13
|
import { findSecretsInPatch, isSecretBearingPath } from '../utils/secret-paths.js';
|
|
14
14
|
import { startPrecomputeDelivery } from './precompute-delivery.js';
|
|
@@ -325,8 +325,7 @@ function precomputeResult(response) {
|
|
|
325
325
|
* service no longer has it). */
|
|
326
326
|
async function uploadSourcePatch(request, token, signal, force = false) {
|
|
327
327
|
const gzipped = Buffer.from(request.patchGzBase64, 'base64');
|
|
328
|
-
const path = `/
|
|
329
|
-
+ `?owner=${encodeURIComponent(request.owner)}&repository=${encodeURIComponent(request.repository)}`;
|
|
328
|
+
const path = repoApiPath(`${request.owner}/${request.repository}`, `/patches/${request.patchSha256}`);
|
|
330
329
|
const found = force ? null : await haystackFetch(path, token, { method: 'HEAD', signal }, [404]);
|
|
331
330
|
if (found === null || found.status === 404) {
|
|
332
331
|
await haystackFetch(path, token, { method: 'PUT', headers: { 'Content-Type': 'application/gzip' }, body: gzipped, signal });
|
|
@@ -339,7 +338,7 @@ async function submitCapture(request, token, signal) {
|
|
|
339
338
|
const { patchGzBase64: _inline, ...named } = request;
|
|
340
339
|
const submit = async (force) => {
|
|
341
340
|
const patchBytes = await uploadSourcePatch(request, token, signal, force);
|
|
342
|
-
return haystackJson('/
|
|
341
|
+
return haystackJson(repoApiPath(`${request.owner}/${request.repository}`, '/checks'), token, { method: 'POST',
|
|
343
342
|
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...named, patchBytes }), signal });
|
|
344
343
|
};
|
|
345
344
|
try {
|