const fs = require("fs").promises;
const path = require("path");
const { glob } = require("glob");

// Dynamic imports for ES modules
let chalk, ora, inquirer;

// Initialize ES modules
async function initializeModules() {
  chalk = (await import("chalk")).default;
  ora = (await import("ora")).default;
  inquirer = (await import("inquirer")).default;
}

class V3ToV4Upgrader {
  constructor() {
  }
  async upgrade(options = {}) {
    try {
      await initializeModules();
      process.stdin.resume();
      console.log(
        chalk.bold("\nWelcome to BMad-Method V3 to V4 Upgrade Tool\n")
      );
      console.log(
        "This tool will help you upgrade your BMad-Method V3 project to V4.\n"
      );
      console.log(chalk.cyan("What this tool does:"));
      console.log("- Creates a backup of your V3 files (.bmad-v3-backup/)");
      console.log("- Installs the new V4 .bmad-core structure");
      console.log(
        "- Preserves your PRD, Architecture, and Stories in the new format\n"
      );
      console.log(chalk.yellow("What this tool does NOT do:"));
      console.log(
        "- Modify your document content (use doc-migration-task after upgrade)"
      );
      console.log("- Touch any files outside bmad-agent/ and docs/\n");
      const projectPath = await this.getProjectPath(options.projectPath);
      const validation = await this.validateV3Project(projectPath);
      if (!validation.isValid) {
        console.error(
          chalk.red("\nError: This doesn't appear to be a V3 project.")
        );
        console.error("Expected to find:");
        console.error("- bmad-agent/ directory");
        console.error("- docs/ directory\n");
        console.error(
          "Please check you're in the correct directory and try again."
        );
        return;
      }
      const analysis = await this.analyzeProject(projectPath);
      await this.showPreflightCheck(analysis, options);
      if (!options.dryRun) {
        const { confirm } = await inquirer.prompt([
          {
            type: "confirm",
            name: "confirm",
            message: "Continue with upgrade?",
            default: true,
          },
        ]);
        if (!confirm) {
          console.log("Upgrade cancelled.");
          return;
        }
      }
      if (options.backup !== false && !options.dryRun) {
        await this.createBackup(projectPath);
      }
      if (!options.dryRun) {
        await this.installV4Structure(projectPath);
      }
      if (!options.dryRun) {
        await this.migrateDocuments(projectPath, analysis);
      }
      if (!options.dryRun) {
        await this.setupIDE(projectPath, options.ides);
      }
      this.showCompletionReport(projectPath, analysis);
      process.exit(0);
    } catch (error) {
      console.error(chalk.red("\nUpgrade error:"), error.message);
      process.exit(1);
    }
  }
  async getProjectPath(providedPath) {
    if (providedPath) {
      return path.resolve(providedPath);
    }
    const { projectPath } = await inquirer.prompt([
      {
        type: "input",
        name: "projectPath",
        message: "Please enter the path to your V3 project:",
        default: process.cwd(),
      },
    ]);
    return path.resolve(projectPath);
  }
  async validateV3Project(projectPath) {
    const spinner = ora("Validating project structure...").start();
    try {
      const bmadAgentPath = path.join(projectPath, "bmad-agent");
      const docsPath = path.join(projectPath, "docs");
      const hasBmadAgent = await this.pathExists(bmadAgentPath);
      const hasDocs = await this.pathExists(docsPath);
      if (hasBmadAgent) {
        spinner.text = "✓ Found bmad-agent/ directory";
        console.log(chalk.green("\n✓ Found bmad-agent/ directory"));
      }
      if (hasDocs) {
        console.log(chalk.green("✓ Found docs/ directory"));
      }
      const isValid = hasBmadAgent && hasDocs;
      if (isValid) {
        spinner.succeed("This appears to be a valid V3 project");
      } else {
        spinner.fail("Invalid V3 project structure");
      }
      return { isValid, hasBmadAgent, hasDocs };
    } catch (error) {
      spinner.fail("Validation failed");
      throw error;
    }
  }
  async analyzeProject(projectPath) {
    const docsPath = path.join(projectPath, "docs");
    const bmadAgentPath = path.join(projectPath, "bmad-agent");
    const prdCandidates = ["prd.md", "PRD.md", "product-requirements.md"];
    let prdFile = null;
    for (const candidate of prdCandidates) {
      const candidatePath = path.join(docsPath, candidate);
      if (await this.pathExists(candidatePath)) {
        prdFile = candidate;
        break;
      }
    }
    const archCandidates = [
      "architecture.md",
      "Architecture.md",
      "technical-architecture.md",
    ];
    let archFile = null;
    for (const candidate of archCandidates) {
      const candidatePath = path.join(docsPath, candidate);
      if (await this.pathExists(candidatePath)) {
        archFile = candidate;
        break;
      }
    }
    const frontEndCandidates = [
      "front-end-architecture.md",
      "frontend-architecture.md",
      "ui-architecture.md",
    ];
    let frontEndArchFile = null;
    for (const candidate of frontEndCandidates) {
      const candidatePath = path.join(docsPath, candidate);
      if (await this.pathExists(candidatePath)) {
        frontEndArchFile = candidate;
        break;
      }
    }
    const uxSpecCandidates = [
      "ux-ui-spec.md",
      "ux-ui-specification.md",
      "ui-spec.md",
      "ux-spec.md",
    ];
    let uxSpecFile = null;
    for (const candidate of uxSpecCandidates) {
      const candidatePath = path.join(docsPath, candidate);
      if (await this.pathExists(candidatePath)) {
        uxSpecFile = candidate;
        break;
      }
    }
    const uxPromptCandidates = [
      "v0-prompt.md",
      "ux-prompt.md",
      "ui-prompt.md",
      "design-prompt.md",
    ];
    let uxPromptFile = null;
    for (const candidate of uxPromptCandidates) {
      const candidatePath = path.join(docsPath, candidate);
      if (await this.pathExists(candidatePath)) {
        uxPromptFile = candidate;
        break;
      }
    }
    const epicFiles = await glob("epic*.md", { cwd: docsPath });
    const storiesPath = path.join(docsPath, "stories");
    let storyFiles = [];
    if (await this.pathExists(storiesPath)) {
      storyFiles = await glob("*.md", { cwd: storiesPath });
    }
    const bmadAgentFiles = await glob("**, "")
        .trim();
      const displayName = epicName.charAt(0).toUpperCase() + epicName.slice(1);
      indexContent += `- [${
        displayName || epicFile.replace(".md", "")
      }](./${epicFile})\n`;
    }
    await fs.writeFile(prdIndexPath, indexContent);
  }
  async createInstallManifest(projectPath) {
    const fileManager = require("../installer/lib/file-manager");
    const { glob } = require("glob");
    const bmadCorePath = path.join(projectPath, ".bmad-core");
    const files = await glob("**.gitnode_modules/**"],
    });
    const manifestFiles = files.map((file) => path.join(".bmad-core", file));
    const config = {
      installType: "full",
      agent: null,
      ide: null,
    };
    await fileManager.createManifest(projectPath, config, manifestFiles);
  }
}

const path = require("node:path");
const fs = require("fs-extra");
const chalk = require("chalk");
const ora = require("ora");
const inquirer = require("inquirer");
const fileManager = require("./file-manager");
const configLoader = require("./config-loader");
const ideSetup = require("./ide-setup");
const { extractYamlFromAgent } = require("../../lib/yaml-utils");
const resourceLocator = require("./resource-locator");

class Installer {
  async getCoreVersion() {
    try {
      // Always use package.json version
      const packagePath = path.join(__dirname, '..', '..', '..', 'package.json');
      const packageJson = require(packagePath);
      return packageJson.version;
    } catch (error) {
      console.warn("Could not read version from package.json, using 'unknown'");
      return "unknown";
    }
  }

  async install(config) {
    const spinner = ora("Analyzing installation directory...").start();

    try {
      // Store the original CWD where npx was executed
      const originalCwd = process.env.INIT_CWD || process.env.PWD || process.cwd();

      // Resolve installation directory relative to where the user ran the command
      let installDir = path.isAbsolute(config.directory)
        ? config.directory
        : path.resolve(originalCwd, config.directory);

      if (path.basename(installDir) === '.bmad-core') {
        // If user points directly to .bmad-core, treat its parent as the project root
        installDir = path.dirname(installDir);
      }

      // Log resolved path for clarity
      if (!path.isAbsolute(config.directory)) {
        spinner.text = `Resolving "${config.directory}" to: ${installDir}`;
      }

      // Check if directory exists and handle non-existent directories
      if (!(await fileManager.pathExists(installDir))) {
        spinner.stop();
        console.log(`\nThe directory ${installDir} does not exist.`);

        const { action } = await inquirer.prompt([
          {
            type: 'list',
            name: 'action',
            message: 'What would you like to do?',
            choices: [
              {
                name: 'Create the directory and continue',
                value: 'create'
              },
              {
                name: 'Choose a different directory',
                value: 'change'
              },
              {
                name: 'Cancel installation',
                value: 'cancel'
              }
            ]
          }
        ]);

        if (action === 'cancel') {
            console.log('Installation cancelled.');
          process.exit(0);
        } else if (action === 'change') {
          const { newDirectory } = await inquirer.prompt([
            {
              type: 'input',
              name: 'newDirectory',
              message: 'Enter the new directory path:',
              validate: (input) => {
                if (!input.trim()) {
                  return 'Please enter a valid directory path';
                }
                return true;
              }
            }
          ]);
          // Preserve the original CWD for the recursive call
          config.directory = newDirectory;
          return await this.install(config); // Recursive call with new directory
        } else if (action === 'create') {
          try {
            await fileManager.ensureDirectory(installDir);
            console.log(`✓ Created directory: ${installDir}`);
          } catch (error) {
            console.error(`Failed to create directory: ${error.message}`);
            console.error('You may need to check permissions or use a different path.');
            process.exit(1);
          }
        }

        spinner.start("Analyzing installation directory...");
      }

      // If this is an update request from early detection, handle it directly
      if (config.installType === 'update') {
        const state = await this.detectInstallationState(installDir);
        if (state.type === 'v4_existing') {
          return await this.performUpdate(config, installDir, state.manifest, spinner);
        } else {
          spinner.fail('No existing v4 installation found to update');
          throw new Error('No existing v4 installation found');
        }
      }

      // Detect current state
      const state = await this.detectInstallationState(installDir);

      // Handle different states
      switch (state.type) {
        case "clean":
          return await this.performFreshInstall(config, installDir, spinner);

        case "v4_existing":
          return await this.handleExistingV4Installation(
            config,
            installDir,
            state,
            spinner
          );

        case "v3_existing":
          return await this.handleV3Installation(
            config,
            installDir,
            state,
            spinner
          );

        case "unknown_existing":
          return await this.handleUnknownInstallation(
            config,
            installDir,
            state,
            spinner
          );
      }
    } catch (error) {
      // Check if modules were initialized
      if (spinner) {
        spinner.fail("Installation failed");
      } else {
        console.error("Installation failed:", error.message);
      }
      throw error;
    }
  }

  async detectInstallationState(installDir) {
    const state = {
      type: "clean",
      hasV4Manifest: false,
      hasV3Structure: false,
      hasBmadCore: false,
      hasOtherFiles: false,
      manifest: null,
      expansionPacks: {},
    };

    // Check if directory exists
    if (!(await fileManager.pathExists(installDir))) {
      return state; // clean install
    }

    // Check for V4 installation (has .bmad-core with manifest)
    const bmadCorePath = path.join(installDir, ".bmad-core");
    const manifestPath = path.join(bmadCorePath, "install-manifest.yaml");

    if (await fileManager.pathExists(manifestPath)) {
      state.type = "v4_existing";
      state.hasV4Manifest = true;
      state.hasBmadCore = true;
      state.manifest = await fileManager.readManifest(installDir);
      return state;
    }

    // Check for V3 installation (has bmad-agent directory)
    const bmadAgentPath = path.join(installDir, "bmad-agent");
    if (await fileManager.pathExists(bmadAgentPath)) {
      state.type = "v3_existing";
      state.hasV3Structure = true;
      return state;
    }

    // Check for .bmad-core without manifest (broken V4 or manual copy)
    if (await fileManager.pathExists(bmadCorePath)) {
      state.type = "unknown_existing";
      state.hasBmadCore = true;
      return state;
    }

    // Check if directory has other files
    const files = await resourceLocator.findFiles("**/*", {
      cwd: installDir,
      nodir: true,
      ignore: ["**/.git/**", "**/node_modules/**"],
    });

    if (files.length > 0) {
      // Directory has other files, but no BMad installation.
      // Treat as clean install but record that it isn't empty.
      state.hasOtherFiles = true;
    }

    // Check for expansion packs (folders starting with .)
    const expansionPacks = await this.detectExpansionPacks(installDir);
    state.expansionPacks = expansionPacks;

    return state; // clean install
  }

  async performFreshInstall(config, installDir, spinner, options = {}) {
    spinner.text = "Installing BMad Method...";

    let files = [];

    if (config.installType === "full") {
      // Full installation - copy entire .bmad-core folder as a subdirectory
      spinner.text = "Copying complete .bmad-core folder...";
      const sourceDir = resourceLocator.getBmadCorePath();
      const bmadCoreDestDir = path.join(installDir, ".bmad-core");
      await fileManager.copyDirectoryWithRootReplacement(sourceDir, bmadCoreDestDir, ".bmad-core");

      // Copy common/ items to .bmad-core
      spinner.text = "Copying common utilities...";
      await this.copyCommonItems(installDir, ".bmad-core", spinner);

      // Get list of all files for manifest
      const foundFiles = await resourceLocator.findFiles("**/*", {
        cwd: bmadCoreDestDir,
        nodir: true,
        ignore: ["**/.git/**", "**/node_modules/**"],
      });
      files = foundFiles.map((file) => path.join(".bmad-core", file));
    } else if (config.installType === "single-agent") {
      // Single agent installation
      spinner.text = `Installing ${config.agent} agent...`;

      // Copy agent file with {root} replacement
      const agentPath = configLoader.getAgentPath(config.agent);
      const destAgentPath = path.join(
        installDir,
        ".bmad-core",
        "agents",
        `${config.agent}.md`
      );
      await fileManager.copyFileWithRootReplacement(agentPath, destAgentPath, ".bmad-core");
      files.push(`.bmad-core/agents/${config.agent}.md`);

      // Copy dependencies
      const { all: dependencies } = await resourceLocator.getAgentDependencies(
        config.agent
      );
      const sourceBase = resourceLocator.getBmadCorePath();

      for (const dep of dependencies) {
        spinner.text = `Copying dependency: ${dep}`;

        if (dep.includes("*")) {
          // Handle glob patterns with {root} replacement
          const copiedFiles = await fileManager.copyGlobPattern(
            dep.replace(".bmad-core/", ""),
            sourceBase,
            path.join(installDir, ".bmad-core"),
            ".bmad-core"
          );
          files.push(...copiedFiles.map(f => `.bmad-core/${f}`));
        } else {
          // Handle single files with {root} replacement if needed
          const sourcePath = path.join(
            sourceBase,
            dep.replace(".bmad-core/", "")
          );
          const destPath = path.join(
            installDir,
            dep
          );

          const needsRootReplacement = dep.endsWith('.md') || dep.endsWith('.yaml') || dep.endsWith('.yml');
          let success = false;

          if (needsRootReplacement) {
            success = await fileManager.copyFileWithRootReplacement(sourcePath, destPath, ".bmad-core");
          } else {
            success = await fileManager.copyFile(sourcePath, destPath);
          }

          if (success) {
            files.push(dep);
          }
        }
      }

      // Copy common/ items to .bmad-core
      spinner.text = "Copying common utilities...";
      const commonFiles = await this.copyCommonItems(installDir, ".bmad-core", spinner);
      files.push(...commonFiles);
    } else if (config.installType === "team") {
      // Team installation
      spinner.text = `Installing ${config.team} team...`;

      // Get team dependencies
      const teamDependencies = await configLoader.getTeamDependencies(config.team);
      const sourceBase = resourceLocator.getBmadCorePath();

      // Install all team dependencies
      for (const dep of teamDependencies) {
        spinner.text = `Copying team dependency: ${dep}`;

        if (dep.includes("*")) {
          // Handle glob patterns with {root} replacement
          const copiedFiles = await fileManager.copyGlobPattern(
            dep.replace(".bmad-core/", ""),
            sourceBase,
            path.join(installDir, ".bmad-core"),
            ".bmad-core"
          );
          files.push(...copiedFiles.map(f => `.bmad-core/${f}`));
        } else {
          // Handle single files with {root} replacement if needed
          const sourcePath = path.join(sourceBase, dep.replace(".bmad-core/", ""));
          const destPath = path.join(installDir, dep);

          const needsRootReplacement = dep.endsWith('.md') || dep.endsWith('.yaml') || dep.endsWith('.yml');
          let success = false;

          if (needsRootReplacement) {
            success = await fileManager.copyFileWithRootReplacement(sourcePath, destPath, ".bmad-core");
          } else {
            success = await fileManager.copyFile(sourcePath, destPath);
          }

          if (success) {
            files.push(dep);
          }
        }
      }

      // Copy common/ items to .bmad-core
      spinner.text = "Copying common utilities...";
      const commonFiles = await this.copyCommonItems(installDir, ".bmad-core", spinner);
      files.push(...commonFiles);
    } else if (config.installType === "expansion-only") {
      // Expansion-only installation - DO NOT create .bmad-core
      // Only install expansion packs
      spinner.text = "Installing expansion packs only...";
    }

    // Install expansion packs if requested
    const expansionFiles = await this.installExpansionPacks(installDir, config.expansionPacks, spinner, config);
    files.push(...expansionFiles);

    // Install web bundles if requested
    if (config.includeWebBundles && config.webBundlesDirectory) {
      spinner.text = "Installing web bundles...";
      // Resolve web bundles directory using the same logic as the main installation directory
      const originalCwd = process.env.INIT_CWD || process.env.PWD || process.cwd();
      let resolvedWebBundlesDir = path.isAbsolute(config.webBundlesDirectory)
        ? config.webBundlesDirectory
        : path.resolve(originalCwd, config.webBundlesDirectory);
      await this.installWebBundles(resolvedWebBundlesDir, config, spinner);
    }

    // Set up IDE integration if requested
    const ides = config.ides || (config.ide ? [config.ide] : []);
    if (ides.length > 0) {
      for (const ide of ides) {
        spinner.text = `Setting up ${ide} integration...`;
        const preConfiguredSettings = ide === 'github-copilot' ? config.githubCopilotConfig : null;
        await ideSetup.setup(ide, installDir, config.agent, spinner, preConfiguredSettings);
      }
    }

    // Modify core-config.yaml if sharding preferences were provided
    if (config.installType !== "expansion-only" && (config.prdSharded !== undefined || config.architectureSharded !== undefined)) {
      spinner.text = "Configuring document sharding settings...";
      await fileManager.modifyCoreConfig(installDir, config);
    }

    // Create manifest (skip for expansion-only installations)
    if (config.installType !== "expansion-only") {
      spinner.text = "Creating installation manifest...";
      await fileManager.createManifest(installDir, config, files);
    }

    spinner.succeed("Installation complete!");
    this.showSuccessMessage(config, installDir, options);
  }

  async handleExistingV4Installation(config, installDir, state, spinner) {
    spinner.stop();

    const currentVersion = state.manifest.version;
    const newVersion = await this.getCoreVersion();
    const versionCompare = this.compareVersions(currentVersion, newVersion);

    console.log(chalk.yellow("\n🔍 Found existing BMad v4 installation"));
    console.log(`   Directory: ${installDir}`);
    console.log(`   Current version: ${currentVersion}`);
    console.log(`   Available version: ${newVersion}`);
    console.log(
      `   Installed: ${new Date(
        state.manifest.installed_at
      ).toLocaleDateString()}`
    );

    // Check file integrity
    spinner.start("Checking installation integrity...");
    const integrity = await fileManager.checkFileIntegrity(installDir, state.manifest);
    spinner.stop();

    const hasMissingFiles = integrity.missing.length > 0;
    const hasModifiedFiles = integrity.modified.length > 0;
    const hasIntegrityIssues = hasMissingFiles || hasModifiedFiles;

    if (hasIntegrityIssues) {
        console.log(chalk.red("\n⚠  Installation issues detected:"));
      if (hasMissingFiles) {
        console.log(chalk.red(`   Missing files: ${integrity.missing.length}`));
        if (integrity.missing.length <= 5) {
          integrity.missing.forEach(file => console.log(chalk.dim(`     - ${file}`)));
        }
      }
      if (hasModifiedFiles) {
        console.log(chalk.yellow(`   Modified files: ${integrity.modified.length}`));
        if (integrity.modified.length <= 5) {
          integrity.modified.forEach(file => console.log(chalk.dim(`     - ${file}`)));
        }
      }
    }

    // Show existing expansion packs
    if (Object.keys(state.expansionPacks).length > 0) {
      console.log(chalk.cyan("\n📦 Installed expansion packs:"));
      for (const [packId, packInfo] of Object.entries(state.expansionPacks)) {
        if (packInfo.hasManifest && packInfo.manifest) {
          console.log(`   - ${packId} (v${packInfo.manifest.version || 'unknown'})`);
        } else {
          console.log(`   - ${packId} (no manifest)`);
        }
      }
    }

    let choices = [];

    if (versionCompare < 0) {
        console.log(chalk.cyan("\n⬆  Upgrade available for BMad core"));
      choices.push({ name: `Upgrade BMad core (v${currentVersion} → v${newVersion})`, value: "upgrade" });
    } else if (versionCompare === 0) {
      if (hasIntegrityIssues) {
        // Offer repair option when files are missing or modified
        choices.push({
          name: "Repair installation (restore missing/modified files)",
          value: "repair"
        });
      }
        console.log(chalk.yellow("\n⚠  Same version already installed"));
      choices.push({ name: `Force reinstall BMad core (v${currentVersion} - reinstall)`, value: "reinstall" });
    } else {
        console.log(chalk.yellow("\n⬇  Installed version is newer than available"));
      choices.push({ name: `Downgrade BMad core (v${currentVersion} → v${newVersion})`, value: "reinstall" });
    }

    choices.push(
      { name: "Add/update expansion packs only", value: "expansions" },
      { name: "Cancel", value: "cancel" }
    );

    const { action } = await inquirer.prompt([
      {
        type: "list",
        name: "action",
        message: "What would you like to do?",
        choices: choices,
      },
    ]);

    switch (action) {
      case "upgrade":
        return await this.performUpdate(config, installDir, state.manifest, spinner);
      case "repair":
        // For repair, restore missing/modified files while backing up modified ones
        return await this.performRepair(config, installDir, state.manifest, integrity, spinner);
      case "reinstall":
        // For reinstall, don't check for modifications - just overwrite
        return await this.performReinstall(config, installDir, spinner);
      case "expansions":
        // Ask which expansion packs to install
        const availableExpansionPacks = await resourceLocator.getExpansionPacks();

        if (availableExpansionPacks.length === 0) {
          console.log(chalk.yellow("No expansion packs available."));
          return;
        }

        const { selectedPacks } = await inquirer.prompt([
          {
            type: 'checkbox',
            name: 'selectedPacks',
            message: 'Select expansion packs to install/update:',
            choices: availableExpansionPacks.map(pack => ({
              name: `${pack.name} (v${pack.version}) .${pack.id}`,
              value: pack.id,
              checked: state.expansionPacks[pack.id] !== undefined
            }))
          }
        ]);

        if (selectedPacks.length === 0) {
          console.log(chalk.yellow("No expansion packs selected."));
          return;
        }

        spinner.start("Installing expansion packs...");
        const expansionFiles = await this.installExpansionPacks(installDir, selectedPacks, spinner, { ides: config.ides || [] });
        spinner.succeed("Expansion packs installed successfully!");

        console.log(chalk.green("\n✓ Installation complete!"));
        console.log(chalk.green(`✓ Expansion packs installed/updated:`));
        for (const packId of selectedPacks) {
          console.log(chalk.green(`  - ${packId} → .${packId}/`));
        }
        return;
      case "cancel":
        console.log("Installation cancelled.");
        return;
    }
  }

  async handleV3Installation(config, installDir, state, spinner) {
    spinner.stop();

    console.log(
      chalk.yellow("\n🔍 Found BMad v3 installation (bmad-agent/ directory)")
    );
    console.log(`   Directory: ${installDir}`);

    const { action } = await inquirer.prompt([
      {
        type: "list",
        name: "action",
        message: "What would you like to do?",
        choices: [
          { name: "Upgrade from v3 to v4 (recommended)", value: "upgrade" },
          { name: "Install v4 alongside v3", value: "alongside" },
          { name: "Cancel", value: "cancel" },
        ],
      },
    ]);

    switch (action) {
      case "upgrade": {
        console.log(chalk.cyan("\n📦 Starting v3 to v4 upgrade process..."));
        const V3ToV4Upgrader = require("../../upgraders/v3-to-v4-upgrader");
        const upgrader = new V3ToV4Upgrader();
        return await upgrader.upgrade({
          projectPath: installDir,
          ides: config.ides || [] // Pass IDE selections from initial config
        });
      }
      case "alongside":
        return await this.performFreshInstall(config, installDir, spinner);
      case "cancel":
        console.log("Installation cancelled.");
        return;
    }
  }

  async handleUnknownInstallation(config, installDir, state, spinner) {
    spinner.stop();

    console.log(chalk.yellow("\n⚠  Directory contains existing files"));
    console.log(`   Directory: ${installDir}`);

    if (state.hasBmadCore) {
      console.log("   Found: .bmad-core directory (but no manifest)");
    }
    if (state.hasOtherFiles) {
      console.log("   Found: Other files in directory");
    }

    const { action } = await inquirer.prompt([
      {
        type: "list",
        name: "action",
        message: "What would you like to do?",
        choices: [
          { name: "Install anyway (may overwrite files)", value: "force" },
          { name: "Choose different directory", value: "different" },
          { name: "Cancel", value: "cancel" },
        ],
      },
    ]);

    switch (action) {
      case "force":
        return await this.performFreshInstall(config, installDir, spinner);
      case "different": {
        const { newDir } = await inquirer.prompt([
          {
            type: "input",
            name: "newDir",
            message: "Enter new installation directory:",
            default: path.join(path.dirname(installDir), "bmad-project"),
          },
        ]);
        config.directory = newDir;
        return await this.install(config);
      }
      case "cancel":
        console.log("Installation cancelled.");
        return;
    }
  }

  async performUpdate(newConfig, installDir, manifest, spinner) {
    spinner.start("Checking for updates...");

    try {
      // Get current and new versions
      const currentVersion = manifest.version;
      const newVersion = await this.getCoreVersion();
      const versionCompare = this.compareVersions(currentVersion, newVersion);

      // Only check for modified files if it's an actual version upgrade
      let modifiedFiles = [];
      if (versionCompare !== 0) {
        spinner.text = "Checking for modified files...";
        modifiedFiles = await fileManager.checkModifiedFiles(
          installDir,
          manifest
        );
      }

      if (modifiedFiles.length > 0) {
        spinner.warn("Found modified files");
        console.log(chalk.yellow("\nThe following files have been modified:"));
        for (const file of modifiedFiles) {
          console.log(`  - ${file}`);
        }

        const { action } = await inquirer.prompt([
          {
            type: "list",
            name: "action",
            message: "How would you like to proceed?",
            choices: [
              { name: "Backup and overwrite modified files", value: "backup" },
              { name: "Skip modified files", value: "skip" },
              { name: "Cancel update", value: "cancel" },
            ],
          },
        ]);

        if (action === "cancel") {
          console.log("Update cancelled.");
          return;
        }

        if (action === "backup") {
          spinner.start("Backing up modified files...");
          for (const file of modifiedFiles) {
            const filePath = path.join(installDir, file);
            const backupPath = await fileManager.backupFile(filePath);
            console.log(
              chalk.dim(`  Backed up: ${file} → ${path.basename(backupPath)}`)
            );
          }
        }
      }

      // Perform update by re-running installation
      spinner.text = versionCompare === 0 ? "Reinstalling files..." : "Updating files...";
      const config = {
        installType: manifest.install_type,
        agent: manifest.agent,
        directory: installDir,
        ides: newConfig?.ides || manifest.ides_setup || [],
      };

      await this.performFreshInstall(config, installDir, spinner, { isUpdate: true });

      // Clean up .yml files that now have .yaml counterparts
      spinner.text = "Cleaning up legacy .yml files...";
      await this.cleanupLegacyYmlFiles(installDir, spinner);
    } catch (error) {
      spinner.fail("Update failed");
      throw error;
    }
  }

