@evident-ai/cli 0.1.5 → 0.2.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/index.js CHANGED
@@ -1,4 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ clearCredentials,
4
+ getApiUrlConfig,
5
+ getCredentials,
6
+ getTunnelUrlConfig,
7
+ setCredentials,
8
+ setEnvironment
9
+ } from "./chunk-MWOWXSOP.js";
2
10
 
3
11
  // src/index.ts
4
12
  import { Command } from "commander";
@@ -8,79 +16,6 @@ import open from "open";
8
16
  import ora from "ora";
9
17
  import chalk2 from "chalk";
10
18
 
11
- // src/lib/config.ts
12
- import Conf from "conf";
13
- import { homedir } from "os";
14
- import { join } from "path";
15
- var environmentPresets = {
16
- local: {
17
- apiUrl: "http://localhost:3000/v1",
18
- tunnelUrl: "ws://localhost:8787"
19
- },
20
- dev: {
21
- apiUrl: "https://api.dev.evident.run/v1",
22
- tunnelUrl: "wss://tunnel.dev.evident.run"
23
- },
24
- production: {
25
- // Production URLs also have aliases: api.evident.run, tunnel.evident.run
26
- apiUrl: "https://api.production.evident.run/v1",
27
- tunnelUrl: "wss://tunnel.production.evident.run"
28
- }
29
- };
30
- var defaults = environmentPresets.production;
31
- var currentEnvironment = "production";
32
- function setEnvironment(env) {
33
- currentEnvironment = env;
34
- }
35
- function getEnvironment() {
36
- const envVar = process.env.EVIDENT_ENV;
37
- if (envVar && environmentPresets[envVar]) {
38
- return envVar;
39
- }
40
- return currentEnvironment;
41
- }
42
- function getEnvConfig() {
43
- return environmentPresets[getEnvironment()];
44
- }
45
- function getApiUrl() {
46
- return process.env.EVIDENT_API_URL ?? getEnvConfig().apiUrl;
47
- }
48
- function getTunnelUrl() {
49
- return process.env.EVIDENT_TUNNEL_URL ?? getEnvConfig().tunnelUrl;
50
- }
51
- var config = new Conf({
52
- projectName: "evident",
53
- projectSuffix: "",
54
- defaults
55
- });
56
- var credentials = new Conf({
57
- projectName: "evident",
58
- projectSuffix: "",
59
- configName: "credentials",
60
- defaults: {}
61
- });
62
- function getApiUrlConfig() {
63
- return getApiUrl();
64
- }
65
- function getTunnelUrlConfig() {
66
- return getTunnelUrl();
67
- }
68
- function getCredentials() {
69
- return {
70
- token: credentials.get("token"),
71
- user: credentials.get("user"),
72
- expiresAt: credentials.get("expiresAt")
73
- };
74
- }
75
- function setCredentials(creds) {
76
- if (creds.token) credentials.set("token", creds.token);
77
- if (creds.user) credentials.set("user", creds.user);
78
- if (creds.expiresAt) credentials.set("expiresAt", creds.expiresAt);
79
- }
80
- function clearCredentials() {
81
- credentials.clear();
82
- }
83
-
84
19
  // src/lib/api.ts
85
20
  var ApiClient = class {
86
21
  baseUrl;
@@ -188,15 +123,15 @@ async function getKeytar() {
188
123
  return null;
189
124
  }
190
125
  }
191
- async function storeToken(credentials2) {
126
+ async function storeToken(credentials) {
192
127
  const keytar = await getKeytar();
193
128
  if (keytar) {
194
- await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(credentials2));
129
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(credentials));
195
130
  } else {
196
131
  setCredentials({
197
- token: credentials2.token,
198
- user: credentials2.user,
199
- expiresAt: credentials2.expiresAt
132
+ token: credentials.token,
133
+ user: credentials.user,
134
+ expiresAt: credentials.expiresAt
200
135
  });
201
136
  }
202
137
  }
