@agent-webui/ai-desk-daemon 1.0.20 → 1.0.21
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/README.md +44 -3
- package/bin/cli.js +231 -0
- package/dist/darwin-arm64/ai-desk-daemon +0 -0
- package/dist/darwin-x64/ai-desk-daemon +0 -0
- package/dist/linux-arm64/ai-desk-daemon +0 -0
- package/dist/linux-x64/ai-desk-daemon +0 -0
- package/dist/win32-x64/ai-desk-daemon.exe +0 -0
- package/lib/config.js +103 -0
- package/lib/daemon-manager.js +197 -0
- package/lib/platform.js +87 -0
- package/package.json +6 -1
- package/scripts/postinstall.js +7 -4
package/README.md
CHANGED
|
@@ -18,7 +18,48 @@
|
|
|
18
18
|
|
|
19
19
|
## 📦 安装
|
|
20
20
|
|
|
21
|
-
###
|
|
21
|
+
### 方式 1: npm 安装(纯 CLI 工具)
|
|
22
|
+
|
|
23
|
+
**适合场景**:开发者、服务器环境、CI/CD、命令行用户
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 全局安装
|
|
27
|
+
npm install -g agent-webui/ai-desk-daemon
|
|
28
|
+
|
|
29
|
+
# 启动 daemon
|
|
30
|
+
aidesk start
|
|
31
|
+
|
|
32
|
+
# 查看状态
|
|
33
|
+
aidesk status
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**包含内容**:
|
|
37
|
+
- ✅ AI Desk Daemon 后台服务
|
|
38
|
+
- ✅ CLI 命令行管理工具
|
|
39
|
+
- ✅ HTTP API (http://localhost:9527)
|
|
40
|
+
|
|
41
|
+
**不包含**:
|
|
42
|
+
- ❌ 系统托盘应用 (Tray)
|
|
43
|
+
- ❌ 桌面 GUI 应用
|
|
44
|
+
|
|
45
|
+
**可用命令**:
|
|
46
|
+
- `aidesk start` - 启动守护进程(后台运行)
|
|
47
|
+
- `aidesk start --log` - 启动守护进程(前台运行,跟随日志)
|
|
48
|
+
- `aidesk stop` - 停止守护进程
|
|
49
|
+
- `aidesk restart` - 重启守护进程
|
|
50
|
+
- `aidesk status` - 查看状态
|
|
51
|
+
- `aidesk logs` - 查看日志
|
|
52
|
+
- `aidesk logs -f` - 实时查看日志(不会停止守护进程)
|
|
53
|
+
|
|
54
|
+
📖 详细使用说明:[NPM_CLI.md](NPM_CLI.md)
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
### 方式 2: 完整安装(Daemon + Tray)
|
|
59
|
+
|
|
60
|
+
**适合场景**:桌面用户、需要系统托盘图标、完整 GUI 体验
|
|
61
|
+
|
|
62
|
+
#### macOS
|
|
22
63
|
|
|
23
64
|
```bash
|
|
24
65
|
# 下载并安装
|
|
@@ -28,7 +69,7 @@ curl -fsSL https://github.com/your-repo/ai-desk-desktop/releases/latest/download
|
|
|
28
69
|
./scripts/install-macos.sh
|
|
29
70
|
```
|
|
30
71
|
|
|
31
|
-
|
|
72
|
+
#### Linux
|
|
32
73
|
|
|
33
74
|
```bash
|
|
34
75
|
# Ubuntu/Debian
|
|
@@ -39,7 +80,7 @@ systemctl --user enable ai-desk-daemon
|
|
|
39
80
|
systemctl --user start ai-desk-daemon
|
|
40
81
|
```
|
|
41
82
|
|
|
42
|
-
|
|
83
|
+
#### Windows
|
|
43
84
|
|
|
44
85
|
```powershell
|
|
45
86
|
# 以管理员身份运行 PowerShell
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AI Desk Daemon CLI
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { program } = require('commander');
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const { start, stop, restart, status } = require('../lib/daemon-manager');
|
|
11
|
+
const { getLogPath } = require('../lib/platform');
|
|
12
|
+
const { VERSION } = require('../lib/platform');
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name('ai-desk-daemon')
|
|
16
|
+
.description('AI Desk Daemon - CLI tool for managing the AI Desk daemon service')
|
|
17
|
+
.version(VERSION);
|
|
18
|
+
|
|
19
|
+
// Start command
|
|
20
|
+
program
|
|
21
|
+
.command('start')
|
|
22
|
+
.description('Start the daemon')
|
|
23
|
+
.option('--log', 'Follow daemon logs in foreground (Ctrl+C to stop daemon)')
|
|
24
|
+
.action(async (options) => {
|
|
25
|
+
try {
|
|
26
|
+
const { getPidPath } = require('../lib/platform');
|
|
27
|
+
const daemonPid = start();
|
|
28
|
+
console.log(chalk.green('✓ Daemon started successfully'));
|
|
29
|
+
|
|
30
|
+
// Only follow logs if --log is specified
|
|
31
|
+
if (options.log) {
|
|
32
|
+
console.log(chalk.cyan('\n📋 Following daemon logs (Ctrl+C to stop daemon)...\n'));
|
|
33
|
+
|
|
34
|
+
// Wait a moment for daemon to start writing logs
|
|
35
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
36
|
+
|
|
37
|
+
const logPath = getLogPath();
|
|
38
|
+
if (!fs.existsSync(logPath)) {
|
|
39
|
+
console.log(chalk.yellow('Waiting for logs...'));
|
|
40
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Follow logs (tail -f) - works on Unix-like systems
|
|
44
|
+
if (process.platform !== 'win32') {
|
|
45
|
+
const { spawn } = require('child_process');
|
|
46
|
+
const tail = spawn('tail', ['-f', logPath]);
|
|
47
|
+
|
|
48
|
+
tail.stdout.on('data', (data) => {
|
|
49
|
+
process.stdout.write(data);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
tail.stderr.on('data', (data) => {
|
|
53
|
+
process.stderr.write(data);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
tail.on('error', (error) => {
|
|
57
|
+
console.error(chalk.red('Failed to follow logs:'), error.message);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Handle Ctrl+C - stop the daemon
|
|
62
|
+
process.on('SIGINT', () => {
|
|
63
|
+
console.log(chalk.yellow('\n\n⏹ Stopping daemon...'));
|
|
64
|
+
tail.kill();
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Stop the daemon
|
|
68
|
+
const { stop } = require('../lib/daemon-manager');
|
|
69
|
+
stop();
|
|
70
|
+
console.log(chalk.green('✓ Daemon stopped successfully'));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(chalk.red('✗ Failed to stop daemon:'), error.message);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
process.exit(0);
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
// Windows: use polling to read log file
|
|
79
|
+
console.log(chalk.yellow('Log following on Windows - press Ctrl+C to stop daemon\n'));
|
|
80
|
+
|
|
81
|
+
let lastSize = 0;
|
|
82
|
+
const pollInterval = setInterval(() => {
|
|
83
|
+
try {
|
|
84
|
+
const stats = fs.statSync(logPath);
|
|
85
|
+
if (stats.size > lastSize) {
|
|
86
|
+
const stream = fs.createReadStream(logPath, {
|
|
87
|
+
start: lastSize,
|
|
88
|
+
end: stats.size
|
|
89
|
+
});
|
|
90
|
+
stream.on('data', (chunk) => {
|
|
91
|
+
process.stdout.write(chunk);
|
|
92
|
+
});
|
|
93
|
+
lastSize = stats.size;
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
// Ignore errors
|
|
97
|
+
}
|
|
98
|
+
}, 500);
|
|
99
|
+
|
|
100
|
+
process.on('SIGINT', () => {
|
|
101
|
+
clearInterval(pollInterval);
|
|
102
|
+
console.log(chalk.yellow('\n\n⏹ Stopping daemon...'));
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// Stop the daemon
|
|
106
|
+
const { stop } = require('../lib/daemon-manager');
|
|
107
|
+
stop();
|
|
108
|
+
console.log(chalk.green('✓ Daemon stopped successfully'));
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(chalk.red('✗ Failed to stop daemon:'), error.message);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
process.exit(0);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error(chalk.red('✗ Failed to start daemon:'), error.message);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Stop command
|
|
124
|
+
program
|
|
125
|
+
.command('stop')
|
|
126
|
+
.description('Stop the daemon')
|
|
127
|
+
.action(async () => {
|
|
128
|
+
try {
|
|
129
|
+
stop();
|
|
130
|
+
console.log(chalk.green('✓ Daemon stopped successfully'));
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error(chalk.red('✗ Failed to stop daemon:'), error.message);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Restart command
|
|
138
|
+
program
|
|
139
|
+
.command('restart')
|
|
140
|
+
.description('Restart the daemon')
|
|
141
|
+
.action(async () => {
|
|
142
|
+
try {
|
|
143
|
+
restart();
|
|
144
|
+
console.log(chalk.green('✓ Daemon restarted successfully'));
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(chalk.red('✗ Failed to restart daemon:'), error.message);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Status command
|
|
152
|
+
program
|
|
153
|
+
.command('status')
|
|
154
|
+
.description('Check daemon status')
|
|
155
|
+
.action(async () => {
|
|
156
|
+
try {
|
|
157
|
+
const statusInfo = await status();
|
|
158
|
+
|
|
159
|
+
console.log('\n' + chalk.bold('AI Desk Daemon Status'));
|
|
160
|
+
console.log('─'.repeat(40));
|
|
161
|
+
|
|
162
|
+
if (statusInfo.running) {
|
|
163
|
+
console.log(chalk.green('● Running'));
|
|
164
|
+
|
|
165
|
+
if (statusInfo.health && statusInfo.health.data) {
|
|
166
|
+
const { status: healthStatus, version } = statusInfo.health.data;
|
|
167
|
+
console.log(`Version: ${version || 'unknown'}`);
|
|
168
|
+
console.log(`Status: ${healthStatus}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log(`Port: ${statusInfo.port}`);
|
|
172
|
+
console.log(`Dashboard: http://localhost:${statusInfo.port}/daemon-dashboard.html`);
|
|
173
|
+
} else {
|
|
174
|
+
console.log(chalk.red('○ Stopped'));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(`Logs: ${statusInfo.logPath}`);
|
|
178
|
+
console.log('');
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error(chalk.red('✗ Failed to get status:'), error.message);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Logs command
|
|
186
|
+
program
|
|
187
|
+
.command('logs')
|
|
188
|
+
.description('View daemon logs')
|
|
189
|
+
.option('-f, --follow', 'Follow log output')
|
|
190
|
+
.option('-n, --lines <number>', 'Number of lines to show', '50')
|
|
191
|
+
.action((options) => {
|
|
192
|
+
const logPath = getLogPath();
|
|
193
|
+
|
|
194
|
+
if (!fs.existsSync(logPath)) {
|
|
195
|
+
console.log(chalk.yellow('No logs found'));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (options.follow) {
|
|
200
|
+
// Follow logs (tail -f)
|
|
201
|
+
const { spawn } = require('child_process');
|
|
202
|
+
const tail = spawn('tail', ['-f', logPath]);
|
|
203
|
+
|
|
204
|
+
tail.stdout.on('data', (data) => {
|
|
205
|
+
process.stdout.write(data);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
tail.on('error', (error) => {
|
|
209
|
+
console.error(chalk.red('Failed to follow logs:'), error.message);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
});
|
|
212
|
+
} else {
|
|
213
|
+
// Show last N lines
|
|
214
|
+
const { execSync } = require('child_process');
|
|
215
|
+
try {
|
|
216
|
+
const output = execSync(`tail -n ${options.lines} "${logPath}"`, { encoding: 'utf8' });
|
|
217
|
+
console.log(output);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error(chalk.red('Failed to read logs:'), error.message);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
program.parse(process.argv);
|
|
226
|
+
|
|
227
|
+
// Show help if no command provided
|
|
228
|
+
if (!process.argv.slice(2).length) {
|
|
229
|
+
program.outputHelp();
|
|
230
|
+
}
|
|
231
|
+
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { getConfigDir, getConfigPath } = require('./platform');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_PORT = 9527;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Ensure config directory exists
|
|
13
|
+
*/
|
|
14
|
+
function ensureConfigDir() {
|
|
15
|
+
const configDir = getConfigDir();
|
|
16
|
+
const logsDir = path.join(configDir, 'logs');
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(configDir)) {
|
|
19
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(logsDir)) {
|
|
23
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load config from file
|
|
29
|
+
*/
|
|
30
|
+
function loadConfig() {
|
|
31
|
+
ensureConfigDir();
|
|
32
|
+
|
|
33
|
+
const configPath = getConfigPath();
|
|
34
|
+
|
|
35
|
+
if (!fs.existsSync(configPath)) {
|
|
36
|
+
// Create default config
|
|
37
|
+
const defaultConfig = {
|
|
38
|
+
port: DEFAULT_PORT,
|
|
39
|
+
logLevel: 'info',
|
|
40
|
+
autoStart: true
|
|
41
|
+
};
|
|
42
|
+
saveConfig(defaultConfig);
|
|
43
|
+
return defaultConfig;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
48
|
+
return JSON.parse(content);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Failed to load config:', error.message);
|
|
51
|
+
return { port: DEFAULT_PORT };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Save config to file
|
|
57
|
+
*/
|
|
58
|
+
function saveConfig(config) {
|
|
59
|
+
ensureConfigDir();
|
|
60
|
+
|
|
61
|
+
const configPath = getConfigPath();
|
|
62
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get port from config
|
|
67
|
+
*/
|
|
68
|
+
function getPort() {
|
|
69
|
+
const config = loadConfig();
|
|
70
|
+
const port = config.port || DEFAULT_PORT;
|
|
71
|
+
// Ensure port is a valid integer
|
|
72
|
+
const portNum = parseInt(port, 10);
|
|
73
|
+
if (isNaN(portNum) || portNum <= 0 || portNum > 65535) {
|
|
74
|
+
console.warn(`Invalid port ${port}, using default ${DEFAULT_PORT}`);
|
|
75
|
+
return DEFAULT_PORT;
|
|
76
|
+
}
|
|
77
|
+
return portNum;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Set port in config
|
|
82
|
+
*/
|
|
83
|
+
function setPort(port) {
|
|
84
|
+
const config = loadConfig();
|
|
85
|
+
// Ensure port is stored as integer
|
|
86
|
+
const portNum = parseInt(port, 10);
|
|
87
|
+
if (isNaN(portNum) || portNum <= 0 || portNum > 65535) {
|
|
88
|
+
throw new Error(`Invalid port number: ${port}`);
|
|
89
|
+
}
|
|
90
|
+
config.port = portNum;
|
|
91
|
+
saveConfig(config);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
ensureConfigDir,
|
|
96
|
+
loadConfig,
|
|
97
|
+
saveConfig,
|
|
98
|
+
getPort,
|
|
99
|
+
setPort,
|
|
100
|
+
getConfigPath, // Re-export from platform.js
|
|
101
|
+
DEFAULT_PORT
|
|
102
|
+
};
|
|
103
|
+
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon process management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const { spawn, execSync } = require('child_process');
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const { getDaemonBinaryPath, getPidPath, getLogPath } = require('./platform');
|
|
9
|
+
const { getPort, getConfigPath } = require('./config');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if daemon is running
|
|
13
|
+
*/
|
|
14
|
+
function isRunning() {
|
|
15
|
+
const pidPath = getPidPath();
|
|
16
|
+
|
|
17
|
+
if (!fs.existsSync(pidPath)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim());
|
|
23
|
+
|
|
24
|
+
// Check if process exists
|
|
25
|
+
process.kill(pid, 0);
|
|
26
|
+
return true;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
// Process doesn't exist
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get daemon health status
|
|
35
|
+
*/
|
|
36
|
+
function getHealth() {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const port = getPort();
|
|
39
|
+
const options = {
|
|
40
|
+
hostname: 'localhost',
|
|
41
|
+
port: port,
|
|
42
|
+
path: '/health',
|
|
43
|
+
method: 'GET',
|
|
44
|
+
timeout: 2000
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const req = http.request(options, (res) => {
|
|
48
|
+
let data = '';
|
|
49
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
50
|
+
res.on('end', () => {
|
|
51
|
+
try {
|
|
52
|
+
const health = JSON.parse(data);
|
|
53
|
+
resolve(health);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
resolve(null);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
req.on('error', () => resolve(null));
|
|
61
|
+
req.on('timeout', () => {
|
|
62
|
+
req.destroy();
|
|
63
|
+
resolve(null);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
req.end();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Start daemon
|
|
72
|
+
*/
|
|
73
|
+
function start() {
|
|
74
|
+
if (isRunning()) {
|
|
75
|
+
throw new Error('Daemon is already running');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const binaryPath = getDaemonBinaryPath();
|
|
79
|
+
if (!fs.existsSync(binaryPath)) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Daemon binary not found at: ${binaryPath}\n` +
|
|
82
|
+
`This might be a corrupted installation. Try reinstalling:\n` +
|
|
83
|
+
` npm install -g @ringcentral/ai-desk-daemon --force`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Ensure binary is executable (Unix-like systems)
|
|
88
|
+
if (process.platform !== 'win32') {
|
|
89
|
+
try {
|
|
90
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
// Ignore permission errors
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const port = getPort();
|
|
97
|
+
const configPath = getConfigPath();
|
|
98
|
+
const logPath = getLogPath();
|
|
99
|
+
|
|
100
|
+
// Ensure port is a number
|
|
101
|
+
const portNumber = typeof port === 'number' ? port : parseInt(port, 10);
|
|
102
|
+
if (isNaN(portNumber)) {
|
|
103
|
+
throw new Error(`Invalid port number: ${port}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Ensure log directory exists
|
|
107
|
+
const logDir = require('path').dirname(logPath);
|
|
108
|
+
if (!fs.existsSync(logDir)) {
|
|
109
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The daemon handles its own logging to file (see daemon/logger.go)
|
|
113
|
+
// It uses io.MultiWriter to write to both stdout and the log file
|
|
114
|
+
// So we should ignore stdout/stderr here to avoid duplication
|
|
115
|
+
const spawnOptions = {
|
|
116
|
+
detached: true,
|
|
117
|
+
stdio: ['ignore', 'ignore', 'ignore'] // Daemon writes to its own log file
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Windows-specific: hide console window
|
|
121
|
+
// This prevents the daemon process from showing a terminal window
|
|
122
|
+
if (process.platform === 'win32') {
|
|
123
|
+
spawnOptions.windowsHide = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const child = spawn(binaryPath, ['--port', portNumber.toString(), '--config', configPath], spawnOptions);
|
|
127
|
+
|
|
128
|
+
child.unref();
|
|
129
|
+
|
|
130
|
+
// Save PID
|
|
131
|
+
fs.writeFileSync(getPidPath(), child.pid.toString());
|
|
132
|
+
|
|
133
|
+
console.log(`Daemon started (PID: ${child.pid})`);
|
|
134
|
+
console.log(`Port: ${portNumber}`);
|
|
135
|
+
console.log(`Logs: ${logPath}`);
|
|
136
|
+
|
|
137
|
+
return child.pid;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Stop daemon
|
|
142
|
+
*/
|
|
143
|
+
function stop() {
|
|
144
|
+
const pidPath = getPidPath();
|
|
145
|
+
|
|
146
|
+
if (!fs.existsSync(pidPath)) {
|
|
147
|
+
throw new Error('Daemon is not running');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim());
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
process.kill(pid, 'SIGTERM');
|
|
154
|
+
fs.unlinkSync(pidPath);
|
|
155
|
+
console.log('Daemon stopped');
|
|
156
|
+
} catch (error) {
|
|
157
|
+
fs.unlinkSync(pidPath);
|
|
158
|
+
throw new Error(`Failed to stop daemon: ${error.message}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Restart daemon
|
|
164
|
+
*/
|
|
165
|
+
function restart() {
|
|
166
|
+
if (isRunning()) {
|
|
167
|
+
stop();
|
|
168
|
+
// Wait a bit for graceful shutdown
|
|
169
|
+
execSync('sleep 1');
|
|
170
|
+
}
|
|
171
|
+
start();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get daemon status
|
|
176
|
+
*/
|
|
177
|
+
async function status() {
|
|
178
|
+
const running = isRunning();
|
|
179
|
+
const health = running ? await getHealth() : null;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
running,
|
|
183
|
+
health,
|
|
184
|
+
port: getPort(),
|
|
185
|
+
logPath: getLogPath()
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = {
|
|
190
|
+
isRunning,
|
|
191
|
+
getHealth,
|
|
192
|
+
start,
|
|
193
|
+
stop,
|
|
194
|
+
restart,
|
|
195
|
+
status
|
|
196
|
+
};
|
|
197
|
+
|
package/lib/platform.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection and binary path resolution
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const VERSION = '1.0.21';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Detect current platform and architecture
|
|
12
|
+
*/
|
|
13
|
+
function detectPlatform() {
|
|
14
|
+
const platform = os.platform();
|
|
15
|
+
const arch = os.arch();
|
|
16
|
+
|
|
17
|
+
let platformKey;
|
|
18
|
+
|
|
19
|
+
// Map Node.js platform to binary directory name
|
|
20
|
+
switch (platform) {
|
|
21
|
+
case 'darwin':
|
|
22
|
+
platformKey = arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
|
|
23
|
+
break;
|
|
24
|
+
case 'linux':
|
|
25
|
+
platformKey = arch === 'arm64' ? 'linux-arm64' : 'linux-x64';
|
|
26
|
+
break;
|
|
27
|
+
case 'win32':
|
|
28
|
+
platformKey = 'win32-x64';
|
|
29
|
+
break;
|
|
30
|
+
default:
|
|
31
|
+
throw new Error(`Unsupported platform: ${platform}-${arch}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { platform, arch, platformKey };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get daemon binary path from npm package
|
|
39
|
+
*/
|
|
40
|
+
function getDaemonBinaryPath() {
|
|
41
|
+
const { platform, platformKey } = detectPlatform();
|
|
42
|
+
|
|
43
|
+
// Binary is in the npm package under dist/<platform>/
|
|
44
|
+
const binaryName = platform === 'win32' ? 'ai-desk-daemon.exe' : 'ai-desk-daemon';
|
|
45
|
+
const binaryPath = path.join(__dirname, '..', 'dist', platformKey, binaryName);
|
|
46
|
+
|
|
47
|
+
return binaryPath;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get config directory
|
|
52
|
+
*/
|
|
53
|
+
function getConfigDir() {
|
|
54
|
+
return path.join(os.homedir(), '.aidesktop');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get config file path
|
|
59
|
+
*/
|
|
60
|
+
function getConfigPath() {
|
|
61
|
+
return path.join(getConfigDir(), 'daemon-config.json');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get log file path
|
|
66
|
+
*/
|
|
67
|
+
function getLogPath() {
|
|
68
|
+
return path.join(getConfigDir(), 'logs', 'daemon.log');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get PID file path
|
|
73
|
+
*/
|
|
74
|
+
function getPidPath() {
|
|
75
|
+
return path.join(getConfigDir(), 'daemon.pid');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
detectPlatform,
|
|
80
|
+
getDaemonBinaryPath,
|
|
81
|
+
getConfigDir,
|
|
82
|
+
getConfigPath,
|
|
83
|
+
getLogPath,
|
|
84
|
+
getPidPath,
|
|
85
|
+
VERSION
|
|
86
|
+
};
|
|
87
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-webui/ai-desk-daemon",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
4
4
|
"description": "AI Desk Daemon - CLI tool for managing the AI Desk daemon service",
|
|
5
5
|
"main": "lib/daemon-manager.js",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
"files": [
|
|
25
25
|
"bin/",
|
|
26
26
|
"lib/",
|
|
27
|
+
"dist/darwin-arm64/",
|
|
28
|
+
"dist/darwin-x64/",
|
|
29
|
+
"dist/linux-arm64/",
|
|
30
|
+
"dist/linux-x64/",
|
|
31
|
+
"dist/win32-x64/",
|
|
27
32
|
"scripts/postinstall.js",
|
|
28
33
|
"README.md"
|
|
29
34
|
],
|
package/scripts/postinstall.js
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Post-install script for @agent-webui/ai-desk-daemon
|
|
5
|
-
* This script does nothing - binaries are managed separately
|
|
6
5
|
*
|
|
7
|
-
*
|
|
6
|
+
* This script runs after npm install to verify the installation
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
|
|
11
|
+
console.log('');
|
|
12
|
+
console.log(chalk.green('✓ @agent-webui/ai-desk-daemon installed successfully'));
|
|
11
13
|
console.log('');
|
|
12
|
-
console.log('
|
|
14
|
+
console.log('Run ' + chalk.cyan('aidesk --help') + ' to get started');
|
|
13
15
|
console.log('');
|
|
16
|
+
|