weacpx 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -45,8 +45,378 @@ var __export = (target, all) => {
45
45
  });
46
46
  };
47
47
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
48
+ var __promiseAll = (args) => Promise.all(args);
48
49
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
49
50
 
51
+ // src/daemon/daemon-status.ts
52
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
53
+ import { dirname } from "node:path";
54
+
55
+ class DaemonStatusStore {
56
+ path;
57
+ constructor(path) {
58
+ this.path = path;
59
+ }
60
+ async load() {
61
+ try {
62
+ const content = await readFile(this.path, "utf8");
63
+ if (content.trim() === "") {
64
+ return null;
65
+ }
66
+ return JSON.parse(content);
67
+ } catch (error) {
68
+ if (error.code === "ENOENT") {
69
+ return null;
70
+ }
71
+ throw error;
72
+ }
73
+ }
74
+ async save(status) {
75
+ await mkdir(dirname(this.path), { recursive: true });
76
+ await writeFile(this.path, JSON.stringify(status, null, 2));
77
+ }
78
+ async clear() {
79
+ await rm(this.path, { force: true });
80
+ }
81
+ }
82
+ var init_daemon_status = () => {};
83
+
84
+ // src/daemon/daemon-controller.ts
85
+ import { mkdir as mkdir2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
86
+ import { dirname as dirname2 } from "node:path";
87
+
88
+ class DaemonController {
89
+ paths;
90
+ deps;
91
+ statusStore;
92
+ startupPollIntervalMs;
93
+ startupTimeoutMs;
94
+ onStartupPoll;
95
+ shutdownPollIntervalMs;
96
+ shutdownTimeoutMs;
97
+ onShutdownPoll;
98
+ constructor(paths, deps) {
99
+ this.paths = paths;
100
+ this.deps = deps;
101
+ this.statusStore = new DaemonStatusStore(paths.statusFile);
102
+ this.startupPollIntervalMs = deps.startupPollIntervalMs ?? 50;
103
+ this.startupTimeoutMs = deps.startupTimeoutMs ?? 5000;
104
+ this.shutdownPollIntervalMs = deps.shutdownPollIntervalMs ?? 50;
105
+ this.shutdownTimeoutMs = deps.shutdownTimeoutMs ?? 5000;
106
+ this.onStartupPoll = deps.onStartupPoll ?? (async () => {
107
+ await new Promise((resolve) => setTimeout(resolve, this.startupPollIntervalMs));
108
+ });
109
+ this.onShutdownPoll = deps.onShutdownPoll ?? (async () => {
110
+ await new Promise((resolve) => setTimeout(resolve, this.shutdownPollIntervalMs));
111
+ });
112
+ }
113
+ async getStatus() {
114
+ const pid = await this.loadPid();
115
+ const status = await this.statusStore.load();
116
+ if (!pid) {
117
+ return { state: "stopped" };
118
+ }
119
+ if (!this.deps.isProcessRunning(pid)) {
120
+ await this.clearRuntimeFiles();
121
+ return { state: "stopped", stale: true };
122
+ }
123
+ if (!status) {
124
+ return { state: "indeterminate", pid, reason: "missing-status" };
125
+ }
126
+ return {
127
+ state: "running",
128
+ pid,
129
+ status
130
+ };
131
+ }
132
+ async start() {
133
+ const current = await this.getStatus();
134
+ if (current.state === "running") {
135
+ return { state: "already-running", pid: current.pid };
136
+ }
137
+ if (current.state === "indeterminate") {
138
+ throw new Error(`weacpx daemon process is already running (pid ${current.pid}) but status metadata is missing`);
139
+ }
140
+ await this.statusStore.clear();
141
+ const pid = await this.deps.spawnDetached();
142
+ await this.writePid(pid);
143
+ await this.waitForStartupMetadata(pid);
144
+ return { state: "started", pid };
145
+ }
146
+ async stop() {
147
+ const pid = await this.loadPid();
148
+ if (!pid) {
149
+ return { state: "stopped", detail: "not-running" };
150
+ }
151
+ if (this.deps.isProcessRunning(pid)) {
152
+ await this.deps.terminateProcess(pid);
153
+ await this.waitForShutdown(pid);
154
+ }
155
+ await this.clearRuntimeFiles();
156
+ return { state: "stopped", detail: "stopped" };
157
+ }
158
+ async loadPid() {
159
+ try {
160
+ const content = await readFile2(this.paths.pidFile, "utf8");
161
+ const pid = Number(content.trim());
162
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
163
+ } catch (error) {
164
+ if (error.code === "ENOENT") {
165
+ return null;
166
+ }
167
+ throw error;
168
+ }
169
+ }
170
+ async writePid(pid) {
171
+ await mkdir2(dirname2(this.paths.pidFile), { recursive: true });
172
+ await writeFile2(this.paths.pidFile, `${pid}
173
+ `);
174
+ }
175
+ async clearRuntimeFiles() {
176
+ await rm2(this.paths.pidFile, { force: true });
177
+ await this.statusStore.clear();
178
+ }
179
+ async waitForStartupMetadata(pid) {
180
+ const deadline = Date.now() + this.startupTimeoutMs;
181
+ while (Date.now() < deadline) {
182
+ const status = await this.statusStore.load();
183
+ if (status?.pid === pid) {
184
+ return;
185
+ }
186
+ if (!this.deps.isProcessRunning(pid)) {
187
+ await this.clearRuntimeFiles();
188
+ throw new Error(`weacpx daemon exited before reporting ready state (pid ${pid})`);
189
+ }
190
+ await this.onStartupPoll();
191
+ }
192
+ throw new Error(`weacpx daemon did not report ready state within ${this.startupTimeoutMs}ms (pid ${pid})`);
193
+ }
194
+ async waitForShutdown(pid) {
195
+ const deadline = Date.now() + this.shutdownTimeoutMs;
196
+ while (Date.now() < deadline) {
197
+ if (!this.deps.isProcessRunning(pid)) {
198
+ return;
199
+ }
200
+ await this.onShutdownPoll();
201
+ }
202
+ if (!this.deps.isProcessRunning(pid)) {
203
+ return;
204
+ }
205
+ throw new Error(`weacpx daemon did not exit within ${this.shutdownTimeoutMs}ms (pid ${pid})`);
206
+ }
207
+ }
208
+ var init_daemon_controller = __esm(() => {
209
+ init_daemon_status();
210
+ });
211
+
212
+ // src/daemon/create-daemon-controller.ts
213
+ import { mkdir as mkdir3, open } from "node:fs/promises";
214
+ import { spawn } from "node:child_process";
215
+ function createDaemonController(paths, options) {
216
+ return new DaemonController(paths, {
217
+ isProcessRunning: options.isProcessRunning ?? defaultIsProcessRunning,
218
+ spawnDetached: async () => {
219
+ await mkdir3(paths.runtimeDir, { recursive: true });
220
+ const stdoutHandle = await open(paths.stdoutLog, "a");
221
+ const stderrHandle = await open(paths.stderrLog, "a");
222
+ try {
223
+ return await (options.spawnProcess ?? defaultSpawnProcess)(buildSpawnRequest(paths, options, stdoutHandle.fd, stderrHandle.fd));
224
+ } finally {
225
+ await stdoutHandle.close();
226
+ await stderrHandle.close();
227
+ }
228
+ },
229
+ terminateProcess: options.terminateProcess ?? defaultTerminateProcess
230
+ });
231
+ }
232
+ function defaultIsProcessRunning(pid) {
233
+ try {
234
+ process.kill(pid, 0);
235
+ return true;
236
+ } catch {
237
+ return false;
238
+ }
239
+ }
240
+ function buildSpawnRequest(paths, options, stdoutFd, stderrFd) {
241
+ const platform = options.platform ?? process.platform;
242
+ if (platform === "win32") {
243
+ return {
244
+ mode: "windows-hidden",
245
+ command: "powershell.exe",
246
+ args: [
247
+ "-NoProfile",
248
+ "-NonInteractive",
249
+ "-EncodedCommand",
250
+ buildWindowsLauncherScript()
251
+ ],
252
+ options: {
253
+ cwd: options.cwd,
254
+ env: {
255
+ ...options.env,
256
+ WEACPX_DAEMON_COMMAND: options.processExecPath,
257
+ WEACPX_DAEMON_ARG0: options.cliEntryPath,
258
+ WEACPX_DAEMON_ARG1: "run",
259
+ WEACPX_DAEMON_CWD: options.cwd,
260
+ WEACPX_DAEMON_STDOUT: paths.stdoutLog,
261
+ WEACPX_DAEMON_STDERR: paths.stderrLog
262
+ },
263
+ stdio: ["ignore", "pipe", "ignore"],
264
+ windowsHide: true
265
+ }
266
+ };
267
+ }
268
+ return {
269
+ mode: "direct",
270
+ command: options.processExecPath,
271
+ args: [options.cliEntryPath, "run"],
272
+ options: {
273
+ cwd: options.cwd,
274
+ detached: true,
275
+ env: options.env,
276
+ stdio: ["ignore", stdoutFd, stderrFd]
277
+ }
278
+ };
279
+ }
280
+ function buildWindowsLauncherScript() {
281
+ const script = [
282
+ "$process = Start-Process -FilePath $env:WEACPX_DAEMON_COMMAND `",
283
+ " -ArgumentList @($env:WEACPX_DAEMON_ARG0, $env:WEACPX_DAEMON_ARG1) `",
284
+ " -WorkingDirectory $env:WEACPX_DAEMON_CWD `",
285
+ " -RedirectStandardOutput $env:WEACPX_DAEMON_STDOUT `",
286
+ " -RedirectStandardError $env:WEACPX_DAEMON_STDERR `",
287
+ " -WindowStyle Hidden `",
288
+ " -PassThru",
289
+ "[Console]::Out.WriteLine($process.Id)"
290
+ ].join(`
291
+ `);
292
+ return Buffer.from(script, "utf16le").toString("base64");
293
+ }
294
+ async function defaultSpawnProcess(request) {
295
+ if (request.mode === "windows-hidden") {
296
+ return await spawnWindowsHiddenProcess(request);
297
+ }
298
+ const child = spawn(request.command, request.args, request.options);
299
+ child.unref();
300
+ return child.pid ?? 0;
301
+ }
302
+ async function spawnWindowsHiddenProcess(request) {
303
+ return await new Promise((resolve, reject) => {
304
+ const child = spawn(request.command, request.args, request.options);
305
+ let stdout = "";
306
+ let settled = false;
307
+ child.stdout?.setEncoding("utf8");
308
+ child.stdout?.on("data", (chunk) => {
309
+ stdout += chunk;
310
+ if (settled) {
311
+ return;
312
+ }
313
+ const pid = Number.parseInt(stdout.trim(), 10);
314
+ if (!Number.isFinite(pid) || pid <= 0) {
315
+ return;
316
+ }
317
+ settled = true;
318
+ child.stdout?.destroy();
319
+ child.unref();
320
+ resolve(pid);
321
+ });
322
+ child.on("error", (error) => {
323
+ if (settled) {
324
+ return;
325
+ }
326
+ settled = true;
327
+ reject(error);
328
+ });
329
+ child.on("close", (code) => {
330
+ if (settled) {
331
+ return;
332
+ }
333
+ if (code !== 0) {
334
+ settled = true;
335
+ reject(new Error(`Failed to launch hidden Windows daemon process (exit ${code ?? 1})`));
336
+ return;
337
+ }
338
+ const pid = Number.parseInt(stdout.trim(), 10);
339
+ if (!Number.isFinite(pid) || pid <= 0) {
340
+ settled = true;
341
+ reject(new Error("Failed to read daemon pid from hidden Windows launcher"));
342
+ return;
343
+ }
344
+ settled = true;
345
+ resolve(pid);
346
+ });
347
+ });
348
+ }
349
+ async function defaultTerminateProcess(pid) {
350
+ await terminateProcessTree(pid);
351
+ }
352
+ async function terminateProcessTree(pid, platform = process.platform, runCommand = defaultRunProcessCommand, killProcess = (targetPid, signal) => {
353
+ process.kill(targetPid, signal);
354
+ }, isProcessRunning = defaultIsProcessRunning) {
355
+ if (platform === "win32") {
356
+ try {
357
+ await runCommand("taskkill", ["/PID", String(pid), "/T", "/F"]);
358
+ } catch {}
359
+ return;
360
+ }
361
+ const targetPid = pid > 0 ? -pid : pid;
362
+ try {
363
+ killProcess(targetPid, "SIGTERM");
364
+ } catch {
365
+ return;
366
+ }
367
+ const deadline = Date.now() + 5000;
368
+ while (Date.now() < deadline) {
369
+ if (!isProcessRunning(targetPid)) {
370
+ return;
371
+ }
372
+ await new Promise((resolve) => setTimeout(resolve, 100));
373
+ }
374
+ try {
375
+ killProcess(targetPid, "SIGKILL");
376
+ } catch {}
377
+ }
378
+ async function defaultRunProcessCommand(command, args) {
379
+ return await new Promise((resolve, reject) => {
380
+ const child = spawn(command, args, { stdio: "ignore" });
381
+ child.on("error", reject);
382
+ child.on("close", (code) => resolve(code ?? 1));
383
+ });
384
+ }
385
+ var init_create_daemon_controller = __esm(() => {
386
+ init_daemon_controller();
387
+ });
388
+
389
+ // src/daemon/daemon-files.ts
390
+ import { join } from "node:path";
391
+ function resolveDaemonPaths(options) {
392
+ const runtimeDir = options.runtimeDir ?? join(options.home, ".weacpx", "runtime");
393
+ return {
394
+ runtimeDir,
395
+ pidFile: join(runtimeDir, "daemon.pid"),
396
+ statusFile: join(runtimeDir, "status.json"),
397
+ stdoutLog: join(runtimeDir, "stdout.log"),
398
+ stderrLog: join(runtimeDir, "stderr.log"),
399
+ appLog: join(runtimeDir, "app.log")
400
+ };
401
+ }
402
+ var init_daemon_files = () => {};
403
+
404
+ // src/version.ts
405
+ import fs from "node:fs";
406
+ import path from "node:path";
407
+ import { fileURLToPath } from "node:url";
408
+ function readVersion() {
409
+ try {
410
+ const dir = path.dirname(fileURLToPath(import.meta.url));
411
+ const pkgPath = path.resolve(dir, "..", "..", "package.json");
412
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
413
+ return pkg.version ?? "unknown";
414
+ } catch {
415
+ return "unknown";
416
+ }
417
+ }
418
+ var init_version = () => {};
419
+
50
420
  // src/weixin/monitor/consumer-lock.ts
51
421
  import { mkdir as mkdir5, open as open2, readFile as readFile3, rm as rm4 } from "node:fs/promises";
52
422
  import { dirname as dirname4, join as join2 } from "node:path";
@@ -128,9 +498,9 @@ function createWeixinConsumerLock(options = {}) {
128
498
  }
129
499
  };
