diff --git a/main.ts b/main.ts index fbc4f53..1b834a8 100755 --- a/main.ts +++ b/main.ts @@ -31,6 +31,7 @@ import stop from "./src/subcommands/stop.ts"; import tag from "./src/subcommands/tag.ts"; import * as volumes from "./src/subcommands/volume.ts"; import { + constructDebianImageURL, constructFedoraImageURL, constructGentooImageURL, constructNixOSImageURL, @@ -247,6 +248,25 @@ if (import.meta.main) { isoPath = basename(gentooImageURL); } } + + const debianImageURL = yield* pipe( + constructDebianImageURL(input), + Effect.catchAll(() => Effect.succeed(null)), + ); + + if (debianImageURL) { + const cached = yield* pipe( + basename(debianImageURL), + fileExists, + Effect.flatMap(() => Effect.succeed(true)), + Effect.catchAll(() => Effect.succeed(false)), + ); + if (!cached) { + isoPath = yield* downloadIso(debianImageURL, options); + } else { + isoPath = basename(debianImageURL); + } + } } const config = yield* pipe( diff --git a/src/constants.ts b/src/constants.ts index 2f03947..1d391aa 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -6,21 +6,27 @@ export const CONFIG_FILE_NAME: string = "vmconfig.toml"; export const IMAGE_DIR: string = `${CONFIG_DIR}/images`; export const VOLUME_DIR: string = `${CONFIG_DIR}/volumes`; -export const UBUNTU_ISO_URL: string = Deno.build.arch === "aarch64" - ? "https://cdimage.ubuntu.com/releases/24.04/release/ubuntu-24.04.3-live-server-arm64.iso" - : "https://releases.ubuntu.com/24.04.3/ubuntu-24.04.3-live-server-amd64.iso"; +export const UBUNTU_ISO_URL: string = + Deno.build.arch === "aarch64" + ? "https://cdimage.ubuntu.com/releases/24.04/release/ubuntu-24.04.3-live-server-arm64.iso" + : "https://releases.ubuntu.com/24.04.3/ubuntu-24.04.3-live-server-amd64.iso"; export const FEDORA_COREOS_DEFAULT_VERSION: string = "43.20251024.3.0"; -export const FEDORA_COREOS_IMG_URL: string = - `https://builds.coreos.fedoraproject.org/prod/streams/stable/builds/${FEDORA_COREOS_DEFAULT_VERSION}/${Deno.build.arch}/fedora-coreos-${FEDORA_COREOS_DEFAULT_VERSION}-qemu.${Deno.build.arch}.qcow2.xz`; +export const FEDORA_COREOS_IMG_URL: string = `https://builds.coreos.fedoraproject.org/prod/streams/stable/builds/${FEDORA_COREOS_DEFAULT_VERSION}/${Deno.build.arch}/fedora-coreos-${FEDORA_COREOS_DEFAULT_VERSION}-qemu.${Deno.build.arch}.qcow2.xz`; export const NIXOS_DEFAULT_VERSION: string = "25.05"; -export const NIXOS_ISO_URL: string = - `https://channels.nixos.org/nixos-${NIXOS_DEFAULT_VERSION}/latest-nixos-minimal-${Deno.build.arch}-linux.iso`; +export const NIXOS_ISO_URL: string = `https://channels.nixos.org/nixos-${NIXOS_DEFAULT_VERSION}/latest-nixos-minimal-${Deno.build.arch}-linux.iso`; -export const FEDORA_IMG_URL: string = - `https://download.fedoraproject.org/pub/fedora/linux/releases/43/Server/${Deno.build.arch}/images/Fedora-Server-Guest-Generic-43-1.6.${Deno.build.arch}.qcow2`; +export const FEDORA_IMG_URL: string = `https://download.fedoraproject.org/pub/fedora/linux/releases/43/Server/${Deno.build.arch}/images/Fedora-Server-Guest-Generic-43-1.6.${Deno.build.arch}.qcow2`; -export const GENTOO_IMG_URL: string = Deno.build.arch === "aarch64" - ? "https://distfiles.gentoo.org/releases/arm64/autobuilds/20251116T233105Z/di-arm64-console-20251116T233105Z.qcow2" - : "https://distfiles.gentoo.org/releases/amd64/autobuilds/20251116T161545Z/di-amd64-console-20251116T161545Z.qcow2"; +export const GENTOO_IMG_URL: string = + Deno.build.arch === "aarch64" + ? "https://distfiles.gentoo.org/releases/arm64/autobuilds/20251116T233105Z/di-arm64-console-20251116T233105Z.qcow2" + : "https://distfiles.gentoo.org/releases/amd64/autobuilds/20251116T161545Z/di-amd64-console-20251116T161545Z.qcow2"; + +export const DEBIAN_DEFAULT_VERSION: string = "13.2.0"; + +export const DEBIAN_ISO_URL: string = + Deno.build.arch === "aarch64" + ? `https://cdimage.debian.org/debian-cd/current/arm64/iso-cd/debian-${DEBIAN_DEFAULT_VERSION}-arm64-netinst.iso` + : `https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-${DEBIAN_DEFAULT_VERSION}-amd64-netinst.iso`; diff --git a/src/utils.ts b/src/utils.ts index a393534..5813067 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,6 +5,8 @@ import chalk from "chalk"; import { Data, Effect, pipe } from "effect"; import Moniker from "moniker"; import { + DEBIAN_DEFAULT_VERSION, + DEBIAN_ISO_URL, EMPTY_DISK_THRESHOLD_KB, FEDORA_COREOS_DEFAULT_VERSION, FEDORA_COREOS_IMG_URL, @@ -66,7 +68,7 @@ export const getCurrentArch = (): string => { export const isValidISOurl = (url?: string): boolean => { return Boolean( (url?.startsWith("http://") || url?.startsWith("https://")) && - url?.endsWith(".iso"), + url?.endsWith(".iso") ); }; @@ -92,7 +94,7 @@ export const humanFileSize = (blocks: number) => }); export const validateImage = ( - image: string, + 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._-]+)?$/; @@ -103,7 +105,7 @@ export const validateImage = ( image, cause: "Image name does not conform to expected format. Should be in the format 'repository/name:tag'.", - }), + }) ); } return Effect.succeed(image); @@ -112,18 +114,18 @@ export const validateImage = ( export const extractTag = (name: string) => pipe( validateImage(name), - Effect.flatMap((image) => Effect.succeed(image.split(":")[1] || "latest")), + Effect.flatMap((image) => Effect.succeed(image.split(":")[1] || "latest")) ); export const failOnMissingImage = ( - image: Image | undefined, + image: Image | undefined ): Effect.Effect => image ? Effect.succeed(image) : Effect.fail(new NoSuchImageError({ cause: "No such image" })); export const du = ( - path: string, + path: string ): Effect.Effect => Effect.tryPromise({ try: async () => { @@ -155,7 +157,7 @@ export const emptyDiskImage = (path: string) => exists ? Effect.succeed(true) : du(path).pipe(Effect.map((size) => size < EMPTY_DISK_THRESHOLD_KB)) - ), + ) ); export const downloadIso = (url: string, options: Options) => @@ -177,8 +179,8 @@ export const downloadIso = (url: string, options: Options) => 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.`, - ), + `Drive image ${options.image} is not empty (size: ${driveSize} KB), skipping ISO download to avoid overwriting existing data.` + ) ); return null; } @@ -196,8 +198,8 @@ export const downloadIso = (url: string, options: Options) => if (outputExists) { console.log( chalk.yellowBright( - `File ${outputPath} already exists, skipping download.`, - ), + `File ${outputPath} already exists, skipping download.` + ) ); return outputPath; } @@ -246,8 +248,8 @@ export const setupFirmwareFilesIfNeeded = () => if (!success) { console.error( chalk.redBright( - "Failed to get QEMU prefix from Homebrew. Ensure QEMU is installed via Homebrew.", - ), + "Failed to get QEMU prefix from Homebrew. Ensure QEMU is installed via Homebrew." + ) ); Deno.exit(1); } @@ -260,7 +262,7 @@ export const setupFirmwareFilesIfNeeded = () => try: () => Deno.copyFile( `${brewPrefix}/share/qemu/edk2-arm-vars.fd`, - edk2VarsAarch64, + edk2VarsAarch64 ), catch: (error) => new LogCommandError({ cause: error }), }); @@ -305,13 +307,13 @@ export const setupCoreOSArgs = (imagePath?: string | null) => const configOK = yield* pipe( fileExists("config.ign"), Effect.flatMap(() => Effect.succeed(true)), - Effect.catchAll(() => Effect.succeed(false)), + Effect.catchAll(() => Effect.succeed(false)) ); if (!configOK) { console.error( chalk.redBright( - "CoreOS image requires a config.ign file in the current directory.", - ), + "CoreOS image requires a config.ign file in the current directory." + ) ); Deno.exit(1); } @@ -346,7 +348,7 @@ export const setupGentooArgs = (imagePath?: string | null) => imagePath && imagePath.endsWith(".qcow2") && imagePath.startsWith( - `di-${Deno.build.arch === "aarch64" ? "arm64" : "amd64"}-console-`, + `di-${Deno.build.arch === "aarch64" ? "arm64" : "amd64"}-console-` ) ) { return ["-drive", `file=${imagePath},format=qcow2,if=virtio`]; @@ -359,9 +361,10 @@ 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 qemu = + Deno.build.arch === "aarch64" + ? "qemu-system-aarch64" + : "qemu-system-x86_64"; const firmwareFiles = yield* setupFirmwareFilesIfNeeded(); let coreosArgs: string[] = yield* setupCoreOSArgs(isoPath || options.image); @@ -413,7 +416,7 @@ export const runQemu = (isoPath: string | null, options: Options) => options.image && [ "-drive", `file=${options.image},format=${options.diskFormat},if=virtio`, - ], + ] ), ]; @@ -428,11 +431,9 @@ export const runQemu = (isoPath: string | null, options: Options) => const logPath = `${LOGS_DIR}/${name}.log`; const fullCommand = options.bridge - ? `sudo ${qemu} ${ - qemuArgs + ? `sudo ${qemu} ${qemuArgs .slice(1) - .join(" ") - } >> "${logPath}" 2>&1 & echo $!` + .join(" ")} >> "${logPath}" 2>&1 & echo $!` : `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`; const { stdout } = yield* Effect.tryPromise({ @@ -458,7 +459,8 @@ export const runQemu = (isoPath: string | null, options: Options) => cpus: options.cpus, cpu: options.cpu, diskSize: options.size || "20G", - diskFormat: (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || + diskFormat: + (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || options.diskFormat || "raw", portForward: options.portForward, @@ -477,7 +479,7 @@ export const runQemu = (isoPath: string | null, options: Options) => }); console.log( - `Virtual machine ${name} started in background (PID: ${qemuPid})`, + `Virtual machine ${name} started in background (PID: ${qemuPid})` ); console.log(`Logs will be written to: ${logPath}`); @@ -500,7 +502,8 @@ export const runQemu = (isoPath: string | null, options: Options) => cpus: options.cpus, cpu: options.cpu, diskSize: options.size || "20G", - diskFormat: (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || + diskFormat: + (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || options.diskFormat || "raw", portForward: options.portForward, @@ -608,8 +611,8 @@ export const createDriveImageIfNeeded = ({ if (pathExists) { console.log( chalk.yellowBright( - `Drive image ${path} already exists, skipping creation.`, - ), + `Drive image ${path} already exists, skipping creation.` + ) ); return; } @@ -636,7 +639,7 @@ export const createDriveImageIfNeeded = ({ }); export const fileExists = ( - path: string, + path: string ): Effect.Effect => Effect.try({ try: () => Deno.statSync(path), @@ -644,7 +647,7 @@ export const fileExists = ( }); export const constructCoreOSImageURL = ( - image: string, + 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+))?$/; @@ -652,7 +655,7 @@ export const constructCoreOSImageURL = ( if (match) { const version = match[3] || FEDORA_COREOS_DEFAULT_VERSION; return Effect.succeed( - FEDORA_COREOS_IMG_URL.replaceAll(FEDORA_COREOS_DEFAULT_VERSION, version), + FEDORA_COREOS_IMG_URL.replaceAll(FEDORA_COREOS_DEFAULT_VERSION, version) ); } @@ -660,7 +663,7 @@ export const constructCoreOSImageURL = ( new InvalidImageNameError({ image, cause: "Image name does not match CoreOS naming conventions.", - }), + }) ); }; @@ -689,7 +692,7 @@ export const extractXz = (path: string | null) => }); export const constructNixOSImageURL = ( - image: string, + image: string ): Effect.Effect => { // detect with regex if image matches NixOS pattern: nixos or nixos- const nixosRegex = /^(nixos)(-(\d+\.\d+))?$/; @@ -697,7 +700,7 @@ export const constructNixOSImageURL = ( if (match) { const version = match[3] || NIXOS_DEFAULT_VERSION; return Effect.succeed( - NIXOS_ISO_URL.replaceAll(NIXOS_DEFAULT_VERSION, version), + NIXOS_ISO_URL.replaceAll(NIXOS_DEFAULT_VERSION, version) ); } @@ -705,12 +708,12 @@ export const constructNixOSImageURL = ( new InvalidImageNameError({ image, cause: "Image name does not match NixOS naming conventions.", - }), + }) ); }; export const constructFedoraImageURL = ( - image: string, + image: string ): Effect.Effect => { // detect with regex if image matches Fedora pattern: fedora const fedoraRegex = /^(fedora)$/; @@ -723,12 +726,12 @@ export const constructFedoraImageURL = ( new InvalidImageNameError({ image, cause: "Image name does not match Fedora naming conventions.", - }), + }) ); }; export const constructGentooImageURL = ( - image: string, + 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))?$/; @@ -737,8 +740,8 @@ export const constructGentooImageURL = ( return Effect.succeed( GENTOO_IMG_URL.replaceAll("20251116T161545Z", match[3]).replaceAll( "20251116T233105Z", - match[3], - ), + match[3] + ) ); } @@ -750,6 +753,30 @@ export const constructGentooImageURL = ( new InvalidImageNameError({ image, cause: "Image name does not match Gentoo naming conventions.", - }), + }) + ); +}; + +export const constructDebianImageURL = ( + image: string +): Effect.Effect => { + // 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.", + }) ); }; diff --git a/src/utils_test.ts b/src/utils_test.ts index 3e30260..0b41037 100644 --- a/src/utils_test.ts +++ b/src/utils_test.ts @@ -7,6 +7,7 @@ import { } from "./constants.ts"; import { constructCoreOSImageURL, + constructDebianImageURL, constructGentooImageURL, constructNixOSImageURL, } from "./utils.ts"; @@ -15,8 +16,8 @@ Deno.test("Test Default Fedora CoreOS Image URL", () => { const url = Effect.runSync( pipe( constructCoreOSImageURL("fedora-coreos"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)), - ), + Effect.catchAll((_error) => Effect.succeed(null as string | null)) + ) ); assertEquals(url, FEDORA_COREOS_IMG_URL); @@ -26,8 +27,8 @@ Deno.test("Test Default Fedora CoreOS Image URL", () => { const url = Effect.runSync( pipe( constructCoreOSImageURL("coreos"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)), - ), + Effect.catchAll((_error) => Effect.succeed(null as string | null)) + ) ); assertEquals(url, FEDORA_COREOS_IMG_URL); @@ -37,14 +38,14 @@ Deno.test("Test Specific Fedora CoreOS Version", () => { const url = Effect.runSync( pipe( constructCoreOSImageURL("fedora-coreos-43.20251024.2.0"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)), - ), + Effect.catchAll((_error) => Effect.succeed(null as string | null)) + ) ); assertEquals( url, "https://builds.coreos.fedoraproject.org/prod/streams/stable/builds/43.20251024.2.0/" + - `${Deno.build.arch}/fedora-coreos-43.20251024.2.0-qemu.${Deno.build.arch}.qcow2.xz`, + `${Deno.build.arch}/fedora-coreos-43.20251024.2.0-qemu.${Deno.build.arch}.qcow2.xz` ); }); @@ -52,8 +53,8 @@ Deno.test("Test invalid Fedora CoreOS Image Name", () => { const url = Effect.runSync( pipe( constructCoreOSImageURL("fedora-coreos-latest"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)), - ), + Effect.catchAll((_error) => Effect.succeed(null as string | null)) + ) ); assertEquals(url, null); @@ -63,8 +64,8 @@ Deno.test("Test Default NixOS Image URL", () => { const url = Effect.runSync( pipe( constructNixOSImageURL("nixos"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)), - ), + Effect.catchAll((_error) => Effect.succeed(null as string | null)) + ) ); assertEquals(url, NIXOS_ISO_URL); @@ -74,13 +75,13 @@ Deno.test("Test Specific NixOS Version", () => { const url = Effect.runSync( pipe( constructNixOSImageURL("nixos-24.05"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)), - ), + Effect.catchAll((_error) => Effect.succeed(null as string | null)) + ) ); assertEquals( url, - `https://channels.nixos.org/nixos-24.05/latest-nixos-minimal-${Deno.build.arch}-linux.iso`, + `https://channels.nixos.org/nixos-24.05/latest-nixos-minimal-${Deno.build.arch}-linux.iso` ); }); @@ -88,8 +89,8 @@ Deno.test("Test invalid NixOS Image Name", () => { const url = Effect.runSync( pipe( constructNixOSImageURL("nixos-latest"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)), - ), + Effect.catchAll((_error) => Effect.succeed(null as string | null)) + ) ); assertEquals(url, null); @@ -99,14 +100,14 @@ Deno.test("Test valid Gentoo Image Name", () => { const url = Effect.runSync( pipe( constructGentooImageURL("gentoo-20251116T161545Z"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)), - ), + Effect.catchAll((_error) => Effect.succeed(null as string | null)) + ) ); const arch = Deno.build.arch === "aarch64" ? "arm64" : "amd64"; assertEquals( url, - `https://distfiles.gentoo.org/releases/${arch}/autobuilds/20251116T161545Z/di-${arch}-console-20251116T161545Z.qcow2`, + `https://distfiles.gentoo.org/releases/${arch}/autobuilds/20251116T161545Z/di-${arch}-console-20251116T161545Z.qcow2` ); }); @@ -114,8 +115,8 @@ Deno.test("Test valid Gentoo Image Name", () => { const url = Effect.runSync( pipe( constructGentooImageURL("gentoo"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)), - ), + Effect.catchAll((_error) => Effect.succeed(null as string | null)) + ) ); assertEquals(url, GENTOO_IMG_URL); @@ -125,8 +126,49 @@ Deno.test("Test invalid Gentoo Image Name", () => { const url = Effect.runSync( pipe( constructGentooImageURL("gentoo-latest"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)), - ), + Effect.catchAll((_error) => Effect.succeed(null as string | null)) + ) + ); + + assertEquals(url, null); +}); + +Deno.test("Test valid Debian Image Name", () => { + const url = Effect.runSync( + pipe( + constructDebianImageURL("debian-13.2.0"), + Effect.catchAll((_error) => Effect.succeed(null as string | null)) + ) + ); + + const arch = Deno.build.arch === "aarch64" ? "arm64" : "amd64"; + assertEquals( + url, + `https://cdimage.debian.org/debian-cd/current/${arch}/iso-cd/debian-13.2.0-${arch}-netinst.iso` + ); +}); + +Deno.test("Test valid Debian Image Name", () => { + const url = Effect.runSync( + pipe( + constructDebianImageURL("debian"), + Effect.catchAll((_error) => Effect.succeed(null as string | null)) + ) + ); + + const arch = Deno.build.arch === "aarch64" ? "arm64" : "amd64"; + assertEquals( + url, + `https://cdimage.debian.org/debian-cd/current/${arch}/iso-cd/debian-13.2.0-${arch}-netinst.iso` + ); +}); + +Deno.test("Test invalid Debian Image Name", () => { + const url = Effect.runSync( + pipe( + constructDebianImageURL("debian-latest"), + Effect.catchAll((_error) => Effect.succeed(null as string | null)) + ) ); assertEquals(url, null); -- 2.51.2 From 2f0f2f3635455e51b73e6ff1efe8d67c92d788ec Mon Sep 17 00:00:00 2001 From: Tsiry Sandratraina Date: Wed, 19 Nov 2025 12:34:48 +0300 Subject: [PATCH 2/3] run format --- src/constants.ts | 30 +++++++-------- src/utils.ts | 95 +++++++++++++++++++++++------------------------ src/utils_test.ts | 62 +++++++++++++++---------------- 3 files changed, 93 insertions(+), 94 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 1d391aa..2220673 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -6,27 +6,27 @@ export const CONFIG_FILE_NAME: string = "vmconfig.toml"; export const IMAGE_DIR: string = `${CONFIG_DIR}/images`; export const VOLUME_DIR: string = `${CONFIG_DIR}/volumes`; -export const UBUNTU_ISO_URL: string = - Deno.build.arch === "aarch64" - ? "https://cdimage.ubuntu.com/releases/24.04/release/ubuntu-24.04.3-live-server-arm64.iso" - : "https://releases.ubuntu.com/24.04.3/ubuntu-24.04.3-live-server-amd64.iso"; +export const UBUNTU_ISO_URL: string = Deno.build.arch === "aarch64" + ? "https://cdimage.ubuntu.com/releases/24.04/release/ubuntu-24.04.3-live-server-arm64.iso" + : "https://releases.ubuntu.com/24.04.3/ubuntu-24.04.3-live-server-amd64.iso"; export const FEDORA_COREOS_DEFAULT_VERSION: string = "43.20251024.3.0"; -export const FEDORA_COREOS_IMG_URL: string = `https://builds.coreos.fedoraproject.org/prod/streams/stable/builds/${FEDORA_COREOS_DEFAULT_VERSION}/${Deno.build.arch}/fedora-coreos-${FEDORA_COREOS_DEFAULT_VERSION}-qemu.${Deno.build.arch}.qcow2.xz`; +export const FEDORA_COREOS_IMG_URL: string = + `https://builds.coreos.fedoraproject.org/prod/streams/stable/builds/${FEDORA_COREOS_DEFAULT_VERSION}/${Deno.build.arch}/fedora-coreos-${FEDORA_COREOS_DEFAULT_VERSION}-qemu.${Deno.build.arch}.qcow2.xz`; export const NIXOS_DEFAULT_VERSION: string = "25.05"; -export const NIXOS_ISO_URL: string = `https://channels.nixos.org/nixos-${NIXOS_DEFAULT_VERSION}/latest-nixos-minimal-${Deno.build.arch}-linux.iso`; +export const NIXOS_ISO_URL: string = + `https://channels.nixos.org/nixos-${NIXOS_DEFAULT_VERSION}/latest-nixos-minimal-${Deno.build.arch}-linux.iso`; -export const FEDORA_IMG_URL: string = `https://download.fedoraproject.org/pub/fedora/linux/releases/43/Server/${Deno.build.arch}/images/Fedora-Server-Guest-Generic-43-1.6.${Deno.build.arch}.qcow2`; +export const FEDORA_IMG_URL: string = + `https://download.fedoraproject.org/pub/fedora/linux/releases/43/Server/${Deno.build.arch}/images/Fedora-Server-Guest-Generic-43-1.6.${Deno.build.arch}.qcow2`; -export const GENTOO_IMG_URL: string = - Deno.build.arch === "aarch64" - ? "https://distfiles.gentoo.org/releases/arm64/autobuilds/20251116T233105Z/di-arm64-console-20251116T233105Z.qcow2" - : "https://distfiles.gentoo.org/releases/amd64/autobuilds/20251116T161545Z/di-amd64-console-20251116T161545Z.qcow2"; +export const GENTOO_IMG_URL: string = Deno.build.arch === "aarch64" + ? "https://distfiles.gentoo.org/releases/arm64/autobuilds/20251116T233105Z/di-arm64-console-20251116T233105Z.qcow2" + : "https://distfiles.gentoo.org/releases/amd64/autobuilds/20251116T161545Z/di-amd64-console-20251116T161545Z.qcow2"; export const DEBIAN_DEFAULT_VERSION: string = "13.2.0"; -export const DEBIAN_ISO_URL: string = - Deno.build.arch === "aarch64" - ? `https://cdimage.debian.org/debian-cd/current/arm64/iso-cd/debian-${DEBIAN_DEFAULT_VERSION}-arm64-netinst.iso` - : `https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-${DEBIAN_DEFAULT_VERSION}-amd64-netinst.iso`; +export const DEBIAN_ISO_URL: string = Deno.build.arch === "aarch64" + ? `https://cdimage.debian.org/debian-cd/current/arm64/iso-cd/debian-${DEBIAN_DEFAULT_VERSION}-arm64-netinst.iso` + : `https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-${DEBIAN_DEFAULT_VERSION}-amd64-netinst.iso`; diff --git a/src/utils.ts b/src/utils.ts index 5813067..a09e7c0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -68,7 +68,7 @@ export const getCurrentArch = (): string => { export const isValidISOurl = (url?: string): boolean => { return Boolean( (url?.startsWith("http://") || url?.startsWith("https://")) && - url?.endsWith(".iso") + url?.endsWith(".iso"), ); }; @@ -94,7 +94,7 @@ export const humanFileSize = (blocks: number) => }); export const validateImage = ( - image: string + 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._-]+)?$/; @@ -105,7 +105,7 @@ export const validateImage = ( image, cause: "Image name does not conform to expected format. Should be in the format 'repository/name:tag'.", - }) + }), ); } return Effect.succeed(image); @@ -114,18 +114,18 @@ export const validateImage = ( export const extractTag = (name: string) => pipe( validateImage(name), - Effect.flatMap((image) => Effect.succeed(image.split(":")[1] || "latest")) + Effect.flatMap((image) => Effect.succeed(image.split(":")[1] || "latest")), ); export const failOnMissingImage = ( - image: Image | undefined + image: Image | undefined, ): Effect.Effect => image ? Effect.succeed(image) : Effect.fail(new NoSuchImageError({ cause: "No such image" })); export const du = ( - path: string + path: string, ): Effect.Effect => Effect.tryPromise({ try: async () => { @@ -157,7 +157,7 @@ export const emptyDiskImage = (path: string) => exists ? Effect.succeed(true) : du(path).pipe(Effect.map((size) => size < EMPTY_DISK_THRESHOLD_KB)) - ) + ), ); export const downloadIso = (url: string, options: Options) => @@ -179,8 +179,8 @@ export const downloadIso = (url: string, options: Options) => 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.` - ) + `Drive image ${options.image} is not empty (size: ${driveSize} KB), skipping ISO download to avoid overwriting existing data.`, + ), ); return null; } @@ -198,8 +198,8 @@ export const downloadIso = (url: string, options: Options) => if (outputExists) { console.log( chalk.yellowBright( - `File ${outputPath} already exists, skipping download.` - ) + `File ${outputPath} already exists, skipping download.`, + ), ); return outputPath; } @@ -248,8 +248,8 @@ export const setupFirmwareFilesIfNeeded = () => if (!success) { console.error( chalk.redBright( - "Failed to get QEMU prefix from Homebrew. Ensure QEMU is installed via Homebrew." - ) + "Failed to get QEMU prefix from Homebrew. Ensure QEMU is installed via Homebrew.", + ), ); Deno.exit(1); } @@ -262,7 +262,7 @@ export const setupFirmwareFilesIfNeeded = () => try: () => Deno.copyFile( `${brewPrefix}/share/qemu/edk2-arm-vars.fd`, - edk2VarsAarch64 + edk2VarsAarch64, ), catch: (error) => new LogCommandError({ cause: error }), }); @@ -307,13 +307,13 @@ export const setupCoreOSArgs = (imagePath?: string | null) => const configOK = yield* pipe( fileExists("config.ign"), Effect.flatMap(() => Effect.succeed(true)), - Effect.catchAll(() => Effect.succeed(false)) + Effect.catchAll(() => Effect.succeed(false)), ); if (!configOK) { console.error( chalk.redBright( - "CoreOS image requires a config.ign file in the current directory." - ) + "CoreOS image requires a config.ign file in the current directory.", + ), ); Deno.exit(1); } @@ -348,7 +348,7 @@ export const setupGentooArgs = (imagePath?: string | null) => imagePath && imagePath.endsWith(".qcow2") && imagePath.startsWith( - `di-${Deno.build.arch === "aarch64" ? "arm64" : "amd64"}-console-` + `di-${Deno.build.arch === "aarch64" ? "arm64" : "amd64"}-console-`, ) ) { return ["-drive", `file=${imagePath},format=qcow2,if=virtio`]; @@ -361,10 +361,9 @@ 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 qemu = Deno.build.arch === "aarch64" + ? "qemu-system-aarch64" + : "qemu-system-x86_64"; const firmwareFiles = yield* setupFirmwareFilesIfNeeded(); let coreosArgs: string[] = yield* setupCoreOSArgs(isoPath || options.image); @@ -416,7 +415,7 @@ export const runQemu = (isoPath: string | null, options: Options) => options.image && [ "-drive", `file=${options.image},format=${options.diskFormat},if=virtio`, - ] + ], ), ]; @@ -431,9 +430,11 @@ export const runQemu = (isoPath: string | null, options: Options) => const logPath = `${LOGS_DIR}/${name}.log`; const fullCommand = options.bridge - ? `sudo ${qemu} ${qemuArgs + ? `sudo ${qemu} ${ + qemuArgs .slice(1) - .join(" ")} >> "${logPath}" 2>&1 & echo $!` + .join(" ") + } >> "${logPath}" 2>&1 & echo $!` : `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`; const { stdout } = yield* Effect.tryPromise({ @@ -459,8 +460,7 @@ export const runQemu = (isoPath: string | null, options: Options) => cpus: options.cpus, cpu: options.cpu, diskSize: options.size || "20G", - diskFormat: - (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || + diskFormat: (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || options.diskFormat || "raw", portForward: options.portForward, @@ -479,7 +479,7 @@ export const runQemu = (isoPath: string | null, options: Options) => }); console.log( - `Virtual machine ${name} started in background (PID: ${qemuPid})` + `Virtual machine ${name} started in background (PID: ${qemuPid})`, ); console.log(`Logs will be written to: ${logPath}`); @@ -502,8 +502,7 @@ export const runQemu = (isoPath: string | null, options: Options) => cpus: options.cpus, cpu: options.cpu, diskSize: options.size || "20G", - diskFormat: - (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || + diskFormat: (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || options.diskFormat || "raw", portForward: options.portForward, @@ -611,8 +610,8 @@ export const createDriveImageIfNeeded = ({ if (pathExists) { console.log( chalk.yellowBright( - `Drive image ${path} already exists, skipping creation.` - ) + `Drive image ${path} already exists, skipping creation.`, + ), ); return; } @@ -639,7 +638,7 @@ export const createDriveImageIfNeeded = ({ }); export const fileExists = ( - path: string + path: string, ): Effect.Effect => Effect.try({ try: () => Deno.statSync(path), @@ -647,7 +646,7 @@ export const fileExists = ( }); export const constructCoreOSImageURL = ( - image: string + 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+))?$/; @@ -655,7 +654,7 @@ export const constructCoreOSImageURL = ( if (match) { const version = match[3] || FEDORA_COREOS_DEFAULT_VERSION; return Effect.succeed( - FEDORA_COREOS_IMG_URL.replaceAll(FEDORA_COREOS_DEFAULT_VERSION, version) + FEDORA_COREOS_IMG_URL.replaceAll(FEDORA_COREOS_DEFAULT_VERSION, version), ); } @@ -663,7 +662,7 @@ export const constructCoreOSImageURL = ( new InvalidImageNameError({ image, cause: "Image name does not match CoreOS naming conventions.", - }) + }), ); }; @@ -692,7 +691,7 @@ export const extractXz = (path: string | null) => }); export const constructNixOSImageURL = ( - image: string + image: string, ): Effect.Effect => { // detect with regex if image matches NixOS pattern: nixos or nixos- const nixosRegex = /^(nixos)(-(\d+\.\d+))?$/; @@ -700,7 +699,7 @@ export const constructNixOSImageURL = ( if (match) { const version = match[3] || NIXOS_DEFAULT_VERSION; return Effect.succeed( - NIXOS_ISO_URL.replaceAll(NIXOS_DEFAULT_VERSION, version) + NIXOS_ISO_URL.replaceAll(NIXOS_DEFAULT_VERSION, version), ); } @@ -708,12 +707,12 @@ export const constructNixOSImageURL = ( new InvalidImageNameError({ image, cause: "Image name does not match NixOS naming conventions.", - }) + }), ); }; export const constructFedoraImageURL = ( - image: string + image: string, ): Effect.Effect => { // detect with regex if image matches Fedora pattern: fedora const fedoraRegex = /^(fedora)$/; @@ -726,12 +725,12 @@ export const constructFedoraImageURL = ( new InvalidImageNameError({ image, cause: "Image name does not match Fedora naming conventions.", - }) + }), ); }; export const constructGentooImageURL = ( - image: string + 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))?$/; @@ -740,8 +739,8 @@ export const constructGentooImageURL = ( return Effect.succeed( GENTOO_IMG_URL.replaceAll("20251116T161545Z", match[3]).replaceAll( "20251116T233105Z", - match[3] - ) + match[3], + ), ); } @@ -753,19 +752,19 @@ export const constructGentooImageURL = ( new InvalidImageNameError({ image, cause: "Image name does not match Gentoo naming conventions.", - }) + }), ); }; export const constructDebianImageURL = ( - image: string + image: string, ): Effect.Effect => { // 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]) + DEBIAN_ISO_URL.replaceAll(DEBIAN_DEFAULT_VERSION, match[3]), ); } @@ -777,6 +776,6 @@ export const constructDebianImageURL = ( new InvalidImageNameError({ image, cause: "Image name does not match Debian naming conventions.", - }) + }), ); }; diff --git a/src/utils_test.ts b/src/utils_test.ts index 0b41037..f2e5afe 100644 --- a/src/utils_test.ts +++ b/src/utils_test.ts @@ -16,8 +16,8 @@ Deno.test("Test Default Fedora CoreOS Image URL", () => { const url = Effect.runSync( pipe( constructCoreOSImageURL("fedora-coreos"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)) - ) + Effect.catchAll((_error) => Effect.succeed(null as string | null)), + ), ); assertEquals(url, FEDORA_COREOS_IMG_URL); @@ -27,8 +27,8 @@ Deno.test("Test Default Fedora CoreOS Image URL", () => { const url = Effect.runSync( pipe( constructCoreOSImageURL("coreos"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)) - ) + Effect.catchAll((_error) => Effect.succeed(null as string | null)), + ), ); assertEquals(url, FEDORA_COREOS_IMG_URL); @@ -38,14 +38,14 @@ Deno.test("Test Specific Fedora CoreOS Version", () => { const url = Effect.runSync( pipe( constructCoreOSImageURL("fedora-coreos-43.20251024.2.0"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)) - ) + Effect.catchAll((_error) => Effect.succeed(null as string | null)), + ), ); assertEquals( url, "https://builds.coreos.fedoraproject.org/prod/streams/stable/builds/43.20251024.2.0/" + - `${Deno.build.arch}/fedora-coreos-43.20251024.2.0-qemu.${Deno.build.arch}.qcow2.xz` + `${Deno.build.arch}/fedora-coreos-43.20251024.2.0-qemu.${Deno.build.arch}.qcow2.xz`, ); }); @@ -53,8 +53,8 @@ Deno.test("Test invalid Fedora CoreOS Image Name", () => { const url = Effect.runSync( pipe( constructCoreOSImageURL("fedora-coreos-latest"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)) - ) + Effect.catchAll((_error) => Effect.succeed(null as string | null)), + ), ); assertEquals(url, null); @@ -64,8 +64,8 @@ Deno.test("Test Default NixOS Image URL", () => { const url = Effect.runSync( pipe( constructNixOSImageURL("nixos"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)) - ) + Effect.catchAll((_error) => Effect.succeed(null as string | null)), + ), ); assertEquals(url, NIXOS_ISO_URL); @@ -75,13 +75,13 @@ Deno.test("Test Specific NixOS Version", () => { const url = Effect.runSync( pipe( constructNixOSImageURL("nixos-24.05"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)) - ) + Effect.catchAll((_error) => Effect.succeed(null as string | null)), + ), ); assertEquals( url, - `https://channels.nixos.org/nixos-24.05/latest-nixos-minimal-${Deno.build.arch}-linux.iso` + `https://channels.nixos.org/nixos-24.05/latest-nixos-minimal-${Deno.build.arch}-linux.iso`, ); }); @@ -89,8 +89,8 @@ Deno.test("Test invalid NixOS Image Name", () => { const url = Effect.runSync( pipe( constructNixOSImageURL("nixos-latest"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)) - ) + Effect.catchAll((_error) => Effect.succeed(null as string | null)), + ), ); assertEquals(url, null); @@ -100,14 +100,14 @@ Deno.test("Test valid Gentoo Image Name", () => { const url = Effect.runSync( pipe( constructGentooImageURL("gentoo-20251116T161545Z"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)) - ) + Effect.catchAll((_error) => Effect.succeed(null as string | null)), + ), ); const arch = Deno.build.arch === "aarch64" ? "arm64" : "amd64"; assertEquals( url, - `https://distfiles.gentoo.org/releases/${arch}/autobuilds/20251116T161545Z/di-${arch}-console-20251116T161545Z.qcow2` + `https://distfiles.gentoo.org/releases/${arch}/autobuilds/20251116T161545Z/di-${arch}-console-20251116T161545Z.qcow2`, ); }); @@ -115,8 +115,8 @@ Deno.test("Test valid Gentoo Image Name", () => { const url = Effect.runSync( pipe( constructGentooImageURL("gentoo"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)) - ) + Effect.catchAll((_error) => Effect.succeed(null as string | null)), + ), ); assertEquals(url, GENTOO_IMG_URL); @@ -126,8 +126,8 @@ Deno.test("Test invalid Gentoo Image Name", () => { const url = Effect.runSync( pipe( constructGentooImageURL("gentoo-latest"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)) - ) + Effect.catchAll((_error) => Effect.succeed(null as string | null)), + ), ); assertEquals(url, null); @@ -137,14 +137,14 @@ Deno.test("Test valid Debian Image Name", () => { const url = Effect.runSync( pipe( constructDebianImageURL("debian-13.2.0"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)) - ) + Effect.catchAll((_error) => Effect.succeed(null as string | null)), + ), ); const arch = Deno.build.arch === "aarch64" ? "arm64" : "amd64"; assertEquals( url, - `https://cdimage.debian.org/debian-cd/current/${arch}/iso-cd/debian-13.2.0-${arch}-netinst.iso` + `https://cdimage.debian.org/debian-cd/current/${arch}/iso-cd/debian-13.2.0-${arch}-netinst.iso`, ); }); @@ -152,14 +152,14 @@ Deno.test("Test valid Debian Image Name", () => { const url = Effect.runSync( pipe( constructDebianImageURL("debian"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)) - ) + Effect.catchAll((_error) => Effect.succeed(null as string | null)), + ), ); const arch = Deno.build.arch === "aarch64" ? "arm64" : "amd64"; assertEquals( url, - `https://cdimage.debian.org/debian-cd/current/${arch}/iso-cd/debian-13.2.0-${arch}-netinst.iso` + `https://cdimage.debian.org/debian-cd/current/${arch}/iso-cd/debian-13.2.0-${arch}-netinst.iso`, ); }); @@ -167,8 +167,8 @@ Deno.test("Test invalid Debian Image Name", () => { const url = Effect.runSync( pipe( constructDebianImageURL("debian-latest"), - Effect.catchAll((_error) => Effect.succeed(null as string | null)) - ) + Effect.catchAll((_error) => Effect.succeed(null as string | null)), + ), ); assertEquals(url, null); -- 2.51.2 From 6307f3ea8335ab5d70afa4a72f0afa0bc7afe7b8 Mon Sep 17 00:00:00 2001 From: Tsiry Sandratraina Date: Wed, 19 Nov 2025 12:59:11 +0300 Subject: [PATCH 3/3] feat: update Debian image URL test to use constant --- src/utils_test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/utils_test.ts b/src/utils_test.ts index f2e5afe..ace09cf 100644 --- a/src/utils_test.ts +++ b/src/utils_test.ts @@ -1,6 +1,7 @@ import { assertEquals } from "@std/assert"; import { Effect, pipe } from "effect"; import { + DEBIAN_ISO_URL, FEDORA_COREOS_IMG_URL, GENTOO_IMG_URL, NIXOS_ISO_URL, @@ -157,10 +158,7 @@ Deno.test("Test valid Debian Image Name", () => { ); const arch = Deno.build.arch === "aarch64" ? "arm64" : "amd64"; - assertEquals( - url, - `https://cdimage.debian.org/debian-cd/current/${arch}/iso-cd/debian-13.2.0-${arch}-netinst.iso`, - ); + assertEquals(url, DEBIAN_ISO_URL); }); Deno.test("Test invalid Debian Image Name", () => {