@haystackeditor/cli 0.26.0 → 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.
- package/README.md +0 -8
- package/dist/capture/app-config.js +5 -5
- 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 +2 -2
- package/dist/commands/init.js +1 -1
- 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/verify.js
CHANGED
|
@@ -27,74 +27,23 @@ import chalk from 'chalk';
|
|
|
27
27
|
import { withSchema } from '../schema.js';
|
|
28
28
|
import { findGitRoot } from '../utils/hooks.js';
|
|
29
29
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
30
|
-
import { classifyHttpError, readWithRetries, SERVICE_SILENCE_LIMIT_MS, ServiceSilentError, transientReadFailure, } from '../utils/haystack-api.js';
|
|
30
|
+
import { classifyHttpError, readWithRetries, SERVICE_SILENCE_LIMIT_MS, ServiceSilentError, transientReadFailure, repoApiPath, } from '../utils/haystack-api.js';
|
|
31
31
|
import { GATEWAY_TIMEOUT_MS, gatewayFetch } from './case-batch.js';
|
|
32
32
|
import { CRAWL_BRIEF_MAX_FUNCTIONS, CRAWL_BRIEF_MAX_GROUPS, CRAWL_BRIEF_MAX_PRODUCTION_ROWS, CRAWL_BUDGET_MAX_MS, CRAWL_BUDGET_MIN_MS, CRAWL_MAX_FINDINGS, CRAWL_MAX_FINDING_STEPS, CRAWL_MAX_IDEAS, CRAWL_MAX_STAND_IN_LINES, CRAWL_MAX_STAND_IN_ROWS, CRAWL_MAX_TEXT_CHARS, CRAWL_WAIT_MAX_MS, CRAWL_POOLS, } from './crawl-contract.js';
|
|
33
33
|
import { captureCheckout, crawlUnavailableText, EXPLICIT_WALL_MS, normalModeFailure, postPrecomputeCapture, } from './verify-precompute.js';
|
|
34
|
+
import { SPOT_WORDS, TERMINAL, VERDICT_WORDS, VERDICTS, currentStep, errorWords, headline, plural, reportedStatus, results, verifyReport, } from './crawl-report.js';
|
|
34
35
|
import { formatOnboarding, onboardingExitCode, readOnboardingStatus, reportOnboardingState, reviewNotes, standInGapNotes, waitForOnboarding, } from './verify-onboarding.js';
|
|
35
36
|
import { formatCapture, readPreVerifyCapture } from './capture-brief.js';
|
|
36
|
-
const CRAWLS_PATH = '/api/agent/cloud-verifier/crawls';
|
|
37
37
|
/** A held read (waitAfter): the service's hold, then the time any read gets. */
|
|
38
38
|
const HELD_READ_TIMEOUT_MS = CRAWL_WAIT_MAX_MS + GATEWAY_TIMEOUT_MS;
|
|
39
39
|
const RUN_ID = /^cv_[0-9a-f]{48}$/;
|
|
40
40
|
const REPOSITORY_COMPONENT = /^[A-Za-z0-9_.-]{1,100}$/;
|
|
41
41
|
const STATUSES = ['queued', 'preparing', 'crawling', 'draining', 'completed', 'prepared', 'incomplete', 'cancelled'];
|
|
42
|
-
const TERMINAL = new Set(['completed', 'prepared', 'incomplete', 'cancelled']);
|
|
43
42
|
const STAGES = ['reconcile', 'source', 'worlds', 'blast', 'knobs', 'prepare', 'crawl', 'publish', 'cleanup'];
|
|
44
43
|
const ERROR_CODES = ['source-mismatch', 'blast-unavailable', 'world-build-failed',
|
|
45
44
|
'prepare-failed', 'crawl-failed', 'no-capacity', 'stalled', 'lease-lost', 'cancelled'];
|
|
46
|
-
const VERDICTS = ['bug', 'cant-tell', 'being-judged', 'not-a-bug'];
|
|
47
45
|
const SPOT_STAGES = ['resolved', 'loaded', 'exposed', 'activated', 'pressed', 'observed', 'confirmed', 'judged'];
|
|
48
46
|
const REUSE = ['hit', 'miss', null];
|
|
49
|
-
const STATUS_WORDS = {
|
|
50
|
-
queued: 'waiting for a machine',
|
|
51
|
-
preparing: 'getting ready',
|
|
52
|
-
crawling: 'exploring the app',
|
|
53
|
-
draining: 'finishing the last checks',
|
|
54
|
-
completed: 'finished',
|
|
55
|
-
prepared: 'built and ready to check',
|
|
56
|
-
incomplete: 'stopped before finishing',
|
|
57
|
-
cancelled: 'cancelled',
|
|
58
|
-
};
|
|
59
|
-
const STAGE_WORDS = {
|
|
60
|
-
reconcile: 'cleaning up after an earlier try',
|
|
61
|
-
source: 'rebuilding your change',
|
|
62
|
-
worlds: 'building and starting the app with and without your change',
|
|
63
|
-
blast: 'waiting for the analysis of what your change touches',
|
|
64
|
-
knobs: "reading the app's settings",
|
|
65
|
-
prepare: 'setting up starting points next to your change',
|
|
66
|
-
crawl: 'exploring the app',
|
|
67
|
-
publish: 'saving the results',
|
|
68
|
-
cleanup: 'shutting down the machines it used',
|
|
69
|
-
};
|
|
70
|
-
const ERROR_WORDS = {
|
|
71
|
-
'source-mismatch': 'your change could not be rebuilt exactly as it was captured',
|
|
72
|
-
'blast-unavailable': 'the analysis of what your change touches was not available',
|
|
73
|
-
'world-build-failed': 'the app did not build or start, with or without your change',
|
|
74
|
-
'prepare-failed': 'setting up starting points next to your change failed',
|
|
75
|
-
'crawl-failed': 'exploring the app failed',
|
|
76
|
-
'no-capacity': 'there was no room on the machines for it',
|
|
77
|
-
stalled: 'it stopped making progress',
|
|
78
|
-
'lease-lost': 'the machine running it stopped responding',
|
|
79
|
-
cancelled: 'it was cancelled',
|
|
80
|
-
};
|
|
81
|
-
const SPOT_WORDS = {
|
|
82
|
-
resolved: 'never reached',
|
|
83
|
-
loaded: 'loaded but never ran',
|
|
84
|
-
exposed: 'shown on screen',
|
|
85
|
-
activated: 'ran',
|
|
86
|
-
pressed: 'ran when its control was pressed',
|
|
87
|
-
observed: 'a difference seen',
|
|
88
|
-
confirmed: 'a difference seen and confirmed',
|
|
89
|
-
// The stage, not the verdict: a judged difference may be judged not a bug. The findings carry the verdicts.
|
|
90
|
-
judged: 'a difference seen, confirmed and judged',
|
|
91
|
-
};
|
|
92
|
-
const VERDICT_WORDS = {
|
|
93
|
-
bug: 'Bug',
|
|
94
|
-
'cant-tell': "Can't tell",
|
|
95
|
-
'being-judged': 'Still being judged',
|
|
96
|
-
'not-a-bug': 'Not a bug',
|
|
97
|
-
};
|
|
98
47
|
/** A response the CLI will not print: malformed, contradictory, or another revision's. */
|
|
99
48
|
export class CrawlResponseError extends Error {
|
|
100
49
|
constructor(message) {
|
|
@@ -363,16 +312,15 @@ async function responseBody(response) {
|
|
|
363
312
|
function read(path, token, waitAfter) {
|
|
364
313
|
return waitAfter === undefined
|
|
365
314
|
? gatewayFetch(path, token)
|
|
366
|
-
: gatewayFetch(`${path}&waitAfter=${encodeURIComponent(waitAfter)}`, token, { method: 'GET', timeoutMs: HELD_READ_TIMEOUT_MS });
|
|
315
|
+
: gatewayFetch(`${path}${path.includes('?') ? '&' : '?'}waitAfter=${encodeURIComponent(waitAfter)}`, token, { method: 'GET', timeoutMs: HELD_READ_TIMEOUT_MS });
|
|
367
316
|
}
|
|
368
317
|
/** The caller's newest crawl of this capture's revision, or null. Its change
|
|
369
318
|
* title may differ from the capture's (sameChange). */
|
|
370
319
|
async function readCrawl(identity, token, waitAfter) {
|
|
371
|
-
const path =
|
|
372
|
-
+ `&workCommit=${encodeURIComponent(identity.workCommit)}`;
|
|
320
|
+
const path = repoApiPath(identity.repository, `/checks?commit=${encodeURIComponent(identity.workCommit)}`);
|
|
373
321
|
const response = await read(path, token, waitAfter);
|
|
374
322
|
if (!response.ok)
|
|
375
|
-
throw await classifyHttpError(response, `Haystack API ${
|
|
323
|
+
throw await classifyHttpError(response, `Haystack API ${path}`);
|
|
376
324
|
const body = await responseBody(response);
|
|
377
325
|
if (!isRecord(body) || !Object.hasOwn(body, 'crawl'))
|
|
378
326
|
invalid('no crawl field');
|
|
@@ -382,9 +330,10 @@ async function readCrawl(identity, token, waitAfter) {
|
|
|
382
330
|
async function readCrawlRun(runId, identity, token, waitAfter) {
|
|
383
331
|
if (!RUN_ID.test(runId))
|
|
384
332
|
invalid('an acknowledged run id');
|
|
385
|
-
const
|
|
333
|
+
const path = repoApiPath(identity.repository, `/checks/${runId}`);
|
|
334
|
+
const response = await read(path, token, waitAfter);
|
|
386
335
|
if (!response.ok)
|
|
387
|
-
throw await classifyHttpError(response, `Haystack API ${
|
|
336
|
+
throw await classifyHttpError(response, `Haystack API ${path}`);
|
|
388
337
|
const view = parseCrawlView(await responseBody(response), identity);
|
|
389
338
|
if (view.runId !== runId || !sameChange(view, identity)) {
|
|
390
339
|
throw new CrawlResponseError(`The crawl service returned another crawl for ${runId}; it is not shown.`);
|
|
@@ -397,52 +346,6 @@ function safe(value) {
|
|
|
397
346
|
// eslint-disable-next-line no-control-regex -- replacing control characters is the point
|
|
398
347
|
return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ');
|
|
399
348
|
}
|
|
400
|
-
function plural(count, noun) {
|
|
401
|
-
return `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
402
|
-
}
|
|
403
|
-
function errorWords(error) {
|
|
404
|
-
return error.code === 'stalled'
|
|
405
|
-
? `${ERROR_WORDS.stalled} while ${STAGE_WORDS[error.stage]}`
|
|
406
|
-
: ERROR_WORDS[error.code];
|
|
407
|
-
}
|
|
408
|
-
/** What a running crawl is doing now: its stage, except while it waits for a
|
|
409
|
-
* machine or finishes its last checks, which its status says better. */
|
|
410
|
-
function currentStep(view) {
|
|
411
|
-
return view.status === 'queued' || view.status === 'draining' ? STATUS_WORDS[view.status] : STAGE_WORDS[view.stage];
|
|
412
|
-
}
|
|
413
|
-
/** The crawl's results: its sealed manifest, else (amendment 9) its answer at the budget, which has the same findings and reach. */
|
|
414
|
-
function results(view) {
|
|
415
|
-
return view.manifest ?? (view.answer?.status === 'answered' ? view.answer : null);
|
|
416
|
-
}
|
|
417
|
-
/** The status the command reports: a running crawl that has answered (amendment 9: its budget is up and its work is
|
|
418
|
-
* done; only its shutdown remains) reads as completed. */
|
|
419
|
-
function reportedStatus(view) {
|
|
420
|
-
return !TERMINAL.has(view.status) && view.answer?.status === 'answered' ? 'completed' : view.status;
|
|
421
|
-
}
|
|
422
|
-
function headline(view) {
|
|
423
|
-
const bugs = results(view)?.bugs ?? view.totals.bugs;
|
|
424
|
-
const found = bugs > 0 ? ` It found ${plural(bugs, 'bug')} before it stopped.` : '';
|
|
425
|
-
switch (reportedStatus(view)) {
|
|
426
|
-
case 'completed':
|
|
427
|
-
return bugs > 0 ? `Found ${plural(bugs, 'bug')} in your change.` : 'No bugs found in your change.';
|
|
428
|
-
case 'incomplete': {
|
|
429
|
-
const error = results(view)?.error ?? view.error;
|
|
430
|
-
return `The crawl could not finish: ${error ? errorWords(error) : 'no reason was recorded'}.${found}`;
|
|
431
|
-
}
|
|
432
|
-
case 'prepared':
|
|
433
|
-
// Amendment 12: the turn-end hook built and froze this change; `haystack verify` asks for the crawl itself.
|
|
434
|
-
return 'Your change is built and ready to check, but was not crawled yet. Run `haystack verify` to check it.';
|
|
435
|
-
case 'cancelled':
|
|
436
|
-
return `The crawl was cancelled.${found}`;
|
|
437
|
-
default: {
|
|
438
|
-
const so = bugs > 0 ? ` ${plural(bugs, 'bug')} found so far.` : '';
|
|
439
|
-
if (view.cancelRequested) {
|
|
440
|
-
return `Being cancelled: ${currentStep(view)}.${so}`;
|
|
441
|
-
}
|
|
442
|
-
return `Still running: ${currentStep(view)}.${so}`;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
349
|
function spotLines(spot) {
|
|
447
350
|
const how = spot.furthest === null ? 'never reached' : SPOT_WORDS[spot.furthest];
|
|
448
351
|
const found = spot.findingIds.length === 0 ? ''
|
|
@@ -555,57 +458,12 @@ export function formatCrawl(view) {
|
|
|
555
458
|
}
|
|
556
459
|
return lines.join('\n');
|
|
557
460
|
}
|
|
558
|
-
export function verifyReport(view) {
|
|
559
|
-
const status = reportedStatus(view);
|
|
560
|
-
const published = results(view);
|
|
561
|
-
const error = published?.error ?? view.error;
|
|
562
|
-
const explored = published !== null && view.clocks.crawlStartedAt !== null;
|
|
563
|
-
const findings = explored ? VERDICTS.flatMap(verdict => published.findings.filter(finding => finding.verdict === verdict)) : [];
|
|
564
|
-
const ideaWords = { covered: 'tried', blocked: 'could not try', unfinished: 'not finished' };
|
|
565
|
-
return {
|
|
566
|
-
runId: view.runId,
|
|
567
|
-
repository: view.repository,
|
|
568
|
-
title: view.changeTitle,
|
|
569
|
-
baseCommit: view.baseCommit,
|
|
570
|
-
workCommit: view.workCommit,
|
|
571
|
-
status,
|
|
572
|
-
headline: headline(view),
|
|
573
|
-
bugs: published?.bugs ?? view.totals.bugs,
|
|
574
|
-
findings: findings.map(finding => ({
|
|
575
|
-
id: finding.id,
|
|
576
|
-
verdict: finding.verdict,
|
|
577
|
-
why: finding.why,
|
|
578
|
-
start: finding.start,
|
|
579
|
-
steps: finding.steps,
|
|
580
|
-
outsideServices: finding.standIns && (finding.standIns.old.length > 0 || finding.standIns.new.length > 0)
|
|
581
|
-
? { withoutChange: finding.standIns.old, withChange: finding.standIns.new }
|
|
582
|
-
: null,
|
|
583
|
-
})),
|
|
584
|
-
spots: explored ? published.reach.spots.map(spot => ({
|
|
585
|
-
file: spot.file,
|
|
586
|
-
line: spot.line,
|
|
587
|
-
reached: spot.furthest === null ? 'never reached' : SPOT_WORDS[spot.furthest],
|
|
588
|
-
reachedBy: spot.reachedBy && { start: spot.reachedBy.start, steps: spot.reachedBy.steps, page: spot.reachedBy.page },
|
|
589
|
-
findingIds: spot.findingIds,
|
|
590
|
-
})) : [],
|
|
591
|
-
ideas: explored && published.ideas !== undefined
|
|
592
|
-
? published.ideas.map(idea => ({ text: idea.text, result: ideaWords[idea.state], reason: idea.reason, findingIds: idea.findingIds }))
|
|
593
|
-
: null,
|
|
594
|
-
neverRan: explored ? {
|
|
595
|
-
files: published.reach.unreachedChangedFiles,
|
|
596
|
-
functions: published.reach.blast === null ? null : { ran: published.reach.blast.ran, total: published.reach.blast.functions },
|
|
597
|
-
} : null,
|
|
598
|
-
notFinishedInTime: explored ? published.notFinishedInTime : 0,
|
|
599
|
-
outsideServiceGaps: explored ? published.standIns?.gaps ?? [] : [],
|
|
600
|
-
error: error ? { code: error.code, message: error.message } : null,
|
|
601
|
-
machinesNotProvenShutDown: TERMINAL.has(view.status) ? view.totals.cleanupUnproven : 0,
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
461
|
/** Case-batch status conventions: 0 finished (or answered: amendment 9, its work
|
|
605
462
|
* is done and only its shutdown remains), or still running with --no-wait;
|
|
606
463
|
* 2 ended without finishing (incomplete, cancelled) or finished with cleanup
|
|
607
464
|
* unproven (an answered crawl has not finished its cleanup, so it is not judged); bugs
|
|
608
465
|
* found never change it. The command's own failures exit 1 elsewhere. */
|
|
466
|
+
export { verifyReport };
|
|
609
467
|
export function crawlExitCode(view) {
|
|
610
468
|
const status = reportedStatus(view);
|
|
611
469
|
if (!TERMINAL.has(status))
|
package/dist/index.js
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Haystack CLI
|
|
3
|
+
* Haystack CLI: `haystack verify` runs your app with and without a change and shows what it broke.
|
|
4
4
|
*
|
|
5
|
-
* Set
|
|
6
|
-
*
|
|
5
|
+
* haystack init Set this repository up for `haystack verify` and start onboarding the app
|
|
6
|
+
* haystack verify After a change: crawl the app with and without it, and report what broke
|
|
7
|
+
* haystack feedback Tell the Haystack team what went wrong
|
|
8
|
+
* haystack login Sign in with GitHub
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
* npx @haystackeditor/cli init # Set this repository up for `haystack verify`
|
|
10
|
-
* npx @haystackeditor/cli setup # Interactive onboarding wizard
|
|
11
|
-
* npx @haystackeditor/cli status # Check configuration
|
|
12
|
-
* npx @haystackeditor/cli login # Authenticate with GitHub
|
|
13
|
-
* npx @haystackeditor/cli submit # Create a PR (auto-merge or review)
|
|
14
|
-
* npx @haystackeditor/cli triage # Check last submitted PR
|
|
15
|
-
* npx @haystackeditor/cli triage 123 # View analysis results for a PR
|
|
16
|
-
* npx @haystackeditor/cli dismiss 123 # Dismiss findings for a PR
|
|
17
|
-
* npx @haystackeditor/cli mark-reviewed 123 # Mark review as not needed
|
|
18
|
-
* npx @haystackeditor/cli request-review 123 # Tag a PR as needing human review
|
|
19
|
-
* npx @haystackeditor/cli pr-status 123 # Show PR status in Haystack pipeline
|
|
20
|
-
* npx @haystackeditor/cli config # Manage preferences
|
|
10
|
+
* Without a global install: npx -y -p @haystackeditor/cli@latest haystack <command>
|
|
21
11
|
*/
|
|
22
12
|
import { readFileSync } from 'node:fs';
|
|
23
13
|
import { fileURLToPath } from 'node:url';
|
|
@@ -29,7 +19,6 @@ import { lazy } from './lazy.js';
|
|
|
29
19
|
// Command modules load on first use (see src/lazy.ts). Keep new commands
|
|
30
20
|
// on this pattern: a static import here puts its whole dependency graph on
|
|
31
21
|
// the startup path of every invocation, including --version and --help.
|
|
32
|
-
const statusCommand = lazy(() => import('./commands/status.js'), 'statusCommand');
|
|
33
22
|
const initCommand = lazy(() => import('./commands/init.js'), 'initCommand');
|
|
34
23
|
const captureManifestCommand = lazy(() => import('./commands/capture-manifest.js'), 'captureManifestCommand');
|
|
35
24
|
const authListCommand = lazy(() => import('./commands/login.js'), 'authListCommand');
|
|
@@ -93,7 +82,7 @@ program
|
|
|
93
82
|
// ignored them. Every command created below inherits this setting.
|
|
94
83
|
.allowExcessArguments();
|
|
95
84
|
program
|
|
96
|
-
.command('schema [name]')
|
|
85
|
+
.command('schema [name]', { hidden: true })
|
|
97
86
|
.description('Print the JSON Schema for a command\'s --json output')
|
|
98
87
|
.action((name) => (name ? schemaCommand(name) : listSchemas()));
|
|
99
88
|
program
|
|
@@ -170,10 +159,6 @@ Examples:
|
|
|
170
159
|
haystack init --yes --json --notes haystack-notes.json
|
|
171
160
|
`)
|
|
172
161
|
.action(options => runPublicCommand(() => initCommand(options), options.json));
|
|
173
|
-
program
|
|
174
|
-
.command('status')
|
|
175
|
-
.description('Check if .haystack.json exists and is valid')
|
|
176
|
-
.action(statusCommand);
|
|
177
162
|
const verify = program
|
|
178
163
|
.command('verify')
|
|
179
164
|
.description('Crawl the running app with your current change: where it shows up and what broke')
|
|
@@ -670,7 +655,7 @@ GitHub expires it.
|
|
|
670
655
|
program
|
|
671
656
|
.command('logout [login]')
|
|
672
657
|
.description('Remove a saved GitHub account (use --headless to remove the local CLI token)')
|
|
673
|
-
.option('--headless', 'Remove the local hsk_live_* token file (does not revoke server-side — use `haystack
|
|
658
|
+
.option('--headless', 'Remove the local hsk_live_* token file (does not revoke server-side — use `haystack auth revoke <id>`)')
|
|
674
659
|
.action(async (login, opts) => {
|
|
675
660
|
if (opts.headless) {
|
|
676
661
|
try {
|
|
@@ -684,24 +669,37 @@ program
|
|
|
684
669
|
}
|
|
685
670
|
return logoutCommand(login);
|
|
686
671
|
});
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
672
|
+
program
|
|
673
|
+
.command('whoami')
|
|
674
|
+
.description('Show the active Haystack GitHub account')
|
|
675
|
+
.action(whoamiCommand);
|
|
676
|
+
const authProgram = program
|
|
677
|
+
.command('auth')
|
|
678
|
+
.description('Saved accounts and long-lived (hsk_live) tokens');
|
|
679
|
+
authProgram
|
|
690
680
|
.command('list')
|
|
691
|
-
.description('List
|
|
681
|
+
.description('List saved Haystack GitHub accounts')
|
|
682
|
+
.action(authListCommand);
|
|
683
|
+
authProgram
|
|
684
|
+
.command('use <login>')
|
|
685
|
+
.description('Set the active Haystack GitHub account')
|
|
686
|
+
.action(authUseCommand);
|
|
687
|
+
authProgram
|
|
688
|
+
.command('tokens')
|
|
689
|
+
.description('List your long-lived (hsk_live) tokens')
|
|
692
690
|
.option('--json', 'Machine-readable output')
|
|
693
691
|
.action(async (opts) => {
|
|
694
692
|
try {
|
|
695
693
|
await listTokensCommand(opts);
|
|
696
694
|
}
|
|
697
695
|
catch (err) {
|
|
698
|
-
console.error(chalk.red('
|
|
696
|
+
console.error(chalk.red('tokens failed:'), err instanceof Error ? err.message : err);
|
|
699
697
|
process.exit(1);
|
|
700
698
|
}
|
|
701
699
|
});
|
|
702
|
-
|
|
700
|
+
authProgram
|
|
703
701
|
.command('revoke <id>')
|
|
704
|
-
.description('Revoke a token by id (cannot be undone)')
|
|
702
|
+
.description('Revoke a long-lived token by id (cannot be undone)')
|
|
705
703
|
.action(async (id) => {
|
|
706
704
|
try {
|
|
707
705
|
await revokeTokenCommand(id);
|
|
@@ -711,21 +709,6 @@ tokens
|
|
|
711
709
|
process.exit(1);
|
|
712
710
|
}
|
|
713
711
|
});
|
|
714
|
-
program
|
|
715
|
-
.command('whoami')
|
|
716
|
-
.description('Show the active Haystack GitHub account')
|
|
717
|
-
.action(whoamiCommand);
|
|
718
|
-
const authProgram = program
|
|
719
|
-
.command('auth')
|
|
720
|
-
.description('Manage saved Haystack GitHub accounts');
|
|
721
|
-
authProgram
|
|
722
|
-
.command('list')
|
|
723
|
-
.description('List saved Haystack GitHub accounts')
|
|
724
|
-
.action(authListCommand);
|
|
725
|
-
authProgram
|
|
726
|
-
.command('use <login>')
|
|
727
|
-
.description('Set the active Haystack GitHub account')
|
|
728
|
-
.action(authUseCommand);
|
|
729
712
|
const telemetry = program.command('telemetry').description('Add privacy-safe production telemetry without an application SDK')
|
|
730
713
|
.addHelpText('after', `
|
|
731
714
|
Next.js bundles its server code, so instead of \`instrument\` add the CLI as a
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
export function haystackApiBase() {
|
|
2
2
|
return process.env.HAYSTACK_API_BASE ?? 'https://haystackeditor.com';
|
|
3
3
|
}
|
|
4
|
+
/** A public API path for `repository` (owner/repo): /v1/repos/{owner}/{repo}{rest}. */
|
|
5
|
+
export function repoApiPath(repository, rest) {
|
|
6
|
+
const parts = repository.split('/');
|
|
7
|
+
if (parts.length !== 2 || parts.some(part => !/^[A-Za-z0-9_.-]{1,100}$/.test(part))) {
|
|
8
|
+
throw new Error(`Invalid repository ${repository}: it must be owner/repo.`);
|
|
9
|
+
}
|
|
10
|
+
return `/v1/repos/${parts[0]}/${parts[1]}${rest}`;
|
|
11
|
+
}
|
|
4
12
|
export function haystackApiUrl(path) {
|
|
5
13
|
if (!path.startsWith('/'))
|
|
6
14
|
throw new Error('Haystack API path must be absolute');
|
package/package.json
CHANGED
package/dist/commands/status.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* haystack status - Check if .haystack.json exists and is valid
|
|
3
|
-
*/
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { loadConfig, findConfigPath } from '../utils/config.js';
|
|
6
|
-
export async function statusCommand() {
|
|
7
|
-
const configPath = await findConfigPath();
|
|
8
|
-
if (!configPath) {
|
|
9
|
-
console.log(chalk.yellow('⚠ No .haystack.json found'));
|
|
10
|
-
console.log(chalk.dim('\nRun `haystack init` to create one.\n'));
|
|
11
|
-
process.exit(1);
|
|
12
|
-
}
|
|
13
|
-
console.log(chalk.green(`✓ Found ${configPath}`));
|
|
14
|
-
try {
|
|
15
|
-
const config = await loadConfig(configPath);
|
|
16
|
-
if (!config) {
|
|
17
|
-
console.log(chalk.red('✗ Failed to load config'));
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
console.log(chalk.green('✓ Config is valid\n'));
|
|
21
|
-
// Show config summary
|
|
22
|
-
if (config.name) {
|
|
23
|
-
console.log(` ${chalk.dim('Name:')} ${config.name}`);
|
|
24
|
-
}
|
|
25
|
-
if (config.services) {
|
|
26
|
-
const serviceNames = Object.keys(config.services);
|
|
27
|
-
console.log(` ${chalk.dim('Services:')} ${serviceNames.join(', ')}`);
|
|
28
|
-
}
|
|
29
|
-
console.log('');
|
|
30
|
-
}
|
|
31
|
-
catch (e) {
|
|
32
|
-
console.log(chalk.red(`✗ Config is invalid: ${e.message}`));
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
}
|