weacpx 0.2.0 → 0.2.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/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
@@ -2065,6 +2423,47 @@ var init_session_guard = __esm(() => {
2065
2423
  pauseUntilMap = new Map;
2066
2424
  });
2067
2425
 
2426
+ // src/weixin/messaging/conversation-executor.ts
2427
+ function createConversationExecutor() {
2428
+ const states = new Map;
2429
+ const getState = (conversationId) => {
2430
+ const existing = states.get(conversationId);
2431
+ if (existing)
2432
+ return existing;
2433
+ const created = { activeControls: 0 };
2434
+ states.set(conversationId, created);
2435
+ return created;
2436
+ };
2437
+ const cleanupState = (conversationId, state) => {
2438
+ if (!state.normalTail && state.activeControls === 0) {
2439
+ states.delete(conversationId);
2440
+ }
2441
+ };
2442
+ return {
2443
+ run(conversationId, lane, task) {
2444
+ const state = getState(conversationId);
2445
+ if (lane === "control") {
2446
+ state.activeControls += 1;
2447
+ return Promise.resolve().then(task).finally(() => {
2448
+ state.activeControls -= 1;
2449
+ cleanupState(conversationId, state);
2450
+ });
2451
+ }
2452
+ const previous = state.normalTail ?? Promise.resolve();
2453
+ const next = previous.catch(() => {
2454
+ return;
2455
+ }).then(task);
2456
+ state.normalTail = next;
2457
+ return next.finally(() => {
2458
+ if (state.normalTail === next) {
2459
+ state.normalTail = undefined;
2460
+ }
2461
+ cleanupState(conversationId, state);
2462
+ });
2463
+ }
2464
+ };
2465
+ }
2466
+
2068
2467
  // src/weixin/api/types.ts
2069
2468
  var UploadMediaType, MessageType, MessageItemType, MessageState, TypingStatus;
