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

run format

+138 -133
+28 -24
src/subcommands/restart.ts
··· 29 29 getInstanceState(name), 30 30 Effect.flatMap((vm) => 31 31 vm ? Effect.succeed(vm) : Effect.fail(new VmNotFoundError({ name })) 32 - ) 32 + ), 33 33 ); 34 34 35 35 const killQemu = (vm: VirtualMachine) => ··· 38 38 success 39 39 ? Effect.succeed(vm) 40 40 : Effect.fail(new KillQemuError({ vmName: vm.name })) 41 - ) 41 + ), 42 42 ); 43 43 44 44 const sleep = (ms: number) => ··· 56 56 const setupFirmware = () => setupFirmwareFilesIfNeeded(); 57 57 58 58 const buildQemuArgs = (vm: VirtualMachine, firmwareArgs: string[]) => { 59 - const qemu = 60 - Deno.build.arch === "aarch64" 61 - ? "qemu-system-aarch64" 62 - : "qemu-system-x86_64"; 59 + const qemu = Deno.build.arch === "aarch64" 60 + ? "qemu-system-aarch64" 61 + : "qemu-system-x86_64"; 63 62 64 63 let coreosArgs: string[] = Effect.runSync(setupCoreOSArgs(vm.drivePath)); 65 64 let alpineArgs: string[] = Effect.runSync(setupAlpineArgs(vm.isoPath)); ··· 101 100 vm.drivePath && [ 102 101 "-drive", 103 102 `file=${vm.drivePath},format=${vm.diskFormat},if=virtio`, 104 - ] 103 + ], 105 104 ), 106 105 ...coreosArgs, 107 106 ...alpineArgs, ··· 109 108 }; 110 109 111 110 const startQemu = (vm: VirtualMachine, qemuArgs: string[]) => { 112 - const qemu = 113 - Deno.build.arch === "aarch64" 114 - ? "qemu-system-aarch64" 115 - : "qemu-system-x86_64"; 111 + const qemu = Deno.build.arch === "aarch64" 112 + ? "qemu-system-aarch64" 113 + : "qemu-system-x86_64"; 116 114 117 115 const logPath = `${LOGS_DIR}/${vm.name}.log`; 118 116 119 117 const fullCommand = vm.bridge 120 - ? `sudo ${qemu} ${qemuArgs 118 + ? `sudo ${qemu} ${ 119 + qemuArgs 121 120 .slice(1) 122 - .join(" ")} >> "${logPath}" 2>&1 & echo $!` 121 + .join(" ") 122 + } >> "${logPath}" 2>&1 & echo $!` 123 123 : `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`; 124 124 125 125 return Effect.tryPromise({ ··· 141 141 const logSuccess = (vm: VirtualMachine, qemuPid: number, logPath: string) => 142 142 Effect.sync(() => { 143 143 console.log( 144 - `${chalk.greenBright(vm.name)} restarted with PID ${chalk.greenBright( 145 - qemuPid 146 - )}.` 144 + `${chalk.greenBright(vm.name)} restarted with PID ${ 145 + chalk.greenBright( 146 + qemuPid, 147 + ) 148 + }.`, 147 149 ); 148 150 console.log(`Logs are being written to ${chalk.blueBright(logPath)}`); 149 151 }); 150 152 151 153 const handleError = ( 152 - error: VmNotFoundError | KillQemuError | CommandError | Error 154 + error: VmNotFoundError | KillQemuError | CommandError | Error, 153 155 ) => 154 156 Effect.sync(() => { 155 157 if (error instanceof VmNotFoundError) { 156 158 console.error( 157 - `Virtual machine with name or ID ${chalk.greenBright( 158 - error.name 159 - )} not found.` 159 + `Virtual machine with name or ID ${ 160 + chalk.greenBright( 161 + error.name, 162 + ) 163 + } not found.`, 160 164 ); 161 165 } else if (error instanceof KillQemuError) { 162 166 console.error( 163 - `Failed to stop virtual machine ${chalk.greenBright(error.vmName)}.` 167 + `Failed to stop virtual machine ${chalk.greenBright(error.vmName)}.`, 164 168 ); 165 169 } else { 166 170 console.error(`An error occurred: ${error}`); ··· 186 190 pipe( 187 191 updateInstanceState(vm.id, "RUNNING", qemuPid), 188 192 Effect.flatMap(() => logSuccess(vm, qemuPid, logPath)), 189 - Effect.flatMap(() => sleep(2000)) 193 + Effect.flatMap(() => sleep(2000)), 190 194 ) 191 - ) 195 + ), 192 196 ) 193 197 ), 194 - Effect.catchAll(handleError) 198 + Effect.catchAll(handleError), 195 199 ); 196 200 197 201 export default async function (name: string) {
+14 -14
src/subcommands/run.ts
··· 22 22 pulledImg 23 23 ? Effect.succeed(pulledImg) 24 24 : Effect.fail(new PullImageError({ cause: "Failed to pull image" })) 25 - ) 25 + ), 26 26 ); 27 - }) 27 + }), 28 28 ); 29 29 30 30 const createVolumeIfNeeded = ( 31 - image: Image 31 + image: Image, 32 32 ): Effect.Effect<[Image, Volume?], Error, never> => 33 33 parseFlags(Deno.args).flags.volume 34 34 ? Effect.gen(function* () { 35 - const size = parseFlags(Deno.args).flags.size as string | undefined; 36 - const volumeName = parseFlags(Deno.args).flags.volume as string; 37 - const volume = yield* getVolume(volumeName); 38 - if (volume) { 39 - return [image, volume]; 40 - } 41 - const newVolume = yield* createVolume(volumeName, image, size); 42 - return [image, newVolume]; 43 - }) 35 + const size = parseFlags(Deno.args).flags.size as string | undefined; 36 + const volumeName = parseFlags(Deno.args).flags.volume as string; 37 + const volume = yield* getVolume(volumeName); 38 + if (volume) { 39 + return [image, volume]; 40 + } 41 + const newVolume = yield* createVolume(volumeName, image, size); 42 + return [image, newVolume]; 43 + }) 44 44 : Effect.succeed([image]); 45 45 46 46 const runImage = ([image, volume]: [Image, Volume?]) => ··· 74 74 console.error(`Failed to run image: ${error.cause} ${image}`); 75 75 Deno.exit(1); 76 76 }) 77 - ) 78 - ) 77 + ), 78 + ), 79 79 ); 80 80 } 81 81
+46 -44
src/subcommands/start.ts
··· 18 18 }> {} 19 19 20 20 export class VmAlreadyRunningError extends Data.TaggedError( 21 - "VmAlreadyRunningError" 21 + "VmAlreadyRunningError", 22 22 )<{ 23 23 name: string; 24 24 }> {} ··· 32 32 getInstanceState(name), 33 33 Effect.flatMap((vm) => 34 34 vm ? Effect.succeed(vm) : Effect.fail(new VmNotFoundError({ name })) 35 - ) 35 + ), 36 36 ); 37 37 38 38 const logStarting = (vm: VirtualMachine) => ··· 45 45 export const setupFirmware = () => setupFirmwareFilesIfNeeded(); 46 46 47 47 export const buildQemuArgs = (vm: VirtualMachine, firmwareArgs: string[]) => { 48 - const qemu = 49 - Deno.build.arch === "aarch64" 50 - ? "qemu-system-aarch64" 51 - : "qemu-system-x86_64"; 48 + const qemu = Deno.build.arch === "aarch64" 49 + ? "qemu-system-aarch64" 50 + : "qemu-system-x86_64"; 52 51 53 52 let coreosArgs: string[] = Effect.runSync(setupCoreOSArgs(vm.drivePath)); 54 53 let alpineArgs: string[] = Effect.runSync(setupAlpineArgs(vm.isoPath)); ··· 90 89 vm.drivePath && [ 91 90 "-drive", 92 91 `file=${vm.drivePath},format=${vm.diskFormat},if=virtio`, 93 - ] 92 + ], 94 93 ), 95 94 ...coreosArgs, 96 95 ...alpineArgs, ··· 107 106 export const startDetachedQemu = ( 108 107 name: string, 109 108 vm: VirtualMachine, 110 - qemuArgs: string[] 109 + qemuArgs: string[], 111 110 ) => { 112 - const qemu = 113 - Deno.build.arch === "aarch64" 114 - ? "qemu-system-aarch64" 115 - : "qemu-system-x86_64"; 111 + const qemu = Deno.build.arch === "aarch64" 112 + ? "qemu-system-aarch64" 113 + : "qemu-system-x86_64"; 116 114 117 115 const logPath = `${LOGS_DIR}/${vm.name}.log`; 118 116 119 117 const fullCommand = vm.bridge 120 - ? `sudo ${qemu} ${qemuArgs 118 + ? `sudo ${qemu} ${ 119 + qemuArgs 121 120 .slice(1) 122 - .join(" ")} >> "${logPath}" 2>&1 & echo $!` 121 + .join(" ") 122 + } >> "${logPath}" 2>&1 & echo $!` 123 123 : `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`; 124 124 125 125 return Effect.tryPromise({ ··· 150 150 Effect.flatMap(({ qemuPid, logPath }) => 151 151 pipe( 152 152 updateInstanceState(name, "RUNNING", qemuPid), 153 - Effect.map(() => ({ vm, qemuPid, logPath })) 153 + Effect.map(() => ({ vm, qemuPid, logPath })), 154 154 ) 155 - ) 155 + ), 156 156 ); 157 157 }; 158 158 ··· 167 167 }) => 168 168 Effect.sync(() => { 169 169 console.log( 170 - `Virtual machine ${vm.name} started in background (PID: ${qemuPid})` 170 + `Virtual machine ${vm.name} started in background (PID: ${qemuPid})`, 171 171 ); 172 172 console.log(`Logs will be written to: ${logPath}`); 173 173 }); ··· 175 175 const startInteractiveQemu = ( 176 176 name: string, 177 177 vm: VirtualMachine, 178 - qemuArgs: string[] 178 + qemuArgs: string[], 179 179 ) => { 180 - const qemu = 181 - Deno.build.arch === "aarch64" 182 - ? "qemu-system-aarch64" 183 - : "qemu-system-x86_64"; 180 + const qemu = Deno.build.arch === "aarch64" 181 + ? "qemu-system-aarch64" 182 + : "qemu-system-x86_64"; 184 183 185 184 return Effect.tryPromise({ 186 185 try: async () => { ··· 216 215 }); 217 216 218 217 export const createVolumeIfNeeded = ( 219 - vm: VirtualMachine 218 + vm: VirtualMachine, 220 219 ): Effect.Effect<[VirtualMachine, Volume?], Error, never> => 221 220 Effect.gen(function* () { 222 221 const { flags } = parseFlags(Deno.args); ··· 231 230 232 231 if (!vm.drivePath) { 233 232 throw new Error( 234 - `Cannot create volume: Virtual machine ${vm.name} has no drivePath defined.` 233 + `Cannot create volume: Virtual machine ${vm.name} has no drivePath defined.`, 235 234 ); 236 235 } 237 236 ··· 274 273 diskFormat: volume ? "qcow2" : vm.diskFormat, 275 274 volume: volume?.path, 276 275 }, 277 - firmwareArgs 276 + firmwareArgs, 278 277 ) 279 278 ), 280 279 Effect.flatMap((qemuArgs) => ··· 284 283 startDetachedQemu(name, { ...vm, volume: volume?.path }, qemuArgs) 285 284 ), 286 285 Effect.tap(logDetachedSuccess), 287 - Effect.map(() => 0) // Exit code 0 286 + Effect.map(() => 0), // Exit code 0 288 287 ) 289 - ) 288 + ), 290 289 ) 291 290 ), 292 - Effect.catchAll(handleError) 291 + Effect.catchAll(handleError), 293 292 ); 294 293 295 294 const startInteractiveEffect = (name: string) => ··· 310 309 diskFormat: volume ? "qcow2" : vm.diskFormat, 311 310 volume: volume?.path, 312 311 }, 313 - firmwareArgs 312 + firmwareArgs, 314 313 ) 315 314 ), 316 315 Effect.flatMap((qemuArgs) => 317 316 startInteractiveQemu(name, { ...vm, volume: volume?.path }, qemuArgs) 318 317 ), 319 - Effect.map((status) => (status.success ? 0 : status.code || 1)) 318 + Effect.map((status) => (status.success ? 0 : status.code || 1)), 320 319 ) 321 320 ), 322 - Effect.catchAll(handleError) 321 + Effect.catchAll(handleError), 323 322 ); 324 323 325 324 export default async function (name: string, detach: boolean = false) { 326 325 const exitCode = await Effect.runPromise( 327 - detach ? startDetachedEffect(name) : startInteractiveEffect(name) 326 + detach ? startDetachedEffect(name) : startInteractiveEffect(name), 328 327 ); 329 328 330 329 if (detach) { ··· 338 337 const { flags } = parseFlags(Deno.args); 339 338 return { 340 339 ...vm, 341 - memory: 342 - flags.memory || flags.m ? String(flags.memory || flags.m) : vm.memory, 340 + memory: flags.memory || flags.m 341 + ? String(flags.memory || flags.m) 342 + : vm.memory, 343 343 cpus: flags.cpus || flags.C ? Number(flags.cpus || flags.C) : vm.cpus, 344 344 cpu: flags.cpu || flags.c ? String(flags.cpu || flags.c) : vm.cpu, 345 345 diskFormat: flags.diskFormat ? String(flags.diskFormat) : vm.diskFormat, 346 - portForward: 347 - flags.portForward || flags.p 348 - ? String(flags.portForward || flags.p) 349 - : vm.portForward, 350 - drivePath: 351 - flags.image || flags.i ? String(flags.image || flags.i) : vm.drivePath, 352 - bridge: 353 - flags.bridge || flags.b ? String(flags.bridge || flags.b) : vm.bridge, 354 - diskSize: 355 - flags.size || flags.s ? String(flags.size || flags.s) : vm.diskSize, 346 + portForward: flags.portForward || flags.p 347 + ? String(flags.portForward || flags.p) 348 + : vm.portForward, 349 + drivePath: flags.image || flags.i 350 + ? String(flags.image || flags.i) 351 + : vm.drivePath, 352 + bridge: flags.bridge || flags.b 353 + ? String(flags.bridge || flags.b) 354 + : vm.bridge, 355 + diskSize: flags.size || flags.s 356 + ? String(flags.size || flags.s) 357 + : vm.diskSize, 356 358 }; 357 359 }
+50 -51
src/utils.ts
··· 70 70 export const isValidISOurl = (url?: string): boolean => { 71 71 return Boolean( 72 72 (url?.startsWith("http://") || url?.startsWith("https://")) && 73 - url?.endsWith(".iso") 73 + url?.endsWith(".iso"), 74 74 ); 75 75 }; 76 76 ··· 96 96 }); 97 97 98 98 export const validateImage = ( 99 - image: string 99 + image: string, 100 100 ): Effect.Effect<string, InvalidImageNameError, never> => { 101 101 const regex = 102 102 /^(?:[a-zA-Z0-9.-]+(?:\.[a-zA-Z0-9.-]+)*\/)?[a-z0-9]+(?:[._-][a-z0-9]+)*\/[a-z0-9]+(?:[._-][a-z0-9]+)*(?::[a-zA-Z0-9._-]+)?$/; ··· 107 107 image, 108 108 cause: 109 109 "Image name does not conform to expected format. Should be in the format 'repository/name:tag'.", 110 - }) 110 + }), 111 111 ); 112 112 } 113 113 return Effect.succeed(image); ··· 116 116 export const extractTag = (name: string) => 117 117 pipe( 118 118 validateImage(name), 119 - Effect.flatMap((image) => Effect.succeed(image.split(":")[1] || "latest")) 119 + Effect.flatMap((image) => Effect.succeed(image.split(":")[1] || "latest")), 120 120 ); 121 121 122 122 export const failOnMissingImage = ( 123 - image: Image | undefined 123 + image: Image | undefined, 124 124 ): Effect.Effect<Image, Error, never> => 125 125 image 126 126 ? Effect.succeed(image) 127 127 : Effect.fail(new NoSuchImageError({ cause: "No such image" })); 128 128 129 129 export const du = ( 130 - path: string 130 + path: string, 131 131 ): Effect.Effect<number, LogCommandError, never> => 132 132 Effect.tryPromise({ 133 133 try: async () => { ··· 159 159 exists 160 160 ? Effect.succeed(true) 161 161 : du(path).pipe(Effect.map((size) => size < EMPTY_DISK_THRESHOLD_KB)) 162 - ) 162 + ), 163 163 ); 164 164 165 165 export const downloadIso = (url: string, options: Options) => ··· 181 181 if (driveSize > EMPTY_DISK_THRESHOLD_KB) { 182 182 console.log( 183 183 chalk.yellowBright( 184 - `Drive image ${options.image} is not empty (size: ${driveSize} KB), skipping ISO download to avoid overwriting existing data.` 185 - ) 184 + `Drive image ${options.image} is not empty (size: ${driveSize} KB), skipping ISO download to avoid overwriting existing data.`, 185 + ), 186 186 ); 187 187 return null; 188 188 } ··· 200 200 if (outputExists) { 201 201 console.log( 202 202 chalk.yellowBright( 203 - `File ${outputPath} already exists, skipping download.` 204 - ) 203 + `File ${outputPath} already exists, skipping download.`, 204 + ), 205 205 ); 206 206 return outputPath; 207 207 } ··· 250 250 if (!success) { 251 251 console.error( 252 252 chalk.redBright( 253 - "Failed to get QEMU prefix from Homebrew. Ensure QEMU is installed via Homebrew." 254 - ) 253 + "Failed to get QEMU prefix from Homebrew. Ensure QEMU is installed via Homebrew.", 254 + ), 255 255 ); 256 256 Deno.exit(1); 257 257 } ··· 264 264 try: () => 265 265 Deno.copyFile( 266 266 `${brewPrefix}/share/qemu/edk2-arm-vars.fd`, 267 - edk2VarsAarch64 267 + edk2VarsAarch64, 268 268 ), 269 269 catch: (error) => new LogCommandError({ cause: error }), 270 270 }); ··· 309 309 const configOK = yield* pipe( 310 310 fileExists("config.ign"), 311 311 Effect.flatMap(() => Effect.succeed(true)), 312 - Effect.catchAll(() => Effect.succeed(false)) 312 + Effect.catchAll(() => Effect.succeed(false)), 313 313 ); 314 314 if (!configOK) { 315 315 console.error( 316 316 chalk.redBright( 317 - "CoreOS image requires a config.ign file in the current directory." 318 - ) 317 + "CoreOS image requires a config.ign file in the current directory.", 318 + ), 319 319 ); 320 320 Deno.exit(1); 321 321 } ··· 350 350 imagePath && 351 351 imagePath.endsWith(".qcow2") && 352 352 imagePath.startsWith( 353 - `di-${Deno.build.arch === "aarch64" ? "arm64" : "amd64"}-console-` 353 + `di-${Deno.build.arch === "aarch64" ? "arm64" : "amd64"}-console-`, 354 354 ) 355 355 ) { 356 356 return ["-drive", `file=${imagePath},format=qcow2,if=virtio`]; ··· 381 381 Effect.gen(function* () { 382 382 const macAddress = yield* generateRandomMacAddress(); 383 383 384 - const qemu = 385 - Deno.build.arch === "aarch64" 386 - ? "qemu-system-aarch64" 387 - : "qemu-system-x86_64"; 384 + const qemu = Deno.build.arch === "aarch64" 385 + ? "qemu-system-aarch64" 386 + : "qemu-system-x86_64"; 388 387 389 388 const firmwareFiles = yield* setupFirmwareFilesIfNeeded(); 390 389 let coreosArgs: string[] = yield* setupCoreOSArgs(isoPath || options.image); ··· 442 441 options.image && [ 443 442 "-drive", 444 443 `file=${options.image},format=${options.diskFormat},if=virtio`, 445 - ] 444 + ], 446 445 ), 447 446 ]; 448 447 ··· 457 456 const logPath = `${LOGS_DIR}/${name}.log`; 458 457 459 458 const fullCommand = options.bridge 460 - ? `sudo ${qemu} ${qemuArgs 459 + ? `sudo ${qemu} ${ 460 + qemuArgs 461 461 .slice(1) 462 - .join(" ")} >> "${logPath}" 2>&1 & echo $!` 462 + .join(" ") 463 + } >> "${logPath}" 2>&1 & echo $!` 463 464 : `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`; 464 465 465 466 const { stdout } = yield* Effect.tryPromise({ ··· 485 486 cpus: options.cpus, 486 487 cpu: options.cpu, 487 488 diskSize: options.size || "20G", 488 - diskFormat: 489 - (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || 489 + diskFormat: (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || 490 490 options.diskFormat || 491 491 "raw", 492 492 portForward: options.portForward, ··· 505 505 }); 506 506 507 507 console.log( 508 - `Virtual machine ${name} started in background (PID: ${qemuPid})` 508 + `Virtual machine ${name} started in background (PID: ${qemuPid})`, 509 509 ); 510 510 console.log(`Logs will be written to: ${logPath}`); 511 511 ··· 528 528 cpus: options.cpus, 529 529 cpu: options.cpu, 530 530 diskSize: options.size || "20G", 531 - diskFormat: 532 - (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || 531 + diskFormat: (isoPath?.endsWith(".qcow2") ? "qcow2" : undefined) || 533 532 options.diskFormat || 534 533 "raw", 535 534 portForward: options.portForward, ··· 637 636 if (pathExists) { 638 637 console.log( 639 638 chalk.yellowBright( 640 - `Drive image ${path} already exists, skipping creation.` 641 - ) 639 + `Drive image ${path} already exists, skipping creation.`, 640 + ), 642 641 ); 643 642 return; 644 643 } ··· 665 664 }); 666 665 667 666 export const fileExists = ( 668 - path: string 667 + path: string, 669 668 ): Effect.Effect<void, NoSuchFileError, never> => 670 669 Effect.try({ 671 670 try: () => Deno.statSync(path), ··· 673 672 }); 674 673 675 674 export const constructCoreOSImageURL = ( 676 - image: string 675 + image: string, 677 676 ): Effect.Effect<string, InvalidImageNameError, never> => { 678 677 // detect with regex if image matches coreos pattern: fedora-coreos or fedora-coreos-<version> or coreos or coreos-<version> 679 678 const coreosRegex = /^(fedora-coreos|coreos)(-(\d+\.\d+\.\d+\.\d+))?$/; ··· 681 680 if (match) { 682 681 const version = match[3] || FEDORA_COREOS_DEFAULT_VERSION; 683 682 return Effect.succeed( 684 - FEDORA_COREOS_IMG_URL.replaceAll(FEDORA_COREOS_DEFAULT_VERSION, version) 683 + FEDORA_COREOS_IMG_URL.replaceAll(FEDORA_COREOS_DEFAULT_VERSION, version), 685 684 ); 686 685 } 687 686 ··· 689 688 new InvalidImageNameError({ 690 689 image, 691 690 cause: "Image name does not match CoreOS naming conventions.", 692 - }) 691 + }), 693 692 ); 694 693 }; 695 694 ··· 718 717 }); 719 718 720 719 export const constructNixOSImageURL = ( 721 - image: string 720 + image: string, 722 721 ): Effect.Effect<string, InvalidImageNameError, never> => { 723 722 // detect with regex if image matches NixOS pattern: nixos or nixos-<version> 724 723 const nixosRegex = /^(nixos)(-(\d+\.\d+))?$/; ··· 726 725 if (match) { 727 726 const version = match[3] || NIXOS_DEFAULT_VERSION; 728 727 return Effect.succeed( 729 - NIXOS_ISO_URL.replaceAll(NIXOS_DEFAULT_VERSION, version) 728 + NIXOS_ISO_URL.replaceAll(NIXOS_DEFAULT_VERSION, version), 730 729 ); 731 730 } 732 731 ··· 734 733 new InvalidImageNameError({ 735 734 image, 736 735 cause: "Image name does not match NixOS naming conventions.", 737 - }) 736 + }), 738 737 ); 739 738 }; 740 739 741 740 export const constructFedoraImageURL = ( 742 - image: string 741 + image: string, 743 742 ): Effect.Effect<string, InvalidImageNameError, never> => { 744 743 // detect with regex if image matches Fedora pattern: fedora 745 744 const fedoraRegex = /^(fedora)$/; ··· 752 751 new InvalidImageNameError({ 753 752 image, 754 753 cause: "Image name does not match Fedora naming conventions.", 755 - }) 754 + }), 756 755 ); 757 756 }; 758 757 759 758 export const constructGentooImageURL = ( 760 - image: string 759 + image: string, 761 760 ): Effect.Effect<string, InvalidImageNameError, never> => { 762 761 // detect with regex if image matches genroo pattern: gentoo-20251116T161545Z or gentoo 763 762 const gentooRegex = /^(gentoo)(-(\d{8}T\d{6}Z))?$/; ··· 766 765 return Effect.succeed( 767 766 GENTOO_IMG_URL.replaceAll("20251116T161545Z", match[3]).replaceAll( 768 767 "20251116T233105Z", 769 - match[3] 770 - ) 768 + match[3], 769 + ), 771 770 ); 772 771 } 773 772 ··· 779 778 new InvalidImageNameError({ 780 779 image, 781 780 cause: "Image name does not match Gentoo naming conventions.", 782 - }) 781 + }), 783 782 ); 784 783 }; 785 784 786 785 export const constructDebianImageURL = ( 787 - image: string 786 + image: string, 788 787 ): Effect.Effect<string, InvalidImageNameError, never> => { 789 788 // detect with regex if image matches debian pattern: debian-<version> or debian 790 789 const debianRegex = /^(debian)(-(\d+\.\d+\.\d+))?$/; 791 790 const match = image.match(debianRegex); 792 791 if (match?.[3]) { 793 792 return Effect.succeed( 794 - DEBIAN_ISO_URL.replaceAll(DEBIAN_DEFAULT_VERSION, match[3]) 793 + DEBIAN_ISO_URL.replaceAll(DEBIAN_DEFAULT_VERSION, match[3]), 795 794 ); 796 795 } 797 796 ··· 803 802 new InvalidImageNameError({ 804 803 image, 805 804 cause: "Image name does not match Debian naming conventions.", 806 - }) 805 + }), 807 806 ); 808 807 }; 809 808 810 809 export const constructAlpineImageURL = ( 811 - image: string 810 + image: string, 812 811 ): Effect.Effect<string, InvalidImageNameError, never> => { 813 812 // detect with regex if image matches alpine pattern: alpine-<version> or alpine 814 813 const alpineRegex = /^(alpine)(-(\d+\.\d+(\.\d+)?))?$/; 815 814 const match = image.match(alpineRegex); 816 815 if (match?.[3]) { 817 816 return Effect.succeed( 818 - ALPINE_ISO_URL.replaceAll(ALPINE_DEFAULT_VERSION, match[3]) 817 + ALPINE_ISO_URL.replaceAll(ALPINE_DEFAULT_VERSION, match[3]), 819 818 ); 820 819 } 821 820 ··· 827 826 new InvalidImageNameError({ 828 827 image, 829 828 cause: "Image name does not match Alpine naming conventions.", 830 - }) 829 + }), 831 830 ); 832 831 };