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 +187 -0
- package/bin/mekong-cli.js +195 -0
- package/lib/detect-port.js +59 -0
- package/lib/find-mekong.js +64 -0
- package/lib/runner.js +165 -0
- package/lib/wait-for-port.js +52 -0
- package/package.json +20 -0
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
|
+
}
|