fix(installer): replace fs-extra with native node:fs to prevent file loss
fs-extra routes all operations through graceful-fs, which globally monkey-patches node:fs with a deferred retry queue. During multi-module installs (~500+ file ops), retried unlink operations from one module's remove phase can fire after the next module's copy phase has written files, silently deleting them non-deterministically. Replace fs-extra with a thin fs-native.js wrapper over node:fs/promises and node:fs. All 21 consumers now use native APIs with no global monkey-patching, eliminating the retry-queue race condition entirely. Closes #1779
This commit is contained in:
parent
82632a4872
commit
a6d075bd0b
|
|
@ -70,7 +70,6 @@
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"commander": "^14.0.0",
|
"commander": "^14.0.0",
|
||||||
"csv-parse": "^6.1.0",
|
"csv-parse": "^6.1.0",
|
||||||
"fs-extra": "^11.3.0",
|
|
||||||
"glob": "^11.0.3",
|
"glob": "^11.0.3",
|
||||||
"ignore": "^7.0.5",
|
"ignore": "^7.0.5",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const os = require('node:os');
|
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 { Installer } = require('../tools/installer/core/installer');
|
||||||
const { ManifestGenerator } = require('../tools/installer/core/manifest-generator');
|
const { ManifestGenerator } = require('../tools/installer/core/manifest-generator');
|
||||||
const { OfficialModules } = require('../tools/installer/modules/official-modules');
|
const { OfficialModules } = require('../tools/installer/modules/official-modules');
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ module.exports = {
|
||||||
const { bmadDir } = await installer.findBmadDir(projectDir);
|
const { bmadDir } = await installer.findBmadDir(projectDir);
|
||||||
|
|
||||||
// Check if bmad directory exists
|
// Check if bmad directory exists
|
||||||
const fs = require('fs-extra');
|
const fs = require('../fs-native');
|
||||||
if (!(await fs.pathExists(bmadDir))) {
|
if (!(await fs.pathExists(bmadDir))) {
|
||||||
await prompts.log.warn('No BMAD installation found in the current directory.');
|
await prompts.log.warn('No BMAD installation found in the current directory.');
|
||||||
await prompts.log.message(`Expected location: ${bmadDir}`);
|
await prompts.log.message(`Expected location: ${bmadDir}`);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('../fs-native');
|
||||||
const prompts = require('../prompts');
|
const prompts = require('../prompts');
|
||||||
const { Installer } = require('../core/installer');
|
const { Installer } = require('../core/installer');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('../fs-native');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
const { Manifest } = require('./manifest');
|
const { Manifest } = require('./manifest');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('../fs-native');
|
||||||
const { getProjectRoot } = require('../project-root');
|
const { getProjectRoot } = require('../project-root');
|
||||||
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
|
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('../fs-native');
|
||||||
const { Manifest } = require('./manifest');
|
const { Manifest } = require('./manifest');
|
||||||
const { OfficialModules } = require('../modules/official-modules');
|
const { OfficialModules } = require('../modules/official-modules');
|
||||||
const { IdeManager } = require('../ide/manager');
|
const { IdeManager } = require('../ide/manager');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('../fs-native');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
const crypto = require('node:crypto');
|
const crypto = require('node:crypto');
|
||||||
const csv = require('csv-parse/sync');
|
const csv = require('csv-parse/sync');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('../fs-native');
|
||||||
const crypto = require('node:crypto');
|
const crypto = require('node:crypto');
|
||||||
const { getProjectRoot } = require('../project-root');
|
const { getProjectRoot } = require('../project-root');
|
||||||
const prompts = require('../prompts');
|
const prompts = require('../prompts');
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const fs = require('fs-extra');
|
const fs = require('./fs-native');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const crypto = require('node:crypto');
|
const crypto = require('node:crypto');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
// 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 srcStat = await fsp.stat(src);
|
||||||
|
|
||||||
|
if (srcStat.isFile()) {
|
||||||
|
if (filterFn && !(await filterFn(src, dest))) return;
|
||||||
|
await fsp.mkdir(path.dirname(dest), { recursive: true });
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('../fs-native');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
const prompts = require('../prompts');
|
const prompts = require('../prompts');
|
||||||
const csv = require('csv-parse/sync');
|
const csv = require('csv-parse/sync');
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const fs = require('fs-extra');
|
const fs = require('../fs-native');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('../../fs-native');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const fs = require('fs-extra');
|
const fs = require('./fs-native');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
const prompts = require('./prompts');
|
const prompts = require('./prompts');
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const fs = require('fs-extra');
|
const fs = require('../fs-native');
|
||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const { execSync } = require('node:child_process');
|
const { execSync } = require('node:child_process');
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const fs = require('fs-extra');
|
const fs = require('../fs-native');
|
||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const { execSync } = require('node:child_process');
|
const { execSync } = require('node:child_process');
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const fs = require('fs-extra');
|
const fs = require('../fs-native');
|
||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const { execSync } = require('node:child_process');
|
const { execSync } = require('node:child_process');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('../fs-native');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
const prompts = require('../prompts');
|
const prompts = require('../prompts');
|
||||||
const { getProjectRoot, getSourcePath, getModulePath } = require('../project-root');
|
const { getProjectRoot, getSourcePath, getModulePath } = require('../project-root');
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const fs = require('fs-extra');
|
const fs = require('../fs-native');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const path = require('node:path');
|
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
|
* Find the BMAD project root directory by looking for package.json
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
const fs = require('fs-extra');
|
const fs = require('./fs-native');
|
||||||
const { CLIUtils } = require('./cli-utils');
|
const { CLIUtils } = require('./cli-utils');
|
||||||
const { ExternalModuleManager } = require('./modules/external-manager');
|
const { ExternalModuleManager } = require('./modules/external-manager');
|
||||||
const { getProjectRoot } = require('./project-root');
|
const { getProjectRoot } = require('./project-root');
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* This should be run once to update existing installations
|
* 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 path = require('node:path');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue