@vigilbase/cli
Version:
Vigilbase CLI — security scanners, agent tools, and Vigilbase One from the terminal.
1,545 lines • 82 kB
JavaScript
/**
* `vigilbase cloudflare-checkup [options]`
* ≡ `vigilbase local cloudflare-checkup [run] [options]`, plus the advanced
* `login|doctor|accounts|collect|inspect|submit|logout` subcommands.
*
* The one command verifies the user's work email (vigilbase.com emails a
* 6-digit code), finds (or, after asking, installs) Cloudflare's cf CLI, signs
* in to Cloudflare with a read-only cf login, lets the user choose the
* account, collects a minimised snapshot with a fixed allowlist of read-only
* cf commands, shows what it contains, uploads exactly that file to
* vigilbase.com after an explicit confirmation and opens the results page.
* cf owns the Cloudflare login: this process never reads, prints, stores or
* uploads a Cloudflare credential. See LOCAL.md.
*/
import { access, constants, stat } from 'node:fs/promises';
import { homedir } from 'node:os';
import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
import { z } from 'zod';
import { CLI_VERSION } from '../version.js';
import { resolveConfigDir } from '../lib/config.js';
import { isHeadlessEnvironment, openUrlInBrowser } from '../lib/open-url.js';
import { shouldShowProgress } from '../lib/progress.js';
import { CF_SUPPORTED, NOT_COLLECTABLE_ZONE_KINDS, PRODUCTION_ORIGIN, RECOMMENDED_CF_SCOPES, SNAPSHOT_COLLECTOR_NAME, SNAPSHOT_LIMITS, SNAPSHOT_SCHEMA_VERSION, } from '../local/cloudflare-checkup/contract.js';
import { ENV_TOKEN_AUTH_HINT, LOCAL_ERROR_HINTS, LocalCheckupError } from '../local/cloudflare-checkup/errors.js';
import { CF_TOKEN_ENV, buildChildEnv, childEnvVariableNames, hasEnvToken, missingEnvTokenMessage, strippedTokenNotice, } from '../local/cloudflare-checkup/cf/child-env.js';
import { assertLocalRuntimeSupported, checkCfVersion, isNodeSupported, locateCf } from '../local/cloudflare-checkup/cf/locate.js';
import { CfInstallError, MANAGED_CF, ensurePrivateDir, findManagedCf, installManagedCf, managedCfRoot, removeManagedCf } from '../local/cloudflare-checkup/cf/install.js';
import { withCfRunner } from '../local/cloudflare-checkup/cf/run-cf.js';
import { CRITICAL_TEMPLATES, DEFAULT_PROFILE, PREFLIGHT_ACCOUNT_ID, TEMPLATES, TEMPLATE_IDS, dryRunMatches, loginArgv, logoutArgv, preflightChecks, templateDisplay, validateProfileName, versionArgv, whoamiArgv, } from '../local/cloudflare-checkup/cf/allowlist.js';
import { CfScheduler, authSourceFor, doctorAuth, isReadOnlyScope } from '../local/cloudflare-checkup/cf/auth-lifecycle.js';
import { RequestBudget, estimateBudgetSeconds, parseRequestBudget, systemClock } from '../local/cloudflare-checkup/rate-budget.js';
import { buildSnapshot, collectAccount, estimateRequests, listAccounts } from '../local/cloudflare-checkup/collector.js';
import { accountName, countEntries, countEntriesByKind, dataCategories, loadSnapshotFile, writeSnapshotFile, zoneCount, } from '../local/cloudflare-checkup/snapshot-file.js';
import { confirmDefaultYes, confirmUpload, hasInteractiveInput, promptLine } from '../local/cloudflare-checkup/prompts.js';
import { SAME_ADDRESS_WAIT, emailLocale, parseEmailFlag, verifyEmail } from '../local/cloudflare-checkup/email-verification.js';
import { SubmitClient, VerificationClient, assertServerAccepts, displayRequester, isLoopbackOrigin, resolveSubmitOrigin, } from '../local/cloudflare-checkup/submit-client.js';
import { displayText, formatBytes, formatClock, formatDuration, maskId, readsSummary, shortSha, startCountsProgress } from '../local/cloudflare-checkup/output.js';
import { CheckupTui } from '../local/cloudflare-checkup/terminal-ui.js';
import { EXIT, assertCommandFlags, ok, outputJson } from './helpers.js';
/**
* How the page, the docs and the hints spell the command: the bare package,
* no version. vigilbase.com refuses CLIs older than it accepts, and that
* error's hint (CLI_UPGRADE_REQUIRED) names `@latest` to get past a cached copy.
*/
export const NPX_COMMAND = 'npx @vigilbase/cli';
const BANNER = 'Vigilbase Cloudflare Checkup · collects on this machine with Cloudflare\'s cf CLI';
const SUBCOMMANDS = ['run', 'login', 'doctor', 'accounts', 'collect', 'inspect', 'submit', 'logout'];
const usage = `Usage: vigilbase cloudflare-checkup [${SUBCOMMANDS.join('|')}] [options]`;
export const LOCAL_HELP = `Usage: vigilbase cloudflare-checkup [options]
vigilbase local cloudflare-checkup [<command>] [options]
Runs the Vigilbase Cloudflare Checkup on this machine, in one command:
${NPX_COMMAND} cloudflare-checkup
It asks for your work email and emails you a 6-digit code to enter, installs
Cloudflare's cf CLI if it is missing (asks first), signs in to Cloudflare with
a read-only cf login, lets you choose the account, collects a snapshot, shows
what it contains, uploads it only after you confirm and opens the results.
cf owns your Cloudflare login; this CLI never reads, prints, stores or
uploads it.
Options (the default command, also called run):
--email <address> Your work email (asked for when omitted)
--account-id <id> Skip the account picker
--out <file> Snapshot path (default: <config dir>/checkups/…);
--overwrite replaces an existing file
--yes Install cf and upload without asking
(--confirm answers only the upload question)
--no-open Print the results link; do not open a browser
--cf-auth auto|profile|env|default
auto (default): env when ${CF_TOKEN_ENV} is set
in this shell (cf reads it itself), else profile
--profile <name> cf profile (default ${DEFAULT_PROFILE})
--cf-bin <path> Use this cf package; never install one
--max-requests-per-5min <n> Cloudflare request budget, 100..1100 (default 900)
--timeout <milliseconds> · --json
Without a terminal, add --yes: the email (unless --email is given) and the
code are read from stdin, one line each, after their prompts on stderr.
Advanced commands:
login Create or refresh the read-only cf profile
[--profile] [--no-open] [--dry-run] [--cf-bin]
doctor Check Node, cf, the offline command mapping and cf auth
accounts List the Cloudflare account IDs cf can read
collect --out <file> Collect only; --dry-run shows every cf command (offline)
inspect <file> Validate a snapshot and show what it contains
submit <file> Verify your email, then upload a collected snapshot
[--email] [--yes] [--no-open]
logout Delete the read-only cf profile and the cf this CLI
installed [--profile] [--cf-bin]
Results go to the verified email address. Uploads go only to
${PRODUCTION_ORIGIN} (or a loopback --base-url).
Exit codes: 0 success, 1 error, 2 rate limited. See LOCAL.md for details.`;
const CF_FLAGS = ['profile', 'cf-auth', 'cf-bin'];
const COLLECT_FLAGS = [...CF_FLAGS, 'account-id', 'out', 'isOverwrite', 'max-requests-per-5min'];
const SUBMIT_FLAGS = ['email', 'isNoOpen', 'isConfirm', 'isYes', 'base-url', 'timeout'];
const ALLOWED_FLAGS = {
run: [...COLLECT_FLAGS, ...SUBMIT_FLAGS, 'isJson'],
login: ['profile', 'cf-bin', 'isDryRun', 'isNoOpen', 'isJson'],
doctor: [...CF_FLAGS, 'isJson'],
accounts: [...CF_FLAGS, 'isJson'],
collect: [...COLLECT_FLAGS, 'isDryRun', 'isJson'],
inspect: ['isJson'],
submit: [...SUBMIT_FLAGS, 'isJson'],
logout: ['profile', 'cf-bin', 'isJson'],
};
/** Validate flags before any spawn, file or network access. */
function assertLocalFlags(ctx, subcommand) {
const allowed = ALLOWED_FLAGS[subcommand];
assertCommandFlags(ctx, allowed);
// assertCommandFlags always permits the global origin flags; local commands do not.
for (const key of ['base-url', 'one-url', 'auth-url', 'client-id', 'timeout']) {
if (ctx.values[key] !== undefined && !allowed.includes(key)) {
throw new Error(`--${key} is not supported for cloudflare-checkup ${subcommand}.`);
}
}
}
const accountIdSchema = z.string()
.regex(/^[0-9a-fA-F]{32}$/, '--account-id must contain 32 hexadecimal characters.')
.transform((id) => id.toLowerCase());
const pathSchema = (flag) => z.string()
.min(1, `${flag} must not be empty.`)
.max(4096, `${flag} is too long.`)
.refine((value) => !value.includes('\0'), `${flag} must not contain NUL characters.`);
function parse(schema, value) {
const result = schema.safeParse(value);
if (!result.success)
throw new Error(result.error.issues.map((issue) => issue.message).join(' '));
return result.data;
}
function parseLocalOptions(ctx, subcommand, args) {
const takesFile = subcommand === 'inspect' || subcommand === 'submit';
if (takesFile ? args.length !== 1 : args.length !== 0) {
const fileUsage = `Usage: vigilbase cloudflare-checkup ${subcommand} <file>`;
throw new Error(!takesFile ? usage : args.length === 0 ? `Missing <file>. ${fileUsage}` : fileUsage);
}
const values = ctx.values;
const requested = parse(z.enum(['auto', 'profile', 'env', 'default'], { message: '--cf-auth must be auto, profile, env or default.' }), values['cf-auth'] ?? (subcommand === 'run' ? 'auto' : 'profile'));
if (values.profile !== undefined && requested !== 'profile' && requested !== 'auto')
throw new Error('--profile applies only to --cf-auth profile.');
// auto: an explicit --profile wins; otherwise env when the token variable is set (presence only).
const authMode = requested !== 'auto' ? requested : values.profile !== undefined ? 'profile' : hasEnvToken(ctx.env) ? 'env' : 'profile';
const profile = validateProfileName(values.profile ?? DEFAULT_PROFILE);
const cfBin = values['cf-bin'] === undefined ? undefined : parse(pathSchema('--cf-bin'), values['cf-bin']);
if (cfBin !== undefined && !isAbsolute(cfBin))
throw new Error('--cf-bin must be an absolute path.');
const out = values.out === undefined ? undefined : parse(pathSchema('--out'), values.out);
if (subcommand === 'collect' && values.isDryRun) {
if (out !== undefined || values['account-id'] !== undefined || values.isOverwrite) {
throw new Error('collect --dry-run makes no Cloudflare calls and writes no file; omit --out, --overwrite and --account-id.');
}
}
else if (subcommand === 'collect' && out === undefined) {
throw new Error('--out <file> is required for cloudflare-checkup collect.');
}
if (values.isOverwrite && out === undefined)
throw new Error('--overwrite applies only with --out.');
// The address is never echoed, even when it is malformed.
const email = values.email === undefined ? undefined : parseEmailFlag(values.email);
const options = {
subcommand,
...(takesFile ? { file: parse(pathSchema('<file>'), args[0]) } : {}),
auth: { authMode, profile },
authAuto: requested === 'auto',
...(cfBin !== undefined ? { cfBin } : {}),
...(values['account-id'] !== undefined ? { accountId: parse(accountIdSchema, values['account-id']) } : {}),
...(out !== undefined ? { out: resolve(out) } : {}),
maxRequests: parseRequestBudget(values['max-requests-per-5min']),
// Environment variables never choose the submit origin.
...(subcommand === 'submit' || subcommand === 'run' ? { origin: resolveSubmitOrigin(values['base-url']) } : {}),
...(email !== undefined ? { email } : {}),
configDir: localConfigDir(ctx, subcommand === 'run' || subcommand === 'logout'),
};
if (subcommand === 'submit' || subcommand === 'run')
assertConfirmationAvailable(ctx, subcommand);
return options;
}
/**
* The config directory (VIGILBASE_CONFIG_DIR, XDG_CONFIG_HOME/vigilbase or
* ~/.config/vigilbase). `run` and `logout` need it and fail on a malformed
* value; the advanced commands only look for a managed cf there, so for them
* a malformed value just means "no managed cf". `inspect` never reads it.
*/
function localConfigDir(ctx, required) {
try {
return resolveConfigDir(undefined, ctx.env);
}
catch (error) {
if (required)
throw error;
return null;
}
}
function isInteractive(ctx) {
return hasInteractiveInput(ctx.stdin ?? process.stdin);
}
/**
* Without a terminal, `submit` needs `--yes` or `--confirm` for the upload
* question, and the one command needs `--yes`: `--confirm` cannot answer its
* question about installing cf. Checked before anything is sent, so no code is
* emailed for a run that could not install cf or confirm the upload. The email
* and the emailed code themselves may come from piped stdin. (A missing cf
* login, or several accounts without `--account-id`, still show only after the
* email is verified: finding them needs cf.)
*/
function assertConfirmationAvailable(ctx, subcommand) {
if (isInteractive(ctx) || ctx.values.isYes)
return;
if (subcommand === 'run') {
throw new LocalCheckupError('CONFIRMATION_REQUIRED', ctx.values.isConfirm
? 'No terminal is available, and --confirm answers only the upload question, not installing cf. Add --yes.'
: 'No terminal is available to confirm installing cf or the upload. Add --yes.');
}
if (!ctx.values.isConfirm)
throw new LocalCheckupError('CONFIRMATION_REQUIRED', 'No terminal is available to confirm the upload.');
}
/** `vigilbase local cloudflare-checkup [<command>] …` */
export const handleLocal = async (ctx) => {
const [group, subcommandRaw, ...args] = ctx.rest;
if (group !== 'cloudflare-checkup')
throw new Error(usage);
const subcommand = (subcommandRaw ?? 'run').toLowerCase();
if (!SUBCOMMANDS.includes(subcommand))
throw new Error(usage);
assertLocalFlags(ctx, subcommand);
const options = parseLocalOptions(ctx, subcommand, args);
switch (subcommand) {
case 'run': return runFull(ctx, options);
case 'login': return runLogin(ctx, options);
case 'doctor': return runDoctor(ctx, options);
case 'accounts': return runAccounts(ctx, options);
case 'collect': return ctx.values.isDryRun ? runCollectDryRun(ctx, options) : runCollect(ctx, options);
case 'inspect': return runInspect(ctx, options);
case 'submit': return runSubmit(ctx, options);
case 'logout': return runLogout(ctx, options);
}
};
/** `vigilbase cloudflare-checkup …`: the top-level alias. */
export const handleCloudflareCheckup = (ctx) => handleLocal({ ...ctx, rest: ['cloudflare-checkup', ...ctx.rest] });
function stderrStream(ctx) {
return ctx.stderrStream ?? process.stderr;
}
/**
* run and submit on an interactive terminal (stdin and stderr are TTYs, TERM
* is not dumb, CI is not set, no --json): the steps render as a timeline on
* stderr (terminal-ui.ts), every other line goes through it too, and an error
* that ends the command is shown there (■) instead of by runCli. Anywhere
* else the plain lines and prompts are unchanged.
*/
async function withTerminalUi(ctx, work) {
const tui = CheckupTui.open({
mode: ctx.mode,
stdin: ctx.stdin ?? process.stdin,
stderr: stderrStream(ctx),
stdout: ctx.stdoutStream ?? process.stdout,
env: ctx.env,
writer: ctx.writer,
});
if (!tui)
return work(ctx);
try {
return await work({ ...ctx, writer: tui.lines, tui });
}
catch (error) {
tui.fail(error);
throw error;
}
finally {
tui.close();
}
}
/** `work` with a spinner (`label…`) in the terminal UI; plain output shows nothing for it. */
function busy(ctx, label, work) {
return ctx.tui ? ctx.tui.busy(label, work) : work();
}
function promptIO(ctx) {
return { stdin: ctx.stdin ?? process.stdin, stderr: stderrStream(ctx) };
}
/** Status lines of the advanced commands: stderr, text mode only. */
function info(ctx, line) {
if (ctx.mode !== 'json')
ctx.writer.stderr(line);
}
/** Status lines of the one command: stdout in text mode, stderr with --json (stdout stays one JSON object). */
function say(ctx, line) {
if (ctx.mode === 'json')
ctx.writer.stderr(line);
else
ctx.writer.stdout(line);
}
function warn(ctx, warnings, warning) {
warnings.push(warning);
if (ctx.mode !== 'json')
ctx.writer.stderr(`Warning: ${warning.message}`);
}
/** Env-mode presence check and the profile-mode notice; the value is never read. */
function checkAuthEnvironment(ctx, options, warnings) {
if (options.auth.authMode === 'env') {
if (!hasEnvToken(ctx.env))
throw new LocalCheckupError('CF_AUTH_REQUIRED', missingEnvTokenMessage(), { hint: ENV_TOKEN_AUTH_HINT });
}
else if (hasEnvToken(ctx.env)) {
warn(ctx, warnings, { code: 'ENV_TOKEN_NOT_USED', message: strippedTokenNotice(options.auth.authMode) });
}
}
async function managedDirFor(options) {
if (options.cfBin !== undefined || options.configDir === null)
return null;
return findManagedCf(options.configDir);
}
async function locateAndWarn(ctx, options, warnings, managedDir) {
const managed = managedDir === undefined ? await managedDirFor(options) : managedDir;
const location = await locateCf({
...(options.cfBin !== undefined ? { cfBin: options.cfBin } : {}),
...(managed ? { managedDir: managed } : {}),
env: ctx.env,
});
if (location.insideCwd) {
warn(ctx, warnings, { code: 'CF_INSIDE_CWD', message: 'The cf package found lies inside the current directory; make sure it is the copy you installed.' });
}
return location;
}
function offlineEnv(ctx, authMode) {
return buildChildEnv(ctx.env, { purpose: 'offline', authMode, accountId: PREFLIGHT_ACCOUNT_ID });
}
async function checkVersion(ctx, runner, location, authMode, warnings) {
const result = await runner.run(versionArgv(), { env: offlineEnv(ctx, authMode) });
if (result.exitCode !== 0) {
if (/cf requires Node\.js/.test(result.stderr))
throw new LocalCheckupError('NODE_UNSUPPORTED', 'cf refused to run under this Node.js version.');
throw new LocalCheckupError('CF_WRONG_BINARY', 'cf --version failed; the package may be damaged. Reinstall cf.');
}
const prepared = checkCfVersion(result.stdout, location.packageVersion);
if (!prepared.tested) {
warn(ctx, warnings, {
code: 'CF_UNTESTED_VERSION',
message: `cf ${prepared.version} has not been tested with this CLI (tested: ${CF_SUPPORTED.tested.join(', ')}); it is used only if every command template maps offline.`,
});
}
return prepared;
}
/** Run every template with `--dry-run`, eight at a time; no auth, no network. */
async function runPreflight(ctx, runner, auth, only) {
const checks = preflightChecks(auth, only);
const env = offlineEnv(ctx, auth.authMode);
const mismatched = [];
let verified = 0;
let next = 0;
const worker = async () => {
while (next < checks.length) {
const check = checks[next++];
const result = await runner.run(check.argv, { env });
if (result.exitCode === 0 && !result.timedOut && dryRunMatches(result.stdout, check))
verified++;
else
mismatched.push(check.settingId ? `zone_setting:${check.settingId}` : check.id);
}
};
await Promise.all(Array.from({ length: Math.min(8, checks.length) }, () => worker()));
mismatched.sort();
return { verified, total: checks.length, mismatched };
}
function assertCriticalTemplates(preflight, critical = CRITICAL_TEMPLATES) {
const broken = critical.filter((id) => preflight.mismatched.includes(id));
if (broken.length > 0) {
throw new LocalCheckupError('CF_ALLOWLIST_MISMATCH', `This cf release does not map the ${broken.join(', ')} command${broken.length === 1 ? '' : 's'} the checkup needs.`);
}
}
function createScheduler(ctx, runner, auth, maxRequests) {
return new CfScheduler({
invoke: (argv, callOptions) => runner.run(argv, {
env: buildChildEnv(ctx.env, { purpose: 'api', authMode: auth.authMode, ...(callOptions.accountId ? { accountId: callOptions.accountId } : {}) }),
...(callOptions.stdoutCapBytes ? { stdoutCapBytes: callOptions.stdoutCapBytes } : {}),
}),
whoami: () => runner.run(whoamiArgv(auth), { env: buildChildEnv(ctx.env, { purpose: 'api', authMode: auth.authMode }) }),
budget: new RequestBudget(maxRequests, systemClock),
clock: systemClock,
authMode: auth.authMode,
terminateAll: () => runner.terminateAll(),
});
}
function profileLine(auth, whoami) {
const label = auth.authMode === 'profile' ? `cf profile "${auth.profile}"` : auth.authMode === 'env' ? 'cf with the API token from this shell' : 'cf default profile';
if (auth.authMode === 'env' || !whoami.scopes)
return label;
const writes = whoami.scopes.filter((scope) => !isReadOnlyScope(scope)).length;
const access = writes === 0 ? 'read-only' : `${writes} write scope${writes === 1 ? '' : 's'}`;
return `${label} · ${access}${whoami.expiresAt ? ` · expires ${formatClock(whoami.expiresAt)}` : ''}`;
}
/** The terminal UI's sign-in line: `read-only profile "vigilbase-checkup"`. */
function signInSummary(auth, whoami, tui) {
if (auth.authMode === 'env')
return 'API token from this shell';
const writes = whoami.scopes?.filter((scope) => !isReadOnlyScope(scope)).length;
const profile = auth.authMode === 'profile' ? `profile "${auth.profile}"` : 'default cf profile';
if (writes === undefined)
return profile;
return writes === 0 ? `read-only ${profile}` : tui.join([profile, `${writes} write scope${writes === 1 ? '' : 's'}`]);
}
const MAX_PICKER_ACCOUNTS = 20;
const MAX_PROMPT_ATTEMPTS = 3;
/**
* Account choice without `--account-id` or a bound account: one account is
* used as is; several get a numbered picker (name and masked ID, up to 20) on
* a terminal, or the arrow-key picker of the terminal UI; without a terminal,
* ACCOUNT_SELECTION_REQUIRED lists the full IDs.
*/
async function chooseAccount(ctx, env, log) {
const accounts = await busy(ctx, 'Listing the Cloudflare accounts cf can read', () => listAccounts(env));
if (accounts.length === 0) {
throw new LocalCheckupError('ACCOUNT_NOT_VISIBLE', 'cf can read no Cloudflare accounts with this login.');
}
// The terminal UI's ellipsis is `...` on an ASCII-only console.
const ellipsis = ctx.tui?.ellipsis;
const label = (account) => displayText(account.name, 60, ellipsis) || '(no name)';
if (accounts.length === 1) {
if (ctx.tui)
ctx.tui.account(label(accounts[0]), maskId(accounts[0].id, ellipsis));
else
log(`cf can read 1 Cloudflare account: ${label(accounts[0])} ${maskId(accounts[0].id)}`);
return accounts[0].id;
}
// The terminal UI scrolls through every account.
if (ctx.tui) {
const id = await ctx.tui.chooseAccount(accounts.map((account) => ({ id: account.id, label: label(account), detail: maskId(account.id, ellipsis) })));
if (id === null)
throw new LocalCheckupError('ACCOUNT_SELECTION_REQUIRED', 'No account was chosen.');
return id;
}
const shown = accounts.slice(0, MAX_PICKER_ACCOUNTS);
const more = accounts.length > shown.length ? accounts.length - shown.length : 0;
if (!isInteractive(ctx)) {
const listing = shown.map((account, index) => ` ${index + 1}) ${account.id} ${displayText(account.name, 80)}`);
throw new LocalCheckupError('ACCOUNT_SELECTION_REQUIRED', `cf can read ${accounts.length} accounts; choose one with --account-id:\n${listing.join('\n')}${more ? `\n … and ${more} more` : ''}`);
}
const width = Math.max(...shown.map((account) => label(account).length));
ctx.writer.stderr(`cf can read ${accounts.length} Cloudflare accounts:`);
shown.forEach((account, index) => ctx.writer.stderr(` ${String(index + 1).padStart(String(shown.length).length)}) ${label(account).padEnd(width)} ${maskId(account.id)}`));
if (more)
ctx.writer.stderr(` … and ${more} more; pass --account-id <ACCOUNT_ID> for one of those.`);
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt++) {
const answer = await promptLine(promptIO(ctx), `Account number (1-${shown.length}): `);
if (answer === null)
throw new LocalCheckupError('ACCOUNT_SELECTION_REQUIRED', 'No account was chosen, and the input ended.');
const choice = Number(answer);
if (Number.isInteger(choice) && choice >= 1 && choice <= shown.length)
return shown[choice - 1].id;
}
throw new LocalCheckupError('ACCOUNT_SELECTION_REQUIRED', 'No account was chosen.');
}
/**
* A path as one shell word, for hints the user may copy. POSIX shells get
* single quotes (nothing inside is expanded), with a leading `~/` left outside
* them so the shell still expands it.
*/
export function shellArg(value, platform = process.platform) {
// Windows paths cannot contain `"`, and cmd and PowerShell keep backslashes inside double quotes.
if (platform === 'win32')
return /^[A-Za-z0-9_@+=:,.\\~-]+$/.test(value) ? value : `"${value}"`;
if (/^[A-Za-z0-9_@%+=:,./~-]+$/.test(value))
return value;
const quote = (text) => `'${text.replace(/'/g, "'\\''")}'`;
return value.startsWith('~/') ? `~/${quote(value.slice(2))}` : quote(value);
}
/**
* `~/…` for paths under the home directory. Not on Windows: cmd and Windows
* PowerShell do not expand `~` for native commands, and the printed path is
* what users pass to `submit`.
*/
export function displayPath(path, env = process.env, platform = process.platform) {
if (platform === 'win32')
return path;
const home = env.HOME || homedir();
return home && path.startsWith(home + sep) ? `~${path.slice(home.length)}` : path;
}
/**
* `cloudflare-checkup-<first 8 of the account ID>-<UTC yyyymmddThhmmssZ>.json`;
* `-2`, `-3`, … only when a run in the same second already used the name.
*/
export function defaultSnapshotName(accountId, at, attempt = 1) {
const stamp = at.toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/[-:]/g, '');
return `cloudflare-checkup-${accountId.slice(0, 8)}-${stamp}${attempt > 1 ? `-${attempt}` : ''}.json`;
}
/** Write to `--out`, or to a fresh default name in the checkups directory (never replacing a file there). */
async function writeToTarget(target, accountId, startedAt, text, overwrite) {
if (target.kind === 'out')
return { path: target.path, ...(await writeSnapshotFile(target.path, text, { overwrite })) };
for (let attempt = 1;; attempt++) {
const path = join(target.dir, defaultSnapshotName(accountId, startedAt, attempt));
try {
return { path, ...(await writeSnapshotFile(path, text, { overwrite: false })) };
}
catch (error) {
if (!(error instanceof LocalCheckupError) || error.code !== 'OUTPUT_EXISTS' || attempt >= 20)
throw error;
}
}
}
// ============================================================================
// login
// ============================================================================
async function runLogin(ctx, options) {
const argv = loginArgv(options.auth.profile, RECOMMENDED_CF_SCOPES, { noBrowser: Boolean(ctx.values.isNoOpen) });
if (ctx.values.isDryRun) {
if (ctx.mode === 'json')
outputJson(ctx, { success: true, dryRun: true, argv: ['cf', ...argv] });
else
ctx.writer.stdout(`cf ${argv.join(' ')}`);
return ok();
}
assertLocalRuntimeSupported();
const warnings = [];
if (hasEnvToken(ctx.env))
warn(ctx, warnings, { code: 'ENV_TOKEN_NOT_USED', message: strippedTokenNotice('profile') });
const location = await locateAndWarn(ctx, options, warnings);
await withCfRunner(location.entry, async (runner) => {
await checkVersion(ctx, runner, location, 'profile', warnings);
info(ctx, `Running: cf auth create ${options.auth.profile} --scopes … (${RECOMMENDED_CF_SCOPES.length} read scopes)`);
// cf keeps the login in its own config directory.
const code = await runner.runInteractive(argv, buildChildEnv(ctx.env, { purpose: 'login', authMode: 'profile' }), { stdoutToStderr: ctx.mode === 'json' });
if (code !== 0)
throw new LocalCheckupError('CF_AUTH_REQUIRED', 'cf auth create did not complete; the profile was not created or refreshed.');
});
if (ctx.mode === 'json') {
outputJson(ctx, { success: true, profile: options.auth.profile, scopes: [...RECOMMENDED_CF_SCOPES], warnings });
}
else {
ctx.writer.stdout(`✓ cf profile "${options.auth.profile}" is ready with read-only scopes.`);
ctx.writer.stdout(`Next: run ${NPX_COMMAND} cloudflare-checkup`);
}
return ok();
}
// ============================================================================
// logout
// ============================================================================
/**
* `cf auth delete <profile>` through the located cf (interactive passthrough,
* so cf's own message is visible), then the managed cf install is removed.
* A profile cf declines to delete (usually: it does not exist) is reported,
* not an error, so logout can be repeated. A cf that cannot run at all is an
* error (exit 1, `success: false`): the login may still be on disk, so the
* managed cf is kept, since it is usually the only cf that can delete it.
*/
async function runLogout(ctx, options) {
const configDir = options.configDir;
const profile = options.auth.profile;
const warnings = [];
let location = null;
try {
location = await locateAndWarn(ctx, options, warnings);
}
catch (error) {
// An explicit --cf-bin that is not cf is an error; with no cf at all, the profile cannot be checked.
if (options.cfBin !== undefined || !(error instanceof LocalCheckupError) || !['CF_NOT_FOUND', 'CF_WRONG_BINARY'].includes(error.code))
throw error;
}
let profileRemoved = false;
let cfProblem = null;
if (location) {
const found = location;
try {
assertLocalRuntimeSupported();
await withCfRunner(found.entry, async (runner) => {
await checkVersion(ctx, runner, found, 'profile', warnings);
say(ctx, `Running: cf auth delete ${profile}`);
const code = await runner.runInteractive(logoutArgv(profile), buildChildEnv(ctx.env, { purpose: 'login', authMode: 'profile' }), { stdoutToStderr: ctx.mode === 'json' });
profileRemoved = code === 0;
});
}
catch (error) {
// A cf that cannot run must not stop a broken managed install from being removed (unless --cf-bin named it).
if (options.cfBin !== undefined || !(error instanceof LocalCheckupError))
throw error;
cfProblem = error;
}
}
// The login could not be deleted: keep the cf Vigilbase installed (if any) for the next attempt.
const keepManaged = cfProblem !== null && await pathExists(managedCfRoot(configDir));
const managed = cfProblem ? { path: managedCfRoot(configDir), removed: false } : await removeManagedCf(configDir);
const hint = cfProblem ? logoutHint(cfProblem, profile, location?.source === 'managed') : undefined;
if (ctx.mode === 'json') {
outputJson(ctx, {
success: cfProblem === null,
profile: { name: profile, removed: profileRemoved, ...(cfProblem ? { problem: cfProblem.code } : {}) },
managedCf: { path: managed.path, removed: managed.removed, ...(keepManaged ? { kept: true } : {}) },
warnings,
...(hint ? { hint } : {}),
});
return cfProblem ? EXIT.ERROR : ok();
}
if (profileRemoved)
ctx.writer.stdout(`✓ Removed the read-only Cloudflare login (cf profile "${profile}").`);
else if (location && !cfProblem)
ctx.writer.stdout(`cf did not delete the profile "${profile}" (see cf's message above; usually it does not exist).`);
else if (!location)
ctx.writer.stdout(`cf is not installed, so the cf profile "${profile}" could not be checked. If you signed in with it, install cf (npm install --global cf@${MANAGED_CF.version}) and run cf auth delete ${profile}.`);
if (keepManaged)
ctx.writer.stdout(`Kept the cf that Vigilbase installed (${displayPath(managed.path, ctx.env)}) to remove the login later.`);
else
ctx.writer.stdout(managed.removed ? `✓ Removed the cf that Vigilbase installed (${displayPath(managed.path, ctx.env)}).` : 'No Vigilbase-installed cf to remove.');
if (cfProblem) {
ctx.writer.stderr(`Error: cf could not run (${cfProblem.code}: ${cfProblem.message}), so the read-only Cloudflare login (cf profile "${profile}") was not removed.`);
ctx.writer.stderr(`Hint: ${hint}`);
return EXIT.ERROR;
}
return ok();
}
/** What to do when logout could not run cf to delete the profile. */
function logoutHint(problem, profile, managedFailed) {
const logout = `${NPX_COMMAND} cloudflare-checkup logout`;
if (problem.code === 'NODE_UNSUPPORTED')
return `Run ${logout} again under Node.js 22.18 or newer (https://nodejs.org).`;
const install = `Install cf with \`npm install --global cf@${MANAGED_CF.version}\``;
// A damaged managed cf would be found first again; --cf-bin names the new one, and a successful logout then removes the old.
if (managedFailed)
return `${install}, then run ${logout} --cf-bin "$(npm root -g)/cf".`;
return `${install}, then run ${logout} again (or \`cf auth delete ${profile}\`).`;
}
async function pathExists(target) {
return (await stat(target).catch(() => null)) !== null;
}
// ============================================================================
// doctor
// ============================================================================
/** doctor makes one whoami call (two when that call refreshed an expired token). */
const DOCTOR_BUDGET = 100;
async function runDoctor(ctx, options) {
const warnings = [];
const problems = [];
const record = (error) => {
if (error instanceof LocalCheckupError)
problems.push({ code: error.code, message: error.message, hint: error.hint });
else
problems.push({ code: 'CF_ERROR', message: error instanceof Error ? error.message : 'Unexpected error.' });
};
const nodeSupported = isNodeSupported();
const cf = { found: false, path: null, version: null, supported: false, tested: false, identityVerified: false };
const commands = { verified: 0, mismatched: [] };
let auth = {
source: authSourceFor(options.auth.authMode),
profile: options.auth.authMode === 'profile' ? options.auth.profile : null,
authenticated: false,
scopes: 'unknown',
};
let runtimeOk = true;
try {
assertLocalRuntimeSupported();
}
catch (error) {
runtimeOk = false;
record(error);
}
let location = null;
try {
location = await locateAndWarn(ctx, options, warnings);
Object.assign(cf, { found: true, path: location.packageDir, identityVerified: true });
}
catch (error) {
record(error);
}
if (location && runtimeOk) {
const found = location;
await withCfRunner(found.entry, async (runner) => {
try {
const prepared = await checkVersion(ctx, runner, found, options.auth.authMode, warnings);
Object.assign(cf, { version: prepared.version, supported: true, tested: prepared.tested });
}
catch (error) {
record(error);
return;
}
const preflight = await runPreflight(ctx, runner, options.auth);
commands.verified = preflight.verified;
commands.mismatched = preflight.mismatched;
if (preflight.mismatched.length > 0) {
problems.push({ code: 'CF_ALLOWLIST_MISMATCH', message: `${preflight.mismatched.length} of ${preflight.total} command templates do not map in this cf release.` });
}
try {
checkAuthEnvironment(ctx, options, warnings);
auth = doctorAuth(await createScheduler(ctx, runner, options.auth, DOCTOR_BUDGET).start(), options.auth.authMode, options.auth.profile);
}
catch (error) {
record(error);
}
});
}
if (auth.authenticated && options.auth.authMode === 'default' && (auth.writeScopeCount ?? 0) > 0) {
warn(ctx, warnings, { code: 'DEFAULT_PROFILE_WRITE_SCOPES', message: `The default cf profile has ${auth.writeScopeCount} write scopes; prefer the read-only profile the checkup creates (--cf-auth profile).` });
}
const success = nodeSupported && cf.supported && commands.mismatched.length === 0 && auth.authenticated && problems.length === 0;
if (ctx.mode === 'json') {
outputJson(ctx, { success, node: { version: process.versions.node, supported: nodeSupported }, cf, auth, commands, warnings, problems });
return success ? EXIT.OK : EXIT.ERROR;
}
const mark = (isOk) => (isOk ? '✓' : '✗');
ctx.writer.stdout(`${mark(nodeSupported)} Node.js ${process.versions.node}${nodeSupported ? '' : ' (22.18 or newer required)'}`);
ctx.writer.stdout(`${mark(cf.supported)} cf ${cf.version ?? (cf.found ? '(version check failed)' : 'not found')}${cf.path ? ` ${cf.path}` : ''}${cf.supported && !cf.tested ? ' (untested)' : ''}`);
ctx.writer.stdout(`${mark(commands.mismatched.length === 0 && commands.verified > 0)} ${commands.verified} command templates mapped offline${commands.mismatched.length ? `; not mapped: ${commands.mismatched.join(', ')}` : ''}`);
const authLabel = options.auth.authMode === 'profile' ? `profile "${options.auth.profile}"` : options.auth.authMode === 'env' ? 'API token from this shell' : 'default profile';
ctx.writer.stdout(`${mark(auth.authenticated)} cf auth (${authLabel}): ${auth.authenticated ? 'signed in' : 'not signed in'}${auth.expiresAt ? ` · expires ${formatClock(auth.expiresAt)}` : ''}`);
if (auth.authenticated && auth.scopes === 'unknown')
ctx.writer.stdout(' scopes: unknown (API token permissions cannot be introspected)');
if (auth.missingRecommendedScopes?.length)
ctx.writer.stdout(` missing recommended scopes: ${auth.missingRecommendedScopes.join(' ')}`);
if (auth.writeScopeCount)
ctx.writer.stdout(` write scopes: ${auth.writeScopeCount}`);
for (const problem of problems) {
ctx.writer.stderr(`${problem.code}: ${problem.message}`);
if (problem.hint)
ctx.writer.stderr(` Hint: ${problem.hint}`);
}
return success ? EXIT.OK : EXIT.ERROR;
}
// ============================================================================
// accounts
// ============================================================================
async function runAccounts(ctx, options) {
assertLocalRuntimeSupported();
const warnings = [];
checkAuthEnvironment(ctx, options, warnings);
const location = await locateAndWarn(ctx, options, warnings);
const accounts = await withCfRunner(location.entry, async (runner) => {
await checkVersion(ctx, runner, location, options.auth.authMode, warnings);
assertCriticalTemplates(await runPreflight(ctx, runner, options.auth, ['account_selection']), ['account_selection']);
const scheduler = createScheduler(ctx, runner, options.auth, options.maxRequests);
await scheduler.start();
return listAccounts({ scheduler, auth: options.auth, unavailable: new Set() });
});
if (ctx.mode === 'json') {
outputJson(ctx, { success: true, accounts });
}
else if (accounts.length === 0) {
ctx.writer.stdout('cf can read no Cloudflare accounts with this login.');
}
else {
for (const account of accounts)
ctx.writer.stdout(`${account.id} ${displayText(account.name, 120)}`);
}
return ok();
}
// ============================================================================
// collect --dry-run
// ============================================================================
const REQUEST_FORMULA = 'about 19 + extra pages + zones × (33 + rulesets per zone) cf requests';
async function runCollectDryRun(ctx, options) {
assertLocalRuntimeSupported();
const warnings = [];
const location = await locateAndWarn(ctx, options, warnings);
const { prepared, preflight } = await withCfRunner(location.entry, async (runner) => ({
prepared: await checkVersion(ctx, runner, location, options.auth.authMode, warnings),
preflight: await runPreflight(ctx, runner, options.auth),
}));
const display = (id) => ['cf', ...templateDisplay(id, options.auth)];
const plan = {
accountSelection: [display('account_selection')],
account: TEMPLATE_IDS.filter((id) => id !== 'account_selection' && TEMPLATES[id].scope === 'account').map(display),
perZone: TEMPLATE_IDS.filter((id) => TEMPLATES[id].scope === 'zone' || TEMPLATES[id].scope === 'setting').map(display),
perRuleset: [display('zone_ruleset')],
notCollected: [...NOT_COLLECTABLE_ZONE_KINDS],
};
const childEnv = childEnvVariableNames(ctx.env, options.auth.authMode);
if (ctx.mode === 'json') {
outputJson(ctx, {
success: true,
dryRun: true,
cf: { version: prepared.version, path: location.packageDir, tested: prepared.tested },
plan,
verified: { commands: preflight.verified, mismatched: preflight.mismatched },
childEnv,
requestFormula: REQUEST_FORMULA,
warnings,
});
return ok();
}
const lines = [
`Using Cloudflare cf ${prepared.version} (${location.packageDir}) · ${preflight.verified}/${preflight.total} command templates mapped offline`,
...(preflight.mismatched.length ? [`Not mapped (recorded as command_unavailable): ${preflight.mismatched.join(', ')}`] : []),
'',
'Account selection (only without --account-id):',
...plan.accountSelection.map((argv) => ` ${argv.join(' ')}`),
'Account (CLOUDFLARE_ACCOUNT_ID=<ACCOUNT_ID> for every call):',
...plan.account.map((argv) => ` ${argv.join(' ')}`),
'Per zone (<SETTING_ID> is each of the 19 fixed zone settings):',
...plan.perZone.map((argv) => ` ${argv.join(' ')}`),
'Per listed ruleset:',
...plan.perRuleset.map((argv) => ` ${argv.join(' ')}`),
`Not collectable with cf (recorded as not_collected for every zone): ${plan.notCollected.join(', ')}`,
`cf child environment (names only): ${childEnv.join(' ')}`,
`Requests: ${REQUEST_FORMULA}, at most ${options.maxRequests} per 5 minutes.`,
'No Cloudflare or Vigilbase requests were made.',
];
for (const line of lines)
ctx.writer.stdout(line);
return ok();
}
/**
* Advisory pre-flight for --out, before any cf spawn, so a typo cannot throw
* away a whole collection. writeSnapshotFile stays the race-safe final guard.
*/
async function assertOutWritable(out, overwrite) {
const existing = await stat(out).catch(() => null);
if (existing && !overwrite)
throw new LocalCheckupError('OUTPUT_EXISTS', 'The --out file already exists; nothing was collected.');
if (existing && !existing.isFile())
throw new Error('--out must name a regular file; nothing was collected.');
const dir = dirname(out);
const parent = await stat(dir).catch(() => null);
if (!parent || !parent.isDirectory())
throw new Error('The --out directory does not exist; nothing was collected.');
try {
await access(dir, constants.W_OK);
}
catch {
throw new Error('The --out directory is not writable; nothing was collected.');
}
}
/** Before any cf spawn: check --out, or create the default checkups directory (0700, never through a symlink). */
async function prepareOutput(ctx, options, overwrite) {
if (options.out !== undefined) {
await assertOutWritable(options.out, overwrite);
return { kind: 'out', path: options.out };
}
const dir = join(options.configDir, 'checkups');
try {
await ensurePrivateDir(dir);
}
catch {
throw new Error(`Could not create ${displayPath(dir, ctx.env)} for the snapshot; pass --out <file> instead.`);
}
return { kind: 'default', dir };
}
/**
* Why a located cf cannot be used, found before any authenticated call: none
* at all, a different program, an unsupported version, or (after `--version`
* and the offline `--dry-run` mapping) a release that renamed commands the
* checkup needs, for example a newer global cf.
*/
const INSTALLABLE_CODES = new Set(['CF_NOT_FOUND', 'CF_WRONG_BINARY', 'CF_UNSUPPORTED_VERSION', 'CF_ALLOWLIST_MISMATCH']);
/** Thrown inside the runner when the located cf is unusable before anything but `--version` and `--dry-run` ran. */
class UnusableCf extends Error {
cause;
constructor(cause) {
super(cause.message);
this.cause = cause;
}
}
/** `cf --version` and the offline mapping of every template; no auth, no network. */
async function prepareCf(ctx, runner, location, auth, warnings) {
const prepared = await checkVersion(ctx, runner, location, auth.authMode, warnings);
const preflight = await runPreflight(ctx, runner, auth);
assertCriticalTemplates(preflight);
// An untested cf release is used only when every template maps (spec §A3).
if (!prepared.tested && preflight.mismatched.length > 0) {
throw new LocalCheckupError('CF_ALLOWLIST_MISMATCH', `cf ${prepared.version} is untested and ${preflight.mismatched.length} command template${preflight.mismatched.length === 1 ? ' does' : 's do'} not map: ${preflight.mismatched.join(', ')}.`);
}
return { ...prepared, preflight };
}
/**
* Locate cf (`--cf-bin`, the managed install, PATH, pnpm, Volta), check its
* version and offline mapping, and run `work`. In the one command, an
* unusable or missing cf (without `--cf-bin`) leads to the managed-install
* offer, then `work` runs with the managed cf; the user's own cf is left alone.
*/
async function withUsableCf(ctx, options, warnings, work) {
const canInstall = options.subcommand === 'run' && options.cfBin === undefined && options.configDir !== null;
const attempt = (location) => withCfRunner(location.entry, async (runner) => {
const warned = warnings.length;
let ready;
try {
ready = await busy(ctx, 'Checking Cloudflare\'s cf CLI', () => prepareCf(ctx, runner, location, options.auth, warnings));
}
catch (error) {
if (canInstall && error instanceof LocalCheckupError && INSTALLABLE_CODES.has(error.code)) {
// Warnings about a cf that is replaced (CF_UNTESTED_VERSION) do not describe the run.
warnings.splice(warned);
throw new UnusableCf(error);
}
throw error;
}
return work(runner, location, ready);
});
let unusable = null;
let replace = false;
let location = null;
try {
location = await busy(ctx, 'Looking for Cloudflare\'s cf CLI', () => locateAndWarn(ctx, options, warnings));
}
catch (error) {
if (!canInstall || !(error instanceof LocalCheckupError) || !INSTALLABLE_CODES.has(error.code))
throw error;
unusable = error;
}
if (location) {
try {
return { result: await attempt(location), managed: location.source === 'managed' };
}
catch (error) {
if (!(error instanceof UnusableCf))
throw error;
unusable = error.cause;
// A managed install whose cf cannot run is reinstalled, not reused.
replace = location.source === 'managed';
}
}
const installed = await offerManagedInstall(ctx, options, unusable, replace);
const managed = await locateAndWarn(ctx, options, warnings, installed);
try {
return { result: await attempt(managed), managed: managed.source === 'managed' };
}
catch (error) {
throw error instanceof UnusableCf ? error.cause : error;
}
}
/** The `[Y/n]` offer to install the pinned cf into the config directory; returns the package directory. */
async function offerManagedInstall(ctx, options, reason, replace) {
const { tui } = ctx;
const what = `Cloudflare's cf CLI (cf@${MANAGED_CF.version}, ${MANAGED_CF.approxSize})`;
if (reason.code !== 'CF_NOT_FOUND') {
if (tui)
tui.warning(`The cf found on this machine cannot be used: ${reason.message}`);
else
say(ctx, `The cf found on this machine cannot be used: ${reason.message}`);
}
const question = reason.code === 'CF_NOT_FOUND'
? `${what} is not installed. Install it for Vigilbase now?`
: `Install ${what} for Vigilbase now?`;
// The terminal UI's question fits one line; the size goes under it.
const short = reason.code === 'CF_NOT_FOUND' ? 'cf is not installed. Install it for Vigilbase?' : 'Install a cf for Vigilbase?';
const detail = `Cloudflare's cf CLI ${MANAGED_CF.version}, ${MANAGED_CF.approxSize}; your own cf is left alone`;
if (ctx.values.isYes) {
if (tui)
tui.answeredByFlag(short, '--yes');
else
ctx.writer.stderr(`${question} [Y/n] y (--yes)`);
}
else if (!isInteractive(ctx)) {
throw new LocalCheckupError('CF_NOT_FOUND', `${reason.code === 'CF_NOT_FOUND' ? 'Cloudflare\'s cf CLI was not found' : 'No usable cf was found'}, and no terminal is available to confirm installing it.`);
}
else if (!(await (tui ? tui.confirm(short, { initial: true, detail }) : confirmDefaultYes(promptIO(ctx), question)))) {
throw new LocalCheckupError('CF_NOT_FOUND', 'cf was not installed, so the checkup cannot collect.');
}
const label = `Installing cf ${MANAGED_CF.version} (one time)`;
const progress = tui ? tui.spinner(label) : startCountsProgress(shouldShowProgress(ctx.mode), stderrStream(ctx));
if (!tui) {
if (shouldShowProgress(ctx.mode))
progress.update(`${label}…`);
else
say(ctx, `${label}…`);
}
try {
const packageDir = await installManagedCf({ configDir: options.configDir, env: ctx.env, replace, onInterrupt: () => progress.stop() });
progress.stop();
if (tui)
tui.step(`Installed cf ${MANAGED_CF.version}`);
else
say(ctx, `✓ Installed cf ${MANAGED_CF.version}`);
return packageDir;
}
catch (error) {
progress.stop();
if (error instanceof CfInstallError && error.npmOutput.length > 0) {
ctx.writer.stderr(`npm output (last ${error.npmOutput.length} lines):`);
for (const line of error.npmOutput)
ctx.writer.stderr(` ${line}`);
}
throw error;
}
}
/**
* whoami before any fan-out. In the one command with a cf profile, a missing
* or expired login on a terminal starts the read-only `cf auth create`
* sign-in, then whoami is retried once (the terminal UI offers the sign-in
* up to twice more when it fails).
*/
async function startAuth(ctx, runner, options, scheduler) {
const whoami = () => busy(ctx, 'Checking the Cloudflare sign-in', () => scheduler.start());
try {
return await whoami();
}
catch (error) {
const canSignIn = options.subcommand === 'run' && options.auth.authMode === 'profile' &&
error instanceof LocalCheckupError && error.code === 'CF_AUTH_REQUIRED' && isInteractive(ctx);
if (!canSignIn)
throw error;
}
const argv = loginArgv(options.auth.profile, RECOMMENDED_CF_SCOPES, { noBrowser: false });
const loginEnv = buildChildEnv(ctx.env, { purpose: 'login', authMode: 'profile' });
// cf prints and reads on the terminal itself; the terminal UI hands it over and offers retries.
if (ctx.tui)
return ctx.tui.signIn(() => runner.runInteractive(argv, loginEnv), whoami);
say(ctx, 'Signing in to Cloudflare with a read-only login. Your browser will open…');
const code = await runner.runInteractive(argv, loginEnv, { stdoutToStderr: ctx.mode === 'json' });
if (code !== 0)
throw new LocalCheckupError('CF_AUTH_REQUIRED', 'The Cloudflare sign-in (cf auth create) did not complete.');
return scheduler.start();
}
async function collectSnapshot(ctx, options, session) {
const full = options.subcommand === 'run';
const log = (line) => (full ? say(ctx, line) : info(ctx, line));
const overwrite = Boolean(ctx.values.isOverwrite);
const target = await prepareOutput(ctx, options, overwrite);
const warnings = [];
checkAuthEnvironment(ctx, options, warnings);
const started = Date.now();
const { result, managed } = await withUsableCf(ctx, options, warnings, async (runner, location, prepared) => {
const { preflight } = prepared;
if (!full)
info(ctx, `Using Cloudflare cf ${prepared.version} (${location.packageDir}) · ${preflight.verified}/${preflight.total} command templates mapped offline`);
if (preflight.mismatched.length) {
warn(ctx, warnings, { code: 'CF_COMMANDS_UNAVAILABLE', message: `Not mapped in this cf release (recorded as command_unavailable): ${preflight.mismatched.join(', ')}` });
}
const scheduler = createScheduler(ctx, runner, options.auth, options.maxRequests);
const whoami = await startAuth(ctx, runner, options, scheduler);
if (ctx.tui)
ctx.tui.signedIn(signInSummary(options.auth, whoami, ctx.tui));
else
log(full ? `✓ Signed in · ${profileLine(options.auth, whoami)}` : profileLine(options.auth, whoami));
if (options.auth.authMode === 'default' && whoami.scopes?.some((scope) => !isReadOnlyScope(scope))) {
warn(ctx, warnings, { code: 'DEFAULT_PROFILE_WRITE_SCOPES', message: 'The default cf profile has write scopes; the checkup still runs only read commands. Prefer the read-only profile (--cf-auth profile).' });
}
let progress = startCountsProgress(false);
// The terminal UI's progress bar, once the zones are known.
let live = null;
const env = {
scheduler,
auth: options.auth,
unavailable: new Set(preflight.mismatched),
onProgress: (state) => (live ? live.update(state) : progress.update(`zones ${state.zonesDone}/${state.zonesTotal} · ${state.requests} requests`)),
};
const bound = options.accountId ?? session?.accountId ?? null;
const accountId = bound ?? await chooseAccount(ctx, env, log);
if (ctx.tui && bound !== null)
ctx.tui.account(maskId(accountId, ctx.tui.ellipsis), options.accountId !== undefined ? 'from --account-id' : 'bound to this checkup');
try {
const startedAt = new Date();
const entries = await collectAccount(env, accountId, {
onZones: (zones) => {
const requests = estimateRequests(zones.length);
if (ctx.tui) {
live = ctx.tui.collecting({ zones: zones.length, plannedReads: requests, maxRequests: options.maxRequests });
return;
}
const seconds = estimateBudgetSeconds(options.maxRequests, requests);
log(`Account ${maskId(accountId)} · ${zones.length} zone${zones.length === 1 ? '' : 's'} · ~${requests} cf requests at ≤${options.maxRequests} per 5 min (${formatDuration(seconds)})`);
// The spinner starts once zones are known (after any account prompt and the estimate line).
progress = startCountsProgress(shouldShowProgress(ctx.mode), stderrStream(ctx));
progress.update(`zones 0/${zones.length} · ${scheduler.invocations} requests`);
},
});
progress.stop();
live?.stop();
const collector = {
name: SNAPSHOT_COLLECTOR_NAME,
version: CLI_VERSION,
cf: { version: prepared.version },
authSource: authSourceFor(options.auth.authMode),
platform: process.platform,
startedAt: startedAt.toISOString(),
finishedAt: new Date().toISOString(),
cfInvocations: scheduler.invocations,
};
const { snapshot, text } = buildSnapshot(accountId, collector, entries);
if (Buffer.byteLength(text) > SNAPSHOT_LIMITS.defaultServerMaxBytes) {
warn(ctx, warnings, {
code: 'SNAPSHOT_OVER_DEFAULT_UPLOAD_LIMIT',
message: `The snapshot is larger than ${SNAPSHOT_LIMITS.defaultServerMaxBytes / 1024 / 1024} MiB, the default upload limit.`,
});
}
const written = await writeToTarget(target, accountId, startedAt, text, overwrite);
return { ...written, snapshot, version: prepared.version, cfInvocations: scheduler.invocations };
}
finally {
progress.stop();
live?.stop();
}
});
return { ...result, durationMs: Date.now() - started, warnings, usedProfile: options.auth.authMode === 'profile', managedCf: managed };
}
function snapshotPayload(outcome) {
return {
path: outcome.path,
bytes: outcome.bytes,
sha256: outcome.sha256,
schemaVersion: outcome.snapshot.schemaVersion,
accountId: outcome.snapshot.accountId,
zoneCount: zoneCount(outcome.snapshot),
entries: countEntries(outcome.snapshot.entries),
};
}
function deniedReads(snapshot) {
return snapshot.entries.filter((entry) => {
const result = entry.result;
return !result.ok && result.reason === 'http_error' && result.status === 403;
}).length;
}
async function runCollect(ctx, options) {
assertLocalRuntimeSupported();
const outcome = await collectSnapshot(ctx, options, null);
if (ctx.mode === 'json') {
outputJson(ctx, {
success: true,
snapshot: snapshotPayload(outcome),
cf: { version: outcome.version, authSource: authSourceFor(options.auth.authMode) },
cfInvocations: outcome.cfInvocations,
durationMs: outcome.durationMs,
warnings: outcome.warnings,
});
return ok();
}
const summary = readsSummary(countEntries(outcome.snapshot.entries), deniedReads(outcome.snapshot));
ctx.writer.stdout(`✓ Wrote ${outcome.path} (${formatBytes(outcome.bytes)}, sha256 ${shortSha(outcome.sha256)})${summary ? ` · ${summary}` : ''}`);
ctx.writer.stdout(`Next: inspect it, then upload it with ${NPX_COMMAND} cloudflare-checkup submit ${shellArg(outcome.path)}`);
return ok();
}
/** The one command's summary of the written file: counts and data categories, no values. */
function printCollectSummary(ctx, outcome) {
const { snapshot } = outcome;
const counts = countEntries(snapshot.entries);
const denied = deniedReads(snapshot);
const zones = zoneCount(snapshot);
const notOk = [
counts.notCollected ? `${counts.notCollected} not collectable with cf` : null,
denied ? `${denied} denied` : null,
counts.httpError - denied ? `${counts.httpError - denied} failed with another HTTP error` : null,
counts.collectionFailed ? `${counts.collectionFailed} could not be collected` : null,
].filter((part) => part !== null);
const categories = dataCategories(snapshot);
const data = [
[categories.memberEmails, 'member email', 'member emails'],
[categories.accessRuleValues, 'access-rule value', 'access-rule values'],
[categories.dnsHostnames, 'DNS name', 'DNS names'],
[categories.pseudonymizedIps, 'pseudonymised IP', 'pseudonymised IPs'],
[categories.ruleDescriptions, 'rule description', 'rule descriptions'],
];
const dataParts = data.filter(([count]) => count > 0).map(([count, one, many]) => `${count} ${count === 1 ? one : many}`);
if (ctx.tui) {
ctx.tui.collected({
zones, total: counts.total, ok: counts.ok, notCollectable: counts.notCollected, denied,
failed: counts.httpError - denied + counts.collectionFailed,
path: displayPath(outcome.path, ctx.env), bytes: outcome.bytes, contents: dataParts,
inspect: `${NPX_COMMAND} cloudflare-checkup inspect ${shellArg(displayPath(outcome.path, ctx.env))}`,
});
return;
}
say(ctx, `✓ Saved ${displayPath(outcome.path, ctx.env)} (${formatBytes(outcome.bytes)})`);
say(ctx, ` ${zones} zone${zones === 1 ? '' : 's'} · ${counts.total} reads (${[`${counts.ok} ok`, ...notOk].join(', ')})${dataParts.length ? ` · ${dataParts.join(' · ')}` : ''}`);
say(ctx, ` No credentials. Inspect it: ${NPX_COMMAND} cloudflare-checkup inspect ${shellArg(outcome.path)}`);
}
// ============================================================================
// The one command
// ============================================================================
function clientOptions(ctx, origin) {
return {
origin,
cliVersion: CLI_VERSION,
userAgent: ctx.config.userAgent,
...(ctx.values.timeout !== undefined ? { timeoutMs: ctx.config.timeoutMs } : {}),
};
}
/**
* Before the email is asked for (or `--email` is used): vigilbase.com says
* whether local checkups are on and which CLI and snapshot schema it accepts,
* so a switched-off local mode (LOCAL_MODE_DISABLED) or an old CLI
* (CLI_UPGRADE_REQUIRED) never costs an emailed code. Without a usable answer
* the run goes on, and the session check after the email decides as before.
*/
async function checkAvailability(ctx, origin, schemaVersion) {
const client = new VerificationClient(clientOptions(ctx, origin));
const availability = await busy(ctx, 'Checking vigilbase.com', () => client.availability());
if (availability)
assertServerAccepts(availability, CLI_VERSION, schemaVersion);
}
/**
* The email verification (prompts on stderr, answers from piped stdin
* without a terminal), then `✓ Verified j***@acme.com`. No address is sent
* before one is entered, and cf is not touched before it is verified.
*/
async function verifyRequester(ctx, options, origin) {
const verified = await verifyEmail({
client: new VerificationClient(clientOptions(ctx, origin)),
io: promptIO(ctx),
locale: emailLocale(ctx.env),
...(options.email !== undefined ? { email: options.email } : {}),
say: (line) => say(ctx, line),
notice: (line) => ctx.writer.stderr(line),
// The terminal UI shows `✓ verified` on the code's line.
...(ctx.tui ? { view: ctx.tui.emailView() } : {}),
});
if (!ctx.tui)
say(ctx, `✓ Verified ${verified.requester}`);
return verified;
}
/**
* The checkup the verified email started. A session that expired (a long
* collection, or a slow answer to the upload question) is renewed once with
* the token and code still in memory; vigilbase.com mints the next session
* code for the same checkup. The snapshot file is kept either way.
*/
class CheckupSession {
ctx;
origin;
verified;
constructor(ctx, origin, verified) {
this.ctx = ctx;
this.origin = origin;
this.verified = verified;
}
client() {
return new SubmitClient({ ...clientOptions(this.ctx, this.origin), code: this.verified.sessionCode });
}
async withRenewal(call) {
try {
return await call(this.client());
}
catch (error) {
if (!(error instanceof LocalCheckupError) || error.code !== 'SESSION_EXPIRED' || !this.verified.renewable)
throw error;
}
try {
await this.verified.renew();
}
catch (error) {
if (error instanceof LocalCheckupError && (error.code === 'VERIFICATION_EXPIRED' || error.code === 'VERIFICATION_INVALID')) {
throw new LocalCheckupError('SESSION_EXPIRED', 'The checkup session expired, and the code from the email can no longer renew it.', error.status !== undefined ? { status: error.status } : {});
}
throw error;
}
if (this.ctx.tui)
this.ctx.tui.step('Renewed the checkup session, which had expired');
else
say(this.ctx, '✓ Renewed the checkup session, which had expired');
return call(this.client());
}
getSession() {
return this.withRenewal((client) => client.getSession());
}
postSnapshot(bytes, sha256) {
return this.withRenewal((client) => client.postSnapshot(bytes, sha256));
}
}
/** Failures after the snapshot is saved that a later `submit <file>` can fix without collecting again. */
const UPLOAD_LATER_CODES = new Set(['SESSION_INVALID', 'SESSION_EXPIRED', 'SESSION_CLOSED', 'VERIFICATION_EXPIRED', 'VERIFICATION_INVALID',
'SUBMIT_FAILED', 'VIGILBASE_RATE_LIMITED', 'VIGILBASE_CHALLENGED', 'LOCAL_MODE_DISABLED']);
/**
* `submit` for a saved file. A loopback origin is named with `--base-url`, so a
* development run never suggests an upload to production; production is the
* CLI's default and is not named.
*/
export function submitCommand(path, origin) {
const baseUrl = isLoopbackOrigin(origin) ? ` --base-url ${shellArg(origin)}` : '';
return `${NPX_COMMAND} cloudflare-checkup submit ${shellArg(path)}${baseUrl}`;
}
/**
* Right before such an error is reported (stderr, both modes): the `submit`
* command for the saved file. A line of its own, because hints are capped at
* 300 characters and a path has no fixed length; in the terminal UI, the
* command has a line of its own too, never wrapped, so it can be copied.
*/
function noteSavedSnapshot(ctx, error, path, origin) {
if (!(error instanceof LocalCheckupError) || !UPLOAD_LATER_CODES.has(error.code))
return;
const submit = submitCommand(path, origin);
if (ctx.tui)
ctx.tui.uploadLater(submit);
else
ctx.writer.stderr(`The snapshot stays saved. To upload it later without collecting again, run ${submit} (it verifies your email again).`);
}
/** Session checks that need no upload: CLI version, single use, and the bound account. */
function assertSessionUsable(session, accountId) {
assertServerAccepts(session, CLI_VERSION, SNAPSHOT_SCHEMA_VERSION);
if (session.state === 'completed')
throw new LocalCheckupError('SESSION_USED', 'This checkup already has results, so it cannot accept a new snapshot.');
// A checkup the email verification just started is never processing; this guards any other state.
if (session.state === 'processing')
throw new LocalCheckupError('SESSION_BUSY', 'A snapshot is already being processed for this checkup.');
if (accountId !== undefined && session.accountId !== null && session.accountId !== accountId) {
throw new LocalCheckupError('ACCOUNT_MISMATCH', `This checkup is bound to Cloudflare account ${maskId(session.accountId)}, not ${maskId(accountId)}. Nothing was collected.`);
}
}
/**
* Without a terminal, a missing cf login and a choice between accounts show
* only once cf runs, after the email was verified (`assertConfirmationAvailable`
* cannot tell beforehand). Their hint then adds that running the command again
* with the same address waits for vigilbase.com's 8 minutes per address. A hint
* that is not the code's default (the API token one) is left as it is.
*/
function withSameAddressWait(error) {
if (!(error instanceof LocalCheckupError) || (error.code !== 'CF_AUTH_REQUIRED' && error.code !== 'ACCOUNT_SELECTION_REQUIRED'))
return error;
if (error.hint !== LOCAL_ERROR_HINTS[error.code])
return error;
return new LocalCheckupError(error.code, error.message, {
hint: `${error.hint} ${SAME_ADDRESS_WAIT}.`,
...(error.status !== undefined ? { status: error.status } : {}),
...(error.retryAfterSec !== undefined ? { retryAfterSec: error.retryAfterSec } : {}),
});
}
function runFull(ctx, options) {
return withTerminalUi(ctx, (local) => runOneCommand(local, options));
}
async function runOneCommand(ctx, options) {
const origin = options.origin ?? PRODUCTION_ORIGIN;
if (ctx.tui)
ctx.tui.intro();
else
say(ctx, BANNER);
assertLocalRuntimeSupported();
// A bad --out never costs an emailed code (collectSnapshot checks it again).
if (options.out !== undefined)
await assertOutWritable(options.out, Boolean(ctx.values.isOverwrite));
// 1-3. Availability, then the email, then the checkup it started are checked before cf is touched.
await checkAvailability(ctx, origin, SNAPSHOT_SCHEMA_VERSION);
const checkup = new CheckupSession(ctx, origin, await verifyRequester(ctx, options, origin));
const early = await busy(ctx, 'Checking the checkup', () => checkup.getSession());
assertSessionUsable(early, options.accountId);
// 4-9. cf, sign-in, account, collection.
let outcome;
try {
outcome = await collectSnapshot(ctx, options, early);
}
catch (error) {
throw isInteractive(ctx) ? error : withSameAddressWait(error);
}
printCollectSummary(ctx, outcome);
if (ctx.tui)
ctx.tui.cancelMessage = 'Cancelled. Nothing was uploaded; the snapshot stays saved.';
// 10. The session, re-checked (and renewed once if it expired) right before uploading.
let result;
try {
const session = await busy(ctx, 'Checking the checkup', () => checkup.getSession());
// Upload exactly the bytes on disk, re-validated.
const loaded = await loadSnapshotFile(outcome.path);
result = await uploadLoaded(ctx, { checkup, session, origin, loaded });
}
catch (error) {
noteSavedSnapshot(ctx, error, outcome.path, origin);
throw error;
}
if (ctx.mode === 'json') {
outputJson(ctx, { ...result, snapshot: snapshotPayload(outcome), warnings: outcome.warnings });
return ok();
}
sayLogoutHint(ctx, outcome.usedProfile, outcome.managedCf);
return ok();
}
/** logout removes the profile, and only the cf that Vigilbase installed (never the user's own). */
function sayLogoutHint(ctx, usedProfile, managedCf) {
const logout = `${NPX_COMMAND} cloudflare-checkup logout`;
const lead = usedProfile ? `Remove the read-only Cloudflare login${managedCf ? ' and the cf Vigilbase installed' : ''} later with:`
: managedCf ? 'Remove the cf that Vigilbase installed later with:' : null;
if (lead === null)
return;
if (ctx.tui)
ctx.tui.footnote(lead, logout);
else
say(ctx, `${lead} ${logout}`);
}
// ============================================================================
// inspect
// ============================================================================
async function runInspect(ctx, options) {
const loaded = await loadSnapshotFile(options.file);
const { snapshot } = loaded;
const name = accountName(snapshot);
const categories = dataCategories(snapshot);
const byKind = countEntriesByKind(snapshot.entries);
if (ctx.mode === 'json') {
outputJson(ctx, {
success: true,
snapshot: {
path: resolve(loaded.path),
bytes: loaded.bytes.byteLength,
sha256: loaded.sha256,
schemaVersion: snapshot.schemaVersion,
accountId: snapshot.accountId,
accountName: name,
zoneCount: zoneCount(snapshot),
collectedAt: snapshot.collector.finishedAt,
collector: snapshot.collector,
},
entries: { byKind },
dataCategories: categories,
});
return ok();
}
const counts = countEntries(snapshot.entries);
const { collector } = snapshot;
const lines = [
`Snapshot ${options.file} ${formatBytes(loaded.bytes.byteLength)} sha256 ${loaded.sha256}`,
`Schema ${snapshot.schemaVersion} (valid)`,
`Account ${snapshot.accountId}${name !== null ? ` ${name} (as reported)` : ''}`,
`Zones ${zoneCount(snapshot)}`,
`Collected ${collector.finishedAt} by ${collector.name} ${collector.version} with cf ${collector.cf.version} (${collector.authSource}, ${collector.platform}, ${collector.cfInvocations} cf calls)`,
`Reads ${counts.total} · ${counts.ok} ok · ${counts.httpError} HTTP error · ${counts.notCollected} not collectable with cf · ${counts.collectionFailed} collection failed`,
...Object.entries(byKind).map(([kind, kindCounts]) => {
const notOk = kindCounts.total - kindCounts.ok;
return ` ${kind.padEnd(34)} ${String(kindCounts.total).padStart(5)}${notOk ? ` (${notOk} not ok)` : ''}`;
}),
'Free text and identifiers in this file:',
` member emails ${categories.memberEmails} · access-rule values ${categories.accessRuleValues} · non-proxied DNS names ${categories.dnsHostnames} · pseudonymised IPs ${categories.pseudonymizedIps} · rule descriptions ${categories.ruleDescriptions}`,
'No credentials are included, and origin IPs are pseudonymised.',
];
for (const line of lines)
ctx.writer.stdout(line);
return ok();
}
// ============================================================================
// submit (and the upload step of the one command)
// ============================================================================
function runSubmit(ctx, options) {
return withTerminalUi(ctx, (local) => submitFile(local, options));
}
async function submitFile(ctx, options) {
const origin = options.origin ?? PRODUCTION_ORIGIN;
const loaded = await loadSnapshotFile(options.file);
assertFresh(loaded.snapshot);
const zones = zoneCount(loaded.snapshot);
const head = `Snapshot ${options.file} ${formatBytes(loaded.bytes.byteLength)} sha256 ${shortSha(loaded.sha256)} account ${maskId(loaded.snapshot.accountId)} ${zones} zone${zones === 1 ? '' : 's'} collected ${formatClock(loaded.snapshot.collector.finishedAt)}`;
if (ctx.tui) {
ctx.tui.intro();
const { ellipsis } = ctx.tui;
ctx.tui.step('Snapshot', ctx.tui.join([
displayText(displayPath(resolve(options.file), ctx.env), 120, ellipsis),
formatBytes(loaded.bytes.byteLength),
`account ${maskId(loaded.snapshot.accountId, ellipsis)}`,
`${zones} zone${zones === 1 ? '' : 's'}`,
`collected ${formatClock(loaded.snapshot.collector.finishedAt)}`,
]));
}
else if (ctx.mode === 'json')
ctx.writer.stderr(head);
else
ctx.writer.stdout(head);
// The file, then vigilbase.com's availability, are checked before the email is verified, so neither costs a code.
await checkAvailability(ctx, origin, loaded.snapshot.schemaVersion);
const checkup = new CheckupSession(ctx, origin, await verifyRequester(ctx, options, origin));
const session = await busy(ctx, 'Checking the checkup', () => checkup.getSession());
const result = await uploadLoaded(ctx, { checkup, session, origin, loaded });
if (ctx.mode === 'json')
outputJson(ctx, result);
return ok();
}
/** Refused locally: vigilbase.com accepts snapshots finished in the last 24 h (5 min clock skew). */
function assertFresh(snapshot, now = Date.now()) {
const finished = Date.parse(snapshot.collector.finishedAt);
if (!(finished >= now - SNAPSHOT_LIMITS.maxAgeHours * 3_600_000) || finished > now + SNAPSHOT_LIMITS.maxFutureSkewSeconds * 1_000) {
throw new LocalCheckupError('SNAPSHOT_STALE', 'The snapshot was collected more than 24 hours ago (or is dated in the future).');
}
}
function numberAt(value, ...path) {
let current = value;
for (const key of path) {
if (!current || typeof current !== 'object')
return undefined;
current = current[key];
}
return typeof current === 'number' && Number.isFinite(current) ? current : undefined;
}
/**
* Opens the results page in the browser, unless `--no-open`, `--json` or no
* browser is available, and only when it is on the upload origin; true when
* a browser was started. The link is printed either way.
*/
function openResults(ctx, url, origin) {
if (ctx.mode === 'json' || ctx.values.isNoOpen || isHeadlessEnvironment(ctx.env))
return false;
if (new URL(url).origin !== origin)
return false;
return openUrlInBrowser(url, ctx.env).isOpened;
}
/**
* Checks against the session (CLI version, bound account, limits), the
* confirmation naming the masked verified address, then `POST` of exactly the
* loaded bytes, and the results link.
*/
async function uploadLoaded(ctx, input) {
const { checkup, session, origin, loaded } = input;
const { snapshot } = loaded;
assertFresh(snapshot);
assertServerAccepts(session, CLI_VERSION, snapshot.schemaVersion);
if (session.accountId !== null && session.accountId !== snapshot.accountId) {
throw new LocalCheckupError('ACCOUNT_MISMATCH', `This checkup is bound to Cloudflare account ${maskId(session.accountId)}, but the snapshot is for ${maskId(snapshot.accountId)}. Nothing was uploaded.`);
}
const zones = zoneCount(snapshot);
if (loaded.bytes.byteLength > session.accepts.maxBytes || zones > session.accepts.maxZones) {
throw new LocalCheckupError('SNAPSHOT_TOO_LARGE', `The snapshot is over the vigilbase.com limit (${formatBytes(session.accepts.maxBytes)}, ${session.accepts.maxZones} zones). Nothing was uploaded.`);
}
const requester = displayRequester(session.requester);
const question = `Results go to ${requester}. Upload ${formatBytes(loaded.bytes.byteLength)} to ${origin}?`;
const flag = ctx.values.isYes ? '--yes' : '--confirm';
const isConfirmed = Boolean(ctx.values.isYes || ctx.values.isConfirm);
const bytes = loaded.bytes.byteLength;
const confirmed = ctx.tui
? await ctx.tui.confirmUpload({ bytes, where: origin === PRODUCTION_ORIGIN ? new URL(origin).host : origin, requester, flag: isConfirmed ? flag : null })
: await confirmUpload(promptIO(ctx), question, { isConfirmed, flag });
if (!confirmed)
throw new Error('Upload cancelled; nothing was sent. The snapshot file is kept.');
const post = () => checkup.postSnapshot(loaded.bytes, loaded.sha256);
const result = ctx.tui ? await ctx.tui.uploading(bytes, post) : await post();
if (ctx.mode !== 'json') {
const passed = numberAt(result.report, 'summary', 'passed');
const warnings = numberAt(result.report, 'summary', 'warnings');
const failedChecks = numberAt(result.report, 'summary', 'failed');
const notCollected = (numberAt(result.provenance, 'coverage', 'reads', 'notCollected') ?? 0) +
(numberAt(result.provenance, 'coverage', 'reads', 'collectionFailed') ?? 0);
const parts = [
passed !== undefined ? `${passed} passed` : null,
warnings !== undefined ? `${warnings} warning${warnings === 1 ? '' : 's'}` : null,
failedChecks !== undefined ? `${failedChecks} failed` : null,
`${notCollected} reads not collected locally`,
].filter((part) => part !== null);
const summary = `${result.replayed ? '✓ Already uploaded (same file); nothing changed.' : '✓ Uploaded.'} Free summary: ${parts.join(' · ')}`;
const emailed = result.resultsUrl !== undefined ? `A link has also been emailed to ${requester}.` : `A link to the results has been emailed to ${requester}.`;
if (ctx.tui) {
const opening = result.resultsUrl !== undefined && openResults(ctx, result.resultsUrl, origin);
ctx.tui.uploaded({
replayed: result.replayed, notCollected, requester, opening,
...(passed !== undefined ? { passed } : {}),
...(warnings !== undefined ? { warnings } : {}),
...(failedChecks !== undefined ? { failed: failedChecks } : {}),
...(result.resultsUrl !== undefined ? { resultsUrl: result.resultsUrl } : {}),
}, [summary, ...(result.resultsUrl !== undefined ? [`Results: ${result.resultsUrl}`] : []), emailed]);
}
else {
ctx.writer.stdout(summary);
if (result.resultsUrl !== undefined) {
ctx.writer.stdout(`Results: ${result.resultsUrl}`);
openResults(ctx, result.resultsUrl, origin);
}
ctx.writer.stdout(emailed);
}
}
return {
success: true,
replayed: result.replayed,
snapshotSha256: result.snapshotSha256,
provenance: result.provenance,
report: result.report,
...(result.resultsUrl !== undefined ? { resultsUrl: result.resultsUrl } : {}),
};
}