cli-wechat-bridge 1.1.0 → 1.1.2
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 +33 -2
- package/bin/_run-entry.mjs +24 -0
- package/dist/bridge/bridge-adapters.claude.js +18 -0
- package/dist/bridge/bridge-adapters.codex.js +25 -0
- package/dist/bridge/bridge-adapters.opencode.js +17 -0
- package/dist/bridge/bridge-adapters.shared.js +38 -44
- package/dist/bridge/bridge-final-reply.js +8 -4
- package/dist/bridge/bridge-process-reaper.js +58 -3
- package/dist/bridge/bridge-state.js +14 -6
- package/dist/bridge/bridge-utils.js +62 -7
- package/dist/bridge/claude-hook.js +7 -0
- package/dist/bridge/claude-hooks.js +10 -2
- package/dist/bridge/wechat-bridge.js +17 -1
- package/dist/commands/check-update.js +15 -4
- package/dist/companion/local-companion-start.js +5 -0
- package/dist/daemon/wechat-daemon.js +15 -2
- package/dist/i18n/messages-en.js +89 -2
- package/dist/i18n/messages-zh.js +89 -2
- package/dist/utils/doctor.js +619 -0
- package/dist/utils/version-checker.js +77 -44
- package/dist/wechat/channel-config.js +28 -0
- package/dist/wechat/wechat-transport.js +49 -13
- package/package.json +3 -1
- package/scripts/ensure-node-pty-permissions.mjs +73 -0
|
@@ -1,48 +1,70 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { execSync } from "node:child_process";
|
|
4
3
|
import { CHANNEL_DATA_DIR, ensureChannelDataDir } from "../wechat/channel-config.js";
|
|
5
4
|
const UPDATE_CHECK_FILE = path.join(CHANNEL_DATA_DIR, "update-check.json");
|
|
6
5
|
const CACHE_DURATION_MS = 24 * 60 * 60 * 1000; // 24小时
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
// npm registry 是权威更新源——它是用户实际安装的渠道;GitHub tags 作为回退,
|
|
7
|
+
// 以便在 registry 暂时不可达或包尚未发布时仍能给出最新版本。
|
|
8
|
+
const NPM_PACKAGE_NAME = "cli-wechat-bridge";
|
|
9
|
+
const DEFAULT_NPM_REGISTRY_URL = `https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`;
|
|
10
|
+
const DEFAULT_GITHUB_TAGS_URL = "https://api.github.com/repos/UNLINEARITY/CLI-WeChat-Bridge/tags?per_page=20";
|
|
11
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
12
|
+
function resolveFetchDeps(options) {
|
|
13
|
+
return {
|
|
14
|
+
fetch: options.fetchImpl ?? globalThis.fetch,
|
|
15
|
+
npmRegistryUrl: options.npmRegistryUrl ?? DEFAULT_NPM_REGISTRY_URL,
|
|
16
|
+
githubTagsUrl: options.githubTagsUrl ?? DEFAULT_GITHUB_TAGS_URL,
|
|
17
|
+
timeoutMs: options.timeoutMs ?? FETCH_TIMEOUT_MS,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async function fetchJsonWithTimeout(url, deps) {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timer = setTimeout(() => controller.abort(), deps.timeoutMs);
|
|
11
23
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
24
|
+
const res = await deps.fetch(url, {
|
|
25
|
+
method: "GET",
|
|
26
|
+
signal: controller.signal,
|
|
27
|
+
headers: { Accept: "application/json" },
|
|
17
28
|
});
|
|
18
|
-
if (!
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
// 解析 tags,找到版本号格式(如 0.1.0, 0.2.0 等)
|
|
22
|
-
const versionTags = tagsOutput
|
|
23
|
-
.split('\n')
|
|
24
|
-
.filter(line => {
|
|
25
|
-
// 匹配 refs/tags/0.5.0 格式(不是 v0.5.0)
|
|
26
|
-
return line.includes('refs/tags/') &&
|
|
27
|
-
!line.includes('^{}') &&
|
|
28
|
-
/^\w+\s+refs\/tags\/\d+\.\d+\.\d+$/.test(line);
|
|
29
|
-
})
|
|
30
|
-
.map(line => {
|
|
31
|
-
const match = line.match(/refs\/tags\/(\d+\.\d+\.\d+)$/);
|
|
32
|
-
return match ? match[1] : null;
|
|
33
|
-
})
|
|
34
|
-
.filter((v) => v !== null);
|
|
35
|
-
if (versionTags.length === 0) {
|
|
29
|
+
if (!res.ok) {
|
|
36
30
|
return null;
|
|
37
31
|
}
|
|
38
|
-
|
|
39
|
-
versionTags.sort((a, b) => compareVersions(b, a));
|
|
40
|
-
return versionTags[0] ?? null;
|
|
32
|
+
return await res.json();
|
|
41
33
|
}
|
|
42
|
-
catch
|
|
43
|
-
//
|
|
34
|
+
catch {
|
|
35
|
+
// 网络/超时/解析失败一律视为该源不可用,交由调用方回退。
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function fetchVersionFromNpm(deps) {
|
|
43
|
+
const data = await fetchJsonWithTimeout(deps.npmRegistryUrl, deps);
|
|
44
|
+
if (!data || typeof data !== "object") {
|
|
44
45
|
return null;
|
|
45
46
|
}
|
|
47
|
+
const version = data.version;
|
|
48
|
+
return typeof version === "string" ? parseVersion(version) : null;
|
|
49
|
+
}
|
|
50
|
+
async function fetchVersionFromGithub(deps) {
|
|
51
|
+
const data = await fetchJsonWithTimeout(deps.githubTagsUrl, deps);
|
|
52
|
+
if (!Array.isArray(data)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const versions = data
|
|
56
|
+
.map((tag) => parseVersion(tag.name))
|
|
57
|
+
.filter((v) => v !== null)
|
|
58
|
+
.sort((a, b) => compareVersions(b, a));
|
|
59
|
+
return versions[0] ?? null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 从 npm registry 获取最新版本号(回退到 GitHub tags)。
|
|
63
|
+
* 通过 HTTPS 请求而非本地 git 命令,因此对全局安装的命令(wechat-check-update)同样可用。
|
|
64
|
+
*/
|
|
65
|
+
export async function fetchLatestVersion(options = {}) {
|
|
66
|
+
const deps = resolveFetchDeps(options);
|
|
67
|
+
return (await fetchVersionFromNpm(deps)) ?? (await fetchVersionFromGithub(deps));
|
|
46
68
|
}
|
|
47
69
|
/**
|
|
48
70
|
* 从本地 package.json 读取当前版本
|
|
@@ -85,6 +107,17 @@ function writeUpdateCache(cache) {
|
|
|
85
107
|
// 静默失败,不影响正常使用
|
|
86
108
|
}
|
|
87
109
|
}
|
|
110
|
+
const VERSION_PATTERN = /(\d+\.\d+\.\d+)/;
|
|
111
|
+
/**
|
|
112
|
+
* 从任意字符串中提取版本号,兼容带 "v" 前缀的 tag(如 "v1.1.1")。
|
|
113
|
+
*/
|
|
114
|
+
export function parseVersion(raw) {
|
|
115
|
+
if (typeof raw !== "string") {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const match = VERSION_PATTERN.exec(raw.trim());
|
|
119
|
+
return match ? (match[1] ?? null) : null;
|
|
120
|
+
}
|
|
88
121
|
/**
|
|
89
122
|
* 比较两个版本号
|
|
90
123
|
* @returns 返回值 > 0 表示 version1 更新,< 0 表示 version2 更新,= 0 表示相等
|
|
@@ -146,16 +179,16 @@ export async function checkForUpdate(forceCheck = false) {
|
|
|
146
179
|
*/
|
|
147
180
|
export function formatUpdateMessage(versionInfo) {
|
|
148
181
|
const { current, latest } = versionInfo;
|
|
149
|
-
return `
|
|
150
|
-
[Update Available] Version ${latest} is available (current: ${current})
|
|
151
|
-
|
|
152
|
-
Update instructions:
|
|
153
|
-
cd CLI-WeChat-Bridge
|
|
154
|
-
git pull
|
|
155
|
-
bun install
|
|
156
|
-
npm install -g .
|
|
157
|
-
|
|
158
|
-
For more information:
|
|
159
|
-
https://github.com/UNLINEARITY/CLI-WeChat-Bridge/releases
|
|
182
|
+
return `
|
|
183
|
+
[Update Available] Version ${latest} is available (current: ${current})
|
|
184
|
+
|
|
185
|
+
Update instructions:
|
|
186
|
+
cd CLI-WeChat-Bridge
|
|
187
|
+
git pull
|
|
188
|
+
bun install
|
|
189
|
+
npm install -g .
|
|
190
|
+
|
|
191
|
+
For more information:
|
|
192
|
+
https://github.com/UNLINEARITY/CLI-WeChat-Bridge/releases
|
|
160
193
|
`;
|
|
161
194
|
}
|
|
@@ -18,6 +18,34 @@ export const SYNC_BUF_FILE = path.join(CHANNEL_DATA_DIR, "sync_buf.txt");
|
|
|
18
18
|
export const CONTEXT_CACHE_FILE = path.join(CHANNEL_DATA_DIR, "context_tokens.json");
|
|
19
19
|
export const BRIDGE_STATE_FILE = path.join(CHANNEL_DATA_DIR, "bridge-state.json");
|
|
20
20
|
export const BRIDGE_LOG_FILE = path.join(CHANNEL_DATA_DIR, "bridge.log");
|
|
21
|
+
// Hard cap for bridge.log. Long-running daemons otherwise grow it without bound.
|
|
22
|
+
export const BRIDGE_LOG_MAX_BYTES = 5 * 1024 * 1024; // 5 MiB
|
|
23
|
+
/**
|
|
24
|
+
* Append a line to a log file, trimming it down to the tail half whenever it
|
|
25
|
+
* exceeds BRIDGE_LOG_MAX_BYTES. Trimming is best-effort under concurrent
|
|
26
|
+
* writers (a few lines may be lost during a trim) but keeps the file bounded.
|
|
27
|
+
*/
|
|
28
|
+
export function appendBoundedLog(filePath, line) {
|
|
29
|
+
try {
|
|
30
|
+
const stat = fs.statSync(filePath);
|
|
31
|
+
if (stat.size > BRIDGE_LOG_MAX_BYTES) {
|
|
32
|
+
const keepSize = Math.floor(BRIDGE_LOG_MAX_BYTES / 2);
|
|
33
|
+
const fd = fs.openSync(filePath, "r");
|
|
34
|
+
try {
|
|
35
|
+
const tail = Buffer.alloc(keepSize);
|
|
36
|
+
const bytesRead = fs.readSync(fd, tail, 0, keepSize, stat.size - keepSize);
|
|
37
|
+
fs.writeFileSync(filePath, tail.subarray(0, bytesRead));
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
fs.closeSync(fd);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// File missing or unreadable: fall through to a plain append.
|
|
46
|
+
}
|
|
47
|
+
fs.appendFileSync(filePath, line);
|
|
48
|
+
}
|
|
21
49
|
export const BRIDGE_LOCK_FILE = path.join(CHANNEL_DATA_DIR, "bridge.lock.json");
|
|
22
50
|
export const DAEMON_ENDPOINT_FILE = path.join(CHANNEL_DATA_DIR, "daemon-endpoint.json");
|
|
23
51
|
export const CODEX_PANEL_ENDPOINT_FILE = path.join(CHANNEL_DATA_DIR, "codex-panel-endpoint.json");
|
|
@@ -306,7 +306,12 @@ export function classifyWechatTransportError(error) {
|
|
|
306
306
|
}
|
|
307
307
|
function writeJsonFile(filePath, value) {
|
|
308
308
|
ensureChannelDataDir();
|
|
309
|
-
|
|
309
|
+
const data = JSON.stringify(value, null, 2);
|
|
310
|
+
// Atomic write via temp file + rename, so a crash mid-write cannot leave a
|
|
311
|
+
// truncated/half-written JSON that would break readers on the next launch.
|
|
312
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
313
|
+
fs.writeFileSync(tempPath, data, "utf-8");
|
|
314
|
+
fs.renameSync(tempPath, filePath);
|
|
310
315
|
}
|
|
311
316
|
function randomWechatUin() {
|
|
312
317
|
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
|
@@ -445,7 +450,15 @@ async function getUploadUrl(account, params) {
|
|
|
445
450
|
token: account.token,
|
|
446
451
|
timeoutMs: SEND_TIMEOUT_MS,
|
|
447
452
|
});
|
|
448
|
-
|
|
453
|
+
try {
|
|
454
|
+
return JSON.parse(raw);
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
// A 200-OK but non-JSON body (e.g. an HTML error page, empty body) would
|
|
458
|
+
// otherwise throw an unclassified SyntaxError that bypasses transport error
|
|
459
|
+
// classification/retry. Surface it as a meaningful error instead.
|
|
460
|
+
throw new Error(`getUploadUrl returned a non-JSON response (${raw.length} bytes): ${String(error)}`, { cause: error });
|
|
461
|
+
}
|
|
449
462
|
}
|
|
450
463
|
function buildCdnUploadUrl(cdnBaseUrl, uploadParam, filekey) {
|
|
451
464
|
return `${cdnBaseUrl}/upload?encrypted_query_param=${encodeURIComponent(uploadParam)}&filekey=${encodeURIComponent(filekey)}`;
|
|
@@ -678,11 +691,25 @@ export function buildInboundMessageClaimPath(messageKey, claimsDir = INBOUND_MES
|
|
|
678
691
|
return path.join(claimsDir, fileName);
|
|
679
692
|
}
|
|
680
693
|
export function clearInboundMessageClaims(claimsDir = INBOUND_MESSAGE_CLAIMS_DIR) {
|
|
694
|
+
// Delete claim files individually from a directory snapshot rather than
|
|
695
|
+
// `rmSync(claimsDir, { recursive })`. A recursive rmdir can remove a file a
|
|
696
|
+
// concurrent process just created via tryClaimInboundMessage, silently
|
|
697
|
+
// invalidating its claim and letting the same message be double-processed.
|
|
698
|
+
// Per-file removal only deletes files present at snapshot time.
|
|
699
|
+
let entries;
|
|
681
700
|
try {
|
|
682
|
-
fs.
|
|
701
|
+
entries = fs.readdirSync(claimsDir);
|
|
683
702
|
}
|
|
684
703
|
catch {
|
|
685
|
-
//
|
|
704
|
+
return; // directory does not exist; nothing to clear
|
|
705
|
+
}
|
|
706
|
+
for (const entry of entries) {
|
|
707
|
+
try {
|
|
708
|
+
fs.rmSync(path.join(claimsDir, entry), { force: true });
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
// best effort
|
|
712
|
+
}
|
|
686
713
|
}
|
|
687
714
|
}
|
|
688
715
|
export function tryClaimInboundMessage(messageKey, options = {}) {
|
|
@@ -719,7 +746,11 @@ export function tryClaimInboundMessage(messageKey, options = {}) {
|
|
|
719
746
|
? error.code
|
|
720
747
|
: "";
|
|
721
748
|
if (code !== "EEXIST") {
|
|
722
|
-
|
|
749
|
+
// Real persistence failure (permissions, disk full, missing dir, ...).
|
|
750
|
+
// Returning true here would claim success WITHOUT writing the claim file,
|
|
751
|
+
// letting concurrent processes all think they own this message and
|
|
752
|
+
// double-process it. Fail closed so the message is retried instead.
|
|
753
|
+
return false;
|
|
723
754
|
}
|
|
724
755
|
}
|
|
725
756
|
try {
|
|
@@ -849,23 +880,28 @@ export class WeChatTransport {
|
|
|
849
880
|
if (!extracted.text && extracted.attachments.length === 0) {
|
|
850
881
|
continue;
|
|
851
882
|
}
|
|
852
|
-
const messageKey = buildMessageKey(rawMessage);
|
|
853
|
-
if (!this.rememberMessage(messageKey)) {
|
|
854
|
-
continue;
|
|
855
|
-
}
|
|
856
|
-
if (!tryClaimInboundMessage(buildScopedMessageClaimKey(account.accountId, messageKey))) {
|
|
857
|
-
continue;
|
|
858
|
-
}
|
|
859
883
|
const senderId = rawMessage.from_user_id ?? "unknown";
|
|
860
884
|
if (rawMessage.context_token) {
|
|
861
885
|
this.cacheContextToken(senderId, rawMessage.context_token);
|
|
862
886
|
}
|
|
887
|
+
// Filter pre-start backlog BEFORE claiming/remembering the message so a
|
|
888
|
+
// skipped message is not permanently locked away from other processes.
|
|
889
|
+
// A missing create_time_ms is treated as fresh rather than dropped.
|
|
863
890
|
const createdAtMs = rawMessage.create_time_ms ?? 0;
|
|
864
891
|
if (typeof options.minCreatedAtMs === "number" &&
|
|
865
|
-
|
|
892
|
+
Number.isFinite(createdAtMs) &&
|
|
893
|
+
createdAtMs > 0 &&
|
|
894
|
+
createdAtMs < options.minCreatedAtMs) {
|
|
866
895
|
ignoredBacklogCount += 1;
|
|
867
896
|
continue;
|
|
868
897
|
}
|
|
898
|
+
const messageKey = buildMessageKey(rawMessage);
|
|
899
|
+
if (!this.rememberMessage(messageKey)) {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (!tryClaimInboundMessage(buildScopedMessageClaimKey(account.accountId, messageKey))) {
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
869
905
|
const { attachments, failureLines } = await this.downloadInboundAttachments(extracted.attachments, rawMessage);
|
|
870
906
|
const text = appendInboundAttachmentFailureText(extracted.text, failureLines);
|
|
871
907
|
messages.push({
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cli-wechat-bridge",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, and PowerShell over WeChat",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"bin/",
|
|
8
8
|
"dist/",
|
|
9
|
+
"scripts/ensure-node-pty-permissions.mjs",
|
|
9
10
|
"README.md",
|
|
10
11
|
"LICENSE.txt"
|
|
11
12
|
],
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"wechat-check-update": "bin/wechat-check-update.mjs"
|
|
49
50
|
},
|
|
50
51
|
"scripts": {
|
|
52
|
+
"postinstall": "node scripts/ensure-node-pty-permissions.mjs",
|
|
51
53
|
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
52
54
|
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
53
55
|
"prepack": "npm run build",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Ensure the bundled node-pty `spawn-helper` is executable.
|
|
4
|
+
//
|
|
5
|
+
// node-pty ships its prebuilt `spawn-helper` binaries with mode 0644 (no exec
|
|
6
|
+
// bit) in the published tarball. On macOS/Linux that makes `posix_spawnp`
|
|
7
|
+
// fail when node-pty tries to fork a PTY, so the bridge silently drops to the
|
|
8
|
+
// non-PTY fallback. The Claude adapter relies on PTY interactive mode, so the
|
|
9
|
+
// fallback surfaces to users as:
|
|
10
|
+
//
|
|
11
|
+
// Error: Input must be provided ... when using --print
|
|
12
|
+
//
|
|
13
|
+
// Restoring the exec bit after install fixes PTY spawning. This runs on
|
|
14
|
+
// postinstall and is idempotent; it never fails the install.
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { createRequire } from "node:module";
|
|
19
|
+
|
|
20
|
+
if (process.platform === "win32") {
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
|
|
26
|
+
let nodePtyDir;
|
|
27
|
+
try {
|
|
28
|
+
nodePtyDir = path.dirname(require.resolve("node-pty/package.json"));
|
|
29
|
+
} catch {
|
|
30
|
+
// node-pty is optional in unsupported environments; nothing to fix.
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const candidates = [
|
|
35
|
+
"prebuilds/darwin-arm64/spawn-helper",
|
|
36
|
+
"prebuilds/darwin-x64/spawn-helper",
|
|
37
|
+
"prebuilds/linux-x64/spawn-helper",
|
|
38
|
+
"prebuilds/linux-arm64/spawn-helper",
|
|
39
|
+
"build/Release/spawn-helper",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
let fixed = 0;
|
|
43
|
+
for (const relative of candidates) {
|
|
44
|
+
const helperPath = path.join(nodePtyDir, relative);
|
|
45
|
+
let stats;
|
|
46
|
+
try {
|
|
47
|
+
stats = fs.statSync(helperPath);
|
|
48
|
+
} catch {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!stats.isFile()) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if ((stats.mode & 0o111) !== 0) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
fs.chmodSync(helperPath, stats.mode | 0o755);
|
|
59
|
+
fixed += 1;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.warn(
|
|
62
|
+
`[cli-wechat-bridge] Could not make node-pty spawn-helper executable: ${helperPath}\n` +
|
|
63
|
+
` ${error instanceof Error ? error.message : String(error)}\n` +
|
|
64
|
+
" PTY mode may be unavailable; run --doctor for details.",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (fixed > 0) {
|
|
70
|
+
console.log(
|
|
71
|
+
`[cli-wechat-bridge] Restored executable bit on ${fixed} node-pty spawn-helper binary(ies).`,
|
|
72
|
+
);
|
|
73
|
+
}
|