@liveport/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @liveport/cli
2
+
3
+ Command-line interface for creating secure localhost tunnels with LivePort.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @liveport/cli
9
+ # or
10
+ pnpm add -g @liveport/cli
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ # Connect a local port (e.g., a dev server on port 3000)
17
+ liveport connect 3000 --key YOUR_BRIDGE_KEY
18
+
19
+ # Check tunnel status
20
+ liveport status
21
+
22
+ # Disconnect
23
+ liveport disconnect
24
+ ```
25
+
26
+ ## Commands
27
+
28
+ ### `liveport connect <port>`
29
+
30
+ Create a tunnel to expose a local port.
31
+
32
+ **Options:**
33
+ - `-k, --key <key>` - Bridge key for authentication
34
+ - `-s, --server <url>` - Tunnel server URL
35
+ - `-r, --region <region>` - Server region
36
+
37
+ **Example:**
38
+ ```bash
39
+ liveport connect 3000 --key bk_abc123
40
+ ```
41
+
42
+ ### `liveport status`
43
+
44
+ Show current tunnel status including URL and connection details.
45
+
46
+ ### `liveport disconnect`
47
+
48
+ Disconnect active tunnel.
49
+
50
+ **Options:**
51
+ - `-a, --all` - Disconnect all tunnels
52
+
53
+ ### `liveport config`
54
+
55
+ Manage CLI configuration.
56
+
57
+ ```bash
58
+ # Set default bridge key
59
+ liveport config set key bk_abc123
60
+
61
+ # Set default server (optional - defaults to tunnel.liveport.dev)
62
+ liveport config set server https://tunnel.liveport.dev
63
+
64
+ # View config
65
+ liveport config list
66
+
67
+ # Get specific value
68
+ liveport config get key
69
+
70
+ # Delete config value
71
+ liveport config delete key
72
+ ```
73
+
74
+ ## Environment Variables
75
+
76
+ - `LIVEPORT_KEY` - Default bridge key
77
+ - `LIVEPORT_SERVER_URL` - Default tunnel server URL
78
+
79
+ ## How It Works
80
+
81
+ 1. Run `liveport connect <port>` to establish a WebSocket connection to the LivePort tunnel server
82
+ 2. The server assigns a unique subdomain (e.g., `abc123.liveport.online`)
83
+ 3. Incoming requests to that URL are proxied through the tunnel to your local port
84
+ 4. AI agents can discover your tunnel using the `@liveport/agent-sdk`
85
+
86
+ ## License
87
+
88
+ MIT
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,759 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/connect.ts
7
+ import ora from "ora";
8
+
9
+ // src/tunnel-client.ts
10
+ import WebSocket from "ws";
11
+ import http from "http";
12
+ var DEFAULT_HEARTBEAT_INTERVAL = 1e4;
13
+ var DEFAULT_RECONNECT_MAX_ATTEMPTS = 5;
14
+ var DEFAULT_RECONNECT_BASE_DELAY = 1e3;
15
+ var TunnelClient = class {
16
+ config;
17
+ socket = null;
18
+ state = "disconnected";
19
+ tunnelInfo = null;
20
+ heartbeatTimer = null;
21
+ reconnectTimer = null;
22
+ reconnectAttempts = 0;
23
+ requestCount = 0;
24
+ shouldReconnect = true;
25
+ // Event handlers
26
+ onConnected = null;
27
+ onDisconnected = null;
28
+ onReconnecting = null;
29
+ onError = null;
30
+ onRequest = null;
31
+ constructor(config2) {
32
+ this.config = {
33
+ ...config2,
34
+ heartbeatInterval: config2.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL,
35
+ reconnectMaxAttempts: config2.reconnectMaxAttempts ?? DEFAULT_RECONNECT_MAX_ATTEMPTS,
36
+ reconnectBaseDelay: config2.reconnectBaseDelay ?? DEFAULT_RECONNECT_BASE_DELAY
37
+ };
38
+ }
39
+ /**
40
+ * Get current connection state
41
+ */
42
+ getState() {
43
+ return this.state;
44
+ }
45
+ /**
46
+ * Get tunnel info (only available when connected)
47
+ */
48
+ getTunnelInfo() {
49
+ return this.tunnelInfo;
50
+ }
51
+ /**
52
+ * Register event handlers
53
+ */
54
+ on(event, handler) {
55
+ switch (event) {
56
+ case "connected":
57
+ this.onConnected = handler;
58
+ break;
59
+ case "disconnected":
60
+ this.onDisconnected = handler;
61
+ break;
62
+ case "reconnecting":
63
+ this.onReconnecting = handler;
64
+ break;
65
+ case "error":
66
+ this.onError = handler;
67
+ break;
68
+ case "request":
69
+ this.onRequest = handler;
70
+ break;
71
+ }
72
+ return this;
73
+ }
74
+ /**
75
+ * Connect to the tunnel server
76
+ */
77
+ async connect() {
78
+ return new Promise((resolve, reject) => {
79
+ if (this.state === "connected" || this.state === "connecting") {
80
+ reject(new Error("Already connected or connecting"));
81
+ return;
82
+ }
83
+ this.state = "connecting";
84
+ this.shouldReconnect = true;
85
+ const wsUrl = this.buildWebSocketUrl();
86
+ const headers = {
87
+ "X-Bridge-Key": this.config.bridgeKey,
88
+ "X-Local-Port": String(this.config.localPort)
89
+ };
90
+ if (this.config.tunnelName) {
91
+ headers["X-Tunnel-Name"] = this.config.tunnelName;
92
+ }
93
+ this.socket = new WebSocket(wsUrl, {
94
+ headers
95
+ });
96
+ const connectTimeout = setTimeout(() => {
97
+ if (this.state === "connecting") {
98
+ this.socket?.close();
99
+ reject(new Error("Connection timeout"));
100
+ }
101
+ }, 3e4);
102
+ this.socket.on("open", () => {
103
+ });
104
+ this.socket.on("message", (data) => {
105
+ try {
106
+ const message = JSON.parse(data.toString());
107
+ this.handleMessage(message, resolve, reject, connectTimeout);
108
+ } catch (err) {
109
+ if (process.env.DEBUG) {
110
+ console.error("[tunnel-client] Failed to parse message:", err);
111
+ }
112
+ }
113
+ });
114
+ this.socket.on("close", (code, reason) => {
115
+ clearTimeout(connectTimeout);
116
+ this.handleClose(code, reason.toString());
117
+ });
118
+ this.socket.on("error", (err) => {
119
+ clearTimeout(connectTimeout);
120
+ if (this.state === "connecting") {
121
+ reject(err);
122
+ }
123
+ this.onError?.(err);
124
+ });
125
+ });
126
+ }
127
+ /**
128
+ * Disconnect from the tunnel server
129
+ */
130
+ disconnect(reason = "Client disconnect") {
131
+ this.shouldReconnect = false;
132
+ this.stopHeartbeat();
133
+ this.stopReconnectTimer();
134
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
135
+ this.send({
136
+ type: "disconnect",
137
+ timestamp: Date.now(),
138
+ payload: { reason }
139
+ });
140
+ this.socket.close(1e3, reason);
141
+ }
142
+ this.state = "disconnected";
143
+ this.tunnelInfo = null;
144
+ this.socket = null;
145
+ }
146
+ /**
147
+ * Build WebSocket URL
148
+ */
149
+ buildWebSocketUrl() {
150
+ const url = new URL(this.config.serverUrl);
151
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
152
+ url.pathname = "/connect";
153
+ return url.toString();
154
+ }
155
+ /**
156
+ * Handle incoming messages
157
+ */
158
+ handleMessage(message, resolve, reject, connectTimeout) {
159
+ switch (message.type) {
160
+ case "connected": {
161
+ clearTimeout(connectTimeout);
162
+ const connMsg = message;
163
+ this.tunnelInfo = {
164
+ tunnelId: connMsg.payload.tunnelId,
165
+ subdomain: connMsg.payload.subdomain,
166
+ url: connMsg.payload.url,
167
+ localPort: this.config.localPort,
168
+ expiresAt: new Date(connMsg.payload.expiresAt)
169
+ };
170
+ this.state = "connected";
171
+ this.reconnectAttempts = 0;
172
+ this.startHeartbeat();
173
+ this.onConnected?.(this.tunnelInfo);
174
+ resolve(this.tunnelInfo);
175
+ break;
176
+ }
177
+ case "error": {
178
+ const errMsg = message;
179
+ const error = new Error(errMsg.payload.message);
180
+ error.code = errMsg.payload.code;
181
+ if (errMsg.payload.fatal) {
182
+ clearTimeout(connectTimeout);
183
+ this.shouldReconnect = false;
184
+ if (this.state === "connecting") {
185
+ reject(error);
186
+ }
187
+ this.onError?.(error);
188
+ } else {
189
+ this.onError?.(error);
190
+ }
191
+ break;
192
+ }
193
+ case "heartbeat_ack": {
194
+ break;
195
+ }
196
+ case "http_request": {
197
+ const reqMsg = message;
198
+ this.handleHttpRequest(reqMsg);
199
+ break;
200
+ }
201
+ case "disconnect": {
202
+ this.shouldReconnect = false;
203
+ break;
204
+ }
205
+ }
206
+ }
207
+ /**
208
+ * Handle WebSocket close
209
+ */
210
+ handleClose(code, reason) {
211
+ this.stopHeartbeat();
212
+ const wasConnected = this.state === "connected";
213
+ this.state = "disconnected";
214
+ this.tunnelInfo = null;
215
+ this.socket = null;
216
+ if (this.shouldReconnect && wasConnected) {
217
+ this.attemptReconnect();
218
+ } else {
219
+ this.onDisconnected?.(reason || `Closed with code ${code}`);
220
+ }
221
+ }
222
+ /**
223
+ * Attempt to reconnect
224
+ */
225
+ attemptReconnect() {
226
+ if (this.reconnectAttempts >= this.config.reconnectMaxAttempts) {
227
+ this.state = "failed";
228
+ this.onDisconnected?.("Max reconnection attempts reached");
229
+ return;
230
+ }
231
+ this.reconnectAttempts++;
232
+ this.state = "reconnecting";
233
+ this.onReconnecting?.(this.reconnectAttempts, this.config.reconnectMaxAttempts);
234
+ const delay = this.config.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts - 1);
235
+ this.reconnectTimer = setTimeout(async () => {
236
+ this.reconnectTimer = null;
237
+ if (!this.shouldReconnect) {
238
+ return;
239
+ }
240
+ try {
241
+ await this.connect();
242
+ } catch {
243
+ }
244
+ }, delay);
245
+ }
246
+ /**
247
+ * Stop reconnect timer
248
+ */
249
+ stopReconnectTimer() {
250
+ if (this.reconnectTimer) {
251
+ clearTimeout(this.reconnectTimer);
252
+ this.reconnectTimer = null;
253
+ }
254
+ }
255
+ /**
256
+ * Start heartbeat timer
257
+ */
258
+ startHeartbeat() {
259
+ this.stopHeartbeat();
260
+ this.heartbeatTimer = setInterval(() => {
261
+ this.sendHeartbeat();
262
+ }, this.config.heartbeatInterval);
263
+ }
264
+ /**
265
+ * Stop heartbeat timer
266
+ */
267
+ stopHeartbeat() {
268
+ if (this.heartbeatTimer) {
269
+ clearInterval(this.heartbeatTimer);
270
+ this.heartbeatTimer = null;
271
+ }
272
+ }
273
+ /**
274
+ * Send heartbeat message
275
+ */
276
+ sendHeartbeat() {
277
+ const heartbeat = {
278
+ type: "heartbeat",
279
+ timestamp: Date.now(),
280
+ payload: {
281
+ requestCount: this.requestCount
282
+ }
283
+ };
284
+ this.send(heartbeat);
285
+ }
286
+ /**
287
+ * Handle HTTP request from tunnel server
288
+ */
289
+ handleHttpRequest(message) {
290
+ const { method, path: path2, headers, body } = message.payload;
291
+ this.requestCount++;
292
+ this.onRequest?.(method, path2);
293
+ const options = {
294
+ hostname: "localhost",
295
+ port: this.config.localPort,
296
+ path: path2,
297
+ method,
298
+ headers
299
+ };
300
+ const req = http.request(options, (res) => {
301
+ const chunks = [];
302
+ res.on("data", (chunk) => chunks.push(chunk));
303
+ res.on("end", () => {
304
+ const responseBody = Buffer.concat(chunks);
305
+ const response = {
306
+ status: res.statusCode || 500,
307
+ headers: res.headers,
308
+ body: responseBody.length > 0 ? responseBody.toString("base64") : void 0
309
+ };
310
+ this.send({
311
+ type: "http_response",
312
+ id: message.id,
313
+ timestamp: Date.now(),
314
+ payload: response
315
+ });
316
+ });
317
+ });
318
+ req.on("error", (err) => {
319
+ const response = {
320
+ status: 502,
321
+ headers: { "Content-Type": "text/plain" },
322
+ body: Buffer.from(`Error connecting to local server: ${err.message}`).toString("base64")
323
+ };
324
+ this.send({
325
+ type: "http_response",
326
+ id: message.id,
327
+ timestamp: Date.now(),
328
+ payload: response
329
+ });
330
+ });
331
+ if (body) {
332
+ req.write(Buffer.from(body, "base64"));
333
+ }
334
+ req.end();
335
+ }
336
+ /**
337
+ * Send message to server
338
+ */
339
+ send(message) {
340
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
341
+ this.socket.send(JSON.stringify(message));
342
+ }
343
+ }
344
+ };
345
+
346
+ // src/logger.ts
347
+ import chalk from "chalk";
348
+ var BANNER = `
349
+ \u2566 \u2566\u2566 \u2566\u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2550\u2557\u2566\u2550\u2557\u2554\u2566\u2557
350
+ \u2551 \u2551\u255A\u2557\u2554\u255D\u2551\u2563 \u2560\u2550\u255D\u2551 \u2551\u2560\u2566\u255D \u2551
351
+ \u2569\u2550\u255D\u2569 \u255A\u255D \u255A\u2550\u255D\u2569 \u255A\u2550\u255D\u2569\u255A\u2550 \u2569
352
+ `;
353
+ var logger = {
354
+ /**
355
+ * Print banner
356
+ */
357
+ banner() {
358
+ console.log(chalk.cyan(BANNER));
359
+ console.log(chalk.dim(" Secure localhost tunnels for AI agents\n"));
360
+ },
361
+ /**
362
+ * Info message
363
+ */
364
+ info(message) {
365
+ console.log(chalk.blue("\u2139"), message);
366
+ },
367
+ /**
368
+ * Success message
369
+ */
370
+ success(message) {
371
+ console.log(chalk.green("\u2714"), message);
372
+ },
373
+ /**
374
+ * Warning message
375
+ */
376
+ warn(message) {
377
+ console.log(chalk.yellow("\u26A0"), message);
378
+ },
379
+ /**
380
+ * Error message
381
+ */
382
+ error(message) {
383
+ console.log(chalk.red("\u2716"), message);
384
+ },
385
+ /**
386
+ * Debug message (only if DEBUG env is set)
387
+ */
388
+ debug(message) {
389
+ if (process.env.DEBUG) {
390
+ console.log(chalk.gray("\u2699"), chalk.gray(message));
391
+ }
392
+ },
393
+ /**
394
+ * Print connection info
395
+ */
396
+ connected(url, localPort) {
397
+ console.log();
398
+ console.log(chalk.green.bold(" Tunnel established!"));
399
+ console.log();
400
+ console.log(chalk.dim(" Public URL:"), chalk.cyan.bold(url));
401
+ console.log(chalk.dim(" Forwarding:"), `${chalk.cyan(url)} \u2192 ${chalk.yellow(`http://localhost:${localPort}`)}`);
402
+ console.log();
403
+ console.log(chalk.dim(" Press"), chalk.bold("Ctrl+C"), chalk.dim("to disconnect"));
404
+ console.log();
405
+ },
406
+ /**
407
+ * Print reconnection attempt
408
+ */
409
+ reconnecting(attempt, maxAttempts) {
410
+ console.log(
411
+ chalk.yellow("\u21BB"),
412
+ `Reconnecting... (attempt ${attempt}/${maxAttempts})`
413
+ );
414
+ },
415
+ /**
416
+ * Print disconnected message
417
+ */
418
+ disconnected(reason) {
419
+ console.log();
420
+ console.log(chalk.red("\u25CF"), chalk.red("Disconnected:"), reason);
421
+ },
422
+ /**
423
+ * Print request log
424
+ */
425
+ request(method, path2) {
426
+ const methodColor = getMethodColor(method);
427
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
428
+ console.log(
429
+ chalk.dim(timestamp),
430
+ methodColor(method.padEnd(7)),
431
+ chalk.white(path2)
432
+ );
433
+ },
434
+ /**
435
+ * Print status line
436
+ */
437
+ status(state, message) {
438
+ const stateColor = getStateColor(state);
439
+ console.log(stateColor("\u25CF"), message);
440
+ },
441
+ /**
442
+ * Print blank line
443
+ */
444
+ blank() {
445
+ console.log();
446
+ },
447
+ /**
448
+ * Print a formatted key-value pair
449
+ */
450
+ keyValue(key, value) {
451
+ console.log(chalk.dim(` ${key}:`), value);
452
+ },
453
+ /**
454
+ * Print a section header
455
+ */
456
+ section(title) {
457
+ console.log();
458
+ console.log(chalk.bold(title));
459
+ console.log(chalk.dim("\u2500".repeat(40)));
460
+ },
461
+ /**
462
+ * Print raw message (no formatting)
463
+ */
464
+ raw(message) {
465
+ console.log(message);
466
+ }
467
+ };
468
+ function getMethodColor(method) {
469
+ switch (method.toUpperCase()) {
470
+ case "GET":
471
+ return chalk.green;
472
+ case "POST":
473
+ return chalk.blue;
474
+ case "PUT":
475
+ return chalk.yellow;
476
+ case "DELETE":
477
+ return chalk.red;
478
+ case "PATCH":
479
+ return chalk.magenta;
480
+ default:
481
+ return chalk.white;
482
+ }
483
+ }
484
+ function getStateColor(state) {
485
+ switch (state) {
486
+ case "connected":
487
+ return chalk.green;
488
+ case "connecting":
489
+ case "reconnecting":
490
+ return chalk.yellow;
491
+ case "disconnected":
492
+ case "failed":
493
+ return chalk.red;
494
+ default:
495
+ return chalk.white;
496
+ }
497
+ }
498
+
499
+ // src/config.ts
500
+ import * as fs from "fs";
501
+ import * as path from "path";
502
+ import * as os from "os";
503
+ var CONFIG_DIR = path.join(os.homedir(), ".liveport");
504
+ var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
505
+ function loadConfig() {
506
+ try {
507
+ if (fs.existsSync(CONFIG_FILE)) {
508
+ const content = fs.readFileSync(CONFIG_FILE, "utf-8");
509
+ return JSON.parse(content);
510
+ }
511
+ } catch {
512
+ }
513
+ return {};
514
+ }
515
+ function saveConfig(config2) {
516
+ try {
517
+ if (!fs.existsSync(CONFIG_DIR)) {
518
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
519
+ }
520
+ fs.chmodSync(CONFIG_DIR, 448);
521
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config2, null, 2), {
522
+ mode: 384
523
+ });
524
+ fs.chmodSync(CONFIG_FILE, 384);
525
+ } catch (error) {
526
+ const err = error;
527
+ throw new Error(`Failed to save config: ${err.message}`);
528
+ }
529
+ }
530
+ function getConfigValue(key, cliValue, envKey) {
531
+ if (cliValue) {
532
+ return cliValue;
533
+ }
534
+ if (envKey && process.env[envKey]) {
535
+ return process.env[envKey];
536
+ }
537
+ const config2 = loadConfig();
538
+ return config2[key];
539
+ }
540
+ function getConfigPath() {
541
+ return CONFIG_FILE;
542
+ }
543
+
544
+ // src/commands/connect.ts
545
+ var DEFAULT_SERVER_URL = "https://tunnel.liveport.dev";
546
+ var activeClient = null;
547
+ function logError(error, prefix) {
548
+ const message = prefix ? `${prefix}: ${error.message}` : error.message;
549
+ if (error.code) {
550
+ logger.error(`${error.code}: ${message}`);
551
+ } else {
552
+ logger.error(message);
553
+ }
554
+ }
555
+ async function connectCommand(port, options) {
556
+ const localPort = parseInt(port, 10);
557
+ if (isNaN(localPort) || localPort < 1 || localPort > 65535) {
558
+ logger.error(`Invalid port number: ${port}`);
559
+ process.exit(1);
560
+ }
561
+ const bridgeKey = getConfigValue("key", options.key, "LIVEPORT_KEY");
562
+ if (!bridgeKey) {
563
+ logger.error("Bridge key required. Use --key, set LIVEPORT_KEY, or run 'liveport config set key <your-key>'");
564
+ logger.blank();
565
+ logger.info("Get a bridge key at: https://liveport.dev/keys");
566
+ process.exit(1);
567
+ }
568
+ const serverUrl = getConfigValue("server", options.server, "LIVEPORT_SERVER_URL") || DEFAULT_SERVER_URL;
569
+ logger.banner();
570
+ const spinner = ora({
571
+ text: `Connecting to tunnel server...`,
572
+ color: "cyan"
573
+ }).start();
574
+ const client = new TunnelClient({
575
+ serverUrl,
576
+ bridgeKey,
577
+ localPort,
578
+ tunnelName: options.name
579
+ });
580
+ activeClient = client;
581
+ client.on("connected", (info) => {
582
+ spinner.stop();
583
+ logger.connected(info.url, info.localPort);
584
+ });
585
+ client.on("disconnected", (reason) => {
586
+ spinner.stop();
587
+ logger.disconnected(reason);
588
+ activeClient = null;
589
+ process.exit(0);
590
+ });
591
+ client.on("reconnecting", (attempt, max) => {
592
+ spinner.stop();
593
+ logger.reconnecting(attempt, max);
594
+ spinner.start("Reconnecting...");
595
+ });
596
+ client.on("error", (error) => {
597
+ spinner.stop();
598
+ logError(error);
599
+ });
600
+ client.on("request", (method, path2) => {
601
+ logger.request(method, path2);
602
+ });
603
+ setupGracefulShutdown(client);
604
+ try {
605
+ await client.connect();
606
+ } catch (error) {
607
+ spinner.stop();
608
+ logError(error, "Connection failed");
609
+ process.exit(1);
610
+ }
611
+ }
612
+ function setupGracefulShutdown(client) {
613
+ const shutdown = (signal) => {
614
+ logger.blank();
615
+ logger.info(`Received ${signal}, disconnecting...`);
616
+ client.disconnect(`${signal} received`);
617
+ activeClient = null;
618
+ };
619
+ process.on("SIGINT", () => shutdown("SIGINT"));
620
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
621
+ process.on("uncaughtException", (error) => {
622
+ logger.error(`Uncaught error: ${error.message}`);
623
+ client.disconnect("Uncaught error");
624
+ process.exit(1);
625
+ });
626
+ process.on("unhandledRejection", (reason) => {
627
+ logger.error(`Unhandled rejection: ${reason}`);
628
+ client.disconnect("Unhandled rejection");
629
+ process.exit(1);
630
+ });
631
+ }
632
+ function getActiveClient() {
633
+ return activeClient;
634
+ }
635
+
636
+ // src/commands/status.ts
637
+ async function statusCommand() {
638
+ const client = getActiveClient();
639
+ if (!client) {
640
+ logger.info("No active tunnel connection");
641
+ return;
642
+ }
643
+ const state = client.getState();
644
+ const info = client.getTunnelInfo();
645
+ logger.section("Tunnel Status");
646
+ logger.status(state, `Connection: ${state}`);
647
+ if (info) {
648
+ logger.keyValue("Tunnel ID", info.tunnelId);
649
+ logger.keyValue("Subdomain", info.subdomain);
650
+ logger.keyValue("Public URL", info.url);
651
+ logger.keyValue("Local Port", String(info.localPort));
652
+ logger.keyValue("Expires", info.expiresAt.toLocaleString());
653
+ }
654
+ logger.blank();
655
+ }
656
+
657
+ // src/commands/disconnect.ts
658
+ async function disconnectCommand(options) {
659
+ const client = getActiveClient();
660
+ if (!client) {
661
+ logger.info("No active tunnel connection to disconnect");
662
+ return;
663
+ }
664
+ const info = client.getTunnelInfo();
665
+ const subdomain = info?.subdomain || "unknown";
666
+ logger.info(`Disconnecting tunnel: ${subdomain}...`);
667
+ client.disconnect("User requested disconnect");
668
+ logger.success("Tunnel disconnected");
669
+ }
670
+
671
+ // src/commands/config.ts
672
+ var VALID_KEYS = ["key", "server"];
673
+ function configSetCommand(key, value) {
674
+ if (!VALID_KEYS.includes(key)) {
675
+ logger.error(`Invalid config key: ${key}`);
676
+ logger.info(`Valid keys: ${VALID_KEYS.join(", ")}`);
677
+ process.exit(1);
678
+ }
679
+ const config2 = loadConfig();
680
+ config2[key] = value;
681
+ try {
682
+ saveConfig(config2);
683
+ logger.success(`Config saved: ${key} = ${key === "key" ? maskKey(value) : value}`);
684
+ logger.keyValue("Config file", getConfigPath());
685
+ } catch (error) {
686
+ const err = error;
687
+ logger.error(err.message);
688
+ process.exit(1);
689
+ }
690
+ }
691
+ function configGetCommand(key) {
692
+ if (!VALID_KEYS.includes(key)) {
693
+ logger.error(`Invalid config key: ${key}`);
694
+ logger.info(`Valid keys: ${VALID_KEYS.join(", ")}`);
695
+ process.exit(1);
696
+ }
697
+ const config2 = loadConfig();
698
+ const value = config2[key];
699
+ if (value) {
700
+ const displayValue = key === "key" ? maskKey(value) : value;
701
+ logger.keyValue(key, displayValue);
702
+ } else {
703
+ logger.info(`${key} is not set`);
704
+ }
705
+ }
706
+ function configListCommand() {
707
+ const config2 = loadConfig();
708
+ logger.section("Configuration");
709
+ logger.keyValue("Config file", getConfigPath());
710
+ logger.blank();
711
+ if (Object.keys(config2).length === 0) {
712
+ logger.info("No configuration set");
713
+ return;
714
+ }
715
+ for (const key of VALID_KEYS) {
716
+ const value = config2[key];
717
+ if (value) {
718
+ const displayValue = key === "key" ? maskKey(value) : value;
719
+ logger.keyValue(key, displayValue);
720
+ }
721
+ }
722
+ }
723
+ function configDeleteCommand(key) {
724
+ if (!VALID_KEYS.includes(key)) {
725
+ logger.error(`Invalid config key: ${key}`);
726
+ logger.info(`Valid keys: ${VALID_KEYS.join(", ")}`);
727
+ process.exit(1);
728
+ }
729
+ const config2 = loadConfig();
730
+ delete config2[key];
731
+ try {
732
+ saveConfig(config2);
733
+ logger.success(`Config deleted: ${key}`);
734
+ } catch (error) {
735
+ const err = error;
736
+ logger.error(err.message);
737
+ process.exit(1);
738
+ }
739
+ }
740
+ function maskKey(key) {
741
+ if (key.length <= 12) {
742
+ return "****";
743
+ }
744
+ return `${key.slice(0, 8)}...${key.slice(-4)}`;
745
+ }
746
+
747
+ // src/index.ts
748
+ var program = new Command();
749
+ program.name("liveport").description("Secure localhost tunnels for AI agents").version("0.1.0");
750
+ program.command("connect <port>").description("Create a tunnel to expose a local port").option("-k, --key <key>", "Bridge key for authentication").option("-s, --server <url>", "Tunnel server URL").option("-r, --region <region>", "Server region").action(connectCommand);
751
+ program.command("status").description("Show current tunnel status").action(statusCommand);
752
+ program.command("disconnect").description("Disconnect active tunnel").option("-a, --all", "Disconnect all tunnels").action(disconnectCommand);
753
+ var config = program.command("config").description("Manage CLI configuration");
754
+ config.command("set <key> <value>").description("Set a config value (key, server)").action(configSetCommand);
755
+ config.command("get <key>").description("Get a config value").action(configGetCommand);
756
+ config.command("list").description("List all config values").action(configListCommand);
757
+ config.command("delete <key>").description("Delete a config value").action(configDeleteCommand);
758
+ program.parse();
759
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/commands/connect.ts","../src/tunnel-client.ts","../src/logger.ts","../src/config.ts","../src/commands/status.ts","../src/commands/disconnect.ts","../src/commands/config.ts"],"sourcesContent":["/**\n * LivePort CLI\n *\n * Command-line interface for creating secure localhost tunnels.\n */\n\nimport { Command } from \"commander\";\nimport { connectCommand } from \"./commands/connect\";\nimport { statusCommand } from \"./commands/status\";\nimport { disconnectCommand } from \"./commands/disconnect\";\nimport { configSetCommand, configGetCommand, configListCommand, configDeleteCommand } from \"./commands/config\";\n\nconst program = new Command();\n\nprogram\n .name(\"liveport\")\n .description(\"Secure localhost tunnels for AI agents\")\n .version(\"0.1.0\");\n\nprogram\n .command(\"connect <port>\")\n .description(\"Create a tunnel to expose a local port\")\n .option(\"-k, --key <key>\", \"Bridge key for authentication\")\n .option(\"-s, --server <url>\", \"Tunnel server URL\")\n .option(\"-r, --region <region>\", \"Server region\")\n .action(connectCommand);\n\nprogram\n .command(\"status\")\n .description(\"Show current tunnel status\")\n .action(statusCommand);\n\nprogram\n .command(\"disconnect\")\n .description(\"Disconnect active tunnel\")\n .option(\"-a, --all\", \"Disconnect all tunnels\")\n .action(disconnectCommand);\n\n// Config command group\nconst config = program\n .command(\"config\")\n .description(\"Manage CLI configuration\");\n\nconfig\n .command(\"set <key> <value>\")\n .description(\"Set a config value (key, server)\")\n .action(configSetCommand);\n\nconfig\n .command(\"get <key>\")\n .description(\"Get a config value\")\n .action(configGetCommand);\n\nconfig\n .command(\"list\")\n .description(\"List all config values\")\n .action(configListCommand);\n\nconfig\n .command(\"delete <key>\")\n .description(\"Delete a config value\")\n .action(configDeleteCommand);\n\nprogram.parse();\n","/**\n * Connect Command\n *\n * Creates a tunnel to expose a local port to the internet.\n */\n\nimport ora from \"ora\";\nimport { TunnelClient } from \"../tunnel-client\";\nimport { logger } from \"../logger\";\nimport { getConfigValue } from \"../config\";\nimport type { ConnectOptions } from \"../types\";\n\n// Default server URL (production tunnel server)\nconst DEFAULT_SERVER_URL = \"https://tunnel.liveport.dev\";\n\n// Active client reference for graceful shutdown\nlet activeClient: TunnelClient | null = null;\n\n/**\n * Log an error with optional error code prefix\n */\nfunction logError(error: Error & { code?: string }, prefix?: string): void {\n const message = prefix ? `${prefix}: ${error.message}` : error.message;\n if (error.code) {\n logger.error(`${error.code}: ${message}`);\n } else {\n logger.error(message);\n }\n}\n\n/**\n * Execute the connect command\n */\nexport async function connectCommand(\n port: string,\n options: ConnectOptions\n): Promise<void> {\n const localPort = parseInt(port, 10);\n\n // Validate port\n if (isNaN(localPort) || localPort < 1 || localPort > 65535) {\n logger.error(`Invalid port number: ${port}`);\n process.exit(1);\n }\n\n // Get bridge key (priority: CLI option > env var > config file)\n const bridgeKey = getConfigValue(\"key\", options.key, \"LIVEPORT_KEY\");\n if (!bridgeKey) {\n logger.error(\"Bridge key required. Use --key, set LIVEPORT_KEY, or run 'liveport config set key <your-key>'\");\n logger.blank();\n logger.info(\"Get a bridge key at: https://liveport.dev/keys\");\n process.exit(1);\n }\n\n // Get server URL (priority: CLI option > env var > config file > default)\n const serverUrl = getConfigValue(\"server\", options.server, \"LIVEPORT_SERVER_URL\") || DEFAULT_SERVER_URL;\n\n // Print banner\n logger.banner();\n\n // Show connecting spinner\n const spinner = ora({\n text: `Connecting to tunnel server...`,\n color: \"cyan\",\n }).start();\n\n // Create tunnel client\n const client = new TunnelClient({\n serverUrl,\n bridgeKey,\n localPort,\n tunnelName: options.name,\n });\n\n // Store for graceful shutdown\n activeClient = client;\n\n // Set up event handlers\n client.on(\"connected\", (info) => {\n spinner.stop();\n logger.connected(info.url, info.localPort);\n });\n\n client.on(\"disconnected\", (reason) => {\n spinner.stop();\n logger.disconnected(reason);\n activeClient = null;\n process.exit(0);\n });\n\n client.on(\"reconnecting\", (attempt, max) => {\n spinner.stop();\n logger.reconnecting(attempt, max);\n spinner.start(\"Reconnecting...\");\n });\n\n client.on(\"error\", (error) => {\n spinner.stop();\n logError(error as Error & { code?: string });\n });\n\n client.on(\"request\", (method, path) => {\n logger.request(method, path);\n });\n\n // Set up graceful shutdown\n setupGracefulShutdown(client);\n\n // Connect\n try {\n await client.connect();\n } catch (error) {\n spinner.stop();\n logError(error as Error & { code?: string }, \"Connection failed\");\n process.exit(1);\n }\n}\n\n/**\n * Set up graceful shutdown handlers\n */\nfunction setupGracefulShutdown(client: TunnelClient): void {\n const shutdown = (signal: string) => {\n logger.blank();\n logger.info(`Received ${signal}, disconnecting...`);\n client.disconnect(`${signal} received`);\n activeClient = null;\n };\n\n process.on(\"SIGINT\", () => shutdown(\"SIGINT\"));\n process.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"));\n\n // Handle uncaught errors\n process.on(\"uncaughtException\", (error) => {\n logger.error(`Uncaught error: ${error.message}`);\n client.disconnect(\"Uncaught error\");\n process.exit(1);\n });\n\n process.on(\"unhandledRejection\", (reason) => {\n logger.error(`Unhandled rejection: ${reason}`);\n client.disconnect(\"Unhandled rejection\");\n process.exit(1);\n });\n}\n\n/**\n * Get the active client (for status/disconnect commands)\n */\nexport function getActiveClient(): TunnelClient | null {\n return activeClient;\n}\n\nexport default connectCommand;\n","/**\n * Tunnel Client\n *\n * WebSocket client for connecting to the LivePort tunnel server.\n * Handles connection, reconnection, heartbeats, and HTTP proxying.\n */\n\nimport WebSocket from \"ws\";\nimport http from \"http\";\nimport type {\n TunnelClientConfig,\n TunnelInfo,\n ConnectionState,\n Message,\n ConnectedMessage,\n ErrorMessage,\n HeartbeatMessage,\n HttpRequestMessage,\n HttpResponsePayload,\n} from \"./types\";\n\nconst DEFAULT_HEARTBEAT_INTERVAL = 10000; // 10 seconds\nconst DEFAULT_RECONNECT_MAX_ATTEMPTS = 5;\nconst DEFAULT_RECONNECT_BASE_DELAY = 1000; // 1 second\n\nexport class TunnelClient {\n private config: TunnelClientConfig & {\n heartbeatInterval: number;\n reconnectMaxAttempts: number;\n reconnectBaseDelay: number;\n };\n private socket: WebSocket | null = null;\n private state: ConnectionState = \"disconnected\";\n private tunnelInfo: TunnelInfo | null = null;\n private heartbeatTimer: NodeJS.Timeout | null = null;\n private reconnectTimer: NodeJS.Timeout | null = null;\n private reconnectAttempts = 0;\n private requestCount = 0;\n private shouldReconnect = true;\n\n // Event handlers\n private onConnected: ((info: TunnelInfo) => void) | null = null;\n private onDisconnected: ((reason: string) => void) | null = null;\n private onReconnecting: ((attempt: number, max: number) => void) | null = null;\n private onError: ((error: Error) => void) | null = null;\n private onRequest: ((method: string, path: string) => void) | null = null;\n\n constructor(config: TunnelClientConfig) {\n this.config = {\n ...config,\n heartbeatInterval: config.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL,\n reconnectMaxAttempts: config.reconnectMaxAttempts ?? DEFAULT_RECONNECT_MAX_ATTEMPTS,\n reconnectBaseDelay: config.reconnectBaseDelay ?? DEFAULT_RECONNECT_BASE_DELAY,\n };\n }\n\n /**\n * Get current connection state\n */\n getState(): ConnectionState {\n return this.state;\n }\n\n /**\n * Get tunnel info (only available when connected)\n */\n getTunnelInfo(): TunnelInfo | null {\n return this.tunnelInfo;\n }\n\n /**\n * Register event handlers\n */\n on<E extends keyof TunnelClientEventHandlers>(\n event: E,\n handler: TunnelClientEventHandlers[E]\n ): this {\n switch (event) {\n case \"connected\":\n this.onConnected = handler as (info: TunnelInfo) => void;\n break;\n case \"disconnected\":\n this.onDisconnected = handler as (reason: string) => void;\n break;\n case \"reconnecting\":\n this.onReconnecting = handler as (attempt: number, max: number) => void;\n break;\n case \"error\":\n this.onError = handler as (error: Error) => void;\n break;\n case \"request\":\n this.onRequest = handler as (method: string, path: string) => void;\n break;\n }\n return this;\n }\n\n /**\n * Connect to the tunnel server\n */\n async connect(): Promise<TunnelInfo> {\n return new Promise((resolve, reject) => {\n if (this.state === \"connected\" || this.state === \"connecting\") {\n reject(new Error(\"Already connected or connecting\"));\n return;\n }\n\n this.state = \"connecting\";\n this.shouldReconnect = true;\n\n const wsUrl = this.buildWebSocketUrl();\n const headers: Record<string, string> = {\n \"X-Bridge-Key\": this.config.bridgeKey,\n \"X-Local-Port\": String(this.config.localPort),\n };\n \n // Add tunnel name if provided\n if (this.config.tunnelName) {\n headers[\"X-Tunnel-Name\"] = this.config.tunnelName;\n }\n \n this.socket = new WebSocket(wsUrl, {\n headers,\n });\n\n // Connection timeout\n const connectTimeout = setTimeout(() => {\n if (this.state === \"connecting\") {\n this.socket?.close();\n reject(new Error(\"Connection timeout\"));\n }\n }, 30000);\n\n this.socket.on(\"open\", () => {\n // Connection open, waiting for \"connected\" message from server\n });\n\n this.socket.on(\"message\", (data) => {\n try {\n const message = JSON.parse(data.toString()) as Message;\n this.handleMessage(message, resolve, reject, connectTimeout);\n } catch (err) {\n // Log parse errors at debug level - malformed messages from server\n if (process.env.DEBUG) {\n console.error(\"[tunnel-client] Failed to parse message:\", err);\n }\n }\n });\n\n this.socket.on(\"close\", (code, reason) => {\n clearTimeout(connectTimeout);\n this.handleClose(code, reason.toString());\n });\n\n this.socket.on(\"error\", (err) => {\n clearTimeout(connectTimeout);\n if (this.state === \"connecting\") {\n reject(err);\n }\n this.onError?.(err);\n });\n });\n }\n\n /**\n * Disconnect from the tunnel server\n */\n disconnect(reason: string = \"Client disconnect\"): void {\n this.shouldReconnect = false;\n this.stopHeartbeat();\n this.stopReconnectTimer();\n\n if (this.socket && this.socket.readyState === WebSocket.OPEN) {\n // Send disconnect message\n this.send({\n type: \"disconnect\",\n timestamp: Date.now(),\n payload: { reason },\n });\n\n this.socket.close(1000, reason);\n }\n\n this.state = \"disconnected\";\n this.tunnelInfo = null;\n this.socket = null;\n }\n\n /**\n * Build WebSocket URL\n */\n private buildWebSocketUrl(): string {\n const url = new URL(this.config.serverUrl);\n url.protocol = url.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n url.pathname = \"/connect\";\n return url.toString();\n }\n\n /**\n * Handle incoming messages\n */\n private handleMessage(\n message: Message,\n resolve: (info: TunnelInfo) => void,\n reject: (error: Error) => void,\n connectTimeout: NodeJS.Timeout\n ): void {\n switch (message.type) {\n case \"connected\": {\n clearTimeout(connectTimeout);\n const connMsg = message as ConnectedMessage;\n this.tunnelInfo = {\n tunnelId: connMsg.payload.tunnelId,\n subdomain: connMsg.payload.subdomain,\n url: connMsg.payload.url,\n localPort: this.config.localPort,\n expiresAt: new Date(connMsg.payload.expiresAt),\n };\n this.state = \"connected\";\n this.reconnectAttempts = 0;\n this.startHeartbeat();\n this.onConnected?.(this.tunnelInfo);\n resolve(this.tunnelInfo);\n break;\n }\n\n case \"error\": {\n const errMsg = message as ErrorMessage;\n const error = new Error(errMsg.payload.message);\n (error as Error & { code: string }).code = errMsg.payload.code;\n\n if (errMsg.payload.fatal) {\n clearTimeout(connectTimeout);\n this.shouldReconnect = false;\n if (this.state === \"connecting\") {\n reject(error);\n }\n this.onError?.(error);\n } else {\n this.onError?.(error);\n }\n break;\n }\n\n case \"heartbeat_ack\": {\n // Heartbeat acknowledged - connection is alive\n break;\n }\n\n case \"http_request\": {\n const reqMsg = message as HttpRequestMessage;\n this.handleHttpRequest(reqMsg);\n break;\n }\n\n case \"disconnect\": {\n // Server requested disconnect\n this.shouldReconnect = false;\n break;\n }\n }\n }\n\n /**\n * Handle WebSocket close\n */\n private handleClose(code: number, reason: string): void {\n this.stopHeartbeat();\n const wasConnected = this.state === \"connected\";\n this.state = \"disconnected\";\n this.tunnelInfo = null;\n this.socket = null;\n\n // Check if we should reconnect\n if (this.shouldReconnect && wasConnected) {\n this.attemptReconnect();\n } else {\n this.onDisconnected?.(reason || `Closed with code ${code}`);\n }\n }\n\n /**\n * Attempt to reconnect\n */\n private attemptReconnect(): void {\n if (this.reconnectAttempts >= this.config.reconnectMaxAttempts) {\n this.state = \"failed\";\n this.onDisconnected?.(\"Max reconnection attempts reached\");\n return;\n }\n\n this.reconnectAttempts++;\n this.state = \"reconnecting\";\n this.onReconnecting?.(this.reconnectAttempts, this.config.reconnectMaxAttempts);\n\n // Exponential backoff\n const delay = this.config.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts - 1);\n\n // Store timer reference so we can cancel if disconnect() is called\n this.reconnectTimer = setTimeout(async () => {\n this.reconnectTimer = null;\n // Check if disconnect was called during the delay\n if (!this.shouldReconnect) {\n return;\n }\n try {\n await this.connect();\n } catch {\n // Will trigger handleClose which will retry\n }\n }, delay);\n }\n\n /**\n * Stop reconnect timer\n */\n private stopReconnectTimer(): void {\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n }\n\n /**\n * Start heartbeat timer\n */\n private startHeartbeat(): void {\n this.stopHeartbeat();\n this.heartbeatTimer = setInterval(() => {\n this.sendHeartbeat();\n }, this.config.heartbeatInterval);\n }\n\n /**\n * Stop heartbeat timer\n */\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n /**\n * Send heartbeat message\n */\n private sendHeartbeat(): void {\n const heartbeat: HeartbeatMessage = {\n type: \"heartbeat\",\n timestamp: Date.now(),\n payload: {\n requestCount: this.requestCount,\n },\n };\n this.send(heartbeat);\n }\n\n /**\n * Handle HTTP request from tunnel server\n */\n private handleHttpRequest(message: HttpRequestMessage): void {\n const { method, path, headers, body } = message.payload;\n this.requestCount++;\n this.onRequest?.(method, path);\n\n // Make request to local server\n const options: http.RequestOptions = {\n hostname: \"localhost\",\n port: this.config.localPort,\n path: path,\n method: method,\n headers: headers,\n };\n\n const req = http.request(options, (res) => {\n const chunks: Buffer[] = [];\n\n res.on(\"data\", (chunk) => chunks.push(chunk));\n\n res.on(\"end\", () => {\n const responseBody = Buffer.concat(chunks);\n const response: HttpResponsePayload = {\n status: res.statusCode || 500,\n headers: res.headers as Record<string, string>,\n body: responseBody.length > 0 ? responseBody.toString(\"base64\") : undefined,\n };\n\n this.send({\n type: \"http_response\",\n id: message.id,\n timestamp: Date.now(),\n payload: response,\n });\n });\n });\n\n req.on(\"error\", (err) => {\n // Send error response\n const response: HttpResponsePayload = {\n status: 502,\n headers: { \"Content-Type\": \"text/plain\" },\n body: Buffer.from(`Error connecting to local server: ${err.message}`).toString(\"base64\"),\n };\n\n this.send({\n type: \"http_response\",\n id: message.id,\n timestamp: Date.now(),\n payload: response,\n });\n });\n\n // Send request body if present\n if (body) {\n req.write(Buffer.from(body, \"base64\"));\n }\n req.end();\n }\n\n /**\n * Send message to server\n */\n private send(message: Message | { type: string; timestamp: number; payload?: unknown; id?: string }): void {\n if (this.socket && this.socket.readyState === WebSocket.OPEN) {\n this.socket.send(JSON.stringify(message));\n }\n }\n}\n\n// Event handler types\ninterface TunnelClientEventHandlers {\n connected: (info: TunnelInfo) => void;\n disconnected: (reason: string) => void;\n reconnecting: (attempt: number, max: number) => void;\n error: (error: Error) => void;\n request: (method: string, path: string) => void;\n}\n","/**\n * CLI Logger\n *\n * Colored terminal output utilities for the CLI.\n */\n\nimport chalk from \"chalk\";\n\n// ASCII art banner\nconst BANNER = `\n ╦ ╦╦ ╦╔═╗╔═╗╔═╗╦═╗╔╦╗\n ║ ║╚╗╔╝║╣ ╠═╝║ ║╠╦╝ ║\n ╩═╝╩ ╚╝ ╚═╝╩ ╚═╝╩╚═ ╩\n`;\n\nexport const logger = {\n /**\n * Print banner\n */\n banner(): void {\n console.log(chalk.cyan(BANNER));\n console.log(chalk.dim(\" Secure localhost tunnels for AI agents\\n\"));\n },\n\n /**\n * Info message\n */\n info(message: string): void {\n console.log(chalk.blue(\"ℹ\"), message);\n },\n\n /**\n * Success message\n */\n success(message: string): void {\n console.log(chalk.green(\"✔\"), message);\n },\n\n /**\n * Warning message\n */\n warn(message: string): void {\n console.log(chalk.yellow(\"⚠\"), message);\n },\n\n /**\n * Error message\n */\n error(message: string): void {\n console.log(chalk.red(\"✖\"), message);\n },\n\n /**\n * Debug message (only if DEBUG env is set)\n */\n debug(message: string): void {\n if (process.env.DEBUG) {\n console.log(chalk.gray(\"⚙\"), chalk.gray(message));\n }\n },\n\n /**\n * Print connection info\n */\n connected(url: string, localPort: number): void {\n console.log();\n console.log(chalk.green.bold(\" Tunnel established!\"));\n console.log();\n console.log(chalk.dim(\" Public URL:\"), chalk.cyan.bold(url));\n console.log(chalk.dim(\" Forwarding:\"), `${chalk.cyan(url)} → ${chalk.yellow(`http://localhost:${localPort}`)}`);\n console.log();\n console.log(chalk.dim(\" Press\"), chalk.bold(\"Ctrl+C\"), chalk.dim(\"to disconnect\"));\n console.log();\n },\n\n /**\n * Print reconnection attempt\n */\n reconnecting(attempt: number, maxAttempts: number): void {\n console.log(\n chalk.yellow(\"↻\"),\n `Reconnecting... (attempt ${attempt}/${maxAttempts})`\n );\n },\n\n /**\n * Print disconnected message\n */\n disconnected(reason: string): void {\n console.log();\n console.log(chalk.red(\"●\"), chalk.red(\"Disconnected:\"), reason);\n },\n\n /**\n * Print request log\n */\n request(method: string, path: string): void {\n const methodColor = getMethodColor(method);\n const timestamp = new Date().toLocaleTimeString();\n console.log(\n chalk.dim(timestamp),\n methodColor(method.padEnd(7)),\n chalk.white(path)\n );\n },\n\n /**\n * Print status line\n */\n status(state: string, message: string): void {\n const stateColor = getStateColor(state);\n console.log(stateColor(\"●\"), message);\n },\n\n /**\n * Print blank line\n */\n blank(): void {\n console.log();\n },\n\n /**\n * Print a formatted key-value pair\n */\n keyValue(key: string, value: string): void {\n console.log(chalk.dim(` ${key}:`), value);\n },\n\n /**\n * Print a section header\n */\n section(title: string): void {\n console.log();\n console.log(chalk.bold(title));\n console.log(chalk.dim(\"─\".repeat(40)));\n },\n\n /**\n * Print raw message (no formatting)\n */\n raw(message: string): void {\n console.log(message);\n },\n};\n\n/**\n * Get color for HTTP method\n */\nfunction getMethodColor(method: string): (text: string) => string {\n switch (method.toUpperCase()) {\n case \"GET\":\n return chalk.green;\n case \"POST\":\n return chalk.blue;\n case \"PUT\":\n return chalk.yellow;\n case \"DELETE\":\n return chalk.red;\n case \"PATCH\":\n return chalk.magenta;\n default:\n return chalk.white;\n }\n}\n\n/**\n * Get color for connection state\n */\nfunction getStateColor(state: string): (text: string) => string {\n switch (state) {\n case \"connected\":\n return chalk.green;\n case \"connecting\":\n case \"reconnecting\":\n return chalk.yellow;\n case \"disconnected\":\n case \"failed\":\n return chalk.red;\n default:\n return chalk.white;\n }\n}\n\nexport default logger;\n","/**\n * CLI Configuration\n *\n * Handles loading and saving CLI configuration from ~/.liveport/config.json\n */\n\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport * as os from \"os\";\n\nexport interface LivePortConfig {\n /** Bridge key for authentication */\n key?: string;\n /** Tunnel server URL */\n server?: string;\n}\n\nconst CONFIG_DIR = path.join(os.homedir(), \".liveport\");\nconst CONFIG_FILE = path.join(CONFIG_DIR, \"config.json\");\n\n/**\n * Load configuration from disk\n */\nexport function loadConfig(): LivePortConfig {\n try {\n if (fs.existsSync(CONFIG_FILE)) {\n const content = fs.readFileSync(CONFIG_FILE, \"utf-8\");\n return JSON.parse(content) as LivePortConfig;\n }\n } catch {\n // Ignore errors, return empty config\n }\n return {};\n}\n\n/**\n * Save configuration to disk\n */\nexport function saveConfig(config: LivePortConfig): void {\n try {\n // Ensure config directory exists with restricted permissions\n if (!fs.existsSync(CONFIG_DIR)) {\n fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });\n }\n // Explicitly set permissions to override umask (stores sensitive bridge keys)\n fs.chmodSync(CONFIG_DIR, 0o700);\n\n // Write config file with restricted permissions\n fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {\n mode: 0o600,\n });\n // Explicitly set file permissions to override umask\n fs.chmodSync(CONFIG_FILE, 0o600);\n } catch (error) {\n const err = error as Error;\n throw new Error(`Failed to save config: ${err.message}`);\n }\n}\n\n/**\n * Get configuration value with fallbacks\n * Priority: CLI option > env var > config file\n */\nexport function getConfigValue(\n key: keyof LivePortConfig,\n cliValue?: string,\n envKey?: string\n): string | undefined {\n // CLI option takes priority\n if (cliValue) {\n return cliValue;\n }\n\n // Then environment variable\n if (envKey && process.env[envKey]) {\n return process.env[envKey];\n }\n\n // Finally, config file\n const config = loadConfig();\n return config[key];\n}\n\n/**\n * Get the config file path\n */\nexport function getConfigPath(): string {\n return CONFIG_FILE;\n}\n","/**\n * Status Command\n *\n * Shows the current tunnel connection status.\n */\n\nimport { logger } from \"../logger\";\nimport { getActiveClient } from \"./connect\";\n\n/**\n * Execute the status command\n */\nexport async function statusCommand(): Promise<void> {\n const client = getActiveClient();\n\n if (!client) {\n logger.info(\"No active tunnel connection\");\n return;\n }\n\n const state = client.getState();\n const info = client.getTunnelInfo();\n\n logger.section(\"Tunnel Status\");\n\n logger.status(state, `Connection: ${state}`);\n\n if (info) {\n logger.keyValue(\"Tunnel ID\", info.tunnelId);\n logger.keyValue(\"Subdomain\", info.subdomain);\n logger.keyValue(\"Public URL\", info.url);\n logger.keyValue(\"Local Port\", String(info.localPort));\n logger.keyValue(\"Expires\", info.expiresAt.toLocaleString());\n }\n\n logger.blank();\n}\n\nexport default statusCommand;\n","/**\n * Disconnect Command\n *\n * Disconnects the active tunnel connection.\n */\n\nimport { logger } from \"../logger\";\nimport { getActiveClient } from \"./connect\";\n\nexport interface DisconnectOptions {\n all?: boolean;\n}\n\n/**\n * Execute the disconnect command\n */\nexport async function disconnectCommand(\n options: DisconnectOptions\n): Promise<void> {\n const client = getActiveClient();\n\n if (!client) {\n logger.info(\"No active tunnel connection to disconnect\");\n return;\n }\n\n const info = client.getTunnelInfo();\n const subdomain = info?.subdomain || \"unknown\";\n\n logger.info(`Disconnecting tunnel: ${subdomain}...`);\n\n client.disconnect(\"User requested disconnect\");\n\n logger.success(\"Tunnel disconnected\");\n}\n\nexport default disconnectCommand;\n","/**\n * Config Command\n *\n * Manage CLI configuration (key, server, etc.)\n */\n\nimport { logger } from \"../logger\";\nimport { loadConfig, saveConfig, getConfigPath, type LivePortConfig } from \"../config\";\n\ntype ConfigKey = keyof LivePortConfig;\n\nconst VALID_KEYS: ConfigKey[] = [\"key\", \"server\"];\n\n/**\n * Set a config value\n */\nexport function configSetCommand(key: string, value: string): void {\n if (!VALID_KEYS.includes(key as ConfigKey)) {\n logger.error(`Invalid config key: ${key}`);\n logger.info(`Valid keys: ${VALID_KEYS.join(\", \")}`);\n process.exit(1);\n }\n\n const config = loadConfig();\n config[key as ConfigKey] = value;\n\n try {\n saveConfig(config);\n logger.success(`Config saved: ${key} = ${key === \"key\" ? maskKey(value) : value}`);\n logger.keyValue(\"Config file\", getConfigPath());\n } catch (error) {\n const err = error as Error;\n logger.error(err.message);\n process.exit(1);\n }\n}\n\n/**\n * Get a config value\n */\nexport function configGetCommand(key: string): void {\n if (!VALID_KEYS.includes(key as ConfigKey)) {\n logger.error(`Invalid config key: ${key}`);\n logger.info(`Valid keys: ${VALID_KEYS.join(\", \")}`);\n process.exit(1);\n }\n\n const config = loadConfig();\n const value = config[key as ConfigKey];\n\n if (value) {\n // Mask key for security\n const displayValue = key === \"key\" ? maskKey(value) : value;\n logger.keyValue(key, displayValue);\n } else {\n logger.info(`${key} is not set`);\n }\n}\n\n/**\n * List all config values\n */\nexport function configListCommand(): void {\n const config = loadConfig();\n\n logger.section(\"Configuration\");\n logger.keyValue(\"Config file\", getConfigPath());\n logger.blank();\n\n if (Object.keys(config).length === 0) {\n logger.info(\"No configuration set\");\n return;\n }\n\n for (const key of VALID_KEYS) {\n const value = config[key];\n if (value) {\n const displayValue = key === \"key\" ? maskKey(value) : value;\n logger.keyValue(key, displayValue);\n }\n }\n}\n\n/**\n * Delete a config value\n */\nexport function configDeleteCommand(key: string): void {\n if (!VALID_KEYS.includes(key as ConfigKey)) {\n logger.error(`Invalid config key: ${key}`);\n logger.info(`Valid keys: ${VALID_KEYS.join(\", \")}`);\n process.exit(1);\n }\n\n const config = loadConfig();\n delete config[key as ConfigKey];\n\n try {\n saveConfig(config);\n logger.success(`Config deleted: ${key}`);\n } catch (error) {\n const err = error as Error;\n logger.error(err.message);\n process.exit(1);\n }\n}\n\n/**\n * Mask a key for display (show only prefix and last 4 chars)\n */\nfunction maskKey(key: string): string {\n if (key.length <= 12) {\n return \"****\";\n }\n return `${key.slice(0, 8)}...${key.slice(-4)}`;\n}\n"],"mappings":"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","names":["config","path","path","config","path","config"]}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@liveport/cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI client for LivePort - secure localhost tunnels for AI agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "liveport": "./dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "files": [
12
+ "dist",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "dev": "tsup --watch",
18
+ "lint": "eslint src/",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "clean": "rm -rf dist",
22
+ "prepublishOnly": "pnpm build"
23
+ },
24
+ "keywords": [
25
+ "cli",
26
+ "tunnel",
27
+ "localhost",
28
+ "liveport",
29
+ "ai-agents",
30
+ "ngrok-alternative",
31
+ "developer-tools"
32
+ ],
33
+ "author": "LivePort",
34
+ "license": "MIT",
35
+ "homepage": "https://liveport.dev",
36
+ "bugs": {
37
+ "url": "https://github.com/dundas/liveport/issues"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/dundas/liveport.git",
42
+ "directory": "packages/cli"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "engines": {
48
+ "node": ">=18.0.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^20.10.0",
52
+ "@types/ws": "^8.5.0",
53
+ "tsup": "^8.0.0",
54
+ "typescript": "^5.3.0",
55
+ "vitest": "^2.0.0"
56
+ },
57
+ "dependencies": {
58
+ "chalk": "^5.3.0",
59
+ "commander": "^12.0.0",
60
+ "ora": "^8.0.0",
61
+ "ws": "^8.16.0"
62
+ }
63
+ }