  async performRepair(config, installDir, manifest, integrity, spinner) {
    spinner.start("Preparing to repair installation...");

    try {
      // Back up modified files
      if (integrity.modified.length > 0) {
        spinner.text = "Backing up modified files...";
        for (const file of integrity.modified) {
          const filePath = path.join(installDir, file);
          if (await fileManager.pathExists(filePath)) {
            const backupPath = await fileManager.backupFile(filePath);
            console.log(chalk.dim(`  Backed up: ${file} → ${path.basename(backupPath)}`));
          }
        }
      }

      // Restore missing and modified files
      spinner.text = "Restoring files...";
      const sourceBase = resourceLocator.getBmadCorePath();
      const filesToRestore = [...integrity.missing, ...integrity.modified];

      for (const file of filesToRestore) {
        // Skip the manifest file itself
        if (file.endsWith('install-manifest.yaml')) continue;

        const relativePath = file.replace('.bmad-core/', '');
        const destPath = path.join(installDir, file);

        // Check if this is a common/ file that needs special processing
        const commonBase = path.dirname(path.dirname(path.dirname(path.dirname(__filename))));
        const commonSourcePath = path.join(commonBase, 'common', relativePath);

        if (await fileManager.pathExists(commonSourcePath)) {
          // This is a common/ file - needs template processing
          const fs = require('fs').promises;
          const content = await fs.readFile(commonSourcePath, 'utf8');
          const updatedContent = content.replace(/\{root\}/g, '.bmad-core');
          await fileManager.ensureDirectory(path.dirname(destPath));
          await fs.writeFile(destPath, updatedContent, 'utf8');
          spinner.text = `Restored: ${file}`;
        } else {
          // Regular file from bmad-core
          const sourcePath = path.join(sourceBase, relativePath);
          if (await fileManager.pathExists(sourcePath)) {
            await fileManager.copyFile(sourcePath, destPath);
            spinner.text = `Restored: ${file}`;

            // If this is a .yaml file, check for and remove corresponding .yml file
            if (file.endsWith('.yaml')) {
              const ymlFile = file.replace(/\.yaml$/, '.yml');
              const ymlPath = path.join(installDir, ymlFile);
              if (await fileManager.pathExists(ymlPath)) {
                const fs = require('fs').promises;
                await fs.unlink(ymlPath);
                console.log(chalk.dim(`  Removed legacy: ${ymlFile} (replaced by ${file})`));
              }
            }
          } else {
            console.warn(chalk.yellow(`  Warning: Source file not found: ${file}`));
          }
        }
      }

      // Clean up .yml files that now have .yaml counterparts
      spinner.text = "Cleaning up legacy .yml files...";
      await this.cleanupLegacyYmlFiles(installDir, spinner);

      spinner.succeed("Repair completed successfully!");

      // Show summary
      console.log(chalk.green("\n✓ Installation repaired!"));
      if (integrity.missing.length > 0) {
        console.log(chalk.green(`  Restored ${integrity.missing.length} missing files`));
      }
      if (integrity.modified.length > 0) {
        console.log(chalk.green(`  Restored ${integrity.modified.length} modified files (backups created)`));
      }

      // Warning for Cursor custom modes if agents were repaired
      const ides = manifest.ides_setup || [];
      if (ides.includes('cursor')) {
        console.log(chalk.yellow.bold("\n⚠  IMPORTANT: Cursor Custom Modes Update Required"));
        console.log(chalk.yellow("Since agent files have been repaired, you need to update any custom agent modes configured in the Cursor custom agent GUI per the Cursor docs."));
      }

    } catch (error) {
      spinner.fail("Repair failed");
      throw error;
    }
  }

  async performReinstall(config, installDir, spinner) {
    spinner.start("Preparing to reinstall BMad Method...");

    // Remove existing .bmad-core
    const bmadCorePath = path.join(installDir, ".bmad-core");
    if (await fileManager.pathExists(bmadCorePath)) {
      spinner.text = "Removing existing installation...";
      await fileManager.removeDirectory(bmadCorePath);
    }

    spinner.text = "Installing fresh copy...";
    const result = await this.performFreshInstall(config, installDir, spinner, { isUpdate: true });

    // Clean up .yml files that now have .yaml counterparts
    spinner.text = "Cleaning up legacy .yml files...";
    await this.cleanupLegacyYmlFiles(installDir, spinner);

    return result;
  }

  showSuccessMessage(config, installDir, options = {}) {
    console.log(chalk.green("\n✓ BMad Method installed successfully!\n"));

    const ides = config.ides || (config.ide ? [config.ide] : []);
    if (ides.length > 0) {
      for (const ide of ides) {
        const ideConfig = configLoader.getIdeConfiguration(ide);
        if (ideConfig?.instructions) {
          console.log(
            chalk.bold(`To use BMad agents in ${ideConfig.name}:`)
          );
          console.log(ideConfig.instructions);
        }
      }
    } else {
      console.log(chalk.yellow("No IDE configuration was set up."));
      console.log(
        "You can manually configure your IDE using the agent files in:",
        installDir
      );
    }

    // Information about installation components
    console.log(chalk.bold("\n🎯 Installation Summary:"));
    if (config.installType !== "expansion-only") {
      console.log(chalk.green("✓ .bmad-core framework installed with all agents and workflows"));
    }

    if (config.expansionPacks && config.expansionPacks.length > 0) {
      console.log(chalk.green(`✓ Expansion packs installed:`));
      for (const packId of config.expansionPacks) {
        console.log(chalk.green(`  - ${packId} → .${packId}/`));
      }
    }

    if (config.includeWebBundles && config.webBundlesDirectory) {
      const bundleInfo = this.getWebBundleInfo(config);
      // Resolve the web bundles directory for display
      const originalCwd = process.env.INIT_CWD || process.env.PWD || process.cwd();
      const resolvedWebBundlesDir = path.isAbsolute(config.webBundlesDirectory)
        ? config.webBundlesDirectory
        : path.resolve(originalCwd, config.webBundlesDirectory);
      console.log(chalk.green(`✓ Web bundles (${bundleInfo}) installed to: ${resolvedWebBundlesDir}`));
    }

    if (ides.length > 0) {
      const ideNames = ides.map(ide => {
        const ideConfig = configLoader.getIdeConfiguration(ide);
        return ideConfig?.name || ide;
      }).join(", ");
      console.log(chalk.green(`✓ IDE rules and configurations set up for: ${ideNames}`));
    }

    // Information about web bundles
    if (!config.includeWebBundles) {
      console.log(chalk.bold("\n📦 Web Bundles Available:"));
      console.log("Pre-built web bundles are available and can be added later:");
      console.log(chalk.cyan("  Run the installer again to add them to your project"));
      console.log("These bundles work independently and can be shared, moved, or used");
      console.log("in other projects as standalone files.");
    }

    if (config.installType === "single-agent") {
      console.log(
        chalk.dim(
          "\nNeed other agents? Run: npx bmad-method install --agent=<name>"
        )
      );
      console.log(
        chalk.dim("Need everything? Run: npx bmad-method install --full")
      );
    }

    // Warning for Cursor custom modes if agents were updated
    if (options.isUpdate && ides.includes('cursor')) {
      console.log(chalk.yellow.bold("\n⚠  IMPORTANT: Cursor Custom Modes Update Required"));
      console.log(chalk.yellow("Since agents have been updated, you need to update any custom agent modes configured in the Cursor custom agent GUI per the Cursor docs."));
    }

    // Important notice to read the user guide
    console.log(chalk.red.bold("\n📖 IMPORTANT: Please read the user guide installed at .bmad-core/user-guide.md"));
    console.log(chalk.red("This guide contains essential information about the BMad workflow and how to use the agents effectively."));
  }

  // Legacy method for backward compatibility
  async update() {
    console.log(chalk.yellow('The "update" command is deprecated.'));
    console.log(
      'Please use "install" instead - it will detect and offer to update existing installations.'
    );

    const installDir = await this.findInstallation();
    if (installDir) {
      const config = {
        installType: "full",
        directory: path.dirname(installDir),
        ide: null,
      };
      return await this.install(config);
    }
    console.log(chalk.red("No BMad installation found."));
  }

  async listAgents() {
    const agents = await resourceLocator.getAvailableAgents();

    console.log(chalk.bold("\nAvailable BMad Agents:\n"));

    for (const agent of agents) {
      console.log(chalk.cyan(`  ${agent.id.padEnd(20)}`), agent.description);
    }

    console.log(
      chalk.dim("\nInstall with: npx bmad-method install --agent=<id>\n")
    );
  }

  async listExpansionPacks() {
    const expansionPacks = await resourceLocator.getExpansionPacks();

    console.log(chalk.bold("\nAvailable BMad Expansion Packs:\n"));

    if (expansionPacks.length === 0) {
      console.log(chalk.yellow("No expansion packs found."));
      return;
    }

    for (const pack of expansionPacks) {
      console.log(chalk.cyan(`  ${pack.id.padEnd(20)}`),
                  `${pack.name} v${pack.version}`);
      console.log(chalk.dim(`  ${' '.repeat(22)}${pack.description}`));
      if (pack.author && pack.author !== 'Unknown') {
        console.log(chalk.dim(`  ${' '.repeat(22)}by ${pack.author}`));
      }
      console.log();
    }

    console.log(
      chalk.dim("Install with: npx bmad-method install --full --expansion-packs <id>\n")
    );
  }

  async showStatus() {
    const installDir = await this.findInstallation();

    if (!installDir) {
      console.log(
        chalk.yellow("No BMad installation found in current directory tree")
      );
      return;
    }

    const manifest = await fileManager.readManifest(installDir);

    if (!manifest) {
      console.log(chalk.red("Invalid installation - manifest not found"));
      return;
    }

    console.log(chalk.bold("\nBMad Installation Status:\n"));
    console.log(`  Directory:      ${installDir}`);
    console.log(`  Version:        ${manifest.version}`);
    console.log(
      `  Installed:      ${new Date(
        manifest.installed_at
      ).toLocaleDateString()}`
    );
    console.log(`  Type:           ${manifest.install_type}`);

    if (manifest.agent) {
      console.log(`  Agent:          ${manifest.agent}`);
    }

    if (manifest.ides_setup && manifest.ides_setup.length > 0) {
      console.log(`  IDE Setup:      ${manifest.ides_setup.join(', ')}`);
    }

    console.log(`  Total Files:    ${manifest.files.length}`);

    // Check for modifications
    const modifiedFiles = await fileManager.checkModifiedFiles(
      installDir,
      manifest
    );
    if (modifiedFiles.length > 0) {
      console.log(chalk.yellow(`  Modified Files: ${modifiedFiles.length}`));
    }

    console.log("");
  }

  async getAvailableAgents() {
    return resourceLocator.getAvailableAgents();
  }

  async getAvailableExpansionPacks() {
    return resourceLocator.getExpansionPacks();
  }

  async getAvailableTeams() {
    return configLoader.getAvailableTeams();
  }

  async installExpansionPacks(installDir, selectedPacks, spinner, config = {}) {
    if (!selectedPacks || selectedPacks.length === 0) {
      return [];
    }

    const installedFiles = [];

    for (const packId of selectedPacks) {
      spinner.text = `Installing expansion pack: ${packId}...`;

      try {
        const expansionPacks = await resourceLocator.getExpansionPacks();
        const pack = expansionPacks.find(p => p.id === packId);

        if (!pack) {
          console.warn(`Expansion pack ${packId} not found, skipping...`);
          continue;
        }

        // Check if expansion pack already exists
        let expansionDotFolder = path.join(installDir, `.${packId}`);
        const existingManifestPath = path.join(expansionDotFolder, 'install-manifest.yaml');

        if (await fileManager.pathExists(existingManifestPath)) {
          spinner.stop();
          const existingManifest = await fileManager.readExpansionPackManifest(installDir, packId);

          console.log(chalk.yellow(`\n🔍 Found existing ${pack.name} installation`));
          console.log(`   Current version: ${existingManifest.version || 'unknown'}`);
          console.log(`   New version: ${pack.version}`);

          // Check integrity of existing expansion pack
          const packIntegrity = await fileManager.checkFileIntegrity(installDir, existingManifest);
          const hasPackIntegrityIssues = packIntegrity.missing.length > 0 || packIntegrity.modified.length > 0;

          if (hasPackIntegrityIssues) {
            console.log(chalk.red("   ⚠  Installation issues detected:"));
            if (packIntegrity.missing.length > 0) {
              console.log(chalk.red(`     Missing files: ${packIntegrity.missing.length}`));
            }
            if (packIntegrity.modified.length > 0) {
              console.log(chalk.yellow(`     Modified files: ${packIntegrity.modified.length}`));
            }
          }

          const versionCompare = this.compareVersions(existingManifest.version || '0.0.0', pack.version);

          if (versionCompare === 0) {
            console.log(chalk.yellow('   ⚠  Same version already installed'));

            const choices = [];
            if (hasPackIntegrityIssues) {
              choices.push({ name: 'Repair (restore missing/modified files)', value: 'repair' });
            }
            choices.push(
              { name: 'Force reinstall (overwrite)', value: 'overwrite' },
              { name: 'Skip this expansion pack', value: 'skip' },
              { name: 'Cancel installation', value: 'cancel' }
            );

            const { action } = await inquirer.prompt([{
              type: 'list',
              name: 'action',
              message: `${pack.name} v${pack.version} is already installed. What would you like to do?`,
              choices: choices
            }]);

            if (action === 'skip') {
              spinner.start();
              continue;
            } else if (action === 'cancel') {
                console.log('Installation cancelled.');
              process.exit(0);
            } else if (action === 'repair') {
              // Repair the expansion pack
              await this.repairExpansionPack(installDir, packId, pack, packIntegrity, spinner);
              continue;
            }
          } else if (versionCompare < 0) {
            console.log(chalk.cyan('   ⬆  Upgrade available'));

            const { proceed } = await inquirer.prompt([{
              type: 'confirm',
              name: 'proceed',
              message: `Upgrade ${pack.name} from v${existingManifest.version} to v${pack.version}?`,
              default: true
            }]);

            if (!proceed) {
              spinner.start();
              continue;
            }
          } else {
            console.log(chalk.yellow('   ⬇  Installed version is newer than available version'));

            const { action } = await inquirer.prompt([{
              type: 'list',
              name: 'action',
              message: 'What would you like to do?',
              choices: [
                { name: 'Keep current version', value: 'skip' },
                { name: 'Downgrade to available version', value: 'downgrade' },
                { name: 'Cancel installation', value: 'cancel' }
              ]
            }]);

            if (action === 'skip') {
              spinner.start();
              continue;
            } else if (action === 'cancel') {
                console.log('Installation cancelled.');
              process.exit(0);
            }
          }

          // If we get here, we're proceeding with installation
          spinner.start(`Removing old ${pack.name} installation...`);
          await fileManager.removeDirectory(expansionDotFolder);
        }

        const expansionPackDir = pack.path;

        // Ensure dedicated dot folder exists for this expansion pack
        expansionDotFolder = path.join(installDir, `.${packId}`);
        await fileManager.ensureDirectory(expansionDotFolder);

        // Define the folders to copy from expansion packs
        const foldersToSync = [
          'agents',
          'agent-teams',
          'templates',
          'tasks',
          'checklists',
          'workflows',
          'data',
          'utils',
          'schemas'
        ];

        // Copy each folder if it exists
        for (const folder of foldersToSync) {
          const sourceFolder = path.join(expansionPackDir, folder);

          // Check if folder exists in expansion pack
          if (await fileManager.pathExists(sourceFolder)) {
            // Get all files in this folder
            const files = await resourceLocator.findFiles('**/*', {
              cwd: sourceFolder,
              nodir: true
            });

            // Copy each file to the expansion pack's dot folder with {root} replacement
            for (const file of files) {
              const sourcePath = path.join(sourceFolder, file);
              const destPath = path.join(expansionDotFolder, folder, file);

              const needsRootReplacement = file.endsWith('.md') || file.endsWith('.yaml') || file.endsWith('.yml');
              let success = false;

              if (needsRootReplacement) {
                success = await fileManager.copyFileWithRootReplacement(sourcePath, destPath, `.${packId}`);
              } else {
                success = await fileManager.copyFile(sourcePath, destPath);
              }

              if (success) {
                installedFiles.push(path.join(`.${packId}`, folder, file));
              }
            }
          }
        }

        // Copy config.yaml with {root} replacement
        const configPath = path.join(expansionPackDir, 'config.yaml');
        if (await fileManager.pathExists(configPath)) {
          const configDestPath = path.join(expansionDotFolder, 'config.yaml');
          if (await fileManager.copyFileWithRootReplacement(configPath, configDestPath, `.${packId}`)) {
            installedFiles.push(path.join(`.${packId}`, 'config.yaml'));
          }
        }

        // Copy README if it exists with {root} replacement
        const readmePath = path.join(expansionPackDir, 'README.md');
        if (await fileManager.pathExists(readmePath)) {
          const readmeDestPath = path.join(expansionDotFolder, 'README.md');
          if (await fileManager.copyFileWithRootReplacement(readmePath, readmeDestPath, `.${packId}`)) {
            installedFiles.push(path.join(`.${packId}`, 'README.md'));
          }
        }

        // Copy common/ items to expansion pack folder
        spinner.text = `Copying common utilities to ${packId}...`;
        await this.copyCommonItems(installDir, `.${packId}`, spinner);

        // Check and resolve core dependencies
        await this.resolveExpansionPackCoreDependencies(installDir, expansionDotFolder, packId, pack, spinner);

        // Check and resolve core agents referenced by teams
        await this.resolveExpansionPackCoreAgents(installDir, expansionDotFolder, packId, spinner);

        // Create manifest for this expansion pack
        spinner.text = `Creating manifest for ${packId}...`;
        const expansionConfig = {
          installType: 'expansion-pack',
          expansionPackId: packId,
          expansionPackName: pack.name,
          expansionPackVersion: pack.version,
          ides: config.ides || []  // Use ides_setup instead of ide_setup
        };

        // Get all files installed in this expansion pack
        const foundFiles = await resourceLocator.findFiles('**/*', {
          cwd: expansionDotFolder,
          nodir: true
        });
        const expansionPackFiles = foundFiles.map(f => path.join(`.${packId}`, f));

        await fileManager.createExpansionPackManifest(installDir, packId, expansionConfig, expansionPackFiles);

        console.log(chalk.green(`✓ Installed expansion pack: ${pack.name} to ${`.${packId}`}`));
      } catch (error) {
        console.error(`Failed to install expansion pack ${packId}: ${error.message}`);
        console.error(`Stack trace: ${error.stack}`);
      }
    }

    return installedFiles;
  }

  async resolveExpansionPackCoreDependencies(installDir, expansionDotFolder, packId, pack, spinner) {
    const yaml = require('js-yaml');
    const fs = require('fs').promises;

    // Find all agent files in the expansion pack
    const agentFiles = await resourceLocator.findFiles('agents/*.md', {
      cwd: expansionDotFolder
    });

    for (const agentFile of agentFiles) {
      const agentPath = path.join(expansionDotFolder, agentFile);
      const agentContent = await fs.readFile(agentPath, 'utf8');

      // Extract YAML frontmatter to check dependencies
      const yamlContent = extractYamlFromAgent(agentContent);
      if (yamlContent) {
        try {
          const agentConfig = yaml.load(yamlContent);
          const dependencies = agentConfig.dependencies || {};

          // Check for core dependencies (those that don't exist in the expansion pack)
          for (const depType of ['tasks', 'templates', 'checklists', 'workflows', 'utils', 'data']) {
            const deps = dependencies[depType] || [];

            for (const dep of deps) {
              const depFileName = dep.endsWith('.md') || dep.endsWith('.yaml') ? dep :
                                  (depType === 'templates' ? `${dep}.yaml` : `${dep}.md`);
              const expansionDepPath = path.join(expansionDotFolder, depType, depFileName);

              // Check if dependency exists in expansion pack dot folder
              if (!(await fileManager.pathExists(expansionDepPath))) {
                // Try to find it in expansion pack source
                const sourceDepPath = path.join(pack.path, depType, depFileName);

                if (await fileManager.pathExists(sourceDepPath)) {
                  // Copy from expansion pack source
                  spinner.text = `Copying ${packId} dependency ${dep}...`;
                  const destPath = path.join(expansionDotFolder, depType, depFileName);
                  await fileManager.copyFileWithRootReplacement(sourceDepPath, destPath, `.${packId}`);
                  console.log(chalk.dim(`  Added ${packId} dependency: ${depType}/${depFileName}`));
                } else {
                  // Try to find it in core
                  const coreDepPath = path.join(resourceLocator.getBmadCorePath(), depType, depFileName);

                    if (await fileManager.pathExists(coreDepPath)) {
                      spinner.text = `Copying core dependency ${dep} for ${packId}...`;

                      // Copy from core to expansion pack dot folder with {root} replacement
                      const destPath = path.join(expansionDotFolder, depType, depFileName);
                      await fileManager.copyFileWithRootReplacement(coreDepPath, destPath, `.${packId}`);

                      console.log(chalk.dim(`  Added core dependency: ${depType}/${depFileName}`));
                    } else {
                      console.warn(chalk.yellow(`  Warning: Dependency ${depType}/${dep} not found in core or expansion pack`));
                    }
                  }
                }
            }
          }
        } catch (error) {
          console.warn(`  Warning: Could not parse agent dependencies: ${error.message}`);
        }
      }
    }
  }

  async resolveExpansionPackCoreAgents(installDir, expansionDotFolder, packId, spinner) {
    const yaml = require('js-yaml');
    const fs = require('fs').promises;

    // Find all team files in the expansion pack
    const teamFiles = await resourceLocator.findFiles('agent-teams/*.yaml', {
      cwd: expansionDotFolder
    });

    // Also get existing agents in the expansion pack
    const existingAgents = new Set();
    const agentFiles = await resourceLocator.findFiles('agents/*.md', {
      cwd: expansionDotFolder
    });
    for (const agentFile of agentFiles) {
      const agentName = path.basename(agentFile, '.md');
      existingAgents.add(agentName);
    }

    // Process each team file
    for (const teamFile of teamFiles) {
      const teamPath = path.join(expansionDotFolder, teamFile);
      const teamContent = await fs.readFile(teamPath, 'utf8');

      try {
        const teamConfig = yaml.load(teamContent);
        const agents = teamConfig.agents || [];

        // Add bmad-orchestrator if not present (required for all teams)
        if (!agents.includes('bmad-orchestrator')) {
          agents.unshift('bmad-orchestrator');
        }

        // Check each agent in the team
        for (const agentId of agents) {
          if (!existingAgents.has(agentId)) {
            // Agent not in expansion pack, try to get from core
            const coreAgentPath = path.join(resourceLocator.getBmadCorePath(), 'agents', `${agentId}.md`);

            if (await fileManager.pathExists(coreAgentPath)) {
              spinner.text = `Copying core agent ${agentId} for ${packId}...`;

              // Copy agent file with {root} replacement
              const destPath = path.join(expansionDotFolder, 'agents', `${agentId}.md`);
              await fileManager.copyFileWithRootReplacement(coreAgentPath, destPath, `.${packId}`);
              existingAgents.add(agentId);

              console.log(chalk.dim(`  Added core agent: ${agentId}`));

              // Now resolve this agent's dependencies too
              const agentContent = await fs.readFile(coreAgentPath, 'utf8');
              const yamlContent = extractYamlFromAgent(agentContent, true);

              if (yamlContent) {
                try {

                  const agentConfig = yaml.load(yamlContent);
                  const dependencies = agentConfig.dependencies || {};

                  // Copy all dependencies for this agent
                  for (const depType of ['tasks', 'templates', 'checklists', 'workflows', 'utils', 'data']) {
                    const deps = dependencies[depType] || [];

                    for (const dep of deps) {
                      const depFileName = dep.endsWith('.md') || dep.endsWith('.yaml') ? dep :
                                          (depType === 'templates' ? `${dep}.yaml` : `${dep}.md`);
                      const expansionDepPath = path.join(expansionDotFolder, depType, depFileName);

                      // Check if dependency exists in expansion pack
                      if (!(await fileManager.pathExists(expansionDepPath))) {
                        // Try to find it in core
                        const coreDepPath = path.join(resourceLocator.getBmadCorePath(), depType, depFileName);

                        if (await fileManager.pathExists(coreDepPath)) {
                          const destDepPath = path.join(expansionDotFolder, depType, depFileName);
                          await fileManager.copyFileWithRootReplacement(coreDepPath, destDepPath, `.${packId}`);
                          console.log(chalk.dim(`    Added agent dependency: ${depType}/${depFileName}`));
                        } else {
                          // Try common folder
                          const sourceBase = path.dirname(path.dirname(path.dirname(path.dirname(__filename)))); // Go up to project root
                          const commonDepPath = path.join(sourceBase, 'common', depType, depFileName);
                          if (await fileManager.pathExists(commonDepPath)) {
                            const destDepPath = path.join(expansionDotFolder, depType, depFileName);
                            await fileManager.copyFile(commonDepPath, destDepPath);
                            console.log(chalk.dim(`    Added agent dependency from common: ${depType}/${depFileName}`));
                          }
                        }
                      }
                    }
                  }
                } catch (error) {
                  console.warn(`  Warning: Could not parse agent ${agentId} dependencies: ${error.message}`);
                }
              }
            } else {
              console.warn(chalk.yellow(`  Warning: Core agent ${agentId} not found for team ${path.basename(teamFile, '.yaml')}`));
            }
          }
        }
      } catch (error) {
        console.warn(`  Warning: Could not parse team file ${teamFile}: ${error.message}`);
      }
    }
  }

  getWebBundleInfo(config) {
    const webBundleType = config.webBundleType || 'all';

    switch (webBundleType) {
      case 'all':
        return 'all bundles';
      case 'agents':
        return 'individual agents only';
      case 'teams':
        return config.selectedWebBundleTeams ?
          `teams: ${config.selectedWebBundleTeams.join(', ')}` :
          'selected teams';
      case 'custom':
        const parts = [];
        if (config.selectedWebBundleTeams && config.selectedWebBundleTeams.length > 0) {
          parts.push(`teams: ${config.selectedWebBundleTeams.join(', ')}`);
        }
        if (config.includeIndividualAgents) {
          parts.push('individual agents');
        }
        return parts.length > 0 ? parts.join(' + ') : 'custom selection';
      default:
        return 'selected bundles';
    }
  }

