This commit is contained in:
Dov Benyomin Sohacheski 2026-06-11 23:19:52 +03:00 committed by GitHub
commit cdaa9ce336
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 92 additions and 2 deletions

View File

@ -3318,6 +3318,66 @@ 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. Fresh Installer
// per run, as in production — reuse would hide a failure to re-track the file.
const installer46b = new Installer();
await installer46b._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');
assert(installer46b.installedFiles.has(bmadGitignore46), 're-install tracks _bmad/.gitignore even when the entry already exists');
// An existing .gitignore with unrelated rules is topped up, not clobbered.
await fs.writeFile(bmadGitignore46, 'notes.local.md\n');
const installer46c = new Installer();
await installer46c._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');
assert(installer46c.installedFiles.has(bmadGitignore46), 'appending install tracks _bmad/.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
// ============================================================

View File

@ -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,36 @@ 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)) {
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');
}
// Track on every path — each run starts a fresh Installer, so skipping the
// already-correct file would drop it from files-manifest.csv on re-installs.
this.installedFiles.add(gitignorePath);
}
async _trackFilesRecursive(dir) {