pinggy 0.5.5 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +596 -0
- package/CLAUDE.md +1 -1
- package/README.md +3 -3
- package/dist/{chunk-NBR2L6LW.js → chunk-GA6UTBOU.js} +43 -11
- package/dist/{chunk-UX64XCC2.js → chunk-HS3OI4OL.js} +40 -6
- package/dist/{chunk-YTJEV27N.js → chunk-I4HXBP5G.js} +2 -0
- package/dist/{daemonChild-ZACX7V6J.js → daemonChild-STFB5OQL.js} +3 -3
- package/dist/index.cjs +70 -11
- package/dist/index.d.cts +4 -5
- package/dist/index.d.ts +4 -5
- package/dist/index.js +3 -3
- package/dist/{ipcClient-XRL2WUUM.js → ipcClient-URHJKNEH.js} +1 -1
- package/dist/{main-KG3Q4LXI.js → main-M5PCTJTB.js} +3 -3
- package/package.json +2 -2
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
x# Pinggy CLI Architecture
|
|
2
|
+
|
|
3
|
+
Reference for developers and AI agents working on the `@pinggy/cli` codebase. Covers the two-process daemon model, IPC, state, session ownership, and module layout introduced on the `feat/daemon` branch.
|
|
4
|
+
|
|
5
|
+
For domain language (Tunnel, Daemon, Client, Origin, Foreground mode, etc.) see `CONTEXT.md`. For project rules and English style see `CLAUDE.md`. This file documents how the pieces fit together.
|
|
6
|
+
|
|
7
|
+
## 1. The two-process model
|
|
8
|
+
|
|
9
|
+
The `pinggy` binary has three execution modes, dispatched in `src/main.ts`:
|
|
10
|
+
|
|
11
|
+
| Mode | Trigger | Entry | Purpose |
|
|
12
|
+
| -------------------- | -------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| Daemon child | `--_daemon-child` flag | `runDaemonChild()` in `src/daemon/daemonChild.ts` | Long-running background process. Owns the `TunnelManager` and the SDK. |
|
|
14
|
+
| Subcommand | First arg in `SUBCOMMANDS` set | `handleSubcommand()` in `src/cli/subcommands.ts` | Short-lived CLI invocation:`config`, `start`, `stop`, `ps`, `attach`, `daemon`, `logs`, `log`, `restart`. |
|
|
15
|
+
| Legacy single-tunnel | No subcommand, has flags | `buildAndStartTunnel()` in `src/cli/buildAndStartTunnel.ts` | Backwards-compatible single-shot tunnel for flags like `-l 3000` or `-R0:...`. |
|
|
16
|
+
|
|
17
|
+
The CLI never opens an SDK tunnel itself. Every tunnel is created inside the daemon. CLI processes talk to the daemon over HTTP + WebSocket on `127.0.0.1`.
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
A given user has **one** daemon. Whichever client (CLI or App) finds no live daemon spawns one; later clients discover it via `daemon.json`.
|
|
22
|
+
|
|
23
|
+
## 2. Filesystem layout
|
|
24
|
+
|
|
25
|
+
State lives under `getPinggyConfigDir()` (`src/utils/configDir.ts`):
|
|
26
|
+
|
|
27
|
+
- Linux: `$XDG_CONFIG_HOME/pinggy` (default `~/.config/pinggy`)
|
|
28
|
+
- macOS: `~/.config/pinggy`
|
|
29
|
+
- Windows: `%APPDATA%/pinggy`
|
|
30
|
+
|
|
31
|
+
Logs live under `getPinggyLogDir()`:
|
|
32
|
+
|
|
33
|
+
- Linux: `$XDG_STATE_HOME/pinggy-cli/logs`
|
|
34
|
+
- macOS: `~/Library/Logs/Pinggy-CLI`
|
|
35
|
+
- Windows: `%LOCALAPPDATA%/Pinggy-CLI/Logs`
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
<configDir>/
|
|
39
|
+
├── daemon.json Daemon discovery: {pid, port, startedAt}
|
|
40
|
+
├── daemon-state.json Active tunnel snapshot for crash recovery
|
|
41
|
+
└── tunnels/
|
|
42
|
+
└── <name>_<configId>.json Saved tunnel configs (from `pinggy config save`)
|
|
43
|
+
|
|
44
|
+
<logDir>/
|
|
45
|
+
├── daemon.log Daemon process log (rolled)
|
|
46
|
+
└── tunnels/
|
|
47
|
+
└── <origin>__<name>__<tunnelId>.log Per-tunnel logs (rolled 10MB×3)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Helpers live in `src/utils/configDir.ts`: `getDaemonInfoPath`, `getDaemonLogPath`, `getTunnelLogPath`, `getTunnelConfigDir`.
|
|
51
|
+
|
|
52
|
+
## 3. Daemon discovery and spawn
|
|
53
|
+
|
|
54
|
+
Every CLI invocation that touches a tunnel asks one question first: is there a live daemon to talk to? `getDaemonInfo()` answers it by reading `daemon.json` and validating the recorded PID with `process.kill(pid, 0)`. Signal 0 never delivers anything; it just throws when the target process is gone, which is exactly the liveness probe we need. A missing file, malformed JSON, or a dead PID all collapse to the same result: return `null`, and let `ensureDaemonRunning()` spawn a fresh child.
|
|
55
|
+
|
|
56
|
+
The spawn path is deliberately one-way. The CLI fires off a detached child with the `--_daemon-child` flag and then polls `daemon.json` until the child writes it. The poll budget is 8 seconds at 200ms intervals; a timeout almost always means the SDK failed to load inside the child, in which case the stderr captured from the spawned process is surfaced in the thrown error. Atomic writes of `daemon.json` (write to `.tmp`, then `fs.renameSync`) keep concurrent readers from ever seeing a half-written file.
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
CLI starts
|
|
60
|
+
│
|
|
61
|
+
▼
|
|
62
|
+
getDaemonInfo() ─── reads daemon.json
|
|
63
|
+
│
|
|
64
|
+
├─ file missing ──▶ return null
|
|
65
|
+
├─ malformed ──▶ return null
|
|
66
|
+
├─ PID dead (kill -0 fails)──▶ unlink daemon.json, return null
|
|
67
|
+
└─ PID alive ──▶ return DaemonInfo
|
|
68
|
+
│
|
|
69
|
+
▼
|
|
70
|
+
has DaemonInfo? ─── yes ──▶ connect IPCClient(port)
|
|
71
|
+
│
|
|
72
|
+
no
|
|
73
|
+
▼
|
|
74
|
+
startDaemon()
|
|
75
|
+
│
|
|
76
|
+
├─ spawn detached child with --_daemon-child
|
|
77
|
+
│ Unix: spawn(execPath, [argv[1], "--_daemon-child"], {detached, stdio: pipe-stderr})
|
|
78
|
+
│ Windows: same + windowsHide:true
|
|
79
|
+
│ Electron: calls ensuredaemon() which internally starts the daemon server if no daemon is there
|
|
80
|
+
│
|
|
81
|
+
├─ child.unref()
|
|
82
|
+
│
|
|
83
|
+
└─ pollForDaemonInfo(8000ms, interval 200ms)
|
|
84
|
+
│
|
|
85
|
+
├─ daemon.json appears ──▶ return DaemonInfo
|
|
86
|
+
├─ child exits early ──▶ throw with stderr
|
|
87
|
+
└─ timeout ──▶ throw "Daemon failed to start within timeout"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Implementation: `src/daemon/daemonManager.ts`.
|
|
91
|
+
|
|
92
|
+
The race between two CLIs invoked at the same moment is bounded but not eliminated: both may observe no `daemon.json`, both may spawn a child. Whichever child's atomic write lands last wins the file; the other daemon is reachable via its port but unreachable through discovery, and dies when its IPC server is closed or its parent CLI exits. In practice this is rare enough that we accept it; tightening it would require a file lock and adds more complexity than it removes.
|
|
93
|
+
|
|
94
|
+
## 4. Daemon child startup sequence
|
|
95
|
+
|
|
96
|
+
`runDaemonChild()` runs once per daemon lifetime, inside the spawned child process. The step order matters: signal handlers must register before any work that could throw, the IPC port must be open before `daemon.json` is written, and crash recovery must run before auto-start so a restored tunnel never races a freshly-spawned auto-start one.
|
|
97
|
+
|
|
98
|
+
`runDaemonChild()` in `src/daemon/daemonChild.ts`:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
1. ensurePinggyConfigDir(), ensurePinggyLogDir()
|
|
102
|
+
2. Configure winston logger → file: daemon.log, stdout: false
|
|
103
|
+
3. Create TunnelManager singleton
|
|
104
|
+
4. Create IPCServer (HTTP + WS, port not yet bound)
|
|
105
|
+
5. Create SessionTracker, wire onSessionDisconnect
|
|
106
|
+
6. Register signal handlers: SIGTERM, SIGINT, exit, uncaughtException
|
|
107
|
+
7. ipcServer.listen() → returns OS-assigned port
|
|
108
|
+
8. Write daemon.json atomically (write to .tmp, rename)
|
|
109
|
+
9. restoreCrashedTunnels() — replay detached tunnels from daemon-state.json
|
|
110
|
+
10. Start auto-start tunnels (getAutoStartConfigs())
|
|
111
|
+
11. Log "Daemon ready"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Step 8 is the contract the rest of the system depends on: once `daemon.json` exists with a live PID, the IPC server is already accepting connections. CLIs that read the file can immediately POST or open a WebSocket; there is no "warming up" window. Steps 9 and 10 may take time when the Pinggy edge is slow, so "Daemon ready" only means the daemon is reachable, not that every tunnel is live. Callers that need tunnel readiness must poll `/tunnels` or subscribe to `url_ready` over the WebSocket.
|
|
117
|
+
|
|
118
|
+
## 5. IPC: HTTP routes
|
|
119
|
+
|
|
120
|
+
HTTP carries every request/response operation: start, stop, list, restart, log management, shutdown. The transport is plain `http` on `127.0.0.1` with an OS-assigned port. Loopback isolation is the security boundary; there is no auth on these routes, so binding anywhere other than `127.0.0.1` is unsafe.
|
|
121
|
+
|
|
122
|
+
Server: `src/daemon/ipcServer.ts`. Client: `src/daemon/ipcClient.ts`. Public facade: `src/daemon/tunnelClient.ts`.
|
|
123
|
+
|
|
124
|
+
| Method | Path | Purpose |
|
|
125
|
+
| ------ | ----------------------------------------------------------- | ----------------------------------------------- |
|
|
126
|
+
| GET | `/ping` | Health + uptime |
|
|
127
|
+
| GET | `/tunnels` | List V2 |
|
|
128
|
+
| GET | `/tunnels/:id` | Get one |
|
|
129
|
+
| POST | `/tunnels/start` | Start by saved-config name |
|
|
130
|
+
| POST | `/tunnels/start-config` | Start with inline V2 config |
|
|
131
|
+
| POST | `/tunnels/stop` | `{tunnelid}` |
|
|
132
|
+
| POST | `/tunnels/restart` | `{tunnelid}` |
|
|
133
|
+
| GET | `/tunnels-v1` | List V1 (compat) |
|
|
134
|
+
| POST | `/tunnels/start-v1` | V1 start (used by remote management) |
|
|
135
|
+
| POST | `/tunnels/update-config` | V1 update |
|
|
136
|
+
| POST | `/tunnels/update-config-v2` | V2 update |
|
|
137
|
+
| POST | `/tunnels/remove-stopped` | Remove a stopped tunnel by tunnelid or configId |
|
|
138
|
+
| GET | `/loglevel`, POST `/loglevel` | Daemon-wide log level |
|
|
139
|
+
| GET | `/config/tunnel-logging`, POST `/config/tunnel-logging` | Tunnel logging toggle |
|
|
140
|
+
| GET | `/logs/paths` | All tunnel log file paths + running flags |
|
|
141
|
+
| GET | `/logs/resolve?q=` | Resolve a name/id/configId to a log path |
|
|
142
|
+
| POST | `/shutdown` | Graceful shutdown |
|
|
143
|
+
|
|
144
|
+
Conventions:
|
|
145
|
+
|
|
146
|
+
- Every request sets `X-Pinggy-Origin: app|cli|remote` (defaulted in `parseOrigin()` to `cli`). Stored on the tunnel and used in log filenames.
|
|
147
|
+
- V2 is the canonical shape. V1 routes (`/tunnels-v1`, `/tunnels/start-v1`, `/tunnels/update-config`) exist for remote management payloads that still use the older schema. New code should target V2.
|
|
148
|
+
- `start` is idempotent on `configId`. If a tunnel with the same `configId` is already running, the daemon returns `ErrorResponse{code: TunnelAlreadyRunningError}` and the CLI prints the existing state instead of starting a duplicate.
|
|
149
|
+
- A successful operation returns its typed response with HTTP 200. An expected application-level failure returns `ErrorResponse` JSON, still with HTTP 200; the route handled the request, the operation just failed. Non-200 means a transport-level problem (daemon died mid-request, port in use after spawn, etc.) and surfaces as a thrown error in `IPCClient`.
|
|
150
|
+
- The `mode` field (added to `start`, `start-config`, `start-v1`) tells `trackIPCTunnelStart` whether to persist the tunnel to `daemon-state.json`. Foreground tunnels are not persisted; detached tunnels are. The default is `"detached"`, which is what remote management and `DaemonTunnelHandler` rely on.
|
|
151
|
+
|
|
152
|
+
## 6. IPC: WebSocket event stream
|
|
153
|
+
|
|
154
|
+
Path: `ws://127.0.0.1:<port>/ws`. Schema in `src/daemon/wsProtocol.ts`.
|
|
155
|
+
|
|
156
|
+
Client → Daemon:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
{ type: "subscribe", tunnelId: "...", mode: "foreground" | "detached" }
|
|
160
|
+
{ type: "unsubscribe", tunnelId: "..." }
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Daemon → Client (always wrapped in a `tunnel_event`):
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
{
|
|
167
|
+
type: "tunnel_event",
|
|
168
|
+
tunnelId: "...",
|
|
169
|
+
event: "url_ready" | "stats" | "disconnect" | "reconnecting" | "reconnected"
|
|
170
|
+
| "reconnection_failed" | "error" | "stopped" | "will_reconnect"
|
|
171
|
+
| "worker_error" | "subscribed" | "error_response",
|
|
172
|
+
payload: { ... }
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
On `subscribe`, the daemon registers a fan-out of listeners against `TunnelManager` for the tunnel and remembers their listener IDs on the session. On `unsubscribe` or session close, every listener is deregistered. See `handleSubscribe`, `deregisterListeners`, `cleanupSession` in `ipcServer.ts`.
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
Subscribe sequence:
|
|
180
|
+
|
|
181
|
+
CLI Daemon TunnelManager
|
|
182
|
+
│ ws.send(subscribe) │ │
|
|
183
|
+
├──────────────────────────────▶ │
|
|
184
|
+
│ │ registerStatsListener │
|
|
185
|
+
│ ├───────────────────────────────▶│
|
|
186
|
+
│ │ registerDisconnectListener │
|
|
187
|
+
│ ├───────────────────────────────▶│
|
|
188
|
+
│ │ (… 7 more listeners) │
|
|
189
|
+
│ │ │
|
|
190
|
+
│ │ commit subscription │
|
|
191
|
+
│ │ sessionTracker.attach(tid, sid, mode)
|
|
192
|
+
│ tunnel_event "subscribed" │ │
|
|
193
|
+
◀──────────────────────────────┤ │
|
|
194
|
+
│ │ stats fired ◀────────────────│
|
|
195
|
+
│ tunnel_event "stats" │ │
|
|
196
|
+
◀──────────────────────────────┤ │
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The CLI side dispatches each `tunnel_event` to user-registered callbacks via `TunnelClient.handleWsMessage`. Callbacks are stored per event type in arrays (`callbacks.stats`, `callbacks.disconnect`, etc.)
|
|
200
|
+
|
|
201
|
+
## 7. Foreground vs detached: SessionTracker
|
|
202
|
+
|
|
203
|
+
`SessionTracker` in `src/daemon/sessionTracker.ts` tracks ownership of each running tunnel.
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
ownership: Map<tunnelId, { sessionId, mode: "foreground" | "detached" }>
|
|
207
|
+
graceTimers: Map<tunnelId, NodeJS.Timeout>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
State machine for a tunnel:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
subscribe(mode="foreground")
|
|
214
|
+
┌──────────────────────────────────────────────┐
|
|
215
|
+
│ ▼
|
|
216
|
+
(no owner) FOREGROUND
|
|
217
|
+
│ ▲ │
|
|
218
|
+
│ │ │ WS session disconnects
|
|
219
|
+
│ │ ▼
|
|
220
|
+
│ │ GRACE (5000ms)
|
|
221
|
+
│ │ subscribe(mode="foreground") │
|
|
222
|
+
│ │ (another CLI re-attaches) │
|
|
223
|
+
│ │ ◀────────────────────────────────────────┤
|
|
224
|
+
│ │ │ timer expires
|
|
225
|
+
│ │ ▼
|
|
226
|
+
│ └─────── manager.stopTunnel(tunnelId) ◀────┘
|
|
227
|
+
│
|
|
228
|
+
│ subscribe(mode="detached")
|
|
229
|
+
│ or remote-management start
|
|
230
|
+
▼
|
|
231
|
+
DETACHED (session disconnect has no effect)
|
|
232
|
+
│
|
|
233
|
+
│ /tunnels/stop or daemon shutdown
|
|
234
|
+
▼
|
|
235
|
+
(removed)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Key invariants:
|
|
239
|
+
|
|
240
|
+
- Foreground tunnels die 5 seconds after their WS session disconnects if nobody re-attaches. Implemented as `startGraceTimer` → `killOrphanedTunnel` in `SessionTracker`.
|
|
241
|
+
- Detached tunnels are immune to session disconnect and persist in `daemon-state.json` for crash recovery.
|
|
242
|
+
- `attach(tunnelId, sessionId, mode)` cancels any pending grace timer for that tunnel.
|
|
243
|
+
- `pinggy attach <name|id>` opens a fresh foreground subscription; if the tunnel was in detached mode, the call still succeeds, the timer is not armed (mode stays foreground while subscribed).
|
|
244
|
+
|
|
245
|
+
## 8. Crash recovery
|
|
246
|
+
|
|
247
|
+
`src/daemon/stateStore.ts` writes `daemon-state.json` after every tunnel start/stop. The schema:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
interface DaemonState {
|
|
251
|
+
tunnels: DaemonStateTunnel[]; // {tunnelId, configId, name, origin, config, mode, startedAt}
|
|
252
|
+
lastUpdated: string;
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Two lifecycle paths:
|
|
257
|
+
|
|
258
|
+
- **Clean shutdown** (`/shutdown` or signal): `clearDaemonState()` deletes the file. Next daemon start sees nothing to recover.
|
|
259
|
+
- **Crash** (SIGKILL, OOM, panic): the file remains. Next daemon start calls `restoreCrashedTunnels()`, which restarts each `mode === "detached"` tunnel using the persisted config and stable `tunnelId` (so log files stay continuous). Foreground tunnels are not restored: there is no CLI to attach.
|
|
260
|
+
|
|
261
|
+
Auto-start tunnels are independent of crash recovery. They come from saved configs marked `autoStart: true` (`getAutoStartConfigs()`), not from the state file.
|
|
262
|
+
|
|
263
|
+
## 9. Tunnel lifecycle end-to-end
|
|
264
|
+
|
|
265
|
+
Foreground start (`pinggy start <name>`):
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
CLI: pinggy start my-tunnel
|
|
269
|
+
│
|
|
270
|
+
▼
|
|
271
|
+
subcommands.handleStart()
|
|
272
|
+
├─ resolveConfig("my-tunnel") → SavedTunnelConfig
|
|
273
|
+
├─ buildFinalConfig() with optional flag overrides
|
|
274
|
+
└─ startForegroundViaDaemon(finalConfig)
|
|
275
|
+
│
|
|
276
|
+
▼
|
|
277
|
+
initClient() → new TunnelClient() → ensureDaemon()
|
|
278
|
+
│ (spawns daemon if missing)
|
|
279
|
+
▼
|
|
280
|
+
client.handleStartV2(finalConfig) ── HTTP POST /tunnels/start-config
|
|
281
|
+
│ │
|
|
282
|
+
│ ▼
|
|
283
|
+
│ IPCServer.handleStartV2
|
|
284
|
+
│ │
|
|
285
|
+
│ ▼
|
|
286
|
+
│ TunnelManager.createTunnel + startTunnel
|
|
287
|
+
│ │
|
|
288
|
+
│ ▼
|
|
289
|
+
│ attachTunnelLogger(tid, origin, name)
|
|
290
|
+
│ │
|
|
291
|
+
│ ▼
|
|
292
|
+
│ SDK opens tunnel, returns urls
|
|
293
|
+
│ TunnelResponseV2 ◀───────────────────── 200 OK
|
|
294
|
+
▼
|
|
295
|
+
client.attach(tunnelId, "foreground") ── WS subscribe
|
|
296
|
+
│
|
|
297
|
+
▼
|
|
298
|
+
connectTui()
|
|
299
|
+
├─ TTY available + !noTui → blessed TUI
|
|
300
|
+
└─ else → plain stdout
|
|
301
|
+
│
|
|
302
|
+
▼
|
|
303
|
+
await TUI exit / SIGINT
|
|
304
|
+
│
|
|
305
|
+
▼
|
|
306
|
+
client.handleStop(tunnelId) ── HTTP POST /tunnels/stop
|
|
307
|
+
client.close() ── WS close
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Background start adds `-b` flag → `startBackgroundTunnels()`. The CLI exits immediately after the daemon confirms start; the tunnel keeps running.
|
|
311
|
+
|
|
312
|
+
Attach (`pinggy attach <name|id>`):
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
attachCommand.handleAttach()
|
|
316
|
+
├─ ensureDaemon()
|
|
317
|
+
├─ client.handleListV2()
|
|
318
|
+
├─ findTunnel(list, nameOrId) (id exact, id prefix, name, configId)
|
|
319
|
+
├─ client.attach(tunnelId, "foreground") (cancels detached grace timer? no, only foreground grace)
|
|
320
|
+
└─ connectTui()
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
The first time a detached tunnel is attached, the SessionTracker entry flips to `foreground` while the WS session is open. When the TUI quits, the user can choose to stop the tunnel (default in `connectTui`'s `onExit`).
|
|
324
|
+
|
|
325
|
+
## 10. Components by directory
|
|
326
|
+
|
|
327
|
+
```
|
|
328
|
+
src/
|
|
329
|
+
├── main.ts Entry dispatch (daemon-child / subcommand / legacy)
|
|
330
|
+
├── index.ts Library exports for embedding in Electron
|
|
331
|
+
├── types.ts Shared types: TunnelStatus, FinalConfig, ErrorResponse, enums
|
|
332
|
+
├── logger.ts, logger/ Winston root + per-tunnel transport attach/detach
|
|
333
|
+
│
|
|
334
|
+
├── cli/ Foreground CLI surface
|
|
335
|
+
│ ├── subcommands.ts Router for `config|start|stop|ps|attach|daemon|logs|log|restart`
|
|
336
|
+
│ ├── startCli.ts Foreground/background/multi/auto-start orchestration
|
|
337
|
+
│ ├── buildAndStartTunnel.ts Legacy single-tunnel path
|
|
338
|
+
│ ├── buildConfig.ts CLI flags + positionals → FinalConfig
|
|
339
|
+
│ ├── configStore.ts Persisted saved-config CRUD
|
|
340
|
+
│ ├── daemonCommands.ts `pinggy daemon start|stop|status|install-service|...`
|
|
341
|
+
│ ├── attachCommand.ts `pinggy attach`
|
|
342
|
+
│ ├── psCommand.ts `pinggy ps`
|
|
343
|
+
│ ├── stopCommand.ts `pinggy stop`
|
|
344
|
+
│ ├── restartCommand.ts `pinggy restart`
|
|
345
|
+
│ ├── logCommand.ts, logsCommand.ts `pinggy log`, `pinggy logs`
|
|
346
|
+
│ ├── options.ts, extendedOptions.ts, defaults.ts, help.ts
|
|
347
|
+
│
|
|
348
|
+
├── daemon/ Daemon process + IPC
|
|
349
|
+
│ ├── daemonChild.ts runDaemonChild() entry. Persistence, recovery, auto-start.
|
|
350
|
+
│ ├── daemonManager.ts getDaemonInfo, startDaemon, stopDaemon (spawn/discovery)
|
|
351
|
+
│ ├── ipcServer.ts HTTP + WS endpoints. Owns Routes map + WS sessions.
|
|
352
|
+
│ ├── ipcClient.ts Raw HTTP wrapper (no WS). Internal to tunnelClient.ts only.
|
|
353
|
+
│ ├── tunnelClient.ts PUBLIC facade. TunnelClient + DaemonTunnelHandler.
|
|
354
|
+
│ ├── wsProtocol.ts ClientMessage, TunnelEvent, TunnelEventPayloadMap.
|
|
355
|
+
│ ├── sessionTracker.ts Foreground/detached ownership + grace timer.
|
|
356
|
+
│ ├── stateStore.ts daemon-state.json read/write/clear.
|
|
357
|
+
│ └── serviceInstaller.ts systemd / launchd / Task Scheduler installers.
|
|
358
|
+
│
|
|
359
|
+
├── tunnel_manager/
|
|
360
|
+
│ └── TunnelManager.ts Singleton wrapping @pinggy/pinggy SDK. Listener fanout.
|
|
361
|
+
│
|
|
362
|
+
├── remote_management/ Remote control via Pinggy management WS
|
|
363
|
+
│ ├── remoteManagement.ts Connect/disconnect + state machine
|
|
364
|
+
│ ├── handler.ts TunnelHandler interface + TunnelOperations impl
|
|
365
|
+
│ ├── remote_schema.ts Zod schemas for V1 + V2 payloads
|
|
366
|
+
│ ├── websocket_handlers.ts, websocket_printer.ts
|
|
367
|
+
│
|
|
368
|
+
├── tui/ Foreground UI
|
|
369
|
+
│ ├── blessed/ blessed-based TUI for tunnels
|
|
370
|
+
│ ├── ink/ React/Ink prototypes
|
|
371
|
+
│ └── spinner/ Loading spinner widget
|
|
372
|
+
│
|
|
373
|
+
├── utils/ Cross-cutting helpers
|
|
374
|
+
│ ├── configDir.ts Per-OS config + log paths
|
|
375
|
+
│ ├── parseArgs.ts CLI arg parser
|
|
376
|
+
│ ├── printer.ts CLIPrinter singleton
|
|
377
|
+
│ ├── getFreePort.ts Free local port for web debugger
|
|
378
|
+
│ ├── FileServer.ts Static file serving
|
|
379
|
+
│ └── htmlTemplates.ts Built-in error pages
|
|
380
|
+
│
|
|
381
|
+
└── workers/
|
|
382
|
+
└── file_serve_worker.ts Worker thread for --serve
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## 11. Key types
|
|
386
|
+
|
|
387
|
+
- `TunnelOrigin = "app" | "cli" | "remote"` (`src/tunnel_manager/TunnelManager.ts`). Set on every tunnel at creation; flows into log filenames.
|
|
388
|
+
- `ClientOrigin = "app" | "cli" | "remote"` (`src/daemon/ipcClient.ts`). Same values, used in the `X-Pinggy-Origin` header.
|
|
389
|
+
- `TunnelStateType` (`src/types.ts`): `idle | starting | running | live | closed | exited`.
|
|
390
|
+
- `FinalConfig` (`src/types.ts`): the SDK's `TunnelConfigurationV1` plus `conf?` and `saveconf?`. Used by `buildConfig.ts` and threaded through to the daemon.
|
|
391
|
+
- `TunnelResponseV2` (`src/remote_management/handler.ts`): `{tunnelid, remoteurls, tunnelconfig, status, stats, greetmsg?}`. The canonical wire shape.
|
|
392
|
+
- `DaemonInfo` (`src/daemon/daemonChild.ts`): `{pid, port, startedAt}`. The contents of `daemon.json`.
|
|
393
|
+
- `DaemonStateTunnel` (`src/daemon/stateStore.ts`): `{tunnelId, configId, name, origin, config, mode, startedAt}`. Crash recovery row.
|
|
394
|
+
|
|
395
|
+
## 12. Singletons and listener maps
|
|
396
|
+
|
|
397
|
+
The daemon process holds these singletons:
|
|
398
|
+
|
|
399
|
+
- `TunnelManager.getInstance()`. Wraps the SDK. Holds `Map<tunnelId, ManagedTunnel>`. All tunnel state lives here.
|
|
400
|
+
- `logger` (winston). Global root logger. Per-tunnel `File` transports are added/removed by `attachTunnelLogger` / `detachTunnelLogger` in `src/logger/tunnelLogger.ts`, gated by `isTunnelLoggingEnabled()`.
|
|
401
|
+
- `CLIPrinter`. Only used by CLI process for foreground output. Daemon does not print to stdout.
|
|
402
|
+
|
|
403
|
+
Listener registration pattern (used by TunnelManager and propagated by IPCServer):
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
Map<tunnelId, Map<listenerId, (...) => void>>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Register returns a `listenerId`; deregister takes it back. The IPCServer remembers `${type}:${listenerId}` strings in `session.listenerIds.get(tunnelId)` so a session can deregister everything cleanly on close.
|
|
410
|
+
|
|
411
|
+
## 13. Origin tagging and log file naming
|
|
412
|
+
|
|
413
|
+
When a tunnel is created, the IPCServer reads the `X-Pinggy-Origin` header and passes it to `TunnelManager.createTunnel(config, origin)`. The origin is stored on `ManagedTunnel.origin` and is used by `getTunnelLogPath(tunnelId, origin, name)`:
|
|
414
|
+
|
|
415
|
+
```
|
|
416
|
+
<origin>__<name>__<tunnelId>.log when name present
|
|
417
|
+
<origin>__<tunnelId>.log when nameless
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Example: `cli__my-api__abc123.log`, `app__1700000000_xyz789.log`, `remote__staging__def456.log`.
|
|
421
|
+
|
|
422
|
+
This is how a shared daemon distinguishes tunnels from different clients on disk. The log viewers in the desktop app filter by the origin prefix.
|
|
423
|
+
|
|
424
|
+
## 14. Logging architecture
|
|
425
|
+
|
|
426
|
+
Three log artifacts:
|
|
427
|
+
|
|
428
|
+
| Log | Writer | Path | Lifetime |
|
|
429
|
+
| --------------- | ------------------------------------- | ----------------------------------------------------- | ----------------------------- |
|
|
430
|
+
| Application log | Electron host via `electron-log` | `<userData>/logs/` | Persistent, rolled |
|
|
431
|
+
| Daemon log | winston root inside daemon | `<logDir>/daemon.log` | Persistent, rolled |
|
|
432
|
+
| Tunnel log | winston `File` transport per tunnel | `<logDir>/tunnels/<origin>__<name>__<tunnelId>.log` | While tunnel runs (10MB × 3) |
|
|
433
|
+
|
|
434
|
+
Toggle: `setTunnelLoggingEnabled(false)` detaches every per-tunnel transport immediately. Tunnels keep running; nothing else changes. Re-enabling does not retroactively re-attach to running tunnels (transports are recreated on next tunnel start).
|
|
435
|
+
|
|
436
|
+
CLI logs (the short-lived foreground process) go to stdout by default. The daemon log captures everything inside the long-running process.
|
|
437
|
+
|
|
438
|
+
## 15. Remote management integration
|
|
439
|
+
|
|
440
|
+
`src/remote_management/remoteManagement.ts` opens a WebSocket to Pinggy's management endpoint and forwards tunnel commands. The receiver implements `TunnelHandler`. Two implementations exist:
|
|
441
|
+
|
|
442
|
+
- `TunnelOperations` (`src/remote_management/handler.ts`). Direct, in-process. Used when remote management runs **inside the daemon** (not the default in CLI).
|
|
443
|
+
- `DaemonTunnelHandler` (`src/daemon/tunnelClient.ts`). Proxy. Forwards every `handleStart`, `handleStop`, etc. over HTTP to the daemon. Used when remote management runs **inside the CLI process** (the current default).
|
|
444
|
+
|
|
445
|
+
`TunnelClient.forRemoteManagement()` returns a `DaemonTunnelHandler` with `origin = "remote"`, so tunnels created via remote management are tagged distinctly and logged separately.
|
|
446
|
+
|
|
447
|
+
Same code path as user-typed subcommands: the daemon does not know or care that the request came from remote management.
|
|
448
|
+
|
|
449
|
+
## 16. Service installation
|
|
450
|
+
|
|
451
|
+
`pinggy daemon install-service` writes a platform-specific service file and enables it:
|
|
452
|
+
|
|
453
|
+
- Linux: systemd user unit at `~/.config/systemd/user/pinggy.service`. `Type=simple`, runs `<binary> --_daemon-child` directly (no fork). `systemctl --user enable --now`.
|
|
454
|
+
- macOS: launchd `LaunchAgent` at `~/Library/LaunchAgents/io.pinggy.agent.plist`. `launchctl load`.
|
|
455
|
+
- Windows: Task Scheduler entry.
|
|
456
|
+
|
|
457
|
+
The installer resolves the binary via `resolveBinary()` in `src/daemon/serviceInstaller.ts`. Prefers `process.execPath` when running as a pkg binary, falls back to `which pinggy`, then to `node <script>`.
|
|
458
|
+
|
|
459
|
+
A system service gives restart-on-failure and start-at-login. The plain daemon mode does not.
|
|
460
|
+
|
|
461
|
+
## 17. Reference for AI agents
|
|
462
|
+
|
|
463
|
+
When making changes, these are the load-bearing files and the contracts that matter.
|
|
464
|
+
|
|
465
|
+
**Entry points**
|
|
466
|
+
|
|
467
|
+
- `src/main.ts` — argv dispatch. Adding a new top-level subcommand means updating `SUBCOMMANDS` in `src/cli/subcommands.ts` AND adding a handler. Updating the dispatch in `main.ts` is rarely needed.
|
|
468
|
+
- `src/daemon/daemonChild.ts` — daemon process entry. Anything that needs to run **once** when the daemon boots goes here.
|
|
469
|
+
|
|
470
|
+
**Adding a new IPC route**
|
|
471
|
+
|
|
472
|
+
1. Add the route key + handler in `IPCServer.registerRoutes()` (`src/daemon/ipcServer.ts`).
|
|
473
|
+
2. Add the wire method to `IPCClient` (`src/daemon/ipcClient.ts`).
|
|
474
|
+
3. Expose it on `TunnelClient` (`src/daemon/tunnelClient.ts`) — never let callers reach `IPCClient` directly.
|
|
475
|
+
4. If it streams events, add the event type to `DaemonEventType` and payload to `TunnelEventPayloadMap` in `src/daemon/wsProtocol.ts`, register the listener in `handleSubscribe`, deregister in `deregisterListeners`, dispatch in `TunnelClient.handleWsMessage`.
|
|
476
|
+
|
|
477
|
+
**Adding a new tunnel state**
|
|
478
|
+
|
|
479
|
+
If new state needs to survive a daemon crash, extend `DaemonStateTunnel` in `src/daemon/stateStore.ts` and update `restoreCrashedTunnels()` in `daemonChild.ts`. If it is only needed in memory, put it on `ManagedTunnel` in `src/tunnel_manager/TunnelManager.ts`.
|
|
480
|
+
|
|
481
|
+
**Foreground vs detached**
|
|
482
|
+
|
|
483
|
+
If you spawn a tunnel and want it to die when the CLI quits, subscribe with `mode: "foreground"`. If it should outlive the CLI, use `mode: "detached"` (and the daemon will write it to `daemon-state.json` for crash recovery).
|
|
484
|
+
|
|
485
|
+
**Origin**
|
|
486
|
+
|
|
487
|
+
Every tunnel must have a correct origin. The default in `parseOrigin()` is `cli`. If you add a new client surface, plumb a new origin value through `TunnelOrigin` and `ClientOrigin`. Origin affects log filenames; renaming an origin is a breaking change for log viewers.
|
|
488
|
+
|
|
489
|
+
**Singletons in tests**
|
|
490
|
+
|
|
491
|
+
`TunnelManager.instance = undefined` between tests, plus `jest.clearAllMocks()`. See `src/_tests_/` for examples. Use `@jest/globals` imports — there is no global `jest`.
|
|
492
|
+
|
|
493
|
+
**English style**
|
|
494
|
+
|
|
495
|
+
No em-dashes. Short phrases. Present tense. See `CLAUDE.md` § "English Style" before writing user-facing strings, error messages, or docs.
|
|
496
|
+
|
|
497
|
+
**Gotchas**
|
|
498
|
+
|
|
499
|
+
- Do not log to stdout from the daemon. The daemon's stdio is closed by `spawn({stdio: "ignore"})` on most platforms; writes will silently fail or fill an OS buffer.
|
|
500
|
+
- Do not call `process.exit()` inside daemon route handlers. The shutdown route is the only path that should terminate the process, and it does so via `setTimeout(() => process.exit(0), 200)` so the HTTP response can flush first.
|
|
501
|
+
- Do not import from `src/daemon/ipcClient.ts` outside `src/daemon/`. Use `TunnelClient` from `tunnelClient.ts`.
|
|
502
|
+
- The first CLI run on a fresh install will spawn the daemon and pay an 8-second worst-case latency. Subsequent runs reuse the daemon.
|
|
503
|
+
- `daemon.json` writes are atomic (write tmp, rename). Direct partial reads should not happen, but `getDaemonInfo()` still tolerates malformed JSON by returning `null` and triggering a respawn.
|
|
504
|
+
- The IPC server only listens on `127.0.0.1`. Do not change to `0.0.0.0` without adding authentication. The current design assumes loopback isolation.
|
|
505
|
+
|
|
506
|
+
## 18. Quick diagrams
|
|
507
|
+
|
|
508
|
+
### Start sequence (foreground, daemon already running)
|
|
509
|
+
|
|
510
|
+
```
|
|
511
|
+
user CLI Daemon SDK / TunnelManager
|
|
512
|
+
│ pinggy start api │ │
|
|
513
|
+
├──────────────────────▶ │
|
|
514
|
+
│ │ GET /ping (sanity) │
|
|
515
|
+
│ ├───────────────▶ │
|
|
516
|
+
│ │ ◀───────────── │ {status:"ok"} │
|
|
517
|
+
│ │ POST /tunnels/start-config │
|
|
518
|
+
│ ├───────────────▶│ createTunnel + start │
|
|
519
|
+
│ │ ├──────────────────────▶
|
|
520
|
+
│ │ │ urls │
|
|
521
|
+
│ │ ◀──────────────────────┤
|
|
522
|
+
│ │ 200 TunnelResponseV2 │
|
|
523
|
+
│ │ ◀──────────────│ │
|
|
524
|
+
│ │ ws connect → /ws │
|
|
525
|
+
│ │ ws subscribe(tid, "foreground") │
|
|
526
|
+
│ ├───────────────▶│ register N listeners │
|
|
527
|
+
│ │ tunnel_event "subscribed" │
|
|
528
|
+
│ │ ◀──────────────│ │
|
|
529
|
+
│ TUI render │ │ │
|
|
530
|
+
│ ◀────────────────────│ │ │
|
|
531
|
+
│ │ tunnel_event "stats" (repeated) │
|
|
532
|
+
│ │ ◀──────────────│ │
|
|
533
|
+
│ Ctrl+C │ │
|
|
534
|
+
├──────────────────────▶ POST /tunnels/stop │
|
|
535
|
+
│ ├───────────────▶│ stopTunnel │
|
|
536
|
+
│ │ ◀──────────────│ │
|
|
537
|
+
│ │ ws close │
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Crash and recovery
|
|
541
|
+
|
|
542
|
+
```
|
|
543
|
+
┌──────────────────────────────────┐
|
|
544
|
+
│ Daemon running, 2 detached │
|
|
545
|
+
│ daemon-state.json: [t1, t2] │
|
|
546
|
+
└──────────────────────────────────┘
|
|
547
|
+
│ SIGKILL / OOM
|
|
548
|
+
▼
|
|
549
|
+
┌──────────────────────────────────┐
|
|
550
|
+
│ Process gone, but state file │
|
|
551
|
+
│ still on disk │
|
|
552
|
+
└──────────────────────────────────┘
|
|
553
|
+
│ next pinggy command
|
|
554
|
+
▼
|
|
555
|
+
getDaemonInfo() returns null (PID dead) ──▶ unlinks daemon.json
|
|
556
|
+
│
|
|
557
|
+
▼
|
|
558
|
+
startDaemon() spawns new child
|
|
559
|
+
│
|
|
560
|
+
▼
|
|
561
|
+
runDaemonChild()
|
|
562
|
+
├─ writeDaemonInfo()
|
|
563
|
+
├─ restoreCrashedTunnels() ──▶ TunnelManager.createTunnel + startTunnel for t1, t2
|
|
564
|
+
│ using same tunnelid (log continuity)
|
|
565
|
+
├─ getAutoStartConfigs() ──▶ starts auto-start tunnels
|
|
566
|
+
└─ "Daemon ready"
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### Two clients, one daemon
|
|
570
|
+
|
|
571
|
+
```
|
|
572
|
+
Time
|
|
573
|
+
│
|
|
574
|
+
│ 10:00 Electron app launches.
|
|
575
|
+
│ App finds no daemon → spawns. Daemon starts.
|
|
576
|
+
│ App opens tunnel "web" with origin=app.
|
|
577
|
+
│ log: app__web__t1.log
|
|
578
|
+
│
|
|
579
|
+
│ 10:05 User runs `pinggy ps` from terminal.
|
|
580
|
+
│ CLI finds daemon.json → connects (no spawn).
|
|
581
|
+
│ Both tunnels (including the App's) appear in ps.
|
|
582
|
+
│
|
|
583
|
+
│ 10:06 User runs `pinggy start api`.
|
|
584
|
+
│ Origin=cli. Foreground TUI opens.
|
|
585
|
+
│ log: cli__api__t2.log
|
|
586
|
+
│
|
|
587
|
+
│ 10:10 User Ctrl+C's the CLI tunnel.
|
|
588
|
+
│ CLI sends /tunnels/stop. Daemon stops t2.
|
|
589
|
+
│ Daemon keeps running. App's tunnel t1 unaffected.
|
|
590
|
+
│
|
|
591
|
+
│ 10:30 User closes Electron app.
|
|
592
|
+
│ App calls /shutdown? No — App leaves the daemon up
|
|
593
|
+
│ since CLI/remote may still use it.
|
|
594
|
+
│ Daemon keeps running with t1 still live.
|
|
595
|
+
▼
|
|
596
|
+
```
|
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`.
|
package/README.md
CHANGED
|
@@ -324,7 +324,7 @@ Extended options provide advanced controls. Specify them as positional values li
|
|
|
324
324
|
- x:reverseproxy Disable built-in reverse-proxy header injection.
|
|
325
325
|
- x:xff Add X-Forwarded-For.
|
|
326
326
|
- x:fullurl | x:fullrequesturl Include original request URL.
|
|
327
|
-
- x:haproxy | x:haproxy:v1 | x:haproxy:v2 Send HAProxy PROXY protocol header to the local server (default v1).
|
|
327
|
+
- x:haproxy | x:haproxy:v1 | x:haproxy:v2 Send HAProxy PROXY protocol header to the local server (default v1). TCP tunnels only.
|
|
328
328
|
- w:<cidr>[,<cidr>...] Whitelist IPs (IPv4 CIDR).
|
|
329
329
|
- k:<token> Set Bearer token(s) for auth (repeatable).
|
|
330
330
|
- b:<user:pass> Add Basic Auth credentials (repeatable).
|
|
@@ -339,8 +339,8 @@ Examples:
|
|
|
339
339
|
- Allow only a local subnet:
|
|
340
340
|
pinggy w:192.168.1.0/24 -l 8080
|
|
341
341
|
|
|
342
|
-
- Send the real client IP to a PROXY-protocol-aware backend:
|
|
343
|
-
pinggy x:haproxy:v2 -l 8080
|
|
342
|
+
- Send the real client IP to a PROXY-protocol-aware backend (TCP tunnels only):
|
|
343
|
+
pinggy --type tcp x:haproxy:v2 -l 8080
|
|
344
344
|
|
|
345
345
|
To generate advanced CLI arguments, use [Configure from Pinggy.io](https://pinggy.io/)
|
|
346
346
|
|
|
@@ -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-
|
|
16
|
+
} from "./chunk-HS3OI4OL.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-
|
|
26
|
+
} from "./chunk-I4HXBP5G.js";
|
|
23
27
|
import {
|
|
24
28
|
ConfigVerb,
|
|
25
29
|
SUBCOMMANDS,
|
|
@@ -137,7 +141,7 @@ function printHelpMessage() {
|
|
|
137
141
|
console.log(" x:noreverseproxy Disable built-in reverse-proxy header injection");
|
|
138
142
|
console.log(" x:localservertls:host Connect to local HTTPS server with SNI");
|
|
139
143
|
console.log(" x:passpreflight Pass CORS preflight requests unchanged");
|
|
140
|
-
console.log(" x:haproxy[:v1|:v2] Enable HAProxy PROXY protocol (default v1)");
|
|
144
|
+
console.log(" x:haproxy[:v1|:v2] Enable HAProxy PROXY protocol, TCP tunnels only (default v1)");
|
|
141
145
|
console.log(" a:Key:Val Add header");
|
|
142
146
|
console.log(" u:Key:Val Update header");
|
|
143
147
|
console.log(" r:Key Remove header");
|
|
@@ -840,6 +844,20 @@ function hasRemoteManagement(values) {
|
|
|
840
844
|
const token = values["remote-management"];
|
|
841
845
|
return typeof token === "string" && token.trim().length > 0;
|
|
842
846
|
}
|
|
847
|
+
function validateHaProxy(finalConfig) {
|
|
848
|
+
if (!finalConfig.haProxy) {
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
const forwarding = finalConfig.forwarding;
|
|
852
|
+
const entries = Array.isArray(forwarding) ? forwarding : [];
|
|
853
|
+
const primaryType = entries.length > 0 ? entries[0].type : void 0;
|
|
854
|
+
if (primaryType === TunnelType.Tcp) {
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
return new Error(
|
|
858
|
+
`HAProxy PROXY protocol (x:haproxy) can only be used with TCP tunnels, but this tunnel is "${primaryType ?? TunnelType.Http}". Make it a TCP tunnel (--type tcp, or the tcp@ server prefix) or drop x:haproxy.`
|
|
859
|
+
);
|
|
860
|
+
}
|
|
843
861
|
function buildFinalConfig(values, positionals, baseConfig) {
|
|
844
862
|
let token;
|
|
845
863
|
let server;
|
|
@@ -902,6 +920,10 @@ function buildFinalConfig(values, positionals, baseConfig) {
|
|
|
902
920
|
}
|
|
903
921
|
normalizeForwardingToArray(finalConfig, type);
|
|
904
922
|
parseArgs2(finalConfig, remainingPositionals);
|
|
923
|
+
const haProxyErr = validateHaProxy(finalConfig);
|
|
924
|
+
if (haProxyErr instanceof Error) {
|
|
925
|
+
throw haProxyErr;
|
|
926
|
+
}
|
|
905
927
|
storeJson(finalConfig, saveconf);
|
|
906
928
|
return finalConfig;
|
|
907
929
|
}
|
|
@@ -1761,9 +1783,13 @@ async function handleDaemon(args) {
|
|
|
1761
1783
|
}
|
|
1762
1784
|
}
|
|
1763
1785
|
async function handleDaemonStart() {
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1786
|
+
const info = getDaemonInfo();
|
|
1787
|
+
if (info) {
|
|
1788
|
+
if (!isIpcCompatible(info)) {
|
|
1789
|
+
printer_default.error(ipcMismatchMessage(info));
|
|
1790
|
+
process.exit(1);
|
|
1791
|
+
}
|
|
1792
|
+
printer_default.print(pico2.yellow(`Daemon already running (PID ${info.pid}, port ${info.port}). Use: pinggy daemon status for details.`));
|
|
1767
1793
|
return;
|
|
1768
1794
|
}
|
|
1769
1795
|
const autoConfigs = getAutoStartConfigs();
|
|
@@ -1776,8 +1802,8 @@ async function handleDaemonStart() {
|
|
|
1776
1802
|
printer_default.print(pico2.cyanBright("Starting daemon..."));
|
|
1777
1803
|
}
|
|
1778
1804
|
try {
|
|
1779
|
-
const
|
|
1780
|
-
printer_default.success(`Daemon started PID ${
|
|
1805
|
+
const info2 = await startDaemon();
|
|
1806
|
+
printer_default.success(`Daemon started PID ${info2.pid}.`);
|
|
1781
1807
|
} catch (err) {
|
|
1782
1808
|
printer_default.error(`Failed to start daemon: ${errorMessage(err)}`);
|
|
1783
1809
|
process.exit(1);
|
|
@@ -1814,6 +1840,10 @@ function handleDaemonStatus() {
|
|
|
1814
1840
|
printer_default.print(` Started: ${info.startedAt}`);
|
|
1815
1841
|
printer_default.print(` Uptime: ${uptimeStr}`);
|
|
1816
1842
|
printer_default.print(` Host: ${info.host === DaemonHost.APP ? "Pinggy app (in-process)" : "standalone (CLI)"}`);
|
|
1843
|
+
printer_default.print(` IPC: v${daemonIpcVersion(info)}`);
|
|
1844
|
+
if (!isIpcCompatible(info)) {
|
|
1845
|
+
printer_default.print(pico2.yellow(` Warning: ${ipcMismatchMessage(info)}`));
|
|
1846
|
+
}
|
|
1817
1847
|
}
|
|
1818
1848
|
|
|
1819
1849
|
// src/cli/subcommand/handlers/psCommand.ts
|
|
@@ -2592,7 +2622,8 @@ var IPCServer = class {
|
|
|
2592
2622
|
[Route.Ping]: () => ({
|
|
2593
2623
|
status: "ok",
|
|
2594
2624
|
pid: process.pid,
|
|
2595
|
-
uptime: Math.floor((Date.now() - this.startedAt) / 1e3)
|
|
2625
|
+
uptime: Math.floor((Date.now() - this.startedAt) / 1e3),
|
|
2626
|
+
ipcVersion: IPC_VERSION
|
|
2596
2627
|
}),
|
|
2597
2628
|
[Route.ListTunnels]: async () => {
|
|
2598
2629
|
const res = await this.ops.handleListV2();
|
|
@@ -3385,7 +3416,8 @@ async function runDaemonChild(opts = {}) {
|
|
|
3385
3416
|
pid: process.pid,
|
|
3386
3417
|
port,
|
|
3387
3418
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3388
|
-
host: daemonHost
|
|
3419
|
+
host: daemonHost,
|
|
3420
|
+
ipcVersion: IPC_VERSION
|
|
3389
3421
|
};
|
|
3390
3422
|
writeDaemonInfo(info);
|
|
3391
3423
|
logger.info("Daemon info written", info);
|
|
@@ -3426,7 +3458,7 @@ async function main() {
|
|
|
3426
3458
|
const { values, positionals, hasAnyArgs } = parseCliArgs(cliOptions);
|
|
3427
3459
|
configureLogger(values);
|
|
3428
3460
|
if (values["_daemon-child"]) {
|
|
3429
|
-
const { runDaemonChild: runDaemonChild2 } = await import("./daemonChild-
|
|
3461
|
+
const { runDaemonChild: runDaemonChild2 } = await import("./daemonChild-STFB5OQL.js");
|
|
3430
3462
|
await runDaemonChild2();
|
|
3431
3463
|
return;
|
|
3432
3464
|
}
|
|
@@ -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-
|
|
7
|
+
} from "./chunk-I4HXBP5G.js";
|
|
7
8
|
import {
|
|
8
9
|
TunnelAlreadyRunningError,
|
|
9
10
|
TunnelManager,
|
|
@@ -256,6 +257,19 @@ var TunnelConfigV1Schema = z.object({
|
|
|
256
257
|
haProxy: z.enum([HaProxyVersion.V1, HaProxyVersion.V2]).optional(),
|
|
257
258
|
serve: z.string().optional(),
|
|
258
259
|
optional: z.record(z.string(), z.unknown()).optional()
|
|
260
|
+
}).superRefine((config, ctx) => {
|
|
261
|
+
if (!config.haProxy) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const entries = Array.isArray(config.forwarding) ? config.forwarding : [];
|
|
265
|
+
const primaryType = entries.length > 0 ? entries[0].type : void 0;
|
|
266
|
+
if (primaryType !== TunnelType.Tcp) {
|
|
267
|
+
ctx.addIssue({
|
|
268
|
+
code: "custom",
|
|
269
|
+
path: ["haProxy"],
|
|
270
|
+
message: `haProxy can only be used with "tcp" tunnels, but the first forwarding rule has type: ${primaryType ?? TunnelType.Http}`
|
|
271
|
+
});
|
|
272
|
+
}
|
|
259
273
|
});
|
|
260
274
|
var TUNNEL_CONFIG_V1_KEYS = Object.keys(
|
|
261
275
|
TunnelConfigV1Schema.shape
|
|
@@ -1599,6 +1613,19 @@ function getDaemonInfo() {
|
|
|
1599
1613
|
function isDaemonRunning() {
|
|
1600
1614
|
return getDaemonInfo() !== null;
|
|
1601
1615
|
}
|
|
1616
|
+
function daemonIpcVersion(info) {
|
|
1617
|
+
return info.ipcVersion ?? 0;
|
|
1618
|
+
}
|
|
1619
|
+
function isIpcCompatible(info) {
|
|
1620
|
+
return daemonIpcVersion(info) === IPC_VERSION;
|
|
1621
|
+
}
|
|
1622
|
+
function ipcMismatchMessage(info) {
|
|
1623
|
+
const versions = `daemon IPC v${daemonIpcVersion(info)}, this CLI expects v${IPC_VERSION}`;
|
|
1624
|
+
if (info.host === DaemonHost.APP) {
|
|
1625
|
+
return `The Pinggy app is running a daemon from a different version (${versions}). Update or restart the Pinggy app.`;
|
|
1626
|
+
}
|
|
1627
|
+
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.`;
|
|
1628
|
+
}
|
|
1602
1629
|
function getDaemonSpawnArgs() {
|
|
1603
1630
|
return {
|
|
1604
1631
|
command: process.execPath,
|
|
@@ -1645,9 +1672,12 @@ async function startDaemon() {
|
|
|
1645
1672
|
}
|
|
1646
1673
|
async function ensureDaemonRunning() {
|
|
1647
1674
|
const existing = getDaemonInfo();
|
|
1648
|
-
if (existing)
|
|
1675
|
+
if (existing) {
|
|
1676
|
+
if (!isIpcCompatible(existing)) throw new Error(ipcMismatchMessage(existing));
|
|
1677
|
+
return existing;
|
|
1678
|
+
}
|
|
1649
1679
|
if (process.versions.electron) {
|
|
1650
|
-
const { runDaemonChild } = await import("./daemonChild-
|
|
1680
|
+
const { runDaemonChild } = await import("./daemonChild-STFB5OQL.js");
|
|
1651
1681
|
const handle = await runDaemonChild({
|
|
1652
1682
|
installSignalHandlers: false,
|
|
1653
1683
|
exitOnFailure: false,
|
|
@@ -1658,7 +1688,8 @@ async function ensureDaemonRunning() {
|
|
|
1658
1688
|
pid: handle.pid,
|
|
1659
1689
|
port: handle.port,
|
|
1660
1690
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1661
|
-
host: DaemonHost.APP
|
|
1691
|
+
host: DaemonHost.APP,
|
|
1692
|
+
ipcVersion: IPC_VERSION
|
|
1662
1693
|
};
|
|
1663
1694
|
}
|
|
1664
1695
|
return startDaemon();
|
|
@@ -1669,7 +1700,7 @@ function getInProcessDaemonHandle() {
|
|
|
1669
1700
|
async function getActiveTunnelSummaries(origin = "app") {
|
|
1670
1701
|
const info = getDaemonInfo();
|
|
1671
1702
|
if (!info) return [];
|
|
1672
|
-
const { IPCClient: IPCClient2 } = await import("./ipcClient-
|
|
1703
|
+
const { IPCClient: IPCClient2 } = await import("./ipcClient-URHJKNEH.js");
|
|
1673
1704
|
const client = new IPCClient2(info.port, origin);
|
|
1674
1705
|
let tunnels;
|
|
1675
1706
|
try {
|
|
@@ -1716,7 +1747,7 @@ async function stopDaemon() {
|
|
|
1716
1747
|
if (!info) return { ok: false, error: "No daemon is running." };
|
|
1717
1748
|
let daemonErrors = [];
|
|
1718
1749
|
try {
|
|
1719
|
-
const { IPCClient: IPCClient2 } = await import("./ipcClient-
|
|
1750
|
+
const { IPCClient: IPCClient2 } = await import("./ipcClient-URHJKNEH.js");
|
|
1720
1751
|
const client = new IPCClient2(info.port);
|
|
1721
1752
|
const result = await client.shutdown();
|
|
1722
1753
|
logger.debug("Sent shutdown command to daemon", { result });
|
|
@@ -2338,6 +2369,9 @@ export {
|
|
|
2338
2369
|
getRemoteManagementState,
|
|
2339
2370
|
getDaemonInfo,
|
|
2340
2371
|
isDaemonRunning,
|
|
2372
|
+
daemonIpcVersion,
|
|
2373
|
+
isIpcCompatible,
|
|
2374
|
+
ipcMismatchMessage,
|
|
2341
2375
|
startDaemon,
|
|
2342
2376
|
ensureDaemonRunning,
|
|
2343
2377
|
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,
|
|
@@ -6,12 +6,12 @@ import {
|
|
|
6
6
|
trackIPCTunnelStart,
|
|
7
7
|
trackTunnelStart,
|
|
8
8
|
trackTunnelStop
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
9
|
+
} from "./chunk-GA6UTBOU.js";
|
|
10
|
+
import "./chunk-HS3OI4OL.js";
|
|
11
11
|
import "./chunk-MT44NAXX.js";
|
|
12
12
|
import {
|
|
13
13
|
DaemonHost
|
|
14
|
-
} from "./chunk-
|
|
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
|
@@ -2443,6 +2443,19 @@ var init_remote_schema = __esm({
|
|
|
2443
2443
|
haProxy: import_zod.z.enum([import_pinggy3.HaProxyVersion.V1, import_pinggy3.HaProxyVersion.V2]).optional(),
|
|
2444
2444
|
serve: import_zod.z.string().optional(),
|
|
2445
2445
|
optional: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
|
|
2446
|
+
}).superRefine((config, ctx) => {
|
|
2447
|
+
if (!config.haProxy) {
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
const entries = Array.isArray(config.forwarding) ? config.forwarding : [];
|
|
2451
|
+
const primaryType = entries.length > 0 ? entries[0].type : void 0;
|
|
2452
|
+
if (primaryType !== import_pinggy3.TunnelType.Tcp) {
|
|
2453
|
+
ctx.addIssue({
|
|
2454
|
+
code: "custom",
|
|
2455
|
+
path: ["haProxy"],
|
|
2456
|
+
message: `haProxy can only be used with "tcp" tunnels, but the first forwarding rule has type: ${primaryType ?? import_pinggy3.TunnelType.Http}`
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2446
2459
|
});
|
|
2447
2460
|
TUNNEL_CONFIG_V1_KEYS = Object.keys(
|
|
2448
2461
|
TunnelConfigV1Schema.shape
|
|
@@ -2778,11 +2791,12 @@ var init_handler = __esm({
|
|
|
2778
2791
|
});
|
|
2779
2792
|
|
|
2780
2793
|
// src/daemon/ipc/ipcRoutes.ts
|
|
2781
|
-
var Route, DaemonHost, ShutdownStatus, SessionMode;
|
|
2794
|
+
var IPC_VERSION, Route, DaemonHost, ShutdownStatus, SessionMode;
|
|
2782
2795
|
var init_ipcRoutes = __esm({
|
|
2783
2796
|
"src/daemon/ipc/ipcRoutes.ts"() {
|
|
2784
2797
|
"use strict";
|
|
2785
2798
|
init_cjs_shims();
|
|
2799
|
+
IPC_VERSION = 1;
|
|
2786
2800
|
Route = {
|
|
2787
2801
|
Ping: "GET /ping",
|
|
2788
2802
|
ListTunnels: "GET /tunnels",
|
|
@@ -3039,7 +3053,7 @@ function printHelpMessage() {
|
|
|
3039
3053
|
console.log(" x:noreverseproxy Disable built-in reverse-proxy header injection");
|
|
3040
3054
|
console.log(" x:localservertls:host Connect to local HTTPS server with SNI");
|
|
3041
3055
|
console.log(" x:passpreflight Pass CORS preflight requests unchanged");
|
|
3042
|
-
console.log(" x:haproxy[:v1|:v2] Enable HAProxy PROXY protocol (default v1)");
|
|
3056
|
+
console.log(" x:haproxy[:v1|:v2] Enable HAProxy PROXY protocol, TCP tunnels only (default v1)");
|
|
3043
3057
|
console.log(" a:Key:Val Add header");
|
|
3044
3058
|
console.log(" u:Key:Val Update header");
|
|
3045
3059
|
console.log(" r:Key Remove header");
|
|
@@ -4645,6 +4659,20 @@ function hasRemoteManagement(values) {
|
|
|
4645
4659
|
const token = values["remote-management"];
|
|
4646
4660
|
return typeof token === "string" && token.trim().length > 0;
|
|
4647
4661
|
}
|
|
4662
|
+
function validateHaProxy(finalConfig) {
|
|
4663
|
+
if (!finalConfig.haProxy) {
|
|
4664
|
+
return null;
|
|
4665
|
+
}
|
|
4666
|
+
const forwarding = finalConfig.forwarding;
|
|
4667
|
+
const entries = Array.isArray(forwarding) ? forwarding : [];
|
|
4668
|
+
const primaryType = entries.length > 0 ? entries[0].type : void 0;
|
|
4669
|
+
if (primaryType === import_pinggy5.TunnelType.Tcp) {
|
|
4670
|
+
return null;
|
|
4671
|
+
}
|
|
4672
|
+
return new Error(
|
|
4673
|
+
`HAProxy PROXY protocol (x:haproxy) can only be used with TCP tunnels, but this tunnel is "${primaryType ?? import_pinggy5.TunnelType.Http}". Make it a TCP tunnel (--type tcp, or the tcp@ server prefix) or drop x:haproxy.`
|
|
4674
|
+
);
|
|
4675
|
+
}
|
|
4648
4676
|
function buildFinalConfig(values, positionals, baseConfig) {
|
|
4649
4677
|
let token;
|
|
4650
4678
|
let server;
|
|
@@ -4707,6 +4735,10 @@ function buildFinalConfig(values, positionals, baseConfig) {
|
|
|
4707
4735
|
}
|
|
4708
4736
|
normalizeForwardingToArray(finalConfig, type);
|
|
4709
4737
|
parseArgs2(finalConfig, remainingPositionals);
|
|
4738
|
+
const haProxyErr = validateHaProxy(finalConfig);
|
|
4739
|
+
if (haProxyErr instanceof Error) {
|
|
4740
|
+
throw haProxyErr;
|
|
4741
|
+
}
|
|
4710
4742
|
storeJson(finalConfig, saveconf);
|
|
4711
4743
|
return finalConfig;
|
|
4712
4744
|
}
|
|
@@ -7496,9 +7528,13 @@ async function handleDaemon(args) {
|
|
|
7496
7528
|
}
|
|
7497
7529
|
}
|
|
7498
7530
|
async function handleDaemonStart() {
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7531
|
+
const info = getDaemonInfo();
|
|
7532
|
+
if (info) {
|
|
7533
|
+
if (!isIpcCompatible(info)) {
|
|
7534
|
+
printer_default.error(ipcMismatchMessage(info));
|
|
7535
|
+
process.exit(1);
|
|
7536
|
+
}
|
|
7537
|
+
printer_default.print(import_picocolors6.default.yellow(`Daemon already running (PID ${info.pid}, port ${info.port}). Use: pinggy daemon status for details.`));
|
|
7502
7538
|
return;
|
|
7503
7539
|
}
|
|
7504
7540
|
const autoConfigs = getAutoStartConfigs();
|
|
@@ -7511,8 +7547,8 @@ async function handleDaemonStart() {
|
|
|
7511
7547
|
printer_default.print(import_picocolors6.default.cyanBright("Starting daemon..."));
|
|
7512
7548
|
}
|
|
7513
7549
|
try {
|
|
7514
|
-
const
|
|
7515
|
-
printer_default.success(`Daemon started PID ${
|
|
7550
|
+
const info2 = await startDaemon();
|
|
7551
|
+
printer_default.success(`Daemon started PID ${info2.pid}.`);
|
|
7516
7552
|
} catch (err) {
|
|
7517
7553
|
printer_default.error(`Failed to start daemon: ${errorMessage(err)}`);
|
|
7518
7554
|
process.exit(1);
|
|
@@ -7549,6 +7585,10 @@ function handleDaemonStatus() {
|
|
|
7549
7585
|
printer_default.print(` Started: ${info.startedAt}`);
|
|
7550
7586
|
printer_default.print(` Uptime: ${uptimeStr}`);
|
|
7551
7587
|
printer_default.print(` Host: ${info.host === DaemonHost.APP ? "Pinggy app (in-process)" : "standalone (CLI)"}`);
|
|
7588
|
+
printer_default.print(` IPC: v${daemonIpcVersion(info)}`);
|
|
7589
|
+
if (!isIpcCompatible(info)) {
|
|
7590
|
+
printer_default.print(import_picocolors6.default.yellow(` Warning: ${ipcMismatchMessage(info)}`));
|
|
7591
|
+
}
|
|
7552
7592
|
}
|
|
7553
7593
|
var import_picocolors6;
|
|
7554
7594
|
var init_daemonCommandsHandler = __esm({
|
|
@@ -8542,7 +8582,8 @@ var init_ipcServer = __esm({
|
|
|
8542
8582
|
[Route.Ping]: () => ({
|
|
8543
8583
|
status: "ok",
|
|
8544
8584
|
pid: process.pid,
|
|
8545
|
-
uptime: Math.floor((Date.now() - this.startedAt) / 1e3)
|
|
8585
|
+
uptime: Math.floor((Date.now() - this.startedAt) / 1e3),
|
|
8586
|
+
ipcVersion: IPC_VERSION
|
|
8546
8587
|
}),
|
|
8547
8588
|
[Route.ListTunnels]: async () => {
|
|
8548
8589
|
const res = await this.ops.handleListV2();
|
|
@@ -9357,7 +9398,8 @@ async function runDaemonChild(opts = {}) {
|
|
|
9357
9398
|
pid: process.pid,
|
|
9358
9399
|
port,
|
|
9359
9400
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9360
|
-
host: daemonHost
|
|
9401
|
+
host: daemonHost,
|
|
9402
|
+
ipcVersion: IPC_VERSION
|
|
9361
9403
|
};
|
|
9362
9404
|
writeDaemonInfo(info);
|
|
9363
9405
|
logger.info("Daemon info written", info);
|
|
@@ -9444,6 +9486,19 @@ function getDaemonInfo() {
|
|
|
9444
9486
|
function isDaemonRunning() {
|
|
9445
9487
|
return getDaemonInfo() !== null;
|
|
9446
9488
|
}
|
|
9489
|
+
function daemonIpcVersion(info) {
|
|
9490
|
+
return info.ipcVersion ?? 0;
|
|
9491
|
+
}
|
|
9492
|
+
function isIpcCompatible(info) {
|
|
9493
|
+
return daemonIpcVersion(info) === IPC_VERSION;
|
|
9494
|
+
}
|
|
9495
|
+
function ipcMismatchMessage(info) {
|
|
9496
|
+
const versions = `daemon IPC v${daemonIpcVersion(info)}, this CLI expects v${IPC_VERSION}`;
|
|
9497
|
+
if (info.host === DaemonHost.APP) {
|
|
9498
|
+
return `The Pinggy app is running a daemon from a different version (${versions}). Update or restart the Pinggy app.`;
|
|
9499
|
+
}
|
|
9500
|
+
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.`;
|
|
9501
|
+
}
|
|
9447
9502
|
function getDaemonSpawnArgs() {
|
|
9448
9503
|
return {
|
|
9449
9504
|
command: process.execPath,
|
|
@@ -9490,7 +9545,10 @@ async function startDaemon() {
|
|
|
9490
9545
|
}
|
|
9491
9546
|
async function ensureDaemonRunning() {
|
|
9492
9547
|
const existing = getDaemonInfo();
|
|
9493
|
-
if (existing)
|
|
9548
|
+
if (existing) {
|
|
9549
|
+
if (!isIpcCompatible(existing)) throw new Error(ipcMismatchMessage(existing));
|
|
9550
|
+
return existing;
|
|
9551
|
+
}
|
|
9494
9552
|
if (process.versions.electron) {
|
|
9495
9553
|
const { runDaemonChild: runDaemonChild2 } = await Promise.resolve().then(() => (init_daemonChild(), daemonChild_exports));
|
|
9496
9554
|
const handle = await runDaemonChild2({
|
|
@@ -9503,7 +9561,8 @@ async function ensureDaemonRunning() {
|
|
|
9503
9561
|
pid: handle.pid,
|
|
9504
9562
|
port: handle.port,
|
|
9505
9563
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9506
|
-
host: DaemonHost.APP
|
|
9564
|
+
host: DaemonHost.APP,
|
|
9565
|
+
ipcVersion: IPC_VERSION
|
|
9507
9566
|
};
|
|
9508
9567
|
}
|
|
9509
9568
|
return startDaemon();
|
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;
|
|
@@ -1085,11 +1086,7 @@ declare class TunnelClient {
|
|
|
1085
1086
|
* Close the WebSocket connection and cleanup.
|
|
1086
1087
|
*/
|
|
1087
1088
|
close(): void;
|
|
1088
|
-
ping(): Promise<
|
|
1089
|
-
status: string;
|
|
1090
|
-
pid: number;
|
|
1091
|
-
uptime: number;
|
|
1092
|
-
}>;
|
|
1089
|
+
ping(): Promise<PingResponse>;
|
|
1093
1090
|
handleStartV2(config: object, noWait?: boolean, mode?: SessionMode): Promise<TunnelResponseV2 | ErrorResponse>;
|
|
1094
1091
|
handleStart(config: object, noWait?: boolean, mode?: SessionMode): Promise<TunnelResponse | ErrorResponse>;
|
|
1095
1092
|
handleUpdateConfig(config: object, noWait?: boolean): Promise<TunnelResponse | ErrorResponse>;
|
|
@@ -1143,6 +1140,8 @@ interface DaemonInfo {
|
|
|
1143
1140
|
port: number;
|
|
1144
1141
|
startedAt: string;
|
|
1145
1142
|
host?: DaemonHost;
|
|
1143
|
+
/** IPC protocol version the daemon speaks. Absent in daemon.json written by older builds. */
|
|
1144
|
+
ipcVersion?: number;
|
|
1146
1145
|
}
|
|
1147
1146
|
|
|
1148
1147
|
/**
|
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;
|
|
@@ -1085,11 +1086,7 @@ declare class TunnelClient {
|
|
|
1085
1086
|
* Close the WebSocket connection and cleanup.
|
|
1086
1087
|
*/
|
|
1087
1088
|
close(): void;
|
|
1088
|
-
ping(): Promise<
|
|
1089
|
-
status: string;
|
|
1090
|
-
pid: number;
|
|
1091
|
-
uptime: number;
|
|
1092
|
-
}>;
|
|
1089
|
+
ping(): Promise<PingResponse>;
|
|
1093
1090
|
handleStartV2(config: object, noWait?: boolean, mode?: SessionMode): Promise<TunnelResponseV2 | ErrorResponse>;
|
|
1094
1091
|
handleStart(config: object, noWait?: boolean, mode?: SessionMode): Promise<TunnelResponse | ErrorResponse>;
|
|
1095
1092
|
handleUpdateConfig(config: object, noWait?: boolean): Promise<TunnelResponse | ErrorResponse>;
|
|
@@ -1143,6 +1140,8 @@ interface DaemonInfo {
|
|
|
1143
1140
|
port: number;
|
|
1144
1141
|
startedAt: string;
|
|
1145
1142
|
host?: DaemonHost;
|
|
1143
|
+
/** IPC protocol version the daemon speaks. Absent in daemon.json written by older builds. */
|
|
1144
|
+
ipcVersion?: number;
|
|
1146
1145
|
}
|
|
1147
1146
|
|
|
1148
1147
|
/**
|
package/dist/index.js
CHANGED
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
initiateRemoteManagement,
|
|
15
15
|
isDaemonRunning,
|
|
16
16
|
startRemoteManagement
|
|
17
|
-
} from "./chunk-
|
|
18
|
-
import "./chunk-
|
|
17
|
+
} from "./chunk-HS3OI4OL.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-
|
|
142
|
+
await import("./main-M5PCTJTB.js");
|
|
143
143
|
}
|
|
144
144
|
verifyAndLoad().catch((err) => {
|
|
145
145
|
printer_default.fatal(`Failed to start CLI:, ${err}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runDaemonChild
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-GA6UTBOU.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-
|
|
16
|
+
} from "./chunk-HS3OI4OL.js";
|
|
17
17
|
import "./chunk-MT44NAXX.js";
|
|
18
18
|
import {
|
|
19
19
|
DaemonHost
|
|
20
|
-
} from "./chunk-
|
|
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.
|
|
3
|
+
"version": "0.5.7",
|
|
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.
|
|
55
|
+
"@pinggy/pinggy": "^0.5.1",
|
|
56
56
|
"blessed": "^0.1.81",
|
|
57
57
|
"clipboardy": "^5.0.0",
|
|
58
58
|
"mime": "^4.1.0",
|