  async installWebBundles(webBundlesDirectory, config, spinner) {

    try {
      // Find the dist directory in the BMad installation
      const distDir = configLoader.getDistPath();

      if (!(await fileManager.pathExists(distDir))) {
        console.warn('Web bundles not found. Run "npm run build" to generate them.');
        return;
      }

      // Ensure web bundles directory exists
      await fileManager.ensureDirectory(webBundlesDirectory);

      const webBundleType = config.webBundleType || 'all';

      if (webBundleType === 'all') {
        // Copy the entire dist directory structure
        await fileManager.copyDirectory(distDir, webBundlesDirectory);
        console.log(chalk.green(`✓ Installed all web bundles to: ${webBundlesDirectory}`));
      } else {
        let copiedCount = 0;

        // Copy specific selections based on type
        if (webBundleType === 'agents' || (webBundleType === 'custom' && config.includeIndividualAgents)) {
          const agentsSource = path.join(distDir, 'agents');
          const agentsTarget = path.join(webBundlesDirectory, 'agents');
          if (await fileManager.pathExists(agentsSource)) {
            await fileManager.copyDirectory(agentsSource, agentsTarget);
            console.log(chalk.green(`✓ Copied individual agent bundles`));
            copiedCount += 10; // Approximate count for agents
          }
        }

        if (webBundleType === 'teams' || webBundleType === 'custom') {
          if (config.selectedWebBundleTeams && config.selectedWebBundleTeams.length > 0) {
            const teamsSource = path.join(distDir, 'teams');
            const teamsTarget = path.join(webBundlesDirectory, 'teams');
            await fileManager.ensureDirectory(teamsTarget);

            for (const teamId of config.selectedWebBundleTeams) {
              const teamFile = `${teamId}.txt`;
              const sourcePath = path.join(teamsSource, teamFile);
              const targetPath = path.join(teamsTarget, teamFile);

              if (await fileManager.pathExists(sourcePath)) {
                await fileManager.copyFile(sourcePath, targetPath);
                copiedCount++;
                console.log(chalk.green(`✓ Copied team bundle: ${teamId}`));
              }
            }
          }
        }

        // Always copy expansion packs if they exist
        const expansionSource = path.join(distDir, 'expansion-packs');
        const expansionTarget = path.join(webBundlesDirectory, 'expansion-packs');
        if (await fileManager.pathExists(expansionSource)) {
          await fileManager.copyDirectory(expansionSource, expansionTarget);
          console.log(chalk.green(`✓ Copied expansion pack bundles`));
        }

        console.log(chalk.green(`✓ Installed ${copiedCount} selected web bundles to: ${webBundlesDirectory}`));
      }
    } catch (error) {
      console.error(`Failed to install web bundles: ${error.message}`);
    }
  }

  async copyCommonItems(installDir, targetSubdir, spinner) {

    const fs = require('fs').promises;
    const sourceBase = path.dirname(path.dirname(path.dirname(path.dirname(__filename)))); // Go up to project root
    const commonPath = path.join(sourceBase, 'common');
    const targetPath = path.join(installDir, targetSubdir);
    const copiedFiles = [];

    // Check if common/ exists
    if (!(await fileManager.pathExists(commonPath))) {
      console.warn('Warning: common/ folder not found');
      return copiedFiles;
    }

    // Copy all items from common/ to target
    const commonItems = await resourceLocator.findFiles('**/*', {
      cwd: commonPath,
      nodir: true
    });

    for (const item of commonItems) {
      const sourcePath = path.join(commonPath, item);
      const destPath = path.join(targetPath, item);

      // Read the file content
      const content = await fs.readFile(sourcePath, 'utf8');

      // Replace {root} with the target subdirectory
      const updatedContent = content.replace(/\{root\}/g, targetSubdir);

      // Ensure directory exists
      await fileManager.ensureDirectory(path.dirname(destPath));

      // Write the updated content
      await fs.writeFile(destPath, updatedContent, 'utf8');
      copiedFiles.push(path.join(targetSubdir, item));
    }

    console.log(chalk.dim(`  Added ${commonItems.length} common utilities`));
    return copiedFiles;
  }

  async detectExpansionPacks(installDir) {
    const expansionPacks = {};
    const glob = require("glob");

    // Find all dot folders that might be expansion packs
    const dotFolders = glob.sync(".*", {
      cwd: installDir,
      ignore: [".git", ".git/**", ".bmad-core", ".bmad-core/**"],
    });

    for (const folder of dotFolders) {
      const folderPath = path.join(installDir, folder);
      const stats = await fileManager.pathExists(folderPath);

      if (stats) {
        // Check if it has a manifest
        const manifestPath = path.join(folderPath, "install-manifest.yaml");
        if (await fileManager.pathExists(manifestPath)) {
          const manifest = await fileManager.readExpansionPackManifest(installDir, folder.substring(1));
          if (manifest) {
            expansionPacks[folder.substring(1)] = {
              path: folderPath,
              manifest: manifest,
              hasManifest: true
            };
          }
        } else {
          // Check if it has a config.yaml (expansion pack without manifest)
          const configPath = path.join(folderPath, "config.yaml");
          if (await fileManager.pathExists(configPath)) {
            expansionPacks[folder.substring(1)] = {
              path: folderPath,
              manifest: null,
              hasManifest: false
            };
          }
        }
      }
    }

    return expansionPacks;
  }

  async repairExpansionPack(installDir, packId, pack, integrity, spinner) {
    spinner.start(`Repairing ${pack.name}...`);

    try {
      const expansionDotFolder = path.join(installDir, `.${packId}`);

      // Back up modified files
      if (integrity.modified.length > 0) {
        spinner.text = "Backing up modified files...";
        for (const file of integrity.modified) {
          const filePath = path.join(installDir, file);
          if (await fileManager.pathExists(filePath)) {
            const backupPath = await fileManager.backupFile(filePath);
            console.log(chalk.dim(`  Backed up: ${file} → ${path.basename(backupPath)}`));
          }
        }
      }

      // Restore missing and modified files
      spinner.text = "Restoring files...";
      const filesToRestore = [...integrity.missing, ...integrity.modified];

      for (const file of filesToRestore) {
        // Skip the manifest file itself
        if (file.endsWith('install-manifest.yaml')) continue;

        const relativePath = file.replace(`.${packId}/`, '');
        const sourcePath = path.join(pack.path, relativePath);
        const destPath = path.join(installDir, file);

        // Check if this is a common/ file that needs special processing
        const commonBase = path.dirname(path.dirname(path.dirname(path.dirname(__filename))));
        const commonSourcePath = path.join(commonBase, 'common', relativePath);

        if (await fileManager.pathExists(commonSourcePath)) {
          // This is a common/ file - needs template processing
          const fs = require('fs').promises;
          const content = await fs.readFile(commonSourcePath, 'utf8');
          const updatedContent = content.replace(/\{root\}/g, `.${packId}`);
          await fileManager.ensureDirectory(path.dirname(destPath));
          await fs.writeFile(destPath, updatedContent, 'utf8');
          spinner.text = `Restored: ${file}`;
        } else if (await fileManager.pathExists(sourcePath)) {
          // Regular file from expansion pack
          await fileManager.copyFile(sourcePath, destPath);
          spinner.text = `Restored: ${file}`;
        } else {
          console.warn(chalk.yellow(`  Warning: Source file not found: ${file}`));
        }
      }

      spinner.succeed(`${pack.name} repaired successfully!`);

      // Show summary
      console.log(chalk.green(`\n✓ ${pack.name} repaired!`));
      if (integrity.missing.length > 0) {
        console.log(chalk.green(`  Restored ${integrity.missing.length} missing files`));
      }
      if (integrity.modified.length > 0) {
        console.log(chalk.green(`  Restored ${integrity.modified.length} modified files (backups created)`));
      }

    } catch (error) {
      if (spinner) spinner.fail(`Failed to repair ${pack.name}`);
      console.error(`Error: ${error.message}`);
    }
  }

  compareVersions(v1, v2) {
    // Simple semver comparison
    const parts1 = v1.split('.').map(Number);
    const parts2 = v2.split('.').map(Number);

    for (let i = 0; i < 3; i++) {
      const part1 = parts1[i] || 0;
      const part2 = parts2[i] || 0;

      if (part1 > part2) return 1;
      if (part1 < part2) return -1;
    }

    return 0;
  }

  async cleanupLegacyYmlFiles(installDir, spinner) {
    const glob = require('glob');
    const fs = require('fs').promises;

    try {
      // Find all .yml files in the installation directory
      const ymlFiles = glob.sync('**/*.yml', {
        cwd: installDir,
        ignore: ['**/node_modules/**', '**/.git/**']
      });

      let deletedCount = 0;

      for (const ymlFile of ymlFiles) {
        // Check if corresponding .yaml file exists
        const yamlFile = ymlFile.replace(/\.yml$/, '.yaml');
        const ymlPath = path.join(installDir, ymlFile);
        const yamlPath = path.join(installDir, yamlFile);

        if (await fileManager.pathExists(yamlPath)) {
          // .yaml counterpart exists, delete the .yml file
          await fs.unlink(ymlPath);
          deletedCount++;
          console.log(chalk.dim(`  Removed legacy: ${ymlFile} (replaced by ${yamlFile})`));
        }
      }

      if (deletedCount > 0) {
        console.log(chalk.green(`✓ Cleaned up ${deletedCount} legacy .yml files`));
      }

    } catch (error) {
      console.warn(`Warning: Could not cleanup legacy .yml files: ${error.message}`);
    }
  }

  async findInstallation() {
    // Look for .bmad-core in current directory or parent directories
    let currentDir = process.cwd();

    while (currentDir !== path.dirname(currentDir)) {
      const bmadDir = path.join(currentDir, ".bmad-core");
      const manifestPath = path.join(bmadDir, "install-manifest.yaml");

      if (await fileManager.pathExists(manifestPath)) {
        return bmadDir;
      }

      currentDir = path.dirname(currentDir);
    }

    // Also check if we're inside a .bmad-core directory
    if (path.basename(process.cwd()) === ".bmad-core") {
      const manifestPath = path.join(process.cwd(), "install-manifest.yaml");
      if (await fileManager.pathExists(manifestPath)) {
        return process.cwd();
      }
    }

    return null;
  }
}

module.exports = new Installer();

const path = require("path");
const fs = require("fs-extra");
const yaml = require("js-yaml");
const chalk = require("chalk");
const inquirer = require("inquirer");
const fileManager = require("./file-manager");
const configLoader = require("./config-loader");
const { extractYamlFromAgent } = require("../../lib/yaml-utils");
const BaseIdeSetup = require("./ide-base-setup");
const resourceLocator = require("./resource-locator");

class IdeSetup extends BaseIdeSetup {
  constructor() {
    super();
    this.ideAgentConfig = null;
  }

  async loadIdeAgentConfig() {
    if (this.ideAgentConfig) return this.ideAgentConfig;

    try {
      const configPath = path.join(__dirname, '..', 'config', 'ide-agent-config.yaml');
      const configContent = await fs.readFile(configPath, 'utf8');
      this.ideAgentConfig = yaml.load(configContent);
      return this.ideAgentConfig;
    } catch (error) {
      console.warn('Failed to load IDE agent configuration, using defaults');
      return {
        'roo-permissions': {},
        'cline-order': {}
      };
    }
  }

  async setup(ide, installDir, selectedAgent = null, spinner = null, preConfiguredSettings = null) {
    const ideConfig = await configLoader.getIdeConfiguration(ide);

    if (!ideConfig) {
      console.log(chalk.yellow(`\nNo configuration available for ${ide}`));
      return false;
    }

    switch (ide) {
      case "cursor":
        return this.setupCursor(installDir, selectedAgent);
      case "claude-code":
        return this.setupClaudeCode(installDir, selectedAgent);
      case "windsurf":
        return this.setupWindsurf(installDir, selectedAgent);
      case "trae":
        return this.setupTrae(installDir, selectedAgent);
      case "roo":
        return this.setupRoo(installDir, selectedAgent);
      case "cline":
        return this.setupCline(installDir, selectedAgent);
      case "gemini":
        return this.setupGeminiCli(installDir, selectedAgent);
      case "github-copilot":
        return this.setupGitHubCopilot(installDir, selectedAgent, spinner, preConfiguredSettings);
      default:
        console.log(chalk.yellow(`\nIDE ${ide} not yet supported`));
        return false;
    }
  }

  async setupCursor(installDir, selectedAgent) {
    const cursorRulesDir = path.join(installDir, ".cursor", "rules");
    const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir);

    await fileManager.ensureDirectory(cursorRulesDir);

    for (const agentId of agents) {
      const agentPath = await this.findAgentPath(agentId, installDir);

      if (agentPath) {
        const mdcContent = await this.createAgentRuleContent(agentId, agentPath, installDir, 'mdc');
        const mdcPath = path.join(cursorRulesDir, `${agentId}.mdc`);
        await fileManager.writeFile(mdcPath, mdcContent);
        console.log(chalk.green(`✓ Created rule: ${agentId}.mdc`));
      }
    }

