@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/chunk-MWOWXSOP.js +120 -0
- package/dist/chunk-MWOWXSOP.js.map +1 -0
- package/dist/config-J7LPYFVS.js +30 -0
- package/dist/config-J7LPYFVS.js.map +1 -0
- package/dist/index.js +1116 -142
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
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(
|
|
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(
|
|
129
|
+
await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(credentials));
|
|
195
130
|
} else {
|
|
196
131
|
setCredentials({
|
|
197
|
-
token:
|
|
198
|
-
user:
|
|
199
|
-
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
|
|
401
|
-
if (!
|
|
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
|
|
413
|
-
if (!
|
|
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(
|
|
419
|
-
console.log(keyValue("User ID",
|
|
420
|
-
if (
|
|
421
|
-
const expiresAt = new Date(
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
478
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
561
|
-
process.
|
|
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
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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").
|
|
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();
|