vmsan 0.1.0-alpha.2 → 0.1.0-alpha.21

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.
@@ -1,19 +1,16 @@
1
- import { n as vmsanPaths } from "./paths.mjs";
2
- import { t as handleCommandError, z as mutuallyExclusiveFlagsError } from "./errors.mjs";
1
+ import "./paths.mjs";
2
+ import { t as handleCommandError } from "./errors.mjs";
3
3
  import { i as initVmsanLogger, n as createScopedLogger, t as createCommandLogger } from "./logger.mjs";
4
- import { t as FirecrackerClient } from "./firecracker.mjs";
5
- import { n as generateVmId, t as FileVmStateStore } from "./vm-state.mjs";
6
- import { a as getVmPid, c as NetworkManager, n as findKernel, o as validateEnvironment, r as findRootfs, s as waitForSocket } from "./environment.mjs";
7
- import { a as Jailer, i as markVmAsError, n as cleanupNetwork, r as killOrphanVmProcess, t as cleanupChroot } from "./cleanup.mjs";
4
+ import "./vm-state.mjs";
5
+ import { t as createVmsan } from "./context.mjs";
6
+ import "./firecracker.mjs";
7
+ import "./timeout-killer.mjs";
8
+ import { n as waitForAgent } from "./vm-context.mjs";
8
9
  import { t as ShellSession } from "./shell.mjs";
9
10
  import { a as parseImageReference, t as parseBandwidth } from "./validation.mjs";
10
- import { a as buildInitialVmState, i as buildCreateSummaryLines, o as parseCreateInput, r as ensureSeccompFilter, t as resolveImageRootfs } from "./image-rootfs.mjs";
11
- import { t as waitForAgent } from "./connect.mjs";
11
+ import { n as parseCreateInput, t as buildCreateSummaryLines } from "./summary.mjs";
12
12
  import { join } from "node:path";
13
- import { spawn } from "node:child_process";
14
13
  import { consola } from "consola";
15
- import { existsSync } from "node:fs";
16
- import { randomBytes } from "node:crypto";
17
14
  import { defineCommand } from "citty";
