@magpiecloud/mags 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
@@ -215,6 +216,7 @@ ${colors.bold}Run Options:${colors.reset}
215
216
  -w, --workspace <id> Use persistent workspace (S3 sync)
216
217
  -n, --name <name> Set job name (for easier reference)
217
218
  -p, --persistent Keep VM alive after script completes
219
+ --base <workspace> Mount workspace read-only as base image
218
220
  -e, --ephemeral No workspace/S3 sync (fastest execution)
219
221
  -f, --file <path> Upload file(s) to VM (repeatable)
220
222
  --url Enable public URL access (requires -p)
@@ -242,6 +244,8 @@ ${colors.bold}Examples:${colors.reset}
242
244
  mags run -e 'echo fast' # Ephemeral (no S3 sync)
243
245
  mags run -f script.py 'python3 script.py' # Upload + run file
244
246
  mags run -w myproject 'python3 script.py'
247
+ mags run --base golden 'npm test' # Use golden as read-only base
248
+ mags run --base golden -w fork-1 'npm test' # Base + save changes to fork-1
245
249
  mags run -p --url 'python3 -m http.server 8080'
246
250
  mags run -n webapp -w webapp -p --url --port 3000 'npm start'
247
251
  mags workspace list # List workspaces
@@ -436,6 +440,7 @@ async function newVM(name) {
436
440
  async function runJob(args) {
437
441
  let script = '';
438
442
  let workspace = '';
443
+ let baseWorkspace = '';
439
444
  let name = '';
440
445
  let persistent = false;
441
446
  let ephemeral = false;
@@ -455,6 +460,9 @@ async function runJob(args) {
455
460
  case '--name':
456
461
  name = args[++i];
457
462
  break;
463
+ case '--base':
464
+ baseWorkspace = args[++i];
465
+ break;
458
466
  case '-p':
459
467
  case '--persistent':
460
468
  persistent = true;
@@ -496,6 +504,10 @@ async function runJob(args) {
496
504
  log('red', 'Error: Cannot use --ephemeral with --persistent; ephemeral VMs are destroyed after execution');
497
505
  process.exit(1);
498
506
  }
507
+ if (ephemeral && baseWorkspace) {
508
+ log('red', 'Error: Cannot use --ephemeral with --base; ephemeral VMs have no workspace support');
509
+ process.exit(1);
510
+ }
499
511
 
500
512
  // Upload files if any
501
513
  let fileIds = [];
@@ -523,6 +535,7 @@ async function runJob(args) {
523
535
  // Only set workspace_id if not ephemeral
524
536
  if (!ephemeral && workspace) payload.workspace_id = workspace;
525
537
  if (name) payload.name = name;
538
+ if (baseWorkspace) payload.base_workspace_id = baseWorkspace;
526
539
  if (startupCommand) payload.startup_command = startupCommand;
527
540
  if (fileIds.length > 0) payload.file_ids = fileIds;
528
541
 
@@ -538,6 +551,7 @@ async function runJob(args) {
538
551
  log('green', `Job submitted: ${requestId}`);
539
552
  if (name) log('blue', `Name: ${name}`);
540
553
  if (workspace) log('blue', `Workspace: ${workspace}`);
554
+ if (baseWorkspace) log('blue', `Base workspace: ${baseWorkspace} (read-only)`);
541
555
  if (persistent) log('yellow', 'Persistent: VM will stay alive');
542
556
 
543
557
  // Poll for completion
@@ -674,6 +688,40 @@ async function stopJob(nameOrId) {
674
688
  }
675
689
  }
676
690
 
691
+ async function syncWorkspace(nameOrId) {
692
+ if (!nameOrId) {
693
+ log('red', 'Error: Workspace name or job ID required');
694
+ console.log('\nUsage: mags sync <workspace|id>\n');
695
+ console.log('Syncs the workspace filesystem to S3 without stopping the VM.');
696
+ console.log('Use this after setting up a base image to persist your changes.\n');
697
+ console.log('Examples:');
698
+ console.log(' mags sync myproject');
699
+ console.log(' mags sync golden # after setting up base image');
700
+ process.exit(1);
701
+ }
702
+
703
+ // Try to find a running job for this workspace
704
+ const existingJob = await findWorkspaceJob(nameOrId);
705
+ let requestId;
706
+
707
+ if (existingJob && existingJob.status === 'running') {
708
+ requestId = existingJob.request_id;
709
+ } else {
710
+ // Try as a direct job ID
711
+ requestId = await resolveJobId(nameOrId);
712
+ }
713
+
714
+ log('blue', `Syncing workspace...`);
715
+ const resp = await request('POST', `/api/v1/mags-jobs/${requestId}/sync`);
716
+ if (resp.success) {
717
+ log('green', 'Workspace synced to S3 successfully');
718
+ } else {
719
+ log('red', 'Failed to sync workspace');
720
+ if (resp.error) log('red', resp.error);
721
+ process.exit(1);
722
+ }
723
+ }
724
+
677
725
  // Download a file from URL
678
726
  function downloadFile(url) {
679
727
  return new Promise((resolve, reject) => {
@@ -1202,7 +1250,7 @@ async function main() {
1202
1250
  break;
1203
1251
  case '--version':
1204
1252
  case '-v':
1205
- console.log('mags v1.6.1');
1253
+ console.log('mags v1.7.0');
1206
1254
  process.exit(0);
1207
1255
  break;
1208
1256
  case 'new':
@@ -1237,6 +1285,10 @@ async function main() {
1237
1285
  await requireAuth();
1238
1286
  await stopJob(args[1]);
1239
1287
  break;
1288
+ case 'sync':
1289
+ await requireAuth();
1290
+ await syncWorkspace(args[1]);
1291
+ break;
1240
1292
  case 'cron':
1241
1293
  await requireAuth();
1242
1294
  await cronCommand(args.slice(1));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpiecloud/mags",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "description": "Mags CLI - Execute scripts on Magpie's instant VM infrastructure",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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,279 @@ 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 url &lt;id&gt; [port]</code></td><td>Enable public URL access</td></tr>
207
+ <tr><td><code>mags workspace list</code></td><td>List persistent workspaces</td></tr>
208
+ <tr><td><code>mags workspace delete &lt;id&gt;</code></td><td>Delete workspace + S3 data</td></tr>
209
+ <tr><td><code>mags cron add [opts] &lt;script&gt;</code></td><td>Create a scheduled cron job</td></tr>
210
+ <tr><td><code>mags cron list</code></td><td>List cron jobs</td></tr>
211
+ <tr><td><code>mags cron remove &lt;id&gt;</code></td><td>Delete a cron job</td></tr>
212
+ </tbody>
213
+ </table>
251
214
  </div>
252
215
  </div>
253
- </div>
254
216
 
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>
217
+ <div class="ref-section">
218
+ <h3>Run Flags</h3>
219
+ <div class="ref-table-wrap">
220
+ <table class="ref-table">
221
+ <thead><tr><th>Flag</th><th>Description</th></tr></thead>
222
+ <tbody>
223
+ <tr><td><code>-w, --workspace &lt;id&gt;</code></td><td>Persist files to S3 across runs</td></tr>
224
+ <tr><td><code>--base &lt;workspace&gt;</code></td><td>Mount a workspace read-only as a base image</td></tr>
225
+ <tr><td><code>-p, --persistent</code></td><td>Keep VM alive after script (sleeps when idle)</td></tr>
226
+ <tr><td><code>-e, --ephemeral</code></td><td>No S3 sync &mdash; fastest execution</td></tr>
227
+ <tr><td><code>-f, --file &lt;path&gt;</code></td><td>Upload file(s) to VM (repeatable)</td></tr>
228
+ <tr><td><code>-n, --name &lt;name&gt;</code></td><td>Name the job for easy reference</td></tr>
229
+ <tr><td><code>--url</code></td><td>Enable public HTTPS URL (requires <code>-p</code>)</td></tr>
230
+ <tr><td><code>--port &lt;port&gt;</code></td><td>Port to expose for URL (default: 8080)</td></tr>
231
+ <tr><td><code>--startup-command &lt;cmd&gt;</code></td><td>Command to run when VM wakes from sleep</td></tr>
232
+ </tbody>
233
+ </table>
261
234
  </div>
262
- <span class="chevron">&#9660;</span>
263
235
  </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
236
+
237
+ <div class="ref-section">
238
+ <h3>Examples</h3>
239
+ <pre><code># Run a one-off command
240
+ mags run 'echo Hello World && uname -a'
241
+
242
+ # Persistent workspace &mdash; files survive across runs
243
+ mags run -w myproject 'pip install flask requests'
244
+ mags run -w myproject 'python3 app.py'
245
+
246
+ # Base image &mdash; start from a pre-configured workspace
247
+ mags run --base golden 'npm test' # read-only, changes discarded
248
+ mags run --base golden -w fork-1 'npm test' # fork: load golden, save to fork-1
249
+
250
+ # SSH into a workspace (auto-starts VM if needed)
272
251
  mags ssh myproject
273
252
 
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)
253
+ # Persistent VM with public URL
254
+ mags run -w webapp -p --url --port 8080 \
255
+ --startup-command 'python3 -m http.server 8080' \
256
+ 'python3 -m http.server 8080'
280
257
 
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>
258
+ # Upload files and run
259
+ mags run -f script.py -f data.csv 'python3 script.py'
260
+
261
+ # Ephemeral (fastest &mdash; no S3 sync)
262
+ mags run -e 'uname -a && df -h'
263
+
264
+ # Cron job
265
+ mags cron add --name backup --schedule "0 0 * * *" \
266
+ -w backups 'tar czf backup.tar.gz /data'</code></pre>
291
267
  </div>
292
268
  </div>
293
269
 
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>
270
+ <!-- ── Python SDK Reference ── -->
271
+ <div class="tab-content" data-tab="ref-python">
272
+ <div class="ref-section">
273
+ <h3>Install</h3>
274
+ <pre><code>pip install magpie-mags
275
+ export MAGS_API_TOKEN="your-token"</code></pre>
302
276
  </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
277
 
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>
278
+ <div class="ref-section">
279
+ <h3>Methods</h3>
280
+ <div class="ref-table-wrap">
281
+ <table class="ref-table">
282
+ <thead><tr><th>Method</th><th>Description</th></tr></thead>
283
+ <tbody>
284
+ <tr><td><code>run(script, **opts)</code></td><td>Submit a job (returns immediately)</td></tr>
285
+ <tr><td><code>run_and_wait(script, **opts)</code></td><td>Submit + block until complete</td></tr>
286
+ <tr><td><code>status(request_id)</code></td><td>Get job status</td></tr>
287
+ <tr><td><code>logs(request_id)</code></td><td>Get job logs</td></tr>
288
+ <tr><td><code>list_jobs()</code></td><td>List recent jobs</td></tr>
289
+ <tr><td><code>enable_access(id, port)</code></td><td>Enable URL or SSH access</td></tr>
290
+ <tr><td><code>upload_files(paths)</code></td><td>Upload files, returns file IDs</td></tr>
291
+ <tr><td><code>list_workspaces()</code></td><td>List persistent workspaces</td></tr>
292
+ <tr><td><code>delete_workspace(id)</code></td><td>Delete workspace + S3 data</td></tr>
293
+ <tr><td><code>cron_create(**opts)</code></td><td>Create a cron job</td></tr>
294
+ <tr><td><code>cron_list()</code></td><td>List cron jobs</td></tr>
295
+ <tr><td><code>cron_delete(id)</code></td><td>Delete a cron job</td></tr>
296
+ <tr><td><code>usage(window_days)</code></td><td>Get usage stats</td></tr>
297
+ </tbody>
298
+ </table>
335
299
  </div>
336
300
  </div>
337
- </div>
338
301
 
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>
302
+ <div class="ref-section">
303
+ <h3>Run Options</h3>
304
+ <div class="ref-table-wrap">
305
+ <table class="ref-table">
306
+ <thead><tr><th>Parameter</th><th>Description</th></tr></thead>
307
+ <tbody>
308
+ <tr><td><code>workspace_id</code></td><td>Persist files to S3 across runs</td></tr>
309
+ <tr><td><code>base_workspace_id</code></td><td>Mount a workspace read-only as base image</td></tr>
310
+ <tr><td><code>persistent</code></td><td>Keep VM alive after script completes</td></tr>
311
+ <tr><td><code>ephemeral</code></td><td>No S3 sync (fastest)</td></tr>
312
+ <tr><td><code>file_ids</code></td><td>List of uploaded file IDs to include</td></tr>
313
+ <tr><td><code>startup_command</code></td><td>Command to run when VM wakes</td></tr>
314
+ </tbody>
315
+ </table>
345
316
  </div>
346
- <span class="chevron">&#9660;</span>
347
317
  </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>
318
+
319
+ <div class="ref-section">
320
+ <h3>Examples</h3>
321
+ <pre><code>from mags import Mags
322
+ m = Mags() # reads MAGS_API_TOKEN from env
323
+
324
+ # Run a command and wait
325
+ result = m.run_and_wait("echo Hello World")
326
+ print(result["status"]) # "completed"
327
+
328
+ # Persistent workspace
329
+ m.run_and_wait("pip install flask", workspace_id="myproject")
330
+ m.run_and_wait("python3 app.py", workspace_id="myproject")
331
+
332
+ # Base image
333
+ m.run_and_wait("npm test", base_workspace_id="golden")
334
+ m.run_and_wait("npm test", base_workspace_id="golden", workspace_id="fork-1")
335
+
336
+ # SSH access
337
+ job = m.run("sleep 3600", workspace_id="dev", persistent=True)
338
+ ssh = m.enable_access(job["request_id"], port=22)
339
+ print(f"ssh root@{ssh['ssh_host']} -p {ssh['ssh_port']}")
340
+
341
+ # Public URL
342
+ job = m.run("python3 -m http.server 8080",
343
+ workspace_id="webapp", persistent=True,
344
+ startup_command="python3 -m http.server 8080")
345
+ access = m.enable_access(job["request_id"], port=8080)
346
+
347
+ # Upload files
348
+ file_ids = m.upload_files(["script.py", "data.csv"])
349
+ m.run_and_wait("python3 /uploads/script.py", file_ids=file_ids)
350
+
351
+ # Workspaces
352
+ workspaces = m.list_workspaces()
353
+ m.delete_workspace("myproject")
354
+
355
+ # Cron
356
+ m.cron_create(name="backup", cron_expression="0 0 * * *",
357
+ script="tar czf backup.tar.gz /data", workspace_id="backups")</code></pre>
376
358
  </div>
359
+ <p><a class="text-link" href="https://pypi.org/project/magpie-mags/" rel="noreferrer">View on PyPI &rarr;</a></p>
377
360
  </div>
378
361
 
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>
362
+ <!-- ── Node.js SDK Reference ── -->
363
+ <div class="tab-content" data-tab="ref-node">
364
+ <div class="ref-section">
365
+ <h3>Install</h3>
366
+ <pre><code>npm install @magpiecloud/mags
367
+ export MAGS_API_TOKEN="your-token"</code></pre>
387
368
  </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
369
 
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>
370
+ <div class="ref-section">
371
+ <h3>Methods</h3>
372
+ <div class="ref-table-wrap">
373
+ <table class="ref-table">
374
+ <thead><tr><th>Method</th><th>Description</th></tr></thead>
375
+ <tbody>
376
+ <tr><td><code>run(script, opts)</code></td><td>Submit a job (returns immediately)</td></tr>
377
+ <tr><td><code>runAndWait(script, opts)</code></td><td>Submit + block until complete</td></tr>
378
+ <tr><td><code>status(requestId)</code></td><td>Get job status</td></tr>
379
+ <tr><td><code>logs(requestId)</code></td><td>Get job logs</td></tr>
380
+ <tr><td><code>listJobs()</code></td><td>List recent jobs</td></tr>
381
+ <tr><td><code>enableAccess(id, { port })</code></td><td>Enable URL or SSH access</td></tr>
382
+ <tr><td><code>uploadFile(path)</code></td><td>Upload a file, returns file ID</td></tr>
383
+ <tr><td><code>cronCreate(opts)</code></td><td>Create a cron job</td></tr>
384
+ <tr><td><code>cronList()</code></td><td>List cron jobs</td></tr>
385
+ <tr><td><code>cronDelete(id)</code></td><td>Delete a cron job</td></tr>
386
+ <tr><td><code>usage(windowDays)</code></td><td>Get usage stats</td></tr>
387
+ </tbody>
388
+ </table>
426
389
  </div>
427
390
  </div>
428
- </div>
429
391
 
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>
392
+ <div class="ref-section">
393
+ <h3>Run Options</h3>
394
+ <div class="ref-table-wrap">
395
+ <table class="ref-table">
396
+ <thead><tr><th>Parameter</th><th>Description</th></tr></thead>
397
+ <tbody>
398
+ <tr><td><code>workspaceId</code></td><td>Persist files to S3 across runs</td></tr>
399
+ <tr><td><code>baseWorkspaceId</code></td><td>Mount a workspace read-only as base image</td></tr>
400
+ <tr><td><code>persistent</code></td><td>Keep VM alive after script completes</td></tr>
401
+ <tr><td><code>ephemeral</code></td><td>No S3 sync (fastest)</td></tr>
402
+ <tr><td><code>fileIds</code></td><td>Array of uploaded file IDs to include</td></tr>
403
+ <tr><td><code>startupCommand</code></td><td>Command to run when VM wakes</td></tr>
404
+ </tbody>
405
+ </table>
436
406
  </div>
437
- <span class="chevron">&#9660;</span>
438
407
  </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
408
 
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"
409
+ <div class="ref-section">
410
+ <h3>Examples</h3>
411
+ <pre><code>const Mags = require('@magpiecloud/mags');
412
+ const mags = new Mags({ apiToken: process.env.MAGS_API_TOKEN });
459
413
 
460
- # Delete a workspace
461
- curl -X DELETE .../mags-workspaces/myproject \
462
- -H "Authorization: Bearer $TOKEN"</code></pre>
463
- </div>
464
- </div>
465
- </div>
414
+ // Run a command and wait
415
+ const result = await mags.runAndWait('echo Hello World');
416
+ console.log(result.status); // "completed"
466
417
 
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>
418
+ // Persistent workspace
419
+ await mags.runAndWait('pip install flask', { workspaceId: 'myproject' });
420
+ await mags.runAndWait('python3 app.py', { workspaceId: 'myproject' });
421
+
422
+ // Base image
423
+ await mags.runAndWait('npm test', { baseWorkspaceId: 'golden' });
424
+ await mags.runAndWait('npm test', { baseWorkspaceId: 'golden', workspaceId: 'fork-1' });
425
+
426
+ // SSH access
427
+ const job = await mags.run('sleep 3600', { workspaceId: 'dev', persistent: true });
428
+ const ssh = await mags.enableAccess(job.requestId, { port: 22 });
429
+ console.log(`ssh root@${ssh.sshHost} -p ${ssh.sshPort}`);
430
+
431
+ // Public URL
432
+ const webJob = await mags.run('python3 -m http.server 8080', {
433
+ workspaceId: 'webapp', persistent: true,
434
+ startupCommand: 'python3 -m http.server 8080',
435
+ });
436
+ const access = await mags.enableAccess(webJob.requestId, { port: 8080 });
437
+ console.log(access.url);
438
+
439
+ // Upload files
440
+ const fileId = await mags.uploadFile('script.py');
441
+ await mags.runAndWait('python3 /uploads/script.py', { fileIds: [fileId] });
442
+
443
+ // Cron
444
+ await mags.cronCreate({
445
+ name: 'backup', cronExpression: '0 0 * * *',
446
+ script: 'tar czf backup.tar.gz /data', workspaceId: 'backups',
447
+ });</code></pre>
487
448
  </div>
449
+ <p><a class="text-link" href="https://www.npmjs.com/package/@magpiecloud/mags" rel="noreferrer">View on npm &rarr;</a></p>
488
450
  </div>
489
451
  </div>
490
452
  </div>
@@ -702,7 +664,7 @@ console.log(result.logs);</code></pre>
702
664
  </div>
703
665
  </footer>
704
666
 
705
- <script src="script.js?v=2"></script>
667
+ <script src="script.js?v=3"></script>
706
668
  <script>
707
669
  (function() {
708
670
  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 {