@magpiecloud/mags 1.6.2 → 1.7.1
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 +55 -4
- package/package.json +1 -1
- package/python/INTEGRATION.md +61 -8
- package/python/src/mags/client.py +8 -0
- package/website/index.html +241 -276
- package/website/styles.css +63 -0
package/bin/mags.js
CHANGED
|
@@ -207,6 +207,7 @@ ${colors.bold}Commands:${colors.reset}
|
|
|
207
207
|
list List recent jobs
|
|
208
208
|
url <name|id> [port] Enable URL access for a job
|
|
209
209
|
stop <name|id> Stop a running job
|
|
210
|
+
sync <workspace|id> Sync workspace to S3 (without stopping)
|
|
210
211
|
workspace list List persistent workspaces
|
|
211
212
|
workspace delete <id> Delete a workspace and its S3 data
|
|
212
213
|
setup-claude Install Mags skill for Claude Code
|
|
@@ -388,10 +389,21 @@ To use Mags, you need to authenticate first.
|
|
|
388
389
|
}
|
|
389
390
|
|
|
390
391
|
// Create a new persistent VM
|
|
391
|
-
async function newVM(
|
|
392
|
+
async function newVM(args) {
|
|
393
|
+
let name = null;
|
|
394
|
+
let baseWorkspace = null;
|
|
395
|
+
|
|
396
|
+
for (let i = 0; i < args.length; i++) {
|
|
397
|
+
if (args[i] === '--base' && args[i + 1]) {
|
|
398
|
+
baseWorkspace = args[++i];
|
|
399
|
+
} else if (!name) {
|
|
400
|
+
name = args[i];
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
392
404
|
if (!name) {
|
|
393
405
|
log('red', 'Error: Name required');
|
|
394
|
-
console.log(`\nUsage: mags new <name
|
|
406
|
+
console.log(`\nUsage: mags new <name> [--base <workspace>]\n`);
|
|
395
407
|
process.exit(1);
|
|
396
408
|
}
|
|
397
409
|
|
|
@@ -403,6 +415,7 @@ async function newVM(name) {
|
|
|
403
415
|
workspace_id: name,
|
|
404
416
|
startup_command: 'sleep infinity'
|
|
405
417
|
};
|
|
418
|
+
if (baseWorkspace) payload.base_workspace_id = baseWorkspace;
|
|
406
419
|
|
|
407
420
|
const response = await request('POST', '/api/v1/mags-jobs', payload);
|
|
408
421
|
|
|
@@ -687,6 +700,40 @@ async function stopJob(nameOrId) {
|
|
|
687
700
|
}
|
|
688
701
|
}
|
|
689
702
|
|
|
703
|
+
async function syncWorkspace(nameOrId) {
|
|
704
|
+
if (!nameOrId) {
|
|
705
|
+
log('red', 'Error: Workspace name or job ID required');
|
|
706
|
+
console.log('\nUsage: mags sync <workspace|id>\n');
|
|
707
|
+
console.log('Syncs the workspace filesystem to S3 without stopping the VM.');
|
|
708
|
+
console.log('Use this after setting up a base image to persist your changes.\n');
|
|
709
|
+
console.log('Examples:');
|
|
710
|
+
console.log(' mags sync myproject');
|
|
711
|
+
console.log(' mags sync golden # after setting up base image');
|
|
712
|
+
process.exit(1);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Try to find a running job for this workspace
|
|
716
|
+
const existingJob = await findWorkspaceJob(nameOrId);
|
|
717
|
+
let requestId;
|
|
718
|
+
|
|
719
|
+
if (existingJob && existingJob.status === 'running') {
|
|
720
|
+
requestId = existingJob.request_id;
|
|
721
|
+
} else {
|
|
722
|
+
// Try as a direct job ID
|
|
723
|
+
requestId = await resolveJobId(nameOrId);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
log('blue', `Syncing workspace...`);
|
|
727
|
+
const resp = await request('POST', `/api/v1/mags-jobs/${requestId}/sync`);
|
|
728
|
+
if (resp.success) {
|
|
729
|
+
log('green', 'Workspace synced to S3 successfully');
|
|
730
|
+
} else {
|
|
731
|
+
log('red', 'Failed to sync workspace');
|
|
732
|
+
if (resp.error) log('red', resp.error);
|
|
733
|
+
process.exit(1);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
690
737
|
// Download a file from URL
|
|
691
738
|
function downloadFile(url) {
|
|
692
739
|
return new Promise((resolve, reject) => {
|
|
@@ -1215,12 +1262,12 @@ async function main() {
|
|
|
1215
1262
|
break;
|
|
1216
1263
|
case '--version':
|
|
1217
1264
|
case '-v':
|
|
1218
|
-
console.log('mags v1.
|
|
1265
|
+
console.log('mags v1.7.0');
|
|
1219
1266
|
process.exit(0);
|
|
1220
1267
|
break;
|
|
1221
1268
|
case 'new':
|
|
1222
1269
|
await requireAuth();
|
|
1223
|
-
await newVM(args
|
|
1270
|
+
await newVM(args.slice(1));
|
|
1224
1271
|
break;
|
|
1225
1272
|
case 'run':
|
|
1226
1273
|
await requireAuth();
|
|
@@ -1250,6 +1297,10 @@ async function main() {
|
|
|
1250
1297
|
await requireAuth();
|
|
1251
1298
|
await stopJob(args[1]);
|
|
1252
1299
|
break;
|
|
1300
|
+
case 'sync':
|
|
1301
|
+
await requireAuth();
|
|
1302
|
+
await syncWorkspace(args[1]);
|
|
1303
|
+
break;
|
|
1253
1304
|
case 'cron':
|
|
1254
1305
|
await requireAuth();
|
|
1255
1306
|
await cronCommand(args.slice(1));
|
package/package.json
CHANGED
package/python/INTEGRATION.md
CHANGED
|
@@ -116,20 +116,39 @@ result = run_with_packages(
|
|
|
116
116
|
|
|
117
117
|
### With a pre-built base image
|
|
118
118
|
|
|
119
|
-
For repeated runs, avoid re-installing packages every time by
|
|
119
|
+
For repeated runs, avoid re-installing packages every time by creating a base workspace and syncing it:
|
|
120
120
|
|
|
121
121
|
```python
|
|
122
122
|
# One-time setup: create a base workspace with common packages
|
|
123
|
-
m.
|
|
123
|
+
job = m.run(
|
|
124
124
|
"pip install pandas numpy requests flask scikit-learn",
|
|
125
125
|
workspace_id="python-base",
|
|
126
|
+
persistent=True,
|
|
126
127
|
)
|
|
127
128
|
|
|
129
|
+
# Wait for setup to finish, then sync to S3
|
|
130
|
+
import time
|
|
131
|
+
for _ in range(60):
|
|
132
|
+
status = m.status(job["request_id"])
|
|
133
|
+
if status["status"] == "running":
|
|
134
|
+
break
|
|
135
|
+
time.sleep(1)
|
|
136
|
+
|
|
137
|
+
# Force sync — persists everything to S3 immediately
|
|
138
|
+
m.sync(job["request_id"])
|
|
139
|
+
|
|
128
140
|
# Every subsequent run inherits the base (read-only, no install needed)
|
|
129
141
|
result = m.run_and_wait(
|
|
130
142
|
"python3 -c 'import pandas; print(pandas.__version__)'",
|
|
131
143
|
base_workspace_id="python-base",
|
|
132
144
|
)
|
|
145
|
+
|
|
146
|
+
# Fork: load base, save changes to a new workspace
|
|
147
|
+
result = m.run_and_wait(
|
|
148
|
+
"pip install torch",
|
|
149
|
+
base_workspace_id="python-base",
|
|
150
|
+
workspace_id="python-ml",
|
|
151
|
+
)
|
|
133
152
|
```
|
|
134
153
|
|
|
135
154
|
---
|
|
@@ -259,7 +278,7 @@ def run_data_pipeline(sql_query, workspace_id="etl-pipeline"):
|
|
|
259
278
|
python3 << 'PYEOF'
|
|
260
279
|
import sqlite3, json
|
|
261
280
|
|
|
262
|
-
conn = sqlite3.connect("/
|
|
281
|
+
conn = sqlite3.connect("/root/data.db")
|
|
263
282
|
cursor = conn.execute("{sql_query}")
|
|
264
283
|
rows = cursor.fetchall()
|
|
265
284
|
print(json.dumps(rows))
|
|
@@ -430,7 +449,7 @@ cron = m.cron_create(
|
|
|
430
449
|
m.cron_create(
|
|
431
450
|
name="db-backup",
|
|
432
451
|
cron_expression="0 2 * * *",
|
|
433
|
-
script="pg_dump $DATABASE_URL | gzip > /
|
|
452
|
+
script="pg_dump $DATABASE_URL | gzip > /root/backup-$(date +%F).sql.gz",
|
|
434
453
|
workspace_id="backups",
|
|
435
454
|
)
|
|
436
455
|
|
|
@@ -462,17 +481,17 @@ def deploy_preview(user_id, html_content):
|
|
|
462
481
|
import shlex
|
|
463
482
|
|
|
464
483
|
script = f"""
|
|
465
|
-
mkdir -p /
|
|
466
|
-
cat > /
|
|
484
|
+
mkdir -p /root/site
|
|
485
|
+
cat > /root/site/index.html << 'HTMLEOF'
|
|
467
486
|
{html_content}
|
|
468
487
|
HTMLEOF
|
|
469
|
-
cd /
|
|
488
|
+
cd /root/site && python3 -m http.server 8080
|
|
470
489
|
"""
|
|
471
490
|
job = m.run(
|
|
472
491
|
script,
|
|
473
492
|
workspace_id=f"preview-{user_id}",
|
|
474
493
|
persistent=True,
|
|
475
|
-
startup_command="cd /
|
|
494
|
+
startup_command="cd /root/site && python3 -m http.server 8080",
|
|
476
495
|
)
|
|
477
496
|
|
|
478
497
|
# Wait for VM to start
|
|
@@ -528,6 +547,40 @@ m.run_and_wait("echo 'no persistence'", ephemeral=True)
|
|
|
528
547
|
| Read-only base | omit | `"my-base"` | Base mounted read-only. Changes discarded. |
|
|
529
548
|
| Fork | `"fork-1"` | `"my-base"` | Starts from base, saves to `fork-1`. |
|
|
530
549
|
|
|
550
|
+
### Syncing workspaces
|
|
551
|
+
|
|
552
|
+
Workspaces sync to S3 automatically when a job completes. For persistent VMs (`persistent=True`), workspaces also sync every 30 seconds and on sleep.
|
|
553
|
+
|
|
554
|
+
Use `m.sync()` to force an immediate sync without stopping the VM — useful for persisting a base image you've just set up:
|
|
555
|
+
|
|
556
|
+
```python
|
|
557
|
+
# Set up a base workspace on a persistent VM
|
|
558
|
+
job = m.run(
|
|
559
|
+
"pip install pandas numpy scikit-learn",
|
|
560
|
+
workspace_id="ml-base",
|
|
561
|
+
persistent=True,
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# Wait for the install to finish
|
|
565
|
+
import time
|
|
566
|
+
for _ in range(60):
|
|
567
|
+
status = m.status(job["request_id"])
|
|
568
|
+
if status["status"] == "running":
|
|
569
|
+
break
|
|
570
|
+
time.sleep(1)
|
|
571
|
+
|
|
572
|
+
# Force sync — base image is now available for other jobs
|
|
573
|
+
m.sync(job["request_id"])
|
|
574
|
+
|
|
575
|
+
# List and manage workspaces
|
|
576
|
+
workspaces = m.list_workspaces()
|
|
577
|
+
for ws in workspaces.get("workspaces", []):
|
|
578
|
+
print(f"{ws['workspace_id']} — {ws['job_count']} jobs")
|
|
579
|
+
|
|
580
|
+
# Delete a workspace (removes stored data from S3)
|
|
581
|
+
m.delete_workspace("old-workspace")
|
|
582
|
+
```
|
|
583
|
+
|
|
531
584
|
---
|
|
532
585
|
|
|
533
586
|
## File Uploads
|
|
@@ -241,6 +241,14 @@ class Mags:
|
|
|
241
241
|
"""
|
|
242
242
|
return self._request("DELETE", f"/mags-workspaces/{workspace_id}")
|
|
243
243
|
|
|
244
|
+
def sync(self, request_id: str) -> dict:
|
|
245
|
+
"""Sync a running job's workspace to S3 without stopping the VM.
|
|
246
|
+
|
|
247
|
+
Use this to persist workspace changes immediately, e.g. after
|
|
248
|
+
setting up a base image.
|
|
249
|
+
"""
|
|
250
|
+
return self._request("POST", f"/mags-jobs/{request_id}/sync")
|
|
251
|
+
|
|
244
252
|
# ── cron jobs ────────────────────────────────────────────────────
|
|
245
253
|
|
|
246
254
|
def cron_create(
|
package/website/index.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
/>
|
|
11
11
|
<meta name="api-base" content="https://api.magpiecloud.com" />
|
|
12
12
|
<meta name="auth-base" content="https://api.magpiecloud.com" />
|
|
13
|
-
<link rel="stylesheet" href="styles.css?v=
|
|
13
|
+
<link rel="stylesheet" href="styles.css?v=3" />
|
|
14
14
|
<script src="env.js"></script>
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
@@ -174,317 +174,282 @@ console.log(result.status); // "completed"</code></pre>
|
|
|
174
174
|
<div class="container">
|
|
175
175
|
<div class="section-title">
|
|
176
176
|
<p>Usage Patterns</p>
|
|
177
|
-
<h2>
|
|
177
|
+
<h2>Everything you can do with Mags.</h2>
|
|
178
178
|
</div>
|
|
179
|
-
<div class="
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
<h4>Run a script</h4>
|
|
185
|
-
<p class="pattern-desc">Execute a one-off command in a fresh microVM.</p>
|
|
186
|
-
</div>
|
|
187
|
-
<span class="chevron">▼</span>
|
|
188
|
-
</div>
|
|
189
|
-
<div class="pattern-body tab-group">
|
|
190
|
-
<div class="tab-bar">
|
|
191
|
-
<button class="tab active" data-tab="p1-cli">CLI</button>
|
|
192
|
-
<button class="tab" data-tab="p1-python">Python</button>
|
|
193
|
-
<button class="tab" data-tab="p1-node">Node.js</button>
|
|
194
|
-
</div>
|
|
195
|
-
<div class="tab-content active" data-tab="p1-cli">
|
|
196
|
-
<pre><code>mags run 'echo Hello World && uname -a'</code></pre>
|
|
197
|
-
</div>
|
|
198
|
-
<div class="tab-content" data-tab="p1-python">
|
|
199
|
-
<pre><code>result = m.run_and_wait("echo Hello World && uname -a")
|
|
200
|
-
print(result["status"]) # "completed"
|
|
201
|
-
print(result["exit_code"]) # 0</code></pre>
|
|
202
|
-
</div>
|
|
203
|
-
<div class="tab-content" data-tab="p1-node">
|
|
204
|
-
<pre><code>const result = await mags.runAndWait('echo Hello World && uname -a');
|
|
205
|
-
console.log(result.status); // "completed"
|
|
206
|
-
console.log(result.exitCode); // 0</code></pre>
|
|
207
|
-
</div>
|
|
208
|
-
</div>
|
|
179
|
+
<div class="tab-group">
|
|
180
|
+
<div class="tab-bar">
|
|
181
|
+
<button class="tab active" data-tab="ref-cli">CLI</button>
|
|
182
|
+
<button class="tab" data-tab="ref-python">Python SDK</button>
|
|
183
|
+
<button class="tab" data-tab="ref-node">Node.js SDK</button>
|
|
209
184
|
</div>
|
|
210
185
|
|
|
211
|
-
<!--
|
|
212
|
-
<div class="
|
|
213
|
-
<div class="
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
</div>
|
|
218
|
-
<span class="chevron">▼</span>
|
|
186
|
+
<!-- ── CLI Reference ── -->
|
|
187
|
+
<div class="tab-content active" data-tab="ref-cli">
|
|
188
|
+
<div class="ref-section">
|
|
189
|
+
<h3>Install</h3>
|
|
190
|
+
<pre><code>npm install -g @magpiecloud/mags
|
|
191
|
+
mags login</code></pre>
|
|
219
192
|
</div>
|
|
220
|
-
<div class="pattern-body tab-group">
|
|
221
|
-
<div class="tab-bar">
|
|
222
|
-
<button class="tab active" data-tab="p2-cli">CLI</button>
|
|
223
|
-
<button class="tab" data-tab="p2-python">Python</button>
|
|
224
|
-
<button class="tab" data-tab="p2-node">Node.js</button>
|
|
225
|
-
</div>
|
|
226
|
-
<div class="tab-content active" data-tab="p2-cli">
|
|
227
|
-
<pre><code># First run: install dependencies
|
|
228
|
-
mags run -w myproject 'pip install flask requests'
|
|
229
193
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
194
|
+
<div class="ref-section">
|
|
195
|
+
<h3>Commands</h3>
|
|
196
|
+
<div class="ref-table-wrap">
|
|
197
|
+
<table class="ref-table">
|
|
198
|
+
<thead><tr><th>Command</th><th>Description</th></tr></thead>
|
|
199
|
+
<tbody>
|
|
200
|
+
<tr><td><code>mags run <script></code></td><td>Execute a script in a fresh microVM</td></tr>
|
|
201
|
+
<tr><td><code>mags ssh <workspace></code></td><td>SSH into a VM (auto-starts if needed)</td></tr>
|
|
202
|
+
<tr><td><code>mags list</code></td><td>List recent jobs</td></tr>
|
|
203
|
+
<tr><td><code>mags status <id></code></td><td>Get job status</td></tr>
|
|
204
|
+
<tr><td><code>mags logs <id></code></td><td>Get job output</td></tr>
|
|
205
|
+
<tr><td><code>mags stop <id></code></td><td>Stop a running job</td></tr>
|
|
206
|
+
<tr><td><code>mags sync <workspace></code></td><td>Sync workspace to S3 (without stopping VM)</td></tr>
|
|
207
|
+
<tr><td><code>mags url <id> [port]</code></td><td>Enable public URL access</td></tr>
|
|
208
|
+
<tr><td><code>mags workspace list</code></td><td>List persistent workspaces</td></tr>
|
|
209
|
+
<tr><td><code>mags workspace delete <id></code></td><td>Delete workspace + S3 data</td></tr>
|
|
210
|
+
<tr><td><code>mags cron add [opts] <script></code></td><td>Create a scheduled cron job</td></tr>
|
|
211
|
+
<tr><td><code>mags cron list</code></td><td>List cron jobs</td></tr>
|
|
212
|
+
<tr><td><code>mags cron remove <id></code></td><td>Delete a cron job</td></tr>
|
|
213
|
+
</tbody>
|
|
214
|
+
</table>
|
|
251
215
|
</div>
|
|
252
216
|
</div>
|
|
253
|
-
</div>
|
|
254
217
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
218
|
+
<div class="ref-section">
|
|
219
|
+
<h3>Run Flags</h3>
|
|
220
|
+
<div class="ref-table-wrap">
|
|
221
|
+
<table class="ref-table">
|
|
222
|
+
<thead><tr><th>Flag</th><th>Description</th></tr></thead>
|
|
223
|
+
<tbody>
|
|
224
|
+
<tr><td><code>-w, --workspace <id></code></td><td>Persist files to S3 across runs</td></tr>
|
|
225
|
+
<tr><td><code>--base <workspace></code></td><td>Mount a workspace read-only as a base image</td></tr>
|
|
226
|
+
<tr><td><code>-p, --persistent</code></td><td>Keep VM alive after script (sleeps when idle)</td></tr>
|
|
227
|
+
<tr><td><code>-e, --ephemeral</code></td><td>No S3 sync — fastest execution</td></tr>
|
|
228
|
+
<tr><td><code>-f, --file <path></code></td><td>Upload file(s) to VM (repeatable)</td></tr>
|
|
229
|
+
<tr><td><code>-n, --name <name></code></td><td>Name the job for easy reference</td></tr>
|
|
230
|
+
<tr><td><code>--url</code></td><td>Enable public HTTPS URL (requires <code>-p</code>)</td></tr>
|
|
231
|
+
<tr><td><code>--port <port></code></td><td>Port to expose for URL (default: 8080)</td></tr>
|
|
232
|
+
<tr><td><code>--startup-command <cmd></code></td><td>Command to run when VM wakes from sleep</td></tr>
|
|
233
|
+
</tbody>
|
|
234
|
+
</table>
|
|
261
235
|
</div>
|
|
262
|
-
<span class="chevron">▼</span>
|
|
263
236
|
</div>
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
237
|
+
|
|
238
|
+
<div class="ref-section">
|
|
239
|
+
<h3>Examples</h3>
|
|
240
|
+
<pre><code># Run a one-off command
|
|
241
|
+
mags run 'echo Hello World && uname -a'
|
|
242
|
+
|
|
243
|
+
# Persistent workspace — files survive across runs
|
|
244
|
+
mags run -w myproject 'pip install flask requests'
|
|
245
|
+
mags run -w myproject 'python3 app.py'
|
|
246
|
+
|
|
247
|
+
# Base image — create a golden image, sync it, then reuse
|
|
248
|
+
mags run -w golden -p 'apt install -y nodejs && npm install -g typescript'
|
|
249
|
+
mags sync golden # persist to S3
|
|
250
|
+
mags run --base golden 'npm test' # read-only, changes discarded
|
|
251
|
+
mags run --base golden -w fork-1 'npm test' # fork: load golden, save to fork-1
|
|
252
|
+
|
|
253
|
+
# SSH into a workspace (auto-starts VM if needed)
|
|
272
254
|
mags ssh myproject
|
|
273
255
|
|
|
274
|
-
#
|
|
275
|
-
mags
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
<pre><code># Start a persistent job, then enable SSH
|
|
279
|
-
job = m.run("sleep 3600", workspace_id="myproject", persistent=True)
|
|
256
|
+
# Persistent VM with public URL
|
|
257
|
+
mags run -w webapp -p --url --port 8080 \
|
|
258
|
+
--startup-command 'python3 -m http.server 8080' \
|
|
259
|
+
'python3 -m http.server 8080'
|
|
280
260
|
|
|
281
|
-
#
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
</div>
|
|
261
|
+
# Upload files and run
|
|
262
|
+
mags run -f script.py -f data.csv 'python3 script.py'
|
|
263
|
+
|
|
264
|
+
# Ephemeral (fastest — no S3 sync)
|
|
265
|
+
mags run -e 'uname -a && df -h'
|
|
266
|
+
|
|
267
|
+
# Cron job
|
|
268
|
+
mags cron add --name backup --schedule "0 0 * * *" \
|
|
269
|
+
-w backups 'tar czf backup.tar.gz /data'</code></pre>
|
|
291
270
|
</div>
|
|
292
271
|
</div>
|
|
293
272
|
|
|
294
|
-
<!--
|
|
295
|
-
<div class="
|
|
296
|
-
<div class="
|
|
297
|
-
<
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
</div>
|
|
301
|
-
<span class="chevron">▼</span>
|
|
273
|
+
<!-- ── Python SDK Reference ── -->
|
|
274
|
+
<div class="tab-content" data-tab="ref-python">
|
|
275
|
+
<div class="ref-section">
|
|
276
|
+
<h3>Install</h3>
|
|
277
|
+
<pre><code>pip install magpie-mags
|
|
278
|
+
export MAGS_API_TOKEN="your-token"</code></pre>
|
|
302
279
|
</div>
|
|
303
|
-
<div class="pattern-body tab-group">
|
|
304
|
-
<div class="tab-bar">
|
|
305
|
-
<button class="tab active" data-tab="p4-cli">CLI</button>
|
|
306
|
-
<button class="tab" data-tab="p4-python">Python</button>
|
|
307
|
-
<button class="tab" data-tab="p4-node">Node.js</button>
|
|
308
|
-
</div>
|
|
309
|
-
<div class="tab-content active" data-tab="p4-cli">
|
|
310
|
-
<pre><code>mags run -w webapp -p --url --port 8080 \
|
|
311
|
-
--startup-command "python3 -m http.server 8080" \
|
|
312
|
-
'echo "Hello" > index.html && python3 -m http.server 8080'
|
|
313
280
|
|
|
314
|
-
|
|
315
|
-
</
|
|
316
|
-
<div class="
|
|
317
|
-
<
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
281
|
+
<div class="ref-section">
|
|
282
|
+
<h3>Methods</h3>
|
|
283
|
+
<div class="ref-table-wrap">
|
|
284
|
+
<table class="ref-table">
|
|
285
|
+
<thead><tr><th>Method</th><th>Description</th></tr></thead>
|
|
286
|
+
<tbody>
|
|
287
|
+
<tr><td><code>run(script, **opts)</code></td><td>Submit a job (returns immediately)</td></tr>
|
|
288
|
+
<tr><td><code>run_and_wait(script, **opts)</code></td><td>Submit + block until complete</td></tr>
|
|
289
|
+
<tr><td><code>status(request_id)</code></td><td>Get job status</td></tr>
|
|
290
|
+
<tr><td><code>logs(request_id)</code></td><td>Get job logs</td></tr>
|
|
291
|
+
<tr><td><code>list_jobs()</code></td><td>List recent jobs</td></tr>
|
|
292
|
+
<tr><td><code>enable_access(id, port)</code></td><td>Enable URL or SSH access</td></tr>
|
|
293
|
+
<tr><td><code>upload_files(paths)</code></td><td>Upload files, returns file IDs</td></tr>
|
|
294
|
+
<tr><td><code>list_workspaces()</code></td><td>List persistent workspaces</td></tr>
|
|
295
|
+
<tr><td><code>delete_workspace(id)</code></td><td>Delete workspace + S3 data</td></tr>
|
|
296
|
+
<tr><td><code>cron_create(**opts)</code></td><td>Create a cron job</td></tr>
|
|
297
|
+
<tr><td><code>cron_list()</code></td><td>List cron jobs</td></tr>
|
|
298
|
+
<tr><td><code>cron_delete(id)</code></td><td>Delete a cron job</td></tr>
|
|
299
|
+
<tr><td><code>usage(window_days)</code></td><td>Get usage stats</td></tr>
|
|
300
|
+
</tbody>
|
|
301
|
+
</table>
|
|
335
302
|
</div>
|
|
336
303
|
</div>
|
|
337
|
-
</div>
|
|
338
304
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
305
|
+
<div class="ref-section">
|
|
306
|
+
<h3>Run Options</h3>
|
|
307
|
+
<div class="ref-table-wrap">
|
|
308
|
+
<table class="ref-table">
|
|
309
|
+
<thead><tr><th>Parameter</th><th>Description</th></tr></thead>
|
|
310
|
+
<tbody>
|
|
311
|
+
<tr><td><code>workspace_id</code></td><td>Persist files to S3 across runs</td></tr>
|
|
312
|
+
<tr><td><code>base_workspace_id</code></td><td>Mount a workspace read-only as base image</td></tr>
|
|
313
|
+
<tr><td><code>persistent</code></td><td>Keep VM alive after script completes</td></tr>
|
|
314
|
+
<tr><td><code>ephemeral</code></td><td>No S3 sync (fastest)</td></tr>
|
|
315
|
+
<tr><td><code>file_ids</code></td><td>List of uploaded file IDs to include</td></tr>
|
|
316
|
+
<tr><td><code>startup_command</code></td><td>Command to run when VM wakes</td></tr>
|
|
317
|
+
</tbody>
|
|
318
|
+
</table>
|
|
345
319
|
</div>
|
|
346
|
-
<span class="chevron">▼</span>
|
|
347
320
|
</div>
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
321
|
+
|
|
322
|
+
<div class="ref-section">
|
|
323
|
+
<h3>Examples</h3>
|
|
324
|
+
<pre><code>from mags import Mags
|
|
325
|
+
m = Mags() # reads MAGS_API_TOKEN from env
|
|
326
|
+
|
|
327
|
+
# Run a command and wait
|
|
328
|
+
result = m.run_and_wait("echo Hello World")
|
|
329
|
+
print(result["status"]) # "completed"
|
|
330
|
+
|
|
331
|
+
# Persistent workspace
|
|
332
|
+
m.run_and_wait("pip install flask", workspace_id="myproject")
|
|
333
|
+
m.run_and_wait("python3 app.py", workspace_id="myproject")
|
|
334
|
+
|
|
335
|
+
# Base image
|
|
336
|
+
m.run_and_wait("npm test", base_workspace_id="golden")
|
|
337
|
+
m.run_and_wait("npm test", base_workspace_id="golden", workspace_id="fork-1")
|
|
338
|
+
|
|
339
|
+
# SSH access
|
|
340
|
+
job = m.run("sleep 3600", workspace_id="dev", persistent=True)
|
|
341
|
+
ssh = m.enable_access(job["request_id"], port=22)
|
|
342
|
+
print(f"ssh root@{ssh['ssh_host']} -p {ssh['ssh_port']}")
|
|
343
|
+
|
|
344
|
+
# Public URL
|
|
345
|
+
job = m.run("python3 -m http.server 8080",
|
|
346
|
+
workspace_id="webapp", persistent=True,
|
|
347
|
+
startup_command="python3 -m http.server 8080")
|
|
348
|
+
access = m.enable_access(job["request_id"], port=8080)
|
|
349
|
+
|
|
350
|
+
# Upload files
|
|
351
|
+
file_ids = m.upload_files(["script.py", "data.csv"])
|
|
352
|
+
m.run_and_wait("python3 /uploads/script.py", file_ids=file_ids)
|
|
353
|
+
|
|
354
|
+
# Workspaces
|
|
355
|
+
workspaces = m.list_workspaces()
|
|
356
|
+
m.delete_workspace("myproject")
|
|
357
|
+
|
|
358
|
+
# Cron
|
|
359
|
+
m.cron_create(name="backup", cron_expression="0 0 * * *",
|
|
360
|
+
script="tar czf backup.tar.gz /data", workspace_id="backups")</code></pre>
|
|
376
361
|
</div>
|
|
362
|
+
<p><a class="text-link" href="https://pypi.org/project/magpie-mags/" rel="noreferrer">View on PyPI →</a></p>
|
|
377
363
|
</div>
|
|
378
364
|
|
|
379
|
-
<!--
|
|
380
|
-
<div class="
|
|
381
|
-
<div class="
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
</div>
|
|
386
|
-
<span class="chevron">▼</span>
|
|
365
|
+
<!-- ── Node.js SDK Reference ── -->
|
|
366
|
+
<div class="tab-content" data-tab="ref-node">
|
|
367
|
+
<div class="ref-section">
|
|
368
|
+
<h3>Install</h3>
|
|
369
|
+
<pre><code>npm install @magpiecloud/mags
|
|
370
|
+
export MAGS_API_TOKEN="your-token"</code></pre>
|
|
387
371
|
</div>
|
|
388
|
-
<div class="pattern-body tab-group">
|
|
389
|
-
<div class="tab-bar">
|
|
390
|
-
<button class="tab active" data-tab="p6-cli">CLI</button>
|
|
391
|
-
<button class="tab" data-tab="p6-python">Python</button>
|
|
392
|
-
<button class="tab" data-tab="p6-rest">REST</button>
|
|
393
|
-
</div>
|
|
394
|
-
<div class="tab-content active" data-tab="p6-cli">
|
|
395
|
-
<pre><code>mags cron add \
|
|
396
|
-
--name "health-check" \
|
|
397
|
-
--schedule "0 */2 * * *" \
|
|
398
|
-
-w monitors \
|
|
399
|
-
'curl -sf https://myapp.com/health'
|
|
400
|
-
|
|
401
|
-
mags cron list
|
|
402
|
-
mags cron remove <id></code></pre>
|
|
403
|
-
</div>
|
|
404
|
-
<div class="tab-content" data-tab="p6-python">
|
|
405
|
-
<pre><code>cron = m.cron_create(
|
|
406
|
-
name="health-check",
|
|
407
|
-
cron_expression="0 */2 * * *",
|
|
408
|
-
script='curl -sf https://myapp.com/health',
|
|
409
|
-
workspace_id="monitors",
|
|
410
|
-
)
|
|
411
|
-
print(cron["id"])
|
|
412
372
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
373
|
+
<div class="ref-section">
|
|
374
|
+
<h3>Methods</h3>
|
|
375
|
+
<div class="ref-table-wrap">
|
|
376
|
+
<table class="ref-table">
|
|
377
|
+
<thead><tr><th>Method</th><th>Description</th></tr></thead>
|
|
378
|
+
<tbody>
|
|
379
|
+
<tr><td><code>run(script, opts)</code></td><td>Submit a job (returns immediately)</td></tr>
|
|
380
|
+
<tr><td><code>runAndWait(script, opts)</code></td><td>Submit + block until complete</td></tr>
|
|
381
|
+
<tr><td><code>status(requestId)</code></td><td>Get job status</td></tr>
|
|
382
|
+
<tr><td><code>logs(requestId)</code></td><td>Get job logs</td></tr>
|
|
383
|
+
<tr><td><code>listJobs()</code></td><td>List recent jobs</td></tr>
|
|
384
|
+
<tr><td><code>enableAccess(id, { port })</code></td><td>Enable URL or SSH access</td></tr>
|
|
385
|
+
<tr><td><code>uploadFile(path)</code></td><td>Upload a file, returns file ID</td></tr>
|
|
386
|
+
<tr><td><code>cronCreate(opts)</code></td><td>Create a cron job</td></tr>
|
|
387
|
+
<tr><td><code>cronList()</code></td><td>List cron jobs</td></tr>
|
|
388
|
+
<tr><td><code>cronDelete(id)</code></td><td>Delete a cron job</td></tr>
|
|
389
|
+
<tr><td><code>usage(windowDays)</code></td><td>Get usage stats</td></tr>
|
|
390
|
+
</tbody>
|
|
391
|
+
</table>
|
|
426
392
|
</div>
|
|
427
393
|
</div>
|
|
428
|
-
</div>
|
|
429
394
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
395
|
+
<div class="ref-section">
|
|
396
|
+
<h3>Run Options</h3>
|
|
397
|
+
<div class="ref-table-wrap">
|
|
398
|
+
<table class="ref-table">
|
|
399
|
+
<thead><tr><th>Parameter</th><th>Description</th></tr></thead>
|
|
400
|
+
<tbody>
|
|
401
|
+
<tr><td><code>workspaceId</code></td><td>Persist files to S3 across runs</td></tr>
|
|
402
|
+
<tr><td><code>baseWorkspaceId</code></td><td>Mount a workspace read-only as base image</td></tr>
|
|
403
|
+
<tr><td><code>persistent</code></td><td>Keep VM alive after script completes</td></tr>
|
|
404
|
+
<tr><td><code>ephemeral</code></td><td>No S3 sync (fastest)</td></tr>
|
|
405
|
+
<tr><td><code>fileIds</code></td><td>Array of uploaded file IDs to include</td></tr>
|
|
406
|
+
<tr><td><code>startupCommand</code></td><td>Command to run when VM wakes</td></tr>
|
|
407
|
+
</tbody>
|
|
408
|
+
</table>
|
|
436
409
|
</div>
|
|
437
|
-
<span class="chevron">▼</span>
|
|
438
410
|
</div>
|
|
439
|
-
<div class="pattern-body tab-group">
|
|
440
|
-
<div class="tab-bar">
|
|
441
|
-
<button class="tab active" data-tab="p7-cli">CLI</button>
|
|
442
|
-
<button class="tab" data-tab="p7-python">Python</button>
|
|
443
|
-
<button class="tab" data-tab="p7-rest">REST</button>
|
|
444
|
-
</div>
|
|
445
|
-
<div class="tab-content active" data-tab="p7-cli">
|
|
446
|
-
<pre><code>mags workspace list
|
|
447
|
-
mags workspace delete myproject</code></pre>
|
|
448
|
-
</div>
|
|
449
|
-
<div class="tab-content" data-tab="p7-python">
|
|
450
|
-
<pre><code>workspaces = m.list_workspaces()
|
|
451
|
-
for ws in workspaces["workspaces"]:
|
|
452
|
-
print(ws["workspace_id"], ws["job_count"])
|
|
453
411
|
|
|
454
|
-
|
|
455
|
-
</
|
|
456
|
-
<
|
|
457
|
-
|
|
458
|
-
curl .../mags-workspaces -H "Authorization: Bearer $TOKEN"
|
|
412
|
+
<div class="ref-section">
|
|
413
|
+
<h3>Examples</h3>
|
|
414
|
+
<pre><code>const Mags = require('@magpiecloud/mags');
|
|
415
|
+
const mags = new Mags({ apiToken: process.env.MAGS_API_TOKEN });
|
|
459
416
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
</div>
|
|
464
|
-
</div>
|
|
465
|
-
</div>
|
|
417
|
+
// Run a command and wait
|
|
418
|
+
const result = await mags.runAndWait('echo Hello World');
|
|
419
|
+
console.log(result.status); // "completed"
|
|
466
420
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
421
|
+
// Persistent workspace
|
|
422
|
+
await mags.runAndWait('pip install flask', { workspaceId: 'myproject' });
|
|
423
|
+
await mags.runAndWait('python3 app.py', { workspaceId: 'myproject' });
|
|
424
|
+
|
|
425
|
+
// Base image
|
|
426
|
+
await mags.runAndWait('npm test', { baseWorkspaceId: 'golden' });
|
|
427
|
+
await mags.runAndWait('npm test', { baseWorkspaceId: 'golden', workspaceId: 'fork-1' });
|
|
428
|
+
|
|
429
|
+
// SSH access
|
|
430
|
+
const job = await mags.run('sleep 3600', { workspaceId: 'dev', persistent: true });
|
|
431
|
+
const ssh = await mags.enableAccess(job.requestId, { port: 22 });
|
|
432
|
+
console.log(`ssh root@${ssh.sshHost} -p ${ssh.sshPort}`);
|
|
433
|
+
|
|
434
|
+
// Public URL
|
|
435
|
+
const webJob = await mags.run('python3 -m http.server 8080', {
|
|
436
|
+
workspaceId: 'webapp', persistent: true,
|
|
437
|
+
startupCommand: 'python3 -m http.server 8080',
|
|
438
|
+
});
|
|
439
|
+
const access = await mags.enableAccess(webJob.requestId, { port: 8080 });
|
|
440
|
+
console.log(access.url);
|
|
441
|
+
|
|
442
|
+
// Upload files
|
|
443
|
+
const fileId = await mags.uploadFile('script.py');
|
|
444
|
+
await mags.runAndWait('python3 /uploads/script.py', { fileIds: [fileId] });
|
|
445
|
+
|
|
446
|
+
// Cron
|
|
447
|
+
await mags.cronCreate({
|
|
448
|
+
name: 'backup', cronExpression: '0 0 * * *',
|
|
449
|
+
script: 'tar czf backup.tar.gz /data', workspaceId: 'backups',
|
|
450
|
+
});</code></pre>
|
|
487
451
|
</div>
|
|
452
|
+
<p><a class="text-link" href="https://www.npmjs.com/package/@magpiecloud/mags" rel="noreferrer">View on npm →</a></p>
|
|
488
453
|
</div>
|
|
489
454
|
</div>
|
|
490
455
|
</div>
|
|
@@ -702,7 +667,7 @@ console.log(result.logs);</code></pre>
|
|
|
702
667
|
</div>
|
|
703
668
|
</footer>
|
|
704
669
|
|
|
705
|
-
<script src="script.js?v=
|
|
670
|
+
<script src="script.js?v=3"></script>
|
|
706
671
|
<script>
|
|
707
672
|
(function() {
|
|
708
673
|
var token = localStorage.getItem('microvm-access-token');
|
package/website/styles.css
CHANGED
|
@@ -823,6 +823,69 @@ code {
|
|
|
823
823
|
display: block;
|
|
824
824
|
}
|
|
825
825
|
|
|
826
|
+
/* ── Reference tables ─────────────────────────────────── */
|
|
827
|
+
|
|
828
|
+
.ref-section {
|
|
829
|
+
margin-bottom: 2rem;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.ref-section h3 {
|
|
833
|
+
margin: 0 0 0.75rem;
|
|
834
|
+
font-size: 1rem;
|
|
835
|
+
font-weight: 600;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.ref-table-wrap {
|
|
839
|
+
border: 1px solid var(--border);
|
|
840
|
+
border-radius: var(--radius-sm);
|
|
841
|
+
overflow: hidden;
|
|
842
|
+
background: var(--surface);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
.ref-table {
|
|
846
|
+
width: 100%;
|
|
847
|
+
border-collapse: collapse;
|
|
848
|
+
font-size: 0.88rem;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
.ref-table thead {
|
|
852
|
+
background: var(--surface-muted);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
.ref-table th {
|
|
856
|
+
text-align: left;
|
|
857
|
+
padding: 0.65rem 1rem;
|
|
858
|
+
font-weight: 600;
|
|
859
|
+
font-size: 0.75rem;
|
|
860
|
+
text-transform: uppercase;
|
|
861
|
+
letter-spacing: 0.06em;
|
|
862
|
+
color: var(--text-muted);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.ref-table td {
|
|
866
|
+
padding: 0.6rem 1rem;
|
|
867
|
+
border-top: 1px solid var(--border);
|
|
868
|
+
vertical-align: top;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
.ref-table td:first-child {
|
|
872
|
+
white-space: nowrap;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
.ref-table code {
|
|
876
|
+
font-family: var(--mono);
|
|
877
|
+
font-size: 0.82rem;
|
|
878
|
+
background: var(--surface-muted);
|
|
879
|
+
padding: 0.15rem 0.4rem;
|
|
880
|
+
border-radius: 4px;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
@media (max-width: 768px) {
|
|
884
|
+
.ref-table td:first-child {
|
|
885
|
+
white-space: normal;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
826
889
|
/* ── Inline tab bar (for endpoint cards in api.html) ───── */
|
|
827
890
|
|
|
828
891
|
.code-tabs {
|