UNPKG

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
#!/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); });