diff --git a/.github/workflows/dev-build.yaml b/.github/workflows/dev-build.yaml index 6194b34c..e94c59f5 100644 --- a/.github/workflows/dev-build.yaml +++ b/.github/workflows/dev-build.yaml @@ -6,7 +6,7 @@ concurrency: on: push: - branches: ['refactor-eslint-frontend'] # put your current branch to create a build. Core team only. + branches: ['4774-feat-refactor-collector-to-remove-fluent-ffmpeg-package'] # put your current branch to create a build. Core team only. paths-ignore: - '**.md' - 'cloud-deployments/*' diff --git a/collector/__tests__/utils/WhisperProviders/ffmpeg/index.test.js b/collector/__tests__/utils/WhisperProviders/ffmpeg/index.test.js new file mode 100644 index 00000000..2373d765 --- /dev/null +++ b/collector/__tests__/utils/WhisperProviders/ffmpeg/index.test.js @@ -0,0 +1,76 @@ +process.env.STORAGE_DIR = "test-storage"; +const fs = require("fs"); +const path = require("path"); + +// Mock fix-path as a noop to prevent SIGSEGV (segfault) +jest.mock("fix-path", () => jest.fn()); + +const { FFMPEGWrapper } = require("../../../../utils/WhisperProviders/ffmpeg"); + +const describeRunner = process.env.GITHUB_ACTIONS ? describe.skip : describe; + +describeRunner("FFMPEGWrapper", () => { + /** @type { import("../../../../utils/WhisperProviders/ffmpeg/index").FFMPEGWrapper } */ + let ffmpeg; + const testDir = path.resolve(__dirname, "../../../../storage/tmp"); + const inputPath = path.resolve(testDir, "test-input.wav"); + const outputPath = path.resolve(testDir, "test-output.wav"); + + beforeEach(() => { + ffmpeg = new FFMPEGWrapper(); + }); + + afterEach(() => { + if (fs.existsSync(inputPath)) fs.rmSync(inputPath); + if (fs.existsSync(outputPath)) fs.rmSync(outputPath); + }); + + it("should find ffmpeg executable", async () => { + const knownPath = 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; + expect(ffmpeg.isValidFFMPEG(knownPath)).toBe(true); + }); + + it("should return false for invalid ffmpeg path", () => { + expect(ffmpeg.isValidFFMPEG("/invalid/path/to/ffmpeg")).toBe(false); + }); + + it("should convert audio file to wav format", async () => { + if (!fs.existsSync(testDir)) fs.mkdirSync(testDir, { recursive: true }); + + const sampleUrl = + "https://github.com/ringcentral/ringcentral-api-docs/blob/main/resources/sample1.wav?raw=true"; + + const response = await fetch(sampleUrl); + if (!response.ok) + throw new Error( + `Failed to download sample file: ${response.statusText}` + ); + + const buffer = await response.arrayBuffer(); + fs.writeFileSync(inputPath, Buffer.from(buffer)); + + const result = ffmpeg.convertAudioToWav(inputPath, outputPath); + + expect(result).toBe(true); + expect(fs.existsSync(outputPath)).toBe(true); + + const stats = fs.statSync(outputPath); + expect(stats.size).toBeGreaterThan(0); + }, 30000); + + it("should throw error when conversion fails", () => { + 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.`); + }); +}); diff --git a/collector/package.json b/collector/package.json index 12639bc8..c2c8575d 100644 --- a/collector/package.json +++ b/collector/package.json @@ -22,7 +22,7 @@ "dotenv": "^16.0.3", "epub2": "git+https://github.com/Mintplex-Labs/epub2-static.git#main", "express": "^4.21.2", - "fluent-ffmpeg": "^2.1.2", + "fix-path": "^4.0.0", "html-to-text": "^9.0.5", "ignore": "^5.3.0", "js-tiktoken": "^1.0.8", diff --git a/collector/utils/WhisperProviders/ffmpeg/index.js b/collector/utils/WhisperProviders/ffmpeg/index.js new file mode 100644 index 00000000..5b39b71e --- /dev/null +++ b/collector/utils/WhisperProviders/ffmpeg/index.js @@ -0,0 +1,122 @@ +const fs = require("fs"); +const path = require("path"); +const { execSync, spawnSync } = require("child_process"); + +/** + * Custom FFMPEG wrapper class for audio file conversion. + * Replaces deprecated fluent-ffmpeg package. + * Locates ffmpeg binary and converts audio files to required + * WAV format (16k hz mono 32f) for Whisper transcription. + * + * @class FFMPEGWrapper + */ +class FFMPEGWrapper { + static _instance; + + constructor() { + if (FFMPEGWrapper._instance) return FFMPEGWrapper._instance; + FFMPEGWrapper._instance = this; + this._ffmpegPath = null; + } + + log(text, ...args) { + console.log(`\x1b[35m[FFMPEG]\x1b[0m ${text}`, ...args); + } + + /** + * Locates ffmpeg binary. + * Uses fix-path on non-Windows platforms to ensure we can find ffmpeg. + * + * @returns {string} Path to ffmpeg binary + * @throws {Error} + */ + get 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"); + } + } + + try { + const which = process.platform === "win32" ? "where" : "which"; + const result = execSync(`${which} ffmpeg`, { encoding: "utf8" }).trim(); + const candidatePath = result?.split("\n")?.[0]?.trim(); + if (!candidatePath) throw new Error("FFMPEG candidate path not found."); + if (!this.isValidFFMPEG(candidatePath)) + throw new Error("FFMPEG candidate path is not valid ffmpeg binary."); + + this.log(`Found FFMPEG binary at ${candidatePath}`); + this._ffmpegPath = candidatePath; + return this._ffmpegPath; + } catch (error) { + this.log(error.message); + } + + throw new Error("FFMPEG binary not found."); + } + + /** + * Validates that path points to a valid ffmpeg binary. + * Runs ffmpeg -version command. + * + * @param {string} pathToTest - Path of ffmpeg binary + * @returns {boolean} + */ + isValidFFMPEG(pathToTest) { + try { + if (!pathToTest || !fs.existsSync(pathToTest)) return false; + execSync(`"${pathToTest}" -version`, { encoding: "utf8", stdio: "pipe" }); + return true; + } catch { + return false; + } + } + + /** + * Converts audio file to WAV format with required parameters for Whisper. + * Output: 16k hz, mono, 32bit float. + * + * @param {string} inputPath - Input path for audio file (any format supported by ffmpeg) + * @param {string} outputPath - Output path for converted file + * @returns {boolean} + * @throws {Error} If ffmpeg binary cannot be found or conversion fails + */ + convertAudioToWav(inputPath, outputPath) { + if (!fs.existsSync(inputPath)) + throw new Error(`Input file ${inputPath} does not exist.`); + const outputDir = path.dirname(outputPath); + if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }); + + this.log(`Converting ${path.basename(inputPath)} to WAV format...`); + // Convert to 16k hz mono 32f + const result = spawnSync( + this.ffmpegPath, + [ + "-i", + inputPath, + "-ar", + "16000", + "-ac", + "1", + "-acodec", + "pcm_f32le", + "-y", + outputPath, + ], + { encoding: "utf8" } + ); + + // ffmpeg writes progress to stderr + if (result.stderr) this.log(result.stderr.trim()); + if (result.status !== 0) throw new Error(`FFMPEG conversion failed`); + this.log(`Conversion complete: ${path.basename(outputPath)}`); + return true; + } +} + +module.exports = { FFMPEGWrapper }; diff --git a/collector/utils/WhisperProviders/localWhisper.js b/collector/utils/WhisperProviders/localWhisper.js index c28df7af..d707735e 100644 --- a/collector/utils/WhisperProviders/localWhisper.js +++ b/collector/utils/WhisperProviders/localWhisper.js @@ -64,52 +64,21 @@ class LocalWhisper { try { let buffer; const wavefile = require("wavefile"); - const ffmpeg = require("fluent-ffmpeg"); + const { FFMPEGWrapper } = require("./ffmpeg"); + const ffmpeg = new FFMPEGWrapper(); const outFolder = path.resolve(__dirname, `../../storage/tmp`); if (!fs.existsSync(outFolder)) fs.mkdirSync(outFolder, { recursive: true }); - const fileExtension = path.extname(sourcePath).toLowerCase(); - if (fileExtension !== ".wav") { - this.#log( - `File conversion required! ${fileExtension} file detected - converting to .wav` + const outputFile = path.resolve(outFolder, `${v4()}.wav`); + const success = ffmpeg.convertAudioToWav(sourcePath, outputFile); + if (!success) + throw new Error( + "[Conversion Failed]: Could not convert file to .wav format!" ); - const outputFile = path.resolve(outFolder, `${v4()}.wav`); - const convert = new Promise((resolve) => { - ffmpeg(sourcePath) - .toFormat("wav") - .on("error", (error) => { - this.#log(`Conversion Error! ${error.message}`); - resolve(false); - }) - .on("progress", (progress) => - this.#log( - `Conversion Processing! ${progress.targetSize}KB converted` - ) - ) - .on("end", () => { - this.#log(`Conversion Complete! File converted to .wav!`); - resolve(true); - }) - .save(outputFile); - }); - const success = await convert; - if (!success) - throw new Error( - "[Conversion Failed]: Could not convert file to .wav format!" - ); - const chunks = []; - const stream = fs.createReadStream(outputFile); - for await (let chunk of stream) chunks.push(chunk); - buffer = Buffer.concat(chunks); - fs.rmSync(outputFile); - } else { - const chunks = []; - const stream = fs.createReadStream(sourcePath); - for await (let chunk of stream) chunks.push(chunk); - buffer = Buffer.concat(chunks); - } + buffer = fs.readFileSync(outputFile); + fs.rmSync(outputFile); const wavFile = new wavefile.WaveFile(buffer); try { @@ -119,6 +88,9 @@ class LocalWhisper { throw new Error(`Invalid audio file: ${error.message}`); } + // Although we use ffmpeg to convert to the correct format (16k hz 32f), + // different versions of ffmpeg produce different results based on the + // environment. To ensure consistency, we convert to the correct format again. wavFile.toBitDepth("32f"); wavFile.toSampleRate(16000); diff --git a/collector/yarn.lock b/collector/yarn.lock index a8831d65..1d5a6e79 100644 --- a/collector/yarn.lock +++ b/collector/yarn.lock @@ -566,11 +566,6 @@ ast-types@^0.13.4: dependencies: tslib "^2.0.1" -async@^0.2.9: - version "0.2.10" - resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" - integrity sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ== - async@^3.2.3: version "3.2.6" resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" @@ -1007,7 +1002,7 @@ cross-fetch@4.0.0: dependencies: node-fetch "^2.6.12" -cross-spawn@^7.0.1, cross-spawn@^7.0.6: +cross-spawn@^7.0.1, cross-spawn@^7.0.3, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -1152,6 +1147,11 @@ deepmerge@^4.3.1: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== +default-shell@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/default-shell/-/default-shell-2.2.0.tgz#31481c19747bfe59319b486591643eaf115a1864" + integrity sha512-sPpMZcVhRQ0nEMDtuMJ+RtCxt7iHPAMBU+I4tAlo5dU1sjRpNax0crj6nR3qKpvVnckaQ9U38enXcwW9nZJeCw== + define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" @@ -1419,6 +1419,21 @@ events@^3.3.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +execa@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + expand-template@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" @@ -1538,6 +1553,13 @@ finalhandler@~1.3.1: statuses "~2.0.2" unpipe "~1.0.0" +fix-path@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fix-path/-/fix-path-4.0.0.tgz#bc1d14f038edb734ac46944a45454106952ca429" + integrity sha512-g31GX207Tt+psI53ZSaB1egprYbEN0ZYl90aKcO22A2LmCNnFsSq3b5YpoKp3E/QEiWByTXGJOkFQG4S07Bc1A== + dependencies: + shell-path "^3.0.0" + flat@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" @@ -1548,14 +1570,6 @@ flatbuffers@^1.12.0: resolved "https://registry.yarnpkg.com/flatbuffers/-/flatbuffers-1.12.0.tgz#72e87d1726cb1b216e839ef02658aa87dcef68aa" integrity sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ== -fluent-ffmpeg@^2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz#d6846be257777844249a4adeb320f25326d239f3" - integrity sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q== - dependencies: - async "^0.2.9" - which "^1.1.1" - fn.name@1.x.x: version "1.1.0" resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" @@ -1669,6 +1683,11 @@ get-stream@^5.1.0: dependencies: pump "^3.0.0" +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + get-uri@^6.0.1: version "6.0.5" resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.5.tgz#714892aa4a871db671abc5395e5e9447bc306a16" @@ -1812,6 +1831,11 @@ https-proxy-agent@^7.0.2, https-proxy-agent@^7.0.6: agent-base "^7.1.2" debug "4" +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + humanize-duration@^3.25.1: version "3.33.1" resolved "https://registry.yarnpkg.com/humanize-duration/-/humanize-duration-3.33.1.tgz#e4df2ce6660f24a6a3bf4a7b3bc63edb5be7826f" @@ -2272,6 +2296,11 @@ merge-descriptors@1.0.3: resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -2299,6 +2328,11 @@ mime@^3.0.0: resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + mimic-response@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" @@ -2484,6 +2518,13 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + nth-check@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" @@ -2538,6 +2579,13 @@ one-time@^1.0.0: dependencies: fn.name "1.x.x" +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + onnx-proto@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/onnx-proto/-/onnx-proto-4.0.4.tgz#2431a25bee25148e915906dda0687aafe3b9e044" @@ -2705,7 +2753,7 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-key@^3.1.0: +path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -3200,6 +3248,22 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +shell-env@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/shell-env/-/shell-env-4.0.1.tgz#883302d9426095d398a39b102a851adb306b8cb8" + integrity sha512-w3oeZ9qg/P6Lu6qqwavvMnB/bwfsz67gPB3WXmLd/n6zuh7TWQZtGa3iMEdmua0kj8rivkwl+vUjgLWlqZOMPw== + dependencies: + default-shell "^2.0.0" + execa "^5.1.1" + strip-ansi "^7.0.1" + +shell-path@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/shell-path/-/shell-path-3.1.0.tgz#950671fe15de70fb4d984b886d55e8a2f10bfe33" + integrity sha512-s/9q9PEtcRmDTz69+cJ3yYBAe9yGrL7e46gm2bU4pQ9N48ecPK9QrGFnLwYgb4smOHskx4PL7wCNMktW2AoD+g== + dependencies: + shell-env "^4.0.1" + side-channel-list@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" @@ -3240,6 +3304,11 @@ side-channel@^1.1.0: side-channel-map "^1.0.1" side-channel-weakmap "^1.0.2" +signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + signal-exit@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" @@ -3334,16 +3403,7 @@ streamx@^2.15.0, streamx@^2.21.0: fast-fifo "^1.3.2" text-decoder "^1.1.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -3375,14 +3435,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm: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@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.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== @@ -3403,6 +3456,11 @@ strip-dirs@^2.0.0: dependencies: is-natural-number "^4.0.1" +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -3746,13 +3804,6 @@ which-typed-array@^1.1.16: gopd "^1.2.0" has-tostringtag "^1.0.2" -which@^1.1.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -3786,16 +3837,7 @@ winston@^3.13.0: triple-beam "^1.3.0" winston-transport "^4.9.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==