pubblue 0.4.5 → 0.4.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.
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
// src/lib/tunnel-api.ts
|
|
2
|
+
var TunnelApiError = class extends Error {
|
|
3
|
+
constructor(message, status, retryAfterSeconds) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
7
|
+
this.name = "TunnelApiError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
2
10
|
var TunnelApiClient = class {
|
|
3
11
|
constructor(baseUrl, apiKey) {
|
|
4
12
|
this.baseUrl = baseUrl;
|
|
@@ -14,13 +22,22 @@ var TunnelApiClient = class {
|
|
|
14
22
|
...options.headers
|
|
15
23
|
}
|
|
16
24
|
});
|
|
25
|
+
const retryAfterHeader = res.headers.get("Retry-After");
|
|
26
|
+
const parsedRetryAfterSeconds = typeof retryAfterHeader === "string" ? Number.parseInt(retryAfterHeader, 10) : void 0;
|
|
27
|
+
const retryAfterSeconds = parsedRetryAfterSeconds !== void 0 && Number.isFinite(parsedRetryAfterSeconds) ? parsedRetryAfterSeconds : void 0;
|
|
17
28
|
let data;
|
|
18
29
|
try {
|
|
19
30
|
data = await res.json();
|
|
20
31
|
} catch {
|
|
21
32
|
data = {};
|
|
22
33
|
}
|
|
23
|
-
if (!res.ok)
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
if (res.status === 429) {
|
|
36
|
+
const retrySuffix = retryAfterSeconds !== void 0 ? ` Retry after ${retryAfterSeconds}s.` : "";
|
|
37
|
+
throw new TunnelApiError(`Rate limit exceeded.${retrySuffix}`, res.status, retryAfterSeconds);
|
|
38
|
+
}
|
|
39
|
+
throw new TunnelApiError(data.error || `Request failed: ${res.status}`, res.status);
|
|
40
|
+
}
|
|
24
41
|
return data;
|
|
25
42
|
}
|
|
26
43
|
async create(opts) {
|
|
@@ -53,5 +70,6 @@ var TunnelApiClient = class {
|
|
|
53
70
|
};
|
|
54
71
|
|
|
55
72
|
export {
|
|
73
|
+
TunnelApiError,
|
|
56
74
|
TunnelApiClient
|
|
57
75
|
};
|
|
@@ -485,13 +485,6 @@ async function startDaemon(config) {
|
|
|
485
485
|
});
|
|
486
486
|
});
|
|
487
487
|
ipcServer.listen(socketPath);
|
|
488
|
-
const infoDir = path.dirname(infoPath);
|
|
489
|
-
if (!fs.existsSync(infoDir)) fs.mkdirSync(infoDir, { recursive: true });
|
|
490
|
-
fs.writeFileSync(
|
|
491
|
-
infoPath,
|
|
492
|
-
JSON.stringify({ pid: process.pid, tunnelId, socketPath, startedAt: startTime })
|
|
493
|
-
);
|
|
494
|
-
scheduleNextPoll(0);
|
|
495
488
|
try {
|
|
496
489
|
await runNegotiationCycle();
|
|
497
490
|
} catch (error) {
|
|
@@ -500,6 +493,13 @@ async function startDaemon(config) {
|
|
|
500
493
|
await cleanup();
|
|
501
494
|
throw new Error(`Failed to generate WebRTC offer: ${message}`);
|
|
502
495
|
}
|
|
496
|
+
const infoDir = path.dirname(infoPath);
|
|
497
|
+
if (!fs.existsSync(infoDir)) fs.mkdirSync(infoDir, { recursive: true });
|
|
498
|
+
fs.writeFileSync(
|
|
499
|
+
infoPath,
|
|
500
|
+
JSON.stringify({ pid: process.pid, tunnelId, socketPath, startedAt: startTime })
|
|
501
|
+
);
|
|
502
|
+
scheduleNextPoll(0);
|
|
503
503
|
async function cleanup() {
|
|
504
504
|
if (stopped) return;
|
|
505
505
|
stopped = true;
|
|
@@ -518,9 +518,6 @@ async function startDaemon(config) {
|
|
|
518
518
|
} catch (error) {
|
|
519
519
|
debugLog("failed to remove daemon info file during cleanup", error);
|
|
520
520
|
}
|
|
521
|
-
await apiClient.close(tunnelId).catch((error) => {
|
|
522
|
-
markError("failed to close tunnel on API during cleanup", error);
|
|
523
|
-
});
|
|
524
521
|
}
|
|
525
522
|
async function shutdown() {
|
|
526
523
|
await cleanup();
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
TunnelApiClient
|
|
4
|
-
|
|
3
|
+
TunnelApiClient,
|
|
4
|
+
TunnelApiError
|
|
5
|
+
} from "./chunk-F4VBBHDR.js";
|
|
5
6
|
import {
|
|
6
7
|
CHANNELS,
|
|
7
8
|
generateMessageId
|
|
@@ -229,25 +230,133 @@ function resolveTunnelIdSelection(tunnelIdArg, tunnelOpt) {
|
|
|
229
230
|
function buildDaemonForkStdio(logFd) {
|
|
230
231
|
return ["ignore", logFd, logFd, "ipc"];
|
|
231
232
|
}
|
|
233
|
+
function getPublicTunnelUrl(tunnelId) {
|
|
234
|
+
const base = process.env.PUBBLUE_PUBLIC_URL || "https://pub.blue";
|
|
235
|
+
return `${base.replace(/\/$/, "")}/t/${tunnelId}`;
|
|
236
|
+
}
|
|
237
|
+
function pickReusableTunnel(tunnels, nowMs = Date.now()) {
|
|
238
|
+
const active = tunnels.filter((t) => t.status === "active" && t.expiresAt > nowMs).sort((a, b) => b.createdAt - a.createdAt);
|
|
239
|
+
return active[0] ?? null;
|
|
240
|
+
}
|
|
241
|
+
function readLogTail(logPath, maxChars = 4e3) {
|
|
242
|
+
if (!fs2.existsSync(logPath)) return null;
|
|
243
|
+
try {
|
|
244
|
+
const content = fs2.readFileSync(logPath, "utf-8");
|
|
245
|
+
if (content.length <= maxChars) return content;
|
|
246
|
+
return content.slice(-maxChars);
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function formatApiError(error) {
|
|
252
|
+
if (error instanceof TunnelApiError) {
|
|
253
|
+
if (error.status === 429 && error.retryAfterSeconds !== void 0) {
|
|
254
|
+
return `Rate limit exceeded. Retry after ${error.retryAfterSeconds}s.`;
|
|
255
|
+
}
|
|
256
|
+
return `${error.message} (HTTP ${error.status})`;
|
|
257
|
+
}
|
|
258
|
+
return error instanceof Error ? error.message : String(error);
|
|
259
|
+
}
|
|
260
|
+
async function cleanupCreatedTunnelOnStartFailure(apiClient, target) {
|
|
261
|
+
if (!target.createdNew) return;
|
|
262
|
+
try {
|
|
263
|
+
await apiClient.close(target.tunnelId);
|
|
264
|
+
} catch (closeError) {
|
|
265
|
+
console.error(
|
|
266
|
+
`Failed to clean up newly created tunnel ${target.tunnelId}: ${formatApiError(closeError)}`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
232
270
|
function registerTunnelCommands(program2) {
|
|
233
271
|
const tunnel = program2.command("tunnel").description("P2P encrypted tunnel to browser");
|
|
234
|
-
tunnel.command("start").description("Start a
|
|
272
|
+
tunnel.command("start").description("Start a tunnel daemon (reuses existing tunnel when possible)").option("--expires <duration>", "Auto-close after duration (e.g. 4h, 1d)", "24h").option("-t, --tunnel <tunnelId>", "Attach/start daemon for an existing tunnel").option("--new", "Always create a new tunnel (skip single-tunnel reuse)").option("--foreground", "Run in foreground (don't fork)").action(async (opts) => {
|
|
235
273
|
await ensureNodeDatachannelAvailable();
|
|
236
274
|
const apiClient = createApiClient();
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
275
|
+
let target = null;
|
|
276
|
+
if (opts.tunnel) {
|
|
277
|
+
try {
|
|
278
|
+
const existing = await apiClient.get(opts.tunnel);
|
|
279
|
+
if (existing.status === "closed" || existing.expiresAt <= Date.now()) {
|
|
280
|
+
console.error(`Tunnel ${opts.tunnel} is closed or expired.`);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
target = {
|
|
284
|
+
createdNew: false,
|
|
285
|
+
expiresAt: existing.expiresAt,
|
|
286
|
+
mode: "existing",
|
|
287
|
+
tunnelId: existing.tunnelId,
|
|
288
|
+
url: getPublicTunnelUrl(existing.tunnelId)
|
|
289
|
+
};
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.error(`Failed to use tunnel ${opts.tunnel}: ${formatApiError(error)}`);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
} else if (!opts.new) {
|
|
295
|
+
try {
|
|
296
|
+
const listed = await apiClient.list();
|
|
297
|
+
const active = listed.filter((t) => t.status === "active" && t.expiresAt > Date.now()).sort((a, b) => b.createdAt - a.createdAt);
|
|
298
|
+
const reusable = pickReusableTunnel(listed);
|
|
299
|
+
if (reusable) {
|
|
300
|
+
target = {
|
|
301
|
+
createdNew: false,
|
|
302
|
+
expiresAt: reusable.expiresAt,
|
|
303
|
+
mode: "existing",
|
|
304
|
+
tunnelId: reusable.tunnelId,
|
|
305
|
+
url: getPublicTunnelUrl(reusable.tunnelId)
|
|
306
|
+
};
|
|
307
|
+
if (active.length > 1) {
|
|
308
|
+
console.error(
|
|
309
|
+
[
|
|
310
|
+
`Multiple active tunnels found: ${active.map((t) => t.tunnelId).join(", ")}`,
|
|
311
|
+
`Reusing most recent active tunnel ${reusable.tunnelId}.`,
|
|
312
|
+
"Use --tunnel <id> to choose explicitly or --new to force creation."
|
|
313
|
+
].join("\n")
|
|
314
|
+
);
|
|
315
|
+
} else {
|
|
316
|
+
console.error(
|
|
317
|
+
`Reusing existing active tunnel ${reusable.tunnelId}. Use --new to force creation.`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.error(`Failed to list tunnels for reuse check: ${formatApiError(error)}`);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (!target) {
|
|
327
|
+
try {
|
|
328
|
+
const created = await apiClient.create({
|
|
329
|
+
expiresIn: opts.expires
|
|
330
|
+
});
|
|
331
|
+
target = {
|
|
332
|
+
createdNew: true,
|
|
333
|
+
expiresAt: created.expiresAt,
|
|
334
|
+
mode: "created",
|
|
335
|
+
tunnelId: created.tunnelId,
|
|
336
|
+
url: created.url
|
|
337
|
+
};
|
|
338
|
+
} catch (error) {
|
|
339
|
+
console.error(`Failed to create tunnel: ${formatApiError(error)}`);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (!target) {
|
|
344
|
+
console.error("Failed to resolve tunnel target.");
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
const socketPath = getSocketPath(target.tunnelId);
|
|
348
|
+
const infoPath = tunnelInfoPath(target.tunnelId);
|
|
349
|
+
const logPath = tunnelLogPath(target.tunnelId);
|
|
242
350
|
if (opts.foreground) {
|
|
243
|
-
const { startDaemon } = await import("./tunnel-daemon-
|
|
244
|
-
console.log(`Tunnel started: ${
|
|
245
|
-
console.log(`Tunnel ID: ${
|
|
246
|
-
console.log(`Expires: ${new Date(
|
|
351
|
+
const { startDaemon } = await import("./tunnel-daemon-4LV6HLYN.js");
|
|
352
|
+
console.log(`Tunnel started: ${target.url}`);
|
|
353
|
+
console.log(`Tunnel ID: ${target.tunnelId}`);
|
|
354
|
+
console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
|
|
355
|
+
if (target.mode === "existing") console.log("Mode: attached existing tunnel");
|
|
247
356
|
console.log("Running in foreground. Press Ctrl+C to stop.");
|
|
248
357
|
try {
|
|
249
358
|
await startDaemon({
|
|
250
|
-
tunnelId:
|
|
359
|
+
tunnelId: target.tunnelId,
|
|
251
360
|
apiClient,
|
|
252
361
|
socketPath,
|
|
253
362
|
infoPath
|
|
@@ -258,16 +367,33 @@ function registerTunnelCommands(program2) {
|
|
|
258
367
|
process.exit(1);
|
|
259
368
|
}
|
|
260
369
|
} else {
|
|
370
|
+
if (isDaemonRunning(target.tunnelId)) {
|
|
371
|
+
try {
|
|
372
|
+
const status = await ipcCall(socketPath, { method: "status", params: {} });
|
|
373
|
+
if (!status.ok) throw new Error(String(status.error || "status check failed"));
|
|
374
|
+
} catch (error) {
|
|
375
|
+
console.error(
|
|
376
|
+
`Daemon process exists but is not responding: ${error instanceof Error ? error.message : String(error)}`
|
|
377
|
+
);
|
|
378
|
+
console.error("Run `pubblue tunnel close <id>` and start again.");
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
console.log(`Tunnel started: ${target.url}`);
|
|
382
|
+
console.log(`Tunnel ID: ${target.tunnelId}`);
|
|
383
|
+
console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
|
|
384
|
+
console.log("Daemon already running for this tunnel.");
|
|
385
|
+
console.log(`Daemon log: ${logPath}`);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
261
388
|
const daemonScript = path2.join(import.meta.dirname, "tunnel-daemon-entry.js");
|
|
262
389
|
const config = getConfig();
|
|
263
|
-
const logPath = tunnelLogPath(result.tunnelId);
|
|
264
390
|
const daemonLogFd = fs2.openSync(logPath, "a");
|
|
265
391
|
const child = fork(daemonScript, [], {
|
|
266
392
|
detached: true,
|
|
267
393
|
stdio: buildDaemonForkStdio(daemonLogFd),
|
|
268
394
|
env: {
|
|
269
395
|
...process.env,
|
|
270
|
-
PUBBLUE_DAEMON_TUNNEL_ID:
|
|
396
|
+
PUBBLUE_DAEMON_TUNNEL_ID: target.tunnelId,
|
|
271
397
|
PUBBLUE_DAEMON_BASE_URL: config.baseUrl,
|
|
272
398
|
PUBBLUE_DAEMON_API_KEY: config.apiKey,
|
|
273
399
|
PUBBLUE_DAEMON_SOCKET: socketPath,
|
|
@@ -279,17 +405,47 @@ function registerTunnelCommands(program2) {
|
|
|
279
405
|
child.disconnect();
|
|
280
406
|
}
|
|
281
407
|
child.unref();
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
408
|
+
console.log(`Starting daemon for tunnel ${target.tunnelId}...`);
|
|
409
|
+
const ready = await waitForDaemonReady({
|
|
410
|
+
child,
|
|
411
|
+
infoPath,
|
|
412
|
+
socketPath,
|
|
413
|
+
timeoutMs: 8e3
|
|
414
|
+
});
|
|
415
|
+
if (!ready.ok) {
|
|
416
|
+
console.error(`Daemon failed to start: ${ready.reason ?? "unknown reason"}`);
|
|
285
417
|
console.error(`Daemon log: ${logPath}`);
|
|
286
|
-
|
|
287
|
-
|
|
418
|
+
const tail = readLogTail(logPath);
|
|
419
|
+
if (tail) {
|
|
420
|
+
console.error("---- daemon log tail ----");
|
|
421
|
+
console.error(tail.trimEnd());
|
|
422
|
+
console.error("---- end daemon log tail ----");
|
|
423
|
+
}
|
|
424
|
+
await cleanupCreatedTunnelOnStartFailure(apiClient, target);
|
|
288
425
|
process.exit(1);
|
|
289
426
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
427
|
+
const offerReady = await waitForAgentOffer({
|
|
428
|
+
apiClient,
|
|
429
|
+
tunnelId: target.tunnelId,
|
|
430
|
+
timeoutMs: 5e3
|
|
431
|
+
});
|
|
432
|
+
if (!offerReady.ok) {
|
|
433
|
+
console.error(`Daemon started but signaling is not ready: ${offerReady.reason}`);
|
|
434
|
+
console.error(`Daemon log: ${logPath}`);
|
|
435
|
+
const tail = readLogTail(logPath);
|
|
436
|
+
if (tail) {
|
|
437
|
+
console.error("---- daemon log tail ----");
|
|
438
|
+
console.error(tail.trimEnd());
|
|
439
|
+
console.error("---- end daemon log tail ----");
|
|
440
|
+
}
|
|
441
|
+
await cleanupCreatedTunnelOnStartFailure(apiClient, target);
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
console.log(`Tunnel started: ${target.url}`);
|
|
445
|
+
console.log(`Tunnel ID: ${target.tunnelId}`);
|
|
446
|
+
console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
|
|
447
|
+
if (target.mode === "existing") console.log("Mode: attached existing tunnel");
|
|
448
|
+
console.log("Daemon health: OK");
|
|
293
449
|
console.log(`Daemon log: ${logPath}`);
|
|
294
450
|
}
|
|
295
451
|
});
|
|
@@ -447,19 +603,16 @@ function registerTunnelCommands(program2) {
|
|
|
447
603
|
});
|
|
448
604
|
tunnel.command("close").description("Close a tunnel and stop its daemon").argument("<tunnelId>", "Tunnel ID").action(async (tunnelId) => {
|
|
449
605
|
const socketPath = getSocketPath(tunnelId);
|
|
450
|
-
let closedByDaemon = false;
|
|
451
606
|
try {
|
|
452
|
-
|
|
453
|
-
closedByDaemon = daemonResult.ok;
|
|
607
|
+
await ipcCall(socketPath, { method: "close", params: {} });
|
|
454
608
|
} catch {
|
|
455
|
-
closedByDaemon = false;
|
|
456
609
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
610
|
+
const apiClient = createApiClient();
|
|
611
|
+
try {
|
|
612
|
+
await apiClient.close(tunnelId);
|
|
613
|
+
} catch (error) {
|
|
614
|
+
const message = formatApiError(error);
|
|
615
|
+
if (!/Tunnel not found/i.test(message)) {
|
|
463
616
|
console.error(`Failed to close tunnel ${tunnelId}: ${message}`);
|
|
464
617
|
process.exit(1);
|
|
465
618
|
}
|
|
@@ -483,23 +636,65 @@ async function resolveActiveTunnel() {
|
|
|
483
636
|
console.error(`Multiple active tunnels: ${active.join(", ")}. Specify one.`);
|
|
484
637
|
process.exit(1);
|
|
485
638
|
}
|
|
486
|
-
function waitForDaemonReady(
|
|
639
|
+
function waitForDaemonReady({
|
|
640
|
+
child,
|
|
641
|
+
infoPath,
|
|
642
|
+
socketPath,
|
|
643
|
+
timeoutMs
|
|
644
|
+
}) {
|
|
487
645
|
return new Promise((resolve3) => {
|
|
488
646
|
let settled = false;
|
|
489
|
-
|
|
647
|
+
let pollInFlight = false;
|
|
648
|
+
let lastIpcError = null;
|
|
649
|
+
const done = (result) => {
|
|
490
650
|
if (settled) return;
|
|
491
651
|
settled = true;
|
|
492
652
|
clearInterval(poll);
|
|
493
653
|
clearTimeout(timeout);
|
|
494
|
-
|
|
654
|
+
child.off("exit", onExit);
|
|
655
|
+
resolve3(result);
|
|
656
|
+
};
|
|
657
|
+
const onExit = (code, signal) => {
|
|
658
|
+
const suffix = signal ? ` (signal ${signal})` : "";
|
|
659
|
+
done({ ok: false, reason: `daemon exited with code ${code ?? 0}${suffix}` });
|
|
495
660
|
};
|
|
496
|
-
child.on("exit",
|
|
661
|
+
child.on("exit", onExit);
|
|
497
662
|
const poll = setInterval(() => {
|
|
498
|
-
if (fs2.existsSync(infoPath))
|
|
499
|
-
|
|
500
|
-
|
|
663
|
+
if (pollInFlight || !fs2.existsSync(infoPath)) return;
|
|
664
|
+
pollInFlight = true;
|
|
665
|
+
void ipcCall(socketPath, { method: "status", params: {} }).then((status) => {
|
|
666
|
+
if (status.ok) done({ ok: true });
|
|
667
|
+
}).catch((error) => {
|
|
668
|
+
lastIpcError = error instanceof Error ? error.message : String(error);
|
|
669
|
+
}).finally(() => {
|
|
670
|
+
pollInFlight = false;
|
|
671
|
+
});
|
|
672
|
+
}, 120);
|
|
673
|
+
const timeout = setTimeout(() => {
|
|
674
|
+
const reason = lastIpcError ? `timed out after ${timeoutMs}ms waiting for daemon readiness (last IPC error: ${lastIpcError})` : `timed out after ${timeoutMs}ms waiting for daemon readiness`;
|
|
675
|
+
done({ ok: false, reason });
|
|
676
|
+
}, timeoutMs);
|
|
501
677
|
});
|
|
502
678
|
}
|
|
679
|
+
async function waitForAgentOffer(params) {
|
|
680
|
+
const startedAt = Date.now();
|
|
681
|
+
let lastError = null;
|
|
682
|
+
while (Date.now() - startedAt < params.timeoutMs) {
|
|
683
|
+
try {
|
|
684
|
+
const tunnel = await params.apiClient.get(params.tunnelId);
|
|
685
|
+
if (typeof tunnel.agentOffer === "string" && tunnel.agentOffer.length > 0) {
|
|
686
|
+
return { ok: true };
|
|
687
|
+
}
|
|
688
|
+
} catch (error) {
|
|
689
|
+
lastError = formatApiError(error);
|
|
690
|
+
}
|
|
691
|
+
await new Promise((resolve3) => setTimeout(resolve3, 150));
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
ok: false,
|
|
695
|
+
reason: lastError ? `agent offer was not published in time (last API error: ${lastError})` : "agent offer was not published in time"
|
|
696
|
+
};
|
|
697
|
+
}
|
|
503
698
|
|
|
504
699
|
// src/lib/api.ts
|
|
505
700
|
var PubApiClient = class {
|
|
@@ -632,7 +827,7 @@ function readFile(filePath) {
|
|
|
632
827
|
basename: path3.basename(resolved)
|
|
633
828
|
};
|
|
634
829
|
}
|
|
635
|
-
program.name("pubblue").description("Publish static content and get shareable URLs").version("0.4.
|
|
830
|
+
program.name("pubblue").description("Publish static content and get shareable URLs").version("0.4.7");
|
|
636
831
|
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) => {
|
|
637
832
|
try {
|
|
638
833
|
const apiKey = await resolveConfigureApiKey(opts);
|