import _ from "@es-toolkit/es-toolkit/compat"; import { createId } from "@paralleldrive/cuid2"; import { dirname } from "@std/path"; import chalk from "chalk"; import { Effect, pipe } from "effect"; import Moniker from "moniker"; import { ALMA_LINUX_IMG_URL, ALPINE_DEFAULT_VERSION, ALPINE_ISO_URL, DEBIAN_CLOUD_IMG_URL, DEBIAN_DEFAULT_VERSION, DEBIAN_ISO_URL, EMPTY_DISK_THRESHOLD_KB, FEDORA_CLOUD_IMG_URL, FEDORA_COREOS_DEFAULT_VERSION, FEDORA_COREOS_IMG_URL, FEDORA_IMG_URL, GENTOO_IMG_URL, LOGS_DIR, NIXOS_DEFAULT_VERSION, NIXOS_ISO_URL, ROCKY_LINUX_IMG_URL, UBUNTU_CLOUD_IMG_URL, UBUNTU_ISO_URL, } from "./constants.ts"; import type { Image } from "./db.ts"; import { InvalidImageNameError, LogCommandError, NoSuchFileError, NoSuchImageError, } from "./errors.ts"; import { generateRandomMacAddress } from "./network.ts"; import { saveInstanceState, updateInstanceState } from "./state.ts"; export interface Options { output?: string; cpu: string; cpus: number; memory: string; image?: string; diskFormat?: string; size?: string; bridge?: string; portForward?: string; detach?: boolean; install?: boolean; volume?: string; cloud?: boolean; seed?: string; } export const getCurrentArch = (): string => { switch (Deno.build.arch) { case "x86_64": return "amd64"; case "aarch64": return "arm64"; default: return Deno.build.arch; } }; export const isValidISOurl = (url?: string): boolean => { return Boolean( (url?.startsWith("http://") || url?.startsWith("https://")) && url?.endsWith(".iso"), ); }; export const humanFileSize = (blocks: number) => Effect.sync(() => { const blockSize = 512; // bytes per block let bytes = blocks * blockSize; const thresh = 1024; if (Math.abs(bytes) < thresh) { return `${bytes}B`; } const units = ["KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; let u = -1; do { bytes /= thresh; ++u; } while (Math.abs(bytes) >= thresh && u < units.length - 1); return `${bytes.toFixed(1)}${units[u]}`; }); export const validateImage = ( image: string, ): Effect.Effect => { const regex = /^(?:[a-zA-Z0-9.-]+(?:\.[a-zA-Z0-9.-]+)*\/)?[a-z0-9]+(?:[._-][a-z0-9]+)*\/[a-z0-9]+(?:[._-][a-z0-9]+)*(?::[a-zA-Z0-9._-]+)?$/; if (!regex.test(image)) { return Effect.fail( new InvalidImageNameError({ image, cause: "Image name does not conform to expected format. Should be in the format 'repository/name:tag'.", }), ); } return Effect.succeed(image); }; export const extractTag = (name: string) => pipe( validateImage(name), Effect.flatMap((image) => Effect.succeed(image.split(":")[1] || "latest")), ); export const failOnMissingImage = ( image: Image | undefined, ): Effect.Effect => image ? Effect.succeed(image) : Effect.fail(new NoSuchImageError({ cause: "No such image" })); export const du = ( path: string, ): Effect.Effect => Effect.tryPromise({ try: async () => { const cmd = new Deno.Command("du", { args: [path], stdout: "piped", stderr: "inherit", }); const { stdout } = await cmd.spawn().output(); const output = new TextDecoder().decode(stdout).trim(); const size = parseInt(output.split("\t")[0], 10); return size; }, catch: (error) => new LogCommandError({ cause: error }), }); export const emptyDiskImage = (path: string) => Effect.tryPromise({ try: async () => { if (!(await Deno.stat(path).catch(() => false))) { return true; } return false; }, catch: (error) => new LogCommandError({ cause: error }), }).pipe( Effect.flatMap((exists) => exists ? Effect.succeed(true) : du(path).pipe(Effect.map((size) => size < EMPTY_DISK_THRESHOLD_KB)) ), ); export const downloadIso = (url: string, options: Options) => Effect.gen(function* () { const filename = url.split("/").pop()!; const outputPath = options.output ?? filename; if (options.image) { const imageExists = yield* Effect.tryPromise({ try: () => Deno.stat(options.image!) .then(() => true) .catch(() => false), catch: (error) => new LogCommandError({ cause: error }), }); if (imageExists) { const driveSize = yield* du(options.image); if (driveSize > EMPTY_DISK_THRESHOLD_KB) { console.log( chalk.yellowBright( `Drive image ${options.image} is not empty (size: ${driveSize} KB), skipping ISO download to avoid overwriting existing data.`, ), ); return null; } } } const outputExists = yield* Effect.tryPromise({ try: () => Deno.stat(outputPath) .then(() => true) .catch(() => false), catch: (error) => new LogCommandError({ cause: error }), }); if (outputExists) { console.log( chalk.yellowBright( `File ${outputPath} already exists, skipping download.`, ), ); return outputPath; } yield* Effect.tryPromise({ try: async () => { console.log( chalk.blueBright( `Downloading ${ url.endsWith(".iso") ? "ISO" : "image" } from ${url}...`, ), ); const cmd = new Deno.Command("curl", { args: ["-L", "-o", outputPath, url], stdin: "inherit", stdout: "inherit", stderr: "inherit", }); const status = await cmd.spawn().status; if (!status.success) { console.error(chalk.redBright("Failed to download ISO image.")); Deno.exit(status.code); } }, catch: (error) => new LogCommandError({ cause: error }), }); console.log(chalk.greenBright(`Downloaded ISO to ${outputPath}`)); return outputPath; }); export const setupFirmwareFilesIfNeeded = () => Effect.gen(function* () { if (Deno.build.arch !== "aarch64") { return []; } const { stdout, success } = yield* Effect.tryPromise({ try: async () => { const brewCmd = new Deno.Command("brew", { args: ["--prefix", "qemu"], stdout: "piped", stderr: "inherit", }); return await brewCmd.spawn().output(); }, catch: (error) => new LogCommandError({ cause: error }), }); if (!success) { console.error( chalk.redBright( "Failed to get QEMU prefix from Homebrew. Ensure QEMU is installed via Homebrew.", ), ); Deno.exit(1); } const brewPrefix = new TextDecoder().decode(stdout).trim(); const edk2Aarch64 = `${brewPrefix}/share/qemu/edk2-aarch64-code.fd`; const edk2VarsAarch64 = "./edk2-arm-vars.fd"; yield* Effect.tryPromise({ try: () => Deno.copyFile( `${brewPrefix}/share/qemu/edk2-arm-vars.fd`, edk2VarsAarch64, ), catch: (error) => new LogCommandError({ cause: error }), }); return [ "-drive", `if=pflash,format=raw,file=${edk2Aarch64},readonly=on`, "-drive", `if=pflash,format=raw,file=${edk2VarsAarch64}`, ]; }); export function setupPortForwardingArgs(portForward?: string): string { if (!portForward) { return ""; } const forwards = portForward.split(",").map((pair) => { const [hostPort, guestPort] = pair.split(":"); return `hostfwd=tcp::${hostPort}-:${guestPort}`; }); return forwards.join(","); } export function setupNATNetworkArgs(portForward?: string): string { if (!portForward) { return "user,id=net0"; } const portForwarding = setupPortForwardingArgs(portForward); return `user,id=net0,${portForwarding}`; } export const setupCoreOSArgs = (imagePath?: string | null) => Effect.gen(function* () { if ( imagePath && imagePath.endsWith(".qcow2") && imagePath.includes("coreos") ) { const configOK = yield* pipe( fileExists("config.ign"), Effect.flatMap(() => Effect.succeed(true)), Effect.catchAll(() => Effect.succeed(false)), ); if (!configOK) { console.error( chalk.redBright( "CoreOS image requires a config.ign file in the current directory.", ), ); Deno.exit(1); } return [ "-drive", `file=${imagePath},format=qcow2,if=virtio`, "-fw_cfg", "name=opt/com.coreos/config,file=config.ign", ]; } return []; }); export const setupFedoraArgs = (imagePath?: string | null, seed?: string) => Effect.sync(() => { if ( imagePath && imagePath.endsWith(".qcow2") && (imagePath.includes("Fedora-Server") || imagePath.includes("Fedora-Cloud")) ) { return [ "-drive", `file=${imagePath},format=qcow2,if=virtio`, ...(seed ? ["-drive", `if=virtio,file=${seed},media=cdrom`] : []), ]; } return []; }); export const setupGentooArgs = (imagePath?: string | null, seed?: string) => Effect.sync(() => { if ( imagePath && imagePath.endsWith(".qcow2") && imagePath.startsWith( `di-${Deno.build.arch === "aarch64" ? "arm64" : "amd64"}-console-`, ) ) { return [ "-drive", `file=${imagePath},format=qcow2,if=virtio`, ...(seed ? ["-drive", `if=virtio,file=${seed},media=cdrom`] : []), ]; } return []; }); export const setupAlpineArgs = ( imagePath?: string | null, seed: string = "seed.iso", ) => Effect.sync(() => { if ( imagePath && imagePath.endsWith(".qcow2") && imagePath.includes("alpine") ) { return [ "-drive", `file=${imagePath},format=qcow2,if=virtio`, "-drive", `if=virtio,file=${seed},media=cdrom`, ]; } return []; }); export const setupDebianArgs = ( imagePath?: string | null, seed: string = "seed.iso", ) => Effect.sync(() => { if ( imagePath && imagePath.endsWith(".qcow2") && imagePath.includes("debian") ) { return [ "-drive", `file=${imagePath},format=qcow2,if=virtio`, "-drive", `if=virtio,file=${seed},media=cdrom`, ]; } return []; }); export const setupUbuntuArgs = ( imagePath?: string | null, seed: string = "seed.iso", ) => Effect.sync(() => { if ( imagePath && imagePath.endsWith(".img") && imagePath.includes("server-cloudimg") ) { return [ "-drive", `file=${imagePath},format=qcow2,if=virtio`, "-drive", `if=virtio,file=${seed},media=cdrom`, ]; } return []; }); export const setupAlmaLinuxArgs = ( imagePath?: string | null, seed: string = "seed.iso", ) => Effect.sync(() => { if ( imagePath && imagePath.endsWith(".qcow2") && imagePath.includes("AlmaLinux") ) { return [ "-drive", `file=${imagePath},format=qcow2,if=virtio`, "-drive", `if=virtio,file=${seed},media=cdrom`, ]; } return []; }); export const setupRockyLinuxArgs = ( imagePath?: string | null, seed: string = "seed.iso", ) => Effect.sync(() => { if ( imagePath && imagePath.endsWith(".qcow2") && imagePath.includes("Rocky") ) { return [ "-drive", `file=${imagePath},format=qcow2,if=virtio`, "-drive", `if=virtio,file=${seed},media=cdrom`, ]; } return []; }); export const runQemu = (isoPath: string | null, options: Options) => Effect.gen(function* () { const macAddress = yield* generateRandomMacAddress(); const qemu = Deno.build.arch === "aarch64" ? "qemu-system-aarch64" : "qemu-system-x86_64"; const firmwareFiles = yield* setupFirmwareFilesIfNeeded(); let coreosArgs: string[] = yield* setupCoreOSArgs(isoPath || options.image); let fedoraArgs: string[] = yield* setupFedoraArgs( isoPath || options.image, options.seed, ); let gentooArgs: string[] = yield* setupGentooArgs( isoPath || options.image, options.seed, ); let alpineArgs: string[] = yield* setupAlpineArgs( isoPath || options.image, options.seed, ); let debianArgs: string[] = yield* setupDebianArgs( isoPath || options.image, options.seed, ); let ubuntuArgs: string[] = yield* setupUbuntuArgs( isoPath || options.image, options.seed, ); let almalinuxArgs: string[] = yield* setupAlmaLinuxArgs( isoPath || options.image, options.seed, ); let rockylinuxArgs: string[] = yield* setupRockyLinuxArgs( isoPath || options.image, options.seed, ); if (coreosArgs.length > 0 && !isoPath) { coreosArgs = coreosArgs.slice(2); } if (fedoraArgs.length > 0 && !isoPath) { fedoraArgs = []; } if (gentooArgs.length > 0 && !isoPath) { gentooArgs = []; } if (alpineArgs.length > 0 && !isoPath) { alpineArgs = alpineArgs.slice(2); } if (debianArgs.length > 0 && !isoPath) { debianArgs = []; } if (ubuntuArgs.length > 0 && !isoPath) { ubuntuArgs = []; } if (almalinuxArgs.length > 0 && !isoPath) { almalinuxArgs = []; } if (rockylinuxArgs.length > 0 && !isoPath) { rockylinuxArgs = []; } const qemuArgs = [ ..._.compact([options.bridge && qemu]), ...(Deno.build.os === "darwin" ? ["-accel", "hvf"] : ["-enable-kvm"]), ...(Deno.build.arch === "aarch64" ? ["-machine", "virt,highmem=on"] : []), "-cpu", options.cpu, "-m", options.memory, "-smp", options.cpus.toString(), ...(isoPath && isoPath.endsWith(".iso") ? ["-cdrom", isoPath] : []), "-netdev", options.bridge ? `bridge,id=net0,br=${options.bridge}` : setupNATNetworkArgs(options.portForward), "-device", `e1000,netdev=net0,mac=${macAddress}`, ...(options.install ? [] : ["-snapshot"]), "-nographic", "-monitor", "none", "-chardev", "stdio,id=con0,signal=off", "-serial", "chardev:con0", ...firmwareFiles, ...coreosArgs, ...fedoraArgs, ...gentooArgs, ...alpineArgs, ...debianArgs, ...ubuntuArgs, ...almalinuxArgs, ...rockylinuxArgs, ..._.compact( options.image && [ "-drive", `file=${options.image},format=${options.diskFormat},if=virtio`, ], ), ]; const name = Moniker.choose(); if (options.detach) { yield* Effect.tryPromise({ try: () => Deno.mkdir(LOGS_DIR, { recursive: true }), catch: (error) => new LogCommandError({ cause: error }), }); const logPath = `${LOGS_DIR}/${name}.log`; const fullCommand = options.bridge ? `sudo ${qemu} ${ qemuArgs .slice(1) .join(" ") } >> "${logPath}" 2>&1 & echo $!` : `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`; const { stdout } = yield* Effect.tryPromise({ try: async () => { const cmd = new Deno.Command("sh", { args: ["-c", fullCommand], stdin: "null", stdout: "piped", }); return await cmd.spawn().output(); }, catch: (error) => new LogCommandError({ cause: error }), }); const qemuPid = parseInt(new TextDecoder().decode(stdout).trim(), 10); yield* saveInstanceState({ id: createId(), name, bridge: options.bridge, macAddress, memory: options.memory, cpus: options.cpus, cpu: options.cpu, diskSize: options.size || "20G", diskFormat: (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || options.diskFormat || "raw", portForward: options.portForward, isoPath: isoPath && !isoPath.includes("coreos") && isoPath.endsWith(".iso") ? Deno.realPathSync(isoPath) : undefined, drivePath: options.image ? Deno.realPathSync(options.image) : isoPath?.endsWith("qcow2") ? Deno.realPathSync(isoPath) : undefined, status: "RUNNING", pid: qemuPid, seed: options.seed, }); console.log( `Virtual machine ${name} started in background (PID: ${qemuPid})`, ); console.log(`Logs will be written to: ${logPath}`); // Exit successfully while keeping VM running in background Deno.exit(0); } else { const cmd = new Deno.Command(options.bridge ? "sudo" : qemu, { args: qemuArgs, stdin: "inherit", stdout: "inherit", stderr: "inherit", }).spawn(); yield* saveInstanceState({ id: createId(), name, bridge: options.bridge, macAddress, memory: options.memory, cpus: options.cpus, cpu: options.cpu, diskSize: options.size || "20G", diskFormat: (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || options.diskFormat || "raw", portForward: options.portForward, isoPath: isoPath && !isoPath.includes("coreos") && isoPath.endsWith(".iso") ? Deno.realPathSync(isoPath) : undefined, drivePath: options.image ? Deno.realPathSync(options.image) : isoPath?.endsWith("qcow2") ? Deno.realPathSync(isoPath) : undefined, status: "RUNNING", pid: cmd.pid, seed: options.seed ? Deno.realPathSync(options.seed) : undefined, }); const status = yield* Effect.tryPromise({ try: () => cmd.status, catch: (error) => new LogCommandError({ cause: error }), }); yield* updateInstanceState(name, "STOPPED"); if (!status.success) { Deno.exit(status.code); } } }); export const safeKillQemu = (pid: number, useSudo: boolean = false) => Effect.gen(function* () { const killArgs = useSudo ? ["sudo", "kill", "-TERM", pid.toString()] : ["kill", "-TERM", pid.toString()]; const termStatus = yield* Effect.tryPromise({ try: async () => { const termCmd = new Deno.Command(killArgs[0], { args: killArgs.slice(1), stdout: "null", stderr: "null", }); return await termCmd.spawn().status; }, catch: (error) => new LogCommandError({ cause: error }), }); if (termStatus.success) { yield* Effect.tryPromise({ try: () => new Promise((resolve) => setTimeout(resolve, 3000)), catch: (error) => new LogCommandError({ cause: error }), }); const checkStatus = yield* Effect.tryPromise({ try: async () => { const checkCmd = new Deno.Command("kill", { args: ["-0", pid.toString()], stdout: "null", stderr: "null", }); return await checkCmd.spawn().status; }, catch: (error) => new LogCommandError({ cause: error }), }); if (!checkStatus.success) { return true; } } const killKillArgs = useSudo ? ["sudo", "kill", "-KILL", pid.toString()] : ["kill", "-KILL", pid.toString()]; const killStatus = yield* Effect.tryPromise({ try: async () => { const killCmd = new Deno.Command(killKillArgs[0], { args: killKillArgs.slice(1), stdout: "null", stderr: "null", }); return await killCmd.spawn().status; }, catch: (error) => new LogCommandError({ cause: error }), }); return killStatus.success; }); export const createDriveImageIfNeeded = ({ image: path, diskFormat: format, size, }: Options) => Effect.gen(function* () { const pathExists = yield* Effect.tryPromise({ try: () => Deno.stat(path!) .then(() => true) .catch(() => false), catch: (error) => new LogCommandError({ cause: error }), }); if (pathExists) { console.log( chalk.yellowBright( `Drive image ${path} already exists, skipping creation.`, ), ); return; } const status = yield* Effect.tryPromise({ try: async () => { const cmd = new Deno.Command("qemu-img", { args: ["create", "-f", format || "raw", path!, size!], stdin: "inherit", stdout: "inherit", stderr: "inherit", }); return await cmd.spawn().status; }, catch: (error) => new LogCommandError({ cause: error }), }); if (!status.success) { console.error(chalk.redBright("Failed to create drive image.")); Deno.exit(status.code); } console.log(chalk.greenBright(`Created drive image at ${path}`)); }); export const fileExists = ( path: string, ): Effect.Effect => Effect.try({ try: () => Deno.statSync(path), catch: (error) => new NoSuchFileError({ cause: String(error) }), }); export const constructCoreOSImageURL = ( image: string, ): Effect.Effect => { // detect with regex if image matches coreos pattern: fedora-coreos or fedora-coreos- or coreos or coreos- const coreosRegex = /^(fedora-coreos|coreos)(-(\d+\.\d+\.\d+\.\d+))?$/; const match = image.match(coreosRegex); if (match) { const version = match[3] || FEDORA_COREOS_DEFAULT_VERSION; return Effect.succeed( FEDORA_COREOS_IMG_URL.replaceAll(FEDORA_COREOS_DEFAULT_VERSION, version), ); } return Effect.fail( new InvalidImageNameError({ image, cause: "Image name does not match CoreOS naming conventions.", }), ); }; export const extractXz = (path: string | null) => Effect.tryPromise({ try: async () => { if (!path) { return null; } const cmd = new Deno.Command("xz", { args: ["-d", path], stdin: "inherit", stdout: "inherit", stderr: "inherit", cwd: dirname(path), }).spawn(); const status = await cmd.status; if (!status.success) { console.error(chalk.redBright("Failed to extract xz file.")); Deno.exit(status.code); } return path.replace(/\.xz$/, ""); }, catch: (error) => new LogCommandError({ cause: error }), }); export const constructNixOSImageURL = ( image: string, ): Effect.Effect => { // detect with regex if image matches NixOS pattern: nixos or nixos- const nixosRegex = /^(nixos)(-(\d+\.\d+))?$/; const match = image.match(nixosRegex); if (match) { const version = match[3] || NIXOS_DEFAULT_VERSION; return Effect.succeed( NIXOS_ISO_URL.replaceAll(NIXOS_DEFAULT_VERSION, version), ); } return Effect.fail( new InvalidImageNameError({ image, cause: "Image name does not match NixOS naming conventions.", }), ); }; export const constructFedoraImageURL = ( image: string, cloud: boolean = false, ): Effect.Effect => { // detect with regex if image matches Fedora pattern: fedora const fedoraRegex = /^(fedora)$/; const match = image.match(fedoraRegex); if (match) { return Effect.succeed(cloud ? FEDORA_CLOUD_IMG_URL : FEDORA_IMG_URL); } return Effect.fail( new InvalidImageNameError({ image, cause: "Image name does not match Fedora naming conventions.", }), ); }; export const constructGentooImageURL = ( image: string, ): Effect.Effect => { // detect with regex if image matches genroo pattern: gentoo-20251116T161545Z or gentoo const gentooRegex = /^(gentoo)(-(\d{8}T\d{6}Z))?$/; const match = image.match(gentooRegex); if (match?.[3]) { return Effect.succeed( GENTOO_IMG_URL.replaceAll("20251116T161545Z", match[3]).replaceAll( "20251116T233105Z", match[3], ), ); } if (match) { return Effect.succeed(GENTOO_IMG_URL); } return Effect.fail( new InvalidImageNameError({ image, cause: "Image name does not match Gentoo naming conventions.", }), ); }; export const constructDebianImageURL = ( image: string, cloud: boolean = false, ): Effect.Effect => { if (cloud && image === "debian") { return Effect.succeed(DEBIAN_CLOUD_IMG_URL); } // detect with regex if image matches debian pattern: debian- or debian const debianRegex = /^(debian)(-(\d+\.\d+\.\d+))?$/; const match = image.match(debianRegex); if (match?.[3]) { return Effect.succeed( DEBIAN_ISO_URL.replaceAll(DEBIAN_DEFAULT_VERSION, match[3]), ); } if (match) { return Effect.succeed(DEBIAN_ISO_URL); } return Effect.fail( new InvalidImageNameError({ image, cause: "Image name does not match Debian naming conventions.", }), ); }; export const constructAlpineImageURL = ( image: string, ): Effect.Effect => { // detect with regex if image matches alpine pattern: alpine- or alpine const alpineRegex = /^(alpine)(-(\d+\.\d+(\.\d+)?))?$/; const match = image.match(alpineRegex); if (match?.[3]) { return Effect.succeed( ALPINE_ISO_URL.replaceAll(ALPINE_DEFAULT_VERSION, match[3]), ); } if (match) { return Effect.succeed(ALPINE_ISO_URL); } return Effect.fail( new InvalidImageNameError({ image, cause: "Image name does not match Alpine naming conventions.", }), ); }; export const constructUbuntuImageURL = ( image: string, cloud: boolean = false, ): Effect.Effect => { // detect with regex if image matches ubuntu pattern: ubuntu const ubuntuRegex = /^(ubuntu)$/; const match = image.match(ubuntuRegex); if (match) { if (cloud) { return Effect.succeed(UBUNTU_CLOUD_IMG_URL); } return Effect.succeed(UBUNTU_ISO_URL); } return Effect.fail( new InvalidImageNameError({ image, cause: "Image name does not match Ubuntu naming conventions.", }), ); }; export const constructAlmaLinuxImageURL = ( image: string, cloud: boolean = false, ): Effect.Effect => { // detect with regex if image matches almalinux pattern: almalinux, almalinux const almaLinuxRegex = /^(almalinux|alma)$/; const match = image.match(almaLinuxRegex); if (match) { if (cloud) { return Effect.succeed(ALMA_LINUX_IMG_URL); } return Effect.succeed(ALMA_LINUX_IMG_URL); } return Effect.fail( new InvalidImageNameError({ image, cause: "Image name does not match AlmaLinux naming conventions.", }), ); }; export const constructRockyLinuxImageURL = ( image: string, cloud: boolean = false, ): Effect.Effect => { // detect with regex if image matches rockylinux pattern: rocky. rockylinux const rockyLinuxRegex = /^(rockylinux|rocky)$/; const match = image.match(rockyLinuxRegex); if (match) { if (cloud) { return Effect.succeed(ROCKY_LINUX_IMG_URL); } return Effect.succeed(ROCKY_LINUX_IMG_URL); } return Effect.fail( new InvalidImageNameError({ image, cause: "Image name does not match RockyLinux naming conventions.", }), ); };