devtunnel-cli 3.1.2 → 3.1.4

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/src/core/index.js CHANGED
@@ -1,374 +1,384 @@
1
- import { spawn } from "child_process";
2
- import { existsSync, readFileSync, writeFileSync } from "fs";
3
- import { homedir } from "os";
4
- import { join, dirname } from "path";
5
- import { fileURLToPath } from "url";
6
-
7
- // Get current directory (needed for ESM)
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = dirname(__filename);
10
-
11
- // Get port and project path from command line arguments
12
- const PORT = parseInt(process.argv[2]);
13
- const PROJECT_NAME = process.argv[3] || `Project (Port ${PORT})`;
14
- const PROJECT_PATH = process.argv[4] || process.cwd();
15
-
16
- // Import bundled cloudflared helpers
17
- const { getBinaryPath, hasBundledCloudflared } = await import("./setup-cloudflared.js");
18
-
19
- // No custom prefix - Cloudflare generates random URLs
20
-
21
- if (!PORT || isNaN(PORT) || PORT < 1 || PORT > 65535) {
22
- console.error("❌ Invalid port number!");
23
- console.error("Usage: node index.js <port> [projectName]");
24
- console.error("Example: node index.js 5173");
25
- process.exit(1);
26
- }
27
-
28
- let tunnelProcess;
29
- let currentTunnelType = null;
30
-
31
- console.log("");
32
- console.log("DevTunnel Tunnel Service");
33
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
34
- console.log(`Project: ${PROJECT_NAME}`);
35
- console.log(`Port: ${PORT}`);
36
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
37
- console.log("");
38
- console.log("Ensure dev server is running on port " + PORT);
39
- console.log("");
40
-
41
- // Check if project is Vite and auto-fix config for Cloudflare
42
- async function fixViteConfigForCloudflare() {
43
- const viteConfigPath = join(PROJECT_PATH, "vite.config.js");
44
- const viteConfigTsPath = join(PROJECT_PATH, "vite.config.ts");
45
-
46
- let configPath = null;
47
- if (existsSync(viteConfigPath)) configPath = viteConfigPath;
48
- else if (existsSync(viteConfigTsPath)) configPath = viteConfigTsPath;
49
-
50
- if (!configPath) return false;
51
-
52
- try {
53
- let config = readFileSync(configPath, "utf-8");
54
-
55
- // Check if already configured for external access
56
- if (config.includes("host: true") || config.includes("host:true") ||
57
- config.includes("host: '0.0.0.0'") || config.includes('host: "0.0.0.0"') ||
58
- config.includes('host:"0.0.0.0"') || config.includes("host:'0.0.0.0'")) {
59
- console.log("✅ Vite config already allows external access\n");
60
- return true;
61
- }
62
-
63
- console.log("📝 Auto-fixing Vite config for tunnel access...");
64
-
65
- let newConfig = config;
66
-
67
- // Case 1: Has existing server config
68
- if (config.includes("server:") || config.includes("server :")) {
69
- // Add host inside existing server block
70
- newConfig = config.replace(
71
- /(server\s*:\s*\{)/,
72
- `$1\n host: true, // Allow tunnel access`
73
- );
74
- }
75
- // Case 2: No server config, add it after defineConfig({
76
- else if (config.includes("defineConfig")) {
77
- newConfig = config.replace(
78
- /defineConfig\(\s*\{/,
79
- `defineConfig({\n server: {\n host: true, // Allow tunnel access\n },`
80
- );
81
- }
82
- // Case 3: Simple export default object
83
- else if (config.includes("export default {")) {
84
- newConfig = config.replace(
85
- /export\s+default\s+\{/,
86
- `export default {\n server: {\n host: true, // Allow tunnel access\n },`
87
- );
88
- }
89
-
90
- if (newConfig !== config) {
91
- // Backup original
92
- writeFileSync(configPath + ".backup", config);
93
- writeFileSync(configPath, newConfig);
94
- console.log("\n" + "=".repeat(60));
95
- console.log("✅ VITE CONFIG UPDATED!");
96
- console.log("=".repeat(60));
97
- console.log("⚠️ IMPORTANT: You MUST restart your dev server now!");
98
- console.log(" 1. Stop your dev server (Ctrl+C)");
99
- console.log(" 2. Run: npm run dev");
100
- console.log(" 3. Then run this tool again");
101
- console.log("\nBackup saved: " + configPath + ".backup");
102
- console.log("=".repeat(60) + "\n");
103
-
104
- // Wait for user to acknowledge
105
- console.log("Press Ctrl+C to exit and restart your dev server...\n");
106
- await new Promise(resolve => setTimeout(resolve, 60000)); // Wait 60 seconds
107
- return true;
108
- }
109
-
110
- console.log("⚠️ Could not auto-fix config. You may need to manually add:\n");
111
- console.log(" server: { host: true }\n");
112
- return false;
113
- } catch (error) {
114
- console.log("⚠️ Could not read Vite config:", error.message);
115
- return false;
116
- }
117
- }
118
-
119
- // Get cloudflared command (bundled or system)
120
- function getCloudflaredCommand() {
121
- return hasBundledCloudflared() ? getBinaryPath() : "cloudflared";
122
- }
123
-
124
- // Tunnel services to try in order (Cloudflare first - no password, fast)
125
- const TUNNEL_SERVICES = [
126
- {
127
- name: "Cloudflare",
128
- get command() {
129
- return getCloudflaredCommand();
130
- },
131
- args: ["tunnel", "--url", `http://localhost:${PORT}`],
132
- available: async () => {
133
- try {
134
- const cmd = getCloudflaredCommand();
135
- const result = spawn(cmd, ["--version"], { shell: true, stdio: "pipe" });
136
- return new Promise((resolve) => {
137
- result.on("close", (code) => resolve(code === 0));
138
- result.on("error", () => resolve(false));
139
- });
140
- } catch {
141
- return false;
142
- }
143
- },
144
- needsViteFix: true
145
- },
146
- {
147
- name: "Ngrok",
148
- command: "ngrok",
149
- args: ["http", PORT.toString()],
150
- available: async () => {
151
- try {
152
- const result = spawn("ngrok", ["--version"], { shell: true, stdio: "pipe" });
153
- return new Promise((resolve) => {
154
- result.on("close", (code) => resolve(code === 0));
155
- result.on("error", () => resolve(false));
156
- });
157
- } catch {
158
- return false;
159
- }
160
- },
161
- needsViteFix: true
162
- },
163
- {
164
- name: "LocalTunnel",
165
- command: "node",
166
- args: [join(__dirname, "tunnel-helpers.js"), "localtunnel", PORT.toString()],
167
- available: async () => {
168
- try {
169
- await import("localtunnel");
170
- return true;
171
- } catch {
172
- return false;
173
- }
174
- },
175
- needsViteFix: false,
176
- warning: "Note: LocalTunnel shows a password page on first visit (uses your public IP)"
177
- }
178
- ];
179
-
180
- // Try each tunnel service
181
- async function tryTunnelServices() {
182
- console.log("");
183
- console.log("Checking available tunnel services...");
184
- console.log("");
185
-
186
- let hasCloudflare = false;
187
-
188
- // Check if Cloudflare is available
189
- for (const service of TUNNEL_SERVICES) {
190
- if (service.name === "Cloudflare" && await service.available()) {
191
- hasCloudflare = true;
192
- break;
193
- }
194
- }
195
-
196
- // Show tip if Cloudflare not installed
197
- if (!hasCloudflare) {
198
- console.log("TIP: Install Cloudflare for best experience (no password, fastest)");
199
- console.log(" winget install Cloudflare.cloudflared\n");
200
- }
201
-
202
- for (const service of TUNNEL_SERVICES) {
203
- const available = await service.available();
204
-
205
- if (available) {
206
- console.log(`${service.name} is available`);
207
-
208
- // Show warning if exists
209
- if (service.warning) {
210
- console.log(service.warning);
211
- }
212
-
213
- // Skip Vite auto-fix - using proxy server instead
214
-
215
- console.log(`Starting ${service.name} tunnel...`);
216
- console.log("");
217
-
218
- currentTunnelType = service.name;
219
- tunnelProcess = spawn(service.command, service.args, {
220
- shell: true,
221
- stdio: "pipe"
222
- });
223
-
224
- setupTunnelHandlers(service.name);
225
-
226
- // Wait a bit to see if tunnel starts successfully
227
- await new Promise(resolve => setTimeout(resolve, 3000));
228
-
229
- // Check if process is still running
230
- if (tunnelProcess && !tunnelProcess.killed) {
231
- console.log(`Successfully connected via ${service.name}!`);
232
- if (service.name === "LocalTunnel") {
233
- console.log("Note: First-time visitors need to enter tunnel password (your public IP)");
234
- console.log("Get password at: https://loca.lt/mytunnelpassword\n");
235
- }
236
- console.log("Press Ctrl+C to stop the tunnel");
237
- console.log("");
238
- return true;
239
- }
240
- } else {
241
- console.log(`${service.name} not available`);
242
- }
243
- }
244
-
245
- console.log("\nNo tunnel services available!");
246
- console.log("\nRecommended: Install Cloudflare (fastest, no password):");
247
- console.log(" winget install Cloudflare.cloudflared");
248
- console.log("\nOr install Ngrok:");
249
- console.log(" Download from: https://ngrok.com/download");
250
- console.log("\nLocalTunnel is already installed but may require restart");
251
- process.exit(1);
252
- }
253
-
254
- function setupTunnelHandlers(serviceName) {
255
- if (!tunnelProcess) return;
256
-
257
- tunnelProcess.stdout.on("data", (data) => {
258
- const output = data.toString();
259
- const lines = output.split("\n");
260
-
261
- lines.forEach(line => {
262
- const trimmed = line.trim();
263
- if (!trimmed) return;
264
-
265
- // Extract URLs from different services
266
- if (serviceName === "Cloudflare") {
267
- // Look for trycloudflare.com URL
268
- if (trimmed.includes("trycloudflare.com")) {
269
- // Extract just the URL
270
- const urlMatch = trimmed.match(/(https?:\/\/[^\s]+trycloudflare\.com[^\s]*)/);
271
- if (urlMatch) {
272
- const url = urlMatch[1];
273
- console.log("");
274
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
275
- console.log("PUBLIC URL:");
276
- console.log(url);
277
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
278
- console.log("Share this URL with anyone!");
279
- console.log("");
280
- }
281
- }
282
- // Show other important messages (but filter out most INF/WRN logs)
283
- else if (!trimmed.includes("INF") && !trimmed.includes("WRN") && !trimmed.includes("+---")) {
284
- // Don't show these lines
285
- }
286
- } else if (serviceName === "Ngrok") {
287
- if (trimmed.includes("https://") || trimmed.includes("http://")) {
288
- const url = trimmed;
289
- console.log("");
290
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
291
- console.log("PUBLIC URL:");
292
- console.log(url);
293
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
294
- console.log("");
295
- }
296
- } else {
297
- // LocalTunnel or other services
298
- if (trimmed.includes("your url is:")) {
299
- const urlMatch = trimmed.match(/https?:\/\/[^\s]+/);
300
- if (urlMatch) {
301
- const url = urlMatch[0];
302
- console.log("");
303
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
304
- console.log("PUBLIC URL:");
305
- console.log(url);
306
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
307
- console.log("");
308
- }
309
- }
310
- }
311
- });
312
- });
313
-
314
- tunnelProcess.stderr.on("data", (data) => {
315
- const output = data.toString();
316
-
317
- // For Cloudflare, check if URL is in stderr (sometimes it is)
318
- if (serviceName === "Cloudflare" && output.includes("trycloudflare.com")) {
319
- const urlMatch = output.match(/(https?:\/\/[^\s]+trycloudflare\.com[^\s]*)/);
320
- if (urlMatch) {
321
- const url = urlMatch[1];
322
- console.log("");
323
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
324
- console.log("PUBLIC URL:");
325
- console.log(url);
326
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
327
- console.log("Share this URL with anyone!");
328
- console.log("");
329
- }
330
- }
331
-
332
- // Only show errors, not info messages
333
- if (!output.includes("INF") && !output.includes("WRN")) {
334
- const trimmed = output.trim();
335
- if (trimmed && !trimmed.includes("originCertPath")) {
336
- console.error(`Error: ${trimmed}`);
337
- }
338
- }
339
- });
340
-
341
- tunnelProcess.on("error", (error) => {
342
- console.error(`\n${serviceName} error:`, error.message);
343
- });
344
-
345
- tunnelProcess.on("exit", (code) => {
346
- if (code !== 0 && code !== null) {
347
- console.error(`\n${serviceName} exited with code ${code}`);
348
- }
349
- });
350
- }
351
-
352
- // Handle cleanup on exit
353
- function cleanup() {
354
- console.log("\nShutting down tunnel...");
355
- try {
356
- if (tunnelProcess) {
357
- tunnelProcess.kill();
358
- }
359
- } catch (e) {
360
- // Ignore errors during cleanup
361
- }
362
- setTimeout(() => {
363
- process.exit(0);
364
- }, 500);
365
- }
366
-
367
- process.on("SIGINT", cleanup);
368
- process.on("SIGTERM", cleanup);
369
-
370
- // Start trying tunnel services
371
- tryTunnelServices().catch((error) => {
372
- console.error("Error:", error);
373
- process.exit(1);
374
- });
1
+ import { spawn } from "child_process";
2
+ import { existsSync, readFileSync, writeFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join, dirname } from "path";
5
+ import { fileURLToPath } from "url";
6
+
7
+ // Get current directory (needed for ESM)
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ // Get port and project path from command line arguments
12
+ const PORT = parseInt(process.argv[2]);
13
+ const PROJECT_NAME = process.argv[3] || `Project (Port ${PORT})`;
14
+ const PROJECT_PATH = process.argv[4] || process.cwd();
15
+
16
+ // Import bundled cloudflared helpers
17
+ const { getBinaryPath, hasBundledCloudflared } = await import("./setup-cloudflared.js");
18
+
19
+ // No custom prefix - Cloudflare generates random URLs
20
+
21
+ if (!PORT || isNaN(PORT) || PORT < 1 || PORT > 65535) {
22
+ console.error("❌ Invalid port number!");
23
+ console.error("Usage: node index.js <port> [projectName]");
24
+ console.error("Example: node index.js 5173");
25
+ process.exit(1);
26
+ }
27
+
28
+ let tunnelProcess;
29
+ let currentTunnelType = null;
30
+
31
+ console.log("");
32
+ console.log("DevTunnel Tunnel Service");
33
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
34
+ console.log(`Project: ${PROJECT_NAME}`);
35
+ console.log(`Port: ${PORT}`);
36
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
37
+ console.log("");
38
+ console.log("Ensure dev server is running on port " + PORT);
39
+ console.log("");
40
+
41
+ // Check if project is Vite and auto-fix config for Cloudflare
42
+ async function fixViteConfigForCloudflare() {
43
+ const viteConfigPath = join(PROJECT_PATH, "vite.config.js");
44
+ const viteConfigTsPath = join(PROJECT_PATH, "vite.config.ts");
45
+
46
+ let configPath = null;
47
+ if (existsSync(viteConfigPath)) configPath = viteConfigPath;
48
+ else if (existsSync(viteConfigTsPath)) configPath = viteConfigTsPath;
49
+
50
+ if (!configPath) return false;
51
+
52
+ try {
53
+ let config = readFileSync(configPath, "utf-8");
54
+
55
+ // Check if already configured for external access
56
+ if (config.includes("host: true") || config.includes("host:true") ||
57
+ config.includes("host: '0.0.0.0'") || config.includes('host: "0.0.0.0"') ||
58
+ config.includes('host:"0.0.0.0"') || config.includes("host:'0.0.0.0'")) {
59
+ console.log("✅ Vite config already allows external access\n");
60
+ return true;
61
+ }
62
+
63
+ console.log("📝 Auto-fixing Vite config for tunnel access...");
64
+
65
+ let newConfig = config;
66
+
67
+ // Case 1: Has existing server config
68
+ if (config.includes("server:") || config.includes("server :")) {
69
+ // Add host inside existing server block
70
+ newConfig = config.replace(
71
+ /(server\s*:\s*\{)/,
72
+ `$1\n host: true, // Allow tunnel access`
73
+ );
74
+ }
75
+ // Case 2: No server config, add it after defineConfig({
76
+ else if (config.includes("defineConfig")) {
77
+ newConfig = config.replace(
78
+ /defineConfig\(\s*\{/,
79
+ `defineConfig({\n server: {\n host: true, // Allow tunnel access\n },`
80
+ );
81
+ }
82
+ // Case 3: Simple export default object
83
+ else if (config.includes("export default {")) {
84
+ newConfig = config.replace(
85
+ /export\s+default\s+\{/,
86
+ `export default {\n server: {\n host: true, // Allow tunnel access\n },`
87
+ );
88
+ }
89
+
90
+ if (newConfig !== config) {
91
+ // Backup original
92
+ writeFileSync(configPath + ".backup", config);
93
+ writeFileSync(configPath, newConfig);
94
+ console.log("\n" + "=".repeat(60));
95
+ console.log("✅ VITE CONFIG UPDATED!");
96
+ console.log("=".repeat(60));
97
+ console.log("⚠️ IMPORTANT: You MUST restart your dev server now!");
98
+ console.log(" 1. Stop your dev server (Ctrl+C)");
99
+ console.log(" 2. Run: npm run dev");
100
+ console.log(" 3. Then run this tool again");
101
+ console.log("\nBackup saved: " + configPath + ".backup");
102
+ console.log("=".repeat(60) + "\n");
103
+
104
+ // Wait for user to acknowledge
105
+ console.log("Press Ctrl+C to exit and restart your dev server...\n");
106
+ await new Promise(resolve => setTimeout(resolve, 60000)); // Wait 60 seconds
107
+ return true;
108
+ }
109
+
110
+ console.log("⚠️ Could not auto-fix config. You may need to manually add:\n");
111
+ console.log(" server: { host: true }\n");
112
+ return false;
113
+ } catch (error) {
114
+ console.log("⚠️ Could not read Vite config:", error.message);
115
+ return false;
116
+ }
117
+ }
118
+
119
+ // Get cloudflared command (bundled or system)
120
+ function getCloudflaredCommand() {
121
+ return hasBundledCloudflared() ? getBinaryPath() : "cloudflared";
122
+ }
123
+
124
+ // Tunnel services to try in order (Cloudflare first - no password, fast)
125
+ const TUNNEL_SERVICES = [
126
+ {
127
+ name: "Cloudflare",
128
+ get command() {
129
+ return getCloudflaredCommand();
130
+ },
131
+ args: ["tunnel", "--url", `http://localhost:${PORT}`],
132
+ available: async () => {
133
+ try {
134
+ const cmd = getCloudflaredCommand();
135
+ const result = spawn(cmd, ["--version"], { shell: true, stdio: "pipe" });
136
+ return new Promise((resolve) => {
137
+ result.on("close", (code) => resolve(code === 0));
138
+ result.on("error", () => resolve(false));
139
+ });
140
+ } catch {
141
+ return false;
142
+ }
143
+ },
144
+ needsViteFix: true
145
+ },
146
+ {
147
+ name: "Ngrok",
148
+ command: "ngrok",
149
+ args: ["http", PORT.toString()],
150
+ available: async () => {
151
+ try {
152
+ const result = spawn("ngrok", ["--version"], { shell: true, stdio: "pipe" });
153
+ return new Promise((resolve) => {
154
+ result.on("close", (code) => resolve(code === 0));
155
+ result.on("error", () => resolve(false));
156
+ });
157
+ } catch {
158
+ return false;
159
+ }
160
+ },
161
+ needsViteFix: true
162
+ },
163
+ {
164
+ name: "LocalTunnel",
165
+ command: "node",
166
+ args: [join(__dirname, "tunnel-helpers.js"), "localtunnel", PORT.toString()],
167
+ available: async () => {
168
+ try {
169
+ await import("localtunnel");
170
+ return true;
171
+ } catch {
172
+ return false;
173
+ }
174
+ },
175
+ needsViteFix: false,
176
+ warning: "Note: LocalTunnel shows a password page on first visit (uses your public IP)"
177
+ }
178
+ ];
179
+
180
+ // Try each tunnel service
181
+ async function tryTunnelServices() {
182
+ console.log("");
183
+ console.log("Checking available tunnel services...");
184
+ console.log("");
185
+
186
+ let hasCloudflare = false;
187
+
188
+ // Check if Cloudflare is available
189
+ for (const service of TUNNEL_SERVICES) {
190
+ if (service.name === "Cloudflare" && await service.available()) {
191
+ hasCloudflare = true;
192
+ break;
193
+ }
194
+ }
195
+
196
+ // Show tip if Cloudflare not installed
197
+ if (!hasCloudflare) {
198
+ console.log("TIP: Install Cloudflare for best experience (no password, fastest)");
199
+ console.log(" winget install Cloudflare.cloudflared\n");
200
+ }
201
+
202
+ for (const service of TUNNEL_SERVICES) {
203
+ const available = await service.available();
204
+
205
+ if (available) {
206
+ console.log(`${service.name} is available`);
207
+
208
+ // Show warning if exists
209
+ if (service.warning) {
210
+ console.log(service.warning);
211
+ }
212
+
213
+ // Skip Vite auto-fix - using proxy server instead
214
+
215
+ console.log(`Starting ${service.name} tunnel...`);
216
+ console.log("");
217
+
218
+ currentTunnelType = service.name;
219
+ tunnelProcess = spawn(service.command, service.args, {
220
+ shell: true,
221
+ stdio: "pipe"
222
+ });
223
+
224
+ tunnelProcess.on("error", (err) => {
225
+ console.error("\nERROR: Tunnel process failed to start:", err.code === "ENOENT" ? `"${service.command}" not found` : err.message);
226
+ process.exit(1);
227
+ });
228
+
229
+ setupTunnelHandlers(service.name);
230
+
231
+ // Wait a bit to see if tunnel starts successfully
232
+ await new Promise(resolve => setTimeout(resolve, 3000));
233
+
234
+ // Check if process is still running
235
+ if (tunnelProcess && !tunnelProcess.killed) {
236
+ console.log(`Successfully connected via ${service.name}!`);
237
+ if (service.name === "LocalTunnel") {
238
+ console.log("Note: First-time visitors need to enter tunnel password (your public IP)");
239
+ console.log("Get password at: https://loca.lt/mytunnelpassword\n");
240
+ }
241
+ console.log("Press Ctrl+C to stop the tunnel");
242
+ console.log("");
243
+ return true;
244
+ }
245
+ } else {
246
+ console.log(`${service.name} not available`);
247
+ }
248
+ }
249
+
250
+ console.log("\nNo tunnel services available!");
251
+ console.log("\nRecommended: Install Cloudflare (fastest, no password):");
252
+ console.log(" winget install Cloudflare.cloudflared");
253
+ console.log("\nOr install Ngrok:");
254
+ console.log(" Download from: https://ngrok.com/download");
255
+ console.log("\nLocalTunnel is already installed but may require restart");
256
+ process.exit(1);
257
+ }
258
+
259
+ function setupTunnelHandlers(serviceName) {
260
+ if (!tunnelProcess) return;
261
+ if (!tunnelProcess.stdout || !tunnelProcess.stderr) return;
262
+
263
+ tunnelProcess.stdout.on("data", (data) => {
264
+ const output = data.toString();
265
+ const lines = output.split("\n");
266
+
267
+ lines.forEach(line => {
268
+ const trimmed = line.trim();
269
+ if (!trimmed) return;
270
+
271
+ // Extract URLs from different services
272
+ if (serviceName === "Cloudflare") {
273
+ // Look for trycloudflare.com URL
274
+ if (trimmed.includes("trycloudflare.com")) {
275
+ // Extract just the URL
276
+ const urlMatch = trimmed.match(/(https?:\/\/[^\s]+trycloudflare\.com[^\s]*)/);
277
+ if (urlMatch) {
278
+ const url = urlMatch[1];
279
+ console.log("");
280
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
281
+ console.log("PUBLIC URL:");
282
+ console.log(url);
283
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
284
+ console.log("Share this URL with anyone!");
285
+ console.log("If you see a white screen, wait a few seconds and refresh.");
286
+ console.log("");
287
+ }
288
+ }
289
+ // Show other important messages (but filter out most INF/WRN logs)
290
+ else if (!trimmed.includes("INF") && !trimmed.includes("WRN") && !trimmed.includes("+---")) {
291
+ // Don't show these lines
292
+ }
293
+ } else if (serviceName === "Ngrok") {
294
+ if (trimmed.includes("https://") || trimmed.includes("http://")) {
295
+ const url = trimmed;
296
+ console.log("");
297
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
298
+ console.log("PUBLIC URL:");
299
+ console.log(url);
300
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
301
+ console.log("If you see a white screen, wait a few seconds and refresh.");
302
+ console.log("");
303
+ }
304
+ } else {
305
+ // LocalTunnel or other services
306
+ if (trimmed.includes("your url is:")) {
307
+ const urlMatch = trimmed.match(/https?:\/\/[^\s]+/);
308
+ if (urlMatch) {
309
+ const url = urlMatch[0];
310
+ console.log("");
311
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
312
+ console.log("PUBLIC URL:");
313
+ console.log(url);
314
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
315
+ console.log("If you see a white screen, wait a few seconds and refresh.");
316
+ console.log("");
317
+ }
318
+ }
319
+ }
320
+ });
321
+ });
322
+
323
+ tunnelProcess.stderr?.on("data", (data) => {
324
+ const output = data.toString();
325
+
326
+ // For Cloudflare, check if URL is in stderr (sometimes it is)
327
+ if (serviceName === "Cloudflare" && output.includes("trycloudflare.com")) {
328
+ const urlMatch = output.match(/(https?:\/\/[^\s]+trycloudflare\.com[^\s]*)/);
329
+ if (urlMatch) {
330
+ const url = urlMatch[1];
331
+ console.log("");
332
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
333
+ console.log("PUBLIC URL:");
334
+ console.log(url);
335
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
336
+ console.log("Share this URL with anyone!");
337
+ console.log("If you see a white screen, wait a few seconds and refresh.");
338
+ console.log("");
339
+ }
340
+ }
341
+
342
+ // Only show errors, not info messages
343
+ if (!output.includes("INF") && !output.includes("WRN")) {
344
+ const trimmed = output.trim();
345
+ if (trimmed && !trimmed.includes("originCertPath")) {
346
+ console.error(`Error: ${trimmed}`);
347
+ }
348
+ }
349
+ });
350
+
351
+ tunnelProcess.on("error", (error) => {
352
+ console.error(`\n${serviceName} error:`, error.message);
353
+ });
354
+
355
+ tunnelProcess.on("exit", (code) => {
356
+ if (code !== 0 && code !== null) {
357
+ console.error(`\n${serviceName} exited with code ${code}`);
358
+ }
359
+ });
360
+ }
361
+
362
+ // Handle cleanup on exit
363
+ function cleanup() {
364
+ console.log("\nShutting down tunnel...");
365
+ try {
366
+ if (tunnelProcess) {
367
+ tunnelProcess.kill();
368
+ }
369
+ } catch (e) {
370
+ // Ignore errors during cleanup
371
+ }
372
+ setTimeout(() => {
373
+ process.exit(0);
374
+ }, 500);
375
+ }
376
+
377
+ process.on("SIGINT", cleanup);
378
+ process.on("SIGTERM", cleanup);
379
+
380
+ // Start trying tunnel services
381
+ tryTunnelServices().catch((error) => {
382
+ console.error("Error:", error);
383
+ process.exit(1);
384
+ });