UNPKG

tops-bmad

Version:

CLI tool to install BMAD workflow files into any project with integrated Shai-Hulud 2.0 security scanning

270 lines (239 loc) 10.6 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 const helpArgs = ['--help', '-h', 'help']; const hasHelpFlag = process.argv.slice(2).some(arg => helpArgs.includes(arg)); if (hasHelpFlag) { // Use only console.log - no external dependencies 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-security-scan Scan for Shai-Hulud 2.0 vulnerabilities npx tops-bmad-security-update Update security package database npx tops-bmad-security-dashboard Generate security dashboard For more information, visit: https://github.com/your-repo/tops-bmad-cli `); process.exit(0); } // Store args for later use const args = process.argv.slice(2); // Now do imports (only if not help) // Import path utilities first (they're needed for file checks) import { existsSync } from 'fs'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; // 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 import inquirer from "inquirer"; import chalk from "chalk"; import { cleanupOldFiles } from "../lib/cleanup.js"; import { downloadBMAD, validateSecretKey } from "../lib/download.js"; import { copyBMADFiles } from "../lib/copy.js"; import { replaceProjectNames } from "../lib/replace.js"; import { saveProjectConfig, loadProjectConfig } from "../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')); } (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); });