pubblue 0.5.0 → 0.6.4
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-JXEXE632.js +608 -0
- package/dist/chunk-QFJDLFK5.js +1366 -0
- package/dist/index.js +321 -572
- package/dist/live-daemon-EEIBVVBU.js +7 -0
- package/dist/live-daemon-entry.js +27 -0
- package/package.json +3 -3
- package/dist/chunk-5GSMS3YU.js +0 -776
- package/dist/chunk-PFZT7M3E.js +0 -114
- package/dist/chunk-YI45G6AG.js +0 -759
- package/dist/tunnel-bridge-entry.js +0 -703
- package/dist/tunnel-daemon-QN6TVUX6.js +0 -8
- package/dist/tunnel-daemon-entry.d.ts +0 -2
- package/dist/tunnel-daemon-entry.js +0 -39
- /package/dist/{tunnel-bridge-entry.d.ts → live-daemon-entry.d.ts} +0 -0
package/dist/chunk-YI45G6AG.js
DELETED
|
@@ -1,759 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ipcCall
|
|
3
|
-
} from "./chunk-PFZT7M3E.js";
|
|
4
|
-
|
|
5
|
-
// src/lib/cli-error.ts
|
|
6
|
-
import { CommanderError } from "commander";
|
|
7
|
-
var CliError = class extends Error {
|
|
8
|
-
exitCode;
|
|
9
|
-
constructor(message, exitCode = 1) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.name = "CliError";
|
|
12
|
-
this.exitCode = exitCode;
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
function failCli(message, exitCode = 1) {
|
|
16
|
-
throw new CliError(message, exitCode);
|
|
17
|
-
}
|
|
18
|
-
function toCliFailure(error) {
|
|
19
|
-
if (error instanceof CommanderError) {
|
|
20
|
-
return {
|
|
21
|
-
exitCode: error.exitCode,
|
|
22
|
-
message: ""
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
if (error instanceof CliError) {
|
|
26
|
-
return {
|
|
27
|
-
exitCode: error.exitCode,
|
|
28
|
-
message: error.message
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
if (error instanceof Error) {
|
|
32
|
-
return {
|
|
33
|
-
exitCode: 1,
|
|
34
|
-
message: error.message
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
return {
|
|
38
|
-
exitCode: 1,
|
|
39
|
-
message: String(error)
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// src/lib/api.ts
|
|
44
|
-
var PubApiError = class extends Error {
|
|
45
|
-
constructor(message, status, retryAfterSeconds) {
|
|
46
|
-
super(message);
|
|
47
|
-
this.status = status;
|
|
48
|
-
this.retryAfterSeconds = retryAfterSeconds;
|
|
49
|
-
this.name = "PubApiError";
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
var PubApiClient = class {
|
|
53
|
-
constructor(baseUrl, apiKey) {
|
|
54
|
-
this.baseUrl = baseUrl;
|
|
55
|
-
this.apiKey = apiKey;
|
|
56
|
-
}
|
|
57
|
-
async request(path3, options = {}) {
|
|
58
|
-
const url = new URL(path3, this.baseUrl);
|
|
59
|
-
const res = await fetch(url, {
|
|
60
|
-
...options,
|
|
61
|
-
headers: {
|
|
62
|
-
"Content-Type": "application/json",
|
|
63
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
64
|
-
...options.headers
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
const retryAfterHeader = res.headers.get("Retry-After");
|
|
68
|
-
const parsedRetryAfterSeconds = typeof retryAfterHeader === "string" ? Number.parseInt(retryAfterHeader, 10) : void 0;
|
|
69
|
-
const retryAfterSeconds = parsedRetryAfterSeconds !== void 0 && Number.isFinite(parsedRetryAfterSeconds) ? parsedRetryAfterSeconds : void 0;
|
|
70
|
-
let data;
|
|
71
|
-
try {
|
|
72
|
-
data = await res.json();
|
|
73
|
-
} catch {
|
|
74
|
-
data = {};
|
|
75
|
-
}
|
|
76
|
-
if (!res.ok) {
|
|
77
|
-
if (res.status === 429) {
|
|
78
|
-
const retrySuffix = retryAfterSeconds !== void 0 ? ` Retry after ${retryAfterSeconds}s.` : "";
|
|
79
|
-
throw new PubApiError(`Rate limit exceeded.${retrySuffix}`, res.status, retryAfterSeconds);
|
|
80
|
-
}
|
|
81
|
-
throw new PubApiError(data.error || `Request failed with status ${res.status}`, res.status);
|
|
82
|
-
}
|
|
83
|
-
return data;
|
|
84
|
-
}
|
|
85
|
-
// -- Pub CRUD -------------------------------------------------------------
|
|
86
|
-
async create(opts) {
|
|
87
|
-
return this.request("/api/v1/pubs", {
|
|
88
|
-
method: "POST",
|
|
89
|
-
body: JSON.stringify(opts)
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
async get(slug) {
|
|
93
|
-
const data = await this.request(`/api/v1/pubs/${encodeURIComponent(slug)}`);
|
|
94
|
-
return data.pub;
|
|
95
|
-
}
|
|
96
|
-
async listPage(cursor, limit) {
|
|
97
|
-
const params = new URLSearchParams();
|
|
98
|
-
if (cursor) params.set("cursor", cursor);
|
|
99
|
-
if (limit) params.set("limit", String(limit));
|
|
100
|
-
const qs = params.toString();
|
|
101
|
-
return this.request(`/api/v1/pubs${qs ? `?${qs}` : ""}`);
|
|
102
|
-
}
|
|
103
|
-
async list() {
|
|
104
|
-
const all = [];
|
|
105
|
-
let cursor;
|
|
106
|
-
do {
|
|
107
|
-
const result = await this.listPage(cursor, 100);
|
|
108
|
-
all.push(...result.pubs);
|
|
109
|
-
cursor = result.hasMore ? result.cursor : void 0;
|
|
110
|
-
} while (cursor);
|
|
111
|
-
return all;
|
|
112
|
-
}
|
|
113
|
-
async update(opts) {
|
|
114
|
-
const { slug, newSlug, ...rest } = opts;
|
|
115
|
-
const body = { ...rest };
|
|
116
|
-
if (newSlug) body.slug = newSlug;
|
|
117
|
-
return this.request(`/api/v1/pubs/${encodeURIComponent(slug)}`, {
|
|
118
|
-
method: "PATCH",
|
|
119
|
-
body: JSON.stringify(body)
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
async remove(slug) {
|
|
123
|
-
await this.request(`/api/v1/pubs/${encodeURIComponent(slug)}`, {
|
|
124
|
-
method: "DELETE"
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
// -- Live management -----------------------------------------------------
|
|
128
|
-
async openLive(slug, opts = {}) {
|
|
129
|
-
return this.request(`/api/v1/pubs/${encodeURIComponent(slug)}/live`, {
|
|
130
|
-
method: "POST",
|
|
131
|
-
body: JSON.stringify(opts)
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
async getLive(slug) {
|
|
135
|
-
const data = await this.request(
|
|
136
|
-
`/api/v1/pubs/${encodeURIComponent(slug)}/live`
|
|
137
|
-
);
|
|
138
|
-
return data.live;
|
|
139
|
-
}
|
|
140
|
-
async signal(slug, opts) {
|
|
141
|
-
await this.request(`/api/v1/pubs/${encodeURIComponent(slug)}/live/signal`, {
|
|
142
|
-
method: "PATCH",
|
|
143
|
-
body: JSON.stringify(opts)
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
async closeLive(slug) {
|
|
147
|
-
await this.request(`/api/v1/pubs/${encodeURIComponent(slug)}/live`, {
|
|
148
|
-
method: "DELETE"
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
// src/commands/tunnel-helpers.ts
|
|
154
|
-
import { fork } from "child_process";
|
|
155
|
-
import * as fs2 from "fs";
|
|
156
|
-
import * as path2 from "path";
|
|
157
|
-
|
|
158
|
-
// src/lib/config.ts
|
|
159
|
-
import * as fs from "fs";
|
|
160
|
-
import * as os from "os";
|
|
161
|
-
import * as path from "path";
|
|
162
|
-
var DEFAULT_BASE_URL = "https://silent-guanaco-514.convex.site";
|
|
163
|
-
function getConfigDir(homeDir) {
|
|
164
|
-
const home = homeDir || os.homedir();
|
|
165
|
-
return path.join(home, ".config", "pubblue");
|
|
166
|
-
}
|
|
167
|
-
function getConfigPath(homeDir) {
|
|
168
|
-
const dir = getConfigDir(homeDir);
|
|
169
|
-
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
170
|
-
try {
|
|
171
|
-
fs.chmodSync(dir, 448);
|
|
172
|
-
} catch {
|
|
173
|
-
}
|
|
174
|
-
return path.join(dir, "config.json");
|
|
175
|
-
}
|
|
176
|
-
function loadConfig(homeDir) {
|
|
177
|
-
const configPath = getConfigPath(homeDir);
|
|
178
|
-
if (!fs.existsSync(configPath)) return null;
|
|
179
|
-
const raw = fs.readFileSync(configPath, "utf-8");
|
|
180
|
-
return JSON.parse(raw);
|
|
181
|
-
}
|
|
182
|
-
function saveConfig(config, homeDir) {
|
|
183
|
-
const configPath = getConfigPath(homeDir);
|
|
184
|
-
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
185
|
-
`, {
|
|
186
|
-
mode: 384
|
|
187
|
-
});
|
|
188
|
-
try {
|
|
189
|
-
fs.chmodSync(configPath, 384);
|
|
190
|
-
} catch {
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
function getConfig(homeDir) {
|
|
194
|
-
const envKey = process.env.PUBBLUE_API_KEY;
|
|
195
|
-
const envUrl = process.env.PUBBLUE_URL;
|
|
196
|
-
const baseUrl = envUrl || DEFAULT_BASE_URL;
|
|
197
|
-
const saved = loadConfig(homeDir);
|
|
198
|
-
if (envKey) {
|
|
199
|
-
return { apiKey: envKey, baseUrl, bridge: saved?.bridge };
|
|
200
|
-
}
|
|
201
|
-
if (!saved) {
|
|
202
|
-
throw new Error(
|
|
203
|
-
"Not configured. Run `pubblue configure` or set PUBBLUE_API_KEY environment variable."
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
return {
|
|
207
|
-
apiKey: saved.apiKey,
|
|
208
|
-
baseUrl,
|
|
209
|
-
bridge: saved.bridge
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
function getTelegramMiniAppUrl(slug) {
|
|
213
|
-
const saved = loadConfig();
|
|
214
|
-
if (!saved?.telegram?.botUsername) return null;
|
|
215
|
-
return `https://t.me/${saved.telegram.botUsername}?startapp=${slug}`;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// src/commands/tunnel-helpers.ts
|
|
219
|
-
var TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
220
|
-
".txt",
|
|
221
|
-
".md",
|
|
222
|
-
".markdown",
|
|
223
|
-
".json",
|
|
224
|
-
".csv",
|
|
225
|
-
".xml",
|
|
226
|
-
".yaml",
|
|
227
|
-
".yml",
|
|
228
|
-
".js",
|
|
229
|
-
".mjs",
|
|
230
|
-
".cjs",
|
|
231
|
-
".ts",
|
|
232
|
-
".tsx",
|
|
233
|
-
".jsx",
|
|
234
|
-
".css",
|
|
235
|
-
".scss",
|
|
236
|
-
".sass",
|
|
237
|
-
".less",
|
|
238
|
-
".log"
|
|
239
|
-
]);
|
|
240
|
-
function getMimeType(filePath) {
|
|
241
|
-
const ext = path2.extname(filePath).toLowerCase();
|
|
242
|
-
const mimeByExt = {
|
|
243
|
-
".html": "text/html; charset=utf-8",
|
|
244
|
-
".htm": "text/html; charset=utf-8",
|
|
245
|
-
".txt": "text/plain; charset=utf-8",
|
|
246
|
-
".md": "text/markdown; charset=utf-8",
|
|
247
|
-
".markdown": "text/markdown; charset=utf-8",
|
|
248
|
-
".json": "application/json",
|
|
249
|
-
".csv": "text/csv; charset=utf-8",
|
|
250
|
-
".xml": "application/xml",
|
|
251
|
-
".yaml": "application/x-yaml",
|
|
252
|
-
".yml": "application/x-yaml",
|
|
253
|
-
".png": "image/png",
|
|
254
|
-
".jpg": "image/jpeg",
|
|
255
|
-
".jpeg": "image/jpeg",
|
|
256
|
-
".gif": "image/gif",
|
|
257
|
-
".webp": "image/webp",
|
|
258
|
-
".svg": "image/svg+xml",
|
|
259
|
-
".pdf": "application/pdf",
|
|
260
|
-
".zip": "application/zip",
|
|
261
|
-
".mp3": "audio/mpeg",
|
|
262
|
-
".wav": "audio/wav",
|
|
263
|
-
".mp4": "video/mp4"
|
|
264
|
-
};
|
|
265
|
-
return mimeByExt[ext] || "application/octet-stream";
|
|
266
|
-
}
|
|
267
|
-
function liveInfoDir() {
|
|
268
|
-
const dir = path2.join(
|
|
269
|
-
process.env.HOME || process.env.USERPROFILE || "/tmp",
|
|
270
|
-
".config",
|
|
271
|
-
"pubblue",
|
|
272
|
-
"lives"
|
|
273
|
-
);
|
|
274
|
-
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
275
|
-
return dir;
|
|
276
|
-
}
|
|
277
|
-
function liveInfoPath(slug) {
|
|
278
|
-
return path2.join(liveInfoDir(), `${slug}.json`);
|
|
279
|
-
}
|
|
280
|
-
function liveLogPath(slug) {
|
|
281
|
-
return path2.join(liveInfoDir(), `${slug}.log`);
|
|
282
|
-
}
|
|
283
|
-
function bridgeInfoPath(slug) {
|
|
284
|
-
return path2.join(liveInfoDir(), `${slug}.bridge.json`);
|
|
285
|
-
}
|
|
286
|
-
function bridgeLogPath(slug) {
|
|
287
|
-
return path2.join(liveInfoDir(), `${slug}.bridge.log`);
|
|
288
|
-
}
|
|
289
|
-
function createApiClient(configOverride) {
|
|
290
|
-
const config = configOverride || getConfig();
|
|
291
|
-
return new PubApiClient(config.baseUrl, config.apiKey);
|
|
292
|
-
}
|
|
293
|
-
function buildBridgeProcessEnv(bridgeConfig) {
|
|
294
|
-
const env = { ...process.env };
|
|
295
|
-
const setIfMissing = (key, value) => {
|
|
296
|
-
if (value === void 0 || value === null) return;
|
|
297
|
-
const current = env[key];
|
|
298
|
-
if (typeof current === "string" && current.length > 0) return;
|
|
299
|
-
env[key] = String(value);
|
|
300
|
-
};
|
|
301
|
-
setIfMissing("PUBBLUE_PROJECT_ROOT", process.cwd());
|
|
302
|
-
if (!bridgeConfig) return env;
|
|
303
|
-
setIfMissing("OPENCLAW_PATH", bridgeConfig.openclawPath);
|
|
304
|
-
setIfMissing("OPENCLAW_SESSION_ID", bridgeConfig.sessionId);
|
|
305
|
-
setIfMissing("OPENCLAW_THREAD_ID", bridgeConfig.threadId);
|
|
306
|
-
if (bridgeConfig.canvasReminderEvery !== void 0) {
|
|
307
|
-
setIfMissing("OPENCLAW_CANVAS_REMINDER_EVERY", bridgeConfig.canvasReminderEvery);
|
|
308
|
-
}
|
|
309
|
-
if (bridgeConfig.deliver !== void 0) {
|
|
310
|
-
setIfMissing("OPENCLAW_DELIVER", bridgeConfig.deliver ? "1" : "0");
|
|
311
|
-
}
|
|
312
|
-
setIfMissing("OPENCLAW_DELIVER_CHANNEL", bridgeConfig.deliverChannel);
|
|
313
|
-
setIfMissing("OPENCLAW_REPLY_TO", bridgeConfig.replyTo);
|
|
314
|
-
if (bridgeConfig.deliverTimeoutMs !== void 0) {
|
|
315
|
-
setIfMissing("OPENCLAW_DELIVER_TIMEOUT_MS", bridgeConfig.deliverTimeoutMs);
|
|
316
|
-
}
|
|
317
|
-
setIfMissing("OPENCLAW_ATTACHMENT_DIR", bridgeConfig.attachmentDir);
|
|
318
|
-
if (bridgeConfig.attachmentMaxBytes !== void 0) {
|
|
319
|
-
setIfMissing("OPENCLAW_ATTACHMENT_MAX_BYTES", bridgeConfig.attachmentMaxBytes);
|
|
320
|
-
}
|
|
321
|
-
return env;
|
|
322
|
-
}
|
|
323
|
-
async function ensureNodeDatachannelAvailable() {
|
|
324
|
-
try {
|
|
325
|
-
await import("node-datachannel");
|
|
326
|
-
} catch (error) {
|
|
327
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
328
|
-
failCli(
|
|
329
|
-
[
|
|
330
|
-
"node-datachannel native module is not available.",
|
|
331
|
-
"Run `pnpm rebuild node-datachannel` in the cli package and retry.",
|
|
332
|
-
`Details: ${message}`
|
|
333
|
-
].join("\n")
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
function isDaemonRunning(slug) {
|
|
338
|
-
return readDaemonProcessInfo(slug) !== null;
|
|
339
|
-
}
|
|
340
|
-
function readDaemonProcessInfo(slug) {
|
|
341
|
-
const infoPath = liveInfoPath(slug);
|
|
342
|
-
if (!fs2.existsSync(infoPath)) return null;
|
|
343
|
-
try {
|
|
344
|
-
const info = JSON.parse(fs2.readFileSync(infoPath, "utf-8"));
|
|
345
|
-
if (!Number.isFinite(info.pid)) throw new Error("invalid daemon pid");
|
|
346
|
-
process.kill(info.pid, 0);
|
|
347
|
-
return info;
|
|
348
|
-
} catch {
|
|
349
|
-
try {
|
|
350
|
-
fs2.unlinkSync(infoPath);
|
|
351
|
-
} catch {
|
|
352
|
-
}
|
|
353
|
-
return null;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
function readBridgeProcessInfo(slug) {
|
|
357
|
-
const infoPath = bridgeInfoPath(slug);
|
|
358
|
-
if (!fs2.existsSync(infoPath)) return null;
|
|
359
|
-
try {
|
|
360
|
-
return JSON.parse(fs2.readFileSync(infoPath, "utf-8"));
|
|
361
|
-
} catch {
|
|
362
|
-
return null;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
function isBridgeRunning(slug) {
|
|
366
|
-
const infoPath = bridgeInfoPath(slug);
|
|
367
|
-
if (!fs2.existsSync(infoPath)) return false;
|
|
368
|
-
try {
|
|
369
|
-
const info = JSON.parse(fs2.readFileSync(infoPath, "utf-8"));
|
|
370
|
-
process.kill(info.pid, 0);
|
|
371
|
-
return true;
|
|
372
|
-
} catch {
|
|
373
|
-
try {
|
|
374
|
-
fs2.unlinkSync(infoPath);
|
|
375
|
-
} catch {
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
return false;
|
|
379
|
-
}
|
|
380
|
-
function latestCliVersionPath() {
|
|
381
|
-
return path2.join(liveInfoDir(), "cli-version.txt");
|
|
382
|
-
}
|
|
383
|
-
function readLatestCliVersion(versionPath) {
|
|
384
|
-
const resolved = versionPath || latestCliVersionPath();
|
|
385
|
-
if (!fs2.existsSync(resolved)) return null;
|
|
386
|
-
try {
|
|
387
|
-
const value = fs2.readFileSync(resolved, "utf-8").trim();
|
|
388
|
-
return value.length === 0 ? null : value;
|
|
389
|
-
} catch {
|
|
390
|
-
return null;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
function writeLatestCliVersion(version, versionPath) {
|
|
394
|
-
if (!version || version.trim().length === 0) return;
|
|
395
|
-
const resolved = versionPath || latestCliVersionPath();
|
|
396
|
-
const dir = path2.dirname(resolved);
|
|
397
|
-
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
398
|
-
fs2.writeFileSync(resolved, version.trim(), "utf-8");
|
|
399
|
-
}
|
|
400
|
-
function isProcessAlive(pid) {
|
|
401
|
-
try {
|
|
402
|
-
process.kill(pid, 0);
|
|
403
|
-
return true;
|
|
404
|
-
} catch {
|
|
405
|
-
return false;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
async function waitForProcessExit(pid, timeoutMs) {
|
|
409
|
-
const startedAt = Date.now();
|
|
410
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
411
|
-
if (!isProcessAlive(pid)) return true;
|
|
412
|
-
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
413
|
-
}
|
|
414
|
-
return !isProcessAlive(pid);
|
|
415
|
-
}
|
|
416
|
-
async function stopBridge(slug) {
|
|
417
|
-
const bridge = readBridgeProcessInfo(slug);
|
|
418
|
-
if (!bridge || !Number.isFinite(bridge.pid)) return null;
|
|
419
|
-
if (!isProcessAlive(bridge.pid)) return null;
|
|
420
|
-
try {
|
|
421
|
-
process.kill(bridge.pid, "SIGTERM");
|
|
422
|
-
} catch (error) {
|
|
423
|
-
return `bridge ${bridge.pid}: failed to send SIGTERM (${error instanceof Error ? error.message : String(error)})`;
|
|
424
|
-
}
|
|
425
|
-
const stopped = await waitForProcessExit(bridge.pid, 6e3);
|
|
426
|
-
if (!stopped) return `bridge ${bridge.pid}: did not exit after SIGTERM`;
|
|
427
|
-
return null;
|
|
428
|
-
}
|
|
429
|
-
async function stopDaemonForLive(info) {
|
|
430
|
-
const pid = info.pid;
|
|
431
|
-
if (!Number.isFinite(pid) || !isProcessAlive(pid)) return null;
|
|
432
|
-
const socketPath = info.socketPath;
|
|
433
|
-
if (socketPath) {
|
|
434
|
-
try {
|
|
435
|
-
await ipcCall(socketPath, { method: "close", params: {} });
|
|
436
|
-
} catch (error) {
|
|
437
|
-
try {
|
|
438
|
-
process.kill(pid, "SIGTERM");
|
|
439
|
-
} catch (killError) {
|
|
440
|
-
return `daemon ${pid}: IPC close failed (${error instanceof Error ? error.message : String(error)}); SIGTERM failed (${killError instanceof Error ? killError.message : String(killError)})`;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
} else {
|
|
444
|
-
try {
|
|
445
|
-
process.kill(pid, "SIGTERM");
|
|
446
|
-
} catch (error) {
|
|
447
|
-
return `daemon ${pid}: no socketPath and SIGTERM failed (${error instanceof Error ? error.message : String(error)})`;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
const stopped = await waitForProcessExit(pid, 8e3);
|
|
451
|
-
if (!stopped) return `daemon ${pid}: did not exit after stop request`;
|
|
452
|
-
return null;
|
|
453
|
-
}
|
|
454
|
-
async function stopOtherDaemons(exceptSlug) {
|
|
455
|
-
const dir = liveInfoDir();
|
|
456
|
-
const entries = fs2.readdirSync(dir).filter((name) => name.endsWith(".json") && !name.endsWith(".bridge.json"));
|
|
457
|
-
const failures = [];
|
|
458
|
-
for (const entry of entries) {
|
|
459
|
-
const slug = entry.replace(/\.json$/, "");
|
|
460
|
-
if (exceptSlug && slug === exceptSlug) continue;
|
|
461
|
-
const bridgeError = await stopBridge(slug);
|
|
462
|
-
if (bridgeError) failures.push(`[${slug}] ${bridgeError}`);
|
|
463
|
-
const info = readDaemonProcessInfo(slug);
|
|
464
|
-
if (!info) continue;
|
|
465
|
-
const daemonError = await stopDaemonForLive(info);
|
|
466
|
-
if (daemonError) failures.push(`[${slug}] ${daemonError}`);
|
|
467
|
-
}
|
|
468
|
-
if (failures.length > 0) {
|
|
469
|
-
throw new Error(
|
|
470
|
-
[
|
|
471
|
-
"Critical: failed to stop previous live daemon/bridge processes.",
|
|
472
|
-
"Starting a new daemon now would leak resources and increase bandwidth usage.",
|
|
473
|
-
...failures
|
|
474
|
-
].join("\n")
|
|
475
|
-
);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
function buildBridgeForkStdio(logFd) {
|
|
479
|
-
return ["ignore", logFd, logFd, "ipc"];
|
|
480
|
-
}
|
|
481
|
-
function getFollowReadDelayMs(disconnected, consecutiveFailures) {
|
|
482
|
-
if (!disconnected) return 1e3;
|
|
483
|
-
return Math.min(5e3, 1e3 * 2 ** Math.min(consecutiveFailures, 3));
|
|
484
|
-
}
|
|
485
|
-
function resolveSlugSelection(slugArg, slugOpt) {
|
|
486
|
-
return slugOpt || slugArg;
|
|
487
|
-
}
|
|
488
|
-
function buildDaemonForkStdio(logFd) {
|
|
489
|
-
return ["ignore", logFd, logFd, "ipc"];
|
|
490
|
-
}
|
|
491
|
-
function parsePositiveIntegerOption(raw, optionName) {
|
|
492
|
-
const parsed = Number.parseInt(raw, 10);
|
|
493
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
494
|
-
throw new Error(`${optionName} must be a positive integer. Received: ${raw}`);
|
|
495
|
-
}
|
|
496
|
-
return parsed;
|
|
497
|
-
}
|
|
498
|
-
function parseBridgeMode(raw) {
|
|
499
|
-
const normalized = raw.trim().toLowerCase();
|
|
500
|
-
if (normalized === "openclaw" || normalized === "none") {
|
|
501
|
-
return normalized;
|
|
502
|
-
}
|
|
503
|
-
throw new Error(`--bridge must be one of: openclaw, none. Received: ${raw}`);
|
|
504
|
-
}
|
|
505
|
-
function shouldRestartDaemonForCliUpgrade(daemonCliVersion, currentCliVersion) {
|
|
506
|
-
if (!daemonCliVersion || daemonCliVersion.trim().length === 0) return true;
|
|
507
|
-
return daemonCliVersion.trim() !== currentCliVersion;
|
|
508
|
-
}
|
|
509
|
-
function messageContainsPong(payload) {
|
|
510
|
-
if (!payload || typeof payload !== "object") return false;
|
|
511
|
-
const message = payload.msg;
|
|
512
|
-
if (!message || typeof message !== "object") return false;
|
|
513
|
-
const type = message.type;
|
|
514
|
-
const data = message.data;
|
|
515
|
-
return type === "text" && typeof data === "string" && data.trim().toLowerCase() === "pong";
|
|
516
|
-
}
|
|
517
|
-
function getPublicUrl(slug) {
|
|
518
|
-
const base = process.env.PUBBLUE_PUBLIC_URL || "https://pub.blue";
|
|
519
|
-
return `${base.replace(/\/$/, "")}/p/${slug}`;
|
|
520
|
-
}
|
|
521
|
-
function pickReusableLive(pubs, nowMs = Date.now()) {
|
|
522
|
-
const active = pubs.filter((p) => p.live?.status === "active" && p.live.expiresAt > nowMs).sort((a, b) => b.createdAt - a.createdAt);
|
|
523
|
-
return active[0] ?? null;
|
|
524
|
-
}
|
|
525
|
-
function readLogTail(logPath, maxChars = 4e3) {
|
|
526
|
-
if (!fs2.existsSync(logPath)) return null;
|
|
527
|
-
try {
|
|
528
|
-
const content = fs2.readFileSync(logPath, "utf-8");
|
|
529
|
-
if (content.length <= maxChars) return content;
|
|
530
|
-
return content.slice(-maxChars);
|
|
531
|
-
} catch {
|
|
532
|
-
return null;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
function formatApiError(error) {
|
|
536
|
-
if (error instanceof PubApiError) {
|
|
537
|
-
if (error.status === 429 && error.retryAfterSeconds !== void 0) {
|
|
538
|
-
return `Rate limit exceeded. Retry after ${error.retryAfterSeconds}s.`;
|
|
539
|
-
}
|
|
540
|
-
return `${error.message} (HTTP ${error.status})`;
|
|
541
|
-
}
|
|
542
|
-
return error instanceof Error ? error.message : String(error);
|
|
543
|
-
}
|
|
544
|
-
async function cleanupLiveOnStartFailure(apiClient, target) {
|
|
545
|
-
if (!target.createdNew) return;
|
|
546
|
-
try {
|
|
547
|
-
await apiClient.closeLive(target.slug);
|
|
548
|
-
} catch (closeError) {
|
|
549
|
-
console.error(`Failed to clean up live for ${target.slug}: ${formatApiError(closeError)}`);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
async function resolveActiveSlug() {
|
|
553
|
-
const dir = liveInfoDir();
|
|
554
|
-
const files = fs2.readdirSync(dir).filter((f) => f.endsWith(".json") && !f.endsWith(".bridge.json"));
|
|
555
|
-
const active = [];
|
|
556
|
-
for (const f of files) {
|
|
557
|
-
const slug = f.replace(".json", "");
|
|
558
|
-
if (isDaemonRunning(slug)) active.push(slug);
|
|
559
|
-
}
|
|
560
|
-
if (active.length === 0) {
|
|
561
|
-
failCli("No active lives. Run `pubblue open <slug>` first.");
|
|
562
|
-
}
|
|
563
|
-
if (active.length === 1) return active[0];
|
|
564
|
-
failCli(`Multiple active lives: ${active.join(", ")}. Specify one with --slug.`);
|
|
565
|
-
}
|
|
566
|
-
function waitForDaemonReady({
|
|
567
|
-
child,
|
|
568
|
-
infoPath,
|
|
569
|
-
socketPath,
|
|
570
|
-
timeoutMs
|
|
571
|
-
}) {
|
|
572
|
-
return new Promise((resolve) => {
|
|
573
|
-
let settled = false;
|
|
574
|
-
let pollInFlight = false;
|
|
575
|
-
let lastIpcError = null;
|
|
576
|
-
const done = (result) => {
|
|
577
|
-
if (settled) return;
|
|
578
|
-
settled = true;
|
|
579
|
-
clearInterval(poll);
|
|
580
|
-
clearTimeout(timeout);
|
|
581
|
-
child.off("exit", onExit);
|
|
582
|
-
resolve(result);
|
|
583
|
-
};
|
|
584
|
-
const onExit = (code, signal) => {
|
|
585
|
-
const suffix = signal ? ` (signal ${signal})` : "";
|
|
586
|
-
done({ ok: false, reason: `daemon exited with code ${code ?? 0}${suffix}` });
|
|
587
|
-
};
|
|
588
|
-
child.on("exit", onExit);
|
|
589
|
-
const poll = setInterval(() => {
|
|
590
|
-
if (pollInFlight || !fs2.existsSync(infoPath)) return;
|
|
591
|
-
pollInFlight = true;
|
|
592
|
-
void ipcCall(socketPath, { method: "status", params: {} }).then((status) => {
|
|
593
|
-
if (status.ok) done({ ok: true });
|
|
594
|
-
}).catch((error) => {
|
|
595
|
-
lastIpcError = error instanceof Error ? error.message : String(error);
|
|
596
|
-
}).finally(() => {
|
|
597
|
-
pollInFlight = false;
|
|
598
|
-
});
|
|
599
|
-
}, 120);
|
|
600
|
-
const timeout = setTimeout(() => {
|
|
601
|
-
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`;
|
|
602
|
-
done({ ok: false, reason });
|
|
603
|
-
}, timeoutMs);
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
async function waitForAgentOffer(params) {
|
|
607
|
-
const startedAt = Date.now();
|
|
608
|
-
let lastError = null;
|
|
609
|
-
while (Date.now() - startedAt < params.timeoutMs) {
|
|
610
|
-
try {
|
|
611
|
-
const session = await params.apiClient.getLive(params.slug);
|
|
612
|
-
if (typeof session.agentOffer === "string" && session.agentOffer.length > 0) {
|
|
613
|
-
return { ok: true };
|
|
614
|
-
}
|
|
615
|
-
} catch (error) {
|
|
616
|
-
lastError = formatApiError(error);
|
|
617
|
-
}
|
|
618
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
619
|
-
}
|
|
620
|
-
return {
|
|
621
|
-
ok: false,
|
|
622
|
-
reason: lastError ? `agent offer was not published in time (last API error: ${lastError})` : "agent offer was not published in time"
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
async function ensureBridgeReady(params) {
|
|
626
|
-
if (params.bridgeMode === "none") {
|
|
627
|
-
return { ok: true };
|
|
628
|
-
}
|
|
629
|
-
const infoPath = bridgeInfoPath(params.slug);
|
|
630
|
-
if (isBridgeRunning(params.slug)) {
|
|
631
|
-
return waitForBridgeReady({
|
|
632
|
-
infoPath,
|
|
633
|
-
slug: params.slug,
|
|
634
|
-
timeoutMs: params.timeoutMs
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
const bridgeScript = path2.join(import.meta.dirname, "tunnel-bridge-entry.js");
|
|
638
|
-
const logPath = bridgeLogPath(params.slug);
|
|
639
|
-
const logFd = fs2.openSync(logPath, "a");
|
|
640
|
-
const child = fork(bridgeScript, [], {
|
|
641
|
-
detached: true,
|
|
642
|
-
stdio: buildBridgeForkStdio(logFd),
|
|
643
|
-
env: {
|
|
644
|
-
...params.bridgeProcessEnv,
|
|
645
|
-
PUBBLUE_BRIDGE_MODE: params.bridgeMode,
|
|
646
|
-
PUBBLUE_BRIDGE_SLUG: params.slug,
|
|
647
|
-
PUBBLUE_BRIDGE_SOCKET: params.socketPath,
|
|
648
|
-
PUBBLUE_BRIDGE_INFO: infoPath
|
|
649
|
-
}
|
|
650
|
-
});
|
|
651
|
-
fs2.closeSync(logFd);
|
|
652
|
-
if (child.connected) {
|
|
653
|
-
child.disconnect();
|
|
654
|
-
}
|
|
655
|
-
child.unref();
|
|
656
|
-
return waitForBridgeReady({
|
|
657
|
-
child,
|
|
658
|
-
infoPath,
|
|
659
|
-
slug: params.slug,
|
|
660
|
-
timeoutMs: params.timeoutMs
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
function waitForBridgeReady({
|
|
664
|
-
child,
|
|
665
|
-
infoPath,
|
|
666
|
-
slug,
|
|
667
|
-
timeoutMs
|
|
668
|
-
}) {
|
|
669
|
-
return new Promise((resolve) => {
|
|
670
|
-
let settled = false;
|
|
671
|
-
let lastState;
|
|
672
|
-
let lastError;
|
|
673
|
-
const done = (result) => {
|
|
674
|
-
if (settled) return;
|
|
675
|
-
settled = true;
|
|
676
|
-
clearInterval(poll);
|
|
677
|
-
clearTimeout(timeout);
|
|
678
|
-
if (child) {
|
|
679
|
-
child.off("exit", onExit);
|
|
680
|
-
}
|
|
681
|
-
resolve(result);
|
|
682
|
-
};
|
|
683
|
-
const onExit = (code, signal) => {
|
|
684
|
-
const suffix = signal ? ` (signal ${signal})` : "";
|
|
685
|
-
done({ ok: false, reason: `bridge exited with code ${code ?? 0}${suffix}` });
|
|
686
|
-
};
|
|
687
|
-
if (child) {
|
|
688
|
-
child.on("exit", onExit);
|
|
689
|
-
}
|
|
690
|
-
const poll = setInterval(() => {
|
|
691
|
-
if (!fs2.existsSync(infoPath)) return;
|
|
692
|
-
const info = readBridgeProcessInfo(slug);
|
|
693
|
-
if (!info) return;
|
|
694
|
-
lastState = info.status;
|
|
695
|
-
lastError = info.lastError;
|
|
696
|
-
if (info.status === "ready" && isBridgeRunning(slug)) {
|
|
697
|
-
done({ ok: true });
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
if (info.status === "error") {
|
|
701
|
-
done({
|
|
702
|
-
ok: false,
|
|
703
|
-
reason: info.lastError ? `bridge reported startup error: ${info.lastError}` : "bridge reported startup error"
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
}, 120);
|
|
707
|
-
const timeout = setTimeout(() => {
|
|
708
|
-
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"})`;
|
|
709
|
-
done({ ok: false, reason });
|
|
710
|
-
}, timeoutMs);
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
export {
|
|
715
|
-
failCli,
|
|
716
|
-
toCliFailure,
|
|
717
|
-
loadConfig,
|
|
718
|
-
saveConfig,
|
|
719
|
-
getConfig,
|
|
720
|
-
getTelegramMiniAppUrl,
|
|
721
|
-
PubApiError,
|
|
722
|
-
PubApiClient,
|
|
723
|
-
TEXT_FILE_EXTENSIONS,
|
|
724
|
-
getMimeType,
|
|
725
|
-
liveInfoPath,
|
|
726
|
-
liveLogPath,
|
|
727
|
-
bridgeInfoPath,
|
|
728
|
-
bridgeLogPath,
|
|
729
|
-
createApiClient,
|
|
730
|
-
buildBridgeProcessEnv,
|
|
731
|
-
ensureNodeDatachannelAvailable,
|
|
732
|
-
isDaemonRunning,
|
|
733
|
-
readDaemonProcessInfo,
|
|
734
|
-
readBridgeProcessInfo,
|
|
735
|
-
isBridgeRunning,
|
|
736
|
-
latestCliVersionPath,
|
|
737
|
-
readLatestCliVersion,
|
|
738
|
-
writeLatestCliVersion,
|
|
739
|
-
waitForProcessExit,
|
|
740
|
-
stopBridge,
|
|
741
|
-
stopOtherDaemons,
|
|
742
|
-
buildBridgeForkStdio,
|
|
743
|
-
getFollowReadDelayMs,
|
|
744
|
-
resolveSlugSelection,
|
|
745
|
-
buildDaemonForkStdio,
|
|
746
|
-
parsePositiveIntegerOption,
|
|
747
|
-
parseBridgeMode,
|
|
748
|
-
shouldRestartDaemonForCliUpgrade,
|
|
749
|
-
messageContainsPong,
|
|
750
|
-
getPublicUrl,
|
|
751
|
-
pickReusableLive,
|
|
752
|
-
readLogTail,
|
|
753
|
-
formatApiError,
|
|
754
|
-
cleanupLiveOnStartFailure,
|
|
755
|
-
resolveActiveSlug,
|
|
756
|
-
waitForDaemonReady,
|
|
757
|
-
waitForAgentOffer,
|
|
758
|
-
ensureBridgeReady
|
|
759
|
-
};
|