    console.log(chalk.green(`\n✓ Created Cursor rules in ${cursorRulesDir}`));
    return true;
  }

  async setupClaudeCode(installDir, selectedAgent) {
    // Setup bmad-core commands
    const coreSlashPrefix = await this.getCoreSlashPrefix(installDir);
    const coreAgents = selectedAgent ? [selectedAgent] : await this.getCoreAgentIds(installDir);
    const coreTasks = await this.getCoreTaskIds(installDir);
    await this.setupClaudeCodeForPackage(installDir, "core", coreSlashPrefix, coreAgents, coreTasks, ".bmad-core");

    // Setup expansion pack commands
    const expansionPacks = await this.getInstalledExpansionPacks(installDir);
    for (const packInfo of expansionPacks) {
      const packSlashPrefix = await this.getExpansionPackSlashPrefix(packInfo.path);
      const packAgents = await this.getExpansionPackAgents(packInfo.path);
      const packTasks = await this.getExpansionPackTasks(packInfo.path);

      if (packAgents.length > 0 || packTasks.length > 0) {
        // Use the actual directory name where the expansion pack is installed
        const rootPath = path.relative(installDir, packInfo.path);
        await this.setupClaudeCodeForPackage(installDir, packInfo.name, packSlashPrefix, packAgents, packTasks, rootPath);
      }
    }

    return true;
  }

  async setupClaudeCodeForPackage(installDir, packageName, slashPrefix, agentIds, taskIds, rootPath) {
    const commandsBaseDir = path.join(installDir, ".claude", "commands", slashPrefix);
    const agentsDir = path.join(commandsBaseDir, "agents");
    const tasksDir = path.join(commandsBaseDir, "tasks");

    // Ensure directories exist
    await fileManager.ensureDirectory(agentsDir);
    await fileManager.ensureDirectory(tasksDir);

    // Setup agents
    for (const agentId of agentIds) {
      // Find the agent file - for expansion packs, prefer the expansion pack version
      let agentPath;
      if (packageName !== "core") {
        // For expansion packs, first try to find the agent in the expansion pack directory
        const expansionPackPath = path.join(installDir, rootPath, "agents", `${agentId}.md`);
        if (await fileManager.pathExists(expansionPackPath)) {
          agentPath = expansionPackPath;
        } else {
          // Fall back to core if not found in expansion pack
          agentPath = await this.findAgentPath(agentId, installDir);
        }
      } else {
        // For core, use the normal search
        agentPath = await this.findAgentPath(agentId, installDir);
      }

      const commandPath = path.join(agentsDir, `${agentId}.md`);

      if (agentPath) {
        // Create command file with agent content
        let agentContent = await fileManager.readFile(agentPath);

        // Replace {root} placeholder with the appropriate root path for this context
        agentContent = agentContent.replace(/{root}/g, rootPath);

        // Add command header
        let commandContent = `# /${agentId} Command\n\n`;
        commandContent += `When this command is used, adopt the following agent persona:\n\n`;
        commandContent += agentContent;

        await fileManager.writeFile(commandPath, commandContent);
        console.log(chalk.green(`✓ Created agent command: /${agentId}`));
      }
    }

    // Setup tasks
    for (const taskId of taskIds) {
      // Find the task file - for expansion packs, prefer the expansion pack version
      let taskPath;
      if (packageName !== "core") {
        // For expansion packs, first try to find the task in the expansion pack directory
        const expansionPackPath = path.join(installDir, rootPath, "tasks", `${taskId}.md`);
        if (await fileManager.pathExists(expansionPackPath)) {
          taskPath = expansionPackPath;
        } else {
          // Fall back to core if not found in expansion pack
          taskPath = await this.findTaskPath(taskId, installDir);
        }
      } else {
        // For core, use the normal search
        taskPath = await this.findTaskPath(taskId, installDir);
      }

      const commandPath = path.join(tasksDir, `${taskId}.md`);

      if (taskPath) {
        // Create command file with task content
        let taskContent = await fileManager.readFile(taskPath);

        // Replace {root} placeholder with the appropriate root path for this context
        taskContent = taskContent.replace(/{root}/g, rootPath);

        // Add command header
        let commandContent = `# /${taskId} Task\n\n`;
        commandContent += `When this command is used, execute the following task:\n\n`;
        commandContent += taskContent;

        await fileManager.writeFile(commandPath, commandContent);
        console.log(chalk.green(`✓ Created task command: /${taskId}`));
      }
    }

    console.log(chalk.green(`\n✓ Created Claude Code commands for ${packageName} in ${commandsBaseDir}`));
    console.log(chalk.dim(`  - Agents in: ${agentsDir}`));
    console.log(chalk.dim(`  - Tasks in: ${tasksDir}`));
  }

  async setupWindsurf(installDir, selectedAgent) {
    const windsurfRulesDir = path.join(installDir, ".windsurf", "rules");
    const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir);

    await fileManager.ensureDirectory(windsurfRulesDir);

    for (const agentId of agents) {
      // Find the agent file
      const agentPath = await this.findAgentPath(agentId, installDir);

      if (agentPath) {
        const agentContent = await fileManager.readFile(agentPath);
        const mdPath = path.join(windsurfRulesDir, `${agentId}.md`);

        // Create MD content (similar to Cursor but without frontmatter)
        let mdContent = `# ${agentId.toUpperCase()} Agent Rule\n\n`;
        mdContent += `This rule is triggered when the user types \`@${agentId}\` and activates the ${await this.getAgentTitle(
          agentId,
          installDir
        )} agent persona.\n\n`;
        mdContent += "## Agent Activation\n\n";
        mdContent +=
          "CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n";
        mdContent += "```yaml\n";
        // Extract just the YAML content from the agent file
        const yamlContent = extractYamlFromAgent(agentContent);
        if (yamlContent) {
          mdContent += yamlContent;
        } else {
          // If no YAML found, include the whole content minus the header
          mdContent += agentContent.replace(/^#.*$/m, "").trim();
        }
        mdContent += "\n```\n\n";
        mdContent += "## File Reference\n\n";
        const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/');
        mdContent += `The complete agent definition is available in [${relativePath}](${relativePath}).\n\n`;
        mdContent += "## Usage\n\n";
        mdContent += `When the user types \`@${agentId}\`, activate this ${await this.getAgentTitle(
          agentId,
          installDir
        )} persona and follow all instructions defined in the YAML configuration above.\n`;

        await fileManager.writeFile(mdPath, mdContent);
        console.log(chalk.green(`✓ Created rule: ${agentId}.md`));
      }
    }

    console.log(chalk.green(`\n✓ Created Windsurf rules in ${windsurfRulesDir}`));

    return true;
  }

  async setupTrae(installDir, selectedAgent) {
    const traeRulesDir = path.join(installDir, ".trae", "rules");
    const agents = selectedAgent? [selectedAgent] : await this.getAllAgentIds(installDir);

    await fileManager.ensureDirectory(traeRulesDir);

    for (const agentId of agents) {
      // Find the agent file
      const agentPath = await this.findAgentPath(agentId, installDir);

      if (agentPath) {
        const agentContent = await fileManager.readFile(agentPath);
        const mdPath = path.join(traeRulesDir, `${agentId}.md`);

        // Create MD content (similar to Cursor but without frontmatter)
        let mdContent = `# ${agentId.toUpperCase()} Agent Rule\n\n`;
        mdContent += `This rule is triggered when the user types \`@${agentId}\` and activates the ${await this.getAgentTitle(
          agentId,
          installDir
        )} agent persona.\n\n`;
        mdContent += "## Agent Activation\n\n";
        mdContent +=
          "CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n";
        mdContent += "```yaml\n";
        // Extract just the YAML content from the agent file
        const yamlContent = extractYamlFromAgent(agentContent);
        if (yamlContent) {
          mdContent += yamlContent;
        }
        else {
          // If no YAML found, include the whole content minus the header
          mdContent += agentContent.replace(/^#.*$/m, "").trim();
        }
        mdContent += "\n```\n\n";
        mdContent += "## File Reference\n\n";
        const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/');
        mdContent += `The complete agent definition is available in [${relativePath}](${relativePath}).\n\n`;
        mdContent += "## Usage\n\n";
        mdContent += `When the user types \`@${agentId}\`, activate this ${await this.getAgentTitle(
          agentId,
          installDir
        )} persona and follow all instructions defined in the YAML configuration above.\n`;

        await fileManager.writeFile(mdPath, mdContent);
        console.log(chalk.green(`✓ Created rule: ${agentId}.md`));
      }
    }
  }

  async findAgentPath(agentId, installDir) {
    // Try to find the agent file in various locations
    const possiblePaths = [
      path.join(installDir, ".bmad-core", "agents", `${agentId}.md`),
      path.join(installDir, "agents", `${agentId}.md`)
    ];

    // Also check expansion pack directories
    const glob = require("glob");
    const expansionDirs = glob.sync(".*/agents", { cwd: installDir });
    for (const expDir of expansionDirs) {
      possiblePaths.push(path.join(installDir, expDir, `${agentId}.md`));
    }

    for (const agentPath of possiblePaths) {
      if (await fileManager.pathExists(agentPath)) {
        return agentPath;
      }
    }

    return null;
  }

  async getAllAgentIds(installDir) {
    const glob = require("glob");
    const allAgentIds = [];

    // Check core agents in .bmad-core or root
    let agentsDir = path.join(installDir, ".bmad-core", "agents");
    if (!(await fileManager.pathExists(agentsDir))) {
      agentsDir = path.join(installDir, "agents");
    }

    if (await fileManager.pathExists(agentsDir)) {
      const agentFiles = glob.sync("*.md", { cwd: agentsDir });
      allAgentIds.push(...agentFiles.map((file) => path.basename(file, ".md")));
    }

    // Also check for expansion pack agents in dot folders
    const expansionDirs = glob.sync(".*/agents", { cwd: installDir });
    for (const expDir of expansionDirs) {
      const fullExpDir = path.join(installDir, expDir);
      const expAgentFiles = glob.sync("*.md", { cwd: fullExpDir });
      allAgentIds.push(...expAgentFiles.map((file) => path.basename(file, ".md")));
    }

    // Remove duplicates
    return [...new Set(allAgentIds)];
  }

  async getCoreAgentIds(installDir) {
    const allAgentIds = [];

    // Check core agents in .bmad-core or root only
    let agentsDir = path.join(installDir, ".bmad-core", "agents");
    if (!(await fileManager.pathExists(agentsDir))) {
      agentsDir = path.join(installDir, "bmad-core", "agents");
    }

    if (await fileManager.pathExists(agentsDir)) {
      const glob = require("glob");
      const agentFiles = glob.sync("*.md", { cwd: agentsDir });
      allAgentIds.push(...agentFiles.map((file) => path.basename(file, ".md")));
    }

    return [...new Set(allAgentIds)];
  }

  async getCoreTaskIds(installDir) {
    const allTaskIds = [];

    // Check core tasks in .bmad-core or root only
    let tasksDir = path.join(installDir, ".bmad-core", "tasks");
    if (!(await fileManager.pathExists(tasksDir))) {
      tasksDir = path.join(installDir, "bmad-core", "tasks");
    }

    if (await fileManager.pathExists(tasksDir)) {
      const glob = require("glob");
      const taskFiles = glob.sync("*.md", { cwd: tasksDir });
      allTaskIds.push(...taskFiles.map((file) => path.basename(file, ".md")));
    }

    // Check common tasks
    const commonTasksDir = path.join(installDir, "common", "tasks");
    if (await fileManager.pathExists(commonTasksDir)) {
      const commonTaskFiles = glob.sync("*.md", { cwd: commonTasksDir });
      allTaskIds.push(...commonTaskFiles.map((file) => path.basename(file, ".md")));
    }

    return [...new Set(allTaskIds)];
  }

  async getAgentTitle(agentId, installDir) {
    // Try to find the agent file in various locations
    const possiblePaths = [
      path.join(installDir, ".bmad-core", "agents", `${agentId}.md`),
      path.join(installDir, "agents", `${agentId}.md`)
    ];

    // Also check expansion pack directories
    const glob = require("glob");
    const expansionDirs = glob.sync(".*/agents", { cwd: installDir });
    for (const expDir of expansionDirs) {
      possiblePaths.push(path.join(installDir, expDir, `${agentId}.md`));
    }

    for (const agentPath of possiblePaths) {
      if (await fileManager.pathExists(agentPath)) {
        try {
          const agentContent = await fileManager.readFile(agentPath);
          const yamlMatch = agentContent.match(/```ya?ml\r?\n([\s\S]*?)```/);

          if (yamlMatch) {
            const yaml = yamlMatch[1];
            const titleMatch = yaml.match(/title:\s*(.+)/);
            if (titleMatch) {
              return titleMatch[1].trim();
            }
          }
        } catch (error) {
          console.warn(`Failed to read agent title for ${agentId}: ${error.message}`);
        }
      }
    }

    // Fallback to formatted agent ID
    return agentId.split('-').map(word =>
      word.charAt(0).toUpperCase() + word.slice(1)
    ).join(' ');
  }

  async getAllTaskIds(installDir) {
    const glob = require("glob");
    const allTaskIds = [];

    // Check core tasks in .bmad-core or root
    let tasksDir = path.join(installDir, ".bmad-core", "tasks");
    if (!(await fileManager.pathExists(tasksDir))) {
      tasksDir = path.join(installDir, "bmad-core", "tasks");
    }

    if (await fileManager.pathExists(tasksDir)) {
      const taskFiles = glob.sync("*.md", { cwd: tasksDir });
      allTaskIds.push(...taskFiles.map((file) => path.basename(file, ".md")));
    }

    // Check common tasks
    const commonTasksDir = path.join(installDir, "common", "tasks");
    if (await fileManager.pathExists(commonTasksDir)) {
      const commonTaskFiles = glob.sync("*.md", { cwd: commonTasksDir });
      allTaskIds.push(...commonTaskFiles.map((file) => path.basename(file, ".md")));
    }

    // Also check for expansion pack tasks in dot folders
    const expansionDirs = glob.sync(".*/tasks", { cwd: installDir });
    for (const expDir of expansionDirs) {
      const fullExpDir = path.join(installDir, expDir);
      const expTaskFiles = glob.sync("*.md", { cwd: fullExpDir });
      allTaskIds.push(...expTaskFiles.map((file) => path.basename(file, ".md")));
    }

    // Check expansion-packs folder tasks
    const expansionPacksDir = path.join(installDir, "expansion-packs");
    if (await fileManager.pathExists(expansionPacksDir)) {
      const expPackDirs = glob.sync("*/tasks", { cwd: expansionPacksDir });
      for (const expDir of expPackDirs) {
        const fullExpDir = path.join(expansionPacksDir, expDir);
        const expTaskFiles = glob.sync("*.md", { cwd: fullExpDir });
        allTaskIds.push(...expTaskFiles.map((file) => path.basename(file, ".md")));
      }
    }

    // Remove duplicates
    return [...new Set(allTaskIds)];
  }

  async findTaskPath(taskId, installDir) {
    // Try to find the task file in various locations
    const possiblePaths = [
      path.join(installDir, ".bmad-core", "tasks", `${taskId}.md`),
      path.join(installDir, "bmad-core", "tasks", `${taskId}.md`),
      path.join(installDir, "common", "tasks", `${taskId}.md`)
    ];

    // Also check expansion pack directories
    const glob = require("glob");

    // Check dot folder expansion packs
    const expansionDirs = glob.sync(".*/tasks", { cwd: installDir });
    for (const expDir of expansionDirs) {
      possiblePaths.push(path.join(installDir, expDir, `${taskId}.md`));
    }

    // Check expansion-packs folder
    const expansionPacksDir = path.join(installDir, "expansion-packs");
    if (await fileManager.pathExists(expansionPacksDir)) {
      const expPackDirs = glob.sync("*/tasks", { cwd: expansionPacksDir });
      for (const expDir of expPackDirs) {
        possiblePaths.push(path.join(expansionPacksDir, expDir, `${taskId}.md`));
      }
    }

    for (const taskPath of possiblePaths) {
      if (await fileManager.pathExists(taskPath)) {
        return taskPath;
      }
    }

    return null;
  }

  async getCoreSlashPrefix(installDir) {
    try {
      const coreConfigPath = path.join(installDir, ".bmad-core", "core-config.yaml");
      if (!(await fileManager.pathExists(coreConfigPath))) {
        // Try bmad-core directory
        const altConfigPath = path.join(installDir, "bmad-core", "core-config.yaml");
        if (await fileManager.pathExists(altConfigPath)) {
          const configContent = await fileManager.readFile(altConfigPath);
          const config = yaml.load(configContent);
          return config.slashPrefix || "BMad";
        }
        return "BMad"; // fallback
      }

      const configContent = await fileManager.readFile(coreConfigPath);
      const config = yaml.load(configContent);
      return config.slashPrefix || "BMad";
    } catch (error) {
      console.warn(`Failed to read core slashPrefix, using default 'BMad': ${error.message}`);
      return "BMad";
    }
  }

  async getInstalledExpansionPacks(installDir) {
    const expansionPacks = [];

    // Check for dot-prefixed expansion packs in install directory
    const glob = require("glob");
    const dotExpansions = glob.sync(".bmad-*", { cwd: installDir });

    for (const dotExpansion of dotExpansions) {
      if (dotExpansion !== ".bmad-core") {
        const packPath = path.join(installDir, dotExpansion);
        const packName = dotExpansion.substring(1); // remove the dot
        expansionPacks.push({
          name: packName,
          path: packPath
        });
      }
    }

    // Check for expansion-packs directory style
    const expansionPacksDir = path.join(installDir, "expansion-packs");
    if (await fileManager.pathExists(expansionPacksDir)) {
      const packDirs = glob.sync("*", { cwd: expansionPacksDir });

      for (const packDir of packDirs) {
        const packPath = path.join(expansionPacksDir, packDir);
        if ((await fileManager.pathExists(packPath)) &&
            (await fileManager.pathExists(path.join(packPath, "config.yaml")))) {
          expansionPacks.push({
            name: packDir,
            path: packPath
          });
        }
      }
    }

    return expansionPacks;
  }

  async getExpansionPackSlashPrefix(packPath) {
    try {
      const configPath = path.join(packPath, "config.yaml");
      if (await fileManager.pathExists(configPath)) {
        const configContent = await fileManager.readFile(configPath);
        const config = yaml.load(configContent);
        return config.slashPrefix || path.basename(packPath);
      }
    } catch (error) {
      console.warn(`Failed to read expansion pack slashPrefix from ${packPath}: ${error.message}`);
    }

    return path.basename(packPath); // fallback to directory name
  }

  async getExpansionPackAgents(packPath) {
    const agentsDir = path.join(packPath, "agents");
    if (!(await fileManager.pathExists(agentsDir))) {
      return [];
    }

    try {
      const glob = require("glob");
      const agentFiles = glob.sync("*.md", { cwd: agentsDir });
      return agentFiles.map(file => path.basename(file, ".md"));
    } catch (error) {
      console.warn(`Failed to read expansion pack agents from ${packPath}: ${error.message}`);
      return [];
    }
  }

  async getExpansionPackTasks(packPath) {
    const tasksDir = path.join(packPath, "tasks");
    if (!(await fileManager.pathExists(tasksDir))) {
      return [];
    }

    try {
      const glob = require("glob");
      const taskFiles = glob.sync("*.md", { cwd: tasksDir });
      return taskFiles.map(file => path.basename(file, ".md"));
    } catch (error) {
      console.warn(`Failed to read expansion pack tasks from ${packPath}: ${error.message}`);
      return [];
    }
  }

  async setupRoo(installDir, selectedAgent) {
    const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir);

    // Check for existing .roomodes file in project root
    const roomodesPath = path.join(installDir, ".roomodes");
    let existingModes = [];
    let existingContent = "";

    if (await fileManager.pathExists(roomodesPath)) {
      existingContent = await fileManager.readFile(roomodesPath);
      // Parse existing modes to avoid duplicates
      const modeMatches = existingContent.matchAll(/- slug: ([\w-]+)/g);
      for (const match of modeMatches) {
        existingModes.push(match[1]);
      }
      console.log(chalk.yellow(`Found existing .roomodes file with ${existingModes.length} modes`));
    }

    // Create new modes content
    let newModesContent = "";

    // Load dynamic agent permissions from configuration
    const config = await this.loadIdeAgentConfig();
    const agentPermissions = config['roo-permissions'] || {};

    for (const agentId of agents) {
      // Skip if already exists
      // Check both with and without bmad- prefix to handle both cases
      const checkSlug = agentId.startsWith('bmad-') ? agentId : `bmad-${agentId}`;
      if (existingModes.includes(checkSlug)) {
        console.log(chalk.dim(`Skipping ${agentId} - already exists in .roomodes`));
        continue;
      }

      // Read agent file to extract all information
      const agentPath = await this.findAgentPath(agentId, installDir);

      if (agentPath) {
        const agentContent = await fileManager.readFile(agentPath);

        // Extract YAML content
        const yamlMatch = agentContent.match(/```ya?ml\r?\n([\s\S]*?)```/);
        if (yamlMatch) {
          const yaml = yamlMatch[1];

          // Extract agent info from YAML
          const titleMatch = yaml.match(/title:\s*(.+)/);
          const iconMatch = yaml.match(/icon:\s*(.+)/);
          const whenToUseMatch = yaml.match(/whenToUse:\s*"(.+)"/);
          const roleDefinitionMatch = yaml.match(/roleDefinition:\s*"(.+)"/);

          const title = titleMatch ? titleMatch[1].trim() : await this.getAgentTitle(agentId, installDir);
          const icon = iconMatch ? iconMatch[1].trim() : "🤖";
          const whenToUse = whenToUseMatch ? whenToUseMatch[1].trim() : `Use for ${title} tasks`;
          const roleDefinition = roleDefinitionMatch
            ? roleDefinitionMatch[1].trim()
            : `You are a ${title} specializing in ${title.toLowerCase()} tasks and responsibilities.`;

          // Build mode entry with proper formatting (matching exact indentation)
          // Avoid double "bmad-" prefix for agents that already have it
          const slug = agentId.startsWith('bmad-') ? agentId : `bmad-${agentId}`;
          newModesContent += ` - slug: ${slug}\n`;
          newModesContent += `   name: '${icon} ${title}'\n`;
          newModesContent += `   roleDefinition: ${roleDefinition}\n`;
          newModesContent += `   whenToUse: ${whenToUse}\n`;
          // Get relative path from installDir to agent file
          const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/');
          newModesContent += `   customInstructions: CRITICAL Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`;
          newModesContent += `   groups:\n`;
          newModesContent += `    - read\n`;

          // Add permissions based on agent type
          const permissions = agentPermissions[agentId];
          if (permissions) {
            newModesContent += `    - - edit\n`;
            newModesContent += `      - fileRegex: ${permissions.fileRegex}\n`;
            newModesContent += `        description: ${permissions.description}\n`;
          } else {
            newModesContent += `    - edit\n`;
          }

          console.log(chalk.green(`✓ Added mode: bmad-${agentId} (${icon} ${title})`));
        }
      }
    }

    // Build final roomodes content
    let roomodesContent = "";
    if (existingContent) {
      // If there's existing content, append new modes to it
      roomodesContent = existingContent.trim() + "\n" + newModesContent;
    } else {
      // Create new .roomodes file with proper YAML structure
      roomodesContent = "customModes:\n" + newModesContent;
    }

    // Write .roomodes file
    await fileManager.writeFile(roomodesPath, roomodesContent);
    console.log(chalk.green("✓ Created .roomodes file in project root"));

    console.log(chalk.green(`\n✓ Roo Code setup complete!`));
    console.log(chalk.dim("Custom modes will be available when you open this project in Roo Code"));

    return true;
  }

  async setupCline(installDir, selectedAgent) {
    const clineRulesDir = path.join(installDir, ".clinerules");
    const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir);

    await fileManager.ensureDirectory(clineRulesDir);

    // Load dynamic agent ordering from configuration
    const config = await this.loadIdeAgentConfig();
    const agentOrder = config['cline-order'] || {};

    for (const agentId of agents) {
      // Find the agent file
      const agentPath = await this.findAgentPath(agentId, installDir);

      if (agentPath) {
        const agentContent = await fileManager.readFile(agentPath);

        // Get numeric prefix for ordering
        const order = agentOrder[agentId] || 99;
        const prefix = order.toString().padStart(2, '0');
        const mdPath = path.join(clineRulesDir, `${prefix}-${agentId}.md`);

        // Create MD content for Cline (focused on project standards and role)
        let mdContent = `# ${await this.getAgentTitle(agentId, installDir)} Agent\n\n`;
        mdContent += `This rule defines the ${await this.getAgentTitle(agentId, installDir)} persona and project standards.\n\n`;
        mdContent += "## Role Definition\n\n";
        mdContent +=
          "When the user types `@" + agentId + "`, adopt this persona and follow these guidelines:\n\n";
        mdContent += "```yaml\n";
        // Extract just the YAML content from the agent file
        const yamlContent = extractYamlFromAgent(agentContent);
        if (yamlContent) {
          mdContent += yamlContent;
        } else {
          // If no YAML found, include the whole content minus the header
          mdContent += agentContent.replace(/^#.*$/m, "").trim();
        }
        mdContent += "\n```\n\n";
        mdContent += "## Project Standards\n\n";
        mdContent += `- Always maintain consistency with project documentation in .bmad-core/\n`;
        mdContent += `- Follow the agent's specific guidelines and constraints\n`;
        mdContent += `- Update relevant project files when making changes\n`;
        const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/');
        mdContent += `- Reference the complete agent definition in [${relativePath}](${relativePath})\n\n`;
        mdContent += "## Usage\n\n";
        mdContent += `Type \`@${agentId}\` to activate this ${await this.getAgentTitle(agentId, installDir)} persona.\n`;

        await fileManager.writeFile(mdPath, mdContent);
        console.log(chalk.green(`✓ Created rule: ${prefix}-${agentId}.md`));
      }
    }

    console.log(chalk.green(`\n✓ Created Cline rules in ${clineRulesDir}`));

    return true;
  }

  async setupGeminiCli(installDir) {
    const geminiDir = path.join(installDir, ".gemini");
    const bmadMethodDir = path.join(geminiDir, "bmad-method");
    await fileManager.ensureDirectory(bmadMethodDir);

    // Update logic for existing settings.json
    const settingsPath = path.join(geminiDir, "settings.json");
    if (await fileManager.pathExists(settingsPath)) {
      try {
        const settingsContent = await fileManager.readFile(settingsPath);
        const settings = JSON.parse(settingsContent);
        let updated = false;

        // Handle contextFileName property
        if (settings.contextFileName && Array.isArray(settings.contextFileName)) {
          const originalLength = settings.contextFileName.length;
          settings.contextFileName = settings.contextFileName.filter(
            (fileName) => !fileName.startsWith("agents/")
          );
          if (settings.contextFileName.length !== originalLength) {
            updated = true;
          }
        }

        if (updated) {
          await fileManager.writeFile(
            settingsPath,
            JSON.stringify(settings, null, 2)
          );
          console.log(chalk.green("✓ Updated .gemini/settings.json - removed agent file references"));
        }
      } catch (error) {
        console.warn(
          chalk.yellow("Could not update .gemini/settings.json"),
          error
        );
      }
    }

    // Remove old agents directory
    const agentsDir = path.join(geminiDir, "agents");
    if (await fileManager.pathExists(agentsDir)) {
      await fileManager.removeDirectory(agentsDir);
      console.log(chalk.green("✓ Removed old .gemini/agents directory"));
    }

    // Get all available agents
    const agents = await this.getAllAgentIds(installDir);
    let concatenatedContent = "";

    for (const agentId of agents) {
      // Find the source agent file
      const agentPath = await this.findAgentPath(agentId, installDir);

      if (agentPath) {
        const agentContent = await fileManager.readFile(agentPath);

        // Create properly formatted agent rule content (similar to trae)
        let agentRuleContent = `# ${agentId.toUpperCase()} Agent Rule\n\n`;
        agentRuleContent += `This rule is triggered when the user types \`*${agentId}\` and activates the ${await this.getAgentTitle(
          agentId,
          installDir
        )} agent persona.\n\n`;
        agentRuleContent += "## Agent Activation\n\n";
        agentRuleContent +=
          "CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n";
        agentRuleContent += "```yaml\n";
        // Extract just the YAML content from the agent file
        const yamlContent = extractYamlFromAgent(agentContent);
        if (yamlContent) {
          agentRuleContent += yamlContent;
        }
        else {
          // If no YAML found, include the whole content minus the header
          agentRuleContent += agentContent.replace(/^#.*$/m, "").trim();
        }
        agentRuleContent += "\n```\n\n";
        agentRuleContent += "## File Reference\n\n";
        const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/');
        agentRuleContent += `The complete agent definition is available in [${relativePath}](${relativePath}).\n\n`;
        agentRuleContent += "## Usage\n\n";
        agentRuleContent += `When the user types \`*${agentId}\`, activate this ${await this.getAgentTitle(
          agentId,
          installDir
        )} persona and follow all instructions defined in the YAML configuration above.\n`;

        // Add to concatenated content with separator
        concatenatedContent += agentRuleContent + "\n\n---\n\n";
        console.log(chalk.green(`✓ Added context for @${agentId}`));
      }
    }

    // Write the concatenated content to GEMINI.md
    const geminiMdPath = path.join(bmadMethodDir, "GEMINI.md");
    await fileManager.writeFile(geminiMdPath, concatenatedContent);
    console.log(chalk.green(`\n✓ Created GEMINI.md in ${bmadMethodDir}`));

    return true;
  }

  async setupGitHubCopilot(installDir, selectedAgent, spinner = null, preConfiguredSettings = null) {
    // Configure VS Code workspace settings first to avoid UI conflicts with loading spinners
    await this.configureVsCodeSettings(installDir, spinner, preConfiguredSettings);

    const chatmodesDir = path.join(installDir, ".github", "chatmodes");
    const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir);

    await fileManager.ensureDirectory(chatmodesDir);

    for (const agentId of agents) {
      // Find the agent file
      const agentPath = await this.findAgentPath(agentId, installDir);
      const chatmodePath = path.join(chatmodesDir, `${agentId}.chatmode.md`);

      if (agentPath) {
        // Create chat mode file with agent content
        const agentContent = await fileManager.readFile(agentPath);
        const agentTitle = await this.getAgentTitle(agentId, installDir);

        // Extract whenToUse for the description
        const yamlMatch = agentContent.match(/```ya?ml\r?\n([\s\S]*?)```/);
        let description = `Activates the ${agentTitle} agent persona.`;
        if (yamlMatch) {
          const whenToUseMatch = yamlMatch[1].match(/whenToUse:\s*"(.*?)"/);
          if (whenToUseMatch && whenToUseMatch[1]) {
            description = whenToUseMatch[1];
          }
        }

        let chatmodeContent = `---
description: "${description.replace(/"/g, '\\"')}"
tools: ['changes', 'codebase', 'fetch', 'findTestFiles', 'githubRepo', 'problems', 'usages', 'editFiles', 'runCommands', 'runTasks', 'runTests', 'search', 'searchResults', 'terminalLastCommand', 'terminalSelection', 'testFailure']
---

`;
        chatmodeContent += agentContent;

        await fileManager.writeFile(chatmodePath, chatmodeContent);
        console.log(chalk.green(`✓ Created chat mode: ${agentId}.chatmode.md`));
      }
    }

    console.log(chalk.green(`\n✓ Github Copilot setup complete!`));
    console.log(chalk.dim(`You can now find the BMad agents in the Chat view's mode selector.`));

    return true;
  }

  async configureVsCodeSettings(installDir, spinner, preConfiguredSettings = null) {
    const vscodeDir = path.join(installDir, ".vscode");
    const settingsPath = path.join(vscodeDir, "settings.json");

    await fileManager.ensureDirectory(vscodeDir);

    // Read existing settings if they exist
    let existingSettings = {};
    if (await fileManager.pathExists(settingsPath)) {
      try {
        const existingContent = await fileManager.readFile(settingsPath);
        existingSettings = JSON.parse(existingContent);
        console.log(chalk.yellow("Found existing .vscode/settings.json. Merging BMad settings..."));
      } catch (error) {
        console.warn(chalk.yellow("Could not parse existing settings.json. Creating new one."));
        existingSettings = {};
      }
    }

    // Use pre-configured settings if provided, otherwise prompt
    let configChoice;
    if (preConfiguredSettings && preConfiguredSettings.configChoice) {
      configChoice = preConfiguredSettings.configChoice;
      console.log(chalk.dim(`Using pre-configured GitHub Copilot settings: ${configChoice}`));
    } else {
      // Clear any previous output and add spacing to avoid conflicts with loaders
      console.log('\n'.repeat(2));
      console.log(chalk.blue("🔧 Github Copilot Agent Settings Configuration"));
      console.log(chalk.dim("BMad works best with specific VS Code settings for optimal agent experience."));
      console.log(''); // Add extra spacing

      const response = await inquirer.prompt([
        {
          type: 'list',
          name: 'configChoice',
          message: chalk.yellow('How would you like to configure GitHub Copilot settings?'),
          choices: [
            {
              name: 'Use recommended defaults (fastest setup)',
              value: 'defaults'
            },
            {
              name: 'Configure each setting manually (customize to your preferences)',
              value: 'manual'
            },
            {
              name: 'Skip settings configuration (I\'ll configure manually later)',
              value: 'skip'
            }
          ],
          default: 'defaults'
        }
      ]);
      configChoice = response.configChoice;
    }

    let bmadSettings = {};

    if (configChoice === 'skip') {
      console.log(chalk.yellow("⚠  Skipping VS Code settings configuration."));
      console.log(chalk.dim("You can manually configure these settings in .vscode/settings.json:"));
      console.log(chalk.dim("  • chat.agent.enabled: true"));
      console.log(chalk.dim("  • chat.agent.maxRequests: 15"));
      console.log(chalk.dim("  • github.copilot.chat.agent.runTasks: true"));
      console.log(chalk.dim("  • chat.mcp.discovery.enabled: true"));
      console.log(chalk.dim("  • github.copilot.chat.agent.autoFix: true"));
      console.log(chalk.dim("  • chat.tools.autoApprove: false"));
      return true;
    }

    if (configChoice === 'defaults') {
      // Use recommended defaults
      bmadSettings = {
        "chat.agent.enabled": true,
        "chat.agent.maxRequests": 15,
        "github.copilot.chat.agent.runTasks": true,
        "chat.mcp.discovery.enabled": true,
        "github.copilot.chat.agent.autoFix": true,
        "chat.tools.autoApprove": false
      };
      console.log(chalk.green("✓ Using recommended BMad defaults for Github Copilot settings"));
    } else {
      // Manual configuration
      console.log(chalk.blue("\n📋 Let's configure each setting for your preferences:"));

      // Pause spinner during manual configuration prompts
      let spinnerWasActive = false;
      if (spinner && spinner.isSpinning) {
        spinner.stop();
        spinnerWasActive = true;
      }

      const manualSettings = await inquirer.prompt([
        {
          type: 'input',
          name: 'maxRequests',
          message: 'Maximum requests per agent session (recommended: 15)?',
          default: '15',
          validate: (input) => {
            const num = parseInt(input);
            if (isNaN(num) || num < 1 || num > 50) {
              return 'Please enter a number between 1 and 50';
            }
            return true;
          }
        },
        {
          type: 'confirm',
          name: 'runTasks',
          message: 'Allow agents to run workspace tasks (package.json scripts, etc.)?',
          default: true
        },
        {
          type: 'confirm',
          name: 'mcpDiscovery',
          message: 'Enable MCP (Model Context Protocol) server discovery?',
          default: true
        },
        {
          type: 'confirm',
          name: 'autoFix',
          message: 'Enable automatic error detection and fixing in generated code?',
          default: true
        },
        {
          type: 'confirm',
          name: 'autoApprove',
          message: 'Auto-approve ALL tools without confirmation? (⚠  EXPERIMENTAL - less secure)',
          default: false
        }
      ]);

      // Restart spinner if it was active before prompts
      if (spinner && spinnerWasActive) {
        spinner.start();
      }

      bmadSettings = {
        "chat.agent.enabled": true, // Always enabled - required for BMad agents
        "chat.agent.maxRequests": parseInt(manualSettings.maxRequests),
        "github.copilot.chat.agent.runTasks": manualSettings.runTasks,
        "chat.mcp.discovery.enabled": manualSettings.mcpDiscovery,
        "github.copilot.chat.agent.autoFix": manualSettings.autoFix,
        "chat.tools.autoApprove": manualSettings.autoApprove
      };

      console.log(chalk.green("✓ Custom settings configured"));
    }

    // Merge settings (existing settings take precedence to avoid overriding user preferences)
    const mergedSettings = { ...bmadSettings, ...existingSettings };

    // Write the updated settings
    await fileManager.writeFile(settingsPath, JSON.stringify(mergedSettings, null, 2));

    console.log(chalk.green("✓ VS Code workspace settings configured successfully"));
    console.log(chalk.dim("  Settings written to .vscode/settings.json:"));
    Object.entries(bmadSettings).forEach(([key, value]) => {
      console.log(chalk.dim(`  • ${key}: ${value}`));
    });
    console.log(chalk.dim(""));
    console.log(chalk.dim("You can modify these settings anytime in .vscode/settings.json"));
  }
}

module.exports = new IdeSetup();

const fs = require("node:fs").promises;
const path = require("node:path");
const DependencyResolver = require("../lib/dependency-resolver");
const yamlUtils = require("../lib/yaml-utils");

class WebBuilder {
  constructor(options = {}) {
    this.rootDir = options.rootDir || process.cwd();
    this.outputDirs = options.outputDirs || [path.join(this.rootDir, "dist")];
    this.resolver = new DependencyResolver(this.rootDir);
    this.templatePath = path.join(
      this.rootDir,
      "tools",
      "md-assets",
      "web-agent-startup-instructions.md"
    );
  }

  parseYaml(content) {
    const yaml = require("js-yaml");
    return yaml.load(content);
  }

  convertToWebPath(filePath, bundleRoot = 'bmad-core') {
    // Convert absolute paths to web bundle paths with dot prefix
    // All resources get installed under the bundle root, so use that path
    const relativePath = path.relative(this.rootDir, filePath);
    const pathParts = relativePath.split(path.sep);

    let resourcePath;
    if (pathParts[0] === 'expansion-packs') {
      // For expansion packs, remove 'expansion-packs/packname' and use the rest
      resourcePath = pathParts.slice(2).join('/');
    } else {
      // For bmad-core, common, etc., remove the first part
      resourcePath = pathParts.slice(1).join('/');
    }

    return `.${bundleRoot}/${resourcePath}`;
  }

  generateWebInstructions(bundleType, packName = null) {
    // Generate dynamic web instructions based on bundle type
    const rootExample = packName ? `.${packName}` : '.bmad-core';
    const examplePath = packName ? `.${packName}/folder/filename.md` : '.bmad-core/folder/filename.md';
    const personasExample = packName ? `.${packName}/personas/analyst.md` : '.bmad-core/personas/analyst.md';
    const tasksExample = packName ? `.${packName}/tasks/create-story.md` : '.bmad-core/tasks/create-story.md';
    const utilsExample = packName ? `.${packName}/utils/template-format.md` : '.bmad-core/utils/template-format.md';
    const tasksRef = packName ? `.${packName}/tasks/create-story.md` : '.bmad-core/tasks/create-story.md';

    return `# Web Agent Bundle Instructions

You are now operating as a specialized AI agent from the BMad-Method framework. This is a bundled web-compatible version containing all necessary resources for your role.

## Important Instructions

1. **Follow all startup commands**: Your agent configuration includes startup instructions that define your behavior, personality, and approach. These MUST be followed exactly.

2. **Resource Navigation**: This bundle contains all resources you need. Resources are marked with tags like:

- \`==================== START: ${examplePath} ====================\`
- \`==================== END: ${examplePath} ====================\`

When you need to reference a resource mentioned in your instructions:

- Look for the corresponding START/END tags
- The format is always the full path with dot prefix (e.g., \`${personasExample}\`, \`${tasksExample}\`)
- If a section is specified (e.g., \`{root}/tasks/create-story.md#section-name\`), navigate to that section within the file

**Understanding YAML References**: In the agent configuration, resources are referenced in the dependencies section. For example:

\`\`\`yaml
dependencies:
  utils:
    - template-format
  tasks:
    - create-story
\`\`\`

These references map directly to bundle sections:

- \`utils: template-format\` → Look for \`==================== START: ${utilsExample} ====================\`
- \`tasks: create-story\` → Look for \`==================== START: ${tasksRef} ====================\`

3. **Execution Context**: You are operating in a web environment. All your capabilities and knowledge are contained within this bundle. Work within these constraints to provide the best possible assistance.

4. **Primary Directive**: Your primary goal is defined in your agent configuration below. Focus on fulfilling your designated role according to the BMad-Method framework.

---

`;
  }

  async cleanOutputDirs() {
    for (const dir of this.outputDirs) {
      try {
        await fs.rm(dir, { recursive: true, force: true });
        console.log(`Cleaned: ${path.relative(this.rootDir, dir)}`);
      } catch (error) {
        console.debug(`Failed to clean directory ${dir}:`, error.message);
        // Directory might not exist, that's fine
      }
    }
  }

  async buildAgents() {
    const agents = await this.resolver.listAgents();

    for (const agentId of agents) {
      console.log(`  Building agent: ${agentId}`);
      const bundle = await this.buildAgentBundle(agentId);

      // Write to all output directories
      for (const outputDir of this.outputDirs) {
        const outputPath = path.join(outputDir, "agents");
        await fs.mkdir(outputPath, { recursive: true });
        const outputFile = path.join(outputPath, `${agentId}.txt`);
        await fs.writeFile(outputFile, bundle, "utf8");
      }
    }

    console.log(`Built ${agents.length} agent bundles in ${this.outputDirs.length} locations`);
  }

  async buildTeams() {
    const teams = await this.resolver.listTeams();

    for (const teamId of teams) {
      console.log(`  Building team: ${teamId}`);
      const bundle = await this.buildTeamBundle(teamId);

      // Write to all output directories
      for (const outputDir of this.outputDirs) {
        const outputPath = path.join(outputDir, "teams");
        await fs.mkdir(outputPath, { recursive: true });
        const outputFile = path.join(outputPath, `${teamId}.txt`);
        await fs.writeFile(outputFile, bundle, "utf8");
      }
    }

    console.log(`Built ${teams.length} team bundles in ${this.outputDirs.length} locations`);
  }

  async buildAgentBundle(agentId) {
    const dependencies = await this.resolver.resolveAgentDependencies(agentId);
    const template = this.generateWebInstructions('agent');

    const sections = [template];

    // Add agent configuration
    const agentPath = this.convertToWebPath(dependencies.agent.path, 'bmad-core');
    sections.push(this.formatSection(agentPath, dependencies.agent.content, 'bmad-core'));

    // Add all dependencies
    for (const resource of dependencies.resources) {
      const resourcePath = this.convertToWebPath(resource.path, 'bmad-core');
      sections.push(this.formatSection(resourcePath, resource.content, 'bmad-core'));
    }

    return sections.join("\n");
  }

