diff --git a/package.json b/package.json index 875d788f5..a26398fdf 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 10639bab8..f1c1be486 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -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'); diff --git a/tools/installer/commands/status.js b/tools/installer/commands/status.js index 49c0afd73..c7f4a816c 100644 --- a/tools/installer/commands/status.js +++ b/tools/installer/commands/status.js @@ -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}`); diff --git a/tools/installer/commands/uninstall.js b/tools/installer/commands/uninstall.js index d0e168a15..727b7b0ef 100644 --- a/tools/installer/commands/uninstall.js +++ b/tools/installer/commands/uninstall.js @@ -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'); diff --git a/tools/installer/core/existing-install.js b/tools/installer/core/existing-install.js index 643f1d946..6bbf191d1 100644 --- a/tools/installer/core/existing-install.js +++ b/tools/installer/core/existing-install.js @@ -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'); diff --git a/tools/installer/core/install-paths.js b/tools/installer/core/install-paths.js index f1c50ee43..e7fb98b6d 100644 --- a/tools/installer/core/install-paths.js +++ b/tools/installer/core/install-paths.js @@ -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'); diff --git a/tools/installer/core/installer.js b/tools/installer/core/installer.js index 95e16adfe..2a9ff3272 100644 --- a/tools/installer/core/installer.js +++ b/tools/installer/core/installer.js @@ -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'); diff --git a/tools/installer/core/manifest-generator.js b/tools/installer/core/manifest-generator.js index 13e33af56..477142888 100644 --- a/tools/installer/core/manifest-generator.js +++ b/tools/installer/core/manifest-generator.js @@ -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'); diff --git a/tools/installer/core/manifest.js b/tools/installer/core/manifest.js index aaa86649a..2dc94ae9f 100644 --- a/tools/installer/core/manifest.js +++ b/tools/installer/core/manifest.js @@ -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'); diff --git a/tools/installer/file-ops.js b/tools/installer/file-ops.js index 5cd7970d8..2a2869930 100644 --- a/tools/installer/file-ops.js +++ b/tools/installer/file-ops.js @@ -1,4 +1,4 @@ -const fs = require('fs-extra'); +const fs = require('./fs-native'); const path = require('node:path'); const crypto = require('node:crypto'); diff --git a/tools/installer/fs-native.js b/tools/installer/fs-native.js new file mode 100644 index 000000000..b6a4abfa5 --- /dev/null +++ b/tools/installer/fs-native.js @@ -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, +}; diff --git a/tools/installer/ide/_config-driven.js b/tools/installer/ide/_config-driven.js index 9c7df4bc5..563818f67 100644 --- a/tools/installer/ide/_config-driven.js +++ b/tools/installer/ide/_config-driven.js @@ -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'); diff --git a/tools/installer/ide/platform-codes.js b/tools/installer/ide/platform-codes.js index 32d82e9cc..f29be8fcb 100644 --- a/tools/installer/ide/platform-codes.js +++ b/tools/installer/ide/platform-codes.js @@ -1,4 +1,4 @@ -const fs = require('fs-extra'); +const fs = require('../fs-native'); const path = require('node:path'); const yaml = require('yaml'); diff --git a/tools/installer/ide/shared/skill-manifest.js b/tools/installer/ide/shared/skill-manifest.js index 746d5d16f..1dfc7eb35 100644 --- a/tools/installer/ide/shared/skill-manifest.js +++ b/tools/installer/ide/shared/skill-manifest.js @@ -1,5 +1,5 @@ const path = require('node:path'); -const fs = require('fs-extra'); +const fs = require('../../fs-native'); const yaml = require('yaml'); /** diff --git a/tools/installer/message-loader.js b/tools/installer/message-loader.js index 03ba7eca1..97f02d6e4 100644 --- a/tools/installer/message-loader.js +++ b/tools/installer/message-loader.js @@ -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'); diff --git a/tools/installer/modules/community-manager.js b/tools/installer/modules/community-manager.js index 0f88cffff..3e0217688 100644 --- a/tools/installer/modules/community-manager.js +++ b/tools/installer/modules/community-manager.js @@ -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'); diff --git a/tools/installer/modules/custom-module-manager.js b/tools/installer/modules/custom-module-manager.js index e0f8b7085..482c4dc43 100644 --- a/tools/installer/modules/custom-module-manager.js +++ b/tools/installer/modules/custom-module-manager.js @@ -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'); diff --git a/tools/installer/modules/external-manager.js b/tools/installer/modules/external-manager.js index 0b8f5074c..5169ffb50 100644 --- a/tools/installer/modules/external-manager.js +++ b/tools/installer/modules/external-manager.js @@ -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'); diff --git a/tools/installer/modules/official-modules.js b/tools/installer/modules/official-modules.js index 6158a7863..19dc0f4dc 100644 --- a/tools/installer/modules/official-modules.js +++ b/tools/installer/modules/official-modules.js @@ -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'); diff --git a/tools/installer/modules/plugin-resolver.js b/tools/installer/modules/plugin-resolver.js index 9fbf325a2..58e20ab88 100644 --- a/tools/installer/modules/plugin-resolver.js +++ b/tools/installer/modules/plugin-resolver.js @@ -1,4 +1,4 @@ -const fs = require('fs-extra'); +const fs = require('../fs-native'); const path = require('node:path'); const yaml = require('yaml'); diff --git a/tools/installer/project-root.js b/tools/installer/project-root.js index 26063f81f..037f1a430 100644 --- a/tools/installer/project-root.js +++ b/tools/installer/project-root.js @@ -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 diff --git a/tools/installer/ui.js b/tools/installer/ui.js index 9e48c647a..d1c5189e9 100644 --- a/tools/installer/ui.js +++ b/tools/installer/ui.js @@ -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'); diff --git a/tools/migrate-custom-module-paths.js b/tools/migrate-custom-module-paths.js index 13aa3e710..b199e8bfe 100755 --- a/tools/migrate-custom-module-paths.js +++ b/tools/migrate-custom-module-paths.js @@ -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');