pubblue 0.4.2 → 0.4.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.
|
@@ -10,6 +10,9 @@ import * as fs from "fs";
|
|
|
10
10
|
import * as net from "net";
|
|
11
11
|
import * as path from "path";
|
|
12
12
|
var OFFER_TIMEOUT_MS = 1e4;
|
|
13
|
+
var SIGNAL_POLL_WAITING_MS = 500;
|
|
14
|
+
var SIGNAL_POLL_CONNECTED_MS = 2e3;
|
|
15
|
+
var RECOVERY_DELAY_MS = 1e3;
|
|
13
16
|
var NOT_CONNECTED_WRITE_ERROR = "No browser connected. Ask the user to open the tunnel URL first, then retry.";
|
|
14
17
|
function getTunnelWriteReadinessError(isConnected) {
|
|
15
18
|
return isConnected ? null : NOT_CONNECTED_WRITE_ERROR;
|
|
@@ -19,17 +22,74 @@ async function startDaemon(config) {
|
|
|
19
22
|
const ndc = await import("node-datachannel");
|
|
20
23
|
const buffer = { messages: [] };
|
|
21
24
|
const startTime = Date.now();
|
|
25
|
+
let stopped = false;
|
|
22
26
|
let connected = false;
|
|
23
|
-
let
|
|
24
|
-
let lastBrowserCandidateCount = 0;
|
|
27
|
+
let recovering = false;
|
|
25
28
|
let remoteDescriptionApplied = false;
|
|
29
|
+
let lastBrowserCandidateCount = 0;
|
|
30
|
+
let lastSentCandidateCount = 0;
|
|
26
31
|
const pendingRemoteCandidates = [];
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
const localCandidates = [];
|
|
33
|
+
let peer = null;
|
|
34
|
+
let channels = /* @__PURE__ */ new Map();
|
|
35
|
+
let pendingInboundBinaryMeta = /* @__PURE__ */ new Map();
|
|
36
|
+
let pollingTimer = null;
|
|
37
|
+
let localCandidateInterval = null;
|
|
38
|
+
let localCandidateStopTimer = null;
|
|
39
|
+
let recoveryTimer = null;
|
|
40
|
+
function clearPollingTimer() {
|
|
41
|
+
if (pollingTimer) {
|
|
42
|
+
clearTimeout(pollingTimer);
|
|
43
|
+
pollingTimer = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function clearLocalCandidateTimers() {
|
|
47
|
+
if (localCandidateInterval) {
|
|
48
|
+
clearInterval(localCandidateInterval);
|
|
49
|
+
localCandidateInterval = null;
|
|
50
|
+
}
|
|
51
|
+
if (localCandidateStopTimer) {
|
|
52
|
+
clearTimeout(localCandidateStopTimer);
|
|
53
|
+
localCandidateStopTimer = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function clearRecoveryTimer() {
|
|
57
|
+
if (recoveryTimer) {
|
|
58
|
+
clearTimeout(recoveryTimer);
|
|
59
|
+
recoveryTimer = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function setupChannel(name, dc) {
|
|
63
|
+
channels.set(name, dc);
|
|
64
|
+
dc.onMessage((data) => {
|
|
65
|
+
if (typeof data === "string") {
|
|
66
|
+
const msg = decodeMessage(data);
|
|
67
|
+
if (!msg) return;
|
|
68
|
+
if (msg.type === "binary" && !msg.data) {
|
|
69
|
+
pendingInboundBinaryMeta.set(name, msg);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
buffer.messages.push({ channel: name, msg, timestamp: Date.now() });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const pendingMeta = pendingInboundBinaryMeta.get(name);
|
|
76
|
+
if (pendingMeta) pendingInboundBinaryMeta.delete(name);
|
|
77
|
+
const binMsg = pendingMeta ? {
|
|
78
|
+
id: pendingMeta.id,
|
|
79
|
+
type: "binary",
|
|
80
|
+
data: data.toString("base64"),
|
|
81
|
+
meta: { ...pendingMeta.meta, size: data.length }
|
|
82
|
+
} : {
|
|
83
|
+
id: `bin-${Date.now()}`,
|
|
84
|
+
type: "binary",
|
|
85
|
+
data: data.toString("base64"),
|
|
86
|
+
meta: { size: data.length }
|
|
87
|
+
};
|
|
88
|
+
buffer.messages.push({ channel: name, msg: binMsg, timestamp: Date.now() });
|
|
89
|
+
});
|
|
90
|
+
}
|
|
32
91
|
function openDataChannel(name) {
|
|
92
|
+
if (!peer) throw new Error("PeerConnection not initialized");
|
|
33
93
|
const existing = channels.get(name);
|
|
34
94
|
if (existing) return existing;
|
|
35
95
|
const dc = peer.createDataChannel(name, { ordered: true });
|
|
@@ -53,57 +113,159 @@ async function startDaemon(config) {
|
|
|
53
113
|
});
|
|
54
114
|
});
|
|
55
115
|
}
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
116
|
+
function resetNegotiationState() {
|
|
117
|
+
connected = false;
|
|
118
|
+
remoteDescriptionApplied = false;
|
|
119
|
+
lastBrowserCandidateCount = 0;
|
|
120
|
+
lastSentCandidateCount = 0;
|
|
121
|
+
pendingRemoteCandidates.length = 0;
|
|
122
|
+
localCandidates.length = 0;
|
|
123
|
+
clearLocalCandidateTimers();
|
|
124
|
+
}
|
|
125
|
+
function startLocalCandidateFlush() {
|
|
126
|
+
clearLocalCandidateTimers();
|
|
127
|
+
localCandidateInterval = setInterval(async () => {
|
|
128
|
+
if (localCandidates.length <= lastSentCandidateCount) return;
|
|
129
|
+
const newOnes = localCandidates.slice(lastSentCandidateCount);
|
|
130
|
+
lastSentCandidateCount = localCandidates.length;
|
|
131
|
+
await apiClient.signal(tunnelId, { candidates: newOnes }).catch(() => {
|
|
132
|
+
});
|
|
133
|
+
}, 500);
|
|
134
|
+
localCandidateStopTimer = setTimeout(() => {
|
|
135
|
+
clearLocalCandidateTimers();
|
|
136
|
+
}, 3e4);
|
|
137
|
+
}
|
|
138
|
+
function attachPeerHandlers(currentPeer) {
|
|
139
|
+
currentPeer.onLocalCandidate((candidate, mid) => {
|
|
140
|
+
if (stopped || currentPeer !== peer) return;
|
|
141
|
+
localCandidates.push(JSON.stringify({ candidate, sdpMid: mid }));
|
|
142
|
+
});
|
|
143
|
+
currentPeer.onStateChange((state) => {
|
|
144
|
+
if (stopped || currentPeer !== peer) return;
|
|
145
|
+
if (state === "connected") {
|
|
146
|
+
connected = true;
|
|
147
|
+
return;
|
|
83
148
|
}
|
|
149
|
+
if (state === "disconnected" || state === "failed") {
|
|
150
|
+
connected = false;
|
|
151
|
+
scheduleRecovery();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
currentPeer.onDataChannel((dc) => {
|
|
155
|
+
if (stopped || currentPeer !== peer) return;
|
|
156
|
+
setupChannel(dc.getLabel(), dc);
|
|
84
157
|
});
|
|
85
158
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
159
|
+
function createPeer() {
|
|
160
|
+
const nextPeer = new ndc.PeerConnection("agent", {
|
|
161
|
+
iceServers: ["stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302"]
|
|
162
|
+
});
|
|
163
|
+
peer = nextPeer;
|
|
164
|
+
channels = /* @__PURE__ */ new Map();
|
|
165
|
+
pendingInboundBinaryMeta = /* @__PURE__ */ new Map();
|
|
166
|
+
attachPeerHandlers(nextPeer);
|
|
167
|
+
openDataChannel(CONTROL_CHANNEL);
|
|
168
|
+
openDataChannel(CHANNELS.CHAT);
|
|
169
|
+
openDataChannel(CHANNELS.CANVAS);
|
|
170
|
+
}
|
|
171
|
+
function closeCurrentPeer() {
|
|
172
|
+
for (const dc of channels.values()) {
|
|
173
|
+
try {
|
|
174
|
+
dc.close();
|
|
175
|
+
} catch {
|
|
99
176
|
}
|
|
100
|
-
} else if (state === "disconnected" || state === "failed") {
|
|
101
|
-
connected = false;
|
|
102
177
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
178
|
+
channels.clear();
|
|
179
|
+
pendingInboundBinaryMeta.clear();
|
|
180
|
+
if (peer) {
|
|
181
|
+
try {
|
|
182
|
+
peer.close();
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
185
|
+
peer = null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function scheduleNextPoll(delayMs) {
|
|
189
|
+
if (stopped) return;
|
|
190
|
+
clearPollingTimer();
|
|
191
|
+
pollingTimer = setTimeout(() => {
|
|
192
|
+
void runPollingLoop();
|
|
193
|
+
}, delayMs);
|
|
194
|
+
}
|
|
195
|
+
async function pollSignalingOnce() {
|
|
196
|
+
const tunnel = await apiClient.get(tunnelId);
|
|
197
|
+
if (tunnel.browserAnswer && !remoteDescriptionApplied) {
|
|
198
|
+
if (!peer) return;
|
|
199
|
+
try {
|
|
200
|
+
const answer = JSON.parse(tunnel.browserAnswer);
|
|
201
|
+
peer.setRemoteDescription(answer.sdp, answer.type);
|
|
202
|
+
remoteDescriptionApplied = true;
|
|
203
|
+
while (pendingRemoteCandidates.length > 0) {
|
|
204
|
+
const next = pendingRemoteCandidates.shift();
|
|
205
|
+
if (!next) break;
|
|
206
|
+
try {
|
|
207
|
+
peer.addRemoteCandidate(next.candidate, next.sdpMid);
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (tunnel.browserCandidates.length > lastBrowserCandidateCount) {
|
|
215
|
+
const newCandidates = tunnel.browserCandidates.slice(lastBrowserCandidateCount);
|
|
216
|
+
lastBrowserCandidateCount = tunnel.browserCandidates.length;
|
|
217
|
+
for (const c of newCandidates) {
|
|
218
|
+
try {
|
|
219
|
+
const parsed = JSON.parse(c);
|
|
220
|
+
if (typeof parsed.candidate !== "string") continue;
|
|
221
|
+
const sdpMid = typeof parsed.sdpMid === "string" ? parsed.sdpMid : "0";
|
|
222
|
+
if (!remoteDescriptionApplied) {
|
|
223
|
+
pendingRemoteCandidates.push({ candidate: parsed.candidate, sdpMid });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (!peer) continue;
|
|
227
|
+
peer.addRemoteCandidate(parsed.candidate, sdpMid);
|
|
228
|
+
} catch {
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function runPollingLoop() {
|
|
234
|
+
if (stopped) return;
|
|
235
|
+
try {
|
|
236
|
+
await pollSignalingOnce();
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
239
|
+
scheduleNextPoll(remoteDescriptionApplied ? SIGNAL_POLL_CONNECTED_MS : SIGNAL_POLL_WAITING_MS);
|
|
240
|
+
}
|
|
241
|
+
async function runNegotiationCycle() {
|
|
242
|
+
if (!peer) throw new Error("PeerConnection not initialized");
|
|
243
|
+
resetNegotiationState();
|
|
244
|
+
const offer = await generateOffer(peer, OFFER_TIMEOUT_MS);
|
|
245
|
+
await apiClient.signal(tunnelId, { offer });
|
|
246
|
+
startLocalCandidateFlush();
|
|
247
|
+
}
|
|
248
|
+
async function recoverPeer() {
|
|
249
|
+
if (stopped || recovering) return;
|
|
250
|
+
recovering = true;
|
|
251
|
+
try {
|
|
252
|
+
closeCurrentPeer();
|
|
253
|
+
createPeer();
|
|
254
|
+
await runNegotiationCycle();
|
|
255
|
+
} finally {
|
|
256
|
+
recovering = false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function scheduleRecovery(delayMs = RECOVERY_DELAY_MS) {
|
|
260
|
+
if (stopped || recovering || recoveryTimer) return;
|
|
261
|
+
recoveryTimer = setTimeout(() => {
|
|
262
|
+
recoveryTimer = null;
|
|
263
|
+
if (stopped || connected) return;
|
|
264
|
+
void recoverPeer().catch(() => {
|
|
265
|
+
if (!stopped) scheduleRecovery(delayMs);
|
|
266
|
+
});
|
|
267
|
+
}, delayMs);
|
|
268
|
+
}
|
|
107
269
|
if (fs.existsSync(socketPath)) {
|
|
108
270
|
let stale = true;
|
|
109
271
|
try {
|
|
@@ -123,6 +285,7 @@ async function startDaemon(config) {
|
|
|
123
285
|
throw new Error(`Daemon already running (socket: ${socketPath})`);
|
|
124
286
|
}
|
|
125
287
|
}
|
|
288
|
+
createPeer();
|
|
126
289
|
const ipcServer = net.createServer((conn) => {
|
|
127
290
|
let data = "";
|
|
128
291
|
conn.on("data", (chunk) => {
|
|
@@ -151,10 +314,21 @@ async function startDaemon(config) {
|
|
|
151
314
|
infoPath,
|
|
152
315
|
JSON.stringify({ pid: process.pid, tunnelId, socketPath, startedAt: startTime })
|
|
153
316
|
);
|
|
317
|
+
scheduleNextPoll(0);
|
|
318
|
+
try {
|
|
319
|
+
await runNegotiationCycle();
|
|
320
|
+
} catch (error) {
|
|
321
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
322
|
+
await cleanup();
|
|
323
|
+
throw new Error(`Failed to generate WebRTC offer: ${message}`);
|
|
324
|
+
}
|
|
154
325
|
async function cleanup() {
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
326
|
+
if (stopped) return;
|
|
327
|
+
stopped = true;
|
|
328
|
+
clearPollingTimer();
|
|
329
|
+
clearLocalCandidateTimers();
|
|
330
|
+
clearRecoveryTimer();
|
|
331
|
+
closeCurrentPeer();
|
|
158
332
|
ipcServer.close();
|
|
159
333
|
try {
|
|
160
334
|
fs.unlinkSync(socketPath);
|
|
@@ -171,72 +345,12 @@ async function startDaemon(config) {
|
|
|
171
345
|
await cleanup();
|
|
172
346
|
process.exit(0);
|
|
173
347
|
}
|
|
174
|
-
process.on("SIGTERM", () =>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
181
|
-
await cleanup();
|
|
182
|
-
throw new Error(`Failed to generate WebRTC offer: ${message}`);
|
|
183
|
-
}
|
|
184
|
-
await apiClient.signal(tunnelId, { offer });
|
|
185
|
-
setTimeout(async () => {
|
|
186
|
-
if (localCandidates.length > 0) {
|
|
187
|
-
await apiClient.signal(tunnelId, { candidates: localCandidates }).catch(() => {
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
}, 1e3);
|
|
191
|
-
let lastSentCandidateCount = 0;
|
|
192
|
-
const candidateInterval = setInterval(async () => {
|
|
193
|
-
if (localCandidates.length > lastSentCandidateCount) {
|
|
194
|
-
const newOnes = localCandidates.slice(lastSentCandidateCount);
|
|
195
|
-
lastSentCandidateCount = localCandidates.length;
|
|
196
|
-
await apiClient.signal(tunnelId, { candidates: newOnes }).catch(() => {
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
}, 500);
|
|
200
|
-
setTimeout(() => clearInterval(candidateInterval), 3e4);
|
|
201
|
-
pollingInterval = setInterval(async () => {
|
|
202
|
-
try {
|
|
203
|
-
const tunnel = await apiClient.get(tunnelId);
|
|
204
|
-
if (tunnel.browserAnswer && !remoteDescriptionApplied) {
|
|
205
|
-
try {
|
|
206
|
-
const answer = JSON.parse(tunnel.browserAnswer);
|
|
207
|
-
peer.setRemoteDescription(answer.sdp, answer.type);
|
|
208
|
-
remoteDescriptionApplied = true;
|
|
209
|
-
while (pendingRemoteCandidates.length > 0) {
|
|
210
|
-
const next = pendingRemoteCandidates.shift();
|
|
211
|
-
if (!next) break;
|
|
212
|
-
try {
|
|
213
|
-
peer.addRemoteCandidate(next.candidate, next.sdpMid);
|
|
214
|
-
} catch {
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
} catch {
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (tunnel.browserCandidates.length > lastBrowserCandidateCount) {
|
|
221
|
-
const newCandidates = tunnel.browserCandidates.slice(lastBrowserCandidateCount);
|
|
222
|
-
lastBrowserCandidateCount = tunnel.browserCandidates.length;
|
|
223
|
-
for (const c of newCandidates) {
|
|
224
|
-
try {
|
|
225
|
-
const parsed = JSON.parse(c);
|
|
226
|
-
if (typeof parsed.candidate !== "string") continue;
|
|
227
|
-
const sdpMid = typeof parsed.sdpMid === "string" ? parsed.sdpMid : "0";
|
|
228
|
-
if (!remoteDescriptionApplied) {
|
|
229
|
-
pendingRemoteCandidates.push({ candidate: parsed.candidate, sdpMid });
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
peer.addRemoteCandidate(parsed.candidate, sdpMid);
|
|
233
|
-
} catch {
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
} catch {
|
|
238
|
-
}
|
|
239
|
-
}, 500);
|
|
348
|
+
process.on("SIGTERM", () => {
|
|
349
|
+
void shutdown();
|
|
350
|
+
});
|
|
351
|
+
process.on("SIGINT", () => {
|
|
352
|
+
void shutdown();
|
|
353
|
+
});
|
|
240
354
|
async function handleIpcRequest(req) {
|
|
241
355
|
switch (req.method) {
|
|
242
356
|
case "write": {
|
|
@@ -245,12 +359,8 @@ async function startDaemon(config) {
|
|
|
245
359
|
if (readinessError) return { ok: false, error: readinessError };
|
|
246
360
|
const msg = req.params.msg;
|
|
247
361
|
const binaryBase64 = typeof req.params.binaryBase64 === "string" ? req.params.binaryBase64 : void 0;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (!targetDc) {
|
|
251
|
-
const newDc = openDataChannel(channel);
|
|
252
|
-
targetDc = newDc;
|
|
253
|
-
}
|
|
362
|
+
let targetDc = channels.get(channel);
|
|
363
|
+
if (!targetDc) targetDc = openDataChannel(channel);
|
|
254
364
|
try {
|
|
255
365
|
await waitForChannelOpen(targetDc);
|
|
256
366
|
} catch (error) {
|
|
@@ -287,10 +397,7 @@ async function startDaemon(config) {
|
|
|
287
397
|
return { ok: true, messages: msgs };
|
|
288
398
|
}
|
|
289
399
|
case "channels": {
|
|
290
|
-
const chList = [...channels.keys()].map((name) => ({
|
|
291
|
-
name,
|
|
292
|
-
direction: "bidi"
|
|
293
|
-
}));
|
|
400
|
+
const chList = [...channels.keys()].map((name) => ({ name, direction: "bidi" }));
|
|
294
401
|
return { ok: true, channels: chList };
|
|
295
402
|
}
|
|
296
403
|
case "status": {
|
package/dist/index.js
CHANGED
|
@@ -215,19 +215,22 @@ function isDaemonRunning(tunnelId) {
|
|
|
215
215
|
return false;
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
|
+
function getFollowReadDelayMs(disconnected, consecutiveFailures) {
|
|
219
|
+
if (!disconnected) return 1e3;
|
|
220
|
+
return Math.min(5e3, 1e3 * 2 ** Math.min(consecutiveFailures, 3));
|
|
221
|
+
}
|
|
218
222
|
function registerTunnelCommands(program2) {
|
|
219
223
|
const tunnel = program2.command("tunnel").description("P2P encrypted tunnel to browser");
|
|
220
|
-
tunnel.command("start").description("Start a new tunnel (spawns background daemon)").option("--
|
|
224
|
+
tunnel.command("start").description("Start a new tunnel (spawns background daemon)").option("--expires <duration>", "Auto-close after duration (e.g. 4h, 1d)", "24h").option("--foreground", "Run in foreground (don't fork)").action(async (opts) => {
|
|
221
225
|
await ensureNodeDatachannelAvailable();
|
|
222
226
|
const apiClient = createApiClient();
|
|
223
227
|
const result = await apiClient.create({
|
|
224
|
-
title: opts.title,
|
|
225
228
|
expiresIn: opts.expires
|
|
226
229
|
});
|
|
227
230
|
const socketPath = getSocketPath(result.tunnelId);
|
|
228
231
|
const infoPath = tunnelInfoPath(result.tunnelId);
|
|
229
232
|
if (opts.foreground) {
|
|
230
|
-
const { startDaemon } = await import("./tunnel-daemon-
|
|
233
|
+
const { startDaemon } = await import("./tunnel-daemon-QPXIGRW7.js");
|
|
231
234
|
console.log(`Tunnel started: ${result.url}`);
|
|
232
235
|
console.log(`Tunnel ID: ${result.tunnelId}`);
|
|
233
236
|
console.log(`Expires: ${new Date(result.expiresAt).toISOString()}`);
|
|
@@ -335,21 +338,33 @@ function registerTunnelCommands(program2) {
|
|
|
335
338
|
const tunnelId = tunnelIdArg || await resolveActiveTunnel();
|
|
336
339
|
const socketPath = getSocketPath(tunnelId);
|
|
337
340
|
if (opts.follow) {
|
|
341
|
+
let consecutiveFailures = 0;
|
|
342
|
+
let warnedDisconnected = false;
|
|
338
343
|
while (true) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
344
|
+
try {
|
|
345
|
+
const response = await ipcCall(socketPath, {
|
|
346
|
+
method: "read",
|
|
347
|
+
params: { channel: opts.channel }
|
|
348
|
+
});
|
|
349
|
+
if (warnedDisconnected) {
|
|
350
|
+
console.error("Daemon reconnected.");
|
|
351
|
+
warnedDisconnected = false;
|
|
352
|
+
}
|
|
353
|
+
consecutiveFailures = 0;
|
|
354
|
+
if (response.messages && response.messages.length > 0) {
|
|
355
|
+
for (const m of response.messages) {
|
|
356
|
+
console.log(JSON.stringify(m));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
consecutiveFailures += 1;
|
|
361
|
+
if (!warnedDisconnected) {
|
|
362
|
+
console.error("Daemon disconnected. Waiting for recovery...");
|
|
363
|
+
warnedDisconnected = true;
|
|
350
364
|
}
|
|
351
365
|
}
|
|
352
|
-
|
|
366
|
+
const delayMs = getFollowReadDelayMs(warnedDisconnected, consecutiveFailures);
|
|
367
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
353
368
|
}
|
|
354
369
|
} else {
|
|
355
370
|
const response = await ipcCall(socketPath, {
|
|
@@ -395,9 +410,7 @@ function registerTunnelCommands(program2) {
|
|
|
395
410
|
const age = Math.floor((Date.now() - t.createdAt) / 6e4);
|
|
396
411
|
const running = isDaemonRunning(t.tunnelId) ? "running" : "no daemon";
|
|
397
412
|
const conn = t.hasConnection ? "connected" : "waiting";
|
|
398
|
-
console.log(
|
|
399
|
-
` ${t.tunnelId} ${t.title || "(untitled)"} ${conn} ${running} ${age}m ago`
|
|
400
|
-
);
|
|
413
|
+
console.log(` ${t.tunnelId} ${conn} ${running} ${age}m ago`);
|
|
401
414
|
}
|
|
402
415
|
});
|
|
403
416
|
tunnel.command("close").description("Close a tunnel and stop its daemon").argument("<tunnelId>", "Tunnel ID").action(async (tunnelId) => {
|
|
@@ -568,6 +581,14 @@ async function resolveConfigureApiKey(opts) {
|
|
|
568
581
|
}
|
|
569
582
|
return readApiKeyFromPrompt();
|
|
570
583
|
}
|
|
584
|
+
function resolveVisibilityFlags(opts) {
|
|
585
|
+
if (opts.public && opts.private) {
|
|
586
|
+
throw new Error(`Use only one of --public or --private for ${opts.commandName}.`);
|
|
587
|
+
}
|
|
588
|
+
if (opts.public) return true;
|
|
589
|
+
if (opts.private) return false;
|
|
590
|
+
return void 0;
|
|
591
|
+
}
|
|
571
592
|
function readFile(filePath) {
|
|
572
593
|
const resolved = path3.resolve(filePath);
|
|
573
594
|
if (!fs3.existsSync(resolved)) {
|
|
@@ -579,7 +600,7 @@ function readFile(filePath) {
|
|
|
579
600
|
basename: path3.basename(resolved)
|
|
580
601
|
};
|
|
581
602
|
}
|
|
582
|
-
program.name("pubblue").description("Publish static content and get shareable URLs").version("0.4.
|
|
603
|
+
program.name("pubblue").description("Publish static content and get shareable URLs").version("0.4.3");
|
|
583
604
|
program.command("configure").description("Configure the CLI with your API key").option("--api-key <key>", "Your API key (less secure: appears in shell history)").option("--api-key-stdin", "Read API key from stdin").action(async (opts) => {
|
|
584
605
|
try {
|
|
585
606
|
const apiKey = await resolveConfigureApiKey(opts);
|
|
@@ -591,7 +612,7 @@ program.command("configure").description("Configure the CLI with your API key").
|
|
|
591
612
|
process.exit(1);
|
|
592
613
|
}
|
|
593
614
|
});
|
|
594
|
-
program.command("create").description("Create a new publication").argument("[file]", "Path to the file (reads stdin if omitted)").option("--slug <slug>", "Custom slug for the URL").option("--title <title>", "Title for the publication").option("--private", "Make the publication private (default)").option("--expires <duration>", "Auto-delete after duration (e.g. 1h, 24h, 7d)").action(
|
|
615
|
+
program.command("create").description("Create a new publication").argument("[file]", "Path to the file (reads stdin if omitted)").option("--slug <slug>", "Custom slug for the URL").option("--title <title>", "Title for the publication").option("--public", "Make the publication public").option("--private", "Make the publication private (default)").option("--expires <duration>", "Auto-delete after duration (e.g. 1h, 24h, 7d)").action(
|
|
595
616
|
async (fileArg, opts) => {
|
|
596
617
|
const client = createClient();
|
|
597
618
|
let content;
|
|
@@ -603,12 +624,17 @@ program.command("create").description("Create a new publication").argument("[fil
|
|
|
603
624
|
} else {
|
|
604
625
|
content = await readFromStdin();
|
|
605
626
|
}
|
|
627
|
+
const resolvedVisibility = resolveVisibilityFlags({
|
|
628
|
+
public: opts.public,
|
|
629
|
+
private: opts.private,
|
|
630
|
+
commandName: "create"
|
|
631
|
+
});
|
|
606
632
|
const result = await client.create({
|
|
607
633
|
content,
|
|
608
634
|
filename,
|
|
609
635
|
title: opts.title,
|
|
610
636
|
slug: opts.slug,
|
|
611
|
-
isPublic: false,
|
|
637
|
+
isPublic: resolvedVisibility ?? false,
|
|
612
638
|
expiresIn: opts.expires
|
|
613
639
|
});
|
|
614
640
|
console.log(`Created: ${result.url}`);
|
|
@@ -633,7 +659,7 @@ program.command("get").description("Get details of a publication").argument("<sl
|
|
|
633
659
|
console.log(` Updated: ${new Date(pub.updatedAt).toLocaleDateString()}`);
|
|
634
660
|
console.log(` Size: ${pub.content.length} bytes`);
|
|
635
661
|
});
|
|
636
|
-
program.command("update").description("Update a publication's content and/or metadata").argument("<slug>", "Slug of the publication to update").option("--file <file>", "New content from file").option("--title <title>", "New title").option("--private", "Make the publication private").option("--slug <newSlug>", "Rename the slug").action(
|
|
662
|
+
program.command("update").description("Update a publication's content and/or metadata").argument("<slug>", "Slug of the publication to update").option("--file <file>", "New content from file").option("--title <title>", "New title").option("--public", "Make the publication public").option("--private", "Make the publication private").option("--slug <newSlug>", "Rename the slug").action(
|
|
637
663
|
async (slug, opts) => {
|
|
638
664
|
const client = createClient();
|
|
639
665
|
let content;
|
|
@@ -643,8 +669,11 @@ program.command("update").description("Update a publication's content and/or met
|
|
|
643
669
|
content = file.content;
|
|
644
670
|
filename = file.basename;
|
|
645
671
|
}
|
|
646
|
-
|
|
647
|
-
|
|
672
|
+
const isPublic = resolveVisibilityFlags({
|
|
673
|
+
public: opts.public,
|
|
674
|
+
private: opts.private,
|
|
675
|
+
commandName: "update"
|
|
676
|
+
});
|
|
648
677
|
const result = await client.update({
|
|
649
678
|
slug,
|
|
650
679
|
content,
|