From 537ff0cbf0b0072fdb224a57fb193a1bb84d1080 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Tue, 7 Apr 2026 21:03:20 -0500 Subject: [PATCH] refactor(installer): remove custom content installation feature Remove the entire local filesystem custom content feature from the installer to make way for marketplace-based plugin installation. Deleted: custom-handler.js, custom-module-cache.js, custom-modules.js Removed: --custom-content CLI flag, interactive custom content prompts, custom module caching, manifest tracking, missing-source resolution, and related test suites. Updated docs across all translations. --- .../cs/how-to/non-interactive-installation.md | 20 +- docs/fr/how-to/install-bmad.md | 2 +- .../fr/how-to/non-interactive-installation.md | 23 +- docs/how-to/install-bmad.md | 2 +- docs/how-to/non-interactive-installation.md | 23 +- docs/vi-vn/how-to/install-bmad.md | 2 +- .../how-to/non-interactive-installation.md | 21 +- docs/zh-cn/how-to/install-bmad.md | 2 +- .../how-to/non-interactive-installation.md | 23 +- test/test-installation-components.js | 151 ----- tools/installer/commands/install.js | 1 - tools/installer/core/custom-module-cache.js | 260 -------- tools/installer/core/existing-install.js | 10 +- tools/installer/core/install-paths.js | 3 - tools/installer/core/installer.js | 401 +----------- tools/installer/core/manifest-generator.js | 9 - tools/installer/core/manifest.js | 71 +- tools/installer/custom-handler.js | 112 ---- tools/installer/modules/custom-modules.js | 302 --------- tools/installer/modules/official-modules.js | 76 +-- tools/installer/ui.js | 614 +----------------- 21 files changed, 34 insertions(+), 2094 deletions(-) delete mode 100644 tools/installer/core/custom-module-cache.js delete mode 100644 tools/installer/custom-handler.js delete mode 100644 tools/installer/modules/custom-modules.js diff --git a/docs/cs/how-to/non-interactive-installation.md b/docs/cs/how-to/non-interactive-installation.md index f6b46c5e2..9e9b1ca3e 100644 --- a/docs/cs/how-to/non-interactive-installation.md +++ b/docs/cs/how-to/non-interactive-installation.md @@ -27,7 +27,6 @@ Vyžaduje [Node.js](https://nodejs.org) v20+ a `npx` (součástí npm). | `--directory ` | Instalační adresář | `--directory ~/projects/myapp` | | `--modules ` | Čárkou oddělená ID modulů | `--modules bmm,bmb` | | `--tools ` | Čárkou oddělená ID nástrojů/IDE (použijte `none` pro přeskočení) | `--tools claude-code,cursor` nebo `--tools none` | -| `--custom-content ` | Čárkou oddělené cesty k vlastním modulům | `--custom-content ~/my-module,~/another-module` | | `--action ` | Akce pro existující instalace: `install` (výchozí), `update` nebo `quick-update` | `--action quick-update` | ### Základní konfigurace @@ -108,16 +107,6 @@ npx bmad-method install \ --action quick-update ``` -### Instalace s vlastním obsahem - -```bash -npx bmad-method install \ - --directory ~/projects/myapp \ - --modules bmm \ - --custom-content ~/my-custom-module,~/another-module \ - --tools claude-code -``` - ## Co získáte - Plně nakonfigurovaný adresář `_bmad/` ve vašem projektu @@ -159,13 +148,6 @@ Neplatné hodnoty buď: - Ověřte, že ID modulu je správné - Externí moduly musí být dostupné v registru -### Neplatná cesta k vlastnímu obsahu - -Ujistěte se, že každá cesta k vlastnímu obsahu: -- Ukazuje na adresář -- Obsahuje soubor `module.yaml` v kořeni -- Má pole `code` v `module.yaml` - -:::note[Stále jste uvízli?] +::: note[Stále jste uvízli?] Spusťte s `--debug` pro detailní výstup, zkuste interaktivní režim pro izolaci problému, nebo nahlaste na . ::: diff --git a/docs/fr/how-to/install-bmad.md b/docs/fr/how-to/install-bmad.md index 4f79743ea..c58f00c23 100644 --- a/docs/fr/how-to/install-bmad.md +++ b/docs/fr/how-to/install-bmad.md @@ -72,7 +72,7 @@ L'installateur affiche les modules disponibles. Sélectionnez ceux dont vous ave ### 5. Suivre les instructions -L'installateur vous guide pour le reste — contenu personnalisé, paramètres, etc. +L'installateur vous guide pour le reste — paramètres, intégrations d'outils, etc. ## Ce que vous obtenez diff --git a/docs/fr/how-to/non-interactive-installation.md b/docs/fr/how-to/non-interactive-installation.md index ee6ddad1c..080c98650 100644 --- a/docs/fr/how-to/non-interactive-installation.md +++ b/docs/fr/how-to/non-interactive-installation.md @@ -27,7 +27,6 @@ Nécessite [Node.js](https://nodejs.org) v20+ et `npx` (inclus avec npm). | `--directory ` | Répertoire d'installation | `--directory ~/projects/myapp` | | `--modules ` | IDs de modules séparés par des virgules | `--modules bmm,bmb` | | `--tools ` | IDs d'outils/IDE séparés par des virgules (utilisez `none` pour ignorer) | `--tools claude-code,cursor` ou `--tools none` | -| `--custom-content ` | Chemins vers des modules personnalisés séparés par des virgules | `--custom-content ~/my-module,~/another-module` | | `--action ` | Action pour les installations existantes : `install` (par défaut), `update`, ou `quick-update` | `--action quick-update` | ### Configuration principale @@ -120,16 +119,6 @@ npx bmad-method install \ --action quick-update ``` -### Installation avec du contenu personnalisé - -```bash -npx bmad-method install \ - --directory ~/projects/myapp \ - --modules bmm \ - --custom-content ~/my-custom-module,~/another-module \ - --tools claude-code -``` - ## Ce que vous obtenez - Un répertoire `_bmad/` entièrement configuré dans votre projet @@ -143,12 +132,11 @@ BMad valide toutes les options fournis : - **Directory** — Doit être un chemin valide avec des permissions d'écriture - **Modules** — Avertit des IDs de modules invalides (mais n'échoue pas) - **Tools** — Avertit des IDs d'outils invalides (mais n'échoue pas) -- **Custom Content** — Chaque chemin doit contenir un fichier `module.yaml` valide - **Action** — Doit être l'une des suivantes : `install`, `update`, `quick-update` Les valeurs invalides entraîneront soit : 1. L’affichage d’un message d'erreur suivi d’un exit (pour les options critiques comme le répertoire) -2. Un avertissement puis la continuation de l’installation (pour les éléments optionnels comme le contenu personnalisé) +2. Un avertissement puis la continuation de l’installation (pour les éléments optionnels) 3. Un retour aux invites interactives (pour les valeurs requises manquantes) :::tip[Bonnes pratiques] @@ -172,13 +160,6 @@ Les valeurs invalides entraîneront soit : - Vérifiez que l'ID du module est correct - Les modules externes doivent être disponibles dans le registre -### Chemin de contenu personnalisé invalide - -Assurez-vous que chaque chemin de contenu personnalisé : -- Pointe vers un répertoire -- Contient un fichier `module.yaml` à la racine -- Possède un champ `code` dans `module.yaml` - -:::note[Toujours bloqué ?] +::: note[Toujours bloqué ?] Exécutez avec `--debug` pour une sortie détaillée, essayez le mode interactif pour isoler le problème, ou signalez-le à . ::: diff --git a/docs/how-to/install-bmad.md b/docs/how-to/install-bmad.md index 3789c6fa9..0913d1540 100644 --- a/docs/how-to/install-bmad.md +++ b/docs/how-to/install-bmad.md @@ -72,7 +72,7 @@ The installer shows available modules. Select whichever ones you need — most u ### 5. Follow the Prompts -The installer guides you through the rest — custom content, settings, etc. +The installer guides you through the rest — settings, tool integrations, etc. ## What You Get diff --git a/docs/how-to/non-interactive-installation.md b/docs/how-to/non-interactive-installation.md index 64687c0a1..1a2340d79 100644 --- a/docs/how-to/non-interactive-installation.md +++ b/docs/how-to/non-interactive-installation.md @@ -27,7 +27,6 @@ Requires [Node.js](https://nodejs.org) v20+ and `npx` (included with npm). | `--directory ` | Installation directory | `--directory ~/projects/myapp` | | `--modules ` | Comma-separated module IDs | `--modules bmm,bmb` | | `--tools ` | Comma-separated tool/IDE IDs (use `none` to skip) | `--tools claude-code,cursor` or `--tools none` | -| `--custom-content ` | Comma-separated paths to custom modules | `--custom-content ~/my-module,~/another-module` | | `--action ` | Action for existing installations: `install` (default), `update`, or `quick-update` | `--action quick-update` | ### Core Configuration @@ -120,16 +119,6 @@ npx bmad-method install \ --action quick-update ``` -### Installation with Custom Content - -```bash -npx bmad-method install \ - --directory ~/projects/myapp \ - --modules bmm \ - --custom-content ~/my-custom-module,~/another-module \ - --tools claude-code -``` - ## What You Get - A fully configured `_bmad/` directory in your project @@ -143,12 +132,11 @@ BMad validates all provided flags: - **Directory** — Must be a valid path with write permissions - **Modules** — Warns about invalid module IDs (but won't fail) - **Tools** — Warns about invalid tool IDs (but won't fail) -- **Custom Content** — Each path must contain a valid `module.yaml` file - **Action** — Must be one of: `install`, `update`, `quick-update` Invalid values will either: 1. Show an error and exit (for critical options like directory) -2. Show a warning and skip (for optional items like custom content) +2. Show a warning and skip (for optional items) 3. Fall back to interactive prompts (for missing required values) :::tip[Best Practices] @@ -172,13 +160,6 @@ Invalid values will either: - Verify the module ID is correct - External modules must be available in the registry -### Custom content path invalid - -Ensure each custom content path: -- Points to a directory -- Contains a `module.yaml` file in the root -- Has a `code` field in the `module.yaml` - -:::note[Still stuck?] +::: note[Still stuck?] Run with `--debug` for detailed output, try interactive mode to isolate the issue, or report at . ::: diff --git a/docs/vi-vn/how-to/install-bmad.md b/docs/vi-vn/how-to/install-bmad.md index 57105864c..c73e89388 100644 --- a/docs/vi-vn/how-to/install-bmad.md +++ b/docs/vi-vn/how-to/install-bmad.md @@ -72,7 +72,7 @@ Trình cài đặt sẽ hiện các module có sẵn. Chọn những module bạ ### 5. Làm theo các prompt -Trình cài đặt sẽ hướng dẫn các bước còn lại - nội dung tùy chỉnh, cài đặt, và các tùy chọn khác. +Trình cài đặt sẽ hướng dẫn các bước còn lại - cài đặt, tích hợp công cụ, và các tùy chọn khác. ## Bạn nhận được gì diff --git a/docs/vi-vn/how-to/non-interactive-installation.md b/docs/vi-vn/how-to/non-interactive-installation.md index 2ba75b7ec..968de3618 100644 --- a/docs/vi-vn/how-to/non-interactive-installation.md +++ b/docs/vi-vn/how-to/non-interactive-installation.md @@ -27,7 +27,6 @@ Yêu cầu [Node.js](https://nodejs.org) v20+ và `npx` (đi kèm với npm). | `--directory ` | Thư mục cài đặt | `--directory ~/projects/myapp` | | `--modules ` | Danh sách ID module, cách nhau bởi dấu phẩy | `--modules bmm,bmb` | | `--tools ` | Danh sách ID công cụ/IDE, cách nhau bởi dấu phẩy (dùng `none` để bỏ qua) | `--tools claude-code,cursor` hoặc `--tools none` | -| `--custom-content ` | Danh sách đường dẫn đến module tùy chỉnh, cách nhau bởi dấu phẩy | `--custom-content ~/my-module,~/another-module` | | `--action ` | Hành động cho bản cài đặt hiện có: `install` (mặc định), `update`, hoặc `quick-update` | `--action quick-update` | ### Cấu hình cốt lõi @@ -120,16 +119,6 @@ npx bmad-method install \ --action quick-update ``` -### Cài đặt với nội dung tùy chỉnh - -```bash -npx bmad-method install \ - --directory ~/projects/myapp \ - --modules bmm \ - --custom-content ~/my-custom-module,~/another-module \ - --tools claude-code -``` - ## Bạn nhận được gì - Thư mục `_bmad/` đã được cấu hình đầy đủ trong dự án của bạn @@ -143,12 +132,11 @@ BMad sẽ kiểm tra tất cả các cờ được cung cấp: - **Directory** - Phải là đường dẫn hợp lệ và có quyền ghi - **Modules** - Cảnh báo nếu ID module không hợp lệ (nhưng không thất bại) - **Tools** - Cảnh báo nếu ID công cụ không hợp lệ (nhưng không thất bại) -- **Custom Content** - Mỗi đường dẫn phải chứa tệp `module.yaml` hợp lệ - **Action** - Phải là một trong: `install`, `update`, `quick-update` Giá trị không hợp lệ sẽ dẫn đến một trong các trường hợp sau: 1. Hiện lỗi và thoát (với các tùy chọn quan trọng như directory) -2. Hiện cảnh báo và bỏ qua (với mục tùy chọn như custom content) +2. Hiện cảnh báo và bỏ qua (với mục tùy chọn) 3. Quay lại hỏi interactive (với giá trị bắt buộc bị thiếu) :::tip[Thực hành tốt] @@ -172,13 +160,6 @@ Giá trị không hợp lệ sẽ dẫn đến một trong các trường hợp - Xác minh ID module có đúng không - Module bên ngoài phải có sẵn trong registry -### Đường dẫn custom content không hợp lệ - -Đảm bảo mỗi đường dẫn custom content: -- Trỏ tới một thư mục -- Chứa tệp `module.yaml` ở cấp gốc -- Có trường `code` trong tệp `module.yaml` - :::note[Vẫn bị mắc?] Chạy với `--debug` để xem output chi tiết, thử chế độ interactive để cô lập vấn đề, hoặc báo cáo tại . ::: diff --git a/docs/zh-cn/how-to/install-bmad.md b/docs/zh-cn/how-to/install-bmad.md index e9fc1af9a..3c5ceff44 100644 --- a/docs/zh-cn/how-to/install-bmad.md +++ b/docs/zh-cn/how-to/install-bmad.md @@ -72,7 +72,7 @@ npx github:bmad-code-org/BMAD-METHOD install ### 5. 按照提示操作 -安装程序会引导你完成剩余步骤——自定义内容、设置等。 +安装程序会引导你完成剩余步骤——设置、工具集成等。 ## 你将获得 diff --git a/docs/zh-cn/how-to/non-interactive-installation.md b/docs/zh-cn/how-to/non-interactive-installation.md index df7259d97..aaff0fa23 100644 --- a/docs/zh-cn/how-to/non-interactive-installation.md +++ b/docs/zh-cn/how-to/non-interactive-installation.md @@ -27,7 +27,6 @@ sidebar: | `--directory ` | 安装目录 | `--directory ~/projects/myapp` | | `--modules ` | 逗号分隔的模块 ID | `--modules bmm,bmb` | | `--tools ` | 逗号分隔的工具/IDE ID(使用 `none` 跳过) | `--tools claude-code,cursor` 或 `--tools none` | -| `--custom-content ` | 逗号分隔的自定义模块路径 | `--custom-content ~/my-module,~/another-module` | | `--action ` | 对现有安装的操作:`install`(默认)、`update` 或 `quick-update` | `--action quick-update` | ### 核心配置 @@ -108,16 +107,6 @@ npx bmad-method install \ --action quick-update ``` -### 使用自定义内容安装 - -```bash -npx bmad-method install \ - --directory ~/projects/myapp \ - --modules bmm \ - --custom-content ~/my-custom-module,~/another-module \ - --tools claude-code -``` - ## 安装结果 - 项目中完全配置的 `_bmad/` 目录 @@ -131,12 +120,11 @@ BMad 会验证你提供的所有参数: - **目录** — 必须是具有写入权限的有效路径 - **模块** — 对无效的模块 ID 发出警告(但不会失败) - **工具** — 对无效的工具 ID 发出警告(但不会失败) -- **自定义内容** — 每个路径必须包含有效的 `module.yaml` 文件 - **操作** — 必须是以下之一:`install`、`update`、`quick-update` 无效值将: 1. 显示错误并退出(对于目录等关键选项) -2. 显示警告并跳过(对于自定义内容等可选项目) +2. 显示警告并跳过(对于可选项目) 3. 回退到交互式提示(对于缺失的必需值) :::tip[最佳实践] @@ -159,14 +147,7 @@ BMad 会验证你提供的所有参数: - 验证模块 ID 是否正确 - 外部模块必须在注册表中可用 -### 自定义内容路径无效 - -确保每个自定义内容路径: -- 指向一个目录 -- 在根目录中包含 `module.yaml` 文件 -- 在 `module.yaml` 中有 `code` 字段 - -:::note[仍然卡住了?] +::: note[仍然卡住了?] 使用 `--debug` 获取详细输出,尝试交互模式定位问题,或在 提交反馈。 ::: diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 6913a6bf5..82094165a 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -128,56 +128,6 @@ async function createSkillCollisionFixture() { return { root: fixtureRoot, bmadDir: fixtureDir }; } -async function createCustomModuleManifestFixture() { - const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-custom-manifest-')); - const bmadDir = path.join(fixtureRoot, '_bmad'); - const configDir = path.join(bmadDir, '_config'); - const moduleSourceDir = path.join(fixtureRoot, 'test-module-source'); - await fs.ensureDir(configDir); - await fs.ensureDir(moduleSourceDir); - - const minimalAgent = 'p'; - await fs.ensureDir(path.join(bmadDir, 'core', 'agents')); - await fs.writeFile(path.join(bmadDir, 'core', 'agents', 'test.md'), minimalAgent); - await fs.ensureDir(path.join(bmadDir, 'test-module', 'agents')); - await fs.writeFile(path.join(bmadDir, 'test-module', 'agents', 'test.md'), minimalAgent); - await fs.writeFile(path.join(moduleSourceDir, 'module.yaml'), ['code: test-module', 'name: Test Module', ''].join('\n')); - - await fs.writeFile( - path.join(configDir, 'manifest.yaml'), - [ - 'installation:', - ' version: 6.2.2', - ' installDate: 2026-03-30T00:00:00.000Z', - ' lastUpdated: 2026-03-30T00:00:00.000Z', - 'modules:', - ' - name: core', - ' version: 6.2.2', - ' installDate: 2026-03-30T00:00:00.000Z', - ' lastUpdated: 2026-03-30T00:00:00.000Z', - ' source: built-in', - ' npmPackage: null', - ' repoUrl: null', - ' - name: test-module', - ' version: null', - ' installDate: 2026-03-30T00:00:00.000Z', - ' lastUpdated: 2026-03-30T00:00:00.000Z', - ' source: custom', - ' npmPackage: null', - ' repoUrl: null', - 'customModules:', - ' - id: test-module', - ' name: "Test Module"', - ` sourcePath: ${JSON.stringify(moduleSourceDir)}`, - 'ides:', - ' - codex', - '', - ].join('\n'), - ); - - return { root: fixtureRoot, bmadDir, manifestPath: path.join(configDir, 'manifest.yaml'), moduleSourceDir }; -} - /** * Test Suite */ @@ -1773,107 +1723,6 @@ async function runTests() { console.log(''); - // ============================================================ - // Suite 33: Main manifest preserves active customModules only - // ============================================================ - console.log(`${colors.yellow}Test Suite 33: Preserve active customModules in main manifest${colors.reset}\n`); - - let customManifestFixture = null; - try { - customManifestFixture = await createCustomModuleManifestFixture(); - const yaml = require('yaml'); - const originalManifest = yaml.parse(await fs.readFile(customManifestFixture.manifestPath, 'utf8')); - originalManifest.customModules.push({ - id: 'removed-module', - name: 'Removed Module', - sourcePath: path.join(customManifestFixture.root, 'removed-module-source'), - }); - await fs.writeFile(customManifestFixture.manifestPath, yaml.stringify(originalManifest), 'utf8'); - - const generator33 = new ManifestGenerator(); - await generator33.generateManifests(customManifestFixture.bmadDir, ['core', 'test-module'], [], { ides: ['codex'] }); - - const updatedManifest = yaml.parse(await fs.readFile(customManifestFixture.manifestPath, 'utf8')); - const customModule = updatedManifest.customModules?.find((entry) => entry.id === 'test-module'); - - assert(Array.isArray(updatedManifest.customModules), 'Main manifest keeps customModules array'); - assert(customModule !== undefined, 'Main manifest preserves existing custom module entry'); - assert( - customModule && customModule.sourcePath === customManifestFixture.moduleSourceDir, - 'Main manifest preserves custom module sourcePath', - ); - assert( - !updatedManifest.customModules?.some((entry) => entry.id === 'removed-module'), - 'Main manifest drops stale custom module entries', - ); - } catch (error) { - assert(false, 'Main manifest preserves customModules test succeeds', error.message); - } finally { - if (customManifestFixture?.root) await fs.remove(customManifestFixture.root).catch(() => {}); - } - - console.log(''); - - // ============================================================ - // Suite 34: Quick update uses manifest-backed custom sources - // ============================================================ - console.log(`${colors.yellow}Test Suite 34: Quick update uses manifest-backed custom module sources${colors.reset}\n`); - - let quickUpdateFixture = null; - const originalListAvailable34 = OfficialModules.prototype.listAvailable; - const originalLoadExistingConfig34 = OfficialModules.prototype.loadExistingConfig; - const originalCollectModuleConfigQuick34 = OfficialModules.prototype.collectModuleConfigQuick; - try { - quickUpdateFixture = await createCustomModuleManifestFixture(); - const installer34 = new Installer(); - installer34.externalModuleManager.hasModule = async () => false; - installer34.externalModuleManager.listAvailable = async () => []; - - let capturedInstallConfig34 = null; - installer34.install = async (config) => { - capturedInstallConfig34 = config; - return { success: true }; - }; - - OfficialModules.prototype.listAvailable = async function () { - return { modules: [], customModules: [] }; - }; - OfficialModules.prototype.loadExistingConfig = async function () { - this.collectedConfig = this.collectedConfig || {}; - }; - OfficialModules.prototype.collectModuleConfigQuick = async function (moduleName) { - this.collectedConfig = this.collectedConfig || {}; - if (!this.collectedConfig[moduleName]) { - this.collectedConfig[moduleName] = {}; - } - return false; - }; - - await installer34.quickUpdate({ - directory: quickUpdateFixture.root, - skipPrompts: true, - }); - - const customModule34 = capturedInstallConfig34?._customModuleSources?.get('test-module'); - - assert(capturedInstallConfig34 !== null, 'Quick update forwards config to install'); - assert(customModule34 !== undefined, 'Quick update keeps manifest-backed custom module updateable'); - assert(customModule34 && customModule34.cached === false, 'Quick update uses manifest-backed source before cache'); - assert( - customModule34 && customModule34.sourcePath === quickUpdateFixture.moduleSourceDir, - 'Quick update uses preserved manifest sourcePath for custom modules', - ); - } catch (error) { - assert(false, 'Quick update manifest-backed custom source test succeeds', error.message); - } finally { - OfficialModules.prototype.listAvailable = originalListAvailable34; - OfficialModules.prototype.loadExistingConfig = originalLoadExistingConfig34; - OfficialModules.prototype.collectModuleConfigQuick = originalCollectModuleConfigQuick34; - if (quickUpdateFixture?.root) await fs.remove(quickUpdateFixture.root).catch(() => {}); - } - - console.log(''); - // ============================================================ // Summary // ============================================================ diff --git a/tools/installer/commands/install.js b/tools/installer/commands/install.js index 96f536ef4..fcac0b72d 100644 --- a/tools/installer/commands/install.js +++ b/tools/installer/commands/install.js @@ -17,7 +17,6 @@ module.exports = { '--tools ', 'Comma-separated list of tool/IDE IDs to configure (e.g., "claude-code,cursor"). Use "none" to skip tool configuration.', ], - ['--custom-content ', 'Comma-separated list of paths to custom modules/agents/workflows'], ['--action ', 'Action type for existing installations: install, update, or quick-update'], ['--user-name ', 'Name for agents to use (default: system username)'], ['--communication-language ', 'Language for agent communication (default: English)'], diff --git a/tools/installer/core/custom-module-cache.js b/tools/installer/core/custom-module-cache.js deleted file mode 100644 index 4afe77884..000000000 --- a/tools/installer/core/custom-module-cache.js +++ /dev/null @@ -1,260 +0,0 @@ -/** - * Custom Module Source Cache - * Caches custom module sources under _config/custom/ to ensure they're never lost - * and can be checked into source control - */ - -const fs = require('fs-extra'); -const path = require('node:path'); -const crypto = require('node:crypto'); -const prompts = require('../prompts'); - -class CustomModuleCache { - constructor(bmadDir) { - this.bmadDir = bmadDir; - this.customCacheDir = path.join(bmadDir, '_config', 'custom'); - this.manifestPath = path.join(this.customCacheDir, 'cache-manifest.yaml'); - } - - /** - * Ensure the custom cache directory exists - */ - async ensureCacheDir() { - await fs.ensureDir(this.customCacheDir); - } - - /** - * Get cache manifest - */ - async getCacheManifest() { - if (!(await fs.pathExists(this.manifestPath))) { - return {}; - } - - const content = await fs.readFile(this.manifestPath, 'utf8'); - const yaml = require('yaml'); - return yaml.parse(content) || {}; - } - - /** - * Update cache manifest - */ - async updateCacheManifest(manifest) { - const yaml = require('yaml'); - // Clean the manifest to remove any non-serializable values - const cleanManifest = structuredClone(manifest); - - const content = yaml.stringify(cleanManifest, { - indent: 2, - lineWidth: 0, - sortKeys: false, - }); - - await fs.writeFile(this.manifestPath, content); - } - - /** - * Stream a file into the hash to avoid loading entire file into memory - */ - async hashFileStream(filePath, hash) { - return new Promise((resolve, reject) => { - const stream = require('node:fs').createReadStream(filePath); - stream.on('data', (chunk) => hash.update(chunk)); - stream.on('end', resolve); - stream.on('error', reject); - }); - } - - /** - * Calculate hash of a file or directory using streaming to minimize memory usage - */ - async calculateHash(sourcePath) { - const hash = crypto.createHash('sha256'); - - const isDir = (await fs.stat(sourcePath)).isDirectory(); - - if (isDir) { - // For directories, hash all files - const files = []; - async function collectFiles(dir) { - const entries = await fs.readdir(dir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isFile()) { - files.push(path.join(dir, entry.name)); - } else if (entry.isDirectory() && !entry.name.startsWith('.')) { - await collectFiles(path.join(dir, entry.name)); - } - } - } - - await collectFiles(sourcePath); - files.sort(); // Ensure consistent order - - for (const file of files) { - const relativePath = path.relative(sourcePath, file); - // Hash the path first, then stream file contents - hash.update(relativePath + '|'); - await this.hashFileStream(file, hash); - } - } else { - // For single files, stream directly into hash - await this.hashFileStream(sourcePath, hash); - } - - return hash.digest('hex'); - } - - /** - * Cache a custom module source - * @param {string} moduleId - Module ID - * @param {string} sourcePath - Original source path - * @param {Object} metadata - Additional metadata to store - * @returns {Object} Cached module info - */ - async cacheModule(moduleId, sourcePath, metadata = {}) { - await this.ensureCacheDir(); - - const cacheDir = path.join(this.customCacheDir, moduleId); - const cacheManifest = await this.getCacheManifest(); - - // Check if already cached and unchanged - if (cacheManifest[moduleId]) { - const cached = cacheManifest[moduleId]; - if (cached.originalHash && cached.originalHash === (await this.calculateHash(sourcePath))) { - // Source unchanged, return existing cache info - return { - moduleId, - cachePath: cacheDir, - ...cached, - }; - } - } - - // Remove existing cache if it exists - if (await fs.pathExists(cacheDir)) { - await fs.remove(cacheDir); - } - - // Copy module to cache - await fs.copy(sourcePath, cacheDir, { - filter: (src) => { - const relative = path.relative(sourcePath, src); - // Skip node_modules, .git, and other common ignore patterns - return !relative.includes('node_modules') && !relative.startsWith('.git') && !relative.startsWith('.DS_Store'); - }, - }); - - // Calculate hash of the source - const sourceHash = await this.calculateHash(sourcePath); - const cacheHash = await this.calculateHash(cacheDir); - - // Update manifest - don't store absolute paths for portability - // Clean metadata to remove absolute paths - const cleanMetadata = { ...metadata }; - if (cleanMetadata.sourcePath) { - delete cleanMetadata.sourcePath; - } - - cacheManifest[moduleId] = { - originalHash: sourceHash, - cacheHash: cacheHash, - cachedAt: new Date().toISOString(), - ...cleanMetadata, - }; - - await this.updateCacheManifest(cacheManifest); - - return { - moduleId, - cachePath: cacheDir, - ...cacheManifest[moduleId], - }; - } - - /** - * Get cached module info - * @param {string} moduleId - Module ID - * @returns {Object|null} Cached module info or null - */ - async getCachedModule(moduleId) { - const cacheManifest = await this.getCacheManifest(); - const cached = cacheManifest[moduleId]; - - if (!cached) { - return null; - } - - const cacheDir = path.join(this.customCacheDir, moduleId); - - if (!(await fs.pathExists(cacheDir))) { - // Cache dir missing, remove from manifest - delete cacheManifest[moduleId]; - await this.updateCacheManifest(cacheManifest); - return null; - } - - // Verify cache integrity - const currentCacheHash = await this.calculateHash(cacheDir); - if (currentCacheHash !== cached.cacheHash) { - await prompts.log.warn(`Cache integrity check failed for ${moduleId}`); - } - - return { - moduleId, - cachePath: cacheDir, - ...cached, - }; - } - - /** - * Get all cached modules - * @returns {Array} Array of cached module info - */ - async getAllCachedModules() { - const cacheManifest = await this.getCacheManifest(); - const cached = []; - - for (const [moduleId, info] of Object.entries(cacheManifest)) { - const cachedModule = await this.getCachedModule(moduleId); - if (cachedModule) { - cached.push(cachedModule); - } - } - - return cached; - } - - /** - * Remove a cached module - * @param {string} moduleId - Module ID to remove - */ - async removeCachedModule(moduleId) { - const cacheManifest = await this.getCacheManifest(); - const cacheDir = path.join(this.customCacheDir, moduleId); - - // Remove cache directory - if (await fs.pathExists(cacheDir)) { - await fs.remove(cacheDir); - } - - // Remove from manifest - delete cacheManifest[moduleId]; - await this.updateCacheManifest(cacheManifest); - } - - /** - * Sync cached modules with a list of module IDs - * @param {Array} moduleIds - Module IDs to keep - */ - async syncCache(moduleIds) { - const cached = await this.getAllCachedModules(); - - for (const cachedModule of cached) { - if (!moduleIds.includes(cachedModule.moduleId)) { - await this.removeCachedModule(cachedModule.moduleId); - } - } - } -} - -module.exports = { CustomModuleCache }; diff --git a/tools/installer/core/existing-install.js b/tools/installer/core/existing-install.js index 8e86f4b03..643f1d946 100644 --- a/tools/installer/core/existing-install.js +++ b/tools/installer/core/existing-install.js @@ -10,14 +10,13 @@ const { Manifest } = require('./manifest'); class ExistingInstall { #version; - constructor({ installed, version, hasCore, modules, ides, customModules }) { + constructor({ installed, version, hasCore, modules, ides }) { this.installed = installed; this.#version = version; this.hasCore = hasCore; this.modules = Object.freeze(modules.map((m) => Object.freeze({ ...m }))); this.moduleIds = Object.freeze(this.modules.map((m) => m.id)); this.ides = Object.freeze([...ides]); - this.customModules = Object.freeze([...customModules]); Object.freeze(this); } @@ -35,7 +34,6 @@ class ExistingInstall { hasCore: false, modules: [], ides: [], - customModules: [], }); } @@ -53,15 +51,11 @@ class ExistingInstall { let hasCore = false; const modules = []; let ides = []; - let customModules = []; const manifest = new Manifest(); const manifestData = await manifest.read(bmadDir); if (manifestData) { version = manifestData.version; - if (manifestData.customModules) { - customModules = manifestData.customModules; - } if (manifestData.ides) { ides = manifestData.ides.filter((ide) => ide && typeof ide === 'string'); } @@ -120,7 +114,7 @@ class ExistingInstall { return ExistingInstall.empty(); } - return new ExistingInstall({ installed, version, hasCore, modules, ides, customModules }); + return new ExistingInstall({ installed, version, hasCore, modules, ides }); } } diff --git a/tools/installer/core/install-paths.js b/tools/installer/core/install-paths.js index 7383f9bfd..f1c50ee43 100644 --- a/tools/installer/core/install-paths.js +++ b/tools/installer/core/install-paths.js @@ -20,14 +20,12 @@ class InstallPaths { const configDir = path.join(bmadDir, '_config'); const agentsDir = path.join(configDir, 'agents'); - const customCacheDir = path.join(configDir, 'custom'); const coreDir = path.join(bmadDir, 'core'); for (const [dir, label] of [ [bmadDir, 'bmad directory'], [configDir, 'config directory'], [agentsDir, 'agents config directory'], - [customCacheDir, 'custom modules cache'], [coreDir, 'core module directory'], ]) { await ensureWritableDir(dir, label); @@ -40,7 +38,6 @@ class InstallPaths { bmadDir, configDir, agentsDir, - customCacheDir, coreDir, isUpdate, }); diff --git a/tools/installer/core/installer.js b/tools/installer/core/installer.js index bc3b3ec20..60245ce1d 100644 --- a/tools/installer/core/installer.js +++ b/tools/installer/core/installer.js @@ -2,7 +2,6 @@ const path = require('node:path'); const fs = require('fs-extra'); const { Manifest } = require('./manifest'); const { OfficialModules } = require('../modules/official-modules'); -const { CustomModules } = require('../modules/custom-modules'); const { IdeManager } = require('../ide/manager'); const { FileOps } = require('../file-ops'); const { Config } = require('./config'); @@ -19,7 +18,6 @@ class Installer { constructor() { this.externalModuleManager = new ExternalModuleManager(); this.manifest = new Manifest(); - this.customModules = new CustomModules(); this.ideManager = new IdeManager(); this.fileOps = new FileOps(); this.installedFiles = new Set(); // Track all installed files @@ -80,8 +78,6 @@ class Installer { const officialModules = await OfficialModules.build(config, paths); const existingInstall = await ExistingInstall.detect(paths.bmadDir); - await this.customModules.discoverPaths(originalConfig, paths); - if (existingInstall.installed) { await this._removeDeselectedModules(existingInstall, config, paths); updateState = await this._prepareUpdateState(paths, config, existingInstall, officialModules); @@ -121,14 +117,9 @@ class Installer { } } - await this._cacheCustomModules(paths, addResult); + const allModules = config.modules || []; - // Compute module lists: official = selected minus custom, all = both - const customModuleIds = new Set(this.customModules.paths.keys()); - const officialModuleIds = (config.modules || []).filter((m) => !customModuleIds.has(m)); - const allModules = [...officialModuleIds, ...[...customModuleIds].filter((id) => !officialModuleIds.includes(id))]; - - await this._installAndConfigure(config, originalConfig, paths, officialModuleIds, allModules, addResult, officialModules); + await this._installAndConfigure(config, originalConfig, paths, allModules, allModules, addResult, officialModules); await this._setupIdes(config, allModules, paths, addResult, previousSkillIds); @@ -242,26 +233,6 @@ class Installer { } } - /** - * Cache custom modules into the local cache directory. - * Updates this.customModules.paths in place with cached locations. - */ - async _cacheCustomModules(paths, addResult) { - if (!this.customModules.paths || this.customModules.paths.size === 0) return; - - const { CustomModuleCache } = require('./custom-module-cache'); - const customCache = new CustomModuleCache(paths.bmadDir); - - for (const [moduleId, sourcePath] of this.customModules.paths) { - const cachedInfo = await customCache.cacheModule(moduleId, sourcePath, { - sourcePath: sourcePath, - }); - this.customModules.paths.set(moduleId, cachedInfo.cachePath); - } - - addResult('Custom modules cached', 'ok'); - } - /** * Install modules, create directories, generate configs and manifests. */ @@ -284,11 +255,6 @@ class Installer { installedModuleNames, }); - await this._installCustomModules(config, paths, addResult, officialModules, { - message, - installedModuleNames, - }); - return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`; }, }); @@ -515,48 +481,7 @@ class Installer { } /** - * Scan the custom module cache directory and register any cached custom modules - * that aren't already known from the manifest or external module list. - * @param {Object} paths - InstallPaths instance - */ - async _scanCachedCustomModules(paths) { - const cacheDir = paths.customCacheDir; - if (!(await fs.pathExists(cacheDir))) { - return; - } - - const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true }); - - for (const cachedModule of cachedModules) { - const moduleId = cachedModule.name; - const cachedPath = path.join(cacheDir, moduleId); - - // Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT - if (!(await fs.pathExists(cachedPath)) || !cachedModule.isDirectory()) { - continue; - } - - // Skip if we already have this module from manifest - if (this.customModules.paths.has(moduleId)) { - continue; - } - - // Check if this is an external official module - skip cache for those - const isExternal = await this.externalModuleManager.hasModule(moduleId); - if (isExternal) { - continue; - } - - // Check if this is actually a custom module (has module.yaml) - const moduleYamlPath = path.join(cachedPath, 'module.yaml'); - if (await fs.pathExists(moduleYamlPath)) { - this.customModules.paths.set(moduleId, cachedPath); - } - } - } - - /** - * Common update preparation: detect files, preserve core config, scan cache, back up. + * Common update preparation: detect files, preserve core config, back up. * @param {Object} paths - InstallPaths instance * @param {Object} config - Clean config (may have coreConfig updated) * @param {Object} existingInstall - Detection result @@ -584,8 +509,6 @@ class Installer { } } - await this._scanCachedCustomModules(paths); - const backupDirs = await this._backupUserFiles(paths, customFiles, modifiedFiles); return { @@ -677,38 +600,6 @@ class Installer { } } - /** - * Install custom modules using CustomModules.install(). - * Source paths come from this.customModules.paths (populated by discoverPaths). - */ - async _installCustomModules(config, paths, addResult, officialModules, ctx) { - const { message, installedModuleNames } = ctx; - const isQuickUpdate = config.isQuickUpdate(); - - for (const [moduleName, sourcePath] of this.customModules.paths) { - if (installedModuleNames.has(moduleName)) continue; - installedModuleNames.add(moduleName); - - message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`); - - const collectedModuleConfig = officialModules.moduleConfigs[moduleName] || {}; - const result = await this.customModules.install(moduleName, paths.bmadDir, (filePath) => this.installedFiles.add(filePath), { - moduleConfig: collectedModuleConfig, - }); - - // Generate runtime config.yaml with merged values - await this.generateModuleConfigs(paths.bmadDir, { - [moduleName]: { ...config.coreConfig, ...result.moduleConfig, ...collectedModuleConfig }, - }); - - // Get display name from source module.yaml; version from marketplace.json - const moduleInfo = await officialModules.getModuleInfo(sourcePath, moduleName, ''); - const displayName = moduleInfo?.name || moduleName; - const version = await this._getMarketplaceVersion(sourcePath); - addResult(displayName, 'ok', '', { moduleCode: moduleName, newVersion: version }); - } - } - /** * Read files-manifest.csv * @param {string} bmadDir - BMAD installation directory @@ -1253,16 +1144,9 @@ class Installer { const configuredIdes = existingInstall.ides; const projectRoot = path.dirname(bmadDir); - const customModuleSources = await this.customModules.assembleQuickUpdateSources( - config, - existingInstall, - bmadDir, - this.externalModuleManager, - ); - // Get available modules (what we have source for) const availableModulesData = await new OfficialModules().listAvailable(); - const availableModules = [...availableModulesData.modules, ...availableModulesData.customModules]; + const availableModules = [...availableModulesData.modules]; // Add external official modules to available modules const externalModules = await this.externalModuleManager.listAvailable(); @@ -1277,52 +1161,12 @@ class Installer { } } - // Add custom modules from manifest if their sources exist - for (const [moduleId, customModule] of customModuleSources) { - const sourcePath = customModule.sourcePath; - if (sourcePath && (await fs.pathExists(sourcePath)) && !availableModules.some((m) => m.id === moduleId)) { - availableModules.push({ - id: moduleId, - name: customModule.name || moduleId, - path: sourcePath, - isCustom: true, - fromManifest: true, - }); - } - } - - // Handle missing custom module sources - const customModuleResult = await this.handleMissingCustomSources( - customModuleSources, - bmadDir, - projectRoot, - 'update', - installedModules, - config.skipPrompts || false, - ); - - const { validCustomModules, keptModulesWithoutSources } = customModuleResult; - - const customModulesFromManifest = validCustomModules.map((m) => ({ - ...m, - isCustom: true, - hasUpdate: true, - })); - - const allAvailableModules = [...availableModules, ...customModulesFromManifest]; - const availableModuleIds = new Set(allAvailableModules.map((m) => m.id)); + const availableModuleIds = new Set(availableModules.map((m) => m.id)); // Only update modules that are BOTH installed AND available (we have source for) const modulesToUpdate = installedModules.filter((id) => availableModuleIds.has(id)); const skippedModules = installedModules.filter((id) => !availableModuleIds.has(id)); - // Add custom modules that were kept without sources to the skipped modules - for (const keptModule of keptModulesWithoutSources) { - if (!skippedModules.includes(keptModule)) { - skippedModules.push(keptModule); - } - } - if (skippedModules.length > 0) { await prompts.log.warn(`Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`); } @@ -1367,9 +1211,7 @@ class Installer { actionType: 'install', _quickUpdate: true, _preserveModules: skippedModules, - _customModuleSources: customModuleSources, _existingModules: installedModules, - customContent: config.customContent, }; await this.install(installConfig); @@ -1504,239 +1346,6 @@ class Installer { return this._readOutputFolder(bmadDir); } - /** - * Handle missing custom module sources interactively - * @param {Map} customModuleSources - Map of custom module ID to info - * @param {string} bmadDir - BMAD directory - * @param {string} projectRoot - Project root directory - * @param {string} operation - Current operation ('update', 'compile', etc.) - * @param {Array} installedModules - Array of installed module IDs (will be modified) - * @param {boolean} [skipPrompts=false] - Skip interactive prompts and keep all modules with missing sources - * @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array - */ - async handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, operation, installedModules, skipPrompts = false) { - const validCustomModules = []; - const keptModulesWithoutSources = []; // Track modules kept without sources - const customModulesWithMissingSources = []; - - // Check which sources exist - for (const [moduleId, customInfo] of customModuleSources) { - if (await fs.pathExists(customInfo.sourcePath)) { - validCustomModules.push({ - id: moduleId, - name: customInfo.name, - path: customInfo.sourcePath, - info: customInfo, - }); - } else { - // For cached modules that are missing, we just skip them without prompting - if (customInfo.cached) { - // Skip cached modules without prompting - keptModulesWithoutSources.push({ - id: moduleId, - name: customInfo.name, - cached: true, - }); - } else { - customModulesWithMissingSources.push({ - id: moduleId, - name: customInfo.name, - sourcePath: customInfo.sourcePath, - relativePath: customInfo.relativePath, - info: customInfo, - }); - } - } - } - - // If no missing sources, return immediately - if (customModulesWithMissingSources.length === 0) { - return { - validCustomModules, - keptModulesWithoutSources: [], - }; - } - - // Non-interactive mode: keep all modules with missing sources - if (skipPrompts) { - for (const missing of customModulesWithMissingSources) { - keptModulesWithoutSources.push(missing.id); - } - return { validCustomModules, keptModulesWithoutSources }; - } - - await prompts.log.warn(`Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`); - - let keptCount = 0; - let updatedCount = 0; - let removedCount = 0; - - for (const missing of customModulesWithMissingSources) { - await prompts.log.message( - `${missing.name} (${missing.id})\n Original source: ${missing.relativePath}\n Full path: ${missing.sourcePath}`, - ); - - const choices = [ - { - name: 'Keep installed (will not be processed)', - value: 'keep', - hint: 'Keep', - }, - { - name: 'Specify new source location', - value: 'update', - hint: 'Update', - }, - ]; - - // Only add remove option if not just compiling agents - if (operation !== 'compile-agents') { - choices.push({ - name: '⚠️ REMOVE module completely (destructive!)', - value: 'remove', - hint: 'Remove', - }); - } - - const action = await prompts.select({ - message: `How would you like to handle "${missing.name}"?`, - choices, - }); - - switch (action) { - case 'update': { - // Use sync validation because @clack/prompts doesn't support async validate - const newSourcePath = await prompts.text({ - message: 'Enter the new path to the custom module:', - default: missing.sourcePath, - validate: (input) => { - if (!input || input.trim() === '') { - return 'Please enter a path'; - } - const expandedPath = path.resolve(input.trim()); - if (!fs.pathExistsSync(expandedPath)) { - return 'Path does not exist'; - } - // Check if it looks like a valid module - const moduleYamlPath = path.join(expandedPath, 'module.yaml'); - const agentsPath = path.join(expandedPath, 'agents'); - const workflowsPath = path.join(expandedPath, 'workflows'); - - if (!fs.pathExistsSync(moduleYamlPath) && !fs.pathExistsSync(agentsPath) && !fs.pathExistsSync(workflowsPath)) { - return 'Path does not appear to contain a valid custom module'; - } - return; // clack expects undefined for valid input - }, - }); - - // Defensive: handleCancel should have exited, but guard against symbol propagation - if (typeof newSourcePath !== 'string') { - keptCount++; - keptModulesWithoutSources.push(missing.id); - continue; - } - - // Update the source in manifest - const resolvedPath = path.resolve(newSourcePath.trim()); - missing.info.sourcePath = resolvedPath; - // Remove relativePath - we only store absolute sourcePath now - delete missing.info.relativePath; - await this.manifest.addCustomModule(bmadDir, missing.info); - - validCustomModules.push({ - id: missing.id, - name: missing.name, - path: resolvedPath, - info: missing.info, - }); - - updatedCount++; - await prompts.log.success('Updated source location'); - - break; - } - case 'remove': { - // Extra confirmation for destructive remove - await prompts.log.error( - `WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!\n Module location: ${path.join(bmadDir, missing.id)}`, - ); - - const confirmDelete = await prompts.confirm({ - message: 'Are you absolutely sure you want to delete this module?', - default: false, - }); - - if (confirmDelete) { - const typedConfirm = await prompts.text({ - message: 'Type "DELETE" to confirm permanent deletion:', - validate: (input) => { - if (input !== 'DELETE') { - return 'You must type "DELETE" exactly to proceed'; - } - return; // clack expects undefined for valid input - }, - }); - - if (typedConfirm === 'DELETE') { - // Remove the module from filesystem and manifest - const modulePath = path.join(bmadDir, missing.id); - if (await fs.pathExists(modulePath)) { - const fsExtra = require('fs-extra'); - await fsExtra.remove(modulePath); - await prompts.log.warn(`Deleted module directory: ${path.relative(projectRoot, modulePath)}`); - } - - await this.manifest.removeModule(bmadDir, missing.id); - await this.manifest.removeCustomModule(bmadDir, missing.id); - await prompts.log.warn('Removed from manifest'); - - // Also remove from installedModules list - if (installedModules && installedModules.includes(missing.id)) { - const index = installedModules.indexOf(missing.id); - if (index !== -1) { - installedModules.splice(index, 1); - } - } - - removedCount++; - await prompts.log.error(`"${missing.name}" has been permanently removed`); - } else { - await prompts.log.message('Removal cancelled - module will be kept'); - keptCount++; - } - } else { - await prompts.log.message('Removal cancelled - module will be kept'); - keptCount++; - } - - break; - } - case 'keep': { - keptCount++; - keptModulesWithoutSources.push(missing.id); - await prompts.log.message('Module will be kept as-is'); - - break; - } - // No default - } - } - - // Show summary - if (keptCount > 0 || updatedCount > 0 || removedCount > 0) { - let summary = 'Summary for custom modules with missing sources:'; - if (keptCount > 0) summary += `\n • ${keptCount} module(s) kept as-is`; - if (updatedCount > 0) summary += `\n • ${updatedCount} module(s) updated with new sources`; - if (removedCount > 0) summary += `\n • ${removedCount} module(s) permanently deleted`; - await prompts.log.message(summary); - } - - return { - validCustomModules, - keptModulesWithoutSources, - }; - } - /** * Find the bmad installation directory in a project * Always uses the standard _bmad folder name diff --git a/tools/installer/core/manifest-generator.js b/tools/installer/core/manifest-generator.js index 74972d36e..28ede065e 100644 --- a/tools/installer/core/manifest-generator.js +++ b/tools/installer/core/manifest-generator.js @@ -375,8 +375,6 @@ class ManifestGenerator { // Read existing manifest to preserve install date let existingInstallDate = null; const existingModulesMap = new Map(); - let existingCustomModules = []; - if (await fs.pathExists(manifestPath)) { try { const existingContent = await fs.readFile(manifestPath, 'utf8'); @@ -397,12 +395,6 @@ class ManifestGenerator { } } } - - if (existingManifest.customModules && Array.isArray(existingManifest.customModules)) { - // We filter here so manifest regeneration preserves source metadata only for custom modules that - // are still installed. Without that, customModules can retain stale entries for modules that were removed. - existingCustomModules = existingManifest.customModules.filter((customModule) => installedModuleSet.has(customModule?.id)); - } } catch { // If we can't read existing manifest, continue with defaults } @@ -438,7 +430,6 @@ class ManifestGenerator { lastUpdated: new Date().toISOString(), }, modules: updatedModules, - customModules: existingCustomModules, ides: this.selectedIdes, }; diff --git a/tools/installer/core/manifest.js b/tools/installer/core/manifest.js index 287b38918..f70482f43 100644 --- a/tools/installer/core/manifest.js +++ b/tools/installer/core/manifest.js @@ -97,7 +97,6 @@ class Manifest { lastUpdated: manifestData.installation?.lastUpdated, modules: moduleNames, // Simple array of module names for backward compatibility modulesDetailed: hasDetailedModules ? modules : null, // New detailed format - customModules: manifestData.customModules || [], // Keep for backward compatibility ides: manifestData.ides || [], }; } catch (error) { @@ -254,7 +253,6 @@ class Manifest { lastUpdated: manifest.installation?.lastUpdated, modules: moduleNames, modulesDetailed: hasDetailedModules ? modules : null, - customModules: manifest.customModules || [], ides: manifest.ides || [], }; } @@ -783,52 +781,6 @@ class Manifest { return configs; } - /** - * Add a custom module to the manifest with its source path - * @param {string} bmadDir - Path to bmad directory - * @param {Object} customModule - Custom module info - */ - async addCustomModule(bmadDir, customModule) { - const manifest = await this.read(bmadDir); - if (!manifest) { - throw new Error('No manifest found'); - } - - if (!manifest.customModules) { - manifest.customModules = []; - } - - // Check if custom module already exists - const existingIndex = manifest.customModules.findIndex((m) => m.id === customModule.id); - if (existingIndex === -1) { - // Add new entry - manifest.customModules.push(customModule); - } else { - // Update existing entry - manifest.customModules[existingIndex] = customModule; - } - - await this.update(bmadDir, { customModules: manifest.customModules }); - } - - /** - * Remove a custom module from the manifest - * @param {string} bmadDir - Path to bmad directory - * @param {string} moduleId - Module ID to remove - */ - async removeCustomModule(bmadDir, moduleId) { - const manifest = await this.read(bmadDir); - if (!manifest || !manifest.customModules) { - return; - } - - const index = manifest.customModules.findIndex((m) => m.id === moduleId); - if (index !== -1) { - manifest.customModules.splice(index, 1); - await this.update(bmadDir, { customModules: manifest.customModules }); - } - } - /** * Get module version info from source * @param {string} moduleName - Module name/code @@ -866,29 +818,8 @@ class Manifest { }; } - // Custom module: resolve path from source or cache before reading version - const customSourcePath = moduleSourcePath || path.join(bmadDir, '_config', 'custom', moduleName); - const version = await this._readMarketplaceVersion(moduleName, customSourcePath); - - const cacheDir = path.join(bmadDir, '_config', 'custom', moduleName); - const moduleYamlPath = path.join(cacheDir, 'module.yaml'); - - if (await fs.pathExists(moduleYamlPath)) { - try { - const yamlContent = await fs.readFile(moduleYamlPath, 'utf8'); - const moduleConfig = yaml.parse(yamlContent); - return { - version: version || moduleConfig.version || null, - source: 'custom', - npmPackage: moduleConfig.npmPackage || null, - repoUrl: moduleConfig.repoUrl || null, - }; - } catch (error) { - await prompts.log.warn(`Failed to read module.yaml for ${moduleName}: ${error.message}`); - } - } - // Unknown module + const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath); return { version, source: 'unknown', diff --git a/tools/installer/custom-handler.js b/tools/installer/custom-handler.js deleted file mode 100644 index a1966b7e7..000000000 --- a/tools/installer/custom-handler.js +++ /dev/null @@ -1,112 +0,0 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const yaml = require('yaml'); -const prompts = require('./prompts'); -/** - * Handler for custom content (custom.yaml) - * Discovers custom agents and workflows in the project - */ -class CustomHandler { - /** - * Find all custom.yaml files in the project - * @param {string} projectRoot - Project root directory - * @returns {Array} List of custom content paths - */ - async findCustomContent(projectRoot) { - const customPaths = []; - - // Helper function to recursively scan directories - async function scanDirectory(dir, excludePaths = []) { - try { - const entries = await fs.readdir(dir, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - - // Skip hidden directories and common exclusions - if ( - entry.name.startsWith('.') || - entry.name === 'node_modules' || - entry.name === 'dist' || - entry.name === 'build' || - entry.name === '.git' || - entry.name === 'bmad' - ) { - continue; - } - - // Skip excluded paths - if (excludePaths.some((exclude) => fullPath.startsWith(exclude))) { - continue; - } - - if (entry.isDirectory()) { - // Recursively scan subdirectories - await scanDirectory(fullPath, excludePaths); - } else if (entry.name === 'custom.yaml') { - // Found a custom.yaml file - customPaths.push(fullPath); - } else if ( - entry.name === 'module.yaml' && // Check if this is a custom module (in root directory) - // Skip if it's in src/modules (those are standard modules) - !fullPath.includes(path.join('src', 'modules')) - ) { - customPaths.push(fullPath); - } - } - } catch { - // Ignore errors (e.g., permission denied) - } - } - - // Scan the entire project, but exclude source directories - await scanDirectory(projectRoot, [path.join(projectRoot, 'src'), path.join(projectRoot, 'tools'), path.join(projectRoot, 'test')]); - - return customPaths; - } - - /** - * Get custom content info from a custom.yaml or module.yaml file - * @param {string} configPath - Path to config file - * @param {string} projectRoot - Project root directory for calculating relative paths - * @returns {Object|null} Custom content info - */ - async getCustomInfo(configPath, projectRoot = null) { - try { - const configContent = await fs.readFile(configPath, 'utf8'); - - // Try to parse YAML with error handling - let config; - try { - config = yaml.parse(configContent); - } catch (parseError) { - await prompts.log.warn('YAML parse error in ' + configPath + ': ' + parseError.message); - return null; - } - - // Check if this is an module.yaml (module) or custom.yaml (custom content) - const isInstallConfig = configPath.endsWith('module.yaml'); - const configDir = path.dirname(configPath); - - // Use provided projectRoot or fall back to process.cwd() - const basePath = projectRoot || process.cwd(); - const relativePath = path.relative(basePath, configDir); - - return { - id: config.code || 'unknown-code', - name: config.name, - description: config.description || '', - path: configDir, - relativePath: relativePath, - defaultSelected: config.default_selected === true, - config: config, - isInstallConfig: isInstallConfig, // Track which type this is - }; - } catch (error) { - await prompts.log.warn('Failed to read ' + configPath + ': ' + error.message); - return null; - } - } -} - -module.exports = { CustomHandler }; diff --git a/tools/installer/modules/custom-modules.js b/tools/installer/modules/custom-modules.js deleted file mode 100644 index 3f8b793be..000000000 --- a/tools/installer/modules/custom-modules.js +++ /dev/null @@ -1,302 +0,0 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const yaml = require('yaml'); -const { CustomHandler } = require('../custom-handler'); -const { Manifest } = require('../core/manifest'); -const prompts = require('../prompts'); - -class CustomModules { - constructor() { - this.paths = new Map(); - } - - has(moduleCode) { - return this.paths.has(moduleCode); - } - - get(moduleCode) { - return this.paths.get(moduleCode); - } - - set(moduleId, sourcePath) { - this.paths.set(moduleId, sourcePath); - } - - /** - * Install a custom module from its source path. - * @param {string} moduleName - Module identifier - * @param {string} bmadDir - Target bmad directory - * @param {Function} fileTrackingCallback - Optional callback to track installed files - * @param {Object} options - Install options - * @param {Object} options.moduleConfig - Pre-collected module configuration - * @returns {Object} Install result - */ - async install(moduleName, bmadDir, fileTrackingCallback = null, options = {}) { - const sourcePath = this.paths.get(moduleName); - if (!sourcePath) { - throw new Error(`No source path for custom module '${moduleName}'`); - } - - if (!(await fs.pathExists(sourcePath))) { - throw new Error(`Source for custom module '${moduleName}' not found at: ${sourcePath}`); - } - - const targetPath = path.join(bmadDir, moduleName); - - // Read custom.yaml and merge into module config - let moduleConfig = options.moduleConfig ? { ...options.moduleConfig } : {}; - const customConfigPath = path.join(sourcePath, 'custom.yaml'); - if (await fs.pathExists(customConfigPath)) { - try { - const content = await fs.readFile(customConfigPath, 'utf8'); - const customConfig = yaml.parse(content); - if (customConfig) { - moduleConfig = { ...moduleConfig, ...customConfig }; - } - } catch (error) { - await prompts.log.warn(`Failed to read custom.yaml for ${moduleName}: ${error.message}`); - } - } - - // Remove existing installation - if (await fs.pathExists(targetPath)) { - await fs.remove(targetPath); - } - - // Copy files with filtering - await this._copyWithFiltering(sourcePath, targetPath, fileTrackingCallback); - - // Add to manifest - const manifest = new Manifest(); - const versionInfo = await manifest.getModuleVersionInfo(moduleName, bmadDir, sourcePath); - await manifest.addModule(bmadDir, moduleName, { - version: versionInfo.version, - source: versionInfo.source, - npmPackage: versionInfo.npmPackage, - repoUrl: versionInfo.repoUrl, - }); - - return { success: true, module: moduleName, path: targetPath, moduleConfig }; - } - - /** - * Copy module files, filtering out install-time-only artifacts. - * @param {string} sourcePath - Source module directory - * @param {string} targetPath - Target module directory - * @param {Function} fileTrackingCallback - Optional callback to track installed files - */ - async _copyWithFiltering(sourcePath, targetPath, fileTrackingCallback = null) { - const files = await this._getFileList(sourcePath); - - for (const file of files) { - if (file.startsWith('sub-modules/')) continue; - - const isInSidecar = path - .dirname(file) - .split('/') - .some((dir) => dir.toLowerCase().endsWith('-sidecar')); - if (isInSidecar) continue; - - if (file === 'module.yaml') continue; - if (file === 'config.yaml') continue; - - const sourceFile = path.join(sourcePath, file); - const targetFile = path.join(targetPath, file); - - // Skip web-only agents - if (file.startsWith('agents/') && file.endsWith('.md')) { - const content = await fs.readFile(sourceFile, 'utf8'); - if (/]*\slocalskip="true"[^>]*>/.test(content)) { - continue; - } - } - - await fs.ensureDir(path.dirname(targetFile)); - await fs.copy(sourceFile, targetFile, { overwrite: true }); - - if (fileTrackingCallback) { - fileTrackingCallback(targetFile); - } - } - } - - /** - * Recursively list all files in a directory. - * @param {string} dir - Directory to scan - * @param {string} baseDir - Base directory for relative paths - * @returns {string[]} Relative file paths - */ - async _getFileList(dir, baseDir = dir) { - const files = []; - const entries = await fs.readdir(dir, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...(await this._getFileList(fullPath, baseDir))); - } else { - files.push(path.relative(baseDir, fullPath)); - } - } - - return files; - } - - /** - * Discover custom module source paths from all available sources. - * @param {Object} config - Installation configuration - * @param {Object} paths - InstallPaths instance - * @returns {Map} Map of module ID to source path - */ - async discoverPaths(config, paths) { - this.paths = new Map(); - - if (config._quickUpdate) { - if (config._customModuleSources) { - for (const [moduleId, customInfo] of config._customModuleSources) { - this.paths.set(moduleId, customInfo.sourcePath); - } - } - return this.paths; - } - - // From UI: selectedFiles - if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) { - const customHandler = new CustomHandler(); - for (const customFile of config.customContent.selectedFiles) { - const customInfo = await customHandler.getCustomInfo(customFile, paths.projectRoot); - if (customInfo && customInfo.id) { - this.paths.set(customInfo.id, customInfo.path); - } - } - } - - // From UI: sources - if (config.customContent && config.customContent.sources) { - for (const source of config.customContent.sources) { - this.paths.set(source.id, source.path); - } - } - - // From UI: cachedModules - if (config.customContent && config.customContent.cachedModules) { - const selectedCachedIds = config.customContent.selectedCachedModules || []; - const shouldIncludeAll = selectedCachedIds.length === 0 && config.customContent.selected; - - for (const cachedModule of config.customContent.cachedModules) { - if (cachedModule.id && cachedModule.cachePath && (shouldIncludeAll || selectedCachedIds.includes(cachedModule.id))) { - this.paths.set(cachedModule.id, cachedModule.cachePath); - } - } - } - - return this.paths; - } - - /** - * Assemble quick-update source candidates before install() hands them to discoverPaths(). - * This exists because discoverPaths() consumes already-prepared quick-update sources, - * while quickUpdate() still has to build that source map from manifest, explicit inputs, - * and cache conventions. - * Precedence: manifest-backed paths, explicit sources override them, then cached modules. - * @param {Object} config - Quick update configuration - * @param {Object} existingInstall - Existing installation snapshot - * @param {string} bmadDir - BMAD directory - * @param {Object} externalModuleManager - External module manager - * @returns {Promise>} Map of custom module ID to source info - */ - async assembleQuickUpdateSources(config, existingInstall, bmadDir, externalModuleManager) { - const projectRoot = path.dirname(bmadDir); - const customModuleSources = new Map(); - - if (existingInstall.customModules) { - for (const customModule of existingInstall.customModules) { - // Skip if no ID - can't reliably track or re-cache without it - if (!customModule?.id) continue; - - let sourcePath = customModule.sourcePath; - if (sourcePath && sourcePath.startsWith('_config')) { - // Paths are relative to BMAD dir, but we want absolute paths for install - sourcePath = path.join(bmadDir, sourcePath); - } else if (!sourcePath && customModule.relativePath) { - // Fall back to relativePath - sourcePath = path.resolve(projectRoot, customModule.relativePath); - } else if (sourcePath && !path.isAbsolute(sourcePath)) { - // If we have a sourcePath but it's not absolute, resolve it relative to project root - sourcePath = path.resolve(projectRoot, sourcePath); - } - - // If we still don't have a valid source path, skip this module - if (!sourcePath || !(await fs.pathExists(sourcePath))) { - continue; - } - - customModuleSources.set(customModule.id, { - id: customModule.id, - name: customModule.name || customModule.id, - sourcePath, - relativePath: customModule.relativePath, - cached: false, - }); - } - } - - if (config.customContent?.sources?.length > 0) { - for (const source of config.customContent.sources) { - if (source.id && source.path) { - customModuleSources.set(source.id, { - id: source.id, - name: source.name || source.id, - sourcePath: source.path, - cached: false, // From CLI, will be re-cached - }); - } - } - } - - const cacheDir = path.join(bmadDir, '_config', 'custom'); - if (!(await fs.pathExists(cacheDir))) { - return customModuleSources; - } - - const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true }); - for (const cachedModule of cachedModules) { - const moduleId = cachedModule.name; - const cachedPath = path.join(cacheDir, moduleId); - - // Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT - if (!(await fs.pathExists(cachedPath))) { - continue; - } - if (!cachedModule.isDirectory()) { - continue; - } - - // Skip if we already have this module from manifest - if (customModuleSources.has(moduleId)) { - continue; - } - - // Check if this is an external official module - skip cache for those - const isExternal = await externalModuleManager.hasModule(moduleId); - if (isExternal) { - continue; - } - - // Check if this is actually a custom module (has module.yaml) - const moduleYamlPath = path.join(cachedPath, 'module.yaml'); - if (await fs.pathExists(moduleYamlPath)) { - customModuleSources.set(moduleId, { - id: moduleId, - name: moduleId, - sourcePath: cachedPath, - cached: true, - }); - } - } - - return customModuleSources; - } -} - -module.exports = { CustomModules }; diff --git a/tools/installer/modules/official-modules.js b/tools/installer/modules/official-modules.js index 5b67fc4dd..b093291db 100644 --- a/tools/installer/modules/official-modules.js +++ b/tools/installer/modules/official-modules.js @@ -102,7 +102,6 @@ class OfficialModules { */ async listAvailable() { const modules = []; - const customModules = []; // Add built-in core module (directly under src/core-skills) const corePath = getSourcePath('core-skills'); @@ -122,7 +121,7 @@ class OfficialModules { } } - return { modules, customModules }; + return { modules }; } /** @@ -133,25 +132,12 @@ class OfficialModules { * @returns {Object|null} Module info or null if not a valid module */ async getModuleInfo(modulePath, defaultName, sourceDescription) { - // Check for module structure (module.yaml OR custom.yaml) const moduleConfigPath = path.join(modulePath, 'module.yaml'); - const rootCustomConfigPath = path.join(modulePath, 'custom.yaml'); - let configPath = null; - if (await fs.pathExists(moduleConfigPath)) { - configPath = moduleConfigPath; - } else if (await fs.pathExists(rootCustomConfigPath)) { - configPath = rootCustomConfigPath; - } - - // Skip if this doesn't look like a module - if (!configPath) { + if (!(await fs.pathExists(moduleConfigPath))) { return null; } - // Mark as custom if it's using custom.yaml OR if it's outside src/bmm or src/core - const isCustomSource = - sourceDescription !== 'src/bmm-skills' && sourceDescription !== 'src/core-skills' && sourceDescription !== 'src/modules'; const moduleInfo = { id: defaultName, path: modulePath, @@ -162,12 +148,11 @@ class OfficialModules { description: 'BMAD Module', version: '5.0.0', source: sourceDescription, - isCustom: configPath === rootCustomConfigPath || isCustomSource, }; // Read module config for metadata try { - const configContent = await fs.readFile(configPath, 'utf8'); + const configContent = await fs.readFile(moduleConfigPath, 'utf8'); const config = yaml.parse(configContent); // Use the code property as the id if available @@ -824,20 +809,15 @@ class OfficialModules { const results = []; for (const moduleName of modules) { - // Resolve module.yaml path - custom paths first, then standard location, then OfficialModules search + // Resolve module.yaml path - standard location first, then OfficialModules search let moduleConfigPath = null; - const customPath = this.customModulePaths?.get(moduleName); - if (customPath) { - moduleConfigPath = path.join(customPath, 'module.yaml'); + const standardPath = path.join(getModulePath(moduleName), 'module.yaml'); + if (await fs.pathExists(standardPath)) { + moduleConfigPath = standardPath; } else { - const standardPath = path.join(getModulePath(moduleName), 'module.yaml'); - if (await fs.pathExists(standardPath)) { - moduleConfigPath = standardPath; - } else { - const moduleSourcePath = await this.findModuleSource(moduleName, { silent: true }); - if (moduleSourcePath) { - moduleConfigPath = path.join(moduleSourcePath, 'module.yaml'); - } + const moduleSourcePath = await this.findModuleSource(moduleName, { silent: true }); + if (moduleSourcePath) { + moduleConfigPath = path.join(moduleSourcePath, 'module.yaml'); } } @@ -882,12 +862,9 @@ class OfficialModules { * @param {Array} modules - List of modules to configure (including 'core') * @param {string} projectDir - Target project directory * @param {Object} options - Additional options - * @param {Map} options.customModulePaths - Map of module ID to source path for custom modules * @param {boolean} options.skipPrompts - Skip prompts and use defaults (for --yes flag) */ async collectAllConfigurations(modules, projectDir, options = {}) { - // Store custom module paths for use in collectModuleConfig - this.customModulePaths = options.customModulePaths || new Map(); this.skipPrompts = options.skipPrompts || false; this.modulesToCustomize = undefined; await this.loadExistingConfig(projectDir); @@ -1042,25 +1019,7 @@ class OfficialModules { } } - let configPath = null; - let isCustomModule = false; - - if (await fs.pathExists(moduleConfigPath)) { - configPath = moduleConfigPath; - } else { - // Check if this is a custom module with custom.yaml - const moduleSourcePath = await this.findModuleSource(moduleName, { silent: true }); - - if (moduleSourcePath) { - const rootCustomConfigPath = path.join(moduleSourcePath, 'custom.yaml'); - - if (await fs.pathExists(rootCustomConfigPath)) { - isCustomModule = true; - // For custom modules, we don't have an install-config schema, so just use existing values - // The custom.yaml values will be loaded and merged during installation - } - } - + if (!(await fs.pathExists(moduleConfigPath))) { // No config schema for this module - use existing values if (this._existingConfig && this._existingConfig[moduleName]) { if (!this.collectedConfig[moduleName]) { @@ -1071,7 +1030,7 @@ class OfficialModules { return false; } - const configContent = await fs.readFile(configPath, 'utf8'); + const configContent = await fs.readFile(moduleConfigPath, 'utf8'); const moduleConfig = yaml.parse(configContent); if (!moduleConfig) { @@ -1332,16 +1291,7 @@ class OfficialModules { this.allAnswers = {}; } // Load module's config - // First, check if we have a custom module path for this module - let moduleConfigPath = null; - - if (this.customModulePaths && this.customModulePaths.has(moduleName)) { - const customPath = this.customModulePaths.get(moduleName); - moduleConfigPath = path.join(customPath, 'module.yaml'); - } else { - // Try the standard src/modules location - moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml'); - } + let moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml'); // If not found in src/modules or custom paths, search the project if (!(await fs.pathExists(moduleConfigPath))) { diff --git a/tools/installer/ui.js b/tools/installer/ui.js index cccf219cc..9b8812f8a 100644 --- a/tools/installer/ui.js +++ b/tools/installer/ui.js @@ -2,7 +2,6 @@ const path = require('node:path'); const os = require('node:os'); const fs = require('fs-extra'); const { CLIUtils } = require('./cli-utils'); -const { CustomHandler } = require('./custom-handler'); const { ExternalModuleManager } = require('./modules/external-manager'); const { getProjectRoot } = require('./project-root'); const prompts = require('./prompts'); @@ -48,19 +47,6 @@ function _extractMarketplaceVersion(data) { return best; } -// Separator class for visual grouping in select/multiselect prompts -// Note: @clack/prompts doesn't support separators natively, they are filtered out -class Separator { - constructor(text = '────────') { - this.line = text; - this.name = text; - } - type = 'separator'; -} - -// Separator for choice lists (compatible interface) -const choiceUtils = { Separator }; - /** * UI utilities for the installer */ @@ -100,11 +86,6 @@ class UI { // Check if there's an existing BMAD installation const hasExistingInstall = await fs.pathExists(bmadDir); - let customContentConfig = { hasCustomContent: false }; - if (!hasExistingInstall) { - customContentConfig._shouldAsk = true; - } - // Track action type (only set if there's an existing installation) let actionType; @@ -153,48 +134,9 @@ class UI { // Handle quick update separately if (actionType === 'quick-update') { - // Pass --custom-content through so installer can re-cache if cache is missing - let customContentForQuickUpdate = { hasCustomContent: false }; - if (options.customContent) { - const paths = options.customContent - .split(',') - .map((p) => p.trim()) - .filter(Boolean); - if (paths.length > 0) { - const customPaths = []; - const selectedModuleIds = []; - const sources = []; - for (const customPath of paths) { - const expandedPath = this.expandUserPath(customPath); - const validation = this.validateCustomContentPathSync(expandedPath); - if (validation) continue; - let moduleMeta; - try { - const moduleYamlPath = path.join(expandedPath, 'module.yaml'); - moduleMeta = require('yaml').parse(await fs.readFile(moduleYamlPath, 'utf-8')); - } catch { - continue; - } - if (!moduleMeta?.code) continue; - customPaths.push(expandedPath); - selectedModuleIds.push(moduleMeta.code); - sources.push({ path: expandedPath, id: moduleMeta.code, name: moduleMeta.name || moduleMeta.code }); - } - if (customPaths.length > 0) { - customContentForQuickUpdate = { - hasCustomContent: true, - selected: true, - sources, - selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')), - selectedModuleIds, - }; - } - } - } return { actionType: 'quick-update', directory: confirmedDirectory, - customContent: customContentForQuickUpdate, skipPrompts: options.yes || false, }; } @@ -225,120 +167,6 @@ class UI { selectedModules = await this.selectAllModules(installedModuleIds); } - // After module selection, ask about custom modules - let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } }; - - if (options.customContent) { - // Use custom content from command-line - const paths = options.customContent - .split(',') - .map((p) => p.trim()) - .filter(Boolean); - await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`); - - // Build custom content config similar to promptCustomContentSource - const customPaths = []; - const selectedModuleIds = []; - const sources = []; - - for (const customPath of paths) { - const expandedPath = this.expandUserPath(customPath); - const validation = this.validateCustomContentPathSync(expandedPath); - if (validation) { - await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`); - continue; - } - - // Read module metadata - let moduleMeta; - try { - const moduleYamlPath = path.join(expandedPath, 'module.yaml'); - const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8'); - const yaml = require('yaml'); - moduleMeta = yaml.parse(moduleYaml); - } catch (error) { - await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`); - continue; - } - - if (!moduleMeta) { - await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`); - continue; - } - - if (!moduleMeta.code) { - await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`); - continue; - } - - customPaths.push(expandedPath); - selectedModuleIds.push(moduleMeta.code); - sources.push({ - path: expandedPath, - id: moduleMeta.code, - name: moduleMeta.name || moduleMeta.code, - }); - } - - if (customPaths.length > 0) { - customModuleResult = { - selectedCustomModules: selectedModuleIds, - customContentConfig: { - hasCustomContent: true, - selected: true, - sources, - selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')), - selectedModuleIds: selectedModuleIds, - }, - }; - } - } else if (options.yes) { - // Non-interactive mode: preserve existing custom modules (matches default: false) - const cacheDir = path.join(bmadDir, '_config', 'custom'); - if (await fs.pathExists(cacheDir)) { - const entries = await fs.readdir(cacheDir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory()) { - customModuleResult.selectedCustomModules.push(entry.name); - } - } - await prompts.log.info( - `Non-interactive mode (--yes): preserving ${customModuleResult.selectedCustomModules.length} existing custom module(s)`, - ); - } else { - await prompts.log.info('Non-interactive mode (--yes): no existing custom modules found'); - } - } else { - const changeCustomModules = await prompts.confirm({ - message: 'Modify custom modules, agents, or workflows?', - default: false, - }); - - if (changeCustomModules) { - customModuleResult = await this.handleCustomModulesInModifyFlow(confirmedDirectory, selectedModules); - } else { - // Preserve existing custom modules if user doesn't want to modify them - const { Installer } = require('./core/installer'); - const installer = new Installer(); - const { bmadDir } = await installer.findBmadDir(confirmedDirectory); - - const cacheDir = path.join(bmadDir, '_config', 'custom'); - if (await fs.pathExists(cacheDir)) { - const entries = await fs.readdir(cacheDir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory()) { - customModuleResult.selectedCustomModules.push(entry.name); - } - } - } - } - } - - // Merge any selected custom modules - if (customModuleResult.selectedCustomModules.length > 0) { - selectedModules.push(...customModuleResult.selectedCustomModules); - } - // Ensure core is in the modules list if (!selectedModules.includes('core')) { selectedModules.unshift('core'); @@ -357,7 +185,6 @@ class UI { skipIde: toolSelection.skipIde, coreConfig: moduleConfigs.core || {}, moduleConfigs: moduleConfigs, - customContent: customModuleResult.customContentConfig, skipPrompts: options.yes || false, }; } @@ -383,84 +210,6 @@ class UI { selectedModules = await this.selectAllModules(installedModuleIds); } - // Ask about custom content (local modules/agents/workflows) - if (options.customContent) { - // Use custom content from command-line - const paths = options.customContent - .split(',') - .map((p) => p.trim()) - .filter(Boolean); - await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`); - - // Build custom content config similar to promptCustomContentSource - const customPaths = []; - const selectedModuleIds = []; - const sources = []; - - for (const customPath of paths) { - const expandedPath = this.expandUserPath(customPath); - const validation = this.validateCustomContentPathSync(expandedPath); - if (validation) { - await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`); - continue; - } - - // Read module metadata - let moduleMeta; - try { - const moduleYamlPath = path.join(expandedPath, 'module.yaml'); - const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8'); - const yaml = require('yaml'); - moduleMeta = yaml.parse(moduleYaml); - } catch (error) { - await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`); - continue; - } - - if (!moduleMeta) { - await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`); - continue; - } - - if (!moduleMeta.code) { - await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`); - continue; - } - - customPaths.push(expandedPath); - selectedModuleIds.push(moduleMeta.code); - sources.push({ - path: expandedPath, - id: moduleMeta.code, - name: moduleMeta.name || moduleMeta.code, - }); - } - - if (customPaths.length > 0) { - customContentConfig = { - hasCustomContent: true, - selected: true, - sources, - selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')), - selectedModuleIds: selectedModuleIds, - }; - } - } else if (!options.yes) { - const wantsCustomContent = await prompts.confirm({ - message: 'Add custom modules, agents, or workflows from your computer?', - default: false, - }); - - if (wantsCustomContent) { - customContentConfig = await this.promptCustomContentSource(); - } - } - - // Add custom content modules if any were selected - if (customContentConfig && customContentConfig.selectedModuleIds) { - selectedModules.push(...customContentConfig.selectedModuleIds); - } - // Ensure core is in the modules list if (!selectedModules.includes('core')) { selectedModules.unshift('core'); @@ -476,7 +225,6 @@ class UI { skipIde: toolSelection.skipIde, coreConfig: moduleConfigs.core || {}, moduleConfigs: moduleConfigs, - customContent: customContentConfig, skipPrompts: options.yes || false, }; } @@ -814,90 +562,6 @@ class UI { return configCollector.collectedConfig; } - /** - * Get module choices for selection - * @param {Set} installedModuleIds - Currently installed module IDs - * @param {Object} customContentConfig - Custom content configuration - * @returns {Array} Module choices for prompt - */ - async getModuleChoices(installedModuleIds, customContentConfig = null) { - const color = await prompts.getColor(); - const moduleChoices = []; - const isNewInstallation = installedModuleIds.size === 0; - - const customContentItems = []; - - // Add custom content items - if (customContentConfig && customContentConfig.hasCustomContent && customContentConfig.customPath) { - // Existing installation - show from directory - const customHandler = new CustomHandler(); - const customFiles = await customHandler.findCustomContent(customContentConfig.customPath); - - for (const customFile of customFiles) { - const customInfo = await customHandler.getCustomInfo(customFile); - if (customInfo) { - customContentItems.push({ - name: `${color.cyan('\u2713')} ${customInfo.name} ${color.dim(`(${customInfo.relativePath})`)}`, - value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content - checked: true, // Default to selected since user chose to provide custom content - path: customInfo.path, // Track path to avoid duplicates - hint: customInfo.description || undefined, - }); - } - } - } - - // Add official modules - const { OfficialModules } = require('./modules/official-modules'); - const officialModules = new OfficialModules(); - const { modules: availableModules, customModules: customModulesFromCache } = await officialModules.listAvailable(); - - // First, add all items to appropriate sections - const allCustomModules = []; - - // Add custom content items from directory - allCustomModules.push(...customContentItems); - - // Add custom modules from cache - for (const mod of customModulesFromCache) { - // Skip if this module is already in customContentItems (by path) - const isDuplicate = allCustomModules.some((item) => item.path && mod.path && path.resolve(item.path) === path.resolve(mod.path)); - - if (!isDuplicate) { - allCustomModules.push({ - name: `${color.cyan('\u2713')} ${mod.name} ${color.dim('(cached)')}`, - value: mod.id, - checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id), - hint: mod.description || undefined, - }); - } - } - - // Add separators and modules in correct order - if (allCustomModules.length > 0) { - // Add separator for custom content, all custom modules, and official content separator - moduleChoices.push( - new choiceUtils.Separator('── Custom Content ──'), - ...allCustomModules, - new choiceUtils.Separator('── Official Content ──'), - ); - } - - // Add official modules (only non-custom ones) - for (const mod of availableModules) { - if (!mod.isCustom) { - moduleChoices.push({ - name: mod.name, - value: mod.id, - checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id), - hint: mod.description || undefined, - }); - } - } - - return moduleChoices; - } - /** * Select all modules (official + community) using grouped multiselect. * Core is shown as locked but filtered from the result since it's always installed separately. @@ -941,7 +605,7 @@ class UI { // Local modules (BMM, BMB, etc.) const localEntries = []; for (const mod of localModules) { - if (!mod.isCustom && mod.id !== 'core') { + if (mod.id !== 'core') { const entry = await buildModuleEntry(mod, mod.id, 'Local'); localEntries.push(entry); if (entry.selected) { @@ -1316,282 +980,6 @@ class UI { return existingInstall.ides; } - /** - * Validate custom content path synchronously - * @param {string} input - User input path - * @returns {string|undefined} Error message or undefined if valid - */ - validateCustomContentPathSync(input) { - // Allow empty input to cancel - if (!input || input.trim() === '') { - return; // Allow empty to exit - } - - try { - // Expand the path - const expandedPath = this.expandUserPath(input.trim()); - - // Check if path exists - if (!fs.pathExistsSync(expandedPath)) { - return 'Path does not exist'; - } - - // Check if it's a directory - const stat = fs.statSync(expandedPath); - if (!stat.isDirectory()) { - return 'Path must be a directory'; - } - - // Check for module.yaml in the root - const moduleYamlPath = path.join(expandedPath, 'module.yaml'); - if (!fs.pathExistsSync(moduleYamlPath)) { - return 'Directory must contain a module.yaml file in the root'; - } - - // Try to parse the module.yaml to get the module ID - try { - const yaml = require('yaml'); - const content = fs.readFileSync(moduleYamlPath, 'utf8'); - const moduleData = yaml.parse(content); - if (!moduleData.code) { - return 'module.yaml must contain a "code" field for the module ID'; - } - } catch (error) { - return 'Invalid module.yaml file: ' + error.message; - } - - return; // Valid - } catch (error) { - return 'Error validating path: ' + error.message; - } - } - - /** - * Prompt user for custom content source location - * @returns {Object} Custom content configuration - */ - async promptCustomContentSource() { - const customContentConfig = { hasCustomContent: true, sources: [] }; - - // Keep asking for more sources until user is done - while (true) { - // First ask if user wants to add another module or continue - if (customContentConfig.sources.length > 0) { - const action = await prompts.select({ - message: 'Would you like to:', - choices: [ - { name: 'Add another custom module', value: 'add' }, - { name: 'Continue with installation', value: 'continue' }, - ], - default: 'continue', - }); - - if (action === 'continue') { - break; - } - } - - let sourcePath; - let isValid = false; - - while (!isValid) { - // Use sync validation because @clack/prompts doesn't support async validate - const inputPath = await prompts.text({ - message: 'Path to custom module folder (press Enter to skip):', - validate: (input) => this.validateCustomContentPathSync(input), - }); - - // If user pressed Enter without typing anything, exit the loop - if (!inputPath || inputPath.trim() === '') { - // If we have no modules yet, return false for no custom content - if (customContentConfig.sources.length === 0) { - return { hasCustomContent: false }; - } - return customContentConfig; - } - - sourcePath = this.expandUserPath(inputPath); - isValid = true; - } - - // Read module.yaml to get module info - const yaml = require('yaml'); - const moduleYamlPath = path.join(sourcePath, 'module.yaml'); - const moduleContent = await fs.readFile(moduleYamlPath, 'utf8'); - const moduleData = yaml.parse(moduleContent); - - // Add to sources - customContentConfig.sources.push({ - path: sourcePath, - id: moduleData.code, - name: moduleData.name || moduleData.code, - }); - - await prompts.log.success(`Confirmed local custom module: ${moduleData.name || moduleData.code}`); - } - - // Ask if user wants to add these to the installation - const shouldInstall = await prompts.confirm({ - message: `Install these ${customContentConfig.sources.length} custom modules?`, - default: true, - }); - - if (shouldInstall) { - customContentConfig.selected = true; - // Store paths to module.yaml files, not directories - customContentConfig.selectedFiles = customContentConfig.sources.map((s) => path.join(s.path, 'module.yaml')); - // Also include module IDs for installation - customContentConfig.selectedModuleIds = customContentConfig.sources.map((s) => s.id); - } - - return customContentConfig; - } - - /** - * Handle custom modules in the modify flow - * @param {string} directory - Installation directory - * @param {Array} selectedModules - Currently selected modules - * @returns {Object} Result with selected custom modules and custom content config - */ - async handleCustomModulesInModifyFlow(directory, selectedModules) { - // Get existing installation to find custom modules - const { existingInstall } = await this.getExistingInstallation(directory); - - // Check if there are any custom modules in cache - const { Installer } = require('./core/installer'); - const installer = new Installer(); - const { bmadDir } = await installer.findBmadDir(directory); - - const cacheDir = path.join(bmadDir, '_config', 'custom'); - const cachedCustomModules = []; - - if (await fs.pathExists(cacheDir)) { - const entries = await fs.readdir(cacheDir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory()) { - const moduleYamlPath = path.join(cacheDir, entry.name, 'module.yaml'); - if (await fs.pathExists(moduleYamlPath)) { - const yaml = require('yaml'); - const content = await fs.readFile(moduleYamlPath, 'utf8'); - const moduleData = yaml.parse(content); - - cachedCustomModules.push({ - id: entry.name, - name: moduleData.name || entry.name, - description: moduleData.description || 'Custom module from cache', - checked: selectedModules.includes(entry.name), - fromCache: true, - }); - } - } - } - } - - const result = { - selectedCustomModules: [], - customContentConfig: { hasCustomContent: false }, - }; - - // Ask user about custom modules - await prompts.log.info('Custom Modules'); - if (cachedCustomModules.length > 0) { - await prompts.log.message('Found custom modules in your installation:'); - } else { - await prompts.log.message('No custom modules currently installed.'); - } - - // Build choices dynamically based on whether we have existing modules - const choices = []; - if (cachedCustomModules.length > 0) { - choices.push( - { name: 'Keep all existing custom modules', value: 'keep' }, - { name: 'Select which custom modules to keep', value: 'select' }, - { name: 'Add new custom modules', value: 'add' }, - { name: 'Remove all custom modules', value: 'remove' }, - ); - } else { - choices.push({ name: 'Add new custom modules', value: 'add' }, { name: 'Cancel (no custom modules)', value: 'cancel' }); - } - - const customAction = await prompts.select({ - message: cachedCustomModules.length > 0 ? 'Manage custom modules?' : 'Add custom modules?', - choices: choices, - default: cachedCustomModules.length > 0 ? 'keep' : 'add', - }); - - switch (customAction) { - case 'keep': { - // Keep all existing custom modules - result.selectedCustomModules = cachedCustomModules.map((m) => m.id); - await prompts.log.message(`Keeping ${result.selectedCustomModules.length} custom module(s)`); - break; - } - - case 'select': { - // Let user choose which to keep - const selectChoices = cachedCustomModules.map((m) => ({ - name: `${m.name} (${m.id})`, - value: m.id, - checked: m.checked, - })); - - // Add "None / I changed my mind" option at the end - const choicesWithSkip = [ - ...selectChoices, - { - name: '⚠ None / I changed my mind - keep no custom modules', - value: '__NONE__', - checked: false, - }, - ]; - - const keepModules = await prompts.multiselect({ - message: 'Select custom modules to keep (use arrow keys, space to toggle):', - choices: choicesWithSkip, - required: true, - }); - - // If user selected both "__NONE__" and other modules, honor the "None" choice - if (keepModules && keepModules.includes('__NONE__') && keepModules.length > 1) { - await prompts.log.warn('"None / I changed my mind" was selected, so no custom modules will be kept.'); - result.selectedCustomModules = []; - } else { - // Filter out the special '__NONE__' value - result.selectedCustomModules = keepModules ? keepModules.filter((m) => m !== '__NONE__') : []; - } - break; - } - - case 'add': { - // By default, keep existing modules when adding new ones - // User chose "Add new" not "Replace", so we assume they want to keep existing - result.selectedCustomModules = cachedCustomModules.map((m) => m.id); - - // Then prompt for new ones (reuse existing method) - const newCustomContent = await this.promptCustomContentSource(); - if (newCustomContent.hasCustomContent && newCustomContent.selected) { - result.selectedCustomModules.push(...newCustomContent.selectedModuleIds); - result.customContentConfig = newCustomContent; - } - break; - } - - case 'remove': { - // Remove all custom modules - await prompts.log.warn('All custom modules will be removed from the installation'); - break; - } - - case 'cancel': { - // User cancelled - no custom modules - await prompts.log.message('No custom modules will be added'); - break; - } - } - - return result; - } - /** * Display module versions with update availability * @param {Array} modules - Array of module info objects with version info