dsh-lark-bot 0.3.0 → 0.4.0

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
@@ -1379,6 +1379,10 @@ function resolveAppPaths(root = defaultHome()) {
1379
1379
  workspacesFile: (profile) => profilePath(profile, "workspaces.json"),
1380
1380
  mediaDir: (profile) => profilePath(profile, "media"),
1381
1381
  logsDir: (profile) => profilePath(profile, "logs"),
1382
+ serviceDir: join4(root, "service"),
1383
+ serviceEnvFile: join4(root, "service", "service.env"),
1384
+ serviceMetadataFile: join4(root, "service", "service.json"),
1385
+ serviceLogFile: (profile) => profilePath(profile, "logs", "bot.log"),
1382
1386
  registryFile: join4(root, "registry", "processes.json"),
1383
1387
  locksDir: join4(root, "registry", "locks")
1384
1388
  };
@@ -1517,9 +1521,9 @@ async function writeFileAtomic(target, data, options = {}) {
1517
1521
  }
1518
1522
  }
1519
1523
  async function rmSilently(path) {
1520
- const { rm } = await import("fs/promises");
1524
+ const { rm: rm3 } = await import("fs/promises");
1521
1525
  try {
1522
- await rm(path, { force: true });
1526
+ await rm3(path, { force: true });
1523
1527
  } catch {
1524
1528
  }
1525
1529
  }
@@ -1690,7 +1694,7 @@ async function runDoctor(options) {
1690
1694
  if (critical) process.exitCode = 1;
1691
1695
  }
1692
1696
 
1693
- // src/cli/commands/start.ts
1697
+ // src/cli/commands/run.ts
1694
1698
  import { mkdir as mkdir7 } from "fs/promises";
1695
1699
 
1696
1700
  // src/bot/active-runs.ts
@@ -3488,21 +3492,10 @@ var WorkspaceStore = class {
3488
3492
  }
3489
3493
  };
3490
3494
 
3491
- // src/cli/commands/start.ts
3495
+ // src/cli/commands/run.ts
3492
3496
  var DEBOUNCE_MS = 600;
