@unlinearity/cli-wechat-bridge 1.0.4 → 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/README.md +128 -22
- package/bin/wechat-daemon.mjs +5 -0
- package/dist/bridge/bridge-adapters.core.js +46 -6
- package/dist/bridge/bridge-controller.js +6 -2
- package/dist/bridge/bridge-utils.js +43 -7
- package/dist/bridge/wechat-bridge.js +44 -6
- package/dist/companion/codex-remote-client.js +8 -4
- package/dist/companion/local-companion-link.js +56 -26
- package/dist/companion/local-companion-start.js +48 -5
- package/dist/companion/local-companion.js +3 -1
- package/dist/daemon/daemon-link.js +175 -0
- package/dist/daemon/wechat-daemon.js +1202 -0
- package/dist/wechat/channel-config.js +126 -33
- package/dist/wechat/standalone-bot.js +5 -6
- package/dist/wechat/wechat-channel.js +21 -10
- package/dist/wechat/wechat-transport.js +291 -9
- package/package.json +3 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
-
import { ensureWorkspaceChannelDir, getWorkspaceChannelPaths, } from "../wechat/channel-config.js";
|
|
3
|
+
import { ensureWorkspaceChannelDir, getWorkspaceAdapterEndpointFile, getWorkspaceChannelPaths, } from "../wechat/channel-config.js";
|
|
4
4
|
import { LOCAL_CLIENT_PROTOCOL_VERSION, } from "../runtime/runtime-types.js";
|
|
5
5
|
function normalizeEndpoint(value) {
|
|
6
6
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -66,51 +66,81 @@ function normalizeEndpoint(value) {
|
|
|
66
66
|
export function buildLocalCompanionToken() {
|
|
67
67
|
return crypto.randomBytes(18).toString("hex");
|
|
68
68
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const payload = {
|
|
69
|
+
function serializeEndpoint(endpoint) {
|
|
70
|
+
return {
|
|
72
71
|
...endpoint,
|
|
73
72
|
protocolVersion: endpoint.protocolVersion ?? LOCAL_CLIENT_PROTOCOL_VERSION,
|
|
74
73
|
sharedThreadId: endpoint.kind === "codex" || endpoint.kind === "opencode"
|
|
75
74
|
? endpoint.sharedSessionId ?? endpoint.sharedThreadId
|
|
76
75
|
: undefined,
|
|
77
76
|
};
|
|
78
|
-
fs.writeFileSync(endpointFile, JSON.stringify(payload, null, 2), "utf8");
|
|
79
77
|
}
|
|
80
|
-
|
|
78
|
+
function readEndpointFile(filePath) {
|
|
79
|
+
if (!fs.existsSync(filePath)) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return normalizeEndpoint(JSON.parse(fs.readFileSync(filePath, "utf8")));
|
|
83
|
+
}
|
|
84
|
+
export function writeLocalCompanionEndpoint(endpoint, options = {}) {
|
|
85
|
+
const { endpointFile } = ensureWorkspaceChannelDir(endpoint.cwd);
|
|
86
|
+
const payload = {
|
|
87
|
+
...serializeEndpoint(endpoint),
|
|
88
|
+
};
|
|
89
|
+
fs.writeFileSync(getWorkspaceAdapterEndpointFile(endpoint.cwd, endpoint.kind), JSON.stringify(payload, null, 2), "utf8");
|
|
90
|
+
if (options.writeLegacy !== false) {
|
|
91
|
+
fs.writeFileSync(endpointFile, JSON.stringify(payload, null, 2), "utf8");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function readLocalCompanionEndpoint(cwd, options = {}) {
|
|
81
95
|
try {
|
|
82
96
|
const { endpointFile } = getWorkspaceChannelPaths(cwd);
|
|
83
|
-
if (
|
|
84
|
-
|
|
97
|
+
if (options.adapter) {
|
|
98
|
+
const scoped = readEndpointFile(getWorkspaceAdapterEndpointFile(cwd, options.adapter));
|
|
99
|
+
if (scoped) {
|
|
100
|
+
return scoped;
|
|
101
|
+
}
|
|
102
|
+
const legacy = readEndpointFile(endpointFile);
|
|
103
|
+
return legacy?.kind === options.adapter ? legacy : null;
|
|
85
104
|
}
|
|
86
|
-
return
|
|
105
|
+
return readEndpointFile(endpointFile);
|
|
87
106
|
}
|
|
88
107
|
catch {
|
|
89
108
|
return null;
|
|
90
109
|
}
|
|
91
110
|
}
|
|
92
|
-
export function clearLocalCompanionEndpoint(cwd, instanceId) {
|
|
111
|
+
export function clearLocalCompanionEndpoint(cwd, instanceId, options = {}) {
|
|
93
112
|
try {
|
|
94
113
|
const { endpointFile } = getWorkspaceChannelPaths(cwd);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
const files = options.adapter
|
|
115
|
+
? [getWorkspaceAdapterEndpointFile(cwd, options.adapter), endpointFile]
|
|
116
|
+
: [
|
|
117
|
+
endpointFile,
|
|
118
|
+
getWorkspaceAdapterEndpointFile(cwd, "codex"),
|
|
119
|
+
getWorkspaceAdapterEndpointFile(cwd, "claude"),
|
|
120
|
+
getWorkspaceAdapterEndpointFile(cwd, "opencode"),
|
|
121
|
+
getWorkspaceAdapterEndpointFile(cwd, "shell"),
|
|
122
|
+
];
|
|
123
|
+
for (const filePath of files) {
|
|
124
|
+
if (!fs.existsSync(filePath)) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!instanceId) {
|
|
128
|
+
fs.rmSync(filePath, { force: true });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const endpoint = readEndpointFile(filePath);
|
|
132
|
+
if (!endpoint || endpoint.instanceId === instanceId) {
|
|
133
|
+
fs.rmSync(filePath, { force: true });
|
|
134
|
+
}
|
|
105
135
|
}
|
|
106
136
|
}
|
|
107
137
|
catch {
|
|
108
138
|
// Best effort cleanup.
|
|
109
139
|
}
|
|
110
140
|
}
|
|
111
|
-
export function clearLocalCompanionOccupancy(cwd, instanceId) {
|
|
141
|
+
export function clearLocalCompanionOccupancy(cwd, instanceId, options = {}) {
|
|
112
142
|
try {
|
|
113
|
-
const endpoint = readLocalCompanionEndpoint(cwd);
|
|
143
|
+
const endpoint = readLocalCompanionEndpoint(cwd, options);
|
|
114
144
|
if (!endpoint) {
|
|
115
145
|
return;
|
|
116
146
|
}
|
|
@@ -130,9 +160,9 @@ export function clearLocalCompanionOccupancy(cwd, instanceId) {
|
|
|
130
160
|
// Best effort cleanup.
|
|
131
161
|
}
|
|
132
162
|
}
|
|
133
|
-
export function updateLocalCompanionHealth(cwd, patch, instanceId) {
|
|
163
|
+
export function updateLocalCompanionHealth(cwd, patch, instanceId, options = {}) {
|
|
134
164
|
try {
|
|
135
|
-
const endpoint = readLocalCompanionEndpoint(cwd);
|
|
165
|
+
const endpoint = readLocalCompanionEndpoint(cwd, options);
|
|
136
166
|
if (!endpoint) {
|
|
137
167
|
return;
|
|
138
168
|
}
|
|
@@ -150,9 +180,9 @@ export function updateLocalCompanionHealth(cwd, patch, instanceId) {
|
|
|
150
180
|
// Best effort cleanup.
|
|
151
181
|
}
|
|
152
182
|
}
|
|
153
|
-
export function updateLocalCompanionOccupancy(cwd, patch, instanceId) {
|
|
183
|
+
export function updateLocalCompanionOccupancy(cwd, patch, instanceId, options = {}) {
|
|
154
184
|
try {
|
|
155
|
-
const endpoint = readLocalCompanionEndpoint(cwd);
|
|
185
|
+
const endpoint = readLocalCompanionEndpoint(cwd, options);
|
|
156
186
|
if (!endpoint) {
|
|
157
187
|
return;
|
|
158
188
|
}
|
|
@@ -9,6 +9,7 @@ import { readBridgeLockFile, shouldAutoReclaimBridgeLock, } from "../bridge/brid
|
|
|
9
9
|
import { clearLocalCompanionOccupancy, clearLocalCompanionEndpoint, readLocalCompanionEndpoint, } from "./local-companion-link.js";
|
|
10
10
|
import { runCodexRemoteClient } from "./codex-remote-client.js";
|
|
11
11
|
import { runLocalCompanion } from "./local-companion.js";
|
|
12
|
+
import { clearDaemonEndpoint, isDaemonEndpointAlive, readDaemonEndpoint, sendDaemonRequest, } from "../daemon/daemon-link.js";
|
|
12
13
|
const MODULE_FILE = fileURLToPath(import.meta.url);
|
|
13
14
|
const MODULE_DIR = path.dirname(MODULE_FILE);
|
|
14
15
|
const RUNTIME_ENTRY_EXTENSION = path.extname(MODULE_FILE) === ".ts" ? ".ts" : ".js";
|
|
@@ -184,7 +185,12 @@ async function stopExistingBridge(lock, requestedAdapter) {
|
|
|
184
185
|
if (!(await waitForProcessExit(pid, 10_000))) {
|
|
185
186
|
throw new Error(`Timed out waiting for existing bridge pid=${pid} to exit.`);
|
|
186
187
|
}
|
|
187
|
-
|
|
188
|
+
if (lock.adapter === "codex" || lock.adapter === "claude" || lock.adapter === "opencode") {
|
|
189
|
+
clearLocalCompanionEndpoint(cwd, undefined, { adapter: lock.adapter });
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
clearLocalCompanionEndpoint(cwd);
|
|
193
|
+
}
|
|
188
194
|
log(requestedAdapter, `Cleared stale local companion endpoint for previous workspace ${cwd}.`);
|
|
189
195
|
}
|
|
190
196
|
async function isEndpointReachable(endpoint) {
|
|
@@ -211,14 +217,14 @@ async function isEndpointReachable(endpoint) {
|
|
|
211
217
|
});
|
|
212
218
|
}
|
|
213
219
|
async function readUsableEndpoint(cwd, adapter) {
|
|
214
|
-
const endpoint = readLocalCompanionEndpoint(cwd);
|
|
220
|
+
const endpoint = readLocalCompanionEndpoint(cwd, { adapter });
|
|
215
221
|
if (!endpoint || endpoint.kind !== adapter) {
|
|
216
222
|
return { endpoint: null };
|
|
217
223
|
}
|
|
218
224
|
if (await isEndpointReachable(endpoint)) {
|
|
219
225
|
return { endpoint };
|
|
220
226
|
}
|
|
221
|
-
clearLocalCompanionEndpoint(cwd, endpoint.instanceId);
|
|
227
|
+
clearLocalCompanionEndpoint(cwd, endpoint.instanceId, { adapter });
|
|
222
228
|
log(adapter, `Removed stale local companion endpoint for ${cwd}.`);
|
|
223
229
|
return { endpoint: null };
|
|
224
230
|
}
|
|
@@ -229,7 +235,9 @@ function isCompanionAlive(endpoint) {
|
|
|
229
235
|
if (isPidAlive(endpoint.companionPid)) {
|
|
230
236
|
return true;
|
|
231
237
|
}
|
|
232
|
-
clearLocalCompanionOccupancy(endpoint.cwd, endpoint.instanceId
|
|
238
|
+
clearLocalCompanionOccupancy(endpoint.cwd, endpoint.instanceId, {
|
|
239
|
+
adapter: endpoint.kind,
|
|
240
|
+
});
|
|
233
241
|
return false;
|
|
234
242
|
}
|
|
235
243
|
export function buildBackgroundBridgeArgs(entryPath, options) {
|
|
@@ -277,7 +285,9 @@ async function ensureBridgeReady(options) {
|
|
|
277
285
|
if (!lock || !lockProcessAlive) {
|
|
278
286
|
if (lock && !lockProcessAlive) {
|
|
279
287
|
log(options.adapter, `Found stale lock for ${options.cwd} (pid=${lock.pid} dead). Clearing.`);
|
|
280
|
-
clearLocalCompanionEndpoint(options.cwd
|
|
288
|
+
clearLocalCompanionEndpoint(options.cwd, undefined, {
|
|
289
|
+
adapter: options.adapter,
|
|
290
|
+
});
|
|
281
291
|
}
|
|
282
292
|
log(options.adapter, `Starting bridge in background for ${options.cwd}...`);
|
|
283
293
|
startBridgeInBackground(options);
|
|
@@ -322,6 +332,36 @@ async function ensureBridgeReady(options) {
|
|
|
322
332
|
await waitForEndpoint(options.cwd, options.adapter, options.timeoutMs);
|
|
323
333
|
return { shouldOpenVisibleClient: true };
|
|
324
334
|
}
|
|
335
|
+
export async function tryDelegateToDaemon(options, deps = {}) {
|
|
336
|
+
const readEndpoint = deps.readEndpoint ?? readDaemonEndpoint;
|
|
337
|
+
const isEndpointAlive = deps.isEndpointAlive ?? isDaemonEndpointAlive;
|
|
338
|
+
const sendRequest = deps.sendRequest ?? sendDaemonRequest;
|
|
339
|
+
const clearEndpoint = deps.clearEndpoint ?? clearDaemonEndpoint;
|
|
340
|
+
const endpoint = readEndpoint();
|
|
341
|
+
if (!endpoint) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
if (!(await isEndpointAlive(endpoint))) {
|
|
345
|
+
clearEndpoint(endpoint.pid);
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
if (!isSameWorkspaceCwd(endpoint.cwd, options.cwd)) {
|
|
349
|
+
throw new Error(`wechat-daemon is running for ${endpoint.cwd}. This launcher requested ${options.cwd}; v1 daemon switching is limited to its startup cwd.`);
|
|
350
|
+
}
|
|
351
|
+
const response = await sendRequest(endpoint, {
|
|
352
|
+
command: "ensure_slot",
|
|
353
|
+
adapter: options.adapter,
|
|
354
|
+
cwd: options.cwd,
|
|
355
|
+
profile: options.profile,
|
|
356
|
+
cliArgs: options.cliArgs,
|
|
357
|
+
openVisible: true,
|
|
358
|
+
});
|
|
359
|
+
if (!response.ok) {
|
|
360
|
+
throw new Error(response.error);
|
|
361
|
+
}
|
|
362
|
+
log(options.adapter, `Delegated to running wechat-daemon for ${options.cwd}.`);
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
325
365
|
export async function runVisibleClient(options, runners = {}) {
|
|
326
366
|
// Keep the foreground client in-process so Windows does not briefly flash an
|
|
327
367
|
// extra bootstrap console before the real companion UI appears.
|
|
@@ -347,6 +387,9 @@ export async function ensureCompanionStartWechatCredentials(adapter, ensureCrede
|
|
|
347
387
|
export async function runLocalCompanionStart(argv = process.argv.slice(2)) {
|
|
348
388
|
const options = parseCliArgs(argv);
|
|
349
389
|
await ensureCompanionStartWechatCredentials(options.adapter);
|
|
390
|
+
if (await tryDelegateToDaemon(options)) {
|
|
391
|
+
return 0;
|
|
392
|
+
}
|
|
350
393
|
const ready = await ensureBridgeReady(options);
|
|
351
394
|
if (!ready.shouldOpenVisibleClient) {
|
|
352
395
|
return 0;
|
|
@@ -60,7 +60,9 @@ function delay(ms) {
|
|
|
60
60
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
61
61
|
}
|
|
62
62
|
function readMatchingEndpoint(options) {
|
|
63
|
-
const endpoint = readLocalCompanionEndpoint(options.cwd
|
|
63
|
+
const endpoint = readLocalCompanionEndpoint(options.cwd, {
|
|
64
|
+
adapter: options.adapter,
|
|
65
|
+
});
|
|
64
66
|
if (!endpoint || endpoint.kind !== options.adapter) {
|
|
65
67
|
throw new Error(`No active ${options.adapter} bridge endpoint was found for ${options.cwd}. Start "wechat-bridge-${options.adapter}" in that directory first.`);
|
|
66
68
|
}
|
|
@@ -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
|
+
}
|