@magpiecloud/mags 1.0.0 → 1.1.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.
Files changed (3) hide show
  1. package/README.md +31 -2
  2. package/bin/mags.js +243 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -10,18 +10,47 @@ npm install -g @magpiecloud/mags
10
10
 
11
11
  ## Quick Start
12
12
 
13
- ### 1. Set your API token
13
+ ### 1. Login to Magpie
14
14
 
15
15
  ```bash
16
- export MAGS_API_TOKEN="your-token-here"
16
+ mags login
17
17
  ```
18
18
 
19
+ This will open your browser to create an API token. Paste the token when prompted, and it will be saved for future use.
20
+
19
21
  ### 2. Run a script
20
22
 
21
23
  ```bash
22
24
  mags run 'echo Hello World'
23
25
  ```
24
26
 
27
+ ## Authentication
28
+
29
+ The CLI supports multiple authentication methods:
30
+
31
+ ### Interactive Login (Recommended)
32
+
33
+ ```bash
34
+ mags login
35
+ ```
36
+
37
+ Opens the Magpie dashboard in your browser where you can create an API token. The token is saved to `~/.mags/config.json` and used automatically for all future commands.
38
+
39
+ ### Other Auth Commands
40
+
41
+ ```bash
42
+ mags whoami # Check current authentication status
43
+ mags logout # Remove saved credentials
44
+ ```
45
+
46
+ ### Environment Variable
47
+
48
+ You can also set the token via environment variable (overrides saved config):
49
+
50
+ ```bash
51
+ export MAGS_API_TOKEN="your-token-here"
52
+ ```
53
+
25
54
  ## CLI Commands
26
55
 
27
56
  ```bash
package/bin/mags.js CHANGED
@@ -3,10 +3,44 @@
3
3
  const https = require('https');
4
4
  const http = require('http');
5
5
  const { URL } = require('url');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const readline = require('readline');
9
+ const { exec } = require('child_process');
10
+
11
+ // Config file path
12
+ const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE, '.mags');
13
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
14
+
15
+ // Load saved config
16
+ function loadConfig() {
17
+ try {
18
+ if (fs.existsSync(CONFIG_FILE)) {
19
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
20
+ }
21
+ } catch (err) {
22
+ // Ignore errors
23
+ }
24
+ return {};
25
+ }
26
+
27
+ // Save config
28
+ function saveConfig(config) {
29
+ try {
30
+ if (!fs.existsSync(CONFIG_DIR)) {
31
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
32
+ }
33
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
34
+ return true;
35
+ } catch (err) {
36
+ return false;
37
+ }
38
+ }
6
39
 
7
40
  // Configuration
8
- const API_URL = process.env.MAGS_API_URL || 'https://api.magpiecloud.com';
9
- const API_TOKEN = process.env.MAGS_API_TOKEN || '';
41
+ const config = loadConfig();
42
+ const API_URL = process.env.MAGS_API_URL || config.api_url || 'https://api.magpiecloud.com';
43
+ let API_TOKEN = process.env.MAGS_API_TOKEN || config.api_token || '';
10
44
 
11
45
  // Colors
12
46
  const colors = {
@@ -16,6 +50,7 @@ const colors = {
16
50
  blue: '\x1b[34m',
17
51
  cyan: '\x1b[36m',
18
52
  gray: '\x1b[90m',
53
+ bold: '\x1b[1m',
19
54
  reset: '\x1b[0m'
20
55
  };
21
56
 
@@ -23,6 +58,42 @@ function log(color, msg) {
23
58
  console.log(`${colors[color]}${msg}${colors.reset}`);
24
59
  }
25
60
 
61
+ // Prompt for input
62
+ function prompt(question) {
63
+ const rl = readline.createInterface({
64
+ input: process.stdin,
65
+ output: process.stdout
66
+ });
67
+
68
+ return new Promise((resolve) => {
69
+ rl.question(question, (answer) => {
70
+ rl.close();
71
+ resolve(answer.trim());
72
+ });
73
+ });
74
+ }
75
+
76
+ // Open URL in browser
77
+ function openBrowser(url) {
78
+ const platform = process.platform;
79
+ let cmd;
80
+
81
+ if (platform === 'darwin') {
82
+ cmd = `open "${url}"`;
83
+ } else if (platform === 'win32') {
84
+ cmd = `start "${url}"`;
85
+ } else {
86
+ cmd = `xdg-open "${url}"`;
87
+ }
88
+
89
+ exec(cmd, (err) => {
90
+ if (err) {
91
+ log('yellow', `Could not open browser automatically.`);
92
+ log('cyan', `Please open this URL manually: ${url}`);
93
+ }
94
+ });
95
+ }
96
+
26
97
  function request(method, path, body = null) {
27
98
  return new Promise((resolve, reject) => {
28
99
  const url = new URL(path, API_URL);
@@ -64,11 +135,14 @@ function sleep(ms) {
64
135
 
65
136
  function usage() {
66
137
  console.log(`
