agent-window 1.0.1 → 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/bin/cli.js +45 -0
- package/docs/WEB_UI_GUIDE.md +249 -0
- package/package.json +11 -2
- 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,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* restart command - Restart an instance
|
|
3
|
+
*
|
|
4
|
+
* Usage: agent-window restart <name>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getInstance } from '../../core/instance/manager.js';
|
|
8
|
+
import { restartProcess, getStatus } from '../../core/instance/pm2-bridge.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Execute restart command
|
|
12
|
+
* @param {string} name - Instance name
|
|
13
|
+
* @returns {Promise<number>} Exit code
|
|
14
|
+
*/
|
|
15
|
+
export async function execute(name) {
|
|
16
|
+
if (!name) {
|
|
17
|
+
console.error('错误: 请提供实例名称');
|
|
18
|
+
console.error('用法: agent-window restart <name>');
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const instance = await getInstance(name);
|
|
23
|
+
if (!instance) {
|
|
24
|
+
console.error(`错误: 实例 "${name}" 不存在`);
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if running
|
|
29
|
+
let wasRunning = false;
|
|
30
|
+
try {
|
|
31
|
+
const status = await getStatus(instance.botName);
|
|
32
|
+
wasRunning = status.exists && status.status === 'online';
|
|
33
|
+
} catch {
|
|
34
|
+
// Ignore
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!wasRunning) {
|
|
38
|
+
console.log(`实例 "${name}" 未运行,正在启动...`);
|
|
39
|
+
const { execute: startCmd } = await import('./start.js');
|
|
40
|
+
return startCmd.execute(name);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(`重启实例 "${name}"...`);
|
|
44
|
+
|
|
45
|
+
const result = await restartProcess(instance.botName);
|
|
46
|
+
|
|
47
|
+
if (!result.success) {
|
|
48
|
+
console.error(`\n错误: ${result.error}`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(`✓ 实例 "${name}" 已重启`);
|
|
53
|
+
|
|
54
|
+
// Check status after restart
|
|
55
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
56
|
+
try {
|
|
57
|
+
const status = await getStatus(instance.botName);
|
|
58
|
+
if (status.exists) {
|
|
59
|
+
console.log(` 状态: ${status.status}`);
|
|
60
|
+
console.log(` PID: ${status.pid || 'N/A'}`);
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// Ignore
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* start command - Start an instance
|
|
3
|
+
*
|
|
4
|
+
* Usage: agent-window start <name>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getInstance } from '../../core/instance/manager.js';
|
|
8
|
+
import { startProcess, getStatus } from '../../core/instance/pm2-bridge.js';
|
|
9
|
+
import { existsSync } from 'fs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Execute start command
|
|
13
|
+
* @param {string} name - Instance name
|
|
14
|
+
* @param {Object} options - Options
|
|
15
|
+
* @returns {Promise<number>} Exit code
|
|
16
|
+
*/
|
|
17
|
+
export async function execute(name, options = {}) {
|
|
18
|
+
if (!name) {
|
|
19
|
+
console.error('错误: 请提供实例名称');
|
|
20
|
+
console.error('用法: agent-window start <name>');
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const instance = await getInstance(name);
|
|
25
|
+
if (!instance) {
|
|
26
|
+
console.error(`错误: 实例 "${name}" 不存在`);
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!instance.enabled) {
|
|
31
|
+
console.error(`错误: 实例 "${name}" 已禁用`);
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check if already running
|
|
36
|
+
try {
|
|
37
|
+
const status = await getStatus(instance.botName);
|
|
38
|
+
if (status.exists && status.status === 'online') {
|
|
39
|
+
console.log(`实例 "${name}" 已经在运行`);
|
|
40
|
+
if (!options.force) {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// Ignore PM2 check errors
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Find bot script
|
|
49
|
+
const botScript = options.script ||
|
|
50
|
+
(instance.pluginPath ? `${instance.pluginPath}/src/bot.js` : null);
|
|
51
|
+
|
|
52
|
+
if (!botScript || !existsSync(botScript)) {
|
|
53
|
+
console.error(`错误: 找不到 bot 脚本: ${botScript}`);
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Find config file
|
|
58
|
+
const configPath = instance.configPath;
|
|
59
|
+
if (!configPath || !existsSync(configPath)) {
|
|
60
|
+
console.error(`错误: 找不到配置文件: ${configPath}`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(`启动实例 "${name}"...`);
|
|
65
|
+
console.log(` 脚本: ${botScript}`);
|
|
66
|
+
console.log(` 配置: ${configPath}`);
|
|
67
|
+
|
|
68
|
+
// Start process
|
|
69
|
+
const result = await startProcess(botScript, instance.botName, {
|
|
70
|
+
cwd: instance.pluginPath,
|
|
71
|
+
env: {
|
|
72
|
+
CONFIG_PATH: configPath,
|
|
73
|
+
NODE_ENV: options.production ? 'production' : 'development'
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!result.success) {
|
|
78
|
+
console.error(`\n错误: ${result.error}`);
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(`\n✓ 实例 "${name}" 已启动`);
|
|
83
|
+
console.log(` 进程名: ${instance.botName}`);
|
|
84
|
+
|
|
85
|
+
// Wait a moment and check status
|
|
86
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
87
|
+
try {
|
|
88
|
+
const status = await getStatus(instance.botName);
|
|
89
|
+
if (status.exists) {
|
|
90
|
+
console.log(` 状态: ${status.status}`);
|
|
91
|
+
console.log(` PID: ${status.pid || 'N/A'}`);
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// Ignore
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(`\n使用以下命令查看日志:`);
|
|
98
|
+
console.log(` agent-window logs ${name}`);
|
|
99
|
+
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status command - Show status of all instances
|
|
3
|
+
*
|
|
4
|
+
* Usage: agent-window status
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { listInstances } from '../../core/instance/manager.js';
|
|
8
|
+
import { getStatus, formatStatus } from '../../core/instance/pm2-bridge.js';
|
|
9
|
+
import Table from 'cli-table3';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Execute status command
|
|
13
|
+
* @returns {Promise<number>} Exit code
|
|
14
|
+
*/
|
|
15
|
+
export async function execute() {
|
|
16
|
+
const instances = await listInstances();
|
|
17
|
+
|
|
18
|
+
if (instances.length === 0) {
|
|
19
|
+
console.log('没有配置实例');
|
|
20
|
+
console.log('\n使用 agent-window add <name> <path> 添加新实例');
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log(`\nAgentWindow 实例状态 (${instances.length} 个)\n`);
|
|
25
|
+
|
|
26
|
+
// Get status for each instance
|
|
27
|
+
const results = await Promise.all(
|
|
28
|
+
instances.map(async (inst) => {
|
|
29
|
+
try {
|
|
30
|
+
const status = await getStatus(inst.botName);
|
|
31
|
+
return { ...inst, status };
|
|
32
|
+
} catch {
|
|
33
|
+
return { ...inst, status: null };
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Count by status
|
|
39
|
+
let online = 0;
|
|
40
|
+
let stopped = 0;
|
|
41
|
+
let error = 0;
|
|
42
|
+
|
|
43
|
+
for (const r of results) {
|
|
44
|
+
if (!r.status || !r.status.exists) {
|
|
45
|
+
stopped++;
|
|
46
|
+
} else if (r.status.status === 'online') {
|
|
47
|
+
online++;
|
|
48
|
+
} else if (r.status.status === 'errored') {
|
|
49
|
+
error++;
|
|
50
|
+
} else {
|
|
51
|
+
stopped++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Display table
|
|
56
|
+
const table = new Table({
|
|
57
|
+
head: ['实例', '状态', 'PID', '内存', '重启', '项目'],
|
|
58
|
+
colWidths: [18, 12, 8, 10, 8, 25],
|
|
59
|
+
style: {
|
|
60
|
+
head: ['cyan', 'bold']
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
for (const r of results) {
|
|
65
|
+
const s = r.status;
|
|
66
|
+
if (s && s.exists) {
|
|
67
|
+
const statusIcon = { online: '✓', stopped: '○', errored: '✗' }[s.status] || '?';
|
|
68
|
+
const memMB = s.memory ? Math.round(s.memory / 1024 / 1024) : '-';
|
|
69
|
+
table.push([
|
|
70
|
+
r.name,
|
|
71
|
+
statusIcon + ' ' + s.status,
|
|
72
|
+
s.pid || '-',
|
|
73
|
+
memMB + 'MB',
|
|
74
|
+
s.restarts || 0,
|
|
75
|
+
r.displayName || r.name
|
|
76
|
+
]);
|
|
77
|
+
} else {
|
|
78
|
+
table.push([
|
|
79
|
+
r.name,
|
|
80
|
+
'○ 未配置',
|
|
81
|
+
'-',
|
|
82
|
+
'-',
|
|
83
|
+
'-',
|
|
84
|
+
r.displayName || r.name
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(table.toString());
|
|
90
|
+
|
|
91
|
+
// Summary
|
|
92
|
+
console.log(`\n总结: ${online} 运行中, ${stopped} 已停止, ${error} 错误`);
|
|
93
|
+
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stop command - Stop an instance
|
|
3
|
+
*
|
|
4
|
+
* Usage: agent-window stop <name>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getInstance } from '../../core/instance/manager.js';
|
|
8
|
+
import { stopProcess, getStatus } from '../../core/instance/pm2-bridge.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Execute stop command
|
|
12
|
+
* @param {string} name - Instance name
|
|
13
|
+
* @returns {Promise<number>} Exit code
|
|
14
|
+
*/
|
|
15
|
+
export async function execute(name) {
|
|
16
|
+
if (!name) {
|
|
17
|
+
console.error('错误: 请提供实例名称');
|
|
18
|
+
console.error('用法: agent-window stop <name>');
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const instance = await getInstance(name);
|
|
23
|
+
if (!instance) {
|
|
24
|
+
console.error(`错误: 实例 "${name}" 不存在`);
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if running
|
|
29
|
+
try {
|
|
30
|
+
const status = await getStatus(instance.botName);
|
|
31
|
+
if (!status.exists || status.status === 'stopped') {
|
|
32
|
+
console.log(`实例 "${name}" 未运行`);
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// Assume not running
|
|
37
|
+
console.log(`实例 "${name}" 未运行`);
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(`停止实例 "${name}"...`);
|
|
42
|
+
|
|
43
|
+
const result = await stopProcess(instance.botName);
|
|
44
|
+
|
|
45
|
+
if (!result.success) {
|
|
46
|
+
console.error(`\n错误: ${result.error}`);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(`✓ 实例 "${name}" 已停止`);
|
|
51
|
+
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Command
|
|
3
|
+
*
|
|
4
|
+
* Launch the AgentWindow Web UI.
|
|
5
|
+
* Starts the API server and serves the frontend.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const help = `
|
|
9
|
+
Launch the AgentWindow Web UI
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
agent-window ui [options]
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--port, -p <port> Port to listen on (default: 3721)
|
|
16
|
+
--no-open Don't open browser automatically
|
|
17
|
+
--dev Run in development mode
|
|
18
|
+
|
|
19
|
+
The UI provides a web-based interface for managing your bot instances:
|
|
20
|
+
- View instance status at a glance
|
|
21
|
+
- Start/stop/restart instances
|
|
22
|
+
- View real-time logs
|
|
23
|
+
- Manage configurations
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
export async function execute(args = []) {
|
|
27
|
+
// Parse arguments
|
|
28
|
+
const portIndex = args.indexOf('--port') ?? args.indexOf('-p');
|
|
29
|
+
const port = portIndex >= 0 && args[portIndex + 1]
|
|
30
|
+
? parseInt(args[portIndex + 1], 10)
|
|
31
|
+
: 3721;
|
|
32
|
+
|
|
33
|
+
const noOpen = args.includes('--no-open');
|
|
34
|
+
const dev = args.includes('--dev');
|
|
35
|
+
|
|
36
|
+
console.log(`\n🪟 AgentWindow Web UI\n`);
|
|
37
|
+
|
|
38
|
+
// Import and start server
|
|
39
|
+
const { startServer } = await import('../../api/server.js');
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await startServer({
|
|
43
|
+
port,
|
|
44
|
+
logger: true,
|
|
45
|
+
open: !noOpen
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error(`Failed to start server: ${error.message}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentWindow CLI
|
|
3
|
+
*
|
|
4
|
+
* Main entry point for instance management commands
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { commands } from './commands/index.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Show help message
|
|
11
|
+
*/
|
|
12
|
+
function showHelp() {
|
|
13
|
+
console.log('\nAgentWindow - BMAD 实例管理\n');
|
|
14
|
+
console.log('用法: agent-window <command> [options]\n');
|
|
15
|
+
console.log('命令:\n');
|
|
16
|
+
|
|
17
|
+
for (const [name, info] of Object.entries(commands)) {
|
|
18
|
+
console.log(` ${name.padEnd(12)} ${info.description}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log('\n使用 "agent-window <command> --help" 查看详细帮助\n');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse command line arguments
|
|
26
|
+
* @param {string[]} args - Command line arguments
|
|
27
|
+
* @returns {Object} Parsed command and options
|
|
28
|
+
*/
|
|
29
|
+
function parseArgs(args) {
|
|
30
|
+
const command = args[0];
|
|
31
|
+
const commandArgs = args.slice(1);
|
|
32
|
+
const options = {};
|
|
33
|
+
|
|
34
|
+
const remaining = [];
|
|
35
|
+
for (let i = 0; i < commandArgs.length; i++) {
|
|
36
|
+
const arg = commandArgs[i];
|
|
37
|
+
if (arg.startsWith('--')) {
|
|
38
|
+
const key = arg.slice(2);
|
|
39
|
+
if (key.includes('=')) {
|
|
40
|
+
const [k, v] = key.split('=');
|
|
41
|
+
options[k] = v;
|
|
42
|
+
} else if (i + 1 < commandArgs.length && !commandArgs[i + 1].startsWith('-')) {
|
|
43
|
+
options[k] = commandArgs[++i];
|
|
44
|
+
} else {
|
|
45
|
+
options[k] = true;
|
|
46
|
+
}
|
|
47
|
+
} else if (arg.startsWith('-')) {
|
|
48
|
+
const key = arg.slice(1);
|
|
49
|
+
if (key.length === 1) {
|
|
50
|
+
options[key] = true;
|
|
51
|
+
} else {
|
|
52
|
+
// Handle short flags like -f, -n
|
|
53
|
+
for (const char of key) {
|
|
54
|
+
options[char] = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
remaining.push(arg);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { command, args: remaining, options };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Main CLI entry point
|
|
67
|
+
* @param {string[]} args - Process arguments (excluding node and script)
|
|
68
|
+
* @returns {Promise<number>} Exit code
|
|
69
|
+
*/
|
|
70
|
+
export async function main(args) {
|
|
71
|
+
if (args.length === 0 || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
|
|
72
|
+
showHelp();
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { command, args: commandArgs, options } = parseArgs(args);
|
|
77
|
+
|
|
78
|
+
// Check if command exists
|
|
79
|
+
const commandInfo = commands[command];
|
|
80
|
+
if (!commandInfo) {
|
|
81
|
+
console.error(`错误: 未知命令 "${command}"`);
|
|
82
|
+
console.error('\n使用 "agent-window help" 查看可用命令');
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Import and execute command
|
|
87
|
+
try {
|
|
88
|
+
const cmdModule = await import(`./commands/${command}.js`);
|
|
89
|
+
return cmdModule.execute(commandArgs, options);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error(`错误执行命令 "${command}": ${err.message}`);
|
|
92
|
+
if (options.debug) {
|
|
93
|
+
console.error(err.stack);
|
|
94
|
+
}
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Run CLI when executed directly
|
|
101
|
+
*/
|
|
102
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
103
|
+
const args = process.argv.slice(2);
|
|
104
|
+
main(args).then(exitCode => {
|
|
105
|
+
process.exit(exitCode);
|
|
106
|
+
}).catch(err => {
|
|
107
|
+
console.error('未处理的错误:', err);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
});
|
|
110
|
+
}
|
package/src/core/config.js
CHANGED
|
@@ -3,27 +3,22 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Centralizes all configuration loading and validation.
|
|
5
5
|
* Supports both config file and environment variables.
|
|
6
|
+
* Uses platform abstraction layer for cross-platform compatibility.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { readFileSync, existsSync } from 'fs';
|
|
9
10
|
import { dirname, join } from 'path';
|
|
10
11
|
import { fileURLToPath } from 'url';
|
|
12
|
+
import { paths } from './platform/index.js';
|
|
11
13
|
|
|
12
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
15
|
const PROJECT_ROOT = join(__dirname, '../..');
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
|
-
* Expand tilde (~) in paths to home directory
|
|
18
|
+
* Expand tilde (~) in paths to home directory (cross-platform)
|
|
17
19
|
*/
|
|
18
20
|
function expandPath(path) {
|
|
19
|
-
|
|
20
|
-
if (path.startsWith('~/')) {
|
|
21
|
-
return join(process.env.HOME, path.slice(2));
|
|
22
|
-
}
|
|
23
|
-
if (path === '~') {
|
|
24
|
-
return process.env.HOME;
|
|
25
|
-
}
|
|
26
|
-
return path;
|
|
21
|
+
return paths.expandTilde(path);
|
|
27
22
|
}
|
|
28
23
|
|
|
29
24
|
/**
|
|
@@ -79,7 +74,7 @@ function loadConfig() {
|
|
|
79
74
|
type: fileConfig.backend?.type || 'claude-code',
|
|
80
75
|
oauthToken: fileConfig.CLAUDE_CODE_OAUTH_TOKEN || process.env.CLAUDE_CODE_OAUTH_TOKEN || '',
|
|
81
76
|
apiKey: fileConfig.backend?.apiKey || process.env.ANTHROPIC_API_KEY || '',
|
|
82
|
-
configDir: expandPath(fileConfig.backend?.configDir) ||
|
|
77
|
+
configDir: expandPath(fileConfig.backend?.configDir) || paths.getClaudeConfigDir(),
|
|
83
78
|
},
|
|
84
79
|
|
|
85
80
|
// === Workspace Configuration ===
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backup Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages configuration backups and rollback operations.
|
|
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 { paths } from '../platform/index.js';
|
|
12
|
+
|
|
13
|
+
/** @private */
|
|
14
|
+
const BACKUP_DIR = path.join(paths.getAgentWindowHome(), 'backups', 'configs');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get backup directory path
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
export function getBackupDir() {
|
|
21
|
+
return BACKUP_DIR;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Ensure backup directory exists
|
|
26
|
+
* @returns {Promise<void>}
|
|
27
|
+
*/
|
|
28
|
+
export async function ensureBackupDir() {
|
|
29
|
+
if (!existsSync(BACKUP_DIR)) {
|
|
30
|
+
await fs.mkdir(BACKUP_DIR, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a backup
|
|
36
|
+
* @param {string} sourcePath - Source file path
|
|
37
|
+
* @param {string} instanceName - Instance name (for naming)
|
|
38
|
+
* @returns {Promise<string>} Backup file path
|
|
39
|
+
*/
|
|
40
|
+
export async function createBackup(sourcePath, instanceName) {
|
|
41
|
+
await ensureBackupDir();
|
|
42
|
+
|
|
43
|
+
if (!existsSync(sourcePath)) {
|
|
44
|
+
throw new Error(`源文件不存在: ${sourcePath}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
48
|
+
const backupFileName = `${instanceName}-${timestamp}.json`;
|
|
49
|
+
const backupPath = path.join(BACKUP_DIR, backupFileName);
|
|
50
|
+
|
|
51
|
+
await fs.copyFile(sourcePath, backupPath);
|
|
52
|
+
|
|
53
|
+
// Create symlink to latest
|
|
54
|
+
const latestPath = path.join(BACKUP_DIR, `${instanceName}-latest.json`);
|
|
55
|
+
try {
|
|
56
|
+
if (existsSync(latestPath)) {
|
|
57
|
+
await fs.unlink(latestPath);
|
|
58
|
+
}
|
|
59
|
+
await fs.symlink(backupFileName, latestPath);
|
|
60
|
+
} catch {
|
|
61
|
+
// Symlink creation might fail, ignore
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return backupPath;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* List all backups for an instance
|
|
69
|
+
* @param {string} instanceName - Instance name
|
|
70
|
+
* @returns {Promise<Array>} List of backup info
|
|
71
|
+
*/
|
|
72
|
+
export async function listBackups(instanceName) {
|
|
73
|
+
await ensureBackupDir();
|
|
74
|
+
|
|
75
|
+
if (!existsSync(BACKUP_DIR)) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const files = await fs.readdir(BACKUP_DIR);
|
|
80
|
+
const pattern = new RegExp(`^${instanceName}-(\\d{4}-\\d{2}-\\d{2}T.+?)\\.json$`);
|
|
81
|
+
|
|
82
|
+
const backups = [];
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
const match = file.match(pattern);
|
|
85
|
+
if (match) {
|
|
86
|
+
const filePath = path.join(BACKUP_DIR, file);
|
|
87
|
+
const stats = await fs.stat(filePath);
|
|
88
|
+
backups.push({
|
|
89
|
+
name: instanceName,
|
|
90
|
+
filename: file,
|
|
91
|
+
path: filePath,
|
|
92
|
+
createdAt: new Date(stats.mtime),
|
|
93
|
+
size: stats.size,
|
|
94
|
+
isLatest: file.includes('latest')
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return backups.sort((a, b) => b.createdAt - a.createdAt);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Restore from backup
|
|
104
|
+
* @param {string} backupPath - Backup file path
|
|
105
|
+
* @param {string} targetPath - Target file path
|
|
106
|
+
* @param {boolean} createBackupOfCurrent - Backup current before restore
|
|
107
|
+
* @returns {Promise<Object>} Result
|
|
108
|
+
*/
|
|
109
|
+
export async function restoreBackup(backupPath, targetPath, createBackupOfCurrent = true) {
|
|
110
|
+
if (!existsSync(backupPath)) {
|
|
111
|
+
return {
|
|
112
|
+
success: false,
|
|
113
|
+
error: `备份文件不存在: ${backupPath}`
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Backup current file if it exists
|
|
118
|
+
if (createBackupOfCurrent && existsSync(targetPath)) {
|
|
119
|
+
const targetName = path.basename(targetPath, '.json');
|
|
120
|
+
try {
|
|
121
|
+
await createBackup(targetPath, `${targetName}-pre-restore`);
|
|
122
|
+
} catch {
|
|
123
|
+
// Ignore backup failure
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
await fs.copyFile(backupPath, targetPath);
|
|
129
|
+
return { success: true, restoredFrom: backupPath };
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return {
|
|
132
|
+
success: false,
|
|
133
|
+
error: error.message
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Delete old backups (keep only N latest)
|
|
140
|
+
* @param {string} instanceName - Instance name
|
|
141
|
+
* @param {number} keep - Number of backups to keep
|
|
142
|
+
* @returns {Promise<number>} Number of deleted backups
|
|
143
|
+
*/
|
|
144
|
+
export async function pruneBackups(instanceName, keep = 10) {
|
|
145
|
+
const backups = await listBackups(instanceName);
|
|
146
|
+
const toDelete = backups.slice(keep); // Keep first N, delete rest
|
|
147
|
+
|
|
148
|
+
let deleted = 0;
|
|
149
|
+
for (const backup of toDelete) {
|
|
150
|
+
if (!backup.isLatest) {
|
|
151
|
+
try {
|
|
152
|
+
await fs.unlink(backup.path);
|
|
153
|
+
deleted++;
|
|
154
|
+
} catch {
|
|
155
|
+
// Ignore deletion errors
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return deleted;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get latest backup for an instance
|
|
165
|
+
* @param {string} instanceName - Instance name
|
|
166
|
+
* @returns {Promise<string|null>} Backup path or null
|
|
167
|
+
*/
|
|
168
|
+
export async function getLatestBackup(instanceName) {
|
|
169
|
+
const backups = await listBackups(instanceName);
|
|
170
|
+
const latest = backups.find(b => b.isLatest);
|
|
171
|
+
return latest ? latest.path : (backups[0]?.path || null);
|
|
172
|
+
}
|