+2
-1
deno.json
+2
-1
deno.json
+7
-2
deno.lock
+7
-2
deno.lock
···
9
9
"jsr:@std/fmt@~1.0.2": "1.0.8",
10
10
"jsr:@std/internal@^1.0.12": "1.0.12",
11
11
"jsr:@std/text@~1.0.7": "1.0.16",
12
-
"npm:chalk@^5.6.2": "5.6.2"
12
+
"npm:chalk@^5.6.2": "5.6.2",
13
+
"npm:lodash@^4.17.21": "4.17.21"
13
14
},
14
15
"jsr": {
15
16
"@cliffy/command@1.0.0-rc.8": {
···
56
57
"npm": {
57
58
"chalk@5.6.2": {
58
59
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="
60
+
},
61
+
"lodash@4.17.21": {
62
+
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
59
63
}
60
64
},
61
65
"workspace": {
62
66
"dependencies": [
63
67
"jsr:@cliffy/command@^1.0.0-rc.8",
64
68
"jsr:@std/assert@1",
65
-
"npm:chalk@^5.6.2"
69
+
"npm:chalk@^5.6.2",
70
+
"npm:lodash@^4.17.21"
66
71
]
67
72
}
68
73
}
+15
-153
main.ts
+15
-153
main.ts
···
1
1
#!/usr/bin/env -S deno run --allow-run --allow-read --allow-env
2
2
3
3
import { Command } from "@cliffy/command";
4
-
import chalk from "chalk";
5
-
6
-
const DEFAULT_VERSION = "20251026";
7
-
8
-
interface Options {
9
-
output?: string;
10
-
cpu: string;
11
-
cpus: number;
12
-
memory: string;
13
-
drive?: string;
14
-
diskFormat: string;
15
-
size: string;
16
-
}
17
-
18
-
async function downloadIso(url: string, outputPath?: string): Promise<string> {
19
-
const filename = url.split("/").pop()!;
20
-
outputPath = outputPath ?? filename;
21
-
22
-
if (await Deno.stat(outputPath).catch(() => false)) {
23
-
console.log(
24
-
chalk.yellowBright(
25
-
`File ${outputPath} already exists, skipping download.`,
26
-
),
27
-
);
28
-
return outputPath;
29
-
}
30
-
31
-
const cmd = new Deno.Command("curl", {
32
-
args: ["-L", "-o", outputPath, url],
33
-
stdin: "inherit",
34
-
stdout: "inherit",
35
-
stderr: "inherit",
36
-
});
37
-
38
-
const status = await cmd.spawn().status;
39
-
if (!status.success) {
40
-
console.error(chalk.redBright("Failed to download ISO image."));
41
-
Deno.exit(status.code);
42
-
}
43
-
44
-
console.log(chalk.greenBright(`Downloaded ISO to ${outputPath}`));
45
-
return outputPath;
46
-
}
47
-
48
-
function constructDownloadUrl(version: string): string {
49
-
return `https://dlc.openindiana.org/isos/hipster/${version}/OI-hipster-text-${version}.iso`;
50
-
}
51
-
52
-
async function runQemu(isoPath: string, options: Options): Promise<void> {
53
-
const cmd = new Deno.Command("qemu-system-x86_64", {
54
-
args: [
55
-
"-enable-kvm",
56
-
"-cpu",
57
-
options.cpu,
58
-
"-m",
59
-
options.memory,
60
-
"-smp",
61
-
options.cpus.toString(),
62
-
"-cdrom",
63
-
isoPath,
64
-
"-netdev",
65
-
"user,id=net0,hostfwd=tcp::2222-:22",
66
-
"-device",
67
-
"e1000,netdev=net0",
68
-
"-nographic",
69
-
"-monitor",
70
-
"none",
71
-
"-chardev",
72
-
"stdio,id=con0,signal=off",
73
-
"-serial",
74
-
"chardev:con0",
75
-
...(options.drive
76
-
? [
77
-
"-drive",
78
-
`file=${options.drive},format=${options.diskFormat},if=virtio`,
79
-
]
80
-
: []),
81
-
],
82
-
stdin: "inherit",
83
-
stdout: "inherit",
84
-
stderr: "inherit",
85
-
});
86
-
87
-
const status = await cmd.spawn().status;
88
-
89
-
if (!status.success) {
90
-
Deno.exit(status.code);
91
-
}
92
-
}
93
-
94
-
function handleInput(input?: string): string {
95
-
if (!input) {
96
-
console.log(
97
-
`No ISO path provided, defaulting to ${chalk.cyan("OpenIndiana")} ${
98
-
chalk.cyan(DEFAULT_VERSION)
99
-
}...`,
100
-
);
101
-
return constructDownloadUrl(DEFAULT_VERSION);
102
-
}
103
-
104
-
const versionRegex = /^\d{8}$/;
105
-
106
-
if (versionRegex.test(input)) {
107
-
console.log(
108
-
`Detected version ${chalk.cyan(input)}, constructing download URL...`,
109
-
);
110
-
return constructDownloadUrl(input);
111
-
}
112
-
113
-
return input;
114
-
}
115
-
116
-
async function createDriveImageIfNeeded(
117
-
{
118
-
drive: path,
119
-
diskFormat: format,
120
-
size,
121
-
}: Options,
122
-
): Promise<void> {
123
-
if (await Deno.stat(path!).catch(() => false)) {
124
-
console.log(
125
-
chalk.yellowBright(
126
-
`Drive image ${path} already exists, skipping creation.`,
127
-
),
128
-
);
129
-
return;
130
-
}
131
-
132
-
const cmd = new Deno.Command("qemu-img", {
133
-
args: ["create", "-f", format, path!, size],
134
-
stdin: "inherit",
135
-
stdout: "inherit",
136
-
stderr: "inherit",
137
-
});
138
-
139
-
const status = await cmd.spawn().status;
140
-
if (!status.success) {
141
-
console.error(chalk.redBright("Failed to create drive image."));
142
-
Deno.exit(status.code);
143
-
}
144
-
145
-
console.log(chalk.greenBright(`Created drive image at ${path}`));
146
-
}
4
+
import {
5
+
createDriveImageIfNeeded,
6
+
downloadIso,
7
+
emptyDiskImage,
8
+
handleInput,
9
+
Options,
10
+
runQemu,
11
+
} from "./utils.ts";
147
12
148
13
if (import.meta.main) {
149
14
await new Command()
···
196
61
)
197
62
.action(async (options: Options, input?: string) => {
198
63
const resolvedInput = handleInput(input);
199
-
let isoPath = resolvedInput;
64
+
let isoPath: string | null = resolvedInput;
200
65
201
66
if (
202
67
resolvedInput.startsWith("https://") ||
203
68
resolvedInput.startsWith("http://")
204
69
) {
205
-
isoPath = await downloadIso(resolvedInput, options.output);
70
+
isoPath = await downloadIso(resolvedInput, options);
206
71
}
207
72
208
73
if (options.drive) {
209
74
await createDriveImageIfNeeded(options);
210
75
}
211
76
212
-
await runQemu(isoPath, {
213
-
cpu: options.cpu,
214
-
memory: options.memory,
215
-
cpus: options.cpus,
216
-
drive: options.drive,
217
-
diskFormat: options.diskFormat,
218
-
size: options.size,
219
-
});
77
+
if (!input && options.drive && !await emptyDiskImage(options.drive)) {
78
+
isoPath = null;
79
+
}
80
+
81
+
await runQemu(isoPath, options);
220
82
})
221
83
.parse(Deno.args);
222
84
}
+183
utils.ts
+183
utils.ts
···
1
+
import chalk from "chalk";
2
+
import _ from "lodash";
3
+
4
+
const DEFAULT_VERSION = "20251026";
5
+
6
+
export interface Options {
7
+
output?: string;
8
+
cpu: string;
9
+
cpus: number;
10
+
memory: string;
11
+
drive?: string;
12
+
diskFormat: string;
13
+
size: string;
14
+
}
15
+
16
+
async function du(path: string): Promise<number> {
17
+
const cmd = new Deno.Command("du", {
18
+
args: [path],
19
+
stdout: "piped",
20
+
stderr: "inherit",
21
+
});
22
+
23
+
const { stdout } = await cmd.spawn().output();
24
+
const output = new TextDecoder().decode(stdout).trim();
25
+
const size = parseInt(output.split("\t")[0], 10);
26
+
return size;
27
+
}
28
+
29
+
export async function emptyDiskImage(path: string): Promise<boolean> {
30
+
if (!await Deno.stat(path).catch(() => false)) {
31
+
return true;
32
+
}
33
+
34
+
const size = await du(path);
35
+
return size < 10;
36
+
}
37
+
38
+
export async function downloadIso(
39
+
url: string,
40
+
options: Options,
41
+
): Promise<string | null> {
42
+
const filename = url.split("/").pop()!;
43
+
const outputPath = options.output ?? filename;
44
+
45
+
if (options.drive && await Deno.stat(options.drive).catch(() => false)) {
46
+
const driveSize = await du(options.drive);
47
+
if (driveSize > 10) {
48
+
console.log(
49
+
chalk.yellowBright(
50
+
`Drive image ${options.drive} is not empty (size: ${driveSize} KB), skipping ISO download to avoid overwriting existing data.`,
51
+
),
52
+
);
53
+
return null;
54
+
}
55
+
}
56
+
57
+
if (await Deno.stat(outputPath).catch(() => false)) {
58
+
console.log(
59
+
chalk.yellowBright(
60
+
`File ${outputPath} already exists, skipping download.`,
61
+
),
62
+
);
63
+
return outputPath;
64
+
}
65
+
66
+
const cmd = new Deno.Command("curl", {
67
+
args: ["-L", "-o", outputPath, url],
68
+
stdin: "inherit",
69
+
stdout: "inherit",
70
+
stderr: "inherit",
71
+
});
72
+
73
+
const status = await cmd.spawn().status;
74
+
if (!status.success) {
75
+
console.error(chalk.redBright("Failed to download ISO image."));
76
+
Deno.exit(status.code);
77
+
}
78
+
79
+
console.log(chalk.greenBright(`Downloaded ISO to ${outputPath}`));
80
+
return outputPath;
81
+
}
82
+
83
+
export function constructDownloadUrl(version: string): string {
84
+
return `https://dlc.openindiana.org/isos/hipster/${version}/OI-hipster-text-${version}.iso`;
85
+
}
86
+
87
+
export async function runQemu(
88
+
isoPath: string | null,
89
+
options: Options,
90
+
): Promise<void> {
91
+
const cmd = new Deno.Command("qemu-system-x86_64", {
92
+
args: [
93
+
"-enable-kvm",
94
+
"-cpu",
95
+
options.cpu,
96
+
"-m",
97
+
options.memory,
98
+
"-smp",
99
+
options.cpus.toString(),
100
+
..._.compact([isoPath && "-cdrom", isoPath]),
101
+
"-netdev",
102
+
"user,id=net0,hostfwd=tcp::2222-:22",
103
+
"-device",
104
+
"e1000,netdev=net0",
105
+
"-nographic",
106
+
"-monitor",
107
+
"none",
108
+
"-chardev",
109
+
"stdio,id=con0,signal=off",
110
+
"-serial",
111
+
"chardev:con0",
112
+
..._.compact(
113
+
options.drive && [
114
+
"-drive",
115
+
`file=${options.drive},format=${options.diskFormat},if=virtio`,
116
+
],
117
+
),
118
+
],
119
+
stdin: "inherit",
120
+
stdout: "inherit",
121
+
stderr: "inherit",
122
+
});
123
+
124
+
const status = await cmd.spawn().status;
125
+
126
+
if (!status.success) {
127
+
Deno.exit(status.code);
128
+
}
129
+
}
130
+
131
+
export function handleInput(input?: string): string {
132
+
if (!input) {
133
+
console.log(
134
+
`No ISO path provided, defaulting to ${chalk.cyan("OpenIndiana")} ${
135
+
chalk.cyan(DEFAULT_VERSION)
136
+
}...`,
137
+
);
138
+
return constructDownloadUrl(DEFAULT_VERSION);
139
+
}
140
+
141
+
const versionRegex = /^\d{8}$/;
142
+
143
+
if (versionRegex.test(input)) {
144
+
console.log(
145
+
`Detected version ${chalk.cyan(input)}, constructing download URL...`,
146
+
);
147
+
return constructDownloadUrl(input);
148
+
}
149
+
150
+
return input;
151
+
}
152
+
153
+
export async function createDriveImageIfNeeded(
154
+
{
155
+
drive: path,
156
+
diskFormat: format,
157
+
size,
158
+
}: Options,
159
+
): Promise<void> {
160
+
if (await Deno.stat(path!).catch(() => false)) {
161
+
console.log(
162
+
chalk.yellowBright(
163
+
`Drive image ${path} already exists, skipping creation.`,
164
+
),
165
+
);
166
+
return;
167
+
}
168
+
169
+
const cmd = new Deno.Command("qemu-img", {
170
+
args: ["create", "-f", format, path!, size],
171
+
stdin: "inherit",
172
+
stdout: "inherit",
173
+
stderr: "inherit",
174
+
});
175
+
176
+
const status = await cmd.spawn().status;
177
+
if (!status.success) {
178
+
console.error(chalk.redBright("Failed to create drive image."));
179
+
Deno.exit(status.code);
180
+
}
181
+
182
+
console.log(chalk.greenBright(`Created drive image at ${path}`));
183
+
}