A convenient CLI tool to quickly spin up DragonflyBSD virtual machines using QEMU with sensible defaults.
1import { parseFlags } from "@cliffy/flags";
2import _ from "@es-toolkit/es-toolkit/compat";
3import * as toml from "@std/toml";
4import z from "@zod/zod";
5import { Data, Effect } from "effect";
6import {
7 constructDownloadUrl,
8 DEFAULT_VERSION,
9 type Options,
10} from "./utils.ts";
11
12export const VmConfigSchema = z.object({
13 vm: z.object({
14 iso: z.string(),
15 output: z.string(),
16 cpu: z.string(),
17 cpus: z.number(),
18 memory: z.string(),
19 image: z.string(),
20 disk_format: z.enum(["qcow2", "raw"]),
21 size: z.string(),
22 }).partial(),
23 network: z.object({
24 bridge: z.string(),
25 port_forward: z.string(),
26 }).partial(),
27 options: z.object({
28 detach: z.boolean(),
29 }).partial(),
30});
31
32export type VmConfig = z.infer<typeof VmConfigSchema>;
33
34class VmConfigError extends Data.TaggedError("VmConfigError")<{
35 cause?: string;
36}> {}
37
38export const initVmFile = (
39 path: string,
40): Effect.Effect<void, VmConfigError, never> =>
41 Effect.tryPromise({
42 try: async () => {
43 const defaultConfig: VmConfig = {
44 vm: {
45 iso: constructDownloadUrl(DEFAULT_VERSION),
46 cpu: Deno.build.os === "darwin" && Deno.build.arch === "aarch64"
47 ? "max"
48 : "host",
49 cpus: 2,
50 memory: "2G",
51 },
52 network: {
53 port_forward: "2222:22",
54 },
55 options: {
56 detach: false,
57 },
58 };
59 const tomlString = toml.stringify(defaultConfig);
60 await Deno.writeTextFile(path, tomlString);
61 },
62 catch: (error) => new VmConfigError({ cause: String(error) }),
63 });
64
65export const parseVmFile = (
66 path: string,
67): Effect.Effect<VmConfig, VmConfigError, never> =>
68 Effect.tryPromise({
69 try: async () => {
70 const fileContent = await Deno.readTextFile(path);
71 const parsedToml = toml.parse(fileContent);
72 return VmConfigSchema.parse(parsedToml);
73 },
74 catch: (error) => new VmConfigError({ cause: String(error) }),
75 });
76
77export const mergeConfig = (
78 config: VmConfig | null,
79 options: Options,
80): Effect.Effect<Options, never, never> => {
81 const { flags } = parseFlags(Deno.args);
82 const defaultConfig: VmConfig = {
83 vm: {
84 iso: _.get(config, "vm.iso"),
85 cpu: _.get(
86 config,
87 "vm.cpu",
88 Deno.build.os === "darwin" && Deno.build.arch === "aarch64"
89 ? "max"
90 : "host",
91 ),
92 cpus: _.get(config, "vm.cpus", 2),
93 memory: _.get(config, "vm.memory", "2G"),
94 image: _.get(config, "vm.image", options.image),
95 disk_format: _.get(config, "vm.disk_format", "raw"),
96 size: _.get(config, "vm.size", "20G"),
97 },
98 network: {
99 bridge: _.get(config, "network.bridge"),
100 port_forward: _.get(config, "network.port_forward", "2222:22"),
101 },
102 options: {
103 detach: _.get(config, "options.detach", false),
104 },
105 };
106 return Effect.succeed({
107 memory: _.get(flags, "memory", defaultConfig.vm.memory!) as string,
108 cpus: _.get(flags, "cpus", defaultConfig.vm.cpus!) as number,
109 cpu: _.get(flags, "cpu", defaultConfig.vm.cpu!) as string,
110 diskFormat: _.get(
111 flags,
112 "diskFormat",
113 defaultConfig.vm.disk_format!,
114 ) as string,
115 portForward: _.get(
116 flags,
117 "portForward",
118 defaultConfig.network.port_forward!,
119 ) as string,
120 image: _.get(flags, "image", defaultConfig.vm.image!) as string,
121 bridge: _.get(flags, "bridge", defaultConfig.network.bridge!) as string,
122 size: _.get(flags, "size", defaultConfig.vm.size!) as string,
123 install: _.get(flags, "install", false) as boolean,
124 });
125};