vmsan 0.1.0-alpha.24 → 0.1.0-alpha.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,26 +29,30 @@ Create, manage, and connect to isolated [Firecracker](https://github.com/firecra
29
29
  - 📂 **File transfer** — upload and download without SSH
30
30
  - 🐳 **Docker images** — build rootfs from any OCI image with `--from-image`
31
31
  - 🏃 **Command execution** — run commands with streaming output, env injection, and sudo
32
- - 🧩 **Multiple runtimes** — `base`, `node22`, `python3.13`
32
+ - 🧩 **Multiple runtimes** — `base`, `node22`, `node24`, `python3.13`
33
33
  - 📸 **VM snapshots** — save and restore VM state
34
34
  - 📊 **JSON output** — `--json` flag for scripting and automation
35
35
 
36
36
  ## 📋 Prerequisites
37
37
 
38
38
  - Linux (x86_64 or aarch64) with KVM support
39
- - [Bun](https://bun.sh) >= 1.2
40
- - [Go](https://go.dev) >= 1.22 (to build the in-VM agent)
41
39
  - Root/sudo access (required for TAP device networking and jailer)
42
- - `squashfs-tools` (for rootfs conversion during install)
40
+ - Docker (for building runtime images and `--from-image`)
43
41
 
44
42
  ## 🚀 Install
45
43
 
46
- ### 1. Install Firecracker, kernel, and rootfs
47
-
48
44
  ```bash
49
45
  curl -fsSL https://vmsan.dev/install | bash
50
46
  ```
51
47
 
48
+ This downloads and installs everything into `~/.vmsan/`:
49
+
50
+ - Firecracker + Jailer (latest release)
51
+ - Linux kernel (vmlinux 6.1)
52
+ - Ubuntu 24.04 rootfs (converted from squashfs to ext4)
53
+ - vmsan CLI + in-VM agent
54
+ - Runtime images (node22, node24, python3.13)
55
+
52
56
  <details>
53
57
  <summary>Uninstall</summary>
54
58
 
@@ -58,53 +62,27 @@ curl -fsSL https://vmsan.dev/install | bash -s -- --uninstall
58
62
 
59
63
  </details>
60
64
 
61
- This downloads and installs into `~/.vmsan/`:
62
-
63
- - Firecracker + Jailer (latest release)
64
- - Linux kernel (vmlinux 6.1)
65
- - Ubuntu 24.04 rootfs (converted from squashfs to ext4)
66
-
67
- ### 2. Install vmsan CLI
68
-
69
- <!-- automd:pm-install -->
70
-
71
- ```sh
72
- # ✨ Auto-detect
73
- npx nypm install vmsan
74
-
75
- # npm
76
- npm install vmsan
77
-
78
- # yarn
79
- yarn add vmsan
80
-
81
- # pnpm
82
- pnpm add vmsan
83
-
84
- # bun
85
- bun install vmsan
86
-
87
- # deno
88
- deno install npm:vmsan
89
- ```
90
-
91
- <!-- /automd -->
65
+ <details>
66
+ <summary>Development setup</summary>
92
67
 
93
- ### 3. Build the in-VM agent
68
+ If you want to build from source:
94
69
 
95
70
  ```bash
96
- cd agent
97
- make install
98
- cd ..
99
- ```
71
+ # Install dependencies
72
+ bun install
100
73
 
101
- ### Link globally (optional)
74
+ # Build the in-VM agent
75
+ cd agent && make install && cd ..
102
76
 
103
- ```bash
104
- bun link
77
+ # Build the CLI
78
+ bun run build
79
+
80
+ # Link local build
81
+ mkdir -p ~/.vmsan/bin
82
+ ln -sf "$(pwd)/dist/bin/cli.mjs" ~/.vmsan/bin/vmsan
105
83
  ```
106
84
 
107
- This makes the `vmsan` command available system-wide.
85
+ </details>
108
86
 
109
87
  ## 📖 Usage
110
88
 
@@ -1,7 +1,6 @@
1
1
  import { n as vmsanPaths } from "./paths.mjs";
2
2
  import { l as handleCommandError } from "./errors.mjs";
3
3
  import { t as createCommandLogger } from "./logger.mjs";
4
- import "./vm-state.mjs";
5
4
  import { n as waitForAgent, t as resolveVmState } from "./vm-context.mjs";
6
5
  import { t as ShellSession } from "./shell.mjs";
7
6
  import { consola } from "consola";
@@ -1,8 +1,9 @@
1
1
  import { n as vmsanPaths } from "./paths.mjs";
2
- import { A as vmNotRunningError, B as invalidDomainPatternError, D as snapshotNotFoundError, F as invalidCidrOctetError, G as invalidNetworkPolicyError, H as invalidImageRefEmptyError, I as invalidCidrPrefixError, J as mutuallyExclusiveFlagsError, K as invalidPortError, L as invalidDiskSizeFormatError, P as invalidCidrFormatError, R as invalidDiskSizeRangeError, T as chrootNotFoundError, U as invalidImageRefTagError, W as invalidIntegerFlagError, X as portConflictError, Z as VmsanError, a as cloudflareNotConfiguredError, c as cloudflaredStartFailedError, d as missingBinaryError, f as noExt4RootfsError, h as noRootfsDirError, i as cloudflareNoZoneError, j as vmNotStoppedError, k as vmNotFoundError, m as noKernelError, n as cloudflareConfigNotFoundError, o as cloudflareTunnelNoIdError, p as noKernelDirError, q as invalidRuntimeError, r as cloudflareNoAccountsError, s as cloudflaredNotFoundError, v as lockTimeoutError, x as defaultInterfaceNotFoundError, y as socketTimeoutError, z as invalidDomainError } from "./errors.mjs";
3
- import { c as safeKill, i as generateVmId, l as sleepSync, m as writeSecure, o as mkdirSecure, p as toError, t as FileVmStateStore } from "./vm-state.mjs";
2
+ import { A as vmNotRunningError, B as invalidDomainPatternError, D as snapshotNotFoundError, F as invalidCidrOctetError, G as invalidNetworkPolicyError, H as invalidImageRefEmptyError, I as invalidCidrPrefixError, J as mutuallyExclusiveFlagsError, K as invalidPortError, L as invalidDiskSizeFormatError, P as invalidCidrFormatError, R as invalidDiskSizeRangeError, T as chrootNotFoundError, U as invalidImageRefTagError, W as invalidIntegerFlagError, X as portConflictError, Z as VmsanError, a as cloudflareNotConfiguredError, c as cloudflaredStartFailedError, d as missingBinaryError, f as noExt4RootfsError, h as noRootfsDirError, i as cloudflareNoZoneError, j as vmNotStoppedError, k as vmNotFoundError, m as noKernelError, n as cloudflareConfigNotFoundError, o as cloudflareTunnelNoIdError, p as noKernelDirError, q as invalidRuntimeError, r as cloudflareNoAccountsError, s as cloudflaredNotFoundError, u as SetupError, v as lockTimeoutError, x as defaultInterfaceNotFoundError, y as socketTimeoutError, z as invalidDomainError } from "./errors.mjs";
3
+ import { c as mkdirSecure, d as sleepSync, g as writeSecure, h as toError, n as waitForAgent, o as generateVmId, r as FileVmStateStore, u as safeKill } from "./vm-context.mjs";
4
4
  import { t as FirecrackerClient } from "./firecracker.mjs";
5
5
  import { t as spawnTimeoutKiller } from "./timeout-killer.mjs";
6
+ import { t as AgentClient } from "./agent.mjs";
6
7
  import { createHooks } from "hookable";
7
8
  import { dirname, join } from "node:path";
8
9
  import { execFileSync, execSync, spawn } from "node:child_process";
@@ -1202,137 +1203,7 @@ var FileLock = class {
1202
1203
  }
1203
1204
  };
1204
1205
  /**
1205
- * Template generators for the node22-demo runtime welcome page.
1206
- * All functions are pure and return string content ready to write to files.
1207
- */
1208
- function generateWelcomeHtml(vmId, ports) {
1209
- ports.map((p) => `<li>${p}</li>`).join("\n ");
1210
- return `<!DOCTYPE html>
1211
- <html lang="en">
1212
- <head>
1213
- <meta charset="utf-8">
1214
- <meta name="viewport" content="width=device-width, initial-scale=1">
1215
- <title>vmsan VM ${vmId}</title>
1216
- <style>
1217
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
1218
- body {
1219
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
1220
- background: #0f172a;
1221
- color: #e2e8f0;
1222
- min-height: 100vh;
1223
- display: flex;
1224
- align-items: center;
1225
- justify-content: center;
1226
- padding: 2rem;
1227
- }
1228
- .container { max-width: 640px; width: 100%; }
1229
- .header { text-align: center; margin-bottom: 2rem; }
1230
- .logo {
1231
- font-size: 2.5rem;
1232
- font-weight: 800;
1233
- background: linear-gradient(135deg, #f97316, #ef4444);
1234
- -webkit-background-clip: text;
1235
- -webkit-text-fill-color: transparent;
1236
- background-clip: text;
1237
- }
1238
- .subtitle { color: #94a3b8; margin-top: 0.5rem; font-size: 1.1rem; }
1239
- .card {
1240
- background: #1e293b;
1241
- border: 1px solid #334155;
1242
- border-radius: 12px;
1243
- padding: 1.5rem;
1244
- margin-bottom: 1.25rem;
1245
- }
1246
- .card h2 { font-size: 1rem; color: #f97316; margin-bottom: 0.75rem; }
1247
- .info-row { display: flex; justify-content: space-between; padding: 0.35rem 0; }
1248
- .info-label { color: #94a3b8; }
1249
- .info-value { font-family: monospace; color: #e2e8f0; }
1250
- ul { list-style: none; }
1251
- ul li { padding: 0.25rem 0; }
1252
- code {
1253
- background: #0f172a;
1254
- border: 1px solid #334155;
1255
- border-radius: 6px;
1256
- padding: 0.2rem 0.5rem;
1257
- font-size: 0.875rem;
1258
- color: #f97316;
1259
- }
1260
- .steps li { padding: 0.5rem 0; color: #cbd5e1; }
1261
- .steps li strong { color: #e2e8f0; }
1262
- .footer { text-align: center; color: #475569; font-size: 0.85rem; margin-top: 1.5rem; }
1263
- </style>
1264
- </head>
1265
- <body>
1266
- <div class="container">
1267
- <div class="header">
1268
- <div class="logo">vmsan</div>
1269
- <div class="subtitle">Your microVM is running</div>
1270
- </div>
1271
- <div class="card">
1272
- <h2>VM Info</h2>
1273
- <div class="info-row">
1274
- <span class="info-label">VM ID</span>
1275
- <span class="info-value">${vmId}</span>
1276
- </div>
1277
- <div class="info-row">
1278
- <span class="info-label">Runtime</span>
1279
- <span class="info-value">node22-demo</span>
1280
- </div>
1281
- <div class="info-row">
1282
- <span class="info-label">Published Ports</span>
1283
- <span class="info-value">${ports.join(", ")}</span>
1284
- </div>
1285
- </div>
1286
- <div class="card">
1287
- <h2>Next Steps</h2>
1288
- <ul class="steps">
1289
- <li><strong>Connect to the VM:</strong> <code>vmsan connect ${vmId}</code></li>
1290
- <li><strong>Deploy your app:</strong> Replace this page by stopping the welcome service and running your own server on the published port(s).</li>
1291
- <li><strong>Stop this page:</strong> <code>systemctl stop vmsan-welcome</code></li>
1292
- </ul>
1293
- </div>
1294
- <div class="footer">Powered by vmsan &middot; Firecracker microVMs</div>
1295
- </div>
1296
- </body>
1297
- </html>`;
1298
- }
1299
- function generateWelcomeServer(ports) {
1300
- return `"use strict";
1301
- const http = require("node:http");
1302
- const fs = require("node:fs");
1303
- const path = require("node:path");
1304
-
1305
- const html = fs.readFileSync(path.join(__dirname, "index.html"), "utf-8");
1306
-
1307
- const server = http.createServer((req, res) => {
1308
- res.writeHead(200, {
1309
- "Content-Type": "text/html; charset=utf-8",
1310
- "Cache-Control": "no-cache",
1311
- });
1312
- res.end(html);
1313
- });
1314
-
1315
- ${ports.map((p) => `server.listen(${p}, "0.0.0.0", () => console.log("vmsan-welcome listening on 0.0.0.0:${p}"));`).join("\n")}
1316
- `;
1317
- }
1318
- function generateWelcomeService(ports) {
1319
- return `[Unit]
1320
- Description=${`vmsan welcome page on port(s) ${ports.join(", ")}`}
1321
- After=network.target
1322
-
1323
- [Service]
1324
- Type=simple
1325
- ExecStart=/usr/local/bin/node /opt/vmsan/welcome/server.js
1326
- Restart=on-failure
1327
- RestartSec=2
1328
-
1329
- [Install]
1330
- WantedBy=multi-user.target
1331
- `;
1332
- }
1333
- /**
1334
1206
  * Template generators for the vmsan-agent systemd service.
1335
- * Follows the same pattern as welcome-page.ts.
1336
1207
  */
1337
1208
  function generateAgentService() {
1338
1209
  return `[Unit]
@@ -1416,19 +1287,10 @@ var Jailer = class {
1416
1287
  try {
1417
1288
  execSync(`sudo mount -o loop "${paths.rootfsPath}" "${tmpMount}"`, { stdio: "pipe" });
1418
1289
  execSync(`rm -f "${tmpMount}/etc/resolv.conf" && ln -s /proc/net/pnp "${tmpMount}/etc/resolv.conf"`, { stdio: "pipe" });
1419
- if (config.welcomePage) {
1420
- const { vmId: welcomeVmId, ports: welcomePorts } = config.welcomePage;
1421
- const welcomeDir = join(tmpMount, "opt", "vmsan", "welcome");
1422
- mkdirSync(welcomeDir, { recursive: true });
1423
- writeFileSync(join(welcomeDir, "index.html"), generateWelcomeHtml(welcomeVmId, welcomePorts));
1424
- writeFileSync(join(welcomeDir, "server.js"), generateWelcomeServer(welcomePorts));
1425
- const systemdDir = join(tmpMount, "etc", "systemd", "system");
1426
- mkdirSync(systemdDir, { recursive: true });
1427
- writeFileSync(join(systemdDir, "vmsan-welcome.service"), generateWelcomeService(welcomePorts));
1428
- const wantsDir = join(systemdDir, "multi-user.target.wants");
1429
- mkdirSync(wantsDir, { recursive: true });
1430
- execSync(`ln -sf /etc/systemd/system/vmsan-welcome.service "${join(wantsDir, "vmsan-welcome.service")}"`, { stdio: "pipe" });
1431
- }
1290
+ writeFileSync(join(tmpMount, "etc", "hostname"), `${this.vmId}\n`);
1291
+ const hostsPath = join(tmpMount, "etc", "hosts");
1292
+ const hostsContent = existsSync(hostsPath) ? readFileSync(hostsPath, "utf-8") : "";
1293
+ if (!hostsContent.includes(this.vmId)) writeFileSync(hostsPath, `${hostsContent.trimEnd()}\n127.0.1.1 ${this.vmId}\n`);
1432
1294
  if (config.agent) {
1433
1295
  const agentDst = join(tmpMount, "usr", "local", "bin", "vmsan-agent");
1434
1296
  mkdirSync(join(tmpMount, "usr", "local", "bin"), { recursive: true });
@@ -1445,7 +1307,19 @@ var Jailer = class {
1445
1307
  execSync(`ln -sf /etc/systemd/system/vmsan-agent.service "${join(wantsDir, "vmsan-agent.service")}"`, { stdio: "pipe" });
1446
1308
  }
1447
1309
  const ubuntuHome = join(tmpMount, "home", "ubuntu");
1448
- if (existsSync(ubuntuHome)) execSync(`sudo chown 1000:1000 "${ubuntuHome}"`, { stdio: "pipe" });
1310
+ const rootfsPasswd = join(tmpMount, "etc", "passwd");
1311
+ if (existsSync(ubuntuHome) && existsSync(rootfsPasswd)) {
1312
+ const ubuntuEntry = readFileSync(rootfsPasswd, "utf-8").split("\n").find((l) => l.startsWith("ubuntu:"));
1313
+ if (ubuntuEntry) {
1314
+ const fields = ubuntuEntry.split(":");
1315
+ if (fields.length >= 4 && /^\d+$/.test(fields[2]) && /^\d+$/.test(fields[3])) execFileSync("sudo", [
1316
+ "chown",
1317
+ "-R",
1318
+ `${fields[2]}:${fields[3]}`,
1319
+ ubuntuHome
1320
+ ], { stdio: "pipe" });
1321
+ }
1322
+ }
1449
1323
  execSync(`sudo umount "${tmpMount}"`, { stdio: "pipe" });
1450
1324
  } catch {
1451
1325
  try {
@@ -1509,6 +1383,34 @@ function findKernel(baseDir) {
1509
1383
  if (files.length === 0) throw noKernelError();
1510
1384
  return join(kernelDir, files.sort().at(-1));
1511
1385
  }
1386
+ const RUNTIME_ROOTFS_MAP = {
1387
+ node22: "node22.ext4",
1388
+ node24: "node24.ext4",
1389
+ "python3.13": "python3.13.ext4"
1390
+ };
1391
+ const BASE_ROOTFS_FILENAMES = ["ubuntu-24.04.ext4"];
1392
+ function findRuntimeRootfs(runtime, baseDir) {
1393
+ const filename = RUNTIME_ROOTFS_MAP[runtime];
1394
+ const rootfsPath = join(baseDir, "rootfs", filename);
1395
+ if (!existsSync(rootfsPath)) throw new SetupError("ERR_SETUP_NO_EXT4_ROOTFS", {
1396
+ message: `Runtime "${runtime}" rootfs not found at ${rootfsPath}`,
1397
+ fix: "Run \"curl -fsSL https://vmsan.dev/install | bash\" to build runtime images."
1398
+ });
1399
+ return rootfsPath;
1400
+ }
1401
+ function findBaseRootfs(baseDir) {
1402
+ const rootfsDir = join(baseDir, "rootfs");
1403
+ if (!existsSync(rootfsDir)) throw noRootfsDirError();
1404
+ for (const filename of BASE_ROOTFS_FILENAMES) {
1405
+ const rootfsPath = join(rootfsDir, filename);
1406
+ if (existsSync(rootfsPath)) return rootfsPath;
1407
+ }
1408
+ const files = readdirSync(rootfsDir).filter((fileName) => fileName.endsWith(".ext4"));
1409
+ const runtimeFilenames = new Set(Object.values(RUNTIME_ROOTFS_MAP));
1410
+ const baseFiles = files.filter((fileName) => !runtimeFilenames.has(fileName));
1411
+ if (baseFiles.length === 0) throw noExt4RootfsError();
1412
+ return join(rootfsDir, baseFiles.sort().at(-1));
1413
+ }
1512
1414
  function findRootfs(baseDir) {
1513
1415
  const rootfsDir = join(baseDir, "rootfs");
1514
1416
  if (!existsSync(rootfsDir)) throw noRootfsDirError();
@@ -1651,16 +1553,18 @@ const APT_PACKAGES = [
1651
1553
  "findutils",
1652
1554
  "git",
1653
1555
  "gzip",
1556
+ "iptables",
1654
1557
  "iputils-ping",
1655
1558
  "libicu-dev",
1656
1559
  "libjpeg-dev",
1657
1560
  "libpng-dev",
1658
1561
  "ncurses-base",
1659
1562
  "libssl-dev",
1660
- "openssh-server",
1661
1563
  "openssl",
1662
1564
  "procps",
1663
1565
  "sudo",
1566
+ "systemd",
1567
+ "systemd-sysv",
1664
1568
  "tar",
1665
1569
  "unzip",
1666
1570
  "debianutils",
@@ -1673,12 +1577,12 @@ const DNF_PACKAGES = [
1673
1577
  "findutils",
1674
1578
  "git",
1675
1579
  "gzip",
1580
+ "iptables",
1676
1581
  "iputils",
1677
1582
  "libicu",
1678
1583
  "libjpeg",
1679
1584
  "libpng",
1680
1585
  "ncurses-libs",
1681
- "openssh-server",
1682
1586
  "openssl",
1683
1587
  "openssl-libs",
1684
1588
  "procps",
@@ -1696,13 +1600,13 @@ const APK_PACKAGES = [
1696
1600
  "findutils",
1697
1601
  "git",
1698
1602
  "gzip",
1603
+ "iptables",
1699
1604
  "iputils",
1700
1605
  "icu-libs",
1701
1606
  "libjpeg-turbo",
1702
1607
  "libpng",
1703
1608
  "ncurses-libs",
1704
1609
  "openrc",
1705
- "openssh",
1706
1610
  "openssl",
1707
1611
  "procps",
1708
1612
  "sudo",
@@ -1711,7 +1615,8 @@ const APK_PACKAGES = [
1711
1615
  "whois",
1712
1616
  "zstd"
1713
1617
  ];
1714
- function generateDockerfile(baseImage) {
1618
+ function generateDockerfile(baseImage, minimal = false) {
1619
+ if (minimal) return `FROM ${baseImage}\n`;
1715
1620
  return `FROM ${baseImage}
1716
1621
  RUN if command -v apt-get >/dev/null 2>&1; then ${`apt-get update && apt-get install -y --no-install-recommends ${APT_PACKAGES.join(" ")} && rm -rf /var/lib/apt/lists/*`}; \\
1717
1622
  elif command -v dnf >/dev/null 2>&1; then ${`dnf install -y ${DNF_PACKAGES.join(" ")} && dnf clean all`}; \\
@@ -1725,13 +1630,22 @@ RUN if command -v apk >/dev/null 2>&1; then \\
1725
1630
  fi; \\
1726
1631
  echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ubuntu; \\
1727
1632
  chmod 440 /etc/sudoers.d/ubuntu; \\
1728
- mkdir -p /home/ubuntu/.ssh && chown -R ubuntu:ubuntu /home/ubuntu
1729
- RUN ssh-keygen -A 2>/dev/null || true; \\
1730
- mkdir -p /root/.ssh && chmod 700 /root/.ssh; \\
1731
- if [ -f /etc/ssh/sshd_config ]; then \\
1732
- sed -i 's/^#*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config; \\
1633
+ chown -R ubuntu:ubuntu /home/ubuntu
1634
+ RUN EXTRA=""; \\
1635
+ if command -v npm >/dev/null 2>&1; then \\
1636
+ su -c 'mkdir -p /home/ubuntu/.npm-global && npm config set prefix /home/ubuntu/.npm-global' ubuntu; \\
1637
+ EXTRA="\${EXTRA}export PATH=\\"/home/ubuntu/.npm-global/bin:\\$PATH\\"\\n"; \\
1733
1638
  fi; \\
1734
- if command -v rc-update >/dev/null 2>&1; then \\
1639
+ if command -v pip3 >/dev/null 2>&1 || command -v pip >/dev/null 2>&1; then \\
1640
+ EXTRA="\${EXTRA}export PATH=\\"/home/ubuntu/.local/bin:\\$PATH\\"\\n"; \\
1641
+ fi; \\
1642
+ if [ -n "$EXTRA" ]; then \\
1643
+ printf '%b' "$EXTRA" >> /home/ubuntu/.profile; \\
1644
+ { printf '%b' "$EXTRA"; cat /home/ubuntu/.bashrc; } > /home/ubuntu/.bashrc.tmp \\
1645
+ && mv /home/ubuntu/.bashrc.tmp /home/ubuntu/.bashrc; \\
1646
+ fi; \\
1647
+ chown -R ubuntu:ubuntu /home/ubuntu
1648
+ RUN if command -v rc-update >/dev/null 2>&1; then \\
1735
1649
  rc-update add devfs sysinit 2>/dev/null || true; \\
1736
1650
  rc-update add mdev sysinit 2>/dev/null || true; \\
1737
1651
  rc-update add hwdrivers sysinit 2>/dev/null || true; \\
@@ -1740,10 +1654,8 @@ RUN ssh-keygen -A 2>/dev/null || true; \\
1740
1654
  rc-update add hostname boot 2>/dev/null || true; \\
1741
1655
  rc-update add bootmisc boot 2>/dev/null || true; \\
1742
1656
  rc-update add networking boot 2>/dev/null || true; \\
1743
- rc-update add sshd default 2>/dev/null || true; \\
1744
1657
  printf '%s\\n' '::sysinit:/sbin/openrc sysinit' '::sysinit:/sbin/openrc boot' '::wait:/sbin/openrc default' '::shutdown:/sbin/openrc shutdown' 'ttyS0::respawn:/sbin/getty 115200 ttyS0' > /etc/inittab; \\
1745
- fi; \\
1746
- if command -v systemctl >/dev/null 2>&1; then systemctl enable sshd 2>/dev/null || systemctl enable ssh 2>/dev/null || true; fi
1658
+ fi
1747
1659
  `;
1748
1660
  }
1749
1661
  function verifyDocker() {
@@ -1753,7 +1665,7 @@ function verifyDocker() {
1753
1665
  throw dockerUnavailableError();
1754
1666
  }
1755
1667
  }
1756
- function buildImageRootfs(imageRef, cacheDir) {
1668
+ function buildImageRootfs(imageRef, cacheDir, minimal = false) {
1757
1669
  const ext4Path = join(cacheDir, "rootfs.ext4");
1758
1670
  verifyDocker();
1759
1671
  const buildTag = `vmsan-rootfs-${imageRef.name.replace(/[^a-z0-9._-]/gi, "-")}:${imageRef.tag}`;
@@ -1762,7 +1674,7 @@ function buildImageRootfs(imageRef, cacheDir) {
1762
1674
  mkdirSync(cacheDir, { recursive: true });
1763
1675
  try {
1764
1676
  consola.start(`Building image from ${imageRef.full}...`);
1765
- execSync(`docker build -t "${buildTag}" -f - . <<'DOCKERFILE'\n${generateDockerfile(imageRef.full)}\nDOCKERFILE`, {
1677
+ execSync(`docker build -t "${buildTag}" -f - . <<'DOCKERFILE'\n${generateDockerfile(imageRef.full, minimal)}\nDOCKERFILE`, {
1766
1678
  stdio: "pipe",
1767
1679
  shell: "/bin/bash"
1768
1680
  });
@@ -1800,19 +1712,20 @@ function buildImageRootfs(imageRef, cacheDir) {
1800
1712
  } catch {}
1801
1713
  }
1802
1714
  }
1803
- function resolveImageRootfs(imageRef, registryDir) {
1804
- const cacheDir = join(registryDir, imageRef.cacheKey);
1715
+ function resolveImageRootfs(imageRef, registryDir, minimal = false) {
1716
+ const cacheSuffix = minimal ? "-minimal" : "";
1717
+ const cacheDir = join(registryDir, `${imageRef.cacheKey}${cacheSuffix}`);
1805
1718
  const ext4Path = join(cacheDir, "rootfs.ext4");
1806
1719
  if (existsSync(ext4Path)) {
1807
1720
  consola.info(`Using cached rootfs for ${imageRef.full}`);
1808
1721
  return ext4Path;
1809
1722
  }
1810
- return buildImageRootfs(imageRef, cacheDir);
1723
+ return buildImageRootfs(imageRef, cacheDir, minimal);
1811
1724
  }
1812
1725
  const VALID_RUNTIMES = [
1813
1726
  "base",
1814
1727
  "node22",
1815
- "node22-demo",
1728
+ "node24",
1816
1729
  "python3.13"
1817
1730
  ];
1818
1731
  const VALID_NETWORK_POLICIES = [
@@ -2042,11 +1955,12 @@ var VMService = class {
2042
1955
  const kernelPath = opts.kernelPath ?? findKernel(paths.baseDir);
2043
1956
  logger.debug(`Kernel resolved: ${kernelPath}`);
2044
1957
  let rootfsPath;
2045
- if (opts.fromImage) rootfsPath = resolveImageRootfs(opts.fromImage, paths.registryDir);
2046
- else rootfsPath = opts.rootfsPath ?? findRootfs(paths.baseDir);
1958
+ if (opts.fromImage) rootfsPath = resolveImageRootfs(opts.fromImage, paths.registryDir, true);
1959
+ else if (runtime !== "base" && !opts.rootfsPath) rootfsPath = findRuntimeRootfs(runtime, paths.baseDir);
1960
+ else rootfsPath = opts.rootfsPath ?? findBaseRootfs(paths.baseDir);
2047
1961
  logger.debug(`Rootfs resolved: ${rootfsPath}`);
2048
1962
  const netnsName = opts.disableNetns ? void 0 : `vmsan-${vmId}`;
2049
- const agentToken = existsSync(paths.agentBin) ? randomBytes(32).toString("hex") : null;
1963
+ const agentToken = !opts.fromImage && existsSync(paths.agentBin) ? randomBytes(32).toString("hex") : null;
2050
1964
  log.start(`Creating VM ${vmId}...`);
2051
1965
  const { net } = new FileLock(join(paths.vmsDir, ".slot-lock"), "slot-alloc").run(() => {
2052
1966
  const slot = this.store.allocateNetworkSlot();
@@ -2100,10 +2014,6 @@ var VMService = class {
2100
2014
  memFile: join(paths.snapshotsDir, snapshotId, "mem_file")
2101
2015
  } : void 0;
2102
2016
  const jailer = new Jailer(vmId, paths.jailerBaseDir);
2103
- const welcomePage = runtime === "node22-demo" && ports.length > 0 ? {
2104
- vmId,
2105
- ports
2106
- } : void 0;
2107
2017
  const agentConfig = agentToken ? {
2108
2018
  binaryPath: paths.agentBin,
2109
2019
  token: agentToken,
@@ -2115,7 +2025,6 @@ var VMService = class {
2115
2025
  rootfsSrc: rootfsPath,
2116
2026
  diskSizeGb,
2117
2027
  snapshot: snapshotConfig,
2118
- welcomePage,
2119
2028
  agent: agentConfig
2120
2029
  });
2121
2030
  chrootDir = jailerPaths.chrootDir;
@@ -2167,6 +2076,7 @@ var VMService = class {
2167
2076
  timeoutMs,
2168
2077
  stateFile: join(paths.vmsDir, `${vmId}.json`)
2169
2078
  });
2079
+ if (agentToken && opts.ports?.length) await this.setupLocalhostPortForwarding(netCfg.guestIp, paths.agentPort, agentToken, opts.ports);
2170
2080
  const finalState = this.store.load(vmId);
2171
2081
  await hooks.callHook("vm:afterCreate", finalState);
2172
2082
  return {
@@ -2321,6 +2231,7 @@ var VMService = class {
2321
2231
  pid
2322
2232
  });
2323
2233
  log.success(`VM ${vmId} is running (PID: ${pid || "unknown"})`);
2234
+ if (state.agentToken && state.network.publishedPorts?.length) await this.setupLocalhostPortForwarding(state.network.guestIp, state.agentPort, state.agentToken, state.network.publishedPorts);
2324
2235
  const finalState = this.store.load(vmId);
2325
2236
  await hooks.callHook("vm:afterStart", finalState);
2326
2237
  return {
@@ -2515,6 +2426,26 @@ var VMService = class {
2515
2426
  await vm.configure(vcpus, memMib);
2516
2427
  await vm.addNetwork("eth0", netCfg.tapDevice, netCfg.macAddress);
2517
2428
  }
2429
+ /**
2430
+ * Set up iptables DNAT rules inside the VM so that traffic arriving on
2431
+ * published ports is forwarded to 127.0.0.1. Many services bind to
2432
+ * localhost only; this lets the Cloudflare tunnel (which connects to the
2433
+ * guest IP) reach them.
2434
+ */
2435
+ async setupLocalhostPortForwarding(guestIp, agentPort, agentToken, ports) {
2436
+ try {
2437
+ await waitForAgent(guestIp, agentPort);
2438
+ const agent = new AgentClient(`http://${guestIp}:${agentPort}`, agentToken);
2439
+ const iptablesRules = ports.map((p) => `sudo iptables-legacy -t nat -A PREROUTING -i eth0 -p tcp --dport ${p} -j DNAT --to-destination 127.0.0.1:${p}`).join(" && ");
2440
+ await agent.runCommand({
2441
+ cmd: "/bin/bash",
2442
+ args: ["-c", `sudo sysctl -w net.ipv4.conf.all.route_localnet=1 && ${iptablesRules}`]
2443
+ });
2444
+ this.logger.debug(`Localhost port forwarding set up for ports: ${ports.join(", ")}`);
2445
+ } catch (err) {
2446
+ this.logger.warn(`Failed to set up localhost port forwarding: ${toError(err).message}`);
2447
+ }
2448
+ }
2518
2449
  markAsError(vmId, error) {
2519
2450
  try {
2520
2451
  this.store.update(vmId, {
@@ -2961,4 +2892,4 @@ async function createVmsan(options) {
2961
2892
  for (const plugin of allPlugins) await plugin.setup(ctx);
2962
2893
  return vmsan;
2963
2894
  }
2964
- export { findKernel as A, createDefaultLogger as B, resolveImageRootfs as C, killOrphanVmProcess as D, cleanupNetwork as E, waitForSocket as F, Jailer as I, detectCgroupVersion as L, getVmJailerPid as M, getVmPid as N, markVmAsError as O, validateEnvironment as P, FileLock as R, validatePublishedPortsAvailable as S, cleanupChroot as T, createSilentLogger as V, parseNetworkPolicy as _, resolveTunnelHostnames as a, parseVcpuCount as b, VMService as c, parseBandwidth as d, parseCidrList as f, parseMemoryMib as g, parseImageReference as h, CloudflareService as i, findRootfs as j, assertSnapshotExists as k, compileSeccompFilter as l, parseDomains as m, cloudflarePlugin as n, PidFile as o, parseDiskSizeGb as p, cleanupCloudflareResources as r, definePlugin as s, createVmsan as t, ensureSeccompFilter as u, parsePublishedPorts as v, buildInitialVmState as w, validateCidr as x, parseRuntime as y, NetworkManager as z };
2895
+ export { findKernel as A, NetworkManager as B, resolveImageRootfs as C, killOrphanVmProcess as D, cleanupNetwork as E, validateEnvironment as F, createSilentLogger as H, waitForSocket as I, Jailer as L, findRuntimeRootfs as M, getVmJailerPid as N, markVmAsError as O, getVmPid as P, detectCgroupVersion as R, validatePublishedPortsAvailable as S, cleanupChroot as T, createDefaultLogger as V, parseNetworkPolicy as _, resolveTunnelHostnames as a, parseVcpuCount as b, VMService as c, parseBandwidth as d, parseCidrList as f, parseMemoryMib as g, parseImageReference as h, CloudflareService as i, findRootfs as j, assertSnapshotExists as k, compileSeccompFilter as l, parseDomains as m, cloudflarePlugin as n, PidFile as o, parseDiskSizeGb as p, cleanupCloudflareResources as r, definePlugin as s, createVmsan as t, ensureSeccompFilter as u, parsePublishedPorts as v, buildInitialVmState as w, validateCidr as x, parseRuntime as y, FileLock as z };