@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/README.md +122 -91
- package/dist/index.d.mts +14 -7
- package/dist/index.d.ts +14 -7
- package/dist/index.js +371 -139
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +372 -140
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
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
|
|
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
|
|
168
|
-
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
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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,148 +251,237 @@ 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",
|
|
375
|
+
sizes: (config, profile, options) => {
|
|
376
|
+
if (options?.sandbox !== void 0 && (options.sandbox === null || typeof options.sandbox !== "object" || Array.isArray(options.sandbox))) {
|
|
377
|
+
throw new import_provider.InstanceSizeError("invalid", "sandbox must be an object when requesting sizeClass");
|
|
378
|
+
}
|
|
379
|
+
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) {
|
|
380
|
+
throw new import_provider.InstanceSizeError("conflict", "cannot combine sizeClass with native Archil CPU/memory fields");
|
|
381
|
+
}
|
|
382
|
+
if (options?.snapshotId !== void 0 || options?.sandboxId !== void 0 || options?.instanceId !== void 0) {
|
|
383
|
+
return { status: "unsupported", reason: "Archil attach/fork does not apply CPU/RAM overrides; create a new persistent VM to request a standard size." };
|
|
384
|
+
}
|
|
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.' };
|
|
387
|
+
}
|
|
388
|
+
if (profile.sizeClass === "xs" || profile.sizeClass === "2xl") {
|
|
389
|
+
return { status: "unknown", reason: "This adapter has not verified this standard shape." };
|
|
390
|
+
}
|
|
391
|
+
return { status: "supported", nativeOptions: { vcpus: profile.vcpus, memoryMiB: profile.memoryMiB } };
|
|
392
|
+
},
|
|
225
393
|
methods: {
|
|
226
394
|
instances: {
|
|
227
395
|
create: async (config, options) => {
|
|
228
396
|
const resolved = resolveConfig(config);
|
|
229
397
|
const client = createClient(config, resolved);
|
|
230
398
|
const execution = options?.ephemeral !== void 0 ? options.ephemeral ? "exec" : "persistent" : resolved.execution;
|
|
231
|
-
if (options?.snapshotId &&
|
|
399
|
+
if (options?.snapshotId && execution === "exec") {
|
|
232
400
|
throw new Error(
|
|
233
|
-
|
|
401
|
+
'Archil snapshots fork persistent VMs and cannot be restored in exec mode. Drop execution: "exec" / ephemeral: true.'
|
|
234
402
|
);
|
|
235
403
|
}
|
|
236
|
-
if (execution === "
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
+
);
|
|
252
423
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
resolved,
|
|
260
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
261
|
-
},
|
|
262
|
-
instanceId: vm.id
|
|
263
|
-
};
|
|
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
|
+
});
|
|
264
430
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
disk: { id: diskId },
|
|
271
|
-
resolved,
|
|
272
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
273
|
-
},
|
|
274
|
-
instanceId: diskId
|
|
275
|
-
};
|
|
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 };
|
|
276
436
|
},
|
|
277
437
|
getById: async (config, sandboxId) => {
|
|
278
438
|
const resolved = resolveConfig(config);
|
|
279
439
|
const client = createClient(config, resolved);
|
|
280
440
|
try {
|
|
281
|
-
if (resolved
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
return {
|
|
285
|
-
instance: {
|
|
286
|
-
client,
|
|
287
|
-
disk: { id: vm.id },
|
|
288
|
-
vm,
|
|
289
|
-
resolved,
|
|
290
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
291
|
-
},
|
|
292
|
-
instanceId: vm.id
|
|
293
|
-
};
|
|
441
|
+
if (modeForId(sandboxId, resolved) === "exec") {
|
|
442
|
+
const disk = await client.disks.get(sandboxId);
|
|
443
|
+
return { instance: diskInstance(client, disk, resolved), instanceId: disk.id };
|
|
294
444
|
}
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
instanceId: disk.id
|
|
299
|
-
};
|
|
445
|
+
const vm = await client.sandboxes.get(sandboxId);
|
|
446
|
+
await ensureVmRunning(vm);
|
|
447
|
+
return { instance: vmInstance(client, vm, resolved), instanceId: vm.id };
|
|
300
448
|
} catch {
|
|
301
449
|
return null;
|
|
302
450
|
}
|
|
303
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.
|
|
304
454
|
list: async (config) => {
|
|
305
455
|
const resolved = resolveConfig(config);
|
|
306
456
|
const client = createClient(config, resolved);
|
|
307
|
-
if (resolved.execution === "
|
|
308
|
-
const
|
|
309
|
-
return
|
|
310
|
-
instance: {
|
|
311
|
-
client,
|
|
312
|
-
disk: { id: vm.id },
|
|
313
|
-
vm,
|
|
314
|
-
resolved,
|
|
315
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
316
|
-
},
|
|
317
|
-
instanceId: vm.id
|
|
318
|
-
}));
|
|
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 }));
|
|
319
460
|
}
|
|
320
|
-
const
|
|
321
|
-
return
|
|
322
|
-
instance: { client, disk, resolved, createdAt: /* @__PURE__ */ new Date() },
|
|
323
|
-
instanceId: disk.id
|
|
324
|
-
}));
|
|
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 }));
|
|
325
463
|
},
|
|
326
464
|
destroy: async (config, sandboxId) => {
|
|
327
465
|
const resolved = resolveConfig(config);
|
|
328
|
-
const mode =
|
|
329
|
-
if (mode === "persistent") {
|
|
330
|
-
const client = createClient(config, resolved);
|
|
331
|
-
const vm = await client.sandboxes.get(sandboxId);
|
|
332
|
-
await stopAndDeleteVm(vm);
|
|
333
|
-
}
|
|
466
|
+
const mode = modeForId(sandboxId, resolved);
|
|
334
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);
|
|
335
478
|
},
|
|
336
479
|
runCommand: async (sandbox, command, options) => {
|
|
337
480
|
const startTime = Date.now();
|
|
338
481
|
try {
|
|
339
482
|
if (sandbox.vm) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (options?.background) {
|
|
343
|
-
const proc = await sandbox.vm.processes.start(wrapped, {
|
|
344
|
-
env: options?.env
|
|
345
|
-
});
|
|
346
|
-
await proc.disconnect();
|
|
347
|
-
return {
|
|
348
|
-
stdout: "",
|
|
349
|
-
stderr: "",
|
|
350
|
-
exitCode: 0,
|
|
351
|
-
durationMs: Date.now() - startTime
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
const result2 = await sandbox.vm.exec(wrapped, {
|
|
355
|
-
env: options?.env,
|
|
356
|
-
timeoutSeconds: options?.timeout !== void 0 ? Math.ceil(options.timeout / 1e3) : void 0
|
|
357
|
-
});
|
|
358
|
-
return {
|
|
359
|
-
stdout: result2.stdout,
|
|
360
|
-
stderr: result2.stderr,
|
|
361
|
-
exitCode: result2.exitCode ?? (result2.status === "completed" ? 0 : 1),
|
|
362
|
-
durationMs: Date.now() - startTime
|
|
363
|
-
};
|
|
483
|
+
const { onStdout: _out, onStderr: _err, ...rest } = options ?? {};
|
|
484
|
+
return await runVmCommand(sandbox.vm, command, rest);
|
|
364
485
|
}
|
|
365
486
|
const result = await execOnDisk(sandbox, wrapCommand(command, options));
|
|
366
487
|
return {
|
|
@@ -370,13 +491,93 @@ var _provider = (0, import_provider.defineProvider)({
|
|
|
370
491
|
durationMs: Date.now() - startTime
|
|
371
492
|
};
|
|
372
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);
|
|
373
506
|
return {
|
|
374
|
-
stdout: "",
|
|
375
|
-
stderr:
|
|
376
|
-
exitCode:
|
|
507
|
+
stdout: result.stdout ?? "",
|
|
508
|
+
stderr: result.stderr ?? "",
|
|
509
|
+
exitCode: result.exitCode,
|
|
377
510
|
durationMs: Date.now() - startTime
|
|
378
511
|
};
|
|
512
|
+
} catch (error) {
|
|
513
|
+
return commandError(error, startTime);
|
|
514
|
+
}
|
|
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
|
+
);
|
|
379
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
|
+
};
|
|
380
581
|
},
|
|
381
582
|
getInfo: async (sandbox) => {
|
|
382
583
|
if (sandbox.vm) {
|
|
@@ -623,33 +824,64 @@ var _provider = (0, import_provider.defineProvider)({
|
|
|
623
824
|
}
|
|
624
825
|
}
|
|
625
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 } },
|
|
626
838
|
getInstance: (sandbox) => sandbox
|
|
627
839
|
},
|
|
628
|
-
// Archil has no separate snapshot resource
|
|
629
|
-
//
|
|
630
|
-
//
|
|
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.
|
|
631
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
|
+
},
|
|
632
853
|
create: async (config, sandboxId, options) => {
|
|
633
854
|
const resolved = resolveConfig(config);
|
|
634
855
|
const client = createClient(config, resolved);
|
|
856
|
+
let fork;
|
|
635
857
|
try {
|
|
636
|
-
const
|
|
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();
|
|
637
861
|
return {
|
|
638
862
|
id: fork.id,
|
|
639
863
|
provider: "archil",
|
|
640
864
|
createdAt: fork.createdAt ?? /* @__PURE__ */ new Date(),
|
|
641
|
-
metadata: { name: fork.name }
|
|
865
|
+
metadata: { name: options?.name, archilName: fork.name, sourceId: sandboxId }
|
|
642
866
|
};
|
|
643
867
|
} catch (error) {
|
|
868
|
+
if (fork) await stopAndDeleteVm(fork).catch(() => {
|
|
869
|
+
});
|
|
644
870
|
throw new Error(
|
|
645
871
|
`Failed to create Archil snapshot: ${error instanceof Error ? error.message : String(error)}`
|
|
646
872
|
);
|
|
647
873
|
}
|
|
648
874
|
},
|
|
649
|
-
list: async (
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
);
|
|
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
|
+
}));
|
|
653
885
|
},
|
|
654
886
|
delete: async (config, snapshotId) => {
|
|
655
887
|
const resolved = resolveConfig(config);
|