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