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/{chunk-HAIOMGND.js → chunk-5GSMS3YU.js} +137 -21
- package/dist/{chunk-4YTJ2WKF.js → chunk-PFZT7M3E.js} +55 -1
- package/dist/chunk-YI45G6AG.js +759 -0
- package/dist/index.js +823 -1247
- package/dist/tunnel-bridge-entry.js +84 -42
- package/dist/tunnel-daemon-QN6TVUX6.js +8 -0
- package/dist/tunnel-daemon-entry.js +24 -10
- package/package.json +2 -2
- package/dist/chunk-7NFHPJ76.js +0 -79
- package/dist/chunk-HJ5LTUHS.js +0 -56
- package/dist/tunnel-daemon-7B2QUHK5.js +0 -11
package/dist/index.js
CHANGED
|
@@ -1,55 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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/
|
|
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 =
|
|
209
|
-
if (!
|
|
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:
|
|
214
|
-
basename:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
369
|
-
return Object.values(
|
|
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
|
|
372
|
-
|
|
373
|
-
|
|
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: ${
|
|
382
|
-
if (
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (saved.
|
|
393
|
-
console.log(`
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
console.log(
|
|
397
|
-
|
|
398
|
-
console.log(
|
|
399
|
-
|
|
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
|
|
341
|
+
"Set config key (repeatable). Example: --set telegram.botToken=<token>",
|
|
406
342
|
collectValues,
|
|
407
343
|
[]
|
|
408
|
-
).option("--unset <key>", "Unset
|
|
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
|
-
|
|
374
|
+
applyConfigSet(nextBridge, nextTelegram, key, value);
|
|
375
|
+
if (key === "telegram.botToken") telegramTokenChanged = true;
|
|
437
376
|
}
|
|
438
377
|
for (const key of opts.unset) {
|
|
439
|
-
|
|
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:
|
|
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/
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
//
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
"
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
const socketPath = getSocketPath(
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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 =
|
|
988
|
-
if (
|
|
845
|
+
const logPath = liveLogPath(slug);
|
|
846
|
+
if (fs2.existsSync(logPath)) {
|
|
989
847
|
console.log(` Log: ${logPath}`);
|
|
990
848
|
}
|
|
991
|
-
const bridgeInfo = readBridgeProcessInfo(
|
|
849
|
+
const bridgeInfo = readBridgeProcessInfo(slug);
|
|
992
850
|
if (bridgeInfo) {
|
|
993
|
-
const bridgeRunning = isBridgeRunning(
|
|
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(
|
|
1010
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
1182
|
-
const ext =
|
|
1183
|
-
const bytes =
|
|
1184
|
-
const filename =
|
|
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
|
|
1223
|
-
const socketPath = getSocketPath(
|
|
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
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
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
|
|
1354
|
-
|
|
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
|
-
|
|
1357
|
-
const
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
const
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
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
|
-
|
|
1035
|
+
console.log("Daemon/channel check: OK");
|
|
1036
|
+
const live = await (async () => {
|
|
1410
1037
|
try {
|
|
1411
|
-
|
|
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
|
-
|
|
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 (
|
|
1426
|
-
|
|
1047
|
+
if (live.expiresAt <= Date.now()) {
|
|
1048
|
+
fail("API reports live is expired.");
|
|
1427
1049
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
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
|
-
|
|
1458
|
-
if (
|
|
1459
|
-
const
|
|
1460
|
-
const
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
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 (!
|
|
1516
|
-
|
|
1517
|
-
`
|
|
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
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
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
|
-
|
|
1094
|
+
console.log("Chat pong roundtrip: OK");
|
|
1542
1095
|
}
|
|
1543
1096
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
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
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
const
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
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
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1132
|
+
const resolvedVisibility = resolveVisibilityFlags({
|
|
1133
|
+
public: opts.public,
|
|
1134
|
+
private: opts.private,
|
|
1135
|
+
commandName: "create"
|
|
1589
1136
|
});
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
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 (
|
|
1605
|
-
|
|
1606
|
-
|
|
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
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
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
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
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
|
|
1234
|
+
program2.name("pubblue").description("Publish content and go live").version(CLI_VERSION);
|
|
1659
1235
|
registerConfigureCommand(program2);
|
|
1660
|
-
|
|
1661
|
-
|
|
1236
|
+
registerPubCommands(program2);
|
|
1237
|
+
registerLiveCommands(program2);
|
|
1662
1238
|
return program2;
|
|
1663
1239
|
}
|
|
1664
1240
|
|