@brierb/brier-cli 0.0.6 → 0.0.8

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 (65) hide show
  1. package/dist/commands/log.js +28 -0
  2. package/dist/commands/restart.js +9 -4
  3. package/dist/commands/run.js +13 -0
  4. package/dist/commands/start.js +2 -2
  5. package/dist/commands/status.js +11 -16
  6. package/dist/commands/stop.js +1 -1
  7. package/dist/config/credentials.js +25 -0
  8. package/dist/config/index.js +16 -0
  9. package/dist/config/load.js +26 -0
  10. package/dist/config/paths.js +14 -0
  11. package/dist/config/version.js +13 -0
  12. package/dist/core/index.js +11 -0
  13. package/dist/core/runtimes.js +74 -0
  14. package/dist/daemon/DaemonManager.js +65 -66
  15. package/dist/daemon/DaemonRunner.js +97 -39
  16. package/dist/daemon/OutputBatcher.js +77 -0
  17. package/dist/daemon/TaskExecutor.js +79 -10
  18. package/dist/daemon/index.js +13 -0
  19. package/dist/daemon/state.js +100 -0
  20. package/dist/definitions/daemon.js +5 -0
  21. package/dist/definitions/index.js +12 -0
  22. package/dist/definitions/tunnel.js +9 -0
  23. package/dist/index.js +12 -31
  24. package/dist/tunnel/backoff.js +21 -0
  25. package/dist/tunnel/dispatcher.js +35 -0
  26. package/dist/tunnel/heartbeat.js +55 -0
  27. package/dist/tunnel/index.js +220 -0
  28. package/dist/tunnel/transport.js +112 -0
  29. package/dist/tunnel/url.js +10 -0
  30. package/package.json +3 -3
  31. package/types/commands/log.d.ts +5 -0
  32. package/types/commands/run.d.ts +5 -0
  33. package/types/commands/status.d.ts +1 -0
  34. package/types/config/credentials.d.ts +8 -0
  35. package/types/config/index.d.ts +16 -0
  36. package/types/config/load.d.ts +7 -0
  37. package/types/config/paths.d.ts +12 -0
  38. package/types/config/version.d.ts +2 -0
  39. package/types/core/index.d.ts +11 -0
  40. package/types/core/runtimes.d.ts +29 -0
  41. package/types/daemon/DaemonManager.d.ts +1 -1
  42. package/types/daemon/OutputBatcher.d.ts +39 -0
  43. package/types/daemon/TaskExecutor.d.ts +4 -2
  44. package/types/daemon/index.d.ts +13 -0
  45. package/types/daemon/state.d.ts +19 -0
  46. package/types/definitions/daemon.d.ts +58 -0
  47. package/types/definitions/index.d.ts +17 -0
  48. package/types/definitions/task.d.ts +25 -0
  49. package/types/definitions/tunnel.d.ts +112 -0
  50. package/types/tunnel/backoff.d.ts +32 -0
  51. package/types/tunnel/dispatcher.d.ts +26 -0
  52. package/types/tunnel/heartbeat.d.ts +36 -0
  53. package/types/tunnel/index.d.ts +38 -0
  54. package/types/tunnel/transport.d.ts +29 -0
  55. package/types/tunnel/url.d.ts +5 -0
  56. package/dist/config.js +0 -81
  57. package/dist/runtimes.js +0 -29
  58. package/dist/tunnel/TunnelClient.js +0 -218
  59. package/types/config.d.ts +0 -9
  60. package/types/runtimes.d.ts +0 -14
  61. package/types/tunnel/TunnelClient.d.ts +0 -10
  62. package/types/types.d.ts +0 -78
  63. /package/dist/{logger.js → core/logger.js} +0 -0
  64. /package/dist/{types.js → definitions/task.js} +0 -0
  65. /package/types/{logger.d.ts → core/logger.d.ts} +0 -0
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 本地任务执行上下文。
3
+ *
4
+ * TunnelClient 收到下行 task-start 消息后,将其转换为本结构交给
5
+ * TaskExecutor.execute() 执行——是“隧道传输形态”(ServerMessage)
6
+ * 与“子进程执行形态”之间的进程内中间对象,不上报服务端。
7
+ */
8
+ export interface TaskInfo {
9
+ taskId: string;
10
+ /** 所属 AI runtime 名称;command 模式为空串 */
11
+ runtime: string;
12
+ /**
13
+ * 可执行命令:
14
+ * - command 模式:服务端下发的显式 shell 命令
15
+ * - runtime 模式:由 resolveRuntimeExecutable 解析为绝对路径(保证脱离受限 PATH 也能 spawn)
16
+ */
17
+ command: string;
18
+ args: string[];
19
+ /** 子进程工作目录(可选) */
20
+ cwd?: string;
21
+ /** 注入子进程的额外环境变量(可选) */
22
+ env?: Record<string, string>;
23
+ /** 自然语言指令(可选):存在时按 RUNTIME_PROMPT_FLAGS 拼到命令参数中执行 */
24
+ prompt?: string;
25
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * 隧道域类型(daemon ⇄ Brier 服务端 的连接与消息)。
3
+ *
4
+ * 聚合三组归属不同的类型:
5
+ * 1. StreamType / ClientMessage / ServerMessage:线协议(WebSocket JSON 文本帧),
6
+ * 与服务端 Rust `brier_type::tunnel::*` 镜像,任何命名/字段改动两端必须同步;
7
+ * 2. TunnelState:daemon 进程内的连接状态机(服务端无对应,不上报)。
8
+ */
9
+ /**
10
+ * 任务输出流的来源。
11
+ *
12
+ * 由 daemon 的 TaskExecutor 转发子进程输出时标记(stdout / stderr),
13
+ * 随上行消息 task-output 一并上报给服务端。
14
+ *
15
+ * 注意:序列化值需与服务端 Rust 侧 `brier_type::tunnel::StreamType` 保持一致(lowercase),
16
+ * 新增流类型时两端必须同步修改。
17
+ */
18
+ export type StreamType = 'stdout' | 'stderr';
19
+ /**
20
+ * 隧道线协议消息(daemon ⇄ Brier 服务端,WebSocket JSON 文本帧)。
21
+ *
22
+ * 使用可辨识联合:以 `type` 字段区分消息,type 值为 kebab-case(如 task-output / auth-ok),
23
+ * 字段名为 camelCase。与服务端 Rust `brier_type::tunnel::{ClientMessage, ServerMessage}`
24
+ * 经 serde(tag="type", rename_all="kebab-case" + 字段 rename)逐字段对齐,
25
+ * 任何一端新增/删除字段或改动命名时,另一端必须同步修改,否则 JSON 解析会失败或丢字段。
26
+ */
27
+ /**
28
+ * 上行消息(daemon → 服务端)。
29
+ *
30
+ * - auth:连接建立后的第一条鉴权消息;token 同时通过 wss Authorization 头携带(双通道,服务端以 header 为准)
31
+ * - heartbeat:应用层心跳,触发服务端落库 last_seen_at 并回执 heartbeat-ack
32
+ * - task-output:任务实时输出;当前实现为 stdout/stderr 每收到一个数据块即发一条
33
+ * - task-complete:子进程正常退出(exitCode 为进程退出码)
34
+ * - task-error:执行失败(如找不到可执行文件、超过并发上限)
35
+ * - runtime-info:runtime 清单上报(服务端目前仅记录日志,属预留能力)
36
+ */
37
+ export type ClientMessage = {
38
+ type: 'auth';
39
+ token: string;
40
+ hostname: string;
41
+ os: string;
42
+ runtimes: string[];
43
+ /** CLI 自身版本(package.json),旧版客户端可能不带该字段 */
44
+ version?: string;
45
+ } | {
46
+ type: 'heartbeat';
47
+ timestamp: number;
48
+ } | {
49
+ type: 'task-output';
50
+ taskId: string;
51
+ stream: StreamType;
52
+ data: string;
53
+ } | {
54
+ type: 'task-complete';
55
+ taskId: string;
56
+ exitCode: number;
57
+ } | {
58
+ type: 'task-error';
59
+ taskId: string;
60
+ error: string;
61
+ } | {
62
+ type: 'runtime-info';
63
+ runtimes: string[];
64
+ };
65
+ /**
66
+ * 下行消息(服务端 → daemon)。
67
+ *
68
+ * - auth-ok:鉴权通过并完成注册,computerId 为本机在服务端的工作电脑 ID
69
+ * - auth-failed:鉴权失败(令牌无效等),reason 为失败原因
70
+ * - heartbeat-ack:心跳回执(echo 客户端发送的 timestamp)
71
+ * - task-start:下发任务执行;prompt 模式由 CLI 按 runtime 拼参数,command 模式直接执行
72
+ * - task-cancel:请求终止正在执行的子进程(SIGTERM)
73
+ * - query-runtimes:查询本机 runtime 清单(当前服务端不会主动发送,属预留;CLI 保留处理以兼容旧服务端)
74
+ */
75
+ export type ServerMessage = {
76
+ type: 'auth-ok';
77
+ computerId: string;
78
+ } | {
79
+ type: 'auth-failed';
80
+ reason: string;
81
+ } | {
82
+ type: 'heartbeat-ack';
83
+ timestamp: number;
84
+ } | {
85
+ type: 'task-start';
86
+ taskId: string;
87
+ /** 所属 AI runtime 名称(如 Claude Code / OpenCode);command 模式为空串 */
88
+ runtime: string;
89
+ /** 可执行命令:command 模式为显式命令;runtime 模式为空,由 CLI 探测解析 */
90
+ command: string;
91
+ args: string[];
92
+ /** 子进程工作目录(可选) */
93
+ cwd?: string;
94
+ /** 注入子进程的额外环境变量(可选) */
95
+ env?: Record<string, string>;
96
+ /** 自然语言指令(可选):存在时按 RUNTIME_PROMPT_FLAGS 拼入执行参数 */
97
+ prompt?: string;
98
+ } | {
99
+ type: 'task-cancel';
100
+ taskId: string;
101
+ } | {
102
+ type: 'query-runtimes';
103
+ };
104
+ /**
105
+ * 隧道连接状态机。
106
+ *
107
+ * 仅存在于 daemon 进程内(TunnelClient 内部维护、DaemonRunner 订阅打印),不上报服务端:
108
+ * connecting(握手)→ connected(收到 auth-ok)→ 断线进入 reconnecting;
109
+ * 收到 auth-failed 或重连次数耗尽(50 次退避上限)时进入 error 并停止,
110
+ * 主动 stop 时进入 disconnected。
111
+ */
112
+ export type TunnelState = 'connecting' | 'connected' | 'disconnected' | 'reconnecting' | 'error';
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 重连退避策略(纯逻辑,无 IO、无副作用)。
3
+ *
4
+ * 与原实现的等价行为:指数退避(1s 起步、封顶 maxMs),累计 maxAttempts 次后放弃。
5
+ * next() 返回 -1 表示“已耗尽、应停止重连”;attempts 仅供日志展示尝试次数。
6
+ */
7
+ export interface BackoffOptions {
8
+ /** 基础延迟(ms),首次重连即使用该值 */
9
+ baseMs: number;
10
+ /** 延迟封顶(ms) */
11
+ maxMs: number;
12
+ /** 最大重连尝试次数,达到后放弃 */
13
+ maxAttempts: number;
14
+ /**
15
+ * 是否启用 full jitter:在 [0, 计算延迟] 内取随机值,
16
+ * 避免大量客户端同时断线后以完全相同的节奏重连(重连风暴)。
17
+ * 默认开启。
18
+ */
19
+ jitter?: boolean;
20
+ }
21
+ export interface Backoff {
22
+ /**
23
+ * 计算下一次等待毫秒数。
24
+ * 已尝试次数达到 maxAttempts 时返回 -1(调用方应停止重连)。
25
+ */
26
+ next(): number;
27
+ /** 已排定/已执行的尝试次数(供日志与判断) */
28
+ readonly attempts: number;
29
+ /** 连接成功后归零 */
30
+ reset(): void;
31
+ }
32
+ export declare const createBackoff: (options: BackoffOptions) => Backoff;
@@ -0,0 +1,26 @@
1
+ import type { ServerMessage } from '../definitions/index.js';
2
+ /**
3
+ * 服务端下行消息解析与分发。
4
+ *
5
+ * 只做两件事:把 WS 文本帧 JSON.parse 成 ServerMessage,再按 type 分发给对应回调。
6
+ * 不接触 ws、状态机或执行器——骨架通过回调把“协议 → 业务动作”接起来,
7
+ * 本模块因此不依赖网络与运行状态,职责保持单一。
8
+ */
9
+ export interface ServerMessageHandlers {
10
+ /** 鉴权通过(携带服务端分配的工作电脑 ID) */
11
+ onAuthOk: (computerId: string) => void;
12
+ /** 鉴权失败(reason 为原因) */
13
+ onAuthFailed: (reason: string) => void;
14
+ /** 心跳回执 */
15
+ onHeartbeatAck: (timestamp: number) => void;
16
+ /** 任务下发 */
17
+ onTaskStart: (message: Extract<ServerMessage, {
18
+ type: 'task-start';
19
+ }>) => void;
20
+ /** 任务取消 */
21
+ onTaskCancel: (taskId: string) => void;
22
+ /** 服务端查询本机 runtime 清单 */
23
+ onQueryRuntimes: () => void;
24
+ }
25
+ /** 解析并分发一条服务端消息;JSON 解析失败或未知 type 记日志后静默返回。 */
26
+ export declare const dispatchServerMessage: (raw: string, handlers: ServerMessageHandlers) => void;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 应用层心跳策略。
3
+ *
4
+ * 只负责节奏:到点调用 onBeat() 发送心跳;onBeat 返回 true(已发出)时启动
5
+ * ack 超时计时,收到 ack 由调用方调 ack() 解除;超时触发 onTimeout()。
6
+ * 本模块“只上报不决策”——超时后如何处理(如关闭连接)由上层骨架决定。
7
+ *
8
+ * 注意:每次触发心跳前先清理上一次的 ack 超时计时器,避免重复计时器叠加。
9
+ */
10
+ export interface HeartbeatOptions {
11
+ /** 心跳发送间隔(ms) */
12
+ intervalMs: number;
13
+ /** 等待 ack 的超时(ms) */
14
+ timeoutMs: number;
15
+ /**
16
+ * 心跳发送回调。返回是否已真正发出(连接未打开时返回 false,
17
+ * 此时不启动 ack 超时计时)。
18
+ */
19
+ onBeat: () => boolean;
20
+ /** ack 超时(对端无响应)回调,由上层决定如何处置 */
21
+ onTimeout: () => void;
22
+ /**
23
+ * onBeat 抛错时的回调(可选)。setInterval 回调中的异常会作为
24
+ * uncaughtException 直接杀死进程,因此 beat() 内部必须吞掉。
25
+ */
26
+ onBeatError?: (error: unknown) => void;
27
+ }
28
+ export interface Heartbeat {
29
+ /** 启动心跳(幂等:先停止已有计时器再开始) */
30
+ start(): void;
31
+ /** 停止心跳并清理所有计时器 */
32
+ stop(): void;
33
+ /** 收到 ack 时调用:解除当前 ack 超时计时 */
34
+ ack(): void;
35
+ }
36
+ export declare const createHeartbeat: (options: HeartbeatOptions) => Heartbeat;
@@ -0,0 +1,38 @@
1
+ import type { ClientMessage, DaemonConfig, ServerMessage, TunnelState } from '../definitions/index.js';
2
+ export interface TunnelClient {
3
+ start: () => void;
4
+ stop: () => Promise<void>;
5
+ /** 发送上行消息;返回是否真正发出(未连接时为 false,调用方决定如何处理) */
6
+ send: (message: ClientMessage) => boolean;
7
+ getState: () => TunnelState;
8
+ onStateChange: (callback: (state: TunnelState) => void) => () => void;
9
+ }
10
+ /**
11
+ * 业务消息处理回调(组合点)。
12
+ *
13
+ * 隧道只负责“协议 → 消息分发”,不解释业务:task-start/task-cancel
14
+ * 原样交给宿主实现(daemon/DaemonRunner 将其翻译成 TaskExecutor 调用)。
15
+ * 隧道域因此不依赖任务执行域。
16
+ */
17
+ export interface TunnelMessageHandlers {
18
+ onTaskStart: (message: Extract<ServerMessage, {
19
+ type: 'task-start';
20
+ }>) => void;
21
+ onTaskCancel: (taskId: string) => void;
22
+ }
23
+ /**
24
+ * 隧道骨架:连接生命周期编排与状态仲裁的唯一入口。
25
+ *
26
+ * 本文件只做“编排”,不接触 ws 细节、不算退避、不解析消息:
27
+ * - transport:负责底层连接与收发帧
28
+ * - heartbeat:负责心跳节奏(只上报不决策)
29
+ * - backoff:负责重连延迟计算(纯逻辑)
30
+ * - dispatcher:负责服务端消息解析与分发
31
+ *
32
+ * 关键时序不变式(重构后保持与原实现一致):
33
+ * 1. 心跳仅在 auth-ok 后启动;
34
+ * 2. close 后仅当 running 才退避重连,stop() 后绝不再连;
35
+ * 3. 仅 auth-ok 归零退避计数;
36
+ * 4. stop 时先优雅 close,3s 兜底 terminate。
37
+ */
38
+ export declare const createTunnelClient: (config: DaemonConfig, messageHandlers: TunnelMessageHandlers) => TunnelClient;
@@ -0,0 +1,29 @@
1
+ export interface TransportEventMap {
2
+ /** 连接建立(TCP+TLS+WS 握手完成) */
3
+ open: () => void;
4
+ /** 连接关闭:code 为关闭码,reason 为关闭原因(已转字符串) */
5
+ close: (code: number, reason: string) => void;
6
+ /** 收到文本消息(已转字符串) */
7
+ message: (data: string) => void;
8
+ /** 底层错误(通常随后触发 close) */
9
+ error: (message: string) => void;
10
+ }
11
+ export interface Transport {
12
+ /** 建立到 url 的连接(携带自定义请求头);会清理上一次连接 */
13
+ connect(url: string, headers: Record<string, string>): void;
14
+ /** 发送文本帧;返回是否已入队(未连接或缓冲积压超限时为 false,不抛错) */
15
+ send(data: string): boolean;
16
+ /** 优雅关闭(仅当连接处于打开/连接中) */
17
+ close(code: number, reason: string): void;
18
+ /** 强制终止底层连接(不触发事件) */
19
+ terminate(): void;
20
+ isOpen(): boolean;
21
+ isConnecting(): boolean;
22
+ /** 连接是否已完全关闭(不存在或 CLOSED):close 事件不会再触发 */
23
+ isClosed(): boolean;
24
+ /** 等待下一次 close 事件,超时 resolve(用于 stop 的优雅关闭兜底) */
25
+ waitClosed(timeoutMs: number): Promise<void>;
26
+ /** 订阅事件,返回取消订阅函数 */
27
+ on<K extends keyof TransportEventMap>(event: K, callback: TransportEventMap[K]): () => void;
28
+ }
29
+ export declare const createWebSocketTransport: () => Transport;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 隧道接入端点工具:把服务端 http(s) 地址转换为 WebSocket 隧道地址。
3
+ * 纯字符串转换(wss/ws://host/tunnel),无状态、无副作用。
4
+ */
5
+ export declare const toWsUrl: (serverUrl: string) => string;
package/dist/config.js DELETED
@@ -1,81 +0,0 @@
1
- import { hostname as getHostname, type as osType, arch, platform, homedir } from 'node:os';
2
- import { execSync } from 'node:child_process';
3
- import { accessSync, constants, readFileSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import { RUNTIME_REGISTRY } from './runtimes.js';
7
- export const BRIER_DIR = join(homedir(), '.brier');
8
- export const PID_FILE = join(BRIER_DIR, 'daemon.pid');
9
- export const LOG_FILE = join(BRIER_DIR, 'daemon.log');
10
- /** CLI 自身版本(读取 dist 同级的 package.json)。 */
11
- const readCliVersion = () => {
12
- try {
13
- const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url));
14
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
15
- return pkg.version ?? 'unknown';
16
- }
17
- catch {
18
- return 'unknown';
19
- }
20
- };
21
- export const loadConfig = (options) => {
22
- const serverUrl = options.serverUrl ?? process.env.BRIER_SERVER_URL;
23
- const token = options.token ?? process.env.BRIER_TOKEN;
24
- if (!serverUrl) {
25
- throw new Error('Server URL is required. Use --server-url or set BRIER_SERVER_URL');
26
- }
27
- if (!token) {
28
- throw new Error('BRIER_TOKEN is required. Pass --token or set BRIER_TOKEN env var');
29
- }
30
- return {
31
- serverUrl,
32
- token,
33
- hostname: getHostname(),
34
- os: `${osType()} ${platform()} ${arch()}`,
35
- runtimes: detectRuntimes(),
36
- version: readCliVersion(),
37
- };
38
- };
39
- export const toWsUrl = (serverUrl) => {
40
- return (serverUrl
41
- .replace(/^https:\/\//, 'wss://')
42
- .replace(/^http:\/\//, 'ws://')
43
- .replace(/\/$/, '') + '/tunnel');
44
- };
45
- // macOS 自带磁盘分区工具 gpt(8) 位于 /usr/sbin/gpt,会与 OpenAI 的 gpt CLI 同名,
46
- // macOS 自带磁盘分区工具 gpt(8) 位于 /usr/sbin/gpt,会与 OpenAI CLI 同名;
47
- // 命中 /sbin、/usr/sbin 视为「未安装」,避免误报。
48
- const SYSTEM_SBIN_RE = /^\/(usr\/)?sbin\//;
49
- // shell 内建命令(如 continue、cd、type)会被 `command -v` 返回为无路径的裸名,
50
- // 不是真实可执行文件,需排除。
51
- const isRealExecutablePath = (hit) => Boolean(hit) && hit.includes('/') && !SYSTEM_SBIN_RE.test(hit);
52
- const isExecutable = (file) => {
53
- try {
54
- accessSync(file, constants.X_OK);
55
- return true;
56
- }
57
- catch {
58
- return false;
59
- }
60
- };
61
- const expandHome = (p) => (p.startsWith('~/') ? join(homedir(), p.slice(2)) : p);
62
- const detectRuntimes = () => {
63
- const runtimes = [];
64
- for (const { name, command, fallbacks } of RUNTIME_REGISTRY) {
65
- let found = false;
66
- try {
67
- const hit = execSync(`command -v ${command}`, { stdio: 'pipe' }).toString().trim();
68
- found = isRealExecutablePath(hit);
69
- }
70
- catch {
71
- // 不在 PATH
72
- }
73
- if (!found) {
74
- const candidates = fallbacks ?? [`~/.local/bin/${command}`];
75
- found = candidates.some((p) => isExecutable(expandHome(p)));
76
- }
77
- if (found)
78
- runtimes.push(name);
79
- }
80
- return runtimes;
81
- };
package/dist/runtimes.js DELETED
@@ -1,29 +0,0 @@
1
- export const RUNTIME_REGISTRY = [
2
- { name: 'Claude Code', command: 'claude', fallbacks: ['~/.claude/bin/claude'] },
3
- { name: 'Codex CLI', command: 'codex' },
4
- { name: 'OpenAI CLI', command: 'openai' },
5
- { name: 'Gemini CLI', command: 'gemini' },
6
- { name: 'Cursor CLI', command: 'cursor' },
7
- {
8
- name: 'OpenCode',
9
- command: 'opencode',
10
- fallbacks: ['~/.opencode/bin/opencode', '~/.local/bin/opencode'],
11
- },
12
- { name: 'Aider', command: 'aider', fallbacks: ['~/.local/bin/aider'] },
13
- { name: 'Goose', command: 'goose', fallbacks: ['~/.local/bin/goose'] },
14
- { name: 'Cody', command: 'cody', fallbacks: ['~/.local/bin/cody'] },
15
- ];
16
- export const RUNTIME_COMMANDS = Object.fromEntries(RUNTIME_REGISTRY.map((r) => [r.name, r.command]));
17
- /**
18
- * AI runtime 单次(非交互)执行参数模板:命令前缀 flags,后面紧跟 prompt。
19
- * 服务端 task-start 携带 prompt 时,CLI 用它拼出
20
- * `spawn(<command>, [...flags, prompt])`;未列出的 runtime 按裸参数执行。
21
- */
22
- export const RUNTIME_PROMPT_FLAGS = {
23
- 'OpenCode': ['run'],
24
- 'Claude Code': ['-p'],
25
- 'Codex CLI': ['exec'],
26
- 'Gemini CLI': ['-p'],
27
- 'Aider': ['--message'],
28
- 'Goose': ['run'],
29
- };
@@ -1,218 +0,0 @@
1
- import { WebSocket } from 'ws';
2
- import { toWsUrl } from '../config.js';
3
- import { logger } from '../logger.js';
4
- const HEARTBEAT_INTERVAL_MS = 30_000;
5
- const HEARTBEAT_TIMEOUT_MS = 10_000;
6
- const BASE_RECONNECT_DELAY_MS = 1_000;
7
- const MAX_RECONNECT_DELAY_MS = 30_000;
8
- const MAX_RECONNECT_ATTEMPTS = 50;
9
- export const createTunnelClient = (config, taskExecutor) => {
10
- let ws = null;
11
- let state = 'disconnected';
12
- let running = false;
13
- let reconnectAttempts = 0;
14
- let heartbeatTimer = null;
15
- let heartbeatTimeoutTimer = null;
16
- let reconnectTimer = null;
17
- const listeners = new Set();
18
- const notifyStateChange = (newState) => {
19
- state = newState;
20
- for (const callback of listeners) {
21
- callback(newState);
22
- }
23
- };
24
- const onStateChange = (callback) => {
25
- listeners.add(callback);
26
- return () => {
27
- listeners.delete(callback);
28
- };
29
- };
30
- const getState = () => state;
31
- const send = (message) => {
32
- if (!ws || ws.readyState !== WebSocket.OPEN) {
33
- throw new Error('Tunnel is not connected');
34
- }
35
- ws.send(JSON.stringify(message));
36
- };
37
- const startHeartbeat = () => {
38
- stopHeartbeat();
39
- heartbeatTimer = setInterval(() => {
40
- if (ws && ws.readyState === WebSocket.OPEN) {
41
- send({ type: 'heartbeat', timestamp: Date.now() });
42
- heartbeatTimeoutTimer = setTimeout(() => {
43
- logger.warn('Heartbeat timeout, forcing reconnect');
44
- ws?.close(4000, 'heartbeat timeout');
45
- }, HEARTBEAT_TIMEOUT_MS);
46
- }
47
- }, HEARTBEAT_INTERVAL_MS);
48
- };
49
- const stopHeartbeat = () => {
50
- if (heartbeatTimer) {
51
- clearInterval(heartbeatTimer);
52
- heartbeatTimer = null;
53
- }
54
- if (heartbeatTimeoutTimer) {
55
- clearTimeout(heartbeatTimeoutTimer);
56
- heartbeatTimeoutTimer = null;
57
- }
58
- };
59
- const handleServerMessage = (message) => {
60
- switch (message.type) {
61
- case 'auth-ok':
62
- logger.info('Tunnel authenticated, computerId:', message.computerId);
63
- reconnectAttempts = 0;
64
- notifyStateChange('connected');
65
- startHeartbeat();
66
- break;
67
- case 'auth-failed':
68
- logger.error('Authentication failed:', message.reason);
69
- notifyStateChange('error');
70
- running = false;
71
- break;
72
- case 'heartbeat-ack':
73
- if (heartbeatTimeoutTimer) {
74
- clearTimeout(heartbeatTimeoutTimer);
75
- heartbeatTimeoutTimer = null;
76
- }
77
- break;
78
- case 'task-start':
79
- logger.info('Task start:', message.taskId, message.command);
80
- taskExecutor.execute({
81
- taskId: message.taskId,
82
- runtime: message.runtime,
83
- command: message.command,
84
- args: message.args,
85
- cwd: message.cwd,
86
- env: message.env,
87
- prompt: message.prompt,
88
- });
89
- break;
90
- case 'task-cancel':
91
- logger.info('Task cancel:', message.taskId);
92
- taskExecutor.cancel(message.taskId);
93
- break;
94
- case 'query-runtimes':
95
- send({ type: 'runtime-info', runtimes: config.runtimes });
96
- break;
97
- }
98
- };
99
- const scheduleReconnect = () => {
100
- if (!running)
101
- return;
102
- if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
103
- logger.error('Max reconnect attempts reached, stopping');
104
- notifyStateChange('error');
105
- running = false;
106
- return;
107
- }
108
- reconnectAttempts++;
109
- const delay = Math.min(BASE_RECONNECT_DELAY_MS * Math.pow(2, reconnectAttempts - 1), MAX_RECONNECT_DELAY_MS);
110
- logger.info(`Reconnecting in ${(delay / 1000).toFixed(0)}s (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
111
- notifyStateChange('reconnecting');
112
- reconnectTimer = setTimeout(() => {
113
- if (running) {
114
- connect();
115
- }
116
- }, delay);
117
- };
118
- const connect = () => {
119
- if (ws) {
120
- ws.removeAllListeners();
121
- ws.terminate();
122
- ws = null;
123
- }
124
- const wsUrl = toWsUrl(config.serverUrl);
125
- logger.info('Connecting to', wsUrl);
126
- notifyStateChange('connecting');
127
- ws = new WebSocket(wsUrl, {
128
- headers: {
129
- Authorization: `Bearer ${config.token}`,
130
- 'X-Brier-Hostname': config.hostname,
131
- 'X-Brier-OS': config.os,
132
- },
133
- });
134
- ws.on('open', () => {
135
- logger.info('WebSocket connected, authenticating...');
136
- send({
137
- type: 'auth',
138
- token: config.token,
139
- hostname: config.hostname,
140
- os: config.os,
141
- runtimes: config.runtimes,
142
- version: config.version,
143
- });
144
- });
145
- ws.on('message', (data) => {
146
- try {
147
- const message = JSON.parse(data.toString());
148
- handleServerMessage(message);
149
- }
150
- catch (err) {
151
- logger.error('Failed to parse server message:', err);
152
- }
153
- });
154
- ws.on('close', (code, reason) => {
155
- const reasonStr = reason.toString() || `code ${code}`;
156
- logger.warn(`WebSocket closed: ${reasonStr}`);
157
- stopHeartbeat();
158
- if (running) {
159
- scheduleReconnect();
160
- }
161
- else {
162
- notifyStateChange('disconnected');
163
- }
164
- });
165
- ws.on('error', (err) => {
166
- logger.error('WebSocket error:', err.message);
167
- });
168
- ws.on('ping', () => {
169
- ws?.pong();
170
- });
171
- };
172
- const start = () => {
173
- if (running) {
174
- logger.warn('Tunnel is already running');
175
- return;
176
- }
177
- running = true;
178
- reconnectAttempts = 0;
179
- connect();
180
- };
181
- const stop = async () => {
182
- running = false;
183
- stopHeartbeat();
184
- taskExecutor.cancelAll();
185
- if (reconnectTimer) {
186
- clearTimeout(reconnectTimer);
187
- reconnectTimer = null;
188
- }
189
- if (ws) {
190
- const closePromise = new Promise((resolve) => {
191
- if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
192
- ws.once('close', () => resolve());
193
- ws.close(1000, 'client shutdown');
194
- setTimeout(() => {
195
- if (ws && ws.readyState !== WebSocket.CLOSED) {
196
- ws.terminate();
197
- }
198
- resolve();
199
- }, 3000);
200
- }
201
- else {
202
- resolve();
203
- }
204
- });
205
- await closePromise;
206
- ws = null;
207
- }
208
- notifyStateChange('disconnected');
209
- logger.info('Tunnel stopped');
210
- };
211
- return {
212
- start,
213
- stop,
214
- send,
215
- getState,
216
- onStateChange,
217
- };
218
- };
package/types/config.d.ts DELETED
@@ -1,9 +0,0 @@
1
- import type { DaemonConfig } from './types.js';
2
- export declare const BRIER_DIR: string;
3
- export declare const PID_FILE: string;
4
- export declare const LOG_FILE: string;
5
- export declare const loadConfig: (options: {
6
- serverUrl?: string;
7
- token?: string;
8
- }) => DaemonConfig;
9
- export declare const toWsUrl: (serverUrl: string) => string;
@@ -1,14 +0,0 @@
1
- export interface RuntimeEntry {
2
- name: string;
3
- command: string;
4
- /** 命令不在 PATH 时的兜底可执行文件(~ 开头表示用户主目录),存在即视为已安装 */
5
- fallbacks?: readonly string[];
6
- }
7
- export declare const RUNTIME_REGISTRY: readonly RuntimeEntry[];
8
- export declare const RUNTIME_COMMANDS: Record<string, string>;
9
- /**
10
- * AI runtime 单次(非交互)执行参数模板:命令前缀 flags,后面紧跟 prompt。
11
- * 服务端 task-start 携带 prompt 时,CLI 用它拼出
12
- * `spawn(<command>, [...flags, prompt])`;未列出的 runtime 按裸参数执行。
13
- */
14
- export declare const RUNTIME_PROMPT_FLAGS: Record<string, readonly string[]>;
@@ -1,10 +0,0 @@
1
- import type { ClientMessage, DaemonConfig, TunnelState } from '../types.js';
2
- import type { TaskExecutor } from '../daemon/TaskExecutor.js';
3
- export interface TunnelClient {
4
- start: () => void;
5
- stop: () => Promise<void>;
6
- send: (message: ClientMessage) => void;
7
- getState: () => TunnelState;
8
- onStateChange: (callback: (state: TunnelState) => void) => () => void;
9
- }
10
- export declare const createTunnelClient: (config: DaemonConfig, taskExecutor: TaskExecutor) => TunnelClient;