pinggy 0.4.5 → 0.4.7
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 +92 -4
- package/dist/{chunk-HUN2MRZO.js → chunk-3RTRUYNW.js} +3 -1
- package/dist/{chunk-MBN3YBO4.js → chunk-443UO6IY.js} +204 -41
- package/dist/index.cjs +1025 -167
- package/dist/index.d.cts +20 -11
- package/dist/index.d.ts +20 -11
- package/dist/index.js +10 -7
- package/dist/{main-2RDHMQT7.js → main-PUM4SD6B.js} +771 -114
- package/dist/workers/file_serve_worker.js +1 -1
- package/package.json +3 -2
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
RemoteManagementUnauthorizedError,
|
|
3
4
|
TunnelManager,
|
|
4
5
|
TunnelOperations,
|
|
6
|
+
buildRemoteManagementWsUrl,
|
|
5
7
|
closeRemoteManagement,
|
|
6
8
|
getRandomId,
|
|
7
9
|
getRemoteManagementState,
|
|
@@ -9,13 +11,14 @@ import {
|
|
|
9
11
|
initiateRemoteManagement,
|
|
10
12
|
isValidPort,
|
|
11
13
|
parseRemoteManagement,
|
|
12
|
-
printer_default
|
|
13
|
-
|
|
14
|
+
printer_default,
|
|
15
|
+
startRemoteManagement
|
|
16
|
+
} from "./chunk-443UO6IY.js";
|
|
14
17
|
import {
|
|
15
18
|
configureLogger,
|
|
16
19
|
enablePackageLogging,
|
|
17
20
|
logger
|
|
18
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-3RTRUYNW.js";
|
|
19
22
|
|
|
20
23
|
// src/cli/options.ts
|
|
21
24
|
var cliOptions = {
|
|
@@ -44,9 +47,13 @@ var cliOptions = {
|
|
|
44
47
|
vv: { type: "boolean", description: "Enable detailed logging for the Node.js SDK and Libpinggy, including both info and debug level logs." },
|
|
45
48
|
vvv: { type: "boolean", description: "Enable all logs from Cli, SDK and internal components." },
|
|
46
49
|
autoreconnect: { type: "string", short: "a", description: "Automatically reconnect tunnel on failure (enabled by default). Use -a false to disable." },
|
|
47
|
-
// Save and load config
|
|
50
|
+
// Save and load config (legacy file-based)
|
|
48
51
|
saveconf: { type: "string", description: "Create the configuration file based on the options provided here" },
|
|
49
52
|
conf: { type: "string", description: "Use the configuration file as base. Other options will be used to override this file" },
|
|
53
|
+
// Used by `pinggy config save` and `buildAndStartTunnel` save flow
|
|
54
|
+
save: { type: "boolean", short: "s", description: "Save the tunnel config (use with config save or -l)", hidden: true },
|
|
55
|
+
name: { type: "string", description: "Name for the tunnel config", hidden: true },
|
|
56
|
+
auto: { type: "boolean", description: "Mark tunnel config for auto-start", hidden: true },
|
|
50
57
|
// File server
|
|
51
58
|
serve: { type: "string", description: "Start a webserver to serve files from the specified path. Eg --serve /path/to/files" },
|
|
52
59
|
// Remote Control
|
|
@@ -94,11 +101,92 @@ function printHelpMessage() {
|
|
|
94
101
|
console.log(" pinggy -R0:localhost:3000 # Basic HTTP tunnel");
|
|
95
102
|
console.log(" pinggy --type tcp -R0:localhost:22 # TCP tunnel for SSH");
|
|
96
103
|
console.log(" pinggy -R0:localhost:8080 -L4300:localhost:4300 # HTTP tunnel with debugger");
|
|
97
|
-
console.log(" pinggy tcp@ap.example.com -R0:localhost:22 # TCP tunnel to region
|
|
104
|
+
console.log(" pinggy tcp@ap.example.com -R0:localhost:22 # TCP tunnel to region");
|
|
105
|
+
console.log("\nConfig Management:");
|
|
106
|
+
console.log(" pinggy config list # List saved configs");
|
|
107
|
+
console.log(" pinggy config show my-tunnel # Show config details");
|
|
108
|
+
console.log(" pinggy config save my-tunnel -l 3000 token@pro.pinggy.io # Save config");
|
|
109
|
+
console.log(" pinggy config save my-tunnel --auto -l 3000 # Save with auto-start");
|
|
110
|
+
console.log(" pinggy config update my-tunnel -l 4000 # Update saved config");
|
|
111
|
+
console.log(" pinggy config delete my-tunnel # Delete saved config");
|
|
112
|
+
console.log(" pinggy config auto my-tunnel # Enable auto-start");
|
|
113
|
+
console.log(" pinggy config noauto my-tunnel # Disable auto-start");
|
|
114
|
+
console.log("\nStart Saved Tunnels:");
|
|
115
|
+
console.log(" pinggy start my-tunnel # Start saved tunnel");
|
|
116
|
+
console.log(" pinggy start my-tunnel -l 4000 # Start with runtime overrides");
|
|
117
|
+
console.log(" pinggy start tunnela tunnelb # Start multiple tunnels");
|
|
118
|
+
console.log(" pinggy start --all # Start all auto-start tunnels\n");
|
|
98
119
|
}
|
|
99
120
|
|
|
121
|
+
// src/utils/parseArgs.ts
|
|
122
|
+
import { parseArgs } from "util";
|
|
123
|
+
import * as os from "os";
|
|
124
|
+
function isAttachedReverseOrLocalFlag(arg) {
|
|
125
|
+
return /^-[RL].+/.test(arg);
|
|
126
|
+
}
|
|
127
|
+
function shouldMergeReverseOrLocalFragment(current, next) {
|
|
128
|
+
if (next.startsWith("-")) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (next.startsWith(".")) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
const body = current.slice(2);
|
|
135
|
+
if (body.endsWith(":")) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if (body.includes("//") && !body.includes(":")) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
function preprocessWindowsArgs(args) {
|
|
144
|
+
if (os.platform() !== "win32") {
|
|
145
|
+
return args;
|
|
146
|
+
}
|
|
147
|
+
;
|
|
148
|
+
const out = [];
|
|
149
|
+
let i = 0;
|
|
150
|
+
while (i < args.length) {
|
|
151
|
+
const arg = args[i];
|
|
152
|
+
if (isAttachedReverseOrLocalFlag(arg)) {
|
|
153
|
+
let merged = arg;
|
|
154
|
+
while (i + 1 < args.length && shouldMergeReverseOrLocalFragment(merged, args[i + 1])) {
|
|
155
|
+
merged += args[i + 1];
|
|
156
|
+
i++;
|
|
157
|
+
}
|
|
158
|
+
out.push(merged);
|
|
159
|
+
i++;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
out.push(arg);
|
|
163
|
+
i++;
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
function parseCliArgs(options, overrideArgs) {
|
|
168
|
+
const rawArgs = overrideArgs ?? process.argv.slice(2);
|
|
169
|
+
const processedArgs = preprocessWindowsArgs(rawArgs);
|
|
170
|
+
const parsed = parseArgs({
|
|
171
|
+
args: processedArgs,
|
|
172
|
+
options,
|
|
173
|
+
allowPositionals: true
|
|
174
|
+
});
|
|
175
|
+
const hasAnyArgs = parsed.positionals.length > 0 || Object.values(parsed.values).some((v) => v !== void 0 && v !== false);
|
|
176
|
+
return {
|
|
177
|
+
...parsed,
|
|
178
|
+
hasAnyArgs
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/main.ts
|
|
183
|
+
import { fileURLToPath } from "url";
|
|
184
|
+
import { argv } from "process";
|
|
185
|
+
import { realpathSync } from "fs";
|
|
186
|
+
|
|
100
187
|
// src/cli/defaults.ts
|
|
101
188
|
var defaultOptions = {
|
|
189
|
+
version: "1.0",
|
|
102
190
|
token: void 0,
|
|
103
191
|
// No default token
|
|
104
192
|
serverAddress: "a.pinggy.io",
|
|
@@ -264,7 +352,9 @@ function removeIPv6Brackets(ip) {
|
|
|
264
352
|
}
|
|
265
353
|
function isValidServerAddress(host) {
|
|
266
354
|
const normalized = removeIPv6Brackets(host.trim());
|
|
267
|
-
if (!normalized)
|
|
355
|
+
if (!normalized) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
268
358
|
return domainRegex.test(normalized) || isIP2(normalized) !== 0;
|
|
269
359
|
}
|
|
270
360
|
var KEYWORDS = /* @__PURE__ */ new Set([
|
|
@@ -285,7 +375,9 @@ function parseUserAndDomain(str) {
|
|
|
285
375
|
let server;
|
|
286
376
|
let qrCode;
|
|
287
377
|
let forceFlag;
|
|
288
|
-
if (!str)
|
|
378
|
+
if (!str) {
|
|
379
|
+
return { token, type, server, qrCode, forceFlag };
|
|
380
|
+
}
|
|
289
381
|
if (str.includes("@")) {
|
|
290
382
|
const [user, domain] = str.split("@", 2);
|
|
291
383
|
if (isValidServerAddress(domain)) {
|
|
@@ -338,21 +430,39 @@ function parseUsers(positionalArgs, explicitToken) {
|
|
|
338
430
|
let remaining = [...positionalArgs];
|
|
339
431
|
if (typeof explicitToken === "string") {
|
|
340
432
|
const parsed = parseUserAndDomain(explicitToken);
|
|
341
|
-
if (parsed.server)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (parsed.
|
|
345
|
-
|
|
433
|
+
if (parsed.server) {
|
|
434
|
+
server = parsed.server;
|
|
435
|
+
}
|
|
436
|
+
if (parsed.type) {
|
|
437
|
+
type = parsed.type;
|
|
438
|
+
}
|
|
439
|
+
if (parsed.token) {
|
|
440
|
+
token = parsed.token;
|
|
441
|
+
}
|
|
442
|
+
if (parsed.forceFlag) {
|
|
443
|
+
forceFlag = true;
|
|
444
|
+
}
|
|
445
|
+
if (parsed.qrCode) {
|
|
446
|
+
qrCode = true;
|
|
447
|
+
}
|
|
346
448
|
}
|
|
347
449
|
if (remaining.length > 0) {
|
|
348
450
|
const first = remaining[0];
|
|
349
451
|
const parsed = parseUserAndDomain(first);
|
|
350
452
|
if (parsed.server) {
|
|
351
453
|
server = parsed.server;
|
|
352
|
-
if (parsed.type)
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (parsed.
|
|
454
|
+
if (parsed.type) {
|
|
455
|
+
type = parsed.type;
|
|
456
|
+
}
|
|
457
|
+
if (parsed.token) {
|
|
458
|
+
token = parsed.token;
|
|
459
|
+
}
|
|
460
|
+
if (parsed.forceFlag) {
|
|
461
|
+
forceFlag = true;
|
|
462
|
+
}
|
|
463
|
+
if (parsed.qrCode) {
|
|
464
|
+
qrCode = true;
|
|
465
|
+
}
|
|
356
466
|
remaining = remaining.slice(1);
|
|
357
467
|
}
|
|
358
468
|
}
|
|
@@ -365,7 +475,9 @@ function parseType(finalConfig, values, inferredType) {
|
|
|
365
475
|
}
|
|
366
476
|
}
|
|
367
477
|
function parseLocalPort(finalConfig, values) {
|
|
368
|
-
if (typeof values.localport !== "string")
|
|
478
|
+
if (typeof values.localport !== "string") {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
369
481
|
let lp = values.localport.trim();
|
|
370
482
|
let isHttps = false;
|
|
371
483
|
if (lp.startsWith("https://")) {
|
|
@@ -397,7 +509,9 @@ function parseLocalPort(finalConfig, values) {
|
|
|
397
509
|
}
|
|
398
510
|
function isValidHostAddress(host) {
|
|
399
511
|
const normalized = removeIPv6Brackets(host.trim());
|
|
400
|
-
if (normalized.length === 0)
|
|
512
|
+
if (normalized.length === 0) {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
401
515
|
return normalized === "localhost" || isIP2(normalized) !== 0;
|
|
402
516
|
}
|
|
403
517
|
function ipv6SafeSplitColon(s) {
|
|
@@ -441,7 +555,9 @@ function parseDefaultForwarding(forwarding) {
|
|
|
441
555
|
}
|
|
442
556
|
function parseAdditionalForwarding(forwarding) {
|
|
443
557
|
const toPort = (v) => {
|
|
444
|
-
if (!v)
|
|
558
|
+
if (!v) {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
445
561
|
const n = parseInt(v, 10);
|
|
446
562
|
return Number.isNaN(n) ? null : n;
|
|
447
563
|
};
|
|
@@ -520,7 +636,9 @@ function parseReverseTunnelAddr(finalConfig, values, primaryType) {
|
|
|
520
636
|
});
|
|
521
637
|
} else if (slicedForwarding.length === 4) {
|
|
522
638
|
const parsed = parseAdditionalForwarding(forwarding);
|
|
523
|
-
if (parsed instanceof Error)
|
|
639
|
+
if (parsed instanceof Error) {
|
|
640
|
+
return parsed;
|
|
641
|
+
}
|
|
524
642
|
forwardingData.push(parsed);
|
|
525
643
|
} else {
|
|
526
644
|
return new Error(
|
|
@@ -532,7 +650,9 @@ function parseReverseTunnelAddr(finalConfig, values, primaryType) {
|
|
|
532
650
|
return null;
|
|
533
651
|
}
|
|
534
652
|
function parseLocalTunnelAddr(finalConfig, values) {
|
|
535
|
-
if (!Array.isArray(values.L) || values.L.length === 0)
|
|
653
|
+
if (!Array.isArray(values.L) || values.L.length === 0) {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
536
656
|
const firstL = values.L[0];
|
|
537
657
|
const parts = ipv6SafeSplitColon(firstL);
|
|
538
658
|
let debuggerHost = "localhost";
|
|
@@ -556,7 +676,9 @@ function parseLocalTunnelAddr(finalConfig, values) {
|
|
|
556
676
|
}
|
|
557
677
|
function parseDebugger(finalConfig, values) {
|
|
558
678
|
let dbg = values.debugger;
|
|
559
|
-
if (typeof dbg !== "string")
|
|
679
|
+
if (typeof dbg !== "string") {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
560
682
|
dbg = dbg.startsWith(":") ? dbg.slice(1) : dbg;
|
|
561
683
|
const d = parseInt(dbg, 10);
|
|
562
684
|
if (!Number.isNaN(d) && isValidPort(d)) {
|
|
@@ -571,7 +693,7 @@ function parseToken(finalConfig, explicitToken) {
|
|
|
571
693
|
finalConfig.token = explicitToken;
|
|
572
694
|
}
|
|
573
695
|
}
|
|
574
|
-
function
|
|
696
|
+
function parseArgs2(finalConfig, remainingPositionals) {
|
|
575
697
|
let localserverTls = "";
|
|
576
698
|
localserverTls = parseExtendedOptions(remainingPositionals, finalConfig, localserverTls);
|
|
577
699
|
if (localserverTls.length > 0 && finalConfig.forwarding) {
|
|
@@ -582,10 +704,10 @@ function parseArgs(finalConfig, remainingPositionals) {
|
|
|
582
704
|
}
|
|
583
705
|
function storeJson(config, saveconf) {
|
|
584
706
|
if (saveconf) {
|
|
585
|
-
const
|
|
707
|
+
const path4 = saveconf;
|
|
586
708
|
try {
|
|
587
|
-
fs.writeFileSync(
|
|
588
|
-
logger.info(`Configuration saved to ${
|
|
709
|
+
fs.writeFileSync(path4, JSON.stringify(config, null, 2), { encoding: "utf-8", flag: "w" });
|
|
710
|
+
logger.info(`Configuration saved to ${path4}`);
|
|
589
711
|
} catch (err) {
|
|
590
712
|
const msg = err instanceof Error ? err.message : String(err);
|
|
591
713
|
logger.error("Error loading configuration:", msg);
|
|
@@ -615,7 +737,9 @@ function isSaveConfOption(values) {
|
|
|
615
737
|
}
|
|
616
738
|
function parseServe(finalConfig, values) {
|
|
617
739
|
const sv = values.serve;
|
|
618
|
-
if (typeof sv !== "string" || sv.trim().length === 0)
|
|
740
|
+
if (typeof sv !== "string" || sv.trim().length === 0) {
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
619
743
|
finalConfig.optional.serve = sv;
|
|
620
744
|
return null;
|
|
621
745
|
}
|
|
@@ -633,7 +757,7 @@ function parseAutoReconnect(finalConfig, values) {
|
|
|
633
757
|
}
|
|
634
758
|
return null;
|
|
635
759
|
}
|
|
636
|
-
async function buildFinalConfig(values, positionals) {
|
|
760
|
+
async function buildFinalConfig(values, positionals, baseConfig) {
|
|
637
761
|
let token;
|
|
638
762
|
let server;
|
|
639
763
|
let type;
|
|
@@ -641,7 +765,7 @@ async function buildFinalConfig(values, positionals) {
|
|
|
641
765
|
let qrCode = false;
|
|
642
766
|
let finalConfig = new Object();
|
|
643
767
|
let saveconf = isSaveConfOption(values);
|
|
644
|
-
const configFromFile = loadJsonConfig(values);
|
|
768
|
+
const configFromFile = baseConfig || loadJsonConfig(values);
|
|
645
769
|
const userParse = parseUsers(positionals, values.token);
|
|
646
770
|
token = userParse.token;
|
|
647
771
|
server = userParse.server;
|
|
@@ -667,73 +791,37 @@ async function buildFinalConfig(values, positionals) {
|
|
|
667
791
|
type = parseType(finalConfig, values, type);
|
|
668
792
|
parseToken(finalConfig, token || values.token);
|
|
669
793
|
const dbgErr = parseDebugger(finalConfig, values);
|
|
670
|
-
if (dbgErr instanceof Error)
|
|
794
|
+
if (dbgErr instanceof Error) {
|
|
795
|
+
throw dbgErr;
|
|
796
|
+
}
|
|
671
797
|
const lpErr = parseLocalPort(finalConfig, values);
|
|
672
|
-
if (lpErr instanceof Error)
|
|
798
|
+
if (lpErr instanceof Error) {
|
|
799
|
+
throw lpErr;
|
|
800
|
+
}
|
|
673
801
|
const rErr = parseReverseTunnelAddr(finalConfig, values, type);
|
|
674
|
-
if (rErr instanceof Error)
|
|
802
|
+
if (rErr instanceof Error) {
|
|
803
|
+
throw rErr;
|
|
804
|
+
}
|
|
675
805
|
const lErr = parseLocalTunnelAddr(finalConfig, values);
|
|
676
|
-
if (lErr instanceof Error)
|
|
806
|
+
if (lErr instanceof Error) {
|
|
807
|
+
throw lErr;
|
|
808
|
+
}
|
|
677
809
|
const serveErr = parseServe(finalConfig, values);
|
|
678
|
-
if (serveErr instanceof Error)
|
|
810
|
+
if (serveErr instanceof Error) {
|
|
811
|
+
throw serveErr;
|
|
812
|
+
}
|
|
679
813
|
const autoReconnectErr = parseAutoReconnect(finalConfig, values);
|
|
680
|
-
if (autoReconnectErr instanceof Error)
|
|
681
|
-
|
|
682
|
-
|
|
814
|
+
if (autoReconnectErr instanceof Error) {
|
|
815
|
+
throw autoReconnectErr;
|
|
816
|
+
}
|
|
817
|
+
if (forceFlag || values.force) {
|
|
818
|
+
finalConfig.force = true;
|
|
819
|
+
}
|
|
820
|
+
parseArgs2(finalConfig, remainingPositionals);
|
|
683
821
|
storeJson(finalConfig, saveconf);
|
|
684
822
|
return finalConfig;
|
|
685
823
|
}
|
|
686
824
|
|
|
687
|
-
// src/utils/parseArgs.ts
|
|
688
|
-
import { parseArgs as parseArgs2 } from "util";
|
|
689
|
-
import * as os from "os";
|
|
690
|
-
function isAttachedReverseOrLocalFlag(arg) {
|
|
691
|
-
return /^-[RL].+/.test(arg);
|
|
692
|
-
}
|
|
693
|
-
function shouldMergeReverseOrLocalFragment(current, next) {
|
|
694
|
-
if (next.startsWith("-")) return false;
|
|
695
|
-
if (next.startsWith(".")) return true;
|
|
696
|
-
const body = current.slice(2);
|
|
697
|
-
if (body.endsWith(":")) return true;
|
|
698
|
-
if (body.includes("//") && !body.includes(":")) return true;
|
|
699
|
-
return false;
|
|
700
|
-
}
|
|
701
|
-
function preprocessWindowsArgs(args) {
|
|
702
|
-
if (os.platform() !== "win32") return args;
|
|
703
|
-
const out = [];
|
|
704
|
-
let i = 0;
|
|
705
|
-
while (i < args.length) {
|
|
706
|
-
const arg = args[i];
|
|
707
|
-
if (isAttachedReverseOrLocalFlag(arg)) {
|
|
708
|
-
let merged = arg;
|
|
709
|
-
while (i + 1 < args.length && shouldMergeReverseOrLocalFragment(merged, args[i + 1])) {
|
|
710
|
-
merged += args[i + 1];
|
|
711
|
-
i++;
|
|
712
|
-
}
|
|
713
|
-
out.push(merged);
|
|
714
|
-
i++;
|
|
715
|
-
continue;
|
|
716
|
-
}
|
|
717
|
-
out.push(arg);
|
|
718
|
-
i++;
|
|
719
|
-
}
|
|
720
|
-
return out;
|
|
721
|
-
}
|
|
722
|
-
function parseCliArgs(options) {
|
|
723
|
-
const rawArgs = process.argv.slice(2);
|
|
724
|
-
const processedArgs = preprocessWindowsArgs(rawArgs);
|
|
725
|
-
const parsed = parseArgs2({
|
|
726
|
-
args: processedArgs,
|
|
727
|
-
options,
|
|
728
|
-
allowPositionals: true
|
|
729
|
-
});
|
|
730
|
-
const hasAnyArgs = parsed.positionals.length > 0 || Object.values(parsed.values).some((v) => v !== void 0 && v !== false);
|
|
731
|
-
return {
|
|
732
|
-
...parsed,
|
|
733
|
-
hasAnyArgs
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
|
|
737
825
|
// src/utils/getFreePort.ts
|
|
738
826
|
import net from "net";
|
|
739
827
|
function getFreePort(webDebugger) {
|
|
@@ -777,13 +865,12 @@ import QRCode from "qrcode";
|
|
|
777
865
|
async function createQrCodes(urls) {
|
|
778
866
|
const codes = [];
|
|
779
867
|
for (const url of urls) {
|
|
780
|
-
const
|
|
781
|
-
type: "
|
|
782
|
-
|
|
783
|
-
margin: 0,
|
|
868
|
+
const raw = await QRCode.toString(url, {
|
|
869
|
+
type: "utf8",
|
|
870
|
+
margin: 2,
|
|
784
871
|
errorCorrectionLevel: "L"
|
|
785
872
|
});
|
|
786
|
-
codes.push(
|
|
873
|
+
codes.push(raw);
|
|
787
874
|
}
|
|
788
875
|
return codes;
|
|
789
876
|
}
|
|
@@ -795,6 +882,7 @@ import WebSocket from "ws";
|
|
|
795
882
|
var defaultTuiConfig = {
|
|
796
883
|
maxRequestPairs: 100,
|
|
797
884
|
visibleRequestCount: 10,
|
|
885
|
+
visibleUrlCount: 7,
|
|
798
886
|
viewportScrollMargin: 2,
|
|
799
887
|
inactivityHttpSelectorTimeoutMs: 1e4
|
|
800
888
|
};
|
|
@@ -802,6 +890,7 @@ function getTuiConfig() {
|
|
|
802
890
|
return {
|
|
803
891
|
maxRequestPairs: defaultTuiConfig.maxRequestPairs,
|
|
804
892
|
visibleRequestCount: defaultTuiConfig.visibleRequestCount,
|
|
893
|
+
visibleUrlCount: defaultTuiConfig.visibleUrlCount,
|
|
805
894
|
viewportScrollMargin: defaultTuiConfig.viewportScrollMargin,
|
|
806
895
|
inactivityHttpSelectorTimeoutMs: defaultTuiConfig.inactivityHttpSelectorTimeoutMs
|
|
807
896
|
};
|
|
@@ -1029,7 +1118,7 @@ function createFullUI(screen, urls, greet, tunnelConfig) {
|
|
|
1029
1118
|
width: "100%-2",
|
|
1030
1119
|
height: `100%-${lowerSectionTop + 6}`
|
|
1031
1120
|
});
|
|
1032
|
-
const isQrCodeRequested = tunnelConfig?.
|
|
1121
|
+
const isQrCodeRequested = tunnelConfig?.isQRCode || false;
|
|
1033
1122
|
const requestsBox = blessed.box({
|
|
1034
1123
|
parent: lowerSection,
|
|
1035
1124
|
top: 0,
|
|
@@ -1172,8 +1261,24 @@ function getBytesInt(b) {
|
|
|
1172
1261
|
// src/tui/blessed/components/DisplayUpdaters.ts
|
|
1173
1262
|
function updateUrlsDisplay(urlsBox, screen, urls, currentQrIndex) {
|
|
1174
1263
|
if (!urlsBox) return;
|
|
1175
|
-
|
|
1176
|
-
|
|
1264
|
+
const config = getTuiConfig();
|
|
1265
|
+
const { visibleUrlCount } = config;
|
|
1266
|
+
let viewportStart = 0;
|
|
1267
|
+
if (urls.length > visibleUrlCount) {
|
|
1268
|
+
viewportStart = Math.max(0, Math.min(
|
|
1269
|
+
currentQrIndex - Math.floor(visibleUrlCount / 2),
|
|
1270
|
+
urls.length - visibleUrlCount
|
|
1271
|
+
));
|
|
1272
|
+
}
|
|
1273
|
+
const viewportEnd = Math.min(viewportStart + visibleUrlCount, urls.length);
|
|
1274
|
+
const visibleUrls = urls.slice(viewportStart, viewportEnd);
|
|
1275
|
+
let content = "{green-fg}{bold}Public URLs{/bold}{/green-fg}";
|
|
1276
|
+
if (viewportStart > 0) {
|
|
1277
|
+
content += ` {gray-fg}\u2191 ${viewportStart} more{/gray-fg}`;
|
|
1278
|
+
}
|
|
1279
|
+
content += "\n";
|
|
1280
|
+
visibleUrls.forEach((url, i) => {
|
|
1281
|
+
const index = viewportStart + i;
|
|
1177
1282
|
const isSelected = index === currentQrIndex;
|
|
1178
1283
|
const prefix = isSelected ? "\u2192 " : "\u2022 ";
|
|
1179
1284
|
const color = isSelected ? "yellow" : "magenta";
|
|
@@ -1185,6 +1290,11 @@ function updateUrlsDisplay(urlsBox, screen, urls, currentQrIndex) {
|
|
|
1185
1290
|
`;
|
|
1186
1291
|
}
|
|
1187
1292
|
});
|
|
1293
|
+
const itemsBelow = urls.length - viewportEnd;
|
|
1294
|
+
if (itemsBelow > 0) {
|
|
1295
|
+
content += `{gray-fg}\u2193 ${itemsBelow} more{/gray-fg}
|
|
1296
|
+
`;
|
|
1297
|
+
}
|
|
1188
1298
|
urlsBox.setContent(content);
|
|
1189
1299
|
screen.render();
|
|
1190
1300
|
}
|
|
@@ -1852,7 +1962,7 @@ var TunnelTui = class {
|
|
|
1852
1962
|
}
|
|
1853
1963
|
}
|
|
1854
1964
|
async generateQrCodes() {
|
|
1855
|
-
if (this.tunnelConfig?.
|
|
1965
|
+
if (this.tunnelConfig?.isQRCode && this.urls.length > 0) {
|
|
1856
1966
|
this.qrCodes = await createQrCodes(this.urls);
|
|
1857
1967
|
this.updateQrCodeDisplay();
|
|
1858
1968
|
}
|
|
@@ -2085,7 +2195,7 @@ async function startCli(finalConfig, manager) {
|
|
|
2085
2195
|
});
|
|
2086
2196
|
}
|
|
2087
2197
|
manager2.registerWorkerErrorListner(tunnel.tunnelid, (_tunnelid, error) => {
|
|
2088
|
-
printer_default.
|
|
2198
|
+
printer_default.fatal(`${error.message}`);
|
|
2089
2199
|
});
|
|
2090
2200
|
await manager2.startTunnel(tunnel.tunnelid);
|
|
2091
2201
|
printer_default.stopSpinnerSuccess(" Connected to Pinggy");
|
|
@@ -2210,19 +2320,568 @@ async function startCli(finalConfig, manager) {
|
|
|
2210
2320
|
}
|
|
2211
2321
|
} catch (err) {
|
|
2212
2322
|
printer_default.stopSpinnerFail("Failed to connect");
|
|
2213
|
-
printer_default.
|
|
2323
|
+
printer_default.fatal(err.message || "Unknown error");
|
|
2214
2324
|
throw err;
|
|
2215
2325
|
}
|
|
2216
2326
|
}
|
|
2217
2327
|
|
|
2328
|
+
// src/cli/configStore.ts
|
|
2329
|
+
import fs3 from "fs";
|
|
2330
|
+
import path3 from "path";
|
|
2331
|
+
|
|
2332
|
+
// src/utils/configDir.ts
|
|
2333
|
+
import os2 from "os";
|
|
2334
|
+
import path2 from "path";
|
|
2335
|
+
import fs2 from "fs";
|
|
2336
|
+
function getPinggyConfigDir() {
|
|
2337
|
+
const platform2 = os2.platform();
|
|
2338
|
+
let baseDir;
|
|
2339
|
+
if (platform2 === "win32") {
|
|
2340
|
+
baseDir = process.env.APPDATA || path2.join(os2.homedir(), "AppData", "Roaming");
|
|
2341
|
+
} else {
|
|
2342
|
+
baseDir = process.env.XDG_CONFIG_HOME || path2.join(os2.homedir(), ".config");
|
|
2343
|
+
}
|
|
2344
|
+
return path2.join(baseDir, "pinggy");
|
|
2345
|
+
}
|
|
2346
|
+
function getTunnelConfigDir() {
|
|
2347
|
+
return path2.join(getPinggyConfigDir(), "tunnels");
|
|
2348
|
+
}
|
|
2349
|
+
function ensureTunnelConfigDir() {
|
|
2350
|
+
const dir = getTunnelConfigDir();
|
|
2351
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
2352
|
+
return dir;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
// src/cli/configStore.ts
|
|
2356
|
+
import pico2 from "picocolors";
|
|
2357
|
+
function buildFilename(name, configId) {
|
|
2358
|
+
return `${name}_${configId}.json`;
|
|
2359
|
+
}
|
|
2360
|
+
function sanitizeName(name) {
|
|
2361
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
2362
|
+
}
|
|
2363
|
+
function validateName(name) {
|
|
2364
|
+
if (!name || name.trim().length === 0) {
|
|
2365
|
+
return new Error("Tunnel name cannot be empty.");
|
|
2366
|
+
}
|
|
2367
|
+
if (name.length > 128) {
|
|
2368
|
+
return new Error("Tunnel name cannot exceed 128 characters.");
|
|
2369
|
+
}
|
|
2370
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
2371
|
+
return new Error("Tunnel name can only contain alphanumeric characters, hyphens, and underscores.");
|
|
2372
|
+
}
|
|
2373
|
+
return null;
|
|
2374
|
+
}
|
|
2375
|
+
function readConfigFile(filePath) {
|
|
2376
|
+
try {
|
|
2377
|
+
const data = fs3.readFileSync(filePath, { encoding: "utf-8" });
|
|
2378
|
+
return JSON.parse(data);
|
|
2379
|
+
} catch (err) {
|
|
2380
|
+
logger.warn(`Failed to read config file ${filePath}:`, err);
|
|
2381
|
+
return null;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
function writeConfigFile(filePath, config) {
|
|
2385
|
+
fs3.writeFileSync(filePath, JSON.stringify(config, null, 2), { encoding: "utf-8" });
|
|
2386
|
+
}
|
|
2387
|
+
function listSavedConfigs() {
|
|
2388
|
+
const dir = getTunnelConfigDir();
|
|
2389
|
+
if (!fs3.existsSync(dir)) {
|
|
2390
|
+
return [];
|
|
2391
|
+
}
|
|
2392
|
+
const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
2393
|
+
const configs = [];
|
|
2394
|
+
for (const file of files) {
|
|
2395
|
+
const config = readConfigFile(path3.join(dir, file));
|
|
2396
|
+
if (config && config.name && config.configId) {
|
|
2397
|
+
configs.push(config);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
return configs;
|
|
2401
|
+
}
|
|
2402
|
+
function findConfigFile(nameOrId) {
|
|
2403
|
+
const dir = getTunnelConfigDir();
|
|
2404
|
+
if (!fs3.existsSync(dir)) return null;
|
|
2405
|
+
const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
2406
|
+
const sanitized = sanitizeName(nameOrId);
|
|
2407
|
+
const nameMatch = files.find((f) => f.startsWith(sanitized + "_"));
|
|
2408
|
+
if (nameMatch) {
|
|
2409
|
+
const filePath = path3.join(dir, nameMatch);
|
|
2410
|
+
const config = readConfigFile(filePath);
|
|
2411
|
+
if (config && config.name === nameOrId) return { filePath, config };
|
|
2412
|
+
}
|
|
2413
|
+
const idCandidates = files.filter((f) => {
|
|
2414
|
+
const withoutExt = f.replace(/\.json$/, "");
|
|
2415
|
+
const lastUnderscore = withoutExt.indexOf("_");
|
|
2416
|
+
if (lastUnderscore === -1) return false;
|
|
2417
|
+
const idPart = withoutExt.slice(lastUnderscore + 1);
|
|
2418
|
+
return idPart.startsWith(nameOrId);
|
|
2419
|
+
});
|
|
2420
|
+
if (idCandidates.length === 1) {
|
|
2421
|
+
const filePath = path3.join(dir, idCandidates[0]);
|
|
2422
|
+
const config = readConfigFile(filePath);
|
|
2423
|
+
if (config) return { filePath, config };
|
|
2424
|
+
}
|
|
2425
|
+
return null;
|
|
2426
|
+
}
|
|
2427
|
+
function findConfigByName(name) {
|
|
2428
|
+
const resolved = findConfigFile(name);
|
|
2429
|
+
return resolved?.config.name === name ? resolved.config : null;
|
|
2430
|
+
}
|
|
2431
|
+
function findConfig(nameOrId) {
|
|
2432
|
+
return findConfigFile(nameOrId)?.config ?? null;
|
|
2433
|
+
}
|
|
2434
|
+
function saveConfig(name, configId, tunnelConfig, autoStart = false) {
|
|
2435
|
+
const nameErr = validateName(name);
|
|
2436
|
+
if (nameErr) {
|
|
2437
|
+
throw nameErr;
|
|
2438
|
+
}
|
|
2439
|
+
const existing = findConfigByName(name);
|
|
2440
|
+
if (existing) {
|
|
2441
|
+
throw new Error(
|
|
2442
|
+
`A tunnel config with the name "${name}" already exists (configId: ${existing.configId}). Please use a different name.`
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2445
|
+
const dir = ensureTunnelConfigDir();
|
|
2446
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2447
|
+
const saved = {
|
|
2448
|
+
name,
|
|
2449
|
+
configId,
|
|
2450
|
+
autoStart,
|
|
2451
|
+
createdAt: now,
|
|
2452
|
+
updatedAt: now,
|
|
2453
|
+
tunnelConfig
|
|
2454
|
+
};
|
|
2455
|
+
const filename = buildFilename(sanitizeName(name), configId);
|
|
2456
|
+
const filePath = path3.join(dir, filename);
|
|
2457
|
+
fs3.writeFileSync(filePath, JSON.stringify(saved, null, 2), { encoding: "utf-8" });
|
|
2458
|
+
logger.info(`Config "${name}" saved to ${filePath}`);
|
|
2459
|
+
return saved;
|
|
2460
|
+
}
|
|
2461
|
+
function deleteConfig(nameOrId) {
|
|
2462
|
+
const resolved = findConfigFile(nameOrId);
|
|
2463
|
+
if (!resolved) return null;
|
|
2464
|
+
fs3.unlinkSync(resolved.filePath);
|
|
2465
|
+
logger.info(`Config "${resolved.config.name}" deleted.`);
|
|
2466
|
+
return resolved.config.name;
|
|
2467
|
+
}
|
|
2468
|
+
function updateConfigAutoStart(nameOrId, autoStart) {
|
|
2469
|
+
const resolved = findConfigFile(nameOrId);
|
|
2470
|
+
if (!resolved) return null;
|
|
2471
|
+
resolved.config.autoStart = autoStart;
|
|
2472
|
+
resolved.config.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2473
|
+
writeConfigFile(resolved.filePath, resolved.config);
|
|
2474
|
+
logger.info(`Config "${resolved.config.name}" auto-start set to ${autoStart}`);
|
|
2475
|
+
return resolved.config;
|
|
2476
|
+
}
|
|
2477
|
+
function updateTunnelConfig(nameOrId, tunnelConfig) {
|
|
2478
|
+
const resolved = findConfigFile(nameOrId);
|
|
2479
|
+
if (!resolved) return null;
|
|
2480
|
+
resolved.config.tunnelConfig = tunnelConfig;
|
|
2481
|
+
resolved.config.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2482
|
+
writeConfigFile(resolved.filePath, resolved.config);
|
|
2483
|
+
logger.info(`Config "${resolved.config.name}" tunnel configuration updated`);
|
|
2484
|
+
return resolved.config;
|
|
2485
|
+
}
|
|
2486
|
+
function getAutoStartConfigs() {
|
|
2487
|
+
return listSavedConfigs().filter((c) => c.autoStart);
|
|
2488
|
+
}
|
|
2489
|
+
function printConfigList() {
|
|
2490
|
+
const configs = listSavedConfigs();
|
|
2491
|
+
if (configs.length === 0) {
|
|
2492
|
+
console.log(pico2.yellow("No saved tunnel configs found."));
|
|
2493
|
+
console.log(pico2.gray(`Config directory: ${getTunnelConfigDir()}`));
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
const nameW = 20;
|
|
2497
|
+
const idW = 12;
|
|
2498
|
+
const typeW = 8;
|
|
2499
|
+
const fwdW = 25;
|
|
2500
|
+
const serverW = 22;
|
|
2501
|
+
const autoW = 10;
|
|
2502
|
+
const header = pico2.bold("Name".padEnd(nameW)) + pico2.bold("Config ID".padEnd(idW)) + pico2.bold("Type".padEnd(typeW)) + pico2.bold("Forwarding".padEnd(fwdW)) + pico2.bold("Server".padEnd(serverW)) + pico2.bold("Auto-start".padEnd(autoW));
|
|
2503
|
+
console.log("\n" + header);
|
|
2504
|
+
console.log(pico2.gray("\u2500".repeat(nameW + idW + typeW + fwdW + serverW + autoW)));
|
|
2505
|
+
for (const c of configs) {
|
|
2506
|
+
const tc = c.tunnelConfig;
|
|
2507
|
+
const forwarding = Array.isArray(tc.forwarding) ? tc.forwarding[0]?.address : String(tc.forwarding || "");
|
|
2508
|
+
const type = (Array.isArray(tc.forwarding) ? tc.forwarding[0]?.type : void 0) || "http";
|
|
2509
|
+
const server = tc.serverAddress || "a.pinggy.io";
|
|
2510
|
+
const line = pico2.cyanBright(c.name.padEnd(nameW)) + pico2.gray(c.configId.slice(0, 8).padEnd(idW)) + type.padEnd(typeW) + forwarding.slice(0, fwdW - 2).padEnd(fwdW) + server.slice(0, serverW - 2).padEnd(serverW) + (c.autoStart ? pico2.green("yes") : pico2.gray("no")).padEnd(autoW);
|
|
2511
|
+
console.log(line);
|
|
2512
|
+
}
|
|
2513
|
+
console.log();
|
|
2514
|
+
}
|
|
2515
|
+
function printConfigDetail(config) {
|
|
2516
|
+
console.log(pico2.bold(`
|
|
2517
|
+
Tunnel Config: ${pico2.cyanBright(config.name)}`));
|
|
2518
|
+
console.log(pico2.gray("\u2500".repeat(40)));
|
|
2519
|
+
console.log(` Config ID: ${config.configId}`);
|
|
2520
|
+
console.log(` Auto-start: ${config.autoStart ? pico2.green("yes") : pico2.gray("no")}`);
|
|
2521
|
+
console.log(` Created: ${config.createdAt}`);
|
|
2522
|
+
console.log(` Updated: ${config.updatedAt}`);
|
|
2523
|
+
console.log(pico2.gray("\u2500".repeat(40)));
|
|
2524
|
+
console.log(` Server: ${config.tunnelConfig.serverAddress || "a.pinggy.io"}`);
|
|
2525
|
+
console.log(` Token: ${config.tunnelConfig.token ? "***" + config.tunnelConfig.token.slice(-4) : "(none)"}`);
|
|
2526
|
+
const fwd = config.tunnelConfig.forwarding;
|
|
2527
|
+
if (Array.isArray(fwd)) {
|
|
2528
|
+
const defaultFwds = [];
|
|
2529
|
+
const customFwds = [];
|
|
2530
|
+
for (const f of fwd) {
|
|
2531
|
+
if (typeof f === "string") {
|
|
2532
|
+
defaultFwds.push(f);
|
|
2533
|
+
} else if (f.listenAddress) {
|
|
2534
|
+
customFwds.push(f);
|
|
2535
|
+
} else {
|
|
2536
|
+
defaultFwds.push(f);
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
for (const f of defaultFwds) {
|
|
2540
|
+
const addr = typeof f === "string" ? f : `${f.address} (${f.type || "http"})`;
|
|
2541
|
+
console.log(` Forwarding: ${addr}`);
|
|
2542
|
+
if (config.tunnelConfig.webDebugger) {
|
|
2543
|
+
console.log(` Debugger: ${config.tunnelConfig.webDebugger}`);
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
if (customFwds.length > 0) {
|
|
2547
|
+
console.log(pico2.gray("\u2500".repeat(40)));
|
|
2548
|
+
console.log(pico2.bold(" Domain Mappings:"));
|
|
2549
|
+
for (const f of customFwds) {
|
|
2550
|
+
if (typeof f === "string") continue;
|
|
2551
|
+
const domain = f.listenAddress;
|
|
2552
|
+
const target = f.address;
|
|
2553
|
+
const type = f.type || "http";
|
|
2554
|
+
console.log(` ${pico2.cyanBright(domain)} \u2192 ${target} (${type})`);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
} else if (fwd) {
|
|
2558
|
+
console.log(` Forwarding: ${fwd}`);
|
|
2559
|
+
}
|
|
2560
|
+
console.log();
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
// src/cli/buildAndStartTunnel.ts
|
|
2564
|
+
async function buildAndStartTunnel(values, positionals, manager) {
|
|
2565
|
+
await initRemoteManagement(values);
|
|
2566
|
+
logger.debug("Building final config from CLI values and positionals", { values, positionals });
|
|
2567
|
+
const finalConfig = await buildFinalConfig(values, positionals);
|
|
2568
|
+
logger.debug("Final configuration built", finalConfig);
|
|
2569
|
+
if (values.save) {
|
|
2570
|
+
const name = values.name;
|
|
2571
|
+
if (!name) {
|
|
2572
|
+
printer_default.error("--save requires --name to specify a name for the tunnel config.");
|
|
2573
|
+
process.exit(1);
|
|
2574
|
+
}
|
|
2575
|
+
const nameErr = validateName(name);
|
|
2576
|
+
if (nameErr) {
|
|
2577
|
+
printer_default.error(nameErr.message);
|
|
2578
|
+
process.exit(1);
|
|
2579
|
+
}
|
|
2580
|
+
const autoStart = !!values.auto;
|
|
2581
|
+
saveConfig(name, finalConfig.configId, finalConfig, autoStart);
|
|
2582
|
+
printer_default.success(`Config "${name}" saved.`);
|
|
2583
|
+
}
|
|
2584
|
+
await startCli(finalConfig, manager);
|
|
2585
|
+
}
|
|
2586
|
+
async function initRemoteManagement(values) {
|
|
2587
|
+
const parseResult = await parseRemoteManagement(values);
|
|
2588
|
+
if (parseResult?.ok === false) {
|
|
2589
|
+
logger.error("Failed to initiate remote management:", parseResult.error);
|
|
2590
|
+
printer_default.fatal(parseResult.error);
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
// src/cli/subcommands.ts
|
|
2595
|
+
import pico3 from "picocolors";
|
|
2596
|
+
var SUBCOMMANDS = /* @__PURE__ */ new Set(["config", "start"]);
|
|
2597
|
+
function isSubcommand(rawArgs) {
|
|
2598
|
+
return rawArgs.length > 0 && SUBCOMMANDS.has(rawArgs[0]);
|
|
2599
|
+
}
|
|
2600
|
+
async function handleSubcommand(rawArgs, manager) {
|
|
2601
|
+
const sub = rawArgs[0];
|
|
2602
|
+
const rest = rawArgs.slice(1);
|
|
2603
|
+
switch (sub) {
|
|
2604
|
+
case "config":
|
|
2605
|
+
await handleConfig(rest);
|
|
2606
|
+
return;
|
|
2607
|
+
case "start":
|
|
2608
|
+
await handleStart(rest, manager);
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
async function handleConfig(args) {
|
|
2613
|
+
if (args.length === 0) {
|
|
2614
|
+
printConfigHelp();
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
const verb = args[0];
|
|
2618
|
+
const rest = args.slice(1);
|
|
2619
|
+
switch (verb) {
|
|
2620
|
+
case "list":
|
|
2621
|
+
case "ls":
|
|
2622
|
+
printConfigList();
|
|
2623
|
+
return;
|
|
2624
|
+
case "show": {
|
|
2625
|
+
const names = requireNames(rest, "config show");
|
|
2626
|
+
for (const name of names) {
|
|
2627
|
+
const saved2 = resolveConfig(name);
|
|
2628
|
+
if (saved2) printConfigDetail(saved2);
|
|
2629
|
+
}
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
case "save": {
|
|
2633
|
+
const name = requireName(rest, "config save");
|
|
2634
|
+
await handleConfigSave(name, rest.slice(1));
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
case "delete": {
|
|
2638
|
+
const names = requireNames(rest, "config delete");
|
|
2639
|
+
for (const name of names) {
|
|
2640
|
+
const deletedName = deleteConfig(name);
|
|
2641
|
+
if (deletedName) {
|
|
2642
|
+
printer_default.success(`Config "${deletedName}" deleted.`);
|
|
2643
|
+
} else {
|
|
2644
|
+
printer_default.error(`No config found matching "${name}". Use: pinggy config list`);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
2649
|
+
case "update": {
|
|
2650
|
+
const name = requireName(rest, "config update");
|
|
2651
|
+
await handleConfigUpdate(name, rest.slice(1));
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
case "auto": {
|
|
2655
|
+
const names = requireNames(rest, "config auto");
|
|
2656
|
+
for (const name of names) {
|
|
2657
|
+
const updated = updateConfigAutoStart(name, true);
|
|
2658
|
+
if (updated) {
|
|
2659
|
+
printer_default.success(`Config "${updated.name}" auto-start set to on.`);
|
|
2660
|
+
} else {
|
|
2661
|
+
printer_default.error(`No config found matching "${name}". Use: pinggy config list`);
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
case "noauto": {
|
|
2667
|
+
const names = requireNames(rest, "config noauto");
|
|
2668
|
+
for (const name of names) {
|
|
2669
|
+
const updated = updateConfigAutoStart(name, false);
|
|
2670
|
+
if (updated) {
|
|
2671
|
+
printer_default.success(`Config "${updated.name}" auto-start set to off.`);
|
|
2672
|
+
} else {
|
|
2673
|
+
printer_default.error(`No config found matching "${name}". Use: pinggy config list`);
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
default:
|
|
2679
|
+
const saved = resolveConfig(verb);
|
|
2680
|
+
if (saved) printConfigDetail(saved);
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
async function handleConfigSave(name, remainingArgs) {
|
|
2685
|
+
const nameErr = validateName(name);
|
|
2686
|
+
if (nameErr) {
|
|
2687
|
+
printer_default.error(nameErr.message);
|
|
2688
|
+
process.exit(1);
|
|
2689
|
+
}
|
|
2690
|
+
const { values, positionals } = parseCliArgs(cliOptions, remainingArgs);
|
|
2691
|
+
const autoStart = !!values.auto;
|
|
2692
|
+
logger.debug("Building config for save", { name, values, positionals });
|
|
2693
|
+
const finalConfig = await buildFinalConfig(values, positionals);
|
|
2694
|
+
saveConfig(name, finalConfig.configId, finalConfig, autoStart);
|
|
2695
|
+
printer_default.success(`Config "${name}" saved.`);
|
|
2696
|
+
}
|
|
2697
|
+
async function handleConfigUpdate(nameOrId, remainingArgs) {
|
|
2698
|
+
const saved = resolveConfig(nameOrId);
|
|
2699
|
+
if (!saved) return;
|
|
2700
|
+
const { values, positionals } = parseCliArgs(cliOptions, remainingArgs);
|
|
2701
|
+
logger.debug("Building updated config", { nameOrId, values, positionals });
|
|
2702
|
+
const updatedConfig = await buildFinalConfig(values, positionals, saved.tunnelConfig);
|
|
2703
|
+
const result = updateTunnelConfig(nameOrId, updatedConfig);
|
|
2704
|
+
if (result) {
|
|
2705
|
+
printer_default.success(`Config "${result.name}" updated.`);
|
|
2706
|
+
printConfigDetail(result);
|
|
2707
|
+
} else {
|
|
2708
|
+
printer_default.error(`Failed to update config "${nameOrId}".`);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
async function handleStart(args, manager) {
|
|
2712
|
+
const startAll = args.includes("--all");
|
|
2713
|
+
const argsWithoutAll = args.filter((a) => a !== "--all");
|
|
2714
|
+
const names = [];
|
|
2715
|
+
let i = 0;
|
|
2716
|
+
while (i < argsWithoutAll.length && !argsWithoutAll[i].startsWith("-")) {
|
|
2717
|
+
names.push(argsWithoutAll[i]);
|
|
2718
|
+
i++;
|
|
2719
|
+
}
|
|
2720
|
+
const flagArgs = argsWithoutAll.slice(i);
|
|
2721
|
+
const { values, positionals } = parseCliArgs(cliOptions, flagArgs);
|
|
2722
|
+
configureLogger(values);
|
|
2723
|
+
if (startAll) {
|
|
2724
|
+
await initRemoteManagementBackground(values);
|
|
2725
|
+
await startAutoStartTunnels(manager);
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
if (names.length === 0) {
|
|
2729
|
+
printStartHelp();
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
const resolved = [];
|
|
2733
|
+
for (const name of names) {
|
|
2734
|
+
const saved = resolveConfig(name);
|
|
2735
|
+
if (!saved) return;
|
|
2736
|
+
resolved.push(saved);
|
|
2737
|
+
}
|
|
2738
|
+
if (resolved.length > 1 && flagArgs.length > 0) {
|
|
2739
|
+
printer_default.error("Runtime overrides (-l, --type, etc.) can only be used when starting a single tunnel.");
|
|
2740
|
+
printer_default.print(" Start one tunnel: pinggy start my-tunnel -l 4000");
|
|
2741
|
+
printer_default.print(" Or update first: pinggy config update my-tunnel -l 4000");
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
await initRemoteManagementBackground(values);
|
|
2745
|
+
if (resolved.length === 1) {
|
|
2746
|
+
const saved = resolved[0];
|
|
2747
|
+
logger.debug("Building config with overrides", { name: saved.name });
|
|
2748
|
+
const finalConfig = await buildFinalConfig(values, positionals, saved.tunnelConfig);
|
|
2749
|
+
finalConfig.configId = saved.configId;
|
|
2750
|
+
await startCli(finalConfig, manager);
|
|
2751
|
+
} else {
|
|
2752
|
+
await startNamedTunnels(resolved, manager);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
async function startAutoStartTunnels(manager) {
|
|
2756
|
+
const configs = getAutoStartConfigs();
|
|
2757
|
+
if (configs.length === 0) {
|
|
2758
|
+
printer_default.warn("No configs marked for auto-start. Use: pinggy config auto <name>");
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
printer_default.print(pico3.cyanBright(`Starting ${configs.length} auto-start tunnel(s)...`));
|
|
2762
|
+
for (const saved of configs) {
|
|
2763
|
+
await startSavedTunnel(saved, manager);
|
|
2764
|
+
}
|
|
2765
|
+
printer_default.print(pico3.gray("\nAll auto-start tunnels launched. Press Ctrl+C to stop.\n"));
|
|
2766
|
+
await new Promise(() => {
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
async function startNamedTunnels(configs, manager) {
|
|
2770
|
+
printer_default.print(pico3.cyanBright(`Starting ${configs.length} tunnel(s)...`));
|
|
2771
|
+
for (const saved of configs) {
|
|
2772
|
+
await startSavedTunnel(saved, manager);
|
|
2773
|
+
}
|
|
2774
|
+
printer_default.print(pico3.gray("\nAll tunnels launched. Press Ctrl+C to stop.\n"));
|
|
2775
|
+
await new Promise(() => {
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
async function startSavedTunnel(saved, manager) {
|
|
2779
|
+
const config = {
|
|
2780
|
+
...saved.tunnelConfig,
|
|
2781
|
+
configId: saved.configId,
|
|
2782
|
+
name: saved.name,
|
|
2783
|
+
optional: {
|
|
2784
|
+
...saved.tunnelConfig.optional,
|
|
2785
|
+
noTui: true
|
|
2786
|
+
}
|
|
2787
|
+
};
|
|
2788
|
+
try {
|
|
2789
|
+
const tunnel = await manager.createTunnel(config);
|
|
2790
|
+
await manager.startTunnel(tunnel.tunnelid);
|
|
2791
|
+
const urls = await manager.getTunnelUrls(tunnel.tunnelid);
|
|
2792
|
+
printer_default.success(`"${saved.name}" started`);
|
|
2793
|
+
(urls ?? []).forEach(
|
|
2794
|
+
(url) => printer_default.print(" " + pico3.magentaBright(url))
|
|
2795
|
+
);
|
|
2796
|
+
manager.registerWorkerErrorListner(tunnel.tunnelid, (_id, error) => {
|
|
2797
|
+
printer_default.error(`[${saved.name}] Fatal: ${error.message}`);
|
|
2798
|
+
});
|
|
2799
|
+
manager.registerDisconnectListener(tunnel.tunnelid, async (_id, error, messages) => {
|
|
2800
|
+
if (error) printer_default.warn(`[${saved.name}] Disconnected: ${error}`);
|
|
2801
|
+
messages?.forEach((m) => printer_default.warn(`[${saved.name}] ${m}`));
|
|
2802
|
+
});
|
|
2803
|
+
manager.registerReconnectingListener(tunnel.tunnelid, (_id, retryCnt) => {
|
|
2804
|
+
printer_default.print(pico3.gray(`[${saved.name}] Reconnecting (attempt #${retryCnt})...`));
|
|
2805
|
+
});
|
|
2806
|
+
manager.registerReconnectionCompletedListener(tunnel.tunnelid, async (_id, urls2) => {
|
|
2807
|
+
printer_default.success(`[${saved.name}] Reconnected`);
|
|
2808
|
+
(urls2 ?? []).forEach(
|
|
2809
|
+
(url) => printer_default.print(" " + pico3.magentaBright(url))
|
|
2810
|
+
);
|
|
2811
|
+
});
|
|
2812
|
+
manager.registerReconnectionFailedListener(tunnel.tunnelid, (_id, retryCnt) => {
|
|
2813
|
+
printer_default.error(`[${saved.name}] Reconnection failed after ${retryCnt} attempts`);
|
|
2814
|
+
});
|
|
2815
|
+
} catch (err) {
|
|
2816
|
+
printer_default.error(`[${saved.name}] Failed to start: ${err.message || err}`);
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
function resolveConfig(nameOrId) {
|
|
2820
|
+
const saved = findConfig(nameOrId);
|
|
2821
|
+
if (!saved) {
|
|
2822
|
+
printer_default.error(`No config found matching "${nameOrId}". Use: pinggy config list`);
|
|
2823
|
+
return null;
|
|
2824
|
+
}
|
|
2825
|
+
return saved;
|
|
2826
|
+
}
|
|
2827
|
+
function requireName(args, command) {
|
|
2828
|
+
if (args.length === 0 || args[0].startsWith("-")) {
|
|
2829
|
+
printer_default.error(`Tunnel name is required. Usage: pinggy ${command} <name>`);
|
|
2830
|
+
process.exit(1);
|
|
2831
|
+
}
|
|
2832
|
+
return args[0];
|
|
2833
|
+
}
|
|
2834
|
+
function requireNames(args, command) {
|
|
2835
|
+
const names = [];
|
|
2836
|
+
for (const arg of args) {
|
|
2837
|
+
if (arg.startsWith("-")) break;
|
|
2838
|
+
names.push(arg);
|
|
2839
|
+
}
|
|
2840
|
+
if (names.length === 0) {
|
|
2841
|
+
printer_default.error(`At least one tunnel name is required. Usage: pinggy ${command} <name> [name2 ...]`);
|
|
2842
|
+
process.exit(1);
|
|
2843
|
+
}
|
|
2844
|
+
return names;
|
|
2845
|
+
}
|
|
2846
|
+
async function initRemoteManagementBackground(values) {
|
|
2847
|
+
const rmToken = values["remote-management"];
|
|
2848
|
+
if (typeof rmToken === "string" && rmToken.trim().length > 0) {
|
|
2849
|
+
const manageHost = values["manage"];
|
|
2850
|
+
try {
|
|
2851
|
+
await startRemoteManagement({
|
|
2852
|
+
apiKey: rmToken,
|
|
2853
|
+
serverUrl: buildRemoteManagementWsUrl(manageHost)
|
|
2854
|
+
});
|
|
2855
|
+
} catch (e) {
|
|
2856
|
+
logger.error("Failed to initiate remote management:", e);
|
|
2857
|
+
printer_default.fatal(e);
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
function printConfigHelp() {
|
|
2862
|
+
console.log("\nUsage: pinggy config <command> [name] [options]\n");
|
|
2863
|
+
console.log("Commands:");
|
|
2864
|
+
console.log(" list List all saved configs");
|
|
2865
|
+
console.log(" show <name> Show config details");
|
|
2866
|
+
console.log(" save <name> [tunnel flags] Save a tunnel config");
|
|
2867
|
+
console.log(" update <name> [tunnel flags] Update a saved config");
|
|
2868
|
+
console.log(" delete <name> Delete a saved config");
|
|
2869
|
+
console.log(" auto <name> Enable auto-start");
|
|
2870
|
+
console.log(" noauto <name> Disable auto-start\n");
|
|
2871
|
+
}
|
|
2872
|
+
function printStartHelp() {
|
|
2873
|
+
console.log("\nUsage: pinggy start <name> [options]\n");
|
|
2874
|
+
console.log("Examples:");
|
|
2875
|
+
console.log(" pinggy start my-tunnel Start a saved tunnel");
|
|
2876
|
+
console.log(" pinggy start my-tunnel -l 4000 Start with override");
|
|
2877
|
+
console.log(" pinggy start tunnela tunnelb Start multiple tunnels");
|
|
2878
|
+
console.log(" pinggy start --all Start all auto-start tunnels\n");
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2218
2881
|
// src/main.ts
|
|
2219
|
-
import { fileURLToPath } from "url";
|
|
2220
|
-
import { argv } from "process";
|
|
2221
|
-
import { realpathSync } from "fs";
|
|
2222
2882
|
async function main() {
|
|
2223
2883
|
try {
|
|
2224
|
-
const
|
|
2225
|
-
configureLogger(values);
|
|
2884
|
+
const rawArgs = process.argv.slice(2);
|
|
2226
2885
|
const manager = TunnelManager.getInstance();
|
|
2227
2886
|
process.on("SIGINT", () => {
|
|
2228
2887
|
logger.info("SIGINT received: stopping tunnels and exiting");
|
|
@@ -2231,6 +2890,12 @@ async function main() {
|
|
|
2231
2890
|
console.log("Tunnels stopped. Exiting.");
|
|
2232
2891
|
process.exit(0);
|
|
2233
2892
|
});
|
|
2893
|
+
if (isSubcommand(rawArgs)) {
|
|
2894
|
+
await handleSubcommand(rawArgs, manager);
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
const { values, positionals, hasAnyArgs } = parseCliArgs(cliOptions);
|
|
2898
|
+
configureLogger(values);
|
|
2234
2899
|
if (!hasAnyArgs || values.help) {
|
|
2235
2900
|
printHelpMessage();
|
|
2236
2901
|
return;
|
|
@@ -2239,19 +2904,10 @@ async function main() {
|
|
|
2239
2904
|
printer_default.print(`Pinggy CLI version: ${getVersion()}`);
|
|
2240
2905
|
return;
|
|
2241
2906
|
}
|
|
2242
|
-
|
|
2243
|
-
if (parseResult?.ok === false) {
|
|
2244
|
-
printer_default.error(parseResult.error);
|
|
2245
|
-
logger.error("Failed to initiate remote management:", parseResult.error);
|
|
2246
|
-
process.exit(1);
|
|
2247
|
-
}
|
|
2248
|
-
logger.debug("Building final config from CLI values and positionals", { values, positionals });
|
|
2249
|
-
const finalConfig = await buildFinalConfig(values, positionals);
|
|
2250
|
-
logger.debug("Final configuration built", finalConfig);
|
|
2251
|
-
await startCli(finalConfig, manager);
|
|
2907
|
+
await buildAndStartTunnel(values, positionals, manager);
|
|
2252
2908
|
} catch (error) {
|
|
2253
2909
|
logger.error("Unhandled error in CLI:", error);
|
|
2254
|
-
printer_default.
|
|
2910
|
+
printer_default.fatal(error);
|
|
2255
2911
|
}
|
|
2256
2912
|
}
|
|
2257
2913
|
var currentFile = fileURLToPath(import.meta.url);
|
|
@@ -2265,6 +2921,7 @@ if (entryFile && entryFile === currentFile) {
|
|
|
2265
2921
|
main();
|
|
2266
2922
|
}
|
|
2267
2923
|
export {
|
|
2924
|
+
RemoteManagementUnauthorizedError,
|
|
2268
2925
|
TunnelManager,
|
|
2269
2926
|
TunnelOperations,
|
|
2270
2927
|
closeRemoteManagement,
|