diff --git a/codex-cli/package.json b/codex-cli/package.json index 6afabe5d..2bfe93ce 100644 --- a/codex-cli/package.json +++ b/codex-cli/package.json @@ -35,6 +35,7 @@ "diff": "^7.0.0", "dotenv": "^16.1.4", "fast-deep-equal": "^3.1.3", + "fast-npm-meta": "^0.4.2", "figures": "^6.1.0", "file-type": "^20.1.0", "ink": "^5.2.0", @@ -44,6 +45,7 @@ "meow": "^13.2.0", "open": "^10.1.0", "openai": "^4.95.1", + "package-manager-detector": "^1.2.0", "react": "^18.2.0", "shell-quote": "^1.8.2", "strip-ansi": "^7.1.0", @@ -57,6 +59,7 @@ "@types/js-yaml": "^4.0.9", "@types/marked-terminal": "^6.1.1", "@types/react": "^18.0.32", + "@types/semver": "^7.7.0", "@types/shell-quote": "^1.7.5", "@types/which": "^3.0.4", "@typescript-eslint/eslint-plugin": "^7.18.0", @@ -71,6 +74,7 @@ "ink-testing-library": "^3.0.0", "prettier": "^2.8.7", "punycode": "^2.3.1", + "semver": "^7.7.1", "ts-node": "^10.9.1", "typescript": "^5.0.3", "vitest": "^3.0.9", diff --git a/codex-cli/src/utils/check-updates.ts b/codex-cli/src/utils/check-updates.ts index b3d6f85a..5e326c1c 100644 --- a/codex-cli/src/utils/check-updates.ts +++ b/codex-cli/src/utils/check-updates.ts @@ -1,87 +1,81 @@ -import { CONFIG_DIR } from "./config"; +import type { AgentName } from "package-manager-detector"; + +import { detectInstallerByPath } from "./package-manager-detector"; +import { CLI_VERSION } from "./session"; import boxen from "boxen"; import chalk from "chalk"; -import * as cp from "node:child_process"; +import { getLatestVersion } from "fast-npm-meta"; import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import which from "which"; +import { getUserAgent } from "package-manager-detector"; +import semver from "semver"; interface UpdateCheckState { lastUpdateCheck?: string; } -interface PackageInfo { - current: string; - wanted: string; - latest: string; - dependent: string; - location: string; -} - interface UpdateCheckInfo { currentVersion: string; latestVersion: string; } -const UPDATE_CHECK_FREQUENCY = 1000 * 60 * 60 * 24; // 1 day - -export async function getNPMCommandPath(): Promise { - try { - return await which(process.platform === "win32" ? "npm.cmd" : "npm"); - } catch { - return undefined; - } +export interface UpdateOptions { + manager: AgentName; + packageName: string; } -export async function checkOutdated( - npmCommandPath: string, -): Promise { - return new Promise((resolve, _reject) => { - // TODO: support local installation - // Right now we're using "--global", which only checks global packages. - // But codex might be installed locally — we should check the local version first, - // and only fall back to the global one if needed. - const args = ["outdated", "--global", "--json", "--", "@openai/codex"]; - // corepack npm wrapper would automatically update package.json. disable that behavior. - // COREPACK_ENABLE_AUTO_PIN disables the package.json overwrite, and - // COREPACK_ENABLE_PROJECT_SPEC makes the npm view command succeed - // even if packageManager specified a package manager other than npm. - const env = { - ...process.env, - COREPACK_ENABLE_AUTO_PIN: "0", - COREPACK_ENABLE_PROJECT_SPEC: "0", - }; - let options: cp.ExecFileOptions = { env }; - let commandPath = npmCommandPath; - if (process.platform === "win32") { - options = { ...options, shell: true }; - commandPath = `"${npmCommandPath}"`; - } - cp.execFile(commandPath, args, options, async (_error, stdout) => { - try { - const { name: packageName } = await import("../../package.json"); - const content: Record = JSON.parse(stdout); - if (!content[packageName]) { - // package not installed or not outdated - resolve(undefined); - return; - } +const UPDATE_CHECK_FREQUENCY = 1000 * 60 * 60 * 24; // 1 day - const currentVersion = content[packageName].current; - const latestVersion = content[packageName].latest; +export function renderUpdateCommand({ + manager, + packageName, +}: UpdateOptions): string { + const updateCommands: Record = { + npm: `npm install -g ${packageName}`, + pnpm: `pnpm add -g ${packageName}`, + bun: `bun add -g ${packageName}`, + /** Only works in yarn@v1 */ + yarn: `yarn global add ${packageName}`, + deno: `deno install -g npm:${packageName}`, + }; - resolve({ currentVersion, latestVersion }); - return; - } catch { - // ignore - } - resolve(undefined); - }); + return updateCommands[manager]; +} + +function renderUpdateMessage(options: UpdateOptions) { + const updateCommand = renderUpdateCommand(options); + return `To update, run ${chalk.magenta(updateCommand)} to update.`; +} + +async function writeState(stateFilePath: string, state: UpdateCheckState) { + await writeFile(stateFilePath, JSON.stringify(state, null, 2), { + encoding: "utf8", }); } +async function getUpdateCheckInfo( + packageName: string, +): Promise { + const metadata = await getLatestVersion(packageName, { + force: true, + throw: false, + }); + + if ("error" in metadata || !metadata?.version) { + return; + } + + return { + currentVersion: CLI_VERSION, + latestVersion: metadata.version, + }; +} + export async function checkForUpdates(): Promise { + const { CONFIG_DIR } = await import("./config"); const stateFile = join(CONFIG_DIR, "update-check.json"); + + // Load previous check timestamp let state: UpdateCheckState | undefined; try { state = JSON.parse(await readFile(stateFile, "utf8")); @@ -89,6 +83,7 @@ export async function checkForUpdates(): Promise { // ignore } + // Bail out if we checked less than the configured frequency ago if ( state?.lastUpdateCheck && Date.now() - new Date(state.lastUpdateCheck).valueOf() < @@ -97,25 +92,39 @@ export async function checkForUpdates(): Promise { return; } - const npmCommandPath = await getNPMCommandPath(); - if (!npmCommandPath) { - return; - } - - const packageInfo = await checkOutdated(npmCommandPath); + // Fetch current vs latest from the registry + const { name: packageName } = await import("../../package.json"); + const packageInfo = await getUpdateCheckInfo(packageName); await writeState(stateFile, { ...state, lastUpdateCheck: new Date().toUTCString(), }); - if (!packageInfo) { + if ( + !packageInfo || + !semver.gt(packageInfo.latestVersion, packageInfo.currentVersion) + ) { return; } - const updateMessage = `To update, run: ${chalk.cyan( - "npm install -g @openai/codex", - )} to update.`; + // Detect global installer + let managerName = await detectInstallerByPath(); + + // Fallback to the local package manager + if (!managerName) { + const local = getUserAgent(); + if (!local) { + // No package managers found, skip it. + return; + } + managerName = local; + } + + const updateMessage = renderUpdateMessage({ + manager: managerName, + packageName, + }); const box = boxen( `\ @@ -135,9 +144,3 @@ ${updateMessage}`, // eslint-disable-next-line no-console console.log(box); } - -async function writeState(stateFilePath: string, state: UpdateCheckState) { - await writeFile(stateFilePath, JSON.stringify(state, null, 2), { - encoding: "utf8", - }); -} diff --git a/codex-cli/src/utils/package-manager-detector.ts b/codex-cli/src/utils/package-manager-detector.ts new file mode 100644 index 00000000..ecd8d9c7 --- /dev/null +++ b/codex-cli/src/utils/package-manager-detector.ts @@ -0,0 +1,73 @@ +import type { AgentName } from "package-manager-detector"; + +import { execFileSync } from "node:child_process"; +import { join, resolve } from "node:path"; +import which from "which"; + +function isInstalled(manager: AgentName): boolean { + try { + which.sync(manager); + return true; + } catch { + return false; + } +} + +function getGlobalBinDir(manager: AgentName): string | undefined { + if (!isInstalled(manager)) { + return; + } + + try { + switch (manager) { + case "npm": { + const stdout = execFileSync("npm", ["prefix", "-g"], { + encoding: "utf-8", + }); + return join(stdout.trim(), "bin"); + } + + case "pnpm": { + // pnpm bin -g prints the bin dir + const stdout = execFileSync("pnpm", ["bin", "-g"], { + encoding: "utf-8", + }); + return stdout.trim(); + } + + case "bun": { + // bun pm bin -g prints your bun global bin folder + const stdout = execFileSync("bun", ["pm", "bin", "-g"], { + encoding: "utf-8", + }); + return stdout.trim(); + } + + default: + return undefined; + } + } catch { + // ignore + } + + return undefined; +} + +export async function detectInstallerByPath(): Promise { + // e.g. /usr/local/bin/codex + const invoked = process.argv[1] && resolve(process.argv[1]); + if (!invoked) { + return; + } + + const supportedManagers: Array = ["npm", "pnpm", "bun"]; + + for (const mgr of supportedManagers) { + const binDir = getGlobalBinDir(mgr); + if (binDir && invoked.startsWith(binDir)) { + return mgr; + } + } + + return undefined; +} diff --git a/codex-cli/tests/__snapshots__/check-updates.test.ts.snap b/codex-cli/tests/__snapshots__/check-updates.test.ts.snap index 2c1631fb..c9514dd3 100644 --- a/codex-cli/tests/__snapshots__/check-updates.test.ts.snap +++ b/codex-cli/tests/__snapshots__/check-updates.test.ts.snap @@ -1,12 +1,12 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Check for updates > should outputs the update message when package is outdated 1`] = ` +exports[`checkForUpdates() > renders a box when a newer version exists and no global installer 1`] = ` " - ╭─────────────────────────────────────────────────────────────╮ - │ │ - │ Update available! 1.0.0 → 2.0.0. │ - │ To update, run: npm install -g @openai/codex to update. │ - │ │ - ╰─────────────────────────────────────────────────────────────╯ + ╭─────────────────────────────────────────────────╮ + │ │ + │ Update available! 1.0.0 → 2.0.0. │ + │ To update, run bun add -g my-pkg to update. │ + │ │ + ╰─────────────────────────────────────────────────╯ " `; diff --git a/codex-cli/tests/check-updates.test.ts b/codex-cli/tests/check-updates.test.ts index a7778968..75ec8aaf 100644 --- a/codex-cli/tests/check-updates.test.ts +++ b/codex-cli/tests/check-updates.test.ts @@ -1,112 +1,178 @@ -import { describe, it, expect, vi } from "vitest"; +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { join } from "node:path"; +import os from "node:os"; +import type { UpdateOptions } from "../src/utils/check-updates"; +import { getLatestVersion } from "fast-npm-meta"; +import { getUserAgent } from "package-manager-detector"; import { checkForUpdates, - checkOutdated, - getNPMCommandPath, -} from "../src/utils/check-updates.js"; -import { execFile } from "node:child_process"; -import { join } from "node:path"; -import { CONFIG_DIR } from "src/utils/config.js"; -import { beforeEach } from "node:test"; - -vi.mock("which", () => ({ - default: vi.fn(() => "/usr/local/bin/npm"), -})); - -vi.mock("child_process", () => ({ - execFile: vi.fn((_cmd, _args, _opts, callback) => { - const stdout = JSON.stringify({ - "@openai/codex": { - current: "1.0.0", - latest: "2.0.0", - }, - }); - callback?.(null, stdout, ""); - return {} as any; - }), -})); + renderUpdateCommand, +} from "../src/utils/check-updates"; +import { detectInstallerByPath } from "../src/utils/package-manager-detector"; +import { CLI_VERSION } from "../src/utils/session"; +// In-memory FS mock let memfs: Record = {}; - -vi.mock("node:fs/promises", async (importOriginal) => ({ - ...(await importOriginal()), - readFile: async (path: string) => { - if (memfs[path] === undefined) { - throw new Error("ENOENT"); - } - return memfs[path]; - }, -})); - -beforeEach(() => { - memfs = {}; // reset in‑memory store +vi.mock("node:fs/promises", async (importOriginal) => { + return { + ...(await importOriginal()), + readFile: async (path: string) => { + if (!(path in memfs)) { + const err: any = new Error( + `ENOENT: no such file or directory, open '${path}'`, + ); + err.code = "ENOENT"; + throw err; + } + return memfs[path]; + }, + writeFile: async (path: string, data: string) => { + memfs[path] = data; + }, + rm: async (path: string) => { + delete memfs[path]; + }, + }; }); -describe("Check for updates", () => { - it("should return the path to npm", async () => { - const npmPath = await getNPMCommandPath(); - expect(npmPath).toBeDefined(); +// Mock package name & CLI version +const MOCK_PKG = "my-pkg"; +vi.mock("../package.json", () => ({ name: MOCK_PKG })); +vi.mock("../src/utils/session", () => ({ CLI_VERSION: "1.0.0" })); +vi.mock("../src/utils/package-manager-detector", async (importOriginal) => { + return { + ...(await importOriginal()), + detectInstallerByPath: vi.fn(), + }; +}); + +// Mock external services +vi.mock("fast-npm-meta", () => ({ getLatestVersion: vi.fn() })); +vi.mock("package-manager-detector", () => ({ getUserAgent: vi.fn() })); + +describe("renderUpdateCommand()", () => { + it.each([ + [{ manager: "npm", packageName: MOCK_PKG }, `npm install -g ${MOCK_PKG}`], + [{ manager: "pnpm", packageName: MOCK_PKG }, `pnpm add -g ${MOCK_PKG}`], + [{ manager: "bun", packageName: MOCK_PKG }, `bun add -g ${MOCK_PKG}`], + [{ manager: "yarn", packageName: MOCK_PKG }, `yarn global add ${MOCK_PKG}`], + [ + { manager: "deno", packageName: MOCK_PKG }, + `deno install -g npm:${MOCK_PKG}`, + ], + ])("%s → command", async (options, cmd) => { + expect(renderUpdateCommand(options as UpdateOptions)).toBe(cmd); + }); +}); + +describe("checkForUpdates()", () => { + // Use a stable directory under the OS temp + const TMP = join(os.tmpdir(), "update-test-memfs"); + const STATE_PATH = join(TMP, "update-check.json"); + + beforeEach(async () => { + memfs = {}; + // Mock CONFIG_DIR to our TMP + vi.doMock("../src/utils/config", () => ({ CONFIG_DIR: TMP })); + + // Freeze time so the 24h logic is deterministic + vi.useFakeTimers().setSystemTime(new Date("2025-01-01T00:00:00Z")); + vi.resetAllMocks(); }); - it("should return undefined if npm is not found", async () => { - vi.mocked(await import("which")).default.mockImplementationOnce(() => { - throw new Error("not found"); - }); - - const npmPath = await getNPMCommandPath(); - expect(npmPath).toBeUndefined(); + afterEach(async () => { + vi.useRealTimers(); }); - it("should return the return value when package is outdated", async () => { - const npmPath = await getNPMCommandPath(); + it("uses global installer when detected, ignoring local agent", async () => { + // seed old timestamp + const old = new Date("2000-01-01T00:00:00Z").toUTCString(); + memfs[STATE_PATH] = JSON.stringify({ lastUpdateCheck: old }); - const info = await checkOutdated(npmPath!); - expect(info).toStrictEqual({ - currentVersion: "1.0.0", - latestVersion: "2.0.0", - }); - }); + // simulate registry says update available + vi.mocked(getLatestVersion).mockResolvedValue({ version: "2.0.0" } as any); + // local agent would be npm, but global detection wins + vi.mocked(getUserAgent).mockReturnValue("npm"); + vi.mocked(detectInstallerByPath).mockReturnValue(Promise.resolve("pnpm")); - it("should return undefined when package is not outdated", async () => { - const npmPath = await getNPMCommandPath(); - vi.mocked(execFile).mockImplementationOnce( - (_cmd, _args, _opts, callback) => { - // Simulate the case where the package is not outdated, returning an empty object - const stdout = JSON.stringify({}); - callback?.(null, stdout, ""); - return {} as any; - }, - ); - - const info = await checkOutdated(npmPath!); - expect(info).toBeUndefined(); - }); - - it("should outputs the update message when package is outdated", async () => { - const codexStatePath = join(CONFIG_DIR, "update-check.json"); - // Use a fixed early date far in the past to ensure it's always at least 1 day before now - memfs[codexStatePath] = JSON.stringify({ - lastUpdateCheck: new Date("2000-01-01T00:00:00Z").toUTCString(), - }); - await checkForUpdates(); - // Spy on console.log to capture output const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + await checkForUpdates(); - expect(logSpy).toHaveBeenCalled(); - // The last call should be the boxen message - const lastCallArg = logSpy.mock.calls.at(-1)?.[0]; - expect(lastCallArg).toMatchSnapshot(); + + // should render using `pnpm` (global) rather than `npm` + expect(logSpy).toHaveBeenCalledOnce(); + const output = logSpy.mock.calls.at(0)?.at(0); + expect(output).toContain("pnpm add -g"); // global branch used + // state updated + const newState = JSON.parse(memfs[STATE_PATH]!); + expect(newState.lastUpdateCheck).toBe(new Date().toUTCString()); }); - it("should not output the update message when package is not outdated", async () => { - const codexStatePath = join(CONFIG_DIR, "update-check.json"); - memfs[codexStatePath] = JSON.stringify({ - lastUpdateCheck: new Date().toUTCString(), - }); - await checkForUpdates(); - // Spy on console.log to capture output + it("skips when lastUpdateCheck is still fresh ( { + // seed a timestamp 12h ago + const recent = new Date(Date.now() - 1000 * 60 * 60 * 12).toUTCString(); + memfs[STATE_PATH] = JSON.stringify({ lastUpdateCheck: recent }); + + const versionSpy = vi.mocked(getLatestVersion); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + await checkForUpdates(); + + expect(versionSpy).not.toHaveBeenCalled(); expect(logSpy).not.toHaveBeenCalled(); }); + + it("does not print when up-to-date", async () => { + vi.mocked(getLatestVersion).mockResolvedValue({ + version: CLI_VERSION, + } as any); + vi.mocked(getUserAgent).mockReturnValue("npm"); + vi.mocked(detectInstallerByPath).mockResolvedValue(undefined); + + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + await checkForUpdates(); + + expect(logSpy).not.toHaveBeenCalled(); + // but state still written + const state = JSON.parse(memfs[STATE_PATH]!); + expect(state.lastUpdateCheck).toBe(new Date().toUTCString()); + }); + + it("does not print when no manager detected at all", async () => { + vi.mocked(getLatestVersion).mockResolvedValue({ version: "2.0.0" } as any); + vi.mocked(detectInstallerByPath).mockResolvedValue(undefined); + vi.mocked(getUserAgent).mockReturnValue(null); + + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + await checkForUpdates(); + + expect(logSpy).not.toHaveBeenCalled(); + // state still written + const state = JSON.parse(memfs[STATE_PATH]!); + expect(state.lastUpdateCheck).toBe(new Date().toUTCString()); + }); + + it("renders a box when a newer version exists and no global installer", async () => { + // old timestamp + const old = new Date("2000-01-01T00:00:00Z").toUTCString(); + memfs[STATE_PATH] = JSON.stringify({ lastUpdateCheck: old }); + + vi.mocked(getLatestVersion).mockResolvedValue({ version: "2.0.0" } as any); + vi.mocked(detectInstallerByPath).mockResolvedValue(undefined); + vi.mocked(getUserAgent).mockReturnValue("bun"); + + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + await checkForUpdates(); + + expect(logSpy).toHaveBeenCalledOnce(); + const output = logSpy.mock.calls[0]![0] as string; + expect(output).toContain("bun add -g"); + expect(output).to.matchSnapshot(); + // state updated + const state = JSON.parse(memfs[STATE_PATH]!); + expect(state.lastUpdateCheck).toBe(new Date().toUTCString()); + }); }); diff --git a/codex-cli/tests/package-manager-detector.test.ts b/codex-cli/tests/package-manager-detector.test.ts new file mode 100644 index 00000000..1e80ea58 --- /dev/null +++ b/codex-cli/tests/package-manager-detector.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; +import which from "which"; +import { detectInstallerByPath } from "../src/utils/package-manager-detector"; +import { execFileSync } from "node:child_process"; + +vi.mock("which", () => ({ + default: { sync: vi.fn() }, +})); +vi.mock("node:child_process", () => ({ execFileSync: vi.fn() })); + +describe("detectInstallerByPath()", () => { + const originalArgv = process.argv; + const fakeBinDirs = { + // `npm prefix -g` returns the global “prefix” (we’ll add `/bin` when detecting) + npm: "/usr/local", + pnpm: "/home/user/.local/share/pnpm/bin", + bun: "/Users/test/.bun/bin", + } as const; + + beforeEach(() => { + vi.resetAllMocks(); + // Pretend each manager binary is on PATH: + vi.mocked(which.sync).mockImplementation(() => "/fake/path"); + + vi.mocked(execFileSync).mockImplementation( + ( + cmd: string, + _args: ReadonlyArray = [], + _options: unknown, + ): string => { + return fakeBinDirs[cmd as keyof typeof fakeBinDirs]; + }, + ); + }); + + afterEach(() => { + // Restore the real argv so tests don’t leak + process.argv = originalArgv; + }); + + it.each(Object.entries(fakeBinDirs))( + "detects %s when invoked from its global-bin", + async (manager, binDir) => { + // Simulate the shim living under that binDir + process.argv = + manager === "npm" + ? [process.argv[0]!, `${binDir}/bin/my-cli`] + : [process.argv[0]!, `${binDir}/my-cli`]; + const detected = await detectInstallerByPath(); + expect(detected).toBe(manager); + }, + ); + + it("returns undefined if argv[1] is missing", async () => { + process.argv = [process.argv[0]!]; + expect(await detectInstallerByPath()).toBeUndefined(); + expect(execFileSync).not.toHaveBeenCalled(); + }); + + it("returns undefined if shim isn't in any manager's bin", async () => { + // stub execFileSync to some other dirs + vi.mocked(execFileSync).mockImplementation(() => "/some/other/dir"); + process.argv = [process.argv[0]!, "/home/user/.node_modules/.bin/my-cli"]; + expect(await detectInstallerByPath()).toBeUndefined(); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1539652c..69491efb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,6 +43,9 @@ importers: fast-deep-equal: specifier: ^3.1.3 version: 3.1.3 + fast-npm-meta: + specifier: ^0.4.2 + version: 0.4.2 figures: specifier: ^6.1.0 version: 6.1.0 @@ -70,6 +73,9 @@ importers: openai: specifier: ^4.95.1 version: 4.95.1(ws@8.18.1)(zod@3.24.3) + package-manager-detector: + specifier: ^1.2.0 + version: 1.2.0 react: specifier: ^18.2.0 version: 18.3.1 @@ -104,6 +110,9 @@ importers: '@types/react': specifier: ^18.0.32 version: 18.3.20 + '@types/semver': + specifier: ^7.7.0 + version: 7.7.0 '@types/shell-quote': specifier: ^1.7.5 version: 1.7.5 @@ -146,6 +155,9 @@ importers: punycode: specifier: ^2.3.1 version: 2.3.1 + semver: + specifier: ^7.7.1 + version: 7.7.1 ts-node: specifier: ^10.9.1 version: 10.9.2(@types/node@22.14.1)(typescript@5.8.3) @@ -548,6 +560,9 @@ packages: '@types/react@18.3.20': resolution: {integrity: sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==} + '@types/semver@7.7.0': + resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} + '@types/shell-quote@1.7.5': resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==} @@ -1168,6 +1183,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-npm-meta@0.4.2: + resolution: {integrity: sha512-BDN/yv8MN3fjh504wa7/niZojPtf/brWBsLKlw7Fv+Xh8Df+6ZEAFpp3zaal4etgDxxav1CuzKX5H0YVM9urEQ==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -1826,6 +1844,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + package-manager-detector@1.2.0: + resolution: {integrity: sha512-PutJepsOtsqVfUsxCzgTTpyXmiAgvKptIgY4th5eq5UXXFhj5PxfQ9hnGkypMeovpAvVshFRItoFHYO18TCOqA==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2757,6 +2778,8 @@ snapshots: '@types/prop-types': 15.7.14 csstype: 3.1.3 + '@types/semver@7.7.0': {} + '@types/shell-quote@1.7.5': {} '@types/which@3.0.4': {} @@ -3558,6 +3581,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-npm-meta@0.4.2: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -4243,6 +4268,8 @@ snapshots: dependencies: p-limit: 3.1.0 + package-manager-detector@1.2.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0