pinggy 0.5.2 → 0.5.5

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
  import {
2
+ DaemonHost,
2
3
  IPCClient,
3
- SessionMode
4
- } from "./chunk-BFARGPGP.js";
4
+ SessionMode,
5
+ ShutdownStatus
6
+ } from "./chunk-YTJEV27N.js";
5
7
  import {
6
8
  TunnelAlreadyRunningError,
7
9
  TunnelManager,
@@ -10,10 +12,10 @@ import {
10
12
  getVersion,
11
13
  isValidPort,
12
14
  printer_default
13
- } from "./chunk-DLNUDW6G.js";
15
+ } from "./chunk-PQSLSIDA.js";
14
16
  import {
15
17
  logger
16
- } from "./chunk-7G6SJEEA.js";
18
+ } from "./chunk-P7WNBN7F.js";
17
19
  import {
18
20
  getDaemonInfoPath,
19
21
  getDaemonLogPath
@@ -127,7 +129,7 @@ var RemoteManagementStatus = {
127
129
  };
128
130
 
129
131
  // src/remote_management/remote_schema.ts
130
- import { TunnelType } from "@pinggy/pinggy";
132
+ import { HaProxyVersion, TunnelType } from "@pinggy/pinggy";
131
133
  import { z } from "zod";
132
134
  var HeaderModificationSchema = z.object({
133
135
  key: z.string(),
@@ -207,7 +209,7 @@ var UpdateConfigSchema = z.object({
207
209
  var ForwardingEntryV2Schema = z.object({
208
210
  listenAddress: z.string().optional(),
209
211
  address: z.string(),
210
- type: z.enum([TunnelType.Http, TunnelType.Tcp, TunnelType.Udp, TunnelType.Tls, TunnelType.TlsTcp])
212
+ type: z.enum([TunnelType.Http, TunnelType.Tcp, TunnelType.Udp, TunnelType.Tls, TunnelType.TlsTcp]).default(TunnelType.Http)
211
213
  });
212
214
  var OptionalSchema = z.object({
213
215
  sniServerName: z.string().optional(),
@@ -251,7 +253,9 @@ var TunnelConfigV1Schema = z.object({
251
253
  httpsOnly: z.boolean().optional(),
252
254
  originalRequestUrl: z.boolean().optional(),
253
255
  allowPreflight: z.boolean().optional(),
254
- optional: OptionalSchema.optional()
256
+ haProxy: z.enum([HaProxyVersion.V1, HaProxyVersion.V2]).optional(),
257
+ serve: z.string().optional(),
258
+ optional: z.record(z.string(), z.unknown()).optional()
255
259
  });
256
260
  var TUNNEL_CONFIG_V1_KEYS = Object.keys(
257
261
  TunnelConfigV1Schema.shape
@@ -284,6 +288,7 @@ function pinggyOptionsToTunnelConfigV1(opts, configStoredInCli) {
284
288
  httpsOnly: opts.httpsOnly ?? false,
285
289
  originalRequestUrl: opts.originalRequestUrl ?? false,
286
290
  allowPreflight: opts.allowPreflight ?? false,
291
+ haProxy: opts.haProxy,
287
292
  optional: opts.optional || {}
288
293
  };
289
294
  }
@@ -504,11 +509,11 @@ var TunnelOperations = class {
504
509
  startPromise.catch((err) => {
505
510
  logger.error("No-wait startTunnel failed", { tunnelid, err: String(err) });
506
511
  });
507
- return this.buildPendingTunnelResponseV2(tunnelid, tunnelConfig, config, config.configId, config.name, config.optional?.serve);
512
+ return this.buildPendingTunnelResponseV2(tunnelid, tunnelConfig, config, config.configId, config.name, config.serve);
508
513
  }
509
514
  await startPromise;
510
515
  const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
511
- return this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.optional?.serve);
516
+ return this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.serve);
512
517
  } catch (err) {
513
518
  if (err instanceof TunnelAlreadyRunningError) {
514
519
  return this.error(ErrorCode.TunnelAlreadyRunningError, err, err.message);
@@ -530,10 +535,12 @@ var TunnelOperations = class {
530
535
  if (noWait) {
531
536
  const existing = this.tunnelManager.getManagedTunnel(config.configid);
532
537
  if (!existing.tunnelConfig) throw new Error("Invalid tunnel state before configuration update");
533
- this.tunnelManager.updateConfig(updateOpts).catch((err) => {
534
- logger.error("No-wait updateConfig failed", { configid: config.configid, err: String(err) });
535
- });
536
- return this.buildPendingTunnelResponse(existing.tunnelid, existing.tunnelConfig, config.configid, existing.tunnelName, existing.serve);
538
+ if (!existing.isStopped) {
539
+ this.tunnelManager.updateConfig(updateOpts).catch((err) => {
540
+ logger.error("No-wait updateConfig failed", { configid: config.configid, err: String(err) });
541
+ });
542
+ return this.buildPendingTunnelResponse(existing.tunnelid, existing.tunnelConfig, config.configid, existing.tunnelName, existing.serve);
543
+ }
537
544
  }
538
545
  const tunnel = await this.tunnelManager.updateConfig(updateOpts);
539
546
  if (!tunnel.instance || !tunnel.tunnelConfig)
@@ -548,10 +555,12 @@ var TunnelOperations = class {
548
555
  if (noWait) {
549
556
  const existing = this.tunnelManager.getManagedTunnel(config.configId);
550
557
  if (!existing.tunnelConfig) throw new Error("Invalid tunnel state before configuration update");
551
- this.tunnelManager.updateConfig(config).catch((err) => {
552
- logger.error("No-wait updateConfigV2 failed", { configId: config.configId, err: String(err) });
553
- });
554
- return this.buildPendingTunnelResponseV2(existing.tunnelid, existing.tunnelConfig, config, config.configId, existing.tunnelName, existing.serve);
558
+ if (!existing.isStopped) {
559
+ this.tunnelManager.updateConfig(config).catch((err) => {
560
+ logger.error("No-wait updateConfigV2 failed", { configId: config.configId, err: String(err) });
561
+ });
562
+ return this.buildPendingTunnelResponseV2(existing.tunnelid, existing.tunnelConfig, config, config.configId, existing.tunnelName, existing.serve);
563
+ }
555
564
  }
556
565
  const tunnel = await this.tunnelManager.updateConfig(config);
557
566
  if (!tunnel.instance || !tunnel.tunnelConfig)
@@ -698,13 +707,71 @@ import WebSocket from "ws";
698
707
 
699
708
  // src/remote_management/websocket_printer.ts
700
709
  import pico from "picocolors";
701
- var PENDING_START_TIMEOUT_MS = 5 * 60 * 1e3;
702
- var RemoteManagementWebSocketPrinter = class {
710
+ var PENDING_TIMEOUT_MS = 5 * 60 * 1e3;
711
+ var PendingTracker = class {
703
712
  constructor() {
704
- this.pendingStarts = /* @__PURE__ */ new Map();
713
+ this.entries = /* @__PURE__ */ new Map();
714
+ }
715
+ queue(entry) {
716
+ this.latestConfigId = entry.configId;
717
+ this.entries.set(entry.configId, entry);
718
+ }
719
+ /** Store an entry without claiming the latest cursor. */
720
+ upsert(entry) {
721
+ this.entries.set(entry.configId, entry);
722
+ }
723
+ get(configId) {
724
+ return this.entries.get(configId);
725
+ }
726
+ find(configId, tunnelId) {
727
+ const byConfigId = this.entries.get(configId);
728
+ if (byConfigId) {
729
+ return byConfigId;
730
+ }
731
+ if (!tunnelId) {
732
+ return void 0;
733
+ }
734
+ for (const entry of this.entries.values()) {
735
+ if (entry.tunnelId === tunnelId) {
736
+ return entry;
737
+ }
738
+ }
739
+ return void 0;
740
+ }
741
+ delete(configId) {
742
+ this.entries.delete(configId);
743
+ if (this.latestConfigId === configId) {
744
+ this.latestConfigId = void 0;
745
+ }
746
+ }
747
+ get latest() {
748
+ return this.latestConfigId;
705
749
  }
706
- setTunnelHandler(tunnelHandler) {
707
- this.tunnelHandler = tunnelHandler;
750
+ get hasLatest() {
751
+ return this.entries.size > 0 && this.latestConfigId !== void 0;
752
+ }
753
+ isLatest(configId) {
754
+ return configId === this.latestConfigId;
755
+ }
756
+ cleanupExpired(onTimeout) {
757
+ const now = Date.now();
758
+ for (const [configId, entry] of this.entries) {
759
+ if (now - entry.queuedAt <= PENDING_TIMEOUT_MS) {
760
+ continue;
761
+ }
762
+ const wasLatest = this.isLatest(configId);
763
+ this.delete(configId);
764
+ onTimeout(entry, wasLatest);
765
+ }
766
+ }
767
+ };
768
+ var RemoteManagementWebSocketPrinter = class {
769
+ constructor() {
770
+ this.pendingStarts = new PendingTracker();
771
+ this.pendingActions = new PendingTracker();
772
+ // tunnelId -> details, fed by monitorList. Stop/restart requests only carry
773
+ // a tunnelId; without this the spinner shows the raw id.
774
+ this.tunnelDetailsCache = /* @__PURE__ */ new Map();
708
775
  }
709
776
  queueStart(config) {
710
777
  this.cleanupExpiredPendingStarts();
@@ -713,24 +780,23 @@ var RemoteManagementWebSocketPrinter = class {
713
780
  configName: this.getConfigNameFromRequest(config),
714
781
  queuedAt: Date.now()
715
782
  };
716
- this.latestPendingConfigId = entry.configId;
717
- this.pendingStarts.set(entry.configId, entry);
783
+ this.pendingStarts.queue(entry);
718
784
  printer_default.startSpinner("Starting tunnel with config name: " + entry.configName);
719
785
  }
720
786
  failQueuedStart(config, reason) {
721
787
  const configId = this.getConfigIdFromRequest(config);
722
788
  const pending = this.pendingStarts.get(configId);
723
789
  const configName = pending?.configName || this.getConfigNameFromRequest(config);
790
+ const wasLatest = this.pendingStarts.isLatest(configId);
724
791
  this.pendingStarts.delete(configId);
725
- if (this.latestPendingConfigId === configId) {
726
- this.latestPendingConfigId = void 0;
792
+ if (wasLatest) {
727
793
  printer_default.stopSpinnerFail(`Failed to start tunnel with config name: ${configName}. ${reason}`);
728
794
  }
729
795
  }
730
796
  handleStartResult(config, result) {
731
797
  this.cleanupExpiredPendingStarts();
732
798
  const requestedConfigId = this.getConfigIdFromRequest(config);
733
- if (this.latestPendingConfigId && requestedConfigId !== this.latestPendingConfigId) {
799
+ if (this.pendingStarts.latest && !this.pendingStarts.isLatest(requestedConfigId)) {
734
800
  this.pendingStarts.delete(requestedConfigId);
735
801
  return;
736
802
  }
@@ -738,20 +804,20 @@ var RemoteManagementWebSocketPrinter = class {
738
804
  this.failQueuedStart(config, result.message);
739
805
  return;
740
806
  }
741
- const configId = this.getConfigIdFromTunnel(result);
742
807
  const pending = this.pendingStarts.get(requestedConfigId) || {
743
808
  configId: requestedConfigId,
744
809
  configName: this.getConfigNameFromRequest(config),
745
810
  queuedAt: Date.now()
746
811
  };
747
812
  pending.tunnelId = result.tunnelid;
748
- this.pendingStarts.set(requestedConfigId, pending);
813
+ this.pendingStarts.upsert(pending);
749
814
  if (result.remoteurls.length > 0) {
750
815
  this.completePendingStart(pending, result.remoteurls);
751
816
  }
752
817
  }
753
818
  printStopRequested(tunnelId) {
754
819
  const details = this.resolveTunnelDetails(tunnelId);
820
+ this.clearPendingAction(details.configId, tunnelId);
755
821
  printer_default.startSpinner("Stopping tunnel with config name: " + details.configName);
756
822
  }
757
823
  handleStopResult(tunnelId, result) {
@@ -765,6 +831,7 @@ var RemoteManagementWebSocketPrinter = class {
765
831
  }
766
832
  printRestartRequested(tunnelId) {
767
833
  const details = this.resolveTunnelDetails(tunnelId);
834
+ this.clearPendingAction(details.configId, tunnelId);
768
835
  printer_default.startSpinner("Restarting tunnel with config name: " + details.configName);
769
836
  }
770
837
  handleRestartResult(tunnelId, result) {
@@ -774,48 +841,101 @@ var RemoteManagementWebSocketPrinter = class {
774
841
  printer_default.stopSpinnerFail("Failed to restart tunnel with config name: " + details.configName);
775
842
  return;
776
843
  }
777
- printer_default.stopSpinnerSuccess("Restarted tunnel with config name: " + details.configName);
778
- if (result.remoteurls?.length > 0) {
779
- printer_default.info(pico.cyanBright("Remote URLs:"));
780
- (result.remoteurls ?? []).forEach(
781
- (url) => printer_default.print(" " + pico.magentaBright(url))
782
- );
844
+ this.queuePendingAction("restart", details.configId, details.configName, result.tunnelid);
845
+ }
846
+ printUpdateConfigRequested(config) {
847
+ const configName = this.getConfigNameFromRequest(config);
848
+ this.clearPendingAction(this.getConfigIdFromRequest(config));
849
+ printer_default.startSpinner("Updating tunnel configuration for: " + configName);
850
+ }
851
+ handleUpdateConfigResult(config, result) {
852
+ const configId = this.getConfigIdFromRequest(config);
853
+ const configName = this.getConfigNameFromRequest(config);
854
+ if (isErrorResponse(result)) {
855
+ printer_default.stopSpinnerFail(`Failed to update tunnel configuration for: ${configName}. ${result.message}`);
856
+ return;
857
+ }
858
+ if (result.status.state === "exited" /* Exited */) {
859
+ printer_default.stopSpinnerSuccess(`Tunnel configuration updated: ${configName}.`);
860
+ return;
861
+ }
862
+ this.queuePendingAction("update", configId, configName, result.tunnelid);
863
+ }
864
+ clearPendingAction(configId, tunnelId) {
865
+ const entry = this.pendingActions.find(configId, tunnelId);
866
+ if (!entry) {
867
+ return;
783
868
  }
869
+ this.pendingActions.delete(entry.configId);
870
+ }
871
+ queuePendingAction(action, configId, configName, tunnelId) {
872
+ this.cleanupExpiredPendingActions();
873
+ this.pendingActions.queue({ action, configId, configName, queuedAt: Date.now(), tunnelId });
874
+ }
875
+ printActionComplete(action, configName, urls) {
876
+ const message = action === "restart" ? `Restarted tunnel with config name: ${configName}` : `Tunnel configuration updated and restarted: ${configName}`;
877
+ printer_default.stopSpinnerSuccess(message);
878
+ printer_default.info(pico.cyanBright("Remote URLs:"));
879
+ urls.forEach((url) => printer_default.print(" " + pico.magentaBright(url)));
784
880
  }
785
881
  monitorList(result) {
786
882
  this.cleanupExpiredPendingStarts();
787
- if (!Array.isArray(result) || this.pendingStarts.size === 0 || !this.latestPendingConfigId) {
883
+ this.cleanupExpiredPendingActions();
884
+ if (!Array.isArray(result)) {
788
885
  return;
789
886
  }
790
887
  for (const tunnel of result) {
791
- const pending = this.findPendingStart(tunnel);
792
- if (!pending) {
793
- continue;
888
+ this.tunnelDetailsCache.set(tunnel.tunnelid, {
889
+ configId: this.getConfigIdFromTunnel(tunnel),
890
+ configName: this.getConfigNameFromTunnel(tunnel)
891
+ });
892
+ }
893
+ this.resolvePendingFromList(
894
+ result,
895
+ this.pendingStarts,
896
+ (entry, urls) => this.completePendingStart(entry, urls),
897
+ (entry, reason) => printer_default.stopSpinnerFail(`Tunnel start did not complete for config name: ${entry.configName}. ${reason}`)
898
+ );
899
+ this.resolvePendingFromList(
900
+ result,
901
+ this.pendingActions,
902
+ (entry, urls) => {
903
+ this.pendingActions.delete(entry.configId);
904
+ this.printActionComplete(entry.action, entry.configName, urls);
905
+ },
906
+ (entry, reason) => {
907
+ const verb = entry.action === "restart" ? "Restart" : "Configuration update";
908
+ printer_default.stopSpinnerFail(`${verb} did not complete for config name: ${entry.configName}. ${reason}`);
794
909
  }
795
- if (pending.configId !== this.latestPendingConfigId) {
910
+ );
911
+ }
912
+ resolvePendingFromList(tunnels, tracker, onComplete, onExited) {
913
+ if (!tracker.hasLatest) {
914
+ return;
915
+ }
916
+ for (const tunnel of tunnels) {
917
+ const entry = tracker.find(this.getConfigIdFromTunnel(tunnel), tunnel.tunnelid);
918
+ if (!entry || !tracker.isLatest(entry.configId)) {
796
919
  continue;
797
920
  }
798
- pending.tunnelId = tunnel.tunnelid;
799
- this.pendingStarts.set(pending.configId, pending);
921
+ entry.tunnelId = tunnel.tunnelid;
922
+ tracker.upsert(entry);
800
923
  if (tunnel.remoteurls.length > 0) {
801
- this.completePendingStart(pending, tunnel.remoteurls);
924
+ onComplete(entry, tunnel.remoteurls);
802
925
  continue;
803
926
  }
804
927
  if (tunnel.status.state === "exited" /* Exited */) {
805
- const reason = tunnel.status.errormsg || "Tunnel exited before a public URL was assigned";
806
- this.pendingStarts.delete(pending.configId);
807
- this.latestPendingConfigId = void 0;
808
- printer_default.stopSpinnerFail(`Tunnel start did not complete for config name: ${pending.configName}. ${reason}`);
928
+ tracker.delete(entry.configId);
929
+ onExited(entry, tunnel.status.errormsg || "Tunnel exited before a public URL was assigned");
809
930
  }
810
931
  }
811
932
  }
812
933
  completePendingStart(entry, urls) {
813
- if (this.latestPendingConfigId && entry.configId !== this.latestPendingConfigId) {
934
+ if (this.pendingStarts.latest && !this.pendingStarts.isLatest(entry.configId)) {
814
935
  this.pendingStarts.delete(entry.configId);
815
936
  return;
816
937
  }
817
938
  this.pendingStarts.delete(entry.configId);
818
- this.latestPendingConfigId = void 0;
819
939
  printer_default.stopSpinnerSuccess(`Tunnel started with config name: ${entry.configName}.`);
820
940
  printer_default.info(pico.cyanBright("Remote URLs:"));
821
941
  (urls ?? []).forEach(
@@ -823,28 +943,27 @@ var RemoteManagementWebSocketPrinter = class {
823
943
  );
824
944
  }
825
945
  cleanupExpiredPendingStarts() {
826
- const now = Date.now();
827
- for (const [configId, entry] of this.pendingStarts.entries()) {
828
- if (now - entry.queuedAt <= PENDING_START_TIMEOUT_MS) {
829
- continue;
946
+ this.pendingStarts.cleanupExpired((entry, wasLatest) => {
947
+ const message = `Timed out while waiting for tunnel URL for config name: ${entry.configName}`;
948
+ if (wasLatest) {
949
+ printer_default.stopSpinnerFail(message);
950
+ } else {
951
+ printer_default.warn(message);
830
952
  }
831
- this.pendingStarts.delete(configId);
832
- printer_default.warn(`Timed out while waiting for tunnel URL for config name: ${entry.configName}`);
833
- logger.warn("Pending websocket start entry expired", { configId, tunnelId: entry.tunnelId });
834
- }
953
+ logger.warn("Pending websocket start entry expired", { configId: entry.configId, tunnelId: entry.tunnelId });
954
+ });
835
955
  }
836
- findPendingStart(tunnel) {
837
- const configId = this.getConfigIdFromTunnel(tunnel);
838
- const byConfigId = this.pendingStarts.get(configId);
839
- if (byConfigId) {
840
- return byConfigId;
841
- }
842
- for (const entry of this.pendingStarts.values()) {
843
- if (entry.tunnelId === tunnel.tunnelid) {
844
- return entry;
956
+ cleanupExpiredPendingActions() {
957
+ this.pendingActions.cleanupExpired((entry, wasLatest) => {
958
+ const verb = entry.action === "restart" ? "restart" : "configuration update";
959
+ const message = `Timed out while waiting for tunnel URL after ${verb} for config name: ${entry.configName}`;
960
+ if (wasLatest) {
961
+ printer_default.stopSpinnerFail(message);
962
+ } else {
963
+ printer_default.warn(message);
845
964
  }
846
- }
847
- return void 0;
965
+ logger.warn("Pending websocket action entry expired", { configId: entry.configId, tunnelId: entry.tunnelId, action: entry.action });
966
+ });
848
967
  }
849
968
  resolveTunnelDetails(tunnelId, result) {
850
969
  if (result && !isErrorResponse(result)) {
@@ -853,7 +972,7 @@ var RemoteManagementWebSocketPrinter = class {
853
972
  configName: this.getConfigNameFromTunnel(result)
854
973
  };
855
974
  }
856
- return {
975
+ return this.tunnelDetailsCache.get(tunnelId) ?? {
857
976
  configId: tunnelId,
858
977
  configName: tunnelId
859
978
  };
@@ -890,7 +1009,6 @@ var WsCommand = {
890
1009
  var WebSocketCommandHandler = class {
891
1010
  constructor(handler) {
892
1011
  this.tunnelHandler = handler ?? new TunnelOperations();
893
- remoteManagementWebSocketPrinter.setTunnelHandler(this.tunnelHandler);
894
1012
  }
895
1013
  safeParse(text) {
896
1014
  if (!text) return void 0;
@@ -957,8 +1075,10 @@ var WebSocketCommandHandler = class {
957
1075
  }
958
1076
  }
959
1077
  async handleStopReq(req, raw) {
1078
+ let tunnelID;
960
1079
  try {
961
1080
  const dc = StopSchema.parse(raw);
1081
+ tunnelID = dc.tunnelID;
962
1082
  remoteManagementWebSocketPrinter.printStopRequested(dc.tunnelID);
963
1083
  const result = await this.tunnelHandler.handleStop(dc.tunnelID);
964
1084
  remoteManagementWebSocketPrinter.handleStopResult(dc.tunnelID, result);
@@ -968,6 +1088,9 @@ var WebSocketCommandHandler = class {
968
1088
  printer_default.warn("Validation failed for stop request");
969
1089
  return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
970
1090
  }
1091
+ if (tunnelID) {
1092
+ remoteManagementWebSocketPrinter.handleStopResult(tunnelID, newErrorResponse({ code: ErrorCode.InternalServerError, message: errorMessage(e) }));
1093
+ }
971
1094
  printer_default.warn(`Error in handleStopReq error: ${String(e)}`);
972
1095
  return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
973
1096
  }
@@ -987,8 +1110,10 @@ var WebSocketCommandHandler = class {
987
1110
  }
988
1111
  }
989
1112
  async handleRestartReq(req, raw) {
1113
+ let tunnelID;
990
1114
  try {
991
1115
  const dc = RestartSchema.parse(raw);
1116
+ tunnelID = dc.tunnelID;
992
1117
  remoteManagementWebSocketPrinter.printRestartRequested(dc.tunnelID);
993
1118
  const result = await this.tunnelHandler.handleRestart(dc.tunnelID, true);
994
1119
  remoteManagementWebSocketPrinter.handleRestartResult(dc.tunnelID, result);
@@ -998,34 +1123,51 @@ var WebSocketCommandHandler = class {
998
1123
  printer_default.warn("Validation failed for restart request");
999
1124
  return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
1000
1125
  }
1126
+ if (tunnelID) {
1127
+ remoteManagementWebSocketPrinter.handleRestartResult(tunnelID, newErrorResponse({ code: ErrorCode.InternalServerError, message: errorMessage(e) }));
1128
+ }
1001
1129
  printer_default.warn(`Error in handleRestartReq error: ${String(e)}`);
1002
1130
  return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
1003
1131
  }
1004
1132
  }
1005
1133
  async handleUpdateConfigReq(req, raw) {
1134
+ let parsedConfig;
1006
1135
  try {
1007
1136
  const dc = UpdateConfigSchema.parse(raw);
1137
+ parsedConfig = dc.tunnelConfig;
1138
+ remoteManagementWebSocketPrinter.printUpdateConfigRequested(dc.tunnelConfig);
1008
1139
  const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig, true);
1140
+ remoteManagementWebSocketPrinter.handleUpdateConfigResult(dc.tunnelConfig, result);
1009
1141
  return this.wrapResponse(result, req);
1010
1142
  } catch (e) {
1011
1143
  if (e instanceof z2.ZodError) {
1012
1144
  printer_default.warn("Validation failed for updateconfig request");
1013
1145
  return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
1014
1146
  }
1147
+ if (parsedConfig) {
1148
+ remoteManagementWebSocketPrinter.handleUpdateConfigResult(parsedConfig, newErrorResponse({ code: ErrorCode.InternalServerError, message: errorMessage(e) }));
1149
+ }
1015
1150
  printer_default.warn(`Error in handleUpdateConfigReq error: ${String(e)}`);
1016
1151
  return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
1017
1152
  }
1018
1153
  }
1019
1154
  async handleUpdateConfigV2Req(req, raw) {
1155
+ let parsedConfig;
1020
1156
  try {
1021
1157
  const dc = UpdateConfigV2Schema.parse(raw);
1158
+ parsedConfig = dc.tunnelConfig;
1159
+ remoteManagementWebSocketPrinter.printUpdateConfigRequested(dc.tunnelConfig);
1022
1160
  const result = await this.tunnelHandler.handleUpdateConfigV2(dc.tunnelConfig, true);
1161
+ remoteManagementWebSocketPrinter.handleUpdateConfigResult(dc.tunnelConfig, result);
1023
1162
  return this.wrapResponse(result, req);
1024
1163
  } catch (e) {
1025
1164
  if (e instanceof z2.ZodError) {
1026
1165
  printer_default.warn("Validation failed for update-config-v2 request");
1027
1166
  return NewErrorResponseObject({ code: ErrorCode.InvalidBodyFormatError, message: "Validation failed" });
1028
1167
  }
1168
+ if (parsedConfig) {
1169
+ remoteManagementWebSocketPrinter.handleUpdateConfigResult(parsedConfig, newErrorResponse({ code: ErrorCode.InternalServerError, message: errorMessage(e) }));
1170
+ }
1029
1171
  printer_default.warn(`Error in handleUpdateConfigV2Req error: ${String(e)}`);
1030
1172
  return NewErrorResponseObject({ code: ErrorCode.InternalServerError, message: String(e) });
1031
1173
  }
@@ -1505,16 +1647,18 @@ async function ensureDaemonRunning() {
1505
1647
  const existing = getDaemonInfo();
1506
1648
  if (existing) return existing;
1507
1649
  if (process.versions.electron) {
1508
- const { runDaemonChild } = await import("./daemonChild-6QA4AVAN.js");
1650
+ const { runDaemonChild } = await import("./daemonChild-ZACX7V6J.js");
1509
1651
  const handle = await runDaemonChild({
1510
1652
  installSignalHandlers: false,
1511
- exitOnFailure: false
1653
+ exitOnFailure: false,
1654
+ host: DaemonHost.APP
1512
1655
  });
1513
1656
  inProcessHandle = handle;
1514
1657
  return getDaemonInfo() ?? {
1515
1658
  pid: handle.pid,
1516
1659
  port: handle.port,
1517
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
1660
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1661
+ host: DaemonHost.APP
1518
1662
  };
1519
1663
  }
1520
1664
  return startDaemon();
@@ -1525,7 +1669,7 @@ function getInProcessDaemonHandle() {
1525
1669
  async function getActiveTunnelSummaries(origin = "app") {
1526
1670
  const info = getDaemonInfo();
1527
1671
  if (!info) return [];
1528
- const { IPCClient: IPCClient2 } = await import("./ipcClient-LZQCCNMR.js");
1672
+ const { IPCClient: IPCClient2 } = await import("./ipcClient-XRL2WUUM.js");
1529
1673
  const client = new IPCClient2(info.port, origin);
1530
1674
  let tunnels;
1531
1675
  try {
@@ -1572,10 +1716,17 @@ async function stopDaemon() {
1572
1716
  if (!info) return { ok: false, error: "No daemon is running." };
1573
1717
  let daemonErrors = [];
1574
1718
  try {
1575
- const { IPCClient: IPCClient2 } = await import("./ipcClient-LZQCCNMR.js");
1719
+ const { IPCClient: IPCClient2 } = await import("./ipcClient-XRL2WUUM.js");
1576
1720
  const client = new IPCClient2(info.port);
1577
1721
  const result = await client.shutdown();
1578
1722
  logger.debug("Sent shutdown command to daemon", { result });
1723
+ if (result?.status === ShutdownStatus.RefusedInApp) {
1724
+ return {
1725
+ ok: false,
1726
+ reason: DaemonHost.APP,
1727
+ error: "The daemon is running inside the Pinggy app."
1728
+ };
1729
+ }
1579
1730
  if (Array.isArray(result?.errors)) daemonErrors = result.errors;
1580
1731
  } catch (e) {
1581
1732
  return { ok: false, error: `Failed to reach daemon: ${errorMessage(e)}` };
@@ -1861,6 +2012,13 @@ var DaemonHealth = class {
1861
2012
  bindPid(pid) {
1862
2013
  this.originalPid = pid;
1863
2014
  }
2015
+ /**
2016
+ * Forget a previous loss verdict. Called when the client re-binds to a
2017
+ * live daemon via ensureDaemon():
2018
+ */
2019
+ reset() {
2020
+ this.lost = false;
2021
+ }
1864
2022
  isLost() {
1865
2023
  return this.lost;
1866
2024
  }
@@ -1965,6 +2123,7 @@ var TunnelClient = class _TunnelClient {
1965
2123
  const info = await ensureDaemonRunning();
1966
2124
  this.ipc = new IPCClient(info.port, this.origin);
1967
2125
  this.health.bindPid(info.pid);
2126
+ this.health.reset();
1968
2127
  }
1969
2128
  static async forRemoteManagement() {
1970
2129
  const client = new _TunnelClient({ origin: "remote" });
@@ -2029,15 +2188,30 @@ var TunnelClient = class _TunnelClient {
2029
2188
  this.assertClient();
2030
2189
  return this.ipc.getTunnel(tunnelId);
2031
2190
  }
2032
- async handleRestart(tunnelId) {
2191
+ async handleRestart(tunnelId, noWait) {
2033
2192
  this.assertClient();
2034
- return this.ipc.restartTunnel(tunnelId);
2193
+ return this.ipc.restartTunnel(tunnelId, noWait);
2035
2194
  }
2036
2195
  async shutdown() {
2037
2196
  this.assertClient();
2038
2197
  await this.ipc.shutdown();
2039
2198
  this.close();
2040
2199
  }
2200
+ /**
2201
+ * Tear down the daemon hosted in THIS process, if any. Stops all tunnels
2202
+ * and removes daemon.json + daemon-state.json so the next CLI run doesn't
2203
+ * "crash-recover" tunnels that died with the host application.
2204
+ */
2205
+ shutdownInProcessDaemon() {
2206
+ const handle = getInProcessDaemonHandle();
2207
+ if (!handle) return false;
2208
+ try {
2209
+ this.close();
2210
+ } catch {
2211
+ }
2212
+ handle.shutdown();
2213
+ return true;
2214
+ }
2041
2215
  async getLogLevel() {
2042
2216
  this.assertClient();
2043
2217
  const res = await this.ipc.getLogLevel();
@@ -21,6 +21,14 @@ var Route = {
21
21
  SetTunnelLogging: "POST /config/tunnel-logging",
22
22
  GetLogPaths: "GET /logs/paths"
23
23
  };
24
+ var DaemonHost = {
25
+ CLI: "cli",
26
+ APP: "app"
27
+ };
28
+ var ShutdownStatus = {
29
+ ShuttingDown: "shutting_down",
30
+ RefusedInApp: "refused_in_app"
31
+ };
24
32
  var SessionMode = {
25
33
  Foreground: "foreground",
26
34
  Detached: "detached"
@@ -54,8 +62,8 @@ var IPCClient = class {
54
62
  async stopTunnel(tunnelid) {
55
63
  return this.call(Route.StopTunnel, { tunnelid });
56
64
  }
57
- async restartTunnel(tunnelid) {
58
- return this.call(Route.RestartTunnel, { tunnelid });
65
+ async restartTunnel(tunnelid, noWait) {
66
+ return this.call(Route.RestartTunnel, { tunnelid, noWait });
59
67
  }
60
68
  // v1 operations (used by remote management via daemon)
61
69
  async startTunnelV1(config, mode, noWait) {
@@ -159,6 +167,8 @@ var IPCClient = class {
159
167
 
160
168
  export {
161
169
  Route,
170
+ DaemonHost,
171
+ ShutdownStatus,
162
172
  SessionMode,
163
173
  IPCClient
164
174
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  logger
3
- } from "./chunk-7G6SJEEA.js";
3
+ } from "./chunk-P7WNBN7F.js";
4
4
  import {
5
5
  ensureTunnelConfigDir,
6
6
  getTunnelConfigDir
@@ -22,8 +22,8 @@ import {
22
22
  validateName,
23
23
  validateNameForStorage,
24
24
  validateNameStrict
25
- } from "./chunk-WM7I57AA.js";
26
- import "./chunk-7G6SJEEA.js";
25
+ } from "./chunk-YWXADADB.js";
26
+ import "./chunk-P7WNBN7F.js";
27
27
  import "./chunk-GBYF2H4H.js";
28
28
  export {
29
29
  CONFIG_VERBS,