Adjust fix path to use ESM import (#4867)

* Adjust fix path to use ESM import

* normalize fix-path imports and usage across the app

* extract path fix logic to utils for server and collector

* add helpers

* repin strip-ansi in collector

* fix log for localWhisper
lint
This commit is contained in:
Timothy Carambat
2026-01-15 16:13:21 -08:00
committed by GitHub
parent 7bdcb9a757
commit feb039ea70
9 changed files with 84 additions and 55 deletions

View File

@@ -6,7 +6,7 @@ concurrency:
on:
push:
branches: ['4391-dmr-support'] # put your current branch to create a build. Core team only.
branches: ['fix-path-patch'] # put your current branch to create a build. Core team only.
paths-ignore:
- '**.md'
- 'cloud-deployments/*'

View File

@@ -3,7 +3,8 @@ const fs = require("fs");
const path = require("path");
// Mock fix-path as a noop to prevent SIGSEGV (segfault)
jest.mock("fix-path", () => jest.fn());
// Returns ESM-style default export for dynamic import()
jest.mock("fix-path", () => ({ default: jest.fn() }));
const { FFMPEGWrapper } = require("../../../../utils/WhisperProviders/ffmpeg");
@@ -26,14 +27,14 @@ describeRunner("FFMPEGWrapper", () => {
});
it("should find ffmpeg executable", async () => {
const knownPath = ffmpeg.ffmpegPath;
const knownPath = await ffmpeg.ffmpegPath();
expect(knownPath).toBeDefined();
expect(typeof knownPath).toBe("string");
expect(knownPath.length).toBeGreaterThan(0);
});
it("should validate ffmpeg executable", async () => {
const knownPath = ffmpeg.ffmpegPath;
const knownPath = await ffmpeg.ffmpegPath();
expect(ffmpeg.isValidFFMPEG(knownPath)).toBe(true);
});
@@ -56,7 +57,7 @@ describeRunner("FFMPEGWrapper", () => {
const buffer = await response.arrayBuffer();
fs.writeFileSync(inputPath, Buffer.from(buffer));
const result = ffmpeg.convertAudioToWav(inputPath, outputPath);
const result = await ffmpeg.convertAudioToWav(inputPath, outputPath);
expect(result).toBe(true);
expect(fs.existsSync(outputPath)).toBe(true);
@@ -69,8 +70,8 @@ describeRunner("FFMPEGWrapper", () => {
const nonExistentFile = path.resolve(testDir, "non-existent-file.wav");
const outputPath = path.resolve(testDir, "test-output-fail.wav");
expect(() => {
ffmpeg.convertAudioToWav(nonExistentFile, outputPath)
}).toThrow(`Input file ${nonExistentFile} does not exist.`);
expect(async () => {
return await ffmpeg.convertAudioToWav(nonExistentFile, outputPath);
}).rejects.toThrow(`Input file ${nonExistentFile} does not exist.`);
});
});

View File

@@ -39,6 +39,7 @@
"puppeteer": "~21.5.2",
"sharp": "^0.33.5",
"slugify": "^1.6.6",
"strip-ansi": "^7.1.2",
"tesseract.js": "^6.0.0",
"url-pattern": "^1.0.3",
"uuid": "^9.0.0",
@@ -54,7 +55,6 @@
},
"resolutions": {
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
}
}

View File

