Skip to content

Commit fe834a2

Browse files
authored
Fix packaged Local Worker startup and add CLI lifecycle controls (#195)
* fix: start packaged local worker * feat: add CLI local worker lifecycle controls * docs: update changelog for local worker lifecycle
1 parent 50eaccd commit fe834a2

29 files changed

Lines changed: 875 additions & 53 deletions

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
3434
- Task: prompt templates for task requirement prefix injection (Global + Project scopes). (#71)
3535
- Sync-first: multi-client snapshot + revision + SSE `/events` for the worker control surface (Desktop/Web/CLI). (#122)
3636
- Worker: PTY session streaming over the control surface (`WS /pty`) + ticket→cookie web auth for the Worker Web Shell. (#133)
37+
- CLI: Local Worker lifecycle controls with `worker status --all` and ownership-safe `worker stop`. (#195)
3738

3839
### 💅 Changed
39-
- Worker: packaged Desktop is now local-only for Home Worker, auto-repairs legacy standalone/remote configs on launch, recovers cleanly if local-worker startup fails, and boots the packaged local worker without Electron-only runtime imports. (#162)
40+
- Worker: packaged Desktop now uses Local Worker mode, auto-repairs legacy standalone/remote configs on launch, recovers cleanly if local-worker startup fails, and boots the packaged local worker without Electron-only runtime imports. (#162, #195)
4041
- Settings: allow auto-focus to center within the visible canvas (accounts for the primary sidebar). (#166)
4142
- Workspace canvas: context menus now stay near the pointer, only flip on real overflow, and reorder note/space actions for faster access. (#64)
4243
- What's New: switched update notes from runtime GitHub compare fetching to release-manifest delivery embedded in each build. (#67)

DEVELOPMENT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@
176176
## 常见问题(FAQ)
177177

178178
- **为什么我用 `pnpm dev` 启动后,Worker/Web UI 相关功能表现很奇怪(恢复/同步/持久化不生效)?**
179-
- 先检查是否启用了 Home Worker(userData 目录下的 `home-worker.json``mode=local`,或环境变量 `OPENCOVE_WORKER_CLIENT=1`)。
179+
- 先检查是否启用了本机 Worker 模式(userData 目录下的 `home-worker.json``mode=local`,或环境变量 `OPENCOVE_WORKER_CLIENT=1`)。
180180
- 然后执行一次 `pnpm build` 并重启 App,确保 `out/main/worker.js` 与当前源码一致。
181181

182182
## 文档地图(按问题找入口)

README_ZH.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ pnpm dev
104104

105105
OpenCove 提供一个**实验性的 Worker Web UI**,允许你用浏览器打开画布(包括在同一内网的其他设备,例如平板)。
106106

107-
-**Settings → Experimental → Worker Web UI** 中手动开启 **Enable Web UI**(可选:设置固定端口),再启动 Local Worker。
107+
-**Settings → Experimental → Worker Web UI** 中手动开启 **Enable Web UI**(可选:设置固定端口),再启动本机 Worker。
108108
- 默认只监听本机回环地址(`127.0.0.1`)。如需内网访问,开启 **LAN Access** 并设置 Web UI 密码。
109109
- 开发提示:LAN 访问会使用 `out/renderer` 的 build 产物(无 HMR)。修改 UI 后需要先跑 `pnpm build` 再刷新。
110110

docs/CLI.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,33 @@ CLI 的默认运行模型应满足:
2020

2121
> 具体 transport(IPC vs HTTP/WS)以实现为准,但必须满足上述约束与 `docs/ARCHITECTURE.md` 的边界规则。
2222
23-
## 3. 命令设计规范
23+
## 3. Worker 生命周期命令
24+
25+
Worker 生命周期属于运行时进程管理问题。参考 [Docker CLI `container stop`](https://docs.docker.com/reference/cli/docker/container/stop/) 的成熟心智:停止命令必须指向明确目标,优先 graceful stop,必要时才强制结束。
26+
27+
OpenCove 的本地转译规则:
28+
29+
- **拓扑名**:跑在本机的 worker 统一叫 **Local Worker / 本机 Worker**;不要在用户文案里引入 Home Worker。
30+
- **生命周期 owner**:connection file 可标记 `startedBy=cli | desktop`;旧 connection file 缺失时视为 `unknown`
31+
- **默认 stop 规则**`opencove worker stop` 只停止 `startedBy=cli` 的本机 Worker。
32+
- **保护 Desktop-owned worker**:若目标是 `startedBy=desktop``unknown`,CLI 默认拒绝;只有显式 `--force` 才允许停止。
33+
- **目标选择**:如果发现多个本机 Worker,CLI 必须要求 `--user-data <dir>``--pid <pid>`,禁止猜测。
34+
- **发现范围**:CLI lifecycle 命令只读取 worker connection file(`worker-control-surface.json`),不把 Desktop 自己的 control surface 当成 Worker。
35+
- **重复启动**:同一 `userData` 下重复执行 `opencove worker start` 不会创建第二个 Worker;Worker 会输出现有 connection 并退出。
36+
37+
关键不变量:
38+
39+
1. CLI 默认不得误停 Desktop 启动/拥有的 Local Worker。
40+
2. 同一 `userData` 下最多一个 Worker;跨 `userData` 的多个 Worker 必须显式选目标。
41+
3. Stop 先发送 `SIGTERM`,超时且用户显式 `--force` 时才允许升级为强制结束。
42+
43+
## 4. 命令设计规范
2444

2545
- 子命令必须按业务领域分组(例如 `space/*``session/*``worktree/*``fs/*`),避免“一把梭”命令集。
2646
- `query` 类命令默认 `--json` 或直接 JSON 输出;人类可读输出必须可选(例如 `--pretty`)。
2747
- `command` 类命令必须明确其副作用与 scope(例如空间/挂载/工作目录的解析必须由 usecase/Control Surface 返回,CLI 不得自行猜测 cwd 规则)。
2848

29-
## 4. 对贡献者:扩展 CLI 的正确方式
49+
## 5. 对贡献者:扩展 CLI 的正确方式
3050

3151
当你需要新增一个 CLI 能力:
3252

@@ -35,4 +55,3 @@ CLI 的默认运行模型应满足:
3555
3. 最后在 CLI 层只做参数解析与输出格式化,不写业务规则。
3656

3757
这样才能保证未来 Web/Remote 复用同一条链路,而不是每个 client 各自实现一套语义。
38-

src/app/cli/constants.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export const CONTROL_SURFACE_CONNECTION_FILE = 'control-surface.json'
2+
export const WORKER_CONTROL_SURFACE_CONNECTION_FILE = 'worker-control-surface.json'
23
export const DEFAULT_TIMEOUT_MS = 20_000
34
export const CONTROL_SURFACE_PROTOCOL_VERSION = 1

src/app/cli/opencove.mjs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ import { invokeAndPrint, invokeControlSurface } from './invoke.mjs'
1111
import { printUsage } from './usage.mjs'
1212
import { CONTROL_SURFACE_PROTOCOL_VERSION } from './constants.mjs'
1313
import { tryHandleMultiEndpointCommands } from './commands/multiEndpoint.mjs'
14+
import {
15+
getWorkerLifecycleStatus,
16+
printWorkerLifecycleResult,
17+
stopWorkerLifecycle,
18+
WorkerLifecycleError,
19+
} from './workerLifecycle.mjs'
1420

1521
function toErrorMessage(error) {
1622
if (error instanceof Error) {
@@ -78,6 +84,8 @@ async function main() {
7884
workerArgs.push('--hostname', hostname)
7985
}
8086

87+
workerArgs.push('--started-by', 'cli')
88+
8189
if (advertiseHostname) {
8290
workerArgs.push('--advertise-hostname', advertiseHostname)
8391
}
@@ -143,6 +151,41 @@ async function main() {
143151
return
144152
}
145153

154+
if (command === 'worker' && args[1] === 'status' && !endpoint) {
155+
const status = await getWorkerLifecycleStatus({
156+
all: args.includes('--all'),
157+
userData: readFlagValue(args, '--user-data'),
158+
timeoutMs,
159+
})
160+
printWorkerLifecycleResult(status, pretty)
161+
return
162+
}
163+
164+
if (command === 'worker' && args[1] === 'stop') {
165+
if (endpoint) {
166+
process.stderr.write('[opencove] worker stop only supports local workers.\n')
167+
process.exit(2)
168+
}
169+
170+
try {
171+
const result = await stopWorkerLifecycle({
172+
userData: readFlagValue(args, '--user-data'),
173+
pid: readFlagValue(args, '--pid'),
174+
force: args.includes('--force'),
175+
timeoutMs,
176+
})
177+
printWorkerLifecycleResult(result, pretty)
178+
return
179+
} catch (error) {
180+
if (error instanceof WorkerLifecycleError) {
181+
process.stderr.write(`${error.message}\n`)
182+
process.exit(2)
183+
}
184+
185+
throw error
186+
}
187+
}
188+
146189
const capabilitiesRequest = { kind: 'query', id: 'system.capabilities', payload: null }
147190

148191
const connection = endpoint

src/app/cli/usage.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ export function printUsage() {
5050
` opencove worker start [--hostname <bindHost>] [--advertise-hostname <host>] [--port <port>] [--user-data <dir>] [--token <token>] [--web-ui-password-hash <hash>] [--approve-root <path>]\n`,
5151
)
5252
process.stdout.write(
53-
` opencove worker status [--endpoint <url>] [--token <token>] [--pretty]\n\n`,
53+
` opencove worker status [--all] [--user-data <dir>] [--endpoint <url>] [--token <token>] [--pretty]\n`,
54+
)
55+
process.stdout.write(
56+
` opencove worker stop [--user-data <dir>] [--pid <pid>] [--force] [--pretty]\n\n`,
5457
)
5558
process.stdout.write(`Global Options:\n`)
5659
process.stdout.write(` --pretty Pretty-print JSON output\n`)

0 commit comments

Comments
 (0)