@agent-webui/ai-desk-daemon 1.0.17
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 +413 -0
- package/bin/cli.js +231 -0
- package/lib/config.js +103 -0
- package/lib/daemon-manager.js +189 -0
- package/lib/platform.js +87 -0
- package/package.json +42 -0
- package/scripts/postinstall.js +157 -0
package/README.md
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
# AI Desk Desktop
|
|
2
|
+
|
|
3
|
+
一个类似 Docker Desktop 的桌面应用,用于管理本地 AI CLI 工具的守护进程。
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="docs/images/screenshot.png" alt="AI Desk Desktop" width="800">
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
## ✨ 特性
|
|
10
|
+
|
|
11
|
+
- 🎯 **自动启动** - Web 访问时自动检测并启动守护进程
|
|
12
|
+
- 🖥️ **图形化管理** - 系统托盘 + 控制面板,便捷管理
|
|
13
|
+
- 🔄 **实时监控** - 查看守护进程状态、日志和统计信息
|
|
14
|
+
- 🚀 **智能启动器** - 多种方式自动唤醒守护进程
|
|
15
|
+
- 🔒 **安全可靠** - 工作目录白名单、认证支持
|
|
16
|
+
- 📦 **跨平台** - macOS、Windows、Linux 统一体验
|
|
17
|
+
- ⚡ **高性能** - 基于 Tauri,体积小(~15MB)、内存占用低
|
|
18
|
+
|
|
19
|
+
## 📦 安装
|
|
20
|
+
|
|
21
|
+
### 方式 1: npm 安装(纯 CLI 工具)
|
|
22
|
+
|
|
23
|
+
**适合场景**:开发者、服务器环境、CI/CD、命令行用户
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 全局安装
|
|
27
|
+
npm install -g @eden_qu/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
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# 下载并安装
|
|
66
|
+
curl -fsSL https://github.com/your-repo/ai-desk-desktop/releases/latest/download/install-macos.sh | bash
|
|
67
|
+
|
|
68
|
+
# 或手动安装
|
|
69
|
+
./scripts/install-macos.sh
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### Linux
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Ubuntu/Debian
|
|
76
|
+
sudo ./scripts/install-linux.sh
|
|
77
|
+
|
|
78
|
+
# 启用自动启动
|
|
79
|
+
systemctl --user enable ai-desk-daemon
|
|
80
|
+
systemctl --user start ai-desk-daemon
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### Windows
|
|
84
|
+
|
|
85
|
+
```powershell
|
|
86
|
+
# 以管理员身份运行 PowerShell
|
|
87
|
+
.\scripts\install-windows.ps1
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 🚀 快速开始
|
|
91
|
+
|
|
92
|
+
### 1. 启动桌面应用
|
|
93
|
+
|
|
94
|
+
**macOS/Linux:**
|
|
95
|
+
```bash
|
|
96
|
+
# 从应用程序菜单启动
|
|
97
|
+
open /Applications/AI\ Desk\ Desktop.app
|
|
98
|
+
|
|
99
|
+
# 或命令行启动
|
|
100
|
+
ai-desk-desktop
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Windows:**
|
|
104
|
+
- 从开始菜单启动 "AI Desk Desktop"
|
|
105
|
+
|
|
106
|
+
### 2. Web 应用集成
|
|
107
|
+
|
|
108
|
+
在你的 Web 应用中集成智能启动器:
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
// App.tsx
|
|
112
|
+
import DaemonInitializer from './components/common/DaemonInitializer';
|
|
113
|
+
|
|
114
|
+
function App() {
|
|
115
|
+
return (
|
|
116
|
+
<DaemonInitializer>
|
|
117
|
+
<YourMainApp />
|
|
118
|
+
</DaemonInitializer>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
启动器会自动:
|
|
124
|
+
- ✅ 检测守护进程是否运行
|
|
125
|
+
- ✅ 如果未运行,通过多种方式尝试启动
|
|
126
|
+
- ✅ 等待守护进程就绪
|
|
127
|
+
- ✅ 失败时显示友好的错误提示
|
|
128
|
+
|
|
129
|
+
### 3. 使用 Daemon API
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { daemonService } from './services/DaemonService';
|
|
133
|
+
|
|
134
|
+
// 执行 CLI 命令
|
|
135
|
+
const result = await daemonService.executeCLI({
|
|
136
|
+
command: 'claude',
|
|
137
|
+
args: ['--verbose'],
|
|
138
|
+
stdin: 'Your prompt here',
|
|
139
|
+
cwd: '/path/to/workspace',
|
|
140
|
+
timeout: 300000,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// 流式执行
|
|
144
|
+
await daemonService.executeStreaming(
|
|
145
|
+
{ command: 'claude', args: [], stdin: 'Hello!' },
|
|
146
|
+
(chunk) => console.log(chunk), // onChunk
|
|
147
|
+
(result) => console.log(result), // onComplete
|
|
148
|
+
(error) => console.error(error) // onError
|
|
149
|
+
);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 🏗️ 架构
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
┌─────────────────────────────────────────────────────┐
|
|
156
|
+
│ AI Desk Desktop App │
|
|
157
|
+
│ (Tauri Application) │
|
|
158
|
+
│ │
|
|
159
|
+
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
|
160
|
+
│ │ System │ │ Control │ │ Settings │ │
|
|
161
|
+
│ │ Tray │ │ Panel │ │ Panel │ │
|
|
162
|
+
│ └──────────┘ └──────────┘ └──────────┘ │
|
|
163
|
+
│ │ │ │
|
|
164
|
+
│ └────────────────┼──────────────────────────┘
|
|
165
|
+
│ ▼
|
|
166
|
+
│ ┌──────────────────┐
|
|
167
|
+
│ │ Daemon Process │
|
|
168
|
+
│ │ HTTP Server :9527│
|
|
169
|
+
│ └──────────────────┘
|
|
170
|
+
│ │
|
|
171
|
+
│ ┌────────────────┼────────────────┐
|
|
172
|
+
│ ▼ ▼ ▼
|
|
173
|
+
│ ┌────────┐ ┌────────┐ ┌────────┐
|
|
174
|
+
│ │ Claude │ │ Gemini │ │ Cursor │
|
|
175
|
+
│ └────────┘ └────────┘ └────────┘
|
|
176
|
+
│
|
|
177
|
+
├─────────────────────────────────────────────────────┤
|
|
178
|
+
│ Web Application │
|
|
179
|
+
│ (React/TypeScript) │
|
|
180
|
+
│ │
|
|
181
|
+
│ ┌────────────────────────────────────┐ │
|
|
182
|
+
│ │ Smart Daemon Starter │ │
|
|
183
|
+
│ │ - Auto-detect & Start │ │
|
|
184
|
+
│ │ - URL Scheme / Extension / HTTP │ │
|
|
185
|
+
│ └────────────────────────────────────┘ │
|
|
186
|
+
└─────────────────────────────────────────────────────┘
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## 📚 组件说明
|
|
190
|
+
|
|
191
|
+
### 1. Daemon(守护进程)
|
|
192
|
+
|
|
193
|
+
基于 Go 开发的 HTTP/WebSocket 服务器:
|
|
194
|
+
|
|
195
|
+
**功能**:
|
|
196
|
+
- CLI 检测(claude、gemini、cursor)
|
|
197
|
+
- CLI 执行(同步和流式)
|
|
198
|
+
- 并发控制和超时管理
|
|
199
|
+
- 安全策略(工作目录白名单)
|
|
200
|
+
- 日志记录
|
|
201
|
+
|
|
202
|
+
**API 端点**:
|
|
203
|
+
- `GET /health` - 健康检查
|
|
204
|
+
- `GET /api/v1/clis` - 检测 CLI
|
|
205
|
+
- `POST /api/v1/execute` - 执行 CLI(REST)
|
|
206
|
+
- `WS /api/v1/execute/stream` - 执行 CLI(WebSocket)
|
|
207
|
+
- `POST /api/v1/execute/{id}/cancel` - 取消执行
|
|
208
|
+
|
|
209
|
+
### 2. Tauri Desktop(桌面应用)
|
|
210
|
+
|
|
211
|
+
基于 Tauri(Rust + React)的桌面应用:
|
|
212
|
+
|
|
213
|
+
**功能**:
|
|
214
|
+
- 系统托盘图标和菜单
|
|
215
|
+
- 守护进程生命周期管理
|
|
216
|
+
- 实时状态监控
|
|
217
|
+
- 日志查看
|
|
218
|
+
- 设置管理
|
|
219
|
+
- URL Scheme 注册
|
|
220
|
+
|
|
221
|
+
### 3. Web Integration(Web 集成)
|
|
222
|
+
|
|
223
|
+
**Smart Daemon Starter(智能启动器)**:
|
|
224
|
+
- 自动检测守护进程
|
|
225
|
+
- 多种启动方式(URL Scheme、Extension、HTTP)
|
|
226
|
+
- 自动重试机制
|
|
227
|
+
- 友好的错误提示
|
|
228
|
+
|
|
229
|
+
**Daemon Service(守护进程服务)**:
|
|
230
|
+
- HTTP/WebSocket 通信
|
|
231
|
+
- CLI 执行和流式输出
|
|
232
|
+
- 错误处理
|
|
233
|
+
|
|
234
|
+
## ⚙️ 配置
|
|
235
|
+
|
|
236
|
+
### Daemon 配置
|
|
237
|
+
|
|
238
|
+
配置文件位置:`~/.aidesktop/daemon-config.json`
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"port": 9527,
|
|
243
|
+
"max_concurrent_executions": 10,
|
|
244
|
+
"execution_timeout": 300,
|
|
245
|
+
"allowed_origins": [
|
|
246
|
+
"*"
|
|
247
|
+
],
|
|
248
|
+
"allowed_working_dirs": [
|
|
249
|
+
"/path/to/your/home"
|
|
250
|
+
],
|
|
251
|
+
"require_authentication": false,
|
|
252
|
+
"auth_token": "",
|
|
253
|
+
"log_level": "INFO"
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
说明:`9527` 是默认端口,实际运行时以 `~/.aidesktop/daemon-config.json` 中的 `port` 为准。
|
|
258
|
+
|
|
259
|
+
### 应用配置
|
|
260
|
+
|
|
261
|
+
配置文件位置:`~/.aidesktop/app-config.json`
|
|
262
|
+
|
|
263
|
+
```json
|
|
264
|
+
{
|
|
265
|
+
"auto_start": true,
|
|
266
|
+
"minimize_to_tray": true,
|
|
267
|
+
"daemon_port": 9527
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
如果你修改了 daemon 配置端口,请保持这里的 `daemon_port` 与之同步。
|
|
272
|
+
|
|
273
|
+
## 🔧 开发
|
|
274
|
+
|
|
275
|
+
### 前置要求
|
|
276
|
+
|
|
277
|
+
- **Node.js** 18+
|
|
278
|
+
- **Rust** 1.70+
|
|
279
|
+
- **Go** 1.21+ (用于守护进程)
|
|
280
|
+
- **Tauri CLI** 1.5+
|
|
281
|
+
|
|
282
|
+
### 构建
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
# 克隆仓库
|
|
286
|
+
git clone https://github.com/your-repo/ai-desk-desktop.git
|
|
287
|
+
cd ai-desk-desktop
|
|
288
|
+
|
|
289
|
+
# 安装依赖
|
|
290
|
+
npm install
|
|
291
|
+
|
|
292
|
+
# 构建所有组件
|
|
293
|
+
./scripts/build.sh
|
|
294
|
+
|
|
295
|
+
# 或分别构建:
|
|
296
|
+
|
|
297
|
+
# 1. 构建守护进程
|
|
298
|
+
cd daemon && go build -o ai-desk-daemon
|
|
299
|
+
|
|
300
|
+
# 2. 构建前端
|
|
301
|
+
npm run build
|
|
302
|
+
|
|
303
|
+
# 3. 构建 Tauri 应用
|
|
304
|
+
npm run tauri build
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### 开发模式
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
# 终端 1: 启动守护进程
|
|
311
|
+
cd daemon
|
|
312
|
+
go run .
|
|
313
|
+
|
|
314
|
+
# 终端 2: 启动 Tauri 应用
|
|
315
|
+
npm run tauri:dev
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## 📖 使用示例
|
|
319
|
+
|
|
320
|
+
### 在 Web 应用中显示守护进程状态
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
import { DaemonStatusIndicator } from './components/common/DaemonStatusBanner';
|
|
324
|
+
|
|
325
|
+
function Layout() {
|
|
326
|
+
return (
|
|
327
|
+
<div>
|
|
328
|
+
<Header />
|
|
329
|
+
<Content />
|
|
330
|
+
<DaemonStatusIndicator /> {/* 显示在右下角 */}
|
|
331
|
+
</div>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### 条件渲染(守护进程可用时)
|
|
337
|
+
|
|
338
|
+
```tsx
|
|
339
|
+
import { useDaemon } from './hooks/useDaemon';
|
|
340
|
+
|
|
341
|
+
function AgentPanel() {
|
|
342
|
+
const { isAvailable, isChecking } = useDaemon();
|
|
343
|
+
|
|
344
|
+
if (isChecking) {
|
|
345
|
+
return <div>Checking daemon...</div>;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!isAvailable) {
|
|
349
|
+
return (
|
|
350
|
+
<div>
|
|
351
|
+
Daemon not available.
|
|
352
|
+
<button onClick={() => window.location.href = 'aidesktop://start'}>
|
|
353
|
+
Launch Now
|
|
354
|
+
</button>
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return <YourAgentInterface />;
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## 🐛 故障排查
|
|
364
|
+
|
|
365
|
+
### macOS Gatekeeper 警告
|
|
366
|
+
|
|
367
|
+
**问题**:macOS 阻止运行未签名的应用
|
|
368
|
+
|
|
369
|
+
**解决方案**:
|
|
370
|
+
```bash
|
|
371
|
+
# 移除隔离属性
|
|
372
|
+
xattr -cr "/Applications/AI Desk Desktop.app"
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### 守护进程未启动
|
|
376
|
+
|
|
377
|
+
**检查日志**:
|
|
378
|
+
```bash
|
|
379
|
+
tail -f ~/.aidesktop/logs/daemon.log
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**手动启动测试**:
|
|
383
|
+
```bash
|
|
384
|
+
/usr/local/bin/ai-desk-daemon --config ~/.aidesktop/daemon-config.json
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Web 应用无法连接
|
|
388
|
+
|
|
389
|
+
1. 检查守护进程是否运行:
|
|
390
|
+
```bash
|
|
391
|
+
curl http://localhost:<port>/health
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
2. 检查防火墙设置
|
|
395
|
+
3. 确认配置文件中的端口未被占用
|
|
396
|
+
|
|
397
|
+
## 📄 许可证
|
|
398
|
+
|
|
399
|
+
MIT License
|
|
400
|
+
|
|
401
|
+
## 🤝 贡献
|
|
402
|
+
|
|
403
|
+
欢迎提交 Issues 和 Pull Requests!
|
|
404
|
+
|
|
405
|
+
## 📧 支持
|
|
406
|
+
|
|
407
|
+
- GitHub Issues: https://github.com/your-repo/ai-desk-desktop/issues
|
|
408
|
+
- 文档: https://docs.aidesktop.com
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
**Version**: 1.0.0
|
|
413
|
+
**Maintained by**: AI Desk Team
|
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
|
+
|
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,189 @@
|
|
|
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 child = spawn(binaryPath, ['--port', portNumber.toString(), '--config', configPath], {
|
|
116
|
+
detached: true,
|
|
117
|
+
stdio: ['ignore', 'ignore', 'ignore'] // Daemon writes to its own log file
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
child.unref();
|
|
121
|
+
|
|
122
|
+
// Save PID
|
|
123
|
+
fs.writeFileSync(getPidPath(), child.pid.toString());
|
|
124
|
+
|
|
125
|
+
console.log(`Daemon started (PID: ${child.pid})`);
|
|
126
|
+
console.log(`Port: ${portNumber}`);
|
|
127
|
+
console.log(`Logs: ${logPath}`);
|
|
128
|
+
|
|
129
|
+
return child.pid;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Stop daemon
|
|
134
|
+
*/
|
|
135
|
+
function stop() {
|
|
136
|
+
const pidPath = getPidPath();
|
|
137
|
+
|
|
138
|
+
if (!fs.existsSync(pidPath)) {
|
|
139
|
+
throw new Error('Daemon is not running');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim());
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
process.kill(pid, 'SIGTERM');
|
|
146
|
+
fs.unlinkSync(pidPath);
|
|
147
|
+
console.log('Daemon stopped');
|
|
148
|
+
} catch (error) {
|
|
149
|
+
fs.unlinkSync(pidPath);
|
|
150
|
+
throw new Error(`Failed to stop daemon: ${error.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Restart daemon
|
|
156
|
+
*/
|
|
157
|
+
function restart() {
|
|
158
|
+
if (isRunning()) {
|
|
159
|
+
stop();
|
|
160
|
+
// Wait a bit for graceful shutdown
|
|
161
|
+
execSync('sleep 1');
|
|
162
|
+
}
|
|
163
|
+
start();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get daemon status
|
|
168
|
+
*/
|
|
169
|
+
async function status() {
|
|
170
|
+
const running = isRunning();
|
|
171
|
+
const health = running ? await getHealth() : null;
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
running,
|
|
175
|
+
health,
|
|
176
|
+
port: getPort(),
|
|
177
|
+
logPath: getLogPath()
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
isRunning,
|
|
183
|
+
getHealth,
|
|
184
|
+
start,
|
|
185
|
+
stop,
|
|
186
|
+
restart,
|
|
187
|
+
status
|
|
188
|
+
};
|
|
189
|
+
|
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.17';
|
|
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
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-webui/ai-desk-daemon",
|
|
3
|
+
"version": "1.0.17",
|
|
4
|
+
"description": "AI Desk Daemon - CLI tool for managing the AI Desk daemon service",
|
|
5
|
+
"main": "lib/daemon-manager.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"aidesk": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"postinstall": "node scripts/postinstall.js",
|
|
11
|
+
"test": "node bin/cli.js --help"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"ai-desk",
|
|
15
|
+
"daemon",
|
|
16
|
+
"cli",
|
|
17
|
+
"agent-webui"
|
|
18
|
+
],
|
|
19
|
+
"author": "Agent WebUI",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=14.0.0"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"bin/",
|
|
26
|
+
"lib/",
|
|
27
|
+
"scripts/postinstall.js",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"commander": "^11.0.0",
|
|
32
|
+
"chalk": "^4.1.2"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/agent-webui/ai-desk-daemon.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/agent-webui",
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Post-install script to download platform-specific daemon binary from GitHub Releases
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
const GITHUB_REPO = 'agent-webui/ai-desk-daemon';
|
|
13
|
+
const PACKAGE_VERSION = require('../package.json').version;
|
|
14
|
+
|
|
15
|
+
// Platform mapping
|
|
16
|
+
const PLATFORM_MAP = {
|
|
17
|
+
'darwin-x64': 'darwin-x64',
|
|
18
|
+
'darwin-arm64': 'darwin-arm64',
|
|
19
|
+
'linux-x64': 'linux-x64',
|
|
20
|
+
'linux-arm64': 'linux-arm64',
|
|
21
|
+
'win32-x64': 'win32-x64'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function getPlatform() {
|
|
25
|
+
const platform = process.platform;
|
|
26
|
+
const arch = process.arch;
|
|
27
|
+
const key = `${platform}-${arch}`;
|
|
28
|
+
|
|
29
|
+
if (!PLATFORM_MAP[key]) {
|
|
30
|
+
console.error(`Unsupported platform: ${platform}-${arch}`);
|
|
31
|
+
console.error('Supported platforms:', Object.keys(PLATFORM_MAP).join(', '));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return PLATFORM_MAP[key];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getBinaryName(platform) {
|
|
39
|
+
return platform.startsWith('win32') ? 'ai-desk-daemon.exe' : 'ai-desk-daemon';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function downloadFile(url, dest) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
console.log(`Downloading from: ${url}`);
|
|
45
|
+
|
|
46
|
+
const file = fs.createWriteStream(dest);
|
|
47
|
+
|
|
48
|
+
https.get(url, (response) => {
|
|
49
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
50
|
+
// Follow redirect
|
|
51
|
+
file.close();
|
|
52
|
+
fs.unlinkSync(dest);
|
|
53
|
+
return downloadFile(response.headers.location, dest).then(resolve).catch(reject);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (response.statusCode !== 200) {
|
|
57
|
+
file.close();
|
|
58
|
+
fs.unlinkSync(dest);
|
|
59
|
+
return reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const totalSize = parseInt(response.headers['content-length'], 10);
|
|
63
|
+
let downloadedSize = 0;
|
|
64
|
+
let lastPercent = 0;
|
|
65
|
+
|
|
66
|
+
response.on('data', (chunk) => {
|
|
67
|
+
downloadedSize += chunk.length;
|
|
68
|
+
const percent = Math.floor((downloadedSize / totalSize) * 100);
|
|
69
|
+
if (percent > lastPercent && percent % 10 === 0) {
|
|
70
|
+
console.log(`Progress: ${percent}%`);
|
|
71
|
+
lastPercent = percent;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
response.pipe(file);
|
|
76
|
+
|
|
77
|
+
file.on('finish', () => {
|
|
78
|
+
file.close();
|
|
79
|
+
console.log('Download complete!');
|
|
80
|
+
resolve();
|
|
81
|
+
});
|
|
82
|
+
}).on('error', (err) => {
|
|
83
|
+
file.close();
|
|
84
|
+
fs.unlinkSync(dest);
|
|
85
|
+
reject(err);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function main() {
|
|
91
|
+
console.log('='.repeat(50));
|
|
92
|
+
console.log('AI Desk Daemon - Post Install');
|
|
93
|
+
console.log('='.repeat(50));
|
|
94
|
+
|
|
95
|
+
const platform = getPlatform();
|
|
96
|
+
const binaryName = getBinaryName(platform);
|
|
97
|
+
|
|
98
|
+
console.log(`Platform: ${platform}`);
|
|
99
|
+
console.log(`Version: ${PACKAGE_VERSION}`);
|
|
100
|
+
console.log(`Binary: ${binaryName}`);
|
|
101
|
+
console.log('');
|
|
102
|
+
|
|
103
|
+
// Create dist directory
|
|
104
|
+
const distDir = path.join(__dirname, '..', 'dist', platform);
|
|
105
|
+
if (!fs.existsSync(distDir)) {
|
|
106
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const binaryPath = path.join(distDir, binaryName);
|
|
110
|
+
|
|
111
|
+
// Check if binary already exists
|
|
112
|
+
if (fs.existsSync(binaryPath)) {
|
|
113
|
+
console.log('Binary already exists, skipping download.');
|
|
114
|
+
console.log(`Location: ${binaryPath}`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Download from GitHub Releases
|
|
119
|
+
// Format: ai-desk-daemon-darwin-arm64 or ai-desk-daemon-win32-x64.exe
|
|
120
|
+
const assetName = platform.startsWith('win32')
|
|
121
|
+
? `ai-desk-daemon-${platform}.exe`
|
|
122
|
+
: `ai-desk-daemon-${platform}`;
|
|
123
|
+
const downloadUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${PACKAGE_VERSION}/${assetName}`;
|
|
124
|
+
|
|
125
|
+
console.log('Downloading daemon binary...');
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
await downloadFile(downloadUrl, binaryPath);
|
|
129
|
+
|
|
130
|
+
// Set executable permission (Unix-like systems)
|
|
131
|
+
if (process.platform !== 'win32') {
|
|
132
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
133
|
+
console.log('Set executable permission');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log('');
|
|
137
|
+
console.log('✓ Installation complete!');
|
|
138
|
+
console.log(`Binary installed at: ${binaryPath}`);
|
|
139
|
+
console.log('');
|
|
140
|
+
console.log('Run "aidesk --help" to get started.');
|
|
141
|
+
console.log('='.repeat(50));
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error('');
|
|
144
|
+
console.error('✗ Failed to download daemon binary');
|
|
145
|
+
console.error(`Error: ${error.message}`);
|
|
146
|
+
console.error('');
|
|
147
|
+
console.error('Please try one of the following:');
|
|
148
|
+
console.error(`1. Download manually from: https://github.com/${GITHUB_REPO}/releases/tag/v${PACKAGE_VERSION}`);
|
|
149
|
+
console.error(`2. Place the binary at: ${binaryPath}`);
|
|
150
|
+
console.error('3. Report this issue at: https://github.com/${GITHUB_REPO}/issues');
|
|
151
|
+
console.error('='.repeat(50));
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
main();
|
|
157
|
+
|