@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 +88 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +759 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
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
|
package/dist/index.d.ts
ADDED
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
|
+
}
|