Compare commits

...

4 Commits

Author SHA1 Message Date
Dov Benyomin Sohacheski a4fa783258
Merge b0f0c4d45f into 242dc6ef75 2026-06-15 09:23:10 +07:00
Dov Benyomin Sohacheski b0f0c4d45f Merge remote-tracking branch 'origin/main' into fix/gitignore-default-user-config
# Conflicts:
#	test/test-installation-components.js
2026-06-12 13:43:55 +03:00
Dov Benyomin Sohacheski 7b1bfbf2b9
fix(installer): track _bmad/.gitignore in manifest on every install run
Address review: the early return when config.user.toml was already
ignored skipped installedFiles.add, dropping _bmad/.gitignore from
files-manifest.csv on re-installs. Suite 46 now uses a fresh Installer
per run, as production does, so the regression is actually exercised.
2026-06-11 23:19:35 +03:00
Dov Benyomin Sohacheski 10288d23d3
fix(installer): gitignore default _bmad/config.user.toml
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
2026-06-11 23:08:37 +03:00
2 changed files with 95 additions and 5 deletions

View File

@ -3319,9 +3319,69 @@ async function runTests() {
console.log(''); console.log('');
// ============================================================ // ============================================================
// Test Suite 46: Python environment check (version parsing + classification) // Test Suite 46: shared-scripts install gitignores config.user.toml (#2456)
// ============================================================ // ============================================================
console.log(`${colors.yellow}Test Suite 46: python-check version parsing and classification${colors.reset}\n`); 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('');
// ============================================================
// Test Suite 47: Python environment check (version parsing + classification)
// ============================================================
console.log(`${colors.yellow}Test Suite 47: python-check version parsing and classification${colors.reset}\n`);
try { try {
const { parsePythonVersion, classifyPython, detectPython } = require('../tools/installer/core/python-check'); const { parsePythonVersion, classifyPython, detectPython } = require('../tools/installer/core/python-check');
@ -3449,7 +3509,7 @@ async function runTests() {
process.exit = real.exit; process.exit = real.exit;
} }
} catch (error) { } catch (error) {
console.log(`${colors.red}Test Suite 46 setup failed: ${error.message}${colors.reset}`); console.log(`${colors.red}Test Suite 47 setup failed: ${error.message}${colors.reset}`);
console.log(error.stack); console.log(error.stack);
failed++; failed++;
} }

View File

@ -658,8 +658,8 @@ class Installer {
* Excludes dev-only tests and Python caches so they don't ship to users. * 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 * Wipes the destination first so files removed or renamed in source
* don't linger and get recorded as installed. Also seeds * don't linger and get recorded as installed. Also seeds
* _bmad/custom/.gitignore on fresh installs so *.user.toml overrides * _bmad/custom/.gitignore so *.user.toml overrides and _bmad/.gitignore
* stay out of version control. * so the default config.user.toml both stay out of version control.
*/ */
async _installSharedScripts(paths) { async _installSharedScripts(paths) {
const srcScriptsDir = path.join(paths.srcDir, 'src', 'scripts'); const srcScriptsDir = path.join(paths.srcDir, 'src', 'scripts');
@ -682,6 +682,36 @@ class Installer {
await fs.writeFile(customGitignore, '*.user.toml\n', 'utf8'); await fs.writeFile(customGitignore, '*.user.toml\n', 'utf8');
this.installedFiles.add(customGitignore); 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) { async _trackFilesRecursive(dir) {