mekong-cli 1.0.0 → 1.2.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 +155 -85
- package/bin/mekong-cli.js +52 -14
- package/lib/init.js +410 -0
- package/lib/wait-for-port.js +24 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# mekong-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Expose your Node.js dev server to the internet in one command.
|
|
4
|
+
> Works with Next.js, Vite, Nuxt, Angular, Astro, Svelte, Express, and more.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
[](https://www.npmjs.com/package/mekong-cli)
|
|
7
|
+
[](LICENSE)
|
|
6
8
|
|
|
7
9
|
---
|
|
8
10
|
|
|
@@ -12,7 +14,7 @@ Think of it as the glue between `next dev` / `vite` / `nuxt dev` and `mekong <po
|
|
|
12
14
|
npm install -g mekong-cli
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
Or use
|
|
17
|
+
Or use without installing:
|
|
16
18
|
|
|
17
19
|
```bash
|
|
18
20
|
npx mekong-cli 3000
|
|
@@ -23,90 +25,133 @@ npx mekong-cli 3000
|
|
|
23
25
|
## Requirements
|
|
24
26
|
|
|
25
27
|
- **Node.js 14+**
|
|
26
|
-
- **mekong binary**
|
|
28
|
+
- **mekong binary** — install separately (see below)
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
### Install the mekong binary
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
```bash
|
|
33
|
+
# macOS / Linux (auto-detect arch)
|
|
34
|
+
curl -fsSL https://github.com/MuyleangIng/MekongTunnel/releases/latest/download/mekong-$(uname -s | tr A-Z a-z)-$(uname -m) \
|
|
35
|
+
-o ~/.local/bin/mekong && chmod +x ~/.local/bin/mekong
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
# macOS Intel
|
|
38
|
+
curl -fsSL https://github.com/MuyleangIng/MekongTunnel/releases/latest/download/mekong-darwin-amd64 \
|
|
39
|
+
-o /usr/local/bin/mekong && chmod +x /usr/local/bin/mekong
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
# macOS Apple Silicon
|
|
42
|
+
curl -fsSL https://github.com/MuyleangIng/MekongTunnel/releases/latest/download/mekong-darwin-arm64 \
|
|
43
|
+
-o /usr/local/bin/mekong && chmod +x /usr/local/bin/mekong
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
curl -
|
|
45
|
+
# Linux amd64
|
|
46
|
+
curl -fsSL https://github.com/MuyleangIng/MekongTunnel/releases/latest/download/mekong-linux-amd64 \
|
|
38
47
|
-o /usr/local/bin/mekong && chmod +x /usr/local/bin/mekong
|
|
48
|
+
|
|
49
|
+
# Windows — download from:
|
|
50
|
+
# https://github.com/MuyleangIng/MekongTunnel/releases/latest
|
|
39
51
|
```
|
|
40
52
|
|
|
41
|
-
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Commands
|
|
56
|
+
|
|
57
|
+
### 1. `mekong-cli <port>` — Tunnel an already-running server
|
|
58
|
+
|
|
59
|
+
Your server is already running. Just pass the port:
|
|
42
60
|
|
|
43
61
|
```bash
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
mekong-cli 3000
|
|
63
|
+
mekong-cli 5173
|
|
64
|
+
mekong-cli 8080
|
|
46
65
|
```
|
|
47
66
|
|
|
48
|
-
|
|
67
|
+
> If nothing is listening on that port, mekong-cli will tell you clearly and exit — it will **not** start the tunnel.
|
|
49
68
|
|
|
50
69
|
---
|
|
51
70
|
|
|
52
|
-
|
|
71
|
+
### 2. `mekong-cli` — Auto-detect port from package.json
|
|
53
72
|
|
|
54
|
-
|
|
73
|
+
No port needed if your `package.json` uses a known framework:
|
|
55
74
|
|
|
56
75
|
```bash
|
|
57
|
-
mekong-cli
|
|
76
|
+
mekong-cli
|
|
58
77
|
```
|
|
59
78
|
|
|
60
|
-
|
|
79
|
+
mekong-cli reads your `package.json` and detects the port automatically:
|
|
80
|
+
|
|
81
|
+
| Framework | Detected port |
|
|
82
|
+
|---|---|
|
|
83
|
+
| Next.js | 3000 |
|
|
84
|
+
| Nuxt | 3000 |
|
|
85
|
+
| React (CRA) | 3000 |
|
|
86
|
+
| Remix | 3000 |
|
|
87
|
+
| Express / Fastify / Koa | 3000 |
|
|
88
|
+
| Vite | 5173 |
|
|
89
|
+
| SvelteKit | 5173 |
|
|
90
|
+
| Angular | 4200 |
|
|
91
|
+
| Astro | 4321 |
|
|
92
|
+
| Gatsby | 8000 |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### 3. `mekong-cli --with "<cmd>" --port <n>` — Start server + tunnel together
|
|
97
|
+
|
|
98
|
+
Start your dev server AND the tunnel at the same time — no separate terminals:
|
|
61
99
|
|
|
62
100
|
```bash
|
|
101
|
+
# Next.js
|
|
63
102
|
mekong-cli --with "next dev" --port 3000
|
|
103
|
+
|
|
104
|
+
# Vite (React, Vue, Svelte, etc.)
|
|
64
105
|
mekong-cli --with "vite" --port 5173
|
|
106
|
+
|
|
107
|
+
# Nuxt
|
|
65
108
|
mekong-cli --with "nuxt dev" --port 3000
|
|
109
|
+
|
|
110
|
+
# Angular
|
|
66
111
|
mekong-cli --with "ng serve" --port 4200
|
|
112
|
+
|
|
113
|
+
# Astro
|
|
67
114
|
mekong-cli --with "astro dev" --port 4321
|
|
68
|
-
```
|
|
69
115
|
|
|
70
|
-
|
|
116
|
+
# SvelteKit
|
|
117
|
+
mekong-cli --with "vite dev" --port 5173
|
|
71
118
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
mekong-cli --with "next dev" --port 3000 --expire 2h
|
|
119
|
+
# Express / Node server
|
|
120
|
+
mekong-cli --with "node server.js" --port 3000
|
|
75
121
|
|
|
76
|
-
#
|
|
77
|
-
mekong-cli --with "
|
|
122
|
+
# Any custom command
|
|
123
|
+
mekong-cli --with "npm run dev" --port 3000
|
|
124
|
+
```
|
|
78
125
|
|
|
79
|
-
|
|
80
|
-
|
|
126
|
+
mekong-cli will:
|
|
127
|
+
1. Start your dev server
|
|
128
|
+
2. Wait for port to accept connections (up to 30s)
|
|
129
|
+
3. Start the mekong tunnel
|
|
130
|
+
4. Print the public URL banner
|
|
131
|
+
5. On Ctrl+C — stop both cleanly
|
|
81
132
|
|
|
82
|
-
|
|
83
|
-
mekong-cli --with "next dev" --port 3000 --mekong ~/bin/mekong
|
|
84
|
-
```
|
|
133
|
+
---
|
|
85
134
|
|
|
86
|
-
###
|
|
135
|
+
### 4. `mekong-cli init` — Auto-setup in your project
|
|
87
136
|
|
|
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
|
-
```
|
|
137
|
+
Run this once in your project root:
|
|
97
138
|
|
|
98
|
-
|
|
139
|
+
```bash
|
|
140
|
+
mekong-cli init
|
|
141
|
+
```
|
|
99
142
|
|
|
100
|
-
|
|
143
|
+
mekong-cli init will:
|
|
144
|
+
- Detect your framework (Next.js, Vite, Nuxt, etc.)
|
|
145
|
+
- Ask which port your server runs on
|
|
146
|
+
- Inject a `dev:tunnel` script into your `package.json`
|
|
101
147
|
|
|
102
|
-
|
|
148
|
+
After running init, your `package.json` will have:
|
|
103
149
|
|
|
104
150
|
```json
|
|
105
151
|
{
|
|
106
152
|
"scripts": {
|
|
107
153
|
"dev": "next dev",
|
|
108
|
-
"tunnel": "mekong-cli --with \"next dev\" --port 3000"
|
|
109
|
-
"tunnel:share": "mekong-cli --with \"next dev\" --port 3000 --expire 2h"
|
|
154
|
+
"dev:tunnel": "mekong-cli --with \"next dev\" --port 3000"
|
|
110
155
|
}
|
|
111
156
|
}
|
|
112
157
|
```
|
|
@@ -114,74 +159,99 @@ You can wire `mekong-cli` directly into your project's `package.json` so the who
|
|
|
114
159
|
Then just run:
|
|
115
160
|
|
|
116
161
|
```bash
|
|
117
|
-
npm run tunnel
|
|
162
|
+
npm run dev:tunnel
|
|
118
163
|
```
|
|
119
164
|
|
|
120
|
-
|
|
165
|
+
---
|
|
121
166
|
|
|
122
|
-
|
|
167
|
+
## All options
|
|
123
168
|
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
169
|
+
```
|
|
170
|
+
mekong-cli [options] [port]
|
|
171
|
+
|
|
172
|
+
<port> Port of an already-running server to tunnel
|
|
173
|
+
--with <cmd> Start this dev server command first, then tunnel
|
|
174
|
+
--port <n> Explicit port (used with --with, or overrides auto-detect)
|
|
175
|
+
--expire <val> Tunnel expiry: 30m, 2h, 1d, 1w
|
|
176
|
+
--daemon Run mekong tunnel in background
|
|
177
|
+
--no-qr Suppress QR code in terminal
|
|
178
|
+
--mekong <path> Custom path to mekong binary
|
|
179
|
+
--help, -h Show help
|
|
180
|
+
|
|
181
|
+
init Auto-setup dev:tunnel script in your project
|
|
131
182
|
```
|
|
132
183
|
|
|
133
|
-
|
|
184
|
+
---
|
|
134
185
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
186
|
+
## Framework quick reference
|
|
187
|
+
|
|
188
|
+
| Framework | Command |
|
|
189
|
+
|---|---|
|
|
190
|
+
| **Next.js** | `mekong-cli --with "next dev" --port 3000` |
|
|
191
|
+
| **Vite** | `mekong-cli --with "vite" --port 5173` |
|
|
192
|
+
| **Nuxt** | `mekong-cli --with "nuxt dev" --port 3000` |
|
|
193
|
+
| **Angular** | `mekong-cli --with "ng serve" --port 4200` |
|
|
194
|
+
| **Astro** | `mekong-cli --with "astro dev" --port 4321` |
|
|
195
|
+
| **SvelteKit** | `mekong-cli --with "vite dev" --port 5173` |
|
|
196
|
+
| **Remix** | `mekong-cli --with "remix dev" --port 3000` |
|
|
197
|
+
| **Gatsby** | `mekong-cli --with "gatsby develop" --port 8000` |
|
|
198
|
+
| **Express** | `mekong-cli --with "node server.js" --port 3000` |
|
|
199
|
+
| **Fastify** | `mekong-cli --with "node server.js" --port 3000` |
|
|
143
200
|
|
|
144
|
-
|
|
201
|
+
---
|
|
145
202
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
203
|
+
## With expiry and options
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# Expire tunnel after 2 hours
|
|
207
|
+
mekong-cli --with "next dev" --port 3000 --expire 2h
|
|
208
|
+
|
|
209
|
+
# Run tunnel in background (daemon mode)
|
|
210
|
+
mekong-cli --with "next dev" --port 3000 --daemon
|
|
211
|
+
|
|
212
|
+
# No QR code
|
|
213
|
+
mekong-cli --with "vite" --port 5173 --no-qr
|
|
214
|
+
|
|
215
|
+
# Custom mekong binary path
|
|
216
|
+
mekong-cli --with "next dev" --port 3000 --mekong ~/bin/mekong
|
|
153
217
|
```
|
|
154
218
|
|
|
155
|
-
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Add scripts to package.json manually
|
|
156
222
|
|
|
157
223
|
```json
|
|
158
224
|
{
|
|
159
225
|
"scripts": {
|
|
160
|
-
"dev": "
|
|
161
|
-
"tunnel": "mekong-cli --with \"
|
|
226
|
+
"dev": "next dev",
|
|
227
|
+
"dev:tunnel": "mekong-cli --with \"next dev\" --port 3000",
|
|
228
|
+
"dev:tunnel:share": "mekong-cli --with \"next dev\" --port 3000 --expire 2h"
|
|
162
229
|
}
|
|
163
230
|
}
|
|
164
231
|
```
|
|
165
232
|
|
|
233
|
+
Or just run `mekong-cli init` and it does this for you automatically.
|
|
234
|
+
|
|
166
235
|
---
|
|
167
236
|
|
|
168
237
|
## How it works
|
|
169
238
|
|
|
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
239
|
```
|
|
176
|
-
|
|
240
|
+
mekong-cli --with "next dev" --port 3000
|
|
241
|
+
|
|
242
|
+
[server] ready - started server on 0.0.0.0:3000
|
|
243
|
+
[server] ...
|
|
244
|
+
✔ Port 3000 is ready. Starting tunnel...
|
|
245
|
+
[tunnel] ✔ Tunnel is live!
|
|
246
|
+
[tunnel] URL https://happy-tiger-a1b2c3d4.mekongtunnel.dev
|
|
247
|
+
|
|
248
|
+
╔══════════════════════════════════════════════════╗
|
|
177
249
|
║ Public URL: https://happy-tiger-a1b2c3d4.mekongtunnel.dev ║
|
|
178
|
-
|
|
250
|
+
╚══════════════════════════════════════════════════╝
|
|
179
251
|
```
|
|
180
252
|
|
|
181
|
-
5. On `Ctrl+C` (or `SIGTERM`), kills the tunnel first, then the server, and exits cleanly.
|
|
182
|
-
|
|
183
253
|
---
|
|
184
254
|
|
|
185
255
|
## License
|
|
186
256
|
|
|
187
|
-
MIT
|
|
257
|
+
MIT © [Ing Muyleang](https://github.com/MuyleangIng) — KhmerStack
|
package/bin/mekong-cli.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
const { spawn } = require('child_process');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
|
-
const { findMekong }
|
|
8
|
-
const { detectPort }
|
|
7
|
+
const { findMekong } = require('../lib/find-mekong');
|
|
8
|
+
const { detectPort } = require('../lib/detect-port');
|
|
9
9
|
const { runWithServer } = require('../lib/runner');
|
|
10
|
+
const { checkPortOpen } = require('../lib/wait-for-port');
|
|
10
11
|
|
|
11
12
|
const BOLD = '\x1b[1m';
|
|
12
13
|
const DIM = '\x1b[2m';
|
|
@@ -23,37 +24,51 @@ const HELP = `
|
|
|
23
24
|
${BOLD}mekong-cli${RESET} — Run your dev server + Mekong tunnel in one command
|
|
24
25
|
|
|
25
26
|
${BOLD}USAGE${RESET}
|
|
26
|
-
mekong-cli <port> Tunnel an already-running server
|
|
27
|
-
mekong-cli
|
|
27
|
+
mekong-cli <port> Tunnel an already-running server on <port>
|
|
28
|
+
mekong-cli Auto-detect port from package.json + tunnel
|
|
28
29
|
mekong-cli --with "<cmd>" --port <n> Start server + tunnel together
|
|
30
|
+
mekong-cli init Auto-setup dev:tunnel script in your project
|
|
29
31
|
|
|
30
32
|
${BOLD}OPTIONS${RESET}
|
|
31
33
|
--with <cmd> Shell command to start the dev server
|
|
32
34
|
--port <n> Local port (auto-detected from package.json if omitted)
|
|
33
|
-
--expire <val> Expiry duration passed to mekong (e.g. 2h, 30m)
|
|
35
|
+
--expire <val> Expiry duration passed to mekong (e.g. 2h, 30m, 1d, 1w)
|
|
34
36
|
--daemon Run mekong in the background (-d flag)
|
|
35
37
|
--no-qr Suppress QR code output
|
|
36
38
|
--mekong <path> Custom path to the mekong binary
|
|
37
|
-
--help
|
|
39
|
+
--help, -h Show this help message
|
|
40
|
+
|
|
41
|
+
${BOLD}SUBCOMMANDS${RESET}
|
|
42
|
+
init Auto-detect your framework, inject dev:tunnel into package.json
|
|
38
43
|
|
|
39
44
|
${BOLD}EXAMPLES${RESET}
|
|
45
|
+
${DIM}# Tunnel a server already running on port 3000${RESET}
|
|
40
46
|
mekong-cli 3000
|
|
47
|
+
|
|
48
|
+
${DIM}# Auto-detect port from package.json and tunnel${RESET}
|
|
49
|
+
mekong-cli
|
|
50
|
+
|
|
51
|
+
${DIM}# Start your dev server AND open a tunnel together${RESET}
|
|
41
52
|
mekong-cli --with "next dev" --port 3000
|
|
42
53
|
mekong-cli --with "vite" --port 5173
|
|
43
54
|
mekong-cli --with "nuxt dev" --port 3000
|
|
44
55
|
mekong-cli --with "ng serve" --port 4200
|
|
45
56
|
mekong-cli --with "astro dev" --port 4321
|
|
57
|
+
|
|
58
|
+
${DIM}# With options${RESET}
|
|
46
59
|
mekong-cli --with "next dev" --port 3000 --expire 2h
|
|
47
|
-
mekong-cli --with "
|
|
60
|
+
mekong-cli --with "vite" --port 5173 --daemon
|
|
61
|
+
mekong-cli --with "next dev" --port 3000 --no-qr
|
|
48
62
|
|
|
49
|
-
${
|
|
50
|
-
|
|
63
|
+
${DIM}# Auto-setup script (run once in your project root)${RESET}
|
|
64
|
+
mekong-cli init
|
|
51
65
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
${BOLD}INSTALL MEKONG BINARY${RESET}
|
|
67
|
+
Linux/macOS (auto-detect arch):
|
|
68
|
+
curl -fsSL https://github.com/MuyleangIng/MekongTunnel/releases/latest/download/mekong-\\$(uname -s | tr A-Z a-z)-\\$(uname -m) \\
|
|
69
|
+
-o ~/.local/bin/mekong && chmod +x ~/.local/bin/mekong
|
|
55
70
|
|
|
56
|
-
Windows: download
|
|
71
|
+
Windows: download from https://github.com/MuyleangIng/MekongTunnel/releases/latest
|
|
57
72
|
`;
|
|
58
73
|
|
|
59
74
|
// ---------------------------------------------------------------------------
|
|
@@ -118,6 +133,16 @@ function parseArgs(argv) {
|
|
|
118
133
|
// Main
|
|
119
134
|
// ---------------------------------------------------------------------------
|
|
120
135
|
async function main() {
|
|
136
|
+
// init subcommand
|
|
137
|
+
if (process.argv[2] === 'init') {
|
|
138
|
+
const { runInit } = require('../lib/init.js')
|
|
139
|
+
runInit().catch(err => {
|
|
140
|
+
process.stderr.write(`${RED}mekong-cli init: ${err.message}${RESET}\n`)
|
|
141
|
+
process.exit(1)
|
|
142
|
+
})
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
121
146
|
const opts = parseArgs(process.argv);
|
|
122
147
|
|
|
123
148
|
if (opts.help) {
|
|
@@ -169,7 +194,20 @@ async function main() {
|
|
|
169
194
|
return;
|
|
170
195
|
}
|
|
171
196
|
|
|
172
|
-
// Tunnel-only mode:
|
|
197
|
+
// Tunnel-only mode: verify the port is actually listening before calling mekong
|
|
198
|
+
const isOpen = await checkPortOpen(port);
|
|
199
|
+
if (!isOpen) {
|
|
200
|
+
process.stderr.write(
|
|
201
|
+
`${RED}${BOLD}mekong-cli: nothing is listening on port ${port}.${RESET}\n\n` +
|
|
202
|
+
` Start your server first, then run:\n` +
|
|
203
|
+
` ${CYAN}mekong-cli ${port}${RESET}\n\n` +
|
|
204
|
+
` Or let mekong-cli start it for you:\n` +
|
|
205
|
+
` ${CYAN}mekong-cli --with "your-start-cmd" --port ${port}${RESET}\n\n` +
|
|
206
|
+
` Or run ${CYAN}mekong-cli init${RESET} to set it up automatically.\n`
|
|
207
|
+
);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
|
|
173
211
|
const mekongArgs = [String(port)];
|
|
174
212
|
if (opts.expire) mekongArgs.push('--expire', opts.expire);
|
|
175
213
|
if (opts.daemon) mekongArgs.push('-d');
|
package/lib/init.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const os = require('os')
|
|
6
|
+
const readline = require('readline')
|
|
7
|
+
|
|
8
|
+
const BOLD = '\x1b[1m'
|
|
9
|
+
const DIM = '\x1b[2m'
|
|
10
|
+
const CYAN = '\x1b[36m'
|
|
11
|
+
const GREEN = '\x1b[32m'
|
|
12
|
+
const YELLOW = '\x1b[33m'
|
|
13
|
+
const RED = '\x1b[31m'
|
|
14
|
+
const RESET = '\x1b[0m'
|
|
15
|
+
const CHECK = '\x1b[32m✓\x1b[0m'
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Framework tables
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const FRAMEWORKS = [
|
|
21
|
+
{ deps: ['next'], name: 'Next.js', cmd: 'next dev', port: 3000 },
|
|
22
|
+
{ deps: ['nuxt', 'nuxt3', 'nuxt-edge'], name: 'Nuxt', cmd: 'nuxt dev', port: 3000 },
|
|
23
|
+
{ deps: ['vite'], name: 'Vite', cmd: 'vite', port: 5173 },
|
|
24
|
+
{ deps: ['react-scripts'], name: 'CRA', cmd: 'react-scripts start', port: 3000 },
|
|
25
|
+
{ deps: ['@angular/core'], name: 'Angular', cmd: 'ng serve', port: 4200 },
|
|
26
|
+
{ deps: ['@sveltejs/kit'], name: 'SvelteKit', cmd: 'vite dev', port: 5173 },
|
|
27
|
+
{ deps: ['svelte'], name: 'Svelte', cmd: 'vite', port: 5173 },
|
|
28
|
+
{ deps: ['astro'], name: 'Astro', cmd: 'astro dev', port: 4321 },
|
|
29
|
+
{ deps: ['gatsby'], name: 'Gatsby', cmd: 'gatsby develop', port: 8000 },
|
|
30
|
+
{ deps: ['remix', '@remix-run/react'], name: 'Remix', cmd: 'remix dev', port: 3000 },
|
|
31
|
+
{ deps: ['@remix-run/dev'], name: 'Remix', cmd: 'remix dev', port: 3000 },
|
|
32
|
+
{ deps: ['express'], name: 'Express', cmd: 'node server.js', port: 3000 },
|
|
33
|
+
{ deps: ['fastify'], name: 'Fastify', cmd: 'node server.js', port: 3000 },
|
|
34
|
+
{ deps: ['hono', '@hono/node-server'], name: 'Hono', cmd: 'node server.js', port: 3000 },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const PY_FRAMEWORKS = [
|
|
38
|
+
{ pkg: 'fastapi', name: 'FastAPI', cmd: 'uvicorn main:app --reload', port: 8000 },
|
|
39
|
+
{ pkg: 'flask', name: 'Flask', cmd: 'flask run', port: 5000 },
|
|
40
|
+
{ pkg: 'django', name: 'Django', cmd: 'python manage.py runserver', port: 8000 },
|
|
41
|
+
{ pkg: 'starlette', name: 'Starlette', cmd: 'uvicorn main:app --reload', port: 8000 },
|
|
42
|
+
{ pkg: 'tornado', name: 'Tornado', cmd: 'python main.py', port: 8888 },
|
|
43
|
+
{ pkg: 'sanic', name: 'Sanic', cmd: 'sanic main.app', port: 8000 },
|
|
44
|
+
{ pkg: 'litestar', name: 'Litestar', cmd: 'uvicorn main:app --reload', port: 8000 },
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Input helpers
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
// Read all stdin lines upfront when stdin is not a TTY (piped/test mode),
|
|
52
|
+
// so readline auto-close on EOF does not swallow buffered answers.
|
|
53
|
+
function readAllStdinLines() {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
const lines = []
|
|
56
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false })
|
|
57
|
+
rl.on('line', (l) => lines.push(l))
|
|
58
|
+
rl.on('close', () => resolve(lines))
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Build an `ask(question)` function. In non-TTY (piped) mode, pre-reads all
|
|
63
|
+
// stdin lines into a queue so readline close-on-EOF doesn't eat answers.
|
|
64
|
+
// Returns { ask, close } where close() tears down the readline interface if any.
|
|
65
|
+
async function makeAsker() {
|
|
66
|
+
if (!process.stdin.isTTY) {
|
|
67
|
+
const lines = await readAllStdinLines()
|
|
68
|
+
let idx = 0
|
|
69
|
+
function ask(question) {
|
|
70
|
+
const answer = idx < lines.length ? lines[idx++] : ''
|
|
71
|
+
process.stdout.write(question + answer + '\n')
|
|
72
|
+
return Promise.resolve(answer)
|
|
73
|
+
}
|
|
74
|
+
return { ask, close: () => {} }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Interactive TTY
|
|
78
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
79
|
+
function ask(question) {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
rl.question(question, (answer) => resolve(answer))
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
return { ask, close: () => rl.close() }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Filesystem helpers
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
function fileExists(filePath) {
|
|
91
|
+
try {
|
|
92
|
+
fs.accessSync(filePath, fs.constants.F_OK)
|
|
93
|
+
return true
|
|
94
|
+
} catch {
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Node.js detection
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
function detectNode(cwd) {
|
|
103
|
+
const pkgPath = path.join(cwd, 'package.json')
|
|
104
|
+
let pkg
|
|
105
|
+
try {
|
|
106
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
107
|
+
} catch (err) {
|
|
108
|
+
throw new Error(`Failed to parse package.json: ${err.message}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const allDeps = Object.assign({}, pkg.dependencies || {}, pkg.devDependencies || {})
|
|
112
|
+
|
|
113
|
+
let detected = null
|
|
114
|
+
for (const fw of FRAMEWORKS) {
|
|
115
|
+
if (fw.deps.some((d) => allDeps[d] !== undefined)) {
|
|
116
|
+
detected = { name: fw.name, cmd: fw.cmd, port: fw.port }
|
|
117
|
+
break
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Try to extract port from scripts.dev if it has --port N
|
|
122
|
+
const devScript = (pkg.scripts && pkg.scripts.dev) || null
|
|
123
|
+
if (devScript) {
|
|
124
|
+
const portMatch = devScript.match(/--port[=\s]+(\d+)/)
|
|
125
|
+
if (portMatch) {
|
|
126
|
+
const extractedPort = parseInt(portMatch[1], 10)
|
|
127
|
+
if (detected) {
|
|
128
|
+
detected.port = extractedPort
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// If no framework detected but dev script exists, use it as the command
|
|
132
|
+
if (!detected) {
|
|
133
|
+
detected = { name: null, cmd: devScript, port: 3000 }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { detected, pkg, pkgPath }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Python detection
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
function detectPython(cwd) {
|
|
144
|
+
// Django via manage.py
|
|
145
|
+
if (fileExists(path.join(cwd, 'manage.py'))) {
|
|
146
|
+
return { name: 'Django', cmd: 'python manage.py runserver', port: 8000 }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const packages = new Set()
|
|
150
|
+
|
|
151
|
+
// requirements.txt
|
|
152
|
+
const reqPath = path.join(cwd, 'requirements.txt')
|
|
153
|
+
if (fileExists(reqPath)) {
|
|
154
|
+
const lines = fs.readFileSync(reqPath, 'utf8').split('\n')
|
|
155
|
+
for (const line of lines) {
|
|
156
|
+
const clean = line.trim().split(/[>=<![\s]/)[0].toLowerCase()
|
|
157
|
+
if (clean) packages.add(clean)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// pyproject.toml — scan [project] dependencies section
|
|
162
|
+
const pyprojectPath = path.join(cwd, 'pyproject.toml')
|
|
163
|
+
if (fileExists(pyprojectPath)) {
|
|
164
|
+
const content = fs.readFileSync(pyprojectPath, 'utf8')
|
|
165
|
+
const lines = content.split('\n')
|
|
166
|
+
let inDeps = false
|
|
167
|
+
for (const line of lines) {
|
|
168
|
+
const trimmed = line.trim()
|
|
169
|
+
if (trimmed === '[project]') { inDeps = false }
|
|
170
|
+
if (inDeps) {
|
|
171
|
+
if (trimmed.startsWith('[') && trimmed !== '[project.dependencies]') { inDeps = false; continue }
|
|
172
|
+
const clean = trimmed.replace(/^["']/, '').split(/[>=<![\s"']/)[0].toLowerCase()
|
|
173
|
+
if (clean && !clean.startsWith('#')) packages.add(clean)
|
|
174
|
+
}
|
|
175
|
+
if (trimmed === 'dependencies' || trimmed === '[project.dependencies]' ||
|
|
176
|
+
(trimmed.startsWith('dependencies') && trimmed.includes('='))) {
|
|
177
|
+
inDeps = true
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const fw of PY_FRAMEWORKS) {
|
|
183
|
+
if (packages.has(fw.pkg)) {
|
|
184
|
+
return { name: fw.name, cmd: fw.cmd, port: fw.port }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Inject Node.js (package.json scripts)
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
async function injectNode(pkgPath, pkg, cmd, port, ask) {
|
|
195
|
+
if (pkg.scripts && pkg.scripts['dev:tunnel']) {
|
|
196
|
+
const answer = await ask(`${YELLOW}dev:tunnel already exists. Overwrite? (Y/n):${RESET} `)
|
|
197
|
+
if (answer.trim().toLowerCase() === 'n') {
|
|
198
|
+
process.stdout.write(`Skipped. No changes made.\n`)
|
|
199
|
+
return false
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!pkg.scripts) pkg.scripts = {}
|
|
204
|
+
pkg.scripts['dev:tunnel'] = `mekong-cli --with "${cmd}" --port ${port}`
|
|
205
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8')
|
|
206
|
+
return true
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Inject Python (Makefile)
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
function injectPython(cwd, cmd, port) {
|
|
213
|
+
const makefilePath = path.join(cwd, 'Makefile')
|
|
214
|
+
const target = `dev-tunnel:\n\tmekong ${cmd} --port ${port}\n`
|
|
215
|
+
|
|
216
|
+
if (fileExists(makefilePath)) {
|
|
217
|
+
const existing = fs.readFileSync(makefilePath, 'utf8')
|
|
218
|
+
fs.writeFileSync(makefilePath, existing.trimEnd() + '\n\n' + target, 'utf8')
|
|
219
|
+
} else {
|
|
220
|
+
fs.writeFileSync(makefilePath, target, 'utf8')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
process.stdout.write(`Also run directly: ${CYAN}mekong ${cmd} --port ${port}${RESET}\n`)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Prompt for manual command + port
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
async function promptManual(ask) {
|
|
230
|
+
const cmd = await ask(`Enter dev server command: `)
|
|
231
|
+
const portStr = await ask(`Enter local port: `)
|
|
232
|
+
const port = parseInt(portStr.trim(), 10)
|
|
233
|
+
if (isNaN(port)) throw new Error(`Invalid port: ${portStr.trim()}`)
|
|
234
|
+
return { cmd: cmd.trim(), port }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Configure Node.js ecosystem
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
async function configureNode(cwd, ask) {
|
|
241
|
+
let { detected, pkg, pkgPath } = detectNode(cwd)
|
|
242
|
+
let cmd, port, frameworkName
|
|
243
|
+
|
|
244
|
+
if (detected) {
|
|
245
|
+
frameworkName = detected.name || 'custom'
|
|
246
|
+
cmd = detected.cmd
|
|
247
|
+
port = detected.port
|
|
248
|
+
|
|
249
|
+
const label = detected.name ? detected.name : 'project'
|
|
250
|
+
process.stdout.write(`\nDetected: ${BOLD}${label}${RESET} on port ${CYAN}${port}${RESET}\n`)
|
|
251
|
+
process.stdout.write(`Command: ${DIM}${cmd}${RESET}\n\n`)
|
|
252
|
+
process.stdout.write(`Will add to package.json:\n`)
|
|
253
|
+
process.stdout.write(` ${CYAN}"dev:tunnel": "mekong-cli --with \\"${cmd}\\" --port ${port}"${RESET}\n\n`)
|
|
254
|
+
|
|
255
|
+
const answer = await ask(`Confirm? (Y/n): `)
|
|
256
|
+
if (answer.trim().toLowerCase() === 'n') {
|
|
257
|
+
const manual = await promptManual(ask)
|
|
258
|
+
cmd = manual.cmd
|
|
259
|
+
port = manual.port
|
|
260
|
+
frameworkName = 'custom'
|
|
261
|
+
|
|
262
|
+
process.stdout.write(`\nWill add to package.json:\n`)
|
|
263
|
+
process.stdout.write(` ${CYAN}"dev:tunnel": "mekong-cli --with \\"${cmd}\\" --port ${port}"${RESET}\n\n`)
|
|
264
|
+
const confirm2 = await ask(`Confirm? (Y/n): `)
|
|
265
|
+
if (confirm2.trim().toLowerCase() === 'n') {
|
|
266
|
+
process.stdout.write(`Skipped. No changes made.\n`)
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
process.stdout.write(`\n${YELLOW}No known Node.js framework detected.${RESET}\n`)
|
|
272
|
+
const manual = await promptManual(ask)
|
|
273
|
+
cmd = manual.cmd
|
|
274
|
+
port = manual.port
|
|
275
|
+
frameworkName = 'custom'
|
|
276
|
+
|
|
277
|
+
process.stdout.write(`\nWill add to package.json:\n`)
|
|
278
|
+
process.stdout.write(` ${CYAN}"dev:tunnel": "mekong-cli --with \\"${cmd}\\" --port ${port}"${RESET}\n\n`)
|
|
279
|
+
const confirm2 = await ask(`Confirm? (Y/n): `)
|
|
280
|
+
if (confirm2.trim().toLowerCase() === 'n') {
|
|
281
|
+
process.stdout.write(`Skipped. No changes made.\n`)
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const wrote = await injectNode(pkgPath, pkg, cmd, port, ask)
|
|
287
|
+
if (!wrote) return
|
|
288
|
+
|
|
289
|
+
const label = detected && detected.name ? detected.name : frameworkName
|
|
290
|
+
process.stdout.write(`\n${CHECK} Done! mekong-cli is set up for ${BOLD}${label}${RESET}\n\n`)
|
|
291
|
+
process.stdout.write(`Run your tunnel:\n`)
|
|
292
|
+
process.stdout.write(` ${CYAN}npm run dev:tunnel${RESET}\n\n`)
|
|
293
|
+
process.stdout.write(`What it does:\n`)
|
|
294
|
+
process.stdout.write(` 1. Starts: ${DIM}${cmd}${RESET} (port ${port})\n`)
|
|
295
|
+
process.stdout.write(` 2. Waits for port ${port} to open\n`)
|
|
296
|
+
process.stdout.write(` 3. Opens a Mekong tunnel\n`)
|
|
297
|
+
process.stdout.write(` 4. Prints your public URL\n\n`)
|
|
298
|
+
process.stdout.write(`Make sure mekong binary is installed:\n`)
|
|
299
|
+
process.stdout.write(` ${CYAN}https://github.com/MuyleangIng/MekongTunnel/releases/latest${RESET}\n`)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
// Configure Python ecosystem
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
async function configurePython(cwd, ask) {
|
|
306
|
+
let detected = detectPython(cwd)
|
|
307
|
+
let cmd, port, frameworkName
|
|
308
|
+
|
|
309
|
+
if (detected) {
|
|
310
|
+
frameworkName = detected.name
|
|
311
|
+
cmd = detected.cmd
|
|
312
|
+
port = detected.port
|
|
313
|
+
|
|
314
|
+
process.stdout.write(`\nDetected: ${BOLD}${detected.name}${RESET} on port ${CYAN}${port}${RESET}\n`)
|
|
315
|
+
process.stdout.write(`Command: ${DIM}${cmd}${RESET}\n\n`)
|
|
316
|
+
process.stdout.write(`Will add to Makefile:\n`)
|
|
317
|
+
process.stdout.write(` ${CYAN}dev-tunnel:${RESET}\n`)
|
|
318
|
+
process.stdout.write(` ${CYAN} mekong ${cmd} --port ${port}${RESET}\n\n`)
|
|
319
|
+
|
|
320
|
+
const answer = await ask(`Confirm? (Y/n): `)
|
|
321
|
+
if (answer.trim().toLowerCase() === 'n') {
|
|
322
|
+
const manual = await promptManual(ask)
|
|
323
|
+
cmd = manual.cmd
|
|
324
|
+
port = manual.port
|
|
325
|
+
frameworkName = 'custom'
|
|
326
|
+
|
|
327
|
+
process.stdout.write(`\nWill add to Makefile:\n`)
|
|
328
|
+
process.stdout.write(` ${CYAN}dev-tunnel:${RESET}\n`)
|
|
329
|
+
process.stdout.write(` ${CYAN} mekong ${cmd} --port ${port}${RESET}\n\n`)
|
|
330
|
+
const confirm2 = await ask(`Confirm? (Y/n): `)
|
|
331
|
+
if (confirm2.trim().toLowerCase() === 'n') {
|
|
332
|
+
process.stdout.write(`Skipped. No changes made.\n`)
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
process.stdout.write(`\n${YELLOW}No known Python framework detected.${RESET}\n`)
|
|
338
|
+
const manual = await promptManual(ask)
|
|
339
|
+
cmd = manual.cmd
|
|
340
|
+
port = manual.port
|
|
341
|
+
frameworkName = 'custom'
|
|
342
|
+
|
|
343
|
+
process.stdout.write(`\nWill add to Makefile:\n`)
|
|
344
|
+
process.stdout.write(` ${CYAN}dev-tunnel:${RESET}\n`)
|
|
345
|
+
process.stdout.write(` ${CYAN} mekong ${cmd} --port ${port}${RESET}\n\n`)
|
|
346
|
+
const confirm2 = await ask(`Confirm? (Y/n): `)
|
|
347
|
+
if (confirm2.trim().toLowerCase() === 'n') {
|
|
348
|
+
process.stdout.write(`Skipped. No changes made.\n`)
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
injectPython(cwd, cmd, port)
|
|
354
|
+
|
|
355
|
+
process.stdout.write(`\n${CHECK} Done! mekong-tunnel is set up for ${BOLD}${frameworkName}${RESET}\n\n`)
|
|
356
|
+
process.stdout.write(`Run your tunnel:\n`)
|
|
357
|
+
process.stdout.write(` ${CYAN}make dev-tunnel${RESET}\n`)
|
|
358
|
+
process.stdout.write(` (or) ${CYAN}mekong ${cmd} --port ${port}${RESET}\n\n`)
|
|
359
|
+
process.stdout.write(`Make sure mekong binary is installed:\n`)
|
|
360
|
+
process.stdout.write(` ${CYAN}https://github.com/MuyleangIng/MekongTunnel/releases/latest${RESET}\n`)
|
|
361
|
+
process.stdout.write(` ${CYAN}pip install mekong-tunnel${RESET}\n`)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// Main exported function
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
async function runInit() {
|
|
368
|
+
const cwd = process.cwd()
|
|
369
|
+
|
|
370
|
+
const hasPkg = fileExists(path.join(cwd, 'package.json'))
|
|
371
|
+
const hasPyProj = fileExists(path.join(cwd, 'pyproject.toml'))
|
|
372
|
+
const hasReqs = fileExists(path.join(cwd, 'requirements.txt'))
|
|
373
|
+
const hasManage = fileExists(path.join(cwd, 'manage.py'))
|
|
374
|
+
const hasPipfile = fileExists(path.join(cwd, 'Pipfile'))
|
|
375
|
+
|
|
376
|
+
const isNode = hasPkg
|
|
377
|
+
const isPython = hasPyProj || hasReqs || hasManage || hasPipfile
|
|
378
|
+
|
|
379
|
+
if (!isNode && !isPython) {
|
|
380
|
+
process.stderr.write(`${RED}No supported project found in current directory.${RESET}\n`)
|
|
381
|
+
process.exit(1)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const { ask, close } = await makeAsker()
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
if (isNode && isPython) {
|
|
388
|
+
process.stdout.write(`\nBoth ${BOLD}Node.js${RESET} and ${BOLD}Python${RESET} projects detected.\n`)
|
|
389
|
+
const answer = await ask(`Configure which? (Node.js / Python / Both) [Node.js]: `)
|
|
390
|
+
const choice = answer.trim().toLowerCase()
|
|
391
|
+
|
|
392
|
+
if (choice === 'python') {
|
|
393
|
+
await configurePython(cwd, ask)
|
|
394
|
+
} else if (choice === 'both') {
|
|
395
|
+
await configureNode(cwd, ask)
|
|
396
|
+
await configurePython(cwd, ask)
|
|
397
|
+
} else {
|
|
398
|
+
await configureNode(cwd, ask)
|
|
399
|
+
}
|
|
400
|
+
} else if (isNode) {
|
|
401
|
+
await configureNode(cwd, ask)
|
|
402
|
+
} else {
|
|
403
|
+
await configurePython(cwd, ask)
|
|
404
|
+
}
|
|
405
|
+
} finally {
|
|
406
|
+
close()
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
module.exports = { runInit }
|
package/lib/wait-for-port.js
CHANGED
|
@@ -49,4 +49,27 @@ function waitForPort(port) {
|
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Single immediate check — resolves true if port is open right now, false otherwise.
|
|
54
|
+
* @param {number} port
|
|
55
|
+
* @returns {Promise<boolean>}
|
|
56
|
+
*/
|
|
57
|
+
function checkPortOpen(port) {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const sock = new net.Socket();
|
|
60
|
+
let done = false;
|
|
61
|
+
function finish(open) {
|
|
62
|
+
if (done) return;
|
|
63
|
+
done = true;
|
|
64
|
+
sock.destroy();
|
|
65
|
+
resolve(open);
|
|
66
|
+
}
|
|
67
|
+
sock.setTimeout(1000);
|
|
68
|
+
sock.once('connect', () => finish(true));
|
|
69
|
+
sock.once('error', () => finish(false));
|
|
70
|
+
sock.once('timeout', () => finish(false));
|
|
71
|
+
sock.connect(port, '127.0.0.1');
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { waitForPort, checkPortOpen };
|