  async buildTeamBundle(teamId) {
    const dependencies = await this.resolver.resolveTeamDependencies(teamId);
    const template = this.generateWebInstructions('team');

    const sections = [template];

    // Add team configuration
    const teamPath = this.convertToWebPath(dependencies.team.path, 'bmad-core');
    sections.push(this.formatSection(teamPath, dependencies.team.content, 'bmad-core'));

    // Add all agents
    for (const agent of dependencies.agents) {
      const agentPath = this.convertToWebPath(agent.path, 'bmad-core');
      sections.push(this.formatSection(agentPath, agent.content, 'bmad-core'));
    }

    // Add all deduplicated resources
    for (const resource of dependencies.resources) {
      const resourcePath = this.convertToWebPath(resource.path, 'bmad-core');
      sections.push(this.formatSection(resourcePath, resource.content, 'bmad-core'));
    }

    return sections.join("\n");
  }

  processAgentContent(content) {
    // First, replace content before YAML with the template
    const yamlContent = yamlUtils.extractYamlFromAgent(content);
    if (!yamlContent) return content;

    const yamlMatch = content.match(/```ya?ml\n([\s\S]*?)\n```/);
    if (!yamlMatch) return content;

    const yamlStartIndex = content.indexOf(yamlMatch[0]);
    const yamlEndIndex = yamlStartIndex + yamlMatch[0].length;

    // Parse YAML and remove root and IDE-FILE-RESOLUTION properties
    try {
      const yaml = require("js-yaml");
      const parsed = yaml.load(yamlContent);

      // Remove the properties if they exist at root level
      delete parsed.root;
      delete parsed["IDE-FILE-RESOLUTION"];
      delete parsed["REQUEST-RESOLUTION"];

      // Also remove from activation-instructions if they exist
      if (parsed["activation-instructions"] && Array.isArray(parsed["activation-instructions"])) {
        parsed["activation-instructions"] = parsed["activation-instructions"].filter(
          (instruction) => {
            return (
              typeof instruction === 'string' &&
              !instruction.startsWith("IDE-FILE-RESOLUTION:") &&
              !instruction.startsWith("REQUEST-RESOLUTION:")
            );
          }
        );
      }

      // Reconstruct the YAML
      const cleanedYaml = yaml.dump(parsed, { lineWidth: -1 });

      // Get the agent name from the YAML for the header
      const agentName = parsed.agent?.id || "agent";

      // Build the new content with just the agent header and YAML
      const newHeader = `# ${agentName}\n\nCRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n`;
      const afterYaml = content.substring(yamlEndIndex);

      return newHeader + "```yaml\n" + cleanedYaml.trim() + "\n```" + afterYaml;
    } catch (error) {
      console.warn("Failed to process agent YAML:", error.message);
      // If parsing fails, return original content
      return content;
    }
  }

  formatSection(path, content, bundleRoot = 'bmad-core') {
    const separator = "====================";

    // Process agent content if this is an agent file
    if (path.includes("/agents/")) {
      content = this.processAgentContent(content);
    }

    // Replace {root} references with the actual bundle root
    content = this.replaceRootReferences(content, bundleRoot);

    return [
      `${separator} START: ${path} ${separator}`,
      content.trim(),
      `${separator} END: ${path} ${separator}`,
      "",
    ].join("\n");
  }

  replaceRootReferences(content, bundleRoot) {
    // Replace {root} with the appropriate bundle root path
    return content.replace(/\{root\}/g, `.${bundleRoot}`);
  }

  async validate() {
    console.log("Validating agent configurations...");
    const agents = await this.resolver.listAgents();
    for (const agentId of agents) {
      try {
        await this.resolver.resolveAgentDependencies(agentId);
        console.log(`  ✓ ${agentId}`);
      } catch (error) {
        console.log(`  ✗ ${agentId}: ${error.message}`);
        throw error;
      }
    }

    console.log("\nValidating team configurations...");
    const teams = await this.resolver.listTeams();
    for (const teamId of teams) {
      try {
        await this.resolver.resolveTeamDependencies(teamId);
        console.log(`  ✓ ${teamId}`);
      } catch (error) {
        console.log(`  ✗ ${teamId}: ${error.message}`);
        throw error;
      }
    }
  }

  async buildAllExpansionPacks(options = {}) {
    const expansionPacks = await this.listExpansionPacks();

    for (const packName of expansionPacks) {
      console.log(`  Building expansion pack: ${packName}`);
      await this.buildExpansionPack(packName, options);
    }

    console.log(`Built ${expansionPacks.length} expansion pack bundles`);
  }

  async buildExpansionPack(packName, options = {}) {
    const packDir = path.join(this.rootDir, "expansion-packs", packName);
    const outputDirs = [path.join(this.rootDir, "dist", "expansion-packs", packName)];

    // Clean output directories if requested
    if (options.clean !== false) {
      for (const outputDir of outputDirs) {
        try {
          await fs.rm(outputDir, { recursive: true, force: true });
        } catch (error) {
          // Directory might not exist, that's fine
        }
      }
    }

    // Build individual agents first
    const agentsDir = path.join(packDir, "agents");
    try {
      const agentFiles = await fs.readdir(agentsDir);
      const agentMarkdownFiles = agentFiles.filter((f) => f.endsWith(".md"));

      if (agentMarkdownFiles.length > 0) {
        console.log(`    Building individual agents for ${packName}:`);

        for (const agentFile of agentMarkdownFiles) {
          const agentName = agentFile.replace(".md", "");
          console.log(`      - ${agentName}`);

          // Build individual agent bundle
          const bundle = await this.buildExpansionAgentBundle(packName, packDir, agentName);

          // Write to all output directories
          for (const outputDir of outputDirs) {
            const agentsOutputDir = path.join(outputDir, "agents");
            await fs.mkdir(agentsOutputDir, { recursive: true });
            const outputFile = path.join(agentsOutputDir, `${agentName}.txt`);
            await fs.writeFile(outputFile, bundle, "utf8");
          }
        }
      }
    } catch (error) {
      console.debug(`    No agents directory found for ${packName}`);
    }

    // Build team bundle
    const agentTeamsDir = path.join(packDir, "agent-teams");
    try {
      const teamFiles = await fs.readdir(agentTeamsDir);
      const teamFile = teamFiles.find((f) => f.endsWith(".yaml"));

      if (teamFile) {
        console.log(`    Building team bundle for ${packName}`);
        const teamConfigPath = path.join(agentTeamsDir, teamFile);

        // Build expansion pack as a team bundle
        const bundle = await this.buildExpansionTeamBundle(packName, packDir, teamConfigPath);

        // Write to all output directories
        for (const outputDir of outputDirs) {
          const teamsOutputDir = path.join(outputDir, "teams");
          await fs.mkdir(teamsOutputDir, { recursive: true });
          const outputFile = path.join(teamsOutputDir, teamFile.replace(".yaml", ".txt"));
          await fs.writeFile(outputFile, bundle, "utf8");
          console.log(`    ✓ Created bundle: ${path.relative(this.rootDir, outputFile)}`);
        }
      } else {
        console.warn(`    ⚠ No team configuration found in ${packName}/agent-teams/`);
      }
    } catch (error) {
      console.warn(`    ⚠ No agent-teams directory found for ${packName}`);
    }
  }

  async buildExpansionAgentBundle(packName, packDir, agentName) {
    const template = this.generateWebInstructions('expansion-agent', packName);
    const sections = [template];

    // Add agent configuration
    const agentPath = path.join(packDir, "agents", `${agentName}.md`);
    const agentContent = await fs.readFile(agentPath, "utf8");
    const agentWebPath = this.convertToWebPath(agentPath, packName);
    sections.push(this.formatSection(agentWebPath, agentContent, packName));

    // Resolve and add agent dependencies
    const yamlContent = yamlUtils.extractYamlFromAgent(agentContent);
    if (yamlContent) {
      try {
        const yaml = require("js-yaml");
        const agentConfig = yaml.load(yamlContent);

        if (agentConfig.dependencies) {
          // Add resources, first try expansion pack, then core
          for (const [resourceType, resources] of Object.entries(agentConfig.dependencies)) {
            if (Array.isArray(resources)) {
              for (const resourceName of resources) {
                let found = false;

                // Try expansion pack first
                const resourcePath = path.join(packDir, resourceType, resourceName);
                try {
                  const resourceContent = await fs.readFile(resourcePath, "utf8");
                  const resourceWebPath = this.convertToWebPath(resourcePath, packName);
                  sections.push(
                    this.formatSection(resourceWebPath, resourceContent, packName)
                  );
                  found = true;
                } catch (error) {
                  // Not in expansion pack, continue
                }

                // If not found in expansion pack, try core
                if (!found) {
                  const corePath = path.join(
                    this.rootDir,
                    "bmad-core",
                    resourceType,
                    resourceName
                  );
                  try {
                    const coreContent = await fs.readFile(corePath, "utf8");
                    const coreWebPath = this.convertToWebPath(corePath, packName);
                    sections.push(
                      this.formatSection(coreWebPath, coreContent, packName)
                    );
                    found = true;
                  } catch (error) {
                    // Not in core either, continue
                  }
                }

                // If not found in core, try common folder
                if (!found) {
                  const commonPath = path.join(
                    this.rootDir,
                    "common",
                    resourceType,
                    resourceName
                  );
                  try {
                    const commonContent = await fs.readFile(commonPath, "utf8");
                    const commonWebPath = this.convertToWebPath(commonPath, packName);
                    sections.push(
                      this.formatSection(commonWebPath, commonContent, packName)
                    );
                    found = true;
                  } catch (error) {
                    // Not in common either, continue
                  }
                }

                if (!found) {
                  console.warn(
                    `    ⚠ Dependency ${resourceType}#${resourceName} not found in expansion pack or core`
                  );
                }
              }
            }
          }
        }
      } catch (error) {
        console.debug(`Failed to parse agent YAML for ${agentName}:`, error.message);
      }
    }

    return sections.join("\n");
  }

  async buildExpansionTeamBundle(packName, packDir, teamConfigPath) {
    const template = this.generateWebInstructions('expansion-team', packName);

    const sections = [template];

    // Add team configuration and parse to get agent list
    const teamContent = await fs.readFile(teamConfigPath, "utf8");
    const teamFileName = path.basename(teamConfigPath, ".yaml");
    const teamConfig = this.parseYaml(teamContent);
    const teamWebPath = this.convertToWebPath(teamConfigPath, packName);
    sections.push(this.formatSection(teamWebPath, teamContent, packName));

    // Get list of expansion pack agents
    const expansionAgents = new Set();
    const agentsDir = path.join(packDir, "agents");
    try {
      const agentFiles = await fs.readdir(agentsDir);
      for (const agentFile of agentFiles.filter((f) => f.endsWith(".md"))) {
        const agentName = agentFile.replace(".md", "");
        expansionAgents.add(agentName);
      }
    } catch (error) {
      console.warn(`    ⚠ No agents directory found in ${packName}`);
    }

    // Build a map of all available expansion pack resources for override checking
    const expansionResources = new Map();
    const resourceDirs = ["templates", "tasks", "checklists", "workflows", "data"];
    for (const resourceDir of resourceDirs) {
      const resourcePath = path.join(packDir, resourceDir);
      try {
        const resourceFiles = await fs.readdir(resourcePath);
        for (const resourceFile of resourceFiles.filter(
          (f) => f.endsWith(".md") || f.endsWith(".yaml")
        )) {
          expansionResources.set(`${resourceDir}#${resourceFile}`, true);
        }
      } catch (error) {
        // Directory might not exist, that's fine
      }
    }

    // Process all agents listed in team configuration
    const agentsToProcess = teamConfig.agents || [];

    // Ensure bmad-orchestrator is always included for teams
    if (!agentsToProcess.includes("bmad-orchestrator")) {
      console.warn(`    ⚠ Team ${teamFileName} missing bmad-orchestrator, adding automatically`);
      agentsToProcess.unshift("bmad-orchestrator");
    }

    // Track all dependencies from all agents (deduplicated)
    const allDependencies = new Map();

    for (const agentId of agentsToProcess) {
      if (expansionAgents.has(agentId)) {
        // Use expansion pack version (override)
        const agentPath = path.join(agentsDir, `${agentId}.md`);
        const agentContent = await fs.readFile(agentPath, "utf8");
        const expansionAgentWebPath = this.convertToWebPath(agentPath, packName);
        sections.push(this.formatSection(expansionAgentWebPath, agentContent, packName));

        // Parse and collect dependencies from expansion agent
        const agentYaml = agentContent.match(/```yaml\n([\s\S]*?)\n```/);
        if (agentYaml) {
          try {
            const agentConfig = this.parseYaml(agentYaml[1]);
            if (agentConfig.dependencies) {
              for (const [resourceType, resources] of Object.entries(agentConfig.dependencies)) {
                if (Array.isArray(resources)) {
                  for (const resourceName of resources) {
                    const key = `${resourceType}#${resourceName}`;
                    if (!allDependencies.has(key)) {
                      allDependencies.set(key, { type: resourceType, name: resourceName });
                    }
                  }
                }
              }
            }
          } catch (error) {
            console.debug(`Failed to parse agent YAML for ${agentId}:`, error.message);
          }
        }
      } else {
        // Use core BMad version
        try {
          const coreAgentPath = path.join(this.rootDir, "bmad-core", "agents", `${agentId}.md`);
          const coreAgentContent = await fs.readFile(coreAgentPath, "utf8");
          const coreAgentWebPath = this.convertToWebPath(coreAgentPath, packName);
          sections.push(this.formatSection(coreAgentWebPath, coreAgentContent, packName));

          // Parse and collect dependencies from core agent
          const yamlContent = yamlUtils.extractYamlFromAgent(coreAgentContent, true);
          if (yamlContent) {
            try {
              const agentConfig = this.parseYaml(yamlContent);
              if (agentConfig.dependencies) {
                for (const [resourceType, resources] of Object.entries(agentConfig.dependencies)) {
                  if (Array.isArray(resources)) {
                    for (const resourceName of resources) {
                      const key = `${resourceType}#${resourceName}`;
                      if (!allDependencies.has(key)) {
                        allDependencies.set(key, { type: resourceType, name: resourceName });
                      }
                    }
                  }
                }
              }
            } catch (error) {
              console.debug(`Failed to parse agent YAML for ${agentId}:`, error.message);
            }
          }
        } catch (error) {
          console.warn(`    ⚠ Agent ${agentId} not found in core or expansion pack`);
        }
      }
    }

    // Add all collected dependencies from agents
    // Always prefer expansion pack versions if they exist
    for (const [key, dep] of allDependencies) {
      let found = false;

      // Always check expansion pack first, even if the dependency came from a core agent
      if (expansionResources.has(key)) {
        // We know it exists in expansion pack, find and load it
        const expansionPath = path.join(packDir, dep.type, dep.name);
        try {
          const content = await fs.readFile(expansionPath, "utf8");
          const expansionWebPath = this.convertToWebPath(expansionPath, packName);
          sections.push(this.formatSection(expansionWebPath, content, packName));
          console.log(`      ✓ Using expansion override for ${key}`);
          found = true;
        } catch (error) {
          // Try next extension
        }
      }

      // If not found in expansion pack (or doesn't exist there), try core
      if (!found) {
        const corePath = path.join(this.rootDir, "bmad-core", dep.type, dep.name);
        try {
          const content = await fs.readFile(corePath, "utf8");
          const coreWebPath = this.convertToWebPath(corePath, packName);
          sections.push(this.formatSection(coreWebPath, content, packName));
          found = true;
        } catch (error) {
          // Not in core either, continue
        }
      }

      // If not found in core, try common folder
      if (!found) {
        const commonPath = path.join(this.rootDir, "common", dep.type, dep.name);
        try {
          const content = await fs.readFile(commonPath, "utf8");
          const commonWebPath = this.convertToWebPath(commonPath, packName);
          sections.push(this.formatSection(commonWebPath, content, packName));
          found = true;
        } catch (error) {
          // Not in common either, continue
        }
      }

      if (!found) {
        console.warn(`    ⚠ Dependency ${key} not found in expansion pack or core`);
      }
    }

    // Add remaining expansion pack resources not already included as dependencies
    for (const resourceDir of resourceDirs) {
      const resourcePath = path.join(packDir, resourceDir);
      try {
        const resourceFiles = await fs.readdir(resourcePath);
        for (const resourceFile of resourceFiles.filter(
          (f) => f.endsWith(".md") || f.endsWith(".yaml")
        )) {
          const filePath = path.join(resourcePath, resourceFile);
          const fileContent = await fs.readFile(filePath, "utf8");
          const fileName = resourceFile.replace(/\.(md|yaml)$/, "");

          // Only add if not already included as a dependency
          const resourceKey = `${resourceDir}#${fileName}`;
          if (!allDependencies.has(resourceKey)) {
            const fullResourcePath = path.join(resourcePath, resourceFile);
            const resourceWebPath = this.convertToWebPath(fullResourcePath, packName);
            sections.push(this.formatSection(resourceWebPath, fileContent, packName));
          }
        }
      } catch (error) {
        // Directory might not exist, that's fine
      }
    }

    return sections.join("\n");
  }

  async listExpansionPacks() {
    const expansionPacksDir = path.join(this.rootDir, "expansion-packs");
    try {
      const entries = await fs.readdir(expansionPacksDir, { withFileTypes: true });
      return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
    } catch (error) {
      console.warn("No expansion-packs directory found");
      return [];
    }
  }

  listAgents() {
    return this.resolver.listAgents();
  }
}

module.exports = WebBuilder;

const fs = require("fs-extra");
const path = require("path");
const crypto = require("crypto");
const yaml = require("js-yaml");
const chalk = require("chalk");
const { createReadStream, createWriteStream, promises: fsPromises } = require('fs');
const { pipeline } = require('stream/promises');
const resourceLocator = require('./resource-locator');

class FileManager {
  constructor() {
    this.manifestDir = ".bmad-core";
    this.manifestFile = "install-manifest.yaml";
  }

  async copyFile(source, destination) {
    try {
      await fs.ensureDir(path.dirname(destination));

      // Use streaming for large files (> 10MB)
      const stats = await fs.stat(source);
      if (stats.size > 10 * 1024 * 1024) {
        await pipeline(
          createReadStream(source),
          createWriteStream(destination)
        );
      } else {
        await fs.copy(source, destination);
      }
      return true;
    } catch (error) {
      console.error(chalk.red(`Failed to copy ${source}:`), error.message);
      return false;
    }
  }

  async copyDirectory(source, destination) {
    try {
      await fs.ensureDir(destination);

      // Use streaming copy for large directories
      const files = await resourceLocator.findFiles('**/*', {
        cwd: source,
        nodir: true
      });

      // Process files in batches to avoid memory issues
      const batchSize = 50;
      for (let i = 0; i < files.length; i += batchSize) {
        const batch = files.slice(i, i + batchSize);
        await Promise.all(
          batch.map(file =>
            this.copyFile(
              path.join(source, file),
              path.join(destination, file)
            )
          )
        );
      }
      return true;
    } catch (error) {
      console.error(
        chalk.red(`Failed to copy directory ${source}:`),
        error.message
      );
      return false;
    }
  }

  async copyGlobPattern(pattern, sourceDir, destDir, rootValue = null) {
    const files = await resourceLocator.findFiles(pattern, { cwd: sourceDir });
    const copied = [];

    for (const file of files) {
      const sourcePath = path.join(sourceDir, file);
      const destPath = path.join(destDir, file);

      // Use root replacement if rootValue is provided and file needs it
      const needsRootReplacement = rootValue && (file.endsWith('.md') || file.endsWith('.yaml') || file.endsWith('.yml'));

      let success = false;
      if (needsRootReplacement) {
        success = await this.copyFileWithRootReplacement(sourcePath, destPath, rootValue);
      } else {
        success = await this.copyFile(sourcePath, destPath);
      }

      if (success) {
        copied.push(file);
      }
    }

    return copied;
  }

  async calculateFileHash(filePath) {
    try {
      // Use streaming for hash calculation to reduce memory usage
      const stream = createReadStream(filePath);
      const hash = crypto.createHash("sha256");

      for await (const chunk of stream) {
        hash.update(chunk);
      }

      return hash.digest("hex").slice(0, 16);
    } catch (error) {
      return null;
    }
  }

  async createManifest(installDir, config, files) {
    const manifestPath = path.join(
      installDir,
      this.manifestDir,
      this.manifestFile
    );

    // Read version from package.json
    let coreVersion = "unknown";
    try {
      const packagePath = path.join(__dirname, '..', '..', '..', 'package.json');
      const packageJson = require(packagePath);
      coreVersion = packageJson.version;
    } catch (error) {
      console.warn("Could not read version from package.json, using 'unknown'");
    }

    const manifest = {
      version: coreVersion,
      installed_at: new Date().toISOString(),
      install_type: config.installType,
      agent: config.agent || null,
      ides_setup: config.ides || [],
      expansion_packs: config.expansionPacks || [],
      files: [],
    };

    // Add file information
    for (const file of files) {
      const filePath = path.join(installDir, file);
      const hash = await this.calculateFileHash(filePath);

      manifest.files.push({
        path: file,
        hash: hash,
        modified: false,
      });
    }

    // Write manifest
    await fs.ensureDir(path.dirname(manifestPath));
    await fs.writeFile(manifestPath, yaml.dump(manifest, { indent: 2 }));

    return manifest;
  }

  async readManifest(installDir) {
    const manifestPath = path.join(
      installDir,
      this.manifestDir,
      this.manifestFile
    );

    try {
      const content = await fs.readFile(manifestPath, "utf8");
      return yaml.load(content);
    } catch (error) {
      return null;
    }
  }

  async readExpansionPackManifest(installDir, packId) {
    const manifestPath = path.join(
      installDir,
      `.${packId}`,
      this.manifestFile
    );

    try {
      const content = await fs.readFile(manifestPath, "utf8");
      return yaml.load(content);
    } catch (error) {
      return null;
    }
  }

  async checkModifiedFiles(installDir, manifest) {
    const modified = [];

    for (const file of manifest.files) {
      const filePath = path.join(installDir, file.path);
      const currentHash = await this.calculateFileHash(filePath);

      if (currentHash && currentHash !== file.hash) {
        modified.push(file.path);
      }
    }

    return modified;
  }

  async checkFileIntegrity(installDir, manifest) {
    const result = {
      missing: [],
      modified: []
    };

    for (const file of manifest.files) {
      const filePath = path.join(installDir, file.path);

      // Skip checking the manifest file itself - it will always be different due to timestamps
      if (file.path.endsWith('install-manifest.yaml')) {
        continue;
      }

      if (!(await this.pathExists(filePath))) {
        result.missing.push(file.path);
      } else {
        const currentHash = await this.calculateFileHash(filePath);
        if (currentHash && currentHash !== file.hash) {
          result.modified.push(file.path);
        }
      }
    }

    return result;
  }

  async backupFile(filePath) {
    const backupPath = filePath + ".bak";
    let counter = 1;
    let finalBackupPath = backupPath;

    // Find a unique backup filename
    while (await fs.pathExists(finalBackupPath)) {
      finalBackupPath = `${filePath}.bak${counter}`;
      counter++;
    }

    await fs.copy(filePath, finalBackupPath);
    return finalBackupPath;
  }

  async ensureDirectory(dirPath) {
    try {
      await fs.ensureDir(dirPath);
      return true;
    } catch (error) {
      throw error;
    }
  }

  async pathExists(filePath) {
    return fs.pathExists(filePath);
  }

  async readFile(filePath) {
    return fs.readFile(filePath, "utf8");
  }

  async writeFile(filePath, content) {
    await fs.ensureDir(path.dirname(filePath));
    await fs.writeFile(filePath, content);
  }

  async removeDirectory(dirPath) {
    await fs.remove(dirPath);
  }

  async createExpansionPackManifest(installDir, packId, config, files) {
    const manifestPath = path.join(
      installDir,
      `.${packId}`,
      this.manifestFile
    );

    const manifest = {
      version: config.expansionPackVersion || require("../../../package.json").version,
      installed_at: new Date().toISOString(),
      install_type: config.installType,
      expansion_pack_id: config.expansionPackId,
      expansion_pack_name: config.expansionPackName,
      ides_setup: config.ides || [],
      files: [],
    };

    // Add file information
    for (const file of files) {
      const filePath = path.join(installDir, file);
      const hash = await this.calculateFileHash(filePath);

      manifest.files.push({
        path: file,
        hash: hash,
        modified: false,
      });
    }

    // Write manifest
    await fs.ensureDir(path.dirname(manifestPath));
    await fs.writeFile(manifestPath, yaml.dump(manifest, { indent: 2 }));

    return manifest;
  }

  async modifyCoreConfig(installDir, config) {
    const coreConfigPath = path.join(installDir, '.bmad-core', 'core-config.yaml');

    try {
      // Read the existing core-config.yaml
      const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
      const coreConfig = yaml.load(coreConfigContent);

      // Modify sharding settings if provided
      if (config.prdSharded !== undefined) {
        coreConfig.prd.prdSharded = config.prdSharded;
      }

      if (config.architectureSharded !== undefined) {
        coreConfig.architecture.architectureSharded = config.architectureSharded;
      }

      // Write back the modified config
      await fs.writeFile(coreConfigPath, yaml.dump(coreConfig, { indent: 2 }));

      return true;
    } catch (error) {
      console.error(chalk.red(`Failed to modify core-config.yaml:`), error.message);
      return false;
    }
  }

  async copyFileWithRootReplacement(source, destination, rootValue) {
    try {
      // Check file size to determine if we should stream
      const stats = await fs.stat(source);

      if (stats.size > 5 * 1024 * 1024) { // 5MB threshold
        // Use streaming for large files
        const { Transform } = require('stream');
        const replaceStream = new Transform({
          transform(chunk, encoding, callback) {
            const modified = chunk.toString().replace(/\{root\}/g, rootValue);
            callback(null, modified);
          }
        });

        await this.ensureDirectory(path.dirname(destination));
        await pipeline(
          createReadStream(source, { encoding: 'utf8' }),
          replaceStream,
          createWriteStream(destination, { encoding: 'utf8' })
        );
      } else {
        // Regular approach for smaller files
        const content = await fsPromises.readFile(source, 'utf8');
        const updatedContent = content.replace(/\{root\}/g, rootValue);
        await this.ensureDirectory(path.dirname(destination));
        await fsPromises.writeFile(destination, updatedContent, 'utf8');
      }

      return true;
    } catch (error) {
      console.error(chalk.red(`Failed to copy ${source} with root replacement:`), error.message);
      return false;
    }
  }

  async copyDirectoryWithRootReplacement(source, destination, rootValue, fileExtensions = ['.md', '.yaml', '.yml']) {
    try {
      await this.ensureDirectory(destination);

      // Get all files in source directory
      const files = await resourceLocator.findFiles('**/*', {
        cwd: source,
        nodir: true
      });

      let replacedCount = 0;

      for (const file of files) {
        const sourcePath = path.join(source, file);
        const destPath = path.join(destination, file);

        // Check if this file type should have {root} replacement
        const shouldReplace = fileExtensions.some(ext => file.endsWith(ext));

        if (shouldReplace) {
          if (await this.copyFileWithRootReplacement(sourcePath, destPath, rootValue)) {
            replacedCount++;
          }
        } else {
          // Regular copy for files that don't need replacement
          await this.copyFile(sourcePath, destPath);
        }
      }

      if (replacedCount > 0) {
        console.log(chalk.dim(`  Processed ${replacedCount} files with {root} replacement`));
      }

      return true;
    } catch (error) {
      console.error(chalk.red(`Failed to copy directory ${source} with root replacement:`), error.message);
      return false;
    }
  }
}

module.exports = new FileManager();

/**
 * Base IDE Setup - Common functionality for all IDE setups
 * Reduces duplication and provides shared methods
 */

