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
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
// src/lib/cli-error.ts
|
|
2
|
+
import { CommanderError } from "commander";
|
|
3
|
+
var CliError = class extends Error {
|
|
4
|
+
exitCode;
|
|
5
|
+
constructor(message, exitCode = 1) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "CliError";
|
|
8
|
+
this.exitCode = exitCode;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
function failCli(message, exitCode = 1) {
|
|
12
|
+
throw new CliError(message, exitCode);
|
|
13
|
+
}
|
|
14
|
+
function errorMessage(error) {
|
|
15
|
+
return error instanceof Error ? error.message : String(error);
|
|
16
|
+
}
|
|
17
|
+
function toCliFailure(error) {
|
|
18
|
+
if (error instanceof CommanderError) {
|
|
19
|
+
return {
|
|
20
|
+
exitCode: error.exitCode,
|
|
21
|
+
message: ""
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (error instanceof CliError) {
|
|
25
|
+
return {
|
|
26
|
+
exitCode: error.exitCode,
|
|
27
|
+
message: error.message
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
exitCode: 1,
|
|
32
|
+
message: errorMessage(error)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/lib/api.ts
|
|
37
|
+
var PubApiError = class extends Error {
|
|
38
|
+
constructor(message, status, retryAfterSeconds) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.status = status;
|
|
41
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
42
|
+
this.name = "PubApiError";
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var PubApiClient = class {
|
|
46
|
+
constructor(baseUrl, apiKey) {
|
|
47
|
+
this.baseUrl = baseUrl;
|
|
48
|
+
this.apiKey = apiKey;
|
|
49
|
+
}
|
|
50
|
+
async request(path2, options = {}) {
|
|
51
|
+
const url = new URL(path2, this.baseUrl);
|
|
52
|
+
const res = await fetch(url, {
|
|
53
|
+
...options,
|
|
54
|
+
headers: {
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
57
|
+
...options.headers
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
const retryAfterHeader = res.headers.get("Retry-After");
|
|
61
|
+
const parsedRetryAfterSeconds = typeof retryAfterHeader === "string" ? Number.parseInt(retryAfterHeader, 10) : void 0;
|
|
62
|
+
const retryAfterSeconds = parsedRetryAfterSeconds !== void 0 && Number.isFinite(parsedRetryAfterSeconds) ? parsedRetryAfterSeconds : void 0;
|
|
63
|
+
let data;
|
|
64
|
+
try {
|
|
65
|
+
data = await res.json();
|
|
66
|
+
} catch {
|
|
67
|
+
data = {};
|
|
68
|
+
}
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
if (res.status === 429) {
|
|
71
|
+
const retrySuffix = retryAfterSeconds !== void 0 ? ` Retry after ${retryAfterSeconds}s.` : "";
|
|
72
|
+
throw new PubApiError(`Rate limit exceeded.${retrySuffix}`, res.status, retryAfterSeconds);
|
|
73
|
+
}
|
|
74
|
+
throw new PubApiError(data.error || `Request failed with status ${res.status}`, res.status);
|
|
75
|
+
}
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
// -- Pub CRUD -------------------------------------------------------------
|
|
79
|
+
async create(opts) {
|
|
80
|
+
return this.request("/api/v1/pubs", {
|
|
81
|
+
method: "POST",
|
|
82
|
+
body: JSON.stringify(opts)
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async get(slug) {
|
|
86
|
+
const data = await this.request(`/api/v1/pubs/${encodeURIComponent(slug)}`);
|
|
87
|
+
return data.pub;
|
|
88
|
+
}
|
|
89
|
+
async listPage(cursor, limit) {
|
|
90
|
+
const params = new URLSearchParams();
|
|
91
|
+
if (cursor) params.set("cursor", cursor);
|
|
92
|
+
if (limit) params.set("limit", String(limit));
|
|
93
|
+
const qs = params.toString();
|
|
94
|
+
return this.request(`/api/v1/pubs${qs ? `?${qs}` : ""}`);
|
|
95
|
+
}
|
|
96
|
+
async list() {
|
|
97
|
+
const all = [];
|
|
98
|
+
let cursor;
|
|
99
|
+
do {
|
|
100
|
+
const result = await this.listPage(cursor, 100);
|
|
101
|
+
all.push(...result.pubs);
|
|
102
|
+
cursor = result.hasMore ? result.cursor : void 0;
|
|
103
|
+
} while (cursor);
|
|
104
|
+
return all;
|
|
105
|
+
}
|
|
106
|
+
async update(opts) {
|
|
107
|
+
const { slug, newSlug, ...rest } = opts;
|
|
108
|
+
const body = { ...rest };
|
|
109
|
+
if (newSlug) body.slug = newSlug;
|
|
110
|
+
return this.request(`/api/v1/pubs/${encodeURIComponent(slug)}`, {
|
|
111
|
+
method: "PATCH",
|
|
112
|
+
body: JSON.stringify(body)
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async deletePub(slug) {
|
|
116
|
+
await this.request(`/api/v1/pubs/${encodeURIComponent(slug)}`, {
|
|
117
|
+
method: "DELETE"
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// -- Agent presence -------------------------------------------------------
|
|
121
|
+
async goOnline() {
|
|
122
|
+
await this.request("/api/v1/agent/online", { method: "POST" });
|
|
123
|
+
}
|
|
124
|
+
async heartbeat() {
|
|
125
|
+
await this.request("/api/v1/agent/heartbeat", { method: "POST" });
|
|
126
|
+
}
|
|
127
|
+
async goOffline() {
|
|
128
|
+
await this.request("/api/v1/agent/offline", { method: "POST" });
|
|
129
|
+
}
|
|
130
|
+
// -- Agent live management ------------------------------------------------
|
|
131
|
+
async getPendingLive() {
|
|
132
|
+
const data = await this.request("/api/v1/agent/live");
|
|
133
|
+
return data.live;
|
|
134
|
+
}
|
|
135
|
+
async signalAnswer(opts) {
|
|
136
|
+
await this.request("/api/v1/agent/live/signal", {
|
|
137
|
+
method: "PATCH",
|
|
138
|
+
body: JSON.stringify(opts)
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
async closeActiveLive() {
|
|
142
|
+
await this.request("/api/v1/agent/live", { method: "DELETE" });
|
|
143
|
+
}
|
|
144
|
+
// -- Per-slug live info ---------------------------------------------------
|
|
145
|
+
async getLive(slug) {
|
|
146
|
+
const data = await this.request(
|
|
147
|
+
`/api/v1/pubs/${encodeURIComponent(slug)}/live`
|
|
148
|
+
);
|
|
149
|
+
return data.live;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// ../shared/bridge-protocol-core.ts
|
|
154
|
+
var CONTROL_CHANNEL = "_control";
|
|
155
|
+
var CHANNELS = {
|
|
156
|
+
CHAT: "chat",
|
|
157
|
+
CANVAS: "canvas",
|
|
158
|
+
AUDIO: "audio",
|
|
159
|
+
MEDIA: "media",
|
|
160
|
+
FILE: "file"
|
|
161
|
+
};
|
|
162
|
+
var idCounter = 0;
|
|
163
|
+
function generateMessageId() {
|
|
164
|
+
const ts = Date.now().toString(36);
|
|
165
|
+
const seq = (idCounter++).toString(36);
|
|
166
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
167
|
+
return `${ts}-${seq}-${rand}`;
|
|
168
|
+
}
|
|
169
|
+
function encodeMessage(msg) {
|
|
170
|
+
return JSON.stringify(msg);
|
|
171
|
+
}
|
|
172
|
+
function decodeMessage(raw) {
|
|
173
|
+
try {
|
|
174
|
+
const parsed = JSON.parse(raw);
|
|
175
|
+
if (parsed && typeof parsed.id === "string" && typeof parsed.type === "string") {
|
|
176
|
+
return parsed;
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function makeEventMessage(event, meta) {
|
|
184
|
+
return { id: generateMessageId(), type: "event", data: event, meta };
|
|
185
|
+
}
|
|
186
|
+
function makeAckMessage(messageId, channel) {
|
|
187
|
+
return makeEventMessage("ack", { messageId, channel, receivedAt: Date.now() });
|
|
188
|
+
}
|
|
189
|
+
function parseAckMessage(msg) {
|
|
190
|
+
if (msg.type !== "event" || msg.data !== "ack" || !msg.meta) return null;
|
|
191
|
+
const messageId = typeof msg.meta.messageId === "string" ? msg.meta.messageId : null;
|
|
192
|
+
const channel = typeof msg.meta.channel === "string" ? msg.meta.channel : null;
|
|
193
|
+
if (!messageId || !channel) return null;
|
|
194
|
+
const receivedAt = typeof msg.meta.receivedAt === "number" ? msg.meta.receivedAt : void 0;
|
|
195
|
+
return { messageId, channel, receivedAt };
|
|
196
|
+
}
|
|
197
|
+
function shouldAcknowledgeMessage(channel, msg) {
|
|
198
|
+
return channel !== CONTROL_CHANNEL && parseAckMessage(msg) === null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/commands/live-helpers.ts
|
|
202
|
+
import * as fs from "fs";
|
|
203
|
+
import { homedir } from "os";
|
|
204
|
+
import * as path from "path";
|
|
205
|
+
|
|
206
|
+
// src/lib/live-ipc.ts
|
|
207
|
+
import * as net from "net";
|
|
208
|
+
function getAgentSocketPath() {
|
|
209
|
+
return "/tmp/pubblue-agent.sock";
|
|
210
|
+
}
|
|
211
|
+
async function ipcCall(socketPath, request) {
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
let settled = false;
|
|
214
|
+
let timeoutId = null;
|
|
215
|
+
const finish = (fn) => {
|
|
216
|
+
if (settled) return;
|
|
217
|
+
settled = true;
|
|
218
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
219
|
+
fn();
|
|
220
|
+
};
|
|
221
|
+
const client = net.createConnection(socketPath, () => {
|
|
222
|
+
client.write(`${JSON.stringify(request)}
|
|
223
|
+
`);
|
|
224
|
+
});
|
|
225
|
+
let data = "";
|
|
226
|
+
client.on("data", (chunk) => {
|
|
227
|
+
data += chunk.toString();
|
|
228
|
+
const newlineIdx = data.indexOf("\n");
|
|
229
|
+
if (newlineIdx !== -1) {
|
|
230
|
+
const line = data.slice(0, newlineIdx);
|
|
231
|
+
client.end();
|
|
232
|
+
try {
|
|
233
|
+
finish(() => resolve(JSON.parse(line)));
|
|
234
|
+
} catch {
|
|
235
|
+
finish(() => reject(new Error("Invalid response from daemon")));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
client.on("error", (err) => {
|
|
240
|
+
if (err.code === "ECONNREFUSED" || err.code === "ENOENT") {
|
|
241
|
+
finish(() => reject(new Error("Daemon not running.")));
|
|
242
|
+
} else {
|
|
243
|
+
finish(() => reject(err));
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
client.on("end", () => {
|
|
247
|
+
if (!data.includes("\n")) {
|
|
248
|
+
finish(() => reject(new Error("Daemon closed connection unexpectedly")));
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
timeoutId = setTimeout(() => {
|
|
252
|
+
client.destroy();
|
|
253
|
+
finish(() => reject(new Error("Daemon request timed out")));
|
|
254
|
+
}, 1e4);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/commands/live-helpers.ts
|
|
259
|
+
var TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
260
|
+
".txt",
|
|
261
|
+
".md",
|
|
262
|
+
".markdown",
|
|
263
|
+
".json",
|
|
264
|
+
".csv",
|
|
265
|
+
".xml",
|
|
266
|
+
".yaml",
|
|
267
|
+
".yml",
|
|
268
|
+
".js",
|
|
269
|
+
".mjs",
|
|
270
|
+
".cjs",
|
|
271
|
+
".ts",
|
|
272
|
+
".tsx",
|
|
273
|
+
".jsx",
|
|
274
|
+
".css",
|
|
275
|
+
".scss",
|
|
276
|
+
".sass",
|
|
277
|
+
".less",
|
|
278
|
+
".log"
|
|
279
|
+
]);
|
|
280
|
+
var MIME_BY_EXT = {
|
|
281
|
+
".html": "text/html; charset=utf-8",
|
|
282
|
+
".htm": "text/html; charset=utf-8",
|
|
283
|
+
".txt": "text/plain; charset=utf-8",
|
|
284
|
+
".md": "text/markdown; charset=utf-8",
|
|
285
|
+
".markdown": "text/markdown; charset=utf-8",
|
|
286
|
+
".json": "application/json",
|
|
287
|
+
".csv": "text/csv; charset=utf-8",
|
|
288
|
+
".xml": "application/xml",
|
|
289
|
+
".yaml": "application/x-yaml",
|
|
290
|
+
".yml": "application/x-yaml",
|
|
291
|
+
".js": "text/javascript; charset=utf-8",
|
|
292
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
293
|
+
".cjs": "text/javascript; charset=utf-8",
|
|
294
|
+
".ts": "text/typescript; charset=utf-8",
|
|
295
|
+
".tsx": "text/typescript; charset=utf-8",
|
|
296
|
+
".jsx": "text/javascript; charset=utf-8",
|
|
297
|
+
".css": "text/css; charset=utf-8",
|
|
298
|
+
".scss": "text/x-scss; charset=utf-8",
|
|
299
|
+
".sass": "text/x-sass; charset=utf-8",
|
|
300
|
+
".less": "text/x-less; charset=utf-8",
|
|
301
|
+
".log": "text/plain; charset=utf-8",
|
|
302
|
+
".png": "image/png",
|
|
303
|
+
".jpg": "image/jpeg",
|
|
304
|
+
".jpeg": "image/jpeg",
|
|
305
|
+
".gif": "image/gif",
|
|
306
|
+
".webp": "image/webp",
|
|
307
|
+
".svg": "image/svg+xml",
|
|
308
|
+
".pdf": "application/pdf",
|
|
309
|
+
".zip": "application/zip",
|
|
310
|
+
".mp3": "audio/mpeg",
|
|
311
|
+
".wav": "audio/wav",
|
|
312
|
+
".mp4": "video/mp4"
|
|
313
|
+
};
|
|
314
|
+
function getMimeType(filePath) {
|
|
315
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
316
|
+
return MIME_BY_EXT[ext] || "application/octet-stream";
|
|
317
|
+
}
|
|
318
|
+
function liveInfoDir() {
|
|
319
|
+
const dir = path.join(homedir(), ".config", "pubblue", "lives");
|
|
320
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
321
|
+
return dir;
|
|
322
|
+
}
|
|
323
|
+
function liveInfoPath(slug) {
|
|
324
|
+
return path.join(liveInfoDir(), `${slug}.json`);
|
|
325
|
+
}
|
|
326
|
+
function liveLogPath(slug) {
|
|
327
|
+
return path.join(liveInfoDir(), `${slug}.log`);
|
|
328
|
+
}
|
|
329
|
+
function buildBridgeProcessEnv(bridgeConfig) {
|
|
330
|
+
const env = { ...process.env };
|
|
331
|
+
const setIfMissing = (key, value) => {
|
|
332
|
+
if (value === void 0) return;
|
|
333
|
+
const current = env[key];
|
|
334
|
+
if (typeof current === "string" && current.length > 0) return;
|
|
335
|
+
env[key] = String(value);
|
|
336
|
+
};
|
|
337
|
+
setIfMissing("PUBBLUE_PROJECT_ROOT", process.cwd());
|
|
338
|
+
setIfMissing("OPENCLAW_HOME", homedir());
|
|
339
|
+
if (!bridgeConfig) return env;
|
|
340
|
+
setIfMissing("OPENCLAW_PATH", bridgeConfig.openclawPath);
|
|
341
|
+
setIfMissing("OPENCLAW_SESSION_ID", bridgeConfig.sessionId);
|
|
342
|
+
setIfMissing("OPENCLAW_THREAD_ID", bridgeConfig.threadId);
|
|
343
|
+
setIfMissing("OPENCLAW_CANVAS_REMINDER_EVERY", bridgeConfig.canvasReminderEvery);
|
|
344
|
+
setIfMissing(
|
|
345
|
+
"OPENCLAW_DELIVER",
|
|
346
|
+
bridgeConfig.deliver === void 0 ? void 0 : bridgeConfig.deliver ? "1" : "0"
|
|
347
|
+
);
|
|
348
|
+
setIfMissing("OPENCLAW_DELIVER_CHANNEL", bridgeConfig.deliverChannel);
|
|
349
|
+
setIfMissing("OPENCLAW_REPLY_TO", bridgeConfig.replyTo);
|
|
350
|
+
setIfMissing("OPENCLAW_DELIVER_TIMEOUT_MS", bridgeConfig.deliverTimeoutMs);
|
|
351
|
+
setIfMissing("OPENCLAW_ATTACHMENT_DIR", bridgeConfig.attachmentDir);
|
|
352
|
+
setIfMissing("OPENCLAW_ATTACHMENT_MAX_BYTES", bridgeConfig.attachmentMaxBytes);
|
|
353
|
+
return env;
|
|
354
|
+
}
|
|
355
|
+
async function ensureNodeDatachannelAvailable() {
|
|
356
|
+
try {
|
|
357
|
+
await import("node-datachannel");
|
|
358
|
+
} catch (error) {
|
|
359
|
+
failCli(
|
|
360
|
+
[
|
|
361
|
+
"node-datachannel native module is not available.",
|
|
362
|
+
"Run `pnpm rebuild node-datachannel` in the cli package and retry.",
|
|
363
|
+
`Details: ${errorMessage(error)}`
|
|
364
|
+
].join("\n")
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function isDaemonRunning(slug) {
|
|
369
|
+
return readDaemonProcessInfo(slug) !== null;
|
|
370
|
+
}
|
|
371
|
+
function readDaemonProcessInfo(slug) {
|
|
372
|
+
const infoPath = liveInfoPath(slug);
|
|
373
|
+
try {
|
|
374
|
+
const info = JSON.parse(fs.readFileSync(infoPath, "utf-8"));
|
|
375
|
+
if (!Number.isFinite(info.pid)) throw new Error("invalid daemon pid");
|
|
376
|
+
if (!isProcessAlive(info.pid)) throw new Error("process not alive");
|
|
377
|
+
return info;
|
|
378
|
+
} catch {
|
|
379
|
+
try {
|
|
380
|
+
fs.unlinkSync(infoPath);
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function latestCliVersionPath() {
|
|
387
|
+
return path.join(liveInfoDir(), "cli-version.txt");
|
|
388
|
+
}
|
|
389
|
+
function readLatestCliVersion(versionPath) {
|
|
390
|
+
const resolved = versionPath || latestCliVersionPath();
|
|
391
|
+
try {
|
|
392
|
+
const value = fs.readFileSync(resolved, "utf-8").trim();
|
|
393
|
+
return value.length === 0 ? null : value;
|
|
394
|
+
} catch {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function writeLatestCliVersion(version, versionPath) {
|
|
399
|
+
const trimmed = version.trim();
|
|
400
|
+
if (trimmed.length === 0) return;
|
|
401
|
+
const resolved = versionPath || latestCliVersionPath();
|
|
402
|
+
const dir = path.dirname(resolved);
|
|
403
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
404
|
+
fs.writeFileSync(resolved, trimmed, "utf-8");
|
|
405
|
+
}
|
|
406
|
+
function isProcessAlive(pid) {
|
|
407
|
+
try {
|
|
408
|
+
process.kill(pid, 0);
|
|
409
|
+
return true;
|
|
410
|
+
} catch {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
415
|
+
const startedAt = Date.now();
|
|
416
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
417
|
+
if (!isProcessAlive(pid)) return true;
|
|
418
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
419
|
+
}
|
|
420
|
+
return !isProcessAlive(pid);
|
|
421
|
+
}
|
|
422
|
+
async function stopDaemonForLive(info) {
|
|
423
|
+
const pid = info.pid;
|
|
424
|
+
if (!isProcessAlive(pid)) return null;
|
|
425
|
+
const socketPath = info.socketPath;
|
|
426
|
+
if (socketPath) {
|
|
427
|
+
try {
|
|
428
|
+
await ipcCall(socketPath, { method: "close", params: {} });
|
|
429
|
+
} catch (error) {
|
|
430
|
+
try {
|
|
431
|
+
process.kill(pid, "SIGTERM");
|
|
432
|
+
} catch (killError) {
|
|
433
|
+
return `daemon ${pid}: IPC close failed (${errorMessage(error)}); SIGTERM failed (${errorMessage(killError)})`;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
try {
|
|
438
|
+
process.kill(pid, "SIGTERM");
|
|
439
|
+
} catch (error) {
|
|
440
|
+
return `daemon ${pid}: no socketPath and SIGTERM failed (${errorMessage(error)})`;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const stopped = await waitForProcessExit(pid, 8e3);
|
|
444
|
+
if (!stopped) return `daemon ${pid}: did not exit after stop request`;
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
async function stopOtherDaemons() {
|
|
448
|
+
const dir = liveInfoDir();
|
|
449
|
+
const entries = fs.readdirSync(dir).filter((name) => name.endsWith(".json"));
|
|
450
|
+
const failures = [];
|
|
451
|
+
for (const entry of entries) {
|
|
452
|
+
const slug = entry.replace(/\.json$/, "");
|
|
453
|
+
const info = readDaemonProcessInfo(slug);
|
|
454
|
+
if (!info) continue;
|
|
455
|
+
const daemonError = await stopDaemonForLive(info);
|
|
456
|
+
if (daemonError) failures.push(`[${slug}] ${daemonError}`);
|
|
457
|
+
}
|
|
458
|
+
if (failures.length > 0) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
[
|
|
461
|
+
"Critical: failed to stop previous live daemon processes.",
|
|
462
|
+
"Starting a new daemon now would leak resources and increase bandwidth usage.",
|
|
463
|
+
...failures
|
|
464
|
+
].join("\n")
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function getFollowReadDelayMs(disconnected, consecutiveFailures) {
|
|
469
|
+
if (!disconnected) return 1e3;
|
|
470
|
+
return Math.min(5e3, 1e3 * 2 ** Math.min(consecutiveFailures, 3));
|
|
471
|
+
}
|
|
472
|
+
function buildDaemonForkStdio(logFd) {
|
|
473
|
+
return ["ignore", logFd, logFd, "ipc"];
|
|
474
|
+
}
|
|
475
|
+
function parsePositiveIntegerOption(raw, optionName) {
|
|
476
|
+
const parsed = Number.parseInt(raw, 10);
|
|
477
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
478
|
+
throw new Error(`${optionName} must be a positive integer. Received: ${raw}`);
|
|
479
|
+
}
|
|
480
|
+
return parsed;
|
|
481
|
+
}
|
|
482
|
+
function parseBridgeMode(raw) {
|
|
483
|
+
const normalized = raw.trim().toLowerCase();
|
|
484
|
+
if (normalized === "openclaw" || normalized === "none") {
|
|
485
|
+
return normalized;
|
|
486
|
+
}
|
|
487
|
+
throw new Error(`--bridge must be one of: openclaw, none. Received: ${raw}`);
|
|
488
|
+
}
|
|
489
|
+
function resolveBridgeMode(opts) {
|
|
490
|
+
return parseBridgeMode(opts.bridge || (opts.foreground ? "none" : "openclaw"));
|
|
491
|
+
}
|
|
492
|
+
function messageContainsPong(payload) {
|
|
493
|
+
if (!payload || typeof payload !== "object") return false;
|
|
494
|
+
const message = payload.msg;
|
|
495
|
+
if (!message || typeof message !== "object") return false;
|
|
496
|
+
const type = message.type;
|
|
497
|
+
const data = message.data;
|
|
498
|
+
return type === "text" && typeof data === "string" && data.trim().toLowerCase() === "pong";
|
|
499
|
+
}
|
|
500
|
+
function readLogTail(logPath, maxChars = 4e3) {
|
|
501
|
+
try {
|
|
502
|
+
const content = fs.readFileSync(logPath, "utf-8");
|
|
503
|
+
if (content.length <= maxChars) return content;
|
|
504
|
+
return content.slice(-maxChars);
|
|
505
|
+
} catch {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function formatApiError(error) {
|
|
510
|
+
if (error instanceof PubApiError) {
|
|
511
|
+
if (error.status === 429 && error.retryAfterSeconds !== void 0) {
|
|
512
|
+
return `Rate limit exceeded. Retry after ${error.retryAfterSeconds}s.`;
|
|
513
|
+
}
|
|
514
|
+
return `${error.message} (HTTP ${error.status})`;
|
|
515
|
+
}
|
|
516
|
+
return errorMessage(error);
|
|
517
|
+
}
|
|
518
|
+
async function resolveActiveSlug() {
|
|
519
|
+
const socketPath = getAgentSocketPath();
|
|
520
|
+
let response;
|
|
521
|
+
try {
|
|
522
|
+
response = await ipcCall(socketPath, { method: "active-slug", params: {} });
|
|
523
|
+
} catch {
|
|
524
|
+
failCli("No active daemon. Run `pubblue start` first.");
|
|
525
|
+
}
|
|
526
|
+
if (response.ok && typeof response.slug === "string" && response.slug.length > 0) {
|
|
527
|
+
return response.slug;
|
|
528
|
+
}
|
|
529
|
+
failCli("Daemon is running but no live is active. Wait for browser to initiate live.");
|
|
530
|
+
}
|
|
531
|
+
function waitForDaemonReady({
|
|
532
|
+
child,
|
|
533
|
+
infoPath,
|
|
534
|
+
socketPath,
|
|
535
|
+
timeoutMs
|
|
536
|
+
}) {
|
|
537
|
+
return new Promise((resolve) => {
|
|
538
|
+
let settled = false;
|
|
539
|
+
let pollInFlight = false;
|
|
540
|
+
let lastIpcError = null;
|
|
541
|
+
const done = (result) => {
|
|
542
|
+
if (settled) return;
|
|
543
|
+
settled = true;
|
|
544
|
+
clearInterval(poll);
|
|
545
|
+
clearTimeout(timeout);
|
|
546
|
+
child.off("exit", onExit);
|
|
547
|
+
resolve(result);
|
|
548
|
+
};
|
|
549
|
+
const onExit = (code, signal) => {
|
|
550
|
+
const suffix = signal ? ` (signal ${signal})` : "";
|
|
551
|
+
done({ ok: false, reason: `daemon exited with code ${code ?? 0}${suffix}` });
|
|
552
|
+
};
|
|
553
|
+
child.on("exit", onExit);
|
|
554
|
+
const poll = setInterval(() => {
|
|
555
|
+
if (pollInFlight || !fs.existsSync(infoPath)) return;
|
|
556
|
+
pollInFlight = true;
|
|
557
|
+
void ipcCall(socketPath, { method: "status", params: {} }).then((status) => {
|
|
558
|
+
if (status.ok) done({ ok: true });
|
|
559
|
+
}).catch((error) => {
|
|
560
|
+
lastIpcError = errorMessage(error);
|
|
561
|
+
}).finally(() => {
|
|
562
|
+
pollInFlight = false;
|
|
563
|
+
});
|
|
564
|
+
}, 120);
|
|
565
|
+
const timeout = setTimeout(() => {
|
|
566
|
+
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`;
|
|
567
|
+
done({ ok: false, reason });
|
|
568
|
+
}, timeoutMs);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export {
|
|
573
|
+
failCli,
|
|
574
|
+
errorMessage,
|
|
575
|
+
toCliFailure,
|
|
576
|
+
PubApiError,
|
|
577
|
+
PubApiClient,
|
|
578
|
+
CONTROL_CHANNEL,
|
|
579
|
+
CHANNELS,
|
|
580
|
+
generateMessageId,
|
|
581
|
+
encodeMessage,
|
|
582
|
+
decodeMessage,
|
|
583
|
+
makeAckMessage,
|
|
584
|
+
parseAckMessage,
|
|
585
|
+
shouldAcknowledgeMessage,
|
|
586
|
+
getAgentSocketPath,
|
|
587
|
+
ipcCall,
|
|
588
|
+
TEXT_FILE_EXTENSIONS,
|
|
589
|
+
getMimeType,
|
|
590
|
+
liveInfoPath,
|
|
591
|
+
liveLogPath,
|
|
592
|
+
buildBridgeProcessEnv,
|
|
593
|
+
ensureNodeDatachannelAvailable,
|
|
594
|
+
isDaemonRunning,
|
|
595
|
+
latestCliVersionPath,
|
|
596
|
+
readLatestCliVersion,
|
|
597
|
+
writeLatestCliVersion,
|
|
598
|
+
stopOtherDaemons,
|
|
599
|
+
getFollowReadDelayMs,
|
|
600
|
+
buildDaemonForkStdio,
|
|
601
|
+
parsePositiveIntegerOption,
|
|
602
|
+
resolveBridgeMode,
|
|
603
|
+
messageContainsPong,
|
|
604
|
+
readLogTail,
|
|
605
|
+
formatApiError,
|
|
606
|
+
resolveActiveSlug,
|
|
607
|
+
waitForDaemonReady
|
|
608
|
+
};
|