2070
2469
  var init_types = __esm(() => {
@@ -2637,7 +3036,7 @@ function markdownToPlainText(text) {
2637
3036
  result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
2638
3037
  result = result.replace(/^\|[\s:|-]+\|$/gm, "");
2639
3038
  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");
3039
+ result = result.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/~~(.+?)~~/g, "$1").replace(/`(.+?)`/g, "$1");
2641
3040
  return result;
2642
3041
  }
2643
3042
  function buildTextMessageReq(params) {
@@ -3002,6 +3401,10 @@ function extractTextBody(itemList) {
3002
3401
  }
3003
3402
  return "";
3004
3403
  }
3404
+ function getWeixinMessageTurnLane(full) {
3405
+ const textBody = extractTextBody(full.item_list).trim().toLowerCase();
3406
+ return textBody === "/cancel" || textBody === "/stop" ? "control" : "normal";
3407
+ }
3005
3408
  function findMediaItem(itemList) {
3006
3409
  if (!itemList?.length)
3007
3410
  return;
@@ -3253,6 +3656,10 @@ async function monitorWeixinProvider(opts) {
3253
3656
  log(`[weixin] no previous sync buf, starting fresh`);
3254
3657
  }
3255
3658
  const configManager = new WeixinConfigManager({ baseUrl, token }, log);
3659
+ const conversationExecutor = createConversationExecutor();
3660
+ const seenMessageIds = new Set;
3661
+ const messageIdOrder = [];
3662
+ const DEDUP_WINDOW = 100;
3256
3663
  let nextTimeoutMs = longPollTimeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS2;
3257
3664
  let consecutiveFailures = 0;
3258
3665
  while (!abortSignal?.aborted) {
@@ -3296,10 +3703,22 @@ async function monitorWeixinProvider(opts) {
3296
3703
  }
3297
3704
  const list = resp.msgs ?? [];
3298
3705
  for (const full of list) {
3706
+ const msgId = full.message_id;
3707
+ if (msgId != null) {
3708
+ if (seenMessageIds.has(msgId)) {
3709
+ aLog.info(`duplicate message skipped: message_id=${msgId}`);
3710
+ continue;
3711
+ }
3712
+ seenMessageIds.add(msgId);
3713
+ messageIdOrder.push(msgId);
3714
+ if (messageIdOrder.length > DEDUP_WINDOW) {
3715
+ seenMessageIds.delete(messageIdOrder.shift());
3716
+ }
3717
+ }
3299
3718
  aLog.info(`inbound: from=${full.from_user_id} types=${full.item_list?.map((i) => i.type).join(",") ?? "none"}`);
3300
3719
  const fromUserId = full.from_user_id ?? "";
3301
3720
  const cachedConfig = await configManager.getForUser(fromUserId, full.context_token);
3302
- await handleWeixinMessageTurn(full, {
3721
+ conversationExecutor.run(full.from_user_id ?? "", getWeixinMessageTurnLane(full), () => handleWeixinMessageTurn(full, {
3303
3722
  accountId,
3304
3723
  agent,
3305
3724
  baseUrl,
@@ -3308,6 +3727,8 @@ async function monitorWeixinProvider(opts) {
3308
3727
  typingTicket: cachedConfig.typingTicket,
3309
3728
  log,
3310
3729
  errLog
3730
+ })).catch((err) => {
3731
+ errLog(`[weixin] message turn failed: ${String(err)}`);
3311
3732
  });
3312
3733
  }
3313
3734
  } catch (err) {
@@ -4479,29 +4900,34 @@ async function handleCancel(context, chatKey) {
4479
4900
  const result = await context.interaction.cancelTransportSession(session);
4480
4901
  return { text: result.message || "cancelled" };
4481
4902
  } catch (error) {
4903
+ const recovered = await context.recovery.tryRecoverMissingSession(session, error);
4904
+ if (recovered) {
4905
+ const result = await context.interaction.cancelTransportSession(recovered);
4906
+ return { text: result.message || "cancelled" };
4907
+ }
4482
4908
  return context.recovery.renderTransportError(session, error);
4483
4909
  }
4484
4910
  }
4485
4911
  async function handleSessionReset(context, chatKey) {
4486
4912
  return await context.lifecycle.resetCurrentSession(chatKey);
4487
4913
  }
4914
+ async function promptWithSession(context, session, text, reply) {
4915
+ const effectiveReplyMode = session.replyMode ?? context.config?.wechat.replyMode ?? "stream";
4916
+ const transportReply = effectiveReplyMode === "stream" ? reply : undefined;
4917
+ const result = await context.interaction.promptTransportSession(session, text, transportReply);
4918
+ return { text: transportReply ? undefined : result.text };
4919
+ }
4488
4920
  async function handlePrompt(context, chatKey, text, reply) {
4489
4921
  const session = await context.sessions.getCurrentSession(chatKey);
4490
4922
  if (!session) {
4491
4923
  return { text: NO_CURRENT_SESSION_TEXT };
4492
4924
  }
4493
4925
  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 };
4926
+ return await promptWithSession(context, session, text, reply);
4498
4927
  } catch (error) {
4499
4928
  const recovered = await context.recovery.tryRecoverMissingSession(session, error);
4500
4929
  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 };
4930
+ return await promptWithSession(context, recovered, text, reply);
4505
4931
  }
4506
4932
  return context.recovery.renderTransportError(session, error);
4507
4933
  }
@@ -4892,7 +5318,7 @@ async function handleSessionShortcutCommand(context, ops, chatKey, agent, target
4892
5318
  `)
4893
5319
  };
4894
5320
  }
4895
- const session = ops.resolveSession(alias, agent, workspace.name, `${workspace.name}:${alias}`);
5321
+ const session = ops.resolveSession(alias, agent, workspace.name, alias);
4896
5322
  try {
4897
5323
  await ops.ensureTransportSession(session);
4898
5324
  const exists = await ops.checkTransportSession(session);
@@ -5635,11 +6061,18 @@ var init_ensure_config = __esm(() => {
5635
6061
 
5636
6062
  // src/config/resolve-acpx-command.ts
5637
6063
  import { readFileSync } from "node:fs";
5638
- import { posix, win32 } from "node:path";
5639
6064
  import { createRequire as createRequire2 } from "node:module";
6065
+ import { posix, win32 } from "node:path";
5640
6066
  function resolveAcpxCommand(options = {}) {
6067
+ return resolveAcpxCommandMetadata(options).command;
6068
+ }
6069
+ function resolveAcpxCommandMetadata(options = {}) {
5641
6070
  if (options.configuredCommand) {
5642
- return options.configuredCommand;
6071
+ return {
6072
+ command: options.configuredCommand,
6073
+ source: "config",
6074
+ explanation: "transport.command is set, so the configured command wins."
6075
+ };
5643
6076
  }
5644
6077
  const platform = options.platform ?? process.platform;
5645
6078
  const resolvePackageJson = options.resolvePackageJson ?? ((id) => require2.resolve(id));
@@ -5651,10 +6084,18 @@ function resolveAcpxCommand(options = {}) {
5651
6084
  const packageDir = pathApi.dirname(packageJsonPath);
5652
6085
  const binPath = typeof pkg.bin === "string" ? pkg.bin : pkg.bin && typeof pkg.bin.acpx === "string" ? pkg.bin.acpx : null;
5653
6086
  if (binPath) {
5654
- return pathApi.resolve(packageDir, binPath);
6087
+ return {
6088
+ command: pathApi.resolve(packageDir, binPath),
6089
+ source: "bundled",
6090
+ explanation: "transport.command is unset, so the bundled acpx dependency is used."
6091
+ };
5655
6092
  }
5656
6093
  } catch {}
5657
- return "acpx";
6094
+ return {
6095
+ command: "acpx",
6096
+ source: "PATH",
6097
+ explanation: "transport.command is unset and no bundled acpx was found, so PATH is the fallback."
6098
+ };
5658
6099
  }
5659
6100
  var require2;
5660
6101
  var init_resolve_acpx_command = __esm(() => {
@@ -6634,6 +7075,7 @@ var init_acpx_cli_transport = __esm(() => {
6634
7075
  var exports_main = {};
6635
7076
  __export(exports_main, {
6636
7077
  resolveRuntimePaths: () => resolveRuntimePaths,
7078
+ resolveBridgeEntryPath: () => resolveBridgeEntryPath,
6637
7079
  main: () => main2,
6638
7080
  buildApp: () => buildApp
6639
7081
  });
@@ -6737,362 +7179,974 @@ var init_main = __esm(async () => {
6737
7179
  if (false) {}
6738
7180
  });
6739
7181
 
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";
7182
+ // src/doctor/checks/acpx-check.ts
7183
+ import { spawn as spawn4 } from "node:child_process";
7184
+ async function checkAcpx(options = {}) {
7185
+ const runtimePaths = (options.resolveRuntimePaths ?? resolveRuntimePaths)();
7186
+ try {
7187
+ const config = await (options.loadConfig ?? loadConfig)(runtimePaths.configPath);
7188
+ const metadata = (options.resolveAcpxCommandMetadata ?? resolveAcpxCommandMetadata)({
7189
+ configuredCommand: config.transport.command
7190
+ });
7191
+ const version = await (options.runVersion ?? defaultRunVersion)(metadata.command);
7192
+ return {
7193
+ id: "acpx",
7194
+ label: "acpx",
7195
+ severity: "pass",
7196
+ summary: `resolved ${metadata.command} (${version})`,
7197
+ details: buildDetails(metadata, version, options.verbose),
7198
+ metadata: {
7199
+ command: metadata.command,
7200
+ source: metadata.source,
7201
+ version
7202
+ }
7203
+ };
7204
+ } catch (error) {
7205
+ const message = formatError(error);
7206
+ const details = [`config path: ${runtimePaths.configPath}`, `error: ${message}`];
7207
+ return {
7208
+ id: "acpx",
7209
+ label: "acpx",
7210
+ severity: "fail",
7211
+ summary: "acpx version check failed",
7212
+ details
7213
+ };
7214
+ }
7215
+ }
7216
+ function buildDetails(metadata, version, verbose) {
7217
+ const details = [
7218
+ `command: ${metadata.command}`,
7219
+ `source: ${metadata.source}`,
7220
+ `version: ${version}`
7221
+ ];
7222
+ if (verbose) {
7223
+ details.push(`resolution: ${metadata.explanation}`);
7224
+ }
7225
+ return details;
7226
+ }
7227
+ async function defaultRunVersion(command) {
7228
+ const spawnSpec = resolveSpawnCommand(command, ["--version"]);
7229
+ return await new Promise((resolve2, reject) => {
7230
+ const child = spawn4(spawnSpec.command, spawnSpec.args, {
7231
+ stdio: ["ignore", "pipe", "pipe"]
7232
+ });
7233
+ let stdout = "";
7234
+ let stderr = "";
7235
+ child.stdout.on("data", (chunk) => {
7236
+ stdout += String(chunk);
7237
+ });
7238
+ child.stderr.on("data", (chunk) => {
7239
+ stderr += String(chunk);
7240
+ });
7241
+ child.on("error", reject);
7242
+ child.on("close", (code) => {
7243
+ if (code === 0) {
7244
+ const version = stdout.trim() || stderr.trim();
7245
+ if (version.length > 0) {
7246
+ resolve2(version);
7247
+ return;
7248
+ }
7249
+ }
7250
+ reject(new Error(stderr.trim() || stdout.trim() || `acpx --version exited with code ${code ?? 1}`));
7251
+ });
7252
+ });
7253
+ }
7254
+ function formatError(error) {
7255
+ return error instanceof Error ? error.message : String(error);
7256
+ }
7257
+ var init_acpx_check = __esm(async () => {
7258
+ init_load_config();
7259
+ init_resolve_acpx_command();
7260
+ init_spawn_command();
7261
+ await init_main();
7262
+ });
6752
7263
 
6753
- // src/daemon/daemon-status.ts
6754
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
6755
- import { dirname } from "node:path";
7264
+ // src/doctor/checks/bridge-check.ts
7265
+ async function checkBridge(options = {}) {
7266
+ const runtimePaths = (options.resolveRuntimePaths ?? resolveRuntimePaths)();
7267
+ try {
7268
+ const config = await (options.loadConfig ?? loadConfig)(runtimePaths.configPath);
7269
+ if (config.transport.type === "acpx-cli") {
7270
+ return {
7271
+ id: "bridge",
7272
+ label: "Bridge",
7273
+ severity: "skip",
7274
+ summary: "bridge check skipped for acpx-cli transport"
7275
+ };
7276
+ }
7277
+ const metadata = (options.resolveAcpxCommandMetadata ?? resolveAcpxCommandMetadata)({
7278
+ configuredCommand: config.transport.command
7279
+ });
7280
+ const client = await (options.spawnAcpxBridgeClient ?? spawnAcpxBridgeClient)({
7281
+ acpxCommand: metadata.command,
7282
+ bridgeEntryPath: (options.resolveBridgeEntryPath ?? resolveBridgeEntryPath)(),
7283
+ cwd: options.cwd ?? process.cwd(),
7284
+ permissionMode: config.transport.permissionMode,
7285
+ nonInteractivePermissions: config.transport.nonInteractivePermissions
7286
+ });
7287
+ try {
7288
+ return {
7289
+ id: "bridge",
7290
+ label: "Bridge",
7291
+ severity: "pass",
7292
+ summary: "bridge responded to ping",
7293
+ details: buildDetails2(metadata, options.verbose),
7294
+ metadata: {
7295
+ acpxCommand: metadata.command,
7296
+ source: metadata.source
7297
+ }
7298
+ };
7299
+ } finally {
7300
+ await client.dispose();
7301
+ }
7302
+ } catch (error) {
7303
+ return {
7304
+ id: "bridge",
7305
+ label: "Bridge",
7306
+ severity: "fail",
7307
+ summary: "bridge startup failed",
7308
+ details: [`config path: ${runtimePaths.configPath}`, `error: ${formatError2(error)}`]
7309
+ };
7310
+ }
7311
+ }
7312
+ function buildDetails2(metadata, verbose) {
7313
+ const details = [`acpx command: ${metadata.command}`, `source: ${metadata.source}`];
7314
+ if (verbose) {
7315
+ details.push(`resolution: ${metadata.explanation}`);
7316
+ }
7317
+ return details;
7318
+ }
7319
+ function formatError2(error) {
7320
+ return error instanceof Error ? error.message : String(error);
7321
+ }
7322
+ var init_bridge_check = __esm(async () => {
7323
+ init_load_config();
7324
+ init_resolve_acpx_command();
7325
+ init_acpx_bridge_client();
7326
+ await init_main();
7327
+ });
6756
7328
 
6757
- class DaemonStatusStore {
6758
- path;
6759
- constructor(path) {
6760
- this.path = path;
7329
+ // src/doctor/checks/config-check.ts
7330
+ async function checkConfig(options = {}) {
7331
+ const runtimePaths = (options.resolveRuntimePaths ?? resolveRuntimePaths)();
7332
+ const configPath = runtimePaths.configPath;
7333
+ try {
7334
+ const config = await (options.loadConfig ?? loadConfig)(configPath);
7335
+ return {
7336
+ id: "config",
7337
+ label: "Config",
7338
+ severity: "pass",
7339
+ summary: "configuration loaded",
7340
+ details: [`config path: ${configPath}`],
7341
+ metadata: {
7342
+ configPath,
7343
+ config
7344
+ }
7345
+ };
7346
+ } catch (error) {
7347
+ return {
7348
+ id: "config",
7349
+ label: "Config",
7350
+ severity: "fail",
7351
+ summary: "configuration is invalid",
7352
+ details: [`config path: ${configPath}`, `error: ${formatError3(error)}`]
7353
+ };
6761
7354
  }
6762
- async load() {
6763
- try {
6764
- const content = await readFile(this.path, "utf8");
6765
- if (content.trim() === "") {
6766
- return null;
7355
+ }
7356
+ function formatError3(error) {
7357
+ if (error instanceof Error) {
7358
+ return error.message;
7359
+ }
7360
+ return String(error);
7361
+ }
7362
+ var init_config_check = __esm(async () => {
7363
+ init_load_config();
7364
+ await init_main();
7365
+ });
7366
+
7367
+ // src/doctor/checks/daemon-check.ts
7368
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
7369
+ import { homedir as homedir6 } from "node:os";
7370
+ async function checkDaemon(options = {}) {
7371
+ const home = options.home ?? process.env.HOME ?? homedir6();
7372
+ const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({ home });
7373
+ const controller = createDaemonController(paths, {
7374
+ processExecPath: options.processExecPath ?? process.execPath,
7375
+ cliEntryPath: options.cliEntryPath ?? resolveCliEntryPath(),
7376
+ cwd: options.cwd ?? process.cwd(),
7377
+ env: options.env ?? process.env,
7378
+ isProcessRunning: options.isProcessRunning ?? defaultIsProcessRunning3
7379
+ });
7380
+ try {
7381
+ const status = await controller.getStatus();
7382
+ switch (status.state) {
7383
+ case "running":
7384
+ return {
7385
+ id: "daemon",
7386
+ label: "Daemon",
7387
+ severity: "pass",
7388
+ summary: "daemon is running",
7389
+ details: [`pid: ${status.pid}`],
7390
+ metadata: {
7391
+ paths,
7392
+ status
7393
+ }
7394
+ };
7395
+ case "stopped":
7396
+ return {
7397
+ id: "daemon",
7398
+ label: "Daemon",
7399
+ severity: "warn",
7400
+ summary: status.stale ? "daemon was stopped and stale runtime files were cleared" : "daemon is not running",
7401
+ details: status.stale ? ["stale runtime files were cleared"] : undefined,
7402
+ suggestions: ["run: weacpx start"],
7403
+ metadata: {
7404
+ paths,
7405
+ status
7406
+ }
7407
+ };
7408
+ case "indeterminate":
7409
+ return {
7410
+ id: "daemon",
7411
+ label: "Daemon",
7412
+ severity: "fail",
7413
+ summary: "daemon status is indeterminate",
7414
+ details: [`pid: ${status.pid}`, `reason: ${status.reason}`],
7415
+ metadata: {
7416
+ paths,
7417
+ status
7418
+ }
7419
+ };
7420
+ }
7421
+ return {
7422
+ id: "daemon",
7423
+ label: "Daemon",
7424
+ severity: "fail",
7425
+ summary: "daemon status lookup returned an unknown state",
7426
+ details: [`state: ${status.state ?? "unknown"}`],
7427
+ metadata: {
7428
+ paths
6767
7429
  }
6768
- return JSON.parse(content);
6769
- } catch (error) {
6770
- if (error.code === "ENOENT") {
6771
- return null;
7430
+ };
7431
+ } catch (error) {
7432
+ return {
7433
+ id: "daemon",
7434
+ label: "Daemon",
7435
+ severity: "fail",
7436
+ summary: "daemon status could not be read",
7437
+ details: [
7438
+ `runtime dir: ${paths.runtimeDir}`,
7439
+ `pid file: ${paths.pidFile}`,
7440
+ `status file: ${paths.statusFile}`,
7441
+ `error: ${formatError4(error)}`
7442
+ ],
7443
+ metadata: {
7444
+ paths
6772
7445
  }
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));
7446
+ };
6779
7447
  }
6780
- async clear() {
6781
- await rm(this.path, { force: true });
7448
+ }
7449
+ function defaultIsProcessRunning3(pid) {
7450
+ try {
7451
+ process.kill(pid, 0);
7452
+ return true;
7453
+ } catch {
7454
+ return false;
6782
7455
  }
6783
7456
  }
7457
+ function resolveCliEntryPath() {
7458
+ return process.argv[1] ?? fileURLToPath4(import.meta.url);
7459
+ }
7460
+ function formatError4(error) {
7461
+ return error instanceof Error ? error.message : String(error);
7462
+ }
7463
+ var init_daemon_check = __esm(() => {
7464
+ init_create_daemon_controller();
7465
+ init_daemon_files();
7466
+ });
6784
7467
 
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
- });
7468
+ // src/doctor/checks/runtime-check.ts
7469
+ import { constants } from "node:fs";
7470
+ import { access as access3, stat as stat2 } from "node:fs/promises";
7471
+ import { dirname as dirname10 } from "node:path";
7472
+ import { homedir as homedir7 } from "node:os";
7473
+ async function checkRuntime(options = {}) {
7474
+ const home = options.home ?? process.env.HOME ?? homedir7();
7475
+ const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({ home });
7476
+ const probe = options.probe ?? createRuntimeFsProbe();
7477
+ const platform = options.platform ?? process.platform;
7478
+ const checks = [
7479
+ await checkDirectoryCreatable("runtimeDir", paths.runtimeDir, probe, platform),
7480
+ await checkFileCreatable("pidFile", paths.pidFile, probe, platform),
7481
+ await checkFileCreatable("statusFile", paths.statusFile, probe, platform),
7482
+ await checkFileCreatable("stdoutLog", paths.stdoutLog, probe, platform),
7483
+ await checkFileCreatable("stderrLog", paths.stderrLog, probe, platform),
7484
+ await checkFileCreatable("appLog", paths.appLog, probe, platform)
7485
+ ];
7486
+ const failure = checks.find((check) => !check.ok);
7487
+ if (failure) {
7488
+ return {
7489
+ id: "runtime",
7490
+ label: "Runtime",
7491
+ severity: "fail",
7492
+ summary: "daemon runtime paths are not usable",
7493
+ details: checks.map((check) => check.detail)
7494
+ };
6810
7495
  }
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 };
7496
+ return {
7497
+ id: "runtime",
7498
+ label: "Runtime",
7499
+ severity: "pass",
7500
+ summary: "daemon runtime paths are usable",
7501
+ details: checks.map((check) => check.detail),
7502
+ metadata: {
7503
+ paths
6820
7504
  }
6821
- if (!status) {
6822
- return { state: "indeterminate", pid, reason: "missing-status" };
7505
+ };
7506
+ }
7507
+ function createRuntimeFsProbe() {
7508
+ return {
7509
+ stat: async (path11) => await stat2(path11),
7510
+ access: async (path11, mode) => await access3(path11, mode)
7511
+ };
7512
+ }
7513
+ async function checkDirectoryCreatable(label, path11, probe, platform) {
7514
+ try {
7515
+ const stats = await probe.stat(path11);
7516
+ if (!stats.isDirectory()) {
7517
+ return {
7518
+ ok: false,
7519
+ detail: `${label}: ${path11} (exists but is not a directory)`
7520
+ };
6823
7521
  }
7522
+ await probe.access(path11, directoryAccessMode(platform));
6824
7523
  return {
6825
- state: "running",
6826
- pid,
6827
- status
7524
+ ok: true,
7525
+ detail: `${label}: ${path11} (writable)`
6828
7526
  };
6829
- }
6830
- async start() {
6831
- const current = await this.getStatus();
6832
- if (current.state === "running") {
6833
- return { state: "already-running", pid: current.pid };
7527
+ } catch (error) {
7528
+ if (!isMissingPathError(error)) {
7529
+ return {
7530
+ ok: false,
7531
+ detail: `${label}: ${path11} (unusable: ${formatError5(error)})`
7532
+ };
6834
7533
  }
6835
- if (current.state === "indeterminate") {
6836
- throw new Error(`weacpx daemon process is already running (pid ${current.pid}) but status metadata is missing`);
7534
+ const parentCheck = await checkCreatableAncestorDirectory(path11, probe, platform);
7535
+ if (!parentCheck.ok) {
7536
+ return {
7537
+ ok: false,
7538
+ detail: `${label}: ${path11} (parent not writable: ${parentCheck.blockingPath})`
7539
+ };
6837
7540
  }
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 };
7541
+ return {
7542
+ ok: true,
7543
+ detail: `${label}: ${path11} (creatable via ${parentCheck.creatableFrom})`
7544
+ };
6843
7545
  }
6844
- async stop() {
6845
- const pid = await this.loadPid();
6846
- if (!pid) {
6847
- return { state: "stopped", detail: "not-running" };
7546
+ }
7547
+ async function checkFileCreatable(label, path11, probe, platform) {
7548
+ try {
7549
+ const stats = await probe.stat(path11);
7550
+ if (stats.isDirectory()) {
7551
+ return {
7552
+ ok: false,
7553
+ detail: `${label}: ${path11} (exists but is a directory)`
7554
+ };
6848
7555
  }
6849
- if (this.deps.isProcessRunning(pid)) {
6850
- await this.deps.terminateProcess(pid);
6851
- await this.waitForShutdown(pid);
7556
+ await probe.access(path11, constants.W_OK);
7557
+ return {
7558
+ ok: true,
7559
+ detail: `${label}: ${path11} (writable)`
7560
+ };
7561
+ } catch (error) {
7562
+ if (!isMissingPathError(error)) {
7563
+ return {
7564
+ ok: false,
7565
+ detail: `${label}: ${path11} (unusable: ${formatError5(error)})`
7566
+ };
6852
7567
  }
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;
7568
+ const parentCheck = await checkCreatableAncestorDirectory(dirname10(path11), probe, platform);
7569
+ if (!parentCheck.ok) {
7570
+ return {
7571
+ ok: false,
7572
+ detail: `${label}: ${path11} (parent not writable: ${parentCheck.blockingPath})`
7573
+ };
6866
7574
  }
7575
+ return {
7576
+ ok: true,
7577
+ detail: `${label}: ${path11} (creatable via ${parentCheck.creatableFrom})`
7578
+ };
6867
7579
  }
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();
7580
+ }
7581
+ async function checkCreatableAncestorDirectory(path11, probe, platform) {
7582
+ try {
7583
+ const stats = await probe.stat(path11);
7584
+ if (!stats.isDirectory()) {
7585
+ return {
7586
+ ok: false,
7587
+ creatableFrom: path11,
7588
+ blockingPath: path11
7589
+ };
6889
7590
  }
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();
7591
+ await probe.access(path11, directoryAccessMode(platform));
7592
+ return {
7593
+ ok: true,
7594
+ creatableFrom: path11
7595
+ };
7596
+ } catch (error) {
7597
+ if (!isMissingPathError(error)) {
7598
+ return {
7599
+ ok: false,
7600
+ creatableFrom: path11,
7601
+ blockingPath: path11
7602
+ };
6899
7603
  }
6900
- if (!this.deps.isProcessRunning(pid)) {
6901
- return;
7604
+ const parent = dirname10(path11);
7605
+ if (parent === path11) {
7606
+ return {
7607
+ ok: false,
7608
+ creatableFrom: path11,
7609
+ blockingPath: path11
7610
+ };
6902
7611
  }
6903
- throw new Error(`weacpx daemon did not exit within ${this.shutdownTimeoutMs}ms (pid ${pid})`);
7612
+ const parentCheck = await checkCreatableAncestorDirectory(parent, probe, platform);
7613
+ if (!parentCheck.ok) {
7614
+ return parentCheck;
7615
+ }
7616
+ return {
7617
+ ok: true,
7618
+ creatableFrom: parentCheck.creatableFrom
7619
+ };
6904
7620
  }
6905
7621
  }
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
- });
7622
+ function directoryAccessMode(platform) {
7623
+ return platform === "win32" ? constants.W_OK : DIRECTORY_USABLE;
6924
7624
  }
6925
- function defaultIsProcessRunning(pid) {
7625
+ function isMissingPathError(error) {
7626
+ return isErrnoError(error) && (error.code === "ENOENT" || error.code === "ENOTDIR");
7627
+ }
7628
+ function isErrnoError(error) {
7629
+ return typeof error === "object" && error !== null && "code" in error;
7630
+ }
7631
+ function formatError5(error) {
7632
+ if (error instanceof Error) {
7633
+ return error.message;
7634
+ }
7635
+ return String(error);
7636
+ }
7637
+ var DIRECTORY_USABLE;
7638
+ var init_runtime_check = __esm(() => {
7639
+ init_daemon_files();
7640
+ DIRECTORY_USABLE = constants.W_OK | constants.X_OK;
7641
+ });
7642
+
7643
+ // src/doctor/checks/smoke-check.ts
7644
+ async function checkSmoke(options = {}, deps = {}) {
7645
+ const resolvedOptions = { ...options, ...deps };
7646
+ const runtimePaths = (resolvedOptions.resolveRuntimePaths ?? resolveRuntimePaths)();
6926
7647
  try {
6927
- process.kill(pid, 0);
6928
- return true;
6929
- } catch {
6930
- return false;
7648
+ const config = await (resolvedOptions.loadConfig ?? loadConfig)(runtimePaths.configPath);
7649
+ const agentSelection = selectAgent(config, resolvedOptions.agent);
7650
+ if (agentSelection.error) {
7651
+ return agentSelection.error;
7652
+ }
7653
+ const workspaceSelection = selectWorkspace(config, resolvedOptions.workspace);
7654
+ if (workspaceSelection.error) {
7655
+ return workspaceSelection.error;
7656
+ }
7657
+ const missingDefaults = [agentSelection.missingDefault, workspaceSelection.missingDefault].filter((value) => typeof value === "string");
7658
+ if (missingDefaults.length > 0) {
7659
+ return {
7660
+ id: "smoke",
7661
+ label: "Smoke",
7662
+ severity: "skip",
7663
+ summary: `smoke prerequisites missing: ${missingDefaults.join(", ")}`,
7664
+ details: [
7665
+ `config path: ${runtimePaths.configPath}`,
7666
+ ...selectionDetails(agentSelection, workspaceSelection)
7667
+ ],
7668
+ suggestions: ["configure at least one agent and one workspace before running --smoke"]
7669
+ };
7670
+ }
7671
+ const agent = agentSelection.value;
7672
+ const workspace = workspaceSelection.value;
7673
+ if (!agent || !workspace) {
7674
+ return {
7675
+ id: "smoke",
7676
+ label: "Smoke",
7677
+ severity: "skip",
7678
+ summary: "smoke prerequisites missing: agent, workspace",
7679
+ details: [
7680
+ `config path: ${runtimePaths.configPath}`,
7681
+ ...selectionDetails(agentSelection, workspaceSelection)
7682
+ ]
7683
+ };
7684
+ }
7685
+ const metadata = (resolvedOptions.resolveAcpxCommandMetadata ?? resolveAcpxCommandMetadata)({
7686
+ configuredCommand: config.transport.command
7687
+ });
7688
+ const transport = await (resolvedOptions.createTransport ?? defaultCreateTransport)({
7689
+ config,
7690
+ metadata,
7691
+ resolveBridgeEntryPath: resolvedOptions.resolveBridgeEntryPath,
7692
+ spawnAcpxBridgeClient: resolvedOptions.spawnAcpxBridgeClient
7693
+ });
7694
+ const session = buildSession({
7695
+ config,
7696
+ agent,
7697
+ workspace,
7698
+ now: resolvedOptions.now
7699
+ });
7700
+ try {
7701
+ await transport.ensureSession(session);
7702
+ const reply = await transport.prompt(session, SMOKE_PROMPT);
7703
+ const replyText = reply.text.trim();
7704
+ if (replyText.length === 0) {
7705
+ return {
7706
+ id: "smoke",
7707
+ label: "Smoke",
7708
+ severity: "fail",
7709
+ summary: "smoke prompt returned empty text",
7710
+ details: buildDetails3({
7711
+ runtimePaths,
7712
+ metadata,
7713
+ session,
7714
+ agentReason: agentSelection.reason,
7715
+ workspaceReason: workspaceSelection.reason,
7716
+ replyText,
7717
+ verbose: resolvedOptions.verbose
7718
+ })
7719
+ };
7720
+ }
7721
+ return {
7722
+ id: "smoke",
7723
+ label: "Smoke",
7724
+ severity: replyText === "ok" ? "pass" : "warn",
7725
+ summary: replyText === "ok" ? "smoke prompt succeeded and reply received" : "smoke prompt succeeded with non-ideal reply",
7726
+ details: buildDetails3({
7727
+ runtimePaths,
7728
+ metadata,
7729
+ session,
7730
+ agentReason: agentSelection.reason,
7731
+ workspaceReason: workspaceSelection.reason,
7732
+ replyText,
7733
+ verbose: resolvedOptions.verbose
7734
+ }),
7735
+ metadata: {
7736
+ agent: session.agent,
7737
+ workspace: session.workspace,
7738
+ transportSession: session.transportSession,
7739
+ replyText
7740
+ }
7741
+ };
7742
+ } finally {
7743
+ await transport.dispose?.();
7744
+ }
7745
+ } catch (error) {
7746
+ return {
7747
+ id: "smoke",
7748
+ label: "Smoke",
7749
+ severity: "fail",
7750
+ summary: "smoke transport probe failed",
7751
+ details: [`config path: ${runtimePaths.configPath}`, `error: ${formatError6(error)}`]
7752
+ };
6931
7753
  }
6932
7754
  }
6933
- function buildSpawnRequest(paths, options, stdoutFd, stderrFd) {
6934
- const platform = options.platform ?? process.platform;
6935
- if (platform === "win32") {
7755
+ function selectAgent(config, explicitAgent) {
7756
+ if (explicitAgent) {
7757
+ if (!(explicitAgent in config.agents)) {
7758
+ return {
7759
+ reason: "explicit --agent",
7760
+ error: createSelectionFailure(`smoke agent not found: ${explicitAgent}`)
7761
+ };
7762
+ }
6936
7763
  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
- }
7764
+ value: explicitAgent,
7765
+ reason: "explicit --agent"
7766
+ };
7767
+ }
7768
+ const firstAgent = Object.keys(config.agents)[0];
7769
+ if (!firstAgent) {
7770
+ return {
7771
+ missingDefault: "agent",
7772
+ reason: "no configured agent available"
6959
7773
  };
6960
7774
  }
6961
7775
  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]
7776
+ value: firstAgent,
7777
+ reason: "default first configured agent"
7778
+ };
7779
+ }
7780
+ function selectWorkspace(config, explicitWorkspace) {
7781
+ if (explicitWorkspace) {
7782
+ if (!(explicitWorkspace in config.workspaces)) {
7783
+ return {
7784
+ reason: "explicit --workspace",
7785
+ error: createSelectionFailure(`smoke workspace not found: ${explicitWorkspace}`)
7786
+ };
6970
7787
  }
7788
+ return {
7789
+ value: explicitWorkspace,
7790
+ reason: "explicit --workspace"
7791
+ };
7792
+ }
7793
+ const firstWorkspace = Object.keys(config.workspaces)[0];
7794
+ if (!firstWorkspace) {
7795
+ return {
7796
+ missingDefault: "workspace",
7797
+ reason: "no configured workspace available"
7798
+ };
7799
+ }
7800
+ return {
7801
+ value: firstWorkspace,
7802
+ reason: "default first configured workspace"
6971
7803
  };
6972
7804
  }
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");
7805
+ function createSelectionFailure(summary) {
7806
+ return {
7807
+ id: "smoke",
7808
+ label: "Smoke",
7809
+ severity: "fail",
7810
+ summary
7811
+ };
6986
7812
  }
6987
- async function defaultSpawnProcess(request) {
6988
- if (request.mode === "windows-hidden") {
6989
- return await spawnWindowsHiddenProcess(request);
7813
+ function selectionDetails(agentSelection, workspaceSelection) {
7814
+ const details = [];
7815
+ if (agentSelection.value) {
7816
+ details.push(`agent: ${agentSelection.value} (${agentSelection.reason})`);
7817
+ } else {
7818
+ details.push(`agent: unavailable (${agentSelection.reason})`);
6990
7819
  }
6991
- const child = spawn(request.command, request.args, request.options);
6992
- child.unref();
6993
- return child.pid ?? 0;
7820
+ if (workspaceSelection.value) {
7821
+ details.push(`workspace: ${workspaceSelection.value} (${workspaceSelection.reason})`);
7822
+ } else {
7823
+ details.push(`workspace: unavailable (${workspaceSelection.reason})`);
7824
+ }
7825
+ return details;
6994
7826
  }
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);
7827
+ function buildSession(options) {
7828
+ const timestamp = (options.now ?? (() => new Date))().getTime();
7829
+ const agentConfig = options.config.agents[options.agent];
7830
+ const workspaceConfig = options.config.workspaces[options.workspace];
7831
+ if (!agentConfig) {
7832
+ throw new Error(`smoke agent not found: ${options.agent}`);
7833
+ }
7834
+ if (!workspaceConfig) {
7835
+ throw new Error(`smoke workspace not found: ${options.workspace}`);
7836
+ }
7837
+ return {
7838
+ alias: "weacpx-doctor",
7839
+ agent: options.agent,
7840
+ ...agentConfig.command ? { agentCommand: agentConfig.command } : {},
7841
+ workspace: options.workspace,
7842
+ transportSession: `weacpx-doctor-${timestamp}`,
7843
+ replyMode: options.config.wechat.replyMode,
7844
+ cwd: workspaceConfig.cwd
7845
+ };
7846
+ }
7847
+ async function defaultCreateTransport(options) {
7848
+ if (options.config.transport.type === "acpx-bridge") {
7849
+ const client = await (options.spawnAcpxBridgeClient ?? spawnAcpxBridgeClient)({
7850
+ acpxCommand: options.metadata.command,
7851
+ bridgeEntryPath: (options.resolveBridgeEntryPath ?? resolveBridgeEntryPath)(),
7852
+ permissionMode: options.config.transport.permissionMode,
7853
+ nonInteractivePermissions: options.config.transport.nonInteractivePermissions
7039
7854
  });
7855
+ return new AcpxBridgeTransport(client);
7856
+ }
7857
+ return new AcpxCliTransport({
7858
+ ...options.config.transport,
7859
+ command: options.metadata.command
7040
7860
  });
7041
7861
  }
7042
- async function defaultTerminateProcess(pid) {
7043
- await terminateProcessTree(pid);
7862
+ function buildDetails3(options) {
7863
+ const details = [
7864
+ `config path: ${options.runtimePaths.configPath}`,
7865
+ `agent: ${options.session.agent} (${options.agentReason})`,
7866
+ `workspace: ${options.session.workspace} (${options.workspaceReason})`,
7867
+ `transport session: ${options.session.transportSession}`,
7868
+ `reply: ${JSON.stringify(options.replyText)}`
7869
+ ];
7870
+ if (options.verbose) {
7871
+ details.push(`transport type: ${options.session.agentCommand ? "agent-command" : "driver-default"}`);
7872
+ details.push(`acpx command: ${options.metadata.command}`);
7873
+ details.push(`acpx source: ${options.metadata.source}`);
7874
+ details.push(`smoke prompt: ${JSON.stringify(SMOKE_PROMPT)}`);
7875
+ }
7876
+ return details;
7044
7877
  }
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") {
7878
+ function formatError6(error) {
7879
+ return error instanceof Error ? error.message : String(error);
7880
+ }
7881
+ var SMOKE_PROMPT = "Reply with exactly: ok";
7882
+ var init_smoke_check = __esm(async () => {
7883
+ init_load_config();
7884
+ init_resolve_acpx_command();
7885
+ init_acpx_bridge_client();
7886
+ init_acpx_cli_transport();
7887
+ await init_main();
7888
+ });
7889
+
7890
+ // src/doctor/checks/wechat-check.ts
7891
+ async function checkWechat(options = {}) {
7892
+ const ids = listWeixinAccountIds();
7893
+ const accounts = ids.map((accountId) => {
7049
7894
  try {
7050
- await runCommand("taskkill", ["/PID", String(pid), "/T", "/F"]);
7051
- } catch {}
7052
- return;
7895
+ return {
7896
+ accountId,
7897
+ account: resolveWeixinAccount(accountId)
7898
+ };
7899
+ } catch (error) {
7900
+ return {
7901
+ accountId,
7902
+ error: formatError7(error)
7903
+ };
7904
+ }
7905
+ });
7906
+ const configuredAccount = accounts.find((entry) => ("account" in entry) && entry.account.configured);
7907
+ const loggedIn = Boolean(configuredAccount);
7908
+ if (!loggedIn) {
7909
+ return {
7910
+ id: "wechat",
7911
+ label: "WeChat",
7912
+ severity: "warn",
7913
+ summary: "wechat is not logged in",
7914
+ details: buildVerboseDetails(false, options.verbose, accounts),
7915
+ suggestions: ["weacpx login"]
7916
+ };
7053
7917
  }
7054
- const targetPid = pid > 0 ? -pid : pid;
7055
- try {
7056
- killProcess(targetPid, "SIGTERM");
7057
- } catch {
7918
+ return {
7919
+ id: "wechat",
7920
+ label: "WeChat",
7921
+ severity: "pass",
7922
+ summary: "wechat is logged in",
7923
+ details: buildVerboseDetails(true, options.verbose, accounts)
7924
+ };
7925
+ }
7926
+ function buildVerboseDetails(loggedIn, verbose, accounts) {
7927
+ if (!verbose) {
7058
7928
  return;
7059
7929
  }
7060
- const deadline = Date.now() + 5000;
7061
- while (Date.now() < deadline) {
7062
- if (!isProcessRunning(targetPid)) {
7063
- return;
7930
+ const details = [];
7931
+ details.push(`loggedIn: ${loggedIn}`);
7932
+ details.push(`accountIds: ${accounts.length > 0 ? accounts.map((entry) => entry.accountId).join(", ") : "(none)"}`);
7933
+ for (const entry of accounts) {
7934
+ if ("account" in entry) {
7935
+ details.push(`account[${entry.account.accountId}].configured: ${entry.account.configured}`);
7936
+ details.push(`account[${entry.account.accountId}].baseUrl: ${entry.account.baseUrl}`);
7937
+ continue;
7064
7938
  }
7065
- await new Promise((resolve) => setTimeout(resolve, 100));
7939
+ details.push(`account[${entry.accountId}].resolveError: ${entry.error ?? "unknown"}`);
7066
7940
  }
7067
- try {
7068
- killProcess(targetPid, "SIGKILL");
7069
- } catch {}
7941
+ return details;
7070
7942
  }
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
- });
7943
+ function formatError7(error) {
7944
+ return error instanceof Error ? error.message : String(error);
7077
7945
  }
7946
+ var init_wechat_check = __esm(() => {
7947
+ init_weixin();
7948
+ });
7078
7949
 
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");
7950
+ // src/doctor/render-doctor.ts
7951
+ function renderDoctor(report, options = {}) {
7952
+ return options.verbose ? renderVerboseDoctor(report) : renderDefaultDoctor(report);
7953
+ }
7954
+ function renderDefaultDoctor(report) {
7955
+ const lines = [];
7956
+ for (const check of report.checks) {
7957
+ lines.push(renderCheckLine(check));
7958
+ }
7959
+ lines.push(renderSummaryLine(report.checks));
7960
+ const suggestions = collectSuggestions(report.checks);
7961
+ if (suggestions.length > 0) {
7962
+ lines.push("Next steps:");
7963
+ for (const suggestion of suggestions) {
7964
+ lines.push(`- ${suggestion}`);
7965
+ }
7966
+ }
7967
+ return lines;
7968
+ }
7969
+ function renderVerboseDoctor(report) {
7970
+ const lines = [];
7971
+ for (const check of report.checks) {
7972
+ lines.push(renderCheckLine(check));
7973
+ for (const detail of check.details ?? []) {
7974
+ lines.push(` detail: ${detail}`);
7975
+ }
7976
+ }
7977
+ lines.push(renderSummaryLine(report.checks));
7978
+ const suggestions = collectSuggestions(report.checks);
7979
+ if (suggestions.length > 0) {
7980
+ lines.push("Next steps:");
7981
+ for (const suggestion of suggestions) {
7982
+ lines.push(`- ${suggestion}`);
7983
+ }
7984
+ }
7985
+ return lines;
7986
+ }
7987
+ function renderCheckLine(check) {
7988
+ return `${SEVERITY_LABELS[check.severity]} ${check.label}: ${check.summary}`;
7989
+ }
7990
+ function renderSummaryLine(checks) {
7991
+ const counts = summarizeChecks(checks);
7992
+ return `Summary: PASS ${counts.pass}, WARN ${counts.warn}, FAIL ${counts.fail}, SKIP ${counts.skip}`;
7993
+ }
7994
+ function summarizeChecks(checks) {
7995
+ return checks.reduce((counts, check) => {
7996
+ counts[check.severity] += 1;
7997
+ return counts;
7998
+ }, { pass: 0, warn: 0, fail: 0, skip: 0 });
7999
+ }
8000
+ function collectSuggestions(checks) {
8001
+ const seen = new Set;
8002
+ const suggestions = [];
8003
+ for (const check of checks) {
8004
+ for (const suggestion of check.suggestions ?? []) {
8005
+ if (seen.has(suggestion)) {
8006
+ continue;
8007
+ }
8008
+ seen.add(suggestion);
8009
+ suggestions.push(suggestion);
8010
+ }
8011
+ }
8012
+ return suggestions;
8013
+ }
8014
+ var SEVERITY_LABELS;
8015
+ var init_render_doctor = __esm(() => {
8016
+ SEVERITY_LABELS = {
8017
+ pass: "PASS",
8018
+ warn: "WARN",
8019
+ fail: "FAIL",
8020
+ skip: "SKIP"
8021
+ };
8022
+ });
8023
+
8024
+ // src/doctor/doctor.ts
8025
+ import { homedir as homedir8 } from "node:os";
8026
+ import { join as join6 } from "node:path";
8027
+ async function runDoctor(options = {}, deps = {}) {
8028
+ const home = deps.home ?? process.env.HOME ?? homedir8();
8029
+ const runtimePaths = resolveDoctorRuntimePaths(home, deps.resolveRuntimePaths);
8030
+ const sharedLoadConfig = createSharedLoadConfig(runtimePaths, deps.loadConfig ?? loadConfig);
8031
+ const checks = [];
8032
+ checks.push(await (deps.checkConfig ?? checkConfig)({
8033
+ loadConfig: sharedLoadConfig,
8034
+ resolveRuntimePaths: () => runtimePaths
8035
+ }));
8036
+ checks.push(await (deps.checkRuntime ?? checkRuntime)({
8037
+ home
8038
+ }));
8039
+ checks.push(await (deps.checkDaemon ?? checkDaemon)({
8040
+ home
8041
+ }));
8042
+ checks.push(await (deps.checkWechat ?? checkWechat)({
8043
+ verbose: options.verbose
8044
+ }));
8045
+ checks.push(await (deps.checkAcpx ?? checkAcpx)({
8046
+ verbose: options.verbose,
8047
+ loadConfig: sharedLoadConfig,
8048
+ resolveRuntimePaths: () => runtimePaths
8049
+ }));
8050
+ checks.push(await (deps.checkBridge ?? checkBridge)({
8051
+ verbose: options.verbose,
8052
+ loadConfig: sharedLoadConfig,
8053
+ resolveRuntimePaths: () => runtimePaths
8054
+ }));
8055
+ checks.push(options.smoke === true ? await (deps.checkSmoke ?? ((runOptions) => defaultCheckSmoke(runOptions, {
8056
+ resolveRuntimePaths: () => runtimePaths,
8057
+ loadConfig: sharedLoadConfig
8058
+ })))(options) : createSmokeSkipResult("smoke probe not requested"));
8059
+ const report = { checks };
8060
+ const output = (deps.renderDoctor ?? renderDoctor)(report, options);
7083
8061
  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")
8062
+ report,
8063
+ output,
8064
+ exitCode: checks.some((check) => check.severity === "fail") ? 1 : 0
8065
+ };
8066
+ }
8067
+ function resolveDoctorRuntimePaths(home, resolver) {
8068
+ if (resolver) {
8069
+ return resolver();
8070
+ }
8071
+ if (depsUseExplicitRuntimeOverrides()) {
8072
+ return resolveRuntimePaths();
8073
+ }
8074
+ return {
8075
+ configPath: join6(home, ".weacpx", "config.json"),
8076
+ statePath: join6(home, ".weacpx", "state.json")
8077
+ };
8078
+ }
8079
+ function depsUseExplicitRuntimeOverrides() {
8080
+ return Boolean(process.env.WEACPX_CONFIG || process.env.WEACPX_STATE);
8081
+ }
8082
+ function createSharedLoadConfig(runtimePaths, loader) {
8083
+ let pending;
8084
+ return async (configPath) => {
8085
+ if (configPath !== runtimePaths.configPath) {
8086
+ return await loader(configPath);
8087
+ }
8088
+ pending ??= loader(configPath);
8089
+ return await pending;
8090
+ };
8091
+ }
8092
+ async function defaultCheckSmoke(options, deps) {
8093
+ return await checkSmoke(options, {
8094
+ resolveRuntimePaths: deps.resolveRuntimePaths,
8095
+ loadConfig: deps.loadConfig
8096
+ });
8097
+ }
8098
+ function createSmokeSkipResult(summary) {
8099
+ return {
8100
+ id: "smoke",
8101
+ label: "Smoke",
8102
+ severity: "skip",
8103
+ summary
7090
8104
  };
7091
8105
  }
8106
+ var init_doctor = __esm(async () => {
8107
+ init_load_config();
8108
+ init_daemon_check();
8109
+ init_runtime_check();
8110
+ init_wechat_check();
8111
+ init_render_doctor();
8112
+ await __promiseAll([
8113
+ init_main(),
8114
+ init_acpx_check(),
8115
+ init_bridge_check(),
8116
+ init_config_check(),
8117
+ init_smoke_check()
8118
+ ]);
8119
+ });
8120
+
8121
+ // src/doctor/index.ts
8122
+ var exports_doctor = {};
8123
+ __export(exports_doctor, {
8124
+ main: () => main3
8125
+ });
8126
+ async function main3(options, deps = {}) {
8127
+ const result = await (deps.runDoctor ?? runDoctor)(options);
8128
+ const print = deps.print ?? ((line) => console.log(line));
8129
+ for (const line of result.output) {
8130
+ print(line);
8131
+ }
8132
+ return result.exitCode;
8133
+ }
8134
+ var init_doctor2 = __esm(async () => {
8135
+ await init_doctor();
8136
+ });
8137
+
8138
+ // src/cli.ts
8139
+ init_create_daemon_controller();
8140
+ init_daemon_files();
8141
+ import { homedir as homedir9 } from "node:os";
8142
+ import { sep } from "node:path";
8143
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
7092
8144
 
7093
8145
  // src/daemon/daemon-runtime.ts
8146
+ init_daemon_status();
7094
8147
  import { mkdir as mkdir4, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
7095
8148
  import { dirname as dirname3 } from "node:path";
8149
+
7096
8150
  class DaemonRuntime {
7097
8151
  paths;
7098
8152
  options;
@@ -7140,6 +8194,7 @@ class DaemonRuntime {
7140
8194
  }
7141
8195
 
7142
8196
  // src/cli.ts
8197
+ init_version();
7143
8198
  init_consumer_lock();
7144
8199
  var HELP_LINES = [
7145
8200
  "用法:",
@@ -7148,13 +8203,26 @@ var HELP_LINES = [
7148
8203
  "weacpx run - 前台运行",
7149
8204
  "weacpx start - 后台启动",
7150
8205
  "weacpx status - 查看状态",
7151
- "weacpx stop - 停止服务"
8206
+ "weacpx stop - 停止服务",
8207
+ "weacpx doctor - 运行诊断",
8208
+ "weacpx version - 查看版本"
7152
8209
  ];
7153
8210
  async function runCli(args, deps = {}) {
7154
8211
  const command = args[0];
7155
8212
  const print = deps.print ?? ((line) => console.log(line));
7156
- const controller = deps.controller ?? createDefaultController();
7157
8213
  switch (command) {
8214
+ case "version":
8215
+ case "--version":
8216
+ case "-v":
8217
+ print(readVersion());
8218
+ return 0;
8219
+ case "--help":
8220
+ case "-h": {
8221
+ for (const line of HELP_LINES) {
8222
+ print(line);
8223
+ }
8224
+ return 0;
8225
+ }
7158
8226
  case "login":
7159
8227
  await (deps.login ?? defaultLogin)();
7160
8228
  return 0;
@@ -7164,7 +8232,18 @@ async function runCli(args, deps = {}) {
7164
8232
  case "run":
7165
8233
  await (deps.run ?? defaultRun)();
7166
8234
  return 0;
8235
+ case "doctor": {
8236
+ const parsed = parseDoctorArgs(args.slice(1));
8237
+ if (!parsed.ok) {
8238
+ for (const line of HELP_LINES) {
8239
+ print(line);
8240
+ }
8241
+ return 1;
8242
+ }
8243
+ return await (deps.doctor ?? defaultDoctor)(parsed.options);
8244
+ }
7167
8245
  case "start": {
8246
+ const controller = deps.controller ?? createDefaultController();
7168
8247
  const result = await controller.start();
7169
8248
  if (result.state === "already-running") {
7170
8249
  print("weacpx 已在后台运行");
@@ -7176,6 +8255,7 @@ async function runCli(args, deps = {}) {
7176
8255
  return 0;
7177
8256
  }
7178
8257
  case "status": {
8258
+ const controller = deps.controller ?? createDefaultController();
7179
8259
  const status = await controller.getStatus();
7180
8260
  if (status.state === "indeterminate") {
7181
8261
  print("weacpx 进程仍在运行,但状态元数据缺失");
@@ -7198,6 +8278,7 @@ async function runCli(args, deps = {}) {
7198
8278
  return 0;
7199
8279
  }
7200
8280
  case "stop": {
8281
+ const controller = deps.controller ?? createDefaultController();
7201
8282
  const result = await controller.stop();
7202
8283
  if (result.detail === "not-running") {
7203
8284
  print("weacpx 未运行");
@@ -7214,8 +8295,8 @@ async function runCli(args, deps = {}) {
7214
8295
  }
7215
8296
  }
7216
8297
  async function defaultLogin() {
7217
- const { main: main3 } = await init_login().then(() => exports_login);
7218
- await main3();
8298
+ const { main: main4 } = await init_login().then(() => exports_login);
8299
+ await main4();
7219
8300
  }
7220
8301
  async function defaultLogout() {
7221
8302
  const { logout: logout2 } = await Promise.resolve().then(() => (init_weixin_sdk(), exports_weixin_sdk));
@@ -7232,7 +8313,7 @@ async function defaultRun() {
7232
8313
  const daemonRuntime = new DaemonRuntime(daemonPaths, { pid: process.pid });
7233
8314
  await runConsole2(runtimePaths, {
7234
8315
  buildApp: (paths) => buildApp2(paths, {
7235
- defaultLoggingLevel: resolveCliEntryPath().includes(`${sep}src${sep}`) ? "debug" : "info"
8316
+ defaultLoggingLevel: resolveCliEntryPath2().includes(`${sep}src${sep}`) ? "debug" : "info"
7236
8317
  }),
7237
8318
  loadWeixinSdk: loadWeixinSdk2,
7238
8319
  daemonRuntime,
@@ -7244,27 +8325,66 @@ async function defaultRun() {
7244
8325
  })
7245
8326
  });
7246
8327
  }
8328
+ async function defaultDoctor(options) {
8329
+ const { main: main4 } = await init_doctor2().then(() => exports_doctor);
8330
+ return await main4(options);
8331
+ }
7247
8332
  function createDefaultController() {
7248
8333
  const daemonPaths = resolveDaemonPaths({ home: requireHome() });
7249
8334
  return createDaemonController(daemonPaths, {
7250
8335
  processExecPath: process.execPath,
7251
- cliEntryPath: resolveCliEntryPath(),
8336
+ cliEntryPath: resolveCliEntryPath2(),
7252
8337
  cwd: process.cwd(),
7253
8338
  env: process.env
7254
8339
  });
7255
8340
  }
7256
8341
  function requireHome() {
7257
- const home = process.env.HOME ?? homedir6();
8342
+ const home = process.env.HOME ?? homedir9();
7258
8343
  if (!home) {
7259
8344
  throw new Error("Unable to resolve the current user home directory");
7260
8345
  }
7261
8346
  return home;
7262
8347
  }
7263
- function resolveCliEntryPath() {
8348
+ function resolveCliEntryPath2() {
7264
8349
  if (process.argv[1]) {
7265
8350
  return process.argv[1];
7266
8351
  }
7267
- return fileURLToPath4(import.meta.url);
8352
+ return fileURLToPath5(import.meta.url);
8353
+ }
8354
+ function parseDoctorArgs(args) {
8355
+ const options = {};
8356
+ for (let index = 0;index < args.length; index++) {
8357
+ const arg = args[index];
8358
+ switch (arg) {
8359
+ case "--verbose":
8360
+ options.verbose = true;
8361
+ break;
8362
+ case "--smoke":
8363
+ options.smoke = true;
8364
+ break;
8365
+ case "--agent": {
8366
+ const value = args[index + 1];
8367
+ if (!value || value.startsWith("--")) {
8368
+ return { ok: false };
8369
+ }
8370
+ options.agent = value;
8371
+ index++;
8372
+ break;
8373
+ }
8374
+ case "--workspace": {
8375
+ const value = args[index + 1];
8376
+ if (!value || value.startsWith("--")) {
8377
+ return { ok: false };
8378
+ }
8379
+ options.workspace = value;
8380
+ index++;
8381
+ break;
8382
+ }
8383
+ default:
8384
+ return { ok: false };
8385
+ }
8386
+ }
8387
+ return { ok: true, options };
7268
8388
  }
7269
8389
  if (__require.main == __require.module) {
7270
8390
  process.exitCode = await runCli(process.argv.slice(2));