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
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
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);
});