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,9 +1,10 @@
1
- import { n as vmsanPaths } from "./paths.mjs";
2
- import "./errors.mjs";
1
+ import "./paths.mjs";
2
+ import { t as handleCommandError } from "./errors.mjs";
3
3
  import { r as getOutputMode, t as createCommandLogger } from "./logger.mjs";
4
- import { c as timeAgo, l as timeRemaining, s as table } from "./vm-state.mjs";
5
- import "./environment.mjs";
6
- import { t as VMService } from "./vm.mjs";
4
+ import { d as timeRemaining, l as table, u as timeAgo } from "./vm-state.mjs";
5
+ import { t as createVmsan } from "./context.mjs";
6
+ import "./firecracker.mjs";
7
+ import "./timeout-killer.mjs";
7
8
  import { consola } from "consola";
8
9
  import { defineCommand } from "citty";
9
10
  const STATUS_COLORS = {
@@ -21,63 +22,68 @@ const listCommand = defineCommand({
21
22
  name: "list",
22
23
  description: "List all VMs"
23
24
  },
24
- run() {
25
+ async run() {
25
26
  const cmdLog = createCommandLogger("list");
26
27
  const log = consola.withTag("list");
27
- const vms = new VMService(vmsanPaths()).list();
28
- if (vms.length === 0) {
28
+ try {
29
+ const vms = (await createVmsan()).list();
30
+ if (vms.length === 0) {
31
+ if (getOutputMode() === "json") cmdLog.set({
32
+ count: 0,
33
+ vms: []
34
+ });
35
+ else {
36
+ log.log("No VMs found.");
37
+ cmdLog.set({ count: 0 });
38
+ }
39
+ cmdLog.emit();
40
+ return;
41
+ }
42
+ const statuses = {};
43
+ for (const vm of vms) statuses[vm.status] = (statuses[vm.status] ?? 0) + 1;
44
+ consola.debug(`Found ${vms.length} VM(s): ${Object.entries(statuses).map(([s, n]) => `${n} ${s}`).join(", ")}`);
29
45
  if (getOutputMode() === "json") cmdLog.set({
30
- count: 0,
31
- vms: []
46
+ count: vms.length,
47
+ statuses,
48
+ vms: vms.map((vm) => ({
49
+ id: vm.id,
50
+ status: vm.status,
51
+ createdAt: vm.createdAt,
52
+ memSizeMib: vm.memSizeMib,
53
+ vcpuCount: vm.vcpuCount,
54
+ runtime: vm.runtime,
55
+ timeoutAt: vm.timeoutAt ?? null,
56
+ snapshot: vm.snapshot ?? null
57
+ }))
32
58
  });
33
59
  else {
34
- log.log("No VMs found.");
35
- cmdLog.set({ count: 0 });
60
+ const output = table({
61
+ rows: vms,
62
+ columns: {
63
+ ID: { value: (vm) => vm.id },
64
+ STATUS: {
65
+ value: (vm) => vm.status,
66
+ color: (vm) => colorStatus(vm.status)
67
+ },
68
+ CREATED: { value: (vm) => timeAgo(vm.createdAt) },
69
+ MEMORY: { value: (vm) => `${vm.memSizeMib} MiB` },
70
+ VCPUS: { value: (vm) => vm.vcpuCount },
71
+ RUNTIME: { value: (vm) => vm.runtime },
72
+ TIMEOUT: { value: (vm) => vm.timeoutAt ? timeRemaining(vm.timeoutAt) : "-" },
73
+ SNAPSHOT: { value: (vm) => vm.snapshot ?? "-" }
74
+ }
75
+ });
76
+ log.log(output);
77
+ cmdLog.set({
78
+ count: vms.length,
79
+ statuses
80
+ });
36
81
  }
37
82
  cmdLog.emit();
38
- return;
39
- }
40
- const statuses = {};
41
- for (const vm of vms) statuses[vm.status] = (statuses[vm.status] ?? 0) + 1;
42
- consola.debug(`Found ${vms.length} VM(s): ${Object.entries(statuses).map(([s, n]) => `${n} ${s}`).join(", ")}`);
43
- if (getOutputMode() === "json") cmdLog.set({
44
- count: vms.length,
45
- statuses,
46
- vms: vms.map((vm) => ({
47
- id: vm.id,
48
- status: vm.status,
49
- createdAt: vm.createdAt,
50
- memSizeMib: vm.memSizeMib,
51
- vcpuCount: vm.vcpuCount,
52
- runtime: vm.runtime,
53
- timeoutAt: vm.timeoutAt ?? null,
54
- snapshot: vm.snapshot ?? null
55
- }))
56
- });
57
- else {
58
- const output = table({
59
- rows: vms,
60
- columns: {
61
- ID: { value: (vm) => vm.id },
62
- STATUS: {
63
- value: (vm) => vm.status,
64
- color: (vm) => colorStatus(vm.status)
65
- },
66
- CREATED: { value: (vm) => timeAgo(vm.createdAt) },
67
- MEMORY: { value: (vm) => `${vm.memSizeMib} MiB` },
68
- VCPUS: { value: (vm) => vm.vcpuCount },
69
- RUNTIME: { value: (vm) => vm.runtime },
70
- TIMEOUT: { value: (vm) => vm.timeoutAt ? timeRemaining(vm.timeoutAt) : "-" },
71
- SNAPSHOT: { value: (vm) => vm.snapshot ?? "-" }
72
- }
73
- });
74
- log.log(output);
75
- cmdLog.set({
76
- count: vms.length,
77
- statuses
78
- });
83
+ } catch (error) {
84
+ handleCommandError(error, cmdLog);
85
+ process.exitCode = 1;
79
86
  }
80
- cmdLog.emit();
81
87
  }
82
88
  });
83
89
  export { listCommand as default };
@@ -1,9 +1,10 @@
1
- import { n as vmsanPaths } from "./paths.mjs";
2
- import { B as policyConflictError, t as handleCommandError } from "./errors.mjs";
1
+ import "./paths.mjs";
2
+ import { V as policyConflictError, t as handleCommandError } from "./errors.mjs";
3
3
  import { r as getOutputMode, t as createCommandLogger } from "./logger.mjs";
4
4
  import "./vm-state.mjs";
5
- import "./environment.mjs";
6
- import { t as VMService } from "./vm.mjs";
5
+ import { t as createVmsan } from "./context.mjs";
6
+ import "./firecracker.mjs";
7
+ import "./timeout-killer.mjs";
7
8
  import { d as validateCidr, i as parseDomains, n as parseCidrList, s as parseNetworkPolicy } from "./validation.mjs";
8
9
  import { consola } from "consola";
9
10
  import { defineCommand } from "citty";
@@ -46,7 +47,7 @@ const networkCommand = defineCommand({
46
47
  for (const cidr of allowedCidrs) validateCidr(cidr);
47
48
  for (const cidr of deniedCidrs) validateCidr(cidr);
48
49
  if (policy === "deny-all" && (domains.length || allowedCidrs.length || deniedCidrs.length)) throw policyConflictError();
49
- const result = await new VMService(vmsanPaths()).updateNetworkPolicy(args.vmId, policy, domains, allowedCidrs, deniedCidrs);
50
+ const result = await (await createVmsan()).updateNetworkPolicy(args.vmId, policy, domains, allowedCidrs, deniedCidrs);
50
51
  if (!result.success) {
51
52
  if (result.error) throw result.error;
52
53
  consola.error(`Failed to update network policy for ${args.vmId}`);
@@ -1,9 +1,10 @@
1
- import { n as vmsanPaths } from "./paths.mjs";
1
+ import "./paths.mjs";
2
2
  import "./errors.mjs";
3
3
  import { t as createCommandLogger } from "./logger.mjs";
4
4
  import "./vm-state.mjs";
5
- import "./environment.mjs";
6
- import { t as VMService } from "./vm.mjs";
5
+ import { t as createVmsan } from "./context.mjs";
6
+ import "./firecracker.mjs";
7
+ import "./timeout-killer.mjs";
7
8
  import { consola } from "consola";
8
9
  import { defineCommand } from "citty";
9
10
  const removeCommand = defineCommand({
@@ -34,9 +35,9 @@ const removeCommand = defineCommand({
34
35
  process.exitCode = 1;
35
36
  return;
36
37
  }
37
- const service = new VMService(vmsanPaths());
38
+ const vmsan = await createVmsan();
38
39
  const missing = [];
39
- for (const id of vmIds) if (!service.get(id)) missing.push(id);
40
+ for (const id of vmIds) if (!vmsan.get(id)) missing.push(id);
40
41
  if (missing.length > 0) {
41
42
  consola.error(`VM(s) not found: ${missing.join(", ")}`);
42
43
  cmdLog.emit();
@@ -45,7 +46,7 @@ const removeCommand = defineCommand({
45
46
  }
46
47
  if (!args.force) {
47
48
  const running = [];
48
- for (const id of vmIds) if (service.get(id).status !== "stopped") running.push(id);
49
+ for (const id of vmIds) if (vmsan.get(id).status !== "stopped") running.push(id);
49
50
  if (running.length > 0) {
50
51
  consola.error(`Cannot remove running VM(s): ${running.join(", ")}. Stop them first or use --force (-f).`);
51
52
  cmdLog.emit();
@@ -57,10 +58,10 @@ const removeCommand = defineCommand({
57
58
  let hasErrors = false;
58
59
  for (const id of vmIds) {
59
60
  const log = consola.withTag(id);
60
- const vm = service.get(id);
61
+ const vm = vmsan.get(id);
61
62
  consola.debug(`VM ${id} status=${vm.status}, force=${args.force}`);
62
63
  log.start(`Removing ${id}...`);
63
- const result = await service.remove(id, { force: args.force });
64
+ const result = await vmsan.remove(id, { force: args.force });
64
65
  if (result.success) {
65
66
  log.success(`Removed ${id}`);
66
67
  results.push({
@@ -100,6 +100,7 @@ var ShellSession = class {
100
100
  process.stdin.resume();
101
101
  this.ws.send(serializeReady());
102
102
  if (process.stdout.columns && process.stdout.rows) this.ws.send(serializeResize(process.stdout.columns, process.stdout.rows));
103
+ if (this.opts.initialCommand) this.ws.send(serializeData(Buffer.from(this.opts.initialCommand)));
103
104
  process.stdin.on("data", onStdinData);
104
105
  process.stdout.on("resize", onResize);
105
106
  });
@@ -136,6 +137,7 @@ var ShellSession = class {
136
137
  const url = new URL(`${proto}://${this.opts.host}:${this.opts.port}/ws/shell`);
137
138
  url.searchParams.set("token", this.opts.token);
138
139
  if (this.opts.shell) url.searchParams.set("shell", this.opts.shell);
140
+ if (this.opts.user) url.searchParams.set("user", this.opts.user);
139
141
  return url.toString();
140
142
  }
141
143
  };
@@ -1,13 +1,10 @@
1
- import { n as vmsanPaths } from "./paths.mjs";
2
- import { S as vmNotStoppedError, _ as chrootNotFoundError, b as vmNotFoundError, t as handleCommandError } from "./errors.mjs";
3
- import { n as createScopedLogger, t as createCommandLogger } from "./logger.mjs";
4
- import { t as FirecrackerClient } from "./firecracker.mjs";
5
- import { t as FileVmStateStore } from "./vm-state.mjs";
6
- import { a as getVmPid, c as NetworkManager, i as getVmJailerPid, o as validateEnvironment, s as waitForSocket } from "./environment.mjs";
7
- import { a as Jailer, i as markVmAsError, n as cleanupNetwork, r as killOrphanVmProcess } from "./cleanup.mjs";
8
- import { dirname, join } from "node:path";
9
- import { consola } from "consola";
10
- import { existsSync, rmSync, unlinkSync } from "node:fs";
1
+ import "./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 { t as createVmsan } from "./context.mjs";
6
+ import "./firecracker.mjs";
7
+ import "./timeout-killer.mjs";
11
8
  import { defineCommand } from "citty";
12
9
  const startCommand = defineCommand({
13
10
  meta: {
@@ -21,169 +18,23 @@ const startCommand = defineCommand({
21
18
  } },
22
19
  async run({ args }) {
23
20
  const cmdLog = createCommandLogger("start");
24
- const paths = vmsanPaths();
25
- const lifecycle = {
26
- vmId: void 0,
27
- networkConfig: void 0
28
- };
29
- const store = new FileVmStateStore(paths.vmsDir);
30
21
  try {
31
22
  const vmId = args.vmId;
32
- lifecycle.vmId = vmId;
33
- const log = createScopedLogger(vmId);
34
- const startTag = `[start:${vmId}]`;
35
- const state = store.load(vmId);
36
- if (!state) throw vmNotFoundError(vmId);
37
- if (state.status !== "stopped") throw vmNotStoppedError(vmId, state.status);
38
- if (!state.chrootDir || !existsSync(state.chrootDir)) throw chrootNotFoundError(vmId);
39
- const baseDir = paths.baseDir;
40
- validateEnvironment(baseDir);
41
- log.start(`Starting VM ${vmId}...`);
42
- const mgr = NetworkManager.fromVmNetwork(state.network);
43
- const networkConfig = mgr.config;
44
- lifecycle.networkConfig = networkConfig;
45
- consola.debug(`Reconstructed network config: slot=${networkConfig.slot}, tap=${networkConfig.tapDevice}, host=${networkConfig.hostIp}, guest=${networkConfig.guestIp}`);
46
- log.start("Setting up networking...");
47
- await mgr.setup();
48
- log.success(`Network: TAP ${networkConfig.tapDevice}, Host ${networkConfig.hostIp}, Guest ${networkConfig.guestIp}`);
49
- const vmRootCandidates = Array.from(new Set([
50
- join(state.chrootDir, "root"),
51
- state.chrootDir,
52
- dirname(dirname(state.apiSocket))
53
- ]));
54
- for (const rootDir of vmRootCandidates) {
55
- const staleFirecrackerBin = join(rootDir, "firecracker");
56
- if (existsSync(staleFirecrackerBin)) unlinkSync(staleFirecrackerBin);
57
- rmSync(join(rootDir, "firecracker.pid"), { force: true });
23
+ const result = await (await createVmsan()).start(vmId);
24
+ if (!result.success) {
25
+ if (result.error) throw result.error;
26
+ process.exitCode = 1;
27
+ cmdLog.emit();
28
+ return;
58
29
  }
59
- const socketPath = state.apiSocket;
60
- if (existsSync(socketPath)) unlinkSync(socketPath);
61
- const removeStaleDevTrees = () => {
62
- for (const rootDir of vmRootCandidates) {
63
- const devDir = join(rootDir, "dev");
64
- if (existsSync(devDir)) rmSync(devDir, {
65
- recursive: true,
66
- force: true
67
- });
68
- }
69
- };
70
- const removeStaleDeviceNodes = () => {
71
- const staleNodes = [
72
- "dev/net/tun",
73
- "dev/kvm",
74
- "dev/userfaultfd",
75
- "dev/urandom"
76
- ];
77
- for (const rootDir of vmRootCandidates) for (const rel of staleNodes) {
78
- const nodePath = join(rootDir, rel);
79
- if (existsSync(nodePath)) rmSync(nodePath, {
80
- recursive: true,
81
- force: true
82
- });
83
- }
84
- };
85
- const firecrackerBin = join(baseDir, "bin", "firecracker");
86
- const jailerBin = join(baseDir, "bin", "jailer");
87
- const jailer = new Jailer(vmId, paths.jailerBaseDir);
88
- let socketReady = false;
89
- const errorMessage = (error) => error instanceof Error ? error.message : String(error);
90
- const logAttemptError = (attempt, error) => {
91
- const message = errorMessage(error);
92
- consola.error(`${startTag} ${attempt} failed: ${message}`);
93
- };
94
- consola.debug(`Stale file cleanup: checked ${vmRootCandidates.length} root candidates`);
95
- const logDiagnostics = () => {
96
- const socketExists = existsSync(socketPath);
97
- const devState = vmRootCandidates.map((rootDir) => `${join(rootDir, "dev")}=${existsSync(join(rootDir, "dev"))}`).join(", ");
98
- const firecrackerPid = getVmPid(vmId);
99
- const jailerPid = getVmJailerPid(vmId);
100
- log.error(`${startTag} diagnostics: socketExists=${socketExists}; firecrackerPid=${firecrackerPid ?? "none"}; jailerPid=${jailerPid ?? "none"}; devDirs=[${devState}]`);
101
- };
102
- const isRecoverableStartError = (message) => {
103
- if (message.includes("Timeout waiting for API socket")) return true;
104
- if (message.includes("mknod inside the jail") && message.includes("File exists")) return true;
105
- if (message.includes("MknodDev(") && message.includes("os error 17")) return true;
106
- return false;
107
- };
108
- const cgroup = {
109
- cpuQuotaUs: state.vcpuCount * 1e5,
110
- cpuPeriodUs: 1e5,
111
- memoryBytes: state.memSizeMib * 1024 * 1024
112
- };
113
- const spawnAndWait = async (timeoutMs) => {
114
- log.start("Spawning Firecracker via jailer...");
115
- consola.debug(`Jailer spawn: firecracker=${firecrackerBin}, jailer=${jailerBin}, chrootBase=${jailer.paths.chrootBase}`);
116
- jailer.spawn({
117
- firecrackerBin,
118
- jailerBin,
119
- chrootBase: jailer.paths.chrootBase,
120
- newPidNs: true,
121
- cgroup,
122
- netns: state.network.netnsName
123
- });
124
- log.start("Waiting for API socket...");
125
- await waitForSocket(socketPath, timeoutMs);
126
- };
127
- try {
128
- removeStaleDeviceNodes();
129
- await spawnAndWait(1e4);
130
- socketReady = true;
131
- } catch (firstStartError) {
132
- const message = errorMessage(firstStartError);
133
- if (!isRecoverableStartError(message)) {
134
- logAttemptError("initial attempt", firstStartError);
135
- logDiagnostics();
136
- throw firstStartError;
137
- }
138
- logAttemptError("initial attempt", firstStartError);
139
- killOrphanVmProcess(vmId);
140
- if (existsSync(socketPath)) unlinkSync(socketPath);
141
- removeStaleDeviceNodes();
142
- removeStaleDevTrees();
143
- for (const rootDir of vmRootCandidates) {
144
- const staleFirecrackerBin = join(rootDir, "firecracker");
145
- if (existsSync(staleFirecrackerBin)) unlinkSync(staleFirecrackerBin);
146
- rmSync(join(rootDir, "firecracker.pid"), { force: true });
147
- }
148
- try {
149
- await spawnAndWait(15e3);
150
- socketReady = true;
151
- } catch (retryError) {
152
- logAttemptError("retry attempt", retryError);
153
- logDiagnostics();
154
- throw new Error(`${startTag} retry failed after cleanup. First error: ${message}. Retry error: ${errorMessage(retryError)}`);
155
- }
156
- }
157
- if (!socketReady) throw new Error(`Timeout waiting for API socket at ${socketPath}`);
158
- log.success("API socket ready");
159
- const vm = new FirecrackerClient(socketPath);
160
- const bootArgs = NetworkManager.bootArgs(networkConfig.slot);
161
- consola.debug(`Boot args: ${bootArgs}`);
162
- await vm.boot("kernel/vmlinux", bootArgs);
163
- await vm.addDrive("rootfs", "rootfs/rootfs.ext4", true, false);
164
- await vm.configure(state.vcpuCount, state.memSizeMib);
165
- await vm.addNetwork("eth0", networkConfig.tapDevice, networkConfig.macAddress);
166
- log.start("Starting VM...");
167
- await vm.start();
168
- const pid = getVmPid(vmId);
169
- store.update(vmId, {
170
- status: "running",
171
- pid
172
- });
173
- log.success(`VM ${vmId} is running (PID: ${pid || "unknown"})`);
174
30
  cmdLog.set({
175
31
  vmId,
176
- pid,
177
- guestIp: networkConfig.guestIp,
178
- networking: networkConfig.networkPolicy
32
+ pid: result.pid,
33
+ guestIp: result.state?.network.guestIp,
34
+ networking: result.state?.network.networkPolicy
179
35
  });
180
36
  cmdLog.emit();
181
37
  } catch (error) {
182
- if (lifecycle.vmId) {
183
- killOrphanVmProcess(lifecycle.vmId);
184
- markVmAsError(lifecycle.vmId, error, paths);
185
- }
186
- cleanupNetwork(lifecycle.networkConfig);
187
38
  handleCommandError(error, cmdLog);
188
39
  process.exitCode = 1;
189
40
  }
@@ -1,9 +1,10 @@
1
- import { n as vmsanPaths } from "./paths.mjs";
1
+ import "./paths.mjs";
2
2
  import "./errors.mjs";
3
3
  import { t as createCommandLogger } from "./logger.mjs";
4
4
  import "./vm-state.mjs";
5
- import "./environment.mjs";
6
- import { t as VMService } from "./vm.mjs";
5
+ import { t as createVmsan } from "./context.mjs";
6
+ import "./firecracker.mjs";
7
+ import "./timeout-killer.mjs";
7
8
  import { consola } from "consola";
8
9
  import { defineCommand } from "citty";
9
10
  const stopCommand = defineCommand({
@@ -26,9 +27,9 @@ const stopCommand = defineCommand({
26
27
  process.exitCode = 1;
27
28
  return;
28
29
  }
29
- const service = new VMService(vmsanPaths());
30
+ const vmsan = await createVmsan();
30
31
  const missing = [];
31
- for (const id of vmIds) if (!service.get(id)) missing.push(id);
32
+ for (const id of vmIds) if (!vmsan.get(id)) missing.push(id);
32
33
  if (missing.length > 0) {
33
34
  consola.error(`VM(s) not found: ${missing.join(", ")}`);
34
35
  cmdLog.emit();
@@ -39,10 +40,10 @@ const stopCommand = defineCommand({
39
40
  let hasErrors = false;
40
41
  for (const id of vmIds) {
41
42
  const log = consola.withTag(id);
42
- const vm = service.get(id);
43
+ const vm = vmsan.get(id);
43
44
  consola.debug(`VM ${id} current status: ${vm?.status ?? "unknown"}`);
44
45
  log.start(`Stopping ${id}...`);
45
- const result = await service.stop(id);
46
+ const result = await vmsan.stop(id);
46
47
  if (result.alreadyStopped) {
47
48
  log.warn(`${id} is already stopped`);
48
49
  results.push({
@@ -0,0 +1,69 @@
1
+ import { V as policyConflictError } from "./errors.mjs";
2
+ import { s as parseDuration } from "./vm-state.mjs";
3
+ import { d as assertSnapshotExists } from "./context.mjs";
4
+ import { c as parsePublishedPorts, d as validateCidr, f as validatePublishedPortsAvailable, i as parseDomains, l as parseRuntime, n as parseCidrList, o as parseMemoryMib, r as parseDiskSizeGb, s as parseNetworkPolicy, u as parseVcpuCount } from "./validation.mjs";
5
+ function parseCreateInput(args, paths) {
6
+ const vcpus = parseVcpuCount(args.vcpus);
7
+ const memMib = parseMemoryMib(args.memory);
8
+ const runtime = parseRuntime(args.runtime);
9
+ const networkPolicy = parseNetworkPolicy(args["network-policy"]);
10
+ const ports = parsePublishedPorts(args["publish-port"]);
11
+ const domains = parseDomains(args["allowed-domain"]);
12
+ const allowedCidrs = parseCidrList(args["allowed-cidr"]);
13
+ const deniedCidrs = parseCidrList(args["denied-cidr"]);
14
+ const timeoutMs = typeof args.timeout === "string" ? parseDuration(args.timeout) : null;
15
+ const snapshotId = typeof args.snapshot === "string" ? args.snapshot : null;
16
+ const diskSizeGb = parseDiskSizeGb(args.disk);
17
+ validatePublishedPortsAvailable(ports, paths);
18
+ for (const cidr of allowedCidrs) validateCidr(cidr);
19
+ for (const cidr of deniedCidrs) validateCidr(cidr);
20
+ if (networkPolicy === "deny-all" && (domains.length || allowedCidrs.length || deniedCidrs.length)) throw policyConflictError();
21
+ if (snapshotId) assertSnapshotExists(snapshotId, paths);
22
+ return {
23
+ vcpus,
24
+ memMib,
25
+ runtime,
26
+ networkPolicy: networkPolicy === "allow-all" && (domains.length > 0 || allowedCidrs.length > 0 || deniedCidrs.length > 0) ? "custom" : networkPolicy,
27
+ ports,
28
+ domains,
29
+ allowedCidrs,
30
+ deniedCidrs,
31
+ timeoutMs,
32
+ snapshotId,
33
+ diskSizeGb
34
+ };
35
+ }
36
+ function buildCreateSummaryLines(input) {
37
+ return [
38
+ `VM Created: ${input.vmId}`,
39
+ "",
40
+ " Status: running",
41
+ ` PID: ${input.pid || "unknown"}`,
42
+ ` vCPUs: ${input.vcpus}`,
43
+ ` Memory: ${input.memMib} MiB`,
44
+ ` Runtime: ${input.runtime}`,
45
+ ` Disk: ${input.diskSizeGb} GB`,
46
+ ...input.project ? [` Project: ${input.project}`] : [],
47
+ "",
48
+ " Network:",
49
+ ` TAP: ${input.tapDevice}`,
50
+ ` Host: ${input.hostIp}`,
51
+ ` Guest: ${input.guestIp}`,
52
+ ` MAC: ${input.macAddress}`,
53
+ ` Policy: ${input.networkPolicy}`,
54
+ ...input.domains.length > 0 ? [` Domains: ${input.domains.join(", ")}`] : [],
55
+ ...input.allowedCidrs.length > 0 ? [` Allowed CIDRs: ${input.allowedCidrs.join(", ")}`] : [],
56
+ ...input.deniedCidrs.length > 0 ? [` Denied CIDRs: ${input.deniedCidrs.join(", ")}`] : [],
57
+ ...input.ports.length > 0 ? [` Ports: ${input.ports.join(", ")}`] : [],
58
+ "",
59
+ ` Kernel: ${input.kernelPath}`,
60
+ ` Rootfs: ${input.rootfsPath}`,
61
+ ...input.snapshotId ? [` Snapshot: ${input.snapshotId}`] : [],
62
+ ...input.timeout ? [` Timeout: ${input.timeout}`] : [],
63
+ "",
64
+ ` Socket: ${input.socketPath}`,
65
+ ` Chroot: ${input.chrootDir}`,
66
+ ` State: ${input.stateFilePath}`
67
+ ];
68
+ }
69
+ export { parseCreateInput as n, buildCreateSummaryLines as t };
@@ -0,0 +1,66 @@
1
+ import { c as safeKill } from "./vm-state.mjs";
2
+ import { t as spawnTimeoutKiller } from "./timeout-killer.mjs";
3
+ import { join } from "node:path";
4
+ const DEFAULT_INTERVAL_MS = 300 * 1e3;
5
+ var TimeoutExtender = class {
6
+ _timer = null;
7
+ _previousKillerPid = null;
8
+ _vmId;
9
+ _store;
10
+ _paths;
11
+ _intervalMs;
12
+ _signal;
13
+ constructor(opts) {
14
+ this._vmId = opts.vmId;
15
+ this._store = opts.store;
16
+ this._paths = opts.paths;
17
+ this._intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
18
+ this._signal = opts.signal;
19
+ }
20
+ start() {
21
+ if (this._timer) return;
22
+ if (this._signal) {
23
+ this._signal.addEventListener("abort", () => this.stop(), { once: true });
24
+ if (this._signal.aborted) {
25
+ this.stop();
26
+ return;
27
+ }
28
+ }
29
+ this._extendSafe();
30
+ this._timer = setInterval(() => this._extendSafe(), this._intervalMs);
31
+ }
32
+ stop() {
33
+ if (this._timer) {
34
+ clearInterval(this._timer);
35
+ this._timer = null;
36
+ }
37
+ if (this._previousKillerPid !== null) {
38
+ safeKill(this._previousKillerPid);
39
+ this._previousKillerPid = null;
40
+ }
41
+ }
42
+ _extendSafe() {
43
+ try {
44
+ this._extend();
45
+ } catch {
46
+ this.stop();
47
+ }
48
+ }
49
+ _extend() {
50
+ const state = this._store.load(this._vmId);
51
+ if (!state || state.status !== "running" || !state.timeoutMs) return;
52
+ const timeoutAt = new Date(Date.now() + state.timeoutMs).toISOString();
53
+ this._store.update(this._vmId, { timeoutAt });
54
+ if (this._previousKillerPid !== null) {
55
+ safeKill(this._previousKillerPid);
56
+ this._previousKillerPid = null;
57
+ }
58
+ if (state.pid) this._previousKillerPid = spawnTimeoutKiller({
59
+ vmId: this._vmId,
60
+ pid: state.pid,
61
+ timeoutMs: state.timeoutMs,
62
+ stateFile: join(this._paths.vmsDir, `${this._vmId}.json`)
63
+ }).pid ?? null;
64
+ }
65
+ };
66
+ export { TimeoutExtender as t };
@@ -0,0 +1,33 @@
1
+ import { spawn } from "node:child_process";
2
+ /**
3
+ * Spawn a detached bash process that kills the VM after timeout.
4
+ * The process sleeps for the timeout duration, then verifies the VM
5
+ * is still running with the expected PID before sending SIGTERM.
6
+ */
7
+ function spawnTimeoutKiller(opts) {
8
+ const { vmId, pid, timeoutMs, stateFile } = opts;
9
+ const timeoutSec = String(Math.ceil(timeoutMs / 1e3));
10
+ const killer = spawn("bash", [
11
+ "-c",
12
+ [
13
+ "sleep \"$1\"",
14
+ "STATE=$(cat -- \"$2\" 2>/dev/null) || exit 0",
15
+ "echo \"$STATE\" | grep -q '\"status\":\"running\"' || exit 0",
16
+ "echo \"$STATE\" | grep -q '\"pid\":'\"$3\" || exit 0",
17
+ "[ -d \"/proc/$3\" ] || exit 0",
18
+ "grep -aq -- \"$4\" \"/proc/$3/cmdline\" 2>/dev/null || exit 0",
19
+ "kill -- \"$3\" 2>/dev/null"
20
+ ].join(" && "),
21
+ "bash",
22
+ timeoutSec,
23
+ stateFile,
24
+ String(pid),
25
+ vmId
26
+ ], {
27
+ detached: true,
28
+ stdio: "ignore"
29
+ });
30
+ killer.unref();
31
+ return killer;
32
+ }
33
+ export { spawnTimeoutKiller as t };
@@ -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";
6
+ import { n as waitForAgent, t as resolveVmState } from "./vm-context.mjs";
7
7
  import { basename } from "node:path";
8
- import { consola } from "consola";
9
8
  import { readFileSync } from "node:fs";
9
+ import { consola } from "consola";
10
10
  import { defineCommand } from "citty";
11
11
  const uploadCommand = defineCommand({
12
12
  meta: {
@@ -30,23 +30,8 @@ const uploadCommand = defineCommand({
30
30
  const cmdLog = createCommandLogger("upload");
31
31
  const paths = vmsanPaths();
32
32
  try {
33
- const state = new FileVmStateStore(paths.vmsDir).load(args.vmId);
34
- if (!state) throw vmNotFoundError(args.vmId);
35
- if (state.status !== "running") {
36
- consola.error(`VM ${args.vmId} is not running (status: ${state.status})`);
37
- cmdLog.emit();
38
- process.exitCode = 1;
39
- return;
40
- }
41
- if (!state.agentToken) {
42
- consola.error("VM has no agent token. Cannot upload files without the agent.");
43
- cmdLog.emit();
44
- process.exitCode = 1;
45
- return;
46
- }
33
+ const { state, guestIp, port } = resolveVmState(args.vmId, paths);
47
34
  const log = consola.withTag(args.vmId);
48
- const guestIp = state.network.guestIp;
49
- const port = state.agentPort || paths.agentPort;
50
35
  consola.debug(`Agent endpoint: ${guestIp}:${port}`);
51
36
  log.start("Waiting for agent...");
52
37
  await waitForAgent(guestIp, port);