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/package.json +1 -1
- package/src/core/RUN.js +71 -64
- package/src/core/index.js +384 -374
- package/src/core/start.js +762 -675
- package/src/utils/folder-picker.js +160 -140
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
console.log(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
console.log("\
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
console.log("");
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
console.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
+
});
|