const path = require("path");
const fs = require("fs-extra");
const yaml = require("js-yaml");
const chalk = require("chalk");
const fileManager = require("./file-manager");
const resourceLocator = require("./resource-locator");
const { extractYamlFromAgent } = require("../../lib/yaml-utils");

class BaseIdeSetup {
  constructor() {
    this._agentCache = new Map();
    this._pathCache = new Map();
  }

  /**
   * Get all agent IDs with caching
   */
  async getAllAgentIds(installDir) {
    const cacheKey = `all-agents:${installDir}`;
    if (this._agentCache.has(cacheKey)) {
      return this._agentCache.get(cacheKey);
    }

    const allAgents = new Set();

    // Get core agents
    const coreAgents = await this.getCoreAgentIds(installDir);
    coreAgents.forEach(id => allAgents.add(id));

    // Get expansion pack agents
    const expansionPacks = await this.getInstalledExpansionPacks(installDir);
    for (const pack of expansionPacks) {
      const packAgents = await this.getExpansionPackAgents(pack.path);
      packAgents.forEach(id => allAgents.add(id));
    }

    const result = Array.from(allAgents);
    this._agentCache.set(cacheKey, result);
    return result;
  }

  /**
   * Get core agent IDs
   */
  async getCoreAgentIds(installDir) {
    const coreAgents = [];
    const corePaths = [
      path.join(installDir, ".bmad-core", "agents"),
      path.join(installDir, "bmad-core", "agents")
    ];

    for (const agentsDir of corePaths) {
      if (await fileManager.pathExists(agentsDir)) {
        const files = await resourceLocator.findFiles("*.md", { cwd: agentsDir });
        coreAgents.push(...files.map(file => path.basename(file, ".md")));
        break; // Use first found
      }
    }

    return coreAgents;
  }

  /**
   * Find agent path with caching
   */
  async findAgentPath(agentId, installDir) {
    const cacheKey = `agent-path:${agentId}:${installDir}`;
    if (this._pathCache.has(cacheKey)) {
      return this._pathCache.get(cacheKey);
    }

    // Use resource locator for efficient path finding
    let agentPath = await resourceLocator.getAgentPath(agentId);

    if (!agentPath) {
      // Check installation-specific paths
      const possiblePaths = [
        path.join(installDir, ".bmad-core", "agents", `${agentId}.md`),
        path.join(installDir, "bmad-core", "agents", `${agentId}.md`),
        path.join(installDir, "common", "agents", `${agentId}.md`)
      ];

      for (const testPath of possiblePaths) {
        if (await fileManager.pathExists(testPath)) {
          agentPath = testPath;
          break;
        }
      }
    }

    if (agentPath) {
      this._pathCache.set(cacheKey, agentPath);
    }
    return agentPath;
  }

  /**
   * Get agent title from metadata
   */
  async getAgentTitle(agentId, installDir) {
    const agentPath = await this.findAgentPath(agentId, installDir);
    if (!agentPath) return agentId;

    try {
      const content = await fileManager.readFile(agentPath);
      const yamlContent = extractYamlFromAgent(content);
      if (yamlContent) {
        const metadata = yaml.load(yamlContent);
        return metadata.agent_name || agentId;
      }
    } catch (error) {
      // Fallback to agent ID
    }
    return agentId;
  }

  /**
   * Get installed expansion packs
   */
  async getInstalledExpansionPacks(installDir) {
    const cacheKey = `expansion-packs:${installDir}`;
    if (this._pathCache.has(cacheKey)) {
      return this._pathCache.get(cacheKey);
    }

    const expansionPacks = [];

    // Check for dot-prefixed expansion packs
    const dotExpansions = await resourceLocator.findFiles(".bmad-*", { cwd: installDir });

    for (const dotExpansion of dotExpansions) {
      if (dotExpansion !== ".bmad-core") {
        const packPath = path.join(installDir, dotExpansion);
        const packName = dotExpansion.substring(1); // remove the dot
        expansionPacks.push({
          name: packName,
          path: packPath
        });
      }
    }

    // Check other dot folders that have config.yaml
    const allDotFolders = await resourceLocator.findFiles(".*", { cwd: installDir });
    for (const folder of allDotFolders) {
      if (!folder.startsWith(".bmad-") && folder !== ".bmad-core") {
        const packPath = path.join(installDir, folder);
        const configPath = path.join(packPath, "config.yaml");
        if (await fileManager.pathExists(configPath)) {
          expansionPacks.push({
            name: folder.substring(1), // remove the dot
            path: packPath
          });
        }
      }
    }

    this._pathCache.set(cacheKey, expansionPacks);
    return expansionPacks;
  }

  /**
   * Get expansion pack agents
   */
  async getExpansionPackAgents(packPath) {
    const agentsDir = path.join(packPath, "agents");
    if (!(await fileManager.pathExists(agentsDir))) {
      return [];
    }

    const agentFiles = await resourceLocator.findFiles("*.md", { cwd: agentsDir });
    return agentFiles.map(file => path.basename(file, ".md"));
  }

  /**
   * Create agent rule content (shared logic)
   */
  async createAgentRuleContent(agentId, agentPath, installDir, format = 'mdc') {
    const agentContent = await fileManager.readFile(agentPath);
    const agentTitle = await this.getAgentTitle(agentId, installDir);
    const yamlContent = extractYamlFromAgent(agentContent);

    let content = "";

    if (format === 'mdc') {
      // MDC format for Cursor
      content = "---\n";
      content += "description: \n";
      content += "globs: []\n";
      content += "alwaysApply: false\n";
      content += "---\n\n";
      content += `# ${agentId.toUpperCase()} Agent Rule\n\n`;
      content += `This rule is triggered when the user types \`@${agentId}\` and activates the ${agentTitle} agent persona.\n\n`;
      content += "## Agent Activation\n\n";
      content += "CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n";
      content += "```yaml\n";
      content += yamlContent || agentContent.replace(/^#.*$/m, "").trim();
      content += "\n```\n\n";
      content += "## File Reference\n\n";
      const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/');
      content += `The complete agent definition is available in [${relativePath}](mdc:${relativePath}).\n\n`;
      content += "## Usage\n\n";
      content += `When the user types \`@${agentId}\`, activate this ${agentTitle} persona and follow all instructions defined in the YAML configuration above.\n`;
    } else if (format === 'claude') {
      // Claude Code format
      content = `# /${agentId} Command\n\n`;
      content += `When this command is used, adopt the following agent persona:\n\n`;
      content += agentContent;
    }

    return content;
  }

  /**
   * Clear all caches
   */
  clearCache() {
    this._agentCache.clear();
    this._pathCache.clear();
  }
}

module.exports = BaseIdeSetup;

#!/usr/bin/env node

const { Command } = require('commander');
const WebBuilder = require('./builders/web-builder');
const V3ToV4Upgrader = require('./upgraders/v3-to-v4-upgrader');
const IdeSetup = require('./installer/lib/ide-setup');
const path = require('path');

const program = new Command();

program
  .name('bmad-build')
  .description('BMad-Method build tool for creating web bundles')
  .version('4.0.0');

program
  .command('build')
  .description('Build web bundles for agents and teams')
  .option('-a, --agents-only', 'Build only agent bundles')
  .option('-t, --teams-only', 'Build only team bundles')
  .option('-e, --expansions-only', 'Build only expansion pack bundles')
  .option('--no-expansions', 'Skip building expansion packs')
  .option('--no-clean', 'Skip cleaning output directories')
  .action(async (options) => {
    const builder = new WebBuilder({
      rootDir: process.cwd()
    });

    try {
      if (options.clean) {
        console.log('Cleaning output directories...');
        await builder.cleanOutputDirs();
      }

      if (options.expansionsOnly) {
        console.log('Building expansion pack bundles...');
        await builder.buildAllExpansionPacks({ clean: false });
      } else {
        if (!options.teamsOnly) {
          console.log('Building agent bundles...');
          await builder.buildAgents();
        }

        if (!options.agentsOnly) {
          console.log('Building team bundles...');
          await builder.buildTeams();
        }

        if (!options.noExpansions) {
          console.log('Building expansion pack bundles...');
          await builder.buildAllExpansionPacks({ clean: false });
        }
      }

      console.log('Build completed successfully!');
    } catch (error) {
      console.error('Build failed:', error.message);
      process.exit(1);
    }
  });

program
  .command('build:expansions')
  .description('Build web bundles for all expansion packs')
  .option('--expansion <name>', 'Build specific expansion pack only')
  .option('--no-clean', 'Skip cleaning output directories')
  .action(async (options) => {
    const builder = new WebBuilder({
      rootDir: process.cwd()
    });

    try {
      if (options.expansion) {
        console.log(`Building expansion pack: ${options.expansion}`);
        await builder.buildExpansionPack(options.expansion, { clean: options.clean });
      } else {
        console.log('Building all expansion packs...');
        await builder.buildAllExpansionPacks({ clean: options.clean });
      }

      console.log('Expansion pack build completed successfully!');
    } catch (error) {
      console.error('Expansion pack build failed:', error.message);
      process.exit(1);
    }
  });

program
  .command('list:agents')
  .description('List all available agents')
  .action(async () => {
    const builder = new WebBuilder({ rootDir: process.cwd() });
    const agents = await builder.resolver.listAgents();
    console.log('Available agents:');
    agents.forEach(agent => console.log(`  - ${agent}`));
  });

program
  .command('list:expansions')
  .description('List all available expansion packs')
  .action(async () => {
    const builder = new WebBuilder({ rootDir: process.cwd() });
    const expansions = await builder.listExpansionPacks();
    console.log('Available expansion packs:');
    expansions.forEach(expansion => console.log(`  - ${expansion}`));
  });

program
  .command('validate')
  .description('Validate agent and team configurations')
  .action(async () => {
    const builder = new WebBuilder({ rootDir: process.cwd() });
    try {
      // Validate by attempting to build all agents and teams
      const agents = await builder.resolver.listAgents();
      const teams = await builder.resolver.listTeams();

      console.log('Validating agents...');
      for (const agent of agents) {
        await builder.resolver.resolveAgentDependencies(agent);
        console.log(`  ✓ ${agent}`);
      }

      console.log('\nValidating teams...');
      for (const team of teams) {
        await builder.resolver.resolveTeamDependencies(team);
        console.log(`  ✓ ${team}`);
      }

      console.log('\nAll configurations are valid!');
    } catch (error) {
      console.error('Validation failed:', error.message);
      process.exit(1);
    }
  });

program
  .command('upgrade')
  .description('Upgrade a BMad-Method V3 project to V4')
  .option('-p, --project <path>', 'Path to V3 project (defaults to current directory)')
  .option('--dry-run', 'Show what would be changed without making changes')
  .option('--no-backup', 'Skip creating backup (not recommended)')
  .action(async (options) => {
    const upgrader = new V3ToV4Upgrader();
    await upgrader.upgrade({
      projectPath: options.project,
      dryRun: options.dryRun,
      backup: options.backup
    });
  });

program
  .command('flatten')
  .description('Flatten codebase to XML format')
  .option('-o, --output <path>', 'Output file path', 'flattened-codebase.xml')
  .action(async (options) => {
    const flattener = require('./flattener/main');
    await flattener.parseAsync(['flatten', '--output', options.output], { from: 'user' });
  });

program.parse();

#!/usr/bin/env node

const { Command } = require('commander');
const fs = require('fs-extra');
const path = require('node:path');
const { glob } = require('glob');
const { minimatch } = require('minimatch');

/**
 * Recursively discover all files in a directory
 * @param {string} rootDir - The root directory to scan
 * @returns {Promise<string[]>} Array of file paths
 */
async function discoverFiles(rootDir) {
  try {
    const gitignorePath = path.join(rootDir, '.gitignore');
    const gitignorePatterns = await parseGitignore(gitignorePath);

    // Common gitignore patterns that should always be ignored
    const commonIgnorePatterns = [
      // Version control
      '.git/**',
      '.svn/**',
      '.hg/**',
      '.bzr/**',

      // Dependencies
      'node_modules/**',
      'bower_components/**',
      'vendor/**',
      'packages/**',

      // Build outputs
      'build/**',
      'dist/**',
      'out/**',
      'target/**',
      'bin/**',
      'obj/**',
      'release/**',
      'debug/**',

      // Environment and config
      '.env',
      '.env.*',
      '*.env',
      '.config',

      // Logs
      'logs/**',
      '*.log',
      'npm-debug.log*',
      'yarn-debug.log*',
      'yarn-error.log*',
      'lerna-debug.log*',

      // Coverage and testing
      'coverage/**',
      '.nyc_output/**',
      '.coverage/**',
      'test-results/**',
      'junit.xml',

      // Cache directories
      '.cache/**',
      '.tmp/**',
      '.temp/**',
      'tmp/**',
      'temp/**',
      '.sass-cache/**',
      '.eslintcache',
      '.stylelintcache',

      // OS generated files
      '.DS_Store',
      '.DS_Store?',
      '._*',
      '.Spotlight-V100',
      '.Trashes',
      'ehthumbs.db',
      'Thumbs.db',
      'desktop.ini',

      // IDE and editor files
      '.vscode/**',
      '.idea/**',
      '*.swp',
      '*.swo',
      '*~',
      '.project',
      '.classpath',
      '.settings/**',
      '*.sublime-project',
      '*.sublime-workspace',

      // Package manager files
      'package-lock.json',
      'yarn.lock',
      'pnpm-lock.yaml',
      'composer.lock',
      'Pipfile.lock',

      // Runtime and compiled files
      '*.pyc',
      '*.pyo',
      '*.pyd',
      '__pycache__/**',
      '*.class',
      '*.jar',
      '*.war',
      '*.ear',
      '*.o',
      '*.so',
      '*.dll',
      '*.exe',

      // Documentation build
      '_site/**',
      '.jekyll-cache/**',
      '.jekyll-metadata',

      // Flattener specific outputs
      'flattened-codebase.xml',
      'repomix-output.xml'
    ];

    const combinedIgnores = [
      ...gitignorePatterns,
      ...commonIgnorePatterns
    ];

    // Use glob to recursively find all files, excluding common ignore patterns
    const files = await glob('**/*', {
      cwd: rootDir,
      nodir: true, // Only files, not directories
      dot: true,   // Include hidden files
      follow: false, // Don't follow symbolic links
      ignore: combinedIgnores
    });

    return files.map(file => path.resolve(rootDir, file));
  } catch (error) {
    console.error('Error discovering files:', error.message);
    return [];
  }
}

/**
 * Parse .gitignore file and return ignore patterns
 * @param {string} gitignorePath - Path to .gitignore file
 * @returns {Promise<string[]>} Array of ignore patterns
 */
async function parseGitignore(gitignorePath) {
  try {
    if (!await fs.pathExists(gitignorePath)) {
      return [];
    }

    const content = await fs.readFile(gitignorePath, 'utf8');
    return content
      .split('\n')
      .map(line => line.trim())
      .filter(line => line && !line.startsWith('#')) // Remove empty lines and comments
      .map(pattern => {
        // Convert gitignore patterns to glob patterns
        if (pattern.endsWith('/')) {
          return pattern + '**';
        }
        return pattern;
      });
  } catch (error) {
    console.error('Error parsing .gitignore:', error.message);
    return [];
  }
}

/**
 * Check if a file is binary using file command and heuristics
 * @param {string} filePath - Path to the file
 * @returns {Promise<boolean>} True if file is binary
 */
async function isBinaryFile(filePath) {
  try {
    // First check by file extension
    const binaryExtensions = [
      '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.ico', '.svg',
      '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
      '.zip', '.tar', '.gz', '.rar', '.7z',
      '.exe', '.dll', '.so', '.dylib',
      '.mp3', '.mp4', '.avi', '.mov', '.wav',
      '.ttf', '.otf', '.woff', '.woff2',
      '.bin', '.dat', '.db', '.sqlite'
    ];

    const ext = path.extname(filePath).toLowerCase();
    if (binaryExtensions.includes(ext)) {
      return true;
    }

    // For files without clear extensions, try to read a small sample
    const stats = await fs.stat(filePath);
    if (stats.size === 0) {
      return false; // Empty files are considered text
    }

    // Read first 1024 bytes to check for null bytes
    const sampleSize = Math.min(1024, stats.size);
    const buffer = await fs.readFile(filePath, { encoding: null, flag: 'r' });
    const sample = buffer.slice(0, sampleSize);
    // If we find null bytes, it's likely binary
    return sample.includes(0);
  } catch (error) {
    console.warn(`Warning: Could not determine if file is binary: ${filePath} - ${error.message}`);
    return false; // Default to text if we can't determine
  }
}

/**
 * Read and aggregate content from text files
 * @param {string[]} files - Array of file paths
 * @param {string} rootDir - The root directory
 * @param {Object} spinner - Optional spinner instance for progress display
 * @returns {Promise<Object>} Object containing file contents and metadata
 */
async function aggregateFileContents(files, rootDir, spinner = null) {
  const results = {
    textFiles: [],
    binaryFiles: [],
    errors: [],
    totalFiles: files.length,
    processedFiles: 0
  };

  for (const filePath of files) {
    try {
      const relativePath = path.relative(rootDir, filePath);

      // Update progress indicator
      if (spinner) {
        spinner.text = `Processing file ${results.processedFiles + 1}/${results.totalFiles}: ${relativePath}`;
      }

      const isBinary = await isBinaryFile(filePath);

      if (isBinary) {
        results.binaryFiles.push({
          path: relativePath,
          absolutePath: filePath,
          size: (await fs.stat(filePath)).size
        });
      } else {
        // Read text file content
        const content = await fs.readFile(filePath, 'utf8');
        results.textFiles.push({
          path: relativePath,
          absolutePath: filePath,
          content: content,
          size: content.length,
          lines: content.split('\n').length
        });
      }

      results.processedFiles++;
    } catch (error) {
      const relativePath = path.relative(rootDir, filePath);
      const errorInfo = {
        path: relativePath,
        absolutePath: filePath,
        error: error.message
      };

      results.errors.push(errorInfo);

      // Log warning without interfering with spinner
      if (spinner) {
        spinner.warn(`Warning: Could not read file ${relativePath}: ${error.message}`);
      } else {
        console.warn(`Warning: Could not read file ${relativePath}: ${error.message}`);
      }

      results.processedFiles++;
    }
  }

  return results;
}

/**
 * Generate XML output with aggregated file contents using streaming
 * @param {Object} aggregatedContent - The aggregated content object
 * @param {string} outputPath - The output file path
 * @returns {Promise<void>} Promise that resolves when writing is complete
 */
async function generateXMLOutput(aggregatedContent, outputPath) {
  const { textFiles } = aggregatedContent;

  // Create write stream for efficient memory usage
  const writeStream = fs.createWriteStream(outputPath, { encoding: 'utf8' });

  return new Promise((resolve, reject) => {
    writeStream.on('error', reject);
    writeStream.on('finish', resolve);

    // Write XML header
    writeStream.write('<?xml version="1.0" encoding="UTF-8"?>\n');
    writeStream.write('<files>\n');

    // Process files one by one to minimize memory usage
    let fileIndex = 0;

    const writeNextFile = () => {
      if (fileIndex >= textFiles.length) {
        // All files processed, close XML and stream
        writeStream.write('</files>\n');
        writeStream.end();
        return;
      }

      const file = textFiles[fileIndex];
      fileIndex++;

      // Write file opening tag
      writeStream.write(`  <file path="${escapeXml(file.path)}">`);

      // Use CDATA for code content, handling CDATA end sequences properly
      if (file.content?.trim()) {
        const indentedContent = indentFileContent(file.content);
        if (file.content.includes(']]>')) {
          // If content contains ]]>, split it and wrap each part in CDATA
          writeStream.write(splitAndWrapCDATA(indentedContent));
        } else {
          writeStream.write(`<![CDATA[\n${indentedContent}\n    ]]>`);
        }
      } else if (file.content) {
        // Handle empty or whitespace-only content
        const indentedContent = indentFileContent(file.content);
        writeStream.write(`<![CDATA[\n${indentedContent}\n    ]]>`);
      }

      // Write file closing tag
      writeStream.write('</file>\n');

      // Continue with next file on next tick to avoid stack overflow
      setImmediate(writeNextFile);
    };

    // Start processing files
    writeNextFile();
  });
}

/**
 * Escape XML special characters for attributes
 * @param {string} str - String to escape
 * @returns {string} Escaped string
 */
function escapeXml(str) {
  if (typeof str !== 'string') {
    return String(str);
  }
  return str
    .replace(/&/g, '&')
    .replace(/</g, '<')
    .replace(/>/g, '>')
    .replace(/"/g, '"')
    .replace(/'/g, ''');
}

/**
 * Indent file content with 4 spaces for each line
 * @param {string} content - Content to indent
 * @returns {string} Indented content
 */
function indentFileContent(content) {
  if (typeof content !== 'string') {
    return String(content);
  }

  // Split content into lines and add 4 spaces of indentation to each line
  return content.split('\n').map(line => `    ${line}`).join('\n');
}

/**
 * Split content containing ]]> and wrap each part in CDATA
 * @param {string} content - Content to process
 * @returns {string} Content with properly wrapped CDATA sections
 */
function splitAndWrapCDATA(content) {
  if (typeof content !== 'string') {
    return String(content);
  }

  // Replace ]]> with ]]]]><![CDATA[> to escape it within CDATA
  const escapedContent = content.replace(/]]>/g, ']]]]><![CDATA[>');
  return `<![CDATA[
${escapedContent}
    ]]>`;
}

/**
 * Calculate statistics for the processed files
 * @param {Object} aggregatedContent - The aggregated content object
 * @param {number} xmlFileSize - The size of the generated XML file in bytes
 * @returns {Object} Statistics object
 */
function calculateStatistics(aggregatedContent, xmlFileSize) {
  const { textFiles, binaryFiles, errors } = aggregatedContent;

  // Calculate total file size in bytes
  const totalTextSize = textFiles.reduce((sum, file) => sum + file.size, 0);
  const totalBinarySize = binaryFiles.reduce((sum, file) => sum + file.size, 0);
  const totalSize = totalTextSize + totalBinarySize;

  // Calculate total lines of code
  const totalLines = textFiles.reduce((sum, file) => sum + file.lines, 0);

  // Estimate token count (rough approximation: 1 token ≈ 4 characters)
  const estimatedTokens = Math.ceil(xmlFileSize / 4);

  // Format file size
  const formatSize = (bytes) => {
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
    return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
  };

  return {
    totalFiles: textFiles.length + binaryFiles.length,
    textFiles: textFiles.length,
    binaryFiles: binaryFiles.length,
    errorFiles: errors.length,
    totalSize: formatSize(totalSize),
    xmlSize: formatSize(xmlFileSize),
    totalLines,
    estimatedTokens: estimatedTokens.toLocaleString()
  };
}

/**
 * Filter files based on .gitignore patterns
 * @param {string[]} files - Array of file paths
 * @param {string} rootDir - The root directory
 * @returns {Promise<string[]>} Filtered array of file paths
 */
async function filterFiles(files, rootDir) {
  const gitignorePath = path.join(rootDir, '.gitignore');
  const ignorePatterns = await parseGitignore(gitignorePath);

  if (ignorePatterns.length === 0) {
    return files;
  }

  // Convert absolute paths to relative for pattern matching
  const relativeFiles = files.map(file => path.relative(rootDir, file));

  // Separate positive and negative patterns
  const positivePatterns = ignorePatterns.filter(p => !p.startsWith('!'));
  const negativePatterns = ignorePatterns.filter(p => p.startsWith('!')).map(p => p.slice(1));

  // Filter out files that match ignore patterns
  const filteredRelative = [];

  for (const file of relativeFiles) {
    let shouldIgnore = false;

    // First check positive patterns (ignore these files)
    for (const pattern of positivePatterns) {
      if (minimatch(file, pattern)) {
        shouldIgnore = true;
        break;
      }
    }

    // Then check negative patterns (don't ignore these files even if they match positive patterns)
    if (shouldIgnore) {
      for (const pattern of negativePatterns) {
        if (minimatch(file, pattern)) {
          shouldIgnore = false;
          break;
        }
      }
    }

    if (!shouldIgnore) {
      filteredRelative.push(file);
    }
  }

  // Convert back to absolute paths
  return filteredRelative.map(file => path.resolve(rootDir, file));
}

const program = new Command();

program
  .name('bmad-flatten')
  .description('BMad-Method codebase flattener tool')
  .version('1.0.0')
  .option('-o, --output <path>', 'Output file path', 'flattened-codebase.xml')
  .action(async (options) => {
    console.log(`Flattening codebase to: ${options.output}`);

    try {
      // Import ora dynamically
      const { default: ora } = await import('ora');

      // Start file discovery with spinner
      const discoverySpinner = ora('🔍 Discovering files...').start();
      const files = await discoverFiles(process.cwd());
      const filteredFiles = await filterFiles(files, process.cwd());
      discoverySpinner.succeed(`📁 Found ${filteredFiles.length} files to include`);

      // Process files with progress tracking
      console.log('Reading file contents');
      const processingSpinner = ora('📄 Processing files...').start();
      const aggregatedContent = await aggregateFileContents(filteredFiles, process.cwd(), processingSpinner);
      processingSpinner.succeed(`✅ Processed ${aggregatedContent.processedFiles}/${filteredFiles.length} files`);

      // Log processing results for test validation
      console.log(`Processed ${aggregatedContent.processedFiles}/${filteredFiles.length} files`);
      if (aggregatedContent.errors.length > 0) {
        console.log(`Errors: ${aggregatedContent.errors.length}`);
      }
      console.log(`Text files: ${aggregatedContent.textFiles.length}`);
      if (aggregatedContent.binaryFiles.length > 0) {
        console.log(`Binary files: ${aggregatedContent.binaryFiles.length}`);
      }

      // Generate XML output using streaming
      const xmlSpinner = ora('🔧 Generating XML output...').start();
      await generateXMLOutput(aggregatedContent, options.output);
      xmlSpinner.succeed('📝 XML generation completed');

      // Calculate and display statistics
      const outputStats = await fs.stat(options.output);
      const stats = calculateStatistics(aggregatedContent, outputStats.size);

      // Display completion summary
      console.log('\n📊 Completion Summary:');
      console.log(`✅ Successfully processed ${filteredFiles.length} files into ${options.output}`);
      console.log(`📁 Output file: ${path.resolve(options.output)}`);
      console.log(`📏 Total source size: ${stats.totalSize}`);
      console.log(`📄 Generated XML size: ${stats.xmlSize}`);
      console.log(`📝 Total lines of code: ${stats.totalLines.toLocaleString()}`);
      console.log(`🔢 Estimated tokens: ${stats.estimatedTokens}`);
      console.log(`📊 File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors`);

    } catch (error) {
      console.error('❌ Critical error:', error.message);
      console.error('An unexpected error occurred.');
      process.exit(1);
    }
  });

if (require.main === module) {
  program.parse();
}

module.exports = program;

#!/usr/bin/env node

const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const { execSync } = require('child_process');

// Dynamic import for ES module
let chalk;

// Initialize ES modules
async function initializeModules() {
  if (!chalk) {
    chalk = (await import('chalk')).default;
  }
}

/**
 * YAML Formatter and Linter for BMad-Method
 * Formats and validates YAML files and YAML embedded in Markdown
 */

