@opentag/cli 0.3.4 → 0.3.5

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.
@@ -1,11 +1,14 @@
1
1
  import { type SetupCommandOptions, type SetupFlowDependencies } from "../setup/flow.js";
2
2
  import { type StartCommandOptions } from "../start.js";
3
+ import { type ServiceCommandOptions } from "../service.js";
3
4
  export type { SetupCommandOptions };
4
5
  export type SetupCommandDependencies = Partial<Omit<SetupFlowDependencies, "prompts" | "scanLarkPersonalAgent">> & {
6
+ platform?: NodeJS.Platform;
5
7
  prompts?: SetupFlowDependencies["prompts"];
6
8
  scanLarkPersonalAgent?: SetupFlowDependencies["scanLarkPersonalAgent"];
7
9
  validateLarkCredentials?: SetupFlowDependencies["validateLarkCredentials"];
8
10
  startOpenTag?(options: StartCommandOptions): Promise<void>;
11
+ startOpenTagService?(options: ServiceCommandOptions): Promise<void>;
9
12
  };
10
13
  export declare function runSetupCommand(options: SetupCommandOptions, dependencies?: SetupCommandDependencies): Promise<void>;
11
14
  //# sourceMappingURL=setup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/commands/setup.ts"],"names":[],"mappings":"AAOA,OAAO,EAAqB,KAAK,mBAAmB,EAAE,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAI3G,OAAO,EAAmB,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAExE,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC,MAAM,MAAM,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,qBAAqB,EAAE,SAAS,GAAG,uBAAuB,CAAC,CAAC,GAAG;IACjH,OAAO,CAAC,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAC;IAC3C,qBAAqB,CAAC,EAAE,qBAAqB,CAAC,uBAAuB,CAAC,CAAC;IACvE,uBAAuB,CAAC,EAAE,qBAAqB,CAAC,yBAAyB,CAAC,CAAC;IAC3E,YAAY,CAAC,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D,CAAC;AAcF,wBAAsB,eAAe,CAAC,OAAO,EAAE,mBAAmB,EAAE,YAAY,GAAE,wBAA6B,GAAG,OAAO,CAAC,IAAI,CAAC,CAqC9H"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/commands/setup.ts"],"names":[],"mappings":"AAOA,OAAO,EAAqB,KAAK,mBAAmB,EAAE,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAI3G,OAAO,EAAmB,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAwD,KAAK,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAEjH,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC,MAAM,MAAM,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,qBAAqB,EAAE,SAAS,GAAG,uBAAuB,CAAC,CAAC,GAAG;IACjH,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,OAAO,CAAC,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAC;IAC3C,qBAAqB,CAAC,EAAE,qBAAqB,CAAC,uBAAuB,CAAC,CAAC;IACvE,uBAAuB,CAAC,EAAE,qBAAqB,CAAC,yBAAyB,CAAC,CAAC;IAC3E,YAAY,CAAC,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,mBAAmB,CAAC,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrE,CAAC;AAwDF,wBAAsB,eAAe,CAAC,OAAO,EAAE,mBAAmB,EAAE,YAAY,GAAE,wBAA6B,GAAG,OAAO,CAAC,IAAI,CAAC,CA8C9H"}
package/dist/index.js CHANGED
@@ -2676,6 +2676,19 @@ var launchAgentCliPath = [
2676
2676
  "/usr/sbin",
2677
2677
  "/sbin"
2678
2678
  ].join(":");
2679
+ function linuxServiceCliPath(home) {
2680
+ return [
2681
+ join3(home, ".local", "bin"),
2682
+ join3(home, ".npm-global", "bin"),
2683
+ join3(home, ".bun", "bin"),
2684
+ "/usr/local/bin",
2685
+ "/usr/bin",
2686
+ "/bin",
2687
+ "/usr/local/sbin",
2688
+ "/usr/sbin",
2689
+ "/sbin"
2690
+ ].join(":");
2691
+ }
2679
2692
  function loggerFrom(dependencies) {
2680
2693
  return dependencies.logger ?? console;
2681
2694
  }
