A Docker-like CLI and HTTP API for managing headless VMs

feat: add support for Gentoo image handling and URL construction

+176 -57
+20
main.ts
··· 32 import * as volumes from "./src/subcommands/volume.ts"; 33 import { 34 constructFedoraImageURL, 35 constructNixOSImageURL, 36 createDriveImageIfNeeded, 37 downloadIso, ··· 225 isoPath = yield* downloadIso(fedoraImageURL, options); 226 } else { 227 isoPath = basename(fedoraImageURL); 228 } 229 } 230 }
··· 32 import * as volumes from "./src/subcommands/volume.ts"; 33 import { 34 constructFedoraImageURL, 35 + constructGentooImageURL, 36 constructNixOSImageURL, 37 createDriveImageIfNeeded, 38 downloadIso, ··· 226 isoPath = yield* downloadIso(fedoraImageURL, options); 227 } else { 228 isoPath = basename(fedoraImageURL); 229 + } 230 + } 231 + 232 + const gentooImageURL = yield* pipe( 233 + constructGentooImageURL(input), 234 + Effect.catchAll(() => Effect.succeed(null)), 235 + ); 236 + 237 + if (gentooImageURL) { 238 + const cached = yield* pipe( 239 + basename(gentooImageURL), 240 + fileExists, 241 + Effect.flatMap(() => Effect.succeed(true)), 242 + Effect.catchAll(() => Effect.succeed(false)), 243 + ); 244 + if (!cached) { 245 + isoPath = yield* downloadIso(gentooImageURL, options); 246 + } else { 247 + isoPath = basename(gentooImageURL); 248 } 249 } 250 }
+4
src/constants.ts
··· 20 21 export const FEDORA_IMG_URL: string = 22 `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`;
··· 20 21 export const FEDORA_IMG_URL: string = 22 `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`; 23 + 24 + export const GENTOO_IMG_URL: string = Deno.build.arch === "aarch64" 25 + ? "https://distfiles.gentoo.org/releases/arm64/autobuilds/20251116T233105Z/di-arm64-console-20251116T233105Z.qcow2" 26 + : "https://distfiles.gentoo.org/releases/amd64/autobuilds/20251116T161545Z/di-amd64-console-20251116T161545Z.qcow2";
+89 -39
src/utils.ts
··· 9 FEDORA_COREOS_DEFAULT_VERSION, 10 FEDORA_COREOS_IMG_URL, 11 FEDORA_IMG_URL, 12 LOGS_DIR, 13 NIXOS_DEFAULT_VERSION, 14 NIXOS_ISO_URL, ··· 65 export const isValidISOurl = (url?: string): boolean => { 66 return Boolean( 67 (url?.startsWith("http://") || url?.startsWith("https://")) && 68 - url?.endsWith(".iso"), 69 ); 70 }; 71 ··· 91 }); 92 93 export const validateImage = ( 94 - image: string, 95 ): Effect.Effect<string, InvalidImageNameError, never> => { 96 const regex = 97 /^(?:[a-zA-Z0-9.-]+(?:\.[a-zA-Z0-9.-]+)*\/)?[a-z0-9]+(?:[._-][a-z0-9]+)*\/[a-z0-9]+(?:[._-][a-z0-9]+)*(?::[a-zA-Z0-9._-]+)?$/; ··· 102 image, 103 cause: 104 "Image name does not conform to expected format. Should be in the format 'repository/name:tag'.", 105 - }), 106 ); 107 } 108 return Effect.succeed(image); ··· 111 export const extractTag = (name: string) => 112 pipe( 113 validateImage(name), 114 - Effect.flatMap((image) => Effect.succeed(image.split(":")[1] || "latest")), 115 ); 116 117 export const failOnMissingImage = ( 118 - image: Image | undefined, 119 ): Effect.Effect<Image, Error, never> => 120 image 121 ? Effect.succeed(image) 122 : Effect.fail(new NoSuchImageError({ cause: "No such image" })); 123 124 export const du = ( 125 - path: string, 126 ): Effect.Effect<number, LogCommandError, never> => 127 Effect.tryPromise({ 128 try: async () => { ··· 154 exists 155 ? Effect.succeed(true) 156 : du(path).pipe(Effect.map((size) => size < EMPTY_DISK_THRESHOLD_KB)) 157 - ), 158 ); 159 160 export const downloadIso = (url: string, options: Options) => ··· 176 if (driveSize > EMPTY_DISK_THRESHOLD_KB) { 177 console.log( 178 chalk.yellowBright( 179 - `Drive image ${options.image} is not empty (size: ${driveSize} KB), skipping ISO download to avoid overwriting existing data.`, 180 - ), 181 ); 182 return null; 183 } ··· 195 if (outputExists) { 196 console.log( 197 chalk.yellowBright( 198 - `File ${outputPath} already exists, skipping download.`, 199 - ), 200 ); 201 return outputPath; 202 } ··· 245 if (!success) { 246 console.error( 247 chalk.redBright( 248 - "Failed to get QEMU prefix from Homebrew. Ensure QEMU is installed via Homebrew.", 249 - ), 250 ); 251 Deno.exit(1); 252 } ··· 259 try: () => 260 Deno.copyFile( 261 `${brewPrefix}/share/qemu/edk2-arm-vars.fd`, 262 - edk2VarsAarch64, 263 ), 264 catch: (error) => new LogCommandError({ cause: error }), 265 }); ··· 304 const configOK = yield* pipe( 305 fileExists("config.ign"), 306 Effect.flatMap(() => Effect.succeed(true)), 307 - Effect.catchAll(() => Effect.succeed(false)), 308 ); 309 if (!configOK) { 310 console.error( 311 chalk.redBright( 312 - "CoreOS image requires a config.ign file in the current directory.", 313 - ), 314 ); 315 Deno.exit(1); 316 } ··· 339 return []; 340 }); 341 342 export const runQemu = (isoPath: string | null, options: Options) => 343 Effect.gen(function* () { 344 const macAddress = yield* generateRandomMacAddress(); 345 346 - const qemu = Deno.build.arch === "aarch64" 347 - ? "qemu-system-aarch64" 348 - : "qemu-system-x86_64"; 349 350 const firmwareFiles = yield* setupFirmwareFilesIfNeeded(); 351 let coreosArgs: string[] = yield* setupCoreOSArgs(isoPath || options.image); 352 let fedoraArgs: string[] = yield* setupFedoraArgs(isoPath || options.image); 353 354 if (coreosArgs.length > 0 && !isoPath) { 355 coreosArgs = coreosArgs.slice(2); ··· 357 358 if (fedoraArgs.length > 0 && !isoPath) { 359 fedoraArgs = []; 360 } 361 362 const qemuArgs = [ ··· 387 ...firmwareFiles, 388 ...coreosArgs, 389 ...fedoraArgs, 390 ..._.compact( 391 options.image && [ 392 "-drive", 393 `file=${options.image},format=${options.diskFormat},if=virtio`, 394 - ], 395 ), 396 ]; 397 ··· 406 const logPath = `${LOGS_DIR}/${name}.log`; 407 408 const fullCommand = options.bridge 409 - ? `sudo ${qemu} ${ 410 - qemuArgs 411 .slice(1) 412 - .join(" ") 413 - } >> "${logPath}" 2>&1 & echo $!` 414 : `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`; 415 416 const { stdout } = yield* Effect.tryPromise({ ··· 436 cpus: options.cpus, 437 cpu: options.cpu, 438 diskSize: options.size || "20G", 439 - diskFormat: (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || 440 options.diskFormat || 441 "raw", 442 portForward: options.portForward, ··· 455 }); 456 457 console.log( 458 - `Virtual machine ${name} started in background (PID: ${qemuPid})`, 459 ); 460 console.log(`Logs will be written to: ${logPath}`); 461 ··· 478 cpus: options.cpus, 479 cpu: options.cpu, 480 diskSize: options.size || "20G", 481 - diskFormat: (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || 482 options.diskFormat || 483 "raw", 484 portForward: options.portForward, ··· 586 if (pathExists) { 587 console.log( 588 chalk.yellowBright( 589 - `Drive image ${path} already exists, skipping creation.`, 590 - ), 591 ); 592 return; 593 } ··· 614 }); 615 616 export const fileExists = ( 617 - path: string, 618 ): Effect.Effect<void, NoSuchFileError, never> => 619 Effect.try({ 620 try: () => Deno.statSync(path), ··· 622 }); 623 624 export const constructCoreOSImageURL = ( 625 - image: string, 626 ): Effect.Effect<string, InvalidImageNameError, never> => { 627 // detect with regex if image matches coreos pattern: fedora-coreos or fedora-coreos-<version> or coreos or coreos-<version> 628 const coreosRegex = /^(fedora-coreos|coreos)(-(\d+\.\d+\.\d+\.\d+))?$/; ··· 630 if (match) { 631 const version = match[3] || FEDORA_COREOS_DEFAULT_VERSION; 632 return Effect.succeed( 633 - FEDORA_COREOS_IMG_URL.replaceAll(FEDORA_COREOS_DEFAULT_VERSION, version), 634 ); 635 } 636 ··· 638 new InvalidImageNameError({ 639 image, 640 cause: "Image name does not match CoreOS naming conventions.", 641 - }), 642 ); 643 }; 644 ··· 667 }); 668 669 export const constructNixOSImageURL = ( 670 - image: string, 671 ): Effect.Effect<string, InvalidImageNameError, never> => { 672 // detect with regex if image matches NixOS pattern: nixos or nixos-<version> 673 const nixosRegex = /^(nixos)(-(\d+\.\d+))?$/; ··· 675 if (match) { 676 const version = match[3] || NIXOS_DEFAULT_VERSION; 677 return Effect.succeed( 678 - NIXOS_ISO_URL.replaceAll(NIXOS_DEFAULT_VERSION, version), 679 ); 680 } 681 ··· 683 new InvalidImageNameError({ 684 image, 685 cause: "Image name does not match NixOS naming conventions.", 686 - }), 687 ); 688 }; 689 690 export const constructFedoraImageURL = ( 691 - image: string, 692 ): Effect.Effect<string, InvalidImageNameError, never> => { 693 // detect with regex if image matches Fedora pattern: fedora 694 const fedoraRegex = /^(fedora)$/; ··· 701 new InvalidImageNameError({ 702 image, 703 cause: "Image name does not match Fedora naming conventions.", 704 - }), 705 ); 706 };
··· 9 FEDORA_COREOS_DEFAULT_VERSION, 10 FEDORA_COREOS_IMG_URL, 11 FEDORA_IMG_URL, 12 + GENTOO_IMG_URL, 13 LOGS_DIR, 14 NIXOS_DEFAULT_VERSION, 15 NIXOS_ISO_URL, ··· 66 export const isValidISOurl = (url?: string): boolean => { 67 return Boolean( 68 (url?.startsWith("http://") || url?.startsWith("https://")) && 69 + url?.endsWith(".iso") 70 ); 71 }; 72 ··· 92 }); 93 94 export const validateImage = ( 95 + image: string 96 ): Effect.Effect<string, InvalidImageNameError, never> => { 97 const regex = 98 /^(?:[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 image, 104 cause: 105 "Image name does not conform to expected format. Should be in the format 'repository/name:tag'.", 106 + }) 107 ); 108 } 109 return Effect.succeed(image); ··· 112 export const extractTag = (name: string) => 113 pipe( 114 validateImage(name), 115 + Effect.flatMap((image) => Effect.succeed(image.split(":")[1] || "latest")) 116 ); 117 118 export const failOnMissingImage = ( 119 + image: Image | undefined 120 ): Effect.Effect<Image, Error, never> => 121 image 122 ? Effect.succeed(image) 123 : Effect.fail(new NoSuchImageError({ cause: "No such image" })); 124 125 export const du = ( 126 + path: string 127 ): Effect.Effect<number, LogCommandError, never> => 128 Effect.tryPromise({ 129 try: async () => { ··· 155 exists 156 ? Effect.succeed(true) 157 : du(path).pipe(Effect.map((size) => size < EMPTY_DISK_THRESHOLD_KB)) 158 + ) 159 ); 160 161 export const downloadIso = (url: string, options: Options) => ··· 177 if (driveSize > EMPTY_DISK_THRESHOLD_KB) { 178 console.log( 179 chalk.yellowBright( 180 + `Drive image ${options.image} is not empty (size: ${driveSize} KB), skipping ISO download to avoid overwriting existing data.` 181 + ) 182 ); 183 return null; 184 } ··· 196 if (outputExists) { 197 console.log( 198 chalk.yellowBright( 199 + `File ${outputPath} already exists, skipping download.` 200 + ) 201 ); 202 return outputPath; 203 } ··· 246 if (!success) { 247 console.error( 248 chalk.redBright( 249 + "Failed to get QEMU prefix from Homebrew. Ensure QEMU is installed via Homebrew." 250 + ) 251 ); 252 Deno.exit(1); 253 } ··· 260 try: () => 261 Deno.copyFile( 262 `${brewPrefix}/share/qemu/edk2-arm-vars.fd`, 263 + edk2VarsAarch64 264 ), 265 catch: (error) => new LogCommandError({ cause: error }), 266 }); ··· 305 const configOK = yield* pipe( 306 fileExists("config.ign"), 307 Effect.flatMap(() => Effect.succeed(true)), 308 + Effect.catchAll(() => Effect.succeed(false)) 309 ); 310 if (!configOK) { 311 console.error( 312 chalk.redBright( 313 + "CoreOS image requires a config.ign file in the current directory." 314 + ) 315 ); 316 Deno.exit(1); 317 } ··· 340 return []; 341 }); 342 343 + export const setupGentooArgs = (imagePath?: string | null) => 344 + Effect.sync(() => { 345 + if ( 346 + imagePath && 347 + imagePath.endsWith(".qcow2") && 348 + imagePath.startsWith( 349 + `di-${Deno.build.arch === "aarch64" ? "arm64" : "amd64"}-console-` 350 + ) 351 + ) { 352 + return ["-drive", `file=${imagePath},format=qcow2,if=virtio`]; 353 + } 354 + 355 + return []; 356 + }); 357 + 358 export const runQemu = (isoPath: string | null, options: Options) => 359 Effect.gen(function* () { 360 const macAddress = yield* generateRandomMacAddress(); 361 362 + const qemu = 363 + Deno.build.arch === "aarch64" 364 + ? "qemu-system-aarch64" 365 + : "qemu-system-x86_64"; 366 367 const firmwareFiles = yield* setupFirmwareFilesIfNeeded(); 368 let coreosArgs: string[] = yield* setupCoreOSArgs(isoPath || options.image); 369 let fedoraArgs: string[] = yield* setupFedoraArgs(isoPath || options.image); 370 + let gentooArgs: string[] = yield* setupGentooArgs(isoPath || options.image); 371 372 if (coreosArgs.length > 0 && !isoPath) { 373 coreosArgs = coreosArgs.slice(2); ··· 375 376 if (fedoraArgs.length > 0 && !isoPath) { 377 fedoraArgs = []; 378 + } 379 + 380 + if (gentooArgs.length > 0 && !isoPath) { 381 + gentooArgs = []; 382 } 383 384 const qemuArgs = [ ··· 409 ...firmwareFiles, 410 ...coreosArgs, 411 ...fedoraArgs, 412 + ...gentooArgs, 413 ..._.compact( 414 options.image && [ 415 "-drive", 416 `file=${options.image},format=${options.diskFormat},if=virtio`, 417 + ] 418 ), 419 ]; 420 ··· 429 const logPath = `${LOGS_DIR}/${name}.log`; 430 431 const fullCommand = options.bridge 432 + ? `sudo ${qemu} ${qemuArgs 433 .slice(1) 434 + .join(" ")} >> "${logPath}" 2>&1 & echo $!` 435 : `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`; 436 437 const { stdout } = yield* Effect.tryPromise({ ··· 457 cpus: options.cpus, 458 cpu: options.cpu, 459 diskSize: options.size || "20G", 460 + diskFormat: 461 + (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || 462 options.diskFormat || 463 "raw", 464 portForward: options.portForward, ··· 477 }); 478 479 console.log( 480 + `Virtual machine ${name} started in background (PID: ${qemuPid})` 481 ); 482 console.log(`Logs will be written to: ${logPath}`); 483 ··· 500 cpus: options.cpus, 501 cpu: options.cpu, 502 diskSize: options.size || "20G", 503 + diskFormat: 504 + (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || 505 options.diskFormat || 506 "raw", 507 portForward: options.portForward, ··· 609 if (pathExists) { 610 console.log( 611 chalk.yellowBright( 612 + `Drive image ${path} already exists, skipping creation.` 613 + ) 614 ); 615 return; 616 } ··· 637 }); 638 639 export const fileExists = ( 640 + path: string 641 ): Effect.Effect<void, NoSuchFileError, never> => 642 Effect.try({ 643 try: () => Deno.statSync(path), ··· 645 }); 646 647 export const constructCoreOSImageURL = ( 648 + image: string 649 ): Effect.Effect<string, InvalidImageNameError, never> => { 650 // detect with regex if image matches coreos pattern: fedora-coreos or fedora-coreos-<version> or coreos or coreos-<version> 651 const coreosRegex = /^(fedora-coreos|coreos)(-(\d+\.\d+\.\d+\.\d+))?$/; ··· 653 if (match) { 654 const version = match[3] || FEDORA_COREOS_DEFAULT_VERSION; 655 return Effect.succeed( 656 + FEDORA_COREOS_IMG_URL.replaceAll(FEDORA_COREOS_DEFAULT_VERSION, version) 657 ); 658 } 659 ··· 661 new InvalidImageNameError({ 662 image, 663 cause: "Image name does not match CoreOS naming conventions.", 664 + }) 665 ); 666 }; 667 ··· 690 }); 691 692 export const constructNixOSImageURL = ( 693 + image: string 694 ): Effect.Effect<string, InvalidImageNameError, never> => { 695 // detect with regex if image matches NixOS pattern: nixos or nixos-<version> 696 const nixosRegex = /^(nixos)(-(\d+\.\d+))?$/; ··· 698 if (match) { 699 const version = match[3] || NIXOS_DEFAULT_VERSION; 700 return Effect.succeed( 701 + NIXOS_ISO_URL.replaceAll(NIXOS_DEFAULT_VERSION, version) 702 ); 703 } 704 ··· 706 new InvalidImageNameError({ 707 image, 708 cause: "Image name does not match NixOS naming conventions.", 709 + }) 710 ); 711 }; 712 713 export const constructFedoraImageURL = ( 714 + image: string 715 ): Effect.Effect<string, InvalidImageNameError, never> => { 716 // detect with regex if image matches Fedora pattern: fedora 717 const fedoraRegex = /^(fedora)$/; ··· 724 new InvalidImageNameError({ 725 image, 726 cause: "Image name does not match Fedora naming conventions.", 727 + }) 728 + ); 729 + }; 730 + 731 + export const constructGentooImageURL = ( 732 + image: string 733 + ): Effect.Effect<string, InvalidImageNameError, never> => { 734 + // detect with regex if image matches genroo pattern: gentoo-20251116T161545Z or gentoo 735 + const gentooRegex = /^(gentoo)(-(\d{8}T\d{6}Z))?$/; 736 + const match = image.match(gentooRegex); 737 + if (match?.[3]) { 738 + return Effect.succeed( 739 + GENTOO_IMG_URL.replaceAll("20251116T161545Z", match[3]).replaceAll( 740 + "20251116T233105Z", 741 + match[3] 742 + ) 743 + ); 744 + } 745 + 746 + if (match) { 747 + return Effect.succeed(GENTOO_IMG_URL); 748 + } 749 + 750 + return Effect.fail( 751 + new InvalidImageNameError({ 752 + image, 753 + cause: "Image name does not match Gentoo naming conventions.", 754 + }) 755 ); 756 };
+63 -18
src/utils_test.ts
··· 1 import { assertEquals } from "@std/assert"; 2 import { Effect, pipe } from "effect"; 3 - import { FEDORA_COREOS_IMG_URL, NIXOS_ISO_URL } from "./constants.ts"; 4 - import { constructCoreOSImageURL, constructNixOSImageURL } from "./utils.ts"; 5 6 Deno.test("Test Default Fedora CoreOS Image URL", () => { 7 const url = Effect.runSync( 8 pipe( 9 constructCoreOSImageURL("fedora-coreos"), 10 - Effect.catchAll((_error) => Effect.succeed(null as string | null)), 11 - ), 12 ); 13 14 assertEquals(url, FEDORA_COREOS_IMG_URL); ··· 18 const url = Effect.runSync( 19 pipe( 20 constructCoreOSImageURL("coreos"), 21 - Effect.catchAll((_error) => Effect.succeed(null as string | null)), 22 - ), 23 ); 24 25 assertEquals(url, FEDORA_COREOS_IMG_URL); ··· 29 const url = Effect.runSync( 30 pipe( 31 constructCoreOSImageURL("fedora-coreos-43.20251024.2.0"), 32 - Effect.catchAll((_error) => Effect.succeed(null as string | null)), 33 - ), 34 ); 35 36 assertEquals( 37 url, 38 "https://builds.coreos.fedoraproject.org/prod/streams/stable/builds/43.20251024.2.0/" + 39 - `${Deno.build.arch}/fedora-coreos-43.20251024.2.0-qemu.${Deno.build.arch}.qcow2.xz`, 40 ); 41 }); 42 ··· 44 const url = Effect.runSync( 45 pipe( 46 constructCoreOSImageURL("fedora-coreos-latest"), 47 - Effect.catchAll((_error) => Effect.succeed(null as string | null)), 48 - ), 49 ); 50 51 assertEquals(url, null); ··· 55 const url = Effect.runSync( 56 pipe( 57 constructNixOSImageURL("nixos"), 58 - Effect.catchAll((_error) => Effect.succeed(null as string | null)), 59 - ), 60 ); 61 62 assertEquals(url, NIXOS_ISO_URL); ··· 66 const url = Effect.runSync( 67 pipe( 68 constructNixOSImageURL("nixos-24.05"), 69 - Effect.catchAll((_error) => Effect.succeed(null as string | null)), 70 - ), 71 ); 72 73 assertEquals( 74 url, 75 - `https://channels.nixos.org/nixos-24.05/latest-nixos-minimal-${Deno.build.arch}-linux.iso`, 76 ); 77 }); 78 ··· 80 const url = Effect.runSync( 81 pipe( 82 constructNixOSImageURL("nixos-latest"), 83 - Effect.catchAll((_error) => Effect.succeed(null as string | null)), 84 - ), 85 ); 86 87 assertEquals(url, null);
··· 1 import { assertEquals } from "@std/assert"; 2 import { Effect, pipe } from "effect"; 3 + import { 4 + FEDORA_COREOS_IMG_URL, 5 + GENTOO_IMG_URL, 6 + NIXOS_ISO_URL, 7 + } from "./constants.ts"; 8 + import { 9 + constructCoreOSImageURL, 10 + constructGentooImageURL, 11 + constructNixOSImageURL, 12 + } from "./utils.ts"; 13 14 Deno.test("Test Default Fedora CoreOS Image URL", () => { 15 const url = Effect.runSync( 16 pipe( 17 constructCoreOSImageURL("fedora-coreos"), 18 + Effect.catchAll((_error) => Effect.succeed(null as string | null)) 19 + ) 20 ); 21 22 assertEquals(url, FEDORA_COREOS_IMG_URL); ··· 26 const url = Effect.runSync( 27 pipe( 28 constructCoreOSImageURL("coreos"), 29 + Effect.catchAll((_error) => Effect.succeed(null as string | null)) 30 + ) 31 ); 32 33 assertEquals(url, FEDORA_COREOS_IMG_URL); ··· 37 const url = Effect.runSync( 38 pipe( 39 constructCoreOSImageURL("fedora-coreos-43.20251024.2.0"), 40 + Effect.catchAll((_error) => Effect.succeed(null as string | null)) 41 + ) 42 ); 43 44 assertEquals( 45 url, 46 "https://builds.coreos.fedoraproject.org/prod/streams/stable/builds/43.20251024.2.0/" + 47 + `${Deno.build.arch}/fedora-coreos-43.20251024.2.0-qemu.${Deno.build.arch}.qcow2.xz` 48 ); 49 }); 50 ··· 52 const url = Effect.runSync( 53 pipe( 54 constructCoreOSImageURL("fedora-coreos-latest"), 55 + Effect.catchAll((_error) => Effect.succeed(null as string | null)) 56 + ) 57 ); 58 59 assertEquals(url, null); ··· 63 const url = Effect.runSync( 64 pipe( 65 constructNixOSImageURL("nixos"), 66 + Effect.catchAll((_error) => Effect.succeed(null as string | null)) 67 + ) 68 ); 69 70 assertEquals(url, NIXOS_ISO_URL); ··· 74 const url = Effect.runSync( 75 pipe( 76 constructNixOSImageURL("nixos-24.05"), 77 + Effect.catchAll((_error) => Effect.succeed(null as string | null)) 78 + ) 79 ); 80 81 assertEquals( 82 url, 83 + `https://channels.nixos.org/nixos-24.05/latest-nixos-minimal-${Deno.build.arch}-linux.iso` 84 ); 85 }); 86 ··· 88 const url = Effect.runSync( 89 pipe( 90 constructNixOSImageURL("nixos-latest"), 91 + Effect.catchAll((_error) => Effect.succeed(null as string | null)) 92 + ) 93 + ); 94 + 95 + assertEquals(url, null); 96 + }); 97 + 98 + Deno.test("Test valid Gentoo Image Name", () => { 99 + const url = Effect.runSync( 100 + pipe( 101 + constructGentooImageURL("gentoo-20251116T161545Z"), 102 + Effect.catchAll((_error) => Effect.succeed(null as string | null)) 103 + ) 104 + ); 105 + 106 + const arch = Deno.build.arch === "aarch64" ? "arm64" : "amd64"; 107 + assertEquals( 108 + url, 109 + `https://distfiles.gentoo.org/releases/${arch}/autobuilds/20251116T161545Z/di-${arch}-console-20251116T161545Z.qcow2` 110 + ); 111 + }); 112 + 113 + Deno.test("Test valid Gentoo Image Name", () => { 114 + const url = Effect.runSync( 115 + pipe( 116 + constructGentooImageURL("gentoo"), 117 + Effect.catchAll((_error) => Effect.succeed(null as string | null)) 118 + ) 119 + ); 120 + 121 + assertEquals(url, GENTOO_IMG_URL); 122 + }); 123 + 124 + Deno.test("Test invalid Gentoo Image Name", () => { 125 + const url = Effect.runSync( 126 + pipe( 127 + constructGentooImageURL("gentoo-latest"), 128 + Effect.catchAll((_error) => Effect.succeed(null as string | null)) 129 + ) 130 ); 131 132 assertEquals(url, null);