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/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