pinggy 0.6.0 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,6 +14,7 @@ Create secure, shareable tunnels to your localhost and manage them from the comm
14
14
  - Per-tunnel and per-daemon log files with `pinggy logs` (tail, follow, rotation-safe)
15
15
  - System-service install for auto-start at boot (systemd, launchd, Task Scheduler)
16
16
  - Remote management via secure WebSocket connection (works with Pinggy Dashboard)
17
+ - Pinggy Devices: enrol the machine with `pinggy device` and keep it visible in the dashboard
17
18
  - Save and load configuration files
18
19
  - Config store for saving, listing, updating, and starting named tunnel configs
19
20
  - Auto-start support for launching saved tunnels automatically
@@ -226,6 +227,92 @@ You can control tunnels remotely using a secure WebSocket connection.
226
227
  ```
227
228
 
228
229
 
230
+ ## Pinggy Devices
231
+
232
+ Enrol this machine as a Pinggy device. The agent opens 1 WebSocket to the dashboard, registers the machine, and holds the connection open so the dashboard can show the device as online.
233
+
234
+ Devices and remote management are separate features. Remote management drives tunnels from the dashboard with an API key. A device is the machine itself, enrolled with its own token. The device agent owns no tunnels, and stopping it leaves every running tunnel alone.
235
+
236
+ The agent runs in the foreground CLI process, not the daemon. It needs no daemon and starts none.
237
+
238
+ ### Connect a device
239
+ ```bash
240
+ pinggy device connect --token <TOKEN>
241
+ ```
242
+
243
+ Add the device in the dashboard first, then pass the token it gives you. The command stays in the foreground and reconnects on its own until you interrupt it.
244
+
245
+ The token is written to disk on the first successful connect, so later runs need no flag:
246
+ ```bash
247
+ pinggy device connect
248
+ ```
249
+
250
+ Point the agent at a different dashboard with `--manage`:
251
+ ```bash
252
+ pinggy device connect --token <TOKEN> --manage dashboard.pinggy.io
253
+ ```
254
+
255
+ `--manage` takes a host. The scheme becomes `wss://` unless you type `ws://` or `wss://` yourself. The agent connects to `/backend/api/v1/device-agent/ws/connect` on that host and sends the token in the `X-Pinggy-Device-Token` header.
256
+
257
+ `--token` here is the device token, not the tunnel token of the same flag name in [Options](#options). The 2 credentials are unrelated and the device agent never reads the tunnel one.
258
+
259
+ ### Show what this machine remembers
260
+ ```bash
261
+ pinggy device status
262
+ ```
263
+
264
+ Prints the device id, server, masked token, and enrolment time from the local file. It contacts nothing.
265
+
266
+ ### Forget the local credential
267
+ ```bash
268
+ pinggy device remove
269
+ ```
270
+
271
+ Deletes the local credential only. The device still exists in the dashboard. Delete it there to revoke it, because a machine that has lost its credential cannot authenticate the delete.
272
+
273
+ ### Reconnect and stop conditions
274
+
275
+ The agent retries a dropped connection every 5 seconds. Some failures are terminal and it exits instead:
276
+
277
+ | Event | Agent |
278
+ |-------|-------|
279
+ | HTTP 401 on the upgrade | Stops. The token is wrong or revoked. Re-enrol from the dashboard |
280
+ | Dashboard refuses the handshake | Stops, and prints the reason |
281
+ | Device is connected from another machine | Retries. The credential is fine, the other socket has to drop first |
282
+ | Dashboard says it is shutting down | Retries |
283
+ | Any other close, or a network error | Retries |
284
+
285
+ Ctrl+C stops the retry loop. The agent exits once the current socket closes; a second Ctrl+C exits at once.
286
+
287
+ ### What the agent does today
288
+
289
+ It enrols, learns its device id and heartbeat interval from the dashboard, and sends a heartbeat on that interval. CPU and memory reporting, and the browser terminal, are not built yet.
290
+
291
+ ### Combining `device` with other commands
292
+
293
+ 1 invocation runs 1 command. The first word decides which, and everything after it belongs to that command. `pinggy device connect start my-tunnel` runs the agent and ignores `start my-tunnel`; it does not start a tunnel.
294
+
295
+ | Command | What happens |
296
+ |---------|--------------|
297
+ | `pinggy device connect -l 3000 -b --all --remote-management <KEY>` | Runs the agent. Those flags parse and are ignored. No tunnel, no remote management, no daemon. Only `--token` and `--manage` are read |
298
+ | `pinggy device start`, `pinggy device daemon stop` | Error, exit 1. The verb after `device` is `login`, `connect`, `stop`, `status`, or `remove` |
299
+ | `pinggy start device`, `pinggy stop device` | `device` reads as a tunnel name. No new config can be called `device`, so nothing matches |
300
+ | `pinggy config save device ...` | Rejected. `device` is a reserved name |
301
+ | `pinggy -l 3000 device` | An ordinary tunnel. `device` is not a valid domain, so it warns `Unknown extended option` and is ignored |
302
+ | `pinggy device connect --typo` | Unknown options are rejected before anything runs |
303
+ | `pinggy devices connect`, or `devices` with anything | Error, exit 1: `"pinggy devices" is now "pinggy device". Run: pinggy device connect`. Nothing runs |
304
+
305
+ `pinggy device` and `pinggy device --help` print the command list. After a verb, `--help` is ignored, so `pinggy device connect --help` tries to connect.
306
+
307
+ ### Devices, tunnels, and the daemon do not interfere
308
+
309
+ - The agent is its own process. `pinggy daemon stop` leaves it running, and Ctrl+C on the agent leaves every tunnel running.
310
+ - `pinggy device connect` never starts the daemon. Run tunnels and the agent side by side in 2 terminals.
311
+ - 1 enrolment per machine. The newest successful connect overwrites `device.json`, and a `--token` on the command line beats the stored one.
312
+ - Running the agent twice on 1 machine does not work. The dashboard refuses the second connection because the device is already connected, and that copy keeps retrying until the first one drops.
313
+ - `pinggy device remove` deletes the credential file but does not stop a running agent, which rewrites the file the next time it connects. Stop the agent first.
314
+
315
+
229
316
  ## Usage
230
317
  Basic syntax:
231
318
  pinggy [options] [user@domain]
@@ -403,6 +490,8 @@ Config dir varies by OS:
403
490
  - Linux/macOS: `~/.config/pinggy/`
404
491
  - Windows: `%APPDATA%\pinggy\`
405
492
 
493
+ It holds `daemon.json` (discovery), `daemon-config.json` (persisted settings), `daemon-state.json` (crash recovery), `tunnels/` (saved configs), and `device.json` (the Pinggy Devices credential, written `0600`).
494
+
406
495
  Log dir varies by OS:
407
496
  - Linux: `~/.local/state/pinggy-cli/logs/` (honors `$XDG_STATE_HOME`)
408
497
  - macOS: `~/Library/Logs/Pinggy-CLI/`
@@ -421,6 +510,7 @@ Optionally combine with other flags (auth, IP whitelist) as needed.
421
510
  - **Foreground tunnel**: Ctrl+C closes the TUI. The daemon arms a 5-second grace timer and stops the tunnel if no other CLI re-attaches.
422
511
  - **Detached tunnel** (`-b`): the CLI already exited. Stop it with `pinggy stop <name|id>`.
423
512
  - **Everything at once**: `pinggy daemon stop` stops every tunnel and shuts the daemon down cleanly. `daemon-state.json` is cleared, so nothing replays on next start.
513
+ - **Device agent** (`pinggy device connect`): Ctrl+C ends the agent. It runs outside the daemon, so no tunnel is affected.
424
514
 
425
515
 
426
516
  ## Versioning
@@ -0,0 +1,13 @@
1
+ import {
2
+ TunnelAlreadyRunningError,
3
+ TunnelManager,
4
+ TunnelRebuildInProgressError
5
+ } from "./chunk-QNUVQ6KD.js";
6
+ import "./chunk-UB26QJ4T.js";
7
+ import "./chunk-MV56U3E6.js";
8
+ import "./chunk-AFMTIELL.js";
9
+ export {
10
+ TunnelAlreadyRunningError,
11
+ TunnelManager,
12
+ TunnelRebuildInProgressError
13
+ };
@@ -1,30 +1,18 @@
1
+ import {
2
+ createQrCodes
3
+ } from "./chunk-XZ64ZAGO.js";
1
4
  import {
2
5
  TunnelManager
3
- } from "./chunk-J2U6P376.js";
6
+ } from "./chunk-QNUVQ6KD.js";
4
7
  import "./chunk-UB26QJ4T.js";
5
8
  import {
6
9
  logger
7
- } from "./chunk-76SOSXXR.js";
8
- import "./chunk-GBYF2H4H.js";
10
+ } from "./chunk-MV56U3E6.js";
11
+ import "./chunk-AFMTIELL.js";
9
12
 
10
13
  // src/tui/blessed/TunnelTui.ts
11
14
  import blessed3 from "blessed";
12
15
 
13
- // src/tui/blessed/qrCodeGenerator.ts
14
- import QRCode from "qrcode";
15
- async function createQrCodes(urls) {
16
- const codes = [];
17
- for (const url of urls) {
18
- const raw = await QRCode.toString(url, {
19
- type: "utf8",
20
- margin: 2,
21
- errorCorrectionLevel: "L"
22
- });
23
- codes.push(raw);
24
- }
25
- return codes;
26
- }
27
-
28
16
  // src/tui/blessed/webDebuggerConnection.ts
29
17
  import WebSocket from "ws";
30
18
 
@@ -20,7 +20,10 @@ var Route = {
20
20
  SetLogLevel: "POST /loglevel",
21
21
  GetTunnelLogging: "GET /config/tunnel-logging",
22
22
  SetTunnelLogging: "POST /config/tunnel-logging",
23
- GetLogPaths: "GET /logs/paths"
23
+ GetLogPaths: "GET /logs/paths",
24
+ GetDeviceAgent: "GET /device",
25
+ StartDeviceAgent: "POST /device/start",
26
+ StopDeviceAgent: "POST /device/stop"
24
27
  };
25
28
  var DaemonHost = {
26
29
  CLI: "cli",
@@ -30,6 +33,12 @@ var ShutdownStatus = {
30
33
  ShuttingDown: "shutting_down",
31
34
  RefusedInApp: "refused_in_app"
32
35
  };
36
+ var DeviceAgentState = {
37
+ Stopped: "stopped",
38
+ Connecting: "connecting",
39
+ Connected: "connected",
40
+ Reconnecting: "reconnecting"
41
+ };
33
42
  var SessionMode = {
34
43
  Foreground: "foreground",
35
44
  Detached: "detached"
@@ -37,6 +46,14 @@ var SessionMode = {
37
46
 
38
47
  // src/daemon/ipc/ipcClient.ts
39
48
  var REQUEST_TIMEOUT_MS = 1e4;
49
+ var IPCHttpError = class extends Error {
50
+ constructor(statusCode, message) {
51
+ super(message);
52
+ this.statusCode = statusCode;
53
+ this.name = "IPCHttpError";
54
+ }
55
+ };
56
+ var HTTP_NOT_FOUND = 404;
40
57
  var IPCClient = class {
41
58
  constructor(port, origin = "cli") {
42
59
  this.port = port;
@@ -103,6 +120,15 @@ var IPCClient = class {
103
120
  async resolveLogPath(q) {
104
121
  return this.request("GET", `/logs/resolve?q=${encodeURIComponent(q)}`);
105
122
  }
123
+ async getDeviceAgentStatus(timeoutMs) {
124
+ return this.call(Route.GetDeviceAgent, void 0, timeoutMs);
125
+ }
126
+ async startDeviceAgent() {
127
+ return this.call(Route.StartDeviceAgent, {});
128
+ }
129
+ async stopDeviceAgent() {
130
+ return this.call(Route.StopDeviceAgent, {});
131
+ }
106
132
  /**
107
133
  * Get the WebSocket URL for event streaming.
108
134
  */
@@ -144,7 +170,10 @@ var IPCClient = class {
144
170
  const text = Buffer.concat(chunks).toString("utf-8");
145
171
  const statusCode = res.statusCode ?? 0;
146
172
  if (statusCode < 200 || statusCode >= 300) {
147
- reject(new Error(`Daemon returned HTTP ${statusCode}: ${text.slice(0, 200)}`));
173
+ reject(new IPCHttpError(
174
+ statusCode,
175
+ `Daemon returned HTTP ${statusCode}: ${text.slice(0, 200)}`
176
+ ));
148
177
  return;
149
178
  }
150
179
  try {
@@ -171,6 +200,9 @@ export {
171
200
  Route,
172
201
  DaemonHost,
173
202
  ShutdownStatus,
203
+ DeviceAgentState,
174
204
  SessionMode,
205
+ IPCHttpError,
206
+ HTTP_NOT_FOUND,
175
207
  IPCClient
176
208
  };
@@ -57,6 +57,9 @@ function getTunnelLogPath(tunnelId, origin, name) {
57
57
  function getDaemonLogPath() {
58
58
  return path.join(getPinggyLogDir(), "daemon.log");
59
59
  }
60
+ function getDeviceConfigPath() {
61
+ return path.join(getPinggyConfigDir(), "device.json");
62
+ }
60
63
  function ensurePinggyConfigDir() {
61
64
  const dir = getPinggyConfigDir();
62
65
  fs.mkdirSync(dir, { recursive: true });
@@ -73,5 +76,6 @@ export {
73
76
  getTunnelLogDir,
74
77
  getTunnelLogPath,
75
78
  getDaemonLogPath,
79
+ getDeviceConfigPath,
76
80
  ensurePinggyConfigDir
77
81
  };
@@ -4,130 +4,32 @@ import {
4
4
  IPC_VERSION,
5
5
  SessionMode,
6
6
  ShutdownStatus
7
- } from "./chunk-I4HXBP5G.js";
7
+ } from "./chunk-7EQKHVQX.js";
8
8
  import {
9
+ ErrorCode,
10
+ NewErrorResponseObject,
11
+ NewResponseObject,
12
+ RemoteManagementStatus,
9
13
  TunnelAlreadyRunningError,
10
14
  TunnelManager,
15
+ TunnelRebuildInProgressError,
11
16
  errorMessage,
12
17
  getLocalAddress,
13
18
  getVersion,
19
+ isErrorResponse,
14
20
  isValidPort,
21
+ newErrorResponse,
22
+ newStats,
23
+ newStatus,
15
24
  printer_default
16
- } from "./chunk-J2U6P376.js";
25
+ } from "./chunk-QNUVQ6KD.js";
17
26
  import {
18
27
  logger
19
- } from "./chunk-76SOSXXR.js";
28
+ } from "./chunk-MV56U3E6.js";
20
29
  import {
21
30
  getDaemonInfoPath,
22
31
  getDaemonLogPath
23
- } from "./chunk-GBYF2H4H.js";
24
-
25
- // src/types.ts
26
- var TunnelStateType = /* @__PURE__ */ ((TunnelStateType2) => {
27
- TunnelStateType2["New"] = "idle";
28
- TunnelStateType2["Starting"] = "starting";
29
- TunnelStateType2["Running"] = "running";
30
- TunnelStateType2["Live"] = "live";
31
- TunnelStateType2["Closed"] = "closed";
32
- TunnelStateType2["Exited"] = "exited";
33
- return TunnelStateType2;
34
- })(TunnelStateType || {});
35
- var TunnelErrorCodeType = /* @__PURE__ */ ((TunnelErrorCodeType2) => {
36
- TunnelErrorCodeType2["NonResponsive"] = "non_responsive";
37
- TunnelErrorCodeType2["FailedToConnect"] = "failed_to_connect";
38
- TunnelErrorCodeType2["ErrorInAdditionalForwarding"] = "additional_forwarding_error";
39
- TunnelErrorCodeType2["WebdebuggerError"] = "webdebugger_error";
40
- TunnelErrorCodeType2["NoError"] = "";
41
- return TunnelErrorCodeType2;
42
- })(TunnelErrorCodeType || {});
43
- var TunnelWarningCode = /* @__PURE__ */ ((TunnelWarningCode2) => {
44
- TunnelWarningCode2["InvalidTunnelServePath"] = "INVALID_TUNNEL_SERVE_PATH";
45
- TunnelWarningCode2["UnknownWarning"] = "UNKNOWN_WARNING";
46
- return TunnelWarningCode2;
47
- })(TunnelWarningCode || {});
48
- var ErrorCode = {
49
- InvalidRequestMethodError: "INVALID_REQUEST_METHOD",
50
- InvalidRequestBodyError: "COULD_NOT_READ_BODY",
51
- InternalServerError: "INTERNAL_SERVER_ERROR",
52
- InvalidBodyFormatError: "INVALID_DATA_FORMAT",
53
- ErrorStartingTunnel: "ERROR_STARTING_TUNNEL",
54
- TunnelNotFound: "TUNNEL_WITH_ID_OR_CONFIG_ID_NOT_FOUND",
55
- TunnelAlreadyRunningError: "TUNNEL_WITH_ID_OR_CONFIG_ID_ALREADY_RUNNING",
56
- WebsocketUpgradeFailError: "WEBSOCKET_UPGRADE_FAILED",
57
- RemoteManagementAlreadyRunning: "REMOTE_MANAGEMENT_ALREADY_RUNNING",
58
- RemoteManagementNotRunning: "REMOTE_MANAGEMENT_NOT_RUNNING",
59
- RemoteManagementDeserializationFailed: "REMOTE_MANAGEMENT_DESERIALIZATION_FAILED"
60
- };
61
- function isErrorResponse(obj) {
62
- return typeof obj === "object" && obj !== null && "code" in obj && "message" in obj && typeof obj.message === "string" && Object.values(ErrorCode).includes(obj.code);
63
- }
64
- function newErrorResponse(codeOrError, message) {
65
- if (typeof codeOrError === "object") {
66
- return codeOrError;
67
- }
68
- return {
69
- code: codeOrError,
70
- message
71
- };
72
- }
73
- function NewResponseObject(data) {
74
- const encoder = new TextEncoder();
75
- const bytes = encoder.encode(JSON.stringify(data));
76
- return {
77
- response: bytes,
78
- requestid: "",
79
- command: "",
80
- error: false,
81
- errorresponse: {}
82
- };
83
- }
84
- function NewErrorResponseObject(errorResponse) {
85
- return {
86
- response: new Uint8Array(),
87
- requestid: "",
88
- command: "",
89
- error: true,
90
- errorresponse: errorResponse
91
- };
92
- }
93
- function newStatus(tunnelState, errorCode, errorMsg) {
94
- let assignedState = tunnelState;
95
- if (tunnelState === "live" /* Live */) {
96
- assignedState = "running" /* Running */;
97
- } else if (tunnelState === "idle" /* New */) {
98
- assignedState = "idle" /* New */;
99
- } else if (tunnelState === "closed" /* Closed */) {
100
- assignedState = "exited" /* Exited */;
101
- }
102
- const now = (/* @__PURE__ */ new Date()).toISOString();
103
- return {
104
- state: assignedState,
105
- errorcode: errorCode,
106
- errormsg: errorMsg,
107
- createdtimestamp: now,
108
- starttimestamp: now,
109
- endtimestamp: now,
110
- warnings: []
111
- };
112
- }
113
- function newStats() {
114
- return {
115
- numLiveConnections: 0,
116
- numTotalConnections: 0,
117
- numTotalReqBytes: 0,
118
- numTotalResBytes: 0,
119
- numTotalTxBytes: 0,
120
- elapsedTime: 0
121
- };
122
- }
123
- var RemoteManagementStatus = {
124
- Connecting: "CONNECTING",
125
- Disconnecting: "DISCONNECTING",
126
- Reconnecting: "RECONNECTING",
127
- Running: "RUNNING",
128
- NotRunning: "NOT_RUNNING",
129
- Error: "ERROR"
130
- };
32
+ } from "./chunk-AFMTIELL.js";
131
33
 
132
34
  // src/remote_management/remote_schema.ts
133
35
  import { HaProxyVersion, TunnelType } from "@pinggy/pinggy";
@@ -428,6 +330,9 @@ var TunnelOperations = class {
428
330
  }
429
331
  if (managed?.lastError) {
430
332
  status.lastError = managed.lastError;
333
+ if (managed.lastError.isFatal) {
334
+ status.errormsg = managed.lastError.message;
335
+ }
431
336
  }
432
337
  } catch (e) {
433
338
  }
@@ -492,6 +397,14 @@ var TunnelOperations = class {
492
397
  message: err instanceof Error ? err.message : fallback
493
398
  });
494
399
  }
400
+ /**
401
+ * A restart or update of a tunnel another one is still rebuilding. A no-wait call checks this before
402
+ * it answers, because a throw from the call it does not await is only logged.
403
+ */
404
+ rebuildInProgress(tunnelid) {
405
+ const err = new TunnelRebuildInProgressError(tunnelid);
406
+ return this.error(ErrorCode.TunnelAlreadyRunningError, err, err.message);
407
+ }
495
408
  // --- Operations ---
496
409
  async handleStart(config, noWait = false, origin = "cli") {
497
410
  try {
@@ -554,6 +467,7 @@ var TunnelOperations = class {
554
467
  if (noWait) {
555
468
  const existing = this.tunnelManager.getManagedTunnel(config.configid);
556
469
  if (!existing.tunnelConfig) throw new Error("Invalid tunnel state before configuration update");
470
+ if (existing.rebuild) return this.rebuildInProgress(existing.tunnelid);
557
471
  if (!existing.isStopped) {
558
472
  this.tunnelManager.updateConfig(updateOpts).catch((err) => {
559
473
  logger.error("No-wait updateConfig failed", { configid: config.configid, err: String(err) });
@@ -574,6 +488,7 @@ var TunnelOperations = class {
574
488
  if (noWait) {
575
489
  const existing = this.tunnelManager.getManagedTunnel(config.configId);
576
490
  if (!existing.tunnelConfig) throw new Error("Invalid tunnel state before configuration update");
491
+ if (existing.rebuild) return this.rebuildInProgress(existing.tunnelid);
577
492
  if (!existing.isStopped) {
578
493
  this.tunnelManager.updateConfig(config).catch((err) => {
579
494
  logger.error("No-wait updateConfigV2 failed", { configId: config.configId, err: String(err) });
@@ -672,6 +587,7 @@ var TunnelOperations = class {
672
587
  if (noWait) {
673
588
  const managed2 = this.tunnelManager.getManagedTunnel("", tunnelid);
674
589
  if (!managed2?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
590
+ if (managed2.rebuild) return this.rebuildInProgress(tunnelid);
675
591
  this.tunnelManager.restartTunnel(tunnelid).catch((err) => {
676
592
  logger.error("No-wait restartTunnel failed", { tunnelid, err: String(err) });
677
593
  });
@@ -682,6 +598,9 @@ var TunnelOperations = class {
682
598
  if (!managed?.tunnelConfig) throw new Error(`Tunnel config for ID "${tunnelid}" not found`);
683
599
  return this.buildTunnelResponse(tunnelid, managed.tunnelConfig, managed.configId, managed.tunnelName, managed.serve);
684
600
  } catch (err) {
601
+ if (err instanceof TunnelRebuildInProgressError) {
602
+ return this.rebuildInProgress(tunnelid);
603
+ }
685
604
  return this.error(ErrorCode.TunnelNotFound, err, "Failed to restart tunnel");
686
605
  }
687
606
  }
@@ -1682,7 +1601,7 @@ async function ensureDaemonRunning() {
1682
1601
  return existing;
1683
1602
  }
1684
1603
  if (process.versions.electron) {
1685
- const { runDaemonChild } = await import("./daemonChild-CV2UTL6T.js");
1604
+ const { runDaemonChild } = await import("./daemonChild-E73NFWKE.js");
1686
1605
  const handle = await runDaemonChild({
1687
1606
  installSignalHandlers: false,
1688
1607
  exitOnFailure: false,
@@ -1705,7 +1624,7 @@ function getInProcessDaemonHandle() {
1705
1624
  async function getActiveTunnelSummaries(origin = "app") {
1706
1625
  const info = getDaemonInfo();
1707
1626
  if (!info) return [];
1708
- const { IPCClient: IPCClient2 } = await import("./ipcClient-URHJKNEH.js");
1627
+ const { IPCClient: IPCClient2 } = await import("./ipcClient-XGF2KCQO.js");
1709
1628
  const client = new IPCClient2(info.port, origin);
1710
1629
  let tunnels;
1711
1630
  try {
@@ -1752,7 +1671,7 @@ async function stopDaemon() {
1752
1671
  if (!info) return { ok: false, error: "No daemon is running." };
1753
1672
  let daemonErrors = [];
1754
1673
  try {
1755
- const { IPCClient: IPCClient2 } = await import("./ipcClient-URHJKNEH.js");
1674
+ const { IPCClient: IPCClient2 } = await import("./ipcClient-XGF2KCQO.js");
1756
1675
  const client = new IPCClient2(info.port);
1757
1676
  const result = await client.shutdown();
1758
1677
  logger.debug("Sent shutdown command to daemon", { result });
@@ -2297,6 +2216,20 @@ var TunnelClient = class _TunnelClient {
2297
2216
  this.assertClient();
2298
2217
  await this.ipc.restartTunnel(tunnelId);
2299
2218
  }
2219
+ // Device agent (`pinggy device connect -b`). The daemon reads the token and the server from
2220
+ // device.json, so none of these takes either.
2221
+ async getDeviceAgentStatus() {
2222
+ this.assertClient();
2223
+ return this.ipc.getDeviceAgentStatus();
2224
+ }
2225
+ async startDeviceAgent() {
2226
+ this.assertClient();
2227
+ return this.ipc.startDeviceAgent();
2228
+ }
2229
+ async stopDeviceAgent() {
2230
+ this.assertClient();
2231
+ return this.ipc.stopDeviceAgent();
2232
+ }
2300
2233
  // Streaming — delegate to WsStream, guarded by daemon-lost
2301
2234
  async attach(tunnelId, mode = SessionMode.Foreground) {
2302
2235
  if (this.health.isLost()) return;
@@ -2432,11 +2365,6 @@ var TunnelClient = class _TunnelClient {
2432
2365
  };
2433
2366
 
2434
2367
  export {
2435
- TunnelStateType,
2436
- TunnelErrorCodeType,
2437
- TunnelWarningCode,
2438
- ErrorCode,
2439
- isErrorResponse,
2440
2368
  TunnelConfigV1Schema,
2441
2369
  TUNNEL_CONFIG_V1_KEYS,
2442
2370
  TunnelOperations,
@@ -14,9 +14,9 @@ function setLogLevel(level) {
14
14
  if (_sdkLogFilePath) {
15
15
  enableLoggingByLogLevelInSdk(level, _sdkLogFilePath);
16
16
  }
17
- import("./daemonConfig-G6S46GPJ.js").then(({ writeDaemonConfig }) => writeDaemonConfig({ logLevel: level })).catch(() => {
17
+ import("./daemonConfig-QVXQ47XH.js").then(({ writeDaemonConfig }) => writeDaemonConfig({ logLevel: level })).catch(() => {
18
18
  });
19
- import("./TunnelManager-I3NW6UXU.js").then(({ TunnelManager }) => TunnelManager.getInstance().applyLogLevelToActiveTunnels(level)).catch(() => {
19
+ import("./TunnelManager-NGSUBZYI.js").then(({ TunnelManager }) => TunnelManager.getInstance().applyLogLevelToActiveTunnels(level)).catch(() => {
20
20
  });
21
21
  }
22
22
  var _logger = null;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ensurePinggyConfigDir,
3
3
  getDaemonConfigPath
4
- } from "./chunk-GBYF2H4H.js";
4
+ } from "./chunk-AFMTIELL.js";
5
5
 
6
6
  // src/daemon/lifecycle/daemonConfig.ts
7
7
  import fs from "fs";