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

run format

+70 -71
+70 -71
src/utils.ts
··· 78 export const isValidISOurl = (url?: string): boolean => { 79 return Boolean( 80 (url?.startsWith("http://") || url?.startsWith("https://")) && 81 - url?.endsWith(".iso") 82 ); 83 }; 84 ··· 104 }); 105 106 export const validateImage = ( 107 - image: string 108 ): Effect.Effect<string, InvalidImageNameError, never> => { 109 const regex = 110 /^(?:[a-zA-Z0-9.-]+(?:\.[a-zA-Z0-9.-]+)*\/)?[a-z0-9]+(?:[._-][a-z0-9]+)*\/[a-z0-9]+(?:[._-][a-z0-9]+)*(?::[a-zA-Z0-9._-]+)?$/; ··· 115 image, 116 cause: 117 "Image name does not conform to expected format. Should be in the format 'repository/name:tag'.", 118 - }) 119 ); 120 } 121 return Effect.succeed(image); ··· 124 export const extractTag = (name: string) => 125 pipe( 126 validateImage(name), 127 - Effect.flatMap((image) => Effect.succeed(image.split(":")[1] || "latest")) 128 ); 129 130 export const failOnMissingImage = ( 131 - image: Image | undefined 132 ): Effect.Effect<Image, Error, never> => 133 image 134 ? Effect.succeed(image) 135 : Effect.fail(new NoSuchImageError({ cause: "No such image" })); 136 137 export const du = ( 138 - path: string 139 ): Effect.Effect<number, LogCommandError, never> => 140 Effect.tryPromise({ 141 try: async () => { ··· 167 exists 168 ? Effect.succeed(true) 169 : du(path).pipe(Effect.map((size) => size < EMPTY_DISK_THRESHOLD_KB)) 170 - ) 171 ); 172 173 export const downloadIso = (url: string, options: Options) => ··· 189 if (driveSize > EMPTY_DISK_THRESHOLD_KB) { 190 console.log( 191 chalk.yellowBright( 192 - `Drive image ${options.image} is not empty (size: ${driveSize} KB), skipping ISO download to avoid overwriting existing data.` 193 - ) 194 ); 195 return null; 196 } ··· 208 if (outputExists) { 209 console.log( 210 chalk.yellowBright( 211 - `File ${outputPath} already exists, skipping download.` 212 - ) 213 ); 214 return outputPath; 215 } ··· 220 chalk.blueBright( 221 `Downloading ${ 222 url.endsWith(".iso") ? "ISO" : "image" 223 - } from ${url}...` 224 - ) 225 ); 226 const cmd = new Deno.Command("curl", { 227 args: ["-L", "-o", outputPath, url], ··· 264 if (!success) { 265 console.error( 266 chalk.redBright( 267 - "Failed to get QEMU prefix from Homebrew. Ensure QEMU is installed via Homebrew." 268 - ) 269 ); 270 Deno.exit(1); 271 } ··· 278 try: () => 279 Deno.copyFile( 280 `${brewPrefix}/share/qemu/edk2-arm-vars.fd`, 281 - edk2VarsAarch64 282 ), 283 catch: (error) => new LogCommandError({ cause: error }), 284 }); ··· 323 const configOK = yield* pipe( 324 fileExists("config.ign"), 325 Effect.flatMap(() => Effect.succeed(true)), 326 - Effect.catchAll(() => Effect.succeed(false)) 327 ); 328 if (!configOK) { 329 console.error( 330 chalk.redBright( 331 - "CoreOS image requires a config.ign file in the current directory." 332 - ) 333 ); 334 Deno.exit(1); 335 } ··· 369 imagePath && 370 imagePath.endsWith(".qcow2") && 371 imagePath.startsWith( 372 - `di-${Deno.build.arch === "aarch64" ? "arm64" : "amd64"}-console-` 373 ) 374 ) { 375 return [ ··· 384 385 export const setupAlpineArgs = ( 386 imagePath?: string | null, 387 - seed: string = "seed.iso" 388 ) => 389 Effect.sync(() => { 390 if ( ··· 405 406 export const setupDebianArgs = ( 407 imagePath?: string | null, 408 - seed: string = "seed.iso" 409 ) => 410 Effect.sync(() => { 411 if ( ··· 426 427 export const setupUbuntuArgs = ( 428 imagePath?: string | null, 429 - seed: string = "seed.iso" 430 ) => 431 Effect.sync(() => { 432 if ( ··· 447 448 export const setupAlmaLinuxArgs = ( 449 imagePath?: string | null, 450 - seed: string = "seed.iso" 451 ) => 452 Effect.sync(() => { 453 if ( ··· 468 469 export const setupRockyLinuxArgs = ( 470 imagePath?: string | null, 471 - seed: string = "seed.iso" 472 ) => 473 Effect.sync(() => { 474 if ( ··· 491 Effect.gen(function* () { 492 const macAddress = yield* generateRandomMacAddress(); 493 494 - const qemu = 495 - Deno.build.arch === "aarch64" 496 - ? "qemu-system-aarch64" 497 - : "qemu-system-x86_64"; 498 499 const firmwareFiles = yield* setupFirmwareFilesIfNeeded(); 500 let coreosArgs: string[] = yield* setupCoreOSArgs(isoPath || options.image); 501 let fedoraArgs: string[] = yield* setupFedoraArgs( 502 isoPath || options.image, 503 - options.seed 504 ); 505 let gentooArgs: string[] = yield* setupGentooArgs( 506 isoPath || options.image, 507 - options.seed 508 ); 509 let alpineArgs: string[] = yield* setupAlpineArgs( 510 isoPath || options.image, 511 - options.seed 512 ); 513 let debianArgs: string[] = yield* setupDebianArgs( 514 isoPath || options.image, 515 - options.seed 516 ); 517 let ubuntuArgs: string[] = yield* setupUbuntuArgs( 518 isoPath || options.image, 519 - options.seed 520 ); 521 let almalinuxArgs: string[] = yield* setupAlmaLinuxArgs( 522 isoPath || options.image, 523 - options.seed 524 ); 525 let rockylinuxArgs: string[] = yield* setupRockyLinuxArgs( 526 isoPath || options.image, 527 - options.seed 528 ); 529 530 if (coreosArgs.length > 0 && !isoPath) { ··· 597 options.image && [ 598 "-drive", 599 `file=${options.image},format=${options.diskFormat},if=virtio`, 600 - ] 601 ), 602 ]; 603 ··· 612 const logPath = `${LOGS_DIR}/${name}.log`; 613 614 const fullCommand = options.bridge 615 - ? `sudo ${qemu} ${qemuArgs 616 .slice(1) 617 - .join(" ")} >> "${logPath}" 2>&1 & echo $!` 618 : `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`; 619 620 const { stdout } = yield* Effect.tryPromise({ ··· 640 cpus: options.cpus, 641 cpu: options.cpu, 642 diskSize: options.size || "20G", 643 - diskFormat: 644 - (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || 645 options.diskFormat || 646 "raw", 647 portForward: options.portForward, ··· 661 }); 662 663 console.log( 664 - `Virtual machine ${name} started in background (PID: ${qemuPid})` 665 ); 666 console.log(`Logs will be written to: ${logPath}`); 667 ··· 684 cpus: options.cpus, 685 cpu: options.cpu, 686 diskSize: options.size || "20G", 687 - diskFormat: 688 - (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || 689 options.diskFormat || 690 "raw", 691 portForward: options.portForward, ··· 794 if (pathExists) { 795 console.log( 796 chalk.yellowBright( 797 - `Drive image ${path} already exists, skipping creation.` 798 - ) 799 ); 800 return; 801 } ··· 822 }); 823 824 export const fileExists = ( 825 - path: string 826 ): Effect.Effect<void, NoSuchFileError, never> => 827 Effect.try({ 828 try: () => Deno.statSync(path), ··· 830 }); 831 832 export const constructCoreOSImageURL = ( 833 - image: string 834 ): Effect.Effect<string, InvalidImageNameError, never> => { 835 // detect with regex if image matches coreos pattern: fedora-coreos or fedora-coreos-<version> or coreos or coreos-<version> 836 const coreosRegex = /^(fedora-coreos|coreos)(-(\d+\.\d+\.\d+\.\d+))?$/; ··· 838 if (match) { 839 const version = match[3] || FEDORA_COREOS_DEFAULT_VERSION; 840 return Effect.succeed( 841 - FEDORA_COREOS_IMG_URL.replaceAll(FEDORA_COREOS_DEFAULT_VERSION, version) 842 ); 843 } 844 ··· 846 new InvalidImageNameError({ 847 image, 848 cause: "Image name does not match CoreOS naming conventions.", 849 - }) 850 ); 851 }; 852 ··· 875 }); 876 877 export const constructNixOSImageURL = ( 878 - image: string 879 ): Effect.Effect<string, InvalidImageNameError, never> => { 880 // detect with regex if image matches NixOS pattern: nixos or nixos-<version> 881 const nixosRegex = /^(nixos)(-(\d+\.\d+))?$/; ··· 883 if (match) { 884 const version = match[3] || NIXOS_DEFAULT_VERSION; 885 return Effect.succeed( 886 - NIXOS_ISO_URL.replaceAll(NIXOS_DEFAULT_VERSION, version) 887 ); 888 } 889 ··· 891 new InvalidImageNameError({ 892 image, 893 cause: "Image name does not match NixOS naming conventions.", 894 - }) 895 ); 896 }; 897 898 export const constructFedoraImageURL = ( 899 image: string, 900 - cloud: boolean = false 901 ): Effect.Effect<string, InvalidImageNameError, never> => { 902 // detect with regex if image matches Fedora pattern: fedora 903 const fedoraRegex = /^(fedora)$/; ··· 910 new InvalidImageNameError({ 911 image, 912 cause: "Image name does not match Fedora naming conventions.", 913 - }) 914 ); 915 }; 916 917 export const constructGentooImageURL = ( 918 - image: string 919 ): Effect.Effect<string, InvalidImageNameError, never> => { 920 // detect with regex if image matches genroo pattern: gentoo-20251116T161545Z or gentoo 921 const gentooRegex = /^(gentoo)(-(\d{8}T\d{6}Z))?$/; ··· 924 return Effect.succeed( 925 GENTOO_IMG_URL.replaceAll("20251116T161545Z", match[3]).replaceAll( 926 "20251116T233105Z", 927 - match[3] 928 - ) 929 ); 930 } 931 ··· 937 new InvalidImageNameError({ 938 image, 939 cause: "Image name does not match Gentoo naming conventions.", 940 - }) 941 ); 942 }; 943 944 export const constructDebianImageURL = ( 945 image: string, 946 - cloud: boolean = false 947 ): Effect.Effect<string, InvalidImageNameError, never> => { 948 if (cloud && image === "debian") { 949 return Effect.succeed(DEBIAN_CLOUD_IMG_URL); ··· 954 const match = image.match(debianRegex); 955 if (match?.[3]) { 956 return Effect.succeed( 957 - DEBIAN_ISO_URL.replaceAll(DEBIAN_DEFAULT_VERSION, match[3]) 958 ); 959 } 960 ··· 966 new InvalidImageNameError({ 967 image, 968 cause: "Image name does not match Debian naming conventions.", 969 - }) 970 ); 971 }; 972 973 export const constructAlpineImageURL = ( 974 - image: string 975 ): Effect.Effect<string, InvalidImageNameError, never> => { 976 // detect with regex if image matches alpine pattern: alpine-<version> or alpine 977 const alpineRegex = /^(alpine)(-(\d+\.\d+(\.\d+)?))?$/; 978 const match = image.match(alpineRegex); 979 if (match?.[3]) { 980 return Effect.succeed( 981 - ALPINE_ISO_URL.replaceAll(ALPINE_DEFAULT_VERSION, match[3]) 982 ); 983 } 984 ··· 990 new InvalidImageNameError({ 991 image, 992 cause: "Image name does not match Alpine naming conventions.", 993 - }) 994 ); 995 }; 996 997 export const constructUbuntuImageURL = ( 998 image: string, 999 - cloud: boolean = false 1000 ): Effect.Effect<string, InvalidImageNameError, never> => { 1001 // detect with regex if image matches ubuntu pattern: ubuntu 1002 const ubuntuRegex = /^(ubuntu)$/; ··· 1012 new InvalidImageNameError({ 1013 image, 1014 cause: "Image name does not match Ubuntu naming conventions.", 1015 - }) 1016 ); 1017 }; 1018 1019 export const constructAlmaLinuxImageURL = ( 1020 image: string, 1021 - cloud: boolean = false 1022 ): Effect.Effect<string, InvalidImageNameError, never> => { 1023 // detect with regex if image matches almalinux pattern: ubuntu 1024 const almaLinuxRegex = /^(almalinux)$/; ··· 1034 new InvalidImageNameError({ 1035 image, 1036 cause: "Image name does not match AlmaLinux naming conventions.", 1037 - }) 1038 ); 1039 }; 1040 1041 export const constructRockyLinuxImageURL = ( 1042 image: string, 1043 - cloud: boolean = false 1044 ): Effect.Effect<string, InvalidImageNameError, never> => { 1045 // detect with regex if image matches rockylinux pattern: ubuntu 1046 const rockyLinuxRegex = /^(rockylinux)$/; ··· 1056 new InvalidImageNameError({ 1057 image, 1058 cause: "Image name does not match RockyLinux naming conventions.", 1059 - }) 1060 ); 1061 };
··· 78 export const isValidISOurl = (url?: string): boolean => { 79 return Boolean( 80 (url?.startsWith("http://") || url?.startsWith("https://")) && 81 + url?.endsWith(".iso"), 82 ); 83 }; 84 ··· 104 }); 105 106 export const validateImage = ( 107 + image: string, 108 ): Effect.Effect<string, InvalidImageNameError, never> => { 109 const regex = 110 /^(?:[a-zA-Z0-9.-]+(?:\.[a-zA-Z0-9.-]+)*\/)?[a-z0-9]+(?:[._-][a-z0-9]+)*\/[a-z0-9]+(?:[._-][a-z0-9]+)*(?::[a-zA-Z0-9._-]+)?$/; ··· 115 image, 116 cause: 117 "Image name does not conform to expected format. Should be in the format 'repository/name:tag'.", 118 + }), 119 ); 120 } 121 return Effect.succeed(image); ··· 124 export const extractTag = (name: string) => 125 pipe( 126 validateImage(name), 127 + Effect.flatMap((image) => Effect.succeed(image.split(":")[1] || "latest")), 128 ); 129 130 export const failOnMissingImage = ( 131 + image: Image | undefined, 132 ): Effect.Effect<Image, Error, never> => 133 image 134 ? Effect.succeed(image) 135 : Effect.fail(new NoSuchImageError({ cause: "No such image" })); 136 137 export const du = ( 138 + path: string, 139 ): Effect.Effect<number, LogCommandError, never> => 140 Effect.tryPromise({ 141 try: async () => { ··· 167 exists 168 ? Effect.succeed(true) 169 : du(path).pipe(Effect.map((size) => size < EMPTY_DISK_THRESHOLD_KB)) 170 + ), 171 ); 172 173 export const downloadIso = (url: string, options: Options) => ··· 189 if (driveSize > EMPTY_DISK_THRESHOLD_KB) { 190 console.log( 191 chalk.yellowBright( 192 + `Drive image ${options.image} is not empty (size: ${driveSize} KB), skipping ISO download to avoid overwriting existing data.`, 193 + ), 194 ); 195 return null; 196 } ··· 208 if (outputExists) { 209 console.log( 210 chalk.yellowBright( 211 + `File ${outputPath} already exists, skipping download.`, 212 + ), 213 ); 214 return outputPath; 215 } ··· 220 chalk.blueBright( 221 `Downloading ${ 222 url.endsWith(".iso") ? "ISO" : "image" 223 + } from ${url}...`, 224 + ), 225 ); 226 const cmd = new Deno.Command("curl", { 227 args: ["-L", "-o", outputPath, url], ··· 264 if (!success) { 265 console.error( 266 chalk.redBright( 267 + "Failed to get QEMU prefix from Homebrew. Ensure QEMU is installed via Homebrew.", 268 + ), 269 ); 270 Deno.exit(1); 271 } ··· 278 try: () => 279 Deno.copyFile( 280 `${brewPrefix}/share/qemu/edk2-arm-vars.fd`, 281 + edk2VarsAarch64, 282 ), 283 catch: (error) => new LogCommandError({ cause: error }), 284 }); ··· 323 const configOK = yield* pipe( 324 fileExists("config.ign"), 325 Effect.flatMap(() => Effect.succeed(true)), 326 + Effect.catchAll(() => Effect.succeed(false)), 327 ); 328 if (!configOK) { 329 console.error( 330 chalk.redBright( 331 + "CoreOS image requires a config.ign file in the current directory.", 332 + ), 333 ); 334 Deno.exit(1); 335 } ··· 369 imagePath && 370 imagePath.endsWith(".qcow2") && 371 imagePath.startsWith( 372 + `di-${Deno.build.arch === "aarch64" ? "arm64" : "amd64"}-console-`, 373 ) 374 ) { 375 return [ ··· 384 385 export const setupAlpineArgs = ( 386 imagePath?: string | null, 387 + seed: string = "seed.iso", 388 ) => 389 Effect.sync(() => { 390 if ( ··· 405 406 export const setupDebianArgs = ( 407 imagePath?: string | null, 408 + seed: string = "seed.iso", 409 ) => 410 Effect.sync(() => { 411 if ( ··· 426 427 export const setupUbuntuArgs = ( 428 imagePath?: string | null, 429 + seed: string = "seed.iso", 430 ) => 431 Effect.sync(() => { 432 if ( ··· 447 448 export const setupAlmaLinuxArgs = ( 449 imagePath?: string | null, 450 + seed: string = "seed.iso", 451 ) => 452 Effect.sync(() => { 453 if ( ··· 468 469 export const setupRockyLinuxArgs = ( 470 imagePath?: string | null, 471 + seed: string = "seed.iso", 472 ) => 473 Effect.sync(() => { 474 if ( ··· 491 Effect.gen(function* () { 492 const macAddress = yield* generateRandomMacAddress(); 493 494 + const qemu = Deno.build.arch === "aarch64" 495 + ? "qemu-system-aarch64" 496 + : "qemu-system-x86_64"; 497 498 const firmwareFiles = yield* setupFirmwareFilesIfNeeded(); 499 let coreosArgs: string[] = yield* setupCoreOSArgs(isoPath || options.image); 500 let fedoraArgs: string[] = yield* setupFedoraArgs( 501 isoPath || options.image, 502 + options.seed, 503 ); 504 let gentooArgs: string[] = yield* setupGentooArgs( 505 isoPath || options.image, 506 + options.seed, 507 ); 508 let alpineArgs: string[] = yield* setupAlpineArgs( 509 isoPath || options.image, 510 + options.seed, 511 ); 512 let debianArgs: string[] = yield* setupDebianArgs( 513 isoPath || options.image, 514 + options.seed, 515 ); 516 let ubuntuArgs: string[] = yield* setupUbuntuArgs( 517 isoPath || options.image, 518 + options.seed, 519 ); 520 let almalinuxArgs: string[] = yield* setupAlmaLinuxArgs( 521 isoPath || options.image, 522 + options.seed, 523 ); 524 let rockylinuxArgs: string[] = yield* setupRockyLinuxArgs( 525 isoPath || options.image, 526 + options.seed, 527 ); 528 529 if (coreosArgs.length > 0 && !isoPath) { ··· 596 options.image && [ 597 "-drive", 598 `file=${options.image},format=${options.diskFormat},if=virtio`, 599 + ], 600 ), 601 ]; 602 ··· 611 const logPath = `${LOGS_DIR}/${name}.log`; 612 613 const fullCommand = options.bridge 614 + ? `sudo ${qemu} ${ 615 + qemuArgs 616 .slice(1) 617 + .join(" ") 618 + } >> "${logPath}" 2>&1 & echo $!` 619 : `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`; 620 621 const { stdout } = yield* Effect.tryPromise({ ··· 641 cpus: options.cpus, 642 cpu: options.cpu, 643 diskSize: options.size || "20G", 644 + diskFormat: (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || 645 options.diskFormat || 646 "raw", 647 portForward: options.portForward, ··· 661 }); 662 663 console.log( 664 + `Virtual machine ${name} started in background (PID: ${qemuPid})`, 665 ); 666 console.log(`Logs will be written to: ${logPath}`); 667 ··· 684 cpus: options.cpus, 685 cpu: options.cpu, 686 diskSize: options.size || "20G", 687 + diskFormat: (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || 688 options.diskFormat || 689 "raw", 690 portForward: options.portForward, ··· 793 if (pathExists) { 794 console.log( 795 chalk.yellowBright( 796 + `Drive image ${path} already exists, skipping creation.`, 797 + ), 798 ); 799 return; 800 } ··· 821 }); 822 823 export const fileExists = ( 824 + path: string, 825 ): Effect.Effect<void, NoSuchFileError, never> => 826 Effect.try({ 827 try: () => Deno.statSync(path), ··· 829 }); 830 831 export const constructCoreOSImageURL = ( 832 + image: string, 833 ): Effect.Effect<string, InvalidImageNameError, never> => { 834 // detect with regex if image matches coreos pattern: fedora-coreos or fedora-coreos-<version> or coreos or coreos-<version> 835 const coreosRegex = /^(fedora-coreos|coreos)(-(\d+\.\d+\.\d+\.\d+))?$/; ··· 837 if (match) { 838 const version = match[3] || FEDORA_COREOS_DEFAULT_VERSION; 839 return Effect.succeed( 840 + FEDORA_COREOS_IMG_URL.replaceAll(FEDORA_COREOS_DEFAULT_VERSION, version), 841 ); 842 } 843 ··· 845 new InvalidImageNameError({ 846 image, 847 cause: "Image name does not match CoreOS naming conventions.", 848 + }), 849 ); 850 }; 851 ··· 874 }); 875 876 export const constructNixOSImageURL = ( 877 + image: string, 878 ): Effect.Effect<string, InvalidImageNameError, never> => { 879 // detect with regex if image matches NixOS pattern: nixos or nixos-<version> 880 const nixosRegex = /^(nixos)(-(\d+\.\d+))?$/; ··· 882 if (match) { 883 const version = match[3] || NIXOS_DEFAULT_VERSION; 884 return Effect.succeed( 885 + NIXOS_ISO_URL.replaceAll(NIXOS_DEFAULT_VERSION, version), 886 ); 887 } 888 ··· 890 new InvalidImageNameError({ 891 image, 892 cause: "Image name does not match NixOS naming conventions.", 893 + }), 894 ); 895 }; 896 897 export const constructFedoraImageURL = ( 898 image: string, 899 + cloud: boolean = false, 900 ): Effect.Effect<string, InvalidImageNameError, never> => { 901 // detect with regex if image matches Fedora pattern: fedora 902 const fedoraRegex = /^(fedora)$/; ··· 909 new InvalidImageNameError({ 910 image, 911 cause: "Image name does not match Fedora naming conventions.", 912 + }), 913 ); 914 }; 915 916 export const constructGentooImageURL = ( 917 + image: string, 918 ): Effect.Effect<string, InvalidImageNameError, never> => { 919 // detect with regex if image matches genroo pattern: gentoo-20251116T161545Z or gentoo 920 const gentooRegex = /^(gentoo)(-(\d{8}T\d{6}Z))?$/; ··· 923 return Effect.succeed( 924 GENTOO_IMG_URL.replaceAll("20251116T161545Z", match[3]).replaceAll( 925 "20251116T233105Z", 926 + match[3], 927 + ), 928 ); 929 } 930 ··· 936 new InvalidImageNameError({ 937 image, 938 cause: "Image name does not match Gentoo naming conventions.", 939 + }), 940 ); 941 }; 942 943 export const constructDebianImageURL = ( 944 image: string, 945 + cloud: boolean = false, 946 ): Effect.Effect<string, InvalidImageNameError, never> => { 947 if (cloud && image === "debian") { 948 return Effect.succeed(DEBIAN_CLOUD_IMG_URL); ··· 953 const match = image.match(debianRegex); 954 if (match?.[3]) { 955 return Effect.succeed( 956 + DEBIAN_ISO_URL.replaceAll(DEBIAN_DEFAULT_VERSION, match[3]), 957 ); 958 } 959 ··· 965 new InvalidImageNameError({ 966 image, 967 cause: "Image name does not match Debian naming conventions.", 968 + }), 969 ); 970 }; 971 972 export const constructAlpineImageURL = ( 973 + image: string, 974 ): Effect.Effect<string, InvalidImageNameError, never> => { 975 // detect with regex if image matches alpine pattern: alpine-<version> or alpine 976 const alpineRegex = /^(alpine)(-(\d+\.\d+(\.\d+)?))?$/; 977 const match = image.match(alpineRegex); 978 if (match?.[3]) { 979 return Effect.succeed( 980 + ALPINE_ISO_URL.replaceAll(ALPINE_DEFAULT_VERSION, match[3]), 981 ); 982 } 983 ··· 989 new InvalidImageNameError({ 990 image, 991 cause: "Image name does not match Alpine naming conventions.", 992 + }), 993 ); 994 }; 995 996 export const constructUbuntuImageURL = ( 997 image: string, 998 + cloud: boolean = false, 999 ): Effect.Effect<string, InvalidImageNameError, never> => { 1000 // detect with regex if image matches ubuntu pattern: ubuntu 1001 const ubuntuRegex = /^(ubuntu)$/; ··· 1011 new InvalidImageNameError({ 1012 image, 1013 cause: "Image name does not match Ubuntu naming conventions.", 1014 + }), 1015 ); 1016 }; 1017 1018 export const constructAlmaLinuxImageURL = ( 1019 image: string, 1020 + cloud: boolean = false, 1021 ): Effect.Effect<string, InvalidImageNameError, never> => { 1022 // detect with regex if image matches almalinux pattern: ubuntu 1023 const almaLinuxRegex = /^(almalinux)$/; ··· 1033 new InvalidImageNameError({ 1034 image, 1035 cause: "Image name does not match AlmaLinux naming conventions.", 1036 + }), 1037 ); 1038 }; 1039 1040 export const constructRockyLinuxImageURL = ( 1041 image: string, 1042 + cloud: boolean = false, 1043 ): Effect.Effect<string, InvalidImageNameError, never> => { 1044 // detect with regex if image matches rockylinux pattern: ubuntu 1045 const rockyLinuxRegex = /^(rockylinux)$/; ··· 1055 new InvalidImageNameError({ 1056 image, 1057 cause: "Image name does not match RockyLinux naming conventions.", 1058 + }), 1059 ); 1060 };