pubblue 0.4.4 → 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
|
|
@@ -226,25 +227,136 @@ function getFollowReadDelayMs(disconnected, consecutiveFailures) {
|
|
|
226
227
|
function resolveTunnelIdSelection(tunnelIdArg, tunnelOpt) {
|
|
227
228
|
return tunnelOpt || tunnelIdArg;
|
|
228
229
|
}
|
|
230
|
+
function buildDaemonForkStdio(logFd) {
|
|
231
|
+
return ["ignore", logFd, logFd, "ipc"];
|
|
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
|
+
}
|
|
229
270
|
function registerTunnelCommands(program2) {
|
|
230
271
|
const tunnel = program2.command("tunnel").description("P2P encrypted tunnel to browser");
|
|
231
|
-
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) => {
|
|
232
273
|
await ensureNodeDatachannelAvailable();
|
|
233
274
|
const apiClient = createApiClient();
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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);
|
|
239
350
|
if (opts.foreground) {
|
|
240
|
-
const { startDaemon } = await import("./tunnel-daemon-
|
|
241
|
-
console.log(`Tunnel started: ${
|
|
242
|
-
console.log(`Tunnel ID: ${
|
|
243
|
-
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");
|
|
244
356
|
console.log("Running in foreground. Press Ctrl+C to stop.");
|
|
245
357
|
try {
|
|
246
358
|
await startDaemon({
|
|
247
|
-
tunnelId:
|
|
359
|
+
tunnelId: target.tunnelId,
|
|
248
360
|
apiClient,
|
|
249
361
|
socketPath,
|
|
250
362
|
infoPath
|
|
@@ -255,16 +367,33 @@ function registerTunnelCommands(program2) {
|
|
|
255
367
|
process.exit(1);
|
|
256
368
|
}
|
|
257
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
|
+
}
|
|
258
388
|
const daemonScript = path2.join(import.meta.dirname, "tunnel-daemon-entry.js");
|
|
259
389
|
const config = getConfig();
|
|
260
|
-
const logPath = tunnelLogPath(result.tunnelId);
|
|
261
390
|
const daemonLogFd = fs2.openSync(logPath, "a");
|
|
262
391
|
const child = fork(daemonScript, [], {
|
|
263
392
|
detached: true,
|
|
264
|
-
stdio:
|
|
393
|
+
stdio: buildDaemonForkStdio(daemonLogFd),
|
|
265
394
|
env: {
|
|
266
395
|
...process.env,
|
|
267
|
-
PUBBLUE_DAEMON_TUNNEL_ID:
|
|
396
|
+
PUBBLUE_DAEMON_TUNNEL_ID: target.tunnelId,
|
|
268
397
|
PUBBLUE_DAEMON_BASE_URL: config.baseUrl,
|
|
269
398
|
PUBBLUE_DAEMON_API_KEY: config.apiKey,
|
|
270
399
|
PUBBLUE_DAEMON_SOCKET: socketPath,
|
|
@@ -272,18 +401,51 @@ function registerTunnelCommands(program2) {
|
|
|
272
401
|
}
|
|
273
402
|
});
|
|
274
403
|
fs2.closeSync(daemonLogFd);
|
|
404
|
+
if (child.connected) {
|
|
405
|
+
child.disconnect();
|
|
406
|
+
}
|
|
275
407
|
child.unref();
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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"}`);
|
|
279
417
|
console.error(`Daemon log: ${logPath}`);
|
|
280
|
-
|
|
281
|
-
|
|
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);
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
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);
|
|
282
442
|
process.exit(1);
|
|
283
443
|
}
|
|
284
|
-
console.log(`Tunnel started: ${
|
|
285
|
-
console.log(`Tunnel ID: ${
|
|
286
|
-
console.log(`Expires: ${new Date(
|
|
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");
|
|
287
449
|
console.log(`Daemon log: ${logPath}`);
|
|
288
450
|
}
|
|
289
451
|
});
|
|
@@ -441,19 +603,16 @@ function registerTunnelCommands(program2) {
|
|
|
441
603
|
});
|
|
442
604
|
tunnel.command("close").description("Close a tunnel and stop its daemon").argument("<tunnelId>", "Tunnel ID").action(async (tunnelId) => {
|
|
443
605
|
const socketPath = getSocketPath(tunnelId);
|
|
444
|
-
let closedByDaemon = false;
|
|
445
606
|
try {
|
|
446
|
-
|
|
447
|
-
closedByDaemon = daemonResult.ok;
|
|
607
|
+
await ipcCall(socketPath, { method: "close", params: {} });
|
|
448
608
|
} catch {
|
|
449
|
-
closedByDaemon = false;
|
|
450
609
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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)) {
|
|
457
616
|
console.error(`Failed to close tunnel ${tunnelId}: ${message}`);
|
|
458
617
|
process.exit(1);
|
|
459
618
|
}
|
|
@@ -477,23 +636,65 @@ async function resolveActiveTunnel() {
|
|
|
477
636
|
console.error(`Multiple active tunnels: ${active.join(", ")}. Specify one.`);
|
|
478
637
|
process.exit(1);
|
|
479
638
|
}
|
|
480
|
-
function waitForDaemonReady(
|
|
639
|
+
function waitForDaemonReady({
|
|
640
|
+
child,
|
|
641
|
+
infoPath,
|
|
642
|
+
socketPath,
|
|
643
|
+
timeoutMs
|
|
644
|
+
}) {
|
|
481
645
|
return new Promise((resolve3) => {
|
|
482
646
|
let settled = false;
|
|
483
|
-
|
|
647
|
+
let pollInFlight = false;
|
|
648
|
+
let lastIpcError = null;
|
|
649
|
+
const done = (result) => {
|
|
484
650
|
if (settled) return;
|
|
485
651
|
settled = true;
|
|
486
652
|
clearInterval(poll);
|
|
487
653
|
clearTimeout(timeout);
|
|
488
|
-
|
|
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}` });
|
|
489
660
|
};
|
|
490
|
-
child.on("exit",
|
|
661
|
+
child.on("exit", onExit);
|
|
491
662
|
const poll = setInterval(() => {
|
|
492
|
-
if (fs2.existsSync(infoPath))
|
|
493
|
-
|
|
494
|
-
|
|
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);
|
|
495
677
|
});
|
|
496
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
|
+
}
|
|
497
698
|
|
|
498
699
|
// src/lib/api.ts
|
|
499
700
|
var PubApiClient = class {
|
|
@@ -626,7 +827,7 @@ function readFile(filePath) {
|
|
|
626
827
|
basename: path3.basename(resolved)
|
|
627
828
|
};
|
|
628
829
|
}
|
|
629
|
-
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");
|
|
630
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) => {
|
|
631
832
|
try {
|
|
632
833
|
const apiKey = await resolveConfigureApiKey(opts);
|