@brierb/brier-cli 0.0.5 → 0.0.7
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 +4 -40
- package/dist/daemon/TaskExecutor.js +27 -7
- package/dist/runtimes.js +52 -0
- package/dist/tunnel/TunnelClient.js +1 -0
- package/package.json +1 -1
- package/types/runtimes.d.ts +15 -0
- package/types/types.d.ts +3 -0
package/dist/config.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { hostname as getHostname, type as osType, arch, platform, homedir } from 'node:os';
|
|
2
|
-
import {
|
|
3
|
-
import { accessSync, constants, readFileSync } from 'node:fs';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
4
3
|
import { join } from 'node:path';
|
|
5
4
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { RUNTIME_REGISTRY } from './runtimes.js';
|
|
5
|
+
import { resolveRuntimeExecutable, RUNTIME_REGISTRY } from './runtimes.js';
|
|
7
6
|
export const BRIER_DIR = join(homedir(), '.brier');
|
|
8
7
|
export const PID_FILE = join(BRIER_DIR, 'daemon.pid');
|
|
9
8
|
export const LOG_FILE = join(BRIER_DIR, 'daemon.log');
|
|
@@ -42,40 +41,5 @@ export const toWsUrl = (serverUrl) => {
|
|
|
42
41
|
.replace(/^http:\/\//, 'ws://')
|
|
43
42
|
.replace(/\/$/, '') + '/tunnel');
|
|
44
43
|
};
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
};
|
|
44
|
+
/** 探测本机已安装的 AI runtime(与执行共用同一可执行文件解析)。 */
|
|
45
|
+
const detectRuntimes = () => RUNTIME_REGISTRY.filter((r) => resolveRuntimeExecutable(r.name) !== null).map((r) => r.name);
|
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { logger } from '../logger.js';
|
|
3
|
-
import {
|
|
3
|
+
import { resolveRuntimeExecutable, RUNTIME_PROMPT_FLAGS } from '../runtimes.js';
|
|
4
4
|
const MAX_CONCURRENT = 3;
|
|
5
5
|
export const createTaskExecutor = (callbacks) => {
|
|
6
6
|
const processes = new Map();
|
|
7
|
+
/**
|
|
8
|
+
* 解析要执行的命令:
|
|
9
|
+
* - 显式 command:直接用(调用方负责其可执行性)
|
|
10
|
+
* - runtime 模式:解析为**绝对路径**(PATH 或官方安装目录),
|
|
11
|
+
* 避免 daemon 进程 PATH 不含 runtime 目录时 spawn ENOENT
|
|
12
|
+
*/
|
|
7
13
|
const resolveCommand = (task) => {
|
|
8
14
|
if (task.command)
|
|
9
15
|
return task.command;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
return resolveRuntimeExecutable(task.runtime);
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* 拼执行参数:
|
|
20
|
+
* - prompt 模式(AI runtime):命令前缀 flags + prompt 原文
|
|
21
|
+
* - 命令模式:透传服务端给的 args
|
|
22
|
+
*/
|
|
23
|
+
const buildArgs = (task) => {
|
|
24
|
+
if (task.prompt) {
|
|
25
|
+
const flags = RUNTIME_PROMPT_FLAGS[task.runtime] ?? [];
|
|
26
|
+
return [...flags, task.prompt];
|
|
27
|
+
}
|
|
28
|
+
return task.args ?? [];
|
|
14
29
|
};
|
|
15
30
|
const execute = (task) => {
|
|
16
31
|
if (processes.has(task.taskId)) {
|
|
@@ -22,11 +37,16 @@ export const createTaskExecutor = (callbacks) => {
|
|
|
22
37
|
return;
|
|
23
38
|
}
|
|
24
39
|
const cmd = resolveCommand(task);
|
|
40
|
+
if (!cmd) {
|
|
41
|
+
callbacks.onError(task.taskId, `Cannot resolve command for runtime: ${task.runtime} (no command provided)`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const args = buildArgs(task);
|
|
25
45
|
const childEnv = task.env ? { ...process.env, ...task.env } : process.env;
|
|
26
|
-
logger.info(`Task ${task.taskId} starting: ${cmd} ${
|
|
46
|
+
logger.info(`Task ${task.taskId} starting: ${cmd} ${args.join(' ')}`, task.runtime);
|
|
27
47
|
let child;
|
|
28
48
|
try {
|
|
29
|
-
child = spawn(cmd,
|
|
49
|
+
child = spawn(cmd, args, {
|
|
30
50
|
cwd: task.cwd,
|
|
31
51
|
env: childEnv,
|
|
32
52
|
stdio: ['pipe', 'pipe', 'pipe'],
|
package/dist/runtimes.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { accessSync, constants } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
1
5
|
export const RUNTIME_REGISTRY = [
|
|
2
6
|
{ name: 'Claude Code', command: 'claude', fallbacks: ['~/.claude/bin/claude'] },
|
|
3
7
|
{ name: 'Codex CLI', command: 'codex' },
|
|
@@ -14,3 +18,51 @@ export const RUNTIME_REGISTRY = [
|
|
|
14
18
|
{ name: 'Cody', command: 'cody', fallbacks: ['~/.local/bin/cody'] },
|
|
15
19
|
];
|
|
16
20
|
export const RUNTIME_COMMANDS = Object.fromEntries(RUNTIME_REGISTRY.map((r) => [r.name, r.command]));
|
|
21
|
+
/**
|
|
22
|
+
* AI runtime 单次(非交互)执行参数模板:命令前缀 flags,后面紧跟 prompt。
|
|
23
|
+
* 服务端 task-start 携带 prompt 时,CLI 用它拼出
|
|
24
|
+
* `spawn(<command>, [...flags, prompt])`;未列出的 runtime 按裸参数执行。
|
|
25
|
+
*/
|
|
26
|
+
export const RUNTIME_PROMPT_FLAGS = {
|
|
27
|
+
OpenCode: ['run'],
|
|
28
|
+
'Claude Code': ['-p'],
|
|
29
|
+
'Codex CLI': ['exec'],
|
|
30
|
+
'Gemini CLI': ['-p'],
|
|
31
|
+
Aider: ['--message'],
|
|
32
|
+
Goose: ['run'],
|
|
33
|
+
};
|
|
34
|
+
// macOS 自带磁盘分区工具 gpt(8) 位于 /usr/sbin/gpt,命中系统目录视为未安装。
|
|
35
|
+
const SYSTEM_SBIN_RE = /^\/(usr\/)?sbin\//;
|
|
36
|
+
const isExecutable = (file) => {
|
|
37
|
+
try {
|
|
38
|
+
accessSync(file, constants.X_OK);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const expandHome = (p) => (p.startsWith('~/') ? join(homedir(), p.slice(2)) : p);
|
|
46
|
+
/**
|
|
47
|
+
* 解析 runtime 的可执行文件**绝对路径**:
|
|
48
|
+
* 1. PATH 中查找(排除系统目录误报,如 /usr/sbin/gpt)
|
|
49
|
+
* 2. 未命中则检查官方安装目录(如 opencode → ~/.opencode/bin/opencode)
|
|
50
|
+
*
|
|
51
|
+
* 探测(runtimes 上报)与执行(spawn 任务)都走这里,保证"探测到"的
|
|
52
|
+
* runtime 一定可被执行,不依赖 daemon 进程的 PATH。
|
|
53
|
+
*/
|
|
54
|
+
export const resolveRuntimeExecutable = (runtime) => {
|
|
55
|
+
const entry = RUNTIME_REGISTRY.find((r) => r.name === runtime);
|
|
56
|
+
if (!entry)
|
|
57
|
+
return null;
|
|
58
|
+
try {
|
|
59
|
+
const hit = execSync(`command -v ${entry.command}`, { stdio: 'pipe' }).toString().trim();
|
|
60
|
+
if (hit.includes('/') && !SYSTEM_SBIN_RE.test(hit))
|
|
61
|
+
return hit;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// 不在 PATH
|
|
65
|
+
}
|
|
66
|
+
const candidates = entry.fallbacks ?? [`~/.local/bin/${entry.command}`];
|
|
67
|
+
return candidates.map(expandHome).find(isExecutable) ?? null;
|
|
68
|
+
};
|
package/package.json
CHANGED
package/types/runtimes.d.ts
CHANGED
|
@@ -6,3 +6,18 @@ export interface RuntimeEntry {
|
|
|
6
6
|
}
|
|
7
7
|
export declare const RUNTIME_REGISTRY: readonly RuntimeEntry[];
|
|
8
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[]>;
|
|
15
|
+
/**
|
|
16
|
+
* 解析 runtime 的可执行文件**绝对路径**:
|
|
17
|
+
* 1. PATH 中查找(排除系统目录误报,如 /usr/sbin/gpt)
|
|
18
|
+
* 2. 未命中则检查官方安装目录(如 opencode → ~/.opencode/bin/opencode)
|
|
19
|
+
*
|
|
20
|
+
* 探测(runtimes 上报)与执行(spawn 任务)都走这里,保证"探测到"的
|
|
21
|
+
* runtime 一定可被执行,不依赖 daemon 进程的 PATH。
|
|
22
|
+
*/
|
|
23
|
+
export declare const resolveRuntimeExecutable: (runtime: string) => string | null;
|
package/types/types.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ export type ServerMessage = {
|
|
|
45
45
|
args: string[];
|
|
46
46
|
cwd?: string;
|
|
47
47
|
env?: Record<string, string>;
|
|
48
|
+
prompt?: string;
|
|
48
49
|
} | {
|
|
49
50
|
type: 'task-cancel';
|
|
50
51
|
taskId: string;
|
|
@@ -58,6 +59,8 @@ export interface TaskInfo {
|
|
|
58
59
|
args: string[];
|
|
59
60
|
cwd?: string;
|
|
60
61
|
env?: Record<string, string>;
|
|
62
|
+
/** 自然语言指令(AI runtime 模式:拼到 runtime 命令参数中执行) */
|
|
63
|
+
prompt?: string;
|
|
61
64
|
}
|
|
62
65
|
export interface DaemonConfig {
|
|
63
66
|
serverUrl: string;
|