@brierb/brier-cli 0.0.4 → 0.0.6
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.
- package/dist/config.js +43 -6
- package/dist/daemon/TaskExecutor.js +21 -6
- package/dist/index.js +0 -0
- package/dist/runtimes.js +23 -4
- package/dist/tunnel/TunnelClient.js +2 -0
- package/package.json +1 -1
- package/types/runtimes.d.ts +8 -0
- package/types/types.d.ts +6 -0
package/dist/config.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
import { hostname as getHostname, type as osType, arch, platform } from 'node:os';
|
|
1
|
+
import { hostname as getHostname, type as osType, arch, platform, homedir } from 'node:os';
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
|
-
import {
|
|
3
|
+
import { accessSync, constants, readFileSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { RUNTIME_REGISTRY } from './runtimes.js';
|
|
6
7
|
export const BRIER_DIR = join(homedir(), '.brier');
|
|
7
8
|
export const PID_FILE = join(BRIER_DIR, 'daemon.pid');
|
|
8
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
|
+
};
|
|
9
21
|
export const loadConfig = (options) => {
|
|
10
22
|
const serverUrl = options.serverUrl ?? process.env.BRIER_SERVER_URL;
|
|
11
23
|
const token = options.token ?? process.env.BRIER_TOKEN;
|
|
@@ -21,6 +33,7 @@ export const loadConfig = (options) => {
|
|
|
21
33
|
hostname: getHostname(),
|
|
22
34
|
os: `${osType()} ${platform()} ${arch()}`,
|
|
23
35
|
runtimes: detectRuntimes(),
|
|
36
|
+
version: readCliVersion(),
|
|
24
37
|
};
|
|
25
38
|
};
|
|
26
39
|
export const toWsUrl = (serverUrl) => {
|
|
@@ -29,16 +42,40 @@ export const toWsUrl = (serverUrl) => {
|
|
|
29
42
|
.replace(/^http:\/\//, 'ws://')
|
|
30
43
|
.replace(/\/$/, '') + '/tunnel');
|
|
31
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);
|
|
32
62
|
const detectRuntimes = () => {
|
|
33
63
|
const runtimes = [];
|
|
34
|
-
for (const { name, command } of RUNTIME_REGISTRY) {
|
|
64
|
+
for (const { name, command, fallbacks } of RUNTIME_REGISTRY) {
|
|
65
|
+
let found = false;
|
|
35
66
|
try {
|
|
36
|
-
execSync(`command -v ${command}`, { stdio: 'pipe' });
|
|
37
|
-
|
|
67
|
+
const hit = execSync(`command -v ${command}`, { stdio: 'pipe' }).toString().trim();
|
|
68
|
+
found = isRealExecutablePath(hit);
|
|
38
69
|
}
|
|
39
70
|
catch {
|
|
40
|
-
//
|
|
71
|
+
// 不在 PATH
|
|
41
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);
|
|
42
79
|
}
|
|
43
80
|
return runtimes;
|
|
44
81
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { logger } from '../logger.js';
|
|
3
|
-
import { RUNTIME_COMMANDS } from '../runtimes.js';
|
|
3
|
+
import { RUNTIME_COMMANDS, RUNTIME_PROMPT_FLAGS } from '../runtimes.js';
|
|
4
4
|
const MAX_CONCURRENT = 3;
|
|
5
5
|
export const createTaskExecutor = (callbacks) => {
|
|
6
6
|
const processes = new Map();
|
|
@@ -8,9 +8,19 @@ export const createTaskExecutor = (callbacks) => {
|
|
|
8
8
|
if (task.command)
|
|
9
9
|
return task.command;
|
|
10
10
|
const mapped = RUNTIME_COMMANDS[task.runtime];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
return mapped ?? null;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* 拼执行参数:
|
|
15
|
+
* - prompt 模式(AI runtime):命令前缀 flags + prompt 原文
|
|
16
|
+
* - 命令模式:透传服务端给的 args
|
|
17
|
+
*/
|
|
18
|
+
const buildArgs = (task) => {
|
|
19
|
+
if (task.prompt) {
|
|
20
|
+
const flags = RUNTIME_PROMPT_FLAGS[task.runtime] ?? [];
|
|
21
|
+
return [...flags, task.prompt];
|
|
22
|
+
}
|
|
23
|
+
return task.args ?? [];
|
|
14
24
|
};
|
|
15
25
|
const execute = (task) => {
|
|
16
26
|
if (processes.has(task.taskId)) {
|
|
@@ -22,11 +32,16 @@ export const createTaskExecutor = (callbacks) => {
|
|
|
22
32
|
return;
|
|
23
33
|
}
|
|
24
34
|
const cmd = resolveCommand(task);
|
|
35
|
+
if (!cmd) {
|
|
36
|
+
callbacks.onError(task.taskId, `Cannot resolve command for runtime: ${task.runtime} (no command provided)`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const args = buildArgs(task);
|
|
25
40
|
const childEnv = task.env ? { ...process.env, ...task.env } : process.env;
|
|
26
|
-
logger.info(`Task ${task.taskId} starting: ${cmd} ${
|
|
41
|
+
logger.info(`Task ${task.taskId} starting: ${cmd} ${args.join(' ')}`, task.runtime);
|
|
27
42
|
let child;
|
|
28
43
|
try {
|
|
29
|
-
child = spawn(cmd,
|
|
44
|
+
child = spawn(cmd, args, {
|
|
30
45
|
cwd: task.cwd,
|
|
31
46
|
env: childEnv,
|
|
32
47
|
stdio: ['pipe', 'pipe', 'pipe'],
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/runtimes.js
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
export const RUNTIME_REGISTRY = [
|
|
2
|
-
{ name: 'Claude Code', command: 'claude' },
|
|
2
|
+
{ name: 'Claude Code', command: 'claude', fallbacks: ['~/.claude/bin/claude'] },
|
|
3
3
|
{ name: 'Codex CLI', command: 'codex' },
|
|
4
|
-
{ name: '
|
|
4
|
+
{ name: 'OpenAI CLI', command: 'openai' },
|
|
5
5
|
{ name: 'Gemini CLI', command: 'gemini' },
|
|
6
6
|
{ name: 'Cursor CLI', command: 'cursor' },
|
|
7
|
-
{
|
|
8
|
-
|
|
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'] },
|
|
9
15
|
];
|
|
10
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
|
+
};
|
|
@@ -84,6 +84,7 @@ export const createTunnelClient = (config, taskExecutor) => {
|
|
|
84
84
|
args: message.args,
|
|
85
85
|
cwd: message.cwd,
|
|
86
86
|
env: message.env,
|
|
87
|
+
prompt: message.prompt,
|
|
87
88
|
});
|
|
88
89
|
break;
|
|
89
90
|
case 'task-cancel':
|
|
@@ -138,6 +139,7 @@ export const createTunnelClient = (config, taskExecutor) => {
|
|
|
138
139
|
hostname: config.hostname,
|
|
139
140
|
os: config.os,
|
|
140
141
|
runtimes: config.runtimes,
|
|
142
|
+
version: config.version,
|
|
141
143
|
});
|
|
142
144
|
});
|
|
143
145
|
ws.on('message', (data) => {
|
package/package.json
CHANGED
package/types/runtimes.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
export interface RuntimeEntry {
|
|
2
2
|
name: string;
|
|
3
3
|
command: string;
|
|
4
|
+
/** 命令不在 PATH 时的兜底可执行文件(~ 开头表示用户主目录),存在即视为已安装 */
|
|
5
|
+
fallbacks?: readonly string[];
|
|
4
6
|
}
|
|
5
7
|
export declare const RUNTIME_REGISTRY: readonly RuntimeEntry[];
|
|
6
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[]>;
|
package/types/types.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type ClientMessage = {
|
|
|
7
7
|
hostname: string;
|
|
8
8
|
os: string;
|
|
9
9
|
runtimes: string[];
|
|
10
|
+
version?: string;
|
|
10
11
|
} | {
|
|
11
12
|
type: 'heartbeat';
|
|
12
13
|
timestamp: number;
|
|
@@ -44,6 +45,7 @@ export type ServerMessage = {
|
|
|
44
45
|
args: string[];
|
|
45
46
|
cwd?: string;
|
|
46
47
|
env?: Record<string, string>;
|
|
48
|
+
prompt?: string;
|
|
47
49
|
} | {
|
|
48
50
|
type: 'task-cancel';
|
|
49
51
|
taskId: string;
|
|
@@ -57,6 +59,8 @@ export interface TaskInfo {
|
|
|
57
59
|
args: string[];
|
|
58
60
|
cwd?: string;
|
|
59
61
|
env?: Record<string, string>;
|
|
62
|
+
/** 自然语言指令(AI runtime 模式:拼到 runtime 命令参数中执行) */
|
|
63
|
+
prompt?: string;
|
|
60
64
|
}
|
|
61
65
|
export interface DaemonConfig {
|
|
62
66
|
serverUrl: string;
|
|
@@ -64,6 +68,8 @@ export interface DaemonConfig {
|
|
|
64
68
|
hostname: string;
|
|
65
69
|
os: string;
|
|
66
70
|
runtimes: string[];
|
|
71
|
+
/** CLI 自身版本(package.json),Auth 时上报给服务端展示。 */
|
|
72
|
+
version: string;
|
|
67
73
|
}
|
|
68
74
|
export interface PidFileData {
|
|
69
75
|
pid: number;
|