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.
@@ -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
+ };