From 10288d23d3252a7bec5367437f29aa791cc5d842 Mon Sep 17 00:00:00 2001 From: Dov Benyomin Sohacheski Date: Thu, 11 Jun 2026 23:08:37 +0300 Subject: [PATCH] fix(installer): gitignore default _bmad/config.user.toml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The installer seeded _bmad/custom/.gitignore so custom/*.user.toml overrides stayed out of version control, but left the default _bmad/config.user.toml — which holds the same personal install answers — uncovered. Seed _bmad/.gitignore during install so both user config locations behave consistently. Fixes #2456 --- test/test-installation-components.js | 55 ++++++++++++++++++++++++++++ tools/installer/core/installer.js | 31 +++++++++++++++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 6e015322a..862100b8f 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -3318,6 +3318,61 @@ async function runTests() { console.log(''); + // ============================================================ + // Test Suite 46: shared-scripts install gitignores config.user.toml (#2456) + // ============================================================ + console.log(`${colors.yellow}Test Suite 46: shared-scripts install gitignores user config${colors.reset}\n`); + + let root46; + try { + root46 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-gitignore-test-')); + const bmadDir46 = path.join(root46, '_bmad'); + const customDir46 = path.join(bmadDir46, 'custom'); + await fs.ensureDir(customDir46); + + // _installSharedScripts only reaches for these four path fields, so a plain + // object stands in for the frozen InstallPaths instance. srcDir points at + // the real repo so src/scripts is copied into scriptsDir as in production. + const paths46 = { + srcDir: projectRoot, + bmadDir: bmadDir46, + customDir: customDir46, + scriptsDir: path.join(bmadDir46, 'scripts'), + }; + + const installer46 = new Installer(); + await installer46._installSharedScripts(paths46); + + const bmadGitignore46 = path.join(bmadDir46, '.gitignore'); + assert(await fs.pathExists(bmadGitignore46), '_installSharedScripts seeds _bmad/.gitignore'); + const ignoreLines46 = (await fs.readFile(bmadGitignore46, 'utf8')).split(/\r?\n/); + assert(ignoreLines46.includes('config.user.toml'), '_bmad/.gitignore ignores config.user.toml'); + assert(installer46.installedFiles.has(bmadGitignore46), '_bmad/.gitignore is tracked as an installed file'); + + // The pre-existing custom/*.user.toml rule is still seeded alongside it. + assert(await fs.pathExists(path.join(customDir46, '.gitignore')), '_installSharedScripts still seeds _bmad/custom/.gitignore'); + + // Idempotent: a second install must not duplicate the entry. + await installer46._installSharedScripts(paths46); + const occurrences46 = (await fs.readFile(bmadGitignore46, 'utf8')).split(/\r?\n/).filter((line) => line === 'config.user.toml').length; + assert(occurrences46 === 1, 'second install does not duplicate the config.user.toml entry'); + + // An existing .gitignore with unrelated rules is topped up, not clobbered. + await fs.writeFile(bmadGitignore46, 'notes.local.md\n'); + await installer46._installSharedScripts(paths46); + const toppedUp46 = await fs.readFile(bmadGitignore46, 'utf8'); + assert(toppedUp46.includes('notes.local.md'), 'existing .gitignore rules are preserved'); + assert(toppedUp46.split(/\r?\n/).includes('config.user.toml'), 'config.user.toml is appended to an existing .gitignore'); + } catch (error) { + console.log(`${colors.red}Test Suite 46 setup failed: ${error.message}${colors.reset}`); + console.log(error.stack); + failed++; + } finally { + if (root46) await fs.remove(root46).catch(() => {}); + } + + console.log(''); + // ============================================================ // Summary // ============================================================ diff --git a/tools/installer/core/installer.js b/tools/installer/core/installer.js index 9c6c6cb6c..c779be52b 100644 --- a/tools/installer/core/installer.js +++ b/tools/installer/core/installer.js @@ -658,8 +658,8 @@ class Installer { * Excludes dev-only tests and Python caches so they don't ship to users. * Wipes the destination first so files removed or renamed in source * don't linger and get recorded as installed. Also seeds - * _bmad/custom/.gitignore on fresh installs so *.user.toml overrides - * stay out of version control. + * _bmad/custom/.gitignore so *.user.toml overrides and _bmad/.gitignore + * so the default config.user.toml both stay out of version control. */ async _installSharedScripts(paths) { const srcScriptsDir = path.join(paths.srcDir, 'src', 'scripts'); @@ -682,6 +682,33 @@ class Installer { await fs.writeFile(customGitignore, '*.user.toml\n', 'utf8'); this.installedFiles.add(customGitignore); } + + // The default _bmad/config.user.toml holds personal install answers, so it + // gets the same treatment as custom/*.user.toml above — seed _bmad/.gitignore + // so the file never lands in version control. Append to an existing + // .gitignore that lacks the entry so the rule reaches projects predating it. + await this._ensureUserConfigGitignored(paths.bmadDir); + } + + /** + * Keep the personal _bmad/config.user.toml out of version control. Creates + * _bmad/.gitignore when missing, or appends the entry to an existing file + * that doesn't already list it, so the rule lands on fresh installs and + * updates alike without duplicating the line. + */ + async _ensureUserConfigGitignored(bmadDir) { + const gitignorePath = path.join(bmadDir, '.gitignore'); + const entry = 'config.user.toml'; + + if (await fs.pathExists(gitignorePath)) { + const existing = await fs.readFile(gitignorePath, 'utf8'); + if (existing.split(/\r?\n/).some((line) => line.trim() === entry)) return; + const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n' : ''; + await fs.writeFile(gitignorePath, `${existing}${separator}${entry}\n`, 'utf8'); + } else { + await fs.writeFile(gitignorePath, `${entry}\n`, 'utf8'); + } + this.installedFiles.add(gitignorePath); } async _trackFilesRecursive(dir) {