create-byan-agent 1.1.2 → 1.2.0
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/CHANGELOG.md +250 -177
- package/LICENSE +21 -21
- package/README.md +1245 -421
- package/bin/create-byan-agent-backup.js +220 -220
- package/bin/create-byan-agent-fixed.js +301 -301
- package/bin/create-byan-agent.js +322 -301
- package/lib/errors.js +61 -0
- package/lib/exit-codes.js +54 -0
- package/lib/platforms/claude-code.js +113 -0
- package/lib/platforms/codex.js +92 -0
- package/lib/platforms/copilot-cli.js +123 -0
- package/lib/platforms/index.js +14 -0
- package/lib/platforms/vscode.js +51 -0
- package/lib/utils/config-loader.js +79 -0
- package/lib/utils/file-utils.js +104 -0
- package/lib/utils/git-detector.js +35 -0
- package/lib/utils/logger.js +64 -0
- package/lib/utils/node-detector.js +58 -0
- package/lib/utils/os-detector.js +74 -0
- package/lib/utils/yaml-utils.js +87 -0
- package/lib/yanstaller/backuper.js +308 -0
- package/lib/yanstaller/detector.js +141 -0
- package/lib/yanstaller/index.js +93 -0
- package/lib/yanstaller/installer.js +225 -0
- package/lib/yanstaller/interviewer.js +250 -0
- package/lib/yanstaller/recommender.js +298 -0
- package/lib/yanstaller/troubleshooter.js +498 -0
- package/lib/yanstaller/validator.js +578 -0
- package/lib/yanstaller/wizard.js +211 -0
- package/package.json +61 -55
- package/templates/.github/agents/bmad-agent-bmad-master.md +15 -15
- package/templates/.github/agents/bmad-agent-bmb-agent-builder.md +15 -15
- package/templates/.github/agents/bmad-agent-bmb-module-builder.md +15 -15
- package/templates/.github/agents/bmad-agent-bmb-workflow-builder.md +15 -15
- package/templates/.github/agents/bmad-agent-bmm-analyst.md +15 -15
- package/templates/.github/agents/bmad-agent-bmm-architect.md +15 -15
- package/templates/.github/agents/bmad-agent-bmm-dev.md +15 -15
- package/templates/.github/agents/bmad-agent-bmm-pm.md +15 -15
- package/templates/.github/agents/bmad-agent-bmm-quick-flow-solo-dev.md +15 -15
- package/templates/.github/agents/bmad-agent-bmm-quinn.md +15 -15
- package/templates/.github/agents/bmad-agent-bmm-sm.md +15 -15
- package/templates/.github/agents/bmad-agent-bmm-tech-writer.md +15 -15
- package/templates/.github/agents/bmad-agent-bmm-ux-designer.md +15 -15
- package/templates/.github/agents/bmad-agent-byan-test.md +32 -0
- package/templates/.github/agents/bmad-agent-byan.md +224 -224
- package/templates/.github/agents/bmad-agent-carmack.md +18 -0
- package/templates/.github/agents/bmad-agent-cis-brainstorming-coach.md +15 -15
- package/templates/.github/agents/bmad-agent-cis-creative-problem-solver.md +15 -15
- package/templates/.github/agents/bmad-agent-cis-design-thinking-coach.md +15 -15
- package/templates/.github/agents/bmad-agent-cis-innovation-strategist.md +15 -15
- package/templates/.github/agents/bmad-agent-cis-presentation-master.md +15 -15
- package/templates/.github/agents/bmad-agent-cis-storyteller.md +15 -15
- package/templates/.github/agents/bmad-agent-marc.md +48 -48
- package/templates/.github/agents/bmad-agent-patnote.md +48 -0
- package/templates/.github/agents/bmad-agent-rachid.md +47 -47
- package/templates/.github/agents/bmad-agent-tea-tea.md +15 -15
- package/templates/.github/agents/bmad-agent-test-dynamic.md +21 -0
- package/templates/.github/agents/expert-merise-agile.md +1 -0
- package/templates/.github/agents/franck.md +379 -0
- package/templates/_bmad/bmb/agents/agent-builder.md +59 -59
- package/templates/_bmad/bmb/agents/byan-test.md +116 -116
- package/templates/_bmad/bmb/agents/byan.md +215 -215
- package/templates/_bmad/bmb/agents/marc.md +303 -303
- package/templates/_bmad/bmb/agents/module-builder.md +60 -60
- package/templates/_bmad/bmb/agents/patnote.md +495 -495
- package/templates/_bmad/bmb/agents/rachid.md +184 -184
- package/templates/_bmad/bmb/agents/workflow-builder.md +61 -61
- package/templates/_bmad/bmb/workflows/byan/data/mantras.yaml +272 -272
- package/templates/_bmad/bmb/workflows/byan/data/templates.yaml +59 -59
- package/templates/_bmad/bmb/workflows/byan/delete-agent-workflow.md +657 -657
- package/templates/_bmad/bmb/workflows/byan/edit-agent-workflow.md +688 -688
- package/templates/_bmad/bmb/workflows/byan/interview-workflow.md +753 -753
- package/templates/_bmad/bmb/workflows/byan/quick-create-workflow.md +450 -450
- package/templates/_bmad/bmb/workflows/byan/templates/base-agent-template.md +79 -79
- package/templates/_bmad/bmb/workflows/byan/validate-agent-workflow.md +676 -676
- package/templates/_bmad/core/agents/carmack.md +238 -238
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Detector Utility
|
|
3
|
+
*
|
|
4
|
+
* Detects if Git is installed.
|
|
5
|
+
*
|
|
6
|
+
* @module utils/git-detector
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { execSync } = require('child_process');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Detect if Git is installed
|
|
13
|
+
*
|
|
14
|
+
* @returns {Promise<{installed: boolean, version: string | null}>}
|
|
15
|
+
*/
|
|
16
|
+
async function detect() {
|
|
17
|
+
try {
|
|
18
|
+
const version = execSync('git --version', { encoding: 'utf8' }).trim();
|
|
19
|
+
const versionMatch = version.match(/git version ([\d.]+)/);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
installed: true,
|
|
23
|
+
version: versionMatch ? versionMatch[1] : null
|
|
24
|
+
};
|
|
25
|
+
} catch {
|
|
26
|
+
return {
|
|
27
|
+
installed: false,
|
|
28
|
+
version: null
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
detect
|
|
35
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger Utility
|
|
3
|
+
*
|
|
4
|
+
* Wrapper around chalk and console for colored logging.
|
|
5
|
+
*
|
|
6
|
+
* @module utils/logger
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Log info message
|
|
13
|
+
*
|
|
14
|
+
* @param {string} message - Message to log
|
|
15
|
+
*/
|
|
16
|
+
function info(message) {
|
|
17
|
+
console.log(chalk.blue('ℹ'), message);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Log success message
|
|
22
|
+
*
|
|
23
|
+
* @param {string} message - Message to log
|
|
24
|
+
*/
|
|
25
|
+
function success(message) {
|
|
26
|
+
console.log(chalk.green('✓'), message);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Log warning message
|
|
31
|
+
*
|
|
32
|
+
* @param {string} message - Message to log
|
|
33
|
+
*/
|
|
34
|
+
function warn(message) {
|
|
35
|
+
console.log(chalk.yellow('⚠'), message);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Log error message
|
|
40
|
+
*
|
|
41
|
+
* @param {string} message - Message to log
|
|
42
|
+
*/
|
|
43
|
+
function error(message) {
|
|
44
|
+
console.error(chalk.red('✖'), message);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Log debug message (only if DEBUG env var set)
|
|
49
|
+
*
|
|
50
|
+
* @param {string} message - Message to log
|
|
51
|
+
*/
|
|
52
|
+
function debug(message) {
|
|
53
|
+
if (process.env.DEBUG) {
|
|
54
|
+
console.log(chalk.gray('[DEBUG]'), message);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
info,
|
|
60
|
+
success,
|
|
61
|
+
warn,
|
|
62
|
+
error,
|
|
63
|
+
debug
|
|
64
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js Detector Utility
|
|
3
|
+
*
|
|
4
|
+
* Detects Node.js version.
|
|
5
|
+
*
|
|
6
|
+
* @module utils/node-detector
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Detect Node.js version
|
|
11
|
+
*
|
|
12
|
+
* @returns {string} - Version string (e.g., '18.19.0')
|
|
13
|
+
*/
|
|
14
|
+
function detect() {
|
|
15
|
+
return process.version.slice(1); // Remove 'v' prefix
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Compare two semver versions
|
|
20
|
+
*
|
|
21
|
+
* Strips version suffixes (-beta, -rc1, etc.) before comparison.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} version1 - First version
|
|
24
|
+
* @param {string} version2 - Second version
|
|
25
|
+
* @returns {number} - -1 if v1 < v2, 0 if equal, 1 if v1 > v2
|
|
26
|
+
*/
|
|
27
|
+
function compareVersions(version1, version2) {
|
|
28
|
+
// Strip suffixes: '18.0.0-beta' → '18.0.0'
|
|
29
|
+
const cleanV1 = version1.replace(/-.*$/, '');
|
|
30
|
+
const cleanV2 = version2.replace(/-.*$/, '');
|
|
31
|
+
|
|
32
|
+
const v1Parts = cleanV1.split('.').map(Number);
|
|
33
|
+
const v2Parts = cleanV2.split('.').map(Number);
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < 3; i++) {
|
|
36
|
+
if (v1Parts[i] > v2Parts[i]) return 1;
|
|
37
|
+
if (v1Parts[i] < v2Parts[i]) return -1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if Node version meets minimum requirement
|
|
45
|
+
*
|
|
46
|
+
* @param {string} currentVersion - Current Node version
|
|
47
|
+
* @param {string} requiredVersion - Required Node version
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
function meetsRequirement(currentVersion, requiredVersion) {
|
|
51
|
+
return compareVersions(currentVersion, requiredVersion) >= 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
detect,
|
|
56
|
+
compareVersions,
|
|
57
|
+
meetsRequirement
|
|
58
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OS Detector Utility
|
|
3
|
+
*
|
|
4
|
+
* Detects operating system and version.
|
|
5
|
+
*
|
|
6
|
+
* @module utils/os-detector
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Detect operating system
|
|
13
|
+
*
|
|
14
|
+
* @returns {{name: string, version: string, platform: string}}
|
|
15
|
+
*/
|
|
16
|
+
function detect() {
|
|
17
|
+
const platform = os.platform();
|
|
18
|
+
const release = os.release();
|
|
19
|
+
|
|
20
|
+
let name;
|
|
21
|
+
switch (platform) {
|
|
22
|
+
case 'win32':
|
|
23
|
+
name = 'windows';
|
|
24
|
+
break;
|
|
25
|
+
case 'darwin':
|
|
26
|
+
name = 'macos';
|
|
27
|
+
break;
|
|
28
|
+
case 'linux':
|
|
29
|
+
name = 'linux';
|
|
30
|
+
break;
|
|
31
|
+
default:
|
|
32
|
+
name = 'unknown';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
name,
|
|
37
|
+
version: release,
|
|
38
|
+
platform
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if running on Windows
|
|
44
|
+
*
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
function isWindows() {
|
|
48
|
+
return os.platform() === 'win32';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if running on macOS
|
|
53
|
+
*
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
function isMacOS() {
|
|
57
|
+
return os.platform() === 'darwin';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if running on Linux
|
|
62
|
+
*
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
function isLinux() {
|
|
66
|
+
return os.platform() === 'linux';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
detect,
|
|
71
|
+
isWindows,
|
|
72
|
+
isMacOS,
|
|
73
|
+
isLinux
|
|
74
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML Utilities
|
|
3
|
+
*
|
|
4
|
+
* Wrapper around js-yaml for YAML parsing/dumping.
|
|
5
|
+
*
|
|
6
|
+
* @module utils/yaml-utils
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const yaml = require('js-yaml');
|
|
10
|
+
const fileUtils = require('./file-utils');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse YAML string
|
|
14
|
+
*
|
|
15
|
+
* @param {string} yamlString - YAML string
|
|
16
|
+
* @returns {Object}
|
|
17
|
+
*/
|
|
18
|
+
function parse(yamlString) {
|
|
19
|
+
return yaml.load(yamlString);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Dump object to YAML string
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} obj - Object to dump
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function dump(obj) {
|
|
29
|
+
return yaml.dump(obj, {
|
|
30
|
+
indent: 2,
|
|
31
|
+
lineWidth: -1
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read YAML file
|
|
37
|
+
*
|
|
38
|
+
* @param {string} filePath - YAML file path
|
|
39
|
+
* @returns {Promise<Object>}
|
|
40
|
+
*/
|
|
41
|
+
async function readYAML(filePath) {
|
|
42
|
+
const content = await fileUtils.readFile(filePath);
|
|
43
|
+
return parse(content);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Write YAML file
|
|
48
|
+
*
|
|
49
|
+
* @param {string} filePath - YAML file path
|
|
50
|
+
* @param {Object} data - Data to write
|
|
51
|
+
* @returns {Promise<void>}
|
|
52
|
+
*/
|
|
53
|
+
async function writeYAML(filePath, data) {
|
|
54
|
+
const yamlString = dump(data);
|
|
55
|
+
await fileUtils.writeFile(filePath, yamlString);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extract YAML frontmatter from markdown
|
|
60
|
+
*
|
|
61
|
+
* @param {string} markdownContent - Markdown content
|
|
62
|
+
* @returns {{frontmatter: Object | null, content: string}}
|
|
63
|
+
*/
|
|
64
|
+
function extractFrontmatter(markdownContent) {
|
|
65
|
+
const frontmatterRegex = /^---\n([\s\S]+?)\n---\n([\s\S]*)$/;
|
|
66
|
+
const match = markdownContent.match(frontmatterRegex);
|
|
67
|
+
|
|
68
|
+
if (!match) {
|
|
69
|
+
return {
|
|
70
|
+
frontmatter: null,
|
|
71
|
+
content: markdownContent
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
frontmatter: parse(match[1]),
|
|
77
|
+
content: match[2]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
parse,
|
|
83
|
+
dump,
|
|
84
|
+
readYAML,
|
|
85
|
+
writeYAML,
|
|
86
|
+
extractFrontmatter
|
|
87
|
+
};
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BACKUPER Module
|
|
3
|
+
*
|
|
4
|
+
* Backs up and restores _bmad/ directory.
|
|
5
|
+
*
|
|
6
|
+
* Phase 6: 24h development
|
|
7
|
+
*
|
|
8
|
+
* @module yanstaller/backuper
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs-extra');
|
|
13
|
+
const fileUtils = require('../utils/file-utils');
|
|
14
|
+
const logger = require('../utils/logger');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} BackupResult
|
|
18
|
+
* @property {boolean} success
|
|
19
|
+
* @property {string} backupPath - Path to backup directory
|
|
20
|
+
* @property {number} filesBackedUp
|
|
21
|
+
* @property {number} size - Backup size in bytes
|
|
22
|
+
* @property {number} duration - Backup duration in ms
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} BackupInfo
|
|
27
|
+
* @property {string} path - Backup directory path
|
|
28
|
+
* @property {number} timestamp - Creation timestamp
|
|
29
|
+
* @property {Date} created - Creation date
|
|
30
|
+
* @property {number} size - Size in bytes
|
|
31
|
+
* @property {number} files - Number of files
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Backup _bmad/ directory
|
|
36
|
+
*
|
|
37
|
+
* @param {string} bmadPath - Path to _bmad/ directory
|
|
38
|
+
* @param {Object} options - Backup options
|
|
39
|
+
* @returns {Promise<BackupResult>}
|
|
40
|
+
*/
|
|
41
|
+
async function backup(bmadPath, options = {}) {
|
|
42
|
+
const startTime = Date.now();
|
|
43
|
+
const timestamp = Date.now();
|
|
44
|
+
const backupPath = `${bmadPath}.backup-${timestamp}`;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
logger.info(`Creating backup: ${backupPath}`);
|
|
48
|
+
|
|
49
|
+
// Check if source exists
|
|
50
|
+
if (!await fileUtils.exists(bmadPath)) {
|
|
51
|
+
throw new BackupError(`Source path does not exist: ${bmadPath}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Copy entire _bmad/ to backup path
|
|
55
|
+
await fileUtils.copy(bmadPath, backupPath);
|
|
56
|
+
|
|
57
|
+
// Count files and calculate size
|
|
58
|
+
const { files, size } = await getDirectoryStats(backupPath);
|
|
59
|
+
|
|
60
|
+
// Create metadata file
|
|
61
|
+
const metadata = {
|
|
62
|
+
timestamp,
|
|
63
|
+
created: new Date(timestamp).toISOString(),
|
|
64
|
+
source: bmadPath,
|
|
65
|
+
files,
|
|
66
|
+
size,
|
|
67
|
+
version: '1.2.0'
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
await fileUtils.writeFile(
|
|
71
|
+
path.join(backupPath, '.backup-metadata.json'),
|
|
72
|
+
JSON.stringify(metadata, null, 2)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const duration = Date.now() - startTime;
|
|
76
|
+
|
|
77
|
+
logger.info(`✓ Backup created: ${files} files, ${formatSize(size)}, ${duration}ms`);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
success: true,
|
|
81
|
+
backupPath,
|
|
82
|
+
filesBackedUp: files,
|
|
83
|
+
size,
|
|
84
|
+
duration
|
|
85
|
+
};
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new BackupError(`Failed to backup ${bmadPath}`, { cause: error });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Restore from backup
|
|
93
|
+
*
|
|
94
|
+
* @param {string} backupPath - Path to backup directory
|
|
95
|
+
* @param {string} targetPath - Target restoration path
|
|
96
|
+
* @param {Object} options - Restore options
|
|
97
|
+
* @returns {Promise<void>}
|
|
98
|
+
*/
|
|
99
|
+
async function restore(backupPath, targetPath, options = {}) {
|
|
100
|
+
try {
|
|
101
|
+
logger.info(`Restoring from backup: ${backupPath}`);
|
|
102
|
+
|
|
103
|
+
// Verify backup exists
|
|
104
|
+
if (!await fileUtils.exists(backupPath)) {
|
|
105
|
+
throw new BackupError(`Backup not found: ${backupPath}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Verify backup metadata
|
|
109
|
+
const metadataPath = path.join(backupPath, '.backup-metadata.json');
|
|
110
|
+
if (await fileUtils.exists(metadataPath)) {
|
|
111
|
+
const metadata = await fileUtils.readJSON(metadataPath);
|
|
112
|
+
logger.info(`Backup created: ${metadata.created}, ${metadata.files} files`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Create backup of current state before restoring (if exists)
|
|
116
|
+
if (await fileUtils.exists(targetPath) && !options.skipCurrentBackup) {
|
|
117
|
+
logger.info('Creating backup of current state before restore...');
|
|
118
|
+
await backup(targetPath);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Remove current installation
|
|
122
|
+
if (await fileUtils.exists(targetPath)) {
|
|
123
|
+
await fileUtils.remove(targetPath);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Copy backup to target
|
|
127
|
+
await fileUtils.copy(backupPath, targetPath);
|
|
128
|
+
|
|
129
|
+
// Remove metadata file from restored directory
|
|
130
|
+
const restoredMetadata = path.join(targetPath, '.backup-metadata.json');
|
|
131
|
+
if (await fileUtils.exists(restoredMetadata)) {
|
|
132
|
+
await fileUtils.remove(restoredMetadata);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
logger.info('✓ Restore completed successfully');
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw new BackupError(`Failed to restore from ${backupPath}`, { cause: error });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* List available backups
|
|
143
|
+
*
|
|
144
|
+
* @param {string} projectRoot - Project root directory
|
|
145
|
+
* @returns {Promise<BackupInfo[]>} - Array of backup info objects
|
|
146
|
+
*/
|
|
147
|
+
async function listBackups(projectRoot) {
|
|
148
|
+
try {
|
|
149
|
+
const backups = [];
|
|
150
|
+
const bmadPath = path.join(projectRoot, '_bmad');
|
|
151
|
+
const parentDir = path.dirname(bmadPath);
|
|
152
|
+
|
|
153
|
+
// Find all _bmad.backup-* directories
|
|
154
|
+
const items = await fileUtils.readDir(parentDir);
|
|
155
|
+
|
|
156
|
+
for (const item of items) {
|
|
157
|
+
if (item.startsWith('_bmad.backup-')) {
|
|
158
|
+
const backupPath = path.join(parentDir, item);
|
|
159
|
+
const timestamp = parseInt(item.replace('_bmad.backup-', ''));
|
|
160
|
+
|
|
161
|
+
// Read metadata if available
|
|
162
|
+
const metadataPath = path.join(backupPath, '.backup-metadata.json');
|
|
163
|
+
let metadata = { files: 0, size: 0 };
|
|
164
|
+
|
|
165
|
+
if (await fileUtils.exists(metadataPath)) {
|
|
166
|
+
metadata = await fileUtils.readJSON(metadataPath);
|
|
167
|
+
} else {
|
|
168
|
+
// Calculate if no metadata
|
|
169
|
+
const stats = await getDirectoryStats(backupPath);
|
|
170
|
+
metadata.files = stats.files;
|
|
171
|
+
metadata.size = stats.size;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
backups.push({
|
|
175
|
+
path: backupPath,
|
|
176
|
+
timestamp,
|
|
177
|
+
created: new Date(timestamp),
|
|
178
|
+
size: metadata.size,
|
|
179
|
+
files: metadata.files
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Sort by timestamp (newest first)
|
|
185
|
+
backups.sort((a, b) => b.timestamp - a.timestamp);
|
|
186
|
+
|
|
187
|
+
return backups;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error.code === 'ENOENT') {
|
|
190
|
+
return []; // No backups found
|
|
191
|
+
}
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Clean old backups (keep last N)
|
|
198
|
+
*
|
|
199
|
+
* @param {string} projectRoot - Project root directory
|
|
200
|
+
* @param {number} keep - Number of backups to keep
|
|
201
|
+
* @returns {Promise<number>} - Number of backups deleted
|
|
202
|
+
*/
|
|
203
|
+
async function cleanOldBackups(projectRoot, keep = 3) {
|
|
204
|
+
try {
|
|
205
|
+
const backups = await listBackups(projectRoot);
|
|
206
|
+
|
|
207
|
+
if (backups.length <= keep) {
|
|
208
|
+
return 0; // Nothing to delete
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Delete oldest backups
|
|
212
|
+
const toDelete = backups.slice(keep);
|
|
213
|
+
let deleted = 0;
|
|
214
|
+
|
|
215
|
+
for (const backup of toDelete) {
|
|
216
|
+
try {
|
|
217
|
+
await fileUtils.remove(backup.path);
|
|
218
|
+
logger.info(`✓ Deleted old backup: ${path.basename(backup.path)}`);
|
|
219
|
+
deleted++;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
logger.warn(`Failed to delete backup ${backup.path}: ${error.message}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return deleted;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
throw new BackupError(`Failed to clean old backups: ${error.message}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get backup size
|
|
233
|
+
*
|
|
234
|
+
* @param {string} backupPath - Path to backup directory
|
|
235
|
+
* @returns {Promise<number>} - Size in bytes
|
|
236
|
+
*/
|
|
237
|
+
async function getBackupSize(backupPath) {
|
|
238
|
+
const stats = await getDirectoryStats(backupPath);
|
|
239
|
+
return stats.size;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get directory statistics (files count and total size)
|
|
244
|
+
*
|
|
245
|
+
* @param {string} dirPath - Directory path
|
|
246
|
+
* @returns {Promise<{files: number, size: number}>}
|
|
247
|
+
*/
|
|
248
|
+
async function getDirectoryStats(dirPath) {
|
|
249
|
+
let files = 0;
|
|
250
|
+
let size = 0;
|
|
251
|
+
|
|
252
|
+
async function traverse(currentPath) {
|
|
253
|
+
const items = await fileUtils.readDir(currentPath);
|
|
254
|
+
|
|
255
|
+
for (const item of items) {
|
|
256
|
+
const itemPath = path.join(currentPath, item);
|
|
257
|
+
const stats = await fs.stat(itemPath);
|
|
258
|
+
|
|
259
|
+
if (stats.isDirectory()) {
|
|
260
|
+
await traverse(itemPath);
|
|
261
|
+
} else {
|
|
262
|
+
files++;
|
|
263
|
+
size += stats.size;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
await traverse(dirPath);
|
|
269
|
+
|
|
270
|
+
return { files, size };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Format size in human-readable format
|
|
275
|
+
*
|
|
276
|
+
* @param {number} bytes - Size in bytes
|
|
277
|
+
* @returns {string}
|
|
278
|
+
*/
|
|
279
|
+
function formatSize(bytes) {
|
|
280
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
281
|
+
let size = bytes;
|
|
282
|
+
let unitIndex = 0;
|
|
283
|
+
|
|
284
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
285
|
+
size /= 1024;
|
|
286
|
+
unitIndex++;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
class BackupError extends Error {
|
|
293
|
+
constructor(message, options) {
|
|
294
|
+
super(message, options);
|
|
295
|
+
this.name = 'BackupError';
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = {
|
|
300
|
+
backup,
|
|
301
|
+
restore,
|
|
302
|
+
listBackups,
|
|
303
|
+
cleanOldBackups,
|
|
304
|
+
getBackupSize,
|
|
305
|
+
getDirectoryStats,
|
|
306
|
+
formatSize,
|
|
307
|
+
BackupError
|
|
308
|
+
};
|