UNPKG

tops-bmad

Version:

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

198 lines (167 loc) 6.16 kB
#!/usr/bin/env node import fs from "fs-extra"; import path from "path"; import { fileURLToPath } from "url"; import AdmZip from "adm-zip"; import inquirer from "inquirer"; import { encryptFile } from "../lib/encrypt.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const PROJECT_ROOT = path.join(__dirname, ".."); const PACKAGE_FOLDER = path.join(PROJECT_ROOT, "bmad-package"); const ZIP_FILE = path.join(PROJECT_ROOT, "bmad-package.zip"); const ENV_FILE = path.join(PROJECT_ROOT, ".env"); /** * Loads environment variables from .env file * @returns {Promise<Object>} Object with environment variables */ async function loadEnvFile() { const envVars = {}; try { if (await fs.pathExists(ENV_FILE)) { const envContent = await fs.readFile(ENV_FILE, "utf8"); const lines = envContent.split("\n"); for (const line of lines) { // Skip comments and empty lines const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) { continue; } // Parse KEY="value" or KEY=value format const match = trimmed.match(/^([^=]+)=(.*)$/); if (match) { const key = match[1].trim(); let value = match[2].trim(); // Remove quotes if present if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } envVars[key] = value; } } } } catch (error) { console.warn(`⚠️ Warning: Could not read .env file: ${error.message}`); } return envVars; } /** * Checks if a file is a valid zip file (not encrypted) * @param {string} filePath - Path to the file to check * @returns {Promise<boolean>} */ async function isValidZip(filePath) { try { const zip = new AdmZip(filePath); // Try to read entries - if it works, it's a valid zip zip.getEntries(); return true; } catch (error) { // If it fails, it's likely encrypted or corrupted return false; } } /** * Creates a zip file from the bmad-package folder * @returns {Promise<void>} */ async function createZipFromFolder() { try { // Check if bmad-package folder exists if (!(await fs.pathExists(PACKAGE_FOLDER))) { console.error(`❌ Error: bmad-package folder not found at: ${PACKAGE_FOLDER}`); console.error(" Please ensure bmad-package folder exists before publishing."); process.exit(1); } console.log("📦 Creating zip file from bmad-package folder..."); // Delete existing zip file if it exists to ensure fresh creation if (await fs.pathExists(ZIP_FILE)) { console.log("🗑️ Deleting existing zip file to create fresh one..."); await fs.remove(ZIP_FILE); } // Create a new zip instance const zip = new AdmZip(); // Add the entire folder to the zip zip.addLocalFolder(PACKAGE_FOLDER, "bmad-package"); // Write the zip file zip.writeZip(ZIP_FILE); console.log(`✅ Zip file created: ${ZIP_FILE}`); } catch (error) { console.error("❌ Error creating zip file:", error.message); throw error; } } /** * Encrypts the package before publishing */ async function ensureEncrypted() { try { // Always create a fresh zip from the bmad-package folder // This ensures we're publishing the latest version of the package await createZipFromFolder(); // Verify the zip file is valid before encryption const isZip = await isValidZip(ZIP_FILE); if (!isZip) { console.error("❌ Error: Created zip file is not valid. Cannot proceed with encryption."); process.exit(1); } // Load .env file first const envVars = await loadEnvFile(); // Get encryption key from (in order of priority): // 1. Environment variable // 2. .env file // 3. Prompt user let encryptionKey = process.env.BMAD_ENCRYPTION_KEY || envVars.BMAD_ENCRYPTION_KEY; if (encryptionKey && envVars.BMAD_ENCRYPTION_KEY && !process.env.BMAD_ENCRYPTION_KEY) { console.log("🔑 Encryption key loaded from .env file"); } if (!encryptionKey) { // If not in env, check if we're in CI/CD (non-interactive) if (process.env.CI || !process.stdout.isTTY) { console.error("❌ Error: BMAD_ENCRYPTION_KEY environment variable is required for non-interactive publishing."); console.error(" Set it with: export BMAD_ENCRYPTION_KEY='your-secret-key'"); process.exit(1); } // Prompt for encryption key const answers = await inquirer.prompt([ { name: "password", type: "password", message: "Enter secret key to encrypt the package:", mask: "*", validate: v => v.trim() !== "" || "Secret key cannot be empty!" }, { name: "confirmPassword", type: "password", message: "Confirm the secret key:", mask: "*", validate: v => v.trim() !== "" || "Secret key cannot be empty!" } ]); if (answers.password !== answers.confirmPassword) { console.error("❌ Error: Secret keys do not match!"); process.exit(1); } encryptionKey = answers.password; } // Now encrypt the file (it should be unencrypted at this point) console.log("🔒 Encrypting package before publishing...\n"); // Create backup of unencrypted version const BACKUP_ZIP = path.join(PROJECT_ROOT, "bmad-package.zip.backup"); if (await fs.pathExists(ZIP_FILE)) { await fs.copy(ZIP_FILE, BACKUP_ZIP); console.log(`📋 Backup created: ${BACKUP_ZIP}`); } // Encrypt the file await encryptFile(ZIP_FILE, ZIP_FILE, encryptionKey); console.log("✅ Package encrypted successfully!"); console.log("⚠️ Remember to share the secret key with authorized users."); } catch (error) { console.error("❌ Error during prepublish:", error.message); process.exit(1); } } // Run the check ensureEncrypted();