From 8fb22b1aa8c1303ca30dcc4aa5e2efd7668e533c Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Sat, 18 Apr 2026 13:44:50 -0500 Subject: [PATCH] feat(installer): provision _bmad/scripts/ and _bmad/custom/ on install - Declare scriptsDir and customDir in InstallPaths and ensure both exist. - New _installSharedScripts task copies src/scripts/* -> _bmad/scripts/ and seeds _bmad/custom/.gitignore with *.user.yaml on fresh installs. - Exclude scripts/ and custom/ from module-directory scans in generateModuleConfigs, mergeModuleHelpCatalogs, and loadExistingConfig so neither is treated as a module. --- tools/installer/core/install-paths.js | 6 +++ tools/installer/core/installer.js | 44 ++++++++++++++++++--- tools/installer/modules/official-modules.js | 4 +- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/tools/installer/core/install-paths.js b/tools/installer/core/install-paths.js index e7fb98b6d..892e2636b 100644 --- a/tools/installer/core/install-paths.js +++ b/tools/installer/core/install-paths.js @@ -21,12 +21,16 @@ class InstallPaths { const configDir = path.join(bmadDir, '_config'); const agentsDir = path.join(configDir, 'agents'); const coreDir = path.join(bmadDir, 'core'); + const scriptsDir = path.join(bmadDir, 'scripts'); + const customDir = path.join(bmadDir, 'custom'); for (const [dir, label] of [ [bmadDir, 'bmad directory'], [configDir, 'config directory'], [agentsDir, 'agents config directory'], [coreDir, 'core module directory'], + [scriptsDir, 'shared scripts directory'], + [customDir, 'customizations directory'], ]) { await ensureWritableDir(dir, label); } @@ -39,6 +43,8 @@ class InstallPaths { configDir, agentsDir, coreDir, + scriptsDir, + customDir, isUpdate, }); } diff --git a/tools/installer/core/installer.js b/tools/installer/core/installer.js index 2a9ff3272..f8b2d98d9 100644 --- a/tools/installer/core/installer.js +++ b/tools/installer/core/installer.js @@ -244,6 +244,15 @@ class Installer { const installTasks = []; + installTasks.push({ + title: 'Installing shared scripts', + task: async () => { + await this._installSharedScripts(paths); + addResult('Shared scripts', 'ok'); + return 'Shared scripts installed'; + }, + }); + if (allModules.length > 0) { installTasks.push({ title: isQuickUpdate ? `Updating ${allModules.length} module(s)` : `Installing ${allModules.length} module(s)`, @@ -558,6 +567,31 @@ class Installer { return { tempBackupDir, tempModifiedBackupDir }; } + /** + * Copy src/scripts/* → _bmad/scripts/ and seed _bmad/custom/.gitignore. + * Agents resolve customizations via these shared scripts; the .gitignore + * ensures *.user.yaml overrides stay out of version control by default. + */ + async _installSharedScripts(paths) { + const srcScriptsDir = path.join(paths.srcDir, 'src', 'scripts'); + if (await fs.pathExists(srcScriptsDir)) { + const entries = await fs.readdir(srcScriptsDir, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isFile()) continue; + const srcFile = path.join(srcScriptsDir, entry.name); + const destFile = path.join(paths.scriptsDir, entry.name); + await fs.copy(srcFile, destFile, { overwrite: true }); + this.installedFiles.add(destFile); + } + } + + const customGitignore = path.join(paths.customDir, '.gitignore'); + if (!(await fs.pathExists(customGitignore))) { + await fs.writeFile(customGitignore, '*.user.yaml\n', 'utf8'); + this.installedFiles.add(customGitignore); + } + } + /** * Install official (non-custom) modules. * @param {Object} config - Installation configuration @@ -789,9 +823,8 @@ class Installer { // Get all installed module directories const entries = await fs.readdir(bmadDir, { withFileTypes: true }); - const installedModules = entries - .filter((entry) => entry.isDirectory() && entry.name !== '_config' && entry.name !== 'docs') - .map((entry) => entry.name); + const nonModuleDirs = new Set(['_config', '_memory', 'docs', 'scripts', 'custom']); + const installedModules = entries.filter((entry) => entry.isDirectory() && !nonModuleDirs.has(entry.name)).map((entry) => entry.name); // Generate config.yaml for each installed module for (const moduleName of installedModules) { @@ -917,9 +950,8 @@ class Installer { // Get all installed module directories const entries = await fs.readdir(bmadDir, { withFileTypes: true }); - const installedModules = entries - .filter((entry) => entry.isDirectory() && entry.name !== '_config' && entry.name !== 'docs' && entry.name !== '_memory') - .map((entry) => entry.name); + const nonModuleDirs = new Set(['_config', '_memory', 'docs', 'scripts', 'custom']); + const installedModules = entries.filter((entry) => entry.isDirectory() && !nonModuleDirs.has(entry.name)).map((entry) => entry.name); // Add core module to scan (it's installed at root level as _config, but we check src/core-skills) const coreModulePath = getSourcePath('core-skills'); diff --git a/tools/installer/modules/official-modules.js b/tools/installer/modules/official-modules.js index 19dc0f4dc..421797a06 100644 --- a/tools/installer/modules/official-modules.js +++ b/tools/installer/modules/official-modules.js @@ -820,10 +820,10 @@ class OfficialModules { let foundAny = false; const entries = await fs.readdir(bmadDir, { withFileTypes: true }); + const nonModuleDirs = new Set(['_config', '_memory', 'docs', 'scripts', 'custom']); for (const entry of entries) { if (entry.isDirectory()) { - // Skip the _config directory - it's for system use - if (entry.name === '_config' || entry.name === '_memory') { + if (nonModuleDirs.has(entry.name)) { continue; }