agent-window 1.0.0 → 1.0.2
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 +21 -8
- package/README.zh-CN.md +151 -0
- package/bin/cli.js +45 -0
- package/docs/WEB_UI_GUIDE.md +249 -0
- package/package.json +15 -3
- package/scripts/test-platform.js +109 -0
- package/src/api/routes/index.js +25 -0
- package/src/api/routes/instances.js +252 -0
- package/src/api/routes/operations.js +118 -0
- package/src/api/routes/system.js +42 -0
- package/src/api/server.js +147 -0
- package/src/api/websocket/index.js +16 -0
- package/src/api/websocket/logs.js +127 -0
- package/src/cli/commands/add.js +80 -0
- package/src/cli/commands/config.js +192 -0
- package/src/cli/commands/index.js +89 -0
- package/src/cli/commands/info.js +94 -0
- package/src/cli/commands/list.js +72 -0
- package/src/cli/commands/logs.js +67 -0
- package/src/cli/commands/remove.js +97 -0
- package/src/cli/commands/restart.js +67 -0
- package/src/cli/commands/start.js +101 -0
- package/src/cli/commands/status.js +95 -0
- package/src/cli/commands/stop.js +53 -0
- package/src/cli/commands/ui.js +51 -0
- package/src/cli/index.js +110 -0
- package/src/core/config.js +5 -10
- package/src/core/instance/backup-manager.js +172 -0
- package/src/core/instance/config-manager.js +279 -0
- package/src/core/instance/index.js +62 -0
- package/src/core/instance/manager.js +220 -0
- package/src/core/instance/pm2-bridge.js +205 -0
- package/src/core/instance/validator.js +161 -0
- package/src/core/platform/detector.js +142 -0
- package/src/core/platform/docker-bridge.js +372 -0
- package/src/core/platform/index.js +27 -0
- package/src/core/platform/paths.js +112 -0
- package/src/core/platform/pm2-bridge.js +314 -0
- package/web/dist/assets/Dashboard-C1smB9Nj.js +1 -0
- package/web/dist/assets/Dashboard-ezbZMSpZ.css +1 -0
- package/web/dist/assets/InstanceDetail-CRPMV7rg.css +1 -0
- package/web/dist/assets/InstanceDetail-C_Ddtrog.js +3 -0
- package/web/dist/assets/Instances-CvnH8iDv.css +1 -0
- package/web/dist/assets/Instances-_u2__M83.js +1 -0
- package/web/dist/assets/Settings-CAu3R9RW.css +1 -0
- package/web/dist/assets/Settings-CIa9MX7m.js +1 -0
- package/web/dist/assets/_plugin-vue_export-helper-DlAUqK2U.js +1 -0
- package/web/dist/assets/element-plus-Jr6qTeY5.js +37 -0
- package/web/dist/assets/main-CalRvcyG.css +1 -0
- package/web/dist/assets/main-D3cdXAiV.js +7 -0
- package/web/dist/assets/vue-vendor-CGSlMM3Y.js +29 -0
- package/web/dist/index.html +16 -0
- package/SECURITY.md +0 -31
- package/docs/legacy/DEVELOPMENT.md +0 -174
- package/docs/legacy/HANDOVER.md +0 -149
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instance Routes
|
|
3
|
+
*
|
|
4
|
+
* REST API endpoints for instance management.
|
|
5
|
+
* - GET /api/instances - List all instances
|
|
6
|
+
* - GET /api/instances/:name - Get instance details
|
|
7
|
+
* - POST /api/instances - Add new instance
|
|
8
|
+
* - DELETE /api/instances/:name - Remove instance
|
|
9
|
+
* - GET /api/instances/:name/status - Get instance status
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
listInstances,
|
|
14
|
+
getInstance,
|
|
15
|
+
addInstance,
|
|
16
|
+
removeInstance,
|
|
17
|
+
updateInstance
|
|
18
|
+
} from '../../core/instance/manager.js';
|
|
19
|
+
import {
|
|
20
|
+
getStatus,
|
|
21
|
+
getLogs
|
|
22
|
+
} from '../../core/instance/pm2-bridge.js';
|
|
23
|
+
import { existsSync } from 'fs';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Register instance routes
|
|
27
|
+
*/
|
|
28
|
+
export async function registerInstanceRoutes(fastify) {
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* GET /api/instances
|
|
32
|
+
* List all registered instances
|
|
33
|
+
*/
|
|
34
|
+
fastify.get('/api/instances', {
|
|
35
|
+
schema: {
|
|
36
|
+
description: 'List all instances',
|
|
37
|
+
tags: ['instances'],
|
|
38
|
+
response: {
|
|
39
|
+
200: {
|
|
40
|
+
type: 'array',
|
|
41
|
+
items: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
name: { type: 'string' },
|
|
45
|
+
displayName: { type: 'string' },
|
|
46
|
+
projectPath: { type: 'string' },
|
|
47
|
+
pluginPath: { type: 'string' },
|
|
48
|
+
enabled: { type: 'boolean' },
|
|
49
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
50
|
+
addedAt: { type: 'string' }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}, async (request, reply) => {
|
|
57
|
+
try {
|
|
58
|
+
const instances = await listInstances();
|
|
59
|
+
return instances;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
reply.code(500).send({
|
|
62
|
+
error: 'Failed to list instances',
|
|
63
|
+
message: error.message
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* GET /api/instances/:name
|
|
70
|
+
* Get specific instance details
|
|
71
|
+
*/
|
|
72
|
+
fastify.get('/api/instances/:name', {
|
|
73
|
+
schema: {
|
|
74
|
+
description: 'Get instance details',
|
|
75
|
+
params: {
|
|
76
|
+
name: { type: 'string' }
|
|
77
|
+
},
|
|
78
|
+
response: {
|
|
79
|
+
200: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
name: { type: 'string' },
|
|
83
|
+
displayName: { type: 'string' },
|
|
84
|
+
projectPath: { type: 'string' },
|
|
85
|
+
configPath: { type: 'string' },
|
|
86
|
+
enabled: { type: 'boolean' }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}, async (request, reply) => {
|
|
92
|
+
try {
|
|
93
|
+
const { name } = request.params;
|
|
94
|
+
const instance = await getInstance(name);
|
|
95
|
+
|
|
96
|
+
if (!instance) {
|
|
97
|
+
return reply.code(404).send({
|
|
98
|
+
error: 'Instance not found',
|
|
99
|
+
name
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return instance;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
reply.code(500).send({
|
|
106
|
+
error: 'Failed to get instance',
|
|
107
|
+
message: error.message
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* POST /api/instances
|
|
114
|
+
* Add a new instance
|
|
115
|
+
*/
|
|
116
|
+
fastify.post('/api/instances', {
|
|
117
|
+
schema: {
|
|
118
|
+
description: 'Add new instance',
|
|
119
|
+
body: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
required: ['name', 'projectPath'],
|
|
122
|
+
properties: {
|
|
123
|
+
name: { type: 'string' },
|
|
124
|
+
projectPath: { type: 'string' },
|
|
125
|
+
displayName: { type: 'string' },
|
|
126
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
127
|
+
configPath: { type: 'string' }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}, async (request, reply) => {
|
|
132
|
+
try {
|
|
133
|
+
const { name, projectPath, displayName, tags, configPath } = request.body;
|
|
134
|
+
|
|
135
|
+
// Validate project path exists
|
|
136
|
+
if (!existsSync(projectPath)) {
|
|
137
|
+
return reply.code(400).send({
|
|
138
|
+
error: 'Project path does not exist',
|
|
139
|
+
path: projectPath
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const result = await addInstance(name, projectPath, {
|
|
144
|
+
displayName,
|
|
145
|
+
tags,
|
|
146
|
+
configPath
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (!result.success) {
|
|
150
|
+
return reply.code(400).send({
|
|
151
|
+
error: result.error,
|
|
152
|
+
validation: result.validation
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
reply.code(201).send(result.instance);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
reply.code(500).send({
|
|
159
|
+
error: 'Failed to add instance',
|
|
160
|
+
message: error.message
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* DELETE /api/instances/:name
|
|
167
|
+
* Remove an instance
|
|
168
|
+
*/
|
|
169
|
+
fastify.delete('/api/instances/:name', {
|
|
170
|
+
schema: {
|
|
171
|
+
description: 'Remove instance',
|
|
172
|
+
params: {
|
|
173
|
+
name: { type: 'string' }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}, async (request, reply) => {
|
|
177
|
+
try {
|
|
178
|
+
const { name } = request.params;
|
|
179
|
+
const result = await removeInstance(name);
|
|
180
|
+
|
|
181
|
+
if (!result.success) {
|
|
182
|
+
return reply.code(404).send({
|
|
183
|
+
error: result.error,
|
|
184
|
+
name
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { message: 'Instance removed', name };
|
|
189
|
+
} catch (error) {
|
|
190
|
+
reply.code(500).send({
|
|
191
|
+
error: 'Failed to remove instance',
|
|
192
|
+
message: error.message
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* GET /api/instances/:name/status
|
|
199
|
+
* Get instance runtime status
|
|
200
|
+
*/
|
|
201
|
+
fastify.get('/api/instances/:name/status', {
|
|
202
|
+
schema: {
|
|
203
|
+
description: 'Get instance status',
|
|
204
|
+
params: {
|
|
205
|
+
name: { type: 'string' }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}, async (request, reply) => {
|
|
209
|
+
try {
|
|
210
|
+
const { name } = request.params;
|
|
211
|
+
const status = await getStatus(name);
|
|
212
|
+
return status;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
reply.code(500).send({
|
|
215
|
+
error: 'Failed to get status',
|
|
216
|
+
message: error.message
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* GET /api/instances/:name/logs
|
|
223
|
+
* Get instance logs (non-streaming)
|
|
224
|
+
*/
|
|
225
|
+
fastify.get('/api/instances/:name/logs', {
|
|
226
|
+
schema: {
|
|
227
|
+
description: 'Get instance logs',
|
|
228
|
+
params: {
|
|
229
|
+
name: { type: 'string' }
|
|
230
|
+
},
|
|
231
|
+
querystring: {
|
|
232
|
+
lines: { type: 'number', default: 100 },
|
|
233
|
+
logType: { type: 'string', enum: ['all', 'out', 'err'], default: 'all' }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}, async (request, reply) => {
|
|
237
|
+
try {
|
|
238
|
+
const { name } = request.params;
|
|
239
|
+
const { lines = 100, logType = 'all' } = request.query;
|
|
240
|
+
|
|
241
|
+
const logs = await getLogs(name, { lines, type: logType });
|
|
242
|
+
return { logs };
|
|
243
|
+
} catch (error) {
|
|
244
|
+
reply.code(500).send({
|
|
245
|
+
error: 'Failed to get logs',
|
|
246
|
+
message: error.message
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
fastify.log.info('Instance routes registered');
|
|
252
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operation Routes
|
|
3
|
+
*
|
|
4
|
+
* REST API endpoints for instance operations.
|
|
5
|
+
* - POST /api/instances/:name/start - Start instance
|
|
6
|
+
* - POST /api/instances/:name/stop - Stop instance
|
|
7
|
+
* - POST /api/instances/:name/restart - Restart instance
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
startProcess,
|
|
12
|
+
stopProcess,
|
|
13
|
+
restartProcess
|
|
14
|
+
} from '../../core/instance/pm2-bridge.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register operation routes
|
|
18
|
+
*/
|
|
19
|
+
export async function registerOperationRoutes(fastify) {
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* POST /api/instances/:name/start
|
|
23
|
+
* Start an instance
|
|
24
|
+
*/
|
|
25
|
+
fastify.post('/api/instances/:name/start', {
|
|
26
|
+
schema: {
|
|
27
|
+
description: 'Start instance',
|
|
28
|
+
params: {
|
|
29
|
+
name: { type: 'string' }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}, async (request, reply) => {
|
|
33
|
+
try {
|
|
34
|
+
const { name } = request.params;
|
|
35
|
+
const result = await startProcess(name);
|
|
36
|
+
|
|
37
|
+
if (!result.success) {
|
|
38
|
+
return reply.code(400).send({
|
|
39
|
+
error: result.error,
|
|
40
|
+
name
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { message: 'Instance started', name };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
reply.code(500).send({
|
|
47
|
+
error: 'Failed to start instance',
|
|
48
|
+
message: error.message
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* POST /api/instances/:name/stop
|
|
55
|
+
* Stop an instance
|
|
56
|
+
*/
|
|
57
|
+
fastify.post('/api/instances/:name/stop', {
|
|
58
|
+
schema: {
|
|
59
|
+
description: 'Stop instance',
|
|
60
|
+
params: {
|
|
61
|
+
name: { type: 'string' }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}, async (request, reply) => {
|
|
65
|
+
try {
|
|
66
|
+
const { name } = request.params;
|
|
67
|
+
const result = await stopProcess(name);
|
|
68
|
+
|
|
69
|
+
if (!result.success) {
|
|
70
|
+
return reply.code(400).send({
|
|
71
|
+
error: result.error,
|
|
72
|
+
name
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { message: 'Instance stopped', name };
|
|
77
|
+
} catch (error) {
|
|
78
|
+
reply.code(500).send({
|
|
79
|
+
error: 'Failed to stop instance',
|
|
80
|
+
message: error.message
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* POST /api/instances/:name/restart
|
|
87
|
+
* Restart an instance
|
|
88
|
+
*/
|
|
89
|
+
fastify.post('/api/instances/:name/restart', {
|
|
90
|
+
schema: {
|
|
91
|
+
description: 'Restart instance',
|
|
92
|
+
params: {
|
|
93
|
+
name: { type: 'string' }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}, async (request, reply) => {
|
|
97
|
+
try {
|
|
98
|
+
const { name } = request.params;
|
|
99
|
+
const result = await restartProcess(name);
|
|
100
|
+
|
|
101
|
+
if (!result.success) {
|
|
102
|
+
return reply.code(400).send({
|
|
103
|
+
error: result.error,
|
|
104
|
+
name
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { message: 'Instance restarted', name };
|
|
109
|
+
} catch (error) {
|
|
110
|
+
reply.code(500).send({
|
|
111
|
+
error: 'Failed to restart instance',
|
|
112
|
+
message: error.message
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
fastify.log.info('Operation routes registered');
|
|
118
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System Routes
|
|
3
|
+
*
|
|
4
|
+
* REST API endpoints for system information.
|
|
5
|
+
* - GET /api/system/info - System information
|
|
6
|
+
* - GET /api/system/stats - System statistics
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { platform } from 'os';
|
|
10
|
+
import { getPackageVersion } from '../server.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Register system routes
|
|
14
|
+
*/
|
|
15
|
+
export async function registerSystemRoutes(fastify) {
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* GET /api/system/info
|
|
19
|
+
* Get system information
|
|
20
|
+
*/
|
|
21
|
+
fastify.get('/api/system/info', async () => ({
|
|
22
|
+
platform: platform(),
|
|
23
|
+
nodeVersion: process.version,
|
|
24
|
+
agentWindow: {
|
|
25
|
+
version: getPackageVersion(),
|
|
26
|
+
home: process.env.AGENT_WINDOW_HOME || `${process.env.HOME}/.agent-window`
|
|
27
|
+
},
|
|
28
|
+
uptime: process.uptime()
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* GET /api/system/stats
|
|
33
|
+
* Get system statistics
|
|
34
|
+
*/
|
|
35
|
+
fastify.get('/api/system/stats', async () => ({
|
|
36
|
+
memory: process.memoryUsage(),
|
|
37
|
+
uptime: process.uptime(),
|
|
38
|
+
cpu: process.cpuUsage()
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
fastify.log.info('System routes registered');
|
|
42
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Server
|
|
3
|
+
*
|
|
4
|
+
* Fastify-based REST API and WebSocket server for AgentWindow Web UI.
|
|
5
|
+
* Local-only access (localhost) with no authentication required.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import Fastify from 'fastify';
|
|
9
|
+
import fastifyStatic from '@fastify/static';
|
|
10
|
+
import fastifyWebSocket from '@fastify/websocket';
|
|
11
|
+
import { join, dirname } from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { existsSync } from 'fs';
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
|
|
17
|
+
// Routes
|
|
18
|
+
import { registerRoutes } from './routes/index.js';
|
|
19
|
+
import { registerWebSocket } from './websocket/index.js';
|
|
20
|
+
|
|
21
|
+
// Server configuration
|
|
22
|
+
const DEFAULT_PORT = 3721;
|
|
23
|
+
const HOST = '127.0.0.1'; // Localhost only
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create and configure Fastify server
|
|
27
|
+
*/
|
|
28
|
+
export function createServer(options = {}) {
|
|
29
|
+
const { logger = true, port = DEFAULT_PORT } = options;
|
|
30
|
+
|
|
31
|
+
const fastify = Fastify({
|
|
32
|
+
logger: logger ? {
|
|
33
|
+
transport: {
|
|
34
|
+
target: 'pino-pretty',
|
|
35
|
+
options: { colorize: true }
|
|
36
|
+
}
|
|
37
|
+
} : false,
|
|
38
|
+
ignoreTrailingSlash: true
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Register WebSocket support
|
|
42
|
+
fastify.register(fastifyWebSocket);
|
|
43
|
+
|
|
44
|
+
// CORS for local development
|
|
45
|
+
fastify.register(async function (fastify) {
|
|
46
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
47
|
+
// Allow only local access
|
|
48
|
+
const ip = request.ip;
|
|
49
|
+
const isLocal = ip === '127.0.0.1' || ip === '::1' || ip === 'localhost';
|
|
50
|
+
|
|
51
|
+
if (!isLocal) {
|
|
52
|
+
reply.code(403).send({ error: 'Access denied - local only' });
|
|
53
|
+
return reply;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Add CORS headers for local development
|
|
57
|
+
reply.header('Access-Control-Allow-Origin', '*');
|
|
58
|
+
reply.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
|
|
59
|
+
reply.header('Access-Control-Allow-Headers', 'Content-Type,Authorization');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Health check
|
|
64
|
+
fastify.get('/api/health', async () => ({
|
|
65
|
+
status: 'ok',
|
|
66
|
+
timestamp: new Date().toISOString(),
|
|
67
|
+
uptime: process.uptime(),
|
|
68
|
+
version: getPackageVersion()
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
// Register API routes
|
|
72
|
+
registerRoutes(fastify);
|
|
73
|
+
|
|
74
|
+
// Register WebSocket routes
|
|
75
|
+
registerWebSocket(fastify);
|
|
76
|
+
|
|
77
|
+
// Serve static files (frontend build)
|
|
78
|
+
const distPath = join(process.cwd(), 'web', 'dist');
|
|
79
|
+
if (existsSync(distPath)) {
|
|
80
|
+
fastify.register(fastifyStatic, {
|
|
81
|
+
root: distPath,
|
|
82
|
+
prefix: '/'
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// SPA fallback - serve index.html for non-API routes
|
|
86
|
+
fastify.setNotFoundHandler(async (request, reply) => {
|
|
87
|
+
if (request.url.startsWith('/api/') || request.url.startsWith('/ws/')) {
|
|
88
|
+
reply.code(404).send({ error: 'Not found' });
|
|
89
|
+
} else {
|
|
90
|
+
reply.sendFile('index.html');
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Graceful shutdown
|
|
96
|
+
const gracefulShutdown = async (signal) => {
|
|
97
|
+
fastify.log.info(`Received ${signal}, shutting down gracefully...`);
|
|
98
|
+
await fastify.close();
|
|
99
|
+
process.exit(0);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
103
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
104
|
+
|
|
105
|
+
return { fastify, port, host: HOST };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get package version
|
|
110
|
+
*/
|
|
111
|
+
export function getPackageVersion() {
|
|
112
|
+
try {
|
|
113
|
+
const pkgPath = join(process.cwd(), 'package.json');
|
|
114
|
+
const pkg = require(pkgPath);
|
|
115
|
+
return pkg.version || 'unknown';
|
|
116
|
+
} catch {
|
|
117
|
+
return 'unknown';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Start the server
|
|
123
|
+
*/
|
|
124
|
+
export async function startServer(options = {}) {
|
|
125
|
+
const { fastify, port, host } = createServer(options);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const address = await fastify.listen({ port, host });
|
|
129
|
+
fastify.log.info(`🚀 AgentWindow UI server running at http://${address}`);
|
|
130
|
+
|
|
131
|
+
// Open browser (optional)
|
|
132
|
+
if (options.open !== false) {
|
|
133
|
+
const { default: open } = await import('open');
|
|
134
|
+
await open(`http://${host}:${port}`).catch(() => {
|
|
135
|
+
fastify.log.warn('Could not open browser automatically');
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return fastify;
|
|
140
|
+
} catch (err) {
|
|
141
|
+
fastify.log.error(err);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// CLI entry point
|
|
147
|
+
export default { startServer, createServer };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Routes Registry
|
|
3
|
+
*
|
|
4
|
+
* Registers all WebSocket routes for real-time features.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { registerLogStream } from './logs.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Register all WebSocket routes
|
|
11
|
+
*/
|
|
12
|
+
export async function registerWebSocket(fastify) {
|
|
13
|
+
await registerLogStream(fastify);
|
|
14
|
+
|
|
15
|
+
fastify.log.info('All WebSocket routes registered');
|
|
16
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Stream WebSocket
|
|
3
|
+
*
|
|
4
|
+
* Real-time log streaming from PM2 processes.
|
|
5
|
+
* WS /ws/logs/:name - Stream logs for specific instance
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from 'child_process';
|
|
9
|
+
import { isWindows, isWSLAvailable } from '../../core/platform/detector.js';
|
|
10
|
+
|
|
11
|
+
// Active log streams by instance name
|
|
12
|
+
const activeStreams = new Map();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Register log streaming WebSocket route
|
|
16
|
+
*/
|
|
17
|
+
export async function registerLogStream(fastify) {
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* WebSocket /ws/logs/:name
|
|
21
|
+
* Stream logs for a specific instance
|
|
22
|
+
*/
|
|
23
|
+
fastify.register(async function (fastify) {
|
|
24
|
+
fastify.get('/ws/logs/:name', { websocket: true }, (connection, req) => {
|
|
25
|
+
const { name } = req.params;
|
|
26
|
+
|
|
27
|
+
// Close existing stream for this instance if any
|
|
28
|
+
if (activeStreams.has(name)) {
|
|
29
|
+
const existing = activeStreams.get(name);
|
|
30
|
+
existing.kill();
|
|
31
|
+
activeStreams.delete(name);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
connection.socket.on('open', () => {
|
|
35
|
+
fastify.log.info(`Log stream started for: ${name}`);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Send initial connection message
|
|
39
|
+
connection.socket.send(JSON.stringify({
|
|
40
|
+
type: 'connected',
|
|
41
|
+
instance: name,
|
|
42
|
+
timestamp: new Date().toISOString()
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// Start PM2 log stream
|
|
46
|
+
let pm2Cmd = 'pm2';
|
|
47
|
+
let pm2Args = ['logs', name, '--lines', '50', '--raw', '--nostream'];
|
|
48
|
+
|
|
49
|
+
// On Windows, use WSL if available
|
|
50
|
+
if (isWindows()) {
|
|
51
|
+
isWSLAvailable().then(wsl => {
|
|
52
|
+
if (wsl) {
|
|
53
|
+
pm2Cmd = 'wsl.exe';
|
|
54
|
+
pm2Args = ['pm2', ...pm2Args];
|
|
55
|
+
startLogStream();
|
|
56
|
+
} else {
|
|
57
|
+
startLogStream();
|
|
58
|
+
}
|
|
59
|
+
}).catch(() => startLogStream());
|
|
60
|
+
} else {
|
|
61
|
+
startLogStream();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function startLogStream() {
|
|
65
|
+
const logProcess = spawn(pm2Cmd, pm2Args, {
|
|
66
|
+
windowsHide: true
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
activeStreams.set(name, logProcess);
|
|
70
|
+
|
|
71
|
+
logProcess.stdout.on('data', (data) => {
|
|
72
|
+
try {
|
|
73
|
+
const lines = data.toString().split('\n').filter(l => l.trim());
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
connection.socket.send(JSON.stringify({
|
|
76
|
+
type: 'log',
|
|
77
|
+
instance: name,
|
|
78
|
+
data: line,
|
|
79
|
+
timestamp: new Date().toISOString()
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
// Socket might be closed
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
logProcess.stderr.on('data', (data) => {
|
|
88
|
+
try {
|
|
89
|
+
connection.socket.send(JSON.stringify({
|
|
90
|
+
type: 'error',
|
|
91
|
+
instance: name,
|
|
92
|
+
data: data.toString(),
|
|
93
|
+
timestamp: new Date().toISOString()
|
|
94
|
+
}));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// Socket might be closed
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
logProcess.on('close', (code) => {
|
|
101
|
+
connection.socket.send(JSON.stringify({
|
|
102
|
+
type: 'closed',
|
|
103
|
+
instance: name,
|
|
104
|
+
code,
|
|
105
|
+
timestamp: new Date().toISOString()
|
|
106
|
+
}));
|
|
107
|
+
activeStreams.delete(name);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
connection.socket.on('close', () => {
|
|
112
|
+
fastify.log.info(`Log stream closed for: ${name}`);
|
|
113
|
+
if (activeStreams.has(name)) {
|
|
114
|
+
const stream = activeStreams.get(name);
|
|
115
|
+
stream.kill();
|
|
116
|
+
activeStreams.delete(name);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
connection.socket.on('error', (err) => {
|
|
121
|
+
fastify.log.error(`WebSocket error for ${name}: ${err.message}`);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
fastify.log.info('Log stream WebSocket registered');
|
|
127
|
+
}
|