@haystackeditor/cli 0.30.1 → 0.31.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/dist/commands/crawl-report.js +2 -4
- package/dist/commands/init-summary.js +115 -0
- package/dist/commands/init.js +75 -39
- package/dist/commands/verify-onboarding.js +16 -7
- package/dist/commands/verify.js +7 -6
- package/dist/index.js +30 -15
- package/dist/schema.js +1 -1
- package/package.json +1 -1
- package/schemas/init.v2.json +299 -0
|
@@ -74,12 +74,10 @@ export function brokenCopy(view) {
|
|
|
74
74
|
}
|
|
75
75
|
/** What `haystack verify` says first of a broken copy. */
|
|
76
76
|
export const BROKEN_COPY_HEADLINE = "We couldn't test your change: the app copy is broken.";
|
|
77
|
-
/** What the coding agent does about a broken copy: the
|
|
78
|
-
* setup (onboarding amendment 38 updates it only from a check that could not start the app), that is sent to the Haystack team
|
|
79
|
-
* (`haystack feedback`), and the user is told the change was not tested. */
|
|
77
|
+
/** What the coding agent does about a broken copy: explain the missing setup in run notes, then ask for the check again. */
|
|
80
78
|
export const BROKEN_COPY_STEPS = [
|
|
81
79
|
"Find what these errors point to in the repository's own setup (docker compose files, env examples, docs): the services, settings and data the app needs to run.",
|
|
82
|
-
'
|
|
80
|
+
'Write what the app needs in a JSON notes file, for example { "run": "<missing services, packages, toolchains or settings>" }, then run `haystack init --notes <file>` and `haystack verify` again. Haystack uses the notes to repair the existing setup and reruns the check when the repair is ready.',
|
|
83
81
|
'Tell your user this check did not test the change; do not read it as passing.',
|
|
84
82
|
];
|
|
85
83
|
/** Text that reads as an error, for the lines a broken copy's start screen shows (every one is in --raw). */
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How `haystack init` ends: a short checklist of what is done and what is left, then exactly one next step (Akshay,
|
|
3
|
+
* 2026-10-07: init as a wizard for coding agents, with no new commands). Codex setting up langgenius/dify got the test copy's
|
|
4
|
+
* setup, production telemetry's questions and hundreds of telemetry candidates in one output, and could not tell when setup
|
|
5
|
+
* was finished. What agent-facing CLIs do (doctor-style checklists, one actionable next step, never a prompt): init does all
|
|
6
|
+
* it can in one run, and stops only when it needs a person, needs to wait, or is done; running it again picks up from there.
|
|
7
|
+
*
|
|
8
|
+
* The next step is one of: a command the coding agent runs itself (`run`), something only its user can do or answer
|
|
9
|
+
* (`ask-user`, the text says what to ask), a long step to wait out (`wait`), or nothing left (`none`).
|
|
10
|
+
*/
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
const TELEMETRY_HINT = 'optional: it needs the app\'s owner. Ask your user whether to track how real users use the app; if they '
|
|
13
|
+
+ 'want it, run `haystack init --telemetry`';
|
|
14
|
+
function onboardingCheck(onboarding) {
|
|
15
|
+
const item = 'Test copy of the app';
|
|
16
|
+
if (onboarding === null)
|
|
17
|
+
return { item, state: 'todo', detail: 'not started yet' };
|
|
18
|
+
switch (onboarding.state) {
|
|
19
|
+
case 'ready': return { item, state: 'done', detail: 'ready' };
|
|
20
|
+
case 'onboarding': return { item, state: 'running', detail: 'being set up' };
|
|
21
|
+
case 'blocked': return { item, state: 'todo', detail: onboarding.block?.ask?.question ? 'needs an answer from your user' : 'blocked' };
|
|
22
|
+
case 'not-started': return { item, state: 'todo', detail: 'not started yet' };
|
|
23
|
+
default: return { item, state: 'todo', detail: 'stopped before finishing' };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function telemetryChecks(telemetry) {
|
|
27
|
+
if (telemetry === null)
|
|
28
|
+
return [{ item: 'Tracking real users', state: 'optional', detail: TELEMETRY_HINT }];
|
|
29
|
+
const part = (item, value) => value.status === 'unsupported'
|
|
30
|
+
? { item, state: 'skipped', detail: `not supported here: ${value.why}` }
|
|
31
|
+
: value.status === 'installed'
|
|
32
|
+
? value.received === null ? { item, state: 'todo', detail: 'set up in the code; nothing received from the live app yet' }
|
|
33
|
+
: { item, state: 'done', detail: `working; last received ${value.received.lastSeenAt}` }
|
|
34
|
+
: { item, state: 'todo', detail: 'needs a step' };
|
|
35
|
+
return [part('Tracking in the browser', telemetry.report.capture), part('Tracking on the server', telemetry.report.server)];
|
|
36
|
+
}
|
|
37
|
+
/** Tracking is finished when every part this app supports is set up and Haystack has received its data from the live app. The
|
|
38
|
+
* report's steps stay listed after that (the token, production's environment, the off switches), as standing advice, never as a
|
|
39
|
+
* next step: an agent running init until Ready would otherwise never reach it (Bugbot on #4224). */
|
|
40
|
+
function trackingDone(report) {
|
|
41
|
+
return [report.capture, report.server].every(part => part.status === 'unsupported' || (part.status === 'installed' && part.received !== null));
|
|
42
|
+
}
|
|
43
|
+
/** Tracking's first step for the person while it is not finished: a manual part's first step, else the first of the report's
|
|
44
|
+
* steps, with the indented lines that continue it. */
|
|
45
|
+
function firstTelemetryStep(telemetry) {
|
|
46
|
+
if (trackingDone(telemetry.report))
|
|
47
|
+
return null;
|
|
48
|
+
for (const value of [telemetry.report.capture, telemetry.report.server]) {
|
|
49
|
+
if (value.status === 'manual' && value.steps.length)
|
|
50
|
+
return value.steps[0];
|
|
51
|
+
}
|
|
52
|
+
const at = telemetry.steps.findIndex(step => !step.startsWith(' '));
|
|
53
|
+
if (at < 0)
|
|
54
|
+
return null;
|
|
55
|
+
const continued = telemetry.steps.slice(at + 1);
|
|
56
|
+
const end = continued.findIndex(step => !step.startsWith(' '));
|
|
57
|
+
return [telemetry.steps[at], ...(end < 0 ? continued : continued.slice(0, end)).map(step => step.trim())].join(' ');
|
|
58
|
+
}
|
|
59
|
+
function nextStep(input) {
|
|
60
|
+
if (input.status === 'login-required') {
|
|
61
|
+
return { kind: 'run', text: 'Run `haystack login`: it gives your user a link to sign in with. Then run `haystack init` again.', command: 'haystack login' };
|
|
62
|
+
}
|
|
63
|
+
if (input.status === 'planned') {
|
|
64
|
+
// Notes save the most time before the app's setup starts; for an app already set up they change nothing.
|
|
65
|
+
const notesHelp = input.notes === null && input.onboarding !== null && input.onboarding.state !== 'ready' && input.onboarding.state !== 'onboarding'
|
|
66
|
+
? ' The app is not set up yet: add `--notes <file>` with what you know about how it runs, signs people in and is used '
|
|
67
|
+
+ '(see `haystack init --help`), so its setup does not have to work that out.' : '';
|
|
68
|
+
return { kind: 'run', text: `Nothing was changed yet. Run \`${input.rerunWithYes}\` to make the changes above.${notesHelp}`, command: input.rerunWithYes };
|
|
69
|
+
}
|
|
70
|
+
if (input.status === 'app-required') {
|
|
71
|
+
return { kind: 'ask-user', text: `Ask your user to install the Haystack GitHub App on ${input.owner}: ${input.installUrl} . Then run \`haystack init\` again.`,
|
|
72
|
+
command: null };
|
|
73
|
+
}
|
|
74
|
+
const onboarding = input.onboarding;
|
|
75
|
+
if (onboarding.state === 'blocked') {
|
|
76
|
+
const block = onboarding.block;
|
|
77
|
+
const ask = block?.ask ?? null;
|
|
78
|
+
if (ask?.question) {
|
|
79
|
+
return { kind: 'ask-user', text: `Ask your user: ${ask.message} (one of: ${ask.question.choices.map(choice => JSON.stringify(choice)).join(', ')}). `
|
|
80
|
+
+ `File their answer with \`haystack verify answer ${ask.question.id} "<their answer>"\`, then run \`haystack init\` again.`,
|
|
81
|
+
command: `haystack verify answer ${ask.question.id} "<their answer>"` };
|
|
82
|
+
}
|
|
83
|
+
if (ask?.command)
|
|
84
|
+
return { kind: 'run', text: `${ask.message} Run \`${ask.command}\`, then \`haystack init\` again.`, command: ask.command };
|
|
85
|
+
return { kind: 'none', text: `Setup is blocked and nothing you can run changes that yet${block ? `: ${block.reason}` : ''}. Tell your user, `
|
|
86
|
+
+ 'and say what happened with `haystack feedback "<what happened>"`.', command: null };
|
|
87
|
+
}
|
|
88
|
+
if (onboarding.state !== 'ready' && onboarding.state !== 'onboarding') {
|
|
89
|
+
return { kind: 'run', text: 'Setting up the test copy stopped before finishing. Run `haystack init` again to start it again.', command: 'haystack init' };
|
|
90
|
+
}
|
|
91
|
+
// Something the person can do now comes before waiting.
|
|
92
|
+
const tracking = input.telemetry === null ? null : firstTelemetryStep(input.telemetry);
|
|
93
|
+
if (tracking !== null)
|
|
94
|
+
return { kind: 'ask-user', text: `For tracking: ${tracking}`, command: null };
|
|
95
|
+
if (onboarding.state === 'onboarding') {
|
|
96
|
+
return { kind: 'wait', text: 'The test copy is being set up. Run `haystack verify onboarding --wait` to follow its progress, then run `haystack init` again. '
|
|
97
|
+
+ 'Ctrl-C stops waiting; setup keeps running.', command: 'haystack verify onboarding --wait' };
|
|
98
|
+
}
|
|
99
|
+
return { kind: 'none', text: 'Ready. After each change, run `haystack verify`.', command: null };
|
|
100
|
+
}
|
|
101
|
+
export function initSummary(input) {
|
|
102
|
+
const checkout = input.changes.length === 0 ? { item: 'This checkout', state: 'done', detail: 'already set up' }
|
|
103
|
+
: input.made ? { item: 'This checkout', state: 'done', detail: `updated ${input.changes.join(', ')}` }
|
|
104
|
+
: { item: 'This checkout', state: 'todo', detail: `${input.changes.join(', ')} not changed yet` };
|
|
105
|
+
const notes = input.notes === null ? []
|
|
106
|
+
: [{ item: 'Your notes', state: input.status === 'done' ? 'done' : 'todo', detail: `${input.status === 'done' ? 'saved' : 'not saved yet'} (${input.notes.join(', ')})` }];
|
|
107
|
+
const checklist = [checkout, ...notes, onboardingCheck(input.onboarding), ...telemetryChecks(input.telemetry)];
|
|
108
|
+
return { ready: input.status === 'done' && input.onboarding?.state === 'ready', checklist, next: nextStep(input) };
|
|
109
|
+
}
|
|
110
|
+
const MARKS = { done: chalk.green('✓'), running: chalk.cyan('…'), todo: chalk.yellow('✗'), skipped: chalk.dim('–'),
|
|
111
|
+
optional: chalk.dim('○') };
|
|
112
|
+
export function formatInitSummary(summary) {
|
|
113
|
+
return [chalk.bold('Checklist'), ...summary.checklist.map(check => ` ${MARKS[check.state]} ${check.item}: ${check.detail}`), '',
|
|
114
|
+
chalk.bold(`Next: ${summary.next.text}`)];
|
|
115
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
* .git/info/exclude), when Claude Code is in use here;
|
|
11
11
|
* 2. a short marked block in AGENTS.md (and in CLAUDE.md when the repository
|
|
12
12
|
* keeps one) telling coding agents to run `haystack verify` after a change.
|
|
13
|
-
* 3. telemetry (
|
|
14
|
-
*
|
|
15
|
-
* until then only the telemetry report
|
|
13
|
+
* 3. with --telemetry only (or one of its answers: --app, --origin, --consent, --url-rewrites), telemetry
|
|
14
|
+
* (init-telemetry.ts, CAPTURE-V1 rule 8): the app's server instrumentation and the CLI dependency it needs, the
|
|
15
|
+
* settings proposal and the browser tag, once the user's answers are given; until then only the telemetry report
|
|
16
|
+
* stops, saying what to ask. It needs the app's owner (their yes, production's origins, a token, a deploy), so a
|
|
17
|
+
* plain init never plans it.
|
|
16
18
|
* It then starts onboarding the app at the base `haystack verify` would use, so
|
|
17
|
-
* the app is usually ready by the first verify.
|
|
19
|
+
* the app is usually ready by the first verify. Every run ends with a short checklist and exactly one next step
|
|
20
|
+
* (init-summary.ts): init does all it can in one run and stops only when it needs a person, needs to wait, or is done. With --notes, the coding agent's
|
|
18
21
|
* notes on how the app runs, signs people in and is used (onboarding-notes.ts)
|
|
19
22
|
* become the repository's onboarding notes first, so onboarding plans from them. Running it again changes only
|
|
20
23
|
* what is missing or out of date and shows where onboarding is.
|
|
@@ -36,7 +39,8 @@ import { findGitRoot } from '../utils/hooks.js';
|
|
|
36
39
|
import { assertNoSymlinkOnPath } from '../utils/safe-write.js';
|
|
37
40
|
import { CLAUDE_LOCAL_SETTINGS, CLAUDE_SHARED_SETTINGS, HAYSTACK_PRECOMPUTE_HOOK_COMMAND, claudeEntries, claudeSettingsPath, isCLIInstalled, isGitIgnored, isInstalledGlobally, isPrecomputeCommand, readClaudeSettings, } from './install-session-hooks.js';
|
|
38
41
|
import { NOTE_FIELDS, readNotes } from './onboarding-notes.js';
|
|
39
|
-
import { formatOnboarding, reportOnboardingState, startOnboarding, writeOnboardingNotes } from './verify-onboarding.js';
|
|
42
|
+
import { formatOnboarding, readOnboardingStatus, reportOnboardingState, startOnboarding, writeOnboardingNotes } from './verify-onboarding.js';
|
|
43
|
+
import { formatInitSummary, initSummary } from './init-summary.js';
|
|
40
44
|
import { deriveBaseSha, EXPLICIT_WALL_MS, resolveOriginRepository } from './verify-precompute.js';
|
|
41
45
|
const NOTE_BEGIN = '<!-- haystack:begin -->';
|
|
42
46
|
const NOTE_END = '<!-- haystack:end -->';
|
|
@@ -332,6 +336,8 @@ function applyAll(gitRoot, changes, say) {
|
|
|
332
336
|
}
|
|
333
337
|
}
|
|
334
338
|
const errorText = (error) => (error instanceof Error ? error.message : String(error));
|
|
339
|
+
/** A value as one shell word: as it is when plain, else single-quoted. */
|
|
340
|
+
const shellWord = (value) => (/^[\w@%+=:,./-]+$/u.test(value) ? value : `'${value.replace(/'/gu, `'\\''`)}'`);
|
|
335
341
|
function applyChange(gitRoot, planned) {
|
|
336
342
|
const inside = relative(gitRoot, planned.target);
|
|
337
343
|
if (!inside.startsWith('..') && !isAbsolute(inside))
|
|
@@ -395,18 +401,24 @@ export async function initCommand(options) {
|
|
|
395
401
|
// Read before anything else, so notes that cannot be used stop init with nothing done.
|
|
396
402
|
const notes = options.notes === undefined ? null : readNotes(options.notes);
|
|
397
403
|
const plan = planInit(gitRoot);
|
|
398
|
-
// Telemetry
|
|
399
|
-
// init
|
|
400
|
-
//
|
|
404
|
+
// Telemetry (CAPTURE-V1 rule 8) only when asked for: --telemetry, or one of its answers. It needs the app's owner, so a plain
|
|
405
|
+
// init, which a coding agent finishes alone, never plans it (Codex on langgenius/dify, 2026-10-07, got its questions and
|
|
406
|
+
// hundreds of candidates in the test copy's setup). A missing answer stops only it. It never fails init: whatever goes
|
|
407
|
+
// wrong with it (a file it cannot read, a package manager that fails, Haystack not answering) puts its own files back and
|
|
408
|
+
// leaves it manual with what happened, and the rest of init goes on.
|
|
409
|
+
const wantTelemetry = options.telemetry === true || options.app !== undefined || (options.origin?.length ?? 0) > 0
|
|
410
|
+
|| options.consent !== undefined || options.urlRewrites !== undefined;
|
|
401
411
|
const telemetryModule = await import('./init-telemetry.js');
|
|
402
412
|
const telemetryOptions = { app: options.app, origins: options.origin ?? [], consent: options.consent, urlRewrites: options.urlRewrites };
|
|
403
413
|
let telemetryFailure = null;
|
|
404
414
|
let telemetryPlan = telemetryModule.emptyTelemetryPlan();
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
415
|
+
if (wantTelemetry) {
|
|
416
|
+
try {
|
|
417
|
+
telemetryPlan = await telemetryModule.planTelemetry(gitRoot, repository, telemetryOptions);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
telemetryFailure = errorText(error);
|
|
421
|
+
}
|
|
410
422
|
}
|
|
411
423
|
const telemetryFrom = plan.changes.length;
|
|
412
424
|
plan.changes.push(...telemetryPlan.changes);
|
|
@@ -423,16 +435,32 @@ export async function initCommand(options) {
|
|
|
423
435
|
const report = () => (telemetryFailure !== null ? telemetryModule.failedTelemetry(telemetryPlan, telemetryFailure)
|
|
424
436
|
: telemetry ?? telemetryModule.previewTelemetry(telemetryPlan, repository, notes));
|
|
425
437
|
const showPreview = () => {
|
|
438
|
+
if (!wantTelemetry)
|
|
439
|
+
return;
|
|
426
440
|
const preview = report();
|
|
427
441
|
for (const line of telemetryModule.formatTelemetry(preview.telemetry, preview.steps))
|
|
428
442
|
say(line);
|
|
429
443
|
say('');
|
|
430
444
|
};
|
|
445
|
+
// This run's command again with --yes: what a run that only showed its changes asks for.
|
|
446
|
+
const rerunWithYes = ['haystack init --yes', ...(options.notes === undefined ? [] : [`--notes ${shellWord(options.notes)}`]),
|
|
447
|
+
...(options.telemetry ? ['--telemetry'] : []), ...(options.app === undefined ? [] : [`--app ${shellWord(options.app)}`]),
|
|
448
|
+
...(options.origin ?? []).map(value => `--origin ${shellWord(value)}`), ...(options.consent === undefined ? [] : [`--consent ${shellWord(options.consent)}`]),
|
|
449
|
+
...(options.urlRewrites === undefined ? [] : [`--url-rewrites ${shellWord(options.urlRewrites)}`])].join(' ');
|
|
450
|
+
let made = false;
|
|
431
451
|
const finish = (status, onboarding, exitCode) => {
|
|
452
|
+
const shown = wantTelemetry ? report() : null;
|
|
453
|
+
const summary = initSummary({ status, owner: origin.owner, changes: plan.changes.map(planned => planned.path), made,
|
|
454
|
+
notes: notes === null ? null : NOTE_FIELDS.filter(field => notes[field] !== ''), onboarding,
|
|
455
|
+
telemetry: shown === null ? null : { report: shown.telemetry, steps: shown.steps }, rerunWithYes, installUrl: HAYSTACK_APP_INSTALL_URL });
|
|
456
|
+
say('');
|
|
457
|
+
for (const line of formatInitSummary(summary))
|
|
458
|
+
say(line);
|
|
432
459
|
if (options.json) {
|
|
433
460
|
const changes = plan.changes.map(({ path, action, reason, diff }) => ({ path, action, reason, diff }));
|
|
434
|
-
process.stdout.write(`${JSON.stringify(withSchema('init', { status, repository,
|
|
435
|
-
|
|
461
|
+
process.stdout.write(`${JSON.stringify(withSchema('init', { status, repository, ready: summary.ready, checklist: summary.checklist,
|
|
462
|
+
next: summary.next, changes, notices: plan.notices, notes: notesWritten, onboarding, telemetry: shown?.telemetry ?? null,
|
|
463
|
+
telemetrySteps: shown?.steps ?? [] }), null, 2)}\n`);
|
|
436
464
|
}
|
|
437
465
|
process.exitCode = exitCode;
|
|
438
466
|
};
|
|
@@ -460,22 +488,34 @@ export async function initCommand(options) {
|
|
|
460
488
|
if (!(error instanceof NotLoggedInError))
|
|
461
489
|
throw error;
|
|
462
490
|
showPreview();
|
|
463
|
-
say(chalk.yellow('Not logged in, so nothing was changed.
|
|
491
|
+
say(chalk.yellow('Not logged in, so nothing was changed.'));
|
|
464
492
|
finish('login-required', null, EXIT_CODES['login-required']);
|
|
465
493
|
return;
|
|
466
494
|
}
|
|
467
495
|
if (plan.changes.length > 0 && !options.yes) {
|
|
468
|
-
|
|
496
|
+
// Never a prompt for a coding agent, even in a terminal it drives (Claude Code sets CLAUDECODE; agents share AI_AGENT):
|
|
497
|
+
// the run stops and names the flag instead.
|
|
498
|
+
const agent = Boolean(process.env.CLAUDECODE) || Boolean(process.env.AI_AGENT);
|
|
499
|
+
const interactive = !options.json && !agent && process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
469
500
|
if (!interactive || !await confirmed()) {
|
|
470
501
|
showPreview();
|
|
471
|
-
|
|
472
|
-
|
|
502
|
+
// Where the app stands, read without starting anything, so the checklist says it and an agent writes notes only for an
|
|
503
|
+
// app that is not set up yet (Codex on langgenius/dify, 2026-10-07, wrote notes for an app already set up).
|
|
504
|
+
let standing = null;
|
|
505
|
+
try {
|
|
506
|
+
standing = await readOnboardingStatus(repository, deriveBaseSha(gitRoot, Date.now() + EXPLICIT_WALL_MS), token);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
// Unread (the GitHub App not installed, Haystack not answering): the checklist says it has not started, as before.
|
|
510
|
+
}
|
|
511
|
+
finish('planned', standing, EXIT_CODES.planned);
|
|
473
512
|
return;
|
|
474
513
|
}
|
|
475
514
|
}
|
|
476
515
|
// Init's own changes (the agent instructions, the Stop hook) first, all or nothing: a failure there is init's.
|
|
477
516
|
try {
|
|
478
517
|
applyAll(gitRoot, plan.changes.slice(0, telemetryFrom), say);
|
|
518
|
+
made = true;
|
|
479
519
|
}
|
|
480
520
|
catch (error) {
|
|
481
521
|
throw new Error(`${errorText(error)} Every file init changed was restored; nothing was changed.`);
|
|
@@ -485,7 +525,7 @@ export async function initCommand(options) {
|
|
|
485
525
|
// without capture's, when it is refused).
|
|
486
526
|
const approved = options.yes === true || plan.changes.length > 0;
|
|
487
527
|
let capture = null;
|
|
488
|
-
if (telemetryFailure === null) {
|
|
528
|
+
if (wantTelemetry && telemetryFailure === null) {
|
|
489
529
|
try {
|
|
490
530
|
capture = telemetryPlan.captureRegistration
|
|
491
531
|
? await telemetryModule.prepareCapture(telemetryPlan.captureRegistration, token, say, approved) : null;
|
|
@@ -527,27 +567,28 @@ export async function initCommand(options) {
|
|
|
527
567
|
}
|
|
528
568
|
catch (error) {
|
|
529
569
|
if (error instanceof HaystackApiError && error.code === INSTALLATION_REQUIRED_CODE) {
|
|
530
|
-
say(chalk.yellow(`Haystack needs its GitHub App on ${origin.owner} to read the repository
|
|
531
|
-
say(` ${HAYSTACK_APP_INSTALL_URL}`);
|
|
532
|
-
say('then run `haystack init` again.');
|
|
570
|
+
say(chalk.yellow(`Haystack needs its GitHub App on ${origin.owner} to read the repository.`));
|
|
533
571
|
finish('app-required', null, EXIT_CODES['app-required']);
|
|
534
572
|
return;
|
|
535
573
|
}
|
|
536
574
|
const done = plan.changes.length > 0 ? 'The changes above were made, but onboarding' : 'Onboarding';
|
|
537
575
|
throw new Error(`${done} could not start: ${error instanceof Error ? error.message : String(error)} Run \`haystack init\` again to start it.`);
|
|
538
576
|
}
|
|
539
|
-
//
|
|
540
|
-
// Said in words, and in --json's notices for the coding agent, which otherwise reads "notes: written" as a new onboarding
|
|
541
|
-
// (Codex on langgenius/dify, 2026-10-07).
|
|
577
|
+
// Notes inform an incremental repair when the next check finds a broken setup; init itself does not rebuild the app.
|
|
542
578
|
if (notesWritten === 'written' && onboarding.state === 'ready') {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
579
|
+
// This init call only saved the notes. The check, not init, starts an incremental repair when one is needed.
|
|
580
|
+
const notices = [
|
|
581
|
+
'The app is already set up, so these notes do not change its setup: Haystack sets each app up once and keeps the notes.',
|
|
582
|
+
'Run `haystack verify` again: if its copy is broken, Haystack uses the saved notes to repair the existing setup and rerun the check.',
|
|
583
|
+
];
|
|
584
|
+
plan.notices.push(...notices);
|
|
585
|
+
for (const notice of notices)
|
|
586
|
+
say(chalk.dim(notice));
|
|
546
587
|
}
|
|
547
588
|
reportOnboardingState(onboarding);
|
|
548
589
|
say(formatOnboarding(onboarding));
|
|
549
590
|
say('');
|
|
550
|
-
if (telemetryFailure === null) {
|
|
591
|
+
if (wantTelemetry && telemetryFailure === null) {
|
|
551
592
|
try {
|
|
552
593
|
telemetry = await telemetryModule.telemetryReport(telemetryPlan, { repository, token, onboarding, notes });
|
|
553
594
|
}
|
|
@@ -557,15 +598,10 @@ export async function initCommand(options) {
|
|
|
557
598
|
telemetry = { telemetry: preview.telemetry, steps: [...preview.steps, `Run \`haystack init --json\` again to read what telemetry Haystack has received (it could not: ${errorText(error)}).`] };
|
|
558
599
|
}
|
|
559
600
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
if (onboarding.state === 'ready')
|
|
565
|
-
say(chalk.green('Ready. After your next change, run `haystack verify`.'));
|
|
566
|
-
else if (onboarding.state === 'onboarding') {
|
|
567
|
-
say(chalk.green('Haystack is preparing the app in the background; the first time can take half an hour.'));
|
|
568
|
-
say('Keep working: `haystack verify` waits for it when needed, and `haystack verify onboarding --wait` follows it.');
|
|
601
|
+
if (wantTelemetry) {
|
|
602
|
+
const shown = report();
|
|
603
|
+
for (const line of telemetryModule.formatTelemetry(shown.telemetry, shown.steps))
|
|
604
|
+
say(line);
|
|
569
605
|
}
|
|
570
606
|
finish('done', onboarding, setUpExitCode(onboarding));
|
|
571
607
|
}
|
|
@@ -117,6 +117,8 @@ export function parseOnboardingStatus(value, expected) {
|
|
|
117
117
|
const recordUpdate = value.recordUpdate;
|
|
118
118
|
if (!(recordUpdate === null || (ready && isRecord(recordUpdate) && typeof recordUpdate.of === 'string' && SHA256.test(recordUpdate.of)
|
|
119
119
|
&& typeof recordUpdate.crawlRunId === 'string' && (recordUpdate.side === 'base' || recordUpdate.side === 'head')
|
|
120
|
+
&& (recordUpdate.cause === undefined || recordUpdate.cause === 'world-build-failed' || recordUpdate.cause === 'app-broken')
|
|
121
|
+
&& (recordUpdate.cause !== 'app-broken' || recordUpdate.side === 'base')
|
|
120
122
|
&& typeof recordUpdate.workCommit === 'string' && COMMIT.test(recordUpdate.workCommit)
|
|
121
123
|
&& Array.isArray(recordUpdate.changes) && recordUpdate.changes.every(change => isRecord(change) && typeof change.field === 'string'
|
|
122
124
|
&& (change.name === null || typeof change.name === 'string') && (change.kind === 'added' || change.kind === 'changed')
|
|
@@ -125,6 +127,8 @@ export function parseOnboardingStatus(value, expected) {
|
|
|
125
127
|
const update = value.update;
|
|
126
128
|
if (!(update === null || (ready && isRecord(update) && typeof update.onboardRunId === 'string' && ONBOARD_RUN_ID.test(update.onboardRunId)
|
|
127
129
|
&& (update.side === 'base' || update.side === 'head') && typeof update.crawlRunId === 'string'
|
|
130
|
+
&& (update.cause === 'world-build-failed' || update.cause === 'app-broken')
|
|
131
|
+
&& (update.cause !== 'app-broken' || update.side === 'base')
|
|
128
132
|
&& (update.state === 'updating' || update.state === 'blocked' || update.state === 'stopped')
|
|
129
133
|
&& (update.state === 'blocked') === (update.block !== null))))
|
|
130
134
|
invalid('its update');
|
|
@@ -349,18 +353,19 @@ export function formatOnboarding(status) {
|
|
|
349
353
|
* line each, and a blocked update's block (what it needs, and what to tell Haystack). */
|
|
350
354
|
export function updateLines(status) {
|
|
351
355
|
const lines = [];
|
|
352
|
-
const failed = (side) =>
|
|
356
|
+
const failed = (side, cause) => cause === 'app-broken'
|
|
357
|
+
? 'the app copy was broken' : side === 'base' ? 'the app did not start at its base commit' : 'the change did not start';
|
|
353
358
|
if (status.recordUpdate) {
|
|
354
359
|
const changes = status.recordUpdate.changes.map(change => `${change.kind} ${change.field === 'env' ? 'variable' : change.field}`
|
|
355
360
|
+ `${change.name === null ? '' : ` ${change.name}`}${change.evidence.length ? ` (from ${change.evidence.join(', ')})` : ''}`);
|
|
356
|
-
lines.push(`Its setup was updated in place after check ${status.recordUpdate.crawlRunId}, where ${failed(status.recordUpdate.side)}`
|
|
361
|
+
lines.push(`Its setup was updated in place after check ${status.recordUpdate.crawlRunId}, where ${failed(status.recordUpdate.side, status.recordUpdate.cause)}`
|
|
357
362
|
+ `${changes.length ? `: ${changes.join('; ')}` : ''}.`);
|
|
358
363
|
}
|
|
359
364
|
const update = status.update;
|
|
360
365
|
if (update === null)
|
|
361
366
|
return lines;
|
|
362
367
|
if (update.state === 'updating') {
|
|
363
|
-
lines.push(`Updating the setup (${update.onboardRunId}): check ${update.crawlRunId} found ${failed(update.side)}. The check runs `
|
|
368
|
+
lines.push(`Updating the setup (${update.onboardRunId}): check ${update.crawlRunId} found ${failed(update.side, update.cause)}. The check runs `
|
|
364
369
|
+ 'again on the updated setup when it is ready.');
|
|
365
370
|
}
|
|
366
371
|
else if (update.state === 'stopped') {
|
|
@@ -370,7 +375,7 @@ export function updateLines(status) {
|
|
|
370
375
|
else {
|
|
371
376
|
lines.push(update.side === 'head'
|
|
372
377
|
? chalk.bold(`Finding: check ${update.crawlRunId}'s change does not start, and no change to the setup started it.`)
|
|
373
|
-
: chalk.bold(`Haystack could not update the setup for check ${update.crawlRunId}, where
|
|
378
|
+
: chalk.bold(`Haystack could not update the setup for check ${update.crawlRunId}, where ${failed(update.side, update.cause)}.`));
|
|
374
379
|
if (update.block)
|
|
375
380
|
lines.push(...blockLines(update.block));
|
|
376
381
|
}
|
|
@@ -390,12 +395,16 @@ function age(ms) {
|
|
|
390
395
|
: [Math.floor(minutes / 1_440), 'day'];
|
|
391
396
|
return count === 0 ? 'just now' : `${count} ${unit}${count === 1 ? '' : 's'} ago`;
|
|
392
397
|
}
|
|
393
|
-
/** Rule 17 (
|
|
398
|
+
/** Rule 17 (amendments 32 and 39): an onboarding the independent review skipped, as the sentence every result shows, saying why
|
|
399
|
+
* for each reason the contract names (a reason this CLI does not know yet is still shown, as the reason the server gave); none
|
|
400
|
+
* otherwise. */
|
|
394
401
|
export function reviewNotes(review) {
|
|
395
402
|
if (review?.verdict !== 'skipped')
|
|
396
403
|
return [];
|
|
397
|
-
|
|
398
|
-
|
|
404
|
+
const why = review.reason === 'internal-test-tenant' ? 'is turned off for this team'
|
|
405
|
+
: review.reason === 'internal-test-requester' ? 'is turned off for onboardings Haystack\'s own people or automation start'
|
|
406
|
+
: 'did not run';
|
|
407
|
+
return [`This onboarding was NOT reviewed: the independent review that checks the setup did not cheat ${why} (${review.reason}).`];
|
|
399
408
|
}
|
|
400
409
|
/** Rule 15(g), amendment 3: a stand-in gap as the sentence every result shows. */
|
|
401
410
|
export function standInGapNotes(gaps) {
|
package/dist/commands/verify.js
CHANGED
|
@@ -687,12 +687,13 @@ function staleCrawl(found, identity) {
|
|
|
687
687
|
return found.mode === 'prepare' || found.status === 'prepared' ? 'prepare-only' : null;
|
|
688
688
|
}
|
|
689
689
|
function staleWords(found, stale) {
|
|
690
|
+
const kind = found.mode === 'prepare' ? 'preparation' : 'crawl';
|
|
690
691
|
switch (stale) {
|
|
691
|
-
case 'retitled': return `The last
|
|
692
|
-
case 'other-time': return `The last
|
|
693
|
-
case 'cancelled': return `This change's last
|
|
694
|
-
case 'incomplete': return `This change's last
|
|
695
|
-
case 'cancelling': return `This change's
|
|
692
|
+
case 'retitled': return `The last ${kind} of this code ran under another title, "${safe(found.changeTitle)}" (${found.runId})`;
|
|
693
|
+
case 'other-time': return `The last ${kind} of this code was asked for ${timeWords(found.budgetMs)} (${found.runId})`;
|
|
694
|
+
case 'cancelled': return `This change's last ${kind} was cancelled (${found.runId})`;
|
|
695
|
+
case 'incomplete': return `This change's last ${kind} stopped before finishing (${found.runId})`;
|
|
696
|
+
case 'cancelling': return `This change's ${kind} is being cancelled (${found.runId})`;
|
|
696
697
|
case 'prepare-only': return found.status === 'prepared'
|
|
697
698
|
? `Your change was built and frozen ahead of time (${found.runId})`
|
|
698
699
|
: `Your change is being built and frozen ahead of time (${found.runId})`;
|
|
@@ -728,7 +729,7 @@ async function submitCapture(capture, token, found) {
|
|
|
728
729
|
if (found === null) {
|
|
729
730
|
return { runId: crawl.runId, onboarding: null, line: crawl.status === 'queued'
|
|
730
731
|
? `No crawl existed for this change yet; ${preparing ? 'started preparing it' : 'started one'} (${crawl.runId}).`
|
|
731
|
-
: `The service already had a
|
|
732
|
+
: `The service already had a check for this change (${crawl.runId}).` };
|
|
732
733
|
}
|
|
733
734
|
if (!anotherCrawl(found.stale) && crawl.runId === found.view.runId) {
|
|
734
735
|
return { runId: crawl.runId, onboarding: null, line: found.stale === 'prepare-only'
|
package/dist/index.js
CHANGED
|
@@ -89,7 +89,8 @@ program
|
|
|
89
89
|
.description('Set this repository up for `haystack verify` and start onboarding the app')
|
|
90
90
|
.option('-y, --yes', 'Make the changes without asking')
|
|
91
91
|
.option('--notes <file>', 'A JSON file (- for stdin) of what you know about running the app; see below')
|
|
92
|
-
.option('--
|
|
92
|
+
.option('--telemetry', 'Also set up tracking of how real users use the live app (optional; needs the app\'s owner); see below')
|
|
93
|
+
.option('--app <dir>', 'With --telemetry: the app whose telemetry init sets up, when the repository has several')
|
|
93
94
|
.option('--origin <url>', 'A production origin of the app (repeat for several): browser capture accepts events only from these', (value, previous = []) => [...previous, value])
|
|
94
95
|
.option('--consent <choice>', 'How the site asks visitors for consent: its consent platform, or not-required when your user decided so')
|
|
95
96
|
.option('--url-rewrites <answer>', 'When init asks: none, or hidden-segment:<name> when the app\'s middleware adds a leading [<name>] segment to page paths')
|
|
@@ -105,12 +106,19 @@ it with --yes (or a yes at the prompt):
|
|
|
105
106
|
• a short note in AGENTS.md (and CLAUDE.md when the repository keeps one)
|
|
106
107
|
telling coding agents to run \`haystack verify\` after a change
|
|
107
108
|
Then it starts onboarding the app, so it is usually ready by the first
|
|
108
|
-
\`haystack verify\`.
|
|
109
|
-
|
|
109
|
+
\`haystack verify\`.
|
|
110
|
+
|
|
111
|
+
Every run does all it can, then ends with a short checklist and exactly one
|
|
112
|
+
"Next:" line: a command to run, a question to ask your user, a wait, or
|
|
113
|
+
"Ready". Keep running what Next says until it says Ready; running init again
|
|
114
|
+
changes only what is missing and picks up where it stopped. It never asks a
|
|
115
|
+
yes/no question when a coding agent runs it: it shows the changes and says to
|
|
116
|
+
run it again with --yes.
|
|
110
117
|
|
|
111
118
|
--notes tells onboarding what a coding agent working here already knows, so it
|
|
112
119
|
does not have to work it out (a wrong guess at the start command can cost half
|
|
113
|
-
an hour).
|
|
120
|
+
an hour). Run \`haystack init\` without --yes first: it changes nothing and says
|
|
121
|
+
whether the app is already set up, when notes are not needed. The file is JSON with any of three notes, each plain text:
|
|
114
122
|
run how production installs, builds and starts the app: the commands
|
|
115
123
|
and their directory, the port, the databases and services and their
|
|
116
124
|
versions, migrations, the environment variables it needs (names and
|
|
@@ -123,10 +131,13 @@ onboarding (an onboarding that stopped on a question starts again with them);
|
|
|
123
131
|
after it, the next check that cannot start the app uses them to update the
|
|
124
132
|
setup in place.
|
|
125
133
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
134
|
+
--telemetry (optional, after the app is ready) also sets up tracking of how
|
|
135
|
+
real users use the live app. It needs the app's owner: their yes, the app's
|
|
136
|
+
live web address, a token and a deploy, so a plain init never does it. --app
|
|
137
|
+
names the app when the repository has several (init never picks one), --origin
|
|
138
|
+
its production origins and --consent the consent choice (each implies
|
|
139
|
+
--telemetry); until they are given, only telemetry waits and Next says what
|
|
140
|
+
to ask. Server telemetry: a Next.js app's next.config
|
|
130
141
|
gains the withHaystackTelemetry wrapper in its own export form; a Node server
|
|
131
142
|
whose build ends in a \`tsc\` compile gains \`haystack telemetry instrument\`
|
|
132
143
|
after it; the CLI becomes a checked, exact-version dependency through the
|
|
@@ -134,10 +145,11 @@ app's package manager; \`haystack telemetry settings --propose\` writes a
|
|
|
134
145
|
settings proposal that only your user approves (\`--approve\`). Each is a
|
|
135
146
|
change shown first like the others, and a failed install restores every file.
|
|
136
147
|
init never mints the ingest token: a repository admin runs
|
|
137
|
-
\`haystack telemetry token\` themselves. \`--json\` reports
|
|
138
|
-
(each part installed, manual or unsupported, and what Haystack
|
|
139
|
-
and \`telemetrySteps\` (what the person does next, including
|
|
140
|
-
HAYSTACK_TELEMETRY=0)
|
|
148
|
+
\`haystack telemetry token\` themselves. With --telemetry, \`--json\` reports
|
|
149
|
+
\`telemetry\` (each part installed, manual or unsupported, and what Haystack
|
|
150
|
+
last received) and \`telemetrySteps\` (what the person does next, including
|
|
151
|
+
the off switch HAYSTACK_TELEMETRY=0); \`--json\` always reports \`ready\`,
|
|
152
|
+
\`checklist\` and \`next\`.
|
|
141
153
|
|
|
142
154
|
Browser capture: --consent is cookiebot, onetrust:<category id>, not-required
|
|
143
155
|
or manual. init adds the self-hosted tag, its consent wiring and the build
|
|
@@ -159,6 +171,7 @@ Examples:
|
|
|
159
171
|
haystack init
|
|
160
172
|
haystack init --yes
|
|
161
173
|
haystack init --yes --json --notes haystack-notes.json
|
|
174
|
+
haystack init --telemetry
|
|
162
175
|
`)
|
|
163
176
|
.action(options => runPublicCommand(() => initCommand(options), options.json));
|
|
164
177
|
const verify = program
|
|
@@ -215,9 +228,11 @@ own setup, its own requests failing in both builds), the crawl could not test
|
|
|
215
228
|
your change, and it says so first: "We couldn't test your change: the app copy
|
|
216
229
|
is broken.", why, what the start screen showed, the requests that failed and
|
|
217
230
|
the console errors, and what to do: find what those errors need in the
|
|
218
|
-
repository's own setup (docker compose files, env examples, docs),
|
|
219
|
-
|
|
220
|
-
|
|
231
|
+
repository's own setup (docker compose files, env examples, docs), write that
|
|
232
|
+
in the "run" field of a JSON notes file, then run \`haystack init --notes <file>\`
|
|
233
|
+
and \`haystack verify\` again. Haystack repairs the existing setup with those
|
|
234
|
+
notes and reruns the check when ready. Tell your user this check did not test
|
|
235
|
+
the change. What the crawl
|
|
221
236
|
found in the broken copy is listed after it, marked as such.
|
|
222
237
|
|
|
223
238
|
The first verify on a base the service has not onboarded yet prepares the app
|
package/dist/schema.js
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://docs.haystackeditor.com/cli/schemas/init.v2.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. `notes` says what became of `--notes`: \"written\" (the repository's onboarding notes are now these), \"unchanged\" (they already were), null without `--notes` or when init stopped before writing them. 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). `telemetry` (CAPTURE-V1 rule 8e, InitTelemetryReport in capture_contracts.ts) is what telemetry does (status done) or will do (planned, login-required: the same report before anything is written, every `received` null because nothing was checked): per part (`capture`, the browser tag; `server`, server telemetry) `installed`, `manual` with the exact `steps`, or `unsupported` with `why`, and `received`, the newest telemetry Haystack holds for this application (null when none; `stale` past 24 hours), plus `dbProfileCommand`, the `haystack db profile` command for the user to run with production read access (null when the app's database is not one it profiles). `telemetrySteps` is what the person does next for telemetry to arrive, in order, including the off switches; [] without telemetry. 2.0.0 (2026-10-07): every run ends with `ready`, a `checklist` and exactly one `next` step; telemetry is planned only with --telemetry (or one of its answers), so `telemetry` is null and `telemetrySteps` empty without it.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"schema_version",
|
|
10
|
+
"status",
|
|
11
|
+
"repository",
|
|
12
|
+
"ready",
|
|
13
|
+
"checklist",
|
|
14
|
+
"next",
|
|
15
|
+
"changes",
|
|
16
|
+
"notices",
|
|
17
|
+
"notes",
|
|
18
|
+
"onboarding",
|
|
19
|
+
"telemetry",
|
|
20
|
+
"telemetrySteps"
|
|
21
|
+
],
|
|
22
|
+
"properties": {
|
|
23
|
+
"schema_version": {
|
|
24
|
+
"const": "2.0.0"
|
|
25
|
+
},
|
|
26
|
+
"status": {
|
|
27
|
+
"enum": [
|
|
28
|
+
"done",
|
|
29
|
+
"planned",
|
|
30
|
+
"login-required",
|
|
31
|
+
"app-required"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "string"
|
|
36
|
+
},
|
|
37
|
+
"ready": {
|
|
38
|
+
"type": "boolean",
|
|
39
|
+
"description": "The test copy of the app is set up: run `haystack verify` after each change."
|
|
40
|
+
},
|
|
41
|
+
"checklist": {
|
|
42
|
+
"type": "array",
|
|
43
|
+
"description": "What is done and what is left, in order.",
|
|
44
|
+
"items": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"additionalProperties": false,
|
|
47
|
+
"required": [
|
|
48
|
+
"item",
|
|
49
|
+
"state",
|
|
50
|
+
"detail"
|
|
51
|
+
],
|
|
52
|
+
"properties": {
|
|
53
|
+
"item": {
|
|
54
|
+
"type": "string"
|
|
55
|
+
},
|
|
56
|
+
"state": {
|
|
57
|
+
"enum": [
|
|
58
|
+
"done",
|
|
59
|
+
"running",
|
|
60
|
+
"todo",
|
|
61
|
+
"skipped",
|
|
62
|
+
"optional"
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
"detail": {
|
|
66
|
+
"type": "string"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"next": {
|
|
72
|
+
"type": "object",
|
|
73
|
+
"additionalProperties": false,
|
|
74
|
+
"required": [
|
|
75
|
+
"kind",
|
|
76
|
+
"text",
|
|
77
|
+
"command"
|
|
78
|
+
],
|
|
79
|
+
"description": "Exactly one next step: run (a command the coding agent runs), ask-user (only the user can do or answer it; text says what to ask), wait (a long step to wait out, then run `command`), none (nothing left, or nothing that can be run).",
|
|
80
|
+
"properties": {
|
|
81
|
+
"kind": {
|
|
82
|
+
"enum": [
|
|
83
|
+
"run",
|
|
84
|
+
"ask-user",
|
|
85
|
+
"wait",
|
|
86
|
+
"none"
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"text": {
|
|
90
|
+
"type": "string"
|
|
91
|
+
},
|
|
92
|
+
"command": {
|
|
93
|
+
"type": [
|
|
94
|
+
"string",
|
|
95
|
+
"null"
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"changes": {
|
|
101
|
+
"type": "array",
|
|
102
|
+
"items": {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"additionalProperties": false,
|
|
105
|
+
"required": [
|
|
106
|
+
"path",
|
|
107
|
+
"action",
|
|
108
|
+
"reason",
|
|
109
|
+
"diff"
|
|
110
|
+
],
|
|
111
|
+
"properties": {
|
|
112
|
+
"path": {
|
|
113
|
+
"type": "string"
|
|
114
|
+
},
|
|
115
|
+
"action": {
|
|
116
|
+
"enum": [
|
|
117
|
+
"create",
|
|
118
|
+
"update",
|
|
119
|
+
"delete"
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
"reason": {
|
|
123
|
+
"type": "string"
|
|
124
|
+
},
|
|
125
|
+
"diff": {
|
|
126
|
+
"type": "array",
|
|
127
|
+
"items": {
|
|
128
|
+
"type": "string"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"notices": {
|
|
135
|
+
"type": "array",
|
|
136
|
+
"items": {
|
|
137
|
+
"type": "string"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"notes": {
|
|
141
|
+
"oneOf": [
|
|
142
|
+
{
|
|
143
|
+
"type": "null"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"enum": [
|
|
147
|
+
"written",
|
|
148
|
+
"unchanged"
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
"onboarding": {
|
|
154
|
+
"oneOf": [
|
|
155
|
+
{
|
|
156
|
+
"type": "null"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"$ref": "verify.v1.json#/$defs/onboardingStatus"
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
},
|
|
163
|
+
"telemetry": {
|
|
164
|
+
"description": "Null unless telemetry was asked for (--telemetry or one of its answers).",
|
|
165
|
+
"anyOf": [
|
|
166
|
+
{
|
|
167
|
+
"type": "null"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"oneOf": [
|
|
171
|
+
{
|
|
172
|
+
"type": "null"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"type": "object",
|
|
176
|
+
"additionalProperties": false,
|
|
177
|
+
"required": [
|
|
178
|
+
"capture",
|
|
179
|
+
"server",
|
|
180
|
+
"dbProfileCommand"
|
|
181
|
+
],
|
|
182
|
+
"properties": {
|
|
183
|
+
"capture": {
|
|
184
|
+
"$ref": "#/$defs/telemetryPart"
|
|
185
|
+
},
|
|
186
|
+
"server": {
|
|
187
|
+
"$ref": "#/$defs/telemetryPart"
|
|
188
|
+
},
|
|
189
|
+
"dbProfileCommand": {
|
|
190
|
+
"type": [
|
|
191
|
+
"string",
|
|
192
|
+
"null"
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
},
|
|
201
|
+
"telemetrySteps": {
|
|
202
|
+
"type": "array",
|
|
203
|
+
"items": {
|
|
204
|
+
"type": "string"
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
"$defs": {
|
|
209
|
+
"telemetryPart": {
|
|
210
|
+
"oneOf": [
|
|
211
|
+
{
|
|
212
|
+
"type": "object",
|
|
213
|
+
"additionalProperties": false,
|
|
214
|
+
"required": [
|
|
215
|
+
"status",
|
|
216
|
+
"received"
|
|
217
|
+
],
|
|
218
|
+
"properties": {
|
|
219
|
+
"status": {
|
|
220
|
+
"const": "installed"
|
|
221
|
+
},
|
|
222
|
+
"received": {
|
|
223
|
+
"$ref": "#/$defs/telemetryReceived"
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"type": "object",
|
|
229
|
+
"additionalProperties": false,
|
|
230
|
+
"required": [
|
|
231
|
+
"status",
|
|
232
|
+
"steps",
|
|
233
|
+
"received"
|
|
234
|
+
],
|
|
235
|
+
"properties": {
|
|
236
|
+
"status": {
|
|
237
|
+
"const": "manual"
|
|
238
|
+
},
|
|
239
|
+
"steps": {
|
|
240
|
+
"type": "array",
|
|
241
|
+
"items": {
|
|
242
|
+
"type": "string"
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
"received": {
|
|
246
|
+
"$ref": "#/$defs/telemetryReceived"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"type": "object",
|
|
252
|
+
"additionalProperties": false,
|
|
253
|
+
"required": [
|
|
254
|
+
"status",
|
|
255
|
+
"why"
|
|
256
|
+
],
|
|
257
|
+
"properties": {
|
|
258
|
+
"status": {
|
|
259
|
+
"const": "unsupported"
|
|
260
|
+
},
|
|
261
|
+
"why": {
|
|
262
|
+
"type": "string"
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
]
|
|
267
|
+
},
|
|
268
|
+
"telemetryReceived": {
|
|
269
|
+
"oneOf": [
|
|
270
|
+
{
|
|
271
|
+
"type": "null"
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"type": "object",
|
|
275
|
+
"additionalProperties": false,
|
|
276
|
+
"required": [
|
|
277
|
+
"lastSeenAt",
|
|
278
|
+
"release",
|
|
279
|
+
"stale"
|
|
280
|
+
],
|
|
281
|
+
"properties": {
|
|
282
|
+
"lastSeenAt": {
|
|
283
|
+
"type": "string"
|
|
284
|
+
},
|
|
285
|
+
"release": {
|
|
286
|
+
"type": [
|
|
287
|
+
"string",
|
|
288
|
+
"null"
|
|
289
|
+
]
|
|
290
|
+
},
|
|
291
|
+
"stale": {
|
|
292
|
+
"type": "boolean"
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|