@@ -2688,6 +2701,14 @@ function homeFrom(dependencies) {
2688
2701
  function uidFrom(dependencies) {
2689
2702
  return dependencies.uid ?? (typeof process.getuid === "function" ? process.getuid() : 0);
2690
2703
  }
2704
+ function serviceControllerForPlatform(platform = process.platform) {
2705
+ if (platform === "darwin") return "launchd";
2706
+ if (platform === "linux") return "systemd";
2707
+ return "unsupported";
2708
+ }
2709
+ function serviceControllerFrom(dependencies) {
2710
+ return serviceControllerForPlatform(platformFrom(dependencies));
2711
+ }
2691
2712
  function servicePaths(options = {}, dependencies = {}) {
2692
2713
  const home = homeFrom(dependencies);
2693
2714
  const env = dependencies.env ?? process.env;
@@ -2700,7 +2721,8 @@ function servicePaths(options = {}, dependencies = {}) {
2700
2721
  logsDir,
2701
2722
  plistPath: join3(home, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`),
2702
2723
  stdoutPath: join3(logsDir, "opentag.log"),
2703
- stderrPath: join3(logsDir, "opentag.err.log")
2724
+ stderrPath: join3(logsDir, "opentag.err.log"),
2725
+ unitPath: join3(home, ".config", "systemd", "user", `${SERVICE_LABEL}.service`)
2704
2726
  };
2705
2727
  }
2706
2728
  function escapeXml(value) {
@@ -2750,6 +2772,36 @@ function buildLaunchAgentPlist(input) {
2750
2772
  ""
2751
2773
  ].join("\n");
2752
2774
  }
2775
+ function systemdQuote(value) {
2776
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("$", "\\$").replaceAll("%", "%%")}"`;
2777
+ }
2778
+ function systemdPathValue(value) {
2779
+ return value.replaceAll("%", "%%");
2780
+ }
2781
+ function buildSystemdUserService(input) {
2782
+ const environment = Object.entries(input.environment ?? {}).map(
2783
+ ([key, value]) => `Environment=${systemdQuote(`${key}=${value}`)}`
2784
+ );
2785
+ return [
2786
+ "[Unit]",
2787
+ "Description=OpenTag local agent",
2788
+ "After=network.target",
2789
+ "",
2790
+ "[Service]",
2791
+ "Type=simple",
2792
+ `WorkingDirectory=${systemdQuote(input.workingDirectory)}`,
2793
+ ...environment,
2794
+ `ExecStart=${input.execStart.map(systemdQuote).join(" ")}`,
2795
+ "Restart=always",
2796
+ "RestartSec=3",
2797
+ `StandardOutput=append:${systemdPathValue(input.stdoutPath)}`,
2798
+ `StandardError=append:${systemdPathValue(input.stderrPath)}`,
2799
+ "",
2800
+ "[Install]",
2801
+ "WantedBy=default.target",
2802
+ ""
2803
+ ].join("\n");
2804
+ }
2753
2805
  function launchctlRunner(dependencies) {
2754
2806
  if (dependencies.launchctl) return dependencies.launchctl;
2755
2807
  return (args) => {
@@ -2761,6 +2813,17 @@ function launchctlRunner(dependencies) {
2761
2813
  };
2762
2814
  };
2763
2815
  }
2816
+ function systemctlRunner(dependencies) {
2817
+ if (dependencies.systemctl) return dependencies.systemctl;
2818
+ return (args) => {
2819
+ const result = spawnSync("systemctl", ["--user", ...args], { encoding: "utf8" });
2820
+ return {
2821
+ status: result.status ?? 1,
2822
+ stdout: result.stdout ?? "",
2823
+ stderr: result.stderr ?? ""
2824
+ };
2825
+ };
2826
+ }
2764
2827
  function launchdDomain(dependencies) {
2765
2828
  return `gui/${uidFrom(dependencies)}`;
2766
2829
  }
@@ -2768,12 +2831,14 @@ function launchdServiceTarget(dependencies) {
2768
2831
  return `${launchdDomain(dependencies)}/${SERVICE_LABEL}`;
2769
2832
  }
2770
2833
  function unsupportedMessage() {
2771
- return "OpenTag service management is not supported yet on this platform. Use `opentag start` in the foreground for now.";
2834
+ return "OpenTag service management is supported on macOS and Linux only. Use `opentag start` in the foreground on this platform.";
2772
2835
  }
2773
- function assertMacOS(dependencies) {
2774
- if (platformFrom(dependencies) !== "darwin") {
2836
+ function assertSupportedServiceController(dependencies) {
2837
+ const controller = serviceControllerFrom(dependencies);
2838
+ if (controller === "unsupported") {
2775
2839
  throw new Error(unsupportedMessage());
2776
2840
  }
2841
+ return controller;
2777
2842
  }
2778
2843
  function runLaunchctlOrThrow(dependencies, args, action) {
2779
2844
  const result = launchctlRunner(dependencies)(args);
@@ -2786,9 +2851,26 @@ function runLaunchctlOrThrow(dependencies, args, action) {
2786
2851
  function launchctlDetail(result) {
2787
2852
  return [result.stderr.trim(), result.stdout.trim()].filter(Boolean).join("\n");
2788
2853
  }
2854
+ function systemctlDetail(result) {
2855
+ return [result.stderr.trim(), result.stdout.trim()].filter(Boolean).join("\n");
2856
+ }
2857
+ function runSystemctlOrThrow(dependencies, args, action) {
2858
+ const result = systemctlRunner(dependencies)(args);
2859
+ if (result.status !== 0) {
2860
+ const detail = systemctlDetail(result);
2861
+ throw new Error(`${action} failed${detail ? `: ${detail}` : "."}`);
2862
+ }
2863
+ return result;
2864
+ }
2789
2865
  function printLaunchdService(dependencies) {
2790
2866
  return launchctlRunner(dependencies)(["print", launchdServiceTarget(dependencies)]);
2791
2867
  }
2868
+ function systemdUnitName() {
2869
+ return `${SERVICE_LABEL}.service`;
2870
+ }
2871
+ function printSystemdService(dependencies) {
2872
+ return systemctlRunner(dependencies)(["is-active", systemdUnitName()]);
2873
+ }
2792
2874
  function sleepFrom(dependencies) {
2793
2875
  return dependencies.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
2794
2876
  }
@@ -2810,6 +2892,38 @@ async function waitForLaunchdUnloaded(dependencies, input = {}) {
2810
2892
  await sleepFrom(dependencies)(intervalMs);
2811
2893
  }
2812
2894
  }
2895
+ async function waitForSystemdActive(dependencies, input = {}) {
2896
+ const intervalMs = input.intervalMs ?? 100;
2897
+ const deadline = Date.now() + (input.timeoutMs ?? 1500);
2898
+ while (true) {
2899
+ const result = printSystemdService(dependencies);
2900
+ if (result.status === 0 && result.stdout.trim() === "active") return true;
2901
+ if (Date.now() >= deadline) return false;
2902
+ await sleepFrom(dependencies)(intervalMs);
2903
+ }
2904
+ }
2905
+ async function waitForSystemdInactive(dependencies, input = {}) {
2906
+ const intervalMs = input.intervalMs ?? 100;
2907
+ const deadline = Date.now() + (input.timeoutMs ?? 1500);
2908
+ while (true) {
2909
+ const result = printSystemdService(dependencies);
2910
+ if (result.status !== 0 || result.stdout.trim() !== "active") return true;
2911
+ if (Date.now() >= deadline) return false;
2912
+ await sleepFrom(dependencies)(intervalMs);
2913
+ }
2914
+ }
2915
+ async function waitForServiceLoaded(dependencies, input = {}) {
2916
+ const controller = serviceControllerFrom(dependencies);
2917
+ if (controller === "launchd") return waitForLaunchdLoaded(dependencies, input);
2918
+ if (controller === "systemd") return waitForSystemdActive(dependencies, input);
2919
+ return false;
2920
+ }
2921
+ async function waitForServiceUnloaded(dependencies, input = {}) {
2922
+ const controller = serviceControllerFrom(dependencies);
2923
+ if (controller === "launchd") return waitForLaunchdUnloaded(dependencies, input);
2924
+ if (controller === "systemd") return waitForSystemdInactive(dependencies, input);
2925
+ return true;
2926
+ }
2813
2927
  function serviceWorkingDirectory(configPath) {
2814
2928
  const config = readCliConfig(configPath);
2815
2929
  return config.daemon.repositories[0]?.checkoutPath ?? dirname2(configPath);
@@ -2883,46 +2997,77 @@ function formatConnectorReadiness(config) {
2883
2997
  }
2884
2998
  return lines.length > 1 ? lines : [...lines, " none configured"];
2885
2999
  }
2886
- function launchAgentEnvironment(options, paths) {
3000
+ function serviceCliPath(dependencies) {
3001
+ const controller = serviceControllerFrom(dependencies);
3002
+ return controller === "systemd" ? linuxServiceCliPath(homeFrom(dependencies)) : launchAgentCliPath;
3003
+ }
3004
+ function serviceEnvironment(options, paths, dependencies) {
2887
3005
  return {
2888
3006
  OPENTAG_CONFIG_PATH: paths.configPath,
2889
- PATH: launchAgentCliPath,
3007
+ PATH: serviceCliPath(dependencies),
2890
3008
  ...serviceHardeningEnvironment(options)
2891
3009
  };
2892
3010
  }
2893
3011
  function installService(options = {}, dependencies = {}) {
2894
- assertMacOS(dependencies);
3012
+ const controller = assertSupportedServiceController(dependencies);
2895
3013
  const paths = servicePaths(options, dependencies);
2896
3014
  const workingDirectory = serviceWorkingDirectory(paths.configPath);
2897
- mkdirSync2(dirname2(paths.plistPath), { recursive: true });
2898
3015
  ensurePrivateDirectory(paths.logsDir);
2899
- const plist = buildLaunchAgentPlist({
3016
+ if (controller === "launchd") {
3017
+ mkdirSync2(dirname2(paths.plistPath), { recursive: true });
3018
+ const plist = buildLaunchAgentPlist({
3019
+ label: paths.label,
3020
+ programArguments: serviceProgramArguments(options, dependencies),
3021
+ runAtLoad: true,
3022
+ keepAlive: true,
3023
+ stdoutPath: paths.stdoutPath,
3024
+ stderrPath: paths.stderrPath,
3025
+ workingDirectory,
3026
+ environment: serviceEnvironment(options, paths, dependencies)
3027
+ });
3028
+ writeFileSync2(paths.plistPath, plist, { mode: 420 });
3029
+ return paths;
3030
+ }
3031
+ mkdirSync2(dirname2(paths.unitPath), { recursive: true });
3032
+ const unit = buildSystemdUserService({
2900
3033
  label: paths.label,
2901
- programArguments: serviceProgramArguments(options, dependencies),
2902
- runAtLoad: true,
2903
- keepAlive: true,
3034
+ execStart: serviceProgramArguments(options, dependencies),
2904
3035
  stdoutPath: paths.stdoutPath,
2905
3036
  stderrPath: paths.stderrPath,
2906
3037
  workingDirectory,
2907
- environment: launchAgentEnvironment(options, paths)
3038
+ environment: serviceEnvironment(options, paths, dependencies)
2908
3039
  });
2909
- writeFileSync2(paths.plistPath, plist, { mode: 420 });
3040
+ writeFileSync2(paths.unitPath, unit, { mode: 420 });
3041
+ runSystemctlOrThrow(dependencies, ["daemon-reload"], "systemctl --user daemon-reload");
3042
+ runSystemctlOrThrow(dependencies, ["enable", systemdUnitName()], "systemctl --user enable");
2910
3043
  return paths;
2911
3044
  }
2912
- function installed(paths) {
2913
- return existsSync2(paths.plistPath);
3045
+ function installed(paths, controller) {
3046
+ if (controller === "launchd") return existsSync2(paths.plistPath);
3047
+ if (controller === "systemd") return existsSync2(paths.unitPath);
3048
+ return false;
2914
3049
  }
2915
3050
  function isNotLoaded(result) {
2916
3051
  const text2 = `${result.stderr}
2917
3052
  ${result.stdout}`.toLowerCase();
2918
3053
  return text2.includes("no such process") || text2.includes("could not find service") || text2.includes("service is not loaded");
2919
3054
  }
3055
+ function isSystemdNotLoaded(result) {
3056
+ const text2 = `${result.stderr}
3057
+ ${result.stdout}`.toLowerCase();
3058
+ return text2.includes("could not be found") || text2.includes("not loaded") || text2.includes("not-found") || text2.includes("no such");
3059
+ }
2920
3060
  function startService(options = {}, dependencies = {}) {
2921
- assertMacOS(dependencies);
3061
+ const controller = assertSupportedServiceController(dependencies);
2922
3062
  const paths = servicePaths(options, dependencies);
2923
- if (!installed(paths)) {
3063
+ if (!installed(paths, controller)) {
2924
3064
  throw new Error(`OpenTag service is not installed. Run \`opentag service install --config ${paths.configPath}\` first.`);
2925
3065
  }
3066
+ if (controller === "systemd") {
3067
+ runSystemctlOrThrow(dependencies, ["daemon-reload"], "systemctl --user daemon-reload");
3068
+ runSystemctlOrThrow(dependencies, ["start", systemdUnitName()], "systemctl --user start");
3069
+ return paths;
3070
+ }
2926
3071
  const launchctl = launchctlRunner(dependencies);
2927
3072
  const bootstrap = launchctl(["bootstrap", launchdDomain(dependencies), paths.plistPath]);
2928
3073
  if (bootstrap.status !== 0) {
@@ -2940,9 +3085,17 @@ function startService(options = {}, dependencies = {}) {
2940
3085
  return paths;
2941
3086
  }
2942
3087
  function stopService(options = {}, dependencies = {}) {
2943
- assertMacOS(dependencies);
3088
+ const controller = assertSupportedServiceController(dependencies);
2944
3089
  const paths = servicePaths(options, dependencies);
2945
- if (!installed(paths)) return paths;
3090
+ if (!installed(paths, controller)) return paths;
3091
+ if (controller === "systemd") {
3092
+ const result = systemctlRunner(dependencies)(["stop", systemdUnitName()]);
3093
+ if (result.status !== 0 && !isSystemdNotLoaded(result)) {
3094
+ const detail = systemctlDetail(result);
3095
+ throw new Error(`systemctl --user stop failed${detail ? `: ${detail}` : "."}`);
3096
+ }
3097
+ return paths;
3098
+ }
2946
3099
  const launchctl = launchctlRunner(dependencies);
2947
3100
  const first = launchctl(["bootout", launchdServiceTarget(dependencies)]);
2948
3101
  if (first.status !== 0) {
@@ -2956,28 +3109,56 @@ function stopService(options = {}, dependencies = {}) {
2956
3109
  return paths;
2957
3110
  }
2958
3111
  function uninstallService(options = {}, dependencies = {}) {
2959
- assertMacOS(dependencies);
3112
+ const controller = assertSupportedServiceController(dependencies);
2960
3113
  const paths = stopService(options, dependencies);
2961
- rmSync2(paths.plistPath, { force: true });
3114
+ if (controller === "launchd") {
3115
+ rmSync2(paths.plistPath, { force: true });
3116
+ return paths;
3117
+ }
3118
+ const disabled = systemctlRunner(dependencies)(["disable", systemdUnitName()]);
3119
+ if (disabled.status !== 0 && !isSystemdNotLoaded(disabled)) {
3120
+ const detail = systemctlDetail(disabled);
3121
+ throw new Error(`systemctl --user disable failed${detail ? `: ${detail}` : "."}`);
3122
+ }
3123
+ rmSync2(paths.unitPath, { force: true });
3124
+ runSystemctlOrThrow(dependencies, ["daemon-reload"], "systemctl --user daemon-reload");
2962
3125
  return paths;
2963
3126
  }
2964
3127
  function enableServiceAutostart(options = {}, dependencies = {}) {
2965
- assertMacOS(dependencies);
2966
- const paths = installed(servicePaths(options, dependencies)) ? servicePaths(options, dependencies) : installService(options, dependencies);
3128
+ const controller = assertSupportedServiceController(dependencies);
3129
+ const candidate = servicePaths(options, dependencies);
3130
+ const paths = installed(candidate, controller) ? candidate : installService(options, dependencies);
3131
+ if (controller === "systemd") {
3132
+ runSystemctlOrThrow(dependencies, ["enable", systemdUnitName()], "systemctl --user enable");
3133
+ return paths;
3134
+ }
2967
3135
  runLaunchctlOrThrow(dependencies, ["enable", launchdServiceTarget(dependencies)], "launchctl enable");
2968
3136
  return paths;
2969
3137
  }
2970
3138
  function disableServiceAutostart(options = {}, dependencies = {}) {
2971
- assertMacOS(dependencies);
3139
+ const controller = assertSupportedServiceController(dependencies);
2972
3140
  const paths = servicePaths(options, dependencies);
2973
- if (installed(paths)) {
3141
+ if (installed(paths, controller)) {
3142
+ if (controller === "systemd") {
3143
+ runSystemctlOrThrow(dependencies, ["disable", systemdUnitName()], "systemctl --user disable");
3144
+ return paths;
3145
+ }
2974
3146
  runLaunchctlOrThrow(dependencies, ["disable", launchdServiceTarget(dependencies)], "launchctl disable");
2975
3147
  }
2976
3148
  return paths;
2977
3149
  }
2978
3150
  function serviceAutostart(paths, dependencies, isInstalled) {
2979
3151
  if (!isInstalled) return "disabled";
2980
- if (platformFrom(dependencies) !== "darwin") return "unknown";
3152
+ const controller = serviceControllerFrom(dependencies);
3153
+ if (controller === "systemd") {
3154
+ const result2 = systemctlRunner(dependencies)(["is-enabled", systemdUnitName()]);
3155
+ const text2 = `${result2.stdout}
3156
+ ${result2.stderr}`.trim().toLowerCase();
3157
+ if (result2.status === 0 && text2.includes("enabled")) return "enabled";
3158
+ if (text2.includes("disabled") || text2.includes("not-found") || text2.includes("could not be found")) return "disabled";
3159
+ return "unknown";
3160
+ }
3161
+ if (controller !== "launchd") return "unknown";
2981
3162
  const result = launchctlRunner(dependencies)(["print-disabled", launchdDomain(dependencies)]);
2982
3163
  if (result.status !== 0) return "unknown";
2983
3164
  const escapedLabel = SERVICE_LABEL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -2989,12 +3170,15 @@ function serviceAutostart(paths, dependencies, isInstalled) {
2989
3170
  }
2990
3171
  function getServiceStatus(options = {}, dependencies = {}) {
2991
3172
  const paths = servicePaths(options, dependencies);
2992
- const controller = platformFrom(dependencies) === "darwin" ? "launchd" : "unsupported";
2993
- const isInstalled = installed(paths);
3173
+ const controller = serviceControllerFrom(dependencies);
3174
+ const isInstalled = installed(paths, controller);
2994
3175
  let running = isInstalled ? "stopped" : "unknown";
2995
3176
  if (controller === "launchd" && isInstalled) {
2996
3177
  const result = launchctlRunner(dependencies)(["print", launchdServiceTarget(dependencies)]);
2997
3178
  running = result.status === 0 ? "running" : "stopped";
3179
+ } else if (controller === "systemd" && isInstalled) {
3180
+ const result = systemctlRunner(dependencies)(["is-active", systemdUnitName()]);
3181
+ running = result.status === 0 && result.stdout.trim() === "active" ? "running" : "stopped";
2998
3182
  }
2999
3183
  let runtimeMode = "unknown";
3000
3184
  let relayUrl;
@@ -3045,10 +3229,34 @@ function readLaunchAgentEnvironment(paths) {
3045
3229
  })
3046
3230
  );
3047
3231
  }
3232
+ function unescapeSystemdQuotedValue(value) {
3233
+ return value.replaceAll("%%", "%").replaceAll('\\"', '"').replaceAll("\\$", "$").replaceAll("\\\\", "\\");
3234
+ }
3235
+ function readSystemdEnvironment(paths) {
3236
+ if (!existsSync2(paths.unitPath)) return {};
3237
+ const unit = readFileSync2(paths.unitPath, "utf8");
3238
+ return Object.fromEntries(
3239
+ unit.split(/\r?\n/).flatMap((line) => {
3240
+ const match = line.match(/^Environment=(?:"((?:\\.|[^"])*)"|(.+))$/);
3241
+ const raw = match?.[1] ?? match?.[2];
3242
+ if (!raw) return [];
3243
+ const value = unescapeSystemdQuotedValue(raw);
3244
+ const separator = value.indexOf("=");
3245
+ if (separator <= 0) return [];
3246
+ return [[value.slice(0, separator), value.slice(separator + 1)]];
3247
+ })
3248
+ );
3249
+ }
3250
+ function readServiceEnvironment(paths) {
3251
+ return {
3252
+ ...readLaunchAgentEnvironment(paths),
3253
+ ...readSystemdEnvironment(paths)
3254
+ };
3255
+ }
3048
3256
  function formatServiceHardening(paths) {
3049
- const environment = readLaunchAgentEnvironment(paths);
3257
+ const environment = readServiceEnvironment(paths);
3050
3258
  const configured2 = serviceHardeningEnvKeys.filter((key) => environment[key]).map((key) => ` ${key}=${environment[key]}`);
3051
- return ["Service Hardening:", ...configured2.length ? configured2 : [" dispatcher hardening env not configured in LaunchAgent"]];
3259
+ return ["Service Hardening:", ...configured2.length ? configured2 : [" dispatcher hardening env not configured in service definition"]];
3052
3260
  }
3053
3261
  function doctorCounts(checks) {
3054
3262
  return {
@@ -3138,6 +3346,7 @@ async function getServiceStatusWithRuntimeReadiness(options = {}, dependencies =
3138
3346
  }
3139
3347
  }
3140
3348
  function formatServiceStatus(summary) {
3349
+ const definitionLine = summary.controller === "launchd" ? `LaunchAgent: ${summary.plistPath}` : summary.controller === "systemd" ? `Systemd unit: ${summary.unitPath}` : void 0;
3141
3350
  return [
3142
3351
  `Controller: ${summary.controller}`,
3143
3352
  `Installed: ${summary.installed ? "yes" : "no"}`,
@@ -3153,7 +3362,7 @@ function formatServiceStatus(summary) {
3153
3362
  ...summary.secrets,
3154
3363
  ...summary.capabilities,
3155
3364
  ...summary.serviceHardening,
3156
- `LaunchAgent: ${summary.plistPath}`,
3365
+ ...definitionLine ? [definitionLine] : [],
3157
3366
  `Stdout log: ${summary.stdoutPath}`,
3158
3367
  `Stderr log: ${summary.stderrPath}`,
3159
3368
  ...summary.controller === "unsupported" ? [unsupportedMessage()] : []
@@ -3203,13 +3412,22 @@ function formatServiceLogs(options = {}, dependencies = {}) {
3203
3412
  }
3204
3413
  async function runServiceInstallCommand(options, dependencies = {}) {
3205
3414
  const paths = installService(options, dependencies);
3206
- loggerFrom(dependencies).log(`OpenTag service installed: ${paths.plistPath}`);
3415
+ const controller = serviceControllerFrom(dependencies);
3416
+ loggerFrom(dependencies).log(`OpenTag service installed: ${controller === "systemd" ? paths.unitPath : paths.plistPath}`);
3207
3417
  loggerFrom(dependencies).log("It will start at login. Run `opentag service start` to start it now.");
3208
3418
  }
3419
+ async function installAndStartService(options = {}, dependencies = {}) {
3420
+ installService(options, dependencies);
3421
+ const paths = startService(options, dependencies);
3422
+ if (!await waitForServiceLoaded(dependencies)) {
3423
+ throw new Error("OpenTag service start did not leave the service manager running. Run `opentag service status` and `opentag service logs` for details.");
3424
+ }
3425
+ return paths;
3426
+ }
3209
3427
  async function runServiceStartCommand(options, dependencies = {}) {
3210
3428
  const paths = startService(options, dependencies);
3211
- if (!await waitForLaunchdLoaded(dependencies)) {
3212
- throw new Error("OpenTag service start did not leave launchd loaded. Run `opentag service status` and `opentag service logs` for details.");
3429
+ if (!await waitForServiceLoaded(dependencies)) {
3430
+ throw new Error("OpenTag service start did not leave the service manager running. Run `opentag service status` and `opentag service logs` for details.");
3213
3431
  }
3214
3432
  loggerFrom(dependencies).log(`OpenTag service started: ${paths.label}`);
3215
3433
  }
@@ -3219,21 +3437,22 @@ async function runServiceStopCommand(options, dependencies = {}) {
3219
3437
  }
3220
3438
  async function runServiceRestartCommand(options, dependencies = {}) {
3221
3439
  stopService(options, dependencies);
3222
- await waitForLaunchdUnloaded(dependencies, { timeoutMs: 1e3 });
3440
+ await waitForServiceUnloaded(dependencies, { timeoutMs: 1e3 });
3223
3441
  let paths = startService(options, dependencies);
3224
- let loaded = await waitForLaunchdLoaded(dependencies, { timeoutMs: 500 });
3442
+ let loaded = await waitForServiceLoaded(dependencies, { timeoutMs: 500 });
3225
3443
  if (!loaded) {
3226
3444
  paths = startService(options, dependencies);
3227
- loaded = await waitForLaunchdLoaded(dependencies);
3445
+ loaded = await waitForServiceLoaded(dependencies);
3228
3446
  }
3229
3447
  if (!loaded) {
3230
- throw new Error("OpenTag service restart did not leave launchd loaded. Run `opentag service status` and `opentag service logs` for details.");
3448
+ throw new Error("OpenTag service restart did not leave the service manager running. Run `opentag service status` and `opentag service logs` for details.");
3231
3449
  }
3232
3450
  loggerFrom(dependencies).log(`OpenTag service restarted: ${paths.label}`);
3233
3451
  }
3234
3452
  async function runServiceUninstallCommand(options, dependencies = {}) {
3235
3453
  const paths = uninstallService(options, dependencies);
3236
- loggerFrom(dependencies).log(`OpenTag service uninstalled: ${paths.plistPath}`);
3454
+ const controller = serviceControllerFrom(dependencies);
3455
+ loggerFrom(dependencies).log(`OpenTag service uninstalled: ${controller === "systemd" ? paths.unitPath : paths.plistPath}`);
3237
3456
  }
3238
3457
  async function runServiceStatusCommand(options, dependencies = {}) {
3239
3458
  const summary = await getServiceStatusWithRuntimeReadiness(options, dependencies);
@@ -4769,16 +4988,54 @@ async function scanLarkPersonalAgent(input = {}, dependencies = {}) {
4769
4988
  }
4770
4989
 
4771
4990
  // src/commands/setup.ts
4772
- function startPromptMessage(language) {
4773
- return language === "zh-CN" ? "\u73B0\u5728\u542F\u52A8 OpenTag\uFF1F" : "Start OpenTag now?";
4774
- }
4775
4991
  function setupCompleteMessage(language) {
4776
4992
  return language === "zh-CN" ? "OpenTag \u8BBE\u7F6E\u5B8C\u6210\u3002" : "OpenTag setup complete.";
4777
4993
  }
4778
4994
  function startingMessage(language) {
4779
4995
  return language === "zh-CN" ? "\u6B63\u5728\u542F\u52A8 OpenTag..." : "Starting OpenTag...";
4780
4996
  }
4997
+ function serviceStartingMessage(language) {
4998
+ return language === "zh-CN" ? "\u6B63\u5728\u5B89\u88C5\u5E76\u542F\u52A8 OpenTag \u540E\u53F0\u670D\u52A1..." : "Installing and starting the OpenTag background service...";
4999
+ }
5000
+ function serviceStartedMessage(language) {
5001
+ return language === "zh-CN" ? "OpenTag \u8BBE\u7F6E\u5B8C\u6210\uFF0C\u540E\u53F0\u670D\u52A1\u5DF2\u542F\u52A8\u3002" : "OpenTag setup complete. The background service is running.";
5002
+ }
5003
+ function runModePromptMessage(language) {
5004
+ return language === "zh-CN" ? "OpenTag \u8981\u5982\u4F55\u8FD0\u884C\uFF1F" : "How should OpenTag run?";
5005
+ }
5006
+ function runModeOptions(language, serviceSupported) {
5007
+ if (language === "zh-CN") {
5008
+ return [
5009
+ ...serviceSupported ? [{ value: "service", label: "\u5173\u95ED\u8FD9\u4E2A\u7EC8\u7AEF\u540E\u7EE7\u7EED\u8FD0\u884C\uFF08\u63A8\u8350\uFF09" }] : [],
5010
+ { value: "terminal", label: "\u53EA\u5728\u5F53\u524D\u7EC8\u7AEF\u91CC\u8FD0\u884C" },
5011
+ { value: "later", label: "\u6682\u65F6\u4E0D\u542F\u52A8" }
5012
+ ];
5013
+ }
5014
+ return [
5015
+ ...serviceSupported ? [{ value: "service", label: "Keep running after I close this terminal (recommended)" }] : [],
5016
+ { value: "terminal", label: "Run only in this terminal" },
5017
+ { value: "later", label: "Do not start now" }
5018
+ ];
5019
+ }
5020
+ async function collectRunMode(options, prompts, language, platform) {
5021
+ if (options.service) return "service";
5022
+ if (options.start === true) return "terminal";
5023
+ if (options.start === false || options.yes) return "later";
5024
+ const serviceSupported = serviceControllerForPlatform(platform) !== "unsupported";
5025
+ return prompts.select({
5026
+ message: runModePromptMessage(language),
5027
+ initialValue: serviceSupported ? "service" : "terminal",
5028
+ options: runModeOptions(language, serviceSupported)
5029
+ });
5030
+ }
4781
5031
  async function runSetupCommand(options, dependencies = {}) {
5032
+ if (options.service && options.start !== void 0) {
5033
+ throw new Error("--service cannot be combined with --start or --no-start.");
5034
+ }
5035
+ const platform = dependencies.platform ?? process.platform;
5036
+ if (options.service && serviceControllerForPlatform(platform) === "unsupported") {
5037
+ throw new Error("OpenTag background service is not supported on this platform. Use `opentag start` to run OpenTag in this terminal.");
5038
+ }
4782
5039
  const env = dependencies.env ?? process.env;
4783
5040
  const configPath = options.config ?? defaultConfigPath(env);
4784
5041
  if (options.yes && existsSync6(configPath) && !options.force) {
@@ -4798,11 +5055,14 @@ async function runSetupCommand(options, dependencies = {}) {
4798
5055
  ensurePrivateDirectory(config.state.worktreeRoot);
4799
5056
  writeCliConfigAtomic(configPath, config);
4800
5057
  prompts.note(formatSetupComplete(config, configPath));
4801
- const shouldStart = options.start ?? (!options.yes ? await prompts.confirm({
4802
- message: startPromptMessage(config.preferences?.language),
4803
- initialValue: true
4804
- }) : false);
4805
- if (shouldStart) {
5058
+ const runMode = await collectRunMode(options, prompts, config.preferences?.language, platform);
5059
+ if (runMode === "service") {
5060
+ prompts.note(serviceStartingMessage(config.preferences?.language));
5061
+ await (dependencies.startOpenTagService ?? installAndStartService)({ config: configPath });
5062
+ prompts.outro(serviceStartedMessage(config.preferences?.language));
5063
+ return;
5064
+ }
5065
+ if (runMode === "terminal") {
4806
5066
  prompts.outro(startingMessage(config.preferences?.language));
4807
5067
  await (dependencies.startOpenTag ?? runStartCommand)({ config: configPath });
4808
5068
  } else {
@@ -4826,7 +5086,7 @@ function runCliAction(handler) {
4826
5086
  };
4827
5087
  }
4828
5088
  program.name(process.env.OPENTAG_CLI_NAME?.trim() || "opentag").description("OpenTag CLI");
4829
- program.command("setup").description("Create a local OpenTag config").option("--platform <platform>", "Platform to configure").option("--config <path>", "Config file path").option("--project <path>", "Project checkout path").option("--language <language>", "Setup language: en or zh-CN").option("--executor <executor>", "Default executor: echo, codex, claude-code, or hermes").option("--hermes-command <command>", "Hermes CLI command").option("--hermes-profile <profile>", "Hermes profile").option("--hermes-profile-template <template>", "Hermes profile template").option("--agent-profile <profile>", "Executor-neutral agent session profile").option("--agent-profile-template <template>", "Executor-neutral agent session profile template").option("--lark-setup <method>", "Lark setup method: saved, scan, or manual").option("--lark-app-id <id>", "Lark app id").option("--lark-app-secret <secret>", "Lark app secret").option("--tenant <tenant>", "Manual Lark / Feishu tenant: feishu or lark").option("--lark-bot-open-id <openId>", "Lark bot open id for group mentions").option("--slack-mode <mode>", "Slack connection mode: socket_mode or events_api").option("--slack-app-token <token>", "Slack app-level token for Socket Mode").option("--slack-signing-secret <secret>", "Slack signing secret").option("--slack-bot-token <token>", "Slack bot user OAuth token").option("--slack-app-id <id>", "Slack app id").option("--slack-team-id <id>", "Slack team id").option("--slack-channel-id <id>", "Slack channel id").option("--slack-port <port>", "Local Slack Events API port").option("--github-token <token>", "GitHub token for comments and apply-1 pull requests").option("--github-webhook-secret <secret>", "GitHub webhook secret; generated when omitted").option("--github-repository <ownerRepo>", "GitHub repository as owner/repo").option("--github-webhook-path <path>", "GitHub webhook path").option("--github-port <port>", "Local GitHub webhook port").option("--github-auto-create-pr", "Create pull requests immediately after runs").option("--no-github-auto-create-pr", "Use the default apply-1 pull request flow").option("--binding <method>", "Binding method: default_project or bind_later").option("--force", "Overwrite an existing config").option("--start", "Start OpenTag immediately after setup").option("--no-start", "Do not ask to start OpenTag after setup").option("-y, --yes", "Skip setup confirmation").action(runCliAction(runSetupCommand));
5089
+ program.command("setup").description("Create a local OpenTag config").option("--platform <platform>", "Platform to configure").option("--config <path>", "Config file path").option("--project <path>", "Project checkout path").option("--language <language>", "Setup language: en or zh-CN").option("--executor <executor>", "Default executor: echo, codex, claude-code, or hermes").option("--hermes-command <command>", "Hermes CLI command").option("--hermes-profile <profile>", "Hermes profile").option("--hermes-profile-template <template>", "Hermes profile template").option("--agent-profile <profile>", "Executor-neutral agent session profile").option("--agent-profile-template <template>", "Executor-neutral agent session profile template").option("--lark-setup <method>", "Lark setup method: saved, scan, or manual").option("--lark-app-id <id>", "Lark app id").option("--lark-app-secret <secret>", "Lark app secret").option("--tenant <tenant>", "Manual Lark / Feishu tenant: feishu or lark").option("--lark-bot-open-id <openId>", "Lark bot open id for group mentions").option("--slack-mode <mode>", "Slack connection mode: socket_mode or events_api").option("--slack-app-token <token>", "Slack app-level token for Socket Mode").option("--slack-signing-secret <secret>", "Slack signing secret").option("--slack-bot-token <token>", "Slack bot user OAuth token").option("--slack-app-id <id>", "Slack app id").option("--slack-team-id <id>", "Slack team id").option("--slack-channel-id <id>", "Slack channel id").option("--slack-port <port>", "Local Slack Events API port").option("--github-token <token>", "GitHub token for comments and apply-1 pull requests").option("--github-webhook-secret <secret>", "GitHub webhook secret; generated when omitted").option("--github-repository <ownerRepo>", "GitHub repository as owner/repo").option("--github-webhook-path <path>", "GitHub webhook path").option("--github-port <port>", "Local GitHub webhook port").option("--github-auto-create-pr", "Create pull requests immediately after runs").option("--no-github-auto-create-pr", "Use the default apply-1 pull request flow").option("--binding <method>", "Binding method: default_project or bind_later").option("--force", "Overwrite an existing config").option("--start", "Start OpenTag immediately after setup").option("--no-start", "Do not ask to start OpenTag after setup").option("--service", "Install and start OpenTag as a background service after setup").option("-y, --yes", "Skip setup confirmation").action(runCliAction(runSetupCommand));
4830
5090
  program.command("pair").description("Pair this local runner with a remote relay").option("--config <path>", "Config file path").option("--relay <url>", "Remote relay dispatcher URL").option("--no-register", "Update config without registering runner and project targets").action(runCliAction(runPairCommand));
4831
5091
  program.command("start").description("Start the local OpenTag stack").option("--config <path>", "Config file path").action(runCliAction(runStartCommand));
4832
5092
  program.command("status").description("Show the local OpenTag status").option("--config <path>", "Config file path").option("--run <runId>", "Show audit details for one run").option("--channel <provider:account/conversation>", "Show active run and queued follow-ups for one source container").action(runCliAction(runStatusCommand));
@@ -4835,7 +5095,7 @@ program.command("doctor").description("Check dispatcher, bindings, checkouts, an
4835
5095
  program.command("ingest").description("Ingest a local external agent progress or completion event").option("--config <path>", "Config file path").requiredOption("--run <runId>", "OpenTag run id").requiredOption("--event <event>", "Event: progress, post_llm_call, before_agent_finalize, agent_end, failed, cancelled, timed_out, or interrupted").option("--source <source>", "External agent runtime source label").option("--message <message>", "Progress or completion summary").option("--type <type>", "Progress event type").option("--idempotency-key <key>", "Stable replay-protection key for retrying the same progress event").option("--result-json <json>", "Complete run with an OpenTagRunResult JSON object").option("--conclusion <conclusion>", "Completion conclusion when --result-json is omitted").option("--summary <summary>", "Completion summary when --result-json is omitted").action(runCliAction(runIngestCommand));
4836
5096
  program.command("ingest-template").description("Print a shell template or manifest for local external agent hook ingest").option("--source <source>", "External agent runtime source label").option("--command <command>", "OpenTag CLI command to use in the template").option("--format <format>", "Template format: shell or manifest").action(runCliAction(runIngestTemplateCommand));
4837
5097
  var serviceCommand = program.command("service").description("Install and control the OpenTag background service");
4838
- serviceCommand.command("install").description("Install the OpenTag user LaunchAgent").option("--config <path>", "Config file path").option("--max-request-body-bytes <bytes>", "Persist dispatcher request body limit in the LaunchAgent").option("--rate-limit-window-ms <ms>", "Persist dispatcher rate-limit window in the LaunchAgent").option("--rate-limit-max-requests <n>", "Persist dispatcher rate-limit max requests in the LaunchAgent").option("--rate-limit-disabled", "Persist an explicit disabled dispatcher rate-limit state in the LaunchAgent").action(runCliAction(runServiceInstallCommand));
5098
+ serviceCommand.command("install").description("Install the OpenTag background service").option("--config <path>", "Config file path").option("--max-request-body-bytes <bytes>", "Persist dispatcher request body limit in the service definition").option("--rate-limit-window-ms <ms>", "Persist dispatcher rate-limit window in the service definition").option("--rate-limit-max-requests <n>", "Persist dispatcher rate-limit max requests in the service definition").option("--rate-limit-disabled", "Persist an explicit disabled dispatcher rate-limit state in the service definition").action(runCliAction(runServiceInstallCommand));
4839
5099
  serviceCommand.command("start").description("Start the OpenTag background service").option("--config <path>", "Config file path").action(runCliAction(runServiceStartCommand));
4840
5100
  serviceCommand.command("stop").description("Stop the OpenTag background service").option("--config <path>", "Config file path").action(runCliAction(runServiceStopCommand));
4841
5101
  serviceCommand.command("restart").description("Restart the OpenTag background service").option("--config <path>", "Config file path").action(runCliAction(runServiceRestartCommand));