@magpiecloud/mags 1.8.12 → 1.8.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Execute scripts instantly on Magpie's microVM infrastructure. VMs boot in <100ms
6
6
 
7
7
  Mags is a CLI and SDK for running scripts on ephemeral microVMs. Each execution gets its own isolated VM that:
8
8
 
9
- - Boots in <100ms (from warm pool)
9
+ - Boots in ~300ms (from warm pool)
10
10
  - Supports optional S3-backed persistent workspaces (with `-p` flag)
11
11
  - Syncs /root directory automatically when persistence is enabled
12
12
  - Can expose public URLs for web services
@@ -419,7 +419,7 @@ mags run 'apk add ffmpeg imagemagick'
419
419
 
420
420
  | Metric | Value |
421
421
  |--------|-------|
422
- | Warm start | <100ms |
422
+ | Warm start | ~300ms |
423
423
  | Cold start | ~4 seconds |
424
424
  | Script overhead | ~50ms |
425
425
  | Workspace sync | Every 30 seconds |
package/bin/mags.js CHANGED
@@ -429,6 +429,23 @@ async function newVM(args) {
429
429
  process.exit(1);
430
430
  }
431
431
 
432
+ // Check if a VM with this name already exists (running or sleeping)
433
+ try {
434
+ const jobs = await request('GET', '/api/v1/mags-jobs');
435
+ const existing = (jobs.jobs || []).find(j =>
436
+ j.name === name && (j.status === 'running' || j.status === 'sleeping')
437
+ );
438
+ if (existing) {
439
+ log('yellow', `Sandbox '${name}' already exists (status: ${existing.status})`);
440
+ console.log(` SSH: mags ssh ${name}`);
441
+ console.log(` Exec: mags exec ${name} <command>`);
442
+ console.log(` Stop: mags stop ${name}`);
443
+ process.exit(0);
444
+ }
445
+ } catch (e) {
446
+ // Non-fatal — proceed with creation if list fails
447
+ }
448
+
432
449
  const payload = {
433
450
  script: 'sleep infinity',
434
451
  type: 'inline',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpiecloud/mags",
3
- "version": "1.8.12",
3
+ "version": "1.8.13",
4
4
  "description": "Mags CLI - Execute scripts on Magpie's instant VM infrastructure",
5
5
  "main": "index.js",
6
6
  "bin": {
package/python/README.md CHANGED
@@ -135,15 +135,19 @@ print(f"Jobs: {usage['total_jobs']}, VM seconds: {usage['vm_seconds']:.0f}")
135
135
  | `find_job(name_or_id)` | Find a running/sleeping job by name or workspace |
136
136
  | `exec(name_or_id, command)` | Run a command on an existing VM via SSH |
137
137
  | `stop(name_or_id)` | Stop a running job |
138
+ | `url(name_or_id, port)` | Enable public URL and return full URL |
138
139
  | `resize(workspace, disk_gb)` | Resize a workspace's disk |
139
140
  | `status(request_id)` | Get job status |
140
141
  | `logs(request_id)` | Get job logs |
141
142
  | `list_jobs(page, page_size)` | List recent jobs |
142
- | `update_job(request_id, startup_command)` | Update job config |
143
+ | `update_job(request_id, **opts)` | Update job config (`startup_command`, `no_sleep`) |
143
144
  | `enable_access(request_id, port)` | Enable URL or SSH access |
144
145
  | `usage(window_days)` | Get usage summary |
145
146
  | `upload_file(path)` | Upload a file, returns file ID |
146
147
  | `upload_files(paths)` | Upload multiple files |
148
+ | `url_alias_create(sub, ws_id)` | Create a stable URL alias |
149
+ | `url_alias_list()` | List URL aliases |
150
+ | `url_alias_delete(sub)` | Delete a URL alias |
147
151
  | `cron_create(**opts)` | Create a cron job |
148
152
  | `cron_list()` | List cron jobs |
149
153
  | `cron_get(id)` | Get a cron job |
@@ -154,4 +158,4 @@ print(f"Jobs: {usage['total_jobs']}, VM seconds: {usage['vm_seconds']:.0f}")
154
158
 
155
159
  - Website: [mags.run](https://mags.run)
156
160
  - Node.js SDK: `npm install @magpiecloud/mags`
157
- - CLI: `go install` or download from releases
161
+ - CLI: `npm install -g @magpiecloud/mags`
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "magpie-mags"
7
- version = "1.3.5"
7
+ version = "1.3.6"
8
8
  description = "Mags SDK - Execute scripts on Magpie's instant VM infrastructure"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: magpie-mags
3
- Version: 1.3.4
3
+ Version: 1.3.5
4
4
  Summary: Mags SDK - Execute scripts on Magpie's instant VM infrastructure
5
5
  Author: Magpie Cloud
6
6
  License: MIT
@@ -156,7 +156,7 @@ print(f"Jobs: {usage['total_jobs']}, VM seconds: {usage['vm_seconds']:.0f}")
156
156
  |--------|-------------|
157
157
  | `run(script, **opts)` | Submit a job (`persistent`, `no_sleep`, `workspace_id`, ...) |
158
158
  | `run_and_wait(script, **opts)` | Submit and block until done |
159
- | `new(name, **opts)` | Create a persistent VM workspace |
159
+ | `new(name, **opts)` | Create a VM sandbox (`persistent=True` for S3) |
160
160
  | `find_job(name_or_id)` | Find a running/sleeping job by name or workspace |
161
161
  | `exec(name_or_id, command)` | Run a command on an existing VM via SSH |
162
162
  | `stop(name_or_id)` | Stop a running job |
package/website/api.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=6" />
13
+ <link rel="stylesheet" href="styles.css?v=7" />
14
14
  <script src="env.js"></script>
15
15
  <style>
16
16
  .endpoint { margin-bottom: 2.5rem; }
@@ -283,11 +283,14 @@ m = Mags(api_token="your-token")
283
283
  <a href="#get-status">Get Status</a>
284
284
  <a href="#get-logs">Get Logs</a>
285
285
  <a href="#list-jobs">List Jobs</a>
286
+ <a href="#stop-job">Stop Job</a>
286
287
  <a href="#enable-access">Enable Access</a>
287
288
  <a href="#update-job">Update Job</a>
289
+ <a href="#sync-workspace">Sync Workspace</a>
288
290
  <a href="#upload-file">Upload File</a>
289
291
  <a href="#list-workspaces">List Workspaces</a>
290
292
  <a href="#delete-workspace">Delete Workspace</a>
293
+ <a href="#url-aliases">URL Aliases</a>
291
294
  <a href="#cron-endpoints">Cron Jobs</a>
292
295
  </div>
293
296
 
@@ -542,12 +545,48 @@ for job in resp["jobs"]:
542
545
  print(job["request_id"], job["status"])</code></pre>
543
546
  </div>
544
547
  <div class="tab-content" data-tab="lj-js">
545
- <pre><code>const resp = await mags.listJobs({ page: 1, pageSize: 10 });
548
+ <pre><code>const resp = await mags.list({ page: 1, pageSize: 10 });
546
549
  resp.jobs.forEach(j => console.log(j.requestId, j.status));</code></pre>
547
550
  </div>
548
551
  </div>
549
552
  </div>
550
553
 
554
+ <!-- Stop Job -->
555
+ <div class="endpoint card wide" id="stop-job" data-reveal>
556
+ <h3>Stop Job</h3>
557
+ <div class="method-url">
558
+ <span class="method post">POST</span>
559
+ <span class="url-path">/api/v1/mags-jobs/:id/stop</span>
560
+ </div>
561
+ <p>Stop a running or sleeping job. The VM is terminated and the workspace is synced to S3 if persistent.</p>
562
+
563
+ <p class="response-label">Response (200)</p>
564
+ <pre><code>{ "success": true, "message": "Job stopped" }</code></pre>
565
+
566
+ <div class="code-tabs tab-group">
567
+ <p class="response-label">SDK examples</p>
568
+ <div class="tab-bar">
569
+ <button class="tab active" data-tab="sj2-curl">curl</button>
570
+ <button class="tab" data-tab="sj2-py">Python</button>
571
+ <button class="tab" data-tab="sj2-js">Node.js</button>
572
+ <button class="tab" data-tab="sj2-cli">CLI</button>
573
+ </div>
574
+ <div class="tab-content active" data-tab="sj2-curl">
575
+ <pre><code>curl -X POST https://api.magpiecloud.com/api/v1/mags-jobs/$ID/stop \
576
+ -H "Authorization: Bearer $TOKEN"</code></pre>
577
+ </div>
578
+ <div class="tab-content" data-tab="sj2-py">
579
+ <pre><code>m.stop("myproject") # accepts name, workspace ID, or request ID</code></pre>
580
+ </div>
581
+ <div class="tab-content" data-tab="sj2-js">
582
+ <pre><code>await mags.stop('myproject'); // accepts name, workspace ID, or request ID</code></pre>
583
+ </div>
584
+ <div class="tab-content" data-tab="sj2-cli">
585
+ <pre><code>mags stop myproject</code></pre>
586
+ </div>
587
+ </div>
588
+ </div>
589
+
551
590
  <!-- Enable Access -->
552
591
  <div class="endpoint card wide" id="enable-access" data-reveal>
553
592
  <h3>Enable Access (SSH or HTTP)</h3>
@@ -601,11 +640,11 @@ print(access.get("url"))</code></pre>
601
640
  </div>
602
641
  <div class="tab-content" data-tab="ea-js">
603
642
  <pre><code>// SSH access
604
- const ssh = await mags.enableAccess(requestId, { port: 22 });
643
+ const ssh = await mags.enableAccess(requestId, 22);
605
644
  console.log(`ssh root@${ssh.sshHost} -p ${ssh.sshPort}`);
606
645
 
607
646
  // HTTP access
608
- const access = await mags.enableAccess(requestId, { port: 8080 });</code></pre>
647
+ const access = await mags.enableAccess(requestId, 8080);</code></pre>
609
648
  </div>
610
649
  </div>
611
650
  </div>
@@ -714,6 +753,37 @@ file_ids = m.upload_files(["script.py", "data.csv"])</code></pre>
714
753
  </div>
715
754
  </div>
716
755
 
756
+ <!-- Sync Workspace -->
757
+ <div class="endpoint card wide" id="sync-workspace" data-reveal>
758
+ <h3>Sync Workspace</h3>
759
+ <div class="method-url">
760
+ <span class="method post">POST</span>
761
+ <span class="url-path">/api/v1/mags-jobs/:id/sync</span>
762
+ </div>
763
+ <p>Trigger an immediate workspace sync to S3 without stopping the VM.</p>
764
+
765
+ <p class="response-label">Response (200)</p>
766
+ <pre><code>{ "success": true, "message": "Sync initiated" }</code></pre>
767
+
768
+ <div class="code-tabs tab-group">
769
+ <p class="response-label">SDK examples</p>
770
+ <div class="tab-bar">
771
+ <button class="tab active" data-tab="sw-py">Python</button>
772
+ <button class="tab" data-tab="sw-js">Node.js</button>
773
+ <button class="tab" data-tab="sw-cli">CLI</button>
774
+ </div>
775
+ <div class="tab-content active" data-tab="sw-py">
776
+ <pre><code>m.sync(request_id)</code></pre>
777
+ </div>
778
+ <div class="tab-content" data-tab="sw-js">
779
+ <pre><code>await mags.sync(requestId);</code></pre>
780
+ </div>
781
+ <div class="tab-content" data-tab="sw-cli">
782
+ <pre><code>mags sync myproject</code></pre>
783
+ </div>
784
+ </div>
785
+ </div>
786
+
717
787
  <!-- List Workspaces -->
718
788
  <div class="endpoint card wide" id="list-workspaces" data-reveal>
719
789
  <h3>List Workspaces</h3>
@@ -794,6 +864,65 @@ mags workspace delete myproject --force # skip confirmation</code></pre>
794
864
  </div>
795
865
  </div>
796
866
 
867
+ <!-- URL Aliases -->
868
+ <div class="endpoint card wide" id="url-aliases" data-reveal>
869
+ <h3>URL Aliases</h3>
870
+ <div class="method-url">
871
+ <span class="method post">POST</span>
872
+ <span class="url-path">/api/v1/mags-url-aliases</span>
873
+ </div>
874
+ <div class="method-url" style="margin-left:0.5rem">
875
+ <span class="method get">GET</span>
876
+ <span class="url-path">/api/v1/mags-url-aliases</span>
877
+ </div>
878
+ <div class="method-url" style="margin-left:0.5rem">
879
+ <span class="method delete">DELETE</span>
880
+ <span class="url-path">/api/v1/mags-url-aliases/:subdomain</span>
881
+ </div>
882
+ <p>Create stable, human-readable URL aliases for your sandboxes. Aliases point to a workspace and automatically follow the active VM.</p>
883
+
884
+ <p class="response-label">Create request body</p>
885
+ <table class="field-table">
886
+ <thead>
887
+ <tr><th>Field</th><th>Type</th><th>Required</th><th>Description</th></tr>
888
+ </thead>
889
+ <tbody>
890
+ <tr><td><code>subdomain</code></td><td>string</td><td><span class="required">YES</span></td><td>Subdomain for the alias (e.g. <code>my-api</code>).</td></tr>
891
+ <tr><td><code>workspace_id</code></td><td>string</td><td><span class="required">YES</span></td><td>Workspace to point the alias at.</td></tr>
892
+ <tr><td><code>domain</code></td><td>string</td><td>no</td><td>Custom domain (default: <code>apps.magpiecloud.com</code>).</td></tr>
893
+ </tbody>
894
+ </table>
895
+
896
+ <div class="code-tabs tab-group">
897
+ <p class="response-label">SDK examples</p>
898
+ <div class="tab-bar">
899
+ <button class="tab active" data-tab="ua-cli">CLI</button>
900
+ <button class="tab" data-tab="ua-py">Python</button>
901
+ <button class="tab" data-tab="ua-js">Node.js</button>
902
+ </div>
903
+ <div class="tab-content active" data-tab="ua-cli">
904
+ <pre><code># Create alias
905
+ mags url alias my-api myproject
906
+
907
+ # List aliases
908
+ mags url alias list
909
+
910
+ # Delete alias
911
+ mags url alias remove my-api</code></pre>
912
+ </div>
913
+ <div class="tab-content" data-tab="ua-py">
914
+ <pre><code>m.url_alias_create("my-api", "myproject")
915
+ aliases = m.url_alias_list()
916
+ m.url_alias_delete("my-api")</code></pre>
917
+ </div>
918
+ <div class="tab-content" data-tab="ua-js">
919
+ <pre><code>await mags.urlAliasCreate('my-api', 'myproject');
920
+ const aliases = await mags.urlAliasList();
921
+ await mags.urlAliasDelete('my-api');</code></pre>
922
+ </div>
923
+ </div>
924
+ </div>
925
+
797
926
  <!-- Cron Endpoints -->
798
927
  <div class="endpoint card wide" id="cron-endpoints" data-reveal>
799
928
  <h3>Cron Jobs</h3>
@@ -961,6 +1090,6 @@ mags cron remove &lt;id&gt;</code></pre>
961
1090
  </div>
962
1091
  </footer>
963
1092
 
964
- <script src="script.js?v=7"></script>
1093
+ <script src="script.js?v=8"></script>
965
1094
  </body>
966
1095
  </html>
@@ -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=6" />
13
+ <link rel="stylesheet" href="styles.css?v=7" />
14
14
  <script src="env.js"></script>
15
15
  </head>
16
16
  <body>
@@ -59,9 +59,8 @@
59
59
  </div>
60
60
  <div class="tab-content active" data-tab="hero-cli">
61
61
  <pre><code>mags run 'echo Hello World'
62
-
63
62
  mags run -w myproject -p 'pip install flask'
64
- mags ssh myproject</code></pre>
63
+ mags new dev && mags ssh dev</code></pre>
65
64
  <p class="card-note">Run a script, persist your files to the cloud, then jump in with SSH.</p>
66
65
  </div>
67
66
  <div class="tab-content" data-tab="hero-python">
@@ -186,14 +185,45 @@ mags login</code></pre>
186
185
  </div>
187
186
 
188
187
  <div class="ref-section">
189
- <h3>Commands</h3>
188
+ <h3>Running Scripts</h3>
189
+ <div class="ref-table-wrap">
190
+ <table class="ref-table">
191
+ <thead><tr><th>Command</th><th>Description</th></tr></thead>
192
+ <tbody>
193
+ <tr><td><code>mags run &lt;script&gt;</code></td><td>Run a script in a fresh sandbox. Fastest &mdash; no workspace, no persistence.</td></tr>
194
+ <tr><td><code>mags run -w &lt;name&gt; &lt;script&gt;</code></td><td>Run with a named workspace. Data stays on the VM only &mdash; deleted after 10 min idle.</td></tr>
195
+ <tr><td><code>mags run -w &lt;name&gt; -p &lt;script&gt;</code></td><td>Run with a persistent workspace. Files synced to S3 and survive across runs indefinitely.</td></tr>
196
+ <tr><td><code>mags run --url --port &lt;port&gt; &lt;script&gt;</code></td><td>Request a public HTTPS URL for your sandbox (requires <code>-p</code>).</td></tr>
197
+ <tr><td><code>mags run --no-sleep &lt;script&gt;</code></td><td>Keep sandbox running 24/7, never auto-sleep (requires <code>-p</code>).</td></tr>
198
+ <tr><td><code>mags run -e &lt;script&gt;</code></td><td>Ephemeral &mdash; no workspace at all, fastest possible execution.</td></tr>
199
+ <tr><td><code>mags run --base &lt;workspace&gt; &lt;script&gt;</code></td><td>Use an existing workspace as a read-only base image (OverlayFS).</td></tr>
200
+ <tr><td><code>mags run -f &lt;file&gt; &lt;script&gt;</code></td><td>Upload file(s) into the sandbox before running (repeatable).</td></tr>
201
+ </tbody>
202
+ </table>
203
+ </div>
204
+ </div>
205
+
206
+ <div class="ref-section">
207
+ <h3>Sandboxes</h3>
208
+ <div class="ref-table-wrap">
209
+ <table class="ref-table">
210
+ <thead><tr><th>Command</th><th>Description</th></tr></thead>
211
+ <tbody>
212
+ <tr><td><code>mags new &lt;name&gt;</code></td><td>Create a new sandbox. Workspace lives on local disk only.</td></tr>
213
+ <tr><td><code>mags new &lt;name&gt; -p</code></td><td>Create a sandbox with persistent workspace &mdash; synced to S3.</td></tr>
214
+ <tr><td><code>mags exec &lt;name&gt; &lt;cmd&gt;</code></td><td>Execute a command on an existing sandbox.</td></tr>
215
+ <tr><td><code>mags ssh &lt;name&gt;</code></td><td>SSH into a sandbox. Auto-starts if sleeping or stopped.</td></tr>
216
+ </tbody>
217
+ </table>
218
+ </div>
219
+ </div>
220
+
221
+ <div class="ref-section">
222
+ <h3>Management</h3>
190
223
  <div class="ref-table-wrap">
191
224
  <table class="ref-table">
192
225
  <thead><tr><th>Command</th><th>Description</th></tr></thead>
193
226
  <tbody>
194
- <tr><td><code>mags run &lt;script&gt;</code></td><td>Execute a script in a fresh sandbox</td></tr>
195
- <tr><td><code>mags ssh &lt;workspace&gt;</code></td><td>SSH into a sandbox (auto-starts if needed)</td></tr>
196
- <tr><td><code>mags exec &lt;workspace&gt; &lt;cmd&gt;</code></td><td>Run a command on an existing sandbox</td></tr>
197
227
  <tr><td><code>mags list</code></td><td>List recent jobs</td></tr>
198
228
  <tr><td><code>mags status &lt;id&gt;</code></td><td>Get job status</td></tr>
199
229
  <tr><td><code>mags logs &lt;id&gt;</code></td><td>Get job output</td></tr>
@@ -201,10 +231,16 @@ mags login</code></pre>
201
231
  <tr><td><code>mags set &lt;id&gt; [options]</code></td><td>Update VM settings (e.g. <code>--no-sleep</code>, <code>--sleep</code>)</td></tr>
202
232
  <tr><td><code>mags sync &lt;workspace&gt;</code></td><td>Sync workspace to the cloud now</td></tr>
203
233
  <tr><td><code>mags url &lt;id&gt; [port]</code></td><td>Enable public URL access</td></tr>
234
+ <tr><td><code>mags resize &lt;workspace&gt; --disk &lt;GB&gt;</code></td><td>Resize workspace disk</td></tr>
204
235
  <tr><td><code>mags workspace list</code></td><td>List persistent workspaces</td></tr>
205
236
  <tr><td><code>mags workspace delete &lt;id&gt;</code></td><td>Delete workspace + cloud data</td></tr>
237
+ <tr><td><code>mags url alias &lt;sub&gt; &lt;workspace&gt;</code></td><td>Create a stable URL alias</td></tr>
238
+ <tr><td><code>mags url alias list</code></td><td>List URL aliases</td></tr>
239
+ <tr><td><code>mags url alias remove &lt;sub&gt;</code></td><td>Delete a URL alias</td></tr>
206
240
  <tr><td><code>mags cron add [opts] &lt;script&gt;</code></td><td>Create a scheduled cron job</td></tr>
207
241
  <tr><td><code>mags cron list</code></td><td>List cron jobs</td></tr>
242
+ <tr><td><code>mags cron enable &lt;id&gt;</code></td><td>Enable a cron job</td></tr>
243
+ <tr><td><code>mags cron disable &lt;id&gt;</code></td><td>Disable a cron job</td></tr>
208
244
  <tr><td><code>mags cron remove &lt;id&gt;</code></td><td>Delete a cron job</td></tr>
209
245
  </tbody>
210
246
  </table>
@@ -212,21 +248,14 @@ mags login</code></pre>
212
248
  </div>
213
249
 
214
250
  <div class="ref-section">
215
- <h3>Run Flags</h3>
216
- <p><code>-w</code> and <code>-n</code> are aliases &mdash; both set the job name and workspace ID.</p>
251
+ <h3>Additional Flags</h3>
217
252
  <div class="ref-table-wrap">
218
253
  <table class="ref-table">
219
254
  <thead><tr><th>Flag</th><th>Description</th></tr></thead>
220
255
  <tbody>
221
- <tr><td><code>-w, --workspace &lt;name&gt;</code></td><td>Name the job/workspace. Data stays local to the VM only. Add <code>-p</code> to sync to S3 and persist.</td></tr>
222
256
  <tr><td><code>-n, --name &lt;name&gt;</code></td><td>Alias for <code>-w</code></td></tr>
223
- <tr><td><code>-p, --persistent</code></td><td>Keep sandbox alive after script, sync workspace to S3. Files persist across runs indefinitely.</td></tr>
224
- <tr><td><code>--base &lt;workspace&gt;</code></td><td>Clone a workspace as a read-only starting point (OverlayFS)</td></tr>
225
- <tr><td><code>--no-sleep</code></td><td>Never auto-sleep this VM (requires <code>-p</code>)</td></tr>
226
- <tr><td><code>-e, --ephemeral</code></td><td>No workspace, no sync &mdash; fastest execution</td></tr>
227
- <tr><td><code>-f, --file &lt;path&gt;</code></td><td>Upload file(s) into the sandbox (repeatable)</td></tr>
228
- <tr><td><code>--url</code></td><td>Enable public HTTPS URL (requires <code>-p</code>)</td></tr>
229
- <tr><td><code>--port &lt;port&gt;</code></td><td>Port to expose for URL (default: 8080)</td></tr>
257
+ <tr><td><code>-e, --ephemeral</code></td><td>No workspace at all, fastest possible execution</td></tr>
258
+ <tr><td><code>--base &lt;workspace&gt;</code></td><td>Use an existing workspace as a read-only base image</td></tr>
230
259
  <tr><td><code>--disk &lt;GB&gt;</code></td><td>Custom disk size in GB (default: 2)</td></tr>
231
260
  <tr><td><code>--startup-command &lt;cmd&gt;</code></td><td>Command to run when sandbox wakes from sleep</td></tr>
232
261
  </tbody>
@@ -236,43 +265,25 @@ mags login</code></pre>
236
265
 
237
266
  <div class="ref-section">
238
267
  <h3>Examples</h3>
239
- <pre><code># Run a one-off command
240
- mags run 'echo Hello World && uname -a'
241
-
242
- # Local workspace &mdash; data stays on VM only (no S3 sync)
243
- # Good for data analysis and throwaway work
244
- mags run -w analysis -f data.csv 'python3 analyze.py'
245
-
246
- # Persistent workspace &mdash; files synced to S3, survive across runs
268
+ <pre><code># Persistent workspace &mdash; install packages, then run your app
247
269
  mags run -w myproject -p 'pip install flask requests'
248
270
  mags run -w myproject -p 'python3 app.py'
249
271
 
250
- # Base image &mdash; create a golden image, sync it, then reuse
272
+ # Golden image &mdash; create once, fork many times
251
273
  mags run -w golden -p 'apk add nodejs npm && npm install -g typescript'
252
- mags sync golden # persist to cloud
253
- mags run --base golden 'npm test' # read-only, changes discarded
254
- mags run --base golden -w fork-1 -p 'npm test' # fork: load golden, save fork-1 to S3
274
+ mags sync golden
275
+ mags run --base golden -w fork-1 -p 'npm test'
255
276
 
256
- # SSH into a workspace (auto-starts sandbox if needed)
257
- mags ssh myproject
277
+ # Interactive sandbox with SSH
278
+ mags new dev -p
279
+ mags ssh dev
280
+ mags exec dev 'node --version'
258
281
 
259
- # Run a command on an existing sandbox
260
- mags exec myproject 'node --version'
261
-
262
- # Persistent sandbox with public URL
263
- mags run -w webapp -p --url --port 8080 \
282
+ # Always-on web server with public URL
283
+ mags run -w webapp -p --no-sleep --url --port 8080 \
264
284
  --startup-command 'python3 -m http.server 8080' \
265
285
  'python3 -m http.server 8080'
266
286
 
267
- # Always-on sandbox (never auto-sleeps)
268
- mags run -w worker -p --no-sleep 'python3 worker.py'
269
-
270
- # Upload files and run
271
- mags run -f script.py -f data.csv 'python3 script.py'
272
-
273
- # Ephemeral (fastest &mdash; no workspace at all)
274
- mags run -e 'uname -a && df -h'
275
-
276
287
  # Cron job
277
288
  mags cron add --name backup --schedule "0 0 * * *" \
278
289
  -w backups 'tar czf backup.tar.gz /data'</code></pre>
@@ -295,21 +306,28 @@ export MAGS_API_TOKEN="your-token"</code></pre>
295
306
  <tbody>
296
307
  <tr><td><code>run(script, **opts)</code></td><td>Submit a job (returns immediately)</td></tr>
297
308
  <tr><td><code>run_and_wait(script, **opts)</code></td><td>Submit + block until complete</td></tr>
298
- <tr><td><code>new(name, persistent=True)</code></td><td>Create VM sandbox (add persistent for S3)</td></tr>
299
- <tr><td><code>exec(name, command)</code></td><td>Run command on existing sandbox</td></tr>
309
+ <tr><td><code>new(name, **opts)</code></td><td>Create VM sandbox (pass <code>persistent=True</code> for S3)</td></tr>
310
+ <tr><td><code>exec(name, command)</code></td><td>Run command on existing sandbox via SSH</td></tr>
300
311
  <tr><td><code>stop(name_or_id)</code></td><td>Stop a running job</td></tr>
301
312
  <tr><td><code>find_job(name_or_id)</code></td><td>Find job by name or workspace</td></tr>
302
313
  <tr><td><code>url(name_or_id, port)</code></td><td>Enable public URL access</td></tr>
314
+ <tr><td><code>resize(workspace, disk_gb)</code></td><td>Resize workspace disk</td></tr>
303
315
  <tr><td><code>status(request_id)</code></td><td>Get job status</td></tr>
304
316
  <tr><td><code>logs(request_id)</code></td><td>Get job logs</td></tr>
305
317
  <tr><td><code>list_jobs()</code></td><td>List recent jobs</td></tr>
318
+ <tr><td><code>update_job(request_id, **opts)</code></td><td>Update job settings (<code>no_sleep</code>, <code>startup_command</code>)</td></tr>
306
319
  <tr><td><code>enable_access(id, port)</code></td><td>Enable URL or SSH access (low-level)</td></tr>
320
+ <tr><td><code>upload_file(path)</code></td><td>Upload a file, returns file ID</td></tr>
307
321
  <tr><td><code>upload_files(paths)</code></td><td>Upload files, returns file IDs</td></tr>
308
322
  <tr><td><code>list_workspaces()</code></td><td>List persistent workspaces</td></tr>
309
323
  <tr><td><code>delete_workspace(id)</code></td><td>Delete workspace + cloud data</td></tr>
310
324
  <tr><td><code>sync(request_id)</code></td><td>Sync workspace to S3 now</td></tr>
325
+ <tr><td><code>url_alias_create(sub, ws_id)</code></td><td>Create a stable URL alias</td></tr>
326
+ <tr><td><code>url_alias_list()</code></td><td>List URL aliases</td></tr>
327
+ <tr><td><code>url_alias_delete(sub)</code></td><td>Delete a URL alias</td></tr>
311
328
  <tr><td><code>cron_create(**opts)</code></td><td>Create a cron job</td></tr>
312
329
  <tr><td><code>cron_list()</code></td><td>List cron jobs</td></tr>
330
+ <tr><td><code>cron_update(id, **opts)</code></td><td>Update a cron job</td></tr>
313
331
  <tr><td><code>cron_delete(id)</code></td><td>Delete a cron job</td></tr>
314
332
  <tr><td><code>usage(window_days)</code></td><td>Get usage stats</td></tr>
315
333
  </tbody>
@@ -401,15 +419,28 @@ export MAGS_API_TOKEN="your-token"</code></pre>
401
419
  <tbody>
402
420
  <tr><td><code>run(script, opts)</code></td><td>Submit a job (returns immediately)</td></tr>
403
421
  <tr><td><code>runAndWait(script, opts)</code></td><td>Submit + block until complete</td></tr>
422
+ <tr><td><code>new(name, opts)</code></td><td>Create a VM sandbox (add <code>persistent: true</code> for S3)</td></tr>
423
+ <tr><td><code>exec(nameOrId, command)</code></td><td>Run command on existing sandbox via SSH</td></tr>
424
+ <tr><td><code>stop(nameOrId)</code></td><td>Stop a running job</td></tr>
425
+ <tr><td><code>findJob(nameOrId)</code></td><td>Find job by name or workspace</td></tr>
426
+ <tr><td><code>url(nameOrId, port)</code></td><td>Enable public URL access</td></tr>
404
427
  <tr><td><code>status(requestId)</code></td><td>Get job status</td></tr>
405
428
  <tr><td><code>logs(requestId)</code></td><td>Get job logs</td></tr>
406
- <tr><td><code>listJobs()</code></td><td>List recent jobs</td></tr>
407
- <tr><td><code>enableAccess(id, { port })</code></td><td>Enable URL or SSH access</td></tr>
408
- <tr><td><code>uploadFile(path)</code></td><td>Upload a file, returns file ID</td></tr>
429
+ <tr><td><code>list()</code></td><td>List recent jobs</td></tr>
430
+ <tr><td><code>updateJob(requestId, opts)</code></td><td>Update job settings (<code>noSleep</code>, <code>startupCommand</code>)</td></tr>
431
+ <tr><td><code>enableAccess(requestId, port)</code></td><td>Enable URL or SSH access</td></tr>
432
+ <tr><td><code>resize(workspace, diskGb)</code></td><td>Resize workspace disk</td></tr>
433
+ <tr><td><code>uploadFiles(paths)</code></td><td>Upload files, returns file IDs</td></tr>
434
+ <tr><td><code>sync(requestId)</code></td><td>Sync workspace to S3 now</td></tr>
435
+ <tr><td><code>listWorkspaces()</code></td><td>List persistent workspaces</td></tr>
436
+ <tr><td><code>deleteWorkspace(id)</code></td><td>Delete workspace + cloud data</td></tr>
437
+ <tr><td><code>urlAliasCreate(sub, wsId)</code></td><td>Create a stable URL alias</td></tr>
438
+ <tr><td><code>urlAliasList()</code></td><td>List URL aliases</td></tr>
439
+ <tr><td><code>urlAliasDelete(sub)</code></td><td>Delete a URL alias</td></tr>
409
440
  <tr><td><code>cronCreate(opts)</code></td><td>Create a cron job</td></tr>
410
441
  <tr><td><code>cronList()</code></td><td>List cron jobs</td></tr>
411
442
  <tr><td><code>cronDelete(id)</code></td><td>Delete a cron job</td></tr>
412
- <tr><td><code>usage(windowDays)</code></td><td>Get usage stats</td></tr>
443
+ <tr><td><code>usage(opts)</code></td><td>Get usage stats</td></tr>
413
444
  </tbody>
414
445
  </table>
415
446
  </div>
@@ -453,9 +484,12 @@ await mags.runAndWait('python3 app.py', { workspaceId: 'myproject', persistent:
453
484
  await mags.runAndWait('npm test', { baseWorkspaceId: 'golden' });
454
485
  await mags.runAndWait('npm test', { baseWorkspaceId: 'golden', workspaceId: 'fork-1', persistent: true });
455
486
 
487
+ // Create a sandbox
488
+ await mags.new('dev', { persistent: true });
489
+
456
490
  // SSH access
457
491
  const job = await mags.run('sleep 3600', { workspaceId: 'dev', persistent: true });
458
- const ssh = await mags.enableAccess(job.requestId, { port: 22 });
492
+ const ssh = await mags.enableAccess(job.requestId, 22);
459
493
  console.log(`ssh root@${ssh.sshHost} -p ${ssh.sshPort}`);
460
494
 
461
495
  // Public URL
@@ -463,8 +497,8 @@ const webJob = await mags.run('python3 -m http.server 8080', {
463
497
  workspaceId: 'webapp', persistent: true,
464
498
  startupCommand: 'python3 -m http.server 8080',
465
499
  });
466
- const access = await mags.enableAccess(webJob.requestId, { port: 8080 });
467
- console.log(access.url);
500
+ const { url } = await mags.url('webapp', 8080);
501
+ console.log(url);
468
502
 
469
503
  // Always-on sandbox (never auto-sleeps)
470
504
  await mags.run('python3 worker.py', {
@@ -593,13 +627,14 @@ print(result["status"]) # "completed"</code></pre>
593
627
  <ul class="list">
594
628
  <li><code>run(script, **opts)</code> &mdash; submit a job</li>
595
629
  <li><code>run_and_wait(script, **opts)</code> &mdash; submit + block</li>
596
- <li><code>new(name, persistent=True)</code> &mdash; create VM sandbox (add persistent for S3)</li>
630
+ <li><code>new(name, **opts)</code> &mdash; create VM sandbox</li>
597
631
  <li><code>exec(name, command)</code> &mdash; run on existing sandbox</li>
598
632
  <li><code>stop(name_or_id)</code> &mdash; stop a job</li>
599
633
  <li><code>find_job(name_or_id)</code> &mdash; find by name/workspace</li>
600
634
  <li><code>url(name_or_id, port)</code> &mdash; enable public URL</li>
635
+ <li><code>resize(workspace, disk_gb)</code> &mdash; resize disk</li>
601
636
  <li><code>status(id)</code> / <code>logs(id)</code> / <code>list_jobs()</code></li>
602
- <li><code>upload_files(paths)</code> &mdash; upload files</li>
637
+ <li><code>upload_file(path)</code> / <code>upload_files(paths)</code></li>
603
638
  <li><code>list_workspaces()</code> / <code>delete_workspace(id)</code></li>
604
639
  <li><code>sync(id)</code> &mdash; sync workspace to S3</li>
605
640
  <li><code>cron_create(**opts)</code> / <code>cron_list()</code> / <code>cron_delete(id)</code></li>
@@ -629,14 +664,17 @@ console.log(result.logs);</code></pre>
629
664
  <ul class="list">
630
665
  <li><code>run(script, opts)</code> &mdash; submit a job</li>
631
666
  <li><code>runAndWait(script, opts)</code> &mdash; submit + block</li>
632
- <li><code>status(requestId)</code> &mdash; get job status</li>
633
- <li><code>logs(requestId)</code> &mdash; get job logs</li>
634
- <li><code>listJobs()</code> &mdash; list recent jobs</li>
635
- <li><code>enableAccess(id, { port })</code> &mdash; URL or SSH</li>
636
- <li><code>uploadFile(path)</code> &mdash; upload a file</li>
637
- <li><code>cronCreate(opts)</code> &mdash; create cron</li>
638
- <li><code>cronList()</code> / <code>cronDelete(id)</code></li>
639
- <li><code>usage(windowDays)</code> &mdash; usage stats</li>
667
+ <li><code>new(name, opts)</code> &mdash; create VM sandbox</li>
668
+ <li><code>exec(nameOrId, command)</code> &mdash; run on existing sandbox</li>
669
+ <li><code>stop(nameOrId)</code> &mdash; stop a job</li>
670
+ <li><code>findJob(nameOrId)</code> &mdash; find by name/workspace</li>
671
+ <li><code>url(nameOrId, port)</code> &mdash; enable public URL</li>
672
+ <li><code>status(id)</code> / <code>logs(id)</code> / <code>list()</code></li>
673
+ <li><code>enableAccess(requestId, port)</code> &mdash; URL or SSH</li>
674
+ <li><code>uploadFiles(paths)</code> &mdash; upload files</li>
675
+ <li><code>listWorkspaces()</code> / <code>deleteWorkspace(id)</code></li>
676
+ <li><code>sync(id)</code> &mdash; sync workspace to S3</li>
677
+ <li><code>cronCreate(opts)</code> / <code>cronList()</code> / <code>cronDelete(id)</code></li>
640
678
  </ul>
641
679
  <p style="margin-top:1rem"><a class="text-link" href="https://www.npmjs.com/package/@magpiecloud/mags" rel="noreferrer">npm &rarr;</a></p>
642
680
  </article>
@@ -663,12 +701,18 @@ console.log(result.logs);</code></pre>
663
701
  <li><code>GET /mags-jobs/:id/status</code> &mdash; status</li>
664
702
  <li><code>GET /mags-jobs/:id/logs</code> &mdash; logs</li>
665
703
  <li><code>POST /mags-jobs/:id/access</code> &mdash; URL/SSH</li>
704
+ <li><code>POST /mags-jobs/:id/stop</code> &mdash; stop job</li>
705
+ <li><code>POST /mags-jobs/:id/sync</code> &mdash; sync workspace</li>
666
706
  <li><code>PATCH /mags-jobs/:id</code> &mdash; update</li>
667
707
  <li><code>POST /mags-files</code> &mdash; upload file</li>
668
708
  <li><code>GET /mags-workspaces</code> &mdash; list ws</li>
669
709
  <li><code>DELETE /mags-workspaces/:id</code> &mdash; delete ws</li>
710
+ <li><code>POST /mags-url-aliases</code> &mdash; create alias</li>
711
+ <li><code>GET /mags-url-aliases</code> &mdash; list aliases</li>
712
+ <li><code>DELETE /mags-url-aliases/:sub</code> &mdash; delete alias</li>
670
713
  <li><code>POST /mags-cron</code> &mdash; create cron</li>
671
714
  <li><code>GET /mags-cron</code> &mdash; list cron</li>
715
+ <li><code>PATCH /mags-cron/:id</code> &mdash; update cron</li>
672
716
  <li><code>DELETE /mags-cron/:id</code> &mdash; delete cron</li>
673
717
  </ul>
674
718
  <p style="margin-top:1rem"><a class="text-link" href="api.html">Full API reference &rarr;</a></p>
@@ -741,7 +785,7 @@ console.log(result.logs);</code></pre>
741
785
  </div>
742
786
  </footer>
743
787
 
744
- <script src="script.js?v=7"></script>
788
+ <script src="script.js?v=8"></script>
745
789
  <script>
746
790
  (function() {
747
791
  var token = localStorage.getItem('microvm-access-token');