lark-remote 0.1.13 → 0.2.1

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.
Files changed (132) hide show
  1. package/CHANGELOG.md +36 -3
  2. package/README.en.md +7 -11
  3. package/README.md +7 -11
  4. package/dist/bridge/final-usage.d.ts +24 -0
  5. package/dist/bridge/final-usage.js +55 -0
  6. package/dist/bridge/index.d.ts +0 -7
  7. package/dist/bridge/index.js +22 -64
  8. package/dist/bridge/queue-manager.js +4 -3
  9. package/dist/card/approval-render.js +39 -94
  10. package/dist/card/bash-renderer.js +12 -21
  11. package/dist/card/card-shared.d.ts +13 -1
  12. package/dist/card/card-shared.js +21 -30
  13. package/dist/card/run-renderer.d.ts +0 -22
  14. package/dist/card/run-renderer.js +206 -153
  15. package/dist/card/run-state.d.ts +29 -1
  16. package/dist/card/run-state.js +172 -84
  17. package/dist/card/text-truncate.d.ts +0 -9
  18. package/dist/card/text-truncate.js +0 -9
  19. package/dist/card/tool-render.d.ts +4 -1
  20. package/dist/card/tool-render.js +76 -32
  21. package/dist/cli.js +2 -2
  22. package/dist/common/fs.d.ts +14 -0
  23. package/dist/common/fs.js +57 -3
  24. package/dist/config/codex-config.js +2 -0
  25. package/dist/config/index.d.ts +0 -5
  26. package/dist/config/index.js +0 -11
  27. package/dist/config/kimi-config.js +1 -0
  28. package/dist/config/opencode-config.js +13 -2
  29. package/dist/config/qr.d.ts +36 -0
  30. package/dist/config/qr.js +124 -0
  31. package/dist/config/wizard.js +69 -27
  32. package/dist/connector/index.d.ts +3 -2
  33. package/dist/connector/index.js +5 -4
  34. package/dist/index.js +15 -42
  35. package/dist/instance-lock.js +7 -0
  36. package/dist/persistence/atomic-write.d.ts +2 -0
  37. package/dist/persistence/atomic-write.js +20 -5
  38. package/dist/persistence/load-json-file.d.ts +1 -2
  39. package/dist/persistence/load-json-file.js +2 -3
  40. package/dist/platform/agent-stopper.d.ts +22 -0
  41. package/dist/platform/agent-stopper.js +19 -0
  42. package/dist/platform/command.d.ts +11 -0
  43. package/dist/platform/command.js +107 -0
  44. package/dist/platform/identity.d.ts +33 -0
  45. package/dist/platform/identity.js +187 -0
  46. package/dist/platform/path.d.ts +24 -0
  47. package/dist/platform/path.js +110 -0
  48. package/dist/platform/select.d.ts +9 -0
  49. package/dist/platform/select.js +12 -0
  50. package/dist/platform/shell.d.ts +51 -0
  51. package/dist/platform/shell.js +75 -0
  52. package/dist/platform/spawn.d.ts +41 -0
  53. package/dist/platform/spawn.js +77 -0
  54. package/dist/platform/terminator-posix.d.ts +38 -0
  55. package/dist/platform/terminator-posix.js +103 -0
  56. package/dist/platform/terminator-win32.d.ts +21 -0
  57. package/dist/platform/terminator-win32.js +110 -0
  58. package/dist/platform/terminator.d.ts +44 -0
  59. package/dist/platform/terminator.js +24 -0
  60. package/dist/platform/types.d.ts +23 -0
  61. package/dist/platform/types.js +5 -0
  62. package/dist/restart.js +4 -2
  63. package/dist/router/card-helpers.d.ts +33 -6
  64. package/dist/router/card-helpers.js +67 -11
  65. package/dist/router/config/common/render.d.ts +1 -1
  66. package/dist/router/config/common/render.js +1 -1
  67. package/dist/router/config-diff.d.ts +4 -0
  68. package/dist/router/config-diff.js +70 -0
  69. package/dist/router/index.d.ts +66 -6
  70. package/dist/router/index.js +451 -316
  71. package/dist/router/utils.d.ts +6 -0
  72. package/dist/router/utils.js +12 -0
  73. package/dist/runner/bash/runner.d.ts +9 -2
  74. package/dist/runner/bash/runner.js +45 -20
  75. package/dist/runner/claude/session.d.ts +4 -1
  76. package/dist/runner/claude/session.js +18 -4
  77. package/dist/runner/codex/app-server/protocol-types.d.ts +35 -1
  78. package/dist/runner/codex/app-server/translator.d.ts +18 -0
  79. package/dist/runner/codex/app-server/translator.js +146 -0
  80. package/dist/runner/common/acp/base-acp-runner.d.ts +116 -0
  81. package/dist/runner/common/acp/base-acp-runner.js +248 -0
  82. package/dist/runner/common/acp/base-translator.d.ts +141 -0
  83. package/dist/runner/common/acp/base-translator.js +271 -0
  84. package/dist/runner/common/acp/protocol-helpers.d.ts +39 -0
  85. package/dist/runner/common/acp/protocol-helpers.js +34 -0
  86. package/dist/runner/common/jsonrpc/transport.d.ts +2 -0
  87. package/dist/runner/common/jsonrpc/transport.js +25 -5
  88. package/dist/runner/common/process-stopper.d.ts +4 -26
  89. package/dist/runner/common/process-stopper.js +4 -76
  90. package/dist/runner/common/runner-utils.d.ts +0 -5
  91. package/dist/runner/common/runner-utils.js +0 -7
  92. package/dist/runner/common/spawning-runner.d.ts +42 -132
  93. package/dist/runner/common/spawning-runner.js +52 -271
  94. package/dist/runner/common/usage.d.ts +2 -0
  95. package/dist/runner/common/usage.js +2 -0
  96. package/dist/runner/index.d.ts +1 -1
  97. package/dist/runner/kimi/acp/runner.d.ts +45 -42
  98. package/dist/runner/kimi/acp/runner.js +140 -293
  99. package/dist/runner/kimi/acp/translator.d.ts +18 -100
  100. package/dist/runner/kimi/acp/translator.js +56 -242
  101. package/dist/runner/opencode/acp/runner.d.ts +22 -46
  102. package/dist/runner/opencode/acp/runner.js +73 -256
  103. package/dist/runner/opencode/acp/translator.d.ts +28 -103
  104. package/dist/runner/opencode/acp/translator.js +51 -236
  105. package/dist/runner/pi/rpc/translator.d.ts +3 -0
  106. package/dist/runner/pi/rpc/translator.js +47 -9
  107. package/dist/runner/probe.d.ts +8 -11
  108. package/dist/runner/probe.js +17 -21
  109. package/dist/runner/registry.d.ts +4 -4
  110. package/dist/runner/registry.js +4 -4
  111. package/dist/runner/types.d.ts +38 -1
  112. package/dist/session/claude/sessions.js +35 -58
  113. package/dist/session/codex/rollout-reader.d.ts +10 -0
  114. package/dist/session/codex/rollout-reader.js +123 -41
  115. package/dist/session/common/constants.d.ts +7 -0
  116. package/dist/session/common/constants.js +9 -0
  117. package/dist/session/common/two-pass.d.ts +22 -0
  118. package/dist/session/common/two-pass.js +70 -0
  119. package/dist/session/dsh/sessions.js +7 -6
  120. package/dist/session/kimi/sessions.d.ts +6 -0
  121. package/dist/session/kimi/sessions.js +50 -73
  122. package/dist/session/opencode/sessions.js +26 -8
  123. package/dist/session/pi/sessions.js +54 -73
  124. package/dist/update/index.d.ts +1 -2
  125. package/dist/update/index.js +1 -2
  126. package/dist/update/install.d.ts +2 -1
  127. package/dist/update/install.js +58 -9
  128. package/dist/update/version-check.d.ts +5 -0
  129. package/dist/update/version-check.js +9 -0
  130. package/package.json +9 -6
  131. package/dist/update/startup-hint.d.ts +0 -5
  132. package/dist/update/startup-hint.js +0 -11
