devtunnel-cli 3.0.44 → 3.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.
@@ -1,427 +1,427 @@
1
- import { spawn, exec } from 'child_process';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import https from 'https';
5
- import { fileURLToPath } from 'url';
6
- import { dirname } from 'path';
7
- import { promisify } from 'util';
8
-
9
- const execAsync = promisify(exec);
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = dirname(__filename);
12
-
13
- const BIN_DIR = path.join(__dirname, '../../bin');
14
- const CLOUDFLARED_VERSION = '2024.8.2'; // Latest stable version
15
-
16
- // Binary URLs with multiple mirrors for reliability
17
- const DOWNLOAD_URLS = {
18
- win32: [
19
- `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-windows-amd64.exe`,
20
- `https://cloudflared-releases.pages.dev/${CLOUDFLARED_VERSION}/cloudflared-windows-amd64.exe`
21
- ],
22
- darwin: [
23
- `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-darwin-amd64`,
24
- `https://cloudflared-releases.pages.dev/${CLOUDFLARED_VERSION}/cloudflared-darwin-amd64`
25
- ],
26
- linux: [
27
- `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64`,
28
- `https://cloudflared-releases.pages.dev/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64`
29
- ]
30
- };
31
-
32
- // Get platform display name
33
- function getPlatformName() {
34
- const platform = process.platform;
35
- return platform === 'win32' ? 'Windows' : platform === 'darwin' ? 'macOS' : 'Linux';
36
- }
37
-
38
- export function getBinaryPath() {
39
- const platform = process.platform;
40
- const binName = platform === 'win32' ? 'cloudflared.exe' : 'cloudflared';
41
- return path.join(BIN_DIR, platform, binName);
42
- }
43
-
44
- function hasEnoughDiskSpace() {
45
- try {
46
- const stats = fs.statfsSync ? fs.statfsSync(BIN_DIR) : null;
47
- if (stats) {
48
- const availableSpace = stats.bavail * stats.bsize;
49
- const requiredSpace = 50 * 1024 * 1024;
50
- return availableSpace > requiredSpace;
51
- }
52
- return true;
53
- } catch {
54
- return true;
55
- }
56
- }
57
-
58
- function safeUnlink(filePath) {
59
- try {
60
- if (fs.existsSync(filePath)) {
61
- fs.unlinkSync(filePath);
62
- }
63
- } catch (err) {
64
- // Ignore permission errors - file might be locked or in use
65
- }
66
- }
67
-
68
- async function isAdmin() {
69
- if (process.platform !== 'win32') {
70
- return process.getuid && process.getuid() === 0;
71
- }
72
-
73
- try {
74
- const { stdout } = await execAsync('net session');
75
- return stdout.length > 0;
76
- } catch {
77
- return false;
78
- }
79
- }
80
-
81
- function showPermissionSolutions(dirPath) {
82
- console.log('\nšŸ’” Solutions:');
83
- if (process.platform === 'win32') {
84
- console.log(' 1. Run terminal as Administrator (Right-click → Run as administrator)');
85
- console.log(' 2. DevTunnel will automatically request admin privileges if needed');
86
- } else {
87
- console.log(' 1. Run with sudo: sudo npm i -g devtunnel-cli');
88
- }
89
- console.log(' 2. Check if antivirus is blocking file writes');
90
- console.log(' 3. Check folder permissions for:', dirPath);
91
- console.log(' 4. Try installing manually: https://github.com/cloudflare/cloudflared/releases\n');
92
- }
93
-
94
- function downloadFile(url, dest, retryCount = 0) {
95
- return new Promise((resolve, reject) => {
96
- const dir = path.dirname(dest);
97
- try {
98
- if (!fs.existsSync(dir)) {
99
- fs.mkdirSync(dir, { recursive: true });
100
- }
101
- } catch (err) {
102
- reject(new Error(`Cannot create directory: ${err.message}. Try running as administrator or choose a different location.`));
103
- return;
104
- }
105
-
106
- const tempDest = dest + '.download';
107
-
108
- // Clean up any existing temp file first
109
- safeUnlink(tempDest);
110
-
111
- let file;
112
- try {
113
- file = fs.createWriteStream(tempDest);
114
- } catch (err) {
115
- if (err.code === 'EPERM' || err.code === 'EACCES') {
116
- reject(new Error(`Permission denied: Cannot write to ${dir}. Try running as administrator or check antivirus settings.`));
117
- } else {
118
- reject(new Error(`Cannot create download file: ${err.message}`));
119
- }
120
- return;
121
- }
122
-
123
- const request = https.get(url, {
124
- headers: {
125
- 'User-Agent': 'DevTunnel/3.0',
126
- 'Accept': '*/*'
127
- },
128
- timeout: 30000 // 30 second timeout
129
- }, (response) => {
130
- if (response.statusCode === 302 || response.statusCode === 301) {
131
- file.close();
132
- safeUnlink(tempDest);
133
- downloadFile(response.headers.location, dest, retryCount)
134
- .then(resolve)
135
- .catch(reject);
136
- return;
137
- }
138
-
139
- if (response.statusCode !== 200) {
140
- file.close();
141
- safeUnlink(tempDest);
142
- reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
143
- return;
144
- }
145
-
146
- const totalSize = parseInt(response.headers['content-length'], 10);
147
- let downloaded = 0;
148
- let lastPercent = 0;
149
-
150
- response.on('data', (chunk) => {
151
- downloaded += chunk.length;
152
- if (totalSize) {
153
- const percent = Math.round((downloaded / totalSize) * 100);
154
- if (percent !== lastPercent && percent % 5 === 0) {
155
- const mb = (downloaded / 1024 / 1024).toFixed(1);
156
- const totalMb = (totalSize / 1024 / 1024).toFixed(1);
157
- process.stdout.write(`\rā³ Downloading: ${percent}% (${mb}/${totalMb} MB)`);
158
- lastPercent = percent;
159
- }
160
- }
161
- });
162
-
163
- response.pipe(file);
164
-
165
- file.on('finish', () => {
166
- file.close(() => {
167
- // Move temp file to final destination
168
- try {
169
- if (fs.existsSync(dest)) {
170
- fs.unlinkSync(dest);
171
- }
172
- fs.renameSync(tempDest, dest);
173
-
174
- console.log('\nāœ… Download complete');
175
-
176
- // Make executable on Unix-like systems
177
- if (process.platform !== 'win32') {
178
- try {
179
- fs.chmodSync(dest, 0o755);
180
- console.log('āœ… Permissions set (executable)');
181
- } catch (err) {
182
- console.log('āš ļø Warning: Could not set executable permissions');
183
- console.log(' Run: chmod +x ' + dest);
184
- }
185
- }
186
-
187
- // Verify file size
188
- const stats = fs.statSync(dest);
189
- if (stats.size < 1000000) { // Less than 1MB is suspicious
190
- fs.unlinkSync(dest);
191
- reject(new Error('Downloaded file is too small (corrupted)'));
192
- return;
193
- }
194
-
195
- resolve();
196
- } catch (err) {
197
- reject(new Error(`Cannot finalize download: ${err.message}`));
198
- }
199
- });
200
- });
201
- });
202
-
203
- request.on('timeout', () => {
204
- request.destroy();
205
- file.close();
206
- safeUnlink(tempDest);
207
- reject(new Error('Download timeout (30 seconds)'));
208
- });
209
-
210
- request.on('error', (err) => {
211
- file.close();
212
- safeUnlink(tempDest);
213
- reject(err);
214
- });
215
-
216
- file.on('error', (err) => {
217
- file.close();
218
- safeUnlink(tempDest);
219
- if (err.code === 'EPERM' || err.code === 'EACCES') {
220
- reject(new Error(`Permission denied: Cannot write to ${tempDest}. Try running as administrator or check antivirus settings.`));
221
- } else {
222
- reject(new Error(`File write error: ${err.message}`));
223
- }
224
- });
225
- });
226
- }
227
-
228
- // Try downloading from multiple URLs with retries
229
- async function downloadWithRetry(urls, dest, maxRetries = 3) {
230
- for (let urlIndex = 0; urlIndex < urls.length; urlIndex++) {
231
- const url = urls[urlIndex];
232
- console.log(`šŸ“„ Source: ${urlIndex === 0 ? 'GitHub' : 'Mirror'} (${urlIndex + 1}/${urls.length})`);
233
-
234
- for (let retry = 0; retry < maxRetries; retry++) {
235
- try {
236
- if (retry > 0) {
237
- console.log(`šŸ”„ Retry ${retry}/${maxRetries - 1}...`);
238
- await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2s before retry
239
- }
240
-
241
- await downloadFile(url, dest, retry);
242
- return true; // Success!
243
-
244
- } catch (err) {
245
- const isLastRetry = retry === maxRetries - 1;
246
- const isLastUrl = urlIndex === urls.length - 1;
247
-
248
- if (err.message.includes('Permission denied') || err.message.includes('EPERM') || err.message.includes('EACCES')) {
249
- console.log(`\nāŒ Permission Error: ${err.message}`);
250
-
251
- if (process.platform === 'win32' && retry === 0) {
252
- const admin = await isAdmin();
253
- if (!admin) {
254
- console.log('\nšŸ” Attempting to request administrator privileges...');
255
- console.log(' Please click "Yes" on the UAC prompt\n');
256
-
257
- try {
258
- const nodePath = process.execPath;
259
- const scriptPath = process.argv[1];
260
- const proc = spawn('powershell', [
261
- '-NoProfile',
262
- '-NonInteractive',
263
- '-ExecutionPolicy', 'Bypass',
264
- '-Command', `Start-Process -FilePath "${nodePath}" -ArgumentList "${scriptPath}" -Verb RunAs -Wait`
265
- ], {
266
- stdio: 'inherit',
267
- shell: false
268
- });
269
-
270
- proc.on('close', () => {
271
- process.exit(0);
272
- });
273
-
274
- proc.on('error', () => {
275
- console.log('\nāš ļø Could not elevate privileges automatically');
276
- showPermissionSolutions(path.dirname(dest));
277
- });
278
-
279
- return false;
280
- } catch {
281
- showPermissionSolutions(path.dirname(dest));
282
- throw err;
283
- }
284
- }
285
- }
286
-
287
- showPermissionSolutions(path.dirname(dest));
288
- throw err;
289
- } else if (err.message.includes('ENOTFOUND') || err.message.includes('ECONNREFUSED')) {
290
- console.log(`\nāŒ Network error: ${err.message}`);
291
- } else if (err.message.includes('timeout')) {
292
- console.log(`\nāŒ Download timeout`);
293
- } else {
294
- console.log(`\nāŒ Error: ${err.message}`);
295
- }
296
-
297
- if (isLastRetry && isLastUrl) {
298
- throw new Error(`All download attempts failed: ${err.message}`);
299
- }
300
-
301
- if (isLastRetry) {
302
- console.log('šŸ’” Trying alternative source...\n');
303
- break;
304
- }
305
- }
306
- }
307
- }
308
-
309
- throw new Error('All download sources failed');
310
- }
311
-
312
- export async function setupCloudflared() {
313
- const platform = process.platform;
314
- const binaryPath = getBinaryPath();
315
-
316
- console.log('\n╔════════════════════════════════════════╗');
317
- console.log('ā•‘ šŸ“¦ Cloudflare Setup (First Run) ā•‘');
318
- console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n');
319
-
320
- // Check if binary already exists
321
- if (fs.existsSync(binaryPath)) {
322
- try {
323
- // Verify it works
324
- const testProc = spawn(binaryPath, ['--version'], { shell: true, stdio: 'pipe' });
325
- const works = await new Promise((resolve) => {
326
- testProc.on('close', (code) => resolve(code === 0));
327
- testProc.on('error', () => resolve(false));
328
- setTimeout(() => resolve(false), 5000);
329
- });
330
-
331
- if (works) {
332
- console.log('āœ… Cloudflare already installed and working\n');
333
- return binaryPath;
334
- } else {
335
- console.log('āš ļø Existing binary not working, re-downloading...\n');
336
- safeUnlink(binaryPath);
337
- }
338
- } catch {
339
- console.log('āš ļø Existing binary corrupted, re-downloading...\n');
340
- safeUnlink(binaryPath);
341
- }
342
- }
343
-
344
- const urls = DOWNLOAD_URLS[platform];
345
- if (!urls) {
346
- console.error(`āŒ ERROR: Platform "${platform}" not supported`);
347
- console.error(' Supported: Windows, macOS, Linux\n');
348
- return null;
349
- }
350
-
351
- console.log(`šŸ–„ļø Platform: ${getPlatformName()}`);
352
- console.log(`šŸ“ Install to: ${binaryPath}`);
353
- console.log(`šŸ“Š Size: ~40 MB\n`);
354
-
355
- // Check disk space
356
- if (!hasEnoughDiskSpace()) {
357
- console.error('āŒ ERROR: Not enough disk space (need 50+ MB)\n');
358
- return null;
359
- }
360
-
361
- // Check write permissions
362
- try {
363
- const dir = path.dirname(binaryPath);
364
- if (!fs.existsSync(dir)) {
365
- fs.mkdirSync(dir, { recursive: true });
366
- }
367
- const testFile = path.join(dir, '.write-test');
368
- fs.writeFileSync(testFile, 'test');
369
- fs.unlinkSync(testFile);
370
- } catch (err) {
371
- console.error('āŒ ERROR: Cannot write to installation directory');
372
- console.error(` Location: ${path.dirname(binaryPath)}`);
373
- console.error(` Reason: ${err.message}\n`);
374
- return null;
375
- }
376
-
377
- console.log('šŸ“„ Starting download...\n');
378
-
379
- try {
380
- await downloadWithRetry(urls, binaryPath);
381
-
382
- // Final verification
383
- console.log('\nšŸ” Verifying installation...');
384
- const testProc = spawn(binaryPath, ['--version'], { shell: true, stdio: 'pipe' });
385
- const works = await new Promise((resolve) => {
386
- testProc.on('close', (code) => resolve(code === 0));
387
- testProc.on('error', () => resolve(false));
388
- setTimeout(() => resolve(false), 5000);
389
- });
390
-
391
- if (works) {
392
- console.log('āœ… Verification successful!');
393
- console.log('āœ… Cloudflare ready to use\n');
394
- return binaryPath;
395
- } else {
396
- console.error('āŒ Downloaded binary not working properly');
397
- safeUnlink(binaryPath);
398
- return null;
399
- }
400
-
401
- } catch (err) {
402
- console.error('\n╔════════════════════════════════════════╗');
403
- console.error('ā•‘ āŒ Installation Failed ā•‘');
404
- console.error('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n');
405
- console.error(`Reason: ${err.message}\n`);
406
-
407
- if (err.message.includes('Permission denied') || err.message.includes('EPERM') || err.message.includes('EACCES')) {
408
- showPermissionSolutions(path.dirname(binaryPath));
409
- } else {
410
- console.log('šŸ’” Troubleshooting:');
411
- console.log(' 1. Check internet connection');
412
- console.log(' 2. Check firewall/antivirus settings');
413
- console.log(' 3. Try running as administrator');
414
- console.log(' 4. Install manually: https://github.com/cloudflare/cloudflared/releases\n');
415
- }
416
-
417
- console.log('šŸ”„ DevTunnel will use fallback tunnels (Ngrok/LocalTunnel)\n');
418
-
419
- return null;
420
- }
421
- }
422
-
423
- // Check if bundled cloudflared exists and is working
424
- export function hasBundledCloudflared() {
425
- const binaryPath = getBinaryPath();
426
- return fs.existsSync(binaryPath);
427
- }
1
+ import { spawn, exec } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import https from 'https';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname } from 'path';
7
+ import { promisify } from 'util';
8
+
9
+ const execAsync = promisify(exec);
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
13
+ const BIN_DIR = path.join(__dirname, '../../bin');
14
+ const CLOUDFLARED_VERSION = '2024.8.2'; // Latest stable version
15
+
16
+ // Binary URLs with multiple mirrors for reliability
17
+ const DOWNLOAD_URLS = {
18
+ win32: [
19
+ `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-windows-amd64.exe`,
20
+ `https://cloudflared-releases.pages.dev/${CLOUDFLARED_VERSION}/cloudflared-windows-amd64.exe`
21
+ ],
22
+ darwin: [
23
+ `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-darwin-amd64`,
24
+ `https://cloudflared-releases.pages.dev/${CLOUDFLARED_VERSION}/cloudflared-darwin-amd64`
25
+ ],
26
+ linux: [
27
+ `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64`,
28
+ `https://cloudflared-releases.pages.dev/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64`
29
+ ]
30
+ };
31
+
32
+ // Get platform display name
33
+ function getPlatformName() {
34
+ const platform = process.platform;
35
+ return platform === 'win32' ? 'Windows' : platform === 'darwin' ? 'macOS' : 'Linux';
36
+ }
37
+
38
+ export function getBinaryPath() {
39
+ const platform = process.platform;
40
+ const binName = platform === 'win32' ? 'cloudflared.exe' : 'cloudflared';
41
+ return path.join(BIN_DIR, platform, binName);
42
+ }
43
+
44
+ function hasEnoughDiskSpace() {
45
+ try {
46
+ const stats = fs.statfsSync ? fs.statfsSync(BIN_DIR) : null;
47
+ if (stats) {
48
+ const availableSpace = stats.bavail * stats.bsize;
49
+ const requiredSpace = 50 * 1024 * 1024;
50
+ return availableSpace > requiredSpace;
51
+ }
52
+ return true;
53
+ } catch {
54
+ return true;
55
+ }
56
+ }
57
+
58
+ function safeUnlink(filePath) {
59
+ try {
60
+ if (fs.existsSync(filePath)) {
61
+ fs.unlinkSync(filePath);
62
+ }
63
+ } catch (err) {
64
+ // Ignore permission errors - file might be locked or in use
65
+ }
66
+ }
67
+
68
+ async function isAdmin() {
69
+ if (process.platform !== 'win32') {
70
+ return process.getuid && process.getuid() === 0;
71
+ }
72
+
73
+ try {
74
+ const { stdout } = await execAsync('net session');
75
+ return stdout.length > 0;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ function showPermissionSolutions(dirPath) {
82
+ console.log('\nšŸ’” Solutions:');
83
+ if (process.platform === 'win32') {
84
+ console.log(' 1. Run terminal as Administrator (Right-click → Run as administrator)');
85
+ console.log(' 2. DevTunnel will automatically request admin privileges if needed');
86
+ } else {
87
+ console.log(' 1. Run with sudo: sudo npm i -g devtunnel-cli');
88
+ }
89
+ console.log(' 2. Check if antivirus is blocking file writes');
90
+ console.log(' 3. Check folder permissions for:', dirPath);
91
+ console.log(' 4. Try installing manually: https://github.com/cloudflare/cloudflared/releases\n');
92
+ }
93
+
94
+ function downloadFile(url, dest, retryCount = 0) {
95
+ return new Promise((resolve, reject) => {
96
+ const dir = path.dirname(dest);
97
+ try {
98
+ if (!fs.existsSync(dir)) {
99
+ fs.mkdirSync(dir, { recursive: true });
100
+ }
101
+ } catch (err) {
102
+ reject(new Error(`Cannot create directory: ${err.message}. Try running as administrator or choose a different location.`));
103
+ return;
104
+ }
105
+
106
+ const tempDest = dest + '.download';
107
+
108
+ // Clean up any existing temp file first
109
+ safeUnlink(tempDest);
110
+
111
+ let file;
112
+ try {
113
+ file = fs.createWriteStream(tempDest);
114
+ } catch (err) {
115
+ if (err.code === 'EPERM' || err.code === 'EACCES') {
116
+ reject(new Error(`Permission denied: Cannot write to ${dir}. Try running as administrator or check antivirus settings.`));
117
+ } else {
118
+ reject(new Error(`Cannot create download file: ${err.message}`));
119
+ }
120
+ return;
121
+ }
122
+
123
+ const request = https.get(url, {
124
+ headers: {
125
+ 'User-Agent': 'DevTunnel/3.0',
126
+ 'Accept': '*/*'
127
+ },
128
+ timeout: 30000 // 30 second timeout
129
+ }, (response) => {
130
+ if (response.statusCode === 302 || response.statusCode === 301) {
131
+ file.close();
132
+ safeUnlink(tempDest);
133
+ downloadFile(response.headers.location, dest, retryCount)
134
+ .then(resolve)
135
+ .catch(reject);
136
+ return;
137
+ }
138
+
139
+ if (response.statusCode !== 200) {
140
+ file.close();
141
+ safeUnlink(tempDest);
142
+ reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
143
+ return;
144
+ }
145
+
146
+ const totalSize = parseInt(response.headers['content-length'], 10);
147
+ let downloaded = 0;
148
+ let lastPercent = 0;
149
+
150
+ response.on('data', (chunk) => {
151
+ downloaded += chunk.length;
152
+ if (totalSize) {
153
+ const percent = Math.round((downloaded / totalSize) * 100);
154
+ if (percent !== lastPercent && percent % 5 === 0) {
155
+ const mb = (downloaded / 1024 / 1024).toFixed(1);
156
+ const totalMb = (totalSize / 1024 / 1024).toFixed(1);
157
+ process.stdout.write(`\rā³ Downloading: ${percent}% (${mb}/${totalMb} MB)`);
158
+ lastPercent = percent;
159
+ }
160
+ }
161
+ });
162
+
163
+ response.pipe(file);
164
+
165
+ file.on('finish', () => {
166
+ file.close(() => {
167
+ // Move temp file to final destination
168
+ try {
169
+ if (fs.existsSync(dest)) {
170
+ fs.unlinkSync(dest);
171
+ }
172
+ fs.renameSync(tempDest, dest);
173
+
174
+ console.log('\nāœ… Download complete');
175
+
176
+ // Make executable on Unix-like systems
177
+ if (process.platform !== 'win32') {
178
+ try {
179
+ fs.chmodSync(dest, 0o755);
180
+ console.log('āœ… Permissions set (executable)');
181
+ } catch (err) {
182
+ console.log('āš ļø Warning: Could not set executable permissions');
183
+ console.log(' Run: chmod +x ' + dest);
184
+ }
185
+ }
186
+
187
+ // Verify file size
188
+ const stats = fs.statSync(dest);
189
+ if (stats.size < 1000000) { // Less than 1MB is suspicious
190
+ fs.unlinkSync(dest);
191
+ reject(new Error('Downloaded file is too small (corrupted)'));
192
+ return;
193
+ }
194
+
195
+ resolve();
196
+ } catch (err) {
197
+ reject(new Error(`Cannot finalize download: ${err.message}`));
198
+ }
199
+ });
200
+ });
201
+ });
202
+
203
+ request.on('timeout', () => {
204
+ request.destroy();
205
+ file.close();
206
+ safeUnlink(tempDest);
207
+ reject(new Error('Download timeout (30 seconds)'));
208
+ });
209
+
210
+ request.on('error', (err) => {
211
+ file.close();
212
+ safeUnlink(tempDest);
213
+ reject(err);
214
+ });
215
+
216
+ file.on('error', (err) => {
217
+ file.close();
218
+ safeUnlink(tempDest);
219
+ if (err.code === 'EPERM' || err.code === 'EACCES') {
220
+ reject(new Error(`Permission denied: Cannot write to ${tempDest}. Try running as administrator or check antivirus settings.`));
221
+ } else {
222
+ reject(new Error(`File write error: ${err.message}`));
223
+ }
224
+ });
225
+ });
226
+ }
227
+
228
+ // Try downloading from multiple URLs with retries
229
+ async function downloadWithRetry(urls, dest, maxRetries = 3) {
230
+ for (let urlIndex = 0; urlIndex < urls.length; urlIndex++) {
231
+ const url = urls[urlIndex];
232
+ console.log(`šŸ“„ Source: ${urlIndex === 0 ? 'GitHub' : 'Mirror'} (${urlIndex + 1}/${urls.length})`);
233
+
234
+ for (let retry = 0; retry < maxRetries; retry++) {
235
+ try {
236
+ if (retry > 0) {
237
+ console.log(`šŸ”„ Retry ${retry}/${maxRetries - 1}...`);
238
+ await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2s before retry
239
+ }
240
+
241
+ await downloadFile(url, dest, retry);
242
+ return true; // Success!
243
+
244
+ } catch (err) {
245
+ const isLastRetry = retry === maxRetries - 1;
246
+ const isLastUrl = urlIndex === urls.length - 1;
247
+
248
+ if (err.message.includes('Permission denied') || err.message.includes('EPERM') || err.message.includes('EACCES')) {
249
+ console.log(`\nāŒ Permission Error: ${err.message}`);
250
+
251
+ if (process.platform === 'win32' && retry === 0) {
252
+ const admin = await isAdmin();
253
+ if (!admin) {
254
+ console.log('\nšŸ” Attempting to request administrator privileges...');
255
+ console.log(' Please click "Yes" on the UAC prompt\n');
256
+
257
+ try {
258
+ const nodePath = process.execPath;
259
+ const scriptPath = process.argv[1];
260
+ const proc = spawn('powershell', [
261
+ '-NoProfile',
262
+ '-NonInteractive',
263
+ '-ExecutionPolicy', 'Bypass',
264
+ '-Command', `Start-Process -FilePath "${nodePath}" -ArgumentList "${scriptPath}" -Verb RunAs -Wait`
265
+ ], {
266
+ stdio: 'inherit',
267
+ shell: false
268
+ });
269
+
270
+ proc.on('close', () => {
271
+ process.exit(0);
272
+ });
273
+
274
+ proc.on('error', () => {
275
+ console.log('\nāš ļø Could not elevate privileges automatically');
276
+ showPermissionSolutions(path.dirname(dest));
277
+ });
278
+
279
+ return false;
280
+ } catch {
281
+ showPermissionSolutions(path.dirname(dest));
282
+ throw err;
283
+ }
284
+ }
285
+ }
286
+
287
+ showPermissionSolutions(path.dirname(dest));
288
+ throw err;
289
+ } else if (err.message.includes('ENOTFOUND') || err.message.includes('ECONNREFUSED')) {
290
+ console.log(`\nāŒ Network error: ${err.message}`);
291
+ } else if (err.message.includes('timeout')) {
292
+ console.log(`\nāŒ Download timeout`);
293
+ } else {
294
+ console.log(`\nāŒ Error: ${err.message}`);
295
+ }
296
+
297
+ if (isLastRetry && isLastUrl) {
298
+ throw new Error(`All download attempts failed: ${err.message}`);
299
+ }
300
+
301
+ if (isLastRetry) {
302
+ console.log('šŸ’” Trying alternative source...\n');
303
+ break;
304
+ }
305
+ }
306
+ }
307
+ }
308
+
309
+ throw new Error('All download sources failed');
310
+ }
311
+
312
+ export async function setupCloudflared() {
313
+ const platform = process.platform;
314
+ const binaryPath = getBinaryPath();
315
+
316
+ console.log('\n╔════════════════════════════════════════╗');
317
+ console.log('ā•‘ šŸ“¦ Cloudflare Setup (First Run) ā•‘');
318
+ console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n');
319
+
320
+ // Check if binary already exists
321
+ if (fs.existsSync(binaryPath)) {
322
+ try {
323
+ // Verify it works
324
+ const testProc = spawn(binaryPath, ['--version'], { shell: true, stdio: 'pipe' });
325
+ const works = await new Promise((resolve) => {
326
+ testProc.on('close', (code) => resolve(code === 0));
327
+ testProc.on('error', () => resolve(false));
328
+ setTimeout(() => resolve(false), 5000);
329
+ });
330
+
331
+ if (works) {
332
+ console.log('āœ… Cloudflare already installed and working\n');
333
+ return binaryPath;
334
+ } else {
335
+ console.log('āš ļø Existing binary not working, re-downloading...\n');
336
+ safeUnlink(binaryPath);
337
+ }
338
+ } catch {
339
+ console.log('āš ļø Existing binary corrupted, re-downloading...\n');
340
+ safeUnlink(binaryPath);
341
+ }
342
+ }
343
+
344
+ const urls = DOWNLOAD_URLS[platform];
345
+ if (!urls) {
346
+ console.error(`āŒ ERROR: Platform "${platform}" not supported`);
347
+ console.error(' Supported: Windows, macOS, Linux\n');
348
+ return null;
349
+ }
350
+
351
+ console.log(`šŸ–„ļø Platform: ${getPlatformName()}`);
352
+ console.log(`šŸ“ Install to: ${binaryPath}`);
353
+ console.log(`šŸ“Š Size: ~40 MB\n`);
354
+
355
+ // Check disk space
356
+ if (!hasEnoughDiskSpace()) {
357
+ console.error('āŒ ERROR: Not enough disk space (need 50+ MB)\n');
358
+ return null;
359
+ }
360
+
361
+ // Check write permissions
362
+ try {
363
+ const dir = path.dirname(binaryPath);
364
+ if (!fs.existsSync(dir)) {
365
+ fs.mkdirSync(dir, { recursive: true });
366
+ }
367
+ const testFile = path.join(dir, '.write-test');
368
+ fs.writeFileSync(testFile, 'test');
369
+ fs.unlinkSync(testFile);
370
+ } catch (err) {
371
+ console.error('āŒ ERROR: Cannot write to installation directory');
372
+ console.error(` Location: ${path.dirname(binaryPath)}`);
373
+ console.error(` Reason: ${err.message}\n`);
374
+ return null;
375
+ }
376
+
377
+ console.log('šŸ“„ Starting download...\n');
378
+
379
+ try {
380
+ await downloadWithRetry(urls, binaryPath);
381
+
382
+ // Final verification
383
+ console.log('\nšŸ” Verifying installation...');
384
+ const testProc = spawn(binaryPath, ['--version'], { shell: true, stdio: 'pipe' });
385
+ const works = await new Promise((resolve) => {
386
+ testProc.on('close', (code) => resolve(code === 0));
387
+ testProc.on('error', () => resolve(false));
388
+ setTimeout(() => resolve(false), 5000);
389
+ });
390
+
391
+ if (works) {
392
+ console.log('āœ… Verification successful!');
393
+ console.log('āœ… Cloudflare ready to use\n');
394
+ return binaryPath;
395
+ } else {
396
+ console.error('āŒ Downloaded binary not working properly');
397
+ safeUnlink(binaryPath);
398
+ return null;
399
+ }
400
+
401
+ } catch (err) {
402
+ console.error('\n╔════════════════════════════════════════╗');
403
+ console.error('ā•‘ āŒ Installation Failed ā•‘');
404
+ console.error('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n');
405
+ console.error(`Reason: ${err.message}\n`);
406
+
407
+ if (err.message.includes('Permission denied') || err.message.includes('EPERM') || err.message.includes('EACCES')) {
408
+ showPermissionSolutions(path.dirname(binaryPath));
409
+ } else {
410
+ console.log('šŸ’” Troubleshooting:');
411
+ console.log(' 1. Check internet connection');
412
+ console.log(' 2. Check firewall/antivirus settings');
413
+ console.log(' 3. Try running as administrator');
414
+ console.log(' 4. Install manually: https://github.com/cloudflare/cloudflared/releases\n');
415
+ }
416
+
417
+ console.log('šŸ”„ DevTunnel will use fallback tunnels (Ngrok/LocalTunnel)\n');
418
+
419
+ return null;
420
+ }
421
+ }
422
+
423
+ // Check if bundled cloudflared exists and is working
424
+ export function hasBundledCloudflared() {
425
+ const binaryPath = getBinaryPath();
426
+ return fs.existsSync(binaryPath);
427
+ }