claude-codex-wechat 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -189,6 +189,38 @@ claude-codex-wechat start
189
189
  - 这是前台进程
190
190
  - npm 本身不负责守护
191
191
  - 长期常驻建议交给进程管理器
192
+ - 如果默认端口已被本程序的后台服务占用,`start` 会先停掉后台服务,再以前台模式启动
193
+ - 如果端口被别的进程占用,`start` 会直接报出占用 PID 和命令,不会强杀陌生进程
194
+
195
+ ### 安装后后台运行
196
+
197
+ 参考 `happier` 的方式,这个仓库现在提供 `service` 命令,把当前 CLI 注册成操作系统服务,而不是在 Node 进程里自己 daemonize。
198
+
199
+ 常用命令:
200
+
201
+ ```bash
202
+ claude-codex-wechat service install
203
+ claude-codex-wechat service start
204
+ claude-codex-wechat service restart
205
+ claude-codex-wechat service status
206
+ claude-codex-wechat service logs
207
+ claude-codex-wechat service tail
208
+ claude-codex-wechat service stop
209
+ claude-codex-wechat service uninstall
210
+ ```
211
+
212
+ 当前支持:
213
+
214
+ - macOS:`launchd`(`~/Library/LaunchAgents/`)
215
+ - Linux:`systemd --user`(`~/.config/systemd/user/`)
216
+
217
+ 推荐安装后直接这样:
218
+
219
+ ```bash
220
+ claude-codex-wechat init
221
+ claude-codex-wechat service install
222
+ claude-codex-wechat service status
223
+ ```
192
224
 
193
225
  ### pm2
194
226
 
@@ -315,6 +347,7 @@ HTTPS_PROXY=http://127.0.0.1:7890 HTTP_PROXY=http://127.0.0.1:7890 ./release.sh
315
347
 
316
348
  这个脚本会:
317
349
 
350
+ - 先检查 `npm whoami`
318
351
  - 先执行 `pnpm typecheck`
319
352
  - 再执行 `pnpm build`
320
353
  - 提交当前改动
@@ -2,8 +2,8 @@
2
2
  import { createRequire as __cjsCreateRequire } from 'node:module'; const require = __cjsCreateRequire(import.meta.url);
3
3
 
4
4
  // src/cli.ts
5
- import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
6
- import { dirname as dirname10, join as join10 } from "node:path";
5
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
6
+ import { dirname as dirname11, join as join11 } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
 
9
9
  // src/daemon/bootstrap.ts
@@ -3989,6 +3989,27 @@ async function resolveProviderCommands(providers) {
3989
3989
  };
3990
3990
  }
3991
3991
 
3992
+ // src/daemon/portGuard.ts
3993
+ import { execFile } from "node:child_process";
3994
+ import { promisify } from "node:util";
3995
+ var execFileAsync = promisify(execFile);
3996
+ async function findListeningProcess(port) {
3997
+ if (process.platform === "win32") return null;
3998
+ try {
3999
+ const { stdout } = await execFileAsync("lsof", ["-n", "-P", `-iTCP:${port}`, "-sTCP:LISTEN"]);
4000
+ const lines = stdout.trim().split(/\r?\n/).filter(Boolean);
4001
+ const record = lines.slice(1)[0];
4002
+ if (!record) return null;
4003
+ const parts = record.trim().split(/\s+/);
4004
+ const command = parts[0] ?? "";
4005
+ const pid = Number(parts[1] ?? "");
4006
+ if (!command || !Number.isFinite(pid)) return null;
4007
+ return { pid, command };
4008
+ } catch {
4009
+ return null;
4010
+ }
4011
+ }
4012
+
3992
4013
  // src/daemon/staticFrontend.ts
3993
4014
  import { readFileSync as readFileSync6 } from "node:fs";
3994
4015
  import { join as join9 } from "node:path";
@@ -4010,9 +4031,318 @@ function attachStaticFrontend(webRoot2) {
4010
4031
  };
4011
4032
  }
4012
4033
 
