@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
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Automated PR review, triage, and merge queue for AI-assisted development.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
|
-
* npx @haystackeditor/cli init # Set up
|
|
9
|
+
* npx @haystackeditor/cli init # Set this repository up for `haystack verify`
|
|
10
10
|
* npx @haystackeditor/cli setup # Interactive onboarding wizard
|
|
11
11
|
* npx @haystackeditor/cli status # Check configuration
|
|
12
12
|
* npx @haystackeditor/cli login # Authenticate with GitHub
|
|
@@ -133,7 +133,7 @@ function getVersion() {
|
|
|
133
133
|
const program = new Command();
|
|
134
134
|
program
|
|
135
135
|
.name('haystack')
|
|
136
|
-
.description('Haystack CLI —
|
|
136
|
+
.description('Haystack CLI — haystack verify runs your app with and without a change and shows what it broke')
|
|
137
137
|
.version(getVersion())
|
|
138
138
|
// commander 13 made surplus positional arguments an error; the CLI has always
|
|
139
139
|
// ignored them. Every command created below inherits this setting.
|
|
@@ -144,18 +144,38 @@ program
|
|
|
144
144
|
.action((name) => (name ? schemaCommand(name) : listSchemas()));
|
|
145
145
|
program
|
|
146
146
|
.command('init')
|
|
147
|
-
.description('
|
|
148
|
-
.option('-
|
|
147
|
+
.description('Set this repository up for `haystack verify` and start onboarding the app')
|
|
148
|
+
.option('-y, --yes', 'Make the changes without asking')
|
|
149
|
+
.option('--json', 'The result as one JSON document on stdout (see `haystack schema init`)')
|
|
149
150
|
.addHelpText('after', `
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
•
|
|
151
|
+
There is no setup file: Haystack works out how to run the app from what the
|
|
152
|
+
repository already keeps working. init shows each change as a diff, then makes
|
|
153
|
+
it with --yes (or a yes at the prompt):
|
|
154
|
+
• Claude Code's Stop hook (\`haystack verify precompute --hook\`) in the
|
|
155
|
+
per-developer .claude/settings.local.json, kept out of commits, when
|
|
156
|
+
Claude Code is in use here
|
|
157
|
+
• a short note in AGENTS.md (and CLAUDE.md when the repository keeps one)
|
|
158
|
+
telling coding agents to run \`haystack verify\` after a change
|
|
159
|
+
Then it starts onboarding the app, so it is usually ready by the first
|
|
160
|
+
\`haystack verify\`. Running it again changes only what is missing and shows
|
|
161
|
+
where onboarding is (\`haystack verify onboarding\` shows that too).
|
|
154
162
|
|
|
155
|
-
|
|
156
|
-
|
|
163
|
+
Exit codes:
|
|
164
|
+
0 set up: onboarding is running or the app is ready
|
|
165
|
+
1 the command failed
|
|
166
|
+
2 changes were shown but not made: run \`haystack init --yes\`
|
|
167
|
+
3 onboarding is blocked: the output says what would unblock it
|
|
168
|
+
4 not logged in: run \`haystack login\`
|
|
169
|
+
5 the Haystack GitHub App is not installed on the repository's owner:
|
|
170
|
+
the output links to it
|
|
171
|
+
6 set up, but onboarding stopped before finishing: run \`haystack init\` again
|
|
172
|
+
|
|
173
|
+
Examples:
|
|
174
|
+
haystack init
|
|
175
|
+
haystack init --yes
|
|
176
|
+
haystack init --yes --json
|
|
157
177
|
`)
|
|
158
|
-
.action(initCommand);
|
|
178
|
+
.action(options => runPublicCommand(() => initCommand(options), options.json));
|
|
159
179
|
program
|
|
160
180
|
.command('setup')
|
|
161
181
|
.description('Onboarding wizard — scan repos for rules/policies and write Haystack config')
|
|
@@ -208,7 +228,7 @@ const verify = program
|
|
|
208
228
|
.option('--repo <owner/repo>', 'GitHub repository (default: origin remote)')
|
|
209
229
|
.option('--account <login>', 'Use a specific saved Haystack account')
|
|
210
230
|
.option('--no-wait', 'Print the crawl\'s current state and return without waiting')
|
|
211
|
-
.option('--
|
|
231
|
+
.option('--minutes <n>', 'How long the crawl may take, 1-30 (default: .haystack.json crawl.minutes, else 3)')
|
|
212
232
|
.option('--json', 'The crawl as one JSON document on stdout (see `haystack schema verify`)')
|
|
213
233
|
.addHelpText('after', `
|
|
214
234
|
Run inside a git checkout; nothing needs to be committed or pushed. It captures
|
|
@@ -222,25 +242,40 @@ again), says so, and follows that crawl.
|
|
|
222
242
|
|
|
223
243
|
A crawl builds the app with and without your change, starts from the changed
|
|
224
244
|
code, explores outward with both side by side, and double-checks and judges
|
|
225
|
-
every difference
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
245
|
+
every difference it can within its time: 3 minutes unless the repository's
|
|
246
|
+
.haystack.json sets crawl.minutes or --minutes says otherwise (1 to 30).
|
|
247
|
+
Differences it had no time to check are listed as not finished in time. The
|
|
248
|
+
command waits for the crawl, printing each step and each bug the moment the
|
|
249
|
+
crawl finds it; it finishes when the crawl answers, as soon as its time is up,
|
|
250
|
+
without waiting for the crawl's machines to shut down. Ctrl-C stops waiting,
|
|
251
|
+
never the crawl. It then prints where your change shows up in the app (each
|
|
252
|
+
changed spot, how far the crawl got with it, and the steps that reached it),
|
|
253
|
+
what the crawl found (bugs first, with the steps to see each), and the changed
|
|
254
|
+
code it never ran.
|
|
255
|
+
|
|
256
|
+
The first verify on a base the service has not onboarded yet prepares the app
|
|
257
|
+
(reads the repository, plans how to run it, builds its runtime, starts it,
|
|
258
|
+
prepares its data and test accounts, proves a workflow); the command shows each
|
|
259
|
+
step, and the crawl runs once the app is ready. When onboarding cannot go on it
|
|
260
|
+
says why and what would unblock it, for example a question to answer with
|
|
261
|
+
\`haystack verify answer <question-id> <choice>\`.
|
|
230
262
|
|
|
231
263
|
Exit codes:
|
|
232
|
-
0 the crawl finished (the bugs it found are in the output), or
|
|
233
|
-
still running and --no-wait was given
|
|
264
|
+
0 the crawl answered or finished (the bugs it found are in the output), or
|
|
265
|
+
it is still running and --no-wait was given
|
|
234
266
|
1 the command failed, or no crawl could be started for this change
|
|
235
267
|
2 the crawl ended without finishing: it stopped early or was cancelled
|
|
236
268
|
(a newer stop in the repository replaces an older crawl), or the
|
|
237
|
-
machines it used could not be proven shut down
|
|
269
|
+
machines it used could not be proven shut down; or onboarding stopped
|
|
270
|
+
before finishing
|
|
271
|
+
3 onboarding is blocked: the output says what would unblock it
|
|
238
272
|
|
|
239
273
|
The hosted fleet run of pushed commits is \`haystack verify hosted start\`.
|
|
240
274
|
|
|
241
275
|
Examples:
|
|
242
276
|
haystack verify
|
|
243
277
|
haystack verify --no-wait
|
|
278
|
+
haystack verify --minutes 10
|
|
244
279
|
haystack verify --json
|
|
245
280
|
haystack verify history owner/repo --limit 20
|
|
246
281
|
haystack verify hosted status cv_<48 lowercase hex characters> --wait
|
|
@@ -267,6 +302,49 @@ function verifyCommandOptions(command) {
|
|
|
267
302
|
}
|
|
268
303
|
return options;
|
|
269
304
|
}
|
|
305
|
+
verify
|
|
306
|
+
.command('answer')
|
|
307
|
+
.description('Answer a question onboarding asked about your app, then run `haystack init` to continue')
|
|
308
|
+
.argument('<question-id>', 'The question id onboarding printed')
|
|
309
|
+
.argument('<choice>', 'One of the question\'s choices, exactly as printed')
|
|
310
|
+
.option('--repo <owner/repo>', 'GitHub repository (default: origin remote)')
|
|
311
|
+
.option('--account <login>', 'Use a specific saved Haystack account')
|
|
312
|
+
.option('--json', 'The repository\'s onboarding facts after the answer, as one JSON document')
|
|
313
|
+
.addHelpText('after', `
|
|
314
|
+
An answer is a fact about your app (never setup); onboarding plans from it the
|
|
315
|
+
next time it runs, so \`haystack init\` or a new \`haystack verify\` continues
|
|
316
|
+
from there.
|
|
317
|
+
|
|
318
|
+
Example:
|
|
319
|
+
haystack verify answer q_<id> "postgres 15 (terraform/db.tf)"
|
|
320
|
+
`)
|
|
321
|
+
.action(async (questionId, choice, _options, cmd) => {
|
|
322
|
+
const options = verifyCommandOptions(cmd);
|
|
323
|
+
const { verifyAnswerCommand } = await import('./commands/verify-onboarding.js');
|
|
324
|
+
return runPublicCommand(() => verifyAnswerCommand(questionId, choice, options), options.json);
|
|
325
|
+
});
|
|
326
|
+
verify
|
|
327
|
+
.command('onboarding')
|
|
328
|
+
.description('Show where onboarding of this checkout\'s base is, without starting anything')
|
|
329
|
+
.option('--account <login>', 'Use a specific saved Haystack account')
|
|
330
|
+
.option('--wait', 'Follow onboarding until it is ready, blocked or stopped')
|
|
331
|
+
.option('--json', 'The onboarding as one JSON document (see `haystack schema verify-onboarding`)')
|
|
332
|
+
.addHelpText('after', `
|
|
333
|
+
Reads the onboarding of the base \`haystack verify\` would use for this
|
|
334
|
+
checkout. \`haystack init\` starts it; so does the first \`haystack verify\`.
|
|
335
|
+
|
|
336
|
+
Exit codes:
|
|
337
|
+
0 the app is ready, or onboarding is running
|
|
338
|
+
1 the command failed
|
|
339
|
+
2 onboarding has not started, stopped before finishing, or was cancelled:
|
|
340
|
+
\`haystack init\` starts it again
|
|
341
|
+
3 onboarding is blocked: the output says what would unblock it
|
|
342
|
+
`)
|
|
343
|
+
.action(async (_options, cmd) => {
|
|
344
|
+
const options = verifyCommandOptions(cmd);
|
|
345
|
+
const { verifyOnboardingCommand } = await import('./commands/verify-onboarding.js');
|
|
346
|
+
return runPublicCommand(() => verifyOnboardingCommand(options), options.json);
|
|
347
|
+
});
|
|
270
348
|
verify
|
|
271
349
|
.command('history')
|
|
272
350
|
.description('Read your recent finished hosted verification runs without starting a run')
|
|
@@ -564,6 +642,15 @@ program
|
|
|
564
642
|
.option('--label <name>', '(--headless) Label to display in the dashboard for this token')
|
|
565
643
|
.option('--scope <repos>', '(--headless) Comma-separated repos to scope the token to (or "all")', (val) => val.split(',').map((s) => s.trim()).filter(Boolean))
|
|
566
644
|
.option('--token <hsk_live_...>', '(--headless) Skip the browser handshake — paste a token minted in the dashboard')
|
|
645
|
+
.option('--no-wait', 'Print the sign-in URL and code and return; the next `haystack login` finishes signing in')
|
|
646
|
+
.option('--json', 'The sign-in (URL and code, or the account signed in) as one JSON document (see `haystack schema login`)')
|
|
647
|
+
.addHelpText('after', `
|
|
648
|
+
Signing in uses GitHub's device flow: open the URL, enter the code. A coding
|
|
649
|
+
agent runs \`haystack login --no-wait\`, shows the person the URL and code, and
|
|
650
|
+
then runs \`haystack login\`, which waits until the code is entered. A login
|
|
651
|
+
stopped while it waits is finished the same way, with the same code, until
|
|
652
|
+
GitHub expires it.
|
|
653
|
+
`)
|
|
567
654
|
.action(async (opts) => {
|
|
568
655
|
if (opts.headless) {
|
|
569
656
|
try {
|
|
@@ -575,7 +662,7 @@ program
|
|
|
575
662
|
}
|
|
576
663
|
return;
|
|
577
664
|
}
|
|
578
|
-
return loginCommand();
|
|
665
|
+
return loginCommand({ wait: opts.wait, json: opts.json });
|
|
579
666
|
});
|
|
580
667
|
program
|
|
581
668
|
.command('logout [login]')
|
|
@@ -1525,15 +1612,6 @@ fleetPolicy
|
|
|
1525
1612
|
const { fleetPolicyHistoryCommand } = await import('./commands/fleet-policy.js');
|
|
1526
1613
|
await runAdminCommand(() => fleetPolicyHistoryCommand(repository, options));
|
|
1527
1614
|
});
|
|
1528
|
-
fleetPolicy
|
|
1529
|
-
.command('import-kv')
|
|
1530
|
-
.description('One time: import the KV policies and tenant overrides into D1')
|
|
1531
|
-
.option('--dry-run', 'Report what would be imported without writing')
|
|
1532
|
-
.option(workerUrlOption, workerUrlHelp)
|
|
1533
|
-
.action(async (options) => {
|
|
1534
|
-
const { fleetPolicyImportKvCommand } = await import('./commands/fleet-policy.js');
|
|
1535
|
-
await runAdminCommand(() => fleetPolicyImportKvCommand(options));
|
|
1536
|
-
});
|
|
1537
1615
|
// ─── rules ───────────────────────────────────────────────────────────────────
|
|
1538
1616
|
const rules = program.command('rules').description('Manage .haystack/pr-rules.yml');
|
|
1539
1617
|
rules
|
package/dist/schema.js
CHANGED
|
@@ -20,7 +20,11 @@ export const SCHEMA_VERSIONS = {
|
|
|
20
20
|
action: '1.0.0',
|
|
21
21
|
'cloud-verifier': '1.0.0',
|
|
22
22
|
'case-batch': '1.0.1',
|
|
23
|
-
verify: '1.0.
|
|
23
|
+
verify: '1.0.2',
|
|
24
|
+
'verify-answer': '1.0.0',
|
|
25
|
+
'verify-onboarding': '1.0.0',
|
|
26
|
+
init: '1.0.0',
|
|
27
|
+
login: '1.0.0',
|
|
24
28
|
error: '1.0.0',
|
|
25
29
|
};
|
|
26
30
|
/** Wrap a payload with the `schema_version` envelope. */
|
package/dist/types.js
CHANGED
|
@@ -294,6 +294,9 @@ const PreferencesSchema = z.object({
|
|
|
294
294
|
export const HaystackConfigSchema = z.object({
|
|
295
295
|
/** Config version (must be "1") */
|
|
296
296
|
version: z.literal('1'),
|
|
297
|
+
/** Hosted crawls of this repository (`haystack verify`, the stop hook): how long one may take, whole minutes from 1 to 30.
|
|
298
|
+
* Absent: the service's default (3 minutes). `haystack verify --minutes` overrides it for one run. */
|
|
299
|
+
crawl: z.object({ minutes: z.number().int().min(1).max(30).optional() }).strict().optional(),
|
|
297
300
|
/** Project name (for display) */
|
|
298
301
|
name: z.string().optional(),
|
|
299
302
|
/** Repository-level Haystack behavior */
|
package/dist/utils/config.js
CHANGED
|
@@ -42,22 +42,6 @@ export async function loadConfig(configPath) {
|
|
|
42
42
|
throw new Error(`Failed to parse ${resolvedPath}: ${err.message}`);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Save config to file
|
|
47
|
-
*/
|
|
48
|
-
export async function saveConfig(config, configPath) {
|
|
49
|
-
const resolvedPath = configPath || path.join(process.cwd(), CONFIG_FILENAME);
|
|
50
|
-
const content = JSON.stringify(config, null, 2);
|
|
51
|
-
await fs.writeFile(resolvedPath, content, 'utf-8');
|
|
52
|
-
return resolvedPath;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Check if config exists
|
|
56
|
-
*/
|
|
57
|
-
export async function configExists(startDir) {
|
|
58
|
-
const configPath = await findConfigPath(startDir);
|
|
59
|
-
return configPath !== null;
|
|
60
|
-
}
|
|
61
45
|
/**
|
|
62
46
|
* Get the project root (directory containing .haystack.json or current dir)
|
|
63
47
|
*/
|
|
@@ -45,6 +45,13 @@ export const SUBSCRIPTION_REQUIRED_MESSAGE = 'This account has no active Haystac
|
|
|
45
45
|
* repository and team). Its message names the repository; print it as is.
|
|
46
46
|
*/
|
|
47
47
|
export const REPO_NOT_ENTITLED_CODE = 'repo_not_entitled';
|
|
48
|
+
/**
|
|
49
|
+
* The server returns this `code` (HTTP 403, beside a readable `error`) when the
|
|
50
|
+
* repository's owner has not installed the Haystack GitHub App, which the
|
|
51
|
+
* service needs to read the repository.
|
|
52
|
+
*/
|
|
53
|
+
export const INSTALLATION_REQUIRED_CODE = 'installation-required';
|
|
54
|
+
export const HAYSTACK_APP_INSTALL_URL = 'https://github.com/apps/haystack-code-reviewer-pr-hook/installations/new';
|
|
48
55
|
/**
|
|
49
56
|
* Extract the server's own message (and its `error` code) if the body is
|
|
50
57
|
* JSON; truncate raw bodies.
|
|
@@ -66,7 +73,8 @@ async function serverMessage(response) {
|
|
|
66
73
|
const parsed = JSON.parse(text);
|
|
67
74
|
return {
|
|
68
75
|
detail: parsed.message ?? parsed.error ?? null,
|
|
69
|
-
|
|
76
|
+
// Routes that answer { error: <readable text>, code } carry the stable code in `code`.
|
|
77
|
+
code: typeof parsed.code === 'string' ? parsed.code : typeof parsed.error === 'string' ? parsed.error : null,
|
|
70
78
|
};
|
|
71
79
|
}
|
|
72
80
|
catch {
|
|
@@ -90,6 +98,9 @@ export async function classifyHttpError(response, context) {
|
|
|
90
98
|
: 'This account is not entitled to use Haystack on this repository. See https://haystack.sh/pricing or email akshay@haystack.sh to get access.';
|
|
91
99
|
return new HaystackApiError(403, message, code);
|
|
92
100
|
}
|
|
101
|
+
if (response.status === 403 && code === INSTALLATION_REQUIRED_CODE) {
|
|
102
|
+
return new HaystackApiError(403, `${detail ?? 'Install the Haystack GitHub App on the repository\'s owner'}: ${HAYSTACK_APP_INSTALL_URL}`, code);
|
|
103
|
+
}
|
|
93
104
|
if (response.status === 401) {
|
|
94
105
|
return new HaystackApiError(401, `${context}: authentication failed${suffix}. Run \`haystack login\` to re-authenticate.`);
|
|
95
106
|
}
|
package/dist/utils/secrets.js
CHANGED
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
* - detect-secrets
|
|
10
10
|
* - truffleHog
|
|
11
11
|
*/
|
|
12
|
-
import * as fs from 'fs/promises';
|
|
13
|
-
import * as path from 'path';
|
|
14
12
|
/**
|
|
15
13
|
* Common secret patterns
|
|
16
14
|
*/
|
|
@@ -188,52 +186,3 @@ export function scanForSecrets(content, filename) {
|
|
|
188
186
|
}
|
|
189
187
|
return findings;
|
|
190
188
|
}
|
|
191
|
-
/**
|
|
192
|
-
* Scan a file for secrets
|
|
193
|
-
*/
|
|
194
|
-
export async function scanFile(filePath) {
|
|
195
|
-
try {
|
|
196
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
197
|
-
return scanForSecrets(content, path.basename(filePath));
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
return [];
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Scan .haystack.json specifically for secrets
|
|
205
|
-
*/
|
|
206
|
-
export async function scanHaystackConfig(configPath) {
|
|
207
|
-
return scanFile(configPath);
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Validate that a config doesn't contain hardcoded secrets
|
|
211
|
-
* Returns true if safe, false if secrets detected
|
|
212
|
-
*/
|
|
213
|
-
export async function validateConfigSecurity(configPath) {
|
|
214
|
-
const findings = await scanHaystackConfig(configPath);
|
|
215
|
-
const highSeverity = findings.filter((f) => f.severity === 'high');
|
|
216
|
-
return {
|
|
217
|
-
safe: highSeverity.length === 0,
|
|
218
|
-
findings,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Get a security report string for display
|
|
223
|
-
*/
|
|
224
|
-
export function formatSecurityReport(findings) {
|
|
225
|
-
if (findings.length === 0) {
|
|
226
|
-
return '✓ No secrets detected';
|
|
227
|
-
}
|
|
228
|
-
const lines = [
|
|
229
|
-
`⚠️ ${findings.length} potential secret(s) detected:\n`,
|
|
230
|
-
];
|
|
231
|
-
for (const finding of findings) {
|
|
232
|
-
const severity = finding.severity === 'high' ? '🔴' : finding.severity === 'medium' ? '🟡' : '🟢';
|
|
233
|
-
lines.push(` ${severity} ${finding.file}:${finding.line}:${finding.column}`);
|
|
234
|
-
lines.push(` ${finding.description}`);
|
|
235
|
-
lines.push(` Matched: ${finding.match}\n`);
|
|
236
|
-
}
|
|
237
|
-
lines.push('Use $VARIABLE syntax to reference secrets instead of hardcoding values.\n');
|
|
238
|
-
return lines.join('\n');
|
|
239
|
-
}
|
package/dist/utils/telemetry.js
CHANGED
|
@@ -197,6 +197,38 @@ export function trackPrecomputeEvent(outcome, wallMs, timeoutMs) {
|
|
|
197
197
|
// Telemetry must never crash or delay the CLI.
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
|
+
/** Track one onboarding state `haystack verify` saw: started, blocked (with the block's kind) or ready. */
|
|
201
|
+
export function trackOnboardingEvent(outcome, kind) {
|
|
202
|
+
try {
|
|
203
|
+
if (telemetryDisabled() || typeof globalThis.fetch !== 'function')
|
|
204
|
+
return;
|
|
205
|
+
const event = {
|
|
206
|
+
api_key: POSTHOG_API_KEY,
|
|
207
|
+
distinct_id: getDistinctId(),
|
|
208
|
+
event: 'cli_verify_onboarding',
|
|
209
|
+
properties: {
|
|
210
|
+
cli_version: getCLIVersion(),
|
|
211
|
+
os: process.platform,
|
|
212
|
+
arch: process.arch,
|
|
213
|
+
timestamp: new Date().toISOString(),
|
|
214
|
+
...minimizeMetadata({ outcome, ...(kind === undefined ? {} : { kind }) }),
|
|
215
|
+
$ip: null,
|
|
216
|
+
},
|
|
217
|
+
timestamp: new Date().toISOString(),
|
|
218
|
+
};
|
|
219
|
+
globalThis.fetch(`${POSTHOG_HOST}/capture/`, {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers: { 'Content-Type': 'application/json' },
|
|
222
|
+
body: JSON.stringify(event),
|
|
223
|
+
}).catch(() => {
|
|
224
|
+
// Documented fire-and-forget product telemetry: delivery cannot affect
|
|
225
|
+
// the CLI command being measured.
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Telemetry must never crash the CLI
|
|
230
|
+
}
|
|
231
|
+
}
|
|
200
232
|
function getCLIVersion() {
|
|
201
233
|
try {
|
|
202
234
|
const packageJson = JSON.parse(readFileSync(PACKAGE_JSON_PATH, 'utf8'));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haystackeditor/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.20.0",
|
|
4
|
+
"description": "haystack verify: run your app with and without a change, and see what the change broke",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"haystack": "dist/index.js"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://docs.haystackeditor.com/cli/schemas/init.v1.json",
|
|
4
|
+
"title": "Haystack init: this repository's setup for `haystack verify`",
|
|
5
|
+
"description": "Machine-readable output of `haystack init --json`. `changes` are the project-local changes init planned (made when status is done); `onboarding` is the base's OnboardingStatusV1 (onboarding_contracts.ts, APP-ONBOARDING-V1) as POST /api/agent/cloud-verifier/onboarding returns it, null unless status is done. Exit codes: 0 done (onboarding running or ready), 1 failed (the error envelope), 2 planned (nothing written; run with --yes), 3 done but onboarding blocked, 4 login-required, 5 app-required (install the Haystack GitHub App), 6 done but onboarding stopped before finishing (run init again).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schema_version", "status", "repository", "changes", "notices", "onboarding"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schema_version": { "const": "1.0.0" },
|
|
11
|
+
"status": { "enum": ["done", "planned", "login-required", "app-required"] },
|
|
12
|
+
"repository": { "type": "string" },
|
|
13
|
+
"changes": {
|
|
14
|
+
"type": "array",
|
|
15
|
+
"items": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"additionalProperties": false,
|
|
18
|
+
"required": ["path", "action", "reason", "diff"],
|
|
19
|
+
"properties": {
|
|
20
|
+
"path": { "type": "string" },
|
|
21
|
+
"action": { "enum": ["create", "update"] },
|
|
22
|
+
"reason": { "type": "string" },
|
|
23
|
+
"diff": { "type": "array", "items": { "type": "string" } }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"notices": { "type": "array", "items": { "type": "string" } },
|
|
28
|
+
"onboarding": {
|
|
29
|
+
"oneOf": [
|
|
30
|
+
{ "type": "null" },
|
|
31
|
+
{ "$ref": "verify.v1.json#/$defs/onboardingStatus" }
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://docs.haystackeditor.com/cli/schemas/login.v1.json",
|
|
4
|
+
"title": "Haystack login: a GitHub device-flow sign-in",
|
|
5
|
+
"description": "Machine-readable output of `haystack login --json`. With --no-wait, status is pending and carries the URL to open and the code to enter (never the device code); the next `haystack login` finishes it. Otherwise status is logged-in with the account signed in.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schema_version", "status", "verificationUri", "userCode", "expiresAt", "login"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schema_version": { "const": "1.0.0" },
|
|
11
|
+
"status": { "enum": ["pending", "logged-in"] },
|
|
12
|
+
"verificationUri": { "type": ["string", "null"] },
|
|
13
|
+
"userCode": { "type": ["string", "null"] },
|
|
14
|
+
"expiresAt": { "type": ["string", "null"], "format": "date-time" },
|
|
15
|
+
"login": { "type": ["string", "null"] }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://docs.haystackeditor.com/cli/schemas/verify-answer.v1.json",
|
|
4
|
+
"title": "Haystack verify answer: the repository's onboarding facts after an answer",
|
|
5
|
+
"description": "Machine-readable output of `haystack verify answer <question-id> <choice> --json`: OnboardingFactsView (onboarding_contracts.ts, APP-ONBOARDING-V1) as POST /api/agent/cloud-verifier/onboarding/answers returns it.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"schema_version",
|
|
10
|
+
"facts"
|
|
11
|
+
],
|
|
12
|
+
"properties": {
|
|
13
|
+
"schema_version": {
|
|
14
|
+
"const": "1.0.0"
|
|
15
|
+
},
|
|
16
|
+
"facts": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"required": [
|
|
19
|
+
"repository",
|
|
20
|
+
"facts",
|
|
21
|
+
"factsDigest",
|
|
22
|
+
"openQuestions"
|
|
23
|
+
],
|
|
24
|
+
"properties": {
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "string"
|
|
27
|
+
},
|
|
28
|
+
"factsDigest": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"pattern": "^[0-9a-f]{64}$"
|
|
31
|
+
},
|
|
32
|
+
"facts": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"required": [
|
|
35
|
+
"dbProfile",
|
|
36
|
+
"answers"
|
|
37
|
+
],
|
|
38
|
+
"properties": {
|
|
39
|
+
"dbProfile": {
|
|
40
|
+
"oneOf": [
|
|
41
|
+
{
|
|
42
|
+
"type": "null"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"type": "string",
|
|
46
|
+
"pattern": "^[0-9a-f]{64}$"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"answers": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"additionalProperties": {
|
|
53
|
+
"type": "string"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"openQuestions": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"items": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"required": [
|
|
63
|
+
"id",
|
|
64
|
+
"message",
|
|
65
|
+
"choices"
|
|
66
|
+
],
|
|
67
|
+
"properties": {
|
|
68
|
+
"id": {
|
|
69
|
+
"type": "string"
|
|
70
|
+
},
|
|
71
|
+
"message": {
|
|
72
|
+
"type": "string"
|
|
73
|
+
},
|
|
74
|
+
"choices": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"items": {
|
|
77
|
+
"type": "string"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://docs.haystackeditor.com/cli/schemas/verify-onboarding.v1.json",
|
|
4
|
+
"title": "Haystack verify onboarding: the onboarding of this checkout's base",
|
|
5
|
+
"description": "Machine-readable output of `haystack verify onboarding --json`: OnboardingStatusV1 (onboarding_contracts.ts, APP-ONBOARDING-V1) of the base `haystack verify` would use, as GET /api/agent/cloud-verifier/onboarding?repository=&baseCommit= returns it.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schema_version", "onboarding"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schema_version": { "const": "1.0.0" },
|
|
11
|
+
"onboarding": { "$ref": "verify.v1.json#/$defs/onboardingStatus" }
|
|
12
|
+
}
|
|
13
|
+
}
|