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