async function formatYamlContent(content, filename) {
  await initializeModules();
  try {
    // First try to fix common YAML issues
    let fixedContent = content
      // Fix "commands :" -> "commands:"
      .replace(/^(\s*)(\w+)\s+:/gm, '$1$2:')
      // Fix inconsistent list indentation
      .replace(/^(\s*)-\s{3,}/gm, '$1- ');

    // Skip auto-fixing for .roomodes files - they have special nested structure
    if (!filename.includes('.roomodes')) {
      fixedContent = fixedContent
        // Fix unquoted list items that contain special characters or multiple parts
        .replace(/^(\s*)-\s+(.*)$/gm, (match, indent, content) => {
          // Skip if already quoted
          if (content.startsWith('"') && content.endsWith('"')) {
            return match;
          }
          // If the content contains special YAML characters or looks complex, quote it
          // BUT skip if it looks like a proper YAML key-value pair (like "key: value")
          if ((content.includes(':') || content.includes('-') || content.includes('{') || content.includes('}')) &&
              !content.match(/^\w+:\s/)) {
            // Remove any existing quotes first, escape internal quotes, then add proper quotes
            const cleanContent = content.replace(/^["']|["']$/g, '').replace(/"/g, '\\"');
            return `${indent}- "${cleanContent}"`;
          }
          return match;
        });
    }

    // Debug: show what we're trying to parse
    if (fixedContent !== content) {
      console.log(chalk.blue(`🔧 Applied YAML fixes to ${filename}`));
    }

    // Parse and re-dump YAML to format it
    const parsed = yaml.load(fixedContent);
    const formatted = yaml.dump(parsed, {
      indent: 2,
      lineWidth: -1, // Disable line wrapping
      noRefs: true,
      sortKeys: false // Preserve key order
    });
    return formatted;
  } catch (error) {
    console.error(chalk.red(`❌ YAML syntax error in ${filename}:`), error.message);
    console.error(chalk.yellow(`💡 Try manually fixing the YAML structure first`));
    return null;
  }
}

async function processMarkdownFile(filePath) {
  await initializeModules();
  const content = fs.readFileSync(filePath, 'utf8');
  let modified = false;
  let newContent = content;

  // Fix untyped code blocks by adding 'text' type
  // Match ``` at start of line followed by newline, but only if it's an opening fence
  newContent = newContent.replace(/^```\n([\s\S]*?)\n```$/gm, '```text\n$1\n```');
  if (newContent !== content) {
    modified = true;
    console.log(chalk.blue(`🔧 Added 'text' type to untyped code blocks in ${filePath}`));
  }

  // Find YAML code blocks
  const yamlBlockRegex = /```ya?ml\n([\s\S]*?)\n```/g;
  let match;
  const replacements = [];

  while ((match = yamlBlockRegex.exec(newContent)) !== null) {
    const [fullMatch, yamlContent] = match;
    const formatted = await formatYamlContent(yamlContent, filePath);
    if (formatted !== null) {
      // Remove trailing newline that js-yaml adds
      const trimmedFormatted = formatted.replace(/\n$/, '');

      if (trimmedFormatted !== yamlContent) {
        modified = true;
        console.log(chalk.green(`✓ Formatted YAML in ${filePath}`));
      }

      replacements.push({
        start: match.index,
        end: match.index + fullMatch.length,
        replacement: `\`\`\`yaml\n${trimmedFormatted}\n\`\`\``
      });
    }
  }

  // Apply replacements in reverse order to maintain indices
  for (let i = replacements.length - 1; i >= 0; i--) {
    const { start, end, replacement } = replacements[i];
    newContent = newContent.slice(0, start) + replacement + newContent.slice(end);
  }

  if (modified) {
    fs.writeFileSync(filePath, newContent);
    return true;
  }
  return false;
}

async function processYamlFile(filePath) {
  await initializeModules();
  const content = fs.readFileSync(filePath, 'utf8');
  const formatted = await formatYamlContent(content, filePath);

  if (formatted === null) {
    return false; // Syntax error
  }

  if (formatted !== content) {
    fs.writeFileSync(filePath, formatted);
    return true;
  }
  return false;
}

async function lintYamlFile(filePath) {
  await initializeModules();
  try {
    // Use yaml-lint for additional validation
    execSync(`npx yaml-lint "${filePath}"`, { stdio: 'pipe' });
    return true;
  } catch (error) {
    console.error(chalk.red(`❌ YAML lint error in ${filePath}:`));
    console.error(error.stdout?.toString() || error.message);
    return false;
  }
}

async function main() {
  await initializeModules();
  const args = process.argv.slice(2);
  const glob = require('glob');

  if (args.length === 0) {
    console.error('Usage: node yaml-format.js <file1> [file2] ...');
    process.exit(1);
  }

  let hasErrors = false;
  let hasChanges = false;
  let filesProcessed = [];

  // Expand glob patterns and collect all files
  const allFiles = [];
  for (const arg of args) {
    if (arg.includes('*')) {
      // It's a glob pattern
      const matches = glob.sync(arg);
      allFiles.push(...matches);
    } else {
      // It's a direct file path
      allFiles.push(arg);
    }
  }

  for (const filePath of allFiles) {
    if (!fs.existsSync(filePath)) {
      // Skip silently for glob patterns that don't match anything
      if (!args.some(arg => arg.includes('*') && filePath === arg)) {
        console.error(chalk.red(`❌ File not found: ${filePath}`));
        hasErrors = true;
      }
      continue;
    }

    const ext = path.extname(filePath).toLowerCase();
    const basename = path.basename(filePath).toLowerCase();

    try {
      let changed = false;
      if (ext === '.md') {
        changed = await processMarkdownFile(filePath);
      } else if (ext === '.yaml' || ext === '.yml' || basename.includes('roomodes') || basename.includes('.yaml') || basename.includes('.yml')) {
        // Handle YAML files and special cases like .roomodes
        changed = await processYamlFile(filePath);

        // Also run linting
        const lintPassed = await lintYamlFile(filePath);
        if (!lintPassed) hasErrors = true;
      } else {
        // Skip silently for unsupported files
        continue;
      }

      if (changed) {
        hasChanges = true;
        filesProcessed.push(filePath);
      }
    } catch (error) {
      console.error(chalk.red(`❌ Error processing ${filePath}:`), error.message);
      hasErrors = true;
    }
  }

  if (hasChanges) {
    console.log(chalk.green(`\n✨ YAML formatting completed! Modified ${filesProcessed.length} files:`));
    filesProcessed.forEach(file => console.log(chalk.blue(`  📝 ${file}`)));
  }

  if (hasErrors) {
    console.error(chalk.red('\n💥 Some files had errors. Please fix them before committing.'));
    process.exit(1);
  }
}

if (require.main === module) {
  main().catch(error => {
    console.error('Error:', error);
    process.exit(1);
  });
}

module.exports = { formatYamlContent, processMarkdownFile, processYamlFile };

/**
 * Resource Locator - Centralized file path resolution and caching
 * Reduces duplicate file system operations and memory usage
 */

const path = require('node:path');
const fs = require('fs-extra');
const moduleManager = require('./module-manager');

class ResourceLocator {
  constructor() {
    this._pathCache = new Map();
    this._globCache = new Map();
    this._bmadCorePath = null;
    this._expansionPacksPath = null;
  }

  /**
   * Get the base path for bmad-core
   */
  getBmadCorePath() {
    if (!this._bmadCorePath) {
      this._bmadCorePath = path.join(__dirname, '../../../bmad-core');
    }
    return this._bmadCorePath;
  }

  /**
   * Get the base path for expansion packs
   */
  getExpansionPacksPath() {
    if (!this._expansionPacksPath) {
      this._expansionPacksPath = path.join(__dirname, '../../../expansion-packs');
    }
    return this._expansionPacksPath;
  }

  /**
   * Find all files matching a pattern, with caching
   * @param {string} pattern - Glob pattern
   * @param {Object} options - Glob options
   * @returns {Promise<string[]>} Array of matched file paths
   */
  async findFiles(pattern, options = {}) {
    const cacheKey = `${pattern}:${JSON.stringify(options)}`;

    if (this._globCache.has(cacheKey)) {
      return this._globCache.get(cacheKey);
    }

    const { glob } = await moduleManager.getModules(['glob']);
    const files = await glob(pattern, options);

    // Cache for 5 minutes
    this._globCache.set(cacheKey, files);
    setTimeout(() => this._globCache.delete(cacheKey), 5 * 60 * 1000);

    return files;
  }

  /**
   * Get agent path with caching
   * @param {string} agentId - Agent identifier
   * @returns {Promise<string|null>} Path to agent file or null if not found
   */
  async getAgentPath(agentId) {
    const cacheKey = `agent:${agentId}`;

    if (this._pathCache.has(cacheKey)) {
      return this._pathCache.get(cacheKey);
    }

    // Check in bmad-core
    let agentPath = path.join(this.getBmadCorePath(), 'agents', `${agentId}.md`);
    if (await fs.pathExists(agentPath)) {
      this._pathCache.set(cacheKey, agentPath);
      return agentPath;
    }

    // Check in expansion packs
    const expansionPacks = await this.getExpansionPacks();
    for (const pack of expansionPacks) {
      agentPath = path.join(pack.path, 'agents', `${agentId}.md`);
      if (await fs.pathExists(agentPath)) {
        this._pathCache.set(cacheKey, agentPath);
        return agentPath;
      }
    }

    return null;
  }

  /**
   * Get available agents with metadata
   * @returns {Promise<Array>} Array of agent objects
   */
  async getAvailableAgents() {
    const cacheKey = 'all-agents';

    if (this._pathCache.has(cacheKey)) {
      return this._pathCache.get(cacheKey);
    }

    const agents = [];
    const yaml = require('js-yaml');
    const { extractYamlFromAgent } = require('../../lib/yaml-utils');

    // Get agents from bmad-core
    const coreAgents = await this.findFiles('agents/*.md', {
      cwd: this.getBmadCorePath()
    });

    for (const agentFile of coreAgents) {
      const content = await fs.readFile(
        path.join(this.getBmadCorePath(), agentFile),
        'utf8'
      );
      const yamlContent = extractYamlFromAgent(content);
      if (yamlContent) {
        try {
          const metadata = yaml.load(yamlContent);
          agents.push({
            id: path.basename(agentFile, '.md'),
            name: metadata.agent_name || path.basename(agentFile, '.md'),
            description: metadata.description || 'No description available',
            source: 'core'
          });
        } catch (e) {
          // Skip invalid agents
        }
      }
    }

    // Cache for 10 minutes
    this._pathCache.set(cacheKey, agents);
    setTimeout(() => this._pathCache.delete(cacheKey), 10 * 60 * 1000);

    return agents;
  }

  /**
   * Get available expansion packs
   * @returns {Promise<Array>} Array of expansion pack objects
   */
  async getExpansionPacks() {
    const cacheKey = 'expansion-packs';

    if (this._pathCache.has(cacheKey)) {
      return this._pathCache.get(cacheKey);
    }

    const packs = [];
    const expansionPacksPath = this.getExpansionPacksPath();

    if (await fs.pathExists(expansionPacksPath)) {
      const entries = await fs.readdir(expansionPacksPath, { withFileTypes: true });

      for (const entry of entries) {
        if (entry.isDirectory()) {
          const configPath = path.join(expansionPacksPath, entry.name, 'config.yaml');
          if (await fs.pathExists(configPath)) {
            try {
              const yaml = require('js-yaml');
              const config = yaml.load(await fs.readFile(configPath, 'utf8'));
              packs.push({
                id: entry.name,
                name: config.name || entry.name,
                version: config.version || '1.0.0',
                description: config.description || 'No description available',
                shortTitle: config['short-title'] || config.description || 'No description available',
                author: config.author || 'Unknown',
                path: path.join(expansionPacksPath, entry.name)
              });
            } catch (e) {
              // Skip invalid packs
            }
          }
        }
      }
    }

    // Cache for 10 minutes
    this._pathCache.set(cacheKey, packs);
    setTimeout(() => this._pathCache.delete(cacheKey), 10 * 60 * 1000);

    return packs;
  }

  /**
   * Get team configuration
   * @param {string} teamId - Team identifier
   * @returns {Promise<Object|null>} Team configuration or null
   */
  async getTeamConfig(teamId) {
    const cacheKey = `team:${teamId}`;

    if (this._pathCache.has(cacheKey)) {
      return this._pathCache.get(cacheKey);
    }

    const teamPath = path.join(this.getBmadCorePath(), 'agent-teams', `${teamId}.yaml`);

    if (await fs.pathExists(teamPath)) {
      try {
        const yaml = require('js-yaml');
        const content = await fs.readFile(teamPath, 'utf8');
        const config = yaml.load(content);
        this._pathCache.set(cacheKey, config);
        return config;
      } catch (e) {
        return null;
      }
    }

    return null;
  }

  /**
   * Get resource dependencies for an agent
   * @param {string} agentId - Agent identifier
   * @returns {Promise<Object>} Dependencies object
   */
  async getAgentDependencies(agentId) {
    const cacheKey = `deps:${agentId}`;

    if (this._pathCache.has(cacheKey)) {
      return this._pathCache.get(cacheKey);
    }

    const agentPath = await this.getAgentPath(agentId);
    if (!agentPath) {
      return { all: [], byType: {} };
    }

    const content = await fs.readFile(agentPath, 'utf8');
    const { extractYamlFromAgent } = require('../../lib/yaml-utils');
    const yamlContent = extractYamlFromAgent(content);

    if (!yamlContent) {
      return { all: [], byType: {} };
    }

    try {
      const yaml = require('js-yaml');
      const metadata = yaml.load(yamlContent);
      const dependencies = metadata.dependencies || {};

      // Flatten dependencies
      const allDeps = [];
      const byType = {};

      for (const [type, deps] of Object.entries(dependencies)) {
        if (Array.isArray(deps)) {
          byType[type] = deps;
          for (const dep of deps) {
            allDeps.push(`.bmad-core/${type}/${dep}`);
          }
        }
      }

      const result = { all: allDeps, byType };
      this._pathCache.set(cacheKey, result);
      return result;
    } catch (e) {
      return { all: [], byType: {} };
    }
  }

  /**
   * Clear all caches to free memory
   */
  clearCache() {
    this._pathCache.clear();
    this._globCache.clear();
  }

  /**
   * Get IDE configuration
   * @param {string} ideId - IDE identifier
   * @returns {Promise<Object|null>} IDE configuration or null
   */
  async getIdeConfig(ideId) {
    const cacheKey = `ide:${ideId}`;

    if (this._pathCache.has(cacheKey)) {
      return this._pathCache.get(cacheKey);
    }

    const idePath = path.join(this.getBmadCorePath(), 'ide-rules', `${ideId}.yaml`);

    if (await fs.pathExists(idePath)) {
      try {
        const yaml = require('js-yaml');
        const content = await fs.readFile(idePath, 'utf8');
        const config = yaml.load(content);
        this._pathCache.set(cacheKey, config);
        return config;
      } catch (e) {
        return null;
      }
    }

    return null;
  }
}

// Singleton instance
const resourceLocator = new ResourceLocator();

module.exports = resourceLocator;

const fs = require('fs-extra');
const path = require('path');
const yaml = require('js-yaml');
const { extractYamlFromAgent } = require('../../lib/yaml-utils');

class ConfigLoader {
  constructor() {
    this.configPath = path.join(__dirname, '..', 'config', 'install.config.yaml');
    this.config = null;
  }

  async load() {
    if (this.config) return this.config;

    try {
      const configContent = await fs.readFile(this.configPath, 'utf8');
      this.config = yaml.load(configContent);
      return this.config;
    } catch (error) {
      throw new Error(`Failed to load configuration: ${error.message}`);
    }
  }

  async getInstallationOptions() {
    const config = await this.load();
    return config['installation-options'] || {};
  }

  async getAvailableAgents() {
    const agentsDir = path.join(this.getBmadCorePath(), 'agents');

    try {
      const entries = await fs.readdir(agentsDir, { withFileTypes: true });
      const agents = [];

      for (const entry of entries) {
        if (entry.isFile() && entry.name.endsWith('.md')) {
          const agentPath = path.join(agentsDir, entry.name);
          const agentId = path.basename(entry.name, '.md');

          try {
            const agentContent = await fs.readFile(agentPath, 'utf8');

            // Extract YAML block from agent file
            const yamlContentText = extractYamlFromAgent(agentContent);
            if (yamlContentText) {
              const yamlContent = yaml.load(yamlContentText);
              const agentConfig = yamlContent.agent || {};

              agents.push({
                id: agentId,
                name: agentConfig.title || agentConfig.name || agentId,
                file: `bmad-core/agents/${entry.name}`,
                description: agentConfig.whenToUse || 'No description available'
              });
            }
          } catch (error) {
            console.warn(`Failed to read agent ${entry.name}: ${error.message}`);
          }
        }
      }

      // Sort agents by name for consistent display
      agents.sort((a, b) => a.name.localeCompare(b.name));

      return agents;
    } catch (error) {
      console.warn(`Failed to read agents directory: ${error.message}`);
      return [];
    }
  }

  async getAvailableExpansionPacks() {
    const expansionPacksDir = path.join(this.getBmadCorePath(), '..', 'expansion-packs');

    try {
      const entries = await fs.readdir(expansionPacksDir, { withFileTypes: true });
      const expansionPacks = [];

      for (const entry of entries) {
        if (entry.isDirectory() && !entry.name.startsWith('.')) {
          const packPath = path.join(expansionPacksDir, entry.name);
          const configPath = path.join(packPath, 'config.yaml');

          try {
            // Read config.yaml
            const configContent = await fs.readFile(configPath, 'utf8');
            const config = yaml.load(configContent);

            expansionPacks.push({
              id: entry.name,
              name: config.name || entry.name,
              description: config['short-title'] || config.description || 'No description available',
              fullDescription: config.description || config['short-title'] || 'No description available',
              version: config.version || '1.0.0',
              author: config.author || 'BMad Team',
              packPath: packPath,
              dependencies: config.dependencies?.agents || []
            });
          } catch (error) {
            // Fallback if config.yaml doesn't exist or can't be read
            console.warn(`Failed to read config for expansion pack ${entry.name}: ${error.message}`);

            // Try to derive info from directory name as fallback
            const name = entry.name
              .split('-')
              .map(word => word.charAt(0).toUpperCase() + word.slice(1))
              .join(' ');

            expansionPacks.push({
              id: entry.name,
              name: name,
              description: 'No description available',
              fullDescription: 'No description available',
              version: '1.0.0',
              author: 'BMad Team',
              packPath: packPath,
              dependencies: []
            });
          }
        }
      }

      return expansionPacks;
    } catch (error) {
      console.warn(`Failed to read expansion packs directory: ${error.message}`);
      return [];
    }
  }

  async getAgentDependencies(agentId) {
    // Use DependencyResolver to dynamically parse agent dependencies
    const DependencyResolver = require('../../lib/dependency-resolver');
    const resolver = new DependencyResolver(path.join(__dirname, '..', '..', '..'));

    const agentDeps = await resolver.resolveAgentDependencies(agentId);

    // Convert to flat list of file paths
    const depPaths = [];

    // Core files and utilities are included automatically by DependencyResolver

    // Add agent file itself is already handled by installer

    // Add all resolved resources
    for (const resource of agentDeps.resources) {
      const filePath = `.bmad-core/${resource.type}/${resource.id}.md`;
      if (!depPaths.includes(filePath)) {
        depPaths.push(filePath);
      }
    }

    return depPaths;
  }

  async getIdeConfiguration(ide) {
    const config = await this.load();
    const ideConfigs = config['ide-configurations'] || {};
    return ideConfigs[ide] || null;
  }

  getBmadCorePath() {
    // Get the path to bmad-core relative to the installer (now under tools)
    return path.join(__dirname, '..', '..', '..', 'bmad-core');
  }

  getDistPath() {
    // Get the path to dist directory relative to the installer
    return path.join(__dirname, '..', '..', '..', 'dist');
  }

  getAgentPath(agentId) {
    return path.join(this.getBmadCorePath(), 'agents', `${agentId}.md`);
  }

  async getAvailableTeams() {
    const teamsDir = path.join(this.getBmadCorePath(), 'agent-teams');

    try {
      const entries = await fs.readdir(teamsDir, { withFileTypes: true });
      const teams = [];

      for (const entry of entries) {
        if (entry.isFile() && entry.name.endsWith('.yaml')) {
          const teamPath = path.join(teamsDir, entry.name);

          try {
            const teamContent = await fs.readFile(teamPath, 'utf8');
            const teamConfig = yaml.load(teamContent);

            if (teamConfig.bundle) {
              teams.push({
                id: path.basename(entry.name, '.yaml'),
                name: teamConfig.bundle.name || entry.name,
                description: teamConfig.bundle.description || 'Team configuration',
                icon: teamConfig.bundle.icon || '📋'
              });
            }
          } catch (error) {
            console.warn(`Warning: Could not load team config ${entry.name}: ${error.message}`);
          }
        }
      }

      return teams;
    } catch (error) {
      console.warn(`Warning: Could not scan teams directory: ${error.message}`);
      return [];
    }
  }

  getTeamPath(teamId) {
    return path.join(this.getBmadCorePath(), 'agent-teams', `${teamId}.yaml`);
  }

  async getTeamDependencies(teamId) {
    // Use DependencyResolver to dynamically parse team dependencies
    const DependencyResolver = require('../../lib/dependency-resolver');
    const resolver = new DependencyResolver(path.join(__dirname, '..', '..', '..'));

    try {
      const teamDeps = await resolver.resolveTeamDependencies(teamId);

      // Convert to flat list of file paths
      const depPaths = [];

      // Add team config file
      depPaths.push(`.bmad-core/agent-teams/${teamId}.yaml`);

      // Add all agents
      for (const agent of teamDeps.agents) {
        const filePath = `.bmad-core/agents/${agent.id}.md`;
        if (!depPaths.includes(filePath)) {
          depPaths.push(filePath);
        }
      }

      // Add all resolved resources
      for (const resource of teamDeps.resources) {
        const filePath = `.bmad-core/${resource.type}/${resource.id}.${resource.type === 'workflows' ? 'yaml' : 'md'}`;
        if (!depPaths.includes(filePath)) {
          depPaths.push(filePath);
        }
      }

      return depPaths;
    } catch (error) {
      throw new Error(`Failed to resolve team dependencies for ${teamId}: ${error.message}`);
    }
  }
}

module.exports = new ConfigLoader();

const fs = require('fs').promises;
const path = require('path');
const yaml = require('js-yaml');
const { extractYamlFromAgent } = require('./yaml-utils');

class DependencyResolver {
  constructor(rootDir) {
    this.rootDir = rootDir;
    this.bmadCore = path.join(rootDir, 'bmad-core');
    this.common = path.join(rootDir, 'common');
    this.cache = new Map();
  }

  async resolveAgentDependencies(agentId) {
    const agentPath = path.join(this.bmadCore, 'agents', `${agentId}.md`);
    const agentContent = await fs.readFile(agentPath, 'utf8');

    // Extract YAML from markdown content with command cleaning
    const yamlContent = extractYamlFromAgent(agentContent, true);
    if (!yamlContent) {
      throw new Error(`No YAML configuration found in agent ${agentId}`);
    }

    const agentConfig = yaml.load(yamlContent);

    const dependencies = {
      agent: {
        id: agentId,
        path: agentPath,
        content: agentContent,
        config: agentConfig
      },
      resources: []
    };

    // Personas are now embedded in agent configs, no need to resolve separately

    // Resolve other dependencies
    const depTypes = ['tasks', 'templates', 'checklists', 'data', 'utils'];
    for (const depType of depTypes) {
      const deps = agentConfig.dependencies?.[depType] || [];
      for (const depId of deps) {
        const resource = await this.loadResource(depType, depId);
        if (resource) dependencies.resources.push(resource);
      }
    }

    return dependencies;
  }

  async resolveTeamDependencies(teamId) {
    const teamPath = path.join(this.bmadCore, 'agent-teams', `${teamId}.yaml`);
    const teamContent = await fs.readFile(teamPath, 'utf8');
    const teamConfig = yaml.load(teamContent);

    const dependencies = {
      team: {
        id: teamId,
        path: teamPath,
        content: teamContent,
        config: teamConfig
      },
      agents: [],
      resources: new Map() // Use Map to deduplicate resources
    };

    // Always add bmad-orchestrator agent first if it's a team
    const bmadAgent = await this.resolveAgentDependencies('bmad-orchestrator');
    dependencies.agents.push(bmadAgent.agent);
    bmadAgent.resources.forEach(res => {
      dependencies.resources.set(res.path, res);
    });

    // Resolve all agents in the team
    let agentsToResolve = teamConfig.agents || [];

    // Handle wildcard "*" - include all agents except bmad-master
    if (agentsToResolve.includes('*')) {
      const allAgents = await this.listAgents();
      // Remove wildcard and add all agents except those already in the list and bmad-master
      agentsToResolve = agentsToResolve.filter(a => a !== '*');
      for (const agent of allAgents) {
        if (!agentsToResolve.includes(agent) && agent !== 'bmad-master') {
          agentsToResolve.push(agent);
        }
      }
    }

    for (const agentId of agentsToResolve) {
      if (agentId === 'bmad-orchestrator' || agentId === 'bmad-master') continue; // Already added or excluded
      const agentDeps = await this.resolveAgentDependencies(agentId);
      dependencies.agents.push(agentDeps.agent);

      // Add resources with deduplication
      agentDeps.resources.forEach(res => {
        dependencies.resources.set(res.path, res);
      });
    }

    // Resolve workflows
    for (const workflowId of teamConfig.workflows || []) {
      const resource = await this.loadResource('workflows', workflowId);
      if (resource) dependencies.resources.set(resource.path, resource);
    }

    // Convert Map back to array
    dependencies.resources = Array.from(dependencies.resources.values());

    return dependencies;
  }

  async loadResource(type, id) {
    const cacheKey = `${type}#${id}`;
    if (this.cache.has(cacheKey)) {
      return this.cache.get(cacheKey);
    }

    try {
      let content = null;
      let filePath = null;

      // First try bmad-core
      try {
        filePath = path.join(this.bmadCore, type, id);
        content = await fs.readFile(filePath, 'utf8');
      } catch (e) {
        // If not found in bmad-core, try common folder
        try {
          filePath = path.join(this.common, type, id);
          content = await fs.readFile(filePath, 'utf8');
        } catch (e2) {
          // File not found in either location
        }
      }

      if (!content) {
        console.warn(`Resource not found: ${type}/${id}`);
        return null;
      }

      const resource = {
        type,
        id,
        path: filePath,
        content
      };

      this.cache.set(cacheKey, resource);
      return resource;
    } catch (error) {
      console.error(`Error loading resource ${type}/${id}:`, error.message);
      return null;
    }
  }

  async listAgents() {
    try {
      const files = await fs.readdir(path.join(this.bmadCore, 'agents'));
      return files
        .filter(f => f.endsWith('.md'))
        .map(f => f.replace('.md', ''));
    } catch (error) {
      return [];
    }
  }

  async listTeams() {
    try {
      const files = await fs.readdir(path.join(this.bmadCore, 'agent-teams'));
      return files
        .filter(f => f.endsWith('.yaml'))
        .map(f => f.replace('.yaml', ''));
    } catch (error) {
      return [];
    }
  }
}

module.exports = DependencyResolver;

#!/usr/bin/env node

const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');

const args = process.argv.slice(2);
const bumpType = args[0] || 'minor'; // default to minor

if (!['major', 'minor', 'patch'].includes(bumpType)) {
  console.log('Usage: node bump-all-versions.js [major|minor|patch]');
  console.log('Default: minor');
  process.exit(1);
}

function bumpVersion(currentVersion, type) {
  const [major, minor, patch] = currentVersion.split('.').map(Number);

  switch (type) {
    case 'major':
      return `${major + 1}.0.0`;
    case 'minor':
      return `${major}.${minor + 1}.0`;
    case 'patch':
      return `${major}.${minor}.${patch + 1}`;
    default:
      return currentVersion;
  }
}

async function bumpAllVersions() {
  const updatedItems = [];

  // First, bump the core version (package.json)
  const packagePath = path.join(__dirname, '..', 'package.json');
  try {
    const packageContent = fs.readFileSync(packagePath, 'utf8');
    const packageJson = JSON.parse(packageContent);
    const oldCoreVersion = packageJson.version || '1.0.0';
    const newCoreVersion = bumpVersion(oldCoreVersion, bumpType);

    packageJson.version = newCoreVersion;

    fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + '\n');

    updatedItems.push({ type: 'core', name: 'BMad Core', oldVersion: oldCoreVersion, newVersion: newCoreVersion });
    console.log(`✓ BMad Core (package.json): ${oldCoreVersion} → ${newCoreVersion}`);
  } catch (error) {
    console.error(`✗ Failed to update BMad Core: ${error.message}`);
  }

  // Then, bump all expansion packs
  const expansionPacksDir = path.join(__dirname, '..', 'expansion-packs');

  try {
    const entries = fs.readdirSync(expansionPacksDir, { withFileTypes: true });

    for (const entry of entries) {
      if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'README.md') {
        const packId = entry.name;
        const configPath = path.join(expansionPacksDir, packId, 'config.yaml');

        if (fs.existsSync(configPath)) {
          try {
            const configContent = fs.readFileSync(configPath, 'utf8');
            const config = yaml.load(configContent);
            const oldVersion = config.version || '1.0.0';
            const newVersion = bumpVersion(oldVersion, bumpType);

            config.version = newVersion;

            const updatedYaml = yaml.dump(config, { indent: 2 });
            fs.writeFileSync(configPath, updatedYaml);

            updatedItems.push({ type: 'expansion', name: packId, oldVersion, newVersion });
            console.log(`✓ ${packId}: ${oldVersion} → ${newVersion}`);

          } catch (error) {
            console.error(`✗ Failed to update ${packId}: ${error.message}`);
          }
        }
      }
    }

    if (updatedItems.length > 0) {
      const coreCount = updatedItems.filter(i => i.type === 'core').length;
      const expansionCount = updatedItems.filter(i => i.type === 'expansion').length;

      console.log(`\n✓ Successfully bumped ${updatedItems.length} item(s) with ${bumpType} version bump`);
      if (coreCount > 0) console.log(`  - ${coreCount} core`);
      if (expansionCount > 0) console.log(`  - ${expansionCount} expansion pack(s)`);

      console.log('\nNext steps:');
      console.log('1. Test the changes');
      console.log('2. Commit: git add -A && git commit -m "chore: bump all versions (' + bumpType + ')"');
    } else {
      console.log('No items found to update');
    }

  } catch (error) {
    console.error('Error reading expansion packs directory:', error.message);
    process.exit(1);
  }
}

