Compare commits

..

3 Commits

Author SHA1 Message Date
Adam Biggs c93cf677ea ci: add fs wrapper tests to quality workflow 2026-03-06 09:53:54 -07:00
Adam Biggs b43b2bc593 fix(installer): narrow error handling in copy() and fix test interdependency
- copy() overwrite:false catch now only ignores ENOENT/ENOTDIR,
  consistent with pathExists(); permission errors propagate correctly
- 'copy creates parent directories' test creates its own fixture
  instead of depending on state from a previous test
2026-03-06 09:53:54 -07:00
Adam Biggs 5c04a0595b fix(installer): replace fs-extra with native fs to prevent non-deterministic file loss
fs-extra routes all async operations through graceful-fs, whose EMFILE
retry queue causes non-deterministic file loss on macOS APFS during bulk
copy operations (~500+ files). Approximately 50% of install runs lose
26+ files from _bmad/.

Replace fs-extra entirely with a thin native wrapper (tools/cli/lib/fs.js)
that provides the same API surface backed by node:fs and node:fs/promises.
Copy and remove operations use synchronous native calls to eliminate the
race condition. Verified across 8+ consecutive runs with zero file loss.

- Add tools/cli/lib/fs.js native wrapper with full fs-extra API compat
- Update all 40 files to require the wrapper instead of fs-extra
- Remove fs-extra from package.json dependencies
- Add 37-test suite including 250-file bulk copy determinism test
2026-03-06 09:53:54 -07:00
2 changed files with 2 additions and 10 deletions

View File

@ -405,15 +405,12 @@ async function runTests() {
const p = path.join(TMP, 'bad.json'); const p = path.join(TMP, 'bad.json');
nativeFs.writeFileSync(p, '{ invalid json }'); nativeFs.writeFileSync(p, '{ invalid json }');
let threw = false; let threw = false;
let errorMessage = '';
try { try {
fs.readJsonSync(p); fs.readJsonSync(p);
} catch (error) { } catch {
threw = true; threw = true;
errorMessage = error.message;
} }
assert(threw, 'readJsonSync did not throw on invalid JSON'); assert(threw, 'readJsonSync did not throw on invalid JSON');
assert(errorMessage.includes(p), 'readJsonSync error did not include file path');
}); });
test('readJsonSync strips UTF-8 BOM', () => { test('readJsonSync strips UTF-8 BOM', () => {

View File

@ -165,10 +165,5 @@ module.exports.move = async function move(src, dest) {
*/ */
module.exports.readJsonSync = function readJsonSync(filePath) { module.exports.readJsonSync = function readJsonSync(filePath) {
const raw = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''); const raw = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
try {
return JSON.parse(raw); return JSON.parse(raw);
} catch (error) {
error.message = `Failed to parse JSON in ${filePath}: ${error.message}`;
throw error;
}
}; };