@magpiecloud/mags 1.8.0 → 1.8.2
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/bin/mags.js +21 -3
- package/package.json +1 -1
- package/python/dist/magpie_mags-1.2.0-py3-none-any.whl +0 -0
- package/python/dist/magpie_mags-1.2.0.tar.gz +0 -0
- package/python/pyproject.toml +1 -1
- package/python/src/magpie_mags.egg-info/PKG-INFO +1 -1
- package/python/src/mags/__init__.py +1 -1
- package/python/src/mags/client.py +189 -0
- package/python/test_sdk.py +78 -0
- package/website/api.html +6 -0
- package/website/index.html +45 -33
- package/website/llms.txt +45 -0
- package/website/mags.md +33 -1
- package/python/dist/magpie_mags-1.0.0-py3-none-any.whl +0 -0
- package/python/dist/magpie_mags-1.0.0.tar.gz +0 -0
package/bin/mags.js
CHANGED
|
@@ -221,6 +221,7 @@ ${colors.bold}Run Options:${colors.reset}
|
|
|
221
221
|
--base <workspace> Mount workspace read-only as base image
|
|
222
222
|
-e, --ephemeral No workspace/S3 sync (fastest execution)
|
|
223
223
|
-f, --file <path> Upload file(s) to VM (repeatable)
|
|
224
|
+
--disk <GB> Custom disk size in GB (default: 2)
|
|
224
225
|
--url Enable public URL access (requires -p)
|
|
225
226
|
--port <port> Port to expose for URL (default: 8080)
|
|
226
227
|
--startup-command <cmd> Command to run when VM wakes from sleep
|
|
@@ -395,10 +396,13 @@ To use Mags, you need to authenticate first.
|
|
|
395
396
|
async function newVM(args) {
|
|
396
397
|
let name = null;
|
|
397
398
|
let baseWorkspace = null;
|
|
399
|
+
let diskGB = 0;
|
|
398
400
|
|
|
399
401
|
for (let i = 0; i < args.length; i++) {
|
|
400
402
|
if (args[i] === '--base' && args[i + 1]) {
|
|
401
403
|
baseWorkspace = args[++i];
|
|
404
|
+
} else if (args[i] === '--disk' && args[i + 1]) {
|
|
405
|
+
diskGB = parseInt(args[++i]) || 0;
|
|
402
406
|
} else if (!name) {
|
|
403
407
|
name = args[i];
|
|
404
408
|
}
|
|
@@ -406,7 +410,7 @@ async function newVM(args) {
|
|
|
406
410
|
|
|
407
411
|
if (!name) {
|
|
408
412
|
log('red', 'Error: Name required');
|
|
409
|
-
console.log(`\nUsage: mags new <name> [--base <workspace>]\n`);
|
|
413
|
+
console.log(`\nUsage: mags new <name> [--base <workspace>] [--disk <GB>]\n`);
|
|
410
414
|
process.exit(1);
|
|
411
415
|
}
|
|
412
416
|
|
|
@@ -419,6 +423,7 @@ async function newVM(args) {
|
|
|
419
423
|
startup_command: 'sleep infinity'
|
|
420
424
|
};
|
|
421
425
|
if (baseWorkspace) payload.base_workspace_id = baseWorkspace;
|
|
426
|
+
if (diskGB) payload.disk_gb = diskGB;
|
|
422
427
|
|
|
423
428
|
const response = await request('POST', '/api/v1/mags-jobs', payload);
|
|
424
429
|
|
|
@@ -463,6 +468,7 @@ async function runJob(args) {
|
|
|
463
468
|
let enableUrl = false;
|
|
464
469
|
let port = 8080;
|
|
465
470
|
let startupCommand = '';
|
|
471
|
+
let diskGB = 0;
|
|
466
472
|
let fileArgs = [];
|
|
467
473
|
|
|
468
474
|
// Parse flags
|
|
@@ -500,6 +506,9 @@ async function runJob(args) {
|
|
|
500
506
|
case '--port':
|
|
501
507
|
port = parseInt(args[++i]) || 8080;
|
|
502
508
|
break;
|
|
509
|
+
case '--disk':
|
|
510
|
+
diskGB = parseInt(args[++i]) || 0;
|
|
511
|
+
break;
|
|
503
512
|
case '--startup-command':
|
|
504
513
|
startupCommand = args[++i];
|
|
505
514
|
break;
|
|
@@ -562,6 +571,7 @@ async function runJob(args) {
|
|
|
562
571
|
if (baseWorkspace) payload.base_workspace_id = baseWorkspace;
|
|
563
572
|
if (startupCommand) payload.startup_command = startupCommand;
|
|
564
573
|
if (fileIds.length > 0) payload.file_ids = fileIds;
|
|
574
|
+
if (diskGB) payload.disk_gb = diskGB;
|
|
565
575
|
|
|
566
576
|
const response = await request('POST', '/api/v1/mags-jobs', payload);
|
|
567
577
|
|
|
@@ -589,6 +599,14 @@ async function runJob(args) {
|
|
|
589
599
|
log('green', `Completed in ${status.script_duration_ms}ms`);
|
|
590
600
|
break;
|
|
591
601
|
} else if (status.status === 'running' && persistent) {
|
|
602
|
+
// If --url requested, wait until VM is actually assigned (vm_id populated)
|
|
603
|
+
if (enableUrl && !status.vm_id) {
|
|
604
|
+
process.stdout.write('.');
|
|
605
|
+
await sleep(1000);
|
|
606
|
+
attempt++;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
|
|
592
610
|
log('green', 'VM running');
|
|
593
611
|
|
|
594
612
|
if (enableUrl && status.subdomain) {
|
|
@@ -597,7 +615,7 @@ async function runJob(args) {
|
|
|
597
615
|
if (accessResp.success) {
|
|
598
616
|
log('green', `URL: https://${status.subdomain}.apps.magpiecloud.com`);
|
|
599
617
|
} else {
|
|
600
|
-
log('yellow',
|
|
618
|
+
log('yellow', `Warning: Could not enable URL access${accessResp.error ? ': ' + accessResp.error : ''}`);
|
|
601
619
|
}
|
|
602
620
|
}
|
|
603
621
|
return;
|
|
@@ -1348,7 +1366,7 @@ async function main() {
|
|
|
1348
1366
|
break;
|
|
1349
1367
|
case '--version':
|
|
1350
1368
|
case '-v':
|
|
1351
|
-
console.log('mags v1.8.
|
|
1369
|
+
console.log('mags v1.8.2');
|
|
1352
1370
|
process.exit(0);
|
|
1353
1371
|
break;
|
|
1354
1372
|
case 'new':
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
package/python/pyproject.toml
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
6
8
|
import time
|
|
7
9
|
from pathlib import Path
|
|
8
10
|
from typing import Any, Dict, List, Optional
|
|
@@ -103,6 +105,7 @@ class Mags:
|
|
|
103
105
|
startup_command: str | None = None,
|
|
104
106
|
environment: Dict[str, str] | None = None,
|
|
105
107
|
file_ids: List[str] | None = None,
|
|
108
|
+
disk_gb: int | None = None,
|
|
106
109
|
) -> dict:
|
|
107
110
|
"""Submit a job for execution.
|
|
108
111
|
|
|
@@ -134,6 +137,8 @@ class Mags:
|
|
|
134
137
|
payload["environment"] = environment
|
|
135
138
|
if file_ids:
|
|
136
139
|
payload["file_ids"] = file_ids
|
|
140
|
+
if disk_gb:
|
|
141
|
+
payload["disk_gb"] = disk_gb
|
|
137
142
|
|
|
138
143
|
return self._request("POST", "/mags-jobs", json=payload)
|
|
139
144
|
|
|
@@ -198,12 +203,196 @@ class Mags:
|
|
|
198
203
|
"POST", f"/mags-jobs/{request_id}/access", json={"port": port}
|
|
199
204
|
)
|
|
200
205
|
|
|
206
|
+
def stop(self, name_or_id: str) -> dict:
|
|
207
|
+
"""Stop a running job.
|
|
208
|
+
|
|
209
|
+
Accepts a job ID, job name, or workspace ID.
|
|
210
|
+
"""
|
|
211
|
+
request_id = self._resolve_job_id(name_or_id)
|
|
212
|
+
return self._request("POST", f"/mags-jobs/{request_id}/stop")
|
|
213
|
+
|
|
214
|
+
def new(
|
|
215
|
+
self,
|
|
216
|
+
name: str,
|
|
217
|
+
*,
|
|
218
|
+
base_workspace_id: str | None = None,
|
|
219
|
+
disk_gb: int | None = None,
|
|
220
|
+
timeout: float = 30.0,
|
|
221
|
+
poll_interval: float = 1.0,
|
|
222
|
+
) -> dict:
|
|
223
|
+
"""Create a new persistent VM workspace and wait until it's running.
|
|
224
|
+
|
|
225
|
+
Equivalent to ``mags new <name>``.
|
|
226
|
+
|
|
227
|
+
Returns ``{"request_id": ..., "status": "running"}``.
|
|
228
|
+
"""
|
|
229
|
+
result = self.run(
|
|
230
|
+
"sleep infinity",
|
|
231
|
+
workspace_id=name,
|
|
232
|
+
persistent=True,
|
|
233
|
+
base_workspace_id=base_workspace_id,
|
|
234
|
+
disk_gb=disk_gb,
|
|
235
|
+
)
|
|
236
|
+
request_id = result["request_id"]
|
|
237
|
+
|
|
238
|
+
deadline = time.monotonic() + timeout
|
|
239
|
+
while time.monotonic() < deadline:
|
|
240
|
+
st = self.status(request_id)
|
|
241
|
+
if st["status"] == "running" and st.get("vm_id"):
|
|
242
|
+
return {"request_id": request_id, "status": "running"}
|
|
243
|
+
if st["status"] in ("completed", "error"):
|
|
244
|
+
raise MagsError(f"Job {request_id} ended unexpectedly: {st['status']}")
|
|
245
|
+
time.sleep(poll_interval)
|
|
246
|
+
|
|
247
|
+
raise MagsError(f"Job {request_id} did not start within {timeout}s")
|
|
248
|
+
|
|
249
|
+
def find_job(self, name_or_id: str) -> dict | None:
|
|
250
|
+
"""Find a running or sleeping job by name, workspace ID, or job ID.
|
|
251
|
+
|
|
252
|
+
Uses the same resolution priority as the CLI:
|
|
253
|
+
running/sleeping exact name → workspace ID → any status exact name.
|
|
254
|
+
Returns the job dict or ``None``.
|
|
255
|
+
"""
|
|
256
|
+
jobs = self.list_jobs(page_size=50).get("jobs", [])
|
|
257
|
+
|
|
258
|
+
# Priority 1: exact name match, running/sleeping
|
|
259
|
+
for j in jobs:
|
|
260
|
+
if j.get("name") == name_or_id and j.get("status") in ("running", "sleeping"):
|
|
261
|
+
return j
|
|
262
|
+
|
|
263
|
+
# Priority 2: workspace_id match, running/sleeping
|
|
264
|
+
for j in jobs:
|
|
265
|
+
if j.get("workspace_id") == name_or_id and j.get("status") in ("running", "sleeping"):
|
|
266
|
+
return j
|
|
267
|
+
|
|
268
|
+
# Priority 3: exact name match, any status
|
|
269
|
+
for j in jobs:
|
|
270
|
+
if j.get("name") == name_or_id:
|
|
271
|
+
return j
|
|
272
|
+
|
|
273
|
+
# Priority 4: workspace_id match, any status
|
|
274
|
+
for j in jobs:
|
|
275
|
+
if j.get("workspace_id") == name_or_id:
|
|
276
|
+
return j
|
|
277
|
+
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
def url(self, name_or_id: str, *, port: int = 8080) -> dict:
|
|
281
|
+
"""Enable public URL access for a job's VM.
|
|
282
|
+
|
|
283
|
+
Accepts a job ID, job name, or workspace ID.
|
|
284
|
+
Returns dict with ``url`` and access details.
|
|
285
|
+
"""
|
|
286
|
+
request_id = self._resolve_job_id(name_or_id)
|
|
287
|
+
st = self.status(request_id)
|
|
288
|
+
resp = self.enable_access(request_id, port=port)
|
|
289
|
+
subdomain = st.get("subdomain") or resp.get("subdomain")
|
|
290
|
+
if subdomain:
|
|
291
|
+
resp["url"] = f"https://{subdomain}.apps.magpiecloud.com"
|
|
292
|
+
return resp
|
|
293
|
+
|
|
294
|
+
def exec(self, name_or_id: str, command: str, *, timeout: int = 30) -> dict:
|
|
295
|
+
"""Execute a command on an existing running/sleeping sandbox via SSH.
|
|
296
|
+
|
|
297
|
+
Equivalent to ``mags exec <workspace> '<command>'``.
|
|
298
|
+
|
|
299
|
+
Returns ``{"exit_code": int, "output": str}``.
|
|
300
|
+
"""
|
|
301
|
+
job = self.find_job(name_or_id)
|
|
302
|
+
if not job:
|
|
303
|
+
raise MagsError(f"No running or sleeping VM found for '{name_or_id}'")
|
|
304
|
+
if job["status"] not in ("running", "sleeping"):
|
|
305
|
+
raise MagsError(
|
|
306
|
+
f"VM for '{name_or_id}' is {job['status']}, needs to be running or sleeping"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
request_id = job.get("request_id") or job.get("id")
|
|
310
|
+
|
|
311
|
+
# Wait for VM to be assigned (status=running doesn't guarantee vm_id yet)
|
|
312
|
+
for _ in range(15):
|
|
313
|
+
st = self.status(request_id)
|
|
314
|
+
if st.get("vm_id"):
|
|
315
|
+
break
|
|
316
|
+
time.sleep(1)
|
|
317
|
+
else:
|
|
318
|
+
raise MagsError(f"VM for '{name_or_id}' has no vm_id after 15s")
|
|
319
|
+
|
|
320
|
+
access = self.enable_access(request_id, port=22)
|
|
321
|
+
|
|
322
|
+
if not access.get("success") or not access.get("ssh_host"):
|
|
323
|
+
raise MagsError(
|
|
324
|
+
f"Failed to enable SSH access: {access.get('error', 'unknown error')}"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
ssh_host = access["ssh_host"]
|
|
328
|
+
ssh_port = str(access["ssh_port"])
|
|
329
|
+
ssh_key = access.get("ssh_private_key", "")
|
|
330
|
+
|
|
331
|
+
# Wrap command to handle chroot overlay, same as CLI
|
|
332
|
+
escaped = command.replace("'", "'\\''")
|
|
333
|
+
wrapped = (
|
|
334
|
+
f"if [ -d /overlay/bin ]; then "
|
|
335
|
+
f"chroot /overlay /bin/sh -l -c 'cd /root 2>/dev/null; {escaped}'; "
|
|
336
|
+
f"else cd /root 2>/dev/null; {escaped}; fi"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
key_file = None
|
|
340
|
+
try:
|
|
341
|
+
ssh_args = [
|
|
342
|
+
"ssh",
|
|
343
|
+
"-o", "StrictHostKeyChecking=no",
|
|
344
|
+
"-o", "UserKnownHostsFile=/dev/null",
|
|
345
|
+
"-o", "LogLevel=ERROR",
|
|
346
|
+
"-p", ssh_port,
|
|
347
|
+
]
|
|
348
|
+
if ssh_key:
|
|
349
|
+
fd, key_file = tempfile.mkstemp(prefix="mags_ssh_")
|
|
350
|
+
os.write(fd, ssh_key.encode())
|
|
351
|
+
os.close(fd)
|
|
352
|
+
os.chmod(key_file, 0o600)
|
|
353
|
+
ssh_args.extend(["-i", key_file])
|
|
354
|
+
|
|
355
|
+
ssh_args.append(f"root@{ssh_host}")
|
|
356
|
+
ssh_args.append(wrapped)
|
|
357
|
+
|
|
358
|
+
proc = subprocess.run(
|
|
359
|
+
ssh_args,
|
|
360
|
+
capture_output=True,
|
|
361
|
+
text=True,
|
|
362
|
+
timeout=timeout,
|
|
363
|
+
)
|
|
364
|
+
return {
|
|
365
|
+
"exit_code": proc.returncode,
|
|
366
|
+
"output": proc.stdout,
|
|
367
|
+
"stderr": proc.stderr,
|
|
368
|
+
}
|
|
369
|
+
except subprocess.TimeoutExpired:
|
|
370
|
+
raise MagsError(f"Command timed out after {timeout}s")
|
|
371
|
+
finally:
|
|
372
|
+
if key_file:
|
|
373
|
+
try:
|
|
374
|
+
os.unlink(key_file)
|
|
375
|
+
except OSError:
|
|
376
|
+
pass
|
|
377
|
+
|
|
201
378
|
def usage(self, *, window_days: int = 30) -> dict:
|
|
202
379
|
"""Get aggregated usage summary."""
|
|
203
380
|
return self._request(
|
|
204
381
|
"GET", "/mags-jobs/usage", params={"window_days": window_days}
|
|
205
382
|
)
|
|
206
383
|
|
|
384
|
+
# ── internal helpers ─────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
def _resolve_job_id(self, name_or_id: str) -> str:
|
|
387
|
+
"""Resolve a job name, workspace ID, or UUID to a request_id."""
|
|
388
|
+
# If it looks like a UUID, use directly
|
|
389
|
+
if len(name_or_id) >= 32 and "-" in name_or_id:
|
|
390
|
+
return name_or_id
|
|
391
|
+
job = self.find_job(name_or_id)
|
|
392
|
+
if not job:
|
|
393
|
+
raise MagsError(f"No job found for '{name_or_id}'")
|
|
394
|
+
return job.get("request_id") or job.get("id")
|
|
395
|
+
|
|
207
396
|
# ── file uploads ─────────────────────────────────────────────────
|
|
208
397
|
|
|
209
398
|
def upload_file(self, file_path: str) -> str:
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Test the Mags Python SDK: new, exec, url."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
os.environ["MAGS_API_TOKEN"] = "da214df72c164bda47970491fd839247a864c2599305ce90b38512d43ed034ea"
|
|
8
|
+
|
|
9
|
+
# Use local src/ instead of installed package
|
|
10
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
|
11
|
+
|
|
12
|
+
from mags import Mags
|
|
13
|
+
|
|
14
|
+
m = Mags()
|
|
15
|
+
WS = "sdk-test-" + str(int(time.time()))
|
|
16
|
+
|
|
17
|
+
print(f"=== Test 1: Create new VM ({WS}) ===")
|
|
18
|
+
result = m.new(WS)
|
|
19
|
+
print(f" request_id: {result['request_id']}")
|
|
20
|
+
print(f" status: {result['status']}")
|
|
21
|
+
assert result["status"] == "running"
|
|
22
|
+
print(" PASS\n")
|
|
23
|
+
|
|
24
|
+
print("=== Test 2: find_job ===")
|
|
25
|
+
job = m.find_job(WS)
|
|
26
|
+
print(f" found: workspace={job.get('workspace_id')} status={job.get('status')}")
|
|
27
|
+
assert job is not None
|
|
28
|
+
assert job["status"] == "running"
|
|
29
|
+
print(" PASS\n")
|
|
30
|
+
|
|
31
|
+
print("=== Test 3: exec - simple command ===")
|
|
32
|
+
try:
|
|
33
|
+
result = m.exec(WS, "echo HELLO_FROM_SDK && uname -a", timeout=15)
|
|
34
|
+
print(f" exit_code: {result['exit_code']}")
|
|
35
|
+
print(f" output: {result['output'].strip()}")
|
|
36
|
+
if result["stderr"]:
|
|
37
|
+
print(f" stderr: {result['stderr'].strip()}")
|
|
38
|
+
assert "HELLO_FROM_SDK" in result["output"]
|
|
39
|
+
print(" PASS\n")
|
|
40
|
+
except Exception as e:
|
|
41
|
+
print(f" FAIL: {e}\n")
|
|
42
|
+
|
|
43
|
+
print("=== Test 4: exec - create HTML file ===")
|
|
44
|
+
html = "<html><body><h1>Mags SDK Test</h1><p>Served from a microVM!</p></body></html>"
|
|
45
|
+
try:
|
|
46
|
+
result = m.exec(WS, f"echo '{html}' > /root/index.html", timeout=15)
|
|
47
|
+
print(f" exit_code: {result['exit_code']}")
|
|
48
|
+
result = m.exec(WS, "cat /root/index.html", timeout=15)
|
|
49
|
+
print(f" content: {result['output'].strip()[:80]}...")
|
|
50
|
+
print(" PASS\n")
|
|
51
|
+
except Exception as e:
|
|
52
|
+
print(f" FAIL: {e}\n")
|
|
53
|
+
|
|
54
|
+
print("=== Test 5: exec - start python HTTP server ===")
|
|
55
|
+
try:
|
|
56
|
+
result = m.exec(WS, "nohup python3 -m http.server 8080 --directory /root > /tmp/srv.log 2>&1 & echo PID=$!", timeout=15)
|
|
57
|
+
print(f" output: {result['output'].strip()}")
|
|
58
|
+
time.sleep(1)
|
|
59
|
+
# Verify it's running
|
|
60
|
+
result = m.exec(WS, "curl -s http://localhost:8080/index.html | head -1", timeout=15)
|
|
61
|
+
print(f" curl: {result['output'].strip()[:80]}")
|
|
62
|
+
print(" PASS\n")
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(f" FAIL: {e}\n")
|
|
65
|
+
|
|
66
|
+
print("=== Test 6: url - enable public URL ===")
|
|
67
|
+
try:
|
|
68
|
+
info = m.url(WS, port=8080)
|
|
69
|
+
print(f" success: {info.get('success')}")
|
|
70
|
+
print(f" url: {info.get('url', 'N/A')}")
|
|
71
|
+
if info.get("url"):
|
|
72
|
+
print(f"\n >>> Visit: {info['url']} <<<")
|
|
73
|
+
print(" PASS\n")
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print(f" FAIL: {e}\n")
|
|
76
|
+
|
|
77
|
+
print("=== All tests complete ===")
|
|
78
|
+
print(f"Workspace: {WS}")
|
package/website/api.html
CHANGED
|
@@ -360,6 +360,12 @@ m = Mags(api_token="your-token")
|
|
|
360
360
|
<td>no</td>
|
|
361
361
|
<td>File IDs from the upload endpoint. Files are downloaded into <code>/root/</code> before script runs.</td>
|
|
362
362
|
</tr>
|
|
363
|
+
<tr>
|
|
364
|
+
<td><code>disk_gb</code></td>
|
|
365
|
+
<td>int</td>
|
|
366
|
+
<td>no</td>
|
|
367
|
+
<td>Custom disk size in GB. Default is 2GB. The rootfs is resized on-the-fly after VM boot.</td>
|
|
368
|
+
</tr>
|
|
363
369
|
</tbody>
|
|
364
370
|
</table>
|
|
365
371
|
|
package/website/index.html
CHANGED
|
@@ -200,6 +200,7 @@ mags login</code></pre>
|
|
|
200
200
|
<tbody>
|
|
201
201
|
<tr><td><code>mags run <script></code></td><td>Execute a script in a fresh sandbox</td></tr>
|
|
202
202
|
<tr><td><code>mags ssh <workspace></code></td><td>SSH into a sandbox (auto-starts if needed)</td></tr>
|
|
203
|
+
<tr><td><code>mags exec <workspace> <cmd></code></td><td>Run a command on an existing sandbox</td></tr>
|
|
203
204
|
<tr><td><code>mags list</code></td><td>List recent jobs</td></tr>
|
|
204
205
|
<tr><td><code>mags status <id></code></td><td>Get job status</td></tr>
|
|
205
206
|
<tr><td><code>mags logs <id></code></td><td>Get job output</td></tr>
|
|
@@ -231,6 +232,7 @@ mags login</code></pre>
|
|
|
231
232
|
<tr><td><code>-n, --name <name></code></td><td>Name the job for easy reference</td></tr>
|
|
232
233
|
<tr><td><code>--url</code></td><td>Enable public HTTPS URL (requires <code>-p</code>)</td></tr>
|
|
233
234
|
<tr><td><code>--port <port></code></td><td>Port to expose for URL (default: 8080)</td></tr>
|
|
235
|
+
<tr><td><code>--disk <GB></code></td><td>Custom disk size in GB (default: 2)</td></tr>
|
|
234
236
|
<tr><td><code>--startup-command <cmd></code></td><td>Command to run when sandbox wakes from sleep</td></tr>
|
|
235
237
|
</tbody>
|
|
236
238
|
</table>
|
|
@@ -255,6 +257,9 @@ mags run --base golden -w fork-1 'npm test' # fork: load golden, save to for
|
|
|
255
257
|
# SSH into a workspace (auto-starts sandbox if needed)
|
|
256
258
|
mags ssh myproject
|
|
257
259
|
|
|
260
|
+
# Run a command on an existing sandbox
|
|
261
|
+
mags exec myproject 'node --version'
|
|
262
|
+
|
|
258
263
|
# Persistent sandbox with public URL
|
|
259
264
|
mags run -w webapp -p --url --port 8080 \
|
|
260
265
|
--startup-command 'python3 -m http.server 8080' \
|
|
@@ -291,13 +296,19 @@ export MAGS_API_TOKEN="your-token"</code></pre>
|
|
|
291
296
|
<tbody>
|
|
292
297
|
<tr><td><code>run(script, **opts)</code></td><td>Submit a job (returns immediately)</td></tr>
|
|
293
298
|
<tr><td><code>run_and_wait(script, **opts)</code></td><td>Submit + block until complete</td></tr>
|
|
299
|
+
<tr><td><code>new(name)</code></td><td>Create persistent VM workspace</td></tr>
|
|
300
|
+
<tr><td><code>exec(name, command)</code></td><td>Run command on existing sandbox</td></tr>
|
|
301
|
+
<tr><td><code>stop(name_or_id)</code></td><td>Stop a running job</td></tr>
|
|
302
|
+
<tr><td><code>find_job(name_or_id)</code></td><td>Find job by name or workspace</td></tr>
|
|
303
|
+
<tr><td><code>url(name_or_id, port)</code></td><td>Enable public URL access</td></tr>
|
|
294
304
|
<tr><td><code>status(request_id)</code></td><td>Get job status</td></tr>
|
|
295
305
|
<tr><td><code>logs(request_id)</code></td><td>Get job logs</td></tr>
|
|
296
306
|
<tr><td><code>list_jobs()</code></td><td>List recent jobs</td></tr>
|
|
297
|
-
<tr><td><code>enable_access(id, port)</code></td><td>Enable URL or SSH access</td></tr>
|
|
307
|
+
<tr><td><code>enable_access(id, port)</code></td><td>Enable URL or SSH access (low-level)</td></tr>
|
|
298
308
|
<tr><td><code>upload_files(paths)</code></td><td>Upload files, returns file IDs</td></tr>
|
|
299
309
|
<tr><td><code>list_workspaces()</code></td><td>List persistent workspaces</td></tr>
|
|
300
310
|
<tr><td><code>delete_workspace(id)</code></td><td>Delete workspace + cloud data</td></tr>
|
|
311
|
+
<tr><td><code>sync(request_id)</code></td><td>Sync workspace to S3 now</td></tr>
|
|
301
312
|
<tr><td><code>cron_create(**opts)</code></td><td>Create a cron job</td></tr>
|
|
302
313
|
<tr><td><code>cron_list()</code></td><td>List cron jobs</td></tr>
|
|
303
314
|
<tr><td><code>cron_delete(id)</code></td><td>Delete a cron job</td></tr>
|
|
@@ -334,27 +345,28 @@ m = Mags() # reads MAGS_API_TOKEN from env
|
|
|
334
345
|
result = m.run_and_wait("echo Hello World")
|
|
335
346
|
print(result["status"]) # "completed"
|
|
336
347
|
|
|
337
|
-
#
|
|
338
|
-
m.
|
|
339
|
-
|
|
348
|
+
# Create a persistent workspace
|
|
349
|
+
m.new("my-project")
|
|
350
|
+
|
|
351
|
+
# Execute commands on existing sandbox
|
|
352
|
+
result = m.exec("my-project", "ls -la /root")
|
|
353
|
+
print(result["output"])
|
|
354
|
+
m.exec("my-project", "pip install flask")
|
|
340
355
|
|
|
341
|
-
#
|
|
342
|
-
m.
|
|
343
|
-
m.run_and_wait("npm test", base_workspace_id="golden", workspace_id="fork-1")
|
|
356
|
+
# Stop a job
|
|
357
|
+
m.stop("my-project")
|
|
344
358
|
|
|
345
|
-
#
|
|
346
|
-
job = m.
|
|
347
|
-
|
|
348
|
-
print(f"ssh root@{ssh['ssh_host']} -p {ssh['ssh_port']}")
|
|
359
|
+
# Find a job by name or workspace
|
|
360
|
+
job = m.find_job("my-project")
|
|
361
|
+
print(job["status"]) # "running", "sleeping", etc.
|
|
349
362
|
|
|
350
363
|
# Public URL
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
access = m.enable_access(job["request_id"], port=8080)
|
|
364
|
+
m.new("webapp")
|
|
365
|
+
info = m.url("webapp", port=3000)
|
|
366
|
+
print(info["url"]) # https://xyz.apps.magpiecloud.com
|
|
355
367
|
|
|
356
368
|
# Always-on sandbox (never auto-sleeps)
|
|
357
|
-
|
|
369
|
+
m.run("python3 worker.py",
|
|
358
370
|
workspace_id="worker", persistent=True, no_sleep=True)
|
|
359
371
|
|
|
360
372
|
# Upload files
|
|
@@ -524,30 +536,30 @@ await mags.cronCreate({
|
|
|
524
536
|
|
|
525
537
|
m = Mags() # reads MAGS_API_TOKEN from env
|
|
526
538
|
|
|
527
|
-
#
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
539
|
+
# Create a workspace, run commands on it
|
|
540
|
+
m.new("demo")
|
|
541
|
+
result = m.exec("demo", "uname -a")
|
|
542
|
+
print(result["output"])
|
|
543
|
+
|
|
544
|
+
# Or run a one-shot script
|
|
545
|
+
result = m.run_and_wait("echo Hello!")
|
|
546
|
+
print(result["status"]) # "completed"</code></pre>
|
|
535
547
|
</article>
|
|
536
548
|
<article class="panel" data-reveal>
|
|
537
549
|
<h3>Available methods</h3>
|
|
538
550
|
<ul class="list">
|
|
539
551
|
<li><code>run(script, **opts)</code> — submit a job</li>
|
|
540
552
|
<li><code>run_and_wait(script, **opts)</code> — submit + block</li>
|
|
541
|
-
<li><code>
|
|
542
|
-
<li><code>
|
|
543
|
-
<li><code>
|
|
544
|
-
<li><code>
|
|
553
|
+
<li><code>new(name)</code> — create persistent VM</li>
|
|
554
|
+
<li><code>exec(name, command)</code> — run on existing sandbox</li>
|
|
555
|
+
<li><code>stop(name_or_id)</code> — stop a job</li>
|
|
556
|
+
<li><code>find_job(name_or_id)</code> — find by name/workspace</li>
|
|
557
|
+
<li><code>url(name_or_id, port)</code> — enable public URL</li>
|
|
558
|
+
<li><code>status(id)</code> / <code>logs(id)</code> / <code>list_jobs()</code></li>
|
|
545
559
|
<li><code>upload_files(paths)</code> — upload files</li>
|
|
546
|
-
<li><code>list_workspaces()</code>
|
|
547
|
-
<li><code>
|
|
548
|
-
<li><code>cron_create(**opts)</code>
|
|
549
|
-
<li><code>cron_list()</code> / <code>cron_delete(id)</code></li>
|
|
550
|
-
<li><code>usage(window_days)</code> — usage stats</li>
|
|
560
|
+
<li><code>list_workspaces()</code> / <code>delete_workspace(id)</code></li>
|
|
561
|
+
<li><code>sync(id)</code> — sync workspace to S3</li>
|
|
562
|
+
<li><code>cron_create(**opts)</code> / <code>cron_list()</code> / <code>cron_delete(id)</code></li>
|
|
551
563
|
</ul>
|
|
552
564
|
<p style="margin-top:1rem"><a class="text-link" href="https://pypi.org/project/magpie-mags/" rel="noreferrer">PyPI →</a></p>
|
|
553
565
|
</article>
|
package/website/llms.txt
CHANGED
|
@@ -29,6 +29,7 @@ mags run 'echo Hello World'
|
|
|
29
29
|
- `mags new <workspace>` — Create a persistent VM workspace
|
|
30
30
|
- `mags run <script>` — Execute a script on a microVM
|
|
31
31
|
- `mags ssh <workspace>` — Open an SSH session into a workspace
|
|
32
|
+
- `mags exec <workspace> <command>` — Run a command on an existing sandbox
|
|
32
33
|
- `mags url <job-id>` — Enable public URL access for a running job
|
|
33
34
|
- `mags list` — List recent jobs
|
|
34
35
|
- `mags logs <job-id>` — View job logs
|
|
@@ -42,10 +43,12 @@ mags run 'echo Hello World'
|
|
|
42
43
|
| `-w, --workspace <id>` | Persist files with a named workspace |
|
|
43
44
|
| `-n, --name <name>` | Job name for easier reference |
|
|
44
45
|
| `-p, --persistent` | Keep VM alive after script for URL or SSH |
|
|
46
|
+
| `--no-sleep` | Never auto-sleep this VM (requires -p) |
|
|
45
47
|
| `-e, --ephemeral` | No workspace, no S3 sync (fastest). Cannot combine with -w or -p |
|
|
46
48
|
| `-f, --file <path>` | Upload a local file into /root/ in the VM (repeatable) |
|
|
47
49
|
| `--url` | Enable public URL (requires -p) |
|
|
48
50
|
| `--port <port>` | Port to expose (default: 8080) |
|
|
51
|
+
| `--disk <GB>` | Custom disk size in GB (default: 2) |
|
|
49
52
|
| `--startup-command <cmd>` | Command to run when a VM wakes |
|
|
50
53
|
|
|
51
54
|
## File Upload
|
|
@@ -173,6 +176,48 @@ curl -X POST https://api.magpiecloud.com/api/v1/mags-jobs \
|
|
|
173
176
|
}'
|
|
174
177
|
```
|
|
175
178
|
|
|
179
|
+
## Python SDK
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
pip install magpie-mags
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from mags import Mags
|
|
187
|
+
|
|
188
|
+
mags = Mags() # uses MAGS_API_TOKEN env var
|
|
189
|
+
|
|
190
|
+
# Run a script and wait for result
|
|
191
|
+
result = mags.run_and_wait('echo Hello World')
|
|
192
|
+
print(result['logs'])
|
|
193
|
+
|
|
194
|
+
# Create a persistent workspace
|
|
195
|
+
mags.new('my-project')
|
|
196
|
+
|
|
197
|
+
# Execute a command on an existing sandbox
|
|
198
|
+
result = mags.exec('my-project', 'ls -la /root')
|
|
199
|
+
print(result['output'])
|
|
200
|
+
|
|
201
|
+
# Stop a running job
|
|
202
|
+
mags.stop('my-project')
|
|
203
|
+
|
|
204
|
+
# Enable public URL
|
|
205
|
+
info = mags.url('my-project', port=3000)
|
|
206
|
+
print(info['url'])
|
|
207
|
+
|
|
208
|
+
# Find a job by name or workspace
|
|
209
|
+
job = mags.find_job('my-project')
|
|
210
|
+
|
|
211
|
+
# Upload files and run
|
|
212
|
+
file_ids = mags.upload_files(['script.py', 'data.csv'])
|
|
213
|
+
mags.run('python3 script.py', file_ids=file_ids)
|
|
214
|
+
|
|
215
|
+
# Cron jobs
|
|
216
|
+
mags.cron_create(name='daily-backup', cron_expression='0 0 * * *',
|
|
217
|
+
script='tar czf backup.tar.gz /data', workspace_id='my-ws')
|
|
218
|
+
mags.cron_delete(cron_id)
|
|
219
|
+
```
|
|
220
|
+
|
|
176
221
|
## Node.js SDK
|
|
177
222
|
|
|
178
223
|
```
|
package/website/mags.md
CHANGED
|
@@ -36,6 +36,11 @@ mags run -w <workspace> -p --url --port 3000 '<script>'
|
|
|
36
36
|
mags new <name>
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
### Execute a command on an existing sandbox
|
|
40
|
+
```bash
|
|
41
|
+
mags exec <name-or-id> '<command>'
|
|
42
|
+
```
|
|
43
|
+
|
|
39
44
|
### SSH into a running VM
|
|
40
45
|
```bash
|
|
41
46
|
mags ssh <name-or-id>
|
|
@@ -92,10 +97,12 @@ mags cron disable <id>
|
|
|
92
97
|
| `-w, --workspace <id>` | Persistent workspace (S3 sync) |
|
|
93
98
|
| `-n, --name <name>` | Job name for easier reference |
|
|
94
99
|
| `-p, --persistent` | Keep VM alive after script |
|
|
100
|
+
| `--no-sleep` | Never auto-sleep this VM (requires -p) |
|
|
95
101
|
| `-e, --ephemeral` | No workspace, no S3 sync (fastest). Cannot combine with -w or -p |
|
|
96
102
|
| `-f, --file <path>` | Upload a file into /root/ in the VM (repeatable) |
|
|
97
103
|
| `--url` | Enable public URL (requires -p) |
|
|
98
104
|
| `--port <port>` | Port to expose (default: 8080) |
|
|
105
|
+
| `--disk <GB>` | Custom disk size in GB (default: 2) |
|
|
99
106
|
| `--startup-command <cmd>` | Command to run when VM wakes |
|
|
100
107
|
|
|
101
108
|
## Workflow
|
|
@@ -129,7 +136,19 @@ mags run -p --url 'python3 -m http.server 8080'
|
|
|
129
136
|
### Persistent workspace
|
|
130
137
|
User: `/mags set up a node project in my-app workspace`
|
|
131
138
|
```bash
|
|
132
|
-
mags run -w my-app '
|
|
139
|
+
mags run -w my-app 'cd /root && npm init -y && npm install express'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Run more commands on existing workspace
|
|
143
|
+
User: `/mags run my server in the my-app workspace`
|
|
144
|
+
```bash
|
|
145
|
+
mags run -w my-app -p --url --port 3000 'cd /root && node index.js'
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Exec into existing workspace sandbox
|
|
149
|
+
User: `/mags install lodash in my-app`
|
|
150
|
+
```bash
|
|
151
|
+
mags exec my-app 'cd /root && npm install lodash'
|
|
133
152
|
```
|
|
134
153
|
|
|
135
154
|
### Ephemeral (no workspace, fastest)
|
|
@@ -150,6 +169,18 @@ User: `/mags run my analysis with the data file`
|
|
|
150
169
|
mags run -f analyze.py -f data.csv 'python3 analyze.py'
|
|
151
170
|
```
|
|
152
171
|
|
|
172
|
+
### Run a command on an existing sandbox
|
|
173
|
+
User: `/mags check what's running on my-app`
|
|
174
|
+
```bash
|
|
175
|
+
mags exec my-app 'ps aux'
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Keep a VM always running (never auto-sleep)
|
|
179
|
+
User: `/mags start a persistent server that never sleeps`
|
|
180
|
+
```bash
|
|
181
|
+
mags run -w my-server -p --no-sleep --url --port 3000 'cd /root && node server.js'
|
|
182
|
+
```
|
|
183
|
+
|
|
153
184
|
### Schedule a cron job
|
|
154
185
|
User: `/mags schedule a health check every hour`
|
|
155
186
|
```bash
|
|
@@ -169,3 +200,4 @@ Do NOT hardcode tokens or use environment variables for auth.
|
|
|
169
200
|
3. The CLI handles job submission, polling, and log retrieval automatically
|
|
170
201
|
4. For multi-line scripts, use semicolons or `&&` to chain commands
|
|
171
202
|
5. If the user wants to create a VM and SSH in, use `mags new <name>` then `mags ssh <name>`
|
|
203
|
+
6. To run a command on an already-running sandbox, use `mags exec <name> '<command>'` instead of starting a new run
|
|
Binary file
|
|
Binary file
|