@magpiecloud/mags 1.8.2 → 1.8.4
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 +165 -2
- package/package.json +1 -1
- package/python/dist/magpie_mags-1.3.0-py3-none-any.whl +0 -0
- package/python/dist/magpie_mags-1.3.0.tar.gz +0 -0
- package/python/pyproject.toml +1 -1
- package/python/src/mags/client.py +57 -0
- package/python/dist/magpie_mags-1.2.0-py3-none-any.whl +0 -0
- package/python/dist/magpie_mags-1.2.0.tar.gz +0 -0
package/bin/mags.js
CHANGED
|
@@ -207,7 +207,11 @@ ${colors.bold}Commands:${colors.reset}
|
|
|
207
207
|
logs <name|id> Get job logs
|
|
208
208
|
list List recent jobs
|
|
209
209
|
url <name|id> [port] Enable URL access for a job
|
|
210
|
+
url alias <sub> <workspace> Create a stable URL alias for a workspace
|
|
211
|
+
url alias list List your URL aliases
|
|
212
|
+
url alias remove <subdomain> Delete a URL alias
|
|
210
213
|
stop <name|id> Stop a running job
|
|
214
|
+
resize <workspace> --disk <GB> Resize a workspace's disk (restarts VM)
|
|
211
215
|
sync <workspace|id> Sync workspace to S3 (without stopping)
|
|
212
216
|
workspace list List persistent workspaces
|
|
213
217
|
workspace delete <id> Delete a workspace and its S3 data
|
|
@@ -259,6 +263,10 @@ ${colors.bold}Examples:${colors.reset}
|
|
|
259
263
|
mags status myvm
|
|
260
264
|
mags logs myvm
|
|
261
265
|
mags url myvm 8080
|
|
266
|
+
mags url alias my-api myvm # Stable URL: my-api.apps.magpiecloud.com
|
|
267
|
+
mags url alias my-api myvm --lfg # Stable URL: my-api.app.lfg.run
|
|
268
|
+
mags url alias list # List all aliases
|
|
269
|
+
mags url alias remove my-api # Remove alias
|
|
262
270
|
mags setup-claude # Install Claude Code skill
|
|
263
271
|
`);
|
|
264
272
|
process.exit(1);
|
|
@@ -667,6 +675,98 @@ async function enableUrlAccess(nameOrId, port = 8080) {
|
|
|
667
675
|
}
|
|
668
676
|
}
|
|
669
677
|
|
|
678
|
+
async function urlAliasCommand(args) {
|
|
679
|
+
if (args.length === 0) {
|
|
680
|
+
log('red', 'Error: URL alias subcommand required');
|
|
681
|
+
console.log('\nUsage:');
|
|
682
|
+
console.log(' mags url alias <subdomain> <workspace> [--lfg]');
|
|
683
|
+
console.log(' mags url alias list');
|
|
684
|
+
console.log(' mags url alias remove <subdomain>');
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const subcommand = args[0];
|
|
689
|
+
|
|
690
|
+
switch (subcommand) {
|
|
691
|
+
case 'list':
|
|
692
|
+
case 'ls':
|
|
693
|
+
await urlAliasList();
|
|
694
|
+
break;
|
|
695
|
+
case 'remove':
|
|
696
|
+
case 'rm':
|
|
697
|
+
case 'delete':
|
|
698
|
+
if (!args[1]) {
|
|
699
|
+
log('red', 'Error: Subdomain required');
|
|
700
|
+
console.log('\nUsage: mags url alias remove <subdomain>');
|
|
701
|
+
process.exit(1);
|
|
702
|
+
}
|
|
703
|
+
await urlAliasRemove(args[1]);
|
|
704
|
+
break;
|
|
705
|
+
default:
|
|
706
|
+
// mags url alias <subdomain> <workspace> [--lfg]
|
|
707
|
+
const subdomain = args[0];
|
|
708
|
+
const workspace = args[1];
|
|
709
|
+
if (!workspace) {
|
|
710
|
+
log('red', 'Error: Workspace required');
|
|
711
|
+
console.log('\nUsage: mags url alias <subdomain> <workspace> [--lfg]');
|
|
712
|
+
process.exit(1);
|
|
713
|
+
}
|
|
714
|
+
const useLfg = args.includes('--lfg');
|
|
715
|
+
await urlAliasCreate(subdomain, workspace, useLfg);
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async function urlAliasCreate(subdomain, workspaceId, useLfg) {
|
|
721
|
+
const domain = useLfg ? 'app.lfg.run' : 'apps.magpiecloud.com';
|
|
722
|
+
log('blue', `Creating URL alias: ${subdomain}.${domain} → workspace '${workspaceId}'...`);
|
|
723
|
+
|
|
724
|
+
const resp = await request('POST', '/api/v1/mags-url-aliases', {
|
|
725
|
+
subdomain,
|
|
726
|
+
workspace_id: workspaceId,
|
|
727
|
+
domain,
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
if (resp.error) {
|
|
731
|
+
log('red', `Error: ${resp.error}`);
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (resp.url) {
|
|
736
|
+
log('green', `URL alias created: ${resp.url}`);
|
|
737
|
+
} else {
|
|
738
|
+
log('green', `URL alias created: https://${subdomain}.${domain}`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
async function urlAliasList() {
|
|
743
|
+
const resp = await request('GET', '/api/v1/mags-url-aliases');
|
|
744
|
+
const aliases = resp.aliases || [];
|
|
745
|
+
|
|
746
|
+
if (aliases.length > 0) {
|
|
747
|
+
log('cyan', 'URL Aliases:\n');
|
|
748
|
+
aliases.forEach(a => {
|
|
749
|
+
console.log(` ${colors.bold}${a.subdomain}${colors.reset}`);
|
|
750
|
+
console.log(` URL: ${colors.green}${a.url}${colors.reset}`);
|
|
751
|
+
console.log(` Workspace: ${a.workspace_id} Domain: ${a.domain}`);
|
|
752
|
+
console.log('');
|
|
753
|
+
});
|
|
754
|
+
log('gray', `Total: ${aliases.length} alias(es)`);
|
|
755
|
+
} else {
|
|
756
|
+
log('yellow', 'No URL aliases found');
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async function urlAliasRemove(subdomain) {
|
|
761
|
+
log('blue', `Removing URL alias '${subdomain}'...`);
|
|
762
|
+
const resp = await request('DELETE', `/api/v1/mags-url-aliases/${subdomain}`);
|
|
763
|
+
if (resp.error) {
|
|
764
|
+
log('red', `Error: ${resp.error}`);
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|
|
767
|
+
log('green', resp.message || `URL alias '${subdomain}' removed`);
|
|
768
|
+
}
|
|
769
|
+
|
|
670
770
|
async function getStatus(nameOrId) {
|
|
671
771
|
if (!nameOrId) {
|
|
672
772
|
log('red', 'Error: Job name or ID required');
|
|
@@ -730,6 +830,61 @@ async function stopJob(nameOrId) {
|
|
|
730
830
|
}
|
|
731
831
|
}
|
|
732
832
|
|
|
833
|
+
async function resizeVM(args) {
|
|
834
|
+
let name = null;
|
|
835
|
+
let diskGB = 0;
|
|
836
|
+
|
|
837
|
+
for (let i = 0; i < args.length; i++) {
|
|
838
|
+
if (args[i] === '--disk' && args[i + 1]) {
|
|
839
|
+
diskGB = parseInt(args[++i]) || 0;
|
|
840
|
+
} else if (!name) {
|
|
841
|
+
name = args[i];
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
if (!name || !diskGB) {
|
|
846
|
+
log('red', 'Error: Workspace name and --disk <GB> required');
|
|
847
|
+
console.log('\nUsage: mags resize <workspace> --disk <GB>\n');
|
|
848
|
+
process.exit(1);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Find existing job for this workspace
|
|
852
|
+
const existingJob = await findWorkspaceJob(name);
|
|
853
|
+
if (existingJob) {
|
|
854
|
+
// Sync workspace before stopping (preserve files)
|
|
855
|
+
if (existingJob.status === 'running') {
|
|
856
|
+
log('blue', 'Syncing workspace before resize...');
|
|
857
|
+
await request('POST', `/api/v1/mags-jobs/${existingJob.request_id}/sync`);
|
|
858
|
+
}
|
|
859
|
+
log('blue', `Stopping existing VM...`);
|
|
860
|
+
await request('POST', `/api/v1/mags-jobs/${existingJob.request_id}/stop`);
|
|
861
|
+
// Brief wait for the stop to complete
|
|
862
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Create new VM with the same workspace name and new disk size
|
|
866
|
+
log('blue', `Creating new VM with ${diskGB}GB disk...`);
|
|
867
|
+
const payload = {
|
|
868
|
+
script: 'sleep infinity',
|
|
869
|
+
type: 'inline',
|
|
870
|
+
persistent: true,
|
|
871
|
+
name: name,
|
|
872
|
+
workspace_id: name,
|
|
873
|
+
startup_command: 'sleep infinity',
|
|
874
|
+
disk_gb: diskGB,
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
const response = await request('POST', '/api/v1/mags-jobs', payload);
|
|
878
|
+
if (!response.request_id) {
|
|
879
|
+
log('red', 'Failed to create VM:');
|
|
880
|
+
console.log(JSON.stringify(response, null, 2));
|
|
881
|
+
process.exit(1);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
log('green', `Resized '${name}' to ${diskGB}GB disk`);
|
|
885
|
+
log('gray', `Job: ${response.request_id}`);
|
|
886
|
+
}
|
|
887
|
+
|
|
733
888
|
async function syncWorkspace(nameOrId) {
|
|
734
889
|
if (!nameOrId) {
|
|
735
890
|
log('red', 'Error: Workspace name or job ID required');
|
|
@@ -1366,7 +1521,7 @@ async function main() {
|
|
|
1366
1521
|
break;
|
|
1367
1522
|
case '--version':
|
|
1368
1523
|
case '-v':
|
|
1369
|
-
console.log('mags v1.8.
|
|
1524
|
+
console.log('mags v1.8.3');
|
|
1370
1525
|
process.exit(0);
|
|
1371
1526
|
break;
|
|
1372
1527
|
case 'new':
|
|
@@ -1387,7 +1542,11 @@ async function main() {
|
|
|
1387
1542
|
break;
|
|
1388
1543
|
case 'url':
|
|
1389
1544
|
await requireAuth();
|
|
1390
|
-
|
|
1545
|
+
if (args[1] === 'alias') {
|
|
1546
|
+
await urlAliasCommand(args.slice(2));
|
|
1547
|
+
} else {
|
|
1548
|
+
await enableUrlAccess(args[1], parseInt(args[2]) || 8080);
|
|
1549
|
+
}
|
|
1391
1550
|
break;
|
|
1392
1551
|
case 'status':
|
|
1393
1552
|
await requireAuth();
|
|
@@ -1405,6 +1564,10 @@ async function main() {
|
|
|
1405
1564
|
await requireAuth();
|
|
1406
1565
|
await stopJob(args[1]);
|
|
1407
1566
|
break;
|
|
1567
|
+
case 'resize':
|
|
1568
|
+
await requireAuth();
|
|
1569
|
+
await resizeVM(args.slice(1));
|
|
1570
|
+
break;
|
|
1408
1571
|
case 'sync':
|
|
1409
1572
|
await requireAuth();
|
|
1410
1573
|
await syncWorkspace(args[1]);
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
package/python/pyproject.toml
CHANGED
|
@@ -211,6 +211,30 @@ class Mags:
|
|
|
211
211
|
request_id = self._resolve_job_id(name_or_id)
|
|
212
212
|
return self._request("POST", f"/mags-jobs/{request_id}/stop")
|
|
213
213
|
|
|
214
|
+
def resize(
|
|
215
|
+
self,
|
|
216
|
+
workspace: str,
|
|
217
|
+
disk_gb: int,
|
|
218
|
+
*,
|
|
219
|
+
timeout: float = 30.0,
|
|
220
|
+
poll_interval: float = 1.0,
|
|
221
|
+
) -> dict:
|
|
222
|
+
"""Resize a workspace's disk. Stops the existing VM, then creates a new one.
|
|
223
|
+
|
|
224
|
+
Workspace files are preserved in S3.
|
|
225
|
+
Returns ``{"request_id": ..., "status": "running"}``.
|
|
226
|
+
"""
|
|
227
|
+
existing = self.find_job(workspace)
|
|
228
|
+
if existing and existing.get("status") == "running":
|
|
229
|
+
self._request("POST", f"/mags-jobs/{existing['request_id']}/sync")
|
|
230
|
+
self._request("POST", f"/mags-jobs/{existing['request_id']}/stop")
|
|
231
|
+
time.sleep(1)
|
|
232
|
+
elif existing and existing.get("status") == "sleeping":
|
|
233
|
+
self._request("POST", f"/mags-jobs/{existing['request_id']}/stop")
|
|
234
|
+
time.sleep(1)
|
|
235
|
+
|
|
236
|
+
return self.new(workspace, disk_gb=disk_gb, timeout=timeout, poll_interval=poll_interval)
|
|
237
|
+
|
|
214
238
|
def new(
|
|
215
239
|
self,
|
|
216
240
|
name: str,
|
|
@@ -483,3 +507,36 @@ class Mags:
|
|
|
483
507
|
def cron_delete(self, cron_id: str) -> dict:
|
|
484
508
|
"""Delete a cron job."""
|
|
485
509
|
return self._request("DELETE", f"/mags-cron/{cron_id}")
|
|
510
|
+
|
|
511
|
+
# ── URL aliases ──────────────────────────────────────────────────
|
|
512
|
+
|
|
513
|
+
def url_alias_create(
|
|
514
|
+
self,
|
|
515
|
+
subdomain: str,
|
|
516
|
+
workspace_id: str,
|
|
517
|
+
domain: str = "apps.magpiecloud.com",
|
|
518
|
+
) -> dict:
|
|
519
|
+
"""Create a stable URL alias for a workspace.
|
|
520
|
+
|
|
521
|
+
The alias maps ``subdomain.<domain>`` to the active job in the workspace.
|
|
522
|
+
Use ``domain="app.lfg.run"`` for the LFG domain.
|
|
523
|
+
|
|
524
|
+
Returns ``{"id": ..., "subdomain": ..., "url": ...}``.
|
|
525
|
+
"""
|
|
526
|
+
return self._request(
|
|
527
|
+
"POST",
|
|
528
|
+
"/mags-url-aliases",
|
|
529
|
+
json={
|
|
530
|
+
"subdomain": subdomain,
|
|
531
|
+
"workspace_id": workspace_id,
|
|
532
|
+
"domain": domain,
|
|
533
|
+
},
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
def url_alias_list(self) -> dict:
|
|
537
|
+
"""List all URL aliases. Returns ``{"aliases": [...], "total": N}``."""
|
|
538
|
+
return self._request("GET", "/mags-url-aliases")
|
|
539
|
+
|
|
540
|
+
def url_alias_delete(self, subdomain: str) -> dict:
|
|
541
|
+
"""Delete a URL alias by subdomain."""
|
|
542
|
+
return self._request("DELETE", f"/mags-url-aliases/{subdomain}")
|
|
Binary file
|
|
Binary file
|