devtunnel-cli 3.1.2 → 3.1.3
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/package.json +1 -1
- package/src/core/RUN.js +10 -3
- package/src/core/index.js +7 -1
- package/src/core/start.js +47 -19
- package/src/utils/folder-picker.js +29 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devtunnel-cli",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DevTunnel-CLI — fast, zero-config tool to share local servers for development, testing, demos, and webhook debugging. npm i -g devtunnel-cli.",
|
|
6
6
|
"main": "src/core/start.js",
|
package/src/core/RUN.js
CHANGED
|
@@ -37,13 +37,20 @@ process.stdout.write('\x1B[2J\x1B[0f');
|
|
|
37
37
|
console.clear();
|
|
38
38
|
|
|
39
39
|
const startPath = join(__dirname, "start.js");
|
|
40
|
-
|
|
40
|
+
// Use same Node binary as current process (works when "node" is not in PATH)
|
|
41
|
+
const nodeBin = process.execPath;
|
|
42
|
+
const child = spawn(nodeBin, [startPath], {
|
|
41
43
|
stdio: "inherit",
|
|
42
44
|
shell: false
|
|
43
45
|
});
|
|
44
46
|
|
|
45
|
-
child.on("error", (
|
|
46
|
-
|
|
47
|
+
child.on("error", (err) => {
|
|
48
|
+
if (err.code === "ENOENT") {
|
|
49
|
+
console.error("❌ Node.js is required but could not be found.");
|
|
50
|
+
console.error(" Install from: https://nodejs.org");
|
|
51
|
+
} else {
|
|
52
|
+
console.error("❌ Error starting app:", err.message);
|
|
53
|
+
}
|
|
47
54
|
process.exit(1);
|
|
48
55
|
});
|
|
49
56
|
|
package/src/core/index.js
CHANGED
|
@@ -221,6 +221,11 @@ async function tryTunnelServices() {
|
|
|
221
221
|
stdio: "pipe"
|
|
222
222
|
});
|
|
223
223
|
|
|
224
|
+
tunnelProcess.on("error", (err) => {
|
|
225
|
+
console.error("\nERROR: Tunnel process failed to start:", err.code === "ENOENT" ? `"${service.command}" not found` : err.message);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
});
|
|
228
|
+
|
|
224
229
|
setupTunnelHandlers(service.name);
|
|
225
230
|
|
|
226
231
|
// Wait a bit to see if tunnel starts successfully
|
|
@@ -253,6 +258,7 @@ async function tryTunnelServices() {
|
|
|
253
258
|
|
|
254
259
|
function setupTunnelHandlers(serviceName) {
|
|
255
260
|
if (!tunnelProcess) return;
|
|
261
|
+
if (!tunnelProcess.stdout || !tunnelProcess.stderr) return;
|
|
256
262
|
|
|
257
263
|
tunnelProcess.stdout.on("data", (data) => {
|
|
258
264
|
const output = data.toString();
|
|
@@ -311,7 +317,7 @@ function setupTunnelHandlers(serviceName) {
|
|
|
311
317
|
});
|
|
312
318
|
});
|
|
313
319
|
|
|
314
|
-
tunnelProcess.stderr
|
|
320
|
+
tunnelProcess.stderr?.on("data", (data) => {
|
|
315
321
|
const output = data.toString();
|
|
316
322
|
|
|
317
323
|
// For Cloudflare, check if URL is in stderr (sometimes it is)
|
package/src/core/start.js
CHANGED
|
@@ -23,7 +23,7 @@ function getPackageVersion() {
|
|
|
23
23
|
return "3.1.1";
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
// Helper to run command
|
|
26
|
+
// Helper to run command (cross-platform)
|
|
27
27
|
function runCommand(command, args = [], cwd = process.cwd()) {
|
|
28
28
|
return new Promise((resolve) => {
|
|
29
29
|
const proc = spawn(command, args, {
|
|
@@ -36,15 +36,16 @@ function runCommand(command, args = [], cwd = process.cwd()) {
|
|
|
36
36
|
proc.stdout?.on("data", (data) => output += data.toString());
|
|
37
37
|
proc.stderr?.on("data", (data) => output += data.toString());
|
|
38
38
|
|
|
39
|
-
proc.on("close", (code) => resolve({ code, output }));
|
|
40
|
-
proc.on("error", () => resolve({ code: 1, output: "" }));
|
|
39
|
+
proc.on("close", (code, signal) => resolve({ code: code ?? (signal ? 1 : 0), output }));
|
|
40
|
+
proc.on("error", (err) => resolve({ code: 1, output: "", error: err }));
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// Check if command exists
|
|
44
|
+
// Check if command exists (Windows: where, macOS/Linux: which)
|
|
45
45
|
async function commandExists(command) {
|
|
46
|
-
const
|
|
47
|
-
|
|
46
|
+
const isWin = process.platform === "win32";
|
|
47
|
+
const result = await runCommand(isWin ? "where" : "which", [command]);
|
|
48
|
+
return result.code === 0 && (result.output || "").trim().length > 0;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
// Check if a port is in use (dev server running)
|
|
@@ -280,14 +281,21 @@ async function main() {
|
|
|
280
281
|
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
281
282
|
console.log("");
|
|
282
283
|
|
|
283
|
-
// Step 1: Check Node.js
|
|
284
|
+
// Step 1: Check Node.js (version and availability)
|
|
284
285
|
console.log("[1/4] Checking Node.js...");
|
|
286
|
+
const minNodeMajor = 16;
|
|
287
|
+
const currentMajor = parseInt(process.version.slice(1).split(".")[0], 10);
|
|
288
|
+
if (isNaN(currentMajor) || currentMajor < minNodeMajor) {
|
|
289
|
+
console.log("ERROR: Node.js " + minNodeMajor + "+ required. Current:", process.version);
|
|
290
|
+
console.log("Install from: https://nodejs.org/");
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
285
293
|
if (!await commandExists("node")) {
|
|
286
|
-
console.log("ERROR: Node.js not found
|
|
294
|
+
console.log("ERROR: Node.js not found in PATH.");
|
|
287
295
|
console.log("Install from: https://nodejs.org/");
|
|
288
296
|
process.exit(1);
|
|
289
297
|
}
|
|
290
|
-
console.log("SUCCESS: Node.js installed");
|
|
298
|
+
console.log("SUCCESS: Node.js " + process.version + " installed");
|
|
291
299
|
console.log("");
|
|
292
300
|
|
|
293
301
|
// Step 2: Check Cloudflare (bundled or system-installed)
|
|
@@ -340,13 +348,19 @@ async function main() {
|
|
|
340
348
|
console.log("[3/4] Checking dependencies...");
|
|
341
349
|
const nodeModulesPath = join(PROJECT_ROOT, "node_modules");
|
|
342
350
|
if (!existsSync(nodeModulesPath)) {
|
|
351
|
+
const hasNpm = await commandExists("npm");
|
|
352
|
+
if (!hasNpm) {
|
|
353
|
+
console.log("ERROR: npm not found. Node.js/npm is required.");
|
|
354
|
+
console.log("Install from: https://nodejs.org/");
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
343
357
|
console.log("Installing dependencies...");
|
|
344
358
|
console.log("");
|
|
345
359
|
// Run npm install in the project root directory
|
|
346
360
|
const result = await runCommand("npm", ["install"], PROJECT_ROOT);
|
|
347
361
|
if (result.code !== 0) {
|
|
348
362
|
console.log("");
|
|
349
|
-
console.log("ERROR: npm install failed");
|
|
363
|
+
console.log("ERROR: npm install failed" + (result.error ? ": " + result.error.message : ""));
|
|
350
364
|
process.exit(1);
|
|
351
365
|
}
|
|
352
366
|
console.log("");
|
|
@@ -608,11 +622,14 @@ async function main() {
|
|
|
608
622
|
if (isHtmlProject && !portInUseNow) {
|
|
609
623
|
console.log("Starting built-in static server for HTML project...");
|
|
610
624
|
const staticServerPath = join(__dirname, "static-server.js");
|
|
611
|
-
|
|
625
|
+
const nodeBin = process.execPath;
|
|
626
|
+
staticServerProcess = spawn(nodeBin, [staticServerPath, projectPath, devPort.toString()], {
|
|
612
627
|
stdio: "pipe",
|
|
613
628
|
shell: false
|
|
614
629
|
});
|
|
615
|
-
staticServerProcess.on("error", () => {
|
|
630
|
+
staticServerProcess.on("error", (err) => {
|
|
631
|
+
console.error("ERROR: Could not start static server:", err.code === "ENOENT" ? "Node not found" : err.message);
|
|
632
|
+
});
|
|
616
633
|
const ready = await waitForServerReady(devPort, 10000);
|
|
617
634
|
if (!ready) {
|
|
618
635
|
if (staticServerProcess) staticServerProcess.kill();
|
|
@@ -627,31 +644,38 @@ async function main() {
|
|
|
627
644
|
// Start proxy server
|
|
628
645
|
console.log("Starting services...");
|
|
629
646
|
console.log("");
|
|
647
|
+
const nodeBin = process.execPath;
|
|
630
648
|
const proxyPath = join(__dirname, "proxy-server.js");
|
|
631
649
|
const proxyArgs = [proxyPath, devPort.toString(), proxyPort.toString(), projectName];
|
|
632
650
|
if (basePath) proxyArgs.push(basePath);
|
|
633
|
-
const proxyProcess = spawn(
|
|
651
|
+
const proxyProcess = spawn(nodeBin, proxyArgs, {
|
|
634
652
|
stdio: "inherit",
|
|
635
653
|
shell: false
|
|
636
654
|
});
|
|
637
655
|
|
|
656
|
+
const onChildError = (name, err) => {
|
|
657
|
+
console.error("\nERROR: " + name + " failed to start:", err.code === "ENOENT" ? "Node not found. Install from https://nodejs.org" : err.message);
|
|
658
|
+
process.exit(1);
|
|
659
|
+
};
|
|
660
|
+
proxyProcess.on("error", (err) => onChildError("Proxy server", err));
|
|
661
|
+
|
|
638
662
|
// Wait for proxy to start
|
|
639
663
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
640
664
|
|
|
641
665
|
// Run main tunnel app (connects to proxy port)
|
|
642
|
-
// Use shell: false to properly handle paths with spaces
|
|
643
666
|
const indexPath = join(__dirname, "index.js");
|
|
644
|
-
const tunnelProcess = spawn(
|
|
667
|
+
const tunnelProcess = spawn(nodeBin, [indexPath, proxyPort.toString(), projectName, projectPath], {
|
|
645
668
|
stdio: "inherit",
|
|
646
669
|
shell: false
|
|
647
670
|
});
|
|
671
|
+
tunnelProcess.on("error", (err) => onChildError("Tunnel service", err));
|
|
648
672
|
|
|
649
673
|
// Handle cleanup
|
|
650
674
|
const cleanup = () => {
|
|
651
675
|
console.log("\nShutting down...");
|
|
652
|
-
if (staticServerProcess) staticServerProcess.kill();
|
|
653
|
-
proxyProcess.kill();
|
|
654
|
-
tunnelProcess.kill();
|
|
676
|
+
if (staticServerProcess) try { staticServerProcess.kill(); } catch (_) {}
|
|
677
|
+
try { proxyProcess.kill(); } catch (_) {}
|
|
678
|
+
try { tunnelProcess.kill(); } catch (_) {}
|
|
655
679
|
process.exit(0);
|
|
656
680
|
};
|
|
657
681
|
|
|
@@ -670,6 +694,10 @@ async function main() {
|
|
|
670
694
|
|
|
671
695
|
// Run
|
|
672
696
|
main().catch((error) => {
|
|
673
|
-
|
|
697
|
+
const msg = error && error.message ? error.message : String(error);
|
|
698
|
+
console.error("\nERROR:", msg);
|
|
699
|
+
if (error && error.code === "ENOENT") {
|
|
700
|
+
console.error(" A required command or file was not found. Check that Node.js is installed: https://nodejs.org");
|
|
701
|
+
}
|
|
674
702
|
process.exit(1);
|
|
675
703
|
});
|
|
@@ -75,7 +75,14 @@ export async function selectFolder() {
|
|
|
75
75
|
return null;
|
|
76
76
|
|
|
77
77
|
} catch (error) {
|
|
78
|
-
|
|
78
|
+
const msg = error && error.message ? error.message : String(error);
|
|
79
|
+
if (os === "win32" && (msg.includes("ENOENT") || msg.includes("spawn powershell"))) {
|
|
80
|
+
console.error("Folder picker: PowerShell not found. Run from a path or pass the project path as an argument.");
|
|
81
|
+
} else if (os !== "win32" && os !== "darwin" && (msg.includes("ENOENT") || msg.includes("spawn"))) {
|
|
82
|
+
console.error("Folder picker: zenity or kdialog not found. Install one (e.g. apt install zenity) or pass the project path as an argument.");
|
|
83
|
+
} else {
|
|
84
|
+
console.error("Folder picker error:", msg);
|
|
85
|
+
}
|
|
79
86
|
return null;
|
|
80
87
|
}
|
|
81
88
|
}
|
|
@@ -96,15 +103,21 @@ function runPowerShell(script) {
|
|
|
96
103
|
let stderr = "";
|
|
97
104
|
proc.stderr?.on("data", (data) => stderr += data.toString());
|
|
98
105
|
|
|
99
|
-
proc.on("close", (code) => {
|
|
106
|
+
proc.on("close", (code, signal) => {
|
|
100
107
|
if (code === 0) {
|
|
101
108
|
resolve();
|
|
102
109
|
} else {
|
|
103
|
-
reject(new Error(stderr || `PowerShell exited with code ${code}`));
|
|
110
|
+
reject(new Error(stderr || `PowerShell exited with code ${code ?? signal ?? 1}`));
|
|
104
111
|
}
|
|
105
112
|
});
|
|
106
113
|
|
|
107
|
-
proc.on("error",
|
|
114
|
+
proc.on("error", (err) => {
|
|
115
|
+
if (err.code === "ENOENT") {
|
|
116
|
+
reject(new Error("PowerShell not found. Install it or use a different method to select the folder."));
|
|
117
|
+
} else {
|
|
118
|
+
reject(err);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
108
121
|
});
|
|
109
122
|
}
|
|
110
123
|
|
|
@@ -124,17 +137,24 @@ function runCommand(command, args, outputFile) {
|
|
|
124
137
|
});
|
|
125
138
|
}
|
|
126
139
|
|
|
127
|
-
proc.on("close", (code) => {
|
|
128
|
-
|
|
140
|
+
proc.on("close", (code, signal) => {
|
|
141
|
+
const exitCode = code ?? (signal ? 1 : 0);
|
|
142
|
+
if (exitCode === 0) {
|
|
129
143
|
if (outputFile && stdout) {
|
|
130
|
-
writeFileSync(outputFile, stdout.trim());
|
|
144
|
+
try { writeFileSync(outputFile, stdout.trim()); } catch (_) {}
|
|
131
145
|
}
|
|
132
146
|
resolve();
|
|
133
147
|
} else {
|
|
134
|
-
reject(new Error(`Command failed with code ${
|
|
148
|
+
reject(new Error(`Command failed with code ${exitCode}`));
|
|
135
149
|
}
|
|
136
150
|
});
|
|
137
151
|
|
|
138
|
-
proc.on("error",
|
|
152
|
+
proc.on("error", (err) => {
|
|
153
|
+
if (err.code === "ENOENT") {
|
|
154
|
+
reject(new Error(`Command not found: ${command}`));
|
|
155
|
+
} else {
|
|
156
|
+
reject(err);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
139
159
|
});
|
|
140
160
|
}
|