fixing regressions from the previous commit

This commit is contained in:
Egor 2025-09-13 22:58:12 -07:00
parent e1b078b9d9
commit 329e191a49
8 changed files with 375 additions and 364 deletions

View file

@ -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',
}, },
}, },

View file

@ -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": {

View file

@ -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 = {

View file

@ -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,

View file

@ -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;
} }

View file

@ -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,

View file

@ -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, {

View file

@ -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"