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,301 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { existsSync } from 'fs';
4
+ import { join } from 'path';
5
+ import ora from 'ora';
6
+ import chalk from 'chalk';
7
+ import { downloadRelease, cleanup as cleanupDownload } from '../lib/downloader.js';
8
+ import { cloneRepository, copyRelevantFiles, cleanupClone } from '../lib/git-clone.js';
9
+ import { extractFramework } from '../lib/extractor.js';
10
+ import { mergePackageJson } from '../lib/package-merger.js';
11
+ import { logger } from '../lib/logger.js';
12
+ import { assertValidRepoUrl } from '../lib/url-validator.js';
13
+
14
+ const execAsync = promisify(exec);
15
+
16
+ // Track installation state for rollback
17
+ const installState = {
18
+ downloadPath: null,
19
+ extractedFiles: [],
20
+ packageJsonBackup: null,
21
+ npmInstalled: false
22
+ };
23
+
24
+ export async function installCommand(options) {
25
+ const startTime = Date.now();
26
+ const spinner = ora();
27
+
28
+ // Set up graceful shutdown
29
+ const cleanupHandler = async () => {
30
+ console.log('\n');
31
+ spinner.fail('Installation interrupted');
32
+ await rollback();
33
+ process.exit(130);
34
+ };
35
+ process.on('SIGINT', cleanupHandler);
36
+
37
+ try {
38
+ const targetDir = process.cwd();
39
+
40
+ // Validate target directory
41
+ logger.info(`Installing BMAD-CYBER to: ${targetDir}\n`);
42
+
43
+ // Step 1: Download or clone
44
+ spinner.start('Step 1/6: Downloading BMAD-CYBER framework...');
45
+
46
+ let sourcePath;
47
+ if (options.fromGit) {
48
+ // Validate repo URL before passing to cloneRepository (defense in depth)
49
+ if (options.repo) {
50
+ assertValidRepoUrl(options.repo);
51
+ }
52
+
53
+ sourcePath = await cloneRepository({
54
+ branch: options.branch || 'main',
55
+ repoUrl: options.repo
56
+ });
57
+ installState.downloadPath = sourcePath;
58
+
59
+ if (!options.dryRun) {
60
+ await copyRelevantFiles(sourcePath, targetDir, {
61
+ withDocs: options.withDocs,
62
+ withDev: options.withDev
63
+ });
64
+ }
65
+ } else {
66
+ sourcePath = await downloadRelease({
67
+ version: options.version,
68
+ branch: options.branch
69
+ });
70
+ installState.downloadPath = sourcePath;
71
+ }
72
+
73
+ spinner.succeed('Step 1/6: Download complete');
74
+
75
+ // Step 2: Extract (if tarball)
76
+ if (!options.fromGit) {
77
+ spinner.start('Step 2/6: Extracting framework files...');
78
+
79
+ const extractResult = await extractFramework(sourcePath, targetDir, {
80
+ force: options.force,
81
+ withDocs: options.withDocs,
82
+ withDev: options.withDev,
83
+ dryRun: options.dryRun
84
+ });
85
+
86
+ if (extractResult.cancelled) {
87
+ spinner.fail('Installation cancelled by user');
88
+ process.exit(0);
89
+ }
90
+
91
+ installState.extractedFiles = extractResult.files || [];
92
+ spinner.succeed(`Step 2/6: Extracted ${extractResult.filesExtracted} files`);
93
+ } else {
94
+ spinner.succeed('Step 2/6: Files copied from repository');
95
+ }
96
+
97
+ // Step 3: Merge package.json
98
+ spinner.start('Step 3/6: Configuring package.json...');
99
+
100
+ const mergeResult = await mergePackageJson(targetDir, {
101
+ yes: options.yes,
102
+ dryRun: options.dryRun
103
+ });
104
+
105
+ if (mergeResult.cancelled) {
106
+ spinner.fail('Installation cancelled by user');
107
+ await rollback();
108
+ process.exit(0);
109
+ }
110
+
111
+ if (mergeResult.backupPath) {
112
+ installState.packageJsonBackup = mergeResult.backupPath;
113
+ }
114
+
115
+ spinner.succeed('Step 3/6: Package.json configured');
116
+
117
+ // Step 4: npm install
118
+ // Security: GH-092-002 - npm postinstall scripts can execute arbitrary code
119
+ // We use --ignore-scripts in secure mode to prevent privilege escalation
120
+ if (!options.skipNpmInstall && !options.dryRun) {
121
+ spinner.start('Step 4/6: Installing dependencies (this may take a moment)...');
122
+
123
+ try {
124
+ // Security: Use --ignore-scripts to prevent postinstall script attacks
125
+ // unless user explicitly opts out with --allow-scripts
126
+ const npmCommand = options.allowScripts
127
+ ? 'npm install'
128
+ : 'npm install --ignore-scripts';
129
+
130
+ if (!options.allowScripts) {
131
+ logger.info('Running npm install with --ignore-scripts for security (use --allow-scripts to enable)');
132
+ }
133
+
134
+ await execAsync(npmCommand, {
135
+ cwd: targetDir,
136
+ timeout: 300000 // 5 min timeout
137
+ });
138
+ installState.npmInstalled = true;
139
+ spinner.succeed('Step 4/6: Dependencies installed');
140
+
141
+ // If we used --ignore-scripts, warn user about potential missing setup
142
+ if (!options.allowScripts) {
143
+ logger.info('Note: Postinstall scripts were skipped. If you need them, run: npm rebuild');
144
+ }
145
+ } catch (error) {
146
+ spinner.warn('Step 4/6: npm install had issues (you may need to run manually)');
147
+ logger.warn(`npm install error: ${error.message}`);
148
+ }
149
+ } else {
150
+ spinner.succeed('Step 4/6: Skipped npm install' +
151
+ (options.dryRun ? ' (dry run)' : ''));
152
+ }
153
+
154
+ // Step 5: Setup wizard
155
+ if (!options.skipWizard && !options.yes && !options.dryRun) {
156
+ spinner.succeed('Step 5/6: Starting setup wizard...');
157
+ console.log('');
158
+
159
+ await runSetupWizard({
160
+ modules: options.modules?.split(','),
161
+ securityTier: options.securityTier
162
+ });
163
+ } else {
164
+ spinner.succeed('Step 5/6: Skipped setup wizard' +
165
+ (options.dryRun ? ' (dry run)' : ''));
166
+ }
167
+
168
+ // Step 6: Health check
169
+ if (!options.dryRun) {
170
+ spinner.start('Step 6/6: Running health check...');
171
+
172
+ const healthResult = await runHealthCheck(targetDir);
173
+
174
+ if (healthResult.success) {
175
+ spinner.succeed('Step 6/6: Installation verified');
176
+ } else {
177
+ spinner.warn('Step 6/6: Health check had warnings');
178
+ healthResult.warnings.forEach(w => logger.warn(` - ${w}`));
179
+ }
180
+ } else {
181
+ spinner.succeed('Step 6/6: Skipped health check (dry run)');
182
+ }
183
+
184
+ // Cleanup temp files
185
+ await cleanupDownload();
186
+ if (options.fromGit && installState.downloadPath) {
187
+ await cleanupClone(installState.downloadPath);
188
+ }
189
+
190
+ // Remove SIGINT handler
191
+ process.off('SIGINT', cleanupHandler);
192
+
193
+ // Calculate elapsed time
194
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
195
+
196
+ // Show success and quick start
197
+ console.log('\n');
198
+ showQuickStart(options.dryRun);
199
+ logger.success(`\nInstallation completed in ${elapsed}s`);
200
+
201
+ } catch (error) {
202
+ spinner.fail(`Installation failed: ${error.message}`);
203
+ logger.error('\nError details:', error.stack);
204
+
205
+ await rollback();
206
+ process.exit(1);
207
+ }
208
+ }
209
+
210
+ async function rollback() {
211
+ logger.info('\nRolling back changes...');
212
+
213
+ // Cleanup downloaded files
214
+ if (installState.downloadPath) {
215
+ try {
216
+ await cleanupDownload();
217
+ await cleanupClone(installState.downloadPath);
218
+ } catch {
219
+ // Ignore cleanup errors
220
+ }
221
+ }
222
+
223
+ // Note: We don't remove extracted files to be safe
224
+ // User can manually clean up if needed
225
+
226
+ logger.info('Rollback complete. Your original files are preserved.');
227
+
228
+ if (installState.packageJsonBackup) {
229
+ logger.info(`Package.json backup: ${installState.packageJsonBackup}`);
230
+ }
231
+ }
232
+
233
+ async function runSetupWizard(preselect) {
234
+ // Import and run setup wizard from extracted files
235
+ try {
236
+ const { runWizard } = await import(
237
+ join(process.cwd(), 'src/utility/tools/setup-wizard/index.js')
238
+ );
239
+ await runWizard(preselect);
240
+ } catch (error) {
241
+ logger.warn('Setup wizard not available. Run `npm run bmad:setup` later.');
242
+ }
243
+ }
244
+
245
+ async function runHealthCheck(targetDir) {
246
+ const warnings = [];
247
+
248
+ // Check for required directories
249
+ const requiredDirs = ['_bmad', '.claude'];
250
+ for (const dir of requiredDirs) {
251
+ const dirPath = join(targetDir, dir);
252
+ if (!existsSync(dirPath)) {
253
+ warnings.push(`Missing directory: ${dir}`);
254
+ }
255
+ }
256
+
257
+ // Check for CLAUDE.md
258
+ if (!existsSync(join(targetDir, 'CLAUDE.md'))) {
259
+ warnings.push('Missing CLAUDE.md');
260
+ }
261
+
262
+ return {
263
+ success: warnings.length === 0,
264
+ warnings
265
+ };
266
+ }
267
+
268
+ function showQuickStart(isDryRun) {
269
+ if (isDryRun) {
270
+ console.log(chalk.cyan.bold('='.repeat(60)));
271
+ console.log(chalk.cyan.bold(' DRY RUN COMPLETE'));
272
+ console.log(chalk.cyan.bold('='.repeat(60)));
273
+ console.log('');
274
+ console.log('Run without --dry-run to actually install.');
275
+ return;
276
+ }
277
+
278
+ console.log(chalk.green.bold('='.repeat(60)));
279
+ console.log(chalk.green.bold(' BMAD-CYBER INSTALLED SUCCESSFULLY'));
280
+ console.log(chalk.green.bold('='.repeat(60)));
281
+ console.log('');
282
+ console.log(chalk.white.bold('Quick Start:'));
283
+ console.log('');
284
+ console.log(' 1. Open this project in Claude Code:');
285
+ console.log(chalk.cyan(' claude .'));
286
+ console.log('');
287
+ console.log(' 2. Start with the master orchestrator:');
288
+ console.log(chalk.cyan(' /agents/abdul'));
289
+ console.log('');
290
+ console.log(' 3. Or explore available modules:');
291
+ console.log(chalk.cyan(' /help'));
292
+ console.log('');
293
+ console.log(chalk.white.bold('Useful Commands:'));
294
+ console.log('');
295
+ console.log(' npm run bmad:modules - Select active modules');
296
+ console.log(' npm run bmad:security - Configure security tier');
297
+ console.log(' npm run bmad:health - Run system health check');
298
+ console.log('');
299
+ console.log(chalk.dim('Documentation: https://github.com/SchenLong/BMAD-CYBERSEC'));
300
+ console.log('');
301
+ }