@magpiecloud/mags 1.7.1 → 1.7.3

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.
Files changed (2) hide show
  1. package/bin/mags.js +80 -0
  2. 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,80 @@ 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, with proper env
1298
+ const escaped = command.replace(/'/g, "'\\''");
1299
+ const wrappedCmd = `if [ -d /overlay/bin ]; then chroot /overlay /bin/sh -lc '${escaped}'; else /bin/sh -lc '${escaped}'; fi`;
1300
+ // Allocate TTY so interactive CLIs (e.g. claude) work
1301
+ if (process.stdin.isTTY) {
1302
+ sshArgs.push('-t');
1303
+ }
1304
+ sshArgs.push(`root@${accessResp.ssh_host}`, wrappedCmd);
1305
+
1306
+ const ssh = spawn('ssh', sshArgs, { stdio: 'inherit' });
1307
+
1308
+ ssh.on('error', (err) => {
1309
+ if (keyFile) try { fs.unlinkSync(keyFile); } catch (e) {}
1310
+ log('red', `SSH error: ${err.message}`);
1311
+ process.exit(1);
1312
+ });
1313
+
1314
+ ssh.on('close', (code) => {
1315
+ if (keyFile) try { fs.unlinkSync(keyFile); } catch (e) {}
1316
+ process.exit(code || 0);
1317
+ });
1318
+ }
1319
+
1244
1320
  async function main() {
1245
1321
  const args = process.argv.slice(2);
1246
1322
  const command = args[0];
@@ -1277,6 +1353,10 @@ async function main() {
1277
1353
  await requireAuth();
1278
1354
  await sshToJob(args[1]);
1279
1355
  break;
1356
+ case 'exec':
1357
+ await requireAuth();
1358
+ await execOnJob(args[1], args.slice(2).join(' '));
1359
+ break;
1280
1360
  case 'url':
1281
1361
  await requireAuth();
1282
1362
  await enableUrlAccess(args[1], parseInt(args[2]) || 8080);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpiecloud/mags",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "Mags CLI - Execute scripts on Magpie's instant VM infrastructure",
5
5
  "main": "index.js",
6
6
  "bin": {