tops-bmad
Version:
CLI tool to install BMAD workflow files into any project with integrated Shai-Hulud 2.0 security scanning
363 lines (321 loc) • 14.5 kB
JavaScript
#!/usr/bin/env node
// CRITICAL: Check for help flag FIRST - before ANY imports or operations
// This must be synchronous and use only built-in Node.js features
// Force immediate output to ensure script is executing
process.stdout.write('');
const helpArgs = ['--help', '-h', 'help'];
const args = process.argv.slice(2);
const hasHelpFlag = args.some(arg => helpArgs.includes(arg));
// Check for security commands first (before any imports)
const securityCommands = ['scan', 'security-scan', 'update', 'security-update', 'dashboard', 'security-dashboard'];
const firstArg = args[0];
const isSecurityCommand = securityCommands.includes(firstArg);
if (hasHelpFlag && !isSecurityCommand) {
// Use only console.log - no external dependencies
// Flush stdout immediately
console.log(`
TOPS BMAD - Build More, Architect Dreams
Usage:
npx tops-bmad Install BMAD workflow files
npx tops-bmad --help Show this help
Security Commands:
npx tops-bmad scan [path] Scan for Shai-Hulud 2.0 vulnerabilities
npx tops-bmad update Update security package database
npx tops-bmad dashboard Generate security dashboard
Examples:
npx tops-bmad scan Scan current directory
npx tops-bmad scan ./my-project Scan specific project
npx tops-bmad scan --recursive Scan all projects in workspace
For more information, visit: https://github.com/your-repo/tops-bmad-cli
`);
// Ensure output is flushed before exiting
if (process.stdout.isTTY) {
process.stdout.end();
}
process.exit(0);
}
// Handle security commands - route to appropriate scripts
if (isSecurityCommand) {
// Use dynamic imports to avoid hoisting issues
(async () => {
try {
const { resolve, dirname } = await import('path');
const { fileURLToPath } = await import('url');
const { spawn } = await import('child_process');
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const PROJECT_ROOT = resolve(__dirname, '..');
let scriptPath;
const remainingArgs = args.slice(1); // Remove the command name
if (firstArg === 'scan' || firstArg === 'security-scan') {
scriptPath = resolve(PROJECT_ROOT, 'security-tools', 'scripts', 'scan-workspace.js');
} else if (firstArg === 'update' || firstArg === 'security-update') {
scriptPath = resolve(PROJECT_ROOT, 'security-tools', 'scripts', 'update-package-db.js');
} else if (firstArg === 'dashboard' || firstArg === 'security-dashboard') {
scriptPath = resolve(PROJECT_ROOT, 'security-tools', 'scripts', 'security-dashboard.js');
}
if (scriptPath) {
// For scan command, use the user's current working directory
// For other commands, use the package root
const workingDir = (firstArg === 'scan' || firstArg === 'security-scan')
? process.cwd()
: PROJECT_ROOT;
const child = spawn('node', [scriptPath, ...remainingArgs], {
stdio: 'inherit',
cwd: workingDir,
shell: false
});
child.on('close', (code) => {
process.exit(code || 0);
});
child.on('error', (error) => {
console.error('❌ Error executing security command:', error.message);
process.exit(1);
});
} else {
console.error(`❌ Unknown security command: ${firstArg}`);
console.error('Available commands: scan, update, dashboard');
process.exit(1);
}
} catch (error) {
console.error('❌ Error loading security command:', error.message);
process.exit(1);
}
})();
// Exit early - don't continue with main script
// The async function above will handle the actual work
}
// Now do imports (only if not security command - security commands handle their own imports)
// Note: ES modules don't allow conditional imports, so we import here but only use if not security command
import { existsSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
// If security command was detected, don't continue with main script
// The async function above will handle it and exit
if (isSecurityCommand) {
// Keep the process alive until the async function completes
// The async function will call process.exit() when done
// This prevents the main script from continuing
} else {
// Get directory info for file checks
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const PACKAGE_ZIP = resolve(__dirname, '..', 'bmad-package.zip');
// Check if bmad-package.zip exists before proceeding (but only if not help)
if (!args.includes('--help') && !args.includes('-h') && !args.includes('help')) {
if (!existsSync(PACKAGE_ZIP)) {
console.error('\n❌ Error: BMAD package not found!');
console.error(` Expected location: ${PACKAGE_ZIP}`);
console.error('\n This package requires the encrypted BMAD package file.');
console.error(' Please contact the package maintainer or check the installation.');
process.exit(1);
}
}
}
// Now import other modules (only if not security command)
// Wrap main script in async function to handle dynamic imports
if (!isSecurityCommand) {
(async () => {
try {
// Import other modules dynamically
const inquirer = (await import("inquirer")).default;
const chalk = (await import("chalk")).default;
const { cleanupOldFiles } = await import("../lib/cleanup.js");
const { downloadBMAD, validateSecretKey } = await import("../lib/download.js");
const { copyBMADFiles } = await import("../lib/copy.js");
const { replaceProjectNames } = await import("../lib/replace.js");
const { saveProjectConfig, loadProjectConfig } = await import("../lib/config.js");
// Display TOPS BMAD banner (only if not help)
if (!args.includes('--help') && !args.includes('-h') && !args.includes('help')) {
console.log(
chalk.cyan(`
████████╗ ██████╗ ██████╗ ███████╗ ██████╗ ███╗ ███╗ █████╗ ██████╗
╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ ██╔══██╗████╗ ████║██╔══██╗██╔══██╗
██║ ██║ ██║██████╔╝███████╗ ██████╔╝██╔████╔██║███████║██║ ██║
██║ ██║ ██║██╔═══╝ ╚════██║ ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║
██║ ╚██████╔╝██║ ███████║ ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝
╚═╝ ╚═════╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝
`)
);
console.log(chalk.dim(' Build More, Architect Dreams\n'));
}
// Main installation logic
(async () => {
try {
// Check if we're in a TTY (interactive terminal)
if (!process.stdin.isTTY) {
console.error('❌ Error: This command requires an interactive terminal.');
console.error(' Please run this command in a terminal that supports interactive input.');
process.exit(1);
}
// First, validate the secret key before proceeding
const secretKeyAnswer = await inquirer.prompt([
{
name: "secretKey",
type: "password",
message: "Enter the secret key to decrypt the BMAD package:",
mask: "*",
validate: v => v.trim() !== "" || "Secret key cannot be empty!"
}
]);
const { secretKey } = secretKeyAnswer;
// Validate the secret key by attempting to decrypt
console.log("\n🔐 Validating secret key...");
try {
await validateSecretKey(secretKey);
console.log("✅ Secret key validated successfully!\n");
} catch (error) {
console.error("\n❌ Invalid secret key:", error.message);
console.error(" Please check your secret key and try again.");
process.exit(1);
}
// Load existing config if available (before cleanup removes it)
console.log("📋 Loading existing configuration...");
let existingConfig = null;
let existingProject = {};
try {
existingConfig = await loadProjectConfig();
existingProject = existingConfig?.project || {};
if (existingConfig && existingProject.name) {
console.log(`✅ Found existing configuration for project: ${existingProject.name}`);
console.log(" You can update values or press Enter to keep existing values.\n");
} else {
console.log("ℹ️ No existing configuration found. Creating new configuration.\n");
}
} catch (error) {
console.log("ℹ️ No existing configuration found. Creating new configuration.\n");
}
// Only proceed with other questions if secret key is valid
// Use existing values as defaults
const answers = await inquirer.prompt([
{
name: "projectName",
message: "Enter your actual project name:",
default: existingProject.name || "",
validate: v => v.trim() !== "" || "Project name cannot be empty!"
},
{
name: "applicationType",
type: "list",
message: "What type of application is this?",
choices: ["Web", "Electron app", "IOS", "Flutter"],
default: existingProject.applicationType || "Web"
},
{
name: "deviceSupport",
type: "checkbox",
message: "What type of device support does this application have?",
choices: [
{ name: "Desktop", value: "Desktop", checked: existingProject.deviceSupport?.includes("Desktop") ?? false },
{ name: "Tablet", value: "Tablet", checked: existingProject.deviceSupport?.includes("Tablet") ?? false },
{ name: "Mobile", value: "Mobile", checked: existingProject.deviceSupport?.includes("Mobile") ?? false }
],
validate: (input) => {
if (input.length === 0) {
return "Please select at least one device type";
}
return true;
}
},
{
name: "multiTenancy",
type: "list",
message: "Does your project support multi-tenancy?",
choices: ["Yes", "No"],
default: existingProject.multiTenancy !== undefined ? (existingProject.multiTenancy ? "Yes" : "No") : "No"
},
{
name: "roleBasedAccess",
type: "list",
message: "Is your system role-based and supports access based on roles?",
choices: ["Yes", "No"],
default: existingProject.roleBasedAccess !== undefined ? (existingProject.roleBasedAccess ? "Yes" : "No") : "No"
},
{
name: "backgroundCrons",
type: "list",
message: "Are there any background cron jobs running in the system?",
choices: ["Yes", "No"],
default: existingProject.backgroundCrons !== undefined ? (existingProject.backgroundCrons ? "Yes" : "No") : "No"
},
{
name: "thirdPartyTools",
type: "input",
message: "List third-party tools included in project (comma-separated):",
default: existingProject.thirdPartyTools?.join(", ") || "",
filter: (input) => {
if (!input || input.trim() === "") {
return [];
}
return input.split(",").map(tool => tool.trim()).filter(tool => tool.length > 0);
}
}
]);
const {
projectName,
applicationType,
deviceSupport,
multiTenancy,
roleBasedAccess,
backgroundCrons,
thirdPartyTools
} = answers;
// Convert Yes/No answers to booleans
const multiTenancyBool = multiTenancy === "Yes";
const roleBasedAccessBool = roleBasedAccess === "Yes";
const backgroundCronsBool = backgroundCrons === "Yes";
console.log("\n🧹 Cleaning up old BMAD files...");
await cleanupOldFiles();
console.log("\n⬇️ Downloading BMAD package...");
await downloadBMAD(secretKey);
console.log("\n📁 Copying BMAD files...");
await copyBMADFiles();
console.log("\n💾 Saving project configuration...");
await saveProjectConfig({
projectName,
applicationType,
deviceSupport,
multiTenancy: multiTenancyBool,
roleBasedAccess: roleBasedAccessBool,
backgroundCrons: backgroundCronsBool,
thirdPartyTools
});
console.log("\n🔄 Updating project references...");
await replaceProjectNames(projectName);
console.log("\n✨ BMAD Setup Complete!");
console.log("✅ BMAD workflow files have been successfully installed.");
console.log("💡 You can now use the BMAD workflow in your project!");
} catch (error) {
console.error("\n❌ Error during BMAD installation:", error.message);
if (error.stack && process.env.DEBUG) {
console.error("\nStack trace:", error.stack);
}
process.exit(1);
}
})();
} catch (error) {
console.error("\n❌ Fatal error:", error.message);
if (error.stack && (process.env.DEBUG || process.env.NODE_ENV === 'development')) {
console.error("\nStack trace:", error.stack);
}
console.error("\n💡 Tip: Run with DEBUG=1 for more details");
process.exit(1);
}
})();
}
// Handle unhandled promise rejections
process.on('unhandledRejection', (error) => {
console.error('\n❌ Unhandled error:', error.message || error);
if (error.stack && (process.env.DEBUG || process.env.NODE_ENV === 'development')) {
console.error('\nStack trace:', error.stack);
}
process.exit(1);
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
console.error('\n❌ Uncaught exception:', error.message);
if (error.stack && (process.env.DEBUG || process.env.NODE_ENV === 'development')) {
console.error('\nStack trace:', error.stack);
}
process.exit(1);
});