cli-wechat-bridge 1.0.5
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/LICENSE.txt +21 -0
- package/README.md +637 -0
- package/bin/_run-entry.mjs +35 -0
- package/bin/wechat-bridge-claude.mjs +5 -0
- package/bin/wechat-bridge-codex.mjs +5 -0
- package/bin/wechat-bridge-opencode.mjs +5 -0
- package/bin/wechat-bridge-shell.mjs +5 -0
- package/bin/wechat-bridge.mjs +5 -0
- package/bin/wechat-check-update.mjs +5 -0
- package/bin/wechat-claude-start.mjs +5 -0
- package/bin/wechat-claude.mjs +5 -0
- package/bin/wechat-codex-start.mjs +5 -0
- package/bin/wechat-codex.mjs +5 -0
- package/bin/wechat-daemon.mjs +5 -0
- package/bin/wechat-opencode-start.mjs +5 -0
- package/bin/wechat-opencode.mjs +5 -0
- package/bin/wechat-setup.mjs +5 -0
- package/dist/bridge/bridge-adapter-common.js +95 -0
- package/dist/bridge/bridge-adapters.claude.js +829 -0
- package/dist/bridge/bridge-adapters.codex.js +2228 -0
- package/dist/bridge/bridge-adapters.core.js +717 -0
- package/dist/bridge/bridge-adapters.js +26 -0
- package/dist/bridge/bridge-adapters.opencode.js +2129 -0
- package/dist/bridge/bridge-adapters.shared.js +1005 -0
- package/dist/bridge/bridge-adapters.shell.js +363 -0
- package/dist/bridge/bridge-controller.js +48 -0
- package/dist/bridge/bridge-final-reply.js +46 -0
- package/dist/bridge/bridge-process-reaper.js +348 -0
- package/dist/bridge/bridge-state.js +362 -0
- package/dist/bridge/bridge-types.js +1 -0
- package/dist/bridge/bridge-utils.js +1240 -0
- package/dist/bridge/claude-hook.js +82 -0
- package/dist/bridge/claude-hooks.js +267 -0
- package/dist/bridge/wechat-bridge.js +1026 -0
- package/dist/commands/check-update.js +30 -0
- package/dist/companion/codex-panel-link.js +72 -0
- package/dist/companion/codex-panel.js +179 -0
- package/dist/companion/codex-remote-client.js +124 -0
- package/dist/companion/local-companion-link.js +240 -0
- package/dist/companion/local-companion-start.js +420 -0
- package/dist/companion/local-companion.js +424 -0
- package/dist/daemon/daemon-link.js +175 -0
- package/dist/daemon/wechat-daemon.js +1202 -0
- package/dist/media/media-types.js +1 -0
- package/dist/runtime/create-runtime-host.js +12 -0
- package/dist/runtime/legacy-adapter-runtime.js +46 -0
- package/dist/runtime/runtime-types.js +5 -0
- package/dist/utils/version-checker.js +161 -0
- package/dist/wechat/channel-config.js +196 -0
- package/dist/wechat/setup.js +283 -0
- package/dist/wechat/standalone-bot.js +355 -0
- package/dist/wechat/wechat-channel.js +492 -0
- package/dist/wechat/wechat-transport.js +1213 -0
- package/package.json +101 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createBridgeAdapter } from "../bridge/bridge-adapters.js";
|
|
5
|
+
import { LOCAL_COMPANION_RECONNECT_GRACE_MS, } from "../bridge/bridge-adapters.shared.js";
|
|
6
|
+
import { runCodexRemoteClientFromEndpoint } from "./codex-remote-client.js";
|
|
7
|
+
import { attachLocalCompanionMessageListener, readLocalCompanionEndpoint, sendLocalCompanionMessage, } from "./local-companion-link.js";
|
|
8
|
+
import { migrateLegacyChannelFiles } from "../wechat/channel-config.js";
|
|
9
|
+
export const LOCAL_COMPANION_RECONNECT_RETRY_MS = 250;
|
|
10
|
+
function log(adapter, message) {
|
|
11
|
+
process.stderr.write(`[${adapter}-companion] ${message}\n`);
|
|
12
|
+
}
|
|
13
|
+
function parseCliArgs(argv) {
|
|
14
|
+
let adapter = null;
|
|
15
|
+
let cwd = process.cwd();
|
|
16
|
+
const cliArgs = [];
|
|
17
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
18
|
+
const arg = argv[i];
|
|
19
|
+
if (!arg) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const next = argv[i + 1];
|
|
23
|
+
if (arg === "--help" || arg === "-h") {
|
|
24
|
+
process.stdout.write([
|
|
25
|
+
"Usage: local-companion --adapter <codex|claude|opencode> [--cwd <path>] [...cli args]",
|
|
26
|
+
"",
|
|
27
|
+
'Starts the visible local companion and connects it to the matching running bridge for the current directory.',
|
|
28
|
+
"Unknown arguments are forwarded to the visible CLI client.",
|
|
29
|
+
"",
|
|
30
|
+
].join("\n"));
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
if (arg === "--adapter") {
|
|
34
|
+
if (!next || !["codex", "claude", "opencode"].includes(next)) {
|
|
35
|
+
throw new Error(`Invalid adapter: ${next ?? "(missing)"}`);
|
|
36
|
+
}
|
|
37
|
+
adapter = next;
|
|
38
|
+
i += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (arg === "--cwd") {
|
|
42
|
+
if (!next) {
|
|
43
|
+
throw new Error("--cwd requires a value");
|
|
44
|
+
}
|
|
45
|
+
cwd = path.resolve(next);
|
|
46
|
+
i += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
cliArgs.push(arg);
|
|
50
|
+
}
|
|
51
|
+
if (!adapter) {
|
|
52
|
+
throw new Error("Missing required --adapter <codex|claude|opencode>");
|
|
53
|
+
}
|
|
54
|
+
return { adapter, cwd, cliArgs };
|
|
55
|
+
}
|
|
56
|
+
function delay(ms) {
|
|
57
|
+
if (ms <= 0) {
|
|
58
|
+
return Promise.resolve();
|
|
59
|
+
}
|
|
60
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
61
|
+
}
|
|
62
|
+
function readMatchingEndpoint(options) {
|
|
63
|
+
const endpoint = readLocalCompanionEndpoint(options.cwd, {
|
|
64
|
+
adapter: options.adapter,
|
|
65
|
+
});
|
|
66
|
+
if (!endpoint || endpoint.kind !== options.adapter) {
|
|
67
|
+
throw new Error(`No active ${options.adapter} bridge endpoint was found for ${options.cwd}. Start "wechat-bridge-${options.adapter}" in that directory first.`);
|
|
68
|
+
}
|
|
69
|
+
return endpoint;
|
|
70
|
+
}
|
|
71
|
+
export function shouldReconnectLocalCompanion(params) {
|
|
72
|
+
return !params.shuttingDown && !params.closeReason;
|
|
73
|
+
}
|
|
74
|
+
export async function runLocalCompanion(options) {
|
|
75
|
+
const initialEndpoint = readMatchingEndpoint(options);
|
|
76
|
+
if (initialEndpoint.kind === "codex" &&
|
|
77
|
+
initialEndpoint.runtimeKind === "codex_runtime_host") {
|
|
78
|
+
return await runCodexRemoteClientFromEndpoint(initialEndpoint, {
|
|
79
|
+
extraCliArgs: options.cliArgs,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const adapter = createBridgeAdapter({
|
|
83
|
+
kind: initialEndpoint.kind,
|
|
84
|
+
command: initialEndpoint.command,
|
|
85
|
+
cwd: initialEndpoint.cwd,
|
|
86
|
+
profile: initialEndpoint.profile,
|
|
87
|
+
initialSharedSessionId: initialEndpoint.sharedSessionId ?? initialEndpoint.sharedThreadId,
|
|
88
|
+
initialResumeConversationId: initialEndpoint.resumeConversationId,
|
|
89
|
+
initialTranscriptPath: initialEndpoint.transcriptPath,
|
|
90
|
+
renderMode: initialEndpoint.kind === "codex" ? "panel" : "companion",
|
|
91
|
+
extraCliArgs: options.cliArgs,
|
|
92
|
+
});
|
|
93
|
+
let shuttingDown = false;
|
|
94
|
+
let closeReason = null;
|
|
95
|
+
let activeSocket = null;
|
|
96
|
+
let detachListener = null;
|
|
97
|
+
let reconnectPromise = null;
|
|
98
|
+
let resolveExitCode = null;
|
|
99
|
+
const exitCodePromise = new Promise((resolve) => {
|
|
100
|
+
resolveExitCode = resolve;
|
|
101
|
+
});
|
|
102
|
+
let signalHandlersRegistered = false;
|
|
103
|
+
const detachActiveSocket = (destroy = false) => {
|
|
104
|
+
const socket = activeSocket;
|
|
105
|
+
activeSocket = null;
|
|
106
|
+
detachListener?.();
|
|
107
|
+
detachListener = null;
|
|
108
|
+
if (!socket) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
socket.removeAllListeners("close");
|
|
112
|
+
socket.removeAllListeners("error");
|
|
113
|
+
if (destroy) {
|
|
114
|
+
try {
|
|
115
|
+
socket.destroy();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Best effort cleanup.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const resolveExit = (code) => {
|
|
123
|
+
const resolve = resolveExitCode;
|
|
124
|
+
if (!resolve) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
resolveExitCode = null;
|
|
128
|
+
resolve(code);
|
|
129
|
+
};
|
|
130
|
+
const unregisterSignalHandlers = () => {
|
|
131
|
+
if (!signalHandlersRegistered) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
signalHandlersRegistered = false;
|
|
135
|
+
process.removeListener("SIGINT", handleSigint);
|
|
136
|
+
process.removeListener("SIGTERM", handleSigterm);
|
|
137
|
+
process.removeListener("SIGHUP", handleSighup);
|
|
138
|
+
if (process.platform === "win32") {
|
|
139
|
+
process.removeListener("SIGBREAK", handleSigbreak);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const publishState = () => {
|
|
143
|
+
if (!activeSocket) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
sendLocalCompanionMessage(activeSocket, {
|
|
147
|
+
type: "state",
|
|
148
|
+
state: adapter.getState(),
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
const sendResponse = (socket, id, ok, result, error) => {
|
|
152
|
+
sendLocalCompanionMessage(socket, {
|
|
153
|
+
type: "response",
|
|
154
|
+
id,
|
|
155
|
+
ok,
|
|
156
|
+
result,
|
|
157
|
+
error,
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
const announceClosing = (reason, exitCode) => {
|
|
161
|
+
closeReason = reason;
|
|
162
|
+
if (!activeSocket) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
sendLocalCompanionMessage(activeSocket, {
|
|
166
|
+
type: "closing",
|
|
167
|
+
reason,
|
|
168
|
+
exitCode,
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
const closeCompanion = async (exitCode = 0, reason = "companion_shutdown") => {
|
|
172
|
+
if (shuttingDown) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
shuttingDown = true;
|
|
176
|
+
announceClosing(reason, exitCode);
|
|
177
|
+
detachActiveSocket(false);
|
|
178
|
+
try {
|
|
179
|
+
await adapter.dispose();
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Best effort cleanup.
|
|
183
|
+
}
|
|
184
|
+
resolveExit(exitCode);
|
|
185
|
+
};
|
|
186
|
+
const handleBridgeRequest = async (socket, message) => {
|
|
187
|
+
try {
|
|
188
|
+
switch (message.payload.command) {
|
|
189
|
+
case "send_input":
|
|
190
|
+
await adapter.sendInput(message.payload.text);
|
|
191
|
+
sendResponse(socket, message.id, true);
|
|
192
|
+
break;
|
|
193
|
+
case "list_resume_sessions":
|
|
194
|
+
case "list_resume_threads":
|
|
195
|
+
sendResponse(socket, message.id, true, await adapter.listResumeSessions(message.payload.limit));
|
|
196
|
+
break;
|
|
197
|
+
case "resume_session":
|
|
198
|
+
await adapter.resumeSession(message.payload.sessionId);
|
|
199
|
+
publishState();
|
|
200
|
+
sendResponse(socket, message.id, true);
|
|
201
|
+
break;
|
|
202
|
+
case "resume_thread":
|
|
203
|
+
await adapter.resumeSession(message.payload.threadId);
|
|
204
|
+
publishState();
|
|
205
|
+
sendResponse(socket, message.id, true);
|
|
206
|
+
break;
|
|
207
|
+
case "create_session":
|
|
208
|
+
if (!adapter.createSession) {
|
|
209
|
+
throw new Error(`/${adapter.getState().kind} does not support creating sessions from WeChat.`);
|
|
210
|
+
}
|
|
211
|
+
await adapter.createSession();
|
|
212
|
+
publishState();
|
|
213
|
+
sendResponse(socket, message.id, true);
|
|
214
|
+
break;
|
|
215
|
+
case "interrupt":
|
|
216
|
+
sendResponse(socket, message.id, true, await adapter.interrupt());
|
|
217
|
+
break;
|
|
218
|
+
case "reset":
|
|
219
|
+
await adapter.reset();
|
|
220
|
+
publishState();
|
|
221
|
+
sendResponse(socket, message.id, true);
|
|
222
|
+
break;
|
|
223
|
+
case "resolve_approval":
|
|
224
|
+
sendResponse(socket, message.id, true, await adapter.resolveApproval(message.payload.action));
|
|
225
|
+
break;
|
|
226
|
+
case "dispose":
|
|
227
|
+
sendResponse(socket, message.id, true);
|
|
228
|
+
await closeCompanion(0, "bridge_dispose");
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
234
|
+
sendResponse(socket, message.id, false, undefined, text);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const connectToBridge = async (endpoint) => {
|
|
238
|
+
await new Promise((resolve, reject) => {
|
|
239
|
+
const socket = net.connect({
|
|
240
|
+
host: "127.0.0.1",
|
|
241
|
+
port: endpoint.port,
|
|
242
|
+
});
|
|
243
|
+
let settled = false;
|
|
244
|
+
let helloAcknowledged = false;
|
|
245
|
+
let localDetach = null;
|
|
246
|
+
const fail = (error) => {
|
|
247
|
+
if (settled) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
settled = true;
|
|
251
|
+
localDetach?.();
|
|
252
|
+
localDetach = null;
|
|
253
|
+
try {
|
|
254
|
+
socket.destroy();
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Best effort cleanup.
|
|
258
|
+
}
|
|
259
|
+
reject(error);
|
|
260
|
+
};
|
|
261
|
+
socket.once("connect", () => {
|
|
262
|
+
socket.setNoDelay(true);
|
|
263
|
+
localDetach = attachLocalCompanionMessageListener(socket, (message) => {
|
|
264
|
+
if (!helloAcknowledged) {
|
|
265
|
+
if (message.type === "hello_ack") {
|
|
266
|
+
helloAcknowledged = true;
|
|
267
|
+
closeReason = null;
|
|
268
|
+
activeSocket = socket;
|
|
269
|
+
detachListener = localDetach;
|
|
270
|
+
if (!settled) {
|
|
271
|
+
settled = true;
|
|
272
|
+
resolve();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (message.type !== "request") {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
void handleBridgeRequest(socket, message);
|
|
281
|
+
});
|
|
282
|
+
socket.once("close", () => {
|
|
283
|
+
if (!settled) {
|
|
284
|
+
fail(new Error(`The ${options.adapter} bridge closed the local companion socket before authentication.`));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (activeSocket === socket) {
|
|
288
|
+
detachActiveSocket(false);
|
|
289
|
+
void (async () => {
|
|
290
|
+
if (!shouldReconnectLocalCompanion({
|
|
291
|
+
shuttingDown,
|
|
292
|
+
closeReason,
|
|
293
|
+
})) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const reconnected = await reconnectToBridge();
|
|
297
|
+
if (!reconnected && !shuttingDown) {
|
|
298
|
+
await closeCompanion(1, "fatal_error");
|
|
299
|
+
}
|
|
300
|
+
})();
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
socket.once("error", (error) => {
|
|
304
|
+
if (!settled) {
|
|
305
|
+
fail(error instanceof Error
|
|
306
|
+
? error
|
|
307
|
+
: new Error(String(error)));
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
sendLocalCompanionMessage(socket, {
|
|
311
|
+
type: "hello",
|
|
312
|
+
token: endpoint.token,
|
|
313
|
+
companionPid: process.pid,
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
socket.once("error", (error) => {
|
|
317
|
+
if (!settled) {
|
|
318
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
const reconnectToBridge = async () => {
|
|
324
|
+
if (reconnectPromise) {
|
|
325
|
+
return await reconnectPromise;
|
|
326
|
+
}
|
|
327
|
+
reconnectPromise = (async () => {
|
|
328
|
+
const deadline = Date.now() + LOCAL_COMPANION_RECONNECT_GRACE_MS;
|
|
329
|
+
let lastError = "";
|
|
330
|
+
log(options.adapter, `Bridge connection dropped unexpectedly. Waiting up to ${Math.ceil(LOCAL_COMPANION_RECONNECT_GRACE_MS / 1000)}s to reconnect...`);
|
|
331
|
+
while (!shuttingDown && Date.now() < deadline) {
|
|
332
|
+
try {
|
|
333
|
+
const nextEndpoint = readMatchingEndpoint(options);
|
|
334
|
+
await connectToBridge(nextEndpoint);
|
|
335
|
+
publishState();
|
|
336
|
+
log(options.adapter, `Reconnected to bridge ${nextEndpoint.instanceId}.`);
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
341
|
+
await delay(LOCAL_COMPANION_RECONNECT_RETRY_MS);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (lastError) {
|
|
345
|
+
log(options.adapter, `Bridge reconnection timed out after ${Math.ceil(LOCAL_COMPANION_RECONNECT_GRACE_MS / 1000)}s: ${lastError}`);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
log(options.adapter, `Bridge reconnection timed out after ${Math.ceil(LOCAL_COMPANION_RECONNECT_GRACE_MS / 1000)}s.`);
|
|
349
|
+
}
|
|
350
|
+
return false;
|
|
351
|
+
})();
|
|
352
|
+
try {
|
|
353
|
+
return await reconnectPromise;
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
reconnectPromise = null;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
adapter.setEventSink((event) => {
|
|
360
|
+
if (activeSocket) {
|
|
361
|
+
sendLocalCompanionMessage(activeSocket, {
|
|
362
|
+
type: "event",
|
|
363
|
+
event,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
publishState();
|
|
367
|
+
if (event.type === "fatal_error") {
|
|
368
|
+
void closeCompanion(1, "fatal_error");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (event.type === "status" && event.status === "stopped") {
|
|
372
|
+
void closeCompanion(0, "worker_exit");
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
const requestSignalShutdown = (signal) => {
|
|
376
|
+
log(options.adapter, `Received ${signal}. Closing local companion.`);
|
|
377
|
+
void closeCompanion(0, "signal");
|
|
378
|
+
};
|
|
379
|
+
const handleSigint = () => requestSignalShutdown("SIGINT");
|
|
380
|
+
const handleSigterm = () => requestSignalShutdown("SIGTERM");
|
|
381
|
+
const handleSighup = () => requestSignalShutdown("SIGHUP");
|
|
382
|
+
const handleSigbreak = () => requestSignalShutdown("SIGBREAK");
|
|
383
|
+
process.once("SIGINT", handleSigint);
|
|
384
|
+
process.once("SIGTERM", handleSigterm);
|
|
385
|
+
process.once("SIGHUP", handleSighup);
|
|
386
|
+
if (process.platform === "win32") {
|
|
387
|
+
process.once("SIGBREAK", handleSigbreak);
|
|
388
|
+
}
|
|
389
|
+
signalHandlersRegistered = true;
|
|
390
|
+
try {
|
|
391
|
+
await connectToBridge(initialEndpoint);
|
|
392
|
+
await adapter.start();
|
|
393
|
+
publishState();
|
|
394
|
+
log(options.adapter, `Connected to bridge ${initialEndpoint.instanceId}.`);
|
|
395
|
+
return await exitCodePromise;
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
unregisterSignalHandlers();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
402
|
+
migrateLegacyChannelFiles((message) => log("local", message));
|
|
403
|
+
try {
|
|
404
|
+
const options = parseCliArgs(argv);
|
|
405
|
+
const exitCode = await runLocalCompanion(options);
|
|
406
|
+
process.exit(exitCode);
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
const adapter = (() => {
|
|
410
|
+
try {
|
|
411
|
+
return parseCliArgs(argv).adapter;
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return "local";
|
|
415
|
+
}
|
|
416
|
+
})();
|
|
417
|
+
log(adapter, error instanceof Error ? error.message : String(error));
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const isDirectRun = Boolean(import.meta.main);
|
|
422
|
+
if (isDirectRun) {
|
|
423
|
+
void main();
|
|
424
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import { DAEMON_ENDPOINT_FILE, ensureChannelDataDir, } from "../wechat/channel-config.js";
|
|
5
|
+
export const DAEMON_PROTOCOL_VERSION = 1;
|
|
6
|
+
function normalizeDaemonEndpoint(value) {
|
|
7
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const record = value;
|
|
11
|
+
if (typeof record.protocolVersion !== "number" ||
|
|
12
|
+
typeof record.pid !== "number" ||
|
|
13
|
+
typeof record.port !== "number" ||
|
|
14
|
+
typeof record.token !== "string" ||
|
|
15
|
+
typeof record.cwd !== "string" ||
|
|
16
|
+
typeof record.startedAt !== "string") {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
protocolVersion: record.protocolVersion,
|
|
21
|
+
pid: record.pid,
|
|
22
|
+
port: record.port,
|
|
23
|
+
token: record.token,
|
|
24
|
+
cwd: record.cwd,
|
|
25
|
+
startedAt: record.startedAt,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function buildDaemonToken() {
|
|
29
|
+
return crypto.randomBytes(18).toString("hex");
|
|
30
|
+
}
|
|
31
|
+
export function writeDaemonEndpoint(endpoint) {
|
|
32
|
+
ensureChannelDataDir();
|
|
33
|
+
fs.writeFileSync(DAEMON_ENDPOINT_FILE, JSON.stringify({
|
|
34
|
+
...endpoint,
|
|
35
|
+
protocolVersion: DAEMON_PROTOCOL_VERSION,
|
|
36
|
+
}, null, 2), "utf8");
|
|
37
|
+
}
|
|
38
|
+
export function readDaemonEndpoint() {
|
|
39
|
+
try {
|
|
40
|
+
if (!fs.existsSync(DAEMON_ENDPOINT_FILE)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return normalizeDaemonEndpoint(JSON.parse(fs.readFileSync(DAEMON_ENDPOINT_FILE, "utf8")));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function clearDaemonEndpoint(pid = process.pid) {
|
|
50
|
+
try {
|
|
51
|
+
const endpoint = readDaemonEndpoint();
|
|
52
|
+
if (!endpoint || endpoint.pid === pid) {
|
|
53
|
+
fs.rmSync(DAEMON_ENDPOINT_FILE, { force: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Best effort cleanup.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function isPidAlive(pid) {
|
|
61
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
process.kill(pid, 0);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function sendDaemonResponse(socket, id, response) {
|
|
73
|
+
if (socket.destroyed || socket.writableEnded) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
return socket.write(`${JSON.stringify({ id, response })}\n`);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function attachDaemonRequestListener(socket, onRequest) {
|
|
84
|
+
let buffer = "";
|
|
85
|
+
const onData = (chunk) => {
|
|
86
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
87
|
+
while (true) {
|
|
88
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
89
|
+
if (newlineIndex < 0) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
93
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
94
|
+
if (!line) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
onRequest(JSON.parse(line));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Ignore malformed daemon IPC frames.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
socket.setEncoding("utf8");
|
|
106
|
+
socket.on("data", onData);
|
|
107
|
+
return () => {
|
|
108
|
+
socket.off("data", onData);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export async function sendDaemonRequest(endpoint, payload, options = {}) {
|
|
112
|
+
const timeoutMs = options.timeoutMs ?? 2_000;
|
|
113
|
+
const id = crypto.randomUUID();
|
|
114
|
+
return await new Promise((resolve) => {
|
|
115
|
+
const socket = net.connect({
|
|
116
|
+
host: "127.0.0.1",
|
|
117
|
+
port: endpoint.port,
|
|
118
|
+
});
|
|
119
|
+
let buffer = "";
|
|
120
|
+
let settled = false;
|
|
121
|
+
const timer = setTimeout(() => {
|
|
122
|
+
finish({ ok: false, error: "Timed out waiting for daemon response." });
|
|
123
|
+
}, timeoutMs);
|
|
124
|
+
const finish = (response) => {
|
|
125
|
+
if (settled) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
settled = true;
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
socket.destroy();
|
|
131
|
+
resolve(response);
|
|
132
|
+
};
|
|
133
|
+
socket.setEncoding("utf8");
|
|
134
|
+
socket.once("connect", () => {
|
|
135
|
+
socket.write(`${JSON.stringify({
|
|
136
|
+
id,
|
|
137
|
+
token: endpoint.token,
|
|
138
|
+
payload,
|
|
139
|
+
})}\n`);
|
|
140
|
+
});
|
|
141
|
+
socket.on("data", (chunk) => {
|
|
142
|
+
buffer += chunk;
|
|
143
|
+
while (true) {
|
|
144
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
145
|
+
if (newlineIndex < 0) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
149
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
150
|
+
if (!line) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const frame = JSON.parse(line);
|
|
155
|
+
if (frame.id === id) {
|
|
156
|
+
finish(frame.response);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Ignore malformed daemon IPC frames.
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
socket.once("error", () => {
|
|
165
|
+
finish({ ok: false, error: "Daemon endpoint is not reachable." });
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
export async function isDaemonEndpointAlive(endpoint, options = {}) {
|
|
170
|
+
if (!isPidAlive(endpoint.pid)) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
const response = await sendDaemonRequest(endpoint, { command: "status" }, options);
|
|
174
|
+
return response.ok;
|
|
175
|
+
}
|