@brierb/brier-cli 0.0.7 → 0.0.9

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 (63) 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/{runtimes.js → core/runtimes.js} +6 -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 +72 -8
  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/{runtimes.d.ts → core/runtimes.d.ts} +6 -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 -45
  57. package/dist/tunnel/TunnelClient.js +0 -218
  58. package/types/config.d.ts +0 -9
  59. package/types/tunnel/TunnelClient.d.ts +0 -10
  60. package/types/types.d.ts +0 -78
  61. /package/dist/{logger.js → core/logger.js} +0 -0
  62. /package/dist/{types.js → definitions/task.js} +0 -0
  63. /package/types/{logger.d.ts → core/logger.d.ts} +0 -0
@@ -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,45 +0,0 @@
1
- import { hostname as getHostname, type as osType, arch, platform, homedir } from 'node:os';
2
- import { readFileSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { resolveRuntimeExecutable, RUNTIME_REGISTRY } from './runtimes.js';
6
- export const BRIER_DIR = join(homedir(), '.brier');
7
- export const PID_FILE = join(BRIER_DIR, 'daemon.pid');
8
- export const LOG_FILE = join(BRIER_DIR, 'daemon.log');
9
- /** CLI 自身版本(读取 dist 同级的 package.json)。 */
10
- const readCliVersion = () => {
11
- try {
12
- const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url));
13
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
14
- return pkg.version ?? 'unknown';
15
- }
16
- catch {
17
- return 'unknown';
18
- }
19
- };
20
- export const loadConfig = (options) => {
21
- const serverUrl = options.serverUrl ?? process.env.BRIER_SERVER_URL;
22
- const token = options.token ?? process.env.BRIER_TOKEN;
23
- if (!serverUrl) {
24
- throw new Error('Server URL is required. Use --server-url or set BRIER_SERVER_URL');
25
- }
26
- if (!token) {
27
- throw new Error('BRIER_TOKEN is required. Pass --token or set BRIER_TOKEN env var');
28
- }
29
- return {
30
- serverUrl,
31
- token,
32
- hostname: getHostname(),
33
- os: `${osType()} ${platform()} ${arch()}`,
34
- runtimes: detectRuntimes(),
35
- version: readCliVersion(),
36
- };
37
- };
38
- export const toWsUrl = (serverUrl) => {
39
- return (serverUrl
40
- .replace(/^https:\/\//, 'wss://')
41
- .replace(/^http:\/\//, 'ws://')
42
- .replace(/\/$/, '') + '/tunnel');
43
- };
44
- /** 探测本机已安装的 AI runtime(与执行共用同一可执行文件解析)。 */
45
- const detectRuntimes = () => RUNTIME_REGISTRY.filter((r) => resolveRuntimeExecutable(r.name) !== null).map((r) => r.name);
@@ -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,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;
package/types/types.d.ts DELETED
@@ -1,78 +0,0 @@
1
- export type DaemonStatus = 'running' | 'stopped' | 'error';
2
- export type TunnelState = 'connecting' | 'connected' | 'disconnected' | 'reconnecting' | 'error';
3
- export type StreamType = 'stdout' | 'stderr';
4
- export type ClientMessage = {
5
- type: 'auth';
6
- token: string;
7
- hostname: string;
8
- os: string;
9
- runtimes: string[];
10
- version?: string;
11
- } | {
12
- type: 'heartbeat';
13
- timestamp: number;
14
- } | {
15
- type: 'task-output';
16
- taskId: string;
17
- stream: StreamType;
18
- data: string;
19
- } | {
20
- type: 'task-complete';
21
- taskId: string;
22
- exitCode: number;
23
- } | {
24
- type: 'task-error';
25
- taskId: string;
26
- error: string;
27
- } | {
28
- type: 'runtime-info';
29
- runtimes: string[];
30
- };
31
- export type ServerMessage = {
32
- type: 'auth-ok';
33
- computerId: string;
34
- } | {
35
- type: 'auth-failed';
36
- reason: string;
37
- } | {
38
- type: 'heartbeat-ack';
39
- timestamp: number;
40
- } | {
41
- type: 'task-start';
42
- taskId: string;
43
- runtime: string;
44
- command: string;
45
- args: string[];
46
- cwd?: string;
47
- env?: Record<string, string>;
48
- prompt?: string;
49
- } | {
50
- type: 'task-cancel';
51
- taskId: string;
52
- } | {
53
- type: 'query-runtimes';
54
- };
55
- export interface TaskInfo {
56
- taskId: string;
57
- runtime: string;
58
- command: string;
59
- args: string[];
60
- cwd?: string;
61
- env?: Record<string, string>;
62
- /** 自然语言指令(AI runtime 模式:拼到 runtime 命令参数中执行) */
63
- prompt?: string;
64
- }
65
- export interface DaemonConfig {
66
- serverUrl: string;
67
- token: string;
68
- hostname: string;
69
- os: string;
70
- runtimes: string[];
71
- /** CLI 自身版本(package.json),Auth 时上报给服务端展示。 */
72
- version: string;
73
- }
74
- export interface PidFileData {
75
- pid: number;
76
- startTime: number;
77
- serverUrl: string;
78
- }
File without changes
File without changes
File without changes