@brierb/brier-cli 0.0.4
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/commands/restart.js +28 -0
- package/dist/commands/start.js +13 -0
- package/dist/commands/status.js +27 -0
- package/dist/commands/stop.js +6 -0
- package/dist/config.js +44 -0
- package/dist/daemon/DaemonManager.js +127 -0
- package/dist/daemon/DaemonRunner.js +70 -0
- package/dist/daemon/TaskExecutor.js +76 -0
- package/dist/index.js +62 -0
- package/dist/logger.js +60 -0
- package/dist/runtimes.js +10 -0
- package/dist/tunnel/TunnelClient.js +216 -0
- package/dist/types.js +1 -0
- package/package.json +53 -0
- package/types/commands/restart.d.ts +5 -0
- package/types/commands/start.d.ts +5 -0
- package/types/commands/status.d.ts +1 -0
- package/types/commands/stop.d.ts +1 -0
- package/types/config.d.ts +9 -0
- package/types/daemon/DaemonManager.d.ts +14 -0
- package/types/daemon/DaemonRunner.d.ts +1 -0
- package/types/daemon/TaskExecutor.d.ts +12 -0
- package/types/index.d.ts +2 -0
- package/types/logger.d.ts +6 -0
- package/types/runtimes.d.ts +6 -0
- package/types/tunnel/TunnelClient.d.ts +10 -0
- package/types/types.d.ts +72 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createDaemonManager } from '../daemon/DaemonManager.js';
|
|
2
|
+
import { loadConfig } from '../config.js';
|
|
3
|
+
export const restartCommand = async (options) => {
|
|
4
|
+
let serverUrl = options.serverUrl;
|
|
5
|
+
let token = options.token ?? process.env.BRIER_TOKEN;
|
|
6
|
+
if (!serverUrl || !token) {
|
|
7
|
+
try {
|
|
8
|
+
const config = loadConfig({});
|
|
9
|
+
if (!serverUrl)
|
|
10
|
+
serverUrl = config.serverUrl;
|
|
11
|
+
if (!token)
|
|
12
|
+
token = config.token;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// Will be caught below
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (!serverUrl) {
|
|
19
|
+
throw new Error('Server URL is required. Pass --server-url or set BRIER_SERVER_URL.');
|
|
20
|
+
}
|
|
21
|
+
if (!token) {
|
|
22
|
+
throw new Error('BRIER_TOKEN is required. Pass --token or set BRIER_TOKEN env var.');
|
|
23
|
+
}
|
|
24
|
+
const manager = createDaemonManager();
|
|
25
|
+
await manager.restart({ serverUrl, token });
|
|
26
|
+
console.log('✓ 后台服务已重启');
|
|
27
|
+
console.log(` Server: ${serverUrl}`);
|
|
28
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createDaemonManager } from '../daemon/DaemonManager.js';
|
|
2
|
+
import { LOG_FILE } from '../config.js';
|
|
3
|
+
export const startCommand = async (options) => {
|
|
4
|
+
const token = options.token ?? process.env.BRIER_TOKEN;
|
|
5
|
+
if (!token) {
|
|
6
|
+
throw new Error('BRIER_TOKEN is required. Pass --token or set BRIER_TOKEN env var.');
|
|
7
|
+
}
|
|
8
|
+
const manager = createDaemonManager();
|
|
9
|
+
await manager.start({ serverUrl: options.serverUrl, token });
|
|
10
|
+
console.log('✓ 后台服务已启动');
|
|
11
|
+
console.log(` Server: ${options.serverUrl}`);
|
|
12
|
+
console.log(` Log: ${LOG_FILE}`);
|
|
13
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createDaemonManager } from '../daemon/DaemonManager.js';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { PID_FILE } from '../config.js';
|
|
4
|
+
export const statusCommand = () => {
|
|
5
|
+
const manager = createDaemonManager();
|
|
6
|
+
const status = manager.status();
|
|
7
|
+
if (status === 'running') {
|
|
8
|
+
let pidInfo = null;
|
|
9
|
+
if (existsSync(PID_FILE)) {
|
|
10
|
+
try {
|
|
11
|
+
pidInfo = JSON.parse(readFileSync(PID_FILE, 'utf-8'));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// ignore
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
console.log('● 后台服务运行中');
|
|
18
|
+
if (pidInfo) {
|
|
19
|
+
console.log(` PID: ${pidInfo.pid}`);
|
|
20
|
+
console.log(` Server: ${pidInfo.serverUrl}`);
|
|
21
|
+
console.log(` Started: ${new Date(pidInfo.startTime).toLocaleString()}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
console.log('○ 后台服务未运行');
|
|
26
|
+
}
|
|
27
|
+
};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { hostname as getHostname, type as osType, arch, platform } from 'node:os';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { 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
|
+
export const loadConfig = (options) => {
|
|
10
|
+
const serverUrl = options.serverUrl ?? process.env.BRIER_SERVER_URL;
|
|
11
|
+
const token = options.token ?? process.env.BRIER_TOKEN;
|
|
12
|
+
if (!serverUrl) {
|
|
13
|
+
throw new Error('Server URL is required. Use --server-url or set BRIER_SERVER_URL');
|
|
14
|
+
}
|
|
15
|
+
if (!token) {
|
|
16
|
+
throw new Error('BRIER_TOKEN is required. Pass --token or set BRIER_TOKEN env var');
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
serverUrl,
|
|
20
|
+
token,
|
|
21
|
+
hostname: getHostname(),
|
|
22
|
+
os: `${osType()} ${platform()} ${arch()}`,
|
|
23
|
+
runtimes: detectRuntimes(),
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export const toWsUrl = (serverUrl) => {
|
|
27
|
+
return (serverUrl
|
|
28
|
+
.replace(/^https:\/\//, 'wss://')
|
|
29
|
+
.replace(/^http:\/\//, 'ws://')
|
|
30
|
+
.replace(/\/$/, '') + '/tunnel');
|
|
31
|
+
};
|
|
32
|
+
const detectRuntimes = () => {
|
|
33
|
+
const runtimes = [];
|
|
34
|
+
for (const { name, command } of RUNTIME_REGISTRY) {
|
|
35
|
+
try {
|
|
36
|
+
execSync(`command -v ${command}`, { stdio: 'pipe' });
|
|
37
|
+
runtimes.push(name);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// not installed
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return runtimes;
|
|
44
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { BRIER_DIR, PID_FILE } from '../config.js';
|
|
6
|
+
import { logger } from '../logger.js';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
const RUNNER_SCRIPT = join(__dirname, 'DaemonRunner.js');
|
|
10
|
+
const isProcessRunning = (pid) => {
|
|
11
|
+
try {
|
|
12
|
+
process.kill(pid, 0);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
return err instanceof Error && 'code' in err && err.code === 'EPERM';
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const readPidFile = () => {
|
|
20
|
+
if (!existsSync(PID_FILE))
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
const data = readFileSync(PID_FILE, 'utf-8');
|
|
24
|
+
return JSON.parse(data);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const writePidFile = (data) => {
|
|
31
|
+
mkdirSync(BRIER_DIR, { recursive: true });
|
|
32
|
+
writeFileSync(PID_FILE, JSON.stringify(data, null, 2));
|
|
33
|
+
};
|
|
34
|
+
const removePidFile = () => {
|
|
35
|
+
if (existsSync(PID_FILE)) {
|
|
36
|
+
unlinkSync(PID_FILE);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const waitForExit = (pid, timeoutMs = 5000) => {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const startTime = Date.now();
|
|
42
|
+
const check = () => {
|
|
43
|
+
if (!isProcessRunning(pid)) {
|
|
44
|
+
resolve(true);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (Date.now() - startTime >= timeoutMs) {
|
|
48
|
+
resolve(false);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
setTimeout(check, 200);
|
|
52
|
+
};
|
|
53
|
+
check();
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
export const createDaemonManager = () => {
|
|
57
|
+
const start = async (options) => {
|
|
58
|
+
const existing = readPidFile();
|
|
59
|
+
if (existing && isProcessRunning(existing.pid)) {
|
|
60
|
+
throw new Error(`Daemon is already running (PID: ${existing.pid})`);
|
|
61
|
+
}
|
|
62
|
+
if (existing) {
|
|
63
|
+
removePidFile();
|
|
64
|
+
}
|
|
65
|
+
const childEnv = {
|
|
66
|
+
...process.env,
|
|
67
|
+
BRIER_TOKEN: options.token,
|
|
68
|
+
BRIER_SERVER_URL: options.serverUrl,
|
|
69
|
+
BRIER_DAEMON_MODE: '1',
|
|
70
|
+
};
|
|
71
|
+
const child = spawn(process.execPath, [RUNNER_SCRIPT], {
|
|
72
|
+
detached: true,
|
|
73
|
+
stdio: 'ignore',
|
|
74
|
+
env: childEnv,
|
|
75
|
+
});
|
|
76
|
+
child.unref();
|
|
77
|
+
if (typeof child.pid !== 'number') {
|
|
78
|
+
throw new Error('Failed to spawn daemon process');
|
|
79
|
+
}
|
|
80
|
+
writePidFile({
|
|
81
|
+
pid: child.pid,
|
|
82
|
+
startTime: Date.now(),
|
|
83
|
+
serverUrl: options.serverUrl,
|
|
84
|
+
});
|
|
85
|
+
logger.info('Daemon started, PID:', child.pid);
|
|
86
|
+
};
|
|
87
|
+
const stop = async () => {
|
|
88
|
+
const data = readPidFile();
|
|
89
|
+
if (!data) {
|
|
90
|
+
logger.warn('No PID file found, daemon may not be running');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!isProcessRunning(data.pid)) {
|
|
94
|
+
logger.info('Process not running, cleaning up PID file');
|
|
95
|
+
removePidFile();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
logger.info('Sending SIGTERM to PID:', data.pid);
|
|
99
|
+
process.kill(data.pid, 'SIGTERM');
|
|
100
|
+
const exited = await waitForExit(data.pid, 5000);
|
|
101
|
+
if (!exited) {
|
|
102
|
+
logger.warn('Process did not exit, sending SIGKILL');
|
|
103
|
+
try {
|
|
104
|
+
process.kill(data.pid, 'SIGKILL');
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
logger.error('Failed to kill process:', err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
removePidFile();
|
|
111
|
+
logger.info('Daemon stopped');
|
|
112
|
+
};
|
|
113
|
+
const restart = async (options) => {
|
|
114
|
+
await stop();
|
|
115
|
+
await start(options);
|
|
116
|
+
};
|
|
117
|
+
const status = () => {
|
|
118
|
+
const data = readPidFile();
|
|
119
|
+
if (!data)
|
|
120
|
+
return 'stopped';
|
|
121
|
+
if (isProcessRunning(data.pid))
|
|
122
|
+
return 'running';
|
|
123
|
+
removePidFile();
|
|
124
|
+
return 'stopped';
|
|
125
|
+
};
|
|
126
|
+
return { start, stop, restart, status };
|
|
127
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { loadConfig, LOG_FILE } from '../config.js';
|
|
2
|
+
import { configureLogger, logger } from '../logger.js';
|
|
3
|
+
import { createTunnelClient } from '../tunnel/TunnelClient.js';
|
|
4
|
+
import { createTaskExecutor } from './TaskExecutor.js';
|
|
5
|
+
let tunnel = null;
|
|
6
|
+
const shutdown = async (signal) => {
|
|
7
|
+
logger.info(`Received ${signal}, shutting down...`);
|
|
8
|
+
if (tunnel) {
|
|
9
|
+
await tunnel.stop();
|
|
10
|
+
tunnel = null;
|
|
11
|
+
}
|
|
12
|
+
process.exit(0);
|
|
13
|
+
};
|
|
14
|
+
const handleUncaughtError = (err) => {
|
|
15
|
+
logger.error('Uncaught error:', err.message);
|
|
16
|
+
if (tunnel) {
|
|
17
|
+
tunnel.stop().finally(() => process.exit(1));
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const run = (config) => {
|
|
24
|
+
const safeSend = (message) => {
|
|
25
|
+
try {
|
|
26
|
+
tunnel?.send(message);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Connection not ready, output is dropped
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const taskExecutor = createTaskExecutor({
|
|
33
|
+
onOutput: (taskId, stream, data) => safeSend({ type: 'task-output', taskId, stream, data }),
|
|
34
|
+
onComplete: (taskId, exitCode) => safeSend({ type: 'task-complete', taskId, exitCode }),
|
|
35
|
+
onError: (taskId, error) => safeSend({ type: 'task-error', taskId, error }),
|
|
36
|
+
});
|
|
37
|
+
tunnel = createTunnelClient(config, taskExecutor);
|
|
38
|
+
tunnel.onStateChange((state) => {
|
|
39
|
+
logger.info('Tunnel state:', state);
|
|
40
|
+
});
|
|
41
|
+
tunnel.start();
|
|
42
|
+
logger.info('Daemon runner started');
|
|
43
|
+
logger.info('Server:', config.serverUrl);
|
|
44
|
+
logger.info('Hostname:', config.hostname);
|
|
45
|
+
logger.info('OS:', config.os);
|
|
46
|
+
logger.info('Runtimes:', config.runtimes.join(', ') || 'none detected');
|
|
47
|
+
};
|
|
48
|
+
const main = () => {
|
|
49
|
+
if (process.env.BRIER_DAEMON_MODE !== '1') {
|
|
50
|
+
console.error('This script is intended to be run as a daemon. Use "brier daemon start" instead.');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
configureLogger(LOG_FILE);
|
|
54
|
+
let config;
|
|
55
|
+
try {
|
|
56
|
+
config = loadConfig({});
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
logger.error('Config error:', err instanceof Error ? err.message : String(err));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
process.on('SIGTERM', () => void shutdown('SIGTERM'));
|
|
63
|
+
process.on('SIGINT', () => void shutdown('SIGINT'));
|
|
64
|
+
process.on('uncaughtException', handleUncaughtError);
|
|
65
|
+
process.on('unhandledRejection', (reason) => {
|
|
66
|
+
logger.error('Unhandled rejection:', reason);
|
|
67
|
+
});
|
|
68
|
+
run(config);
|
|
69
|
+
};
|
|
70
|
+
main();
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
3
|
+
import { RUNTIME_COMMANDS } from '../runtimes.js';
|
|
4
|
+
const MAX_CONCURRENT = 3;
|
|
5
|
+
export const createTaskExecutor = (callbacks) => {
|
|
6
|
+
const processes = new Map();
|
|
7
|
+
const resolveCommand = (task) => {
|
|
8
|
+
if (task.command)
|
|
9
|
+
return task.command;
|
|
10
|
+
const mapped = RUNTIME_COMMANDS[task.runtime];
|
|
11
|
+
if (mapped)
|
|
12
|
+
return mapped;
|
|
13
|
+
throw new Error(`Cannot resolve command for runtime: ${task.runtime}`);
|
|
14
|
+
};
|
|
15
|
+
const execute = (task) => {
|
|
16
|
+
if (processes.has(task.taskId)) {
|
|
17
|
+
logger.warn(`Task ${task.taskId} is already running, cancelling previous instance`);
|
|
18
|
+
cancel(task.taskId);
|
|
19
|
+
}
|
|
20
|
+
if (processes.size >= MAX_CONCURRENT) {
|
|
21
|
+
callbacks.onError(task.taskId, `Max concurrent tasks (${MAX_CONCURRENT}) reached`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const cmd = resolveCommand(task);
|
|
25
|
+
const childEnv = task.env ? { ...process.env, ...task.env } : process.env;
|
|
26
|
+
logger.info(`Task ${task.taskId} starting: ${cmd} ${task.args.join(' ')}`, task.runtime);
|
|
27
|
+
let child;
|
|
28
|
+
try {
|
|
29
|
+
child = spawn(cmd, task.args, {
|
|
30
|
+
cwd: task.cwd,
|
|
31
|
+
env: childEnv,
|
|
32
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
callbacks.onError(task.taskId, err instanceof Error ? err.message : String(err));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
processes.set(task.taskId, child);
|
|
40
|
+
child.stdout?.on('data', (data) => {
|
|
41
|
+
callbacks.onOutput(task.taskId, 'stdout', data.toString());
|
|
42
|
+
});
|
|
43
|
+
child.stderr?.on('data', (data) => {
|
|
44
|
+
callbacks.onOutput(task.taskId, 'stderr', data.toString());
|
|
45
|
+
});
|
|
46
|
+
child.on('error', (err) => {
|
|
47
|
+
processes.delete(task.taskId);
|
|
48
|
+
logger.error(`Task ${task.taskId} process error:`, err.message);
|
|
49
|
+
callbacks.onError(task.taskId, err.message);
|
|
50
|
+
});
|
|
51
|
+
child.on('close', (code) => {
|
|
52
|
+
processes.delete(task.taskId);
|
|
53
|
+
const exitCode = code ?? 0;
|
|
54
|
+
logger.info(`Task ${task.taskId} completed with exit code ${exitCode}`);
|
|
55
|
+
callbacks.onComplete(task.taskId, exitCode);
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
const cancel = (taskId) => {
|
|
59
|
+
const child = processes.get(taskId);
|
|
60
|
+
if (!child) {
|
|
61
|
+
logger.warn(`Task ${taskId} not found, cannot cancel`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
child.kill('SIGTERM');
|
|
65
|
+
processes.delete(taskId);
|
|
66
|
+
logger.info(`Task ${taskId} cancelled`);
|
|
67
|
+
};
|
|
68
|
+
const cancelAll = () => {
|
|
69
|
+
for (const [taskId, child] of processes) {
|
|
70
|
+
child.kill('SIGTERM');
|
|
71
|
+
logger.info(`Task ${taskId} cancelled (shutdown)`);
|
|
72
|
+
}
|
|
73
|
+
processes.clear();
|
|
74
|
+
};
|
|
75
|
+
return { execute, cancel, cancelAll };
|
|
76
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { startCommand } from './commands/start.js';
|
|
4
|
+
import { stopCommand } from './commands/stop.js';
|
|
5
|
+
import { restartCommand } from './commands/restart.js';
|
|
6
|
+
import { statusCommand } from './commands/status.js';
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name('brier')
|
|
10
|
+
.description('Brier CLI - Connect to Brier platform via encrypted tunnel')
|
|
11
|
+
.version('0.0.1');
|
|
12
|
+
const daemon = program.command('daemon').description('Manage the brier background service');
|
|
13
|
+
daemon
|
|
14
|
+
.command('start')
|
|
15
|
+
.description('Start the background service')
|
|
16
|
+
.requiredOption('--server-url <url>', 'Brier server URL')
|
|
17
|
+
.option('--token <token>', 'BRIER_TOKEN (or set BRIER_TOKEN env var)')
|
|
18
|
+
.action(async (opts) => {
|
|
19
|
+
try {
|
|
20
|
+
await startCommand(opts);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
console.error('Error:', err instanceof Error ? err.message : String(err));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
daemon
|
|
28
|
+
.command('stop')
|
|
29
|
+
.description('Stop the background service')
|
|
30
|
+
.action(async () => {
|
|
31
|
+
try {
|
|
32
|
+
await stopCommand();
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
console.error('Error:', err instanceof Error ? err.message : String(err));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
daemon
|
|
40
|
+
.command('restart')
|
|
41
|
+
.description('Restart the background service')
|
|
42
|
+
.option('--server-url <url>', 'Brier server URL (defaults to previous config)')
|
|
43
|
+
.option('--token <token>', 'BRIER_TOKEN (or set BRIER_TOKEN env var)')
|
|
44
|
+
.action(async (opts) => {
|
|
45
|
+
try {
|
|
46
|
+
await restartCommand(opts);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.error('Error:', err instanceof Error ? err.message : String(err));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
daemon
|
|
54
|
+
.command('status')
|
|
55
|
+
.description('Check the background service status')
|
|
56
|
+
.action(() => {
|
|
57
|
+
statusCommand();
|
|
58
|
+
});
|
|
59
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
60
|
+
console.error('Fatal:', err instanceof Error ? err.message : String(err));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
});
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { mkdirSync, appendFileSync, statSync, renameSync, existsSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
const MAX_LOG_SIZE = 5 * 1024 * 1024;
|
|
4
|
+
let logFile;
|
|
5
|
+
export const configureLogger = (filePath) => {
|
|
6
|
+
logFile = filePath;
|
|
7
|
+
};
|
|
8
|
+
const formatMessage = (level, message, ...args) => {
|
|
9
|
+
const timestamp = new Date().toISOString();
|
|
10
|
+
const levelTag = level.toUpperCase().padEnd(5);
|
|
11
|
+
const rest = args.length > 0
|
|
12
|
+
? ' ' + args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ')
|
|
13
|
+
: '';
|
|
14
|
+
return `${timestamp} [${levelTag}] ${message}${rest}`;
|
|
15
|
+
};
|
|
16
|
+
const rotateIfNeeded = () => {
|
|
17
|
+
if (!logFile)
|
|
18
|
+
return;
|
|
19
|
+
try {
|
|
20
|
+
if (!existsSync(logFile))
|
|
21
|
+
return;
|
|
22
|
+
if (statSync(logFile).size < MAX_LOG_SIZE)
|
|
23
|
+
return;
|
|
24
|
+
const backup = logFile + '.1';
|
|
25
|
+
if (existsSync(backup)) {
|
|
26
|
+
try {
|
|
27
|
+
renameSync(backup, backup + '.old');
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* ignore */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
renameSync(logFile, backup);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// rotation failed, continue writing to current file
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const write = (level, message, ...args) => {
|
|
40
|
+
const formatted = formatMessage(level, message, ...args);
|
|
41
|
+
if (logFile) {
|
|
42
|
+
try {
|
|
43
|
+
mkdirSync(dirname(logFile), { recursive: true });
|
|
44
|
+
rotateIfNeeded();
|
|
45
|
+
appendFileSync(logFile, formatted + '\n');
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
console.error(formatted);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const method = level === 'info' ? 'log' : level;
|
|
53
|
+
console[method](formatted);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
export const logger = {
|
|
57
|
+
info: (message, ...args) => write('info', message, ...args),
|
|
58
|
+
warn: (message, ...args) => write('warn', message, ...args),
|
|
59
|
+
error: (message, ...args) => write('error', message, ...args),
|
|
60
|
+
};
|
package/dist/runtimes.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const RUNTIME_REGISTRY = [
|
|
2
|
+
{ name: 'Claude Code', command: 'claude' },
|
|
3
|
+
{ name: 'Codex CLI', command: 'codex' },
|
|
4
|
+
{ name: 'GPT-4o CLI', command: 'gpt' },
|
|
5
|
+
{ name: 'Gemini CLI', command: 'gemini' },
|
|
6
|
+
{ name: 'Cursor CLI', command: 'cursor' },
|
|
7
|
+
{ name: 'Node.js', command: 'node' },
|
|
8
|
+
{ name: 'Python', command: 'python3' },
|
|
9
|
+
];
|
|
10
|
+
export const RUNTIME_COMMANDS = Object.fromEntries(RUNTIME_REGISTRY.map((r) => [r.name, r.command]));
|
|
@@ -0,0 +1,216 @@
|
|
|
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
|
+
});
|
|
88
|
+
break;
|
|
89
|
+
case 'task-cancel':
|
|
90
|
+
logger.info('Task cancel:', message.taskId);
|
|
91
|
+
taskExecutor.cancel(message.taskId);
|
|
92
|
+
break;
|
|
93
|
+
case 'query-runtimes':
|
|
94
|
+
send({ type: 'runtime-info', runtimes: config.runtimes });
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const scheduleReconnect = () => {
|
|
99
|
+
if (!running)
|
|
100
|
+
return;
|
|
101
|
+
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
102
|
+
logger.error('Max reconnect attempts reached, stopping');
|
|
103
|
+
notifyStateChange('error');
|
|
104
|
+
running = false;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
reconnectAttempts++;
|
|
108
|
+
const delay = Math.min(BASE_RECONNECT_DELAY_MS * Math.pow(2, reconnectAttempts - 1), MAX_RECONNECT_DELAY_MS);
|
|
109
|
+
logger.info(`Reconnecting in ${(delay / 1000).toFixed(0)}s (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
|
|
110
|
+
notifyStateChange('reconnecting');
|
|
111
|
+
reconnectTimer = setTimeout(() => {
|
|
112
|
+
if (running) {
|
|
113
|
+
connect();
|
|
114
|
+
}
|
|
115
|
+
}, delay);
|
|
116
|
+
};
|
|
117
|
+
const connect = () => {
|
|
118
|
+
if (ws) {
|
|
119
|
+
ws.removeAllListeners();
|
|
120
|
+
ws.terminate();
|
|
121
|
+
ws = null;
|
|
122
|
+
}
|
|
123
|
+
const wsUrl = toWsUrl(config.serverUrl);
|
|
124
|
+
logger.info('Connecting to', wsUrl);
|
|
125
|
+
notifyStateChange('connecting');
|
|
126
|
+
ws = new WebSocket(wsUrl, {
|
|
127
|
+
headers: {
|
|
128
|
+
Authorization: `Bearer ${config.token}`,
|
|
129
|
+
'X-Brier-Hostname': config.hostname,
|
|
130
|
+
'X-Brier-OS': config.os,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
ws.on('open', () => {
|
|
134
|
+
logger.info('WebSocket connected, authenticating...');
|
|
135
|
+
send({
|
|
136
|
+
type: 'auth',
|
|
137
|
+
token: config.token,
|
|
138
|
+
hostname: config.hostname,
|
|
139
|
+
os: config.os,
|
|
140
|
+
runtimes: config.runtimes,
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
ws.on('message', (data) => {
|
|
144
|
+
try {
|
|
145
|
+
const message = JSON.parse(data.toString());
|
|
146
|
+
handleServerMessage(message);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
logger.error('Failed to parse server message:', err);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
ws.on('close', (code, reason) => {
|
|
153
|
+
const reasonStr = reason.toString() || `code ${code}`;
|
|
154
|
+
logger.warn(`WebSocket closed: ${reasonStr}`);
|
|
155
|
+
stopHeartbeat();
|
|
156
|
+
if (running) {
|
|
157
|
+
scheduleReconnect();
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
notifyStateChange('disconnected');
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
ws.on('error', (err) => {
|
|
164
|
+
logger.error('WebSocket error:', err.message);
|
|
165
|
+
});
|
|
166
|
+
ws.on('ping', () => {
|
|
167
|
+
ws?.pong();
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
const start = () => {
|
|
171
|
+
if (running) {
|
|
172
|
+
logger.warn('Tunnel is already running');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
running = true;
|
|
176
|
+
reconnectAttempts = 0;
|
|
177
|
+
connect();
|
|
178
|
+
};
|
|
179
|
+
const stop = async () => {
|
|
180
|
+
running = false;
|
|
181
|
+
stopHeartbeat();
|
|
182
|
+
taskExecutor.cancelAll();
|
|
183
|
+
if (reconnectTimer) {
|
|
184
|
+
clearTimeout(reconnectTimer);
|
|
185
|
+
reconnectTimer = null;
|
|
186
|
+
}
|
|
187
|
+
if (ws) {
|
|
188
|
+
const closePromise = new Promise((resolve) => {
|
|
189
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
190
|
+
ws.once('close', () => resolve());
|
|
191
|
+
ws.close(1000, 'client shutdown');
|
|
192
|
+
setTimeout(() => {
|
|
193
|
+
if (ws && ws.readyState !== WebSocket.CLOSED) {
|
|
194
|
+
ws.terminate();
|
|
195
|
+
}
|
|
196
|
+
resolve();
|
|
197
|
+
}, 3000);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
resolve();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
await closePromise;
|
|
204
|
+
ws = null;
|
|
205
|
+
}
|
|
206
|
+
notifyStateChange('disconnected');
|
|
207
|
+
logger.info('Tunnel stopped');
|
|
208
|
+
};
|
|
209
|
+
return {
|
|
210
|
+
start,
|
|
211
|
+
stop,
|
|
212
|
+
send,
|
|
213
|
+
getState,
|
|
214
|
+
onStateChange,
|
|
215
|
+
};
|
|
216
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@brierb/brier-cli",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"description": "Brier 平台接入命令行工具:通过加密隧道将本机接入 Brier 远程编码池,接收并执行远程任务",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"brier",
|
|
7
|
+
"cli",
|
|
8
|
+
"tunnel",
|
|
9
|
+
"agent"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"bin": {
|
|
14
|
+
"brier": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"types": "./types/index.d.ts"
|
|
20
|
+
},
|
|
21
|
+
"./tunnel": {
|
|
22
|
+
"import": "./dist/tunnel/TunnelClient.js",
|
|
23
|
+
"types": "./types/tunnel/TunnelClient.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"types"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc",
|
|
35
|
+
"dev": "tsc --watch",
|
|
36
|
+
"prepublishOnly": "pnpm run build",
|
|
37
|
+
"release:patch": "npm version patch --no-git-tag-version && npm publish --access public",
|
|
38
|
+
"release:minor": "npm version minor --no-git-tag-version && npm publish --access public",
|
|
39
|
+
"release:major": "npm version major --no-git-tag-version && npm publish --access public"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"commander": "^12.1.0",
|
|
46
|
+
"ws": "^8.18.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^24.13.3",
|
|
50
|
+
"@types/ws": "^8.5.13",
|
|
51
|
+
"typescript": "^6.0.3"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const statusCommand: () => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const stopCommand: () => Promise<void>;
|
|
@@ -0,0 +1,9 @@
|
|
|
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;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DaemonStatus } from '../types.js';
|
|
2
|
+
export interface DaemonManager {
|
|
3
|
+
start: (options: {
|
|
4
|
+
serverUrl: string;
|
|
5
|
+
token: string;
|
|
6
|
+
}) => Promise<void>;
|
|
7
|
+
stop: () => Promise<void>;
|
|
8
|
+
restart: (options: {
|
|
9
|
+
serverUrl: string;
|
|
10
|
+
token: string;
|
|
11
|
+
}) => Promise<void>;
|
|
12
|
+
status: () => DaemonStatus;
|
|
13
|
+
}
|
|
14
|
+
export declare const createDaemonManager: () => DaemonManager;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TaskInfo } from '../types.js';
|
|
2
|
+
export interface TaskExecutorCallbacks {
|
|
3
|
+
onOutput: (taskId: string, stream: 'stdout' | 'stderr', data: string) => void;
|
|
4
|
+
onComplete: (taskId: string, exitCode: number) => void;
|
|
5
|
+
onError: (taskId: string, error: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export interface TaskExecutor {
|
|
8
|
+
execute: (task: TaskInfo) => void;
|
|
9
|
+
cancel: (taskId: string) => void;
|
|
10
|
+
cancelAll: () => void;
|
|
11
|
+
}
|
|
12
|
+
export declare const createTaskExecutor: (callbacks: TaskExecutorCallbacks) => TaskExecutor;
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const configureLogger: (filePath: string | undefined) => void;
|
|
2
|
+
export declare const logger: {
|
|
3
|
+
info: (message: string, ...args: unknown[]) => void;
|
|
4
|
+
warn: (message: string, ...args: unknown[]) => void;
|
|
5
|
+
error: (message: string, ...args: unknown[]) => void;
|
|
6
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
} | {
|
|
11
|
+
type: 'heartbeat';
|
|
12
|
+
timestamp: number;
|
|
13
|
+
} | {
|
|
14
|
+
type: 'task-output';
|
|
15
|
+
taskId: string;
|
|
16
|
+
stream: StreamType;
|
|
17
|
+
data: string;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'task-complete';
|
|
20
|
+
taskId: string;
|
|
21
|
+
exitCode: number;
|
|
22
|
+
} | {
|
|
23
|
+
type: 'task-error';
|
|
24
|
+
taskId: string;
|
|
25
|
+
error: string;
|
|
26
|
+
} | {
|
|
27
|
+
type: 'runtime-info';
|
|
28
|
+
runtimes: string[];
|
|
29
|
+
};
|
|
30
|
+
export type ServerMessage = {
|
|
31
|
+
type: 'auth-ok';
|
|
32
|
+
computerId: string;
|
|
33
|
+
} | {
|
|
34
|
+
type: 'auth-failed';
|
|
35
|
+
reason: string;
|
|
36
|
+
} | {
|
|
37
|
+
type: 'heartbeat-ack';
|
|
38
|
+
timestamp: number;
|
|
39
|
+
} | {
|
|
40
|
+
type: 'task-start';
|
|
41
|
+
taskId: string;
|
|
42
|
+
runtime: string;
|
|
43
|
+
command: string;
|
|
44
|
+
args: string[];
|
|
45
|
+
cwd?: string;
|
|
46
|
+
env?: Record<string, string>;
|
|
47
|
+
} | {
|
|
48
|
+
type: 'task-cancel';
|
|
49
|
+
taskId: string;
|
|
50
|
+
} | {
|
|
51
|
+
type: 'query-runtimes';
|
|
52
|
+
};
|
|
53
|
+
export interface TaskInfo {
|
|
54
|
+
taskId: string;
|
|
55
|
+
runtime: string;
|
|
56
|
+
command: string;
|
|
57
|
+
args: string[];
|
|
58
|
+
cwd?: string;
|
|
59
|
+
env?: Record<string, string>;
|
|
60
|
+
}
|
|
61
|
+
export interface DaemonConfig {
|
|
62
|
+
serverUrl: string;
|
|
63
|
+
token: string;
|
|
64
|
+
hostname: string;
|
|
65
|
+
os: string;
|
|
66
|
+
runtimes: string[];
|
|
67
|
+
}
|
|
68
|
+
export interface PidFileData {
|
|
69
|
+
pid: number;
|
|
70
|
+
startTime: number;
|
|
71
|
+
serverUrl: string;
|
|
72
|
+
}
|