@@ -0,0 +1,51 @@
1
+ /**
2
+ * ShellBackend:把 `!` 命令与 kimi ACP terminal 的 shell 依赖收进 seam
3
+ * (design.md §7.2)。
4
+ *
5
+ * - posix:`bash -c <command>`(与现状 BashProcessRunner 同语义);
6
+ * - win32:默认 Git Bash(`bash.exe -c <command>`),powershell / cmd 仅作为
7
+ * 显式用户选择 §7.3;
8
+ * - 首版不做命令翻译器:Git Bash 缺失时抛 {@link ShellUnavailableError},
9
+ * 由调用方转成明确错误卡(提示需要 Git Bash)。
10
+ *
11
+ * kimi ACP 的 terminal/create 固定 `bash -c`,因此 Git Bash 不是 `!` 的专属
12
+ * 需求,而是 kimi agent 在 Windows 上可用的前置条件(§4.4)。
13
+ */
14
+ import type { ChildProcess, SpawnOptions } from 'node:child_process';
15
+ import { type ResolveOptions, type LaunchSpec } from './command.js';
16
+ export type ShellKind = 'bash' | 'powershell' | 'cmd';
17
+ export interface ShellBackend {
18
+ readonly kind: ShellKind;
19
+ /**
20
+ * 执行 command。`options` 透传给底层 spawn(如 stdio/detached);
21
+ * win32 会强制附加 `windowsHide: true`,调用方无法覆盖。
22
+ */
23
+ spawn(command: string, opts: {
24
+ cwd: string;
25
+ options?: SpawnOptions;
26
+ }): ChildProcess;
27
+ }
28
+ /** shell 不可用(如 win32 上没装 Git Bash):调用方应转成明确错误卡。 */
29
+ export declare class ShellUnavailableError extends Error {
30
+ readonly kind: ShellKind;
31
+ constructor(kind: ShellKind, message: string);
32
+ }
33
+ export type SpawnLike = (file: string, args: readonly string[], opts?: SpawnOptions) => ChildProcess;
34
+ export interface ShellBackendDeps {
35
+ /** 平台注入(测试用);默认当前宿主平台 */
36
+ platform?: NodeJS.Platform;
37
+ /** win32 下的后端选择:默认 bash(Git Bash) */
38
+ kind?: ShellKind;
39
+ /** spawn 注入(测试用);默认 platform/spawn 的 cross-spawn 包装 */
40
+ spawn?: SpawnLike;
41
+ /** PATH 注入(测试 / 守护 env 用);win32 解析 bash.exe 时透传 */
42
+ pathEnv?: string;
43
+ /** 命令解析注入(测试用);默认 resolveExecutable */
44
+ resolve?: (name: string, opts?: ResolveOptions) => LaunchSpec | null;
45
+ }
46
+ /** posix bash 后端:`bash -c <command>`。 */
47
+ export declare function createBashShellBackend(deps?: ShellBackendDeps): ShellBackend;
48
+ /** win32 后端:默认 Git Bash,powershell/cmd 为显式用户选择。 */
49
+ export declare function createWin32ShellBackend(deps?: ShellBackendDeps): ShellBackend;
50
+ /** 平台分发的唯一入口:posix → bash,win32 → 默认 Git Bash。 */
51
+ export declare function createShellBackend(deps?: ShellBackendDeps): ShellBackend;
@@ -0,0 +1,75 @@
1
+ import { currentPlatform, isWin32 } from './select.js';
2
+ import { resolveExecutable } from './command.js';
3
+ import { spawnProcess } from './spawn.js';
4
+ /** shell 不可用(如 win32 上没装 Git Bash):调用方应转成明确错误卡。 */
5
+ export class ShellUnavailableError extends Error {
6
+ kind;
7
+ constructor(kind, message) {
8
+ super(message);
9
+ this.name = 'ShellUnavailableError';
10
+ this.kind = kind;
11
+ }
12
+ }
13
+ /** posix bash 后端:`bash -c <command>`。 */
14
+ export function createBashShellBackend(deps = {}) {
15
+ const spawnFn = deps.spawn ?? spawnProcess;
16
+ return {
17
+ kind: 'bash',
18
+ spawn(command, opts) {
19
+ return spawnFn('bash', ['-c', command], { cwd: opts.cwd, ...opts.options });
20
+ },
21
+ };
22
+ }
23
+ /** win32 后端:默认 Git Bash,powershell/cmd 为显式用户选择。 */
24
+ export function createWin32ShellBackend(deps = {}) {
25
+ const spawnFn = deps.spawn ?? spawnProcess;
26
+ const kind = deps.kind ?? 'bash';
27
+ if (kind === 'powershell') {
28
+ return {
29
+ kind,
30
+ spawn(command, opts) {
31
+ return spawnFn('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', command], {
32
+ cwd: opts.cwd,
33
+ ...opts.options,
34
+ windowsHide: true,
35
+ });
36
+ },
37
+ };
38
+ }
39
+ if (kind === 'cmd') {
40
+ return {
41
+ kind,
42
+ spawn(command, opts) {
43
+ return spawnFn('cmd.exe', ['/d', '/s', '/c', command], {
44
+ cwd: opts.cwd,
45
+ ...opts.options,
46
+ windowsHide: true,
47
+ });
48
+ },
49
+ };
50
+ }
51
+ return {
52
+ kind: 'bash',
53
+ spawn(command, opts) {
54
+ const resolve = deps.resolve ?? resolveExecutable;
55
+ const spec = resolve('bash', { platform: 'win32', pathEnv: deps.pathEnv });
56
+ if (!spec) {
57
+ throw new ShellUnavailableError('bash', 'Windows 上执行 bash 命令需要 Git Bash(未在 PATH 中找到 bash.exe);' +
58
+ '请安装 Git for Windows 后重试');
59
+ }
60
+ // 执行语义由 cross-spawn 负责(垫片 cmd.exe 受控执行);这里只保证
61
+ // bash 存在性并给出明确的「需要 Git Bash」错误
62
+ return spawnFn(spec.file, ['-c', command], {
63
+ cwd: opts.cwd,
64
+ ...opts.options,
65
+ windowsHide: true,
66
+ });
67
+ },
68
+ };
69
+ }
70
+ /** 平台分发的唯一入口:posix → bash,win32 → 默认 Git Bash。 */
71
+ export function createShellBackend(deps = {}) {
72
+ const platform = deps.platform ?? currentPlatform;
73
+ return isWin32(platform) ? createWin32ShellBackend(deps) : createBashShellBackend(deps);
74
+ }
75
+ //# sourceMappingURL=shell.js.map
@@ -0,0 +1,41 @@
1
+ import type { ChildProcess, SpawnOptions, SpawnSyncOptions } from 'node:child_process';
2
+ import type { SpawnSyncReturns } from 'child_process';
3
+ /**
4
+ * cross-spawn 包装:posix 直通,win32 PATHEXT + 垫片 cmd.exe 受控执行。
5
+ *
6
+ * 默认注入 `windowsHide: true`:win32 上 .cmd/.bat 垫片经 cmd.exe 执行时,
7
+ * 不加该选项会闪控制台窗口(测试批量拉起 mock agent 时尤其明显)。该选项
8
+ * 在 posix 上被 Node 直接忽略(Windows-only),因此默认值零跨平台影响;
9
+ * 调用方显式传 `windowsHide: false` 仍可覆盖。
10
+ */
11
+ export declare function spawnProcess(command: string, args?: readonly string[], options?: SpawnOptions): ChildProcess;
12
+ /** cross-spawn 同步版(一次性命令探测/目录查询用)。默认注入同 {@link spawnProcess}。 */
13
+ export declare function spawnProcessSync(command: string, args?: readonly string[], options?: SpawnSyncOptions): SpawnSyncReturns<string | Buffer>;
14
+ /**
15
+ * env 覆盖合并:先按不区分大小写删除 base 中的同名旧键,再设新值。
16
+ * Windows env 键大小写不敏感,`{...process.env, PATH:x}` 会产生 `PATH`/`Path`
17
+ * 双键(§8.3)。注意 posix 上并非普通 spread:base 中大小写不同名的键(如
18
+ * `Path`)也会被删除,而非与 `PATH` 并存——可接受(posix 语义键为全大写
19
+ * `PATH`,大小写变体本身就不是语义键),但语义与 spread 不同,勿混淆。
20
+ */
21
+ export declare function mergeProcessEnv(base?: NodeJS.ProcessEnv, overrides?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
22
+ /**
23
+ * 是否需要为子进程建独立进程组(`spawn` 的 `detached: true`)。
24
+ *
25
+ * posix:ProcessStopper 的组杀用负 PID(`kill(-pgid)`),子进程必须是组长,
26
+ * 否则 kill 抛 ESRCH 被吞,停止/清理链路全失效。
27
+ *
28
+ * win32:组杀已由 terminator 的 `taskkill /PID <pid> /T /F` 树杀替代,不需要
29
+ * 进程组;而 `detached: true` 会让 **`.cmd` 垫片丢掉父进程的 stdio 管道** ——
30
+ * Windows 上 `DETACHED_PROCESS` 下 cmd.exe 另建控制台并重绑标准句柄,于是
31
+ * 子进程(npm 安装的 agent 全是 `.cmd` 垫片)的 stdout/stderr 永不抵达父进程,
32
+ * 表现为 agent 全程静默、turn 超时。实测 `windowsHide: true` 不能改善,
33
+ * 因此 v1 §3.3「保留 detached + windowsHide」的假设据此修正为「win32 不
34
+ * detached」。调用方应同时传 `windowsHide: true`,避免 cmd.exe 闪控制台窗口。
35
+ */
36
+ export declare function useDetachedProcessGroup(platform?: NodeJS.Platform): boolean;
37
+ /**
38
+ * win32 command-not-found 行嗅探(§4.4):命中即视为「命令缺失」失败而非
39
+ * 普通非零退出。platform 注入使其在任意宿主可测。
40
+ */
41
+ export declare function isWindowsCommandNotFoundLine(line: string, platform?: NodeJS.Platform): boolean;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * spawn 统一收口。
3
+ *
4
+ * 全部子进程拉起必须走本模块,禁止直接 child_process.spawn:
5
+ * - cross-spawn 在 posix 是 child_process.spawn 直通(零行为差异);win32 上
6
+ * 做 PATHEXT 解析,.cmd/.bat 经 cmd.exe 以正确的引号转义执行——取代 v1 的
7
+ * 手写垫片解析层;
8
+ * - mergeProcessEnv:Windows env 键大小写不敏感,直接 `{...process.env, PATH:x}`
9
+ * 在键为 `Path` 的机器上产生双键、子进程行为未定义——任何 env 覆盖注入点
10
+ * 必须经它合并(先按不区分大小写删旧键再设新值);
11
+ * - isWindowsCommandNotFoundLine:win32 经 cmd 垫片启动失败不是 spawn 的
12
+ * error 事件 ENOENT,而是子进程往 stderr 打
13
+ * "'xxx' is not recognized as an internal or external command"(§4.4)。
14
+ */
15
+ import crossSpawn from 'cross-spawn';
16
+ import { currentPlatform, isWin32 } from './select.js';
17
+ /**
18
+ * cross-spawn 包装:posix 直通,win32 PATHEXT + 垫片 cmd.exe 受控执行。
19
+ *
20
+ * 默认注入 `windowsHide: true`:win32 上 .cmd/.bat 垫片经 cmd.exe 执行时,
21
+ * 不加该选项会闪控制台窗口(测试批量拉起 mock agent 时尤其明显)。该选项
22
+ * 在 posix 上被 Node 直接忽略(Windows-only),因此默认值零跨平台影响;
23
+ * 调用方显式传 `windowsHide: false` 仍可覆盖。
24
+ */
25
+ export function spawnProcess(command, args = [], options = {}) {
26
+ return crossSpawn(command, [...args], { windowsHide: true, ...options });
27
+ }
28
+ /** cross-spawn 同步版(一次性命令探测/目录查询用)。默认注入同 {@link spawnProcess}。 */
29
+ export function spawnProcessSync(command, args = [], options = {}) {
30
+ return crossSpawn.sync(command, [...args], { windowsHide: true, ...options });
31
+ }
32
+ /**
33
+ * env 覆盖合并:先按不区分大小写删除 base 中的同名旧键,再设新值。
34
+ * Windows env 键大小写不敏感,`{...process.env, PATH:x}` 会产生 `PATH`/`Path`
35
+ * 双键(§8.3)。注意 posix 上并非普通 spread:base 中大小写不同名的键(如
36
+ * `Path`)也会被删除,而非与 `PATH` 并存——可接受(posix 语义键为全大写
37
+ * `PATH`,大小写变体本身就不是语义键),但语义与 spread 不同,勿混淆。
38
+ */
39
+ export function mergeProcessEnv(base = process.env, overrides = {}) {
40
+ const out = { ...base };
41
+ for (const [key, value] of Object.entries(overrides)) {
42
+ for (const existing of Object.keys(out)) {
43
+ if (existing.toLowerCase() === key.toLowerCase()) {
44
+ delete out[existing];
45
+ }
46
+ }
47
+ if (value !== undefined)
48
+ out[key] = value;
49
+ }
50
+ return out;
51
+ }
52
+ const COMMAND_NOT_FOUND_PATTERN = /is not recognized as an internal or external command|operable program or batch file/i;
53
+ /**
54
+ * 是否需要为子进程建独立进程组(`spawn` 的 `detached: true`)。
55
+ *
56
+ * posix:ProcessStopper 的组杀用负 PID(`kill(-pgid)`),子进程必须是组长,
57
+ * 否则 kill 抛 ESRCH 被吞,停止/清理链路全失效。
58
+ *
59
+ * win32:组杀已由 terminator 的 `taskkill /PID <pid> /T /F` 树杀替代,不需要
60
+ * 进程组;而 `detached: true` 会让 **`.cmd` 垫片丢掉父进程的 stdio 管道** ——
61
+ * Windows 上 `DETACHED_PROCESS` 下 cmd.exe 另建控制台并重绑标准句柄,于是
62
+ * 子进程(npm 安装的 agent 全是 `.cmd` 垫片)的 stdout/stderr 永不抵达父进程,
63
+ * 表现为 agent 全程静默、turn 超时。实测 `windowsHide: true` 不能改善,
64
+ * 因此 v1 §3.3「保留 detached + windowsHide」的假设据此修正为「win32 不
65
+ * detached」。调用方应同时传 `windowsHide: true`,避免 cmd.exe 闪控制台窗口。
66
+ */
67
+ export function useDetachedProcessGroup(platform = currentPlatform) {
68
+ return !isWin32(platform);
69
+ }
70
+ /**
71
+ * win32 command-not-found 行嗅探(§4.4):命中即视为「命令缺失」失败而非
72
+ * 普通非零退出。platform 注入使其在任意宿主可测。
73
+ */
74
+ export function isWindowsCommandNotFoundLine(line, platform = currentPlatform) {
75
+ return platform === 'win32' && COMMAND_NOT_FOUND_PATTERN.test(line);
76
+ }
77
+ //# sourceMappingURL=spawn.js.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * posix 终止器:`src/runner/common/process-stopper.ts` 的原样迁移(design.md §3.2)。
3
+ *
4
+ * 语义与迁移前逐条对齐:SIGTERM 打负 PID 进程组 → 等 graceMs → SIGKILL;
5
+ * immediate 则两个信号连发。本次只新增两样东西:
6
+ * - 返回 {@link TerminateResult} 让终止途经可观测(与 win32 同构);
7
+ * - `cleanupOnExit`(Terminator 接口要求的进程级退出清理)。
8
+ *
9
+ * `ProcessStopper` 保留为兼容壳:行为(含日志文案)零变化,既有调用方与
10
+ * 测试无需改动;M2 调用点迁移时逐个换成 Terminator 后即可删除。
11
+ */
12
+ import type { ChildProcess } from 'node:child_process';
13
+ import type { Terminator } from './terminator.js';
14
+ export type LogLevel = 'debug' | 'info';
15
+ export type TerminatorLogger = (level: LogLevel, message: string) => void;
16
+ export interface PosixTerminatorDeps {
17
+ /** 优雅等待窗口(毫秒),对应现状 stopGraceMs */
18
+ graceMs: number;
19
+ /** 日志注入(测试用);默认 logger 单例 */
20
+ log?: TerminatorLogger;
21
+ }
22
+ export declare function createPosixTerminator(deps: PosixTerminatorDeps): Terminator;
23
+ interface ProcessStopperOptions {
24
+ graceMs: number;
25
+ }
26
+ interface StopOptions {
27
+ immediate?: boolean;
28
+ }
29
+ /**
30
+ * 迁移前的类名(兼容壳):返回 void、接受 null proc。
31
+ * 新代码请用 {@link createPosixTerminator} / `createTerminator`。
32
+ */
33
+ export declare class ProcessStopper {
34
+ private graceMs;
35
+ constructor(opts: ProcessStopperOptions);
36
+ stop(proc: ChildProcess | null, opts?: StopOptions): Promise<void>;
37
+ }
38
+ export {};
@@ -0,0 +1,103 @@
1
+ import { getLogger } from '../logger/index.js';
2
+ /** 默认日志出口:与迁移前同级别(debug 记信号,info 记 grace 超时)。 */
3
+ function defaultLog(level, message) {
4
+ const logger = getLogger();
5
+ if (level === 'debug')
6
+ logger.debug(message);
7
+ else
8
+ logger.info(message);
9
+ }
10
+ /** 退出判定统一口径(CLAUDE.md 红线):exitCode !== null || signalCode !== null。 */
11
+ function isAlive(proc) {
12
+ return proc.exitCode === null && proc.signalCode === null;
13
+ }
14
+ async function stopPosix(proc, opts, log) {
15
+ if (!isAlive(proc)) {
16
+ return { requested: false, via: 'already-exited' };
17
+ }
18
+ const pid = proc.pid;
19
+ if (pid === undefined) {
20
+ return { requested: false, via: 'already-exited' };
21
+ }
22
+ // 负 PID = 整个进程组:shell 包装层与孙进程一并带走
23
+ const pgid = -pid;
24
+ log('debug', `[process-stopper] sending SIGTERM to pgid=${pgid} immediate=${opts.immediate}`);
25
+ try {
26
+ process.kill(pgid, 'SIGTERM');
27
+ }
28
+ catch {
29
+ /* process may have exited */
30
+ }
31
+ if (opts.immediate) {
32
+ // 不等待:SIGKILL 紧随其后;进程已死时 kill 抛 ESRCH,忽略即可
33
+ try {
34
+ process.kill(pgid, 'SIGKILL');
35
+ }
36
+ catch {
37
+ /* process already gone */
38
+ }
39
+ return { requested: true, via: 'taskkill' };
40
+ }
41
+ // 与 exit 事件竞速等 grace;用 once 避免重复 stop() 累积监听器(P2-15)
42
+ let graceTimer;
43
+ const exited = await Promise.race([
44
+ new Promise((resolve) => {
45
+ if (!isAlive(proc))
46
+ return resolve(true);
47
+ proc.once('exit', () => resolve(true));
48
+ }),
49
+ new Promise((resolve) => {
50
+ graceTimer = setTimeout(() => resolve(false), opts.graceMs);
51
+ }),
52
+ ]);
53
+ if (graceTimer)
54
+ clearTimeout(graceTimer);
55
+ if (!exited) {
56
+ log('info', `[process-stopper] process group ${pgid} did not exit within grace period, sending SIGKILL`);
57
+ try {
58
+ process.kill(pgid, 'SIGKILL');
59
+ }
60
+ catch {
61
+ /* process already gone */
62
+ }
63
+ return { requested: true, via: 'taskkill' };
64
+ }
65
+ return { requested: true, via: 'cooperative' };
66
+ }
67
+ export function createPosixTerminator(deps) {
68
+ const log = deps.log ?? defaultLog;
69
+ return {
70
+ stop(proc, opts) {
71
+ return stopPosix(proc, { graceMs: deps.graceMs, immediate: opts.immediate }, log);
72
+ },
73
+ cleanupOnExit(proc) {
74
+ if (!isAlive(proc))
75
+ return;
76
+ const pid = proc.pid;
77
+ if (pid === undefined)
78
+ return;
79
+ try {
80
+ process.kill(-pid, 'SIGKILL');
81
+ }
82
+ catch {
83
+ /* process already gone */
84
+ }
85
+ },
86
+ };
87
+ }
88
+ /**
89
+ * 迁移前的类名(兼容壳):返回 void、接受 null proc。
90
+ * 新代码请用 {@link createPosixTerminator} / `createTerminator`。
91
+ */
92
+ export class ProcessStopper {
93
+ graceMs;
94
+ constructor(opts) {
95
+ this.graceMs = opts.graceMs;
96
+ }
97
+ async stop(proc, opts) {
98
+ if (!proc)
99
+ return;
100
+ await stopPosix(proc, { graceMs: this.graceMs, immediate: opts?.immediate === true }, defaultLog);
101
+ }
102
+ }
103
+ //# sourceMappingURL=terminator-posix.js.map
@@ -0,0 +1,21 @@
1
+ import type { ChildProcess, SpawnOptions } from 'node:child_process';
2
+ import type { Terminator } from './terminator.js';
3
+ import { AgentStopperRegistry } from './agent-stopper.js';
4
+ export type SpawnLike = (file: string, args: string[], opts?: SpawnOptions) => ChildProcess;
5
+ /** 等待 graceMs 与 exit 事件竞速;返回 true 表示等待结束后进程仍存活。 */
6
+ export type WaitForExit = (proc: ChildProcess, ms: number) => Promise<boolean>;
7
+ export interface Win32TerminatorDeps {
8
+ /** 协议停止后的优雅等待窗口(毫秒),对应 posix 的 stopGraceMs */
9
+ graceMs: number;
10
+ /** 当前 run 所属 agent:用于查协议停止通道;缺省视为无通道 */
11
+ agent?: string;
12
+ /** 协议通道注册表注入(测试隔离);默认进程级单例 */
13
+ stoppers?: AgentStopperRegistry;
14
+ /** spawn 注入(测试用);默认 node:child_process */
15
+ spawn?: SpawnLike;
16
+ /** grace 等待器注入(测试用);默认与 exit 事件竞速 */
17
+ waitForExit?: WaitForExit;
18
+ /** 日志注入:本模块处于 seam 底层,不反向依赖 logger 单例 */
19
+ log?: (message: string) => void;
20
+ }
21
+ export declare function createWin32Terminator(deps: Win32TerminatorDeps): Terminator;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * win32 终止器:协议停止通道 → grace 等待 → taskkill 树杀(design.md §3.2)。
3
+ *
4
+ * 状态机:
5
+ * 1. 进程已退出 → nothing(via='already-exited')
6
+ * 2. immediate → taskkill /PID <pid> /T /F
7
+ * 3. 优雅 → AgentStopperRegistry 查协议通道
8
+ * - 无通道:打日志显式跳过优雅段(不等一个不会来的退出事件)→ 直接树杀
9
+ * - 有通道:调用后与 exit 事件竞速等 graceMs,仍存活 → 树杀
10
+ *
11
+ * 依赖(spawn / 等待器 / 日志)全部构造注入,win32 语义可在任意宿主上单测。
12
+ */
13
+ import { spawn as nodeSpawn } from 'node:child_process';
14
+ import { agentStopperRegistry } from './agent-stopper.js';
15
+ /** 退出判定统一口径(CLAUDE.md 红线):exitCode !== null || signalCode !== null。 */
16
+ function isAlive(proc) {
17
+ return proc.exitCode === null && proc.signalCode === null;
18
+ }
19
+ /** 默认等待器:exit 事件与定时器竞速,谁先到算谁。 */
20
+ async function defaultWaitForExit(proc, ms) {
21
+ if (!isAlive(proc))
22
+ return false;
23
+ return new Promise((resolve) => {
24
+ let settled = false;
25
+ const onExit = () => finish(false);
26
+ const finish = (alive) => {
27
+ if (settled)
28
+ return;
29
+ settled = true;
30
+ clearTimeout(timer);
31
+ proc.removeListener('exit', onExit);
32
+ resolve(alive);
33
+ };
34
+ proc.on('exit', onExit);
35
+ const timer = setTimeout(() => finish(isAlive(proc)), ms);
36
+ });
37
+ }
38
+ export function createWin32Terminator(deps) {
39
+ const spawnFn = deps.spawn ?? nodeSpawn;
40
+ const stoppers = deps.stoppers ?? agentStopperRegistry;
41
+ const waitForExit = deps.waitForExit ?? defaultWaitForExit;
42
+ const log = deps.log;
43
+ /** 树杀:taskkill /PID <pid> /T /F —— posix 负 PID 组杀的等价物。 */
44
+ function killTree(pid) {
45
+ if (pid === undefined) {
46
+ log?.(`win32 树杀跳过:子进程没有 pid(spawn 未成功)`);
47
+ return;
48
+ }
49
+ try {
50
+ // fire-and-forget:不等 taskkill 自身返回,退出由 proc 的 exit 事件驱动
51
+ const killer = spawnFn('taskkill', ['/PID', String(pid), '/T', '/F'], {
52
+ stdio: 'ignore',
53
+ windowsHide: true,
54
+ });
55
+ killer?.on?.('error', () => {
56
+ // taskkill 拉起失败不致命:调用方仍可走 killOrphan / 陈旧锁自愈
57
+ });
58
+ }
59
+ catch (error) {
60
+ log?.(`win32 树杀失败:taskkill 拉起异常 ${String(error)}`);
61
+ }
62
+ }
63
+ return {
64
+ async stop(proc, opts) {
65
+ if (!isAlive(proc))
66
+ return { requested: false, via: 'already-exited' };
67
+ if (proc.pid === undefined) {
68
+ // 与 posix 同口径:拿不到 pid 就没有可请求的终止目标,不能谎报 requested
69
+ log?.('win32 停止跳过:子进程没有 pid(spawn 未成功),按已退出处理');
70
+ return { requested: false, via: 'already-exited' };
71
+ }
72
+ if (opts.immediate) {
73
+ killTree(proc.pid);
74
+ return { requested: true, via: 'taskkill' };
75
+ }
76
+ const agent = deps.agent;
77
+ const stopper = agent === undefined ? undefined : stoppers.get(agent);
78
+ if (!stopper) {
79
+ // 无通道是合法状态:显式记录后走树杀,不做无意义的优雅空转
80
+ log?.(`win32 优雅停止跳过:agent=${agent ?? '(unknown)'} 无协议停止通道,直接树杀`);
81
+ killTree(proc.pid);
82
+ return { requested: true, via: 'skipped-no-channel' };
83
+ }
84
+ try {
85
+ await stopper(proc);
86
+ }
87
+ catch (error) {
88
+ // 通道异常不能吞掉终止流程:记录后继续走树杀兜底
89
+ log?.(`win32 协议停止通道异常,转入树杀:${String(error)}`);
90
+ }
91
+ if (!isAlive(proc))
92
+ return { requested: true, via: 'cooperative' };
93
+ const stillAlive = await waitForExit(proc, deps.graceMs);
94
+ if (!stillAlive)
95
+ return { requested: true, via: 'cooperative' };
96
+ killTree(proc.pid);
97
+ return { requested: true, via: 'taskkill' };
98
+ },
99
+ cleanupOnExit(proc) {
100
+ // M0 真机验证项(§9.2 矩阵):本方法在 process.on('exit') 里调用,而
101
+ // killTree 依赖 libuv uv_spawn 在 exit handler 内同步发起 taskkill——
102
+ // 大概率可用(uv_spawn 本身同步),但与 posix 路径(同步 process.kill)
103
+ // 可靠性不等价;失败表现为 win32 上 bridge 退出后 agent 变孤儿。
104
+ if (!isAlive(proc))
105
+ return;
106
+ killTree(proc.pid);
107
+ },
108
+ };
109
+ }
110
+ //# sourceMappingURL=terminator-win32.js.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * 平台终止器 seam 的接口层(design.md §3.2 / §3.3)。
3
+ *
4
+ * Windows 没有可拦截的跨进程 SIGTERM、负 PID 无效,因此把「进程组 + 信号」
5
+ * 换成「协议停止通道 + 进程树杀」。本文件只放接口与平台分发,实现见
6
+ * terminator-posix.ts / terminator-win32.ts。
7
+ */
8
+ import type { ChildProcess } from 'node:child_process';
9
+ import type { AgentStopperRegistry } from './agent-stopper.js';
10
+ import type { TerminateResult } from './types.js';
11
+ export { AgentStopperRegistry, agentStopperRegistry } from './agent-stopper.js';
12
+ export type { AgentStopper } from './agent-stopper.js';
13
+ /**
14
+ * 进程终止器。
15
+ *
16
+ * `stop` 的 `immediate` 语义与现状 `process-stopper` 的 `stop({immediate})` 对齐。
17
+ * 设计文档声明返回 `Promise<void>`,这里收窄为 {@link TerminateResult}:降级
18
+ * 必须可观测(优雅段被跳过、走了树杀、进程早已退出),否则 win32 上「等一个
19
+ * 不会来的退出事件」这类退化无法被发现。
20
+ */
21
+ export interface Terminator {
22
+ stop(proc: ChildProcess, opts: {
23
+ immediate: boolean;
24
+ }): Promise<TerminateResult>;
25
+ /** 进程级退出清理:fire-and-forget 强杀 */
26
+ cleanupOnExit(proc: ChildProcess): void;
27
+ }
28
+ export interface TerminatorDeps {
29
+ /** 优雅等待窗口(毫秒);win32 为协议停止后的等待,posix 为 SIGTERM→SIGKILL 的 grace */
30
+ graceMs: number;
31
+ /** 平台注入(测试用);默认当前宿主平台 */
32
+ platform?: NodeJS.Platform;
33
+ /** 当前 run 所属 agent(win32 查协议通道用;posix 忽略) */
34
+ agent?: string;
35
+ /** 协议通道注册表注入(测试隔离);默认进程级单例 */
36
+ stoppers?: AgentStopperRegistry;
37
+ /** 日志注入(测试用);默认走 logger 单例 */
38
+ log?: (level: 'debug' | 'info', message: string) => void;
39
+ }
40
+ /**
41
+ * 唯一的终止器选择入口:posix 走组杀,win32 走「协议通道 + taskkill 树杀」。
42
+ * 其余模块一律拿这里的 Terminator,禁止散落平台 if(§2.2)。
43
+ */
44
+ export declare function createTerminator(deps: TerminatorDeps): Terminator;
@@ -0,0 +1,24 @@
1
+ import { currentPlatform, isWin32 } from './select.js';
2
+ import { createPosixTerminator } from './terminator-posix.js';
3
+ import { createWin32Terminator } from './terminator-win32.js';
4
+ // 注册表实现在 agent-stopper.ts(避免与 terminator-win32.ts 循环依赖),
5
+ // 这里 re-export 以维持调用方的单一入口。
6
+ export { AgentStopperRegistry, agentStopperRegistry } from './agent-stopper.js';
7
+ /**
8
+ * 唯一的终止器选择入口:posix 走组杀,win32 走「协议通道 + taskkill 树杀」。
9
+ * 其余模块一律拿这里的 Terminator,禁止散落平台 if(§2.2)。
10
+ */
11
+ export function createTerminator(deps) {
12
+ const platform = deps.platform ?? currentPlatform;
13
+ if (isWin32(platform)) {
14
+ return createWin32Terminator({
15
+ graceMs: deps.graceMs,
16
+ agent: deps.agent,
17
+ stoppers: deps.stoppers,
18
+ // win32 侧日志不带级别(无 debug 级输出),统一按 info 透传
19
+ log: deps.log ? (message) => deps.log?.('info', message) : undefined,
20
+ });
21
+ }
22
+ return createPosixTerminator({ graceMs: deps.graceMs, log: deps.log });
23
+ }
24
+ //# sourceMappingURL=terminator.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 平台 seam 纯类型。
3
+ */
4
+ /**
5
+ * spawn 发现规格 —— resolveExecutable 的产出(v2 §4.3,仅探测/诊断用)。
6
+ * v2 收窄:.cmd 垫片命中即报告垫片路径本身,不再解析内容定位入口;
7
+ * 执行统一走 platform/spawn.ts 的 cross-spawn 包装。
8
+ */
9
+ export interface LaunchSpec {
10
+ kind: 'direct';
11
+ /** 可执行文件路径(posix 绝对路径 / win32 含 .cmd 垫片路径) */
12
+ file: string;
13
+ }
14
+ /** 终止结果(§3.2 win32 状态机产出;可观测降级用)。 */
15
+ export interface TerminateResult {
16
+ /** 进程是否已确认被请求终止(不代表已退出,退出由 exit 事件驱动) */
17
+ requested: boolean;
18
+ /**
19
+ * 终止途经:协议停止 / 强制终止 / 进程已退出 / 无通道跳过优雅段。
20
+ * 强制终止在 posix 下是 SIGKILL 组杀、win32 下是 taskkill 树杀,同枚举值。
21
+ */
22
+ via: 'cooperative' | 'taskkill' | 'already-exited' | 'skipped-no-channel';
23
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 平台 seam 纯类型。
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
package/dist/restart.js CHANGED
@@ -1,4 +1,4 @@
1
- import { spawn } from 'node:child_process';
1
+ import { spawnProcess } from './platform/spawn.js';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { getLogger } from './logger/index.js';
@@ -24,10 +24,12 @@ const POLL_MS = 100;
24
24
  export function spawnReplacementBridge(logsDir) {
25
25
  fs.mkdirSync(logsDir, { recursive: true });
26
26
  const out = fs.openSync(path.join(logsDir, 'restart-child.log'), 'a');
27
- const child = spawn(process.execPath, process.argv.slice(1), {
27
+ const child = spawnProcess(process.execPath, process.argv.slice(1), {
28
28
  cwd: process.cwd(),
29
29
  env: { ...process.env, [RESTART_WAIT_PID_ENV]: String(process.pid) },
30
30
  detached: true,
31
+ // win32:detached 继任者默认会闪控制台窗口(§3.7)
32
+ windowsHide: true,
31
33
  stdio: ['ignore', out, out],
32
34
  });
33
35
  // Late spawn errors (e.g. binary removed mid-run) must not crash the