@sandbank.dev/boxlite 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,YAAY,EAGZ,UAAU,EACV,cAAc,EACd,WAAW,EAIZ,MAAM,oBAAoB,CAAA;AAI3B,OAAO,KAAK,EAAE,oBAAoB,EAA6B,MAAM,YAAY,CAAA;AAoKjF,qBAAa,cAAe,YAAW,cAAc;IACnD,QAAQ,CAAC,IAAI,aAAY;IACzB,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,UAAU,CAAC,CAAA;IAE9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAQ;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyC;gBAEtD,MAAM,EAAE,oBAAoB;IAalC,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC;IAgD5D,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAW/C,aAAa,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IA0B1D,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU/C,8EAA8E;IACxE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAG/B"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,YAAY,EAGZ,UAAU,EACV,cAAc,EACd,WAAW,EAIZ,MAAM,oBAAoB,CAAA;AAI3B,OAAO,KAAK,EAAE,oBAAoB,EAA6B,MAAM,YAAY,CAAA;AAoKjF,qBAAa,cAAe,YAAW,cAAc;IACnD,QAAQ,CAAC,IAAI,aAAY;IACzB,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,UAAU,CAAC,CAAA;IAE9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAQ;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyC;gBAEtD,MAAM,EAAE,oBAAoB;IASlC,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC;IAiD5D,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAW/C,aAAa,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IA0B1D,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU/C,8EAA8E;IACxE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAG/B"}
package/dist/adapter.js CHANGED
@@ -153,19 +153,16 @@ export class BoxLiteAdapter {
153
153
  this.config = config;
154
154
  this.host = resolveHost(config);
155
155
  this.client = createClient(config);
156
- // Local mode: snapshots not supported yet
157
- const caps = ['exec.stream', 'terminal', 'sleep', 'port.expose'];
158
- if (config.mode !== 'local') {
159
- caps.push('snapshot');
160
- }
156
+ const caps = ['exec.stream', 'terminal', 'sleep', 'port.expose', 'snapshot'];
161
157
  this.capabilities = new Set(caps);
162
158
  }
