UNPKG

@vigilbase/cli

Version:

Vigilbase CLI — security scanners, agent tools, and Vigilbase One from the terminal.

107 lines (106 loc) • 5.29 kB
import { z } from 'zod'; import { assertCommandFlags } from './helpers.js'; /** A flag or argument the CLI rejected before any request; `--json` reports it as code INVALID_ARGUMENT. */ export class InvalidArgumentError extends Error { code = 'INVALID_ARGUMENT'; constructor(message) { super(message); this.name = 'InvalidArgumentError'; } } /** * Exactly `count` positional arguments. When some are missing, they are named from the trailing `<placeholders>` * of `usage` (for example `vigilbase run <tool> <target>`), so `usage` lists only positional placeholders. * An empty or whitespace-only argument counts as missing. */ export function validateArguments(ctx, count, flags = [], usage) { if (ctx.rest.length < count) throw new Error(missingArguments(ctx.command, count - ctx.rest.length, usage)); if (ctx.rest.length > count) throw new Error(`Unexpected arguments. Run \`vigilbase ${ctx.command} --help\` for usage.`); const blank = ctx.rest.flatMap((value, index) => value.trim() ? [] : [index]); if (blank.length) { const placeholders = usage?.match(/<[^>]+>/g) ?? []; const missing = blank.map((index) => placeholders[index - (count - placeholders.length)]); throw new Error(missing.every(Boolean) && usage ? `Missing ${missing.join(' and ')}. Usage: ${usage}` : `Missing arguments. Run \`vigilbase ${ctx.command} --help\` for usage.`); } assertCommandFlags(ctx, flags); } /** "Missing <target>. Usage: vigilbase headers <url>", or a pointer to --help without a usage. */ export function missingArguments(command, missingCount, usage) { const missing = usage?.match(/<[^>]+>/g)?.slice(-missingCount) ?? []; if (!usage || missing.length !== missingCount) return `Missing arguments. Run \`vigilbase ${command} --help\` for usage.`; return `Missing ${missing.join(' and ')}. Usage: ${usage}`; } /** 'Unknown security command "x". Run `vigilbase security --help` for usage.' The pointer names the top-level group. */ export function unknownSubcommand(group, subcommand) { return new InvalidArgumentError(`Unknown ${group} command "${subcommand.slice(0, 80)}". Run \`vigilbase ${group.split(' ')[0]} --help\` for usage.`); } /** Parses user input with `schema`, throwing `message` instead of Zod's issue list. */ export function parseArgument(schema, value, message) { const result = schema.safeParse(value); if (!result.success) throw new InvalidArgumentError(message); return result.data; } /** An integer already checked as digits, within [min, max]. */ export function inRange(label, value, min, max) { if (!Number.isSafeInteger(value) || value < min || value > max) throw new InvalidArgumentError(`${label} must be between ${min} and ${max}.`); return value; } export function oneOf(label, values, value) { if (value === undefined) return undefined; if (!values.includes(value)) throw new InvalidArgumentError(`${label} must be one of: ${values.join(', ')}.`); return value; } /** A trimmed, non-empty flag value of at most `max` characters. */ export function textValue(label, value, max) { if (value === undefined) return undefined; const trimmed = value.trim(); if (!trimmed) throw new InvalidArgumentError(`${label} must not be empty.`); if (trimmed.length > max) throw new InvalidArgumentError(`${label} must be at most ${max} characters.`); return trimmed; } export function isoDateTime(label, value) { return parseArgument(z.iso.datetime({ offset: true }), value, `${label} must be an ISO 8601 date-time with an offset, for example 2026-10-12T00:00:00Z.`); } export function limitValue(raw, fallback, max) { if (raw === undefined) return fallback; if (!/^\d+$/.test(raw)) throw new InvalidArgumentError('--limit must be an integer.'); return inRange('--limit', Number(raw), 1, max); } const invitationRoles = ['admin', 'member', 'readonly', 'partner_owner', 'partner_sales', 'partner_operations', 'partner_finance', 'partner_readonly', 'billing_only', 'sales_only']; export const invitationSchema = z.object({ email: z.string().trim().email(), role: z.enum(invitationRoles).default('member'), }); export function invitationValue(email, role) { return { email: parseArgument(invitationSchema.shape.email, email, 'Enter a valid email address for the invitation.'), role: oneOf('--role', invitationRoles, role) ?? 'member', }; } export const toolNameSchema = z.string().trim().min(1).max(128).regex(/^[a-z0-9-]+$/, 'Tool names must contain lowercase letters, digits, or hyphens.'); export function toolNameValue(value) { return parseArgument(toolNameSchema, value, 'Tool names must contain lowercase letters, digits, or hyphens.'); } /** A scan target: trimmed, 1-2048 characters, no control characters. */ export function targetValue(value) { const trimmed = value.trim(); if (/[\u0000-\u001f\u007f]/.test(trimmed)) throw new InvalidArgumentError('Target must not contain control characters.'); if (!trimmed || trimmed.length > 2048) throw new InvalidArgumentError('Target must be 1-2048 characters.'); return trimmed; }