@sandbank.dev/boxlite 0.2.0 → 0.3.0
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 +111 -0
- package/dist/adapter.d.ts +3 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +60 -61
- package/dist/client.d.ts +6 -22
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +61 -113
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/local-client.d.ts +7 -0
- package/dist/local-client.d.ts.map +1 -0
- package/dist/local-client.js +509 -0
- package/dist/types.d.ts +60 -13
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/package.json +12 -3
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7C,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7C,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,EAClB,aAAa,GACd,MAAM,YAAY,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
// @sandbank.dev/boxlite — BoxLite
|
|
1
|
+
// @sandbank.dev/boxlite — BoxLite sandbox adapter (remote REST API + local Python SDK)
|
|
2
2
|
export { BoxLiteAdapter } from './adapter.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BoxLiteClient, BoxLiteLocalConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a BoxLite local client that communicates with the boxlite Python SDK
|
|
4
|
+
* via a JSON-line subprocess bridge.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createBoxLiteLocalClient(config: BoxLiteLocalConfig): BoxLiteClient;
|
|
7
|
+
//# sourceMappingURL=local-client.d.ts.map
|
|
@@ -0,0 +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;AAwRnB;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,GAAG,aAAa,CAgRlF"}
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { writeFileSync, unlinkSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
// ─── Python bridge script (embedded) ──────────────────────────────────────────
|
|
7
|
+
const BRIDGE_SCRIPT = `#!/usr/bin/env python3
|
|
8
|
+
"""boxlite_bridge.py — JSON-line bridge between TypeScript and boxlite Python SDK.
|
|
9
|
+
|
|
10
|
+
Protocol:
|
|
11
|
+
→ stdin: one JSON object per line {id, action, ...params}
|
|
12
|
+
← stdout: one JSON object per line {id, result} | {id, error}
|
|
13
|
+
First output line: {ready: true, version: "..."}
|
|
14
|
+
"""
|
|
15
|
+
import asyncio, json, sys, os, traceback
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import boxlite
|
|
20
|
+
except ImportError:
|
|
21
|
+
sys.stdout.write(json.dumps({
|
|
22
|
+
"ready": False,
|
|
23
|
+
"error": "boxlite Python package not found. Install with: pip install boxlite"
|
|
24
|
+
}) + "\\n")
|
|
25
|
+
sys.stdout.flush()
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Bridge:
|
|
30
|
+
def __init__(self, home=None):
|
|
31
|
+
self._home = home or os.environ.get("BOXLITE_HOME", os.path.expanduser("~/.boxlite"))
|
|
32
|
+
self._runtime = None
|
|
33
|
+
self._boxes = {} # box_id -> box object
|
|
34
|
+
self._simple_boxes = {} # box_id -> SimpleBox (for cleanup)
|
|
35
|
+
|
|
36
|
+
async def _ensure_runtime(self):
|
|
37
|
+
if self._runtime is not None:
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
# Try BoxliteRuntime (full API)
|
|
41
|
+
for attr in ("BoxliteRuntime", "Runtime", "runtime"):
|
|
42
|
+
cls = getattr(boxlite, attr, None)
|
|
43
|
+
if cls is None:
|
|
44
|
+
continue
|
|
45
|
+
try:
|
|
46
|
+
rt = cls(home=self._home) if callable(cls) else cls
|
|
47
|
+
if asyncio.iscoroutinefunction(getattr(rt, "start", None)):
|
|
48
|
+
await rt.start()
|
|
49
|
+
self._runtime = rt
|
|
50
|
+
return
|
|
51
|
+
except Exception:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
# Fallback: no runtime, use SimpleBox per box
|
|
55
|
+
self._runtime = "simple_box"
|
|
56
|
+
|
|
57
|
+
async def create(self, params):
|
|
58
|
+
await self._ensure_runtime()
|
|
59
|
+
|
|
60
|
+
image = params["image"]
|
|
61
|
+
kwargs = {"image": image}
|
|
62
|
+
for k in ("cpu", "memory_mb", "disk_size_gb", "env", "working_dir"):
|
|
63
|
+
if params.get(k) is not None:
|
|
64
|
+
kwargs[k] = params[k]
|
|
65
|
+
|
|
66
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
67
|
+
|
|
68
|
+
if self._runtime == "simple_box":
|
|
69
|
+
sb = boxlite.SimpleBox(**kwargs)
|
|
70
|
+
box = await sb.__aenter__()
|
|
71
|
+
box_id = str(getattr(box, "id", None)
|
|
72
|
+
or getattr(getattr(box, "_box", None), "id", None)
|
|
73
|
+
or id(box))
|
|
74
|
+
self._boxes[box_id] = box
|
|
75
|
+
self._simple_boxes[box_id] = sb
|
|
76
|
+
else:
|
|
77
|
+
# Try runtime.create / runtime.create_box
|
|
78
|
+
create_fn = getattr(self._runtime, "create", None) or getattr(self._runtime, "create_box", None)
|
|
79
|
+
if create_fn is None:
|
|
80
|
+
raise RuntimeError("boxlite runtime has no create/create_box method")
|
|
81
|
+
box = await create_fn(**kwargs)
|
|
82
|
+
box_id = str(box.id)
|
|
83
|
+
self._boxes[box_id] = box
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
"id": box_id,
|
|
87
|
+
"status": "running",
|
|
88
|
+
"image": image,
|
|
89
|
+
"cpu": kwargs.get("cpu", 1),
|
|
90
|
+
"memory_mb": kwargs.get("memory_mb", 512),
|
|
91
|
+
"created_at": now,
|
|
92
|
+
"name": None,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async def get(self, box_id):
|
|
96
|
+
box = self._boxes.get(box_id)
|
|
97
|
+
if box is None:
|
|
98
|
+
raise ValueError(f"Box not found: {box_id}")
|
|
99
|
+
return {
|
|
100
|
+
"id": box_id,
|
|
101
|
+
"status": str(getattr(box, "status", "running")),
|
|
102
|
+
"image": getattr(box, "image", "unknown"),
|
|
103
|
+
"cpu": getattr(box, "cpu", 1),
|
|
104
|
+
"memory_mb": getattr(box, "memory_mb", 512),
|
|
105
|
+
"created_at": str(getattr(box, "created_at", "")),
|
|
106
|
+
"name": getattr(box, "name", None),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async def list_boxes(self):
|
|
110
|
+
results = []
|
|
111
|
+
for box_id, box in self._boxes.items():
|
|
112
|
+
results.append({
|
|
113
|
+
"id": box_id,
|
|
114
|
+
"status": str(getattr(box, "status", "running")),
|
|
115
|
+
"image": getattr(box, "image", "unknown"),
|
|
116
|
+
"cpu": getattr(box, "cpu", 1),
|
|
117
|
+
"memory_mb": getattr(box, "memory_mb", 512),
|
|
118
|
+
"created_at": str(getattr(box, "created_at", "")),
|
|
119
|
+
"name": getattr(box, "name", None),
|
|
120
|
+
})
|
|
121
|
+
return results
|
|
122
|
+
|
|
123
|
+
async def exec_cmd(self, box_id, cmd, **kwargs):
|
|
124
|
+
box = self._boxes.get(box_id)
|
|
125
|
+
if box is None:
|
|
126
|
+
raise ValueError(f"Box not found: {box_id}")
|
|
127
|
+
|
|
128
|
+
result = None
|
|
129
|
+
errors = []
|
|
130
|
+
|
|
131
|
+
# Strategy 1: box.exec(*cmd)
|
|
132
|
+
try:
|
|
133
|
+
result = await box.exec(*cmd)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
errors.append(f"box.exec(*cmd): {e}")
|
|
136
|
+
|
|
137
|
+
# Strategy 2: box.exec(cmd[0], args=cmd[1:])
|
|
138
|
+
if result is None:
|
|
139
|
+
try:
|
|
140
|
+
result = await box.exec(cmd[0], args=cmd[1:])
|
|
141
|
+
except Exception as e:
|
|
142
|
+
errors.append(f"box.exec(cmd[0], args=...): {e}")
|
|
143
|
+
|
|
144
|
+
# Strategy 3: box._box.exec(cmd[0], args=cmd[1:])
|
|
145
|
+
if result is None and hasattr(box, "_box"):
|
|
146
|
+
try:
|
|
147
|
+
exec_obj = await box._box.exec(cmd[0], args=cmd[1:])
|
|
148
|
+
if hasattr(exec_obj, "wait"):
|
|
149
|
+
result = await exec_obj.wait()
|
|
150
|
+
else:
|
|
151
|
+
result = exec_obj
|
|
152
|
+
except Exception as e:
|
|
153
|
+
errors.append(f"box._box.exec(...): {e}")
|
|
154
|
+
|
|
155
|
+
if result is None:
|
|
156
|
+
raise RuntimeError(f"All exec strategies failed: {'; '.join(errors)}")
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
"stdout": str(getattr(result, "stdout", "") or ""),
|
|
160
|
+
"stderr": str(getattr(result, "stderr", "") or ""),
|
|
161
|
+
"exit_code": int(getattr(result, "exit_code",
|
|
162
|
+
getattr(result, "returncode", 0)) or 0),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async def destroy(self, box_id):
|
|
166
|
+
box = self._boxes.pop(box_id, None)
|
|
167
|
+
sb = self._simple_boxes.pop(box_id, None)
|
|
168
|
+
|
|
169
|
+
if sb is not None:
|
|
170
|
+
try:
|
|
171
|
+
await sb.__aexit__(None, None, None)
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
if box is not None and self._runtime != "simple_box":
|
|
177
|
+
for method_name in ("destroy", "delete", "remove"):
|
|
178
|
+
fn = getattr(self._runtime, method_name, None)
|
|
179
|
+
if fn is not None:
|
|
180
|
+
try:
|
|
181
|
+
await fn(box_id)
|
|
182
|
+
return
|
|
183
|
+
except Exception:
|
|
184
|
+
continue
|
|
185
|
+
if hasattr(box, "destroy"):
|
|
186
|
+
await box.destroy()
|
|
187
|
+
elif hasattr(box, "stop"):
|
|
188
|
+
await box.stop()
|
|
189
|
+
|
|
190
|
+
async def stop(self, box_id):
|
|
191
|
+
box = self._boxes.get(box_id)
|
|
192
|
+
if box and hasattr(box, "stop"):
|
|
193
|
+
await box.stop()
|
|
194
|
+
|
|
195
|
+
async def start(self, box_id):
|
|
196
|
+
box = self._boxes.get(box_id)
|
|
197
|
+
if box and hasattr(box, "start"):
|
|
198
|
+
await box.start()
|
|
199
|
+
|
|
200
|
+
async def cleanup(self):
|
|
201
|
+
for box_id in list(self._boxes.keys()):
|
|
202
|
+
try:
|
|
203
|
+
await self.destroy(box_id)
|
|
204
|
+
except Exception:
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def write_json(obj):
|
|
209
|
+
sys.stdout.write(json.dumps(obj) + "\\n")
|
|
210
|
+
sys.stdout.flush()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
async def main():
|
|
214
|
+
home = os.environ.get("BOXLITE_BRIDGE_HOME")
|
|
215
|
+
bridge = Bridge(home=home)
|
|
216
|
+
loop = asyncio.get_running_loop()
|
|
217
|
+
|
|
218
|
+
write_json({"ready": True, "version": getattr(boxlite, "__version__", "unknown")})
|
|
219
|
+
|
|
220
|
+
while True:
|
|
221
|
+
line = await loop.run_in_executor(None, sys.stdin.readline)
|
|
222
|
+
if not line:
|
|
223
|
+
break
|
|
224
|
+
line = line.strip()
|
|
225
|
+
if not line:
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
req_id = 0
|
|
229
|
+
try:
|
|
230
|
+
cmd = json.loads(line)
|
|
231
|
+
req_id = cmd.get("id", 0)
|
|
232
|
+
action = cmd.get("action", "")
|
|
233
|
+
|
|
234
|
+
if action == "create":
|
|
235
|
+
result = await bridge.create(cmd)
|
|
236
|
+
elif action == "get":
|
|
237
|
+
result = await bridge.get(cmd["box_id"])
|
|
238
|
+
elif action == "list":
|
|
239
|
+
result = await bridge.list_boxes()
|
|
240
|
+
elif action == "exec":
|
|
241
|
+
result = await bridge.exec_cmd(cmd["box_id"], cmd["cmd"])
|
|
242
|
+
elif action == "destroy":
|
|
243
|
+
await bridge.destroy(cmd["box_id"])
|
|
244
|
+
result = {}
|
|
245
|
+
elif action == "start":
|
|
246
|
+
await bridge.start(cmd["box_id"])
|
|
247
|
+
result = {}
|
|
248
|
+
elif action == "stop":
|
|
249
|
+
await bridge.stop(cmd["box_id"])
|
|
250
|
+
result = {}
|
|
251
|
+
elif action == "ping":
|
|
252
|
+
result = {"pong": True}
|
|
253
|
+
else:
|
|
254
|
+
raise ValueError(f"Unknown action: {action}")
|
|
255
|
+
|
|
256
|
+
write_json({"id": req_id, "result": result})
|
|
257
|
+
except Exception as e:
|
|
258
|
+
write_json({"id": req_id, "error": f"{type(e).__name__}: {e}"})
|
|
259
|
+
|
|
260
|
+
await bridge.cleanup()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
if __name__ == "__main__":
|
|
264
|
+
asyncio.run(main())
|
|
265
|
+
`;
|
|
266
|
+
/**
|
|
267
|
+
* Create a BoxLite local client that communicates with the boxlite Python SDK
|
|
268
|
+
* via a JSON-line subprocess bridge.
|
|
269
|
+
*/
|
|
270
|
+
export function createBoxLiteLocalClient(config) {
|
|
271
|
+
const pythonPath = config.pythonPath ?? 'python3';
|
|
272
|
+
const boxliteHome = config.boxliteHome;
|
|
273
|
+
let process = null;
|
|
274
|
+
let readline = null;
|
|
275
|
+
let requestId = 0;
|
|
276
|
+
let readyPromise = null;
|
|
277
|
+
const pending = new Map();
|
|
278
|
+
// Write bridge script to a temp file
|
|
279
|
+
let bridgeScriptPath = null;
|
|
280
|
+
function getBridgeScriptPath() {
|
|
281
|
+
if (bridgeScriptPath)
|
|
282
|
+
return bridgeScriptPath;
|
|
283
|
+
bridgeScriptPath = join(tmpdir(), `boxlite-bridge-${process?.pid ?? Date.now()}.py`);
|
|
284
|
+
writeFileSync(bridgeScriptPath, BRIDGE_SCRIPT, 'utf-8');
|
|
285
|
+
return bridgeScriptPath;
|
|
286
|
+
}
|
|
287
|
+
function ensureBridge() {
|
|
288
|
+
if (readyPromise)
|
|
289
|
+
return readyPromise;
|
|
290
|
+
readyPromise = new Promise((resolveReady, rejectReady) => {
|
|
291
|
+
const scriptPath = getBridgeScriptPath();
|
|
292
|
+
const env = { ...globalThis.process.env };
|
|
293
|
+
if (boxliteHome) {
|
|
294
|
+
env['BOXLITE_BRIDGE_HOME'] = boxliteHome;
|
|
295
|
+
}
|
|
296
|
+
process = spawn(pythonPath, [scriptPath], {
|
|
297
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
298
|
+
env,
|
|
299
|
+
});
|
|
300
|
+
// Collect stderr for error reporting
|
|
301
|
+
let stderrBuf = '';
|
|
302
|
+
process.stderr?.on('data', (chunk) => {
|
|
303
|
+
stderrBuf += chunk.toString();
|
|
304
|
+
});
|
|
305
|
+
process.on('error', (err) => {
|
|
306
|
+
rejectReady(new Error(`Failed to start boxlite bridge: ${err.message}`));
|
|
307
|
+
cleanup();
|
|
308
|
+
});
|
|
309
|
+
process.on('exit', (code) => {
|
|
310
|
+
if (code !== 0 && code !== null) {
|
|
311
|
+
const msg = stderrBuf || `Bridge exited with code ${code}`;
|
|
312
|
+
rejectReady(new Error(`BoxLite bridge error: ${msg}`));
|
|
313
|
+
// Reject all pending requests
|
|
314
|
+
for (const [id, req] of pending) {
|
|
315
|
+
req.reject(new Error(`BoxLite bridge exited unexpectedly: ${msg}`));
|
|
316
|
+
clearTimeout(req.timer);
|
|
317
|
+
pending.delete(id);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
cleanup();
|
|
321
|
+
});
|
|
322
|
+
readline = createInterface({ input: process.stdout });
|
|
323
|
+
let gotReady = false;
|
|
324
|
+
readline.on('line', (line) => {
|
|
325
|
+
let msg;
|
|
326
|
+
try {
|
|
327
|
+
msg = JSON.parse(line);
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return; // Ignore non-JSON output
|
|
331
|
+
}
|
|
332
|
+
// Handle ready signal
|
|
333
|
+
if (!gotReady && 'ready' in msg) {
|
|
334
|
+
gotReady = true;
|
|
335
|
+
if (msg.ready) {
|
|
336
|
+
resolveReady();
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
rejectReady(new Error(`BoxLite bridge init failed: ${msg.error ?? 'unknown error'}`));
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
// Handle response to a request
|
|
344
|
+
const id = msg.id;
|
|
345
|
+
if (id === undefined)
|
|
346
|
+
return;
|
|
347
|
+
const req = pending.get(id);
|
|
348
|
+
if (!req)
|
|
349
|
+
return;
|
|
350
|
+
pending.delete(id);
|
|
351
|
+
clearTimeout(req.timer);
|
|
352
|
+
if (msg.error) {
|
|
353
|
+
req.reject(new Error(`BoxLite local: ${msg.error}`));
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
req.resolve(msg.result);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
return readyPromise;
|
|
361
|
+
}
|
|
362
|
+
function cleanup() {
|
|
363
|
+
if (bridgeScriptPath) {
|
|
364
|
+
try {
|
|
365
|
+
unlinkSync(bridgeScriptPath);
|
|
366
|
+
}
|
|
367
|
+
catch { /* ignore */ }
|
|
368
|
+
bridgeScriptPath = null;
|
|
369
|
+
}
|
|
370
|
+
readline?.close();
|
|
371
|
+
readline = null;
|
|
372
|
+
process = null;
|
|
373
|
+
readyPromise = null;
|
|
374
|
+
}
|
|
375
|
+
async function send(command, timeoutMs = 300_000) {
|
|
376
|
+
await ensureBridge();
|
|
377
|
+
if (!process?.stdin?.writable) {
|
|
378
|
+
throw new Error('BoxLite bridge is not running');
|
|
379
|
+
}
|
|
380
|
+
const id = ++requestId;
|
|
381
|
+
return new Promise((resolve, reject) => {
|
|
382
|
+
const timer = setTimeout(() => {
|
|
383
|
+
pending.delete(id);
|
|
384
|
+
reject(new Error(`BoxLite bridge request timed out after ${timeoutMs}ms`));
|
|
385
|
+
}, timeoutMs);
|
|
386
|
+
pending.set(id, {
|
|
387
|
+
resolve: resolve,
|
|
388
|
+
reject,
|
|
389
|
+
timer,
|
|
390
|
+
});
|
|
391
|
+
process.stdin.write(JSON.stringify({ id, ...command }) + '\n');
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
// ─── BoxLiteClient implementation ───
|
|
395
|
+
return {
|
|
396
|
+
async createBox(params) {
|
|
397
|
+
return send({ action: 'create', ...params });
|
|
398
|
+
},
|
|
399
|
+
async getBox(boxId) {
|
|
400
|
+
return send({ action: 'get', box_id: boxId });
|
|
401
|
+
},
|
|
402
|
+
async listBoxes() {
|
|
403
|
+
return send({ action: 'list' });
|
|
404
|
+
},
|
|
405
|
+
async deleteBox(boxId) {
|
|
406
|
+
await send({ action: 'destroy', box_id: boxId });
|
|
407
|
+
},
|
|
408
|
+
async startBox(boxId) {
|
|
409
|
+
await send({ action: 'start', box_id: boxId });
|
|
410
|
+
},
|
|
411
|
+
async stopBox(boxId) {
|
|
412
|
+
await send({ action: 'stop', box_id: boxId });
|
|
413
|
+
},
|
|
414
|
+
async exec(boxId, req) {
|
|
415
|
+
const timeoutMs = (req.timeout_seconds ?? 300) * 1000;
|
|
416
|
+
const result = await send({ action: 'exec', box_id: boxId, cmd: req.cmd }, timeoutMs);
|
|
417
|
+
return {
|
|
418
|
+
stdout: result.stdout ?? '',
|
|
419
|
+
stderr: result.stderr ?? '',
|
|
420
|
+
exitCode: result.exit_code ?? 0,
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
async execStream(boxId, req) {
|
|
424
|
+
const result = await this.exec(boxId, req);
|
|
425
|
+
const encoder = new TextEncoder();
|
|
426
|
+
return new ReadableStream({
|
|
427
|
+
start(controller) {
|
|
428
|
+
if (result.stdout)
|
|
429
|
+
controller.enqueue(encoder.encode(result.stdout));
|
|
430
|
+
if (result.stderr)
|
|
431
|
+
controller.enqueue(encoder.encode(result.stderr));
|
|
432
|
+
controller.close();
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
},
|
|
436
|
+
async uploadFiles(boxId, path, tarData) {
|
|
437
|
+
// Pipe tar data through exec: base64 decode → tar extract
|
|
438
|
+
const b64 = Buffer.from(tarData).toString('base64');
|
|
439
|
+
// Split into chunks to avoid shell argument limit
|
|
440
|
+
const chunkSize = 50_000;
|
|
441
|
+
const chunks = [];
|
|
442
|
+
for (let i = 0; i < b64.length; i += chunkSize) {
|
|
443
|
+
chunks.push(b64.slice(i, i + chunkSize));
|
|
444
|
+
}
|
|
445
|
+
if (chunks.length === 1) {
|
|
446
|
+
await this.exec(boxId, {
|
|
447
|
+
cmd: ['bash', '-c', `echo '${chunks[0]}' | base64 -d | tar xf - -C '${path}'`],
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
// Write base64 to a temp file in chunks, then decode
|
|
452
|
+
const tmpFile = `/tmp/.boxlite-upload-${Date.now()}`;
|
|
453
|
+
for (const chunk of chunks) {
|
|
454
|
+
await this.exec(boxId, {
|
|
455
|
+
cmd: ['bash', '-c', `printf '%s' '${chunk}' >> ${tmpFile}`],
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
await this.exec(boxId, {
|
|
459
|
+
cmd: ['bash', '-c', `base64 -d ${tmpFile} | tar xf - -C '${path}' && rm -f ${tmpFile}`],
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
async downloadFiles(boxId, path) {
|
|
464
|
+
const result = await this.exec(boxId, {
|
|
465
|
+
cmd: ['bash', '-c', `tar cf - -C '${path}' . 2>/dev/null | base64`],
|
|
466
|
+
});
|
|
467
|
+
const data = Buffer.from(result.stdout.trim(), 'base64');
|
|
468
|
+
return new ReadableStream({
|
|
469
|
+
start(controller) {
|
|
470
|
+
controller.enqueue(new Uint8Array(data));
|
|
471
|
+
controller.close();
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
},
|
|
475
|
+
async createSnapshot(boxId, name) {
|
|
476
|
+
throw new Error('Snapshots are not yet supported in local mode');
|
|
477
|
+
},
|
|
478
|
+
async restoreSnapshot(boxId, name) {
|
|
479
|
+
throw new Error('Snapshots are not yet supported in local mode');
|
|
480
|
+
},
|
|
481
|
+
async listSnapshots(boxId) {
|
|
482
|
+
throw new Error('Snapshots are not yet supported in local mode');
|
|
483
|
+
},
|
|
484
|
+
async deleteSnapshot(boxId, name) {
|
|
485
|
+
throw new Error('Snapshots are not yet supported in local mode');
|
|
486
|
+
},
|
|
487
|
+
async dispose() {
|
|
488
|
+
if (process?.stdin?.writable) {
|
|
489
|
+
process.stdin.end();
|
|
490
|
+
}
|
|
491
|
+
// Give the bridge a moment to cleanup
|
|
492
|
+
await new Promise(resolve => {
|
|
493
|
+
if (!process) {
|
|
494
|
+
resolve();
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const timeout = setTimeout(() => {
|
|
498
|
+
process?.kill();
|
|
499
|
+
resolve();
|
|
500
|
+
}, 3000);
|
|
501
|
+
process.on('exit', () => {
|
|
502
|
+
clearTimeout(timeout);
|
|
503
|
+
resolve();
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
cleanup();
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export interface
|
|
3
|
-
|
|
1
|
+
/** Remote mode: connect to a BoxRun REST API */
|
|
2
|
+
export interface BoxLiteRemoteConfig {
|
|
3
|
+
mode?: 'remote';
|
|
4
|
+
/** BoxRun REST API base URL, e.g. 'http://localhost:8090' */
|
|
4
5
|
apiUrl: string;
|
|
5
|
-
/** Multi-tenant prefix
|
|
6
|
+
/** Multi-tenant prefix (e.g. 'default') */
|
|
6
7
|
prefix?: string;
|
|
7
8
|
/** Bearer token (if already obtained) */
|
|
8
9
|
apiToken?: string;
|
|
@@ -11,27 +12,73 @@ export interface BoxLiteAdapterConfig {
|
|
|
11
12
|
/** OAuth2 client secret (for automatic token acquisition) */
|
|
12
13
|
clientSecret?: string;
|
|
13
14
|
}
|
|
15
|
+
/** Local mode: use boxlite Python SDK directly on this machine */
|
|
16
|
+
export interface BoxLiteLocalConfig {
|
|
17
|
+
mode: 'local';
|
|
18
|
+
/** Path to Python 3.10+ interpreter (default: 'python3') */
|
|
19
|
+
pythonPath?: string;
|
|
20
|
+
/** BoxLite home directory (default: '~/.boxlite') */
|
|
21
|
+
boxliteHome?: string;
|
|
22
|
+
}
|
|
23
|
+
/** BoxLite adapter configuration — remote (BoxRun REST API) or local (Python SDK) */
|
|
24
|
+
export type BoxLiteAdapterConfig = BoxLiteRemoteConfig | BoxLiteLocalConfig;
|
|
25
|
+
export interface BoxLiteClient {
|
|
26
|
+
createBox(params: BoxLiteCreateParams): Promise<BoxLiteBox>;
|
|
27
|
+
getBox(boxId: string): Promise<BoxLiteBox>;
|
|
28
|
+
listBoxes(status?: string, pageSize?: number): Promise<BoxLiteBox[]>;
|
|
29
|
+
deleteBox(boxId: string, force?: boolean): Promise<void>;
|
|
30
|
+
startBox(boxId: string): Promise<void>;
|
|
31
|
+
stopBox(boxId: string): Promise<void>;
|
|
32
|
+
exec(boxId: string, req: BoxLiteExecRequest): Promise<{
|
|
33
|
+
stdout: string;
|
|
34
|
+
stderr: string;
|
|
35
|
+
exitCode: number;
|
|
36
|
+
}>;
|
|
37
|
+
execStream(boxId: string, req: BoxLiteExecRequest): Promise<ReadableStream<Uint8Array>>;
|
|
38
|
+
uploadFiles(boxId: string, path: string, tarData: Uint8Array): Promise<void>;
|
|
39
|
+
downloadFiles(boxId: string, path: string): Promise<ReadableStream<Uint8Array>>;
|
|
40
|
+
createSnapshot(boxId: string, name: string): Promise<BoxLiteSnapshot>;
|
|
41
|
+
restoreSnapshot(boxId: string, name: string): Promise<void>;
|
|
42
|
+
listSnapshots(boxId: string): Promise<BoxLiteSnapshot[]>;
|
|
43
|
+
deleteSnapshot(boxId: string, name: string): Promise<void>;
|
|
44
|
+
/** Dispose of the client (cleanup subprocess, etc.) */
|
|
45
|
+
dispose?(): Promise<void>;
|
|
46
|
+
}
|
|
14
47
|
export interface BoxLiteBox {
|
|
15
|
-
|
|
48
|
+
id: string;
|
|
49
|
+
boxlite_id?: string;
|
|
16
50
|
name: string | null;
|
|
17
51
|
status: BoxStatus;
|
|
18
52
|
created_at: string;
|
|
19
|
-
|
|
53
|
+
started_at?: string | null;
|
|
54
|
+
stopped_at?: string | null;
|
|
20
55
|
image: string;
|
|
21
|
-
|
|
22
|
-
|
|
56
|
+
cpu: number;
|
|
57
|
+
memory_mb: number;
|
|
58
|
+
disk_size_gb?: number;
|
|
59
|
+
workdir?: string;
|
|
60
|
+
env?: Record<string, string> | null;
|
|
61
|
+
network?: boolean;
|
|
62
|
+
error_code?: string | null;
|
|
63
|
+
error_message?: string | null;
|
|
64
|
+
volumes?: unknown;
|
|
23
65
|
}
|
|
24
66
|
export type BoxStatus = 'configured' | 'running' | 'stopping' | 'stopped' | 'paused' | 'unknown';
|
|
25
67
|
export interface BoxLiteExecRequest {
|
|
26
|
-
|
|
27
|
-
args?: string[];
|
|
68
|
+
cmd: string[];
|
|
28
69
|
env?: Record<string, string>;
|
|
29
70
|
timeout_seconds?: number;
|
|
30
71
|
working_dir?: string;
|
|
31
72
|
tty?: boolean;
|
|
32
73
|
}
|
|
33
74
|
export interface BoxLiteExecution {
|
|
34
|
-
|
|
75
|
+
id: string;
|
|
76
|
+
box_id?: string;
|
|
77
|
+
cmd?: string[];
|
|
78
|
+
status: string;
|
|
79
|
+
exit_code: number | null;
|
|
80
|
+
stdout?: string;
|
|
81
|
+
stderr?: string;
|
|
35
82
|
}
|
|
36
83
|
export interface BoxLiteSnapshot {
|
|
37
84
|
id: string;
|
|
@@ -45,8 +92,8 @@ export interface BoxLiteSnapshot {
|
|
|
45
92
|
export interface BoxLiteCreateParams {
|
|
46
93
|
image: string;
|
|
47
94
|
name?: string;
|
|
48
|
-
|
|
49
|
-
|
|
95
|
+
cpu?: number;
|
|
96
|
+
memory_mb?: number;
|
|
50
97
|
disk_size_gb?: number;
|
|
51
98
|
working_dir?: string;
|
|
52
99
|
env?: Record<string, string>;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
CHANGED