@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 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', `/api/v1/mags-jobs?page=1&page_size=50`);
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', `/api/v1/mags-jobs?page=1&page_size=50`);
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', '/api/v1/mags-jobs?page=1&page_size=1');
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', '/api/v1/mags-jobs?page=1&page_size=1');
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', '/api/v1/mags-jobs');
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', '/api/v1/mags-jobs', payload);
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', `/api/v1/mags-jobs/${response.request_id}/status`);
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', '/api/v1/mags-jobs', payload);
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', `/api/v1/mags-jobs/${requestId}/status`);
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', `/api/v1/mags-jobs/${requestId}/access`, { port });
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', `/api/v1/mags-jobs/${requestId}/logs`);
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', `/api/v1/mags-jobs/${requestId}/access`, { port });
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', `/api/v1/mags-jobs/${requestId}/status`);
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', '/api/v1/mags-url-aliases', {
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', '/api/v1/mags-url-aliases');
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', `/api/v1/mags-url-aliases/${subdomain}`);
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', `/api/v1/mags-jobs/${requestId}/status`);
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', `/api/v1/mags-jobs/${requestId}/logs`);
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', '/api/v1/mags-jobs?page=1&page_size=10');
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', `/api/v1/mags-jobs/${requestId}/stop`);
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', `/api/v1/mags-jobs/${requestId}`, payload);
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', `/api/v1/mags-jobs/${existingJob.request_id}/sync`);
952
+ await request('POST', `${apiPath('mags-jobs')}/${existingJob.request_id}/sync`);
939
953
  }
940
954
  log('blue', `Stopping existing VM...`);
941
- await request('POST', `/api/v1/mags-jobs/${existingJob.request_id}/stop`);
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', '/api/v1/mags-jobs', payload);
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', `/api/v1/mags-jobs/${requestId}/sync`);
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('/api/v1/mags-files', API_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', '/api/v1/mags-cron', payload);
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', '/api/v1/mags-cron');
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', `/api/v1/mags-cron/${id}`);
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', `/api/v1/mags-cron/${id}`, { enabled });
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', '/api/v1/mags-workspaces');
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', `/api/v1/mags-workspaces/${workspaceId}`);
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', `/api/v1/mags-jobs/${existingJob.request_id}/status`);
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', '/api/v1/mags-jobs', payload);
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', `/api/v1/mags-jobs/${requestId}/status`);
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(`/api/v1/mags-jobs/${jobID}/tunnel`, API_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(`/api/v1/mags-jobs/${jobID}/tunnel`, API_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', '/api/v1/mags-jobs', payload);
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', `/api/v1/mags-jobs/${jobID}/status`);
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', `/api/v1/mags-jobs/${jobID}/access`, { port: 22 });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpiecloud/mags",
3
- "version": "1.8.17",
3
+ "version": "1.9.0",
4
4
  "description": "Mags CLI & SDK - Execute commands and scripts on Mags Sandboxes",
5
5
  "main": "index.js",
6
6
  "bin": {
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
+ });