pubblue 0.4.12 → 0.6.1
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-BBJOOZHS.js +676 -0
- package/dist/chunk-WXNNDR4T.js +1313 -0
- package/dist/index.js +457 -1265
- package/dist/tunnel-daemon-BR5XKNEA.js +7 -0
- package/dist/tunnel-daemon-entry.js +14 -12
- package/package.json +4 -4
- package/dist/chunk-4YTJ2WKF.js +0 -60
- package/dist/chunk-7NFHPJ76.js +0 -79
- package/dist/chunk-HJ5LTUHS.js +0 -56
- package/dist/chunk-UW7JILRJ.js +0 -677
- package/dist/tunnel-bridge-entry.d.ts +0 -2
- package/dist/tunnel-bridge-entry.js +0 -703
- package/dist/tunnel-daemon-RKWEA5BV.js +0 -14
package/dist/index.js
CHANGED
|
@@ -1,55 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
getSocketPath,
|
|
4
|
-
ipcCall
|
|
5
|
-
} from "./chunk-HJ5LTUHS.js";
|
|
6
|
-
import {
|
|
7
|
-
TunnelApiClient,
|
|
8
|
-
TunnelApiError
|
|
9
|
-
} from "./chunk-7NFHPJ76.js";
|
|
10
2
|
import {
|
|
11
3
|
CHANNELS,
|
|
12
4
|
CONTROL_CHANNEL,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
exitCode: 1,
|
|
45
|
-
message: error.message
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
return {
|
|
49
|
-
exitCode: 1,
|
|
50
|
-
message: String(error)
|
|
51
|
-
};
|
|
52
|
-
}
|
|
5
|
+
PubApiClient,
|
|
6
|
+
TEXT_FILE_EXTENSIONS,
|
|
7
|
+
buildBridgeProcessEnv,
|
|
8
|
+
buildDaemonForkStdio,
|
|
9
|
+
createApiClient,
|
|
10
|
+
ensureNodeDatachannelAvailable,
|
|
11
|
+
failCli,
|
|
12
|
+
formatApiError,
|
|
13
|
+
generateMessageId,
|
|
14
|
+
getAgentSocketPath,
|
|
15
|
+
getConfig,
|
|
16
|
+
getFollowReadDelayMs,
|
|
17
|
+
getMimeType,
|
|
18
|
+
getTelegramMiniAppUrl,
|
|
19
|
+
ipcCall,
|
|
20
|
+
isDaemonRunning,
|
|
21
|
+
liveInfoPath,
|
|
22
|
+
liveLogPath,
|
|
23
|
+
loadConfig,
|
|
24
|
+
messageContainsPong,
|
|
25
|
+
parsePositiveIntegerOption,
|
|
26
|
+
readLogTail,
|
|
27
|
+
resolveActiveSlug,
|
|
28
|
+
resolveBridgeMode,
|
|
29
|
+
saveConfig,
|
|
30
|
+
stopOtherDaemons,
|
|
31
|
+
toCliFailure,
|
|
32
|
+
waitForDaemonReady,
|
|
33
|
+
writeLatestCliVersion
|
|
34
|
+
} from "./chunk-BBJOOZHS.js";
|
|
53
35
|
|
|
54
36
|
// src/program.ts
|
|
55
37
|
import { Command } from "commander";
|
|
@@ -57,136 +39,9 @@ import { Command } from "commander";
|
|
|
57
39
|
// src/commands/configure.ts
|
|
58
40
|
import { createInterface } from "readline/promises";
|
|
59
41
|
|
|
60
|
-
// src/
|
|
42
|
+
// src/commands/shared.ts
|
|
61
43
|
import * as fs from "fs";
|
|
62
|
-
import * as os from "os";
|
|
63
44
|
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
|
-
function getTelegramMiniAppUrl(type, id) {
|
|
115
|
-
const saved = loadConfig();
|
|
116
|
-
if (!saved?.telegram?.botUsername) return null;
|
|
117
|
-
return `https://t.me/${saved.telegram.botUsername}?startapp=${type === "pub" ? "p" : "t"}_${id}`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// src/commands/shared.ts
|
|
121
|
-
import * as fs2 from "fs";
|
|
122
|
-
import * as path2 from "path";
|
|
123
|
-
|
|
124
|
-
// src/lib/api.ts
|
|
125
|
-
var PubApiClient = class {
|
|
126
|
-
constructor(baseUrl, apiKey) {
|
|
127
|
-
this.baseUrl = baseUrl;
|
|
128
|
-
this.apiKey = apiKey;
|
|
129
|
-
}
|
|
130
|
-
async request(path6, options = {}) {
|
|
131
|
-
const url = new URL(path6, this.baseUrl);
|
|
132
|
-
const res = await fetch(url, {
|
|
133
|
-
...options,
|
|
134
|
-
headers: {
|
|
135
|
-
"Content-Type": "application/json",
|
|
136
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
137
|
-
...options.headers
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
const data = await res.json();
|
|
141
|
-
if (!res.ok) {
|
|
142
|
-
throw new Error(data.error || `Request failed with status ${res.status}`);
|
|
143
|
-
}
|
|
144
|
-
return data;
|
|
145
|
-
}
|
|
146
|
-
async create(opts) {
|
|
147
|
-
return this.request("/api/v1/publications", {
|
|
148
|
-
method: "POST",
|
|
149
|
-
body: JSON.stringify(opts)
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
async get(slug) {
|
|
153
|
-
const data = await this.request(`/api/v1/publications/${encodeURIComponent(slug)}`);
|
|
154
|
-
return data.publication;
|
|
155
|
-
}
|
|
156
|
-
async listPage(cursor, limit) {
|
|
157
|
-
const params = new URLSearchParams();
|
|
158
|
-
if (cursor) params.set("cursor", cursor);
|
|
159
|
-
if (limit) params.set("limit", String(limit));
|
|
160
|
-
const qs = params.toString();
|
|
161
|
-
return this.request(`/api/v1/publications${qs ? `?${qs}` : ""}`);
|
|
162
|
-
}
|
|
163
|
-
async list() {
|
|
164
|
-
const all = [];
|
|
165
|
-
let cursor;
|
|
166
|
-
do {
|
|
167
|
-
const result = await this.listPage(cursor, 100);
|
|
168
|
-
all.push(...result.publications);
|
|
169
|
-
cursor = result.hasMore ? result.cursor : void 0;
|
|
170
|
-
} while (cursor);
|
|
171
|
-
return all;
|
|
172
|
-
}
|
|
173
|
-
async update(opts) {
|
|
174
|
-
const { slug, newSlug, ...rest } = opts;
|
|
175
|
-
const body = { ...rest };
|
|
176
|
-
if (newSlug) body.slug = newSlug;
|
|
177
|
-
return this.request(`/api/v1/publications/${encodeURIComponent(slug)}`, {
|
|
178
|
-
method: "PATCH",
|
|
179
|
-
body: JSON.stringify(body)
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
async remove(slug) {
|
|
183
|
-
await this.request(`/api/v1/publications/${encodeURIComponent(slug)}`, {
|
|
184
|
-
method: "DELETE"
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
// src/commands/shared.ts
|
|
190
45
|
function createClient() {
|
|
191
46
|
const config = getConfig();
|
|
192
47
|
return new PubApiClient(config.baseUrl, config.apiKey);
|
|
@@ -210,13 +65,13 @@ function resolveVisibilityFlags(opts) {
|
|
|
210
65
|
return void 0;
|
|
211
66
|
}
|
|
212
67
|
function readFile(filePath) {
|
|
213
|
-
const resolved =
|
|
214
|
-
if (!
|
|
68
|
+
const resolved = path.resolve(filePath);
|
|
69
|
+
if (!fs.existsSync(resolved)) {
|
|
215
70
|
failCli(`File not found: ${resolved}`);
|
|
216
71
|
}
|
|
217
72
|
return {
|
|
218
|
-
content:
|
|
219
|
-
basename:
|
|
73
|
+
content: fs.readFileSync(resolved, "utf-8"),
|
|
74
|
+
basename: path.basename(resolved)
|
|
220
75
|
};
|
|
221
76
|
}
|
|
222
77
|
|
|
@@ -271,11 +126,6 @@ function parseBooleanValue(raw, key) {
|
|
|
271
126
|
return false;
|
|
272
127
|
throw new Error(`Invalid boolean value for ${key}: ${raw}`);
|
|
273
128
|
}
|
|
274
|
-
function parseBridgeModeValue(raw) {
|
|
275
|
-
const normalized = raw.trim().toLowerCase();
|
|
276
|
-
if (normalized === "openclaw" || normalized === "none") return normalized;
|
|
277
|
-
throw new Error(`Invalid bridge mode: ${raw}. Use openclaw or none.`);
|
|
278
|
-
}
|
|
279
129
|
function parsePositiveInteger(raw, key) {
|
|
280
130
|
const parsed = Number.parseInt(raw, 10);
|
|
281
131
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
@@ -284,7 +134,6 @@ function parsePositiveInteger(raw, key) {
|
|
|
284
134
|
return parsed;
|
|
285
135
|
}
|
|
286
136
|
var SUPPORTED_KEYS = [
|
|
287
|
-
"bridge.mode",
|
|
288
137
|
"openclaw.path",
|
|
289
138
|
"openclaw.sessionId",
|
|
290
139
|
"openclaw.threadId",
|
|
@@ -299,9 +148,6 @@ var SUPPORTED_KEYS = [
|
|
|
299
148
|
];
|
|
300
149
|
function applyConfigSet(bridge, telegram, key, value) {
|
|
301
150
|
switch (key) {
|
|
302
|
-
case "bridge.mode":
|
|
303
|
-
bridge.mode = parseBridgeModeValue(value);
|
|
304
|
-
return;
|
|
305
151
|
case "openclaw.path":
|
|
306
152
|
bridge.openclawPath = value;
|
|
307
153
|
return;
|
|
@@ -347,9 +193,6 @@ function applyConfigSet(bridge, telegram, key, value) {
|
|
|
347
193
|
}
|
|
348
194
|
function applyConfigUnset(bridge, telegram, key) {
|
|
349
195
|
switch (key) {
|
|
350
|
-
case "bridge.mode":
|
|
351
|
-
delete bridge.mode;
|
|
352
|
-
return;
|
|
353
196
|
case "openclaw.path":
|
|
354
197
|
delete bridge.openclawPath;
|
|
355
198
|
return;
|
|
@@ -407,13 +250,11 @@ async function telegramGetMe(token) {
|
|
|
407
250
|
hasMainWebApp: data.result.has_main_web_app === true
|
|
408
251
|
};
|
|
409
252
|
}
|
|
410
|
-
async function telegramSetMenuButton(token,
|
|
253
|
+
async function telegramSetMenuButton(token, button) {
|
|
411
254
|
const resp = await fetch(`https://api.telegram.org/bot${token}/setChatMenuButton`, {
|
|
412
255
|
method: "POST",
|
|
413
256
|
headers: { "Content-Type": "application/json" },
|
|
414
|
-
body: JSON.stringify({
|
|
415
|
-
menu_button: { type: "web_app", text: "Open", web_app: { url } }
|
|
416
|
-
})
|
|
257
|
+
body: JSON.stringify({ menu_button: button })
|
|
417
258
|
});
|
|
418
259
|
const data = await resp.json();
|
|
419
260
|
if (!data.ok) {
|
|
@@ -428,7 +269,6 @@ function printConfigSummary(saved) {
|
|
|
428
269
|
console.log("Saved config:");
|
|
429
270
|
console.log(` apiKey: ${maskSecret(saved.apiKey)}`);
|
|
430
271
|
if (saved.bridge && hasValues(saved.bridge)) {
|
|
431
|
-
console.log(` bridge.mode: ${saved.bridge.mode ?? "(unset)"}`);
|
|
432
272
|
if (saved.bridge.openclawPath) console.log(` openclaw.path: ${saved.bridge.openclawPath}`);
|
|
433
273
|
if (saved.bridge.sessionId) console.log(` openclaw.sessionId: ${saved.bridge.sessionId}`);
|
|
434
274
|
if (saved.bridge.threadId) console.log(` openclaw.threadId: ${saved.bridge.threadId}`);
|
|
@@ -503,6 +343,16 @@ function registerConfigureCommand(program2) {
|
|
|
503
343
|
if (key === "telegram.botToken") telegramTokenChanged = true;
|
|
504
344
|
}
|
|
505
345
|
for (const key of opts.unset) {
|
|
346
|
+
if (key.trim() === "telegram.botToken" && nextTelegram.botToken) {
|
|
347
|
+
try {
|
|
348
|
+
await telegramSetMenuButton(nextTelegram.botToken, { type: "default" });
|
|
349
|
+
console.log("Telegram menu button reset to default.");
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.error(
|
|
352
|
+
`Warning: failed to reset Telegram menu button: ${error instanceof Error ? error.message : String(error)}`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
506
356
|
applyConfigUnset(nextBridge, nextTelegram, key.trim());
|
|
507
357
|
}
|
|
508
358
|
if (telegramTokenChanged && nextTelegram.botToken) {
|
|
@@ -511,7 +361,11 @@ function registerConfigureCommand(program2) {
|
|
|
511
361
|
nextTelegram.botUsername = bot.username;
|
|
512
362
|
nextTelegram.hasMainWebApp = bot.hasMainWebApp;
|
|
513
363
|
console.log(` Bot: @${bot.username}`);
|
|
514
|
-
await telegramSetMenuButton(nextTelegram.botToken,
|
|
364
|
+
await telegramSetMenuButton(nextTelegram.botToken, {
|
|
365
|
+
type: "web_app",
|
|
366
|
+
text: "Open",
|
|
367
|
+
web_app: { url: "https://pub.blue" }
|
|
368
|
+
});
|
|
515
369
|
console.log(" Menu button set to https://pub.blue");
|
|
516
370
|
if (!bot.hasMainWebApp) {
|
|
517
371
|
console.log("");
|
|
@@ -534,525 +388,316 @@ function registerConfigureCommand(program2) {
|
|
|
534
388
|
);
|
|
535
389
|
}
|
|
536
390
|
|
|
537
|
-
// src/commands/
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
391
|
+
// src/commands/live.ts
|
|
392
|
+
import * as fs2 from "fs";
|
|
393
|
+
import * as path2 from "path";
|
|
394
|
+
|
|
395
|
+
// package.json
|
|
396
|
+
var package_default = {
|
|
397
|
+
name: "pubblue",
|
|
398
|
+
version: "0.6.1",
|
|
399
|
+
description: "CLI tool for publishing content and running interactive sessions via pub.blue",
|
|
400
|
+
type: "module",
|
|
401
|
+
bin: {
|
|
402
|
+
pubblue: "./dist/index.js"
|
|
403
|
+
},
|
|
404
|
+
scripts: {
|
|
405
|
+
build: "tsup src/index.ts src/tunnel-daemon-entry.ts --format esm --dts --clean",
|
|
406
|
+
dev: "tsup src/index.ts src/tunnel-daemon-entry.ts --format esm --watch",
|
|
407
|
+
test: "vitest run",
|
|
408
|
+
"test:watch": "vitest",
|
|
409
|
+
lint: "tsc --noEmit"
|
|
410
|
+
},
|
|
411
|
+
dependencies: {
|
|
412
|
+
commander: "^13.0.0",
|
|
413
|
+
"node-datachannel": "^0.32.0"
|
|
414
|
+
},
|
|
415
|
+
devDependencies: {
|
|
416
|
+
"@types/node": "22.10.2",
|
|
417
|
+
tsup: "^8.3.6",
|
|
418
|
+
typescript: "^5.7.2",
|
|
419
|
+
vitest: "^3.0.0"
|
|
420
|
+
},
|
|
421
|
+
files: [
|
|
422
|
+
"dist"
|
|
423
|
+
],
|
|
424
|
+
repository: {
|
|
425
|
+
type: "git",
|
|
426
|
+
url: "git+https://github.com/xmanatee/pub.git",
|
|
427
|
+
directory: "cli"
|
|
428
|
+
},
|
|
429
|
+
publishConfig: {
|
|
430
|
+
access: "public"
|
|
431
|
+
},
|
|
432
|
+
pnpm: {
|
|
433
|
+
onlyBuiltDependencies: [
|
|
434
|
+
"esbuild",
|
|
435
|
+
"node-datachannel"
|
|
436
|
+
]
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// src/lib/version.ts
|
|
441
|
+
var version = package_default.version;
|
|
442
|
+
if (typeof version !== "string" || version.length === 0) {
|
|
443
|
+
throw new Error("Invalid CLI version in package.json");
|
|
444
|
+
}
|
|
445
|
+
var CLI_VERSION = version;
|
|
446
|
+
|
|
447
|
+
// src/commands/live.ts
|
|
448
|
+
function registerLiveCommands(program2) {
|
|
449
|
+
registerStartCommand(program2);
|
|
450
|
+
registerStopCommand(program2);
|
|
451
|
+
registerStatusCommand(program2);
|
|
452
|
+
registerWriteCommand(program2);
|
|
453
|
+
registerReadCommand(program2);
|
|
454
|
+
registerChannelsCommand(program2);
|
|
455
|
+
registerDoctorCommand(program2);
|
|
456
|
+
}
|
|
457
|
+
function registerStartCommand(program2) {
|
|
458
|
+
program2.command("start").description("Start the agent daemon (registers presence, awaits live requests)").requiredOption("--agent-name <name>", "Agent display name shown to the browser user").option("--bridge <mode>", "Bridge mode: openclaw|none").option("--foreground", "Run in foreground (don't fork)").action(async (opts) => {
|
|
459
|
+
await ensureNodeDatachannelAvailable();
|
|
460
|
+
writeLatestCliVersion(CLI_VERSION);
|
|
461
|
+
const runtimeConfig = getConfig();
|
|
462
|
+
const apiClient = createApiClient(runtimeConfig);
|
|
463
|
+
const bridgeMode = resolveBridgeMode(opts);
|
|
464
|
+
const bridgeProcessEnv = buildBridgeProcessEnv(runtimeConfig.bridge);
|
|
465
|
+
const socketPath = getAgentSocketPath();
|
|
466
|
+
const infoPath = liveInfoPath("agent");
|
|
467
|
+
const logPath = liveLogPath("agent");
|
|
468
|
+
await stopOtherDaemons();
|
|
469
|
+
if (opts.foreground) {
|
|
470
|
+
const { startDaemon } = await import("./tunnel-daemon-BR5XKNEA.js");
|
|
471
|
+
console.log("Agent daemon starting in foreground...");
|
|
472
|
+
console.log("Press Ctrl+C to stop.");
|
|
473
|
+
try {
|
|
474
|
+
await startDaemon({
|
|
475
|
+
cliVersion: CLI_VERSION,
|
|
476
|
+
apiClient,
|
|
477
|
+
socketPath,
|
|
478
|
+
infoPath,
|
|
479
|
+
bridgeMode,
|
|
480
|
+
agentName: opts.agentName
|
|
481
|
+
});
|
|
482
|
+
} catch (error) {
|
|
483
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
484
|
+
failCli(`Daemon failed: ${message}`);
|
|
569
485
|
}
|
|
570
|
-
}
|
|
571
|
-
);
|
|
572
|
-
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) => {
|
|
573
|
-
const client = createClient();
|
|
574
|
-
const pub = await client.get(slug);
|
|
575
|
-
if (opts.content) {
|
|
576
|
-
process.stdout.write(pub.content);
|
|
577
486
|
return;
|
|
578
487
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
const file = readFile(opts.file);
|
|
595
|
-
content = file.content;
|
|
596
|
-
filename = file.basename;
|
|
488
|
+
const { fork } = await import("child_process");
|
|
489
|
+
const daemonScript = path2.join(import.meta.dirname, "tunnel-daemon-entry.js");
|
|
490
|
+
const daemonLogFd = fs2.openSync(logPath, "a");
|
|
491
|
+
const child = fork(daemonScript, [], {
|
|
492
|
+
detached: true,
|
|
493
|
+
stdio: buildDaemonForkStdio(daemonLogFd),
|
|
494
|
+
env: {
|
|
495
|
+
...bridgeProcessEnv,
|
|
496
|
+
PUBBLUE_DAEMON_BASE_URL: runtimeConfig.baseUrl,
|
|
497
|
+
PUBBLUE_DAEMON_API_KEY: runtimeConfig.apiKey,
|
|
498
|
+
PUBBLUE_DAEMON_SOCKET: socketPath,
|
|
499
|
+
PUBBLUE_DAEMON_INFO: infoPath,
|
|
500
|
+
PUBBLUE_DAEMON_AGENT_NAME: opts.agentName,
|
|
501
|
+
PUBBLUE_CLI_VERSION: CLI_VERSION,
|
|
502
|
+
PUBBLUE_DAEMON_BRIDGE_MODE: bridgeMode
|
|
597
503
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
});
|
|
603
|
-
const result = await client.update({
|
|
604
|
-
slug,
|
|
605
|
-
content,
|
|
606
|
-
filename,
|
|
607
|
-
title: opts.title,
|
|
608
|
-
isPublic,
|
|
609
|
-
newSlug: opts.slug
|
|
610
|
-
});
|
|
611
|
-
console.log(`Updated: ${result.slug}`);
|
|
612
|
-
if (result.title) console.log(` Title: ${result.title}`);
|
|
613
|
-
console.log(` Status: ${formatVisibility(result.isPublic)}`);
|
|
614
|
-
}
|
|
615
|
-
);
|
|
616
|
-
program2.command("list").description("List your publications").action(async () => {
|
|
617
|
-
const client = createClient();
|
|
618
|
-
const pubs = await client.list();
|
|
619
|
-
if (pubs.length === 0) {
|
|
620
|
-
console.log("No publications.");
|
|
621
|
-
return;
|
|
504
|
+
});
|
|
505
|
+
fs2.closeSync(daemonLogFd);
|
|
506
|
+
if (child.connected) {
|
|
507
|
+
child.disconnect();
|
|
622
508
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
509
|
+
child.unref();
|
|
510
|
+
console.log("Starting agent daemon...");
|
|
511
|
+
const ready = await waitForDaemonReady({
|
|
512
|
+
child,
|
|
513
|
+
infoPath,
|
|
514
|
+
socketPath,
|
|
515
|
+
timeoutMs: 8e3
|
|
516
|
+
});
|
|
517
|
+
if (!ready.ok) {
|
|
518
|
+
const lines = [
|
|
519
|
+
`Daemon failed to start: ${ready.reason ?? "unknown reason"}`,
|
|
520
|
+
`Daemon log: ${logPath}`
|
|
521
|
+
];
|
|
522
|
+
const tail = readLogTail(logPath);
|
|
523
|
+
if (tail) {
|
|
524
|
+
lines.push("---- daemon log tail ----");
|
|
525
|
+
lines.push(tail.trimEnd());
|
|
526
|
+
lines.push("---- end daemon log tail ----");
|
|
527
|
+
}
|
|
528
|
+
failCli(lines.join("\n"));
|
|
629
529
|
}
|
|
530
|
+
console.log("Agent daemon started. Waiting for browser to initiate live.");
|
|
531
|
+
console.log(`Daemon log: ${logPath}`);
|
|
532
|
+
console.log(`Bridge mode: ${bridgeMode}`);
|
|
630
533
|
});
|
|
631
|
-
program2.command("delete").description("Delete a publication").argument("<slug>", "Slug of the publication to delete").action(async (slug) => {
|
|
632
|
-
const client = createClient();
|
|
633
|
-
await client.remove(slug);
|
|
634
|
-
console.log(`Deleted: ${slug}`);
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// src/commands/tunnel/management-commands.ts
|
|
639
|
-
import * as fs4 from "fs";
|
|
640
|
-
|
|
641
|
-
// src/commands/tunnel-helpers.ts
|
|
642
|
-
import { fork } from "child_process";
|
|
643
|
-
import * as fs3 from "fs";
|
|
644
|
-
import * as path3 from "path";
|
|
645
|
-
var TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
646
|
-
".txt",
|
|
647
|
-
".md",
|
|
648
|
-
".markdown",
|
|
649
|
-
".json",
|
|
650
|
-
".csv",
|
|
651
|
-
".xml",
|
|
652
|
-
".yaml",
|
|
653
|
-
".yml",
|
|
654
|
-
".js",
|
|
655
|
-
".mjs",
|
|
656
|
-
".cjs",
|
|
657
|
-
".ts",
|
|
658
|
-
".tsx",
|
|
659
|
-
".jsx",
|
|
660
|
-
".css",
|
|
661
|
-
".scss",
|
|
662
|
-
".sass",
|
|
663
|
-
".less",
|
|
664
|
-
".log"
|
|
665
|
-
]);
|
|
666
|
-
function getMimeType(filePath) {
|
|
667
|
-
const ext = path3.extname(filePath).toLowerCase();
|
|
668
|
-
const mimeByExt = {
|
|
669
|
-
".html": "text/html; charset=utf-8",
|
|
670
|
-
".htm": "text/html; charset=utf-8",
|
|
671
|
-
".txt": "text/plain; charset=utf-8",
|
|
672
|
-
".md": "text/markdown; charset=utf-8",
|
|
673
|
-
".markdown": "text/markdown; charset=utf-8",
|
|
674
|
-
".json": "application/json",
|
|
675
|
-
".csv": "text/csv; charset=utf-8",
|
|
676
|
-
".xml": "application/xml",
|
|
677
|
-
".yaml": "application/x-yaml",
|
|
678
|
-
".yml": "application/x-yaml",
|
|
679
|
-
".png": "image/png",
|
|
680
|
-
".jpg": "image/jpeg",
|
|
681
|
-
".jpeg": "image/jpeg",
|
|
682
|
-
".gif": "image/gif",
|
|
683
|
-
".webp": "image/webp",
|
|
684
|
-
".svg": "image/svg+xml",
|
|
685
|
-
".pdf": "application/pdf",
|
|
686
|
-
".zip": "application/zip",
|
|
687
|
-
".mp3": "audio/mpeg",
|
|
688
|
-
".wav": "audio/wav",
|
|
689
|
-
".mp4": "video/mp4"
|
|
690
|
-
};
|
|
691
|
-
return mimeByExt[ext] || "application/octet-stream";
|
|
692
|
-
}
|
|
693
|
-
function tunnelInfoDir() {
|
|
694
|
-
const dir = path3.join(
|
|
695
|
-
process.env.HOME || process.env.USERPROFILE || "/tmp",
|
|
696
|
-
".config",
|
|
697
|
-
"pubblue",
|
|
698
|
-
"tunnels"
|
|
699
|
-
);
|
|
700
|
-
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
701
|
-
return dir;
|
|
702
|
-
}
|
|
703
|
-
function tunnelInfoPath(tunnelId) {
|
|
704
|
-
return path3.join(tunnelInfoDir(), `${tunnelId}.json`);
|
|
705
|
-
}
|
|
706
|
-
function tunnelLogPath(tunnelId) {
|
|
707
|
-
return path3.join(tunnelInfoDir(), `${tunnelId}.log`);
|
|
708
|
-
}
|
|
709
|
-
function bridgeInfoPath(tunnelId) {
|
|
710
|
-
return path3.join(tunnelInfoDir(), `${tunnelId}.bridge.json`);
|
|
711
534
|
}
|
|
712
|
-
function
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
return new TunnelApiClient(config.baseUrl, config.apiKey);
|
|
718
|
-
}
|
|
719
|
-
function buildBridgeProcessEnv(bridgeConfig) {
|
|
720
|
-
const env = { ...process.env };
|
|
721
|
-
if (!bridgeConfig) return env;
|
|
722
|
-
const setIfMissing = (key, value) => {
|
|
723
|
-
if (value === void 0 || value === null) return;
|
|
724
|
-
const current = env[key];
|
|
725
|
-
if (typeof current === "string" && current.length > 0) return;
|
|
726
|
-
env[key] = String(value);
|
|
727
|
-
};
|
|
728
|
-
setIfMissing("OPENCLAW_PATH", bridgeConfig.openclawPath);
|
|
729
|
-
setIfMissing("OPENCLAW_SESSION_ID", bridgeConfig.sessionId);
|
|
730
|
-
setIfMissing("OPENCLAW_THREAD_ID", bridgeConfig.threadId);
|
|
731
|
-
if (bridgeConfig.canvasReminderEvery !== void 0) {
|
|
732
|
-
setIfMissing("OPENCLAW_CANVAS_REMINDER_EVERY", bridgeConfig.canvasReminderEvery);
|
|
733
|
-
}
|
|
734
|
-
if (bridgeConfig.deliver !== void 0) {
|
|
735
|
-
setIfMissing("OPENCLAW_DELIVER", bridgeConfig.deliver ? "1" : "0");
|
|
736
|
-
}
|
|
737
|
-
setIfMissing("OPENCLAW_DELIVER_CHANNEL", bridgeConfig.deliverChannel);
|
|
738
|
-
setIfMissing("OPENCLAW_REPLY_TO", bridgeConfig.replyTo);
|
|
739
|
-
if (bridgeConfig.deliverTimeoutMs !== void 0) {
|
|
740
|
-
setIfMissing("OPENCLAW_DELIVER_TIMEOUT_MS", bridgeConfig.deliverTimeoutMs);
|
|
741
|
-
}
|
|
742
|
-
setIfMissing("OPENCLAW_ATTACHMENT_DIR", bridgeConfig.attachmentDir);
|
|
743
|
-
if (bridgeConfig.attachmentMaxBytes !== void 0) {
|
|
744
|
-
setIfMissing("OPENCLAW_ATTACHMENT_MAX_BYTES", bridgeConfig.attachmentMaxBytes);
|
|
745
|
-
}
|
|
746
|
-
return env;
|
|
747
|
-
}
|
|
748
|
-
async function ensureNodeDatachannelAvailable() {
|
|
749
|
-
try {
|
|
750
|
-
await import("node-datachannel");
|
|
751
|
-
} catch (error) {
|
|
752
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
753
|
-
failCli(
|
|
754
|
-
[
|
|
755
|
-
"node-datachannel native module is not available.",
|
|
756
|
-
"Run `pnpm rebuild node-datachannel` in the cli package and retry.",
|
|
757
|
-
`Details: ${message}`
|
|
758
|
-
].join("\n")
|
|
759
|
-
);
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
function isDaemonRunning(tunnelId) {
|
|
763
|
-
return readDaemonProcessInfo(tunnelId) !== null;
|
|
764
|
-
}
|
|
765
|
-
function readDaemonProcessInfo(tunnelId) {
|
|
766
|
-
const infoPath = tunnelInfoPath(tunnelId);
|
|
767
|
-
if (!fs3.existsSync(infoPath)) return null;
|
|
768
|
-
try {
|
|
769
|
-
const info = JSON.parse(fs3.readFileSync(infoPath, "utf-8"));
|
|
770
|
-
if (!Number.isFinite(info.pid)) throw new Error("invalid daemon pid");
|
|
771
|
-
process.kill(info.pid, 0);
|
|
772
|
-
return info;
|
|
773
|
-
} catch {
|
|
774
|
-
try {
|
|
775
|
-
fs3.unlinkSync(infoPath);
|
|
776
|
-
} catch {
|
|
535
|
+
function registerStopCommand(program2) {
|
|
536
|
+
program2.command("stop").description("Stop the agent daemon (deregisters presence, closes active live)").action(async () => {
|
|
537
|
+
if (!isDaemonRunning("agent")) {
|
|
538
|
+
console.log("Agent daemon is not running.");
|
|
539
|
+
return;
|
|
777
540
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
}
|
|
781
|
-
function readBridgeProcessInfo(tunnelId) {
|
|
782
|
-
const infoPath = bridgeInfoPath(tunnelId);
|
|
783
|
-
if (!fs3.existsSync(infoPath)) return null;
|
|
784
|
-
try {
|
|
785
|
-
return JSON.parse(fs3.readFileSync(infoPath, "utf-8"));
|
|
786
|
-
} catch {
|
|
787
|
-
return null;
|
|
788
|
-
}
|
|
541
|
+
await stopOtherDaemons();
|
|
542
|
+
console.log("Agent daemon stopped.");
|
|
543
|
+
});
|
|
789
544
|
}
|
|
790
|
-
function
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
const info = JSON.parse(fs3.readFileSync(infoPath, "utf-8"));
|
|
795
|
-
process.kill(info.pid, 0);
|
|
796
|
-
return true;
|
|
797
|
-
} catch {
|
|
545
|
+
function registerStatusCommand(program2) {
|
|
546
|
+
program2.command("status").description("Check agent daemon and live connection status").action(async () => {
|
|
547
|
+
const socketPath = getAgentSocketPath();
|
|
548
|
+
let response;
|
|
798
549
|
try {
|
|
799
|
-
|
|
550
|
+
response = await ipcCall(socketPath, { method: "status", params: {} });
|
|
800
551
|
} catch {
|
|
552
|
+
console.log("Agent daemon is not running.");
|
|
553
|
+
return;
|
|
801
554
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
|
|
555
|
+
const activeSlug = response.activeSlug;
|
|
556
|
+
console.log(` Daemon: running`);
|
|
557
|
+
console.log(` Active slug: ${activeSlug || "(none)"}`);
|
|
558
|
+
console.log(` Status: ${response.connected ? "connected" : "waiting"}`);
|
|
559
|
+
console.log(` Uptime: ${response.uptime}s`);
|
|
560
|
+
const chNames = Array.isArray(response.channels) ? response.channels.map((c) => typeof c === "string" ? c : String(c)) : [];
|
|
561
|
+
console.log(` Channels: ${chNames.join(", ") || "(none)"}`);
|
|
562
|
+
console.log(` Buffered: ${response.bufferedMessages ?? 0} messages`);
|
|
563
|
+
if (typeof response.lastError === "string" && response.lastError.length > 0) {
|
|
564
|
+
console.log(` Last error: ${response.lastError}`);
|
|
565
|
+
}
|
|
566
|
+
const logPath = liveLogPath("agent");
|
|
567
|
+
if (fs2.existsSync(logPath)) {
|
|
568
|
+
console.log(` Log: ${logPath}`);
|
|
569
|
+
}
|
|
570
|
+
const bridge = response.bridge;
|
|
571
|
+
if (bridge) {
|
|
572
|
+
console.log(` Bridge: openclaw (${bridge.running ? "running" : "stopped"})`);
|
|
573
|
+
if (bridge.sessionId) {
|
|
574
|
+
console.log(` Bridge session: ${bridge.sessionId}`);
|
|
575
|
+
}
|
|
576
|
+
if (bridge.sessionSource) {
|
|
577
|
+
console.log(` Bridge session source: ${bridge.sessionSource}`);
|
|
578
|
+
}
|
|
579
|
+
if (bridge.sessionKey) {
|
|
580
|
+
console.log(` Bridge session key: ${bridge.sessionKey}`);
|
|
581
|
+
}
|
|
582
|
+
if (bridge.forwardedMessages !== void 0) {
|
|
583
|
+
console.log(` Bridge forwarded: ${bridge.forwardedMessages} messages`);
|
|
584
|
+
}
|
|
585
|
+
if (bridge.lastError) {
|
|
586
|
+
console.log(` Bridge last error: ${bridge.lastError}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
});
|
|
825
590
|
}
|
|
826
|
-
function
|
|
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
|
-
function pickReusableTunnel(tunnels, nowMs = Date.now()) {
|
|
857
|
-
const active = tunnels.filter((t) => t.status === "active" && t.expiresAt > nowMs).sort((a, b) => b.createdAt - a.createdAt);
|
|
858
|
-
return active[0] ?? null;
|
|
859
|
-
}
|
|
860
|
-
function readLogTail(logPath, maxChars = 4e3) {
|
|
861
|
-
if (!fs3.existsSync(logPath)) return null;
|
|
862
|
-
try {
|
|
863
|
-
const content = fs3.readFileSync(logPath, "utf-8");
|
|
864
|
-
if (content.length <= maxChars) return content;
|
|
865
|
-
return content.slice(-maxChars);
|
|
866
|
-
} catch {
|
|
867
|
-
return null;
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
function formatApiError(error) {
|
|
871
|
-
if (error instanceof TunnelApiError) {
|
|
872
|
-
if (error.status === 429 && error.retryAfterSeconds !== void 0) {
|
|
873
|
-
return `Rate limit exceeded. Retry after ${error.retryAfterSeconds}s.`;
|
|
874
|
-
}
|
|
875
|
-
return `${error.message} (HTTP ${error.status})`;
|
|
876
|
-
}
|
|
877
|
-
return error instanceof Error ? error.message : String(error);
|
|
878
|
-
}
|
|
879
|
-
async function cleanupCreatedTunnelOnStartFailure(apiClient, target) {
|
|
880
|
-
if (!target.createdNew) return;
|
|
881
|
-
try {
|
|
882
|
-
await apiClient.close(target.tunnelId);
|
|
883
|
-
} catch (closeError) {
|
|
884
|
-
console.error(
|
|
885
|
-
`Failed to clean up newly created tunnel ${target.tunnelId}: ${formatApiError(closeError)}`
|
|
886
|
-
);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
async function resolveActiveTunnel() {
|
|
890
|
-
const dir = tunnelInfoDir();
|
|
891
|
-
const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".json") && !f.endsWith(".bridge.json"));
|
|
892
|
-
const active = [];
|
|
893
|
-
for (const f of files) {
|
|
894
|
-
const tunnelId = f.replace(".json", "");
|
|
895
|
-
if (isDaemonRunning(tunnelId)) active.push(tunnelId);
|
|
896
|
-
}
|
|
897
|
-
if (active.length === 0) {
|
|
898
|
-
failCli("No active tunnels. Run `pubblue tunnel start` first.");
|
|
899
|
-
}
|
|
900
|
-
if (active.length === 1) return active[0];
|
|
901
|
-
failCli(`Multiple active tunnels: ${active.join(", ")}. Specify one.`);
|
|
902
|
-
}
|
|
903
|
-
function waitForDaemonReady({
|
|
904
|
-
child,
|
|
905
|
-
infoPath,
|
|
906
|
-
socketPath,
|
|
907
|
-
timeoutMs
|
|
908
|
-
}) {
|
|
909
|
-
return new Promise((resolve3) => {
|
|
910
|
-
let settled = false;
|
|
911
|
-
let pollInFlight = false;
|
|
912
|
-
let lastIpcError = null;
|
|
913
|
-
const done = (result) => {
|
|
914
|
-
if (settled) return;
|
|
915
|
-
settled = true;
|
|
916
|
-
clearInterval(poll);
|
|
917
|
-
clearTimeout(timeout);
|
|
918
|
-
child.off("exit", onExit);
|
|
919
|
-
resolve3(result);
|
|
920
|
-
};
|
|
921
|
-
const onExit = (code, signal) => {
|
|
922
|
-
const suffix = signal ? ` (signal ${signal})` : "";
|
|
923
|
-
done({ ok: false, reason: `daemon exited with code ${code ?? 0}${suffix}` });
|
|
924
|
-
};
|
|
925
|
-
child.on("exit", onExit);
|
|
926
|
-
const poll = setInterval(() => {
|
|
927
|
-
if (pollInFlight || !fs3.existsSync(infoPath)) return;
|
|
928
|
-
pollInFlight = true;
|
|
929
|
-
void ipcCall(socketPath, { method: "status", params: {} }).then((status) => {
|
|
930
|
-
if (status.ok) done({ ok: true });
|
|
931
|
-
}).catch((error) => {
|
|
932
|
-
lastIpcError = error instanceof Error ? error.message : String(error);
|
|
933
|
-
}).finally(() => {
|
|
934
|
-
pollInFlight = false;
|
|
935
|
-
});
|
|
936
|
-
}, 120);
|
|
937
|
-
const timeout = setTimeout(() => {
|
|
938
|
-
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`;
|
|
939
|
-
done({ ok: false, reason });
|
|
940
|
-
}, timeoutMs);
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
|
-
async function waitForAgentOffer(params) {
|
|
944
|
-
const startedAt = Date.now();
|
|
945
|
-
let lastError = null;
|
|
946
|
-
while (Date.now() - startedAt < params.timeoutMs) {
|
|
947
|
-
try {
|
|
948
|
-
const tunnel = await params.apiClient.get(params.tunnelId);
|
|
949
|
-
if (typeof tunnel.agentOffer === "string" && tunnel.agentOffer.length > 0) {
|
|
950
|
-
return { ok: true };
|
|
591
|
+
function registerWriteCommand(program2) {
|
|
592
|
+
program2.command("write").description("Write data to a live channel").argument("[message]", "Text message (or use --file)").option("-c, --channel <channel>", "Channel name", "chat").option("-f, --file <file>", "Read content from file").action(async (messageArg, opts) => {
|
|
593
|
+
let msg;
|
|
594
|
+
let binaryBase64;
|
|
595
|
+
if (opts.file) {
|
|
596
|
+
const filePath = path2.resolve(opts.file);
|
|
597
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
598
|
+
const bytes = fs2.readFileSync(filePath);
|
|
599
|
+
const filename = path2.basename(filePath);
|
|
600
|
+
if (ext === ".html" || ext === ".htm") {
|
|
601
|
+
msg = {
|
|
602
|
+
id: generateMessageId(),
|
|
603
|
+
type: "html",
|
|
604
|
+
data: bytes.toString("utf-8"),
|
|
605
|
+
meta: { title: filename, filename, mime: getMimeType(filePath), size: bytes.length }
|
|
606
|
+
};
|
|
607
|
+
} else if (TEXT_FILE_EXTENSIONS.has(ext)) {
|
|
608
|
+
msg = {
|
|
609
|
+
id: generateMessageId(),
|
|
610
|
+
type: "text",
|
|
611
|
+
data: bytes.toString("utf-8"),
|
|
612
|
+
meta: { filename, mime: getMimeType(filePath), size: bytes.length }
|
|
613
|
+
};
|
|
614
|
+
} else {
|
|
615
|
+
msg = {
|
|
616
|
+
id: generateMessageId(),
|
|
617
|
+
type: "binary",
|
|
618
|
+
meta: { filename, mime: getMimeType(filePath), size: bytes.length }
|
|
619
|
+
};
|
|
620
|
+
binaryBase64 = bytes.toString("base64");
|
|
951
621
|
}
|
|
952
|
-
}
|
|
953
|
-
|
|
622
|
+
} else if (messageArg) {
|
|
623
|
+
msg = {
|
|
624
|
+
id: generateMessageId(),
|
|
625
|
+
type: "text",
|
|
626
|
+
data: messageArg
|
|
627
|
+
};
|
|
628
|
+
} else {
|
|
629
|
+
const chunks = [];
|
|
630
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
631
|
+
msg = {
|
|
632
|
+
id: generateMessageId(),
|
|
633
|
+
type: "text",
|
|
634
|
+
data: Buffer.concat(chunks).toString("utf-8").trim()
|
|
635
|
+
};
|
|
954
636
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
reason: lastError ? `agent offer was not published in time (last API error: ${lastError})` : "agent offer was not published in time"
|
|
960
|
-
};
|
|
961
|
-
}
|
|
962
|
-
async function ensureBridgeReady(params) {
|
|
963
|
-
if (params.bridgeMode === "none") {
|
|
964
|
-
return { ok: true };
|
|
965
|
-
}
|
|
966
|
-
const infoPath = bridgeInfoPath(params.tunnelId);
|
|
967
|
-
if (isBridgeRunning(params.tunnelId)) {
|
|
968
|
-
return waitForBridgeReady({
|
|
969
|
-
infoPath,
|
|
970
|
-
tunnelId: params.tunnelId,
|
|
971
|
-
timeoutMs: params.timeoutMs
|
|
637
|
+
const socketPath = getAgentSocketPath();
|
|
638
|
+
const response = await ipcCall(socketPath, {
|
|
639
|
+
method: "write",
|
|
640
|
+
params: { channel: opts.channel, msg, binaryBase64 }
|
|
972
641
|
});
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
const logPath = bridgeLogPath(params.tunnelId);
|
|
976
|
-
const logFd = fs3.openSync(logPath, "a");
|
|
977
|
-
const child = fork(bridgeScript, [], {
|
|
978
|
-
detached: true,
|
|
979
|
-
stdio: buildBridgeForkStdio(logFd),
|
|
980
|
-
env: {
|
|
981
|
-
...params.bridgeProcessEnv,
|
|
982
|
-
PUBBLUE_BRIDGE_MODE: params.bridgeMode,
|
|
983
|
-
PUBBLUE_BRIDGE_TUNNEL_ID: params.tunnelId,
|
|
984
|
-
PUBBLUE_BRIDGE_SOCKET: params.socketPath,
|
|
985
|
-
PUBBLUE_BRIDGE_INFO: infoPath
|
|
642
|
+
if (!response.ok) {
|
|
643
|
+
failCli(`Failed: ${response.error}`);
|
|
986
644
|
}
|
|
987
645
|
});
|
|
988
|
-
fs3.closeSync(logFd);
|
|
989
|
-
if (child.connected) {
|
|
990
|
-
child.disconnect();
|
|
991
|
-
}
|
|
992
|
-
child.unref();
|
|
993
|
-
return waitForBridgeReady({
|
|
994
|
-
child,
|
|
995
|
-
infoPath,
|
|
996
|
-
tunnelId: params.tunnelId,
|
|
997
|
-
timeoutMs: params.timeoutMs
|
|
998
|
-
});
|
|
999
646
|
}
|
|
1000
|
-
function
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
let lastError;
|
|
1010
|
-
const done = (result) => {
|
|
1011
|
-
if (settled) return;
|
|
1012
|
-
settled = true;
|
|
1013
|
-
clearInterval(poll);
|
|
1014
|
-
clearTimeout(timeout);
|
|
1015
|
-
if (child) {
|
|
1016
|
-
child.off("exit", onExit);
|
|
647
|
+
function registerReadCommand(program2) {
|
|
648
|
+
program2.command("read").description("Read buffered messages from live channels").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(async (opts) => {
|
|
649
|
+
const socketPath = getAgentSocketPath();
|
|
650
|
+
const readChannel = opts.channel || (opts.follow && !opts.all ? CHANNELS.CHAT : void 0);
|
|
651
|
+
if (opts.follow) {
|
|
652
|
+
if (!opts.channel && !opts.all) {
|
|
653
|
+
console.error(
|
|
654
|
+
"Following chat channel by default. Use `--all` to include binary/file channels."
|
|
655
|
+
);
|
|
1017
656
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
657
|
+
let consecutiveFailures = 0;
|
|
658
|
+
let warnedDisconnected = false;
|
|
659
|
+
while (true) {
|
|
660
|
+
try {
|
|
661
|
+
const response = await ipcCall(socketPath, {
|
|
662
|
+
method: "read",
|
|
663
|
+
params: { channel: readChannel }
|
|
664
|
+
});
|
|
665
|
+
if (warnedDisconnected) {
|
|
666
|
+
console.error("Daemon reconnected.");
|
|
667
|
+
warnedDisconnected = false;
|
|
668
|
+
}
|
|
669
|
+
consecutiveFailures = 0;
|
|
670
|
+
if (response.messages && response.messages.length > 0) {
|
|
671
|
+
for (const m of response.messages) {
|
|
672
|
+
console.log(JSON.stringify(m));
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
} catch (error) {
|
|
676
|
+
consecutiveFailures += 1;
|
|
677
|
+
if (!warnedDisconnected) {
|
|
678
|
+
const detail = error instanceof Error ? ` ${error.message}` : "";
|
|
679
|
+
console.error(`Daemon disconnected. Waiting for recovery...${detail}`);
|
|
680
|
+
warnedDisconnected = true;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const delayMs = getFollowReadDelayMs(warnedDisconnected, consecutiveFailures);
|
|
684
|
+
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
|
1036
685
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
686
|
+
} else {
|
|
687
|
+
const response = await ipcCall(socketPath, {
|
|
688
|
+
method: "read",
|
|
689
|
+
params: { channel: readChannel }
|
|
690
|
+
});
|
|
691
|
+
if (!response.ok) {
|
|
692
|
+
failCli(`Failed: ${response.error}`);
|
|
1042
693
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
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"})`;
|
|
1046
|
-
done({ ok: false, reason });
|
|
1047
|
-
}, timeoutMs);
|
|
694
|
+
console.log(JSON.stringify(response.messages || [], null, 2));
|
|
695
|
+
}
|
|
1048
696
|
});
|
|
1049
697
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
tunnel.command("channels").description("List active channels").argument("[tunnelId]", "Tunnel ID").option("-t, --tunnel <tunnelId>", "Tunnel ID (alternative to positional arg)").action(async (tunnelIdArg, opts) => {
|
|
1054
|
-
const tunnelId = resolveTunnelIdSelection(tunnelIdArg, opts.tunnel) || await resolveActiveTunnel();
|
|
1055
|
-
const socketPath = getSocketPath(tunnelId);
|
|
698
|
+
function registerChannelsCommand(program2) {
|
|
699
|
+
program2.command("channels").description("List active live channels").action(async () => {
|
|
700
|
+
const socketPath = getAgentSocketPath();
|
|
1056
701
|
const response = await ipcCall(socketPath, { method: "channels", params: {} });
|
|
1057
702
|
if (response.channels) {
|
|
1058
703
|
for (const ch of response.channels) {
|
|
@@ -1060,54 +705,17 @@ function registerTunnelManagementCommands(tunnel) {
|
|
|
1060
705
|
}
|
|
1061
706
|
}
|
|
1062
707
|
});
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
const response = await ipcCall(socketPath, { method: "status", params: {} });
|
|
1067
|
-
console.log(` Status: ${response.connected ? "connected" : "waiting"}`);
|
|
1068
|
-
console.log(` Uptime: ${response.uptime}s`);
|
|
1069
|
-
const chNames = Array.isArray(response.channels) ? response.channels.map((c) => typeof c === "string" ? c : String(c)) : [];
|
|
1070
|
-
console.log(` Channels: ${chNames.join(", ")}`);
|
|
1071
|
-
console.log(` Buffered: ${response.bufferedMessages ?? 0} messages`);
|
|
1072
|
-
if (typeof response.lastError === "string" && response.lastError.length > 0) {
|
|
1073
|
-
console.log(` Last error: ${response.lastError}`);
|
|
1074
|
-
}
|
|
1075
|
-
const logPath = tunnelLogPath(tunnelId);
|
|
1076
|
-
if (fs4.existsSync(logPath)) {
|
|
1077
|
-
console.log(` Log: ${logPath}`);
|
|
1078
|
-
}
|
|
1079
|
-
const bridgeInfo = readBridgeProcessInfo(tunnelId);
|
|
1080
|
-
if (bridgeInfo) {
|
|
1081
|
-
const bridgeRunning = isBridgeRunning(tunnelId);
|
|
1082
|
-
const bridgeState = bridgeInfo.status || (bridgeRunning ? "running" : "stopped");
|
|
1083
|
-
console.log(` Bridge: ${bridgeInfo.mode} (${bridgeState})`);
|
|
1084
|
-
if (bridgeInfo.sessionId) {
|
|
1085
|
-
console.log(` Bridge session: ${bridgeInfo.sessionId}`);
|
|
1086
|
-
}
|
|
1087
|
-
if (bridgeInfo.sessionSource) {
|
|
1088
|
-
console.log(` Bridge session source: ${bridgeInfo.sessionSource}`);
|
|
1089
|
-
}
|
|
1090
|
-
if (bridgeInfo.sessionKey) {
|
|
1091
|
-
console.log(` Bridge session key: ${bridgeInfo.sessionKey}`);
|
|
1092
|
-
}
|
|
1093
|
-
if (bridgeInfo.lastError) {
|
|
1094
|
-
console.log(` Bridge last error: ${bridgeInfo.lastError}`);
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
const bridgeLog = bridgeLogPath(tunnelId);
|
|
1098
|
-
if (fs4.existsSync(bridgeLog)) {
|
|
1099
|
-
console.log(` Bridge log: ${bridgeLog}`);
|
|
1100
|
-
}
|
|
1101
|
-
});
|
|
1102
|
-
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(
|
|
708
|
+
}
|
|
709
|
+
function registerDoctorCommand(program2) {
|
|
710
|
+
program2.command("doctor").description("Run end-to-end live checks (daemon, channels, chat/canvas ping)").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(
|
|
1103
711
|
async (opts) => {
|
|
1104
712
|
const timeoutSeconds = parsePositiveIntegerOption(opts.timeout, "--timeout");
|
|
1105
713
|
const timeoutMs = timeoutSeconds * 1e3;
|
|
1106
|
-
const
|
|
1107
|
-
const
|
|
714
|
+
const socketPath = getAgentSocketPath();
|
|
715
|
+
const slug = await resolveActiveSlug();
|
|
1108
716
|
const apiClient = createApiClient();
|
|
1109
717
|
const fail = (message) => failCli(`Doctor failed: ${message}`);
|
|
1110
|
-
console.log(`Doctor
|
|
718
|
+
console.log(`Doctor: ${slug}`);
|
|
1111
719
|
let statusResponse = null;
|
|
1112
720
|
try {
|
|
1113
721
|
statusResponse = await ipcCall(socketPath, {
|
|
@@ -1136,23 +744,25 @@ function registerTunnelManagementCommands(tunnel) {
|
|
|
1136
744
|
}
|
|
1137
745
|
}
|
|
1138
746
|
console.log("Daemon/channel check: OK");
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
747
|
+
const live = await (async () => {
|
|
748
|
+
try {
|
|
749
|
+
return await apiClient.getLive(slug);
|
|
750
|
+
} catch (error) {
|
|
751
|
+
fail(`failed to fetch live info from API: ${formatApiError(error)}`);
|
|
752
|
+
}
|
|
753
|
+
throw new Error("unreachable");
|
|
754
|
+
})();
|
|
755
|
+
if (live.status !== "active") {
|
|
756
|
+
fail(`API reports live is not active (status: ${live.status})`);
|
|
1147
757
|
}
|
|
1148
|
-
if (
|
|
1149
|
-
fail("API reports
|
|
758
|
+
if (live.expiresAt <= Date.now()) {
|
|
759
|
+
fail("API reports live is expired.");
|
|
1150
760
|
}
|
|
1151
|
-
if (
|
|
1152
|
-
fail("
|
|
761
|
+
if (typeof live.browserOffer !== "string" || live.browserOffer.length === 0) {
|
|
762
|
+
fail("browser offer was not published.");
|
|
1153
763
|
}
|
|
1154
|
-
if (typeof
|
|
1155
|
-
fail("agent
|
|
764
|
+
if (typeof live.agentAnswer !== "string" || live.agentAnswer.length === 0) {
|
|
765
|
+
fail("agent answer was not published.");
|
|
1156
766
|
}
|
|
1157
767
|
console.log("API/signaling check: OK");
|
|
1158
768
|
if (!opts.skipChat) {
|
|
@@ -1214,545 +824,127 @@ function registerTunnelManagementCommands(tunnel) {
|
|
|
1214
824
|
}
|
|
1215
825
|
console.log("Canvas ping write ACK: OK");
|
|
1216
826
|
}
|
|
1217
|
-
console.log("
|
|
827
|
+
console.log("Doctor: PASS");
|
|
1218
828
|
}
|
|
1219
829
|
);
|
|
1220
|
-
tunnel.command("list").description("List active tunnels").action(async () => {
|
|
1221
|
-
const apiClient = createApiClient();
|
|
1222
|
-
const tunnels = await apiClient.list();
|
|
1223
|
-
if (tunnels.length === 0) {
|
|
1224
|
-
console.log("No active tunnels.");
|
|
1225
|
-
return;
|
|
1226
|
-
}
|
|
1227
|
-
for (const t of tunnels) {
|
|
1228
|
-
const age = Math.floor((Date.now() - t.createdAt) / 6e4);
|
|
1229
|
-
const running = isDaemonRunning(t.tunnelId) ? "running" : "no daemon";
|
|
1230
|
-
const bridgeInfo = readBridgeProcessInfo(t.tunnelId);
|
|
1231
|
-
const bridge = bridgeInfo ? isBridgeRunning(t.tunnelId) ? `${bridgeInfo.mode}:running` : `${bridgeInfo.mode}:stopped` : "none";
|
|
1232
|
-
const conn = t.hasConnection ? "connected" : "waiting";
|
|
1233
|
-
console.log(` ${t.tunnelId} ${conn} ${running} bridge=${bridge} ${age}m ago`);
|
|
1234
|
-
}
|
|
1235
|
-
});
|
|
1236
|
-
tunnel.command("close").description("Close a tunnel and stop its daemon").argument("<tunnelId>", "Tunnel ID").action(async (tunnelId) => {
|
|
1237
|
-
stopBridgeProcess(tunnelId);
|
|
1238
|
-
try {
|
|
1239
|
-
fs4.unlinkSync(bridgeInfoPath(tunnelId));
|
|
1240
|
-
} catch {
|
|
1241
|
-
}
|
|
1242
|
-
const socketPath = getSocketPath(tunnelId);
|
|
1243
|
-
try {
|
|
1244
|
-
await ipcCall(socketPath, { method: "close", params: {} });
|
|
1245
|
-
} catch {
|
|
1246
|
-
}
|
|
1247
|
-
const apiClient = createApiClient();
|
|
1248
|
-
try {
|
|
1249
|
-
await apiClient.close(tunnelId);
|
|
1250
|
-
} catch (error) {
|
|
1251
|
-
const message = formatApiError(error);
|
|
1252
|
-
if (!/Tunnel not found/i.test(message)) {
|
|
1253
|
-
failCli(`Failed to close tunnel ${tunnelId}: ${message}`);
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
console.log(`Closed: ${tunnelId}`);
|
|
1257
|
-
});
|
|
1258
830
|
}
|
|
1259
831
|
|
|
1260
|
-
// src/commands/
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
let
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
const bytes = fs5.readFileSync(filePath);
|
|
1272
|
-
const filename = path4.basename(filePath);
|
|
1273
|
-
if (ext === ".html" || ext === ".htm") {
|
|
1274
|
-
msg = {
|
|
1275
|
-
id: generateMessageId(),
|
|
1276
|
-
type: "html",
|
|
1277
|
-
data: bytes.toString("utf-8"),
|
|
1278
|
-
meta: { title: filename, filename, mime: getMimeType(filePath), size: bytes.length }
|
|
1279
|
-
};
|
|
1280
|
-
} else if (TEXT_FILE_EXTENSIONS.has(ext)) {
|
|
1281
|
-
msg = {
|
|
1282
|
-
id: generateMessageId(),
|
|
1283
|
-
type: "text",
|
|
1284
|
-
data: bytes.toString("utf-8"),
|
|
1285
|
-
meta: { filename, mime: getMimeType(filePath), size: bytes.length }
|
|
1286
|
-
};
|
|
1287
|
-
} else {
|
|
1288
|
-
msg = {
|
|
1289
|
-
id: generateMessageId(),
|
|
1290
|
-
type: "binary",
|
|
1291
|
-
meta: { filename, mime: getMimeType(filePath), size: bytes.length }
|
|
1292
|
-
};
|
|
1293
|
-
binaryBase64 = bytes.toString("base64");
|
|
1294
|
-
}
|
|
1295
|
-
} else if (messageArg) {
|
|
1296
|
-
msg = {
|
|
1297
|
-
id: generateMessageId(),
|
|
1298
|
-
type: "text",
|
|
1299
|
-
data: messageArg
|
|
1300
|
-
};
|
|
832
|
+
// src/commands/pubs.ts
|
|
833
|
+
function registerPubCommands(program2) {
|
|
834
|
+
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)").action(
|
|
835
|
+
async (fileArg, opts) => {
|
|
836
|
+
const client = createClient();
|
|
837
|
+
let content;
|
|
838
|
+
let filename;
|
|
839
|
+
if (fileArg) {
|
|
840
|
+
const file = readFile(fileArg);
|
|
841
|
+
content = file.content;
|
|
842
|
+
filename = file.basename;
|
|
1301
843
|
} else {
|
|
1302
|
-
|
|
1303
|
-
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
1304
|
-
msg = {
|
|
1305
|
-
id: generateMessageId(),
|
|
1306
|
-
type: "text",
|
|
1307
|
-
data: Buffer.concat(chunks).toString("utf-8").trim()
|
|
1308
|
-
};
|
|
844
|
+
content = await readFromStdin();
|
|
1309
845
|
}
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
params: { channel: opts.channel, msg, binaryBase64 }
|
|
846
|
+
const resolvedVisibility = resolveVisibilityFlags({
|
|
847
|
+
public: opts.public,
|
|
848
|
+
private: opts.private,
|
|
849
|
+
commandName: "create"
|
|
1315
850
|
});
|
|
1316
|
-
|
|
1317
|
-
|
|
851
|
+
const result = await client.create({
|
|
852
|
+
content,
|
|
853
|
+
filename,
|
|
854
|
+
title: opts.title,
|
|
855
|
+
slug: opts.slug,
|
|
856
|
+
isPublic: resolvedVisibility ?? false,
|
|
857
|
+
expiresIn: opts.expires
|
|
858
|
+
});
|
|
859
|
+
console.log(`Created: ${result.url}`);
|
|
860
|
+
const tmaUrl = getTelegramMiniAppUrl(result.slug);
|
|
861
|
+
if (tmaUrl) console.log(`Telegram: ${tmaUrl}`);
|
|
862
|
+
if (result.expiresAt) {
|
|
863
|
+
console.log(` Expires: ${new Date(result.expiresAt).toISOString()}`);
|
|
1318
864
|
}
|
|
1319
865
|
}
|
|
1320
866
|
);
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
if (!opts.channel && !opts.all) {
|
|
1328
|
-
console.error(
|
|
1329
|
-
"Following chat channel by default. Use `--all` to include binary/file channels."
|
|
1330
|
-
);
|
|
1331
|
-
}
|
|
1332
|
-
let consecutiveFailures = 0;
|
|
1333
|
-
let warnedDisconnected = false;
|
|
1334
|
-
while (true) {
|
|
1335
|
-
try {
|
|
1336
|
-
const response = await ipcCall(socketPath, {
|
|
1337
|
-
method: "read",
|
|
1338
|
-
params: { channel: readChannel }
|
|
1339
|
-
});
|
|
1340
|
-
if (warnedDisconnected) {
|
|
1341
|
-
console.error("Daemon reconnected.");
|
|
1342
|
-
warnedDisconnected = false;
|
|
1343
|
-
}
|
|
1344
|
-
consecutiveFailures = 0;
|
|
1345
|
-
if (response.messages && response.messages.length > 0) {
|
|
1346
|
-
for (const m of response.messages) {
|
|
1347
|
-
console.log(JSON.stringify(m));
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
} catch (error) {
|
|
1351
|
-
consecutiveFailures += 1;
|
|
1352
|
-
if (!warnedDisconnected) {
|
|
1353
|
-
const detail = error instanceof Error ? ` ${error.message}` : "";
|
|
1354
|
-
console.error(`Daemon disconnected. Waiting for recovery...${detail}`);
|
|
1355
|
-
warnedDisconnected = true;
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
const delayMs = getFollowReadDelayMs(warnedDisconnected, consecutiveFailures);
|
|
1359
|
-
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
|
1360
|
-
}
|
|
1361
|
-
} else {
|
|
1362
|
-
const response = await ipcCall(socketPath, {
|
|
1363
|
-
method: "read",
|
|
1364
|
-
params: { channel: readChannel }
|
|
1365
|
-
});
|
|
1366
|
-
if (!response.ok) {
|
|
1367
|
-
failCli(`Failed: ${response.error}`);
|
|
1368
|
-
}
|
|
1369
|
-
console.log(JSON.stringify(response.messages || [], null, 2));
|
|
1370
|
-
}
|
|
867
|
+
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) => {
|
|
868
|
+
const client = createClient();
|
|
869
|
+
const pub = await client.get(slug);
|
|
870
|
+
if (opts.content) {
|
|
871
|
+
process.stdout.write(pub.content ?? "");
|
|
872
|
+
return;
|
|
1371
873
|
}
|
|
1372
|
-
);
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
},
|
|
1396
|
-
dependencies: {
|
|
1397
|
-
commander: "^13.0.0",
|
|
1398
|
-
"node-datachannel": "^0.32.0"
|
|
1399
|
-
},
|
|
1400
|
-
devDependencies: {
|
|
1401
|
-
"@types/node": "22.10.2",
|
|
1402
|
-
tsup: "^8.3.6",
|
|
1403
|
-
typescript: "^5.7.2",
|
|
1404
|
-
vitest: "^3.0.0"
|
|
1405
|
-
},
|
|
1406
|
-
files: [
|
|
1407
|
-
"dist"
|
|
1408
|
-
],
|
|
1409
|
-
repository: {
|
|
1410
|
-
type: "git",
|
|
1411
|
-
url: "git+https://github.com/xmanatee/pub.git",
|
|
1412
|
-
directory: "cli"
|
|
1413
|
-
},
|
|
1414
|
-
publishConfig: {
|
|
1415
|
-
access: "public"
|
|
1416
|
-
},
|
|
1417
|
-
pnpm: {
|
|
1418
|
-
onlyBuiltDependencies: [
|
|
1419
|
-
"esbuild",
|
|
1420
|
-
"node-datachannel"
|
|
1421
|
-
]
|
|
1422
|
-
}
|
|
1423
|
-
};
|
|
1424
|
-
|
|
1425
|
-
// src/lib/version.ts
|
|
1426
|
-
var version = package_default.version;
|
|
1427
|
-
if (typeof version !== "string" || version.length === 0) {
|
|
1428
|
-
throw new Error("Invalid CLI version in package.json");
|
|
1429
|
-
}
|
|
1430
|
-
var CLI_VERSION = version;
|
|
1431
|
-
|
|
1432
|
-
// src/commands/tunnel/start-command.ts
|
|
1433
|
-
async function waitForStopped(isRunning, timeoutMs, pollMs = 120) {
|
|
1434
|
-
const started = Date.now();
|
|
1435
|
-
while (Date.now() - started < timeoutMs) {
|
|
1436
|
-
if (!isRunning()) return true;
|
|
1437
|
-
await new Promise((resolve3) => setTimeout(resolve3, pollMs));
|
|
1438
|
-
}
|
|
1439
|
-
return !isRunning();
|
|
1440
|
-
}
|
|
1441
|
-
function registerTunnelStartCommand(tunnel) {
|
|
1442
|
-
tunnel.command("start").description("Start a tunnel daemon (reuses existing tunnel when possible)").option("--expires <duration>", "Auto-close after duration (e.g. 4h, 1d)", "24h").option("-t, --tunnel <tunnelId>", "Attach/start daemon for an existing tunnel").option("--new", "Always create a new tunnel (skip single-tunnel reuse)").option("--bridge <mode>", "Bridge mode: openclaw|none").option("--foreground", "Run in foreground (don't fork, no managed bridge)").action(
|
|
1443
|
-
async (opts) => {
|
|
1444
|
-
await ensureNodeDatachannelAvailable();
|
|
1445
|
-
const runtimeConfig = getConfig();
|
|
1446
|
-
const apiClient = createApiClient(runtimeConfig);
|
|
1447
|
-
let target = null;
|
|
1448
|
-
const bridgeMode = parseBridgeMode(opts.bridge || runtimeConfig.bridge?.mode || "openclaw");
|
|
1449
|
-
const bridgeProcessEnv = buildBridgeProcessEnv(runtimeConfig.bridge);
|
|
1450
|
-
if (opts.tunnel) {
|
|
1451
|
-
try {
|
|
1452
|
-
const existing = await apiClient.get(opts.tunnel);
|
|
1453
|
-
if (existing.status === "closed" || existing.expiresAt <= Date.now()) {
|
|
1454
|
-
failCli(`Tunnel ${opts.tunnel} is closed or expired.`);
|
|
1455
|
-
}
|
|
1456
|
-
target = {
|
|
1457
|
-
createdNew: false,
|
|
1458
|
-
expiresAt: existing.expiresAt,
|
|
1459
|
-
mode: "existing",
|
|
1460
|
-
tunnelId: existing.tunnelId,
|
|
1461
|
-
url: getPublicTunnelUrl(existing.tunnelId)
|
|
1462
|
-
};
|
|
1463
|
-
} catch (error) {
|
|
1464
|
-
failCli(`Failed to use tunnel ${opts.tunnel}: ${formatApiError(error)}`);
|
|
1465
|
-
}
|
|
1466
|
-
} else if (!opts.new) {
|
|
1467
|
-
try {
|
|
1468
|
-
const listed = await apiClient.list();
|
|
1469
|
-
const active = listed.filter((t) => t.status === "active" && t.expiresAt > Date.now()).sort((a, b) => b.createdAt - a.createdAt);
|
|
1470
|
-
const reusable = pickReusableTunnel(listed);
|
|
1471
|
-
if (reusable) {
|
|
1472
|
-
target = {
|
|
1473
|
-
createdNew: false,
|
|
1474
|
-
expiresAt: reusable.expiresAt,
|
|
1475
|
-
mode: "existing",
|
|
1476
|
-
tunnelId: reusable.tunnelId,
|
|
1477
|
-
url: getPublicTunnelUrl(reusable.tunnelId)
|
|
1478
|
-
};
|
|
1479
|
-
if (active.length > 1) {
|
|
1480
|
-
console.error(
|
|
1481
|
-
[
|
|
1482
|
-
`Multiple active tunnels found: ${active.map((t) => t.tunnelId).join(", ")}`,
|
|
1483
|
-
`Reusing most recent active tunnel ${reusable.tunnelId}.`,
|
|
1484
|
-
"Use --tunnel <id> to choose explicitly or --new to force creation."
|
|
1485
|
-
].join("\n")
|
|
1486
|
-
);
|
|
1487
|
-
} else {
|
|
1488
|
-
console.error(
|
|
1489
|
-
`Reusing existing active tunnel ${reusable.tunnelId}. Use --new to force creation.`
|
|
1490
|
-
);
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
} catch (error) {
|
|
1494
|
-
failCli(`Failed to list tunnels for reuse check: ${formatApiError(error)}`);
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
if (!target) {
|
|
1498
|
-
try {
|
|
1499
|
-
const created = await apiClient.create({
|
|
1500
|
-
expiresIn: opts.expires
|
|
1501
|
-
});
|
|
1502
|
-
target = {
|
|
1503
|
-
createdNew: true,
|
|
1504
|
-
expiresAt: created.expiresAt,
|
|
1505
|
-
mode: "created",
|
|
1506
|
-
tunnelId: created.tunnelId,
|
|
1507
|
-
url: created.url
|
|
1508
|
-
};
|
|
1509
|
-
} catch (error) {
|
|
1510
|
-
failCli(`Failed to create tunnel: ${formatApiError(error)}`);
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
if (!target) {
|
|
1514
|
-
failCli("Failed to resolve tunnel target.");
|
|
1515
|
-
}
|
|
1516
|
-
const socketPath = getSocketPath(target.tunnelId);
|
|
1517
|
-
const infoPath = tunnelInfoPath(target.tunnelId);
|
|
1518
|
-
const logPath = tunnelLogPath(target.tunnelId);
|
|
1519
|
-
if (opts.foreground) {
|
|
1520
|
-
if (bridgeMode !== "none") {
|
|
1521
|
-
throw new Error(
|
|
1522
|
-
"Foreground mode disables managed bridge process. Use background mode for --bridge openclaw."
|
|
1523
|
-
);
|
|
1524
|
-
}
|
|
1525
|
-
const { startDaemon } = await import("./tunnel-daemon-RKWEA5BV.js");
|
|
1526
|
-
console.log(`Tunnel started: ${target.url}`);
|
|
1527
|
-
const fgTma = getTelegramMiniAppUrl("tunnel", target.tunnelId);
|
|
1528
|
-
if (fgTma) console.log(`Telegram: ${fgTma}`);
|
|
1529
|
-
console.log(`Tunnel ID: ${target.tunnelId}`);
|
|
1530
|
-
console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
|
|
1531
|
-
if (target.mode === "existing") console.log("Mode: attached existing tunnel");
|
|
1532
|
-
console.log("Running in foreground. Press Ctrl+C to stop.");
|
|
1533
|
-
try {
|
|
1534
|
-
await startDaemon({
|
|
1535
|
-
cliVersion: CLI_VERSION,
|
|
1536
|
-
tunnelId: target.tunnelId,
|
|
1537
|
-
apiClient,
|
|
1538
|
-
socketPath,
|
|
1539
|
-
infoPath
|
|
1540
|
-
});
|
|
1541
|
-
} catch (error) {
|
|
1542
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1543
|
-
failCli(`Daemon failed: ${message}`);
|
|
1544
|
-
}
|
|
1545
|
-
return;
|
|
1546
|
-
}
|
|
1547
|
-
const runningDaemonInfo = readDaemonProcessInfo(target.tunnelId);
|
|
1548
|
-
if (runningDaemonInfo) {
|
|
1549
|
-
const daemonVersion = runningDaemonInfo.cliVersion;
|
|
1550
|
-
const shouldRestartForUpgrade = shouldRestartDaemonForCliUpgrade(
|
|
1551
|
-
daemonVersion,
|
|
1552
|
-
CLI_VERSION
|
|
1553
|
-
);
|
|
1554
|
-
if (shouldRestartForUpgrade) {
|
|
1555
|
-
console.error(
|
|
1556
|
-
`Restarting daemon for CLI version ${CLI_VERSION} (running: ${daemonVersion || "unknown"}).`
|
|
1557
|
-
);
|
|
1558
|
-
if (isBridgeRunning(target.tunnelId)) {
|
|
1559
|
-
stopBridgeProcess(target.tunnelId);
|
|
1560
|
-
const bridgeStopped = await waitForStopped(
|
|
1561
|
-
() => isBridgeRunning(target.tunnelId),
|
|
1562
|
-
5e3
|
|
1563
|
-
);
|
|
1564
|
-
if (!bridgeStopped) {
|
|
1565
|
-
failCli("Bridge process did not stop during daemon upgrade restart.");
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
try {
|
|
1569
|
-
await ipcCall(socketPath, { method: "close", params: {} });
|
|
1570
|
-
} catch (error) {
|
|
1571
|
-
failCli(
|
|
1572
|
-
[
|
|
1573
|
-
`Failed to stop running daemon for upgrade: ${error instanceof Error ? error.message : String(error)}`,
|
|
1574
|
-
"Run `pubblue tunnel close <id>` and retry."
|
|
1575
|
-
].join("\n")
|
|
1576
|
-
);
|
|
1577
|
-
}
|
|
1578
|
-
const daemonStopped = await waitForStopped(
|
|
1579
|
-
() => isDaemonRunning(target.tunnelId),
|
|
1580
|
-
6e3
|
|
1581
|
-
);
|
|
1582
|
-
if (!daemonStopped) {
|
|
1583
|
-
failCli("Daemon did not stop in time during upgrade restart.");
|
|
1584
|
-
}
|
|
1585
|
-
} else {
|
|
1586
|
-
try {
|
|
1587
|
-
const status = await ipcCall(socketPath, { method: "status", params: {} });
|
|
1588
|
-
if (!status.ok) throw new Error(String(status.error || "status check failed"));
|
|
1589
|
-
} catch (error) {
|
|
1590
|
-
failCli(
|
|
1591
|
-
[
|
|
1592
|
-
`Daemon process exists but is not responding: ${error instanceof Error ? error.message : String(error)}`,
|
|
1593
|
-
"Run `pubblue tunnel close <id>` and start again."
|
|
1594
|
-
].join("\n")
|
|
1595
|
-
);
|
|
1596
|
-
}
|
|
1597
|
-
if (bridgeMode !== "none") {
|
|
1598
|
-
const bridgeReady = await ensureBridgeReady({
|
|
1599
|
-
bridgeMode,
|
|
1600
|
-
tunnelId: target.tunnelId,
|
|
1601
|
-
socketPath,
|
|
1602
|
-
bridgeProcessEnv,
|
|
1603
|
-
timeoutMs: 8e3
|
|
1604
|
-
});
|
|
1605
|
-
if (!bridgeReady.ok) {
|
|
1606
|
-
const lines = [
|
|
1607
|
-
`Bridge failed to start for running tunnel: ${bridgeReady.reason ?? "unknown reason"}`
|
|
1608
|
-
];
|
|
1609
|
-
const existingBridgeLog = bridgeLogPath(target.tunnelId);
|
|
1610
|
-
if (fs6.existsSync(existingBridgeLog)) {
|
|
1611
|
-
lines.push(`Bridge log: ${existingBridgeLog}`);
|
|
1612
|
-
const bridgeTail = readLogTail(existingBridgeLog);
|
|
1613
|
-
if (bridgeTail) {
|
|
1614
|
-
lines.push("---- bridge log tail ----");
|
|
1615
|
-
lines.push(bridgeTail.trimEnd());
|
|
1616
|
-
lines.push("---- end bridge log tail ----");
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
failCli(lines.join("\n"));
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
console.log(`Tunnel started: ${target.url}`);
|
|
1623
|
-
const runTma = getTelegramMiniAppUrl("tunnel", target.tunnelId);
|
|
1624
|
-
if (runTma) console.log(`Telegram: ${runTma}`);
|
|
1625
|
-
console.log(`Tunnel ID: ${target.tunnelId}`);
|
|
1626
|
-
console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
|
|
1627
|
-
console.log("Daemon already running for this tunnel.");
|
|
1628
|
-
console.log(`Daemon log: ${logPath}`);
|
|
1629
|
-
if (bridgeMode !== "none") {
|
|
1630
|
-
console.log("Bridge mode: openclaw");
|
|
1631
|
-
console.log(`Bridge log: ${bridgeLogPath(target.tunnelId)}`);
|
|
1632
|
-
}
|
|
1633
|
-
return;
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
const daemonScript = path5.join(import.meta.dirname, "tunnel-daemon-entry.js");
|
|
1637
|
-
const daemonLogFd = fs6.openSync(logPath, "a");
|
|
1638
|
-
const child = fork2(daemonScript, [], {
|
|
1639
|
-
detached: true,
|
|
1640
|
-
stdio: buildDaemonForkStdio(daemonLogFd),
|
|
1641
|
-
env: {
|
|
1642
|
-
...process.env,
|
|
1643
|
-
PUBBLUE_DAEMON_TUNNEL_ID: target.tunnelId,
|
|
1644
|
-
PUBBLUE_DAEMON_BASE_URL: runtimeConfig.baseUrl,
|
|
1645
|
-
PUBBLUE_DAEMON_API_KEY: runtimeConfig.apiKey,
|
|
1646
|
-
PUBBLUE_DAEMON_SOCKET: socketPath,
|
|
1647
|
-
PUBBLUE_DAEMON_INFO: infoPath,
|
|
1648
|
-
PUBBLUE_CLI_VERSION: CLI_VERSION
|
|
1649
|
-
}
|
|
1650
|
-
});
|
|
1651
|
-
fs6.closeSync(daemonLogFd);
|
|
1652
|
-
if (child.connected) {
|
|
1653
|
-
child.disconnect();
|
|
874
|
+
console.log(` Slug: ${pub.slug}`);
|
|
875
|
+
if (pub.contentType) console.log(` Type: ${pub.contentType}`);
|
|
876
|
+
if (pub.title) console.log(` Title: ${pub.title}`);
|
|
877
|
+
console.log(` Status: ${formatVisibility(pub.isPublic)}`);
|
|
878
|
+
if (pub.expiresAt) console.log(` Expires: ${new Date(pub.expiresAt).toISOString()}`);
|
|
879
|
+
console.log(` Created: ${new Date(pub.createdAt).toLocaleDateString()}`);
|
|
880
|
+
console.log(` Updated: ${new Date(pub.updatedAt).toLocaleDateString()}`);
|
|
881
|
+
if (pub.content) console.log(` Size: ${pub.content.length} bytes`);
|
|
882
|
+
if (pub.live) {
|
|
883
|
+
console.log(` Live: ${pub.live.status}`);
|
|
884
|
+
console.log(` Connected: ${pub.live.hasConnection ? "yes" : "no"}`);
|
|
885
|
+
console.log(` Expires: ${new Date(pub.live.expiresAt).toISOString()}`);
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
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(
|
|
889
|
+
async (slug, opts) => {
|
|
890
|
+
const client = createClient();
|
|
891
|
+
let content;
|
|
892
|
+
let filename;
|
|
893
|
+
if (opts.file) {
|
|
894
|
+
const file = readFile(opts.file);
|
|
895
|
+
content = file.content;
|
|
896
|
+
filename = file.basename;
|
|
1654
897
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
infoPath,
|
|
1660
|
-
socketPath,
|
|
1661
|
-
timeoutMs: 8e3
|
|
898
|
+
const isPublic = resolveVisibilityFlags({
|
|
899
|
+
public: opts.public,
|
|
900
|
+
private: opts.private,
|
|
901
|
+
commandName: "update"
|
|
1662
902
|
});
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
lines.push("---- daemon log tail ----");
|
|
1671
|
-
lines.push(tail.trimEnd());
|
|
1672
|
-
lines.push("---- end daemon log tail ----");
|
|
1673
|
-
}
|
|
1674
|
-
await cleanupCreatedTunnelOnStartFailure(apiClient, target);
|
|
1675
|
-
failCli(lines.join("\n"));
|
|
1676
|
-
}
|
|
1677
|
-
const offerReady = await waitForAgentOffer({
|
|
1678
|
-
apiClient,
|
|
1679
|
-
tunnelId: target.tunnelId,
|
|
1680
|
-
timeoutMs: 5e3
|
|
903
|
+
const result = await client.update({
|
|
904
|
+
slug,
|
|
905
|
+
content,
|
|
906
|
+
filename,
|
|
907
|
+
title: opts.title,
|
|
908
|
+
isPublic,
|
|
909
|
+
newSlug: opts.slug
|
|
1681
910
|
});
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
`Daemon log: ${logPath}`
|
|
1686
|
-
];
|
|
1687
|
-
const tail = readLogTail(logPath);
|
|
1688
|
-
if (tail) {
|
|
1689
|
-
lines.push("---- daemon log tail ----");
|
|
1690
|
-
lines.push(tail.trimEnd());
|
|
1691
|
-
lines.push("---- end daemon log tail ----");
|
|
1692
|
-
}
|
|
1693
|
-
await cleanupCreatedTunnelOnStartFailure(apiClient, target);
|
|
1694
|
-
failCli(lines.join("\n"));
|
|
1695
|
-
}
|
|
1696
|
-
if (bridgeMode !== "none") {
|
|
1697
|
-
const bridgeReady = await ensureBridgeReady({
|
|
1698
|
-
bridgeMode,
|
|
1699
|
-
tunnelId: target.tunnelId,
|
|
1700
|
-
socketPath,
|
|
1701
|
-
bridgeProcessEnv,
|
|
1702
|
-
timeoutMs: 8e3
|
|
1703
|
-
});
|
|
1704
|
-
if (!bridgeReady.ok) {
|
|
1705
|
-
const lines = [`Bridge failed to start: ${bridgeReady.reason ?? "unknown reason"}`];
|
|
1706
|
-
const bridgeLog = bridgeLogPath(target.tunnelId);
|
|
1707
|
-
if (fs6.existsSync(bridgeLog)) {
|
|
1708
|
-
lines.push(`Bridge log: ${bridgeLog}`);
|
|
1709
|
-
const bridgeTail = readLogTail(bridgeLog);
|
|
1710
|
-
if (bridgeTail) {
|
|
1711
|
-
lines.push("---- bridge log tail ----");
|
|
1712
|
-
lines.push(bridgeTail.trimEnd());
|
|
1713
|
-
lines.push("---- end bridge log tail ----");
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
try {
|
|
1717
|
-
await ipcCall(socketPath, { method: "close", params: {} });
|
|
1718
|
-
} catch {
|
|
1719
|
-
}
|
|
1720
|
-
await cleanupCreatedTunnelOnStartFailure(apiClient, target);
|
|
1721
|
-
failCli(lines.join("\n"));
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
console.log(`Tunnel started: ${target.url}`);
|
|
1725
|
-
const tma = getTelegramMiniAppUrl("tunnel", target.tunnelId);
|
|
1726
|
-
if (tma) console.log(`Telegram: ${tma}`);
|
|
1727
|
-
console.log(`Tunnel ID: ${target.tunnelId}`);
|
|
1728
|
-
console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
|
|
1729
|
-
if (target.mode === "existing") console.log("Mode: attached existing tunnel");
|
|
1730
|
-
console.log("Daemon health: OK");
|
|
1731
|
-
console.log(`Daemon log: ${logPath}`);
|
|
1732
|
-
if (bridgeMode !== "none") {
|
|
1733
|
-
console.log("Bridge mode: openclaw");
|
|
1734
|
-
console.log(`Bridge log: ${bridgeLogPath(target.tunnelId)}`);
|
|
1735
|
-
}
|
|
911
|
+
console.log(`Updated: ${result.slug}`);
|
|
912
|
+
if (result.title) console.log(` Title: ${result.title}`);
|
|
913
|
+
console.log(` Status: ${formatVisibility(result.isPublic)}`);
|
|
1736
914
|
}
|
|
1737
915
|
);
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
916
|
+
program2.command("list").description("List your pubs").action(async () => {
|
|
917
|
+
const client = createClient();
|
|
918
|
+
const pubs = await client.list();
|
|
919
|
+
if (pubs.length === 0) {
|
|
920
|
+
console.log("No pubs.");
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
for (const pub of pubs) {
|
|
924
|
+
const date = new Date(pub.createdAt).toLocaleDateString();
|
|
925
|
+
const expires = pub.expiresAt ? ` expires:${new Date(pub.expiresAt).toISOString()}` : "";
|
|
926
|
+
const contentLabel = pub.contentType ? `[${pub.contentType}]` : "[no content]";
|
|
927
|
+
const sessionLabel = pub.live?.status === "active" ? " [live]" : "";
|
|
928
|
+
console.log(
|
|
929
|
+
` ${pub.slug} ${contentLabel} ${formatVisibility(pub.isPublic)} ${date}${expires}${sessionLabel}`
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
program2.command("delete").description("Delete a pub").argument("<slug>", "Slug of the pub to delete").action(async (slug) => {
|
|
934
|
+
const client = createClient();
|
|
935
|
+
await client.remove(slug);
|
|
936
|
+
console.log(`Deleted: ${slug}`);
|
|
937
|
+
});
|
|
1746
938
|
}
|
|
1747
939
|
|
|
1748
940
|
// src/program.ts
|
|
1749
941
|
function buildProgram() {
|
|
1750
942
|
const program2 = new Command();
|
|
1751
943
|
program2.exitOverride();
|
|
1752
|
-
program2.name("pubblue").description("Publish
|
|
944
|
+
program2.name("pubblue").description("Publish content and go live").version(CLI_VERSION);
|
|
1753
945
|
registerConfigureCommand(program2);
|
|
1754
|
-
|
|
1755
|
-
|
|
946
|
+
registerPubCommands(program2);
|
|
947
|
+
registerLiveCommands(program2);
|
|
1756
948
|
return program2;
|
|
1757
949
|
}
|
|
1758
950
|
|