67
- ${colors.cyan}Mags CLI - Instant VM Execution${colors.reset}
138
+ ${colors.cyan}${colors.bold}Mags CLI - Instant VM Execution${colors.reset}
68
139
 
69
140
  Usage: mags <command> [options] [script]
70
141
 
71
- Commands:
142
+ ${colors.bold}Commands:${colors.reset}
143
+ login Authenticate with Magpie
144
+ logout Remove saved credentials
145
+ whoami Show current authenticated user
72
146
  run [options] <script> Execute a script on a microVM
73
147
  status <job-id> Get job status
74
148
  logs <job-id> Get job logs
@@ -76,18 +150,15 @@ Commands:
76
150
  url <job-id> [port] Enable URL access for a job
77
151
  stop <job-id> Stop a running job
78
152
 
79
- Run Options:
153
+ ${colors.bold}Run Options:${colors.reset}
80
154
  -w, --workspace <id> Use persistent workspace (S3 sync)
81
155
  -p, --persistent Keep VM alive after script completes
82
156
  --url Enable public URL access (requires -p)
83
157
  --port <port> Port to expose for URL (default: 8080)
84
158
  --startup-command <cmd> Command to run when VM wakes from sleep
85
159
 
86
- Environment:
87
- MAGS_API_TOKEN Your API token (required)
88
- MAGS_API_URL API endpoint (default: https://api.magpiecloud.com)
89
-
90
- Examples:
160
+ ${colors.bold}Examples:${colors.reset}
161
+ mags login
91
162
  mags run 'echo Hello World'
92
163
  mags run -w myproject 'python3 script.py'
93
164
  mags run -p --url 'python3 -m http.server 8080'
@@ -99,6 +170,133 @@ Examples:
99
170
  process.exit(1);
100
171
  }
101
172
 
