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.
@@ -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
- } from "./chunk-MBN3YBO4.js";
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-HUN2MRZO.js";
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\n");
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) return false;
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) return { token, type, server, qrCode, forceFlag };
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) server = parsed.server;
342
- if (parsed.type) type = parsed.type;
343
- if (parsed.token) token = parsed.token;
344
- if (parsed.forceFlag) forceFlag = true;
345
- if (parsed.qrCode) qrCode = true;
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) type = parsed.type;
353
- if (parsed.token) token = parsed.token;
354
- if (parsed.forceFlag) forceFlag = true;
355
- if (parsed.qrCode) qrCode = true;
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") return null;
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) return false;
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) return null;
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) return parsed;
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) return null;
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") return;
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 parseArgs(finalConfig, remainingPositionals) {
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 path2 = saveconf;
707
+ const path4 = saveconf;
586
708
  try {
587
- fs.writeFileSync(path2, JSON.stringify(config, null, 2), { encoding: "utf-8", flag: "w" });
588
- logger.info(`Configuration saved to ${path2}`);
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) return null;
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) throw dbgErr;
794
+ if (dbgErr instanceof Error) {
795
+ throw dbgErr;
796
+ }
671
797
  const lpErr = parseLocalPort(finalConfig, values);
672
- if (lpErr instanceof Error) throw lpErr;
798
+ if (lpErr instanceof Error) {
799
+ throw lpErr;
800
+ }
673
801
  const rErr = parseReverseTunnelAddr(finalConfig, values, type);
674
- if (rErr instanceof Error) throw rErr;
802
+ if (rErr instanceof Error) {
803
+ throw rErr;
804
+ }
675
805
  const lErr = parseLocalTunnelAddr(finalConfig, values);
676
- if (lErr instanceof Error) throw lErr;
806
+ if (lErr instanceof Error) {
807
+ throw lErr;
808
+ }
677
809
  const serveErr = parseServe(finalConfig, values);
678
- if (serveErr instanceof Error) throw serveErr;
810
+ if (serveErr instanceof Error) {
811
+ throw serveErr;
812
+ }
679
813
  const autoReconnectErr = parseAutoReconnect(finalConfig, values);
680
- if (autoReconnectErr instanceof Error) throw autoReconnectErr;
681
- if (forceFlag || values.force) finalConfig.force = true;
682
- parseArgs(finalConfig, remainingPositionals);
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 qr = await QRCode.toString(url, {
781
- type: "terminal",
782
- small: true,
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(qr);
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?.qrCode || false;
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
- let content = "{green-fg}{bold}Public URLs{/bold}{/green-fg}\n";
1176
- urls.forEach((url, index) => {
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?.qrCode && this.urls.length > 0) {
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.error(`${error.message}`);
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.error(err.message || "Unknown error");
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 { values, positionals, hasAnyArgs } = parseCliArgs(cliOptions);
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
- const parseResult = await parseRemoteManagement(values);
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.error(error);
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,