@vigilbase/cli
Version:
Vigilbase CLI — security scanners, agent tools, and Vigilbase One from the terminal.
228 lines (227 loc) • 11.4 kB
JavaScript
/**
* Locating Cloudflare's cf CLI and checking its identity (spec §F2).
*
* Candidates, in order: `--cf-bin`; the Vigilbase-managed install
* (`<configDir>/cf/<version>`, see install.ts); absolute PATH entries holding
* `cf`, then `cloudflare` (`cf.cmd` / `cloudflare.cmd` on Windows); then pnpm
* and Volta global package directories, probed without spawning a package
* manager. A
* candidate is accepted only if its package.json says it is Cloudflare's cf
* (`name`, `repository`, `bin.cf`); the Cloud Foundry CLI, which also installs
* `cf`, and unresolvable shims are skipped. cf is then run as
* `process.execPath <bin.cf entry>`, never through a shim or shell.
*/
import { promises as fsPromises } from 'node:fs';
import { homedir } from 'node:os';
import path from 'node:path';
import { CF_SUPPORTED, isSupportedCfVersion } from '../contract.js';
import { LocalCheckupError } from '../errors.js';
const nodeFs = {
stat: (target) => fsPromises.stat(target),
realpath: (target) => fsPromises.realpath(target),
readFile: (target) => fsPromises.readFile(target, 'utf8'),
readdir: (target) => fsPromises.readdir(target),
};
const MIN_NODE = [22, 18];
const MAX_PACKAGE_JSON_BYTES = 1024 * 1024;
const MAX_WALK_UP = 4;
const MAX_PACKAGE_MANAGER_PROBES = 20;
const MAX_VOLTA_DEPTH = 4;
export function isNodeSupported(version = process.versions.node) {
const [major, minor] = version.split('.').map((part) => Number.parseInt(part, 10));
if (!Number.isInteger(major) || !Number.isInteger(minor))
return false;
return major > MIN_NODE[0] || (major === MIN_NODE[0] && minor >= MIN_NODE[1]);
}
/** cf runs under this Node via process.execPath; workerd ships no win32-arm64 build. */
export function assertLocalRuntimeSupported(version = process.versions.node, platform = process.platform, arch = process.arch) {
if (!isNodeSupported(version)) {
throw new LocalCheckupError('NODE_UNSUPPORTED', `Node.js ${version} is too old for cf; local checkups need Node.js 22.18 or newer.`);
}
if (!['darwin', 'linux', 'win32'].includes(platform) || (platform === 'win32' && arch === 'arm64')) {
throw new LocalCheckupError('NODE_UNSUPPORTED', `cf does not support ${platform}-${arch}. Use macOS, Linux or Windows x64.`);
}
}
function envLookup(env, platform, name) {
if (platform !== 'win32')
return env[name];
const key = Object.keys(env).find((candidate) => candidate.toUpperCase() === name.toUpperCase());
return key === undefined ? undefined : env[key];
}
function repositoryUrl(repository) {
if (typeof repository === 'string')
return repository;
if (repository && typeof repository === 'object' && typeof repository.url === 'string') {
return repository.url;
}
return '';
}
export async function locateCf(options) {
const platform = options.platform ?? process.platform;
const p = platform === 'win32' ? path.win32 : path.posix;
const fs = options.fs ?? nodeFs;
const cwd = options.cwd ?? process.cwd();
const home = options.home ?? (envLookup(options.env, platform, platform === 'win32' ? 'USERPROFILE' : 'HOME') || homedir());
const cwdIsHome = Boolean(home) && p.isAbsolute(home) && p.relative(home, cwd) === '';
let sawOther = false;
const isFile = async (target) => (await fs.stat(target).catch(() => null))?.isFile() === true;
const isDir = async (target) => (await fs.stat(target).catch(() => null))?.isDirectory() === true;
const verifyPackage = async (packageDir, expectedEntry) => {
const text = await fs.readFile(p.join(packageDir, 'package.json')).catch(() => null);
if (text === null)
return { ok: false, reason: 'missing' };
if (text.length > MAX_PACKAGE_JSON_BYTES)
return { ok: false, reason: 'impostor' };
let manifest;
try {
manifest = JSON.parse(text);
}
catch {
return { ok: false, reason: 'impostor' };
}
const binCf = manifest.bin && typeof manifest.bin === 'object' ? manifest.bin.cf : undefined;
if (manifest.name !== 'cf' || !repositoryUrl(manifest.repository).includes('github.com/cloudflare/cf') ||
typeof binCf !== 'string' || typeof manifest.version !== 'string') {
return { ok: false, reason: 'impostor' };
}
const entry = p.resolve(packageDir, binCf);
const relative = p.relative(packageDir, entry);
if (!relative || relative.startsWith('..') || p.isAbsolute(relative) || !(await isFile(entry))) {
return { ok: false, reason: 'impostor' };
}
if (expectedEntry !== undefined) {
const realEntry = await fs.realpath(entry).catch(() => entry);
if (realEntry !== expectedEntry)
return { ok: false, reason: 'impostor' };
}
return { ok: true, packageDir, entry, version: manifest.version };
};
/** A POSIX bin (symlink or file): resolve it and walk up to the nearest package.json. */
const fromPosixBin = async (bin) => {
const real = await fs.realpath(bin).catch(() => null);
if (real === null)
return { ok: false, reason: 'missing' };
let dir = p.dirname(real);
for (let depth = 0; depth < MAX_WALK_UP; depth++) {
if (await isFile(p.join(dir, 'package.json')))
return verifyPackage(dir, real);
const parent = p.dirname(dir);
if (parent === dir)
break;
dir = parent;
}
return { ok: false, reason: 'missing' };
};
const accept = (verified, source) => {
const relative = p.relative(cwd, verified.packageDir);
const inside = relative === '' || (!relative.startsWith('..') && !p.isAbsolute(relative));
return {
packageDir: verified.packageDir,
entry: verified.entry,
packageVersion: verified.version,
source,
insideCwd: inside && source !== 'managed' && !cwdIsHome,
};
};
// 1. --cf-bin: the package directory or its bin/cf. An explicit path is not
// silently replaced by another cf.
if (options.cfBin !== undefined) {
if (!p.isAbsolute(options.cfBin))
throw new Error('--cf-bin must be an absolute path.');
let verified;
if (await isDir(options.cfBin))
verified = await verifyPackage(options.cfBin);
else if (platform === 'win32')
verified = await verifyPackage(p.dirname(p.dirname(options.cfBin)));
else
verified = await fromPosixBin(options.cfBin);
if (verified.ok)
return accept(verified, 'cf-bin');
if (verified.reason === 'missing' && !(await fs.stat(options.cfBin).catch(() => null))) {
throw new LocalCheckupError('CF_NOT_FOUND', 'No file or directory exists at --cf-bin.');
}
throw new LocalCheckupError('CF_WRONG_BINARY', '--cf-bin does not point to Cloudflare\'s cf package (name "cf", repository github.com/cloudflare/cf).');
}
// 2. The Vigilbase-managed install; an unusable one falls through to the other candidates.
if (options.managedDir !== undefined && p.isAbsolute(options.managedDir)) {
const verified = await verifyPackage(options.managedDir);
if (verified.ok)
return accept(verified, 'managed');
}
// 3. Absolute PATH entries; `cf` everywhere first, then `cloudflare`.
const pathValue = envLookup(options.env, platform, 'PATH') ?? '';
const dirs = [...new Set(pathValue.split(platform === 'win32' ? ';' : ':'))]
.filter((dir) => dir !== '' && dir !== '.' && p.isAbsolute(dir));
const names = platform === 'win32' ? ['cf.cmd', 'cloudflare.cmd'] : ['cf', 'cloudflare'];
for (const name of names) {
for (const dir of dirs) {
const hit = p.join(dir, name);
if (!(await isFile(hit)))
continue;
// npm's Windows global layout keeps the package beside the shim.
const verified = platform === 'win32' ? await verifyPackage(p.join(dir, 'node_modules', 'cf')) : await fromPosixBin(hit);
if (verified.ok)
return accept(verified, 'path');
// Cloud Foundry's cf, or a shim (pnpm) without a resolvable package: keep searching.
sawOther = true;
}
}
// 4. Package-manager global directories, at most 20 filesystem probes.
let probes = 0;
const probe = async (work, fallback) => {
if (probes >= MAX_PACKAGE_MANAGER_PROBES)
return fallback;
probes++;
return work().catch(() => fallback);
};
const pnpmHome = envLookup(options.env, platform, 'PNPM_HOME');
if (pnpmHome && p.isAbsolute(pnpmHome)) {
const globalDir = p.join(pnpmHome, 'global');
const versions = await probe(() => fs.readdir(globalDir), []);
for (const version of versions.sort()) {
const packageDir = p.join(globalDir, version, 'node_modules', 'cf');
if (!(await probe(() => isFile(p.join(packageDir, 'package.json')), false)))
continue;
const verified = await verifyPackage(packageDir);
if (verified.ok)
return accept(verified, 'pnpm');
sawOther = true;
}
}
const voltaHome = envLookup(options.env, platform, 'VOLTA_HOME');
if (voltaHome && p.isAbsolute(voltaHome)) {
let frontier = [p.join(voltaHome, 'tools', 'image', 'packages', 'cf')];
for (let depth = 0; depth <= MAX_VOLTA_DEPTH && frontier.length > 0; depth++) {
const next = [];
for (const dir of frontier) {
if (p.basename(dir) === 'cf' && p.basename(p.dirname(dir)) === 'node_modules' &&
await probe(() => isFile(p.join(dir, 'package.json')), false)) {
const verified = await verifyPackage(dir);
if (verified.ok)
return accept(verified, 'volta');
sawOther = true;
continue;
}
const children = await probe(() => fs.readdir(dir), []);
for (const child of children.sort())
next.push(p.join(dir, child));
}
frontier = next;
}
}
if (sawOther) {
throw new LocalCheckupError('CF_WRONG_BINARY', 'A program named cf was found, but it is not Cloudflare\'s cf CLI (for example the Cloud Foundry CLI or an unresolvable shim).');
}
throw new LocalCheckupError('CF_NOT_FOUND', 'Cloudflare\'s cf CLI was not found on PATH or in the pnpm and Volta global directories.');
}
/** Gate the version cf prints against its package.json and `CF_SUPPORTED`. */
export function checkCfVersion(printed, packageVersion) {
const version = printed.trim();
if (version !== packageVersion) {
throw new LocalCheckupError('CF_WRONG_BINARY', 'cf --version does not match the version in its package.json.');
}
if (!isSupportedCfVersion(version)) {
throw new LocalCheckupError('CF_UNSUPPORTED_VERSION', `cf ${version.slice(0, 40)} is not supported; local checkups need cf >= ${CF_SUPPORTED.min} and < ${CF_SUPPORTED.maxExclusive}.`);
}
return { version, tested: CF_SUPPORTED.tested.includes(version) };
}