4034
+ // src/daemon/service.ts
4035
+ import { existsSync as existsSync8, statSync } from "node:fs";
4036
+ import { mkdir as mkdir4, readFile as readFile6, rm as rm2, writeFile as writeFile5 } from "node:fs/promises";
4037
+ import { homedir as homedir7 } from "node:os";
4038
+ import { dirname as dirname10, join as join10 } from "node:path";
4039
+ import { createReadStream } from "node:fs";
4040
+ import { execFile as execFile2, spawn as spawn6 } from "node:child_process";
4041
+ import { createInterface } from "node:readline";
4042
+ import { promisify as promisify2 } from "node:util";
4043
+ var execFileAsync2 = promisify2(execFile2);
4044
+ async function installService(context) {
4045
+ if (process.platform === "darwin") {
4046
+ const spec = buildLaunchdSpec(context);
4047
+ await mkdir4(dirname10(spec.plistPath), { recursive: true });
4048
+ await mkdir4(dirname10(spec.stdoutPath), { recursive: true });
4049
+ await writeFile5(spec.plistPath, renderLaunchdPlist(spec), "utf8");
4050
+ await runLaunchctl(["unload", spec.plistPath]).catch(() => void 0);
4051
+ await runLaunchctl(["load", spec.plistPath]);
4052
+ await runLaunchctl(["kickstart", "-k", `gui/${process.getuid?.() ?? 0}/${spec.label}`]);
4053
+ return await readServiceStatus(context);
4054
+ }
4055
+ if (process.platform === "linux") {
4056
+ const spec = buildSystemdUserSpec(context);
4057
+ await mkdir4(dirname10(spec.unitPath), { recursive: true });
4058
+ await mkdir4(dirname10(spec.stdoutPath), { recursive: true });
4059
+ await writeFile5(spec.unitPath, renderSystemdUnit(spec), "utf8");
4060
+ await runSystemctl(["--user", "daemon-reload"]);
4061
+ await runSystemctl(["--user", "enable", "--now", spec.unitName]);
4062
+ return await readServiceStatus(context);
4063
+ }
4064
+ throw new Error("service_install_not_supported_on_this_platform");
4065
+ }
4066
+ async function startService(context) {
4067
+ if (process.platform === "darwin") {
4068
+ const spec = buildLaunchdSpec(context);
4069
+ await ensureFileExists(spec.plistPath, "service_not_installed");
4070
+ await runLaunchctl(["load", spec.plistPath]).catch(() => void 0);
4071
+ await runLaunchctl(["kickstart", "-k", `gui/${process.getuid?.() ?? 0}/${spec.label}`]);
4072
+ return await readServiceStatus(context);
4073
+ }
4074
+ if (process.platform === "linux") {
4075
+ const spec = buildSystemdUserSpec(context);
4076
+ await ensureFileExists(spec.unitPath, "service_not_installed");
4077
+ await runSystemctl(["--user", "start", spec.unitName]);
4078
+ return await readServiceStatus(context);
4079
+ }
4080
+ throw new Error("service_start_not_supported_on_this_platform");
4081
+ }
4082
+ async function stopService(context) {
4083
+ if (process.platform === "darwin") {
4084
+ const spec = buildLaunchdSpec(context);
4085
+ if (!existsSync8(spec.plistPath)) return await readServiceStatus(context);
4086
+ await runLaunchctl(["bootout", `gui/${process.getuid?.() ?? 0}`, spec.plistPath]).catch(() => void 0);
4087
+ return await readServiceStatus(context);
4088
+ }
4089
+ if (process.platform === "linux") {
4090
+ const spec = buildSystemdUserSpec(context);
4091
+ if (!existsSync8(spec.unitPath)) return await readServiceStatus(context);
4092
+ await runSystemctl(["--user", "stop", spec.unitName]).catch(() => void 0);
4093
+ return await readServiceStatus(context);
4094
+ }
4095
+ throw new Error("service_stop_not_supported_on_this_platform");
4096
+ }
4097
+ async function uninstallService(context) {
4098
+ if (process.platform === "darwin") {
4099
+ const spec = buildLaunchdSpec(context);
4100
+ if (existsSync8(spec.plistPath)) {
4101
+ await runLaunchctl(["bootout", `gui/${process.getuid?.() ?? 0}`, spec.plistPath]).catch(() => void 0);
4102
+ await rm2(spec.plistPath, { force: true });
4103
+ }
4104
+ return await readServiceStatus(context);
4105
+ }
4106
+ if (process.platform === "linux") {
4107
+ const spec = buildSystemdUserSpec(context);
4108
+ if (existsSync8(spec.unitPath)) {
4109
+ await runSystemctl(["--user", "disable", "--now", spec.unitName]).catch(() => void 0);
4110
+ await rm2(spec.unitPath, { force: true });
4111
+ await runSystemctl(["--user", "daemon-reload"]).catch(() => void 0);
4112
+ }
4113
+ return await readServiceStatus(context);
4114
+ }
4115
+ throw new Error("service_uninstall_not_supported_on_this_platform");
4116
+ }
4117
+ async function restartService(context) {
4118
+ if (process.platform === "darwin") {
4119
+ const spec = buildLaunchdSpec(context);
4120
+ await ensureFileExists(spec.plistPath, "service_not_installed");
4121
+ await runLaunchctl(["kickstart", "-k", `gui/${process.getuid?.() ?? 0}/${spec.label}`]);
4122
+ return await readServiceStatus(context);
4123
+ }
4124
+ if (process.platform === "linux") {
4125
+ const spec = buildSystemdUserSpec(context);
4126
+ await ensureFileExists(spec.unitPath, "service_not_installed");
4127
+ await runSystemctl(["--user", "restart", spec.unitName]);
4128
+ return await readServiceStatus(context);
4129
+ }
4130
+ throw new Error("service_restart_not_supported_on_this_platform");
4131
+ }
4132
+ async function readServiceLogs(context, lines = 120) {
4133
+ const status = await readServiceStatus(context);
4134
+ const stdoutTail = await tailFile(status.stdoutPath, lines);
4135
+ const stderrTail = await tailFile(status.stderrPath, lines);
4136
+ return [
4137
+ `[stdout] ${status.stdoutPath}`,
4138
+ stdoutTail || "(empty)",
4139
+ "",
4140
+ `[stderr] ${status.stderrPath}`,
4141
+ stderrTail || "(empty)"
4142
+ ].join("\n");
4143
+ }
4144
+ async function tailServiceLogs(context) {
4145
+ const status = await readServiceStatus(context);
4146
+ await tailFiles([status.stdoutPath, status.stderrPath]);
4147
+ }
4148
+ async function readServiceStatus(context) {
4149
+ if (process.platform === "darwin") {
4150
+ const spec = buildLaunchdSpec(context);
4151
+ const installed = existsSync8(spec.plistPath);
4152
+ const running = installed ? await runLaunchctl(["print", `gui/${process.getuid?.() ?? 0}/${spec.label}`]).then(() => true, () => false) : false;
4153
+ return {
4154
+ manager: "launchd",
4155
+ installed,
4156
+ running,
4157
+ serviceFilePath: spec.plistPath,
4158
+ label: spec.label,
4159
+ stdoutPath: spec.stdoutPath,
4160
+ stderrPath: spec.stderrPath
4161
+ };
4162
+ }
4163
+ if (process.platform === "linux") {
4164
+ const spec = buildSystemdUserSpec(context);
4165
+ const installed = existsSync8(spec.unitPath);
4166
+ const running = installed ? await runSystemctl(["--user", "is-active", "--quiet", spec.unitName]).then(() => true, () => false) : false;
4167
+ return {
4168
+ manager: "systemd-user",
4169
+ installed,
4170
+ running,
4171
+ serviceFilePath: spec.unitPath,
4172
+ label: spec.unitName,
4173
+ stdoutPath: spec.stdoutPath,
4174
+ stderrPath: spec.stderrPath
4175
+ };
4176
+ }
4177
+ throw new Error("service_status_not_supported_on_this_platform");
4178
+ }
4179
+ function buildLaunchdSpec(context) {
4180
+ const stateDir = join10(homedir7(), ".claude-codex-wechat");
4181
+ return {
4182
+ label: "com.claude-codex-wechat",
4183
+ plistPath: join10(homedir7(), "Library", "LaunchAgents", "com.claude-codex-wechat.plist"),
4184
+ programArgs: [context.nodePath ?? process.execPath, context.cliEntrypointPath, "start"],
4185
+ workingDirectory: homedir7(),
4186
+ stdoutPath: join10(stateDir, "logs", "service.stdout.log"),
4187
+ stderrPath: join10(stateDir, "logs", "service.stderr.log"),
4188
+ environment: buildServiceEnvironment(context)
4189
+ };
4190
+ }
4191
+ function buildSystemdUserSpec(context) {
4192
+ const stateDir = join10(homedir7(), ".claude-codex-wechat");
4193
+ return {
4194
+ unitName: "claude-codex-wechat.service",
4195
+ unitPath: join10(homedir7(), ".config", "systemd", "user", "claude-codex-wechat.service"),
4196
+ execStart: [context.nodePath ?? process.execPath, context.cliEntrypointPath, "start"],
4197
+ workingDirectory: homedir7(),
4198
+ stdoutPath: join10(stateDir, "logs", "service.stdout.log"),
4199
+ stderrPath: join10(stateDir, "logs", "service.stderr.log"),
4200
+ environment: buildServiceEnvironment(context)
4201
+ };
4202
+ }
4203
+ function buildServiceEnvironment(context) {
4204
+ const env = {
4205
+ PATH: process.env.PATH ?? "",
4206
+ HOME: process.env.HOME ?? homedir7()
4207
+ };
4208
+ const configPath = context.configPath ?? process.env.BRIDGE_CONFIG;
4209
+ const port = context.port ?? Number(process.env.BRIDGE_PORT ?? 8787);
4210
+ if (configPath) env.BRIDGE_CONFIG = configPath;
4211
+ if (Number.isFinite(port)) env.BRIDGE_PORT = String(port);
4212
+ for (const key of [
4213
+ "BRIDGE_WECHAT_ENABLED",
4214
+ "BRIDGE_WECHAT_BASE_URL",
4215
+ "BRIDGE_WECHAT_TOKEN",
4216
+ "BRIDGE_WECHAT_ACCOUNT_ID",
4217
+ "BRIDGE_CLAUDE_COMMAND",
4218
+ "BRIDGE_CODEX_COMMAND",
4219
+ "HTTP_PROXY",
4220
+ "HTTPS_PROXY",
4221
+ "http_proxy",
4222
+ "https_proxy"
4223
+ ]) {
4224
+ const value = process.env[key];
4225
+ if (value) env[key] = value;
4226
+ }
4227
+ return env;
4228
+ }
4229
+ function renderLaunchdPlist(spec) {
4230
+ const programArgs = spec.programArgs.map((arg) => ` <string>${escapeXml(arg)}</string>`).join("\n");
4231
+ const envEntries = Object.entries(spec.environment).map(([key, value]) => ` <key>${escapeXml(key)}</key>
4232
+ <string>${escapeXml(value)}</string>`).join("\n");
4233
+ return [
4234
+ '<?xml version="1.0" encoding="UTF-8"?>',
4235
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
4236
+ '<plist version="1.0">',
4237
+ "<dict>",
4238
+ " <key>Label</key>",
4239
+ ` <string>${escapeXml(spec.label)}</string>`,
4240
+ " <key>ProgramArguments</key>",
4241
+ " <array>",
4242
+ programArgs,
4243
+ " </array>",
4244
+ " <key>WorkingDirectory</key>",
4245
+ ` <string>${escapeXml(spec.workingDirectory)}</string>`,
4246
+ " <key>EnvironmentVariables</key>",
4247
+ " <dict>",
4248
+ envEntries,
4249
+ " </dict>",
4250
+ " <key>RunAtLoad</key>",
4251
+ " <true/>",
4252
+ " <key>KeepAlive</key>",
4253
+ " <true/>",
4254
+ " <key>StandardOutPath</key>",
4255
+ ` <string>${escapeXml(spec.stdoutPath)}</string>`,
4256
+ " <key>StandardErrorPath</key>",
4257
+ ` <string>${escapeXml(spec.stderrPath)}</string>`,
4258
+ "</dict>",
4259
+ "</plist>",
4260
+ ""
4261
+ ].join("\n");
4262
+ }
4263
+ function renderSystemdUnit(spec) {
4264
+ const envEntries = Object.entries(spec.environment).map(([key, value]) => `Environment=${quoteSystemdEnv(`${key}=${value}`)}`).join("\n");
4265
+ return [
4266
+ "[Unit]",
4267
+ "Description=claude-codex-wechat bridge daemon",
4268
+ "After=network.target",
4269
+ "",
4270
+ "[Service]",
4271
+ "Type=simple",
4272
+ `WorkingDirectory=${spec.workingDirectory}`,
4273
+ `ExecStart=${spec.execStart.map(quoteSystemdArg).join(" ")}`,
4274
+ envEntries,
4275
+ `StandardOutput=append:${spec.stdoutPath}`,
4276
+ `StandardError=append:${spec.stderrPath}`,
4277
+ "Restart=on-failure",
4278
+ "RestartSec=5",
4279
+ "",
4280
+ "[Install]",
4281
+ "WantedBy=default.target",
4282
+ ""
4283
+ ].join("\n");
4284
+ }
4285
+ function escapeXml(value) {
4286
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
4287
+ }
4288
+ function quoteSystemdArg(value) {
4289
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
4290
+ }
4291
+ function quoteSystemdEnv(value) {
4292
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
4293
+ }
4294
+ async function runLaunchctl(args) {
4295
+ await execFileAsync2("launchctl", args);
4296
+ }
4297
+ async function runSystemctl(args) {
4298
+ await execFileAsync2("systemctl", args);
4299
+ }
4300
+ async function ensureFileExists(path, errorCode) {
4301
+ if (!existsSync8(path)) throw new Error(errorCode);
4302
+ await readFile6(path, "utf8");
4303
+ }
4304
+ async function tailFile(path, lines) {
4305
+ if (!existsSync8(path)) return "";
4306
+ const content = await readFile6(path, "utf8");
4307
+ return content.split(/\r?\n/).slice(-lines).join("\n").trim();
4308
+ }
4309
+ async function tailFiles(paths) {
4310
+ const readers = paths.filter((path) => existsSync8(path)).map((path) => {
4311
+ const rl = createInterface({
4312
+ input: createReadStream(path, { encoding: "utf8", start: Math.max(0, fileSizeHint(path) - 8192) }),
4313
+ crlfDelay: Infinity
4314
+ });
4315
+ rl.on("line", (line) => {
4316
+ process.stdout.write(`[${path}] ${line}
4317
+ `);
4318
+ });
4319
+ return rl;
4320
+ });
4321
+ if (process.platform !== "linux" && process.platform !== "darwin") {
4322
+ return;
4323
+ }
4324
+ const child = spawn6("tail", ["-f", ...paths.filter((path) => existsSync8(path))], { stdio: ["ignore", "pipe", "inherit"] });
4325
+ child.stdout.on("data", (chunk) => {
4326
+ process.stdout.write(String(chunk));
4327
+ });
4328
+ await new Promise((resolve, reject) => {
4329
+ child.on("exit", () => resolve());
4330
+ child.on("error", reject);
4331
+ }).finally(() => {
4332
+ for (const reader of readers) reader.close();
4333
+ });
4334
+ }
4335
+ function fileSizeHint(path) {
4336
+ try {
4337
+ return statSync(path).size;
4338
+ } catch {
4339
+ return 0;
4340
+ }
4341
+ }
4342
+
4013
4343
  // src/cli.ts