bumpAllVersions();

/**
 * Module Manager - Centralized dynamic import management
 * Handles loading and caching of ES modules to reduce memory overhead
 */

class ModuleManager {
  constructor() {
    this._cache = new Map();
    this._loadingPromises = new Map();
  }

  /**
   * Initialize all commonly used ES modules at once
   * @returns {Promise<Object>} Object containing all loaded modules
   */
  async initializeCommonModules() {
    const modules = await Promise.all([
      this.getModule('chalk'),
      this.getModule('ora'),
      this.getModule('inquirer')
    ]);

    return {
      chalk: modules[0],
      ora: modules[1],
      inquirer: modules[2]
    };
  }

  /**
   * Get a module by name, with caching
   * @param {string} moduleName - Name of the module to load
   * @returns {Promise<any>} The loaded module
   */
  async getModule(moduleName) {
    // Return from cache if available
    if (this._cache.has(moduleName)) {
      return this._cache.get(moduleName);
    }

    // If already loading, return the existing promise
    if (this._loadingPromises.has(moduleName)) {
      return this._loadingPromises.get(moduleName);
    }

    // Start loading the module
    const loadPromise = this._loadModule(moduleName);
    this._loadingPromises.set(moduleName, loadPromise);

    try {
      const module = await loadPromise;
      this._cache.set(moduleName, module);
      this._loadingPromises.delete(moduleName);
      return module;
    } catch (error) {
      this._loadingPromises.delete(moduleName);
      throw error;
    }
  }

  /**
   * Internal method to load a specific module
   * @private
   */
  async _loadModule(moduleName) {
    switch (moduleName) {
      case 'chalk':
        return (await import('chalk')).default;
      case 'ora':
        return (await import('ora')).default;
      case 'inquirer':
        return (await import('inquirer')).default;
      case 'glob':
        return (await import('glob')).glob;
      case 'globSync':
        return (await import('glob')).globSync;
      default:
        throw new Error(`Unknown module: ${moduleName}`);
    }
  }

  /**
   * Clear the module cache to free memory
   */
  clearCache() {
    this._cache.clear();
    this._loadingPromises.clear();
  }

  /**
   * Get multiple modules at once
   * @param {string[]} moduleNames - Array of module names
   * @returns {Promise<Object>} Object with module names as keys
   */
  async getModules(moduleNames) {
    const modules = await Promise.all(
      moduleNames.map(name => this.getModule(name))
    );

    return moduleNames.reduce((acc, name, index) => {
      acc[name] = modules[index];
      return acc;
    }, {});
  }
}

// Singleton instance
const moduleManager = new ModuleManager();

module.exports = moduleManager;

#!/usr/bin/env node

// Load required modules
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');

// Parse CLI arguments
const args = process.argv.slice(2);
const packId = args[0];
const bumpType = args[1] || 'minor';

// Validate arguments
if (!packId || args.length > 2) {
  console.log('Usage: node bump-expansion-version.js <expansion-pack-id> [major|minor|patch]');
  console.log('Default: minor');
  console.log('Example: node bump-expansion-version.js bmad-creator-tools patch');
  process.exit(1);
}

if (!['major', 'minor', 'patch'].includes(bumpType)) {
  console.error('Error: Bump type must be major, minor, or patch');
  process.exit(1);
}

// Version bump logic
function bumpVersion(currentVersion, type) {
  const [major, minor, patch] = currentVersion.split('.').map(Number);

  switch (type) {
    case 'major': return `${major + 1}.0.0`;
    case 'minor': return `${major}.${minor + 1}.0`;
    case 'patch': return `${major}.${minor}.${patch + 1}`;
    default: return currentVersion;
  }
}

// Main function to bump version
async function updateVersion() {
  const configPath = path.join(__dirname, '..', 'expansion-packs', packId, 'config.yaml');

  // Check if config exists
  if (!fs.existsSync(configPath)) {
    console.error(`Error: Expansion pack '${packId}' not found`);
    console.log('\nAvailable expansion packs:');

    const packsDir = path.join(__dirname, '..', 'expansion-packs');
    const entries = fs.readdirSync(packsDir, { withFileTypes: true });

    entries.forEach(entry => {
      if (entry.isDirectory() && !entry.name.startsWith('.')) {
        console.log(`  - ${entry.name}`);
      }
    });

    process.exit(1);
  }

  try {
    const configContent = fs.readFileSync(configPath, 'utf8');
    const config = yaml.load(configContent);

    const oldVersion = config.version || '1.0.0';
    const newVersion = bumpVersion(oldVersion, bumpType);

    config.version = newVersion;

    const updatedYaml = yaml.dump(config, { indent: 2 });
    fs.writeFileSync(configPath, updatedYaml);

    console.log(`✓ ${packId}: ${oldVersion} → ${newVersion}`);
    console.log(`\n✓ Successfully bumped ${packId} with ${bumpType} version bump`);
    console.log('\nNext steps:');
    console.log(`1. Test the changes`);
    console.log(`2. Commit: git add -A && git commit -m "chore: bump ${packId} version (${bumpType})"`);

  } catch (error) {
    console.error('Error updating version:', error.message);
    process.exit(1);
  }
}

updateVersion();

#!/usr/bin/env node

const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');

const args = process.argv.slice(2);

if (args.length < 2) {
  console.log('Usage: node update-expansion-version.js <expansion-pack-id> <new-version>');
  console.log('Example: node update-expansion-version.js bmad-creator-tools 1.1.0');
  process.exit(1);
}

const [packId, newVersion] = args;

// Validate version format
if (!/^\d+\.\d+\.\d+$/.test(newVersion)) {
  console.error('Error: Version must be in format X.Y.Z (e.g., 1.2.3)');
  process.exit(1);
}

async function updateVersion() {
  try {
    // Update in config.yaml
    const configPath = path.join(__dirname, '..', 'expansion-packs', packId, 'config.yaml');

    if (!fs.existsSync(configPath)) {
      console.error(`Error: Expansion pack '${packId}' not found`);
      process.exit(1);
    }

    const configContent = fs.readFileSync(configPath, 'utf8');
    const config = yaml.load(configContent);
    const oldVersion = config.version || 'unknown';

    config.version = newVersion;

    const updatedYaml = yaml.dump(config, { indent: 2 });
    fs.writeFileSync(configPath, updatedYaml);

    console.log(`✓ Updated ${packId}/config.yaml: ${oldVersion} → ${newVersion}`);
    console.log(`\n✓ Successfully updated ${packId} to version ${newVersion}`);
    console.log('\nNext steps:');
    console.log('1. Test the changes');
    console.log('2. Commit: git add -A && git commit -m "chore: bump ' + packId + ' to v' + newVersion + '"');

  } catch (error) {
    console.error('Error updating version:', error.message);
    process.exit(1);
  }
}

updateVersion();

/**
 * Memory Profiler - Track memory usage during installation
 * Helps identify memory leaks and optimize resource usage
 */

const v8 = require('v8');

class MemoryProfiler {
  constructor() {
    this.checkpoints = [];
    this.startTime = Date.now();
    this.peakMemory = 0;
  }

  /**
   * Create a memory checkpoint
   * @param {string} label - Label for this checkpoint
   */
  checkpoint(label) {
    const memUsage = process.memoryUsage();
    const heapStats = v8.getHeapStatistics();

    const checkpoint = {
      label,
      timestamp: Date.now() - this.startTime,
      memory: {
        rss: this.formatBytes(memUsage.rss),
        heapTotal: this.formatBytes(memUsage.heapTotal),
        heapUsed: this.formatBytes(memUsage.heapUsed),
        external: this.formatBytes(memUsage.external),
        arrayBuffers: this.formatBytes(memUsage.arrayBuffers || 0)
      },
      heap: {
        totalHeapSize: this.formatBytes(heapStats.total_heap_size),
        usedHeapSize: this.formatBytes(heapStats.used_heap_size),
        heapSizeLimit: this.formatBytes(heapStats.heap_size_limit),
        mallocedMemory: this.formatBytes(heapStats.malloced_memory),
        externalMemory: this.formatBytes(heapStats.external_memory)
      },
      raw: {
        heapUsed: memUsage.heapUsed
      }
    };

    // Track peak memory
    if (memUsage.heapUsed > this.peakMemory) {
      this.peakMemory = memUsage.heapUsed;
    }

    this.checkpoints.push(checkpoint);
    return checkpoint;
  }

  /**
   * Force garbage collection (requires --expose-gc flag)
   */
  forceGC() {
    if (global.gc) {
      global.gc();
      return true;
    }
    return false;
  }

  /**
   * Get memory usage summary
   */
  getSummary() {
    const currentMemory = process.memoryUsage();

    return {
      currentUsage: {
        rss: this.formatBytes(currentMemory.rss),
        heapTotal: this.formatBytes(currentMemory.heapTotal),
        heapUsed: this.formatBytes(currentMemory.heapUsed)
      },
      peakMemory: this.formatBytes(this.peakMemory),
      totalCheckpoints: this.checkpoints.length,
      runTime: `${((Date.now() - this.startTime) / 1000).toFixed(2)}s`
    };
  }

  /**
   * Get detailed report of memory usage
   */
  getDetailedReport() {
    const summary = this.getSummary();
    const memoryGrowth = this.calculateMemoryGrowth();

    return {
      summary,
      memoryGrowth,
      checkpoints: this.checkpoints,
      recommendations: this.getRecommendations(memoryGrowth)
    };
  }

  /**
   * Calculate memory growth between checkpoints
   */
  calculateMemoryGrowth() {
    if (this.checkpoints.length < 2) return [];

    const growth = [];
    for (let i = 1; i < this.checkpoints.length; i++) {
      const prev = this.checkpoints[i - 1];
      const curr = this.checkpoints[i];

      const heapDiff = curr.raw.heapUsed - prev.raw.heapUsed;

      growth.push({
        from: prev.label,
        to: curr.label,
        heapGrowth: this.formatBytes(Math.abs(heapDiff)),
        isIncrease: heapDiff > 0,
        timeDiff: `${((curr.timestamp - prev.timestamp) / 1000).toFixed(2)}s`
      });
    }

    return growth;
  }

  /**
   * Get recommendations based on memory usage
   */
  getRecommendations(memoryGrowth) {
    const recommendations = [];

    // Check for large memory growth
    const largeGrowths = memoryGrowth.filter(g => {
      const bytes = this.parseBytes(g.heapGrowth);
      return bytes > 50 * 1024 * 1024; // 50MB
    });

    if (largeGrowths.length > 0) {
      recommendations.push({
        type: 'warning',
        message: `Large memory growth detected in ${largeGrowths.length} operations`,
        details: largeGrowths.map(g => `${g.from} → ${g.to}: ${g.heapGrowth}`)
      });
    }

    // Check peak memory
    if (this.peakMemory > 500 * 1024 * 1024) { // 500MB
      recommendations.push({
        type: 'warning',
        message: `High peak memory usage: ${this.formatBytes(this.peakMemory)}`,
        suggestion: 'Consider processing files in smaller batches'
      });
    }

    // Check for potential memory leaks
    const continuousGrowth = this.checkContinuousGrowth();
    if (continuousGrowth) {
      recommendations.push({
        type: 'error',
        message: 'Potential memory leak detected',
        details: 'Memory usage continuously increases without significant decreases'
      });
    }

    return recommendations;
  }

  /**
   * Check for continuous memory growth (potential leak)
   */
  checkContinuousGrowth() {
    if (this.checkpoints.length < 5) return false;

    let increasingCount = 0;
    for (let i = 1; i < this.checkpoints.length; i++) {
      if (this.checkpoints[i].raw.heapUsed > this.checkpoints[i - 1].raw.heapUsed) {
        increasingCount++;
      }
    }

    // If memory increases in more than 80% of checkpoints, might be a leak
    return increasingCount / (this.checkpoints.length - 1) > 0.8;
  }

  /**
   * Format bytes to human-readable string
   */
  formatBytes(bytes) {
    if (bytes === 0) return '0 B';

    const k = 1024;
    const sizes = ['B', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));

    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  }

  /**
   * Parse human-readable bytes back to number
   */
  parseBytes(str) {
    const match = str.match(/^([\d.]+)\s*([KMGT]?B?)$/i);
    if (!match) return 0;

    const value = parseFloat(match[1]);
    const unit = match[2].toUpperCase();

    const multipliers = {
      'B': 1,
      'KB': 1024,
      'MB': 1024 * 1024,
      'GB': 1024 * 1024 * 1024
    };

    return value * (multipliers[unit] || 1);
  }

  /**
   * Clear checkpoints to free memory
   */
  clear() {
    this.checkpoints = [];
  }
}

// Export singleton instance
module.exports = new MemoryProfiler();

#!/usr/bin/env node

/**
 * BMad Method CLI - Direct execution wrapper for npx
 * This file ensures proper execution when run via npx from GitHub
 */

const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');

// Check if we're running in an npx temporary directory
const isNpxExecution = __dirname.includes('_npx') || __dirname.includes('.npm');

// If running via npx, we need to handle things differently
if (isNpxExecution) {
  // The actual bmad.js is in installer/bin/ (relative to tools directory)
  const bmadScriptPath = path.join(__dirname, 'installer', 'bin', 'bmad.js');

  // Verify the file exists
  if (!fs.existsSync(bmadScriptPath)) {
    console.error('Error: Could not find bmad.js at', bmadScriptPath);
    console.error('Current directory:', __dirname);
    process.exit(1);
  }

  // Execute with proper working directory
  try {
    execSync(`node "${bmadScriptPath}" ${process.argv.slice(2).join(' ')}`, {
      stdio: 'inherit',
      cwd: path.dirname(__dirname)
    });
  } catch (error) {
    // execSync will throw if the command exits with non-zero
    // But the stdio is inherited, so the error is already displayed
    process.exit(error.status || 1);
  }
} else {
  // Local execution - just require the installer directly
  require('./installer/bin/bmad.js');
}

/**
 * Utility functions for YAML extraction from agent files
 */

/**
 * Extract YAML content from agent markdown files
 * @param {string} agentContent - The full content of the agent file
 * @param {boolean} cleanCommands - Whether to clean command descriptions (default: false)
 * @returns {string|null} - The extracted YAML content or null if not found
 */
function extractYamlFromAgent(agentContent, cleanCommands = false) {
  // Remove carriage returns and match YAML block
  const yamlMatch = agentContent.replace(/\r/g, "").match(/```ya?ml\n([\s\S]*?)\n```/);
  if (!yamlMatch) return null;

  let yamlContent = yamlMatch[1].trim();

  // Clean up command descriptions if requested
  // Converts "- command - description" to just "- command"
  if (cleanCommands) {
    yamlContent = yamlContent.replace(/^(\s*-)(\s*"[^"]+")(\s*-\s*.*)$/gm, '$1$2');
  }

  return yamlContent;
}

module.exports = {
  extractYamlFromAgent
};

/**
 * Semantic-release plugin to sync installer package.json version
 */

const fs = require('fs');
const path = require('path');

// This function runs during the "prepare" step of semantic-release
function prepare(_, { nextRelease, logger }) {
  // Define the path to the installer package.json file
  const file = path.join(process.cwd(), 'tools/installer/package.json');

  // If the file does not exist, skip syncing and log a message
  if (!fs.existsSync(file)) return logger.log('Installer package.json not found, skipping');

  // Read and parse the package.json file
  const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));

  // Update the version field with the next release version
  pkg.version = nextRelease.version;

  // Write the updated JSON back to the file
  fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + '\n');

  // Log success message
  logger.log(`Synced installer package.json to version ${nextRelease.version}`);
}

// Export the prepare function so semantic-release can use it
module.exports = { prepare };

#!/usr/bin/env node

/**
 * Sync installer package.json version with main package.json
 * Used by semantic-release to keep versions in sync
 */

const fs = require('fs');
const path = require('path');

function syncInstallerVersion() {
  // Read main package.json
  const mainPackagePath = path.join(__dirname, '..', 'package.json');
  const mainPackage = JSON.parse(fs.readFileSync(mainPackagePath, 'utf8'));

  // Read installer package.json
  const installerPackagePath = path.join(__dirname, 'installer', 'package.json');
  const installerPackage = JSON.parse(fs.readFileSync(installerPackagePath, 'utf8'));

  // Update installer version to match main version
  installerPackage.version = mainPackage.version;

  // Write back installer package.json
  fs.writeFileSync(installerPackagePath, JSON.stringify(installerPackage, null, 2) + '\n');

  console.log(`Synced installer version to ${mainPackage.version}`);
}

// Run if called directly
if (require.main === module) {
  syncInstallerVersion();
}

module.exports = { syncInstallerVersion };

class
V3ToV4Upgrader
{
constructor () {}
async upgrade (options = {}) {}
async getProjectPath (providedPath) {}
async validateV3Project (projectPath) {}
async analyzeProject (projectPath) {}
async showPreflightCheck (analysis, options) {}
async createBackup (projectPath) {}
async installV4Structure (projectPath) {}
async migrateDocuments (projectPath, analysis) {}
async setupIDE (projectPath, selectedIdes) {}
showCompletionReport (projectPath, analysis) {}
async pathExists (filePath) {}
async copyDirectory (src, dest) {}
async createPrdIndex (projectPath, analysis) {}
async createInstallManifest (projectPath) {}
}

This code segment functions to: Facilitate the migration process between software versions by validating existing project structures, transforming legacy document formats, and guiding user interactions through automated upgrade steps while preserving critical project assets and generating system compatibility configurations.

```javascript
[TASK_BEGIN]
class V3ToV4Upgrader {
  constructor() {
    this.backupPath = null;
    this.ides = ['vscode', 'webstorm'];
  }

  async upgrade(options = {}) {
    await initializeModules();
    const projectPath = await this.getProjectPath(options.path);
    await this.validateV3Project(projectPath);
    const analysis = await this.analyzeProject(projectPath);
    await this.showPreflightCheck(analysis, options);
    await this.createBackup(projectPath);
    await this.installV4Structure(projectPath);
    await this.migrateDocuments(projectPath, analysis);
    await this.setupIDE(projectPath, options.ide || []);
    this.showCompletionReport(projectPath, analysis);
  }

  async getProjectPath(providedPath) {
    if (providedPath) return path.resolve(providedPath);
    const matches = await glob('**/config.xml', { ignore: 'node_modules/**' });
    if (matches.length === 0) throw new Error('No v3 project found');
    return path.dirname(matches[0]);
  }

  async validateV3Project(projectPath) {
    const requiredFiles = ['config.xml', 'documenttypes.xml'];
    for (const file of requiredFiles) {
      if (!await this.pathExists(path.join(projectPath, file))) {
        throw new Error(`Invalid v3 project: Missing ${file}`);
      }
    }
  }

  async analyzeProject(projectPath) {
    const documentTypes = await glob(path.join(projectPath, '**/*.doctype.xml'));
    const templates = await glob(path.join(projectPath, '**/*.template.html'));
    return {
      docTypes: documentTypes.length,
      templates: templates.length,
      hasLegacyPlugins: await this.pathExists(path.join(projectPath, 'plugins'))
    };
  }

  async showPreflightCheck(analysis, options) {
    const spinner = ora('Analyzing project').start();
    await new Promise(resolve => setTimeout(resolve, 1000));
    spinner.succeed(`Found ${analysis.docTypes} document types and ${analysis.templates} templates`);

    if (!options.force) {
      const { confirm } = await inquirer.prompt({
        type: 'confirm',
        name: 'confirm',
        message: 'Proceed with upgrade?',
        default: true
      });
      if (!confirm) process.exit(0);
    }
  }

  async createBackup(projectPath) {
    const backupDir = path.join(projectPath, 'backup');
    this.backupPath = path.join(backupDir, `v3-backup-${Date.now()}`);
    await this.copyDirectory(projectPath, this.backupPath);
  }

  async installV4Structure(projectPath) {
    const templatePath = path.join(__dirname, 'templates/v4-structure');
    await this.copyDirectory(templatePath, projectPath);
  }

  async migrateDocuments(projectPath, analysis) {
    const docsDest = path.join(projectPath, 'src/document-types');
    await fs.mkdir(docsDest, { recursive: true });
    await this.copyDirectory(path.join(projectPath, 'document-types'), docsDest);
  }

  async setupIDE(projectPath, selectedIdes) {
    for (const ide of selectedIdes) {
      await this.copyDirectory(
        path.join(__dirname, `templates/ide/${ide}`),
        projectPath
      );
    }
  }

  showCompletionReport(projectPath, analysis) {
    console.log(chalk.green('\nUpgrade completed successfully!'));
    console.log(chalk.yellow(`\nBackup created at: ${this.backupPath}`));
    if (analysis.hasLegacyPlugins) {
      console.log(chalk.yellow('NOTE: Manual migration required for legacy plugins'));
    }
  }

  async pathExists(filePath) {
    try {
      await fs.access(filePath);
      return true;
    } catch {
      return false;
    }
  }

  async copyDirectory(src, dest) {
    await fs.mkdir(dest, { recursive: true });
    const entries = await fs.readdir(src, { withFileTypes: true });
    for (const entry of entries) {
      const srcPath = path.join(src, entry.name);
      const destPath = path.join(dest, entry.name);
      entry.isDirectory() ?
        await this.copyDirectory(srcPath, destPath) :
        await fs.copyFile(srcPath, destPath);
    }
  }

  async createPrdIndex(projectPath, analysis) {
    const indexPath = path.join(projectPath, 'src/prd-index.js');
    const content = `// Auto-generated index file\n\nexport const documentTypes = ${analysis.docTypes};\n`;
    await fs.writeFile(indexPath, content);
  }

  async createInstallManifest(projectPath) {
    const manifestPath = path.join(projectPath, 'upgrade-manifest.json');
    const manifest = {
      timestamp: new Date().toISOString(),
      backupLocation: this.backupPath,
      version: '4.0.0'
    };
    await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
  }
}
[TASK_END]
```

class V3ToV4Upgrader {
  constructor() {
    this.backupPath = null;
    this.ides = ['vscode', 'webstorm'];
  }
  async upgrade(options = {}) {
    await initializeModules();
    const projectPath = await this.getProjectPath(options.path);
    await this.validateV3Project(projectPath);
    const analysis = await this.analyzeProject(projectPath);
    await this.showPreflightCheck(analysis, options);
    await this.createBackup(projectPath);
    await this.installV4Structure(projectPath);
    await this.migrateDocuments(projectPath, analysis);
    await this.setupIDE(projectPath, options.ide || []);
    this.showCompletionReport(projectPath, analysis);
  }
  async getProjectPath(providedPath) {
    if (providedPath) return path.resolve(providedPath);
    const matches = await glob('**/config.xml', { ignore: 'node_modules*.doctype.xml'));
    const templates = await glob(path.join(projectPath, '**/*.template.html'));
    return {
      docTypes: documentTypes.length,
      templates: templates.length,
      hasLegacyPlugins: await this.pathExists(path.join(projectPath, 'plugins'))
    };
  }
  async showPreflightCheck(analysis, options) {
    const spinner = ora('Analyzing project').start();
    await new Promise(resolve => setTimeout(resolve, 1000));
    spinner.succeed(`Found ${analysis.docTypes} document types and ${analysis.templates} templates`);
    if (!options.force) {
      const { confirm } = await inquirer.prompt({
        type: 'confirm',
        name: 'confirm',
        message: 'Proceed with upgrade?',
        default: true
      });
      if (!confirm) process.exit(0);
    }
  }
  async createBackup(projectPath) {
    const backupDir = path.join(projectPath, 'backup');
    this.backupPath = path.join(backupDir, `v3-backup-${Date.now()}`);
    await this.copyDirectory(projectPath, this.backupPath);
  }
  async installV4Structure(projectPath) {
    const templatePath = path.join(__dirname, 'templates/v4-structure');
    await this.copyDirectory(templatePath, projectPath);
  }
  async migrateDocuments(projectPath, analysis) {
    const docsDest = path.join(projectPath, 'src/document-types');
    await fs.mkdir(docsDest, { recursive: true });
    await this.copyDirectory(path.join(projectPath, 'document-types'), docsDest);
  }
  async setupIDE(projectPath, selectedIdes) {
    for (const ide of selectedIdes) {
      await this.copyDirectory(
        path.join(__dirname, `templates/ide/${ide}`),
        projectPath
      );
    }
  }
  showCompletionReport(projectPath, analysis) {
    console.log(chalk.green('\nUpgrade completed successfully!'));
    console.log(chalk.yellow(`\nBackup created at: ${this.backupPath}`));
    if (analysis.hasLegacyPlugins) {
      console.log(chalk.yellow('NOTE: Manual migration required for legacy plugins'));
    }
  }
  async pathExists(filePath) {
    try {
      await fs.access(filePath);
      return true;
    } catch {
      return false;
    }
  }
  async copyDirectory(src, dest) {
    await fs.mkdir(dest, { recursive: true });
    const entries = await fs.readdir(src, { withFileTypes: true });
    for (const entry of entries) {
      const srcPath = path.join(src, entry.name);
      const destPath = path.join(dest, entry.name);
      entry.isDirectory() ?
        await this.copyDirectory(srcPath, destPath) :
        await fs.copyFile(srcPath, destPath);
    }
  }
  async createPrdIndex(projectPath, analysis) {
    const indexPath = path.join(projectPath, 'src/prd-index.js');
    const content = `
    await fs.writeFile(indexPath, content);
  }
  async createInstallManifest(projectPath) {
    const manifestPath = path.join(projectPath, 'upgrade-manifest.json');
    const manifest = {
      timestamp: new Date().toISOString(),
      backupLocation: this.backupPath,
      version: '4.0.0'
    };
    await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
  }
}