pubblue 0.4.10 → 0.4.12
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-MW35LBNH.js → chunk-4YTJ2WKF.js} +1 -1
- package/dist/{chunk-HOHLQGQT.js → chunk-UW7JILRJ.js} +61 -40
- package/dist/index.js +1465 -1169
- package/dist/tunnel-bridge-entry.js +477 -79
- package/dist/{tunnel-daemon-4LV6HLYN.js → tunnel-daemon-RKWEA5BV.js} +5 -2
- package/dist/tunnel-daemon-entry.js +6 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,18 +11,51 @@ import {
|
|
|
11
11
|
CHANNELS,
|
|
12
12
|
CONTROL_CHANNEL,
|
|
13
13
|
generateMessageId
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-4YTJ2WKF.js";
|
|
15
15
|
|
|
16
|
-
// src/
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
// src/lib/cli-error.ts
|
|
17
|
+
import { CommanderError } from "commander";
|
|
18
|
+
var CliError = class extends Error {
|
|
19
|
+
exitCode;
|
|
20
|
+
constructor(message, exitCode = 1) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "CliError";
|
|
23
|
+
this.exitCode = exitCode;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
function failCli(message, exitCode = 1) {
|
|
27
|
+
throw new CliError(message, exitCode);
|
|
28
|
+
}
|
|
29
|
+
function toCliFailure(error) {
|
|
30
|
+
if (error instanceof CommanderError) {
|
|
31
|
+
return {
|
|
32
|
+
exitCode: error.exitCode,
|
|
33
|
+
message: ""
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (error instanceof CliError) {
|
|
37
|
+
return {
|
|
38
|
+
exitCode: error.exitCode,
|
|
39
|
+
message: error.message
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (error instanceof Error) {
|
|
43
|
+
return {
|
|
44
|
+
exitCode: 1,
|
|
45
|
+
message: error.message
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
exitCode: 1,
|
|
50
|
+
message: String(error)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/program.ts
|
|
20
55
|
import { Command } from "commander";
|
|
21
56
|
|
|
22
|
-
// src/commands/
|
|
23
|
-
import {
|
|
24
|
-
import * as fs2 from "fs";
|
|
25
|
-
import * as path2 from "path";
|
|
57
|
+
// src/commands/configure.ts
|
|
58
|
+
import { createInterface } from "readline/promises";
|
|
26
59
|
|
|
27
60
|
// src/lib/config.ts
|
|
28
61
|
import * as fs from "fs";
|
|
@@ -78,827 +111,794 @@ function getConfig(homeDir) {
|
|
|
78
111
|
bridge: saved.bridge
|
|
79
112
|
};
|
|
80
113
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
".md",
|
|
86
|
-
".markdown",
|
|
87
|
-
".json",
|
|
88
|
-
".csv",
|
|
89
|
-
".xml",
|
|
90
|
-
".yaml",
|
|
91
|
-
".yml",
|
|
92
|
-
".js",
|
|
93
|
-
".mjs",
|
|
94
|
-
".cjs",
|
|
95
|
-
".ts",
|
|
96
|
-
".tsx",
|
|
97
|
-
".jsx",
|
|
98
|
-
".css",
|
|
99
|
-
".scss",
|
|
100
|
-
".sass",
|
|
101
|
-
".less",
|
|
102
|
-
".log"
|
|
103
|
-
]);
|
|
104
|
-
function getMimeType(filePath) {
|
|
105
|
-
const ext = path2.extname(filePath).toLowerCase();
|
|
106
|
-
const mimeByExt = {
|
|
107
|
-
".html": "text/html; charset=utf-8",
|
|
108
|
-
".htm": "text/html; charset=utf-8",
|
|
109
|
-
".txt": "text/plain; charset=utf-8",
|
|
110
|
-
".md": "text/markdown; charset=utf-8",
|
|
111
|
-
".markdown": "text/markdown; charset=utf-8",
|
|
112
|
-
".json": "application/json",
|
|
113
|
-
".csv": "text/csv; charset=utf-8",
|
|
114
|
-
".xml": "application/xml",
|
|
115
|
-
".yaml": "application/x-yaml",
|
|
116
|
-
".yml": "application/x-yaml",
|
|
117
|
-
".png": "image/png",
|
|
118
|
-
".jpg": "image/jpeg",
|
|
119
|
-
".jpeg": "image/jpeg",
|
|
120
|
-
".gif": "image/gif",
|
|
121
|
-
".webp": "image/webp",
|
|
122
|
-
".svg": "image/svg+xml",
|
|
123
|
-
".pdf": "application/pdf",
|
|
124
|
-
".zip": "application/zip",
|
|
125
|
-
".mp3": "audio/mpeg",
|
|
126
|
-
".wav": "audio/wav",
|
|
127
|
-
".mp4": "video/mp4"
|
|
128
|
-
};
|
|
129
|
-
return mimeByExt[ext] || "application/octet-stream";
|
|
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}`;
|
|
130
118
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
function createClient() {
|
|
191
|
+
const config = getConfig();
|
|
192
|
+
return new PubApiClient(config.baseUrl, config.apiKey);
|
|
140
193
|
}
|
|
141
|
-
function
|
|
142
|
-
|
|
194
|
+
async function readFromStdin() {
|
|
195
|
+
const chunks = [];
|
|
196
|
+
for await (const chunk of process.stdin) {
|
|
197
|
+
chunks.push(chunk);
|
|
198
|
+
}
|
|
199
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
143
200
|
}
|
|
144
|
-
function
|
|
145
|
-
return
|
|
201
|
+
function formatVisibility(isPublic) {
|
|
202
|
+
return isPublic ? "public" : "private";
|
|
146
203
|
}
|
|
147
|
-
function
|
|
148
|
-
|
|
204
|
+
function resolveVisibilityFlags(opts) {
|
|
205
|
+
if (opts.public && opts.private) {
|
|
206
|
+
throw new Error(`Use only one of --public or --private for ${opts.commandName}.`);
|
|
207
|
+
}
|
|
208
|
+
if (opts.public) return true;
|
|
209
|
+
if (opts.private) return false;
|
|
210
|
+
return void 0;
|
|
149
211
|
}
|
|
150
|
-
function
|
|
151
|
-
|
|
212
|
+
function readFile(filePath) {
|
|
213
|
+
const resolved = path2.resolve(filePath);
|
|
214
|
+
if (!fs2.existsSync(resolved)) {
|
|
215
|
+
failCli(`File not found: ${resolved}`);
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
content: fs2.readFileSync(resolved, "utf-8"),
|
|
219
|
+
basename: path2.basename(resolved)
|
|
220
|
+
};
|
|
152
221
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
222
|
+
|
|
223
|
+
// src/commands/configure.ts
|
|
224
|
+
function readApiKeyFromPrompt() {
|
|
225
|
+
const rl = createInterface({
|
|
226
|
+
input: process.stdin,
|
|
227
|
+
output: process.stdout
|
|
228
|
+
});
|
|
229
|
+
return rl.question("Enter API key: ").then((answer) => answer.trim()).finally(() => {
|
|
230
|
+
rl.close();
|
|
231
|
+
});
|
|
156
232
|
}
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const setIfMissing = (key, value) => {
|
|
161
|
-
if (value === void 0 || value === null) return;
|
|
162
|
-
const current = env[key];
|
|
163
|
-
if (typeof current === "string" && current.length > 0) return;
|
|
164
|
-
env[key] = String(value);
|
|
165
|
-
};
|
|
166
|
-
setIfMissing("OPENCLAW_PATH", bridgeConfig.openclawPath);
|
|
167
|
-
setIfMissing("OPENCLAW_SESSION_ID", bridgeConfig.sessionId);
|
|
168
|
-
setIfMissing("OPENCLAW_THREAD_ID", bridgeConfig.threadId);
|
|
169
|
-
if (bridgeConfig.deliver !== void 0) {
|
|
170
|
-
setIfMissing("OPENCLAW_DELIVER", bridgeConfig.deliver ? "1" : "0");
|
|
233
|
+
async function resolveConfigureApiKey(opts) {
|
|
234
|
+
if (opts.apiKey && opts.apiKeyStdin) {
|
|
235
|
+
throw new Error("Use only one of --api-key or --api-key-stdin.");
|
|
171
236
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (bridgeConfig.deliverTimeoutMs !== void 0) {
|
|
175
|
-
setIfMissing("OPENCLAW_DELIVER_TIMEOUT_MS", bridgeConfig.deliverTimeoutMs);
|
|
237
|
+
if (opts.apiKey) {
|
|
238
|
+
return opts.apiKey.trim();
|
|
176
239
|
}
|
|
177
|
-
|
|
240
|
+
if (opts.apiKeyStdin) {
|
|
241
|
+
return readFromStdin();
|
|
242
|
+
}
|
|
243
|
+
const envKey = process.env.PUBBLUE_API_KEY?.trim();
|
|
244
|
+
if (envKey) return envKey;
|
|
245
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
"No TTY available. Provide --api-key, --api-key-stdin, or PUBBLUE_API_KEY for configure."
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return readApiKeyFromPrompt();
|
|
178
251
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
process.exit(1);
|
|
252
|
+
function collectValues(value, previous) {
|
|
253
|
+
previous.push(value);
|
|
254
|
+
return previous;
|
|
255
|
+
}
|
|
256
|
+
function parseSetInput(raw) {
|
|
257
|
+
const sepIndex = raw.indexOf("=");
|
|
258
|
+
if (sepIndex <= 0 || sepIndex === raw.length - 1) {
|
|
259
|
+
throw new Error(`Invalid --set entry "${raw}". Use key=value.`);
|
|
188
260
|
}
|
|
261
|
+
return {
|
|
262
|
+
key: raw.slice(0, sepIndex).trim(),
|
|
263
|
+
value: raw.slice(sepIndex + 1).trim()
|
|
264
|
+
};
|
|
189
265
|
}
|
|
190
|
-
function
|
|
191
|
-
const
|
|
192
|
-
if (
|
|
193
|
-
try {
|
|
194
|
-
const info = JSON.parse(fs2.readFileSync(infoPath, "utf-8"));
|
|
195
|
-
process.kill(info.pid, 0);
|
|
266
|
+
function parseBooleanValue(raw, key) {
|
|
267
|
+
const normalized = raw.trim().toLowerCase();
|
|
268
|
+
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on")
|
|
196
269
|
return true;
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
fs2.unlinkSync(infoPath);
|
|
200
|
-
} catch {
|
|
201
|
-
}
|
|
270
|
+
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off")
|
|
202
271
|
return false;
|
|
203
|
-
}
|
|
272
|
+
throw new Error(`Invalid boolean value for ${key}: ${raw}`);
|
|
204
273
|
}
|
|
205
|
-
function
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
return JSON.parse(fs2.readFileSync(infoPath, "utf-8"));
|
|
210
|
-
} catch {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
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.`);
|
|
213
278
|
}
|
|
214
|
-
function
|
|
215
|
-
const infoPath = bridgeInfoPath(tunnelId);
|
|
216
|
-
if (!fs2.existsSync(infoPath)) return false;
|
|
217
|
-
try {
|
|
218
|
-
const info = JSON.parse(fs2.readFileSync(infoPath, "utf-8"));
|
|
219
|
-
process.kill(info.pid, 0);
|
|
220
|
-
return true;
|
|
221
|
-
} catch {
|
|
222
|
-
try {
|
|
223
|
-
fs2.unlinkSync(infoPath);
|
|
224
|
-
} catch {
|
|
225
|
-
}
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
function stopBridgeProcess(tunnelId) {
|
|
230
|
-
const info = readBridgeProcessInfo(tunnelId);
|
|
231
|
-
if (!info || !Number.isFinite(info.pid)) return;
|
|
232
|
-
try {
|
|
233
|
-
process.kill(info.pid, "SIGTERM");
|
|
234
|
-
} catch {
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
function buildBridgeForkStdio(logFd) {
|
|
238
|
-
return ["ignore", logFd, logFd, "ipc"];
|
|
239
|
-
}
|
|
240
|
-
function getFollowReadDelayMs(disconnected, consecutiveFailures) {
|
|
241
|
-
if (!disconnected) return 1e3;
|
|
242
|
-
return Math.min(5e3, 1e3 * 2 ** Math.min(consecutiveFailures, 3));
|
|
243
|
-
}
|
|
244
|
-
function resolveTunnelIdSelection(tunnelIdArg, tunnelOpt) {
|
|
245
|
-
return tunnelOpt || tunnelIdArg;
|
|
246
|
-
}
|
|
247
|
-
function buildDaemonForkStdio(logFd) {
|
|
248
|
-
return ["ignore", logFd, logFd, "ipc"];
|
|
249
|
-
}
|
|
250
|
-
function parsePositiveIntegerOption(raw, optionName) {
|
|
279
|
+
function parsePositiveInteger(raw, key) {
|
|
251
280
|
const parsed = Number.parseInt(raw, 10);
|
|
252
281
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
253
|
-
throw new Error(`${
|
|
282
|
+
throw new Error(`${key} must be a positive integer. Received: ${raw}`);
|
|
254
283
|
}
|
|
255
284
|
return parsed;
|
|
256
285
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
286
|
+
var SUPPORTED_KEYS = [
|
|
287
|
+
"bridge.mode",
|
|
288
|
+
"openclaw.path",
|
|
289
|
+
"openclaw.sessionId",
|
|
290
|
+
"openclaw.threadId",
|
|
291
|
+
"openclaw.canvasReminderEvery",
|
|
292
|
+
"openclaw.deliver",
|
|
293
|
+
"openclaw.deliverChannel",
|
|
294
|
+
"openclaw.replyTo",
|
|
295
|
+
"openclaw.deliverTimeoutMs",
|
|
296
|
+
"openclaw.attachmentDir",
|
|
297
|
+
"openclaw.attachmentMaxBytes",
|
|
298
|
+
"telegram.botToken"
|
|
299
|
+
];
|
|
300
|
+
function applyConfigSet(bridge, telegram, key, value) {
|
|
301
|
+
switch (key) {
|
|
302
|
+
case "bridge.mode":
|
|
303
|
+
bridge.mode = parseBridgeModeValue(value);
|
|
304
|
+
return;
|
|
305
|
+
case "openclaw.path":
|
|
306
|
+
bridge.openclawPath = value;
|
|
307
|
+
return;
|
|
308
|
+
case "openclaw.sessionId":
|
|
309
|
+
bridge.sessionId = value;
|
|
310
|
+
return;
|
|
311
|
+
case "openclaw.threadId":
|
|
312
|
+
bridge.threadId = value;
|
|
313
|
+
return;
|
|
314
|
+
case "openclaw.canvasReminderEvery":
|
|
315
|
+
bridge.canvasReminderEvery = parsePositiveInteger(value, key);
|
|
316
|
+
return;
|
|
317
|
+
case "openclaw.deliver":
|
|
318
|
+
bridge.deliver = parseBooleanValue(value, key);
|
|
319
|
+
return;
|
|
320
|
+
case "openclaw.deliverChannel":
|
|
321
|
+
bridge.deliverChannel = value;
|
|
322
|
+
return;
|
|
323
|
+
case "openclaw.replyTo":
|
|
324
|
+
bridge.replyTo = value;
|
|
325
|
+
return;
|
|
326
|
+
case "openclaw.deliverTimeoutMs":
|
|
327
|
+
bridge.deliverTimeoutMs = parsePositiveInteger(value, key);
|
|
328
|
+
return;
|
|
329
|
+
case "openclaw.attachmentDir":
|
|
330
|
+
bridge.attachmentDir = value;
|
|
331
|
+
return;
|
|
332
|
+
case "openclaw.attachmentMaxBytes":
|
|
333
|
+
bridge.attachmentMaxBytes = parsePositiveInteger(value, key);
|
|
334
|
+
return;
|
|
335
|
+
case "telegram.botToken":
|
|
336
|
+
telegram.botToken = value;
|
|
337
|
+
return;
|
|
338
|
+
default:
|
|
339
|
+
throw new Error(
|
|
340
|
+
[
|
|
341
|
+
`Unknown config key: ${key}`,
|
|
342
|
+
"Supported keys:",
|
|
343
|
+
...SUPPORTED_KEYS.map((k) => ` ${k}`)
|
|
344
|
+
].join("\n")
|
|
345
|
+
);
|
|
261
346
|
}
|
|
262
|
-
throw new Error(`--bridge must be one of: openclaw, none. Received: ${raw}`);
|
|
263
347
|
}
|
|
264
|
-
function
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
348
|
+
function applyConfigUnset(bridge, telegram, key) {
|
|
349
|
+
switch (key) {
|
|
350
|
+
case "bridge.mode":
|
|
351
|
+
delete bridge.mode;
|
|
352
|
+
return;
|
|
353
|
+
case "openclaw.path":
|
|
354
|
+
delete bridge.openclawPath;
|
|
355
|
+
return;
|
|
356
|
+
case "openclaw.sessionId":
|
|
357
|
+
delete bridge.sessionId;
|
|
358
|
+
return;
|
|
359
|
+
case "openclaw.threadId":
|
|
360
|
+
delete bridge.threadId;
|
|
361
|
+
return;
|
|
362
|
+
case "openclaw.canvasReminderEvery":
|
|
363
|
+
delete bridge.canvasReminderEvery;
|
|
364
|
+
return;
|
|
365
|
+
case "openclaw.deliver":
|
|
366
|
+
delete bridge.deliver;
|
|
367
|
+
return;
|
|
368
|
+
case "openclaw.deliverChannel":
|
|
369
|
+
delete bridge.deliverChannel;
|
|
370
|
+
return;
|
|
371
|
+
case "openclaw.replyTo":
|
|
372
|
+
delete bridge.replyTo;
|
|
373
|
+
return;
|
|
374
|
+
case "openclaw.deliverTimeoutMs":
|
|
375
|
+
delete bridge.deliverTimeoutMs;
|
|
376
|
+
return;
|
|
377
|
+
case "openclaw.attachmentDir":
|
|
378
|
+
delete bridge.attachmentDir;
|
|
379
|
+
return;
|
|
380
|
+
case "openclaw.attachmentMaxBytes":
|
|
381
|
+
delete bridge.attachmentMaxBytes;
|
|
382
|
+
return;
|
|
383
|
+
case "telegram.botToken":
|
|
384
|
+
delete telegram.botToken;
|
|
385
|
+
delete telegram.botUsername;
|
|
386
|
+
delete telegram.hasMainWebApp;
|
|
387
|
+
return;
|
|
388
|
+
default:
|
|
389
|
+
throw new Error(`Unknown config key for --unset: ${key}`);
|
|
390
|
+
}
|
|
271
391
|
}
|
|
272
|
-
function
|
|
273
|
-
|
|
274
|
-
return `${base.replace(/\/$/, "")}/t/${tunnelId}`;
|
|
392
|
+
function hasValues(obj) {
|
|
393
|
+
return Object.values(obj).some((value) => value !== void 0);
|
|
275
394
|
}
|
|
276
|
-
function
|
|
277
|
-
|
|
278
|
-
return
|
|
395
|
+
function maskSecret(value) {
|
|
396
|
+
if (value.length <= 8) return "********";
|
|
397
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
279
398
|
}
|
|
280
|
-
function
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
return content.slice(-maxChars);
|
|
286
|
-
} catch {
|
|
287
|
-
return null;
|
|
399
|
+
async function telegramGetMe(token) {
|
|
400
|
+
const resp = await fetch(`https://api.telegram.org/bot${token}/getMe`);
|
|
401
|
+
const data = await resp.json();
|
|
402
|
+
if (!data.ok || !data.result?.username) {
|
|
403
|
+
throw new Error(data.description ?? "Invalid bot token");
|
|
288
404
|
}
|
|
405
|
+
return {
|
|
406
|
+
username: data.result.username,
|
|
407
|
+
hasMainWebApp: data.result.has_main_web_app === true
|
|
408
|
+
};
|
|
289
409
|
}
|
|
290
|
-
function
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
410
|
+
async function telegramSetMenuButton(token, url) {
|
|
411
|
+
const resp = await fetch(`https://api.telegram.org/bot${token}/setChatMenuButton`, {
|
|
412
|
+
method: "POST",
|
|
413
|
+
headers: { "Content-Type": "application/json" },
|
|
414
|
+
body: JSON.stringify({
|
|
415
|
+
menu_button: { type: "web_app", text: "Open", web_app: { url } }
|
|
416
|
+
})
|
|
417
|
+
});
|
|
418
|
+
const data = await resp.json();
|
|
419
|
+
if (!data.ok) {
|
|
420
|
+
throw new Error(data.description ?? "setChatMenuButton failed");
|
|
296
421
|
}
|
|
297
|
-
return error instanceof Error ? error.message : String(error);
|
|
298
422
|
}
|
|
299
|
-
|
|
300
|
-
if (!
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
423
|
+
function printConfigSummary(saved) {
|
|
424
|
+
if (!saved) {
|
|
425
|
+
console.log("Saved config: none");
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
console.log("Saved config:");
|
|
429
|
+
console.log(` apiKey: ${maskSecret(saved.apiKey)}`);
|
|
430
|
+
if (saved.bridge && hasValues(saved.bridge)) {
|
|
431
|
+
console.log(` bridge.mode: ${saved.bridge.mode ?? "(unset)"}`);
|
|
432
|
+
if (saved.bridge.openclawPath) console.log(` openclaw.path: ${saved.bridge.openclawPath}`);
|
|
433
|
+
if (saved.bridge.sessionId) console.log(` openclaw.sessionId: ${saved.bridge.sessionId}`);
|
|
434
|
+
if (saved.bridge.threadId) console.log(` openclaw.threadId: ${saved.bridge.threadId}`);
|
|
435
|
+
if (saved.bridge.canvasReminderEvery !== void 0)
|
|
436
|
+
console.log(` openclaw.canvasReminderEvery: ${saved.bridge.canvasReminderEvery}`);
|
|
437
|
+
if (saved.bridge.deliver !== void 0)
|
|
438
|
+
console.log(` openclaw.deliver: ${saved.bridge.deliver ? "true" : "false"}`);
|
|
439
|
+
if (saved.bridge.deliverChannel)
|
|
440
|
+
console.log(` openclaw.deliverChannel: ${saved.bridge.deliverChannel}`);
|
|
441
|
+
if (saved.bridge.replyTo) console.log(` openclaw.replyTo: ${saved.bridge.replyTo}`);
|
|
442
|
+
if (saved.bridge.deliverTimeoutMs !== void 0)
|
|
443
|
+
console.log(` openclaw.deliverTimeoutMs: ${saved.bridge.deliverTimeoutMs}`);
|
|
444
|
+
if (saved.bridge.attachmentDir)
|
|
445
|
+
console.log(` openclaw.attachmentDir: ${saved.bridge.attachmentDir}`);
|
|
446
|
+
if (saved.bridge.attachmentMaxBytes !== void 0)
|
|
447
|
+
console.log(` openclaw.attachmentMaxBytes: ${saved.bridge.attachmentMaxBytes}`);
|
|
448
|
+
} else {
|
|
449
|
+
console.log(" bridge: none");
|
|
450
|
+
}
|
|
451
|
+
if (saved.telegram?.botToken && saved.telegram.botUsername) {
|
|
452
|
+
console.log(` telegram.botToken: ${maskSecret(saved.telegram.botToken)}`);
|
|
453
|
+
console.log(` telegram.botUsername: @${saved.telegram.botUsername}`);
|
|
454
|
+
if (!saved.telegram.hasMainWebApp) {
|
|
455
|
+
console.log(" INFO: Register Mini App in @BotFather for deep links to open in Telegram");
|
|
456
|
+
}
|
|
457
|
+
} else if (saved.telegram?.botToken) {
|
|
458
|
+
console.log(` telegram.botToken: ${maskSecret(saved.telegram.botToken)}`);
|
|
459
|
+
console.log(" telegram.botUsername: (not resolved)");
|
|
460
|
+
} else {
|
|
461
|
+
console.log(" telegram: not configured");
|
|
462
|
+
console.log(" INFO: Set telegram.botToken to enable Telegram Mini App links");
|
|
463
|
+
console.log(" Example: pubblue configure --set telegram.botToken=<BOT_TOKEN>");
|
|
307
464
|
}
|
|
308
465
|
}
|
|
309
|
-
function
|
|
310
|
-
|
|
311
|
-
|
|
466
|
+
function registerConfigureCommand(program2) {
|
|
467
|
+
program2.command("configure").description("Configure the CLI with your API key").option("--api-key <key>", "Your API key (less secure: appears in shell history)").option("--api-key-stdin", "Read API key from stdin").option(
|
|
468
|
+
"--set <key=value>",
|
|
469
|
+
"Set config key (repeatable). Example: --set telegram.botToken=<token>",
|
|
470
|
+
collectValues,
|
|
471
|
+
[]
|
|
472
|
+
).option("--unset <key>", "Unset config key (repeatable)", collectValues, []).option("--show", "Show saved configuration").action(
|
|
312
473
|
async (opts) => {
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
322
|
-
process.exit(1);
|
|
474
|
+
const saved = loadConfig();
|
|
475
|
+
const hasApiUpdate = Boolean(opts.apiKey || opts.apiKeyStdin);
|
|
476
|
+
const hasSet = opts.set.length > 0;
|
|
477
|
+
const hasUnset = opts.unset.length > 0;
|
|
478
|
+
const hasMutation = hasApiUpdate || hasSet || hasUnset;
|
|
479
|
+
if (!hasMutation && opts.show) {
|
|
480
|
+
printConfigSummary(saved);
|
|
481
|
+
return;
|
|
323
482
|
}
|
|
324
|
-
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
const existing = await apiClient.get(opts.tunnel);
|
|
328
|
-
if (existing.status === "closed" || existing.expiresAt <= Date.now()) {
|
|
329
|
-
console.error(`Tunnel ${opts.tunnel} is closed or expired.`);
|
|
330
|
-
process.exit(1);
|
|
331
|
-
}
|
|
332
|
-
target = {
|
|
333
|
-
createdNew: false,
|
|
334
|
-
expiresAt: existing.expiresAt,
|
|
335
|
-
mode: "existing",
|
|
336
|
-
tunnelId: existing.tunnelId,
|
|
337
|
-
url: getPublicTunnelUrl(existing.tunnelId)
|
|
338
|
-
};
|
|
339
|
-
} catch (error) {
|
|
340
|
-
console.error(`Failed to use tunnel ${opts.tunnel}: ${formatApiError(error)}`);
|
|
341
|
-
process.exit(1);
|
|
342
|
-
}
|
|
343
|
-
} else if (!opts.new) {
|
|
344
|
-
try {
|
|
345
|
-
const listed = await apiClient.list();
|
|
346
|
-
const active = listed.filter((t) => t.status === "active" && t.expiresAt > Date.now()).sort((a, b) => b.createdAt - a.createdAt);
|
|
347
|
-
const reusable = pickReusableTunnel(listed);
|
|
348
|
-
if (reusable) {
|
|
349
|
-
target = {
|
|
350
|
-
createdNew: false,
|
|
351
|
-
expiresAt: reusable.expiresAt,
|
|
352
|
-
mode: "existing",
|
|
353
|
-
tunnelId: reusable.tunnelId,
|
|
354
|
-
url: getPublicTunnelUrl(reusable.tunnelId)
|
|
355
|
-
};
|
|
356
|
-
if (active.length > 1) {
|
|
357
|
-
console.error(
|
|
358
|
-
[
|
|
359
|
-
`Multiple active tunnels found: ${active.map((t) => t.tunnelId).join(", ")}`,
|
|
360
|
-
`Reusing most recent active tunnel ${reusable.tunnelId}.`,
|
|
361
|
-
"Use --tunnel <id> to choose explicitly or --new to force creation."
|
|
362
|
-
].join("\n")
|
|
363
|
-
);
|
|
364
|
-
} else {
|
|
365
|
-
console.error(
|
|
366
|
-
`Reusing existing active tunnel ${reusable.tunnelId}. Use --new to force creation.`
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
} catch (error) {
|
|
371
|
-
console.error(`Failed to list tunnels for reuse check: ${formatApiError(error)}`);
|
|
372
|
-
process.exit(1);
|
|
373
|
-
}
|
|
483
|
+
let apiKey = saved?.apiKey;
|
|
484
|
+
if (hasApiUpdate || !hasMutation) {
|
|
485
|
+
apiKey = await resolveConfigureApiKey(opts);
|
|
374
486
|
}
|
|
375
|
-
if (!
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
mode: "created",
|
|
384
|
-
tunnelId: created.tunnelId,
|
|
385
|
-
url: created.url
|
|
386
|
-
};
|
|
387
|
-
} catch (error) {
|
|
388
|
-
console.error(`Failed to create tunnel: ${formatApiError(error)}`);
|
|
389
|
-
process.exit(1);
|
|
487
|
+
if (!apiKey) {
|
|
488
|
+
const envKey = process.env.PUBBLUE_API_KEY?.trim();
|
|
489
|
+
if (envKey) {
|
|
490
|
+
apiKey = envKey;
|
|
491
|
+
} else {
|
|
492
|
+
throw new Error(
|
|
493
|
+
"No API key available. Provide --api-key/--api-key-stdin (or run plain `pubblue configure` first)."
|
|
494
|
+
);
|
|
390
495
|
}
|
|
391
496
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
497
|
+
const nextBridge = { ...saved?.bridge ?? {} };
|
|
498
|
+
const nextTelegram = { ...saved?.telegram ?? {} };
|
|
499
|
+
let telegramTokenChanged = false;
|
|
500
|
+
for (const entry of opts.set) {
|
|
501
|
+
const { key, value } = parseSetInput(entry);
|
|
502
|
+
applyConfigSet(nextBridge, nextTelegram, key, value);
|
|
503
|
+
if (key === "telegram.botToken") telegramTokenChanged = true;
|
|
395
504
|
}
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
console.log(
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
await startDaemon({
|
|
413
|
-
tunnelId: target.tunnelId,
|
|
414
|
-
apiClient,
|
|
415
|
-
socketPath,
|
|
416
|
-
infoPath
|
|
417
|
-
});
|
|
418
|
-
} catch (error) {
|
|
419
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
420
|
-
console.error(`Daemon failed: ${message}`);
|
|
421
|
-
process.exit(1);
|
|
422
|
-
}
|
|
423
|
-
} else {
|
|
424
|
-
if (isDaemonRunning(target.tunnelId)) {
|
|
425
|
-
try {
|
|
426
|
-
const status = await ipcCall(socketPath, { method: "status", params: {} });
|
|
427
|
-
if (!status.ok) throw new Error(String(status.error || "status check failed"));
|
|
428
|
-
} catch (error) {
|
|
429
|
-
console.error(
|
|
430
|
-
`Daemon process exists but is not responding: ${error instanceof Error ? error.message : String(error)}`
|
|
431
|
-
);
|
|
432
|
-
console.error("Run `pubblue tunnel close <id>` and start again.");
|
|
433
|
-
process.exit(1);
|
|
434
|
-
}
|
|
435
|
-
if (bridgeMode !== "none") {
|
|
436
|
-
const bridgeReady = await ensureBridgeReady({
|
|
437
|
-
bridgeMode,
|
|
438
|
-
tunnelId: target.tunnelId,
|
|
439
|
-
socketPath,
|
|
440
|
-
bridgeProcessEnv,
|
|
441
|
-
timeoutMs: 8e3
|
|
442
|
-
});
|
|
443
|
-
if (!bridgeReady.ok) {
|
|
444
|
-
console.error(
|
|
445
|
-
`Bridge failed to start for running tunnel: ${bridgeReady.reason ?? "unknown reason"}`
|
|
446
|
-
);
|
|
447
|
-
const existingBridgeLog = bridgeLogPath(target.tunnelId);
|
|
448
|
-
if (fs2.existsSync(existingBridgeLog)) {
|
|
449
|
-
console.error(`Bridge log: ${existingBridgeLog}`);
|
|
450
|
-
const bridgeTail = readLogTail(existingBridgeLog);
|
|
451
|
-
if (bridgeTail) {
|
|
452
|
-
console.error("---- bridge log tail ----");
|
|
453
|
-
console.error(bridgeTail.trimEnd());
|
|
454
|
-
console.error("---- end bridge log tail ----");
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
process.exit(1);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
console.log(`Tunnel started: ${target.url}`);
|
|
461
|
-
console.log(`Tunnel ID: ${target.tunnelId}`);
|
|
462
|
-
console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
|
|
463
|
-
console.log("Daemon already running for this tunnel.");
|
|
464
|
-
console.log(`Daemon log: ${logPath}`);
|
|
465
|
-
if (bridgeMode !== "none") {
|
|
466
|
-
console.log("Bridge mode: openclaw");
|
|
467
|
-
console.log(`Bridge log: ${bridgeLogPath(target.tunnelId)}`);
|
|
468
|
-
}
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
const daemonScript = path2.join(import.meta.dirname, "tunnel-daemon-entry.js");
|
|
472
|
-
const config = getConfig();
|
|
473
|
-
const daemonLogFd = fs2.openSync(logPath, "a");
|
|
474
|
-
const child = fork(daemonScript, [], {
|
|
475
|
-
detached: true,
|
|
476
|
-
stdio: buildDaemonForkStdio(daemonLogFd),
|
|
477
|
-
env: {
|
|
478
|
-
...process.env,
|
|
479
|
-
PUBBLUE_DAEMON_TUNNEL_ID: target.tunnelId,
|
|
480
|
-
PUBBLUE_DAEMON_BASE_URL: config.baseUrl,
|
|
481
|
-
PUBBLUE_DAEMON_API_KEY: config.apiKey,
|
|
482
|
-
PUBBLUE_DAEMON_SOCKET: socketPath,
|
|
483
|
-
PUBBLUE_DAEMON_INFO: infoPath
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
fs2.closeSync(daemonLogFd);
|
|
487
|
-
if (child.connected) {
|
|
488
|
-
child.disconnect();
|
|
489
|
-
}
|
|
490
|
-
child.unref();
|
|
491
|
-
console.log(`Starting daemon for tunnel ${target.tunnelId}...`);
|
|
492
|
-
const ready = await waitForDaemonReady({
|
|
493
|
-
child,
|
|
494
|
-
infoPath,
|
|
495
|
-
socketPath,
|
|
496
|
-
timeoutMs: 8e3
|
|
497
|
-
});
|
|
498
|
-
if (!ready.ok) {
|
|
499
|
-
console.error(`Daemon failed to start: ${ready.reason ?? "unknown reason"}`);
|
|
500
|
-
console.error(`Daemon log: ${logPath}`);
|
|
501
|
-
const tail = readLogTail(logPath);
|
|
502
|
-
if (tail) {
|
|
503
|
-
console.error("---- daemon log tail ----");
|
|
504
|
-
console.error(tail.trimEnd());
|
|
505
|
-
console.error("---- end daemon log tail ----");
|
|
506
|
-
}
|
|
507
|
-
await cleanupCreatedTunnelOnStartFailure(apiClient, target);
|
|
508
|
-
process.exit(1);
|
|
509
|
-
}
|
|
510
|
-
const offerReady = await waitForAgentOffer({
|
|
511
|
-
apiClient,
|
|
512
|
-
tunnelId: target.tunnelId,
|
|
513
|
-
timeoutMs: 5e3
|
|
514
|
-
});
|
|
515
|
-
if (!offerReady.ok) {
|
|
516
|
-
console.error(`Daemon started but signaling is not ready: ${offerReady.reason}`);
|
|
517
|
-
console.error(`Daemon log: ${logPath}`);
|
|
518
|
-
const tail = readLogTail(logPath);
|
|
519
|
-
if (tail) {
|
|
520
|
-
console.error("---- daemon log tail ----");
|
|
521
|
-
console.error(tail.trimEnd());
|
|
522
|
-
console.error("---- end daemon log tail ----");
|
|
523
|
-
}
|
|
524
|
-
await cleanupCreatedTunnelOnStartFailure(apiClient, target);
|
|
525
|
-
process.exit(1);
|
|
526
|
-
}
|
|
527
|
-
if (bridgeMode !== "none") {
|
|
528
|
-
const bridgeReady = await ensureBridgeReady({
|
|
529
|
-
bridgeMode,
|
|
530
|
-
tunnelId: target.tunnelId,
|
|
531
|
-
socketPath,
|
|
532
|
-
bridgeProcessEnv,
|
|
533
|
-
timeoutMs: 8e3
|
|
534
|
-
});
|
|
535
|
-
if (!bridgeReady.ok) {
|
|
536
|
-
console.error(`Bridge failed to start: ${bridgeReady.reason ?? "unknown reason"}`);
|
|
537
|
-
const bridgeLog = bridgeLogPath(target.tunnelId);
|
|
538
|
-
if (fs2.existsSync(bridgeLog)) {
|
|
539
|
-
console.error(`Bridge log: ${bridgeLog}`);
|
|
540
|
-
const bridgeTail = readLogTail(bridgeLog);
|
|
541
|
-
if (bridgeTail) {
|
|
542
|
-
console.error("---- bridge log tail ----");
|
|
543
|
-
console.error(bridgeTail.trimEnd());
|
|
544
|
-
console.error("---- end bridge log tail ----");
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
try {
|
|
548
|
-
await ipcCall(socketPath, { method: "close", params: {} });
|
|
549
|
-
} catch {
|
|
550
|
-
}
|
|
551
|
-
await cleanupCreatedTunnelOnStartFailure(apiClient, target);
|
|
552
|
-
process.exit(1);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
console.log(`Tunnel started: ${target.url}`);
|
|
556
|
-
console.log(`Tunnel ID: ${target.tunnelId}`);
|
|
557
|
-
console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
|
|
558
|
-
if (target.mode === "existing") console.log("Mode: attached existing tunnel");
|
|
559
|
-
console.log("Daemon health: OK");
|
|
560
|
-
console.log(`Daemon log: ${logPath}`);
|
|
561
|
-
if (bridgeMode !== "none") {
|
|
562
|
-
console.log("Bridge mode: openclaw");
|
|
563
|
-
console.log(`Bridge log: ${bridgeLogPath(target.tunnelId)}`);
|
|
505
|
+
for (const key of opts.unset) {
|
|
506
|
+
applyConfigUnset(nextBridge, nextTelegram, key.trim());
|
|
507
|
+
}
|
|
508
|
+
if (telegramTokenChanged && nextTelegram.botToken) {
|
|
509
|
+
console.log("Verifying Telegram bot token...");
|
|
510
|
+
const bot = await telegramGetMe(nextTelegram.botToken);
|
|
511
|
+
nextTelegram.botUsername = bot.username;
|
|
512
|
+
nextTelegram.hasMainWebApp = bot.hasMainWebApp;
|
|
513
|
+
console.log(` Bot: @${bot.username}`);
|
|
514
|
+
await telegramSetMenuButton(nextTelegram.botToken, "https://pub.blue");
|
|
515
|
+
console.log(" Menu button set to https://pub.blue");
|
|
516
|
+
if (!bot.hasMainWebApp) {
|
|
517
|
+
console.log("");
|
|
518
|
+
console.log(" INFO: For deep links to open inside Telegram, register the Mini App:");
|
|
519
|
+
console.log(" @BotFather \u2192 /mybots \u2192 your bot \u2192 Bot Settings \u2192 Configure Mini App");
|
|
520
|
+
console.log(" Set Web App URL to: https://pub.blue");
|
|
564
521
|
}
|
|
565
522
|
}
|
|
523
|
+
const nextConfig = {
|
|
524
|
+
apiKey,
|
|
525
|
+
bridge: hasValues(nextBridge) ? nextBridge : void 0,
|
|
526
|
+
telegram: hasValues(nextTelegram) ? nextTelegram : void 0
|
|
527
|
+
};
|
|
528
|
+
saveConfig(nextConfig);
|
|
529
|
+
console.log("Configuration saved.");
|
|
530
|
+
if (opts.show || hasSet || hasUnset) {
|
|
531
|
+
printConfigSummary(nextConfig);
|
|
532
|
+
}
|
|
566
533
|
}
|
|
567
534
|
);
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
data: bytes.toString("utf-8"),
|
|
582
|
-
meta: { title: filename, filename, mime: getMimeType(filePath), size: bytes.length }
|
|
583
|
-
};
|
|
584
|
-
} else if (TEXT_FILE_EXTENSIONS.has(ext)) {
|
|
585
|
-
msg = {
|
|
586
|
-
id: generateMessageId(),
|
|
587
|
-
type: "text",
|
|
588
|
-
data: bytes.toString("utf-8"),
|
|
589
|
-
meta: { filename, mime: getMimeType(filePath), size: bytes.length }
|
|
590
|
-
};
|
|
591
|
-
} else {
|
|
592
|
-
msg = {
|
|
593
|
-
id: generateMessageId(),
|
|
594
|
-
type: "binary",
|
|
595
|
-
meta: { filename, mime: getMimeType(filePath), size: bytes.length }
|
|
596
|
-
};
|
|
597
|
-
binaryBase64 = bytes.toString("base64");
|
|
598
|
-
}
|
|
599
|
-
} else if (messageArg) {
|
|
600
|
-
msg = {
|
|
601
|
-
id: generateMessageId(),
|
|
602
|
-
type: "text",
|
|
603
|
-
data: messageArg
|
|
604
|
-
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/commands/publications.ts
|
|
538
|
+
function registerPublicationCommands(program2) {
|
|
539
|
+
program2.command("create").description("Create a new publication").argument("[file]", "Path to the file (reads stdin if omitted)").option("--slug <slug>", "Custom slug for the URL").option("--title <title>", "Title for the publication").option("--public", "Make the publication public").option("--private", "Make the publication private (default)").option("--expires <duration>", "Auto-delete after duration (e.g. 1h, 24h, 7d)").action(
|
|
540
|
+
async (fileArg, opts) => {
|
|
541
|
+
const client = createClient();
|
|
542
|
+
let content;
|
|
543
|
+
let filename;
|
|
544
|
+
if (fileArg) {
|
|
545
|
+
const file = readFile(fileArg);
|
|
546
|
+
content = file.content;
|
|
547
|
+
filename = file.basename;
|
|
605
548
|
} else {
|
|
606
|
-
|
|
607
|
-
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
608
|
-
msg = {
|
|
609
|
-
id: generateMessageId(),
|
|
610
|
-
type: "text",
|
|
611
|
-
data: Buffer.concat(chunks).toString("utf-8").trim()
|
|
612
|
-
};
|
|
549
|
+
content = await readFromStdin();
|
|
613
550
|
}
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
params: { channel: opts.channel, msg, binaryBase64 }
|
|
551
|
+
const resolvedVisibility = resolveVisibilityFlags({
|
|
552
|
+
public: opts.public,
|
|
553
|
+
private: opts.private,
|
|
554
|
+
commandName: "create"
|
|
619
555
|
});
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
if (
|
|
632
|
-
|
|
633
|
-
console.error(
|
|
634
|
-
"Following chat channel by default. Use `--all` to include binary/file channels."
|
|
635
|
-
);
|
|
636
|
-
}
|
|
637
|
-
let consecutiveFailures = 0;
|
|
638
|
-
let warnedDisconnected = false;
|
|
639
|
-
while (true) {
|
|
640
|
-
try {
|
|
641
|
-
const response = await ipcCall(socketPath, {
|
|
642
|
-
method: "read",
|
|
643
|
-
params: { channel: readChannel }
|
|
644
|
-
});
|
|
645
|
-
if (warnedDisconnected) {
|
|
646
|
-
console.error("Daemon reconnected.");
|
|
647
|
-
warnedDisconnected = false;
|
|
648
|
-
}
|
|
649
|
-
consecutiveFailures = 0;
|
|
650
|
-
if (response.messages && response.messages.length > 0) {
|
|
651
|
-
for (const m of response.messages) {
|
|
652
|
-
console.log(JSON.stringify(m));
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
} catch (error) {
|
|
656
|
-
consecutiveFailures += 1;
|
|
657
|
-
if (!warnedDisconnected) {
|
|
658
|
-
const detail = error instanceof Error ? ` ${error.message}` : "";
|
|
659
|
-
console.error(`Daemon disconnected. Waiting for recovery...${detail}`);
|
|
660
|
-
warnedDisconnected = true;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
const delayMs = getFollowReadDelayMs(warnedDisconnected, consecutiveFailures);
|
|
664
|
-
await new Promise((r) => setTimeout(r, delayMs));
|
|
665
|
-
}
|
|
666
|
-
} else {
|
|
667
|
-
const response = await ipcCall(socketPath, {
|
|
668
|
-
method: "read",
|
|
669
|
-
params: { channel: readChannel }
|
|
670
|
-
});
|
|
671
|
-
if (!response.ok) {
|
|
672
|
-
console.error(`Failed: ${response.error}`);
|
|
673
|
-
process.exit(1);
|
|
674
|
-
}
|
|
675
|
-
console.log(JSON.stringify(response.messages || [], null, 2));
|
|
556
|
+
const result = await client.create({
|
|
557
|
+
content,
|
|
558
|
+
filename,
|
|
559
|
+
title: opts.title,
|
|
560
|
+
slug: opts.slug,
|
|
561
|
+
isPublic: resolvedVisibility ?? false,
|
|
562
|
+
expiresIn: opts.expires
|
|
563
|
+
});
|
|
564
|
+
console.log(`Created: ${result.url}`);
|
|
565
|
+
const tmaUrl = getTelegramMiniAppUrl("pub", result.slug);
|
|
566
|
+
if (tmaUrl) console.log(`Telegram: ${tmaUrl}`);
|
|
567
|
+
if (result.expiresAt) {
|
|
568
|
+
console.log(` Expires: ${new Date(result.expiresAt).toISOString()}`);
|
|
676
569
|
}
|
|
677
570
|
}
|
|
678
571
|
);
|
|
679
|
-
|
|
680
|
-
const
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
console.log(` ${ch.name} [${ch.direction}]`);
|
|
686
|
-
}
|
|
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
|
+
return;
|
|
687
578
|
}
|
|
579
|
+
console.log(` Slug: ${pub.slug}`);
|
|
580
|
+
console.log(` Type: ${pub.contentType}`);
|
|
581
|
+
if (pub.title) console.log(` Title: ${pub.title}`);
|
|
582
|
+
console.log(` Status: ${formatVisibility(pub.isPublic)}`);
|
|
583
|
+
if (pub.expiresAt) console.log(` Expires: ${new Date(pub.expiresAt).toISOString()}`);
|
|
584
|
+
console.log(` Created: ${new Date(pub.createdAt).toLocaleDateString()}`);
|
|
585
|
+
console.log(` Updated: ${new Date(pub.updatedAt).toLocaleDateString()}`);
|
|
586
|
+
console.log(` Size: ${pub.content.length} bytes`);
|
|
688
587
|
});
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
if (typeof response.lastError === "string" && response.lastError.length > 0) {
|
|
699
|
-
console.log(` Last error: ${response.lastError}`);
|
|
700
|
-
}
|
|
701
|
-
const logPath = tunnelLogPath(tunnelId);
|
|
702
|
-
if (fs2.existsSync(logPath)) {
|
|
703
|
-
console.log(` Log: ${logPath}`);
|
|
704
|
-
}
|
|
705
|
-
const bridgeInfo = readBridgeProcessInfo(tunnelId);
|
|
706
|
-
if (bridgeInfo) {
|
|
707
|
-
const bridgeRunning = isBridgeRunning(tunnelId);
|
|
708
|
-
const bridgeState = bridgeInfo.status || (bridgeRunning ? "running" : "stopped");
|
|
709
|
-
console.log(` Bridge: ${bridgeInfo.mode} (${bridgeState})`);
|
|
710
|
-
if (bridgeInfo.sessionId) {
|
|
711
|
-
console.log(` Bridge session: ${bridgeInfo.sessionId}`);
|
|
712
|
-
}
|
|
713
|
-
if (!bridgeRunning && bridgeInfo.lastError) {
|
|
714
|
-
console.log(` Bridge error: ${bridgeInfo.lastError}`);
|
|
588
|
+
program2.command("update").description("Update a publication's content and/or metadata").argument("<slug>", "Slug of the publication to update").option("--file <file>", "New content from file").option("--title <title>", "New title").option("--public", "Make the publication public").option("--private", "Make the publication private").option("--slug <newSlug>", "Rename the slug").action(
|
|
589
|
+
async (slug, opts) => {
|
|
590
|
+
const client = createClient();
|
|
591
|
+
let content;
|
|
592
|
+
let filename;
|
|
593
|
+
if (opts.file) {
|
|
594
|
+
const file = readFile(opts.file);
|
|
595
|
+
content = file.content;
|
|
596
|
+
filename = file.basename;
|
|
715
597
|
}
|
|
598
|
+
const isPublic = resolveVisibilityFlags({
|
|
599
|
+
public: opts.public,
|
|
600
|
+
private: opts.private,
|
|
601
|
+
commandName: "update"
|
|
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)}`);
|
|
716
614
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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;
|
|
622
|
+
}
|
|
623
|
+
for (const pub of pubs) {
|
|
624
|
+
const date = new Date(pub.createdAt).toLocaleDateString();
|
|
625
|
+
const expires = pub.expiresAt ? ` expires:${new Date(pub.expiresAt).toISOString()}` : "";
|
|
626
|
+
console.log(
|
|
627
|
+
` ${pub.slug} [${pub.contentType}] ${formatVisibility(pub.isPublic)} ${date}${expires}`
|
|
628
|
+
);
|
|
720
629
|
}
|
|
721
630
|
});
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const tunnelId = opts.tunnel || await resolveActiveTunnel();
|
|
727
|
-
const socketPath = getSocketPath(tunnelId);
|
|
728
|
-
const apiClient = createApiClient();
|
|
729
|
-
const fail = (message) => {
|
|
730
|
-
console.error(`Doctor failed: ${message}`);
|
|
731
|
-
process.exit(1);
|
|
732
|
-
};
|
|
733
|
-
console.log(`Doctor tunnel: ${tunnelId}`);
|
|
734
|
-
let statusResponse = null;
|
|
735
|
-
try {
|
|
736
|
-
statusResponse = await ipcCall(socketPath, {
|
|
737
|
-
method: "status",
|
|
738
|
-
params: {}
|
|
739
|
-
});
|
|
740
|
-
} catch (error) {
|
|
741
|
-
fail(
|
|
742
|
-
`daemon is unreachable (${error instanceof Error ? error.message : String(error)}).`
|
|
743
|
-
);
|
|
744
|
-
}
|
|
745
|
-
if (!statusResponse) {
|
|
746
|
-
fail("daemon status returned no response.");
|
|
747
|
-
}
|
|
748
|
-
const daemonStatus = statusResponse;
|
|
749
|
-
if (!daemonStatus.ok) {
|
|
750
|
-
fail(`daemon returned non-ok status: ${String(daemonStatus.error || "unknown error")}`);
|
|
751
|
-
}
|
|
752
|
-
if (!daemonStatus.connected) {
|
|
753
|
-
fail("daemon is running but browser is not connected.");
|
|
754
|
-
}
|
|
755
|
-
const channelNames = Array.isArray(daemonStatus.channels) ? daemonStatus.channels.map((entry) => String(entry)) : [];
|
|
756
|
-
for (const required of [CONTROL_CHANNEL, CHANNELS.CHAT, CHANNELS.CANVAS]) {
|
|
757
|
-
if (!channelNames.includes(required)) {
|
|
758
|
-
fail(`required channel is missing: ${required}`);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
console.log("Daemon/channel check: OK");
|
|
762
|
-
let tunnelInfo = null;
|
|
763
|
-
try {
|
|
764
|
-
tunnelInfo = await apiClient.get(tunnelId);
|
|
765
|
-
} catch (error) {
|
|
766
|
-
fail(`failed to fetch tunnel info from API: ${formatApiError(error)}`);
|
|
767
|
-
}
|
|
768
|
-
if (!tunnelInfo) {
|
|
769
|
-
fail("API returned no tunnel payload.");
|
|
770
|
-
}
|
|
771
|
-
const apiTunnel = tunnelInfo;
|
|
772
|
-
if (apiTunnel.status !== "active") {
|
|
773
|
-
fail(`API reports tunnel is not active (status: ${apiTunnel.status})`);
|
|
774
|
-
}
|
|
775
|
-
if (apiTunnel.expiresAt <= Date.now()) {
|
|
776
|
-
fail("API reports tunnel is expired.");
|
|
777
|
-
}
|
|
778
|
-
if (!apiTunnel.hasConnection) {
|
|
779
|
-
fail("API reports no browser connection.");
|
|
780
|
-
}
|
|
781
|
-
if (typeof apiTunnel.agentOffer !== "string" || apiTunnel.agentOffer.length === 0) {
|
|
782
|
-
fail("agent offer was not published.");
|
|
783
|
-
}
|
|
784
|
-
console.log("API/signaling check: OK");
|
|
785
|
-
if (!opts.skipChat) {
|
|
786
|
-
const pingText = "This is a ping test. Reply with 'pong'.";
|
|
787
|
-
const pingMsg = {
|
|
788
|
-
id: generateMessageId(),
|
|
789
|
-
type: "text",
|
|
790
|
-
data: pingText
|
|
791
|
-
};
|
|
792
|
-
const writeResponse = await ipcCall(socketPath, {
|
|
793
|
-
method: "write",
|
|
794
|
-
params: { channel: CHANNELS.CHAT, msg: pingMsg }
|
|
795
|
-
});
|
|
796
|
-
if (!writeResponse.ok) {
|
|
797
|
-
fail(`chat ping failed: ${String(writeResponse.error || "unknown write error")}`);
|
|
798
|
-
}
|
|
799
|
-
console.log("Chat ping write ACK: OK");
|
|
800
|
-
if (opts.waitPong) {
|
|
801
|
-
const startedAt = Date.now();
|
|
802
|
-
let receivedPong = false;
|
|
803
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
804
|
-
const readResponse = await ipcCall(socketPath, {
|
|
805
|
-
method: "read",
|
|
806
|
-
params: { channel: CHANNELS.CHAT }
|
|
807
|
-
});
|
|
808
|
-
if (!readResponse.ok) {
|
|
809
|
-
fail(
|
|
810
|
-
`chat read failed while waiting for pong: ${String(readResponse.error || "unknown read error")}`
|
|
811
|
-
);
|
|
812
|
-
}
|
|
813
|
-
const messages = Array.isArray(readResponse.messages) ? readResponse.messages : [];
|
|
814
|
-
if (messages.some((entry) => messageContainsPong(entry))) {
|
|
815
|
-
receivedPong = true;
|
|
816
|
-
break;
|
|
817
|
-
}
|
|
818
|
-
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
819
|
-
}
|
|
820
|
-
if (!receivedPong) {
|
|
821
|
-
fail(
|
|
822
|
-
`timed out after ${timeoutSeconds}s waiting for exact 'pong' reply on chat channel.`
|
|
823
|
-
);
|
|
824
|
-
}
|
|
825
|
-
console.log("Chat pong roundtrip: OK");
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
if (!opts.skipCanvas) {
|
|
829
|
-
const stamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
830
|
-
const canvasMsg = {
|
|
831
|
-
id: generateMessageId(),
|
|
832
|
-
type: "html",
|
|
833
|
-
data: `<!doctype html><html><body style="margin:0;padding:24px;font-family:system-ui;background:#111;color:#f5f5f5">Canvas ping OK<br><small>${stamp}</small></body></html>`
|
|
834
|
-
};
|
|
835
|
-
const canvasResponse = await ipcCall(socketPath, {
|
|
836
|
-
method: "write",
|
|
837
|
-
params: { channel: CHANNELS.CANVAS, msg: canvasMsg }
|
|
838
|
-
});
|
|
839
|
-
if (!canvasResponse.ok) {
|
|
840
|
-
fail(`canvas ping failed: ${String(canvasResponse.error || "unknown write error")}`);
|
|
841
|
-
}
|
|
842
|
-
console.log("Canvas ping write ACK: OK");
|
|
843
|
-
}
|
|
844
|
-
console.log("Tunnel doctor: PASS");
|
|
845
|
-
}
|
|
846
|
-
);
|
|
847
|
-
tunnel.command("list").description("List active tunnels").action(async () => {
|
|
848
|
-
const apiClient = createApiClient();
|
|
849
|
-
const tunnels = await apiClient.list();
|
|
850
|
-
if (tunnels.length === 0) {
|
|
851
|
-
console.log("No active tunnels.");
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
|
-
for (const t of tunnels) {
|
|
855
|
-
const age = Math.floor((Date.now() - t.createdAt) / 6e4);
|
|
856
|
-
const running = isDaemonRunning(t.tunnelId) ? "running" : "no daemon";
|
|
857
|
-
const bridgeInfo = readBridgeProcessInfo(t.tunnelId);
|
|
858
|
-
const bridge = bridgeInfo ? isBridgeRunning(t.tunnelId) ? `${bridgeInfo.mode}:running` : `${bridgeInfo.mode}:stopped` : "none";
|
|
859
|
-
const conn = t.hasConnection ? "connected" : "waiting";
|
|
860
|
-
console.log(` ${t.tunnelId} ${conn} ${running} bridge=${bridge} ${age}m ago`);
|
|
861
|
-
}
|
|
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}`);
|
|
862
635
|
});
|
|
863
|
-
|
|
864
|
-
|
|
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
|
+
}
|
|
712
|
+
function bridgeLogPath(tunnelId) {
|
|
713
|
+
return path3.join(tunnelInfoDir(), `${tunnelId}.bridge.log`);
|
|
714
|
+
}
|
|
715
|
+
function createApiClient(configOverride) {
|
|
716
|
+
const config = configOverride || getConfig();
|
|
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 {
|
|
865
774
|
try {
|
|
866
|
-
|
|
775
|
+
fs3.unlinkSync(infoPath);
|
|
867
776
|
} catch {
|
|
868
777
|
}
|
|
869
|
-
|
|
778
|
+
return null;
|
|
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
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function isBridgeRunning(tunnelId) {
|
|
791
|
+
const infoPath = bridgeInfoPath(tunnelId);
|
|
792
|
+
if (!fs3.existsSync(infoPath)) return false;
|
|
793
|
+
try {
|
|
794
|
+
const info = JSON.parse(fs3.readFileSync(infoPath, "utf-8"));
|
|
795
|
+
process.kill(info.pid, 0);
|
|
796
|
+
return true;
|
|
797
|
+
} catch {
|
|
870
798
|
try {
|
|
871
|
-
|
|
799
|
+
fs3.unlinkSync(infoPath);
|
|
872
800
|
} catch {
|
|
873
801
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
function stopBridgeProcess(tunnelId) {
|
|
806
|
+
const info = readBridgeProcessInfo(tunnelId);
|
|
807
|
+
if (!info || !Number.isFinite(info.pid)) return;
|
|
808
|
+
try {
|
|
809
|
+
process.kill(info.pid, "SIGTERM");
|
|
810
|
+
} catch {
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function buildBridgeForkStdio(logFd) {
|
|
814
|
+
return ["ignore", logFd, logFd, "ipc"];
|
|
815
|
+
}
|
|
816
|
+
function getFollowReadDelayMs(disconnected, consecutiveFailures) {
|
|
817
|
+
if (!disconnected) return 1e3;
|
|
818
|
+
return Math.min(5e3, 1e3 * 2 ** Math.min(consecutiveFailures, 3));
|
|
819
|
+
}
|
|
820
|
+
function resolveTunnelIdSelection(tunnelIdArg, tunnelOpt) {
|
|
821
|
+
return tunnelOpt || tunnelIdArg;
|
|
822
|
+
}
|
|
823
|
+
function buildDaemonForkStdio(logFd) {
|
|
824
|
+
return ["ignore", logFd, logFd, "ipc"];
|
|
825
|
+
}
|
|
826
|
+
function parsePositiveIntegerOption(raw, optionName) {
|
|
827
|
+
const parsed = Number.parseInt(raw, 10);
|
|
828
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
829
|
+
throw new Error(`${optionName} must be a positive integer. Received: ${raw}`);
|
|
830
|
+
}
|
|
831
|
+
return parsed;
|
|
832
|
+
}
|
|
833
|
+
function parseBridgeMode(raw) {
|
|
834
|
+
const normalized = raw.trim().toLowerCase();
|
|
835
|
+
if (normalized === "openclaw" || normalized === "none") {
|
|
836
|
+
return normalized;
|
|
837
|
+
}
|
|
838
|
+
throw new Error(`--bridge must be one of: openclaw, none. Received: ${raw}`);
|
|
839
|
+
}
|
|
840
|
+
function shouldRestartDaemonForCliUpgrade(daemonCliVersion, currentCliVersion) {
|
|
841
|
+
if (!daemonCliVersion || daemonCliVersion.trim().length === 0) return true;
|
|
842
|
+
return daemonCliVersion.trim() !== currentCliVersion;
|
|
843
|
+
}
|
|
844
|
+
function messageContainsPong(payload) {
|
|
845
|
+
if (!payload || typeof payload !== "object") return false;
|
|
846
|
+
const message = payload.msg;
|
|
847
|
+
if (!message || typeof message !== "object") return false;
|
|
848
|
+
const type = message.type;
|
|
849
|
+
const data = message.data;
|
|
850
|
+
return type === "text" && typeof data === "string" && data.trim().toLowerCase() === "pong";
|
|
851
|
+
}
|
|
852
|
+
function getPublicTunnelUrl(tunnelId) {
|
|
853
|
+
const base = process.env.PUBBLUE_PUBLIC_URL || "https://pub.blue";
|
|
854
|
+
return `${base.replace(/\/$/, "")}/t/${tunnelId}`;
|
|
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);
|
|
894
896
|
}
|
|
895
897
|
if (active.length === 0) {
|
|
896
|
-
|
|
897
|
-
process.exit(1);
|
|
898
|
+
failCli("No active tunnels. Run `pubblue tunnel start` first.");
|
|
898
899
|
}
|
|
899
900
|
if (active.length === 1) return active[0];
|
|
900
|
-
|
|
901
|
-
process.exit(1);
|
|
901
|
+
failCli(`Multiple active tunnels: ${active.join(", ")}. Specify one.`);
|
|
902
902
|
}
|
|
903
903
|
function waitForDaemonReady({
|
|
904
904
|
child,
|
|
@@ -924,7 +924,7 @@ function waitForDaemonReady({
|
|
|
924
924
|
};
|
|
925
925
|
child.on("exit", onExit);
|
|
926
926
|
const poll = setInterval(() => {
|
|
927
|
-
if (pollInFlight || !
|
|
927
|
+
if (pollInFlight || !fs3.existsSync(infoPath)) return;
|
|
928
928
|
pollInFlight = true;
|
|
929
929
|
void ipcCall(socketPath, { method: "status", params: {} }).then((status) => {
|
|
930
930
|
if (status.ok) done({ ok: true });
|
|
@@ -971,9 +971,9 @@ async function ensureBridgeReady(params) {
|
|
|
971
971
|
timeoutMs: params.timeoutMs
|
|
972
972
|
});
|
|
973
973
|
}
|
|
974
|
-
const bridgeScript =
|
|
974
|
+
const bridgeScript = path3.join(import.meta.dirname, "tunnel-bridge-entry.js");
|
|
975
975
|
const logPath = bridgeLogPath(params.tunnelId);
|
|
976
|
-
const logFd =
|
|
976
|
+
const logFd = fs3.openSync(logPath, "a");
|
|
977
977
|
const child = fork(bridgeScript, [], {
|
|
978
978
|
detached: true,
|
|
979
979
|
stdio: buildBridgeForkStdio(logFd),
|
|
@@ -985,7 +985,7 @@ async function ensureBridgeReady(params) {
|
|
|
985
985
|
PUBBLUE_BRIDGE_INFO: infoPath
|
|
986
986
|
}
|
|
987
987
|
});
|
|
988
|
-
|
|
988
|
+
fs3.closeSync(logFd);
|
|
989
989
|
if (child.connected) {
|
|
990
990
|
child.disconnect();
|
|
991
991
|
}
|
|
@@ -1025,7 +1025,7 @@ function waitForBridgeReady({
|
|
|
1025
1025
|
child.on("exit", onExit);
|
|
1026
1026
|
}
|
|
1027
1027
|
const poll = setInterval(() => {
|
|
1028
|
-
if (!
|
|
1028
|
+
if (!fs3.existsSync(infoPath)) return;
|
|
1029
1029
|
const info = readBridgeProcessInfo(tunnelId);
|
|
1030
1030
|
if (!info) return;
|
|
1031
1031
|
lastState = info.status;
|
|
@@ -1048,424 +1048,720 @@ function waitForBridgeReady({
|
|
|
1048
1048
|
});
|
|
1049
1049
|
}
|
|
1050
1050
|
|
|
1051
|
-
// src/
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
...options,
|
|
1061
|
-
headers: {
|
|
1062
|
-
"Content-Type": "application/json",
|
|
1063
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
1064
|
-
...options.headers
|
|
1051
|
+
// src/commands/tunnel/management-commands.ts
|
|
1052
|
+
function registerTunnelManagementCommands(tunnel) {
|
|
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);
|
|
1056
|
+
const response = await ipcCall(socketPath, { method: "channels", params: {} });
|
|
1057
|
+
if (response.channels) {
|
|
1058
|
+
for (const ch of response.channels) {
|
|
1059
|
+
console.log(` ${ch.name} [${ch.direction}]`);
|
|
1065
1060
|
}
|
|
1066
|
-
});
|
|
1067
|
-
const data = await res.json();
|
|
1068
|
-
if (!res.ok) {
|
|
1069
|
-
throw new Error(data.error || `Request failed with status ${res.status}`);
|
|
1070
1061
|
}
|
|
1071
|
-
return data;
|
|
1072
|
-
}
|
|
1073
|
-
async create(opts) {
|
|
1074
|
-
return this.request("/api/v1/publications", {
|
|
1075
|
-
method: "POST",
|
|
1076
|
-
body: JSON.stringify(opts)
|
|
1077
|
-
});
|
|
1078
|
-
}
|
|
1079
|
-
async get(slug) {
|
|
1080
|
-
const data = await this.request(`/api/v1/publications/${encodeURIComponent(slug)}`);
|
|
1081
|
-
return data.publication;
|
|
1082
|
-
}
|
|
1083
|
-
async listPage(cursor, limit) {
|
|
1084
|
-
const params = new URLSearchParams();
|
|
1085
|
-
if (cursor) params.set("cursor", cursor);
|
|
1086
|
-
if (limit) params.set("limit", String(limit));
|
|
1087
|
-
const qs = params.toString();
|
|
1088
|
-
return this.request(`/api/v1/publications${qs ? `?${qs}` : ""}`);
|
|
1089
|
-
}
|
|
1090
|
-
async list() {
|
|
1091
|
-
const all = [];
|
|
1092
|
-
let cursor;
|
|
1093
|
-
do {
|
|
1094
|
-
const result = await this.listPage(cursor, 100);
|
|
1095
|
-
all.push(...result.publications);
|
|
1096
|
-
cursor = result.hasMore ? result.cursor : void 0;
|
|
1097
|
-
} while (cursor);
|
|
1098
|
-
return all;
|
|
1099
|
-
}
|
|
1100
|
-
async update(opts) {
|
|
1101
|
-
const { slug, newSlug, ...rest } = opts;
|
|
1102
|
-
const body = { ...rest };
|
|
1103
|
-
if (newSlug) body.slug = newSlug;
|
|
1104
|
-
return this.request(`/api/v1/publications/${encodeURIComponent(slug)}`, {
|
|
1105
|
-
method: "PATCH",
|
|
1106
|
-
body: JSON.stringify(body)
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
async remove(slug) {
|
|
1110
|
-
await this.request(`/api/v1/publications/${encodeURIComponent(slug)}`, {
|
|
1111
|
-
method: "DELETE"
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
};
|
|
1115
|
-
|
|
1116
|
-
// src/index.ts
|
|
1117
|
-
var program = new Command();
|
|
1118
|
-
function createClient() {
|
|
1119
|
-
const config = getConfig();
|
|
1120
|
-
return new PubApiClient(config.baseUrl, config.apiKey);
|
|
1121
|
-
}
|
|
1122
|
-
async function readFromStdin() {
|
|
1123
|
-
const chunks = [];
|
|
1124
|
-
for await (const chunk of process.stdin) {
|
|
1125
|
-
chunks.push(chunk);
|
|
1126
|
-
}
|
|
1127
|
-
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
1128
|
-
}
|
|
1129
|
-
function formatVisibility(isPublic) {
|
|
1130
|
-
return isPublic ? "public" : "private";
|
|
1131
|
-
}
|
|
1132
|
-
async function readApiKeyFromPrompt() {
|
|
1133
|
-
const rl = createInterface({
|
|
1134
|
-
input: process.stdin,
|
|
1135
|
-
output: process.stdout
|
|
1136
1062
|
});
|
|
1137
|
-
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
};
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1063
|
+
tunnel.command("status").description("Check tunnel connection status").argument("[tunnelId]", "Tunnel ID").option("-t, --tunnel <tunnelId>", "Tunnel ID (alternative to positional arg)").action(async (tunnelIdArg, opts) => {
|
|
1064
|
+
const tunnelId = resolveTunnelIdSelection(tunnelIdArg, opts.tunnel) || await resolveActiveTunnel();
|
|
1065
|
+
const socketPath = getSocketPath(tunnelId);
|
|
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(
|
|
1103
|
+
async (opts) => {
|
|
1104
|
+
const timeoutSeconds = parsePositiveIntegerOption(opts.timeout, "--timeout");
|
|
1105
|
+
const timeoutMs = timeoutSeconds * 1e3;
|
|
1106
|
+
const tunnelId = opts.tunnel || await resolveActiveTunnel();
|
|
1107
|
+
const socketPath = getSocketPath(tunnelId);
|
|
1108
|
+
const apiClient = createApiClient();
|
|
1109
|
+
const fail = (message) => failCli(`Doctor failed: ${message}`);
|
|
1110
|
+
console.log(`Doctor tunnel: ${tunnelId}`);
|
|
1111
|
+
let statusResponse = null;
|
|
1112
|
+
try {
|
|
1113
|
+
statusResponse = await ipcCall(socketPath, {
|
|
1114
|
+
method: "status",
|
|
1115
|
+
params: {}
|
|
1116
|
+
});
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
fail(
|
|
1119
|
+
`daemon is unreachable (${error instanceof Error ? error.message : String(error)}).`
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
if (!statusResponse) {
|
|
1123
|
+
fail("daemon status returned no response.");
|
|
1124
|
+
}
|
|
1125
|
+
const daemonStatus = statusResponse;
|
|
1126
|
+
if (!daemonStatus.ok) {
|
|
1127
|
+
fail(`daemon returned non-ok status: ${String(daemonStatus.error || "unknown error")}`);
|
|
1128
|
+
}
|
|
1129
|
+
if (!daemonStatus.connected) {
|
|
1130
|
+
fail("daemon is running but browser is not connected.");
|
|
1131
|
+
}
|
|
1132
|
+
const channelNames = Array.isArray(daemonStatus.channels) ? daemonStatus.channels.map((entry) => String(entry)) : [];
|
|
1133
|
+
for (const required of [CONTROL_CHANNEL, CHANNELS.CHAT, CHANNELS.CANVAS]) {
|
|
1134
|
+
if (!channelNames.includes(required)) {
|
|
1135
|
+
fail(`required channel is missing: ${required}`);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
console.log("Daemon/channel check: OK");
|
|
1139
|
+
let apiTunnel;
|
|
1140
|
+
try {
|
|
1141
|
+
apiTunnel = await apiClient.get(tunnelId);
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
fail(`failed to fetch tunnel info from API: ${formatApiError(error)}`);
|
|
1144
|
+
}
|
|
1145
|
+
if (apiTunnel.status !== "active") {
|
|
1146
|
+
fail(`API reports tunnel is not active (status: ${apiTunnel.status})`);
|
|
1147
|
+
}
|
|
1148
|
+
if (apiTunnel.expiresAt <= Date.now()) {
|
|
1149
|
+
fail("API reports tunnel is expired.");
|
|
1150
|
+
}
|
|
1151
|
+
if (!apiTunnel.hasConnection) {
|
|
1152
|
+
fail("API reports no browser connection.");
|
|
1153
|
+
}
|
|
1154
|
+
if (typeof apiTunnel.agentOffer !== "string" || apiTunnel.agentOffer.length === 0) {
|
|
1155
|
+
fail("agent offer was not published.");
|
|
1156
|
+
}
|
|
1157
|
+
console.log("API/signaling check: OK");
|
|
1158
|
+
if (!opts.skipChat) {
|
|
1159
|
+
const pingText = "This is a ping test. Reply with 'pong'.";
|
|
1160
|
+
const pingMsg = {
|
|
1161
|
+
id: generateMessageId(),
|
|
1162
|
+
type: "text",
|
|
1163
|
+
data: pingText
|
|
1164
|
+
};
|
|
1165
|
+
const writeResponse = await ipcCall(socketPath, {
|
|
1166
|
+
method: "write",
|
|
1167
|
+
params: { channel: CHANNELS.CHAT, msg: pingMsg }
|
|
1168
|
+
});
|
|
1169
|
+
if (!writeResponse.ok) {
|
|
1170
|
+
fail(`chat ping failed: ${String(writeResponse.error || "unknown write error")}`);
|
|
1171
|
+
}
|
|
1172
|
+
console.log("Chat ping write ACK: OK");
|
|
1173
|
+
if (opts.waitPong) {
|
|
1174
|
+
const startedAt = Date.now();
|
|
1175
|
+
let receivedPong = false;
|
|
1176
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
1177
|
+
const readResponse = await ipcCall(socketPath, {
|
|
1178
|
+
method: "read",
|
|
1179
|
+
params: { channel: CHANNELS.CHAT }
|
|
1180
|
+
});
|
|
1181
|
+
if (!readResponse.ok) {
|
|
1182
|
+
fail(
|
|
1183
|
+
`chat read failed while waiting for pong: ${String(readResponse.error || "unknown read error")}`
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
const messages = Array.isArray(readResponse.messages) ? readResponse.messages : [];
|
|
1187
|
+
if (messages.some((entry) => messageContainsPong(entry))) {
|
|
1188
|
+
receivedPong = true;
|
|
1189
|
+
break;
|
|
1190
|
+
}
|
|
1191
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
1192
|
+
}
|
|
1193
|
+
if (!receivedPong) {
|
|
1194
|
+
fail(
|
|
1195
|
+
`timed out after ${timeoutSeconds}s waiting for exact 'pong' reply on chat channel.`
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
console.log("Chat pong roundtrip: OK");
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (!opts.skipCanvas) {
|
|
1202
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1203
|
+
const canvasMsg = {
|
|
1204
|
+
id: generateMessageId(),
|
|
1205
|
+
type: "html",
|
|
1206
|
+
data: `<!doctype html><html><body style="margin:0;padding:24px;font-family:system-ui;background:#111;color:#f5f5f5">Canvas ping OK<br><small>${stamp}</small></body></html>`
|
|
1207
|
+
};
|
|
1208
|
+
const canvasResponse = await ipcCall(socketPath, {
|
|
1209
|
+
method: "write",
|
|
1210
|
+
params: { channel: CHANNELS.CANVAS, msg: canvasMsg }
|
|
1211
|
+
});
|
|
1212
|
+
if (!canvasResponse.ok) {
|
|
1213
|
+
fail(`canvas ping failed: ${String(canvasResponse.error || "unknown write error")}`);
|
|
1214
|
+
}
|
|
1215
|
+
console.log("Canvas ping write ACK: OK");
|
|
1216
|
+
}
|
|
1217
|
+
console.log("Tunnel doctor: PASS");
|
|
1218
|
+
}
|
|
1219
|
+
);
|
|
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.");
|
|
1265
1225
|
return;
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
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
|
+
});
|
|
1276
1258
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1259
|
+
|
|
1260
|
+
// src/commands/tunnel/message-commands.ts
|
|
1261
|
+
import * as fs5 from "fs";
|
|
1262
|
+
import * as path4 from "path";
|
|
1263
|
+
function registerTunnelMessageCommands(tunnel) {
|
|
1264
|
+
tunnel.command("write").description("Write data to a channel").argument("[message]", "Text message (or use --file)").option("-t, --tunnel <tunnelId>", "Tunnel ID (auto-detected if one active)").option("-c, --channel <channel>", "Channel name", "chat").option("-f, --file <file>", "Read content from file").action(
|
|
1265
|
+
async (messageArg, opts) => {
|
|
1266
|
+
let msg;
|
|
1267
|
+
let binaryBase64;
|
|
1268
|
+
if (opts.file) {
|
|
1269
|
+
const filePath = path4.resolve(opts.file);
|
|
1270
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
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
|
+
};
|
|
1301
|
+
} else {
|
|
1302
|
+
const chunks = [];
|
|
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
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
const tunnelId = opts.tunnel || await resolveActiveTunnel();
|
|
1311
|
+
const socketPath = getSocketPath(tunnelId);
|
|
1312
|
+
const response = await ipcCall(socketPath, {
|
|
1313
|
+
method: "write",
|
|
1314
|
+
params: { channel: opts.channel, msg, binaryBase64 }
|
|
1315
|
+
});
|
|
1316
|
+
if (!response.ok) {
|
|
1317
|
+
failCli(`Failed: ${response.error}`);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
);
|
|
1321
|
+
tunnel.command("read").description("Read buffered messages from channels").argument("[tunnelId]", "Tunnel ID (auto-detected if one active)").option("-t, --tunnel <tunnelId>", "Tunnel ID (alternative to positional arg)").option("-c, --channel <channel>", "Filter by channel").option("--follow", "Stream messages continuously").option("--all", "With --follow, include all channels instead of chat-only default").action(
|
|
1322
|
+
async (tunnelIdArg, opts) => {
|
|
1323
|
+
const tunnelId = resolveTunnelIdSelection(tunnelIdArg, opts.tunnel) || await resolveActiveTunnel();
|
|
1324
|
+
const socketPath = getSocketPath(tunnelId);
|
|
1325
|
+
const readChannel = opts.channel || (opts.follow && !opts.all ? CHANNELS.CHAT : void 0);
|
|
1326
|
+
if (opts.follow) {
|
|
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
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
);
|
|
1299
1373
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1374
|
+
|
|
1375
|
+
// src/commands/tunnel/start-command.ts
|
|
1376
|
+
import { fork as fork2 } from "child_process";
|
|
1377
|
+
import * as fs6 from "fs";
|
|
1378
|
+
import * as path5 from "path";
|
|
1379
|
+
|
|
1380
|
+
// package.json
|
|
1381
|
+
var package_default = {
|
|
1382
|
+
name: "pubblue",
|
|
1383
|
+
version: "0.4.12",
|
|
1384
|
+
description: "CLI tool for publishing static content via pub.blue",
|
|
1385
|
+
type: "module",
|
|
1386
|
+
bin: {
|
|
1387
|
+
pubblue: "./dist/index.js"
|
|
1388
|
+
},
|
|
1389
|
+
scripts: {
|
|
1390
|
+
build: "tsup src/index.ts src/tunnel-daemon-entry.ts src/tunnel-bridge-entry.ts --format esm --dts --clean",
|
|
1391
|
+
dev: "tsup src/index.ts src/tunnel-daemon-entry.ts src/tunnel-bridge-entry.ts --format esm --watch",
|
|
1392
|
+
test: "vitest run",
|
|
1393
|
+
"test:watch": "vitest",
|
|
1394
|
+
lint: "tsc --noEmit"
|
|
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
|
+
]
|
|
1303
1422
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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");
|
|
1307
1429
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
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));
|
|
1313
1438
|
}
|
|
1314
|
-
return
|
|
1315
|
-
content: fs3.readFileSync(resolved, "utf-8"),
|
|
1316
|
-
basename: path3.basename(resolved)
|
|
1317
|
-
};
|
|
1439
|
+
return !isRunning();
|
|
1318
1440
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|
+
}
|
|
1336
1496
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
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
|
+
}
|
|
1340
1512
|
}
|
|
1341
|
-
if (!
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
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") {
|
|
1346
1521
|
throw new Error(
|
|
1347
|
-
"
|
|
1522
|
+
"Foreground mode disables managed bridge process. Use background mode for --bridge openclaw."
|
|
1348
1523
|
);
|
|
1349
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;
|
|
1350
1546
|
}
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
const
|
|
1354
|
-
|
|
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
|
+
}
|
|
1355
1635
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
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();
|
|
1358
1654
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1655
|
+
child.unref();
|
|
1656
|
+
console.log(`Starting daemon for tunnel ${target.tunnelId}...`);
|
|
1657
|
+
const ready = await waitForDaemonReady({
|
|
1658
|
+
child,
|
|
1659
|
+
infoPath,
|
|
1660
|
+
socketPath,
|
|
1661
|
+
timeoutMs: 8e3
|
|
1662
|
+
});
|
|
1663
|
+
if (!ready.ok) {
|
|
1664
|
+
const lines = [
|
|
1665
|
+
`Daemon failed to start: ${ready.reason ?? "unknown reason"}`,
|
|
1666
|
+
`Daemon log: ${logPath}`
|
|
1667
|
+
];
|
|
1668
|
+
const tail = readLogTail(logPath);
|
|
1669
|
+
if (tail) {
|
|
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
|
|
1681
|
+
});
|
|
1682
|
+
if (!offerReady.ok) {
|
|
1683
|
+
const lines = [
|
|
1684
|
+
`Daemon started but signaling is not ready: ${offerReady.reason}`,
|
|
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)}`);
|
|
1367
1735
|
}
|
|
1368
|
-
} catch (error) {
|
|
1369
|
-
const message = error instanceof Error ? error.message : "Failed to configure CLI.";
|
|
1370
|
-
console.error(message);
|
|
1371
|
-
process.exit(1);
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
);
|
|
1375
|
-
program.command("create").description("Create a new publication").argument("[file]", "Path to the file (reads stdin if omitted)").option("--slug <slug>", "Custom slug for the URL").option("--title <title>", "Title for the publication").option("--public", "Make the publication public").option("--private", "Make the publication private (default)").option("--expires <duration>", "Auto-delete after duration (e.g. 1h, 24h, 7d)").action(
|
|
1376
|
-
async (fileArg, opts) => {
|
|
1377
|
-
const client = createClient();
|
|
1378
|
-
let content;
|
|
1379
|
-
let filename;
|
|
1380
|
-
if (fileArg) {
|
|
1381
|
-
const file = readFile(fileArg);
|
|
1382
|
-
content = file.content;
|
|
1383
|
-
filename = file.basename;
|
|
1384
|
-
} else {
|
|
1385
|
-
content = await readFromStdin();
|
|
1386
|
-
}
|
|
1387
|
-
const resolvedVisibility = resolveVisibilityFlags({
|
|
1388
|
-
public: opts.public,
|
|
1389
|
-
private: opts.private,
|
|
1390
|
-
commandName: "create"
|
|
1391
|
-
});
|
|
1392
|
-
const result = await client.create({
|
|
1393
|
-
content,
|
|
1394
|
-
filename,
|
|
1395
|
-
title: opts.title,
|
|
1396
|
-
slug: opts.slug,
|
|
1397
|
-
isPublic: resolvedVisibility ?? false,
|
|
1398
|
-
expiresIn: opts.expires
|
|
1399
|
-
});
|
|
1400
|
-
console.log(`Created: ${result.url}`);
|
|
1401
|
-
if (result.expiresAt) {
|
|
1402
|
-
console.log(` Expires: ${new Date(result.expiresAt).toISOString()}`);
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
);
|
|
1406
|
-
program.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) => {
|
|
1407
|
-
const client = createClient();
|
|
1408
|
-
const pub = await client.get(slug);
|
|
1409
|
-
if (opts.content) {
|
|
1410
|
-
process.stdout.write(pub.content);
|
|
1411
|
-
return;
|
|
1412
|
-
}
|
|
1413
|
-
console.log(` Slug: ${pub.slug}`);
|
|
1414
|
-
console.log(` Type: ${pub.contentType}`);
|
|
1415
|
-
if (pub.title) console.log(` Title: ${pub.title}`);
|
|
1416
|
-
console.log(` Status: ${formatVisibility(pub.isPublic)}`);
|
|
1417
|
-
if (pub.expiresAt) console.log(` Expires: ${new Date(pub.expiresAt).toISOString()}`);
|
|
1418
|
-
console.log(` Created: ${new Date(pub.createdAt).toLocaleDateString()}`);
|
|
1419
|
-
console.log(` Updated: ${new Date(pub.updatedAt).toLocaleDateString()}`);
|
|
1420
|
-
console.log(` Size: ${pub.content.length} bytes`);
|
|
1421
|
-
});
|
|
1422
|
-
program.command("update").description("Update a publication's content and/or metadata").argument("<slug>", "Slug of the publication to update").option("--file <file>", "New content from file").option("--title <title>", "New title").option("--public", "Make the publication public").option("--private", "Make the publication private").option("--slug <newSlug>", "Rename the slug").action(
|
|
1423
|
-
async (slug, opts) => {
|
|
1424
|
-
const client = createClient();
|
|
1425
|
-
let content;
|
|
1426
|
-
let filename;
|
|
1427
|
-
if (opts.file) {
|
|
1428
|
-
const file = readFile(opts.file);
|
|
1429
|
-
content = file.content;
|
|
1430
|
-
filename = file.basename;
|
|
1431
1736
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
);
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
console.log(
|
|
1461
|
-
` ${pub.slug} [${pub.contentType}] ${formatVisibility(pub.isPublic)} ${date}${expires}`
|
|
1462
|
-
);
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// src/commands/tunnel.ts
|
|
1741
|
+
function registerTunnelCommands(program2) {
|
|
1742
|
+
const tunnel = program2.command("tunnel").description("P2P encrypted tunnel to browser");
|
|
1743
|
+
registerTunnelStartCommand(tunnel);
|
|
1744
|
+
registerTunnelMessageCommands(tunnel);
|
|
1745
|
+
registerTunnelManagementCommands(tunnel);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// src/program.ts
|
|
1749
|
+
function buildProgram() {
|
|
1750
|
+
const program2 = new Command();
|
|
1751
|
+
program2.exitOverride();
|
|
1752
|
+
program2.name("pubblue").description("Publish static content and get shareable URLs").version(CLI_VERSION);
|
|
1753
|
+
registerConfigureCommand(program2);
|
|
1754
|
+
registerPublicationCommands(program2);
|
|
1755
|
+
registerTunnelCommands(program2);
|
|
1756
|
+
return program2;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// src/index.ts
|
|
1760
|
+
var program = buildProgram();
|
|
1761
|
+
await program.parseAsync(process.argv).catch((error) => {
|
|
1762
|
+
const failure = toCliFailure(error);
|
|
1763
|
+
if (failure.message) {
|
|
1764
|
+
console.error(failure.message);
|
|
1463
1765
|
}
|
|
1766
|
+
process.exit(failure.exitCode);
|
|
1464
1767
|
});
|
|
1465
|
-
program.command("delete").description("Delete a publication").argument("<slug>", "Slug of the publication to delete").action(async (slug) => {
|
|
1466
|
-
const client = createClient();
|
|
1467
|
-
await client.remove(slug);
|
|
1468
|
-
console.log(`Deleted: ${slug}`);
|
|
1469
|
-
});
|
|
1470
|
-
registerTunnelCommands(program);
|
|
1471
|
-
program.parse();
|