tops-bmad
Version:
CLI tool to install BMAD workflow files into any project with integrated Shai-Hulud 2.0 security scanning
145 lines (123 loc) • 4.61 kB
JavaScript
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");
/**
* 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);
}
// Get encryption key from environment variable or prompt
let encryptionKey = process.env.BMAD_ENCRYPTION_KEY;
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();