@computesdk/archil 0.4.27 → 0.4.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { defineProvider } from "@computesdk/provider";
2
+ import { defineProvider, InstanceSizeError } from "@computesdk/provider";
3
3
  import { Archil as ArchilClient, ArchilError } from "disk";
4
4
  import { randomUUID } from "crypto";
5
5
  import { posix } from "path";
@@ -21,6 +21,21 @@ function regionToBaseUrl(region) {
21
21
  const color = REGION_COLORS[region] ?? "green";
22
22
  return `https://control.${color}.${suffix}.${cloud}.prod.archil.com`;
23
23
  }
24
+ function vmInstance(client, vm, resolved) {
25
+ return {
26
+ client,
27
+ disk: { id: vm.id },
28
+ vm,
29
+ resolved,
30
+ createdAt: /* @__PURE__ */ new Date(),
31
+ pause: async () => {
32
+ await vm.pause();
33
+ },
34
+ resume: async () => {
35
+ await vm.resume();
36
+ }
37
+ };
38
+ }
24
39
  function resolveConfig(config) {
25
40
  const apiKey = config.apiKey ?? process.env.ARCHIL_API_KEY;
26
41
  const region = config.region ?? process.env.ARCHIL_REGION;
@@ -38,7 +53,16 @@ function resolveConfig(config) {
38
53
  }
39
54
  baseUrl = regionToBaseUrl(region);
40
55
  }
41
- return { apiKey, baseUrl, execution: config.execution ?? "exec" };
56
+ return { apiKey, baseUrl, execution: resolveExecution(config) };
57
+ }
58
+ function resolveExecution(config) {
59
+ if (config.execution) return config.execution;
60
+ const fromEnv = process.env.ARCHIL_EXECUTION?.trim().toLowerCase();
61
+ if (!fromEnv) return "persistent";
62
+ if (fromEnv === "exec" || fromEnv === "persistent") return fromEnv;
63
+ throw new Error(
64
+ `Invalid ARCHIL_EXECUTION "${process.env.ARCHIL_EXECUTION}". Expected "persistent" (default) or "exec".`
65
+ );
42
66
  }
43
67
  function createClient(config, resolved) {
44
68
  const region = config.region ?? process.env.ARCHIL_REGION ?? "aws-us-east-1";
@@ -53,10 +77,6 @@ function resolveCreateDiskId(options) {
53
77
  }
54
78
  return diskId;
55
79
  }
