pubblue 0.4.11 → 0.5.0

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/dist/index.js CHANGED
@@ -1,55 +1,54 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- getSocketPath,
4
- ipcCall
5
- } from "./chunk-HJ5LTUHS.js";
6
- import {
7
- TunnelApiClient,
8
- TunnelApiError
9
- } from "./chunk-7NFHPJ76.js";
3
+ PubApiClient,
4
+ PubApiError,
5
+ TEXT_FILE_EXTENSIONS,
6
+ bridgeInfoPath,
7
+ bridgeLogPath,
8
+ buildBridgeProcessEnv,
9
+ buildDaemonForkStdio,
10
+ cleanupLiveOnStartFailure,
11
+ createApiClient,
12
+ ensureBridgeReady,
13
+ ensureNodeDatachannelAvailable,
14
+ failCli,
15
+ formatApiError,
16
+ getConfig,
17
+ getFollowReadDelayMs,
18
+ getMimeType,
19
+ getPublicUrl,
20
+ getTelegramMiniAppUrl,
21
+ isBridgeRunning,
22
+ isDaemonRunning,
23
+ liveInfoPath,
24
+ liveLogPath,
25
+ loadConfig,
26
+ messageContainsPong,
27
+ parseBridgeMode,
28
+ parsePositiveIntegerOption,
29
+ pickReusableLive,
30
+ readBridgeProcessInfo,
31
+ readDaemonProcessInfo,
32
+ readLogTail,
33
+ resolveActiveSlug,
34
+ resolveSlugSelection,
35
+ saveConfig,
36
+ shouldRestartDaemonForCliUpgrade,
37
+ stopBridge,
38
+ stopOtherDaemons,
39
+ toCliFailure,
40
+ waitForAgentOffer,
41
+ waitForDaemonReady,
42
+ waitForProcessExit,
43
+ writeLatestCliVersion
44
+ } from "./chunk-YI45G6AG.js";
10
45
  import {
11
46
  CHANNELS,
12
47
  CONTROL_CHANNEL,
13
- generateMessageId
14
- } from "./chunk-4YTJ2WKF.js";
15
-
16
- // src/lib/cli-error.ts
17
- import { CommanderError } from "commander";
18
- var CliError = class extends Error {
19
- exitCode;
20
- constructor(message, exitCode = 1) {
21
- super(message);
22
- this.name = "CliError";
23
- this.exitCode = exitCode;
24
- }
25
- };
26
- function failCli(message, exitCode = 1) {
27
- throw new CliError(message, exitCode);
28
- }
29
- function toCliFailure(error) {
30
- if (error instanceof CommanderError) {
31
- return {
32
- exitCode: error.exitCode,
33
- message: ""
34
- };
35
- }
36
- if (error instanceof CliError) {
37
- return {
38
- exitCode: error.exitCode,
39
- message: error.message
40
- };
41
- }
42
- if (error instanceof Error) {
43
- return {
44
- exitCode: 1,
45
- message: error.message
46
- };
47
- }
48
- return {
49
- exitCode: 1,
50
- message: String(error)
51
- };
52
- }
48
+ generateMessageId,
49
+ getSocketPath,
50
+ ipcCall
51
+ } from "./chunk-PFZT7M3E.js";
53
52
 
54
53
  // src/program.ts
55
54
  import { Command } from "commander";
@@ -57,131 +56,9 @@ import { Command } from "commander";
57
56
  // src/commands/configure.ts
58
57
  import { createInterface } from "readline/promises";
59
58
 
60
- // src/lib/config.ts
59
+ // src/commands/shared.ts
61
60
  import * as fs from "fs";
62
- import * as os from "os";
63
61
  import * as path from "path";
64
- var DEFAULT_BASE_URL = "https://silent-guanaco-514.convex.site";
65
- function getConfigDir(homeDir) {
66
- const home = homeDir || os.homedir();
67
- return path.join(home, ".config", "pubblue");
68
- }
69
- function getConfigPath(homeDir) {
70
- const dir = getConfigDir(homeDir);
71
- fs.mkdirSync(dir, { recursive: true, mode: 448 });
72
- try {
73
- fs.chmodSync(dir, 448);
74
- } catch {
75
- }
76
- return path.join(dir, "config.json");
77
- }
78
- function loadConfig(homeDir) {
79
- const configPath = getConfigPath(homeDir);
80
- if (!fs.existsSync(configPath)) return null;
81
- const raw = fs.readFileSync(configPath, "utf-8");
82
- return JSON.parse(raw);
83
- }
84
- function saveConfig(config, homeDir) {
85
- const configPath = getConfigPath(homeDir);
86
- fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
87
- `, {
88
- mode: 384
89
- });
90
- try {
91
- fs.chmodSync(configPath, 384);
92
- } catch {
93
- }
94
- }
95
- function getConfig(homeDir) {
96
- const envKey = process.env.PUBBLUE_API_KEY;
97
- const envUrl = process.env.PUBBLUE_URL;
98
- const baseUrl = envUrl || DEFAULT_BASE_URL;
99
- const saved = loadConfig(homeDir);
100
- if (envKey) {
101
- return { apiKey: envKey, baseUrl, bridge: saved?.bridge };
102
- }
103
- if (!saved) {
104
- throw new Error(
105
- "Not configured. Run `pubblue configure` or set PUBBLUE_API_KEY environment variable."
106
- );
107
- }
108
- return {
109
- apiKey: saved.apiKey,
110
- baseUrl,
111
- bridge: saved.bridge
112
- };
113
- }
114
-
115
- // src/commands/shared.ts
116
- import * as fs2 from "fs";
117
- import * as path2 from "path";
118
-
119
- // src/lib/api.ts
120
- var PubApiClient = class {
121
- constructor(baseUrl, apiKey) {
122
- this.baseUrl = baseUrl;
123
- this.apiKey = apiKey;
124
- }
125
- async request(path6, options = {}) {
126
- const url = new URL(path6, this.baseUrl);
127
- const res = await fetch(url, {
128
- ...options,
129
- headers: {
130
- "Content-Type": "application/json",
131
- Authorization: `Bearer ${this.apiKey}`,
132
- ...options.headers
133
- }
134
- });
135
- const data = await res.json();
136
- if (!res.ok) {
137
- throw new Error(data.error || `Request failed with status ${res.status}`);
138
- }
139
- return data;
140
- }
141
- async create(opts) {
142
- return this.request("/api/v1/publications", {
143
- method: "POST",
144
- body: JSON.stringify(opts)
145
- });
146
- }
147
- async get(slug) {
148
- const data = await this.request(`/api/v1/publications/${encodeURIComponent(slug)}`);
149
- return data.publication;
150
- }
151
- async listPage(cursor, limit) {
152
- const params = new URLSearchParams();
153
- if (cursor) params.set("cursor", cursor);
154
- if (limit) params.set("limit", String(limit));
155
- const qs = params.toString();
156
- return this.request(`/api/v1/publications${qs ? `?${qs}` : ""}`);
157
- }
158
- async list() {
159
- const all = [];
160
- let cursor;
161
- do {
162
- const result = await this.listPage(cursor, 100);
163
- all.push(...result.publications);
164
- cursor = result.hasMore ? result.cursor : void 0;
165
- } while (cursor);
166
- return all;
167
- }
168
- async update(opts) {
169
- const { slug, newSlug, ...rest } = opts;
170
- const body = { ...rest };
171
- if (newSlug) body.slug = newSlug;
172
- return this.request(`/api/v1/publications/${encodeURIComponent(slug)}`, {
173
- method: "PATCH",
174
- body: JSON.stringify(body)
175
- });
176
- }
177
- async remove(slug) {
178
- await this.request(`/api/v1/publications/${encodeURIComponent(slug)}`, {
179
- method: "DELETE"
180
- });
181
- }
182
- };
183
-
184
- // src/commands/shared.ts
185
62
  function createClient() {
186
63
  const config = getConfig();
187
64
  return new PubApiClient(config.baseUrl, config.apiKey);
@@ -205,13 +82,13 @@ function resolveVisibilityFlags(opts) {
205
82
  return void 0;
206
83
  }
207
84
  function readFile(filePath) {
208
- const resolved = path2.resolve(filePath);
209
- if (!fs2.existsSync(resolved)) {
85
+ const resolved = path.resolve(filePath);
86
+ if (!fs.existsSync(resolved)) {
210
87
  failCli(`File not found: ${resolved}`);
211
88
  }
212
89
  return {
213
- content: fs2.readFileSync(resolved, "utf-8"),
214
- basename: path2.basename(resolved)
90
+ content: fs.readFileSync(resolved, "utf-8"),
91
+ basename: path.basename(resolved)
215
92
  };
216
93
  }
217
94
 
@@ -278,7 +155,21 @@ function parsePositiveInteger(raw, key) {
278
155
  }
279
156
  return parsed;
280
157
  }
281
- function applyBridgeSet(bridge, key, value) {
158
+ var SUPPORTED_KEYS = [
159
+ "bridge.mode",
160
+ "openclaw.path",
161
+ "openclaw.sessionId",
162
+ "openclaw.threadId",
163
+ "openclaw.canvasReminderEvery",
164
+ "openclaw.deliver",
165
+ "openclaw.deliverChannel",
166
+ "openclaw.replyTo",
167
+ "openclaw.deliverTimeoutMs",
168
+ "openclaw.attachmentDir",
169
+ "openclaw.attachmentMaxBytes",
170
+ "telegram.botToken"
171
+ ];
172
+ function applyConfigSet(bridge, telegram, key, value) {
282
173
  switch (key) {
283
174
  case "bridge.mode":
284
175
  bridge.mode = parseBridgeModeValue(value);
@@ -292,6 +183,9 @@ function applyBridgeSet(bridge, key, value) {
292
183
  case "openclaw.threadId":
293
184
  bridge.threadId = value;
294
185
  return;
186
+ case "openclaw.canvasReminderEvery":
187
+ bridge.canvasReminderEvery = parsePositiveInteger(value, key);
188
+ return;
295
189
  case "openclaw.deliver":
296
190
  bridge.deliver = parseBooleanValue(value, key);
297
191
  return;
@@ -310,26 +204,20 @@ function applyBridgeSet(bridge, key, value) {
310
204
  case "openclaw.attachmentMaxBytes":
311
205
  bridge.attachmentMaxBytes = parsePositiveInteger(value, key);
312
206
  return;
207
+ case "telegram.botToken":
208
+ telegram.botToken = value;
209
+ return;
313
210
  default:
314
211
  throw new Error(
315
212
  [
316
213
  `Unknown config key: ${key}`,
317
214
  "Supported keys:",
318
- " bridge.mode",
319
- " openclaw.path",
320
- " openclaw.sessionId",
321
- " openclaw.threadId",
322
- " openclaw.deliver",
323
- " openclaw.deliverChannel",
324
- " openclaw.replyTo",
325
- " openclaw.deliverTimeoutMs",
326
- " openclaw.attachmentDir",
327
- " openclaw.attachmentMaxBytes"
215
+ ...SUPPORTED_KEYS.map((k) => ` ${k}`)
328
216
  ].join("\n")
329
217
  );
330
218
  }
331
219
  }
332
- function applyBridgeUnset(bridge, key) {
220
+ function applyConfigUnset(bridge, telegram, key) {
333
221
  switch (key) {
334
222
  case "bridge.mode":
335
223
  delete bridge.mode;
@@ -343,6 +231,9 @@ function applyBridgeUnset(bridge, key) {
343
231
  case "openclaw.threadId":
344
232
  delete bridge.threadId;
345
233
  return;
234
+ case "openclaw.canvasReminderEvery":
235
+ delete bridge.canvasReminderEvery;
236
+ return;
346
237
  case "openclaw.deliver":
347
238
  delete bridge.deliver;
348
239
  return;
@@ -361,16 +252,45 @@ function applyBridgeUnset(bridge, key) {
361
252
  case "openclaw.attachmentMaxBytes":
362
253
  delete bridge.attachmentMaxBytes;
363
254
  return;
255
+ case "telegram.botToken":
256
+ delete telegram.botToken;
257
+ delete telegram.botUsername;
258
+ delete telegram.hasMainWebApp;
259
+ return;
364
260
  default:
365
261
  throw new Error(`Unknown config key for --unset: ${key}`);
366
262
  }
367
263
  }
368
- function hasBridgeValues(bridge) {
369
- return Object.values(bridge).some((value) => value !== void 0);
264
+ function hasValues(obj) {
265
+ return Object.values(obj).some((value) => value !== void 0);
266
+ }
267
+ function maskSecret(value) {
268
+ if (value.length <= 8) return "********";
269
+ return `${value.slice(0, 4)}...${value.slice(-4)}`;
270
+ }
271
+ async function telegramGetMe(token) {
272
+ const resp = await fetch(`https://api.telegram.org/bot${token}/getMe`);
273
+ const data = await resp.json();
274
+ if (!data.ok || !data.result?.username) {
275
+ throw new Error(data.description ?? "Invalid bot token");
276
+ }
277
+ return {
278
+ username: data.result.username,
279
+ hasMainWebApp: data.result.has_main_web_app === true
280
+ };
370
281
  }
