@magpiecloud/mags 1.8.13 → 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/bin/mags.js +196 -104
- package/index.js +6 -52
- package/nodejs/index.js +6 -52
- package/package.json +4 -1
- package/python/dist/{magpie_mags-1.3.5-py3-none-any.whl → 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 +7 -3
- package/python/src/mags/client.py +12 -62
- package/website/api.html +6 -6
- 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 +3 -3
- 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.5.tar.gz +0 -0
package/bin/mags.js
CHANGED
|
@@ -1466,123 +1466,225 @@ async function browserSession(args) {
|
|
|
1466
1466
|
log('gray', `Stop with: mags stop ${workspace || requestId}`);
|
|
1467
1467
|
}
|
|
1468
1468
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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
|
+
}
|
|
1479
1486
|
}
|
|
1480
1487
|
|
|
1481
|
-
|
|
1482
|
-
|
|
1488
|
+
const ws = new WebSocket(wsUrl, {
|
|
1489
|
+
headers: {
|
|
1490
|
+
'Authorization': `Bearer ${API_TOKEN}`,
|
|
1491
|
+
},
|
|
1492
|
+
perMessageDeflate: false,
|
|
1493
|
+
rejectUnauthorized: false,
|
|
1494
|
+
});
|
|
1483
1495
|
|
|
1484
|
-
|
|
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);
|
|
1485
1591
|
|
|
1486
1592
|
if (existingJob && existingJob.status === 'running') {
|
|
1487
1593
|
log('green', `Found running VM for '${nameOrId}'`);
|
|
1488
|
-
|
|
1594
|
+
return existingJob.request_id;
|
|
1489
1595
|
} else if (existingJob && existingJob.status === 'sleeping') {
|
|
1490
1596
|
log('yellow', `Waking sleeping VM for '${nameOrId}'...`);
|
|
1491
|
-
|
|
1492
|
-
}
|
|
1493
|
-
// No running/sleeping VM — start a new persistent one
|
|
1494
|
-
log('blue', `Starting VM with workspace '${nameOrId}'...`);
|
|
1495
|
-
|
|
1496
|
-
const payload = {
|
|
1497
|
-
script: 'echo "SSH session ready" && sleep 3600',
|
|
1498
|
-
type: 'inline',
|
|
1499
|
-
workspace_id: nameOrId,
|
|
1500
|
-
persistent: true,
|
|
1501
|
-
startup_command: 'sleep 3600'
|
|
1502
|
-
};
|
|
1597
|
+
return existingJob.request_id;
|
|
1598
|
+
}
|
|
1503
1599
|
|
|
1504
|
-
|
|
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
|
+
};
|
|
1505
1609
|
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
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
|
+
}
|
|
1511
1616
|
|
|
1512
|
-
|
|
1513
|
-
|
|
1617
|
+
const jobID = response.request_id;
|
|
1618
|
+
log('gray', `Job: ${jobID}`);
|
|
1514
1619
|
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
process.exit(1);
|
|
1523
|
-
}
|
|
1524
|
-
process.stdout.write('.');
|
|
1525
|
-
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);
|
|
1526
1627
|
}
|
|
1527
|
-
|
|
1628
|
+
process.stdout.write('.');
|
|
1629
|
+
await sleep(300);
|
|
1528
1630
|
}
|
|
1631
|
+
console.log('');
|
|
1632
|
+
return jobID;
|
|
1633
|
+
}
|
|
1529
1634
|
|
|
1530
|
-
|
|
1531
|
-
|
|
1635
|
+
// Get SSH key for a job (calls EnableAccess to set up agent proxy and get key)
|
|
1636
|
+
async function getSSHKey(jobID) {
|
|
1532
1637
|
const accessResp = await request('POST', `/api/v1/mags-jobs/${jobID}/access`, { port: 22 });
|
|
1533
|
-
|
|
1534
1638
|
if (!accessResp.success) {
|
|
1535
1639
|
log('red', 'Failed to enable SSH access');
|
|
1536
|
-
if (accessResp.error)
|
|
1537
|
-
log('red', accessResp.error);
|
|
1538
|
-
}
|
|
1640
|
+
if (accessResp.error) log('red', accessResp.error);
|
|
1539
1641
|
process.exit(1);
|
|
1540
1642
|
}
|
|
1643
|
+
return accessResp.ssh_private_key;
|
|
1644
|
+
}
|
|
1541
1645
|
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
log('
|
|
1548
|
-
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');
|
|
1549
1655
|
process.exit(1);
|
|
1550
1656
|
}
|
|
1551
1657
|
|
|
1552
|
-
|
|
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...`);
|
|
1553
1668
|
console.log(`${colors.gray}(Use Ctrl+D or 'exit' to disconnect)${colors.reset}\n`);
|
|
1554
1669
|
|
|
1555
|
-
// Build SSH arguments
|
|
1556
1670
|
const sshArgs = [
|
|
1557
|
-
'-tt',
|
|
1671
|
+
'-tt',
|
|
1558
1672
|
'-o', 'StrictHostKeyChecking=no',
|
|
1559
1673
|
'-o', 'UserKnownHostsFile=/dev/null',
|
|
1560
1674
|
'-o', 'LogLevel=ERROR',
|
|
1561
|
-
'-
|
|
1675
|
+
'-o', `ProxyCommand=${magsPath} proxy ${jobID}`,
|
|
1676
|
+
'-i', keyFile,
|
|
1677
|
+
'root@mags-vm',
|
|
1562
1678
|
];
|
|
1563
1679
|
|
|
1564
|
-
// If API returned an SSH key, write it to a temp file and use it
|
|
1565
|
-
let keyFile = null;
|
|
1566
|
-
if (sshKey) {
|
|
1567
|
-
keyFile = path.join(os.tmpdir(), `mags_ssh_${Date.now()}`);
|
|
1568
|
-
fs.writeFileSync(keyFile, sshKey, { mode: 0o600 });
|
|
1569
|
-
sshArgs.push('-i', keyFile);
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
sshArgs.push(`root@${sshHost}`);
|
|
1573
|
-
|
|
1574
1680
|
const ssh = spawn('ssh', sshArgs, {
|
|
1575
|
-
stdio: 'inherit'
|
|
1681
|
+
stdio: 'inherit'
|
|
1576
1682
|
});
|
|
1577
1683
|
|
|
1578
1684
|
ssh.on('error', (err) => {
|
|
1579
|
-
|
|
1580
|
-
try { fs.unlinkSync(keyFile); } catch (e) {}
|
|
1581
|
-
}
|
|
1685
|
+
try { fs.unlinkSync(keyFile); } catch (e) {}
|
|
1582
1686
|
if (err.code === 'ENOENT') {
|
|
1583
1687
|
log('red', 'SSH client not found. Please install OpenSSH.');
|
|
1584
|
-
log('gray', 'On macOS/Linux: ssh is usually pre-installed');
|
|
1585
|
-
log('gray', 'On Windows: Install OpenSSH or use WSL');
|
|
1586
1688
|
} else {
|
|
1587
1689
|
log('red', `SSH error: ${err.message}`);
|
|
1588
1690
|
}
|
|
@@ -1590,10 +1692,7 @@ async function sshToJob(nameOrId) {
|
|
|
1590
1692
|
});
|
|
1591
1693
|
|
|
1592
1694
|
ssh.on('close', (code) => {
|
|
1593
|
-
|
|
1594
|
-
if (keyFile) {
|
|
1595
|
-
try { fs.unlinkSync(keyFile); } catch (e) {}
|
|
1596
|
-
}
|
|
1695
|
+
try { fs.unlinkSync(keyFile); } catch (e) {}
|
|
1597
1696
|
if (code === 0) {
|
|
1598
1697
|
log('green', '\nSSH session ended');
|
|
1599
1698
|
} else {
|
|
@@ -1629,31 +1728,21 @@ async function execOnJob(nameOrId, command) {
|
|
|
1629
1728
|
process.exit(1);
|
|
1630
1729
|
}
|
|
1631
1730
|
|
|
1632
|
-
//
|
|
1633
|
-
log('blue', '
|
|
1634
|
-
const
|
|
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 });
|
|
1635
1737
|
|
|
1636
|
-
if (!accessResp.success || !accessResp.ssh_host || !accessResp.ssh_port) {
|
|
1637
|
-
log('red', 'Failed to enable SSH access');
|
|
1638
|
-
if (accessResp.error) log('red', accessResp.error);
|
|
1639
|
-
process.exit(1);
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
// Write SSH key to temp file
|
|
1643
|
-
let keyFile = null;
|
|
1644
1738
|
const sshArgs = [
|
|
1645
1739
|
'-o', 'StrictHostKeyChecking=no',
|
|
1646
1740
|
'-o', 'UserKnownHostsFile=/dev/null',
|
|
1647
1741
|
'-o', 'LogLevel=ERROR',
|
|
1648
|
-
'-
|
|
1742
|
+
'-o', `ProxyCommand=${magsPath} proxy ${jobID}`,
|
|
1743
|
+
'-i', keyFile,
|
|
1649
1744
|
];
|
|
1650
1745
|
|
|
1651
|
-
if (accessResp.ssh_private_key) {
|
|
1652
|
-
keyFile = path.join(os.tmpdir(), `mags_ssh_${Date.now()}`);
|
|
1653
|
-
fs.writeFileSync(keyFile, accessResp.ssh_private_key, { mode: 0o600 });
|
|
1654
|
-
sshArgs.push('-i', keyFile);
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
1746
|
// Wrap command to use chroot if overlay is mounted, with proper env
|
|
1658
1747
|
const escaped = command.replace(/'/g, "'\\''");
|
|
1659
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`;
|
|
@@ -1661,18 +1750,18 @@ async function execOnJob(nameOrId, command) {
|
|
|
1661
1750
|
if (process.stdin.isTTY) {
|
|
1662
1751
|
sshArgs.push('-t');
|
|
1663
1752
|
}
|
|
1664
|
-
sshArgs.push(
|
|
1753
|
+
sshArgs.push('root@mags-vm', wrappedCmd);
|
|
1665
1754
|
|
|
1666
1755
|
const ssh = spawn('ssh', sshArgs, { stdio: 'inherit' });
|
|
1667
1756
|
|
|
1668
1757
|
ssh.on('error', (err) => {
|
|
1669
|
-
|
|
1758
|
+
try { fs.unlinkSync(keyFile); } catch (e) {}
|
|
1670
1759
|
log('red', `SSH error: ${err.message}`);
|
|
1671
1760
|
process.exit(1);
|
|
1672
1761
|
});
|
|
1673
1762
|
|
|
1674
1763
|
ssh.on('close', (code) => {
|
|
1675
|
-
|
|
1764
|
+
try { fs.unlinkSync(keyFile); } catch (e) {}
|
|
1676
1765
|
process.exit(code || 0);
|
|
1677
1766
|
});
|
|
1678
1767
|
}
|
|
@@ -1768,6 +1857,9 @@ async function main() {
|
|
|
1768
1857
|
case 'setup-claude':
|
|
1769
1858
|
await setupClaude();
|
|
1770
1859
|
break;
|
|
1860
|
+
case 'proxy':
|
|
1861
|
+
await proxyTunnel(args[1]);
|
|
1862
|
+
break;
|
|
1771
1863
|
default:
|
|
1772
1864
|
if (!command) {
|
|
1773
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/dist/{magpie_mags-1.3.5-py3-none-any.whl → magpie_mags-1.3.6-py3-none-any.whl}
RENAMED
|
Binary file
|
|
Binary file
|
package/python/pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: magpie-mags
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.6
|
|
4
4
|
Summary: Mags SDK - Execute scripts on Magpie's instant VM infrastructure
|
|
5
5
|
Author: Magpie Cloud
|
|
6
6
|
License: MIT
|
|
@@ -160,15 +160,19 @@ print(f"Jobs: {usage['total_jobs']}, VM seconds: {usage['vm_seconds']:.0f}")
|
|
|
160
160
|
| `find_job(name_or_id)` | Find a running/sleeping job by name or workspace |
|
|
161
161
|
| `exec(name_or_id, command)` | Run a command on an existing VM via SSH |
|
|
162
162
|
| `stop(name_or_id)` | Stop a running job |
|
|
163
|
+
| `url(name_or_id, port)` | Enable public URL and return full URL |
|
|
163
164
|
| `resize(workspace, disk_gb)` | Resize a workspace's disk |
|
|
164
165
|
| `status(request_id)` | Get job status |
|
|
165
166
|
| `logs(request_id)` | Get job logs |
|
|
166
167
|
| `list_jobs(page, page_size)` | List recent jobs |
|
|
167
|
-
| `update_job(request_id,
|
|
168
|
+
| `update_job(request_id, **opts)` | Update job config (`startup_command`, `no_sleep`) |
|
|
168
169
|
| `enable_access(request_id, port)` | Enable URL or SSH access |
|
|
169
170
|
| `usage(window_days)` | Get usage summary |
|
|
170
171
|
| `upload_file(path)` | Upload a file, returns file ID |
|
|
171
172
|
| `upload_files(paths)` | Upload multiple files |
|
|
173
|
+
| `url_alias_create(sub, ws_id)` | Create a stable URL alias |
|
|
174
|
+
| `url_alias_list()` | List URL aliases |
|
|
175
|
+
| `url_alias_delete(sub)` | Delete a URL alias |
|
|
172
176
|
| `cron_create(**opts)` | Create a cron job |
|
|
173
177
|
| `cron_list()` | List cron jobs |
|
|
174
178
|
| `cron_get(id)` | Get a cron job |
|
|
@@ -179,4 +183,4 @@ print(f"Jobs: {usage['total_jobs']}, VM seconds: {usage['vm_seconds']:.0f}")
|
|
|
179
183
|
|
|
180
184
|
- Website: [mags.run](https://mags.run)
|
|
181
185
|
- Node.js SDK: `npm install @magpiecloud/mags`
|
|
182
|
-
- CLI: `
|
|
186
|
+
- CLI: `npm install -g @magpiecloud/mags`
|