Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/USAGE.en-GB.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,16 @@ cbth daemon stop

Mutating job, batch, task, and maintenance commands route through the same-user daemon by default. Read-only inspection commands read the local store directly.

Service autostart commands:

```bash
cbth service install
cbth service status
cbth service uninstall
```

On macOS, `service install` writes and loads a user LaunchAgent under `~/Library/LaunchAgents` that starts `cbth --home <path> service run` at login. `service status --json` reports the LaunchAgent plist path, launchd target, load state, and generated program arguments. Linux user systemd support remains a documented future shape.

## Desktop Bridge Commands

Desktop bridge commands currently expose operator/helper surfaces, not an enabled Desktop automatic delivery path.
Expand Down
10 changes: 10 additions & 0 deletions docs/USAGE.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,16 @@ cbth daemon stop

Mutating job、batch、task 和 maintenance commands 默认通过同用户 daemon 执行。Read-only inspection commands 直接读取本地 store。

Service 自启动命令:

```bash
cbth service install
cbth service status
cbth service uninstall
```

在 macOS 上,`service install` 会在 `~/Library/LaunchAgents` 下写入并加载用户级 LaunchAgent,登录时启动 `cbth --home <path> service run`。`service status --json` 会报告 LaunchAgent plist 路径、launchd target、加载状态和生成的 program arguments。Linux user systemd support 仍是文档化的后续形状。

## Desktop Bridge Commands

Desktop bridge commands 当前只是 operator/helper surfaces,不是已启用的 Desktop 自动投递路径。
Expand Down
9 changes: 7 additions & 2 deletions docs/design/HOST_PLUGIN_RUNTIME_AND_DELIVERY.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,18 +147,23 @@ service 只接受已通过 `plugin.hello` 且仍匹配当前 supervisor process

### Install And Autostart

`cbth service` 后续应提供
`cbth service` 的 macOS v1 install/manage surface 提供

```text
cbth service install
cbth service uninstall
cbth service status
```

后续 plugin lifecycle surface 继续预留:

```text
cbth plugin enable <name>
cbth plugin disable <name>
cbth plugin status <name>
```

macOS v1 用 `LaunchAgent` 登录自启动。Linux 后续可加 user `systemd`。`LaunchAgent` 应启动 `cbth service run`,而不是直接启动 Webex worker。
macOS v1 用 `LaunchAgent` 登录自启动。Linux 后续可加 user `systemd`。`LaunchAgent` 启动 `cbth service run`,而不是直接启动 Webex worker。

### Crash Recovery

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
id: 20260519-99aaa7f-c6-service-install-manage
title: C6 Service Install Manage
status: completed
created: 2026-05-19
updated: 2026-05-19
branch: codex/c6-service-install-manage
pr: https://github.com/JoeyTeng/codex-background-task-handler/pull/92
supersedes:
superseded_by:
---

# C6 Service Install Manage

## Summary

- C6 在 `cbth` 中增加 operator-facing `cbth service install`、`cbth service uninstall` 和 `cbth service status`。
- macOS v1 使用用户级 LaunchAgent 登录自启动,plist 启动 `cbth --home <path> service run`,不会直接启动 Webex worker 或任何 plugin worker。
- Linux user systemd 仍保留为设计文档中的后续形状,本 PR 不引入 systemd runtime 管理。

## Current State

- `cbth service install` 会写入 `~/Library/LaunchAgents/com.joeyteng.cbth.service.plist`,必要时 reload 已加载的 LaunchAgent,并在 `launchctl print` 确认加载后才返回成功。
- install 流程会在 bootstrap 前 `launchctl enable gui/<uid>/com.joeyteng.cbth.service`,用于恢复曾被用户或系统 disabled 的登录自启动;fresh install 下若 launchd 尚未认识该 label,则在 bootstrap 成功后再次 enable。
- LaunchAgent plist 中的 `ProgramArguments`、stdout log 和 stderr log 路径都会写成绝对路径,避免 launchd 工作目录影响相对 `CBTH_HOME`。
- `cbth service uninstall` 会 best-effort `launchctl bootout` 用户 LaunchAgent,并删除 plist。
- `cbth service status` 不启动 service,只报告 plist path、launchd target、load state 和生成的 program arguments;macOS headless / SSH 场景中缺少 `gui/<uid>` launchd domain 时按 not-loaded 处理;非 macOS 平台返回 unsupported/future-shape status。

## Next Steps