371
- function maskApiKey(apiKey) {
372
- if (apiKey.length <= 8) return "********";
373
- return `${apiKey.slice(0, 4)}...${apiKey.slice(-4)}`;
282
+ async function telegramSetMenuButton(token, url) {
283
+ const resp = await fetch(`https://api.telegram.org/bot${token}/setChatMenuButton`, {
284
+ method: "POST",
285
+ headers: { "Content-Type": "application/json" },
286
+ body: JSON.stringify({
287
+ menu_button: { type: "web_app", text: "Open", web_app: { url } }
288
+ })
289
+ });
290
+ const data = await resp.json();
291
+ if (!data.ok) {
292
+ throw new Error(data.description ?? "setChatMenuButton failed");
293
+ }
374
294
  }
375
295
  function printConfigSummary(saved) {
376
296
  if (!saved) {
@@ -378,34 +298,50 @@ function printConfigSummary(saved) {
378
298
  return;
379
299
  }
380
300
  console.log("Saved config:");
381
- console.log(` apiKey: ${maskApiKey(saved.apiKey)}`);
382
- if (!saved.bridge || !hasBridgeValues(saved.bridge)) {
301
+ console.log(` apiKey: ${maskSecret(saved.apiKey)}`);
302
+ if (saved.bridge && hasValues(saved.bridge)) {
303
+ console.log(` bridge.mode: ${saved.bridge.mode ?? "(unset)"}`);
304
+ if (saved.bridge.openclawPath) console.log(` openclaw.path: ${saved.bridge.openclawPath}`);
305
+ if (saved.bridge.sessionId) console.log(` openclaw.sessionId: ${saved.bridge.sessionId}`);
306
+ if (saved.bridge.threadId) console.log(` openclaw.threadId: ${saved.bridge.threadId}`);
307
+ if (saved.bridge.canvasReminderEvery !== void 0)
308
+ console.log(` openclaw.canvasReminderEvery: ${saved.bridge.canvasReminderEvery}`);
309
+ if (saved.bridge.deliver !== void 0)
310
+ console.log(` openclaw.deliver: ${saved.bridge.deliver ? "true" : "false"}`);
311
+ if (saved.bridge.deliverChannel)
312
+ console.log(` openclaw.deliverChannel: ${saved.bridge.deliverChannel}`);
313
+ if (saved.bridge.replyTo) console.log(` openclaw.replyTo: ${saved.bridge.replyTo}`);
314
+ if (saved.bridge.deliverTimeoutMs !== void 0)
315
+ console.log(` openclaw.deliverTimeoutMs: ${saved.bridge.deliverTimeoutMs}`);
316
+ if (saved.bridge.attachmentDir)
317
+ console.log(` openclaw.attachmentDir: ${saved.bridge.attachmentDir}`);
318
+ if (saved.bridge.attachmentMaxBytes !== void 0)
319
+ console.log(` openclaw.attachmentMaxBytes: ${saved.bridge.attachmentMaxBytes}`);
320
+ } else {
383
321
  console.log(" bridge: none");
384
- return;
385
322
  }
386
- console.log(` bridge.mode: ${saved.bridge.mode ?? "(unset)"}`);
387
- if (saved.bridge.openclawPath) console.log(` openclaw.path: ${saved.bridge.openclawPath}`);
388
- if (saved.bridge.sessionId) console.log(` openclaw.sessionId: ${saved.bridge.sessionId}`);
389
- if (saved.bridge.threadId) console.log(` openclaw.threadId: ${saved.bridge.threadId}`);
390
- if (saved.bridge.deliver !== void 0)
391
- console.log(` openclaw.deliver: ${saved.bridge.deliver ? "true" : "false"}`);
392
- if (saved.bridge.deliverChannel)
393
- console.log(` openclaw.deliverChannel: ${saved.bridge.deliverChannel}`);
394
- if (saved.bridge.replyTo) console.log(` openclaw.replyTo: ${saved.bridge.replyTo}`);
395
- if (saved.bridge.deliverTimeoutMs !== void 0)
396
- console.log(` openclaw.deliverTimeoutMs: ${saved.bridge.deliverTimeoutMs}`);
397
- if (saved.bridge.attachmentDir)
398
- console.log(` openclaw.attachmentDir: ${saved.bridge.attachmentDir}`);
399
- if (saved.bridge.attachmentMaxBytes !== void 0)
400
- console.log(` openclaw.attachmentMaxBytes: ${saved.bridge.attachmentMaxBytes}`);
323
+ if (saved.telegram?.botToken && saved.telegram.botUsername) {
324
+ console.log(` telegram.botToken: ${maskSecret(saved.telegram.botToken)}`);
325
+ console.log(` telegram.botUsername: @${saved.telegram.botUsername}`);
326
+ if (!saved.telegram.hasMainWebApp) {
327
+ console.log(" INFO: Register Mini App in @BotFather for deep links to open in Telegram");
328
+ }
329
+ } else if (saved.telegram?.botToken) {
330
+ console.log(` telegram.botToken: ${maskSecret(saved.telegram.botToken)}`);
331
+ console.log(" telegram.botUsername: (not resolved)");
332
+ } else {
333
+ console.log(" telegram: not configured");
334
+ console.log(" INFO: Set telegram.botToken to enable Telegram Mini App links");
335
+ console.log(" Example: pubblue configure --set telegram.botToken=<BOT_TOKEN>");
336
+ }
401
337
  }
402
338
  function registerConfigureCommand(program2) {
403
339
  program2.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").option(
404
340
  "--set <key=value>",
405
- "Set advanced config (repeatable). Example: --set openclaw.sessionId=<id>",
341
+ "Set config key (repeatable). Example: --set telegram.botToken=<token>",
406
342
  collectValues,
407
343
  []
408
- ).option("--unset <key>", "Unset advanced config key (repeatable)", collectValues, []).option("--show", "Show saved configuration").action(
344
+ ).option("--unset <key>", "Unset config key (repeatable)", collectValues, []).option("--show", "Show saved configuration").action(
409
345
  async (opts) => {
410
346
  const saved = loadConfig();
411
347
  const hasApiUpdate = Boolean(opts.apiKey || opts.apiKeyStdin);
@@ -431,16 +367,35 @@ function registerConfigureCommand(program2) {
431
367
  }
432
368
  }
433
369
  const nextBridge = { ...saved?.bridge ?? {} };
370
+ const nextTelegram = { ...saved?.telegram ?? {} };
371
+ let telegramTokenChanged = false;
434
372
  for (const entry of opts.set) {
435
373
  const { key, value } = parseSetInput(entry);
436
- applyBridgeSet(nextBridge, key, value);
374
+ applyConfigSet(nextBridge, nextTelegram, key, value);
375
+ if (key === "telegram.botToken") telegramTokenChanged = true;
437
376
  }
438
377
  for (const key of opts.unset) {
439
- applyBridgeUnset(nextBridge, key.trim());
378
+ applyConfigUnset(nextBridge, nextTelegram, key.trim());
379
+ }
380
+ if (telegramTokenChanged && nextTelegram.botToken) {
381
+ console.log("Verifying Telegram bot token...");
382
+ const bot = await telegramGetMe(nextTelegram.botToken);
383
+ nextTelegram.botUsername = bot.username;
384
+ nextTelegram.hasMainWebApp = bot.hasMainWebApp;
385
+ console.log(` Bot: @${bot.username}`);
386
+ await telegramSetMenuButton(nextTelegram.botToken, "https://pub.blue");
387
+ console.log(" Menu button set to https://pub.blue");
388
+ if (!bot.hasMainWebApp) {
389
+ console.log("");
390
+ console.log(" INFO: For deep links to open inside Telegram, register the Mini App:");
391
+ console.log(" @BotFather \u2192 /mybots \u2192 your bot \u2192 Bot Settings \u2192 Configure Mini App");
392
+ console.log(" Set Web App URL to: https://pub.blue");
393
+ }
440
394
  }
441
395
  const nextConfig = {
442
396
  apiKey,
443
- bridge: hasBridgeValues(nextBridge) ? nextBridge : void 0
397
+ bridge: hasValues(nextBridge) ? nextBridge : void 0,
398
+ telegram: hasValues(nextTelegram) ? nextTelegram : void 0
444
399
  };
445
400
  saveConfig(nextConfig);
446
401
  console.log("Configuration saved.");
@@ -451,530 +406,433 @@ function registerConfigureCommand(program2) {
451
406
  );
452
407
  }
453
408
 
454
- // src/commands/publications.ts
455
- function registerPublicationCommands(program2) {
456
- program2.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(
457
- async (fileArg, opts) => {
458
- const client = createClient();
459
- let content;
460
- let filename;
461
- if (fileArg) {
462
- const file = readFile(fileArg);
463
- content = file.content;
464
- filename = file.basename;
465
- } else {
466
- content = await readFromStdin();
467
- }
468
- const resolvedVisibility = resolveVisibilityFlags({
469
- public: opts.public,
470
- private: opts.private,
471
- commandName: "create"
472
- });
473
- const result = await client.create({
474
- content,
475
- filename,
476
- title: opts.title,
477
- slug: opts.slug,
478
- isPublic: resolvedVisibility ?? false,
479
- expiresIn: opts.expires
480
- });
481
- console.log(`Created: ${result.url}`);
482
- if (result.expiresAt) {
483
- console.log(` Expires: ${new Date(result.expiresAt).toISOString()}`);
484
- }
485
- }
486
- );
487
- program2.command("get").description("Get details of a publication").argument("<slug>", "Slug of the publication").option("--content", "Output raw content to stdout (no metadata, pipeable)").action(async (slug, opts) => {
488
- const client = createClient();
489
- const pub = await client.get(slug);
490
- if (opts.content) {
491
- process.stdout.write(pub.content);
492
- return;
493
- }
494
- console.log(` Slug: ${pub.slug}`);
495
- console.log(` Type: ${pub.contentType}`);
496
- if (pub.title) console.log(` Title: ${pub.title}`);
497
- console.log(` Status: ${formatVisibility(pub.isPublic)}`);
498
- if (pub.expiresAt) console.log(` Expires: ${new Date(pub.expiresAt).toISOString()}`);
499
- console.log(` Created: ${new Date(pub.createdAt).toLocaleDateString()}`);
500
- console.log(` Updated: ${new Date(pub.updatedAt).toLocaleDateString()}`);
501
- console.log(` Size: ${pub.content.length} bytes`);
502
- });
503
- program2.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(
504
- async (slug, opts) => {
505
- const client = createClient();
506
- let content;
507
- let filename;
508
- if (opts.file) {
509
- const file = readFile(opts.file);
510
- content = file.content;
511
- filename = file.basename;
512
- }
513
- const isPublic = resolveVisibilityFlags({
514
- public: opts.public,
515
- private: opts.private,
516
- commandName: "update"
517
- });
518
- const result = await client.update({
519
- slug,
520
- content,
521
- filename,
522
- title: opts.title,
523
- isPublic,
524
- newSlug: opts.slug
525
- });
526
- console.log(`Updated: ${result.slug}`);
527
- if (result.title) console.log(` Title: ${result.title}`);
528
- console.log(` Status: ${formatVisibility(result.isPublic)}`);
529
- }
530
- );
531
- program2.command("list").description("List your publications").action(async () => {
532
- const client = createClient();
533
- const pubs = await client.list();
534
- if (pubs.length === 0) {
535
- console.log("No publications.");
536
- return;
537
- }
538
- for (const pub of pubs) {
539
- const date = new Date(pub.createdAt).toLocaleDateString();
540
- const expires = pub.expiresAt ? ` expires:${new Date(pub.expiresAt).toISOString()}` : "";
541
- console.log(
542
- ` ${pub.slug} [${pub.contentType}] ${formatVisibility(pub.isPublic)} ${date}${expires}`
543
- );
544
- }
545
- });
546
- program2.command("delete").description("Delete a publication").argument("<slug>", "Slug of the publication to delete").action(async (slug) => {
547
- const client = createClient();
548
- await client.remove(slug);
549
- console.log(`Deleted: ${slug}`);
550
- });
551
- }
552
-
553
- // src/commands/tunnel/management-commands.ts
554
- import * as fs4 from "fs";
409
+ // src/commands/live.ts
410
+ import * as fs2 from "fs";
411
+ import * as path2 from "path";
555
412
 
556
- // src/commands/tunnel-helpers.ts
557
- import { fork } from "child_process";
558
- import * as fs3 from "fs";
559
- import * as path3 from "path";
560
- var TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
561
- ".txt",
562
- ".md",
563
- ".markdown",
564
- ".json",
565
- ".csv",
566
- ".xml",
567
- ".yaml",
568
- ".yml",
569
- ".js",
570
- ".mjs",
571
- ".cjs",
572
- ".ts",
573
- ".tsx",
574
- ".jsx",
575
- ".css",
576
- ".scss",
577
- ".sass",
578
- ".less",
579
- ".log"
580
- ]);
581
- function getMimeType(filePath) {
582
- const ext = path3.extname(filePath).toLowerCase();
583
- const mimeByExt = {
584
- ".html": "text/html; charset=utf-8",
585
- ".htm": "text/html; charset=utf-8",
586
- ".txt": "text/plain; charset=utf-8",
587
- ".md": "text/markdown; charset=utf-8",
588
- ".markdown": "text/markdown; charset=utf-8",
589
- ".json": "application/json",
590
- ".csv": "text/csv; charset=utf-8",
591
- ".xml": "application/xml",
592
- ".yaml": "application/x-yaml",
593
- ".yml": "application/x-yaml",
594
- ".png": "image/png",
595
- ".jpg": "image/jpeg",
596
- ".jpeg": "image/jpeg",
597
- ".gif": "image/gif",
598
- ".webp": "image/webp",
599
- ".svg": "image/svg+xml",
600
- ".pdf": "application/pdf",
601
- ".zip": "application/zip",
602
- ".mp3": "audio/mpeg",
603
- ".wav": "audio/wav",
604
- ".mp4": "video/mp4"
605
- };
606
- return mimeByExt[ext] || "application/octet-stream";
607
- }
608
- function tunnelInfoDir() {
609
- const dir = path3.join(
610
- process.env.HOME || process.env.USERPROFILE || "/tmp",
611
- ".config",
612
- "pubblue",
613
- "tunnels"
614
- );
615
- if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
616
- return dir;
617
- }
618
- function tunnelInfoPath(tunnelId) {
619
- return path3.join(tunnelInfoDir(), `${tunnelId}.json`);
620
- }
621
- function tunnelLogPath(tunnelId) {
622
- return path3.join(tunnelInfoDir(), `${tunnelId}.log`);
623
- }
624
- function bridgeInfoPath(tunnelId) {
625
- return path3.join(tunnelInfoDir(), `${tunnelId}.bridge.json`);
626
- }
627
- function bridgeLogPath(tunnelId) {
628
- return path3.join(tunnelInfoDir(), `${tunnelId}.bridge.log`);
629
- }
630
- function createApiClient(configOverride) {
631
- const config = configOverride || getConfig();
632
- return new TunnelApiClient(config.baseUrl, config.apiKey);
633
- }
634
- function buildBridgeProcessEnv(bridgeConfig) {
635
- const env = { ...process.env };
636
- if (!bridgeConfig) return env;
637
- const setIfMissing = (key, value) => {
638
- if (value === void 0 || value === null) return;
639
- const current = env[key];
640
- if (typeof current === "string" && current.length > 0) return;
641
- env[key] = String(value);
642
- };
643
- setIfMissing("OPENCLAW_PATH", bridgeConfig.openclawPath);
644
- setIfMissing("OPENCLAW_SESSION_ID", bridgeConfig.sessionId);
645
- setIfMissing("OPENCLAW_THREAD_ID", bridgeConfig.threadId);
646
- if (bridgeConfig.deliver !== void 0) {
647
- setIfMissing("OPENCLAW_DELIVER", bridgeConfig.deliver ? "1" : "0");
648
- }
649
- setIfMissing("OPENCLAW_DELIVER_CHANNEL", bridgeConfig.deliverChannel);
650
- setIfMissing("OPENCLAW_REPLY_TO", bridgeConfig.replyTo);
651
- if (bridgeConfig.deliverTimeoutMs !== void 0) {
652
- setIfMissing("OPENCLAW_DELIVER_TIMEOUT_MS", bridgeConfig.deliverTimeoutMs);
653
- }
654
- setIfMissing("OPENCLAW_ATTACHMENT_DIR", bridgeConfig.attachmentDir);
655
- if (bridgeConfig.attachmentMaxBytes !== void 0) {
656
- setIfMissing("OPENCLAW_ATTACHMENT_MAX_BYTES", bridgeConfig.attachmentMaxBytes);
657
- }
658
- return env;
659
- }
660
- async function ensureNodeDatachannelAvailable() {
661
- try {
662
- await import("node-datachannel");
663
- } catch (error) {
664
- const message = error instanceof Error ? error.message : String(error);
665
- failCli(
666
- [
667
- "node-datachannel native module is not available.",
668
- "Run `pnpm rebuild node-datachannel` in the cli package and retry.",
669
- `Details: ${message}`
670
- ].join("\n")
671
- );
672
- }
673
- }
674
- function isDaemonRunning(tunnelId) {
675
- return readDaemonProcessInfo(tunnelId) !== null;
676
- }
677
- function readDaemonProcessInfo(tunnelId) {
678
- const infoPath = tunnelInfoPath(tunnelId);
679
- if (!fs3.existsSync(infoPath)) return null;
680
- try {
681
- const info = JSON.parse(fs3.readFileSync(infoPath, "utf-8"));
682
- if (!Number.isFinite(info.pid)) throw new Error("invalid daemon pid");
683
- process.kill(info.pid, 0);
684
- return info;
685
- } catch {
686
- try {
687
- fs3.unlinkSync(infoPath);
688
- } catch {
689
- }
690
- return null;
691
- }
692
- }
693
- function readBridgeProcessInfo(tunnelId) {
694
- const infoPath = bridgeInfoPath(tunnelId);
695
- if (!fs3.existsSync(infoPath)) return null;
696
- try {
697
- return JSON.parse(fs3.readFileSync(infoPath, "utf-8"));
698
- } catch {
699
- return null;
700
- }
701
- }
702
- function isBridgeRunning(tunnelId) {
703
- const infoPath = bridgeInfoPath(tunnelId);
704
- if (!fs3.existsSync(infoPath)) return false;
705
- try {
706
- const info = JSON.parse(fs3.readFileSync(infoPath, "utf-8"));
707
- process.kill(info.pid, 0);
708
- return true;
709
- } catch {
710
- try {
711
- fs3.unlinkSync(infoPath);
712
- } catch {
713
- }
714
- return false;
715
- }
716
- }
717
- function stopBridgeProcess(tunnelId) {
718
- const info = readBridgeProcessInfo(tunnelId);
719
- if (!info || !Number.isFinite(info.pid)) return;
720
- try {
721
- process.kill(info.pid, "SIGTERM");
722
- } catch {
723
- }
724
- }
725
- function buildBridgeForkStdio(logFd) {
726
- return ["ignore", logFd, logFd, "ipc"];
727
- }
728
- function getFollowReadDelayMs(disconnected, consecutiveFailures) {
729
- if (!disconnected) return 1e3;
730
- return Math.min(5e3, 1e3 * 2 ** Math.min(consecutiveFailures, 3));
731
- }
732
- function resolveTunnelIdSelection(tunnelIdArg, tunnelOpt) {
733
- return tunnelOpt || tunnelIdArg;
734
- }
735
- function buildDaemonForkStdio(logFd) {
736
- return ["ignore", logFd, logFd, "ipc"];
737
- }
738
- function parsePositiveIntegerOption(raw, optionName) {
739
- const parsed = Number.parseInt(raw, 10);
740
- if (!Number.isFinite(parsed) || parsed <= 0) {
741
- throw new Error(`${optionName} must be a positive integer. Received: ${raw}`);
742
- }
743
- return parsed;
744
- }
745
- function parseBridgeMode(raw) {
746
- const normalized = raw.trim().toLowerCase();
747
- if (normalized === "openclaw" || normalized === "none") {
748
- return normalized;
749
- }
750
- throw new Error(`--bridge must be one of: openclaw, none. Received: ${raw}`);
751
- }
752
- function shouldRestartDaemonForCliUpgrade(daemonCliVersion, currentCliVersion) {
753
- if (!daemonCliVersion || daemonCliVersion.trim().length === 0) return true;
754
- return daemonCliVersion.trim() !== currentCliVersion;
755
- }
756
- function messageContainsPong(payload) {
757
- if (!payload || typeof payload !== "object") return false;
758
- const message = payload.msg;
759
- if (!message || typeof message !== "object") return false;
760
- const type = message.type;
761
- const data = message.data;
762
- return type === "text" && typeof data === "string" && data.trim().toLowerCase() === "pong";
763
- }
764
- function getPublicTunnelUrl(tunnelId) {
765
- const base = process.env.PUBBLUE_PUBLIC_URL || "https://pub.blue";
766
- return `${base.replace(/\/$/, "")}/t/${tunnelId}`;
767
- }
768
- function pickReusableTunnel(tunnels, nowMs = Date.now()) {
769
- const active = tunnels.filter((t) => t.status === "active" && t.expiresAt > nowMs).sort((a, b) => b.createdAt - a.createdAt);
770
- return active[0] ?? null;
771
- }
772
- function readLogTail(logPath, maxChars = 4e3) {
773
- if (!fs3.existsSync(logPath)) return null;
774
- try {
775
- const content = fs3.readFileSync(logPath, "utf-8");
776
- if (content.length <= maxChars) return content;
777
- return content.slice(-maxChars);
778
- } catch {
779
- return null;
780
- }
781
- }
782
- function formatApiError(error) {
783
- if (error instanceof TunnelApiError) {
784
- if (error.status === 429 && error.retryAfterSeconds !== void 0) {
785
- return `Rate limit exceeded. Retry after ${error.retryAfterSeconds}s.`;
786
- }
787
- return `${error.message} (HTTP ${error.status})`;
788
- }
789
- return error instanceof Error ? error.message : String(error);
790
- }
791
- async function cleanupCreatedTunnelOnStartFailure(apiClient, target) {
792
- if (!target.createdNew) return;
793
- try {
794
- await apiClient.close(target.tunnelId);
795
- } catch (closeError) {
796
- console.error(
797
- `Failed to clean up newly created tunnel ${target.tunnelId}: ${formatApiError(closeError)}`
798
- );
413
+ // package.json
414
+ var package_default = {
415
+ name: "pubblue",
416
+ version: "0.5.0",
417
+ description: "CLI tool for publishing content and running interactive sessions via pub.blue",
418
+ type: "module",
419
+ bin: {
420
+ pubblue: "./dist/index.js"
421
+ },
422
+ scripts: {
423
+ build: "tsup src/index.ts src/tunnel-daemon-entry.ts src/tunnel-bridge-entry.ts --format esm --dts --clean",
424
+ dev: "tsup src/index.ts src/tunnel-daemon-entry.ts src/tunnel-bridge-entry.ts --format esm --watch",
425
+ test: "vitest run",
426
+ "test:watch": "vitest",
427
+ lint: "tsc --noEmit"
428
+ },
429
+ dependencies: {
430
+ commander: "^13.0.0",
431
+ "node-datachannel": "^0.32.0"
432
+ },
433
+ devDependencies: {
434
+ "@types/node": "22.10.2",
435
+ tsup: "^8.3.6",
436
+ typescript: "^5.7.2",
437
+ vitest: "^3.0.0"
438
+ },
439
+ files: [
440
+ "dist"
441
+ ],
442
+ repository: {
443
+ type: "git",
444
+ url: "git+https://github.com/xmanatee/pub.git",
445
+ directory: "cli"
446
+ },
447
+ publishConfig: {
448
+ access: "public"
449
+ },
450
+ pnpm: {
451
+ onlyBuiltDependencies: [
452
+ "esbuild",
453
+ "node-datachannel"
454
+ ]
799
455
  }
456
+ };
457
+
458
+ // src/lib/version.ts
459
+ var version = package_default.version;
460
+ if (typeof version !== "string" || version.length === 0) {
461
+ throw new Error("Invalid CLI version in package.json");
800
462
  }
801
- async function resolveActiveTunnel() {
802
- const dir = tunnelInfoDir();
803
- const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".json") && !f.endsWith(".bridge.json"));
804
- const active = [];
805
- for (const f of files) {
806
- const tunnelId = f.replace(".json", "");
807
- if (isDaemonRunning(tunnelId)) active.push(tunnelId);
808
- }
809
- if (active.length === 0) {
810
- failCli("No active tunnels. Run `pubblue tunnel start` first.");
811
- }
812
- if (active.length === 1) return active[0];
813
- failCli(`Multiple active tunnels: ${active.join(", ")}. Specify one.`);
814
- }
815
- function waitForDaemonReady({
816
- child,
817
- infoPath,
818
- socketPath,
819
- timeoutMs
820
- }) {
821
- return new Promise((resolve3) => {
822
- let settled = false;
823
- let pollInFlight = false;
824
- let lastIpcError = null;
825
- const done = (result) => {
826
- if (settled) return;
827
- settled = true;
828
- clearInterval(poll);
829
- clearTimeout(timeout);
830
- child.off("exit", onExit);
831
- resolve3(result);
832
- };
833
- const onExit = (code, signal) => {
834
- const suffix = signal ? ` (signal ${signal})` : "";
835
- done({ ok: false, reason: `daemon exited with code ${code ?? 0}${suffix}` });
836
- };
837
- child.on("exit", onExit);
838
- const poll = setInterval(() => {
839
- if (pollInFlight || !fs3.existsSync(infoPath)) return;
840
- pollInFlight = true;
841
- void ipcCall(socketPath, { method: "status", params: {} }).then((status) => {
842
- if (status.ok) done({ ok: true });
843
- }).catch((error) => {
844
- lastIpcError = error instanceof Error ? error.message : String(error);
845
- }).finally(() => {
846
- pollInFlight = false;
847
- });
848
- }, 120);
849
- const timeout = setTimeout(() => {
850
- 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`;
851
- done({ ok: false, reason });
852
- }, timeoutMs);
853
- });
854
- }
855
- async function waitForAgentOffer(params) {
856
- const startedAt = Date.now();
857
- let lastError = null;
858
- while (Date.now() - startedAt < params.timeoutMs) {
859
- try {
860
- const tunnel = await params.apiClient.get(params.tunnelId);
861
- if (typeof tunnel.agentOffer === "string" && tunnel.agentOffer.length > 0) {
862
- return { ok: true };
463
+ var CLI_VERSION = version;
464
+
465
+ // src/commands/live.ts
466
+ function registerLiveCommands(program2) {
467
+ registerOpenCommand(program2);
468
+ registerCloseCommand(program2);
469
+ registerStatusCommand(program2);
470
+ registerWriteCommand(program2);
471
+ registerReadCommand(program2);
472
+ registerChannelsCommand(program2);
473
+ registerDoctorCommand(program2);
474
+ }
475
+ function registerOpenCommand(program2) {
476
+ program2.command("open").description("Go live on a pub (starts WebRTC daemon)").argument("[slug]", "Pub slug (reuses existing live when possible)").option("--expires <duration>", "Auto-close after duration (e.g. 4h, 1d)", "24h").option("--new", "Always create a new live (skip reuse)").option("--bridge <mode>", "Bridge mode: openclaw|none").option("--foreground", "Run in foreground (don't fork, no managed bridge)").action(
477
+ async (slugArg, opts) => {
478
+ await ensureNodeDatachannelAvailable();
479
+ writeLatestCliVersion(CLI_VERSION);
480
+ const runtimeConfig = getConfig();
481
+ const apiClient = createApiClient(runtimeConfig);
482
+ let target = null;
483
+ const bridgeMode = parseBridgeMode(opts.bridge || runtimeConfig.bridge?.mode || "openclaw");
484
+ const bridgeProcessEnv = buildBridgeProcessEnv(runtimeConfig.bridge);
485
+ if (slugArg && !opts.new) {
486
+ try {
487
+ const pub = await apiClient.get(slugArg);
488
+ if (pub.live?.status === "active" && pub.live.expiresAt > Date.now()) {
489
+ target = {
490
+ createdNew: false,
491
+ expiresAt: pub.live.expiresAt,
492
+ mode: "existing",
493
+ slug: pub.slug,
494
+ url: getPublicUrl(pub.slug)
495
+ };
496
+ console.error(`Reusing existing active live for ${pub.slug}.`);
497
+ }
498
+ } catch (error) {
499
+ if (!(error instanceof PubApiError && error.status === 404)) {
500
+ failCli(`Failed to inspect pub ${slugArg}: ${formatApiError(error)}`);
501
+ }
502
+ }
503
+ } else if (!slugArg && !opts.new) {
504
+ try {
505
+ const pubs = await apiClient.list();
506
+ const reusable = pickReusableLive(pubs);
507
+ if (reusable) {
508
+ if (!reusable.live) {
509
+ failCli("Internal error: reusable live is missing from selected pub.");
510
+ }
511
+ target = {
512
+ createdNew: false,
513
+ expiresAt: reusable.live.expiresAt,
514
+ mode: "existing",
515
+ slug: reusable.slug,
516
+ url: getPublicUrl(reusable.slug)
517
+ };
518
+ const activeLives = pubs.filter(
519
+ (p) => p.live?.status === "active" && p.live.expiresAt > Date.now()
520
+ );
521
+ if (activeLives.length > 1) {
522
+ console.error(
523
+ [
524
+ `Multiple active lives found: ${activeLives.map((p) => p.slug).join(", ")}`,
525
+ `Reusing most recent: ${reusable.slug}.`,
526
+ "Use `pubblue open <slug>` to choose explicitly or --new to force creation."
527
+ ].join("\n")
528
+ );
529
+ } else {
530
+ console.error(
531
+ `Reusing existing live for ${reusable.slug}. Use --new to force creation.`
532
+ );
533
+ }
534
+ }
535
+ } catch (error) {
536
+ failCli(`Failed to list pubs for live reuse check: ${formatApiError(error)}`);
537
+ }
863
538
  }
864
- } catch (error) {
865
- lastError = formatApiError(error);
866
- }
867
- await new Promise((resolve3) => setTimeout(resolve3, 150));
868
- }
869
- return {
870
- ok: false,
871
- reason: lastError ? `agent offer was not published in time (last API error: ${lastError})` : "agent offer was not published in time"
872
- };
873
- }
874
- async function ensureBridgeReady(params) {
875
- if (params.bridgeMode === "none") {
876
- return { ok: true };
877
- }
878
- const infoPath = bridgeInfoPath(params.tunnelId);
879
- if (isBridgeRunning(params.tunnelId)) {
880
- return waitForBridgeReady({
881
- infoPath,
882
- tunnelId: params.tunnelId,
883
- timeoutMs: params.timeoutMs
884
- });
885
- }
886
- const bridgeScript = path3.join(import.meta.dirname, "tunnel-bridge-entry.js");
887
- const logPath = bridgeLogPath(params.tunnelId);
888
- const logFd = fs3.openSync(logPath, "a");
889
- const child = fork(bridgeScript, [], {
890
- detached: true,
891
- stdio: buildBridgeForkStdio(logFd),
892
- env: {
893
- ...params.bridgeProcessEnv,
894
- PUBBLUE_BRIDGE_MODE: params.bridgeMode,
895
- PUBBLUE_BRIDGE_TUNNEL_ID: params.tunnelId,
896
- PUBBLUE_BRIDGE_SOCKET: params.socketPath,
897
- PUBBLUE_BRIDGE_INFO: infoPath
898
- }
899
- });
900
- fs3.closeSync(logFd);
901
- if (child.connected) {
902
- child.disconnect();
903
- }
904
- child.unref();
905
- return waitForBridgeReady({
906
- child,
907
- infoPath,
908
- tunnelId: params.tunnelId,
909
- timeoutMs: params.timeoutMs
910
- });
911
- }
912
- function waitForBridgeReady({
913
- child,
914
- infoPath,
915
- tunnelId,
916
- timeoutMs
917
- }) {
918
- return new Promise((resolve3) => {
919
- let settled = false;
920
- let lastState;
921
- let lastError;
922
- const done = (result) => {
923
- if (settled) return;
924
- settled = true;
925
- clearInterval(poll);
926
- clearTimeout(timeout);
927
- if (child) {
928
- child.off("exit", onExit);
539
+ if (!target) {
540
+ try {
541
+ let created;
542
+ if (slugArg) {
543
+ created = await apiClient.openLive(slugArg, {
544
+ expiresIn: opts.expires
545
+ });
546
+ } else {
547
+ const newPub = await apiClient.create({});
548
+ try {
549
+ created = await apiClient.openLive(newPub.slug, {
550
+ expiresIn: opts.expires
551
+ });
552
+ } catch (error) {
553
+ try {
554
+ await apiClient.remove(newPub.slug);
555
+ } catch (cleanupError) {
556
+ console.error(
557
+ `Warning: failed to remove pub ${newPub.slug} after open failure: ${formatApiError(cleanupError)}`
558
+ );
559
+ }
560
+ throw error;
561
+ }
562
+ }
563
+ target = {
564
+ createdNew: true,
565
+ expiresAt: created.expiresAt,
566
+ mode: "created",
567
+ slug: created.slug,
568
+ url: created.url
569
+ };
570
+ } catch (error) {
571
+ failCli(`Failed to go live: ${formatApiError(error)}`);
572
+ }
929
573
  }
930
- resolve3(result);
931
- };
932
- const onExit = (code, signal) => {
933
- const suffix = signal ? ` (signal ${signal})` : "";
934
- done({ ok: false, reason: `bridge exited with code ${code ?? 0}${suffix}` });
935
- };
936
- if (child) {
937
- child.on("exit", onExit);
938
- }
939
- const poll = setInterval(() => {
940
- if (!fs3.existsSync(infoPath)) return;
941
- const info = readBridgeProcessInfo(tunnelId);
942
- if (!info) return;
943
- lastState = info.status;
944
- lastError = info.lastError;
945
- if (info.status === "ready" && isBridgeRunning(tunnelId)) {
946
- done({ ok: true });
574
+ if (!target) {
575
+ failCli("Failed to resolve live target.");
576
+ }
577
+ const socketPath = getSocketPath(target.slug);
578
+ const infoPath = liveInfoPath(target.slug);
579
+ const logPath = liveLogPath(target.slug);
580
+ try {
581
+ await stopOtherDaemons(target.slug);
582
+ } catch (error) {
583
+ failCli(error instanceof Error ? error.message : String(error));
584
+ }
585
+ if (opts.foreground) {
586
+ if (bridgeMode !== "none") {
587
+ throw new Error(
588
+ "Foreground mode disables managed bridge process. Use background mode for --bridge openclaw."
589
+ );
590
+ }
591
+ const { startDaemon } = await import("./tunnel-daemon-QN6TVUX6.js");
592
+ console.log(`Live started: ${target.url}`);
593
+ const fgTma = getTelegramMiniAppUrl(target.slug);
594
+ if (fgTma) console.log(`Telegram: ${fgTma}`);
595
+ console.log(`Slug: ${target.slug}`);
596
+ console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
597
+ if (target.mode === "existing") console.log("Mode: attached existing live");
598
+ console.log("Running in foreground. Press Ctrl+C to stop.");
599
+ try {
600
+ await startDaemon({
601
+ cliVersion: CLI_VERSION,
602
+ slug: target.slug,
603
+ apiClient,
604
+ socketPath,
605
+ infoPath
606
+ });
607
+ } catch (error) {
608
+ const message = error instanceof Error ? error.message : String(error);
609
+ failCli(`Daemon failed: ${message}`);
610
+ }
947
611
  return;
948
612
  }
949
- if (info.status === "error") {
950
- done({
951
- ok: false,
952
- reason: info.lastError ? `bridge reported startup error: ${info.lastError}` : "bridge reported startup error"
613
+ const runningDaemonInfo = readDaemonProcessInfo(target.slug);
614
+ if (runningDaemonInfo) {
615
+ const daemonVersion = runningDaemonInfo.cliVersion;
616
+ const shouldRestartForUpgrade = shouldRestartDaemonForCliUpgrade(
617
+ daemonVersion,
618
+ CLI_VERSION
619
+ );
620
+ if (shouldRestartForUpgrade) {
621
+ console.error(
622
+ `Restarting daemon for CLI version ${CLI_VERSION} (running: ${daemonVersion || "unknown"}).`
623
+ );
624
+ const bridgeError = await stopBridge(target.slug);
625
+ if (bridgeError) failCli(bridgeError);
626
+ try {
627
+ await ipcCall(socketPath, { method: "close", params: {} });
628
+ } catch (error) {
629
+ failCli(
630
+ [
631
+ `Failed to stop running daemon for upgrade: ${error instanceof Error ? error.message : String(error)}`,
632
+ "Run `pubblue close <slug>` and retry."
633
+ ].join("\n")
634
+ );
635
+ }
636
+ const daemonStopped = await waitForProcessExit(runningDaemonInfo.pid, 6e3);
637
+ if (!daemonStopped) {
638
+ failCli("Daemon did not stop in time during upgrade restart.");
639
+ }
640
+ } else {
641
+ try {
642
+ const status = await ipcCall(socketPath, { method: "status", params: {} });
643
+ if (!status.ok) throw new Error(String(status.error || "status check failed"));
644
+ } catch (error) {
645
+ failCli(
646
+ [
647
+ `Daemon process exists but is not responding: ${error instanceof Error ? error.message : String(error)}`,
648
+ "Run `pubblue close <slug>` and start again."
649
+ ].join("\n")
650
+ );
651
+ }
652
+ if (bridgeMode !== "none") {
653
+ const bridgeReady = await ensureBridgeReady({
654
+ bridgeMode,
655
+ slug: target.slug,
656
+ socketPath,
657
+ bridgeProcessEnv,
658
+ timeoutMs: 8e3
659
+ });
660
+ if (!bridgeReady.ok) {
661
+ const lines = [
662
+ `Bridge failed to start for running live: ${bridgeReady.reason ?? "unknown reason"}`
663
+ ];
664
+ const existingBridgeLog = bridgeLogPath(target.slug);
665
+ if (fs2.existsSync(existingBridgeLog)) {
666
+ lines.push(`Bridge log: ${existingBridgeLog}`);
667
+ const bridgeTail = readLogTail(existingBridgeLog);
668
+ if (bridgeTail) {
669
+ lines.push("---- bridge log tail ----");
670
+ lines.push(bridgeTail.trimEnd());
671
+ lines.push("---- end bridge log tail ----");
672
+ }
673
+ }
674
+ failCli(lines.join("\n"));
675
+ }
676
+ }
677
+ console.log(`Live started: ${target.url}`);
678
+ const runTma = getTelegramMiniAppUrl(target.slug);
679
+ if (runTma) console.log(`Telegram: ${runTma}`);
680
+ console.log(`Slug: ${target.slug}`);
681
+ console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
682
+ console.log("Daemon already running for this live.");
683
+ console.log(`Daemon log: ${logPath}`);
684
+ if (bridgeMode !== "none") {
685
+ console.log("Bridge mode: openclaw");
686
+ console.log(`Bridge log: ${bridgeLogPath(target.slug)}`);
687
+ }
688
+ return;
689
+ }
690
+ }
691
+ const { fork } = await import("child_process");
692
+ const daemonScript = path2.join(import.meta.dirname, "tunnel-daemon-entry.js");
693
+ const bridgeScript = path2.join(import.meta.dirname, "tunnel-bridge-entry.js");
694
+ const daemonLogFd = fs2.openSync(logPath, "a");
695
+ const child = fork(daemonScript, [], {
696
+ detached: true,
697
+ stdio: buildDaemonForkStdio(daemonLogFd),
698
+ env: {
699
+ ...bridgeProcessEnv,
700
+ PUBBLUE_DAEMON_SLUG: target.slug,
701
+ PUBBLUE_DAEMON_BASE_URL: runtimeConfig.baseUrl,
702
+ PUBBLUE_DAEMON_API_KEY: runtimeConfig.apiKey,
703
+ PUBBLUE_DAEMON_SOCKET: socketPath,
704
+ PUBBLUE_DAEMON_INFO: infoPath,
705
+ PUBBLUE_CLI_VERSION: CLI_VERSION,
706
+ PUBBLUE_DAEMON_BRIDGE_MODE: bridgeMode,
707
+ PUBBLUE_DAEMON_BRIDGE_SCRIPT: bridgeScript,
708
+ PUBBLUE_DAEMON_BRIDGE_INFO: bridgeInfoPath(target.slug),
709
+ PUBBLUE_DAEMON_BRIDGE_LOG: bridgeLogPath(target.slug)
710
+ }
711
+ });
712
+ fs2.closeSync(daemonLogFd);
713
+ if (child.connected) {
714
+ child.disconnect();
715
+ }
716
+ child.unref();
717
+ console.log(`Starting daemon for ${target.slug}...`);
718
+ const ready = await waitForDaemonReady({
719
+ child,
720
+ infoPath,
721
+ socketPath,
722
+ timeoutMs: 8e3
723
+ });
724
+ if (!ready.ok) {
725
+ const lines = [
726
+ `Daemon failed to start: ${ready.reason ?? "unknown reason"}`,
727
+ `Daemon log: ${logPath}`
728
+ ];
729
+ const tail = readLogTail(logPath);
730
+ if (tail) {
731
+ lines.push("---- daemon log tail ----");
732
+ lines.push(tail.trimEnd());
733
+ lines.push("---- end daemon log tail ----");
734
+ }
735
+ await cleanupLiveOnStartFailure(apiClient, target);
736
+ failCli(lines.join("\n"));
737
+ }
738
+ const offerReady = await waitForAgentOffer({
739
+ apiClient,
740
+ slug: target.slug,
741
+ timeoutMs: 5e3
742
+ });
743
+ if (!offerReady.ok) {
744
+ const lines = [
745
+ `Daemon started but signaling is not ready: ${offerReady.reason}`,
746
+ `Daemon log: ${logPath}`
747
+ ];
748
+ const tail = readLogTail(logPath);
749
+ if (tail) {
750
+ lines.push("---- daemon log tail ----");
751
+ lines.push(tail.trimEnd());
752
+ lines.push("---- end daemon log tail ----");
753
+ }
754
+ await cleanupLiveOnStartFailure(apiClient, target);
755
+ failCli(lines.join("\n"));
756
+ }
757
+ if (bridgeMode !== "none") {
758
+ const bridgeReady = await ensureBridgeReady({
759
+ bridgeMode,
760
+ slug: target.slug,
761
+ socketPath,
762
+ bridgeProcessEnv,
763
+ timeoutMs: 8e3
953
764
  });
765
+ if (!bridgeReady.ok) {
766
+ const lines = [`Bridge failed to start: ${bridgeReady.reason ?? "unknown reason"}`];
767
+ const bridgeLog = bridgeLogPath(target.slug);
768
+ if (fs2.existsSync(bridgeLog)) {
769
+ lines.push(`Bridge log: ${bridgeLog}`);
770
+ const bridgeTail = readLogTail(bridgeLog);
771
+ if (bridgeTail) {
772
+ lines.push("---- bridge log tail ----");
773
+ lines.push(bridgeTail.trimEnd());
774
+ lines.push("---- end bridge log tail ----");
775
+ }
776
+ }
777
+ let daemonCloseWarning = null;
778
+ try {
779
+ await ipcCall(socketPath, { method: "close", params: {} });
780
+ } catch (error) {
781
+ daemonCloseWarning = `failed to stop daemon after bridge startup failure: ${error instanceof Error ? error.message : String(error)}`;
782
+ }
783
+ if (daemonCloseWarning) {
784
+ lines.push(`Warning: ${daemonCloseWarning}`);
785
+ }
786
+ await cleanupLiveOnStartFailure(apiClient, target);
787
+ failCli(lines.join("\n"));
788
+ }
954
789
  }
955
- }, 120);
956
- const timeout = setTimeout(() => {
957
- const reason = lastError && lastError.length > 0 ? `timed out after ${timeoutMs}ms waiting for bridge readiness (last error: ${lastError})` : `timed out after ${timeoutMs}ms waiting for bridge readiness (state: ${lastState || "unknown"})`;
958
- done({ ok: false, reason });
959
- }, timeoutMs);
960
- });
790
+ console.log(`Live started: ${target.url}`);
791
+ const tma = getTelegramMiniAppUrl(target.slug);
792
+ if (tma) console.log(`Telegram: ${tma}`);
793
+ console.log(`Slug: ${target.slug}`);
794
+ console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
795
+ if (target.mode === "existing") console.log("Mode: attached existing live");
796
+ console.log("Daemon health: OK");
797
+ console.log(`Daemon log: ${logPath}`);
798
+ if (bridgeMode !== "none") {
799
+ console.log("Bridge mode: openclaw");
800
+ console.log(`Bridge log: ${bridgeLogPath(target.slug)}`);
801
+ }
802
+ }
803
+ );
961
804
  }
962
-
963
- // src/commands/tunnel/management-commands.ts
964
- function registerTunnelManagementCommands(tunnel) {
965
- tunnel.command("channels").description("List active channels").argument("[tunnelId]", "Tunnel ID").option("-t, --tunnel <tunnelId>", "Tunnel ID (alternative to positional arg)").action(async (tunnelIdArg, opts) => {
966
- const tunnelId = resolveTunnelIdSelection(tunnelIdArg, opts.tunnel) || await resolveActiveTunnel();
967
- const socketPath = getSocketPath(tunnelId);
968
- const response = await ipcCall(socketPath, { method: "channels", params: {} });
969
- if (response.channels) {
970
- for (const ch of response.channels) {
971
- console.log(` ${ch.name} [${ch.direction}]`);
805
+ function registerCloseCommand(program2) {
806
+ program2.command("close").description("Close a live and stop its daemon").argument("<slug>", "Pub slug").action(async (slug) => {
807
+ const bridgeError = await stopBridge(slug);
808
+ if (bridgeError) console.error(bridgeError);
809
+ fs2.rmSync(bridgeInfoPath(slug), { force: true });
810
+ const socketPath = getSocketPath(slug);
811
+ if (isDaemonRunning(slug)) {
812
+ try {
813
+ await ipcCall(socketPath, { method: "close", params: {} });
814
+ } catch (error) {
815
+ console.error(
816
+ `Warning: failed to stop daemon over IPC for ${slug}: ${error instanceof Error ? error.message : String(error)}`
817
+ );
818
+ }
819
+ }
820
+ const apiClient = createApiClient();
821
+ try {
822
+ await apiClient.closeLive(slug);
823
+ } catch (error) {
824
+ const message = formatApiError(error);
825
+ if (!/Live not found/i.test(message)) {
826
+ failCli(`Failed to close live for ${slug}: ${message}`);
972
827
  }
973
828
  }
829
+ console.log(`Closed: ${slug}`);
974
830
  });
975
- tunnel.command("status").description("Check tunnel connection status").argument("[tunnelId]", "Tunnel ID").option("-t, --tunnel <tunnelId>", "Tunnel ID (alternative to positional arg)").action(async (tunnelIdArg, opts) => {
976
- const tunnelId = resolveTunnelIdSelection(tunnelIdArg, opts.tunnel) || await resolveActiveTunnel();
977
- const socketPath = getSocketPath(tunnelId);
831
+ }
832
+ function registerStatusCommand(program2) {
833
+ program2.command("status").description("Check live connection status").argument("[slug]", "Pub slug").option("-s, --slug <slug>", "Pub slug (alternative to positional arg)").action(async (slugArg, opts) => {
834
+ const slug = resolveSlugSelection(slugArg, opts.slug) || await resolveActiveSlug();
835
+ const socketPath = getSocketPath(slug);
978
836
  const response = await ipcCall(socketPath, { method: "status", params: {} });
979
837
  console.log(` Status: ${response.connected ? "connected" : "waiting"}`);
980
838
  console.log(` Uptime: ${response.uptime}s`);
@@ -984,13 +842,13 @@ function registerTunnelManagementCommands(tunnel) {
984
842
  if (typeof response.lastError === "string" && response.lastError.length > 0) {
985
843
  console.log(` Last error: ${response.lastError}`);
986
844
  }
987
- const logPath = tunnelLogPath(tunnelId);
988
- if (fs4.existsSync(logPath)) {
845
+ const logPath = liveLogPath(slug);
846
+ if (fs2.existsSync(logPath)) {
989
847
  console.log(` Log: ${logPath}`);
990
848
  }
991
- const bridgeInfo = readBridgeProcessInfo(tunnelId);
849
+ const bridgeInfo = readBridgeProcessInfo(slug);
992
850
  if (bridgeInfo) {
993
- const bridgeRunning = isBridgeRunning(tunnelId);
851
+ const bridgeRunning = isBridgeRunning(slug);
994
852
  const bridgeState = bridgeInfo.status || (bridgeRunning ? "running" : "stopped");
995
853
  console.log(` Bridge: ${bridgeInfo.mode} (${bridgeState})`);
996
854
  if (bridgeInfo.sessionId) {
@@ -1006,182 +864,22 @@ function registerTunnelManagementCommands(tunnel) {
1006
864
  console.log(` Bridge last error: ${bridgeInfo.lastError}`);
1007
865
  }
1008
866
  }
1009
- const bridgeLog = bridgeLogPath(tunnelId);
1010
- if (fs4.existsSync(bridgeLog)) {
867
+ const bridgeLog = bridgeLogPath(slug);
868
+ if (fs2.existsSync(bridgeLog)) {
1011
869
  console.log(` Bridge log: ${bridgeLog}`);
1012
870
  }
1013
871
  });
1014
- tunnel.command("doctor").description("Run strict end-to-end tunnel checks (daemon, channels, chat/canvas ping)").option("-t, --tunnel <tunnelId>", "Tunnel ID (auto-detected if one active)").option("--timeout <seconds>", "Timeout for pong wait and repeated reads", "30").option("--wait-pong", "Wait for user to reply with exact text 'pong' on chat channel").option("--skip-chat", "Skip chat ping check").option("--skip-canvas", "Skip canvas ping check").action(
1015
- async (opts) => {
1016
- const timeoutSeconds = parsePositiveIntegerOption(opts.timeout, "--timeout");
1017
- const timeoutMs = timeoutSeconds * 1e3;
1018
- const tunnelId = opts.tunnel || await resolveActiveTunnel();
1019
- const socketPath = getSocketPath(tunnelId);
1020
- const apiClient = createApiClient();
1021
- const fail = (message) => failCli(`Doctor failed: ${message}`);
1022
- console.log(`Doctor tunnel: ${tunnelId}`);
1023
- let statusResponse = null;
1024
- try {
1025
- statusResponse = await ipcCall(socketPath, {
1026
- method: "status",
1027
- params: {}
1028
- });
1029
- } catch (error) {
1030
- fail(
1031
- `daemon is unreachable (${error instanceof Error ? error.message : String(error)}).`
1032
- );
1033
- }
1034
- if (!statusResponse) {
1035
- fail("daemon status returned no response.");
1036
- }
1037
- const daemonStatus = statusResponse;
1038
- if (!daemonStatus.ok) {
1039
- fail(`daemon returned non-ok status: ${String(daemonStatus.error || "unknown error")}`);
1040
- }
1041
- if (!daemonStatus.connected) {
1042
- fail("daemon is running but browser is not connected.");
1043
- }
1044
- const channelNames = Array.isArray(daemonStatus.channels) ? daemonStatus.channels.map((entry) => String(entry)) : [];
1045
- for (const required of [CONTROL_CHANNEL, CHANNELS.CHAT, CHANNELS.CANVAS]) {
1046
- if (!channelNames.includes(required)) {
1047
- fail(`required channel is missing: ${required}`);
1048
- }
1049
- }
1050
- console.log("Daemon/channel check: OK");
1051
- let apiTunnel;
1052
- try {
1053
- apiTunnel = await apiClient.get(tunnelId);
1054
- } catch (error) {
1055
- fail(`failed to fetch tunnel info from API: ${formatApiError(error)}`);
1056
- }
1057
- if (apiTunnel.status !== "active") {
1058
- fail(`API reports tunnel is not active (status: ${apiTunnel.status})`);
1059
- }
1060
- if (apiTunnel.expiresAt <= Date.now()) {
1061
- fail("API reports tunnel is expired.");
1062
- }
1063
- if (!apiTunnel.hasConnection) {
1064
- fail("API reports no browser connection.");
1065
- }
1066
- if (typeof apiTunnel.agentOffer !== "string" || apiTunnel.agentOffer.length === 0) {
1067
- fail("agent offer was not published.");
1068
- }
1069
- console.log("API/signaling check: OK");
1070
- if (!opts.skipChat) {
1071
- const pingText = "This is a ping test. Reply with 'pong'.";
1072
- const pingMsg = {
1073
- id: generateMessageId(),
1074
- type: "text",
1075
- data: pingText
1076
- };
1077
- const writeResponse = await ipcCall(socketPath, {
1078
- method: "write",
1079
- params: { channel: CHANNELS.CHAT, msg: pingMsg }
1080
- });
1081
- if (!writeResponse.ok) {
1082
- fail(`chat ping failed: ${String(writeResponse.error || "unknown write error")}`);
1083
- }
1084
- console.log("Chat ping write ACK: OK");
1085
- if (opts.waitPong) {
1086
- const startedAt = Date.now();
1087
- let receivedPong = false;
1088
- while (Date.now() - startedAt < timeoutMs) {
1089
- const readResponse = await ipcCall(socketPath, {
1090
- method: "read",
1091
- params: { channel: CHANNELS.CHAT }
1092
- });
1093
- if (!readResponse.ok) {
1094
- fail(
1095
- `chat read failed while waiting for pong: ${String(readResponse.error || "unknown read error")}`
1096
- );
1097
- }
1098
- const messages = Array.isArray(readResponse.messages) ? readResponse.messages : [];
1099
- if (messages.some((entry) => messageContainsPong(entry))) {
1100
- receivedPong = true;
1101
- break;
1102
- }
1103
- await new Promise((resolve3) => setTimeout(resolve3, 1e3));
1104
- }
1105
- if (!receivedPong) {
1106
- fail(
1107
- `timed out after ${timeoutSeconds}s waiting for exact 'pong' reply on chat channel.`
1108
- );
1109
- }
1110
- console.log("Chat pong roundtrip: OK");
1111
- }
1112
- }
1113
- if (!opts.skipCanvas) {
1114
- const stamp = (/* @__PURE__ */ new Date()).toISOString();
1115
- const canvasMsg = {
1116
- id: generateMessageId(),
1117
- type: "html",
1118
- data: `<!doctype html><html><body style="margin:0;padding:24px;font-family:system-ui;background:#111;color:#f5f5f5">Canvas ping OK<br><small>${stamp}</small></body></html>`
1119
- };
1120
- const canvasResponse = await ipcCall(socketPath, {
1121
- method: "write",
1122
- params: { channel: CHANNELS.CANVAS, msg: canvasMsg }
1123
- });
1124
- if (!canvasResponse.ok) {
1125
- fail(`canvas ping failed: ${String(canvasResponse.error || "unknown write error")}`);
1126
- }
1127
- console.log("Canvas ping write ACK: OK");
1128
- }
1129
- console.log("Tunnel doctor: PASS");
1130
- }
1131
- );
1132
- tunnel.command("list").description("List active tunnels").action(async () => {
1133
- const apiClient = createApiClient();
1134
- const tunnels = await apiClient.list();
1135
- if (tunnels.length === 0) {
1136
- console.log("No active tunnels.");
1137
- return;
1138
- }
1139
- for (const t of tunnels) {
1140
- const age = Math.floor((Date.now() - t.createdAt) / 6e4);
1141
- const running = isDaemonRunning(t.tunnelId) ? "running" : "no daemon";
1142
- const bridgeInfo = readBridgeProcessInfo(t.tunnelId);
1143
- const bridge = bridgeInfo ? isBridgeRunning(t.tunnelId) ? `${bridgeInfo.mode}:running` : `${bridgeInfo.mode}:stopped` : "none";
1144
- const conn = t.hasConnection ? "connected" : "waiting";
1145
- console.log(` ${t.tunnelId} ${conn} ${running} bridge=${bridge} ${age}m ago`);
1146
- }
1147
- });
1148
- tunnel.command("close").description("Close a tunnel and stop its daemon").argument("<tunnelId>", "Tunnel ID").action(async (tunnelId) => {
1149
- stopBridgeProcess(tunnelId);
1150
- try {
1151
- fs4.unlinkSync(bridgeInfoPath(tunnelId));
1152
- } catch {
1153
- }
1154
- const socketPath = getSocketPath(tunnelId);
1155
- try {
1156
- await ipcCall(socketPath, { method: "close", params: {} });
1157
- } catch {
1158
- }
1159
- const apiClient = createApiClient();
1160
- try {
1161
- await apiClient.close(tunnelId);
1162
- } catch (error) {
1163
- const message = formatApiError(error);
1164
- if (!/Tunnel not found/i.test(message)) {
1165
- failCli(`Failed to close tunnel ${tunnelId}: ${message}`);
1166
- }
1167
- }
1168
- console.log(`Closed: ${tunnelId}`);
1169
- });
1170
872
  }
1171
-
1172
- // src/commands/tunnel/message-commands.ts
1173
- import * as fs5 from "fs";
1174
- import * as path4 from "path";
1175
- function registerTunnelMessageCommands(tunnel) {
1176
- tunnel.command("write").description("Write data to a channel").argument("[message]", "Text message (or use --file)").option("-t, --tunnel <tunnelId>", "Tunnel ID (auto-detected if one active)").option("-c, --channel <channel>", "Channel name", "chat").option("-f, --file <file>", "Read content from file").action(
873
+ function registerWriteCommand(program2) {
874
+ program2.command("write").description("Write data to a live channel").argument("[message]", "Text message (or use --file)").option("-s, --slug <slug>", "Pub slug (auto-detected if one active)").option("-c, --channel <channel>", "Channel name", "chat").option("-f, --file <file>", "Read content from file").action(
1177
875
  async (messageArg, opts) => {
1178
876
  let msg;
1179
877
  let binaryBase64;
1180
878
  if (opts.file) {
1181
- const filePath = path4.resolve(opts.file);
1182
- const ext = path4.extname(filePath).toLowerCase();
1183
- const bytes = fs5.readFileSync(filePath);
1184
- const filename = path4.basename(filePath);
879
+ const filePath = path2.resolve(opts.file);
880
+ const ext = path2.extname(filePath).toLowerCase();
881
+ const bytes = fs2.readFileSync(filePath);
882
+ const filename = path2.basename(filePath);
1185
883
  if (ext === ".html" || ext === ".htm") {
1186
884
  msg = {
1187
885
  id: generateMessageId(),
@@ -1219,8 +917,8 @@ function registerTunnelMessageCommands(tunnel) {
1219
917
  data: Buffer.concat(chunks).toString("utf-8").trim()
1220
918
  };
1221
919
  }
1222
- const tunnelId = opts.tunnel || await resolveActiveTunnel();
1223
- const socketPath = getSocketPath(tunnelId);
920
+ const slug = opts.slug || await resolveActiveSlug();
921
+ const socketPath = getSocketPath(slug);
1224
922
  const response = await ipcCall(socketPath, {
1225
923
  method: "write",
1226
924
  params: { channel: opts.channel, msg, binaryBase64 }
@@ -1230,10 +928,12 @@ function registerTunnelMessageCommands(tunnel) {
1230
928
  }
1231
929
  }
1232
930
  );
1233
- tunnel.command("read").description("Read buffered messages from channels").argument("[tunnelId]", "Tunnel ID (auto-detected if one active)").option("-t, --tunnel <tunnelId>", "Tunnel ID (alternative to positional arg)").option("-c, --channel <channel>", "Filter by channel").option("--follow", "Stream messages continuously").option("--all", "With --follow, include all channels instead of chat-only default").action(
1234
- async (tunnelIdArg, opts) => {
1235
- const tunnelId = resolveTunnelIdSelection(tunnelIdArg, opts.tunnel) || await resolveActiveTunnel();
1236
- const socketPath = getSocketPath(tunnelId);
931
+ }
932
+ function registerReadCommand(program2) {
933
+ program2.command("read").description("Read buffered messages from live channels").argument("[slug]", "Pub slug (auto-detected if one active)").option("-s, --slug <slug>", "Pub slug (alternative to positional arg)").option("-c, --channel <channel>", "Filter by channel").option("--follow", "Stream messages continuously").option("--all", "With --follow, include all channels instead of chat-only default").action(
934
+ async (slugArg, opts) => {
935
+ const slug = resolveSlugSelection(slugArg, opts.slug) || await resolveActiveSlug();
936
+ const socketPath = getSocketPath(slug);
1237
937
  const readChannel = opts.channel || (opts.follow && !opts.all ? CHANNELS.CHAT : void 0);
1238
938
  if (opts.follow) {
1239
939
  if (!opts.channel && !opts.all) {
@@ -1283,382 +983,258 @@ function registerTunnelMessageCommands(tunnel) {
1283
983
  }
1284
984
  );
1285
985
  }
1286
-
1287
- // src/commands/tunnel/start-command.ts
1288
- import { fork as fork2 } from "child_process";
1289
- import * as fs6 from "fs";
1290
- import * as path5 from "path";
1291
-
1292
- // package.json
1293
- var package_default = {
1294
- name: "pubblue",
1295
- version: "0.4.11",
1296
- description: "CLI tool for publishing static content via pub.blue",
1297
- type: "module",
1298
- bin: {
1299
- pubblue: "./dist/index.js"
1300
- },
1301
- scripts: {
1302
- build: "tsup src/index.ts src/tunnel-daemon-entry.ts src/tunnel-bridge-entry.ts --format esm --dts --clean",
1303
- dev: "tsup src/index.ts src/tunnel-daemon-entry.ts src/tunnel-bridge-entry.ts --format esm --watch",
1304
- test: "vitest run",
1305
- "test:watch": "vitest",
1306
- lint: "tsc --noEmit"
1307
- },
1308
- dependencies: {
1309
- commander: "^13.0.0",
1310
- "node-datachannel": "^0.32.0"
1311
- },
1312
- devDependencies: {
1313
- "@types/node": "22.10.2",
1314
- tsup: "^8.3.6",
1315
- typescript: "^5.7.2",
1316
- vitest: "^3.0.0"
1317
- },
1318
- files: [
1319
- "dist"
1320
- ],
1321
- repository: {
1322
- type: "git",
1323
- url: "git+https://github.com/xmanatee/pub.git",
1324
- directory: "cli"
1325
- },
1326
- publishConfig: {
1327
- access: "public"
1328
- },
1329
- pnpm: {
1330
- onlyBuiltDependencies: [
1331
- "esbuild",
1332
- "node-datachannel"
1333
- ]
1334
- }
1335
- };
1336
-
1337
- // src/lib/version.ts
1338
- var version = package_default.version;
1339
- if (typeof version !== "string" || version.length === 0) {
1340
- throw new Error("Invalid CLI version in package.json");
1341
- }
1342
- var CLI_VERSION = version;
1343
-
1344
- // src/commands/tunnel/start-command.ts
1345
- async function waitForStopped(isRunning, timeoutMs, pollMs = 120) {
1346
- const started = Date.now();
1347
- while (Date.now() - started < timeoutMs) {
1348
- if (!isRunning()) return true;
1349
- await new Promise((resolve3) => setTimeout(resolve3, pollMs));
1350
- }
1351
- return !isRunning();
986
+ function registerChannelsCommand(program2) {
987
+ program2.command("channels").description("List active live channels").argument("[slug]", "Pub slug").option("-s, --slug <slug>", "Pub slug (alternative to positional arg)").action(async (slugArg, opts) => {
988
+ const slug = resolveSlugSelection(slugArg, opts.slug) || await resolveActiveSlug();
989
+ const socketPath = getSocketPath(slug);
990
+ const response = await ipcCall(socketPath, { method: "channels", params: {} });
991
+ if (response.channels) {
992
+ for (const ch of response.channels) {
993
+ console.log(` ${ch.name} [${ch.direction}]`);
994
+ }
995
+ }
996
+ });
1352
997
  }
1353
- function registerTunnelStartCommand(tunnel) {
1354
- 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("--bridge <mode>", "Bridge mode: openclaw|none").option("--foreground", "Run in foreground (don't fork, no managed bridge)").action(
998
+ function registerDoctorCommand(program2) {
999
+ program2.command("doctor").description("Run end-to-end live checks (daemon, channels, chat/canvas ping)").option("-s, --slug <slug>", "Pub slug (auto-detected if one active)").option("--timeout <seconds>", "Timeout for pong wait and repeated reads", "30").option("--wait-pong", "Wait for user to reply with exact text 'pong' on chat channel").option("--skip-chat", "Skip chat ping check").option("--skip-canvas", "Skip canvas ping check").action(
1355
1000
  async (opts) => {
1356
- await ensureNodeDatachannelAvailable();
1357
- const runtimeConfig = getConfig();
1358
- const apiClient = createApiClient(runtimeConfig);
1359
- let target = null;
1360
- const bridgeMode = parseBridgeMode(opts.bridge || runtimeConfig.bridge?.mode || "openclaw");
1361
- const bridgeProcessEnv = buildBridgeProcessEnv(runtimeConfig.bridge);
1362
- if (opts.tunnel) {
1363
- try {
1364
- const existing = await apiClient.get(opts.tunnel);
1365
- if (existing.status === "closed" || existing.expiresAt <= Date.now()) {
1366
- failCli(`Tunnel ${opts.tunnel} is closed or expired.`);
1367
- }
1368
- target = {
1369
- createdNew: false,
1370
- expiresAt: existing.expiresAt,
1371
- mode: "existing",
1372
- tunnelId: existing.tunnelId,
1373
- url: getPublicTunnelUrl(existing.tunnelId)
1374
- };
1375
- } catch (error) {
1376
- failCli(`Failed to use tunnel ${opts.tunnel}: ${formatApiError(error)}`);
1377
- }
1378
- } else if (!opts.new) {
1379
- try {
1380
- const listed = await apiClient.list();
1381
- const active = listed.filter((t) => t.status === "active" && t.expiresAt > Date.now()).sort((a, b) => b.createdAt - a.createdAt);
1382
- const reusable = pickReusableTunnel(listed);
1383
- if (reusable) {
1384
- target = {
1385
- createdNew: false,
1386
- expiresAt: reusable.expiresAt,
1387
- mode: "existing",
1388
- tunnelId: reusable.tunnelId,
1389
- url: getPublicTunnelUrl(reusable.tunnelId)
1390
- };
1391
- if (active.length > 1) {
1392
- console.error(
1393
- [
1394
- `Multiple active tunnels found: ${active.map((t) => t.tunnelId).join(", ")}`,
1395
- `Reusing most recent active tunnel ${reusable.tunnelId}.`,
1396
- "Use --tunnel <id> to choose explicitly or --new to force creation."
1397
- ].join("\n")
1398
- );
1399
- } else {
1400
- console.error(
1401
- `Reusing existing active tunnel ${reusable.tunnelId}. Use --new to force creation.`
1402
- );
1403
- }
1404
- }
1405
- } catch (error) {
1406
- failCli(`Failed to list tunnels for reuse check: ${formatApiError(error)}`);
1001
+ const timeoutSeconds = parsePositiveIntegerOption(opts.timeout, "--timeout");
1002
+ const timeoutMs = timeoutSeconds * 1e3;
1003
+ const slug = opts.slug || await resolveActiveSlug();
1004
+ const socketPath = getSocketPath(slug);
1005
+ const apiClient = createApiClient();
1006
+ const fail = (message) => failCli(`Doctor failed: ${message}`);
1007
+ console.log(`Doctor: ${slug}`);
1008
+ let statusResponse = null;
1009
+ try {
1010
+ statusResponse = await ipcCall(socketPath, {
1011
+ method: "status",
1012
+ params: {}
1013
+ });
1014
+ } catch (error) {
1015
+ fail(
1016
+ `daemon is unreachable (${error instanceof Error ? error.message : String(error)}).`
1017
+ );
1018
+ }
1019
+ if (!statusResponse) {
1020
+ fail("daemon status returned no response.");
1021
+ }
1022
+ const daemonStatus = statusResponse;
1023
+ if (!daemonStatus.ok) {
1024
+ fail(`daemon returned non-ok status: ${String(daemonStatus.error || "unknown error")}`);
1025
+ }
1026
+ if (!daemonStatus.connected) {
1027
+ fail("daemon is running but browser is not connected.");
1028
+ }
1029
+ const channelNames = Array.isArray(daemonStatus.channels) ? daemonStatus.channels.map((entry) => String(entry)) : [];
1030
+ for (const required of [CONTROL_CHANNEL, CHANNELS.CHAT, CHANNELS.CANVAS]) {
1031
+ if (!channelNames.includes(required)) {
1032
+ fail(`required channel is missing: ${required}`);
1407
1033
  }
1408
1034
  }
1409
- if (!target) {
1035
+ console.log("Daemon/channel check: OK");
1036
+ const live = await (async () => {
1410
1037
  try {
1411
- const created = await apiClient.create({
1412
- expiresIn: opts.expires
1413
- });
1414
- target = {
1415
- createdNew: true,
1416
- expiresAt: created.expiresAt,
1417
- mode: "created",
1418
- tunnelId: created.tunnelId,
1419
- url: created.url
1420
- };
1038
+ return await apiClient.getLive(slug);
1421
1039
  } catch (error) {
1422
- failCli(`Failed to create tunnel: ${formatApiError(error)}`);
1040
+ fail(`failed to fetch live info from API: ${formatApiError(error)}`);
1423
1041
  }
1042
+ throw new Error("unreachable");
1043
+ })();
1044
+ if (live.status !== "active") {
1045
+ fail(`API reports live is not active (status: ${live.status})`);
1424
1046
  }
1425
- if (!target) {
1426
- failCli("Failed to resolve tunnel target.");
1047
+ if (live.expiresAt <= Date.now()) {
1048
+ fail("API reports live is expired.");
1427
1049
  }
1428
- const socketPath = getSocketPath(target.tunnelId);
1429
- const infoPath = tunnelInfoPath(target.tunnelId);
1430
- const logPath = tunnelLogPath(target.tunnelId);
1431
- if (opts.foreground) {
1432
- if (bridgeMode !== "none") {
1433
- throw new Error(
1434
- "Foreground mode disables managed bridge process. Use background mode for --bridge openclaw."
1435
- );
1436
- }
1437
- const { startDaemon } = await import("./tunnel-daemon-7B2QUHK5.js");
1438
- console.log(`Tunnel started: ${target.url}`);
1439
- console.log(`Tunnel ID: ${target.tunnelId}`);
1440
- console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
1441
- if (target.mode === "existing") console.log("Mode: attached existing tunnel");
1442
- console.log("Running in foreground. Press Ctrl+C to stop.");
1443
- try {
1444
- await startDaemon({
1445
- cliVersion: CLI_VERSION,
1446
- tunnelId: target.tunnelId,
1447
- apiClient,
1448
- socketPath,
1449
- infoPath
1450
- });
1451
- } catch (error) {
1452
- const message = error instanceof Error ? error.message : String(error);
1453
- failCli(`Daemon failed: ${message}`);
1454
- }
1455
- return;
1050
+ if (typeof live.agentOffer !== "string" || live.agentOffer.length === 0) {
1051
+ fail("agent offer was not published.");
1456
1052
  }
1457
- const runningDaemonInfo = readDaemonProcessInfo(target.tunnelId);
1458
- if (runningDaemonInfo) {
1459
- const daemonVersion = runningDaemonInfo.cliVersion;
1460
- const shouldRestartForUpgrade = shouldRestartDaemonForCliUpgrade(
1461
- daemonVersion,
1462
- CLI_VERSION
1463
- );
1464
- if (shouldRestartForUpgrade) {
1465
- console.error(
1466
- `Restarting daemon for CLI version ${CLI_VERSION} (running: ${daemonVersion || "unknown"}).`
1467
- );
1468
- if (isBridgeRunning(target.tunnelId)) {
1469
- stopBridgeProcess(target.tunnelId);
1470
- const bridgeStopped = await waitForStopped(
1471
- () => isBridgeRunning(target.tunnelId),
1472
- 5e3
1473
- );
1474
- if (!bridgeStopped) {
1475
- failCli("Bridge process did not stop during daemon upgrade restart.");
1476
- }
1477
- }
1478
- try {
1479
- await ipcCall(socketPath, { method: "close", params: {} });
1480
- } catch (error) {
1481
- failCli(
1482
- [
1483
- `Failed to stop running daemon for upgrade: ${error instanceof Error ? error.message : String(error)}`,
1484
- "Run `pubblue tunnel close <id>` and retry."
1485
- ].join("\n")
1486
- );
1487
- }
1488
- const daemonStopped = await waitForStopped(
1489
- () => isDaemonRunning(target.tunnelId),
1490
- 6e3
1491
- );
1492
- if (!daemonStopped) {
1493
- failCli("Daemon did not stop in time during upgrade restart.");
1494
- }
1495
- } else {
1496
- try {
1497
- const status = await ipcCall(socketPath, { method: "status", params: {} });
1498
- if (!status.ok) throw new Error(String(status.error || "status check failed"));
1499
- } catch (error) {
1500
- failCli(
1501
- [
1502
- `Daemon process exists but is not responding: ${error instanceof Error ? error.message : String(error)}`,
1503
- "Run `pubblue tunnel close <id>` and start again."
1504
- ].join("\n")
1505
- );
1506
- }
1507
- if (bridgeMode !== "none") {
1508
- const bridgeReady = await ensureBridgeReady({
1509
- bridgeMode,
1510
- tunnelId: target.tunnelId,
1511
- socketPath,
1512
- bridgeProcessEnv,
1513
- timeoutMs: 8e3
1053
+ console.log("API/signaling check: OK");
1054
+ if (!opts.skipChat) {
1055
+ const pingText = "This is a ping test. Reply with 'pong'.";
1056
+ const pingMsg = {
1057
+ id: generateMessageId(),
1058
+ type: "text",
1059
+ data: pingText
1060
+ };
1061
+ const writeResponse = await ipcCall(socketPath, {
1062
+ method: "write",
1063
+ params: { channel: CHANNELS.CHAT, msg: pingMsg }
1064
+ });
1065
+ if (!writeResponse.ok) {
1066
+ fail(`chat ping failed: ${String(writeResponse.error || "unknown write error")}`);
1067
+ }
1068
+ console.log("Chat ping write ACK: OK");
1069
+ if (opts.waitPong) {
1070
+ const startedAt = Date.now();
1071
+ let receivedPong = false;
1072
+ while (Date.now() - startedAt < timeoutMs) {
1073
+ const readResponse = await ipcCall(socketPath, {
1074
+ method: "read",
1075
+ params: { channel: CHANNELS.CHAT }
1514
1076
  });
1515
- if (!bridgeReady.ok) {
1516
- const lines = [
1517
- `Bridge failed to start for running tunnel: ${bridgeReady.reason ?? "unknown reason"}`
1518
- ];
1519
- const existingBridgeLog = bridgeLogPath(target.tunnelId);
1520
- if (fs6.existsSync(existingBridgeLog)) {
1521
- lines.push(`Bridge log: ${existingBridgeLog}`);
1522
- const bridgeTail = readLogTail(existingBridgeLog);
1523
- if (bridgeTail) {
1524
- lines.push("---- bridge log tail ----");
1525
- lines.push(bridgeTail.trimEnd());
1526
- lines.push("---- end bridge log tail ----");
1527
- }
1528
- }
1529
- failCli(lines.join("\n"));
1077
+ if (!readResponse.ok) {
1078
+ fail(
1079
+ `chat read failed while waiting for pong: ${String(readResponse.error || "unknown read error")}`
1080
+ );
1530
1081
  }
1082
+ const messages = Array.isArray(readResponse.messages) ? readResponse.messages : [];
1083
+ if (messages.some((entry) => messageContainsPong(entry))) {
1084
+ receivedPong = true;
1085
+ break;
1086
+ }
1087
+ await new Promise((resolve3) => setTimeout(resolve3, 1e3));
1531
1088
  }
1532
- console.log(`Tunnel started: ${target.url}`);
1533
- console.log(`Tunnel ID: ${target.tunnelId}`);
1534
- console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
1535
- console.log("Daemon already running for this tunnel.");
1536
- console.log(`Daemon log: ${logPath}`);
1537
- if (bridgeMode !== "none") {
1538
- console.log("Bridge mode: openclaw");
1539
- console.log(`Bridge log: ${bridgeLogPath(target.tunnelId)}`);
1089
+ if (!receivedPong) {
1090
+ fail(
1091
+ `timed out after ${timeoutSeconds}s waiting for exact 'pong' reply on chat channel.`
1092
+ );
1540
1093
  }
1541
- return;
1094
+ console.log("Chat pong roundtrip: OK");
1542
1095
  }
1543
1096
  }
1544
- const daemonScript = path5.join(import.meta.dirname, "tunnel-daemon-entry.js");
1545
- const daemonLogFd = fs6.openSync(logPath, "a");
1546
- const child = fork2(daemonScript, [], {
1547
- detached: true,
1548
- stdio: buildDaemonForkStdio(daemonLogFd),
1549
- env: {
1550
- ...process.env,
1551
- PUBBLUE_DAEMON_TUNNEL_ID: target.tunnelId,
1552
- PUBBLUE_DAEMON_BASE_URL: runtimeConfig.baseUrl,
1553
- PUBBLUE_DAEMON_API_KEY: runtimeConfig.apiKey,
1554
- PUBBLUE_DAEMON_SOCKET: socketPath,
1555
- PUBBLUE_DAEMON_INFO: infoPath,
1556
- PUBBLUE_CLI_VERSION: CLI_VERSION
1097
+ if (!opts.skipCanvas) {
1098
+ const stamp = (/* @__PURE__ */ new Date()).toISOString();
1099
+ const canvasMsg = {
1100
+ id: generateMessageId(),
1101
+ type: "html",
1102
+ data: `<!doctype html><html><body style="margin:0;padding:24px;font-family:system-ui;background:#111;color:#f5f5f5">Canvas ping OK<br><small>${stamp}</small></body></html>`
1103
+ };
1104
+ const canvasResponse = await ipcCall(socketPath, {
1105
+ method: "write",
1106
+ params: { channel: CHANNELS.CANVAS, msg: canvasMsg }
1107
+ });
1108
+ if (!canvasResponse.ok) {
1109
+ fail(`canvas ping failed: ${String(canvasResponse.error || "unknown write error")}`);
1557
1110
  }
1558
- });
1559
- fs6.closeSync(daemonLogFd);
1560
- if (child.connected) {
1561
- child.disconnect();
1111
+ console.log("Canvas ping write ACK: OK");
1562
1112
  }
1563
- child.unref();
1564
- console.log(`Starting daemon for tunnel ${target.tunnelId}...`);
1565
- const ready = await waitForDaemonReady({
1566
- child,
1567
- infoPath,
1568
- socketPath,
1569
- timeoutMs: 8e3
1570
- });
1571
- if (!ready.ok) {
1572
- const lines = [
1573
- `Daemon failed to start: ${ready.reason ?? "unknown reason"}`,
1574
- `Daemon log: ${logPath}`
1575
- ];
1576
- const tail = readLogTail(logPath);
1577
- if (tail) {
1578
- lines.push("---- daemon log tail ----");
1579
- lines.push(tail.trimEnd());
1580
- lines.push("---- end daemon log tail ----");
1581
- }
1582
- await cleanupCreatedTunnelOnStartFailure(apiClient, target);
1583
- failCli(lines.join("\n"));
1113
+ console.log("Doctor: PASS");
1114
+ }
1115
+ );
1116
+ }
1117
+
1118
+ // src/commands/pubs.ts
1119
+ function registerPubCommands(program2) {
1120
+ program2.command("create").description("Create a new pub").argument("[file]", "Path to the file (reads stdin if omitted)").option("--slug <slug>", "Custom slug for the URL").option("--title <title>", "Title for the pub").option("--public", "Make the pub public").option("--private", "Make the pub private (default)").option("--expires <duration>", "Auto-delete after duration (e.g. 1h, 24h, 7d)").option("--open", "Also open an interactive session immediately").option("--bridge <mode>", "Bridge mode if --open (openclaw/none)").action(
1121
+ async (fileArg, opts) => {
1122
+ const client = createClient();
1123
+ let content;
1124
+ let filename;
1125
+ if (fileArg) {
1126
+ const file = readFile(fileArg);
1127
+ content = file.content;
1128
+ filename = file.basename;
1129
+ } else if (!opts.open) {
1130
+ content = await readFromStdin();
1584
1131
  }
1585
- const offerReady = await waitForAgentOffer({
1586
- apiClient,
1587
- tunnelId: target.tunnelId,
1588
- timeoutMs: 5e3
1132
+ const resolvedVisibility = resolveVisibilityFlags({
1133
+ public: opts.public,
1134
+ private: opts.private,
1135
+ commandName: "create"
1589
1136
  });
1590
- if (!offerReady.ok) {
1591
- const lines = [
1592
- `Daemon started but signaling is not ready: ${offerReady.reason}`,
1593
- `Daemon log: ${logPath}`
1594
- ];
1595
- const tail = readLogTail(logPath);
1596
- if (tail) {
1597
- lines.push("---- daemon log tail ----");
1598
- lines.push(tail.trimEnd());
1599
- lines.push("---- end daemon log tail ----");
1600
- }
1601
- await cleanupCreatedTunnelOnStartFailure(apiClient, target);
1602
- failCli(lines.join("\n"));
1137
+ const result = await client.create({
1138
+ content,
1139
+ filename,
1140
+ title: opts.title,
1141
+ slug: opts.slug,
1142
+ isPublic: resolvedVisibility ?? false,
1143
+ expiresIn: opts.expires
1144
+ });
1145
+ console.log(`Created: ${result.url}`);
1146
+ const tmaUrl = getTelegramMiniAppUrl(result.slug);
1147
+ if (tmaUrl) console.log(`Telegram: ${tmaUrl}`);
1148
+ if (result.expiresAt) {
1149
+ console.log(` Expires: ${new Date(result.expiresAt).toISOString()}`);
1603
1150
  }
1604
- if (bridgeMode !== "none") {
1605
- const bridgeReady = await ensureBridgeReady({
1606
- bridgeMode,
1607
- tunnelId: target.tunnelId,
1608
- socketPath,
1609
- bridgeProcessEnv,
1610
- timeoutMs: 8e3
1611
- });
1612
- if (!bridgeReady.ok) {
1613
- const lines = [`Bridge failed to start: ${bridgeReady.reason ?? "unknown reason"}`];
1614
- const bridgeLog = bridgeLogPath(target.tunnelId);
1615
- if (fs6.existsSync(bridgeLog)) {
1616
- lines.push(`Bridge log: ${bridgeLog}`);
1617
- const bridgeTail = readLogTail(bridgeLog);
1618
- if (bridgeTail) {
1619
- lines.push("---- bridge log tail ----");
1620
- lines.push(bridgeTail.trimEnd());
1621
- lines.push("---- end bridge log tail ----");
1622
- }
1623
- }
1624
- try {
1625
- await ipcCall(socketPath, { method: "close", params: {} });
1626
- } catch {
1627
- }
1628
- await cleanupCreatedTunnelOnStartFailure(apiClient, target);
1629
- failCli(lines.join("\n"));
1630
- }
1151
+ if (opts.open) {
1152
+ console.log(`
1153
+ To open an interactive session, use: pubblue open ${result.slug}`);
1631
1154
  }
1632
- console.log(`Tunnel started: ${target.url}`);
1633
- console.log(`Tunnel ID: ${target.tunnelId}`);
1634
- console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
1635
- if (target.mode === "existing") console.log("Mode: attached existing tunnel");
1636
- console.log("Daemon health: OK");
1637
- console.log(`Daemon log: ${logPath}`);
1638
- if (bridgeMode !== "none") {
1639
- console.log("Bridge mode: openclaw");
1640
- console.log(`Bridge log: ${bridgeLogPath(target.tunnelId)}`);
1155
+ }
1156
+ );
1157
+ program2.command("get").description("Get details of a pub").argument("<slug>", "Slug of the pub").option("--content", "Output raw content to stdout (no metadata, pipeable)").action(async (slug, opts) => {
1158
+ const client = createClient();
1159
+ const pub = await client.get(slug);
1160
+ if (opts.content) {
1161
+ process.stdout.write(pub.content ?? "");
1162
+ return;
1163
+ }
1164
+ console.log(` Slug: ${pub.slug}`);
1165
+ if (pub.contentType) console.log(` Type: ${pub.contentType}`);
1166
+ if (pub.title) console.log(` Title: ${pub.title}`);
1167
+ console.log(` Status: ${formatVisibility(pub.isPublic)}`);
1168
+ if (pub.expiresAt) console.log(` Expires: ${new Date(pub.expiresAt).toISOString()}`);
1169
+ console.log(` Created: ${new Date(pub.createdAt).toLocaleDateString()}`);
1170
+ console.log(` Updated: ${new Date(pub.updatedAt).toLocaleDateString()}`);
1171
+ if (pub.content) console.log(` Size: ${pub.content.length} bytes`);
1172
+ if (pub.live) {
1173
+ console.log(` Live: ${pub.live.status}`);
1174
+ console.log(` Connected: ${pub.live.hasConnection ? "yes" : "no"}`);
1175
+ console.log(` Expires: ${new Date(pub.live.expiresAt).toISOString()}`);
1176
+ }
1177
+ });
1178
+ program2.command("update").description("Update a pub's content and/or metadata").argument("<slug>", "Slug of the pub to update").option("--file <file>", "New content from file").option("--title <title>", "New title").option("--public", "Make the pub public").option("--private", "Make the pub private").option("--slug <newSlug>", "Rename the slug").action(
1179
+ async (slug, opts) => {
1180
+ const client = createClient();
1181
+ let content;
1182
+ let filename;
1183
+ if (opts.file) {
1184
+ const file = readFile(opts.file);
1185
+ content = file.content;
1186
+ filename = file.basename;
1641
1187
  }
1188
+ const isPublic = resolveVisibilityFlags({
1189
+ public: opts.public,
1190
+ private: opts.private,
1191
+ commandName: "update"
1192
+ });
1193
+ const result = await client.update({
1194
+ slug,
1195
+ content,
1196
+ filename,
1197
+ title: opts.title,
1198
+ isPublic,
1199
+ newSlug: opts.slug
1200
+ });
1201
+ console.log(`Updated: ${result.slug}`);
1202
+ if (result.title) console.log(` Title: ${result.title}`);
1203
+ console.log(` Status: ${formatVisibility(result.isPublic)}`);
1642
1204
  }
1643
1205
  );
1644
- }
1645
-
1646
- // src/commands/tunnel.ts
1647
- function registerTunnelCommands(program2) {
1648
- const tunnel = program2.command("tunnel").description("P2P encrypted tunnel to browser");
1649
- registerTunnelStartCommand(tunnel);
1650
- registerTunnelMessageCommands(tunnel);
1651
- registerTunnelManagementCommands(tunnel);
1206
+ program2.command("list").description("List your pubs").action(async () => {
1207
+ const client = createClient();
1208
+ const pubs = await client.list();
1209
+ if (pubs.length === 0) {
1210
+ console.log("No pubs.");
1211
+ return;
1212
+ }
1213
+ for (const pub of pubs) {
1214
+ const date = new Date(pub.createdAt).toLocaleDateString();
1215
+ const expires = pub.expiresAt ? ` expires:${new Date(pub.expiresAt).toISOString()}` : "";
1216
+ const contentLabel = pub.contentType ? `[${pub.contentType}]` : "[no content]";
1217
+ const sessionLabel = pub.live?.status === "active" ? " [live]" : "";
1218
+ console.log(
1219
+ ` ${pub.slug} ${contentLabel} ${formatVisibility(pub.isPublic)} ${date}${expires}${sessionLabel}`
1220
+ );
1221
+ }
1222
+ });
1223
+ program2.command("delete").description("Delete a pub").argument("<slug>", "Slug of the pub to delete").action(async (slug) => {
1224
+ const client = createClient();
1225
+ await client.remove(slug);
1226
+ console.log(`Deleted: ${slug}`);
1227
+ });
1652
1228
  }
1653
1229
 
1654
1230
  // src/program.ts
1655
1231
  function buildProgram() {
1656
1232
  const program2 = new Command();
1657
1233
  program2.exitOverride();
1658
- program2.name("pubblue").description("Publish static content and get shareable URLs").version(CLI_VERSION);
1234
+ program2.name("pubblue").description("Publish content and go live").version(CLI_VERSION);
1659
1235
  registerConfigureCommand(program2);
1660
- registerPublicationCommands(program2);
1661
- registerTunnelCommands(program2);
1236
+ registerPubCommands(program2);
1237
+ registerLiveCommands(program2);
1662
1238
  return program2;
1663
1239
  }
1664
1240