@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.
- package/README.md +31 -2
- package/bin/mags.js +243 -29
- 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.
|
|
13
|
+
### 1. Login to Magpie
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
|
|
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
|
|
9
|
-
const
|
|
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
|
-
|
|
87
|
-
|
|
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}`);
|