173
+ async function login() {
174
+ console.log(`
175
+ ${colors.cyan}${colors.bold}Mags Login${colors.reset}
176
+
177
+ To authenticate, you need an API token from Magpie.
178
+ `);
179
+
180
+ log('blue', 'Opening Magpie dashboard to create an API token...');
181
+ console.log('');
182
+
183
+ const tokenUrl = 'https://magpiecloud.com/api-keys';
184
+ openBrowser(tokenUrl);
185
+
186
+ await sleep(1000);
187
+
188
+ console.log(`${colors.gray}If the browser didn't open, visit:${colors.reset}`);
189
+ console.log(`${colors.cyan}${tokenUrl}${colors.reset}`);
190
+ console.log('');
191
+ console.log(`${colors.gray}1. Click "Create Token"${colors.reset}`);
192
+ console.log(`${colors.gray}2. Give it a name (e.g., "mags-cli")${colors.reset}`);
193
+ console.log(`${colors.gray}3. Copy the token and paste it below${colors.reset}`);
194
+ console.log('');
195
+
196
+ const token = await prompt(`${colors.bold}Paste your API token: ${colors.reset}`);
197
+
198
+ if (!token) {
199
+ log('red', 'No token provided. Login cancelled.');
200
+ process.exit(1);
201
+ }
202
+
203
+ // Validate token by making a test request
204
+ API_TOKEN = token;
205
+ log('blue', 'Verifying token...');
206
+
207
+ try {
208
+ const resp = await request('GET', '/api/v1/mags-jobs?page=1&page_size=1');
209
+
210
+ if (resp.error === 'unauthorized' || resp.error === 'Unauthorized') {
211
+ log('red', 'Invalid token. Please check and try again.');
212
+ process.exit(1);
213
+ }
214
+
215
+ // Save token
216
+ const newConfig = { ...config, api_token: token };
217
+ if (saveConfig(newConfig)) {
218
+ console.log('');
219
+ log('green', 'Login successful!');
220
+ log('gray', `Token saved to ${CONFIG_FILE}`);
221
+ console.log('');
222
+ log('cyan', 'You can now run mags commands. Try:');
223
+ console.log(` ${colors.bold}mags run 'echo Hello World'${colors.reset}`);
224
+ } else {
225
+ log('yellow', 'Login successful, but could not save token to config file.');
226
+ log('yellow', 'You may need to login again next time.');
227
+ }
228
+ } catch (err) {
229
+ log('red', `Error validating token: ${err.message}`);
230
+ process.exit(1);
231
+ }
232
+ }
233
+
234
+ async function logout() {
235
+ if (!config.api_token) {
236
+ log('yellow', 'You are not logged in.');
237
+ return;
238
+ }
239
+
240
+ const newConfig = { ...config };
241
+ delete newConfig.api_token;
242
+
243
+ if (saveConfig(newConfig)) {
244
+ log('green', 'Logged out successfully.');
245
+ log('gray', 'Token removed from config.');
246
+ } else {
247
+ log('red', 'Could not remove token from config file.');
248
+ }
249
+ }
250
+
251
+ async function whoami() {
252
+ if (!API_TOKEN) {
253
+ log('yellow', 'Not logged in.');
254
+ log('gray', 'Run `mags login` to authenticate.');
255
+ return;
256
+ }
257
+
258
+ log('blue', 'Checking authentication...');
259
+
260
+ try {
261
+ const resp = await request('GET', '/api/v1/mags-jobs?page=1&page_size=1');
262
+
263
+ if (resp.error === 'unauthorized' || resp.error === 'Unauthorized') {
264
+ log('red', 'Token is invalid or expired.');
265
+ log('gray', 'Run `mags login` to re-authenticate.');
266
+ return;
267
+ }
268
+
269
+ log('green', 'Authenticated');
270
+ if (process.env.MAGS_API_TOKEN) {
271
+ log('gray', 'Using token from MAGS_API_TOKEN environment variable');
272
+ } else {
273
+ log('gray', `Using token from ${CONFIG_FILE}`);
274
+ }
275
+ } catch (err) {
276
+ log('red', `Error: ${err.message}`);
277
+ }
278
+ }
279
+
280
+ async function requireAuth() {
281
+ if (API_TOKEN) return true;
282
+
283
+ console.log(`
284
+ ${colors.yellow}You are not logged in.${colors.reset}
285
+
286
+ To use Mags, you need to authenticate first.
287
+ `);
288
+
289
+ const answer = await prompt(`${colors.bold}Would you like to login now? (Y/n): ${colors.reset}`);
290
+
291
+ if (answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') {
292
+ log('gray', 'You can login later with: mags login');
293
+ process.exit(0);
294
+ }
295
+
296
+ await login();
297
+ return true;
298
+ }
299
+
102
300
  async function runJob(args) {
103
301
  let script = '';
104
302
  let workspace = '';
@@ -296,51 +494,67 @@ async function stopJob(requestId) {
296
494
 
297
495
  async function main() {
298
496
  const args = process.argv.slice(2);
299
-
300
- if (!API_TOKEN) {
301
- log('red', 'Error: MAGS_API_TOKEN not set');
302
- console.log('Set it via: export MAGS_API_TOKEN=your-token');
303
- process.exit(1);
304
- }
305
-
306
497
  const command = args[0];
307
498
 
499
+ // Commands that don't require auth
500
+ const noAuthCommands = ['login', '--help', '-h', '--version', '-v'];
501
+
308
502
  try {
309
503
  switch (command) {
504
+ case 'login':
505
+ await login();
506
+ break;
507
+ case 'logout':
508
+ await logout();
509
+ break;
510
+ case 'whoami':
511
+ await whoami();
512
+ break;
513
+ case '--help':
514
+ case '-h':
515
+ usage();
516
+ break;
517
+ case '--version':
518
+ case '-v':
519
+ console.log('mags v1.0.0');
520
+ process.exit(0);
521
+ break;
310
522
  case 'run':
523
+ await requireAuth();
311
524
  await runJob(args.slice(1));
312
525
  break;
313
526
  case 'url':
527
+ await requireAuth();
314
528
  await enableUrlAccess(args[1], parseInt(args[2]) || 8080);
315
529
  break;
316
530
  case 'status':
531
+ await requireAuth();
317
532
  await getStatus(args[1]);
318
533
  break;
319
534
  case 'logs':
535
+ await requireAuth();
320
536
  await getLogs(args[1]);
321
537
  break;
322
538
  case 'list':
539
+ await requireAuth();
323
540
  await listJobs();
324
541
  break;
325
542
  case 'stop':
543
+ await requireAuth();
326
544
  await stopJob(args[1]);
327
545
  break;
328
- case '--help':
329
- case '-h':
330
- case '--version':
331
- case '-v':
332
- if (command === '--version' || command === '-v') {
333
- console.log('mags v1.0.0');
334
- process.exit(0);
335
- }
336
- usage();
337
- break;
338
546
  default:
339
547
  if (!command) {
548
+ // No command - check if logged in
549
+ if (!API_TOKEN) {
550
+ await requireAuth();
551
+ } else {
552
+ usage();
553
+ }
554
+ } else {
555
+ log('red', `Unknown command: ${command}`);
340
556
  usage();
341
557
  }
342
- log('red', `Unknown command: ${command}`);
343
- usage();
344
558
  }
345
559
  } catch (err) {
346
560
  log('red', `Error: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpiecloud/mags",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Mags CLI - Execute scripts on Magpie's instant VM infrastructure",
5
5
  "main": "index.js",
6
6
  "bin": {