@magpiecloud/mags 1.8.17 → 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/bin/mags.js +56 -42
- package/nodejs/index.js +1 -2
- package/package.json +1 -1
- package/test-sdk.js +68 -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');
|
|
@@ -311,7 +326,7 @@ To authenticate, you need an API token from Magpie.
|
|
|
311
326
|
log('blue', 'Verifying token...');
|
|
312
327
|
|
|
313
328
|
try {
|
|
314
|
-
const resp = await request('GET', '
|
|
329
|
+
const resp = await request('GET', `${apiPath('mags-jobs')}?page=1&page_size=1`);
|
|
315
330
|
|
|
316
331
|
if (resp.error === 'unauthorized' || resp.error === 'Unauthorized') {
|
|
317
332
|
log('red', 'Invalid token. Please check and try again.');
|
|
@@ -365,7 +380,7 @@ async function whoami() {
|
|
|
365
380
|
log('blue', 'Checking authentication...');
|
|
366
381
|
|
|
367
382
|
try {
|
|
368
|
-
const resp = await request('GET', '
|
|
383
|
+
const resp = await request('GET', `${apiPath('mags-jobs')}?page=1&page_size=1`);
|
|
369
384
|
|
|
370
385
|
if (resp.error === 'unauthorized' || resp.error === 'Unauthorized') {
|
|
371
386
|
log('red', 'Token is invalid or expired.');
|
|
@@ -436,7 +451,7 @@ async function newVM(args) {
|
|
|
436
451
|
|
|
437
452
|
// Check if a VM with this name already exists (running or sleeping)
|
|
438
453
|
try {
|
|
439
|
-
const jobs = await request('GET', '
|
|
454
|
+
const jobs = await request('GET', apiPath('mags-jobs'));
|
|
440
455
|
const existing = (jobs.jobs || []).find(j =>
|
|
441
456
|
j.name === name && (j.status === 'running' || j.status === 'sleeping')
|
|
442
457
|
);
|
|
@@ -457,14 +472,13 @@ async function newVM(args) {
|
|
|
457
472
|
persistent: true,
|
|
458
473
|
name: name,
|
|
459
474
|
workspace_id: name,
|
|
460
|
-
startup_command: 'sleep infinity'
|
|
461
|
-
no_sync: !persistent
|
|
475
|
+
startup_command: 'sleep infinity'
|
|
462
476
|
};
|
|
463
477
|
if (baseWorkspace) payload.base_workspace_id = baseWorkspace;
|
|
464
478
|
if (diskGB) payload.disk_gb = diskGB;
|
|
465
479
|
if (rootfsType) payload.rootfs_type = rootfsType;
|
|
466
480
|
|
|
467
|
-
const response = await request('POST', '
|
|
481
|
+
const response = await request('POST', apiPath('mags-jobs'), payload);
|
|
468
482
|
|
|
469
483
|
if (!response.request_id) {
|
|
470
484
|
log('red', 'Failed to create VM:');
|
|
@@ -477,7 +491,7 @@ async function newVM(args) {
|
|
|
477
491
|
let attempt = 0;
|
|
478
492
|
|
|
479
493
|
while (attempt < maxAttempts) {
|
|
480
|
-
const status = await request('GET',
|
|
494
|
+
const status = await request('GET', `${apiPath('mags-jobs')}/${response.request_id}/status`);
|
|
481
495
|
|
|
482
496
|
if (status.status === 'running') {
|
|
483
497
|
log('green', `VM '${name}' created successfully${persistent ? ' (persistent)' : ' (local disk)'}`);
|
|
@@ -621,7 +635,7 @@ async function runJob(args) {
|
|
|
621
635
|
if (diskGB) payload.disk_gb = diskGB;
|
|
622
636
|
if (rootfsType) payload.rootfs_type = rootfsType;
|
|
623
637
|
|
|
624
|
-
const response = await request('POST', '
|
|
638
|
+
const response = await request('POST', apiPath('mags-jobs'), payload);
|
|
625
639
|
|
|
626
640
|
if (!response.request_id) {
|
|
627
641
|
log('red', 'Failed to submit job:');
|
|
@@ -640,7 +654,7 @@ async function runJob(args) {
|
|
|
640
654
|
let attempt = 0;
|
|
641
655
|
|
|
642
656
|
while (attempt < maxAttempts) {
|
|
643
|
-
const status = await request('GET',
|
|
657
|
+
const status = await request('GET', `${apiPath('mags-jobs')}/${requestId}/status`);
|
|
644
658
|
|
|
645
659
|
if (status.status === 'completed') {
|
|
646
660
|
log('green', `Completed in ${status.script_duration_ms}ms`);
|
|
@@ -658,7 +672,7 @@ async function runJob(args) {
|
|
|
658
672
|
|
|
659
673
|
if (enableUrl && status.subdomain) {
|
|
660
674
|
log('blue', `Enabling URL access on port ${port}...`);
|
|
661
|
-
const accessResp = await request('POST',
|
|
675
|
+
const accessResp = await request('POST', `${apiPath('mags-jobs')}/${requestId}/access`, { port });
|
|
662
676
|
if (accessResp.success) {
|
|
663
677
|
log('green', `URL: https://${status.subdomain}.apps.magpiecloud.com`);
|
|
664
678
|
} else {
|
|
@@ -681,7 +695,7 @@ async function runJob(args) {
|
|
|
681
695
|
|
|
682
696
|
// Get logs
|
|
683
697
|
log('cyan', 'Output:');
|
|
684
|
-
const logsResp = await request('GET',
|
|
698
|
+
const logsResp = await request('GET', `${apiPath('mags-jobs')}/${requestId}/logs`);
|
|
685
699
|
if (logsResp.logs) {
|
|
686
700
|
logsResp.logs
|
|
687
701
|
.filter(l => l.source === 'stdout' || l.source === 'stderr')
|
|
@@ -698,10 +712,10 @@ async function enableUrlAccess(nameOrId, port = 8080) {
|
|
|
698
712
|
const requestId = await resolveJobId(nameOrId);
|
|
699
713
|
log('blue', `Enabling URL access on port ${port}...`);
|
|
700
714
|
|
|
701
|
-
const accessResp = await request('POST',
|
|
715
|
+
const accessResp = await request('POST', `${apiPath('mags-jobs')}/${requestId}/access`, { port });
|
|
702
716
|
|
|
703
717
|
if (accessResp.success) {
|
|
704
|
-
const status = await request('GET',
|
|
718
|
+
const status = await request('GET', `${apiPath('mags-jobs')}/${requestId}/status`);
|
|
705
719
|
if (status.subdomain) {
|
|
706
720
|
log('green', `URL enabled: https://${status.subdomain}.apps.magpiecloud.com`);
|
|
707
721
|
} else {
|
|
@@ -760,7 +774,7 @@ async function urlAliasCreate(subdomain, workspaceId, useLfg) {
|
|
|
760
774
|
const domain = useLfg ? 'app.lfg.run' : 'apps.magpiecloud.com';
|
|
761
775
|
log('blue', `Creating URL alias: ${subdomain}.${domain} → workspace '${workspaceId}'...`);
|
|
762
776
|
|
|
763
|
-
const resp = await request('POST', '
|
|
777
|
+
const resp = await request('POST', apiPath('mags-url-aliases'), {
|
|
764
778
|
subdomain,
|
|
765
779
|
workspace_id: workspaceId,
|
|
766
780
|
domain,
|
|
@@ -779,7 +793,7 @@ async function urlAliasCreate(subdomain, workspaceId, useLfg) {
|
|
|
779
793
|
}
|
|
780
794
|
|
|
781
795
|
async function urlAliasList() {
|
|
782
|
-
const resp = await request('GET', '
|
|
796
|
+
const resp = await request('GET', apiPath('mags-url-aliases'));
|
|
783
797
|
const aliases = resp.aliases || [];
|
|
784
798
|
|
|
785
799
|
if (aliases.length > 0) {
|
|
@@ -798,7 +812,7 @@ async function urlAliasList() {
|
|
|
798
812
|
|
|
799
813
|
async function urlAliasRemove(subdomain) {
|
|
800
814
|
log('blue', `Removing URL alias '${subdomain}'...`);
|
|
801
|
-
const resp = await request('DELETE',
|
|
815
|
+
const resp = await request('DELETE', `${apiPath('mags-url-aliases')}/${subdomain}`);
|
|
802
816
|
if (resp.error) {
|
|
803
817
|
log('red', `Error: ${resp.error}`);
|
|
804
818
|
process.exit(1);
|
|
@@ -812,7 +826,7 @@ async function getStatus(nameOrId) {
|
|
|
812
826
|
usage();
|
|
813
827
|
}
|
|
814
828
|
const requestId = await resolveJobId(nameOrId);
|
|
815
|
-
const status = await request('GET',
|
|
829
|
+
const status = await request('GET', `${apiPath('mags-jobs')}/${requestId}/status`);
|
|
816
830
|
console.log(JSON.stringify(status, null, 2));
|
|
817
831
|
}
|
|
818
832
|
|
|
@@ -822,7 +836,7 @@ async function getLogs(nameOrId) {
|
|
|
822
836
|
usage();
|
|
823
837
|
}
|
|
824
838
|
const requestId = await resolveJobId(nameOrId);
|
|
825
|
-
const logsResp = await request('GET',
|
|
839
|
+
const logsResp = await request('GET', `${apiPath('mags-jobs')}/${requestId}/logs`);
|
|
826
840
|
if (logsResp.logs) {
|
|
827
841
|
logsResp.logs.forEach(l => {
|
|
828
842
|
const levelColor = l.level === 'error' ? 'red' : l.level === 'warn' ? 'yellow' : 'gray';
|
|
@@ -832,7 +846,7 @@ async function getLogs(nameOrId) {
|
|
|
832
846
|
}
|
|
833
847
|
|
|
834
848
|
async function listJobs() {
|
|
835
|
-
const resp = await request('GET', '
|
|
849
|
+
const resp = await request('GET', `${apiPath('mags-jobs')}?page=1&page_size=10`);
|
|
836
850
|
if (resp.jobs && resp.jobs.length > 0) {
|
|
837
851
|
log('cyan', 'Recent Jobs:\n');
|
|
838
852
|
resp.jobs.forEach(job => {
|
|
@@ -860,7 +874,7 @@ async function stopJob(nameOrId) {
|
|
|
860
874
|
}
|
|
861
875
|
const requestId = await resolveJobId(nameOrId);
|
|
862
876
|
log('blue', `Stopping job ${requestId}...`);
|
|
863
|
-
const resp = await request('POST',
|
|
877
|
+
const resp = await request('POST', `${apiPath('mags-jobs')}/${requestId}/stop`);
|
|
864
878
|
if (resp.success) {
|
|
865
879
|
log('green', 'Job stopped');
|
|
866
880
|
} else {
|
|
@@ -902,7 +916,7 @@ async function setJobSettings(args) {
|
|
|
902
916
|
|
|
903
917
|
const requestId = await resolveJobId(nameOrId);
|
|
904
918
|
log('blue', `Updating settings for ${requestId}...`);
|
|
905
|
-
const resp = await request('PATCH',
|
|
919
|
+
const resp = await request('PATCH', `${apiPath('mags-jobs')}/${requestId}`, payload);
|
|
906
920
|
if (resp.success) {
|
|
907
921
|
if (noSleep === true) log('green', 'VM set to never auto-sleep');
|
|
908
922
|
if (noSleep === false) log('green', 'VM set to auto-sleep when idle');
|
|
@@ -935,10 +949,10 @@ async function resizeVM(args) {
|
|
|
935
949
|
// Sync workspace before stopping (preserve files)
|
|
936
950
|
if (existingJob.status === 'running') {
|
|
937
951
|
log('blue', 'Syncing workspace before resize...');
|
|
938
|
-
await request('POST',
|
|
952
|
+
await request('POST', `${apiPath('mags-jobs')}/${existingJob.request_id}/sync`);
|
|
939
953
|
}
|
|
940
954
|
log('blue', `Stopping existing VM...`);
|
|
941
|
-
await request('POST',
|
|
955
|
+
await request('POST', `${apiPath('mags-jobs')}/${existingJob.request_id}/stop`);
|
|
942
956
|
// Brief wait for the stop to complete
|
|
943
957
|
await new Promise(r => setTimeout(r, 1000));
|
|
944
958
|
}
|
|
@@ -955,7 +969,7 @@ async function resizeVM(args) {
|
|
|
955
969
|
disk_gb: diskGB,
|
|
956
970
|
};
|
|
957
971
|
|
|
958
|
-
const response = await request('POST', '
|
|
972
|
+
const response = await request('POST', apiPath('mags-jobs'), payload);
|
|
959
973
|
if (!response.request_id) {
|
|
960
974
|
log('red', 'Failed to create VM:');
|
|
961
975
|
console.log(JSON.stringify(response, null, 2));
|
|
@@ -990,7 +1004,7 @@ async function syncWorkspace(nameOrId) {
|
|
|
990
1004
|
}
|
|
991
1005
|
|
|
992
1006
|
log('blue', `Syncing workspace...`);
|
|
993
|
-
const resp = await request('POST',
|
|
1007
|
+
const resp = await request('POST', `${apiPath('mags-jobs')}/${requestId}/sync`);
|
|
994
1008
|
if (resp.success) {
|
|
995
1009
|
log('green', 'Workspace synced to S3 successfully');
|
|
996
1010
|
} else {
|
|
@@ -1110,7 +1124,7 @@ async function uploadFile(filePath) {
|
|
|
1110
1124
|
const body = Buffer.concat([header, fileData, footer]);
|
|
1111
1125
|
|
|
1112
1126
|
return new Promise((resolve, reject) => {
|
|
1113
|
-
const url = new URL('
|
|
1127
|
+
const url = new URL(apiPath('mags-files'), API_URL);
|
|
1114
1128
|
const isHttps = url.protocol === 'https:';
|
|
1115
1129
|
const lib = isHttps ? https : http;
|
|
1116
1130
|
|
|
@@ -1247,7 +1261,7 @@ async function cronAdd(args) {
|
|
|
1247
1261
|
};
|
|
1248
1262
|
if (workspace) payload.workspace_id = workspace;
|
|
1249
1263
|
|
|
1250
|
-
const resp = await request('POST', '
|
|
1264
|
+
const resp = await request('POST', apiPath('mags-cron'), payload);
|
|
1251
1265
|
if (resp.id) {
|
|
1252
1266
|
log('green', `Cron job created: ${resp.id}`);
|
|
1253
1267
|
log('blue', `Name: ${cronName}`);
|
|
@@ -1261,7 +1275,7 @@ async function cronAdd(args) {
|
|
|
1261
1275
|
}
|
|
1262
1276
|
|
|
1263
1277
|
async function cronList() {
|
|
1264
|
-
const resp = await request('GET', '
|
|
1278
|
+
const resp = await request('GET', apiPath('mags-cron'));
|
|
1265
1279
|
if (resp.cron_jobs && resp.cron_jobs.length > 0) {
|
|
1266
1280
|
log('cyan', 'Cron Jobs:\n');
|
|
1267
1281
|
resp.cron_jobs.forEach(cron => {
|
|
@@ -1283,7 +1297,7 @@ async function cronList() {
|
|
|
1283
1297
|
}
|
|
1284
1298
|
|
|
1285
1299
|
async function cronRemove(id) {
|
|
1286
|
-
const resp = await request('DELETE',
|
|
1300
|
+
const resp = await request('DELETE', `${apiPath('mags-cron')}/${id}`);
|
|
1287
1301
|
if (resp.success) {
|
|
1288
1302
|
log('green', 'Cron job deleted');
|
|
1289
1303
|
} else {
|
|
@@ -1293,7 +1307,7 @@ async function cronRemove(id) {
|
|
|
1293
1307
|
}
|
|
1294
1308
|
|
|
1295
1309
|
async function cronToggle(id, enabled) {
|
|
1296
|
-
const resp = await request('PATCH',
|
|
1310
|
+
const resp = await request('PATCH', `${apiPath('mags-cron')}/${id}`, { enabled });
|
|
1297
1311
|
if (resp.id) {
|
|
1298
1312
|
log('green', `Cron job ${enabled ? 'enabled' : 'disabled'}`);
|
|
1299
1313
|
if (resp.next_run_at) log('blue', `Next run: ${resp.next_run_at}`);
|
|
@@ -1340,7 +1354,7 @@ async function workspaceCommand(args) {
|
|
|
1340
1354
|
}
|
|
1341
1355
|
|
|
1342
1356
|
async function workspaceList() {
|
|
1343
|
-
const resp = await request('GET', '
|
|
1357
|
+
const resp = await request('GET', apiPath('mags-workspaces'));
|
|
1344
1358
|
if (resp.workspaces && resp.workspaces.length > 0) {
|
|
1345
1359
|
log('cyan', 'Workspaces:\n');
|
|
1346
1360
|
resp.workspaces.forEach(ws => {
|
|
@@ -1358,7 +1372,7 @@ async function workspaceList() {
|
|
|
1358
1372
|
|
|
1359
1373
|
async function workspaceDelete(workspaceId) {
|
|
1360
1374
|
log('blue', `Deleting workspace '${workspaceId}'...`);
|
|
1361
|
-
const resp = await request('DELETE',
|
|
1375
|
+
const resp = await request('DELETE', `${apiPath('mags-workspaces')}/${workspaceId}`);
|
|
1362
1376
|
if (resp.success) {
|
|
1363
1377
|
log('green', resp.message || `Workspace '${workspaceId}' deleted`);
|
|
1364
1378
|
} else {
|
|
@@ -1389,7 +1403,7 @@ async function browserSession(args) {
|
|
|
1389
1403
|
|
|
1390
1404
|
if (existingJob && existingJob.status === 'running') {
|
|
1391
1405
|
// Check if it's already a browser session
|
|
1392
|
-
const status = await request('GET',
|
|
1406
|
+
const status = await request('GET', `${apiPath('mags-jobs')}/${existingJob.request_id}/status`);
|
|
1393
1407
|
if (status.browser_mode && status.debug_ws_url) {
|
|
1394
1408
|
log('green', 'Found existing browser session');
|
|
1395
1409
|
console.log('');
|
|
@@ -1417,7 +1431,7 @@ async function browserSession(args) {
|
|
|
1417
1431
|
if (name) payload.name = name;
|
|
1418
1432
|
if (!name && workspace) payload.name = `browser-${workspace}`;
|
|
1419
1433
|
|
|
1420
|
-
const response = await request('POST', '
|
|
1434
|
+
const response = await request('POST', apiPath('mags-jobs'), payload);
|
|
1421
1435
|
|
|
1422
1436
|
if (!response.request_id) {
|
|
1423
1437
|
log('red', 'Failed to start browser session:');
|
|
@@ -1432,7 +1446,7 @@ async function browserSession(args) {
|
|
|
1432
1446
|
log('blue', 'Waiting for Chromium to start...');
|
|
1433
1447
|
let status;
|
|
1434
1448
|
for (let i = 0; i < 90; i++) {
|
|
1435
|
-
status = await request('GET',
|
|
1449
|
+
status = await request('GET', `${apiPath('mags-jobs')}/${requestId}/status`);
|
|
1436
1450
|
if (status.status === 'running' && status.debug_ws_url) {
|
|
1437
1451
|
break;
|
|
1438
1452
|
}
|
|
@@ -1481,7 +1495,7 @@ async function browserSession(args) {
|
|
|
1481
1495
|
// WebSocket tunnel proxy — pipes stdin/stdout through a WebSocket to the agent SSH proxy.
|
|
1482
1496
|
// Used as SSH ProxyCommand: ssh -o ProxyCommand="mags proxy <jobID>" root@mags-vm
|
|
1483
1497
|
async function proxyTunnel(jobID) {
|
|
1484
|
-
const url = new URL(
|
|
1498
|
+
const url = new URL(`${apiPath('mags-jobs')}/${jobID}/tunnel`, API_URL);
|
|
1485
1499
|
const wsUrl = url.toString().replace(/^http/, 'ws');
|
|
1486
1500
|
|
|
1487
1501
|
// Dynamic import for WebSocket
|
|
@@ -1564,7 +1578,7 @@ async function proxyTunnel(jobID) {
|
|
|
1564
1578
|
// Fetch SSH key from tunnel endpoint (quick WS handshake)
|
|
1565
1579
|
async function fetchTunnelKey(jobID) {
|
|
1566
1580
|
const WebSocket = require('ws');
|
|
1567
|
-
const url = new URL(
|
|
1581
|
+
const url = new URL(`${apiPath('mags-jobs')}/${jobID}/tunnel`, API_URL);
|
|
1568
1582
|
const wsUrl = url.toString().replace(/^http/, 'ws');
|
|
1569
1583
|
|
|
1570
1584
|
return new Promise((resolve, reject) => {
|
|
@@ -1624,7 +1638,7 @@ async function resolveOrCreateJob(nameOrId) {
|
|
|
1624
1638
|
startup_command: 'sleep 3600'
|
|
1625
1639
|
};
|
|
1626
1640
|
|
|
1627
|
-
const response = await request('POST', '
|
|
1641
|
+
const response = await request('POST', apiPath('mags-jobs'), payload);
|
|
1628
1642
|
if (!response.request_id) {
|
|
1629
1643
|
log('red', 'Failed to start VM:');
|
|
1630
1644
|
console.log(JSON.stringify(response, null, 2));
|
|
@@ -1636,7 +1650,7 @@ async function resolveOrCreateJob(nameOrId) {
|
|
|
1636
1650
|
|
|
1637
1651
|
log('blue', 'Waiting for VM...');
|
|
1638
1652
|
for (let i = 0; i < 60; i++) {
|
|
1639
|
-
const status = await request('GET',
|
|
1653
|
+
const status = await request('GET', `${apiPath('mags-jobs')}/${jobID}/status`);
|
|
1640
1654
|
if (status.status === 'running' && status.vm_id) break;
|
|
1641
1655
|
if (status.status === 'error') {
|
|
1642
1656
|
log('red', 'VM failed to start');
|
|
@@ -1651,7 +1665,7 @@ async function resolveOrCreateJob(nameOrId) {
|
|
|
1651
1665
|
|
|
1652
1666
|
// Get SSH key for a job (calls EnableAccess to set up agent proxy and get key)
|
|
1653
1667
|
async function getSSHKey(jobID) {
|
|
1654
|
-
const accessResp = await request('POST',
|
|
1668
|
+
const accessResp = await request('POST', `${apiPath('mags-jobs')}/${jobID}/access`, { port: 22 });
|
|
1655
1669
|
if (!accessResp.success) {
|
|
1656
1670
|
log('red', 'Failed to enable SSH access');
|
|
1657
1671
|
if (accessResp.error) log('red', accessResp.error);
|
package/nodejs/index.js
CHANGED
|
@@ -124,7 +124,6 @@ class Mags {
|
|
|
124
124
|
};
|
|
125
125
|
|
|
126
126
|
if (options.noSleep) payload.no_sleep = true;
|
|
127
|
-
if (options.noSync) payload.no_sync = true;
|
|
128
127
|
if (options.name) payload.name = options.name;
|
|
129
128
|
if (!options.ephemeral && options.workspaceId) payload.workspace_id = options.workspaceId;
|
|
130
129
|
if (options.baseWorkspaceId) payload.base_workspace_id = options.baseWorkspaceId;
|
|
@@ -284,9 +283,9 @@ class Mags {
|
|
|
284
283
|
const result = await this.run('sleep infinity', {
|
|
285
284
|
workspaceId: name,
|
|
286
285
|
persistent: true,
|
|
287
|
-
noSync: !options.persistent,
|
|
288
286
|
baseWorkspaceId: options.baseWorkspaceId,
|
|
289
287
|
diskGb: options.diskGb,
|
|
288
|
+
rootfsType: options.rootfsType,
|
|
290
289
|
});
|
|
291
290
|
const requestId = result.request_id;
|
|
292
291
|
|
package/package.json
CHANGED
package/test-sdk.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const Mags = require('./nodejs/index.js');
|
|
2
|
+
|
|
3
|
+
const API_TOKEN = 'e1e90cc27dfe6a50cc28699cdcb937ef8c443567b62cf064a063f9b34af0b91b';
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
const mags = new Mags({ apiToken: API_TOKEN });
|
|
7
|
+
|
|
8
|
+
// Test 1: Basic runAndWait
|
|
9
|
+
console.log('=== Test 1: Basic runAndWait ===');
|
|
10
|
+
const r1 = await mags.runAndWait('echo "hello from SDK" && node --version');
|
|
11
|
+
const r1out = (r1.logs || []).map(l => l.output || l.message || '').join('').trim();
|
|
12
|
+
console.log('Output:', r1out || '(check logs)');
|
|
13
|
+
console.log('Status:', r1.status, '| Exit:', r1.exitCode);
|
|
14
|
+
console.log('Logs:', JSON.stringify(r1.logs?.slice(0, 3)));
|
|
15
|
+
console.log();
|
|
16
|
+
|
|
17
|
+
// Test 2: Run with claude type
|
|
18
|
+
console.log('=== Test 2: runAndWait with --type claude ===');
|
|
19
|
+
const r2 = await mags.runAndWait('claude --version && node --version', { rootfsType: 'claude', timeout: 120000 });
|
|
20
|
+
const r2out = (r2.logs || []).map(l => l.output || l.message || '').join('').trim();
|
|
21
|
+
console.log('Output:', r2out || '(check logs)');
|
|
22
|
+
console.log('Status:', r2.status);
|
|
23
|
+
console.log();
|
|
24
|
+
|
|
25
|
+
// Test 3: Create persistent VM — use run() not runAndWait() since persistent stays "running"
|
|
26
|
+
console.log('=== Test 3: Create persistent VM ===');
|
|
27
|
+
const r3 = await mags.run('echo "data123" > /root/sdk-test.txt && echo "created"', {
|
|
28
|
+
name: 'sdk-test-vm',
|
|
29
|
+
persistent: true,
|
|
30
|
+
});
|
|
31
|
+
console.log('Submitted:', r3.request_id);
|
|
32
|
+
// Wait for VM to be fully ready (needs VM ID assigned)
|
|
33
|
+
let s3;
|
|
34
|
+
for (let i = 0; i < 20; i++) {
|
|
35
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
36
|
+
s3 = await mags.status(r3.request_id);
|
|
37
|
+
if (s3.vm_id) break;
|
|
38
|
+
}
|
|
39
|
+
console.log('Status:', s3.status, '| VM:', s3.vm_id);
|
|
40
|
+
console.log();
|
|
41
|
+
|
|
42
|
+
// Test 4: exec on persistent VM
|
|
43
|
+
console.log('=== Test 4: exec on persistent VM ===');
|
|
44
|
+
const r4 = await mags.exec('sdk-test-vm', 'cat /root/sdk-test.txt && echo "exec works"');
|
|
45
|
+
console.log('Output:', r4.output?.trim());
|
|
46
|
+
console.log('Exit:', r4.exitCode);
|
|
47
|
+
console.log();
|
|
48
|
+
|
|
49
|
+
// Test 5: exec again after wait
|
|
50
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
51
|
+
console.log('=== Test 5: Second exec (VM still alive?) ===');
|
|
52
|
+
const r5 = await mags.exec('sdk-test-vm', 'cat /root/sdk-test.txt && uptime');
|
|
53
|
+
console.log('Output:', r5.output?.trim());
|
|
54
|
+
console.log('Exit:', r5.exitCode);
|
|
55
|
+
console.log();
|
|
56
|
+
|
|
57
|
+
// Cleanup
|
|
58
|
+
console.log('=== Cleanup ===');
|
|
59
|
+
await mags.stop('sdk-test-vm');
|
|
60
|
+
console.log('Stopped.');
|
|
61
|
+
|
|
62
|
+
console.log('\n✓ All tests passed');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
main().catch(err => {
|
|
66
|
+
console.error('FAILED:', err.message);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|