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.
Files changed (55) hide show
  1. package/README.md +21 -8
  2. package/README.zh-CN.md +151 -0
  3. package/bin/cli.js +45 -0
  4. package/docs/WEB_UI_GUIDE.md +249 -0
  5. package/package.json +15 -3
  6. package/scripts/test-platform.js +109 -0
  7. package/src/api/routes/index.js +25 -0
  8. package/src/api/routes/instances.js +252 -0
  9. package/src/api/routes/operations.js +118 -0
  10. package/src/api/routes/system.js +42 -0
  11. package/src/api/server.js +147 -0
  12. package/src/api/websocket/index.js +16 -0
  13. package/src/api/websocket/logs.js +127 -0
  14. package/src/cli/commands/add.js +80 -0
  15. package/src/cli/commands/config.js +192 -0
  16. package/src/cli/commands/index.js +89 -0
  17. package/src/cli/commands/info.js +94 -0
  18. package/src/cli/commands/list.js +72 -0
  19. package/src/cli/commands/logs.js +67 -0
  20. package/src/cli/commands/remove.js +97 -0
  21. package/src/cli/commands/restart.js +67 -0
  22. package/src/cli/commands/start.js +101 -0
  23. package/src/cli/commands/status.js +95 -0
  24. package/src/cli/commands/stop.js +53 -0
  25. package/src/cli/commands/ui.js +51 -0
  26. package/src/cli/index.js +110 -0
  27. package/src/core/config.js +5 -10
  28. package/src/core/instance/backup-manager.js +172 -0
  29. package/src/core/instance/config-manager.js +279 -0
  30. package/src/core/instance/index.js +62 -0
  31. package/src/core/instance/manager.js +220 -0
  32. package/src/core/instance/pm2-bridge.js +205 -0
  33. package/src/core/instance/validator.js +161 -0
  34. package/src/core/platform/detector.js +142 -0
  35. package/src/core/platform/docker-bridge.js +372 -0
  36. package/src/core/platform/index.js +27 -0
  37. package/src/core/platform/paths.js +112 -0
  38. package/src/core/platform/pm2-bridge.js +314 -0
  39. package/web/dist/assets/Dashboard-C1smB9Nj.js +1 -0
  40. package/web/dist/assets/Dashboard-ezbZMSpZ.css +1 -0
  41. package/web/dist/assets/InstanceDetail-CRPMV7rg.css +1 -0
  42. package/web/dist/assets/InstanceDetail-C_Ddtrog.js +3 -0
  43. package/web/dist/assets/Instances-CvnH8iDv.css +1 -0
  44. package/web/dist/assets/Instances-_u2__M83.js +1 -0
  45. package/web/dist/assets/Settings-CAu3R9RW.css +1 -0
  46. package/web/dist/assets/Settings-CIa9MX7m.js +1 -0
  47. package/web/dist/assets/_plugin-vue_export-helper-DlAUqK2U.js +1 -0
  48. package/web/dist/assets/element-plus-Jr6qTeY5.js +37 -0
  49. package/web/dist/assets/main-CalRvcyG.css +1 -0
  50. package/web/dist/assets/main-D3cdXAiV.js +7 -0
  51. package/web/dist/assets/vue-vendor-CGSlMM3Y.js +29 -0
  52. package/web/dist/index.html +16 -0
  53. package/SECURITY.md +0 -31
  54. package/docs/legacy/DEVELOPMENT.md +0 -174
  55. 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
+ }