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 pollingInterval = null;
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 peer = new ndc.PeerConnection("agent", {
28
- iceServers: ["stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302"]
29
- });
30
- const channels = /* @__PURE__ */ new Map();
31
- const pendingInboundBinaryMeta = /* @__PURE__ */ new Map();
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 setupChannel(name, dc) {
57
- channels.set(name, dc);
58
- dc.onMessage((data) => {
59
- if (typeof data === "string") {
60
- const msg = decodeMessage(data);
61
- if (msg) {
62
- if (msg.type === "binary" && !msg.data) {
63
- pendingInboundBinaryMeta.set(name, msg);
64
- return;
65
- }
66
- buffer.messages.push({ channel: name, msg, timestamp: Date.now() });
67
- }
68
- } else {
69
- const pendingMeta = pendingInboundBinaryMeta.get(name);
70
- if (pendingMeta) pendingInboundBinaryMeta.delete(name);
71
- const binMsg = pendingMeta ? {
72
- id: pendingMeta.id,
73
- type: "binary",
74
- data: data.toString("base64"),
75
- meta: { ...pendingMeta.meta, size: data.length }
76
- } : {
77
- id: `bin-${Date.now()}`,
78
- type: "binary",
79
- data: data.toString("base64"),
80
- meta: { size: data.length }
81
- };
82
- buffer.messages.push({ channel: name, msg: binMsg, timestamp: Date.now() });
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
- openDataChannel(CONTROL_CHANNEL);
87
- openDataChannel(CHANNELS.CHAT);
88
- openDataChannel(CHANNELS.CANVAS);
89
- const localCandidates = [];
90
- peer.onLocalCandidate((candidate, mid) => {
91
- localCandidates.push(JSON.stringify({ candidate, sdpMid: mid }));
92
- });
93
- peer.onStateChange((state) => {
94
- if (state === "connected") {
95
- connected = true;
96
- if (pollingInterval) {
97
- clearInterval(pollingInterval);
98
- pollingInterval = null;
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
- peer.onDataChannel((dc) => {
105
- setupChannel(dc.getLabel(), dc);
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 (pollingInterval) clearInterval(pollingInterval);
156
- for (const dc of channels.values()) dc.close();
157
- peer.close();
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", () => void shutdown());
175
- process.on("SIGINT", () => void shutdown());
176
- let offer;
177
- try {
178
- offer = await generateOffer(peer, OFFER_TIMEOUT_MS);
179
- } catch (error) {
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
- const dc = channels.get(channel);
249
- let targetDc = dc;
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("--title <title>", "Tunnel title").option("--expires <duration>", "Auto-close after duration (e.g. 4h, 1d)", "24h").option("--foreground", "Run in foreground (don't fork)").action(async (opts) => {
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-K7Z7FUFN.js");
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
- const response = await ipcCall(socketPath, {
340
- method: "read",
341
- params: { channel: opts.channel }
342
- }).catch(() => null);
343
- if (!response) {
344
- console.error("Daemon disconnected.");
345
- process.exit(1);
346
- }
347
- if (response.messages && response.messages.length > 0) {
348
- for (const m of response.messages) {
349
- console.log(JSON.stringify(m));
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
- await new Promise((r) => setTimeout(r, 1e3));
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.2");
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
- let isPublic;
647
- if (opts.private) isPublic = false;
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,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getTunnelWriteReadinessError,
3
3
  startDaemon
4
- } from "./chunk-3RFMAQOM.js";
4
+ } from "./chunk-YHFY3TW5.js";
5
5
  import "./chunk-56IKFMJ2.js";
6
6
  export {
7
7
  getTunnelWriteReadinessError,
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-BV423NLA.js";
4
4
  import {
5
5
  startDaemon
6
- } from "./chunk-3RFMAQOM.js";
6
+ } from "./chunk-YHFY3TW5.js";
7
7
  import "./chunk-56IKFMJ2.js";
8
8
 
9
9
  // src/tunnel-daemon-entry.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pubblue",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "CLI tool for publishing static content via pub.blue",
5
5
  "type": "module",
6
6
  "bin": {