Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "DeepChat",
"version": "0.4.8",
"version": "0.4.9",
"description": "DeepChat,一个简单易用的AI客户端",
"main": "./out/main/index.js",
"author": "ThinkInAIXYZ",
Expand Down Expand Up @@ -43,18 +43,18 @@
"build:linux:x64": "pnpm run build && electron-builder --linux --x64",
"build:linux:arm64": "pnpm run build && electron-builder --linux --arm64",
"afterSign": "scripts/notarize.js",
"installRuntime": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.8.8 && npx -y tiny-runtime-injector --type bun --dir ./runtime/bun --runtime-version v1.2.20",
"installRuntime": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.8.8 && npx -y tiny-runtime-injector --type node --dir ./runtime/node",
"installRuntime:win:x64": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.8.8 -a x64 -p win32 && npx -y tiny-runtime-injector --type node --dir ./runtime/node -a x64 -p win32",
"installRuntime:win:arm64": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.8.8 -a arm64 -p win32 && npx -y tiny-runtime-injector --type node --dir ./runtime/node -a arm64 -p win32",
"installRuntime:mac:arm64": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.8.8 -a arm64 -p darwin && npx -y tiny-runtime-injector --type bun --runtime-version v1.2.20 --dir ./runtime/bun -a arm64 -p darwin",
"installRuntime:mac:x64": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.8.8 -a x64 -p darwin && npx -y tiny-runtime-injector --type bun --runtime-version v1.2.20 --dir ./runtime/bun -a x64 -p darwin",
"installRuntime:linux:x64": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.8.8 -a x64 -p linux && npx -y tiny-runtime-injector --type bun --runtime-version v1.2.20 --dir ./runtime/bun -a x64 -p linux",
"installRuntime:linux:arm64": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.8.8 -a arm64 -p linux && npx -y tiny-runtime-injector --type bun --runtime-version v1.2.20 --dir ./runtime/bun -a arm64 -p linux",
"installRuntime:mac:arm64": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.8.8 -a arm64 -p darwin && npx -y tiny-runtime-injector --type node --dir ./runtime/node -a arm64 -p darwin",
"installRuntime:mac:x64": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.8.8 -a x64 -p darwin && npx -y tiny-runtime-injector --type node --dir ./runtime/node -a x64 -p darwin",
"installRuntime:linux:x64": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.8.8 -a x64 -p linux && npx -y tiny-runtime-injector --type node --dir ./runtime/node -a x64 -p linux",
"installRuntime:linux:arm64": "npx -y tiny-runtime-injector --type uv --dir ./runtime/uv --runtime-version 0.8.8 -a arm64 -p linux && npx -y tiny-runtime-injector --type node --dir ./runtime/node -a arm64 -p linux",
"installRuntime:duckdb:vss": "node scripts/installVss.js",
"i18n": "i18n-check -s zh-CN -f i18next --locales src/renderer/src/i18n",
"i18n:en": "i18n-check -s en-US -f i18next --locales src/renderer/src/i18n",
"i18n:types": "node scripts/generate-i18n-types.js",
"cleanRuntime": "rm -rf runtime/uv runtime/bun runtime/node",
"cleanRuntime": "rm -rf runtime/uv runtime/node",
"update-shadcn": "node scripts/update-shadcn.js"
},
"dependencies": {
Expand Down Expand Up @@ -86,6 +86,7 @@
"jsonrepair": "^3.13.1",
"mammoth": "^1.11.0",
"nanoid": "^5.1.6",
"node-pty": "^1.0.0",
"ollama": "^0.5.18",
"openai": "^5.23.2",
"pdf-parse-new": "^1.4.1",
Expand Down Expand Up @@ -175,7 +176,9 @@
"vue-virtual-scroller": "^2.0.0-beta.8",
"vuedraggable": "^4.1.0",
"yaml": "^2.8.1",
"zod-to-json-schema": "^3.24.6"
"zod-to-json-schema": "^3.24.6",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged && pnpm typecheck",
Expand Down
322 changes: 322 additions & 0 deletions src/main/lib/runtimeHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
import { app } from 'electron'
import * as fs from 'fs'
import * as path from 'path'

/**
* RuntimeHelper - Utility class for managing runtime paths and environment variables
* Uses singleton pattern to cache runtime paths and avoid repeated filesystem checks
*/
export class RuntimeHelper {
private static instance: RuntimeHelper | null = null
private nodeRuntimePath: string | null = null
private uvRuntimePath: string | null = null
private runtimesInitialized: boolean = false

private constructor() {
// Private constructor to enforce singleton pattern
}

/**
* Get the singleton instance of RuntimeHelper
*/
public static getInstance(): RuntimeHelper {
if (!RuntimeHelper.instance) {
RuntimeHelper.instance = new RuntimeHelper()
}
return RuntimeHelper.instance
}

/**
* Initialize runtime paths (idempotent operation)
* Caches Node.js and UV runtime paths to avoid repeated filesystem checks
*/
public initializeRuntimes(): void {
if (this.runtimesInitialized) {
return
}

const runtimeBasePath = path
.join(app.getAppPath(), 'runtime')
.replace('app.asar', 'app.asar.unpacked')

// Check if node runtime file exists
const nodeRuntimePath = path.join(runtimeBasePath, 'node')
if (process.platform === 'win32') {
const nodeExe = path.join(nodeRuntimePath, 'node.exe')
if (fs.existsSync(nodeExe)) {
this.nodeRuntimePath = nodeRuntimePath
} else {
this.nodeRuntimePath = null
}
} else {
const nodeBin = path.join(nodeRuntimePath, 'bin', 'node')
if (fs.existsSync(nodeBin)) {
this.nodeRuntimePath = nodeRuntimePath
} else {
this.nodeRuntimePath = null
}
}

// Check if uv runtime file exists
const uvRuntimePath = path.join(runtimeBasePath, 'uv')
if (process.platform === 'win32') {
const uvExe = path.join(uvRuntimePath, 'uv.exe')
const uvxExe = path.join(uvRuntimePath, 'uvx.exe')
if (fs.existsSync(uvExe) && fs.existsSync(uvxExe)) {
this.uvRuntimePath = uvRuntimePath
} else {
this.uvRuntimePath = null
}
} else {
const uvBin = path.join(uvRuntimePath, 'uv')
const uvxBin = path.join(uvRuntimePath, 'uvx')
if (fs.existsSync(uvBin) && fs.existsSync(uvxBin)) {
this.uvRuntimePath = uvRuntimePath
} else {
this.uvRuntimePath = null
}
}

this.runtimesInitialized = true
}

/**
* Get Node.js runtime path
* @returns Node.js runtime path or null if not found
*/
public getNodeRuntimePath(): string | null {
return this.nodeRuntimePath
}

/**
* Get UV runtime path
* @returns UV runtime path or null if not found
*/
public getUvRuntimePath(): string | null {
return this.uvRuntimePath
}

/**
* Replace command with runtime version if needed
* @param command Original command
* @param useBuiltinRuntime Whether to use builtin runtime
* @param checkExists Whether to check if file exists (default: true)
* @returns Processed command path or original command
*/
public replaceWithRuntimeCommand(
command: string,
useBuiltinRuntime: boolean,
checkExists: boolean = true
): string {
// If useBuiltinRuntime is false, return original command
if (!useBuiltinRuntime) {
return command
}

// Get command basename (remove path)
const basename = path.basename(command)

// Handle Node.js related commands (all platforms use same logic)
if (['node', 'npm', 'npx'].includes(basename)) {
if (this.nodeRuntimePath) {
if (process.platform === 'win32') {
if (basename === 'node') {
const nodeExe = path.join(this.nodeRuntimePath, 'node.exe')
if (checkExists) {
if (fs.existsSync(nodeExe)) {
return nodeExe
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return nodeExe
}
} else if (basename === 'npm') {
// Windows usually has npm as .cmd file
const npmCmd = path.join(this.nodeRuntimePath, 'npm.cmd')
if (checkExists) {
if (fs.existsSync(npmCmd)) {
return npmCmd
}
// Check if npm exists without .cmd extension
const npmPath = path.join(this.nodeRuntimePath, 'npm')
if (fs.existsSync(npmPath)) {
return npmPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
// For mcpClient: return default path without checking
if (fs.existsSync(npmCmd)) {
return npmCmd
}
return path.join(this.nodeRuntimePath, 'npm')
}
} else if (basename === 'npx') {
// On Windows, npx is typically a .cmd file
const npxCmd = path.join(this.nodeRuntimePath, 'npx.cmd')
if (checkExists) {
if (fs.existsSync(npxCmd)) {
return npxCmd
}
// Check if npx exists without .cmd extension
const npxPath = path.join(this.nodeRuntimePath, 'npx')
if (fs.existsSync(npxPath)) {
return npxPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
// For mcpClient: return default path without checking
if (fs.existsSync(npxCmd)) {
return npxCmd
}
return path.join(this.nodeRuntimePath, 'npx')
}
}
} else {
// Non-Windows platforms
let targetCommand: string
if (basename === 'node') {
targetCommand = 'node'
} else if (basename === 'npm') {
targetCommand = 'npm'
} else if (basename === 'npx') {
targetCommand = 'npx'
} else {
targetCommand = basename
}
const nodePath = path.join(this.nodeRuntimePath, 'bin', targetCommand)
if (checkExists) {
if (fs.existsSync(nodePath)) {
return nodePath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return nodePath
}
}
}
}

// UV command handling (all platforms)
if (['uv', 'uvx'].includes(basename)) {
if (!this.uvRuntimePath) {
return command
}

// Both uv and uvx use their corresponding commands
const targetCommand = basename === 'uvx' ? 'uvx' : 'uv'

if (process.platform === 'win32') {
const uvPath = path.join(this.uvRuntimePath, `${targetCommand}.exe`)
if (checkExists) {
if (fs.existsSync(uvPath)) {
return uvPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return uvPath
}
} else {
const uvPath = path.join(this.uvRuntimePath, targetCommand)
if (checkExists) {
if (fs.existsSync(uvPath)) {
return uvPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return uvPath
}
}
}

return command
}
Comment on lines +106 to +238
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent checkExists behavior for npm/npx on Windows.

When checkExists=false, lines 150-154 (npm) and 171-174 (npx) still call fs.existsSync() to decide between .cmd and non-.cmd versions. This contradicts the parameter name and the comment "return default path without checking."

For consistency, when checkExists=false, return the expected default path without any filesystem checks:

           } else {
-            // For mcpClient: return default path without checking
-            if (fs.existsSync(npmCmd)) {
-              return npmCmd
-            }
-            return path.join(this.nodeRuntimePath, 'npm')
+            // For mcpClient: return default .cmd path without checking
+            return npmCmd
           }

Apply the same fix for npx at lines 171-174.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public replaceWithRuntimeCommand(
command: string,
useBuiltinRuntime: boolean,
checkExists: boolean = true
): string {
// If useBuiltinRuntime is false, return original command
if (!useBuiltinRuntime) {
return command
}
// Get command basename (remove path)
const basename = path.basename(command)
// Handle Node.js related commands (all platforms use same logic)
if (['node', 'npm', 'npx'].includes(basename)) {
if (this.nodeRuntimePath) {
if (process.platform === 'win32') {
if (basename === 'node') {
const nodeExe = path.join(this.nodeRuntimePath, 'node.exe')
if (checkExists) {
if (fs.existsSync(nodeExe)) {
return nodeExe
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return nodeExe
}
} else if (basename === 'npm') {
// Windows usually has npm as .cmd file
const npmCmd = path.join(this.nodeRuntimePath, 'npm.cmd')
if (checkExists) {
if (fs.existsSync(npmCmd)) {
return npmCmd
}
// Check if npm exists without .cmd extension
const npmPath = path.join(this.nodeRuntimePath, 'npm')
if (fs.existsSync(npmPath)) {
return npmPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
// For mcpClient: return default path without checking
if (fs.existsSync(npmCmd)) {
return npmCmd
}
return path.join(this.nodeRuntimePath, 'npm')
}
} else if (basename === 'npx') {
// On Windows, npx is typically a .cmd file
const npxCmd = path.join(this.nodeRuntimePath, 'npx.cmd')
if (checkExists) {
if (fs.existsSync(npxCmd)) {
return npxCmd
}
// Check if npx exists without .cmd extension
const npxPath = path.join(this.nodeRuntimePath, 'npx')
if (fs.existsSync(npxPath)) {
return npxPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
// For mcpClient: return default path without checking
if (fs.existsSync(npxCmd)) {
return npxCmd
}
return path.join(this.nodeRuntimePath, 'npx')
}
}
} else {
// Non-Windows platforms
let targetCommand: string
if (basename === 'node') {
targetCommand = 'node'
} else if (basename === 'npm') {
targetCommand = 'npm'
} else if (basename === 'npx') {
targetCommand = 'npx'
} else {
targetCommand = basename
}
const nodePath = path.join(this.nodeRuntimePath, 'bin', targetCommand)
if (checkExists) {
if (fs.existsSync(nodePath)) {
return nodePath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return nodePath
}
}
}
}
// UV command handling (all platforms)
if (['uv', 'uvx'].includes(basename)) {
if (!this.uvRuntimePath) {
return command
}
// Both uv and uvx use their corresponding commands
const targetCommand = basename === 'uvx' ? 'uvx' : 'uv'
if (process.platform === 'win32') {
const uvPath = path.join(this.uvRuntimePath, `${targetCommand}.exe`)
if (checkExists) {
if (fs.existsSync(uvPath)) {
return uvPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return uvPath
}
} else {
const uvPath = path.join(this.uvRuntimePath, targetCommand)
if (checkExists) {
if (fs.existsSync(uvPath)) {
return uvPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return uvPath
}
}
}
return command
}
public replaceWithRuntimeCommand(
command: string,
useBuiltinRuntime: boolean,
checkExists: boolean = true
): string {
// If useBuiltinRuntime is false, return original command
if (!useBuiltinRuntime) {
return command
}
// Get command basename (remove path)
const basename = path.basename(command)
// Handle Node.js related commands (all platforms use same logic)
if (['node', 'npm', 'npx'].includes(basename)) {
if (this.nodeRuntimePath) {
if (process.platform === 'win32') {
if (basename === 'node') {
const nodeExe = path.join(this.nodeRuntimePath, 'node.exe')
if (checkExists) {
if (fs.existsSync(nodeExe)) {
return nodeExe
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return nodeExe
}
} else if (basename === 'npm') {
// Windows usually has npm as .cmd file
const npmCmd = path.join(this.nodeRuntimePath, 'npm.cmd')
if (checkExists) {
if (fs.existsSync(npmCmd)) {
return npmCmd
}
// Check if npm exists without .cmd extension
const npmPath = path.join(this.nodeRuntimePath, 'npm')
if (fs.existsSync(npmPath)) {
return npmPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
// For mcpClient: return default .cmd path without checking
return npmCmd
}
} else if (basename === 'npx') {
// On Windows, npx is typically a .cmd file
const npxCmd = path.join(this.nodeRuntimePath, 'npx.cmd')
if (checkExists) {
if (fs.existsSync(npxCmd)) {
return npxCmd
}
// Check if npx exists without .cmd extension
const npxPath = path.join(this.nodeRuntimePath, 'npx')
if (fs.existsSync(npxPath)) {
return npxPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
// For mcpClient: return default .cmd path without checking
return npxCmd
}
}
} else {
// Non-Windows platforms
let targetCommand: string
if (basename === 'node') {
targetCommand = 'node'
} else if (basename === 'npm') {
targetCommand = 'npm'
} else if (basename === 'npx') {
targetCommand = 'npx'
} else {
targetCommand = basename
}
const nodePath = path.join(this.nodeRuntimePath, 'bin', targetCommand)
if (checkExists) {
if (fs.existsSync(nodePath)) {
return nodePath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return nodePath
}
}
}
}
// UV command handling (all platforms)
if (['uv', 'uvx'].includes(basename)) {
if (!this.uvRuntimePath) {
return command
}
// Both uv and uvx use their corresponding commands
const targetCommand = basename === 'uvx' ? 'uvx' : 'uv'
if (process.platform === 'win32') {
const uvPath = path.join(this.uvRuntimePath, `${targetCommand}.exe`)
if (checkExists) {
if (fs.existsSync(uvPath)) {
return uvPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return uvPath
}
} else {
const uvPath = path.join(this.uvRuntimePath, targetCommand)
if (checkExists) {
if (fs.existsSync(uvPath)) {
return uvPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return uvPath
}
}
}
return command
}
🤖 Prompt for AI Agents
src/main/lib/runtimeHelper.ts lines 106-238: the Windows branches for npm (lines
~150-154) and npx (lines ~171-174) still call fs.existsSync() when checkExists
is false, contradicting the parameter and comment; change those branches to stop
checking the filesystem and unconditionally return the default .cmd path
(npm.cmd / npx.cmd) when checkExists is false, mirroring the node and UV
handling and ensuring no filesystem checks occur in the non-checking code path.


/**
* Process command and arguments with runtime replacement (for mcpClient)
* This method does not check file existence and always tries to replace
* @param command Original command
* @param args Command arguments
* @returns Processed command and arguments
*/
public processCommandWithArgs(
command: string,
args: string[]
): { command: string; args: string[] } {
return {
command: this.replaceWithRuntimeCommand(command, true, false),
args: args.map((arg) => this.replaceWithRuntimeCommand(arg, true, false))
}
}

/**
* Expand various symbols and variables in paths
* @param inputPath Input path that may contain ~ or environment variables
* @returns Expanded path
*/
public expandPath(inputPath: string): string {
let expandedPath = inputPath

// Handle ~ symbol (user home directory)
if (expandedPath.startsWith('~/') || expandedPath === '~') {
const homeDir = app.getPath('home')
expandedPath = expandedPath.replace('~', homeDir)
}

// Handle environment variable expansion
expandedPath = expandedPath.replace(/\$\{([^}]+)\}/g, (match, varName) => {
return process.env[varName] || match
})

// Handle simple $VAR format (without braces)
expandedPath = expandedPath.replace(/\$([A-Z_][A-Z0-9_]*)/g, (match, varName) => {
return process.env[varName] || match
})

return expandedPath
}

/**
* Normalize PATH environment variable
* @param paths Array of paths to merge
* @returns Normalized PATH key-value pair
*/
public normalizePathEnv(paths: string[]): { key: string; value: string } {
const isWindows = process.platform === 'win32'
const separator = isWindows ? ';' : ':'
const pathKey = isWindows ? 'Path' : 'PATH'
const pathValue = paths.filter(Boolean).join(separator)
return { key: pathKey, value: pathValue }
}

/**
* Get system-specific default paths
* @param homeDir User home directory
* @returns Array of default system paths
*/
public getDefaultPaths(homeDir: string): string[] {
if (process.platform === 'darwin') {
return [
'/bin',
'/usr/bin',
'/usr/local/bin',
'/usr/local/sbin',
'/opt/homebrew/bin',
'/opt/homebrew/sbin',
'/usr/local/opt/node/bin',
'/opt/local/bin',
`${homeDir}/.cargo/bin`
]
} else if (process.platform === 'linux') {
return ['/bin', '/usr/bin', '/usr/local/bin', `${homeDir}/.cargo/bin`]
} else {
// Windows
return [`${homeDir}\\.cargo\\bin`, `${homeDir}\\.local\\bin`]
}
}
}
Loading