@computesdk/archil 0.4.28 → 0.4.30

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.js CHANGED
@@ -45,6 +45,21 @@ function regionToBaseUrl(region) {
45
45
  const color = REGION_COLORS[region] ?? "green";
46
46
  return `https://control.${color}.${suffix}.${cloud}.prod.archil.com`;
47
47
  }
48
+ function vmInstance(client, vm, resolved) {
49
+ return {
50
+ client,
51
+ disk: { id: vm.id },
52
+ vm,
53
+ resolved,
54
+ createdAt: /* @__PURE__ */ new Date(),
55
+ pause: async () => {
56
+ await vm.pause();
57
+ },
58
+ resume: async () => {
59
+ await vm.resume();
60
+ }
61
+ };
62
+ }
48
63
  function resolveConfig(config) {
49
64
  const apiKey = config.apiKey ?? process.env.ARCHIL_API_KEY;
50
65
  const region = config.region ?? process.env.ARCHIL_REGION;
@@ -62,7 +77,16 @@ function resolveConfig(config) {
62
77
  }
63
78
  baseUrl = regionToBaseUrl(region);
64
79
  }
65
- return { apiKey, baseUrl, execution: config.execution ?? "exec" };
80
+ return { apiKey, baseUrl, execution: resolveExecution(config) };
81
+ }
82
+ function resolveExecution(config) {
83
+ if (config.execution) return config.execution;
84
+ const fromEnv = process.env.ARCHIL_EXECUTION?.trim().toLowerCase();
85
+ if (!fromEnv) return "persistent";
86
+ if (fromEnv === "exec" || fromEnv === "persistent") return fromEnv;
87
+ throw new Error(
88
+ `Invalid ARCHIL_EXECUTION "${process.env.ARCHIL_EXECUTION}". Expected "persistent" (default) or "exec".`
89
+ );
66
90
  }
67
91
  function createClient(config, resolved) {
68
92
  const region = config.region ?? process.env.ARCHIL_REGION ?? "aws-us-east-1";
@@ -77,10 +101,6 @@ function resolveCreateDiskId(options) {
77
101
  }
78
102
  return diskId;
79
103
  }