18
15
  const createCommandArgs = {
19
16
  vcpus: {
@@ -118,13 +115,6 @@ const createCommandArgs = {
118
115
  }
119
116
  };
120
117
  const RUNTIME_DEFAULT_IMAGES = { "node22-demo": "node:22" };
121
- function createLifecycleState() {
122
- return {
123
- networkConfig: void 0,
124
- vmId: void 0,
125
- chrootDir: void 0
126
- };
127
- }
128
118
  const createCommand = defineCommand({
129
119
  meta: {
130
120
  name: "create",
@@ -135,161 +125,45 @@ const createCommand = defineCommand({
135
125
  const commandArgs = args;
136
126
  if (commandArgs.silent) initVmsanLogger("silent");
137
127
  const cmdLog = createCommandLogger("create");
138
- const lifecycle = createLifecycleState();
139
- const paths = vmsanPaths();
140
- const store = new FileVmStateStore(paths.vmsDir);
141
128
  try {
142
- const baseDir = paths.baseDir;
143
- validateEnvironment(baseDir);
144
- if (commandArgs["from-image"] && commandArgs.rootfs) throw mutuallyExclusiveFlagsError("--from-image", "--rootfs");
145
- const parsedInput = parseCreateInput(commandArgs, paths);
129
+ const vmsan = await createVmsan();
130
+ const parsedInput = parseCreateInput(commandArgs, vmsan.paths);
146
131
  const defaultImage = RUNTIME_DEFAULT_IMAGES[parsedInput.runtime];
147
132
  if (defaultImage && !commandArgs["from-image"] && !commandArgs.rootfs) {
148
133
  commandArgs["from-image"] = defaultImage;
149
134
  consola.info(`Runtime "${parsedInput.runtime}" auto-selected image: ${defaultImage}`);
150
135
  }
151
136
  if (parsedInput.runtime === "node22-demo" && parsedInput.ports.length === 0) consola.warn("Runtime node22-demo serves a welcome page, but no --publish-port was specified. The page won't be accessible externally.");
152
- const kernelPath = typeof commandArgs.kernel === "string" ? commandArgs.kernel : findKernel(baseDir);
153
- consola.debug(`Kernel resolved: ${kernelPath}`);
154
- let rootfsPath;
155
- if (typeof commandArgs["from-image"] === "string") rootfsPath = resolveImageRootfs(parseImageReference(commandArgs["from-image"]), paths.registryDir);
156
- else rootfsPath = typeof commandArgs.rootfs === "string" ? commandArgs.rootfs : findRootfs(baseDir);
157
- consola.debug(`Rootfs resolved: ${rootfsPath}`);
158
137
  if (parsedInput.domains.length > 0) consola.warn("Domain filtering is DNS-based best effort. Direct-IP and DoH traffic may bypass allow-lists.");
159
138
  const bandwidthMbit = parseBandwidth(commandArgs.bandwidth);
160
- lifecycle.vmId = generateVmId();
161
- const log = createScopedLogger(lifecycle.vmId);
162
- const slot = store.allocateNetworkSlot();
163
- consola.debug(`Network slot allocated: ${slot}`);
164
- const netnsName = commandArgs["no-netns"] ? void 0 : `vmsan-${lifecycle.vmId}`;
165
- const net = new NetworkManager(slot, parsedInput.networkPolicy, parsedInput.domains, parsedInput.allowedCidrs, parsedInput.deniedCidrs, parsedInput.ports, bandwidthMbit, netnsName);
166
- lifecycle.networkConfig = net.config;
167
- log.start(`Creating VM ${lifecycle.vmId}...`);
168
- const agentToken = existsSync(paths.agentBin) ? randomBytes(32).toString("hex") : null;
169
- const state = buildInitialVmState({
170
- vmId: lifecycle.vmId,
171
- project: commandArgs.project || "",
172
- runtime: parsedInput.runtime,
173
- diskSizeGb: parsedInput.diskSizeGb,
174
- kernelPath,
175
- rootfsPath,
139
+ const fromImage = commandArgs["from-image"] ? parseImageReference(commandArgs["from-image"]) : void 0;
140
+ const result = await vmsan.create({
176
141
  vcpus: parsedInput.vcpus,
177
142
  memMib: parsedInput.memMib,
143
+ diskSizeGb: parsedInput.diskSizeGb,
144
+ kernelPath: typeof commandArgs.kernel === "string" ? commandArgs.kernel : void 0,
145
+ rootfsPath: typeof commandArgs.rootfs === "string" ? commandArgs.rootfs : void 0,
146
+ fromImage,
147
+ project: commandArgs.project || "",
148
+ runtime: parsedInput.runtime,
178
149
  networkPolicy: parsedInput.networkPolicy,
179
150
  domains: parsedInput.domains,
180
151
  allowedCidrs: parsedInput.allowedCidrs,
181
152
  deniedCidrs: parsedInput.deniedCidrs,
182
153
  ports: parsedInput.ports,
183
- tapDevice: lifecycle.networkConfig.tapDevice,
184
- hostIp: lifecycle.networkConfig.hostIp,
185
- guestIp: lifecycle.networkConfig.guestIp,
186
- subnetMask: lifecycle.networkConfig.subnetMask,
187
- macAddress: lifecycle.networkConfig.macAddress,
188
- snapshotId: parsedInput.snapshotId,
189
- timeoutMs: parsedInput.timeoutMs,
190
- agentToken,
191
- agentPort: paths.agentPort,
192
154
  bandwidthMbit,
193
- netnsName
155
+ disableNetns: commandArgs["no-netns"],
156
+ disableSeccomp: commandArgs["no-seccomp"],
157
+ disablePidNs: commandArgs["no-pid-ns"],
158
+ disableCgroup: commandArgs["no-cgroup"],
159
+ timeoutMs: parsedInput.timeoutMs ?? void 0,
160
+ snapshotId: parsedInput.snapshotId ?? void 0
194
161
  });
195
- store.save(state);
196
- log.start("Setting up networking...");
197
- await net.setup();
198
- log.success(`Network: TAP ${lifecycle.networkConfig.tapDevice}, Host ${lifecycle.networkConfig.hostIp}, Guest ${lifecycle.networkConfig.guestIp}`);
199
- log.start("Preparing chroot...");
200
- const snapshotConfig = parsedInput.snapshotId ? {
201
- snapshotFile: join(paths.snapshotsDir, parsedInput.snapshotId, "snapshot_file"),
202
- memFile: join(paths.snapshotsDir, parsedInput.snapshotId, "mem_file")
203
- } : void 0;
204
- const jailer = new Jailer(lifecycle.vmId, paths.jailerBaseDir);
205
- const welcomePage = parsedInput.runtime === "node22-demo" && parsedInput.ports.length > 0 ? {
206
- vmId: lifecycle.vmId,
207
- ports: parsedInput.ports
208
- } : void 0;
209
- const agentConfig = agentToken ? {
210
- binaryPath: paths.agentBin,
211
- token: agentToken,
212
- port: paths.agentPort,
213
- vmId: lifecycle.vmId
214
- } : void 0;
215
- const jailerPaths = jailer.prepare({
216
- kernelSrc: kernelPath,
217
- rootfsSrc: rootfsPath,
218
- diskSizeGb: parsedInput.diskSizeGb,
219
- snapshot: snapshotConfig,
220
- welcomePage,
221
- agent: agentConfig
222
- });
223
- lifecycle.chrootDir = jailerPaths.chrootDir;
224
- store.update(lifecycle.vmId, {
225
- chrootDir: jailerPaths.chrootDir,
226
- apiSocket: jailerPaths.socketPath
227
- });
228
- consola.debug(`Jailer chroot: ${jailerPaths.chrootDir}`);
229
- consola.debug(`API socket path: ${jailerPaths.socketPath}`);
230
- log.start("Spawning Firecracker via jailer...");
231
- const firecrackerBin = join(baseDir, "bin", "firecracker");
232
- const jailerBin = join(baseDir, "bin", "jailer");
233
- const seccompFilter = commandArgs["no-seccomp"] ? void 0 : ensureSeccompFilter(paths);
234
- if (seccompFilter) consola.debug(`Seccomp filter: ${seccompFilter}`);
235
- const cgroup = commandArgs["no-cgroup"] ? void 0 : {
236
- cpuQuotaUs: parsedInput.vcpus * 1e5,
237
- cpuPeriodUs: 1e5,
238
- memoryBytes: parsedInput.memMib * 1024 * 1024
239
- };
240
- jailer.spawn({
241
- firecrackerBin,
242
- jailerBin,
243
- chrootBase: jailerPaths.chrootBase,
244
- seccompFilter: seccompFilter ?? void 0,
245
- newPidNs: !commandArgs["no-pid-ns"],
246
- cgroup,
247
- netns: netnsName
248
- });
249
- log.start("Waiting for API socket...");
250
- await waitForSocket(jailerPaths.socketPath, 5e3);
251
- log.success("API socket ready");
252
- const vm = new FirecrackerClient(jailerPaths.socketPath);
253
- if (parsedInput.snapshotId) {
254
- log.start("Restoring from snapshot...");
255
- await vm.loadSnapshot("snapshot/snapshot_file", "snapshot/mem_file");
256
- await vm.resume();
257
- log.success("Snapshot restored and VM resumed");
258
- } else {
259
- const bootArgs = NetworkManager.bootArgs(slot);
260
- consola.debug(`Boot args: ${bootArgs}`);
261
- await vm.boot("kernel/vmlinux", bootArgs);
262
- await vm.addDrive("rootfs", "rootfs/rootfs.ext4", true, false);
263
- await vm.configure(parsedInput.vcpus, parsedInput.memMib);
264
- await vm.addNetwork("eth0", lifecycle.networkConfig.tapDevice, lifecycle.networkConfig.macAddress);
265
- log.start("Starting VM...");
266
- await vm.start();
267
- }
268
- const pid = getVmPid(lifecycle.vmId);
269
- consola.debug(`Firecracker PID: ${pid ?? "unknown"}`);
270
- store.update(lifecycle.vmId, {
271
- status: "running",
272
- pid
273
- });
274
- log.success(`VM ${lifecycle.vmId} is running (PID: ${pid || "unknown"})`);
275
- if (parsedInput.timeoutMs && pid) {
276
- const stateFile = join(paths.vmsDir, `${lifecycle.vmId}.json`);
277
- spawn("bash", ["-c", [
278
- `sleep ${Math.ceil(parsedInput.timeoutMs / 1e3)}`,
279
- `STATE=$(cat "${stateFile}" 2>/dev/null) || exit 0`,
280
- `echo "$STATE" | grep -q '"status":"running"' || exit 0`,
281
- `echo "$STATE" | grep -q '"pid":${pid}' || exit 0`,
282
- `[ -d /proc/${pid} ] || exit 0`,
283
- `grep -q "${lifecycle.vmId}" /proc/${pid}/cmdline 2>/dev/null || exit 0`,
284
- `kill ${pid} 2>/dev/null`
285
- ].join(" && ")], {
286
- detached: true,
287
- stdio: "ignore"
288
- }).unref();
289
- }
162
+ const log = createScopedLogger(result.vmId);
163
+ const state = result.state;
290
164
  const summaryLines = buildCreateSummaryLines({
291
- vmId: lifecycle.vmId,
292
- pid,
165
+ vmId: result.vmId,
166
+ pid: result.pid,
293
167
  vcpus: parsedInput.vcpus,
294
168
  memMib: parsedInput.memMib,
295
169
  runtime: parsedInput.runtime,
@@ -300,50 +174,44 @@ const createCommand = defineCommand({
300
174
  allowedCidrs: parsedInput.allowedCidrs,
301
175
  deniedCidrs: parsedInput.deniedCidrs,
302
176
  ports: parsedInput.ports,
303
- kernelPath,
304
- rootfsPath,
177
+ kernelPath: state.kernel,
178
+ rootfsPath: state.rootfs,
305
179
  snapshotId: parsedInput.snapshotId,
306
180
  timeout: commandArgs.timeout,
307
- socketPath: jailerPaths.socketPath,
308
- chrootDir: jailerPaths.chrootDir,
309
- tapDevice: lifecycle.networkConfig.tapDevice,
310
- hostIp: lifecycle.networkConfig.hostIp,
311
- guestIp: lifecycle.networkConfig.guestIp,
312
- macAddress: lifecycle.networkConfig.macAddress,
313
- stateFilePath: join(paths.vmsDir, `${lifecycle.vmId}.json`)
181
+ socketPath: state.apiSocket,
182
+ chrootDir: state.chrootDir,
183
+ tapDevice: state.network.tapDevice,
184
+ hostIp: state.network.hostIp,
185
+ guestIp: state.network.guestIp,
186
+ macAddress: state.network.macAddress,
187
+ stateFilePath: join(vmsan.paths.vmsDir, `${result.vmId}.json`)
314
188
  });
315
189
  log.box(summaryLines.join("\n"));
316
190
  cmdLog.set({
317
- vmId: lifecycle.vmId,
191
+ vmId: result.vmId,
318
192
  vcpus: parsedInput.vcpus,
319
193
  memMib: parsedInput.memMib,
320
194
  runtime: parsedInput.runtime,
321
195
  networkPolicy: parsedInput.networkPolicy,
322
- guestIp: lifecycle.networkConfig.guestIp,
323
- pid,
324
- kernel: kernelPath,
325
- rootfs: rootfsPath,
196
+ guestIp: state.network.guestIp,
197
+ pid: result.pid,
198
+ kernel: state.kernel,
199
+ rootfs: state.rootfs,
326
200
  diskSizeGb: parsedInput.diskSizeGb
327
201
  });
328
202
  cmdLog.emit();
329
- if (commandArgs.connect && lifecycle.networkConfig && agentToken) {
203
+ if (commandArgs.connect && state.agentToken) {
330
204
  log.start("Waiting for agent to become ready...");
331
- await waitForAgent(lifecycle.networkConfig.guestIp, paths.agentPort);
205
+ await waitForAgent(state.network.guestIp, vmsan.paths.agentPort);
332
206
  log.success("Agent is ready. Connecting via PTY shell...");
333
207
  const shell = new ShellSession({
334
- host: lifecycle.networkConfig.guestIp,
335
- port: paths.agentPort,
336
- token: agentToken
208
+ host: state.network.guestIp,
209
+ port: vmsan.paths.agentPort,
210
+ token: state.agentToken
337
211
  });
338
- if (!(await shell.connect()).sessionDestroyed && shell.sessionId) process.stderr.write(`\nResume this session with:\n vmsan connect ${lifecycle.vmId} --session ${shell.sessionId}\n`);
212
+ if (!(await shell.connect()).sessionDestroyed && shell.sessionId) process.stderr.write(`\nResume this session with:\n vmsan connect ${result.vmId} --session ${shell.sessionId}\n`);
339
213
  }
340
214
  } catch (error) {
341
- if (lifecycle.vmId) {
342
- killOrphanVmProcess(lifecycle.vmId);
343
- markVmAsError(lifecycle.vmId, error, paths);
344
- }
345
- cleanupNetwork(lifecycle.networkConfig);
346
- cleanupChroot(lifecycle.chrootDir);
347
215
  handleCommandError(error, cmdLog);
348
216
  process.exitCode = 1;
349
217
  }
@@ -1,12 +1,12 @@
1
1
  import { n as vmsanPaths } from "./paths.mjs";
2
- import { b as vmNotFoundError, t as handleCommandError } from "./errors.mjs";
2
+ import { t as handleCommandError } from "./errors.mjs";
3
3
  import { t as createCommandLogger } from "./logger.mjs";
4
+ import "./vm-state.mjs";
4
5
  import { t as AgentClient } from "./agent.mjs";
5
- import { t as FileVmStateStore } from "./vm-state.mjs";
6
- import { t as waitForAgent } from "./connect.mjs";
7
- import { basename, resolve } from "node:path";
6
+ import { n as waitForAgent, t as resolveVmState } from "./vm-context.mjs";
7
+ import { basename, join, resolve } from "node:path";
8
+ import { existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
8
9
  import { consola } from "consola";
9
- import { writeFileSync } from "node:fs";
10
10
  import { defineCommand } from "citty";
11
11
  const downloadCommand = defineCommand({
12
12
  meta: {
@@ -29,23 +29,8 @@ const downloadCommand = defineCommand({
29
29
  const cmdLog = createCommandLogger("download");
30
30
  const paths = vmsanPaths();
31
31
  try {
32
- const state = new FileVmStateStore(paths.vmsDir).load(args.vmId);
33
- if (!state) throw vmNotFoundError(args.vmId);
34
- if (state.status !== "running") {
35
- consola.error(`VM ${args.vmId} is not running (status: ${state.status})`);
36
- cmdLog.emit();
37
- process.exitCode = 1;
38
- return;
39
- }
40
- if (!state.agentToken) {
41
- consola.error("VM has no agent token. Cannot download files without the agent.");
42
- cmdLog.emit();
43
- process.exitCode = 1;
44
- return;
45
- }
32
+ const { state, guestIp, port } = resolveVmState(args.vmId, paths);
46
33
  const log = consola.withTag(args.vmId);
47
- const guestIp = state.network.guestIp;
48
- const port = state.agentPort || paths.agentPort;
49
34
  consola.debug(`Agent endpoint: ${guestIp}:${port}`);
50
35
  log.start("Waiting for agent...");
51
36
  await waitForAgent(guestIp, port);
@@ -66,7 +51,14 @@ const downloadCommand = defineCommand({
66
51
  process.exitCode = 1;
67
52
  return;
68
53
  }
69
- const localPath = args.dest ? resolve(args.dest) : resolve(basename(remotePath));
54
+ let localPath;
55
+ if (args.dest) {
56
+ const resolved = resolve(args.dest);
57
+ if (args.dest.endsWith("/") || existsSync(resolved) && statSync(resolved).isDirectory()) {
58
+ mkdirSync(resolved, { recursive: true });
59
+ localPath = join(resolved, basename(remotePath));
60
+ } else localPath = resolved;
61
+ } else localPath = resolve(basename(remotePath));
70
62
  writeFileSync(localPath, data);
71
63
  log.success(`Downloaded to ${localPath} (${data.length} bytes)`);
72
64
  cmdLog.set({
@@ -122,10 +122,16 @@ const chrootNotFoundError = (vmId) => new VmError("ERR_VM_CHROOT_NOT_FOUND", {
122
122
  fix: "The VM must be recreated with 'vmsan create'."
123
123
  });
124
124
  const networkSlotsExhaustedError = () => new VmError("ERR_VM_NETWORK_SLOTS_EXHAUSTED", { message: "No available network slots (max 255 VMs)" });
125
- const vmNotRunningError = (vmId) => new VmError("ERR_VM_NOT_RUNNING", {
125
+ const vmNotRunningError = (vmId, currentStatus) => new VmError("ERR_VM_NOT_RUNNING", {
126
126
  vmId,
127
- message: `VM ${vmId} is not running`,
128
- fix: "The VM must be running to update its network policy. Start it with 'vmsan start <vm-id>'."
127
+ message: currentStatus ? `VM ${vmId} is not running (current status: ${currentStatus})` : `VM ${vmId} is not running`,
128
+ fix: "The VM must be running. Start it with 'vmsan start <vm-id>'."
129
+ });
130
+ const vmNoAgentTokenError = (vmId) => new VmError("ERR_VM_NO_AGENT_TOKEN", {
131
+ vmId,
132
+ message: `VM ${vmId} has no agent token`,
133
+ why: "The vmsan-agent binary was not found at ~/.vmsan/bin/vmsan-agent when this VM was created.",
134
+ fix: "Install the agent binary into ~/.vmsan/bin/vmsan-agent and recreate the VM with 'vmsan create'."
129
135
  });
130
136
  const snapshotNotFoundError = (snapshotId) => new VmError("ERR_VM_SNAPSHOT_NOT_FOUND", { message: `Snapshot not found: ${snapshotId}` });
131
137
  var FirecrackerApiError = class extends VmsanError {
@@ -197,7 +203,7 @@ var SetupError = class extends VmsanError {
197
203
  this.name = "SetupError";
198
204
  }
199
205
  };
200
- const INSTALL_FIX = `Run the install script to set up all dependencies:\n\ncurl -fsSL https://raw.githubusercontent.com/angelorc/vmsan/main/install.sh | bash`;
206
+ const INSTALL_FIX = `Run the install script to set up all dependencies:\n\ncurl -fsSL https://vmsan.dev/install | bash`;
201
207
  const missingBinaryError = (binary, path) => new SetupError("ERR_SETUP_MISSING_BINARY", {
202
208
  message: `${binary} not found at ${path}`,
203
209
  fix: INSTALL_FIX
@@ -229,4 +235,4 @@ function handleCommandError(error, cmdLog) {
229
235
  if (error.link) consola.log(` More: ${error.link}`);
230
236
  } else consola.error(error instanceof Error ? error.message : String(error));
231
237
  }
232
- export { invalidDomainError as A, policyConflictError as B, vmStateNotFoundError as C, invalidCidrPrefixError as D, invalidCidrOctetError as E, invalidIntegerFlagError as F, VmsanError as H, invalidNetworkPolicyError as I, invalidPortError as L, invalidDurationError as M, invalidImageRefEmptyError as N, invalidDiskSizeFormatError as O, invalidImageRefTagError as P, invalidRuntimeError as R, vmNotStoppedError as S, invalidCidrFormatError as T, portConflictError as V, chrootNotFoundError as _, noKernelDirError as a, vmNotFoundError as b, TimeoutError as c, socketTimeoutError as d, NetworkError as f, VmError as g, firecrackerApiError as h, noExt4RootfsError as i, invalidDomainPatternError as j, invalidDiskSizeRangeError as k, agentTimeoutError as l, FirecrackerApiError as m, SetupError as n, noKernelError as o, defaultInterfaceNotFoundError as p, missingBinaryError as r, noRootfsDirError as s, handleCommandError as t, lockTimeoutError as u, networkSlotsExhaustedError as v, ValidationError as w, vmNotRunningError as x, snapshotNotFoundError as y, mutuallyExclusiveFlagsError as z };
238
+ export { invalidDiskSizeRangeError as A, mutuallyExclusiveFlagsError as B, vmNotStoppedError as C, invalidCidrOctetError as D, invalidCidrFormatError as E, invalidImageRefTagError as F, portConflictError as H, invalidIntegerFlagError as I, invalidNetworkPolicyError as L, invalidDomainPatternError as M, invalidDurationError as N, invalidCidrPrefixError as O, invalidImageRefEmptyError as P, invalidPortError as R, vmNotRunningError as S, ValidationError as T, VmsanError as U, policyConflictError as V, chrootNotFoundError as _, noKernelDirError as a, vmNoAgentTokenError as b, TimeoutError as c, socketTimeoutError as d, NetworkError as f, VmError as g, firecrackerApiError as h, noExt4RootfsError as i, invalidDomainError as j, invalidDiskSizeFormatError as k, agentTimeoutError as l, FirecrackerApiError as m, SetupError as n, noKernelError as o, defaultInterfaceNotFoundError as p, missingBinaryError as r, noRootfsDirError as s, handleCommandError as t, lockTimeoutError as u, networkSlotsExhaustedError as v, vmStateNotFoundError as w, vmNotFoundError as x, snapshotNotFoundError as y, invalidRuntimeError as z };
@@ -0,0 +1,190 @@
1
+ import { n as vmsanPaths } from "./paths.mjs";
2
+ import { t as handleCommandError } from "./errors.mjs";
3
+ import { t as createCommandLogger } from "./logger.mjs";
4
+ import "./vm-state.mjs";
5
+ import "./timeout-killer.mjs";
6
+ import { t as AgentClient } from "./agent.mjs";
7
+ import { t as TimeoutExtender } from "./timeout-extender.mjs";
8
+ import { n as waitForAgent, t as resolveVmState } from "./vm-context.mjs";
9
+ import { t as ShellSession } from "./shell.mjs";
10
+ import { consola } from "consola";
11
+ import { defineCommand } from "citty";
12
+ import { isatty } from "node:tty";
13
+ function shellEscape(s) {
14
+ if (/^[a-zA-Z0-9._\-/=:@]+$/.test(s)) return s;
15
+ return "'" + s.replace(/'/g, "'\\''") + "'";
16
+ }
17
+ function parseEnvFlags(targetCommand) {
18
+ const env = {};
19
+ const argv = process.argv;
20
+ let positionalCount = 0;
21
+ for (let i = 2; i < argv.length; i++) {
22
+ const arg = argv[i];
23
+ if (arg === "--") break;
24
+ if (!arg.startsWith("-")) {
25
+ positionalCount++;
26
+ if (positionalCount >= 3) break;
27
+ continue;
28
+ }
29
+ let value;
30
+ if (arg === "--env" || arg === "-e") {
31
+ value = argv[i + 1];
32
+ if (value !== void 0) i++;
33
+ } else if (arg.startsWith("--env=")) value = arg.slice(6);
34
+ else if (arg.startsWith("-e=")) value = arg.slice(3);
35
+ if (value !== void 0) {
36
+ const eqIdx = value.indexOf("=");
37
+ if (eqIdx > 0) env[value.slice(0, eqIdx)] = value.slice(eqIdx + 1);
38
+ }
39
+ }
40
+ return env;
41
+ }
42
+ function buildVmsanPrompt(vmId) {
43
+ return `\\[\\033[1;32m\\]vmsan:${vmId.slice(0, 8)}\\[\\033[0m\\]:\\[\\033[1;34m\\]\\w\\[\\033[0m\\]\\$ `;
44
+ }
45
+ const execCommand = defineCommand({
46
+ meta: {
47
+ name: "exec",
48
+ description: "Execute a command inside a running VM"
49
+ },
50
+ args: {
51
+ vmId: {
52
+ type: "positional",
53
+ description: "VM ID, followed by command and arguments",
54
+ required: true
55
+ },
56
+ sudo: {
57
+ type: "boolean",
58
+ default: false,
59
+ description: "Run with extended privileges (sudo)"
60
+ },
61
+ interactive: {
62
+ type: "boolean",
63
+ alias: "i",
64
+ default: false,
65
+ description: "Interactive shell mode (PTY)"
66
+ },
67
+ "no-extend-timeout": {
68
+ type: "boolean",
69
+ default: false,
70
+ description: "Skip timeout extension (interactive only)"
71
+ },
72
+ tty: {
73
+ type: "boolean",
74
+ alias: "t",
75
+ default: false,
76
+ description: "Allocate a pseudo-TTY (accepted for compatibility)"
77
+ },
78
+ workdir: {
79
+ type: "string",
80
+ alias: "w",
81
+ description: "Working directory inside the VM"
82
+ },
83
+ env: {
84
+ type: "string",
85
+ alias: "e",
86
+ description: "Environment variable (KEY=VAL), repeatable"
87
+ }
88
+ },
89
+ async run({ args }) {
90
+ const cmdLog = createCommandLogger("exec");
91
+ const paths = vmsanPaths();
92
+ try {
93
+ const command = args._[1];
94
+ const commandArgs = args._.slice(2);
95
+ if (!command) {
96
+ consola.error("No command provided. Usage: vmsan exec <vm_id> <command> [...args]");
97
+ cmdLog.emit();
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+ if (args.interactive && !isatty(1)) {
102
+ consola.error("--interactive requires a terminal (TTY).");
103
+ process.exitCode = 1;
104
+ return;
105
+ }
106
+ const envVars = parseEnvFlags(command);
107
+ const { state, guestIp, port, store } = resolveVmState(args.vmId, paths);
108
+ consola.debug(`Agent endpoint: ${guestIp}:${port}`);
109
+ consola.debug(`Waiting for agent on ${guestIp}:${port}...`);
110
+ await waitForAgent(guestIp, port);
111
+ if (args.interactive) {
112
+ const parts = [];
113
+ parts.push(`export PS1=${shellEscape(buildVmsanPrompt(args.vmId))} TERM=xterm-256color &&`);
114
+ if (args.workdir) parts.push(`cd ${shellEscape(args.workdir)} &&`);
115
+ for (const [key, val] of Object.entries(envVars)) parts.push(`${key}=${shellEscape(val)}`);
116
+ parts.push(shellEscape(command));
117
+ for (const a of commandArgs) parts.push(shellEscape(a));
118
+ const injectedCmd = "clear; " + parts.join(" ") + "; exit $?\n";
119
+ let extender = null;
120
+ if (!args["no-extend-timeout"] && state.timeoutMs) {
121
+ extender = new TimeoutExtender({
122
+ vmId: args.vmId,
123
+ store,
124
+ paths
125
+ });
126
+ extender.start();
127
+ }
128
+ try {
129
+ await new ShellSession({
130
+ host: guestIp,
131
+ port,
132
+ token: state.agentToken,
133
+ initialCommand: injectedCmd,
134
+ user: args.sudo ? "root" : void 0
135
+ }).connect();
136
+ } finally {
137
+ extender?.stop();
138
+ }
139
+ cmdLog.set({
140
+ vmId: args.vmId,
141
+ mode: "interactive",
142
+ command,
143
+ args: commandArgs,
144
+ ...args["no-extend-timeout"] && { noExtendTimeout: true }
145
+ });
146
+ cmdLog.emit();
147
+ } else {
148
+ consola.debug(`exec: ${command} ${commandArgs.join(" ")}`);
149
+ const agent = new AgentClient(`http://${guestIp}:${port}`, state.agentToken);
150
+ const params = {
151
+ cmd: command,
152
+ args: commandArgs.length > 0 ? commandArgs : void 0,
153
+ cwd: args.workdir || void 0,
154
+ env: Object.keys(envVars).length > 0 ? envVars : void 0,
155
+ user: args.sudo ? "root" : void 0
156
+ };
157
+ const ac = new AbortController();
158
+ const onSignal = () => ac.abort();
159
+ process.on("SIGINT", onSignal);
160
+ process.on("SIGTERM", onSignal);
161
+ try {
162
+ const result = await (await agent.exec(params, {
163
+ signal: ac.signal,
164
+ onStdout: (line) => process.stdout.write(line + "\n"),
165
+ onStderr: (line) => process.stderr.write(line + "\n")
166
+ })).wait();
167
+ process.exitCode = result.exitCode;
168
+ if (result.timedOut) consola.error("Command timed out.");
169
+ } catch (err) {
170
+ if (!ac.signal.aborted) throw err;
171
+ process.exitCode = 130;
172
+ } finally {
173
+ process.removeListener("SIGINT", onSignal);
174
+ process.removeListener("SIGTERM", onSignal);
175
+ }
176
+ cmdLog.set({
177
+ vmId: args.vmId,
178
+ mode: "non-interactive",
179
+ command,
180
+ args: commandArgs
181
+ });
182
+ cmdLog.emit();
183
+ }
184
+ } catch (error) {
185
+ handleCommandError(error, cmdLog);
186
+ process.exitCode = 1;
187
+ }
188
+ }
189
+ });
190
+ export { execCommand as default };