56
- async function forkSandboxVm(client, sourceId, name) {
57
- const source = await client.sandboxes.get(sourceId);
58
- return source.fork({ name });
59
- }
60
80
  function shellEscape(value) {
61
81
  return `'${value.replace(/'/g, `'\\''`)}'`;
62
82
  }
@@ -136,15 +156,15 @@ function maxWriteChunkSize(parent, tempPath, diskPath) {
136
156
  }
137
157
  function wrapCommand(command, options) {
138
158
  let wrapped = command;
159
+ if (options?.cwd) {
160
+ wrapped = `cd ${shellEscape(options.cwd)} && ${wrapped}`;
161
+ }
139
162
  const envEntries = Object.entries(options?.env ?? {});
140
163
  if (!envEntries.some(([key]) => key === "HOME")) {
141
164
  envEntries.unshift(["HOME", process.env.HOME || "/tmp"]);
142
165
  }
143
- const envPrefix = envEntries.map(([k, v]) => `${k}=${shellEscape(String(v))}`).join(" ");
144
- wrapped = `${envPrefix} ${wrapped}`;
145
- if (options?.cwd) {
146
- wrapped = `cd ${shellEscape(options.cwd)} && ${wrapped}`;
147
- }
166
+ const exports = envEntries.map(([k, v]) => `${k}=${shellEscape(String(v))}`).join(" ");
167
+ wrapped = `export ${exports}; ${wrapped}`;
148
168
  if (options?.background) {
149
169
  wrapped = `nohup sh -c ${shellEscape(wrapped)} > /dev/null 2>&1 &`;
150
170
  }
@@ -153,15 +173,9 @@ function wrapCommand(command, options) {
153
173
  async function execOnDisk(sandbox, command) {
154
174
  return sandbox.client.disks.exec(sandbox.disk.id, command);
155
175
  }
156
- function wrapSandboxCommand(command, options) {
157
- let wrapped = command;
158
- if (options?.cwd) {
159
- wrapped = `cd ${shellEscape(options.cwd)} && ${wrapped}`;
160
- }
161
- if (options?.background) {
162
- wrapped = `nohup sh -c ${shellEscape(wrapped)} > /dev/null 2>&1 &`;
163
- }
164
- return wrapped;
176
+ function withCwd(command, cwd) {
177
+ return cwd ? `cd ${shellEscape(cwd)} || exit 1
178
+ ${command}` : command;
165
179
  }
166
180
  async function ensureVmRunning(vm) {
167
181
  await vm.refresh();
@@ -172,12 +186,30 @@ async function ensureVmRunning(vm) {
172
186
  await vm.start();
173
187
  }
174
188
  }
189
+ function requireVm(sandbox, action) {
190
+ if (!sandbox.vm) {
191
+ throw new Error(
192
+ `Archil disk exec handles cannot ${action}; create a persistent VM (ephemeral: false) instead.`
193
+ );
194
+ }
195
+ return sandbox.vm;
196
+ }
175
197
  async function stopAndDeleteVm(vm) {
176
198
  if (vm.status === "deleting" || vm.status === "deleted") return;
177
- if (vm.status !== "stopped" && vm.status !== "exited" && vm.status !== "failed") {
178
- await vm.stop();
199
+ try {
200
+ if (vm.status !== "stopped" && vm.status !== "exited" && vm.status !== "failed") {
201
+ await vm.stop();
202
+ }
203
+ await vm.delete();
204
+ } catch (error) {
205
+ if (error instanceof ArchilError && error.status === 404) return;
206
+ if (error instanceof ArchilError && error.status === 409) {
207
+ throw new Error(
208
+ `Archil refused to delete sandbox ${vm.id} (409): ${error.message}. Delete its forks and snapshots first; Archil deletes forks before their parent.`
209
+ );
210
+ }
211
+ throw error;
179
212
  }
180
- await vm.delete();
181
213
  }
182
214
  function toSandboxRequest(options) {
183
215
  const request = {
@@ -195,148 +227,237 @@ function toSandboxRequest(options) {
195
227
  }
196
228
  return request;
197
229
  }
230
+ var SNAPSHOT_NAME_PREFIX = "csdk-snap-";
231
+ function snapshotName(name) {
232
+ const suffix = randomUUID().slice(0, 8);
233
+ const slug = (name ?? "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63 - SNAPSHOT_NAME_PREFIX.length - suffix.length - 1).replace(/-+$/g, "");
234
+ return `${SNAPSHOT_NAME_PREFIX}${slug ? `${slug}-` : ""}${suffix}`;
235
+ }
236
+ function isSnapshotVm(vm) {
237
+ return vm.name?.startsWith(SNAPSHOT_NAME_PREFIX) ?? false;
238
+ }
239
+ function isDiskId(id) {
240
+ return id.startsWith("dsk-");
241
+ }
198
242
  var sandboxModes = /* @__PURE__ */ new Map();
243
+ function modeForId(id, resolved) {
244
+ return sandboxModes.get(id) ?? (isDiskId(id) ? "exec" : resolved.execution);
245
+ }
246
+ function diskInstance(client, disk, resolved) {
247
+ return { client, disk, resolved, createdAt: /* @__PURE__ */ new Date() };
248
+ }
249
+ var TIMEOUT_EXIT_CODE = 124;
250
+ var KILLED_EXIT_CODE = 137;
251
+ function mapProcessResult(result, timedOut, killSignal) {
252
+ if (timedOut || result.status === "timed_out") {
253
+ return { exitCode: TIMEOUT_EXIT_CODE, signal: "SIGKILL", stderrSuffix: "" };
254
+ }
255
+ if (result.status === "cancelled") {
256
+ return { exitCode: KILLED_EXIT_CODE, signal: killSignal ?? "SIGKILL", stderrSuffix: "" };
257
+ }
258
+ if (result.status === "failed") {
259
+ return {
260
+ exitCode: result.exitCode ?? 1,
261
+ signal: null,
262
+ stderrSuffix: result.exitReason ?? ""
263
+ };
264
+ }
265
+ return { exitCode: result.exitCode ?? 0, signal: null, stderrSuffix: "" };
266
+ }
267
+ async function startVmProcess(vm, command, options) {
268
+ await ensureVmRunning(vm);
269
+ const decoders = { stdout: new TextDecoder(), stderr: new TextDecoder() };
270
+ const output = { stdout: "", stderr: "" };
271
+ const emit = (stream, text) => {
272
+ if (!text) return;
273
+ output[stream] += text;
274
+ (stream === "stdout" ? options.onStdout : options.onStderr)?.(text);
275
+ };
276
+ const proc = await vm.processes.start(withCwd(command, options.cwd), {
277
+ env: options.env,
278
+ collectOutput: false,
279
+ timeoutSeconds: options.timeout !== void 0 ? Math.max(1, Math.ceil(options.timeout / 1e3)) : void 0,
280
+ onOutput: (chunk) => emit(chunk.stream, decoders[chunk.stream].decode(chunk.data, { stream: true }))
281
+ });
282
+ const state = { timedOut: false, killSignal: null };
283
+ let timer;
284
+ if (options.timeout !== void 0) {
285
+ timer = setTimeout(() => {
286
+ state.timedOut = true;
287
+ proc.kill().catch(() => {
288
+ });
289
+ }, options.timeout);
290
+ }
291
+ const done = proc.wait().then((result) => {
292
+ emit("stdout", decoders.stdout.decode());
293
+ emit("stderr", decoders.stderr.decode());
294
+ const mapped = mapProcessResult(result, state.timedOut, state.killSignal);
295
+ return {
296
+ stdout: output.stdout,
297
+ stderr: mapped.stderrSuffix ? `${output.stderr}${output.stderr && !output.stderr.endsWith("\n") ? "\n" : ""}${mapped.stderrSuffix}` : output.stderr,
298
+ exitCode: mapped.exitCode,
299
+ signal: mapped.signal
300
+ };
301
+ }).catch((error) => ({
302
+ stdout: output.stdout,
303
+ stderr: `${output.stderr}${error instanceof Error ? error.message : String(error)}`,
304
+ exitCode: 1,
305
+ signal: null
306
+ })).finally(() => {
307
+ if (timer) clearTimeout(timer);
308
+ });
309
+ return { proc, done, state, output };
310
+ }
311
+ async function runVmCommand(vm, command, options) {
312
+ const startTime = Date.now();
313
+ if (options?.background) {
314
+ await ensureVmRunning(vm);
315
+ const proc = await vm.processes.start(withCwd(command, options.cwd), {
316
+ env: options.env,
317
+ collectOutput: false
318
+ });
319
+ await proc.disconnect();
320
+ return { stdout: "", stderr: "", exitCode: 0, durationMs: Date.now() - startTime };
321
+ }
322
+ const { done } = await startVmProcess(vm, command, {
323
+ cwd: options?.cwd,
324
+ env: options?.env,
325
+ timeout: options?.timeout,
326
+ onStdout: options?.onStdout,
327
+ onStderr: options?.onStderr
328
+ });
329
+ const result = await done;
330
+ return {
331
+ stdout: result.stdout,
332
+ stderr: result.stderr,
333
+ exitCode: result.exitCode,
334
+ durationMs: Date.now() - startTime
335
+ };
336
+ }
337
+ function commandError(error, startTime) {
338
+ return {
339
+ stdout: "",
340
+ stderr: error instanceof Error ? error.message : String(error),
341
+ exitCode: 1,
342
+ durationMs: Date.now() - startTime
343
+ };
344
+ }
345
+ function resolveSizingExecution(config, options) {
346
+ if (options?.ephemeral !== void 0) return options.ephemeral ? "exec" : "persistent";
347
+ return resolveExecution(config);
348
+ }
199
349
  var _provider = defineProvider({
200
350
  name: "archil",
351
+ sizes: (config, profile, options) => {
352
+ if (options?.sandbox !== void 0 && (options.sandbox === null || typeof options.sandbox !== "object" || Array.isArray(options.sandbox))) {
353
+ throw new InstanceSizeError("invalid", "sandbox must be an object when requesting sizeClass");
354
+ }
355
+ if (options?.vcpuCount !== void 0 || options?.memSizeMiB !== void 0 || options?.vcpu_count !== void 0 || options?.mem_size_mib !== void 0 || options?.sandbox?.vcpu_count !== void 0 || options?.sandbox?.mem_size_mib !== void 0 || options?.sandbox?.vcpuCount !== void 0 || options?.sandbox?.memSizeMiB !== void 0) {
356
+ throw new InstanceSizeError("conflict", "cannot combine sizeClass with native Archil CPU/memory fields");
357
+ }
358
+ if (options?.snapshotId !== void 0 || options?.sandboxId !== void 0 || options?.instanceId !== void 0) {
359
+ return { status: "unsupported", reason: "Archil attach/fork does not apply CPU/RAM overrides; create a new persistent VM to request a standard size." };
360
+ }
361
+ if (resolveSizingExecution(config, options) !== "persistent") {
362
+ return { status: "unavailable", reason: 'Archil exec containers do not expose VM sizing. Drop execution: "exec" (persistent VMs are the default) or set ephemeral: false.' };
363
+ }
364
+ if (profile.sizeClass === "xs" || profile.sizeClass === "2xl") {
365
+ return { status: "unknown", reason: "This adapter has not verified this standard shape." };
366
+ }
367
+ return { status: "supported", nativeOptions: { vcpus: profile.vcpus, memoryMiB: profile.memoryMiB } };
368
+ },
201
369
  methods: {
202
370
  instances: {
203
371
  create: async (config, options) => {
204
372
  const resolved = resolveConfig(config);
205
373
  const client = createClient(config, resolved);
206
374
  const execution = options?.ephemeral !== void 0 ? options.ephemeral ? "exec" : "persistent" : resolved.execution;
207
- if (options?.snapshotId && options?.ephemeral === true) {
375
+ if (options?.snapshotId && execution === "exec") {
208
376
  throw new Error(
209
- "Archil cannot combine ephemeral: true with snapshotId \u2014 snapshots fork persistent VMs. Drop ephemeral or set it to false."
377
+ 'Archil snapshots fork persistent VMs and cannot be restored in exec mode. Drop execution: "exec" / ephemeral: true.'
210
378
  );
211
379
  }
212
- if (execution === "persistent" || options?.snapshotId) {
213
- let vm;
214
- if (options?.snapshotId) {
215
- vm = await forkSandboxVm(client, options.snapshotId, options?.name);
216
- } else if (options?.sandboxId) {
217
- if (options?.signal) {
218
- throw new Error(
219
- "create({ sandboxId }) attaches to an existing sandbox and cannot be combined with an abort signal"
220
- );
221
- }
222
- vm = await client.sandboxes.get(options.sandboxId);
223
- await ensureVmRunning(vm);
224
- } else {
225
- vm = await client.sandboxes.create(toSandboxRequest(options), {
226
- wait: true
227
- });
380
+ if (execution === "exec") {
381
+ const diskId = resolveCreateDiskId(options);
382
+ sandboxModes.set(diskId, "exec");
383
+ return { instance: diskInstance(client, { id: diskId }, resolved), instanceId: diskId };
384
+ }
385
+ if (options?.diskId) {
386
+ throw new Error(
387
+ 'Archil create({ diskId }) attaches a disk exec handle, which is opt-in now that persistent VMs are the default.\n\nOpt in with archil({ execution: "exec" }) (or ARCHIL_EXECUTION=exec), or pass ephemeral: true.'
388
+ );
389
+ }
390
+ let vm;
391
+ if (options?.snapshotId) {
392
+ const source = await client.sandboxes.get(options.snapshotId);
393
+ vm = await source.fork({ name: options?.name });
394
+ } else if (options?.sandboxId) {
395
+ if (options?.signal) {
396
+ throw new Error(
397
+ "create({ sandboxId }) attaches to an existing sandbox and cannot be combined with an abort signal"
398
+ );
228
399
  }
229
- sandboxModes.set(vm.id, "persistent");
230
- return {
231
- instance: {
232
- client,
233
- disk: { id: vm.id },
234
- vm,
235
- resolved,
236
- createdAt: /* @__PURE__ */ new Date()
237
- },
238
- instanceId: vm.id
239
- };
400
+ vm = await client.sandboxes.get(options.sandboxId);
401
+ await ensureVmRunning(vm);
402
+ } else {
403
+ vm = await client.sandboxes.create(toSandboxRequest(options), {
404
+ wait: true
405
+ });
240
406
  }
241
- const diskId = resolveCreateDiskId(options);
242
- sandboxModes.set(diskId, "exec");
243
- return {
244
- instance: {
245
- client,
246
- disk: { id: diskId },
247
- resolved,
248
- createdAt: /* @__PURE__ */ new Date()
249
- },
250
- instanceId: diskId
251
- };
407
+ if (vm.status === "failed") {
408
+ throw new Error(`Archil sandbox ${vm.id} failed to start${vm.exitReason ? `: ${vm.exitReason}` : ""}`);
409
+ }
410
+ sandboxModes.set(vm.id, "persistent");
411
+ return { instance: vmInstance(client, vm, resolved), instanceId: vm.id };
252
412
  },
253
413
  getById: async (config, sandboxId) => {
254
414
  const resolved = resolveConfig(config);
255
415
  const client = createClient(config, resolved);
256
416
  try {
257
- if (resolved.execution === "persistent") {
258
- const vm = await client.sandboxes.get(sandboxId);
259
- await ensureVmRunning(vm);
260
- return {
261
- instance: {
262
- client,
263
- disk: { id: vm.id },
264
- vm,
265
- resolved,
266
- createdAt: /* @__PURE__ */ new Date()
267
- },
268
- instanceId: vm.id
269
- };
417
+ if (modeForId(sandboxId, resolved) === "exec") {
418
+ const disk = await client.disks.get(sandboxId);
419
+ return { instance: diskInstance(client, disk, resolved), instanceId: disk.id };
270
420
  }
271
- const disk = await client.disks.get(sandboxId);
272
- return {
273
- instance: { client, disk, resolved, createdAt: /* @__PURE__ */ new Date() },
274
- instanceId: disk.id
275
- };
421
+ const vm = await client.sandboxes.get(sandboxId);
422
+ await ensureVmRunning(vm);
423
+ return { instance: vmInstance(client, vm, resolved), instanceId: vm.id };
276
424
  } catch {
277
425
  return null;
278
426
  }
279
427
  },
428
+ // Archil's list endpoints return every sandbox (and the SDK follows disk
429
+ // pagination), so a single call yields the full account listing.
280
430
  list: async (config) => {
281
431
  const resolved = resolveConfig(config);
282
432
  const client = createClient(config, resolved);
283
- if (resolved.execution === "persistent") {
284
- const vms = await client.sandboxes.list();
285
- return vms.map((vm) => ({
286
- instance: {
287
- client,
288
- disk: { id: vm.id },
289
- vm,
290
- resolved,
291
- createdAt: /* @__PURE__ */ new Date()
292
- },
293
- instanceId: vm.id
294
- }));
433
+ if (resolved.execution === "exec") {
434
+ const disks = await client.disks.list();
435
+ return disks.map((disk) => ({ instance: diskInstance(client, disk, resolved), instanceId: disk.id }));
295
436
  }
296
- const disks = await client.disks.list();
297
- return disks.map((disk) => ({
298
- instance: { client, disk, resolved, createdAt: /* @__PURE__ */ new Date() },
299
- instanceId: disk.id
300
- }));
437
+ const vms = await client.sandboxes.list();
438
+ return vms.filter((vm) => !isSnapshotVm(vm) && vm.status !== "deleting" && vm.status !== "deleted").map((vm) => ({ instance: vmInstance(client, vm, resolved), instanceId: vm.id }));
301
439
  },
302
440
  destroy: async (config, sandboxId) => {
303
441
  const resolved = resolveConfig(config);
304
- const mode = sandboxModes.get(sandboxId) ?? resolved.execution;
305
- if (mode === "persistent") {
306
- const client = createClient(config, resolved);
307
- const vm = await client.sandboxes.get(sandboxId);
308
- await stopAndDeleteVm(vm);
309
- }
442
+ const mode = modeForId(sandboxId, resolved);
310
443
  sandboxModes.delete(sandboxId);
444
+ if (mode === "exec") return;
445
+ const client = createClient(config, resolved);
446
+ let vm;
447
+ try {
448
+ vm = await client.sandboxes.get(sandboxId);
449
+ } catch (error) {
450
+ if (error instanceof ArchilError && (error.status === 404 || error.status === 400)) return;
451
+ throw error;
452
+ }
453
+ await stopAndDeleteVm(vm);
311
454
  },
312
455
  runCommand: async (sandbox, command, options) => {
313
456
  const startTime = Date.now();
314
457
  try {
315
458
  if (sandbox.vm) {
316
- await ensureVmRunning(sandbox.vm);
317
- const wrapped = wrapSandboxCommand(command, options);
318
- if (options?.background) {
319
- const proc = await sandbox.vm.processes.start(wrapped, {
320
- env: options?.env
321
- });
322
- await proc.disconnect();
323
- return {
324
- stdout: "",
325
- stderr: "",
326
- exitCode: 0,
327
- durationMs: Date.now() - startTime
328
- };
329
- }
330
- const result2 = await sandbox.vm.exec(wrapped, {
331
- env: options?.env,
332
- timeoutSeconds: options?.timeout !== void 0 ? Math.ceil(options.timeout / 1e3) : void 0
333
- });
334
- return {
335
- stdout: result2.stdout,
336
- stderr: result2.stderr,
337
- exitCode: result2.exitCode ?? (result2.status === "completed" ? 0 : 1),
338
- durationMs: Date.now() - startTime
339
- };
459
+ const { onStdout: _out, onStderr: _err, ...rest } = options ?? {};
460
+ return await runVmCommand(sandbox.vm, command, rest);
340
461
  }
341
462
  const result = await execOnDisk(sandbox, wrapCommand(command, options));
342
463
  return {
@@ -346,13 +467,93 @@ var _provider = defineProvider({
346
467
  durationMs: Date.now() - startTime
347
468
  };
348
469
  } catch (error) {
470
+ return commandError(error, startTime);
471
+ }
472
+ },
473
+ // Persistent VMs stream through the process WebSocket. Disk exec only
474
+ // returns output once the command exits, so it is delivered at the end.
475
+ streamCommand: async (sandbox, command, options) => {
476
+ const startTime = Date.now();
477
+ try {
478
+ if (sandbox.vm) return await runVmCommand(sandbox.vm, command, options);
479
+ const result = await execOnDisk(sandbox, wrapCommand(command, options));
480
+ if (result.stdout) options.onStdout?.(result.stdout);
481
+ if (result.stderr) options.onStderr?.(result.stderr);
349
482
  return {
350
- stdout: "",
351
- stderr: error instanceof Error ? error.message : String(error),
352
- exitCode: 1,
483
+ stdout: result.stdout ?? "",
484
+ stderr: result.stderr ?? "",
485
+ exitCode: result.exitCode,
353
486
  durationMs: Date.now() - startTime
354
487
  };
488
+ } catch (error) {
489
+ return commandError(error, startTime);
490
+ }
491
+ },
492
+ startProcess: async (sandbox, command, options) => {
493
+ if (!sandbox.vm) {
494
+ throw new Error(
495
+ "Archil exec mode runs each command in a fresh container, so it cannot host background processes. Use a persistent VM (the default)."
496
+ );
355
497
  }
498
+ const startTime = Date.now();
499
+ const { proc, done, state, output } = await startVmProcess(sandbox.vm, command, {
500
+ cwd: options.cwd,
501
+ env: options.env,
502
+ onStdout: options.onStdout,
503
+ onStderr: options.onStderr
504
+ });
505
+ if (!options.stdin) await proc.closeStdin().catch(() => {
506
+ });
507
+ let exited;
508
+ const finished = done.then((result) => {
509
+ exited = result;
510
+ options.onExit?.({ exitCode: result.exitCode, signal: result.signal });
511
+ return result;
512
+ });
513
+ const toResult = (result) => ({
514
+ stdout: result.stdout,
515
+ stderr: result.stderr,
516
+ exitCode: result.exitCode,
517
+ signal: result.signal,
518
+ durationMs: Date.now() - startTime
519
+ });
520
+ return {
521
+ pid: null,
522
+ jobId: proc.id,
523
+ runtime: "native",
524
+ write: async (data) => {
525
+ if (!options.stdin) {
526
+ throw new Error("Process was started without stdin: true");
527
+ }
528
+ await proc.sendInput(data);
529
+ },
530
+ closeStdin: async () => {
531
+ if (options.stdin && !exited) await proc.closeStdin();
532
+ },
533
+ status: async () => exited ? { status: "exited", exitCode: exited.exitCode, signal: exited.signal, stdout: exited.stdout, stderr: exited.stderr } : { status: "running", exitCode: null, signal: null, stdout: output.stdout, stderr: output.stderr },
534
+ wait: async (waitOptions) => {
535
+ if (waitOptions?.timeout === void 0) return toResult(await finished);
536
+ let timer;
537
+ const timeout = new Promise((_, reject) => {
538
+ timer = setTimeout(
539
+ () => reject(new Error(`Process ${proc.id} did not exit within ${waitOptions.timeout}ms`)),
540
+ waitOptions.timeout
541
+ );
542
+ });
543
+ try {
544
+ return toResult(await Promise.race([finished, timeout]));
545
+ } finally {
546
+ clearTimeout(timer);
547
+ }
548
+ },
549
+ // Archil's process control only cancels (SIGKILL semantics); the
550
+ // requested signal name is what the result reports.
551
+ kill: async (signal) => {
552
+ if (exited) return;
553
+ state.killSignal = signal ?? "SIGKILL";
554
+ await proc.kill();
555
+ }
556
+ };
356
557
  },
357
558
  getInfo: async (sandbox) => {
358
559
  if (sandbox.vm) {
@@ -599,33 +800,64 @@ var _provider = defineProvider({
599
800
  }
600
801
  }
601
802
  },
803
+ // Native pause keeps CPU and memory: files anywhere (including /tmp)
804
+ // and running processes survive. Processes must outlive the command
805
+ // that started them (startProcess, or setsid): Archil reaps a command's
806
+ // process group when it exits. Disk exec handles have no VM to pause.
807
+ pause: async (sandbox) => {
808
+ await requireVm(sandbox, "pause").pause();
809
+ },
810
+ resume: async (sandbox) => {
811
+ await requireVm(sandbox, "resume").resume();
812
+ },
813
+ pauseCapabilities: { memory: true, createOptions: { ephemeral: false } },
602
814
  getInstance: (sandbox) => sandbox
603
815
  },
604
- // Archil has no separate snapshot resource: a "snapshot" is a sandbox
605
- // fork (an isolated writable branch of the source sandbox's state), so
606
- // snapshot ids are ordinary sandbox ids that create() forks again.
816
+ // Archil has no separate snapshot resource. A snapshot is a fork of the
817
+ // source VM (memory and disk), paused so it holds state without running,
818
+ // and named with SNAPSHOT_NAME_PREFIX so it can be listed. create({
819
+ // snapshotId }) forks it again. Forks must be deleted before their parent.
607
820
  snapshot: {
821
+ // A fork copies the VM's memory and whole disk, so restores keep the
822
+ // rootfs (including /tmp) and running processes; the source keeps running.
823
+ capabilities: {
824
+ scope: "rootfs",
825
+ memory: true,
826
+ destroysSource: false,
827
+ createOptions: { ephemeral: false }
828
+ },
608
829
  create: async (config, sandboxId, options) => {
609
830
  const resolved = resolveConfig(config);
610
831
  const client = createClient(config, resolved);
832
+ let fork;
611
833
  try {
612
- const fork = await forkSandboxVm(client, sandboxId, options?.name);
834
+ const source = await client.sandboxes.get(sandboxId);
835
+ fork = await source.fork({ name: snapshotName(options?.name) });
836
+ if (fork.status === "running") await fork.pause();
613
837
  return {
614
838
  id: fork.id,
615
839
  provider: "archil",
616
840
  createdAt: fork.createdAt ?? /* @__PURE__ */ new Date(),
617
- metadata: { name: fork.name }
841
+ metadata: { name: options?.name, archilName: fork.name, sourceId: sandboxId }
618
842
  };
619
843
  } catch (error) {
844
+ if (fork) await stopAndDeleteVm(fork).catch(() => {
845
+ });
620
846
  throw new Error(
621
847
  `Failed to create Archil snapshot: ${error instanceof Error ? error.message : String(error)}`
622
848
  );
623
849
  }
624
850
  },
625
- list: async (_config) => {
626
- throw new Error(
627
- "Archil provider does not support listing snapshots."
628
- );
851
+ list: async (config) => {
852
+ const resolved = resolveConfig(config);
853
+ const client = createClient(config, resolved);
854
+ const vms = await client.sandboxes.list();
855
+ return vms.filter((vm) => isSnapshotVm(vm) && vm.status !== "deleting" && vm.status !== "deleted").map((vm) => ({
856
+ id: vm.id,
857
+ provider: "archil",
858
+ createdAt: vm.createdAt,
859
+ metadata: { archilName: vm.name, archilStatus: vm.status }
860
+ }));
629
861
  },
630
862
  delete: async (config, snapshotId) => {
631
863
  const resolved = resolveConfig(config);