@@ -397,8 +332,8 @@ async function login(options) {
397
332
 
398
333
  // src/commands/logout.ts
399
334
  async function logout() {
400
- const credentials2 = await getToken();
401
- if (!credentials2) {
335
+ const credentials = await getToken();
336
+ if (!credentials) {
402
337
  printWarning("You are not logged in.");
403
338
  return;
404
339
  }
@@ -409,16 +344,16 @@ async function logout() {
409
344
  // src/commands/whoami.ts
410
345
  import chalk3 from "chalk";
411
346
  async function whoami() {
412
- const credentials2 = await getToken();
413
- if (!credentials2) {
347
+ const credentials = await getToken();
348
+ if (!credentials) {
414
349
  printError("Not logged in. Run the `login` command to authenticate.");
415
350
  process.exit(1);
416
351
  }
417
352
  blank();
418
- console.log(keyValue("User", chalk3.bold(credentials2.user.email)));
419
- console.log(keyValue("User ID", credentials2.user.id));
420
- if (credentials2.expiresAt) {
421
- const expiresAt = new Date(credentials2.expiresAt);
353
+ console.log(keyValue("User", chalk3.bold(credentials.user.email)));
354
+ console.log(keyValue("User ID", credentials.user.id));
355
+ if (credentials.expiresAt) {
356
+ const expiresAt = new Date(credentials.expiresAt);
422
357
  const now = /* @__PURE__ */ new Date();
423
358
  if (expiresAt < now) {
424
359
  console.log(keyValue("Status", chalk3.red("Token expired")));
@@ -436,33 +371,481 @@ async function whoami() {
436
371
  import WebSocket from "ws";
437
372
  import chalk4 from "chalk";
438
373
  import ora2 from "ora";
374
+ import { execSync, spawn } from "child_process";
375
+ import { select } from "@inquirer/prompts";
376
+
377
+ // src/lib/telemetry.ts
378
+ var CLI_VERSION = process.env.npm_package_version || "unknown";
379
+ var eventBuffer = [];
380
+ var flushTimeout = null;
381
+ var isShuttingDown = false;
382
+ var FLUSH_INTERVAL_MS = 5e3;
383
+ var MAX_BUFFER_SIZE = 50;
384
+ var FLUSH_TIMEOUT_MS = 3e3;
385
+ function logEvent(eventType, options = {}) {
386
+ const event = {
387
+ event_type: eventType,
388
+ severity: options.severity || "info",
389
+ message: options.message,
390
+ metadata: options.metadata,
391
+ sandbox_id: options.sandboxId,
392
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
393
+ };
394
+ eventBuffer.push(event);
395
+ if (options.severity === "error" || eventBuffer.length >= MAX_BUFFER_SIZE) {
396
+ void flushEvents();
397
+ } else if (!flushTimeout && !isShuttingDown) {
398
+ flushTimeout = setTimeout(() => {
399
+ flushTimeout = null;
400
+ void flushEvents();
401
+ }, FLUSH_INTERVAL_MS);
402
+ }
403
+ }
404
+ var telemetry = {
405
+ debug: (eventType, message, metadata, sandboxId) => logEvent(eventType, { severity: "debug", message, metadata, sandboxId }),
406
+ info: (eventType, message, metadata, sandboxId) => logEvent(eventType, { severity: "info", message, metadata, sandboxId }),
407
+ warn: (eventType, message, metadata, sandboxId) => logEvent(eventType, { severity: "warning", message, metadata, sandboxId }),
408
+ error: (eventType, message, metadata, sandboxId) => logEvent(eventType, { severity: "error", message, metadata, sandboxId })
409
+ };
410
+ async function flushEvents() {
411
+ if (eventBuffer.length === 0) return;
412
+ const events = eventBuffer;
413
+ eventBuffer = [];
414
+ if (flushTimeout) {
415
+ clearTimeout(flushTimeout);
416
+ flushTimeout = null;
417
+ }
418
+ try {
419
+ const credentials = await getToken();
420
+ if (!credentials) {
421
+ return;
422
+ }
423
+ const apiUrl = getApiUrlConfig();
424
+ const controller = new AbortController();
425
+ const timeout = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
426
+ try {
427
+ const response = await fetch(`${apiUrl}/telemetry/events`, {
428
+ method: "POST",
429
+ headers: {
430
+ "Content-Type": "application/json",
431
+ Authorization: `Bearer ${credentials.token}`
432
+ },
433
+ body: JSON.stringify({
434
+ events,
435
+ client_type: "cli",
436
+ client_version: CLI_VERSION
437
+ }),
438
+ signal: controller.signal
439
+ });
440
+ if (!response.ok) {
441
+ console.error(`Telemetry flush failed: ${response.status}`);
442
+ }
443
+ } finally {
444
+ clearTimeout(timeout);
445
+ }
446
+ } catch (error2) {
447
+ if (process.env.DEBUG) {
448
+ console.error("Telemetry error:", error2);
449
+ }
450
+ }
451
+ }
452
+ async function shutdownTelemetry() {
453
+ isShuttingDown = true;
454
+ if (flushTimeout) {
455
+ clearTimeout(flushTimeout);
456
+ flushTimeout = null;
457
+ }
458
+ await flushEvents();
459
+ }
460
+ var EventTypes = {
461
+ // Tunnel lifecycle
462
+ TUNNEL_STARTING: "tunnel.starting",
463
+ TUNNEL_CONNECTED: "tunnel.connected",
464
+ TUNNEL_DISCONNECTED: "tunnel.disconnected",
465
+ TUNNEL_RECONNECTING: "tunnel.reconnecting",
466
+ TUNNEL_ERROR: "tunnel.error",
467
+ // OpenCode communication
468
+ OPENCODE_HEALTH_CHECK: "opencode.health_check",
469
+ OPENCODE_HEALTH_OK: "opencode.health_ok",
470
+ OPENCODE_HEALTH_FAILED: "opencode.health_failed",
471
+ OPENCODE_REQUEST_RECEIVED: "opencode.request_received",
472
+ OPENCODE_REQUEST_FORWARDED: "opencode.request_forwarded",
473
+ OPENCODE_RESPONSE_SENT: "opencode.response_sent",
474
+ OPENCODE_UNREACHABLE: "opencode.unreachable",
475
+ OPENCODE_ERROR: "opencode.error",
476
+ // Authentication
477
+ AUTH_LOGIN_STARTED: "auth.login_started",
478
+ AUTH_LOGIN_SUCCESS: "auth.login_success",
479
+ AUTH_LOGIN_FAILED: "auth.login_failed",
480
+ AUTH_LOGOUT: "auth.logout",
481
+ // CLI lifecycle
482
+ CLI_STARTED: "cli.started",
483
+ CLI_COMMAND: "cli.command",
484
+ CLI_ERROR: "cli.error"
485
+ };
486
+
487
+ // src/commands/tunnel.ts
439
488
  var MAX_RECONNECT_DELAY = 3e4;
440
489
  var BASE_RECONNECT_DELAY = 500;
441
- function getReconnectDelay(attempt) {
442
- const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
443
- const jitter = Math.random() * 1e3;
444
- return Math.min(exponentialDelay + jitter, MAX_RECONNECT_DELAY);
490
+ var MAX_ACTIVITY_LOG_ENTRIES = 10;
491
+ var CHUNK_THRESHOLD = 512 * 1024;
492
+ var CHUNK_SIZE = 768 * 1024;
493
+ var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
494
+ function getProcessCwd(pid) {
495
+ const platform = process.platform;
496
+ try {
497
+ if (platform === "darwin") {
498
+ const output = execSync(`lsof -a -p ${pid} -d cwd -Fn 2>/dev/null`, {
499
+ encoding: "utf-8",
500
+ stdio: ["pipe", "pipe", "pipe"]
501
+ }).trim();
502
+ const lines = output.split("\n");
503
+ for (const line of lines) {
504
+ if (line.startsWith("n") && !line.startsWith("n ")) {
505
+ return line.slice(1);
506
+ }
507
+ }
508
+ } else if (platform === "linux") {
509
+ const output = execSync(`readlink /proc/${pid}/cwd 2>/dev/null`, {
510
+ encoding: "utf-8",
511
+ stdio: ["pipe", "pipe", "pipe"]
512
+ }).trim();
513
+ if (output) return output;
514
+ }
515
+ } catch {
516
+ }
517
+ return void 0;
445
518
  }
519
+ function isPortInUse(port) {
520
+ const platform = process.platform;
521
+ try {
522
+ if (platform === "darwin" || platform === "linux") {
523
+ execSync(`lsof -i :${port} -sTCP:LISTEN 2>/dev/null`, {
524
+ encoding: "utf-8",
525
+ stdio: ["pipe", "pipe", "pipe"]
526
+ });
527
+ return true;
528
+ }
529
+ } catch {
530
+ }
531
+ return false;
532
+ }
533
+ function findAvailablePort(startPort, maxAttempts = 10) {
534
+ for (let i = 0; i < maxAttempts; i++) {
535
+ const port = startPort + i;
536
+ if (!isPortInUse(port)) {
537
+ return port;
538
+ }
539
+ }
540
+ return null;
541
+ }
542
+ function findOpenCodeProcesses() {
543
+ const instances = [];
544
+ try {
545
+ const platform = process.platform;
546
+ if (platform === "darwin" || platform === "linux") {
547
+ let pids = [];
548
+ try {
549
+ const pgrepOutput = execSync('pgrep -f "opencode serve|opencode-serve"', {
550
+ encoding: "utf-8",
551
+ stdio: ["pipe", "pipe", "pipe"]
552
+ }).trim();
553
+ if (pgrepOutput) {
554
+ pids = pgrepOutput.split("\n").map((p) => parseInt(p.trim(), 10)).filter((p) => !isNaN(p));
555
+ }
556
+ } catch {
557
+ try {
558
+ const psOutput = execSync('ps aux | grep -E "opencode (serve|--port)" | grep -v grep', {
559
+ encoding: "utf-8",
560
+ stdio: ["pipe", "pipe", "pipe"]
561
+ }).trim();
562
+ if (psOutput) {
563
+ for (const line of psOutput.split("\n")) {
564
+ const parts = line.trim().split(/\s+/);
565
+ if (parts.length >= 2) {
566
+ const pid = parseInt(parts[1], 10);
567
+ if (!isNaN(pid)) pids.push(pid);
568
+ }
569
+ }
570
+ }
571
+ } catch {
572
+ }
573
+ }
574
+ for (const pid of pids) {
575
+ try {
576
+ const lsofOutput = execSync(`lsof -Pan -p ${pid} -i TCP -sTCP:LISTEN 2>/dev/null`, {
577
+ encoding: "utf-8",
578
+ stdio: ["pipe", "pipe", "pipe"]
579
+ }).trim();
580
+ for (const line of lsofOutput.split("\n")) {
581
+ const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
582
+ if (portMatch) {
583
+ const port = parseInt(portMatch[1], 10);
584
+ if (!isNaN(port) && !instances.some((i) => i.port === port)) {
585
+ const cwd = getProcessCwd(pid);
586
+ instances.push({ pid, port, cwd });
587
+ }
588
+ }
589
+ }
590
+ } catch {
591
+ }
592
+ }
593
+ }
594
+ } catch {
595
+ }
596
+ return instances;
597
+ }
598
+ async function scanPortsForOpenCode() {
599
+ const instances = [];
600
+ const checks = OPENCODE_PORT_RANGE.map(async (port) => {
601
+ const health = await checkOpenCodeHealth(port);
602
+ if (health.healthy) {
603
+ let pid = 0;
604
+ try {
605
+ const lsofOutput = execSync(`lsof -ti :${port} -sTCP:LISTEN 2>/dev/null`, {
606
+ encoding: "utf-8",
607
+ stdio: ["pipe", "pipe", "pipe"]
608
+ }).trim();
609
+ if (lsofOutput) {
610
+ pid = parseInt(lsofOutput.split("\n")[0], 10) || 0;
611
+ }
612
+ } catch {
613
+ }
614
+ const cwd = pid ? getProcessCwd(pid) : void 0;
615
+ return { pid, port, cwd, version: health.version };
616
+ }
617
+ return null;
618
+ });
619
+ const results = await Promise.all(checks);
620
+ for (const result of results) {
621
+ if (result) {
622
+ instances.push(result);
623
+ }
624
+ }
625
+ return instances;
626
+ }
627
+ async function checkOpenCodeHealth(port) {
628
+ try {
629
+ const response = await fetch(`http://localhost:${port}/health`, {
630
+ signal: AbortSignal.timeout(2e3)
631
+ // 2 second timeout
632
+ });
633
+ if (!response.ok) {
634
+ return { healthy: false, error: `HTTP ${response.status}` };
635
+ }
636
+ const data = await response.json().catch(() => ({}));
637
+ return { healthy: true, version: data.version };
638
+ } catch (error2) {
639
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
640
+ return { healthy: false, error: message };
641
+ }
642
+ }
643
+ async function findHealthyOpenCodeInstances() {
644
+ const processes = findOpenCodeProcesses();
645
+ const healthy = [];
646
+ for (const proc of processes) {
647
+ const health = await checkOpenCodeHealth(proc.port);
648
+ if (health.healthy) {
649
+ healthy.push({ ...proc, version: health.version });
650
+ }
651
+ }
652
+ if (healthy.length === 0) {
653
+ const scanned = await scanPortsForOpenCode();
654
+ return scanned;
655
+ }
656
+ return healthy;
657
+ }
658
+ function logActivity(state, entry) {
659
+ const fullEntry = {
660
+ ...entry,
661
+ timestamp: /* @__PURE__ */ new Date()
662
+ };
663
+ state.activityLog.push(fullEntry);
664
+ if (state.activityLog.length > MAX_ACTIVITY_LOG_ENTRIES) {
665
+ state.activityLog.shift();
666
+ }
667
+ state.lastActivity = fullEntry.timestamp;
668
+ }
669
+ function formatActivityEntry(entry, _verbose) {
670
+ const time = entry.timestamp.toLocaleTimeString("en-US", {
671
+ hour12: false,
672
+ hour: "2-digit",
673
+ minute: "2-digit",
674
+ second: "2-digit"
675
+ });
676
+ switch (entry.type) {
677
+ case "request": {
678
+ const duration = entry.durationMs ? ` (${entry.durationMs}ms)` : "";
679
+ const status = entry.status ? ` \u2192 ${colorizeStatus(entry.status)}` : " ...";
680
+ return ` ${chalk4.dim(`[${time}]`)} ${chalk4.cyan("\u2190")} ${entry.method} ${entry.path}${status}${duration}`;
681
+ }
682
+ case "response": {
683
+ const duration = entry.durationMs ? ` (${entry.durationMs}ms)` : "";
684
+ return ` ${chalk4.dim(`[${time}]`)} ${chalk4.green("\u2192")} ${entry.method} ${entry.path} ${colorizeStatus(entry.status)}${duration}`;
685
+ }
686
+ case "error": {
687
+ const errorMsg = entry.error || "Unknown error";
688
+ const path = entry.path ? ` ${entry.method} ${entry.path}` : "";
689
+ return ` ${chalk4.dim(`[${time}]`)} ${chalk4.red("\u2717")}${path} - ${chalk4.red(errorMsg)}`;
690
+ }
691
+ case "info": {
692
+ return ` ${chalk4.dim(`[${time}]`)} ${chalk4.blue("\u25CF")} ${entry.message}`;
693
+ }
694
+ default:
695
+ return ` ${chalk4.dim(`[${time}]`)} ${entry.message || "Unknown"}`;
696
+ }
697
+ }
698
+ function colorizeStatus(status) {
699
+ if (status >= 200 && status < 300) {
700
+ return chalk4.green(status.toString());
701
+ } else if (status >= 300 && status < 400) {
702
+ return chalk4.yellow(status.toString());
703
+ } else if (status >= 400 && status < 500) {
704
+ return chalk4.red(status.toString());
705
+ } else if (status >= 500) {
706
+ return chalk4.bgRed.white(` ${status} `);
707
+ }
708
+ return status.toString();
709
+ }
710
+ var ANSI = {
711
+ saveCursor: "\x1B[s",
712
+ restoreCursor: "\x1B[u",
713
+ clearToEnd: "\x1B[J",
714
+ moveTo: (row) => `\x1B[${row};1H`,
715
+ moveUp: (n) => `\x1B[${n}A`
716
+ };
717
+ var STATUS_DISPLAY_HEIGHT = 20;
446
718
  function displayStatus(state) {
447
- console.clear();
448
- console.log(chalk4.bold("Evident Tunnel"));
449
- console.log(chalk4.dim("\u2500".repeat(50)));
450
- blank();
719
+ const lines = [];
720
+ lines.push(chalk4.bold("Evident Tunnel"));
721
+ lines.push(chalk4.dim("\u2500".repeat(60)));
722
+ lines.push("");
723
+ if (state.sandboxName) {
724
+ lines.push(` Sandbox: ${state.sandboxName}`);
725
+ }
726
+ lines.push(` ID: ${state.sandboxId ?? "Unknown"}`);
727
+ lines.push("");
451
728
  if (state.connected) {
452
- console.log(` ${chalk4.green("\u25CF")} Status: ${chalk4.green("Online")}`);
453
- console.log(` Sandbox: ${state.sandboxId ?? "Unknown"}`);
454
- console.log(` Last activity: ${state.lastActivity.toLocaleTimeString()}`);
729
+ lines.push(` ${chalk4.green("\u25CF")} Tunnel: ${chalk4.green("Connected to Evident")}`);
455
730
  } else {
456
- console.log(` ${chalk4.yellow("\u25CB")} Status: ${chalk4.yellow("Reconnecting...")}`);
457
731
  if (state.reconnectAttempt > 0) {
458
- console.log(` Attempt: ${state.reconnectAttempt}`);
732
+ lines.push(
733
+ ` ${chalk4.yellow("\u25CB")} Tunnel: ${chalk4.yellow(`Reconnecting... (attempt ${state.reconnectAttempt})`)}`
734
+ );
735
+ } else {
736
+ lines.push(` ${chalk4.yellow("\u25CB")} Tunnel: ${chalk4.yellow("Connecting...")}`);
459
737
  }
460
738
  }
739
+ if (state.opencodeConnected) {
740
+ const version = state.opencodeVersion ? ` (v${state.opencodeVersion})` : "";
741
+ lines.push(` ${chalk4.green("\u25CF")} OpenCode: ${chalk4.green(`Running${version}`)}`);
742
+ } else {
743
+ lines.push(` ${chalk4.red("\u25CB")} OpenCode: ${chalk4.red("Not connected")}`);
744
+ }
745
+ lines.push("");
746
+ if (state.activityLog.length > 0) {
747
+ lines.push(chalk4.bold(" Activity:"));
748
+ for (const entry of state.activityLog) {
749
+ lines.push(formatActivityEntry(entry, state.verbose));
750
+ }
751
+ } else {
752
+ lines.push(chalk4.dim(" No activity yet. Waiting for requests..."));
753
+ }
754
+ lines.push("");
755
+ lines.push(chalk4.dim("\u2500".repeat(60)));
756
+ if (state.verbose) {
757
+ lines.push(chalk4.dim(" Verbose mode: ON (request/response bodies will be logged)"));
758
+ }
759
+ lines.push(chalk4.dim(" Press Ctrl+C to disconnect"));
760
+ while (lines.length < STATUS_DISPLAY_HEIGHT) {
761
+ lines.push("");
762
+ }
763
+ if (!state.displayInitialized) {
764
+ console.log("");
765
+ console.log(chalk4.dim("\u2550".repeat(60)));
766
+ console.log("");
767
+ for (const line of lines) {
768
+ console.log(line);
769
+ }
770
+ state.displayInitialized = true;
771
+ } else {
772
+ process.stdout.write(ANSI.moveUp(STATUS_DISPLAY_HEIGHT + 3));
773
+ console.log(chalk4.dim("\u2550".repeat(60)));
774
+ console.log("");
775
+ for (const line of lines) {
776
+ process.stdout.write("\x1B[2K");
777
+ console.log(line);
778
+ }
779
+ }
780
+ }
781
+ function displayError(_state, error2, details) {
782
+ blank();
783
+ console.log(chalk4.bgRed.white.bold(" ERROR "));
784
+ console.log(chalk4.red(` ${error2}`));
785
+ if (details) {
786
+ console.log(chalk4.dim(` ${details}`));
787
+ }
461
788
  blank();
462
- console.log(chalk4.dim("Press Ctrl+C to disconnect"));
463
789
  }
464
- async function forwardToOpenCode(port, request) {
790
+ async function validateSandbox(token, sandboxId) {
791
+ const apiUrl = getApiUrlConfig();
792
+ try {
793
+ const response = await fetch(`${apiUrl}/sandboxes/${sandboxId}`, {
794
+ headers: {
795
+ Authorization: `Bearer ${token}`
796
+ }
797
+ });
798
+ if (response.status === 404) {
799
+ return { valid: false, error: "Sandbox not found" };
800
+ }
801
+ if (response.status === 401) {
802
+ return { valid: false, error: "Authentication failed. Please run `evident login` again." };
803
+ }
804
+ if (!response.ok) {
805
+ return { valid: false, error: `API error: ${response.status}` };
806
+ }
807
+ const sandbox = await response.json();
808
+ if (sandbox.sandbox_type !== "remote") {
809
+ return {
810
+ valid: false,
811
+ error: `Sandbox is type '${sandbox.sandbox_type}', must be 'remote' for tunnel connection`
812
+ };
813
+ }
814
+ return { valid: true, name: sandbox.name };
815
+ } catch (error2) {
816
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
817
+ return { valid: false, error: `Failed to validate sandbox: ${message}` };
818
+ }
819
+ }
820
+ async function forwardToOpenCode(port, request, requestId, state) {
465
821
  const url = `http://localhost:${port}${request.path}`;
822
+ const startTime = Date.now();
823
+ state.pendingRequests.set(requestId, {
824
+ startTime,
825
+ method: request.method,
826
+ path: request.path
827
+ });
828
+ logActivity(state, {
829
+ type: "request",
830
+ method: request.method,
831
+ path: request.path,
832
+ requestId
833
+ });
834
+ displayStatus(state);
835
+ if (state.verbose && request.body) {
836
+ console.log(chalk4.dim(` Request body: ${JSON.stringify(request.body, null, 2)}`));
837
+ }
838
+ telemetry.debug(
839
+ EventTypes.OPENCODE_REQUEST_FORWARDED,
840
+ `Forwarding ${request.method} ${request.path}`,
841
+ {
842
+ method: request.method,
843
+ path: request.path,
844
+ port,
845
+ requestId
846
+ },
847
+ state.sandboxId ?? void 0
848
+ );
466
849
  try {
467
850
  const response = await fetch(url, {
468
851
  method: request.method,
@@ -474,26 +857,230 @@ async function forwardToOpenCode(port, request) {
474
857
  });
475
858
  let body;
476
859
  const contentType = response.headers.get("Content-Type");
477
- if (contentType?.includes("application/json")) {
478
- body = await response.json();
860
+ const text = await response.text();
861
+ if (!text || text.length === 0) {
862
+ body = null;
863
+ } else if (contentType?.includes("application/json")) {
864
+ try {
865
+ body = JSON.parse(text);
866
+ } catch {
867
+ body = text;
868
+ }
479
869
  } else {
480
- body = await response.text();
870
+ body = text;
871
+ }
872
+ const durationMs = Date.now() - startTime;
873
+ state.pendingRequests.delete(requestId);
874
+ if (!state.opencodeConnected) {
875
+ state.opencodeConnected = true;
481
876
  }
877
+ const lastEntry = state.activityLog[state.activityLog.length - 1];
878
+ if (lastEntry && lastEntry.requestId === requestId) {
879
+ lastEntry.type = "response";
880
+ lastEntry.status = response.status;
881
+ lastEntry.durationMs = durationMs;
882
+ } else {
883
+ logActivity(state, {
884
+ type: "response",
885
+ method: request.method,
886
+ path: request.path,
887
+ status: response.status,
888
+ durationMs,
889
+ requestId
890
+ });
891
+ }
892
+ displayStatus(state);
893
+ if (state.verbose && body) {
894
+ const bodyStr = typeof body === "string" ? body : JSON.stringify(body, null, 2);
895
+ const truncated = bodyStr.length > 500 ? bodyStr.substring(0, 500) + "..." : bodyStr;
896
+ console.log(chalk4.dim(` Response body: ${truncated}`));
897
+ }
898
+ telemetry.debug(
899
+ EventTypes.OPENCODE_RESPONSE_SENT,
900
+ `Response ${response.status}`,
901
+ {
902
+ status: response.status,
903
+ path: request.path,
904
+ durationMs,
905
+ requestId
906
+ },
907
+ state.sandboxId ?? void 0
908
+ );
482
909
  return {
483
910
  status: response.status,
484
911
  body
485
912
  };
486
913
  } catch (error2) {
487
914
  const message = error2 instanceof Error ? error2.message : "Unknown error";
915
+ const durationMs = Date.now() - startTime;
916
+ state.pendingRequests.delete(requestId);
917
+ state.opencodeConnected = false;
918
+ logActivity(state, {
919
+ type: "error",
920
+ method: request.method,
921
+ path: request.path,
922
+ error: `OpenCode unreachable: ${message}`,
923
+ durationMs,
924
+ requestId
925
+ });
926
+ displayStatus(state);
927
+ telemetry.error(
928
+ EventTypes.OPENCODE_UNREACHABLE,
929
+ `Failed to connect to OpenCode: ${message}`,
930
+ {
931
+ port,
932
+ path: request.path,
933
+ error: message,
934
+ requestId
935
+ },
936
+ state.sandboxId ?? void 0
937
+ );
488
938
  return {
489
939
  status: 502,
490
940
  body: { error: "Failed to connect to OpenCode", message }
491
941
  };
492
942
  }
493
943
  }
944
+ async function subscribeToOpenCodeEvents(port, subscriptionId, ws, state) {
945
+ const url = `http://localhost:${port}/event`;
946
+ logActivity(state, {
947
+ type: "info",
948
+ message: `Starting event subscription ${subscriptionId.slice(0, 8)}`
949
+ });
950
+ displayStatus(state);
951
+ const abortController = new AbortController();
952
+ state.activeEventSubscriptions.set(subscriptionId, abortController);
953
+ try {
954
+ const response = await fetch(url, {
955
+ headers: { Accept: "text/event-stream" },
956
+ signal: abortController.signal
957
+ });
958
+ if (!response.ok) {
959
+ throw new Error(`Failed to connect to OpenCode events: ${response.status}`);
960
+ }
961
+ if (!response.body) {
962
+ throw new Error("No response body");
963
+ }
964
+ const reader = response.body.getReader();
965
+ const decoder = new TextDecoder();
966
+ let buffer = "";
967
+ while (true) {
968
+ const { done, value } = await reader.read();
969
+ if (done) {
970
+ ws.send(JSON.stringify({ type: "event_end", id: subscriptionId }));
971
+ break;
972
+ }
973
+ buffer += decoder.decode(value, { stream: true });
974
+ const lines = buffer.split("\n");
975
+ buffer = lines.pop() || "";
976
+ for (const line of lines) {
977
+ if (line.startsWith("data: ")) {
978
+ try {
979
+ const event = JSON.parse(line.slice(6));
980
+ ws.send(JSON.stringify({ type: "event", id: subscriptionId, event }));
981
+ } catch {
982
+ }
983
+ }
984
+ }
985
+ }
986
+ } catch (error2) {
987
+ if (abortController.signal.aborted) {
988
+ return;
989
+ }
990
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
991
+ logActivity(state, {
992
+ type: "error",
993
+ error: `Event subscription failed: ${message}`
994
+ });
995
+ displayStatus(state);
996
+ ws.send(JSON.stringify({ type: "event_error", id: subscriptionId, error: message }));
997
+ } finally {
998
+ state.activeEventSubscriptions.delete(subscriptionId);
999
+ }
1000
+ }
1001
+ function cancelEventSubscription(subscriptionId, state) {
1002
+ const controller = state.activeEventSubscriptions.get(subscriptionId);
1003
+ if (controller) {
1004
+ controller.abort();
1005
+ state.activeEventSubscriptions.delete(subscriptionId);
1006
+ }
1007
+ }
1008
+ function sendResponse(ws, requestId, response) {
1009
+ const bodyStr = JSON.stringify(response.body ?? null);
1010
+ const bodyBytes = Buffer.from(bodyStr, "utf-8");
1011
+ if (bodyBytes.length < CHUNK_THRESHOLD) {
1012
+ ws.send(
1013
+ JSON.stringify({
1014
+ type: "response",
1015
+ id: requestId,
1016
+ payload: response
1017
+ })
1018
+ );
1019
+ return;
1020
+ }
1021
+ sendResponseAsChunks(ws, requestId, response, bodyBytes);
1022
+ }
1023
+ function sendResponseAsChunks(ws, requestId, response, bodyBytes) {
1024
+ const chunks = splitIntoChunks(bodyBytes, CHUNK_SIZE);
1025
+ ws.send(
1026
+ JSON.stringify({
1027
+ type: "response_start",
1028
+ id: requestId,
1029
+ total_chunks: chunks.length,
1030
+ total_size: bodyBytes.length,
1031
+ payload: {
1032
+ status: response.status,
1033
+ headers: response.headers
1034
+ }
1035
+ })
1036
+ );
1037
+ for (let i = 0; i < chunks.length; i++) {
1038
+ ws.send(
1039
+ JSON.stringify({
1040
+ type: "response_chunk",
1041
+ id: requestId,
1042
+ chunk_index: i,
1043
+ data: chunks[i].toString("base64")
1044
+ })
1045
+ );
1046
+ }
1047
+ ws.send(
1048
+ JSON.stringify({
1049
+ type: "response_end",
1050
+ id: requestId
1051
+ })
1052
+ );
1053
+ }
1054
+ function splitIntoChunks(data, chunkSize) {
1055
+ const chunks = [];
1056
+ for (let i = 0; i < data.length; i += chunkSize) {
1057
+ chunks.push(data.subarray(i, i + chunkSize));
1058
+ }
1059
+ return chunks;
1060
+ }
1061
+ function getReconnectDelay(attempt) {
1062
+ const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
1063
+ const jitter = Math.random() * 1e3;
1064
+ return Math.min(exponentialDelay + jitter, MAX_RECONNECT_DELAY);
1065
+ }
494
1066
  async function connect(token, sandboxId, port, state) {
495
1067
  const tunnelUrl = getTunnelUrlConfig();
496
- const url = sandboxId ? `${tunnelUrl}/tunnel/${sandboxId}/connect` : `${tunnelUrl}/tunnel/default/connect`;
1068
+ const url = `${tunnelUrl}/tunnel/${sandboxId}/connect`;
1069
+ logActivity(state, {
1070
+ type: "info",
1071
+ message: "Connecting to tunnel relay..."
1072
+ });
1073
+ displayStatus(state);
1074
+ telemetry.info(
1075
+ EventTypes.TUNNEL_STARTING,
1076
+ `Connecting to ${url}`,
1077
+ {
1078
+ sandboxId,
1079
+ port,
1080
+ tunnelUrl
1081
+ },
1082
+ sandboxId
1083
+ );
497
1084
  return new Promise((resolve, reject) => {
498
1085
  const ws = new WebSocket(url, {
499
1086
  headers: {
@@ -503,20 +1090,47 @@ async function connect(token, sandboxId, port, state) {
503
1090
  ws.on("open", () => {
504
1091
  state.connected = true;
505
1092
  state.reconnectAttempt = 0;
506
- state.lastActivity = /* @__PURE__ */ new Date();
1093
+ logActivity(state, {
1094
+ type: "info",
1095
+ message: "WebSocket connection established"
1096
+ });
507
1097
  displayStatus(state);
508
1098
  });
509
1099
  ws.on("message", async (data) => {
510
1100
  try {
511
1101
  const message = JSON.parse(data.toString());
512
- state.lastActivity = /* @__PURE__ */ new Date();
513
1102
  switch (message.type) {
514
1103
  case "connected":
515
- state.sandboxId = message.sandbox_id ?? null;
1104
+ state.sandboxId = message.sandbox_id ?? sandboxId;
1105
+ logActivity(state, {
1106
+ type: "info",
1107
+ message: `Tunnel connected (sandbox: ${state.sandboxId})`
1108
+ });
1109
+ telemetry.info(
1110
+ EventTypes.TUNNEL_CONNECTED,
1111
+ `Tunnel connected`,
1112
+ {
1113
+ sandboxId: message.sandbox_id
1114
+ },
1115
+ message.sandbox_id
1116
+ );
516
1117
  displayStatus(state);
517
1118
  break;
518
1119
  case "error":
519
- printError(`Tunnel error: ${message.message}`);
1120
+ logActivity(state, {
1121
+ type: "error",
1122
+ error: message.message || "Unknown tunnel error"
1123
+ });
1124
+ telemetry.error(
1125
+ EventTypes.TUNNEL_ERROR,
1126
+ `Tunnel error: ${message.message}`,
1127
+ {
1128
+ code: message.code,
1129
+ message: message.message
1130
+ },
1131
+ state.sandboxId ?? void 0
1132
+ );
1133
+ displayStatus(state);
520
1134
  if (message.code === "unauthorized") {
521
1135
  ws.close();
522
1136
  reject(new Error("Unauthorized"));
@@ -527,81 +1141,440 @@ async function connect(token, sandboxId, port, state) {
527
1141
  break;
528
1142
  case "request":
529
1143
  if (message.id && message.payload) {
530
- const response = await forwardToOpenCode(port, message.payload);
531
- ws.send(
532
- JSON.stringify({
533
- type: "response",
534
- id: message.id,
535
- payload: response
536
- })
1144
+ telemetry.debug(
1145
+ EventTypes.OPENCODE_REQUEST_RECEIVED,
1146
+ `Request: ${message.payload.method} ${message.payload.path}`,
1147
+ {
1148
+ requestId: message.id,
1149
+ method: message.payload.method,
1150
+ path: message.payload.path
1151
+ },
1152
+ state.sandboxId ?? void 0
537
1153
  );
538
- displayStatus(state);
1154
+ const response = await forwardToOpenCode(port, message.payload, message.id, state);
1155
+ sendResponse(ws, message.id, response);
1156
+ }
1157
+ break;
1158
+ case "subscribe_events":
1159
+ if (message.id) {
1160
+ void subscribeToOpenCodeEvents(port, message.id, ws, state);
1161
+ }
1162
+ break;
1163
+ case "unsubscribe_events":
1164
+ if (message.id) {
1165
+ cancelEventSubscription(message.id, state);
539
1166
  }
540
1167
  break;
541
1168
  }
542
1169
  } catch (error2) {
543
- console.error("Failed to handle message:", error2);
1170
+ const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
1171
+ logActivity(state, {
1172
+ type: "error",
1173
+ error: `Failed to handle message: ${errorMessage}`
1174
+ });
1175
+ telemetry.error(
1176
+ EventTypes.TUNNEL_ERROR,
1177
+ `Failed to handle message: ${errorMessage}`,
1178
+ {
1179
+ error: errorMessage
1180
+ },
1181
+ state.sandboxId ?? void 0
1182
+ );
1183
+ displayStatus(state);
544
1184
  }
545
1185
  });
546
- ws.on("close", () => {
1186
+ ws.on("close", (code, reason) => {
547
1187
  state.connected = false;
1188
+ const reasonStr = reason.toString() || "No reason provided";
1189
+ logActivity(state, {
1190
+ type: "info",
1191
+ message: `Disconnected (code: ${code}, reason: ${reasonStr})`
1192
+ });
1193
+ telemetry.info(
1194
+ EventTypes.TUNNEL_DISCONNECTED,
1195
+ "Tunnel disconnected",
1196
+ {
1197
+ sandboxId: state.sandboxId,
1198
+ code,
1199
+ reason: reasonStr
1200
+ },
1201
+ state.sandboxId ?? void 0
1202
+ );
548
1203
  displayStatus(state);
549
1204
  resolve();
550
1205
  });
551
1206
  ws.on("error", (error2) => {
552
1207
  state.connected = false;
1208
+ logActivity(state, {
1209
+ type: "error",
1210
+ error: `Connection error: ${error2.message}`
1211
+ });
1212
+ telemetry.error(
1213
+ EventTypes.TUNNEL_ERROR,
1214
+ `Connection error: ${error2.message}`,
1215
+ {
1216
+ error: error2.message
1217
+ },
1218
+ state.sandboxId ?? void 0
1219
+ );
553
1220
  displayStatus(state);
554
- console.error(chalk4.dim(`Connection error: ${error2.message}`));
555
1221
  });
556
- const cleanup = () => {
1222
+ const cleanup = async () => {
1223
+ process.removeAllListeners("SIGINT");
1224
+ process.removeAllListeners("SIGTERM");
1225
+ logActivity(state, {
1226
+ type: "info",
1227
+ message: "Shutting down..."
1228
+ });
1229
+ displayStatus(state);
1230
+ telemetry.info(
1231
+ EventTypes.TUNNEL_DISCONNECTED,
1232
+ "Tunnel stopped by user",
1233
+ {
1234
+ sandboxId: state.sandboxId
1235
+ },
1236
+ state.sandboxId ?? void 0
1237
+ );
1238
+ await shutdownTelemetry();
557
1239
  ws.close();
558
1240
  process.exit(0);
559
1241
  };
560
- process.on("SIGINT", cleanup);
561
- process.on("SIGTERM", cleanup);
1242
+ process.removeAllListeners("SIGINT");
1243
+ process.removeAllListeners("SIGTERM");
1244
+ process.once("SIGINT", () => void cleanup());
1245
+ process.once("SIGTERM", () => void cleanup());
562
1246
  });
563
1247
  }
564
1248
  async function tunnel(options) {
565
- const credentials2 = await getToken();
566
- if (!credentials2) {
567
- printError("Not logged in. Run the `login` command first.");
1249
+ const verbose = options.verbose ?? false;
1250
+ const credentials = await getToken();
1251
+ if (!credentials) {
1252
+ const cliName = (await import("./config-J7LPYFVS.js")).getCliName();
1253
+ telemetry.error(EventTypes.CLI_ERROR, "Not logged in", { command: "tunnel" });
1254
+ printError(`Not logged in. Run \`${cliName} login\` first.`);
568
1255
  process.exit(1);
569
1256
  }
570
1257
  const port = options.port ?? 4096;
571
1258
  const sandboxId = options.sandbox;
572
- const spinner = ora2("Checking OpenCode connection...").start();
573
- try {
574
- const response = await fetch(`http://localhost:${port}/health`);
575
- if (!response.ok) {
576
- throw new Error("Health check failed");
577
- }
578
- spinner.succeed(`OpenCode detected on port ${port}`);
579
- } catch {
580
- spinner.warn(`Could not connect to OpenCode on port ${port}`);
581
- printWarning("Make sure OpenCode is running before starting the tunnel.");
1259
+ if (!sandboxId) {
1260
+ const cliName = (await import("./config-J7LPYFVS.js")).getCliName();
1261
+ printError("--sandbox <id> is required");
582
1262
  blank();
1263
+ console.log(chalk4.dim("To find your sandbox ID:"));
1264
+ console.log(chalk4.dim(" 1. Create a remote sandbox in the Evident web UI"));
1265
+ console.log(chalk4.dim(" 2. Copy the sandbox ID from the URL or settings"));
1266
+ console.log(chalk4.dim(` 3. Run: ${cliName} tunnel --sandbox <id>`));
1267
+ blank();
1268
+ telemetry.error(EventTypes.CLI_ERROR, "Missing sandbox ID", { command: "tunnel" });
1269
+ process.exit(1);
583
1270
  }
584
1271
  const state = {
585
1272
  connected: false,
586
- sandboxId: null,
1273
+ opencodeConnected: false,
1274
+ opencodeVersion: null,
1275
+ sandboxId,
1276
+ sandboxName: null,
587
1277
  reconnectAttempt: 0,
588
- lastActivity: /* @__PURE__ */ new Date()
1278
+ lastActivity: /* @__PURE__ */ new Date(),
1279
+ activityLog: [],
1280
+ pendingRequests: /* @__PURE__ */ new Map(),
1281
+ verbose,
1282
+ displayInitialized: false,
1283
+ activeEventSubscriptions: /* @__PURE__ */ new Map()
589
1284
  };
1285
+ telemetry.info(
1286
+ EventTypes.CLI_COMMAND,
1287
+ "Starting tunnel command",
1288
+ {
1289
+ command: "tunnel",
1290
+ port,
1291
+ sandboxId,
1292
+ verbose
1293
+ },
1294
+ sandboxId
1295
+ );
1296
+ logActivity(state, {
1297
+ type: "info",
1298
+ message: `Starting tunnel (port: ${port}, verbose: ${verbose})`
1299
+ });
1300
+ logActivity(state, {
1301
+ type: "info",
1302
+ message: "Validating sandbox..."
1303
+ });
1304
+ const validateSpinner = ora2("Validating sandbox...").start();
1305
+ const validation = await validateSandbox(credentials.token, sandboxId);
1306
+ if (!validation.valid) {
1307
+ validateSpinner.fail(`Sandbox validation failed: ${validation.error}`);
1308
+ logActivity(state, {
1309
+ type: "error",
1310
+ error: `Sandbox validation failed: ${validation.error}`
1311
+ });
1312
+ telemetry.error(EventTypes.CLI_ERROR, `Sandbox validation failed: ${validation.error}`, {
1313
+ command: "tunnel",
1314
+ sandboxId
1315
+ });
1316
+ displayStatus(state);
1317
+ process.exit(1);
1318
+ }
1319
+ state.sandboxName = validation.name ?? null;
1320
+ validateSpinner.succeed(`Sandbox: ${validation.name || sandboxId}`);
1321
+ logActivity(state, {
1322
+ type: "info",
1323
+ message: `Sandbox validated: ${validation.name || sandboxId}`
1324
+ });
1325
+ logActivity(state, {
1326
+ type: "info",
1327
+ message: `Checking OpenCode on port ${port}...`
1328
+ });
1329
+ const opencodeSpinner = ora2("Checking OpenCode connection...").start();
1330
+ const healthCheck = await checkOpenCodeHealth(port);
1331
+ if (healthCheck.healthy) {
1332
+ state.opencodeConnected = true;
1333
+ state.opencodeVersion = healthCheck.version ?? null;
1334
+ const version = healthCheck.version ? ` (v${healthCheck.version})` : "";
1335
+ telemetry.info(
1336
+ EventTypes.OPENCODE_HEALTH_OK,
1337
+ `OpenCode healthy on port ${port}`,
1338
+ {
1339
+ port,
1340
+ version: healthCheck.version
1341
+ },
1342
+ sandboxId
1343
+ );
1344
+ opencodeSpinner.succeed(`OpenCode running on port ${port}${version}`);
1345
+ logActivity(state, {
1346
+ type: "info",
1347
+ message: `OpenCode running on port ${port}${version}`
1348
+ });
1349
+ } else {
1350
+ telemetry.warn(
1351
+ EventTypes.OPENCODE_HEALTH_FAILED,
1352
+ `Could not connect to OpenCode: ${healthCheck.error}`,
1353
+ {
1354
+ port,
1355
+ error: healthCheck.error
1356
+ },
1357
+ sandboxId
1358
+ );
1359
+ opencodeSpinner.warn(`Could not connect to OpenCode on port ${port}`);
1360
+ logActivity(state, {
1361
+ type: "error",
1362
+ error: `OpenCode not reachable on port ${port}: ${healthCheck.error}`
1363
+ });
1364
+ const runningInstances = await findHealthyOpenCodeInstances();
1365
+ if (runningInstances.length > 0) {
1366
+ blank();
1367
+ console.log(chalk4.yellow("Found OpenCode running on different port(s):"));
1368
+ for (const instance of runningInstances) {
1369
+ const ver = instance.version ? ` (v${instance.version})` : "";
1370
+ const cwd = instance.cwd ? ` in ${instance.cwd}` : "";
1371
+ console.log(chalk4.dim(` \u2022 Port ${instance.port}${ver}${cwd}`));
1372
+ }
1373
+ blank();
1374
+ if (runningInstances.length === 1) {
1375
+ console.log(chalk4.yellow("Tip: Run with the correct port:"));
1376
+ console.log(
1377
+ chalk4.dim(
1378
+ ` npx @evident-ai/cli@latest tunnel --sandbox ${sandboxId} --port ${runningInstances[0].port}`
1379
+ )
1380
+ );
1381
+ } else {
1382
+ console.log(chalk4.yellow("Tip: Specify which port to use:"));
1383
+ console.log(
1384
+ chalk4.dim(` npx @evident-ai/cli@latest tunnel --sandbox ${sandboxId} --port <PORT>`)
1385
+ );
1386
+ }
1387
+ blank();
1388
+ } else {
1389
+ blank();
1390
+ const action = await select({
1391
+ message: "OpenCode is not running. What would you like to do?",
1392
+ choices: [
1393
+ {
1394
+ name: "Start OpenCode for me",
1395
+ value: "start",
1396
+ description: `Run 'opencode serve --port ${port}' in a new process`
1397
+ },
1398
+ {
1399
+ name: "Show me the command to run",
1400
+ value: "manual",
1401
+ description: "Display instructions for starting OpenCode manually"
1402
+ },
1403
+ {
1404
+ name: "Continue without OpenCode",
1405
+ value: "continue",
1406
+ description: "Connect the tunnel anyway (requests will fail until OpenCode starts)"
1407
+ }
1408
+ ]
1409
+ });
1410
+ if (action === "start") {
1411
+ let actualPort = port;
1412
+ if (isPortInUse(port)) {
1413
+ console.log(chalk4.yellow(`
1414
+ Port ${port} is already in use by another process.`));
1415
+ const alternativePort = findAvailablePort(port + 1);
1416
+ if (alternativePort) {
1417
+ const useAlternative = await select({
1418
+ message: `Would you like to use port ${alternativePort} instead?`,
1419
+ choices: [
1420
+ { name: `Yes, use port ${alternativePort}`, value: "yes" },
1421
+ { name: "No, I will free up the port manually", value: "no" }
1422
+ ]
1423
+ });
1424
+ if (useAlternative === "yes") {
1425
+ actualPort = alternativePort;
1426
+ } else {
1427
+ console.log(chalk4.dim(`
1428
+ Free up port ${port} and run the tunnel command again.`));
1429
+ blank();
1430
+ process.exit(1);
1431
+ }
1432
+ } else {
1433
+ console.log(
1434
+ chalk4.red(
1435
+ `Could not find an available port. Please free up port ${port} and try again.`
1436
+ )
1437
+ );
1438
+ blank();
1439
+ process.exit(1);
1440
+ }
1441
+ }
1442
+ const opencodeStartSpinner = ora2(`Starting OpenCode on port ${actualPort}...`).start();
1443
+ try {
1444
+ let command = "opencode";
1445
+ let args = ["serve", "--port", actualPort.toString()];
1446
+ try {
1447
+ execSync("which opencode", { stdio: "ignore" });
1448
+ } catch {
1449
+ command = "npx";
1450
+ args = ["opencode", "serve", "--port", actualPort.toString()];
1451
+ }
1452
+ const child = spawn(command, args, {
1453
+ detached: true,
1454
+ stdio: "ignore",
1455
+ cwd: process.cwd()
1456
+ // Start in current working directory
1457
+ });
1458
+ child.unref();
1459
+ const maxRetries = 10;
1460
+ const retryDelayMs = 1e3;
1461
+ let healthy = false;
1462
+ let version;
1463
+ for (let i = 0; i < maxRetries; i++) {
1464
+ await sleep(retryDelayMs);
1465
+ const retryHealth = await checkOpenCodeHealth(actualPort);
1466
+ if (retryHealth.healthy) {
1467
+ healthy = true;
1468
+ version = retryHealth.version;
1469
+ break;
1470
+ }
1471
+ opencodeStartSpinner.text = `Starting OpenCode on port ${actualPort}... (${i + 1}/${maxRetries})`;
1472
+ }
1473
+ if (healthy) {
1474
+ state.opencodeConnected = true;
1475
+ state.opencodeVersion = version ?? null;
1476
+ const versionStr = version ? ` (v${version})` : "";
1477
+ opencodeStartSpinner.succeed(`OpenCode started on port ${actualPort}${versionStr}`);
1478
+ logActivity(state, {
1479
+ type: "info",
1480
+ message: `OpenCode started on port ${actualPort}${versionStr}`
1481
+ });
1482
+ } else {
1483
+ opencodeStartSpinner.warn(
1484
+ "OpenCode process started but not responding. Check if it started correctly."
1485
+ );
1486
+ logActivity(state, {
1487
+ type: "info",
1488
+ message: "OpenCode may still be starting..."
1489
+ });
1490
+ console.log(chalk4.dim("\nTip: Check for errors by running OpenCode manually:"));
1491
+ console.log(chalk4.dim(` opencode serve --port ${actualPort}`));
1492
+ blank();
1493
+ }
1494
+ } catch (error2) {
1495
+ const msg = error2 instanceof Error ? error2.message : "Unknown error";
1496
+ opencodeStartSpinner.fail(`Failed to start OpenCode: ${msg}`);
1497
+ logActivity(state, {
1498
+ type: "error",
1499
+ error: `Failed to start OpenCode: ${msg}`
1500
+ });
1501
+ console.log(chalk4.yellow("\nYou can try starting it manually:"));
1502
+ console.log(chalk4.dim(` opencode serve --port ${actualPort}`));
1503
+ blank();
1504
+ }
1505
+ } else if (action === "manual") {
1506
+ blank();
1507
+ console.log(chalk4.yellow("To start OpenCode, run one of these commands:"));
1508
+ blank();
1509
+ console.log(chalk4.dim(" # Start OpenCode in your project directory:"));
1510
+ console.log(chalk4.dim(` opencode serve --port ${port}`));
1511
+ blank();
1512
+ console.log(chalk4.dim(" # Or if you have OpenCode installed globally:"));
1513
+ console.log(chalk4.dim(` npx opencode serve --port ${port}`));
1514
+ blank();
1515
+ console.log(
1516
+ chalk4.dim("The tunnel will automatically forward requests once OpenCode is running.")
1517
+ );
1518
+ blank();
1519
+ }
1520
+ }
1521
+ }
590
1522
  while (true) {
591
1523
  try {
592
- await connect(credentials2.token, sandboxId, port, state);
1524
+ await connect(credentials.token, sandboxId, port, state);
593
1525
  state.reconnectAttempt++;
594
1526
  const delay = getReconnectDelay(state.reconnectAttempt);
1527
+ logActivity(state, {
1528
+ type: "info",
1529
+ message: `Reconnecting in ${Math.round(delay / 1e3)}s (attempt ${state.reconnectAttempt})...`
1530
+ });
1531
+ telemetry.info(
1532
+ EventTypes.TUNNEL_RECONNECTING,
1533
+ `Reconnecting (attempt ${state.reconnectAttempt})`,
1534
+ {
1535
+ attempt: state.reconnectAttempt,
1536
+ delayMs: delay
1537
+ },
1538
+ state.sandboxId ?? void 0
1539
+ );
595
1540
  displayStatus(state);
596
1541
  await sleep(delay);
597
1542
  } catch (error2) {
598
1543
  const message = error2 instanceof Error ? error2.message : "Unknown error";
599
1544
  if (message === "Unauthorized") {
600
- printError("Authentication failed. Please run the `login` command again.");
1545
+ telemetry.error(
1546
+ EventTypes.CLI_ERROR,
1547
+ "Authentication failed",
1548
+ {
1549
+ command: "tunnel",
1550
+ error: message
1551
+ },
1552
+ state.sandboxId ?? void 0
1553
+ );
1554
+ await shutdownTelemetry();
1555
+ displayError(state, "Authentication failed", "Please run `evident login` again.");
601
1556
  process.exit(1);
602
1557
  }
1558
+ logActivity(state, {
1559
+ type: "error",
1560
+ error: message
1561
+ });
1562
+ telemetry.error(
1563
+ EventTypes.TUNNEL_ERROR,
1564
+ `Tunnel error: ${message}`,
1565
+ {
1566
+ error: message,
1567
+ attempt: state.reconnectAttempt
1568
+ },
1569
+ state.sandboxId ?? void 0
1570
+ );
603
1571
  state.reconnectAttempt++;
604
1572
  const delay = getReconnectDelay(state.reconnectAttempt);
1573
+ logActivity(state, {
1574
+ type: "info",
1575
+ message: `Reconnecting in ${Math.round(delay / 1e3)}s (attempt ${state.reconnectAttempt})...`
1576
+ });
1577
+ displayStatus(state);
605
1578
  await sleep(delay);
606
1579
  }
607
1580
  }
@@ -618,10 +1591,11 @@ program.name("evident").description("Run OpenCode locally and connect it to Evid
618
1591
  program.command("login").description("Authenticate with Evident").option("--token", "Use token-based authentication (for CI/CD)").option("--no-browser", "Do not open the browser automatically").action(login);
619
1592
  program.command("logout").description("Remove stored credentials").action(logout);
620
1593
  program.command("whoami").description("Show the currently logged in user").action(whoami);
621
- program.command("tunnel").description("Establish a tunnel to Evident for Local Mode").option("-s, --sandbox <id>", "Sandbox ID to connect to").option("-p, --port <port>", "OpenCode port (default: 4096)", "4096").action((options) => {
1594
+ program.command("tunnel").description("Establish a tunnel to Evident for Local Mode").requiredOption("-s, --sandbox <id>", "Sandbox ID to connect to (required)").option("-p, --port <port>", "OpenCode port (default: 4096)", "4096").option("-v, --verbose", "Show detailed request/response information").action((options) => {
622
1595
  tunnel({
623
1596
  sandbox: options.sandbox,
624
- port: parseInt(options.port, 10)
1597
+ port: parseInt(options.port, 10),
1598
+ verbose: options.verbose
625
1599
  });
626
1600
  });
627
1601
  program.parse();