163
159
  async createSandbox(config) {
164
160
  try {
165
161
  // If image looks like an absolute path, treat it as a local OCI rootfs
166
- const isLocalPath = config.image.startsWith('/');
162
+ const image = config.image ?? 'ubuntu:24.04';
163
+ const isLocalPath = image.startsWith('/');
167
164
  const box = await this.client.createBox({
168
- ...(isLocalPath ? { rootfs_path: config.image } : { image: config.image }),
165
+ ...(isLocalPath ? { rootfs_path: image } : { image }),
169
166
  cpu: config.resources?.cpu,
170
167
  memory_mb: config.resources?.memory,
171
168
  disk_size_gb: config.resources?.disk,
@@ -1 +1 @@
1
- {"version":3,"file":"local-client.d.ts","sourceRoot":"","sources":["../src/local-client.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,aAAa,EAGb,kBAAkB,EAEnB,MAAM,YAAY,CAAA;AA6XnB;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,GAAG,aAAa,CAsRlF"}
1
+ {"version":3,"file":"local-client.d.ts","sourceRoot":"","sources":["../src/local-client.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,aAAa,EAGb,kBAAkB,EAEnB,MAAM,YAAY,CAAA;AAmfnB;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,GAAG,aAAa,CAsRlF"}
@@ -86,6 +86,9 @@ class Bridge:
86
86
  if ports is not None:
87
87
  sb_kwargs["ports"] = ports
88
88
 
89
+ # Disable auto_remove so runtime.get() works after stop (needed for snapshot restore)
90
+ sb_kwargs["auto_remove"] = params.get("auto_remove", False)
91
+
89
92
  # Pass the Boxlite runtime to SimpleBox so it uses the correct home_dir
90
93
  if getattr(self, "_boxlite_rt", None) is not None:
91
94
  sb_kwargs["runtime"] = self._boxlite_rt
@@ -298,6 +301,111 @@ class Bridge:
298
301
  if box and hasattr(box, "start"):
299
302
  await box.start()
300
303
 
304
+ def _get_box(self, box_id):
305
+ box = self._boxes.get(box_id)
306
+ if box is None:
307
+ raise ValueError(f"Box not found: {box_id}")
308
+ # SimpleBox wraps the real Box — unwrap to get snapshot handle
309
+ inner = getattr(box, "_box", box)
310
+ return inner
311
+
312
+ async def create_snapshot(self, box_id, name):
313
+ # boxlite snapshot uses fork_qcow2 (rename + COW child). If QEMU is
314
+ # running, its FD still points to the renamed inode, so post-snapshot
315
+ # writes corrupt the snapshot file. Must stop → snapshot → restart.
316
+ inner = self._get_box(box_id)
317
+ await inner.stop()
318
+
319
+ rt = getattr(self, "_boxlite_rt", None)
320
+ if rt is None:
321
+ raise RuntimeError("Cannot create snapshot: no Boxlite runtime")
322
+ fresh = await rt.get(box_id)
323
+ if fresh is None:
324
+ raise RuntimeError(f"Cannot get fresh handle for box {box_id}")
325
+ snap_handle = getattr(fresh, "snapshot", None)
326
+ if snap_handle is None:
327
+ raise RuntimeError("Fresh handle has no snapshot support")
328
+
329
+ info = await snap_handle.create(name=name)
330
+ snap_name = str(getattr(info, "name", name))
331
+
332
+ # Restart the VM with the new COW child disk
333
+ await fresh.__aenter__()
334
+ # Update SimpleBox/Box internal references
335
+ old_sb = self._simple_boxes.get(box_id)
336
+ old_box = self._boxes.get(box_id)
337
+ if old_sb and hasattr(old_sb, "_box"):
338
+ old_sb._box = fresh
339
+ old_sb._started = True
340
+ if old_box and hasattr(old_box, "_box"):
341
+ old_box._box = fresh
342
+ else:
343
+ self._boxes[box_id] = fresh
344
+
345
+ return {
346
+ "id": str(getattr(info, "id", snap_name)),
347
+ "box_id": box_id,
348
+ "name": snap_name,
349
+ "created_at": int(getattr(info, "created_at", 0)),
350
+ "size_bytes": int(getattr(info, "size_bytes", 0)),
351
+ "guest_disk_bytes": int(getattr(info, "guest_disk_bytes", 0) or 0),
352
+ "container_disk_bytes": int(getattr(info, "container_disk_bytes", 0) or 0),
353
+ }
354
+
355
+ async def restore_snapshot(self, box_id, name):
356
+ # Same stop → fresh handle pattern as create_snapshot.
357
+ # stop() also invalidates the LiteBox handle (cancels shutdown_token).
358
+ inner = self._get_box(box_id)
359
+ await inner.stop()
360
+
361
+ rt = getattr(self, "_boxlite_rt", None)
362
+ if rt is None:
363
+ raise RuntimeError("Cannot restore snapshot: no Boxlite runtime")
364
+ fresh = await rt.get(box_id)
365
+ if fresh is None:
366
+ raise RuntimeError(f"Cannot get fresh handle for box {box_id}")
367
+ fresh_snap = getattr(fresh, "snapshot", None)
368
+ if fresh_snap is None:
369
+ raise RuntimeError("Fresh handle has no snapshot support")
370
+
371
+ await fresh_snap.restore(name)
372
+
373
+ # Restart with the restored disk
374
+ await fresh.__aenter__()
375
+ # Update SimpleBox/Box internal references
376
+ old_sb = self._simple_boxes.get(box_id)
377
+ old_box = self._boxes.get(box_id)
378
+ if old_sb and hasattr(old_sb, "_box"):
379
+ old_sb._box = fresh
380
+ old_sb._started = True
381
+ if old_box and hasattr(old_box, "_box"):
382
+ old_box._box = fresh
383
+ else:
384
+ self._boxes[box_id] = fresh
385
+
386
+ async def list_snapshots(self, box_id):
387
+ inner = self._get_box(box_id)
388
+ snap_handle = getattr(inner, "snapshot", None)
389
+ if snap_handle is None:
390
+ raise RuntimeError("Box does not support snapshots")
391
+ snapshots = await snap_handle.list()
392
+ return [{
393
+ "id": str(getattr(s, "id", "")),
394
+ "box_id": box_id,
395
+ "name": str(getattr(s, "name", "")),
396
+ "created_at": int(getattr(s, "created_at", 0)),
397
+ "size_bytes": int(getattr(s, "size_bytes", 0)),
398
+ "guest_disk_bytes": int(getattr(s, "guest_disk_bytes", 0) or 0),
399
+ "container_disk_bytes": int(getattr(s, "container_disk_bytes", 0) or 0),
400
+ } for s in snapshots]
401
+
402
+ async def delete_snapshot(self, box_id, name):
403
+ inner = self._get_box(box_id)
404
+ snap_handle = getattr(inner, "snapshot", None)
405
+ if snap_handle is None:
406
+ raise RuntimeError("Box does not support snapshots")
407
+ await snap_handle.remove(name)
408
+
301
409
  async def cleanup(self):
302
410
  for box_id in list(self._boxes.keys()):
303
411
  try:
@@ -349,6 +457,16 @@ async def main():
349
457
  elif action == "stop":
350
458
  await bridge.stop(cmd["box_id"])
351
459
  result = {}
460
+ elif action == "create_snapshot":
461
+ result = await bridge.create_snapshot(cmd["box_id"], cmd["name"])
462
+ elif action == "restore_snapshot":
463
+ await bridge.restore_snapshot(cmd["box_id"], cmd["name"])
464
+ result = {}
465
+ elif action == "list_snapshots":
466
+ result = await bridge.list_snapshots(cmd["box_id"])
467
+ elif action == "delete_snapshot":
468
+ await bridge.delete_snapshot(cmd["box_id"], cmd["name"])
469
+ result = {}
352
470
  elif action == "ping":
353
471
  result = {"pong": True}
354
472
  else:
@@ -580,16 +698,16 @@ export function createBoxLiteLocalClient(config) {
580
698
  });
581
699
  },
582
700
  async createSnapshot(boxId, name) {
583
- throw new Error('Snapshots are not yet supported in local mode');
701
+ return send({ action: 'create_snapshot', box_id: boxId, name });
584
702
  },
585
703
  async restoreSnapshot(boxId, name) {
586
- throw new Error('Snapshots are not yet supported in local mode');
704
+ await send({ action: 'restore_snapshot', box_id: boxId, name });
587
705
  },
588
706
  async listSnapshots(boxId) {
589
- throw new Error('Snapshots are not yet supported in local mode');
707
+ return send({ action: 'list_snapshots', box_id: boxId });
590
708
  },
591
709
  async deleteSnapshot(boxId, name) {
592
- throw new Error('Snapshots are not yet supported in local mode');
710
+ await send({ action: 'delete_snapshot', box_id: boxId, name });
593
711
  },
594
712
  async dispose() {
595
713
  if (process?.stdin?.writable) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sandbank.dev/boxlite",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "BoxLite bare-metal sandbox adapter for Sandbank",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -32,7 +32,7 @@
32
32
  "clean": "rm -rf dist"
33
33
  },
34
34
  "dependencies": {
35
- "@sandbank.dev/core": "^0.3.0"
35
+ "@sandbank.dev/core": "^0.3.4"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^25.3.0",