- C7 继续负责 plugin release manager、shadow/quiesce/drain/promote/rollback 和可选 handoff hooks;C6 不修改这些 lifecycle upgrade internals。
- 后续 Linux user systemd 支持应沿用同一 operator surface,但在新的平台 slice 中实现。

## Evidence

- Base branch head: `99aaa7f`
- Branch: `codex/c6-service-install-manage`
- PR: https://github.com/JoeyTeng/codex-background-task-handler/pull/92
- Validation:
- `cargo fmt --check`
- `cargo test --lib service::tests -- --test-threads=1`
- `cargo test --test cli_help -- --test-threads=1`
- `cargo clippy --locked --all-targets -- -D warnings`
- `cargo test --locked`
- `uv run /Users/hoteng/.codex/skills/project-journal/scripts/project_journal.py validate --repo .`
- Local review:
- helper-backed `codex-review` stateful live-snapshot review: no blocking findings.
- frozen-diff review on `99aaa7f..042aef6` found LaunchAgent log path normalization issue; fixed before final PR readiness rerun.
- online `codex/review-gate` on `042aef6` found missing `launchctl enable`; fixed before final PR readiness rerun.
- frozen-diff review on `99aaa7f..5a1593c` found missing launchd-domain handling for status/uninstall; fixed before final PR readiness rerun.
62 changes: 61 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ use crate::models::{
use crate::self_update::{
SelfUpdateOptions, current_release_target_triple, run_self_update, run_self_update_interactive,
};
use crate::service::{ServiceRunOptions, service_run, status_report, write_status_human};
use crate::service::{
ServiceRunOptions, service_install, service_run, service_status, service_uninstall,
status_report, write_service_management_human, write_status_human,
};
use crate::store::{Store, new_id};

const MAX_METADATA_BYTES: u64 = 1024 * 1024;
Expand Down Expand Up @@ -2300,10 +2303,34 @@ enum DaemonCommand {

#[derive(Debug, Subcommand)]
enum ServiceCommand {
#[command(about = "Install and load the cbth macOS LaunchAgent")]
Install(ServiceInstallArgs),
#[command(about = "Unload and remove the cbth macOS LaunchAgent")]
Uninstall(ServiceUninstallArgs),
#[command(about = "Inspect cbth service autostart state")]
Status(ServiceStatusArgs),
#[command(about = "Run the host plugin supervisor in the foreground")]
Run(ServiceRunArgs),
}

#[derive(Debug, Args)]
struct ServiceInstallArgs {
#[arg(long, help = "Emit JSON output")]
json: bool,
}

#[derive(Debug, Args)]
struct ServiceUninstallArgs {
#[arg(long, help = "Emit JSON output")]
json: bool,
}

#[derive(Debug, Args)]
struct ServiceStatusArgs {
#[arg(long, help = "Emit JSON output")]
json: bool,
}

#[derive(Debug, Args)]
struct ServiceRunArgs {
#[arg(long, hide = true)]
Expand Down Expand Up @@ -2445,6 +2472,27 @@ pub fn run() -> Result<()> {
write_status_human(&report)?;
return Ok(());
}
Commands::Service {
command: ServiceCommand::Install(args),
} if !args.json => {
let report = service_install(&layout)?;
write_service_management_human(&report)?;
return Ok(());
}
Commands::Service {
command: ServiceCommand::Uninstall(args),
} if !args.json => {
let report = service_uninstall(&layout)?;
write_service_management_human(&report)?;
return Ok(());
}
Commands::Service {
command: ServiceCommand::Status(args),
} if !args.json => {
let report = service_status(&layout)?;
write_service_management_human(&report)?;
return Ok(());
}
Commands::New(args) => {
if cli.direct_store {
bail!("new does not support --direct-store");
Expand Down Expand Up @@ -13713,6 +13761,18 @@ fn dispatch_daemon(command: DaemonCommand, layout: &FsLayout) -> Result<Value> {

fn dispatch_service(command: ServiceCommand, layout: &FsLayout) -> Result<Value> {
match command {
ServiceCommand::Install(_) => {
let report = service_install(layout)?;
Ok(json!({ "service_install": report }))
}
ServiceCommand::Uninstall(_) => {
let report = service_uninstall(layout)?;
Ok(json!({ "service_uninstall": report }))
}
ServiceCommand::Status(_) => {
let report = service_status(layout)?;
Ok(json!({ "service_status": report }))
}
ServiceCommand::Run(args) => service_run(
layout,
ServiceRunOptions {
Expand Down
Loading
Loading