130
500
  }
131
- async function loadLockMetadata(path) {
501
+ async function loadLockMetadata(path2) {
132
502
  try {
133
- const raw = await readFile3(path, "utf8");
503
+ const raw = await readFile3(path2, "utf8");
134
504
  const parsed = JSON.parse(raw);
135
505
  if (!parsed || typeof parsed.pid !== "number" || !parsed.mode || !parsed.configPath || !parsed.statePath) {
136
506
  return null;
@@ -1189,15 +1559,15 @@ var require_main = __commonJS((exports, module) => {
1189
1559
 
1190
1560
  // src/weixin/storage/state-dir.ts
1191
1561
  import os from "node:os";
1192
- import path from "node:path";
1562
+ import path2 from "node:path";
1193
1563
  function resolveStateDir() {
1194
- return process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim() || path.join(os.homedir(), ".openclaw");
1564
+ return process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim() || path2.join(os.homedir(), ".openclaw");
1195
1565
  }
1196
1566
  var init_state_dir = () => {};
1197
1567
 
1198
1568
  // src/weixin/auth/accounts.ts
1199
- import fs from "node:fs";
1200
- import path2 from "node:path";
1569
+ import fs2 from "node:fs";
1570
+ import path3 from "node:path";
1201
1571
  function normalizeAccountId(raw) {
1202
1572
  return raw.trim().toLowerCase().replace(/[@.]/g, "-");
1203
1573
  }
@@ -1211,17 +1581,17 @@ function deriveRawAccountId(normalizedId) {
1211
1581
  return;
1212
1582
  }
1213
1583
  function resolveWeixinStateDir() {
1214
- return path2.join(resolveStateDir(), "openclaw-weixin");
1584
+ return path3.join(resolveStateDir(), "openclaw-weixin");
1215
1585
  }
1216
1586
  function resolveAccountIndexPath() {
1217
- return path2.join(resolveWeixinStateDir(), "accounts.json");
1587
+ return path3.join(resolveWeixinStateDir(), "accounts.json");
1218
1588
  }
1219
1589
  function listIndexedWeixinAccountIds() {
1220
1590
  const filePath = resolveAccountIndexPath();
1221
1591
  try {
1222
- if (!fs.existsSync(filePath))
1592
+ if (!fs2.existsSync(filePath))
1223
1593
  return [];
1224
- const raw = fs.readFileSync(filePath, "utf-8");
1594
+ const raw = fs2.readFileSync(filePath, "utf-8");
1225
1595
  const parsed = JSON.parse(raw);
1226
1596
  if (!Array.isArray(parsed))
1227
1597
  return [];
@@ -1232,21 +1602,21 @@ function listIndexedWeixinAccountIds() {
1232
1602
  }
1233
1603
  function registerWeixinAccountId(accountId) {
1234
1604
  const dir = resolveWeixinStateDir();
1235
- fs.mkdirSync(dir, { recursive: true });
1236
- fs.writeFileSync(resolveAccountIndexPath(), JSON.stringify([accountId], null, 2), "utf-8");
1605
+ fs2.mkdirSync(dir, { recursive: true });
1606
+ fs2.writeFileSync(resolveAccountIndexPath(), JSON.stringify([accountId], null, 2), "utf-8");
1237
1607
  }
1238
1608
  function resolveAccountsDir() {
1239
- return path2.join(resolveWeixinStateDir(), "accounts");
1609
+ return path3.join(resolveWeixinStateDir(), "accounts");
1240
1610
  }
1241
1611
  function resolveAccountPath(accountId) {
1242
- return path2.join(resolveAccountsDir(), `${accountId}.json`);
1612
+ return path3.join(resolveAccountsDir(), `${accountId}.json`);
1243
1613
  }
1244
1614
  function loadLegacyToken() {
1245
- const legacyPath = path2.join(resolveStateDir(), "credentials", "openclaw-weixin", "credentials.json");
1615
+ const legacyPath = path3.join(resolveStateDir(), "credentials", "openclaw-weixin", "credentials.json");
1246
1616
  try {
1247
- if (!fs.existsSync(legacyPath))
1617
+ if (!fs2.existsSync(legacyPath))
1248
1618
  return;
1249
- const raw = fs.readFileSync(legacyPath, "utf-8");
1619
+ const raw = fs2.readFileSync(legacyPath, "utf-8");
1250
1620
  const parsed = JSON.parse(raw);
1251
1621
  return typeof parsed.token === "string" ? parsed.token : undefined;
1252
1622
  } catch {
@@ -1255,8 +1625,8 @@ function loadLegacyToken() {
1255
1625
  }
1256
1626
  function readAccountFile(filePath) {
1257
1627
  try {
1258
- if (fs.existsSync(filePath)) {
1259
- return JSON.parse(fs.readFileSync(filePath, "utf-8"));
1628
+ if (fs2.existsSync(filePath)) {
1629
+ return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
1260
1630
  }
1261
1631
  } catch {}
1262
1632
  return null;
@@ -1278,7 +1648,7 @@ function loadWeixinAccount(accountId) {
1278
1648
  }
1279
1649
  function saveWeixinAccount(accountId, update) {
1280
1650
  const dir = resolveAccountsDir();
1281
- fs.mkdirSync(dir, { recursive: true });
1651
+ fs2.mkdirSync(dir, { recursive: true });
1282
1652
  const existing = loadWeixinAccount(accountId) ?? {};
1283
1653
  const token = update.token?.trim() || existing.token;
1284
1654
  const baseUrl = update.baseUrl?.trim() || existing.baseUrl;
@@ -1289,14 +1659,14 @@ function saveWeixinAccount(accountId, update) {
1289
1659
  ...userId ? { userId } : {}
1290
1660
  };
1291
1661
  const filePath = resolveAccountPath(accountId);
1292
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
1662
+ fs2.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
1293
1663
  try {
1294
- fs.chmodSync(filePath, 384);
1664
+ fs2.chmodSync(filePath, 384);
1295
1665
  } catch {}
1296
1666
  }
1297
1667
  function clearWeixinAccount(accountId) {
1298
1668
  try {
1299
- fs.unlinkSync(resolveAccountPath(accountId));
1669
+ fs2.unlinkSync(resolveAccountPath(accountId));
1300
1670
  } catch {}
1301
1671
  }
1302
1672
  function clearAllWeixinAccounts() {
@@ -1305,21 +1675,21 @@ function clearAllWeixinAccounts() {
1305
1675
  clearWeixinAccount(id);
1306
1676
  }
1307
1677
  try {
1308
- fs.writeFileSync(resolveAccountIndexPath(), "[]", "utf-8");
1678
+ fs2.writeFileSync(resolveAccountIndexPath(), "[]", "utf-8");
1309
1679
  } catch {}
1310
1680
  }
1311
1681
  function resolveConfigPath() {
1312
1682
  const envPath = process.env.OPENCLAW_CONFIG?.trim();
1313
1683
  if (envPath)
1314
1684
  return envPath;
1315
- return path2.join(resolveStateDir(), "openclaw.json");
1685
+ return path3.join(resolveStateDir(), "openclaw.json");
1316
1686
  }
1317
1687
  function loadConfigRouteTag(accountId) {
1318
1688
  try {
1319
1689
  const configPath = resolveConfigPath();
1320
- if (!fs.existsSync(configPath))
1690
+ if (!fs2.existsSync(configPath))
1321
1691
  return;
1322
- const raw = fs.readFileSync(configPath, "utf-8");
1692
+ const raw = fs2.readFileSync(configPath, "utf-8");
1323
1693
  const cfg = JSON.parse(raw);
1324
1694
  const channels = cfg.channels;
1325
1695
  const section = channels?.["openclaw-weixin"];
@@ -1367,9 +1737,9 @@ var init_accounts = __esm(() => {
1367
1737
  });
1368
1738
 
1369
1739
  // src/weixin/util/logger.ts
1370
- import fs2 from "node:fs";
1740
+ import fs3 from "node:fs";
1371
1741
  import os2 from "node:os";
1372
- import path3 from "node:path";
1742
+ import path4 from "node:path";
1373
1743
  function resolveMinLevel() {
1374
1744
  const env = process.env.OPENCLAW_LOG_LEVEL?.toUpperCase();
1375
1745
  if (env && env in LEVEL_IDS)
@@ -1388,7 +1758,7 @@ function localDateKey(now) {
1388
1758
  }
1389
1759
  function resolveMainLogPath() {
1390
1760
  const dateKey = localDateKey(new Date);
1391
- return path3.join(MAIN_LOG_DIR, `openclaw-${dateKey}.log`);
1761
+ return path4.join(MAIN_LOG_DIR, `openclaw-${dateKey}.log`);
1392
1762
  }
1393
1763
  function buildLoggerName(accountId) {
1394
1764
  return accountId ? `${SUBSYSTEM}/${accountId}` : SUBSYSTEM;
@@ -1417,10 +1787,10 @@ function writeLog(level, message, accountId) {
1417
1787
  });
1418
1788
  try {
1419
1789
  if (!logDirEnsured) {
1420
- fs2.mkdirSync(MAIN_LOG_DIR, { recursive: true });
1790
+ fs3.mkdirSync(MAIN_LOG_DIR, { recursive: true });
1421
1791
  logDirEnsured = true;
1422
1792
  }
1423
- fs2.appendFileSync(resolveMainLogPath(), `${entry}
1793
+ fs3.appendFileSync(resolveMainLogPath(), `${entry}
1424
1794
  `, "utf-8");
1425
1795
  } catch {}
1426
1796
  }
@@ -1449,7 +1819,7 @@ function createLogger(accountId) {
1449
1819
  }
1450
1820
  var MAIN_LOG_DIR, SUBSYSTEM = "gateway/channels/openclaw-weixin", RUNTIME = "node", RUNTIME_VERSION, HOSTNAME, PARENT_NAMES, LEVEL_IDS, DEFAULT_LOG_LEVEL = "INFO", minLevelId, logDirEnsured = false, logger;
1451
1821
  var init_logger = __esm(() => {
1452
- MAIN_LOG_DIR = path3.join("/tmp", "openclaw");
1822
+ MAIN_LOG_DIR = path4.join("/tmp", "openclaw");
1453
1823
  RUNTIME_VERSION = process.versions.node;
1454
1824
  HOSTNAME = os2.hostname() || "unknown";
1455
1825
  PARENT_NAMES = ["openclaw"];
@@ -1500,19 +1870,6 @@ var DEFAULT_BODY_MAX_LEN = 200, DEFAULT_TOKEN_PREFIX_LEN = 6;
1500
1870
 
1501
1871
  // src/weixin/api/api.ts
1502
1872
  import crypto from "node:crypto";
1503
- import fs3 from "node:fs";
1504
- import path4 from "node:path";
1505
- import { fileURLToPath } from "node:url";
1506
- function readChannelVersion() {
1507
- try {
1508
- const dir = path4.dirname(fileURLToPath(import.meta.url));
1509
- const pkgPath = path4.resolve(dir, "..", "..", "package.json");
1510
- const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
1511
- return pkg.version ?? "unknown";
1512
- } catch {
1513
- return "unknown";
1514
- }
1515
- }
1516
1873
  function buildBaseInfo() {
1517
1874
  return { channel_version: CHANNEL_VERSION };
1518
1875
  }
@@ -1688,9 +2045,10 @@ async function sendTyping(params) {
1688
2045
  }
1689
2046
  var CHANNEL_VERSION, DEFAULT_LONG_POLL_TIMEOUT_MS = 35000, DEFAULT_API_TIMEOUT_MS = 15000, DEFAULT_CONFIG_TIMEOUT_MS = 1e4;
1690
2047
  var init_api = __esm(() => {
2048
+ init_version();
1691
2049
  init_accounts();
1692
2050
  init_logger();
1693
- CHANNEL_VERSION = readChannelVersion();
2051
+ CHANNEL_VERSION = readVersion();
1694
2052
  });
1695
2053
 
1696
2054
  // src/weixin/auth/login-qr.ts
@@ -2637,7 +2995,7 @@ function markdownToPlainText(text) {
2637
2995
  result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
2638
2996
  result = result.replace(/^\|[\s:|-]+\|$/gm, "");
2639
2997
  result = result.replace(/^\|(.+)\|$/gm, (_, inner) => inner.split("|").map((cell) => cell.trim()).join(" "));
2640
- result = result.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/_(.+?)_/g, "$1").replace(/~~(.+?)~~/g, "$1").replace(/`(.+?)`/g, "$1");
2998
+ result = result.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/~~(.+?)~~/g, "$1").replace(/`(.+?)`/g, "$1");
2641
2999
  return result;
2642
3000
  }
2643
3001
  function buildTextMessageReq(params) {
@@ -3253,6 +3611,9 @@ async function monitorWeixinProvider(opts) {
3253
3611
  log(`[weixin] no previous sync buf, starting fresh`);
3254
3612
  }
3255
3613
  const configManager = new WeixinConfigManager({ baseUrl, token }, log);
3614
+ const seenMessageIds = new Set;
3615
+ const messageIdOrder = [];
3616
+ const DEDUP_WINDOW = 100;
3256
3617
  let nextTimeoutMs = longPollTimeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS2;
3257
3618
  let consecutiveFailures = 0;
3258
3619
  while (!abortSignal?.aborted) {
@@ -3296,6 +3657,18 @@ async function monitorWeixinProvider(opts) {
3296
3657
  }
3297
3658
  const list = resp.msgs ?? [];
3298
3659
  for (const full of list) {
3660
+ const msgId = full.message_id;
3661
+ if (msgId != null) {
3662
+ if (seenMessageIds.has(msgId)) {
3663
+ aLog.info(`duplicate message skipped: message_id=${msgId}`);
3664
+ continue;
3665
+ }
3666
+ seenMessageIds.add(msgId);
3667
+ messageIdOrder.push(msgId);
3668
+ if (messageIdOrder.length > DEDUP_WINDOW) {
3669
+ seenMessageIds.delete(messageIdOrder.shift());
3670
+ }
3671
+ }
3299
3672
  aLog.info(`inbound: from=${full.from_user_id} types=${full.item_list?.map((i) => i.type).join(",") ?? "none"}`);
3300
3673
  const fromUserId = full.from_user_id ?? "";
3301
3674
  const cachedConfig = await configManager.getForUser(fromUserId, full.context_token);
@@ -4485,23 +4858,23 @@ async function handleCancel(context, chatKey) {
4485
4858
  async function handleSessionReset(context, chatKey) {
4486
4859
  return await context.lifecycle.resetCurrentSession(chatKey);
4487
4860
  }
4861
+ async function promptWithSession(context, session, text, reply) {
4862
+ const effectiveReplyMode = session.replyMode ?? context.config?.wechat.replyMode ?? "stream";
4863
+ const transportReply = effectiveReplyMode === "stream" ? reply : undefined;
4864
+ const result = await context.interaction.promptTransportSession(session, text, transportReply);
4865
+ return { text: transportReply ? undefined : result.text };
4866
+ }
4488
4867
  async function handlePrompt(context, chatKey, text, reply) {
4489
4868
  const session = await context.sessions.getCurrentSession(chatKey);
4490
4869
  if (!session) {
4491
4870
  return { text: NO_CURRENT_SESSION_TEXT };
4492
4871
  }
4493
4872
  try {
4494
- const effectiveReplyMode = session.replyMode ?? context.config?.wechat.replyMode ?? "stream";
4495
- const transportReply = effectiveReplyMode === "stream" ? reply : undefined;
4496
- const result = await context.interaction.promptTransportSession(session, text, transportReply);
4497
- return { text: result.text };
4873
+ return await promptWithSession(context, session, text, reply);
4498
4874
  } catch (error) {
4499
4875
  const recovered = await context.recovery.tryRecoverMissingSession(session, error);
4500
4876
  if (recovered) {
4501
- const effectiveReplyMode = recovered.replyMode ?? context.config?.wechat.replyMode ?? "stream";
4502
- const transportReply = effectiveReplyMode === "stream" ? reply : undefined;
4503
- const result = await context.interaction.promptTransportSession(recovered, text, transportReply);
4504
- return { text: result.text };
4877
+ return await promptWithSession(context, recovered, text, reply);
4505
4878
  }
4506
4879
  return context.recovery.renderTransportError(session, error);
4507
4880
  }
@@ -4892,7 +5265,7 @@ async function handleSessionShortcutCommand(context, ops, chatKey, agent, target
4892
5265
  `)
4893
5266
  };
4894
5267
  }
4895
- const session = ops.resolveSession(alias, agent, workspace.name, `${workspace.name}:${alias}`);
5268
+ const session = ops.resolveSession(alias, agent, workspace.name, alias);
4896
5269
  try {
4897
5270
  await ops.ensureTransportSession(session);
4898
5271
  const exists = await ops.checkTransportSession(session);
@@ -5635,11 +6008,18 @@ var init_ensure_config = __esm(() => {
5635
6008
 
5636
6009
  // src/config/resolve-acpx-command.ts
5637
6010
  import { readFileSync } from "node:fs";
5638
- import { posix, win32 } from "node:path";
5639
6011
  import { createRequire as createRequire2 } from "node:module";
6012
+ import { posix, win32 } from "node:path";
5640
6013
  function resolveAcpxCommand(options = {}) {
6014
+ return resolveAcpxCommandMetadata(options).command;
6015
+ }
6016
+ function resolveAcpxCommandMetadata(options = {}) {
5641
6017
  if (options.configuredCommand) {
5642
- return options.configuredCommand;
6018
+ return {
6019
+ command: options.configuredCommand,
6020
+ source: "config",
6021
+ explanation: "transport.command is set, so the configured command wins."
6022
+ };
5643
6023
  }
5644
6024
  const platform = options.platform ?? process.platform;
5645
6025
  const resolvePackageJson = options.resolvePackageJson ?? ((id) => require2.resolve(id));
@@ -5651,10 +6031,18 @@ function resolveAcpxCommand(options = {}) {
5651
6031
  const packageDir = pathApi.dirname(packageJsonPath);
5652
6032
  const binPath = typeof pkg.bin === "string" ? pkg.bin : pkg.bin && typeof pkg.bin.acpx === "string" ? pkg.bin.acpx : null;
5653
6033
  if (binPath) {
5654
- return pathApi.resolve(packageDir, binPath);
6034
+ return {
6035
+ command: pathApi.resolve(packageDir, binPath),
6036
+ source: "bundled",
6037
+ explanation: "transport.command is unset, so the bundled acpx dependency is used."
6038
+ };
5655
6039
  }
5656
6040
  } catch {}
5657
- return "acpx";
6041
+ return {
6042
+ command: "acpx",
6043
+ source: "PATH",
6044
+ explanation: "transport.command is unset and no bundled acpx was found, so PATH is the fallback."
6045
+ };
5658
6046
  }
5659
6047
  var require2;
5660
6048
  var init_resolve_acpx_command = __esm(() => {
@@ -6634,6 +7022,7 @@ var init_acpx_cli_transport = __esm(() => {
6634
7022
  var exports_main = {};
6635
7023
  __export(exports_main, {
6636
7024
  resolveRuntimePaths: () => resolveRuntimePaths,
7025
+ resolveBridgeEntryPath: () => resolveBridgeEntryPath,
6637
7026
  main: () => main2,
6638
7027
  buildApp: () => buildApp
6639
7028
  });
@@ -6737,362 +7126,974 @@ var init_main = __esm(async () => {
6737
7126
  if (false) {}
6738
7127
  });
6739
7128
 
6740
- // src/cli.ts
6741
- import { homedir as homedir6 } from "node:os";
6742
- import { sep } from "node:path";
6743
- import { fileURLToPath as fileURLToPath4 } from "node:url";
6744
-
6745
- // src/daemon/create-daemon-controller.ts
6746
- import { mkdir as mkdir3, open } from "node:fs/promises";
6747
- import { spawn } from "node:child_process";
6748
-
6749
- // src/daemon/daemon-controller.ts
6750
- import { mkdir as mkdir2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
6751
- import { dirname as dirname2 } from "node:path";
7129
+ // src/doctor/checks/acpx-check.ts
7130
+ import { spawn as spawn4 } from "node:child_process";
7131
+ async function checkAcpx(options = {}) {
7132
+ const runtimePaths = (options.resolveRuntimePaths ?? resolveRuntimePaths)();
7133
+ try {
7134
+ const config = await (options.loadConfig ?? loadConfig)(runtimePaths.configPath);
7135
+ const metadata = (options.resolveAcpxCommandMetadata ?? resolveAcpxCommandMetadata)({
7136
+ configuredCommand: config.transport.command
7137
+ });
7138
+ const version = await (options.runVersion ?? defaultRunVersion)(metadata.command);
7139
+ return {
7140
+ id: "acpx",
7141
+ label: "acpx",
7142
+ severity: "pass",
7143
+ summary: `resolved ${metadata.command} (${version})`,
7144
+ details: buildDetails(metadata, version, options.verbose),
7145
+ metadata: {
7146
+ command: metadata.command,
7147
+ source: metadata.source,
7148
+ version
7149
+ }
7150
+ };
7151
+ } catch (error) {
7152
+ const message = formatError(error);
7153
+ const details = [`config path: ${runtimePaths.configPath}`, `error: ${message}`];
7154
+ return {
7155
+ id: "acpx",
7156
+ label: "acpx",
7157
+ severity: "fail",
7158
+ summary: "acpx version check failed",
7159
+ details
7160
+ };
7161
+ }
7162
+ }
7163
+ function buildDetails(metadata, version, verbose) {
7164
+ const details = [
7165
+ `command: ${metadata.command}`,
7166
+ `source: ${metadata.source}`,
7167
+ `version: ${version}`
7168
+ ];
7169
+ if (verbose) {
7170
+ details.push(`resolution: ${metadata.explanation}`);
7171
+ }
7172
+ return details;
7173
+ }
7174
+ async function defaultRunVersion(command) {
7175
+ const spawnSpec = resolveSpawnCommand(command, ["--version"]);
7176
+ return await new Promise((resolve2, reject) => {
7177
+ const child = spawn4(spawnSpec.command, spawnSpec.args, {
7178
+ stdio: ["ignore", "pipe", "pipe"]
7179
+ });
7180
+ let stdout = "";
7181
+ let stderr = "";
7182
+ child.stdout.on("data", (chunk) => {
7183
+ stdout += String(chunk);
7184
+ });
7185
+ child.stderr.on("data", (chunk) => {
7186
+ stderr += String(chunk);
7187
+ });
7188
+ child.on("error", reject);
7189
+ child.on("close", (code) => {
7190
+ if (code === 0) {
7191
+ const version = stdout.trim() || stderr.trim();
7192
+ if (version.length > 0) {
7193
+ resolve2(version);
7194
+ return;
7195
+ }
7196
+ }
7197
+ reject(new Error(stderr.trim() || stdout.trim() || `acpx --version exited with code ${code ?? 1}`));
7198
+ });
7199
+ });
7200
+ }
7201
+ function formatError(error) {
7202
+ return error instanceof Error ? error.message : String(error);
7203
+ }
7204
+ var init_acpx_check = __esm(async () => {
7205
+ init_load_config();
7206
+ init_resolve_acpx_command();
7207
+ init_spawn_command();
7208
+ await init_main();
7209
+ });
6752
7210
 
6753
- // src/daemon/daemon-status.ts
6754
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
6755
- import { dirname } from "node:path";
7211
+ // src/doctor/checks/bridge-check.ts
7212
+ async function checkBridge(options = {}) {
7213
+ const runtimePaths = (options.resolveRuntimePaths ?? resolveRuntimePaths)();
7214
+ try {
7215
+ const config = await (options.loadConfig ?? loadConfig)(runtimePaths.configPath);
7216
+ if (config.transport.type === "acpx-cli") {
7217
+ return {
7218
+ id: "bridge",
7219
+ label: "Bridge",
7220
+ severity: "skip",
7221
+ summary: "bridge check skipped for acpx-cli transport"
7222
+ };
7223
+ }
7224
+ const metadata = (options.resolveAcpxCommandMetadata ?? resolveAcpxCommandMetadata)({
7225
+ configuredCommand: config.transport.command
7226
+ });
7227
+ const client = await (options.spawnAcpxBridgeClient ?? spawnAcpxBridgeClient)({
7228
+ acpxCommand: metadata.command,
7229
+ bridgeEntryPath: (options.resolveBridgeEntryPath ?? resolveBridgeEntryPath)(),
7230
+ cwd: options.cwd ?? process.cwd(),
7231
+ permissionMode: config.transport.permissionMode,
7232
+ nonInteractivePermissions: config.transport.nonInteractivePermissions
7233
+ });
7234
+ try {
7235
+ return {
7236
+ id: "bridge",
7237
+ label: "Bridge",
7238
+ severity: "pass",
7239
+ summary: "bridge responded to ping",
7240
+ details: buildDetails2(metadata, options.verbose),
7241
+ metadata: {
7242
+ acpxCommand: metadata.command,
7243
+ source: metadata.source
7244
+ }
7245
+ };
7246
+ } finally {
7247
+ await client.dispose();
7248
+ }
7249
+ } catch (error) {
7250
+ return {
7251
+ id: "bridge",
7252
+ label: "Bridge",
7253
+ severity: "fail",
7254
+ summary: "bridge startup failed",
7255
+ details: [`config path: ${runtimePaths.configPath}`, `error: ${formatError2(error)}`]
7256
+ };
7257
+ }
7258
+ }
7259
+ function buildDetails2(metadata, verbose) {
7260
+ const details = [`acpx command: ${metadata.command}`, `source: ${metadata.source}`];
7261
+ if (verbose) {
7262
+ details.push(`resolution: ${metadata.explanation}`);
7263
+ }
7264
+ return details;
7265
+ }
7266
+ function formatError2(error) {
7267
+ return error instanceof Error ? error.message : String(error);
7268
+ }
7269
+ var init_bridge_check = __esm(async () => {
7270
+ init_load_config();
7271
+ init_resolve_acpx_command();
7272
+ init_acpx_bridge_client();
7273
+ await init_main();
7274
+ });
6756
7275
 
6757
- class DaemonStatusStore {
6758
- path;
6759
- constructor(path) {
6760
- this.path = path;
7276
+ // src/doctor/checks/config-check.ts
7277
+ async function checkConfig(options = {}) {
7278
+ const runtimePaths = (options.resolveRuntimePaths ?? resolveRuntimePaths)();
7279
+ const configPath = runtimePaths.configPath;
7280
+ try {
7281
+ const config = await (options.loadConfig ?? loadConfig)(configPath);
7282
+ return {
7283
+ id: "config",
7284
+ label: "Config",
7285
+ severity: "pass",
7286
+ summary: "configuration loaded",
7287
+ details: [`config path: ${configPath}`],
7288
+ metadata: {
7289
+ configPath,
7290
+ config
7291
+ }
7292
+ };
7293
+ } catch (error) {
7294
+ return {
7295
+ id: "config",
7296
+ label: "Config",
7297
+ severity: "fail",
7298
+ summary: "configuration is invalid",
7299
+ details: [`config path: ${configPath}`, `error: ${formatError3(error)}`]
7300
+ };
6761
7301
  }
6762
- async load() {
6763
- try {
6764
- const content = await readFile(this.path, "utf8");
6765
- if (content.trim() === "") {
6766
- return null;
7302
+ }
7303
+ function formatError3(error) {
7304
+ if (error instanceof Error) {
7305
+ return error.message;
7306
+ }
7307
+ return String(error);
7308
+ }
7309
+ var init_config_check = __esm(async () => {
7310
+ init_load_config();
7311
+ await init_main();
7312
+ });
7313
+
7314
+ // src/doctor/checks/daemon-check.ts
7315
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
7316
+ import { homedir as homedir6 } from "node:os";
7317
+ async function checkDaemon(options = {}) {
7318
+ const home = options.home ?? process.env.HOME ?? homedir6();
7319
+ const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({ home });
7320
+ const controller = createDaemonController(paths, {
7321
+ processExecPath: options.processExecPath ?? process.execPath,
7322
+ cliEntryPath: options.cliEntryPath ?? resolveCliEntryPath(),
7323
+ cwd: options.cwd ?? process.cwd(),
7324
+ env: options.env ?? process.env,
7325
+ isProcessRunning: options.isProcessRunning ?? defaultIsProcessRunning3
7326
+ });
7327
+ try {
7328
+ const status = await controller.getStatus();
7329
+ switch (status.state) {
7330
+ case "running":
7331
+ return {
7332
+ id: "daemon",
7333
+ label: "Daemon",
7334
+ severity: "pass",
7335
+ summary: "daemon is running",
7336
+ details: [`pid: ${status.pid}`],
7337
+ metadata: {
7338
+ paths,
7339
+ status
7340
+ }
7341
+ };
7342
+ case "stopped":
7343
+ return {
7344
+ id: "daemon",
7345
+ label: "Daemon",
7346
+ severity: "warn",
7347
+ summary: status.stale ? "daemon was stopped and stale runtime files were cleared" : "daemon is not running",
7348
+ details: status.stale ? ["stale runtime files were cleared"] : undefined,
7349
+ suggestions: ["run: weacpx start"],
7350
+ metadata: {
7351
+ paths,
7352
+ status
7353
+ }
7354
+ };
7355
+ case "indeterminate":
7356
+ return {
7357
+ id: "daemon",
7358
+ label: "Daemon",
7359
+ severity: "fail",
7360
+ summary: "daemon status is indeterminate",
7361
+ details: [`pid: ${status.pid}`, `reason: ${status.reason}`],
7362
+ metadata: {
7363
+ paths,
7364
+ status
7365
+ }
7366
+ };
7367
+ }
7368
+ return {
7369
+ id: "daemon",
7370
+ label: "Daemon",
7371
+ severity: "fail",
7372
+ summary: "daemon status lookup returned an unknown state",
7373
+ details: [`state: ${status.state ?? "unknown"}`],
7374
+ metadata: {
7375
+ paths
6767
7376
  }
6768
- return JSON.parse(content);
6769
- } catch (error) {
6770
- if (error.code === "ENOENT") {
6771
- return null;
7377
+ };
7378
+ } catch (error) {
7379
+ return {
7380
+ id: "daemon",
7381
+ label: "Daemon",
7382
+ severity: "fail",
7383
+ summary: "daemon status could not be read",
7384
+ details: [
7385
+ `runtime dir: ${paths.runtimeDir}`,
7386
+ `pid file: ${paths.pidFile}`,
7387
+ `status file: ${paths.statusFile}`,
7388
+ `error: ${formatError4(error)}`
7389
+ ],
7390
+ metadata: {
7391
+ paths
6772
7392
  }
6773
- throw error;
6774
- }
6775
- }
6776
- async save(status) {
6777
- await mkdir(dirname(this.path), { recursive: true });
6778
- await writeFile(this.path, JSON.stringify(status, null, 2));
7393
+ };
6779
7394
  }
6780
- async clear() {
6781
- await rm(this.path, { force: true });
7395
+ }
7396
+ function defaultIsProcessRunning3(pid) {
7397
+ try {
7398
+ process.kill(pid, 0);
7399
+ return true;
7400
+ } catch {
7401
+ return false;
6782
7402
  }
6783
7403
  }
7404
+ function resolveCliEntryPath() {
7405
+ return process.argv[1] ?? fileURLToPath4(import.meta.url);
7406
+ }
7407
+ function formatError4(error) {
7408
+ return error instanceof Error ? error.message : String(error);
7409
+ }
7410
+ var init_daemon_check = __esm(() => {
7411
+ init_create_daemon_controller();
7412
+ init_daemon_files();
7413
+ });
6784
7414
 
6785
- // src/daemon/daemon-controller.ts
6786
- class DaemonController {
6787
- paths;
6788
- deps;
6789
- statusStore;
6790
- startupPollIntervalMs;
6791
- startupTimeoutMs;
6792
- onStartupPoll;
6793
- shutdownPollIntervalMs;
6794
- shutdownTimeoutMs;
6795
- onShutdownPoll;
6796
- constructor(paths, deps) {
6797
- this.paths = paths;
6798
- this.deps = deps;
6799
- this.statusStore = new DaemonStatusStore(paths.statusFile);
6800
- this.startupPollIntervalMs = deps.startupPollIntervalMs ?? 50;
6801
- this.startupTimeoutMs = deps.startupTimeoutMs ?? 5000;
6802
- this.shutdownPollIntervalMs = deps.shutdownPollIntervalMs ?? 50;
6803
- this.shutdownTimeoutMs = deps.shutdownTimeoutMs ?? 5000;
6804
- this.onStartupPoll = deps.onStartupPoll ?? (async () => {
6805
- await new Promise((resolve) => setTimeout(resolve, this.startupPollIntervalMs));
6806
- });
6807
- this.onShutdownPoll = deps.onShutdownPoll ?? (async () => {
6808
- await new Promise((resolve) => setTimeout(resolve, this.shutdownPollIntervalMs));
6809
- });
7415
+ // src/doctor/checks/runtime-check.ts
7416
+ import { constants } from "node:fs";
7417
+ import { access as access3, stat as stat2 } from "node:fs/promises";
7418
+ import { dirname as dirname10 } from "node:path";
7419
+ import { homedir as homedir7 } from "node:os";
7420
+ async function checkRuntime(options = {}) {
7421
+ const home = options.home ?? process.env.HOME ?? homedir7();
7422
+ const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({ home });
7423
+ const probe = options.probe ?? createRuntimeFsProbe();
7424
+ const platform = options.platform ?? process.platform;
7425
+ const checks = [
7426
+ await checkDirectoryCreatable("runtimeDir", paths.runtimeDir, probe, platform),
7427
+ await checkFileCreatable("pidFile", paths.pidFile, probe, platform),
7428
+ await checkFileCreatable("statusFile", paths.statusFile, probe, platform),
7429
+ await checkFileCreatable("stdoutLog", paths.stdoutLog, probe, platform),
7430
+ await checkFileCreatable("stderrLog", paths.stderrLog, probe, platform),
7431
+ await checkFileCreatable("appLog", paths.appLog, probe, platform)
7432
+ ];
7433
+ const failure = checks.find((check) => !check.ok);
7434
+ if (failure) {
7435
+ return {
7436
+ id: "runtime",
7437
+ label: "Runtime",
7438
+ severity: "fail",
7439
+ summary: "daemon runtime paths are not usable",
7440
+ details: checks.map((check) => check.detail)
7441
+ };
6810
7442
  }
6811
- async getStatus() {
6812
- const pid = await this.loadPid();
6813
- const status = await this.statusStore.load();
6814
- if (!pid) {
6815
- return { state: "stopped" };
6816
- }
6817
- if (!this.deps.isProcessRunning(pid)) {
6818
- await this.clearRuntimeFiles();
6819
- return { state: "stopped", stale: true };
7443
+ return {
7444
+ id: "runtime",
7445
+ label: "Runtime",
7446
+ severity: "pass",
7447
+ summary: "daemon runtime paths are usable",
7448
+ details: checks.map((check) => check.detail),
7449
+ metadata: {
7450
+ paths
6820
7451
  }
6821
- if (!status) {
6822
- return { state: "indeterminate", pid, reason: "missing-status" };
7452
+ };
7453
+ }
7454
+ function createRuntimeFsProbe() {
7455
+ return {
7456
+ stat: async (path11) => await stat2(path11),
7457
+ access: async (path11, mode) => await access3(path11, mode)
7458
+ };
7459
+ }
7460
+ async function checkDirectoryCreatable(label, path11, probe, platform) {
7461
+ try {
7462
+ const stats = await probe.stat(path11);
7463
+ if (!stats.isDirectory()) {
7464
+ return {
7465
+ ok: false,
7466
+ detail: `${label}: ${path11} (exists but is not a directory)`
7467
+ };
6823
7468
  }
7469
+ await probe.access(path11, directoryAccessMode(platform));
6824
7470
  return {
6825
- state: "running",
6826
- pid,
6827
- status
7471
+ ok: true,
7472
+ detail: `${label}: ${path11} (writable)`
6828
7473
  };
6829
- }
6830
- async start() {
6831
- const current = await this.getStatus();
6832
- if (current.state === "running") {
6833
- return { state: "already-running", pid: current.pid };
7474
+ } catch (error) {
7475
+ if (!isMissingPathError(error)) {
7476
+ return {
7477
+ ok: false,
7478
+ detail: `${label}: ${path11} (unusable: ${formatError5(error)})`
7479
+ };
6834
7480
  }
6835
- if (current.state === "indeterminate") {
6836
- throw new Error(`weacpx daemon process is already running (pid ${current.pid}) but status metadata is missing`);
7481
+ const parentCheck = await checkCreatableAncestorDirectory(path11, probe, platform);
7482
+ if (!parentCheck.ok) {
7483
+ return {
7484
+ ok: false,
7485
+ detail: `${label}: ${path11} (parent not writable: ${parentCheck.blockingPath})`
7486
+ };
6837
7487
  }
6838
- await this.statusStore.clear();
6839
- const pid = await this.deps.spawnDetached();
6840
- await this.writePid(pid);
6841
- await this.waitForStartupMetadata(pid);
6842
- return { state: "started", pid };
7488
+ return {
7489
+ ok: true,
7490
+ detail: `${label}: ${path11} (creatable via ${parentCheck.creatableFrom})`
7491
+ };
6843
7492
  }
6844
- async stop() {
6845
- const pid = await this.loadPid();
6846
- if (!pid) {
6847
- return { state: "stopped", detail: "not-running" };
7493
+ }
7494
+ async function checkFileCreatable(label, path11, probe, platform) {
7495
+ try {
7496
+ const stats = await probe.stat(path11);
7497
+ if (stats.isDirectory()) {
7498
+ return {
7499
+ ok: false,
7500
+ detail: `${label}: ${path11} (exists but is a directory)`
7501
+ };
6848
7502
  }
6849
- if (this.deps.isProcessRunning(pid)) {
6850
- await this.deps.terminateProcess(pid);
6851
- await this.waitForShutdown(pid);
7503
+ await probe.access(path11, constants.W_OK);
7504
+ return {
7505
+ ok: true,
7506
+ detail: `${label}: ${path11} (writable)`
7507
+ };
7508
+ } catch (error) {
7509
+ if (!isMissingPathError(error)) {
7510
+ return {
7511
+ ok: false,
7512
+ detail: `${label}: ${path11} (unusable: ${formatError5(error)})`
7513
+ };
6852
7514
  }
6853
- await this.clearRuntimeFiles();
6854
- return { state: "stopped", detail: "stopped" };
6855
- }
6856
- async loadPid() {
6857
- try {
6858
- const content = await readFile2(this.paths.pidFile, "utf8");
6859
- const pid = Number(content.trim());
6860
- return Number.isFinite(pid) && pid > 0 ? pid : null;
6861
- } catch (error) {
6862
- if (error.code === "ENOENT") {
6863
- return null;
6864
- }
6865
- throw error;
7515
+ const parentCheck = await checkCreatableAncestorDirectory(dirname10(path11), probe, platform);
7516
+ if (!parentCheck.ok) {
7517
+ return {
7518
+ ok: false,
7519
+ detail: `${label}: ${path11} (parent not writable: ${parentCheck.blockingPath})`
7520
+ };
6866
7521
  }
7522
+ return {
7523
+ ok: true,
7524
+ detail: `${label}: ${path11} (creatable via ${parentCheck.creatableFrom})`
7525
+ };
6867
7526
  }
6868
- async writePid(pid) {
6869
- await mkdir2(dirname2(this.paths.pidFile), { recursive: true });
6870
- await writeFile2(this.paths.pidFile, `${pid}
6871
- `);
6872
- }
6873
- async clearRuntimeFiles() {
6874
- await rm2(this.paths.pidFile, { force: true });
6875
- await this.statusStore.clear();
6876
- }
6877
- async waitForStartupMetadata(pid) {
6878
- const deadline = Date.now() + this.startupTimeoutMs;
6879
- while (Date.now() < deadline) {
6880
- const status = await this.statusStore.load();
6881
- if (status?.pid === pid) {
6882
- return;
6883
- }
6884
- if (!this.deps.isProcessRunning(pid)) {
6885
- await this.clearRuntimeFiles();
6886
- throw new Error(`weacpx daemon exited before reporting ready state (pid ${pid})`);
6887
- }
6888
- await this.onStartupPoll();
7527
+ }
7528
+ async function checkCreatableAncestorDirectory(path11, probe, platform) {
7529
+ try {
7530
+ const stats = await probe.stat(path11);
7531
+ if (!stats.isDirectory()) {
7532
+ return {
7533
+ ok: false,
7534
+ creatableFrom: path11,
7535
+ blockingPath: path11
7536
+ };
6889
7537
  }
6890
- throw new Error(`weacpx daemon did not report ready state within ${this.startupTimeoutMs}ms (pid ${pid})`);
6891
- }
6892
- async waitForShutdown(pid) {
6893
- const deadline = Date.now() + this.shutdownTimeoutMs;
6894
- while (Date.now() < deadline) {
6895
- if (!this.deps.isProcessRunning(pid)) {
6896
- return;
6897
- }
6898
- await this.onShutdownPoll();
7538
+ await probe.access(path11, directoryAccessMode(platform));
7539
+ return {
7540
+ ok: true,
7541
+ creatableFrom: path11
7542
+ };
7543
+ } catch (error) {
7544
+ if (!isMissingPathError(error)) {
7545
+ return {
7546
+ ok: false,
7547
+ creatableFrom: path11,
7548
+ blockingPath: path11
7549
+ };
6899
7550
  }
6900
- if (!this.deps.isProcessRunning(pid)) {
6901
- return;
7551
+ const parent = dirname10(path11);
7552
+ if (parent === path11) {
7553
+ return {
7554
+ ok: false,
7555
+ creatableFrom: path11,
7556
+ blockingPath: path11
7557
+ };
6902
7558
  }
6903
- throw new Error(`weacpx daemon did not exit within ${this.shutdownTimeoutMs}ms (pid ${pid})`);
7559
+ const parentCheck = await checkCreatableAncestorDirectory(parent, probe, platform);
7560
+ if (!parentCheck.ok) {
7561
+ return parentCheck;
7562
+ }
7563
+ return {
7564
+ ok: true,
7565
+ creatableFrom: parentCheck.creatableFrom
7566
+ };
6904
7567
  }
6905
7568
  }
6906
-
6907
- // src/daemon/create-daemon-controller.ts
6908
- function createDaemonController(paths, options) {
6909
- return new DaemonController(paths, {
6910
- isProcessRunning: options.isProcessRunning ?? defaultIsProcessRunning,
6911
- spawnDetached: async () => {
6912
- await mkdir3(paths.runtimeDir, { recursive: true });
6913
- const stdoutHandle = await open(paths.stdoutLog, "a");
6914
- const stderrHandle = await open(paths.stderrLog, "a");
6915
- try {
6916
- return await (options.spawnProcess ?? defaultSpawnProcess)(buildSpawnRequest(paths, options, stdoutHandle.fd, stderrHandle.fd));
6917
- } finally {
6918
- await stdoutHandle.close();
6919
- await stderrHandle.close();
6920
- }
6921
- },
6922
- terminateProcess: options.terminateProcess ?? defaultTerminateProcess
6923
- });
7569
+ function directoryAccessMode(platform) {
7570
+ return platform === "win32" ? constants.W_OK : DIRECTORY_USABLE;
6924
7571
  }
6925
- function defaultIsProcessRunning(pid) {
7572
+ function isMissingPathError(error) {
7573
+ return isErrnoError(error) && (error.code === "ENOENT" || error.code === "ENOTDIR");
7574
+ }
7575
+ function isErrnoError(error) {
7576
+ return typeof error === "object" && error !== null && "code" in error;
7577
+ }
7578
+ function formatError5(error) {
7579
+ if (error instanceof Error) {
7580
+ return error.message;
7581
+ }
7582
+ return String(error);
7583
+ }
7584
+ var DIRECTORY_USABLE;
7585
+ var init_runtime_check = __esm(() => {
7586
+ init_daemon_files();
7587
+ DIRECTORY_USABLE = constants.W_OK | constants.X_OK;
7588
+ });
7589
+
7590
+ // src/doctor/checks/smoke-check.ts
7591
+ async function checkSmoke(options = {}, deps = {}) {
7592
+ const resolvedOptions = { ...options, ...deps };
7593
+ const runtimePaths = (resolvedOptions.resolveRuntimePaths ?? resolveRuntimePaths)();
6926
7594
  try {
6927
- process.kill(pid, 0);
6928
- return true;
6929
- } catch {
6930
- return false;
7595
+ const config = await (resolvedOptions.loadConfig ?? loadConfig)(runtimePaths.configPath);
7596
+ const agentSelection = selectAgent(config, resolvedOptions.agent);
7597
+ if (agentSelection.error) {
7598
+ return agentSelection.error;
7599
+ }
7600
+ const workspaceSelection = selectWorkspace(config, resolvedOptions.workspace);
7601
+ if (workspaceSelection.error) {
7602
+ return workspaceSelection.error;
7603
+ }
7604
+ const missingDefaults = [agentSelection.missingDefault, workspaceSelection.missingDefault].filter((value) => typeof value === "string");
7605
+ if (missingDefaults.length > 0) {
7606
+ return {
7607
+ id: "smoke",
7608
+ label: "Smoke",
7609
+ severity: "skip",
7610
+ summary: `smoke prerequisites missing: ${missingDefaults.join(", ")}`,
7611
+ details: [
7612
+ `config path: ${runtimePaths.configPath}`,
7613
+ ...selectionDetails(agentSelection, workspaceSelection)
7614
+ ],
7615
+ suggestions: ["configure at least one agent and one workspace before running --smoke"]
7616
+ };
7617
+ }
7618
+ const agent = agentSelection.value;
7619
+ const workspace = workspaceSelection.value;
7620
+ if (!agent || !workspace) {
7621
+ return {
7622
+ id: "smoke",
7623
+ label: "Smoke",
7624
+ severity: "skip",
7625
+ summary: "smoke prerequisites missing: agent, workspace",
7626
+ details: [
7627
+ `config path: ${runtimePaths.configPath}`,
7628
+ ...selectionDetails(agentSelection, workspaceSelection)
7629
+ ]
7630
+ };
7631
+ }
7632
+ const metadata = (resolvedOptions.resolveAcpxCommandMetadata ?? resolveAcpxCommandMetadata)({
7633
+ configuredCommand: config.transport.command
7634
+ });
7635
+ const transport = await (resolvedOptions.createTransport ?? defaultCreateTransport)({
7636
+ config,
7637
+ metadata,
7638
+ resolveBridgeEntryPath: resolvedOptions.resolveBridgeEntryPath,
7639
+ spawnAcpxBridgeClient: resolvedOptions.spawnAcpxBridgeClient
7640
+ });
7641
+ const session = buildSession({
7642
+ config,
7643
+ agent,
7644
+ workspace,
7645
+ now: resolvedOptions.now
7646
+ });
7647
+ try {
7648
+ await transport.ensureSession(session);
7649
+ const reply = await transport.prompt(session, SMOKE_PROMPT);
7650
+ const replyText = reply.text.trim();
7651
+ if (replyText.length === 0) {
7652
+ return {
7653
+ id: "smoke",
7654
+ label: "Smoke",
7655
+ severity: "fail",
7656
+ summary: "smoke prompt returned empty text",
7657
+ details: buildDetails3({
7658
+ runtimePaths,
7659
+ metadata,
7660
+ session,
7661
+ agentReason: agentSelection.reason,
7662
+ workspaceReason: workspaceSelection.reason,
7663
+ replyText,
7664
+ verbose: resolvedOptions.verbose
7665
+ })
7666
+ };
7667
+ }
7668
+ return {
7669
+ id: "smoke",
7670
+ label: "Smoke",
7671
+ severity: replyText === "ok" ? "pass" : "warn",
7672
+ summary: replyText === "ok" ? "smoke prompt succeeded and reply received" : "smoke prompt succeeded with non-ideal reply",
7673
+ details: buildDetails3({
7674
+ runtimePaths,
7675
+ metadata,
7676
+ session,
7677
+ agentReason: agentSelection.reason,
7678
+ workspaceReason: workspaceSelection.reason,
7679
+ replyText,
7680
+ verbose: resolvedOptions.verbose
7681
+ }),
7682
+ metadata: {
7683
+ agent: session.agent,
7684
+ workspace: session.workspace,
7685
+ transportSession: session.transportSession,
7686
+ replyText
7687
+ }
7688
+ };
7689
+ } finally {
7690
+ await transport.dispose?.();
7691
+ }
7692
+ } catch (error) {
7693
+ return {
7694
+ id: "smoke",
7695
+ label: "Smoke",
7696
+ severity: "fail",
7697
+ summary: "smoke transport probe failed",
7698
+ details: [`config path: ${runtimePaths.configPath}`, `error: ${formatError6(error)}`]
7699
+ };
6931
7700
  }
6932
7701
  }
6933
- function buildSpawnRequest(paths, options, stdoutFd, stderrFd) {
6934
- const platform = options.platform ?? process.platform;
6935
- if (platform === "win32") {
7702
+ function selectAgent(config, explicitAgent) {
7703
+ if (explicitAgent) {
7704
+ if (!(explicitAgent in config.agents)) {
7705
+ return {
7706
+ reason: "explicit --agent",
7707
+ error: createSelectionFailure(`smoke agent not found: ${explicitAgent}`)
7708
+ };
7709
+ }
6936
7710
  return {
6937
- mode: "windows-hidden",
6938
- command: "powershell.exe",
6939
- args: [
6940
- "-NoProfile",
6941
- "-NonInteractive",
6942
- "-EncodedCommand",
6943
- buildWindowsLauncherScript()
6944
- ],
6945
- options: {
6946
- cwd: options.cwd,
6947
- env: {
6948
- ...options.env,
6949
- WEACPX_DAEMON_COMMAND: options.processExecPath,
6950
- WEACPX_DAEMON_ARG0: options.cliEntryPath,
6951
- WEACPX_DAEMON_ARG1: "run",
6952
- WEACPX_DAEMON_CWD: options.cwd,
6953
- WEACPX_DAEMON_STDOUT: paths.stdoutLog,
6954
- WEACPX_DAEMON_STDERR: paths.stderrLog
6955
- },
6956
- stdio: ["ignore", "pipe", "ignore"],
6957
- windowsHide: true
6958
- }
7711
+ value: explicitAgent,
7712
+ reason: "explicit --agent"
7713
+ };
7714
+ }
7715
+ const firstAgent = Object.keys(config.agents)[0];
7716
+ if (!firstAgent) {
7717
+ return {
7718
+ missingDefault: "agent",
7719
+ reason: "no configured agent available"
6959
7720
  };
6960
7721
  }
6961
7722
  return {
6962
- mode: "direct",
6963
- command: options.processExecPath,
6964
- args: [options.cliEntryPath, "run"],
6965
- options: {
6966
- cwd: options.cwd,
6967
- detached: true,
6968
- env: options.env,
6969
- stdio: ["ignore", stdoutFd, stderrFd]
7723
+ value: firstAgent,
7724
+ reason: "default first configured agent"
7725
+ };
7726
+ }
7727
+ function selectWorkspace(config, explicitWorkspace) {
7728
+ if (explicitWorkspace) {
7729
+ if (!(explicitWorkspace in config.workspaces)) {
7730
+ return {
7731
+ reason: "explicit --workspace",
7732
+ error: createSelectionFailure(`smoke workspace not found: ${explicitWorkspace}`)
7733
+ };
6970
7734
  }
7735
+ return {
7736
+ value: explicitWorkspace,
7737
+ reason: "explicit --workspace"
7738
+ };
7739
+ }
7740
+ const firstWorkspace = Object.keys(config.workspaces)[0];
7741
+ if (!firstWorkspace) {
7742
+ return {
7743
+ missingDefault: "workspace",
7744
+ reason: "no configured workspace available"
7745
+ };
7746
+ }
7747
+ return {
7748
+ value: firstWorkspace,
7749
+ reason: "default first configured workspace"
6971
7750
  };
6972
7751
  }
6973
- function buildWindowsLauncherScript() {
6974
- const script = [
6975
- "$process = Start-Process -FilePath $env:WEACPX_DAEMON_COMMAND `",
6976
- " -ArgumentList @($env:WEACPX_DAEMON_ARG0, $env:WEACPX_DAEMON_ARG1) `",
6977
- " -WorkingDirectory $env:WEACPX_DAEMON_CWD `",
6978
- " -RedirectStandardOutput $env:WEACPX_DAEMON_STDOUT `",
6979
- " -RedirectStandardError $env:WEACPX_DAEMON_STDERR `",
6980
- " -WindowStyle Hidden `",
6981
- " -PassThru",
6982
- "[Console]::Out.WriteLine($process.Id)"
6983
- ].join(`
6984
- `);
6985
- return Buffer.from(script, "utf16le").toString("base64");
7752
+ function createSelectionFailure(summary) {
7753
+ return {
7754
+ id: "smoke",
7755
+ label: "Smoke",
7756
+ severity: "fail",
7757
+ summary
7758
+ };
6986
7759
  }
6987
- async function defaultSpawnProcess(request) {
6988
- if (request.mode === "windows-hidden") {
6989
- return await spawnWindowsHiddenProcess(request);
7760
+ function selectionDetails(agentSelection, workspaceSelection) {
7761
+ const details = [];
7762
+ if (agentSelection.value) {
7763
+ details.push(`agent: ${agentSelection.value} (${agentSelection.reason})`);
7764
+ } else {
7765
+ details.push(`agent: unavailable (${agentSelection.reason})`);
6990
7766
  }
6991
- const child = spawn(request.command, request.args, request.options);
6992
- child.unref();
6993
- return child.pid ?? 0;
7767
+ if (workspaceSelection.value) {
7768
+ details.push(`workspace: ${workspaceSelection.value} (${workspaceSelection.reason})`);
7769
+ } else {
7770
+ details.push(`workspace: unavailable (${workspaceSelection.reason})`);
7771
+ }
7772
+ return details;
6994
7773
  }
6995
- async function spawnWindowsHiddenProcess(request) {
6996
- return await new Promise((resolve, reject) => {
6997
- const child = spawn(request.command, request.args, request.options);
6998
- let stdout = "";
6999
- let settled = false;
7000
- child.stdout?.setEncoding("utf8");
7001
- child.stdout?.on("data", (chunk) => {
7002
- stdout += chunk;
7003
- if (settled) {
7004
- return;
7005
- }
7006
- const pid = Number.parseInt(stdout.trim(), 10);
7007
- if (!Number.isFinite(pid) || pid <= 0) {
7008
- return;
7009
- }
7010
- settled = true;
7011
- child.stdout?.destroy();
7012
- child.unref();
7013
- resolve(pid);
7014
- });
7015
- child.on("error", (error) => {
7016
- if (settled) {
7017
- return;
7018
- }
7019
- settled = true;
7020
- reject(error);
7021
- });
7022
- child.on("close", (code) => {
7023
- if (settled) {
7024
- return;
7025
- }
7026
- if (code !== 0) {
7027
- settled = true;
7028
- reject(new Error(`Failed to launch hidden Windows daemon process (exit ${code ?? 1})`));
7029
- return;
7030
- }
7031
- const pid = Number.parseInt(stdout.trim(), 10);
7032
- if (!Number.isFinite(pid) || pid <= 0) {
7033
- settled = true;
7034
- reject(new Error("Failed to read daemon pid from hidden Windows launcher"));
7035
- return;
7036
- }
7037
- settled = true;
7038
- resolve(pid);
7774
+ function buildSession(options) {
7775
+ const timestamp = (options.now ?? (() => new Date))().getTime();
7776
+ const agentConfig = options.config.agents[options.agent];
7777
+ const workspaceConfig = options.config.workspaces[options.workspace];
7778
+ if (!agentConfig) {
7779
+ throw new Error(`smoke agent not found: ${options.agent}`);
7780
+ }
7781
+ if (!workspaceConfig) {
7782
+ throw new Error(`smoke workspace not found: ${options.workspace}`);
7783
+ }
7784
+ return {
7785
+ alias: "weacpx-doctor",
7786
+ agent: options.agent,
7787
+ ...agentConfig.command ? { agentCommand: agentConfig.command } : {},
7788
+ workspace: options.workspace,
7789
+ transportSession: `weacpx-doctor-${timestamp}`,
7790
+ replyMode: options.config.wechat.replyMode,
7791
+ cwd: workspaceConfig.cwd
7792
+ };
7793
+ }
7794
+ async function defaultCreateTransport(options) {
7795
+ if (options.config.transport.type === "acpx-bridge") {
7796
+ const client = await (options.spawnAcpxBridgeClient ?? spawnAcpxBridgeClient)({
7797
+ acpxCommand: options.metadata.command,
7798
+ bridgeEntryPath: (options.resolveBridgeEntryPath ?? resolveBridgeEntryPath)(),
7799
+ permissionMode: options.config.transport.permissionMode,
7800
+ nonInteractivePermissions: options.config.transport.nonInteractivePermissions
7039
7801
  });
7802
+ return new AcpxBridgeTransport(client);
7803
+ }
7804
+ return new AcpxCliTransport({
7805
+ ...options.config.transport,
7806
+ command: options.metadata.command
7040
7807
  });
7041
7808
  }
7042
- async function defaultTerminateProcess(pid) {
7043
- await terminateProcessTree(pid);
7809
+ function buildDetails3(options) {
7810
+ const details = [
7811
+ `config path: ${options.runtimePaths.configPath}`,
7812
+ `agent: ${options.session.agent} (${options.agentReason})`,
7813
+ `workspace: ${options.session.workspace} (${options.workspaceReason})`,
7814
+ `transport session: ${options.session.transportSession}`,
7815
+ `reply: ${JSON.stringify(options.replyText)}`
7816
+ ];
7817
+ if (options.verbose) {
7818
+ details.push(`transport type: ${options.session.agentCommand ? "agent-command" : "driver-default"}`);
7819
+ details.push(`acpx command: ${options.metadata.command}`);
7820
+ details.push(`acpx source: ${options.metadata.source}`);
7821
+ details.push(`smoke prompt: ${JSON.stringify(SMOKE_PROMPT)}`);
7822
+ }
7823
+ return details;
7044
7824
  }
7045
- async function terminateProcessTree(pid, platform = process.platform, runCommand = defaultRunProcessCommand, killProcess = (targetPid, signal) => {
7046
- process.kill(targetPid, signal);
7047
- }, isProcessRunning = defaultIsProcessRunning) {
7048
- if (platform === "win32") {
7825
+ function formatError6(error) {
7826
+ return error instanceof Error ? error.message : String(error);
7827
+ }
7828
+ var SMOKE_PROMPT = "Reply with exactly: ok";
7829
+ var init_smoke_check = __esm(async () => {
7830
+ init_load_config();
7831
+ init_resolve_acpx_command();
7832
+ init_acpx_bridge_client();
7833
+ init_acpx_cli_transport();
7834
+ await init_main();
7835
+ });
7836
+
7837
+ // src/doctor/checks/wechat-check.ts
7838
+ async function checkWechat(options = {}) {
7839
+ const ids = listWeixinAccountIds();
7840
+ const accounts = ids.map((accountId) => {
7049
7841
  try {
7050
- await runCommand("taskkill", ["/PID", String(pid), "/T", "/F"]);
7051
- } catch {}
7052
- return;
7842
+ return {
7843
+ accountId,
7844
+ account: resolveWeixinAccount(accountId)
7845
+ };
7846
+ } catch (error) {
7847
+ return {
7848
+ accountId,
7849
+ error: formatError7(error)
7850
+ };
7851
+ }
7852
+ });
7853
+ const configuredAccount = accounts.find((entry) => ("account" in entry) && entry.account.configured);
7854
+ const loggedIn = Boolean(configuredAccount);
7855
+ if (!loggedIn) {
7856
+ return {
7857
+ id: "wechat",
7858
+ label: "WeChat",
7859
+ severity: "warn",
7860
+ summary: "wechat is not logged in",
7861
+ details: buildVerboseDetails(false, options.verbose, accounts),
7862
+ suggestions: ["weacpx login"]
7863
+ };
7053
7864
  }
7054
- const targetPid = pid > 0 ? -pid : pid;
7055
- try {
7056
- killProcess(targetPid, "SIGTERM");
7057
- } catch {
7865
+ return {
7866
+ id: "wechat",
7867
+ label: "WeChat",
7868
+ severity: "pass",
7869
+ summary: "wechat is logged in",
7870
+ details: buildVerboseDetails(true, options.verbose, accounts)
7871
+ };
7872
+ }
7873
+ function buildVerboseDetails(loggedIn, verbose, accounts) {
7874
+ if (!verbose) {
7058
7875
  return;
7059
7876
  }
7060
- const deadline = Date.now() + 5000;
7061
- while (Date.now() < deadline) {
7062
- if (!isProcessRunning(targetPid)) {
7063
- return;
7877
+ const details = [];
7878
+ details.push(`loggedIn: ${loggedIn}`);
7879
+ details.push(`accountIds: ${accounts.length > 0 ? accounts.map((entry) => entry.accountId).join(", ") : "(none)"}`);
7880
+ for (const entry of accounts) {
7881
+ if ("account" in entry) {
7882
+ details.push(`account[${entry.account.accountId}].configured: ${entry.account.configured}`);
7883
+ details.push(`account[${entry.account.accountId}].baseUrl: ${entry.account.baseUrl}`);
7884
+ continue;
7064
7885
  }
7065
- await new Promise((resolve) => setTimeout(resolve, 100));
7886
+ details.push(`account[${entry.accountId}].resolveError: ${entry.error ?? "unknown"}`);
7066
7887
  }
7067
- try {
7068
- killProcess(targetPid, "SIGKILL");
7069
- } catch {}
7888
+ return details;
7070
7889
  }
7071
- async function defaultRunProcessCommand(command, args) {
7072
- return await new Promise((resolve, reject) => {
7073
- const child = spawn(command, args, { stdio: "ignore" });
7074
- child.on("error", reject);
7075
- child.on("close", (code) => resolve(code ?? 1));
7076
- });
7890
+ function formatError7(error) {
7891
+ return error instanceof Error ? error.message : String(error);
7077
7892
  }
7893
+ var init_wechat_check = __esm(() => {
7894
+ init_weixin();
7895
+ });
7078
7896
 
7079
- // src/daemon/daemon-files.ts
7080
- import { join } from "node:path";
7081
- function resolveDaemonPaths(options) {
7082
- const runtimeDir = options.runtimeDir ?? join(options.home, ".weacpx", "runtime");
7897
+ // src/doctor/render-doctor.ts
7898
+ function renderDoctor(report, options = {}) {
7899
+ return options.verbose ? renderVerboseDoctor(report) : renderDefaultDoctor(report);
7900
+ }
7901
+ function renderDefaultDoctor(report) {
7902
+ const lines = [];
7903
+ for (const check of report.checks) {
7904
+ lines.push(renderCheckLine(check));
7905
+ }
7906
+ lines.push(renderSummaryLine(report.checks));
7907
+ const suggestions = collectSuggestions(report.checks);
7908
+ if (suggestions.length > 0) {
7909
+ lines.push("Next steps:");
7910
+ for (const suggestion of suggestions) {
7911
+ lines.push(`- ${suggestion}`);
7912
+ }
7913
+ }
7914
+ return lines;
7915
+ }
7916
+ function renderVerboseDoctor(report) {
7917
+ const lines = [];
7918
+ for (const check of report.checks) {
7919
+ lines.push(renderCheckLine(check));
7920
+ for (const detail of check.details ?? []) {
7921
+ lines.push(` detail: ${detail}`);
7922
+ }
7923
+ }
7924
+ lines.push(renderSummaryLine(report.checks));
7925
+ const suggestions = collectSuggestions(report.checks);
7926
+ if (suggestions.length > 0) {
7927
+ lines.push("Next steps:");
7928
+ for (const suggestion of suggestions) {
7929
+ lines.push(`- ${suggestion}`);
7930
+ }
7931
+ }
7932
+ return lines;
7933
+ }
7934
+ function renderCheckLine(check) {
7935
+ return `${SEVERITY_LABELS[check.severity]} ${check.label}: ${check.summary}`;
7936
+ }
7937
+ function renderSummaryLine(checks) {
7938
+ const counts = summarizeChecks(checks);
7939
+ return `Summary: PASS ${counts.pass}, WARN ${counts.warn}, FAIL ${counts.fail}, SKIP ${counts.skip}`;
7940
+ }
7941
+ function summarizeChecks(checks) {
7942
+ return checks.reduce((counts, check) => {
7943
+ counts[check.severity] += 1;
7944
+ return counts;
7945
+ }, { pass: 0, warn: 0, fail: 0, skip: 0 });
7946
+ }
7947
+ function collectSuggestions(checks) {
7948
+ const seen = new Set;
7949
+ const suggestions = [];
7950
+ for (const check of checks) {
7951
+ for (const suggestion of check.suggestions ?? []) {
7952
+ if (seen.has(suggestion)) {
7953
+ continue;
7954
+ }
7955
+ seen.add(suggestion);
7956
+ suggestions.push(suggestion);
7957
+ }
7958
+ }
7959
+ return suggestions;
7960
+ }
7961
+ var SEVERITY_LABELS;
7962
+ var init_render_doctor = __esm(() => {
7963
+ SEVERITY_LABELS = {
7964
+ pass: "PASS",
7965
+ warn: "WARN",
7966
+ fail: "FAIL",
7967
+ skip: "SKIP"
7968
+ };
7969
+ });
7970
+
7971
+ // src/doctor/doctor.ts
7972
+ import { homedir as homedir8 } from "node:os";
7973
+ import { join as join6 } from "node:path";
7974
+ async function runDoctor(options = {}, deps = {}) {
7975
+ const home = deps.home ?? process.env.HOME ?? homedir8();
7976
+ const runtimePaths = resolveDoctorRuntimePaths(home, deps.resolveRuntimePaths);
7977
+ const sharedLoadConfig = createSharedLoadConfig(runtimePaths, deps.loadConfig ?? loadConfig);
7978
+ const checks = [];
7979
+ checks.push(await (deps.checkConfig ?? checkConfig)({
7980
+ loadConfig: sharedLoadConfig,
7981
+ resolveRuntimePaths: () => runtimePaths
7982
+ }));
7983
+ checks.push(await (deps.checkRuntime ?? checkRuntime)({
7984
+ home
7985
+ }));
7986
+ checks.push(await (deps.checkDaemon ?? checkDaemon)({
7987
+ home
7988
+ }));
7989
+ checks.push(await (deps.checkWechat ?? checkWechat)({
7990
+ verbose: options.verbose
7991
+ }));
7992
+ checks.push(await (deps.checkAcpx ?? checkAcpx)({
7993
+ verbose: options.verbose,
7994
+ loadConfig: sharedLoadConfig,
7995
+ resolveRuntimePaths: () => runtimePaths
7996
+ }));
7997
+ checks.push(await (deps.checkBridge ?? checkBridge)({
7998
+ verbose: options.verbose,
7999
+ loadConfig: sharedLoadConfig,
8000
+ resolveRuntimePaths: () => runtimePaths
8001
+ }));
8002
+ checks.push(options.smoke === true ? await (deps.checkSmoke ?? ((runOptions) => defaultCheckSmoke(runOptions, {
8003
+ resolveRuntimePaths: () => runtimePaths,
8004
+ loadConfig: sharedLoadConfig
8005
+ })))(options) : createSmokeSkipResult("smoke probe not requested"));
8006
+ const report = { checks };
8007
+ const output = (deps.renderDoctor ?? renderDoctor)(report, options);
7083
8008
  return {
7084
- runtimeDir,
7085
- pidFile: join(runtimeDir, "daemon.pid"),
7086
- statusFile: join(runtimeDir, "status.json"),
7087
- stdoutLog: join(runtimeDir, "stdout.log"),
7088
- stderrLog: join(runtimeDir, "stderr.log"),
7089
- appLog: join(runtimeDir, "app.log")
8009
+ report,
8010
+ output,
8011
+ exitCode: checks.some((check) => check.severity === "fail") ? 1 : 0
8012
+ };
8013
+ }
8014
+ function resolveDoctorRuntimePaths(home, resolver) {
8015
+ if (resolver) {
8016
+ return resolver();
8017
+ }
8018
+ if (depsUseExplicitRuntimeOverrides()) {
8019
+ return resolveRuntimePaths();
8020
+ }
8021
+ return {
8022
+ configPath: join6(home, ".weacpx", "config.json"),
8023
+ statePath: join6(home, ".weacpx", "state.json")
8024
+ };
8025
+ }
8026
+ function depsUseExplicitRuntimeOverrides() {
8027
+ return Boolean(process.env.WEACPX_CONFIG || process.env.WEACPX_STATE);
8028
+ }
8029
+ function createSharedLoadConfig(runtimePaths, loader) {
8030
+ let pending;
8031
+ return async (configPath) => {
8032
+ if (configPath !== runtimePaths.configPath) {
8033
+ return await loader(configPath);
8034
+ }
8035
+ pending ??= loader(configPath);
8036
+ return await pending;
8037
+ };
8038
+ }
8039
+ async function defaultCheckSmoke(options, deps) {
8040
+ return await checkSmoke(options, {
8041
+ resolveRuntimePaths: deps.resolveRuntimePaths,
8042
+ loadConfig: deps.loadConfig
8043
+ });
8044
+ }
8045
+ function createSmokeSkipResult(summary) {
8046
+ return {
8047
+ id: "smoke",
8048
+ label: "Smoke",
8049
+ severity: "skip",
8050
+ summary
7090
8051
  };
7091
8052
  }
8053
+ var init_doctor = __esm(async () => {
8054
+ init_load_config();
8055
+ init_daemon_check();
8056
+ init_runtime_check();
8057
+ init_wechat_check();
8058
+ init_render_doctor();
8059
+ await __promiseAll([
8060
+ init_main(),
8061
+ init_acpx_check(),
8062
+ init_bridge_check(),
8063
+ init_config_check(),
8064
+ init_smoke_check()
8065
+ ]);
8066
+ });
8067
+
8068
+ // src/doctor/index.ts
8069
+ var exports_doctor = {};
8070
+ __export(exports_doctor, {
8071
+ main: () => main3
8072
+ });
8073
+ async function main3(options, deps = {}) {
8074
+ const result = await (deps.runDoctor ?? runDoctor)(options);
8075
+ const print = deps.print ?? ((line) => console.log(line));
8076
+ for (const line of result.output) {
8077
+ print(line);
8078
+ }
8079
+ return result.exitCode;
8080
+ }
8081
+ var init_doctor2 = __esm(async () => {
8082
+ await init_doctor();
8083
+ });
8084
+
8085
+ // src/cli.ts
8086
+ init_create_daemon_controller();
8087
+ init_daemon_files();
8088
+ import { homedir as homedir9 } from "node:os";
8089
+ import { sep } from "node:path";
8090
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
7092
8091
 
7093
8092
  // src/daemon/daemon-runtime.ts
8093
+ init_daemon_status();
7094
8094
  import { mkdir as mkdir4, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
7095
8095
  import { dirname as dirname3 } from "node:path";
8096
+
7096
8097
  class DaemonRuntime {
7097
8098
  paths;
7098
8099
  options;
@@ -7140,6 +8141,7 @@ class DaemonRuntime {
7140
8141
  }
7141
8142
 
7142
8143
  // src/cli.ts
8144
+ init_version();
7143
8145
  init_consumer_lock();
7144
8146
  var HELP_LINES = [
7145
8147
  "用法:",
@@ -7148,13 +8150,26 @@ var HELP_LINES = [
7148
8150
  "weacpx run - 前台运行",
7149
8151
  "weacpx start - 后台启动",
7150
8152
  "weacpx status - 查看状态",
7151
- "weacpx stop - 停止服务"
8153
+ "weacpx stop - 停止服务",
8154
+ "weacpx doctor - 运行诊断",
8155
+ "weacpx version - 查看版本"
7152
8156
  ];
7153
8157
  async function runCli(args, deps = {}) {
7154
8158
  const command = args[0];
7155
8159
  const print = deps.print ?? ((line) => console.log(line));
7156
- const controller = deps.controller ?? createDefaultController();
7157
8160
  switch (command) {
8161
+ case "version":
8162
+ case "--version":
8163
+ case "-v":
8164
+ print(readVersion());
8165
+ return 0;
8166
+ case "--help":
8167
+ case "-h": {
8168
+ for (const line of HELP_LINES) {
8169
+ print(line);
8170
+ }
8171
+ return 0;
8172
+ }
7158
8173
  case "login":
7159
8174
  await (deps.login ?? defaultLogin)();
7160
8175
  return 0;
@@ -7164,7 +8179,18 @@ async function runCli(args, deps = {}) {
7164
8179
  case "run":
7165
8180
  await (deps.run ?? defaultRun)();
7166
8181
  return 0;
8182
+ case "doctor": {
8183
+ const parsed = parseDoctorArgs(args.slice(1));
8184
+ if (!parsed.ok) {
8185
+ for (const line of HELP_LINES) {
8186
+ print(line);
8187
+ }
8188
+ return 1;
8189
+ }
8190
+ return await (deps.doctor ?? defaultDoctor)(parsed.options);
8191
+ }
7167
8192
  case "start": {
8193
+ const controller = deps.controller ?? createDefaultController();
7168
8194
  const result = await controller.start();
7169
8195
  if (result.state === "already-running") {
7170
8196
  print("weacpx 已在后台运行");
@@ -7176,6 +8202,7 @@ async function runCli(args, deps = {}) {
7176
8202
  return 0;
7177
8203
  }
7178
8204
  case "status": {
8205
+ const controller = deps.controller ?? createDefaultController();
7179
8206
  const status = await controller.getStatus();
7180
8207
  if (status.state === "indeterminate") {
7181
8208
  print("weacpx 进程仍在运行,但状态元数据缺失");
@@ -7198,6 +8225,7 @@ async function runCli(args, deps = {}) {
7198
8225
  return 0;
7199
8226
  }
7200
8227
  case "stop": {
8228
+ const controller = deps.controller ?? createDefaultController();
7201
8229
  const result = await controller.stop();
7202
8230
  if (result.detail === "not-running") {
7203
8231
  print("weacpx 未运行");
@@ -7214,8 +8242,8 @@ async function runCli(args, deps = {}) {
7214
8242
  }
7215
8243
  }
7216
8244
  async function defaultLogin() {
7217
- const { main: main3 } = await init_login().then(() => exports_login);
7218
- await main3();
8245
+ const { main: main4 } = await init_login().then(() => exports_login);
8246
+ await main4();
7219
8247
  }
7220
8248
  async function defaultLogout() {
7221
8249
  const { logout: logout2 } = await Promise.resolve().then(() => (init_weixin_sdk(), exports_weixin_sdk));
@@ -7232,7 +8260,7 @@ async function defaultRun() {
7232
8260
  const daemonRuntime = new DaemonRuntime(daemonPaths, { pid: process.pid });
7233
8261
  await runConsole2(runtimePaths, {
7234
8262
  buildApp: (paths) => buildApp2(paths, {
7235
- defaultLoggingLevel: resolveCliEntryPath().includes(`${sep}src${sep}`) ? "debug" : "info"
8263
+ defaultLoggingLevel: resolveCliEntryPath2().includes(`${sep}src${sep}`) ? "debug" : "info"
7236
8264
  }),
7237
8265
  loadWeixinSdk: loadWeixinSdk2,
7238
8266
  daemonRuntime,
@@ -7244,27 +8272,66 @@ async function defaultRun() {
7244
8272
  })
7245
8273
  });
7246
8274
  }
8275
+ async function defaultDoctor(options) {
8276
+ const { main: main4 } = await init_doctor2().then(() => exports_doctor);
8277
+ return await main4(options);
8278
+ }
7247
8279
  function createDefaultController() {
7248
8280
  const daemonPaths = resolveDaemonPaths({ home: requireHome() });
7249
8281
  return createDaemonController(daemonPaths, {
7250
8282
  processExecPath: process.execPath,
7251
- cliEntryPath: resolveCliEntryPath(),
8283
+ cliEntryPath: resolveCliEntryPath2(),
7252
8284
  cwd: process.cwd(),
7253
8285
  env: process.env
7254
8286
  });
7255
8287
  }
7256
8288
  function requireHome() {
7257
- const home = process.env.HOME ?? homedir6();
8289
+ const home = process.env.HOME ?? homedir9();
7258
8290
  if (!home) {
7259
8291
  throw new Error("Unable to resolve the current user home directory");
7260
8292
  }
7261
8293
  return home;
7262
8294
  }
7263
- function resolveCliEntryPath() {
8295
+ function resolveCliEntryPath2() {
7264
8296
  if (process.argv[1]) {
7265
8297
  return process.argv[1];
7266
8298
  }
7267
- return fileURLToPath4(import.meta.url);
8299
+ return fileURLToPath5(import.meta.url);
8300
+ }
8301
+ function parseDoctorArgs(args) {
8302
+ const options = {};
8303
+ for (let index = 0;index < args.length; index++) {
8304
+ const arg = args[index];
8305
+ switch (arg) {
8306
+ case "--verbose":
8307
+ options.verbose = true;
8308
+ break;
8309
+ case "--smoke":
8310
+ options.smoke = true;
8311
+ break;
8312
+ case "--agent": {
8313
+ const value = args[index + 1];
8314
+ if (!value || value.startsWith("--")) {
8315
+ return { ok: false };
8316
+ }
8317
+ options.agent = value;
8318
+ index++;
8319
+ break;
8320
+ }
8321
+ case "--workspace": {
8322
+ const value = args[index + 1];
8323
+ if (!value || value.startsWith("--")) {
8324
+ return { ok: false };
8325
+ }
8326
+ options.workspace = value;
8327
+ index++;
8328
+ break;
8329
+ }
8330
+ default:
8331
+ return { ok: false };
8332
+ }
8333
+ }
8334
+ return { ok: true, options };
7268
8335
  }
7269
8336
  if (__require.main == __require.module) {
7270
8337
  process.exitCode = await runCli(process.argv.slice(2));