@haystackeditor/cli 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -45
- package/dist/commands/case-batch.js +3 -1
- package/dist/commands/crawl-contract.js +6 -0
- package/dist/commands/fleet-policy.js +1 -22
- package/dist/commands/init.js +376 -129
- package/dist/commands/install-session-hooks.js +27 -10
- package/dist/commands/login.js +133 -34
- package/dist/commands/onboarding-contract.js +6 -0
- package/dist/commands/precompute-delivery.js +6 -3
- package/dist/commands/setup.js +1 -2
- package/dist/commands/verify-onboarding.js +365 -0
- package/dist/commands/verify-precompute.js +48 -12
- package/dist/commands/verify.js +242 -84
- package/dist/index.js +108 -30
- package/dist/schema.js +5 -1
- package/dist/types.js +3 -0
- package/dist/utils/config.js +0 -16
- package/dist/utils/haystack-api.js +12 -1
- package/dist/utils/secrets.js +0 -51
- package/dist/utils/telemetry.js +32 -0
- package/package.json +2 -2
- package/schemas/init.v1.json +35 -0
- package/schemas/login.v1.json +17 -0
- package/schemas/verify-answer.v1.json +86 -0
- package/schemas/verify-onboarding.v1.json +13 -0
- package/schemas/verify.v1.json +296 -8
- package/dist/utils/detect.js +0 -507
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn, execFileSync } from 'node:child_process';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
-
import { closeSync, constants, cpSync, existsSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { closeSync, constants, cpSync, existsSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { createGzip } from 'node:zlib';
|
|
5
5
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
@@ -12,7 +12,8 @@ import { HaystackApiError, haystackJson, REPO_NOT_ENTITLED_CODE, SUBSCRIPTION_RE
|
|
|
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';
|
|
15
|
-
import { CRAWL_TITLE_MAX_CHARS } from './crawl-contract.js';
|
|
15
|
+
import { CRAWL_BUDGET_MAX_MS, CRAWL_BUDGET_MIN_MS, CRAWL_TITLE_MAX_CHARS } from './crawl-contract.js';
|
|
16
|
+
import { blockLines, parseOnboardingBlock } from './verify-onboarding.js';
|
|
16
17
|
// --hook has five seconds to capture the exact source and publish local custody.
|
|
17
18
|
// Its detached sender owns the bounded network delivery and acknowledgement.
|
|
18
19
|
// An explicit invocation still waits synchronously for the server response.
|
|
@@ -66,7 +67,7 @@ function gitOutput(args, gitRoot, deadline, options = {}) {
|
|
|
66
67
|
throw new PrecomputeFailure('Could not derive the repository snapshot.');
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
|
-
function resolveOriginRepository(gitRoot) {
|
|
70
|
+
export function resolveOriginRepository(gitRoot) {
|
|
70
71
|
try {
|
|
71
72
|
const remote = parseRemoteUrl('origin');
|
|
72
73
|
if (!isRepositoryComponent(remote.owner)
|
|
@@ -80,8 +81,8 @@ function resolveOriginRepository(gitRoot) {
|
|
|
80
81
|
throw new PrecomputeFailure('No supported origin repository was found.');
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
|
-
/** The shared base rule (utils/verify-base.ts), bounded by the
|
|
84
|
-
function deriveBaseSha(gitRoot, deadline) {
|
|
84
|
+
/** The shared base rule (utils/verify-base.ts), bounded by the caller's deadline. */
|
|
85
|
+
export function deriveBaseSha(gitRoot, deadline) {
|
|
85
86
|
try {
|
|
86
87
|
return resolveVerifyBaseSha('HEAD', {
|
|
87
88
|
run: (args, options) => gitOutput(args, gitRoot, deadline, options?.remote
|
|
@@ -259,7 +260,9 @@ async function buildCompressedPatch(gitRoot, baseSha, workCommit, deadline) {
|
|
|
259
260
|
child.stdout.pipe(gzip);
|
|
260
261
|
});
|
|
261
262
|
}
|
|
262
|
-
export async function derivePrecomputeRequest(gitRoot, owner, repository, deadline = Date.now() + HOOK_WALL_MS
|
|
263
|
+
export async function derivePrecomputeRequest(gitRoot, owner, repository, deadline = Date.now() + HOOK_WALL_MS,
|
|
264
|
+
/** How long the crawl may take (CRAWL-V1 amendment 8); undefined: the service's default. */
|
|
265
|
+
budgetMs) {
|
|
263
266
|
// Capture this before git and compression work so delivery latency cannot
|
|
264
267
|
// make an older stop look newer than a later one.
|
|
265
268
|
const observedAt = Date.now();
|
|
@@ -287,7 +290,7 @@ export async function derivePrecomputeRequest(gitRoot, owner, repository, deadli
|
|
|
287
290
|
cacheKey,
|
|
288
291
|
patchGzBase64: patchGzip.toString('base64'),
|
|
289
292
|
patchSha256: createHash('sha256').update(patchGzip).digest('hex'),
|
|
290
|
-
...(workCommit === baseSha ? {} : { crawl: { treeSha, changeTitle } }),
|
|
293
|
+
...(workCommit === baseSha ? {} : { crawl: { treeSha, changeTitle, ...(budgetMs === undefined ? {} : { budgetMs }) } }),
|
|
291
294
|
},
|
|
292
295
|
};
|
|
293
296
|
}
|
|
@@ -330,7 +333,8 @@ function readWalkerStatus(value) {
|
|
|
330
333
|
}
|
|
331
334
|
throw new PrecomputeFailure('The precompute service returned an invalid walker status.');
|
|
332
335
|
}
|
|
333
|
-
const CRAWL_UNAVAILABLE_REASONS = ['
|
|
336
|
+
const CRAWL_UNAVAILABLE_REASONS = ['runner-unavailable', 'superseded', 'enqueue-failed'];
|
|
337
|
+
const isOnboardRunId = (value) => typeof value === 'string' && /^ob_[0-9a-f]{48}$/.test(value);
|
|
334
338
|
function readCrawlHookStatus(value) {
|
|
335
339
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
336
340
|
const record = value;
|
|
@@ -341,6 +345,18 @@ function readCrawlHookStatus(value) {
|
|
|
341
345
|
const reason = CRAWL_UNAVAILABLE_REASONS.find(known => known === record.reason);
|
|
342
346
|
if (record.status === 'unavailable' && reason !== undefined)
|
|
343
347
|
return { status: record.status, reason };
|
|
348
|
+
// Crawl amendment 8: the base has no published onboarding record yet.
|
|
349
|
+
if (record.status === 'onboarding' && isOnboardRunId(record.onboardRunId))
|
|
350
|
+
return { status: 'onboarding', onboardRunId: record.onboardRunId };
|
|
351
|
+
}
|
|
352
|
+
if (Object.keys(record).length === 3 && record.status === 'onboarding-blocked'
|
|
353
|
+
&& (record.onboardRunId === null || isOnboardRunId(record.onboardRunId))) {
|
|
354
|
+
try {
|
|
355
|
+
return { status: 'onboarding-blocked', onboardRunId: record.onboardRunId, block: parseOnboardingBlock(record.block) };
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
throw new PrecomputeFailure(`The precompute service returned an invalid onboarding block (${error instanceof Error ? error.message : String(error)}).`);
|
|
359
|
+
}
|
|
344
360
|
}
|
|
345
361
|
}
|
|
346
362
|
throw new PrecomputeFailure('The precompute service returned an invalid crawl status.');
|
|
@@ -348,9 +364,7 @@ function readCrawlHookStatus(value) {
|
|
|
348
364
|
/** Why the service could not start a crawl, in plain words. */
|
|
349
365
|
export function crawlUnavailableText(reason) {
|
|
350
366
|
switch (reason) {
|
|
351
|
-
case 'not-configured': return 'this repository is not set up for crawling yet';
|
|
352
367
|
case 'runner-unavailable': return 'no machine that can crawl this repository is available right now';
|
|
353
|
-
case 'profile-ambiguous': return 'the machines that crawl this repository disagree about how to run it';
|
|
354
368
|
case 'superseded': return 'a newer stop in this repository came first';
|
|
355
369
|
case 'enqueue-failed': return 'the service could not queue it';
|
|
356
370
|
}
|
|
@@ -447,19 +461,41 @@ function printResult(result) {
|
|
|
447
461
|
console.log(`A matching crawl already exists (${crawl.runId}).`);
|
|
448
462
|
else if (crawl?.status === 'unavailable')
|
|
449
463
|
console.log(`Crawl unavailable: ${crawlUnavailableText(crawl.reason)}.`);
|
|
464
|
+
else if (crawl?.status === 'onboarding') {
|
|
465
|
+
console.log(`The app is being onboarded for this base (${crawl.onboardRunId}); the crawl runs when it is ready. Run \`haystack verify\` to follow it.`);
|
|
466
|
+
}
|
|
467
|
+
else if (crawl?.status === 'onboarding-blocked')
|
|
468
|
+
console.log(blockLines(crawl.block).join('\n'));
|
|
450
469
|
else
|
|
451
470
|
console.log('Crawl status was not reported by the service.');
|
|
452
471
|
}
|
|
453
472
|
/** The hook, an explicit precompute and `haystack verify` all capture here.
|
|
454
473
|
* The repository defaults to origin's, as the hook's does. */
|
|
455
|
-
|
|
474
|
+
/** The repository's crawl time: .haystack.json crawl.minutes at the git root, whole minutes from 1 to 30 (CRAWL-V1 amendment 8),
|
|
475
|
+
* or undefined when the file or the field is absent. A value out of range fails the capture, never silently becomes the default. */
|
|
476
|
+
function configuredCrawlMinutes(gitRoot) {
|
|
477
|
+
const path = join(gitRoot, '.haystack.json');
|
|
478
|
+
if (!existsSync(path))
|
|
479
|
+
return undefined;
|
|
480
|
+
const minutes = JSON.parse(readFileSync(path, 'utf8')).crawl?.minutes;
|
|
481
|
+
if (minutes === undefined)
|
|
482
|
+
return undefined;
|
|
483
|
+
if (typeof minutes !== 'number' || !Number.isInteger(minutes) || minutes * 60_000 < CRAWL_BUDGET_MIN_MS || minutes * 60_000 > CRAWL_BUDGET_MAX_MS) {
|
|
484
|
+
throw new PrecomputeFailure(`.haystack.json crawl.minutes must be a whole number from ${CRAWL_BUDGET_MIN_MS / 60_000} to ${CRAWL_BUDGET_MAX_MS / 60_000}.`);
|
|
485
|
+
}
|
|
486
|
+
return minutes;
|
|
487
|
+
}
|
|
488
|
+
export async function captureCheckout(deadline, repository,
|
|
489
|
+
/** `haystack verify --minutes`; undefined: the repository's .haystack.json crawl.minutes, else the service's default. */
|
|
490
|
+
minutes) {
|
|
456
491
|
const gitRoot = findGitRoot();
|
|
457
492
|
if (!gitRoot) {
|
|
458
493
|
throw new PrecomputeFailure('Run this command from a git repository.');
|
|
459
494
|
}
|
|
460
495
|
const metadataDirectory = walkerMetadataDirectory(gitRoot, deadline);
|
|
461
496
|
const target = repository ?? resolveOriginRepository(gitRoot);
|
|
462
|
-
const
|
|
497
|
+
const chosen = minutes ?? configuredCrawlMinutes(gitRoot);
|
|
498
|
+
const derivation = await derivePrecomputeRequest(gitRoot, target.owner, target.repository, deadline, chosen === undefined ? undefined : chosen * 60_000);
|
|
463
499
|
return { metadataDirectory, derivation };
|
|
464
500
|
}
|
|
465
501
|
/** The explicit (non-hook) submission of a capture: one synchronous
|