nport 1.0.5 → 2.0.1

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 CHANGED
@@ -1,68 +1,263 @@
1
1
  # <img src="https://nport.link/assets/imgs/nport-logo.png" height="30" style="vertical-align: middle;"> NPort
2
2
 
3
- > A Node.js tool for exposing local servers through Socket.IO tunnels
3
+ > 🚀 Free & open source ngrok alternative - Tunnel localhost to the internet via Cloudflare Edge
4
4
 
5
5
  [![GitHub](https://img.shields.io/github/stars/tuanngocptn/nport?style=social)](https://github.com/tuanngocptn/nport)
6
6
  [![NPM](https://img.shields.io/npm/v/nport?color=red&logo=npm)](https://www.npmjs.com/package/nport)
7
7
  [![Website](https://img.shields.io/website?url=https%3A%2F%2Fnport.link&up_message=nport.link&up_color=blue&down_color=lightgrey&down_message=offline)](https://nport.link)
8
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
8
9
 
9
10
  ## What is NPort?
10
11
 
11
- ![nport](https://github.com/user-attachments/assets/3f61ae7a-bff7-45d8-8f40-8e04b301a63a)
12
+ [![nport](https://github.com/user-attachments/assets/6e188d75-4633-4efa-a7f4-63df6a4a7ac8)](https://nport.link)
12
13
 
13
- NPort allows you to expose your local HTTP servers to the internet using Socket.IO streams. Perfect for:
14
- - Development environments
15
- - Testing webhooks
16
- - Sharing local projects
14
+ NPort is a powerful, lightweight **ngrok alternative** that creates secure HTTP/HTTPS tunnels from your localhost to public URLs using **Cloudflare's global edge network**. No configuration, no accounts, just instant tunnels with custom subdomains!
15
+
16
+ Perfect for:
17
+ - 🚀 **Development environments** - Share your local work instantly
18
+ - 🔒 **Testing webhooks** - Receive webhooks from GitHub, Stripe, PayPal, etc.
19
+ - 📱 **Mobile testing** - Test your web app on real devices
20
+ - đŸ› ī¸ **API development** - Debug integrations with external services
21
+ - đŸ‘Ĩ **Demo to clients** - Show your progress without deployment
17
22
 
18
23
  ## ✨ Features
19
24
 
20
- - 🔒 **Secure Tunneling**: Share your local server safely using Socket.IO
21
- - 🚀 **Easy Setup**: Minimal configuration required
22
- - 🌐 **Custom Domains**: Get readable URLs like `https://yourname.nport.link`
23
- - 📡 **WebSocket Ready**: Full WebSocket connection support
24
- - đŸ’ģ **Cross-Platform**: Runs on Windows, macOS, and Linux
25
+ - ⚡ **Instant Setup**: One command to expose your localhost
26
+ - 🌐 **Custom Subdomains**: Choose your own URL (e.g., `myapp.nport.link`)
27
+ - 🔒 **Automatic HTTPS**: SSL/TLS encryption via Cloudflare
28
+ - 🌍 **Global Edge Network**: Fast connections worldwide via Cloudflare
29
+ - 📡 **WebSocket Support**: Full WebSocket and Server-Sent Events support
30
+ - đŸŽ¯ **No Configuration**: Works out of the box
31
+ - đŸ’ģ **Cross-Platform**: Windows, macOS, and Linux support
32
+ - 🆓 **100% Free**: No accounts, no limits, no paywalls
33
+ - 🔓 **Open Source**: MIT licensed
25
34
 
26
35
  ## đŸ“Ļ Installation
27
36
 
28
- **NPM Package**
29
- ```bash
30
- # Local installation
31
- npm install nport
37
+ ### NPM (Recommended)
32
38
 
39
+ ```bash
33
40
  # Global installation
34
41
  npm install -g nport
42
+
43
+ # Or use npx without installation
44
+ npx nport 3000 -s myapp
35
45
  ```
36
46
 
37
- **From GitHub**
38
- ```bash
39
- # Local installation
40
- npm install git+https://github.com/tuanngocptn/nport.git
47
+ ### From GitHub
41
48
 
42
- # Global installation
49
+ ```bash
43
50
  npm install -g git+https://github.com/tuanngocptn/nport.git
44
51
  ```
45
52
 
46
53
  ## 🚀 Quick Start
47
54
 
48
55
  ### Basic Usage
56
+
57
+ Expose port 3000 with a random subdomain:
49
58
  ```bash
50
- # Local installation
51
- npx nport -s myapp -p 3000
59
+ nport 3000
60
+ ```
52
61
 
53
- # Global installation
54
- nport -s myapp -p 3000
62
+ Output:
55
63
  ```
56
- This will create a tunnel at `https://myapp.nport.link`
64
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
65
+ NPort - Free & Open Source ngrok Alternative
66
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
67
+ 🌐 Website: https://nport.link
68
+ đŸ“Ļ NPM: https://www.npmjs.com/package/nport
69
+ đŸ’ģ GitHub: https://github.com/tuanngocptn/nport
70
+ ☕ Support: https://buymeacoffee.com/tuanngocptn
71
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
57
72
 
58
- ### Advanced Options
73
+ 🚀 Starting Tunnel for port 3000...
74
+ ✔ Tunnel created!
75
+ 🌍 Public URL: https://user-1234.nport.link
76
+ ```
77
+
78
+ ### Custom Subdomain
79
+
80
+ Choose your own subdomain:
81
+ ```bash
82
+ nport 3000 -s myapp
83
+ # Creates: https://myapp.nport.link
84
+ ```
85
+
86
+ Or using long form:
59
87
  ```bash
60
- # Full configuration
61
- npx nport --server https://server.nport.link \
62
- --subdomain myapp \
63
- --hostname 127.0.0.1 \
64
- --port 3000
88
+ nport 3000 --subdomain myapp
65
89
  ```
66
90
 
67
- ## 📝 Credits
68
- Inspired by [Socket Tunnel](https://github.com/ericbarch/socket-tunnel). Read more about the concept in this [blog post](https://ericbarch.com/post/sockettunnel/).
91
+ ## 📖 Usage Examples
92
+
93
+ ### Web Development
94
+
95
+ ```bash
96
+ # Next.js
97
+ npm run dev
98
+ nport 3000 -s my-nextjs-app
99
+
100
+ # React (Create React App)
101
+ npm start
102
+ nport 3000 -s my-react-app
103
+
104
+ # Vue.js
105
+ npm run dev
106
+ nport 8080 -s my-vue-app
107
+
108
+ # Express.js
109
+ node server.js
110
+ nport 3000 -s my-api
111
+ ```
112
+
113
+ ### Webhook Testing
114
+
115
+ ```bash
116
+ # Start your local webhook receiver
117
+ node webhook-receiver.js
118
+
119
+ # Expose it to the internet
120
+ nport 4000 -s my-webhooks
121
+
122
+ # Use in GitHub webhook settings:
123
+ # https://my-webhooks.nport.link/webhook
124
+ ```
125
+
126
+ ### Mobile Device Testing
127
+
128
+ ```bash
129
+ # Start your local dev server
130
+ npm run dev
131
+
132
+ # Create tunnel
133
+ nport 3000 -s mobile-test
134
+
135
+ # Open on your phone:
136
+ # https://mobile-test.nport.link
137
+ ```
138
+
139
+ ## đŸŽ¯ CLI Options
140
+
141
+ ```bash
142
+ nport <port> [options]
143
+ ```
144
+
145
+ | Option | Short | Description | Example |
146
+ |--------|-------|-------------|---------|
147
+ | `<port>` | - | Local port to tunnel (default: 8080) | `nport 3000` |
148
+ | `--subdomain` | `-s` | Custom subdomain | `nport 3000 -s myapp` |
149
+ | `-s=value` | - | Alternative format | `nport 3000 -s=myapp` |
150
+
151
+ ## 🔧 How It Works
152
+
153
+ 1. **You run** `nport 3000 -s myapp`
154
+ 2. **NPort creates** a Cloudflare Tunnel
155
+ 3. **DNS record** is created: `myapp.nport.link` → Cloudflare Edge
156
+ 4. **Cloudflared binary** connects your localhost:3000 to Cloudflare
157
+ 5. **Traffic flows** through Cloudflare's global network to your machine
158
+ 6. **On exit** (Ctrl+C), tunnel and DNS are automatically cleaned up
159
+
160
+ ```
161
+ Internet → Cloudflare Edge → Cloudflare Tunnel → Your localhost:3000
162
+ (https://myapp.nport.link)
163
+ ```
164
+
165
+ ## đŸ›Ąī¸ Security
166
+
167
+ - **HTTPS by default**: All tunnels use SSL/TLS encryption
168
+ - **Cloudflare protection**: DDoS protection and security features
169
+ - **Automatic cleanup**: Tunnels are removed when you stop the process
170
+ - **No data logging**: We don't store or log your traffic
171
+
172
+ ## 🆚 Comparison with ngrok
173
+
174
+ | Feature | NPort | ngrok |
175
+ |---------|-------|-------|
176
+ | Price | 100% Free | Free tier limited |
177
+ | Custom subdomains | ✅ Always | ❌ Paid only |
178
+ | HTTPS | ✅ Always | ✅ |
179
+ | Account required | ❌ No | ✅ Yes |
180
+ | Time limits | ❌ None | âš ī¸ Free tier limited |
181
+ | Open source | ✅ MIT | ❌ Proprietary |
182
+ | Global network | ✅ Cloudflare | ✅ ngrok Edge |
183
+
184
+ ## 🧹 Cleanup
185
+
186
+ NPort automatically cleans up resources when you:
187
+ - Press **Ctrl+C** to exit
188
+ - Kill the process
189
+ - Terminal closes
190
+
191
+ The cleanup process:
192
+ 1. ✅ Deletes DNS record (`myapp.nport.link`)
193
+ 2. ✅ Removes Cloudflare Tunnel
194
+ 3. ✅ Stops cloudflared process
195
+
196
+ ## 🐛 Troubleshooting
197
+
198
+ ### Binary not found
199
+
200
+ If you see "Cloudflared binary not found":
201
+ ```bash
202
+ npm install -g nport --force
203
+ ```
204
+
205
+ ### Port already in use
206
+
207
+ Make sure your local server is running on the specified port:
208
+ ```bash
209
+ # Check if something is listening on port 3000
210
+ lsof -i :3000 # macOS/Linux
211
+ netstat -ano | findstr :3000 # Windows
212
+ ```
213
+
214
+ ### Subdomain already taken
215
+
216
+ Choose a different subdomain name:
217
+ ```bash
218
+ nport 3000 -s myapp-v2
219
+ ```
220
+
221
+ ### Connection issues
222
+
223
+ The `ERR Cannot determine default origin certificate path` warning is harmless and can be ignored. It appears because cloudflared checks for certificate-based authentication (we use token-based instead).
224
+
225
+ ## 🤝 Contributing
226
+
227
+ Contributions are welcome! Please feel free to submit a Pull Request.
228
+
229
+ 1. Fork the repository
230
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
231
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
232
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
233
+ 5. Open a Pull Request
234
+
235
+ ## 💖 Support
236
+
237
+ If you find NPort useful, please consider supporting the project:
238
+
239
+ - ⭐ [Star on GitHub](https://github.com/tuanngocptn/nport)
240
+ - ☕ [Buy me a coffee](https://buymeacoffee.com/tuanngocptn)
241
+ - đŸ’Ŧ Share with your friends and colleagues
242
+ - 🐛 [Report bugs](https://github.com/tuanngocptn/nport/issues)
243
+
244
+ ## 📄 License
245
+
246
+ [MIT License](LICENSE) - Feel free to use NPort in your projects!
247
+
248
+ ## 🙏 Credits
249
+
250
+ - Created by [Nick Pham](https://github.com/tuanngocptn) đŸ‡ģđŸ‡ŗ
251
+ - Powered by [Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/)
252
+ - Inspired by [ngrok](https://ngrok.com) and [localtunnel](https://github.com/localtunnel/localtunnel)
253
+
254
+ ## 🔗 Links
255
+
256
+ - 🌐 Website: [https://nport.link](https://nport.link)
257
+ - đŸ“Ļ NPM: [https://www.npmjs.com/package/nport](https://www.npmjs.com/package/nport)
258
+ - đŸ’ģ GitHub: [https://github.com/tuanngocptn/nport](https://github.com/tuanngocptn/nport)
259
+ - 📧 Email: tuanngocptn@gmail.com
260
+
261
+ ---
262
+
263
+ Made with â¤ī¸ by [Nick Pham](https://github.com/tuanngocptn)
package/bin-manager.js ADDED
@@ -0,0 +1,378 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import https from "https";
4
+ import os from "os";
5
+ import { execSync } from "child_process";
6
+ import { fileURLToPath } from "url";
7
+
8
+ // ============================================================================
9
+ // Configuration & Constants
10
+ // ============================================================================
11
+
12
+ // Fix for __dirname in ES modules
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+
16
+ // Binary configuration
17
+ const BIN_DIR = path.join(__dirname, "bin");
18
+ const BINARY_NAME = "cloudflared";
19
+ const COMPRESSED_SUFFIX = ".tgz";
20
+ const TEMP_ARCHIVE_NAME = "cloudflared.tgz";
21
+
22
+ // Platform detection
23
+ const PLATFORM = os.platform();
24
+ const ARCH = os.arch();
25
+ const IS_WINDOWS = PLATFORM === "win32";
26
+ const IS_MACOS = PLATFORM === "darwin";
27
+ const IS_LINUX = PLATFORM === "linux";
28
+
29
+ // Binary paths
30
+ const BIN_NAME = IS_WINDOWS ? `${BINARY_NAME}.exe` : BINARY_NAME;
31
+ const BIN_PATH = path.join(BIN_DIR, BIN_NAME);
32
+
33
+ // Download configuration
34
+ const GITHUB_BASE_URL = "https://github.com/cloudflare/cloudflared/releases/latest/download";
35
+ const REDIRECT_CODES = [301, 302];
36
+ const SUCCESS_CODE = 200;
37
+ const UNIX_EXECUTABLE_MODE = "755";
38
+
39
+ // ============================================================================
40
+ // Platform Detection
41
+ // ============================================================================
42
+
43
+ /**
44
+ * Platform and architecture mapping for cloudflared releases
45
+ */
46
+ const PLATFORM_MAPPINGS = {
47
+ darwin: {
48
+ amd64: "cloudflared-darwin-amd64.tgz",
49
+ arm64: "cloudflared-darwin-amd64.tgz", // macOS uses universal binary
50
+ },
51
+ win32: {
52
+ x64: "cloudflared-windows-amd64.exe",
53
+ ia32: "cloudflared-windows-386.exe",
54
+ },
55
+ linux: {
56
+ x64: "cloudflared-linux-amd64",
57
+ arm64: "cloudflared-linux-arm64",
58
+ arm: "cloudflared-linux-arm",
59
+ },
60
+ };
61
+
62
+ /**
63
+ * Normalizes architecture name for mapping lookup
64
+ * @param {string} arch - Raw architecture from os.arch()
65
+ * @returns {string} Normalized architecture name
66
+ */
67
+ function normalizeArch(arch) {
68
+ const archMap = {
69
+ x64: "x64",
70
+ amd64: "amd64",
71
+ arm64: "arm64",
72
+ ia32: "ia32",
73
+ arm: "arm",
74
+ };
75
+ return archMap[arch] || arch;
76
+ }
77
+
78
+ /**
79
+ * Determines the download URL based on current platform and architecture
80
+ * @returns {string} Download URL for cloudflared binary
81
+ * @throws {Error} If platform/architecture combination is not supported
82
+ */
83
+ function getDownloadUrl() {
84
+ const normalizedArch = normalizeArch(ARCH);
85
+ const platformMapping = PLATFORM_MAPPINGS[PLATFORM];
86
+
87
+ if (!platformMapping) {
88
+ throw new Error(
89
+ `Unsupported platform: ${PLATFORM}. Supported platforms: darwin, win32, linux`
90
+ );
91
+ }
92
+
93
+ const binaryName = platformMapping[normalizedArch];
94
+
95
+ if (!binaryName) {
96
+ throw new Error(
97
+ `Unsupported architecture: ${ARCH} for platform ${PLATFORM}. ` +
98
+ `Supported architectures: ${Object.keys(platformMapping).join(", ")}`
99
+ );
100
+ }
101
+
102
+ return `${GITHUB_BASE_URL}/${binaryName}`;
103
+ }
104
+
105
+ /**
106
+ * Checks if the download URL points to a compressed archive
107
+ * @param {string} url - Download URL
108
+ * @returns {boolean} True if URL is for a compressed file
109
+ */
110
+ function isCompressedArchive(url) {
111
+ return url.endsWith(COMPRESSED_SUFFIX);
112
+ }
113
+
114
+ // ============================================================================
115
+ // File System Utilities
116
+ // ============================================================================
117
+
118
+ /**
119
+ * Ensures directory exists, creates it if it doesn't
120
+ * @param {string} dirPath - Directory path to ensure
121
+ */
122
+ function ensureDirectory(dirPath) {
123
+ if (!fs.existsSync(dirPath)) {
124
+ fs.mkdirSync(dirPath, { recursive: true });
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Safely removes a file if it exists
130
+ * @param {string} filePath - Path to file to remove
131
+ */
132
+ function safeUnlink(filePath) {
133
+ try {
134
+ if (fs.existsSync(filePath)) {
135
+ fs.unlinkSync(filePath);
136
+ }
137
+ } catch (err) {
138
+ // Ignore errors during cleanup
139
+ console.warn(`Warning: Could not remove ${filePath}:`, err.message);
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Sets executable permissions on Unix-like systems
145
+ * @param {string} filePath - Path to file
146
+ * @param {string} mode - Permission mode (e.g., "755")
147
+ */
148
+ function setExecutablePermissions(filePath, mode = UNIX_EXECUTABLE_MODE) {
149
+ if (!IS_WINDOWS) {
150
+ fs.chmodSync(filePath, mode);
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Validates that a file exists at the given path
156
+ * @param {string} filePath - Path to validate
157
+ * @param {string} errorMessage - Error message if file doesn't exist
158
+ * @throws {Error} If file doesn't exist
159
+ */
160
+ function validateFileExists(filePath, errorMessage) {
161
+ if (!fs.existsSync(filePath)) {
162
+ throw new Error(errorMessage || `File not found: ${filePath}`);
163
+ }
164
+ }
165
+
166
+ // ============================================================================
167
+ // Download Utilities
168
+ // ============================================================================
169
+
170
+ /**
171
+ * Downloads a file from a URL with automatic redirect handling
172
+ * @param {string} url - URL to download from
173
+ * @param {string} dest - Destination file path
174
+ * @returns {Promise<string>} Resolves with destination path on success
175
+ */
176
+ async function downloadFile(url, dest) {
177
+ return new Promise((resolve, reject) => {
178
+ const file = fs.createWriteStream(dest);
179
+
180
+ https
181
+ .get(url, (response) => {
182
+ // Handle redirects
183
+ if (REDIRECT_CODES.includes(response.statusCode)) {
184
+ file.close();
185
+ safeUnlink(dest);
186
+ downloadFile(response.headers.location, dest)
187
+ .then(resolve)
188
+ .catch(reject);
189
+ return;
190
+ }
191
+
192
+ // Handle non-success status codes
193
+ if (response.statusCode !== SUCCESS_CODE) {
194
+ file.close();
195
+ safeUnlink(dest);
196
+ reject(
197
+ new Error(
198
+ `Download failed with status code ${response.statusCode} from ${url}`
199
+ )
200
+ );
201
+ return;
202
+ }
203
+
204
+ // Stream response to file
205
+ response.pipe(file);
206
+
207
+ file.on("finish", () => {
208
+ file.close(() => resolve(dest));
209
+ });
210
+
211
+ file.on("error", (err) => {
212
+ file.close();
213
+ safeUnlink(dest);
214
+ reject(err);
215
+ });
216
+ })
217
+ .on("error", (err) => {
218
+ file.close();
219
+ safeUnlink(dest);
220
+ reject(new Error(`Network error: ${err.message}`));
221
+ });
222
+ });
223
+ }
224
+
225
+ // ============================================================================
226
+ // Archive Extraction
227
+ // ============================================================================
228
+
229
+ /**
230
+ * Extracts a .tgz archive to a directory
231
+ * @param {string} archivePath - Path to .tgz file
232
+ * @param {string} targetDir - Directory to extract to
233
+ * @throws {Error} If extraction fails
234
+ */
235
+ function extractTarGz(archivePath, targetDir) {
236
+ try {
237
+ execSync(`tar -xzf "${archivePath}" -C "${targetDir}"`, {
238
+ stdio: "pipe", // Suppress output
239
+ });
240
+ } catch (err) {
241
+ throw new Error(`Extraction failed: ${err.message}`);
242
+ }
243
+ }
244
+
245
+ // ============================================================================
246
+ // Logging Utilities
247
+ // ============================================================================
248
+
249
+ /**
250
+ * Console logging with consistent formatting
251
+ */
252
+ const logger = {
253
+ info: (msg) => console.log(`â„šī¸ ${msg}`),
254
+ success: (msg) => console.log(`✅ ${msg}`),
255
+ warn: (msg) => console.warn(`âš ī¸ ${msg}`),
256
+ error: (msg) => console.error(`❌ ${msg}`),
257
+ progress: (msg) => console.log(`🚧 ${msg}`),
258
+ extract: (msg) => console.log(`đŸ“Ļ ${msg}`),
259
+ };
260
+
261
+ // ============================================================================
262
+ // Core Binary Management
263
+ // ============================================================================
264
+
265
+ /**
266
+ * Downloads and installs the cloudflared binary
267
+ * @returns {Promise<string>} Path to installed binary
268
+ */
269
+ async function installBinary() {
270
+ logger.progress(
271
+ "Cloudflared binary not found. Downloading... (This happens only once)"
272
+ );
273
+
274
+ const url = getDownloadUrl();
275
+ const isArchive = isCompressedArchive(url);
276
+ const downloadDest = isArchive
277
+ ? path.join(BIN_DIR, TEMP_ARCHIVE_NAME)
278
+ : BIN_PATH;
279
+
280
+ try {
281
+ // Download binary or archive
282
+ await downloadFile(url, downloadDest);
283
+
284
+ // Extract if it's an archive (macOS)
285
+ if (isArchive) {
286
+ logger.extract("Extracting binary...");
287
+ extractTarGz(downloadDest, BIN_DIR);
288
+
289
+ // Clean up archive
290
+ safeUnlink(downloadDest);
291
+
292
+ // Validate extraction succeeded
293
+ validateFileExists(
294
+ BIN_PATH,
295
+ "Extraction failed: Binary not found after extraction"
296
+ );
297
+ }
298
+
299
+ // Set executable permissions (Unix-like systems)
300
+ setExecutablePermissions(BIN_PATH);
301
+
302
+ logger.success("Download complete.");
303
+ return BIN_PATH;
304
+ } catch (error) {
305
+ // Clean up any partial downloads
306
+ safeUnlink(downloadDest);
307
+ safeUnlink(BIN_PATH);
308
+ throw error;
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Ensures cloudflared binary is available, downloading if necessary
314
+ * @returns {Promise<string>} Path to cloudflared binary
315
+ */
316
+ export async function ensureCloudflared() {
317
+ // Ensure bin directory exists
318
+ ensureDirectory(BIN_DIR);
319
+
320
+ // Check if binary already exists
321
+ if (fs.existsSync(BIN_PATH)) {
322
+ // Always ensure permissions are set correctly (in case they were lost)
323
+ setExecutablePermissions(BIN_PATH);
324
+ return BIN_PATH;
325
+ }
326
+
327
+ // Download and install
328
+ try {
329
+ return await installBinary();
330
+ } catch (error) {
331
+ logger.error(`Installation failed: ${error.message}`);
332
+ process.exit(1);
333
+ }
334
+ }
335
+
336
+ // ============================================================================
337
+ // CLI Entry Point
338
+ // ============================================================================
339
+
340
+ /**
341
+ * Checks if we're running in a CI environment
342
+ * @returns {boolean} True if in CI environment
343
+ */
344
+ function isCI() {
345
+ return !!(
346
+ process.env.CI || // Generic CI flag
347
+ process.env.GITHUB_ACTIONS || // GitHub Actions
348
+ process.env.GITLAB_CI || // GitLab CI
349
+ process.env.CIRCLECI || // CircleCI
350
+ process.env.TRAVIS || // Travis CI
351
+ process.env.JENKINS_URL || // Jenkins
352
+ process.env.BUILDKITE // Buildkite
353
+ );
354
+ }
355
+
356
+ /**
357
+ * Main function when run directly from command line
358
+ */
359
+ async function main() {
360
+ // Skip binary download in CI environments to keep package lightweight
361
+ if (isCI()) {
362
+ logger.info("Running in CI environment - skipping binary download");
363
+ return;
364
+ }
365
+
366
+ try {
367
+ const binaryPath = await ensureCloudflared();
368
+ logger.success(`Cloudflared binary is ready at: ${binaryPath}`);
369
+ } catch (error) {
370
+ logger.error(error.message);
371
+ process.exit(1);
372
+ }
373
+ }
374
+
375
+ // Run if executed directly
376
+ if (process.argv[1] === __filename) {
377
+ main();
378
+ }