@magpiecloud/mags 1.0.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 +111 -0
- package/bin/mags.js +351 -0
- package/index.js +180 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# @magpiecloud/mags
|
|
2
|
+
|
|
3
|
+
Execute scripts instantly on Magpie's microVM infrastructure. VMs boot in <100ms from a warm pool.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @magpiecloud/mags
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### 1. Set your API token
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export MAGS_API_TOKEN="your-token-here"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 2. Run a script
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
mags run 'echo Hello World'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## CLI Commands
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Simple command
|
|
29
|
+
mags run 'echo Hello'
|
|
30
|
+
|
|
31
|
+
# With persistent workspace (S3 sync)
|
|
32
|
+
mags run -w my-project 'apk add nodejs && node --version'
|
|
33
|
+
|
|
34
|
+
# Persistent VM with public URL
|
|
35
|
+
mags run -w webapp -p --url 'python3 -m http.server 8080'
|
|
36
|
+
|
|
37
|
+
# With startup command (for auto-wake)
|
|
38
|
+
mags run -w webapp -p --url --startup-command 'npm start' 'npm install && npm start'
|
|
39
|
+
|
|
40
|
+
# Custom port
|
|
41
|
+
mags run -w webapp -p --url --port 3000 'npm start'
|
|
42
|
+
|
|
43
|
+
# Enable URL for existing job
|
|
44
|
+
mags url <job-id>
|
|
45
|
+
mags url <job-id> 8080
|
|
46
|
+
|
|
47
|
+
# Other commands
|
|
48
|
+
mags status <job-id>
|
|
49
|
+
mags logs <job-id>
|
|
50
|
+
mags list
|
|
51
|
+
mags stop <job-id>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## CLI Flags
|
|
55
|
+
|
|
56
|
+
| Flag | Description | Default |
|
|
57
|
+
|------|-------------|---------|
|
|
58
|
+
| `-w, --workspace` | Workspace ID for persistent storage | auto |
|
|
59
|
+
| `-p, --persistent` | Keep VM alive for URL/SSH access | false |
|
|
60
|
+
| `--url` | Enable public URL access (requires -p) | false |
|
|
61
|
+
| `--port` | Port to expose for URL access | 8080 |
|
|
62
|
+
| `--startup-command` | Command when VM wakes from sleep | none |
|
|
63
|
+
|
|
64
|
+
## Node.js SDK
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
const Mags = require('@magpiecloud/mags');
|
|
68
|
+
|
|
69
|
+
const mags = new Mags({
|
|
70
|
+
apiToken: process.env.MAGS_API_TOKEN
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Run and wait for completion
|
|
74
|
+
const result = await mags.runAndWait('echo Hello World');
|
|
75
|
+
console.log(result.logs);
|
|
76
|
+
|
|
77
|
+
// Run with workspace
|
|
78
|
+
const { requestId } = await mags.run('python script.py', {
|
|
79
|
+
workspaceId: 'myproject',
|
|
80
|
+
persistent: true
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Get status
|
|
84
|
+
const status = await mags.status(requestId);
|
|
85
|
+
|
|
86
|
+
// Enable URL access
|
|
87
|
+
await mags.enableUrl(requestId, 8080);
|
|
88
|
+
|
|
89
|
+
// List jobs
|
|
90
|
+
const jobs = await mags.list({ page: 1, pageSize: 10 });
|
|
91
|
+
|
|
92
|
+
// Stop a job
|
|
93
|
+
await mags.stop(requestId);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Environment Variables
|
|
97
|
+
|
|
98
|
+
| Variable | Description | Default |
|
|
99
|
+
|----------|-------------|---------|
|
|
100
|
+
| `MAGS_API_TOKEN` | Your API token (required) | - |
|
|
101
|
+
| `MAGS_API_URL` | API endpoint | https://api.magpiecloud.com |
|
|
102
|
+
|
|
103
|
+
## Performance
|
|
104
|
+
|
|
105
|
+
- **Warm start**: <100ms (VM from pool)
|
|
106
|
+
- **Cold start**: ~4 seconds (new VM boot)
|
|
107
|
+
- **Script overhead**: ~50ms
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
package/bin/mags.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const { URL } = require('url');
|
|
6
|
+
|
|
7
|
+
// Configuration
|
|
8
|
+
const API_URL = process.env.MAGS_API_URL || 'https://api.magpiecloud.com';
|
|
9
|
+
const API_TOKEN = process.env.MAGS_API_TOKEN || '';
|
|
10
|
+
|
|
11
|
+
// Colors
|
|
12
|
+
const colors = {
|
|
13
|
+
red: '\x1b[31m',
|
|
14
|
+
green: '\x1b[32m',
|
|
15
|
+
yellow: '\x1b[33m',
|
|
16
|
+
blue: '\x1b[34m',
|
|
17
|
+
cyan: '\x1b[36m',
|
|
18
|
+
gray: '\x1b[90m',
|
|
19
|
+
reset: '\x1b[0m'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function log(color, msg) {
|
|
23
|
+
console.log(`${colors[color]}${msg}${colors.reset}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function request(method, path, body = null) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const url = new URL(path, API_URL);
|
|
29
|
+
const isHttps = url.protocol === 'https:';
|
|
30
|
+
const lib = isHttps ? https : http;
|
|
31
|
+
|
|
32
|
+
const options = {
|
|
33
|
+
hostname: url.hostname,
|
|
34
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
35
|
+
path: url.pathname + url.search,
|
|
36
|
+
method,
|
|
37
|
+
headers: {
|
|
38
|
+
'Authorization': `Bearer ${API_TOKEN}`,
|
|
39
|
+
'Content-Type': 'application/json'
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const req = lib.request(options, (res) => {
|
|
44
|
+
let data = '';
|
|
45
|
+
res.on('data', chunk => data += chunk);
|
|
46
|
+
res.on('end', () => {
|
|
47
|
+
try {
|
|
48
|
+
resolve(JSON.parse(data));
|
|
49
|
+
} catch {
|
|
50
|
+
resolve(data);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
req.on('error', reject);
|
|
56
|
+
if (body) req.write(JSON.stringify(body));
|
|
57
|
+
req.end();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sleep(ms) {
|
|
62
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function usage() {
|
|
66
|
+
console.log(`
|
|
67
|
+
${colors.cyan}Mags CLI - Instant VM Execution${colors.reset}
|
|
68
|
+
|
|
69
|
+
Usage: mags <command> [options] [script]
|
|
70
|
+
|
|
71
|
+
Commands:
|
|
72
|
+
run [options] <script> Execute a script on a microVM
|
|
73
|
+
status <job-id> Get job status
|
|
74
|
+
logs <job-id> Get job logs
|
|
75
|
+
list List recent jobs
|
|
76
|
+
url <job-id> [port] Enable URL access for a job
|
|
77
|
+
stop <job-id> Stop a running job
|
|
78
|
+
|
|
79
|
+
Run Options:
|
|
80
|
+
-w, --workspace <id> Use persistent workspace (S3 sync)
|
|
81
|
+
-p, --persistent Keep VM alive after script completes
|
|
82
|
+
--url Enable public URL access (requires -p)
|
|
83
|
+
--port <port> Port to expose for URL (default: 8080)
|
|
84
|
+
--startup-command <cmd> Command to run when VM wakes from sleep
|
|
85
|
+
|
|
86
|
+
Environment:
|
|
87
|
+
MAGS_API_TOKEN Your API token (required)
|
|
88
|
+
MAGS_API_URL API endpoint (default: https://api.magpiecloud.com)
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
mags run 'echo Hello World'
|
|
92
|
+
mags run -w myproject 'python3 script.py'
|
|
93
|
+
mags run -p --url 'python3 -m http.server 8080'
|
|
94
|
+
mags run -w webapp -p --url --port 3000 'npm start'
|
|
95
|
+
mags status abc123
|
|
96
|
+
mags logs abc123
|
|
97
|
+
mags url abc123 8080
|
|
98
|
+
`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function runJob(args) {
|
|
103
|
+
let script = '';
|
|
104
|
+
let workspace = '';
|
|
105
|
+
let persistent = false;
|
|
106
|
+
let enableUrl = false;
|
|
107
|
+
let port = 8080;
|
|
108
|
+
let startupCommand = '';
|
|
109
|
+
|
|
110
|
+
// Parse flags
|
|
111
|
+
for (let i = 0; i < args.length; i++) {
|
|
112
|
+
switch (args[i]) {
|
|
113
|
+
case '-w':
|
|
114
|
+
case '--workspace':
|
|
115
|
+
workspace = args[++i];
|
|
116
|
+
break;
|
|
117
|
+
case '-p':
|
|
118
|
+
case '--persistent':
|
|
119
|
+
persistent = true;
|
|
120
|
+
break;
|
|
121
|
+
case '--url':
|
|
122
|
+
enableUrl = true;
|
|
123
|
+
break;
|
|
124
|
+
case '--port':
|
|
125
|
+
port = parseInt(args[++i]) || 8080;
|
|
126
|
+
break;
|
|
127
|
+
case '--startup-command':
|
|
128
|
+
startupCommand = args[++i];
|
|
129
|
+
break;
|
|
130
|
+
default:
|
|
131
|
+
script = args.slice(i).join(' ');
|
|
132
|
+
i = args.length;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!script) {
|
|
137
|
+
log('red', 'Error: No script provided');
|
|
138
|
+
usage();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
log('blue', 'Submitting job...');
|
|
142
|
+
|
|
143
|
+
const payload = {
|
|
144
|
+
script,
|
|
145
|
+
type: 'inline',
|
|
146
|
+
persistent
|
|
147
|
+
};
|
|
148
|
+
if (workspace) payload.workspace_id = workspace;
|
|
149
|
+
if (startupCommand) payload.startup_command = startupCommand;
|
|
150
|
+
|
|
151
|
+
const response = await request('POST', '/api/v1/mags-jobs', payload);
|
|
152
|
+
|
|
153
|
+
if (!response.request_id) {
|
|
154
|
+
log('red', 'Failed to submit job:');
|
|
155
|
+
console.log(JSON.stringify(response, null, 2));
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const requestId = response.request_id;
|
|
160
|
+
log('green', `Job submitted: ${requestId}`);
|
|
161
|
+
if (workspace) log('blue', `Workspace: ${workspace}`);
|
|
162
|
+
if (persistent) log('yellow', 'Persistent: VM will stay alive');
|
|
163
|
+
|
|
164
|
+
// Poll for completion
|
|
165
|
+
const maxAttempts = 120;
|
|
166
|
+
let attempt = 0;
|
|
167
|
+
|
|
168
|
+
while (attempt < maxAttempts) {
|
|
169
|
+
const status = await request('GET', `/api/v1/mags-jobs/${requestId}/status`);
|
|
170
|
+
|
|
171
|
+
if (status.status === 'completed') {
|
|
172
|
+
log('green', `Completed in ${status.script_duration_ms}ms`);
|
|
173
|
+
break;
|
|
174
|
+
} else if (status.status === 'running' && persistent) {
|
|
175
|
+
log('green', 'VM running');
|
|
176
|
+
|
|
177
|
+
if (enableUrl && status.subdomain) {
|
|
178
|
+
log('blue', `Enabling URL access on port ${port}...`);
|
|
179
|
+
const accessResp = await request('POST', `/api/v1/mags-jobs/${requestId}/access`, { port });
|
|
180
|
+
if (accessResp.success) {
|
|
181
|
+
log('green', `URL: https://${status.subdomain}.apps.magpiecloud.com`);
|
|
182
|
+
} else {
|
|
183
|
+
log('yellow', 'Warning: Could not enable URL access');
|
|
184
|
+
}
|
|
185
|
+
} else if (status.subdomain) {
|
|
186
|
+
log('cyan', `Subdomain: ${status.subdomain}`);
|
|
187
|
+
log('cyan', `To enable URL: mags url ${requestId} ${port}`);
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
} else if (status.status === 'error') {
|
|
191
|
+
log('red', 'Job failed');
|
|
192
|
+
console.log(JSON.stringify(status, null, 2));
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
process.stdout.write('.');
|
|
197
|
+
await sleep(1000);
|
|
198
|
+
attempt++;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log('');
|
|
202
|
+
|
|
203
|
+
// Get logs
|
|
204
|
+
log('cyan', 'Output:');
|
|
205
|
+
const logsResp = await request('GET', `/api/v1/mags-jobs/${requestId}/logs`);
|
|
206
|
+
if (logsResp.logs) {
|
|
207
|
+
logsResp.logs
|
|
208
|
+
.filter(l => l.source === 'stdout' || l.source === 'stderr')
|
|
209
|
+
.forEach(l => console.log(l.message));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function enableUrlAccess(requestId, port = 8080) {
|
|
214
|
+
if (!requestId) {
|
|
215
|
+
log('red', 'Error: Job ID required');
|
|
216
|
+
usage();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
log('blue', `Enabling URL access on port ${port}...`);
|
|
220
|
+
|
|
221
|
+
const accessResp = await request('POST', `/api/v1/mags-jobs/${requestId}/access`, { port });
|
|
222
|
+
|
|
223
|
+
if (accessResp.success) {
|
|
224
|
+
const status = await request('GET', `/api/v1/mags-jobs/${requestId}/status`);
|
|
225
|
+
if (status.subdomain) {
|
|
226
|
+
log('green', `URL enabled: https://${status.subdomain}.apps.magpiecloud.com`);
|
|
227
|
+
} else {
|
|
228
|
+
log('green', 'URL access enabled');
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
log('red', 'Failed to enable URL access');
|
|
232
|
+
console.log(JSON.stringify(accessResp, null, 2));
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function getStatus(requestId) {
|
|
238
|
+
if (!requestId) {
|
|
239
|
+
log('red', 'Error: Job ID required');
|
|
240
|
+
usage();
|
|
241
|
+
}
|
|
242
|
+
const status = await request('GET', `/api/v1/mags-jobs/${requestId}/status`);
|
|
243
|
+
console.log(JSON.stringify(status, null, 2));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function getLogs(requestId) {
|
|
247
|
+
if (!requestId) {
|
|
248
|
+
log('red', 'Error: Job ID required');
|
|
249
|
+
usage();
|
|
250
|
+
}
|
|
251
|
+
const logsResp = await request('GET', `/api/v1/mags-jobs/${requestId}/logs`);
|
|
252
|
+
if (logsResp.logs) {
|
|
253
|
+
logsResp.logs.forEach(l => {
|
|
254
|
+
const levelColor = l.level === 'error' ? 'red' : l.level === 'warn' ? 'yellow' : 'gray';
|
|
255
|
+
console.log(`${colors[levelColor]}[${l.level}]${colors.reset} ${l.message}`);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function listJobs() {
|
|
261
|
+
const resp = await request('GET', '/api/v1/mags-jobs?page=1&page_size=10');
|
|
262
|
+
if (resp.jobs && resp.jobs.length > 0) {
|
|
263
|
+
log('cyan', 'Recent Jobs:\n');
|
|
264
|
+
resp.jobs.forEach(job => {
|
|
265
|
+
const statusColor = job.status === 'completed' ? 'green'
|
|
266
|
+
: job.status === 'running' ? 'blue'
|
|
267
|
+
: job.status === 'error' ? 'red'
|
|
268
|
+
: 'yellow';
|
|
269
|
+
console.log(`${colors.gray}${job.request_id}${colors.reset}`);
|
|
270
|
+
console.log(` Name: ${job.name || '-'}`);
|
|
271
|
+
console.log(` Status: ${colors[statusColor]}${job.status}${colors.reset}`);
|
|
272
|
+
console.log(` Workspace: ${job.workspace_id || '-'}`);
|
|
273
|
+
console.log(` Duration: ${job.script_duration_ms ? job.script_duration_ms + 'ms' : '-'}`);
|
|
274
|
+
console.log(` Created: ${job.created_at || '-'}`);
|
|
275
|
+
console.log('');
|
|
276
|
+
});
|
|
277
|
+
} else {
|
|
278
|
+
log('yellow', 'No jobs found');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function stopJob(requestId) {
|
|
283
|
+
if (!requestId) {
|
|
284
|
+
log('red', 'Error: Job ID required');
|
|
285
|
+
usage();
|
|
286
|
+
}
|
|
287
|
+
log('blue', `Stopping job ${requestId}...`);
|
|
288
|
+
const resp = await request('POST', `/api/v1/mags-jobs/${requestId}/stop`);
|
|
289
|
+
if (resp.success) {
|
|
290
|
+
log('green', 'Job stopped');
|
|
291
|
+
} else {
|
|
292
|
+
log('red', 'Failed to stop job');
|
|
293
|
+
console.log(JSON.stringify(resp, null, 2));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function main() {
|
|
298
|
+
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
|
+
const command = args[0];
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
switch (command) {
|
|
310
|
+
case 'run':
|
|
311
|
+
await runJob(args.slice(1));
|
|
312
|
+
break;
|
|
313
|
+
case 'url':
|
|
314
|
+
await enableUrlAccess(args[1], parseInt(args[2]) || 8080);
|
|
315
|
+
break;
|
|
316
|
+
case 'status':
|
|
317
|
+
await getStatus(args[1]);
|
|
318
|
+
break;
|
|
319
|
+
case 'logs':
|
|
320
|
+
await getLogs(args[1]);
|
|
321
|
+
break;
|
|
322
|
+
case 'list':
|
|
323
|
+
await listJobs();
|
|
324
|
+
break;
|
|
325
|
+
case 'stop':
|
|
326
|
+
await stopJob(args[1]);
|
|
327
|
+
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
|
+
default:
|
|
339
|
+
if (!command) {
|
|
340
|
+
usage();
|
|
341
|
+
}
|
|
342
|
+
log('red', `Unknown command: ${command}`);
|
|
343
|
+
usage();
|
|
344
|
+
}
|
|
345
|
+
} catch (err) {
|
|
346
|
+
log('red', `Error: ${err.message}`);
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
main();
|
package/index.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mags SDK - Execute scripts on Magpie's instant VM infrastructure
|
|
3
|
+
* @module @magpiecloud/mags
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const { URL } = require('url');
|
|
9
|
+
|
|
10
|
+
class Mags {
|
|
11
|
+
/**
|
|
12
|
+
* Create a Mags client
|
|
13
|
+
* @param {object} options - Configuration options
|
|
14
|
+
* @param {string} options.apiUrl - API endpoint (default: https://api.magpiecloud.com)
|
|
15
|
+
* @param {string} options.apiToken - API token (required, or set MAGS_API_TOKEN env var)
|
|
16
|
+
*/
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
this.apiUrl = options.apiUrl || process.env.MAGS_API_URL || 'https://api.magpiecloud.com';
|
|
19
|
+
this.apiToken = options.apiToken || process.env.MAGS_API_TOKEN;
|
|
20
|
+
|
|
21
|
+
if (!this.apiToken) {
|
|
22
|
+
throw new Error('API token required. Set MAGS_API_TOKEN or pass apiToken option.');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_request(method, path, body = null) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const url = new URL(path, this.apiUrl);
|
|
29
|
+
const isHttps = url.protocol === 'https:';
|
|
30
|
+
const lib = isHttps ? https : http;
|
|
31
|
+
|
|
32
|
+
const options = {
|
|
33
|
+
hostname: url.hostname,
|
|
34
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
35
|
+
path: url.pathname + url.search,
|
|
36
|
+
method,
|
|
37
|
+
headers: {
|
|
38
|
+
'Authorization': `Bearer ${this.apiToken}`,
|
|
39
|
+
'Content-Type': 'application/json'
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const req = lib.request(options, (res) => {
|
|
44
|
+
let data = '';
|
|
45
|
+
res.on('data', chunk => data += chunk);
|
|
46
|
+
res.on('end', () => {
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(data);
|
|
49
|
+
if (res.statusCode >= 400) {
|
|
50
|
+
reject(new Error(parsed.error || parsed.message || `HTTP ${res.statusCode}`));
|
|
51
|
+
} else {
|
|
52
|
+
resolve(parsed);
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
resolve(data);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
req.on('error', reject);
|
|
61
|
+
if (body) req.write(JSON.stringify(body));
|
|
62
|
+
req.end();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Submit a job for execution
|
|
68
|
+
* @param {string} script - Script to execute
|
|
69
|
+
* @param {object} options - Job options
|
|
70
|
+
* @param {string} options.name - Job name
|
|
71
|
+
* @param {string} options.workspaceId - Persistent workspace ID
|
|
72
|
+
* @param {boolean} options.persistent - Keep VM alive after script
|
|
73
|
+
* @param {string} options.startupCommand - Command to run when waking from sleep
|
|
74
|
+
* @param {object} options.environment - Environment variables
|
|
75
|
+
* @returns {Promise<{requestId: string, status: string}>}
|
|
76
|
+
*/
|
|
77
|
+
async run(script, options = {}) {
|
|
78
|
+
const payload = {
|
|
79
|
+
script,
|
|
80
|
+
type: 'inline',
|
|
81
|
+
name: options.name,
|
|
82
|
+
workspace_id: options.workspaceId,
|
|
83
|
+
persistent: options.persistent || false,
|
|
84
|
+
startup_command: options.startupCommand,
|
|
85
|
+
environment: options.environment
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const response = await this._request('POST', '/api/v1/mags-jobs', payload);
|
|
89
|
+
return {
|
|
90
|
+
requestId: response.request_id,
|
|
91
|
+
status: response.status
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get job status
|
|
97
|
+
* @param {string} requestId - Job request ID
|
|
98
|
+
* @returns {Promise<object>}
|
|
99
|
+
*/
|
|
100
|
+
async status(requestId) {
|
|
101
|
+
return this._request('GET', `/api/v1/mags-jobs/${requestId}/status`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get job logs
|
|
106
|
+
* @param {string} requestId - Job request ID
|
|
107
|
+
* @returns {Promise<{logs: Array}>}
|
|
108
|
+
*/
|
|
109
|
+
async logs(requestId) {
|
|
110
|
+
return this._request('GET', `/api/v1/mags-jobs/${requestId}/logs`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* List recent jobs
|
|
115
|
+
* @param {object} options - Pagination options
|
|
116
|
+
* @param {number} options.page - Page number (default: 1)
|
|
117
|
+
* @param {number} options.pageSize - Page size (default: 20)
|
|
118
|
+
* @returns {Promise<{jobs: Array, total: number}>}
|
|
119
|
+
*/
|
|
120
|
+
async list(options = {}) {
|
|
121
|
+
const page = options.page || 1;
|
|
122
|
+
const pageSize = options.pageSize || 20;
|
|
123
|
+
return this._request('GET', `/api/v1/mags-jobs?page=${page}&page_size=${pageSize}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Enable URL access for a job
|
|
128
|
+
* @param {string} requestId - Job request ID
|
|
129
|
+
* @param {number} port - Port to expose (default: 8080)
|
|
130
|
+
* @returns {Promise<object>}
|
|
131
|
+
*/
|
|
132
|
+
async enableUrl(requestId, port = 8080) {
|
|
133
|
+
return this._request('POST', `/api/v1/mags-jobs/${requestId}/access`, { port });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Stop a running job
|
|
138
|
+
* @param {string} requestId - Job request ID
|
|
139
|
+
* @returns {Promise<object>}
|
|
140
|
+
*/
|
|
141
|
+
async stop(requestId) {
|
|
142
|
+
return this._request('POST', `/api/v1/mags-jobs/${requestId}/stop`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Run a job and wait for completion
|
|
147
|
+
* @param {string} script - Script to execute
|
|
148
|
+
* @param {object} options - Job options
|
|
149
|
+
* @param {number} options.timeout - Timeout in ms (default: 60000)
|
|
150
|
+
* @returns {Promise<{status: string, exitCode: number, logs: Array}>}
|
|
151
|
+
*/
|
|
152
|
+
async runAndWait(script, options = {}) {
|
|
153
|
+
const timeout = options.timeout || 60000;
|
|
154
|
+
const { requestId } = await this.run(script, options);
|
|
155
|
+
|
|
156
|
+
const startTime = Date.now();
|
|
157
|
+
while (Date.now() - startTime < timeout) {
|
|
158
|
+
const status = await this.status(requestId);
|
|
159
|
+
|
|
160
|
+
if (status.status === 'completed' || status.status === 'error') {
|
|
161
|
+
const logsResp = await this.logs(requestId);
|
|
162
|
+
return {
|
|
163
|
+
requestId,
|
|
164
|
+
status: status.status,
|
|
165
|
+
exitCode: status.exit_code,
|
|
166
|
+
durationMs: status.script_duration_ms,
|
|
167
|
+
logs: logsResp.logs || []
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
throw new Error(`Job ${requestId} timed out after ${timeout}ms`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = Mags;
|
|
179
|
+
module.exports.Mags = Mags;
|
|
180
|
+
module.exports.default = Mags;
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@magpiecloud/mags",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Mags CLI - Execute scripts on Magpie's instant VM infrastructure",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mags": "./bin/mags.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node bin/mags.js --help"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"magpie",
|
|
14
|
+
"mags",
|
|
15
|
+
"vm",
|
|
16
|
+
"microvm",
|
|
17
|
+
"cloud",
|
|
18
|
+
"serverless",
|
|
19
|
+
"execution",
|
|
20
|
+
"cli"
|
|
21
|
+
],
|
|
22
|
+
"author": "Magpie Cloud",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/magpiecloud/mags"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://magpiecloud.com/docs/mags",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/magpiecloud/mags/issues"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=14.0.0"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"index.js",
|
|
37
|
+
"bin/mags.js",
|
|
38
|
+
"README.md"
|
|
39
|
+
]
|
|
40
|
+
}
|