@@ -1,7 +1,7 @@
const fs = require("fs");
const path = require("path");
const { execSync, spawnSync } = require("child_process");
const { patchShellEnvironmentPath } = require("../../shell");
/**
* Custom FFMPEG wrapper class for audio file conversion.
* Replaces deprecated fluent-ffmpeg package.
@@ -27,20 +27,12 @@ class FFMPEGWrapper {
* Locates ffmpeg binary.
* Uses fix-path on non-Windows platforms to ensure we can find ffmpeg.
*
* @returns {string} Path to ffmpeg binary
* @returns {Promise<string>} Path to ffmpeg binary
* @throws {Error}
*/
get ffmpegPath() {
async ffmpegPath() {
if (this._ffmpegPath) return this._ffmpegPath;
if (process.platform !== "win32") {
try {
const fixPath = require("fix-path");
fixPath();
} catch (error) {
this.log("Could not load fix-path, using system PATH");
}
}
await patchShellEnvironmentPath();
try {
const which = process.platform === "win32" ? "where" : "which";
@@ -83,10 +75,10 @@ class FFMPEGWrapper {
*
* @param {string} inputPath - Input path for audio file (any format supported by ffmpeg)
* @param {string} outputPath - Output path for converted file
* @returns {boolean}
* @returns {Promise<boolean>}
* @throws {Error} If ffmpeg binary cannot be found or conversion fails
*/
convertAudioToWav(inputPath, outputPath) {
async convertAudioToWav(inputPath, outputPath) {
if (!fs.existsSync(inputPath))
throw new Error(`Input file ${inputPath} does not exist.`);
const outputDir = path.dirname(outputPath);
@@ -95,7 +87,7 @@ class FFMPEGWrapper {
this.log(`Converting ${path.basename(inputPath)} to WAV format...`);
// Convert to 16k hz mono 32f
const result = spawnSync(
this.ffmpegPath,
await this.ffmpegPath(),
[
"-i",
inputPath,

View File

@@ -71,7 +71,7 @@ class LocalWhisper {
fs.mkdirSync(outFolder, { recursive: true });
const outputFile = path.resolve(outFolder, `${v4()}.wav`);
const success = ffmpeg.convertAudioToWav(sourcePath, outputFile);
const success = await ffmpeg.convertAudioToWav(sourcePath, outputFile);
if (!success)
throw new Error(
"[Conversion Failed]: Could not convert file to .wav format!"
@@ -136,7 +136,7 @@ class LocalWhisper {
progress_callback: (data) => {
if (!data.hasOwnProperty("progress")) return;
console.log(
`\x1b[34m[Embedding - Downloading Model Files]\x1b[0m ${
`\x1b[34m[ONNXWhisper - Downloading Model Files]\x1b[0m ${
data.file
} ${~~data?.progress}%`
);

25
collector/utils/shell.js Normal file
View File

@@ -0,0 +1,25 @@
/**
* Patch the shell environment path to ensure the PATH is properly set for the current platform.
* On Docker, we are on Node v18 and cannot support fix-path v5.
* So we need to use the ESM-style import() to import the fix-path module + add the strip-ansi call to patch the PATH, which is the only change between v4 and v5.
* https://github.com/sindresorhus/fix-path/issues/6
* @returns {Promise<{[key: string]: string}>} - Environment variables from shell
*/
async function patchShellEnvironmentPath() {
try {
if (process.platform === "win32") return process.env;
const { default: fixPath } = await import("fix-path");
const { default: stripAnsi } = await import("strip-ansi");
fixPath();
if (process.env.PATH) process.env.PATH = stripAnsi(process.env.PATH);
console.log("Shell environment path patched successfully.");
return process.env;
} catch (error) {
console.error("Failed to patch shell environment path:", error);
return process.env;
}
}
module.exports = {
patchShellEnvironmentPath,
};

View File

@@ -504,6 +504,11 @@ ansi-regex@^5.0.1:
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
ansi-regex@^6.0.1:
version "6.2.2"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1"
integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==
ansi-styles@^4.0.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
@@ -3422,13 +3427,20 @@ string_decoder@~1.1.1:
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1, strip-ansi@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba"
integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==
dependencies:
ansi-regex "^6.0.1"
strip-dirs@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5"

View File

@@ -11,6 +11,7 @@ const {
const {
StreamableHTTPClientTransport,
} = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
const { patchShellEnvironmentPath } = require("../../helpers/shell");
/**
* @typedef {'stdio' | 'http' | 'sse'} MCPServerTypes
@@ -230,33 +231,6 @@ class MCPHypervisor {
this.mcpLoadingResults = {};
}
/**
* Load shell environment for desktop applications.
* MacOS and Linux don't inherit login shell environment. So this function
* fixes the PATH and accessible commands when running AnythingLLM outside of Docker during development on Mac/Linux and in-container (Linux).
* @returns {Promise<{[key: string]: string}>} - Environment variables from shell
*/
async #loadShellEnvironment() {
try {
if (process.platform === "win32") return process.env;
const { default: fixPath } = await import("fix-path");
const { default: stripAnsi } = await import("strip-ansi");
fixPath();
// Due to node v20 requirement to have a minimum version of fix-path v5, we need to strip ANSI codes manually
// which was the only patch between v4 and v5. Here we just apply manually.
// https://github.com/sindresorhus/fix-path/issues/6
if (process.env.PATH) process.env.PATH = stripAnsi(process.env.PATH);
return process.env;
} catch (error) {
console.warn(
"Failed to load shell environment, using process.env:",
error.message
);
return process.env;
}
}
/**
* Build the MCP server environment variables - ensures proper PATH and NODE_PATH
* inheritance across all platforms and deployment scenarios.
@@ -264,7 +238,7 @@ class MCPHypervisor {
* @returns {Promise<{env: { [key: string]: string } | {}}}> - The environment variables
*/
async #buildMCPServerENV(server) {
const shellEnv = await this.#loadShellEnvironment();
const shellEnv = await patchShellEnvironmentPath();
let baseEnv = {
PATH:
shellEnv.PATH ||

View File

@@ -0,0 +1,25 @@
/**
* Patch the shell environment path to ensure the PATH is properly set for the current platform.
* On Docker, we are on Node v18 and cannot support fix-path v5.
* So we need to use the ESM-style import() to import the fix-path module + add the strip-ansi call to patch the PATH, which is the only change between v4 and v5.
* https://github.com/sindresorhus/fix-path/issues/6
* @returns {Promise<{[key: string]: string}>} - Environment variables from shell
*/
async function patchShellEnvironmentPath() {
try {
if (process.platform === "win32") return process.env;
const { default: fixPath } = await import("fix-path");
const { default: stripAnsi } = await import("strip-ansi");
fixPath();
if (process.env.PATH) process.env.PATH = stripAnsi(process.env.PATH);
console.log("Shell environment path patched successfully.");
return process.env;
} catch (error) {
console.error("Failed to patch shell environment path:", error);
return process.env;
}
}
module.exports = {
patchShellEnvironmentPath,
};