80
- async function forkSandboxVm(client, sourceId, name) {
81
- const source = await client.sandboxes.get(sourceId);
82
- return source.fork({ name });
83
- }
84
104
  function shellEscape(value) {
85
105
  return `'${value.replace(/'/g, `'\\''`)}'`;
86
106
  }
@@ -160,15 +180,15 @@ function maxWriteChunkSize(parent, tempPath, diskPath) {
160
180
  }
161
181
  function wrapCommand(command, options) {
162
182
  let wrapped = command;
183
+ if (options?.cwd) {
184
+ wrapped = `cd ${shellEscape(options.cwd)} && ${wrapped}`;
185
+ }
163
186
  const envEntries = Object.entries(options?.env ?? {});
164
187
  if (!envEntries.some(([key]) => key === "HOME")) {
165
188
  envEntries.unshift(["HOME", process.env.HOME || "/tmp"]);
166
189
  }
167
- const envPrefix = envEntries.map(([k, v]) => `${k}=${shellEscape(String(v))}`).join(" ");
168
- wrapped = `${envPrefix} ${wrapped}`;
169
- if (options?.cwd) {
170
- wrapped = `cd ${shellEscape(options.cwd)} && ${wrapped}`;
171
- }
190
+ const exports2 = envEntries.map(([k, v]) => `${k}=${shellEscape(String(v))}`).join(" ");
191
+ wrapped = `export ${exports2}; ${wrapped}`;
172
192
  if (options?.background) {
173
193
  wrapped = `nohup sh -c ${shellEscape(wrapped)} > /dev/null 2>&1 &`;
174
194
  }
@@ -177,15 +197,9 @@ function wrapCommand(command, options) {
177
197
  async function execOnDisk(sandbox, command) {
178
198
  return sandbox.client.disks.exec(sandbox.disk.id, command);
179
199
  }
180
- function wrapSandboxCommand(command, options) {
181
- let wrapped = command;
182
- if (options?.cwd) {
183
- wrapped = `cd ${shellEscape(options.cwd)} && ${wrapped}`;
184
- }
185
- if (options?.background) {
186
- wrapped = `nohup sh -c ${shellEscape(wrapped)} > /dev/null 2>&1 &`;
187
- }
188
- return wrapped;
200
+ function withCwd(command, cwd) {
201
+ return cwd ? `cd ${shellEscape(cwd)} || exit 1
202
+ ${command}` : command;
189
203
  }
190
204
  async function ensureVmRunning(vm) {
191
205
  await vm.refresh();
@@ -196,12 +210,30 @@ async function ensureVmRunning(vm) {
196
210
  await vm.start();
197
211
  }
198
212
  }
213
+ function requireVm(sandbox, action) {
214
+ if (!sandbox.vm) {
215
+ throw new Error(
216
+ `Archil disk exec handles cannot ${action}; create a persistent VM (ephemeral: false) instead.`
217
+ );
218
+ }
219
+ return sandbox.vm;
220
+ }
199
221
  async function stopAndDeleteVm(vm) {
200
222
  if (vm.status === "deleting" || vm.status === "deleted") return;
201
- if (vm.status !== "stopped" && vm.status !== "exited" && vm.status !== "failed") {
202
- await vm.stop();
223
+ try {
224
+ if (vm.status !== "stopped" && vm.status !== "exited" && vm.status !== "failed") {
225
+ await vm.stop();
226
+ }
227
+ await vm.delete();
228
+ } catch (error) {
229
+ if (error instanceof import_disk.ArchilError && error.status === 404) return;
230
+ if (error instanceof import_disk.ArchilError && error.status === 409) {
231
+ throw new Error(
232
+ `Archil refused to delete sandbox ${vm.id} (409): ${error.message}. Delete its forks and snapshots first; Archil deletes forks before their parent.`
233
+ );
234
+ }
235
+ throw error;
203
236
  }
204
- await vm.delete();
205
237
  }
206
238
  function toSandboxRequest(options) {
207
239
  const request = {
@@ -219,7 +251,125 @@ function toSandboxRequest(options) {
219
251
  }
220
252
  return request;
221
253
  }
254
+ var SNAPSHOT_NAME_PREFIX = "csdk-snap-";
255
+ function snapshotName(name) {
256
+ const suffix = (0, import_node_crypto.randomUUID)().slice(0, 8);
257
+ const slug = (name ?? "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63 - SNAPSHOT_NAME_PREFIX.length - suffix.length - 1).replace(/-+$/g, "");
258
+ return `${SNAPSHOT_NAME_PREFIX}${slug ? `${slug}-` : ""}${suffix}`;
259
+ }
260
+ function isSnapshotVm(vm) {
261
+ return vm.name?.startsWith(SNAPSHOT_NAME_PREFIX) ?? false;
262
+ }
263
+ function isDiskId(id) {
264
+ return id.startsWith("dsk-");
265
+ }
222
266
  var sandboxModes = /* @__PURE__ */ new Map();
267
+ function modeForId(id, resolved) {
268
+ return sandboxModes.get(id) ?? (isDiskId(id) ? "exec" : resolved.execution);
269
+ }
270
+ function diskInstance(client, disk, resolved) {
271
+ return { client, disk, resolved, createdAt: /* @__PURE__ */ new Date() };
272
+ }
273
+ var TIMEOUT_EXIT_CODE = 124;
274
+ var KILLED_EXIT_CODE = 137;
275
+ function mapProcessResult(result, timedOut, killSignal) {
276
+ if (timedOut || result.status === "timed_out") {
277
+ return { exitCode: TIMEOUT_EXIT_CODE, signal: "SIGKILL", stderrSuffix: "" };
278
+ }
279
+ if (result.status === "cancelled") {
280
+ return { exitCode: KILLED_EXIT_CODE, signal: killSignal ?? "SIGKILL", stderrSuffix: "" };
281
+ }
282
+ if (result.status === "failed") {
283
+ return {
284
+ exitCode: result.exitCode ?? 1,
285
+ signal: null,
286
+ stderrSuffix: result.exitReason ?? ""
287
+ };
288
+ }
289
+ return { exitCode: result.exitCode ?? 0, signal: null, stderrSuffix: "" };
290
+ }
291
+ async function startVmProcess(vm, command, options) {
292
+ await ensureVmRunning(vm);
293
+ const decoders = { stdout: new TextDecoder(), stderr: new TextDecoder() };
294
+ const output = { stdout: "", stderr: "" };
295
+ const emit = (stream, text) => {
296
+ if (!text) return;
297
+ output[stream] += text;
298
+ (stream === "stdout" ? options.onStdout : options.onStderr)?.(text);
299
+ };
300
+ const proc = await vm.processes.start(withCwd(command, options.cwd), {
301
+ env: options.env,
302
+ collectOutput: false,
303
+ timeoutSeconds: options.timeout !== void 0 ? Math.max(1, Math.ceil(options.timeout / 1e3)) : void 0,
304
+ onOutput: (chunk) => emit(chunk.stream, decoders[chunk.stream].decode(chunk.data, { stream: true }))
305
+ });
306
+ const state = { timedOut: false, killSignal: null };
307
+ let timer;
308
+ if (options.timeout !== void 0) {
309
+ timer = setTimeout(() => {
310
+ state.timedOut = true;
311
+ proc.kill().catch(() => {
312
+ });
313
+ }, options.timeout);
314
+ }
315
+ const done = proc.wait().then((result) => {
316
+ emit("stdout", decoders.stdout.decode());
317
+ emit("stderr", decoders.stderr.decode());
318
+ const mapped = mapProcessResult(result, state.timedOut, state.killSignal);
319
+ return {
320
+ stdout: output.stdout,
321
+ stderr: mapped.stderrSuffix ? `${output.stderr}${output.stderr && !output.stderr.endsWith("\n") ? "\n" : ""}${mapped.stderrSuffix}` : output.stderr,
322
+ exitCode: mapped.exitCode,
323
+ signal: mapped.signal
324
+ };
325
+ }).catch((error) => ({
326
+ stdout: output.stdout,
327
+ stderr: `${output.stderr}${error instanceof Error ? error.message : String(error)}`,
328
+ exitCode: 1,
329
+ signal: null
330
+ })).finally(() => {
331
+ if (timer) clearTimeout(timer);
332
+ });
333
+ return { proc, done, state, output };
334
+ }
335
+ async function runVmCommand(vm, command, options) {
336
+ const startTime = Date.now();
337
+ if (options?.background) {
338
+ await ensureVmRunning(vm);
339
+ const proc = await vm.processes.start(withCwd(command, options.cwd), {
340
+ env: options.env,
341
+ collectOutput: false
342
+ });
343
+ await proc.disconnect();
344
+ return { stdout: "", stderr: "", exitCode: 0, durationMs: Date.now() - startTime };
345
+ }
346
+ const { done } = await startVmProcess(vm, command, {
347
+ cwd: options?.cwd,
348
+ env: options?.env,
349
+ timeout: options?.timeout,
350
+ onStdout: options?.onStdout,
351
+ onStderr: options?.onStderr
352
+ });
353
+ const result = await done;
354
+ return {
355
+ stdout: result.stdout,
356
+ stderr: result.stderr,
357
+ exitCode: result.exitCode,
358
+ durationMs: Date.now() - startTime
359
+ };
360
+ }
361
+ function commandError(error, startTime) {
362
+ return {
363
+ stdout: "",
364
+ stderr: error instanceof Error ? error.message : String(error),
365
+ exitCode: 1,
366
+ durationMs: Date.now() - startTime
367
+ };
368
+ }
369
+ function resolveSizingExecution(config, options) {
370
+ if (options?.ephemeral !== void 0) return options.ephemeral ? "exec" : "persistent";
371
+ return resolveExecution(config);
372
+ }
223
373
  var _provider = (0, import_provider.defineProvider)({
224
374
  name: "archil",
225
375
  sizes: (config, profile, options) => {
@@ -232,9 +382,8 @@ var _provider = (0, import_provider.defineProvider)({
232
382
  if (options?.snapshotId !== void 0 || options?.sandboxId !== void 0 || options?.instanceId !== void 0) {
233
383
  return { status: "unsupported", reason: "Archil attach/fork does not apply CPU/RAM overrides; create a new persistent VM to request a standard size." };
234
384
  }
235
- const execution = options?.ephemeral === void 0 ? config.execution ?? "exec" : options.ephemeral ? "exec" : "persistent";
236
- if (execution !== "persistent") {
237
- return { status: "unavailable", reason: "Archil exec containers do not expose persistent VM sizing. Select execution: persistent or ephemeral: false." };
385
+ if (resolveSizingExecution(config, options) !== "persistent") {
386
+ return { status: "unavailable", reason: 'Archil exec containers do not expose VM sizing. Drop execution: "exec" (persistent VMs are the default) or set ephemeral: false.' };
238
387
  }
239
388
  if (profile.sizeClass === "xs" || profile.sizeClass === "2xl") {
240
389
  return { status: "unknown", reason: "This adapter has not verified this standard shape." };
@@ -247,139 +396,92 @@ var _provider = (0, import_provider.defineProvider)({
247
396
  const resolved = resolveConfig(config);
248
397
  const client = createClient(config, resolved);
249
398
  const execution = options?.ephemeral !== void 0 ? options.ephemeral ? "exec" : "persistent" : resolved.execution;
250
- if (options?.snapshotId && options?.ephemeral === true) {
399
+ if (options?.snapshotId && execution === "exec") {
251
400
  throw new Error(
252
- "Archil cannot combine ephemeral: true with snapshotId \u2014 snapshots fork persistent VMs. Drop ephemeral or set it to false."
401
+ 'Archil snapshots fork persistent VMs and cannot be restored in exec mode. Drop execution: "exec" / ephemeral: true.'
253
402
  );
254
403
  }
255
- if (execution === "persistent" || options?.snapshotId) {
256
- let vm;
257
- if (options?.snapshotId) {
258
- vm = await forkSandboxVm(client, options.snapshotId, options?.name);
259
- } else if (options?.sandboxId) {
260
- if (options?.signal) {
261
- throw new Error(
262
- "create({ sandboxId }) attaches to an existing sandbox and cannot be combined with an abort signal"
263
- );
264
- }
265
- vm = await client.sandboxes.get(options.sandboxId);
266
- await ensureVmRunning(vm);
267
- } else {
268
- vm = await client.sandboxes.create(toSandboxRequest(options), {
269
- wait: true
270
- });
404
+ if (execution === "exec") {
405
+ const diskId = resolveCreateDiskId(options);
406
+ sandboxModes.set(diskId, "exec");
407
+ return { instance: diskInstance(client, { id: diskId }, resolved), instanceId: diskId };
408
+ }
409
+ if (options?.diskId) {
410
+ throw new Error(
411
+ '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.'
412
+ );
413
+ }
414
+ let vm;
415
+ if (options?.snapshotId) {
416
+ const source = await client.sandboxes.get(options.snapshotId);
417
+ vm = await source.fork({ name: options?.name });
418
+ } else if (options?.sandboxId) {
419
+ if (options?.signal) {
420
+ throw new Error(
421
+ "create({ sandboxId }) attaches to an existing sandbox and cannot be combined with an abort signal"
422
+ );
271
423
  }
272
- sandboxModes.set(vm.id, "persistent");
273
- return {
274
- instance: {
275
- client,
276
- disk: { id: vm.id },
277
- vm,
278
- resolved,
279
- createdAt: /* @__PURE__ */ new Date()
280
- },
281
- instanceId: vm.id
282
- };
424
+ vm = await client.sandboxes.get(options.sandboxId);
425
+ await ensureVmRunning(vm);
426
+ } else {
427
+ vm = await client.sandboxes.create(toSandboxRequest(options), {
428
+ wait: true
429
+ });
283
430
  }
284
- const diskId = resolveCreateDiskId(options);
285
- sandboxModes.set(diskId, "exec");
286
- return {
287
- instance: {
288
- client,
289
- disk: { id: diskId },
290
- resolved,
291
- createdAt: /* @__PURE__ */ new Date()
292
- },
293
- instanceId: diskId
294
- };
431
+ if (vm.status === "failed") {
432
+ throw new Error(`Archil sandbox ${vm.id} failed to start${vm.exitReason ? `: ${vm.exitReason}` : ""}`);
433
+ }
434
+ sandboxModes.set(vm.id, "persistent");
435
+ return { instance: vmInstance(client, vm, resolved), instanceId: vm.id };
295
436
  },
296
437
  getById: async (config, sandboxId) => {
297
438
  const resolved = resolveConfig(config);
298
439
  const client = createClient(config, resolved);
299
440
  try {
300
- if (resolved.execution === "persistent") {
301
- const vm = await client.sandboxes.get(sandboxId);
302
- await ensureVmRunning(vm);
303
- return {
304
- instance: {
305
- client,
306
- disk: { id: vm.id },
307
- vm,
308
- resolved,
309
- createdAt: /* @__PURE__ */ new Date()
310
- },
311
- instanceId: vm.id
312
- };
441
+ if (modeForId(sandboxId, resolved) === "exec") {
442
+ const disk = await client.disks.get(sandboxId);
443
+ return { instance: diskInstance(client, disk, resolved), instanceId: disk.id };
313
444
  }
314
- const disk = await client.disks.get(sandboxId);
315
- return {
316
- instance: { client, disk, resolved, createdAt: /* @__PURE__ */ new Date() },
317
- instanceId: disk.id
318
- };
445
+ const vm = await client.sandboxes.get(sandboxId);
446
+ await ensureVmRunning(vm);
447
+ return { instance: vmInstance(client, vm, resolved), instanceId: vm.id };
319
448
  } catch {
320
449
  return null;
321
450
  }
322
451
  },
452
+ // Archil's list endpoints return every sandbox (and the SDK follows disk
453
+ // pagination), so a single call yields the full account listing.
323
454
  list: async (config) => {
324
455
  const resolved = resolveConfig(config);
325
456
  const client = createClient(config, resolved);
326
- if (resolved.execution === "persistent") {
327
- const vms = await client.sandboxes.list();
328
- return vms.map((vm) => ({
329
- instance: {
330
- client,
331
- disk: { id: vm.id },
332
- vm,
333
- resolved,
334
- createdAt: /* @__PURE__ */ new Date()
335
- },
336
- instanceId: vm.id
337
- }));
457
+ if (resolved.execution === "exec") {
458
+ const disks = await client.disks.list();
459
+ return disks.map((disk) => ({ instance: diskInstance(client, disk, resolved), instanceId: disk.id }));
338
460
  }
339
- const disks = await client.disks.list();
340
- return disks.map((disk) => ({
341
- instance: { client, disk, resolved, createdAt: /* @__PURE__ */ new Date() },
342
- instanceId: disk.id
343
- }));
461
+ const vms = await client.sandboxes.list();
462
+ return vms.filter((vm) => !isSnapshotVm(vm) && vm.status !== "deleting" && vm.status !== "deleted").map((vm) => ({ instance: vmInstance(client, vm, resolved), instanceId: vm.id }));
344
463
  },
345
464
  destroy: async (config, sandboxId) => {
346
465
  const resolved = resolveConfig(config);
347
- const mode = sandboxModes.get(sandboxId) ?? resolved.execution;
348
- if (mode === "persistent") {
349
- const client = createClient(config, resolved);
350
- const vm = await client.sandboxes.get(sandboxId);
351
- await stopAndDeleteVm(vm);
352
- }
466
+ const mode = modeForId(sandboxId, resolved);
353
467
  sandboxModes.delete(sandboxId);
468
+ if (mode === "exec") return;
469
+ const client = createClient(config, resolved);
470
+ let vm;
471
+ try {
472
+ vm = await client.sandboxes.get(sandboxId);
473
+ } catch (error) {
474
+ if (error instanceof import_disk.ArchilError && (error.status === 404 || error.status === 400)) return;
475
+ throw error;
476
+ }
477
+ await stopAndDeleteVm(vm);
354
478
  },
355
479
  runCommand: async (sandbox, command, options) => {
356
480
  const startTime = Date.now();
357
481
  try {
358
482
  if (sandbox.vm) {
359
- await ensureVmRunning(sandbox.vm);
360
- const wrapped = wrapSandboxCommand(command, options);
361
- if (options?.background) {
362
- const proc = await sandbox.vm.processes.start(wrapped, {
363
- env: options?.env
364
- });
365
- await proc.disconnect();
366
- return {
367
- stdout: "",
368
- stderr: "",
369
- exitCode: 0,
370
- durationMs: Date.now() - startTime
371
- };
372
- }
373
- const result2 = await sandbox.vm.exec(wrapped, {
374
- env: options?.env,
375
- timeoutSeconds: options?.timeout !== void 0 ? Math.ceil(options.timeout / 1e3) : void 0
376
- });
377
- return {
378
- stdout: result2.stdout,
379
- stderr: result2.stderr,
380
- exitCode: result2.exitCode ?? (result2.status === "completed" ? 0 : 1),
381
- durationMs: Date.now() - startTime
382
- };
483
+ const { onStdout: _out, onStderr: _err, ...rest } = options ?? {};
484
+ return await runVmCommand(sandbox.vm, command, rest);
383
485
  }
384
486
  const result = await execOnDisk(sandbox, wrapCommand(command, options));
385
487
  return {
@@ -389,14 +491,94 @@ var _provider = (0, import_provider.defineProvider)({
389
491
  durationMs: Date.now() - startTime
390
492
  };
391
493
  } catch (error) {
494
+ return commandError(error, startTime);
495
+ }
496
+ },
497
+ // Persistent VMs stream through the process WebSocket. Disk exec only
498
+ // returns output once the command exits, so it is delivered at the end.
499
+ streamCommand: async (sandbox, command, options) => {
500
+ const startTime = Date.now();
501
+ try {
502
+ if (sandbox.vm) return await runVmCommand(sandbox.vm, command, options);
503
+ const result = await execOnDisk(sandbox, wrapCommand(command, options));
504
+ if (result.stdout) options.onStdout?.(result.stdout);
505
+ if (result.stderr) options.onStderr?.(result.stderr);
392
506
  return {
393
- stdout: "",
394
- stderr: error instanceof Error ? error.message : String(error),
395
- exitCode: 1,
507
+ stdout: result.stdout ?? "",
508
+ stderr: result.stderr ?? "",
509
+ exitCode: result.exitCode,
396
510
  durationMs: Date.now() - startTime
397
511
  };
512
+ } catch (error) {
513
+ return commandError(error, startTime);
398
514
  }
399
515
  },
516
+ startProcess: async (sandbox, command, options) => {
517
+ if (!sandbox.vm) {
518
+ throw new Error(
519
+ "Archil exec mode runs each command in a fresh container, so it cannot host background processes. Use a persistent VM (the default)."
520
+ );
521
+ }
522
+ const startTime = Date.now();
523
+ const { proc, done, state, output } = await startVmProcess(sandbox.vm, command, {
524
+ cwd: options.cwd,
525
+ env: options.env,
526
+ onStdout: options.onStdout,
527
+ onStderr: options.onStderr
528
+ });
529
+ if (!options.stdin) await proc.closeStdin().catch(() => {
530
+ });
531
+ let exited;
532
+ const finished = done.then((result) => {
533
+ exited = result;
534
+ options.onExit?.({ exitCode: result.exitCode, signal: result.signal });
535
+ return result;
536
+ });
537
+ const toResult = (result) => ({
538
+ stdout: result.stdout,
539
+ stderr: result.stderr,
540
+ exitCode: result.exitCode,
541
+ signal: result.signal,
542
+ durationMs: Date.now() - startTime
543
+ });
544
+ return {
545
+ pid: null,
546
+ jobId: proc.id,
547
+ runtime: "native",
548
+ write: async (data) => {
549
+ if (!options.stdin) {
550
+ throw new Error("Process was started without stdin: true");
551
+ }
552
+ await proc.sendInput(data);
553
+ },
554
+ closeStdin: async () => {
555
+ if (options.stdin && !exited) await proc.closeStdin();
556
+ },
557
+ 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 },
558
+ wait: async (waitOptions) => {
559
+ if (waitOptions?.timeout === void 0) return toResult(await finished);
560
+ let timer;
561
+ const timeout = new Promise((_, reject) => {
562
+ timer = setTimeout(
563
+ () => reject(new Error(`Process ${proc.id} did not exit within ${waitOptions.timeout}ms`)),
564
+ waitOptions.timeout
565
+ );
566
+ });
567
+ try {
568
+ return toResult(await Promise.race([finished, timeout]));
569
+ } finally {
570
+ clearTimeout(timer);
571
+ }
572
+ },
573
+ // Archil's process control only cancels (SIGKILL semantics); the
574
+ // requested signal name is what the result reports.
575
+ kill: async (signal) => {
576
+ if (exited) return;
577
+ state.killSignal = signal ?? "SIGKILL";
578
+ await proc.kill();
579
+ }
580
+ };
581
+ },
400
582
  getInfo: async (sandbox) => {
401
583
  if (sandbox.vm) {
402
584
  const vm = sandbox.vm;
@@ -642,33 +824,64 @@ var _provider = (0, import_provider.defineProvider)({
642
824
  }
643
825
  }
644
826
  },
827
+ // Native pause keeps CPU and memory: files anywhere (including /tmp)
828
+ // and running processes survive. Processes must outlive the command
829
+ // that started them (startProcess, or setsid): Archil reaps a command's
830
+ // process group when it exits. Disk exec handles have no VM to pause.
831
+ pause: async (sandbox) => {
832
+ await requireVm(sandbox, "pause").pause();
833
+ },
834
+ resume: async (sandbox) => {
835
+ await requireVm(sandbox, "resume").resume();
836
+ },
837
+ pauseCapabilities: { memory: true, createOptions: { ephemeral: false } },
645
838
  getInstance: (sandbox) => sandbox
646
839
  },
647
- // Archil has no separate snapshot resource: a "snapshot" is a sandbox
648
- // fork (an isolated writable branch of the source sandbox's state), so
649
- // snapshot ids are ordinary sandbox ids that create() forks again.
840
+ // Archil has no separate snapshot resource. A snapshot is a fork of the
841
+ // source VM (memory and disk), paused so it holds state without running,
842
+ // and named with SNAPSHOT_NAME_PREFIX so it can be listed. create({
843
+ // snapshotId }) forks it again. Forks must be deleted before their parent.
650
844
  snapshot: {
845
+ // A fork copies the VM's memory and whole disk, so restores keep the
846
+ // rootfs (including /tmp) and running processes; the source keeps running.
847
+ capabilities: {
848
+ scope: "rootfs",
849
+ memory: true,
850
+ destroysSource: false,
851
+ createOptions: { ephemeral: false }
852
+ },
651
853
  create: async (config, sandboxId, options) => {
652
854
  const resolved = resolveConfig(config);
653
855
  const client = createClient(config, resolved);
856
+ let fork;
654
857
  try {
655
- const fork = await forkSandboxVm(client, sandboxId, options?.name);
858
+ const source = await client.sandboxes.get(sandboxId);
859
+ fork = await source.fork({ name: snapshotName(options?.name) });
860
+ if (fork.status === "running") await fork.pause();
656
861
  return {
657
862
  id: fork.id,
658
863
  provider: "archil",
659
864
  createdAt: fork.createdAt ?? /* @__PURE__ */ new Date(),
660
- metadata: { name: fork.name }
865
+ metadata: { name: options?.name, archilName: fork.name, sourceId: sandboxId }
661
866
  };
662
867
  } catch (error) {
868
+ if (fork) await stopAndDeleteVm(fork).catch(() => {
869
+ });
663
870
  throw new Error(
664
871
  `Failed to create Archil snapshot: ${error instanceof Error ? error.message : String(error)}`
665
872
  );
666
873
  }
667
874
  },
668
- list: async (_config) => {
669
- throw new Error(
670
- "Archil provider does not support listing snapshots."
671
- );
875
+ list: async (config) => {
876
+ const resolved = resolveConfig(config);
877
+ const client = createClient(config, resolved);
878
+ const vms = await client.sandboxes.list();
879
+ return vms.filter((vm) => isSnapshotVm(vm) && vm.status !== "deleting" && vm.status !== "deleted").map((vm) => ({
880
+ id: vm.id,
881
+ provider: "archil",
882
+ createdAt: vm.createdAt,
883
+ metadata: { archilName: vm.name, archilStatus: vm.status }
884
+ }));
672
885
  },
673
886
  delete: async (config, snapshotId) => {
674
887
  const resolved = resolveConfig(config);