@vigilbase/cli
Version:
Vigilbase CLI — security scanners, agent tools, and Vigilbase One from the terminal.
107 lines (106 loc) • 5.29 kB
JavaScript
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;
}