instavm 0.13.0 → 0.15.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 +82 -11
- package/dist/cli.js +3446 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.mts +27 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.js +30 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +30 -4
- package/dist/index.mjs.map +1 -1
- package/dist/integrations/openai/index.js +0 -1
- package/dist/integrations/openai/index.js.map +1 -1
- package/dist/integrations/openai/index.mjs +0 -2
- package/dist/integrations/openai/index.mjs.map +1 -1
- package/package.json +6 -2
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3446 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/cli.ts
|
|
32
|
+
var cli_exports = {};
|
|
33
|
+
__export(cli_exports, {
|
|
34
|
+
createProgram: () => createProgram,
|
|
35
|
+
runCli: () => runCli
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(cli_exports);
|
|
38
|
+
var import_fs3 = __toESM(require("fs"));
|
|
39
|
+
var import_path12 = __toESM(require("path"));
|
|
40
|
+
var import_child_process = require("child_process");
|
|
41
|
+
var import_commander = require("commander");
|
|
42
|
+
|
|
43
|
+
// src/client/HTTPClient.ts
|
|
44
|
+
var import_axios = __toESM(require("axios"));
|
|
45
|
+
|
|
46
|
+
// src/errors/BaseError.ts
|
|
47
|
+
var InstaVMError = class extends Error {
|
|
48
|
+
constructor(message, options) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = this.constructor.name;
|
|
51
|
+
this.code = options?.code;
|
|
52
|
+
this.statusCode = options?.statusCode;
|
|
53
|
+
this.response = options?.response;
|
|
54
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
55
|
+
if (Error.captureStackTrace) {
|
|
56
|
+
Error.captureStackTrace(this, this.constructor);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var AuthenticationError = class extends InstaVMError {
|
|
61
|
+
constructor(message = "Authentication failed", options) {
|
|
62
|
+
super(message, { ...options, statusCode: 401 });
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var RateLimitError = class extends InstaVMError {
|
|
66
|
+
constructor(message = "Rate limit exceeded", retryAfter, options) {
|
|
67
|
+
super(message, { ...options, statusCode: 429 });
|
|
68
|
+
this.retryAfter = retryAfter;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var QuotaExceededError = class extends InstaVMError {
|
|
72
|
+
constructor(message = "Usage quota exceeded", options) {
|
|
73
|
+
super(message, { ...options, statusCode: 402 });
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var NetworkError = class extends InstaVMError {
|
|
77
|
+
constructor(message = "Network error", options) {
|
|
78
|
+
super(message, options);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
var ExecutionError = class extends InstaVMError {
|
|
82
|
+
constructor(message, executionOutput, executionTime, options) {
|
|
83
|
+
super(message, options);
|
|
84
|
+
this.executionOutput = executionOutput;
|
|
85
|
+
this.executionTime = executionTime;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
var SessionError = class extends InstaVMError {
|
|
89
|
+
constructor(message = "Session error", options) {
|
|
90
|
+
super(message, options);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
var BrowserError = class extends InstaVMError {
|
|
94
|
+
constructor(message = "Browser error", options) {
|
|
95
|
+
super(message, options);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
var BrowserSessionError = class extends BrowserError {
|
|
99
|
+
constructor(message = "Browser session error", options) {
|
|
100
|
+
super(message, options);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
var BrowserInteractionError = class extends BrowserError {
|
|
104
|
+
constructor(message = "Browser interaction error", options) {
|
|
105
|
+
super(message, options);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
var BrowserTimeoutError = class extends BrowserError {
|
|
109
|
+
constructor(message = "Browser operation timed out", options) {
|
|
110
|
+
super(message, options);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var BrowserNavigationError = class extends BrowserError {
|
|
114
|
+
constructor(message = "Browser navigation error", options) {
|
|
115
|
+
super(message, options);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var ElementNotFoundError = class extends BrowserError {
|
|
119
|
+
constructor(message = "Element not found", selector, options) {
|
|
120
|
+
super(message, options);
|
|
121
|
+
this.selector = selector;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
var UnsupportedOperationError = class extends InstaVMError {
|
|
125
|
+
constructor(message = "Operation not supported", options) {
|
|
126
|
+
super(message, options);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/utils/retry.ts
|
|
131
|
+
function defaultRetryCondition(error) {
|
|
132
|
+
if (error instanceof NetworkError) return true;
|
|
133
|
+
if (error instanceof RateLimitError) return true;
|
|
134
|
+
if (error.response?.status >= 500) return true;
|
|
135
|
+
if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") return true;
|
|
136
|
+
if (error.code === "ECONNRESET" || error.code === "ENOTFOUND") return true;
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
function calculateRetryDelay(attempt, baseDelay, maxDelay = 3e4) {
|
|
140
|
+
const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
|
|
141
|
+
const jitteredDelay = exponentialDelay * (0.5 + Math.random() * 0.5);
|
|
142
|
+
return Math.min(jitteredDelay, maxDelay);
|
|
143
|
+
}
|
|
144
|
+
async function withRetry(fn, options) {
|
|
145
|
+
const {
|
|
146
|
+
retries,
|
|
147
|
+
retryDelay,
|
|
148
|
+
maxRetryDelay = 3e4,
|
|
149
|
+
retryCondition = defaultRetryCondition
|
|
150
|
+
} = options;
|
|
151
|
+
let lastError;
|
|
152
|
+
for (let attempt = 1; attempt <= retries + 1; attempt++) {
|
|
153
|
+
try {
|
|
154
|
+
return await fn();
|
|
155
|
+
} catch (error) {
|
|
156
|
+
lastError = error;
|
|
157
|
+
if (attempt === retries + 1) {
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
if (!retryCondition(error)) {
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
const delay = calculateRetryDelay(attempt, retryDelay, maxRetryDelay);
|
|
164
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
throw lastError;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/client/HTTPClient.ts
|
|
171
|
+
var HTTPClient = class {
|
|
172
|
+
get apiKey() {
|
|
173
|
+
return this.config.apiKey;
|
|
174
|
+
}
|
|
175
|
+
constructor(config) {
|
|
176
|
+
this.config = config;
|
|
177
|
+
this.client = import_axios.default.create({
|
|
178
|
+
baseURL: config.baseURL,
|
|
179
|
+
timeout: config.timeout,
|
|
180
|
+
headers: {
|
|
181
|
+
"Content-Type": "application/json",
|
|
182
|
+
"User-Agent": "instavm-js-sdk/0.15.0"
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
this.setupInterceptors();
|
|
186
|
+
}
|
|
187
|
+
setupInterceptors() {
|
|
188
|
+
this.client.interceptors.request.use(
|
|
189
|
+
(config) => {
|
|
190
|
+
if (config.url?.includes("/browser/")) {
|
|
191
|
+
config.headers["X-API-Key"] = this.config.apiKey;
|
|
192
|
+
}
|
|
193
|
+
return config;
|
|
194
|
+
},
|
|
195
|
+
(error) => Promise.reject(error)
|
|
196
|
+
);
|
|
197
|
+
this.client.interceptors.response.use(
|
|
198
|
+
(response) => response,
|
|
199
|
+
(error) => {
|
|
200
|
+
const axiosError = error;
|
|
201
|
+
const status = axiosError.response?.status;
|
|
202
|
+
const data = axiosError.response?.data;
|
|
203
|
+
const message = data?.message || data?.error || data?.detail || axiosError.message;
|
|
204
|
+
switch (status) {
|
|
205
|
+
case 401:
|
|
206
|
+
throw new AuthenticationError(message, {
|
|
207
|
+
statusCode: status,
|
|
208
|
+
response: data
|
|
209
|
+
});
|
|
210
|
+
case 402:
|
|
211
|
+
throw new QuotaExceededError(message, {
|
|
212
|
+
statusCode: status,
|
|
213
|
+
response: data
|
|
214
|
+
});
|
|
215
|
+
case 429: {
|
|
216
|
+
const retryAfter = parseInt(
|
|
217
|
+
axiosError.response?.headers["retry-after"] || "60"
|
|
218
|
+
);
|
|
219
|
+
const rateLimitMessage = data?.detail || message;
|
|
220
|
+
throw new RateLimitError(rateLimitMessage, retryAfter, {
|
|
221
|
+
statusCode: status,
|
|
222
|
+
response: data
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
case 500:
|
|
226
|
+
case 502:
|
|
227
|
+
case 503:
|
|
228
|
+
case 504:
|
|
229
|
+
throw new NetworkError(message, {
|
|
230
|
+
statusCode: status,
|
|
231
|
+
response: data
|
|
232
|
+
});
|
|
233
|
+
default:
|
|
234
|
+
if (axiosError.code === "ECONNABORTED") {
|
|
235
|
+
throw new NetworkError("Request timeout", {
|
|
236
|
+
code: axiosError.code
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
throw new InstaVMError(message, {
|
|
240
|
+
statusCode: status,
|
|
241
|
+
response: data,
|
|
242
|
+
code: axiosError.code
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Make an HTTP request with retry logic
|
|
250
|
+
*/
|
|
251
|
+
async request(requestConfig) {
|
|
252
|
+
const axiosConfig = {
|
|
253
|
+
method: requestConfig.method,
|
|
254
|
+
url: requestConfig.url,
|
|
255
|
+
headers: requestConfig.headers,
|
|
256
|
+
data: requestConfig.data,
|
|
257
|
+
params: requestConfig.params,
|
|
258
|
+
timeout: requestConfig.timeout || this.config.timeout
|
|
259
|
+
};
|
|
260
|
+
const makeRequest = async () => {
|
|
261
|
+
const response = await this.client.request(axiosConfig);
|
|
262
|
+
return response.data;
|
|
263
|
+
};
|
|
264
|
+
return withRetry(makeRequest, {
|
|
265
|
+
retries: this.config.maxRetries,
|
|
266
|
+
retryDelay: this.config.retryDelay
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* POST request with JSON body
|
|
271
|
+
*/
|
|
272
|
+
async post(url, data, headers, timeout) {
|
|
273
|
+
return this.request({
|
|
274
|
+
method: "POST",
|
|
275
|
+
url,
|
|
276
|
+
data,
|
|
277
|
+
headers,
|
|
278
|
+
timeout
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* POST request for code execution (uses X-API-Key header like Python client)
|
|
283
|
+
*/
|
|
284
|
+
async postExecution(url, data, headers, timeout) {
|
|
285
|
+
const requestHeaders = {
|
|
286
|
+
"X-API-Key": this.config.apiKey,
|
|
287
|
+
...headers
|
|
288
|
+
};
|
|
289
|
+
return this.request({
|
|
290
|
+
method: "POST",
|
|
291
|
+
url,
|
|
292
|
+
data,
|
|
293
|
+
headers: requestHeaders,
|
|
294
|
+
timeout
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* POST request with form data (for file uploads)
|
|
299
|
+
*/
|
|
300
|
+
async postFormData(url, formData, headers) {
|
|
301
|
+
const requestHeaders = {
|
|
302
|
+
"X-API-Key": this.config.apiKey,
|
|
303
|
+
...formData.getHeaders(),
|
|
304
|
+
...headers
|
|
305
|
+
};
|
|
306
|
+
return this.request({
|
|
307
|
+
method: "POST",
|
|
308
|
+
url,
|
|
309
|
+
data: formData,
|
|
310
|
+
headers: requestHeaders
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* POST request with URL-encoded form data (like Python requests data= parameter)
|
|
315
|
+
*/
|
|
316
|
+
async postFormUrlEncoded(url, data, headers) {
|
|
317
|
+
const params = new URLSearchParams();
|
|
318
|
+
for (const [key, value] of Object.entries(data)) {
|
|
319
|
+
params.append(key, String(value));
|
|
320
|
+
}
|
|
321
|
+
const requestHeaders = {
|
|
322
|
+
"X-API-Key": this.config.apiKey,
|
|
323
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
324
|
+
...headers
|
|
325
|
+
};
|
|
326
|
+
return this.request({
|
|
327
|
+
method: "POST",
|
|
328
|
+
url,
|
|
329
|
+
data: params.toString(),
|
|
330
|
+
headers: requestHeaders
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* GET request
|
|
335
|
+
*/
|
|
336
|
+
async get(url, params, headers) {
|
|
337
|
+
return this.request({
|
|
338
|
+
method: "GET",
|
|
339
|
+
url,
|
|
340
|
+
params,
|
|
341
|
+
headers
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* DELETE request
|
|
346
|
+
*/
|
|
347
|
+
async delete(url, headers) {
|
|
348
|
+
return this.request({
|
|
349
|
+
method: "DELETE",
|
|
350
|
+
url,
|
|
351
|
+
headers
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* PUT request
|
|
356
|
+
*/
|
|
357
|
+
async put(url, data, headers) {
|
|
358
|
+
return this.request({
|
|
359
|
+
method: "PUT",
|
|
360
|
+
url,
|
|
361
|
+
data,
|
|
362
|
+
headers
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* PATCH request
|
|
367
|
+
*/
|
|
368
|
+
async patch(url, data, headers) {
|
|
369
|
+
return this.request({
|
|
370
|
+
method: "PATCH",
|
|
371
|
+
url,
|
|
372
|
+
data,
|
|
373
|
+
headers
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* OPTIONS request
|
|
378
|
+
*/
|
|
379
|
+
async options(url, headers, params) {
|
|
380
|
+
return this.request({
|
|
381
|
+
method: "OPTIONS",
|
|
382
|
+
url,
|
|
383
|
+
headers,
|
|
384
|
+
params
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* POST request that returns raw binary data (for file downloads)
|
|
389
|
+
*/
|
|
390
|
+
async postRaw(url, data, headers) {
|
|
391
|
+
const requestHeaders = {
|
|
392
|
+
"X-API-Key": this.config.apiKey,
|
|
393
|
+
...headers
|
|
394
|
+
};
|
|
395
|
+
const axiosConfig = {
|
|
396
|
+
method: "POST",
|
|
397
|
+
url,
|
|
398
|
+
data,
|
|
399
|
+
headers: requestHeaders,
|
|
400
|
+
responseType: "arraybuffer",
|
|
401
|
+
timeout: this.config.timeout
|
|
402
|
+
};
|
|
403
|
+
const makeRequest = async () => {
|
|
404
|
+
const response = await this.client.request(axiosConfig);
|
|
405
|
+
return response.data;
|
|
406
|
+
};
|
|
407
|
+
return withRetry(makeRequest, {
|
|
408
|
+
retries: this.config.maxRetries,
|
|
409
|
+
retryDelay: this.config.retryDelay
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// src/client/BrowserSession.ts
|
|
415
|
+
var import_eventemitter3 = require("eventemitter3");
|
|
416
|
+
function getErrorMessage(error) {
|
|
417
|
+
return error instanceof Error ? error.message : String(error);
|
|
418
|
+
}
|
|
419
|
+
var BrowserSession = class extends import_eventemitter3.EventEmitter {
|
|
420
|
+
constructor(sessionId, httpClient) {
|
|
421
|
+
super();
|
|
422
|
+
this._isActive = true;
|
|
423
|
+
this.sessionId = sessionId;
|
|
424
|
+
this.httpClient = httpClient;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Navigate to a URL
|
|
428
|
+
*/
|
|
429
|
+
async navigate(url, options = {}) {
|
|
430
|
+
this.ensureActive();
|
|
431
|
+
const requestData = {
|
|
432
|
+
url,
|
|
433
|
+
session_id: this.sessionId,
|
|
434
|
+
wait_timeout: options.waitTimeout || 3e4,
|
|
435
|
+
wait_until: options.waitUntil || "load"
|
|
436
|
+
};
|
|
437
|
+
try {
|
|
438
|
+
const response = await this.httpClient.post(
|
|
439
|
+
"/v1/browser/interactions/navigate",
|
|
440
|
+
requestData
|
|
441
|
+
);
|
|
442
|
+
const result = {
|
|
443
|
+
success: response.success !== false,
|
|
444
|
+
url: response.url || url,
|
|
445
|
+
title: response.title,
|
|
446
|
+
status: response.status
|
|
447
|
+
};
|
|
448
|
+
this.emit("navigation", result);
|
|
449
|
+
return result;
|
|
450
|
+
} catch (error) {
|
|
451
|
+
const navigationError = new BrowserNavigationError(
|
|
452
|
+
`Navigation failed: ${getErrorMessage(error)}`,
|
|
453
|
+
{ cause: error }
|
|
454
|
+
);
|
|
455
|
+
this.emit("error", navigationError);
|
|
456
|
+
throw navigationError;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Click an element
|
|
461
|
+
*/
|
|
462
|
+
async click(selector, options = {}) {
|
|
463
|
+
this.ensureActive();
|
|
464
|
+
const requestData = {
|
|
465
|
+
selector,
|
|
466
|
+
session_id: this.sessionId,
|
|
467
|
+
timeout: options.timeout || 1e4,
|
|
468
|
+
button: options.button || "left",
|
|
469
|
+
click_count: options.clickCount || 1,
|
|
470
|
+
force: options.force || false
|
|
471
|
+
};
|
|
472
|
+
try {
|
|
473
|
+
await this.httpClient.post(
|
|
474
|
+
"/v1/browser/interactions/click",
|
|
475
|
+
requestData
|
|
476
|
+
);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
const errorMessage = getErrorMessage(error);
|
|
479
|
+
if (errorMessage.includes("not found") || errorMessage.includes("selector")) {
|
|
480
|
+
throw new ElementNotFoundError(
|
|
481
|
+
`Element not found: ${selector}`,
|
|
482
|
+
selector,
|
|
483
|
+
{ cause: error }
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
if (errorMessage.includes("timeout")) {
|
|
487
|
+
throw new BrowserTimeoutError(
|
|
488
|
+
`Click timeout: ${selector}`,
|
|
489
|
+
{ cause: error }
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
throw new BrowserInteractionError(
|
|
493
|
+
`Click failed: ${errorMessage}`,
|
|
494
|
+
{ cause: error }
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Type text into an element
|
|
500
|
+
*/
|
|
501
|
+
async type(selector, text, options = {}) {
|
|
502
|
+
this.ensureActive();
|
|
503
|
+
const requestData = {
|
|
504
|
+
selector,
|
|
505
|
+
text,
|
|
506
|
+
session_id: this.sessionId,
|
|
507
|
+
timeout: options.timeout || 1e4,
|
|
508
|
+
delay: options.delay || 0,
|
|
509
|
+
clear: options.clear !== false
|
|
510
|
+
};
|
|
511
|
+
try {
|
|
512
|
+
await this.httpClient.post(
|
|
513
|
+
"/v1/browser/interactions/type",
|
|
514
|
+
requestData
|
|
515
|
+
);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
const errorMessage = getErrorMessage(error);
|
|
518
|
+
if (errorMessage.includes("not found") || errorMessage.includes("selector")) {
|
|
519
|
+
throw new ElementNotFoundError(
|
|
520
|
+
`Element not found: ${selector}`,
|
|
521
|
+
selector,
|
|
522
|
+
{ cause: error }
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
throw new BrowserInteractionError(
|
|
526
|
+
`Type failed: ${errorMessage}`,
|
|
527
|
+
{ cause: error }
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Fill a form field
|
|
533
|
+
*/
|
|
534
|
+
async fill(selector, value, options = {}) {
|
|
535
|
+
this.ensureActive();
|
|
536
|
+
const requestData = {
|
|
537
|
+
selector,
|
|
538
|
+
value,
|
|
539
|
+
session_id: this.sessionId,
|
|
540
|
+
timeout: options.timeout || 1e4,
|
|
541
|
+
force: options.force || false
|
|
542
|
+
};
|
|
543
|
+
try {
|
|
544
|
+
await this.httpClient.post(
|
|
545
|
+
"/v1/browser/interactions/fill",
|
|
546
|
+
requestData
|
|
547
|
+
);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
const errorMessage = getErrorMessage(error);
|
|
550
|
+
if (errorMessage.includes("not found") || errorMessage.includes("selector")) {
|
|
551
|
+
throw new ElementNotFoundError(
|
|
552
|
+
`Element not found: ${selector}`,
|
|
553
|
+
selector,
|
|
554
|
+
{ cause: error }
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
throw new BrowserInteractionError(
|
|
558
|
+
`Fill failed: ${errorMessage}`,
|
|
559
|
+
{ cause: error }
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Scroll the page
|
|
565
|
+
*/
|
|
566
|
+
async scroll(options = {}) {
|
|
567
|
+
this.ensureActive();
|
|
568
|
+
const requestData = {
|
|
569
|
+
session_id: this.sessionId,
|
|
570
|
+
x: options.x || 0,
|
|
571
|
+
y: options.y || 500,
|
|
572
|
+
behavior: options.behavior || "auto"
|
|
573
|
+
};
|
|
574
|
+
try {
|
|
575
|
+
await this.httpClient.post(
|
|
576
|
+
"/v1/browser/interactions/scroll",
|
|
577
|
+
requestData
|
|
578
|
+
);
|
|
579
|
+
} catch (error) {
|
|
580
|
+
throw new BrowserInteractionError(
|
|
581
|
+
`Scroll failed: ${getErrorMessage(error)}`,
|
|
582
|
+
{ cause: error }
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Take a screenshot
|
|
588
|
+
*/
|
|
589
|
+
async screenshot(options = {}) {
|
|
590
|
+
this.ensureActive();
|
|
591
|
+
const requestData = {
|
|
592
|
+
session_id: this.sessionId,
|
|
593
|
+
full_page: options.fullPage !== false,
|
|
594
|
+
format: options.format || "png",
|
|
595
|
+
quality: options.quality || 90,
|
|
596
|
+
clip: options.clip
|
|
597
|
+
};
|
|
598
|
+
try {
|
|
599
|
+
const response = await this.httpClient.post(
|
|
600
|
+
"/v1/browser/interactions/screenshot",
|
|
601
|
+
requestData
|
|
602
|
+
);
|
|
603
|
+
if (!response.screenshot) {
|
|
604
|
+
throw new BrowserError("Screenshot data not returned");
|
|
605
|
+
}
|
|
606
|
+
return response.screenshot;
|
|
607
|
+
} catch (error) {
|
|
608
|
+
throw new BrowserInteractionError(
|
|
609
|
+
`Screenshot failed: ${getErrorMessage(error)}`,
|
|
610
|
+
{ cause: error }
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Extract elements from the page
|
|
616
|
+
*/
|
|
617
|
+
async extractElements(selector, attributes = ["text"], options = {}) {
|
|
618
|
+
this.ensureActive();
|
|
619
|
+
const requestData = {
|
|
620
|
+
selector,
|
|
621
|
+
attributes,
|
|
622
|
+
session_id: this.sessionId,
|
|
623
|
+
max_results: options.maxResults || 100
|
|
624
|
+
};
|
|
625
|
+
try {
|
|
626
|
+
const response = await this.httpClient.post(
|
|
627
|
+
"/v1/browser/interactions/extract",
|
|
628
|
+
requestData
|
|
629
|
+
);
|
|
630
|
+
return response.elements || [];
|
|
631
|
+
} catch (error) {
|
|
632
|
+
throw new BrowserInteractionError(
|
|
633
|
+
`Element extraction failed: ${getErrorMessage(error)}`,
|
|
634
|
+
{ cause: error }
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Extract LLM-friendly content from the current page
|
|
640
|
+
*
|
|
641
|
+
* Returns clean article content, interactive elements, and content anchors
|
|
642
|
+
* for intelligent browser automation with LLMs.
|
|
643
|
+
*
|
|
644
|
+
* @param options - Content extraction options
|
|
645
|
+
* @returns Extracted content with readable text, interactive elements, and content anchors
|
|
646
|
+
*
|
|
647
|
+
* @example
|
|
648
|
+
* ```typescript
|
|
649
|
+
* const content = await session.extractContent();
|
|
650
|
+
*
|
|
651
|
+
* // LLM reads clean content
|
|
652
|
+
* const article = content.readableContent.content;
|
|
653
|
+
*
|
|
654
|
+
* // LLM finds "Sign Up" in content and uses anchors to get selector
|
|
655
|
+
* const signUpAnchor = content.contentAnchors?.find(
|
|
656
|
+
* anchor => anchor.text.toLowerCase().includes('sign up')
|
|
657
|
+
* );
|
|
658
|
+
* if (signUpAnchor) {
|
|
659
|
+
* await session.click(signUpAnchor.selector);
|
|
660
|
+
* }
|
|
661
|
+
* ```
|
|
662
|
+
*/
|
|
663
|
+
async extractContent(options = {}) {
|
|
664
|
+
this.ensureActive();
|
|
665
|
+
const requestData = {
|
|
666
|
+
session_id: this.sessionId,
|
|
667
|
+
include_interactive: options.includeInteractive !== false,
|
|
668
|
+
include_anchors: options.includeAnchors !== false,
|
|
669
|
+
max_anchors: options.maxAnchors ?? 50
|
|
670
|
+
};
|
|
671
|
+
try {
|
|
672
|
+
const response = await this.httpClient.post(
|
|
673
|
+
"/v1/browser/interactions/content",
|
|
674
|
+
requestData
|
|
675
|
+
);
|
|
676
|
+
return {
|
|
677
|
+
readableContent: {
|
|
678
|
+
...response.readable_content || {},
|
|
679
|
+
content: response.readable_content?.content || ""
|
|
680
|
+
},
|
|
681
|
+
interactiveElements: response.interactive_elements,
|
|
682
|
+
contentAnchors: response.content_anchors
|
|
683
|
+
};
|
|
684
|
+
} catch (error) {
|
|
685
|
+
throw new BrowserInteractionError(
|
|
686
|
+
`Content extraction failed: ${getErrorMessage(error)}`,
|
|
687
|
+
{ cause: error }
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Wait for a condition
|
|
693
|
+
*/
|
|
694
|
+
async wait(condition, timeout = 1e4) {
|
|
695
|
+
this.ensureActive();
|
|
696
|
+
let requestData = {
|
|
697
|
+
session_id: this.sessionId,
|
|
698
|
+
timeout
|
|
699
|
+
};
|
|
700
|
+
if (condition.type === "timeout") {
|
|
701
|
+
await new Promise((resolve) => setTimeout(resolve, condition.ms));
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
requestData = {
|
|
705
|
+
...requestData,
|
|
706
|
+
condition: condition.type,
|
|
707
|
+
selector: "selector" in condition ? condition.selector : void 0
|
|
708
|
+
};
|
|
709
|
+
try {
|
|
710
|
+
await this.httpClient.post(
|
|
711
|
+
"/v1/browser/interactions/wait",
|
|
712
|
+
requestData
|
|
713
|
+
);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
const errorMessage = getErrorMessage(error);
|
|
716
|
+
if (errorMessage.includes("timeout")) {
|
|
717
|
+
throw new BrowserTimeoutError(
|
|
718
|
+
`Wait condition timeout: ${condition.type}`,
|
|
719
|
+
{ cause: error }
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
throw new BrowserInteractionError(
|
|
723
|
+
`Wait failed: ${errorMessage}`,
|
|
724
|
+
{ cause: error }
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Close the browser session
|
|
730
|
+
*/
|
|
731
|
+
async close() {
|
|
732
|
+
if (!this._isActive) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
try {
|
|
736
|
+
await this.httpClient.delete(`/v1/browser/sessions/${this.sessionId}`);
|
|
737
|
+
} catch (error) {
|
|
738
|
+
console.warn(`Failed to close browser session ${this.sessionId}:`, getErrorMessage(error));
|
|
739
|
+
} finally {
|
|
740
|
+
this._isActive = false;
|
|
741
|
+
this.emit("close");
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Check if session is active
|
|
746
|
+
*/
|
|
747
|
+
get isActive() {
|
|
748
|
+
return this._isActive;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Ensure session is active before operations
|
|
752
|
+
*/
|
|
753
|
+
ensureActive() {
|
|
754
|
+
if (!this._isActive) {
|
|
755
|
+
throw new BrowserError("Browser session is not active");
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
// src/client/BrowserManager.ts
|
|
761
|
+
var BrowserManager = class {
|
|
762
|
+
constructor(httpClient, local = false) {
|
|
763
|
+
this.activeSessions = /* @__PURE__ */ new Map();
|
|
764
|
+
this.httpClient = httpClient;
|
|
765
|
+
this.local = local;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Create a new browser session
|
|
769
|
+
*/
|
|
770
|
+
async createSession(options = {}) {
|
|
771
|
+
if (this.local) {
|
|
772
|
+
throw new UnsupportedOperationError(
|
|
773
|
+
"Browser session management is not supported in local mode. Use navigate() or extractContent() with URL directly."
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
const requestData = {
|
|
777
|
+
viewport_width: options.viewportWidth || 1920,
|
|
778
|
+
viewport_height: options.viewportHeight || 1080,
|
|
779
|
+
user_agent: options.userAgent
|
|
780
|
+
};
|
|
781
|
+
try {
|
|
782
|
+
const response = await this.httpClient.post(
|
|
783
|
+
"/v1/browser/sessions/",
|
|
784
|
+
requestData
|
|
785
|
+
);
|
|
786
|
+
if (!response.session_id) {
|
|
787
|
+
throw new BrowserSessionError("No session ID returned from server");
|
|
788
|
+
}
|
|
789
|
+
const session = new BrowserSession(response.session_id, this.httpClient);
|
|
790
|
+
this.activeSessions.set(response.session_id, session);
|
|
791
|
+
session.on("close", () => {
|
|
792
|
+
this.activeSessions.delete(response.session_id);
|
|
793
|
+
});
|
|
794
|
+
return session;
|
|
795
|
+
} catch (error) {
|
|
796
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
797
|
+
throw new BrowserSessionError(
|
|
798
|
+
`Failed to create browser session: ${errorMessage}`,
|
|
799
|
+
{ cause: error }
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Get information about a specific session
|
|
805
|
+
*/
|
|
806
|
+
async getSession(sessionId) {
|
|
807
|
+
try {
|
|
808
|
+
const response = await this.httpClient.get(
|
|
809
|
+
`/v1/browser/sessions/${sessionId}`
|
|
810
|
+
);
|
|
811
|
+
return {
|
|
812
|
+
sessionId: response.session_id,
|
|
813
|
+
status: response.status || "active",
|
|
814
|
+
createdAt: response.created_at,
|
|
815
|
+
viewportWidth: response.viewport_width,
|
|
816
|
+
viewportHeight: response.viewport_height,
|
|
817
|
+
userAgent: response.user_agent
|
|
818
|
+
};
|
|
819
|
+
} catch (error) {
|
|
820
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
821
|
+
throw new BrowserSessionError(
|
|
822
|
+
`Failed to get session info: ${errorMessage}`,
|
|
823
|
+
{ cause: error }
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* List all browser sessions
|
|
829
|
+
*/
|
|
830
|
+
async listSessions() {
|
|
831
|
+
try {
|
|
832
|
+
const response = await this.httpClient.get("/v1/browser/sessions/");
|
|
833
|
+
if (!Array.isArray(response.sessions)) {
|
|
834
|
+
return [];
|
|
835
|
+
}
|
|
836
|
+
return response.sessions.map((session) => ({
|
|
837
|
+
sessionId: session.session_id,
|
|
838
|
+
status: session.status || "active",
|
|
839
|
+
createdAt: session.created_at,
|
|
840
|
+
viewportWidth: session.viewport_width,
|
|
841
|
+
viewportHeight: session.viewport_height,
|
|
842
|
+
userAgent: session.user_agent
|
|
843
|
+
}));
|
|
844
|
+
} catch (error) {
|
|
845
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
846
|
+
throw new BrowserSessionError(
|
|
847
|
+
`Failed to list sessions: ${errorMessage}`,
|
|
848
|
+
{ cause: error }
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Get a locally tracked session
|
|
854
|
+
*/
|
|
855
|
+
getLocalSession(sessionId) {
|
|
856
|
+
return this.activeSessions.get(sessionId);
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Get all locally tracked sessions
|
|
860
|
+
*/
|
|
861
|
+
getLocalSessions() {
|
|
862
|
+
return Array.from(this.activeSessions.values());
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Close all active sessions
|
|
866
|
+
*/
|
|
867
|
+
async closeAllSessions() {
|
|
868
|
+
const sessions = Array.from(this.activeSessions.values());
|
|
869
|
+
await Promise.allSettled(
|
|
870
|
+
sessions.map((session) => session.close())
|
|
871
|
+
);
|
|
872
|
+
this.activeSessions.clear();
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Navigate to a URL (local mode support - no session required)
|
|
876
|
+
*/
|
|
877
|
+
async navigate(url, options = {}) {
|
|
878
|
+
if (!this.local) {
|
|
879
|
+
throw new UnsupportedOperationError(
|
|
880
|
+
"navigate() without session is only supported in local mode. In cloud mode, create a session first."
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
const requestData = {
|
|
884
|
+
url,
|
|
885
|
+
wait_timeout: options.waitTimeout || 3e4
|
|
886
|
+
};
|
|
887
|
+
try {
|
|
888
|
+
const response = await this.httpClient.post(
|
|
889
|
+
"/v1/browser/interactions/navigate",
|
|
890
|
+
requestData
|
|
891
|
+
);
|
|
892
|
+
return {
|
|
893
|
+
success: response.success !== false,
|
|
894
|
+
url: response.url || url,
|
|
895
|
+
title: response.title,
|
|
896
|
+
status: response.status
|
|
897
|
+
};
|
|
898
|
+
} catch (error) {
|
|
899
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
900
|
+
throw new BrowserNavigationError(
|
|
901
|
+
`Navigation failed: ${errorMessage}`,
|
|
902
|
+
{ cause: error }
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Extract LLM-friendly content (local mode support - no session required)
|
|
908
|
+
*/
|
|
909
|
+
async extractContent(options = {}) {
|
|
910
|
+
if (!this.local) {
|
|
911
|
+
throw new UnsupportedOperationError(
|
|
912
|
+
"extractContent() without session is only supported in local mode. In cloud mode, create a session first."
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
if (!options.url) {
|
|
916
|
+
throw new BrowserInteractionError("url is required in local mode");
|
|
917
|
+
}
|
|
918
|
+
const requestData = {
|
|
919
|
+
url: options.url,
|
|
920
|
+
include_interactive: options.includeInteractive !== false,
|
|
921
|
+
include_anchors: options.includeAnchors !== false,
|
|
922
|
+
max_anchors: options.maxAnchors ?? 50
|
|
923
|
+
};
|
|
924
|
+
try {
|
|
925
|
+
const response = await this.httpClient.post(
|
|
926
|
+
"/v1/browser/interactions/content",
|
|
927
|
+
requestData
|
|
928
|
+
);
|
|
929
|
+
return {
|
|
930
|
+
readableContent: {
|
|
931
|
+
...response.readable_content || {},
|
|
932
|
+
content: response.readable_content?.content || ""
|
|
933
|
+
},
|
|
934
|
+
interactiveElements: response.interactive_elements,
|
|
935
|
+
contentAnchors: response.content_anchors
|
|
936
|
+
};
|
|
937
|
+
} catch (error) {
|
|
938
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
939
|
+
throw new BrowserInteractionError(
|
|
940
|
+
`Content extraction failed: ${errorMessage}`,
|
|
941
|
+
{ cause: error }
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Clean up resources
|
|
947
|
+
*/
|
|
948
|
+
async dispose() {
|
|
949
|
+
await this.closeAllSessions();
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
// src/utils/path.ts
|
|
954
|
+
function encodePathSegment(value) {
|
|
955
|
+
const segment = String(value);
|
|
956
|
+
if (segment === "." || segment === "..") {
|
|
957
|
+
throw new Error("Path traversal not allowed in path segment");
|
|
958
|
+
}
|
|
959
|
+
return encodeURIComponent(segment);
|
|
960
|
+
}
|
|
961
|
+
function encodePathSegments(path4) {
|
|
962
|
+
const segments = path4.replace(/^\/+/, "").split("/").filter((segment) => segment.length > 0 && segment !== ".");
|
|
963
|
+
if (segments.some((segment) => segment === "..")) {
|
|
964
|
+
throw new Error("Path traversal not allowed in proxy path");
|
|
965
|
+
}
|
|
966
|
+
return segments.map((segment) => encodeURIComponent(segment)).join("/");
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/client/VMsManager.ts
|
|
970
|
+
var VMsManager = class {
|
|
971
|
+
constructor(httpClient, local = false) {
|
|
972
|
+
this.httpClient = httpClient;
|
|
973
|
+
this.local = local;
|
|
974
|
+
}
|
|
975
|
+
ensureCloud(operation) {
|
|
976
|
+
if (this.local) {
|
|
977
|
+
throw new UnsupportedOperationError(
|
|
978
|
+
`${operation} is not supported in local mode.`
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
async create(payload = {}, wait = true) {
|
|
983
|
+
this.ensureCloud("VM creation");
|
|
984
|
+
return this.httpClient.request({
|
|
985
|
+
method: "POST",
|
|
986
|
+
url: "/v1/vms",
|
|
987
|
+
params: { wait },
|
|
988
|
+
data: payload,
|
|
989
|
+
headers: { "X-API-Key": this.httpClient.apiKey }
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
async list() {
|
|
993
|
+
this.ensureCloud("VM listing");
|
|
994
|
+
return this.httpClient.get(
|
|
995
|
+
"/v1/vms",
|
|
996
|
+
void 0,
|
|
997
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
async listAll() {
|
|
1001
|
+
return this.listAllRecords();
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Calls GET /v1/vms/ (trailing slash), a distinct route from GET /v1/vms.
|
|
1005
|
+
*/
|
|
1006
|
+
async listAllRecords() {
|
|
1007
|
+
this.ensureCloud("VM listing");
|
|
1008
|
+
return this.httpClient.get(
|
|
1009
|
+
"/v1/vms/",
|
|
1010
|
+
void 0,
|
|
1011
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
async get(itemId) {
|
|
1015
|
+
this.ensureCloud("VM lookup");
|
|
1016
|
+
const safeItemId = encodePathSegment(itemId);
|
|
1017
|
+
return this.httpClient.get(
|
|
1018
|
+
`/v1/vms/${safeItemId}`,
|
|
1019
|
+
void 0,
|
|
1020
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
async update(vmId, payload = {}) {
|
|
1024
|
+
this.ensureCloud("VM update");
|
|
1025
|
+
const safeVmId = encodePathSegment(vmId);
|
|
1026
|
+
return this.httpClient.patch(
|
|
1027
|
+
`/v1/vms/${safeVmId}`,
|
|
1028
|
+
payload,
|
|
1029
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
async delete(vmId) {
|
|
1033
|
+
this.ensureCloud("VM deletion");
|
|
1034
|
+
const safeVmId = encodePathSegment(vmId);
|
|
1035
|
+
return this.httpClient.delete(
|
|
1036
|
+
`/v1/vms/${safeVmId}`,
|
|
1037
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
async clone(vmId, payload = {}, wait = true) {
|
|
1041
|
+
this.ensureCloud("VM clone");
|
|
1042
|
+
const safeVmId = encodePathSegment(vmId);
|
|
1043
|
+
return this.httpClient.request({
|
|
1044
|
+
method: "POST",
|
|
1045
|
+
url: `/v1/vms/${safeVmId}/clone`,
|
|
1046
|
+
params: { wait },
|
|
1047
|
+
data: payload,
|
|
1048
|
+
headers: { "X-API-Key": this.httpClient.apiKey }
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
async snapshot(vmId, payload = {}, wait = true) {
|
|
1052
|
+
this.ensureCloud("VM snapshot");
|
|
1053
|
+
const safeVmId = encodePathSegment(vmId);
|
|
1054
|
+
return this.httpClient.request({
|
|
1055
|
+
method: "POST",
|
|
1056
|
+
url: `/v1/vms/${safeVmId}/snapshot`,
|
|
1057
|
+
params: { wait },
|
|
1058
|
+
data: payload,
|
|
1059
|
+
headers: { "X-API-Key": this.httpClient.apiKey }
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
async mountVolume(vmId, payload, wait = true) {
|
|
1063
|
+
this.ensureCloud("VM volume mount");
|
|
1064
|
+
const safeVmId = encodePathSegment(vmId);
|
|
1065
|
+
return this.httpClient.request({
|
|
1066
|
+
method: "POST",
|
|
1067
|
+
url: `/v1/vms/${safeVmId}/volumes`,
|
|
1068
|
+
params: { wait },
|
|
1069
|
+
data: payload,
|
|
1070
|
+
headers: { "X-API-Key": this.httpClient.apiKey }
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
async listVolumes(vmId) {
|
|
1074
|
+
this.ensureCloud("VM volume listing");
|
|
1075
|
+
const safeVmId = encodePathSegment(vmId);
|
|
1076
|
+
return this.httpClient.get(
|
|
1077
|
+
`/v1/vms/${safeVmId}/volumes`,
|
|
1078
|
+
void 0,
|
|
1079
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
async unmountVolume(vmId, volumeId, mountPath, wait = true) {
|
|
1083
|
+
this.ensureCloud("VM volume unmount");
|
|
1084
|
+
const safeVmId = encodePathSegment(vmId);
|
|
1085
|
+
const safeVolumeId = encodePathSegment(volumeId);
|
|
1086
|
+
return this.httpClient.request({
|
|
1087
|
+
method: "DELETE",
|
|
1088
|
+
url: `/v1/vms/${safeVmId}/volumes/${safeVolumeId}`,
|
|
1089
|
+
params: { mount_path: mountPath, wait },
|
|
1090
|
+
headers: { "X-API-Key": this.httpClient.apiKey }
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
// src/client/SnapshotsManager.ts
|
|
1096
|
+
var SnapshotsManager = class {
|
|
1097
|
+
constructor(httpClient, local = false) {
|
|
1098
|
+
this.httpClient = httpClient;
|
|
1099
|
+
this.local = local;
|
|
1100
|
+
}
|
|
1101
|
+
ensureCloud(operation) {
|
|
1102
|
+
if (this.local) {
|
|
1103
|
+
throw new UnsupportedOperationError(
|
|
1104
|
+
`${operation} is not supported in local mode.`
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
async create(payload) {
|
|
1109
|
+
this.ensureCloud("Snapshot creation");
|
|
1110
|
+
return this.httpClient.post(
|
|
1111
|
+
"/v1/snapshots",
|
|
1112
|
+
payload,
|
|
1113
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
async list(options = {}) {
|
|
1117
|
+
this.ensureCloud("Snapshot listing");
|
|
1118
|
+
return this.httpClient.get(
|
|
1119
|
+
"/v1/snapshots",
|
|
1120
|
+
options,
|
|
1121
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
async get(snapshotId) {
|
|
1125
|
+
this.ensureCloud("Snapshot lookup");
|
|
1126
|
+
const safeSnapshotId = encodePathSegment(snapshotId);
|
|
1127
|
+
return this.httpClient.get(
|
|
1128
|
+
`/v1/snapshots/${safeSnapshotId}`,
|
|
1129
|
+
void 0,
|
|
1130
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
async delete(snapshotId) {
|
|
1134
|
+
this.ensureCloud("Snapshot deletion");
|
|
1135
|
+
const safeSnapshotId = encodePathSegment(snapshotId);
|
|
1136
|
+
return this.httpClient.delete(
|
|
1137
|
+
`/v1/snapshots/${safeSnapshotId}`,
|
|
1138
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
// src/client/SharesManager.ts
|
|
1144
|
+
var SharesManager = class {
|
|
1145
|
+
constructor(httpClient, local = false, getSessionId = () => null) {
|
|
1146
|
+
this.httpClient = httpClient;
|
|
1147
|
+
this.local = local;
|
|
1148
|
+
this.getSessionId = getSessionId;
|
|
1149
|
+
}
|
|
1150
|
+
ensureCloud(operation) {
|
|
1151
|
+
if (this.local) {
|
|
1152
|
+
throw new UnsupportedOperationError(
|
|
1153
|
+
`${operation} is not supported in local mode.`
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
async create(payload) {
|
|
1158
|
+
this.ensureCloud("Share creation");
|
|
1159
|
+
const body = { ...payload };
|
|
1160
|
+
if (!body.session_id && !body.vm_id) {
|
|
1161
|
+
const sid = this.getSessionId();
|
|
1162
|
+
if (!sid) {
|
|
1163
|
+
throw new SessionError(
|
|
1164
|
+
"Provide session_id/vm_id or create a session before creating a share."
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
body.session_id = sid;
|
|
1168
|
+
}
|
|
1169
|
+
return this.httpClient.post(
|
|
1170
|
+
"/v1/shares",
|
|
1171
|
+
body,
|
|
1172
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
async update(shareId, payload) {
|
|
1176
|
+
this.ensureCloud("Share update");
|
|
1177
|
+
const safeShareId = encodePathSegment(shareId);
|
|
1178
|
+
return this.httpClient.patch(
|
|
1179
|
+
`/v1/shares/${safeShareId}`,
|
|
1180
|
+
payload,
|
|
1181
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
// src/client/CustomDomainsManager.ts
|
|
1187
|
+
var CustomDomainsManager = class {
|
|
1188
|
+
constructor(httpClient, local = false) {
|
|
1189
|
+
this.httpClient = httpClient;
|
|
1190
|
+
this.local = local;
|
|
1191
|
+
}
|
|
1192
|
+
ensureCloud(operation) {
|
|
1193
|
+
if (this.local) {
|
|
1194
|
+
throw new UnsupportedOperationError(
|
|
1195
|
+
`${operation} is not supported in local mode.`
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
async create(payload) {
|
|
1200
|
+
this.ensureCloud("Custom domain creation");
|
|
1201
|
+
return this.httpClient.post(
|
|
1202
|
+
"/v1/custom-domains",
|
|
1203
|
+
payload,
|
|
1204
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
async list() {
|
|
1208
|
+
this.ensureCloud("Custom domain listing");
|
|
1209
|
+
return this.httpClient.get(
|
|
1210
|
+
"/v1/custom-domains",
|
|
1211
|
+
void 0,
|
|
1212
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
async health(domainId) {
|
|
1216
|
+
this.ensureCloud("Custom domain health");
|
|
1217
|
+
const safeDomainId = encodePathSegment(domainId);
|
|
1218
|
+
return this.httpClient.get(
|
|
1219
|
+
`/v1/custom-domains/${safeDomainId}/health`,
|
|
1220
|
+
void 0,
|
|
1221
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
async verify(domainId) {
|
|
1225
|
+
this.ensureCloud("Custom domain verification");
|
|
1226
|
+
const safeDomainId = encodePathSegment(domainId);
|
|
1227
|
+
return this.httpClient.post(
|
|
1228
|
+
`/v1/custom-domains/${safeDomainId}/verify`,
|
|
1229
|
+
{},
|
|
1230
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
async delete(domainId) {
|
|
1234
|
+
this.ensureCloud("Custom domain deletion");
|
|
1235
|
+
const safeDomainId = encodePathSegment(domainId);
|
|
1236
|
+
return this.httpClient.delete(
|
|
1237
|
+
`/v1/custom-domains/${safeDomainId}`,
|
|
1238
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
// src/client/ComputerUseManager.ts
|
|
1244
|
+
var ComputerUseManager = class {
|
|
1245
|
+
constructor(httpClient, local = false) {
|
|
1246
|
+
this.httpClient = httpClient;
|
|
1247
|
+
this.local = local;
|
|
1248
|
+
}
|
|
1249
|
+
ensureCloud(operation) {
|
|
1250
|
+
if (this.local) {
|
|
1251
|
+
throw new UnsupportedOperationError(
|
|
1252
|
+
`${operation} is not supported in local mode.`
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
async viewerUrl(sessionId) {
|
|
1257
|
+
this.ensureCloud("Computer-use viewer URL");
|
|
1258
|
+
const safeSessionId = encodePathSegment(sessionId);
|
|
1259
|
+
return this.httpClient.get(
|
|
1260
|
+
`/v1/computeruse/${safeSessionId}/viewer-url`,
|
|
1261
|
+
void 0,
|
|
1262
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
async proxy(sessionId, path4, method = "GET", options = {}) {
|
|
1266
|
+
this.ensureCloud("Computer-use proxy");
|
|
1267
|
+
const safeSessionId = encodePathSegment(sessionId);
|
|
1268
|
+
const cleanPath = encodePathSegments(path4);
|
|
1269
|
+
return this.httpClient.request({
|
|
1270
|
+
method,
|
|
1271
|
+
url: `/v1/computeruse/${safeSessionId}/${cleanPath}`,
|
|
1272
|
+
params: options.params,
|
|
1273
|
+
data: options.body,
|
|
1274
|
+
headers: { "X-API-Key": this.httpClient.apiKey }
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
async get(sessionId, path4, params) {
|
|
1278
|
+
return this.proxy(sessionId, path4, "GET", { params });
|
|
1279
|
+
}
|
|
1280
|
+
async post(sessionId, path4, body) {
|
|
1281
|
+
return this.proxy(sessionId, path4, "POST", { body });
|
|
1282
|
+
}
|
|
1283
|
+
async put(sessionId, path4, body) {
|
|
1284
|
+
return this.proxy(sessionId, path4, "PUT", { body });
|
|
1285
|
+
}
|
|
1286
|
+
async patch(sessionId, path4, body) {
|
|
1287
|
+
return this.proxy(sessionId, path4, "PATCH", { body });
|
|
1288
|
+
}
|
|
1289
|
+
async delete(sessionId, path4, params) {
|
|
1290
|
+
return this.proxy(sessionId, path4, "DELETE", { params });
|
|
1291
|
+
}
|
|
1292
|
+
async options(sessionId, path4, params) {
|
|
1293
|
+
return this.proxy(sessionId, path4, "OPTIONS", { params });
|
|
1294
|
+
}
|
|
1295
|
+
async head(sessionId, path4, params) {
|
|
1296
|
+
return this.proxy(sessionId, path4, "HEAD", { params });
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Get the VNC WebSocket URL for a computer-use session.
|
|
1300
|
+
* Returns the URL to connect to; the actual connection requires a WebSocket client.
|
|
1301
|
+
*/
|
|
1302
|
+
async vncWebsockify(sessionId) {
|
|
1303
|
+
this.ensureCloud("Computer-use VNC websockify");
|
|
1304
|
+
const safeSessionId = encodePathSegment(sessionId);
|
|
1305
|
+
return this.httpClient.get(
|
|
1306
|
+
`/v1/computeruse/${safeSessionId}/vnc/websockify`,
|
|
1307
|
+
void 0,
|
|
1308
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
// src/client/APIKeysManager.ts
|
|
1314
|
+
var APIKeysManager = class {
|
|
1315
|
+
constructor(httpClient, local = false) {
|
|
1316
|
+
this.httpClient = httpClient;
|
|
1317
|
+
this.local = local;
|
|
1318
|
+
}
|
|
1319
|
+
ensureCloud(operation) {
|
|
1320
|
+
if (this.local) {
|
|
1321
|
+
throw new UnsupportedOperationError(
|
|
1322
|
+
`${operation} is not supported in local mode.`
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
async create(payload = {}) {
|
|
1327
|
+
this.ensureCloud("API key creation");
|
|
1328
|
+
return this.httpClient.post(
|
|
1329
|
+
"/v1/api-keys/",
|
|
1330
|
+
payload,
|
|
1331
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
async list() {
|
|
1335
|
+
this.ensureCloud("API key listing");
|
|
1336
|
+
return this.httpClient.get(
|
|
1337
|
+
"/v1/api-keys/",
|
|
1338
|
+
void 0,
|
|
1339
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
async get(itemId) {
|
|
1343
|
+
this.ensureCloud("API key lookup");
|
|
1344
|
+
const safeItemId = encodePathSegment(itemId);
|
|
1345
|
+
return this.httpClient.get(
|
|
1346
|
+
`/v1/api-keys/${safeItemId}`,
|
|
1347
|
+
void 0,
|
|
1348
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
async update(itemId, payload) {
|
|
1352
|
+
this.ensureCloud("API key update");
|
|
1353
|
+
const safeItemId = encodePathSegment(itemId);
|
|
1354
|
+
return this.httpClient.patch(
|
|
1355
|
+
`/v1/api-keys/${safeItemId}`,
|
|
1356
|
+
payload,
|
|
1357
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
async delete(itemId) {
|
|
1361
|
+
this.ensureCloud("API key deletion");
|
|
1362
|
+
const safeItemId = encodePathSegment(itemId);
|
|
1363
|
+
return this.httpClient.delete(
|
|
1364
|
+
`/v1/api-keys/${safeItemId}`,
|
|
1365
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
// src/client/AuditManager.ts
|
|
1371
|
+
var AuditManager = class {
|
|
1372
|
+
constructor(httpClient, local = false) {
|
|
1373
|
+
this.httpClient = httpClient;
|
|
1374
|
+
this.local = local;
|
|
1375
|
+
}
|
|
1376
|
+
ensureCloud(operation) {
|
|
1377
|
+
if (this.local) {
|
|
1378
|
+
throw new UnsupportedOperationError(
|
|
1379
|
+
`${operation} is not supported in local mode.`
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
async catalog() {
|
|
1384
|
+
this.ensureCloud("Audit catalog");
|
|
1385
|
+
return this.httpClient.get(
|
|
1386
|
+
"/v1/audit/catalog",
|
|
1387
|
+
void 0,
|
|
1388
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
async events(query = {}) {
|
|
1392
|
+
this.ensureCloud("Audit event listing");
|
|
1393
|
+
return this.httpClient.get(
|
|
1394
|
+
"/v1/audit/events",
|
|
1395
|
+
query,
|
|
1396
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
async getEvent(eventId) {
|
|
1400
|
+
this.ensureCloud("Audit event lookup");
|
|
1401
|
+
const safeEventId = encodePathSegment(eventId);
|
|
1402
|
+
return this.httpClient.get(
|
|
1403
|
+
`/v1/audit/events/${safeEventId}`,
|
|
1404
|
+
void 0,
|
|
1405
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1410
|
+
// src/client/WebhooksManager.ts
|
|
1411
|
+
var WebhooksManager = class {
|
|
1412
|
+
constructor(httpClient, local = false) {
|
|
1413
|
+
this.httpClient = httpClient;
|
|
1414
|
+
this.local = local;
|
|
1415
|
+
}
|
|
1416
|
+
ensureCloud(operation) {
|
|
1417
|
+
if (this.local) {
|
|
1418
|
+
throw new UnsupportedOperationError(
|
|
1419
|
+
`${operation} is not supported in local mode.`
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
async createEndpoint(payload) {
|
|
1424
|
+
this.ensureCloud("Webhook endpoint creation");
|
|
1425
|
+
return this.httpClient.post(
|
|
1426
|
+
"/v1/webhooks/endpoints",
|
|
1427
|
+
payload,
|
|
1428
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
async listEndpoints() {
|
|
1432
|
+
this.ensureCloud("Webhook endpoint listing");
|
|
1433
|
+
return this.httpClient.get(
|
|
1434
|
+
"/v1/webhooks/endpoints",
|
|
1435
|
+
void 0,
|
|
1436
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
async getEndpoint(endpointId) {
|
|
1440
|
+
this.ensureCloud("Webhook endpoint lookup");
|
|
1441
|
+
const safeEndpointId = encodePathSegment(endpointId);
|
|
1442
|
+
return this.httpClient.get(
|
|
1443
|
+
`/v1/webhooks/endpoints/${safeEndpointId}`,
|
|
1444
|
+
void 0,
|
|
1445
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
async updateEndpoint(endpointId, payload) {
|
|
1449
|
+
this.ensureCloud("Webhook endpoint update");
|
|
1450
|
+
const safeEndpointId = encodePathSegment(endpointId);
|
|
1451
|
+
return this.httpClient.patch(
|
|
1452
|
+
`/v1/webhooks/endpoints/${safeEndpointId}`,
|
|
1453
|
+
payload,
|
|
1454
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
async deleteEndpoint(endpointId) {
|
|
1458
|
+
this.ensureCloud("Webhook endpoint deletion");
|
|
1459
|
+
const safeEndpointId = encodePathSegment(endpointId);
|
|
1460
|
+
return this.httpClient.delete(
|
|
1461
|
+
`/v1/webhooks/endpoints/${safeEndpointId}`,
|
|
1462
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
async rotateSecret(endpointId) {
|
|
1466
|
+
this.ensureCloud("Webhook secret rotation");
|
|
1467
|
+
const safeEndpointId = encodePathSegment(endpointId);
|
|
1468
|
+
return this.httpClient.post(
|
|
1469
|
+
`/v1/webhooks/endpoints/${safeEndpointId}/rotate-secret`,
|
|
1470
|
+
{},
|
|
1471
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
async sendTestEvent(endpointId) {
|
|
1475
|
+
this.ensureCloud("Webhook test event");
|
|
1476
|
+
const safeEndpointId = encodePathSegment(endpointId);
|
|
1477
|
+
return this.httpClient.post(
|
|
1478
|
+
`/v1/webhooks/endpoints/${safeEndpointId}/test`,
|
|
1479
|
+
{},
|
|
1480
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
async verifyEndpoint(endpointId) {
|
|
1484
|
+
this.ensureCloud("Webhook endpoint verification");
|
|
1485
|
+
const safeEndpointId = encodePathSegment(endpointId);
|
|
1486
|
+
return this.httpClient.post(
|
|
1487
|
+
`/v1/webhooks/endpoints/${safeEndpointId}/verify`,
|
|
1488
|
+
{},
|
|
1489
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
async listDeliveries(query = {}) {
|
|
1493
|
+
this.ensureCloud("Webhook delivery listing");
|
|
1494
|
+
return this.httpClient.get(
|
|
1495
|
+
"/v1/webhooks/deliveries",
|
|
1496
|
+
query,
|
|
1497
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
async replayDelivery(deliveryId) {
|
|
1501
|
+
this.ensureCloud("Webhook delivery replay");
|
|
1502
|
+
const safeDeliveryId = encodePathSegment(deliveryId);
|
|
1503
|
+
return this.httpClient.post(
|
|
1504
|
+
`/v1/webhooks/deliveries/${safeDeliveryId}/replay`,
|
|
1505
|
+
{},
|
|
1506
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
};
|
|
1510
|
+
|
|
1511
|
+
// src/client/VolumesManager.ts
|
|
1512
|
+
var import_fs = __toESM(require("fs"));
|
|
1513
|
+
var import_path9 = __toESM(require("path"));
|
|
1514
|
+
var import_form_data = __toESM(require("form-data"));
|
|
1515
|
+
var VolumesManager = class {
|
|
1516
|
+
constructor(httpClient, local = false) {
|
|
1517
|
+
this.httpClient = httpClient;
|
|
1518
|
+
this.local = local;
|
|
1519
|
+
}
|
|
1520
|
+
ensureCloud(operation) {
|
|
1521
|
+
if (this.local) {
|
|
1522
|
+
throw new UnsupportedOperationError(
|
|
1523
|
+
`${operation} is not supported in local mode.`
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
async create(payload) {
|
|
1528
|
+
this.ensureCloud("Volume creation");
|
|
1529
|
+
return this.httpClient.post(
|
|
1530
|
+
"/v1/volumes",
|
|
1531
|
+
payload,
|
|
1532
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
async list(refreshUsage = false) {
|
|
1536
|
+
this.ensureCloud("Volume listing");
|
|
1537
|
+
return this.httpClient.get(
|
|
1538
|
+
"/v1/volumes",
|
|
1539
|
+
{ refresh_usage: refreshUsage },
|
|
1540
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
async get(volumeId, refreshUsage = false) {
|
|
1544
|
+
this.ensureCloud("Volume lookup");
|
|
1545
|
+
const safeVolumeId = encodePathSegment(volumeId);
|
|
1546
|
+
return this.httpClient.get(
|
|
1547
|
+
`/v1/volumes/${safeVolumeId}`,
|
|
1548
|
+
{ refresh_usage: refreshUsage },
|
|
1549
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
async update(volumeId, payload) {
|
|
1553
|
+
this.ensureCloud("Volume update");
|
|
1554
|
+
const safeVolumeId = encodePathSegment(volumeId);
|
|
1555
|
+
return this.httpClient.patch(
|
|
1556
|
+
`/v1/volumes/${safeVolumeId}`,
|
|
1557
|
+
payload,
|
|
1558
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
async delete(volumeId) {
|
|
1562
|
+
this.ensureCloud("Volume deletion");
|
|
1563
|
+
const safeVolumeId = encodePathSegment(volumeId);
|
|
1564
|
+
return this.httpClient.delete(
|
|
1565
|
+
`/v1/volumes/${safeVolumeId}`,
|
|
1566
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
async createCheckpoint(volumeId, payload = {}) {
|
|
1570
|
+
this.ensureCloud("Volume checkpoint creation");
|
|
1571
|
+
const safeVolumeId = encodePathSegment(volumeId);
|
|
1572
|
+
return this.httpClient.post(
|
|
1573
|
+
`/v1/volumes/${safeVolumeId}/checkpoints`,
|
|
1574
|
+
payload,
|
|
1575
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
async listCheckpoints(volumeId) {
|
|
1579
|
+
this.ensureCloud("Volume checkpoint listing");
|
|
1580
|
+
const safeVolumeId = encodePathSegment(volumeId);
|
|
1581
|
+
return this.httpClient.get(
|
|
1582
|
+
`/v1/volumes/${safeVolumeId}/checkpoints`,
|
|
1583
|
+
void 0,
|
|
1584
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
async deleteCheckpoint(volumeId, checkpointId) {
|
|
1588
|
+
this.ensureCloud("Volume checkpoint deletion");
|
|
1589
|
+
const safeVolumeId = encodePathSegment(volumeId);
|
|
1590
|
+
const safeCheckpointId = encodePathSegment(checkpointId);
|
|
1591
|
+
return this.httpClient.delete(
|
|
1592
|
+
`/v1/volumes/${safeVolumeId}/checkpoints/${safeCheckpointId}`,
|
|
1593
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
async uploadFile(volumeId, payload) {
|
|
1597
|
+
this.ensureCloud("Volume file upload");
|
|
1598
|
+
if (!payload.path || payload.path.trim().length === 0) {
|
|
1599
|
+
throw new Error("payload.path is required");
|
|
1600
|
+
}
|
|
1601
|
+
const hasFilePath = typeof payload.filePath === "string" && payload.filePath.length > 0;
|
|
1602
|
+
const hasFileContent = payload.fileContent !== void 0;
|
|
1603
|
+
if (hasFilePath === hasFileContent) {
|
|
1604
|
+
throw new Error("Provide exactly one of payload.filePath or payload.fileContent");
|
|
1605
|
+
}
|
|
1606
|
+
const safeVolumeId = encodePathSegment(volumeId);
|
|
1607
|
+
const formData = new import_form_data.default();
|
|
1608
|
+
if (hasFilePath) {
|
|
1609
|
+
const sourcePath = payload.filePath;
|
|
1610
|
+
const filename = payload.filename || import_path9.default.basename(sourcePath);
|
|
1611
|
+
formData.append("file", import_fs.default.createReadStream(sourcePath), { filename });
|
|
1612
|
+
} else {
|
|
1613
|
+
const filename = payload.filename || "upload.bin";
|
|
1614
|
+
const raw = payload.fileContent;
|
|
1615
|
+
const buffer = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
1616
|
+
formData.append("file", buffer, { filename });
|
|
1617
|
+
}
|
|
1618
|
+
formData.append("path", payload.path);
|
|
1619
|
+
if (payload.overwrite !== void 0) {
|
|
1620
|
+
formData.append("overwrite", String(payload.overwrite));
|
|
1621
|
+
}
|
|
1622
|
+
return this.httpClient.postFormData(
|
|
1623
|
+
`/v1/volumes/${safeVolumeId}/files/upload`,
|
|
1624
|
+
formData
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
async downloadFile(volumeId, path4) {
|
|
1628
|
+
this.ensureCloud("Volume file download");
|
|
1629
|
+
const safeVolumeId = encodePathSegment(volumeId);
|
|
1630
|
+
const response = await this.httpClient.post(
|
|
1631
|
+
`/v1/volumes/${safeVolumeId}/files/download`,
|
|
1632
|
+
{ path: path4 },
|
|
1633
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1634
|
+
);
|
|
1635
|
+
return {
|
|
1636
|
+
path: response.path,
|
|
1637
|
+
filename: response.filename,
|
|
1638
|
+
size: response.size,
|
|
1639
|
+
content: Buffer.from(response.content || "", "base64")
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
async listFiles(volumeId, options = {}) {
|
|
1643
|
+
this.ensureCloud("Volume file listing");
|
|
1644
|
+
const safeVolumeId = encodePathSegment(volumeId);
|
|
1645
|
+
return this.httpClient.get(
|
|
1646
|
+
`/v1/volumes/${safeVolumeId}/files`,
|
|
1647
|
+
{
|
|
1648
|
+
prefix: options.prefix ?? "",
|
|
1649
|
+
recursive: options.recursive ?? true,
|
|
1650
|
+
limit: options.limit ?? 1e3
|
|
1651
|
+
},
|
|
1652
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
async deleteFile(volumeId, targetPath) {
|
|
1656
|
+
this.ensureCloud("Volume file deletion");
|
|
1657
|
+
const safeVolumeId = encodePathSegment(volumeId);
|
|
1658
|
+
return this.httpClient.request({
|
|
1659
|
+
method: "DELETE",
|
|
1660
|
+
url: `/v1/volumes/${safeVolumeId}/files`,
|
|
1661
|
+
params: { path: targetPath },
|
|
1662
|
+
headers: { "X-API-Key": this.httpClient.apiKey }
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
|
|
1667
|
+
// src/client/InstaVM.ts
|
|
1668
|
+
var import_form_data2 = __toESM(require("form-data"));
|
|
1669
|
+
var InstaVM = class {
|
|
1670
|
+
constructor(apiKey, options = {}) {
|
|
1671
|
+
this._sessionId = null;
|
|
1672
|
+
this._vmUsed = false;
|
|
1673
|
+
this.local = options.local || false;
|
|
1674
|
+
this.timeout = options.timeout || 3e5;
|
|
1675
|
+
this.memory_mb = options.memory_mb;
|
|
1676
|
+
this.cpu_count = options.cpu_count;
|
|
1677
|
+
this.metadata = options.metadata;
|
|
1678
|
+
this.env = options.env;
|
|
1679
|
+
if (!this.local && !apiKey) {
|
|
1680
|
+
throw new AuthenticationError("API key is required for cloud mode");
|
|
1681
|
+
}
|
|
1682
|
+
const config = {
|
|
1683
|
+
baseURL: this.local ? options.localURL || "http://coderunner.local:8222" : options.baseURL || "https://api.instavm.io",
|
|
1684
|
+
timeout: this.timeout,
|
|
1685
|
+
maxRetries: options.maxRetries || 3,
|
|
1686
|
+
retryDelay: options.retryDelay || 1e3,
|
|
1687
|
+
apiKey: apiKey || ""
|
|
1688
|
+
};
|
|
1689
|
+
this.httpClient = new HTTPClient(config);
|
|
1690
|
+
this.browser = new BrowserManager(this.httpClient, this.local);
|
|
1691
|
+
this.vms = new VMsManager(this.httpClient, this.local);
|
|
1692
|
+
this.snapshots = new SnapshotsManager(this.httpClient, this.local);
|
|
1693
|
+
this.shares = new SharesManager(this.httpClient, this.local, () => this._sessionId);
|
|
1694
|
+
this.customDomains = new CustomDomainsManager(this.httpClient, this.local);
|
|
1695
|
+
this.computerUse = new ComputerUseManager(this.httpClient, this.local);
|
|
1696
|
+
this.apiKeys = new APIKeysManager(this.httpClient, this.local);
|
|
1697
|
+
this.audit = new AuditManager(this.httpClient, this.local);
|
|
1698
|
+
this.webhooks = new WebhooksManager(this.httpClient, this.local);
|
|
1699
|
+
this.volumes = new VolumesManager(this.httpClient, this.local);
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Ensure operation is not called in local mode
|
|
1703
|
+
*/
|
|
1704
|
+
ensureNotLocal(operationName) {
|
|
1705
|
+
if (this.local) {
|
|
1706
|
+
throw new UnsupportedOperationError(
|
|
1707
|
+
`${operationName} is not supported in local mode. This operation is only available when using the cloud API.`
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Execute code synchronously
|
|
1713
|
+
*
|
|
1714
|
+
* @param command - Command to execute
|
|
1715
|
+
* @param options - Execution options
|
|
1716
|
+
* @param options.timeout - Request timeout in milliseconds (used for HTTP request timeout and sent to API in seconds)
|
|
1717
|
+
* @returns Execution result
|
|
1718
|
+
*/
|
|
1719
|
+
async execute(command, options = {}) {
|
|
1720
|
+
let sessionId = options.sessionId || this._sessionId;
|
|
1721
|
+
if (!this.local && !sessionId) {
|
|
1722
|
+
sessionId = await this.createSession();
|
|
1723
|
+
}
|
|
1724
|
+
const requestData = {
|
|
1725
|
+
command,
|
|
1726
|
+
language: options.language || "python"
|
|
1727
|
+
};
|
|
1728
|
+
if (options.timeout !== void 0) {
|
|
1729
|
+
requestData.timeout = Math.floor(options.timeout / 1e3);
|
|
1730
|
+
}
|
|
1731
|
+
if (!this.local && sessionId) {
|
|
1732
|
+
requestData.session_id = sessionId;
|
|
1733
|
+
}
|
|
1734
|
+
const requestTimeout = options.timeout !== void 0 ? options.timeout : this.timeout;
|
|
1735
|
+
try {
|
|
1736
|
+
const response = await this.httpClient.postExecution(
|
|
1737
|
+
"/execute",
|
|
1738
|
+
requestData,
|
|
1739
|
+
void 0,
|
|
1740
|
+
requestTimeout
|
|
1741
|
+
);
|
|
1742
|
+
if (response.session_id) {
|
|
1743
|
+
this._sessionId = response.session_id;
|
|
1744
|
+
}
|
|
1745
|
+
if (!this.local) {
|
|
1746
|
+
this._vmUsed = true;
|
|
1747
|
+
}
|
|
1748
|
+
return {
|
|
1749
|
+
stdout: response.stdout || "",
|
|
1750
|
+
stderr: response.stderr || "",
|
|
1751
|
+
success: response.success !== false,
|
|
1752
|
+
executionTime: response.execution_time,
|
|
1753
|
+
cpuTime: response.cpu_time,
|
|
1754
|
+
createdAt: response.created_at,
|
|
1755
|
+
sessionId: response.session_id,
|
|
1756
|
+
error: response.error
|
|
1757
|
+
};
|
|
1758
|
+
} catch (error) {
|
|
1759
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1760
|
+
throw new ExecutionError(
|
|
1761
|
+
`Code execution failed: ${errorMessage}`,
|
|
1762
|
+
void 0,
|
|
1763
|
+
void 0,
|
|
1764
|
+
{ cause: error }
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Execute code asynchronously
|
|
1770
|
+
*/
|
|
1771
|
+
async executeAsync(command, options = {}) {
|
|
1772
|
+
this.ensureNotLocal("Async execution");
|
|
1773
|
+
let sessionId = options.sessionId || this._sessionId;
|
|
1774
|
+
if (!sessionId) {
|
|
1775
|
+
sessionId = await this.createSession();
|
|
1776
|
+
}
|
|
1777
|
+
const requestData = {
|
|
1778
|
+
command,
|
|
1779
|
+
language: options.language || "python",
|
|
1780
|
+
timeout: options.timeout || 15,
|
|
1781
|
+
session_id: sessionId
|
|
1782
|
+
};
|
|
1783
|
+
try {
|
|
1784
|
+
const response = await this.httpClient.postExecution(
|
|
1785
|
+
"/execute_async",
|
|
1786
|
+
requestData
|
|
1787
|
+
);
|
|
1788
|
+
if (response.session_id) {
|
|
1789
|
+
this._sessionId = response.session_id;
|
|
1790
|
+
}
|
|
1791
|
+
this._vmUsed = true;
|
|
1792
|
+
return {
|
|
1793
|
+
taskId: response.task_id,
|
|
1794
|
+
status: response.status || "pending",
|
|
1795
|
+
output: response.stdout || response.output,
|
|
1796
|
+
success: response.success,
|
|
1797
|
+
executionTime: response.execution_time,
|
|
1798
|
+
sessionId: response.session_id,
|
|
1799
|
+
error: response.error
|
|
1800
|
+
};
|
|
1801
|
+
} catch (error) {
|
|
1802
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1803
|
+
throw new ExecutionError(
|
|
1804
|
+
`Async code execution failed: ${errorMessage}`,
|
|
1805
|
+
void 0,
|
|
1806
|
+
void 0,
|
|
1807
|
+
{ cause: error }
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Poll for async task result
|
|
1813
|
+
*
|
|
1814
|
+
* @param taskId - The task ID from executeAsync
|
|
1815
|
+
* @param pollInterval - Seconds between polls (default: 1)
|
|
1816
|
+
* @param timeout - Maximum seconds to wait (default: 60)
|
|
1817
|
+
* @returns Task result with stdout, stderr, execution time, etc.
|
|
1818
|
+
* @throws Error if task doesn't complete within timeout
|
|
1819
|
+
*/
|
|
1820
|
+
async getTaskResult(taskId, pollInterval = 1, timeout = 60) {
|
|
1821
|
+
this.ensureNotLocal("Async task result retrieval");
|
|
1822
|
+
const startTime = Date.now();
|
|
1823
|
+
while (Date.now() - startTime < timeout * 1e3) {
|
|
1824
|
+
try {
|
|
1825
|
+
const response = await this.httpClient.get(
|
|
1826
|
+
`/v1/executions/${taskId}`,
|
|
1827
|
+
void 0,
|
|
1828
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1829
|
+
);
|
|
1830
|
+
if (response.is_complete) {
|
|
1831
|
+
return {
|
|
1832
|
+
stdout: response.serialized_stdout || "",
|
|
1833
|
+
stderr: response.serialized_stderr || "",
|
|
1834
|
+
cpuTime: response.cpu_time,
|
|
1835
|
+
executionTime: response.execution_time,
|
|
1836
|
+
createdAt: response.created_at
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval * 1e3));
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
if (error instanceof AuthenticationError || error instanceof RateLimitError || error instanceof NetworkError) {
|
|
1842
|
+
throw error;
|
|
1843
|
+
}
|
|
1844
|
+
if (Date.now() - startTime >= timeout * 1e3) {
|
|
1845
|
+
throw new ExecutionError(
|
|
1846
|
+
`Failed to get task result: ${error instanceof Error ? error.message : String(error)}`
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1849
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval * 1e3));
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
throw new ExecutionError(`Task ${taskId} timed out after ${timeout}s`);
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Upload files to the execution environment
|
|
1856
|
+
*/
|
|
1857
|
+
async upload(files, options = {}) {
|
|
1858
|
+
this.ensureNotLocal("File upload");
|
|
1859
|
+
const sessionId = options.sessionId || this._sessionId;
|
|
1860
|
+
if (!sessionId) {
|
|
1861
|
+
throw new SessionError("Session ID not set. Please create a session first using createSession().");
|
|
1862
|
+
}
|
|
1863
|
+
const formData = new import_form_data2.default();
|
|
1864
|
+
for (const file of files) {
|
|
1865
|
+
if (Buffer.isBuffer(file.content)) {
|
|
1866
|
+
formData.append("files", file.content, file.name);
|
|
1867
|
+
} else {
|
|
1868
|
+
formData.append("files", Buffer.from(file.content), file.name);
|
|
1869
|
+
}
|
|
1870
|
+
if (file.path) {
|
|
1871
|
+
formData.append("paths", file.path);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
formData.append("session_id", sessionId);
|
|
1875
|
+
if (options.remotePath) {
|
|
1876
|
+
formData.append("remote_path", options.remotePath);
|
|
1877
|
+
}
|
|
1878
|
+
if (options.recursive !== void 0) {
|
|
1879
|
+
formData.append("recursive", String(options.recursive));
|
|
1880
|
+
}
|
|
1881
|
+
try {
|
|
1882
|
+
const response = await this.httpClient.postFormData(
|
|
1883
|
+
"/upload",
|
|
1884
|
+
formData
|
|
1885
|
+
);
|
|
1886
|
+
return {
|
|
1887
|
+
success: response.success !== false,
|
|
1888
|
+
files: response.files || [],
|
|
1889
|
+
message: response.message
|
|
1890
|
+
};
|
|
1891
|
+
} catch (error) {
|
|
1892
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1893
|
+
throw new SessionError(
|
|
1894
|
+
`File upload failed: ${errorMessage}`,
|
|
1895
|
+
{ cause: error }
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* Create a new execution session
|
|
1901
|
+
*/
|
|
1902
|
+
async createSession() {
|
|
1903
|
+
this.ensureNotLocal("Session management");
|
|
1904
|
+
try {
|
|
1905
|
+
const requestBody = {
|
|
1906
|
+
api_key: this.httpClient.apiKey,
|
|
1907
|
+
vm_lifetime_seconds: Math.floor(this.timeout / 1e3)
|
|
1908
|
+
};
|
|
1909
|
+
if (this.memory_mb !== void 0) {
|
|
1910
|
+
requestBody.memory_mb = this.memory_mb;
|
|
1911
|
+
}
|
|
1912
|
+
if (this.cpu_count !== void 0) {
|
|
1913
|
+
requestBody.vcpu_count = this.cpu_count;
|
|
1914
|
+
}
|
|
1915
|
+
if (this.metadata !== void 0) {
|
|
1916
|
+
requestBody.metadata = this.metadata;
|
|
1917
|
+
}
|
|
1918
|
+
if (this.env !== void 0) {
|
|
1919
|
+
requestBody.env = this.env;
|
|
1920
|
+
}
|
|
1921
|
+
const response = await this.httpClient.post(
|
|
1922
|
+
"/v1/sessions/session",
|
|
1923
|
+
requestBody
|
|
1924
|
+
);
|
|
1925
|
+
if (response.session_id) {
|
|
1926
|
+
this._sessionId = response.session_id;
|
|
1927
|
+
return response.session_id;
|
|
1928
|
+
}
|
|
1929
|
+
throw new SessionError("Session creation failed: No session ID returned");
|
|
1930
|
+
} catch (error) {
|
|
1931
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1932
|
+
throw new SessionError(
|
|
1933
|
+
`Session creation failed: ${errorMessage}`,
|
|
1934
|
+
{ cause: error }
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1939
|
+
* Close a session
|
|
1940
|
+
* Note: Sessions auto-expire on the server side. This just clears local state.
|
|
1941
|
+
*/
|
|
1942
|
+
async closeSession(sessionId) {
|
|
1943
|
+
const targetSessionId = sessionId || this._sessionId;
|
|
1944
|
+
if (!targetSessionId) {
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
if (targetSessionId === this._sessionId) {
|
|
1948
|
+
this._sessionId = null;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Check if current session is still active by checking VM status
|
|
1953
|
+
*
|
|
1954
|
+
* @returns true if session is active, false otherwise
|
|
1955
|
+
*/
|
|
1956
|
+
async isSessionActive(sessionId) {
|
|
1957
|
+
const targetSessionId = sessionId || this._sessionId;
|
|
1958
|
+
if (!targetSessionId) {
|
|
1959
|
+
return false;
|
|
1960
|
+
}
|
|
1961
|
+
try {
|
|
1962
|
+
const response = await this.httpClient.get(
|
|
1963
|
+
`/v1/sessions/status/${targetSessionId}`,
|
|
1964
|
+
void 0,
|
|
1965
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1966
|
+
);
|
|
1967
|
+
return response.vm_status === "active";
|
|
1968
|
+
} catch (error) {
|
|
1969
|
+
if (error instanceof SessionError || error instanceof AuthenticationError || error instanceof NetworkError) {
|
|
1970
|
+
return false;
|
|
1971
|
+
}
|
|
1972
|
+
return false;
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Get the app URL for a session
|
|
1977
|
+
*
|
|
1978
|
+
* @param sessionId - Session ID (uses current session if not provided)
|
|
1979
|
+
* @param port - Port to expose (1-65535, default: 80)
|
|
1980
|
+
*/
|
|
1981
|
+
async getSessionAppUrl(sessionId, port) {
|
|
1982
|
+
this.ensureNotLocal("Session app URL");
|
|
1983
|
+
const targetSessionId = sessionId || this._sessionId;
|
|
1984
|
+
if (!targetSessionId) {
|
|
1985
|
+
throw new SessionError("Session ID not set. Please create a session first.");
|
|
1986
|
+
}
|
|
1987
|
+
const params = {};
|
|
1988
|
+
if (port !== void 0) {
|
|
1989
|
+
params.port = port;
|
|
1990
|
+
}
|
|
1991
|
+
return this.httpClient.get(
|
|
1992
|
+
`/v1/sessions/app-url/${targetSessionId}`,
|
|
1993
|
+
params,
|
|
1994
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* List sandbox records with optional filtering
|
|
1999
|
+
*
|
|
2000
|
+
* @param options.metadata - JSON-serializable metadata filters
|
|
2001
|
+
* @param options.limit - Maximum number of results (1-1000, default: 100)
|
|
2002
|
+
*/
|
|
2003
|
+
async listSandboxes(options = {}) {
|
|
2004
|
+
this.ensureNotLocal("Sandbox listing");
|
|
2005
|
+
const params = {};
|
|
2006
|
+
if (options.metadata !== void 0) {
|
|
2007
|
+
params.metadata = JSON.stringify(options.metadata);
|
|
2008
|
+
}
|
|
2009
|
+
if (options.limit !== void 0) {
|
|
2010
|
+
params.limit = options.limit;
|
|
2011
|
+
}
|
|
2012
|
+
return this.httpClient.get(
|
|
2013
|
+
"/v1/sessions/sandboxes",
|
|
2014
|
+
params,
|
|
2015
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Get usage statistics for a session
|
|
2020
|
+
*/
|
|
2021
|
+
async getUsage(sessionId) {
|
|
2022
|
+
this.ensureNotLocal("Usage tracking");
|
|
2023
|
+
const targetSessionId = sessionId || this._sessionId;
|
|
2024
|
+
if (!targetSessionId) {
|
|
2025
|
+
throw new SessionError("No active session to get usage for");
|
|
2026
|
+
}
|
|
2027
|
+
try {
|
|
2028
|
+
const response = await this.httpClient.get(
|
|
2029
|
+
`/v1/sessions/usage/${targetSessionId}`,
|
|
2030
|
+
void 0,
|
|
2031
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
2032
|
+
);
|
|
2033
|
+
return {
|
|
2034
|
+
sessionsUsed: response.sessions_used || 0,
|
|
2035
|
+
executionTime: response.execution_time || 0,
|
|
2036
|
+
quotaRemaining: response.quota_remaining || 0,
|
|
2037
|
+
quotaLimit: response.quota_limit || 0
|
|
2038
|
+
};
|
|
2039
|
+
} catch (error) {
|
|
2040
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2041
|
+
throw new SessionError(
|
|
2042
|
+
`Failed to get usage stats: ${errorMessage}`,
|
|
2043
|
+
{ cause: error }
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Get the current user profile.
|
|
2049
|
+
*/
|
|
2050
|
+
async getCurrentUser() {
|
|
2051
|
+
this.ensureNotLocal("User profile lookup");
|
|
2052
|
+
return this.httpClient.get(
|
|
2053
|
+
"/v1/me",
|
|
2054
|
+
void 0,
|
|
2055
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* Get the current status of a session.
|
|
2060
|
+
*/
|
|
2061
|
+
async getSessionStatus(sessionId) {
|
|
2062
|
+
this.ensureNotLocal("Session management");
|
|
2063
|
+
const targetSessionId = sessionId || this._sessionId;
|
|
2064
|
+
if (!targetSessionId) {
|
|
2065
|
+
throw new SessionError("Session ID not set. Please create a session first.");
|
|
2066
|
+
}
|
|
2067
|
+
return this.httpClient.get(
|
|
2068
|
+
`/v1/sessions/status/${targetSessionId}`,
|
|
2069
|
+
void 0,
|
|
2070
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* Download a file from the remote VM
|
|
2075
|
+
*/
|
|
2076
|
+
async download(filename, options = {}) {
|
|
2077
|
+
this.ensureNotLocal("File download");
|
|
2078
|
+
const targetSessionId = options.sessionId || this._sessionId;
|
|
2079
|
+
if (!targetSessionId) {
|
|
2080
|
+
throw new SessionError("No active session to download from");
|
|
2081
|
+
}
|
|
2082
|
+
try {
|
|
2083
|
+
const response = await this.httpClient.postFormUrlEncoded("/download", {
|
|
2084
|
+
filename,
|
|
2085
|
+
session_id: targetSessionId
|
|
2086
|
+
});
|
|
2087
|
+
const encodedContent = response.content || "";
|
|
2088
|
+
const content = encodedContent ? Buffer.from(encodedContent, "base64") : Buffer.from(response);
|
|
2089
|
+
return {
|
|
2090
|
+
success: true,
|
|
2091
|
+
filename,
|
|
2092
|
+
content,
|
|
2093
|
+
size: content.length
|
|
2094
|
+
};
|
|
2095
|
+
} catch (error) {
|
|
2096
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2097
|
+
throw new SessionError(
|
|
2098
|
+
`File download failed: ${errorMessage}`,
|
|
2099
|
+
{ cause: error }
|
|
2100
|
+
);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
/**
|
|
2104
|
+
* Get the current session ID
|
|
2105
|
+
*/
|
|
2106
|
+
get sessionId() {
|
|
2107
|
+
return this._sessionId;
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Kill the VM associated with a session
|
|
2111
|
+
*
|
|
2112
|
+
* @param sessionId - The session UUID whose VM should be killed. If not provided, uses current sessionId
|
|
2113
|
+
* @returns Result containing success message and killed VM ID
|
|
2114
|
+
*/
|
|
2115
|
+
async kill(sessionId) {
|
|
2116
|
+
this.ensureNotLocal("VM kill operation");
|
|
2117
|
+
const targetSessionId = sessionId || this._sessionId;
|
|
2118
|
+
if (!targetSessionId) {
|
|
2119
|
+
throw new SessionError("Session ID not set. Please provide a sessionId or start a session first.");
|
|
2120
|
+
}
|
|
2121
|
+
try {
|
|
2122
|
+
const response = await this.httpClient.post(
|
|
2123
|
+
"/kill",
|
|
2124
|
+
{ session_id: targetSessionId },
|
|
2125
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
2126
|
+
);
|
|
2127
|
+
if (!sessionId && this._sessionId === targetSessionId) {
|
|
2128
|
+
this._sessionId = null;
|
|
2129
|
+
}
|
|
2130
|
+
return response;
|
|
2131
|
+
} catch (error) {
|
|
2132
|
+
if (error instanceof Error && "statusCode" in error) {
|
|
2133
|
+
const statusCode = error.statusCode;
|
|
2134
|
+
if (statusCode === 403) {
|
|
2135
|
+
throw new AuthenticationError("You don't own this session", { cause: error });
|
|
2136
|
+
}
|
|
2137
|
+
if (statusCode === 404) {
|
|
2138
|
+
throw new SessionError("Session not found or no VM assigned to session (has execute() been called?)", { cause: error });
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2142
|
+
throw new SessionError(
|
|
2143
|
+
`Failed to kill VM: ${errorMessage}`,
|
|
2144
|
+
{ cause: error }
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
// --- Egress Policy Methods ---
|
|
2149
|
+
/**
|
|
2150
|
+
* Set egress policy for a session
|
|
2151
|
+
*/
|
|
2152
|
+
async setSessionEgress(options = {}, sessionId) {
|
|
2153
|
+
this.ensureNotLocal("Egress policy management");
|
|
2154
|
+
const targetSessionId = sessionId || this._sessionId;
|
|
2155
|
+
if (!targetSessionId) {
|
|
2156
|
+
throw new SessionError("Session ID not set. Please create a session first.");
|
|
2157
|
+
}
|
|
2158
|
+
const response = await this.httpClient.post(
|
|
2159
|
+
`/v1/egress/session/${targetSessionId}`,
|
|
2160
|
+
{
|
|
2161
|
+
allow_public_repos: options.allowPackageManagers ?? true,
|
|
2162
|
+
allow_http: options.allowHttp ?? true,
|
|
2163
|
+
allow_https: options.allowHttps ?? true,
|
|
2164
|
+
allowed_domains: options.allowedDomains ?? [],
|
|
2165
|
+
allowed_cidrs: options.allowedCidrs ?? []
|
|
2166
|
+
},
|
|
2167
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
2168
|
+
);
|
|
2169
|
+
return response;
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Get egress policy for a session
|
|
2173
|
+
*/
|
|
2174
|
+
async getSessionEgress(sessionId) {
|
|
2175
|
+
this.ensureNotLocal("Egress policy management");
|
|
2176
|
+
const targetSessionId = sessionId || this._sessionId;
|
|
2177
|
+
if (!targetSessionId) {
|
|
2178
|
+
throw new SessionError("Session ID not set. Please create a session first.");
|
|
2179
|
+
}
|
|
2180
|
+
const response = await this.httpClient.get(
|
|
2181
|
+
`/v1/egress/session/${targetSessionId}`,
|
|
2182
|
+
void 0,
|
|
2183
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
2184
|
+
);
|
|
2185
|
+
return {
|
|
2186
|
+
allowPackageManagers: response.allow_public_repos,
|
|
2187
|
+
allowHttp: response.allow_http,
|
|
2188
|
+
allowHttps: response.allow_https,
|
|
2189
|
+
allowedDomains: response.allowed_domains || [],
|
|
2190
|
+
allowedCidrs: response.allowed_cidrs || []
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Set egress policy for a specific VM
|
|
2195
|
+
*/
|
|
2196
|
+
async setVmEgress(vmId, options = {}) {
|
|
2197
|
+
this.ensureNotLocal("Egress policy management");
|
|
2198
|
+
const response = await this.httpClient.post(
|
|
2199
|
+
`/v1/egress/vm/${vmId}`,
|
|
2200
|
+
{
|
|
2201
|
+
allow_public_repos: options.allowPackageManagers ?? true,
|
|
2202
|
+
allow_http: options.allowHttp ?? true,
|
|
2203
|
+
allow_https: options.allowHttps ?? true,
|
|
2204
|
+
allowed_domains: options.allowedDomains ?? [],
|
|
2205
|
+
allowed_cidrs: options.allowedCidrs ?? []
|
|
2206
|
+
},
|
|
2207
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
2208
|
+
);
|
|
2209
|
+
return response;
|
|
2210
|
+
}
|
|
2211
|
+
/**
|
|
2212
|
+
* Get egress policy for a specific VM
|
|
2213
|
+
*/
|
|
2214
|
+
async getVmEgress(vmId) {
|
|
2215
|
+
this.ensureNotLocal("Egress policy management");
|
|
2216
|
+
const response = await this.httpClient.get(
|
|
2217
|
+
`/v1/egress/vm/${vmId}`,
|
|
2218
|
+
void 0,
|
|
2219
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
2220
|
+
);
|
|
2221
|
+
return {
|
|
2222
|
+
allowPackageManagers: response.allow_public_repos,
|
|
2223
|
+
allowHttp: response.allow_http,
|
|
2224
|
+
allowHttps: response.allow_https,
|
|
2225
|
+
allowedDomains: response.allowed_domains || [],
|
|
2226
|
+
allowedCidrs: response.allowed_cidrs || []
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
// --- SSH Key Methods ---
|
|
2230
|
+
/**
|
|
2231
|
+
* Add an SSH public key
|
|
2232
|
+
*/
|
|
2233
|
+
async addSshKey(publicKey) {
|
|
2234
|
+
this.ensureNotLocal("SSH key management");
|
|
2235
|
+
const response = await this.httpClient.post(
|
|
2236
|
+
"/v1/ssh-keys",
|
|
2237
|
+
{ public_key: publicKey },
|
|
2238
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
2239
|
+
);
|
|
2240
|
+
return {
|
|
2241
|
+
id: response.id,
|
|
2242
|
+
fingerprint: response.fingerprint,
|
|
2243
|
+
keyType: response.key_type,
|
|
2244
|
+
comment: response.comment ?? null,
|
|
2245
|
+
createdAt: response.created_at
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
/**
|
|
2249
|
+
* List all active SSH keys
|
|
2250
|
+
*/
|
|
2251
|
+
async listSshKeys() {
|
|
2252
|
+
this.ensureNotLocal("SSH key management");
|
|
2253
|
+
const response = await this.httpClient.get(
|
|
2254
|
+
"/v1/ssh-keys",
|
|
2255
|
+
void 0,
|
|
2256
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
2257
|
+
);
|
|
2258
|
+
const keys = response.keys || [];
|
|
2259
|
+
return keys.map((k) => ({
|
|
2260
|
+
id: k.id,
|
|
2261
|
+
fingerprint: k.fingerprint,
|
|
2262
|
+
keyType: k.key_type,
|
|
2263
|
+
comment: k.comment ?? null,
|
|
2264
|
+
createdAt: k.created_at
|
|
2265
|
+
}));
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* Delete an SSH key by ID
|
|
2269
|
+
*/
|
|
2270
|
+
async deleteSshKey(keyId) {
|
|
2271
|
+
this.ensureNotLocal("SSH key management");
|
|
2272
|
+
const response = await this.httpClient.delete(
|
|
2273
|
+
`/v1/ssh-keys/${keyId}`,
|
|
2274
|
+
{ "X-API-Key": this.httpClient.apiKey }
|
|
2275
|
+
);
|
|
2276
|
+
return response;
|
|
2277
|
+
}
|
|
2278
|
+
/**
|
|
2279
|
+
* Clean up resources
|
|
2280
|
+
*/
|
|
2281
|
+
async dispose() {
|
|
2282
|
+
if (this._sessionId && !this.local && this._vmUsed) {
|
|
2283
|
+
try {
|
|
2284
|
+
await this.kill();
|
|
2285
|
+
} catch (e) {
|
|
2286
|
+
}
|
|
2287
|
+
} else if (this._sessionId) {
|
|
2288
|
+
await this.closeSession();
|
|
2289
|
+
}
|
|
2290
|
+
await this.browser.dispose();
|
|
2291
|
+
}
|
|
2292
|
+
};
|
|
2293
|
+
|
|
2294
|
+
// src/cli/config.ts
|
|
2295
|
+
var import_fs2 = __toESM(require("fs"));
|
|
2296
|
+
var import_os = __toESM(require("os"));
|
|
2297
|
+
var import_path11 = __toESM(require("path"));
|
|
2298
|
+
var import_url = require("url");
|
|
2299
|
+
var CONFIG_DIRNAME = ".instavm";
|
|
2300
|
+
var CONFIG_FILENAME = "config.json";
|
|
2301
|
+
var SCHEMA_VERSION = 1;
|
|
2302
|
+
var DEFAULT_BASE_URL = "https://api.instavm.io";
|
|
2303
|
+
var DEFAULT_DOCS_URL = "https://docs.instavm.io";
|
|
2304
|
+
var DEFAULT_BILLING_URL = "https://dashboard.instavm.io/billing";
|
|
2305
|
+
var DEFAULT_SSH_HOST = "instavm.dev";
|
|
2306
|
+
var ConfigError = class extends Error {
|
|
2307
|
+
};
|
|
2308
|
+
function getConfigDir(homeDir = import_os.default.homedir()) {
|
|
2309
|
+
return import_path11.default.join(homeDir, CONFIG_DIRNAME);
|
|
2310
|
+
}
|
|
2311
|
+
function getConfigPath(homeDir = import_os.default.homedir()) {
|
|
2312
|
+
return import_path11.default.join(getConfigDir(homeDir), CONFIG_FILENAME);
|
|
2313
|
+
}
|
|
2314
|
+
function defaultConfig() {
|
|
2315
|
+
return {
|
|
2316
|
+
schema_version: SCHEMA_VERSION,
|
|
2317
|
+
active_profile: "default",
|
|
2318
|
+
profiles: {
|
|
2319
|
+
default: {
|
|
2320
|
+
auth: {
|
|
2321
|
+
type: "api_key",
|
|
2322
|
+
api_key: null
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
function normalizeProfile(profile) {
|
|
2329
|
+
const normalized = { ...profile };
|
|
2330
|
+
const auth = typeof profile.auth === "object" && profile.auth !== null ? { ...profile.auth } : {};
|
|
2331
|
+
auth.type = String(auth.type || "api_key").trim() || "api_key";
|
|
2332
|
+
auth.api_key = auth.api_key ? String(auth.api_key).trim() : null;
|
|
2333
|
+
normalized.auth = auth;
|
|
2334
|
+
for (const field of ["base_url", "ssh_host"]) {
|
|
2335
|
+
const rawValue = normalized[field];
|
|
2336
|
+
if (typeof rawValue !== "string") {
|
|
2337
|
+
delete normalized[field];
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2340
|
+
const cleaned = rawValue.trim();
|
|
2341
|
+
if (cleaned) {
|
|
2342
|
+
normalized[field] = cleaned;
|
|
2343
|
+
} else {
|
|
2344
|
+
delete normalized[field];
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
return normalized;
|
|
2348
|
+
}
|
|
2349
|
+
function normalizeConfig(rawConfig) {
|
|
2350
|
+
const config = defaultConfig();
|
|
2351
|
+
if (!rawConfig || typeof rawConfig !== "object") {
|
|
2352
|
+
return config;
|
|
2353
|
+
}
|
|
2354
|
+
if (Number.isInteger(rawConfig.schema_version)) {
|
|
2355
|
+
config.schema_version = Number(rawConfig.schema_version);
|
|
2356
|
+
}
|
|
2357
|
+
if (rawConfig.profiles && typeof rawConfig.profiles === "object") {
|
|
2358
|
+
config.profiles = {};
|
|
2359
|
+
for (const [name, profile] of Object.entries(rawConfig.profiles)) {
|
|
2360
|
+
if (typeof name === "string" && profile && typeof profile === "object") {
|
|
2361
|
+
config.profiles[name] = normalizeProfile(profile);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
if (!config.profiles.default) {
|
|
2366
|
+
config.profiles.default = defaultConfig().profiles.default;
|
|
2367
|
+
}
|
|
2368
|
+
if (typeof rawConfig.active_profile === "string" && Object.prototype.hasOwnProperty.call(config.profiles, rawConfig.active_profile)) {
|
|
2369
|
+
config.active_profile = rawConfig.active_profile;
|
|
2370
|
+
}
|
|
2371
|
+
return config;
|
|
2372
|
+
}
|
|
2373
|
+
function loadConfig(configPath = getConfigPath()) {
|
|
2374
|
+
if (!import_fs2.default.existsSync(configPath)) {
|
|
2375
|
+
return defaultConfig();
|
|
2376
|
+
}
|
|
2377
|
+
try {
|
|
2378
|
+
const raw = import_fs2.default.readFileSync(configPath, "utf8");
|
|
2379
|
+
return normalizeConfig(JSON.parse(raw));
|
|
2380
|
+
} catch (error) {
|
|
2381
|
+
throw new ConfigError(`Unable to load CLI config from ${configPath}`);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
function applyPosixMode(targetPath, mode) {
|
|
2385
|
+
if (process.platform !== "win32") {
|
|
2386
|
+
import_fs2.default.chmodSync(targetPath, mode);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
function saveConfig(config, configPath = getConfigPath()) {
|
|
2390
|
+
const configDir = import_path11.default.dirname(configPath);
|
|
2391
|
+
try {
|
|
2392
|
+
import_fs2.default.mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
2393
|
+
applyPosixMode(configDir, 448);
|
|
2394
|
+
const normalized = normalizeConfig(config);
|
|
2395
|
+
const tempPath = import_path11.default.join(configDir, `.${import_path11.default.basename(configPath)}.${process.pid}.${Date.now()}.tmp`);
|
|
2396
|
+
import_fs2.default.writeFileSync(tempPath, `${JSON.stringify(normalized, null, 2)}
|
|
2397
|
+
`, "utf8");
|
|
2398
|
+
applyPosixMode(tempPath, 384);
|
|
2399
|
+
import_fs2.default.renameSync(tempPath, configPath);
|
|
2400
|
+
applyPosixMode(configPath, 384);
|
|
2401
|
+
} catch (error) {
|
|
2402
|
+
throw new ConfigError(`Unable to write CLI config to ${configPath}`);
|
|
2403
|
+
}
|
|
2404
|
+
return configPath;
|
|
2405
|
+
}
|
|
2406
|
+
function getActiveProfile(config) {
|
|
2407
|
+
return config.profiles[config.active_profile] || config.profiles.default;
|
|
2408
|
+
}
|
|
2409
|
+
function redactSecret(secret) {
|
|
2410
|
+
if (!secret) {
|
|
2411
|
+
return null;
|
|
2412
|
+
}
|
|
2413
|
+
const trimmed = secret.trim();
|
|
2414
|
+
if (trimmed.length <= 8) {
|
|
2415
|
+
return "*".repeat(trimmed.length);
|
|
2416
|
+
}
|
|
2417
|
+
return `${trimmed.slice(0, 8)}...${trimmed.slice(-4)}`;
|
|
2418
|
+
}
|
|
2419
|
+
function deriveSshHost(baseURL) {
|
|
2420
|
+
try {
|
|
2421
|
+
const parsed = new import_url.URL(baseURL);
|
|
2422
|
+
const host = parsed.hostname.trim().toLowerCase();
|
|
2423
|
+
if (!host) {
|
|
2424
|
+
return DEFAULT_SSH_HOST;
|
|
2425
|
+
}
|
|
2426
|
+
if (host === "localhost" || host === "127.0.0.1") {
|
|
2427
|
+
return host;
|
|
2428
|
+
}
|
|
2429
|
+
if (host.includes("staging") && host.includes("instavm")) {
|
|
2430
|
+
return "staging.instavm.dev";
|
|
2431
|
+
}
|
|
2432
|
+
if (host.startsWith("api.") && host.includes("instavm")) {
|
|
2433
|
+
return DEFAULT_SSH_HOST;
|
|
2434
|
+
}
|
|
2435
|
+
if (host.includes("instavm")) {
|
|
2436
|
+
return DEFAULT_SSH_HOST;
|
|
2437
|
+
}
|
|
2438
|
+
return host;
|
|
2439
|
+
} catch (error) {
|
|
2440
|
+
return DEFAULT_SSH_HOST;
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
function resolveRuntimeConfig(options = {}) {
|
|
2444
|
+
const env = options.env || process.env;
|
|
2445
|
+
const configPath = options.configPath || getConfigPath();
|
|
2446
|
+
const config = normalizeConfig(options.config || loadConfig(configPath));
|
|
2447
|
+
const profile = getActiveProfile(config);
|
|
2448
|
+
const storedKey = profile.auth?.api_key ? String(profile.auth.api_key).trim() : void 0;
|
|
2449
|
+
let apiKey;
|
|
2450
|
+
let apiKeySource = "missing";
|
|
2451
|
+
if (options.apiKey) {
|
|
2452
|
+
apiKey = options.apiKey.trim();
|
|
2453
|
+
apiKeySource = "flag";
|
|
2454
|
+
} else if (env.INSTAVM_API_KEY) {
|
|
2455
|
+
apiKey = env.INSTAVM_API_KEY.trim();
|
|
2456
|
+
apiKeySource = "env";
|
|
2457
|
+
} else if (storedKey) {
|
|
2458
|
+
apiKey = storedKey;
|
|
2459
|
+
apiKeySource = "config";
|
|
2460
|
+
}
|
|
2461
|
+
let baseURL = DEFAULT_BASE_URL;
|
|
2462
|
+
let baseURLSource = "default";
|
|
2463
|
+
if (options.baseURL) {
|
|
2464
|
+
baseURL = options.baseURL.trim();
|
|
2465
|
+
baseURLSource = "flag";
|
|
2466
|
+
} else if (env.INSTAVM_BASE_URL) {
|
|
2467
|
+
baseURL = env.INSTAVM_BASE_URL.trim();
|
|
2468
|
+
baseURLSource = "env";
|
|
2469
|
+
} else if (profile.base_url) {
|
|
2470
|
+
baseURL = String(profile.base_url).trim();
|
|
2471
|
+
baseURLSource = "config";
|
|
2472
|
+
}
|
|
2473
|
+
let sshHost = deriveSshHost(baseURL);
|
|
2474
|
+
let sshHostSource = "derived";
|
|
2475
|
+
if (options.sshHost) {
|
|
2476
|
+
sshHost = options.sshHost.trim();
|
|
2477
|
+
sshHostSource = "flag";
|
|
2478
|
+
} else if (env.INSTAVM_SSH_HOST) {
|
|
2479
|
+
sshHost = env.INSTAVM_SSH_HOST.trim();
|
|
2480
|
+
sshHostSource = "env";
|
|
2481
|
+
} else if (profile.ssh_host) {
|
|
2482
|
+
sshHost = String(profile.ssh_host).trim();
|
|
2483
|
+
sshHostSource = "config";
|
|
2484
|
+
}
|
|
2485
|
+
return {
|
|
2486
|
+
apiKey,
|
|
2487
|
+
baseURL,
|
|
2488
|
+
sshHost,
|
|
2489
|
+
apiKeySource,
|
|
2490
|
+
baseURLSource,
|
|
2491
|
+
sshHostSource,
|
|
2492
|
+
configPath,
|
|
2493
|
+
config
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
function updateProfileSettings(options) {
|
|
2497
|
+
const configPath = options.configPath || getConfigPath();
|
|
2498
|
+
const config = loadConfig(configPath);
|
|
2499
|
+
const profileName = config.active_profile || "default";
|
|
2500
|
+
const existing = config.profiles[profileName] || { auth: { type: "api_key", api_key: null } };
|
|
2501
|
+
const profile = normalizeProfile(existing);
|
|
2502
|
+
const auth = { ...profile.auth || {} };
|
|
2503
|
+
auth.type = String(auth.type || "api_key");
|
|
2504
|
+
if (options.clearApiKey) {
|
|
2505
|
+
auth.api_key = null;
|
|
2506
|
+
} else if (options.apiKey !== void 0) {
|
|
2507
|
+
auth.api_key = options.apiKey.trim() || null;
|
|
2508
|
+
}
|
|
2509
|
+
if (options.baseURL !== void 0) {
|
|
2510
|
+
const cleaned = options.baseURL.trim();
|
|
2511
|
+
if (cleaned) {
|
|
2512
|
+
profile.base_url = cleaned;
|
|
2513
|
+
} else {
|
|
2514
|
+
delete profile.base_url;
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
if (options.sshHost !== void 0) {
|
|
2518
|
+
const cleaned = options.sshHost.trim();
|
|
2519
|
+
if (cleaned) {
|
|
2520
|
+
profile.ssh_host = cleaned;
|
|
2521
|
+
} else {
|
|
2522
|
+
delete profile.ssh_host;
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
profile.auth = auth;
|
|
2526
|
+
config.profiles[profileName] = profile;
|
|
2527
|
+
return saveConfig(config, configPath);
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
// src/cli.ts
|
|
2531
|
+
var defaultDeps = {
|
|
2532
|
+
stdout: process.stdout,
|
|
2533
|
+
stderr: process.stderr,
|
|
2534
|
+
spawnSync: import_child_process.spawnSync,
|
|
2535
|
+
resolveRuntimeConfig,
|
|
2536
|
+
updateProfileSettings,
|
|
2537
|
+
clientFactory: (apiKey, options) => new InstaVM(apiKey, options),
|
|
2538
|
+
promptSecret,
|
|
2539
|
+
readStdin,
|
|
2540
|
+
writeFile: (outputPath, content) => {
|
|
2541
|
+
import_fs3.default.writeFileSync(outputPath, content);
|
|
2542
|
+
}
|
|
2543
|
+
};
|
|
2544
|
+
function printJson(deps, payload) {
|
|
2545
|
+
deps.stdout.write(`${JSON.stringify(payload)}
|
|
2546
|
+
`);
|
|
2547
|
+
}
|
|
2548
|
+
function printLines(deps, lines) {
|
|
2549
|
+
for (const line of lines) {
|
|
2550
|
+
deps.stdout.write(`${line}
|
|
2551
|
+
`);
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
function emitOutput(deps, options, payload, textLines) {
|
|
2555
|
+
if (options.json) {
|
|
2556
|
+
printJson(deps, payload);
|
|
2557
|
+
} else {
|
|
2558
|
+
printLines(deps, textLines);
|
|
2559
|
+
}
|
|
2560
|
+
return 0;
|
|
2561
|
+
}
|
|
2562
|
+
function addRuntimeOptions(command, options = {}) {
|
|
2563
|
+
command.option("--api-key <apiKey>", "Override the InstaVM API key for this command");
|
|
2564
|
+
command.option("--base-url <baseUrl>", "Override the InstaVM API base URL for this command");
|
|
2565
|
+
if (options.includeSshHost) {
|
|
2566
|
+
command.option("--ssh-host <sshHost>", "Override the SSH gateway host for this command");
|
|
2567
|
+
}
|
|
2568
|
+
if (options.includeJson !== false) {
|
|
2569
|
+
command.option("-j, --json", "Emit JSON output");
|
|
2570
|
+
}
|
|
2571
|
+
return command;
|
|
2572
|
+
}
|
|
2573
|
+
function parseSize(value) {
|
|
2574
|
+
const raw = value.trim().toLowerCase();
|
|
2575
|
+
if (!raw) {
|
|
2576
|
+
throw new Error("size is required");
|
|
2577
|
+
}
|
|
2578
|
+
const units = [
|
|
2579
|
+
["tb", 1024 ** 4],
|
|
2580
|
+
["gb", 1024 ** 3],
|
|
2581
|
+
["mb", 1024 ** 2],
|
|
2582
|
+
["kb", 1024],
|
|
2583
|
+
["b", 1]
|
|
2584
|
+
];
|
|
2585
|
+
for (const [suffix, multiplier] of units) {
|
|
2586
|
+
if (raw.endsWith(suffix) && raw !== suffix) {
|
|
2587
|
+
const number = raw.slice(0, -suffix.length).trim();
|
|
2588
|
+
return Math.floor(Number.parseFloat(number) * multiplier);
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
return Number.parseInt(raw, 10);
|
|
2592
|
+
}
|
|
2593
|
+
function humanBytes(value) {
|
|
2594
|
+
let amount = Number(value || 0);
|
|
2595
|
+
for (const unit of ["B", "KB", "MB", "GB", "TB"]) {
|
|
2596
|
+
if (amount < 1024 || unit === "TB") {
|
|
2597
|
+
if (unit === "B") {
|
|
2598
|
+
return `${Math.trunc(amount)}${unit}`;
|
|
2599
|
+
}
|
|
2600
|
+
return `${amount.toFixed(1)}${unit}`;
|
|
2601
|
+
}
|
|
2602
|
+
amount /= 1024;
|
|
2603
|
+
}
|
|
2604
|
+
return `${Math.trunc(Number(value || 0))}B`;
|
|
2605
|
+
}
|
|
2606
|
+
function parseEnvPairs(values) {
|
|
2607
|
+
const pairs = {};
|
|
2608
|
+
for (const entry of values || []) {
|
|
2609
|
+
const separatorIndex = entry.indexOf("=");
|
|
2610
|
+
if (separatorIndex < 1) {
|
|
2611
|
+
throw new Error(`Expected KEY=VALUE, got: ${entry}`);
|
|
2612
|
+
}
|
|
2613
|
+
const key = entry.slice(0, separatorIndex).trim();
|
|
2614
|
+
const value = entry.slice(separatorIndex + 1);
|
|
2615
|
+
if (!key) {
|
|
2616
|
+
throw new Error(`Invalid env key: ${entry}`);
|
|
2617
|
+
}
|
|
2618
|
+
pairs[key] = value;
|
|
2619
|
+
}
|
|
2620
|
+
return pairs;
|
|
2621
|
+
}
|
|
2622
|
+
function parseVolumeSpec(spec) {
|
|
2623
|
+
const parts = spec.split(":");
|
|
2624
|
+
if (parts.length < 2) {
|
|
2625
|
+
throw new Error("volume mounts must use <volume_id>:<mount_path>[:rw|ro[:checkpoint_id]]");
|
|
2626
|
+
}
|
|
2627
|
+
const volumeId = parts[0].trim();
|
|
2628
|
+
const mountPath = parts[1].trim();
|
|
2629
|
+
let mode = "rw";
|
|
2630
|
+
let checkpointId = null;
|
|
2631
|
+
if (parts[2]?.trim()) {
|
|
2632
|
+
mode = parts[2].trim().toLowerCase();
|
|
2633
|
+
}
|
|
2634
|
+
if (parts[3]?.trim()) {
|
|
2635
|
+
checkpointId = parts[3].trim();
|
|
2636
|
+
}
|
|
2637
|
+
if (parts.length > 4) {
|
|
2638
|
+
throw new Error("volume mounts must use <volume_id>:<mount_path>[:rw|ro[:checkpoint_id]]");
|
|
2639
|
+
}
|
|
2640
|
+
if (!["rw", "ro"].includes(mode)) {
|
|
2641
|
+
throw new Error("volume mount mode must be rw or ro");
|
|
2642
|
+
}
|
|
2643
|
+
if (mode === "rw" && checkpointId) {
|
|
2644
|
+
throw new Error("checkpoint_id is only allowed for ro mounts");
|
|
2645
|
+
}
|
|
2646
|
+
if (mode === "ro" && !checkpointId) {
|
|
2647
|
+
checkpointId = "latest";
|
|
2648
|
+
}
|
|
2649
|
+
return {
|
|
2650
|
+
volume_id: volumeId,
|
|
2651
|
+
mount_path: mountPath,
|
|
2652
|
+
mode,
|
|
2653
|
+
checkpoint_id: checkpointId
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
function looksLikeUuid(value) {
|
|
2657
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
2658
|
+
}
|
|
2659
|
+
function statusPayload(deps, options) {
|
|
2660
|
+
const settings = deps.resolveRuntimeConfig({
|
|
2661
|
+
apiKey: options.apiKey,
|
|
2662
|
+
baseURL: options.baseUrl,
|
|
2663
|
+
sshHost: options.sshHost
|
|
2664
|
+
});
|
|
2665
|
+
const profile = getActiveProfile(settings.config);
|
|
2666
|
+
const storedKey = profile.auth?.api_key ? String(profile.auth.api_key) : void 0;
|
|
2667
|
+
return {
|
|
2668
|
+
config_path: settings.configPath,
|
|
2669
|
+
active_profile: settings.config.active_profile,
|
|
2670
|
+
api_key: {
|
|
2671
|
+
configured: Boolean(settings.apiKey),
|
|
2672
|
+
redacted: redactSecret(settings.apiKey),
|
|
2673
|
+
source: settings.apiKeySource,
|
|
2674
|
+
stored: Boolean(storedKey)
|
|
2675
|
+
},
|
|
2676
|
+
base_url: {
|
|
2677
|
+
value: settings.baseURL,
|
|
2678
|
+
source: settings.baseURLSource
|
|
2679
|
+
},
|
|
2680
|
+
ssh_host: {
|
|
2681
|
+
value: settings.sshHost,
|
|
2682
|
+
source: settings.sshHostSource
|
|
2683
|
+
}
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
function statusText(payload) {
|
|
2687
|
+
const lines = [
|
|
2688
|
+
`Config path: ${payload.config_path}`,
|
|
2689
|
+
`Active profile: ${payload.active_profile}`,
|
|
2690
|
+
`API key: ${payload.api_key.redacted || "not configured"} (${payload.api_key.source})`,
|
|
2691
|
+
`Base URL: ${payload.base_url.value} (${payload.base_url.source})`,
|
|
2692
|
+
`SSH host: ${payload.ssh_host.value} (${payload.ssh_host.source})`
|
|
2693
|
+
];
|
|
2694
|
+
if (payload.api_key.stored) {
|
|
2695
|
+
lines.push("Stored key: present");
|
|
2696
|
+
}
|
|
2697
|
+
return lines;
|
|
2698
|
+
}
|
|
2699
|
+
function resolveSettings(deps, options) {
|
|
2700
|
+
return deps.resolveRuntimeConfig({
|
|
2701
|
+
apiKey: options.apiKey,
|
|
2702
|
+
baseURL: options.baseUrl,
|
|
2703
|
+
sshHost: options.sshHost
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
function requireClient(deps, options) {
|
|
2707
|
+
const settings = resolveSettings(deps, options);
|
|
2708
|
+
if (!settings.apiKey) {
|
|
2709
|
+
throw new Error("No InstaVM API key configured. Run `instavm auth set-key` or export INSTAVM_API_KEY.");
|
|
2710
|
+
}
|
|
2711
|
+
return {
|
|
2712
|
+
client: deps.clientFactory(settings.apiKey, { baseURL: settings.baseURL }),
|
|
2713
|
+
settings
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
async function resolveDesktopTarget(client, target) {
|
|
2717
|
+
if (looksLikeUuid(target)) {
|
|
2718
|
+
const status = await client.getSessionStatus(target);
|
|
2719
|
+
return { ...status, session_id: target };
|
|
2720
|
+
}
|
|
2721
|
+
const vm = await client.vms.get(target);
|
|
2722
|
+
const sessionId = vm.session_id ? String(vm.session_id) : null;
|
|
2723
|
+
if (sessionId) {
|
|
2724
|
+
const status = await client.getSessionStatus(sessionId);
|
|
2725
|
+
return {
|
|
2726
|
+
...status,
|
|
2727
|
+
session_id: sessionId,
|
|
2728
|
+
vm_id: status.vm_id || vm.vm_id
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
return {
|
|
2732
|
+
session_id: null,
|
|
2733
|
+
vm_id: vm.vm_id,
|
|
2734
|
+
vm_status: vm.status,
|
|
2735
|
+
vm_alive: vm.status === "active"
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2738
|
+
async function desktopPayload(client, target) {
|
|
2739
|
+
const payload = await resolveDesktopTarget(client, target);
|
|
2740
|
+
if (payload.vm_alive && payload.session_id) {
|
|
2741
|
+
try {
|
|
2742
|
+
Object.assign(payload, await client.computerUse.viewerUrl(String(payload.session_id)));
|
|
2743
|
+
} catch (error) {
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
return payload;
|
|
2747
|
+
}
|
|
2748
|
+
function createProgram(deps = defaultDeps) {
|
|
2749
|
+
const program = new import_commander.Command();
|
|
2750
|
+
program.name("instavm").description("InstaVM CLI for VM, snapshot, volume, desktop, identity, and sharing workflows.").showHelpAfterError().showSuggestionAfterError().configureOutput({
|
|
2751
|
+
writeOut: (str) => deps.stdout.write(str),
|
|
2752
|
+
writeErr: (str) => deps.stderr.write(str)
|
|
2753
|
+
}).addHelpCommand("help [command]", "Show help for the CLI or a subcommand").exitOverride();
|
|
2754
|
+
const auth = program.command("auth").description("Manage stored auth for the installed CLI");
|
|
2755
|
+
addRuntimeOptions(
|
|
2756
|
+
auth.command("set-key").description("Store an InstaVM API key in ~/.instavm/config.json from a hidden prompt or stdin").action(async (options) => {
|
|
2757
|
+
const apiKey = process.stdin.isTTY ? (await deps.promptSecret("InstaVM API key: ")).trim() : (await deps.readStdin()).trim();
|
|
2758
|
+
if (!apiKey) {
|
|
2759
|
+
throw new Error("API key is required. Enter it at the prompt or pipe it over stdin.");
|
|
2760
|
+
}
|
|
2761
|
+
const configPath = deps.updateProfileSettings({
|
|
2762
|
+
apiKey,
|
|
2763
|
+
baseURL: options.baseUrl,
|
|
2764
|
+
sshHost: options.sshHost
|
|
2765
|
+
});
|
|
2766
|
+
const payload = {
|
|
2767
|
+
status: "stored",
|
|
2768
|
+
config_path: configPath,
|
|
2769
|
+
api_key: redactSecret(apiKey),
|
|
2770
|
+
base_url: options.baseUrl || null,
|
|
2771
|
+
ssh_host: options.sshHost || null
|
|
2772
|
+
};
|
|
2773
|
+
emitOutput(
|
|
2774
|
+
deps,
|
|
2775
|
+
options,
|
|
2776
|
+
payload,
|
|
2777
|
+
[
|
|
2778
|
+
`Stored API key in ${configPath}`,
|
|
2779
|
+
`API key: ${payload.api_key}`,
|
|
2780
|
+
...options.baseUrl ? [`Default base URL: ${options.baseUrl}`] : [],
|
|
2781
|
+
...options.sshHost ? [`Default SSH host: ${options.sshHost}`] : []
|
|
2782
|
+
]
|
|
2783
|
+
);
|
|
2784
|
+
}),
|
|
2785
|
+
{ includeSshHost: true }
|
|
2786
|
+
);
|
|
2787
|
+
addRuntimeOptions(
|
|
2788
|
+
auth.command("status").description("Show the effective CLI auth and endpoint settings").action(async (options) => {
|
|
2789
|
+
const payload = statusPayload(deps, options);
|
|
2790
|
+
emitOutput(deps, options, payload, statusText(payload));
|
|
2791
|
+
}),
|
|
2792
|
+
{ includeSshHost: true }
|
|
2793
|
+
);
|
|
2794
|
+
addRuntimeOptions(
|
|
2795
|
+
auth.command("logout").description("Remove the stored API key from the local CLI config").action(async (options) => {
|
|
2796
|
+
const settings = resolveSettings(deps, options);
|
|
2797
|
+
const configPath = deps.updateProfileSettings({ clearApiKey: true });
|
|
2798
|
+
const payload = {
|
|
2799
|
+
status: "logged_out",
|
|
2800
|
+
config_path: configPath,
|
|
2801
|
+
env_key_active: settings.apiKeySource === "env"
|
|
2802
|
+
};
|
|
2803
|
+
emitOutput(
|
|
2804
|
+
deps,
|
|
2805
|
+
options,
|
|
2806
|
+
payload,
|
|
2807
|
+
[
|
|
2808
|
+
`Removed the stored API key from ${configPath}`,
|
|
2809
|
+
...payload.env_key_active ? ["INSTAVM_API_KEY is still set in the environment and will remain active."] : []
|
|
2810
|
+
]
|
|
2811
|
+
);
|
|
2812
|
+
}),
|
|
2813
|
+
{ includeSshHost: true }
|
|
2814
|
+
);
|
|
2815
|
+
addRuntimeOptions(
|
|
2816
|
+
program.command("whoami").description("Show the current account and SSH key registrations").action(async (options) => {
|
|
2817
|
+
const { client } = requireClient(deps, options);
|
|
2818
|
+
const [user, sshKeys] = await Promise.all([client.getCurrentUser(), client.listSshKeys()]);
|
|
2819
|
+
const payload = {
|
|
2820
|
+
id: user.id,
|
|
2821
|
+
email: user.email,
|
|
2822
|
+
name: user.name,
|
|
2823
|
+
is_verified: user.is_verified,
|
|
2824
|
+
ssh_keys: sshKeys
|
|
2825
|
+
};
|
|
2826
|
+
emitOutput(
|
|
2827
|
+
deps,
|
|
2828
|
+
options,
|
|
2829
|
+
payload,
|
|
2830
|
+
[
|
|
2831
|
+
`Email: ${payload.email || "-"}`,
|
|
2832
|
+
`User ID: ${payload.id}`,
|
|
2833
|
+
`Name: ${payload.name || "-"}`,
|
|
2834
|
+
`Verified: ${payload.is_verified ? "yes" : "no"}`,
|
|
2835
|
+
"SSH Keys:",
|
|
2836
|
+
...sshKeys.length ? sshKeys.map((key) => ` ${key.id} ${key.fingerprint} ${key.comment || ""}`.trimEnd()) : [" none"]
|
|
2837
|
+
]
|
|
2838
|
+
);
|
|
2839
|
+
})
|
|
2840
|
+
);
|
|
2841
|
+
addRuntimeOptions(
|
|
2842
|
+
program.command("create").alias("new").description("Create a new VM or desktop").option("--type <type>", "VM type", "standard").option("--timeout <seconds>", "VM lifetime in seconds").option("--memory <mb>", "Memory in MB").option("--vcpu <count>", "vCPU count").option("--snapshot <snapshotId>", "Snapshot ID to boot from").option("--session <sessionId>", "Session ID to attach or respawn").option("--volume <spec>", "Volume spec: <volume_id>:<mount_path>[:rw|ro[:checkpoint_id]]", (value, all) => {
|
|
2843
|
+
all.push(value);
|
|
2844
|
+
return all;
|
|
2845
|
+
}, []).action(async (options) => {
|
|
2846
|
+
const { client, settings } = requireClient(deps, options);
|
|
2847
|
+
const payload = {};
|
|
2848
|
+
if (options.type === "computer-use") {
|
|
2849
|
+
payload.image_variant = "computer-use";
|
|
2850
|
+
}
|
|
2851
|
+
if (options.timeout) {
|
|
2852
|
+
payload.vm_lifetime_seconds = Number.parseInt(options.timeout, 10);
|
|
2853
|
+
}
|
|
2854
|
+
if (options.memory) {
|
|
2855
|
+
payload.memory_mb = Number.parseInt(options.memory, 10);
|
|
2856
|
+
}
|
|
2857
|
+
if (options.vcpu) {
|
|
2858
|
+
payload.vcpu_count = Number.parseInt(options.vcpu, 10);
|
|
2859
|
+
}
|
|
2860
|
+
if (options.snapshot) {
|
|
2861
|
+
payload.snapshot_id = options.snapshot;
|
|
2862
|
+
}
|
|
2863
|
+
if (options.session) {
|
|
2864
|
+
payload.session_id = options.session;
|
|
2865
|
+
}
|
|
2866
|
+
if (options.volume.length > 0) {
|
|
2867
|
+
payload.volumes = options.volume.map(parseVolumeSpec);
|
|
2868
|
+
}
|
|
2869
|
+
const result = await client.vms.create(payload, true);
|
|
2870
|
+
emitOutput(
|
|
2871
|
+
deps,
|
|
2872
|
+
options,
|
|
2873
|
+
result,
|
|
2874
|
+
[
|
|
2875
|
+
`VM: ${result.vm_id || "-"}`,
|
|
2876
|
+
`Session: ${result.session_id || "-"}`,
|
|
2877
|
+
`Status: ${result.status || "-"}`,
|
|
2878
|
+
...result.vm_id ? [`SSH: ssh ${result.vm_id}@${settings.sshHost}`] : []
|
|
2879
|
+
]
|
|
2880
|
+
);
|
|
2881
|
+
})
|
|
2882
|
+
);
|
|
2883
|
+
addRuntimeOptions(
|
|
2884
|
+
program.command("ls").alias("list").description("List your VMs").option("--all", "Include all VM records, not just active VMs").action(async (options) => {
|
|
2885
|
+
const { client } = requireClient(deps, options);
|
|
2886
|
+
const vms = options.all ? await client.vms.listAllRecords() : await client.vms.list();
|
|
2887
|
+
emitOutput(
|
|
2888
|
+
deps,
|
|
2889
|
+
options,
|
|
2890
|
+
{ vms },
|
|
2891
|
+
vms.length > 0 ? vms.map((vm) => `${vm.vm_id} ${vm.status || "-"} ${vm.created_at || "-"} ${vm.ssh_host || "-"}`) : ["No VMs."]
|
|
2892
|
+
);
|
|
2893
|
+
})
|
|
2894
|
+
);
|
|
2895
|
+
addRuntimeOptions(
|
|
2896
|
+
program.command("rm").alias("delete").description("Delete one or more VMs").argument("<vmIds...>").action(async (vmIds, options) => {
|
|
2897
|
+
const { client } = requireClient(deps, options);
|
|
2898
|
+
const results = [];
|
|
2899
|
+
for (const vmId of vmIds) {
|
|
2900
|
+
results.push({ vm_id: vmId, ...await client.vms.delete(vmId) });
|
|
2901
|
+
}
|
|
2902
|
+
emitOutput(
|
|
2903
|
+
deps,
|
|
2904
|
+
options,
|
|
2905
|
+
{ results },
|
|
2906
|
+
results.map((entry) => `${entry.vm_id}: ${entry.status || "ok"}`)
|
|
2907
|
+
);
|
|
2908
|
+
})
|
|
2909
|
+
);
|
|
2910
|
+
addRuntimeOptions(
|
|
2911
|
+
program.command("clone").description("Clone a VM via snapshot").argument("<vmId>").option("--name <name>", "Snapshot name used during clone").action(async (vmId, options) => {
|
|
2912
|
+
const { client, settings } = requireClient(deps, options);
|
|
2913
|
+
const result = await client.vms.clone(vmId, options.name ? { snapshot_name: options.name } : {}, true);
|
|
2914
|
+
emitOutput(
|
|
2915
|
+
deps,
|
|
2916
|
+
options,
|
|
2917
|
+
result,
|
|
2918
|
+
[
|
|
2919
|
+
`VM: ${result.vm_id || "-"}`,
|
|
2920
|
+
`Session: ${result.session_id || "-"}`,
|
|
2921
|
+
`Status: ${result.status || "-"}`,
|
|
2922
|
+
...result.vm_id ? [`SSH: ssh ${result.vm_id}@${settings.sshHost}`] : []
|
|
2923
|
+
]
|
|
2924
|
+
);
|
|
2925
|
+
})
|
|
2926
|
+
);
|
|
2927
|
+
addRuntimeOptions(
|
|
2928
|
+
program.command("connect").description("SSH into a VM with the local ssh client").argument("<vmId>").argument("[sshArgs...]").action(async (vmId, sshArgs, options) => {
|
|
2929
|
+
const settings = resolveSettings(deps, options);
|
|
2930
|
+
const sshBinary = process.platform === "win32" ? findExecutable(deps, "ssh.exe") || findExecutable(deps, "ssh") : findExecutable(deps, "ssh") || findExecutable(deps, "ssh.exe");
|
|
2931
|
+
if (!sshBinary) {
|
|
2932
|
+
throw new Error("ssh client not found on PATH");
|
|
2933
|
+
}
|
|
2934
|
+
const passthroughArgs = [...sshArgs || []];
|
|
2935
|
+
if (passthroughArgs[0] === "--") {
|
|
2936
|
+
passthroughArgs.shift();
|
|
2937
|
+
}
|
|
2938
|
+
const command = [sshBinary, `${vmId}@${settings.sshHost}`, ...passthroughArgs];
|
|
2939
|
+
if (options.json) {
|
|
2940
|
+
printJson(deps, { command });
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2943
|
+
const result = deps.spawnSync(command[0], command.slice(1), { stdio: "inherit" });
|
|
2944
|
+
if (result.error) {
|
|
2945
|
+
throw result.error;
|
|
2946
|
+
}
|
|
2947
|
+
if ((result.status ?? 0) !== 0) {
|
|
2948
|
+
throw new import_commander.CommanderError(result.status ?? 1, "ssh.exit", `ssh exited with status ${result.status ?? 1}`);
|
|
2949
|
+
}
|
|
2950
|
+
}),
|
|
2951
|
+
{ includeSshHost: true }
|
|
2952
|
+
);
|
|
2953
|
+
const snapshot = program.command("snapshot").description("List, inspect, build, create, and delete snapshots");
|
|
2954
|
+
addRuntimeOptions(
|
|
2955
|
+
snapshot.command("ls").alias("list").description("List snapshots").option("--type <type>", "Snapshot type filter").action(async (options) => {
|
|
2956
|
+
const { client } = requireClient(deps, options);
|
|
2957
|
+
const snapshots = await client.snapshots.list(options.type ? { type: options.type } : {});
|
|
2958
|
+
emitOutput(
|
|
2959
|
+
deps,
|
|
2960
|
+
options,
|
|
2961
|
+
{ snapshots },
|
|
2962
|
+
snapshots.length > 0 ? snapshots.map((snap) => `${snap.id} ${snap.name || "-"} ${snap.status || "-"} ${snap.type || "-"}`) : ["No snapshots."]
|
|
2963
|
+
);
|
|
2964
|
+
})
|
|
2965
|
+
);
|
|
2966
|
+
addRuntimeOptions(
|
|
2967
|
+
snapshot.command("create").description("Create a snapshot from a running VM").argument("<vmId>").option("--name <name>", "Snapshot name").action(async (vmId, options) => {
|
|
2968
|
+
const { client } = requireClient(deps, options);
|
|
2969
|
+
const result = await client.vms.snapshot(vmId, options.name ? { name: options.name } : {}, true);
|
|
2970
|
+
emitOutput(
|
|
2971
|
+
deps,
|
|
2972
|
+
options,
|
|
2973
|
+
result,
|
|
2974
|
+
[`Snapshot: ${result.snapshot_id || result.id || "-"}`, `Status: ${result.status || "-"}`]
|
|
2975
|
+
);
|
|
2976
|
+
})
|
|
2977
|
+
);
|
|
2978
|
+
addRuntimeOptions(
|
|
2979
|
+
snapshot.command("build").description("Build a snapshot from an OCI image").argument("<ociImage>").option("--name <name>", "Snapshot name").option("--vcpu <count>", "vCPU count").option("--memory <mb>", "Memory in MB").option("--disk-size <gb>", "Snapshot disk size in GB").option("--env <pair>", "Build environment variable in KEY=VALUE form", (value, all) => {
|
|
2980
|
+
all.push(value);
|
|
2981
|
+
return all;
|
|
2982
|
+
}, []).option("--git-clone <url>", "Git repository to clone during build").option("--git-branch <branch>", "Git branch to checkout").option("--run <command>", "Command to run after the image is prepared").option("--apt <packages>", "Extra apt packages").option("--pip <packages>", "Extra pip packages").option("--npm <packages>", "Extra npm packages").option("--type <type>", "Snapshot type", "user").action(async (ociImage, options) => {
|
|
2983
|
+
const { client } = requireClient(deps, options);
|
|
2984
|
+
const buildArgs = {};
|
|
2985
|
+
if (options.apt) buildArgs.extra_apt_packages = options.apt;
|
|
2986
|
+
if (options.pip) buildArgs.extra_pip_packages = options.pip;
|
|
2987
|
+
if (options.npm) buildArgs.extra_npm_packages = options.npm;
|
|
2988
|
+
if (options.gitClone) buildArgs.git_clone_url = options.gitClone;
|
|
2989
|
+
if (options.gitBranch) buildArgs.git_clone_branch = options.gitBranch;
|
|
2990
|
+
if (options.diskSize) buildArgs.disk_size_gb = Number.parseInt(options.diskSize, 10);
|
|
2991
|
+
if (options.run) buildArgs.post_build_cmd = options.run;
|
|
2992
|
+
if (options.env.length > 0) buildArgs.envs = parseEnvPairs(options.env);
|
|
2993
|
+
const result = await client.snapshots.create({
|
|
2994
|
+
oci_image: ociImage,
|
|
2995
|
+
name: options.name,
|
|
2996
|
+
vcpu_count: options.vcpu ? Number.parseInt(options.vcpu, 10) : void 0,
|
|
2997
|
+
memory_mb: options.memory ? Number.parseInt(options.memory, 10) : void 0,
|
|
2998
|
+
build_args: Object.keys(buildArgs).length > 0 ? buildArgs : void 0,
|
|
2999
|
+
type: options.type
|
|
3000
|
+
});
|
|
3001
|
+
emitOutput(
|
|
3002
|
+
deps,
|
|
3003
|
+
options,
|
|
3004
|
+
result,
|
|
3005
|
+
[`Snapshot: ${result.id || "-"}`, `Name: ${result.name || "-"}`, `Status: ${result.status || "-"}`]
|
|
3006
|
+
);
|
|
3007
|
+
})
|
|
3008
|
+
);
|
|
3009
|
+
addRuntimeOptions(
|
|
3010
|
+
snapshot.command("get").description("Get a snapshot by ID").argument("<snapshotId>").action(async (snapshotId, options) => {
|
|
3011
|
+
const { client } = requireClient(deps, options);
|
|
3012
|
+
const result = await client.snapshots.get(snapshotId);
|
|
3013
|
+
emitOutput(
|
|
3014
|
+
deps,
|
|
3015
|
+
options,
|
|
3016
|
+
result,
|
|
3017
|
+
[
|
|
3018
|
+
`Snapshot: ${result.id || "-"}`,
|
|
3019
|
+
`Name: ${result.name || "-"}`,
|
|
3020
|
+
`Status: ${result.status || "-"}`,
|
|
3021
|
+
`Type: ${result.type || "-"}`
|
|
3022
|
+
]
|
|
3023
|
+
);
|
|
3024
|
+
})
|
|
3025
|
+
);
|
|
3026
|
+
addRuntimeOptions(
|
|
3027
|
+
snapshot.command("rm").alias("delete").description("Delete a snapshot").argument("<snapshotId>").action(async (snapshotId, options) => {
|
|
3028
|
+
const { client } = requireClient(deps, options);
|
|
3029
|
+
const result = await client.snapshots.delete(snapshotId);
|
|
3030
|
+
emitOutput(deps, options, { snapshot_id: snapshotId, ...result }, [`Deleted snapshot ${snapshotId}`]);
|
|
3031
|
+
})
|
|
3032
|
+
);
|
|
3033
|
+
const share = program.command("share").description("Create and manage VM shares");
|
|
3034
|
+
addRuntimeOptions(
|
|
3035
|
+
share.command("create").description("Create a share for a VM and port").argument("<vmId>").argument("<port>").option("--public", "Create the share as public").action(async (vmId, port, options) => {
|
|
3036
|
+
const { client } = requireClient(deps, options);
|
|
3037
|
+
const result = await client.shares.create({
|
|
3038
|
+
vm_id: vmId,
|
|
3039
|
+
port: Number.parseInt(port, 10),
|
|
3040
|
+
is_public: Boolean(options.public)
|
|
3041
|
+
});
|
|
3042
|
+
emitOutput(
|
|
3043
|
+
deps,
|
|
3044
|
+
options,
|
|
3045
|
+
result,
|
|
3046
|
+
[
|
|
3047
|
+
`Share: ${result.share_id}`,
|
|
3048
|
+
`URL: ${result.url}`,
|
|
3049
|
+
`Port: ${result.port}`,
|
|
3050
|
+
`Visibility: ${result.is_public ? "public" : "private"}`
|
|
3051
|
+
]
|
|
3052
|
+
);
|
|
3053
|
+
})
|
|
3054
|
+
);
|
|
3055
|
+
const addShareUpdateCommand = (name, description, mode) => {
|
|
3056
|
+
addRuntimeOptions(
|
|
3057
|
+
share.command(name).description(description).argument("<shareId>").action(async (shareId, options) => {
|
|
3058
|
+
const { client } = requireClient(deps, options);
|
|
3059
|
+
const result = await client.shares.update(
|
|
3060
|
+
shareId,
|
|
3061
|
+
mode === "revoke" ? { revoke: true } : { is_public: mode === "public" }
|
|
3062
|
+
);
|
|
3063
|
+
emitOutput(
|
|
3064
|
+
deps,
|
|
3065
|
+
options,
|
|
3066
|
+
{ share_id: shareId, ...result },
|
|
3067
|
+
[mode === "revoke" ? `Revoked share ${shareId}` : `Updated share ${shareId} to ${mode}`]
|
|
3068
|
+
);
|
|
3069
|
+
})
|
|
3070
|
+
);
|
|
3071
|
+
};
|
|
3072
|
+
addShareUpdateCommand("set-public", "Set a share to public visibility", "public");
|
|
3073
|
+
addShareUpdateCommand("set-private", "Set a share to private visibility", "private");
|
|
3074
|
+
addShareUpdateCommand("revoke", "Revoke a share by share ID", "revoke");
|
|
3075
|
+
const sshKey = program.command("ssh-key").alias("sshkey").description("Manage SSH keys on your account");
|
|
3076
|
+
addRuntimeOptions(
|
|
3077
|
+
sshKey.command("list").description("List active SSH keys").action(async (options) => {
|
|
3078
|
+
const { client } = requireClient(deps, options);
|
|
3079
|
+
const keys = await client.listSshKeys();
|
|
3080
|
+
emitOutput(
|
|
3081
|
+
deps,
|
|
3082
|
+
options,
|
|
3083
|
+
{ keys },
|
|
3084
|
+
keys.length > 0 ? keys.map((key) => `${key.id} ${key.fingerprint} ${key.comment || ""}`.trimEnd()) : ["No SSH keys."]
|
|
3085
|
+
);
|
|
3086
|
+
})
|
|
3087
|
+
);
|
|
3088
|
+
addRuntimeOptions(
|
|
3089
|
+
sshKey.command("add").description("Add an SSH public key").argument("[publicKey]").action(async (publicKey, options) => {
|
|
3090
|
+
const { client } = requireClient(deps, options);
|
|
3091
|
+
const value = publicKey?.trim() || (!process.stdin.isTTY ? (await deps.readStdin()).trim() : "");
|
|
3092
|
+
if (!value) {
|
|
3093
|
+
throw new Error("Public key is required");
|
|
3094
|
+
}
|
|
3095
|
+
const result = await client.addSshKey(value);
|
|
3096
|
+
emitOutput(deps, options, result, [`Added ${result.fingerprint || result.id}`]);
|
|
3097
|
+
})
|
|
3098
|
+
);
|
|
3099
|
+
addRuntimeOptions(
|
|
3100
|
+
sshKey.command("remove").description("Remove an SSH key by key ID").argument("<keyId>").action(async (keyId, options) => {
|
|
3101
|
+
const { client } = requireClient(deps, options);
|
|
3102
|
+
const result = await client.deleteSshKey(Number.parseInt(keyId, 10));
|
|
3103
|
+
emitOutput(deps, options, { key_id: Number.parseInt(keyId, 10), ...result }, [`Removed ${keyId}`]);
|
|
3104
|
+
})
|
|
3105
|
+
);
|
|
3106
|
+
const desktop = program.command("desktop").description("Manage computer-use desktops and viewer handoff");
|
|
3107
|
+
const addDesktopCommand = (name, description, action) => {
|
|
3108
|
+
addRuntimeOptions(
|
|
3109
|
+
desktop.command(name).description(description).argument("<target>").action(action)
|
|
3110
|
+
);
|
|
3111
|
+
};
|
|
3112
|
+
addDesktopCommand("status", "Show desktop status for a VM or session", async (target, options) => {
|
|
3113
|
+
const { client } = requireClient(deps, options);
|
|
3114
|
+
const payload = await desktopPayload(client, target);
|
|
3115
|
+
emitOutput(
|
|
3116
|
+
deps,
|
|
3117
|
+
options,
|
|
3118
|
+
payload,
|
|
3119
|
+
[
|
|
3120
|
+
`Session: ${payload.session_id || "-"}`,
|
|
3121
|
+
`VM: ${payload.vm_id || "-"}`,
|
|
3122
|
+
`Status: ${payload.vm_status || (payload.vm_alive ? "active" : "stopped")}`,
|
|
3123
|
+
...payload.viewer_url ? [`Viewer: ${payload.viewer_url}`] : []
|
|
3124
|
+
]
|
|
3125
|
+
);
|
|
3126
|
+
});
|
|
3127
|
+
addDesktopCommand("viewer", "Get the noVNC viewer URL for a running desktop", async (target, options) => {
|
|
3128
|
+
const { client } = requireClient(deps, options);
|
|
3129
|
+
const payload = await desktopPayload(client, target);
|
|
3130
|
+
if (!payload.session_id || !payload.vm_alive) {
|
|
3131
|
+
throw new Error("Desktop is not running.");
|
|
3132
|
+
}
|
|
3133
|
+
if (!payload.viewer_url) {
|
|
3134
|
+
Object.assign(payload, await client.computerUse.viewerUrl(String(payload.session_id)));
|
|
3135
|
+
}
|
|
3136
|
+
emitOutput(
|
|
3137
|
+
deps,
|
|
3138
|
+
options,
|
|
3139
|
+
payload,
|
|
3140
|
+
[
|
|
3141
|
+
`Session: ${payload.session_id}`,
|
|
3142
|
+
`VM: ${payload.vm_id || "-"}`,
|
|
3143
|
+
`Viewer URL: ${payload.viewer_url}`,
|
|
3144
|
+
...payload.websocket_url ? [`WebSocket: ${payload.websocket_url}`] : []
|
|
3145
|
+
]
|
|
3146
|
+
);
|
|
3147
|
+
});
|
|
3148
|
+
addDesktopCommand("start", "Start a computer-use desktop for a session", async (target, options) => {
|
|
3149
|
+
const { client } = requireClient(deps, options);
|
|
3150
|
+
const payload = await desktopPayload(client, target);
|
|
3151
|
+
if (payload.vm_alive) {
|
|
3152
|
+
emitOutput(
|
|
3153
|
+
deps,
|
|
3154
|
+
options,
|
|
3155
|
+
payload,
|
|
3156
|
+
[`Desktop already running: ${payload.vm_id || "-"}`, `Session: ${payload.session_id || "-"}`]
|
|
3157
|
+
);
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3160
|
+
if (!payload.session_id) {
|
|
3161
|
+
throw new Error("Desktop start requires a session-backed VM or session ID.");
|
|
3162
|
+
}
|
|
3163
|
+
const result = await client.vms.create({ session_id: String(payload.session_id), image_variant: "computer-use" }, true);
|
|
3164
|
+
const merged = await desktopPayload(client, String(result.session_id || payload.session_id));
|
|
3165
|
+
Object.assign(merged, result);
|
|
3166
|
+
emitOutput(
|
|
3167
|
+
deps,
|
|
3168
|
+
options,
|
|
3169
|
+
merged,
|
|
3170
|
+
[
|
|
3171
|
+
`Desktop started: ${merged.vm_id || result.vm_id || "-"}`,
|
|
3172
|
+
`Session: ${merged.session_id || result.session_id || "-"}`,
|
|
3173
|
+
...merged.viewer_url ? [`Viewer: ${merged.viewer_url}`] : []
|
|
3174
|
+
]
|
|
3175
|
+
);
|
|
3176
|
+
});
|
|
3177
|
+
addDesktopCommand("stop", "Stop a running desktop VM", async (target, options) => {
|
|
3178
|
+
const { client } = requireClient(deps, options);
|
|
3179
|
+
const payload = await desktopPayload(client, target);
|
|
3180
|
+
if (!payload.vm_alive) {
|
|
3181
|
+
emitOutput(deps, options, payload, ["Desktop is already stopped."]);
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
3184
|
+
const result = payload.session_id ? await client.kill(String(payload.session_id)) : await client.vms.delete(String(payload.vm_id));
|
|
3185
|
+
emitOutput(
|
|
3186
|
+
deps,
|
|
3187
|
+
options,
|
|
3188
|
+
{ session_id: payload.session_id, vm_id: payload.vm_id, ...result },
|
|
3189
|
+
[`Stopping ${payload.vm_id || payload.session_id}...`]
|
|
3190
|
+
);
|
|
3191
|
+
});
|
|
3192
|
+
const volume = program.command("volume").alias("volumes").description("Manage persistent volumes");
|
|
3193
|
+
addRuntimeOptions(
|
|
3194
|
+
volume.command("ls").alias("list").description("List volumes").option("--refresh", "Refresh usage before listing").action(async (options) => {
|
|
3195
|
+
const { client } = requireClient(deps, options);
|
|
3196
|
+
const volumes = await client.volumes.list(Boolean(options.refresh));
|
|
3197
|
+
emitOutput(
|
|
3198
|
+
deps,
|
|
3199
|
+
options,
|
|
3200
|
+
{ volumes },
|
|
3201
|
+
volumes.length > 0 ? volumes.map((entry) => `${entry.id} ${entry.name} ${humanBytes(entry.used_bytes)}/${humanBytes(entry.quota_bytes)} ${entry.status}`) : ["No volumes."]
|
|
3202
|
+
);
|
|
3203
|
+
})
|
|
3204
|
+
);
|
|
3205
|
+
addRuntimeOptions(
|
|
3206
|
+
volume.command("get").description("Get a volume").argument("<volumeId>").option("--refresh", "Refresh usage before reading").action(async (volumeId, options) => {
|
|
3207
|
+
const { client } = requireClient(deps, options);
|
|
3208
|
+
const result = await client.volumes.get(volumeId, Boolean(options.refresh));
|
|
3209
|
+
emitOutput(
|
|
3210
|
+
deps,
|
|
3211
|
+
options,
|
|
3212
|
+
result,
|
|
3213
|
+
[
|
|
3214
|
+
`ID: ${result.id}`,
|
|
3215
|
+
`Name: ${result.name}`,
|
|
3216
|
+
`Status: ${result.status}`,
|
|
3217
|
+
`Quota: ${humanBytes(result.quota_bytes)}`,
|
|
3218
|
+
`Used: ${humanBytes(result.used_bytes)}`
|
|
3219
|
+
]
|
|
3220
|
+
);
|
|
3221
|
+
})
|
|
3222
|
+
);
|
|
3223
|
+
addRuntimeOptions(
|
|
3224
|
+
volume.command("create").description("Create a volume").argument("<name>").requiredOption("--quota <size>", "Quota in bytes or units like 5gb").action(async (name, options) => {
|
|
3225
|
+
const { client } = requireClient(deps, options);
|
|
3226
|
+
const result = await client.volumes.create({ name, quota_bytes: parseSize(options.quota) });
|
|
3227
|
+
emitOutput(
|
|
3228
|
+
deps,
|
|
3229
|
+
options,
|
|
3230
|
+
result,
|
|
3231
|
+
[`Volume: ${result.id}`, `Name: ${result.name}`, `Quota: ${humanBytes(result.quota_bytes)}`]
|
|
3232
|
+
);
|
|
3233
|
+
})
|
|
3234
|
+
);
|
|
3235
|
+
addRuntimeOptions(
|
|
3236
|
+
volume.command("update").description("Update a volume").argument("<volumeId>").option("--name <name>", "Updated volume name").option("--quota <size>", "Updated quota in bytes or units like 5gb").action(async (volumeId, options) => {
|
|
3237
|
+
const { client } = requireClient(deps, options);
|
|
3238
|
+
const payload = {};
|
|
3239
|
+
if (options.name) payload.name = options.name;
|
|
3240
|
+
if (options.quota) payload.quota_bytes = parseSize(options.quota);
|
|
3241
|
+
if (Object.keys(payload).length === 0) {
|
|
3242
|
+
throw new Error("Provide --name and/or --quota");
|
|
3243
|
+
}
|
|
3244
|
+
const result = await client.volumes.update(volumeId, payload);
|
|
3245
|
+
emitOutput(deps, options, result, [`Updated volume ${result.id || volumeId}`]);
|
|
3246
|
+
})
|
|
3247
|
+
);
|
|
3248
|
+
addRuntimeOptions(
|
|
3249
|
+
volume.command("rm").alias("delete").description("Delete a volume").argument("<volumeId>").action(async (volumeId, options) => {
|
|
3250
|
+
const { client } = requireClient(deps, options);
|
|
3251
|
+
const result = await client.volumes.delete(volumeId);
|
|
3252
|
+
emitOutput(deps, options, { volume_id: volumeId, ...result }, [`Deleted ${volumeId}`]);
|
|
3253
|
+
})
|
|
3254
|
+
);
|
|
3255
|
+
const checkpoint = volume.command("checkpoint").description("Manage checkpoints for a volume");
|
|
3256
|
+
addRuntimeOptions(
|
|
3257
|
+
checkpoint.command("ls").alias("list").description("List checkpoints for a volume").argument("<volumeId>").action(async (volumeId, options) => {
|
|
3258
|
+
const { client } = requireClient(deps, options);
|
|
3259
|
+
const checkpoints = await client.volumes.listCheckpoints(volumeId);
|
|
3260
|
+
emitOutput(
|
|
3261
|
+
deps,
|
|
3262
|
+
options,
|
|
3263
|
+
{ checkpoints },
|
|
3264
|
+
checkpoints.length > 0 ? checkpoints.map((entry) => `${entry.id} ${entry.name || "-"} ${entry.status || "-"}`) : ["No checkpoints."]
|
|
3265
|
+
);
|
|
3266
|
+
})
|
|
3267
|
+
);
|
|
3268
|
+
addRuntimeOptions(
|
|
3269
|
+
checkpoint.command("create").description("Create a checkpoint for a volume").argument("<volumeId>").option("--name <name>", "Checkpoint name").action(async (volumeId, options) => {
|
|
3270
|
+
const { client } = requireClient(deps, options);
|
|
3271
|
+
const result = await client.volumes.createCheckpoint(volumeId, options.name ? { name: options.name } : {});
|
|
3272
|
+
emitOutput(deps, options, result, [`Created checkpoint ${result.id || "-"}`]);
|
|
3273
|
+
})
|
|
3274
|
+
);
|
|
3275
|
+
addRuntimeOptions(
|
|
3276
|
+
checkpoint.command("rm").alias("delete").description("Delete a checkpoint").argument("<volumeId>").argument("<checkpointId>").action(async (volumeId, checkpointId, options) => {
|
|
3277
|
+
const { client } = requireClient(deps, options);
|
|
3278
|
+
const result = await client.volumes.deleteCheckpoint(volumeId, checkpointId);
|
|
3279
|
+
emitOutput(deps, options, { checkpoint_id: checkpointId, ...result }, [`Deleted checkpoint ${checkpointId}`]);
|
|
3280
|
+
})
|
|
3281
|
+
);
|
|
3282
|
+
const files = volume.command("files").description("Manage files stored inside a volume");
|
|
3283
|
+
addRuntimeOptions(
|
|
3284
|
+
files.command("ls").alias("list").description("List files inside a volume").argument("<volumeId>").option("--prefix <prefix>", "Path prefix filter").option("--limit <limit>", "Maximum number of entries", "1000").option("--no-recursive", "Do not recurse into subdirectories").action(async (volumeId, options) => {
|
|
3285
|
+
const { client } = requireClient(deps, options);
|
|
3286
|
+
const result = await client.volumes.listFiles(volumeId, {
|
|
3287
|
+
prefix: options.prefix || "",
|
|
3288
|
+
recursive: options.recursive,
|
|
3289
|
+
limit: Number.parseInt(options.limit, 10)
|
|
3290
|
+
});
|
|
3291
|
+
emitOutput(
|
|
3292
|
+
deps,
|
|
3293
|
+
options,
|
|
3294
|
+
{ files: result },
|
|
3295
|
+
result.length > 0 ? result.map((entry) => `${entry.path} ${humanBytes(entry.size)}`) : ["No files."]
|
|
3296
|
+
);
|
|
3297
|
+
})
|
|
3298
|
+
);
|
|
3299
|
+
addRuntimeOptions(
|
|
3300
|
+
files.command("upload").description("Upload a file into a volume").argument("<volumeId>").argument("<localPath>").option("--path <remotePath>", "Remote path inside the volume").option("--overwrite", "Overwrite if the path already exists").action(async (volumeId, localPath, options) => {
|
|
3301
|
+
const { client } = requireClient(deps, options);
|
|
3302
|
+
const remotePath = options.path || import_path12.default.basename(localPath);
|
|
3303
|
+
const result = await client.volumes.uploadFile(volumeId, {
|
|
3304
|
+
filePath: localPath,
|
|
3305
|
+
path: remotePath,
|
|
3306
|
+
overwrite: Boolean(options.overwrite)
|
|
3307
|
+
});
|
|
3308
|
+
emitOutput(deps, options, result, [`Uploaded ${localPath} -> ${remotePath}`]);
|
|
3309
|
+
})
|
|
3310
|
+
);
|
|
3311
|
+
addRuntimeOptions(
|
|
3312
|
+
files.command("download").description("Download a file from a volume").argument("<volumeId>").argument("<remotePath>").option("--out <outputPath>", "Local output path").action(async (volumeId, remotePath, options) => {
|
|
3313
|
+
const { client } = requireClient(deps, options);
|
|
3314
|
+
const result = await client.volumes.downloadFile(volumeId, remotePath);
|
|
3315
|
+
const outputPath = options.out || import_path12.default.basename(remotePath);
|
|
3316
|
+
deps.writeFile(outputPath, result.content);
|
|
3317
|
+
emitOutput(
|
|
3318
|
+
deps,
|
|
3319
|
+
options,
|
|
3320
|
+
{
|
|
3321
|
+
path: result.path,
|
|
3322
|
+
filename: result.filename,
|
|
3323
|
+
size: result.size,
|
|
3324
|
+
local_path: outputPath
|
|
3325
|
+
},
|
|
3326
|
+
[`Downloaded ${remotePath} -> ${outputPath}`]
|
|
3327
|
+
);
|
|
3328
|
+
})
|
|
3329
|
+
);
|
|
3330
|
+
addRuntimeOptions(
|
|
3331
|
+
files.command("rm").alias("delete").description("Delete a file from a volume").argument("<volumeId>").argument("<remotePath>").action(async (volumeId, remotePath, options) => {
|
|
3332
|
+
const { client } = requireClient(deps, options);
|
|
3333
|
+
const result = await client.volumes.deleteFile(volumeId, remotePath);
|
|
3334
|
+
emitOutput(deps, options, { path: remotePath, ...result }, [`Deleted ${remotePath}`]);
|
|
3335
|
+
})
|
|
3336
|
+
);
|
|
3337
|
+
program.command("doc").alias("docs").description("Show InstaVM documentation links").option("-j, --json", "Emit JSON output").action((options) => {
|
|
3338
|
+
emitOutput(deps, options, { url: DEFAULT_DOCS_URL }, [DEFAULT_DOCS_URL]);
|
|
3339
|
+
});
|
|
3340
|
+
program.command("billing").description("Show the billing portal URL").option("-j, --json", "Emit JSON output").action((options) => {
|
|
3341
|
+
emitOutput(deps, options, { url: DEFAULT_BILLING_URL }, [DEFAULT_BILLING_URL]);
|
|
3342
|
+
});
|
|
3343
|
+
return program;
|
|
3344
|
+
}
|
|
3345
|
+
async function runCli(argv, deps = defaultDeps) {
|
|
3346
|
+
const program = createProgram(deps);
|
|
3347
|
+
try {
|
|
3348
|
+
await program.parseAsync(["node", "instavm", ...argv]);
|
|
3349
|
+
return 0;
|
|
3350
|
+
} catch (error) {
|
|
3351
|
+
if (error instanceof import_commander.CommanderError) {
|
|
3352
|
+
if (error.code === "commander.helpDisplayed") {
|
|
3353
|
+
return error.exitCode;
|
|
3354
|
+
}
|
|
3355
|
+
deps.stderr.write(`${error.message}
|
|
3356
|
+
`);
|
|
3357
|
+
return error.exitCode || 1;
|
|
3358
|
+
}
|
|
3359
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3360
|
+
deps.stderr.write(`Error: ${message}
|
|
3361
|
+
`);
|
|
3362
|
+
return 1;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
function findExecutable(deps, binary) {
|
|
3366
|
+
const result = deps.spawnSync(process.platform === "win32" ? "where" : "which", [binary], {
|
|
3367
|
+
encoding: "utf8",
|
|
3368
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3369
|
+
});
|
|
3370
|
+
if (result.status === 0) {
|
|
3371
|
+
const output = (result.stdout || "").toString().split(/\r?\n/).find(Boolean);
|
|
3372
|
+
return output || null;
|
|
3373
|
+
}
|
|
3374
|
+
return null;
|
|
3375
|
+
}
|
|
3376
|
+
async function promptSecret(prompt) {
|
|
3377
|
+
if (!process.stdin.isTTY) {
|
|
3378
|
+
return "";
|
|
3379
|
+
}
|
|
3380
|
+
return new Promise((resolve, reject) => {
|
|
3381
|
+
const stdin = process.stdin;
|
|
3382
|
+
const stdout = process.stdout;
|
|
3383
|
+
let value = "";
|
|
3384
|
+
const cleanup = () => {
|
|
3385
|
+
stdin.removeListener("data", onData);
|
|
3386
|
+
stdin.pause();
|
|
3387
|
+
if (typeof stdin.setRawMode === "function") {
|
|
3388
|
+
stdin.setRawMode(false);
|
|
3389
|
+
}
|
|
3390
|
+
stdout.write("\n");
|
|
3391
|
+
};
|
|
3392
|
+
const onData = (chunk) => {
|
|
3393
|
+
const input = chunk.toString("utf8");
|
|
3394
|
+
for (const character of input) {
|
|
3395
|
+
if (character === "") {
|
|
3396
|
+
cleanup();
|
|
3397
|
+
reject(new Error("Interrupted."));
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
if (character === "\r" || character === "\n") {
|
|
3401
|
+
cleanup();
|
|
3402
|
+
resolve(value);
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
if (character === "\x7F") {
|
|
3406
|
+
value = value.slice(0, -1);
|
|
3407
|
+
continue;
|
|
3408
|
+
}
|
|
3409
|
+
value += character;
|
|
3410
|
+
}
|
|
3411
|
+
};
|
|
3412
|
+
stdout.write(prompt);
|
|
3413
|
+
stdin.resume();
|
|
3414
|
+
stdin.setEncoding("utf8");
|
|
3415
|
+
if (typeof stdin.setRawMode === "function") {
|
|
3416
|
+
stdin.setRawMode(true);
|
|
3417
|
+
}
|
|
3418
|
+
stdin.on("data", onData);
|
|
3419
|
+
});
|
|
3420
|
+
}
|
|
3421
|
+
async function readStdin() {
|
|
3422
|
+
if (process.stdin.isTTY) {
|
|
3423
|
+
return "";
|
|
3424
|
+
}
|
|
3425
|
+
return new Promise((resolve, reject) => {
|
|
3426
|
+
const chunks = [];
|
|
3427
|
+
process.stdin.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
3428
|
+
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
3429
|
+
process.stdin.on("error", reject);
|
|
3430
|
+
});
|
|
3431
|
+
}
|
|
3432
|
+
async function main() {
|
|
3433
|
+
const exitCode = await runCli(process.argv.slice(2));
|
|
3434
|
+
if (exitCode !== 0) {
|
|
3435
|
+
process.exitCode = exitCode;
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
if (require.main === module) {
|
|
3439
|
+
void main();
|
|
3440
|
+
}
|
|
3441
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3442
|
+
0 && (module.exports = {
|
|
3443
|
+
createProgram,
|
|
3444
|
+
runCli
|
|
3445
|
+
});
|
|
3446
|
+
//# sourceMappingURL=cli.js.map
|