Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 63 additions & 3 deletions test/test-installation-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -3319,9 +3319,69 @@ async function runTests() {
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();

@augmentcode augmentcode Bot Jun 11, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This suite reuses the same Installer instance across multiple _installSharedScripts calls; since real installs typically create a new Installer per run, this may miss regressions where _bmad/.gitignore isn’t re-tracked/manifested once it already contains config.user.toml.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

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 {
const { parsePythonVersion, classifyPython, detectPython } = require('../tools/installer/core/python-check');
Expand Down Expand Up @@ -3449,7 +3509,7 @@ async function runTests() {
process.exit = real.exit;
}
} 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);
failed++;
}
Expand Down
34 changes: 32 additions & 2 deletions tools/installer/core/installer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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) {
Expand Down
Loading