@computesdk/archil 0.4.28 → 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 +109 -96
- package/dist/index.d.mts +11 -6
- package/dist/index.d.ts +11 -6
- package/dist/index.js +355 -142
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +355 -142
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -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
|
|
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
|
|
144
|
-
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
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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,7 +227,125 @@ 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",
|
|
201
351
|
sizes: (config, profile, options) => {
|
|
@@ -208,9 +358,8 @@ var _provider = defineProvider({
|
|
|
208
358
|
if (options?.snapshotId !== void 0 || options?.sandboxId !== void 0 || options?.instanceId !== void 0) {
|
|
209
359
|
return { status: "unsupported", reason: "Archil attach/fork does not apply CPU/RAM overrides; create a new persistent VM to request a standard size." };
|
|
210
360
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return { status: "unavailable", reason: "Archil exec containers do not expose persistent VM sizing. Select execution: persistent or ephemeral: false." };
|
|
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.' };
|
|
214
363
|
}
|
|
215
364
|
if (profile.sizeClass === "xs" || profile.sizeClass === "2xl") {
|
|
216
365
|
return { status: "unknown", reason: "This adapter has not verified this standard shape." };
|
|
@@ -223,139 +372,92 @@ var _provider = defineProvider({
|
|
|
223
372
|
const resolved = resolveConfig(config);
|
|
224
373
|
const client = createClient(config, resolved);
|
|
225
374
|
const execution = options?.ephemeral !== void 0 ? options.ephemeral ? "exec" : "persistent" : resolved.execution;
|
|
226
|
-
if (options?.snapshotId &&
|
|
375
|
+
if (options?.snapshotId && execution === "exec") {
|
|
227
376
|
throw new Error(
|
|
228
|
-
|
|
377
|
+
'Archil snapshots fork persistent VMs and cannot be restored in exec mode. Drop execution: "exec" / ephemeral: true.'
|
|
229
378
|
);
|
|
230
379
|
}
|
|
231
|
-
if (execution === "
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
+
);
|
|
247
399
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
resolved,
|
|
255
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
256
|
-
},
|
|
257
|
-
instanceId: vm.id
|
|
258
|
-
};
|
|
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
|
+
});
|
|
259
406
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
disk: { id: diskId },
|
|
266
|
-
resolved,
|
|
267
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
268
|
-
},
|
|
269
|
-
instanceId: diskId
|
|
270
|
-
};
|
|
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 };
|
|
271
412
|
},
|
|
272
413
|
getById: async (config, sandboxId) => {
|
|
273
414
|
const resolved = resolveConfig(config);
|
|
274
415
|
const client = createClient(config, resolved);
|
|
275
416
|
try {
|
|
276
|
-
if (resolved
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
return {
|
|
280
|
-
instance: {
|
|
281
|
-
client,
|
|
282
|
-
disk: { id: vm.id },
|
|
283
|
-
vm,
|
|
284
|
-
resolved,
|
|
285
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
286
|
-
},
|
|
287
|
-
instanceId: vm.id
|
|
288
|
-
};
|
|
417
|
+
if (modeForId(sandboxId, resolved) === "exec") {
|
|
418
|
+
const disk = await client.disks.get(sandboxId);
|
|
419
|
+
return { instance: diskInstance(client, disk, resolved), instanceId: disk.id };
|
|
289
420
|
}
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
instanceId: disk.id
|
|
294
|
-
};
|
|
421
|
+
const vm = await client.sandboxes.get(sandboxId);
|
|
422
|
+
await ensureVmRunning(vm);
|
|
423
|
+
return { instance: vmInstance(client, vm, resolved), instanceId: vm.id };
|
|
295
424
|
} catch {
|
|
296
425
|
return null;
|
|
297
426
|
}
|
|
298
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.
|
|
299
430
|
list: async (config) => {
|
|
300
431
|
const resolved = resolveConfig(config);
|
|
301
432
|
const client = createClient(config, resolved);
|
|
302
|
-
if (resolved.execution === "
|
|
303
|
-
const
|
|
304
|
-
return
|
|
305
|
-
instance: {
|
|
306
|
-
client,
|
|
307
|
-
disk: { id: vm.id },
|
|
308
|
-
vm,
|
|
309
|
-
resolved,
|
|
310
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
311
|
-
},
|
|
312
|
-
instanceId: vm.id
|
|
313
|
-
}));
|
|
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 }));
|
|
314
436
|
}
|
|
315
|
-
const
|
|
316
|
-
return
|
|
317
|
-
instance: { client, disk, resolved, createdAt: /* @__PURE__ */ new Date() },
|
|
318
|
-
instanceId: disk.id
|
|
319
|
-
}));
|
|
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 }));
|
|
320
439
|
},
|
|
321
440
|
destroy: async (config, sandboxId) => {
|
|
322
441
|
const resolved = resolveConfig(config);
|
|
323
|
-
const mode =
|
|
324
|
-
if (mode === "persistent") {
|
|
325
|
-
const client = createClient(config, resolved);
|
|
326
|
-
const vm = await client.sandboxes.get(sandboxId);
|
|
327
|
-
await stopAndDeleteVm(vm);
|
|
328
|
-
}
|
|
442
|
+
const mode = modeForId(sandboxId, resolved);
|
|
329
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);
|
|
330
454
|
},
|
|
331
455
|
runCommand: async (sandbox, command, options) => {
|
|
332
456
|
const startTime = Date.now();
|
|
333
457
|
try {
|
|
334
458
|
if (sandbox.vm) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (options?.background) {
|
|
338
|
-
const proc = await sandbox.vm.processes.start(wrapped, {
|
|
339
|
-
env: options?.env
|
|
340
|
-
});
|
|
341
|
-
await proc.disconnect();
|
|
342
|
-
return {
|
|
343
|
-
stdout: "",
|
|
344
|
-
stderr: "",
|
|
345
|
-
exitCode: 0,
|
|
346
|
-
durationMs: Date.now() - startTime
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
const result2 = await sandbox.vm.exec(wrapped, {
|
|
350
|
-
env: options?.env,
|
|
351
|
-
timeoutSeconds: options?.timeout !== void 0 ? Math.ceil(options.timeout / 1e3) : void 0
|
|
352
|
-
});
|
|
353
|
-
return {
|
|
354
|
-
stdout: result2.stdout,
|
|
355
|
-
stderr: result2.stderr,
|
|
356
|
-
exitCode: result2.exitCode ?? (result2.status === "completed" ? 0 : 1),
|
|
357
|
-
durationMs: Date.now() - startTime
|
|
358
|
-
};
|
|
459
|
+
const { onStdout: _out, onStderr: _err, ...rest } = options ?? {};
|
|
460
|
+
return await runVmCommand(sandbox.vm, command, rest);
|
|
359
461
|
}
|
|
360
462
|
const result = await execOnDisk(sandbox, wrapCommand(command, options));
|
|
361
463
|
return {
|
|
@@ -365,14 +467,94 @@ var _provider = defineProvider({
|
|
|
365
467
|
durationMs: Date.now() - startTime
|
|
366
468
|
};
|
|
367
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);
|
|
368
482
|
return {
|
|
369
|
-
stdout: "",
|
|
370
|
-
stderr:
|
|
371
|
-
exitCode:
|
|
483
|
+
stdout: result.stdout ?? "",
|
|
484
|
+
stderr: result.stderr ?? "",
|
|
485
|
+
exitCode: result.exitCode,
|
|
372
486
|
durationMs: Date.now() - startTime
|
|
373
487
|
};
|
|
488
|
+
} catch (error) {
|
|
489
|
+
return commandError(error, startTime);
|
|
374
490
|
}
|
|
375
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
|
+
);
|
|
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
|
+
};
|
|
557
|
+
},
|
|
376
558
|
getInfo: async (sandbox) => {
|
|
377
559
|
if (sandbox.vm) {
|
|
378
560
|
const vm = sandbox.vm;
|
|
@@ -618,33 +800,64 @@ var _provider = defineProvider({
|
|
|
618
800
|
}
|
|
619
801
|
}
|
|
620
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 } },
|
|
621
814
|
getInstance: (sandbox) => sandbox
|
|
622
815
|
},
|
|
623
|
-
// Archil has no separate snapshot resource
|
|
624
|
-
//
|
|
625
|
-
//
|
|
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.
|
|
626
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
|
+
},
|
|
627
829
|
create: async (config, sandboxId, options) => {
|
|
628
830
|
const resolved = resolveConfig(config);
|
|
629
831
|
const client = createClient(config, resolved);
|
|
832
|
+
let fork;
|
|
630
833
|
try {
|
|
631
|
-
const
|
|
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();
|
|
632
837
|
return {
|
|
633
838
|
id: fork.id,
|
|
634
839
|
provider: "archil",
|
|
635
840
|
createdAt: fork.createdAt ?? /* @__PURE__ */ new Date(),
|
|
636
|
-
metadata: { name: fork.name }
|
|
841
|
+
metadata: { name: options?.name, archilName: fork.name, sourceId: sandboxId }
|
|
637
842
|
};
|
|
638
843
|
} catch (error) {
|
|
844
|
+
if (fork) await stopAndDeleteVm(fork).catch(() => {
|
|
845
|
+
});
|
|
639
846
|
throw new Error(
|
|
640
847
|
`Failed to create Archil snapshot: ${error instanceof Error ? error.message : String(error)}`
|
|
641
848
|
);
|
|
642
849
|
}
|
|
643
850
|
},
|
|
644
|
-
list: async (
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
);
|
|
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
|
+
}));
|
|
648
861
|
},
|
|
649
862
|
delete: async (config, snapshotId) => {
|
|
650
863
|
const resolved = resolveConfig(config);
|