Merge pull request #2253 from bmad-code-org/fix-fs-extra-graceful-fs

fix(installer): replace fs-extra with native node:fs to prevent file loss
This commit is contained in:
Brian 2026-04-13 00:55:09 -05:00 committed by GitHub
commit 5456b26ab7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 132 additions and 22 deletions

View File

@ -70,7 +70,6 @@
"chalk": "^4.1.2",
"commander": "^14.0.0",
"csv-parse": "^6.1.0",
"fs-extra": "^11.3.0",
"glob": "^11.0.3",
"ignore": "^7.0.5",
"js-yaml": "^4.1.0",

View File

@ -13,7 +13,7 @@
const path = require('node:path');
const os = require('node:os');
const fs = require('fs-extra');
const fs = require('../tools/installer/fs-native');
const { Installer } = require('../tools/installer/core/installer');
const { ManifestGenerator } = require('../tools/installer/core/manifest-generator');
const { OfficialModules } = require('../tools/installer/modules/official-modules');

View File

@ -19,7 +19,7 @@ module.exports = {
const { bmadDir } = await installer.findBmadDir(projectDir);
// Check if bmad directory exists
const fs = require('fs-extra');
const fs = require('../fs-native');
if (!(await fs.pathExists(bmadDir))) {
await prompts.log.warn('No BMAD installation found in the current directory.');
await prompts.log.message(`Expected location: ${bmadDir}`);

View File

@ -1,5 +1,5 @@
const path = require('node:path');
const fs = require('fs-extra');
const fs = require('../fs-native');
const prompts = require('../prompts');
const { Installer } = require('../core/installer');

View File

@ -1,5 +1,5 @@
const path = require('node:path');
const fs = require('fs-extra');
const fs = require('../fs-native');
const yaml = require('yaml');
const { Manifest } = require('./manifest');

View File

@ -1,5 +1,5 @@
const path = require('node:path');
const fs = require('fs-extra');
const fs = require('../fs-native');
const { getProjectRoot } = require('../project-root');
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');

View File

@ -1,5 +1,5 @@
const path = require('node:path');
const fs = require('fs-extra');
const fs = require('../fs-native');
const { Manifest } = require('./manifest');
const { OfficialModules } = require('../modules/official-modules');
const { IdeManager } = require('../ide/manager');

View File

@ -1,5 +1,5 @@
const path = require('node:path');
const fs = require('fs-extra');
const fs = require('../fs-native');
const yaml = require('yaml');
const crypto = require('node:crypto');
const csv = require('csv-parse/sync');

View File

@ -1,5 +1,5 @@
const path = require('node:path');
const fs = require('fs-extra');
const fs = require('../fs-native');
const crypto = require('node:crypto');
const { getProjectRoot } = require('../project-root');
const prompts = require('../prompts');

View File

@ -1,4 +1,4 @@
const fs = require('fs-extra');
const fs = require('./fs-native');
const path = require('node:path');
const crypto = require('node:crypto');

View File

@ -0,0 +1,111 @@
// Drop-in replacement for fs-extra using native node:fs APIs.
// Eliminates graceful-fs monkey-patching that causes non-deterministic
// file loss during multi-module installs on macOS (issue #1779).
const fsp = require('node:fs/promises');
const fs = require('node:fs');
const path = require('node:path');
async function pathExists(p) {
try {
await fsp.access(p);
return true;
} catch {
return false;
}
}
async function ensureDir(dir) {
await fsp.mkdir(dir, { recursive: true });
}
async function remove(p) {
await fsp.rm(p, { recursive: true, force: true });
}
async function copy(src, dest, options = {}) {
const filterFn = options.filter;
const overwrite = options.overwrite !== false;
const srcStat = await fsp.stat(src);
if (srcStat.isFile()) {
if (filterFn && !(await filterFn(src, dest))) return;
await fsp.mkdir(path.dirname(dest), { recursive: true });
if (!overwrite) {
try {
await fsp.access(dest);
if (options.errorOnExist) throw new Error(`${dest} already exists`);
return;
} catch (error) {
if (error.message.includes('already exists')) throw error;
}
}
await fsp.copyFile(src, dest);
return;
}
if (srcStat.isDirectory()) {
if (filterFn && !(await filterFn(src, dest))) return;
await fsp.mkdir(dest, { recursive: true });
const entries = await fsp.readdir(src, { withFileTypes: true });
for (const entry of entries) {
await copy(path.join(src, entry.name), path.join(dest, entry.name), options);
}
}
}
async function move(src, dest) {
try {
await fsp.rename(src, dest);
} catch (error) {
if (error.code === 'EXDEV') {
await copy(src, dest);
await fsp.rm(src, { recursive: true, force: true });
} else {
throw error;
}
}
}
function readJsonSync(p) {
return JSON.parse(fs.readFileSync(p, 'utf8'));
}
async function writeJson(p, data, options = {}) {
const spaces = options.spaces ?? 2;
await fsp.writeFile(p, JSON.stringify(data, null, spaces) + '\n', 'utf8');
}
module.exports = {
// Native async (node:fs/promises)
readFile: fsp.readFile,
writeFile: fsp.writeFile,
stat: fsp.stat,
readdir: fsp.readdir,
access: fsp.access,
rename: fsp.rename,
unlink: fsp.unlink,
chmod: fsp.chmod,
mkdir: fsp.mkdir,
mkdtemp: fsp.mkdtemp,
copyFile: fsp.copyFile,
rm: fsp.rm,
// fs-extra compatible helpers (native implementations)
pathExists,
ensureDir,
remove,
copy,
move,
readJsonSync,
writeJson,
// Sync methods from core node:fs
existsSync: fs.existsSync.bind(fs),
readFileSync: fs.readFileSync.bind(fs),
writeFileSync: fs.writeFileSync.bind(fs),
createReadStream: fs.createReadStream.bind(fs),
pathExistsSync: fs.existsSync.bind(fs),
// Constants
constants: fs.constants,
};

View File

@ -1,6 +1,6 @@
const os = require('node:os');
const path = require('node:path');
const fs = require('fs-extra');
const fs = require('../fs-native');
const yaml = require('yaml');
const prompts = require('../prompts');
const csv = require('csv-parse/sync');

View File

@ -1,4 +1,4 @@
const fs = require('fs-extra');
const fs = require('../fs-native');
const path = require('node:path');
const yaml = require('yaml');

View File

@ -1,5 +1,5 @@
const path = require('node:path');
const fs = require('fs-extra');
const fs = require('../../fs-native');
const yaml = require('yaml');
/**

View File

@ -1,4 +1,4 @@
const fs = require('fs-extra');
const fs = require('./fs-native');
const path = require('node:path');
const yaml = require('yaml');
const prompts = require('./prompts');

View File

@ -1,4 +1,4 @@
const fs = require('fs-extra');
const fs = require('../fs-native');
const os = require('node:os');
const path = require('node:path');
const { execSync } = require('node:child_process');

View File

@ -1,4 +1,4 @@
const fs = require('fs-extra');
const fs = require('../fs-native');
const os = require('node:os');
const path = require('node:path');
const { execSync } = require('node:child_process');

View File

@ -1,4 +1,4 @@
const fs = require('fs-extra');
const fs = require('../fs-native');
const os = require('node:os');
const path = require('node:path');
const { execSync } = require('node:child_process');

View File

@ -1,5 +1,5 @@
const path = require('node:path');
const fs = require('fs-extra');
const fs = require('../fs-native');
const yaml = require('yaml');
const prompts = require('../prompts');
const { getProjectRoot, getSourcePath, getModulePath } = require('../project-root');

View File

@ -1,4 +1,4 @@
const fs = require('fs-extra');
const fs = require('../fs-native');
const path = require('node:path');
const yaml = require('yaml');

View File

@ -1,5 +1,5 @@
const path = require('node:path');
const fs = require('fs-extra');
const fs = require('./fs-native');
/**
* Find the BMAD project root directory by looking for package.json

View File

@ -1,6 +1,6 @@
const path = require('node:path');
const os = require('node:os');
const fs = require('fs-extra');
const fs = require('./fs-native');
const { CLIUtils } = require('./cli-utils');
const { ExternalModuleManager } = require('./modules/external-manager');
const { getProjectRoot } = require('./project-root');

View File

@ -3,7 +3,7 @@
* This should be run once to update existing installations
*/
const fs = require('fs-extra');
const fs = require('./installer/fs-native');
const path = require('node:path');
const yaml = require('yaml');
const chalk = require('chalk');