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,6 +1,196 @@
1
1
  import { createGzip } from "node:zlib";
2
2
  import { Readable } from "node:stream";
3
3
  import { pack } from "tar-stream";
4
+ /**
5
+ * Lightweight async iterable queue for pushing events and yielding them to consumers.
6
+ */
7
+ var AsyncQueue = class {
8
+ _buffer = [];
9
+ _waiting = [];
10
+ _closed = false;
11
+ push(item) {
12
+ if (this._closed) return;
13
+ if (this._waiting.length > 0) this._waiting.shift()({
14
+ value: item,
15
+ done: false
16
+ });
17
+ else this._buffer.push(item);
18
+ }
19
+ close() {
20
+ this._closed = true;
21
+ for (const resolve of this._waiting) resolve({
22
+ value: void 0,
23
+ done: true
24
+ });
25
+ this._waiting.length = 0;
26
+ }
27
+ async *[Symbol.asyncIterator]() {
28
+ while (true) if (this._buffer.length > 0) yield this._buffer.shift();
29
+ else if (this._closed) return;
30
+ else {
31
+ const item = await new Promise((resolve) => {
32
+ this._waiting.push(resolve);
33
+ });
34
+ if (item.done) return;
35
+ yield item.value;
36
+ }
37
+ }
38
+ };
39
+ var CommandFinished = class {
40
+ cmdId;
41
+ exitCode;
42
+ stdout;
43
+ stderr;
44
+ output;
45
+ timedOut;
46
+ startedAt;
47
+ constructor(opts) {
48
+ this.cmdId = opts.cmdId;
49
+ this.exitCode = opts.exitCode;
50
+ this.stdout = opts.stdout;
51
+ this.stderr = opts.stderr;
52
+ this.output = opts.output;
53
+ this.timedOut = opts.timedOut;
54
+ this.startedAt = opts.startedAt;
55
+ }
56
+ get ok() {
57
+ return this.exitCode === 0;
58
+ }
59
+ };
60
+ const MAX_LOG_ENTRIES = 1e5;
61
+ var Command = class {
62
+ cmdId;
63
+ _startedAt;
64
+ _exitCode = null;
65
+ _timedOut = false;
66
+ _logEntries = [];
67
+ _logTruncated = false;
68
+ _eventQueue = new AsyncQueue();
69
+ _completion;
70
+ _stdoutPromise = null;
71
+ _stderrPromise = null;
72
+ _outputPromise = null;
73
+ _agent;
74
+ constructor(init) {
75
+ const { agent, cmdId, startedAt, stream, signal, onStdout, onStderr } = init;
76
+ this._agent = agent;
77
+ this.cmdId = cmdId;
78
+ this._startedAt = startedAt;
79
+ let resolveCompletion;
80
+ let rejectCompletion;
81
+ this._completion = new Promise((resolve, reject) => {
82
+ resolveCompletion = resolve;
83
+ rejectCompletion = reject;
84
+ });
85
+ let onAbort;
86
+ if (signal) {
87
+ onAbort = () => {
88
+ this.kill().catch(() => {});
89
+ };
90
+ if (signal.aborted) this.kill().catch(() => {});
91
+ else signal.addEventListener("abort", onAbort, { once: true });
92
+ }
93
+ (async () => {
94
+ try {
95
+ for await (const event of stream) {
96
+ this._eventQueue.push(event);
97
+ switch (event.type) {
98
+ case "stdout":
99
+ case "stderr":
100
+ if (event.data !== void 0) {
101
+ if (this._logEntries.length < MAX_LOG_ENTRIES) this._logEntries.push({
102
+ stream: event.type,
103
+ data: event.data
104
+ });
105
+ else this._logTruncated = true;
106
+ (event.type === "stdout" ? onStdout : onStderr)?.(event.data);
107
+ }
108
+ break;
109
+ case "exit":
110
+ this._exitCode = event.exitCode ?? 1;
111
+ break;
112
+ case "timeout":
113
+ this._timedOut = true;
114
+ this._exitCode = 124;
115
+ break;
116
+ case "error":
117
+ this._exitCode = 1;
118
+ break;
119
+ }
120
+ }
121
+ this._eventQueue.close();
122
+ resolveCompletion(new CommandFinished({
123
+ cmdId,
124
+ exitCode: this._exitCode ?? 1,
125
+ stdout: this._logEntries.filter((e) => e.stream === "stdout").map((e) => e.data).join("\n"),
126
+ stderr: this._logEntries.filter((e) => e.stream === "stderr").map((e) => e.data).join("\n"),
127
+ output: this._logEntries.map((e) => e.data).join("\n"),
128
+ timedOut: this._timedOut,
129
+ startedAt
130
+ }));
131
+ this._logEntries.length = 0;
132
+ } catch (err) {
133
+ this._eventQueue.close();
134
+ const error = err instanceof Error ? err : new Error(String(err));
135
+ rejectCompletion(error);
136
+ } finally {
137
+ if (signal && onAbort) signal.removeEventListener("abort", onAbort);
138
+ }
139
+ })();
140
+ }
141
+ get startedAt() {
142
+ return this._startedAt;
143
+ }
144
+ get exitCode() {
145
+ return this._exitCode;
146
+ }
147
+ async *logs(opts) {
148
+ const signal = opts?.signal;
149
+ if (signal?.aborted) return;
150
+ for await (const event of this._eventQueue) {
151
+ if (signal?.aborted) return;
152
+ if (event.type === "stdout" || event.type === "stderr") yield {
153
+ stream: event.type,
154
+ data: event.data ?? ""
155
+ };
156
+ }
157
+ }
158
+ stdout(opts) {
159
+ if (!this._stdoutPromise) this._stdoutPromise = this._completion.then((r) => r.stdout);
160
+ return this._withSignal(this._stdoutPromise, opts?.signal);
161
+ }
162
+ stderr(opts) {
163
+ if (!this._stderrPromise) this._stderrPromise = this._completion.then((r) => r.stderr);
164
+ return this._withSignal(this._stderrPromise, opts?.signal);
165
+ }
166
+ output(stream, opts) {
167
+ if (stream === "stdout") return this.stdout(opts);
168
+ if (stream === "stderr") return this.stderr(opts);
169
+ if (!this._outputPromise) this._outputPromise = this._completion.then((r) => r.output);
170
+ return this._withSignal(this._outputPromise, opts?.signal);
171
+ }
172
+ wait(opts) {
173
+ return this._withSignal(this._completion, opts?.signal);
174
+ }
175
+ async kill(signal, opts) {
176
+ await this._agent.killCommand(this.cmdId, signal, opts?.abortSignal);
177
+ }
178
+ _withSignal(promise, signal) {
179
+ if (!signal) return promise;
180
+ if (signal.aborted) return Promise.reject(new DOMException("The operation was aborted.", "AbortError"));
181
+ return new Promise((resolve, reject) => {
182
+ const onAbort = () => reject(new DOMException("The operation was aborted.", "AbortError"));
183
+ signal.addEventListener("abort", onAbort, { once: true });
184
+ promise.then((value) => {
185
+ signal.removeEventListener("abort", onAbort);
186
+ resolve(value);
187
+ }, (err) => {
188
+ signal.removeEventListener("abort", onAbort);
189
+ reject(err);
190
+ });
191
+ });
192
+ }
193
+ };
4
194
  var AgentClient = class {
5
195
  constructor(baseUrl, token) {
6
196
  this.baseUrl = baseUrl;
@@ -11,14 +201,15 @@ var AgentClient = class {
11
201
  if (!res.ok) throw new Error(`Agent health check failed: ${res.status}`);
12
202
  return res.json();
13
203
  }
14
- async *run(params) {
204
+ async *run(params, signal) {
15
205
  const res = await fetch(`${this.baseUrl}/exec`, {
16
206
  method: "POST",
17
207
  headers: {
18
208
  "Content-Type": "application/json",
19
209
  Authorization: `Bearer ${this.token}`
20
210
  },
21
- body: JSON.stringify(params)
211
+ body: JSON.stringify(params),
212
+ signal
22
213
  });
23
214
  if (!res.ok) {
24
215
  const text = await res.text();
@@ -41,7 +232,7 @@ var AgentClient = class {
41
232
  }
42
233
  if (buffer.trim()) yield JSON.parse(buffer.trim());
43
234
  }
44
- async killCommand(cmdId, signal) {
235
+ async killCommand(cmdId, signal, abortSignal) {
45
236
  const url = `${this.baseUrl}/exec/${cmdId}/kill`;
46
237
  const res = await fetch(url, {
47
238
  method: "POST",
@@ -49,7 +240,8 @@ var AgentClient = class {
49
240
  "Content-Type": "application/json",
50
241
  Authorization: `Bearer ${this.token}`
51
242
  },
52
- body: signal ? JSON.stringify({ signal }) : void 0
243
+ body: signal ? JSON.stringify({ signal }) : void 0,
244
+ signal: abortSignal
53
245
  });
54
246
  if (!res.ok) {
55
247
  const text = await res.text();
@@ -94,6 +286,41 @@ var AgentClient = class {
94
286
  throw new Error(`Agent killShellSession failed (${res.status}): ${text}`);
95
287
  }
96
288
  }
289
+ async exec(params, opts) {
290
+ const stream = this.run(params, opts?.signal);
291
+ const first = await stream.next();
292
+ if (first.done) throw new Error("Stream ended without 'started' event");
293
+ if (first.value.type !== "started") throw new Error(`Expected 'started' event, got '${first.value.type}'`);
294
+ const cmdId = first.value.id;
295
+ if (!cmdId) throw new Error("'started' event missing 'id' field");
296
+ const startedAt = first.value.ts ? new Date(first.value.ts) : /* @__PURE__ */ new Date();
297
+ return new Command({
298
+ agent: this,
299
+ cmdId,
300
+ startedAt,
301
+ stream,
302
+ signal: opts?.signal,
303
+ onStdout: opts?.onStdout,
304
+ onStderr: opts?.onStderr
305
+ });
306
+ }
307
+ async runCommand(cmdOrParams, args, opts) {
308
+ let params;
309
+ if (typeof cmdOrParams === "string") params = {
310
+ cmd: cmdOrParams,
311
+ args,
312
+ signal: opts?.signal
313
+ };
314
+ else params = cmdOrParams;
315
+ const { signal, onStdout, onStderr, ...runParams } = params;
316
+ const command = await this.exec(runParams, {
317
+ signal,
318
+ onStdout,
319
+ onStderr
320
+ });
321
+ if (params.detached) return command;
322
+ return command.wait({ signal });
323
+ }
97
324
  async readFile(path) {
98
325
  const res = await fetch(`${this.baseUrl}/files/read`, {
99
326
  method: "POST",
@@ -1,13 +1,55 @@
1
- import { l as agentTimeoutError } from "./errors.mjs";
2
- async function waitForAgent(guestIp, port, timeoutMs = 6e4) {
3
- const start = Date.now();
4
- const url = `http://${guestIp}:${port}/health`;
5
- while (Date.now() - start < timeoutMs) {
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 { n as waitForAgent, t as resolveVmState } from "./vm-context.mjs";
6
+ import { t as ShellSession } from "./shell.mjs";
7
+ import { consola } from "consola";
8
+ import { defineCommand } from "citty";
9
+ const connectCommand = defineCommand({
10
+ meta: {
11
+ name: "connect",
12
+ description: "Connect to a running VM"
13
+ },
14
+ args: {
15
+ vmId: {
16
+ type: "positional",
17
+ description: "VM ID to connect to",
18
+ required: true
19
+ },
20
+ session: {
21
+ type: "string",
22
+ alias: "s",
23
+ description: "Attach to an existing shell session ID",
24
+ required: false
25
+ }
26
+ },
27
+ async run({ args }) {
28
+ const cmdLog = createCommandLogger("connect");
29
+ const paths = vmsanPaths();
6
30
  try {
7
- if ((await fetch(url, { signal: AbortSignal.timeout(2e3) })).ok) return;
8
- } catch {}
9
- await new Promise((r) => setTimeout(r, 500));
31
+ const { state, guestIp, port } = resolveVmState(args.vmId, paths);
32
+ consola.debug(`Agent endpoint: ${guestIp}:${port}`);
33
+ consola.debug("Waiting for agent to become ready...");
34
+ await waitForAgent(guestIp, port);
35
+ const shell = new ShellSession({
36
+ host: guestIp,
37
+ port,
38
+ token: state.agentToken,
39
+ sessionId: args.session
40
+ });
41
+ const closeInfo = await shell.connect();
42
+ cmdLog.set({
43
+ vmId: args.vmId,
44
+ method: "pty"
45
+ });
46
+ cmdLog.emit();
47
+ if (!closeInfo.sessionDestroyed && shell.sessionId) process.stderr.write(`\nResume this session with:\n vmsan connect ${args.vmId} --session ${shell.sessionId}\n`);
48
+ process.exit(0);
49
+ } catch (error) {
50
+ handleCommandError(error, cmdLog);
51
+ process.exit(1);
52
+ }
10
53
  }
11
- throw agentTimeoutError(guestIp, timeoutMs);
12
- }
13
- export { waitForAgent as t };
54
+ });
55
+ export { connectCommand as default };