mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
refactor vulkan detection
This commit is contained in:
parent
f3b85efd56
commit
ec1a28cc03
5 changed files with 209 additions and 205 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "gerbil",
|
"name": "gerbil",
|
||||||
"productName": "Gerbil",
|
"productName": "Gerbil",
|
||||||
"version": "1.6.0-beta-1",
|
"version": "1.6.0",
|
||||||
"description": "Run Large Language Models locally",
|
"description": "Run Large Language Models locally",
|
||||||
"main": "out/main/index.js",
|
"main": "out/main/index.js",
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
"@types/node": "^24.5.2",
|
"@types/node": "^24.5.2",
|
||||||
"@types/react": "^19.1.13",
|
"@types/react": "^19.1.14",
|
||||||
"@types/react-dom": "^19.1.9",
|
"@types/react-dom": "^19.1.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
||||||
"@typescript-eslint/parser": "^8.44.1",
|
"@typescript-eslint/parser": "^8.44.1",
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,11 @@
|
||||||
import {
|
import {
|
||||||
cpu as siCpu,
|
cpu as siCpu,
|
||||||
cpuFlags,
|
cpuFlags,
|
||||||
graphics as siGraphics,
|
|
||||||
mem as siMem,
|
mem as siMem,
|
||||||
memLayout as siMemLayout,
|
memLayout as siMemLayout,
|
||||||
} from 'systeminformation';
|
} from 'systeminformation';
|
||||||
import { safeExecute } from '@/utils/node/logging';
|
import { safeExecute } from '@/utils/node/logging';
|
||||||
import { getGPUData, isDiscreteBusAddress } from '@/utils/node/gpu';
|
import { getGPUData, detectGPUViaSI } from '@/utils/node/gpu';
|
||||||
import type {
|
import type {
|
||||||
CPUCapabilities,
|
CPUCapabilities,
|
||||||
GPUCapabilities,
|
GPUCapabilities,
|
||||||
|
|
@ -17,6 +16,7 @@ import type {
|
||||||
import { execa } from 'execa';
|
import { execa } from 'execa';
|
||||||
import { formatDeviceName } from '@/utils/format';
|
import { formatDeviceName } from '@/utils/format';
|
||||||
import { platform } from 'process';
|
import { platform } from 'process';
|
||||||
|
import { getVulkanInfo, detectLinuxGPUViaVulkan } from '@/utils/node/vulkan';
|
||||||
|
|
||||||
const COMMON_EXEC_OPTIONS = {
|
const COMMON_EXEC_OPTIONS = {
|
||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
|
|
@ -27,16 +27,6 @@ let cpuCapabilitiesCache: CPUCapabilities | null = null;
|
||||||
let basicGPUInfoCache: BasicGPUInfo | null = null;
|
let basicGPUInfoCache: BasicGPUInfo | null = null;
|
||||||
let gpuCapabilitiesCache: GPUCapabilities | null = null;
|
let gpuCapabilitiesCache: GPUCapabilities | null = null;
|
||||||
let gpuMemoryInfoCache: GPUMemoryInfo[] | null = null;
|
let gpuMemoryInfoCache: GPUMemoryInfo[] | null = null;
|
||||||
let vulkanInfoCache: {
|
|
||||||
discreteGPUs: {
|
|
||||||
deviceName: string;
|
|
||||||
driverInfo?: string;
|
|
||||||
apiVersion?: string;
|
|
||||||
hasAMD: boolean;
|
|
||||||
hasNVIDIA: boolean;
|
|
||||||
}[];
|
|
||||||
apiVersion?: string;
|
|
||||||
} | null = null;
|
|
||||||
|
|
||||||
export async function detectCPU() {
|
export async function detectCPU() {
|
||||||
if (cpuCapabilitiesCache) {
|
if (cpuCapabilitiesCache) {
|
||||||
|
|
@ -99,192 +89,6 @@ export async function detectGPU() {
|
||||||
return basicGPUInfoCache;
|
return basicGPUInfoCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
async function getVulkanInfo() {
|
|
||||||
if (vulkanInfoCache) {
|
|
||||||
return vulkanInfoCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { stdout } = await execa(
|
|
||||||
'vulkaninfo',
|
|
||||||
['--summary'],
|
|
||||||
COMMON_EXEC_OPTIONS
|
|
||||||
);
|
|
||||||
|
|
||||||
const discreteGPUs: {
|
|
||||||
deviceName: string;
|
|
||||||
driverInfo?: string;
|
|
||||||
apiVersion?: string;
|
|
||||||
hasAMD: boolean;
|
|
||||||
hasNVIDIA: boolean;
|
|
||||||
}[] = [];
|
|
||||||
let globalApiVersion: string | undefined;
|
|
||||||
|
|
||||||
if (stdout.trim()) {
|
|
||||||
const lines = stdout.split('\n');
|
|
||||||
let foundDiscreteGPU = false;
|
|
||||||
let currentGPU: (typeof discreteGPUs)[0] | null = null;
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (
|
|
||||||
!globalApiVersion &&
|
|
||||||
line.includes('apiVersion') &&
|
|
||||||
line.includes('=')
|
|
||||||
) {
|
|
||||||
const match = line.match(/=\s*(\d+\.\d+(?:\.\d+)?)/);
|
|
||||||
if (match) {
|
|
||||||
globalApiVersion = match[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.includes('PHYSICAL_DEVICE_TYPE_DISCRETE_GPU')) {
|
|
||||||
foundDiscreteGPU = true;
|
|
||||||
currentGPU = {
|
|
||||||
deviceName: '',
|
|
||||||
hasAMD: false,
|
|
||||||
hasNVIDIA: false,
|
|
||||||
};
|
|
||||||
} else if (
|
|
||||||
foundDiscreteGPU &&
|
|
||||||
currentGPU &&
|
|
||||||
line.includes('deviceName') &&
|
|
||||||
line.includes('=')
|
|
||||||
) {
|
|
||||||
const parts = line.split('=');
|
|
||||||
if (parts.length >= 2) {
|
|
||||||
const name = parts[1]?.trim();
|
|
||||||
if (name) {
|
|
||||||
currentGPU.deviceName = name;
|
|
||||||
currentGPU.hasAMD =
|
|
||||||
name.toLowerCase().includes('amd') ||
|
|
||||||
name.toLowerCase().includes('radeon');
|
|
||||||
currentGPU.hasNVIDIA =
|
|
||||||
name.toLowerCase().includes('nvidia') ||
|
|
||||||
name.toLowerCase().includes('geforce') ||
|
|
||||||
name.toLowerCase().includes('rtx') ||
|
|
||||||
name.toLowerCase().includes('gtx');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
foundDiscreteGPU &&
|
|
||||||
currentGPU &&
|
|
||||||
line.includes('driverInfo')
|
|
||||||
) {
|
|
||||||
const mesaMatch = line.match(/Mesa\s+(.+)/);
|
|
||||||
if (mesaMatch) {
|
|
||||||
currentGPU.driverInfo = `Mesa ${mesaMatch[1].trim()}`;
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
foundDiscreteGPU &&
|
|
||||||
currentGPU &&
|
|
||||||
line.includes('apiVersion') &&
|
|
||||||
line.includes('=')
|
|
||||||
) {
|
|
||||||
const match = line.match(/=\s*(\d+\.\d+(?:\.\d+)?)/);
|
|
||||||
if (match) {
|
|
||||||
currentGPU.apiVersion = match[1];
|
|
||||||
if (!globalApiVersion) {
|
|
||||||
globalApiVersion = match[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (foundDiscreteGPU && currentGPU && line.includes('GPU')) {
|
|
||||||
if (currentGPU.deviceName) {
|
|
||||||
discreteGPUs.push(currentGPU);
|
|
||||||
}
|
|
||||||
foundDiscreteGPU = false;
|
|
||||||
currentGPU = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundDiscreteGPU && currentGPU && currentGPU.deviceName) {
|
|
||||||
discreteGPUs.push(currentGPU);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vulkanInfoCache = {
|
|
||||||
discreteGPUs,
|
|
||||||
apiVersion: globalApiVersion,
|
|
||||||
};
|
|
||||||
|
|
||||||
return vulkanInfoCache;
|
|
||||||
} catch {
|
|
||||||
vulkanInfoCache = {
|
|
||||||
discreteGPUs: [],
|
|
||||||
};
|
|
||||||
return vulkanInfoCache;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function detectLinuxGPUViaVulkan() {
|
|
||||||
try {
|
|
||||||
const vulkanInfo = await getVulkanInfo();
|
|
||||||
|
|
||||||
let hasAMD = false;
|
|
||||||
let hasNVIDIA = false;
|
|
||||||
const gpuInfo: string[] = [];
|
|
||||||
|
|
||||||
for (const gpu of vulkanInfo.discreteGPUs) {
|
|
||||||
gpuInfo.push(formatDeviceName(gpu.deviceName));
|
|
||||||
|
|
||||||
if (gpu.hasAMD) {
|
|
||||||
hasAMD = true;
|
|
||||||
}
|
|
||||||
if (gpu.hasNVIDIA) {
|
|
||||||
hasNVIDIA = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasAMD,
|
|
||||||
hasNVIDIA,
|
|
||||||
gpuInfo: gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'],
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return {
|
|
||||||
hasAMD: false,
|
|
||||||
hasNVIDIA: false,
|
|
||||||
gpuInfo: ['GPU detection failed'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function detectGPUViaSI() {
|
|
||||||
const graphics = await siGraphics();
|
|
||||||
|
|
||||||
let hasAMD = false;
|
|
||||||
let hasNVIDIA = false;
|
|
||||||
const gpuInfo: string[] = [];
|
|
||||||
|
|
||||||
const discreteControllers = graphics.controllers.filter(
|
|
||||||
(controller) =>
|
|
||||||
controller.busAddress && isDiscreteBusAddress(controller.busAddress)
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const controller of discreteControllers) {
|
|
||||||
if (
|
|
||||||
controller.vendor?.toLowerCase().includes('amd') ||
|
|
||||||
controller.vendor?.toLowerCase().includes('ati')
|
|
||||||
) {
|
|
||||||
hasAMD = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controller.vendor?.toLowerCase().includes('nvidia')) {
|
|
||||||
hasNVIDIA = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controller.model) {
|
|
||||||
gpuInfo.push(controller.model);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasAMD,
|
|
||||||
hasNVIDIA,
|
|
||||||
gpuInfo: gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function detectGPUCapabilities() {
|
export async function detectGPUCapabilities() {
|
||||||
// WARNING: we're not worrying about the users that update their system
|
// WARNING: we're not worrying about the users that update their system
|
||||||
// during runtime and not restart. Should we be though?
|
// during runtime and not restart. Should we be though?
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { readFile, readdir } from 'fs/promises';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { platform } from 'process';
|
import { platform } from 'process';
|
||||||
import { execa } from 'execa';
|
import { execa } from 'execa';
|
||||||
|
import { graphics as siGraphics } from 'systeminformation';
|
||||||
|
|
||||||
interface CachedGPUInfo {
|
interface CachedGPUInfo {
|
||||||
devicePath: string;
|
devicePath: string;
|
||||||
|
|
@ -241,3 +242,39 @@ export function isDiscreteBusAddress(busAddress?: string) {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function detectGPUViaSI() {
|
||||||
|
const graphics = await siGraphics();
|
||||||
|
|
||||||
|
let hasAMD = false;
|
||||||
|
let hasNVIDIA = false;
|
||||||
|
const gpuInfo: string[] = [];
|
||||||
|
|
||||||
|
const discreteControllers = graphics.controllers.filter(
|
||||||
|
(controller) =>
|
||||||
|
controller.busAddress && isDiscreteBusAddress(controller.busAddress)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const controller of discreteControllers) {
|
||||||
|
if (
|
||||||
|
controller.vendor?.toLowerCase().includes('amd') ||
|
||||||
|
controller.vendor?.toLowerCase().includes('ati')
|
||||||
|
) {
|
||||||
|
hasAMD = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller.vendor?.toLowerCase().includes('nvidia')) {
|
||||||
|
hasNVIDIA = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller.model) {
|
||||||
|
gpuInfo.push(controller.model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasAMD,
|
||||||
|
hasNVIDIA,
|
||||||
|
gpuInfo: gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
163
src/utils/node/vulkan.ts
Normal file
163
src/utils/node/vulkan.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
import { execa } from 'execa';
|
||||||
|
|
||||||
|
import { formatDeviceName } from '@/utils/format';
|
||||||
|
|
||||||
|
let vulkanInfoCache: {
|
||||||
|
discreteGPUs: {
|
||||||
|
deviceName: string;
|
||||||
|
driverInfo?: string;
|
||||||
|
apiVersion?: string;
|
||||||
|
hasAMD: boolean;
|
||||||
|
hasNVIDIA: boolean;
|
||||||
|
}[];
|
||||||
|
apiVersion?: string;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
export async function getVulkanInfo() {
|
||||||
|
if (vulkanInfoCache) {
|
||||||
|
return vulkanInfoCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout } = await execa('vulkaninfo', ['--summary'], {
|
||||||
|
timeout: 3000,
|
||||||
|
reject: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const discreteGPUs: {
|
||||||
|
deviceName: string;
|
||||||
|
driverInfo?: string;
|
||||||
|
apiVersion?: string;
|
||||||
|
hasAMD: boolean;
|
||||||
|
hasNVIDIA: boolean;
|
||||||
|
}[] = [];
|
||||||
|
let globalApiVersion: string | undefined;
|
||||||
|
|
||||||
|
if (stdout.trim()) {
|
||||||
|
const lines = stdout.split('\n');
|
||||||
|
let foundDiscreteGPU = false;
|
||||||
|
let currentGPU: (typeof discreteGPUs)[0] | null = null;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (
|
||||||
|
!globalApiVersion &&
|
||||||
|
line.includes('apiVersion') &&
|
||||||
|
line.includes('=')
|
||||||
|
) {
|
||||||
|
const match = line.match(/=\s*(\d+\.\d+(?:\.\d+)?)/);
|
||||||
|
if (match) {
|
||||||
|
globalApiVersion = match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.includes('PHYSICAL_DEVICE_TYPE_DISCRETE_GPU')) {
|
||||||
|
foundDiscreteGPU = true;
|
||||||
|
currentGPU = {
|
||||||
|
deviceName: '',
|
||||||
|
hasAMD: false,
|
||||||
|
hasNVIDIA: false,
|
||||||
|
};
|
||||||
|
} else if (
|
||||||
|
foundDiscreteGPU &&
|
||||||
|
currentGPU &&
|
||||||
|
line.includes('deviceName') &&
|
||||||
|
line.includes('=')
|
||||||
|
) {
|
||||||
|
const parts = line.split('=');
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
const name = parts[1]?.trim();
|
||||||
|
if (name) {
|
||||||
|
currentGPU.deviceName = name;
|
||||||
|
currentGPU.hasAMD =
|
||||||
|
name.toLowerCase().includes('amd') ||
|
||||||
|
name.toLowerCase().includes('radeon');
|
||||||
|
currentGPU.hasNVIDIA =
|
||||||
|
name.toLowerCase().includes('nvidia') ||
|
||||||
|
name.toLowerCase().includes('geforce') ||
|
||||||
|
name.toLowerCase().includes('rtx') ||
|
||||||
|
name.toLowerCase().includes('gtx');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
foundDiscreteGPU &&
|
||||||
|
currentGPU &&
|
||||||
|
line.includes('driverInfo')
|
||||||
|
) {
|
||||||
|
const mesaMatch = line.match(/Mesa\s+(.+)/);
|
||||||
|
if (mesaMatch) {
|
||||||
|
currentGPU.driverInfo = `Mesa ${mesaMatch[1].trim()}`;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
foundDiscreteGPU &&
|
||||||
|
currentGPU &&
|
||||||
|
line.includes('apiVersion') &&
|
||||||
|
line.includes('=')
|
||||||
|
) {
|
||||||
|
const match = line.match(/=\s*(\d+\.\d+(?:\.\d+)?)/);
|
||||||
|
if (match) {
|
||||||
|
currentGPU.apiVersion = match[1];
|
||||||
|
if (!globalApiVersion) {
|
||||||
|
globalApiVersion = match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (foundDiscreteGPU && currentGPU && line.includes('GPU')) {
|
||||||
|
if (currentGPU.deviceName) {
|
||||||
|
discreteGPUs.push(currentGPU);
|
||||||
|
}
|
||||||
|
foundDiscreteGPU = false;
|
||||||
|
currentGPU = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundDiscreteGPU && currentGPU && currentGPU.deviceName) {
|
||||||
|
discreteGPUs.push(currentGPU);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vulkanInfoCache = {
|
||||||
|
discreteGPUs,
|
||||||
|
apiVersion: globalApiVersion,
|
||||||
|
};
|
||||||
|
|
||||||
|
return vulkanInfoCache;
|
||||||
|
} catch {
|
||||||
|
vulkanInfoCache = {
|
||||||
|
discreteGPUs: [],
|
||||||
|
};
|
||||||
|
return vulkanInfoCache;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function detectLinuxGPUViaVulkan() {
|
||||||
|
try {
|
||||||
|
const vulkanInfo = await getVulkanInfo();
|
||||||
|
|
||||||
|
let hasAMD = false;
|
||||||
|
let hasNVIDIA = false;
|
||||||
|
const gpuInfo: string[] = [];
|
||||||
|
|
||||||
|
for (const gpu of vulkanInfo.discreteGPUs) {
|
||||||
|
gpuInfo.push(formatDeviceName(gpu.deviceName));
|
||||||
|
|
||||||
|
if (gpu.hasAMD) {
|
||||||
|
hasAMD = true;
|
||||||
|
}
|
||||||
|
if (gpu.hasNVIDIA) {
|
||||||
|
hasNVIDIA = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasAMD,
|
||||||
|
hasNVIDIA,
|
||||||
|
gpuInfo: gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'],
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
hasAMD: false,
|
||||||
|
hasNVIDIA: false,
|
||||||
|
gpuInfo: ['GPU detection failed'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
10
yarn.lock
10
yarn.lock
|
|
@ -1456,12 +1456,12 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/react@npm:^19.1.13":
|
"@types/react@npm:^19.1.14":
|
||||||
version: 19.1.13
|
version: 19.1.14
|
||||||
resolution: "@types/react@npm:19.1.13"
|
resolution: "@types/react@npm:19.1.14"
|
||||||
dependencies:
|
dependencies:
|
||||||
csstype: "npm:^3.0.2"
|
csstype: "npm:^3.0.2"
|
||||||
checksum: 10c0/75e35b54883f5ed07d3b5cb16a4711b6dbb7ec6b74301bcb9bfa697c9d9fff022ec508e1719e7b2c69e2e8b042faac1125be7717b5e5e084f816a2c88e136920
|
checksum: 10c0/0d1629e065413be79c949f011845836e7f515c8f97dbfef057719be2f312b404405d2d064cb28e03363c62c153c100e84268c09dd9aeda488858d45033b520f0
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
@ -3866,7 +3866,7 @@ __metadata:
|
||||||
"@mantine/core": "npm:8.3.1"
|
"@mantine/core": "npm:8.3.1"
|
||||||
"@mantine/hooks": "npm:8.3.1"
|
"@mantine/hooks": "npm:8.3.1"
|
||||||
"@types/node": "npm:^24.5.2"
|
"@types/node": "npm:^24.5.2"
|
||||||
"@types/react": "npm:^19.1.13"
|
"@types/react": "npm:^19.1.14"
|
||||||
"@types/react-dom": "npm:^19.1.9"
|
"@types/react-dom": "npm:^19.1.9"
|
||||||
"@types/yauzl": "npm:^2.10.3"
|
"@types/yauzl": "npm:^2.10.3"
|
||||||
"@typescript-eslint/eslint-plugin": "npm:^8.44.1"
|
"@typescript-eslint/eslint-plugin": "npm:^8.44.1"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue