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,279 @@
1
+ /**
2
+ * Config Manager
3
+ *
4
+ * Manages configuration files for AgentWindow instances.
5
+ * Handles reading, writing, validation, and external modification detection.
6
+ * Uses platform abstraction layer for cross-platform compatibility.
7
+ */
8
+
9
+ import fs from 'fs/promises';
10
+ import path from 'path';
11
+ import { existsSync } from 'fs';
12
+ import crypto from 'crypto';
13
+ import { paths } from '../platform/index.js';
14
+
15
+ /** @private */
16
+ const BACKUP_DIR = path.join(paths.getAgentWindowHome(), 'backups', 'configs');
17
+
18
+ /**
19
+ * Ensure backup directory exists
20
+ * @private
21
+ */
22
+ async function ensureBackupDir() {
23
+ if (!existsSync(BACKUP_DIR)) {
24
+ await fs.mkdir(BACKUP_DIR, { recursive: true });
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Calculate file checksum for change detection
30
+ * @param {string} content - File content
31
+ * @returns {string} SHA256 hash
32
+ */
33
+ function calculateChecksum(content) {
34
+ return crypto.createHash('sha256').update(content).digest('hex');
35
+ }
36
+
37
+ /**
38
+ * Read config file
39
+ * @param {string} configPath - Path to config file
40
+ * @returns {Promise<Object>} Config object
41
+ */
42
+ export async function readConfig(configPath) {
43
+ if (!existsSync(configPath)) {
44
+ throw new Error(`配置文件不存在: ${configPath}`);
45
+ }
46
+
47
+ const content = await fs.readFile(configPath, 'utf-8');
48
+ return JSON.parse(content);
49
+ }
50
+
51
+ /**
52
+ * Write config file with backup
53
+ * @param {string} configPath - Path to config file
54
+ * @param {Object} config - Config object to write
55
+ * @param {Object} options - Options
56
+ * @param {boolean} options.backup - Create backup before writing
57
+ * @returns {Promise<Object>} Result with checksum
58
+ */
59
+ export async function writeConfig(configPath, config, options = {}) {
60
+ const content = JSON.stringify(config, null, 2);
61
+ const checksum = calculateChecksum(content);
62
+
63
+ // Create backup if requested and file exists
64
+ if (options.backup !== false && existsSync(configPath)) {
65
+ await backupConfig(configPath);
66
+ }
67
+
68
+ // Write config atomically
69
+ const tmpPath = `${configPath}.tmp`;
70
+ await fs.writeFile(tmpPath, content, 'utf-8');
71
+ await fs.rename(tmpPath, configPath);
72
+
73
+ return { checksum };
74
+ }
75
+
76
+ /**
77
+ * Backup config file
78
+ * @param {string} configPath - Path to config file
79
+ * @returns {Promise<string>} Backup file path
80
+ */
81
+ export async function backupConfig(configPath) {
82
+ await ensureBackupDir();
83
+
84
+ if (!existsSync(configPath)) {
85
+ throw new Error(`无法备份不存在的文件: ${configPath}`);
86
+ }
87
+
88
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
89
+ const basename = path.basename(configPath, '.json');
90
+ const backupPath = path.join(BACKUP_DIR, `${basename}-${timestamp}.json`);
91
+
92
+ await fs.copyFile(configPath, backupPath);
93
+ return backupPath;
94
+ }
95
+
96
+ /**
97
+ * List available backups for a config
98
+ * @param {string} instanceName - Instance name
99
+ * @returns {Promise<Array>} List of backup files with metadata
100
+ */
101
+ export async function listBackups(instanceName) {
102
+ await ensureBackupDir();
103
+
104
+ if (!existsSync(BACKUP_DIR)) {
105
+ return [];
106
+ }
107
+
108
+ const files = await fs.readdir(BACKUP_DIR);
109
+ const pattern = new RegExp(`^${instanceName}-config-(\\d{4}-\\d{2}-\\d{2}T.+?)\\.json$`);
110
+
111
+ const backups = [];
112
+ for (const file of files) {
113
+ const match = file.match(pattern);
114
+ if (match) {
115
+ const filePath = path.join(BACKUP_DIR, file);
116
+ const stats = await fs.stat(filePath);
117
+ backups.push({
118
+ instance: instanceName,
119
+ file,
120
+ path: filePath,
121
+ createdAt: new Date(stats.mtime),
122
+ size: stats.size
123
+ });
124
+ }
125
+ }
126
+
127
+ return backups.sort((a, b) => b.createdAt - a.createdAt);
128
+ }
129
+
130
+ /**
131
+ * Restore config from backup
132
+ * @param {string} backupPath - Path to backup file
133
+ * @param {string} targetPath - Target config path
134
+ * @returns {Promise<void>}
135
+ */
136
+ export async function restoreBackup(backupPath, targetPath) {
137
+ if (!existsSync(backupPath)) {
138
+ throw new Error(`备份文件不存在: ${backupPath}`);
139
+ }
140
+
141
+ // Create backup of current before restoring
142
+ if (existsSync(targetPath)) {
143
+ await backupConfig(targetPath);
144
+ }
145
+
146
+ await fs.copyFile(backupPath, targetPath);
147
+ }
148
+
149
+ /**
150
+ * Get a config value by key path
151
+ * @param {Object} config - Config object
152
+ * @param {string} keyPath - Dot-separated key path (e.g., 'workspace.containerName')
153
+ * @returns {*} Config value
154
+ */
155
+ export function getConfigValue(config, keyPath) {
156
+ const keys = keyPath.split('.');
157
+ let value = config;
158
+
159
+ for (const key of keys) {
160
+ if (value && typeof value === 'object') {
161
+ value = value[key];
162
+ } else {
163
+ return undefined;
164
+ }
165
+ }
166
+
167
+ return value;
168
+ }
169
+
170
+ /**
171
+ * Set a config value by key path
172
+ * @param {Object} config - Config object
173
+ * @param {string} keyPath - Dot-separated key path
174
+ * @param {*} value - Value to set
175
+ * @returns {Object} Modified config
176
+ */
177
+ export function setConfigValue(config, keyPath, value) {
178
+ const keys = keyPath.split('.');
179
+ const result = { ...config };
180
+ let current = result;
181
+
182
+ for (let i = 0; i < keys.length - 1; i++) {
183
+ const key = keys[i];
184
+ if (!(key in current) || typeof current[key] !== 'object') {
185
+ current[key] = {};
186
+ }
187
+ current = current[key];
188
+ }
189
+
190
+ current[keys[keys.length - 1]] = value;
191
+ return result;
192
+ }
193
+
194
+ /**
195
+ * Detect if config was modified externally
196
+ * @param {string} configPath - Path to config file
197
+ * @param {string} storedChecksum - Previously stored checksum
198
+ * @returns {Promise<boolean>} True if modified
199
+ */
200
+ export async function detectExternalModification(configPath, storedChecksum) {
201
+ if (!existsSync(configPath)) {
202
+ return false;
203
+ }
204
+
205
+ const content = await fs.readFile(configPath, 'utf-8');
206
+ const currentChecksum = calculateChecksum(content);
207
+
208
+ return currentChecksum !== storedChecksum;
209
+ }
210
+
211
+ /**
212
+ * Config metadata tracker (stores checksums for external change detection)
213
+ * @private */
214
+ const METADATA_FILE = path.join(paths.getAgentWindowHome(), 'config-metadata.json');
215
+
216
+ /**
217
+ * Read config metadata
218
+ * @private
219
+ * @returns {Promise<Object>}
220
+ */
221
+ async function readMetadata() {
222
+ if (!existsSync(METADATA_FILE)) {
223
+ return {};
224
+ }
225
+ const content = await fs.readFile(METADATA_FILE, 'utf-8');
226
+ return JSON.parse(content);
227
+ }
228
+
229
+ /**
230
+ * Write config metadata
231
+ * @private
232
+ * @param {Object} metadata
233
+ * @returns {Promise<void>}
234
+ */
235
+ async function writeMetadata(metadata) {
236
+ const dir = path.dirname(METADATA_FILE);
237
+ if (!existsSync(dir)) {
238
+ await fs.mkdir(dir, { recursive: true });
239
+ }
240
+ await fs.writeFile(METADATA_FILE, JSON.stringify(metadata, null, 2));
241
+ }
242
+
243
+ /**
244
+ * Update config metadata after write
245
+ * @param {string} instanceName - Instance name
246
+ * @param {string} checksum - Config checksum
247
+ * @returns {Promise<void>}
248
+ */
249
+ export async function updateConfigMetadata(instanceName, checksum) {
250
+ const metadata = await readMetadata();
251
+ metadata[instanceName] = {
252
+ checksum,
253
+ updatedAt: new Date().toISOString()
254
+ };
255
+ await writeMetadata(metadata);
256
+ }
257
+
258
+ /**
259
+ * Check for external config modifications
260
+ * @param {string} instanceName - Instance name
261
+ * @param {string} configPath - Path to config file
262
+ * @returns {Promise<Object>} Result with modified flag
263
+ */
264
+ export async function checkExternalModification(instanceName, configPath) {
265
+ const metadata = await readMetadata();
266
+ const stored = metadata[instanceName];
267
+
268
+ if (!stored) {
269
+ return { modified: false, reason: 'no_metadata' };
270
+ }
271
+
272
+ const modified = await detectExternalModification(configPath, stored.checksum);
273
+
274
+ return {
275
+ modified,
276
+ checksum: stored.checksum,
277
+ updatedAt: stored.updatedAt
278
+ };
279
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Instance Management Module
3
+ *
4
+ * Unified exports for instance management functionality.
5
+ */
6
+
7
+ // Validator
8
+ export {
9
+ validateInstance,
10
+ formatValidationResult
11
+ } from './validator.js';
12
+
13
+ // Manager
14
+ export {
15
+ getInstancesFilePath,
16
+ readInstances,
17
+ writeInstances,
18
+ addInstance,
19
+ removeInstance,
20
+ getInstance,
21
+ listInstances,
22
+ updateInstance
23
+ } from './manager.js';
24
+
25
+ // Config Manager
26
+ export {
27
+ readConfig,
28
+ writeConfig,
29
+ backupConfig as backupConfigFile,
30
+ restoreBackup as restoreConfigBackup,
31
+ listBackups as listConfigBackups,
32
+ getConfigValue,
33
+ setConfigValue,
34
+ detectExternalModification,
35
+ checkExternalModification,
36
+ updateConfigMetadata
37
+ } from './config-manager.js';
38
+
39
+ // PM2 Bridge
40
+ export {
41
+ getProcesses,
42
+ getProcess,
43
+ getProcessesByPattern,
44
+ startProcess,
45
+ stopProcess,
46
+ restartProcess,
47
+ deleteProcess,
48
+ getLogs,
49
+ getStatus,
50
+ formatStatus
51
+ } from './pm2-bridge.js';
52
+
53
+ // Backup Manager
54
+ export {
55
+ getBackupDir,
56
+ ensureBackupDir,
57
+ createBackup,
58
+ listBackups,
59
+ restoreBackup,
60
+ pruneBackups,
61
+ getLatestBackup
62
+ } from './backup-manager.js';
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Instance Manager
3
+ *
4
+ * Manages AgentWindow BMAD plugin instances - add, list, remove, get info.
5
+ * Uses platform abstraction layer for cross-platform compatibility.
6
+ */
7
+
8
+ import fs from 'fs/promises';
9
+ import path from 'path';
10
+ import { existsSync } from 'fs';
11
+ import { validateInstance } from './validator.js';
12
+ import { paths } from '../platform/index.js';
13
+
14
+ /** @private */
15
+ const INSTANCES_DIR = paths.getAgentWindowHome();
16
+ /** @private */
17
+ const INSTANCES_FILE = path.join(INSTANCES_DIR, 'instances.json');
18
+
19
+ /**
20
+ * Get instances file path
21
+ * @returns {string}
22
+ */
23
+ export function getInstancesFilePath() {
24
+ return INSTANCES_FILE;
25
+ }
26
+
27
+ /**
28
+ * Ensure instances directory and file exist
29
+ * @private
30
+ */
31
+ async function ensureInstancesFile() {
32
+ if (!existsSync(INSTANCES_DIR)) {
33
+ await fs.mkdir(INSTANCES_DIR, { recursive: true });
34
+ }
35
+ if (!existsSync(INSTANCES_FILE)) {
36
+ await fs.writeFile(INSTANCES_FILE, JSON.stringify({
37
+ version: '1.0',
38
+ instances: []
39
+ }, null, 2));
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Read instances from file
45
+ * @returns {Promise<Object>} Instances data
46
+ */
47
+ export async function readInstances() {
48
+ await ensureInstancesFile();
49
+ const content = await fs.readFile(INSTANCES_FILE, 'utf-8');
50
+ return JSON.parse(content);
51
+ }
52
+
53
+ /**
54
+ * Write instances to file
55
+ * @param {Object} data - Instances data
56
+ * @returns {Promise<void>}
57
+ */
58
+ export async function writeInstances(data) {
59
+ await ensureInstancesFile();
60
+ await fs.writeFile(INSTANCES_FILE, JSON.stringify(data, null, 2));
61
+ }
62
+
63
+ /**
64
+ * Add a new instance
65
+ * @param {string} name - Instance name (identifier)
66
+ * @param {string} projectPath - Path to BMAD project
67
+ * @param {Object} options - Additional options
68
+ * @param {string} options.displayName - Display name
69
+ * @param {string[]} options.tags - Tags for categorization
70
+ * @param {string} options.configPath - Path to config file (optional)
71
+ * @returns {Promise<Object>} Result with success and message
72
+ */
73
+ export async function addInstance(name, projectPath, options = {}) {
74
+ // Validate input
75
+ if (!name || !name.match(/^[a-z0-9-]+$/)) {
76
+ return {
77
+ success: false,
78
+ error: '实例名称只能包含小写字母、数字和连字符'
79
+ };
80
+ }
81
+
82
+ // Check if instance already exists
83
+ const data = await readInstances();
84
+ if (data.instances.find(i => i.name === name)) {
85
+ return {
86
+ success: false,
87
+ error: `实例 "${name}" 已存在`
88
+ };
89
+ }
90
+
91
+ // Validate the instance
92
+ const validation = await validateInstance(projectPath);
93
+ if (!validation.valid) {
94
+ return {
95
+ success: false,
96
+ error: '验证失败:项目不是有效的 AgentWindow BMAD 插件',
97
+ validation
98
+ };
99
+ }
100
+
101
+ // Determine config path
102
+ const botsDir = paths.getBotsDir();
103
+ const defaultConfigPath = path.join(botsDir, name, 'config.json');
104
+ const configPath = options.configPath ||
105
+ (existsSync(defaultConfigPath) ? defaultConfigPath : null);
106
+
107
+ // Add instance
108
+ const newInstance = {
109
+ name,
110
+ displayName: options.displayName || name,
111
+ projectPath: validation.projectPath,
112
+ pluginPath: validation.pluginPath,
113
+ configPath,
114
+ botName: `bot-${name}`,
115
+ addedAt: new Date().toISOString(),
116
+ tags: options.tags || [],
117
+ enabled: true
118
+ };
119
+
120
+ data.instances.push(newInstance);
121
+ await writeInstances(data);
122
+
123
+ return {
124
+ success: true,
125
+ instance: newInstance
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Remove an instance
131
+ * @param {string} name - Instance name
132
+ * @returns {Promise<Object>} Result with success and message
133
+ */
134
+ export async function removeInstance(name) {
135
+ const data = await readInstances();
136
+ const index = data.instances.findIndex(i => i.name === name);
137
+
138
+ if (index === -1) {
139
+ return {
140
+ success: false,
141
+ error: `实例 "${name}" 不存在`
142
+ };
143
+ }
144
+
145
+ const removed = data.instances.splice(index, 1)[0];
146
+ await writeInstances(data);
147
+
148
+ return {
149
+ success: true,
150
+ instance: removed
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Get an instance by name
156
+ * @param {string} name - Instance name
157
+ * @returns {Promise<Object|null>} Instance or null
158
+ */
159
+ export async function getInstance(name) {
160
+ const data = await readInstances();
161
+ return data.instances.find(i => i.name === name) || null;
162
+ }
163
+
164
+ /**
165
+ * List all instances
166
+ * @param {Object} options - Filter options
167
+ * @param {boolean} options.enabledOnly - Only show enabled instances
168
+ * @param {string} options.tag - Filter by tag
169
+ * @returns {Promise<Array>} List of instances
170
+ */
171
+ export async function listInstances(options = {}) {
172
+ const data = await readInstances();
173
+ let instances = data.instances;
174
+
175
+ if (options.enabledOnly) {
176
+ instances = instances.filter(i => i.enabled);
177
+ }
178
+
179
+ if (options.tag) {
180
+ instances = instances.filter(i => i.tags?.includes(options.tag));
181
+ }
182
+
183
+ return instances;
184
+ }
185
+
186
+ /**
187
+ * Update an instance
188
+ * @param {string} name - Instance name
189
+ * @param {Object} updates - Fields to update
190
+ * @returns {Promise<Object>} Result with success and message
191
+ */
192
+ export async function updateInstance(name, updates) {
193
+ const data = await readInstances();
194
+ const index = data.instances.findIndex(i => i.name === name);
195
+
196
+ if (index === -1) {
197
+ return {
198
+ success: false,
199
+ error: `实例 "${name}" 不存在`
200
+ };
201
+ }
202
+
203
+ // Allowed fields to update
204
+ const allowedFields = ['displayName', 'tags', 'enabled', 'configPath'];
205
+ const instance = data.instances[index];
206
+
207
+ for (const [key, value] of Object.entries(updates)) {
208
+ if (allowedFields.includes(key)) {
209
+ instance[key] = value;
210
+ }
211
+ }
212
+
213
+ instance.updatedAt = new Date().toISOString();
214
+ await writeInstances(data);
215
+
216
+ return {
217
+ success: true,
218
+ instance
219
+ };
220
+ }