@vm0/runner 3.11.3 → 3.12.0

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.
Files changed (2) hide show
  1. package/index.js +506 -202
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -44,7 +44,7 @@ var runnerPaths = {
44
44
  /** Runner status file */
45
45
  statusFile: (baseDir) => path.join(baseDir, "status.json"),
46
46
  /** Snapshot generation work directory */
47
- snapshotWorkDir: (baseDir) => path.join(baseDir, "workspaces", ".snapshot-work"),
47
+ snapshotWorkDir: (baseDir) => path.join(baseDir, "workspaces", "snapshot"),
48
48
  /** Check if a directory name is a VM workspace */
49
49
  isVmWorkspace: (dirname) => dirname.startsWith(VM_WORKSPACE_PREFIX),
50
50
  /** Extract vmId from workspace directory name */
@@ -62,6 +62,14 @@ var vmPaths = {
62
62
  /** Overlay filesystem for VM writes */
63
63
  overlay: (workDir) => path.join(workDir, "overlay.ext4")
64
64
  };
65
+ var snapshotOutputPaths = {
66
+ /** VM state snapshot */
67
+ snapshot: (outputDir) => path.join(outputDir, "snapshot.bin"),
68
+ /** VM memory snapshot */
69
+ memory: (outputDir) => path.join(outputDir, "memory.bin"),
70
+ /** Golden overlay with guest state */
71
+ overlay: (outputDir) => path.join(outputDir, "overlay.ext4")
72
+ };
65
73
  var tempPaths = {
66
74
  /** Default proxy CA directory */
67
75
  proxyDir: `${VM0_TMP_PREFIX}-proxy`,
@@ -102,7 +110,12 @@ var runnerConfigSchema = z.object({
102
110
  firecracker: z.object({
103
111
  binary: z.string().min(1, "Firecracker binary path is required"),
104
112
  kernel: z.string().min(1, "Kernel path is required"),
105
- rootfs: z.string().min(1, "Rootfs path is required")
113
+ rootfs: z.string().min(1, "Rootfs path is required"),
114
+ snapshot: z.object({
115
+ snapshot: z.string().min(1, "Snapshot state file path is required"),
116
+ memory: z.string().min(1, "Snapshot memory file path is required"),
117
+ overlay: z.string().min(1, "Snapshot overlay file path is required")
118
+ }).optional()
106
119
  }),
107
120
  proxy: z.object({
108
121
  // TODO: Allow 0 to auto-find available port
@@ -131,7 +144,12 @@ var debugConfigSchema = z.object({
131
144
  firecracker: z.object({
132
145
  binary: z.string().min(1, "Firecracker binary path is required"),
133
146
  kernel: z.string().min(1, "Kernel path is required"),
134
- rootfs: z.string().min(1, "Rootfs path is required")
147
+ rootfs: z.string().min(1, "Rootfs path is required"),
148
+ snapshot: z.object({
149
+ snapshot: z.string().min(1, "Snapshot state file path is required"),
150
+ memory: z.string().min(1, "Snapshot memory file path is required"),
151
+ overlay: z.string().min(1, "Snapshot overlay file path is required")
152
+ }).optional()
135
153
  }),
136
154
  proxy: z.object({
137
155
  port: z.number().int().min(1024).max(65535).default(PROXY_DEFAULTS.port),
@@ -172,6 +190,13 @@ function validateFirecrackerPaths(config) {
172
190
  { path: config.kernel, name: "Kernel" },
173
191
  { path: config.rootfs, name: "Rootfs" }
174
192
  ];
193
+ if (config.snapshot) {
194
+ checks.push(
195
+ { path: config.snapshot.snapshot, name: "Snapshot state file" },
196
+ { path: config.snapshot.memory, name: "Snapshot memory file" },
197
+ { path: config.snapshot.overlay, name: "Snapshot overlay file" }
198
+ );
199
+ }
175
200
  for (const check of checks) {
176
201
  if (!fs.existsSync(check.path)) {
177
202
  throw new Error(`${check.name} not found: ${check.path}`);
@@ -337,10 +362,13 @@ async function subscribeToJobs(server, group, onJob, onConnectionChange) {
337
362
 
338
363
  // src/lib/executor.ts
339
364
  import fs9 from "fs";
365
+ import path6 from "path";
340
366
 
341
367
  // src/lib/firecracker/vm.ts
342
368
  import { spawn } from "child_process";
343
369
  import fs4 from "fs";
370
+ import os from "os";
371
+ import path4 from "path";
344
372
  import readline from "readline";
345
373
 
346
374
  // src/lib/firecracker/netns-pool.ts
@@ -379,8 +407,8 @@ var DEFAULT_OPTIONS = {
379
407
  maxTimeout: 1e3
380
408
  }
381
409
  };
382
- async function withFileLock(path7, fn, options) {
383
- const release = await lockfile.lock(path7, { ...DEFAULT_OPTIONS, ...options });
410
+ async function withFileLock(path9, fn, options) {
411
+ const release = await lockfile.lock(path9, { ...DEFAULT_OPTIONS, ...options });
384
412
  try {
385
413
  return await fn();
386
414
  } finally {
@@ -436,6 +464,10 @@ async function createNetnsWithTap(nsName, tap) {
436
464
  await execCommand(`ip netns exec ${nsName} ip link set ${tap.tapName} up`);
437
465
  await execCommand(`ip netns exec ${nsName} ip link set lo up`);
438
466
  }
467
+ async function deleteNetns(nsName) {
468
+ await execCommand(`ip netns del ${nsName}`).catch(() => {
469
+ });
470
+ }
439
471
 
440
472
  // src/lib/firecracker/netns-pool.ts
441
473
  var logger = createLogger("NetnsPool");
@@ -1077,10 +1109,10 @@ import * as http from "http";
1077
1109
  import * as fs3 from "fs";
1078
1110
  var logger3 = createLogger("FirecrackerClient");
1079
1111
  var FirecrackerApiError = class extends Error {
1080
- constructor(statusCode, path7, faultMessage) {
1081
- super(`Firecracker API error ${statusCode} on ${path7}: ${faultMessage}`);
1112
+ constructor(statusCode, path9, faultMessage) {
1113
+ super(`Firecracker API error ${statusCode} on ${path9}: ${faultMessage}`);
1082
1114
  this.statusCode = statusCode;
1083
- this.path = path7;
1115
+ this.path = path9;
1084
1116
  this.faultMessage = faultMessage;
1085
1117
  this.name = "FirecrackerApiError";
1086
1118
  }
@@ -1193,27 +1225,27 @@ var FirecrackerClient = class {
1193
1225
  /**
1194
1226
  * GET request
1195
1227
  */
1196
- async get(path7) {
1197
- return this.request("GET", path7);
1228
+ async get(path9) {
1229
+ return this.request("GET", path9);
1198
1230
  }
1199
1231
  /**
1200
1232
  * PATCH request
1201
1233
  */
1202
- async patch(path7, body) {
1203
- return this.request("PATCH", path7, body);
1234
+ async patch(path9, body) {
1235
+ return this.request("PATCH", path9, body);
1204
1236
  }
1205
1237
  /**
1206
1238
  * PUT request
1207
1239
  */
1208
- async put(path7, body) {
1209
- return this.request("PUT", path7, body);
1240
+ async put(path9, body) {
1241
+ return this.request("PUT", path9, body);
1210
1242
  }
1211
1243
  /**
1212
1244
  * Make an HTTP request to Firecracker API
1213
1245
  *
1214
1246
  * @param timeoutMs Request timeout in milliseconds (default: 30000ms)
1215
1247
  */
1216
- request(method, path7, body, timeoutMs = 3e4) {
1248
+ request(method, path9, body, timeoutMs = 3e4) {
1217
1249
  return new Promise((resolve, reject) => {
1218
1250
  const bodyStr = body !== void 0 ? JSON.stringify(body) : void 0;
1219
1251
  const headers = {
@@ -1227,7 +1259,7 @@ var FirecrackerClient = class {
1227
1259
  }
1228
1260
  const options = {
1229
1261
  socketPath: this.socketPath,
1230
- path: path7,
1262
+ path: path9,
1231
1263
  method,
1232
1264
  headers,
1233
1265
  timeout: timeoutMs,
@@ -1235,7 +1267,7 @@ var FirecrackerClient = class {
1235
1267
  // Firecracker's single-threaded API can have issues with pipelined requests
1236
1268
  agent: false
1237
1269
  };
1238
- logger3.log(`${method} ${path7}${bodyStr ? " " + bodyStr : ""}`);
1270
+ logger3.log(`${method} ${path9}${bodyStr ? " " + bodyStr : ""}`);
1239
1271
  const req = http.request(options, (res) => {
1240
1272
  let data = "";
1241
1273
  res.on("data", (chunk) => {
@@ -1252,14 +1284,14 @@ var FirecrackerClient = class {
1252
1284
  faultMessage = errorBody.fault_message || data;
1253
1285
  } catch {
1254
1286
  }
1255
- reject(new FirecrackerApiError(statusCode, path7, faultMessage));
1287
+ reject(new FirecrackerApiError(statusCode, path9, faultMessage));
1256
1288
  }
1257
1289
  });
1258
1290
  });
1259
1291
  req.on("timeout", () => {
1260
1292
  req.destroy();
1261
1293
  reject(
1262
- new Error(`Request timeout after ${timeoutMs}ms: ${method} ${path7}`)
1294
+ new Error(`Request timeout after ${timeoutMs}ms: ${method} ${path9}`)
1263
1295
  );
1264
1296
  });
1265
1297
  req.on("error", (err) => {
@@ -1352,7 +1384,7 @@ var FirecrackerVM = class {
1352
1384
  this.workDir = config.workDir;
1353
1385
  this.vsockPath = vmPaths.vsock(this.workDir);
1354
1386
  this.configPath = vmPaths.config(this.workDir);
1355
- this.apiSocketPath = `${this.workDir}/api.sock`;
1387
+ this.apiSocketPath = vmPaths.apiSock(this.workDir);
1356
1388
  }
1357
1389
  /**
1358
1390
  * Get current VM state
@@ -1451,6 +1483,7 @@ var FirecrackerVM = class {
1451
1483
  const config = this.buildConfig();
1452
1484
  fs4.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
1453
1485
  logger4.log(`[VM ${this.config.vmId}] Starting Firecracker (fresh boot)...`);
1486
+ const currentUser = os.userInfo().username;
1454
1487
  this.process = spawn(
1455
1488
  "sudo",
1456
1489
  [
@@ -1458,6 +1491,9 @@ var FirecrackerVM = class {
1458
1491
  "netns",
1459
1492
  "exec",
1460
1493
  this.netns.name,
1494
+ "sudo",
1495
+ "-u",
1496
+ currentUser,
1461
1497
  this.config.firecrackerBinary,
1462
1498
  "--config-file",
1463
1499
  this.configPath,
@@ -1475,25 +1511,58 @@ var FirecrackerVM = class {
1475
1511
  * Start VM from snapshot
1476
1512
  * Uses --api-sock to load snapshot via API
1477
1513
  *
1478
- * Drive configuration must be done before loading snapshot
1479
- * because our overlay path differs from the snapshot's original path.
1514
+ * Snapshot contains original absolute paths for drives. We use mount namespace
1515
+ * isolation to bind mount our actual overlay file to the path expected by the snapshot.
1516
+ * This allows concurrent VMs to each have their own overlay while restoring from
1517
+ * the same snapshot.
1480
1518
  */
1481
1519
  async startFromSnapshot(snapshot) {
1482
1520
  logger4.log(
1483
1521
  `[VM ${this.config.vmId}] Starting Firecracker (snapshot restore)...`
1484
1522
  );
1485
- logger4.log(`[VM ${this.config.vmId}] Snapshot: ${snapshot.snapshotPath}`);
1486
- logger4.log(`[VM ${this.config.vmId}] Memory: ${snapshot.memoryPath}`);
1523
+ logger4.log(`[VM ${this.config.vmId}] Snapshot: ${snapshot.snapshot}`);
1524
+ logger4.log(`[VM ${this.config.vmId}] Memory: ${snapshot.memory}`);
1525
+ const actualVsockDir = vmPaths.vsockDir(this.workDir);
1526
+ logger4.log(
1527
+ `[VM ${this.config.vmId}] Snapshot vsock: ${snapshot.snapshotVsockDir}`
1528
+ );
1529
+ logger4.log(
1530
+ `[VM ${this.config.vmId}] Snapshot overlay: ${snapshot.snapshotOverlay}`
1531
+ );
1532
+ logger4.log(`[VM ${this.config.vmId}] Actual vsock: ${actualVsockDir}`);
1533
+ logger4.log(
1534
+ `[VM ${this.config.vmId}] Actual overlay: ${this.vmOverlayPath}`
1535
+ );
1536
+ fs4.mkdirSync(snapshot.snapshotVsockDir, { recursive: true });
1537
+ fs4.mkdirSync(path4.dirname(snapshot.snapshotOverlay), {
1538
+ recursive: true
1539
+ });
1540
+ if (!fs4.existsSync(snapshot.snapshotOverlay)) {
1541
+ fs4.writeFileSync(snapshot.snapshotOverlay, "");
1542
+ }
1543
+ const currentUser = os.userInfo().username;
1544
+ const bindMountVsock = `mount --bind "${actualVsockDir}" "${snapshot.snapshotVsockDir}"`;
1545
+ const bindMountOverlay = `mount --bind "${this.vmOverlayPath}" "${snapshot.snapshotOverlay}"`;
1546
+ const firecrackerCmd = [
1547
+ "ip",
1548
+ "netns",
1549
+ "exec",
1550
+ this.netns.name,
1551
+ "sudo",
1552
+ "-u",
1553
+ currentUser,
1554
+ this.config.firecrackerBinary,
1555
+ "--api-sock",
1556
+ this.apiSocketPath
1557
+ ].join(" ");
1487
1558
  this.process = spawn(
1488
1559
  "sudo",
1489
1560
  [
1490
- "ip",
1491
- "netns",
1492
- "exec",
1493
- this.netns.name,
1494
- this.config.firecrackerBinary,
1495
- "--api-sock",
1496
- this.apiSocketPath
1561
+ "unshare",
1562
+ "--mount",
1563
+ "bash",
1564
+ "-c",
1565
+ `${bindMountVsock} && ${bindMountOverlay} && ${firecrackerCmd}`
1497
1566
  ],
1498
1567
  {
1499
1568
  cwd: this.workDir,
@@ -1504,26 +1573,11 @@ var FirecrackerVM = class {
1504
1573
  this.setupProcessHandlers();
1505
1574
  const client = new FirecrackerClient(this.apiSocketPath);
1506
1575
  await this.waitForApiReady(client);
1507
- logger4.log(`[VM ${this.config.vmId}] Configuring drives...`);
1508
- await Promise.all([
1509
- client.configureDrive({
1510
- drive_id: "rootfs",
1511
- path_on_host: this.config.rootfsPath,
1512
- is_root_device: true,
1513
- is_read_only: true
1514
- }),
1515
- client.configureDrive({
1516
- drive_id: "overlay",
1517
- path_on_host: this.vmOverlayPath,
1518
- is_root_device: false,
1519
- is_read_only: false
1520
- })
1521
- ]);
1522
1576
  logger4.log(`[VM ${this.config.vmId}] Loading snapshot...`);
1523
1577
  await client.loadSnapshot({
1524
- snapshot_path: snapshot.snapshotPath,
1578
+ snapshot_path: snapshot.snapshot,
1525
1579
  mem_backend: {
1526
- backend_path: snapshot.memoryPath,
1580
+ backend_path: snapshot.memory,
1527
1581
  backend_type: "File"
1528
1582
  },
1529
1583
  resume_vm: true
@@ -1756,8 +1810,8 @@ function encodeExecPayload(command, timeoutMs) {
1756
1810
  cmdBuf.copy(payload, 8);
1757
1811
  return payload;
1758
1812
  }
1759
- function encodeWriteFilePayload(path7, content, sudo) {
1760
- const pathBuf = Buffer.from(path7, "utf-8");
1813
+ function encodeWriteFilePayload(path9, content, sudo) {
1814
+ const pathBuf = Buffer.from(path9, "utf-8");
1761
1815
  if (pathBuf.length > 65535) {
1762
1816
  throw new Error(`Path too long: ${pathBuf.length} bytes (max 65535)`);
1763
1817
  }
@@ -2652,8 +2706,8 @@ function getErrorMap() {
2652
2706
  return overrideErrorMap;
2653
2707
  }
2654
2708
  var makeIssue = (params) => {
2655
- const { data, path: path7, errorMaps, issueData } = params;
2656
- const fullPath = [...path7, ...issueData.path || []];
2709
+ const { data, path: path9, errorMaps, issueData } = params;
2710
+ const fullPath = [...path9, ...issueData.path || []];
2657
2711
  const fullIssue = {
2658
2712
  ...issueData,
2659
2713
  path: fullPath
@@ -2752,11 +2806,11 @@ var errorUtil;
2752
2806
  errorUtil2.toString = (message) => typeof message === "string" ? message : message === null || message === void 0 ? void 0 : message.message;
2753
2807
  })(errorUtil || (errorUtil = {}));
2754
2808
  var ParseInputLazyPath = class {
2755
- constructor(parent, value, path7, key) {
2809
+ constructor(parent, value, path9, key) {
2756
2810
  this._cachedPath = [];
2757
2811
  this.parent = parent;
2758
2812
  this.data = value;
2759
- this._path = path7;
2813
+ this._path = path9;
2760
2814
  this._key = key;
2761
2815
  }
2762
2816
  get path() {
@@ -9220,7 +9274,7 @@ function initVMRegistry(registryPath) {
9220
9274
  // src/lib/proxy/proxy-manager.ts
9221
9275
  import { spawn as spawn2 } from "child_process";
9222
9276
  import fs7 from "fs";
9223
- import path4 from "path";
9277
+ import path5 from "path";
9224
9278
 
9225
9279
  // src/lib/proxy/mitm-addon-script.ts
9226
9280
  var RUNNER_MITM_ADDON_SCRIPT = `#!/usr/bin/env python3
@@ -9716,7 +9770,7 @@ var ProxyManager = class {
9716
9770
  process = null;
9717
9771
  isRunning = false;
9718
9772
  constructor(config) {
9719
- const addonPath = path4.join(config.caDir, "mitm_addon.py");
9773
+ const addonPath = path5.join(config.caDir, "mitm_addon.py");
9720
9774
  this.config = {
9721
9775
  ...DEFAULT_PROXY_OPTIONS,
9722
9776
  ...config,
@@ -9743,7 +9797,7 @@ var ProxyManager = class {
9743
9797
  * Ensure the addon script exists at the configured path
9744
9798
  */
9745
9799
  ensureAddonScript() {
9746
- const addonDir = path4.dirname(this.config.addonPath);
9800
+ const addonDir = path5.dirname(this.config.addonPath);
9747
9801
  if (!fs7.existsSync(addonDir)) {
9748
9802
  fs7.mkdirSync(addonDir, { recursive: true });
9749
9803
  }
@@ -9759,7 +9813,7 @@ var ProxyManager = class {
9759
9813
  if (!fs7.existsSync(this.config.caDir)) {
9760
9814
  throw new Error(`Proxy CA directory not found: ${this.config.caDir}`);
9761
9815
  }
9762
- const caCertPath = path4.join(this.config.caDir, "mitmproxy-ca.pem");
9816
+ const caCertPath = path5.join(this.config.caDir, "mitmproxy-ca.pem");
9763
9817
  if (!fs7.existsSync(caCertPath)) {
9764
9818
  throw new Error(`Proxy CA certificate not found: ${caCertPath}`);
9765
9819
  }
@@ -10212,7 +10266,21 @@ async function executeJob(context, config, options = {}) {
10212
10266
  const guestConnectionPromise = guest.waitForGuestConnection(3e4);
10213
10267
  logger9.log(`Creating VM ${vmId}...`);
10214
10268
  vm = new FirecrackerVM(vmConfig);
10215
- await withSandboxTiming("vm_create", () => vm.start());
10269
+ const snapshotConfig = config.firecracker.snapshot;
10270
+ let snapshotPaths;
10271
+ if (snapshotConfig) {
10272
+ const snapshotDir = path6.dirname(snapshotConfig.snapshot);
10273
+ const originalBaseDir = path6.dirname(snapshotDir);
10274
+ const snapshotBaseDir = runnerPaths.snapshotBaseDir(originalBaseDir);
10275
+ const snapshotWorkDir = runnerPaths.snapshotWorkDir(snapshotBaseDir);
10276
+ snapshotPaths = {
10277
+ snapshot: snapshotConfig.snapshot,
10278
+ memory: snapshotConfig.memory,
10279
+ snapshotOverlay: vmPaths.overlay(snapshotWorkDir),
10280
+ snapshotVsockDir: vmPaths.vsockDir(snapshotWorkDir)
10281
+ };
10282
+ }
10283
+ await withSandboxTiming("vm_create", () => vm.start(snapshotPaths));
10216
10284
  guestIp = vm.getGuestIp();
10217
10285
  vethNsIp = vm.getNetns()?.vethNsIp ?? null;
10218
10286
  if (!guestIp || !vethNsIp) {
@@ -10240,6 +10308,10 @@ async function executeJob(context, config, options = {}) {
10240
10308
  logger9.log(`Waiting for guest connection...`);
10241
10309
  await withSandboxTiming("guest_wait", () => guestConnectionPromise);
10242
10310
  logger9.log(`Guest client ready`);
10311
+ if (config.firecracker.snapshot) {
10312
+ const timestamp = (Date.now() / 1e3).toFixed(3);
10313
+ await guest.exec(`date -s "@${timestamp}"`);
10314
+ }
10243
10315
  if (context.storageManifest) {
10244
10316
  await withSandboxTiming(
10245
10317
  "storage_download",
@@ -10442,7 +10514,7 @@ async function isPortInUse(port) {
10442
10514
 
10443
10515
  // src/lib/runner/runner-lock.ts
10444
10516
  import fs10 from "fs";
10445
- import path5 from "path";
10517
+ import path7 from "path";
10446
10518
  var logger11 = createLogger("RunnerLock");
10447
10519
  var DEFAULT_PID_FILE = runtimePaths.runnerPid;
10448
10520
  var currentPidFile = null;
@@ -10459,7 +10531,7 @@ function isProcessRunning(pid) {
10459
10531
  }
10460
10532
  function acquireRunnerLock(options = {}) {
10461
10533
  const pidFile = options.pidFile ?? DEFAULT_PID_FILE;
10462
- const runDir = path5.dirname(pidFile);
10534
+ const runDir = path7.dirname(pidFile);
10463
10535
  fs10.mkdirSync(runDir, { recursive: true });
10464
10536
  if (fs10.existsSync(pidFile)) {
10465
10537
  const pidStr = fs10.readFileSync(pidFile, "utf-8").trim();
@@ -10493,7 +10565,7 @@ function releaseRunnerLock() {
10493
10565
  var logger12 = createLogger("Runner");
10494
10566
  async function setupEnvironment(options) {
10495
10567
  const { config } = options;
10496
- await acquireRunnerLock();
10568
+ acquireRunnerLock();
10497
10569
  const networkCheck = checkNetworkPrerequisites();
10498
10570
  if (!networkCheck.ok) {
10499
10571
  logger12.error("Network prerequisites not met:");
@@ -10523,10 +10595,16 @@ async function setupEnvironment(options) {
10523
10595
  );
10524
10596
  }
10525
10597
  logger12.log("Initializing overlay pool...");
10598
+ const snapshotConfig = config.firecracker.snapshot;
10526
10599
  await initOverlayPool({
10527
10600
  size: config.sandbox.max_concurrent + 2,
10528
10601
  replenishThreshold: config.sandbox.max_concurrent,
10529
- poolDir: runnerPaths.overlayPool(config.base_dir)
10602
+ poolDir: runnerPaths.overlayPool(config.base_dir),
10603
+ createFile: snapshotConfig ? (filePath) => execCommand(
10604
+ `cp --sparse=always "${snapshotConfig.overlay}" "${filePath}"`,
10605
+ false
10606
+ ).then(() => {
10607
+ }) : void 0
10530
10608
  });
10531
10609
  logger12.log("Initializing namespace pool...");
10532
10610
  await initNetnsPool({
@@ -10834,7 +10912,7 @@ import { existsSync as existsSync5, readFileSync as readFileSync3, readdirSync a
10834
10912
 
10835
10913
  // src/lib/firecracker/process.ts
10836
10914
  import { readdirSync, readFileSync as readFileSync2, existsSync as existsSync4 } from "fs";
10837
- import path6 from "path";
10915
+ import path8 from "path";
10838
10916
  function parseFirecrackerCmdline(cmdline) {
10839
10917
  const args = cmdline.split("\0");
10840
10918
  if (!args[0]?.includes("firecracker")) return null;
@@ -10867,7 +10945,7 @@ function findFirecrackerProcesses() {
10867
10945
  for (const entry of entries) {
10868
10946
  if (!/^\d+$/.test(entry)) continue;
10869
10947
  const pid = parseInt(entry, 10);
10870
- const cmdlinePath = path6.join(procDir, entry, "cmdline");
10948
+ const cmdlinePath = path8.join(procDir, entry, "cmdline");
10871
10949
  if (!existsSync4(cmdlinePath)) continue;
10872
10950
  try {
10873
10951
  const cmdline = readFileSync2(cmdlinePath, "utf-8");
@@ -10925,7 +11003,7 @@ function findMitmproxyProcess() {
10925
11003
  for (const entry of entries) {
10926
11004
  if (!/^\d+$/.test(entry)) continue;
10927
11005
  const pid = parseInt(entry, 10);
10928
- const cmdlinePath = path6.join(procDir, entry, "cmdline");
11006
+ const cmdlinePath = path8.join(procDir, entry, "cmdline");
10929
11007
  if (!existsSync4(cmdlinePath)) continue;
10930
11008
  try {
10931
11009
  const cmdline = readFileSync2(cmdlinePath, "utf-8");
@@ -10941,142 +11019,127 @@ function findMitmproxyProcess() {
10941
11019
  }
10942
11020
 
10943
11021
  // src/commands/doctor.ts
10944
- var doctorCommand = new Command2("doctor").description("Diagnose runner health, check network, and detect issues").option("--config <path>", "Config file path", "./runner.yaml").action(
10945
- // eslint-disable-next-line complexity -- TODO: refactor complex function
10946
- async (options) => {
10947
- try {
10948
- const config = loadConfig(options.config);
10949
- const statusFilePath = runnerPaths.statusFile(config.base_dir);
10950
- const workspacesDir = runnerPaths.workspacesDir(config.base_dir);
10951
- console.log(`Runner: ${config.name}`);
10952
- let status = null;
10953
- if (existsSync5(statusFilePath)) {
10954
- try {
10955
- status = JSON.parse(
10956
- readFileSync3(statusFilePath, "utf-8")
10957
- );
10958
- console.log(`Mode: ${status.mode}`);
10959
- if (status.started_at) {
10960
- const started = new Date(status.started_at);
10961
- const uptime = formatUptime(Date.now() - started.getTime());
10962
- console.log(
10963
- `Started: ${started.toLocaleString()} (uptime: ${uptime})`
10964
- );
10965
- }
10966
- } catch {
10967
- console.log("Mode: unknown (status.json unreadable)");
10968
- }
10969
- } else {
10970
- console.log("Mode: unknown (no status.json)");
10971
- }
10972
- console.log("");
10973
- console.log("API Connectivity:");
10974
- try {
10975
- await pollForJob(config.server, config.group);
10976
- console.log(` \u2713 Connected to ${config.server.url}`);
10977
- console.log(" \u2713 Authentication: OK");
10978
- } catch (error) {
10979
- console.log(` \u2717 Cannot connect to ${config.server.url}`);
10980
- console.log(
10981
- ` Error: ${error instanceof Error ? error.message : "Unknown error"}`
10982
- );
10983
- }
10984
- console.log("");
10985
- console.log("Network:");
10986
- const warnings = [];
10987
- const proxyPort = config.proxy.port;
10988
- const mitmProc = findMitmproxyProcess();
10989
- const portInUse = await isPortInUse(proxyPort);
10990
- if (mitmProc) {
10991
- console.log(
10992
- ` \u2713 Proxy mitmproxy (PID ${mitmProc.pid}) on :${proxyPort}`
10993
- );
10994
- } else if (portInUse) {
10995
- console.log(
10996
- ` \u26A0\uFE0F Proxy port :${proxyPort} in use but mitmproxy process not found`
10997
- );
10998
- warnings.push({
10999
- message: `Port ${proxyPort} is in use but mitmproxy process not detected`
11000
- });
11001
- } else {
11002
- console.log(` \u2717 Proxy mitmproxy not running`);
11003
- warnings.push({ message: "Proxy mitmproxy is not running" });
11004
- }
11005
- console.log(
11006
- ` \u2139 Namespaces: each VM runs in isolated namespace with IP ${SNAPSHOT_NETWORK.guestIp}`
11007
- );
11008
- console.log("");
11009
- const processes = findFirecrackerProcesses();
11010
- const workspaces = existsSync5(workspacesDir) ? readdirSync2(workspacesDir).filter(runnerPaths.isVmWorkspace) : [];
11011
- const jobs = [];
11012
- const statusVmIds = /* @__PURE__ */ new Set();
11013
- if (status?.active_run_ids) {
11014
- for (const runId of status.active_run_ids) {
11015
- const vmId = createVmId(runId);
11016
- statusVmIds.add(vmId);
11017
- const proc = processes.find((p) => p.vmId === vmId);
11018
- jobs.push({
11019
- runId,
11020
- vmId,
11021
- hasProcess: !!proc,
11022
- pid: proc?.pid
11023
- });
11024
- }
11025
- }
11026
- const maxConcurrent = config.sandbox.max_concurrent;
11027
- console.log(`Runs (${jobs.length} active, max ${maxConcurrent}):`);
11028
- if (jobs.length === 0) {
11029
- console.log(" No active runs");
11030
- } else {
11031
- console.log(
11032
- " Run ID VM ID Status"
11033
- );
11034
- for (const job of jobs) {
11035
- const statusText = job.hasProcess ? `\u2713 Running (PID ${job.pid})` : "\u26A0\uFE0F No process";
11036
- console.log(` ${job.runId} ${job.vmId} ${statusText}`);
11037
- }
11038
- }
11039
- console.log("");
11040
- for (const job of jobs) {
11041
- if (!job.hasProcess) {
11042
- warnings.push({
11043
- message: `Run ${job.vmId} in status.json but no Firecracker process running`
11044
- });
11045
- }
11046
- }
11047
- const processVmIds = new Set(processes.map((p) => p.vmId));
11048
- for (const proc of processes) {
11049
- if (!statusVmIds.has(proc.vmId)) {
11050
- warnings.push({
11051
- message: `Orphan process: PID ${proc.pid} (vmId ${proc.vmId}) not in status.json`
11052
- });
11053
- }
11054
- }
11055
- for (const ws of workspaces) {
11056
- const vmId = runnerPaths.extractVmId(ws);
11057
- if (!processVmIds.has(vmId) && !statusVmIds.has(vmId)) {
11058
- warnings.push({
11059
- message: `Orphan workspace: ${ws} (no matching job or process)`
11060
- });
11061
- }
11062
- }
11063
- console.log("Warnings:");
11064
- if (warnings.length === 0) {
11065
- console.log(" None");
11066
- } else {
11067
- for (const w of warnings) {
11068
- console.log(` - ${w.message}`);
11069
- }
11070
- }
11071
- process.exit(warnings.length > 0 ? 1 : 0);
11072
- } catch (error) {
11073
- console.error(
11074
- `Error: ${error instanceof Error ? error.message : "Unknown error"}`
11075
- );
11076
- process.exit(1);
11022
+ function displayRunnerStatus(statusFilePath) {
11023
+ if (!existsSync5(statusFilePath)) {
11024
+ console.log("Mode: unknown (no status.json)");
11025
+ return null;
11026
+ }
11027
+ try {
11028
+ const status = JSON.parse(
11029
+ readFileSync3(statusFilePath, "utf-8")
11030
+ );
11031
+ console.log(`Mode: ${status.mode}`);
11032
+ if (status.started_at) {
11033
+ const started = new Date(status.started_at);
11034
+ const uptime = formatUptime(Date.now() - started.getTime());
11035
+ console.log(`Started: ${started.toLocaleString()} (uptime: ${uptime})`);
11077
11036
  }
11037
+ return status;
11038
+ } catch {
11039
+ console.log("Mode: unknown (status.json unreadable)");
11040
+ return null;
11078
11041
  }
11079
- );
11042
+ }
11043
+ async function checkApiConnectivity(config) {
11044
+ console.log("API Connectivity:");
11045
+ try {
11046
+ await pollForJob(config.server, config.group);
11047
+ console.log(` \u2713 Connected to ${config.server.url}`);
11048
+ console.log(" \u2713 Authentication: OK");
11049
+ } catch (error) {
11050
+ console.log(` \u2717 Cannot connect to ${config.server.url}`);
11051
+ console.log(
11052
+ ` Error: ${error instanceof Error ? error.message : "Unknown error"}`
11053
+ );
11054
+ }
11055
+ }
11056
+ async function checkNetwork(config, warnings) {
11057
+ console.log("Network:");
11058
+ const proxyPort = config.proxy.port;
11059
+ const mitmProc = findMitmproxyProcess();
11060
+ const portInUse = await isPortInUse(proxyPort);
11061
+ if (mitmProc) {
11062
+ console.log(` \u2713 Proxy mitmproxy (PID ${mitmProc.pid}) on :${proxyPort}`);
11063
+ } else if (portInUse) {
11064
+ console.log(
11065
+ ` \u26A0\uFE0F Proxy port :${proxyPort} in use but mitmproxy process not found`
11066
+ );
11067
+ warnings.push({
11068
+ message: `Port ${proxyPort} is in use but mitmproxy process not detected`
11069
+ });
11070
+ } else {
11071
+ console.log(` \u2717 Proxy mitmproxy not running`);
11072
+ warnings.push({ message: "Proxy mitmproxy is not running" });
11073
+ }
11074
+ console.log(
11075
+ ` \u2139 Namespaces: each VM runs in isolated namespace with IP ${SNAPSHOT_NETWORK.guestIp}`
11076
+ );
11077
+ }
11078
+ function buildJobInfo(status, processes) {
11079
+ const jobs = [];
11080
+ const statusVmIds = /* @__PURE__ */ new Set();
11081
+ if (status?.active_run_ids) {
11082
+ for (const runId of status.active_run_ids) {
11083
+ const vmId = createVmId(runId);
11084
+ statusVmIds.add(vmId);
11085
+ const proc = processes.find((p) => p.vmId === vmId);
11086
+ jobs.push({
11087
+ runId,
11088
+ vmId,
11089
+ hasProcess: !!proc,
11090
+ pid: proc?.pid
11091
+ });
11092
+ }
11093
+ }
11094
+ return { jobs, statusVmIds };
11095
+ }
11096
+ function displayRuns(jobs, maxConcurrent) {
11097
+ console.log(`Runs (${jobs.length} active, max ${maxConcurrent}):`);
11098
+ if (jobs.length === 0) {
11099
+ console.log(" No active runs");
11100
+ return;
11101
+ }
11102
+ console.log(" Run ID VM ID Status");
11103
+ for (const job of jobs) {
11104
+ const statusText = job.hasProcess ? `\u2713 Running (PID ${job.pid})` : "\u26A0\uFE0F No process";
11105
+ console.log(` ${job.runId} ${job.vmId} ${statusText}`);
11106
+ }
11107
+ }
11108
+ function detectOrphanResources(jobs, processes, workspaces, statusVmIds, warnings) {
11109
+ for (const job of jobs) {
11110
+ if (!job.hasProcess) {
11111
+ warnings.push({
11112
+ message: `Run ${job.vmId} in status.json but no Firecracker process running`
11113
+ });
11114
+ }
11115
+ }
11116
+ const processVmIds = new Set(processes.map((p) => p.vmId));
11117
+ for (const proc of processes) {
11118
+ if (!statusVmIds.has(proc.vmId)) {
11119
+ warnings.push({
11120
+ message: `Orphan process: PID ${proc.pid} (vmId ${proc.vmId}) not in status.json`
11121
+ });
11122
+ }
11123
+ }
11124
+ for (const ws of workspaces) {
11125
+ const vmId = runnerPaths.extractVmId(ws);
11126
+ if (!processVmIds.has(vmId) && !statusVmIds.has(vmId)) {
11127
+ warnings.push({
11128
+ message: `Orphan workspace: ${ws} (no matching job or process)`
11129
+ });
11130
+ }
11131
+ }
11132
+ }
11133
+ function displayWarnings(warnings) {
11134
+ console.log("Warnings:");
11135
+ if (warnings.length === 0) {
11136
+ console.log(" None");
11137
+ } else {
11138
+ for (const w of warnings) {
11139
+ console.log(` - ${w.message}`);
11140
+ }
11141
+ }
11142
+ }
11080
11143
  function formatUptime(ms) {
11081
11144
  const seconds = Math.floor(ms / 1e3);
11082
11145
  const minutes = Math.floor(seconds / 60);
@@ -11087,6 +11150,34 @@ function formatUptime(ms) {
11087
11150
  if (minutes > 0) return `${minutes}m`;
11088
11151
  return `${seconds}s`;
11089
11152
  }
11153
+ var doctorCommand = new Command2("doctor").description("Diagnose runner health, check network, and detect issues").option("--config <path>", "Config file path", "./runner.yaml").action(async (options) => {
11154
+ try {
11155
+ const config = loadConfig(options.config);
11156
+ const statusFilePath = runnerPaths.statusFile(config.base_dir);
11157
+ const workspacesDir = runnerPaths.workspacesDir(config.base_dir);
11158
+ const warnings = [];
11159
+ console.log(`Runner: ${config.name}`);
11160
+ const status = displayRunnerStatus(statusFilePath);
11161
+ console.log("");
11162
+ await checkApiConnectivity(config);
11163
+ console.log("");
11164
+ await checkNetwork(config, warnings);
11165
+ console.log("");
11166
+ const processes = findFirecrackerProcesses();
11167
+ const workspaces = existsSync5(workspacesDir) ? readdirSync2(workspacesDir).filter(runnerPaths.isVmWorkspace) : [];
11168
+ const { jobs, statusVmIds } = buildJobInfo(status, processes);
11169
+ displayRuns(jobs, config.sandbox.max_concurrent);
11170
+ console.log("");
11171
+ detectOrphanResources(jobs, processes, workspaces, statusVmIds, warnings);
11172
+ displayWarnings(warnings);
11173
+ process.exit(warnings.length > 0 ? 1 : 0);
11174
+ } catch (error) {
11175
+ console.error(
11176
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
11177
+ );
11178
+ process.exit(1);
11179
+ }
11180
+ });
11090
11181
 
11091
11182
  // src/commands/kill.ts
11092
11183
  import { Command as Command3 } from "commander";
@@ -11323,10 +11414,16 @@ var benchmarkCommand = new Command4("benchmark").description(
11323
11414
  process.exit(1);
11324
11415
  }
11325
11416
  timer.log("Initializing pools...");
11417
+ const snapshotConfig = config.firecracker.snapshot;
11326
11418
  await initOverlayPool({
11327
11419
  size: 2,
11328
11420
  replenishThreshold: 1,
11329
- poolDir: runnerPaths.overlayPool(config.base_dir)
11421
+ poolDir: runnerPaths.overlayPool(config.base_dir),
11422
+ createFile: snapshotConfig ? (filePath) => execCommand(
11423
+ `cp --sparse=always "${snapshotConfig.overlay}" "${filePath}"`,
11424
+ false
11425
+ ).then(() => {
11426
+ }) : void 0
11330
11427
  });
11331
11428
  await initNetnsPool({ name: config.name, size: 2 });
11332
11429
  poolsInitialized = true;
@@ -11354,12 +11451,219 @@ var benchmarkCommand = new Command4("benchmark").description(
11354
11451
  process.exit(exitCode);
11355
11452
  });
11356
11453
 
11454
+ // src/commands/snapshot.ts
11455
+ import { Command as Command5 } from "commander";
11456
+ import { spawn as spawn3 } from "child_process";
11457
+ import fs11 from "fs";
11458
+ import os2 from "os";
11459
+ import readline3 from "readline";
11460
+ var logger15 = createLogger("Snapshot");
11461
+ function startFirecracker(nsName, firecrackerBinary, apiSocketPath, workDir) {
11462
+ logger15.log("Starting Firecracker with API socket...");
11463
+ const currentUser = os2.userInfo().username;
11464
+ const fcProcess = spawn3(
11465
+ "sudo",
11466
+ [
11467
+ "ip",
11468
+ "netns",
11469
+ "exec",
11470
+ nsName,
11471
+ "sudo",
11472
+ "-u",
11473
+ currentUser,
11474
+ firecrackerBinary,
11475
+ "--api-sock",
11476
+ apiSocketPath
11477
+ ],
11478
+ {
11479
+ cwd: workDir,
11480
+ stdio: ["ignore", "pipe", "pipe"],
11481
+ detached: false
11482
+ }
11483
+ );
11484
+ if (fcProcess.stdout) {
11485
+ const stdoutRL = readline3.createInterface({ input: fcProcess.stdout });
11486
+ stdoutRL.on("line", (line) => {
11487
+ if (line.trim()) logger15.log(`[FC] ${line}`);
11488
+ });
11489
+ }
11490
+ if (fcProcess.stderr) {
11491
+ const stderrRL = readline3.createInterface({ input: fcProcess.stderr });
11492
+ stderrRL.on("line", (line) => {
11493
+ if (line.trim()) logger15.log(`[FC stderr] ${line}`);
11494
+ });
11495
+ }
11496
+ fcProcess.on("error", (err) => logger15.log(`Firecracker error: ${err}`));
11497
+ fcProcess.on(
11498
+ "exit",
11499
+ (code, signal) => logger15.log(`Firecracker exited: code=${code}, signal=${signal}`)
11500
+ );
11501
+ return fcProcess;
11502
+ }
11503
+ var snapshotCommand = new Command5("snapshot").description("Generate a Firecracker snapshot for fast VM startup").argument("<output-dir>", "Output directory for snapshot files").option("--config <path>", "Config file path", "./runner.yaml").action(
11504
+ async (outputDir, opts) => {
11505
+ const options = {
11506
+ config: opts.config ?? "./runner.yaml",
11507
+ output: outputDir
11508
+ };
11509
+ const timer = new Timer();
11510
+ setGlobalLogger(timer.log.bind(timer));
11511
+ logger15.log("Loading configuration...");
11512
+ const config = loadDebugConfig(options.config);
11513
+ validateFirecrackerPaths(config.firecracker);
11514
+ const nsName = "vm0-snapshot";
11515
+ const workDir = runnerPaths.snapshotWorkDir(config.base_dir);
11516
+ const overlayPath = vmPaths.overlay(workDir);
11517
+ const vsockPath = vmPaths.vsock(workDir);
11518
+ const apiSocketPath = vmPaths.apiSock(workDir);
11519
+ const outputSnapshot = snapshotOutputPaths.snapshot(options.output);
11520
+ const outputMemory = snapshotOutputPaths.memory(options.output);
11521
+ const outputOverlay = snapshotOutputPaths.overlay(options.output);
11522
+ let fcProcess = null;
11523
+ let vsockClient = null;
11524
+ let exitCode = 0;
11525
+ try {
11526
+ if (fs11.existsSync(workDir)) {
11527
+ logger15.log("Cleaning up stale work directory...");
11528
+ fs11.rmSync(workDir, { recursive: true, force: true });
11529
+ }
11530
+ logger15.log(`Creating directories...`);
11531
+ fs11.mkdirSync(options.output, { recursive: true });
11532
+ fs11.mkdirSync(workDir, { recursive: true });
11533
+ fs11.mkdirSync(vmPaths.vsockDir(workDir), { recursive: true });
11534
+ logger15.log("Creating overlay filesystem...");
11535
+ await createOverlayFile(overlayPath);
11536
+ logger15.log(`Overlay created: ${overlayPath}`);
11537
+ logger15.log(`Creating network namespace: ${nsName}`);
11538
+ await deleteNetns(nsName);
11539
+ await createNetnsWithTap(nsName, {
11540
+ tapName: SNAPSHOT_NETWORK.tapName,
11541
+ gatewayIpWithPrefix: `${SNAPSHOT_NETWORK.gatewayIp}/${SNAPSHOT_NETWORK.prefixLen}`
11542
+ });
11543
+ logger15.log("Network namespace created");
11544
+ fcProcess = startFirecracker(
11545
+ nsName,
11546
+ config.firecracker.binary,
11547
+ apiSocketPath,
11548
+ workDir
11549
+ );
11550
+ const apiClient = new FirecrackerClient(apiSocketPath);
11551
+ logger15.log("Waiting for API to be ready...");
11552
+ await apiClient.waitForReady();
11553
+ logger15.log("API ready");
11554
+ logger15.log("Configuring VM via API...");
11555
+ await Promise.all([
11556
+ apiClient.configureMachine({
11557
+ vcpu_count: config.sandbox.vcpu,
11558
+ mem_size_mib: config.sandbox.memory_mb
11559
+ }),
11560
+ apiClient.configureBootSource({
11561
+ kernel_image_path: config.firecracker.kernel,
11562
+ boot_args: buildBootArgs()
11563
+ }),
11564
+ apiClient.configureDrive({
11565
+ drive_id: "rootfs",
11566
+ path_on_host: config.firecracker.rootfs,
11567
+ is_root_device: true,
11568
+ is_read_only: true
11569
+ }),
11570
+ apiClient.configureDrive({
11571
+ drive_id: "overlay",
11572
+ path_on_host: overlayPath,
11573
+ is_root_device: false,
11574
+ is_read_only: false
11575
+ }),
11576
+ apiClient.configureNetworkInterface({
11577
+ iface_id: "eth0",
11578
+ guest_mac: SNAPSHOT_NETWORK.guestMac,
11579
+ host_dev_name: SNAPSHOT_NETWORK.tapName
11580
+ }),
11581
+ apiClient.configureVsock({
11582
+ guest_cid: 3,
11583
+ uds_path: vsockPath
11584
+ })
11585
+ ]);
11586
+ logger15.log("VM configured");
11587
+ logger15.log("Starting vsock listener...");
11588
+ vsockClient = new VsockClient(vsockPath);
11589
+ const guestConnectionPromise = vsockClient.waitForGuestConnection(6e4);
11590
+ logger15.log("Starting VM...");
11591
+ await apiClient.startInstance();
11592
+ logger15.log("VM started");
11593
+ logger15.log("Waiting for guest connection...");
11594
+ await guestConnectionPromise;
11595
+ logger15.log("Guest connected");
11596
+ logger15.log("Verifying guest is responsive...");
11597
+ const reachable = await vsockClient.isReachable();
11598
+ if (!reachable) {
11599
+ throw new Error("Guest is not responsive");
11600
+ }
11601
+ logger15.log("Guest is responsive");
11602
+ logger15.log("Pausing VM...");
11603
+ await apiClient.pause();
11604
+ logger15.log("VM paused");
11605
+ logger15.log("Creating snapshot...");
11606
+ await apiClient.createSnapshot({
11607
+ snapshot_type: "Full",
11608
+ snapshot_path: outputSnapshot,
11609
+ mem_file_path: outputMemory
11610
+ });
11611
+ logger15.log("Snapshot created");
11612
+ logger15.log("Copying overlay as golden overlay...");
11613
+ await execCommand(
11614
+ `cp --sparse=always "${overlayPath}" "${outputOverlay}"`,
11615
+ false
11616
+ );
11617
+ logger15.log("Golden overlay created");
11618
+ logger15.log("=".repeat(40));
11619
+ logger15.log("Snapshot generation complete!");
11620
+ logger15.log("Files (logical size):");
11621
+ const lsOutput = await execCommand(`ls -lh "${options.output}"`, false);
11622
+ logger15.log(lsOutput);
11623
+ logger15.log("Actual disk usage:");
11624
+ const duOutput = await execCommand(
11625
+ `du -h "${options.output}"/*`,
11626
+ false
11627
+ );
11628
+ logger15.log(duOutput);
11629
+ logger15.log("=".repeat(40));
11630
+ } catch (error) {
11631
+ logger15.error(
11632
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
11633
+ );
11634
+ exitCode = 1;
11635
+ } finally {
11636
+ logger15.log("Cleaning up...");
11637
+ if (vsockClient) {
11638
+ vsockClient.close();
11639
+ }
11640
+ if (fcProcess && !fcProcess.killed) {
11641
+ fcProcess.kill("SIGKILL");
11642
+ }
11643
+ await execCommand(
11644
+ `pkill -9 -f "firecracker.*${apiSocketPath}"`,
11645
+ true
11646
+ ).catch(() => {
11647
+ });
11648
+ await deleteNetns(nsName);
11649
+ if (fs11.existsSync(workDir)) {
11650
+ fs11.rmSync(workDir, { recursive: true, force: true });
11651
+ }
11652
+ logger15.log("Cleanup complete");
11653
+ }
11654
+ if (exitCode !== 0) {
11655
+ process.exit(exitCode);
11656
+ }
11657
+ }
11658
+ );
11659
+
11357
11660
  // src/index.ts
11358
- var version = true ? "3.11.3" : "0.1.0";
11661
+ var version = true ? "3.12.0" : "0.1.0";
11359
11662
  program.name("vm0-runner").version(version).description("Self-hosted runner for VM0 agents");
11360
11663
  program.addCommand(startCommand);
11361
11664
  program.addCommand(doctorCommand);
11362
11665
  program.addCommand(killCommand);
11363
11666
  program.addCommand(benchmarkCommand);
11667
+ program.addCommand(snapshotCommand);
11364
11668
  program.parse();
11365
11669
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vm0/runner",
3
- "version": "3.11.3",
3
+ "version": "3.12.0",
4
4
  "description": "Self-hosted runner for VM0 agents",
5
5
  "repository": {
6
6
  "type": "git",