bmad-cybersec 2.0.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/README.md +483 -0
- package/cli.js +77 -0
- package/commands/install.js +301 -0
- package/commands/update.js +417 -0
- package/commands/version.js +18 -0
- package/index.js +2 -0
- package/lib/config.js +21 -0
- package/lib/downloader.js +297 -0
- package/lib/extractor.js +353 -0
- package/lib/git-clone.js +207 -0
- package/lib/logger.js +34 -0
- package/lib/package-merger.js +480 -0
- package/lib/url-validator.js +109 -0
- package/lib/utils.js +44 -0
- package/package.json +55 -0
package/lib/git-clone.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { mkdir, rm, cp, readdir, stat } from 'fs/promises';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import { CONFIG } from './config.js';
|
|
8
|
+
import { logger } from './logger.js';
|
|
9
|
+
import { assertValidRepoUrl } from './url-validator.js';
|
|
10
|
+
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
|
|
13
|
+
const DEFAULT_REPO_URL = `https://github.com/${CONFIG.GITHUB_OWNER}/${CONFIG.GITHUB_REPO}.git`;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Clones the BMAD framework repository from GitHub
|
|
17
|
+
* @description Performs a shallow clone of the repository to a temporary directory.
|
|
18
|
+
* Checks for git availability first and provides helpful error messages.
|
|
19
|
+
* @param {Object} [options={}] - Clone options
|
|
20
|
+
* @param {string} [options.branch='main'] - Branch name to clone
|
|
21
|
+
* @param {number} [options.depth=1] - Clone depth (1 for shallow clone)
|
|
22
|
+
* @param {string} [options.repoUrl] - Custom repository URL (defaults to BMAD-CYBERSEC repo)
|
|
23
|
+
* @returns {Promise<string>} Path to the cloned repository in temp directory
|
|
24
|
+
* @throws {Error} If git is not installed or not in PATH
|
|
25
|
+
* @throws {Error} If the specified branch is not found
|
|
26
|
+
* @throws {Error} If clone operation times out (120 second limit)
|
|
27
|
+
* @example
|
|
28
|
+
* // Clone main branch
|
|
29
|
+
* const repoPath = await cloneRepository();
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // Clone specific branch
|
|
33
|
+
* const repoPath = await cloneRepository({ branch: 'develop' });
|
|
34
|
+
*/
|
|
35
|
+
export async function cloneRepository(options = {}) {
|
|
36
|
+
const {
|
|
37
|
+
branch = 'main',
|
|
38
|
+
depth = 1,
|
|
39
|
+
repoUrl = DEFAULT_REPO_URL
|
|
40
|
+
} = options;
|
|
41
|
+
|
|
42
|
+
const spinner = ora();
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// 0. Validate repository URL to prevent command injection
|
|
46
|
+
assertValidRepoUrl(repoUrl);
|
|
47
|
+
|
|
48
|
+
// 1. Check git availability
|
|
49
|
+
spinner.start('Checking git availability...');
|
|
50
|
+
if (!await isGitAvailable()) {
|
|
51
|
+
spinner.fail('Git is not installed');
|
|
52
|
+
throw new Error(
|
|
53
|
+
'Git is not installed or not in PATH.\n' +
|
|
54
|
+
'Please install git or use the default download method (without --from-git).'
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
spinner.succeed('Git is available');
|
|
58
|
+
|
|
59
|
+
// 2. Create temp directory
|
|
60
|
+
const tempDir = join(tmpdir(), `${CONFIG.TEMP_DIR_PREFIX}-clone-${Date.now()}`);
|
|
61
|
+
await mkdir(tempDir, { recursive: true });
|
|
62
|
+
|
|
63
|
+
// 3. Validate branch name to prevent command injection
|
|
64
|
+
// Branch names can contain alphanumeric, -, _, ., and /
|
|
65
|
+
// See: https://git-scm.com/docs/git-check-ref-format
|
|
66
|
+
const SAFE_BRANCH_PATTERN = /^[a-zA-Z0-9][\w.\-\/]*$/;
|
|
67
|
+
if (!SAFE_BRANCH_PATTERN.test(branch)) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Invalid branch name: "${branch}". Branch names must start with alphanumeric ` +
|
|
70
|
+
`and contain only letters, numbers, hyphens, underscores, dots, and forward slashes.`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 4. Clone repository using execFile for safer execution (no shell injection)
|
|
75
|
+
spinner.start(`Cloning from ${branch} branch...`);
|
|
76
|
+
// Use execFile with array arguments - no shell interpolation, prevents command injection
|
|
77
|
+
const gitArgs = [
|
|
78
|
+
'clone',
|
|
79
|
+
'--depth', String(depth),
|
|
80
|
+
'--branch', branch,
|
|
81
|
+
repoUrl,
|
|
82
|
+
tempDir
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await execFileAsync('git', gitArgs, { timeout: 120000 }); // 2 min timeout
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error.message.includes('not found') || error.stderr?.includes('not found')) {
|
|
89
|
+
throw new Error(`Branch '${branch}' not found in repository`);
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
spinner.succeed('Repository cloned');
|
|
95
|
+
|
|
96
|
+
return tempDir;
|
|
97
|
+
|
|
98
|
+
} catch (error) {
|
|
99
|
+
spinner.fail(`Clone failed: ${error.message}`);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function isGitAvailable() {
|
|
105
|
+
try {
|
|
106
|
+
await execFileAsync('git', ['--version']);
|
|
107
|
+
return true;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Copies relevant framework files from source to target directory
|
|
115
|
+
* @description Recursively copies files while excluding unnecessary items like
|
|
116
|
+
* .git, node_modules, tests, and optionally docs/dev-tools.
|
|
117
|
+
* @param {string} sourceDir - Source directory (cloned repository)
|
|
118
|
+
* @param {string} targetDir - Target directory to copy files into
|
|
119
|
+
* @param {Object} [options={}] - Copy options
|
|
120
|
+
* @param {boolean} [options.withDocs=false] - Include documentation files
|
|
121
|
+
* @param {boolean} [options.withDev=false] - Include development tools
|
|
122
|
+
* @returns {Promise<number>} Number of files copied
|
|
123
|
+
* @example
|
|
124
|
+
* const fileCount = await copyRelevantFiles('./temp-clone', './my-project');
|
|
125
|
+
* console.log(`Copied ${fileCount} files`);
|
|
126
|
+
*/
|
|
127
|
+
export async function copyRelevantFiles(sourceDir, targetDir, options = {}) {
|
|
128
|
+
const spinner = ora('Copying framework files...').start();
|
|
129
|
+
|
|
130
|
+
const excludePatterns = [
|
|
131
|
+
'.git',
|
|
132
|
+
'node_modules',
|
|
133
|
+
'.github',
|
|
134
|
+
'__tests__',
|
|
135
|
+
'*.test.js',
|
|
136
|
+
'*.test.ts',
|
|
137
|
+
'*.spec.js',
|
|
138
|
+
'*.spec.ts',
|
|
139
|
+
'coverage',
|
|
140
|
+
'.nyc_output'
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// If not including docs/dev
|
|
144
|
+
if (!options.withDocs) {
|
|
145
|
+
excludePatterns.push('Docs');
|
|
146
|
+
}
|
|
147
|
+
if (!options.withDev) {
|
|
148
|
+
excludePatterns.push('dev-tools');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const filesCopied = await copyRecursive(sourceDir, targetDir, excludePatterns);
|
|
152
|
+
|
|
153
|
+
spinner.succeed(`Copied ${filesCopied} files`);
|
|
154
|
+
return filesCopied;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function copyRecursive(src, dest, excludePatterns, count = { files: 0 }) {
|
|
158
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
159
|
+
|
|
160
|
+
await mkdir(dest, { recursive: true });
|
|
161
|
+
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
const srcPath = join(src, entry.name);
|
|
164
|
+
const destPath = join(dest, entry.name);
|
|
165
|
+
|
|
166
|
+
// Check exclusions
|
|
167
|
+
if (shouldExclude(entry.name, excludePatterns)) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (entry.isDirectory()) {
|
|
172
|
+
await copyRecursive(srcPath, destPath, excludePatterns, count);
|
|
173
|
+
} else {
|
|
174
|
+
await cp(srcPath, destPath);
|
|
175
|
+
count.files++;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return count.files;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function shouldExclude(name, patterns) {
|
|
183
|
+
return patterns.some(pattern => {
|
|
184
|
+
if (pattern.includes('*')) {
|
|
185
|
+
const regex = new RegExp(pattern.replace('*', '.*'));
|
|
186
|
+
return regex.test(name);
|
|
187
|
+
}
|
|
188
|
+
return name === pattern;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Cleans up a cloned repository directory
|
|
194
|
+
* @description Removes the temporary directory created during clone operation.
|
|
195
|
+
* Silently ignores any cleanup errors.
|
|
196
|
+
* @param {string} tempDir - Path to the temporary clone directory to remove
|
|
197
|
+
* @returns {Promise<void>}
|
|
198
|
+
* @example
|
|
199
|
+
* await cleanupClone('/tmp/bmad-cyber-install-clone-123456');
|
|
200
|
+
*/
|
|
201
|
+
export async function cleanupClone(tempDir) {
|
|
202
|
+
try {
|
|
203
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
204
|
+
} catch {
|
|
205
|
+
// Ignore cleanup errors
|
|
206
|
+
}
|
|
207
|
+
}
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Logging utility with colored output
|
|
5
|
+
* @description Provides standardized logging methods with color-coded prefixes
|
|
6
|
+
* for different message types. Debug messages only appear when DEBUG env var is set.
|
|
7
|
+
* @type {Object}
|
|
8
|
+
* @property {Function} info - Logs an info message with blue prefix
|
|
9
|
+
* @property {Function} success - Logs a success message with green prefix
|
|
10
|
+
* @property {Function} warn - Logs a warning message with yellow prefix
|
|
11
|
+
* @property {Function} error - Logs an error message with red prefix
|
|
12
|
+
* @property {Function} debug - Logs a debug message (only when DEBUG env var is set)
|
|
13
|
+
* @example
|
|
14
|
+
* import { logger } from './logger.js';
|
|
15
|
+
*
|
|
16
|
+
* logger.info('Starting installation...');
|
|
17
|
+
* logger.success('Installation complete!');
|
|
18
|
+
* logger.warn('No checksum file found');
|
|
19
|
+
* logger.error('Download failed');
|
|
20
|
+
*
|
|
21
|
+
* // Debug logging (requires DEBUG=1 or DEBUG=true)
|
|
22
|
+
* logger.debug('Detailed debug information');
|
|
23
|
+
*/
|
|
24
|
+
export const logger = {
|
|
25
|
+
info: (msg) => console.log(chalk.blue('i'), msg),
|
|
26
|
+
success: (msg) => console.log(chalk.green('v'), msg),
|
|
27
|
+
warn: (msg) => console.log(chalk.yellow('!'), msg),
|
|
28
|
+
error: (msg) => console.log(chalk.red('x'), msg),
|
|
29
|
+
debug: (msg) => {
|
|
30
|
+
if (process.env.DEBUG) {
|
|
31
|
+
console.log(chalk.gray('[DEBUG]'), msg);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|