opentunnel-cli 1.0.19 → 1.0.21
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 +142 -5
- package/dist/cli/index.js +419 -65
- package/dist/cli/index.js.map +1 -1
- package/dist/server/TunnelServer.d.ts +3 -0
- package/dist/server/TunnelServer.d.ts.map +1 -1
- package/dist/server/TunnelServer.js +186 -51
- package/dist/server/TunnelServer.js.map +1 -1
- package/dist/shared/types.d.ts +1 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -53,6 +53,28 @@ function getRegistryPath() {
|
|
|
53
53
|
const registryDir = path.join(os.homedir(), ".opentunnel");
|
|
54
54
|
return path.join(registryDir, "registry.json");
|
|
55
55
|
}
|
|
56
|
+
function getLogsDir() {
|
|
57
|
+
const os = require("os");
|
|
58
|
+
const path = require("path");
|
|
59
|
+
const fs = require("fs");
|
|
60
|
+
const logsDir = path.join(os.homedir(), ".opentunnel", "logs");
|
|
61
|
+
// Ensure directory exists
|
|
62
|
+
if (!fs.existsSync(logsDir)) {
|
|
63
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
return logsDir;
|
|
66
|
+
}
|
|
67
|
+
function getLogFilePath(instanceName) {
|
|
68
|
+
const path = require("path");
|
|
69
|
+
// Sanitize instance name for filename
|
|
70
|
+
const safeName = instanceName.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
71
|
+
return path.join(getLogsDir(), `${safeName}.log`);
|
|
72
|
+
}
|
|
73
|
+
function getPidFilePath(instanceName) {
|
|
74
|
+
const path = require("path");
|
|
75
|
+
const safeName = instanceName.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
76
|
+
return path.join(getLogsDir(), `${safeName}.pid`);
|
|
77
|
+
}
|
|
56
78
|
function loadRegistry() {
|
|
57
79
|
const fs = require("fs");
|
|
58
80
|
const path = require("path");
|
|
@@ -98,6 +120,49 @@ function unregisterInstanceByPid(pid) {
|
|
|
98
120
|
registry.instances = registry.instances.filter(i => i.pid !== pid);
|
|
99
121
|
saveRegistry(registry);
|
|
100
122
|
}
|
|
123
|
+
function getConfigPath() {
|
|
124
|
+
const os = require("os");
|
|
125
|
+
const configDir = path.join(os.homedir(), ".opentunnel");
|
|
126
|
+
return path.join(configDir, "config.json");
|
|
127
|
+
}
|
|
128
|
+
function loadCLIConfig() {
|
|
129
|
+
const configPath = getConfigPath();
|
|
130
|
+
try {
|
|
131
|
+
if (fs.existsSync(configPath)) {
|
|
132
|
+
return JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch { }
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
function saveCLIConfig(config) {
|
|
139
|
+
const configPath = getConfigPath();
|
|
140
|
+
const dir = path.dirname(configPath);
|
|
141
|
+
if (!fs.existsSync(dir)) {
|
|
142
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
145
|
+
}
|
|
146
|
+
function getDefaultDomain() {
|
|
147
|
+
const config = loadCLIConfig();
|
|
148
|
+
return config.defaultDomain || null;
|
|
149
|
+
}
|
|
150
|
+
function setDefaultDomain(domain, basePath) {
|
|
151
|
+
const config = loadCLIConfig();
|
|
152
|
+
config.defaultDomain = { domain };
|
|
153
|
+
if (basePath)
|
|
154
|
+
config.defaultDomain.basePath = basePath;
|
|
155
|
+
saveCLIConfig(config);
|
|
156
|
+
}
|
|
157
|
+
function clearDefaultDomain() {
|
|
158
|
+
const config = loadCLIConfig();
|
|
159
|
+
if (config.defaultDomain) {
|
|
160
|
+
delete config.defaultDomain;
|
|
161
|
+
saveCLIConfig(config);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
101
166
|
// Load .env file if exists
|
|
102
167
|
function loadEnvFile() {
|
|
103
168
|
const envPath = path.join(process.cwd(), ".env");
|
|
@@ -156,7 +221,7 @@ program
|
|
|
156
221
|
.name("opentunnel")
|
|
157
222
|
.alias("ot")
|
|
158
223
|
.description("Expose local ports to the internet via custom domains or ngrok")
|
|
159
|
-
.version("1.0.
|
|
224
|
+
.version("1.0.21");
|
|
160
225
|
// Helper function to build WebSocket URL from domain
|
|
161
226
|
// User only provides base domain (e.g., fjrg2007.com), system handles the rest
|
|
162
227
|
// Note: --insecure flag only affects certificate verification, not the protocol
|
|
@@ -351,6 +416,178 @@ program
|
|
|
351
416
|
process.exit(1);
|
|
352
417
|
}
|
|
353
418
|
});
|
|
419
|
+
// Domain management commands
|
|
420
|
+
program
|
|
421
|
+
.command("setdomain <domain>")
|
|
422
|
+
.description("Set a default domain for quick tunnels")
|
|
423
|
+
.option("-b, --base-path <path>", "Server base path (default: op)")
|
|
424
|
+
.action((domain, options) => {
|
|
425
|
+
setDefaultDomain(domain, options.basePath);
|
|
426
|
+
console.log(chalk_1.default.green(`\n Default domain set to: ${chalk_1.default.cyan(domain)}`));
|
|
427
|
+
if (options.basePath) {
|
|
428
|
+
console.log(chalk_1.default.gray(` Base path: ${options.basePath}`));
|
|
429
|
+
}
|
|
430
|
+
console.log(chalk_1.default.gray(`\n Now you can use 'opentunnel expl <port>' without specifying -s\n`));
|
|
431
|
+
});
|
|
432
|
+
program
|
|
433
|
+
.command("getdomain")
|
|
434
|
+
.description("Show the default domain configuration")
|
|
435
|
+
.action(() => {
|
|
436
|
+
const config = getDefaultDomain();
|
|
437
|
+
if (config) {
|
|
438
|
+
console.log(chalk_1.default.cyan("\n Default Domain Configuration"));
|
|
439
|
+
console.log(chalk_1.default.gray(" ─────────────────────────────"));
|
|
440
|
+
console.log(` ${chalk_1.default.white("Domain:")} ${chalk_1.default.green(config.domain)}`);
|
|
441
|
+
if (config.basePath) {
|
|
442
|
+
console.log(` ${chalk_1.default.white("Base Path:")} ${chalk_1.default.gray(config.basePath)}`);
|
|
443
|
+
}
|
|
444
|
+
console.log("");
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
console.log(chalk_1.default.yellow("\n No default domain configured"));
|
|
448
|
+
console.log(chalk_1.default.gray(" Use 'opentunnel setdomain <domain>' to set one\n"));
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
program
|
|
452
|
+
.command("cleardomain")
|
|
453
|
+
.description("Remove the default domain configuration")
|
|
454
|
+
.action(() => {
|
|
455
|
+
if (clearDefaultDomain()) {
|
|
456
|
+
console.log(chalk_1.default.green("\n Default domain configuration removed\n"));
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
console.log(chalk_1.default.yellow("\n No default domain was configured\n"));
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
// Expose Local Server command (shortcut for quick --local-server)
|
|
463
|
+
program
|
|
464
|
+
.command("expl <port>")
|
|
465
|
+
.description("Expose local port via local server (shortcut for 'quick <port> --local-server')")
|
|
466
|
+
.option("-s, --domain <domain>", "Server domain (uses default if not specified)")
|
|
467
|
+
.option("-b, --base-path <path>", "Server base path (default: op)")
|
|
468
|
+
.option("-n, --subdomain <name>", "Request a specific subdomain (e.g., 'myapp')")
|
|
469
|
+
.option("-p, --protocol <proto>", "Protocol (http, https, tcp)", "http")
|
|
470
|
+
.option("-h, --host <host>", "Local host to forward to", "localhost")
|
|
471
|
+
.option("-t, --token <token>", "Authentication token (if server requires it)")
|
|
472
|
+
.option("--insecure", "Skip SSL certificate verification (for self-signed certs)")
|
|
473
|
+
.option("--server-port <port>", "Port for the local server (default: 443)", "443")
|
|
474
|
+
.action(async (port, options) => {
|
|
475
|
+
// Get domain from options or default config
|
|
476
|
+
let domain = options.domain;
|
|
477
|
+
let basePath = options.basePath;
|
|
478
|
+
if (!domain) {
|
|
479
|
+
const defaultConfig = getDefaultDomain();
|
|
480
|
+
if (defaultConfig) {
|
|
481
|
+
domain = defaultConfig.domain;
|
|
482
|
+
if (!basePath && defaultConfig.basePath) {
|
|
483
|
+
basePath = defaultConfig.basePath;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (!domain) {
|
|
488
|
+
console.log(chalk_1.default.red("Error: No domain specified and no default domain configured"));
|
|
489
|
+
console.log(chalk_1.default.gray("\nOptions:"));
|
|
490
|
+
console.log(chalk_1.default.cyan(" 1. Specify domain: opentunnel expl 3000 -s example.com"));
|
|
491
|
+
console.log(chalk_1.default.cyan(" 2. Set default: opentunnel setdomain example.com"));
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
const { TunnelServer } = await Promise.resolve().then(() => __importStar(require("../server/TunnelServer")));
|
|
495
|
+
const serverPort = parseInt(options.serverPort);
|
|
496
|
+
console.log(chalk_1.default.cyan(`
|
|
497
|
+
██████╗ ██████╗ ███████╗███╗ ██╗████████╗██╗ ██╗███╗ ██╗███╗ ██╗███████╗██╗
|
|
498
|
+
██╔═══██╗██╔══██╗██╔════╝████╗ ██║╚══██╔══╝██║ ██║████╗ ██║████╗ ██║██╔════╝██║
|
|
499
|
+
██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ ██║ ██║██╔██╗ ██║██╔██╗ ██║█████╗ ██║
|
|
500
|
+
██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║ ██║ ██║██║╚██╗██║██║╚██╗██║██╔══╝ ██║
|
|
501
|
+
╚██████╔╝██║ ███████╗██║ ╚████║ ██║ ╚██████╔╝██║ ╚████║██║ ╚████║███████╗███████╗
|
|
502
|
+
╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝╚══════╝╚══════╝
|
|
503
|
+
`));
|
|
504
|
+
console.log(chalk_1.default.gray(` Starting local server on port ${serverPort}...\n`));
|
|
505
|
+
let localServer;
|
|
506
|
+
try {
|
|
507
|
+
localServer = new TunnelServer({
|
|
508
|
+
port: serverPort,
|
|
509
|
+
host: "0.0.0.0",
|
|
510
|
+
domain: domain,
|
|
511
|
+
basePath: basePath || "op",
|
|
512
|
+
tunnelPortRange: { min: 10000, max: 20000 },
|
|
513
|
+
selfSignedHttps: { enabled: true },
|
|
514
|
+
auth: options.token ? { required: true, tokens: [options.token] } : undefined,
|
|
515
|
+
});
|
|
516
|
+
await localServer.start();
|
|
517
|
+
console.log(chalk_1.default.green(` Server running on port ${serverPort}\n`));
|
|
518
|
+
}
|
|
519
|
+
catch (err) {
|
|
520
|
+
console.log(chalk_1.default.red(`Failed to start server: ${err.message}`));
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
// Connect to local server
|
|
524
|
+
const serverUrl = `wss://localhost:${serverPort}/_tunnel`;
|
|
525
|
+
const spinner = (0, ora_1.default)("Connecting to local server...").start();
|
|
526
|
+
try {
|
|
527
|
+
const client = new TunnelClient_1.TunnelClient({
|
|
528
|
+
serverUrl,
|
|
529
|
+
token: options.token,
|
|
530
|
+
reconnect: true,
|
|
531
|
+
silent: true,
|
|
532
|
+
rejectUnauthorized: false // Local server uses self-signed cert
|
|
533
|
+
});
|
|
534
|
+
await client.connect();
|
|
535
|
+
spinner.text = "Creating tunnel...";
|
|
536
|
+
const { tunnelId, publicUrl } = await client.createTunnel({
|
|
537
|
+
protocol: options.protocol,
|
|
538
|
+
localHost: options.host,
|
|
539
|
+
localPort: parseInt(port),
|
|
540
|
+
subdomain: options.subdomain,
|
|
541
|
+
});
|
|
542
|
+
spinner.succeed("Tunnel established!");
|
|
543
|
+
console.log("");
|
|
544
|
+
console.log(chalk_1.default.cyan(` OpenTunnel ${chalk_1.default.gray(`(local server → ${domain})`)}`));
|
|
545
|
+
console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
|
|
546
|
+
console.log(` ${chalk_1.default.white("Status:")} ${chalk_1.default.green("● Online")}`);
|
|
547
|
+
console.log(` ${chalk_1.default.white("Protocol:")} ${chalk_1.default.yellow(options.protocol.toUpperCase())}`);
|
|
548
|
+
console.log(` ${chalk_1.default.white("Local:")} ${chalk_1.default.gray(`${options.host}:${port}`)}`);
|
|
549
|
+
console.log(` ${chalk_1.default.white("Public:")} ${chalk_1.default.green(publicUrl)}`);
|
|
550
|
+
console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
|
|
551
|
+
console.log("");
|
|
552
|
+
console.log(chalk_1.default.gray(" Press Ctrl+C to close the tunnel"));
|
|
553
|
+
console.log("");
|
|
554
|
+
// Keep alive with uptime counter
|
|
555
|
+
const startTime = Date.now();
|
|
556
|
+
const statsInterval = setInterval(() => {
|
|
557
|
+
const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
|
|
558
|
+
process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
|
|
559
|
+
}, 1000);
|
|
560
|
+
// Handle exit
|
|
561
|
+
const cleanup = async () => {
|
|
562
|
+
clearInterval(statsInterval);
|
|
563
|
+
console.log("\n");
|
|
564
|
+
const closeSpinner = (0, ora_1.default)("Closing tunnel...").start();
|
|
565
|
+
await client.closeTunnel(tunnelId);
|
|
566
|
+
await client.disconnect();
|
|
567
|
+
if (localServer) {
|
|
568
|
+
await localServer.stop();
|
|
569
|
+
}
|
|
570
|
+
closeSpinner.succeed("Tunnel closed");
|
|
571
|
+
process.exit(0);
|
|
572
|
+
};
|
|
573
|
+
process.on("SIGINT", cleanup);
|
|
574
|
+
process.on("SIGTERM", cleanup);
|
|
575
|
+
// Handle reconnection
|
|
576
|
+
client.on("disconnected", () => {
|
|
577
|
+
console.log(chalk_1.default.yellow("\n Disconnected, reconnecting..."));
|
|
578
|
+
});
|
|
579
|
+
client.on("connected", () => {
|
|
580
|
+
console.log(chalk_1.default.green(" Reconnected!"));
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
catch (err) {
|
|
584
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
585
|
+
if (localServer) {
|
|
586
|
+
await localServer.stop();
|
|
587
|
+
}
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
});
|
|
354
591
|
// HTTP tunnel command
|
|
355
592
|
program
|
|
356
593
|
.command("http <port>")
|
|
@@ -550,8 +787,8 @@ program
|
|
|
550
787
|
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
551
788
|
const fsAsync = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
552
789
|
const pathAsync = await Promise.resolve().then(() => __importStar(require("path")));
|
|
553
|
-
const pidFile =
|
|
554
|
-
const logFile =
|
|
790
|
+
const pidFile = getPidFilePath("server");
|
|
791
|
+
const logFile = getLogFilePath("server");
|
|
555
792
|
// Check if already running
|
|
556
793
|
if (fsAsync.existsSync(pidFile)) {
|
|
557
794
|
const oldPid = fsAsync.readFileSync(pidFile, "utf-8").trim();
|
|
@@ -722,7 +959,7 @@ program
|
|
|
722
959
|
.action(async () => {
|
|
723
960
|
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
724
961
|
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
725
|
-
const pidFile =
|
|
962
|
+
const pidFile = getPidFilePath("server");
|
|
726
963
|
if (!fs.existsSync(pidFile)) {
|
|
727
964
|
console.log(chalk_1.default.yellow("No server running (PID file not found)"));
|
|
728
965
|
return;
|
|
@@ -742,43 +979,6 @@ program
|
|
|
742
979
|
console.log(chalk_1.default.red(`Failed to stop server: ${err.message}`));
|
|
743
980
|
}
|
|
744
981
|
});
|
|
745
|
-
// Logs command
|
|
746
|
-
program
|
|
747
|
-
.command("logs")
|
|
748
|
-
.description("Show server logs")
|
|
749
|
-
.option("-f, --follow", "Follow log output")
|
|
750
|
-
.option("-n, --lines <n>", "Number of lines to show", "50")
|
|
751
|
-
.action(async (options) => {
|
|
752
|
-
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
753
|
-
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
754
|
-
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
755
|
-
const logFile = path.join(process.cwd(), "opentunnel.log");
|
|
756
|
-
if (!fs.existsSync(logFile)) {
|
|
757
|
-
console.log(chalk_1.default.yellow("No log file found"));
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
if (options.follow) {
|
|
761
|
-
// Use tail -f on Unix or PowerShell on Windows
|
|
762
|
-
const isWindows = process.platform === "win32";
|
|
763
|
-
if (isWindows) {
|
|
764
|
-
const child = spawn("powershell", ["-Command", `Get-Content -Path "${logFile}" -Tail ${options.lines} -Wait`], {
|
|
765
|
-
stdio: "inherit"
|
|
766
|
-
});
|
|
767
|
-
child.on("error", () => {
|
|
768
|
-
// Fallback: just read the file
|
|
769
|
-
console.log(fs.readFileSync(logFile, "utf-8"));
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
else
|
|
773
|
-
spawn("tail", ["-f", "-n", options.lines, logFile], { stdio: "inherit" });
|
|
774
|
-
}
|
|
775
|
-
else {
|
|
776
|
-
const content = fs.readFileSync(logFile, "utf-8");
|
|
777
|
-
const lines = content.split("\n");
|
|
778
|
-
const lastLines = lines.slice(-parseInt(options.lines));
|
|
779
|
-
console.log(lastLines.join("\n"));
|
|
780
|
-
}
|
|
781
|
-
});
|
|
782
982
|
// Status command
|
|
783
983
|
program
|
|
784
984
|
.command("status")
|
|
@@ -1158,8 +1358,8 @@ program
|
|
|
1158
1358
|
const shouldDetach = options.detach === true;
|
|
1159
1359
|
if (shouldDetach) {
|
|
1160
1360
|
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
1161
|
-
const pidFile =
|
|
1162
|
-
const logFile =
|
|
1361
|
+
const pidFile = getPidFilePath(instanceName);
|
|
1362
|
+
const logFile = getLogFilePath(instanceName);
|
|
1163
1363
|
// Check if already running
|
|
1164
1364
|
if (fsModule.existsSync(pidFile)) {
|
|
1165
1365
|
const oldPid = fsModule.readFileSync(pidFile, "utf-8").trim();
|
|
@@ -1298,13 +1498,25 @@ program
|
|
|
1298
1498
|
const useHttps = config.server?.https !== false;
|
|
1299
1499
|
const hasTunnels = tunnelsToStart.length > 0;
|
|
1300
1500
|
// Parse multiple domains from config
|
|
1501
|
+
// Helper to check if domain is DuckDNS
|
|
1502
|
+
const isDuckDns = (domain) => domain.toLowerCase().endsWith(".duckdns.org");
|
|
1301
1503
|
let serverDomains;
|
|
1302
1504
|
if (config.server?.domains && config.server.domains.length > 0) {
|
|
1303
1505
|
serverDomains = config.server.domains.map(d => {
|
|
1304
1506
|
if (typeof d === "string") {
|
|
1305
|
-
|
|
1507
|
+
// DuckDNS domains don't use basePath
|
|
1508
|
+
return { domain: d, basePath: isDuckDns(d) ? "" : basePath };
|
|
1306
1509
|
}
|
|
1307
|
-
|
|
1510
|
+
// If basePath is explicitly set for DuckDNS, it will error in TunnelServer
|
|
1511
|
+
// If not set, default to empty for DuckDNS, or global basePath for others
|
|
1512
|
+
const domainBasePath = d.basePath !== undefined
|
|
1513
|
+
? d.basePath
|
|
1514
|
+
: (isDuckDns(d.domain) ? "" : basePath);
|
|
1515
|
+
return {
|
|
1516
|
+
domain: d.domain,
|
|
1517
|
+
basePath: domainBasePath,
|
|
1518
|
+
wildcard: d.wildcard,
|
|
1519
|
+
};
|
|
1308
1520
|
});
|
|
1309
1521
|
}
|
|
1310
1522
|
// Check if we have domain configuration (single or multiple)
|
|
@@ -1554,18 +1766,27 @@ program
|
|
|
1554
1766
|
const pidFile = instance.pidFile;
|
|
1555
1767
|
const instanceName = instance.name;
|
|
1556
1768
|
const cwd = instance.cwd;
|
|
1557
|
-
|
|
1769
|
+
// Open file descriptor for logging (required for detached processes)
|
|
1770
|
+
const logFd = fs.openSync(logFile, "a");
|
|
1558
1771
|
const child = spawn(process.execPath, [process.argv[1], "up", "-f", pathModule.basename(configPath)], {
|
|
1559
1772
|
cwd,
|
|
1560
1773
|
detached: true,
|
|
1561
|
-
stdio: ["ignore",
|
|
1774
|
+
stdio: ["ignore", logFd, logFd],
|
|
1562
1775
|
env: { ...process.env, OPENTUNNEL_INSTANCE_NAME: instanceName },
|
|
1563
1776
|
});
|
|
1564
1777
|
child.unref();
|
|
1565
|
-
|
|
1566
|
-
|
|
1778
|
+
fs.closeSync(logFd);
|
|
1779
|
+
// Wait and check if process started successfully
|
|
1780
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
1781
|
+
let processRunning = false;
|
|
1567
1782
|
try {
|
|
1568
1783
|
process.kill(child.pid, 0);
|
|
1784
|
+
processRunning = true;
|
|
1785
|
+
}
|
|
1786
|
+
catch {
|
|
1787
|
+
processRunning = false;
|
|
1788
|
+
}
|
|
1789
|
+
if (processRunning) {
|
|
1569
1790
|
// Process is running, register it
|
|
1570
1791
|
registerInstance({
|
|
1571
1792
|
name: instanceName,
|
|
@@ -1580,11 +1801,43 @@ program
|
|
|
1580
1801
|
fs.writeFileSync(pidFile, child.pid.toString());
|
|
1581
1802
|
console.log(chalk_1.default.green(` ↑ Started ${instanceName} (PID: ${child.pid})`));
|
|
1582
1803
|
}
|
|
1583
|
-
|
|
1804
|
+
else {
|
|
1805
|
+
// Process failed - check logs for error
|
|
1584
1806
|
console.log(chalk_1.default.red(` ✗ Failed to start ${instanceName}`));
|
|
1807
|
+
// Read last lines of log to show error
|
|
1808
|
+
if (fs.existsSync(logFile)) {
|
|
1809
|
+
const logContent = fs.readFileSync(logFile, "utf-8");
|
|
1810
|
+
const lines = logContent.split("\n");
|
|
1811
|
+
// Find error lines
|
|
1812
|
+
const errorLines = [];
|
|
1813
|
+
let capturing = false;
|
|
1814
|
+
for (let i = lines.length - 1; i >= 0 && errorLines.length < 10; i--) {
|
|
1815
|
+
const line = lines[i];
|
|
1816
|
+
if (line.includes("Error:") || line.includes("error:") || capturing) {
|
|
1817
|
+
errorLines.unshift(line);
|
|
1818
|
+
capturing = true;
|
|
1819
|
+
}
|
|
1820
|
+
if (line.includes("throw new Error") || line.includes("at new")) {
|
|
1821
|
+
capturing = true;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
if (errorLines.length > 0) {
|
|
1825
|
+
console.log(chalk_1.default.red("\n Error details:"));
|
|
1826
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(60)));
|
|
1827
|
+
// Find the actual error message
|
|
1828
|
+
const errorMsg = errorLines.find(l => l.includes("Error:"));
|
|
1829
|
+
if (errorMsg) {
|
|
1830
|
+
const match = errorMsg.match(/Error:\s*(.+)/);
|
|
1831
|
+
if (match) {
|
|
1832
|
+
console.log(chalk_1.default.red(` ${match[1]}`));
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(60)));
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1585
1838
|
}
|
|
1586
1839
|
}
|
|
1587
|
-
console.log(
|
|
1840
|
+
console.log();
|
|
1588
1841
|
});
|
|
1589
1842
|
// PS command - list running tunnel processes (global)
|
|
1590
1843
|
program
|
|
@@ -1725,6 +1978,104 @@ program
|
|
|
1725
1978
|
console.log(lines.join("\n"));
|
|
1726
1979
|
}
|
|
1727
1980
|
});
|
|
1981
|
+
// Logs clean command - remove log files
|
|
1982
|
+
program
|
|
1983
|
+
.command("logs-clean [name]")
|
|
1984
|
+
.description("Clean log files")
|
|
1985
|
+
.option("--all", "Clean all log files")
|
|
1986
|
+
.action(async (name, options) => {
|
|
1987
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
1988
|
+
const logsDir = getLogsDir();
|
|
1989
|
+
if (!fs.existsSync(logsDir)) {
|
|
1990
|
+
console.log(chalk_1.default.yellow("No logs directory found"));
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
let files;
|
|
1994
|
+
if (options.all) {
|
|
1995
|
+
// Clean all log files
|
|
1996
|
+
files = fs.readdirSync(logsDir).filter(f => f.endsWith(".log"));
|
|
1997
|
+
}
|
|
1998
|
+
else if (name) {
|
|
1999
|
+
// Clean specific instance logs
|
|
2000
|
+
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
2001
|
+
const logFile = `${safeName}.log`;
|
|
2002
|
+
files = fs.existsSync(path.join(logsDir, logFile)) ? [logFile] : [];
|
|
2003
|
+
}
|
|
2004
|
+
else {
|
|
2005
|
+
console.log(chalk_1.default.yellow("Specify instance name or use --all to clean all logs"));
|
|
2006
|
+
console.log(chalk_1.default.gray(` opentunnel logs-clean <name> Clean logs for specific instance`));
|
|
2007
|
+
console.log(chalk_1.default.gray(` opentunnel logs-clean --all Clean all log files`));
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
if (files.length === 0) {
|
|
2011
|
+
console.log(chalk_1.default.yellow(name ? `No logs found for "${name}"` : "No log files found"));
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
let totalSize = 0;
|
|
2015
|
+
for (const file of files) {
|
|
2016
|
+
const filePath = path.join(logsDir, file);
|
|
2017
|
+
try {
|
|
2018
|
+
const stat = fs.statSync(filePath);
|
|
2019
|
+
totalSize += stat.size;
|
|
2020
|
+
fs.unlinkSync(filePath);
|
|
2021
|
+
console.log(chalk_1.default.green(` ✓ Deleted ${file} (${(stat.size / 1024).toFixed(1)} KB)`));
|
|
2022
|
+
}
|
|
2023
|
+
catch (err) {
|
|
2024
|
+
console.log(chalk_1.default.red(` ✗ Failed to delete ${file}: ${err.message}`));
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
console.log(chalk_1.default.gray(`\nCleaned ${files.length} file(s), freed ${(totalSize / 1024).toFixed(1)} KB`));
|
|
2028
|
+
console.log(chalk_1.default.gray(`Logs directory: ${logsDir}`));
|
|
2029
|
+
});
|
|
2030
|
+
// Logs list command - show logs directory and list log files
|
|
2031
|
+
program
|
|
2032
|
+
.command("logs-list")
|
|
2033
|
+
.description("List all log files and show logs directory")
|
|
2034
|
+
.action(async () => {
|
|
2035
|
+
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
2036
|
+
const logsDir = getLogsDir();
|
|
2037
|
+
console.log(chalk_1.default.cyan(`Logs directory: ${logsDir}\n`));
|
|
2038
|
+
if (!fs.existsSync(logsDir)) {
|
|
2039
|
+
console.log(chalk_1.default.yellow("No logs directory found"));
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
const files = fs.readdirSync(logsDir).filter(f => f.endsWith(".log") || f.endsWith(".pid"));
|
|
2043
|
+
if (files.length === 0) {
|
|
2044
|
+
console.log(chalk_1.default.yellow("No log files found"));
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
const logFiles = files.filter(f => f.endsWith(".log"));
|
|
2048
|
+
const pidFiles = files.filter(f => f.endsWith(".pid"));
|
|
2049
|
+
if (logFiles.length > 0) {
|
|
2050
|
+
console.log(chalk_1.default.white("Log files:"));
|
|
2051
|
+
let totalSize = 0;
|
|
2052
|
+
for (const file of logFiles) {
|
|
2053
|
+
const filePath = path.join(logsDir, file);
|
|
2054
|
+
const stat = fs.statSync(filePath);
|
|
2055
|
+
const size = (stat.size / 1024).toFixed(1);
|
|
2056
|
+
const modified = stat.mtime.toLocaleString();
|
|
2057
|
+
totalSize += stat.size;
|
|
2058
|
+
console.log(chalk_1.default.gray(` ${file.padEnd(35)} ${size.padStart(8)} KB ${modified}`));
|
|
2059
|
+
}
|
|
2060
|
+
console.log(chalk_1.default.gray(`\n Total: ${logFiles.length} file(s), ${(totalSize / 1024).toFixed(1)} KB`));
|
|
2061
|
+
}
|
|
2062
|
+
if (pidFiles.length > 0) {
|
|
2063
|
+
console.log(chalk_1.default.white("\nPID files (active instances):"));
|
|
2064
|
+
for (const file of pidFiles) {
|
|
2065
|
+
const filePath = path.join(logsDir, file);
|
|
2066
|
+
const pid = fs.readFileSync(filePath, "utf-8").trim();
|
|
2067
|
+
const name = file.replace(".pid", "");
|
|
2068
|
+
let status = chalk_1.default.green("running");
|
|
2069
|
+
try {
|
|
2070
|
+
process.kill(parseInt(pid), 0);
|
|
2071
|
+
}
|
|
2072
|
+
catch {
|
|
2073
|
+
status = chalk_1.default.red("stale");
|
|
2074
|
+
}
|
|
2075
|
+
console.log(chalk_1.default.gray(` ${name.padEnd(35)} PID: ${pid.padStart(6)} ${status}`));
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
1728
2079
|
// Test server command - simple HTTP server for testing tunnels
|
|
1729
2080
|
program
|
|
1730
2081
|
.command("test-server")
|
|
@@ -1738,8 +2089,8 @@ program
|
|
|
1738
2089
|
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
1739
2090
|
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
1740
2091
|
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1741
|
-
const pidFile =
|
|
1742
|
-
const logFile =
|
|
2092
|
+
const pidFile = getPidFilePath(`test-server-${port}`);
|
|
2093
|
+
const logFile = getLogFilePath(`test-server-${port}`);
|
|
1743
2094
|
if (fs.existsSync(pidFile)) {
|
|
1744
2095
|
const oldPid = fs.readFileSync(pidFile, "utf-8").trim();
|
|
1745
2096
|
try {
|
|
@@ -1827,22 +2178,25 @@ program
|
|
|
1827
2178
|
.option("-p, --port <port>", "Stop specific port")
|
|
1828
2179
|
.action(async (options) => {
|
|
1829
2180
|
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
1830
|
-
const
|
|
2181
|
+
const logsDir = getLogsDir();
|
|
1831
2182
|
let pidFiles;
|
|
1832
2183
|
if (options.port) {
|
|
1833
|
-
const
|
|
1834
|
-
pidFiles = fs.existsSync(
|
|
2184
|
+
const pidPath = getPidFilePath(`test-server-${options.port}`);
|
|
2185
|
+
pidFiles = fs.existsSync(pidPath) ? [pidPath] : [];
|
|
2186
|
+
}
|
|
2187
|
+
else {
|
|
2188
|
+
pidFiles = fs.readdirSync(logsDir)
|
|
2189
|
+
.filter(f => f.startsWith("test-server-") && f.endsWith(".pid"))
|
|
2190
|
+
.map(f => path.join(logsDir, f));
|
|
1835
2191
|
}
|
|
1836
|
-
else
|
|
1837
|
-
pidFiles = fs.readdirSync(process.cwd()).filter(f => f.startsWith(".test-server-") && f.endsWith(".pid"));
|
|
1838
2192
|
if (pidFiles.length === 0) {
|
|
1839
2193
|
console.log(chalk_1.default.yellow("No test servers running"));
|
|
1840
2194
|
return;
|
|
1841
2195
|
}
|
|
1842
|
-
for (const
|
|
1843
|
-
const pidPath = path.join(process.cwd(), pidFile);
|
|
2196
|
+
for (const pidPath of pidFiles) {
|
|
1844
2197
|
const pid = parseInt(fs.readFileSync(pidPath, "utf-8").trim());
|
|
1845
|
-
const
|
|
2198
|
+
const fileName = path.basename(pidPath);
|
|
2199
|
+
const port = fileName.replace("test-server-", "").replace(".pid", "");
|
|
1846
2200
|
try {
|
|
1847
2201
|
process.kill(pid, "SIGTERM");
|
|
1848
2202
|
fs.unlinkSync(pidPath);
|
|
@@ -1860,8 +2214,8 @@ async function runTunnelInBackgroundFromConfig(name, protocol, port, options) {
|
|
|
1860
2214
|
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
1861
2215
|
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
1862
2216
|
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1863
|
-
const pidFile =
|
|
1864
|
-
const logFile =
|
|
2217
|
+
const pidFile = getPidFilePath(name);
|
|
2218
|
+
const logFile = getLogFilePath(name);
|
|
1865
2219
|
// Check if already running
|
|
1866
2220
|
if (fs.existsSync(pidFile)) {
|
|
1867
2221
|
const oldPid = fs.readFileSync(pidFile, "utf-8").trim();
|
|
@@ -1978,8 +2332,8 @@ async function runTunnelInBackground(command, port, options) {
|
|
|
1978
2332
|
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
1979
2333
|
const path = await Promise.resolve().then(() => __importStar(require("path")));
|
|
1980
2334
|
const tunnelId = `tunnel-${port}-${Date.now()}`;
|
|
1981
|
-
const pidFile =
|
|
1982
|
-
const logFile =
|
|
2335
|
+
const pidFile = getPidFilePath(`tunnel-${port}`);
|
|
2336
|
+
const logFile = getLogFilePath(`tunnel-${port}`);
|
|
1983
2337
|
// Check if already running
|
|
1984
2338
|
if (fs.existsSync(pidFile)) {
|
|
1985
2339
|
const oldPid = fs.readFileSync(pidFile, "utf-8").trim();
|