3493
- async function runStart(options) {
3494
- const env = loadRuntimeEnv({
3495
- ...process.env,
3496
- ...options.workspace ? { DSH_LARK_WORKSPACE: options.workspace } : {},
3497
- ...options.tenant ? { DSH_LARK_TENANT: options.tenant } : {},
3498
- ...options.appId ? { DSH_LARK_APP_ID: options.appId } : {},
3499
- ...options.appSecret ? { DSH_LARK_APP_SECRET: options.appSecret } : {}
3500
- });
3501
- const paths = resolveAppPaths(env.home);
3502
- const profileName = options.profile ?? "default";
3503
- const configStore = new ConfigStore(paths.configFile);
3504
- await configStore.load();
3505
- const accessManager = new AccessManager(configStore, profileName);
3497
+ async function ensureBotProfile(store, options) {
3498
+ const { env, profileName } = options;
3506
3499
  if (env.appId && env.appSecret) {
3507
3500
  const profileInput = {
3508
3501
  tenant: env.tenant,
@@ -3513,31 +3506,61 @@ async function runStart(options) {
3513
3506
  runTimeoutMs: env.runTimeoutMs
3514
3507
  };
3515
3508
  if (env.workspace !== void 0) profileInput.workspace = env.workspace;
3516
- await configStore.saveProfile(profileName, profileInput);
3509
+ await store.saveProfile(profileName, profileInput);
3517
3510
  }
3518
- const profile = configStore.getProfile(profileName);
3519
- if (!profile) {
3520
- try {
3521
- const created = await onboardPersonalAgent();
3522
- const onboardingProfile = {
3523
- tenant: created.tenant,
3524
- appId: created.appId,
3525
- appSecret: created.appSecret,
3526
- model: env.model,
3527
- stopGraceMs: env.stopGraceMs,
3528
- runTimeoutMs: env.runTimeoutMs
3529
- };
3530
- if (created.operatorOpenId !== void 0) {
3531
- onboardingProfile.operatorOpenId = created.operatorOpenId;
3532
- }
3533
- if (env.workspace !== void 0) onboardingProfile.workspace = env.workspace;
3534
- await configStore.saveProfile(profileName, onboardingProfile);
3535
- } catch (error) {
3536
- log.fail("onboarding", error);
3537
- process.stderr.write("\u626B\u7801\u521B\u5EFA\u5E94\u7528\u5931\u8D25\uFF0C\u672A\u5199\u5165\u672C\u5730\u914D\u7F6E\u3002\n");
3538
- process.exitCode = 1;
3539
- return;
3511
+ if (store.getProfile(profileName)) return true;
3512
+ if (!options.allowOnboarding) {
3513
+ process.stderr.write(
3514
+ "\u672A\u68C0\u6D4B\u5230\u98DE\u4E66 / Lark \u5E94\u7528\u51ED\u636E\u3002\u8BF7\u5148\u5728\u7EC8\u7AEF\u8FD0\u884C `dsh-lark-bot start` \u5B8C\u6210\u626B\u7801\u7ED1\u5B9A\u3002\n"
3515
+ );
3516
+ return false;
3517
+ }
3518
+ try {
3519
+ const created = await onboardPersonalAgent();
3520
+ const onboardingProfile = {
3521
+ tenant: created.tenant,
3522
+ appId: created.appId,
3523
+ appSecret: created.appSecret,
3524
+ model: env.model,
3525
+ stopGraceMs: env.stopGraceMs,
3526
+ runTimeoutMs: env.runTimeoutMs
3527
+ };
3528
+ if (created.operatorOpenId !== void 0) {
3529
+ onboardingProfile.operatorOpenId = created.operatorOpenId;
3540
3530
  }
3531
+ if (env.workspace !== void 0) onboardingProfile.workspace = env.workspace;
3532
+ await store.saveProfile(profileName, onboardingProfile);
3533
+ return true;
3534
+ } catch (error) {
3535
+ log.fail("onboarding", error);
3536
+ process.stderr.write("\u626B\u7801\u521B\u5EFA\u5E94\u7528\u5931\u8D25\uFF0C\u672A\u5199\u5165\u672C\u5730\u914D\u7F6E\u3002\n");
3537
+ process.exitCode = 1;
3538
+ return false;
3539
+ }
3540
+ }
3541
+ function mergeStartEnv(options) {
3542
+ return {
3543
+ ...process.env,
3544
+ ...options.workspace ? { DSH_LARK_WORKSPACE: options.workspace } : {},
3545
+ ...options.tenant ? { DSH_LARK_TENANT: options.tenant } : {},
3546
+ ...options.appId ? { DSH_LARK_APP_ID: options.appId } : {},
3547
+ ...options.appSecret ? { DSH_LARK_APP_SECRET: options.appSecret } : {}
3548
+ };
3549
+ }
3550
+ async function runBot(options) {
3551
+ const env = loadRuntimeEnv(mergeStartEnv(options));
3552
+ const paths = resolveAppPaths(env.home);
3553
+ const profileName = options.profile ?? "default";
3554
+ const configStore = new ConfigStore(paths.configFile);
3555
+ await configStore.load();
3556
+ const ready = await ensureBotProfile(configStore, {
3557
+ env,
3558
+ profileName,
3559
+ allowOnboarding: false
3560
+ });
3561
+ if (!ready) {
3562
+ process.exitCode = 1;
3563
+ return;
3541
3564
  }
3542
3565
  const activeProfile = configStore.getProfile(profileName);
3543
3566
  if (!activeProfile) {
@@ -3629,7 +3652,7 @@ async function runStart(options) {
3629
3652
  questions,
3630
3653
  densityStore,
3631
3654
  defaultRunTimeoutMs: activeProfile.preferences.runTimeoutMs ?? env.runTimeoutMs,
3632
- accessManager,
3655
+ accessManager: new AccessManager(configStore, profileName),
3633
3656
  pending,
3634
3657
  defaultWorkspace,
3635
3658
  accessDefaultDeny: env.accessDefaultDeny,
@@ -3672,21 +3695,959 @@ function waitForShutdown() {
3672
3695
  });
3673
3696
  }
3674
3697
 
3698
+ // src/service/manager.ts
3699
+ import { mkdir as mkdir12, readFile as readFile7 } from "fs/promises";
3700
+ import { dirname as dirname6 } from "path";
3701
+
3702
+ // src/service/command.ts
3703
+ import { execFile as execFile2 } from "child_process";
3704
+ import { basename, dirname as dirname4, join as join9 } from "path";
3705
+ import { fileURLToPath } from "url";
3706
+ var runCommand = (command, args, options) => new Promise((resolve5) => {
3707
+ execFile2(
3708
+ command,
3709
+ [...args],
3710
+ {
3711
+ env: options?.env,
3712
+ maxBuffer: 16 * 1024 * 1024,
3713
+ windowsHide: true
3714
+ },
3715
+ (error, stdout, stderr) => {
3716
+ let code = 0;
3717
+ if (error) {
3718
+ const errorCode = error.code;
3719
+ code = typeof errorCode === "number" ? errorCode : 1;
3720
+ }
3721
+ resolve5({ code, stdout: stdout ?? "", stderr: stderr ?? "" });
3722
+ }
3723
+ );
3724
+ });
3725
+ function resolveCliJsPath(metaUrl = import.meta.url) {
3726
+ const current = fileURLToPath(metaUrl);
3727
+ const name = basename(current);
3728
+ if (name === "cli.js" || name === "cli.mjs" || name === "cli.cjs") return current;
3729
+ return join9(dirname4(current), "..", "dist", "cli.js");
3730
+ }
3731
+ function slugify2(value) {
3732
+ const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3733
+ return slug || "default";
3734
+ }
3735
+ function serviceNameFor(profile) {
3736
+ return profile === "default" ? "dsh-lark-bot" : `dsh-lark-bot-${slugify2(profile)}`;
3737
+ }
3738
+ function launchdLabelFor(profile) {
3739
+ return profile === "default" ? "io.dsh-lark-bot" : `io.dsh-lark-bot.${slugify2(profile)}`;
3740
+ }
3741
+
3742
+ // src/service/env-snapshot.ts
3743
+ import { readFile as readFile5 } from "fs/promises";
3744
+ var EXTRA_KEYS = ["PATH", "HOME", "DEEPSEEK_API_KEY", "DSH_HOME"];
3745
+ function snapshotServiceEnv(source = process.env) {
3746
+ const env = {};
3747
+ for (const [key, value] of Object.entries(source)) {
3748
+ if (value === void 0) continue;
3749
+ if (key.startsWith("DSH_LARK_") || EXTRA_KEYS.includes(key)) {
3750
+ env[key] = value;
3751
+ }
3752
+ }
3753
+ return env;
3754
+ }
3755
+ function formatEnvFile(env) {
3756
+ const lines = [];
3757
+ for (const [key, value] of Object.entries(env)) {
3758
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
3759
+ throw new Error(`invalid service env key: ${key}`);
3760
+ }
3761
+ if (/[\r\n]/.test(value)) {
3762
+ throw new Error(`service env value for ${key} contains a newline`);
3763
+ }
3764
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
3765
+ lines.push(`${key}="${escaped}"`);
3766
+ }
3767
+ return `${lines.join("\n")}
3768
+ `;
3769
+ }
3770
+ function parseEnvFile(text) {
3771
+ const env = {};
3772
+ for (const rawLine of text.split("\n")) {
3773
+ const line = rawLine.trim();
3774
+ if (!line || line.startsWith("#") || line.startsWith(";")) continue;
3775
+ const eq = line.indexOf("=");
3776
+ if (eq <= 0) continue;
3777
+ const key = line.slice(0, eq).trim();
3778
+ let value = line.slice(eq + 1).trim();
3779
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
3780
+ value = value.slice(1, -1).replace(/\\(.)/g, "$1");
3781
+ } else if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
3782
+ value = value.slice(1, -1);
3783
+ }
3784
+ env[key] = value;
3785
+ }
3786
+ return env;
3787
+ }
3788
+ async function writeServiceEnv(file, env) {
3789
+ await writeFileAtomic(file, formatEnvFile(env), { mode: 384 });
3790
+ }
3791
+ async function readServiceEnv(file) {
3792
+ try {
3793
+ return parseEnvFile(await readFile5(file, "utf8"));
3794
+ } catch (error) {
3795
+ if (error.code === "ENOENT") return {};
3796
+ throw error;
3797
+ }
3798
+ }
3799
+
3800
+ // src/service/linux-systemd.ts
3801
+ import { mkdir as mkdir8, stat as stat3 } from "fs/promises";
3802
+ import { homedir as homedir5 } from "os";
3803
+ import { join as join10 } from "path";
3804
+ function defaultSystemdUnitDir() {
3805
+ return join10(homedir5(), ".config", "systemd", "user");
3806
+ }
3807
+ function quoteSystemdArg(arg) {
3808
+ if (!/[\s"\\]/.test(arg)) return arg;
3809
+ return `"${arg.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
3810
+ }
3811
+ function buildSystemdUnit(spec) {
3812
+ const exec = [spec.nodePath, spec.cliJsPath, "run", "--profile", spec.profile].map(quoteSystemdArg).join(" ");
3813
+ return [
3814
+ "[Unit]",
3815
+ "Description=dsh-lark-bot bridge service",
3816
+ "After=network-online.target",
3817
+ "Wants=network-online.target",
3818
+ "",
3819
+ "[Service]",
3820
+ "Type=simple",
3821
+ `EnvironmentFile=${quoteSystemdArg(spec.envFile)}`,
3822
+ `ExecStart=${exec}`,
3823
+ "Restart=always",
3824
+ "RestartSec=5",
3825
+ "KillMode=mixed",
3826
+ "TimeoutStopSec=30",
3827
+ `StandardOutput=append:${spec.logFile}`,
3828
+ `StandardError=append:${spec.logFile}`,
3829
+ "",
3830
+ "[Install]",
3831
+ "WantedBy=default.target",
3832
+ ""
3833
+ ].join("\n");
3834
+ }
3835
+ async function isSystemdUserAvailable(runner = runCommand) {
3836
+ const result = await runner("systemctl", ["--user", "show-environment"]);
3837
+ return result.code === 0;
3838
+ }
3839
+ async function fileExists(path) {
3840
+ try {
3841
+ return (await stat3(path)).isFile();
3842
+ } catch {
3843
+ return false;
3844
+ }
3845
+ }
3846
+ function parseMainPid(value) {
3847
+ const raw = value?.trim();
3848
+ if (!raw || raw === "0" || raw === "4294967295") return void 0;
3849
+ const pid = Number(raw);
3850
+ return Number.isInteger(pid) && pid > 0 ? pid : void 0;
3851
+ }
3852
+ function parseNumber(value) {
3853
+ const raw = value?.trim();
3854
+ if (!raw) return void 0;
3855
+ const parsed = Number(raw);
3856
+ return Number.isInteger(parsed) ? parsed : void 0;
3857
+ }
3858
+ var SystemdServiceController = class {
3859
+ platform = "linux-systemd";
3860
+ run;
3861
+ unitDir;
3862
+ constructor(options = {}) {
3863
+ this.run = options.runCommand ?? runCommand;
3864
+ this.unitDir = options.userUnitDir ?? defaultSystemdUnitDir();
3865
+ }
3866
+ unitPath(spec) {
3867
+ return join10(this.unitDir, `${spec.serviceName}.service`);
3868
+ }
3869
+ unitName(spec) {
3870
+ return `${spec.serviceName}.service`;
3871
+ }
3872
+ async isActive(name) {
3873
+ const result = await this.run("systemctl", ["--user", "is-active", name]);
3874
+ return result.code === 0;
3875
+ }
3876
+ async installAndStart(spec) {
3877
+ await mkdir8(this.unitDir, { recursive: true });
3878
+ await writeFileAtomic(this.unitPath(spec), buildSystemdUnit(spec));
3879
+ await this.run("systemctl", ["--user", "daemon-reload"]);
3880
+ const name = this.unitName(spec);
3881
+ if (await this.isActive(name)) {
3882
+ await this.run("systemctl", ["--user", "restart", name]);
3883
+ } else {
3884
+ await this.run("systemctl", ["--user", "enable", "--now", name]);
3885
+ }
3886
+ }
3887
+ async stop(spec) {
3888
+ await this.run("systemctl", ["--user", "disable", "--now", this.unitName(spec)]);
3889
+ }
3890
+ async restart(spec) {
3891
+ await this.run("systemctl", ["--user", "daemon-reload"]);
3892
+ await this.run("systemctl", ["--user", "restart", this.unitName(spec)]);
3893
+ }
3894
+ async status(spec) {
3895
+ const name = this.unitName(spec);
3896
+ const unitFile = this.unitPath(spec);
3897
+ const installed = await fileExists(unitFile);
3898
+ let autostartEnabled = false;
3899
+ if (installed) {
3900
+ const enabled = await this.run("systemctl", ["--user", "is-enabled", name]);
3901
+ autostartEnabled = enabled.code === 0 && enabled.stdout.trim() === "enabled";
3902
+ }
3903
+ const active = await this.run("systemctl", ["--user", "is-active", name]);
3904
+ const isActive = active.code === 0;
3905
+ let detail = active.stdout.trim() || (isActive ? "active" : "inactive");
3906
+ let pid;
3907
+ let restarts;
3908
+ if (isActive || installed) {
3909
+ const show = await this.run(
3910
+ "systemctl",
3911
+ ["--user", "show", name, "-p", "MainPID", "-p", "ActiveState", "-p", "NRestarts", "--value"]
3912
+ );
3913
+ const [activeState, mainPid, nRestarts] = show.stdout.trim().split("\n");
3914
+ if (activeState?.trim()) detail = activeState.trim();
3915
+ pid = parseMainPid(mainPid);
3916
+ restarts = parseNumber(nRestarts);
3917
+ }
3918
+ let state;
3919
+ if (!installed) {
3920
+ state = "stopped";
3921
+ } else if (detail === "failed") {
3922
+ state = "error";
3923
+ } else if (isActive || detail === "activating" || detail === "deactivating" || detail === "reloading") {
3924
+ state = "running";
3925
+ } else {
3926
+ state = "stopped";
3927
+ }
3928
+ return {
3929
+ name: spec.serviceName,
3930
+ platform: this.platform,
3931
+ installed,
3932
+ autostartEnabled,
3933
+ state,
3934
+ pid,
3935
+ detail,
3936
+ restarts
3937
+ };
3938
+ }
3939
+ };
3940
+
3941
+ // src/service/macos-launchd.ts
3942
+ import { mkdir as mkdir9, rm, stat as stat4 } from "fs/promises";
3943
+ import { homedir as homedir6 } from "os";
3944
+ import { join as join11 } from "path";
3945
+ function defaultLaunchAgentDir() {
3946
+ return join11(homedir6(), "Library", "LaunchAgents");
3947
+ }
3948
+ function xmlEscape(value) {
3949
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3950
+ }
3951
+ function buildLaunchdPlist(spec) {
3952
+ const envLines = Object.entries(spec.env).filter(([, value]) => value !== void 0).map(
3953
+ ([key, value]) => ` <key>${xmlEscape(key)}</key>
3954
+ <string>${xmlEscape(value)}</string>`
3955
+ ).join("\n");
3956
+ return [
3957
+ '<?xml version="1.0" encoding="UTF-8"?>',
3958
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
3959
+ '<plist version="1.0">',
3960
+ "<dict>",
3961
+ " <key>Label</key>",
3962
+ ` <string>${xmlEscape(spec.label)}</string>`,
3963
+ " <key>ProgramArguments</key>",
3964
+ " <array>",
3965
+ ` <string>${xmlEscape(spec.nodePath)}</string>`,
3966
+ ` <string>${xmlEscape(spec.cliJsPath)}</string>`,
3967
+ " <string>run</string>",
3968
+ " <string>--profile</string>",
3969
+ ` <string>${xmlEscape(spec.profile)}</string>`,
3970
+ " </array>",
3971
+ " <key>RunAtLoad</key>",
3972
+ " <true/>",
3973
+ " <key>KeepAlive</key>",
3974
+ " <true/>",
3975
+ " <key>EnvironmentVariables</key>",
3976
+ " <dict>",
3977
+ envLines,
3978
+ " </dict>",
3979
+ " <key>StandardOutPath</key>",
3980
+ ` <string>${xmlEscape(spec.logFile)}</string>`,
3981
+ " <key>StandardErrorPath</key>",
3982
+ ` <string>${xmlEscape(spec.logFile)}</string>`,
3983
+ " <key>ProcessType</key>",
3984
+ " <string>Background</string>",
3985
+ "</dict>",
3986
+ "</plist>",
3987
+ ""
3988
+ ].join("\n");
3989
+ }
3990
+ async function fileExists2(path) {
3991
+ try {
3992
+ return (await stat4(path)).isFile();
3993
+ } catch {
3994
+ return false;
3995
+ }
3996
+ }
3997
+ var LaunchdServiceController = class {
3998
+ platform = "darwin-launchd";
3999
+ run;
4000
+ agentDir;
4001
+ uid;
4002
+ constructor(options = {}) {
4003
+ this.run = options.runCommand ?? runCommand;
4004
+ this.agentDir = options.launchAgentDir ?? defaultLaunchAgentDir();
4005
+ this.uid = options.uid ?? process.getuid?.() ?? -1;
4006
+ }
4007
+ domain() {
4008
+ return `gui/${this.uid}`;
4009
+ }
4010
+ plistPath(spec) {
4011
+ return join11(this.agentDir, `${spec.label}.plist`);
4012
+ }
4013
+ serviceRef(spec) {
4014
+ return `${this.domain()}/${spec.label}`;
4015
+ }
4016
+ async installAndStart(spec) {
4017
+ await mkdir9(this.agentDir, { recursive: true });
4018
+ await writeFileAtomic(this.plistPath(spec), buildLaunchdPlist(spec));
4019
+ const bootstrap = await this.run("launchctl", ["bootstrap", this.domain(), this.plistPath(spec)]);
4020
+ if (bootstrap.code !== 0) {
4021
+ await this.run("launchctl", ["kickstart", "-k", this.serviceRef(spec)]);
4022
+ }
4023
+ }
4024
+ async stop(spec) {
4025
+ await this.run("launchctl", ["bootout", this.serviceRef(spec)]);
4026
+ await rm(this.plistPath(spec), { force: true });
4027
+ }
4028
+ async restart(spec) {
4029
+ const kickstart = await this.run("launchctl", ["kickstart", "-k", this.serviceRef(spec)]);
4030
+ if (kickstart.code !== 0) {
4031
+ await this.installAndStart(spec);
4032
+ }
4033
+ }
4034
+ async status(spec) {
4035
+ const plistExists = await fileExists2(this.plistPath(spec));
4036
+ const printed = await this.run("launchctl", ["print", this.serviceRef(spec)]);
4037
+ const loaded = printed.code === 0;
4038
+ let pid;
4039
+ let state = "stopped";
4040
+ let detail = loaded ? "loaded" : "not loaded";
4041
+ if (loaded) {
4042
+ const pidMatch = /pid = (\d+)/.exec(printed.stdout);
4043
+ if (pidMatch?.[1]) pid = Number(pidMatch[1]);
4044
+ if (/state = running/.test(printed.stdout)) {
4045
+ state = "running";
4046
+ detail = "running";
4047
+ } else if (/state = exited|state = waiting/.test(printed.stdout)) {
4048
+ state = "stopped";
4049
+ detail = /state = exited/.test(printed.stdout) ? "exited" : "waiting";
4050
+ }
4051
+ }
4052
+ return {
4053
+ name: spec.serviceName,
4054
+ platform: this.platform,
4055
+ installed: plistExists,
4056
+ autostartEnabled: loaded,
4057
+ state,
4058
+ pid,
4059
+ detail,
4060
+ restarts: void 0
4061
+ };
4062
+ }
4063
+ };
4064
+
4065
+ // src/service/portable.ts
4066
+ import { spawn as spawn5 } from "child_process";
4067
+ import { mkdir as mkdir10, readFile as readFile6, rm as rm2, stat as stat5, writeFile as writeFile4 } from "fs/promises";
4068
+ import { homedir as homedir7 } from "os";
4069
+ import { join as join12 } from "path";
4070
+ function defaultAutostartDir() {
4071
+ return join12(homedir7(), ".config", "autostart");
4072
+ }
4073
+ function quoteDesktopArg(arg) {
4074
+ const escaped = arg.replace(/%/g, "%%").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
4075
+ return /[\s"\\]/.test(arg) ? `"${escaped}"` : escaped;
4076
+ }
4077
+ function buildDesktopEntry(spec) {
4078
+ const exec = [spec.nodePath, spec.cliJsPath, "supervise", "--profile", spec.profile].map(quoteDesktopArg).join(" ");
4079
+ return [
4080
+ "[Desktop Entry]",
4081
+ "Type=Application",
4082
+ `Name=${spec.serviceName}`,
4083
+ "Comment=Keep dsh-lark-bot running in the background",
4084
+ `Exec=${exec}`,
4085
+ "Terminal=false",
4086
+ "X-GNOME-Autostart-enabled=true",
4087
+ "Hidden=false",
4088
+ ""
4089
+ ].join("\n");
4090
+ }
4091
+ function supervisorStatusFile(serviceDir, serviceName) {
4092
+ return join12(serviceDir, `supervisor-${serviceName}.json`);
4093
+ }
4094
+ function isProcessAlive(pid) {
4095
+ try {
4096
+ process.kill(pid, 0);
4097
+ return true;
4098
+ } catch (error) {
4099
+ return error.code === "EPERM";
4100
+ }
4101
+ }
4102
+ async function readSupervisorStatus(file) {
4103
+ try {
4104
+ return JSON.parse(await readFile6(file, "utf8"));
4105
+ } catch {
4106
+ return void 0;
4107
+ }
4108
+ }
4109
+ function defaultSpawnDetached(nodePath, args) {
4110
+ const child = spawn5(nodePath, [...args], {
4111
+ detached: true,
4112
+ stdio: "ignore",
4113
+ env: process.env
4114
+ });
4115
+ child.unref();
4116
+ return child;
4117
+ }
4118
+ var PortableServiceController = class {
4119
+ platform = "linux-portable";
4120
+ autostartDir;
4121
+ serviceDir;
4122
+ spawnDetached;
4123
+ constructor(options = {}) {
4124
+ this.autostartDir = options.autostartDir ?? defaultAutostartDir();
4125
+ this.serviceDir = options.serviceDir ?? "";
4126
+ this.spawnDetached = options.spawnDetached ?? defaultSpawnDetached;
4127
+ }
4128
+ desktopPath(spec) {
4129
+ return join12(this.autostartDir, `${spec.serviceName}.desktop`);
4130
+ }
4131
+ statusFile(spec) {
4132
+ return supervisorStatusFile(this.serviceDir, spec.serviceName);
4133
+ }
4134
+ async sendStopSignal(spec) {
4135
+ const status = await readSupervisorStatus(this.statusFile(spec));
4136
+ if (!status) return;
4137
+ try {
4138
+ process.kill(status.pid, "SIGTERM");
4139
+ } catch {
4140
+ return;
4141
+ }
4142
+ for (let attempt = 0; attempt < 20; attempt += 1) {
4143
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 250));
4144
+ if (!isProcessAlive(status.pid)) return;
4145
+ }
4146
+ try {
4147
+ process.kill(status.pid, "SIGKILL");
4148
+ } catch {
4149
+ }
4150
+ }
4151
+ async installAndStart(spec) {
4152
+ await this.sendStopSignal(spec);
4153
+ await mkdir10(this.autostartDir, { recursive: true });
4154
+ await writeFile4(this.desktopPath(spec), buildDesktopEntry(spec), { encoding: "utf8", mode: 420 });
4155
+ if (!this.serviceDir) throw new Error("serviceDir is required for the portable supervisor");
4156
+ await mkdir10(this.serviceDir, { recursive: true });
4157
+ this.spawnDetached(spec.nodePath, [spec.cliJsPath, "supervise", "--profile", spec.profile]);
4158
+ }
4159
+ async stop(spec) {
4160
+ await this.sendStopSignal(spec);
4161
+ await rm2(this.desktopPath(spec), { force: true });
4162
+ }
4163
+ async restart(spec) {
4164
+ await this.stop(spec);
4165
+ await this.installAndStart(spec);
4166
+ }
4167
+ async status(spec) {
4168
+ let desktopExists = false;
4169
+ try {
4170
+ desktopExists = (await stat5(this.desktopPath(spec))).isFile();
4171
+ } catch {
4172
+ desktopExists = false;
4173
+ }
4174
+ const status = await readSupervisorStatus(this.statusFile(spec));
4175
+ const alive = status !== void 0 && isProcessAlive(status.pid);
4176
+ return {
4177
+ name: spec.serviceName,
4178
+ platform: this.platform,
4179
+ installed: desktopExists,
4180
+ autostartEnabled: desktopExists,
4181
+ state: alive ? "running" : "stopped",
4182
+ pid: status?.childPid,
4183
+ detail: alive ? `supervisor ${status?.pid ?? "?"}` : "stopped",
4184
+ restarts: status?.restarts
4185
+ };
4186
+ }
4187
+ };
4188
+
4189
+ // src/service/windows-task.ts
4190
+ import { mkdir as mkdir11, writeFile as writeFile5 } from "fs/promises";
4191
+ import { dirname as dirname5, join as join13 } from "path";
4192
+ function psQuote(value) {
4193
+ return `'${value.replace(/'/g, "''")}'`;
4194
+ }
4195
+ function buildScheduledTaskScript(action, spec) {
4196
+ const vars = [
4197
+ `$taskName = ${psQuote(spec.serviceName)}`,
4198
+ `$node = ${psQuote(spec.nodePath)}`,
4199
+ `$cli = ${psQuote(spec.cliJsPath)}`,
4200
+ `$log = ${psQuote(spec.logFile)}`,
4201
+ `$profile = ${psQuote(spec.profile)}`,
4202
+ `$workDir = ${psQuote(dirname5(spec.cliJsPath))}`
4203
+ ].join("\n");
4204
+ const install = [
4205
+ `$inner = '"' + $node + '" "' + $cli + '" run --profile "' + $profile + '" >> "' + $log + '" 2>&1'`,
4206
+ `$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument ('/c ' + $inner) -WorkingDirectory $workDir`,
4207
+ `$trigger = New-ScheduledTaskTrigger -AtLogOn`,
4208
+ `$settings = New-ScheduledTaskSettingsSet -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries`,
4209
+ `Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue`,
4210
+ `Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Settings $settings -Force`,
4211
+ `Enable-ScheduledTask -TaskName $taskName`,
4212
+ `Start-ScheduledTask -TaskName $taskName`
4213
+ ].join("\n");
4214
+ const stop = [
4215
+ `Stop-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue`,
4216
+ `Disable-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue`
4217
+ ].join("\n");
4218
+ const restart = [
4219
+ `Stop-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue`,
4220
+ `Start-ScheduledTask -TaskName $taskName`
4221
+ ].join("\n");
4222
+ const status = [
4223
+ `$task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue`,
4224
+ `if ($null -eq $task) { Write-Output 'STATE:missing'; Write-Output 'ENABLED:false'; exit 2 }`,
4225
+ `$info = Get-ScheduledTaskInfo -TaskName $taskName`,
4226
+ `Write-Output ('STATE:' + $task.State)`,
4227
+ `Write-Output ('ENABLED:' + $task.Settings.Enabled)`,
4228
+ `Write-Output ('LAST_RESULT:' + $info.LastTaskResult)`,
4229
+ `Write-Output ('LAST_RUN:' + $info.LastRunTime)`
4230
+ ].join("\n");
4231
+ const body = {
4232
+ install,
4233
+ stop,
4234
+ restart,
4235
+ status
4236
+ };
4237
+ return [
4238
+ `$ErrorActionPreference = 'Stop'`,
4239
+ vars,
4240
+ body[action],
4241
+ ""
4242
+ ].join("\n");
4243
+ }
4244
+ function parseTaskStatusOutput(output) {
4245
+ const fields = /* @__PURE__ */ new Map();
4246
+ for (const rawLine of output.split(/\r?\n/)) {
4247
+ const line = rawLine.trim();
4248
+ const eq = line.indexOf(":");
4249
+ if (eq <= 0) continue;
4250
+ fields.set(line.slice(0, eq).trim(), line.slice(eq + 1).trim());
4251
+ }
4252
+ const stateValue = fields.get("STATE") ?? "missing";
4253
+ const enabled = fields.get("ENABLED") === "True";
4254
+ const installed = stateValue !== "missing";
4255
+ let state = "unknown";
4256
+ if (stateValue === "Running") state = "running";
4257
+ else if (stateValue === "Ready" || stateValue === "Disabled") state = "stopped";
4258
+ else if (stateValue === "Unknown") state = "unknown";
4259
+ else if (stateValue === "missing") state = "stopped";
4260
+ const lastResultRaw = fields.get("LAST_RESULT");
4261
+ const lastResult = lastResultRaw !== void 0 ? Number.parseInt(lastResultRaw, 10) : void 0;
4262
+ const lastResultValue = lastResult !== void 0 && Number.isFinite(lastResult) ? lastResult : void 0;
4263
+ if (installed && lastResultValue !== void 0 && lastResultValue !== 0 && stateValue === "Ready") {
4264
+ state = "error";
4265
+ }
4266
+ return {
4267
+ installed,
4268
+ autostartEnabled: enabled,
4269
+ state,
4270
+ detail: stateValue,
4271
+ lastResult: lastResultValue
4272
+ };
4273
+ }
4274
+ var WindowsTaskServiceController = class {
4275
+ platform = "win32-task";
4276
+ run;
4277
+ scriptDir;
4278
+ constructor(options = {}) {
4279
+ this.run = options.runCommand ?? runCommand;
4280
+ this.scriptDir = options.scriptDir ?? "";
4281
+ }
4282
+ scriptPath(spec, action) {
4283
+ return join13(this.scriptDir, `${action}-${spec.serviceName}.ps1`);
4284
+ }
4285
+ async runScript(spec, action) {
4286
+ const script = this.scriptPath(spec, action);
4287
+ if (this.scriptDir) await mkdir11(this.scriptDir, { recursive: true });
4288
+ await writeFile5(script, buildScheduledTaskScript(action, spec), { encoding: "utf8" });
4289
+ const result = await this.run("powershell.exe", [
4290
+ "-NoProfile",
4291
+ "-NonInteractive",
4292
+ "-ExecutionPolicy",
4293
+ "Bypass",
4294
+ "-File",
4295
+ script
4296
+ ]);
4297
+ return { result, script };
4298
+ }
4299
+ async installAndStart(spec) {
4300
+ const { result } = await this.runScript(spec, "install");
4301
+ if (result.code !== 0) {
4302
+ throw new Error(
4303
+ `\u65E0\u6CD5\u6CE8\u518C Windows \u8BA1\u5212\u4EFB\u52A1\uFF08exit ${result.code}\uFF09\uFF1A${result.stderr.trim() || result.stdout.trim() || "\u672A\u77E5\u9519\u8BEF"}`
4304
+ );
4305
+ }
4306
+ }
4307
+ async stop(spec) {
4308
+ const { result } = await this.runScript(spec, "stop");
4309
+ if (result.code !== 0) {
4310
+ throw new Error(
4311
+ `\u65E0\u6CD5\u505C\u6B62 Windows \u8BA1\u5212\u4EFB\u52A1\uFF08exit ${result.code}\uFF09\uFF1A${result.stderr.trim() || result.stdout.trim() || "\u672A\u77E5\u9519\u8BEF"}`
4312
+ );
4313
+ }
4314
+ }
4315
+ async restart(spec) {
4316
+ const { result } = await this.runScript(spec, "restart");
4317
+ if (result.code !== 0) {
4318
+ throw new Error(
4319
+ `\u65E0\u6CD5\u91CD\u542F Windows \u8BA1\u5212\u4EFB\u52A1\uFF08exit ${result.code}\uFF09\uFF1A${result.stderr.trim() || result.stdout.trim() || "\u672A\u77E5\u9519\u8BEF"}`
4320
+ );
4321
+ }
4322
+ }
4323
+ async status(spec) {
4324
+ const { result } = await this.runScript(spec, "status");
4325
+ const parsed = parseTaskStatusOutput(result.stdout);
4326
+ if (result.code !== 0 && result.code !== 2) {
4327
+ throw new Error(
4328
+ `\u65E0\u6CD5\u67E5\u8BE2 Windows \u8BA1\u5212\u4EFB\u52A1\uFF08exit ${result.code}\uFF09\uFF1A${result.stderr.trim() || result.stdout.trim() || "\u672A\u77E5\u9519\u8BEF"}`
4329
+ );
4330
+ }
4331
+ return {
4332
+ name: spec.serviceName,
4333
+ platform: this.platform,
4334
+ installed: parsed.installed,
4335
+ autostartEnabled: parsed.autostartEnabled,
4336
+ state: parsed.state,
4337
+ pid: void 0,
4338
+ detail: parsed.detail,
4339
+ restarts: void 0
4340
+ };
4341
+ }
4342
+ };
4343
+
4344
+ // src/service/manager.ts
4345
+ var ServiceManager = class {
4346
+ profile;
4347
+ sourceEnv;
4348
+ paths;
4349
+ version;
4350
+ controllerOverride;
4351
+ constructor(deps = {}) {
4352
+ this.profile = deps.profile ?? "default";
4353
+ this.sourceEnv = deps.env ?? process.env;
4354
+ this.paths = deps.paths ?? resolveAppPaths();
4355
+ this.version = deps.version ?? "0.0.0";
4356
+ this.controllerOverride = deps.controller;
4357
+ }
4358
+ buildSpec() {
4359
+ const serviceName = serviceNameFor(this.profile);
4360
+ return {
4361
+ serviceName,
4362
+ label: launchdLabelFor(this.profile),
4363
+ profile: this.profile,
4364
+ nodePath: process.execPath,
4365
+ cliJsPath: resolveCliJsPath(),
4366
+ envFile: this.paths.serviceEnvFile,
4367
+ logFile: this.paths.serviceLogFile(this.profile),
4368
+ env: snapshotServiceEnv(this.sourceEnv)
4369
+ };
4370
+ }
4371
+ async resolveController() {
4372
+ if (this.controllerOverride) return this.controllerOverride;
4373
+ if (process.platform === "darwin") return new LaunchdServiceController();
4374
+ if (process.platform === "win32") return new WindowsTaskServiceController({
4375
+ scriptDir: this.paths.serviceDir
4376
+ });
4377
+ const systemd = new SystemdServiceController();
4378
+ if (await isSystemdUserAvailable()) return systemd;
4379
+ return new PortableServiceController({ serviceDir: this.paths.serviceDir });
4380
+ }
4381
+ async saveMetadata(platform) {
4382
+ const metadata = {
4383
+ schemaVersion: 1,
4384
+ serviceName: this.buildSpec().serviceName,
4385
+ profile: this.profile,
4386
+ platform,
4387
+ version: this.version,
4388
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
4389
+ };
4390
+ await writeFileAtomic(this.paths.serviceMetadataFile, `${JSON.stringify(metadata, null, 2)}
4391
+ `);
4392
+ }
4393
+ async readMetadata() {
4394
+ try {
4395
+ return JSON.parse(await readFile7(this.paths.serviceMetadataFile, "utf8"));
4396
+ } catch {
4397
+ return void 0;
4398
+ }
4399
+ }
4400
+ async prepareFiles(spec) {
4401
+ await mkdir12(dirname6(spec.logFile), { recursive: true });
4402
+ await mkdir12(dirname6(spec.envFile), { recursive: true });
4403
+ await writeServiceEnv(spec.envFile, spec.env);
4404
+ }
4405
+ async start() {
4406
+ const controller = await this.resolveController();
4407
+ const spec = this.buildSpec();
4408
+ await this.prepareFiles(spec);
4409
+ await controller.installAndStart(spec);
4410
+ await this.saveMetadata(controller.platform);
4411
+ return controller.status(spec);
4412
+ }
4413
+ async stop() {
4414
+ const controller = await this.resolveController();
4415
+ const spec = this.buildSpec();
4416
+ await controller.stop(spec);
4417
+ return controller.status(spec);
4418
+ }
4419
+ async restart() {
4420
+ const controller = await this.resolveController();
4421
+ const spec = this.buildSpec();
4422
+ const before = await controller.status(spec);
4423
+ if (!before.installed) {
4424
+ throw new Error("\u540E\u53F0\u670D\u52A1\u5C1A\u672A\u5B89\u88C5\uFF0C\u8BF7\u5148\u8FD0\u884C dsh-lark-bot start");
4425
+ }
4426
+ await this.prepareFiles(spec);
4427
+ await controller.restart(spec);
4428
+ await this.saveMetadata(controller.platform);
4429
+ return controller.status(spec);
4430
+ }
4431
+ async status() {
4432
+ const controller = await this.resolveController();
4433
+ const spec = this.buildSpec();
4434
+ return controller.status(spec);
4435
+ }
4436
+ };
4437
+
4438
+ // src/cli/commands/service.ts
4439
+ function mergeStartEnv2(options) {
4440
+ return {
4441
+ ...process.env,
4442
+ ...options.workspace ? { DSH_LARK_WORKSPACE: options.workspace } : {},
4443
+ ...options.tenant ? { DSH_LARK_TENANT: options.tenant } : {},
4444
+ ...options.appId ? { DSH_LARK_APP_ID: options.appId } : {},
4445
+ ...options.appSecret ? { DSH_LARK_APP_SECRET: options.appSecret } : {}
4446
+ };
4447
+ }
4448
+ function createManager(options, version) {
4449
+ const env = loadRuntimeEnv(mergeStartEnv2(options));
4450
+ const paths = resolveAppPaths(env.home);
4451
+ return new ServiceManager({
4452
+ profile: options.profile ?? "default",
4453
+ env: mergeStartEnv2(options),
4454
+ paths,
4455
+ version
4456
+ });
4457
+ }
4458
+ function printServiceStatus(status, output) {
4459
+ const lines = [
4460
+ "dsh-lark-bot \u670D\u52A1\u72B6\u6001",
4461
+ ` name: ${status.name}`,
4462
+ ` platform: ${status.platform}`,
4463
+ ` installed: ${status.installed ? "\u662F" : "\u5426"}`,
4464
+ ` autostart: ${status.autostartEnabled ? "\u5DF2\u542F\u7528\uFF08\u5F00\u673A\u81EA\u542F\uFF09" : "\u672A\u542F\u7528"}`,
4465
+ ` state: ${status.state}`,
4466
+ ` detail: ${status.detail}`,
4467
+ ` pid: ${status.pid ?? "-"}`,
4468
+ ` restarts: ${status.restarts ?? "-"}`
4469
+ ];
4470
+ output(`${lines.join("\n")}
4471
+ `);
4472
+ }
4473
+ async function runServiceStart(options, deps = {}) {
4474
+ const version = deps.version ?? "0.0.0";
4475
+ const output = deps.output ?? ((text) => process.stdout.write(text));
4476
+ if (!deps.manager) {
4477
+ const env = loadRuntimeEnv(mergeStartEnv2(options));
4478
+ const paths = resolveAppPaths(env.home);
4479
+ const profileName = options.profile ?? "default";
4480
+ const store = new ConfigStore(paths.configFile);
4481
+ await store.load();
4482
+ const ready = await ensureBotProfile(store, {
4483
+ env,
4484
+ profileName,
4485
+ allowOnboarding: true
4486
+ });
4487
+ if (!ready) {
4488
+ process.exitCode = 1;
4489
+ return;
4490
+ }
4491
+ }
4492
+ const manager = deps.manager ?? createManager(options, version);
4493
+ try {
4494
+ const status = await manager.start();
4495
+ printServiceStatus(status, output);
4496
+ if (status.state !== "running") process.exitCode = 1;
4497
+ } catch (error) {
4498
+ output(`${error instanceof Error ? error.message : String(error)}
4499
+ `);
4500
+ process.exitCode = 1;
4501
+ }
4502
+ }
4503
+ async function runServiceCommand(action, options, deps = {}) {
4504
+ const output = deps.output ?? ((text) => process.stdout.write(text));
4505
+ const manager = deps.manager ?? createManager(options, deps.version ?? "0.0.0");
4506
+ try {
4507
+ let status;
4508
+ if (action === "status") {
4509
+ status = await manager.status();
4510
+ } else if (action === "restart") {
4511
+ status = await manager.restart();
4512
+ } else {
4513
+ status = await manager.stop();
4514
+ }
4515
+ printServiceStatus(status, output);
4516
+ if (action === "status") {
4517
+ if (status.state !== "running") process.exitCode = 1;
4518
+ } else if (action === "restart") {
4519
+ if (status.state !== "running") process.exitCode = 1;
4520
+ } else if (status.state === "running") {
4521
+ process.exitCode = 1;
4522
+ }
4523
+ } catch (error) {
4524
+ output(`${error instanceof Error ? error.message : String(error)}
4525
+ `);
4526
+ process.exitCode = 1;
4527
+ }
4528
+ }
4529
+
4530
+ // src/cli/commands/supervise.ts
4531
+ import { spawn as spawn6 } from "child_process";
4532
+ import { closeSync, mkdirSync, openSync } from "fs";
4533
+ import { dirname as dirname7 } from "path";
4534
+ var BASE_BACKOFF_MS = 5e3;
4535
+ var MAX_BACKOFF_MS = 3e4;
4536
+ function computeBackoffMs(restarts) {
4537
+ return Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** Math.min(restarts, 3));
4538
+ }
4539
+ function waitForExit(child) {
4540
+ return new Promise((resolvePromise) => {
4541
+ const done = () => resolvePromise();
4542
+ child.once("exit", done);
4543
+ child.once("error", done);
4544
+ });
4545
+ }
4546
+ function sleep(ms) {
4547
+ return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
4548
+ }
4549
+ async function runSupervise(options) {
4550
+ const env = loadRuntimeEnv(process.env);
4551
+ const paths = resolveAppPaths(env.home);
4552
+ const profile = options.profile ?? "default";
4553
+ const serviceName = serviceNameFor(profile);
4554
+ const statusFile = supervisorStatusFile(paths.serviceDir, serviceName);
4555
+ const logFile = paths.serviceLogFile(profile);
4556
+ const serviceEnv = await readServiceEnv(paths.serviceEnvFile);
4557
+ const childEnv = { ...process.env, ...serviceEnv };
4558
+ const cliJsPath = resolveCliJsPath();
4559
+ mkdirSync(dirname7(statusFile), { recursive: true });
4560
+ mkdirSync(dirname7(logFile), { recursive: true });
4561
+ const logFd = openSync(logFile, "a");
4562
+ const writeStatus = async (state, childPid, restarts2) => {
4563
+ const status = {
4564
+ pid: process.pid,
4565
+ childPid,
4566
+ state,
4567
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4568
+ restarts: restarts2,
4569
+ profile
4570
+ };
4571
+ await writeFileAtomic(statusFile, `${JSON.stringify(status)}
4572
+ `);
4573
+ };
4574
+ let stopping = false;
4575
+ let child;
4576
+ const stop = () => {
4577
+ stopping = true;
4578
+ child?.kill("SIGTERM");
4579
+ };
4580
+ process.once("SIGTERM", stop);
4581
+ process.once("SIGINT", stop);
4582
+ let restarts = 0;
4583
+ try {
4584
+ while (!stopping) {
4585
+ await writeStatus("running", void 0, restarts);
4586
+ const spawned = spawn6(process.execPath, [cliJsPath, "run", "--profile", profile], {
4587
+ env: childEnv,
4588
+ stdio: ["ignore", logFd, logFd]
4589
+ });
4590
+ child = spawned;
4591
+ await writeStatus("running", spawned.pid, restarts);
4592
+ await waitForExit(spawned);
4593
+ child = void 0;
4594
+ if (stopping) break;
4595
+ restarts += 1;
4596
+ await writeStatus("restarting", void 0, restarts);
4597
+ const backoff = computeBackoffMs(restarts);
4598
+ await sleep(backoff);
4599
+ }
4600
+ } finally {
4601
+ await writeStatus("stopped", void 0, restarts);
4602
+ closeSync(logFd);
4603
+ }
4604
+ }
4605
+
3675
4606
  // src/cli.ts
3676
4607
  function packageVersion() {
3677
4608
  const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
3678
4609
  const pkg = JSON.parse(raw);
3679
4610
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
3680
4611
  }
4612
+ function addBotOptions(command) {
4613
+ return command.option("--profile <name>", "profile name").option("--workspace <path>", "initial working directory").option("--app-id <id>", "existing Lark/Feishu app id").option("--app-secret <secret>", "existing Lark/Feishu app secret").option("--tenant <tenant>", "feishu or lark");
4614
+ }
3681
4615
  function buildProgram() {
3682
4616
  const program = new Command();
3683
4617
  program.name("dsh-lark-bot").description("Bridge DeepSeek Harness into Feishu / Lark").version(packageVersion(), "-v, --version");
3684
- program.command("start").description("Start the bridge in the foreground").option("--profile <name>", "profile name").option("--workspace <path>", "initial working directory").option("--app-id <id>", "existing Lark/Feishu app id").option("--app-secret <secret>", "existing Lark/Feishu app secret").option("--tenant <tenant>", "feishu or lark").action(async (opts) => {
3685
- await runStart(opts);
4618
+ addBotOptions(
4619
+ program.command("start").description("Install and start the background service (autostart + auto-restart)")
4620
+ ).action(async (opts) => {
4621
+ await runServiceStart(opts, { version: packageVersion() });
3686
4622
  });
4623
+ addBotOptions(program.command("status").description("Show background service status")).action(
4624
+ async (opts) => {
4625
+ await runServiceCommand("status", opts, { version: packageVersion() });
4626
+ }
4627
+ );
4628
+ addBotOptions(program.command("restart").description("Restart the background service")).action(
4629
+ async (opts) => {
4630
+ await runServiceCommand("restart", opts, { version: packageVersion() });
4631
+ }
4632
+ );
4633
+ addBotOptions(program.command("stop").description("Stop the background service and remove autostart")).action(
4634
+ async (opts) => {
4635
+ await runServiceCommand("stop", opts, { version: packageVersion() });
4636
+ }
4637
+ );
3687
4638
  program.command("doctor").description("Run local diagnostics").option("--profile <name>", "profile name").option("--workspace <path>", "initial working directory").option("--app-id <id>", "existing Lark/Feishu app id").option("--app-secret <secret>", "existing Lark/Feishu app secret").option("--tenant <tenant>", "feishu or lark").action(async (opts) => {
3688
4639
  await runDoctor({ ...opts, version: packageVersion() });
3689
4640
  });
4641
+ addBotOptions(
4642
+ program.command("run", { hidden: true }).description("Run the bridge process (managed by the background service; not for interactive use)")
4643
+ ).action(async (opts) => {
4644
+ await runBot(opts);
4645
+ });
4646
+ addBotOptions(
4647
+ program.command("supervise", { hidden: true }).description("Supervisor loop for the portable background service; not for interactive use")
4648
+ ).action(async (opts) => {
4649
+ await runSupervise(opts);
4650
+ });
3690
4651
  return program;
3691
4652
  }
3692
4653
  async function main(argv = process.argv) {