mekong-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # mekong-cli
2
+
3
+ Run your dev server and a [Mekong](https://mekongtunnel.dev) tunnel together in a single command — no separate terminal needed.
4
+
5
+ Think of it as the glue between `next dev` / `vite` / `nuxt dev` and `mekong <port>`.
6
+
7
+ ---
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install -g mekong-cli
13
+ ```
14
+
15
+ Or use it without installing:
16
+
17
+ ```bash
18
+ npx mekong-cli 3000
19
+ ```
20
+
21
+ ---
22
+
23
+ ## Requirements
24
+
25
+ - **Node.js 14+**
26
+ - **mekong binary** installed separately (see below)
27
+
28
+ ---
29
+
30
+ ## Install the mekong binary
31
+
32
+ Download from the [GitHub releases page](https://github.com/MuyleangIng/MekongTunnel/releases/tag/v1.4.9).
33
+
34
+ **Linux / macOS (amd64) — quick one-liner:**
35
+
36
+ ```bash
37
+ curl -sL https://github.com/MuyleangIng/MekongTunnel/releases/download/v1.4.9/mekong-linux-amd64 \
38
+ -o /usr/local/bin/mekong && chmod +x /usr/local/bin/mekong
39
+ ```
40
+
41
+ **macOS (Apple Silicon):**
42
+
43
+ ```bash
44
+ curl -sL https://github.com/MuyleangIng/MekongTunnel/releases/download/v1.4.9/mekong-darwin-arm64 \
45
+ -o /usr/local/bin/mekong && chmod +x /usr/local/bin/mekong
46
+ ```
47
+
48
+ **Windows:** Download `mekong-windows-amd64.exe`, rename it to `mekong.exe`, and place it somewhere on your `PATH` (e.g. `C:\Windows\System32\` or `%USERPROFILE%\AppData\Local\`).
49
+
50
+ ---
51
+
52
+ ## Usage
53
+
54
+ ### Tunnel an already-running server
55
+
56
+ ```bash
57
+ mekong-cli 3000
58
+ ```
59
+
60
+ ### Start server + tunnel together
61
+
62
+ ```bash
63
+ mekong-cli --with "next dev" --port 3000
64
+ mekong-cli --with "vite" --port 5173
65
+ mekong-cli --with "nuxt dev" --port 3000
66
+ mekong-cli --with "ng serve" --port 4200
67
+ mekong-cli --with "astro dev" --port 4321
68
+ ```
69
+
70
+ ### Extra options
71
+
72
+ ```bash
73
+ # Set tunnel expiry
74
+ mekong-cli --with "next dev" --port 3000 --expire 2h
75
+
76
+ # Run mekong in background (daemon mode)
77
+ mekong-cli --with "next dev" --port 3000 --daemon
78
+
79
+ # Suppress QR code
80
+ mekong-cli --with "next dev" --port 3000 --no-qr
81
+
82
+ # Use a custom mekong binary path
83
+ mekong-cli --with "next dev" --port 3000 --mekong ~/bin/mekong
84
+ ```
85
+
86
+ ### All options
87
+
88
+ ```
89
+ --with <cmd> Shell command to start the dev server
90
+ --port <n> Local port (auto-detected from package.json if omitted)
91
+ --expire <val> Expiry duration passed to mekong (e.g. 2h, 30m)
92
+ --daemon Run mekong in the background (-d flag)
93
+ --no-qr Suppress QR code output
94
+ --mekong <path> Custom path to the mekong binary
95
+ --help Show help
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Add to package.json scripts
101
+
102
+ You can wire `mekong-cli` directly into your project's `package.json` so the whole team uses it consistently:
103
+
104
+ ```json
105
+ {
106
+ "scripts": {
107
+ "dev": "next dev",
108
+ "tunnel": "mekong-cli --with \"next dev\" --port 3000",
109
+ "tunnel:share": "mekong-cli --with \"next dev\" --port 3000 --expire 2h"
110
+ }
111
+ }
112
+ ```
113
+
114
+ Then just run:
115
+
116
+ ```bash
117
+ npm run tunnel
118
+ ```
119
+
120
+ ### Framework-specific examples
121
+
122
+ **Vite / SvelteKit:**
123
+
124
+ ```json
125
+ {
126
+ "scripts": {
127
+ "dev": "vite",
128
+ "tunnel": "mekong-cli --with \"vite\" --port 5173"
129
+ }
130
+ }
131
+ ```
132
+
133
+ **Nuxt:**
134
+
135
+ ```json
136
+ {
137
+ "scripts": {
138
+ "dev": "nuxt dev",
139
+ "tunnel": "mekong-cli --with \"nuxt dev\" --port 3000"
140
+ }
141
+ }
142
+ ```
143
+
144
+ **Angular:**
145
+
146
+ ```json
147
+ {
148
+ "scripts": {
149
+ "start": "ng serve",
150
+ "tunnel": "mekong-cli --with \"ng serve\" --port 4200"
151
+ }
152
+ }
153
+ ```
154
+
155
+ **Astro:**
156
+
157
+ ```json
158
+ {
159
+ "scripts": {
160
+ "dev": "astro dev",
161
+ "tunnel": "mekong-cli --with \"astro dev\" --port 4321"
162
+ }
163
+ }
164
+ ```
165
+
166
+ ---
167
+
168
+ ## How it works
169
+
170
+ 1. Spawns your dev server (`--with`) with its stdout/stderr streamed to your terminal, prefixed with `[server]`.
171
+ 2. Polls the local port every 500 ms until the server is accepting connections (up to 30 s).
172
+ 3. Starts `mekong <port>` and streams its output prefixed with `[tunnel]`.
173
+ 4. When the public URL appears in mekong's output, prints a banner:
174
+
175
+ ```
176
+ ╔══════════════════════════════════════════╗
177
+ ║ Public URL: https://happy-tiger-a1b2c3d4.mekongtunnel.dev ║
178
+ ╚══════════════════════════════════════════╝
179
+ ```
180
+
181
+ 5. On `Ctrl+C` (or `SIGTERM`), kills the tunnel first, then the server, and exits cleanly.
182
+
183
+ ---
184
+
185
+ ## License
186
+
187
+ MIT — Author: Ing Muyleang
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { spawn } = require('child_process');
5
+ const path = require('path');
6
+
7
+ const { findMekong } = require('../lib/find-mekong');
8
+ const { detectPort } = require('../lib/detect-port');
9
+ const { runWithServer } = require('../lib/runner');
10
+
11
+ const BOLD = '\x1b[1m';
12
+ const DIM = '\x1b[2m';
13
+ const CYAN = '\x1b[36m';
14
+ const GREEN = '\x1b[32m';
15
+ const YELLOW = '\x1b[33m';
16
+ const RED = '\x1b[31m';
17
+ const RESET = '\x1b[0m';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Help text
21
+ // ---------------------------------------------------------------------------
22
+ const HELP = `
23
+ ${BOLD}mekong-cli${RESET} — Run your dev server + Mekong tunnel in one command
24
+
25
+ ${BOLD}USAGE${RESET}
26
+ mekong-cli <port> Tunnel an already-running server
27
+ mekong-cli --port <n> Tunnel only (auto-detect port)
28
+ mekong-cli --with "<cmd>" --port <n> Start server + tunnel together
29
+
30
+ ${BOLD}OPTIONS${RESET}
31
+ --with <cmd> Shell command to start the dev server
32
+ --port <n> Local port (auto-detected from package.json if omitted)
33
+ --expire <val> Expiry duration passed to mekong (e.g. 2h, 30m)
34
+ --daemon Run mekong in the background (-d flag)
35
+ --no-qr Suppress QR code output
36
+ --mekong <path> Custom path to the mekong binary
37
+ --help Show this help message
38
+
39
+ ${BOLD}EXAMPLES${RESET}
40
+ mekong-cli 3000
41
+ mekong-cli --with "next dev" --port 3000
42
+ mekong-cli --with "vite" --port 5173
43
+ mekong-cli --with "nuxt dev" --port 3000
44
+ mekong-cli --with "ng serve" --port 4200
45
+ mekong-cli --with "astro dev" --port 4321
46
+ mekong-cli --with "next dev" --port 3000 --expire 2h
47
+ mekong-cli --with "next dev" --port 3000 --daemon
48
+
49
+ ${BOLD}INSTALL MEKONG BINARY${RESET}
50
+ Download from: https://github.com/MuyleangIng/MekongTunnel/releases/tag/v1.4.9
51
+
52
+ Linux/macOS (quick install):
53
+ curl -sL https://github.com/MuyleangIng/MekongTunnel/releases/download/v1.4.9/mekong-linux-amd64 \\
54
+ -o /usr/local/bin/mekong && chmod +x /usr/local/bin/mekong
55
+
56
+ Windows: download mekong-windows-amd64.exe and place in PATH
57
+ `;
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Argument parser
61
+ // ---------------------------------------------------------------------------
62
+ function parseArgs(argv) {
63
+ const args = argv.slice(2); // strip node + script
64
+ const opts = {
65
+ port: null,
66
+ with: null,
67
+ expire: null,
68
+ daemon: false,
69
+ noQr: false,
70
+ mekongPath: null,
71
+ help: false,
72
+ positional: [],
73
+ };
74
+
75
+ let i = 0;
76
+ while (i < args.length) {
77
+ const a = args[i];
78
+ switch (a) {
79
+ case '--help':
80
+ case '-h':
81
+ opts.help = true;
82
+ break;
83
+ case '--port':
84
+ case '-p':
85
+ opts.port = parseInt(args[++i], 10);
86
+ break;
87
+ case '--with':
88
+ opts.with = args[++i];
89
+ break;
90
+ case '--expire':
91
+ opts.expire = args[++i];
92
+ break;
93
+ case '--daemon':
94
+ case '-d':
95
+ opts.daemon = true;
96
+ break;
97
+ case '--no-qr':
98
+ opts.noQr = true;
99
+ break;
100
+ case '--mekong':
101
+ opts.mekongPath = args[++i];
102
+ break;
103
+ default:
104
+ if (!a.startsWith('-')) {
105
+ opts.positional.push(a);
106
+ } else {
107
+ // unknown flag — warn but continue
108
+ process.stderr.write(`${YELLOW}mekong-cli: unknown option: ${a}${RESET}\n`);
109
+ }
110
+ }
111
+ i++;
112
+ }
113
+
114
+ return opts;
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Main
119
+ // ---------------------------------------------------------------------------
120
+ async function main() {
121
+ const opts = parseArgs(process.argv);
122
+
123
+ if (opts.help) {
124
+ process.stdout.write(HELP);
125
+ process.exit(0);
126
+ }
127
+
128
+ // Resolve mekong binary
129
+ const mekongBin = opts.mekongPath || findMekong();
130
+ if (!mekongBin) {
131
+ process.stderr.write(
132
+ `${RED}${BOLD}mekong-cli: mekong binary not found.${RESET}\n\n` +
133
+ `Install it from:\n` +
134
+ ` https://github.com/MuyleangIng/MekongTunnel/releases/tag/v1.4.9\n\n` +
135
+ `Quick install (Linux/macOS):\n` +
136
+ ` curl -sL https://github.com/MuyleangIng/MekongTunnel/releases/download/v1.4.9/mekong-linux-amd64 \\\n` +
137
+ ` -o /usr/local/bin/mekong && chmod +x /usr/local/bin/mekong\n\n` +
138
+ `Or specify a custom path with: ${CYAN}--mekong /path/to/mekong${RESET}\n`
139
+ );
140
+ process.exit(1);
141
+ }
142
+
143
+ // Resolve port
144
+ let port = opts.port;
145
+ if (!port && opts.positional.length > 0) {
146
+ port = parseInt(opts.positional[0], 10);
147
+ }
148
+ if (!port) {
149
+ port = detectPort();
150
+ }
151
+ if (!port || isNaN(port)) {
152
+ process.stderr.write(
153
+ `${RED}mekong-cli: could not determine port.${RESET}\n` +
154
+ `Specify it with ${CYAN}--port <n>${RESET} or pass it as the first argument.\n`
155
+ );
156
+ process.exit(1);
157
+ }
158
+
159
+ const runnerOpts = {
160
+ mekongBin,
161
+ expire: opts.expire,
162
+ daemon: opts.daemon,
163
+ noQr: opts.noQr,
164
+ };
165
+
166
+ // --with: start server + tunnel
167
+ if (opts.with) {
168
+ await runWithServer(opts.with, port, runnerOpts);
169
+ return;
170
+ }
171
+
172
+ // Tunnel-only mode: just proxy straight to mekong
173
+ const mekongArgs = [String(port)];
174
+ if (opts.expire) mekongArgs.push('--expire', opts.expire);
175
+ if (opts.daemon) mekongArgs.push('-d');
176
+ if (opts.noQr) mekongArgs.push('--no-qr');
177
+
178
+ process.stdout.write(
179
+ `${CYAN}${BOLD}mekong-cli${RESET} Tunneling ${CYAN}localhost:${port}${RESET} via mekong...\n`
180
+ );
181
+
182
+ const tunnel = spawn(mekongBin, mekongArgs, { stdio: 'inherit' });
183
+
184
+ tunnel.on('exit', (code) => {
185
+ process.exit(code == null ? 0 : code);
186
+ });
187
+
188
+ process.on('SIGINT', () => { tunnel.kill('SIGTERM'); });
189
+ process.on('SIGTERM', () => { tunnel.kill('SIGTERM'); });
190
+ }
191
+
192
+ main().catch((err) => {
193
+ process.stderr.write(`${RED}mekong-cli: ${err.message}${RESET}\n`);
194
+ process.exit(1);
195
+ });
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Auto-detect the local dev server port from package.json in cwd.
8
+ * Returns a number or null if not detected.
9
+ */
10
+ function detectPort() {
11
+ const pkgPath = path.join(process.cwd(), 'package.json');
12
+
13
+ let pkg;
14
+ try {
15
+ const raw = fs.readFileSync(pkgPath, 'utf8');
16
+ pkg = JSON.parse(raw);
17
+ } catch (_) {
18
+ return null;
19
+ }
20
+
21
+ // Check scripts for explicit --port N
22
+ const scripts = pkg.scripts || {};
23
+ for (const key of ['dev', 'start']) {
24
+ const script = scripts[key];
25
+ if (typeof script === 'string') {
26
+ const m = script.match(/--port[=\s]+(\d+)/);
27
+ if (m) return parseInt(m[1], 10);
28
+ // also handle -p N
29
+ const m2 = script.match(/-p[=\s]+(\d+)/);
30
+ if (m2) return parseInt(m2[1], 10);
31
+ }
32
+ }
33
+
34
+ // Check dependencies / devDependencies for known frameworks
35
+ const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
36
+
37
+ const frameworkPort = [
38
+ [['next', 'next.js'], 3000],
39
+ [['nuxt', 'nuxt3', 'nuxt-edge'], 3000],
40
+ [['vite'], 5173],
41
+ [['react-scripts'], 3000],
42
+ [['@angular/core'], 4200],
43
+ [['svelte', '@sveltejs/kit', '@sveltejs/vite-plugin-svelte'], 5173],
44
+ [['gatsby'], 8000],
45
+ [['remix', '@remix-run/node', '@remix-run/react', '@remix-run/serve'], 3000],
46
+ [['astro'], 4321],
47
+ [['express', 'fastify', 'koa', '@hapi/hapi', 'hapi'], 3000],
48
+ ];
49
+
50
+ for (const [keys, port] of frameworkPort) {
51
+ for (const key of keys) {
52
+ if (key in deps) return port;
53
+ }
54
+ }
55
+
56
+ return null;
57
+ }
58
+
59
+ module.exports = { detectPort };
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+
8
+ /**
9
+ * Locate the mekong binary.
10
+ * Returns an absolute path string or null if not found.
11
+ */
12
+ function findMekong() {
13
+ const isWindows = process.platform === 'win32';
14
+
15
+ // 1. Try `which mekong` (or `where mekong` on Windows)
16
+ try {
17
+ const cmd = isWindows ? 'where mekong' : 'which mekong';
18
+ const result = execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] })
19
+ .toString()
20
+ .trim()
21
+ .split('\n')[0]
22
+ .trim();
23
+ if (result && fs.existsSync(result)) return result;
24
+ } catch (_) {
25
+ // not on PATH
26
+ }
27
+
28
+ // 2. Common Unix paths
29
+ if (!isWindows) {
30
+ const unixPaths = [
31
+ path.join(os.homedir(), '.local', 'bin', 'mekong'),
32
+ '/usr/local/bin/mekong',
33
+ '/usr/bin/mekong',
34
+ ];
35
+ for (const p of unixPaths) {
36
+ if (fs.existsSync(p)) return p;
37
+ }
38
+ }
39
+
40
+ // 3. Windows paths
41
+ if (isWindows) {
42
+ const winPaths = [
43
+ path.join(os.homedir(), '.local', 'bin', 'mekong.exe'),
44
+ 'C:\\Program Files\\mekong\\mekong.exe',
45
+ path.join(process.env.USERPROFILE || os.homedir(), 'AppData', 'Local', 'mekong.exe'),
46
+ ];
47
+ for (const p of winPaths) {
48
+ if (fs.existsSync(p)) return p;
49
+ }
50
+
51
+ // Also try without .exe in same locations
52
+ const winPathsNoExt = [
53
+ path.join(os.homedir(), '.local', 'bin', 'mekong'),
54
+ path.join(process.env.USERPROFILE || os.homedir(), 'AppData', 'Local', 'mekong'),
55
+ ];
56
+ for (const p of winPathsNoExt) {
57
+ if (fs.existsSync(p)) return p;
58
+ }
59
+ }
60
+
61
+ return null;
62
+ }
63
+
64
+ module.exports = { findMekong };
package/lib/runner.js ADDED
@@ -0,0 +1,165 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('child_process');
4
+ const { waitForPort } = require('./wait-for-port');
5
+
6
+ const BOLD = '\x1b[1m';
7
+ const DIM = '\x1b[2m';
8
+ const CYAN = '\x1b[36m';
9
+ const GREEN = '\x1b[32m';
10
+ const YELLOW = '\x1b[33m';
11
+ const RED = '\x1b[31m';
12
+ const RESET = '\x1b[0m';
13
+
14
+ /**
15
+ * Prefix and stream lines from a readable stream.
16
+ * @param {import('stream').Readable} readable
17
+ * @param {string} prefix
18
+ * @param {(line: string) => void} [onLine]
19
+ */
20
+ function streamLines(readable, prefix, onLine) {
21
+ let buf = '';
22
+ readable.on('data', (chunk) => {
23
+ buf += chunk.toString();
24
+ let idx;
25
+ while ((idx = buf.indexOf('\n')) !== -1) {
26
+ const line = buf.slice(0, idx);
27
+ buf = buf.slice(idx + 1);
28
+ process.stdout.write(`${DIM}${prefix}${RESET} ${line}\n`);
29
+ if (onLine) onLine(line);
30
+ }
31
+ });
32
+ readable.on('end', () => {
33
+ if (buf.length > 0) {
34
+ process.stdout.write(`${DIM}${prefix}${RESET} ${buf}\n`);
35
+ if (onLine) onLine(buf);
36
+ buf = '';
37
+ }
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Print a banner with the public tunnel URL.
43
+ * @param {string} url
44
+ */
45
+ function printBanner(url) {
46
+ const label = ' Public URL: ';
47
+ const inner = label + url + ' ';
48
+ const width = Math.max(inner.length + 2, 42);
49
+ const top = '╔' + '═'.repeat(width) + '╗';
50
+ const bottom = '╚' + '═'.repeat(width) + '╝';
51
+ const pad = width - inner.length;
52
+ const middle = '║' + inner + ' '.repeat(pad) + '║';
53
+
54
+ process.stdout.write('\n');
55
+ process.stdout.write(`${GREEN}${BOLD}${top}${RESET}\n`);
56
+ process.stdout.write(`${GREEN}${BOLD}${middle}${RESET}\n`);
57
+ process.stdout.write(`${GREEN}${BOLD}${bottom}${RESET}\n`);
58
+ process.stdout.write('\n');
59
+ }
60
+
61
+ /**
62
+ * Run a dev server command alongside a mekong tunnel.
63
+ *
64
+ * @param {string} serverCmd - shell command to start the dev server
65
+ * @param {number} port - local port the server will listen on
66
+ * @param {object} opts
67
+ * @param {string} opts.mekongBin - path to mekong binary
68
+ * @param {string} [opts.expire] - --expire value passed to mekong
69
+ * @param {boolean} [opts.daemon] - pass -d to mekong
70
+ * @param {boolean} [opts.noQr] - pass --no-qr to mekong
71
+ */
72
+ async function runWithServer(serverCmd, port, opts) {
73
+ const { mekongBin, expire, daemon, noQr } = opts;
74
+
75
+ process.stdout.write(
76
+ `${CYAN}${BOLD}mekong-cli${RESET} Starting dev server: ${YELLOW}${serverCmd}${RESET}\n`
77
+ );
78
+
79
+ // 1. Spawn the dev server
80
+ const server = spawn(serverCmd, [], {
81
+ shell: true,
82
+ stdio: ['ignore', 'pipe', 'pipe'],
83
+ });
84
+
85
+ streamLines(server.stdout, '[server]');
86
+ streamLines(server.stderr, '[server]');
87
+
88
+ let tunnelProc = null;
89
+
90
+ function cleanup(exitCode) {
91
+ if (tunnelProc && !tunnelProc.killed) {
92
+ tunnelProc.kill('SIGTERM');
93
+ }
94
+ if (server && !server.killed) {
95
+ server.kill('SIGTERM');
96
+ }
97
+ process.exit(exitCode == null ? 0 : exitCode);
98
+ }
99
+
100
+ process.on('SIGINT', () => cleanup(0));
101
+ process.on('SIGTERM', () => cleanup(0));
102
+
103
+ server.on('exit', (code) => {
104
+ if (code !== 0 && code !== null) {
105
+ process.stderr.write(
106
+ `${RED}[server] exited with code ${code}${RESET}\n`
107
+ );
108
+ cleanup(code);
109
+ }
110
+ });
111
+
112
+ // 2. Wait for the port to be ready
113
+ process.stdout.write(
114
+ `${DIM}mekong-cli${RESET} Waiting for port ${CYAN}${port}${RESET} to be ready...\n`
115
+ );
116
+
117
+ try {
118
+ await waitForPort(port);
119
+ } catch (err) {
120
+ process.stderr.write(`${RED}mekong-cli: ${err.message}${RESET}\n`);
121
+ cleanup(1);
122
+ return;
123
+ }
124
+
125
+ process.stdout.write(
126
+ `${GREEN}mekong-cli${RESET} Port ${CYAN}${port}${RESET} is ready. Starting tunnel...\n`
127
+ );
128
+
129
+ // 3. Build mekong args
130
+ const mekongArgs = [String(port)];
131
+ if (expire) mekongArgs.push('--expire', expire);
132
+ if (daemon) mekongArgs.push('-d');
133
+ if (noQr) mekongArgs.push('--no-qr');
134
+
135
+ // 4. Spawn mekong
136
+ tunnelProc = spawn(mekongBin, mekongArgs, {
137
+ stdio: ['ignore', 'pipe', 'pipe'],
138
+ });
139
+
140
+ const urlRegex = /https?:\/\/[^\s]+/;
141
+ let bannerShown = false;
142
+
143
+ function handleTunnelLine(line) {
144
+ if (!bannerShown) {
145
+ const m = line.match(urlRegex);
146
+ if (m) {
147
+ bannerShown = true;
148
+ printBanner(m[0]);
149
+ }
150
+ }
151
+ }
152
+
153
+ streamLines(tunnelProc.stdout, '[tunnel]', handleTunnelLine);
154
+ streamLines(tunnelProc.stderr, '[tunnel]', handleTunnelLine);
155
+
156
+ tunnelProc.on('exit', (code) => {
157
+ if (code !== 0 && code !== null) {
158
+ process.stderr.write(
159
+ `${YELLOW}[tunnel] exited with code ${code}${RESET}\n`
160
+ );
161
+ }
162
+ });
163
+ }
164
+
165
+ module.exports = { runWithServer };
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ const net = require('net');
4
+
5
+ /**
6
+ * Poll a TCP port every 500ms until it accepts a connection.
7
+ * Rejects after 30 seconds.
8
+ *
9
+ * @param {number} port
10
+ * @returns {Promise<void>}
11
+ */
12
+ function waitForPort(port) {
13
+ const INTERVAL_MS = 500;
14
+ const TIMEOUT_MS = 30_000;
15
+
16
+ return new Promise((resolve, reject) => {
17
+ const started = Date.now();
18
+
19
+ function attempt() {
20
+ const elapsed = Date.now() - started;
21
+ if (elapsed >= TIMEOUT_MS) {
22
+ return reject(
23
+ new Error(`Timed out waiting for port ${port} after ${TIMEOUT_MS / 1000}s`)
24
+ );
25
+ }
26
+
27
+ const sock = new net.Socket();
28
+ let settled = false;
29
+
30
+ function cleanup(err) {
31
+ if (settled) return;
32
+ settled = true;
33
+ sock.destroy();
34
+ if (err) {
35
+ setTimeout(attempt, INTERVAL_MS);
36
+ } else {
37
+ resolve();
38
+ }
39
+ }
40
+
41
+ sock.setTimeout(INTERVAL_MS);
42
+ sock.once('connect', () => cleanup(null));
43
+ sock.once('error', (err) => cleanup(err));
44
+ sock.once('timeout', () => cleanup(new Error('timeout')));
45
+ sock.connect(port, '127.0.0.1');
46
+ }
47
+
48
+ attempt();
49
+ });
50
+ }
51
+
52
+ module.exports = { waitForPort };
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "mekong-cli",
3
+ "version": "1.0.0",
4
+ "description": "Run your dev server + Mekong tunnel in one command",
5
+ "bin": {
6
+ "mekong-cli": "bin/mekong-cli.js"
7
+ },
8
+ "license": "MIT",
9
+ "author": "Ing Muyleang",
10
+ "keywords": [
11
+ "tunnel",
12
+ "mekong",
13
+ "ngrok",
14
+ "devserver",
15
+ "nextjs",
16
+ "vite",
17
+ "nuxt"
18
+ ],
19
+ "dependencies": {}
20
+ }