create-byan-agent 1.1.3 → 1.2.1
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 +273 -202
- package/LICENSE +21 -21
- package/README.md +1251 -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 +155 -322
- 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,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
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DETECTOR Module
|
|
3
|
+
*
|
|
4
|
+
* Detects OS, Node.js version, Git, and installed platforms.
|
|
5
|
+
*
|
|
6
|
+
* Phase 1: 40h development
|
|
7
|
+
*
|
|
8
|
+
* @module yanstaller/detector
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const osDetector = require('../utils/os-detector');
|
|
12
|
+
const nodeDetector = require('../utils/node-detector');
|
|
13
|
+
const gitDetector = require('../utils/git-detector');
|
|
14
|
+
const platforms = require('../platforms');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} DetectionResult
|
|
18
|
+
* @property {string} os - 'windows' | 'linux' | 'macos'
|
|
19
|
+
* @property {string} osVersion - e.g., '11' for Windows 11
|
|
20
|
+
* @property {string} nodeVersion - e.g., '18.19.0'
|
|
21
|
+
* @property {boolean} hasGit
|
|
22
|
+
* @property {string} [gitVersion] - e.g., '2.43.0'
|
|
23
|
+
* @property {PlatformInfo[]} platforms - Detected platforms
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} PlatformInfo
|
|
28
|
+
* @property {string} name - 'copilot-cli' | 'vscode' | 'claude' | 'codex'
|
|
29
|
+
* @property {boolean} detected
|
|
30
|
+
* @property {string} [path] - Installation path if detected
|
|
31
|
+
* @property {string} [version] - Version if detected
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const logger = require('../utils/logger');
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Detect full environment
|
|
38
|
+
*
|
|
39
|
+
* Runs parallel detection for speed.
|
|
40
|
+
* Non-blocking: platform detection failures are caught and logged.
|
|
41
|
+
*
|
|
42
|
+
* @returns {Promise<DetectionResult>}
|
|
43
|
+
*/
|
|
44
|
+
async function detect() {
|
|
45
|
+
// Parallel detection for speed (Mantra #7 KISS)
|
|
46
|
+
const [osInfo, nodeVersion, gitInfo] = await Promise.all([
|
|
47
|
+
osDetector.detect(),
|
|
48
|
+
Promise.resolve(nodeDetector.detect()), // Sync wrapped in Promise
|
|
49
|
+
gitDetector.detect()
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
// Platform detection with timeout protection
|
|
53
|
+
const platformNames = ['copilot-cli', 'vscode', 'claude', 'codex'];
|
|
54
|
+
const platformsInfo = await Promise.all(
|
|
55
|
+
platformNames.map(name => detectPlatform(name))
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Check if ALL platforms failed
|
|
59
|
+
const allFailed = platformsInfo.every(p => !p.detected);
|
|
60
|
+
if (allFailed) {
|
|
61
|
+
const errors = platformsInfo
|
|
62
|
+
.filter(p => p.error)
|
|
63
|
+
.map(p => `${p.name}: ${p.error}`)
|
|
64
|
+
.join(', ');
|
|
65
|
+
if (errors) {
|
|
66
|
+
logger.warn(`0/4 platforms detected. Errors: [${errors}]`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
os: osInfo.name,
|
|
72
|
+
osVersion: osInfo.version,
|
|
73
|
+
nodeVersion,
|
|
74
|
+
hasGit: gitInfo.installed,
|
|
75
|
+
gitVersion: gitInfo.version,
|
|
76
|
+
platforms: platformsInfo
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if Node.js version meets minimum requirement
|
|
82
|
+
*
|
|
83
|
+
* Handles version suffixes (-beta, -rc1) by stripping them.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} currentVersion - e.g., '18.19.0'
|
|
86
|
+
* @param {string} requiredVersion - e.g., '18.0.0'
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
function isNodeVersionValid(currentVersion, requiredVersion) {
|
|
90
|
+
return nodeDetector.meetsRequirement(currentVersion, requiredVersion);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Detect specific platform
|
|
95
|
+
*
|
|
96
|
+
* Non-blocking: errors are caught and returned in result.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} platformName - 'copilot-cli' | 'vscode' | 'claude' | 'codex'
|
|
99
|
+
* @returns {Promise<PlatformInfo>}
|
|
100
|
+
*/
|
|
101
|
+
async function detectPlatform(platformName) {
|
|
102
|
+
const platform = platforms[platformName];
|
|
103
|
+
if (!platform) {
|
|
104
|
+
throw new Error(`Unknown platform: ${platformName}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const detected = await platform.detect();
|
|
109
|
+
|
|
110
|
+
// Handle timeout response format (object with detected + error)
|
|
111
|
+
if (typeof detected === 'object' && 'error' in detected) {
|
|
112
|
+
logger.warn(`Platform ${platformName} detection failed: ${detected.error}`);
|
|
113
|
+
return {
|
|
114
|
+
name: platformName,
|
|
115
|
+
detected: false,
|
|
116
|
+
error: detected.error
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
name: platformName,
|
|
122
|
+
detected: !!detected,
|
|
123
|
+
path: detected ? platform.getPath() : undefined
|
|
124
|
+
};
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// Non-blocking: platform detection failure shouldn't crash detection
|
|
127
|
+
// Error UX: Log warning and include in report for user visibility
|
|
128
|
+
logger.warn(`Platform ${platformName} detection failed: ${error.message}`);
|
|
129
|
+
return {
|
|
130
|
+
name: platformName,
|
|
131
|
+
detected: false,
|
|
132
|
+
error: error.message
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
detect,
|
|
139
|
+
isNodeVersionValid,
|
|
140
|
+
detectPlatform
|
|
141
|
+
};
|