@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 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(name) {
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>\n`);
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.6.2');
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[1]);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpiecloud/mags",
3
- "version": "1.6.2",
3
+ "version": "1.7.1",
4
4
  "description": "Mags CLI - Execute scripts on Magpie's instant VM infrastructure",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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 using a base workspace:
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.run_and_wait(
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("/workspace/data.db")
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 > /workspace/backup-$(date +%F).sql.gz",
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 /workspace/site
466
- cat > /workspace/site/index.html << 'HTMLEOF'
484
+ mkdir -p /root/site
485
+ cat > /root/site/index.html << 'HTMLEOF'
467
486
  {html_content}
468
487
  HTMLEOF
469
- cd /workspace/site && python3 -m http.server 8080
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 /workspace/site && python3 -m http.server 8080",
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(
@@ -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=2" />
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>Click a pattern to see how it works.</h2>
177
+ <h2>Everything you can do with Mags.</h2>
178
178
  </div>
179
- <div class="pattern-list">
180
- <!-- Run a script -->
181
- <div class="pattern-card open">
182
- <div class="pattern-header">
183
- <div>
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">&#9660;</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
- <!-- Persistent workspace -->
212
- <div class="pattern-card">
213
- <div class="pattern-header">
214
- <div>
215
- <h4>Persistent workspace</h4>
216
- <p class="pattern-desc">Install packages once, reuse across runs. Files persist to S3.</p>
217
- </div>
218
- <span class="chevron">&#9660;</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
- # Second run: they're already there
231
- mags run -w myproject 'python3 -c "import flask; print(flask.__version__)"'</code></pre>
232
- </div>
233
- <div class="tab-content" data-tab="p2-python">
234
- <pre><code># First run
235
- m.run_and_wait("pip install flask requests", workspace_id="myproject")
236
-
237
- # Second run: flask is already installed
238
- m.run_and_wait(
239
- 'python3 -c "import flask; print(flask.__version__)"',
240
- workspace_id="myproject",
241
- )</code></pre>
242
- </div>
243
- <div class="tab-content" data-tab="p2-node">
244
- <pre><code>// First run
245
- await mags.runAndWait('pip install flask requests', { workspaceId: 'myproject' });
246
-
247
- // Second run: flask is already installed
248
- await mags.runAndWait('python3 -c "import flask; print(flask.__version__)"', {
249
- workspaceId: 'myproject',
250
- });</code></pre>
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 &lt;script&gt;</code></td><td>Execute a script in a fresh microVM</td></tr>
201
+ <tr><td><code>mags ssh &lt;workspace&gt;</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 &lt;id&gt;</code></td><td>Get job status</td></tr>
204
+ <tr><td><code>mags logs &lt;id&gt;</code></td><td>Get job output</td></tr>
205
+ <tr><td><code>mags stop &lt;id&gt;</code></td><td>Stop a running job</td></tr>
206
+ <tr><td><code>mags sync &lt;workspace&gt;</code></td><td>Sync workspace to S3 (without stopping VM)</td></tr>
207
+ <tr><td><code>mags url &lt;id&gt; [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 &lt;id&gt;</code></td><td>Delete workspace + S3 data</td></tr>
210
+ <tr><td><code>mags cron add [opts] &lt;script&gt;</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 &lt;id&gt;</code></td><td>Delete a cron job</td></tr>
213
+ </tbody>
214
+ </table>
251
215
  </div>
252
216
  </div>
253
- </div>
254
217
 
255
- <!-- SSH into a VM -->
256
- <div class="pattern-card">
257
- <div class="pattern-header">
258
- <div>
259
- <h4>SSH into a VM</h4>
260
- <p class="pattern-desc">Open an interactive shell or run a command via SSH.</p>
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 &lt;id&gt;</code></td><td>Persist files to S3 across runs</td></tr>
225
+ <tr><td><code>--base &lt;workspace&gt;</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 &mdash; fastest execution</td></tr>
228
+ <tr><td><code>-f, --file &lt;path&gt;</code></td><td>Upload file(s) to VM (repeatable)</td></tr>
229
+ <tr><td><code>-n, --name &lt;name&gt;</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 &lt;port&gt;</code></td><td>Port to expose for URL (default: 8080)</td></tr>
232
+ <tr><td><code>--startup-command &lt;cmd&gt;</code></td><td>Command to run when VM wakes from sleep</td></tr>
233
+ </tbody>
234
+ </table>
261
235
  </div>
262
- <span class="chevron">&#9660;</span>
263
236
  </div>
264
- <div class="pattern-body tab-group">
265
- <div class="tab-bar">
266
- <button class="tab active" data-tab="p3-cli">CLI</button>
267
- <button class="tab" data-tab="p3-python">Python</button>
268
- <button class="tab" data-tab="p3-rest">REST</button>
269
- </div>
270
- <div class="tab-content active" data-tab="p3-cli">
271
- <pre><code># Interactive shell
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 &mdash; 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 &mdash; 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
- # Run a single command
275
- mags ssh myproject "cat /etc/os-release"</code></pre>
276
- </div>
277
- <div class="tab-content" data-tab="p3-python">
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
- # Enable SSH access
282
- ssh = m.enable_access(job["request_id"], port=22)
283
- print(f"ssh root@{ssh['ssh_host']} -p {ssh['ssh_port']}")</code></pre>
284
- </div>
285
- <div class="tab-content" data-tab="p3-rest">
286
- <pre><code>curl -X POST .../mags-jobs/$ID/access \
287
- -H "Authorization: Bearer $TOKEN" \
288
- -d '{"port": 22}'
289
- # Returns ssh_host, ssh_port, ssh_private_key</code></pre>
290
- </div>
261
+ # Upload files and run
262
+ mags run -f script.py -f data.csv 'python3 script.py'
263
+
264
+ # Ephemeral (fastest &mdash; 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
- <!-- Expose a URL -->
295
- <div class="pattern-card">
296
- <div class="pattern-header">
297
- <div>
298
- <h4>Expose a public URL</h4>
299
- <p class="pattern-desc">Run a web server and get a public HTTPS URL. Auto-wakes from sleep.</p>
300
- </div>
301
- <span class="chevron">&#9660;</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
- # Output: https://&lt;subdomain&gt;.apps.magpiecloud.com</code></pre>
315
- </div>
316
- <div class="tab-content" data-tab="p4-python">
317
- <pre><code>job = m.run(
318
- 'echo "Hello" > index.html && python3 -m http.server 8080',
319
- workspace_id="webapp",
320
- persistent=True,
321
- startup_command="python3 -m http.server 8080",
322
- )
323
- # Wait for running, then enable access
324
- access = m.enable_access(job["request_id"], port=8080)
325
- print(access["url"])</code></pre>
326
- </div>
327
- <div class="tab-content" data-tab="p4-node">
328
- <pre><code>const job = await mags.run('python3 -m http.server 8080', {
329
- workspaceId: 'webapp',
330
- persistent: true,
331
- startupCommand: 'python3 -m http.server 8080',
332
- });
333
- const access = await mags.enableAccess(job.requestId, { port: 8080 });
334
- console.log(access.url);</code></pre>
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
- <!-- Upload files -->
340
- <div class="pattern-card">
341
- <div class="pattern-header">
342
- <div>
343
- <h4>Upload files</h4>
344
- <p class="pattern-desc">Upload local files into the VM before your script runs.</p>
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">&#9660;</span>
347
320
  </div>
348
- <div class="pattern-body tab-group">
349
- <div class="tab-bar">
350
- <button class="tab active" data-tab="p5-cli">CLI</button>
351
- <button class="tab" data-tab="p5-python">Python</button>
352
- <button class="tab" data-tab="p5-rest">REST</button>
353
- </div>
354
- <div class="tab-content active" data-tab="p5-cli">
355
- <pre><code>mags run -f script.py -f data.csv \
356
- 'python3 script.py'</code></pre>
357
- </div>
358
- <div class="tab-content" data-tab="p5-python">
359
- <pre><code>file_ids = m.upload_files(["script.py", "data.csv"])
360
- result = m.run_and_wait(
361
- "python3 /uploads/script.py",
362
- file_ids=file_ids,
363
- )</code></pre>
364
- </div>
365
- <div class="tab-content" data-tab="p5-rest">
366
- <pre><code># 1. Upload file
367
- curl -X POST .../mags-files \
368
- -H "Authorization: Bearer $TOKEN" \
369
- -F "file=@script.py"
370
- # Returns: { "file_id": "abc123" }
371
-
372
- # 2. Use in job
373
- curl -X POST .../mags-jobs \
374
- -d '{ "script": "python3 /uploads/script.py", "file_ids": ["abc123"], "type": "inline" }'</code></pre>
375
- </div>
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 &rarr;</a></p>
377
363
  </div>
378
364
 
379
- <!-- Cron jobs -->
380
- <div class="pattern-card">
381
- <div class="pattern-header">
382
- <div>
383
- <h4>Schedule cron jobs</h4>
384
- <p class="pattern-desc">Run scripts on a recurring schedule.</p>
385
- </div>
386
- <span class="chevron">&#9660;</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 &lt;id&gt;</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
- # List and manage
414
- jobs = m.cron_list()
415
- m.cron_delete(cron["id"])</code></pre>
416
- </div>
417
- <div class="tab-content" data-tab="p6-rest">
418
- <pre><code>curl -X POST .../mags-cron \
419
- -H "Authorization: Bearer $TOKEN" \
420
- -d '{
421
- "name": "health-check",
422
- "cron_expression": "0 */2 * * *",
423
- "script": "curl -sf https://myapp.com/health",
424
- "workspace_id": "monitors"
425
- }'</code></pre>
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
- <!-- Manage workspaces -->
431
- <div class="pattern-card">
432
- <div class="pattern-header">
433
- <div>
434
- <h4>Manage workspaces</h4>
435
- <p class="pattern-desc">List and delete persistent workspaces.</p>
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">&#9660;</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
- m.delete_workspace("myproject")</code></pre>
455
- </div>
456
- <div class="tab-content" data-tab="p7-rest">
457
- <pre><code># List workspaces
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
- # Delete a workspace
461
- curl -X DELETE .../mags-workspaces/myproject \
462
- -H "Authorization: Bearer $TOKEN"</code></pre>
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
- <!-- Ephemeral run -->
468
- <div class="pattern-card">
469
- <div class="pattern-header">
470
- <div>
471
- <h4>Ephemeral run (fastest)</h4>
472
- <p class="pattern-desc">Skip S3 sync for maximum speed. No workspace data is saved.</p>
473
- </div>
474
- <span class="chevron">&#9660;</span>
475
- </div>
476
- <div class="pattern-body tab-group">
477
- <div class="tab-bar">
478
- <button class="tab active" data-tab="p8-cli">CLI</button>
479
- <button class="tab" data-tab="p8-python">Python</button>
480
- </div>
481
- <div class="tab-content active" data-tab="p8-cli">
482
- <pre><code>mags run -e 'uname -a && df -h'</code></pre>
483
- </div>
484
- <div class="tab-content" data-tab="p8-python">
485
- <pre><code>result = m.run_and_wait("uname -a && df -h", ephemeral=True)</code></pre>
486
- </div>
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 &rarr;</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=2"></script>
670
+ <script src="script.js?v=3"></script>
706
671
  <script>
707
672
  (function() {
708
673
  var token = localStorage.getItem('microvm-access-token');
@@ -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 {