@magpiecloud/mags 1.8.12 → 1.8.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/mags.js +213 -104
- package/index.js +6 -52
- package/nodejs/index.js +6 -52
- package/package.json +4 -1
- package/python/README.md +6 -2
- package/python/dist/magpie_mags-1.3.6-py3-none-any.whl +0 -0
- package/python/dist/magpie_mags-1.3.6.tar.gz +0 -0
- package/python/pyproject.toml +1 -1
- package/python/src/magpie_mags.egg-info/PKG-INFO +8 -4
- package/python/src/mags/client.py +12 -62
- package/website/api.html +139 -10
- package/website/claude-skill.html +2 -2
- package/website/cookbook/hn-marketing.html +1 -1
- package/website/cookbook.html +3 -3
- package/website/docs.html +677 -0
- package/website/index.html +111 -67
- package/website/login.html +2 -2
- package/website/styles.css +206 -39
- package/website/tokens.html +1 -1
- package/website/usage.html +1 -1
- package/python/dist/magpie_mags-1.3.4-py3-none-any.whl +0 -0
- package/python/dist/magpie_mags-1.3.4.tar.gz +0 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Execute scripts instantly on Magpie's microVM infrastructure. VMs boot in <100ms
|
|
|
6
6
|
|
|
7
7
|
Mags is a CLI and SDK for running scripts on ephemeral microVMs. Each execution gets its own isolated VM that:
|
|
8
8
|
|
|
9
|
-
- Boots in
|
|
9
|
+
- Boots in ~300ms (from warm pool)
|
|
10
10
|
- Supports optional S3-backed persistent workspaces (with `-p` flag)
|
|
11
11
|
- Syncs /root directory automatically when persistence is enabled
|
|
12
12
|
- Can expose public URLs for web services
|
|
@@ -419,7 +419,7 @@ mags run 'apk add ffmpeg imagemagick'
|
|
|
419
419
|
|
|
420
420
|
| Metric | Value |
|
|
421
421
|
|--------|-------|
|
|
422
|
-
| Warm start |
|
|
422
|
+
| Warm start | ~300ms |
|
|
423
423
|
| Cold start | ~4 seconds |
|
|
424
424
|
| Script overhead | ~50ms |
|
|
425
425
|
| Workspace sync | Every 30 seconds |
|
package/bin/mags.js
CHANGED
|
@@ -429,6 +429,23 @@ async function newVM(args) {
|
|
|
429
429
|
process.exit(1);
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
+
// Check if a VM with this name already exists (running or sleeping)
|
|
433
|
+
try {
|
|
434
|
+
const jobs = await request('GET', '/api/v1/mags-jobs');
|
|
435
|
+
const existing = (jobs.jobs || []).find(j =>
|
|
436
|
+
j.name === name && (j.status === 'running' || j.status === 'sleeping')
|
|
437
|
+
);
|
|
438
|
+
if (existing) {
|
|
439
|
+
log('yellow', `Sandbox '${name}' already exists (status: ${existing.status})`);
|
|
440
|
+
console.log(` SSH: mags ssh ${name}`);
|
|
441
|
+
console.log(` Exec: mags exec ${name} <command>`);
|
|
442
|
+
console.log(` Stop: mags stop ${name}`);
|
|
443
|
+
process.exit(0);
|
|
444
|
+
}
|
|
445
|
+
} catch (e) {
|
|
446
|
+
// Non-fatal — proceed with creation if list fails
|
|
447
|
+
}
|
|
448
|
+
|
|
432
449
|
const payload = {
|
|
433
450
|
script: 'sleep infinity',
|
|
434
451
|
type: 'inline',
|
|
@@ -1449,123 +1466,225 @@ async function browserSession(args) {
|
|
|
1449
1466
|
log('gray', `Stop with: mags stop ${workspace || requestId}`);
|
|
1450
1467
|
}
|
|
1451
1468
|
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1469
|
+
// WebSocket tunnel proxy — pipes stdin/stdout through a WebSocket to the agent SSH proxy.
|
|
1470
|
+
// Used as SSH ProxyCommand: ssh -o ProxyCommand="mags proxy <jobID>" root@mags-vm
|
|
1471
|
+
async function proxyTunnel(jobID) {
|
|
1472
|
+
const url = new URL(`/api/v1/mags-jobs/${jobID}/tunnel`, API_URL);
|
|
1473
|
+
const wsUrl = url.toString().replace(/^http/, 'ws');
|
|
1474
|
+
|
|
1475
|
+
// Dynamic import for WebSocket
|
|
1476
|
+
let WebSocket;
|
|
1477
|
+
try {
|
|
1478
|
+
WebSocket = require('ws');
|
|
1479
|
+
} catch {
|
|
1480
|
+
if (typeof globalThis.WebSocket !== 'undefined') {
|
|
1481
|
+
WebSocket = globalThis.WebSocket;
|
|
1482
|
+
} else {
|
|
1483
|
+
process.stderr.write('Error: ws package required. Run: npm install -g ws\n');
|
|
1484
|
+
process.exit(1);
|
|
1485
|
+
}
|
|
1462
1486
|
}
|
|
1463
1487
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1488
|
+
const ws = new WebSocket(wsUrl, {
|
|
1489
|
+
headers: {
|
|
1490
|
+
'Authorization': `Bearer ${API_TOKEN}`,
|
|
1491
|
+
},
|
|
1492
|
+
perMessageDeflate: false,
|
|
1493
|
+
rejectUnauthorized: false,
|
|
1494
|
+
});
|
|
1466
1495
|
|
|
1467
|
-
|
|
1496
|
+
// Buffer stdin data until WebSocket is open AND key is received
|
|
1497
|
+
const pendingData = [];
|
|
1498
|
+
let bridgeReady = false;
|
|
1499
|
+
let firstMessage = true;
|
|
1500
|
+
|
|
1501
|
+
process.stdin.on('data', (data) => {
|
|
1502
|
+
if (bridgeReady && ws.readyState === WebSocket.OPEN) {
|
|
1503
|
+
ws.send(data);
|
|
1504
|
+
} else {
|
|
1505
|
+
pendingData.push(Buffer.from(data));
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
ws.on('open', () => {
|
|
1510
|
+
// Don't flush yet — wait for first message (SSH key)
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
ws.on('message', (data, isBinary) => {
|
|
1514
|
+
if (firstMessage && !isBinary) {
|
|
1515
|
+
// First text message is the SSH private key — save to temp file
|
|
1516
|
+
firstMessage = false;
|
|
1517
|
+
const keyData = Buffer.from(data).toString('utf8');
|
|
1518
|
+
if (keyData.startsWith('-----BEGIN')) {
|
|
1519
|
+
const keyPath = path.join(os.tmpdir(), `mags_tunnel_key_${jobID}`);
|
|
1520
|
+
fs.writeFileSync(keyPath, keyData, { mode: 0o600 });
|
|
1521
|
+
}
|
|
1522
|
+
// Now start the SSH data bridge
|
|
1523
|
+
bridgeReady = true;
|
|
1524
|
+
for (const buf of pendingData) {
|
|
1525
|
+
ws.send(buf);
|
|
1526
|
+
}
|
|
1527
|
+
pendingData.length = 0;
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
process.stdout.write(Buffer.from(data));
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
ws.on('close', () => {
|
|
1534
|
+
process.exit(0);
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
ws.on('error', (err) => {
|
|
1538
|
+
process.stderr.write(`Tunnel error: ${err.message}\n`);
|
|
1539
|
+
process.exit(1);
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
process.stdin.on('end', () => {
|
|
1543
|
+
ws.close();
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// Fetch SSH key from tunnel endpoint (quick WS handshake)
|
|
1548
|
+
async function fetchTunnelKey(jobID) {
|
|
1549
|
+
const WebSocket = require('ws');
|
|
1550
|
+
const url = new URL(`/api/v1/mags-jobs/${jobID}/tunnel`, API_URL);
|
|
1551
|
+
const wsUrl = url.toString().replace(/^http/, 'ws');
|
|
1552
|
+
|
|
1553
|
+
return new Promise((resolve, reject) => {
|
|
1554
|
+
const ws = new WebSocket(wsUrl, {
|
|
1555
|
+
headers: { 'Authorization': `Bearer ${API_TOKEN}` },
|
|
1556
|
+
perMessageDeflate: false,
|
|
1557
|
+
rejectUnauthorized: false,
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
const timeout = setTimeout(() => {
|
|
1561
|
+
ws.close();
|
|
1562
|
+
reject(new Error('Tunnel key fetch timeout'));
|
|
1563
|
+
}, 60000);
|
|
1564
|
+
|
|
1565
|
+
ws.on('message', (data, isBinary) => {
|
|
1566
|
+
if (!isBinary) {
|
|
1567
|
+
const key = Buffer.from(data).toString('utf8');
|
|
1568
|
+
if (key.startsWith('-----BEGIN')) {
|
|
1569
|
+
clearTimeout(timeout);
|
|
1570
|
+
ws.close();
|
|
1571
|
+
resolve(key);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
// Not a key message — shouldn't happen as first message
|
|
1576
|
+
clearTimeout(timeout);
|
|
1577
|
+
ws.close();
|
|
1578
|
+
reject(new Error('No SSH key received from tunnel'));
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
ws.on('error', (err) => {
|
|
1582
|
+
clearTimeout(timeout);
|
|
1583
|
+
reject(err);
|
|
1584
|
+
});
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// Resolve a workspace name to a job ID, creating a new VM if needed
|
|
1589
|
+
async function resolveOrCreateJob(nameOrId) {
|
|
1590
|
+
const existingJob = await findWorkspaceJob(nameOrId);
|
|
1468
1591
|
|
|
1469
1592
|
if (existingJob && existingJob.status === 'running') {
|
|
1470
1593
|
log('green', `Found running VM for '${nameOrId}'`);
|
|
1471
|
-
|
|
1594
|
+
return existingJob.request_id;
|
|
1472
1595
|
} else if (existingJob && existingJob.status === 'sleeping') {
|
|
1473
1596
|
log('yellow', `Waking sleeping VM for '${nameOrId}'...`);
|
|
1474
|
-
|
|
1475
|
-
}
|
|
1476
|
-
// No running/sleeping VM — start a new persistent one
|
|
1477
|
-
log('blue', `Starting VM with workspace '${nameOrId}'...`);
|
|
1478
|
-
|
|
1479
|
-
const payload = {
|
|
1480
|
-
script: 'echo "SSH session ready" && sleep 3600',
|
|
1481
|
-
type: 'inline',
|
|
1482
|
-
workspace_id: nameOrId,
|
|
1483
|
-
persistent: true,
|
|
1484
|
-
startup_command: 'sleep 3600'
|
|
1485
|
-
};
|
|
1597
|
+
return existingJob.request_id;
|
|
1598
|
+
}
|
|
1486
1599
|
|
|
1487
|
-
|
|
1600
|
+
// No running/sleeping VM — start a new persistent one
|
|
1601
|
+
log('blue', `Starting VM with workspace '${nameOrId}'...`);
|
|
1602
|
+
const payload = {
|
|
1603
|
+
script: 'echo "SSH session ready" && sleep 3600',
|
|
1604
|
+
type: 'inline',
|
|
1605
|
+
workspace_id: nameOrId,
|
|
1606
|
+
persistent: true,
|
|
1607
|
+
startup_command: 'sleep 3600'
|
|
1608
|
+
};
|
|
1488
1609
|
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1610
|
+
const response = await request('POST', '/api/v1/mags-jobs', payload);
|
|
1611
|
+
if (!response.request_id) {
|
|
1612
|
+
log('red', 'Failed to start VM:');
|
|
1613
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1614
|
+
process.exit(1);
|
|
1615
|
+
}
|
|
1494
1616
|
|
|
1495
|
-
|
|
1496
|
-
|
|
1617
|
+
const jobID = response.request_id;
|
|
1618
|
+
log('gray', `Job: ${jobID}`);
|
|
1497
1619
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
process.exit(1);
|
|
1506
|
-
}
|
|
1507
|
-
process.stdout.write('.');
|
|
1508
|
-
await sleep(300);
|
|
1620
|
+
log('blue', 'Waiting for VM...');
|
|
1621
|
+
for (let i = 0; i < 60; i++) {
|
|
1622
|
+
const status = await request('GET', `/api/v1/mags-jobs/${jobID}/status`);
|
|
1623
|
+
if (status.status === 'running' && status.vm_id) break;
|
|
1624
|
+
if (status.status === 'error') {
|
|
1625
|
+
log('red', 'VM failed to start');
|
|
1626
|
+
process.exit(1);
|
|
1509
1627
|
}
|
|
1510
|
-
|
|
1628
|
+
process.stdout.write('.');
|
|
1629
|
+
await sleep(300);
|
|
1511
1630
|
}
|
|
1631
|
+
console.log('');
|
|
1632
|
+
return jobID;
|
|
1633
|
+
}
|
|
1512
1634
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1635
|
+
// Get SSH key for a job (calls EnableAccess to set up agent proxy and get key)
|
|
1636
|
+
async function getSSHKey(jobID) {
|
|
1515
1637
|
const accessResp = await request('POST', `/api/v1/mags-jobs/${jobID}/access`, { port: 22 });
|
|
1516
|
-
|
|
1517
1638
|
if (!accessResp.success) {
|
|
1518
1639
|
log('red', 'Failed to enable SSH access');
|
|
1519
|
-
if (accessResp.error)
|
|
1520
|
-
log('red', accessResp.error);
|
|
1521
|
-
}
|
|
1640
|
+
if (accessResp.error) log('red', accessResp.error);
|
|
1522
1641
|
process.exit(1);
|
|
1523
1642
|
}
|
|
1643
|
+
return accessResp.ssh_private_key;
|
|
1644
|
+
}
|
|
1524
1645
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
log('
|
|
1531
|
-
console.log(
|
|
1646
|
+
async function sshToJob(nameOrId) {
|
|
1647
|
+
if (!nameOrId) {
|
|
1648
|
+
log('red', 'Error: Workspace, job name, or job ID required');
|
|
1649
|
+
console.log(`\nUsage: mags ssh <workspace|name|id>\n`);
|
|
1650
|
+
console.log('Examples:');
|
|
1651
|
+
console.log(' mags ssh myproject # Auto-starts VM if needed');
|
|
1652
|
+
console.log(' mags ssh 7bd12031-25ff...');
|
|
1653
|
+
console.log('');
|
|
1654
|
+
console.log('Get job names/IDs with: mags list');
|
|
1532
1655
|
process.exit(1);
|
|
1533
1656
|
}
|
|
1534
1657
|
|
|
1535
|
-
|
|
1658
|
+
const jobID = await resolveOrCreateJob(nameOrId);
|
|
1659
|
+
|
|
1660
|
+
// Fetch SSH key from tunnel endpoint (also warms up the agent proxy)
|
|
1661
|
+
log('blue', 'Setting up tunnel...');
|
|
1662
|
+
const sshKey = await fetchTunnelKey(jobID);
|
|
1663
|
+
const magsPath = process.argv[1];
|
|
1664
|
+
const keyFile = path.join(os.tmpdir(), `mags_tunnel_key_${jobID}`);
|
|
1665
|
+
fs.writeFileSync(keyFile, sshKey, { mode: 0o600 });
|
|
1666
|
+
|
|
1667
|
+
log('green', `Connecting via secure tunnel...`);
|
|
1536
1668
|
console.log(`${colors.gray}(Use Ctrl+D or 'exit' to disconnect)${colors.reset}\n`);
|
|
1537
1669
|
|
|
1538
|
-
// Build SSH arguments
|
|
1539
1670
|
const sshArgs = [
|
|
1540
|
-
'-tt',
|
|
1671
|
+
'-tt',
|
|
1541
1672
|
'-o', 'StrictHostKeyChecking=no',
|
|
1542
1673
|
'-o', 'UserKnownHostsFile=/dev/null',
|
|
1543
1674
|
'-o', 'LogLevel=ERROR',
|
|
1544
|
-
'-
|
|
1675
|
+
'-o', `ProxyCommand=${magsPath} proxy ${jobID}`,
|
|
1676
|
+
'-i', keyFile,
|
|
1677
|
+
'root@mags-vm',
|
|
1545
1678
|
];
|
|
1546
1679
|
|
|
1547
|
-
// If API returned an SSH key, write it to a temp file and use it
|
|
1548
|
-
let keyFile = null;
|
|
1549
|
-
if (sshKey) {
|
|
1550
|
-
keyFile = path.join(os.tmpdir(), `mags_ssh_${Date.now()}`);
|
|
1551
|
-
fs.writeFileSync(keyFile, sshKey, { mode: 0o600 });
|
|
1552
|
-
sshArgs.push('-i', keyFile);
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
sshArgs.push(`root@${sshHost}`);
|
|
1556
|
-
|
|
1557
1680
|
const ssh = spawn('ssh', sshArgs, {
|
|
1558
|
-
stdio: 'inherit'
|
|
1681
|
+
stdio: 'inherit'
|
|
1559
1682
|
});
|
|
1560
1683
|
|
|
1561
1684
|
ssh.on('error', (err) => {
|
|
1562
|
-
|
|
1563
|
-
try { fs.unlinkSync(keyFile); } catch (e) {}
|
|
1564
|
-
}
|
|
1685
|
+
try { fs.unlinkSync(keyFile); } catch (e) {}
|
|
1565
1686
|
if (err.code === 'ENOENT') {
|
|
1566
1687
|
log('red', 'SSH client not found. Please install OpenSSH.');
|
|
1567
|
-
log('gray', 'On macOS/Linux: ssh is usually pre-installed');
|
|
1568
|
-
log('gray', 'On Windows: Install OpenSSH or use WSL');
|
|
1569
1688
|
} else {
|
|
1570
1689
|
log('red', `SSH error: ${err.message}`);
|
|
1571
1690
|
}
|
|
@@ -1573,10 +1692,7 @@ async function sshToJob(nameOrId) {
|
|
|
1573
1692
|
});
|
|
1574
1693
|
|
|
1575
1694
|
ssh.on('close', (code) => {
|
|
1576
|
-
|
|
1577
|
-
if (keyFile) {
|
|
1578
|
-
try { fs.unlinkSync(keyFile); } catch (e) {}
|
|
1579
|
-
}
|
|
1695
|
+
try { fs.unlinkSync(keyFile); } catch (e) {}
|
|
1580
1696
|
if (code === 0) {
|
|
1581
1697
|
log('green', '\nSSH session ended');
|
|
1582
1698
|
} else {
|
|
@@ -1612,31 +1728,21 @@ async function execOnJob(nameOrId, command) {
|
|
|
1612
1728
|
process.exit(1);
|
|
1613
1729
|
}
|
|
1614
1730
|
|
|
1615
|
-
//
|
|
1616
|
-
log('blue', '
|
|
1617
|
-
const
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
if (accessResp.error) log('red', accessResp.error);
|
|
1622
|
-
process.exit(1);
|
|
1623
|
-
}
|
|
1731
|
+
// Fetch SSH key from tunnel endpoint (also warms up the agent proxy)
|
|
1732
|
+
log('blue', 'Setting up tunnel...');
|
|
1733
|
+
const sshKey = await fetchTunnelKey(jobID);
|
|
1734
|
+
const magsPath = process.argv[1];
|
|
1735
|
+
const keyFile = path.join(os.tmpdir(), `mags_tunnel_key_${jobID}`);
|
|
1736
|
+
fs.writeFileSync(keyFile, sshKey, { mode: 0o600 });
|
|
1624
1737
|
|
|
1625
|
-
// Write SSH key to temp file
|
|
1626
|
-
let keyFile = null;
|
|
1627
1738
|
const sshArgs = [
|
|
1628
1739
|
'-o', 'StrictHostKeyChecking=no',
|
|
1629
1740
|
'-o', 'UserKnownHostsFile=/dev/null',
|
|
1630
1741
|
'-o', 'LogLevel=ERROR',
|
|
1631
|
-
'-
|
|
1742
|
+
'-o', `ProxyCommand=${magsPath} proxy ${jobID}`,
|
|
1743
|
+
'-i', keyFile,
|
|
1632
1744
|
];
|
|
1633
1745
|
|
|
1634
|
-
if (accessResp.ssh_private_key) {
|
|
1635
|
-
keyFile = path.join(os.tmpdir(), `mags_ssh_${Date.now()}`);
|
|
1636
|
-
fs.writeFileSync(keyFile, accessResp.ssh_private_key, { mode: 0o600 });
|
|
1637
|
-
sshArgs.push('-i', keyFile);
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
1746
|
// Wrap command to use chroot if overlay is mounted, with proper env
|
|
1641
1747
|
const escaped = command.replace(/'/g, "'\\''");
|
|
1642
1748
|
const wrappedCmd = `if [ -d /overlay/bin ]; then chroot /overlay /bin/sh -l -c 'cd /root 2>/dev/null; ${escaped}'; else cd /root 2>/dev/null; ${escaped}; fi`;
|
|
@@ -1644,18 +1750,18 @@ async function execOnJob(nameOrId, command) {
|
|
|
1644
1750
|
if (process.stdin.isTTY) {
|
|
1645
1751
|
sshArgs.push('-t');
|
|
1646
1752
|
}
|
|
1647
|
-
sshArgs.push(
|
|
1753
|
+
sshArgs.push('root@mags-vm', wrappedCmd);
|
|
1648
1754
|
|
|
1649
1755
|
const ssh = spawn('ssh', sshArgs, { stdio: 'inherit' });
|
|
1650
1756
|
|
|
1651
1757
|
ssh.on('error', (err) => {
|
|
1652
|
-
|
|
1758
|
+
try { fs.unlinkSync(keyFile); } catch (e) {}
|
|
1653
1759
|
log('red', `SSH error: ${err.message}`);
|
|
1654
1760
|
process.exit(1);
|
|
1655
1761
|
});
|
|
1656
1762
|
|
|
1657
1763
|
ssh.on('close', (code) => {
|
|
1658
|
-
|
|
1764
|
+
try { fs.unlinkSync(keyFile); } catch (e) {}
|
|
1659
1765
|
process.exit(code || 0);
|
|
1660
1766
|
});
|
|
1661
1767
|
}
|
|
@@ -1751,6 +1857,9 @@ async function main() {
|
|
|
1751
1857
|
case 'setup-claude':
|
|
1752
1858
|
await setupClaude();
|
|
1753
1859
|
break;
|
|
1860
|
+
case 'proxy':
|
|
1861
|
+
await proxyTunnel(args[1]);
|
|
1862
|
+
break;
|
|
1754
1863
|
default:
|
|
1755
1864
|
if (!command) {
|
|
1756
1865
|
// No command - check if logged in
|
package/index.js
CHANGED
|
@@ -333,7 +333,7 @@ class Mags {
|
|
|
333
333
|
}
|
|
334
334
|
|
|
335
335
|
/**
|
|
336
|
-
* Execute a command on an existing running/sleeping VM via
|
|
336
|
+
* Execute a command on an existing running/sleeping VM via HTTP exec endpoint
|
|
337
337
|
* @param {string} nameOrId - Job name, workspace ID, or request ID
|
|
338
338
|
* @param {string} command - Command to execute
|
|
339
339
|
* @param {object} options - Options
|
|
@@ -350,58 +350,12 @@ class Mags {
|
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
const requestId = job.request_id || job.id;
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const { execFile } = require('child_process');
|
|
360
|
-
const fs = require('fs');
|
|
361
|
-
const os = require('os');
|
|
362
|
-
const path = require('path');
|
|
363
|
-
|
|
364
|
-
const escaped = command.replace(/'/g, "'\\''");
|
|
365
|
-
const wrapped =
|
|
366
|
-
`if [ -d /overlay/bin ]; then ` +
|
|
367
|
-
`chroot /overlay /bin/sh -l -c 'cd /root 2>/dev/null; ${escaped}'; ` +
|
|
368
|
-
`else cd /root 2>/dev/null; ${escaped}; fi`;
|
|
369
|
-
|
|
370
|
-
let keyFile = null;
|
|
371
|
-
try {
|
|
372
|
-
const sshArgs = [
|
|
373
|
-
'-o', 'StrictHostKeyChecking=no',
|
|
374
|
-
'-o', 'UserKnownHostsFile=/dev/null',
|
|
375
|
-
'-o', 'LogLevel=ERROR',
|
|
376
|
-
'-p', String(access.ssh_port),
|
|
377
|
-
];
|
|
378
|
-
|
|
379
|
-
if (access.ssh_private_key) {
|
|
380
|
-
keyFile = path.join(os.tmpdir(), `mags_ssh_${Date.now()}`);
|
|
381
|
-
fs.writeFileSync(keyFile, access.ssh_private_key, { mode: 0o600 });
|
|
382
|
-
sshArgs.push('-i', keyFile);
|
|
383
|
-
}
|
|
353
|
+
const resp = await this._request('POST', `/api/v1/mags-jobs/${requestId}/exec`, {
|
|
354
|
+
command,
|
|
355
|
+
timeout: Math.ceil(timeout / 1000),
|
|
356
|
+
});
|
|
384
357
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return new Promise((resolve, reject) => {
|
|
388
|
-
execFile('ssh', sshArgs, { timeout, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
389
|
-
if (keyFile) try { fs.unlinkSync(keyFile); } catch {}
|
|
390
|
-
if (err && err.killed) {
|
|
391
|
-
reject(new MagsError(`Command timed out after ${timeout}ms`));
|
|
392
|
-
} else {
|
|
393
|
-
resolve({
|
|
394
|
-
exitCode: err ? err.code || 1 : 0,
|
|
395
|
-
output: stdout,
|
|
396
|
-
stderr: stderr,
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
});
|
|
401
|
-
} catch (e) {
|
|
402
|
-
if (keyFile) try { require('fs').unlinkSync(keyFile); } catch {}
|
|
403
|
-
throw e;
|
|
404
|
-
}
|
|
358
|
+
return { exitCode: resp.exit_code, output: resp.stdout, stderr: resp.stderr };
|
|
405
359
|
}
|
|
406
360
|
|
|
407
361
|
/**
|
package/nodejs/index.js
CHANGED
|
@@ -328,7 +328,7 @@ class Mags {
|
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
/**
|
|
331
|
-
* Execute a command on an existing running/sleeping VM via
|
|
331
|
+
* Execute a command on an existing running/sleeping VM via HTTP exec endpoint
|
|
332
332
|
* @param {string} nameOrId - Job name, workspace ID, or request ID
|
|
333
333
|
* @param {string} command - Command to execute
|
|
334
334
|
* @param {object} options - Options
|
|
@@ -345,58 +345,12 @@ class Mags {
|
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
const requestId = job.request_id || job.id;
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const { execFileSync, execFile } = require('child_process');
|
|
355
|
-
const fs = require('fs');
|
|
356
|
-
const os = require('os');
|
|
357
|
-
const path = require('path');
|
|
358
|
-
|
|
359
|
-
const escaped = command.replace(/'/g, "'\\''");
|
|
360
|
-
const wrapped =
|
|
361
|
-
`if [ -d /overlay/bin ]; then ` +
|
|
362
|
-
`chroot /overlay /bin/sh -l -c 'cd /root 2>/dev/null; ${escaped}'; ` +
|
|
363
|
-
`else cd /root 2>/dev/null; ${escaped}; fi`;
|
|
364
|
-
|
|
365
|
-
let keyFile = null;
|
|
366
|
-
try {
|
|
367
|
-
const sshArgs = [
|
|
368
|
-
'-o', 'StrictHostKeyChecking=no',
|
|
369
|
-
'-o', 'UserKnownHostsFile=/dev/null',
|
|
370
|
-
'-o', 'LogLevel=ERROR',
|
|
371
|
-
'-p', String(access.ssh_port),
|
|
372
|
-
];
|
|
373
|
-
|
|
374
|
-
if (access.ssh_private_key) {
|
|
375
|
-
keyFile = path.join(os.tmpdir(), `mags_ssh_${Date.now()}`);
|
|
376
|
-
fs.writeFileSync(keyFile, access.ssh_private_key, { mode: 0o600 });
|
|
377
|
-
sshArgs.push('-i', keyFile);
|
|
378
|
-
}
|
|
348
|
+
const resp = await this._request('POST', `/api/v1/mags-jobs/${requestId}/exec`, {
|
|
349
|
+
command,
|
|
350
|
+
timeout: Math.ceil(timeout / 1000),
|
|
351
|
+
});
|
|
379
352
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
return new Promise((resolve, reject) => {
|
|
383
|
-
const proc = execFile('ssh', sshArgs, { timeout, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
384
|
-
if (keyFile) try { fs.unlinkSync(keyFile); } catch {}
|
|
385
|
-
if (err && err.killed) {
|
|
386
|
-
reject(new MagsError(`Command timed out after ${timeout}ms`));
|
|
387
|
-
} else {
|
|
388
|
-
resolve({
|
|
389
|
-
exitCode: err ? err.code || 1 : 0,
|
|
390
|
-
output: stdout,
|
|
391
|
-
stderr: stderr,
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
});
|
|
395
|
-
});
|
|
396
|
-
} catch (e) {
|
|
397
|
-
if (keyFile) try { require('fs').unlinkSync(keyFile); } catch {}
|
|
398
|
-
throw e;
|
|
399
|
-
}
|
|
353
|
+
return { exitCode: resp.exit_code, output: resp.stdout, stderr: resp.stderr };
|
|
400
354
|
}
|
|
401
355
|
|
|
402
356
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpiecloud/mags",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.14",
|
|
4
4
|
"description": "Mags CLI - Execute scripts on Magpie's instant VM infrastructure",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -23,5 +23,8 @@
|
|
|
23
23
|
"homepage": "https://mags.run",
|
|
24
24
|
"engines": {
|
|
25
25
|
"node": ">=14.0.0"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"ws": "^8.18.0"
|
|
26
29
|
}
|
|
27
30
|
}
|
package/python/README.md
CHANGED
|
@@ -135,15 +135,19 @@ print(f"Jobs: {usage['total_jobs']}, VM seconds: {usage['vm_seconds']:.0f}")
|
|
|
135
135
|
| `find_job(name_or_id)` | Find a running/sleeping job by name or workspace |
|
|
136
136
|
| `exec(name_or_id, command)` | Run a command on an existing VM via SSH |
|
|
137
137
|
| `stop(name_or_id)` | Stop a running job |
|
|
138
|
+
| `url(name_or_id, port)` | Enable public URL and return full URL |
|
|
138
139
|
| `resize(workspace, disk_gb)` | Resize a workspace's disk |
|
|
139
140
|
| `status(request_id)` | Get job status |
|
|
140
141
|
| `logs(request_id)` | Get job logs |
|
|
141
142
|
| `list_jobs(page, page_size)` | List recent jobs |
|
|
142
|
-
| `update_job(request_id,
|
|
143
|
+
| `update_job(request_id, **opts)` | Update job config (`startup_command`, `no_sleep`) |
|
|
143
144
|
| `enable_access(request_id, port)` | Enable URL or SSH access |
|
|
144
145
|
| `usage(window_days)` | Get usage summary |
|
|
145
146
|
| `upload_file(path)` | Upload a file, returns file ID |
|
|
146
147
|
| `upload_files(paths)` | Upload multiple files |
|
|
148
|
+
| `url_alias_create(sub, ws_id)` | Create a stable URL alias |
|
|
149
|
+
| `url_alias_list()` | List URL aliases |
|
|
150
|
+
| `url_alias_delete(sub)` | Delete a URL alias |
|
|
147
151
|
| `cron_create(**opts)` | Create a cron job |
|
|
148
152
|
| `cron_list()` | List cron jobs |
|
|
149
153
|
| `cron_get(id)` | Get a cron job |
|
|
@@ -154,4 +158,4 @@ print(f"Jobs: {usage['total_jobs']}, VM seconds: {usage['vm_seconds']:.0f}")
|
|
|
154
158
|
|
|
155
159
|
- Website: [mags.run](https://mags.run)
|
|
156
160
|
- Node.js SDK: `npm install @magpiecloud/mags`
|
|
157
|
-
- CLI: `
|
|
161
|
+
- CLI: `npm install -g @magpiecloud/mags`
|
|
Binary file
|
|
Binary file
|
package/python/pyproject.toml
CHANGED