mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
fixing regressions from the previous commit
This commit is contained in:
parent
e1b078b9d9
commit
329e191a49
8 changed files with 375 additions and 364 deletions
|
|
@ -52,11 +52,11 @@ const config = [
|
||||||
plugins: {
|
plugins: {
|
||||||
'@typescript-eslint': tseslint,
|
'@typescript-eslint': tseslint,
|
||||||
'react-hooks': reactHooks,
|
'react-hooks': reactHooks,
|
||||||
react: react,
|
react,
|
||||||
import: importPlugin,
|
sonarjs,
|
||||||
sonarjs: sonarjs,
|
|
||||||
'no-comments': noComments,
|
'no-comments': noComments,
|
||||||
promise: promise,
|
import: importPlugin,
|
||||||
|
promise,
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
|
|
@ -135,6 +135,18 @@ const config = [
|
||||||
message:
|
message:
|
||||||
'Direct setting of currentKoboldBinary config is forbidden. Use window.electronAPI.kobold.setCurrentVersion() instead.',
|
'Direct setting of currentKoboldBinary config is forbidden. Use window.electronAPI.kobold.setCurrentVersion() instead.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
selector:
|
||||||
|
'ExpressionStatement[expression.type="AwaitExpression"]:has(CallExpression[callee.name="ensureDir"]) + ExpressionStatement[expression.type="AwaitExpression"]:has(CallExpression[callee.name="ensureDir"])',
|
||||||
|
message:
|
||||||
|
'Sequential ensureDir() calls detected. These can run in parallel using Promise.all().',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector:
|
||||||
|
'ExpressionStatement[expression.type="AwaitExpression"]:has(CallExpression[callee.object.name="fs"][callee.property.name=/^(unlink|rmdir|mkdir|writeFile)$/]) + ExpressionStatement[expression.type="AwaitExpression"]:has(CallExpression[callee.object.name="fs"][callee.property.name=/^(unlink|rmdir|mkdir|writeFile)$/])',
|
||||||
|
message:
|
||||||
|
'Sequential file system operations detected. Independent operations can run in parallel using Promise.all().',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
'import/no-default-export': 'error',
|
'import/no-default-export': 'error',
|
||||||
|
|
@ -156,22 +168,25 @@ const config = [
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
'@typescript-eslint/return-await': ['error', 'never'],
|
'@typescript-eslint/return-await': ['error', 'never'],
|
||||||
|
'@typescript-eslint/prefer-promise-reject-errors': 'error',
|
||||||
|
|
||||||
'sonarjs/cognitive-complexity': ['warn', 25],
|
'sonarjs/cognitive-complexity': ['warn', 25],
|
||||||
|
|
||||||
// Promise rules to prevent sequential awaits (no warnings, only errors)
|
// Promise rules to prevent sequential awaits (smarter detection)
|
||||||
'promise/prefer-await-to-then': 'error', // Enforce async/await
|
'promise/prefer-await-to-then': 'error',
|
||||||
'promise/prefer-await-to-callbacks': 'off', // Too aggressive for Electron APIs
|
'promise/prefer-await-to-callbacks': 'off',
|
||||||
'promise/no-nesting': 'error', // No nested promises
|
'promise/no-nesting': 'error',
|
||||||
'promise/no-promise-in-callback': 'off', // Common in Electron
|
'promise/no-promise-in-callback': 'off',
|
||||||
'promise/no-callback-in-promise': 'off', // Common in Electron
|
'promise/no-callback-in-promise': 'off',
|
||||||
'promise/avoid-new': 'off', // Sometimes needed for wrapping APIs
|
'promise/avoid-new': 'off',
|
||||||
'promise/no-new-statics': 'error',
|
'promise/no-new-statics': 'error',
|
||||||
'promise/no-return-wrap': 'error',
|
'promise/no-return-wrap': 'error',
|
||||||
'promise/param-names': 'error',
|
'promise/param-names': 'error',
|
||||||
'promise/catch-or-return': 'off', // Too strict for some patterns
|
'promise/catch-or-return': 'off',
|
||||||
'promise/no-native': 'off',
|
'promise/no-native': 'off',
|
||||||
|
|
||||||
|
// Node.js specific async rules
|
||||||
|
|
||||||
'no-comments/disallowComments': 'error',
|
'no-comments/disallowComments': 'error',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "gerbil",
|
"name": "gerbil",
|
||||||
"productName": "Gerbil",
|
"productName": "Gerbil",
|
||||||
"version": "1.2.3",
|
"version": "1.2.4",
|
||||||
"description": "Run Large Language Models locally",
|
"description": "Run Large Language Models locally",
|
||||||
"main": "out/main/index.js",
|
"main": "out/main/index.js",
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
|
|
@ -66,6 +66,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "^8.3.1",
|
"@mantine/core": "^8.3.1",
|
||||||
"@mantine/hooks": "^8.3.1",
|
"@mantine/hooks": "^8.3.1",
|
||||||
|
"@types/yauzl": "^2.10.3",
|
||||||
"axios": "^1.12.1",
|
"axios": "^1.12.1",
|
||||||
"execa": "^9.6.0",
|
"execa": "^9.6.0",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.544.0",
|
||||||
|
|
@ -75,6 +76,7 @@
|
||||||
"systeminformation": "^5.27.9",
|
"systeminformation": "^5.27.9",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-daily-rotate-file": "^5.0.0",
|
"winston-daily-rotate-file": "^5.0.0",
|
||||||
|
"yauzl": "^3.2.0",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ export const GITHUB_API = {
|
||||||
KOBOLDCPP_REPO: 'LostRuins/koboldcpp',
|
KOBOLDCPP_REPO: 'LostRuins/koboldcpp',
|
||||||
KOBOLDCPP_ROCM_REPO: 'YellowRoseCx/koboldcpp-rocm',
|
KOBOLDCPP_ROCM_REPO: 'YellowRoseCx/koboldcpp-rocm',
|
||||||
GERBIL_REPO: 'lone-cloud/gerbil',
|
GERBIL_REPO: 'lone-cloud/gerbil',
|
||||||
|
COMFYUI_REPO: 'comfyanonymous/ComfyUI',
|
||||||
get LATEST_RELEASE_URL() {
|
get LATEST_RELEASE_URL() {
|
||||||
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases/latest`;
|
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases/latest`;
|
||||||
},
|
},
|
||||||
|
|
@ -68,6 +69,9 @@ export const GITHUB_API = {
|
||||||
get ROCM_LATEST_RELEASE_URL() {
|
get ROCM_LATEST_RELEASE_URL() {
|
||||||
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_ROCM_REPO}/releases/latest`;
|
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_ROCM_REPO}/releases/latest`;
|
||||||
},
|
},
|
||||||
|
get COMFYUI_DOWNLOAD_URL() {
|
||||||
|
return `https://github.com/${this.COMFYUI_REPO}/archive/refs/heads/master.zip`;
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ASSET_SUFFIXES = {
|
export const ASSET_SUFFIXES = {
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { homedir } from 'os';
|
|
||||||
import { access } from 'fs/promises';
|
import { access } from 'fs/promises';
|
||||||
import type { ChildProcess } from 'child_process';
|
import type { ChildProcess } from 'child_process';
|
||||||
|
import yauzl from 'yauzl';
|
||||||
|
|
||||||
import { logError } from './logging';
|
import { logError } from './logging';
|
||||||
import { sendKoboldOutput } from './window';
|
import { sendKoboldOutput } from './window';
|
||||||
import { getInstallDir } from './config';
|
import { getInstallDir } from './config';
|
||||||
import { COMFYUI, SERVER_READY_SIGNALS } from '@/constants';
|
import { COMFYUI, SERVER_READY_SIGNALS, GITHUB_API } from '@/constants';
|
||||||
import { terminateProcess } from '@/utils/node/process';
|
import { terminateProcess } from '@/utils/node/process';
|
||||||
import { parseKoboldConfig } from '@/utils/node/kobold';
|
import { parseKoboldConfig } from '@/utils/node/kobold';
|
||||||
import { getAppVersion, ensureDir } from '@/utils/node/fs';
|
import { getAppVersion, ensureDir } from '@/utils/node/fs';
|
||||||
import { getGPUData } from '@/utils/node/gpu';
|
import { getGPUData } from '@/utils/node/gpu';
|
||||||
|
import { getUvEnvironment } from './dependencies';
|
||||||
|
|
||||||
let comfyUIProcess: ChildProcess | null = null;
|
let comfyUIProcess: ChildProcess | null = null;
|
||||||
|
|
||||||
|
|
@ -23,39 +24,6 @@ process.on('SIGTERM', () => {
|
||||||
void cleanup();
|
void cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getUvEnvironment() {
|
|
||||||
const env = { ...process.env };
|
|
||||||
|
|
||||||
const uvPaths = [
|
|
||||||
join(homedir(), '.cargo', 'bin'),
|
|
||||||
join(homedir(), '.local', 'bin'),
|
|
||||||
];
|
|
||||||
|
|
||||||
const existingPaths: string[] = [];
|
|
||||||
for (const path of uvPaths) {
|
|
||||||
try {
|
|
||||||
await access(path);
|
|
||||||
existingPaths.push(path);
|
|
||||||
} catch {
|
|
||||||
void 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingPaths.length > 0) {
|
|
||||||
const pathSeparator = process.platform === 'win32' ? ';' : ':';
|
|
||||||
env.PATH = `${existingPaths.join(pathSeparator)}${pathSeparator}${env.PATH}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
env.PYTHONIOENCODING = 'utf-8';
|
|
||||||
env.PYTHONLEGACYWINDOWSSTDIO = '1';
|
|
||||||
env.PYTHONUTF8 = '1';
|
|
||||||
env.CHCP = '65001';
|
|
||||||
}
|
|
||||||
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getPyTorchInstallArgs(pythonPath: string) {
|
async function getPyTorchInstallArgs(pythonPath: string) {
|
||||||
const args = [
|
const args = [
|
||||||
'pip',
|
'pip',
|
||||||
|
|
@ -110,8 +78,6 @@ async function ensureComfyUIInstalled() {
|
||||||
sendKoboldOutput('ComfyUI not found, installing via uv...');
|
sendKoboldOutput('ComfyUI not found, installing via uv...');
|
||||||
|
|
||||||
const env = await getUvEnvironment();
|
const env = await getUvEnvironment();
|
||||||
env.PYTHONIOENCODING = 'utf-8';
|
|
||||||
env.PYTHONUNBUFFERED = '1';
|
|
||||||
|
|
||||||
sendKoboldOutput(
|
sendKoboldOutput(
|
||||||
'Creating virtual environment and installing ComfyUI dependencies...'
|
'Creating virtual environment and installing ComfyUI dependencies...'
|
||||||
|
|
@ -134,9 +100,7 @@ async function ensureComfyUIInstalled() {
|
||||||
'Virtual environment created, downloading ComfyUI...'
|
'Virtual environment created, downloading ComfyUI...'
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(GITHUB_API.COMFYUI_DOWNLOAD_URL);
|
||||||
'https://github.com/comfyanonymous/ComfyUI/archive/refs/heads/master.zip'
|
|
||||||
);
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to download ComfyUI: ${response.statusText}`
|
`Failed to download ComfyUI: ${response.statusText}`
|
||||||
|
|
@ -174,81 +138,132 @@ async function ensureComfyUIInstalled() {
|
||||||
`ComfyUI downloaded (${Math.round(zipStats.size / 1024 / 1024)}MB), extracting...`
|
`ComfyUI downloaded (${Math.round(zipStats.size / 1024 / 1024)}MB), extracting...`
|
||||||
);
|
);
|
||||||
|
|
||||||
const extractProcess = spawn(
|
try {
|
||||||
'unzip',
|
await new Promise<void>((resolve, reject) => {
|
||||||
['-o', zipPath, '-d', comfyUIWorkspace],
|
yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
|
||||||
{
|
if (err) {
|
||||||
stdio: 'pipe',
|
reject(err);
|
||||||
env,
|
return;
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
extractProcess.on('exit', async (extractCode) => {
|
|
||||||
if (extractCode === 0) {
|
|
||||||
try {
|
|
||||||
const extractedDir = path.join(
|
|
||||||
comfyUIWorkspace,
|
|
||||||
'ComfyUI-master'
|
|
||||||
);
|
|
||||||
const files = await fs.readdir(extractedDir);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const srcPath = path.join(extractedDir, file);
|
|
||||||
const destPath = path.join(comfyUIWorkspace, file);
|
|
||||||
await fs.rename(srcPath, destPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.rmdir(extractedDir);
|
if (!zipfile) {
|
||||||
await fs.unlink(zipPath);
|
reject(new Error('Failed to open zip file'));
|
||||||
|
return;
|
||||||
sendKoboldOutput(
|
|
||||||
'ComfyUI extracted, installing dependencies...'
|
|
||||||
);
|
|
||||||
|
|
||||||
const requirementsPath = join(
|
|
||||||
comfyUIWorkspace,
|
|
||||||
'requirements.txt'
|
|
||||||
);
|
|
||||||
const requirementsExists = await fs
|
|
||||||
.access(requirementsPath)
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false);
|
|
||||||
|
|
||||||
if (!requirementsExists) {
|
|
||||||
throw new Error(
|
|
||||||
'requirements.txt not found in ComfyUI directory'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pythonPath = join(
|
zipfile.readEntry();
|
||||||
comfyUIWorkspace,
|
|
||||||
'.venv',
|
|
||||||
'bin',
|
|
||||||
'python'
|
|
||||||
);
|
|
||||||
const torchInstallArgs =
|
|
||||||
await getPyTorchInstallArgs(pythonPath);
|
|
||||||
|
|
||||||
const torchInstallProcess = spawn('uv', torchInstallArgs, {
|
zipfile.on('entry', (entry) => {
|
||||||
stdio: 'pipe',
|
if (/\/$/.test(entry.fileName)) {
|
||||||
env,
|
zipfile.readEntry();
|
||||||
|
} else {
|
||||||
|
zipfile.openReadStream(entry, (err, readStream) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!readStream) {
|
||||||
|
reject(new Error('Failed to create read stream'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = entry.fileName.replace(/^[^/]+\//, '');
|
||||||
|
const outputPath = path.join(
|
||||||
|
comfyUIWorkspace,
|
||||||
|
fileName
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.mkdir(path.dirname(outputPath), { recursive: true })
|
||||||
|
.then(() => {
|
||||||
|
const writeStream = createWriteStream(outputPath);
|
||||||
|
readStream.pipe(writeStream);
|
||||||
|
|
||||||
|
writeStream.on('close', () => {
|
||||||
|
zipfile.readEntry();
|
||||||
|
});
|
||||||
|
|
||||||
|
writeStream.on('error', reject);
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
torchInstallProcess.on('exit', (torchCode: number | null) => {
|
zipfile.on('end', () => {
|
||||||
if (torchCode === 0) {
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
zipfile.on('error', reject);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
await fs.unlink(zipPath);
|
||||||
|
|
||||||
|
sendKoboldOutput('ComfyUI extracted, installing dependencies...');
|
||||||
|
|
||||||
|
const requirementsPath = join(
|
||||||
|
comfyUIWorkspace,
|
||||||
|
'requirements.txt'
|
||||||
|
);
|
||||||
|
const requirementsExists = await fs
|
||||||
|
.access(requirementsPath)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
if (!requirementsExists) {
|
||||||
|
throw new Error(
|
||||||
|
'requirements.txt not found in ComfyUI directory'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pythonPath = join(
|
||||||
|
comfyUIWorkspace,
|
||||||
|
'.venv',
|
||||||
|
'bin',
|
||||||
|
'python'
|
||||||
|
);
|
||||||
|
const torchInstallArgs = await getPyTorchInstallArgs(pythonPath);
|
||||||
|
|
||||||
|
const torchInstallProcess = spawn('uv', torchInstallArgs, {
|
||||||
|
stdio: 'pipe',
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
|
torchInstallProcess.on('exit', (torchCode: number | null) => {
|
||||||
|
if (torchCode === 0) {
|
||||||
|
sendKoboldOutput(
|
||||||
|
'PyTorch with ROCm installed, installing remaining dependencies...'
|
||||||
|
);
|
||||||
|
|
||||||
|
const pipInstallProcess = spawn(
|
||||||
|
'uv',
|
||||||
|
[
|
||||||
|
'pip',
|
||||||
|
'install',
|
||||||
|
'--python',
|
||||||
|
join(comfyUIWorkspace, '.venv', 'bin', 'python'),
|
||||||
|
'-r',
|
||||||
|
requirementsPath,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
stdio: 'pipe',
|
||||||
|
env,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
pipInstallProcess.on('exit', async (pipCode) => {
|
||||||
|
if (pipCode === 0) {
|
||||||
sendKoboldOutput(
|
sendKoboldOutput(
|
||||||
'PyTorch with ROCm installed, installing remaining dependencies...'
|
'Dependencies installation completed, verifying...'
|
||||||
);
|
);
|
||||||
|
|
||||||
const pipInstallProcess = spawn(
|
const verifyProcess = spawn(
|
||||||
'uv',
|
join(comfyUIWorkspace, '.venv', 'bin', 'python'),
|
||||||
[
|
[
|
||||||
'pip',
|
'-c',
|
||||||
'install',
|
'import einops; print("einops found:", einops.__version__)',
|
||||||
'--python',
|
|
||||||
join(comfyUIWorkspace, '.venv', 'bin', 'python'),
|
|
||||||
'-r',
|
|
||||||
requirementsPath,
|
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
stdio: 'pipe',
|
stdio: 'pipe',
|
||||||
|
|
@ -256,136 +271,89 @@ async function ensureComfyUIInstalled() {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
pipInstallProcess.on('exit', async (pipCode) => {
|
verifyProcess.on('exit', (verifyCode) => {
|
||||||
if (pipCode === 0) {
|
if (verifyCode === 0) {
|
||||||
sendKoboldOutput(
|
sendKoboldOutput('ComfyUI installed successfully!');
|
||||||
'Dependencies installation completed, verifying...'
|
resolve();
|
||||||
);
|
|
||||||
|
|
||||||
const verifyProcess = spawn(
|
|
||||||
join(comfyUIWorkspace, '.venv', 'bin', 'python'),
|
|
||||||
[
|
|
||||||
'-c',
|
|
||||||
'import einops; print("einops found:", einops.__version__)',
|
|
||||||
],
|
|
||||||
{
|
|
||||||
stdio: 'pipe',
|
|
||||||
env,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
verifyProcess.on('exit', (verifyCode) => {
|
|
||||||
if (verifyCode === 0) {
|
|
||||||
sendKoboldOutput(
|
|
||||||
'ComfyUI installed successfully!'
|
|
||||||
);
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
sendKoboldOutput(
|
|
||||||
'Warning: einops verification failed, but continuing...'
|
|
||||||
);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (verifyProcess.stdout) {
|
|
||||||
verifyProcess.stdout.on('data', (data: Buffer) => {
|
|
||||||
sendKoboldOutput(
|
|
||||||
`Verify: ${data.toString('utf8')}`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (verifyProcess.stderr) {
|
|
||||||
verifyProcess.stderr.on('data', (data: Buffer) => {
|
|
||||||
sendKoboldOutput(
|
|
||||||
`Verify Error: ${data.toString('utf8')}`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
reject(
|
sendKoboldOutput(
|
||||||
new Error(
|
'Warning: einops verification failed, but continuing...'
|
||||||
`Dependency installation failed with code ${pipCode}`
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
pipInstallProcess.on('error', (error) => {
|
if (verifyProcess.stdout) {
|
||||||
reject(error);
|
verifyProcess.stdout.on('data', (data: Buffer) => {
|
||||||
});
|
sendKoboldOutput(
|
||||||
|
`Verify: ${data.toString('utf8')}`,
|
||||||
if (pipInstallProcess.stdout) {
|
true
|
||||||
pipInstallProcess.stdout.on('data', (data: Buffer) => {
|
);
|
||||||
sendKoboldOutput(data.toString('utf8'), true);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pipInstallProcess.stderr) {
|
if (verifyProcess.stderr) {
|
||||||
pipInstallProcess.stderr.on('data', (data: Buffer) => {
|
verifyProcess.stderr.on('data', (data: Buffer) => {
|
||||||
sendKoboldOutput(data.toString('utf8'), true);
|
sendKoboldOutput(
|
||||||
|
`Verify Error: ${data.toString('utf8')}`,
|
||||||
|
true
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
reject(
|
reject(
|
||||||
new Error(
|
new Error(
|
||||||
`PyTorch installation failed with code ${torchCode}`
|
`Dependency installation failed with code ${pipCode}`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
torchInstallProcess.on('error', (error) => {
|
pipInstallProcess.on('error', (error) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (torchInstallProcess.stdout) {
|
if (pipInstallProcess.stdout) {
|
||||||
torchInstallProcess.stdout.on('data', (data: Buffer) => {
|
pipInstallProcess.stdout.on('data', (data: Buffer) => {
|
||||||
sendKoboldOutput(data.toString('utf8'), true);
|
sendKoboldOutput(data.toString('utf8'), true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (torchInstallProcess.stderr) {
|
if (pipInstallProcess.stderr) {
|
||||||
torchInstallProcess.stderr.on('data', (data: Buffer) => {
|
pipInstallProcess.stderr.on('data', (data: Buffer) => {
|
||||||
sendKoboldOutput(data.toString('utf8'), true);
|
sendKoboldOutput(data.toString('utf8'), true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} else {
|
||||||
reject(
|
reject(
|
||||||
new Error(
|
new Error(
|
||||||
`Failed to extract ComfyUI: ${error instanceof Error ? error.message : String(error)}`
|
`PyTorch installation failed with code ${torchCode}`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
});
|
||||||
reject(
|
|
||||||
new Error(
|
torchInstallProcess.on('error', (error) => {
|
||||||
`Failed to extract ComfyUI: unzip exit code ${extractCode}`
|
reject(error);
|
||||||
)
|
});
|
||||||
);
|
|
||||||
|
if (torchInstallProcess.stdout) {
|
||||||
|
torchInstallProcess.stdout.on('data', (data: Buffer) => {
|
||||||
|
sendKoboldOutput(data.toString('utf8'), true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
extractProcess.on('error', (error) => {
|
if (torchInstallProcess.stderr) {
|
||||||
reject(new Error(`Extraction process error: ${error.message}`));
|
torchInstallProcess.stderr.on('data', (data: Buffer) => {
|
||||||
});
|
sendKoboldOutput(data.toString('utf8'), true);
|
||||||
|
});
|
||||||
if (extractProcess.stdout) {
|
}
|
||||||
extractProcess.stdout.on('data', (data: Buffer) => {
|
} catch (error) {
|
||||||
sendKoboldOutput(data.toString('utf8'), true);
|
reject(
|
||||||
});
|
new Error(
|
||||||
}
|
`Failed to extract ComfyUI: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
)
|
||||||
if (extractProcess.stderr) {
|
);
|
||||||
extractProcess.stderr.on('data', (data: Buffer) => {
|
|
||||||
sendKoboldOutput(
|
|
||||||
`Extract stderr: ${data.toString('utf8')}`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(
|
reject(
|
||||||
|
|
@ -477,8 +445,7 @@ export async function startFrontend(args: string[]) {
|
||||||
const comfyUIWorkspace = join(installDir, 'comfyui-workspace');
|
const comfyUIWorkspace = join(installDir, 'comfyui-workspace');
|
||||||
const comfyUIMainPath = join(comfyUIWorkspace, 'main.py');
|
const comfyUIMainPath = join(comfyUIWorkspace, 'main.py');
|
||||||
|
|
||||||
await ensureDir(comfyUIDataDir);
|
await Promise.all([ensureDir(comfyUIDataDir), ensureDir(comfyUIWorkspace)]);
|
||||||
await ensureDir(comfyUIWorkspace);
|
|
||||||
|
|
||||||
const comfyUIArgs = [
|
const comfyUIArgs = [
|
||||||
comfyUIMainPath,
|
comfyUIMainPath,
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,172 @@
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
|
import { access, readdir } from 'fs/promises';
|
||||||
|
import { homedir } from 'os';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
export async function isUvAvailable() {
|
export async function isUvAvailable() {
|
||||||
return new Promise((resolve) => {
|
try {
|
||||||
const uvProcess = spawn('uv', ['--version'], {
|
const env = await getUvEnvironment();
|
||||||
stdio: 'ignore',
|
const testProcess = spawn('uv', ['--version'], { stdio: 'pipe', env });
|
||||||
});
|
|
||||||
|
|
||||||
uvProcess.on('close', (code) => {
|
return new Promise<boolean>((resolve) => {
|
||||||
resolve(code === 0);
|
const timeout = setTimeout(() => {
|
||||||
});
|
testProcess.kill();
|
||||||
|
resolve(false);
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
uvProcess.on('error', () => {
|
testProcess.on('exit', (code) => {
|
||||||
resolve(false);
|
clearTimeout(timeout);
|
||||||
});
|
resolve(code === 0);
|
||||||
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
testProcess.on('error', () => {
|
||||||
uvProcess.kill();
|
clearTimeout(timeout);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
}, 5000);
|
});
|
||||||
});
|
});
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUvEnvironment() {
|
||||||
|
const env = { ...process.env };
|
||||||
|
|
||||||
|
const uvPaths = [
|
||||||
|
join(homedir(), '.cargo', 'bin'),
|
||||||
|
join(homedir(), '.local', 'bin'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const existingPaths: string[] = [];
|
||||||
|
for (const path of uvPaths) {
|
||||||
|
try {
|
||||||
|
await access(path);
|
||||||
|
existingPaths.push(path);
|
||||||
|
} catch {
|
||||||
|
void 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingPaths.length > 0) {
|
||||||
|
const pathSeparator = process.platform === 'win32' ? ';' : ':';
|
||||||
|
env.PATH = `${existingPaths.join(pathSeparator)}${pathSeparator}${env.PATH}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
env.PYTHONIOENCODING = 'utf-8';
|
||||||
|
env.PYTHONLEGACYWINDOWSSTDIO = '1';
|
||||||
|
env.PYTHONUTF8 = '1';
|
||||||
|
env.CHCP = '65001';
|
||||||
|
}
|
||||||
|
|
||||||
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isNpxAvailable() {
|
export async function isNpxAvailable() {
|
||||||
return new Promise((resolve) => {
|
try {
|
||||||
const npxProcess = spawn('npx', ['--version'], {
|
const env = await getNodeEnvironment();
|
||||||
stdio: 'ignore',
|
const testProcess = spawn('npx', ['--version'], {
|
||||||
|
stdio: 'pipe',
|
||||||
|
env,
|
||||||
|
shell: process.platform === 'win32',
|
||||||
});
|
});
|
||||||
|
|
||||||
npxProcess.on('close', (code) => {
|
return new Promise<boolean>((resolve) => {
|
||||||
resolve(code === 0);
|
const timeout = setTimeout(() => {
|
||||||
});
|
testProcess.kill();
|
||||||
|
resolve(false);
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
npxProcess.on('error', () => {
|
testProcess.on('exit', (code) => {
|
||||||
resolve(false);
|
clearTimeout(timeout);
|
||||||
});
|
resolve(code === 0);
|
||||||
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
testProcess.on('error', () => {
|
||||||
npxProcess.kill();
|
clearTimeout(timeout);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
}, 5000);
|
});
|
||||||
});
|
});
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNodeEnvironment() {
|
||||||
|
const env = { ...process.env };
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionManagerPaths = [
|
||||||
|
join(homedir(), '.local', 'share', 'fnm', 'node-versions'),
|
||||||
|
join(homedir(), '.nvm', 'versions', 'node'),
|
||||||
|
join(homedir(), '.volta', 'tools', 'image', 'node'),
|
||||||
|
join(homedir(), '.asdf', 'installs', 'nodejs'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const systemPaths: string[] = [];
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
systemPaths.push('/opt/homebrew/bin', '/usr/local/bin');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const systemPath of systemPaths) {
|
||||||
|
try {
|
||||||
|
await access(systemPath);
|
||||||
|
await tryAddPathToEnv(env, systemPath);
|
||||||
|
return env;
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const versionPath of versionManagerPaths) {
|
||||||
|
if (await tryVersionManagerPath(versionPath, env)) {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryVersionManagerPath(
|
||||||
|
basePath: string,
|
||||||
|
env: Record<string, string | undefined>
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await access(basePath);
|
||||||
|
const versions = await readdir(basePath);
|
||||||
|
if (versions.length > 0) {
|
||||||
|
const latestVersion = versions.sort().pop();
|
||||||
|
if (latestVersion) {
|
||||||
|
const binSubPath = basePath.includes('fnm')
|
||||||
|
? join('installation', 'bin')
|
||||||
|
: 'bin';
|
||||||
|
const nodeBinPath = join(basePath, latestVersion, binSubPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await access(nodeBinPath);
|
||||||
|
return tryAddPathToEnv(env, nodeBinPath);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryAddPathToEnv(
|
||||||
|
env: Record<string, string | undefined>,
|
||||||
|
path: string
|
||||||
|
) {
|
||||||
|
const pathSeparator = process.platform === 'win32' ? ';' : ':';
|
||||||
|
if (!env.PATH?.includes(path)) {
|
||||||
|
env.PATH = `${path}${pathSeparator}${env.PATH}`;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import type { ChildProcess } from 'child_process';
|
import type { ChildProcess } from 'child_process';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { homedir } from 'os';
|
|
||||||
import { access } from 'fs/promises';
|
|
||||||
|
|
||||||
import { logError } from './logging';
|
import { logError } from './logging';
|
||||||
import { sendKoboldOutput } from './window';
|
import { sendKoboldOutput } from './window';
|
||||||
|
|
@ -11,6 +9,7 @@ import { OPENWEBUI, SERVER_READY_SIGNALS } from '@/constants';
|
||||||
import { terminateProcess } from '@/utils/node/process';
|
import { terminateProcess } from '@/utils/node/process';
|
||||||
import { parseKoboldConfig } from '@/utils/node/kobold';
|
import { parseKoboldConfig } from '@/utils/node/kobold';
|
||||||
import { getAppVersion } from '@/utils/node/fs';
|
import { getAppVersion } from '@/utils/node/fs';
|
||||||
|
import { getUvEnvironment } from './dependencies';
|
||||||
|
|
||||||
let openWebUIProcess: ChildProcess | null = null;
|
let openWebUIProcess: ChildProcess | null = null;
|
||||||
|
|
||||||
|
|
@ -24,48 +23,10 @@ process.on('SIGTERM', () => {
|
||||||
void cleanup();
|
void cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getUvEnvironment() {
|
|
||||||
const env = { ...process.env };
|
|
||||||
|
|
||||||
const uvPaths = [
|
|
||||||
join(homedir(), '.cargo', 'bin'),
|
|
||||||
join(homedir(), '.local', 'bin'),
|
|
||||||
];
|
|
||||||
|
|
||||||
const existingPaths: string[] = [];
|
|
||||||
for (const path of uvPaths) {
|
|
||||||
try {
|
|
||||||
await access(path);
|
|
||||||
existingPaths.push(path);
|
|
||||||
} catch {
|
|
||||||
void 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingPaths.length > 0) {
|
|
||||||
const pathSeparator = process.platform === 'win32' ? ';' : ':';
|
|
||||||
env.PATH = `${existingPaths.join(pathSeparator)}${pathSeparator}${env.PATH}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
env.PYTHONIOENCODING = 'utf-8';
|
|
||||||
env.PYTHONLEGACYWINDOWSSTDIO = '1';
|
|
||||||
env.PYTHONUTF8 = '1';
|
|
||||||
env.CHCP = '65001';
|
|
||||||
}
|
|
||||||
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createUvProcess(args: string[], env?: Record<string, string>) {
|
async function createUvProcess(args: string[], env?: Record<string, string>) {
|
||||||
const uvEnv = await getUvEnvironment();
|
const uvEnv = await getUvEnvironment();
|
||||||
const mergedEnv = { ...uvEnv, ...env };
|
const mergedEnv = { ...uvEnv, ...env };
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
mergedEnv.PYTHONIOENCODING = 'utf-8';
|
|
||||||
mergedEnv.PYTHONUTF8 = '1';
|
|
||||||
}
|
|
||||||
|
|
||||||
return spawn('uvx', args, {
|
return spawn('uvx', args, {
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
detached: false,
|
detached: false,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { spawn } from 'child_process';
|
||||||
import { createServer, request, type Server } from 'http';
|
import { createServer, request, type Server } from 'http';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { access, readdir } from 'fs/promises';
|
|
||||||
import type { ChildProcess } from 'child_process';
|
import type { ChildProcess } from 'child_process';
|
||||||
|
|
||||||
import { logError } from './logging';
|
import { logError } from './logging';
|
||||||
|
|
@ -11,6 +10,7 @@ import { SILLYTAVERN, SERVER_READY_SIGNALS } from '@/constants';
|
||||||
import { terminateProcess } from '@/utils/node/process';
|
import { terminateProcess } from '@/utils/node/process';
|
||||||
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs';
|
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs';
|
||||||
import { parseKoboldConfig } from '@/utils/node/kobold';
|
import { parseKoboldConfig } from '@/utils/node/kobold';
|
||||||
|
import { getNodeEnvironment } from './dependencies';
|
||||||
|
|
||||||
let sillyTavernProcess: ChildProcess | null = null;
|
let sillyTavernProcess: ChildProcess | null = null;
|
||||||
let proxyServer: Server | null = null;
|
let proxyServer: Server | null = null;
|
||||||
|
|
@ -69,85 +69,6 @@ async function getSillyTavernSettingsPath() {
|
||||||
return join(dataRoot, 'default-user', 'settings.json');
|
return join(dataRoot, 'default-user', 'settings.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tryAddPathToEnv(
|
|
||||||
env: Record<string, string | undefined>,
|
|
||||||
path: string
|
|
||||||
) {
|
|
||||||
const pathSeparator = process.platform === 'win32' ? ';' : ':';
|
|
||||||
if (!env.PATH?.includes(path)) {
|
|
||||||
env.PATH = `${path}${pathSeparator}${env.PATH}`;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function tryVersionManagerPath(
|
|
||||||
basePath: string,
|
|
||||||
env: Record<string, string | undefined>
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
await access(basePath);
|
|
||||||
const versions = await readdir(basePath);
|
|
||||||
if (versions.length > 0) {
|
|
||||||
const latestVersion = versions.sort().pop();
|
|
||||||
if (latestVersion) {
|
|
||||||
const binSubPath = basePath.includes('fnm')
|
|
||||||
? join('installation', 'bin')
|
|
||||||
: 'bin';
|
|
||||||
const nodeBinPath = join(basePath, latestVersion, binSubPath);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await access(nodeBinPath);
|
|
||||||
return tryAddPathToEnv(env, nodeBinPath);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getNodeEnvironment() {
|
|
||||||
const env = { ...process.env };
|
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
|
|
||||||
const versionManagerPaths = [
|
|
||||||
join(homedir(), '.local', 'share', 'fnm', 'node-versions'),
|
|
||||||
join(homedir(), '.nvm', 'versions', 'node'),
|
|
||||||
join(homedir(), '.volta', 'tools', 'image', 'node'),
|
|
||||||
join(homedir(), '.asdf', 'installs', 'nodejs'),
|
|
||||||
];
|
|
||||||
|
|
||||||
const systemPaths: string[] = [];
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
systemPaths.push('/opt/homebrew/bin', '/usr/local/bin');
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const systemPath of systemPaths) {
|
|
||||||
try {
|
|
||||||
await access(systemPath);
|
|
||||||
await tryAddPathToEnv(env, systemPath);
|
|
||||||
return env;
|
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const versionPath of versionManagerPaths) {
|
|
||||||
if (await tryVersionManagerPath(versionPath, env)) {
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createNpxProcess(args: string[]) {
|
async function createNpxProcess(args: string[]) {
|
||||||
const env = await getNodeEnvironment();
|
const env = await getNodeEnvironment();
|
||||||
return spawn('npx', args, {
|
return spawn('npx', args, {
|
||||||
|
|
|
||||||
14
yarn.lock
14
yarn.lock
|
|
@ -1349,7 +1349,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/yauzl@npm:^2.9.1":
|
"@types/yauzl@npm:^2.10.3, @types/yauzl@npm:^2.9.1":
|
||||||
version: 2.10.3
|
version: 2.10.3
|
||||||
resolution: "@types/yauzl@npm:2.10.3"
|
resolution: "@types/yauzl@npm:2.10.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -3643,6 +3643,7 @@ __metadata:
|
||||||
"@types/node": "npm:^24.3.3"
|
"@types/node": "npm:^24.3.3"
|
||||||
"@types/react": "npm:^19.1.13"
|
"@types/react": "npm:^19.1.13"
|
||||||
"@types/react-dom": "npm:^19.1.9"
|
"@types/react-dom": "npm:^19.1.9"
|
||||||
|
"@types/yauzl": "npm:^2.10.3"
|
||||||
"@typescript-eslint/eslint-plugin": "npm:^8.43.0"
|
"@typescript-eslint/eslint-plugin": "npm:^8.43.0"
|
||||||
"@typescript-eslint/parser": "npm:^8.43.0"
|
"@typescript-eslint/parser": "npm:^8.43.0"
|
||||||
"@vitejs/plugin-react": "npm:^5.0.2"
|
"@vitejs/plugin-react": "npm:^5.0.2"
|
||||||
|
|
@ -3672,6 +3673,7 @@ __metadata:
|
||||||
vite: "npm:^7.1.5"
|
vite: "npm:^7.1.5"
|
||||||
winston: "npm:^3.17.0"
|
winston: "npm:^3.17.0"
|
||||||
winston-daily-rotate-file: "npm:^5.0.0"
|
winston-daily-rotate-file: "npm:^5.0.0"
|
||||||
|
yauzl: "npm:^3.2.0"
|
||||||
zustand: "npm:^5.0.8"
|
zustand: "npm:^5.0.8"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
@ -7424,6 +7426,16 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"yauzl@npm:^3.2.0":
|
||||||
|
version: 3.2.0
|
||||||
|
resolution: "yauzl@npm:3.2.0"
|
||||||
|
dependencies:
|
||||||
|
buffer-crc32: "npm:~0.2.3"
|
||||||
|
pend: "npm:~1.2.0"
|
||||||
|
checksum: 10c0/7b40b3dc46b95761a2a764391d257a11f494d365875af73a1b48fe16d4bd103dd178612e60168d12a0e59a8ba4f6411a15a5e8871d5a5f78255d6cc1ce39ee62
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"yocto-queue@npm:^0.1.0":
|
"yocto-queue@npm:^0.1.0":
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
resolution: "yocto-queue@npm:0.1.0"
|
resolution: "yocto-queue@npm:0.1.0"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue