pinggy 0.5.6 → 0.5.8

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/CLAUDE.md CHANGED
@@ -48,7 +48,7 @@ CLI code calls `TunnelClient` (`src/daemon/tunnelClient.ts`), the public facade
48
48
 
49
49
  Single daemon per user. State lives under `~/.config/pinggy/` on Linux/macOS or `%APPDATA%/pinggy/` on Windows (helper: `src/utils/configDir.ts`):
50
50
 
51
- - `daemon.json`: `{pid, port, startedAt}`.
51
+ - `daemon.json`: `{pid, port, startedAt, host, ipcVersion}`. `ipcVersion` is `IPC_VERSION` from `src/daemon/ipc/ipcRoutes.ts`. `ensureDaemonRunning()` refuses a live daemon whose version differs and tells the user to run `pinggy daemon stop`. Bump `IPC_VERSION` on any incompatible IPC or WebSocket change.
52
52
  - `daemon-state.json`: detached tunnel configs for crash recovery (`src/daemon/stateStore.ts`). Deleted on clean shutdown; replayed on next start.
53
53
  - `daemon.log`: SDK + daemon logs. CLI logs stay separate.
54
54
  - `tunnels/<name>_<configId>.json`: saved tunnel configs from `pinggy config save`.
@@ -95,8 +95,12 @@ tsup bundles to `dist/` (CJS + ESM). tsc type-checks only (`npm run build:tsc`).
95
95
 
96
96
  ## English Style
97
97
 
98
- - **No em-dashes.** Use a period, colon, or restructure the sentence. No obvious characters or constructions used by LLMs and AI.
98
+ - **Hyphen only.** The hyphen (-) is the only dash. No em-dash and no en-dash anywhere in any file. This includes numeric ranges (write 1-20) and empty-cell placeholders in tables. Use a period, a colon, or restructure the sentence instead.
99
+ - **No AI-sounding text.** Do not use words, phrases, or symbols typical of LLM output.
100
+ - **No emoji in the codebase.** Documentation may use one only if it is very apt.
101
+ - **Simple English.** Say things in a straightforward way. Keep every technical term and its exact meaning. Use simple but accurate technical terms, not jargon.
99
102
  - **Short phrases.** Enough to convey technical meaning. Nothing more.
103
+ - **No metaphor or personification** in technical statements. State the behavior literally. Write "a partial report is scored and enters the benchmark", never "a partial is a full citizen".
100
104
  - **No filler words.** Cut: "in order to", "it is important to note", "please note that", "essentially", "basically", "simply".
101
105
  - **No passive voice** unless the subject is unknown or irrelevant.
102
106
  - **No nominalizations.** Prefer "detect" over "perform detection"; "configure" over "apply configuration".
@@ -110,3 +114,12 @@ tsup bundles to `dist/` (CJS + ESM). tsc type-checks only (`npm run build:tsc`).
110
114
  ## Code Style
111
115
  - **No dead code.** Remove unused imports, functions, and variables immediately. `ruff` enforces this.
112
116
  - **Small functions.** If a function needs a comment to explain its sections, split it.
117
+
118
+ ## Answering Questions
119
+
120
+ - **A question is not a change request.** When asked a question, do not change code. Answer it.
121
+
122
+ ## Commit Messages
123
+
124
+ - **Plain change description.** Write the message as a human author would: what changed and why.
125
+ - **No assistant attribution.** Do not mention Claude, AI, or any assistant. No `Co-Authored-By` assistant trailer, no "Generated with" line, no tool attribution.
package/README.md CHANGED
@@ -287,7 +287,7 @@ These flags apply to the CLI invocation. For daemon-wide log level and per-tunne
287
287
  ### **AutoReconnect**
288
288
  | Flag | Description |
289
289
  |------|-------------|
290
- | `--autoreconnect`, `-a` | Automatically reconnect tunnel on failure (enabled by default; pass `false` to disable) |
290
+ | `--no-autoreconnect`, `-a` | Disable automatic reconnection on failure. Enabled by default, and it keeps retrying until the tunnel is back (no attempt limit) |
291
291
 
292
292
  ---
293
293
 
@@ -1,9 +1,10 @@
1
1
  import {
2
2
  DaemonHost,
3
3
  IPCClient,
4
+ IPC_VERSION,
4
5
  SessionMode,
5
6
  ShutdownStatus
6
- } from "./chunk-YTJEV27N.js";
7
+ } from "./chunk-I4HXBP5G.js";
7
8
  import {
8
9
  TunnelAlreadyRunningError,
9
10
  TunnelManager,
@@ -289,6 +290,8 @@ function pinggyOptionsToTunnelConfigV1(opts, configStoredInCli) {
289
290
  serverAddress: opts.serverAddress || "a.pinggy.io:443",
290
291
  token: opts.token || "",
291
292
  autoReconnect: opts.autoReconnect ?? true,
293
+ reconnectInterval: opts.reconnectInterval,
294
+ maxReconnectAttempts: opts.maxReconnectAttempts,
292
295
  force: opts.force ?? false,
293
296
  webDebugger: opts.webDebugger || "",
294
297
  forwarding: opts.forwarding ? opts.forwarding : "",
@@ -1612,6 +1615,19 @@ function getDaemonInfo() {
1612
1615
  function isDaemonRunning() {
1613
1616
  return getDaemonInfo() !== null;
1614
1617
  }
1618
+ function daemonIpcVersion(info) {
1619
+ return info.ipcVersion ?? 0;
1620
+ }
1621
+ function isIpcCompatible(info) {
1622
+ return daemonIpcVersion(info) === IPC_VERSION;
1623
+ }
1624
+ function ipcMismatchMessage(info) {
1625
+ const versions = `daemon IPC v${daemonIpcVersion(info)}, this CLI expects v${IPC_VERSION}`;
1626
+ if (info.host === DaemonHost.APP) {
1627
+ return `The Pinggy app is running a daemon from a different version (${versions}). Update or restart the Pinggy app.`;
1628
+ }
1629
+ return `A daemon from a different Pinggy CLI version is running (PID ${info.pid}; ${versions}). Run "pinggy daemon stop" and retry. This stops all running tunnels.`;
1630
+ }
1615
1631
  function getDaemonSpawnArgs() {
1616
1632
  return {
1617
1633
  command: process.execPath,
@@ -1658,9 +1674,12 @@ async function startDaemon() {
1658
1674
  }
1659
1675
  async function ensureDaemonRunning() {
1660
1676
  const existing = getDaemonInfo();
1661
- if (existing) return existing;
1677
+ if (existing) {
1678
+ if (!isIpcCompatible(existing)) throw new Error(ipcMismatchMessage(existing));
1679
+ return existing;
1680
+ }
1662
1681
  if (process.versions.electron) {
1663
- const { runDaemonChild } = await import("./daemonChild-3ONQO2XI.js");
1682
+ const { runDaemonChild } = await import("./daemonChild-ZPZK5F4Q.js");
1664
1683
  const handle = await runDaemonChild({
1665
1684
  installSignalHandlers: false,
1666
1685
  exitOnFailure: false,
@@ -1671,7 +1690,8 @@ async function ensureDaemonRunning() {
1671
1690
  pid: handle.pid,
1672
1691
  port: handle.port,
1673
1692
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1674
- host: DaemonHost.APP
1693
+ host: DaemonHost.APP,
1694
+ ipcVersion: IPC_VERSION
1675
1695
  };
1676
1696
  }
1677
1697
  return startDaemon();
@@ -1682,7 +1702,7 @@ function getInProcessDaemonHandle() {
1682
1702
  async function getActiveTunnelSummaries(origin = "app") {
1683
1703
  const info = getDaemonInfo();
1684
1704
  if (!info) return [];
1685
- const { IPCClient: IPCClient2 } = await import("./ipcClient-XRL2WUUM.js");
1705
+ const { IPCClient: IPCClient2 } = await import("./ipcClient-URHJKNEH.js");
1686
1706
  const client = new IPCClient2(info.port, origin);
1687
1707
  let tunnels;
1688
1708
  try {
@@ -1729,7 +1749,7 @@ async function stopDaemon() {
1729
1749
  if (!info) return { ok: false, error: "No daemon is running." };
1730
1750
  let daemonErrors = [];
1731
1751
  try {
1732
- const { IPCClient: IPCClient2 } = await import("./ipcClient-XRL2WUUM.js");
1752
+ const { IPCClient: IPCClient2 } = await import("./ipcClient-URHJKNEH.js");
1733
1753
  const client = new IPCClient2(info.port);
1734
1754
  const result = await client.shutdown();
1735
1755
  logger.debug("Sent shutdown command to daemon", { result });
@@ -1884,6 +1904,9 @@ var WsStream = class {
1884
1904
  subscriptionCount() {
1885
1905
  return this.subscribedTunnels.size;
1886
1906
  }
1907
+ isSubscribed(tunnelId) {
1908
+ return this.subscribedTunnels.has(tunnelId);
1909
+ }
1887
1910
  /** Snapshot of current subscriptions, for the reconnect path to replay. */
1888
1911
  snapshotSubscriptions() {
1889
1912
  return Array.from(this.subscribedTunnels.entries());
@@ -1917,6 +1940,16 @@ var WsStream = class {
1917
1940
  onDisconnect(cb) {
1918
1941
  this.callbacks.disconnect.push(cb);
1919
1942
  }
1943
+ // Removal replaces the array instead of splicing it, so a callback that
1944
+ // unregisters itself while handleMessage is iterating cannot make the
1945
+ // dispatch loop skip the next callback. The removal applies from the next
1946
+ // event on.
1947
+ offStats(cb) {
1948
+ this.callbacks.stats = this.callbacks.stats.filter((c) => c !== cb);
1949
+ }
1950
+ offDisconnect(cb) {
1951
+ this.callbacks.disconnect = this.callbacks.disconnect.filter((c) => c !== cb);
1952
+ }
1920
1953
  onReconnecting(cb) {
1921
1954
  this.callbacks.reconnecting.push(cb);
1922
1955
  }
@@ -2125,6 +2158,12 @@ var DaemonHealth = class {
2125
2158
  var TunnelClient = class _TunnelClient {
2126
2159
  constructor(options = {}) {
2127
2160
  this.ipc = null;
2161
+ // App-compat shim state: registered listeners by id, and the tunnels whose
2162
+ // stream subscription those registrations opened (as opposed to attach()).
2163
+ this.shimStatsListeners = /* @__PURE__ */ new Map();
2164
+ this.shimDisconnectListeners = /* @__PURE__ */ new Map();
2165
+ this.shimSubscribedTunnels = /* @__PURE__ */ new Set();
2166
+ this.shimListenerSeq = 0;
2128
2167
  this.origin = options.origin ?? "cli";
2129
2168
  this.stream = new WsStream(() => {
2130
2169
  if (!this.ipc) throw new Error("TunnelClient not initialized. Call ensureDaemon() first.");
@@ -2272,6 +2311,12 @@ var TunnelClient = class _TunnelClient {
2272
2311
  onDisconnect(cb) {
2273
2312
  this.stream.onDisconnect(cb);
2274
2313
  }
2314
+ offStats(cb) {
2315
+ this.stream.offStats(cb);
2316
+ }
2317
+ offDisconnect(cb) {
2318
+ this.stream.offDisconnect(cb);
2319
+ }
2275
2320
  onReconnecting(cb) {
2276
2321
  this.stream.onReconnecting(cb);
2277
2322
  }
@@ -2309,24 +2354,73 @@ var TunnelClient = class _TunnelClient {
2309
2354
  isDaemonLost() {
2310
2355
  return this.health.isLost();
2311
2356
  }
2312
- // App-compat shims (register listener + auto-attach in detached mode)
2357
+ // App-compat shims. Each registration is scoped to ONE tunnel, returns an
2358
+ // id, and is undone by the matching unregister call. Before this, every
2359
+ // register appended a callback that saw every subscribed tunnel's events
2360
+ // and could never be removed, so a consumer that registered per page view
2361
+ // received each stats update N times, forever.
2362
+ //
2363
+ // The first registration for a tunnel opens its stream subscription and
2364
+ // the last unregister drops it with a plain unsubscribe. detach() is not
2365
+ // used here on purpose: it closes the client once nothing is subscribed,
2366
+ // which also stops the daemon-health heartbeat, and a stats consumer must
2367
+ // not have that side effect.
2313
2368
  handleRegisterStatsListener(tunnelId, listener) {
2314
- this.onStats(listener);
2315
- this.attach(tunnelId, "detached").catch(() => {
2316
- });
2369
+ const wrapped = (id, stats) => {
2370
+ if (id === tunnelId) listener(id, stats);
2371
+ };
2372
+ const listenerId = this.nextShimListenerId("stats");
2373
+ this.shimStatsListeners.set(listenerId, { tunnelId, wrapped });
2374
+ this.stream.onStats(wrapped);
2375
+ this.subscribeForShim(tunnelId);
2376
+ return listenerId;
2377
+ }
2378
+ handleUnregisterStatsListener(_tunnelId, listenerId) {
2379
+ const entry = this.shimStatsListeners.get(listenerId);
2380
+ if (!entry) return;
2381
+ this.shimStatsListeners.delete(listenerId);
2382
+ this.stream.offStats(entry.wrapped);
2383
+ this.releaseShimSubscription(entry.tunnelId);
2317
2384
  }
2318
2385
  handleRegisterDisconnectListener(tunnelId, listener) {
2319
- this.onDisconnect(listener);
2320
- this.attach(tunnelId, "detached").catch(() => {
2321
- });
2322
- }
2323
- handleUnregisterStatsListener(_tunnelId, _listenerId) {
2386
+ const wrapped = (id, error, messages) => {
2387
+ if (id === tunnelId) listener(id, error, messages);
2388
+ };
2389
+ const listenerId = this.nextShimListenerId("disconnect");
2390
+ this.shimDisconnectListeners.set(listenerId, { tunnelId, wrapped });
2391
+ this.stream.onDisconnect(wrapped);
2392
+ this.subscribeForShim(tunnelId);
2393
+ return listenerId;
2394
+ }
2395
+ handleUnregisterDisconnectListener(_tunnelId, listenerId) {
2396
+ const entry = this.shimDisconnectListeners.get(listenerId);
2397
+ if (!entry) return;
2398
+ this.shimDisconnectListeners.delete(listenerId);
2399
+ this.stream.offDisconnect(entry.wrapped);
2400
+ this.releaseShimSubscription(entry.tunnelId);
2324
2401
  }
2325
2402
  async handleGetTunnelStats(tunnelId) {
2326
2403
  this.assertClient();
2327
2404
  return this.ipc.getTunnelStats(tunnelId);
2328
2405
  }
2329
2406
  // Private
2407
+ nextShimListenerId(kind) {
2408
+ this.shimListenerSeq += 1;
2409
+ return `${kind}-${this.shimListenerSeq}`;
2410
+ }
2411
+ subscribeForShim(tunnelId) {
2412
+ if (!this.stream.isSubscribed(tunnelId)) this.shimSubscribedTunnels.add(tunnelId);
2413
+ this.attach(tunnelId, SessionMode.Detached).catch((err) => {
2414
+ logger.debug("Listener subscribe failed", { tunnelId, error: errorMessage(err) });
2415
+ });
2416
+ }
2417
+ releaseShimSubscription(tunnelId) {
2418
+ if (!this.shimSubscribedTunnels.has(tunnelId)) return;
2419
+ for (const entry of this.shimStatsListeners.values()) if (entry.tunnelId === tunnelId) return;
2420
+ for (const entry of this.shimDisconnectListeners.values()) if (entry.tunnelId === tunnelId) return;
2421
+ this.shimSubscribedTunnels.delete(tunnelId);
2422
+ this.stream.unsubscribe(tunnelId);
2423
+ }
2330
2424
  assertClient() {
2331
2425
  if (!this.ipc) {
2332
2426
  throw new Error("TunnelClient not initialized. Call ensureDaemon() first.");
@@ -2351,6 +2445,9 @@ export {
2351
2445
  getRemoteManagementState,
2352
2446
  getDaemonInfo,
2353
2447
  isDaemonRunning,
2448
+ daemonIpcVersion,
2449
+ isIpcCompatible,
2450
+ ipcMismatchMessage,
2354
2451
  startDaemon,
2355
2452
  ensureDaemonRunning,
2356
2453
  getInProcessDaemonHandle,
@@ -2,6 +2,7 @@
2
2
  import http from "http";
3
3
 
4
4
  // src/daemon/ipc/ipcRoutes.ts
5
+ var IPC_VERSION = 1;
5
6
  var Route = {
6
7
  Ping: "GET /ping",
7
8
  ListTunnels: "GET /tunnels",
@@ -166,6 +167,7 @@ var IPCClient = class {
166
167
  };
167
168
 
168
169
  export {
170
+ IPC_VERSION,
169
171
  Route,
170
172
  DaemonHost,
171
173
  ShutdownStatus,
@@ -3,23 +3,27 @@ import {
3
3
  TunnelClient,
4
4
  TunnelOperations,
5
5
  buildRemoteManagementWsUrl,
6
+ daemonIpcVersion,
6
7
  getDaemonInfo,
7
8
  initiateRemoteManagement,
9
+ ipcMismatchMessage,
8
10
  isDaemonRunning,
9
11
  isErrorResponse,
12
+ isIpcCompatible,
10
13
  startDaemon,
11
14
  startRemoteManagement,
12
15
  stopDaemon
13
- } from "./chunk-HN4YLM7U.js";
16
+ } from "./chunk-B6IKYBLU.js";
14
17
  import {
15
18
  readDaemonConfig
16
19
  } from "./chunk-MT44NAXX.js";
17
20
  import {
18
21
  DaemonHost,
22
+ IPC_VERSION,
19
23
  Route,
20
24
  SessionMode,
21
25
  ShutdownStatus
22
- } from "./chunk-YTJEV27N.js";
26
+ } from "./chunk-I4HXBP5G.js";
23
27
  import {
24
28
  ConfigVerb,
25
29
  SUBCOMMANDS,
@@ -271,7 +275,10 @@ var defaultOptions = {
271
275
  originalRequestUrl: false,
272
276
  allowPreflight: false,
273
277
  reverseProxy: true,
274
- autoReconnect: true
278
+ autoReconnect: true,
279
+ // 0 = never give up: keep retrying every reconnectInterval until the tunnel
280
+ // is back. Not user-configurable from the CLI on purpose.
281
+ maxReconnectAttempts: 0
275
282
  };
276
283
 
277
284
  // src/cli/extendedOptions.ts
@@ -879,7 +886,9 @@ function buildFinalConfig(values, positionals, baseConfig) {
879
886
  token: token || (configFromFile?.token || (typeof values.token === "string" ? values.token : "")),
880
887
  serverAddress: server ? removeIPv6Brackets(server) : configFromFile?.serverAddress || defaultOptions.serverAddress,
881
888
  isQRCode: qrCode || (configFromFile?.isQRCode || false),
882
- autoReconnect: configFromFile?.autoReconnect ? configFromFile.autoReconnect : defaultOptions.autoReconnect,
889
+ // `??`, not a truthiness check: a saved `autoReconnect: false` must not
890
+ // flip back to the default.
891
+ autoReconnect: configFromFile?.autoReconnect ?? defaultOptions.autoReconnect,
883
892
  optional: {
884
893
  serve: configFromFile?.optional?.serve || void 0,
885
894
  noTui: values.noTui || values.notui || hasRemoteManagement(values) || (configFromFile?.optional?.noTui || false)
@@ -1779,9 +1788,13 @@ async function handleDaemon(args) {
1779
1788
  }
1780
1789
  }
1781
1790
  async function handleDaemonStart() {
1782
- if (isDaemonRunning()) {
1783
- const info = getDaemonInfo();
1784
- printer_default.print(pico2.yellow(`Daemon already running (PID ${info?.pid}, port ${info?.port}). Use: pinggy daemon status for details.`));
1791
+ const info = getDaemonInfo();
1792
+ if (info) {
1793
+ if (!isIpcCompatible(info)) {
1794
+ printer_default.error(ipcMismatchMessage(info));
1795
+ process.exit(1);
1796
+ }
1797
+ printer_default.print(pico2.yellow(`Daemon already running (PID ${info.pid}, port ${info.port}). Use: pinggy daemon status for details.`));
1785
1798
  return;
1786
1799
  }
1787
1800
  const autoConfigs = getAutoStartConfigs();
@@ -1794,8 +1807,8 @@ async function handleDaemonStart() {
1794
1807
  printer_default.print(pico2.cyanBright("Starting daemon..."));
1795
1808
  }
1796
1809
  try {
1797
- const info = await startDaemon();
1798
- printer_default.success(`Daemon started PID ${info.pid}.`);
1810
+ const info2 = await startDaemon();
1811
+ printer_default.success(`Daemon started PID ${info2.pid}.`);
1799
1812
  } catch (err) {
1800
1813
  printer_default.error(`Failed to start daemon: ${errorMessage(err)}`);
1801
1814
  process.exit(1);
@@ -1832,6 +1845,10 @@ function handleDaemonStatus() {
1832
1845
  printer_default.print(` Started: ${info.startedAt}`);
1833
1846
  printer_default.print(` Uptime: ${uptimeStr}`);
1834
1847
  printer_default.print(` Host: ${info.host === DaemonHost.APP ? "Pinggy app (in-process)" : "standalone (CLI)"}`);
1848
+ printer_default.print(` IPC: v${daemonIpcVersion(info)}`);
1849
+ if (!isIpcCompatible(info)) {
1850
+ printer_default.print(pico2.yellow(` Warning: ${ipcMismatchMessage(info)}`));
1851
+ }
1835
1852
  }
1836
1853
 
1837
1854
  // src/cli/subcommand/handlers/psCommand.ts
@@ -2610,7 +2627,8 @@ var IPCServer = class {
2610
2627
  [Route.Ping]: () => ({
2611
2628
  status: "ok",
2612
2629
  pid: process.pid,
2613
- uptime: Math.floor((Date.now() - this.startedAt) / 1e3)
2630
+ uptime: Math.floor((Date.now() - this.startedAt) / 1e3),
2631
+ ipcVersion: IPC_VERSION
2614
2632
  }),
2615
2633
  [Route.ListTunnels]: async () => {
2616
2634
  const res = await this.ops.handleListV2();
@@ -3403,7 +3421,8 @@ async function runDaemonChild(opts = {}) {
3403
3421
  pid: process.pid,
3404
3422
  port,
3405
3423
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3406
- host: daemonHost
3424
+ host: daemonHost,
3425
+ ipcVersion: IPC_VERSION
3407
3426
  };
3408
3427
  writeDaemonInfo(info);
3409
3428
  logger.info("Daemon info written", info);
@@ -3444,7 +3463,7 @@ async function main() {
3444
3463
  const { values, positionals, hasAnyArgs } = parseCliArgs(cliOptions);
3445
3464
  configureLogger(values);
3446
3465
  if (values["_daemon-child"]) {
3447
- const { runDaemonChild: runDaemonChild2 } = await import("./daemonChild-3ONQO2XI.js");
3466
+ const { runDaemonChild: runDaemonChild2 } = await import("./daemonChild-ZPZK5F4Q.js");
3448
3467
  await runDaemonChild2();
3449
3468
  return;
3450
3469
  }
@@ -6,12 +6,12 @@ import {
6
6
  trackIPCTunnelStart,
7
7
  trackTunnelStart,
8
8
  trackTunnelStop
9
- } from "./chunk-OH6FSLYF.js";
10
- import "./chunk-HN4YLM7U.js";
9
+ } from "./chunk-P7XRY5N4.js";
10
+ import "./chunk-B6IKYBLU.js";
11
11
  import "./chunk-MT44NAXX.js";
12
12
  import {
13
13
  DaemonHost
14
- } from "./chunk-YTJEV27N.js";
14
+ } from "./chunk-I4HXBP5G.js";
15
15
  import "./chunk-YWXADADB.js";
16
16
  import "./chunk-PQSLSIDA.js";
17
17
  import "./chunk-UB26QJ4T.js";
package/dist/index.cjs CHANGED
@@ -2194,6 +2194,8 @@ function pinggyOptionsToTunnelConfigV1(opts, configStoredInCli) {
2194
2194
  serverAddress: opts.serverAddress || "a.pinggy.io:443",
2195
2195
  token: opts.token || "",
2196
2196
  autoReconnect: opts.autoReconnect ?? true,
2197
+ reconnectInterval: opts.reconnectInterval,
2198
+ maxReconnectAttempts: opts.maxReconnectAttempts,
2197
2199
  force: opts.force ?? false,
2198
2200
  webDebugger: opts.webDebugger || "",
2199
2201
  forwarding: opts.forwarding ? opts.forwarding : "",
@@ -2791,11 +2793,12 @@ var init_handler = __esm({
2791
2793
  });
2792
2794
 
2793
2795
  // src/daemon/ipc/ipcRoutes.ts
2794
- var Route, DaemonHost, ShutdownStatus, SessionMode;
2796
+ var IPC_VERSION, Route, DaemonHost, ShutdownStatus, SessionMode;
2795
2797
  var init_ipcRoutes = __esm({
2796
2798
  "src/daemon/ipc/ipcRoutes.ts"() {
2797
2799
  "use strict";
2798
2800
  init_cjs_shims();
2801
+ IPC_VERSION = 1;
2799
2802
  Route = {
2800
2803
  Ping: "GET /ping",
2801
2804
  ListTunnels: "GET /tunnels",
@@ -4093,7 +4096,10 @@ var init_defaults = __esm({
4093
4096
  originalRequestUrl: false,
4094
4097
  allowPreflight: false,
4095
4098
  reverseProxy: true,
4096
- autoReconnect: true
4099
+ autoReconnect: true,
4100
+ // 0 = never give up: keep retrying every reconnectInterval until the tunnel
4101
+ // is back. Not user-configurable from the CLI on purpose.
4102
+ maxReconnectAttempts: 0
4097
4103
  };
4098
4104
  }
4099
4105
  });
@@ -4697,7 +4703,9 @@ function buildFinalConfig(values, positionals, baseConfig) {
4697
4703
  token: token || (configFromFile?.token || (typeof values.token === "string" ? values.token : "")),
4698
4704
  serverAddress: server ? removeIPv6Brackets(server) : configFromFile?.serverAddress || defaultOptions.serverAddress,
4699
4705
  isQRCode: qrCode || (configFromFile?.isQRCode || false),
4700
- autoReconnect: configFromFile?.autoReconnect ? configFromFile.autoReconnect : defaultOptions.autoReconnect,
4706
+ // `??`, not a truthiness check: a saved `autoReconnect: false` must not
4707
+ // flip back to the default.
4708
+ autoReconnect: configFromFile?.autoReconnect ?? defaultOptions.autoReconnect,
4701
4709
  optional: {
4702
4710
  serve: configFromFile?.optional?.serve || void 0,
4703
4711
  noTui: values.noTui || values.notui || hasRemoteManagement(values) || (configFromFile?.optional?.noTui || false)
@@ -7527,9 +7535,13 @@ async function handleDaemon(args) {
7527
7535
  }
7528
7536
  }
7529
7537
  async function handleDaemonStart() {
7530
- if (isDaemonRunning()) {
7531
- const info = getDaemonInfo();
7532
- printer_default.print(import_picocolors6.default.yellow(`Daemon already running (PID ${info?.pid}, port ${info?.port}). Use: pinggy daemon status for details.`));
7538
+ const info = getDaemonInfo();
7539
+ if (info) {
7540
+ if (!isIpcCompatible(info)) {
7541
+ printer_default.error(ipcMismatchMessage(info));
7542
+ process.exit(1);
7543
+ }
7544
+ printer_default.print(import_picocolors6.default.yellow(`Daemon already running (PID ${info.pid}, port ${info.port}). Use: pinggy daemon status for details.`));
7533
7545
  return;
7534
7546
  }
7535
7547
  const autoConfigs = getAutoStartConfigs();
@@ -7542,8 +7554,8 @@ async function handleDaemonStart() {
7542
7554
  printer_default.print(import_picocolors6.default.cyanBright("Starting daemon..."));
7543
7555
  }
7544
7556
  try {
7545
- const info = await startDaemon();
7546
- printer_default.success(`Daemon started PID ${info.pid}.`);
7557
+ const info2 = await startDaemon();
7558
+ printer_default.success(`Daemon started PID ${info2.pid}.`);
7547
7559
  } catch (err) {
7548
7560
  printer_default.error(`Failed to start daemon: ${errorMessage(err)}`);
7549
7561
  process.exit(1);
@@ -7580,6 +7592,10 @@ function handleDaemonStatus() {
7580
7592
  printer_default.print(` Started: ${info.startedAt}`);
7581
7593
  printer_default.print(` Uptime: ${uptimeStr}`);
7582
7594
  printer_default.print(` Host: ${info.host === DaemonHost.APP ? "Pinggy app (in-process)" : "standalone (CLI)"}`);
7595
+ printer_default.print(` IPC: v${daemonIpcVersion(info)}`);
7596
+ if (!isIpcCompatible(info)) {
7597
+ printer_default.print(import_picocolors6.default.yellow(` Warning: ${ipcMismatchMessage(info)}`));
7598
+ }
7583
7599
  }
7584
7600
  var import_picocolors6;
7585
7601
  var init_daemonCommandsHandler = __esm({
@@ -8573,7 +8589,8 @@ var init_ipcServer = __esm({
8573
8589
  [Route.Ping]: () => ({
8574
8590
  status: "ok",
8575
8591
  pid: process.pid,
8576
- uptime: Math.floor((Date.now() - this.startedAt) / 1e3)
8592
+ uptime: Math.floor((Date.now() - this.startedAt) / 1e3),
8593
+ ipcVersion: IPC_VERSION
8577
8594
  }),
8578
8595
  [Route.ListTunnels]: async () => {
8579
8596
  const res = await this.ops.handleListV2();
@@ -9388,7 +9405,8 @@ async function runDaemonChild(opts = {}) {
9388
9405
  pid: process.pid,
9389
9406
  port,
9390
9407
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
9391
- host: daemonHost
9408
+ host: daemonHost,
9409
+ ipcVersion: IPC_VERSION
9392
9410
  };
9393
9411
  writeDaemonInfo(info);
9394
9412
  logger.info("Daemon info written", info);
@@ -9475,6 +9493,19 @@ function getDaemonInfo() {
9475
9493
  function isDaemonRunning() {
9476
9494
  return getDaemonInfo() !== null;
9477
9495
  }
9496
+ function daemonIpcVersion(info) {
9497
+ return info.ipcVersion ?? 0;
9498
+ }
9499
+ function isIpcCompatible(info) {
9500
+ return daemonIpcVersion(info) === IPC_VERSION;
9501
+ }
9502
+ function ipcMismatchMessage(info) {
9503
+ const versions = `daemon IPC v${daemonIpcVersion(info)}, this CLI expects v${IPC_VERSION}`;
9504
+ if (info.host === DaemonHost.APP) {
9505
+ return `The Pinggy app is running a daemon from a different version (${versions}). Update or restart the Pinggy app.`;
9506
+ }
9507
+ return `A daemon from a different Pinggy CLI version is running (PID ${info.pid}; ${versions}). Run "pinggy daemon stop" and retry. This stops all running tunnels.`;
9508
+ }
9478
9509
  function getDaemonSpawnArgs() {
9479
9510
  return {
9480
9511
  command: process.execPath,
@@ -9521,7 +9552,10 @@ async function startDaemon() {
9521
9552
  }
9522
9553
  async function ensureDaemonRunning() {
9523
9554
  const existing = getDaemonInfo();
9524
- if (existing) return existing;
9555
+ if (existing) {
9556
+ if (!isIpcCompatible(existing)) throw new Error(ipcMismatchMessage(existing));
9557
+ return existing;
9558
+ }
9525
9559
  if (process.versions.electron) {
9526
9560
  const { runDaemonChild: runDaemonChild2 } = await Promise.resolve().then(() => (init_daemonChild(), daemonChild_exports));
9527
9561
  const handle = await runDaemonChild2({
@@ -9534,7 +9568,8 @@ async function ensureDaemonRunning() {
9534
9568
  pid: handle.pid,
9535
9569
  port: handle.port,
9536
9570
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
9537
- host: DaemonHost.APP
9571
+ host: DaemonHost.APP,
9572
+ ipcVersion: IPC_VERSION
9538
9573
  };
9539
9574
  }
9540
9575
  return startDaemon();
@@ -9773,6 +9808,9 @@ var init_wsStream = __esm({
9773
9808
  subscriptionCount() {
9774
9809
  return this.subscribedTunnels.size;
9775
9810
  }
9811
+ isSubscribed(tunnelId) {
9812
+ return this.subscribedTunnels.has(tunnelId);
9813
+ }
9776
9814
  /** Snapshot of current subscriptions, for the reconnect path to replay. */
9777
9815
  snapshotSubscriptions() {
9778
9816
  return Array.from(this.subscribedTunnels.entries());
@@ -9806,6 +9844,16 @@ var init_wsStream = __esm({
9806
9844
  onDisconnect(cb) {
9807
9845
  this.callbacks.disconnect.push(cb);
9808
9846
  }
9847
+ // Removal replaces the array instead of splicing it, so a callback that
9848
+ // unregisters itself while handleMessage is iterating cannot make the
9849
+ // dispatch loop skip the next callback. The removal applies from the next
9850
+ // event on.
9851
+ offStats(cb) {
9852
+ this.callbacks.stats = this.callbacks.stats.filter((c) => c !== cb);
9853
+ }
9854
+ offDisconnect(cb) {
9855
+ this.callbacks.disconnect = this.callbacks.disconnect.filter((c) => c !== cb);
9856
+ }
9809
9857
  onReconnecting(cb) {
9810
9858
  this.callbacks.reconnecting.push(cb);
9811
9859
  }
@@ -10040,6 +10088,8 @@ var init_tunnelClient = __esm({
10040
10088
  init_cjs_shims();
10041
10089
  init_ipcClient();
10042
10090
  init_daemonManager();
10091
+ init_logger();
10092
+ init_util();
10043
10093
  init_wsStream();
10044
10094
  init_daemonHealth();
10045
10095
  init_daemonTunnelHandler();
@@ -10047,6 +10097,12 @@ var init_tunnelClient = __esm({
10047
10097
  TunnelClient = class _TunnelClient {
10048
10098
  constructor(options = {}) {
10049
10099
  this.ipc = null;
10100
+ // App-compat shim state: registered listeners by id, and the tunnels whose
10101
+ // stream subscription those registrations opened (as opposed to attach()).
10102
+ this.shimStatsListeners = /* @__PURE__ */ new Map();
10103
+ this.shimDisconnectListeners = /* @__PURE__ */ new Map();
10104
+ this.shimSubscribedTunnels = /* @__PURE__ */ new Set();
10105
+ this.shimListenerSeq = 0;
10050
10106
  this.origin = options.origin ?? "cli";
10051
10107
  this.stream = new WsStream(() => {
10052
10108
  if (!this.ipc) throw new Error("TunnelClient not initialized. Call ensureDaemon() first.");
@@ -10194,6 +10250,12 @@ var init_tunnelClient = __esm({
10194
10250
  onDisconnect(cb) {
10195
10251
  this.stream.onDisconnect(cb);
10196
10252
  }
10253
+ offStats(cb) {
10254
+ this.stream.offStats(cb);
10255
+ }
10256
+ offDisconnect(cb) {
10257
+ this.stream.offDisconnect(cb);
10258
+ }
10197
10259
  onReconnecting(cb) {
10198
10260
  this.stream.onReconnecting(cb);
10199
10261
  }
@@ -10231,24 +10293,73 @@ var init_tunnelClient = __esm({
10231
10293
  isDaemonLost() {
10232
10294
  return this.health.isLost();
10233
10295
  }
10234
- // App-compat shims (register listener + auto-attach in detached mode)
10296
+ // App-compat shims. Each registration is scoped to ONE tunnel, returns an
10297
+ // id, and is undone by the matching unregister call. Before this, every
10298
+ // register appended a callback that saw every subscribed tunnel's events
10299
+ // and could never be removed, so a consumer that registered per page view
10300
+ // received each stats update N times, forever.
10301
+ //
10302
+ // The first registration for a tunnel opens its stream subscription and
10303
+ // the last unregister drops it with a plain unsubscribe. detach() is not
10304
+ // used here on purpose: it closes the client once nothing is subscribed,
10305
+ // which also stops the daemon-health heartbeat, and a stats consumer must
10306
+ // not have that side effect.
10235
10307
  handleRegisterStatsListener(tunnelId, listener) {
10236
- this.onStats(listener);
10237
- this.attach(tunnelId, "detached").catch(() => {
10238
- });
10308
+ const wrapped = (id, stats) => {
10309
+ if (id === tunnelId) listener(id, stats);
10310
+ };
10311
+ const listenerId = this.nextShimListenerId("stats");
10312
+ this.shimStatsListeners.set(listenerId, { tunnelId, wrapped });
10313
+ this.stream.onStats(wrapped);
10314
+ this.subscribeForShim(tunnelId);
10315
+ return listenerId;
10316
+ }
10317
+ handleUnregisterStatsListener(_tunnelId, listenerId) {
10318
+ const entry = this.shimStatsListeners.get(listenerId);
10319
+ if (!entry) return;
10320
+ this.shimStatsListeners.delete(listenerId);
10321
+ this.stream.offStats(entry.wrapped);
10322
+ this.releaseShimSubscription(entry.tunnelId);
10239
10323
  }
10240
10324
  handleRegisterDisconnectListener(tunnelId, listener) {
10241
- this.onDisconnect(listener);
10242
- this.attach(tunnelId, "detached").catch(() => {
10243
- });
10244
- }
10245
- handleUnregisterStatsListener(_tunnelId, _listenerId) {
10325
+ const wrapped = (id, error, messages) => {
10326
+ if (id === tunnelId) listener(id, error, messages);
10327
+ };
10328
+ const listenerId = this.nextShimListenerId("disconnect");
10329
+ this.shimDisconnectListeners.set(listenerId, { tunnelId, wrapped });
10330
+ this.stream.onDisconnect(wrapped);
10331
+ this.subscribeForShim(tunnelId);
10332
+ return listenerId;
10333
+ }
10334
+ handleUnregisterDisconnectListener(_tunnelId, listenerId) {
10335
+ const entry = this.shimDisconnectListeners.get(listenerId);
10336
+ if (!entry) return;
10337
+ this.shimDisconnectListeners.delete(listenerId);
10338
+ this.stream.offDisconnect(entry.wrapped);
10339
+ this.releaseShimSubscription(entry.tunnelId);
10246
10340
  }
10247
10341
  async handleGetTunnelStats(tunnelId) {
10248
10342
  this.assertClient();
10249
10343
  return this.ipc.getTunnelStats(tunnelId);
10250
10344
  }
10251
10345
  // Private
10346
+ nextShimListenerId(kind) {
10347
+ this.shimListenerSeq += 1;
10348
+ return `${kind}-${this.shimListenerSeq}`;
10349
+ }
10350
+ subscribeForShim(tunnelId) {
10351
+ if (!this.stream.isSubscribed(tunnelId)) this.shimSubscribedTunnels.add(tunnelId);
10352
+ this.attach(tunnelId, SessionMode.Detached).catch((err) => {
10353
+ logger.debug("Listener subscribe failed", { tunnelId, error: errorMessage(err) });
10354
+ });
10355
+ }
10356
+ releaseShimSubscription(tunnelId) {
10357
+ if (!this.shimSubscribedTunnels.has(tunnelId)) return;
10358
+ for (const entry of this.shimStatsListeners.values()) if (entry.tunnelId === tunnelId) return;
10359
+ for (const entry of this.shimDisconnectListeners.values()) if (entry.tunnelId === tunnelId) return;
10360
+ this.shimSubscribedTunnels.delete(tunnelId);
10361
+ this.stream.unsubscribe(tunnelId);
10362
+ }
10252
10363
  assertClient() {
10253
10364
  if (!this.ipc) {
10254
10365
  throw new Error("TunnelClient not initialized. Call ensureDaemon() first.");
package/dist/index.d.cts CHANGED
@@ -658,6 +658,7 @@ interface PingResponse {
658
658
  status: string;
659
659
  pid: number;
660
660
  uptime: number;
661
+ ipcVersion: number;
661
662
  }
662
663
  type ListTunnelsResponse = (TunnelResponseV2 & {
663
664
  mode?: SessionMode;
@@ -991,6 +992,7 @@ declare class WsStream {
991
992
  unsubscribe(tunnelId: string): boolean;
992
993
  hasSubscriptions(): boolean;
993
994
  subscriptionCount(): number;
995
+ isSubscribed(tunnelId: string): boolean;
994
996
  /** Snapshot of current subscriptions, for the reconnect path to replay. */
995
997
  snapshotSubscriptions(): Array<[string, SubscriptionInfo]>;
996
998
  /**
@@ -1002,6 +1004,8 @@ declare class WsStream {
1002
1004
  onClose(cb: (code: number) => void): void;
1003
1005
  onStats(cb: StatsCallback): void;
1004
1006
  onDisconnect(cb: DisconnectCallback): void;
1007
+ offStats(cb: StatsCallback): void;
1008
+ offDisconnect(cb: DisconnectCallback): void;
1005
1009
  onReconnecting(cb: ReconnectingCallback): void;
1006
1010
  onReconnected(cb: ReconnectedCallback): void;
1007
1011
  onReconnectionFailed(cb: ReconnectionFailedCallback): void;
@@ -1078,6 +1082,10 @@ declare class TunnelClient {
1078
1082
  private origin;
1079
1083
  private stream;
1080
1084
  health: DaemonHealth;
1085
+ private shimStatsListeners;
1086
+ private shimDisconnectListeners;
1087
+ private shimSubscribedTunnels;
1088
+ private shimListenerSeq;
1081
1089
  constructor(options?: TunnelClientOptions);
1082
1090
  ensureDaemon(): Promise<void>;
1083
1091
  static forRemoteManagement(): Promise<TunnelClient>;
@@ -1085,11 +1093,7 @@ declare class TunnelClient {
1085
1093
  * Close the WebSocket connection and cleanup.
1086
1094
  */
1087
1095
  close(): void;
1088
- ping(): Promise<{
1089
- status: string;
1090
- pid: number;
1091
- uptime: number;
1092
- }>;
1096
+ ping(): Promise<PingResponse>;
1093
1097
  handleStartV2(config: object, noWait?: boolean, mode?: SessionMode): Promise<TunnelResponseV2 | ErrorResponse>;
1094
1098
  handleStart(config: object, noWait?: boolean, mode?: SessionMode): Promise<TunnelResponse | ErrorResponse>;
1095
1099
  handleUpdateConfig(config: object, noWait?: boolean): Promise<TunnelResponse | ErrorResponse>;
@@ -1119,6 +1123,8 @@ declare class TunnelClient {
1119
1123
  detach(tunnelId: string): void;
1120
1124
  onStats(cb: StatsCallback): void;
1121
1125
  onDisconnect(cb: DisconnectCallback): void;
1126
+ offStats(cb: StatsCallback): void;
1127
+ offDisconnect(cb: DisconnectCallback): void;
1122
1128
  onReconnecting(cb: ReconnectingCallback): void;
1123
1129
  onReconnected(cb: ReconnectedCallback): void;
1124
1130
  onReconnectionFailed(cb: ReconnectionFailedCallback): void;
@@ -1131,10 +1137,14 @@ declare class TunnelClient {
1131
1137
  onDaemonReconnecting(cb: DaemonReconnectingCallback): void;
1132
1138
  onDaemonReconnected(cb: DaemonReconnectedCallback): void;
1133
1139
  isDaemonLost(): boolean;
1134
- handleRegisterStatsListener(tunnelId: string, listener: (tunnelId: string, stats: TunnelUsageType) => void): void;
1135
- handleRegisterDisconnectListener(tunnelId: string, listener: (tunnelId: string, error: string, messages: string[]) => void): void;
1136
- handleUnregisterStatsListener(_tunnelId: string, _listenerId: string): void;
1140
+ handleRegisterStatsListener(tunnelId: string, listener: StatsCallback): string;
1141
+ handleUnregisterStatsListener(_tunnelId: string, listenerId: string): void;
1142
+ handleRegisterDisconnectListener(tunnelId: string, listener: DisconnectCallback): string;
1143
+ handleUnregisterDisconnectListener(_tunnelId: string, listenerId: string): void;
1137
1144
  handleGetTunnelStats(tunnelId: string): Promise<TunnelUsageType[] | ErrorResponse>;
1145
+ private nextShimListenerId;
1146
+ private subscribeForShim;
1147
+ private releaseShimSubscription;
1138
1148
  private assertClient;
1139
1149
  }
1140
1150
 
@@ -1143,6 +1153,8 @@ interface DaemonInfo {
1143
1153
  port: number;
1144
1154
  startedAt: string;
1145
1155
  host?: DaemonHost;
1156
+ /** IPC protocol version the daemon speaks. Absent in daemon.json written by older builds. */
1157
+ ipcVersion?: number;
1146
1158
  }
1147
1159
 
1148
1160
  /**
package/dist/index.d.ts CHANGED
@@ -658,6 +658,7 @@ interface PingResponse {
658
658
  status: string;
659
659
  pid: number;
660
660
  uptime: number;
661
+ ipcVersion: number;
661
662
  }
662
663
  type ListTunnelsResponse = (TunnelResponseV2 & {
663
664
  mode?: SessionMode;
@@ -991,6 +992,7 @@ declare class WsStream {
991
992
  unsubscribe(tunnelId: string): boolean;
992
993
  hasSubscriptions(): boolean;
993
994
  subscriptionCount(): number;
995
+ isSubscribed(tunnelId: string): boolean;
994
996
  /** Snapshot of current subscriptions, for the reconnect path to replay. */
995
997
  snapshotSubscriptions(): Array<[string, SubscriptionInfo]>;
996
998
  /**
@@ -1002,6 +1004,8 @@ declare class WsStream {
1002
1004
  onClose(cb: (code: number) => void): void;
1003
1005
  onStats(cb: StatsCallback): void;
1004
1006
  onDisconnect(cb: DisconnectCallback): void;
1007
+ offStats(cb: StatsCallback): void;
1008
+ offDisconnect(cb: DisconnectCallback): void;
1005
1009
  onReconnecting(cb: ReconnectingCallback): void;
1006
1010
  onReconnected(cb: ReconnectedCallback): void;
1007
1011
  onReconnectionFailed(cb: ReconnectionFailedCallback): void;
@@ -1078,6 +1082,10 @@ declare class TunnelClient {
1078
1082
  private origin;
1079
1083
  private stream;
1080
1084
  health: DaemonHealth;
1085
+ private shimStatsListeners;
1086
+ private shimDisconnectListeners;
1087
+ private shimSubscribedTunnels;
1088
+ private shimListenerSeq;
1081
1089
  constructor(options?: TunnelClientOptions);
1082
1090
  ensureDaemon(): Promise<void>;
1083
1091
  static forRemoteManagement(): Promise<TunnelClient>;
@@ -1085,11 +1093,7 @@ declare class TunnelClient {
1085
1093
  * Close the WebSocket connection and cleanup.
1086
1094
  */
1087
1095
  close(): void;
1088
- ping(): Promise<{
1089
- status: string;
1090
- pid: number;
1091
- uptime: number;
1092
- }>;
1096
+ ping(): Promise<PingResponse>;
1093
1097
  handleStartV2(config: object, noWait?: boolean, mode?: SessionMode): Promise<TunnelResponseV2 | ErrorResponse>;
1094
1098
  handleStart(config: object, noWait?: boolean, mode?: SessionMode): Promise<TunnelResponse | ErrorResponse>;
1095
1099
  handleUpdateConfig(config: object, noWait?: boolean): Promise<TunnelResponse | ErrorResponse>;
@@ -1119,6 +1123,8 @@ declare class TunnelClient {
1119
1123
  detach(tunnelId: string): void;
1120
1124
  onStats(cb: StatsCallback): void;
1121
1125
  onDisconnect(cb: DisconnectCallback): void;
1126
+ offStats(cb: StatsCallback): void;
1127
+ offDisconnect(cb: DisconnectCallback): void;
1122
1128
  onReconnecting(cb: ReconnectingCallback): void;
1123
1129
  onReconnected(cb: ReconnectedCallback): void;
1124
1130
  onReconnectionFailed(cb: ReconnectionFailedCallback): void;
@@ -1131,10 +1137,14 @@ declare class TunnelClient {
1131
1137
  onDaemonReconnecting(cb: DaemonReconnectingCallback): void;
1132
1138
  onDaemonReconnected(cb: DaemonReconnectedCallback): void;
1133
1139
  isDaemonLost(): boolean;
1134
- handleRegisterStatsListener(tunnelId: string, listener: (tunnelId: string, stats: TunnelUsageType) => void): void;
1135
- handleRegisterDisconnectListener(tunnelId: string, listener: (tunnelId: string, error: string, messages: string[]) => void): void;
1136
- handleUnregisterStatsListener(_tunnelId: string, _listenerId: string): void;
1140
+ handleRegisterStatsListener(tunnelId: string, listener: StatsCallback): string;
1141
+ handleUnregisterStatsListener(_tunnelId: string, listenerId: string): void;
1142
+ handleRegisterDisconnectListener(tunnelId: string, listener: DisconnectCallback): string;
1143
+ handleUnregisterDisconnectListener(_tunnelId: string, listenerId: string): void;
1137
1144
  handleGetTunnelStats(tunnelId: string): Promise<TunnelUsageType[] | ErrorResponse>;
1145
+ private nextShimListenerId;
1146
+ private subscribeForShim;
1147
+ private releaseShimSubscription;
1138
1148
  private assertClient;
1139
1149
  }
1140
1150
 
@@ -1143,6 +1153,8 @@ interface DaemonInfo {
1143
1153
  port: number;
1144
1154
  startedAt: string;
1145
1155
  host?: DaemonHost;
1156
+ /** IPC protocol version the daemon speaks. Absent in daemon.json written by older builds. */
1157
+ ipcVersion?: number;
1146
1158
  }
1147
1159
 
1148
1160
  /**
package/dist/index.js CHANGED
@@ -14,8 +14,8 @@ import {
14
14
  initiateRemoteManagement,
15
15
  isDaemonRunning,
16
16
  startRemoteManagement
17
- } from "./chunk-HN4YLM7U.js";
18
- import "./chunk-YTJEV27N.js";
17
+ } from "./chunk-B6IKYBLU.js";
18
+ import "./chunk-I4HXBP5G.js";
19
19
  import {
20
20
  bulkReplace,
21
21
  configureStorageLogger,
@@ -139,7 +139,7 @@ async function verifyAndLoad() {
139
139
  process.exit(1);
140
140
  }
141
141
  }
142
- await import("./main-WXFARRMN.js");
142
+ await import("./main-VBWX7HTB.js");
143
143
  }
144
144
  verifyAndLoad().catch((err) => {
145
145
  printer_default.fatal(`Failed to start CLI:, ${err}`);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  IPCClient
3
- } from "./chunk-YTJEV27N.js";
3
+ } from "./chunk-I4HXBP5G.js";
4
4
  export {
5
5
  IPCClient
6
6
  };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runDaemonChild
4
- } from "./chunk-OH6FSLYF.js";
4
+ } from "./chunk-P7XRY5N4.js";
5
5
  import {
6
6
  RemoteManagementUnauthorizedError,
7
7
  TunnelOperations,
@@ -13,11 +13,11 @@ import {
13
13
  getRemoteManagementState,
14
14
  initiateRemoteManagement,
15
15
  isDaemonRunning
16
- } from "./chunk-HN4YLM7U.js";
16
+ } from "./chunk-B6IKYBLU.js";
17
17
  import "./chunk-MT44NAXX.js";
18
18
  import {
19
19
  DaemonHost
20
- } from "./chunk-YTJEV27N.js";
20
+ } from "./chunk-I4HXBP5G.js";
21
21
  import "./chunk-YWXADADB.js";
22
22
  import {
23
23
  TunnelManager
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinggy",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "description": "Create secure, shareable tunnels to your localhost and manage them from the command line. ",
@@ -52,7 +52,7 @@
52
52
  ]
53
53
  },
54
54
  "dependencies": {
55
- "@pinggy/pinggy": "^0.5.1",
55
+ "@pinggy/pinggy": "^0.5.2",
56
56
  "blessed": "^0.1.81",
57
57
  "clipboardy": "^5.0.0",
58
58
  "mime": "^4.1.0",