4014
- var here = dirname10(fileURLToPath(import.meta.url));
4015
- var webRoot = join10(here, "..", "web");
4344
+ var here = dirname11(fileURLToPath(import.meta.url));
4345
+ var webRoot = join11(here, "..", "web");
4016
4346
  async function main() {
4017
4347
  const command = process.argv[2];
4018
4348
  switch (command) {
@@ -4026,6 +4356,9 @@ async function main() {
4026
4356
  case "doctor":
4027
4357
  await cmdDoctor();
4028
4358
  return;
4359
+ case "service":
4360
+ await cmdService(process.argv.slice(3));
4361
+ return;
4029
4362
  case "print-config":
4030
4363
  cmdPrintConfig();
4031
4364
  return;
@@ -4042,21 +4375,36 @@ async function main() {
4042
4375
  }
4043
4376
  }
4044
4377
  async function cmdStart() {
4045
- if (!existsSync8(webRoot)) {
4378
+ if (!existsSync9(webRoot)) {
4046
4379
  console.error(`\u627E\u4E0D\u5230\u524D\u7AEF\u6784\u5EFA\u4EA7\u7269: ${webRoot}`);
4047
4380
  console.error("\u8BF7\u5148\u8FD0\u884C\u6784\u5EFA (pnpm build) \u540E\u518D\u542F\u52A8\uFF0C\u6216\u91CD\u65B0\u5B89\u88C5\u5B8C\u6574\u7684 npm \u5305\u3002");
4048
4381
  process.exitCode = 1;
4049
4382
  return;
4050
4383
  }
4384
+ const context = createServiceContext();
4385
+ const port = context.port ?? 8787;
4386
+ const occupiedBy = await findListeningProcess(port);
4387
+ if (occupiedBy) {
4388
+ const serviceStatus = await readServiceStatus(context).catch(() => null);
4389
+ if (serviceStatus?.installed && serviceStatus.running) {
4390
+ console.log(`\u7AEF\u53E3 ${port} \u5DF2\u88AB\u540E\u53F0\u670D\u52A1\u5360\u7528\uFF0C\u5148\u505C\u6B62\u670D\u52A1\u518D\u4EE5\u524D\u53F0\u6A21\u5F0F\u542F\u52A8\u3002`);
4391
+ await stopService(context);
4392
+ } else {
4393
+ console.error(`\u7AEF\u53E3 ${port} \u5DF2\u88AB\u5360\u7528: PID=${occupiedBy.pid} COMMAND=${occupiedBy.command}`);
4394
+ console.error("\u8BF7\u5148\u505C\u6B62\u5360\u7528\u8FDB\u7A0B\uFF0C\u6216\u6539\u7528 `claude-codex-wechat service stop` \u505C\u6389\u540E\u53F0\u670D\u52A1\u3002");
4395
+ process.exitCode = 1;
4396
+ return;
4397
+ }
4398
+ }
4051
4399
  await startDaemon({
4052
4400
  attachFrontend: attachStaticFrontend(webRoot)
4053
4401
  });
4054
4402
  }
4055
4403
  function cmdInit() {
4056
4404
  const configPath = process.env.BRIDGE_CONFIG ?? defaultConfigPath();
4057
- const dir = dirname10(configPath);
4405
+ const dir = dirname11(configPath);
4058
4406
  mkdirSync6(dir, { recursive: true });
4059
- if (existsSync8(configPath)) {
4407
+ if (existsSync9(configPath)) {
4060
4408
  console.log(`\u914D\u7F6E\u5DF2\u5B58\u5728\uFF0C\u672A\u8986\u76D6: ${configPath}`);
4061
4409
  return;
4062
4410
  }
@@ -4081,13 +4429,13 @@ function cmdInit() {
4081
4429
  async function cmdDoctor() {
4082
4430
  const configPath = process.env.BRIDGE_CONFIG ?? defaultConfigPath();
4083
4431
  console.log("claude-codex-wechat doctor\n");
4084
- report("\u914D\u7F6E\u6587\u4EF6", existsSync8(configPath) ? configPath : `\u7F3A\u5931 (${configPath})\uFF0C\u8FD0\u884C init \u521B\u5EFA`);
4085
- report("\u524D\u7AEF\u4EA7\u7269", existsSync8(webRoot) ? webRoot : `\u7F3A\u5931 (${webRoot})`);
4432
+ report("\u914D\u7F6E\u6587\u4EF6", existsSync9(configPath) ? configPath : `\u7F3A\u5931 (${configPath})\uFF0C\u8FD0\u884C init \u521B\u5EFA`);
4433
+ report("\u524D\u7AEF\u4EA7\u7269", existsSync9(webRoot) ? webRoot : `\u7F3A\u5931 (${webRoot})`);
4086
4434
  const claude = await findExecutable("claude");
4087
4435
  report("claude \u53EF\u6267\u884C", claude ?? "\u672A\u627E\u5230\uFF0C\u9700\u5148\u5B89\u88C5\u5E76\u767B\u5F55 Claude Code");
4088
4436
  const codex = await findExecutable("codex");
4089
4437
  report("codex \u53EF\u6267\u884C", codex ?? "\u672A\u627E\u5230\uFF08\u4EC5\u4F7F\u7528 Claude \u65F6\u53EF\u5FFD\u7565\uFF09");
4090
- if (existsSync8(configPath)) {
4438
+ if (existsSync9(configPath)) {
4091
4439
  const config = loadBridgeConfig(configPath);
4092
4440
  const wechat = config.wechat;
4093
4441
  report("\u5FAE\u4FE1\u542F\u7528", wechat?.enabled ? "\u662F" : "\u5426");
@@ -4097,16 +4445,84 @@ async function cmdDoctor() {
4097
4445
  }
4098
4446
  function cmdPrintConfig() {
4099
4447
  const configPath = process.env.BRIDGE_CONFIG ?? defaultConfigPath();
4100
- if (!existsSync8(configPath)) {
4448
+ if (!existsSync9(configPath)) {
4101
4449
  console.error(`\u914D\u7F6E\u4E0D\u5B58\u5728: ${configPath}\uFF08\u8FD0\u884C init \u521B\u5EFA\uFF09`);
4102
4450
  process.exitCode = 1;
4103
4451
  return;
4104
4452
  }
4105
4453
  console.log(readFileSync7(configPath, "utf8"));
4106
4454
  }
4455
+ async function cmdService(args) {
4456
+ const action = args[0] ?? "status";
4457
+ const context = createServiceContext();
4458
+ switch (action) {
4459
+ case "install": {
4460
+ const status = await installService(context);
4461
+ printServiceStatus("service installed", status);
4462
+ return;
4463
+ }
4464
+ case "start": {
4465
+ const status = await startService(context);
4466
+ printServiceStatus("service started", status);
4467
+ return;
4468
+ }
4469
+ case "stop": {
4470
+ const status = await stopService(context);
4471
+ printServiceStatus("service stopped", status);
4472
+ return;
4473
+ }
4474
+ case "restart": {
4475
+ const status = await restartService(context);
4476
+ printServiceStatus("service restarted", status);
4477
+ return;
4478
+ }
4479
+ case "logs": {
4480
+ console.log(await readServiceLogs(context));
4481
+ return;
4482
+ }
4483
+ case "tail": {
4484
+ await tailServiceLogs(context);
4485
+ return;
4486
+ }
4487
+ case "uninstall": {
4488
+ const status = await uninstallService(context);
4489
+ printServiceStatus("service uninstalled", status);
4490
+ return;
4491
+ }
4492
+ case "status": {
4493
+ const status = await readServiceStatus(context);
4494
+ printServiceStatus("service status", status);
4495
+ return;
4496
+ }
4497
+ default:
4498
+ console.error(`\u672A\u77E5 service \u5B50\u547D\u4EE4: ${action}
4499
+ `);
4500
+ printUsage();
4501
+ process.exitCode = 1;
4502
+ }
4503
+ }
4504
+ function createServiceContext() {
4505
+ return {
4506
+ cliEntrypointPath: fileURLToPath(import.meta.url),
4507
+ nodePath: process.execPath,
4508
+ configPath: process.env.BRIDGE_CONFIG ?? defaultConfigPath(),
4509
+ port: Number(process.env.BRIDGE_PORT ?? 8787)
4510
+ };
4511
+ }
4107
4512
  function report(label, value) {
4108
4513
  console.log(` ${label.padEnd(14)}: ${value}`);
4109
4514
  }
4515
+ function printServiceStatus(title, status) {
4516
+ console.log(`claude-codex-wechat ${title}
4517
+ `);
4518
+ report("service manager", status.manager);
4519
+ report("installed", status.installed ? "yes" : "no");
4520
+ report("running", status.running ? "yes" : "no");
4521
+ report("label", status.label);
4522
+ report("service file", status.serviceFilePath);
4523
+ report("stdout log", status.stdoutPath);
4524
+ report("stderr log", status.stderrPath);
4525
+ }
4110
4526
  function printUsage() {
4111
4527
  console.log(`claude-codex-wechat \u2014 \u672C\u5730 WeChat \u2194 Claude/Codex bridge daemon
4112
4528
 
@@ -4117,6 +4533,7 @@ function printUsage() {
4117
4533
  start \u542F\u52A8 daemon\uFF08\u9ED8\u8BA4\u547D\u4EE4\uFF0C\u524D\u53F0\u8FD0\u884C\uFF09
4118
4534
  init \u5728 ~/.claude-codex-wechat/ \u521B\u5EFA\u9ED8\u8BA4\u914D\u7F6E
4119
4535
  doctor \u68C0\u67E5\u914D\u7F6E\u3001\u524D\u7AEF\u4EA7\u7269\u4E0E claude/codex \u53EF\u6267\u884C\u6587\u4EF6
4536
+ service \u7BA1\u7406\u540E\u53F0\u670D\u52A1\uFF08install/start/stop/restart/status/logs/tail/uninstall\uFF09
4120
4537
  print-config \u6253\u5370\u5F53\u524D\u914D\u7F6E\u6587\u4EF6\u5185\u5BB9
4121
4538
  help \u663E\u793A\u672C\u5E2E\u52A9
4122
4539
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-codex-wechat",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {