@magpiecloud/mags 1.8.16 → 1.9.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/API.md +388 -0
- package/Mags-API.postman_collection.json +374 -0
- package/QUICKSTART.md +295 -0
- package/README.md +378 -95
- package/bin/mags.js +79 -44
- package/deploy-page.sh +171 -0
- package/index.js +0 -2
- package/mags +0 -0
- package/mags.sh +270 -0
- package/nodejs/README.md +197 -0
- package/nodejs/bin/mags.js +1882 -0
- package/nodejs/index.js +602 -0
- package/nodejs/package.json +45 -0
- package/package.json +3 -18
- package/python/INTEGRATION.md +800 -0
- package/python/README.md +161 -0
- package/python/dist/magpie_mags-1.3.8-py3-none-any.whl +0 -0
- package/python/dist/magpie_mags-1.3.8.tar.gz +0 -0
- package/python/examples/demo.py +181 -0
- package/python/pyproject.toml +39 -0
- package/python/src/magpie_mags.egg-info/PKG-INFO +186 -0
- package/python/src/magpie_mags.egg-info/SOURCES.txt +9 -0
- package/python/src/magpie_mags.egg-info/dependency_links.txt +1 -0
- package/python/src/magpie_mags.egg-info/requires.txt +1 -0
- package/python/src/magpie_mags.egg-info/top_level.txt +1 -0
- package/python/src/mags/__init__.py +6 -0
- package/python/src/mags/client.py +527 -0
- package/python/test_sdk.py +78 -0
- package/skill.md +153 -0
- package/test-sdk.js +68 -0
- package/website/api.html +1095 -0
- package/website/claude-skill.html +481 -0
- package/website/cookbook/hn-marketing.html +410 -0
- package/website/cookbook/hn-marketing.sh +42 -0
- package/website/cookbook.html +282 -0
- package/website/docs.html +677 -0
- package/website/env.js +4 -0
- package/website/index.html +801 -0
- package/website/llms.txt +334 -0
- package/website/login.html +108 -0
- package/website/mags.md +210 -0
- package/website/script.js +453 -0
- package/website/styles.css +1075 -0
- package/website/tokens.html +169 -0
- package/website/usage.html +185 -0
package/bin/mags.js
CHANGED
|
@@ -48,6 +48,21 @@ const config = loadConfig();
|
|
|
48
48
|
const API_URL = process.env.MAGS_API_URL || config.api_url || 'https://api.magpiecloud.com';
|
|
49
49
|
let API_TOKEN = process.env.MAGS_API_TOKEN || config.api_token || '';
|
|
50
50
|
|
|
51
|
+
// V2 flag: --v2 switches API paths from /api/v1/ to /api/v2/
|
|
52
|
+
let apiVersion = process.env.MAGS_API_VERSION || 'v1';
|
|
53
|
+
{
|
|
54
|
+
const v2Index = process.argv.indexOf('--v2');
|
|
55
|
+
if (v2Index !== -1) {
|
|
56
|
+
apiVersion = 'v2';
|
|
57
|
+
process.argv.splice(v2Index, 1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Helper to build versioned API paths
|
|
62
|
+
function apiPath(resource) {
|
|
63
|
+
return `/api/${apiVersion}/${resource}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
51
66
|
// Colors
|
|
52
67
|
const colors = {
|
|
53
68
|
red: '\x1b[31m',
|
|
@@ -147,7 +162,7 @@ async function resolveJobId(nameOrId) {
|
|
|
147
162
|
}
|
|
148
163
|
|
|
149
164
|
// Otherwise, search by name/workspace
|
|
150
|
-
const resp = await request('GET',
|
|
165
|
+
const resp = await request('GET', `${apiPath('mags-jobs')}?page=1&page_size=50`);
|
|
151
166
|
if (resp.jobs && resp.jobs.length > 0) {
|
|
152
167
|
// Try exact match on name first (prefer running/sleeping)
|
|
153
168
|
let job = resp.jobs.find(j => j.name === nameOrId && (j.status === 'running' || j.status === 'sleeping'));
|
|
@@ -178,7 +193,7 @@ async function resolveJobId(nameOrId) {
|
|
|
178
193
|
|
|
179
194
|
// Find a running or sleeping job for a workspace
|
|
180
195
|
async function findWorkspaceJob(workspace) {
|
|
181
|
-
const resp = await request('GET',
|
|
196
|
+
const resp = await request('GET', `${apiPath('mags-jobs')}?page=1&page_size=50`);
|
|
182
197
|
if (resp.jobs && resp.jobs.length > 0) {
|
|
183
198
|
// Prefer running, then sleeping
|
|
184
199
|
let job = resp.jobs.find(j => j.workspace_id === workspace && j.status === 'running');
|
|
@@ -230,6 +245,7 @@ ${colors.bold}Run Options:${colors.reset}
|
|
|
230
245
|
--url Enable public URL access (requires -p)
|
|
231
246
|
--port <port> Port to expose for URL (default: 8080)
|
|
232
247
|
--startup-command <cmd> Command to run when VM wakes from sleep
|
|
248
|
+
-t, --type <type> VM rootfs type: standard, claude, pi
|
|
233
249
|
|
|
234
250
|
${colors.bold}Cron Commands:${colors.reset}
|
|
235
251
|
cron add [options] <script> Create a scheduled cron job
|
|
@@ -310,7 +326,7 @@ To authenticate, you need an API token from Magpie.
|
|
|
310
326
|
log('blue', 'Verifying token...');
|
|
311
327
|
|
|
312
328
|
try {
|
|
313
|
-
const resp = await request('GET', '
|
|
329
|
+
const resp = await request('GET', `${apiPath('mags-jobs')}?page=1&page_size=1`);
|
|
314
330
|
|
|
315
331
|
if (resp.error === 'unauthorized' || resp.error === 'Unauthorized') {
|
|
316
332
|
log('red', 'Invalid token. Please check and try again.');
|
|
@@ -364,7 +380,7 @@ async function whoami() {
|
|
|
364
380
|
log('blue', 'Checking authentication...');
|
|
365
381
|
|
|
366
382
|
try {
|
|
367
|
-
const resp = await request('GET', '
|
|
383
|
+
const resp = await request('GET', `${apiPath('mags-jobs')}?page=1&page_size=1`);
|
|
368
384
|
|
|
369
385
|
if (resp.error === 'unauthorized' || resp.error === 'Unauthorized') {
|
|
370
386
|
log('red', 'Token is invalid or expired.');
|
|
@@ -409,6 +425,7 @@ async function newVM(args) {
|
|
|
409
425
|
let baseWorkspace = null;
|
|
410
426
|
let diskGB = 0;
|
|
411
427
|
let persistent = false;
|
|
428
|
+
let rootfsType = '';
|
|
412
429
|
|
|
413
430
|
for (let i = 0; i < args.length; i++) {
|
|
414
431
|
if (args[i] === '-p' || args[i] === '--persistent') {
|
|
@@ -417,6 +434,8 @@ async function newVM(args) {
|
|
|
417
434
|
baseWorkspace = args[++i];
|
|
418
435
|
} else if (args[i] === '--disk' && args[i + 1]) {
|
|
419
436
|
diskGB = parseInt(args[++i]) || 0;
|
|
437
|
+
} else if ((args[i] === '--type' || args[i] === '-t') && args[i + 1]) {
|
|
438
|
+
rootfsType = args[++i];
|
|
420
439
|
} else if (!name) {
|
|
421
440
|
name = args[i];
|
|
422
441
|
}
|
|
@@ -424,14 +443,15 @@ async function newVM(args) {
|
|
|
424
443
|
|
|
425
444
|
if (!name) {
|
|
426
445
|
log('red', 'Error: Name required');
|
|
427
|
-
console.log(`\nUsage: mags new <name> [-p] [--base <workspace>] [--disk <GB>]`);
|
|
428
|
-
console.log(` -p, --persistent Enable S3 data persistence
|
|
446
|
+
console.log(`\nUsage: mags new <name> [-p] [--base <workspace>] [--disk <GB>] [--type <type>]`);
|
|
447
|
+
console.log(` -p, --persistent Enable S3 data persistence`);
|
|
448
|
+
console.log(` -t, --type VM type: standard (default), claude, pi\n`);
|
|
429
449
|
process.exit(1);
|
|
430
450
|
}
|
|
431
451
|
|
|
432
452
|
// Check if a VM with this name already exists (running or sleeping)
|
|
433
453
|
try {
|
|
434
|
-
const jobs = await request('GET', '
|
|
454
|
+
const jobs = await request('GET', apiPath('mags-jobs'));
|
|
435
455
|
const existing = (jobs.jobs || []).find(j =>
|
|
436
456
|
j.name === name && (j.status === 'running' || j.status === 'sleeping')
|
|
437
457
|
);
|
|
@@ -452,13 +472,13 @@ async function newVM(args) {
|
|
|
452
472
|
persistent: true,
|
|
453
473
|
name: name,
|
|
454
474
|
workspace_id: name,
|
|
455
|
-
startup_command: 'sleep infinity'
|
|
456
|
-
no_sync: !persistent
|
|
475
|
+
startup_command: 'sleep infinity'
|
|
457
476
|
};
|
|
458
477
|
if (baseWorkspace) payload.base_workspace_id = baseWorkspace;
|
|
459
478
|
if (diskGB) payload.disk_gb = diskGB;
|
|
479
|
+
if (rootfsType) payload.rootfs_type = rootfsType;
|
|
460
480
|
|
|
461
|
-
const response = await request('POST', '
|
|
481
|
+
const response = await request('POST', apiPath('mags-jobs'), payload);
|
|
462
482
|
|
|
463
483
|
if (!response.request_id) {
|
|
464
484
|
log('red', 'Failed to create VM:');
|
|
@@ -471,7 +491,7 @@ async function newVM(args) {
|
|
|
471
491
|
let attempt = 0;
|
|
472
492
|
|
|
473
493
|
while (attempt < maxAttempts) {
|
|
474
|
-
const status = await request('GET',
|
|
494
|
+
const status = await request('GET', `${apiPath('mags-jobs')}/${response.request_id}/status`);
|
|
475
495
|
|
|
476
496
|
if (status.status === 'running') {
|
|
477
497
|
log('green', `VM '${name}' created successfully${persistent ? ' (persistent)' : ' (local disk)'}`);
|
|
@@ -506,6 +526,7 @@ async function runJob(args) {
|
|
|
506
526
|
let startupCommand = '';
|
|
507
527
|
let diskGB = 0;
|
|
508
528
|
let fileArgs = [];
|
|
529
|
+
let rootfsType = '';
|
|
509
530
|
|
|
510
531
|
// Parse flags
|
|
511
532
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -546,6 +567,10 @@ async function runJob(args) {
|
|
|
546
567
|
case '--startup-command':
|
|
547
568
|
startupCommand = args[++i];
|
|
548
569
|
break;
|
|
570
|
+
case '--type':
|
|
571
|
+
case '-t':
|
|
572
|
+
rootfsType = args[++i];
|
|
573
|
+
break;
|
|
549
574
|
default:
|
|
550
575
|
script = args.slice(i).join(' ');
|
|
551
576
|
i = args.length;
|
|
@@ -608,8 +633,9 @@ async function runJob(args) {
|
|
|
608
633
|
if (startupCommand) payload.startup_command = startupCommand;
|
|
609
634
|
if (fileIds.length > 0) payload.file_ids = fileIds;
|
|
610
635
|
if (diskGB) payload.disk_gb = diskGB;
|
|
636
|
+
if (rootfsType) payload.rootfs_type = rootfsType;
|
|
611
637
|
|
|
612
|
-
const response = await request('POST', '
|
|
638
|
+
const response = await request('POST', apiPath('mags-jobs'), payload);
|
|
613
639
|
|
|
614
640
|
if (!response.request_id) {
|
|
615
641
|
log('red', 'Failed to submit job:');
|
|
@@ -628,7 +654,7 @@ async function runJob(args) {
|
|
|
628
654
|
let attempt = 0;
|
|
629
655
|
|
|
630
656
|
while (attempt < maxAttempts) {
|
|
631
|
-
const status = await request('GET',
|
|
657
|
+
const status = await request('GET', `${apiPath('mags-jobs')}/${requestId}/status`);
|
|
632
658
|
|
|
633
659
|
if (status.status === 'completed') {
|
|
634
660
|
log('green', `Completed in ${status.script_duration_ms}ms`);
|
|
@@ -646,7 +672,7 @@ async function runJob(args) {
|
|
|
646
672
|
|
|
647
673
|
if (enableUrl && status.subdomain) {
|
|
648
674
|
log('blue', `Enabling URL access on port ${port}...`);
|
|
649
|
-
const accessResp = await request('POST',
|
|
675
|
+
const accessResp = await request('POST', `${apiPath('mags-jobs')}/${requestId}/access`, { port });
|
|
650
676
|
if (accessResp.success) {
|
|
651
677
|
log('green', `URL: https://${status.subdomain}.apps.magpiecloud.com`);
|
|
652
678
|
} else {
|
|
@@ -669,7 +695,7 @@ async function runJob(args) {
|
|
|
669
695
|
|
|
670
696
|
// Get logs
|
|
671
697
|
log('cyan', 'Output:');
|
|
672
|
-
const logsResp = await request('GET',
|
|
698
|
+
const logsResp = await request('GET', `${apiPath('mags-jobs')}/${requestId}/logs`);
|
|
673
699
|
if (logsResp.logs) {
|
|
674
700
|
logsResp.logs
|
|
675
701
|
.filter(l => l.source === 'stdout' || l.source === 'stderr')
|
|
@@ -686,10 +712,10 @@ async function enableUrlAccess(nameOrId, port = 8080) {
|
|
|
686
712
|
const requestId = await resolveJobId(nameOrId);
|
|
687
713
|
log('blue', `Enabling URL access on port ${port}...`);
|
|
688
714
|
|
|
689
|
-
const accessResp = await request('POST',
|
|
715
|
+
const accessResp = await request('POST', `${apiPath('mags-jobs')}/${requestId}/access`, { port });
|
|
690
716
|
|
|
691
717
|
if (accessResp.success) {
|
|
692
|
-
const status = await request('GET',
|
|
718
|
+
const status = await request('GET', `${apiPath('mags-jobs')}/${requestId}/status`);
|
|
693
719
|
if (status.subdomain) {
|
|
694
720
|
log('green', `URL enabled: https://${status.subdomain}.apps.magpiecloud.com`);
|
|
695
721
|
} else {
|
|
@@ -748,7 +774,7 @@ async function urlAliasCreate(subdomain, workspaceId, useLfg) {
|
|
|
748
774
|
const domain = useLfg ? 'app.lfg.run' : 'apps.magpiecloud.com';
|
|
749
775
|
log('blue', `Creating URL alias: ${subdomain}.${domain} → workspace '${workspaceId}'...`);
|
|
750
776
|
|
|
751
|
-
const resp = await request('POST', '
|
|
777
|
+
const resp = await request('POST', apiPath('mags-url-aliases'), {
|
|
752
778
|
subdomain,
|
|
753
779
|
workspace_id: workspaceId,
|
|
754
780
|
domain,
|
|
@@ -767,7 +793,7 @@ async function urlAliasCreate(subdomain, workspaceId, useLfg) {
|
|
|
767
793
|
}
|
|
768
794
|
|
|
769
795
|
async function urlAliasList() {
|
|
770
|
-
const resp = await request('GET', '
|
|
796
|
+
const resp = await request('GET', apiPath('mags-url-aliases'));
|
|
771
797
|
const aliases = resp.aliases || [];
|
|
772
798
|
|
|
773
799
|
if (aliases.length > 0) {
|
|
@@ -786,7 +812,7 @@ async function urlAliasList() {
|
|
|
786
812
|
|
|
787
813
|
async function urlAliasRemove(subdomain) {
|
|
788
814
|
log('blue', `Removing URL alias '${subdomain}'...`);
|
|
789
|
-
const resp = await request('DELETE',
|
|
815
|
+
const resp = await request('DELETE', `${apiPath('mags-url-aliases')}/${subdomain}`);
|
|
790
816
|
if (resp.error) {
|
|
791
817
|
log('red', `Error: ${resp.error}`);
|
|
792
818
|
process.exit(1);
|
|
@@ -800,7 +826,7 @@ async function getStatus(nameOrId) {
|
|
|
800
826
|
usage();
|
|
801
827
|
}
|
|
802
828
|
const requestId = await resolveJobId(nameOrId);
|
|
803
|
-
const status = await request('GET',
|
|
829
|
+
const status = await request('GET', `${apiPath('mags-jobs')}/${requestId}/status`);
|
|
804
830
|
console.log(JSON.stringify(status, null, 2));
|
|
805
831
|
}
|
|
806
832
|
|
|
@@ -810,7 +836,7 @@ async function getLogs(nameOrId) {
|
|
|
810
836
|
usage();
|
|
811
837
|
}
|
|
812
838
|
const requestId = await resolveJobId(nameOrId);
|
|
813
|
-
const logsResp = await request('GET',
|
|
839
|
+
const logsResp = await request('GET', `${apiPath('mags-jobs')}/${requestId}/logs`);
|
|
814
840
|
if (logsResp.logs) {
|
|
815
841
|
logsResp.logs.forEach(l => {
|
|
816
842
|
const levelColor = l.level === 'error' ? 'red' : l.level === 'warn' ? 'yellow' : 'gray';
|
|
@@ -820,7 +846,7 @@ async function getLogs(nameOrId) {
|
|
|
820
846
|
}
|
|
821
847
|
|
|
822
848
|
async function listJobs() {
|
|
823
|
-
const resp = await request('GET', '
|
|
849
|
+
const resp = await request('GET', `${apiPath('mags-jobs')}?page=1&page_size=10`);
|
|
824
850
|
if (resp.jobs && resp.jobs.length > 0) {
|
|
825
851
|
log('cyan', 'Recent Jobs:\n');
|
|
826
852
|
resp.jobs.forEach(job => {
|
|
@@ -848,7 +874,7 @@ async function stopJob(nameOrId) {
|
|
|
848
874
|
}
|
|
849
875
|
const requestId = await resolveJobId(nameOrId);
|
|
850
876
|
log('blue', `Stopping job ${requestId}...`);
|
|
851
|
-
const resp = await request('POST',
|
|
877
|
+
const resp = await request('POST', `${apiPath('mags-jobs')}/${requestId}/stop`);
|
|
852
878
|
if (resp.success) {
|
|
853
879
|
log('green', 'Job stopped');
|
|
854
880
|
} else {
|
|
@@ -890,7 +916,7 @@ async function setJobSettings(args) {
|
|
|
890
916
|
|
|
891
917
|
const requestId = await resolveJobId(nameOrId);
|
|
892
918
|
log('blue', `Updating settings for ${requestId}...`);
|
|
893
|
-
const resp = await request('PATCH',
|
|
919
|
+
const resp = await request('PATCH', `${apiPath('mags-jobs')}/${requestId}`, payload);
|
|
894
920
|
if (resp.success) {
|
|
895
921
|
if (noSleep === true) log('green', 'VM set to never auto-sleep');
|
|
896
922
|
if (noSleep === false) log('green', 'VM set to auto-sleep when idle');
|
|
@@ -923,10 +949,10 @@ async function resizeVM(args) {
|
|
|
923
949
|
// Sync workspace before stopping (preserve files)
|
|
924
950
|
if (existingJob.status === 'running') {
|
|
925
951
|
log('blue', 'Syncing workspace before resize...');
|
|
926
|
-
await request('POST',
|
|
952
|
+
await request('POST', `${apiPath('mags-jobs')}/${existingJob.request_id}/sync`);
|
|
927
953
|
}
|
|
928
954
|
log('blue', `Stopping existing VM...`);
|
|
929
|
-
await request('POST',
|
|
955
|
+
await request('POST', `${apiPath('mags-jobs')}/${existingJob.request_id}/stop`);
|
|
930
956
|
// Brief wait for the stop to complete
|
|
931
957
|
await new Promise(r => setTimeout(r, 1000));
|
|
932
958
|
}
|
|
@@ -943,7 +969,7 @@ async function resizeVM(args) {
|
|
|
943
969
|
disk_gb: diskGB,
|
|
944
970
|
};
|
|
945
971
|
|
|
946
|
-
const response = await request('POST', '
|
|
972
|
+
const response = await request('POST', apiPath('mags-jobs'), payload);
|
|
947
973
|
if (!response.request_id) {
|
|
948
974
|
log('red', 'Failed to create VM:');
|
|
949
975
|
console.log(JSON.stringify(response, null, 2));
|
|
@@ -978,7 +1004,7 @@ async function syncWorkspace(nameOrId) {
|
|
|
978
1004
|
}
|
|
979
1005
|
|
|
980
1006
|
log('blue', `Syncing workspace...`);
|
|
981
|
-
const resp = await request('POST',
|
|
1007
|
+
const resp = await request('POST', `${apiPath('mags-jobs')}/${requestId}/sync`);
|
|
982
1008
|
if (resp.success) {
|
|
983
1009
|
log('green', 'Workspace synced to S3 successfully');
|
|
984
1010
|
} else {
|
|
@@ -1098,7 +1124,7 @@ async function uploadFile(filePath) {
|
|
|
1098
1124
|
const body = Buffer.concat([header, fileData, footer]);
|
|
1099
1125
|
|
|
1100
1126
|
return new Promise((resolve, reject) => {
|
|
1101
|
-
const url = new URL('
|
|
1127
|
+
const url = new URL(apiPath('mags-files'), API_URL);
|
|
1102
1128
|
const isHttps = url.protocol === 'https:';
|
|
1103
1129
|
const lib = isHttps ? https : http;
|
|
1104
1130
|
|
|
@@ -1235,7 +1261,7 @@ async function cronAdd(args) {
|
|
|
1235
1261
|
};
|
|
1236
1262
|
if (workspace) payload.workspace_id = workspace;
|
|
1237
1263
|
|
|
1238
|
-
const resp = await request('POST', '
|
|
1264
|
+
const resp = await request('POST', apiPath('mags-cron'), payload);
|
|
1239
1265
|
if (resp.id) {
|
|
1240
1266
|
log('green', `Cron job created: ${resp.id}`);
|
|
1241
1267
|
log('blue', `Name: ${cronName}`);
|
|
@@ -1249,7 +1275,7 @@ async function cronAdd(args) {
|
|
|
1249
1275
|
}
|
|
1250
1276
|
|
|
1251
1277
|
async function cronList() {
|
|
1252
|
-
const resp = await request('GET', '
|
|
1278
|
+
const resp = await request('GET', apiPath('mags-cron'));
|
|
1253
1279
|
if (resp.cron_jobs && resp.cron_jobs.length > 0) {
|
|
1254
1280
|
log('cyan', 'Cron Jobs:\n');
|
|
1255
1281
|
resp.cron_jobs.forEach(cron => {
|
|
@@ -1271,7 +1297,7 @@ async function cronList() {
|
|
|
1271
1297
|
}
|
|
1272
1298
|
|
|
1273
1299
|
async function cronRemove(id) {
|
|
1274
|
-
const resp = await request('DELETE',
|
|
1300
|
+
const resp = await request('DELETE', `${apiPath('mags-cron')}/${id}`);
|
|
1275
1301
|
if (resp.success) {
|
|
1276
1302
|
log('green', 'Cron job deleted');
|
|
1277
1303
|
} else {
|
|
@@ -1281,7 +1307,7 @@ async function cronRemove(id) {
|
|
|
1281
1307
|
}
|
|
1282
1308
|
|
|
1283
1309
|
async function cronToggle(id, enabled) {
|
|
1284
|
-
const resp = await request('PATCH',
|
|
1310
|
+
const resp = await request('PATCH', `${apiPath('mags-cron')}/${id}`, { enabled });
|
|
1285
1311
|
if (resp.id) {
|
|
1286
1312
|
log('green', `Cron job ${enabled ? 'enabled' : 'disabled'}`);
|
|
1287
1313
|
if (resp.next_run_at) log('blue', `Next run: ${resp.next_run_at}`);
|
|
@@ -1328,7 +1354,7 @@ async function workspaceCommand(args) {
|
|
|
1328
1354
|
}
|
|
1329
1355
|
|
|
1330
1356
|
async function workspaceList() {
|
|
1331
|
-
const resp = await request('GET', '
|
|
1357
|
+
const resp = await request('GET', apiPath('mags-workspaces'));
|
|
1332
1358
|
if (resp.workspaces && resp.workspaces.length > 0) {
|
|
1333
1359
|
log('cyan', 'Workspaces:\n');
|
|
1334
1360
|
resp.workspaces.forEach(ws => {
|
|
@@ -1346,7 +1372,7 @@ async function workspaceList() {
|
|
|
1346
1372
|
|
|
1347
1373
|
async function workspaceDelete(workspaceId) {
|
|
1348
1374
|
log('blue', `Deleting workspace '${workspaceId}'...`);
|
|
1349
|
-
const resp = await request('DELETE',
|
|
1375
|
+
const resp = await request('DELETE', `${apiPath('mags-workspaces')}/${workspaceId}`);
|
|
1350
1376
|
if (resp.success) {
|
|
1351
1377
|
log('green', resp.message || `Workspace '${workspaceId}' deleted`);
|
|
1352
1378
|
} else {
|
|
@@ -1377,7 +1403,7 @@ async function browserSession(args) {
|
|
|
1377
1403
|
|
|
1378
1404
|
if (existingJob && existingJob.status === 'running') {
|
|
1379
1405
|
// Check if it's already a browser session
|
|
1380
|
-
const status = await request('GET',
|
|
1406
|
+
const status = await request('GET', `${apiPath('mags-jobs')}/${existingJob.request_id}/status`);
|
|
1381
1407
|
if (status.browser_mode && status.debug_ws_url) {
|
|
1382
1408
|
log('green', 'Found existing browser session');
|
|
1383
1409
|
console.log('');
|
|
@@ -1405,7 +1431,7 @@ async function browserSession(args) {
|
|
|
1405
1431
|
if (name) payload.name = name;
|
|
1406
1432
|
if (!name && workspace) payload.name = `browser-${workspace}`;
|
|
1407
1433
|
|
|
1408
|
-
const response = await request('POST', '
|
|
1434
|
+
const response = await request('POST', apiPath('mags-jobs'), payload);
|
|
1409
1435
|
|
|
1410
1436
|
if (!response.request_id) {
|
|
1411
1437
|
log('red', 'Failed to start browser session:');
|
|
@@ -1420,7 +1446,7 @@ async function browserSession(args) {
|
|
|
1420
1446
|
log('blue', 'Waiting for Chromium to start...');
|
|
1421
1447
|
let status;
|
|
1422
1448
|
for (let i = 0; i < 90; i++) {
|
|
1423
|
-
status = await request('GET',
|
|
1449
|
+
status = await request('GET', `${apiPath('mags-jobs')}/${requestId}/status`);
|
|
1424
1450
|
if (status.status === 'running' && status.debug_ws_url) {
|
|
1425
1451
|
break;
|
|
1426
1452
|
}
|
|
@@ -1469,7 +1495,7 @@ async function browserSession(args) {
|
|
|
1469
1495
|
// WebSocket tunnel proxy — pipes stdin/stdout through a WebSocket to the agent SSH proxy.
|
|
1470
1496
|
// Used as SSH ProxyCommand: ssh -o ProxyCommand="mags proxy <jobID>" root@mags-vm
|
|
1471
1497
|
async function proxyTunnel(jobID) {
|
|
1472
|
-
const url = new URL(
|
|
1498
|
+
const url = new URL(`${apiPath('mags-jobs')}/${jobID}/tunnel`, API_URL);
|
|
1473
1499
|
const wsUrl = url.toString().replace(/^http/, 'ws');
|
|
1474
1500
|
|
|
1475
1501
|
// Dynamic import for WebSocket
|
|
@@ -1508,6 +1534,11 @@ async function proxyTunnel(jobID) {
|
|
|
1508
1534
|
|
|
1509
1535
|
ws.on('open', () => {
|
|
1510
1536
|
// Don't flush yet — wait for first message (SSH key)
|
|
1537
|
+
// Send pings every 30s to keep the connection alive through proxies/CDNs
|
|
1538
|
+
const pingInterval = setInterval(() => {
|
|
1539
|
+
if (ws.readyState === WebSocket.OPEN) ws.ping();
|
|
1540
|
+
}, 30000);
|
|
1541
|
+
ws.on('close', () => clearInterval(pingInterval));
|
|
1511
1542
|
});
|
|
1512
1543
|
|
|
1513
1544
|
ws.on('message', (data, isBinary) => {
|
|
@@ -1547,7 +1578,7 @@ async function proxyTunnel(jobID) {
|
|
|
1547
1578
|
// Fetch SSH key from tunnel endpoint (quick WS handshake)
|
|
1548
1579
|
async function fetchTunnelKey(jobID) {
|
|
1549
1580
|
const WebSocket = require('ws');
|
|
1550
|
-
const url = new URL(
|
|
1581
|
+
const url = new URL(`${apiPath('mags-jobs')}/${jobID}/tunnel`, API_URL);
|
|
1551
1582
|
const wsUrl = url.toString().replace(/^http/, 'ws');
|
|
1552
1583
|
|
|
1553
1584
|
return new Promise((resolve, reject) => {
|
|
@@ -1607,7 +1638,7 @@ async function resolveOrCreateJob(nameOrId) {
|
|
|
1607
1638
|
startup_command: 'sleep 3600'
|
|
1608
1639
|
};
|
|
1609
1640
|
|
|
1610
|
-
const response = await request('POST', '
|
|
1641
|
+
const response = await request('POST', apiPath('mags-jobs'), payload);
|
|
1611
1642
|
if (!response.request_id) {
|
|
1612
1643
|
log('red', 'Failed to start VM:');
|
|
1613
1644
|
console.log(JSON.stringify(response, null, 2));
|
|
@@ -1619,7 +1650,7 @@ async function resolveOrCreateJob(nameOrId) {
|
|
|
1619
1650
|
|
|
1620
1651
|
log('blue', 'Waiting for VM...');
|
|
1621
1652
|
for (let i = 0; i < 60; i++) {
|
|
1622
|
-
const status = await request('GET',
|
|
1653
|
+
const status = await request('GET', `${apiPath('mags-jobs')}/${jobID}/status`);
|
|
1623
1654
|
if (status.status === 'running' && status.vm_id) break;
|
|
1624
1655
|
if (status.status === 'error') {
|
|
1625
1656
|
log('red', 'VM failed to start');
|
|
@@ -1634,7 +1665,7 @@ async function resolveOrCreateJob(nameOrId) {
|
|
|
1634
1665
|
|
|
1635
1666
|
// Get SSH key for a job (calls EnableAccess to set up agent proxy and get key)
|
|
1636
1667
|
async function getSSHKey(jobID) {
|
|
1637
|
-
const accessResp = await request('POST',
|
|
1668
|
+
const accessResp = await request('POST', `${apiPath('mags-jobs')}/${jobID}/access`, { port: 22 });
|
|
1638
1669
|
if (!accessResp.success) {
|
|
1639
1670
|
log('red', 'Failed to enable SSH access');
|
|
1640
1671
|
if (accessResp.error) log('red', accessResp.error);
|
|
@@ -1672,6 +1703,8 @@ async function sshToJob(nameOrId) {
|
|
|
1672
1703
|
'-o', 'StrictHostKeyChecking=no',
|
|
1673
1704
|
'-o', 'UserKnownHostsFile=/dev/null',
|
|
1674
1705
|
'-o', 'LogLevel=ERROR',
|
|
1706
|
+
'-o', 'ServerAliveInterval=15',
|
|
1707
|
+
'-o', 'ServerAliveCountMax=4',
|
|
1675
1708
|
'-o', `ProxyCommand=${magsPath} proxy ${jobID}`,
|
|
1676
1709
|
'-i', keyFile,
|
|
1677
1710
|
'root@mags-vm',
|
|
@@ -1739,6 +1772,8 @@ async function execOnJob(nameOrId, command) {
|
|
|
1739
1772
|
'-o', 'StrictHostKeyChecking=no',
|
|
1740
1773
|
'-o', 'UserKnownHostsFile=/dev/null',
|
|
1741
1774
|
'-o', 'LogLevel=ERROR',
|
|
1775
|
+
'-o', 'ServerAliveInterval=15',
|
|
1776
|
+
'-o', 'ServerAliveCountMax=4',
|
|
1742
1777
|
'-o', `ProxyCommand=${magsPath} proxy ${jobID}`,
|
|
1743
1778
|
'-i', keyFile,
|
|
1744
1779
|
];
|
package/deploy-page.sh
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Deploy an HTML page to a public Mags URL
|
|
3
|
+
# Usage: ./deploy-page.sh <html-file-or-content> [name]
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
MAGS_API_URL="${MAGS_API_URL:-https://api.magpiecloud.com}"
|
|
8
|
+
MAGS_API_TOKEN="${MAGS_API_TOKEN:-}"
|
|
9
|
+
|
|
10
|
+
# Colors
|
|
11
|
+
RED='\033[0;31m'
|
|
12
|
+
GREEN='\033[0;32m'
|
|
13
|
+
YELLOW='\033[1;33m'
|
|
14
|
+
BLUE='\033[0;34m'
|
|
15
|
+
CYAN='\033[0;36m'
|
|
16
|
+
NC='\033[0m'
|
|
17
|
+
|
|
18
|
+
# Load .env if exists
|
|
19
|
+
if [ -f ".env" ]; then
|
|
20
|
+
export $(grep -v '^#' .env | xargs)
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
if [ -z "$MAGS_API_TOKEN" ]; then
|
|
24
|
+
echo -e "${RED}Error: MAGS_API_TOKEN not set${NC}"
|
|
25
|
+
echo "Set it via: export MAGS_API_TOKEN=your-token"
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
usage() {
|
|
30
|
+
echo -e "${CYAN}Deploy HTML Page to Mags${NC}"
|
|
31
|
+
echo ""
|
|
32
|
+
echo "Usage: $0 <html-file-or-content> [name]"
|
|
33
|
+
echo ""
|
|
34
|
+
echo "Arguments:"
|
|
35
|
+
echo " html-file-or-content Path to HTML file, or HTML content directly"
|
|
36
|
+
echo " name Optional name for the deployment (default: page)"
|
|
37
|
+
echo ""
|
|
38
|
+
echo "Examples:"
|
|
39
|
+
echo " $0 index.html"
|
|
40
|
+
echo " $0 index.html my-site"
|
|
41
|
+
echo " $0 '<h1>Hello World</h1>'"
|
|
42
|
+
echo ""
|
|
43
|
+
echo "Environment:"
|
|
44
|
+
echo " MAGS_API_TOKEN Your API token (required)"
|
|
45
|
+
echo " MAGS_API_URL API URL (default: https://api.magpiecloud.com)"
|
|
46
|
+
exit 1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
[ -z "$1" ] && usage
|
|
50
|
+
|
|
51
|
+
INPUT="$1"
|
|
52
|
+
NAME="${2:-page}"
|
|
53
|
+
|
|
54
|
+
# Check if input is a file or raw HTML
|
|
55
|
+
if [ -f "$INPUT" ]; then
|
|
56
|
+
echo -e "${BLUE}Reading HTML from file: $INPUT${NC}"
|
|
57
|
+
HTML_CONTENT=$(cat "$INPUT")
|
|
58
|
+
else
|
|
59
|
+
echo -e "${BLUE}Using provided HTML content${NC}"
|
|
60
|
+
HTML_CONTENT="$INPUT"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# Escape the HTML for JSON
|
|
64
|
+
HTML_ESCAPED=$(echo "$HTML_CONTENT" | jq -Rs .)
|
|
65
|
+
|
|
66
|
+
# Create the script that will serve the HTML
|
|
67
|
+
SCRIPT=$(cat << 'SCRIPT_EOF'
|
|
68
|
+
#!/bin/sh
|
|
69
|
+
# Create the HTML file
|
|
70
|
+
cat > /tmp/index.html << 'HTML_CONTENT_MARKER'
|
|
71
|
+
__HTML_PLACEHOLDER__
|
|
72
|
+
HTML_CONTENT_MARKER
|
|
73
|
+
|
|
74
|
+
# Start Python HTTP server on port 8080
|
|
75
|
+
cd /tmp
|
|
76
|
+
echo "Server running on port 8080"
|
|
77
|
+
python3 -m http.server 8080
|
|
78
|
+
SCRIPT_EOF
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Replace placeholder with actual HTML
|
|
82
|
+
SCRIPT=$(echo "$SCRIPT" | sed "s|__HTML_PLACEHOLDER__|$(echo "$HTML_CONTENT" | sed 's/|/\\|/g; s/&/\\&/g')|")
|
|
83
|
+
|
|
84
|
+
echo -e "${BLUE}Deploying page...${NC}"
|
|
85
|
+
|
|
86
|
+
# Submit the persistent job
|
|
87
|
+
RESPONSE=$(curl -s -X POST "${MAGS_API_URL}/api/v1/mags-jobs" \
|
|
88
|
+
-H "Authorization: Bearer $MAGS_API_TOKEN" \
|
|
89
|
+
-H "Content-Type: application/json" \
|
|
90
|
+
-d "$(jq -n --arg script "$SCRIPT" --arg name "$NAME" '{
|
|
91
|
+
script: $script,
|
|
92
|
+
type: "inline",
|
|
93
|
+
name: $name,
|
|
94
|
+
persistent: true
|
|
95
|
+
}')")
|
|
96
|
+
|
|
97
|
+
REQUEST_ID=$(echo "$RESPONSE" | jq -r '.request_id // empty')
|
|
98
|
+
|
|
99
|
+
if [ -z "$REQUEST_ID" ]; then
|
|
100
|
+
echo -e "${RED}Failed to deploy:${NC}"
|
|
101
|
+
echo "$RESPONSE" | jq .
|
|
102
|
+
exit 1
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
echo -e "${GREEN}Deployment started: $REQUEST_ID${NC}"
|
|
106
|
+
|
|
107
|
+
# Wait for job to be running
|
|
108
|
+
echo -e "${BLUE}Waiting for VM to start...${NC}"
|
|
109
|
+
MAX_ATTEMPTS=30
|
|
110
|
+
ATTEMPT=0
|
|
111
|
+
|
|
112
|
+
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
|
|
113
|
+
STATUS_RESP=$(curl -s -X GET "${MAGS_API_URL}/api/v1/mags-jobs/${REQUEST_ID}/status" \
|
|
114
|
+
-H "Authorization: Bearer $MAGS_API_TOKEN")
|
|
115
|
+
|
|
116
|
+
STATUS=$(echo "$STATUS_RESP" | jq -r '.status // "unknown"')
|
|
117
|
+
SUBDOMAIN=$(echo "$STATUS_RESP" | jq -r '.subdomain // empty')
|
|
118
|
+
|
|
119
|
+
if [ "$STATUS" = "error" ]; then
|
|
120
|
+
echo -e "${RED}Deployment failed!${NC}"
|
|
121
|
+
echo "$STATUS_RESP" | jq .
|
|
122
|
+
exit 1
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
if [ "$STATUS" = "running" ]; then
|
|
126
|
+
echo -e " ${GREEN}VM running${NC}"
|
|
127
|
+
break
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
printf "."
|
|
131
|
+
sleep 1
|
|
132
|
+
ATTEMPT=$((ATTEMPT + 1))
|
|
133
|
+
done
|
|
134
|
+
|
|
135
|
+
if [ "$STATUS" != "running" ]; then
|
|
136
|
+
echo -e "${RED}Timed out waiting for VM to start${NC}"
|
|
137
|
+
exit 1
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# Enable URL access
|
|
141
|
+
echo -e "${BLUE}Enabling URL access...${NC}"
|
|
142
|
+
ACCESS_RESP=$(curl -s -X POST "${MAGS_API_URL}/api/v1/mags-jobs/${REQUEST_ID}/access" \
|
|
143
|
+
-H "Authorization: Bearer $MAGS_API_TOKEN" \
|
|
144
|
+
-H "Content-Type: application/json" \
|
|
145
|
+
-d '{"access_type": "url", "port": 8080}')
|
|
146
|
+
|
|
147
|
+
URL=$(echo "$ACCESS_RESP" | jq -r '.url // empty')
|
|
148
|
+
SUCCESS=$(echo "$ACCESS_RESP" | jq -r '.success // false')
|
|
149
|
+
|
|
150
|
+
if [ "$SUCCESS" != "true" ] || [ -z "$URL" ]; then
|
|
151
|
+
echo -e "${YELLOW}Warning: Could not get URL from access endpoint${NC}"
|
|
152
|
+
echo "$ACCESS_RESP" | jq .
|
|
153
|
+
# Fallback: construct URL from subdomain
|
|
154
|
+
URL="https://${SUBDOMAIN}.apps.magpiecloud.com"
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
echo ""
|
|
158
|
+
echo -e "${GREEN}═══════════════════════════════════════════════════════════${NC}"
|
|
159
|
+
echo -e "${GREEN} Page Deployed Successfully!${NC}"
|
|
160
|
+
echo -e "${GREEN}═══════════════════════════════════════════════════════════${NC}"
|
|
161
|
+
echo ""
|
|
162
|
+
echo -e " ${CYAN}URL:${NC} ${GREEN}${URL}${NC}"
|
|
163
|
+
echo -e " ${CYAN}Subdomain:${NC} ${SUBDOMAIN}"
|
|
164
|
+
echo -e " ${CYAN}Request ID:${NC} ${REQUEST_ID}"
|
|
165
|
+
echo ""
|
|
166
|
+
echo -e "${YELLOW}Note: The VM will sleep after idle timeout and wake on next request.${NC}"
|
|
167
|
+
echo ""
|
|
168
|
+
echo "To check status:"
|
|
169
|
+
echo " curl -s -H 'Authorization: Bearer \$MAGS_API_TOKEN' \\"
|
|
170
|
+
echo " '${MAGS_API_URL}/api/v1/mags-jobs/${REQUEST_ID}/status' | jq ."
|
|
171
|
+
echo ""
|
package/index.js
CHANGED
|
@@ -103,7 +103,6 @@ class Mags {
|
|
|
103
103
|
* @param {object} options.environment - Environment variables
|
|
104
104
|
* @param {string[]} options.fileIds - File IDs from uploadFiles()
|
|
105
105
|
* @param {number} options.diskGb - Custom disk size in GB (default 2)
|
|
106
|
-
* @param {string} options.rootfsType - VM rootfs type: standard, claude, pi
|
|
107
106
|
* @returns {Promise<{request_id: string, status: string}>}
|
|
108
107
|
*/
|
|
109
108
|
async run(script, options = {}) {
|
|
@@ -132,7 +131,6 @@ class Mags {
|
|
|
132
131
|
if (options.environment) payload.environment = options.environment;
|
|
133
132
|
if (options.fileIds && options.fileIds.length > 0) payload.file_ids = options.fileIds;
|
|
134
133
|
if (options.diskGb) payload.disk_gb = options.diskGb;
|
|
135
|
-
if (options.rootfsType) payload.rootfs_type = options.rootfsType;
|
|
136
134
|
|
|
137
135
|
return this._request('POST', '/api/v1/mags-jobs', payload);
|
|
138
136
|
}
|
package/mags
ADDED
|
Binary file
|