@magpiecloud/mags 1.7.1 → 1.7.2
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 +75 -0
- package/package.json +1 -1
package/bin/mags.js
CHANGED
|
@@ -202,6 +202,7 @@ ${colors.bold}Commands:${colors.reset}
|
|
|
202
202
|
new <name> Create a new persistent VM (returns ID only)
|
|
203
203
|
run [options] <script> Execute a script on a microVM
|
|
204
204
|
ssh <workspace|name|id> Open SSH session (auto-starts VM if needed)
|
|
205
|
+
exec <workspace> <command> Run a command on an existing VM
|
|
205
206
|
status <name|id> Get job status
|
|
206
207
|
logs <name|id> Get job logs
|
|
207
208
|
list List recent jobs
|
|
@@ -240,6 +241,7 @@ ${colors.bold}Examples:${colors.reset}
|
|
|
240
241
|
mags login
|
|
241
242
|
mags new myvm # Create VM, get ID
|
|
242
243
|
mags ssh myvm # SSH (auto-starts if needed)
|
|
244
|
+
mags exec myvm 'ls -la' # Run command on existing VM
|
|
243
245
|
mags run 'echo Hello World'
|
|
244
246
|
mags run -e 'echo fast' # Ephemeral (no S3 sync)
|
|
245
247
|
mags run -f script.py 'python3 script.py' # Upload + run file
|
|
@@ -1241,6 +1243,75 @@ async function sshToJob(nameOrId) {
|
|
|
1241
1243
|
});
|
|
1242
1244
|
}
|
|
1243
1245
|
|
|
1246
|
+
async function execOnJob(nameOrId, command) {
|
|
1247
|
+
if (!nameOrId || !command) {
|
|
1248
|
+
log('red', 'Error: Workspace and command required');
|
|
1249
|
+
console.log(`\nUsage: mags exec <workspace|name|id> <command>\n`);
|
|
1250
|
+
console.log('Examples:');
|
|
1251
|
+
console.log(' mags exec myproject "ls -la"');
|
|
1252
|
+
console.log(' mags exec myproject "node --version"');
|
|
1253
|
+
process.exit(1);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// Find a running/sleeping job for this workspace
|
|
1257
|
+
const existingJob = await findWorkspaceJob(nameOrId);
|
|
1258
|
+
|
|
1259
|
+
let jobID;
|
|
1260
|
+
if (existingJob && existingJob.status === 'running') {
|
|
1261
|
+
log('green', `Found running VM for '${nameOrId}'`);
|
|
1262
|
+
jobID = existingJob.request_id;
|
|
1263
|
+
} else if (existingJob && existingJob.status === 'sleeping') {
|
|
1264
|
+
log('yellow', `Waking sleeping VM for '${nameOrId}'...`);
|
|
1265
|
+
jobID = existingJob.request_id;
|
|
1266
|
+
} else {
|
|
1267
|
+
log('red', `No running or sleeping VM found for '${nameOrId}'`);
|
|
1268
|
+
log('gray', `Start one with: mags new ${nameOrId}`);
|
|
1269
|
+
process.exit(1);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// Enable SSH access
|
|
1273
|
+
log('blue', 'Enabling SSH access...');
|
|
1274
|
+
const accessResp = await request('POST', `/api/v1/mags-jobs/${jobID}/access`, { port: 22 });
|
|
1275
|
+
|
|
1276
|
+
if (!accessResp.success || !accessResp.ssh_host || !accessResp.ssh_port) {
|
|
1277
|
+
log('red', 'Failed to enable SSH access');
|
|
1278
|
+
if (accessResp.error) log('red', accessResp.error);
|
|
1279
|
+
process.exit(1);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Write SSH key to temp file
|
|
1283
|
+
let keyFile = null;
|
|
1284
|
+
const sshArgs = [
|
|
1285
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
1286
|
+
'-o', 'UserKnownHostsFile=/dev/null',
|
|
1287
|
+
'-o', 'LogLevel=ERROR',
|
|
1288
|
+
'-p', accessResp.ssh_port.toString()
|
|
1289
|
+
];
|
|
1290
|
+
|
|
1291
|
+
if (accessResp.ssh_private_key) {
|
|
1292
|
+
keyFile = path.join(os.tmpdir(), `mags_ssh_${Date.now()}`);
|
|
1293
|
+
fs.writeFileSync(keyFile, accessResp.ssh_private_key, { mode: 0o600 });
|
|
1294
|
+
sshArgs.push('-i', keyFile);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Wrap command to use chroot if overlay is mounted
|
|
1298
|
+
const wrappedCmd = `if [ -d /overlay/bin ]; then chroot /overlay /bin/sh -c '${command.replace(/'/g, "'\\''")}'; else /bin/sh -c '${command.replace(/'/g, "'\\''")}'; fi`;
|
|
1299
|
+
sshArgs.push(`root@${accessResp.ssh_host}`, wrappedCmd);
|
|
1300
|
+
|
|
1301
|
+
const ssh = spawn('ssh', sshArgs, { stdio: 'inherit' });
|
|
1302
|
+
|
|
1303
|
+
ssh.on('error', (err) => {
|
|
1304
|
+
if (keyFile) try { fs.unlinkSync(keyFile); } catch (e) {}
|
|
1305
|
+
log('red', `SSH error: ${err.message}`);
|
|
1306
|
+
process.exit(1);
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
ssh.on('close', (code) => {
|
|
1310
|
+
if (keyFile) try { fs.unlinkSync(keyFile); } catch (e) {}
|
|
1311
|
+
process.exit(code || 0);
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1244
1315
|
async function main() {
|
|
1245
1316
|
const args = process.argv.slice(2);
|
|
1246
1317
|
const command = args[0];
|
|
@@ -1277,6 +1348,10 @@ async function main() {
|
|
|
1277
1348
|
await requireAuth();
|
|
1278
1349
|
await sshToJob(args[1]);
|
|
1279
1350
|
break;
|
|
1351
|
+
case 'exec':
|
|
1352
|
+
await requireAuth();
|
|
1353
|
+
await execOnJob(args[1], args.slice(2).join(' '));
|
|
1354
|
+
break;
|
|
1280
1355
|
case 'url':
|
|
1281
1356
|
await requireAuth();
|
|
1282
1357
|
await enableUrlAccess(args[1], parseInt(args[2]) || 8080);
|